Playwright + axe-core
import AxeBuilder from '@axe-core/playwright';
// Check for missing labels
const results = await new AxeBuilder({ page })
.withRules(['label', 'label-title-only'])
.analyze();
// Find inputs without labels
const inputs = await page.$$('input, select, textarea');
for (const input of inputs) {
const id = await input.getAttribute('id');
const ariaLabel = await input.getAttribute('aria-label');
const hasLabel = id
? await page.$(`label[for="${id}"]`)
: await input.evaluate(
el => el.closest('label') !== null
);
if (!hasLabel && !ariaLabel) {
console.log('Missing label');
}
}Role-based Queries
// Playwright's getByLabel uses accessible names
const emailInput = page.getByLabel('Email Address');
await expect(emailInput).toBeVisible();
// Check required fields have aria-required
const required = await page.$$('[aria-required="true"]');
expect(required.length).toBeGreaterThan(0);
// Verify fieldset/legend for radio groups
const fieldset = page.locator('fieldset');
const legend = fieldset.locator('legend');
await expect(legend).toHaveText('Gender');
// Check aria-describedby links exist
const input = page.getByLabel('Email');
const describedBy = await input.getAttribute(
'aria-describedby'
);
if (describedBy) {
const hint = page.locator(`#${describedBy}`);
await expect(hint).toBeAttached();
}