import { render, screen, fireEvent, within } from '@testing-library/react' import { describe, it, expect, vi, beforeEach } from 'vitest' import { SUPPORTED_LANGUAGES } from '../../i18n' import { useSettingsStore } from '../../store/settingsStore' import PublicLanguagePicker from './PublicLanguagePicker' describe('PublicLanguagePicker', () => { beforeEach(() => { useSettingsStore.setState(s => ({ settings: { ...s.settings, language: 'en' } })) }) it('FE-PUBLANG-001: the trigger keeps the current language as its accessible name', () => { render() expect(screen.getByRole('button', { name: 'Deutsch' })).toBeInTheDocument() }) // `br` and `zh-TW` are whole values, not prefixes: matching the prefix first // labelled Brazilian Portuguese "Language" and zh-TW "简体中文". it('FE-PUBLANG-001b: a hyphenated or non-ISO stored value labels itself correctly', () => { const { unmount } = render() expect(screen.getByRole('button', { name: 'Português (Brasil)' })).toBeInTheDocument() unmount() render() expect(screen.getByRole('button', { name: '繁體中文' })).toBeInTheDocument() }) it('FE-PUBLANG-002: an unknown locale falls back to the generic label', () => { render() expect(screen.getByRole('button', { name: 'Language' })).toBeInTheDocument() }) it('FE-PUBLANG-003: closed renders no options at all', () => { render() expect(screen.queryByTestId('public-language-menu')).toBeNull() expect(screen.queryByRole('button', { name: 'Deutsch' })).toBeNull() }) it('FE-PUBLANG-004: open lists every supported language', () => { render() const menu = within(screen.getByTestId('public-language-menu')) for (const lang of SUPPORTED_LANGUAGES) { expect(menu.getByRole('button', { name: lang.label })).toBeInTheDocument() } }) // The regression guard for #2248: the journey hero clips its own overflow, so a // menu rendered inside the page tree is amputated at the hero's bottom edge. it('FE-PUBLANG-005: the menu is portalled straight to the document body', () => { const { container } = render() const menu = screen.getByTestId('public-language-menu') expect(menu.parentElement).toBe(document.body) expect(container.contains(menu)).toBe(false) }) it('FE-PUBLANG-006: the menu is fixed, capped in height and scrolls its own overflow', () => { render() const menu = screen.getByTestId('public-language-menu') expect(menu.style.position).toBe('fixed') expect(menu.style.overflowY).toBe('auto') expect(menu.style.overscrollBehavior).toBe('contain') expect(Number.parseInt(menu.style.maxHeight, 10)).toBeLessThanOrEqual(320) }) it('FE-PUBLANG-007: the trigger toggles through an updater so a controlled parent stays in sync', () => { const onOpenChange = vi.fn() render() fireEvent.click(screen.getByRole('button', { name: 'English' })) const toggle = onOpenChange.mock.calls[0][0] as (v: boolean) => boolean expect(toggle(false)).toBe(true) expect(toggle(true)).toBe(false) }) it('FE-PUBLANG-008: picking a language stores it locally and closes the menu', () => { const onOpenChange = vi.fn() render() fireEvent.click(screen.getByRole('button', { name: 'Deutsch' })) expect(useSettingsStore.getState().settings.language).toBe('de') expect(onOpenChange).toHaveBeenCalledWith(false) }) it('FE-PUBLANG-009: Escape closes the menu', () => { const onOpenChange = vi.fn() render() fireEvent.keyDown(document, { key: 'Escape' }) expect(onOpenChange).toHaveBeenCalledWith(false) }) it('FE-PUBLANG-010: a pointer press outside closes it, one on the trigger or the menu does not', () => { const onOpenChange = vi.fn() const { container } = render() fireEvent.mouseDown(within(container).getByRole('button', { name: 'English' })) fireEvent.mouseDown(screen.getByRole('button', { name: 'Deutsch' })) expect(onOpenChange).not.toHaveBeenCalled() fireEvent.mouseDown(document.body) expect(onOpenChange).toHaveBeenCalledWith(false) }) it('FE-PUBLANG-011: hovering an option paints and clears its background', () => { render() const option = screen.getByRole('button', { name: 'Deutsch' }) fireEvent.mouseEnter(option) expect(option.style.background).toBe('var(--bg-hover)') fireEvent.mouseLeave(option) expect(option.style.background).toBe('none') }) it('FE-PUBLANG-012: the current language is marked for assistive tech', () => { render() const menu = within(screen.getByTestId('public-language-menu')) expect(menu.getByRole('button', { name: 'Deutsch' })).toHaveAttribute('aria-current', 'true') expect(menu.getByRole('button', { name: 'English' })).toHaveAttribute('aria-current', 'false') }) })