Description
Product
axe-core
Product Version
4.10.2
Latest Version
- I have tested the issue with the latest version of the product
Issue Description
We received a customer report recently of a color-contrast false-negative where it reported a bgOverlap
incomplete
for a case that it ought to have been able to detect as a violation. This happens due to axe's grid machinery calculating an incorrect stacking order for the elements in question; it incorrectly determines that one of the ancestor container elements is being rendered on top of the leaf descendant element, which is inconsistent with the actual browser paint order (and the order reported by document.elementsFromPoint
).
The specific customer repro is private, but the test cases below represent minimal repros of the same underlying issue. In the motivating example, this pattern was part of a more complicated interactive grid control.
Expectation
The suggested new test cases below should all be satisfied
Actual
The test cases are not satisfied; the integration tests incomplete
with bgOverlap
and the unit test shows an incorrect stack order calculation:
1) should correctly order deeply nested stacking contexts
dom.getElementStack stack order
AssertionError: expected [ '2', 'target', '3', '1', 'fixture' ] to deeply equal [ 'target', '3', '2', '1', 'fixture' ]
at Context.<anonymous> (test/commons/dom/get-element-stack.js:434:14)
How to Reproduce
Suggested new case for test/commons/dom/get-element-stack.js
it('should correctly order nested combinations of positioning and stacking contexts', () => {
fixture.innerHTML = `
<div id="1" style="position: relative; z-index: 4; width: 200px; height: 200px;">
<div id="2" style="position: absolute;">
<div id="3" style="transform: translate3d(0px, 0px, 0px);">
<span id="target">text</span>
</div>
</div>
</div>
`;
axe.testUtils.flatTreeSetup(fixture);
const target = fixture.querySelector('#target');
const stack = mapToIDs(getElementStack(target));
// matches browsers' document.elementsFromPoint behavior
assert.deepEqual(stack, ['target', '3', '2', '1', 'fixture']);
});
Suggested new cases for test/integration/rules/color-contrast/color-contrast.html
Updating color-contrast.json
with #fail13
, #fail14
, #pass24
, #pass25
:
<div style="position: relative; z-index: 1; width: 200px; height: 200px;">
<div style="position: absolute;">
<div style="transform: translate3d(0px, 0px, 0px);">
<span id="fail13" style="background-color: #111; color: #000">low contrast</span>
<span id="pass24" style="background-color: #fff; color: #000">ok contrast</span>
</div>
</div>
</div>
<div style="position: relative; z-index: 1; width: 200px; height: 200px;">
<div style="position: absolute;">
<div style="opacity: 0.99;">
<span id="fail14" style="background-color: #111; color: #000">low contrast</span>
<span id="pass25" style="background-color: #fff; color: #000">ok contrast</span>
</div>
</div>
</div>
Additional context
We think this is probably the same root cause as #4350 (where it causes a target-size false positive instead of this color-contrast false negative). We started working on this in #4351 but didn't complete before hitting our timebox.
Validating if a specific element is affected
Because we think the root cause is the same as #4350, you can use the same script I posted there to validate whether a specific element is likely impacted by this bug:
// Usage: Right click -> inspect the element suspected of being a false negative, then run this code snippet in the F12 dev console
function testForInconsistentElmStack(elementOrSelector) {
const elm = (typeof elementOrSelector === 'string') ? document.querySelector(elementOrSelector) : elementOrSelector;
const elmRect = elm.getBoundingClientRect();
const elmX = elmRect.left + elmRect.width / 2;
const elmY = elmRect.top + elmRect.height / 2;
const prettyStack = elms => JSON.stringify(elms.map(elm => axe.utils.getSelector(elm)), null, 2);
axe.teardown()
try {
axe.setup()
const axeStack = prettyStack(axe.commons.dom.getElementStack(elm));
const rawUaStack = document.elementsFromPoint(elmX, elmY);
const uaStack = prettyStack(rawUaStack);
if (rawUaStack[0] !== elm) {
console.log('The user agent thinks this element is obscured.');
}
if (axeStack !== uaStack) {
console.log(`Axe stack:\n${axeStack}\nUA stack:\n${uaStack}`);
console.log('Axe and UA stacks do not match. This is probably a repro of axe-core#4629 / #4350');
} else {
console.log(`Stack:\n${axeStack}`);
console.log('Axe and UA stacks match. This is probably *not* a repro of axe-core#4629 / #4350');
}
} finally {
axe.teardown()
}
}
testForInconsistentElmStack($0)