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
101 lines
3.3 KiB
TypeScript
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);
|
|
});
|
|
});
|