@cementic/cementic-test 0.2.14 → 0.2.15

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.
package/README.md CHANGED
@@ -216,9 +216,9 @@ node --test --test-name-pattern="counting intent" test/prompt.spec.mjs
216
216
 
217
217
  Note: on current Node test runner versions, use `--test-reporter=spec`, not `--reporter=spec`.
218
218
 
219
- ### v0.2.14 acceptance criteria
219
+ ### v0.2.15 acceptance criteria
220
220
 
221
- All of the following must be true before publishing `v0.2.14`:
221
+ All of the following must be true before publishing `v0.2.15`:
222
222
 
223
223
  - `npm test` passes from a clean checkout and includes both the integration suite and `test/prompt.spec.mjs`
224
224
  - `prepublishOnly` runs the same full release gate, not just `build`
@@ -226,6 +226,8 @@ All of the following must be true before publishing `v0.2.14`:
226
226
  - counting intents generate `toHaveCount()` or `.count()`
227
227
  - intent text is never reused as `getByText(...)`
228
228
  - presence-only intents generate visibility assertions without click steps
229
+ - CTA-like presence intents can fall back from `button` to `link` selectors with `Locator.or(...)`
230
+ - counting button intents exclude CTA links that are really anchors
229
231
  - generated specs always use `await expect(...)`
230
232
  - auth/navigation intents generate `toHaveURL(...)`
231
233
  - negative intents generate `.not.` or `toBeHidden()`
@@ -236,6 +238,7 @@ All of the following must be true before publishing `v0.2.14`:
236
238
  - `ct tc url --ai` works in both modes:
237
239
  - with an LLM key, capture analysis remains intent-first and selector-safe
238
240
  - without an LLM key, deterministic capture-backed scenario generation still produces runnable, intent-aligned output
241
+ - capture semantics never classify `<a href="...">` as a button unless `role="button"` is explicit
239
242
  - `ct normalize` preserves:
240
243
  - `ct:url` metadata
241
244
  - selector hints
@@ -448,6 +451,13 @@ And start automating.
448
451
 
449
452
  ## Changelog
450
453
 
454
+ ### v0.2.15
455
+
456
+ - fixed capture semantics so `<a href="...">` elements are classified as links unless they explicitly expose `role="button"`
457
+ - made CTA-like presence selectors resilient by generating `Locator.or(...)` fallbacks across button and link roles when the intent is ambiguous
458
+ - tightened intent label cleanup so trailing filler phrases like `on the page` do not leak into generated locator text
459
+ - updated capture artifact metadata and capture user agent version strings to `0.2.15`
460
+
451
461
  ### v0.2.14
452
462
 
