@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 +12 -0
- package/dist/config.js +11 -0
- package/dist/fixtures.d.ts +1 -1
- package/dist/fixtures.js +182 -2
- package/dist/helpers.js +40 -16
- package/dist/index.d.ts +5 -7
- package/dist/index.js +8 -4
- package/dist/locator-healing.d.ts +152 -0
- package/dist/locator-healing.js +663 -0
- package/dist/reporter.d.ts +2 -0
- package/dist/reporter.js +45 -3
- package/dist/run-submitter.js +2 -0
- package/dist/serializer.js +2 -0
- package/dist/stream-manager.d.ts +14 -0
- package/dist/stream-manager.js +51 -0
- package/dist/types.d.ts +5 -0
- package/dist/uploader.d.ts +1 -0
- package/package.json +1 -1
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
|
}
|
package/dist/fixtures.d.ts
CHANGED
|
@@ -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,
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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;
|