test(accessibility): improve confirm dialog test coverage and refactor media state helper
Expand accessibility tests for ConfirmDialog to assert dialog semantics, ARIA attributes, and visible destructive actions using test IDs. Refactor expectMediaState helper to check both UI control state and underlying media signals for more robust playback state detection.
This commit is contained in:
parent
35dc5ae053
commit
3fc0938aec
3 changed files with 44 additions and 33 deletions
|
|
@ -30,7 +30,7 @@ export function ConfirmDialog({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalFrame open={isOpen} onClose={onClose} onKeyDown={handleKeyDown}>
|
<ModalFrame open={isOpen} onClose={onClose} onKeyDown={handleKeyDown} panelTestId="confirm-dialog-panel">
|
||||||
<ModalTitle>{title}</ModalTitle>
|
<ModalTitle>{title}</ModalTitle>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<p className="text-sm text-soft break-words">{message}</p>
|
<p className="text-sm text-soft break-words">{message}</p>
|
||||||
|
|
|
||||||
|
|
@ -39,14 +39,18 @@ test.describe('Accessibility smoke', () => {
|
||||||
// Open the confirm dialog by clicking the row delete button
|
// Open the confirm dialog by clicking the row delete button
|
||||||
await page.getByRole('button', { name: /^Delete sample\.pdf$/i }).first().click();
|
await page.getByRole('button', { name: /^Delete sample\.pdf$/i }).first().click();
|
||||||
|
|
||||||
// Title and dialog role visible
|
// Title and dialog semantics are present
|
||||||
const heading = page.getByRole('heading', { name: 'Delete Document' });
|
const heading = page.getByRole('heading', { name: 'Delete Document' });
|
||||||
await expect(heading).toBeVisible({ timeout: 10000 });
|
await expect(heading).toBeVisible({ timeout: 10000 });
|
||||||
const dialog = heading.locator('xpath=ancestor::*[@role="dialog"][1]');
|
const dialog = page.getByRole('dialog', { name: 'Delete Document' }).first();
|
||||||
await expect(dialog).toBeVisible();
|
await expect(dialog).toHaveAttribute('aria-modal', 'true');
|
||||||
|
|
||||||
|
// The visible panel content is rendered and includes destructive action
|
||||||
|
const panel = page.getByTestId('confirm-dialog-panel').first();
|
||||||
|
await expect(panel).toBeVisible();
|
||||||
|
|
||||||
// Has a destructive action (Delete)
|
// Has a destructive action (Delete)
|
||||||
await expect(dialog.getByRole('button', { name: 'Delete' })).toBeVisible();
|
await expect(panel.getByRole('button', { name: /^Delete$/ })).toBeVisible();
|
||||||
|
|
||||||
// Close with Escape to avoid deleting test data
|
// Close with Escape to avoid deleting test data
|
||||||
await page.keyboard.press('Escape');
|
await page.keyboard.press('Escape');
|
||||||
|
|
|
||||||
|
|
@ -497,37 +497,44 @@ export async function expectProcessingTransition(page: Page) {
|
||||||
|
|
||||||
// Expect navigator.mediaSession.playbackState to equal given state
|
// Expect navigator.mediaSession.playbackState to equal given state
|
||||||
export async function expectMediaState(page: Page, state: 'playing' | 'paused') {
|
export async function expectMediaState(page: Page, state: 'playing' | 'paused') {
|
||||||
// WebKit (and sometimes other engines) may not reliably update navigator.mediaSession.playbackState.
|
// Engines can intermittently miss mediaSession updates. Accept either:
|
||||||
// Fallback heuristics:
|
// - expected UI control state (Pause button for playing, Play button for paused), or
|
||||||
// 1. Prefer mediaSession if it matches desired state.
|
// - underlying media state from mediaSession/audio element signals.
|
||||||
// 2. Otherwise inspect any <audio> element: use paused flag and currentTime progression.
|
const desiredButtonName = state === 'playing' ? 'Pause' : 'Play';
|
||||||
// 3. Allow short grace period for first frame to advance.
|
await expect
|
||||||
// 4. If neither detectable, keep polling until timeout.
|
.poll(async () => {
|
||||||
await page.waitForFunction((desired) => {
|
const uiMatches = await page
|
||||||
try {
|
.getByRole('button', { name: desiredButtonName })
|
||||||
const msState = (navigator.mediaSession && navigator.mediaSession.playbackState) || '';
|
.first()
|
||||||
if (msState === desired) return true;
|
.isVisible()
|
||||||
|
.catch(() => false);
|
||||||
|
if (uiMatches) return true;
|
||||||
|
|
||||||
const audio: HTMLAudioElement | null = document.querySelector('audio');
|
return page.evaluate((desired) => {
|
||||||
if (audio) {
|
try {
|
||||||
// Track advancement by storing last time on the element dataset
|
const msState = (navigator.mediaSession && navigator.mediaSession.playbackState) || '';
|
||||||
const last = parseFloat(audio.dataset.lastTime || '0');
|
if (msState === desired) return true;
|
||||||
const curr = audio.currentTime;
|
|
||||||
audio.dataset.lastTime = String(curr);
|
|
||||||
|
|
||||||
if (desired === 'playing') {
|
const audio: HTMLAudioElement | null = document.querySelector('audio');
|
||||||
// Consider playing if not paused AND time has advanced at least a tiny amount
|
if (audio) {
|
||||||
if (!audio.paused && curr > 0 && curr > last) return true;
|
const w = window as Window & { __openreaderLastAudioTime?: number };
|
||||||
} else {
|
const last = w.__openreaderLastAudioTime ?? -1;
|
||||||
// paused target
|
const curr = audio.currentTime;
|
||||||
if (audio.paused) return true;
|
w.__openreaderLastAudioTime = curr;
|
||||||
|
|
||||||
|
if (desired === 'playing') {
|
||||||
|
if (!audio.paused && curr > 0 && curr > last) return true;
|
||||||
|
} else if (audio.paused) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}, state);
|
||||||
return false;
|
}, { timeout: 45000 })
|
||||||
} catch {
|
.toBe(true);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}, state);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use Navigator to go to a specific page number (PDF)
|
// Use Navigator to go to a specific page number (PDF)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue