Skip to content

color-contrast false negative with nested combination of stacking contexts #4629

Open
@dbjorge

Description

@dbjorge

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    color contrastColor contrast issuesfixBug fixesrulesIssue or false result from an axe-core rulesupport

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions