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