ghost-qa 0.0.0-stage → 1.3.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 (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +273 -3
  3. package/SKILL.md +105 -0
  4. package/dist/bin/ghost-qa.d.ts +35 -0
  5. package/dist/bin/ghost-qa.d.ts.map +1 -0
  6. package/dist/bin/ghost-qa.js +406 -0
  7. package/dist/bin/ghost-qa.js.map +1 -0
  8. package/dist/src/engine/audit-runner.d.ts +10 -0
  9. package/dist/src/engine/audit-runner.d.ts.map +1 -0
  10. package/dist/src/engine/audit-runner.js +138 -0
  11. package/dist/src/engine/audit-runner.js.map +1 -0
  12. package/dist/src/engine/browser-session.d.ts +37 -0
  13. package/dist/src/engine/browser-session.d.ts.map +1 -0
  14. package/dist/src/engine/browser-session.js +114 -0
  15. package/dist/src/engine/browser-session.js.map +1 -0
  16. package/dist/src/engine/interaction-runner.d.ts +41 -0
  17. package/dist/src/engine/interaction-runner.d.ts.map +1 -0
  18. package/dist/src/engine/interaction-runner.js +274 -0
  19. package/dist/src/engine/interaction-runner.js.map +1 -0
  20. package/dist/src/engine/route-crawler.d.ts +31 -0
  21. package/dist/src/engine/route-crawler.d.ts.map +1 -0
  22. package/dist/src/engine/route-crawler.js +110 -0
  23. package/dist/src/engine/route-crawler.js.map +1 -0
  24. package/dist/src/engine/server-lifecycle.d.ts +41 -0
  25. package/dist/src/engine/server-lifecycle.d.ts.map +1 -0
  26. package/dist/src/engine/server-lifecycle.js +154 -0
  27. package/dist/src/engine/server-lifecycle.js.map +1 -0
  28. package/dist/src/index.d.ts +16 -0
  29. package/dist/src/index.d.ts.map +1 -0
  30. package/dist/src/index.js +16 -0
  31. package/dist/src/index.js.map +1 -0
  32. package/dist/src/mcp/server.d.ts +111 -0
  33. package/dist/src/mcp/server.d.ts.map +1 -0
  34. package/dist/src/mcp/server.js +267 -0
  35. package/dist/src/mcp/server.js.map +1 -0
  36. package/dist/src/reporter/agent-diagnostic.d.ts +17 -0
  37. package/dist/src/reporter/agent-diagnostic.d.ts.map +1 -0
  38. package/dist/src/reporter/agent-diagnostic.js +80 -0
  39. package/dist/src/reporter/agent-diagnostic.js.map +1 -0
  40. package/dist/src/reporter/html-report.d.ts +9 -0
  41. package/dist/src/reporter/html-report.d.ts.map +1 -0
  42. package/dist/src/reporter/html-report.js +142 -0
  43. package/dist/src/reporter/html-report.js.map +1 -0
  44. package/dist/src/reporter/terminal-summary.d.ts +5 -0
  45. package/dist/src/reporter/terminal-summary.d.ts.map +1 -0
  46. package/dist/src/reporter/terminal-summary.js +71 -0
  47. package/dist/src/reporter/terminal-summary.js.map +1 -0
  48. package/dist/src/traps/a11y-trap.d.ts +27 -0
  49. package/dist/src/traps/a11y-trap.d.ts.map +1 -0
  50. package/dist/src/traps/a11y-trap.js +173 -0
  51. package/dist/src/traps/a11y-trap.js.map +1 -0
  52. package/dist/src/traps/console-trap.d.ts +28 -0
  53. package/dist/src/traps/console-trap.d.ts.map +1 -0
  54. package/dist/src/traps/console-trap.js +151 -0
  55. package/dist/src/traps/console-trap.js.map +1 -0
  56. package/dist/src/traps/layout-trap.d.ts +31 -0
  57. package/dist/src/traps/layout-trap.d.ts.map +1 -0
  58. package/dist/src/traps/layout-trap.js +150 -0
  59. package/dist/src/traps/layout-trap.js.map +1 -0
  60. package/dist/src/traps/network-trap.d.ts +24 -0
  61. package/dist/src/traps/network-trap.d.ts.map +1 -0
  62. package/dist/src/traps/network-trap.js +132 -0
  63. package/dist/src/traps/network-trap.js.map +1 -0
  64. package/dist/src/traps/perf-trap.d.ts +42 -0
  65. package/dist/src/traps/perf-trap.d.ts.map +1 -0
  66. package/dist/src/traps/perf-trap.js +196 -0
  67. package/dist/src/traps/perf-trap.js.map +1 -0
  68. package/dist/src/types.d.ts +95 -0
  69. package/dist/src/types.d.ts.map +1 -0
  70. package/dist/src/types.js +80 -0
  71. package/dist/src/types.js.map +1 -0
  72. package/package.json +89 -4
  73. package/scripts/prepare-build.mjs +40 -0
@@ -0,0 +1,114 @@
1
+ import { chromium } from 'playwright';
2
+ export const VIEWPORT_PROFILES = {
3
+ desktop: {
4
+ name: 'desktop',
5
+ width: 1440,
6
+ height: 900,
7
+ isMobile: false,
8
+ hasTouch: false,
9
+ deviceScaleFactor: 1,
10
+ },
11
+ mobile: {
12
+ name: 'mobile',
13
+ width: 390,
14
+ height: 844,
15
+ isMobile: true,
16
+ hasTouch: true,
17
+ deviceScaleFactor: 3,
18
+ userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1',
19
+ },
20
+ };
21
+ export class BrowserNotInstalledError extends Error {
22
+ constructor(cause) {
23
+ super('ghost-qa could not launch Chromium. Playwright browsers are not installed yet.\n' +
24
+ 'Fix (one-time): npx playwright install chromium\n' +
25
+ (cause instanceof Error ? `Underlying error: ${cause.message}` : ''));
26
+ this.name = 'BrowserNotInstalledError';
27
+ }
28
+ }
29
+ /**
30
+ * Owns the headless Chromium lifecycle: one browser, one isolated context per
31
+ * viewport profile, pages created on demand. Always `close()` (or use
32
+ * `await using`-style try/finally) — leaked browsers hang CI.
33
+ */
34
+ export class BrowserSession {
35
+ browser = null;
36
+ contexts = new Map();
37
+ headless;
38
+ constructor(headless = true) {
39
+ this.headless = headless;
40
+ }
41
+ async launch() {
42
+ if (this.browser)
43
+ return;
44
+ // GHOST_QA_CHROMIUM_PATH lets CI / serverless environments point ghost-qa
45
+ // at a pre-provisioned Chromium (e.g. @sparticuz/chromium) instead of the
46
+ // Playwright-managed download.
47
+ const executablePath = process.env.GHOST_QA_CHROMIUM_PATH;
48
+ try {
49
+ this.browser = await chromium.launch({
50
+ headless: this.headless,
51
+ ...(executablePath ? { executablePath } : {}),
52
+ });
53
+ }
54
+ catch (error) {
55
+ throw new BrowserNotInstalledError(error);
56
+ }
57
+ }
58
+ get isLaunched() {
59
+ return this.browser !== null;
60
+ }
61
+ async contextFor(viewport) {
62
+ if (!this.browser)
63
+ await this.launch();
64
+ const existing = this.contexts.get(viewport);
65
+ if (existing)
66
+ return existing;
67
+ const profile = VIEWPORT_PROFILES[viewport];
68
+ const context = await this.browser.newContext({
69
+ viewport: { width: profile.width, height: profile.height },
70
+ isMobile: profile.isMobile,
71
+ hasTouch: profile.hasTouch,
72
+ deviceScaleFactor: profile.deviceScaleFactor,
73
+ ...(profile.userAgent ? { userAgent: profile.userAgent } : {}),
74
+ });
75
+ this.contexts.set(viewport, context);
76
+ return context;
77
+ }
78
+ async newPage(viewport) {
79
+ const context = await this.contextFor(viewport);
80
+ return context.newPage();
81
+ }
82
+ async close() {
83
+ for (const context of this.contexts.values()) {
84
+ await context.close().catch(() => undefined);
85
+ }
86
+ this.contexts.clear();
87
+ if (this.browser) {
88
+ await this.browser.close().catch(() => undefined);
89
+ this.browser = null;
90
+ }
91
+ }
92
+ }
93
+ /** Lightweight launch smoke-test used by `ghost-qa doctor`. */
94
+ export async function smokeTestBrowser() {
95
+ let session = null;
96
+ try {
97
+ session = new BrowserSession(true);
98
+ const page = await session.newPage('desktop');
99
+ await page.goto('about:blank');
100
+ const title = await page.evaluate(() => document.title);
101
+ void title;
102
+ return { ok: true, detail: 'Chromium launched and rendered about:blank successfully.' };
103
+ }
104
+ catch (error) {
105
+ return {
106
+ ok: false,
107
+ detail: error instanceof Error ? error.message : String(error),
108
+ };
109
+ }
110
+ finally {
111
+ await session?.close().catch(() => undefined);
112
+ }
113
+ }
114
+ //# sourceMappingURL=browser-session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-session.js","sourceRoot":"","sources":["../../../src/engine/browser-session.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAgD,MAAM,YAAY,CAAC;AAapF,MAAM,CAAC,MAAM,iBAAiB,GAA0C;IACtE,OAAO,EAAE;QACP,IAAI,EAAE,SAAS;QACf,KAAK,EAAE,IAAI;QACX,MAAM,EAAE,GAAG;QACX,QAAQ,EAAE,KAAK;QACf,QAAQ,EAAE,KAAK;QACf,iBAAiB,EAAE,CAAC;KACrB;IACD,MAAM,EAAE;QACN,IAAI,EAAE,QAAQ;QACd,KAAK,EAAE,GAAG;QACV,MAAM,EAAE,GAAG;QACX,QAAQ,EAAE,IAAI;QACd,QAAQ,EAAE,IAAI;QACd,iBAAiB,EAAE,CAAC;QACpB,SAAS,EACP,yIAAyI;KAC5I;CACF,CAAC;AAEF,MAAM,OAAO,wBAAyB,SAAQ,KAAK;IACjD,YAAY,KAAe;QACzB,KAAK,CACH,kFAAkF;YAChF,oDAAoD;YACpD,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,qBAAqB,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CACvE,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,0BAA0B,CAAC;IACzC,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,OAAO,cAAc;IACjB,OAAO,GAAmB,IAAI,CAAC;IACtB,QAAQ,GAAG,IAAI,GAAG,EAAgC,CAAC;IACnD,QAAQ,CAAU;IAEnC,YAAY,QAAQ,GAAG,IAAI;QACzB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,CAAC;IAED,KAAK,CAAC,MAAM;QACV,IAAI,IAAI,CAAC,OAAO;YAAE,OAAO;QACzB,0EAA0E;QAC1E,0EAA0E;QAC1E,+BAA+B;QAC/B,MAAM,cAAc,GAAG,OAAO,CAAC,GAAG,CAAC,sBAAsB,CAAC;QAC1D,IAAI,CAAC;YACH,IAAI,CAAC,OAAO,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC;gBACnC,QAAQ,EAAE,IAAI,CAAC,QAAQ;gBACvB,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC9C,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,wBAAwB,CAAC,KAAK,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC;IAED,IAAI,UAAU;QACZ,OAAO,IAAI,CAAC,OAAO,KAAK,IAAI,CAAC;IAC/B,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,QAAsB;QACrC,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC;QACvC,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC7C,IAAI,QAAQ;YAAE,OAAO,QAAQ,CAAC;QAE9B,MAAM,OAAO,GAAG,iBAAiB,CAAC,QAAQ,CAAC,CAAC;QAC5C,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,OAAQ,CAAC,UAAU,CAAC;YAC7C,QAAQ,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE;YAC1D,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,iBAAiB,EAAE,OAAO,CAAC,iBAAiB;YAC5C,GAAG,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC/D,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QACrC,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,QAAsB;QAClC,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;QAChD,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;IAC3B,CAAC;IAED,KAAK,CAAC,KAAK;QACT,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC;YAC7C,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QAC/C,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;QACtB,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,MAAM,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;YAClD,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACtB,CAAC;IACH,CAAC;CACF;AAED,+DAA+D;AAC/D,MAAM,CAAC,KAAK,UAAU,gBAAgB;IACpC,IAAI,OAAO,GAA0B,IAAI,CAAC;IAC1C,IAAI,CAAC;QACH,OAAO,GAAG,IAAI,cAAc,CAAC,IAAI,CAAC,CAAC;QACnC,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QAC9C,MAAM,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QAC/B,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;QACxD,KAAK,KAAK,CAAC;QACX,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,0DAA0D,EAAE,CAAC;IAC1F,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO;YACL,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;SAC/D,CAAC;IACJ,CAAC;YAAS,CAAC;QACT,MAAM,OAAO,EAAE,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;IAChD,CAAC;AACH,CAAC"}
@@ -0,0 +1,41 @@
1
+ import type { Page } from 'playwright';
2
+ import { type InteractionAction, type QAIssue, type ViewportName } from '../types.js';
3
+ /**
4
+ * True when an anchor points at a different route than the page under test.
5
+ * The crawler already covers those routes with their own traps attached.
6
+ */
7
+ export declare function isOtherRouteLink(href: string, currentRoute: string, currentUrl: string): boolean;
8
+ export declare function isDestructiveLabel(label: string): boolean;
9
+ /** Pure safety gate for a candidate interactive element. */
10
+ export declare function isSafeInteraction(input: {
11
+ label: string;
12
+ href?: string | null;
13
+ type?: string | null;
14
+ disabled?: boolean;
15
+ }): {
16
+ safe: boolean;
17
+ reason?: string;
18
+ };
19
+ export interface InteractionRunResult {
20
+ actions: InteractionAction[];
21
+ issues: QAIssue[];
22
+ /** Hrefs visible after interactions (modals/menus may reveal new routes). */
23
+ discoveredHrefs: string[];
24
+ }
25
+ /**
26
+ * Safely exercises the interactive surface of one loaded page:
27
+ * fills form fields with inert test data, clicks non-destructive buttons,
28
+ * opens tabs and modals, then closes dialogs / navigates back so the page is
29
+ * left roughly as it was found. Never submits destructive flows.
30
+ */
31
+ export declare class InteractionRunner {
32
+ private readonly maxInteractions;
33
+ private readonly settleMs;
34
+ constructor(maxInteractions?: number, settleMs?: number);
35
+ run(page: Page, route: string, viewport: ViewportName): Promise<InteractionRunResult>;
36
+ private fillField;
37
+ private clickElement;
38
+ /** Close any dialog we opened and return from accidental navigations. */
39
+ private recover;
40
+ }
41
+ //# sourceMappingURL=interaction-runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interaction-runner.d.ts","sourceRoot":"","sources":["../../../src/engine/interaction-runner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAa,KAAK,iBAAiB,EAAE,KAAK,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAkBjG;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAShG;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED,4DAA4D;AAC5D,wBAAgB,iBAAiB,CAAC,KAAK,EAAE;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAoBrC;AAkED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,iBAAiB,EAAE,CAAC;IAC7B,MAAM,EAAE,OAAO,EAAE,CAAC;IAClB,6EAA6E;IAC7E,eAAe,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED;;;;;GAKG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;gBAEtB,eAAe,SAAK,EAAE,QAAQ,SAAM;IAK1C,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,GAAG,OAAO,CAAC,oBAAoB,CAAC;YA0G7E,SAAS;YAmCT,YAAY;IAO1B,yEAAyE;YAC3D,OAAO;CActB"}
@@ -0,0 +1,274 @@
1
+ import { makeIssue } from '../types.js';
2
+ /**
3
+ * Labels whose click could mutate real data, spend money, end a session or
4
+ * leave the test environment. The runner never clicks these — ghost-qa tests
5
+ * locally, but habits formed here should be safe anywhere.
6
+ */
7
+ const DESTRUCTIVE_PATTERNS = [
8
+ /\b(delete|remove|destroy|erase|wipe|purge|drop)\b/i,
9
+ /\b(checkout|pay(ment)?|purchase|buy now|place order|subscribe|donate)\b/i,
10
+ /\b(log ?out|sign ?out|sign ?in|log ?in|register|sign ?up)\b/i,
11
+ /\b(send|submit order|confirm order|publish|deploy|release)\b/i,
12
+ /\b(unsubscribe|close account|deactivate)\b/i,
13
+ /\b(github|google|apple|facebook|twitter|x\.com)\b.*\b(continue|login|sign)/i,
14
+ ];
15
+ const EXTERNAL_HREF = /^https?:\/\//i;
16
+ /**
17
+ * True when an anchor points at a different route than the page under test.
18
+ * The crawler already covers those routes with their own traps attached.
19
+ */
20
+ export function isOtherRouteLink(href, currentRoute, currentUrl) {
21
+ try {
22
+ const target = new URL(href, currentUrl);
23
+ const targetRoute = (target.pathname || '/') + target.search;
24
+ const normalizedCurrent = currentRoute === '' ? '/' : currentRoute;
25
+ return targetRoute !== normalizedCurrent;
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
31
+ export function isDestructiveLabel(label) {
32
+ return DESTRUCTIVE_PATTERNS.some((p) => p.test(label));
33
+ }
34
+ /** Pure safety gate for a candidate interactive element. */
35
+ export function isSafeInteraction(input) {
36
+ if (input.disabled)
37
+ return { safe: false, reason: 'element is disabled' };
38
+ const label = (input.label ?? '').trim();
39
+ if (isDestructiveLabel(label)) {
40
+ return { safe: false, reason: `destructive label blocked: "${label.slice(0, 60)}"` };
41
+ }
42
+ if (input.type === 'file')
43
+ return { safe: false, reason: 'file inputs are never exercised' };
44
+ if (input.href && EXTERNAL_HREF.test(input.href)) {
45
+ try {
46
+ // External navigations are exercised only by the crawler's origin check
47
+ // upstream; the interaction runner stays on-page.
48
+ const url = new URL(input.href);
49
+ if (!['localhost', '127.0.0.1', '::1'].includes(url.hostname)) {
50
+ return { safe: false, reason: 'external link not exercised' };
51
+ }
52
+ }
53
+ catch {
54
+ return { safe: false, reason: 'unparseable href' };
55
+ }
56
+ }
57
+ return { safe: true };
58
+ }
59
+ function collectCandidates() {
60
+ const describe = (el) => {
61
+ const tag = el.tagName.toLowerCase();
62
+ const id = el.id ? `#${el.id}` : '';
63
+ const role = el.getAttribute('role');
64
+ return `${tag}${id}${role ? `[role=${role}]` : ''}`;
65
+ };
66
+ const labelOf = (el) => (el.getAttribute('aria-label') ||
67
+ el.innerText ||
68
+ el.getAttribute('value') ||
69
+ el.getAttribute('placeholder') ||
70
+ '')
71
+ .replace(/\s+/g, ' ')
72
+ .trim()
73
+ .slice(0, 80);
74
+ const results = [];
75
+ const seen = new Set();
76
+ const push = (el, kind) => {
77
+ if (seen.has(el) || results.length >= 60)
78
+ return;
79
+ seen.add(el);
80
+ // Tag the live node so the runner can re-locate this exact element with
81
+ // a Playwright locator, regardless of how the DOM re-renders afterwards.
82
+ el.setAttribute('data-ghost-qa-target', String(results.length));
83
+ const style = window.getComputedStyle(el);
84
+ if (style.display === 'none' || style.visibility === 'hidden')
85
+ return;
86
+ results.push({
87
+ index: results.length,
88
+ descriptor: describe(el),
89
+ label: labelOf(el),
90
+ kind,
91
+ href: el instanceof HTMLAnchorElement ? el.href : null,
92
+ inputType: el instanceof HTMLInputElement ? el.type : null,
93
+ disabled: el.disabled === true || el.getAttribute('aria-disabled') === 'true',
94
+ });
95
+ };
96
+ document
97
+ .querySelectorAll('input:not([type="hidden"]), textarea, select')
98
+ .forEach((el) => push(el, 'fill'));
99
+ document
100
+ .querySelectorAll('[role="tab"], [data-toggle="tab"], .tab')
101
+ .forEach((el) => push(el, 'tab'));
102
+ document
103
+ .querySelectorAll('button, [role="button"], a[href], summary, [data-toggle="modal"]')
104
+ .forEach((el) => push(el, el.tagName.toLowerCase() === 'a' ? 'click' : 'click'));
105
+ return results;
106
+ }
107
+ /**
108
+ * Safely exercises the interactive surface of one loaded page:
109
+ * fills form fields with inert test data, clicks non-destructive buttons,
110
+ * opens tabs and modals, then closes dialogs / navigates back so the page is
111
+ * left roughly as it was found. Never submits destructive flows.
112
+ */
113
+ export class InteractionRunner {
114
+ maxInteractions;
115
+ settleMs;
116
+ constructor(maxInteractions = 12, settleMs = 300) {
117
+ this.maxInteractions = maxInteractions;
118
+ this.settleMs = settleMs;
119
+ }
120
+ async run(page, route, viewport) {
121
+ const actions = [];
122
+ const issues = [];
123
+ const startUrl = page.url();
124
+ const attempted = new Set();
125
+ const candidateKey = (c) => [c.kind, c.descriptor, c.label, c.href ?? '', c.inputType ?? ''].join('|');
126
+ // Candidates are re-collected before every action: clicks can navigate,
127
+ // re-render or open dialogs, which invalidates both element handles and
128
+ // the data-ghost-qa-target tags from the previous snapshot.
129
+ for (let step = 0; step < this.maxInteractions * 3; step++) {
130
+ if (actions.filter((a) => a.outcome !== 'skipped').length >= this.maxInteractions)
131
+ break;
132
+ let candidates = [];
133
+ try {
134
+ candidates = await page.evaluate(collectCandidates);
135
+ }
136
+ catch {
137
+ break;
138
+ }
139
+ const candidate = candidates.find((c) => !attempted.has(candidateKey(c)));
140
+ if (!candidate)
141
+ break;
142
+ attempted.add(candidateKey(candidate));
143
+ const target = candidate.label
144
+ ? `${candidate.descriptor} "${candidate.label}"`
145
+ : candidate.descriptor;
146
+ // Route links are the crawler's job — clicking them here would load a
147
+ // different page under this route's traps and pollute its report.
148
+ if (candidate.href && isOtherRouteLink(candidate.href, route, startUrl)) {
149
+ actions.push({
150
+ kind: candidate.kind,
151
+ target,
152
+ route,
153
+ viewport,
154
+ outcome: 'skipped',
155
+ reason: 'route link covered by the crawler',
156
+ });
157
+ continue;
158
+ }
159
+ const safety = isSafeInteraction({
160
+ label: candidate.label,
161
+ href: candidate.href,
162
+ type: candidate.inputType,
163
+ disabled: candidate.disabled,
164
+ });
165
+ if (!safety.safe) {
166
+ actions.push({
167
+ kind: candidate.kind,
168
+ target,
169
+ route,
170
+ viewport,
171
+ outcome: 'skipped',
172
+ ...(safety.reason ? { reason: safety.reason } : {}),
173
+ });
174
+ continue;
175
+ }
176
+ try {
177
+ if (candidate.kind === 'fill') {
178
+ await this.fillField(page, candidate);
179
+ }
180
+ else {
181
+ await this.clickElement(page, candidate);
182
+ }
183
+ actions.push({ kind: candidate.kind, target, route, viewport, outcome: 'passed' });
184
+ }
185
+ catch (error) {
186
+ const message = error instanceof Error ? error.message : String(error);
187
+ actions.push({
188
+ kind: candidate.kind,
189
+ target,
190
+ route,
191
+ viewport,
192
+ outcome: 'failed',
193
+ reason: message.slice(0, 200),
194
+ });
195
+ issues.push(makeIssue({
196
+ category: 'interaction',
197
+ severity: 'major',
198
+ type: 'interaction-failed',
199
+ message: `Interaction failed on ${target}: ${message.replace(/\s+/g, ' ').slice(0, 240)}`,
200
+ route,
201
+ viewport,
202
+ details: { target },
203
+ }));
204
+ }
205
+ await page.waitForTimeout(this.settleMs).catch(() => undefined);
206
+ await this.recover(page, startUrl);
207
+ }
208
+ let discoveredHrefs = [];
209
+ try {
210
+ discoveredHrefs = await page.evaluate(() => Array.from(document.querySelectorAll('a[href]')).map((a) => a.href));
211
+ }
212
+ catch {
213
+ /* page navigated away mid-run */
214
+ }
215
+ return { actions, issues, discoveredHrefs };
216
+ }
217
+ async fillField(page, candidate) {
218
+ const locator = page.locator(`[data-ghost-qa-target="${candidate.index}"]`).first();
219
+ const tag = await locator.evaluate((el) => el.tagName.toLowerCase()).catch(() => '');
220
+ if (tag === 'select') {
221
+ const value = await locator
222
+ .evaluate((el) => {
223
+ const select = el;
224
+ const option = Array.from(select.options).find((o) => o.value);
225
+ return option?.value ?? null;
226
+ })
227
+ .catch(() => null);
228
+ if (value)
229
+ await locator.selectOption(value, { timeout: 2000 });
230
+ return;
231
+ }
232
+ const type = candidate.inputType ?? 'text';
233
+ if (['checkbox', 'radio'].includes(type)) {
234
+ await locator.click({ timeout: 2000 });
235
+ return;
236
+ }
237
+ if (['password', 'file'].includes(type))
238
+ return;
239
+ const sample = type === 'email'
240
+ ? 'ghost-qa@example.com'
241
+ : type === 'number'
242
+ ? '42'
243
+ : type === 'tel'
244
+ ? '5550100'
245
+ : type === 'url'
246
+ ? 'https://example.com'
247
+ : type === 'date'
248
+ ? '2026-01-15'
249
+ : 'ghost-qa test';
250
+ await locator.fill(sample, { timeout: 2000 });
251
+ }
252
+ async clickElement(page, candidate) {
253
+ await page
254
+ .locator(`[data-ghost-qa-target="${candidate.index}"]`)
255
+ .first()
256
+ .click({ timeout: 2500 });
257
+ }
258
+ /** Close any dialog we opened and return from accidental navigations. */
259
+ async recover(page, startUrl) {
260
+ await page.keyboard.press('Escape').catch(() => undefined);
261
+ const closeButton = page
262
+ .locator('[role="dialog"] button[aria-label*="close" i], [role="dialog"] .close, dialog button')
263
+ .first();
264
+ if (await closeButton.isVisible().catch(() => false)) {
265
+ await closeButton.click({ timeout: 1000 }).catch(() => undefined);
266
+ }
267
+ if (page.url() !== startUrl) {
268
+ await page.goBack({ timeout: 5000 }).catch(async () => {
269
+ await page.goto(startUrl, { timeout: 5000 }).catch(() => undefined);
270
+ });
271
+ }
272
+ }
273
+ }
274
+ //# sourceMappingURL=interaction-runner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interaction-runner.js","sourceRoot":"","sources":["../../../src/engine/interaction-runner.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ import type { Page } from 'playwright';
2
+ /**
3
+ * Normalises a URL for de-duplication: strips the hash and trailing slash,
4
+ * keeps the query string (query-driven views are distinct routes).
5
+ * Returns `null` for unparseable input.
6
+ */
7
+ export declare function normalizeUrl(raw: string, base?: string): string | null;
8
+ export declare function isSameOrigin(candidate: string, origin: string): boolean;
9
+ /** True when an href points at a crawlable, same-origin application route. */
10
+ export declare function isCrawlableHref(href: string, origin: string): boolean;
11
+ export declare function pathOf(url: string): string;
12
+ export interface DiscoveredRoute {
13
+ url: string;
14
+ path: string;
15
+ depth: number;
16
+ }
17
+ /**
18
+ * Breadth-first route discovery. The `visit` callback loads a URL (with all
19
+ * traps attached) and returns the raw hrefs found on it; the crawler owns the
20
+ * queue, depth limit, route cap and de-duplication so traversal policy is
21
+ * testable without a browser.
22
+ */
23
+ export declare class RouteCrawler {
24
+ private readonly maxDepth;
25
+ private readonly maxRoutes;
26
+ constructor(maxDepth?: number, maxRoutes?: number);
27
+ crawl(startUrl: string, visit: (route: DiscoveredRoute) => Promise<string[]>): Promise<DiscoveredRoute[]>;
28
+ /** Extracts every anchor href currently in the DOM of a loaded page. */
29
+ static extractLinks(page: Page): Promise<string[]>;
30
+ }
31
+ //# sourceMappingURL=route-crawler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-crawler.d.ts","sourceRoot":"","sources":["../../../src/engine/route-crawler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAMvC;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAWtE;AAED,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAMvE;AAED,8EAA8E;AAC9E,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAUrE;AAED,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAO1C;AAED,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;GAKG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;gBAEvB,QAAQ,SAAI,EAAE,SAAS,SAAK;IAKlC,KAAK,CACT,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,GACnD,OAAO,CAAC,eAAe,EAAE,CAAC;IA6B7B,wEAAwE;WAC3D,YAAY,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;CAWzD"}
@@ -0,0 +1,110 @@
1
+ /** Extensions and protocols that are never crawlable application routes. */
2
+ const SKIP_EXTENSIONS = /\.(?:png|jpe?g|gif|svg|webp|avif|ico|css|js|mjs|map|json|xml|txt|pdf|zip|gz|woff2?|ttf|otf|mp4|webm|mp3|wav)$/i;
3
+ const SKIP_PREFIXES = ['mailto:', 'tel:', 'javascript:', 'data:', 'blob:', '#'];
4
+ /**
5
+ * Normalises a URL for de-duplication: strips the hash and trailing slash,
6
+ * keeps the query string (query-driven views are distinct routes).
7
+ * Returns `null` for unparseable input.
8
+ */
9
+ export function normalizeUrl(raw, base) {
10
+ try {
11
+ const url = base ? new URL(raw, base) : new URL(raw);
12
+ url.hash = '';
13
+ if (url.pathname.length > 1 && url.pathname.endsWith('/')) {
14
+ url.pathname = url.pathname.replace(/\/+$/, '');
15
+ }
16
+ return url.toString();
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
22
+ export function isSameOrigin(candidate, origin) {
23
+ try {
24
+ return new URL(candidate).origin === new URL(origin).origin;
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
30
+ /** True when an href points at a crawlable, same-origin application route. */
31
+ export function isCrawlableHref(href, origin) {
32
+ if (!href)
33
+ return false;
34
+ const trimmed = href.trim();
35
+ if (SKIP_PREFIXES.some((p) => trimmed.toLowerCase().startsWith(p)))
36
+ return false;
37
+ const normalized = normalizeUrl(trimmed, origin);
38
+ if (!normalized)
39
+ return false;
40
+ if (!/^https?:$/.test(new URL(normalized).protocol))
41
+ return false;
42
+ if (!isSameOrigin(normalized, origin))
43
+ return false;
44
+ if (SKIP_EXTENSIONS.test(new URL(normalized).pathname))
45
+ return false;
46
+ return true;
47
+ }
48
+ export function pathOf(url) {
49
+ try {
50
+ const parsed = new URL(url);
51
+ return (parsed.pathname || '/') + parsed.search;
52
+ }
53
+ catch {
54
+ return url;
55
+ }
56
+ }
57
+ /**
58
+ * Breadth-first route discovery. The `visit` callback loads a URL (with all
59
+ * traps attached) and returns the raw hrefs found on it; the crawler owns the
60
+ * queue, depth limit, route cap and de-duplication so traversal policy is
61
+ * testable without a browser.
62
+ */
63
+ export class RouteCrawler {
64
+ maxDepth;
65
+ maxRoutes;
66
+ constructor(maxDepth = 2, maxRoutes = 25) {
67
+ this.maxDepth = maxDepth;
68
+ this.maxRoutes = maxRoutes;
69
+ }
70
+ async crawl(startUrl, visit) {
71
+ const start = normalizeUrl(startUrl);
72
+ if (!start)
73
+ throw new Error(`Invalid target URL: ${startUrl}`);
74
+ const origin = new URL(start).origin;
75
+ const visited = new Set();
76
+ const completed = [];
77
+ const queue = [{ url: start, path: pathOf(start), depth: 0 }];
78
+ while (queue.length > 0 && completed.length < this.maxRoutes) {
79
+ const route = queue.shift();
80
+ if (visited.has(route.url))
81
+ continue;
82
+ visited.add(route.url);
83
+ completed.push(route);
84
+ const hrefs = await visit(route);
85
+ if (route.depth >= this.maxDepth)
86
+ continue;
87
+ for (const href of hrefs) {
88
+ if (!isCrawlableHref(href, origin))
89
+ continue;
90
+ const normalized = normalizeUrl(href, origin);
91
+ if (visited.has(normalized) || queue.some((q) => q.url === normalized))
92
+ continue;
93
+ if (completed.length + queue.length >= this.maxRoutes)
94
+ break;
95
+ queue.push({ url: normalized, path: pathOf(normalized), depth: route.depth + 1 });
96
+ }
97
+ }
98
+ return completed;
99
+ }
100
+ /** Extracts every anchor href currently in the DOM of a loaded page. */
101
+ static async extractLinks(page) {
102
+ try {
103
+ return await page.evaluate(() => Array.from(document.querySelectorAll('a[href]')).map((a) => a.href));
104
+ }
105
+ catch {
106
+ return [];
107
+ }
108
+ }
109
+ }
110
+ //# sourceMappingURL=route-crawler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-crawler.js","sourceRoot":"","sources":["../../../src/engine/route-crawler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ /** Polls a URL until it answers with any HTTP status (even 404 = server up). */
2
+ export declare function waitForUrl(url: string, timeoutMs?: number, pollIntervalMs?: number): Promise<void>;
3
+ export interface ManagedServerOptions {
4
+ /** Working directory for the command (default: process.cwd()). */
5
+ cwd?: string;
6
+ /** Extra environment variables merged over process.env. */
7
+ env?: Record<string, string>;
8
+ /** How many characters of server output to keep for error reporting. */
9
+ outputBufferChars?: number;
10
+ }
11
+ /**
12
+ * Owns a spawned dev-server process tree: `start()` spawns it through the
13
+ * platform shell in its own process group, `waitUntilReady()` polls the
14
+ * target URL, and `stop()` terminates the whole tree (SIGTERM, then SIGKILL)
15
+ * so no orphaned dev servers survive a ghost-qa run.
16
+ */
17
+ export declare class ManagedDevServer {
18
+ private child;
19
+ private output;
20
+ readonly command: string;
21
+ private readonly options;
22
+ constructor(command: string, options?: ManagedServerOptions);
23
+ get isRunning(): boolean;
24
+ get pid(): number | undefined;
25
+ /** Last chunk of captured stdout+stderr (for "server failed to start" errors). */
26
+ get outputTail(): string;
27
+ start(): void;
28
+ /** Resolves once the URL responds; rejects with the server output on timeout/early exit. */
29
+ waitUntilReady(url: string, timeoutMs?: number): Promise<void>;
30
+ /** Terminates the spawned process tree. Safe to call multiple times. */
31
+ stop(graceMs?: number): Promise<void>;
32
+ private signalTree;
33
+ }
34
+ /**
35
+ * Convenience wrapper: spawn `command`, wait for `url`, run `fn`, and always
36
+ * tear the server down afterwards — even when `fn` throws.
37
+ */
38
+ export declare function withManagedDevServer<T>(command: string, url: string, fn: () => Promise<T>, options?: ManagedServerOptions & {
39
+ readyTimeoutMs?: number;
40
+ }): Promise<T>;
41
+ //# sourceMappingURL=server-lifecycle.d.ts.map