@mozole/cli 1.4.2 → 2.0.0-b
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.
- package/LICENSE +16 -4
- package/README.md +85 -89
- package/dist/cli.d.ts +2 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +204 -157
- package/dist/cli.js.map +1 -1
- package/dist/commands/adopt.d.ts +5 -8
- package/dist/commands/adopt.d.ts.map +1 -1
- package/dist/commands/adopt.js +49 -812
- package/dist/commands/adopt.js.map +1 -1
- package/dist/commands/doctor.d.ts +1 -7
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +69 -377
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/new.d.ts +8 -10
- package/dist/commands/new.d.ts.map +1 -1
- package/dist/commands/new.js +32 -149
- package/dist/commands/new.js.map +1 -1
- package/dist/commands/phase.d.ts +11 -0
- package/dist/commands/phase.d.ts.map +1 -0
- package/dist/commands/phase.js +106 -0
- package/dist/commands/phase.js.map +1 -0
- package/dist/commands/prototype.d.ts +6 -0
- package/dist/commands/prototype.d.ts.map +1 -0
- package/dist/commands/prototype.js +28 -0
- package/dist/commands/prototype.js.map +1 -0
- package/dist/commands/repomap.d.ts +6 -0
- package/dist/commands/repomap.d.ts.map +1 -0
- package/dist/commands/repomap.js +71 -0
- package/dist/commands/repomap.js.map +1 -0
- package/dist/commands/test.d.ts +6 -0
- package/dist/commands/test.d.ts.map +1 -0
- package/dist/commands/test.js +115 -0
- package/dist/commands/test.js.map +1 -0
- package/dist/commands/ui.d.ts +1 -37
- package/dist/commands/ui.d.ts.map +1 -1
- package/dist/commands/ui.js +75 -317
- package/dist/commands/ui.js.map +1 -1
- package/dist/commands/verify.d.ts +12 -7
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +148 -65
- package/dist/commands/verify.js.map +1 -1
- package/dist/qa/a11y.d.ts +24 -0
- package/dist/qa/a11y.d.ts.map +1 -0
- package/dist/qa/a11y.js +124 -0
- package/dist/qa/a11y.js.map +1 -0
- package/dist/qa/breakpoints.d.ts +16 -0
- package/dist/qa/breakpoints.d.ts.map +1 -0
- package/dist/qa/breakpoints.js +45 -0
- package/dist/qa/breakpoints.js.map +1 -0
- package/dist/qa/contract.d.ts +21 -0
- package/dist/qa/contract.d.ts.map +1 -0
- package/dist/qa/contract.js +116 -0
- package/dist/qa/contract.js.map +1 -0
- package/dist/qa/index.d.ts +6 -0
- package/dist/qa/index.d.ts.map +1 -0
- package/dist/qa/index.js +6 -0
- package/dist/qa/index.js.map +1 -0
- package/dist/qa/probe.d.ts +48 -0
- package/dist/qa/probe.d.ts.map +1 -0
- package/dist/qa/probe.js +206 -0
- package/dist/qa/probe.js.map +1 -0
- package/dist/qa/runner.d.ts +27 -32
- package/dist/qa/runner.d.ts.map +1 -1
- package/dist/qa/runner.js +286 -336
- package/dist/qa/runner.js.map +1 -1
- package/dist/scaffold/agents.d.ts +5 -0
- package/dist/scaffold/agents.d.ts.map +1 -0
- package/dist/scaffold/agents.js +84 -0
- package/dist/scaffold/agents.js.map +1 -0
- package/dist/scaffold/backend.d.ts +6 -8
- package/dist/scaffold/backend.d.ts.map +1 -1
- package/dist/scaffold/backend.js +318 -199
- package/dist/scaffold/backend.js.map +1 -1
- package/dist/scaffold/design.d.ts +4 -3
- package/dist/scaffold/design.d.ts.map +1 -1
- package/dist/scaffold/design.js +38 -47
- package/dist/scaffold/design.js.map +1 -1
- package/dist/scaffold/flagship.d.ts +8 -0
- package/dist/scaffold/flagship.d.ts.map +1 -0
- package/dist/scaffold/flagship.js +337 -0
- package/dist/scaffold/flagship.js.map +1 -0
- package/dist/scaffold/index.d.ts +9 -0
- package/dist/scaffold/index.d.ts.map +1 -0
- package/dist/scaffold/index.js +48 -0
- package/dist/scaffold/index.js.map +1 -0
- package/dist/scaffold/phases.d.ts +10 -0
- package/dist/scaffold/phases.d.ts.map +1 -0
- package/dist/scaffold/phases.js +165 -0
- package/dist/scaffold/phases.js.map +1 -0
- package/dist/scaffold/repomap.d.ts +6 -0
- package/dist/scaffold/repomap.d.ts.map +1 -0
- package/dist/scaffold/repomap.js +89 -0
- package/dist/scaffold/repomap.js.map +1 -0
- package/dist/scaffold/standard.d.ts +13 -0
- package/dist/scaffold/standard.d.ts.map +1 -0
- package/dist/scaffold/standard.js +371 -0
- package/dist/scaffold/standard.js.map +1 -0
- package/dist/scaffold/tokens.d.ts +3 -0
- package/dist/scaffold/tokens.d.ts.map +1 -0
- package/dist/scaffold/tokens.js +126 -0
- package/dist/scaffold/tokens.js.map +1 -0
- package/dist/server/manager.d.ts +23 -0
- package/dist/server/manager.d.ts.map +1 -0
- package/dist/server/manager.js +96 -0
- package/dist/server/manager.js.map +1 -0
- package/dist/utils/ai-trace.d.ts +8 -0
- package/dist/utils/ai-trace.d.ts.map +1 -0
- package/dist/utils/ai-trace.js +69 -0
- package/dist/utils/ai-trace.js.map +1 -0
- package/dist/utils/browser.d.ts +10 -0
- package/dist/utils/browser.d.ts.map +1 -0
- package/dist/utils/browser.js +127 -0
- package/dist/utils/browser.js.map +1 -0
- package/dist/utils/fs.d.ts +11 -0
- package/dist/utils/fs.d.ts.map +1 -0
- package/dist/utils/fs.js +166 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/git.d.ts +9 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +69 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/process.d.ts +21 -0
- package/dist/utils/process.d.ts.map +1 -0
- package/dist/utils/process.js +120 -0
- package/dist/utils/process.js.map +1 -0
- package/dist/version.d.ts +21 -36
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +21 -44
- package/dist/version.js.map +1 -1
- package/package.json +19 -38
- package/CHANGELOG.md +0 -30
- package/dist/commands/cce.d.ts +0 -7
- package/dist/commands/cce.d.ts.map +0 -1
- package/dist/commands/cce.js +0 -37
- package/dist/commands/cce.js.map +0 -1
- package/dist/commands/logs.d.ts +0 -3
- package/dist/commands/logs.d.ts.map +0 -1
- package/dist/commands/logs.js +0 -39
- package/dist/commands/logs.js.map +0 -1
- package/dist/commands/qa.d.ts +0 -12
- package/dist/commands/qa.d.ts.map +0 -1
- package/dist/commands/qa.js +0 -143
- package/dist/commands/qa.js.map +0 -1
- package/dist/commands/stats.d.ts +0 -37
- package/dist/commands/stats.d.ts.map +0 -1
- package/dist/commands/stats.js +0 -169
- package/dist/commands/stats.js.map +0 -1
- package/dist/config/project.d.ts +0 -51
- package/dist/config/project.d.ts.map +0 -1
- package/dist/config/project.js +0 -81
- package/dist/config/project.js.map +0 -1
- package/dist/context/cce.d.ts +0 -39
- package/dist/context/cce.d.ts.map +0 -1
- package/dist/context/cce.js +0 -455
- package/dist/context/cce.js.map +0 -1
- package/dist/governor/fingerprint.d.ts +0 -7
- package/dist/governor/fingerprint.d.ts.map +0 -1
- package/dist/governor/fingerprint.js +0 -129
- package/dist/governor/fingerprint.js.map +0 -1
- package/dist/governor/hook.d.ts +0 -13
- package/dist/governor/hook.d.ts.map +0 -1
- package/dist/governor/hook.js +0 -389
- package/dist/governor/hook.js.map +0 -1
- package/dist/governor/state.d.ts +0 -45
- package/dist/governor/state.d.ts.map +0 -1
- package/dist/governor/state.js +0 -60
- package/dist/governor/state.js.map +0 -1
- package/dist/integrations/antigravity.d.ts +0 -20
- package/dist/integrations/antigravity.d.ts.map +0 -1
- package/dist/integrations/antigravity.js +0 -153
- package/dist/integrations/antigravity.js.map +0 -1
- package/dist/output.d.ts +0 -5
- package/dist/output.d.ts.map +0 -1
- package/dist/output.js +0 -2
- package/dist/output.js.map +0 -1
- package/dist/platform/executable.d.ts +0 -11
- package/dist/platform/executable.d.ts.map +0 -1
- package/dist/platform/executable.js +0 -69
- package/dist/platform/executable.js.map +0 -1
- package/dist/platform/process.d.ts +0 -25
- package/dist/platform/process.d.ts.map +0 -1
- package/dist/platform/process.js +0 -179
- package/dist/platform/process.js.map +0 -1
- package/dist/progress.d.ts +0 -44
- package/dist/progress.d.ts.map +0 -1
- package/dist/progress.js +0 -123
- package/dist/progress.js.map +0 -1
- package/dist/qa/diff.d.ts +0 -10
- package/dist/qa/diff.d.ts.map +0 -1
- package/dist/qa/diff.js +0 -91
- package/dist/qa/diff.js.map +0 -1
- package/dist/qa/server.d.ts +0 -10
- package/dist/qa/server.d.ts.map +0 -1
- package/dist/qa/server.js +0 -127
- package/dist/qa/server.js.map +0 -1
- package/dist/scaffold/adoption.d.ts +0 -17
- package/dist/scaffold/adoption.d.ts.map +0 -1
- package/dist/scaffold/adoption.js +0 -279
- package/dist/scaffold/adoption.js.map +0 -1
- package/dist/scaffold/legacy.d.ts +0 -4
- package/dist/scaffold/legacy.d.ts.map +0 -1
- package/dist/scaffold/legacy.js +0 -139
- package/dist/scaffold/legacy.js.map +0 -1
- package/dist/scaffold/routes.d.ts +0 -3
- package/dist/scaffold/routes.d.ts.map +0 -1
- package/dist/scaffold/routes.js +0 -37
- package/dist/scaffold/routes.js.map +0 -1
- package/dist/scaffold/standalone.d.ts +0 -7
- package/dist/scaffold/standalone.d.ts.map +0 -1
- package/dist/scaffold/standalone.js +0 -294
- package/dist/scaffold/standalone.js.map +0 -1
- package/dist/scaffold/templates.d.ts +0 -8
- package/dist/scaffold/templates.d.ts.map +0 -1
- package/dist/scaffold/templates.js +0 -312
- package/dist/scaffold/templates.js.map +0 -1
- package/dist/scaffold/write.d.ts +0 -2
- package/dist/scaffold/write.d.ts.map +0 -1
- package/dist/scaffold/write.js +0 -38
- package/dist/scaffold/write.js.map +0 -1
- package/dist/telemetry/store.d.ts +0 -33
- package/dist/telemetry/store.d.ts.map +0 -1
- package/dist/telemetry/store.js +0 -75
- package/dist/telemetry/store.js.map +0 -1
- package/dist/verify/runner.d.ts +0 -15
- package/dist/verify/runner.d.ts.map +0 -1
- package/dist/verify/runner.js +0 -112
- package/dist/verify/runner.js.map +0 -1
- package/docs/agent-audit.md +0 -24
package/dist/qa/runner.js
CHANGED
|
@@ -1,368 +1,318 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import http from "node:http";
|
|
2
3
|
import path from "node:path";
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
async function
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
const root = document.documentElement;
|
|
30
|
-
const overflow = Math.max(root.scrollWidth, document.body?.scrollWidth ?? 0) -
|
|
31
|
-
window.innerWidth;
|
|
32
|
-
const offenders = [...document.querySelectorAll("body *")]
|
|
33
|
-
.filter((element) => element.getBoundingClientRect().right > window.innerWidth + 1)
|
|
34
|
-
.slice(0, 5)
|
|
35
|
-
.map((element) => ({
|
|
36
|
-
selector: element.id
|
|
37
|
-
? `#${element.id}`
|
|
38
|
-
: element.classList.length
|
|
39
|
-
? `${element.tagName.toLowerCase()}.${[...element.classList].join(".")}`
|
|
40
|
-
: element.tagName.toLowerCase(),
|
|
41
|
-
pixels: Math.ceil(element.getBoundingClientRect().right - window.innerWidth),
|
|
42
|
-
}));
|
|
43
|
-
const unnamed = [
|
|
44
|
-
...document.querySelectorAll("button, input, select, textarea, [role=button], [role=link]"),
|
|
45
|
-
]
|
|
46
|
-
.filter((element) => {
|
|
47
|
-
const text = element.getAttribute("aria-label") ||
|
|
48
|
-
element.getAttribute("aria-labelledby") ||
|
|
49
|
-
element.getAttribute("title") ||
|
|
50
|
-
element.textContent?.trim() ||
|
|
51
|
-
element.name;
|
|
52
|
-
return !text && !element.hasAttribute("disabled");
|
|
53
|
-
})
|
|
54
|
-
.slice(0, 5)
|
|
55
|
-
.map((element) => element.outerHTML.slice(0, 120));
|
|
56
|
-
const hiddenImportant = [
|
|
57
|
-
...document.querySelectorAll("[data-mozole-important]"),
|
|
58
|
-
]
|
|
59
|
-
.filter((element) => {
|
|
60
|
-
const box = element.getBoundingClientRect();
|
|
61
|
-
const style = getComputedStyle(element);
|
|
62
|
-
return (box.width === 0 ||
|
|
63
|
-
box.height === 0 ||
|
|
64
|
-
style.visibility === "hidden" ||
|
|
65
|
-
style.display === "none");
|
|
66
|
-
})
|
|
67
|
-
.map((element) => element.id || element.className || element.tagName.toLowerCase());
|
|
68
|
-
return {
|
|
69
|
-
overflow,
|
|
70
|
-
offenders,
|
|
71
|
-
unnamed,
|
|
72
|
-
hiddenImportant,
|
|
73
|
-
title: document.title,
|
|
74
|
-
lang: document.documentElement.lang,
|
|
75
|
-
headings: document.querySelectorAll("h1").length,
|
|
76
|
-
};
|
|
77
|
-
});
|
|
78
|
-
probes += 6;
|
|
79
|
-
if (geometry.overflow > 1)
|
|
80
|
-
issues.push({
|
|
81
|
-
viewport,
|
|
82
|
-
route,
|
|
83
|
-
kind: "overflow",
|
|
84
|
-
evidence: `${geometry.offenders[0]?.selector ?? "document"}: horizontal overflow ${Math.ceil(geometry.overflow)}px`,
|
|
85
|
-
});
|
|
86
|
-
if (!geometry.title)
|
|
87
|
-
issues.push({
|
|
88
|
-
viewport,
|
|
89
|
-
route,
|
|
90
|
-
kind: "accessibility",
|
|
91
|
-
evidence: "document title is empty",
|
|
92
|
-
});
|
|
93
|
-
if (!geometry.lang)
|
|
94
|
-
issues.push({
|
|
95
|
-
viewport,
|
|
96
|
-
route,
|
|
97
|
-
kind: "accessibility",
|
|
98
|
-
evidence: "html lang is missing",
|
|
99
|
-
});
|
|
100
|
-
if (!geometry.headings)
|
|
101
|
-
issues.push({
|
|
102
|
-
viewport,
|
|
103
|
-
route,
|
|
104
|
-
kind: "accessibility",
|
|
105
|
-
evidence: "page has no h1",
|
|
106
|
-
});
|
|
107
|
-
if (geometry.unnamed.length)
|
|
108
|
-
issues.push({
|
|
109
|
-
viewport,
|
|
110
|
-
route,
|
|
111
|
-
kind: "accessibility",
|
|
112
|
-
evidence: `unnamed interactive control: ${geometry.unnamed[0]}`,
|
|
113
|
-
});
|
|
114
|
-
for (const element of geometry.hiddenImportant)
|
|
115
|
-
issues.push({
|
|
116
|
-
viewport,
|
|
117
|
-
route,
|
|
118
|
-
kind: "visibility",
|
|
119
|
-
evidence: `${element}: visible false`,
|
|
120
|
-
});
|
|
121
|
-
const links = await page.locator("a[href]").evaluateAll((nodes) => nodes
|
|
122
|
-
.map((node) => node.href)
|
|
123
|
-
.filter((href) => {
|
|
4
|
+
import pc from "picocolors";
|
|
5
|
+
import { chromium } from "playwright-core";
|
|
6
|
+
import { findAvailablePort } from "../server/manager.js";
|
|
7
|
+
import { ensureChromiumBrowser, getBrowserLaunchArgs } from "../utils/browser.js";
|
|
8
|
+
import { exists, findProjectRoot } from "../utils/fs.js";
|
|
9
|
+
import { extractBreakpointBoundaries } from "./breakpoints.js";
|
|
10
|
+
import { probeDOM } from "./probe.js";
|
|
11
|
+
const MIME_TYPES = {
|
|
12
|
+
".html": "text/html; charset=utf-8",
|
|
13
|
+
".css": "text/css; charset=utf-8",
|
|
14
|
+
".js": "application/javascript; charset=utf-8",
|
|
15
|
+
".mjs": "application/javascript; charset=utf-8",
|
|
16
|
+
".json": "application/json; charset=utf-8",
|
|
17
|
+
".svg": "image/svg+xml",
|
|
18
|
+
".png": "image/png",
|
|
19
|
+
".jpg": "image/jpeg",
|
|
20
|
+
".jpeg": "image/jpeg",
|
|
21
|
+
".webp": "image/webp",
|
|
22
|
+
".woff2": "font/woff2",
|
|
23
|
+
".woff": "font/woff",
|
|
24
|
+
".ico": "image/x-icon",
|
|
25
|
+
};
|
|
26
|
+
async function createStaticServer(rootDirectory, port) {
|
|
27
|
+
const realRoot = await fs.realpath(rootDirectory).catch(() => path.resolve(rootDirectory));
|
|
28
|
+
const realRootPrefix = realRoot.endsWith(path.sep) ? realRoot : realRoot + path.sep;
|
|
29
|
+
const server = http.createServer(async (req, res) => {
|
|
124
30
|
try {
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
31
|
+
const parsedUrl = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
|
|
32
|
+
let reqPath;
|
|
33
|
+
try {
|
|
34
|
+
reqPath = decodeURIComponent(parsedUrl.pathname);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
38
|
+
res.end("Bad Request");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const normalizedRoot = path.resolve(rootDirectory);
|
|
42
|
+
let filePath = path.resolve(normalizedRoot, `.${path.normalize(reqPath)}`);
|
|
43
|
+
// Prevent directory traversal attacks escaping rootDirectory
|
|
44
|
+
if (!filePath.startsWith(normalizedRoot + path.sep) && filePath !== normalizedRoot) {
|
|
45
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
46
|
+
res.end("Forbidden: Access denied outside web root");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
let stat = await fs.stat(filePath).catch(() => null);
|
|
50
|
+
if (stat?.isDirectory()) {
|
|
51
|
+
filePath = path.join(filePath, "index.html");
|
|
52
|
+
stat = await fs.stat(filePath).catch(() => null);
|
|
53
|
+
}
|
|
54
|
+
// Route fallback: /about -> /about.html
|
|
55
|
+
if (!stat && !path.extname(reqPath)) {
|
|
56
|
+
const withHtml = path.resolve(normalizedRoot, `.${path.normalize(reqPath)}.html`);
|
|
57
|
+
if (withHtml.startsWith(normalizedRoot + path.sep)) {
|
|
58
|
+
stat = await fs.stat(withHtml).catch(() => null);
|
|
59
|
+
if (stat)
|
|
60
|
+
filePath = withHtml;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// SPA fallback to index.html
|
|
64
|
+
if (!stat) {
|
|
65
|
+
const spaFallback = path.join(normalizedRoot, "index.html");
|
|
66
|
+
stat = await fs.stat(spaFallback).catch(() => null);
|
|
67
|
+
if (stat)
|
|
68
|
+
filePath = spaFallback;
|
|
69
|
+
}
|
|
70
|
+
if (!stat || !stat.isFile()) {
|
|
71
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
72
|
+
res.end("Not Found");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const realTarget = await fs.realpath(filePath).catch(() => null);
|
|
76
|
+
if (!realTarget || (realTarget !== realRoot && !realTarget.startsWith(realRootPrefix))) {
|
|
77
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
78
|
+
res.end("Forbidden: Access denied outside web root");
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
82
|
+
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
83
|
+
const content = await fs.readFile(filePath);
|
|
84
|
+
res.writeHead(200, {
|
|
85
|
+
"Content-Type": contentType,
|
|
86
|
+
"Content-Length": content.length,
|
|
87
|
+
"Cache-Control": "no-cache",
|
|
88
|
+
});
|
|
89
|
+
res.end(content);
|
|
128
90
|
}
|
|
129
91
|
catch {
|
|
130
|
-
|
|
92
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
93
|
+
res.end("Internal Server Error");
|
|
131
94
|
}
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
95
|
+
});
|
|
96
|
+
await new Promise((resolve, reject) => {
|
|
97
|
+
server.listen(port, "127.0.0.1", () => resolve());
|
|
98
|
+
server.once("error", reject);
|
|
99
|
+
});
|
|
100
|
+
return {
|
|
101
|
+
serverUrl: `http://127.0.0.1:${port}`,
|
|
102
|
+
close: () => new Promise((resolve) => {
|
|
103
|
+
server.close(() => resolve());
|
|
104
|
+
}),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
async function discoverRoutesAndDirectory(projectRoot, explicitStaticDir) {
|
|
108
|
+
let staticDir = explicitStaticDir;
|
|
109
|
+
if (!staticDir) {
|
|
110
|
+
const candidates = [
|
|
111
|
+
path.join(projectRoot, "build", "client"),
|
|
112
|
+
path.join(projectRoot, "dist"),
|
|
113
|
+
projectRoot,
|
|
114
|
+
];
|
|
115
|
+
for (const dir of candidates) {
|
|
116
|
+
if (await exists(dir)) {
|
|
117
|
+
const indexHtml = path.join(dir, "index.html");
|
|
118
|
+
if (await exists(indexHtml)) {
|
|
119
|
+
staticDir = dir;
|
|
120
|
+
break;
|
|
153
121
|
}
|
|
154
122
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (!staticDir || !(await exists(staticDir))) {
|
|
126
|
+
throw new Error(`No built static directory found in ${projectRoot}. Run 'npm run build' before running live DOM probe.`);
|
|
127
|
+
}
|
|
128
|
+
const routes = new Set(["/"]);
|
|
129
|
+
async function scanHtmlRoutes(dir, base) {
|
|
130
|
+
if (!(await exists(dir)))
|
|
131
|
+
return;
|
|
132
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
133
|
+
for (const entry of entries) {
|
|
134
|
+
const full = path.join(dir, entry.name);
|
|
135
|
+
if (entry.isDirectory()) {
|
|
136
|
+
await scanHtmlRoutes(full, `${base}/${entry.name}`);
|
|
137
|
+
}
|
|
138
|
+
else if (entry.isFile() && entry.name.endsWith(".html")) {
|
|
139
|
+
if (entry.name === "__spa-fallback.html")
|
|
140
|
+
continue;
|
|
141
|
+
if (entry.name === "index.html") {
|
|
142
|
+
routes.add(base || "/");
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
const routeName = entry.name.replace(/\.html$/, "");
|
|
146
|
+
routes.add(`${base}/${routeName}`);
|
|
159
147
|
}
|
|
160
|
-
catch { }
|
|
161
|
-
issues.push({
|
|
162
|
-
viewport,
|
|
163
|
-
route,
|
|
164
|
-
kind: "broken-link",
|
|
165
|
-
evidence: `${pathname}: request failed`,
|
|
166
|
-
});
|
|
167
148
|
}
|
|
168
|
-
}
|
|
149
|
+
}
|
|
169
150
|
}
|
|
170
|
-
|
|
151
|
+
await scanHtmlRoutes(staticDir, "");
|
|
152
|
+
return { staticDir, routes: Array.from(routes) };
|
|
171
153
|
}
|
|
172
|
-
async function
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
154
|
+
export async function runLiveGeometryProbe(options = {}) {
|
|
155
|
+
const startTime = Date.now();
|
|
156
|
+
const cwd = options.cwd ?? process.cwd();
|
|
157
|
+
const projectRoot = (await findProjectRoot(cwd)) ?? cwd;
|
|
158
|
+
console.log(pc.cyan(`\n🌐 Launching Headless DOM Geometry & Trace Runner in ${projectRoot}...`));
|
|
159
|
+
// 1. Detect or provision Chromium/Brave
|
|
160
|
+
const browserInfo = await ensureChromiumBrowser();
|
|
161
|
+
console.log(pc.dim(` • Using browser engine: ${browserInfo.name}${browserInfo.version ? ` (${browserInfo.version})` : ""}`));
|
|
162
|
+
// 2. Discover built static files & routes
|
|
163
|
+
const { staticDir, routes: discoveredRoutes } = await discoverRoutesAndDirectory(projectRoot, options.staticDir);
|
|
164
|
+
const targetRoutes = options.routes ?? discoveredRoutes;
|
|
165
|
+
// 3. Extract CSS breakpoints to populate boundary viewports
|
|
166
|
+
const defaultViewports = [
|
|
167
|
+
{ width: 320, height: 600, name: "Mobile Small (320px)" },
|
|
168
|
+
{ width: 390, height: 844, name: "Mobile Standard (390px)" },
|
|
169
|
+
{ width: 768, height: 1024, name: "Tablet (768px)" },
|
|
170
|
+
{ width: 1280, height: 800, name: "Desktop (1280px)" },
|
|
171
|
+
];
|
|
172
|
+
const cssList = [];
|
|
173
|
+
async function collectCss(dir) {
|
|
174
|
+
if (!(await exists(dir)))
|
|
175
|
+
return;
|
|
176
|
+
for (const e of await fs.readdir(dir, { withFileTypes: true })) {
|
|
177
|
+
const full = path.join(dir, e.name);
|
|
178
|
+
if (e.isDirectory() && !["node_modules", ".git"].includes(e.name)) {
|
|
179
|
+
await collectCss(full);
|
|
180
|
+
}
|
|
181
|
+
else if (e.isFile() && e.name.endsWith(".css")) {
|
|
182
|
+
cssList.push(await fs.readFile(full, "utf8"));
|
|
183
|
+
}
|
|
194
184
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
});
|
|
185
|
+
}
|
|
186
|
+
await collectCss(staticDir);
|
|
187
|
+
const boundaryPoints = extractBreakpointBoundaries(cssList, { minWidth: 320, maxWidth: 1440 });
|
|
188
|
+
for (const bp of boundaryPoints) {
|
|
189
|
+
if (!defaultViewports.some((v) => v.width === bp)) {
|
|
190
|
+
defaultViewports.push({ width: bp, height: 800, name: `Breakpoint boundary (${bp}px)` });
|
|
202
191
|
}
|
|
203
192
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
kind: "keyboard",
|
|
211
|
-
evidence: "Tab did not move focus",
|
|
212
|
-
});
|
|
213
|
-
return { issues, probes };
|
|
214
|
-
}
|
|
215
|
-
export async function runQa(root, config, mode, signal) {
|
|
216
|
-
const server = await acquireServer(root, config, signal);
|
|
193
|
+
defaultViewports.sort((a, b) => a.width - b.width);
|
|
194
|
+
const viewportsToTest = options.viewports ?? defaultViewports;
|
|
195
|
+
// 4. Start local static HTTP server
|
|
196
|
+
const port = options.port ?? (await findAvailablePort(4173));
|
|
197
|
+
const staticServer = await createStaticServer(staticDir, port);
|
|
198
|
+
const findings = [];
|
|
217
199
|
let browser;
|
|
218
|
-
|
|
219
|
-
const visualDiffs = [];
|
|
220
|
-
let probes = 0;
|
|
221
|
-
let screenshots = 0;
|
|
222
|
-
let consoleErrors = 0;
|
|
223
|
-
let failedRequests = 0;
|
|
200
|
+
let tracePath;
|
|
224
201
|
try {
|
|
225
|
-
browser = await chromium.launch({
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
route: page.url(),
|
|
258
|
-
kind: "network",
|
|
259
|
-
evidence: `${request.method()} ${request.url()} ${failure}`,
|
|
260
|
-
});
|
|
202
|
+
browser = await chromium.launch({
|
|
203
|
+
executablePath: browserInfo.executablePath,
|
|
204
|
+
headless: true,
|
|
205
|
+
args: getBrowserLaunchArgs(),
|
|
206
|
+
});
|
|
207
|
+
const context = await browser.newContext({
|
|
208
|
+
serviceWorkers: "block",
|
|
209
|
+
});
|
|
210
|
+
// Zero-screenshot policy: snapshots = true, screenshots = false
|
|
211
|
+
await context.tracing.start({
|
|
212
|
+
screenshots: false,
|
|
213
|
+
snapshots: true,
|
|
214
|
+
sources: true,
|
|
215
|
+
});
|
|
216
|
+
const page = await context.newPage();
|
|
217
|
+
for (const route of targetRoutes) {
|
|
218
|
+
const url = `${staticServer.serverUrl}${route}`;
|
|
219
|
+
for (const vp of viewportsToTest) {
|
|
220
|
+
await page.setViewportSize({ width: vp.width, height: vp.height });
|
|
221
|
+
await page.goto(url, { waitUntil: "domcontentloaded" });
|
|
222
|
+
await page.waitForTimeout(50);
|
|
223
|
+
// Run live probe in browser context
|
|
224
|
+
const result = (await page.evaluate(probeDOM));
|
|
225
|
+
for (const [rule, issues] of Object.entries(result.issues)) {
|
|
226
|
+
for (const issue of issues) {
|
|
227
|
+
findings.push({
|
|
228
|
+
rule,
|
|
229
|
+
issue,
|
|
230
|
+
route,
|
|
231
|
+
viewport: vp,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
261
234
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
evidence: `${response.request().method()} ${response.url()} HTTP ${response.status()}`,
|
|
235
|
+
// Stress test: 200% font size reflow check (capture sizes first to prevent compounding inheritance)
|
|
236
|
+
await page.evaluate(() => {
|
|
237
|
+
const elements = Array.from(document.querySelectorAll("body, body *"));
|
|
238
|
+
const sizes = elements.map((el) => Number.parseFloat(window.getComputedStyle(el).fontSize));
|
|
239
|
+
elements.forEach((el, i) => {
|
|
240
|
+
if (sizes[i] > 0) {
|
|
241
|
+
el.style.setProperty("font-size", `${sizes[i] * 2}px`, "important");
|
|
242
|
+
}
|
|
271
243
|
});
|
|
272
|
-
}
|
|
273
|
-
});
|
|
274
|
-
for (const route of config.routes) {
|
|
275
|
-
signal?.throwIfAborted();
|
|
276
|
-
const response = await page.goto(`${server.url}${route}`, {
|
|
277
|
-
waitUntil: "networkidle",
|
|
278
|
-
timeout: 30_000,
|
|
279
244
|
});
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
245
|
+
await page.waitForTimeout(50);
|
|
246
|
+
const stressResult = (await page.evaluate(probeDOM));
|
|
247
|
+
for (const issue of stressResult.issues.overflow) {
|
|
248
|
+
findings.push({
|
|
249
|
+
rule: "stress-overflow-200",
|
|
250
|
+
issue,
|
|
284
251
|
route,
|
|
285
|
-
|
|
286
|
-
evidence: `HTTP ${response?.status() ?? "no response"}`,
|
|
252
|
+
viewport: vp,
|
|
287
253
|
});
|
|
288
|
-
continue;
|
|
289
254
|
}
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
}
|
|
298
|
-
if (mode.visual) {
|
|
299
|
-
const screenshotPath = path.join(artifactRoot, `${viewportName}-${visualArtifactSlug(route)}.png`);
|
|
300
|
-
const screenshot = await page.screenshot({ fullPage: true });
|
|
301
|
-
if (mode.diff) {
|
|
302
|
-
const comparison = await compareVisualArtifact(screenshotPath, screenshot, config.qa.visualDiffThreshold ?? 0.01);
|
|
303
|
-
if (comparison.exceeded && comparison.diffPath) {
|
|
304
|
-
visualDiffs.push({
|
|
305
|
-
viewport: viewportName,
|
|
306
|
-
route,
|
|
307
|
-
ratio: comparison.ratio,
|
|
308
|
-
changedPixels: comparison.changedPixels,
|
|
309
|
-
totalPixels: comparison.totalPixels,
|
|
310
|
-
diffPath: path.relative(root, comparison.diffPath),
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
else {
|
|
315
|
-
await writeFile(screenshotPath, screenshot);
|
|
316
|
-
}
|
|
317
|
-
screenshots++;
|
|
255
|
+
for (const issue of stressResult.issues.clipping) {
|
|
256
|
+
findings.push({
|
|
257
|
+
rule: "stress-clipping-200",
|
|
258
|
+
issue,
|
|
259
|
+
route,
|
|
260
|
+
viewport: vp,
|
|
261
|
+
});
|
|
318
262
|
}
|
|
319
263
|
}
|
|
320
|
-
await context.close();
|
|
321
264
|
}
|
|
265
|
+
if (findings.length > 0 && options.saveTraceOnFailure !== false) {
|
|
266
|
+
const traceDir = path.join(projectRoot, "docs", "qa");
|
|
267
|
+
await fs.mkdir(traceDir, { recursive: true });
|
|
268
|
+
tracePath = options.tracePath ?? path.join(traceDir, "trace.zip");
|
|
269
|
+
await context.tracing.stop({ path: tracePath });
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
await context.tracing.stop();
|
|
273
|
+
}
|
|
274
|
+
await context.close();
|
|
322
275
|
}
|
|
323
276
|
finally {
|
|
324
|
-
|
|
325
|
-
|
|
277
|
+
if (browser) {
|
|
278
|
+
await browser.close().catch(() => { });
|
|
279
|
+
}
|
|
280
|
+
await staticServer.close().catch(() => { });
|
|
281
|
+
}
|
|
282
|
+
const durationMs = Date.now() - startTime;
|
|
283
|
+
if (findings.length > 0) {
|
|
284
|
+
console.log(pc.red(`\n ✗ Found ${findings.length} live DOM geometry / accessibility defect(s) during probe:`));
|
|
285
|
+
for (const f of findings.slice(0, 15)) {
|
|
286
|
+
console.log(pc.yellow(` [${f.rule.toUpperCase()}] ${f.route} @ ${f.viewport.width}px (${f.viewport.name ?? ""})`));
|
|
287
|
+
console.log(pc.dim(` Selector: ${f.issue.selector}`));
|
|
288
|
+
if (f.issue.problem)
|
|
289
|
+
console.log(pc.dim(` Problem: ${f.issue.problem}`));
|
|
290
|
+
if (f.issue.text)
|
|
291
|
+
console.log(pc.dim(` Text: "${f.issue.text}"`));
|
|
292
|
+
if (f.issue.width || f.issue.height) {
|
|
293
|
+
console.log(pc.dim(` Size: ${f.issue.width ?? 0}x${f.issue.height ?? 0}px`));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (findings.length > 15) {
|
|
297
|
+
console.log(pc.dim(` ... and ${findings.length - 15} more findings.`));
|
|
298
|
+
}
|
|
299
|
+
if (tracePath) {
|
|
300
|
+
console.log(pc.cyan(`\n 📦 Trace recording saved to: ${tracePath}`));
|
|
301
|
+
console.log(pc.dim(" Inspect trace online at https://trace.playwright.dev (zero screenshots, full DOM snapshots & network)."));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
console.log(pc.green(` ✓ Live DOM geometry verified across ${targetRoutes.length} route(s) and ${viewportsToTest.length} viewport(s) with zero defects.`));
|
|
306
|
+
console.log(pc.dim(` • Verified in ${durationMs}ms with zero raster screenshots.`));
|
|
326
307
|
}
|
|
327
|
-
const unique = [
|
|
328
|
-
...new Map(issues.map((issue) => [
|
|
329
|
-
`${issue.viewport}|${issue.route}|${issue.kind}|${issue.evidence}`,
|
|
330
|
-
issue,
|
|
331
|
-
])).values(),
|
|
332
|
-
];
|
|
333
|
-
const passed = unique.length === 0;
|
|
334
|
-
const diffOutput = mode.diff
|
|
335
|
-
? visualDiffs.length
|
|
336
|
-
? `\n\nVISUAL DIFFS\n${visualDiffs.map(formatVisualDiff).join("\n")}`
|
|
337
|
-
: "\n\nVISUAL DIFFS\nNo changes above threshold."
|
|
338
|
-
: "";
|
|
339
|
-
const output = (passed
|
|
340
|
-
? `QA PASS\n\n${config.qa.viewports.map((viewport) => `${label(viewport).padEnd(11)} ✓`).join("\n")}\n\nconsole errors ${consoleErrors}\nfailed requests ${failedRequests}\noverflow ${unique.filter((issue) => issue.kind === "overflow").length}\nbroken links ${unique.filter((issue) => issue.kind === "broken-link").length}${mode.visual ? `\nscreenshots ${screenshots}` : ""}`
|
|
341
|
-
: `QA FAIL\n\n${unique
|
|
342
|
-
.slice(0, 12)
|
|
343
|
-
.map((issue) => `${issue.viewport} ${issue.route}\n${issue.evidence}`)
|
|
344
|
-
.join("\n\n")}`) + diffOutput;
|
|
345
|
-
if (config.telemetry?.enabled)
|
|
346
|
-
await updateTelemetry(root, (telemetry) => {
|
|
347
|
-
telemetry.qa.runs++;
|
|
348
|
-
telemetry.qa.probes += probes;
|
|
349
|
-
telemetry.qa.screenshots += screenshots;
|
|
350
|
-
if (mode.visual)
|
|
351
|
-
telemetry.qa.visualRuns++;
|
|
352
|
-
telemetry.governor.modelFacingDiagnosticChars += output.length;
|
|
353
|
-
});
|
|
354
308
|
return {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
probes,
|
|
363
|
-
screenshots,
|
|
364
|
-
visualDiffs,
|
|
365
|
-
output,
|
|
309
|
+
success: findings.length === 0,
|
|
310
|
+
browserName: browserInfo.name,
|
|
311
|
+
findings,
|
|
312
|
+
routesChecked: targetRoutes,
|
|
313
|
+
viewportsChecked: viewportsToTest,
|
|
314
|
+
durationMs,
|
|
315
|
+
traceSavedTo: tracePath,
|
|
366
316
|
};
|
|
367
317
|
}
|
|
368
318
|
//# sourceMappingURL=runner.js.map
|