summaryrefslogtreecommitdiff
path: root/frontend/scripts/ui-verify.mjs
blob: 1f53e9944a1baf914aaa0eeeb33d8138eeab3927 (plain)
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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
// Playwright visual/overflow verification: real browser, three widths,
// zero pageerror + zero document horizontal overflow asserted per page.
// Usage: node frontend/scripts/ui-verify.mjs <distDir> <outDir> [routesCsv]
import { chromium } from 'playwright';
import { mkdirSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import http from 'node:http';
import { createReadStream } from 'node:fs';

const dist = path.resolve(process.argv[2] || 'dist');
const outDir = path.resolve(process.argv[3] || '../artifacts/ui-shadcn');
const routes = (process.argv[4] || 'login,register,reset')
  .split(',')
  .map(function (s) { return s.trim(); })
  .filter(Boolean);

mkdirSync(outDir, { recursive: true });

const MIME = {
  '.html': 'text/html',
  '.js': 'text/javascript',
  '.css': 'text/css',
  '.svg': 'image/svg+xml',
  '.png': 'image/png',
  '.woff2': 'font/woff2',
  '.json': 'application/json'
};

// tiny static server for the built dist (hash router friendly, /api 404s)
const server = http.createServer(function (req, res) {
  const url = req.url || '/';
  if (url.startsWith('/api')) {
    res.writeHead(404, { 'content-type': 'application/json' });
    res.end(JSON.stringify({ error: { code: 'not_found', message: 'no backend in visual probe' } }));
    return;
  }
  let file = url.split('?')[0].split('#')[0];
  if (file === '/' || file === '') file = '/index.html';
  var responded = false;
  const send = function (fp, isFallback) {
    const stream = createReadStream(fp);
    stream.on('open', function () {
      if (responded) return;
      responded = true;
      res.writeHead(200, { 'content-type': MIME[path.extname(fp)] || 'application/octet-stream' });
      stream.pipe(res);
    });
    stream.on('error', function () {
      if (isFallback) {
        if (!responded) {
          responded = true;
          res.writeHead(404);
          res.end();
        }
        return;
      }
      send(path.join(dist, 'index.html'), true);
    });
  };
  send(path.join(dist, file));
});
await new Promise(function (res) { server.listen(0, '127.0.0.1', res); });
const port = server.address().port;

const widths = [375, 768, 1440];
const results = {};
const browser = await chromium.launch();

for (const route of routes) {
  const url = 'http://127.0.0.1:' + port + '/#' + route;
  for (const w of widths) {
    const context = await browser.newContext({ viewport: { width: w, height: Math.round(w * 2) } });
    const page = await context.newPage();
    const pageErrors = [];
    page.on('pageerror', function (e) { pageErrors.push(String(e)); });
    await page.goto(url, { waitUntil: 'networkidle' });
    await page.waitForTimeout(400);
    const m = await page.evaluate(function () {
      return {
        scrollWidth: document.documentElement.scrollWidth,
        clientWidth: document.documentElement.clientWidth
      };
    });
    const overflow = Math.max(0, m.scrollWidth - m.clientWidth);
    const bg = await page.evaluate(function () { return getComputedStyle(document.body).backgroundColor; });
    const fg = await page.evaluate(function () { return getComputedStyle(document.body).color; });
    const shot = path.join(outDir, route + '-' + w + '.png');
    await page.screenshot({ path: shot, fullPage: true });
    results[route + '@' + w] = {
      overflow: overflow,
      clientWidth: m.clientWidth,
      scrollWidth: m.scrollWidth,
      pageErrors: pageErrors,
      bg: bg,
      fg: fg,
      shot: path.relative(process.cwd(), shot)
    };
    await context.close();
  }
}

await browser.close();
server.close();
writeFileSync(path.join(outDir, 'overflow-probe.json'), JSON.stringify(results, null, 2));
let bad = 0;
for (const k of Object.keys(results)) {
  const r = results[k];
  const ok = r.overflow === 0 && r.pageErrors.length === 0 && r.bg !== r.fg;
  console.log((ok ? 'OK   ' : 'FAIL ') + k + ': overflow=' + r.overflow + ' pageErrors=' + r.pageErrors.length + ' bg=' + r.bg + ' fg=' + r.fg);
  if (!ok) bad++;
}
process.exit(bad === 0 ? 0 : 1);