Files
nexu-io--open-design/apps/web/tests/components/EntrySettingsMenu.language.test.tsx
wehub-resource-sync 070959e133
landing-page-staging / Deploy landing page to staging (push) Has been skipped
landing-page-ci / Validate landing page (push) Failing after 4s
visual-baseline / Capture visual baselines (push) Has been cancelled
bake-plugin-previews / Bake plugin previews (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:00:47 +08:00

101 lines
3.3 KiB
TypeScript

// @vitest-environment jsdom
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { EntrySettingsMenu } from '../../src/components/EntrySettingsMenu';
import { I18nProvider } from '../../src/i18n';
import type { AppConfig } from '../../src/types';
vi.mock('../../src/analytics/provider', async (importOriginal) => {
const actual = await importOriginal<typeof import('../../src/analytics/provider')>();
return { ...actual, useAnalytics: () => ({ track: vi.fn() }) };
});
const originalFetch = globalThis.fetch;
function jsonResponse(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { 'Content-Type': 'application/json' },
});
}
function baseConfig(overrides: Partial<AppConfig> = {}): AppConfig {
return {
mode: 'daemon',
agentId: null,
agentModels: {},
apiProtocol: 'anthropic',
apiProtocolConfigs: {},
apiKey: '',
baseUrl: '',
model: '',
theme: 'system',
...overrides,
} as AppConfig;
}
function renderMenu() {
return render(
<I18nProvider initial="en">
<EntrySettingsMenu
config={baseConfig()}
onThemeChange={vi.fn()}
onOpenSettings={vi.fn()}
/>
</I18nProvider>,
);
}
beforeEach(() => {
globalThis.fetch = vi.fn(async () => jsonResponse({})) as typeof fetch;
});
afterEach(() => {
cleanup();
globalThis.fetch = originalFetch;
vi.restoreAllMocks();
});
describe('EntrySettingsMenu language picker a11y', () => {
it('keeps one consistent menu model and hides the collapsed locale list from a11y/focus', () => {
const { container } = renderMenu();
fireEvent.click(screen.getByTestId('entry-settings-menu-trigger'));
// The picker trigger participates in the surrounding role="menu" popover as
// a menuitem that opens a submenu — not a listbox combobox.
const langTrigger = container.querySelector(
'.entry-settings-menu__select-trigger',
) as HTMLElement;
expect(langTrigger.getAttribute('role')).toBe('menuitem');
expect(langTrigger.getAttribute('aria-haspopup')).toBe('menu');
expect(langTrigger.getAttribute('aria-expanded')).toBe('false');
// No mixed listbox/option ARIA — locale choices are menuitemradios.
expect(container.querySelector('[role="listbox"]')).toBeNull();
expect(container.querySelector('[role="option"]')).toBeNull();
const panel = container.querySelector(
'.entry-settings-menu__select-panel',
) as HTMLElement;
expect(panel.getAttribute('role')).toBe('menu');
const radios = panel.querySelectorAll('[role="menuitemradio"]');
expect(radios.length).toBeGreaterThan(1);
expect(
Array.from(radios).filter((r) => r.getAttribute('aria-checked') === 'true'),
).toHaveLength(1);
// Collapsed: the list is inert, so the options stay out of the a11y tree
// and the tab order even though they remain mounted for the animation.
const list = container.querySelector(
'.entry-settings-menu__select-list',
) as HTMLElement;
expect(list.hasAttribute('inert')).toBe(true);
// Opening flips aria-expanded and lifts inert.
fireEvent.click(langTrigger);
expect(langTrigger.getAttribute('aria-expanded')).toBe('true');
expect(list.hasAttribute('inert')).toBe(false);
});
});