@qatonic_innovations/qaios 0.5.3 → 0.6.1
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/README.md +40 -32
- package/dist/GateReview-MDXNA542.js +127 -0
- package/dist/SnapshotReview-7TPWX62T.js +137 -0
- package/dist/a11y/scripts/axe-runner.mjs +8 -1
- package/dist/a11y-D2ESBYWX.js +360 -0
- package/dist/chunk-4D352FXP.js +185 -0
- package/dist/chunk-AW4GQLOM.js +67 -0
- package/dist/chunk-BZOQ4NNA.js +533 -0
- package/dist/chunk-DYAAVZGS.js +27 -0
- package/dist/chunk-EAMUH7ZB.js +23 -0
- package/dist/chunk-EEFQVLZX.js +264 -0
- package/dist/chunk-HZDJWR25.js +1816 -0
- package/dist/chunk-JCVLIA63.js +63 -0
- package/dist/chunk-OOI2XRPM.js +231 -0
- package/dist/chunk-PGTYCHBV.js +18 -0
- package/dist/chunk-RDZE7TZG.js +17 -0
- package/dist/chunk-UOPD5XI2.js +298 -0
- package/dist/chunk-VMTUIR5O.js +14611 -0
- package/dist/chunk-VSSX6F42.js +27 -0
- package/dist/chunk-WUHORHPY.js +42 -0
- package/dist/chunk-ZL6W6TH2.js +2104 -0
- package/dist/config-NRRBZZ35.js +657 -0
- package/dist/config-schema.d.ts +20 -0
- package/dist/config-schema.js +3 -0
- package/dist/dist-QCMTQH2D.js +2 -0
- package/dist/doctor-4LUAESSS.js +942 -0
- package/dist/evidence-L2GG45YA.js +224 -0
- package/dist/explore-VJIQQBEG.js +425 -0
- package/dist/fix-2QC6ZIX6.js +804 -0
- package/dist/healing/scripts/capture-page.mjs +22 -3
- package/dist/history-UBEWZE2G.js +506 -0
- package/dist/index.d.ts +45 -1
- package/dist/index.js +826 -14869
- package/dist/init-64R4KSLV.js +204 -0
- package/dist/manifest-seal-7HRI6C5U.js +5 -0
- package/dist/mcp-FE4XUYRP.js +272 -0
- package/dist/migrations/0004_review_records.sql +56 -0
- package/dist/review-HX34P7FQ.js +499 -0
- package/dist/run-XFKBCGNB.js +312 -0
- package/dist/schemas/config-layer-v1.schema.json +395 -0
- package/dist/schemas/config-v1.schema.json +398 -0
- package/dist/snapshot-UTPP6UNI.js +443 -0
- package/dist/snapshot-review-IBN3QV7G.js +7 -0
- package/dist/test-WXHGG4GY.js +10 -0
- package/dist/validation/worker.d.ts +2 -0
- package/dist/validation/worker.js +686 -0
- package/dist/visual/scripts/capture-screenshot.mjs +3 -0
- package/dist/xray-HK6PIENO.js +674 -0
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ QAIOS acts like a QA engineer on your team. Point it at a feature description or
|
|
|
10
10
|
|
|
11
11
|
**Mental model: Claude Code, but for QA work instead of feature coding.**
|
|
12
12
|
|
|
13
|
-
> **Status: pre-1.0 (
|
|
13
|
+
> **Status: pre-1.0** ([changelog](https://github.com/qatonic/qaios/blob/main/CHANGELOG.md)). Core flows — `init`, `doctor`, test design
|
|
14
14
|
> & generation, Playwright execution, self-healing, accessibility, and the
|
|
15
15
|
> audit log — work today. Expect rough edges and please
|
|
16
16
|
> [report issues](https://github.com/qatonic/qaios/issues). After installing,
|
|
@@ -35,10 +35,11 @@ The published package is `@qatonic_innovations/qaios`; the installed **command i
|
|
|
35
35
|
|
|
36
36
|
### Requirements
|
|
37
37
|
|
|
38
|
-
- **Node.js 22
|
|
39
|
-
(`node:sqlite
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
- **Node.js 22.13 or newer.** QAIOS uses Node's **built-in** SQLite
|
|
39
|
+
(`node:sqlite`) for its local audit log — **no native module to compile**,
|
|
40
|
+
so `npm install` is just a download. `node:sqlite` loads without a flag from
|
|
41
|
+
Node 22.13; before that it isn't available, so upgrade Node. Node 22 still
|
|
42
|
+
labels it experimental; QAIOS hides that one warning.
|
|
42
43
|
- An **LLM provider API key.** QAIOS uses **Anthropic** by default — get a key
|
|
43
44
|
at [console.anthropic.com](https://console.anthropic.com/settings/keys) and
|
|
44
45
|
export it:
|
|
@@ -63,7 +64,7 @@ The published package is `@qatonic_innovations/qaios`; the installed **command i
|
|
|
63
64
|
|
|
64
65
|
QAIOS has **no native dependencies** — it uses Node's built-in SQLite — so
|
|
65
66
|
install is friction-free. If `qaios init` reports a SQLite/`node:sqlite` error,
|
|
66
|
-
your Node is too old: run `node -v` and upgrade to **Node 22
|
|
67
|
+
your Node is too old: run `node -v` and upgrade to **Node 22.13 or newer**.
|
|
67
68
|
|
|
68
69
|
`qaios doctor` will tell you exactly which check failed and what to run.
|
|
69
70
|
|
|
@@ -107,11 +108,11 @@ Cost: 11,403 tokens · $0.06 (3 of 20 LLM calls used)
|
|
|
107
108
|
| Command | What it does |
|
|
108
109
|
| ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
|
109
110
|
| `qaios init [path]` | Bootstrap a project: create `.qaios/`, `QAIOS.md`, run migrations. |
|
|
110
|
-
| `qaios doctor` | Health-check your environment
|
|
111
|
+
| `qaios doctor` | Health-check your environment. Use `--deep` for read-only Chromium/MCP probes and `--json` for stable CI diagnostics. |
|
|
111
112
|
| `qaios test <description>` | Design + generate tests from natural language. `--type api` for OpenAPI. `--run` to execute. `--yes` to auto-approve gates. |
|
|
112
113
|
| `qaios run [pattern]` | Execute Playwright tests, classify failures (real / flaky / locator drift), auto-heal locator breaks, file defects. |
|
|
113
114
|
| `qaios fix [target]` | Self-heal a broken locator. `--last-failure` to target the most recent failure, `--auto` to apply, `--dry-run` to preview. |
|
|
114
|
-
| `qaios explore <url>` |
|
|
115
|
+
| `qaios explore <url>` | Charter + unverified hypotheses from one page snapshot (no browser session). `--focus`, `--file-hypotheses`. |
|
|
115
116
|
| `qaios a11y <url>` | Deterministic axe-core accessibility scan + AI triage. `--wcag-level A\|AA\|AAA`. Exits 1 on real violations (CI-gateable). |
|
|
116
117
|
| `qaios snapshot capture\|check\|review` | Visual regression: capture baselines, diff, and triage AI-classified diffs. |
|
|
117
118
|
| `qaios review` | Walk pending human-review gates and approve/reject; resumes the workflow. |
|
|
@@ -136,7 +137,7 @@ qaios fix tests/auth/login.spec.ts --last-failure --dry-run
|
|
|
136
137
|
# Accessibility scan, gate CI on real WCAG violations
|
|
137
138
|
qaios a11y https://staging.myapp.com --wcag-level AA
|
|
138
139
|
|
|
139
|
-
#
|
|
140
|
+
# Exploration charter + unverified hypotheses (no browser session)
|
|
140
141
|
qaios explore https://staging.myapp.com --duration 900 --focus "checkout flow"
|
|
141
142
|
|
|
142
143
|
# Visual regression
|
|
@@ -165,11 +166,13 @@ testing:
|
|
|
165
166
|
framework: playwright
|
|
166
167
|
testDir: tests
|
|
167
168
|
testIdAttribute: data-testid # e.g. set to 'data-test' for some apps
|
|
169
|
+
# fixtures: tests/fixtures.ts # optional: your `base.extend(...)` test; generated specs use it
|
|
168
170
|
locatorPreference: [data-testid, role, text]
|
|
169
171
|
gates:
|
|
170
172
|
autoExpireOnTimeout: abort # abort | auto_approve | auto_reject
|
|
171
173
|
defects:
|
|
172
174
|
target: stdout # stdout | github | jira
|
|
175
|
+
reproduceBeforeFiling: true # GitHub/Jira only get failures that fail again on a re-run
|
|
173
176
|
```
|
|
174
177
|
|
|
175
178
|
`qaios config show` prints the resolved config; `qaios config set <key> <value>` validates against the schema before writing. **API keys are never written to config** — they come from environment variables.
|
|
@@ -189,18 +192,20 @@ llm:
|
|
|
189
192
|
# OpenAI
|
|
190
193
|
llm:
|
|
191
194
|
provider: openai # reads OPENAI_API_KEY
|
|
192
|
-
model: gpt-
|
|
195
|
+
model: gpt-6.1-sol # optional; the default (try gpt-6-luna for lower cost)
|
|
193
196
|
```
|
|
194
197
|
|
|
195
198
|
```bash
|
|
196
199
|
export OPENAI_API_KEY=sk-...
|
|
197
|
-
qaios doctor # confirms the
|
|
200
|
+
qaios doctor # confirms the provider accepts your key (add --offline to skip the call)
|
|
198
201
|
```
|
|
199
202
|
|
|
200
203
|
The rest of QAIOS is provider-blind — every command (`test`, `run`, `fix`,
|
|
201
204
|
`explore`, `a11y`, …) works identically on either. Structured output uses each
|
|
202
|
-
provider's native
|
|
203
|
-
|
|
205
|
+
provider's native structured-output mode (a `submit` tool on Anthropic; strict
|
|
206
|
+
function calling on OpenAI), and every answer is validated against the skill's
|
|
207
|
+
schema, so generated artifacts stay schema-valid. GPT-5 and later models are
|
|
208
|
+
called through OpenAI's Responses API; GPT-4 models through Chat Completions. You can
|
|
204
209
|
override the key's env-var name with `llm.apiKeyEnv` if needed.
|
|
205
210
|
|
|
206
211
|
#### Picking a model
|
|
@@ -209,21 +214,22 @@ override the key's env-var name with `llm.apiKeyEnv` if needed.
|
|
|
209
214
|
that provider's API accepts works**, not just the defaults. When `llm.model` is
|
|
210
215
|
omitted, QAIOS uses a sensible default per provider:
|
|
211
216
|
|
|
212
|
-
| Provider | Default
|
|
213
|
-
| ----------- |
|
|
214
|
-
| `anthropic` | `claude-sonnet-
|
|
215
|
-
| `openai` | `gpt-
|
|
217
|
+
| Provider | Default ($ per 1M tokens in / out) | Other examples you can set |
|
|
218
|
+
| ----------- | ---------------------------------- | --------------------------------------------------------------- |
|
|
219
|
+
| `anthropic` | `claude-sonnet-5-5` ($2 / $10) | `claude-opus-5-5` ($4 / $20), `claude-haiku-5-5` (cheapest), … |
|
|
220
|
+
| `openai` | `gpt-6.1-sol` ($2 / $10) | `gpt-6-astra` ($10 / $50), `gpt-6-luna` (cheapest), `gpt-4o`, … |
|
|
216
221
|
|
|
217
222
|
```yaml
|
|
218
223
|
llm:
|
|
219
224
|
provider: openai
|
|
220
|
-
model: gpt-
|
|
225
|
+
model: gpt-6-luna # any OpenAI model id
|
|
221
226
|
```
|
|
222
227
|
|
|
223
228
|
Two things to know about non-default models:
|
|
224
229
|
|
|
225
230
|
- **Cost tracking.** QAIOS knows exact pricing for a built-in set of models
|
|
226
|
-
(Sonnet/Opus/Haiku
|
|
231
|
+
(Claude Sonnet/Opus/Haiku/Fable 4.x-5.x; GPT-6 Sol/Astra/Luna and
|
|
232
|
+
gpt-4o/4o-mini/4.1/4.1-mini). A model **outside** that
|
|
227
233
|
set still runs fine, but the USD figure in the audit log is approximate — it
|
|
228
234
|
bills at a default rate and prints a one-time `no pricing for model …`
|
|
229
235
|
warning. The per-workflow call/cost **cap still applies** regardless.
|
|
@@ -259,18 +265,18 @@ qaios config set mode TRUST
|
|
|
259
265
|
|
|
260
266
|
Every command exits with a stable, documented code so you can gate pipelines:
|
|
261
267
|
|
|
262
|
-
| Code | Meaning | Example
|
|
263
|
-
| ----- | ------------------------------- |
|
|
264
|
-
| `0` | Success | tests generated; `doctor` all-green; `
|
|
265
|
-
| `1` | User error / actionable failure | bad flag; `
|
|
266
|
-
| `2` | Gate blocked (informational) |
|
|
267
|
-
| `3` | Tool/dependency error | Playwright or axe not installed; SQLite binding missing
|
|
268
|
-
| `4` | LLM error | rate limit, timeout, or invalid API key
|
|
269
|
-
| `5` | Internal error | unexpected crash (re-run with `--debug` for a stack trace)
|
|
270
|
-
| `130` | Cancelled | you pressed Ctrl+C
|
|
268
|
+
| Code | Meaning | Example |
|
|
269
|
+
| ----- | ------------------------------- | ------------------------------------------------------------------------ |
|
|
270
|
+
| `0` | Success | tests generated; `doctor` all-green; `fix --dry-run` found a valid patch |
|
|
271
|
+
| `1` | User error / actionable failure | bad flag; `run` had failing tests; `fix` found no valid fix |
|
|
272
|
+
| `2` | Gate blocked (informational) | paused for human review — run `qaios review`, or `fix` without `--auto` |
|
|
273
|
+
| `3` | Tool/dependency error | Playwright or axe not installed; SQLite binding missing |
|
|
274
|
+
| `4` | LLM error | rate limit, timeout, or invalid API key |
|
|
275
|
+
| `5` | Internal error | unexpected crash (re-run with `--debug` for a stack trace) |
|
|
276
|
+
| `130` | Cancelled | you pressed Ctrl+C |
|
|
271
277
|
|
|
272
|
-
|
|
273
|
-
|
|
278
|
+
Every command supports protocol-v1 machine output: `--json` emits one terminal object,
|
|
279
|
+
while `--jsonl` emits ordered events followed by one terminal object.
|
|
274
280
|
|
|
275
281
|
---
|
|
276
282
|
|
|
@@ -288,8 +294,10 @@ output for CI consumption.
|
|
|
288
294
|
|
|
289
295
|
## Status & roadmap
|
|
290
296
|
|
|
291
|
-
|
|
292
|
-
|
|
297
|
+
Releases and their changes are listed in the
|
|
298
|
+
[changelog](https://github.com/qatonic/qaios/blob/main/CHANGELOG.md). Current work focuses on
|
|
299
|
+
truth and consistency before the trustworthy-alpha, agent-runtime, quality-intelligence,
|
|
300
|
+
ecosystem, and stable-OS phases.
|
|
293
301
|
The [active roadmap](https://github.com/qatonic/qaios/blob/main/QAIOS_PRODUCT_AND_ENGINEERING_ROADMAP.md)
|
|
294
302
|
is validation-gated, not calendar-driven.
|
|
295
303
|
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { useApp, useInput, Box, Text } from 'ink';
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
5
|
+
|
|
6
|
+
function dispatchGateKeypress(input, ctrl = false) {
|
|
7
|
+
const key = input.toLowerCase();
|
|
8
|
+
if (key === "q" || ctrl && key === "c") return { kind: "quit" };
|
|
9
|
+
if (key === "a") return { kind: "decide", action: "approve" };
|
|
10
|
+
if (key === "r") return { kind: "decide", action: "reject" };
|
|
11
|
+
if (key === "s") return { kind: "decide", action: "skip" };
|
|
12
|
+
return { kind: "ignore" };
|
|
13
|
+
}
|
|
14
|
+
function severityColor(level) {
|
|
15
|
+
switch (level) {
|
|
16
|
+
case "CRITICAL":
|
|
17
|
+
return "red";
|
|
18
|
+
case "HIGH":
|
|
19
|
+
return "yellow";
|
|
20
|
+
case "MEDIUM":
|
|
21
|
+
return "cyan";
|
|
22
|
+
default:
|
|
23
|
+
return "gray";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function GatePreview({ gate }) {
|
|
27
|
+
const payload = gate.payload;
|
|
28
|
+
const summaryLines = [];
|
|
29
|
+
if (gate.gateType === "design_review") {
|
|
30
|
+
if (payload["featureName"]) summaryLines.push(`feature: ${String(payload["featureName"])}`);
|
|
31
|
+
if (payload["suiteName"]) summaryLines.push(`suite: ${String(payload["suiteName"])}`);
|
|
32
|
+
if (payload["scenarioCount"])
|
|
33
|
+
summaryLines.push(`scenarios: ${String(payload["scenarioCount"])}`);
|
|
34
|
+
} else if (gate.gateType === "writing_review") {
|
|
35
|
+
if (payload["fileCount"]) summaryLines.push(`files: ${String(payload["fileCount"])}`);
|
|
36
|
+
if (payload["qaiosMdSuggestionCount"]) {
|
|
37
|
+
summaryLines.push(`review notes: ${String(payload["qaiosMdSuggestionCount"])}`);
|
|
38
|
+
}
|
|
39
|
+
} else if (gate.gateType === "healing_review") {
|
|
40
|
+
if (payload["file"]) summaryLines.push(`file: ${String(payload["file"])}`);
|
|
41
|
+
if (payload["oldLocator"]) summaryLines.push(`- ${String(payload["oldLocator"])}`);
|
|
42
|
+
if (payload["newLocator"]) summaryLines.push(`+ ${String(payload["newLocator"])}`);
|
|
43
|
+
} else if (gate.gateType === "visual_review") {
|
|
44
|
+
if (payload["target"]) summaryLines.push(`screenshot: ${String(payload["target"])}`);
|
|
45
|
+
if (payload["changed"]) summaryLines.push(`changed: ${String(payload["changed"])}`);
|
|
46
|
+
if (payload["verdict"]) summaryLines.push(`classifier: ${String(payload["verdict"])}`);
|
|
47
|
+
if (payload["currentImage"]) summaryLines.push(`new image: ${String(payload["currentImage"])}`);
|
|
48
|
+
if (payload["diffImage"]) summaryLines.push(`diff image: ${String(payload["diffImage"])}`);
|
|
49
|
+
}
|
|
50
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingLeft: 2, children: [
|
|
51
|
+
summaryLines.map((l) => /* @__PURE__ */ jsx(Text, { color: "white", children: l }, l)),
|
|
52
|
+
payload["reasoning"] !== void 0 && payload["reasoning"] !== null && String(payload["reasoning"]).length > 0 && /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
53
|
+
String(payload["reasoning"]).slice(0, 240),
|
|
54
|
+
String(payload["reasoning"]).length > 240 ? "\u2026" : ""
|
|
55
|
+
] }) })
|
|
56
|
+
] });
|
|
57
|
+
}
|
|
58
|
+
function GateReview(props) {
|
|
59
|
+
const { gates, onDecide, onExit } = props;
|
|
60
|
+
const [index, setIndex] = useState(0);
|
|
61
|
+
const { exit } = useApp();
|
|
62
|
+
const current = gates[index];
|
|
63
|
+
useInput((input, key) => {
|
|
64
|
+
if (current === void 0) return;
|
|
65
|
+
const intent = dispatchGateKeypress(input, key.ctrl);
|
|
66
|
+
if (intent.kind === "quit") {
|
|
67
|
+
onExit("quit");
|
|
68
|
+
exit();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (intent.kind === "decide") {
|
|
72
|
+
onDecide(current, intent.action);
|
|
73
|
+
next();
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
function next() {
|
|
77
|
+
if (index >= gates.length - 1) {
|
|
78
|
+
onExit("completed");
|
|
79
|
+
exit();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
setIndex(index + 1);
|
|
83
|
+
}
|
|
84
|
+
if (gates.length === 0) {
|
|
85
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: /* @__PURE__ */ jsx(Text, { color: "green", children: "\u2713 No pending gates." }) });
|
|
86
|
+
}
|
|
87
|
+
if (current === void 0) {
|
|
88
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: /* @__PURE__ */ jsxs(Text, { children: [
|
|
89
|
+
"Reviewing gates\u2026 (",
|
|
90
|
+
index + 1,
|
|
91
|
+
" of ",
|
|
92
|
+
gates.length,
|
|
93
|
+
")"
|
|
94
|
+
] }) });
|
|
95
|
+
}
|
|
96
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
97
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
98
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: "\u26A0 Review: " }),
|
|
99
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: severityColor(current.riskLevel), children: current.gateType }),
|
|
100
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
101
|
+
" ",
|
|
102
|
+
"(",
|
|
103
|
+
index + 1,
|
|
104
|
+
"/",
|
|
105
|
+
gates.length,
|
|
106
|
+
")"
|
|
107
|
+
] })
|
|
108
|
+
] }),
|
|
109
|
+
/* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, { children: [
|
|
110
|
+
"Skill: ",
|
|
111
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: current.skillId }),
|
|
112
|
+
" \xB7 Risk:",
|
|
113
|
+
" ",
|
|
114
|
+
/* @__PURE__ */ jsx(Text, { color: severityColor(current.riskLevel), children: current.riskLevel }),
|
|
115
|
+
" \xB7 Confidence:",
|
|
116
|
+
" ",
|
|
117
|
+
current.confidence.toFixed(2),
|
|
118
|
+
" \xB7 Action: ",
|
|
119
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: current.requiredAction })
|
|
120
|
+
] }) }),
|
|
121
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { children: "Preview:" }) }),
|
|
122
|
+
/* @__PURE__ */ jsx(GatePreview, { gate: current }),
|
|
123
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "[a]pprove \xB7 [r]eject \xB7 [s]kip \xB7 [q]uit" }) })
|
|
124
|
+
] });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export { GateReview, dispatchGateKeypress };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { useApp, useInput, Box, Text } from 'ink';
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
5
|
+
|
|
6
|
+
function dispatchKeypress(input) {
|
|
7
|
+
switch (input.toLowerCase()) {
|
|
8
|
+
case "a":
|
|
9
|
+
return { kind: "decide", action: "approve" };
|
|
10
|
+
case "r":
|
|
11
|
+
return { kind: "decide", action: "reject" };
|
|
12
|
+
case "s":
|
|
13
|
+
return { kind: "decide", action: "skip" };
|
|
14
|
+
case "q":
|
|
15
|
+
return { kind: "quit" };
|
|
16
|
+
default:
|
|
17
|
+
return { kind: "ignore" };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function formatPct(p) {
|
|
21
|
+
if (p >= 1) return `${p.toFixed(2)}%`;
|
|
22
|
+
if (p > 0) return `${p.toFixed(3)}%`;
|
|
23
|
+
return `0%`;
|
|
24
|
+
}
|
|
25
|
+
function ClassificationView({
|
|
26
|
+
classification
|
|
27
|
+
}) {
|
|
28
|
+
if (classification === null) {
|
|
29
|
+
return /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: "(unclassified \u2014 visual.diff was skipped or failed)" });
|
|
30
|
+
}
|
|
31
|
+
const color = classification.verdict === "regression" ? "red" : classification.verdict === "intentional" ? "green" : classification.verdict === "noise" ? "gray" : "yellow";
|
|
32
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
33
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
34
|
+
"Verdict:",
|
|
35
|
+
" ",
|
|
36
|
+
/* @__PURE__ */ jsx(Text, { color, bold: true, children: classification.verdict }),
|
|
37
|
+
" ",
|
|
38
|
+
"(confidence ",
|
|
39
|
+
classification.confidence.toFixed(2),
|
|
40
|
+
")"
|
|
41
|
+
] }),
|
|
42
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
43
|
+
"Suggested: ",
|
|
44
|
+
classification.suggestedAction
|
|
45
|
+
] }),
|
|
46
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { wrap: "wrap", children: classification.reasoning }) }),
|
|
47
|
+
classification.affectedAreas.length > 0 ? /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { children: [
|
|
48
|
+
"Affected: ",
|
|
49
|
+
classification.affectedAreas.join(", ")
|
|
50
|
+
] }) }) : null
|
|
51
|
+
] });
|
|
52
|
+
}
|
|
53
|
+
function SnapshotReview({
|
|
54
|
+
items,
|
|
55
|
+
onDecide,
|
|
56
|
+
onExit
|
|
57
|
+
}) {
|
|
58
|
+
const { exit } = useApp();
|
|
59
|
+
const [index, setIndex] = useState(0);
|
|
60
|
+
const [exited, setExited] = useState(false);
|
|
61
|
+
const finish = (reason) => {
|
|
62
|
+
if (exited) return;
|
|
63
|
+
setExited(true);
|
|
64
|
+
onExit(reason);
|
|
65
|
+
exit();
|
|
66
|
+
};
|
|
67
|
+
useInput((input) => {
|
|
68
|
+
if (exited || items.length === 0 || index >= items.length) return;
|
|
69
|
+
const intent = dispatchKeypress(input);
|
|
70
|
+
if (intent.kind === "ignore") return;
|
|
71
|
+
if (intent.kind === "quit") {
|
|
72
|
+
finish("quit");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
onDecide(items[index], intent.action);
|
|
76
|
+
const next = index + 1;
|
|
77
|
+
if (next >= items.length) finish("completed");
|
|
78
|
+
else setIndex(next);
|
|
79
|
+
});
|
|
80
|
+
if (items.length === 0) {
|
|
81
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: /* @__PURE__ */ jsx(Text, { children: "(no pending visual diffs)" }) });
|
|
82
|
+
}
|
|
83
|
+
if (exited || index >= items.length) {
|
|
84
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: /* @__PURE__ */ jsx(Text, { children: "Review complete." }) });
|
|
85
|
+
}
|
|
86
|
+
const current = items[index];
|
|
87
|
+
const total = items.length;
|
|
88
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
89
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
90
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: "QAIOS visual review" }),
|
|
91
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: ` (${index + 1} / ${total})` })
|
|
92
|
+
] }),
|
|
93
|
+
/* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
|
|
94
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
95
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: current.baseline.name }),
|
|
96
|
+
/* @__PURE__ */ jsx(Text, { children: `@${current.baseline.viewport}` })
|
|
97
|
+
] }),
|
|
98
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
99
|
+
"route: ",
|
|
100
|
+
current.baseline.route
|
|
101
|
+
] }),
|
|
102
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
103
|
+
formatPct(current.diff.percentageChanged),
|
|
104
|
+
" pixels changed (",
|
|
105
|
+
current.diff.pixelsChanged,
|
|
106
|
+
" ",
|
|
107
|
+
"px)"
|
|
108
|
+
] }),
|
|
109
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
110
|
+
"baseline: ",
|
|
111
|
+
current.baseline.imagePath
|
|
112
|
+
] }),
|
|
113
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
114
|
+
"current: ",
|
|
115
|
+
current.diff.currentImagePath
|
|
116
|
+
] }),
|
|
117
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
118
|
+
"diff: ",
|
|
119
|
+
current.diff.diffImagePath
|
|
120
|
+
] })
|
|
121
|
+
] }),
|
|
122
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx(ClassificationView, { classification: current.diff.classification ?? null }) }),
|
|
123
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { children: [
|
|
124
|
+
"[",
|
|
125
|
+
/* @__PURE__ */ jsx(Text, { color: "green", children: "a" }),
|
|
126
|
+
"]pprove \u2192 update baseline \xB7 [",
|
|
127
|
+
/* @__PURE__ */ jsx(Text, { color: "red", children: "r" }),
|
|
128
|
+
"]eject \u2192 keep baseline \xB7 [",
|
|
129
|
+
/* @__PURE__ */ jsx(Text, { color: "yellow", children: "s" }),
|
|
130
|
+
"]kip \xB7 [",
|
|
131
|
+
/* @__PURE__ */ jsx(Text, { children: "q" }),
|
|
132
|
+
"]uit"
|
|
133
|
+
] }) })
|
|
134
|
+
] });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export { SnapshotReview, dispatchKeypress };
|
|
@@ -78,6 +78,12 @@ try {
|
|
|
78
78
|
const context = await browser.newContext();
|
|
79
79
|
const page = await context.newPage();
|
|
80
80
|
await page.goto(url, { timeout: timeoutMs, waitUntil });
|
|
81
|
+
// Single-page apps render after DOMContentLoaded (live test: an Angular shop
|
|
82
|
+
// scanned as an empty shell, 0 violations vs 27). Let the network settle,
|
|
83
|
+
// best effort and bounded.
|
|
84
|
+
if (waitUntil !== 'networkidle') {
|
|
85
|
+
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {});
|
|
86
|
+
}
|
|
81
87
|
|
|
82
88
|
const builder = new AxeBuilder({ page });
|
|
83
89
|
if (tags.length > 0) builder.withTags(tags);
|
|
@@ -99,7 +105,8 @@ try {
|
|
|
99
105
|
...tag(results.incomplete, 'incomplete'),
|
|
100
106
|
];
|
|
101
107
|
|
|
102
|
-
|
|
108
|
+
// finalUrl: where navigation ended (a login redirect is reported, not hidden).
|
|
109
|
+
writeFileSync(out, JSON.stringify({ url, finalUrl: page.url(), findings }), 'utf-8');
|
|
103
110
|
} catch (err) {
|
|
104
111
|
process.stderr.write(`axe-runner: ${err?.message ?? err}\n`);
|
|
105
112
|
process.exit(4);
|