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 rawJson = useStorage('json-prettify:raw-json', '{"hello": "world", "foo": "bar"}'); | ||||||
| const indentSize = useStorage('json-prettify:indent-size', 3); | const indentSize = useStorage('json-prettify:indent-size', 3); | ||||||
| const sortKeys = useStorage('json-prettify:sort-keys', true); | 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({ | const rawJsonValidation = useValidation({ | ||||||
|   source: rawJson, |   source: rawJson, | ||||||
|   rules: [ |   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.', |       message: 'Provided JSON is not valid.', | ||||||
|     }, |     }, | ||||||
|   ], |   ], | ||||||
|  |   watch: [autoUnescape], | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|   <div style="flex: 0 0 100%"> |   <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-form-item label="Sort keys :" label-placement="left" label-width="100"> | ||||||
|         <n-switch v-model:value="sortKeys" /> |         <n-switch v-model:value="sortKeys" /> | ||||||
|       </n-form-item> |       </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-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-input-number v-model:value="indentSize" min="0" max="10" style="width: 100px" /> | ||||||
|       </n-form-item> |       </n-form-item> | ||||||
| @ -44,7 +80,7 @@ const rawJsonValidation = useValidation({ | |||||||
|     <c-input-text |     <c-input-text | ||||||
|       ref="inputElement" |       ref="inputElement" | ||||||
|       v-model:value="rawJson" |       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" |       rows="20" | ||||||
|       multiline |       multiline | ||||||
|       autocomplete="off" |       autocomplete="off" | ||||||
| @ -52,6 +88,7 @@ const rawJsonValidation = useValidation({ | |||||||
|       autocapitalize="off" |       autocapitalize="off" | ||||||
|       spellcheck="false" |       spellcheck="false" | ||||||
|       monospace |       monospace | ||||||
|  |       test-id="json-prettify-input" | ||||||
|     /> |     /> | ||||||
|   </n-form-item> |   </n-form-item> | ||||||
|   <n-form-item label="Prettified version of your JSON"> |   <n-form-item label="Prettified version of your JSON"> | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| import { describe, expect, it } from 'vitest'; | 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('json models', () => { | ||||||
|   describe('sortObjectKeys', () => { |   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; |     }, {} 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({ | function formatJson({ | ||||||
|   rawJson, |   rawJson, | ||||||
|   sortKeys = true, |   sortKeys = true, | ||||||
|   indentSize = 3, |   indentSize = 3, | ||||||
|  |   autoUnescape = false, | ||||||
| }: { | }: { | ||||||
|   rawJson: MaybeRef<string> |   rawJson: MaybeRef<string> | ||||||
|   sortKeys?: MaybeRef<boolean> |   sortKeys?: MaybeRef<boolean> | ||||||
|   indentSize?: MaybeRef<number> |   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)); |   return JSON.stringify(get(sortKeys) ? sortObjectKeys(parsedObject) : parsedObject, null, get(indentSize)); | ||||||
| } | } | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user