@monoes/monobrowse 1.0.12 → 1.0.13
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/dist/src/__tests__/profiler-heap.test.d.ts +2 -0
- package/dist/src/__tests__/profiler-heap.test.d.ts.map +1 -0
- package/dist/src/__tests__/profiler-heap.test.js +154 -0
- package/dist/src/__tests__/profiler-heap.test.js.map +1 -0
- package/dist/src/__tests__/report-a11y.test.d.ts +2 -0
- package/dist/src/__tests__/report-a11y.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-a11y.test.js +147 -0
- package/dist/src/__tests__/report-a11y.test.js.map +1 -0
- package/dist/src/__tests__/report-analyze.test.d.ts +2 -0
- package/dist/src/__tests__/report-analyze.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-analyze.test.js +157 -0
- package/dist/src/__tests__/report-analyze.test.js.map +1 -0
- package/dist/src/__tests__/report-budget.test.d.ts +2 -0
- package/dist/src/__tests__/report-budget.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-budget.test.js +80 -0
- package/dist/src/__tests__/report-budget.test.js.map +1 -0
- package/dist/src/__tests__/report-diff.test.d.ts +2 -0
- package/dist/src/__tests__/report-diff.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-diff.test.js +231 -0
- package/dist/src/__tests__/report-diff.test.js.map +1 -0
- package/dist/src/__tests__/report-evidence.test.d.ts +2 -0
- package/dist/src/__tests__/report-evidence.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-evidence.test.js +214 -0
- package/dist/src/__tests__/report-evidence.test.js.map +1 -0
- package/dist/src/__tests__/report-flake.test.d.ts +2 -0
- package/dist/src/__tests__/report-flake.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-flake.test.js +182 -0
- package/dist/src/__tests__/report-flake.test.js.map +1 -0
- package/dist/src/__tests__/report-history.test.d.ts +2 -0
- package/dist/src/__tests__/report-history.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-history.test.js +229 -0
- package/dist/src/__tests__/report-history.test.js.map +1 -0
- package/dist/src/__tests__/report-output.test.d.ts +2 -0
- package/dist/src/__tests__/report-output.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-output.test.js +169 -0
- package/dist/src/__tests__/report-output.test.js.map +1 -0
- package/dist/src/__tests__/report-png.test.d.ts +2 -0
- package/dist/src/__tests__/report-png.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-png.test.js +84 -0
- package/dist/src/__tests__/report-png.test.js.map +1 -0
- package/dist/src/__tests__/report-render-extras.test.d.ts +2 -0
- package/dist/src/__tests__/report-render-extras.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-render-extras.test.js +285 -0
- package/dist/src/__tests__/report-render-extras.test.js.map +1 -0
- package/dist/src/__tests__/report-render.test.d.ts +2 -0
- package/dist/src/__tests__/report-render.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-render.test.js +143 -0
- package/dist/src/__tests__/report-render.test.js.map +1 -0
- package/dist/src/__tests__/report-run.test.d.ts +2 -0
- package/dist/src/__tests__/report-run.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-run.test.js +234 -0
- package/dist/src/__tests__/report-run.test.js.map +1 -0
- package/dist/src/browser/index.d.ts +1 -0
- package/dist/src/browser/index.d.ts.map +1 -1
- package/dist/src/browser/index.js +1 -0
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/browser/profiler.d.ts.map +1 -1
- package/dist/src/browser/profiler.js +19 -22
- package/dist/src/browser/profiler.js.map +1 -1
- package/dist/src/cli/commands.d.ts.map +1 -1
- package/dist/src/cli/commands.js +254 -0
- package/dist/src/cli/commands.js.map +1 -1
- package/dist/src/report/a11y.d.ts +52 -0
- package/dist/src/report/a11y.d.ts.map +1 -0
- package/dist/src/report/a11y.js +231 -0
- package/dist/src/report/a11y.js.map +1 -0
- package/dist/src/report/analyze.d.ts +20 -0
- package/dist/src/report/analyze.d.ts.map +1 -0
- package/dist/src/report/analyze.js +136 -0
- package/dist/src/report/analyze.js.map +1 -0
- package/dist/src/report/budget.d.ts +27 -0
- package/dist/src/report/budget.d.ts.map +1 -0
- package/dist/src/report/budget.js +143 -0
- package/dist/src/report/budget.js.map +1 -0
- package/dist/src/report/collect-a11y.d.ts +25 -0
- package/dist/src/report/collect-a11y.d.ts.map +1 -0
- package/dist/src/report/collect-a11y.js +154 -0
- package/dist/src/report/collect-a11y.js.map +1 -0
- package/dist/src/report/collect.d.ts +45 -0
- package/dist/src/report/collect.d.ts.map +1 -0
- package/dist/src/report/collect.js +275 -0
- package/dist/src/report/collect.js.map +1 -0
- package/dist/src/report/evidence.d.ts +80 -0
- package/dist/src/report/evidence.d.ts.map +1 -0
- package/dist/src/report/evidence.js +279 -0
- package/dist/src/report/evidence.js.map +1 -0
- package/dist/src/report/flake.d.ts +88 -0
- package/dist/src/report/flake.d.ts.map +1 -0
- package/dist/src/report/flake.js +232 -0
- package/dist/src/report/flake.js.map +1 -0
- package/dist/src/report/history.d.ts +85 -0
- package/dist/src/report/history.d.ts.map +1 -0
- package/dist/src/report/history.js +176 -0
- package/dist/src/report/history.js.map +1 -0
- package/dist/src/report/index.d.ts +102 -0
- package/dist/src/report/index.d.ts.map +1 -0
- package/dist/src/report/index.js +216 -0
- package/dist/src/report/index.js.map +1 -0
- package/dist/src/report/pixel-diff.d.ts +30 -0
- package/dist/src/report/pixel-diff.d.ts.map +1 -0
- package/dist/src/report/pixel-diff.js +175 -0
- package/dist/src/report/pixel-diff.js.map +1 -0
- package/dist/src/report/png.d.ts +28 -0
- package/dist/src/report/png.d.ts.map +1 -0
- package/dist/src/report/png.js +259 -0
- package/dist/src/report/png.js.map +1 -0
- package/dist/src/report/render-evidence.d.ts +16 -0
- package/dist/src/report/render-evidence.d.ts.map +1 -0
- package/dist/src/report/render-evidence.js +174 -0
- package/dist/src/report/render-evidence.js.map +1 -0
- package/dist/src/report/render-trend.d.ts +16 -0
- package/dist/src/report/render-trend.d.ts.map +1 -0
- package/dist/src/report/render-trend.js +194 -0
- package/dist/src/report/render-trend.js.map +1 -0
- package/dist/src/report/render.d.ts +21 -0
- package/dist/src/report/render.d.ts.map +1 -0
- package/dist/src/report/render.js +286 -0
- package/dist/src/report/render.js.map +1 -0
- package/dist/src/report/run-diff.d.ts +18 -0
- package/dist/src/report/run-diff.d.ts.map +1 -0
- package/dist/src/report/run-diff.js +51 -0
- package/dist/src/report/run-diff.js.map +1 -0
- package/dist/src/report/structure.d.ts +41 -0
- package/dist/src/report/structure.d.ts.map +1 -0
- package/dist/src/report/structure.js +210 -0
- package/dist/src/report/structure.js.map +1 -0
- package/dist/src/report/trend.d.ts +26 -0
- package/dist/src/report/trend.d.ts.map +1 -0
- package/dist/src/report/trend.js +164 -0
- package/dist/src/report/trend.js.map +1 -0
- package/dist/src/report/types.d.ts +320 -0
- package/dist/src/report/types.d.ts.map +1 -0
- package/dist/src/report/types.js +11 -0
- package/dist/src/report/types.js.map +1 -0
- package/dist/src/report/util.d.ts +17 -0
- package/dist/src/report/util.d.ts.map +1 -0
- package/dist/src/report/util.js +45 -0
- package/dist/src/report/util.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/__tests__/profiler-heap.test.ts +191 -0
- package/src/__tests__/report-a11y.test.ts +180 -0
- package/src/__tests__/report-analyze.test.ts +202 -0
- package/src/__tests__/report-budget.test.ts +92 -0
- package/src/__tests__/report-diff.test.ts +272 -0
- package/src/__tests__/report-evidence.test.ts +276 -0
- package/src/__tests__/report-flake.test.ts +214 -0
- package/src/__tests__/report-history.test.ts +293 -0
- package/src/__tests__/report-output.test.ts +210 -0
- package/src/__tests__/report-png.test.ts +95 -0
- package/src/__tests__/report-render-extras.test.ts +315 -0
- package/src/__tests__/report-render.test.ts +175 -0
- package/src/__tests__/report-run.test.ts +264 -0
- package/src/browser/index.ts +1 -0
- package/src/browser/profiler.ts +28 -21
- package/src/cli/commands.ts +272 -0
- package/src/report/a11y.ts +242 -0
- package/src/report/analyze.ts +181 -0
- package/src/report/budget.ts +155 -0
- package/src/report/collect-a11y.ts +200 -0
- package/src/report/collect.ts +408 -0
- package/src/report/evidence.ts +379 -0
- package/src/report/flake.ts +303 -0
- package/src/report/history.ts +219 -0
- package/src/report/index.ts +307 -0
- package/src/report/pixel-diff.ts +197 -0
- package/src/report/png.ts +290 -0
- package/src/report/render-evidence.ts +194 -0
- package/src/report/render-trend.ts +225 -0
- package/src/report/render.ts +314 -0
- package/src/report/run-diff.ts +63 -0
- package/src/report/structure.ts +212 -0
- package/src/report/trend.ts +189 -0
- package/src/report/types.ts +356 -0
- package/src/report/util.ts +48 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accessibility rules computed from the CDP accessibility tree.
|
|
3
|
+
*
|
|
4
|
+
* Deliberate scope limit: colour contrast is NOT computed here. Contrast
|
|
5
|
+
* needs resolved foreground/background pixels or computed styles, and the AX
|
|
6
|
+
* tree carries neither — guessing it would produce confident nonsense. The
|
|
7
|
+
* report says so explicitly instead.
|
|
8
|
+
*
|
|
9
|
+
* Every rule is a pure function of a node array, so the fixtures in the tests
|
|
10
|
+
* are just hand-written AX trees.
|
|
11
|
+
*/
|
|
12
|
+
import type { A11yFinding, AxNode, FocusCandidate } from './types.js';
|
|
13
|
+
/**
|
|
14
|
+
* Depth-first document order. `Accessibility.getFullAXTree` usually returns
|
|
15
|
+
* nodes pre-ordered already, but heading-order is the one rule where a
|
|
16
|
+
* reordered array would silently produce wrong findings, so we walk childIds
|
|
17
|
+
* ourselves. Nodes unreachable from a root are appended in array order rather
|
|
18
|
+
* than dropped.
|
|
19
|
+
*/
|
|
20
|
+
export declare function orderNodes(nodes: AxNode[]): AxNode[];
|
|
21
|
+
/**
|
|
22
|
+
* Nameless controls, images without alt text, and unlabelled form fields.
|
|
23
|
+
*
|
|
24
|
+
* `ignored` nodes are skipped: that is how `<img alt="">` correctly escapes
|
|
25
|
+
* the image rule — a deliberately decorative image is dropped from the AX
|
|
26
|
+
* tree, while `<img>` with no alt attribute at all stays in it with an empty
|
|
27
|
+
* name, which is the bug we want to report.
|
|
28
|
+
*/
|
|
29
|
+
export declare function findNamingIssues(nodes: AxNode[], locators?: Map<number, string>): A11yFinding[];
|
|
30
|
+
/**
|
|
31
|
+
* Headings that skip a level (h1 -> h3), plus a document whose first heading
|
|
32
|
+
* is not h1. Both break the outline screen-reader users navigate by.
|
|
33
|
+
*/
|
|
34
|
+
export declare function findHeadingIssues(nodes: AxNode[], locators?: Map<number, string>): A11yFinding[];
|
|
35
|
+
/**
|
|
36
|
+
* Interactive elements pulled out of the keyboard tab order by a negative
|
|
37
|
+
* tabindex. They stay clickable with a mouse and stay in the AX tree, so
|
|
38
|
+
* nothing else in this file can see them — the candidates come from a DOM
|
|
39
|
+
* sweep in collect.ts.
|
|
40
|
+
*/
|
|
41
|
+
export declare function findFocusOrderIssues(candidates: FocusCandidate[]): A11yFinding[];
|
|
42
|
+
export interface A11yInput {
|
|
43
|
+
nodes: AxNode[];
|
|
44
|
+
focusCandidates?: FocusCandidate[];
|
|
45
|
+
locators?: Map<number, string>;
|
|
46
|
+
}
|
|
47
|
+
export declare function runA11yRules({ nodes, focusCandidates, locators }: A11yInput): A11yFinding[];
|
|
48
|
+
export declare function countA11y(findings: A11yFinding[]): {
|
|
49
|
+
errors: number;
|
|
50
|
+
warnings: number;
|
|
51
|
+
};
|
|
52
|
+
//# sourceMappingURL=a11y.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"a11y.d.ts","sourceRoot":"","sources":["../../../src/report/a11y.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAuDtE;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAsBpD;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,QAAQ,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,WAAW,EAAE,CA6C/F;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,QAAQ,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,WAAW,EAAE,CAoChG;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,cAAc,EAAE,GAAG,WAAW,EAAE,CAgBhF;AAED,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,eAAe,CAAC,EAAE,cAAc,EAAE,CAAC;IACnC,QAAQ,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC;AAED,wBAAgB,YAAY,CAAC,EAAE,KAAK,EAAE,eAAoB,EAAE,QAAQ,EAAE,EAAE,SAAS,GAAG,WAAW,EAAE,CAMhG;AAED,wBAAgB,SAAS,CAAC,QAAQ,EAAE,WAAW,EAAE,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAQvF"}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accessibility rules computed from the CDP accessibility tree.
|
|
3
|
+
*
|
|
4
|
+
* Deliberate scope limit: colour contrast is NOT computed here. Contrast
|
|
5
|
+
* needs resolved foreground/background pixels or computed styles, and the AX
|
|
6
|
+
* tree carries neither — guessing it would produce confident nonsense. The
|
|
7
|
+
* report says so explicitly instead.
|
|
8
|
+
*
|
|
9
|
+
* Every rule is a pure function of a node array, so the fixtures in the tests
|
|
10
|
+
* are just hand-written AX trees.
|
|
11
|
+
*/
|
|
12
|
+
/** Roles whose whole purpose is to be activated — a nameless one is unusable. */
|
|
13
|
+
const COMMAND_ROLES = new Set([
|
|
14
|
+
'button',
|
|
15
|
+
'link',
|
|
16
|
+
'menuitem',
|
|
17
|
+
'menuitemcheckbox',
|
|
18
|
+
'menuitemradio',
|
|
19
|
+
'tab',
|
|
20
|
+
'switch',
|
|
21
|
+
'treeitem',
|
|
22
|
+
]);
|
|
23
|
+
/** Roles that take user input and therefore need a programmatic label. */
|
|
24
|
+
const FORM_FIELD_ROLES = new Set([
|
|
25
|
+
'textbox',
|
|
26
|
+
'searchbox',
|
|
27
|
+
'combobox',
|
|
28
|
+
'listbox',
|
|
29
|
+
'checkbox',
|
|
30
|
+
'radio',
|
|
31
|
+
'slider',
|
|
32
|
+
'spinbutton',
|
|
33
|
+
]);
|
|
34
|
+
const IMAGE_ROLES = new Set(['image', 'img']);
|
|
35
|
+
/** Natively focusable tags — a negative tabindex on these removes them from the tab order. */
|
|
36
|
+
const NATIVELY_FOCUSABLE = new Set(['a', 'button', 'input', 'select', 'textarea', 'summary']);
|
|
37
|
+
function strValue(v) {
|
|
38
|
+
return typeof v === 'string' ? v : v == null ? '' : String(v);
|
|
39
|
+
}
|
|
40
|
+
function roleOf(node) {
|
|
41
|
+
return strValue(node.role?.value).toLowerCase();
|
|
42
|
+
}
|
|
43
|
+
function nameOf(node) {
|
|
44
|
+
return strValue(node.name?.value).trim();
|
|
45
|
+
}
|
|
46
|
+
function propOf(node, name) {
|
|
47
|
+
return node.properties?.find((p) => p.name === name)?.value?.value;
|
|
48
|
+
}
|
|
49
|
+
function locatorFor(node, locators) {
|
|
50
|
+
if (node.backendDOMNodeId !== undefined) {
|
|
51
|
+
const found = locators?.get(node.backendDOMNodeId);
|
|
52
|
+
if (found)
|
|
53
|
+
return found;
|
|
54
|
+
}
|
|
55
|
+
return `ax-node:${node.nodeId}`;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Depth-first document order. `Accessibility.getFullAXTree` usually returns
|
|
59
|
+
* nodes pre-ordered already, but heading-order is the one rule where a
|
|
60
|
+
* reordered array would silently produce wrong findings, so we walk childIds
|
|
61
|
+
* ourselves. Nodes unreachable from a root are appended in array order rather
|
|
62
|
+
* than dropped.
|
|
63
|
+
*/
|
|
64
|
+
export function orderNodes(nodes) {
|
|
65
|
+
const byId = new Map();
|
|
66
|
+
for (const n of nodes)
|
|
67
|
+
byId.set(String(n.nodeId), n);
|
|
68
|
+
const ids = new Set(byId.keys());
|
|
69
|
+
const roots = nodes.filter((n) => n.parentId === undefined || !ids.has(String(n.parentId)));
|
|
70
|
+
const seen = new Set();
|
|
71
|
+
const ordered = [];
|
|
72
|
+
const visit = (node) => {
|
|
73
|
+
const id = String(node.nodeId);
|
|
74
|
+
if (seen.has(id))
|
|
75
|
+
return;
|
|
76
|
+
seen.add(id);
|
|
77
|
+
ordered.push(node);
|
|
78
|
+
for (const childId of node.childIds ?? []) {
|
|
79
|
+
const child = byId.get(String(childId));
|
|
80
|
+
if (child)
|
|
81
|
+
visit(child);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
for (const root of roots)
|
|
85
|
+
visit(root);
|
|
86
|
+
for (const node of nodes)
|
|
87
|
+
if (!seen.has(String(node.nodeId)))
|
|
88
|
+
ordered.push(node);
|
|
89
|
+
return ordered;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Nameless controls, images without alt text, and unlabelled form fields.
|
|
93
|
+
*
|
|
94
|
+
* `ignored` nodes are skipped: that is how `<img alt="">` correctly escapes
|
|
95
|
+
* the image rule — a deliberately decorative image is dropped from the AX
|
|
96
|
+
* tree, while `<img>` with no alt attribute at all stays in it with an empty
|
|
97
|
+
* name, which is the bug we want to report.
|
|
98
|
+
*/
|
|
99
|
+
export function findNamingIssues(nodes, locators) {
|
|
100
|
+
const findings = [];
|
|
101
|
+
for (const node of nodes) {
|
|
102
|
+
if (node.ignored)
|
|
103
|
+
continue;
|
|
104
|
+
const role = roleOf(node);
|
|
105
|
+
if (!role)
|
|
106
|
+
continue;
|
|
107
|
+
const name = nameOf(node);
|
|
108
|
+
if (name)
|
|
109
|
+
continue;
|
|
110
|
+
if (FORM_FIELD_ROLES.has(role)) {
|
|
111
|
+
findings.push({
|
|
112
|
+
rule: 'form-field-no-label',
|
|
113
|
+
impact: 'error',
|
|
114
|
+
role,
|
|
115
|
+
name: null,
|
|
116
|
+
locator: locatorFor(node, locators),
|
|
117
|
+
detail: `${role} has no accessible name — no <label>, aria-label or aria-labelledby resolves to one`,
|
|
118
|
+
});
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (COMMAND_ROLES.has(role)) {
|
|
122
|
+
findings.push({
|
|
123
|
+
rule: 'unlabelled-control',
|
|
124
|
+
impact: 'error',
|
|
125
|
+
role,
|
|
126
|
+
name: null,
|
|
127
|
+
locator: locatorFor(node, locators),
|
|
128
|
+
detail: `${role} has no accessible name — a screen reader announces it as just "${role}"`,
|
|
129
|
+
});
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (IMAGE_ROLES.has(role)) {
|
|
133
|
+
findings.push({
|
|
134
|
+
rule: 'image-missing-alt',
|
|
135
|
+
impact: 'error',
|
|
136
|
+
role,
|
|
137
|
+
name: null,
|
|
138
|
+
locator: locatorFor(node, locators),
|
|
139
|
+
detail: 'image has no alt text (use alt="" if it is purely decorative)',
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return findings;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Headings that skip a level (h1 -> h3), plus a document whose first heading
|
|
147
|
+
* is not h1. Both break the outline screen-reader users navigate by.
|
|
148
|
+
*/
|
|
149
|
+
export function findHeadingIssues(nodes, locators) {
|
|
150
|
+
const findings = [];
|
|
151
|
+
let previous = null;
|
|
152
|
+
for (const node of orderNodes(nodes)) {
|
|
153
|
+
if (node.ignored)
|
|
154
|
+
continue;
|
|
155
|
+
if (roleOf(node) !== 'heading')
|
|
156
|
+
continue;
|
|
157
|
+
const raw = propOf(node, 'level');
|
|
158
|
+
const level = typeof raw === 'number' ? raw : Number.parseInt(strValue(raw), 10);
|
|
159
|
+
if (!Number.isFinite(level) || level < 1)
|
|
160
|
+
continue;
|
|
161
|
+
const name = nameOf(node);
|
|
162
|
+
if (previous === null) {
|
|
163
|
+
if (level > 1) {
|
|
164
|
+
findings.push({
|
|
165
|
+
rule: 'heading-order-jump',
|
|
166
|
+
impact: 'warning',
|
|
167
|
+
role: 'heading',
|
|
168
|
+
name: name || null,
|
|
169
|
+
locator: locatorFor(node, locators),
|
|
170
|
+
detail: `document outline starts at h${level} — the first heading should be h1`,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else if (level > previous + 1) {
|
|
175
|
+
findings.push({
|
|
176
|
+
rule: 'heading-order-jump',
|
|
177
|
+
impact: 'warning',
|
|
178
|
+
role: 'heading',
|
|
179
|
+
name: name || null,
|
|
180
|
+
locator: locatorFor(node, locators),
|
|
181
|
+
detail: `heading level jumps h${previous} -> h${level}`,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
previous = level;
|
|
185
|
+
}
|
|
186
|
+
return findings;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Interactive elements pulled out of the keyboard tab order by a negative
|
|
190
|
+
* tabindex. They stay clickable with a mouse and stay in the AX tree, so
|
|
191
|
+
* nothing else in this file can see them — the candidates come from a DOM
|
|
192
|
+
* sweep in collect.ts.
|
|
193
|
+
*/
|
|
194
|
+
export function findFocusOrderIssues(candidates) {
|
|
195
|
+
const findings = [];
|
|
196
|
+
for (const c of candidates) {
|
|
197
|
+
if (c.tabindex >= 0)
|
|
198
|
+
continue;
|
|
199
|
+
const interactive = NATIVELY_FOCUSABLE.has(c.tag.toLowerCase()) || c.role !== '';
|
|
200
|
+
if (!interactive)
|
|
201
|
+
continue;
|
|
202
|
+
findings.push({
|
|
203
|
+
rule: 'negative-tabindex',
|
|
204
|
+
impact: 'warning',
|
|
205
|
+
role: c.role || c.tag.toLowerCase(),
|
|
206
|
+
name: c.name,
|
|
207
|
+
locator: c.locator,
|
|
208
|
+
detail: `<${c.tag.toLowerCase()}> has tabindex="${c.tabindex}" — mouse users can reach it, keyboard users cannot`,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
return findings;
|
|
212
|
+
}
|
|
213
|
+
export function runA11yRules({ nodes, focusCandidates = [], locators }) {
|
|
214
|
+
return [
|
|
215
|
+
...findNamingIssues(nodes, locators),
|
|
216
|
+
...findHeadingIssues(nodes, locators),
|
|
217
|
+
...findFocusOrderIssues(focusCandidates),
|
|
218
|
+
];
|
|
219
|
+
}
|
|
220
|
+
export function countA11y(findings) {
|
|
221
|
+
let errors = 0;
|
|
222
|
+
let warnings = 0;
|
|
223
|
+
for (const f of findings) {
|
|
224
|
+
if (f.impact === 'error')
|
|
225
|
+
errors++;
|
|
226
|
+
else
|
|
227
|
+
warnings++;
|
|
228
|
+
}
|
|
229
|
+
return { errors, warnings };
|
|
230
|
+
}
|
|
231
|
+
//# sourceMappingURL=a11y.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"a11y.js","sourceRoot":"","sources":["../../../src/report/a11y.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Budget evaluation — the step that makes "the agent tested it" mean
|
|
3
|
+
* something. Pure function of CaptureData + Budget, so CI behaviour is
|
|
4
|
+
* testable without a browser.
|
|
5
|
+
*/
|
|
6
|
+
import type { Budget, BudgetFailure, CaptureData, ConsoleEntry, Report, RequestEntry, VerdictValue } from './types.js';
|
|
7
|
+
export declare function consoleErrors(entries: ConsoleEntry[]): ConsoleEntry[];
|
|
8
|
+
export declare function consoleWarnings(entries: ConsoleEntry[]): ConsoleEntry[];
|
|
9
|
+
export declare function failedRequests(requests: RequestEntry[]): RequestEntry[];
|
|
10
|
+
export interface Evaluation {
|
|
11
|
+
verdict: VerdictValue;
|
|
12
|
+
failures: BudgetFailure[];
|
|
13
|
+
/** Budgets that were set but could not be checked — the page never reported the metric. */
|
|
14
|
+
unmeasured: string[];
|
|
15
|
+
}
|
|
16
|
+
export declare function evaluateBudget(data: CaptureData, budget: Budget): Evaluation;
|
|
17
|
+
export declare function buildReport(data: CaptureData, budget: Budget): Report;
|
|
18
|
+
/** One line for a CI log / agent transcript. */
|
|
19
|
+
export declare function summarizeVerdict(report: Report): string;
|
|
20
|
+
//# sourceMappingURL=analyze.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyze.d.ts","sourceRoot":"","sources":["../../../src/report/analyze.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAEV,MAAM,EACN,aAAa,EACb,WAAW,EACX,YAAY,EACZ,MAAM,EACN,YAAY,EACZ,YAAY,EACb,MAAM,YAAY,CAAC;AAKpB,wBAAgB,aAAa,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,YAAY,EAAE,CAErE;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,YAAY,EAAE,CAEvE;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,YAAY,EAAE,GAAG,YAAY,EAAE,CAEvE;AAgCD,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,YAAY,CAAC;IACtB,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,2FAA2F;IAC3F,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,CA+E5E;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAmBrE;AAED,gDAAgD;AAChD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAMvD"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Budget evaluation — the step that makes "the agent tested it" mean
|
|
3
|
+
* something. Pure function of CaptureData + Budget, so CI behaviour is
|
|
4
|
+
* testable without a browser.
|
|
5
|
+
*/
|
|
6
|
+
import { countA11y } from './a11y.js';
|
|
7
|
+
import { budgetLabel, formatThreshold } from './budget.js';
|
|
8
|
+
/** How many concrete examples to quote in a failure's `detail`. */
|
|
9
|
+
const DETAIL_SAMPLE = 3;
|
|
10
|
+
export function consoleErrors(entries) {
|
|
11
|
+
return entries.filter((e) => e.type === 'error');
|
|
12
|
+
}
|
|
13
|
+
export function consoleWarnings(entries) {
|
|
14
|
+
return entries.filter((e) => e.type === 'warn' || e.type === 'warning');
|
|
15
|
+
}
|
|
16
|
+
export function failedRequests(requests) {
|
|
17
|
+
return requests.filter((r) => r.failed);
|
|
18
|
+
}
|
|
19
|
+
function truncate(text, max = 160) {
|
|
20
|
+
const flat = text.replace(/\s+/g, ' ').trim();
|
|
21
|
+
return flat.length > max ? `${flat.slice(0, max - 1)}…` : flat;
|
|
22
|
+
}
|
|
23
|
+
function sample(items) {
|
|
24
|
+
// Not `.map(truncate)`: map passes the index as the second argument, which
|
|
25
|
+
// would become truncate's `max` and shave every sample down to nothing.
|
|
26
|
+
const shown = items.slice(0, DETAIL_SAMPLE).map((item) => truncate(item));
|
|
27
|
+
const rest = items.length - shown.length;
|
|
28
|
+
return rest > 0 ? `${shown.join(' | ')} (+${rest} more)` : shown.join(' | ');
|
|
29
|
+
}
|
|
30
|
+
function describeRequest(r) {
|
|
31
|
+
const status = r.status !== undefined ? String(r.status) : (r.errorText ?? 'no response');
|
|
32
|
+
return `${status} ${r.method} ${r.url}`;
|
|
33
|
+
}
|
|
34
|
+
export function evaluateBudget(data, budget) {
|
|
35
|
+
const failures = [];
|
|
36
|
+
const unmeasured = [];
|
|
37
|
+
const cErrors = consoleErrors(data.console);
|
|
38
|
+
const failed = failedRequests(data.requests);
|
|
39
|
+
const a11y = countA11y(data.a11y);
|
|
40
|
+
const counts = [
|
|
41
|
+
{
|
|
42
|
+
key: 'maxConsoleErrors',
|
|
43
|
+
actual: cErrors.length,
|
|
44
|
+
detail: sample(cErrors.map((e) => e.text)),
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
key: 'maxPageErrors',
|
|
48
|
+
actual: data.pageErrors.length,
|
|
49
|
+
detail: sample(data.pageErrors.map((e) => e.text)),
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
key: 'maxFailedRequests',
|
|
53
|
+
actual: failed.length,
|
|
54
|
+
detail: sample(failed.map(describeRequest)),
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
key: 'maxA11yErrors',
|
|
58
|
+
actual: a11y.errors,
|
|
59
|
+
detail: sample(data.a11y
|
|
60
|
+
.filter((f) => f.impact === 'error')
|
|
61
|
+
.map((f) => `${f.rule} at ${f.locator}`)),
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
for (const check of counts) {
|
|
65
|
+
const limit = budget[check.key];
|
|
66
|
+
if (limit === null)
|
|
67
|
+
continue;
|
|
68
|
+
if (check.actual > limit) {
|
|
69
|
+
failures.push({
|
|
70
|
+
budget: check.key,
|
|
71
|
+
expected: formatThreshold(check.key, limit),
|
|
72
|
+
actual: String(check.actual),
|
|
73
|
+
detail: check.detail || undefined,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const ms = (v) => `${Math.round(v)}ms`;
|
|
78
|
+
const score = (v) => v.toFixed(4).replace(/0+$/, '').replace(/\.$/, '');
|
|
79
|
+
const metrics = [
|
|
80
|
+
{ key: 'lcpMs', actual: data.vitals.lcp, format: ms },
|
|
81
|
+
{ key: 'clsScore', actual: data.vitals.cls, format: score },
|
|
82
|
+
{ key: 'inpMs', actual: data.vitals.inp, format: ms },
|
|
83
|
+
{ key: 'fcpMs', actual: data.vitals.fcp, format: ms },
|
|
84
|
+
{ key: 'ttfbMs', actual: data.vitals.ttfb, format: ms },
|
|
85
|
+
];
|
|
86
|
+
for (const metric of metrics) {
|
|
87
|
+
const limit = budget[metric.key];
|
|
88
|
+
if (limit === null)
|
|
89
|
+
continue;
|
|
90
|
+
if (metric.actual === undefined || !Number.isFinite(metric.actual)) {
|
|
91
|
+
// A page that never fires an LCP entry (no contentful paint, an
|
|
92
|
+
// observer the browser does not support, a collector that timed out)
|
|
93
|
+
// must not be silently graded as passing — but it is not a breach
|
|
94
|
+
// either. Surface it as unmeasured and let the report say so.
|
|
95
|
+
unmeasured.push(budgetLabel(metric.key));
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (metric.actual > limit) {
|
|
99
|
+
failures.push({
|
|
100
|
+
budget: metric.key,
|
|
101
|
+
expected: formatThreshold(metric.key, limit),
|
|
102
|
+
actual: metric.format(metric.actual),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { verdict: failures.length > 0 ? 'fail' : 'pass', failures, unmeasured };
|
|
107
|
+
}
|
|
108
|
+
export function buildReport(data, budget) {
|
|
109
|
+
const evaluation = evaluateBudget(data, budget);
|
|
110
|
+
const a11y = countA11y(data.a11y);
|
|
111
|
+
return {
|
|
112
|
+
...data,
|
|
113
|
+
budget,
|
|
114
|
+
verdict: evaluation.verdict,
|
|
115
|
+
failures: evaluation.failures,
|
|
116
|
+
unmeasured: evaluation.unmeasured,
|
|
117
|
+
counts: {
|
|
118
|
+
consoleErrors: consoleErrors(data.console).length,
|
|
119
|
+
consoleWarnings: consoleWarnings(data.console).length,
|
|
120
|
+
pageErrors: data.pageErrors.length,
|
|
121
|
+
requests: data.requests.length,
|
|
122
|
+
failedRequests: failedRequests(data.requests).length,
|
|
123
|
+
a11yErrors: a11y.errors,
|
|
124
|
+
a11yWarnings: a11y.warnings,
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/** One line for a CI log / agent transcript. */
|
|
129
|
+
export function summarizeVerdict(report) {
|
|
130
|
+
if (report.verdict === 'pass') {
|
|
131
|
+
return `PASS — ${report.counts.requests} requests, 0 budget failures`;
|
|
132
|
+
}
|
|
133
|
+
const names = report.failures.map((f) => budgetLabel(f.budget)).join(', ');
|
|
134
|
+
return `FAIL — ${report.failures.length} budget failure(s): ${names}`;
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=analyze.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyze.js","sourceRoot":"","sources":["../../../src/report/analyze.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Budget parsing for `monobrowse report`.
|
|
3
|
+
*
|
|
4
|
+
* A budget is what turns "the agent looked at the page" into "the agent
|
|
5
|
+
* tested the page": the thresholds the run is gated on. Defaults are the
|
|
6
|
+
* strict ones you actually want in CI — zero errors, zero failed requests,
|
|
7
|
+
* Core Web Vitals at Google's "good" boundary.
|
|
8
|
+
*/
|
|
9
|
+
import type { Budget } from './types.js';
|
|
10
|
+
export declare const DEFAULT_BUDGET: Budget;
|
|
11
|
+
export declare function budgetLabel(key: keyof Budget): string;
|
|
12
|
+
/** `2500` -> `<= 2500ms`, `0` -> `<= 0`, `0.1` -> `<= 0.1`. */
|
|
13
|
+
export declare function formatThreshold(key: keyof Budget, value: number): string;
|
|
14
|
+
/**
|
|
15
|
+
* Turn a parsed JSON object into a Budget, merged over the defaults.
|
|
16
|
+
*
|
|
17
|
+
* Unknown keys throw rather than being ignored: a typo'd `max_console_error`
|
|
18
|
+
* silently loosening a CI gate is exactly the failure mode budgets exist to
|
|
19
|
+
* prevent.
|
|
20
|
+
*/
|
|
21
|
+
export declare function parseBudget(input: unknown, source?: string): Budget;
|
|
22
|
+
/**
|
|
23
|
+
* Resolve `--budget`. The value is either inline JSON (starts with `{`) or a
|
|
24
|
+
* path to a JSON file. Omitted means DEFAULT_BUDGET.
|
|
25
|
+
*/
|
|
26
|
+
export declare function loadBudget(spec?: string): Promise<Budget>;
|
|
27
|
+
//# sourceMappingURL=budget.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"budget.d.ts","sourceRoot":"","sources":["../../../src/report/budget.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEzC,eAAO,MAAM,cAAc,EAAE,MAU5B,CAAC;AAiDF,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,MAAM,GAAG,MAAM,CAErD;AAED,+DAA+D;AAC/D,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAIxE;AAMD;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,SAAW,GAAG,MAAM,CA4BrE;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA2B/D"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Budget parsing for `monobrowse report`.
|
|
3
|
+
*
|
|
4
|
+
* A budget is what turns "the agent looked at the page" into "the agent
|
|
5
|
+
* tested the page": the thresholds the run is gated on. Defaults are the
|
|
6
|
+
* strict ones you actually want in CI — zero errors, zero failed requests,
|
|
7
|
+
* Core Web Vitals at Google's "good" boundary.
|
|
8
|
+
*/
|
|
9
|
+
import { readFile } from 'node:fs/promises';
|
|
10
|
+
export const DEFAULT_BUDGET = {
|
|
11
|
+
maxConsoleErrors: 0,
|
|
12
|
+
maxPageErrors: 0,
|
|
13
|
+
maxFailedRequests: 0,
|
|
14
|
+
maxA11yErrors: 0,
|
|
15
|
+
lcpMs: 2500,
|
|
16
|
+
clsScore: 0.1,
|
|
17
|
+
inpMs: 200,
|
|
18
|
+
fcpMs: null,
|
|
19
|
+
ttfbMs: null,
|
|
20
|
+
};
|
|
21
|
+
/** Every spelling we accept, normalized (lowercased, `_`/`-` stripped). */
|
|
22
|
+
const KEY_ALIASES = {
|
|
23
|
+
maxconsoleerrors: 'maxConsoleErrors',
|
|
24
|
+
consoleerrors: 'maxConsoleErrors',
|
|
25
|
+
maxpageerrors: 'maxPageErrors',
|
|
26
|
+
pageerrors: 'maxPageErrors',
|
|
27
|
+
maxfailedrequests: 'maxFailedRequests',
|
|
28
|
+
failedrequests: 'maxFailedRequests',
|
|
29
|
+
maxa11yerrors: 'maxA11yErrors',
|
|
30
|
+
a11yerrors: 'maxA11yErrors',
|
|
31
|
+
maxaccessibilityerrors: 'maxA11yErrors',
|
|
32
|
+
lcp: 'lcpMs',
|
|
33
|
+
lcpms: 'lcpMs',
|
|
34
|
+
cls: 'clsScore',
|
|
35
|
+
clsscore: 'clsScore',
|
|
36
|
+
inp: 'inpMs',
|
|
37
|
+
inpms: 'inpMs',
|
|
38
|
+
fcp: 'fcpMs',
|
|
39
|
+
fcpms: 'fcpMs',
|
|
40
|
+
ttfb: 'ttfbMs',
|
|
41
|
+
ttfbms: 'ttfbMs',
|
|
42
|
+
};
|
|
43
|
+
const BUDGET_UNITS = {
|
|
44
|
+
maxConsoleErrors: 'count',
|
|
45
|
+
maxPageErrors: 'count',
|
|
46
|
+
maxFailedRequests: 'count',
|
|
47
|
+
maxA11yErrors: 'count',
|
|
48
|
+
lcpMs: 'ms',
|
|
49
|
+
clsScore: 'score',
|
|
50
|
+
inpMs: 'ms',
|
|
51
|
+
fcpMs: 'ms',
|
|
52
|
+
ttfbMs: 'ms',
|
|
53
|
+
};
|
|
54
|
+
const BUDGET_LABELS = {
|
|
55
|
+
maxConsoleErrors: 'Console errors',
|
|
56
|
+
maxPageErrors: 'Uncaught page errors',
|
|
57
|
+
maxFailedRequests: 'Failed requests',
|
|
58
|
+
maxA11yErrors: 'Accessibility errors',
|
|
59
|
+
lcpMs: 'LCP (Largest Contentful Paint)',
|
|
60
|
+
clsScore: 'CLS (Cumulative Layout Shift)',
|
|
61
|
+
inpMs: 'INP (Interaction to Next Paint)',
|
|
62
|
+
fcpMs: 'FCP (First Contentful Paint)',
|
|
63
|
+
ttfbMs: 'TTFB (Time to First Byte)',
|
|
64
|
+
};
|
|
65
|
+
export function budgetLabel(key) {
|
|
66
|
+
return BUDGET_LABELS[key];
|
|
67
|
+
}
|
|
68
|
+
/** `2500` -> `<= 2500ms`, `0` -> `<= 0`, `0.1` -> `<= 0.1`. */
|
|
69
|
+
export function formatThreshold(key, value) {
|
|
70
|
+
const unit = BUDGET_UNITS[key];
|
|
71
|
+
if (unit === 'ms')
|
|
72
|
+
return `<= ${value}ms`;
|
|
73
|
+
return `<= ${value}`;
|
|
74
|
+
}
|
|
75
|
+
function normalizeKey(raw) {
|
|
76
|
+
return raw.toLowerCase().replace(/[_\-\s]/g, '');
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Turn a parsed JSON object into a Budget, merged over the defaults.
|
|
80
|
+
*
|
|
81
|
+
* Unknown keys throw rather than being ignored: a typo'd `max_console_error`
|
|
82
|
+
* silently loosening a CI gate is exactly the failure mode budgets exist to
|
|
83
|
+
* prevent.
|
|
84
|
+
*/
|
|
85
|
+
export function parseBudget(input, source = 'budget') {
|
|
86
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
87
|
+
throw new Error(`Invalid ${source}: expected a JSON object of budget keys`);
|
|
88
|
+
}
|
|
89
|
+
const budget = { ...DEFAULT_BUDGET };
|
|
90
|
+
for (const [rawKey, rawValue] of Object.entries(input)) {
|
|
91
|
+
const key = KEY_ALIASES[normalizeKey(rawKey)];
|
|
92
|
+
if (!key) {
|
|
93
|
+
throw new Error(`Unknown budget key "${rawKey}" in ${source}. Known keys: ${Object.keys(DEFAULT_BUDGET).join(', ')}`);
|
|
94
|
+
}
|
|
95
|
+
if (rawValue === null) {
|
|
96
|
+
budget[key] = null;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (typeof rawValue !== 'number' || !Number.isFinite(rawValue)) {
|
|
100
|
+
throw new Error(`Budget "${rawKey}" must be a number or null, got ${JSON.stringify(rawValue)}`);
|
|
101
|
+
}
|
|
102
|
+
if (rawValue < 0) {
|
|
103
|
+
throw new Error(`Budget "${rawKey}" must not be negative, got ${rawValue}`);
|
|
104
|
+
}
|
|
105
|
+
budget[key] = rawValue;
|
|
106
|
+
}
|
|
107
|
+
return budget;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Resolve `--budget`. The value is either inline JSON (starts with `{`) or a
|
|
111
|
+
* path to a JSON file. Omitted means DEFAULT_BUDGET.
|
|
112
|
+
*/
|
|
113
|
+
export async function loadBudget(spec) {
|
|
114
|
+
if (spec === undefined || spec === '')
|
|
115
|
+
return { ...DEFAULT_BUDGET };
|
|
116
|
+
const trimmed = spec.trim();
|
|
117
|
+
if (trimmed.startsWith('{')) {
|
|
118
|
+
let parsed;
|
|
119
|
+
try {
|
|
120
|
+
parsed = JSON.parse(trimmed);
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
throw new Error(`--budget is not valid JSON: ${err.message}`);
|
|
124
|
+
}
|
|
125
|
+
return parseBudget(parsed, 'inline --budget');
|
|
126
|
+
}
|
|
127
|
+
let text;
|
|
128
|
+
try {
|
|
129
|
+
text = await readFile(trimmed, 'utf8');
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
throw new Error(`Budget file not found or unreadable: ${trimmed}`);
|
|
133
|
+
}
|
|
134
|
+
let parsed;
|
|
135
|
+
try {
|
|
136
|
+
parsed = JSON.parse(text);
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
throw new Error(`Budget file ${trimmed} is not valid JSON: ${err.message}`);
|
|
140
|
+
}
|
|
141
|
+
return parseBudget(parsed, trimmed);
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=budget.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"budget.js","sourceRoot":"","sources":["../../../src/report/budget.ts"],"names":[],"mappings":"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"}
|