Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/config/routes.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const ROUTES = {
'whats-my-ip': '/whats-my-ip',
'ip-location': '/ip-location',
'json-formatter': '/json',
'json-escape': '/json-escape',
'xml-formatter': '/xml',
} as const

Expand Down
11 changes: 11 additions & 0 deletions src/config/tools.config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
MapPin,
Braces,
Code2,
Quote,
} from 'lucide-react'
import { ROUTES } from './routes.config'
import { CATEGORIES, type Tool } from '@/types/tool.types'
Expand Down Expand Up @@ -257,6 +258,16 @@ export const tools: Tool[] = [
},
],
},
{
title: 'JSON Escape',
href: ROUTES['json-escape'],
description: 'Escape or unescape JSON special characters in text',
icon: Quote,
categories: [CATEGORIES.DEVELOPMENT],
searchTags: ['json', 'escape', 'unescape', 'quotes', 'string', 'format'],
component: React.lazy(() => import('@/tools/JsonEscape')),
online: false,
},
{
title: 'XML Formatter',
href: ROUTES['xml-formatter'],
Expand Down
1 change: 1 addition & 0 deletions src/stores/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { useBase64Store } from './base64.store'
export { useURLStore } from './url.store'
export { useJSONFormatterStore } from './json-formatter.store'
export { useJsonEscapeStore } from './json-escape.store'
17 changes: 17 additions & 0 deletions src/stores/json-escape.store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { create } from 'zustand'

/**
* JSON Escape/Unescape store
* Simple store for managing the text area input
*/
interface JsonEscapeStore {
input: string
setInput: (value: string) => void
reset: () => void
}

export const useJsonEscapeStore = create<JsonEscapeStore>((set) => ({
input: '',
setInput: (value) => set({ input: value }),
reset: () => set({ input: '' }),
}))
268 changes: 268 additions & 0 deletions src/tools/JsonEscape.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
import { describe, it, expect, beforeEach } from 'vitest'
import { render, screen, act } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import JsonEscape from './JsonEscape'
import { useJsonEscapeStore } from '@/stores'

// Reset store state before each test
beforeEach(() => {
useJsonEscapeStore.getState().reset()
})

describe('JsonEscape', () => {
it('renders with empty textarea and three buttons', () => {
render(<JsonEscape />)
expect(screen.getByPlaceholderText(/paste your text here/i)).toBeInTheDocument()
expect(screen.getByRole('button', { name: /^escape$/i })).toBeInTheDocument()
expect(screen.getByRole('button', { name: /unescape/i })).toBeInTheDocument()
expect(screen.getByRole('button', { name: /clear/i })).toBeInTheDocument()
})

describe('Escape functionality', () => {
it('escapes double quotes', async () => {
const user = userEvent.setup()
render(<JsonEscape />)

const input = screen.getByPlaceholderText(/paste your text here/i)
act(() => {
act(() => {
useJsonEscapeStore.getState().setInput('Hello "World"')
})
})
await user.click(screen.getByRole('button', { name: /^escape$/i }))

expect(input).toHaveValue('Hello \\"World\\"')
})

it('escapes backslashes', async () => {
const user = userEvent.setup()
render(<JsonEscape />)

const input = screen.getByPlaceholderText(/paste your text here/i)
act(() => {
act(() => {
useJsonEscapeStore.getState().setInput('C:\\Users\\Test')
})
})
await user.click(screen.getByRole('button', { name: /^escape$/i }))

expect(input).toHaveValue('C:\\\\Users\\\\Test')
})

it('escapes newlines', async () => {
const user = userEvent.setup()
render(<JsonEscape />)

const input = screen.getByPlaceholderText(/paste your text here/i)
// Directly set value with newline
act(() => {
useJsonEscapeStore.getState().setInput('Line1\nLine2')
})
await user.click(screen.getByRole('button', { name: /^escape$/i }))

expect(input).toHaveValue('Line1\\nLine2')
})

it('escapes tabs', async () => {
const user = userEvent.setup()
render(<JsonEscape />)

const input = screen.getByPlaceholderText(/paste your text here/i)
act(() => {
useJsonEscapeStore.getState().setInput('Column1\tColumn2')
})
await user.click(screen.getByRole('button', { name: /^escape$/i }))

expect(input).toHaveValue('Column1\\tColumn2')
})

it('escapes carriage returns', async () => {
const user = userEvent.setup()
render(<JsonEscape />)

const input = screen.getByPlaceholderText(/paste your text here/i)
act(() => {
useJsonEscapeStore.getState().setInput('Line1\rLine2')
})
await user.click(screen.getByRole('button', { name: /^escape$/i }))

expect(input).toHaveValue('Line1\\rLine2')
})

it('escapes multiple special characters at once', async () => {
const user = userEvent.setup()
render(<JsonEscape />)

const input = screen.getByPlaceholderText(/paste your text here/i)
act(() => {
useJsonEscapeStore.getState().setInput('Hello "World"\nNew line\tTab')
})
await user.click(screen.getByRole('button', { name: /^escape$/i }))

expect(input).toHaveValue('Hello \\"World\\"\\nNew line\\tTab')
})

it('handles empty input when escaping', async () => {
const user = userEvent.setup()
render(<JsonEscape />)

const input = screen.getByPlaceholderText(/paste your text here/i)
await user.click(screen.getByRole('button', { name: /^escape$/i }))

expect(input).toHaveValue('')
})
})

describe('Unescape functionality', () => {
it('unescapes double quotes', async () => {
const user = userEvent.setup()
render(<JsonEscape />)

const input = screen.getByPlaceholderText(/paste your text here/i)
act(() => {
useJsonEscapeStore.getState().setInput('Hello \\"World\\"')
})
await user.click(screen.getByRole('button', { name: /unescape/i }))

expect(input).toHaveValue('Hello "World"')
})

it('unescapes newlines', async () => {
const user = userEvent.setup()
render(<JsonEscape />)

const input = screen.getByPlaceholderText(/paste your text here/i)
act(() => {
useJsonEscapeStore.getState().setInput('Line1\\nLine2')
})
await user.click(screen.getByRole('button', { name: /unescape/i }))

expect(input).toHaveValue('Line1\nLine2')
})

it('unescapes tabs', async () => {
const user = userEvent.setup()
render(<JsonEscape />)

const input = screen.getByPlaceholderText(/paste your text here/i)
act(() => {
useJsonEscapeStore.getState().setInput('Column1\\tColumn2')
})
await user.click(screen.getByRole('button', { name: /unescape/i }))

expect(input).toHaveValue('Column1\tColumn2')
})

it('unescapes backslashes', async () => {
const user = userEvent.setup()
render(<JsonEscape />)

const input = screen.getByPlaceholderText(/paste your text here/i)
act(() => {
useJsonEscapeStore.getState().setInput('C:\\\\Users\\\\Test')
})
await user.click(screen.getByRole('button', { name: /unescape/i }))

expect(input).toHaveValue('C:\\Users\\Test')
})

it('unescapes multiple special characters at once', async () => {
const user = userEvent.setup()
render(<JsonEscape />)

const input = screen.getByPlaceholderText(/paste your text here/i)
act(() => {
useJsonEscapeStore.getState().setInput('Hello \\"World\\"\\nNew line\\tTab')
})
await user.click(screen.getByRole('button', { name: /unescape/i }))

expect(input).toHaveValue('Hello "World"\nNew line\tTab')
})

it('handles empty input when unescaping', async () => {
const user = userEvent.setup()
render(<JsonEscape />)

const input = screen.getByPlaceholderText(/paste your text here/i)
await user.click(screen.getByRole('button', { name: /unescape/i }))

expect(input).toHaveValue('')
})
})

describe('Clear functionality', () => {
it('clears the textarea when Clear button is clicked', async () => {
const user = userEvent.setup()
render(<JsonEscape />)

const input = screen.getByPlaceholderText(/paste your text here/i)
await user.type(input, 'Some text')
expect(input).toHaveValue('Some text')

await user.click(screen.getByRole('button', { name: /clear/i }))

expect(input).toHaveValue('')
})

it('clears after escape operation', async () => {
const user = userEvent.setup()
render(<JsonEscape />)

const input = screen.getByPlaceholderText(/paste your text here/i)
await user.type(input, 'Hello "World"')
await user.click(screen.getByRole('button', { name: /^escape$/i }))
expect(input).toHaveValue('Hello \\"World\\"')

await user.click(screen.getByRole('button', { name: /clear/i }))

expect(input).toHaveValue('')
})
})

describe('Round-trip operations', () => {
it('escape then unescape returns original text', async () => {
const user = userEvent.setup()
render(<JsonEscape />)

const input = screen.getByPlaceholderText(/paste your text here/i)
const originalText = 'Hello "World"\nNew line\tTab'
act(() => {
useJsonEscapeStore.getState().setInput(originalText)
})

// Escape
await user.click(screen.getByRole('button', { name: /^escape$/i }))
const escapedValue = (input as HTMLTextAreaElement).value
expect(escapedValue).toBe('Hello \\"World\\"\\nNew line\\tTab')

// Unescape
await user.click(screen.getByRole('button', { name: /unescape/i }))
expect(input).toHaveValue(originalText)
})
})

describe('Store integration', () => {
it('updates store when typing in textarea', async () => {
const user = userEvent.setup()
render(<JsonEscape />)

const input = screen.getByPlaceholderText(/paste your text here/i)
await user.type(input, 'test')

expect(useJsonEscapeStore.getState().input).toBe('test')
})

it('resets store when Clear is clicked', async () => {
const user = userEvent.setup()
render(<JsonEscape />)

const input = screen.getByPlaceholderText(/paste your text here/i)
await user.type(input, 'test')
expect(useJsonEscapeStore.getState().input).toBe('test')

await user.click(screen.getByRole('button', { name: /clear/i }))

expect(useJsonEscapeStore.getState().input).toBe('')
})
})
})
Loading