@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,109 @@
1
+ /**
2
+ * Screenshots, indexed rather than taken.
3
+ *
4
+ * This module is the whole reason the visual report does not breach decision 4.
5
+ * Setsquare holds no pixels: it takes no screenshot, writes no image, and
6
+ * copies nothing. What it does is read the image attachments Playwright ALREADY
7
+ * wrote — because the customer set `screenshot: 'on'`, called
8
+ * `toHaveScreenshot()`, or attached one themselves — and record where they are.
9
+ * The contact sheet then links to those files where they lie.
10
+ *
11
+ * That distinction is not pedantry. "The local path holds no pixels" is what
12
+ * makes PII-safety structural rather than best-effort, and a helper that
13
+ * screenshotted on the customer's behalf would move the responsibility for an
14
+ * unmasked capture from their config to our code. Indexing leaves it exactly
15
+ * where it already was: with the person who turned Playwright's screenshots on.
16
+ *
17
+ * It also means the SDK cannot compare anything. Where a comparison appears in
18
+ * the report, Playwright's own `toHaveScreenshot()` made it — we found the
19
+ * expected/actual/diff trio it attached and put the three side by side. There is
20
+ * no baseline here, no history, and no verdict: a person scans and decides.
21
+ */
22
+ /**
23
+ * Which part of a comparison an image is, or `shot` for a standalone capture.
24
+ *
25
+ * Read from the attachment name because that is where Playwright puts it:
26
+ * `toHaveScreenshot()` attaches `<base>-expected`, `-actual`, `-diff` and
27
+ * (on a retry that changed) `-previous`, all sharing one base name.
28
+ */
29
+ export type ShotRole = 'expected' | 'previous' | 'actual' | 'diff' | 'shot';
30
+ export interface Shot {
31
+ /** The attachment name, exactly as Playwright reported it. */
32
+ name: string;
33
+ /** Name with the role suffix and extension removed; the trio's shared key. */
34
+ base: string;
35
+ role: ShotRole;
36
+ contentType: string;
37
+ /** Absolute path, on the machine that captured it. Never read, only linked. */
38
+ path: string;
39
+ }
40
+ /**
41
+ * The attachment shape, structurally typed.
42
+ *
43
+ * Playwright's own `TestResult['attachments']` element, restated as the three
44
+ * fields this module reads. Typed structurally so the classifier can be tested
45
+ * without constructing a Playwright test result, which is a large object with a
46
+ * lot of it irrelevant here.
47
+ */
48
+ export interface ImageAttachment {
49
+ name: string;
50
+ contentType: string;
51
+ path?: string | undefined;
52
+ body?: Buffer | undefined;
53
+ }
54
+ export interface CollectShotsResult {
55
+ shots: Shot[];
56
+ /** Image attachments that exist only in memory, and so cannot be linked to. */
57
+ inMemory: number;
58
+ }
59
+ /**
60
+ * Pull the image attachments out of one test result.
61
+ *
62
+ * Two cases are deliberately not equivalent. An attachment with a `path` is a
63
+ * file on disk we can link to. An attachment with only a `body` is an image
64
+ * Playwright is holding in memory — `testInfo.attach(name, { body })` never
65
+ * writes it — and putting it in the report would mean writing the bytes
66
+ * ourselves, which is the one thing this module exists not to do. Those are
67
+ * COUNTED and reported as a warning, because an image the reader cannot see
68
+ * must not silently look like an image that was never taken.
69
+ *
70
+ * Selection is by `contentType` rather than by file extension. Playwright always
71
+ * sets it for a screenshot, and reading the type is both more correct than
72
+ * guessing from a name and the reason this file names no image format at all.
73
+ */
74
+ export declare function collectShots(attachments: readonly ImageAttachment[]): CollectShotsResult;
75
+ /** One image, or one comparison's images, as the report renders them together. */
76
+ export interface ShotGroup {
77
+ base: string;
78
+ /** True when Playwright attached more than one role under this base name. */
79
+ comparison: boolean;
80
+ shots: Shot[];
81
+ }
82
+ /**
83
+ * Group a test's shots into what the report shows as a single unit.
84
+ *
85
+ * A `toHaveScreenshot()` failure attaches three or four images that only mean
86
+ * anything together, so they are grouped by their shared base name and ordered
87
+ * expected → previous → actual → diff. Everything else is its own group, in the
88
+ * order it was attached, which is the order the test took them in — the closest
89
+ * thing the report has to a walkthrough of the flow.
90
+ */
91
+ export declare function groupShots(shots: readonly Shot[]): ShotGroup[];
92
+ /**
93
+ * Drop shots whose file is no longer where the attachment said it was.
94
+ *
95
+ * Two ordinary situations produce them, and both would otherwise fill a
96
+ * QA-facing page with broken image icons — which is the fastest way to make
97
+ * someone stop trusting a report. A merged run group can hold parts captured on
98
+ * other machines, whose paths mean nothing here. And a suite that cleans its
99
+ * output directory between the run and the reading leaves the paths dangling.
100
+ *
101
+ * Missing shots are COUNTED for the caller to warn about, never quietly
102
+ * dropped: "the report shows nine of eleven captures" is a fact the reader needs
103
+ * in order to know they are not looking at everything.
104
+ */
105
+ export declare function reachableShots(shots: readonly Shot[]): {
106
+ shots: Shot[];
107
+ missing: number;
108
+ };
109
+ //# sourceMappingURL=shots.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shots.d.ts","sourceRoot":"","sources":["../src/shots.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH;;;;;;GAMG;AACH,MAAM,MAAM,QAAQ,GAAG,UAAU,GAAG,UAAU,GAAG,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;AAE5E,MAAM,WAAW,IAAI;IACnB,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAC;IACb,8EAA8E;IAC9E,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,QAAQ,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,+EAA+E;IAC/E,IAAI,EAAE,MAAM,CAAC;CACd;AAcD;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3B;AAkBD,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,+EAA+E;IAC/E,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,YAAY,CAAC,WAAW,EAAE,SAAS,eAAe,EAAE,GAAG,kBAAkB,CAuBxF;AAED,kFAAkF;AAClF,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,UAAU,EAAE,OAAO,CAAC;IACpB,KAAK,EAAE,IAAI,EAAE,CAAC;CACf;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,SAAS,IAAI,EAAE,GAAG,SAAS,EAAE,CAiB9D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,IAAI,EAAE,GAAG;IAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAUzF"}
package/dist/shots.js ADDED
@@ -0,0 +1,113 @@
1
+ import { existsSync } from 'node:fs';
2
+ /** Fixed so a comparison always reads left-to-right in the same order. */
3
+ const ROLE_ORDER = {
4
+ expected: 0,
5
+ previous: 1,
6
+ actual: 2,
7
+ diff: 3,
8
+ shot: 4,
9
+ };
10
+ /** The suffixes Playwright's snapshot matchers append, longest first. */
11
+ const ROLE_SUFFIXES = ['expected', 'previous', 'actual', 'diff'];
12
+ /** Extension stripped without ever naming an image format. */
13
+ function withoutExtension(name) {
14
+ return name.replace(/\.[A-Za-z0-9]{1,5}$/, '');
15
+ }
16
+ function classify(name) {
17
+ const stem = withoutExtension(name);
18
+ for (const role of ROLE_SUFFIXES) {
19
+ const suffix = `-${role}`;
20
+ if (stem.endsWith(suffix))
21
+ return { base: stem.slice(0, -suffix.length), role };
22
+ }
23
+ return { base: stem, role: 'shot' };
24
+ }
25
+ /**
26
+ * Pull the image attachments out of one test result.
27
+ *
28
+ * Two cases are deliberately not equivalent. An attachment with a `path` is a
29
+ * file on disk we can link to. An attachment with only a `body` is an image
30
+ * Playwright is holding in memory — `testInfo.attach(name, { body })` never
31
+ * writes it — and putting it in the report would mean writing the bytes
32
+ * ourselves, which is the one thing this module exists not to do. Those are
33
+ * COUNTED and reported as a warning, because an image the reader cannot see
34
+ * must not silently look like an image that was never taken.
35
+ *
36
+ * Selection is by `contentType` rather than by file extension. Playwright always
37
+ * sets it for a screenshot, and reading the type is both more correct than
38
+ * guessing from a name and the reason this file names no image format at all.
39
+ */
40
+ export function collectShots(attachments) {
41
+ const shots = [];
42
+ let inMemory = 0;
43
+ for (const attachment of attachments) {
44
+ if (!attachment.contentType?.startsWith('image/'))
45
+ continue;
46
+ if (!attachment.path) {
47
+ if (attachment.body)
48
+ inMemory += 1;
49
+ continue;
50
+ }
51
+ const { base, role } = classify(attachment.name);
52
+ shots.push({
53
+ name: attachment.name,
54
+ base,
55
+ role,
56
+ contentType: attachment.contentType,
57
+ path: attachment.path,
58
+ });
59
+ }
60
+ return { shots, inMemory };
61
+ }
62
+ /**
63
+ * Group a test's shots into what the report shows as a single unit.
64
+ *
65
+ * A `toHaveScreenshot()` failure attaches three or four images that only mean
66
+ * anything together, so they are grouped by their shared base name and ordered
67
+ * expected → previous → actual → diff. Everything else is its own group, in the
68
+ * order it was attached, which is the order the test took them in — the closest
69
+ * thing the report has to a walkthrough of the flow.
70
+ */
71
+ export function groupShots(shots) {
72
+ const groups = new Map();
73
+ for (const shot of shots) {
74
+ const existing = groups.get(shot.base);
75
+ if (existing)
76
+ existing.push(shot);
77
+ else
78
+ groups.set(shot.base, [shot]);
79
+ }
80
+ return [...groups].map(([base, members]) => {
81
+ const roles = new Set(members.map((shot) => shot.role));
82
+ return {
83
+ base,
84
+ comparison: roles.size > 1 || !roles.has('shot'),
85
+ shots: [...members].sort((a, b) => ROLE_ORDER[a.role] - ROLE_ORDER[b.role]),
86
+ };
87
+ });
88
+ }
89
+ /**
90
+ * Drop shots whose file is no longer where the attachment said it was.
91
+ *
92
+ * Two ordinary situations produce them, and both would otherwise fill a
93
+ * QA-facing page with broken image icons — which is the fastest way to make
94
+ * someone stop trusting a report. A merged run group can hold parts captured on
95
+ * other machines, whose paths mean nothing here. And a suite that cleans its
96
+ * output directory between the run and the reading leaves the paths dangling.
97
+ *
98
+ * Missing shots are COUNTED for the caller to warn about, never quietly
99
+ * dropped: "the report shows nine of eleven captures" is a fact the reader needs
100
+ * in order to know they are not looking at everything.
101
+ */
102
+ export function reachableShots(shots) {
103
+ const reachable = [];
104
+ let missing = 0;
105
+ for (const shot of shots) {
106
+ if (existsSync(shot.path))
107
+ reachable.push(shot);
108
+ else
109
+ missing += 1;
110
+ }
111
+ return { shots: reachable, missing };
112
+ }
113
+ //# sourceMappingURL=shots.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shots.js","sourceRoot":"","sources":["../src/shots.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ export interface Style {
2
+ bold(value: string): string;
3
+ dim(value: string): string;
4
+ /** For the one or two figures a line exists to deliver. */
5
+ figure(value: string): string;
6
+ heading(value: string): string;
7
+ /** The wordmark, and nothing else. */
8
+ accent(value: string): string;
9
+ on: boolean;
10
+ }
11
+ export declare function createStyle(env?: NodeJS.ProcessEnv, isTTY?: boolean): Style;
12
+ /**
13
+ * A number in a fixed right-aligned gutter, with its label after it.
14
+ *
15
+ * This is the whole layout idea. Putting every figure in the same column, ahead
16
+ * of its prose, lets a reader scan one narrow strip down the left and take in the
17
+ * shape of the report without reading a word of it. It degrades perfectly to
18
+ * plain text, greps cleanly, and survives being pasted into a narrow Slack window
19
+ * — which a bordered table does not, and which is the main thing that happens to
20
+ * this output.
21
+ */
22
+ export declare function figureLine(style: Style, value: string | number, label: string, gutter?: number): string;
23
+ /**
24
+ * Soft-wrap a long value under the gutter, on word boundaries.
25
+ *
26
+ * Wrapping rather than truncating, for the reason the run summary learned the
27
+ * hard way: a truncated line cuts off the useful half. A repository with five
28
+ * Playwright configs has a config list longer than any terminal, and the fifth
29
+ * name matters as much as the first.
30
+ */
31
+ export declare function continuationLines(style: Style, value: string, width?: number, gutter?: number): string[];
32
+ /** A section heading. Capitals, so it still reads as a heading with colour stripped. */
33
+ export declare function heading(style: Style, text: string): string;
34
+ /**
35
+ * A heading followed by figure rows and their notes.
36
+ *
37
+ * The one repeated shape in the run summary, extracted because P1b adds five more
38
+ * sections of exactly this form and five hand-rolled copies would drift in
39
+ * spacing. Fifteen lines rather than a section framework: the existing sections
40
+ * keep their bespoke layouts, and nothing here retrofits them.
41
+ */
42
+ export declare function figureBlock(style: Style, title: string, rows: readonly {
43
+ value: number | string;
44
+ label: string;
45
+ notes?: string[];
46
+ }[], notes?: readonly string[], width?: number): string[];
47
+ //# sourceMappingURL=style.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"style.d.ts","sourceRoot":"","sources":["../src/style.ts"],"names":[],"mappings":"AAkDA,MAAM,WAAW,KAAK;IACpB,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,2DAA2D;IAC3D,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9B,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,sCAAsC;IACtC,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9B,EAAE,EAAE,OAAO,CAAC;CACb;AAED,wBAAgB,WAAW,CACzB,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,KAAK,GAAE,OAAuC,GAC7C,KAAK,CAkCP;AAED;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,MAAM,GAAG,MAAM,EACtB,KAAK,EAAE,MAAM,EACb,MAAM,SAAI,GACT,MAAM,CASR;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,SAAK,EAAE,MAAM,SAAI,GAAG,MAAM,EAAE,CAkB/F;AAED,wFAAwF;AACxF,wBAAgB,OAAO,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,SAAS;IAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,EAAE,EAC5E,KAAK,GAAE,SAAS,MAAM,EAAO,EAC7B,KAAK,SAAK,GACT,MAAM,EAAE,CAcV"}
package/dist/style.js ADDED
@@ -0,0 +1,154 @@
1
+ /*
2
+ * DUPLICATED FROM `packages/review-cli/src/style.ts`, deliberately, and the reason is
3
+ * structural rather than laziness.
4
+ *
5
+ * Decision 9 (2026-08-17) makes the CLI unable to load into a customer's test
6
+ * process — that is the whole point of the two-package split, and it is enforced
7
+ * by there being no import path rather than by a convention anybody has to
8
+ * remember. The SDK does declare `@setsquare/review-cli` as a dependency, but only so
9
+ * one install links the `setsquare` binary; importing CLI code from the reporter would
10
+ * pull the audit, its parser and its adapters into the import graph of every test
11
+ * run, which is exactly what the split exists to prevent.
12
+ *
13
+ * So sixty zero-dependency lines are copied instead. The alternative — a third
14
+ * published package for a string-wrapping helper — costs more to sign, publish
15
+ * and review each release than this costs to keep in step, and the two are held
16
+ * in step by the assertion that matters being made in both suites: strip the
17
+ * escapes and the output is unchanged.
18
+ */
19
+ /**
20
+ * The small amount of terminal styling this product allows itself.
21
+ *
22
+ * The governing rule predates this file and does not bend: **colour is
23
+ * decoration, never information.** Every line must carry its full meaning in
24
+ * plain text, because we sell accessibility and because CI logs, pipes and pasted
25
+ * output all lose colour. Nothing here may be the only thing telling a reader
26
+ * that something failed.
27
+ *
28
+ * What styling IS for is hierarchy — making the number a person came for land
29
+ * before the sentence explaining it. That is a real problem rather than a vanity
30
+ * one: a well-structured report where every line carries equal weight reads as a
31
+ * wall, and the reader ends up doing the tool's job of working out what matters.
32
+ *
33
+ * Detection follows the informal standard rather than our preference: an explicit
34
+ * NO_COLOR of any value wins over everything, then FORCE_COLOR, then whether we
35
+ * are actually talking to a terminal. CI is treated as no-colour deliberately —
36
+ * build logs are read as text far more often than they are watched live.
37
+ *
38
+ * Escapes are written as escape SEQUENCES rather than literal control bytes. A
39
+ * literal escape makes the file binary to grep and awkward to diff, and a styling
40
+ * module is precisely the file nobody should have to fight to read.
41
+ */
42
+ function enabled(env, isTTY) {
43
+ if (env['NO_COLOR'] !== undefined)
44
+ return false;
45
+ if (env['FORCE_COLOR'] !== undefined)
46
+ return env['FORCE_COLOR'] !== '0';
47
+ if (env['CI'] !== undefined)
48
+ return false;
49
+ return isTTY;
50
+ }
51
+ export function createStyle(env = process.env, isTTY = Boolean(process.stdout.isTTY)) {
52
+ const on = enabled(env, isTTY);
53
+ const wrap = (code) => (value) => on ? `\u001B[${code}m${value}\u001B[0m` : value;
54
+ return {
55
+ on,
56
+ bold: wrap('1'),
57
+ dim: wrap('2'),
58
+ /*
59
+ * Figures stay BOLD, deliberately, while headings get hue.
60
+ *
61
+ * The numbers are the content — they must read as figures on a monochrome
62
+ * terminal, in a pipe, and to anyone whose colour perception differs from
63
+ * ours, so they lean on weight, which survives all three. Hue is spent on
64
+ * structure instead: headings and the wordmark, where losing it costs a
65
+ * reader nothing but noticing it helps them find their place.
66
+ */
67
+ figure: wrap('1'),
68
+ /*
69
+ * The basic 16-colour codes rather than 256-colour or truecolour, and that is
70
+ * an accessibility decision rather than a compatibility one.
71
+ *
72
+ * Codes 30-37 are remapped by the terminal to the user's OWN theme, so cyan on
73
+ * a light Solarized profile and cyan on a dark one are both legible because
74
+ * the person chose them. A hard-coded #7c3aed looks right in our terminal and
75
+ * can be genuinely unreadable in someone else's — and picking an unreadable
76
+ * colour would be a poor advertisement for an accessibility company.
77
+ */
78
+ heading: wrap('1;36'),
79
+ // Bold folded in, so callers never nest two wrappers and emit a double reset.
80
+ accent: wrap('1;35'),
81
+ };
82
+ }
83
+ /**
84
+ * A number in a fixed right-aligned gutter, with its label after it.
85
+ *
86
+ * This is the whole layout idea. Putting every figure in the same column, ahead
87
+ * of its prose, lets a reader scan one narrow strip down the left and take in the
88
+ * shape of the report without reading a word of it. It degrades perfectly to
89
+ * plain text, greps cleanly, and survives being pasted into a narrow Slack window
90
+ * — which a bordered table does not, and which is the main thing that happens to
91
+ * this output.
92
+ */
93
+ export function figureLine(style, value, label, gutter = 7) {
94
+ const shown = typeof value === 'number' ? value.toLocaleString('en-GB') : value;
95
+ // An empty gutter is a continuation line, not a figure. Styling it emits escape
96
+ // codes around nothing, which is invisible on screen and litters any log that
97
+ // keeps them.
98
+ if (shown === '')
99
+ return ` ${' '.repeat(gutter)} ${label}`;
100
+ return ` ${style.figure(String(shown).padStart(gutter))} ${label}`;
101
+ }
102
+ /**
103
+ * Soft-wrap a long value under the gutter, on word boundaries.
104
+ *
105
+ * Wrapping rather than truncating, for the reason the run summary learned the
106
+ * hard way: a truncated line cuts off the useful half. A repository with five
107
+ * Playwright configs has a config list longer than any terminal, and the fifth
108
+ * name matters as much as the first.
109
+ */
110
+ export function continuationLines(style, value, width = 80, gutter = 7) {
111
+ const indent = ' '.repeat(gutter + 4);
112
+ const room = Math.max(24, width - indent.length);
113
+ const lines = [];
114
+ let current = '';
115
+ for (const word of value.split(' ')) {
116
+ if (current !== '' && `${current} ${word}`.length > room) {
117
+ lines.push(`${indent}${style.dim(current)}`);
118
+ current = word;
119
+ }
120
+ else {
121
+ current = current === '' ? word : `${current} ${word}`;
122
+ }
123
+ }
124
+ if (current !== '')
125
+ lines.push(`${indent}${style.dim(current)}`);
126
+ return lines;
127
+ }
128
+ /** A section heading. Capitals, so it still reads as a heading with colour stripped. */
129
+ export function heading(style, text) {
130
+ return ` ${style.heading(text.toUpperCase())}`;
131
+ }
132
+ /**
133
+ * A heading followed by figure rows and their notes.
134
+ *
135
+ * The one repeated shape in the run summary, extracted because P1b adds five more
136
+ * sections of exactly this form and five hand-rolled copies would drift in
137
+ * spacing. Fifteen lines rather than a section framework: the existing sections
138
+ * keep their bespoke layouts, and nothing here retrofits them.
139
+ */
140
+ export function figureBlock(style, title, rows, notes = [], width = 80) {
141
+ if (rows.length === 0 && notes.length === 0)
142
+ return [];
143
+ const lines = [heading(style, title)];
144
+ for (const row of rows) {
145
+ lines.push(figureLine(style, row.value, row.label));
146
+ for (const note of row.notes ?? [])
147
+ lines.push(...continuationLines(style, note, width));
148
+ }
149
+ for (const note of notes)
150
+ lines.push(...continuationLines(style, note, width));
151
+ lines.push('');
152
+ return lines;
153
+ }
154
+ //# sourceMappingURL=style.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"style.js","sourceRoot":"","sources":["../src/style.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Reductions over Playwright's own `TestResult`, which the reporter used to drop.
3
+ *
4
+ * `onTestEnd` read six fields and discarded the rest. What was on the object:
5
+ * `steps` (a tree, with timings), `errors`, `workerIndex`, `annotations`. From
6
+ * that, with no new instrumentation, come three things nothing else in the run
7
+ * can say:
8
+ *
9
+ * - where the suite's wall clock actually goes, down to the step
10
+ * - which worker held the run open, which is the commonest cheap CI win and is
11
+ * invisible in Playwright's own HTML report
12
+ * - why a test failed, so `run.json` can be triaged by whoever it is handed to
13
+ * rather than only by whoever watched it happen
14
+ *
15
+ * Kept out of `reporter.ts` because these are pure functions over plain data and
16
+ * deserve to be tested as such. The reporter's job is to call them.
17
+ *
18
+ * NOT here, and never: `result.stdout`. It is arbitrary customer application and
19
+ * test log output — the highest-PII surface on the object — and no cap makes it
20
+ * safe to carry. A grep assertion in `observe.test.ts` keeps it that way.
21
+ */
22
+ /** The subset of Playwright's `TestStep` this module needs. Structural, so a literal works in tests. */
23
+ export interface StepLike {
24
+ title: string;
25
+ category: string;
26
+ duration: number;
27
+ error?: unknown;
28
+ steps?: StepLike[];
29
+ }
30
+ export interface SlowStep {
31
+ title: string;
32
+ category: string;
33
+ durationMs: number;
34
+ }
35
+ export interface TestFailureFacts {
36
+ errorClass?: string;
37
+ message?: string;
38
+ step?: string;
39
+ }
40
+ export declare function errorClassOf(message: string): string | undefined;
41
+ /**
42
+ * Collapse a message to one line and cap it.
43
+ *
44
+ * A Playwright error message is natively multi-line — assertion diff, call log,
45
+ * code frame — and a newline is a C0 control, which the contract's character
46
+ * constraint bans. Collapsing rather than permitting newlines is a deliberate
47
+ * contract decision recorded in `ingest.ts`: one sanitisation rule, not two that
48
+ * can drift. The repro command is how someone gets the readable original.
49
+ */
50
+ export declare function collapseMessage(value: string): string;
51
+ /**
52
+ * The deepest failing step's title.
53
+ *
54
+ * Deepest, not first: Playwright marks every ancestor of a failure as failed too,
55
+ * so the outermost failing step is usually `Before Hooks` or the test body — true,
56
+ * and useless. The innermost one is the actual operation that broke.
57
+ */
58
+ export declare function deepestFailingStep(steps: readonly StepLike[] | undefined): string | undefined;
59
+ /**
60
+ * Why this test failed, from the error list and the step tree.
61
+ *
62
+ * Returns `undefined` for a test that did not fail, and for a failed test whose
63
+ * result carried neither an error nor an identifiable failing step — because
64
+ * `{}` would imply we looked and found a failure with no properties, which is not
65
+ * what happened.
66
+ */
67
+ export declare function failureFacts(errors: readonly {
68
+ message?: string;
69
+ }[] | undefined, steps: readonly StepLike[] | undefined): TestFailureFacts | undefined;
70
+ /**
71
+ * The slowest LEAF steps.
72
+ *
73
+ * Leaves only, and that is the load-bearing decision. Playwright's step tree
74
+ * nests, and a parent's duration CONTAINS its children's — so a list mixing the
75
+ * two double-counts, and "the slowest step took 8s" would be answered by a
76
+ * wrapper whose children did all the work. Leaves are the actual operations.
77
+ *
78
+ * The cost of that choice, stated: a slow custom step that wraps many fast ones
79
+ * will not appear, even though it is arguably the interesting row. Reporting both
80
+ * would need a tree in the output, and the useful artefact here is a short list.
81
+ */
82
+ export declare function slowestSteps(steps: readonly StepLike[] | undefined, limit?: number): SlowStep[];
83
+ export interface WorkerLoad {
84
+ /**
85
+ * Distinct worker PROCESSES that ran at least one test.
86
+ *
87
+ * **Not the parallelism.** Playwright recycles workers — a new process per
88
+ * project, and another after a worker dies — so a strictly serial suite still
89
+ * reports several. Measured on the SDK's own fixture suite: `workers: 1` in the
90
+ * config, five worker indices across five projects.
91
+ *
92
+ * `configured` below is the parallelism, and it is what decides whether any of
93
+ * this is worth showing.
94
+ */
95
+ workers: number;
96
+ /**
97
+ * `workers` from the Playwright config, when the caller knows it.
98
+ *
99
+ * Load below is only an IMBALANCE if more than one worker could run at once. On
100
+ * a serial suite the spread between processes is just the shape of the projects,
101
+ * and presenting it as imbalance would be a confident wrong answer — which is
102
+ * how this field came to exist: the first version reported `tailMs: 6350` on a
103
+ * one-worker run and read as a serious finding.
104
+ *
105
+ * Absent when the config could not be read; renderers must then withhold the
106
+ * imbalance framing rather than assume parallelism.
107
+ */
108
+ configured?: number;
109
+ /** Busy milliseconds on the busiest worker process. */
110
+ busiestMs: number;
111
+ /** Busy milliseconds on the least busy worker process. */
112
+ idlestMs: number;
113
+ /**
114
+ * How far the busiest worker sits above the median.
115
+ *
116
+ * Meaningful only when `configured` is greater than 1, where a large value means
117
+ * one worker is holding the run open while the others are idle — the commonest
118
+ * cheap CI win. On a serial run it is an artefact and must not be shown.
119
+ */
120
+ tailMs: number;
121
+ }
122
+ /**
123
+ * Per-worker busy time.
124
+ *
125
+ * **Busy time, never wall clock.** These are summed test durations, so the total
126
+ * across workers exceeds the run's elapsed time by roughly the parallelism factor.
127
+ * Nothing may present these as "the run took X" or subtract them from anything —
128
+ * the same discipline scan overhead follows, and for the same reason: we can
129
+ * measure what we spent, not what the run would have cost without it.
130
+ *
131
+ * `undefined` when no outcome carried a worker index, which is the honest answer
132
+ * for a run where Playwright never told us (and for envelopes written before the
133
+ * field existed). Renderers must then say nothing rather than reporting one
134
+ * worker.
135
+ */
136
+ export declare function workerLoad(outcomes: readonly {
137
+ workerIndex?: number;
138
+ durationMs: number;
139
+ }[], configured?: number): WorkerLoad | undefined;
140
+ export interface FlakyTest {
141
+ testId: string;
142
+ /** Which attempt finally passed. */
143
+ passedOnRetry: number;
144
+ /** How the earliest failing attempt failed, when the result said. */
145
+ failure?: TestFailureFacts;
146
+ }
147
+ /**
148
+ * Tests that failed and then passed, itemised.
149
+ *
150
+ * The summary already says "2 flaky". It does not say WHICH, on which attempt, or
151
+ * on which step — and all three are already in the data, which is the whole
152
+ * complaint. No schema change was needed for this; it is a projection over
153
+ * `(testId, retry, status)` plus the failure facts captured alongside.
154
+ */
155
+ interface Attempt {
156
+ testId: string;
157
+ retry: number;
158
+ status: string;
159
+ failure?: TestFailureFacts;
160
+ }
161
+ export declare function flakyTests(outcomes: readonly Attempt[]): FlakyTest[];
162
+ export interface OverheadFacts {
163
+ scans: number;
164
+ scanMs: number;
165
+ axeMs: number;
166
+ runDurationMs: number;
167
+ }
168
+ export interface SuiteFacts {
169
+ slowestTests: {
170
+ testId: string;
171
+ durationMs: number;
172
+ }[];
173
+ slowestSteps: {
174
+ testId: string;
175
+ title: string;
176
+ category: string;
177
+ durationMs: number;
178
+ }[];
179
+ workers?: WorkerLoad;
180
+ flaky: FlakyTest[];
181
+ }
182
+ /**
183
+ * The shape `localFacts` needs, described structurally rather than imported.
184
+ *
185
+ * `TestOutcome` lives in `envelope.ts` and `RunMeta` in `contract.ts`, and both
186
+ * already depend on this module for `FlakyTest`/`WorkerLoad`. Naming the input
187
+ * structurally keeps the dependency pointing one way — and it is what lets both
188
+ * the solo reporter and the run-group merge call this with their own arrays.
189
+ */
190
+ export interface OutcomeLike {
191
+ testId: string;
192
+ durationMs: number;
193
+ retry: number;
194
+ status: string;
195
+ workerIndex?: number;
196
+ failure?: TestFailureFacts;
197
+ slowestSteps?: SlowStep[];
198
+ records: readonly {
199
+ kind: string;
200
+ cost?: {
201
+ totalMs: number;
202
+ axeMs: number;
203
+ };
204
+ }[];
205
+ }
206
+ /**
207
+ * The local-only suite/overhead/coverage blocks for `meta.json`.
208
+ *
209
+ * Assembled in one place because all three are reductions over the same
210
+ * `outcomes` array, and because a renderer recomputing any of them is how the
211
+ * terminal, the HTML report and the markdown output come to disagree.
212
+ */
213
+ export declare function localFacts(outcomes: readonly OutcomeLike[], runDurationMs: number, configuredWorkers?: number): {
214
+ overhead?: OverheadFacts;
215
+ suite: SuiteFacts;
216
+ };
217
+ export {};
218
+ //# sourceMappingURL=suite.d.ts.map