File size: 6,049 Bytes
473337c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
import { chromium, webkit } from 'playwright';
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';

const url = process.argv[2] ?? 'http://127.0.0.1:4173/';
const label = process.argv[3] ?? 'mascot-transparency';
const result = { url, checks: [], errors: [] };
const webkitRoot = process.env.WEBKIT_MINIBROWSER_ROOT;
const points = [[.04,.04], [.5,.04], [.96,.5], [.05,.85], [.5,.97], [.75,.7]];
for (const engine of ['chromium', 'webkit']) {
  const browser = engine === 'chromium'
    ? await chromium.launch({ executablePath: '/usr/bin/chromium', headless: true })
    : await webkit.launch({ headless: true, ...(webkitRoot ? {
      executablePath: webkitRoot + '/bin/MiniBrowser', env: { ...process.env,
        WEBKIT_EXEC_PATH: webkitRoot + '/bin', WEBKIT_INJECTED_BUNDLE_PATH: webkitRoot + '/lib',
        WEBKIT_INSPECTOR_RESOURCES_PATH: webkitRoot + '/share',
        LD_LIBRARY_PATH: webkitRoot + '/lib:' + webkitRoot + '/sys/lib:' + (process.env.WEBKIT_EXTRA_LIBS ?? ''),
      },
    } : {}) });
  try {
    for (const [width, height, scale] of [[1440,1000,1], [390,844,2]]) {
      const context = await browser.newContext({ viewport: { width, height }, deviceScaleFactor: scale });
      const page = await context.newPage();
      page.on('pageerror', e => result.errors.push(e.message));
      const figure = page.locator('.mascot-cat');
      const ready = async () => {
        await page.waitForFunction(() => window.browserPi?.terminal);
        await page.evaluate(() => window.browserPi.ready);
        await figure.waitFor({ state: 'visible' });
      };
      async function check(name) {
        const rect = await figure.boundingBox();
        const actual = await page.screenshot();
        // Compare the real backdrop at identical coordinates with the cat hidden.
        await figure.evaluate(el => { el.style.visibility = 'hidden'; });
        const backdrop = await page.screenshot();
        await figure.evaluate(el => { el.style.visibility = ''; });
        const pixels = await page.evaluate(async ({ actual, backdrop, rect, scale, points }) => {
          async function read(data) {
            const img = new Image(); img.src = 'data:image/png;base64,' + data; await img.decode();
            const canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = img.height;
            const ctx = canvas.getContext('2d'); ctx.drawImage(img,0,0);
            return points.map(([x,y]) => [...ctx.getImageData(Math.floor((rect.x+x*rect.width)*scale), Math.floor((rect.y+y*rect.height)*scale),1,1).data]);
          }
          const outside = { actual: await read(actual), backdrop: await read(backdrop) };
          points = [[.45,.3], [.235,.18], [.32,.42]];
          return { ...outside, cat: await read(actual) };
        }, { actual: actual.toString('base64'), backdrop: backdrop.toString('base64'), rect, scale, points });
        assert.deepEqual(pixels.actual,pixels.backdrop, `${engine} ${width} ${name}: opaque pixels outside silhouette`);
        const [fur, ear, eye] = pixels.cat;
        assert(fur[0] > 200 && fur[1] > 200 && fur[2] > 170, 'Cream fur must remain visible');
        assert(ear[0] > 200 && ear[0] > ear[1] + 20, 'Pink inner ear must remain visible');
        assert(eye.slice(0,3).every(v => v < 20), 'Black eye must be preserved, not color-keyed away');
        await fs.writeFile(`../validation/${label}-${engine}-${width}-${name}.png`, actual);
        result.checks.push({ engine, width, height, scale, name, passed: true });
      }
      await page.goto(url); await ready();
      // Pause timers before the first automatic hop so screenshots cannot hide a first-paint defect.
      await page.clock.install(); await page.clock.pauseAt(new Date());
      await check('cold-load');
      assert.equal(await figure.evaluate(el => getComputedStyle(el).mixBlendMode), 'normal');
      await figure.click();
      const hopping = await figure.evaluate(el => {
        const a = el.getAnimations().find(a => a.id === 'mascot-bump');
        if (!a) return false; a.pause(); a.currentTime = 170; return true;
      });
      assert(hopping, 'Click must still animate the cat');
      await check('mid-hop');
      await figure.evaluate(el => el.getAnimations().forEach(a => a.finish()));
      await check('after-hop');
      await page.emulateMedia({ reducedMotion: 'reduce' });
      await page.clock.resume();
      await page.reload(); await ready();
      await check('reduced-motion-reload');
      assert.equal(await figure.evaluate(el => el.getAnimations().length), 0);
      // A contrasting background proves actual clipping, not a dark-color cover-up.
      await page.addStyleTag({ content: '.mascot-cat { background: repeating-conic-gradient(#f57bc5 0 25%, #53d4ce 0 50%) 0 / 16px 16px !important; }' });
      const rect = await figure.boundingBox();
      const screenshot = await page.screenshot();
      const colors = await page.evaluate(async ({ data, rect, scale, points }) => {
        const img=new Image();img.src='data:image/png;base64,'+data;await img.decode();
        const canvas=document.createElement('canvas');canvas.width=img.width;canvas.height=img.height;
        const ctx=canvas.getContext('2d');ctx.drawImage(img,0,0);
        return points.map(([x,y])=>[...ctx.getImageData(Math.floor((rect.x+x*rect.width)*scale),Math.floor((rect.y+y*rect.height)*scale),1,1).data]);
      }, { data:screenshot.toString('base64'),rect,scale,points });
      for (const pixel of colors) assert(pixel[0] > 50 && pixel[1] > 100 && pixel[2] > 150, JSON.stringify(pixel));
      result.checks.push({engine,width,name:'patterned-backdrop',passed:true});
      await fs.writeFile(`../validation/${label}-${engine}-${width}-patterned-backdrop.png`,screenshot);
      await context.close();
    }
  } finally { await browser.close(); }
}
assert.deepEqual(result.errors, []);
result.passed = true;
await fs.writeFile(`../validation/${label}.json`,JSON.stringify(result,null,2)+'\n');
console.log(JSON.stringify(result));