Merge a831874e60 into 0de73e8971
				
					
				
			This commit is contained in:
		
						commit
						165a5aea5b
					
				
							
								
								
									
										87
									
								
								src/tools/json-viewer/json-viewer.e2e.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								src/tools/json-viewer/json-viewer.e2e.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,87 @@ | ||||
| import { expect, test } from '@playwright/test'; | ||||
| 
 | ||||
| test.describe('Tool - JSON prettify and format', () => { | ||||
|   test.beforeEach(async ({ page }) => { | ||||
|     await page.goto('/json-prettify'); | ||||
|   }); | ||||
| 
 | ||||
|   test('Has correct title', async ({ page }) => { | ||||
|     await expect(page).toHaveTitle('JSON prettify and format - IT Tools'); | ||||
|   }); | ||||
| 
 | ||||
|   test('prettifies and formats valid JSON', async ({ page }) => { | ||||
|     await page.getByTestId('json-prettify-input').fill('{"b":2,"a":1,"c":{"z":3,"y":2}}'); | ||||
| 
 | ||||
|     const prettifiedJson = await page.getByTestId('area-content').innerText(); | ||||
| 
 | ||||
|     expect(prettifiedJson.trim()).toContain('"a": 1'); | ||||
|     expect(prettifiedJson.trim()).toContain('"b": 2'); | ||||
|     // Keys should be sorted alphabetically
 | ||||
|     expect(prettifiedJson.indexOf('"a"')).toBeLessThan(prettifiedJson.indexOf('"b"')); | ||||
|   }); | ||||
| 
 | ||||
|   test('handles sort keys toggle', async ({ page }) => { | ||||
|     await page.getByTestId('json-prettify-input').fill('{"b":2,"a":1}'); | ||||
| 
 | ||||
|     // Disable sort keys
 | ||||
|     await page.locator('label:has-text("Sort keys")').locator('input[type="checkbox"]').click(); | ||||
| 
 | ||||
|     const unsortedJson = await page.getByTestId('area-content').innerText(); | ||||
| 
 | ||||
|     // Keys should maintain original order when sorting is disabled
 | ||||
|     expect(unsortedJson.indexOf('"b"')).toBeLessThan(unsortedJson.indexOf('"a"')); | ||||
|   }); | ||||
| 
 | ||||
|   test('handles custom indent size', async ({ page }) => { | ||||
|     await page.getByTestId('json-prettify-input').fill('{"a":1}'); | ||||
| 
 | ||||
|     // Change indent size to 2
 | ||||
|     await page.locator('label:has-text("Indent size")').locator('input[type="number"]').fill('2'); | ||||
| 
 | ||||
|     const formattedJson = await page.getByTestId('area-content').innerText(); | ||||
| 
 | ||||
|     // Should use 2-space indentation
 | ||||
|     expect(formattedJson).toContain('  "a": 1'); | ||||
|   }); | ||||
| 
 | ||||
|   test('auto-unescape functionality works with escaped JSON', async ({ page }) => { | ||||
|     const escapedJson = '"{\\\"id\\\":\\\"123\\\",\\\"name\\\":\\\"test\\\"}"'; | ||||
| 
 | ||||
|     await page.getByTestId('json-prettify-input').fill(escapedJson); | ||||
| 
 | ||||
|     // Enable auto-unescape
 | ||||
|     await page.locator('label:has-text("Auto-unescape")').locator('input[type="checkbox"]').click(); | ||||
| 
 | ||||
|     const unescapedJson = await page.getByTestId('area-content').innerText(); | ||||
| 
 | ||||
|     expect(unescapedJson).toContain('"id": "123"'); | ||||
|     expect(unescapedJson).toContain('"name": "test"'); | ||||
|     expect(unescapedJson).not.toContain('\\"'); | ||||
|   }); | ||||
| 
 | ||||
|   test('auto-unescape toggle affects validation', async ({ page }) => { | ||||
|     const escapedJson = '"{\\\"valid\\\":\\\"json\\\"}"'; | ||||
| 
 | ||||
|     // First, paste escaped JSON without auto-unescape (should show validation error)
 | ||||
|     await page.getByTestId('json-prettify-input').fill(escapedJson); | ||||
| 
 | ||||
|     // Should show validation error
 | ||||
|     await expect(page.locator('text=Provided JSON is not valid.')).toBeVisible(); | ||||
| 
 | ||||
|     // Enable auto-unescape
 | ||||
|     await page.locator('label:has-text("Auto-unescape")').locator('input[type="checkbox"]').click(); | ||||
| 
 | ||||
|     // Validation error should disappear
 | ||||
|     await expect(page.locator('text=Provided JSON is not valid.')).not.toBeVisible(); | ||||
| 
 | ||||
|     // Output should be properly formatted
 | ||||
|     const formattedJson = await page.getByTestId('area-content').innerText(); | ||||
|     expect(formattedJson).toContain('"valid": "json"'); | ||||
|   }); | ||||
| 
 | ||||
|   test('displays helpful placeholder text', async ({ page }) => { | ||||
|     const textarea = page.getByTestId('json-prettify-input'); | ||||
| 
 | ||||
|     await expect(textarea).toHaveAttribute('placeholder', /auto-unescape.*escaped json/i); | ||||
|   }); | ||||
| }); | ||||
| @ -11,25 +11,61 @@ const inputElement = ref<HTMLElement>(); | ||||
| const rawJson = useStorage('json-prettify:raw-json', '{"hello": "world", "foo": "bar"}'); | ||||
| const indentSize = useStorage('json-prettify:indent-size', 3); | ||||
| const sortKeys = useStorage('json-prettify:sort-keys', true); | ||||
| const cleanJson = computed(() => withDefaultOnError(() => formatJson({ rawJson, indentSize, sortKeys }), '')); | ||||
| const autoUnescape = useStorage('json-prettify:auto-unescape', false); | ||||
| const cleanJson = computed(() => withDefaultOnError(() => formatJson({ rawJson, indentSize, sortKeys, autoUnescape }), '')); | ||||
| 
 | ||||
