Playwright + axe-core
import AxeBuilder from '@axe-core/playwright';
// Check heading order
const results = await new AxeBuilder({ page })
.withRules([
'heading-order', // Sequential levels
'page-has-heading-one', // Has h1
'empty-heading' // No empty headings
])
.analyze();
// Get document outline using $$eval
const headings = await page.$$eval(
'h1, h2, h3, h4, h5, h6',
els => els.map(el => ({
level: parseInt(el.tagName[1]),
text: el.textContent?.trim()
}))
);
// Verify no skipped levels
let prevLevel = 0;
for (const h of headings) {
if (h.level > prevLevel + 1) {
console.log('Skipped: h' + prevLevel + ' to h' + h.level);
}
prevLevel = h.level;
}HeadingsMap Extension
// Browser extension for manual testing:
// "HeadingsMap" shows document outline
// Programmatic outline generation:
const getOutline = async (page) => {
return page.$$eval(
'h1, h2, h3, h4, h5, h6',
els => els.map(h => ({
tag: h.tagName,
text: h.textContent?.trim(),
// Check if heading is visible
visible: h.offsetParent !== null
}))
);
};
// Verify single h1
const h1Count = await page.locator('h1').count();
expect(h1Count).toBe(1);