152 lines
4.7 KiB
TypeScript
152 lines
4.7 KiB
TypeScript
|
|
import { cleanup, render, screen } from '@testing-library/svelte';
|
||
|
|
import userEvent from '@testing-library/user-event';
|
||
|
|
import axe from 'axe-core';
|
||
|
|
import { afterEach, describe, expect, test, vi } from 'vitest';
|
||
|
|
import Feedback from '../../src/components/Feedback.svelte';
|
||
|
|
import ObjectBrowser from '../../src/components/ObjectBrowser.svelte';
|
||
|
|
import UploadPanel from '../../src/components/UploadPanel.svelte';
|
||
|
|
import WorkspaceHeader from '../../src/components/WorkspaceHeader.svelte';
|
||
|
|
|
||
|
|
afterEach(cleanup);
|
||
|
|
|
||
|
|
describe('accessible component behavior', () => {
|
||
|
|
test('root state has names, pressed state, and keyboard activation', async () => {
|
||
|
|
const onselect = vi.fn(async () => {});
|
||
|
|
const user = userEvent.setup();
|
||
|
|
const { container } = render(WorkspaceHeader, {
|
||
|
|
root: 'personal',
|
||
|
|
busy: false,
|
||
|
|
onselect
|
||
|
|
});
|
||
|
|
|
||
|
|
const personal = screen.getByRole('button', { name: 'Personal' });
|
||
|
|
const shared = screen.getByRole('button', { name: 'Shared' });
|
||
|
|
expect(personal.getAttribute('aria-pressed')).toBe('true');
|
||
|
|
expect(shared.getAttribute('aria-pressed')).toBe('false');
|
||
|
|
shared.focus();
|
||
|
|
await user.keyboard('{Enter}');
|
||
|
|
expect(onselect).toHaveBeenCalledWith('shared');
|
||
|
|
await expectNoSeriousViolations(container);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('feedback exposes live notices, alerts, and retry names', async () => {
|
||
|
|
const { container } = render(Feedback, {
|
||
|
|
notice: 'Uploaded file.txt',
|
||
|
|
error: 'Delete is pending',
|
||
|
|
uploadPending: true,
|
||
|
|
uploadFollowup: 'replay',
|
||
|
|
deletePending: true,
|
||
|
|
deleteFollowup: 'restart',
|
||
|
|
onuploadretry: async () => {},
|
||
|
|
ondeleteretry: async () => {}
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(screen.getByRole('status').textContent).toContain('Uploaded file.txt');
|
||
|
|
expect(screen.getByRole('alert').textContent).toContain('Delete is pending');
|
||
|
|
screen.getByRole('button', { name: 'Retry same upload' });
|
||
|
|
screen.getByRole('button', { name: 'Start new delete operation' });
|
||
|
|
await expectNoSeriousViolations(container);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('browser state names deletion controls and reports busy state', async () => {
|
||
|
|
const rendered = render(ObjectBrowser, browserProps());
|
||
|
|
|
||
|
|
expect(screen.getByRole('textbox', { name: 'Search this root' })).toBeTruthy();
|
||
|
|
expect(screen.getByRole('button', { name: 'Delete alpha.txt' })).toBeTruthy();
|
||
|
|
expect(rendered.container.querySelector('.list')?.getAttribute('aria-busy')).toBe(
|
||
|
|
'false'
|
||
|
|
);
|
||
|
|
await rendered.rerender({ ...browserProps(), busy: true });
|
||
|
|
expect(rendered.container.querySelector('.list')?.getAttribute('aria-busy')).toBe(
|
||
|
|
'true'
|
||
|
|
);
|
||
|
|
expect(
|
||
|
|
(screen.getByRole('textbox', {
|
||
|
|
name: 'Search this root'
|
||
|
|
}) as HTMLInputElement).disabled
|
||
|
|
).toBe(true);
|
||
|
|
expect(
|
||
|
|
(screen.getByRole('button', {
|
||
|
|
name: 'Delete alpha.txt'
|
||
|
|
}) as HTMLButtonElement).disabled
|
||
|
|
).toBe(true);
|
||
|
|
await expectNoSeriousViolations(rendered.container);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('post-delete focus moves to the logical surviving entry', async () => {
|
||
|
|
const onfocused = vi.fn();
|
||
|
|
const rendered = render(ObjectBrowser, {
|
||
|
|
...browserProps(),
|
||
|
|
onfocused
|
||
|
|
});
|
||
|
|
|
||
|
|
await rendered.rerender({
|
||
|
|
...browserProps(),
|
||
|
|
entries: [
|
||
|
|
{
|
||
|
|
name: 'beta.txt',
|
||
|
|
path: 'beta.txt',
|
||
|
|
is_prefix: false,
|
||
|
|
size: 4
|
||
|
|
}
|
||
|
|
],
|
||
|
|
focusPath: 'beta.txt',
|
||
|
|
onfocused
|
||
|
|
});
|
||
|
|
|
||
|
|
await vi.waitFor(() => {
|
||
|
|
expect(document.activeElement?.textContent).toContain('beta.txt');
|
||
|
|
});
|
||
|
|
expect(onfocused).toHaveBeenCalledOnce();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('upload input retains a native named keyboard alternative', async () => {
|
||
|
|
const { container } = render(UploadPanel, {
|
||
|
|
busy: false,
|
||
|
|
onfile: async () => {}
|
||
|
|
});
|
||
|
|
expect(screen.getByLabelText('Choose file').getAttribute('type')).toBe('file');
|
||
|
|
await expectNoSeriousViolations(container);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
function browserProps() {
|
||
|
|
return {
|
||
|
|
root: 'personal' as const,
|
||
|
|
entries: [
|
||
|
|
{
|
||
|
|
name: 'alpha.txt',
|
||
|
|
path: 'alpha.txt',
|
||
|
|
is_prefix: false,
|
||
|
|
size: 5
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: 'beta.txt',
|
||
|
|
path: 'beta.txt',
|
||
|
|
is_prefix: false,
|
||
|
|
size: 4
|
||
|
|
}
|
||
|
|
],
|
||
|
|
continuation: null,
|
||
|
|
partial: false,
|
||
|
|
busy: false,
|
||
|
|
query: '',
|
||
|
|
focusPath: null,
|
||
|
|
onquery: () => {},
|
||
|
|
onbrowse: async () => {},
|
||
|
|
onopen: async () => {},
|
||
|
|
ondelete: async () => {},
|
||
|
|
onfocused: () => {}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
async function expectNoSeriousViolations(container: HTMLElement) {
|
||
|
|
const results = await axe.run(container, {
|
||
|
|
rules: { 'color-contrast': { enabled: false } }
|
||
|
|
});
|
||
|
|
expect(
|
||
|
|
results.violations.filter(({ impact }) =>
|
||
|
|
impact === 'serious' || impact === 'critical'
|
||
|
|
)
|
||
|
|
).toEqual([]);
|
||
|
|
}
|