Playwright
// Get DOM order vs visual order using $$eval
const elements = await page.$$eval(
'[data-testid="accessible-layout"] *',
els => els.map(el => ({
tag: el.tagName,
text: el.textContent?.slice(0, 30),
// Get visual position
rect: el.getBoundingClientRect()
}))
);
// Sort by visual position (top to bottom)
const visualOrder = [...elements].sort(
(a, b) => a.rect.top - b.rect.top ||
a.rect.left - b.rect.left
);
// Compare DOM order to visual order
for (let i = 0; i < elements.length; i++) {
if (elements[i].text !== visualOrder[i].text) {
console.log('Order mismatch at position', i);
console.log('DOM:', elements[i].text);
console.log('Visual:', visualOrder[i].text);
}
}axe-core Rules
import AxeBuilder from '@axe-core/playwright';
// Note: axe-core has limited ability to detect
// CSS reordering issues automatically
// Manual check: Tab through and verify order
const tabOrder = [];
let prevFocused = null;
for (let i = 0; i < 20; i++) {
await page.keyboard.press('Tab');
const focused = await page.$eval(
':focus', el => el.textContent
);
if (focused !== prevFocused) {
tabOrder.push(focused);
prevFocused = focused;
}
}
console.log('Tab order:', tabOrder);
// Check for CSS properties that break order
const reorderingCSS = await page.$$eval('*', els =>
els.filter(el => {
const style = getComputedStyle(el);
return style.order !== '0' ||
style.flexDirection.includes('reverse');
}).map(el => el.outerHTML.slice(0, 100))
);