| const rawJsonValidation = useValidation({ | ||||
|   source: rawJson, | ||||
|   rules: [ | ||||
|     { | ||||
|       validator: v => v === '' || JSON5.parse(v), | ||||
|       validator: (v: string) => { | ||||
|         if (v === '') { | ||||
|           return true; | ||||
|         } | ||||
|         try { | ||||
|           let jsonString = v; | ||||
|           if (autoUnescape.value) { | ||||
|             // Apply the same unescaping logic for validation | ||||
|             jsonString = jsonString.trim(); | ||||
| 
 | ||||
|             if ((jsonString.startsWith('"') && jsonString.endsWith('"')) | ||||
|                 || (jsonString.startsWith('\'') && jsonString.endsWith('\''))) { | ||||
|               jsonString = jsonString.slice(1, -1); | ||||
|             } | ||||
| 
 | ||||
|             jsonString = jsonString | ||||
|               .replace(/\\"/g, '"') | ||||
|               .replace(/\\\\/g, '\\') | ||||
|               .replace(/\\n/g, '\n') | ||||
|               .replace(/\\r/g, '\r') | ||||
|               .replace(/\\t/g, '\t') | ||||
|               .replace(/\\f/g, '\f') | ||||
|               .replace(/\\b/g, '\b') | ||||
|               .replace(/\\\//g, '/'); | ||||
|           } | ||||
|           JSON5.parse(jsonString); | ||||
|           return true; | ||||
|         } | ||||
|         catch { | ||||
|           return false; | ||||
|         } | ||||
|       }, | ||||
|       message: 'Provided JSON is not valid.', | ||||
|     }, | ||||
|   ], | ||||
|   watch: [autoUnescape], | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <div style="flex: 0 0 100%"> | ||||
|     <div style="margin: 0 auto; max-width: 600px" flex justify-center gap-3> | ||||
|     <div style="margin: 0 auto; max-width: 700px" flex flex-wrap justify-center gap-3> | ||||
|       <n-form-item label="Sort keys :" label-placement="left" label-width="100"> | ||||
|         <n-switch v-model:value="sortKeys" /> | ||||
|       </n-form-item> | ||||
|       <n-form-item label="Auto-unescape :" label-placement="left" label-width="130"> | ||||
|         <n-switch v-model:value="autoUnescape" /> | ||||
|       </n-form-item> | ||||
|       <n-form-item label="Indent size :" label-placement="left" label-width="100" :show-feedback="false"> | ||||
|         <n-input-number v-model:value="indentSize" min="0" max="10" style="width: 100px" /> | ||||
|       </n-form-item> | ||||
| @ -44,7 +80,7 @@ const rawJsonValidation = useValidation({ | ||||
|     <c-input-text | ||||
|       ref="inputElement" | ||||
|       v-model:value="rawJson" | ||||
|       placeholder="Paste your raw JSON here..." | ||||
|       placeholder="Paste your raw JSON here... Enable 'Auto-unescape' for escaped JSON strings" | ||||
|       rows="20" | ||||
|       multiline | ||||
|       autocomplete="off" | ||||
| @ -52,6 +88,7 @@ const rawJsonValidation = useValidation({ | ||||
|       autocapitalize="off" | ||||
|       spellcheck="false" | ||||
|       monospace | ||||
|       test-id="json-prettify-input" | ||||
|     /> | ||||
|   </n-form-item> | ||||
|   <n-form-item label="Prettified version of your JSON"> | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| import { describe, expect, it } from 'vitest'; | ||||
| import { sortObjectKeys } from './json.models'; | ||||
| import { ref } from 'vue'; | ||||
| import { formatJson, sortObjectKeys } from './json.models'; | ||||
| 
 | ||||
| describe('json models', () => { | ||||
|   describe('sortObjectKeys', () => { | ||||
| @ -13,4 +14,105 @@ describe('json models', () => { | ||||
|       ); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('formatJson', () => { | ||||
|     const testJson = '{"b": 2, "a": 1}'; | ||||
|     const expectedSorted = '{\n   "a": 1,\n   "b": 2\n}'; | ||||
|     const expectedUnsorted = '{\n   "b": 2,\n   "a": 1\n}'; | ||||
| 
 | ||||
|     it('formats JSON with default options (sorted keys, 3 spaces)', () => { | ||||
|       const result = formatJson({ rawJson: testJson }); | ||||
|       expect(result).toBe(expectedSorted); | ||||
|     }); | ||||
| 
 | ||||
|     it('formats JSON without sorting keys when sortKeys is false', () => { | ||||
|       const result = formatJson({ rawJson: testJson, sortKeys: false }); | ||||
|       expect(result).toBe(expectedUnsorted); | ||||
|     }); | ||||
| 
 | ||||
|     it('formats JSON with custom indent size', () => { | ||||
|       const result = formatJson({ rawJson: testJson, indentSize: 2 }); | ||||
|       const expected = '{\n  "a": 1,\n  "b": 2\n}'; | ||||
|       expect(result).toBe(expected); | ||||
|     }); | ||||
| 
 | ||||
|     it('works with reactive refs', () => { | ||||
|       const rawJsonRef = ref(testJson); | ||||
|       const sortKeysRef = ref(true); | ||||
|       const indentSizeRef = ref(3); | ||||
| 
 | ||||
|       const result = formatJson({ | ||||
|         rawJson: rawJsonRef, | ||||
|         sortKeys: sortKeysRef, | ||||
|         indentSize: indentSizeRef, | ||||
|       }); | ||||
|       expect(result).toBe(expectedSorted); | ||||
|     }); | ||||
| 
 | ||||
|     describe('autoUnescape functionality', () => { | ||||
|       it('unescapes escaped JSON strings when autoUnescape is true', () => { | ||||
|         const escapedJson = '"{\\\"id\\\":\\\"123\\\",\\\"name\\\":\\\"test\\\"}"'; | ||||
|         const result = formatJson({ rawJson: escapedJson, autoUnescape: true, indentSize: 2 }); | ||||
|         const expected = '{\n  "id": "123",\n  "name": "test"\n}'; | ||||
|         expect(result).toBe(expected); | ||||
|       }); | ||||
| 
 | ||||
|       it('handles escaped JSON without outer quotes', () => { | ||||
|         const escapedJson = '{\\\"id\\\":\\\"123\\\",\\\"name\\\":\\\"test\\\"}'; | ||||
|         const result = formatJson({ rawJson: escapedJson, autoUnescape: true, indentSize: 2 }); | ||||
|         const expected = '{\n  "id": "123",\n  "name": "test"\n}'; | ||||
|         expect(result).toBe(expected); | ||||
|       }); | ||||
| 
 | ||||
|       it('unescapes various escape sequences', () => { | ||||
|         const escapedJson = '{\\\"text\\\":\\\"Hello\\\\\\\\World\\\",\\\"path\\\":\\\"/api\\\\/test\\\"}'; | ||||
|         const result = formatJson({ rawJson: escapedJson, autoUnescape: true, indentSize: 2 }); | ||||
|         const expected = '{\n  "path": "/api/test",\n  "text": "Hello\\\\World"\n}'; | ||||
|         expect(result).toBe(expected); | ||||
|       }); | ||||
| 
 | ||||
|       it('handles single-quoted outer strings', () => { | ||||
|         const escapedJson = '\'{\\\"id\\\":\\\"123\\\"}\''; | ||||
|         const result = formatJson({ rawJson: escapedJson, autoUnescape: true, indentSize: 2 }); | ||||
|         const expected = '{\n  "id": "123"\n}'; | ||||
|         expect(result).toBe(expected); | ||||
|       }); | ||||
| 
 | ||||
|       it('processes regular JSON normally when autoUnescape is false', () => { | ||||
|         const normalJson = '{"id":"123","name":"test"}'; | ||||
|         const result = formatJson({ rawJson: normalJson, autoUnescape: false, indentSize: 2 }); | ||||
|         const expected = '{\n  "id": "123",\n  "name": "test"\n}'; | ||||
|         expect(result).toBe(expected); | ||||
|       }); | ||||
| 
 | ||||
|       it('handles malformed escaped JSON gracefully', () => { | ||||
|         const malformedJson = '"{\\\"incomplete'; | ||||
|         // Should fall back to original string and fail parsing
 | ||||
|         expect(() => formatJson({ rawJson: malformedJson, autoUnescape: true })).toThrow(); | ||||
|       }); | ||||
| 
 | ||||
|       it('works with complex nested objects', () => { | ||||
|         const complexEscaped = '"{\\\"users\\\":[{\\\"id\\\":\\\"1\\\",\\\"data\\\":{\\\"active\\\":true}}],\\\"meta\\\":{\\\"total\\\":1}}"'; | ||||
|         const result = formatJson({ rawJson: complexEscaped, autoUnescape: true, indentSize: 2 }); | ||||
|         const expected = '{\n  "meta": {\n    "total": 1\n  },\n  "users": [\n    {\n      "data": {\n        "active": true\n      },\n      "id": "1"\n    }\n  ]\n}'; | ||||
|         expect(result).toBe(expected); | ||||
|       }); | ||||
| 
 | ||||
|       it('works with reactive autoUnescape ref', () => { | ||||
|         const escapedJson = '"{\\\"test\\\":\\\"value\\\"}"'; | ||||
|         const autoUnescapeRef = ref(true); | ||||
|         const result = formatJson({ rawJson: escapedJson, autoUnescape: autoUnescapeRef, indentSize: 2 }); | ||||
|         const expected = '{\n  "test": "value"\n}'; | ||||
|         expect(result).toBe(expected); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     it('handles empty string input', () => { | ||||
|       expect(() => formatJson({ rawJson: '' })).toThrow(); | ||||
|     }); | ||||
| 
 | ||||
|     it('handles invalid JSON input', () => { | ||||
|       expect(() => formatJson({ rawJson: 'invalid json' })).toThrow(); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| @ -20,16 +20,55 @@ function sortObjectKeys<T>(obj: T): T { | ||||
|     }, {} as Record<string, unknown>) as T; | ||||
| } | ||||
| 
 | ||||
| function unescapeJson(jsonString: string): string { | ||||
|   try { | ||||
|     // First, try to handle double-escaped scenarios
 | ||||
|     let result = jsonString.trim(); | ||||
| 
 | ||||
|     // If the string starts and ends with quotes, and contains escaped quotes inside,
 | ||||
|     // it might be a JSON string that needs to be unescaped
 | ||||
|     if ((result.startsWith('"') && result.endsWith('"')) | ||||
|         || (result.startsWith('\'') && result.endsWith('\''))) { | ||||
|       // Remove outer quotes first
 | ||||
|       result = result.slice(1, -1); | ||||
|     } | ||||
| 
 | ||||
|     // Handle common escape sequences
 | ||||
|     result = result | ||||
|       .replace(/\\"/g, '"') // Unescape quotes
 | ||||
|       .replace(/\\\\/g, '\\') // Unescape backslashes (do this after quotes!)
 | ||||
|       .replace(/\\n/g, '\n') // Unescape newlines
 | ||||
|       .replace(/\\r/g, '\r') // Unescape carriage returns
 | ||||
|       .replace(/\\t/g, '\t') // Unescape tabs
 | ||||
|       .replace(/\\f/g, '\f') // Unescape form feeds
 | ||||
|       .replace(/\\b/g, '\b') // Unescape backspaces
 | ||||
|       .replace(/\\\//g, '/'); // Unescape forward slashes
 | ||||
| 
 | ||||
|     return result; | ||||
|   } | ||||
|   catch { | ||||
|     return jsonString; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function formatJson({ | ||||
|   rawJson, | ||||
|   sortKeys = true, | ||||
|   indentSize = 3, | ||||
|   autoUnescape = false, | ||||
| }: { | ||||
|   rawJson: MaybeRef<string> | ||||
|   sortKeys?: MaybeRef<boolean> | ||||
|   indentSize?: MaybeRef<number> | ||||
|   autoUnescape?: MaybeRef<boolean> | ||||
| }) { | ||||
|   const parsedObject = JSON5.parse(get(rawJson)); | ||||
|   let jsonString = get(rawJson); | ||||
| 
 | ||||
|   if (get(autoUnescape)) { | ||||
|     jsonString = unescapeJson(jsonString); | ||||
|   } | ||||
| 
 | ||||
|   const parsedObject = JSON5.parse(jsonString); | ||||
| 
 | ||||
|   return JSON.stringify(get(sortKeys) ? sortObjectKeys(parsedObject) : parsedObject, null, get(indentSize)); | ||||
| } | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user