From a831874e60ec114381ce20abd786d7eb8a6e127c Mon Sep 17 00:00:00 2001 From: Qi Yu Date: Fri, 29 Aug 2025 14:29:48 +0800 Subject: [PATCH] feat(json-viewer): add auto-unescape functionality for escaped JSON strings - Add auto-unescape toggle to handle escaped JSON strings like '{\"key\":\"value\"}' - Implement smart unescaping logic that handles quotes, backslashes, and common escape sequences - Support both quoted and unquoted escaped JSON formats - Add real-time validation that works with auto-unescape toggle - Improve UI layout with wider label for auto-unescape option - Add comprehensive unit tests (15 test cases) covering all functionality - Add E2E tests (6 test scenarios) for complete user workflow testing - Update placeholder text to guide users about the new feature - Add test-id for better E2E test reliability This enhancement allows developers to easily prettify JSON from logs, API responses, and other sources where JSON is commonly escaped in strings. --- src/tools/json-viewer/json-viewer.e2e.spec.ts | 87 +++++++++++++++ src/tools/json-viewer/json-viewer.vue | 45 +++++++- src/tools/json-viewer/json.models.test.ts | 104 +++++++++++++++++- src/tools/json-viewer/json.models.ts | 41 ++++++- 4 files changed, 271 insertions(+), 6 deletions(-) create mode 100644 src/tools/json-viewer/json-viewer.e2e.spec.ts diff --git a/src/tools/json-viewer/json-viewer.e2e.spec.ts b/src/tools/json-viewer/json-viewer.e2e.spec.ts new file mode 100644 index 00000000..dfb0521c --- /dev/null +++ b/src/tools/json-viewer/json-viewer.e2e.spec.ts @@ -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); + }); +}); diff --git a/src/tools/json-viewer/json-viewer.vue b/src/tools/json-viewer/json-viewer.vue index 3928a44f..b02f303f 100644 --- a/src/tools/json-viewer/json-viewer.vue +++ b/src/tools/json-viewer/json-viewer.vue @@ -11,25 +11,61 @@ const inputElement = ref(); 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], });