Files
SIG-CM2.0/src/web/src/features/chargeableChars/__tests__/SymbolInput.test.tsx
dmolinari c2a0612a70 feat(frontend): chargeableChars feature — table + dialog + copy-to-all (PRC-001)
- 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
2026-04-20 12:59:27 -03:00

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)
})
})