- types.ts: ChargeableCharConfig, PagedResult, requests (validFrom/validTo as yyyy-MM-dd strings, UDT-011)
- categories.ts: CHARGEABLE_CHAR_CATEGORIES + CATEGORY_LABELS
- api/: 5 functions (list, getById, create, schedulePriceChange, deactivate) via axiosClient
- hooks/: 5 TanStack Query hooks; mutations invalidate ['chargeableChars','list'] + byId
- SymbolInput.tsx: emoji-blocking input (/\p{Extended_Pictographic}/u), max 4 chars
- ChargeableCharsTable.tsx: shadcn DataTable; medio filter + activeOnly toggle; Vigente/Cerrada badges; formatCivilDate (UDT-011)
- ChargeableCharFormDialog.tsx: dual-mode create/schedulePrice; Zod schema; todayArgentina() min date; 409 inline error
- CopyToAllMediaDialog.tsx: Promise.allSettled over active medios; preview symbol/price/date
- ChargeableCharsPage.tsx: orchestrates table + dialogs + state
- routes.tsx: path/permission constants
- router.tsx: route /admin/tasacion/chargeable-chars registered
- AppSidebar.tsx: nav item "Caracteres Tasables" with Hash icon
- Tests: 22 new RTL/vitest tests (5 test files) — strict TDD RED→GREEN→REFACTOR
79 lines
3.0 KiB
TypeScript
79 lines
3.0 KiB
TypeScript
import { describe, it, expect, vi, afterEach } from 'vitest'
|
|
import { render, screen } from '@testing-library/react'
|
|
import userEvent from '@testing-library/user-event'
|
|
import { SymbolInput } from '../components/SymbolInput'
|
|
|
|
afterEach(() => vi.clearAllMocks())
|
|
|
|
describe('SymbolInput — emoji blocking', () => {
|
|
it('typing ASCII chars updates value via onChange', async () => {
|
|
const onChange = vi.fn()
|
|
render(<SymbolInput value="" onChange={onChange} />)
|
|
|
|
const input = screen.getByRole('textbox')
|
|
await userEvent.type(input, '$')
|
|
|
|
expect(onChange).toHaveBeenCalledWith('$')
|
|
})
|
|
|
|
it('typing an emoji does NOT call onChange with emoji content (emoji blocked)', async () => {
|
|
const onChange = vi.fn()
|
|
render(<SymbolInput value="" onChange={onChange} />)
|
|
|
|
const input = screen.getByRole('textbox')
|
|
// userEvent.type fires change events for each char; emoji chars may be split into surrogates.
|
|
// The contract: onChange must never be called with a value containing an Extended_Pictographic char.
|
|
await userEvent.type(input, '😀')
|
|
|
|
const calls = onChange.mock.calls.map(([v]: [string]) => v)
|
|
const hasEmoji = calls.some((v) => /\p{Extended_Pictographic}/u.test(v))
|
|
expect(hasEmoji).toBe(false)
|
|
})
|
|
|
|
it('pasting a string with emoji — onChange NOT called with emoji content', async () => {
|
|
const onChange = vi.fn()
|
|
render(<SymbolInput value="" onChange={onChange} />)
|
|
|
|
const input = screen.getByRole('textbox')
|
|
await userEvent.click(input)
|
|
// userEvent.paste triggers onPaste handler with the given text
|
|
await userEvent.paste('😀')
|
|
|
|
// onChange must NOT have been called with an emoji
|
|
const calls = onChange.mock.calls.map(([v]: [string]) => v)
|
|
const hasEmoji = calls.some((v) => /\p{Extended_Pictographic}/u.test(v))
|
|
expect(hasEmoji).toBe(false)
|
|
})
|
|
|
|
it('pasting normal text (no emoji) allows value update', async () => {
|
|
const onChange = vi.fn()
|
|
render(<SymbolInput value="" onChange={onChange} />)
|
|
|
|
const input = screen.getByRole('textbox')
|
|
await userEvent.click(input)
|
|
// Paste normal ASCII — should go through onChange
|
|
await userEvent.paste('$')
|
|
|
|
// onChange may be called with '$' or the merged result
|
|
// The key assertion: no rejection for non-emoji
|
|
const calls = onChange.mock.calls.map(([v]: [string]) => v)
|
|
const allNonEmoji = calls.every((v) => !/\p{Extended_Pictographic}/u.test(v))
|
|
expect(allNonEmoji).toBe(true)
|
|
})
|
|
|
|
it('value is capped at 4 characters — 5th char is rejected via onChange not called with 5+ chars', async () => {
|
|
// Start with value of 4 chars already set
|
|
const onChange = vi.fn()
|
|
render(<SymbolInput value="$$$$" onChange={onChange} />)
|
|
|
|
const input = screen.getByRole('textbox')
|
|
// DOM value is controlled at 4 chars; any additional char should be blocked
|
|
await userEvent.type(input, '$')
|
|
|
|
// onChange should NOT be called with a 5-char string
|
|
const calls = onChange.mock.calls.map(([v]: [string]) => v)
|
|
const tooLong = calls.some((v) => v.length > 4)
|
|
expect(tooLong).toBe(false)
|
|
})
|
|
})
|