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([]); }