dungbeetle 0.1.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 (116) hide show
  1. package/LICENSE +105 -0
  2. package/NOTICE +19 -0
  3. package/README.md +139 -0
  4. package/dist/api/capture.d.ts +24 -0
  5. package/dist/api/capture.js +61 -0
  6. package/dist/baselines.d.ts +7 -0
  7. package/dist/baselines.js +38 -0
  8. package/dist/brand.d.ts +2 -0
  9. package/dist/brand.js +9 -0
  10. package/dist/capture.d.ts +15 -0
  11. package/dist/capture.js +7 -0
  12. package/dist/captures/api.d.ts +2 -0
  13. package/dist/captures/api.js +114 -0
  14. package/dist/captures/check.d.ts +2 -0
  15. package/dist/captures/check.js +116 -0
  16. package/dist/captures/desktop.d.ts +2 -0
  17. package/dist/captures/desktop.js +97 -0
  18. package/dist/captures/game.d.ts +4 -0
  19. package/dist/captures/game.js +266 -0
  20. package/dist/captures/performance.d.ts +2 -0
  21. package/dist/captures/performance.js +47 -0
  22. package/dist/captures/registry.d.ts +4 -0
  23. package/dist/captures/registry.js +23 -0
  24. package/dist/captures/terminal.d.ts +2 -0
  25. package/dist/captures/terminal.js +65 -0
  26. package/dist/captures/types.d.ts +18 -0
  27. package/dist/captures/types.js +1 -0
  28. package/dist/captures/web.d.ts +3 -0
  29. package/dist/captures/web.js +248 -0
  30. package/dist/check/capture.d.ts +15 -0
  31. package/dist/check/capture.js +76 -0
  32. package/dist/check/junit.d.ts +9 -0
  33. package/dist/check/junit.js +51 -0
  34. package/dist/check/laravel.d.ts +2 -0
  35. package/dist/check/laravel.js +44 -0
  36. package/dist/check/parsers.d.ts +12 -0
  37. package/dist/check/parsers.js +278 -0
  38. package/dist/check/schema.d.ts +2 -0
  39. package/dist/check/schema.js +114 -0
  40. package/dist/cloud.d.ts +42 -0
  41. package/dist/cloud.js +334 -0
  42. package/dist/compare/shared.d.ts +42 -0
  43. package/dist/compare/shared.js +115 -0
  44. package/dist/compare.d.ts +3 -0
  45. package/dist/compare.js +33 -0
  46. package/dist/config.d.ts +146 -0
  47. package/dist/config.js +382 -0
  48. package/dist/desktop/a11y.d.ts +18 -0
  49. package/dist/desktop/a11y.js +74 -0
  50. package/dist/desktop/capture.d.ts +13 -0
  51. package/dist/desktop/capture.js +80 -0
  52. package/dist/desktop/macos.d.ts +8 -0
  53. package/dist/desktop/macos.js +98 -0
  54. package/dist/desktop/ocr.d.ts +17 -0
  55. package/dist/desktop/ocr.js +99 -0
  56. package/dist/diff/lcs.d.ts +5 -0
  57. package/dist/diff/lcs.js +42 -0
  58. package/dist/diff/numeric.d.ts +6 -0
  59. package/dist/diff/numeric.js +24 -0
  60. package/dist/diff/pixel.d.ts +23 -0
  61. package/dist/diff/pixel.js +97 -0
  62. package/dist/diff/structural.d.ts +11 -0
  63. package/dist/diff/structural.js +38 -0
  64. package/dist/diff/text.d.ts +7 -0
  65. package/dist/diff/text.js +64 -0
  66. package/dist/diff/tree.d.ts +46 -0
  67. package/dist/diff/tree.js +188 -0
  68. package/dist/doctor.d.ts +18 -0
  69. package/dist/doctor.js +57 -0
  70. package/dist/game/capture.d.ts +24 -0
  71. package/dist/game/capture.js +51 -0
  72. package/dist/game/protocol.d.ts +30 -0
  73. package/dist/game/protocol.js +146 -0
  74. package/dist/game/walkthrough.d.ts +45 -0
  75. package/dist/game/walkthrough.js +85 -0
  76. package/dist/guards.d.ts +2 -0
  77. package/dist/guards.js +15 -0
  78. package/dist/index.d.ts +2 -0
  79. package/dist/index.js +504 -0
  80. package/dist/json.d.ts +2 -0
  81. package/dist/json.js +40 -0
  82. package/dist/lifecycle.d.ts +14 -0
  83. package/dist/lifecycle.js +190 -0
  84. package/dist/normalization.d.ts +4 -0
  85. package/dist/normalization.js +27 -0
  86. package/dist/perf/ab.d.ts +6 -0
  87. package/dist/perf/ab.js +89 -0
  88. package/dist/perf/autocannon.d.ts +6 -0
  89. package/dist/perf/autocannon.js +101 -0
  90. package/dist/perf/capture.d.ts +7 -0
  91. package/dist/perf/capture.js +6 -0
  92. package/dist/perf/k6.d.ts +9 -0
  93. package/dist/perf/k6.js +44 -0
  94. package/dist/perf/parsers.d.ts +15 -0
  95. package/dist/perf/parsers.js +69 -0
  96. package/dist/perf/run.d.ts +8 -0
  97. package/dist/perf/run.js +45 -0
  98. package/dist/perf/toolOutput.d.ts +3 -0
  99. package/dist/perf/toolOutput.js +24 -0
  100. package/dist/reporters.d.ts +11 -0
  101. package/dist/reporters.js +314 -0
  102. package/dist/runner.d.ts +48 -0
  103. package/dist/runner.js +352 -0
  104. package/dist/snapshot.d.ts +48 -0
  105. package/dist/snapshot.js +37 -0
  106. package/dist/terminal/ansi.d.ts +21 -0
  107. package/dist/terminal/ansi.js +144 -0
  108. package/dist/terminal/capture.d.ts +30 -0
  109. package/dist/terminal/capture.js +91 -0
  110. package/dist/tty.d.ts +72 -0
  111. package/dist/tty.js +175 -0
  112. package/dist/web/domSnapshot.d.ts +27 -0
  113. package/dist/web/domSnapshot.js +55 -0
  114. package/dist/web/playwrightCapture.d.ts +16 -0
  115. package/dist/web/playwrightCapture.js +64 -0
  116. package/package.json +79 -0
