dequelabs / axe-core

Accessibility engine for automated Web UI testing
https://www.deque.com/axe/
Mozilla Public License 2.0
6.03k stars 785 forks source link

Color contrast miscalculated due to dark color-scheme background misinterpreted as #ffffff #4608

Open fdelapena opened 1 month ago

fdelapena commented 1 month ago

Product

axe-core

Product Version

4.10.0

Latest Version

Issue Description

Expectation

The report passes without getting insufficient contrast in a element when using dark color-scheme.

Actual

The report says the background color is #ffffff when it is likely #000000. The foreground color is calculated as #9e9eff. Additionally, the contrast filter seems to be ignored, where the link foreground should be calculated somehow close to #ffffff.

How to Reproduce

Set the system to dark mode. Crate a page with the following HTML and CSS and test it with DevTools axe extension (tested with 4.92.0) or Lighthouse (provided with Chromium):

<doctype html>
<html lang=en>
<meta charset=utf-8>
<meta name=description content="Some test">
<title>Test</title>
<p>Some <a href=//example.org>link example</a>.
html {
    color-scheme: light dark;
}

/* optional, secondary issue, seems to be ignored in results as well */
a {
    filter: contrast(10);
}

Additional context

Tested under latest Chromium on GNOME.

WilcoFiers commented 1 month ago

Interesting scenario. Thanks for raising this. You're right that axe is assuming the background is white. The difficulty here is that the computed background color for the HTML element will be transparent, regardless of the color-scheme. There are ways to figure out which contrast mode is used, like inject a text with a contrast filter on it, that's going to trigger DOM observer events, which can have unforeseen consequences.

@fdelapena have you seen this cause false positives on a live site? If so it would be nice to have that as an example to go with the issue.

dylanb commented 1 month ago

We could have an option you pass into analyze to specify the mode to be used for this default instead of white always.

also, this might work

const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
dbjorge commented 1 month ago

Reminder for ourselves that if we do use something like media query + color-scheme to infer a page canvas background color, that color-scheme can be specified in meta tags as well as CSS.

I think the bigger difficulty than determining which color scheme is in play might actually be determining which exact background color we should use for the color scheme. UAs in practice all use different values for their dark mode background canvas color. Today on the same machine/monitor, I see #121212 in Chrome, #1E1E1E in Safari, and #1C1C22 in Firefox. It gets more complicated still if we also want to extend that support to forced color modes. Anyone have any bright ideas for querying the color directly?

straker commented 1 month ago

This is similar to https://github.com/dequelabs/axe-core/issues/3605

dbjorge commented 1 month ago

This feels close to something that could work, but unfortunately it just resolves the default values of the system colors, not the system color values adjusted for UA settings and page color-scheme :(

function getCanvasColor(doc) {
  const canvasEl = doc.createElement('canvas')
  const ctx = canvasEl.getContext('2d')
  ctx.fillStyle = 'Canvas'
  return ctx.fillStyle // in chrome, returns '#ffffff' regardless of color scheme
}
fdelapena commented 1 month ago

@fdelapena have you seen this cause false positives on a live site? If so it would be nice to have that as an example to go with the issue.

Yes, it is currently live at https://abierta.cr/ , hope it helps.

Note that the site currently has a strict header-based policy that could prevent some JavaScript tests without using extensions to circumvent them.

xorgy commented 1 month ago

...have you seen this cause false positives on a live site? If so it would be nice to have that as an example to go with the issue.

This creates false positives with any document that doesn't override the default background color and link color in chrome with the default stylesheet... which is a few sites

xorgy commented 1 month ago

An alternative to @dbjorge's approach which will evaluate in the context of the page, is to set the html element's background to Canvas if it's not overridden, or alternatively to do something similar but with an element inserted into the document.

function getCanvasColor(doc) {
  const el = doc.createElement('div');
  div.style.display = 'none';
  div.style.backgroundColor = 'Canvas';
  document.firstElementChild.appendChild(el);
  const color = getComputedStyle(el).backgroundColor;
  el.remove();
  return color;
}

But obviously this adds DOM, which is clearly not what axe would like to do. Another option is to temporarily style the root with background: Canvas, measure it, and then set it back to whatever it was.