@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.
Files changed (181) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +57 -0
  3. package/README.md +338 -0
  4. package/THIRD-PARTY-NOTICES.md +61 -0
  5. package/dist/autoscan.d.ts +25 -0
  6. package/dist/autoscan.d.ts.map +1 -0
  7. package/dist/autoscan.js +332 -0
  8. package/dist/autoscan.js.map +1 -0
  9. package/dist/axe.d.ts +96 -0
  10. package/dist/axe.d.ts.map +1 -0
  11. package/dist/axe.js +210 -0
  12. package/dist/axe.js.map +1 -0
  13. package/dist/bin/setsquare.d.ts +3 -0
  14. package/dist/bin/setsquare.d.ts.map +1 -0
  15. package/dist/bin/setsquare.js +21 -0
  16. package/dist/bin/setsquare.js.map +1 -0
  17. package/dist/brand.d.ts +84 -0
  18. package/dist/brand.d.ts.map +1 -0
  19. package/dist/brand.js +335 -0
  20. package/dist/brand.js.map +1 -0
  21. package/dist/checks/dynamic.d.ts +57 -0
  22. package/dist/checks/dynamic.d.ts.map +1 -0
  23. package/dist/checks/dynamic.js +184 -0
  24. package/dist/checks/dynamic.js.map +1 -0
  25. package/dist/checks/rule-pack.d.ts +34 -0
  26. package/dist/checks/rule-pack.d.ts.map +1 -0
  27. package/dist/checks/rule-pack.js +270 -0
  28. package/dist/checks/rule-pack.js.map +1 -0
  29. package/dist/checks/versions.d.ts +28 -0
  30. package/dist/checks/versions.d.ts.map +1 -0
  31. package/dist/checks/versions.js +45 -0
  32. package/dist/checks/versions.js.map +1 -0
  33. package/dist/ci.d.ts +37 -0
  34. package/dist/ci.d.ts.map +1 -0
  35. package/dist/ci.js +199 -0
  36. package/dist/ci.js.map +1 -0
  37. package/dist/contract.d.ts +334 -0
  38. package/dist/contract.d.ts.map +1 -0
  39. package/dist/contract.js +35 -0
  40. package/dist/contract.js.map +1 -0
  41. package/dist/dedupe.d.ts +79 -0
  42. package/dist/dedupe.d.ts.map +1 -0
  43. package/dist/dedupe.js +102 -0
  44. package/dist/dedupe.js.map +1 -0
  45. package/dist/document.d.ts +29 -0
  46. package/dist/document.d.ts.map +1 -0
  47. package/dist/document.js +94 -0
  48. package/dist/document.js.map +1 -0
  49. package/dist/envelope.d.ts +124 -0
  50. package/dist/envelope.d.ts.map +1 -0
  51. package/dist/envelope.js +337 -0
  52. package/dist/envelope.js.map +1 -0
  53. package/dist/escape.d.ts +14 -0
  54. package/dist/escape.d.ts.map +1 -0
  55. package/dist/escape.js +20 -0
  56. package/dist/escape.js.map +1 -0
  57. package/dist/findings.d.ts +47 -0
  58. package/dist/findings.d.ts.map +1 -0
  59. package/dist/findings.js +229 -0
  60. package/dist/findings.js.map +1 -0
  61. package/dist/fixture.d.ts +7 -0
  62. package/dist/fixture.d.ts.map +1 -0
  63. package/dist/fixture.js +237 -0
  64. package/dist/fixture.js.map +1 -0
  65. package/dist/group.d.ts +214 -0
  66. package/dist/group.d.ts.map +1 -0
  67. package/dist/group.js +709 -0
  68. package/dist/group.js.map +1 -0
  69. package/dist/html-report.d.ts +68 -0
  70. package/dist/html-report.d.ts.map +1 -0
  71. package/dist/html-report.js +649 -0
  72. package/dist/html-report.js.map +1 -0
  73. package/dist/human.d.ts +65 -0
  74. package/dist/human.d.ts.map +1 -0
  75. package/dist/human.js +111 -0
  76. package/dist/human.js.map +1 -0
  77. package/dist/index.d.ts +34 -0
  78. package/dist/index.d.ts.map +1 -0
  79. package/dist/index.js +33 -0
  80. package/dist/index.js.map +1 -0
  81. package/dist/infer.d.ts +65 -0
  82. package/dist/infer.d.ts.map +1 -0
  83. package/dist/infer.js +67 -0
  84. package/dist/infer.js.map +1 -0
  85. package/dist/insights.d.ts +140 -0
  86. package/dist/insights.d.ts.map +1 -0
  87. package/dist/insights.js +483 -0
  88. package/dist/insights.js.map +1 -0
  89. package/dist/install.d.ts +8 -0
  90. package/dist/install.d.ts.map +1 -0
  91. package/dist/install.js +114 -0
  92. package/dist/install.js.map +1 -0
  93. package/dist/links.d.ts +45 -0
  94. package/dist/links.d.ts.map +1 -0
  95. package/dist/links.js +45 -0
  96. package/dist/links.js.map +1 -0
  97. package/dist/mask.d.ts +85 -0
  98. package/dist/mask.d.ts.map +1 -0
  99. package/dist/mask.js +98 -0
  100. package/dist/mask.js.map +1 -0
  101. package/dist/observe.d.ts +51 -0
  102. package/dist/observe.d.ts.map +1 -0
  103. package/dist/observe.js +233 -0
  104. package/dist/observe.js.map +1 -0
  105. package/dist/options.d.ts +249 -0
  106. package/dist/options.d.ts.map +1 -0
  107. package/dist/options.js +185 -0
  108. package/dist/options.js.map +1 -0
  109. package/dist/output.d.ts +113 -0
  110. package/dist/output.d.ts.map +1 -0
  111. package/dist/output.js +231 -0
  112. package/dist/output.js.map +1 -0
  113. package/dist/perf.d.ts +11 -0
  114. package/dist/perf.d.ts.map +1 -0
  115. package/dist/perf.js +150 -0
  116. package/dist/perf.js.map +1 -0
  117. package/dist/reading-level.d.ts +73 -0
  118. package/dist/reading-level.d.ts.map +1 -0
  119. package/dist/reading-level.js +189 -0
  120. package/dist/reading-level.js.map +1 -0
  121. package/dist/reporter.d.ts +94 -0
  122. package/dist/reporter.d.ts.map +1 -0
  123. package/dist/reporter.js +709 -0
  124. package/dist/reporter.js.map +1 -0
  125. package/dist/rules.d.ts +55 -0
  126. package/dist/rules.d.ts.map +1 -0
  127. package/dist/rules.js +77 -0
  128. package/dist/rules.js.map +1 -0
  129. package/dist/runtime.d.ts +32 -0
  130. package/dist/runtime.d.ts.map +1 -0
  131. package/dist/runtime.js +67 -0
  132. package/dist/runtime.js.map +1 -0
  133. package/dist/sanitise.d.ts +116 -0
  134. package/dist/sanitise.d.ts.map +1 -0
  135. package/dist/sanitise.js +247 -0
  136. package/dist/sanitise.js.map +1 -0
  137. package/dist/scan.d.ts +60 -0
  138. package/dist/scan.d.ts.map +1 -0
  139. package/dist/scan.js +246 -0
  140. package/dist/scan.js.map +1 -0
  141. package/dist/screens.d.ts +98 -0
  142. package/dist/screens.d.ts.map +1 -0
  143. package/dist/screens.js +301 -0
  144. package/dist/screens.js.map +1 -0
  145. package/dist/settle.d.ts +65 -0
  146. package/dist/settle.d.ts.map +1 -0
  147. package/dist/settle.js +162 -0
  148. package/dist/settle.js.map +1 -0
  149. package/dist/shots.d.ts +109 -0
  150. package/dist/shots.d.ts.map +1 -0
  151. package/dist/shots.js +113 -0
  152. package/dist/shots.js.map +1 -0
  153. package/dist/style.d.ts +47 -0
  154. package/dist/style.d.ts.map +1 -0
  155. package/dist/style.js +154 -0
  156. package/dist/style.js.map +1 -0
  157. package/dist/suite.d.ts +218 -0
  158. package/dist/suite.d.ts.map +1 -0
  159. package/dist/suite.js +219 -0
  160. package/dist/suite.js.map +1 -0
  161. package/dist/summary.d.ts +268 -0
  162. package/dist/summary.d.ts.map +1 -0
  163. package/dist/summary.js +1308 -0
  164. package/dist/summary.js.map +1 -0
  165. package/dist/unscannable.d.ts +75 -0
  166. package/dist/unscannable.d.ts.map +1 -0
  167. package/dist/unscannable.js +111 -0
  168. package/dist/unscannable.js.map +1 -0
  169. package/dist/versions.d.ts +13 -0
  170. package/dist/versions.d.ts.map +1 -0
  171. package/dist/versions.js +39 -0
  172. package/dist/versions.js.map +1 -0
  173. package/dist/wcag-map.d.ts +19 -0
  174. package/dist/wcag-map.d.ts.map +1 -0
  175. package/dist/wcag-map.js +52 -0
  176. package/dist/wcag-map.js.map +1 -0
  177. package/dist/with-setsquare.d.ts +26 -0
  178. package/dist/with-setsquare.d.ts.map +1 -0
  179. package/dist/with-setsquare.js +70 -0
  180. package/dist/with-setsquare.js.map +1 -0
  181. 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"}
@@ -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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"}
@@ -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"}