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