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
6 changes: 5 additions & 1 deletion frontend/src/i18n/__tests__/i18n.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,11 @@ describe('i18n configuration', () => {
});

it('uses each registered language for the document and i18next', async () => {
expect(SUPPORTED_LANGUAGES).toEqual(LANGUAGE_REGISTRY.map(({ code }) => code));
// Pinned as a literal, not derived from LANGUAGE_REGISTRY: comparing the
// list to the map that defines it passes however the map changes
// (Sprint Review 5611893458 on #2399). v2/__tests__/languageSurfaces.test.tsx
// carries the same pin for the three surfaces that read the registry.
expect(SUPPORTED_LANGUAGES).toEqual(['en', 'zh-CN']);
for (const { code } of LANGUAGE_REGISTRY) {
await act(async () => { await i18n.changeLanguage(code); });
expect(i18n.resolvedLanguage).toBe(code);
Expand Down
18 changes: 16 additions & 2 deletions frontend/src/v2/__tests__/V2SettingsPage.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
import i18n, { i18nReady } from '../../i18n';
import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import i18n, { i18nReady, LANGUAGE_REGISTRY } from '../../i18n';
import axios from '../../utils/axiosConfig';
import { MemoryRouter } from 'react-router-dom';
import { AuthContext } from '../../context/AuthContext';
Expand Down Expand Up @@ -90,6 +90,20 @@ describe('V2SettingsPage', () => {
await act(async () => { await i18n.changeLanguage('en'); });
});

// Pinned as a literal AND compared with the registry: the first assertion
// fails when the shipped languages change, the second when this surface
// stops reading LANGUAGE_REGISTRY and drifts from it.
test('builds the language radios from the registry, and only those', async () => {
await i18nReady;
(axios.get as jest.Mock).mockResolvedValue({ data: { hasToken: false } });
renderSettings();

const radios = within(screen.getByRole('radiogroup', { name: 'Language' })).getAllByRole('radio');
const labels = radios.map((radio) => radio.textContent);
expect(labels).toEqual(['English', '中文']);
expect(labels).toEqual(LANGUAGE_REGISTRY.map(({ label }) => label));
});

test('keeps every folded settings concern visible on one page', () => {
(axios.get as jest.Mock).mockResolvedValue({ data: { hasToken: false } });
renderSettings();
Expand Down
122 changes: 122 additions & 0 deletions frontend/src/v2/__tests__/languageSurfaces.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import React from 'react';
import { act, fireEvent, render, screen, within } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import axios from 'axios';
import i18n, { i18nReady, LANGUAGE_REGISTRY, LANGUAGE_STORAGE_KEY } from '../../i18n';
import { useAuth } from '../../context/AuthContext';
import V2LangSwitch from '../components/V2LangSwitch';
import V2NavRail from '../components/V2NavRail';
import V2LandingPage from '../landing/V2LandingPage';

jest.mock('axios');
jest.mock('../../context/AuthContext', () => ({
useAuth: jest.fn(),
}));
jest.mock('../hooks/useV2PodAttention', () => ({
useV2PodAttention: () => ({ attentionByPod: {}, totalNeedsYou: 0 }),
}));
jest.mock('../components/V2AccountMenu', () => {
const MockV2AccountMenu = () => <div>account menu</div>;
MockV2AccountMenu.displayName = 'MockV2AccountMenu';
return { __esModule: true, default: MockV2AccountMenu };
});
jest.mock('../components/V2FeedbackMenu', () => {
const MockV2FeedbackMenu = () => <div>feedback menu</div>;
MockV2FeedbackMenu.displayName = 'MockV2FeedbackMenu';
return { __esModule: true, default: MockV2FeedbackMenu };
});

// Written out on purpose, NOT derived from LANGUAGE_REGISTRY. TASK-318 left one
// registry feeding the switcher, Settings and <html lang>, and its own check
// compared SUPPORTED_LANGUAGES to the same map that defines it — an assertion
// that moves with whatever it is meant to pin (Sprint Review 5611893458).
// Changing the shipped languages has to fail here first, so that whoever adds
// one is sent through every surface below.
const EXPECTED_LANGUAGES = [
{ code: 'en', label: 'English', short: 'EN' },
{ code: 'zh-CN', label: '中文', short: '中文' },
];

const mockUseAuth = useAuth as jest.Mock;
const mockAxiosGet = axios.get as jest.Mock;

// The trigger and menu are labelled through i18n, so their accessible names
// change with the active language. Role alone is the stable handle here.
const openSwitcherMenu = (trigger: HTMLElement) => {
fireEvent.click(trigger);
return screen.getByRole('listbox');
};

describe('language surfaces derive from the registry', () => {
beforeAll(async () => {
await i18nReady;
});

beforeEach(async () => {
jest.clearAllMocks();
mockUseAuth.mockReturnValue({ isAuthenticated: false, user: null });
mockAxiosGet.mockResolvedValue({ data: {} });
localStorage.removeItem(LANGUAGE_STORAGE_KEY);
await act(async () => {
await i18n.changeLanguage('en');
});
});

afterAll(async () => {
localStorage.removeItem(LANGUAGE_STORAGE_KEY);
await i18n.changeLanguage('en');
});

it('ships exactly the pinned languages', () => {
expect(LANGUAGE_REGISTRY.map(({ code, label, short }) => ({ code, label, short })))
.toEqual(EXPECTED_LANGUAGES);
});

it('offers every registered language in the switcher menu, and nothing else', () => {
render(<V2LangSwitch />);

const options = within(openSwitcherMenu(screen.getByRole('button', { name: 'Language' })))
.getAllByRole('option');
expect(options.map((option) => option.textContent?.replace('✓', '').trim()))
.toEqual(EXPECTED_LANGUAGES.map(({ label }) => label));
});

it('switches to each registered language from the switcher menu', async () => {
render(<V2LangSwitch />);

const trigger = screen.getByRole('button', { name: 'Language' });

for (const { code, label, short } of EXPECTED_LANGUAGES) {
// The trigger click must settle before the option is queried, so opening
// the menu stays outside act() — fireEvent already flushes it.
const option = within(openSwitcherMenu(trigger)).getByRole('button', { name: label });
// eslint-disable-next-line no-await-in-loop
await act(async () => {
fireEvent.click(option);
});
expect(i18n.resolvedLanguage).toBe(code);
expect(document.documentElement.lang).toBe(code);
expect(trigger).toHaveTextContent(short);
}
});

it('keeps the language menu on the rail', () => {
render(
<MemoryRouter>
<V2NavRail needsYouCount={0} />
</MemoryRouter>,
);

expect(screen.getByRole('button', { name: 'Language' })).toBeInTheDocument();
});

it('keeps the language menu on the landing nav', () => {
render(
<MemoryRouter>
<V2LandingPage />
</MemoryRouter>,
);

expect(screen.getAllByRole('button', { name: 'Language' }).length).toBeGreaterThan(0);
});
});
Loading