@setsquare/review-sdk 0.0.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/LICENSE +202 -0
- package/NOTICE +57 -0
- package/README.md +338 -0
- package/THIRD-PARTY-NOTICES.md +61 -0
- package/dist/autoscan.d.ts +25 -0
- package/dist/autoscan.d.ts.map +1 -0
- package/dist/autoscan.js +332 -0
- package/dist/autoscan.js.map +1 -0
- package/dist/axe.d.ts +96 -0
- package/dist/axe.d.ts.map +1 -0
- package/dist/axe.js +210 -0
- package/dist/axe.js.map +1 -0
- package/dist/bin/setsquare.d.ts +3 -0
- package/dist/bin/setsquare.d.ts.map +1 -0
- package/dist/bin/setsquare.js +21 -0
- package/dist/bin/setsquare.js.map +1 -0
- package/dist/brand.d.ts +84 -0
- package/dist/brand.d.ts.map +1 -0
- package/dist/brand.js +335 -0
- package/dist/brand.js.map +1 -0
- package/dist/checks/dynamic.d.ts +57 -0
- package/dist/checks/dynamic.d.ts.map +1 -0
- package/dist/checks/dynamic.js +184 -0
- package/dist/checks/dynamic.js.map +1 -0
- package/dist/checks/rule-pack.d.ts +34 -0
- package/dist/checks/rule-pack.d.ts.map +1 -0
- package/dist/checks/rule-pack.js +270 -0
- package/dist/checks/rule-pack.js.map +1 -0
- package/dist/checks/versions.d.ts +28 -0
- package/dist/checks/versions.d.ts.map +1 -0
- package/dist/checks/versions.js +45 -0
- package/dist/checks/versions.js.map +1 -0
- package/dist/ci.d.ts +37 -0
- package/dist/ci.d.ts.map +1 -0
- package/dist/ci.js +199 -0
- package/dist/ci.js.map +1 -0
- package/dist/contract.d.ts +334 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +35 -0
- package/dist/contract.js.map +1 -0
- package/dist/dedupe.d.ts +79 -0
- package/dist/dedupe.d.ts.map +1 -0
- package/dist/dedupe.js +102 -0
- package/dist/dedupe.js.map +1 -0
- package/dist/document.d.ts +29 -0
- package/dist/document.d.ts.map +1 -0
- package/dist/document.js +94 -0
- package/dist/document.js.map +1 -0
- package/dist/envelope.d.ts +124 -0
- package/dist/envelope.d.ts.map +1 -0
- package/dist/envelope.js +337 -0
- package/dist/envelope.js.map +1 -0
- package/dist/escape.d.ts +14 -0
- package/dist/escape.d.ts.map +1 -0
- package/dist/escape.js +20 -0
- package/dist/escape.js.map +1 -0
- package/dist/findings.d.ts +47 -0
- package/dist/findings.d.ts.map +1 -0
- package/dist/findings.js +229 -0
- package/dist/findings.js.map +1 -0
- package/dist/fixture.d.ts +7 -0
- package/dist/fixture.d.ts.map +1 -0
- package/dist/fixture.js +237 -0
- package/dist/fixture.js.map +1 -0
- package/dist/group.d.ts +214 -0
- package/dist/group.d.ts.map +1 -0
- package/dist/group.js +709 -0
- package/dist/group.js.map +1 -0
- package/dist/html-report.d.ts +68 -0
- package/dist/html-report.d.ts.map +1 -0
- package/dist/html-report.js +649 -0
- package/dist/html-report.js.map +1 -0
- package/dist/human.d.ts +65 -0
- package/dist/human.d.ts.map +1 -0
- package/dist/human.js +111 -0
- package/dist/human.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/infer.d.ts +65 -0
- package/dist/infer.d.ts.map +1 -0
- package/dist/infer.js +67 -0
- package/dist/infer.js.map +1 -0
- package/dist/insights.d.ts +140 -0
- package/dist/insights.d.ts.map +1 -0
- package/dist/insights.js +483 -0
- package/dist/insights.js.map +1 -0
- package/dist/install.d.ts +8 -0
- package/dist/install.d.ts.map +1 -0
- package/dist/install.js +114 -0
- package/dist/install.js.map +1 -0
- package/dist/links.d.ts +45 -0
- package/dist/links.d.ts.map +1 -0
- package/dist/links.js +45 -0
- package/dist/links.js.map +1 -0
- package/dist/mask.d.ts +85 -0
- package/dist/mask.d.ts.map +1 -0
- package/dist/mask.js +98 -0
- package/dist/mask.js.map +1 -0
- package/dist/observe.d.ts +51 -0
- package/dist/observe.d.ts.map +1 -0
- package/dist/observe.js +233 -0
- package/dist/observe.js.map +1 -0
- package/dist/options.d.ts +249 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +185 -0
- package/dist/options.js.map +1 -0
- package/dist/output.d.ts +113 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +231 -0
- package/dist/output.js.map +1 -0
- package/dist/perf.d.ts +11 -0
- package/dist/perf.d.ts.map +1 -0
- package/dist/perf.js +150 -0
- package/dist/perf.js.map +1 -0
- package/dist/reading-level.d.ts +73 -0
- package/dist/reading-level.d.ts.map +1 -0
- package/dist/reading-level.js +189 -0
- package/dist/reading-level.js.map +1 -0
- package/dist/reporter.d.ts +94 -0
- package/dist/reporter.d.ts.map +1 -0
- package/dist/reporter.js +709 -0
- package/dist/reporter.js.map +1 -0
- package/dist/rules.d.ts +55 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +77 -0
- package/dist/rules.js.map +1 -0
- package/dist/runtime.d.ts +32 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +67 -0
- package/dist/runtime.js.map +1 -0
- package/dist/sanitise.d.ts +116 -0
- package/dist/sanitise.d.ts.map +1 -0
- package/dist/sanitise.js +247 -0
- package/dist/sanitise.js.map +1 -0
- package/dist/scan.d.ts +60 -0
- package/dist/scan.d.ts.map +1 -0
- package/dist/scan.js +246 -0
- package/dist/scan.js.map +1 -0
- package/dist/screens.d.ts +98 -0
- package/dist/screens.d.ts.map +1 -0
- package/dist/screens.js +301 -0
- package/dist/screens.js.map +1 -0
- package/dist/settle.d.ts +65 -0
- package/dist/settle.d.ts.map +1 -0
- package/dist/settle.js +162 -0
- package/dist/settle.js.map +1 -0
- package/dist/shots.d.ts +109 -0
- package/dist/shots.d.ts.map +1 -0
- package/dist/shots.js +113 -0
- package/dist/shots.js.map +1 -0
- package/dist/style.d.ts +47 -0
- package/dist/style.d.ts.map +1 -0
- package/dist/style.js +154 -0
- package/dist/style.js.map +1 -0
- package/dist/suite.d.ts +218 -0
- package/dist/suite.d.ts.map +1 -0
- package/dist/suite.js +219 -0
- package/dist/suite.js.map +1 -0
- package/dist/summary.d.ts +268 -0
- package/dist/summary.d.ts.map +1 -0
- package/dist/summary.js +1308 -0
- package/dist/summary.js.map +1 -0
- package/dist/unscannable.d.ts +75 -0
- package/dist/unscannable.d.ts.map +1 -0
- package/dist/unscannable.js +111 -0
- package/dist/unscannable.js.map +1 -0
- package/dist/versions.d.ts +13 -0
- package/dist/versions.d.ts.map +1 -0
- package/dist/versions.js +39 -0
- package/dist/versions.js.map +1 -0
- package/dist/wcag-map.d.ts +19 -0
- package/dist/wcag-map.d.ts.map +1 -0
- package/dist/wcag-map.js +52 -0
- package/dist/wcag-map.js.map +1 -0
- package/dist/with-setsquare.d.ts +26 -0
- package/dist/with-setsquare.d.ts.map +1 -0
- package/dist/with-setsquare.js +70 -0
- package/dist/with-setsquare.js.map +1 -0
- package/package.json +81 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { TestOutcome } from './envelope.js';
|
|
2
|
+
import { type ShotGroup } from './shots.js';
|
|
3
|
+
/**
|
|
4
|
+
* The visual contact sheet: every capture a run produced, under the name of the
|
|
5
|
+
* test that produced it, on one scrollable page.
|
|
6
|
+
*
|
|
7
|
+
* The problem it solves is narrow and real. Playwright takes the screenshots
|
|
8
|
+
* already, but reading them means opening its report and clicking into each test
|
|
9
|
+
* in turn — so nobody scans them, and a change that visibly broke a page passes
|
|
10
|
+
* review because all the assertions still held. A QA or a developer wants one
|
|
11
|
+
* page they can scroll in thirty seconds. That is all this is.
|
|
12
|
+
*
|
|
13
|
+
* What it is NOT, said plainly and said in the page itself: a verdict. Setsquare
|
|
14
|
+
* compares nothing — no baselines, no history, no "changed since last week",
|
|
15
|
+
* which is the platform's job and deliberately not the local tool's. Where a
|
|
16
|
+
* comparison appears here, Playwright's `toHaveScreenshot()` made it and we
|
|
17
|
+
* found the expected/actual/diff trio it attached. Everything else is a picture
|
|
18
|
+
* with a caption, offered to a person who can judge it.
|
|
19
|
+
*
|
|
20
|
+
* Rendered from data with no clock and no filesystem reads, so two renders of
|
|
21
|
+
* one run are byte-identical; the same fragment is embedded in the accessibility
|
|
22
|
+
* report and served standalone, so the two can never drift apart.
|
|
23
|
+
*/
|
|
24
|
+
export interface ScreenRow {
|
|
25
|
+
testId: string;
|
|
26
|
+
title: string;
|
|
27
|
+
project?: string | undefined;
|
|
28
|
+
/** The run-group lane this test came from, when the run was merged. */
|
|
29
|
+
lane?: string | undefined;
|
|
30
|
+
status: TestOutcome['status'];
|
|
31
|
+
retry: number;
|
|
32
|
+
groups: ShotGroup[];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Which tests appear, and in what order.
|
|
36
|
+
*
|
|
37
|
+
* Ordered by how likely the reader is to care, not alphabetically: comparisons
|
|
38
|
+
* Playwright flagged come first, then tests that failed or timed out, then
|
|
39
|
+
* everything else in the order it ran. A contact sheet is scanned from the top
|
|
40
|
+
* and abandoned somewhere in the middle, so what goes at the top decides
|
|
41
|
+
* whether it did any good.
|
|
42
|
+
*
|
|
43
|
+
* Tests with no captures are omitted entirely rather than listed empty — the
|
|
44
|
+
* page's own header says how many tests had nothing, which is the honest place
|
|
45
|
+
* for that number.
|
|
46
|
+
*/
|
|
47
|
+
export declare function screenRows(outcomes: readonly TestOutcome[]): ScreenRow[];
|
|
48
|
+
/**
|
|
49
|
+
* A path to a file on disk, as an `src` a browser opening the report from disk
|
|
50
|
+
* can follow.
|
|
51
|
+
*
|
|
52
|
+
* Relative, always: the reports are meant to be zipped, attached to a ticket and
|
|
53
|
+
* opened somewhere else, and an absolute `/Users/someone/...` would break the
|
|
54
|
+
* moment it left the machine that wrote it. Encoded per segment because a spec
|
|
55
|
+
* title becomes a directory name, and test titles contain spaces, brackets and
|
|
56
|
+
* occasionally a `#` — which would otherwise truncate the URL at the fragment.
|
|
57
|
+
*/
|
|
58
|
+
export declare function shotSrc(runDir: string, path: string): string;
|
|
59
|
+
export interface ScreensInput {
|
|
60
|
+
rows: ScreenRow[];
|
|
61
|
+
/** Directory the HTML lands in. Every `src` is computed relative to it. */
|
|
62
|
+
runDir: string;
|
|
63
|
+
/** How many of the run's tests produced no capture at all. */
|
|
64
|
+
testsWithoutShots: number;
|
|
65
|
+
generatedAt: string;
|
|
66
|
+
branch: string;
|
|
67
|
+
commit: string;
|
|
68
|
+
/** Anything the sheet could not show, named rather than omitted. */
|
|
69
|
+
notes?: string[];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The styles the section needs, exported because the section has two homes.
|
|
73
|
+
*
|
|
74
|
+
* The contact sheet is both a standalone page and a section of the
|
|
75
|
+
* accessibility report, and a copied rule block is a rule block that drifts —
|
|
76
|
+
* so both documents include this string instead of describing the same layout
|
|
77
|
+
* twice.
|
|
78
|
+
*/
|
|
79
|
+
export declare const SCREENS_CSS = "\n .shot-test { border: 1px solid var(--line); border-radius: 8px; padding: .75rem 1rem; margin: 1rem 0; }\n .shot-test > h3 { font-size: 1rem; margin: 0 0 .1rem; overflow-wrap: anywhere; }\n .shot-meta { color: var(--muted); font-size: .875rem; margin: 0 0 .75rem; }\n .shot-strip { display: flex; flex-wrap: wrap; gap: 1rem; margin: 0; padding: 0; }\n .shot-strip.compare { border: 1px dashed var(--line); border-radius: 8px; padding: .75rem; }\n .shot { margin: 0; flex: 1 1 22rem; min-width: 0; }\n .shot img {\n display: block; width: 100%; height: auto; border: 1px solid var(--line);\n border-radius: 4px; background: var(--code-bg);\n /*\n * Capped so one full-page capture of a long page cannot push every other\n * test off the screen. It scales rather than crops, because a visual\n * review tool that hides part of the picture is worse than none. The image\n * links to itself for a full-size look.\n */\n max-height: 28rem; object-fit: contain; object-position: top;\n }\n .shot figcaption { font-size: .8125rem; color: var(--muted); margin-top: .35rem; overflow-wrap: anywhere; }\n .shot figcaption .role { color: var(--fg); font-weight: 600; }\n .compare-label { font-size: .8125rem; font-weight: 600; margin: 0 0 .5rem; }\n";
|
|
80
|
+
/**
|
|
81
|
+
* The section, embeddable in any document that includes `SCREENS_CSS`.
|
|
82
|
+
*
|
|
83
|
+
* Takes its heading level as given: `h2` for the section, `h3` per test, which
|
|
84
|
+
* is correct both standalone and inside the accessibility report. A section that
|
|
85
|
+
* chose its own levels would break the heading order of one of its two homes.
|
|
86
|
+
*/
|
|
87
|
+
export declare function renderScreensSection(input: ScreensInput): string;
|
|
88
|
+
/**
|
|
89
|
+
* The standalone page.
|
|
90
|
+
*
|
|
91
|
+
* Its own file rather than only a section of the accessibility report, because
|
|
92
|
+
* the audiences are different: this one is opened by whoever is about to say
|
|
93
|
+
* "looks fine to me", and it should not arrive wrapped in a WCAG conformance
|
|
94
|
+
* document. Same palette, same section, no JavaScript, no external requests —
|
|
95
|
+
* the images are the only thing it loads, and they are local files.
|
|
96
|
+
*/
|
|
97
|
+
export declare function renderScreensPage(input: ScreensInput): string;
|
|
98
|
+
//# sourceMappingURL=screens.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"screens.d.ts","sourceRoot":"","sources":["../src/screens.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAGjD,OAAO,EAAyB,KAAK,SAAS,EAAiB,MAAM,YAAY,CAAC;AAElF;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,uEAAuE;IACvE,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,MAAM,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAC;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,SAAS,EAAE,CAAC;CACrB;AAwBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,SAAS,WAAW,EAAE,GAAG,SAAS,EAAE,CA8BxE;AAED;;;;;;;;;GASG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAK5D;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,SAAS,EAAE,CAAC;IAClB,2EAA2E;IAC3E,MAAM,EAAE,MAAM,CAAC;IACf,8DAA8D;IAC9D,iBAAiB,EAAE,MAAM,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,oEAAoE;IACpE,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,uwCAqBvB,CAAC;AA2GF;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAuChE;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAwD7D"}
|
package/dist/screens.js
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { relative, sep } from 'node:path';
|
|
2
|
+
import { BRAND_CSS, MASTHEAD_CSS, masthead } from './brand.js';
|
|
3
|
+
import { esc } from './escape.js';
|
|
4
|
+
import { humanInstant, shortCommit } from './human.js';
|
|
5
|
+
import { groupShots } from './shots.js';
|
|
6
|
+
const ROLE_LABEL = {
|
|
7
|
+
expected: 'Expected (committed baseline)',
|
|
8
|
+
previous: 'Previous attempt',
|
|
9
|
+
actual: 'Actual (this run)',
|
|
10
|
+
diff: 'Difference',
|
|
11
|
+
shot: 'Capture',
|
|
12
|
+
};
|
|
13
|
+
const STATUS_WORD = {
|
|
14
|
+
passed: 'passed',
|
|
15
|
+
failed: 'failed',
|
|
16
|
+
timedOut: 'timed out',
|
|
17
|
+
skipped: 'skipped',
|
|
18
|
+
};
|
|
19
|
+
const STATUS_CLASS = {
|
|
20
|
+
passed: 'ok',
|
|
21
|
+
failed: 'bad',
|
|
22
|
+
timedOut: 'bad',
|
|
23
|
+
skipped: 'muted',
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Which tests appear, and in what order.
|
|
27
|
+
*
|
|
28
|
+
* Ordered by how likely the reader is to care, not alphabetically: comparisons
|
|
29
|
+
* Playwright flagged come first, then tests that failed or timed out, then
|
|
30
|
+
* everything else in the order it ran. A contact sheet is scanned from the top
|
|
31
|
+
* and abandoned somewhere in the middle, so what goes at the top decides
|
|
32
|
+
* whether it did any good.
|
|
33
|
+
*
|
|
34
|
+
* Tests with no captures are omitted entirely rather than listed empty — the
|
|
35
|
+
* page's own header says how many tests had nothing, which is the honest place
|
|
36
|
+
* for that number.
|
|
37
|
+
*/
|
|
38
|
+
export function screenRows(outcomes) {
|
|
39
|
+
const rows = [];
|
|
40
|
+
for (const outcome of outcomes) {
|
|
41
|
+
const shots = outcome.shots ?? [];
|
|
42
|
+
if (shots.length === 0)
|
|
43
|
+
continue;
|
|
44
|
+
rows.push({
|
|
45
|
+
testId: outcome.testId,
|
|
46
|
+
title: outcome.title,
|
|
47
|
+
...(outcome.project ? { project: outcome.project } : {}),
|
|
48
|
+
...(outcome.lane ? { lane: outcome.lane } : {}),
|
|
49
|
+
status: outcome.status,
|
|
50
|
+
retry: outcome.retry,
|
|
51
|
+
groups: groupShots(shots),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
const rank = (row) => {
|
|
55
|
+
if (row.groups.some((group) => group.comparison))
|
|
56
|
+
return 0;
|
|
57
|
+
if (row.status === 'failed' || row.status === 'timedOut')
|
|
58
|
+
return 1;
|
|
59
|
+
return 2;
|
|
60
|
+
};
|
|
61
|
+
// A stable sort by rank only: within a bucket the run's own order survives,
|
|
62
|
+
// which for a walkthrough is the order the flow happened in.
|
|
63
|
+
return rows
|
|
64
|
+
.map((row, index) => ({ row, index }))
|
|
65
|
+
.sort((a, b) => rank(a.row) - rank(b.row) || a.index - b.index)
|
|
66
|
+
.map((entry) => entry.row);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* A path to a file on disk, as an `src` a browser opening the report from disk
|
|
70
|
+
* can follow.
|
|
71
|
+
*
|
|
72
|
+
* Relative, always: the reports are meant to be zipped, attached to a ticket and
|
|
73
|
+
* opened somewhere else, and an absolute `/Users/someone/...` would break the
|
|
74
|
+
* moment it left the machine that wrote it. Encoded per segment because a spec
|
|
75
|
+
* title becomes a directory name, and test titles contain spaces, brackets and
|
|
76
|
+
* occasionally a `#` — which would otherwise truncate the URL at the fragment.
|
|
77
|
+
*/
|
|
78
|
+
export function shotSrc(runDir, path) {
|
|
79
|
+
return relative(runDir, path)
|
|
80
|
+
.split(sep)
|
|
81
|
+
.map((segment) => (segment === '..' ? segment : encodeURIComponent(segment)))
|
|
82
|
+
.join('/');
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The styles the section needs, exported because the section has two homes.
|
|
86
|
+
*
|
|
87
|
+
* The contact sheet is both a standalone page and a section of the
|
|
88
|
+
* accessibility report, and a copied rule block is a rule block that drifts —
|
|
89
|
+
* so both documents include this string instead of describing the same layout
|
|
90
|
+
* twice.
|
|
91
|
+
*/
|
|
92
|
+
export const SCREENS_CSS = `
|
|
93
|
+
.shot-test { border: 1px solid var(--line); border-radius: 8px; padding: .75rem 1rem; margin: 1rem 0; }
|
|
94
|
+
.shot-test > h3 { font-size: 1rem; margin: 0 0 .1rem; overflow-wrap: anywhere; }
|
|
95
|
+
.shot-meta { color: var(--muted); font-size: .875rem; margin: 0 0 .75rem; }
|
|
96
|
+
.shot-strip { display: flex; flex-wrap: wrap; gap: 1rem; margin: 0; padding: 0; }
|
|
97
|
+
.shot-strip.compare { border: 1px dashed var(--line); border-radius: 8px; padding: .75rem; }
|
|
98
|
+
.shot { margin: 0; flex: 1 1 22rem; min-width: 0; }
|
|
99
|
+
.shot img {
|
|
100
|
+
display: block; width: 100%; height: auto; border: 1px solid var(--line);
|
|
101
|
+
border-radius: 4px; background: var(--code-bg);
|
|
102
|
+
/*
|
|
103
|
+
* Capped so one full-page capture of a long page cannot push every other
|
|
104
|
+
* test off the screen. It scales rather than crops, because a visual
|
|
105
|
+
* review tool that hides part of the picture is worse than none. The image
|
|
106
|
+
* links to itself for a full-size look.
|
|
107
|
+
*/
|
|
108
|
+
max-height: 28rem; object-fit: contain; object-position: top;
|
|
109
|
+
}
|
|
110
|
+
.shot figcaption { font-size: .8125rem; color: var(--muted); margin-top: .35rem; overflow-wrap: anywhere; }
|
|
111
|
+
.shot figcaption .role { color: var(--fg); font-weight: 600; }
|
|
112
|
+
.compare-label { font-size: .8125rem; font-weight: 600; margin: 0 0 .5rem; }
|
|
113
|
+
`;
|
|
114
|
+
function shotFigure(runDir, shot, testTitle, labelled) {
|
|
115
|
+
const src = shotSrc(runDir, shot.path);
|
|
116
|
+
/*
|
|
117
|
+
* Alt text names the capture; it does not describe it, because nothing here
|
|
118
|
+
* knows what is in the pixels and inventing a description would be worse than
|
|
119
|
+
* having none. The caption carries the file name and the link, so the two are
|
|
120
|
+
* not the same sentence read twice.
|
|
121
|
+
*/
|
|
122
|
+
const alt = `Screenshot ${shot.name} from the test “${testTitle}”. This report cannot describe what the image shows.`;
|
|
123
|
+
const role = labelled ? `<span class="role">${esc(ROLE_LABEL[shot.role])}</span> — ` : '';
|
|
124
|
+
return `<figure class="shot">
|
|
125
|
+
<a href="${esc(src)}"><img src="${esc(src)}" alt="${esc(alt)}" loading="lazy"></a>
|
|
126
|
+
<figcaption>${role}${esc(shot.name)}</figcaption>
|
|
127
|
+
</figure>`;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* A comparison, boxed and captioned so the three images read as one statement.
|
|
131
|
+
*
|
|
132
|
+
* Its own block, always: a diff sitting loose in a strip of unrelated captures
|
|
133
|
+
* is a diff nobody realises is a diff.
|
|
134
|
+
*/
|
|
135
|
+
function comparisonBlock(runDir, group, testTitle) {
|
|
136
|
+
const figures = group.shots
|
|
137
|
+
.map((shot) => shotFigure(runDir, shot, testTitle, true))
|
|
138
|
+
.join('\n ');
|
|
139
|
+
return `<div>
|
|
140
|
+
<p class="compare-label">Playwright compared <code>${esc(group.base)}</code> against its committed baseline.</p>
|
|
141
|
+
<div class="shot-strip compare">
|
|
142
|
+
${figures}
|
|
143
|
+
</div>
|
|
144
|
+
</div>`;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Consecutive standalone captures share one strip.
|
|
148
|
+
*
|
|
149
|
+
* They are the steps of a flow, and the whole value of the sheet is seeing them
|
|
150
|
+
* beside each other — a strip per capture would stack them vertically and turn a
|
|
151
|
+
* walkthrough back into the scrolling exercise this page exists to replace.
|
|
152
|
+
* A comparison interrupts the strip, because it is a different kind of claim.
|
|
153
|
+
*/
|
|
154
|
+
function stripBlock(runDir, groups, testTitle) {
|
|
155
|
+
const figures = groups
|
|
156
|
+
.flatMap((group) => group.shots.map((shot) => shotFigure(runDir, shot, testTitle, false)))
|
|
157
|
+
.join('\n ');
|
|
158
|
+
return `<div class="shot-strip">\n ${figures}\n </div>`;
|
|
159
|
+
}
|
|
160
|
+
function rowBlock(runDir, row) {
|
|
161
|
+
const meta = [
|
|
162
|
+
`<span class="${STATUS_CLASS[row.status]}">${STATUS_WORD[row.status]}</span>`,
|
|
163
|
+
...(row.retry > 0 ? [`retry ${row.retry}`] : []),
|
|
164
|
+
...(row.project ? [esc(row.project)] : []),
|
|
165
|
+
...(row.lane ? [`lane ${esc(row.lane)}`] : []),
|
|
166
|
+
].join(' · ');
|
|
167
|
+
/*
|
|
168
|
+
* Runs of standalone captures collapse into one strip; each comparison stands
|
|
169
|
+
* alone. Built as a fold rather than a map because the unit of layout is a run
|
|
170
|
+
* of groups, not a group.
|
|
171
|
+
*/
|
|
172
|
+
const blocks = [];
|
|
173
|
+
let pending = [];
|
|
174
|
+
const flush = () => {
|
|
175
|
+
if (pending.length > 0)
|
|
176
|
+
blocks.push(stripBlock(runDir, pending, row.title));
|
|
177
|
+
pending = [];
|
|
178
|
+
};
|
|
179
|
+
for (const group of row.groups) {
|
|
180
|
+
if (group.comparison) {
|
|
181
|
+
flush();
|
|
182
|
+
blocks.push(comparisonBlock(runDir, group, row.title));
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
pending.push(group);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
flush();
|
|
189
|
+
return ` <article class="shot-test">
|
|
190
|
+
<h3>${esc(row.title)}</h3>
|
|
191
|
+
<p class="shot-meta">${meta}<br>${esc(row.testId)}</p>
|
|
192
|
+
${blocks.join('\n ')}
|
|
193
|
+
</article>`;
|
|
194
|
+
}
|
|
195
|
+
/** How to get captures, for a run that produced none. Shown, never assumed known. */
|
|
196
|
+
const HOW_TO_CAPTURE = `<p>Nothing to show: this run produced no screenshots. Setsquare never takes them —
|
|
197
|
+
it indexes the ones Playwright already wrote. Three ways to give it something to index:</p>
|
|
198
|
+
<ul>
|
|
199
|
+
<li><code>use: { screenshot: 'on' }</code> in your Playwright config. One capture per test, and no
|
|
200
|
+
other change to your suite.</li>
|
|
201
|
+
<li><code>await expect(page).toHaveScreenshot()</code>. Playwright compares against a committed
|
|
202
|
+
baseline, and this page puts expected, actual and difference side by side.</li>
|
|
203
|
+
<li>Attach your own at the moments that matter, with
|
|
204
|
+
<code>testInfo.attach(name, { path })</code> — which turns this page into a step-by-step
|
|
205
|
+
walkthrough of the flow. The README has the four-line helper.</li>
|
|
206
|
+
</ul>
|
|
207
|
+
<p class="muted">An attachment given a <code>body</code> rather than a <code>path</code> stays in
|
|
208
|
+
Playwright's memory and never reaches disk, so this sheet cannot link to it.</p>`;
|
|
209
|
+
/**
|
|
210
|
+
* The section, embeddable in any document that includes `SCREENS_CSS`.
|
|
211
|
+
*
|
|
212
|
+
* Takes its heading level as given: `h2` for the section, `h3` per test, which
|
|
213
|
+
* is correct both standalone and inside the accessibility report. A section that
|
|
214
|
+
* chose its own levels would break the heading order of one of its two homes.
|
|
215
|
+
*/
|
|
216
|
+
export function renderScreensSection(input) {
|
|
217
|
+
const shots = input.rows.reduce((total, row) => total + row.groups.reduce((n, group) => n + group.shots.length, 0), 0);
|
|
218
|
+
const comparisons = input.rows.reduce((total, row) => total + row.groups.filter((group) => group.comparison).length, 0);
|
|
219
|
+
const notes = (input.notes ?? []).filter((note) => note.length > 0);
|
|
220
|
+
const header = input.rows.length === 0
|
|
221
|
+
? HOW_TO_CAPTURE
|
|
222
|
+
: `<p>${shots} capture${shots === 1 ? '' : 's'} from ${input.rows.length} test${input.rows.length === 1 ? '' : 's'}${input.testsWithoutShots > 0
|
|
223
|
+
? `; ${input.testsWithoutShots} test${input.testsWithoutShots === 1 ? '' : 's'} in this run produced none`
|
|
224
|
+
: ''}. ${comparisons > 0
|
|
225
|
+
? `${comparisons} of them ${comparisons === 1 ? 'is a comparison' : 'are comparisons'} Playwright made against a committed baseline.`
|
|
226
|
+
: 'None of them is a comparison — Playwright had no committed baseline to check against.'}</p>
|
|
227
|
+
<p class="muted">Setsquare compares nothing and stores no baseline. This page shows you what the
|
|
228
|
+
run saw so a person can spot what automation cannot: nothing here is a pass or a failure.</p>`;
|
|
229
|
+
const noteBlock = notes.length > 0
|
|
230
|
+
? `\n <ul class="muted">\n ${notes.map((note) => `<li>${esc(note)}</li>`).join('\n ')}\n </ul>`
|
|
231
|
+
: '';
|
|
232
|
+
return ` <section aria-labelledby="h-screens">
|
|
233
|
+
<h2 id="h-screens">Screens <span class="muted">(what the run saw)</span></h2>
|
|
234
|
+
${header}${noteBlock}
|
|
235
|
+
${input.rows.map((row) => rowBlock(input.runDir, row)).join('\n')}
|
|
236
|
+
</section>`;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* The standalone page.
|
|
240
|
+
*
|
|
241
|
+
* Its own file rather than only a section of the accessibility report, because
|
|
242
|
+
* the audiences are different: this one is opened by whoever is about to say
|
|
243
|
+
* "looks fine to me", and it should not arrive wrapped in a WCAG conformance
|
|
244
|
+
* document. Same palette, same section, no JavaScript, no external requests —
|
|
245
|
+
* the images are the only thing it loads, and they are local files.
|
|
246
|
+
*/
|
|
247
|
+
export function renderScreensPage(input) {
|
|
248
|
+
return `<!doctype html>
|
|
249
|
+
<html lang="en">
|
|
250
|
+
<head>
|
|
251
|
+
<meta charset="utf-8">
|
|
252
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
253
|
+
<meta name="color-scheme" content="light dark">
|
|
254
|
+
<title>Setsquare — screens, ${esc(humanInstant(input.generatedAt))}</title>
|
|
255
|
+
<style>
|
|
256
|
+
${BRAND_CSS}
|
|
257
|
+
/*
|
|
258
|
+
* The page's own names, mapped onto brand roles. Mapping rather than renaming
|
|
259
|
+
* every rule below keeps the diff to this block, and keeps one place to look
|
|
260
|
+
* when a role changes.
|
|
261
|
+
*/
|
|
262
|
+
:root {
|
|
263
|
+
--bg: var(--ss-surface); --fg: var(--ss-ink); --muted: var(--ss-ink-muted);
|
|
264
|
+
--line: var(--ss-border); --code-bg: var(--ss-surface-raised);
|
|
265
|
+
--bad-fg: var(--ss-danger); --good-fg: var(--ss-success);
|
|
266
|
+
}
|
|
267
|
+
${MASTHEAD_CSS}
|
|
268
|
+
* { box-sizing: border-box; }
|
|
269
|
+
body {
|
|
270
|
+
margin: 0 auto; padding: 2rem 1.5rem 4rem; max-width: 78rem;
|
|
271
|
+
background: var(--bg); color: var(--fg);
|
|
272
|
+
font: 16px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
273
|
+
}
|
|
274
|
+
h1 { font-size: 1.6rem; margin: 0 0 .25rem; }
|
|
275
|
+
h2 { font-size: 1.2rem; margin: 2rem 0 .75rem; }
|
|
276
|
+
a { color: inherit; }
|
|
277
|
+
:focus-visible { outline: 3px solid var(--fg); outline-offset: 2px; }
|
|
278
|
+
code { background: var(--code-bg); padding: .1em .35em; border-radius: 4px; font-size: .875em; overflow-wrap: anywhere; }
|
|
279
|
+
.muted { color: var(--muted); }
|
|
280
|
+
.ok { color: var(--good-fg); }
|
|
281
|
+
.bad { color: var(--bad-fg); font-weight: 600; }
|
|
282
|
+
footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--line); color: var(--muted); font-size: .875rem; }
|
|
283
|
+
${SCREENS_CSS}
|
|
284
|
+
</style>
|
|
285
|
+
</head>
|
|
286
|
+
<body>
|
|
287
|
+
${masthead('Setsquare — screens', `<time datetime="${esc(input.generatedAt)}">${esc(humanInstant(input.generatedAt))}</time>` +
|
|
288
|
+
` · ${esc(input.branch)} @ ${esc(shortCommit(input.commit))}`)}
|
|
289
|
+
<main>
|
|
290
|
+
${renderScreensSection(input)}
|
|
291
|
+
</main>
|
|
292
|
+
<footer>
|
|
293
|
+
<p>Every image on this page was written by Playwright, and is linked where it lies. Setsquare took
|
|
294
|
+
no screenshot, wrote no image, and copied nothing — which is why no unmasked capture can reach your
|
|
295
|
+
disk through it, and why it cannot tell you whether any of this changed.</p>
|
|
296
|
+
</footer>
|
|
297
|
+
</body>
|
|
298
|
+
</html>
|
|
299
|
+
`;
|
|
300
|
+
}
|
|
301
|
+
//# sourceMappingURL=screens.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"screens.js","sourceRoot":"","sources":["../src/screens.ts"],"names":[],"mappings":"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"}
|
package/dist/settle.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Settled navigation" — the definition auto-scan fires on.
|
|
3
|
+
*
|
|
4
|
+
* This is the riskiest heuristic in the SDK, and the failure it guards against is
|
|
5
|
+
* not a crash. It is a **confident false pass**: scan a page before its content
|
|
6
|
+
* arrives, find nothing, and report conformance nobody verified. An evidence
|
|
7
|
+
* product can survive missing a finding; it cannot survive inventing a clean one.
|
|
8
|
+
*
|
|
9
|
+
* The reference implementation's own Playwright config warns about exactly this
|
|
10
|
+
* ("an uncompiled route renders a shell that passes every rule") and pays for a
|
|
11
|
+
* whole warmup project to avoid it. Our fixture site reproduces it deliberately:
|
|
12
|
+
* `/slow` renders a perfectly conformant shell and injects two violations a
|
|
13
|
+
* second later.
|
|
14
|
+
*
|
|
15
|
+
* A page is settled when ALL of these hold:
|
|
16
|
+
*
|
|
17
|
+
* 1. `document.readyState === 'complete'`
|
|
18
|
+
* 2. the URL has not changed for `quietMs`
|
|
19
|
+
* 3. no DOM mutations for `quietMs`
|
|
20
|
+
*
|
|
21
|
+
* with a hard ceiling of `settleTimeoutMs`. **On the ceiling we scan anyway and
|
|
22
|
+
* say so** (`settleTimedOut`), rather than skipping: a page that never goes quiet
|
|
23
|
+
* — a spinner, a poller, a carousel — is extremely common, and silently declining
|
|
24
|
+
* to scan it would produce exactly the "no findings" that looks like "no
|
|
25
|
+
* problems".
|
|
26
|
+
*
|
|
27
|
+
* Every scan carries its settle diagnostics, so a suspicious result is
|
|
28
|
+
* identifiable in the evidence rather than silently trusted.
|
|
29
|
+
*/
|
|
30
|
+
/** Name of the page-side callback the SDK exposes. Namespaced; documented pollution. */
|
|
31
|
+
export declare const MUTATION_TOTAL = "__setsquareMutationTotal";
|
|
32
|
+
export declare const SETTLE_BINDING = "__setsquareSettle";
|
|
33
|
+
export interface SettleSignal {
|
|
34
|
+
url: string;
|
|
35
|
+
/** Why the scan is firing now — a quiet page, or the ceiling running out. */
|
|
36
|
+
reason: 'quiet' | 'ceiling' | 'teardown';
|
|
37
|
+
/** How the URL was reached. A pushState route change is not a navigation. */
|
|
38
|
+
via?: 'load' | 'route-change';
|
|
39
|
+
/** Milliseconds from the navigation to this signal. */
|
|
40
|
+
sinceNavMs: number;
|
|
41
|
+
/** DOM mutations counted since the navigation. Cheap signal of how busy the page was. */
|
|
42
|
+
mutations: number;
|
|
43
|
+
/** True when the ceiling fired rather than the page going quiet. */
|
|
44
|
+
settleTimedOut: boolean;
|
|
45
|
+
}
|
|
46
|
+
export interface SettleConfig {
|
|
47
|
+
quietMs: number;
|
|
48
|
+
settleTimeoutMs: number;
|
|
49
|
+
}
|
|
50
|
+
export declare const DEFAULT_SETTLE: SettleConfig;
|
|
51
|
+
/**
|
|
52
|
+
* The in-page detector.
|
|
53
|
+
*
|
|
54
|
+
* Written as a string rather than a function because `addInitScript` serialises
|
|
55
|
+
* it into a context that has none of this module's scope, and because the config
|
|
56
|
+
* values are interpolated at install time — there is no channel to pass arguments
|
|
57
|
+
* to an init script that runs before every navigation.
|
|
58
|
+
*
|
|
59
|
+
* Deliberately defensive throughout: this runs inside a customer's page, before
|
|
60
|
+
* their own scripts, on every navigation. It must never throw, never retain
|
|
61
|
+
* anything large, and never be observable in a way that changes their app's
|
|
62
|
+
* behaviour.
|
|
63
|
+
*/
|
|
64
|
+
export declare function settleScript(config: SettleConfig): string;
|
|
65
|
+
//# sourceMappingURL=settle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settle.d.ts","sourceRoot":"","sources":["../src/settle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,wFAAwF;AACxF,eAAO,MAAM,cAAc,6BAA6B,CAAC;AAEzD,eAAO,MAAM,cAAc,sBAAsB,CAAC;AAElD,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,6EAA6E;IAC7E,MAAM,EAAE,OAAO,GAAG,SAAS,GAAG,UAAU,CAAC;IACzC,6EAA6E;IAC7E,GAAG,CAAC,EAAE,MAAM,GAAG,cAAc,CAAC;IAC9B,uDAAuD;IACvD,UAAU,EAAE,MAAM,CAAC;IACnB,yFAAyF;IACzF,SAAS,EAAE,MAAM,CAAC;IAClB,oEAAoE;IACpE,cAAc,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,eAAO,MAAM,cAAc,EAAE,YAoB5B,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CA8FzD"}
|
package/dist/settle.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Settled navigation" — the definition auto-scan fires on.
|
|
3
|
+
*
|
|
4
|
+
* This is the riskiest heuristic in the SDK, and the failure it guards against is
|
|
5
|
+
* not a crash. It is a **confident false pass**: scan a page before its content
|
|
6
|
+
* arrives, find nothing, and report conformance nobody verified. An evidence
|
|
7
|
+
* product can survive missing a finding; it cannot survive inventing a clean one.
|
|
8
|
+
*
|
|
9
|
+
* The reference implementation's own Playwright config warns about exactly this
|
|
10
|
+
* ("an uncompiled route renders a shell that passes every rule") and pays for a
|
|
11
|
+
* whole warmup project to avoid it. Our fixture site reproduces it deliberately:
|
|
12
|
+
* `/slow` renders a perfectly conformant shell and injects two violations a
|
|
13
|
+
* second later.
|
|
14
|
+
*
|
|
15
|
+
* A page is settled when ALL of these hold:
|
|
16
|
+
*
|
|
17
|
+
* 1. `document.readyState === 'complete'`
|
|
18
|
+
* 2. the URL has not changed for `quietMs`
|
|
19
|
+
* 3. no DOM mutations for `quietMs`
|
|
20
|
+
*
|
|
21
|
+
* with a hard ceiling of `settleTimeoutMs`. **On the ceiling we scan anyway and
|
|
22
|
+
* say so** (`settleTimedOut`), rather than skipping: a page that never goes quiet
|
|
23
|
+
* — a spinner, a poller, a carousel — is extremely common, and silently declining
|
|
24
|
+
* to scan it would produce exactly the "no findings" that looks like "no
|
|
25
|
+
* problems".
|
|
26
|
+
*
|
|
27
|
+
* Every scan carries its settle diagnostics, so a suspicious result is
|
|
28
|
+
* identifiable in the evidence rather than silently trusted.
|
|
29
|
+
*/
|
|
30
|
+
/** Name of the page-side callback the SDK exposes. Namespaced; documented pollution. */
|
|
31
|
+
export const MUTATION_TOTAL = '__setsquareMutationTotal';
|
|
32
|
+
export const SETTLE_BINDING = '__setsquareSettle';
|
|
33
|
+
export const DEFAULT_SETTLE = {
|
|
34
|
+
/*
|
|
35
|
+
* 500ms rather than something snappier.
|
|
36
|
+
*
|
|
37
|
+
* A shorter window lands inside the gap between a shell rendering and its
|
|
38
|
+
* content arriving, which is precisely the trap. Measured against the fixture
|
|
39
|
+
* site's `/slow` route, which injects its violations after 1000ms: at 250ms the
|
|
40
|
+
* scanner reports a clean page, at 500ms plus the ceiling it does not.
|
|
41
|
+
*/
|
|
42
|
+
quietMs: 500,
|
|
43
|
+
/*
|
|
44
|
+
* 5s, and it is a scan trigger rather than a give-up.
|
|
45
|
+
*
|
|
46
|
+
* Long enough for a slow route to finish, short enough that a permanently
|
|
47
|
+
* animating page does not stall the suite. The reference implementation allows
|
|
48
|
+
* 120s for a cold navigation, so this ceiling will fire there — which is the
|
|
49
|
+
* correct outcome: scan what is on screen, and record that the page never went
|
|
50
|
+
* quiet.
|
|
51
|
+
*/
|
|
52
|
+
settleTimeoutMs: 5000,
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* The in-page detector.
|
|
56
|
+
*
|
|
57
|
+
* Written as a string rather than a function because `addInitScript` serialises
|
|
58
|
+
* it into a context that has none of this module's scope, and because the config
|
|
59
|
+
* values are interpolated at install time — there is no channel to pass arguments
|
|
60
|
+
* to an init script that runs before every navigation.
|
|
61
|
+
*
|
|
62
|
+
* Deliberately defensive throughout: this runs inside a customer's page, before
|
|
63
|
+
* their own scripts, on every navigation. It must never throw, never retain
|
|
64
|
+
* anything large, and never be observable in a way that changes their app's
|
|
65
|
+
* behaviour.
|
|
66
|
+
*/
|
|
67
|
+
export function settleScript(config) {
|
|
68
|
+
return `
|
|
69
|
+
(() => {
|
|
70
|
+
if (window.__setsquareSettleInstalled) return;
|
|
71
|
+
Object.defineProperty(window, '__setsquareSettleInstalled', {
|
|
72
|
+
value: true, enumerable: false, configurable: true,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const QUIET = ${config.quietMs};
|
|
76
|
+
const CEILING = ${config.settleTimeoutMs};
|
|
77
|
+
|
|
78
|
+
let quietTimer;
|
|
79
|
+
let ceilingTimer;
|
|
80
|
+
let navigatedAt = Date.now();
|
|
81
|
+
let mutations = 0;
|
|
82
|
+
let fired = false;
|
|
83
|
+
// Monotonic across the document lifetime, NOT reset per navigation: the
|
|
84
|
+
// teardown sweep uses it to decide whether a page changed after being scanned.
|
|
85
|
+
window.__setsquareMutationTotal = 0;
|
|
86
|
+
let via = 'load';
|
|
87
|
+
|
|
88
|
+
const report = (reason) => {
|
|
89
|
+
if (fired) return;
|
|
90
|
+
fired = true;
|
|
91
|
+
clearTimeout(quietTimer);
|
|
92
|
+
clearTimeout(ceilingTimer);
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
// The binding is installed by the SDK. If it is missing — a page that
|
|
96
|
+
// reloaded before installation completed — there is nothing to do and
|
|
97
|
+
// certainly nothing worth throwing over.
|
|
98
|
+
if (typeof window.${SETTLE_BINDING} === 'function') {
|
|
99
|
+
window.${SETTLE_BINDING}({
|
|
100
|
+
url: location.href,
|
|
101
|
+
reason,
|
|
102
|
+
via,
|
|
103
|
+
sinceNavMs: Date.now() - navigatedAt,
|
|
104
|
+
mutations,
|
|
105
|
+
settleTimedOut: reason === 'ceiling',
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
} catch {}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const scheduleQuiet = () => {
|
|
112
|
+
clearTimeout(quietTimer);
|
|
113
|
+
quietTimer = setTimeout(() => {
|
|
114
|
+
// readyState is checked HERE rather than as a precondition, so a page that
|
|
115
|
+
// goes quiet while still loading keeps waiting instead of being scanned
|
|
116
|
+
// half-built.
|
|
117
|
+
if (document.readyState === 'complete') report('quiet');
|
|
118
|
+
else scheduleQuiet();
|
|
119
|
+
}, QUIET);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const restart = (kind) => {
|
|
123
|
+
via = kind;
|
|
124
|
+
fired = false;
|
|
125
|
+
navigatedAt = Date.now();
|
|
126
|
+
mutations = 0;
|
|
127
|
+
clearTimeout(ceilingTimer);
|
|
128
|
+
ceilingTimer = setTimeout(() => report('ceiling'), CEILING);
|
|
129
|
+
scheduleQuiet();
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
// Observing \`document\` rather than \`document.documentElement\`: an init
|
|
134
|
+
// script runs before the element exists, and a subtree observer on the
|
|
135
|
+
// document sees everything anyway.
|
|
136
|
+
new MutationObserver((records) => {
|
|
137
|
+
mutations += records.length;
|
|
138
|
+
window.__setsquareMutationTotal += records.length;
|
|
139
|
+
// Any mutation restarts the quiet window. This is what makes the scanner
|
|
140
|
+
// wait for late content instead of auditing a shell.
|
|
141
|
+
scheduleQuiet();
|
|
142
|
+
}).observe(document, {
|
|
143
|
+
subtree: true, childList: true, attributes: true, characterData: true,
|
|
144
|
+
});
|
|
145
|
+
} catch {}
|
|
146
|
+
|
|
147
|
+
for (const method of ['pushState', 'replaceState']) {
|
|
148
|
+
const original = history[method];
|
|
149
|
+
history[method] = function (...args) {
|
|
150
|
+
const result = original.apply(this, args);
|
|
151
|
+
try { restart('route-change'); } catch {}
|
|
152
|
+
return result;
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
window.addEventListener('popstate', () => { try { restart('route-change'); } catch {} });
|
|
157
|
+
|
|
158
|
+
restart('load');
|
|
159
|
+
})();
|
|
160
|
+
`;
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=settle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settle.js","sourceRoot":"","sources":["../src/settle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,wFAAwF;AACxF,MAAM,CAAC,MAAM,cAAc,GAAG,0BAA0B,CAAC;AAEzD,MAAM,CAAC,MAAM,cAAc,GAAG,mBAAmB,CAAC;AAqBlD,MAAM,CAAC,MAAM,cAAc,GAAiB;IAC1C;;;;;;;OAOG;IACH,OAAO,EAAE,GAAG;IACZ;;;;;;;;OAQG;IACH,eAAe,EAAE,IAAI;CACtB,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,YAAY,CAAC,MAAoB;IAC/C,OAAO;;;;;;;kBAOS,MAAM,CAAC,OAAO;oBACZ,MAAM,CAAC,eAAe;;;;;;;;;;;;;;;;;;;;;;0BAsBhB,cAAc;iBACvB,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6D9B,CAAC;AACF,CAAC"}
|