453
463
  - hardened the release gate so `npm test` now includes the prompt regression suite and `prepublishOnly` runs the full test gate
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ CaptureRuntimeError,
4
+ captureElements,
5
+ extractDomDataFromEnvironment,
6
+ formatCaptureFailure,
7
+ inferInteractiveRole,
8
+ toPageSummary
9
+ } from "./chunk-XUWFEWJZ.js";
10
+ export {
11
+ CaptureRuntimeError,
12
+ captureElements,
13
+ extractDomDataFromEnvironment,
14
+ formatCaptureFailure,
15
+ inferInteractiveRole,
16
+ toPageSummary
17
+ };
18
+ //# sourceMappingURL=capture.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,622 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/core/playwright.ts
4
+ import { existsSync } from "fs";
5
+ import { dirname, join, resolve } from "path";
6
+ import { createRequire } from "module";
7
+ import { fileURLToPath, pathToFileURL } from "url";
8
+ var require2 = createRequire(import.meta.url);
9
+ var moduleDir = dirname(fileURLToPath(import.meta.url));
10
+ async function resolvePlaywrightChromium(cwd = process.cwd()) {
11
+ const searchRoots = buildSearchRoots(cwd);
12
+ for (const packageName of ["@playwright/test", "playwright-core"]) {
13
+ for (const searchRoot of searchRoots) {
14
+ const resolvedPath = resolveFromRoot(packageName, searchRoot);
15
+ if (!resolvedPath) continue;
16
+ const imported = await import(pathToFileURL(resolvedPath).href);
17
+ const chromium = imported.chromium ?? imported.default?.chromium;
18
+ if (chromium) {
19
+ return {
20
+ packageName,
21
+ resolvedPath,
22
+ searchRoot,
23
+ chromium
24
+ };
25
+ }
26
+ }
27
+ }
28
+ throw new Error(
29
+ "Playwright runtime not found in this project.\nTry:\n npm install\n npx playwright install chromium"
30
+ );
31
+ }
32
+ function buildSearchRoots(cwd) {
33
+ const roots = [resolve(cwd)];
34
+ const projectRoot = findNearestProjectRoot(cwd);
35
+ if (projectRoot && projectRoot !== roots[0]) {
36
+ roots.push(projectRoot);
37
+ }
38
+ roots.push(moduleDir);
39
+ return Array.from(new Set(roots));
40
+ }
41
+ function findNearestProjectRoot(startDir) {
42
+ let currentDir = resolve(startDir);
43
+ while (true) {
44
+ if (existsSync(join(currentDir, "package.json"))) {
45
+ return currentDir;
46
+ }
47
+ const parentDir = dirname(currentDir);
48
+ if (parentDir === currentDir) return void 0;
49
+ currentDir = parentDir;
50
+ }
51
+ }
52
+ function resolveFromRoot(packageName, searchRoot) {
53
+ try {
54
+ return require2.resolve(packageName, { paths: [searchRoot] });
55
+ } catch {
56
+ return void 0;
57
+ }
58
+ }
59
+
60
+ // src/core/capture.ts
61
+ var SETTLE_MS = 1200;
62
+ var DEFAULT_TIMEOUT_MS = 3e4;
63
+ var MAX_PER_CATEGORY = 50;
64
+ var CaptureRuntimeError = class extends Error {
65
+ constructor(code, message, options) {
66
+ super(message);
67
+ this.name = "CaptureRuntimeError";
68
+ this.code = code;
69
+ if (options?.cause !== void 0) {
70
+ this.cause = options.cause;
71
+ }
72
+ }
73
+ };
74
+ function inferInteractiveRole(tagName, attrs = {}) {
75
+ const tag = String(tagName ?? "").trim().toLowerCase();
76
+ const explicitRole = String(attrs.role ?? "").trim().toLowerCase();
77
+ const type = String(attrs.type ?? "").trim().toLowerCase();
78
+ const href = String(attrs.href ?? "").trim();
79
+ if (explicitRole === "button") return "button";
80
+ if (explicitRole === "link") return "link";
81
+ if (explicitRole === "textbox") return "textbox";
82
+ if (explicitRole === "checkbox") return "checkbox";
83
+ if (tag === "a" && href) return "link";
84
+ if (tag === "button") return "button";
85
+ if (tag === "textarea") return "textbox";
86
+ if (tag === "select") return "textbox";
87
+ if (tag === "input") {
88
+ if (type === "submit" || type === "button" || type === "reset" || type === "image") return "button";
89
+ if (type === "checkbox") return "checkbox";
90
+ return "textbox";
91
+ }
92
+ return void 0;
93
+ }
94
+ async function captureElements(url, options = {}) {
95
+ const {
96
+ headless = true,
97
+ timeoutMs = DEFAULT_TIMEOUT_MS,
98
+ verbose = false,
99
+ userAgent = "Mozilla/5.0 (compatible; CementicTest/0.2.15 capture)"
100
+ } = options;
101
+ const chromium = await loadChromium();
102
+ const mode = headless ? "headless" : "headed";
103
+ log(verbose, `
104
+ [capture] Starting ${mode} capture: ${url}`);
105
+ let browser;
106
+ try {
107
+ browser = await chromium.launch({
108
+ headless,
109
+ slowMo: headless ? 0 : 150
110
+ });
111
+ } catch (error) {
112
+ throw classifyCaptureError(error);
113
+ }
114
+ const context = await browser.newContext({
115
+ userAgent,
116
+ viewport: { width: 1280, height: 800 },
117
+ ignoreHTTPSErrors: true
118
+ });
119
+ const page = await context.newPage();
120
+ try {
121
+ log(verbose, ` -> Navigating (timeout: ${timeoutMs}ms)`);
122
+ try {
123
+ await page.goto(url, {
124
+ waitUntil: "domcontentloaded",
125
+ timeout: timeoutMs
126
+ });
127
+ } catch (error) {
128
+ throw new CaptureRuntimeError(
129
+ "PAGE_LOAD_FAILED",
130
+ `Page load failed for ${url}. ${error?.message ?? error}`,
131
+ { cause: error }
132
+ );
133
+ }
134
+ log(verbose, ` -> Waiting ${SETTLE_MS}ms for page settle`);
135
+ await page.waitForTimeout(SETTLE_MS);
136
+ log(verbose, " -> Extracting accessibility snapshot");
137
+ const a11ySnapshot = await getAccessibilitySnapshot(page, verbose);
138
+ log(verbose, " -> Extracting DOM data");
139
+ const domData = await page.evaluate(extractDomData);
140
+ const title = await page.title();
141
+ const finalUrl = page.url();
142
+ const elements = buildElementMap(domData);
143
+ const warnings = buildWarnings(elements, domData, a11ySnapshot);
144
+ const byCategory = {};
145
+ for (const element of elements) {
146
+ byCategory[element.category] = (byCategory[element.category] ?? 0) + 1;
147
+ }
148
+ const result = {
149
+ url: finalUrl,
150
+ title,
151
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
152
+ mode,
153
+ summary: {
154
+ totalElements: elements.length,
155
+ byCategory
156
+ },
157
+ elements,
158
+ warnings
159
+ };
160
+ log(verbose, ` -> Captured ${elements.length} testable elements from "${title}"`);
161
+ return result;
162
+ } catch (error) {
163
+ throw classifyCaptureError(error);
164
+ } finally {
165
+ await browser?.close();
166
+ }
167
+ }
168
+ function toPageSummary(elementMap) {
169
+ const inputs = elementMap.elements.filter((element) => element.category === "input").slice(0, 30).map((element) => ({
170
+ label: asString(element.attributes.label),
171
+ placeholder: asString(element.attributes.placeholder),
172
+ name: asString(element.attributes.name),
173
+ type: asString(element.attributes.type),
174
+ testId: asString(element.attributes.testId)
175
+ }));
176
+ return {
177
+ url: elementMap.url,
178
+ title: elementMap.title,
179
+ headings: elementMap.elements.filter((element) => element.category === "heading").map((element) => element.name ?? "").filter(Boolean).slice(0, 20),
180
+ buttons: elementMap.elements.filter((element) => element.category === "button").map((element) => element.name ?? "").filter(Boolean).slice(0, 30),
181
+ links: elementMap.elements.filter((element) => element.category === "link").map((element) => element.name ?? "").filter(Boolean).slice(0, 50),
182
+ inputs,
183
+ landmarks: [],
184
+ rawLength: elementMap.elements.length
185
+ };
186
+ }
187
+ async function loadChromium() {
188
+ try {
189
+ const resolution = await resolvePlaywrightChromium(process.cwd());
190
+ return resolution.chromium;
191
+ } catch (error) {
192
+ throw new CaptureRuntimeError(
193
+ "PLAYWRIGHT_NOT_FOUND",
194
+ error?.message ?? "Playwright runtime not found in this project.\nTry:\n npm install\n npx playwright install chromium",
195
+ { cause: error }
196
+ );
197
+ }
198
+ }
199
+ function extractDomDataFromEnvironment(doc, win) {
200
+ const localMaxPerCategory = 50;
201
+ const attr = (el, name) => el.getAttribute(name)?.trim() || void 0;
202
+ const text = (el) => el?.textContent?.replace(/\s+/g, " ").trim() || void 0;
203
+ const jsString = (value) => JSON.stringify(value);
204
+ const semanticRole = (el) => inferInteractiveRole(el.tagName, {
205
+ role: attr(el, "role"),
206
+ href: attr(el, "href"),
207
+ type: attr(el, "type")
208
+ });
209
+ const findLabel = (el) => {
210
+ const id = attr(el, "id");
211
+ if (id) {
212
+ const labelEl = doc.querySelector(`label[for="${id}"]`);
213
+ if (labelEl) return text(labelEl);
214
+ }
215
+ const ariaLabel = attr(el, "aria-label");
216
+ if (ariaLabel) return ariaLabel;
217
+ const labelledBy = attr(el, "aria-labelledby");
218
+ if (labelledBy) {
219
+ const labelEl = doc.getElementById(labelledBy);
220
+ if (labelEl) return text(labelEl);
221
+ }
222
+ const closestLabel = el.closest("label");
223
+ if (closestLabel) {
224
+ const raw = text(closestLabel) || "";
225
+ const placeholder = attr(el, "placeholder") || "";
226
+ return raw.replace(placeholder, "").trim() || void 0;
227
+ }
228
+ const previous = el.previousElementSibling;
229
+ if (previous?.tagName === "LABEL") return text(previous);
230
+ return void 0;
231
+ };
232
+ const buildSelector = (el, labelText, buttonText) => {
233
+ const testId = attr(el, "data-testid");
234
+ if (testId) return `getByTestId(${jsString(testId)})`;
235
+ const id = attr(el, "id");
236
+ if (id && !id.match(/^(ember|react|vue|ng|auto|rand)/i)) {
237
+ return `locator(${jsString(`#${id}`)})`;
238
+ }
239
+ const ariaLabel = attr(el, "aria-label");
240
+ if (ariaLabel) {
241
+ const role = semanticRole(el);
242
+ if (role === "textbox") return `getByLabel(${jsString(ariaLabel)})`;
243
+ if (role) {
244
+ return `getByRole(${jsString(role)}, { name: ${jsString(ariaLabel)} })`;
245
+ }
246
+ }
247
+ if (labelText) {
248
+ const tag = el.tagName.toLowerCase();
249
+ if (tag === "input" || tag === "textarea" || tag === "select") {
250
+ return `getByLabel(${jsString(labelText)})`;
251
+ }
252
+ }
253
+ if (buttonText) {
254
+ const role = semanticRole(el);
255
+ if (role === "button" || role === "link") {
256
+ return `getByRole(${jsString(role)}, { name: ${jsString(buttonText)} })`;
257
+ }
258
+ }
259
+ const name = attr(el, "name");
260
+ if (name) return `locator(${jsString(`[name="${name}"]`)})`;
261
+ return null;
262
+ };
263
+ const buttons = [];
264
+ Array.from(doc.querySelectorAll('button, [role="button"], input[type="submit"], input[type="button"]')).forEach((el) => {
265
+ const role = semanticRole(el);
266
+ if (role !== "button") return;
267
+ const buttonText = attr(el, "aria-label") || text(el) || attr(el, "value");
268
+ const testId = attr(el, "data-testid");
269
+ const id = attr(el, "id");
270
+ const type = attr(el, "type");
271
+ const disabled = el.hasAttribute("disabled") || el.getAttribute("aria-disabled") === "true";
272
+ const selector = buildSelector(el, void 0, buttonText);
273
+ if (buttonText || testId) {
274
+ buttons.push({
275
+ tag: el.tagName.toLowerCase(),
276
+ role,
277
+ text: buttonText,
278
+ testId,
279
+ id,
280
+ type,
281
+ disabled,
282
+ selector,
283
+ cssPath: testId ? `[data-testid="${testId}"]` : id ? `#${id}` : null
284
+ });
285
+ }
286
+ });
287
+ const inputs = [];
288
+ Array.from(doc.querySelectorAll('input:not([type="hidden"]):not([type="submit"]):not([type="button"]), textarea, select')).forEach((el) => {
289
+ const label = findLabel(el);
290
+ const placeholder = attr(el, "placeholder");
291
+ const name = attr(el, "name");
292
+ const id = attr(el, "id");
293
+ const type = attr(el, "type") || el.tagName.toLowerCase();
294
+ const testId = attr(el, "data-testid");
295
+ const required = el.hasAttribute("required");
296
+ const selector = buildSelector(el, label);
297
+ if (label || placeholder || name || testId || id) {
298
+ inputs.push({
299
+ tag: el.tagName.toLowerCase(),
300
+ type,
301
+ label,
302
+ placeholder,
303
+ name,
304
+ id,
305
+ testId,
306
+ required,
307
+ selector,
308
+ cssPath: testId ? `[data-testid="${testId}"]` : id ? `#${id}` : name ? `[name="${name}"]` : null
309
+ });
310
+ }
311
+ });
312
+ const links = [];
313
+ Array.from(doc.querySelectorAll("a[href]")).forEach((el) => {
314
+ const role = semanticRole(el);
315
+ if (role !== "link") return;
316
+ const linkText = attr(el, "aria-label") || text(el);
317
+ const href = attr(el, "href");
318
+ const testId = attr(el, "data-testid");
319
+ if (!linkText || href === "#") return;
320
+ links.push({
321
+ tag: el.tagName.toLowerCase(),
322
+ role,
323
+ text: linkText,
324
+ href,
325
+ testId,
326
+ external: Boolean(href?.startsWith("http") && !href.includes(win.location.hostname)),
327
+ selector: testId ? `getByTestId(${jsString(testId)})` : `getByRole('link', { name: ${jsString(linkText)} })`
328
+ });
329
+ });
330
+ const headings = [];
331
+ Array.from(doc.querySelectorAll("h1, h2, h3")).forEach((el) => {
332
+ const headingText = text(el);
333
+ if (headingText) {
334
+ headings.push({
335
+ level: el.tagName.toLowerCase(),
336
+ text: headingText,
337
+ selector: `getByRole('heading', { name: ${jsString(headingText)} })`
338
+ });
339
+ }
340
+ });
341
+ const landmarks = [];
342
+ Array.from(doc.querySelectorAll("[role], main, nav, header, footer, aside, section[aria-label]")).forEach((el) => {
343
+ const role = attr(el, "role") || el.tagName.toLowerCase();
344
+ const label = attr(el, "aria-label") || text(el)?.slice(0, 40);
345
+ if (role && label) landmarks.push({ role, label });
346
+ });
347
+ const statusRegions = [];
348
+ Array.from(doc.querySelectorAll('[role="alert"], [role="status"], [aria-live]')).forEach((el) => {
349
+ const role = attr(el, "role") || "live";
350
+ statusRegions.push({
351
+ role,
352
+ ariaLive: attr(el, "aria-live"),
353
+ text: text(el),
354
+ selector: el.getAttribute("role") ? `getByRole(${jsString(role)})` : `locator('[aria-live]')`
355
+ });
356
+ });
357
+ const forms = [];
358
+ Array.from(doc.querySelectorAll("form")).forEach((form, index) => {
359
+ forms.push({
360
+ id: attr(form, "id"),
361
+ label: attr(form, "aria-label") || attr(form, "aria-labelledby"),
362
+ action: attr(form, "action"),
363
+ method: attr(form, "method") || "get",
364
+ fieldCount: Array.from(form.querySelectorAll?.("input, textarea, select") ?? []).length,
365
+ index
366
+ });
367
+ });
368
+ return {
369
+ buttons: buttons.slice(0, localMaxPerCategory),
370
+ inputs: inputs.slice(0, localMaxPerCategory),
371
+ links: links.slice(0, localMaxPerCategory),
372
+ headings: headings.slice(0, 20),
373
+ landmarks,
374
+ statusRegions,
375
+ forms,
376
+ pageUrl: win.location.href,
377
+ pageTitle: doc.title ?? ""
378
+ };
379
+ }
380
+ function extractDomData() {
381
+ return extractDomDataFromEnvironment(
382
+ document,
383
+ window
384
+ );
385
+ }
386
+ function buildElementMap(domData) {
387
+ const elements = [];
388
+ for (const input of domData.inputs) {
389
+ const displayName = input.label || input.placeholder || input.name || input.testId;
390
+ if (!displayName) continue;
391
+ const selector = input.selector || (input.testId ? `getByTestId(${JSON.stringify(input.testId)})` : null) || (input.label ? `getByLabel(${JSON.stringify(input.label)})` : null) || (input.id ? `locator(${JSON.stringify(`#${input.id}`)})` : null) || (input.name ? `locator(${JSON.stringify(`[name="${input.name}"]`)})` : null) || `locator(${JSON.stringify(`${input.tag}[placeholder="${input.placeholder ?? ""}"]`)})`;
392
+ const selectorAlt = [];
393
+ if (input.id && !selector.includes(`#${input.id}`)) selectorAlt.push(`locator(${JSON.stringify(`#${input.id}`)})`);
394
+ if (input.name && !selector.includes(input.name)) selectorAlt.push(`locator(${JSON.stringify(`[name="${input.name}"]`)})`);
395
+ if (input.testId && !selector.includes(input.testId)) selectorAlt.push(`getByTestId(${JSON.stringify(input.testId)})`);
396
+ if (input.placeholder && !selector.includes(input.placeholder)) selectorAlt.push(`getByPlaceholder(${JSON.stringify(input.placeholder)})`);
397
+ if (input.label && !selector.includes(input.label)) selectorAlt.push(`getByLabel(${JSON.stringify(input.label)})`);
398
+ elements.push({
399
+ category: "input",
400
+ role: input.type === "checkbox" ? "checkbox" : "textbox",
401
+ name: displayName,
402
+ selector,
403
+ selectorAlt,
404
+ purpose: input.required ? `Required ${input.type} field - "${displayName}"` : `${input.type} field - "${displayName}"`,
405
+ confidence: input.testId || input.label || input.id ? "high" : input.placeholder ? "medium" : "low",
406
+ attributes: {
407
+ type: input.type,
408
+ label: input.label,
409
+ placeholder: input.placeholder,
410
+ name: input.name,
411
+ id: input.id,
412
+ testId: input.testId,
413
+ required: input.required,
414
+ tag: input.tag
415
+ }
416
+ });
417
+ }
418
+ for (const button of domData.buttons) {
419
+ const buttonRole = button.role || "button";
420
+ const displayName = button.text || button.testId;
421
+ if (!displayName) continue;
422
+ const selector = button.selector || (button.testId ? `getByTestId(${JSON.stringify(button.testId)})` : null) || (button.text ? `getByRole(${JSON.stringify(buttonRole)}, { name: ${JSON.stringify(button.text)} })` : null) || (button.id ? `locator(${JSON.stringify(`#${button.id}`)})` : null) || (button.tag === "button" ? `locator('button')` : `locator('[role="button"]')`);
423
+ const selectorAlt = [];
424
+ if (button.id && !selector.includes(button.id)) selectorAlt.push(`locator(${JSON.stringify(`#${button.id}`)})`);
425
+ if (button.testId && !selector.includes(button.testId)) selectorAlt.push(`getByTestId(${JSON.stringify(button.testId)})`);
426
+ if (button.text && !selector.includes(button.text)) selectorAlt.push(`getByText(${JSON.stringify(button.text)})`);
427
+ if (button.cssPath) selectorAlt.push(`locator(${JSON.stringify(button.cssPath)})`);
428
+ elements.push({
429
+ category: "button",
430
+ role: buttonRole,
431
+ name: displayName,
432
+ selector,
433
+ selectorAlt,
434
+ purpose: button.disabled ? `Disabled button - "${displayName}"` : button.type === "submit" ? `Form submit button - "${displayName}"` : `Button - "${displayName}"`,
435
+ confidence: button.testId || button.text ? "high" : button.id ? "medium" : "low",
436
+ attributes: {
437
+ text: button.text,
438
+ testId: button.testId,
439
+ id: button.id,
440
+ type: button.type,
441
+ disabled: button.disabled,
442
+ tag: button.tag,
443
+ role: buttonRole
444
+ }
445
+ });
446
+ }
447
+ for (const link of domData.links) {
448
+ const linkRole = link.role || "link";
449
+ elements.push({
450
+ category: "link",
451
+ role: linkRole,
452
+ name: link.text,
453
+ selector: link.selector,
454
+ selectorAlt: [
455
+ ...link.testId ? [`getByTestId(${JSON.stringify(link.testId)})`] : [],
456
+ `getByText(${JSON.stringify(link.text)})`
457
+ ],
458
+ purpose: link.external ? `External link to "${link.href}" - "${link.text}"` : `Internal navigation link - "${link.text}" -> ${link.href}`,
459
+ confidence: link.testId ? "high" : "medium",
460
+ attributes: {
461
+ text: link.text,
462
+ href: link.href,
463
+ testId: link.testId,
464
+ external: link.external,
465
+ tag: link.tag,
466
+ role: linkRole
467
+ }
468
+ });
469
+ }
470
+ for (const heading of domData.headings) {
471
+ elements.push({
472
+ category: "heading",
473
+ role: "heading",
474
+ name: heading.text,
475
+ selector: heading.selector,
476
+ selectorAlt: [`getByText(${JSON.stringify(heading.text)})`],
477
+ purpose: `Page ${heading.level} heading - use to assert the correct page or section loaded`,
478
+ confidence: "medium",
479
+ attributes: {
480
+ level: heading.level,
481
+ text: heading.text
482
+ }
483
+ });
484
+ }
485
+ for (const status of domData.statusRegions) {
486
+ elements.push({
487
+ category: "status",
488
+ role: status.role,
489
+ name: status.text || status.role,
490
+ selector: status.selector,
491
+ selectorAlt: [],
492
+ purpose: "Live region - use to assert error messages, success toasts, and validation feedback",
493
+ confidence: "medium",
494
+ attributes: {
495
+ role: status.role,
496
+ ariaLive: status.ariaLive,
497
+ currentText: status.text
498
+ }
499
+ });
500
+ }
501
+ return elements;
502
+ }
503
+ function buildWarnings(elements, domData, a11ySnapshot) {
504
+ const warnings = [];
505
+ if (!a11ySnapshot) {
506
+ warnings.push("Playwright accessibility snapshot was unavailable. Capture continued using DOM extraction only.");
507
+ }
508
+ const lowConfidenceInputs = elements.filter((element) => element.category === "input" && element.confidence === "low");
509
+ if (lowConfidenceInputs.length > 0) {
510
+ warnings.push(
511
+ `${lowConfidenceInputs.length} input(s) have low-confidence selectors. Consider adding data-testid attributes to: ${lowConfidenceInputs.map((element) => element.name).filter(Boolean).join(", ")}`
512
+ );
513
+ }
514
+ if (domData.statusRegions.length === 0) {
515
+ warnings.push("No ARIA alert or status regions detected. Error message assertions may need manual selector adjustments after generation.");
516
+ }
517
+ for (const form of domData.forms) {
518
+ const hasSubmit = domData.buttons.some((button) => button.type === "submit");
519
+ if (form.fieldCount > 0 && !hasSubmit) {
520
+ warnings.push(
521
+ `Form ${form.id || `#${form.index}`} has ${form.fieldCount} field(s) but no detected submit button. It may use keyboard submit or a custom handler.`
522
+ );
523
+ }
524
+ }
525
+ if (domData.links.length >= MAX_PER_CATEGORY) {
526
+ warnings.push(`Link count hit the ${MAX_PER_CATEGORY} capture limit. Generation will focus on forms and buttons.`);
527
+ }
528
+ const interactive = elements.filter((element) => element.category === "button" || element.category === "input" || element.category === "link");
529
+ if (interactive.length === 0) {
530
+ warnings.push("No interactive elements detected. The page may require authentication, render later than the current settle window, or be mostly static content.");
531
+ }
532
+ return warnings;
533
+ }
534
+ function log(verbose, message) {
535
+ if (verbose) console.log(message);
536
+ }
537
+ async function getAccessibilitySnapshot(page, verbose) {
538
+ if (!page.accessibility || typeof page.accessibility.snapshot !== "function") {
539
+ log(verbose, " -> Accessibility snapshot API unavailable; continuing with DOM-only capture");
540
+ return null;
541
+ }
542
+ try {
543
+ return await page.accessibility.snapshot({ interestingOnly: false });
544
+ } catch (error) {
545
+ log(verbose, ` -> Accessibility snapshot failed (${error?.message ?? error}); continuing with DOM-only capture`);
546
+ return null;
547
+ }
548
+ }
549
+ function asString(value) {
550
+ return typeof value === "string" ? value : void 0;
551
+ }
552
+ function formatCaptureFailure(error) {
553
+ const resolved = classifyCaptureError(error);
554
+ if (resolved.code === "PLAYWRIGHT_NOT_FOUND") {
555
+ return [
556
+ "Playwright not resolvable.",
557
+ "Playwright runtime not found in this project.",
558
+ "Try:",
559
+ " npm install",
560
+ " npx playwright install chromium"
561
+ ];
562
+ }
563
+ if (resolved.code === "BROWSER_NOT_INSTALLED") {
564
+ return [
565
+ "Browser not installed.",
566
+ "Playwright resolved, but Chromium is not installed for this project.",
567
+ "Try:",
568
+ " npx playwright install chromium"
569
+ ];
570
+ }
571
+ if (resolved.code === "PAGE_LOAD_FAILED") {
572
+ return [
573
+ "Page load failed.",
574
+ resolved.message
575
+ ];
576
+ }
577
+ return [
578
+ "Capture failed.",
579
+ resolved.message
580
+ ];
581
+ }
582
+ function classifyCaptureError(error) {
583
+ if (error instanceof CaptureRuntimeError) {
584
+ return error;
585
+ }
586
+ const message = errorMessage(error);
587
+ if (isBrowserInstallError(message)) {
588
+ return new CaptureRuntimeError(
589
+ "BROWSER_NOT_INSTALLED",
590
+ "Playwright resolved, but Chromium is not installed for this project.",
591
+ { cause: error }
592
+ );
593
+ }
594
+ if (isPlaywrightNotFoundError(message)) {
595
+ return new CaptureRuntimeError(
596
+ "PLAYWRIGHT_NOT_FOUND",
597
+ "Playwright runtime not found in this project.",
598
+ { cause: error }
599
+ );
600
+ }
601
+ return new CaptureRuntimeError("CAPTURE_FAILED", message || "Unknown capture failure.", { cause: error });
602
+ }
603
+ function isBrowserInstallError(message) {
604
+ return /Executable doesn't exist|browserType\.launch: Executable doesn't exist|Please run the following command|playwright install/i.test(message);
605
+ }
606
+ function isPlaywrightNotFoundError(message) {
607
+ return /Playwright runtime not found|Cannot find package ['"](?:@playwright\/test|playwright-core)['"]/i.test(message);
608
+ }
609
+ function errorMessage(error) {
610
+ if (error instanceof Error) return error.message;
611
+ return String(error ?? "");
612
+ }
613
+
614
+ export {
615
+ CaptureRuntimeError,
616
+ inferInteractiveRole,
617
+ captureElements,
618
+ toPageSummary,
619
+ extractDomDataFromEnvironment,
620
+ formatCaptureFailure
621
+ };
622
+ //# sourceMappingURL=chunk-XUWFEWJZ.js.map