From 8b83fb5c536949bbc6d0caea3197fedca1826ff5 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Wed, 12 Aug 2026 20:41:12 +0800 Subject: [PATCH] feat: add JSON escape utility with comprehensive edge case tests --- packages/core/src/__tests__/escape.test.ts | 63 ++++++++++++++++++++++ packages/core/src/escape.ts | 15 ++++++ packages/core/src/index.ts | 1 + 3 files changed, 79 insertions(+) create mode 100644 packages/core/src/__tests__/escape.test.ts create mode 100644 packages/core/src/escape.ts diff --git a/packages/core/src/__tests__/escape.test.ts b/packages/core/src/__tests__/escape.test.ts new file mode 100644 index 0000000..1b7c5e5 --- /dev/null +++ b/packages/core/src/__tests__/escape.test.ts @@ -0,0 +1,63 @@ +import { describe, it, expect } from 'vitest'; +import { safeStringify, isValidJson } from '../escape.js'; + +describe('safeStringify', () => { + it('returns valid JSON for simple objects', () => { + const result = safeStringify({ ok: true, data: { id: 'p_123' } }); + expect(() => JSON.parse(result)).not.toThrow(); + expect(JSON.parse(result)).toEqual({ ok: true, data: { id: 'p_123' } }); + }); + + it('escapes double quotes in string values', () => { + const result = safeStringify({ text: 'He said "hello"' }); + const parsed = JSON.parse(result); + expect(parsed.text).toBe('He said "hello"'); + }); + + it('escapes backslashes in string values', () => { + const result = safeStringify({ path: 'C:\\Users\\test' }); + const parsed = JSON.parse(result); + expect(parsed.path).toBe('C:\\Users\\test'); + }); + + it('escapes control characters (newline, tab)', () => { + const result = safeStringify({ text: 'line1\nline2\tindented' }); + const parsed = JSON.parse(result); + expect(parsed.text).toBe('line1\nline2\tindented'); + }); + + it('handles unicode characters', () => { + const result = safeStringify({ text: '你好世界 🌍' }); + const parsed = JSON.parse(result); + expect(parsed.text).toBe('你好世界 🌍'); + }); + + it('handles HTML-like content without breaking JSON', () => { + const html = '
Hello
'; + const result = safeStringify({ html }); + const parsed = JSON.parse(result); + expect(parsed.html).toBe(html); + }); + + it('handles empty string and null', () => { + expect(JSON.parse(safeStringify({ a: '' }))).toEqual({ a: '' }); + expect(JSON.parse(safeStringify({ a: null }))).toEqual({ a: null }); + }); + + it('handles arrays with special characters', () => { + const result = safeStringify({ items: ['a"b', 'c\\d', 'e\nf'] }); + const parsed = JSON.parse(result); + expect(parsed.items).toEqual(['a"b', 'c\\d', 'e\nf']); + }); +}); + +describe('isValidJson', () => { + it('returns true for valid JSON', () => { + expect(isValidJson('{"ok":true}')).toBe(true); + }); + + it('returns false for invalid JSON', () => { + expect(isValidJson('{ok:true}')).toBe(false); + expect(isValidJson('')).toBe(false); + }); +}); diff --git a/packages/core/src/escape.ts b/packages/core/src/escape.ts new file mode 100644 index 0000000..8eafd54 --- /dev/null +++ b/packages/core/src/escape.ts @@ -0,0 +1,15 @@ +// JSON safe serialization / JSON 安全序列化 +// Always use this instead of manual string concatenation for JSON output + +export function safeStringify(obj: unknown): string { + return JSON.stringify(obj); +} + +export function isValidJson(str: string): boolean { + try { + JSON.parse(str); + return true; + } catch { + return false; + } +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 9d0d3bd..39c3620 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -8,3 +8,4 @@ export { type CanvasNoiseConfig, type PermissionState, } from './types/fingerprint.js'; +export { safeStringify, isValidJson } from './escape.js';