import '@testing-library/jest-dom' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { useForm } from 'react-hook-form' import { axe, toHaveNoViolations } from 'jest-axe' import { vi } from 'vitest' import { PktButton } from '../button/Button' import { PktTimepicker } from './Timepicker' expect.extend(toHaveNoViolations) const id = 'test-timepicker' const label = 'Test Timepicker' describe('PktTimepicker', () => { describe('Rendering', () => { test('renders without errors', () => { const { container } = render() expect(container.querySelector('.pkt-timepicker')).toBeInTheDocument() }) test('renders spinbutton inputs and separator', () => { render() const spinbuttons = screen.getAllByRole('spinbutton') expect(spinbuttons).toHaveLength(2) expect(spinbuttons[0]).toHaveAttribute('id', `${id}-hours`) expect(spinbuttons[1]).toHaveAttribute('id', `${id}-minutes`) }) test('adds stepper modifier when stepArrows is set', () => { const { container } = render( , ) expect(container.querySelector('.pkt-timepicker--stepper')).toBeInTheDocument() }) test('adds fullwidth modifier when fullwidth is set', () => { const { container } = render( , ) expect(container.querySelector('.pkt-timepicker--fullwidth')).toBeInTheDocument() }) test('renders clock button when picker is visible', () => { render() expect( screen.getByRole('button', { name: /åpne tidspunkt-velger/i }), ).toBeInTheDocument() }) test('hides clock button when hidePicker is set', () => { render() expect( screen.queryByRole('button', { name: /åpne tidspunkt-velger/i }), ).not.toBeInTheDocument() }) test('renders decorative clock icon when hidePicker is set', () => { const { container } = render() expect(container.querySelector('.pkt-timepicker__icon')).toBeInTheDocument() }) test('does not render popup when hidePicker is set', () => { const { container } = render() expect(container.querySelector('.pkt-timepicker-popup')).not.toBeInTheDocument() }) test('does not render popup when stepArrows is set', () => { const { container } = render() expect(container.querySelector('.pkt-timepicker-popup')).not.toBeInTheDocument() }) test('renders prev/next buttons when stepArrows is set', () => { render() expect(screen.getByRole('button', { name: /forrige tidspunkt/i })).toBeInTheDocument() expect(screen.getByRole('button', { name: /neste tidspunkt/i })).toBeInTheDocument() }) test('popup is hidden by default', () => { const { container } = render() const popup = container.querySelector('.pkt-timepicker-popup') expect(popup).toBeInTheDocument() expect(popup).toHaveAttribute('hidden') }) }) describe('Properties', () => { test('value sets display inputs', () => { render() const spinbuttons = screen.getAllByRole('spinbutton') expect(spinbuttons[0]).toHaveValue('09') expect(spinbuttons[1]).toHaveValue('30') }) test('empty value renders empty display inputs', () => { render() const spinbuttons = screen.getAllByRole('spinbutton') expect(spinbuttons[0]).toHaveValue('') expect(spinbuttons[1]).toHaveValue('') }) test('disabled disables inputs and clock button', () => { render() const spinbuttons = screen.getAllByRole('spinbutton') expect(spinbuttons[0]).toBeDisabled() expect(spinbuttons[1]).toBeDisabled() expect(screen.getByRole('button', { name: /åpne tidspunkt-velger/i })).toBeDisabled() }) test('min/max/step are not on hidden input (constraints use setCustomValidity on hours for native form validation)', () => { const { container } = render( , ) const hidden = container.querySelector(`#${id}-input`) expect(hidden?.hasAttribute('min')).toBe(false) expect(hidden?.hasAttribute('max')).toBe(false) expect(hidden?.hasAttribute('step')).toBe(false) expect(hidden?.hasAttribute('required')).toBe(false) }) test('hidden input reflects value', () => { const { container } = render( , ) const hidden = container.querySelector(`#${id}-input`) expect(hidden?.value).toBe('09:30') }) test('native form submit flushes spinbutton digits into the named hidden input before validation', async () => { const user = userEvent.setup() const onSubmit = vi.fn() render(
{ e.preventDefault() onSubmit(Object.fromEntries(new FormData(e.currentTarget).entries())) }} > , ) const [hoursEl, minutesEl] = screen.getAllByRole('spinbutton') await user.click(hoursEl) await user.keyboard('01') await user.click(minutesEl) await user.keyboard('02') await user.click(screen.getByRole('button', { name: /^send$/i })) expect(onSubmit).toHaveBeenCalledWith({ tid: '01:02' }) }) }) describe('Popup', () => { test('clock button opens popup', async () => { const user = userEvent.setup() const { container } = render() await user.click(screen.getByRole('button', { name: /åpne tidspunkt-velger/i })) const popup = container.querySelector('.pkt-timepicker-popup') expect(popup).not.toHaveAttribute('hidden') }) test('Escape closes popup', async () => { const user = userEvent.setup() render() const btn = screen.getByRole('button', { name: /åpne tidspunkt-velger/i }) await user.click(btn) await waitFor(() => expect(btn).toHaveAttribute('aria-expanded', 'true')) await user.keyboard('{Escape}') await waitFor(() => expect(btn).toHaveAttribute('aria-expanded', 'false')) }) test('popup renders hour and minute columns', async () => { const user = userEvent.setup() const { container } = render() await user.click(screen.getByRole('button', { name: /åpne tidspunkt-velger/i })) const cols = container.querySelectorAll('.pkt-timepicker-popup__col') expect(cols).toHaveLength(2) }) }) describe('Stepper', () => { test('next button increments by one minute step', async () => { const user = userEvent.setup() const { container } = render( , ) await user.click(screen.getByRole('button', { name: /neste tidspunkt/i })) const hidden = container.querySelector(`#${id}-input`) expect(hidden?.value).toBe('09:01') }) test('prev button decrements by step (5 min)', async () => { const user = userEvent.setup() const { container } = render( , ) await user.click(screen.getByRole('button', { name: /forrige tidspunkt/i })) const hidden = container.querySelector(`#${id}-input`) expect(hidden?.value).toBe('09:00') }) test('next button at 59 minutes rolls over to next hour', async () => { const user = userEvent.setup() const { container } = render( , ) await user.click(screen.getByRole('button', { name: /neste tidspunkt/i })) const hidden = container.querySelector(`#${id}-input`) expect(hidden?.value).toBe('10:00') }) }) describe('Events', () => { test('onFocus fires when a spinbutton is focused (focus must bubble to root handler)', async () => { const user = userEvent.setup() const onFocus = vi.fn() const { container } = render( , ) const hoursInput = container.querySelector(`#${id}-hours`)! await user.click(hoursInput) expect(onFocus).toHaveBeenCalledTimes(1) }) test('Backspace removes digits in hours and updates hidden value (controlled inputs)', async () => { const user = userEvent.setup() const { container } = render( , ) const hoursInput = container.querySelector(`#${id}-hours`)! const hidden = container.querySelector(`#${id}-input`) hoursInput.focus() await user.keyboard('{Backspace}') expect(hoursInput).toHaveValue('0') expect(hidden?.value).toBe('00:30') await user.keyboard('{Backspace}') expect(hoursInput).toHaveValue('') expect(hidden?.value).toBe('') }) test('onValueChange fires when hours change via ArrowUp', async () => { const handleValueChange = vi.fn() const { container } = render( , ) const hoursInput = container.querySelector(`#${id}-hours`)! fireEvent.keyDown(hoursInput, { key: 'ArrowUp' }) await waitFor(() => { expect(handleValueChange).toHaveBeenCalledWith('10:30') }) }) }) describe('Accessibility', () => { test('passes axe in default state', async () => { const { container } = render() await window.customElements.whenDefined('pkt-icon') const results = await axe(container) expect(results).toHaveNoViolations() }) test('hours input has spinbutton role and bounds', () => { render() const hours = screen.getAllByRole('spinbutton')[0] expect(hours).toHaveAttribute('role', 'spinbutton') expect(hours).toHaveAttribute('aria-valuemin', '0') expect(hours).toHaveAttribute('aria-valuemax', '23') expect(hours).toHaveAttribute('aria-valuenow', '9') }) }) }) describe('PktTimepicker + React Hook Form', () => { function WatchForm() { const { register, watch } = useForm<{ tid: string }>({ defaultValues: { tid: '09:30' }, }) const values = watch() return (
{values.tid ?? ''} ) } test('register() defaultValues are reflected in watch()', async () => { render() await waitFor(() => { expect(screen.getByTestId('watched')).toHaveTextContent('09:30') }) }) function SubmitForm({ onSubmit, }: { onSubmit: (data: { tid: string }) => void }) { const { register, handleSubmit } = useForm<{ tid: string }>({ defaultValues: { tid: '14:45' }, }) return (
Send ) } test('submit sends current time value from register()', async () => { const user = userEvent.setup() const onSubmit = vi.fn() render() await user.click(screen.getByRole('button', { name: /send/i })) await waitFor(() => { expect(onSubmit).toHaveBeenCalledWith({ tid: '14:45' }, expect.anything()) }) }) })