@mozole/cli 1.4.3 → 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 -112
- package/dist/cli.d.ts +2 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +204 -184
- package/dist/cli.js.map +1 -1
- package/dist/commands/adopt.d.ts +5 -10
- package/dist/commands/adopt.d.ts.map +1 -1
- package/dist/commands/adopt.js +49 -849
- 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 -197
- 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 -42
- package/dist/commands/ui.d.ts.map +1 -1
- package/dist/commands/ui.js +74 -466
- 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 -33
- package/dist/qa/runner.d.ts.map +1 -1
- package/dist/qa/runner.js +286 -332
- 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 -38
- 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 -40
- 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/commands/template.d.ts +0 -8
- package/dist/commands/template.d.ts.map +0 -1
- package/dist/commands/template.js +0 -392
- package/dist/commands/template.js.map +0 -1
- package/dist/config/project.d.ts +0 -54
- package/dist/config/project.d.ts.map +0 -1
- package/dist/config/project.js +0 -100
- 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 -120
- 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-safety.d.ts +0 -3
- package/dist/output-safety.d.ts.map +0 -1
- package/dist/output-safety.js +0 -44
- package/dist/output-safety.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 -27
- package/dist/platform/process.d.ts.map +0 -1
- package/dist/platform/process.js +0 -189
- 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/artifacts.d.ts +0 -3
- package/dist/qa/artifacts.d.ts.map +0 -1
- package/dist/qa/artifacts.js +0 -15
- package/dist/qa/artifacts.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 -90
- 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 -374
- package/dist/scaffold/standalone.js.map +0 -1
- package/dist/scaffold/templates.d.ts +0 -9
- package/dist/scaffold/templates.d.ts.map +0 -1
- package/dist/scaffold/templates.js +0 -317
- 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/template/servers.d.ts +0 -21
- package/dist/template/servers.d.ts.map +0 -1
- package/dist/template/servers.js +0 -193
- package/dist/template/servers.js.map +0 -1
- package/dist/template/workspace.d.ts +0 -23
- package/dist/template/workspace.d.ts.map +0 -1
- package/dist/template/workspace.js +0 -152
- package/dist/template/workspace.js.map +0 -1
- package/dist/verify/runner.d.ts +0 -16
- 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,364 +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
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
.map((element) => ({
|
|
30
|
-
selector: element.id
|
|
31
|
-
? `#${element.id}`
|
|
32
|
-
: element.classList.length
|
|
33
|
-
? `${element.tagName.toLowerCase()}.${[...element.classList].join(".")}`
|
|
34
|
-
: element.tagName.toLowerCase(),
|
|
35
|
-
pixels: Math.ceil(element.getBoundingClientRect().right - window.innerWidth),
|
|
36
|
-
}));
|
|
37
|
-
const unnamed = [
|
|
38
|
-
...document.querySelectorAll("button, input, select, textarea, [role=button], [role=link]"),
|
|
39
|
-
]
|
|
40
|
-
.filter((element) => {
|
|
41
|
-
const text = element.getAttribute("aria-label") ||
|
|
42
|
-
element.getAttribute("aria-labelledby") ||
|
|
43
|
-
element.getAttribute("title") ||
|
|
44
|
-
element.textContent?.trim() ||
|
|
45
|
-
element.name;
|
|
46
|
-
return !text && !element.hasAttribute("disabled");
|
|
47
|
-
})
|
|
48
|
-
.slice(0, 5)
|
|
49
|
-
.map((element) => element.outerHTML.slice(0, 120));
|
|
50
|
-
const hiddenImportant = [
|
|
51
|
-
...document.querySelectorAll("[data-mozole-important]"),
|
|
52
|
-
]
|
|
53
|
-
.filter((element) => {
|
|
54
|
-
const box = element.getBoundingClientRect();
|
|
55
|
-
const style = getComputedStyle(element);
|
|
56
|
-
return (box.width === 0 ||
|
|
57
|
-
box.height === 0 ||
|
|
58
|
-
style.visibility === "hidden" ||
|
|
59
|
-
style.display === "none");
|
|
60
|
-
})
|
|
61
|
-
.map((element) => element.id || element.className || element.tagName.toLowerCase());
|
|
62
|
-
return {
|
|
63
|
-
overflow,
|
|
64
|
-
offenders,
|
|
65
|
-
unnamed,
|
|
66
|
-
hiddenImportant,
|
|
67
|
-
title: document.title,
|
|
68
|
-
lang: document.documentElement.lang,
|
|
69
|
-
headings: document.querySelectorAll("h1").length,
|
|
70
|
-
};
|
|
71
|
-
});
|
|
72
|
-
probes += 6;
|
|
73
|
-
if (geometry.overflow > 1)
|
|
74
|
-
issues.push({
|
|
75
|
-
viewport,
|
|
76
|
-
route,
|
|
77
|
-
kind: "overflow",
|
|
78
|
-
evidence: `${geometry.offenders[0]?.selector ?? "document"}: horizontal overflow ${Math.ceil(geometry.overflow)}px`,
|
|
79
|
-
});
|
|
80
|
-
if (!geometry.title)
|
|
81
|
-
issues.push({
|
|
82
|
-
viewport,
|
|
83
|
-
route,
|
|
84
|
-
kind: "accessibility",
|
|
85
|
-
evidence: "document title is empty",
|
|
86
|
-
});
|
|
87
|
-
if (!geometry.lang)
|
|
88
|
-
issues.push({
|
|
89
|
-
viewport,
|
|
90
|
-
route,
|
|
91
|
-
kind: "accessibility",
|
|
92
|
-
evidence: "html lang is missing",
|
|
93
|
-
});
|
|
94
|
-
if (!geometry.headings)
|
|
95
|
-
issues.push({
|
|
96
|
-
viewport,
|
|
97
|
-
route,
|
|
98
|
-
kind: "accessibility",
|
|
99
|
-
evidence: "page has no h1",
|
|
100
|
-
});
|
|
101
|
-
if (geometry.unnamed.length)
|
|
102
|
-
issues.push({
|
|
103
|
-
viewport,
|
|
104
|
-
route,
|
|
105
|
-
kind: "accessibility",
|
|
106
|
-
evidence: `unnamed interactive control: ${geometry.unnamed[0]}`,
|
|
107
|
-
});
|
|
108
|
-
for (const element of geometry.hiddenImportant)
|
|
109
|
-
issues.push({
|
|
110
|
-
viewport,
|
|
111
|
-
route,
|
|
112
|
-
kind: "visibility",
|
|
113
|
-
evidence: `${element}: visible false`,
|
|
114
|
-
});
|
|
115
|
-
const links = await page.locator("a[href]").evaluateAll((nodes) => nodes
|
|
116
|
-
.map((node) => node.href)
|
|
117
|
-
.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) => {
|
|
118
30
|
try {
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
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);
|
|
122
90
|
}
|
|
123
91
|
catch {
|
|
124
|
-
|
|
92
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
93
|
+
res.end("Internal Server Error");
|
|
125
94
|
}
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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;
|
|
147
121
|
}
|
|
148
122
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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}`);
|
|
153
147
|
}
|
|
154
|
-
catch { }
|
|
155
|
-
issues.push({
|
|
156
|
-
viewport,
|
|
157
|
-
route,
|
|
158
|
-
kind: "broken-link",
|
|
159
|
-
evidence: `${pathname}: request failed`,
|
|
160
|
-
});
|
|
161
148
|
}
|
|
162
|
-
}
|
|
149
|
+
}
|
|
163
150
|
}
|
|
164
|
-
|
|
151
|
+
await scanHtmlRoutes(staticDir, "");
|
|
152
|
+
return { staticDir, routes: Array.from(routes) };
|
|
165
153
|
}
|
|
166
|
-
async function
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
+
}
|
|
188
184
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
});
|
|
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)` });
|
|
196
191
|
}
|
|
197
192
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
kind: "keyboard",
|
|
205
|
-
evidence: "Tab did not move focus",
|
|
206
|
-
});
|
|
207
|
-
return { issues, probes };
|
|
208
|
-
}
|
|
209
|
-
export async function runQa(root, config, mode, signal) {
|
|
210
|
-
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 = [];
|
|
211
199
|
let browser;
|
|
212
|
-
|
|
213
|
-
const visualDiffs = [];
|
|
214
|
-
let probes = 0;
|
|
215
|
-
let screenshots = 0;
|
|
216
|
-
let consoleErrors = 0;
|
|
217
|
-
let failedRequests = 0;
|
|
200
|
+
let tracePath;
|
|
218
201
|
try {
|
|
219
|
-
browser = await chromium.launch({
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
route: page.url(),
|
|
252
|
-
kind: "network",
|
|
253
|
-
evidence: `${request.method()} ${request.url()} ${failure}`,
|
|
254
|
-
});
|
|
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
|
+
}
|
|
255
234
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
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
|
+
}
|
|
265
243
|
});
|
|
266
|
-
}
|
|
267
|
-
});
|
|
268
|
-
for (const route of config.routes) {
|
|
269
|
-
signal?.throwIfAborted();
|
|
270
|
-
const response = await page.goto(`${server.url}${route}`, {
|
|
271
|
-
waitUntil: "networkidle",
|
|
272
|
-
timeout: 30_000,
|
|
273
244
|
});
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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,
|
|
278
251
|
route,
|
|
279
|
-
|
|
280
|
-
evidence: `HTTP ${response?.status() ?? "no response"}`,
|
|
252
|
+
viewport: vp,
|
|
281
253
|
});
|
|
282
|
-
continue;
|
|
283
254
|
}
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
292
|
-
if (mode.visual) {
|
|
293
|
-
const screenshotPath = path.join(artifactRoot, `${viewportName}-${visualArtifactKey(route)}.png`);
|
|
294
|
-
const screenshot = await page.screenshot({ fullPage: true });
|
|
295
|
-
if (mode.diff) {
|
|
296
|
-
const comparison = await compareVisualArtifact(screenshotPath, screenshot, config.qa.visualDiffThreshold ?? 0.01);
|
|
297
|
-
if (comparison.exceeded) {
|
|
298
|
-
visualDiffs.push({
|
|
299
|
-
viewport: viewportName,
|
|
300
|
-
route,
|
|
301
|
-
ratio: comparison.ratio,
|
|
302
|
-
changedPixels: comparison.changedPixels,
|
|
303
|
-
totalPixels: comparison.totalPixels,
|
|
304
|
-
diffPath: comparison.diffPath
|
|
305
|
-
? path.relative(root, comparison.diffPath)
|
|
306
|
-
: undefined,
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
else {
|
|
311
|
-
await writeFile(screenshotPath, screenshot);
|
|
312
|
-
}
|
|
313
|
-
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
|
+
});
|
|
314
262
|
}
|
|
315
263
|
}
|
|
316
|
-
await context.close();
|
|
317
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();
|
|
318
275
|
}
|
|
319
276
|
finally {
|
|
320
|
-
|
|
321
|
-
|
|
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.`));
|
|
322
307
|
}
|
|
323
|
-
const unique = [
|
|
324
|
-
...new Map(issues.map((issue) => [
|
|
325
|
-
`${issue.viewport}|${issue.route}|${issue.kind}|${issue.evidence}`,
|
|
326
|
-
issue,
|
|
327
|
-
])).values(),
|
|
328
|
-
];
|
|
329
|
-
const passed = qaResultPassed(unique, visualDiffs);
|
|
330
|
-
const diffOutput = mode.diff
|
|
331
|
-
? visualDiffs.length
|
|
332
|
-
? `\n\nVISUAL DIFFS\n${visualDiffs.map(formatVisualDiff).join("\n")}`
|
|
333
|
-
: "\n\nVISUAL DIFFS\nNo changes above threshold."
|
|
334
|
-
: "";
|
|
335
|
-
const output = (passed
|
|
336
|
-
? `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}` : ""}`
|
|
337
|
-
: `QA FAIL\n\n${unique
|
|
338
|
-
.slice(0, 12)
|
|
339
|
-
.map((issue) => `${issue.viewport} ${issue.route}\n${issue.evidence}`)
|
|
340
|
-
.join("\n\n")}`) + diffOutput;
|
|
341
|
-
if (config.telemetry?.enabled)
|
|
342
|
-
await updateTelemetry(root, (telemetry) => {
|
|
343
|
-
telemetry.qa.runs++;
|
|
344
|
-
telemetry.qa.probes += probes;
|
|
345
|
-
telemetry.qa.screenshots += screenshots;
|
|
346
|
-
if (mode.visual)
|
|
347
|
-
telemetry.qa.visualRuns++;
|
|
348
|
-
telemetry.governor.modelFacingDiagnosticChars += output.length;
|
|
349
|
-
});
|
|
350
308
|
return {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
probes,
|
|
359
|
-
screenshots,
|
|
360
|
-
visualDiffs,
|
|
361
|
-
output,
|
|
309
|
+
success: findings.length === 0,
|
|
310
|
+
browserName: browserInfo.name,
|
|
311
|
+
findings,
|
|
312
|
+
routesChecked: targetRoutes,
|
|
313
|
+
viewportsChecked: viewportsToTest,
|
|
314
|
+
durationMs,
|
|
315
|
+
traceSavedTo: tracePath,
|
|
362
316
|
};
|
|
363
317
|
}
|
|
364
318
|
//# sourceMappingURL=runner.js.map
|