mosaic-headless 1.6.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -34,6 +34,14 @@ AUDIT what only a browser knows, checked whether or not it was declared: fon
34
34
  fallback, tracking against script, text contrast, horizontal overflow,
35
35
  clipped text, and line measure.
36
36
 
37
+ Its scope is stated rather than implied: readings are taken with the page
38
+ at rest at scroll-top, so a defect that only exists part-way down a scroll
39
+ - a fixed element crossing a band of a different colour - is outside what
40
+ this pass sees. The contrast check does hit-test the ground behind a fixed
41
+ or sticky element rather than walking its ancestors, so it would find such
42
+ a case if it were looking there; it simply is not scrolling. `verify_intro`
43
+ samples over time; nothing here yet samples over scroll.
44
+
37
45
  Exit status is non-zero if any declaration is OVERRIDDEN or any audit finding is
38
46
  rated `error`.
39
47
  """
@@ -307,11 +315,31 @@ PROBE = r"""
307
315
 
308
316
  // Effective background behind a text node, for contrast. Walks up until something
309
317
  // is actually painted; a transparent parent is not a background.
318
+ const solid = b => {
319
+ const n = (b.match(/[\d.]+/g) || []).map(Number);
320
+ return (n.length >= 3 && (n.length < 4 || n[3] > 0.55)) ? n.slice(0, 3) : null;
321
+ };
310
322
  const bgOf = el => {
323
+ const cs0 = getComputedStyle(el);
324
+ // A FIXED or STICKY element does not sit on its ancestors - it sits on whatever
325
+ // the page has scrolled underneath it, and the ancestor chain knows nothing
326
+ // about that. The clause index on this page is fixed to the left margin in a
327
+ // faint grey; when an accent-coloured band scrolls behind it the text becomes
328
+ // unreadable, and walking parents reports the paper ground it was declared on
329
+ // and passes. Hit-testing is the only way to ask what is actually back there.
330
+ if (cs0.position === 'fixed' || cs0.position === 'sticky') {
331
+ const r = el.getBoundingClientRect();
332
+ const x = Math.min(Math.max(r.left + r.width / 2, 1), innerWidth - 1);
333
+ const y = Math.min(Math.max(r.top + r.height / 2, 1), innerHeight - 1);
334
+ for (const under of document.elementsFromPoint(x, y)) {
335
+ if (under === el || el.contains(under) || under.contains(el)) continue;
336
+ const c = solid(getComputedStyle(under).backgroundColor);
337
+ if (c) return c;
338
+ }
339
+ }
311
340
  for (let p = el; p; p = p.parentElement) {
312
- const b = getComputedStyle(p).backgroundColor;
313
- const n = (b.match(/[\d.]+/g) || []).map(Number);
314
- if (n.length >= 3 && (n.length < 4 || n[3] > 0.55)) return n.slice(0, 3);
341
+ const c = solid(getComputedStyle(p).backgroundColor);
342
+ if (c) return c;
315
343
  }
316
344
  return [255, 255, 255];
317
345
  };
@@ -363,8 +391,22 @@ PROBE = r"""
363
391
  sample: own.slice(0, 24)});
364
392
  }
365
393
 
366
- const fg = (cs.color.match(/[\d.]+/g) || []).map(Number).slice(0, 3);
367
- if (fg.length === 3) {
394
+ // Text can be painted by something other than `color`. A headline using
395
+ // `background-clip:text` sets `color:transparent`, and a naive parse reads
396
+ // rgba(0,0,0,0) as pure black - which then scores a perfect contrast ratio
397
+ // against any light ground. That is a blind spot scoring itself as a pass, so
398
+ // it gets its own finding instead: the check cannot run here, and says so.
399
+ const rgba = (cs.color.match(/[\d.]+/g) || []).map(Number);
400
+ if (rgba.length === 4 && rgba[3] < 0.05) {
401
+ out.audit.push({check: 'TEXT_CLIP', level: 'warn', node: id,
402
+ detail: 'color is ' + cs.color + '; painted by ' +
403
+ (cs.backgroundClip === 'text' ? 'background-clip:text'
404
+ : 'something else') +
405
+ ' - contrast NOT checked here',
406
+ sample: own.slice(0, 24)});
407
+ }
408
+ const fg = rgba.slice(0, 3);
409
+ if (fg.length === 3 && !(rgba.length === 4 && rgba[3] < 0.05)) {
368
410
  const r = ratio(fg, bgOf(el));
369
411
  const large = size >= 24 || (size >= 18.66 && px(cs.fontWeight) >= 700);
370
412
  const need = large ? 3.0 : 4.5;
@@ -577,8 +619,12 @@ def main():
577
619
  f.get("sample", "")])
578
620
  print("wrote %s (%d findings)" % (a.audit, len(audit)))
579
621
 
580
- print("\n%s" % ("PASS - every comparable declaration is what the browser "
581
- "computed, and the audit is clean"
622
+ # "clean" was overstating it once the audit began reporting blind spots as
623
+ # warnings. A run carrying two labelled unknowns is not a run carrying none, and
624
+ # the summary line is the part people read.
625
+ print("\n%s" % (("PASS - every comparable declaration is what the browser "
626
+ "computed; 0 audit errors, %d warnings"
627
+ % (len(audit) - len(errors)))
582
628
  if not hard else
583
629
  "FAIL - %d overridden declarations, %d audit errors"
584
630
  % (counts.get("OVERRIDDEN", 0), len(errors))))