ghost-qa 0.0.0-stage → 1.3.0

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 +91 -6
  73. package/scripts/prepare-build.mjs +40 -0
@@ -0,0 +1,151 @@
1
+ import { makeIssue } from '../types.js';
2
+ const HYDRATION_PATTERNS = [
3
+ /hydration/i,
4
+ /did not match/i,
5
+ /server rendered (html|markup)/i,
6
+ /text content does not match/i,
7
+ /a tree hydrated but some attributes/i,
8
+ /prop `[^`]+` did not match/i,
9
+ ];
10
+ const REJECTION_PATTERNS = [
11
+ /unhandled ?(promise ?)?rejection/i,
12
+ /unhandledrejection/i,
13
+ /promise rejection/i,
14
+ /uncaught \(in promise\)/i,
15
+ ];
16
+ /** Noise that ships in almost every dev server and tells the agent nothing. */
17
+ const BENIGN_PATTERNS = [
18
+ /download the react devtools/i,
19
+ /\[vite\]/i,
20
+ /\[hmr\]/i,
21
+ /hot module replacement/i,
22
+ /favicon/i,
23
+ /devtools/i,
24
+ // Chromium's generic resource-load console line duplicates the Network
25
+ // Trap's precise per-request issue (status + URL), so it adds only noise.
26
+ /failed to load resource: the server responded with a status of/i,
27
+ ];
28
+ export function isHydrationError(text) {
29
+ return HYDRATION_PATTERNS.some((p) => p.test(text));
30
+ }
31
+ export function isUnhandledRejection(text) {
32
+ return REJECTION_PATTERNS.some((p) => p.test(text));
33
+ }
34
+ export function isBenignConsoleNoise(text) {
35
+ return BENIGN_PATTERNS.some((p) => p.test(text));
36
+ }
37
+ /**
38
+ * Pure classifier — maps a raw console/page-error string to a trap verdict.
39
+ * Kept side-effect free so it is fully unit-testable without a browser.
40
+ */
41
+ export function classifyConsoleText(text, source = 'console-error') {
42
+ if (isBenignConsoleNoise(text))
43
+ return 'benign';
44
+ if (isHydrationError(text))
45
+ return 'hydration-mismatch';
46
+ if (isUnhandledRejection(text))
47
+ return 'unhandled-rejection';
48
+ if (source === 'pageerror')
49
+ return 'page-error';
50
+ if (source === 'console-warning')
51
+ return 'console-warning';
52
+ return 'console-error';
53
+ }
54
+ export function consoleIssueFromText(text, route, viewport, source = 'console-error', stack) {
55
+ const kind = classifyConsoleText(text, source);
56
+ if (kind === 'benign')
57
+ return null;
58
+ const clean = text.replace(/\s+/g, ' ').trim().slice(0, 500);
59
+ switch (kind) {
60
+ case 'hydration-mismatch':
61
+ return makeIssue({
62
+ category: 'console',
63
+ severity: 'critical',
64
+ type: 'hydration-mismatch',
65
+ message: `React hydration mismatch: ${clean}`,
66
+ route,
67
+ viewport,
68
+ ...(stack ? { stack: stack.slice(0, 2000) } : {}),
69
+ });
70
+ case 'unhandled-rejection':
71
+ return makeIssue({
72
+ category: 'console',
73
+ severity: 'critical',
74
+ type: 'unhandled-rejection',
75
+ message: `Unhandled promise rejection: ${clean}`,
76
+ route,
77
+ viewport,
78
+ ...(stack ? { stack: stack.slice(0, 2000) } : {}),
79
+ });
80
+ case 'page-error':
81
+ return makeIssue({
82
+ category: 'console',
83
+ severity: 'critical',
84
+ type: 'page-error',
85
+ message: `Uncaught page error: ${clean}`,
86
+ route,
87
+ viewport,
88
+ ...(stack ? { stack: stack.slice(0, 2000) } : {}),
89
+ });
90
+ case 'console-warning':
91
+ return makeIssue({
92
+ category: 'console',
93
+ severity: 'minor',
94
+ type: 'console-warning',
95
+ message: `Console warning: ${clean}`,
96
+ route,
97
+ viewport,
98
+ });
99
+ default:
100
+ return makeIssue({
101
+ category: 'console',
102
+ severity: 'major',
103
+ type: 'console-error',
104
+ message: `Console error: ${clean}`,
105
+ route,
106
+ viewport,
107
+ ...(stack ? { stack: stack.slice(0, 2000) } : {}),
108
+ });
109
+ }
110
+ }
111
+ /**
112
+ * Attaches `pageerror` + `console` listeners to a Playwright page and
113
+ * accumulates structured issues. One trap instance per page.
114
+ */
115
+ export class ConsoleTrap {
116
+ issues = [];
117
+ route = '/';
118
+ viewport;
119
+ detached = false;
120
+ constructor(viewport) {
121
+ this.viewport = viewport;
122
+ }
123
+ /** Tell the trap which route subsequent events belong to. */
124
+ setRoute(route) {
125
+ this.route = route;
126
+ }
127
+ attach(page) {
128
+ page.on('pageerror', (error) => {
129
+ if (this.detached)
130
+ return;
131
+ const issue = consoleIssueFromText(error.message || String(error), this.route, this.viewport, 'pageerror', error.stack);
132
+ if (issue)
133
+ this.issues.push(issue);
134
+ });
135
+ page.on('console', (message) => {
136
+ if (this.detached)
137
+ return;
138
+ const type = message.type();
139
+ if (type !== 'error' && type !== 'warning')
140
+ return;
141
+ const text = message.text();
142
+ const issue = consoleIssueFromText(text, this.route, this.viewport, type === 'error' ? 'console-error' : 'console-warning');
143
+ if (issue)
144
+ this.issues.push(issue);
145
+ });
146
+ }
147
+ detach() {
148
+ this.detached = true;
149
+ }
150
+ }
151
+ //# sourceMappingURL=console-trap.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"console-trap.js","sourceRoot":"","sources":["../../../src/traps/console-trap.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ import type { Page } from 'playwright';
2
+ import { type QAIssue, type ViewportName } from '../types.js';
3
+ export interface OffendingElement {
4
+ descriptor: string;
5
+ right: number;
6
+ width: number;
7
+ }
8
+ export interface LayoutMetrics {
9
+ innerWidth: number;
10
+ scrollWidth: number;
11
+ hasViewportMeta: boolean;
12
+ /** Interactive elements rendered at 0×0 (or effectively invisible). */
13
+ zeroSizeInteractives: string[];
14
+ /** Images that finished loading but decoded to nothing. */
15
+ brokenImages: string[];
16
+ /** Elements extending past the right edge of the viewport. */
17
+ overflowingElements: OffendingElement[];
18
+ }
19
+ export declare const EMPTY_METRICS: LayoutMetrics;
20
+ /**
21
+ * Pure evaluator: DOM geometry in, structured layout issues out.
22
+ * This is the heart of the Layout Trap and is fully unit-testable.
23
+ */
24
+ export declare function evaluateLayoutMetrics(metrics: LayoutMetrics, route: string, viewport: ViewportName): QAIssue[];
25
+ /** Runs the in-page probe against a live Playwright page and evaluates it. */
26
+ export declare class LayoutTrap {
27
+ private readonly viewport;
28
+ constructor(viewport: ViewportName);
29
+ collect(page: Page, route: string): Promise<QAIssue[]>;
30
+ }
31
+ //# sourceMappingURL=layout-trap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layout-trap.d.ts","sourceRoot":"","sources":["../../../src/traps/layout-trap.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAEvC,OAAO,EAAa,KAAK,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAEzE,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,OAAO,CAAC;IACzB,uEAAuE;IACvE,oBAAoB,EAAE,MAAM,EAAE,CAAC;IAC/B,2DAA2D;IAC3D,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,8DAA8D;IAC9D,mBAAmB,EAAE,gBAAgB,EAAE,CAAC;CACzC;AAED,eAAO,MAAM,aAAa,EAAE,aAO3B,CAAC;AAEF;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,aAAa,EACtB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,YAAY,GACrB,OAAO,EAAE,CA0EX;AA2DD,8EAA8E;AAC9E,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAe;gBAE5B,QAAQ,EAAE,YAAY;IAI5B,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;CAiB7D"}
@@ -0,0 +1,150 @@
1
+ import { VIEWPORT_PROFILES } from '../engine/browser-session.js';
2
+ import { makeIssue } from '../types.js';
3
+ export const EMPTY_METRICS = {
4
+ innerWidth: 0,
5
+ scrollWidth: 0,
6
+ hasViewportMeta: false,
7
+ zeroSizeInteractives: [],
8
+ brokenImages: [],
9
+ overflowingElements: [],
10
+ };
11
+ /**
12
+ * Pure evaluator: DOM geometry in, structured layout issues out.
13
+ * This is the heart of the Layout Trap and is fully unit-testable.
14
+ */
15
+ export function evaluateLayoutMetrics(metrics, route, viewport) {
16
+ const issues = [];
17
+ const tolerancePx = 1;
18
+ if (metrics.innerWidth > 0 && metrics.scrollWidth > metrics.innerWidth + tolerancePx) {
19
+ const overflowPx = metrics.scrollWidth - metrics.innerWidth;
20
+ const offenders = metrics.overflowingElements
21
+ .slice(0, 3)
22
+ .map((el) => el.descriptor)
23
+ .join(', ');
24
+ issues.push(makeIssue({
25
+ category: 'layout',
26
+ severity: viewport === 'mobile' ? 'critical' : 'major',
27
+ type: 'horizontal-overflow',
28
+ message: `Horizontal overflow on ${viewport}: page scrolls sideways by ${overflowPx}px ` +
29
+ `(scrollWidth ${metrics.scrollWidth} > viewport ${metrics.innerWidth}).` +
30
+ (offenders ? ` Likely culprits: ${offenders}.` : '') +
31
+ ' Common causes: 100vw widths, fixed-width children, negative margins, unshrunk flex/grid items.',
32
+ route,
33
+ viewport,
34
+ details: {
35
+ scrollWidth: metrics.scrollWidth,
36
+ innerWidth: metrics.innerWidth,
37
+ overflowPx,
38
+ },
39
+ }));
40
+ }
41
+ for (const descriptor of metrics.zeroSizeInteractives.slice(0, 5)) {
42
+ issues.push(makeIssue({
43
+ category: 'layout',
44
+ severity: 'major',
45
+ type: 'zero-size-interactive',
46
+ message: `Interactive element has zero size and cannot be tapped/clicked: ${descriptor}. Check for display/width/height CSS collapsing it or content that never rendered.`,
47
+ route,
48
+ viewport,
49
+ details: { element: descriptor },
50
+ }));
51
+ }
52
+ for (const src of metrics.brokenImages.slice(0, 5)) {
53
+ issues.push(makeIssue({
54
+ category: 'layout',
55
+ severity: 'major',
56
+ type: 'broken-image',
57
+ message: `Broken image (naturalWidth === 0): ${src}. Verify the asset path exists and is served with a 200.`,
58
+ route,
59
+ viewport,
60
+ details: { src },
61
+ }));
62
+ }
63
+ if (viewport === 'mobile' && !metrics.hasViewportMeta) {
64
+ issues.push(makeIssue({
65
+ category: 'layout',
66
+ severity: 'major',
67
+ type: 'missing-viewport-meta',
68
+ message: 'Missing <meta name="viewport"> — mobile browsers will render at a desktop width and scale down. Add <meta name="viewport" content="width=device-width, initial-scale=1">.',
69
+ route,
70
+ viewport,
71
+ }));
72
+ }
73
+ return issues;
74
+ }
75
+ /** In-page probe: measures live DOM geometry. Executed inside Chromium. */
76
+ function probeLayout() {
77
+ const describe = (el) => {
78
+ const tag = el.tagName.toLowerCase();
79
+ const id = el.id ? `#${el.id}` : '';
80
+ const cls = typeof el.className === 'string' && el.className.trim()
81
+ ? `.${el.className.trim().split(/\s+/).slice(0, 2).join('.')}`
82
+ : '';
83
+ const text = (el.textContent ?? '').replace(/\s+/g, ' ').trim().slice(0, 40);
84
+ return `${tag}${id}${cls}${text ? ` "${text}"` : ''}`;
85
+ };
86
+ const interactiveSelector = 'a[href], button, input, select, textarea, [role="button"], [role="tab"], [tabindex]';
87
+ const zeroSizeInteractives = [];
88
+ document.querySelectorAll(interactiveSelector).forEach((el) => {
89
+ const rect = el.getBoundingClientRect();
90
+ const style = window.getComputedStyle(el);
91
+ const intentionallyHidden = style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0';
92
+ if (!intentionallyHidden && (rect.width === 0 || rect.height === 0)) {
93
+ zeroSizeInteractives.push(describe(el));
94
+ }
95
+ });
96
+ const brokenImages = [];
97
+ document.querySelectorAll('img').forEach((img) => {
98
+ if (img.complete && img.naturalWidth === 0) {
99
+ brokenImages.push(img.currentSrc || img.src || img.alt || '(image without src)');
100
+ }
101
+ });
102
+ const overflowingElements = [];
103
+ const innerWidth = window.innerWidth;
104
+ document.querySelectorAll('body *').forEach((el) => {
105
+ if (overflowingElements.length >= 10)
106
+ return;
107
+ const rect = el.getBoundingClientRect();
108
+ if (rect.width > 0 && rect.right > innerWidth + 1) {
109
+ overflowingElements.push({
110
+ descriptor: describe(el),
111
+ right: Math.round(rect.right),
112
+ width: Math.round(rect.width),
113
+ });
114
+ }
115
+ });
116
+ return {
117
+ innerWidth,
118
+ scrollWidth: document.documentElement.scrollWidth,
119
+ hasViewportMeta: Boolean(document.querySelector('meta[name="viewport"]')),
120
+ zeroSizeInteractives,
121
+ brokenImages,
122
+ overflowingElements,
123
+ };
124
+ }
125
+ /** Runs the in-page probe against a live Playwright page and evaluates it. */
126
+ export class LayoutTrap {
127
+ viewport;
128
+ constructor(viewport) {
129
+ this.viewport = viewport;
130
+ }
131
+ async collect(page, route) {
132
+ let metrics;
133
+ try {
134
+ metrics = await page.evaluate(probeLayout);
135
+ }
136
+ catch {
137
+ return [];
138
+ }
139
+ // Judge overflow against the intended profile width, not just the live
140
+ // innerWidth: mobile emulation (and some headless builds) can let the
141
+ // layout viewport grow with wide content, which would mask exactly the
142
+ // overflow this trap exists to catch.
143
+ const expectedWidth = VIEWPORT_PROFILES[this.viewport].width;
144
+ if (metrics.innerWidth > expectedWidth) {
145
+ metrics = { ...metrics, innerWidth: expectedWidth };
146
+ }
147
+ return evaluateLayoutMetrics(metrics, route, this.viewport);
148
+ }
149
+ }
150
+ //# sourceMappingURL=layout-trap.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layout-trap.js","sourceRoot":"","sources":["../../../src/traps/layout-trap.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ import type { Page } from 'playwright';
2
+ import { type QAIssue, type ViewportName } from '../types.js';
3
+ export declare function isAssetUrl(url: string): boolean;
4
+ export declare function isCorsFailure(errorText: string): boolean;
5
+ /**
6
+ * Pure evaluator for an HTTP response. Returns an issue for status >= 400,
7
+ * or `null` for healthy responses. Unit-testable without a browser.
8
+ */
9
+ export declare function evaluateHttpResponse(status: number, url: string, route: string, viewport: ViewportName, resourceType?: string): QAIssue | null;
10
+ /** Pure evaluator for a request that never completed (DNS, CORS, abort…). */
11
+ export declare function evaluateRequestFailure(errorText: string, url: string, route: string, viewport: ViewportName, resourceType?: string): QAIssue | null;
12
+ export declare function shortUrl(url: string): string;
13
+ /** Attaches `response` + `requestfailed` listeners to a page. */
14
+ export declare class NetworkTrap {
15
+ readonly issues: QAIssue[];
16
+ private route;
17
+ private readonly viewport;
18
+ private detached;
19
+ constructor(viewport: ViewportName);
20
+ setRoute(route: string): void;
21
+ attach(page: Page): void;
22
+ detach(): void;
23
+ }
24
+ //# sourceMappingURL=network-trap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"network-trap.d.ts","sourceRoot":"","sources":["../../../src/traps/network-trap.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAa,KAAK,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAIzE,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAE/C;AAED,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAExD;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,YAAY,EACtB,YAAY,SAAa,GACxB,OAAO,GAAG,IAAI,CA0ChB;AAED,6EAA6E;AAC7E,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,YAAY,EACtB,YAAY,SAAQ,GACnB,OAAO,GAAG,IAAI,CA0BhB;AAED,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAQ5C;AAED,iEAAiE;AACjE,qBAAa,WAAW;IACtB,QAAQ,CAAC,MAAM,EAAE,OAAO,EAAE,CAAM;IAChC,OAAO,CAAC,KAAK,CAAO;IACpB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAe;IACxC,OAAO,CAAC,QAAQ,CAAS;gBAEb,QAAQ,EAAE,YAAY;IAIlC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAI7B,MAAM,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI;IAgCxB,MAAM,IAAI,IAAI;CAGf"}
@@ -0,0 +1,132 @@
1
+ import { makeIssue } from '../types.js';
2
+ const ASSET_EXTENSIONS = /\.(?:js|mjs|css|png|jpe?g|gif|svg|webp|avif|ico|woff2?|ttf|otf|map|json|wasm)(?:\?|$)/i;
3
+ export function isAssetUrl(url) {
4
+ return ASSET_EXTENSIONS.test(url);
5
+ }
6
+ export function isCorsFailure(errorText) {
7
+ return /cors|cross-origin|access-control|ERR_FAILED|net::ERR_BLOCKED_BY/i.test(errorText);
8
+ }
9
+ /**
10
+ * Pure evaluator for an HTTP response. Returns an issue for status >= 400,
11
+ * or `null` for healthy responses. Unit-testable without a browser.
12
+ */
13
+ export function evaluateHttpResponse(status, url, route, viewport, resourceType = 'document') {
14
+ if (status < 400)
15
+ return null;
16
+ const isFavicon = /favicon[^/]*$/i.test(url);
17
+ if (isFavicon) {
18
+ return makeIssue({
19
+ category: 'network',
20
+ severity: 'minor',
21
+ type: 'broken-asset',
22
+ message: `Missing favicon (HTTP ${status}): ${url}`,
23
+ route,
24
+ viewport,
25
+ url,
26
+ details: { status, resourceType },
27
+ });
28
+ }
29
+ const asset = isAssetUrl(url) || ['script', 'stylesheet', 'image', 'font'].includes(resourceType);
30
+ if (status >= 500) {
31
+ return makeIssue({
32
+ category: 'network',
33
+ severity: 'critical',
34
+ type: 'http-error',
35
+ message: `Server error HTTP ${status} for ${shortUrl(url)} — backend/API is crashing on this route.`,
36
+ route,
37
+ viewport,
38
+ url,
39
+ details: { status, resourceType },
40
+ });
41
+ }
42
+ return makeIssue({
43
+ category: 'network',
44
+ severity: asset ? 'major' : 'major',
45
+ type: asset ? 'broken-asset' : 'http-error',
46
+ message: asset
47
+ ? `Broken asset HTTP ${status}: ${shortUrl(url)} (resource failed to load)`
48
+ : `HTTP ${status} response for ${shortUrl(url)}`,
49
+ route,
50
+ viewport,
51
+ url,
52
+ details: { status, resourceType },
53
+ });
54
+ }
55
+ /** Pure evaluator for a request that never completed (DNS, CORS, abort…). */
56
+ export function evaluateRequestFailure(errorText, url, route, viewport, resourceType = 'xhr') {
57
+ // Aborted requests are usually the crawler navigating away — not a bug.
58
+ if (/ERR_ABORTED/i.test(errorText))
59
+ return null;
60
+ if (isCorsFailure(errorText)) {
61
+ return makeIssue({
62
+ category: 'network',
63
+ severity: 'critical',
64
+ type: 'cors-error',
65
+ message: `CORS / blocked request failed: ${shortUrl(url)} (${errorText}). Check API CORS headers and the fetch origin.`,
66
+ route,
67
+ viewport,
68
+ url,
69
+ details: { errorText, resourceType },
70
+ });
71
+ }
72
+ return makeIssue({
73
+ category: 'network',
74
+ severity: isAssetUrl(url) ? 'major' : 'critical',
75
+ type: 'request-failed',
76
+ message: `Request failed (${resourceType}): ${shortUrl(url)} — ${errorText}`,
77
+ route,
78
+ viewport,
79
+ url,
80
+ details: { errorText, resourceType },
81
+ });
82
+ }
83
+ export function shortUrl(url) {
84
+ try {
85
+ const parsed = new URL(url);
86
+ const path = parsed.pathname + parsed.search;
87
+ return path.length > 120 ? `${path.slice(0, 117)}…` : path || '/';
88
+ }
89
+ catch {
90
+ return url.slice(0, 120);
91
+ }
92
+ }
93
+ /** Attaches `response` + `requestfailed` listeners to a page. */
94
+ export class NetworkTrap {
95
+ issues = [];
96
+ route = '/';
97
+ viewport;
98
+ detached = false;
99
+ constructor(viewport) {
100
+ this.viewport = viewport;
101
+ }
102
+ setRoute(route) {
103
+ this.route = route;
104
+ }
105
+ attach(page) {
106
+ page.on('response', (response) => {
107
+ if (this.detached)
108
+ return;
109
+ try {
110
+ const request = response.request();
111
+ const issue = evaluateHttpResponse(response.status(), response.url(), this.route, this.viewport, request.resourceType());
112
+ if (issue)
113
+ this.issues.push(issue);
114
+ }
115
+ catch {
116
+ /* response already disposed — nothing actionable */
117
+ }
118
+ });
119
+ page.on('requestfailed', (request) => {
120
+ if (this.detached)
121
+ return;
122
+ const failure = request.failure();
123
+ const issue = evaluateRequestFailure(failure?.errorText ?? 'unknown network failure', request.url(), this.route, this.viewport, request.resourceType());
124
+ if (issue)
125
+ this.issues.push(issue);
126
+ });
127
+ }
128
+ detach() {
129
+ this.detached = true;
130
+ }
131
+ }
132
+ //# sourceMappingURL=network-trap.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"network-trap.js","sourceRoot":"","sources":["../../../src/traps/network-trap.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ import type { Page } from 'playwright';
2
+ import { type QAIssue, type ViewportName } from '../types.js';
3
+ export interface ResourceSize {
4
+ url: string;
5
+ transferBytes: number;
6
+ }
7
+ export interface PerfMetrics {
8
+ domContentLoadedMs: number;
9
+ loadMs: number;
10
+ resourceCount: number;
11
+ totalTransferBytes: number;
12
+ oversizedResources: ResourceSize[];
13
+ /** Scripts/stylesheets that block first paint (head, no async/defer/media tricks). */
14
+ renderBlockingCount: number;
15
+ domNodeCount: number;
16
+ /** Long Tasks API (Chromium): tasks > 50ms that freeze the main thread. */
17
+ longTaskCount: number;
18
+ longTaskTotalMs: number;
19
+ }
20
+ export declare const EMPTY_PERF_METRICS: PerfMetrics;
21
+ export declare const PERF_THRESHOLDS: {
22
+ readonly domContentLoadedMs: 3000;
23
+ readonly loadMs: 5000;
24
+ readonly singleResourceBytes: 1000000;
25
+ readonly totalTransferBytes: 3000000;
26
+ readonly renderBlockingCount: 5;
27
+ readonly domNodeCount: 1500;
28
+ readonly longTaskCount: 5;
29
+ readonly longTaskTotalMs: 500;
30
+ };
31
+ /**
32
+ * Pure evaluator: Navigation/Resource Timing facts in, perf issues out.
33
+ * Fully unit-testable without a browser.
34
+ */
35
+ export declare function evaluatePerfMetrics(metrics: PerfMetrics, route: string, viewport: ViewportName): QAIssue[];
36
+ /** Runs the in-page perf probe against a live page and evaluates it. */
37
+ export declare class PerfTrap {
38
+ private readonly viewport;
39
+ constructor(viewport: ViewportName);
40
+ collect(page: Page, route: string): Promise<QAIssue[]>;
41
+ }
42
+ //# sourceMappingURL=perf-trap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"perf-trap.d.ts","sourceRoot":"","sources":["../../../src/traps/perf-trap.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAa,KAAK,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAEzE,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,WAAW;IAC1B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,kBAAkB,EAAE,YAAY,EAAE,CAAC;IACnC,sFAAsF;IACtF,mBAAmB,EAAE,MAAM,CAAC;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,eAAO,MAAM,kBAAkB,EAAE,WAUhC,CAAC;AAEF,eAAO,MAAM,eAAe;;;;;;;;;CASlB,CAAC;AAKX;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,WAAW,EACpB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,YAAY,GACrB,OAAO,EAAE,CA8GX;AAqED,wEAAwE;AACxE,qBAAa,QAAQ;IACnB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAe;gBAE5B,QAAQ,EAAE,YAAY;IAI5B,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;CAS7D"}