@piwitests/reporter 0.4.3 → 0.5.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.
package/dist/config.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { PlaywrightTestConfig } from '@playwright/test';
2
+ export type { PlaywrightTestConfig } from '@playwright/test';
2
3
  /** Playwright shard info — mirrors `config.shard` shape */
3
4
  export interface ShardInfo {
4
5
  current: number;
@@ -6,6 +7,8 @@ export interface ShardInfo {
6
7
  }
7
8
  /** Options for configuring the Piwi Dashboard reporter */
8
9
  export interface PiwiDashboardOptions extends PlaywrightTestConfig {
10
+ /** Explicitly enable or disable the reporter. Defaults to `true` when `serverUrl` is set. Set to `false` to disable even if `serverUrl` is provided. */
11
+ enabled?: boolean;
9
12
  /** URL of the Piwi Dashboard server */
10
13
  serverUrl?: string;
11
14
  /** Name of the project to report results under. Defaults to `'default-project'`. */
@@ -24,6 +27,14 @@ export interface PiwiDashboardOptions extends PlaywrightTestConfig {
24
27
  collectCiInfo?: boolean;
25
28
  /** Collect step timings, network requests and web vitals. Defaults to `true`. */
26
29
  collectPerformanceMetrics?: boolean;
30
+ /**
31
+ * Capture per-action locator snapshots that power failure-time healing
32
+ * suggestions. Adds a small per-action cost (one DOM read, sometimes an ARIA
33
+ * snapshot) in the test worker. Defaults to `true`; automatically disabled
34
+ * when `collectPerformanceMetrics` is `false` (the reporter discards the data
35
+ * in that case anyway). Can also be forced off with `PIWI_CAPTURE_LOCATORS=false`.
36
+ */
37
+ captureLocators?: boolean;
27
38
  /** Enable live streaming of results (falls back to batch if unsupported). Defaults to `true`. */
28
39
  streaming?: boolean;
29
40
  /** Number of test results to batch before sending during streaming. Defaults to `5`. */
@@ -81,6 +92,7 @@ export declare const PIWI_ENV_KEYS: {
81
92
  readonly liveFileUploads: "PIWI_LIVE_FILE_UPLOADS";
82
93
  readonly uploadTraces: "PIWI_UPLOAD_TRACES";
83
94
  readonly uploadReport: "PIWI_UPLOAD_REPORT";
95
+ readonly captureLocators: "PIWI_CAPTURE_LOCATORS";
84
96
  };
85
97
  /**
86
98
  * Merge raw user options with defaults, reading from `PIWI_*` env vars when
package/dist/config.js CHANGED
@@ -11,6 +11,7 @@ const DEFAULTS = {
11
11
  collectScmInfo: true,
12
12
  collectCiInfo: true,
13
13
  collectPerformanceMetrics: true,
14
+ captureLocators: true,
14
15
  streaming: true,
15
16
  streamingBatchSize: 5,
16
17
  streamingBatchDelay: 2000,
@@ -41,6 +42,7 @@ exports.PIWI_ENV_KEYS = {
41
42
  liveFileUploads: 'PIWI_LIVE_FILE_UPLOADS',
42
43
  uploadTraces: 'PIWI_UPLOAD_TRACES',
43
44
  uploadReport: 'PIWI_UPLOAD_REPORT',
45
+ captureLocators: 'PIWI_CAPTURE_LOCATORS',
44
46
  };
45
47
  function readBool(val) {
46
48
  if (val === undefined)
@@ -92,6 +94,8 @@ function resolveOptions(raw) {
92
94
  mergedRaw.uploadTraces = readBool(env[exports.PIWI_ENV_KEYS.uploadTraces]);
93
95
  if (mergedRaw.uploadReport === undefined && env[exports.PIWI_ENV_KEYS.uploadReport] !== undefined)
94
96
  mergedRaw.uploadReport = readBool(env[exports.PIWI_ENV_KEYS.uploadReport]);
97
+ if (mergedRaw.captureLocators === undefined && env[exports.PIWI_ENV_KEYS.captureLocators] !== undefined)
98
+ mergedRaw.captureLocators = readBool(env[exports.PIWI_ENV_KEYS.captureLocators]);
95
99
  const opts = { ...DEFAULTS, ...mergedRaw };
96
100
  // Preserved quirk: PIWI_VERBOSE wins over both default and user option.
97
101
  if (env[exports.PIWI_ENV_KEYS.verbose] !== undefined)
@@ -124,4 +128,11 @@ function applyOptionsToEnv(options) {
124
128
  env[exports.PIWI_ENV_KEYS.label] = options.label;
125
129
  if (options.runLabel)
126
130
  env[exports.PIWI_ENV_KEYS.runLabel] = options.runLabel;
131
+ // Locator capture is part of performance-metric collection; switch it off in
132
+ // the worker when either flag is disabled so the fixture skips the per-action
133
+ // cost. Only an explicit `true` overrides the unset (default-on) state.
134
+ if (options.captureLocators === false || options.collectPerformanceMetrics === false)
135
+ env[exports.PIWI_ENV_KEYS.captureLocators] = 'false';
136
+ else if (options.captureLocators === true)
137
+ env[exports.PIWI_ENV_KEYS.captureLocators] = 'true';
127
138
  }
@@ -1,7 +1,7 @@
1
1
  import type { Fixtures } from '@playwright/test';
2
2
  /**
3
3
  * Playwright fixtures that collect network requests, console entries,
4
- * web vitals, and ARIA snapshots during a test.
4
+ * web vitals, ARIA snapshots, and locator interaction data during a test.
5
5
  *
6
6
  * Attaches collected data as `piwi-dashboard-*` test-info attachments
7
7
  * which the Piwi Dashboard reporter parses on `onTestEnd`.
package/dist/fixtures.js CHANGED
@@ -3,9 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.dashboardFixtures = void 0;
4
4
  exports.extendDashboardFixtures = extendDashboardFixtures;
5
5
  const zlib_1 = require("zlib");
6
+ const locator_healing_js_1 = require("./locator-healing.js");
6
7
  /**
7
8
  * Playwright fixtures that collect network requests, console entries,
8
- * web vitals, and ARIA snapshots during a test.
9
+ * web vitals, ARIA snapshots, and locator interaction data during a test.
9
10
  *
10
11
  * Attaches collected data as `piwi-dashboard-*` test-info attachments
11
12
  * which the Piwi Dashboard reporter parses on `onTestEnd`.
@@ -15,6 +16,151 @@ exports.dashboardFixtures = {
15
16
  const networkRequests = [];
16
17
  const consoleEntries = [];
17
18
  const pendingHandlers = [];
19
+ // ── Locator interaction capture ──────────────────────────────────────
20
+ // Opt-out: skipped when PIWI_CAPTURE_LOCATORS=false (set automatically when
21
+ // the reporter's collectPerformanceMetrics / captureLocators is disabled),
22
+ // so the per-action DOM read + ARIA snapshot cost is never paid when unused.
23
+ const captureLocators = process.env.PIWI_CAPTURE_LOCATORS !== 'false';
24
+ const capturedLocators = [];
25
+ const capturePromises = [];
26
+ // Locator actions that threw — used at teardown to suggest a fresh locator
27
+ // from the current page when the element appears renamed/moved.
28
+ const failedLocators = [];
29
+ // Chain methods that take args and define a new locator scope (not just narrow).
30
+ // Origin method/args update to the chain call, e.g. .locator('.item') → locator('.item').
31
+ // Positional/filter chains that narrow but don't change locator identity.
32
+ // Origin stays from the page-level call, e.g. .first(), .nth(2), .filter(...).
33
+ function wrapLocator(locator, originMethod, originArgs) {
34
+ return new Proxy(locator, {
35
+ get(target, prop) {
36
+ const original = target[prop];
37
+ if (typeof original !== 'function')
38
+ return original;
39
+ if (locator_healing_js_1.CHAIN_METHODS.includes(prop)) {
40
+ return (...args) => {
41
+ const next = original.apply(target, args);
42
+ if (locator_healing_js_1.LOCATOR_CREATING_CHAINS.has(prop)) {
43
+ return wrapLocator(next, String(prop), args);
44
+ }
45
+ return wrapLocator(next, originMethod, originArgs);
46
+ };
47
+ }
48
+ if (!locator_healing_js_1.ACTION_METHODS.includes(prop))
49
+ return original;
50
+ return async (...callArgs) => {
51
+ const seq = capturedLocators.length;
52
+ // Capture the test call-site now (sync) so the snapshot's location
53
+ // matches the error stack's first user frame — independent of
54
+ // pw:api step ordering, worker interleaving, or concurrent actions.
55
+ const callerLocation = (0, locator_healing_js_1.captureCallerLocation)();
56
+ // Push a placeholder immediately — DOM capture runs async below
57
+ capturedLocators.push({
58
+ location: callerLocation,
59
+ stepIndex: seq,
60
+ used: {
61
+ method: originMethod,
62
+ args: originArgs,
63
+ raw: `${originMethod}(${JSON.stringify(originArgs)})`,
64
+ },
65
+ element: null,
66
+ alternatives: [],
67
+ });
68
+ // The placeholder is already pushed. If the action throws, record
69
+ // the failed locator (so teardown can suggest a fresh one for the
70
+ // element's current identity) and re-throw so the test still fails.
71
+ let result;
72
+ try {
73
+ result = await original.apply(target, callArgs);
74
+ }
75
+ catch (err) {
76
+ failedLocators.push({ method: originMethod, args: originArgs });
77
+ throw err;
78
+ }
79
+ // Fire-and-forget: capture element data without blocking the test.
80
+ // evaluate() can hang when page navigates (element detaches), so
81
+ // race it against a 500ms deadline and never throw.
82
+ const resolveAttrs = (async () => {
83
+ try {
84
+ const attrs = (await Promise.race([
85
+ target.evaluate((el, keep) => {
86
+ const attrMap = {};
87
+ for (const key of keep) {
88
+ const v = el.getAttribute(key) ?? el[key];
89
+ attrMap[key] = typeof v === 'string' ? v.slice(0, 200) : v ? String(v).slice(0, 200) : null;
90
+ }
91
+ const r = el.getBoundingClientRect();
92
+ return {
93
+ tagName: el.tagName?.toLowerCase?.() ?? 'unknown',
94
+ attributes: attrMap,
95
+ textContent: (el.textContent || '').trim().slice(0, 80),
96
+ center: {
97
+ x: Math.round(r.x + r.width / 2),
98
+ y: Math.round(r.y + r.height / 2),
99
+ },
100
+ };
101
+ }, [...locator_healing_js_1.CAPTURED_ATTRIBUTES]),
102
+ new Promise((_, reject) => setTimeout(() => reject(new Error('locator capture timeout')), 500)),
103
+ ]));
104
+ // The browser-computed accessible name only feeds role-based and
105
+ // form-field alternatives, so only pay for the extra ARIA
106
+ // snapshot when the element actually has a role or is a field.
107
+ const role = (0, locator_healing_js_1.resolveAriaRole)({
108
+ tagName: attrs.tagName,
109
+ attributes: attrs.attributes,
110
+ textContent: attrs.textContent,
111
+ accessibleName: null,
112
+ center: attrs.center,
113
+ });
114
+ const isFormField = ['input', 'select', 'textarea'].includes(attrs.tagName);
115
+ // Bound with a timeout: without it, ariaSnapshot waits up to the
116
+ // test timeout when the page is mid-navigation, which hangs the
117
+ // fixture teardown that drains these capture promises.
118
+ const aria = role || isFormField
119
+ ? (await target.ariaSnapshot({ ref: true, timeout: 500 }).catch(() => null))
120
+ : null;
121
+ const accessibleName = (0, locator_healing_js_1.extractAccessibleName)(aria) ||
122
+ (0, locator_healing_js_1.approximateAccessibleName)({
123
+ tagName: attrs.tagName,
124
+ attributes: attrs.attributes,
125
+ textContent: attrs.textContent,
126
+ accessibleName: null,
127
+ center: attrs.center,
128
+ });
129
+ capturedLocators[seq] = {
130
+ location: callerLocation,
131
+ stepIndex: seq,
132
+ used: {
133
+ method: originMethod,
134
+ args: originArgs,
135
+ raw: `${originMethod}(${JSON.stringify(originArgs)})`,
136
+ },
137
+ element: { ...attrs, accessibleName },
138
+ alternatives: (0, locator_healing_js_1.generateAlternatives)({
139
+ tagName: attrs.tagName,
140
+ attributes: attrs.attributes,
141
+ textContent: attrs.textContent,
142
+ accessibleName,
143
+ center: attrs.center,
144
+ }),
145
+ };
146
+ }
147
+ catch {
148
+ // element detached or timeout — keep the placeholder
149
+ }
150
+ })();
151
+ capturePromises.push(resolveAttrs);
152
+ return result;
153
+ };
154
+ },
155
+ });
156
+ }
157
+ if (captureLocators) {
158
+ for (const method of locator_healing_js_1.LOCATOR_METHODS) {
159
+ const original = page[method].bind(page);
160
+ page[method] = (...args) => wrapLocator(original(...args), method, args);
161
+ }
162
+ }
163
+ // ── Existing event listeners ──────────────────────────────────────────
18
164
  page.on('console', (msg) => {
19
165
  const type = msg.type();
20
166
  if (['warning', 'error', 'assert'].includes(type)) {
@@ -49,7 +195,13 @@ exports.dashboardFixtures = {
49
195
  resourceType,
50
196
  };
51
197
  if (response) {
52
- const logHeader = response.headers()['x-piwi-logs'];
198
+ const headers = response.headers();
199
+ // Response content type (without charset/boundary params) — relevant
200
+ // per-request metadata for distinguishing API/JSON vs document/HTML calls.
201
+ const contentType = headers['content-type'];
202
+ if (contentType)
203
+ entry.contentType = contentType.split(';')[0].trim();
204
+ const logHeader = headers['x-piwi-logs'];
53
205
  if (logHeader) {
54
206
  try {
55
207
  entry.serverLogs = JSON.parse((0, zlib_1.gunzipSync)(Buffer.from(logHeader, 'base64')).toString('utf-8'));
@@ -72,6 +224,17 @@ exports.dashboardFixtures = {
72
224
  // networkRequests — the last request (often the one that failed the test)
73
225
  // races with fixture teardown and its serverLogs would otherwise be lost.
74
226
  await Promise.allSettled(pendingHandlers);
227
+ // ── Attach locator snapshots ──────────────────────────────────────────
228
+ // Cap the drain so a stuck capture (e.g. a navigation in flight) can never
229
+ // hang teardown past the test timeout; per-action evaluate/ariaSnapshot are
230
+ // already bounded, this is a backstop.
231
+ await Promise.race([Promise.allSettled(capturePromises), new Promise((resolve) => setTimeout(resolve, 2000))]);
232
+ if (capturedLocators.length > 0) {
233
+ await testInfo.attach('piwi-dashboard-locators', {
234
+ contentType: 'application/json',
235
+ body: Buffer.from(JSON.stringify(capturedLocators)),
236
+ });
237
+ }
75
238
  if (testInfo.status !== 'passed' && testInfo.status !== 'skipped') {
76
239
  try {
77
240
  const snapshot = await page.locator(':root').ariaSnapshot();
@@ -80,6 +243,23 @@ exports.dashboardFixtures = {
80
243
  contentType: 'text/plain',
81
244
  body: snapshot,
82
245
  });
246
+ // Suggest a fresh locator for the failed action from the current page.
247
+ // When the element was renamed/moved, the pre-captured alternatives
248
+ // describe the old element, so this points at where it went now — as a
249
+ // Playwright annotation (shown in the report + trace) and an attachment
250
+ // (shown in the trace viewer). It does NOT change the locator.
251
+ const failed = failedLocators[failedLocators.length - 1];
252
+ const suggestion = failed ? (0, locator_healing_js_1.suggestLocatorsFromAria)(failed, snapshot) : null;
253
+ if (suggestion) {
254
+ testInfo.annotations.push({
255
+ type: 'piwi-locator-suggestion',
256
+ description: `${suggestion.failing} matched nothing on the failing page — the element may have been renamed or moved. Suggested: ${suggestion.suggestions.join(' | ')}`,
257
+ });
258
+ await testInfo.attach('piwi-dashboard-locator-suggestion', {
259
+ contentType: 'application/json',
260
+ body: Buffer.from(JSON.stringify(suggestion)),
261
+ });
262
+ }
83
263
  }
84
264
  }
85
265
  catch {
package/dist/helpers.js CHANGED
@@ -224,28 +224,52 @@ function createLimiter(maxConcurrent) {
224
224
  */
225
225
  function createGlobalSetup(options, userSetup) {
226
226
  return async function globalSetupFn(config) {
227
- const opts = (0, config_js_1.resolveOptions)((options ?? {}));
227
+ const piwiReporterPath = path.resolve(__dirname, 'index.js');
228
+ // Extract options from the Piwi reporter entry in the Playwright config so
229
+ // that serverUrl / projectName etc. set inline in the reporters array are
230
+ // visible here without requiring PIWI_* env vars or a separate wrapConfig call.
231
+ let inlineReporterOptions = {};
232
+ if (Array.isArray(config?.reporter)) {
233
+ for (const r of config.reporter) {
234
+ if (!Array.isArray(r) || typeof r[0] !== 'string')
235
+ continue;
236
+ const isPiwi = r[0].toLowerCase().includes('piwi') ||
237
+ (() => {
238
+ try {
239
+ return path.resolve(require.resolve(r[0])) === piwiReporterPath;
240
+ }
241
+ catch {
242
+ return false;
243
+ }
244
+ })();
245
+ if (isPiwi && r[1] && typeof r[1] === 'object') {
246
+ inlineReporterOptions = r[1];
247
+ break;
248
+ }
249
+ }
250
+ }
251
+ const opts = (0, config_js_1.resolveOptions)({ ...inlineReporterOptions, ...(options ?? {}) });
228
252
  const logger = new logger_js_1.Logger(opts.verbose ?? false);
229
- if (!opts.serverUrl) {
253
+ if (opts.enabled === false || !opts.serverUrl) {
230
254
  logger.info('Not enabled — set PIWI_DASHBOARD_URL or serverUrl to enable.');
231
255
  if (userSetup)
232
256
  return userSetup(config);
233
257
  return;
234
258
  }
235
- const piwiReporterPath = path.resolve(__dirname, 'index.js');
236
- const hasPiwi = Array.isArray(config?.reporter) &&
237
- config.reporter.some((r) => {
238
- if (!Array.isArray(r) || typeof r[0] !== 'string')
239
- return false;
240
- if (r[0].toLowerCase().includes('piwi'))
241
- return true;
242
- try {
243
- return path.resolve(require.resolve(r[0])) === piwiReporterPath;
244
- }
245
- catch {
246
- return false;
247
- }
248
- });
259
+ const hasPiwi = Object.keys(inlineReporterOptions).length > 0 ||
260
+ (Array.isArray(config?.reporter) &&
261
+ config.reporter.some((r) => {
262
+ if (!Array.isArray(r) || typeof r[0] !== 'string')
263
+ return false;
264
+ if (r[0].toLowerCase().includes('piwi'))
265
+ return true;
266
+ try {
267
+ return path.resolve(require.resolve(r[0])) === piwiReporterPath;
268
+ }
269
+ catch {
270
+ return false;
271
+ }
272
+ }));
249
273
  if (!hasPiwi) {
250
274
  logger.debug('Not reporting — Piwi is not in the Playwright reporters list.');
251
275
  if (userSetup)
package/dist/index.d.ts CHANGED
@@ -1,8 +1,6 @@
1
1
  import { PiwiDashboardReporter } from './reporter.js';
2
- import { wrapConfig } from './config-wrapper.js';
3
- import { createGlobalSetup } from './helpers.js';
4
- declare const _default: typeof PiwiDashboardReporter & {
5
- wrapConfig: typeof wrapConfig;
6
- createGlobalSetup: typeof createGlobalSetup;
7
- };
8
- export = _default;
2
+ export default PiwiDashboardReporter;
3
+ export { PiwiDashboardReporter };
4
+ export { wrapConfig } from './config-wrapper.js';
5
+ export { createGlobalSetup } from './helpers.js';
6
+ export type { PiwiDashboardOptions, PlaywrightTestConfig } from './config.js';
package/dist/index.js CHANGED
@@ -1,6 +1,10 @@
1
1
  "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createGlobalSetup = exports.wrapConfig = exports.PiwiDashboardReporter = void 0;
2
4
  const reporter_js_1 = require("./reporter.js");
3
- const config_wrapper_js_1 = require("./config-wrapper.js");
4
- const helpers_js_1 = require("./helpers.js");
5
- const _default = Object.assign(reporter_js_1.PiwiDashboardReporter, { wrapConfig: config_wrapper_js_1.wrapConfig, createGlobalSetup: helpers_js_1.createGlobalSetup });
6
- module.exports = _default;
5
+ Object.defineProperty(exports, "PiwiDashboardReporter", { enumerable: true, get: function () { return reporter_js_1.PiwiDashboardReporter; } });
6
+ exports.default = reporter_js_1.PiwiDashboardReporter;
7
+ var config_wrapper_js_1 = require("./config-wrapper.js");
8
+ Object.defineProperty(exports, "wrapConfig", { enumerable: true, get: function () { return config_wrapper_js_1.wrapConfig; } });
9
+ var helpers_js_1 = require("./helpers.js");
10
+ Object.defineProperty(exports, "createGlobalSetup", { enumerable: true, get: function () { return helpers_js_1.createGlobalSetup; } });
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Reporter-local locator healing — pure functions for alternative generation,
3
+ * stability scoring, and the Playwright method-surface constants shared with
4
+ * the fixture proxy. No Playwright dependency — takes raw element attributes,
5
+ * returns ranked locator suggestions.
6
+ */
7
+ export interface RankedLocator {
8
+ locator: string;
9
+ method: string;
10
+ args: Record<string, unknown>;
11
+ /** 0-100 stability score. data-testid=100, semantic CSS=35-40, hash-suffixed=10. */
12
+ score: number;
13
+ }
14
+ export interface ElementAttributes {
15
+ tagName: string;
16
+ attributes: Record<string, string | null>;
17
+ textContent: string | null;
18
+ accessibleName: string | null;
19
+ center: {
20
+ x: number;
21
+ y: number;
22
+ } | null;
23
+ }
24
+ export interface LocatorSnapshot {
25
+ location: string | null;
26
+ stepIndex: number;
27
+ used: {
28
+ method: string;
29
+ args: unknown[];
30
+ raw: string;
31
+ };
32
+ element: {
33
+ tagName: string;
34
+ attributes: Record<string, string | null>;
35
+ textContent: string | null;
36
+ accessibleName: string | null;
37
+ center: {
38
+ x: number;
39
+ y: number;
40
+ } | null;
41
+ } | null;
42
+ alternatives: RankedLocator[];
43
+ }
44
+ /**
45
+ * Page-level locator-building methods wrapped by the capture proxy. Imported by
46
+ * both `reporter/src/fixtures.ts` and the dogfooding `application/tests/fixtures.ts`
47
+ * so the two stay in sync (a prior drift missed `scrollIntoViewIfNeeded`).
48
+ */
49
+ export declare const LOCATOR_METHODS: string[];
50
+ /**
51
+ * Methods that can be chained onto a wrapped locator. Locator-creating chains
52
+ * (those also in `LOCATOR_METHODS`) update the origin; positional/filter chains
53
+ * (`first`, `nth`, `filter`, …) narrow without changing locator identity.
54
+ */
55
+ export declare const CHAIN_METHODS: string[];
56
+ /** Locator action methods that trigger element capture. */
57
+ export declare const ACTION_METHODS: string[];
58
+ /** Chain methods that create a new locator scope (origin tracks the chain call). */
59
+ export declare const LOCATOR_CREATING_CHAINS: ReadonlySet<string>;
60
+ /**
61
+ * Element attributes to capture after a successful action, passed into the
62
+ * in-page `evaluate`. Shared so the reporter and dogfooding fixtures capture
63
+ * the same attribute set.
64
+ */
65
+ export declare const CAPTURED_ATTRIBUTES: string[];
66
+ /**
67
+ * Resolve the ARIA role for an element. An explicit `role` attribute wins;
68
+ * otherwise the implicit role is derived from the tag name (and `type` for
69
+ * `<input>`). Returns null when the element has no ARIA role (e.g. `<div>`,
70
+ * `<span>`, `<a>` without `href`) — `getByRole` is not a valid locator for
71
+ * such elements and other alternatives take over.
72
+ */
73
+ export declare function resolveAriaRole(attrs: ElementAttributes): string | null;
74
+ /**
75
+ * Build a ranked list of alternative locators from the captured element
76
+ * attributes. The list is sorted descending by stability score.
77
+ *
78
+ * Only generates alternatives that differ from each other — no duplicates
79
+ * of the same locator expression.
80
+ */
81
+ export declare function generateAlternatives(attrs: ElementAttributes): RankedLocator[];
82
+ /**
83
+ * Score a CSS class name on a 0-40 stability scale.
84
+ *
85
+ * Heuristics (inherited from common CSS-naming conventions):
86
+ * - Hash-like suffixes (≥4 hex chars) → 10 — auto-generated, fragile
87
+ * - CSS-in-JS patterns (css-, sc-, emotion-, styled-, _) → 15
88
+ * - Tailwind/utility classes → 25
89
+ * - BEM-style semantic → 35
90
+ * - Plain semantic → 40
91
+ */
92
+ export declare function classifyCssStability(className: string): number;
93
+ /**
94
+ * Detects GUID-like, auto-incremented, or hash-suffixed IDs that are
95
+ * likely regenerated on each render and unstable for testing.
96
+ */
97
+ export declare function isAutoGenerated(value: string): boolean;
98
+ /**
99
+ * Extract the accessible name from a YAML-like ariaSnapshot() output.
100
+ *
101
+ * Format example:
102
+ * - button "Submit order"
103
+ * - heading "Welcome, Alice"
104
+ * - textbox "Email" [ref=e12]
105
+ * - generic
106
+ *
107
+ * Returns the first quoted string after the role, or null if none found.
108
+ */
109
+ export declare function extractAccessibleName(ariaSnapshot: string | null): string | null;
110
+ /**
111
+ * Approximate the accessible name from HTML attributes when ariaSnapshot()
112
+ * is unavailable. Priority: aria-label > text content > title > placeholder.
113
+ */
114
+ export declare function approximateAccessibleName(attrs: ElementAttributes): string | null;
115
+ /** A failed locator action, used to suggest a fresh locator from the live page. */
116
+ export interface FailedLocatorInfo {
117
+ method: string;
118
+ args: unknown[];
119
+ }
120
+ export interface LocatorSuggestion {
121
+ /** The failed locator rendered as source, e.g. `getByText('Go to page')`. */
122
+ failing: string;
123
+ /** Fresh locator suggestions for the element's current identity, best first. */
124
+ suggestions: string[];
125
+ }
126
+ /**
127
+ * Best-effort runtime suggestion for a locator that matched nothing: find the
128
+ * element on the *current* page that the failed locator most likely targeted
129
+ * (by accessible name similarity, restricted to the failed role when given) and
130
+ * return fresh locators for it.
131
+ *
132
+ * Unlike the server lookup this has no pre-captured fingerprint — only the
133
+ * failed locator + the live page — so it's a hint, not a guarantee. Returns null
134
+ * for non-name-based locators (testid/CSS), when the targeted name is still
135
+ * present (so the failure wasn't a rename), or when no candidate is confident.
136
+ */
137
+ export declare function suggestLocatorsFromAria(failed: FailedLocatorInfo, ariaSnapshot: string | null): LocatorSuggestion | null;
138
+ /**
139
+ * Capture the calling test's source location (`file:line:col`) from the current
140
+ * stack, for stamping onto a locator snapshot. The path is made cwd-relative so
141
+ * it matches the location format Playwright embeds in error messages — which is
142
+ * what the server's exact-match healing lookup (`extractErrorLocation`) parses.
143
+ *
144
+ * Stamping at action *call* time (rather than correlating with `pw:api` step
145
+ * indices at step *end* time) avoids three classes of misalignment: `pw:api`
146
+ * steps with no wrapped locator (e.g. `page.keyboard.press`), cross-worker step
147
+ * interleaving, and concurrent actions reordering by end time.
148
+ *
149
+ * Returns null when no user frame can be identified — the snapshot keeps
150
+ * `location: null` and the server falls back to fingerprint / ARIA lookup.
151
+ */
152
+ export declare function captureCallerLocation(): string | null;