@@ -0,0 +1,91 @@
1
+ import { spawn } from "node:child_process";
2
+ import { normalizeAnsiStream } from "./ansi.js";
3
+ export async function captureTerminal(options) {
4
+ const cwd = options.cwd ?? process.cwd();
5
+ const startedAt = performance.now();
6
+ const result = await runShellCommand({
7
+ command: options.command,
8
+ cwd,
9
+ timeoutMs: options.timeoutMs ?? 30_000
10
+ });
11
+ return {
12
+ kind: "terminal",
13
+ command: options.command,
14
+ cwd,
15
+ exitCode: result.exitCode,
16
+ signal: result.signal,
17
+ durationMs: Math.round(performance.now() - startedAt),
18
+ stdout: normalizeAnsiStream(result.stdout, options.maskRules),
19
+ stderr: normalizeAnsiStream(result.stderr, options.maskRules)
20
+ };
21
+ }
22
+ // Terminate the spawned shell and any commands it started. On POSIX the child
23
+ // is a process-group leader (spawned `detached`), so signalling the negated PID
24
+ // reaches the whole group; on Windows process groups work differently, so fall
25
+ // back to a direct kill (this path is exercised by callers, not the timeout
26
+ // test, which is skipped on win32).
27
+ function killTree(child) {
28
+ if (process.platform === "win32" || child.pid === undefined) {
29
+ child.kill("SIGTERM");
30
+ return;
31
+ }
32
+ try {
33
+ process.kill(-child.pid, "SIGTERM");
34
+ }
35
+ catch {
36
+ // The group may already be gone (race with normal exit); signal the child
37
+ // directly as a best effort.
38
+ child.kill("SIGTERM");
39
+ }
40
+ }
41
+ export function runShellCommand(options) {
42
+ return new Promise((resolve, reject) => {
43
+ // `shell: true` means the command runs as a child of a shell process. On
44
+ // its own, `child.kill()` would only signal that shell, leaving the actual
45
+ // command (e.g. a long-running `node`) alive and holding the stdout/stderr
46
+ // pipes open — so `close` never fires until it finishes. Run the child in
47
+ // its own process group (`detached`) so a single signal to the group tears
48
+ // down the shell and everything it spawned.
49
+ const child = spawn(options.command, {
50
+ cwd: options.cwd,
51
+ shell: true,
52
+ stdio: ["ignore", "pipe", "pipe"],
53
+ detached: process.platform !== "win32"
54
+ });
55
+ let stdout = "";
56
+ let stderr = "";
57
+ let settled = false;
58
+ const timeout = setTimeout(() => {
59
+ killTree(child);
60
+ }, options.timeoutMs);
61
+ child.stdout.setEncoding("utf8");
62
+ child.stderr.setEncoding("utf8");
63
+ child.stdout.on("data", (chunk) => {
64
+ stdout += chunk;
65
+ });
66
+ child.stderr.on("data", (chunk) => {
67
+ stderr += chunk;
68
+ });
69
+ child.on("error", (error) => {
70
+ if (settled) {
71
+ return;
72
+ }
73
+ settled = true;
74
+ clearTimeout(timeout);
75
+ reject(error);
76
+ });
77
+ child.on("close", (exitCode, signal) => {
78
+ if (settled) {
79
+ return;
80
+ }
81
+ settled = true;
82
+ clearTimeout(timeout);
83
+ resolve({
84
+ stdout,
85
+ stderr,
86
+ exitCode,
87
+ signal
88
+ });
89
+ });
90
+ });
91
+ }
package/dist/tty.d.ts ADDED
@@ -0,0 +1,72 @@
1
+ export type WriteStream = NodeJS.WriteStream;
2
+ /**
3
+ * Whether ANSI colour escapes should be emitted for the given stream.
4
+ *
5
+ * Honors the de-facto standards: `FORCE_COLOR` wins over everything, then
6
+ * `NO_COLOR` (presence disables, per the no-color.org spec), `TERM=dumb`, and
7
+ * finally whether the stream is an interactive TTY.
8
+ */
9
+ export declare function colorEnabled(stream?: WriteStream): boolean;
10
+ /**
11
+ * Whether unicode/emoji glyphs are safe to print.
12
+ *
13
+ * Non-Windows terminals handle unicode unless they are the bare Linux console.
14
+ * On Windows, only modern hosts (Windows Terminal, VS Code, ConEmu) render
15
+ * emoji reliably, so legacy `cmd.exe` / PowerShell consoles fall back to ASCII.
16
+ * `DUNGBEETLE_ASCII=1` forces ASCII everywhere for testing or troublesome setups.
17
+ */
18
+ export declare function unicodeEnabled(): boolean;
19
+ export type Palette = {
20
+ enabled: boolean;
21
+ bold(value: string): string;
22
+ dim(value: string): string;
23
+ red(value: string): string;
24
+ green(value: string): string;
25
+ yellow(value: string): string;
26
+ blue(value: string): string;
27
+ magenta(value: string): string;
28
+ cyan(value: string): string;
29
+ gray(value: string): string;
30
+ };
31
+ /** Build a palette whose functions are no-ops when colour is disabled. */
32
+ export declare function makePalette(enabled: boolean): Palette;
33
+ export type Symbols = {
34
+ passed: string;
35
+ failed: string;
36
+ missing: string;
37
+ updated: string;
38
+ error: string;
39
+ bullet: string;
40
+ arrow: string;
41
+ clock: string;
42
+ };
43
+ /** Status glyphs with an ASCII fallback for terminals without emoji. */
44
+ export declare function makeSymbols(unicode: boolean): Symbols;
45
+ /**
46
+ * A minimal loading spinner.
47
+ *
48
+ * It animates only on an interactive TTY; in CI, pipes, and dumb terminals it
49
+ * is completely silent so it never pollutes logs or report output. It writes
50
+ * to stderr by default to keep stdout free for JSON/HTML report paths.
51
+ */
52
+ export declare class Spinner {
53
+ private readonly stream;
54
+ private readonly frames;
55
+ private readonly palette;
56
+ private readonly active;
57
+ private readonly intervalMs;
58
+ private timer;
59
+ private frame;
60
+ private text;
61
+ constructor(options?: {
62
+ stream?: WriteStream;
63
+ intervalMs?: number;
64
+ });
65
+ start(text: string): this;
66
+ setText(text: string): this;
67
+ /** Stop the spinner and clear its line, leaving nothing behind. */
68
+ stop(): this;
69
+ private render;
70
+ }
71
+ /** Format a millisecond duration as a short human string (e.g. `1.2s`). */
72
+ export declare function formatDuration(ms: number): string;
package/dist/tty.js ADDED
@@ -0,0 +1,175 @@
1
+ // Small, dependency-free terminal styling helpers.
2
+ //
3
+ // Everything here degrades gracefully so output stays readable in any
4
+ // terminal — including Windows legacy consoles, dumb terminals, piped output,
5
+ // and CI logs. Colour and unicode are detected independently: a terminal may
6
+ // support ANSI colour but not render emoji well, so the two are gated apart.
7
+ function readEnv(name) {
8
+ return process.env[name];
9
+ }
10
+ /**
11
+ * Whether ANSI colour escapes should be emitted for the given stream.
12
+ *
13
+ * Honors the de-facto standards: `FORCE_COLOR` wins over everything, then
14
+ * `NO_COLOR` (presence disables, per the no-color.org spec), `TERM=dumb`, and
15
+ * finally whether the stream is an interactive TTY.
16
+ */
17
+ export function colorEnabled(stream = process.stdout) {
18
+ const force = readEnv("FORCE_COLOR");
19
+ if (force !== undefined) {
20
+ return force !== "0" && force !== "false" && force !== "";
21
+ }
22
+ if ("NO_COLOR" in process.env) {
23
+ return false;
24
+ }
25
+ if (readEnv("TERM") === "dumb") {
26
+ return false;
27
+ }
28
+ return Boolean(stream.isTTY);
29
+ }
30
+ /**
31
+ * Whether unicode/emoji glyphs are safe to print.
32
+ *
33
+ * Non-Windows terminals handle unicode unless they are the bare Linux console.
34
+ * On Windows, only modern hosts (Windows Terminal, VS Code, ConEmu) render
35
+ * emoji reliably, so legacy `cmd.exe` / PowerShell consoles fall back to ASCII.
36
+ * `DUNGBEETLE_ASCII=1` forces ASCII everywhere for testing or troublesome setups.
37
+ */
38
+ export function unicodeEnabled() {
39
+ if (readEnv("DUNGBEETLE_ASCII") === "1") {
40
+ return false;
41
+ }
42
+ if (process.platform !== "win32") {
43
+ return readEnv("TERM") !== "linux";
44
+ }
45
+ return Boolean(readEnv("WT_SESSION") || readEnv("TERM_PROGRAM") || readEnv("ConEmuTask") || readEnv("TERM"));
46
+ }
47
+ const ANSI = {
48
+ reset: "",
49
+ bold: "",
50
+ dim: "",
51
+ red: "",
52
+ green: "",
53
+ yellow: "",
54
+ blue: "",
55
+ magenta: "",
56
+ cyan: "",
57
+ gray: ""
58
+ };
59
+ /** Build a palette whose functions are no-ops when colour is disabled. */
60
+ export function makePalette(enabled) {
61
+ const wrap = (code) => (value) => enabled ? `${code}${value}${ANSI.reset}` : value;
62
+ return {
63
+ enabled,
64
+ bold: wrap(ANSI.bold),
65
+ dim: wrap(ANSI.dim),
66
+ red: wrap(ANSI.red),
67
+ green: wrap(ANSI.green),
68
+ yellow: wrap(ANSI.yellow),
69
+ blue: wrap(ANSI.blue),
70
+ magenta: wrap(ANSI.magenta),
71
+ cyan: wrap(ANSI.cyan),
72
+ gray: wrap(ANSI.gray)
73
+ };
74
+ }
75
+ /** Status glyphs with an ASCII fallback for terminals without emoji. */
76
+ export function makeSymbols(unicode) {
77
+ if (unicode) {
78
+ return {
79
+ passed: "✅",
80
+ failed: "❌",
81
+ missing: "⚠️",
82
+ updated: "📝",
83
+ error: "🛑",
84
+ bullet: "•",
85
+ arrow: "›",
86
+ clock: "⏱"
87
+ };
88
+ }
89
+ return {
90
+ passed: "[pass]",
91
+ failed: "[fail]",
92
+ missing: "[miss]",
93
+ updated: "[upd]",
94
+ error: "[err]",
95
+ bullet: "-",
96
+ arrow: ">",
97
+ clock: ""
98
+ };
99
+ }
100
+ const SPINNER_FRAMES = {
101
+ unicode: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
102
+ ascii: ["-", "\\", "|", "/"]
103
+ };
104
+ const CLEAR_LINE = "\r";
105
+ /**
106
+ * A minimal loading spinner.
107
+ *
108
+ * It animates only on an interactive TTY; in CI, pipes, and dumb terminals it
109
+ * is completely silent so it never pollutes logs or report output. It writes
110
+ * to stderr by default to keep stdout free for JSON/HTML report paths.
111
+ */
112
+ export class Spinner {
113
+ stream;
114
+ frames;
115
+ palette;
116
+ active;
117
+ intervalMs;
118
+ timer;
119
+ frame = 0;
120
+ text = "";
121
+ constructor(options = {}) {
122
+ this.stream = options.stream ?? process.stderr;
123
+ this.intervalMs = options.intervalMs ?? 80;
124
+ const unicode = unicodeEnabled();
125
+ this.frames = unicode ? SPINNER_FRAMES.unicode : SPINNER_FRAMES.ascii;
126
+ this.palette = makePalette(colorEnabled(this.stream));
127
+ this.active = Boolean(this.stream.isTTY);
128
+ }
129
+ start(text) {
130
+ this.text = text;
131
+ if (!this.active) {
132
+ return this;
133
+ }
134
+ this.render();
135
+ this.timer = setInterval(() => {
136
+ this.frame = (this.frame + 1) % this.frames.length;
137
+ this.render();
138
+ }, this.intervalMs);
139
+ // Don't keep the event loop alive solely for the spinner.
140
+ this.timer.unref?.();
141
+ return this;
142
+ }
143
+ setText(text) {
144
+ this.text = text;
145
+ if (this.active && this.timer) {
146
+ this.render();
147
+ }
148
+ return this;
149
+ }
150
+ /** Stop the spinner and clear its line, leaving nothing behind. */
151
+ stop() {
152
+ if (this.timer) {
153
+ clearInterval(this.timer);
154
+ this.timer = undefined;
155
+ }
156
+ if (this.active) {
157
+ this.stream.write(CLEAR_LINE);
158
+ }
159
+ return this;
160
+ }
161
+ render() {
162
+ const glyph = this.palette.cyan(this.frames[this.frame] ?? "");
163
+ this.stream.write(`${CLEAR_LINE}${glyph} ${this.text}`);
164
+ }
165
+ }
166
+ /** Format a millisecond duration as a short human string (e.g. `1.2s`). */
167
+ export function formatDuration(ms) {
168
+ if (!Number.isFinite(ms) || ms < 0) {
169
+ return "0ms";
170
+ }
171
+ if (ms < 1000) {
172
+ return `${Math.round(ms)}ms`;
173
+ }
174
+ return `${(ms / 1000).toFixed(1)}s`;
175
+ }
@@ -0,0 +1,27 @@
1
+ import type { MaskRule } from "../config.js";
2
+ export type DomSnapshotNode = {
3
+ type: "element";
4
+ tagName: string;
5
+ attributes: Record<string, string>;
6
+ children: DomSnapshotNode[];
7
+ } | {
8
+ type: "text";
9
+ value: string;
10
+ };
11
+ export type DomSnapshot = {
12
+ kind: "web";
13
+ source: string;
14
+ capturedAt: "masked";
15
+ root: DomSnapshotNode[];
16
+ };
17
+ export type WebFileSnapshot = DomSnapshot & {
18
+ driver: "file";
19
+ screenshot: {
20
+ mimeType: "image/png";
21
+ data: string;
22
+ };
23
+ };
24
+ export declare function createDomSnapshot(html: string, options: {
25
+ source: string;
26
+ maskRules?: MaskRule[];
27
+ }): DomSnapshot;
@@ -0,0 +1,55 @@
1
+ import { parse } from "parse5";
2
+ import { applyMaskRules, collapseWhitespace } from "../normalization.js";
3
+ export function createDomSnapshot(html, options) {
4
+ const parsed = parse(html);
5
+ return {
6
+ kind: "web",
7
+ source: options.source,
8
+ capturedAt: "masked",
9
+ root: snapshotChildren(parsed.childNodes ?? [], options.maskRules ?? [])
10
+ };
11
+ }
12
+ function snapshotChildren(nodes, maskRules) {
13
+ return nodes
14
+ .map((node) => snapshotNode(node, maskRules))
15
+ .filter((node) => node !== null);
16
+ }
17
+ function snapshotNode(node, maskRules) {
18
+ if (node.nodeName === "#text") {
19
+ const value = collapseWhitespace(applyMaskRules(node.value ?? "", maskRules));
20
+ if (!value) {
21
+ return null;
22
+ }
23
+ return {
24
+ type: "text",
25
+ value
26
+ };
27
+ }
28
+ if (node.nodeName?.startsWith("#")) {
29
+ return null;
30
+ }
31
+ if (!node.tagName) {
32
+ return {
33
+ type: "element",
34
+ tagName: node.nodeName ?? "document",
35
+ attributes: {},
36
+ children: snapshotChildren(node.childNodes ?? [], maskRules)
37
+ };
38
+ }
39
+ return {
40
+ type: "element",
41
+ tagName: node.tagName,
42
+ attributes: snapshotAttributes(node.attrs ?? [], maskRules),
43
+ children: snapshotChildren(node.childNodes ?? [], maskRules)
44
+ };
45
+ }
46
+ // Attributes that are volatile by construction and would make every capture of
47
+ // a CSP-protected page diff against itself: a nonce is a fresh random value per
48
+ // response, exactly the class of dynamic content masks exist for.
49
+ const VOLATILE_ATTRIBUTES = new Set(["nonce"]);
50
+ function snapshotAttributes(attrs, maskRules) {
51
+ return Object.fromEntries(attrs
52
+ .filter((attr) => !VOLATILE_ATTRIBUTES.has(attr.name.toLowerCase()))
53
+ .map((attr) => [attr.name, applyMaskRules(attr.value, maskRules)])
54
+ .sort(([left], [right]) => left.localeCompare(right)));
55
+ }
@@ -0,0 +1,16 @@
1
+ import type { CaptureTarget, MaskRule } from "../config.js";
2
+ import { type DomSnapshot } from "./domSnapshot.js";
3
+ export type PlaywrightWebSnapshot = DomSnapshot & {
4
+ driver: "playwright";
5
+ accessibility?: unknown;
6
+ screenshot?: {
7
+ mimeType: "image/png";
8
+ data: string;
9
+ };
10
+ };
11
+ export declare function capturePlaywrightWeb(target: Extract<CaptureTarget, {
12
+ kind: "web";
13
+ }>, options: {
14
+ maskRules: MaskRule[];
15
+ timeoutMs: number;
16
+ }): Promise<PlaywrightWebSnapshot>;
@@ -0,0 +1,64 @@
1
+ import { createDomSnapshot } from "./domSnapshot.js";
2
+ export async function capturePlaywrightWeb(target, options) {
3
+ if (!target.url) {
4
+ throw new Error(`Playwright web target "${target.name}" requires a url.`);
5
+ }
6
+ const playwright = await import("playwright-core");
7
+ const browser = await launchBrowser(playwright, target);
8
+ try {
9
+ const context = await browser.newContext({
10
+ viewport: target.viewport ?? {
11
+ width: 1280,
12
+ height: 720
13
+ }
14
+ });
15
+ const page = await context.newPage();
16
+ await page.goto(target.url, {
17
+ waitUntil: "networkidle",
18
+ timeout: target.timeoutMs ?? options.timeoutMs
19
+ });
20
+ const dom = createDomSnapshot(await page.content(), {
21
+ source: target.url,
22
+ maskRules: options.maskRules
23
+ });
24
+ dom.driver = "playwright";
25
+ if (target.accessibility) {
26
+ dom.accessibility = await captureAccessibility(page);
27
+ }
28
+ if (target.screenshot) {
29
+ dom.screenshot = {
30
+ mimeType: "image/png",
31
+ data: (await page.screenshot({
32
+ fullPage: true,
33
+ type: "png"
34
+ })).toString("base64")
35
+ };
36
+ }
37
+ await context.close();
38
+ return dom;
39
+ }
40
+ finally {
41
+ await browser.close();
42
+ }
43
+ }
44
+ async function launchBrowser(playwright, target) {
45
+ const executablePath = target.browser?.executablePath ?? process.env.DUNGBEETLE_CHROMIUM_EXECUTABLE_PATH;
46
+ return playwright.chromium.launch({
47
+ channel: target.browser?.channel,
48
+ executablePath,
49
+ headless: true
50
+ });
51
+ }
52
+ async function captureAccessibility(page) {
53
+ const pageLike = page;
54
+ if (pageLike.accessibility?.snapshot) {
55
+ return pageLike.accessibility.snapshot({
56
+ interestingOnly: false
57
+ });
58
+ }
59
+ const body = pageLike.locator?.("body");
60
+ if (body?.ariaSnapshot) {
61
+ return body.ariaSnapshot();
62
+ }
63
+ return null;
64
+ }
package/package.json ADDED
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "dungbeetle",
3
+ "version": "0.1.1",
4
+ "description": "Web, desktop, terminal, and eventually anything — zero adoption cost and runs anywhere.",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=22.5.0"
8
+ },
9
+ "bin": {
10
+ "dungbeetle": "dist/index.js"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "README.md",
15
+ "LICENSE",
16
+ "NOTICE"
17
+ ],
18
+ "main": "./dist/index.js",
19
+ "types": "./dist/index.d.ts",
20
+ "scripts": {
21
+ "build": "tsc -p tsconfig.build.json",
22
+ "check": "npm run lint && npm run typecheck && npm test",
23
+ "lint": "biome check src test",
24
+ "lint:fix": "biome check --write src test",
25
+ "format": "biome format --write src test",
26
+ "format:check": "biome format src test",
27
+ "check:terms": "bash scripts/check-banned-terms.sh",
28
+ "coverage": "vitest run --coverage",
29
+ "prepare": "npm run build",
30
+ "prepublishOnly": "npm run check",
31
+ "dev": "tsx src/index.ts",
32
+ "p1:ci": "tsx src/index.ts ci --config examples/demo/dungbeetle.config.json --json .dungbeetle/p1/ci-report.json --html .dungbeetle/p1/ci-report.html",
33
+ "p1:ci:json": "tsx src/index.ts ci --config examples/demo/dungbeetle.config.json --json .dungbeetle/p1/ci-report.json --json-only",
34
+ "p1:doctor": "tsx src/index.ts doctor --config examples/demo/dungbeetle.config.json --json .dungbeetle/p1/doctor-report.json",
35
+ "p1:doctor:playwright": "tsx src/index.ts doctor --config examples/p1/playwright/dungbeetle.config.json --json .dungbeetle/p1/playwright-doctor-report.json",
36
+ "p1:test": "tsx src/index.ts test --config examples/demo/dungbeetle.config.json --json .dungbeetle/p1/test-report.json --html .dungbeetle/p1/test-report.html",
37
+ "p1:update": "tsx src/index.ts update --config examples/demo/dungbeetle.config.json --json .dungbeetle/p1/update-report.json --html .dungbeetle/p1/update-report.html",
38
+ "game:doctor": "tsx src/index.ts doctor --config examples/game/dungbeetle.config.json",
39
+ "game:ci": "tsx src/index.ts ci --config examples/game/dungbeetle.config.json",
40
+ "game:update": "tsx src/index.ts update --config examples/game/dungbeetle.config.json",
41
+ "game:flake": "tsx src/index.ts flake --config examples/game/dungbeetle.config.json --repeat 5",
42
+ "test": "vitest run",
43
+ "typecheck": "tsc --noEmit"
44
+ },
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "git+https://github.com/DungbeetleTech/client.git"
48
+ },
49
+ "keywords": [
50
+ "snapshot-testing",
51
+ "visual-regression",
52
+ "terminal",
53
+ "dom",
54
+ "testing"
55
+ ],
56
+ "author": "DungbeetleDev <tech@dungbeetle.dev>",
57
+ "license": "SEE LICENSE IN LICENSE",
58
+ "bugs": {
59
+ "url": "https://github.com/DungbeetleTech/client/issues"
60
+ },
61
+ "homepage": "https://github.com/DungbeetleTech/client#readme",
62
+ "dependencies": {
63
+ "commander": "^15.0.0",
64
+ "parse5": "^8.0.1",
65
+ "playwright-core": "^1.61.1",
66
+ "pngjs": "^7.0.0",
67
+ "zod": "^4.4.3"
68
+ },
69
+ "devDependencies": {
70
+ "@biomejs/biome": "2.5.1",
71
+ "@types/node": "^26.0.1",
72
+ "@types/pngjs": "^6.0.5",
73
+ "@vitest/coverage-v8": "^4.1.9",
74
+ "esbuild": "^0.28.1",
75
+ "tsx": "^4.22.4",
76
+ "typescript": "^6.0.3",
77
+ "vitest": "^4.1.9"
78
+ }
79
+ }