@piwitests/reporter 0.24.0 → 0.25.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/index.js
CHANGED
|
@@ -3092,7 +3092,16 @@ function probeElementAttrs(el, arg) {
|
|
|
3092
3092
|
}
|
|
3093
3093
|
}
|
|
3094
3094
|
const hasLabel = !!(el.labels && el.labels.length > 0);
|
|
3095
|
-
|
|
3095
|
+
let labelText;
|
|
3096
|
+
if (includeLabelText) {
|
|
3097
|
+
labelText = null;
|
|
3098
|
+
try {
|
|
3099
|
+
const label = hasLabel ? el.labels[0] : null;
|
|
3100
|
+
labelText = (label && label.textContent || "").replace(/\s+/g, " ").trim().slice(0, 120) || null;
|
|
3101
|
+
} catch {
|
|
3102
|
+
labelText = null;
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3096
3105
|
return {
|
|
3097
3106
|
tagName: el.tagName?.toLowerCase?.() ?? "unknown",
|
|
3098
3107
|
attributes: attrMap,
|
|
@@ -3796,6 +3805,7 @@ var CAPTURED_ATTRIBUTES = [
|
|
|
3796
3805
|
"alt",
|
|
3797
3806
|
"title",
|
|
3798
3807
|
"aria-label",
|
|
3808
|
+
"aria-labelledby",
|
|
3799
3809
|
"aria-level",
|
|
3800
3810
|
"role",
|
|
3801
3811
|
"type",
|
|
@@ -4387,6 +4397,43 @@ function extractAccessibleName(ariaSnapshot) {
|
|
|
4387
4397
|
if (match) return match[1];
|
|
4388
4398
|
return null;
|
|
4389
4399
|
}
|
|
4400
|
+
var FORM_FIELD_TAGS = /* @__PURE__ */ new Set(["input", "select", "textarea"]);
|
|
4401
|
+
var NAME_FROM_CONTENT_ROLES = /* @__PURE__ */ new Set([
|
|
4402
|
+
"button",
|
|
4403
|
+
"cell",
|
|
4404
|
+
"checkbox",
|
|
4405
|
+
"columnheader",
|
|
4406
|
+
"gridcell",
|
|
4407
|
+
"heading",
|
|
4408
|
+
"link",
|
|
4409
|
+
"menuitem",
|
|
4410
|
+
"menuitemcheckbox",
|
|
4411
|
+
"menuitemradio",
|
|
4412
|
+
"option",
|
|
4413
|
+
"radio",
|
|
4414
|
+
"row",
|
|
4415
|
+
"rowheader",
|
|
4416
|
+
"switch",
|
|
4417
|
+
"tab",
|
|
4418
|
+
"tooltip",
|
|
4419
|
+
"treeitem"
|
|
4420
|
+
]);
|
|
4421
|
+
var PROBE_TEXT_CAP = 80;
|
|
4422
|
+
var PROBE_LABEL_TEXT_CAP = 120;
|
|
4423
|
+
function exactAccessibleName(attrs, role) {
|
|
4424
|
+
if (attrs.attributes["aria-labelledby"]) return null;
|
|
4425
|
+
const ariaLabel = attrs.attributes["aria-label"];
|
|
4426
|
+
if (ariaLabel) return ariaLabel;
|
|
4427
|
+
if (FORM_FIELD_TAGS.has(attrs.tagName)) {
|
|
4428
|
+
const labelText = attrs.labelText;
|
|
4429
|
+
return labelText && labelText.length < PROBE_LABEL_TEXT_CAP ? labelText : null;
|
|
4430
|
+
}
|
|
4431
|
+
if (role && NAME_FROM_CONTENT_ROLES.has(role)) {
|
|
4432
|
+
const text = attrs.textContent;
|
|
4433
|
+
return text && text.length < PROBE_TEXT_CAP ? text : null;
|
|
4434
|
+
}
|
|
4435
|
+
return null;
|
|
4436
|
+
}
|
|
4390
4437
|
var NAME_BASED_METHODS = /* @__PURE__ */ new Set([
|
|
4391
4438
|
"getByText",
|
|
4392
4439
|
"getByRole",
|
|
@@ -4831,7 +4878,7 @@ function createSink() {
|
|
|
4831
4878
|
capturedLocators: [],
|
|
4832
4879
|
capturePromises: [],
|
|
4833
4880
|
failedLocators: [],
|
|
4834
|
-
|
|
4881
|
+
probedLocations: /* @__PURE__ */ new Set(),
|
|
4835
4882
|
lastActivePage: null,
|
|
4836
4883
|
testInfo: null,
|
|
4837
4884
|
stashedWebVitals: null,
|
|
@@ -5058,7 +5105,6 @@ var INSTRUMENTED_CONTEXTS = /* @__PURE__ */ new WeakSet();
|
|
|
5058
5105
|
var PATCHED_BROWSERS = /* @__PURE__ */ new WeakSet();
|
|
5059
5106
|
var CHAIN_METHOD_SET = new Set(CHAIN_METHODS);
|
|
5060
5107
|
var ACTION_METHOD_SET = new Set(ACTION_METHODS);
|
|
5061
|
-
var FORM_FIELD_TAGS = /* @__PURE__ */ new Set(["input", "select", "textarea"]);
|
|
5062
5108
|
var CAPTURED_ATTRS_ARG = {
|
|
5063
5109
|
keep: [...CAPTURED_ATTRIBUTES],
|
|
5064
5110
|
tagRoles: TAG_TO_ROLE,
|
|
@@ -5067,11 +5113,32 @@ var CAPTURED_ATTRS_ARG = {
|
|
|
5067
5113
|
// special-cased logic in the probe, so add them explicitly).
|
|
5068
5114
|
roleSources: [.../* @__PURE__ */ new Set(["[role]", "input", "select", ...Object.keys(TAG_TO_ROLE)])].join(","),
|
|
5069
5115
|
// The reporter always wants ancestor-anchored alternatives (the picker's
|
|
5070
|
-
// anchors step and generateAnchoredAlternatives both need them)
|
|
5071
|
-
//
|
|
5116
|
+
// anchors step and generateAnchoredAlternatives both need them). `labelText`
|
|
5117
|
+
// is what names a form field, and the probe reads it from `el.labels` in the
|
|
5118
|
+
// same pass — so asking for it here is free and settles the accessible name
|
|
5119
|
+
// of a labeled field without a second round trip (`exactAccessibleName`).
|
|
5072
5120
|
includeStructural: true,
|
|
5073
|
-
includeLabelText:
|
|
5121
|
+
includeLabelText: true
|
|
5074
5122
|
};
|
|
5123
|
+
var PROBE_GLOBAL = "__piwiProbeElement";
|
|
5124
|
+
var PROBE_INIT_SCRIPT = `(() => {
|
|
5125
|
+
const probe = ${probeElementAttrs.toString()};
|
|
5126
|
+
const arg = ${JSON.stringify(CAPTURED_ATTRS_ARG)};
|
|
5127
|
+
globalThis[${JSON.stringify(PROBE_GLOBAL)}] = (el) => probe(el, arg);
|
|
5128
|
+
})();`;
|
|
5129
|
+
var PROBE_UNSEEDED_PAGES = /* @__PURE__ */ new WeakSet();
|
|
5130
|
+
function probeElement(page, target) {
|
|
5131
|
+
const shipSource = () => target.evaluate(probeElementAttrs, CAPTURED_ATTRS_ARG);
|
|
5132
|
+
if (!page || PROBE_UNSEEDED_PAGES.has(page)) return shipSource();
|
|
5133
|
+
return target.evaluate((el, name) => {
|
|
5134
|
+
const seeded = globalThis[name];
|
|
5135
|
+
return typeof seeded === "function" ? seeded(el) : null;
|
|
5136
|
+
}, PROBE_GLOBAL).then((attrs) => {
|
|
5137
|
+
if (attrs) return attrs;
|
|
5138
|
+
PROBE_UNSEEDED_PAGES.add(page);
|
|
5139
|
+
return shipSource();
|
|
5140
|
+
});
|
|
5141
|
+
}
|
|
5075
5142
|
async function ariaSnapshotBestEffort(target, timeout) {
|
|
5076
5143
|
if (typeof target.ariaSnapshot !== "function") return null;
|
|
5077
5144
|
try {
|
|
@@ -5090,8 +5157,8 @@ async function ariaSnapshotBestEffort(target, timeout) {
|
|
|
5090
5157
|
}
|
|
5091
5158
|
}
|
|
5092
5159
|
}
|
|
5093
|
-
function startElementCapture(sink, target, seq, callerLocation, used) {
|
|
5094
|
-
const probe =
|
|
5160
|
+
function startElementCapture(sink, page, target, seq, callerLocation, used) {
|
|
5161
|
+
const probe = probeElement(page, target);
|
|
5095
5162
|
const settledProbe = probe.then(
|
|
5096
5163
|
() => void 0,
|
|
5097
5164
|
() => void 0
|
|
@@ -5110,8 +5177,9 @@ function startElementCapture(sink, target, seq, callerLocation, used) {
|
|
|
5110
5177
|
]);
|
|
5111
5178
|
const role = resolveAriaRole({ ...attrs, accessibleName: null });
|
|
5112
5179
|
const isFormField = FORM_FIELD_TAGS.has(attrs.tagName);
|
|
5113
|
-
const
|
|
5114
|
-
const
|
|
5180
|
+
const exactName = exactAccessibleName(attrs, role);
|
|
5181
|
+
const aria = exactName === null && (role || isFormField) ? await ariaSnapshotBestEffort(target, 500) : null;
|
|
5182
|
+
const accessibleName = exactName ?? (extractAccessibleName(aria) || approximateAccessibleName({ ...attrs, accessibleName: null }));
|
|
5115
5183
|
sink.capturedLocators[seq] = {
|
|
5116
5184
|
location: callerLocation,
|
|
5117
5185
|
used,
|
|
@@ -5131,6 +5199,7 @@ function startElementCapture(sink, target, seq, callerLocation, used) {
|
|
|
5131
5199
|
alternatives: generateAlternatives({ ...attrs, accessibleName })
|
|
5132
5200
|
};
|
|
5133
5201
|
} catch {
|
|
5202
|
+
if (callerLocation) sink.probedLocations.delete(callerLocation);
|
|
5134
5203
|
} finally {
|
|
5135
5204
|
clearTimeout(deadline);
|
|
5136
5205
|
}
|
|
@@ -5167,17 +5236,17 @@ function wrapLocator(page, locator, originMethod, originArgs) {
|
|
|
5167
5236
|
args: originArgs,
|
|
5168
5237
|
raw: `${originMethod}(${JSON.stringify(originArgs)})`
|
|
5169
5238
|
};
|
|
5170
|
-
const
|
|
5239
|
+
const alreadyProbed = callerLocation !== null && sink.probedLocations.has(callerLocation);
|
|
5171
5240
|
let seq = -1;
|
|
5172
|
-
if (!
|
|
5241
|
+
if (!alreadyProbed) {
|
|
5173
5242
|
seq = sink.capturedLocators.length;
|
|
5174
5243
|
sink.capturedLocators.push({ location: callerLocation, used, element: null, alternatives: [] });
|
|
5175
5244
|
}
|
|
5176
5245
|
const result = await fn.apply(target, callArgs);
|
|
5177
5246
|
const matches = result?.matches;
|
|
5178
|
-
if (matches === true && !
|
|
5179
|
-
if (callerLocation) sink.
|
|
5180
|
-
startElementCapture(sink, target, seq, callerLocation, used);
|
|
5247
|
+
if (matches === true && !alreadyProbed) {
|
|
5248
|
+
if (callerLocation) sink.probedLocations.add(callerLocation);
|
|
5249
|
+
startElementCapture(sink, page, target, seq, callerLocation, used);
|
|
5181
5250
|
} else if (matches === false) {
|
|
5182
5251
|
sink.failedLocators.push({ method: originMethod, args: originArgs, location: callerLocation });
|
|
5183
5252
|
}
|
|
@@ -5189,19 +5258,23 @@ function wrapLocator(page, locator, originMethod, originArgs) {
|
|
|
5189
5258
|
const sink = currentSink;
|
|
5190
5259
|
if (!sink) return fn.apply(target, callArgs);
|
|
5191
5260
|
sink.lastActivePage = page;
|
|
5192
|
-
const seq = sink.capturedLocators.length;
|
|
5193
5261
|
const callerLocation = captureCallerLocation();
|
|
5194
5262
|
const used = {
|
|
5195
5263
|
method: originMethod,
|
|
5196
5264
|
args: originArgs,
|
|
5197
5265
|
raw: `${originMethod}(${JSON.stringify(originArgs)})`
|
|
5198
5266
|
};
|
|
5199
|
-
sink.
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5267
|
+
const alreadyProbed = callerLocation !== null && sink.probedLocations.has(callerLocation);
|
|
5268
|
+
let seq = -1;
|
|
5269
|
+
if (!alreadyProbed) {
|
|
5270
|
+
seq = sink.capturedLocators.length;
|
|
5271
|
+
sink.capturedLocators.push({
|
|
5272
|
+
location: callerLocation,
|
|
5273
|
+
used,
|
|
5274
|
+
element: null,
|
|
5275
|
+
alternatives: []
|
|
5276
|
+
});
|
|
5277
|
+
}
|
|
5205
5278
|
let result;
|
|
5206
5279
|
try {
|
|
5207
5280
|
result = await fn.apply(target, callArgs);
|
|
@@ -5209,7 +5282,10 @@ function wrapLocator(page, locator, originMethod, originArgs) {
|
|
|
5209
5282
|
sink.failedLocators.push({ method: originMethod, args: originArgs, location: callerLocation });
|
|
5210
5283
|
throw error;
|
|
5211
5284
|
}
|
|
5212
|
-
|
|
5285
|
+
if (!alreadyProbed) {
|
|
5286
|
+
if (callerLocation) sink.probedLocations.add(callerLocation);
|
|
5287
|
+
startElementCapture(sink, page, target, seq, callerLocation, used);
|
|
5288
|
+
}
|
|
5213
5289
|
return result;
|
|
5214
5290
|
};
|
|
5215
5291
|
}
|
|
@@ -5242,6 +5318,9 @@ function instrumentPage(page) {
|
|
|
5242
5318
|
return wrapLocator(page, original(...args), method, args);
|
|
5243
5319
|
};
|
|
5244
5320
|
}
|
|
5321
|
+
if (typeof page.on === "function") {
|
|
5322
|
+
page.on("framenavigated", () => PROBE_UNSEEDED_PAGES.delete(page));
|
|
5323
|
+
}
|
|
5245
5324
|
}
|
|
5246
5325
|
page.on("console", (msg) => {
|
|
5247
5326
|
const sink = currentSink;
|
|
@@ -5306,6 +5385,10 @@ function instrumentPage(page) {
|
|
|
5306
5385
|
function instrumentContext(context) {
|
|
5307
5386
|
if (!context || INSTRUMENTED_CONTEXTS.has(context)) return;
|
|
5308
5387
|
INSTRUMENTED_CONTEXTS.add(context);
|
|
5388
|
+
if (process.env.PIWI_CAPTURE_LOCATORS !== "false" && typeof context.addInitScript === "function") {
|
|
5389
|
+
void Promise.resolve(context.addInitScript({ content: PROBE_INIT_SCRIPT })).catch(() => {
|
|
5390
|
+
});
|
|
5391
|
+
}
|
|
5309
5392
|
const originalNewPage = context.newPage.bind(context);
|
|
5310
5393
|
context.newPage = async (...args) => {
|
|
5311
5394
|
const page = await originalNewPage(...args);
|
|
@@ -309,7 +309,16 @@ function probeElementAttrs(el, arg) {
|
|
|
309
309
|
}
|
|
310
310
|
}
|
|
311
311
|
const hasLabel = !!(el.labels && el.labels.length > 0);
|
|
312
|
-
|
|
312
|
+
let labelText;
|
|
313
|
+
if (includeLabelText) {
|
|
314
|
+
labelText = null;
|
|
315
|
+
try {
|
|
316
|
+
const label = hasLabel ? el.labels[0] : null;
|
|
317
|
+
labelText = (label && label.textContent || "").replace(/\s+/g, " ").trim().slice(0, 120) || null;
|
|
318
|
+
} catch {
|
|
319
|
+
labelText = null;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
313
322
|
return {
|
|
314
323
|
tagName: el.tagName?.toLowerCase?.() ?? "unknown",
|
|
315
324
|
attributes: attrMap,
|
|
@@ -1013,6 +1022,7 @@ var CAPTURED_ATTRIBUTES = [
|
|
|
1013
1022
|
"alt",
|
|
1014
1023
|
"title",
|
|
1015
1024
|
"aria-label",
|
|
1025
|
+
"aria-labelledby",
|
|
1016
1026
|
"aria-level",
|
|
1017
1027
|
"role",
|
|
1018
1028
|
"type",
|
|
@@ -1548,6 +1558,43 @@ function extractAccessibleName(ariaSnapshot) {
|
|
|
1548
1558
|
if (match) return match[1];
|
|
1549
1559
|
return null;
|
|
1550
1560
|
}
|
|
1561
|
+
var FORM_FIELD_TAGS = /* @__PURE__ */ new Set(["input", "select", "textarea"]);
|
|
1562
|
+
var NAME_FROM_CONTENT_ROLES = /* @__PURE__ */ new Set([
|
|
1563
|
+
"button",
|
|
1564
|
+
"cell",
|
|
1565
|
+
"checkbox",
|
|
1566
|
+
"columnheader",
|
|
1567
|
+
"gridcell",
|
|
1568
|
+
"heading",
|
|
1569
|
+
"link",
|
|
1570
|
+
"menuitem",
|
|
1571
|
+
"menuitemcheckbox",
|
|
1572
|
+
"menuitemradio",
|
|
1573
|
+
"option",
|
|
1574
|
+
"radio",
|
|
1575
|
+
"row",
|
|
1576
|
+
"rowheader",
|
|
1577
|
+
"switch",
|
|
1578
|
+
"tab",
|
|
1579
|
+
"tooltip",
|
|
1580
|
+
"treeitem"
|
|
1581
|
+
]);
|
|
1582
|
+
var PROBE_TEXT_CAP = 80;
|
|
1583
|
+
var PROBE_LABEL_TEXT_CAP = 120;
|
|
1584
|
+
function exactAccessibleName(attrs, role) {
|
|
1585
|
+
if (attrs.attributes["aria-labelledby"]) return null;
|
|
1586
|
+
const ariaLabel = attrs.attributes["aria-label"];
|
|
1587
|
+
if (ariaLabel) return ariaLabel;
|
|
1588
|
+
if (FORM_FIELD_TAGS.has(attrs.tagName)) {
|
|
1589
|
+
const labelText = attrs.labelText;
|
|
1590
|
+
return labelText && labelText.length < PROBE_LABEL_TEXT_CAP ? labelText : null;
|
|
1591
|
+
}
|
|
1592
|
+
if (role && NAME_FROM_CONTENT_ROLES.has(role)) {
|
|
1593
|
+
const text = attrs.textContent;
|
|
1594
|
+
return text && text.length < PROBE_TEXT_CAP ? text : null;
|
|
1595
|
+
}
|
|
1596
|
+
return null;
|
|
1597
|
+
}
|
|
1551
1598
|
var NAME_BASED_METHODS = /* @__PURE__ */ new Set([
|
|
1552
1599
|
"getByText",
|
|
1553
1600
|
"getByRole",
|
|
@@ -2009,7 +2056,7 @@ function createSink() {
|
|
|
2009
2056
|
capturedLocators: [],
|
|
2010
2057
|
capturePromises: [],
|
|
2011
2058
|
failedLocators: [],
|
|
2012
|
-
|
|
2059
|
+
probedLocations: /* @__PURE__ */ new Set(),
|
|
2013
2060
|
lastActivePage: null,
|
|
2014
2061
|
testInfo: null,
|
|
2015
2062
|
stashedWebVitals: null,
|
|
@@ -2236,7 +2283,6 @@ var INSTRUMENTED_CONTEXTS = /* @__PURE__ */ new WeakSet();
|
|
|
2236
2283
|
var PATCHED_BROWSERS = /* @__PURE__ */ new WeakSet();
|
|
2237
2284
|
var CHAIN_METHOD_SET = new Set(CHAIN_METHODS);
|
|
2238
2285
|
var ACTION_METHOD_SET = new Set(ACTION_METHODS);
|
|
2239
|
-
var FORM_FIELD_TAGS = /* @__PURE__ */ new Set(["input", "select", "textarea"]);
|
|
2240
2286
|
var CAPTURED_ATTRS_ARG = {
|
|
2241
2287
|
keep: [...CAPTURED_ATTRIBUTES],
|
|
2242
2288
|
tagRoles: TAG_TO_ROLE,
|
|
@@ -2245,11 +2291,32 @@ var CAPTURED_ATTRS_ARG = {
|
|
|
2245
2291
|
// special-cased logic in the probe, so add them explicitly).
|
|
2246
2292
|
roleSources: [.../* @__PURE__ */ new Set(["[role]", "input", "select", ...Object.keys(TAG_TO_ROLE)])].join(","),
|
|
2247
2293
|
// The reporter always wants ancestor-anchored alternatives (the picker's
|
|
2248
|
-
// anchors step and generateAnchoredAlternatives both need them)
|
|
2249
|
-
//
|
|
2294
|
+
// anchors step and generateAnchoredAlternatives both need them). `labelText`
|
|
2295
|
+
// is what names a form field, and the probe reads it from `el.labels` in the
|
|
2296
|
+
// same pass — so asking for it here is free and settles the accessible name
|
|
2297
|
+
// of a labeled field without a second round trip (`exactAccessibleName`).
|
|
2250
2298
|
includeStructural: true,
|
|
2251
|
-
includeLabelText:
|
|
2299
|
+
includeLabelText: true
|
|
2252
2300
|
};
|
|
2301
|
+
var PROBE_GLOBAL = "__piwiProbeElement";
|
|
2302
|
+
var PROBE_INIT_SCRIPT = `(() => {
|
|
2303
|
+
const probe = ${probeElementAttrs.toString()};
|
|
2304
|
+
const arg = ${JSON.stringify(CAPTURED_ATTRS_ARG)};
|
|
2305
|
+
globalThis[${JSON.stringify(PROBE_GLOBAL)}] = (el) => probe(el, arg);
|
|
2306
|
+
})();`;
|
|
2307
|
+
var PROBE_UNSEEDED_PAGES = /* @__PURE__ */ new WeakSet();
|
|
2308
|
+
function probeElement(page, target) {
|
|
2309
|
+
const shipSource = () => target.evaluate(probeElementAttrs, CAPTURED_ATTRS_ARG);
|
|
2310
|
+
if (!page || PROBE_UNSEEDED_PAGES.has(page)) return shipSource();
|
|
2311
|
+
return target.evaluate((el, name) => {
|
|
2312
|
+
const seeded = globalThis[name];
|
|
2313
|
+
return typeof seeded === "function" ? seeded(el) : null;
|
|
2314
|
+
}, PROBE_GLOBAL).then((attrs) => {
|
|
2315
|
+
if (attrs) return attrs;
|
|
2316
|
+
PROBE_UNSEEDED_PAGES.add(page);
|
|
2317
|
+
return shipSource();
|
|
2318
|
+
});
|
|
2319
|
+
}
|
|
2253
2320
|
async function ariaSnapshotBestEffort(target, timeout) {
|
|
2254
2321
|
if (typeof target.ariaSnapshot !== "function") return null;
|
|
2255
2322
|
try {
|
|
@@ -2268,8 +2335,8 @@ async function ariaSnapshotBestEffort(target, timeout) {
|
|
|
2268
2335
|
}
|
|
2269
2336
|
}
|
|
2270
2337
|
}
|
|
2271
|
-
function startElementCapture(sink, target, seq, callerLocation, used) {
|
|
2272
|
-
const probe =
|
|
2338
|
+
function startElementCapture(sink, page, target, seq, callerLocation, used) {
|
|
2339
|
+
const probe = probeElement(page, target);
|
|
2273
2340
|
const settledProbe = probe.then(
|
|
2274
2341
|
() => void 0,
|
|
2275
2342
|
() => void 0
|
|
@@ -2288,8 +2355,9 @@ function startElementCapture(sink, target, seq, callerLocation, used) {
|
|
|
2288
2355
|
]);
|
|
2289
2356
|
const role = resolveAriaRole({ ...attrs, accessibleName: null });
|
|
2290
2357
|
const isFormField = FORM_FIELD_TAGS.has(attrs.tagName);
|
|
2291
|
-
const
|
|
2292
|
-
const
|
|
2358
|
+
const exactName = exactAccessibleName(attrs, role);
|
|
2359
|
+
const aria = exactName === null && (role || isFormField) ? await ariaSnapshotBestEffort(target, 500) : null;
|
|
2360
|
+
const accessibleName = exactName ?? (extractAccessibleName(aria) || approximateAccessibleName({ ...attrs, accessibleName: null }));
|
|
2293
2361
|
sink.capturedLocators[seq] = {
|
|
2294
2362
|
location: callerLocation,
|
|
2295
2363
|
used,
|
|
@@ -2309,6 +2377,7 @@ function startElementCapture(sink, target, seq, callerLocation, used) {
|
|
|
2309
2377
|
alternatives: generateAlternatives({ ...attrs, accessibleName })
|
|
2310
2378
|
};
|
|
2311
2379
|
} catch {
|
|
2380
|
+
if (callerLocation) sink.probedLocations.delete(callerLocation);
|
|
2312
2381
|
} finally {
|
|
2313
2382
|
clearTimeout(deadline);
|
|
2314
2383
|
}
|
|
@@ -2345,17 +2414,17 @@ function wrapLocator(page, locator, originMethod, originArgs) {
|
|
|
2345
2414
|
args: originArgs,
|
|
2346
2415
|
raw: `${originMethod}(${JSON.stringify(originArgs)})`
|
|
2347
2416
|
};
|
|
2348
|
-
const
|
|
2417
|
+
const alreadyProbed = callerLocation !== null && sink.probedLocations.has(callerLocation);
|
|
2349
2418
|
let seq = -1;
|
|
2350
|
-
if (!
|
|
2419
|
+
if (!alreadyProbed) {
|
|
2351
2420
|
seq = sink.capturedLocators.length;
|
|
2352
2421
|
sink.capturedLocators.push({ location: callerLocation, used, element: null, alternatives: [] });
|
|
2353
2422
|
}
|
|
2354
2423
|
const result = await fn.apply(target, callArgs);
|
|
2355
2424
|
const matches = result?.matches;
|
|
2356
|
-
if (matches === true && !
|
|
2357
|
-
if (callerLocation) sink.
|
|
2358
|
-
startElementCapture(sink, target, seq, callerLocation, used);
|
|
2425
|
+
if (matches === true && !alreadyProbed) {
|
|
2426
|
+
if (callerLocation) sink.probedLocations.add(callerLocation);
|
|
2427
|
+
startElementCapture(sink, page, target, seq, callerLocation, used);
|
|
2359
2428
|
} else if (matches === false) {
|
|
2360
2429
|
sink.failedLocators.push({ method: originMethod, args: originArgs, location: callerLocation });
|
|
2361
2430
|
}
|
|
@@ -2367,19 +2436,23 @@ function wrapLocator(page, locator, originMethod, originArgs) {
|
|
|
2367
2436
|
const sink = currentSink;
|
|
2368
2437
|
if (!sink) return fn.apply(target, callArgs);
|
|
2369
2438
|
sink.lastActivePage = page;
|
|
2370
|
-
const seq = sink.capturedLocators.length;
|
|
2371
2439
|
const callerLocation = captureCallerLocation();
|
|
2372
2440
|
const used = {
|
|
2373
2441
|
method: originMethod,
|
|
2374
2442
|
args: originArgs,
|
|
2375
2443
|
raw: `${originMethod}(${JSON.stringify(originArgs)})`
|
|
2376
2444
|
};
|
|
2377
|
-
sink.
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2445
|
+
const alreadyProbed = callerLocation !== null && sink.probedLocations.has(callerLocation);
|
|
2446
|
+
let seq = -1;
|
|
2447
|
+
if (!alreadyProbed) {
|
|
2448
|
+
seq = sink.capturedLocators.length;
|
|
2449
|
+
sink.capturedLocators.push({
|
|
2450
|
+
location: callerLocation,
|
|
2451
|
+
used,
|
|
2452
|
+
element: null,
|
|
2453
|
+
alternatives: []
|
|
2454
|
+
});
|
|
2455
|
+
}
|
|
2383
2456
|
let result;
|
|
2384
2457
|
try {
|
|
2385
2458
|
result = await fn.apply(target, callArgs);
|
|
@@ -2387,7 +2460,10 @@ function wrapLocator(page, locator, originMethod, originArgs) {
|
|
|
2387
2460
|
sink.failedLocators.push({ method: originMethod, args: originArgs, location: callerLocation });
|
|
2388
2461
|
throw error;
|
|
2389
2462
|
}
|
|
2390
|
-
|
|
2463
|
+
if (!alreadyProbed) {
|
|
2464
|
+
if (callerLocation) sink.probedLocations.add(callerLocation);
|
|
2465
|
+
startElementCapture(sink, page, target, seq, callerLocation, used);
|
|
2466
|
+
}
|
|
2391
2467
|
return result;
|
|
2392
2468
|
};
|
|
2393
2469
|
}
|
|
@@ -2420,6 +2496,9 @@ function instrumentPage(page) {
|
|
|
2420
2496
|
return wrapLocator(page, original(...args), method, args);
|
|
2421
2497
|
};
|
|
2422
2498
|
}
|
|
2499
|
+
if (typeof page.on === "function") {
|
|
2500
|
+
page.on("framenavigated", () => PROBE_UNSEEDED_PAGES.delete(page));
|
|
2501
|
+
}
|
|
2423
2502
|
}
|
|
2424
2503
|
page.on("console", (msg) => {
|
|
2425
2504
|
const sink = currentSink;
|
|
@@ -2484,6 +2563,10 @@ function instrumentPage(page) {
|
|
|
2484
2563
|
function instrumentContext(context) {
|
|
2485
2564
|
if (!context || INSTRUMENTED_CONTEXTS.has(context)) return;
|
|
2486
2565
|
INSTRUMENTED_CONTEXTS.add(context);
|
|
2566
|
+
if (process.env.PIWI_CAPTURE_LOCATORS !== "false" && typeof context.addInitScript === "function") {
|
|
2567
|
+
void Promise.resolve(context.addInitScript({ content: PROBE_INIT_SCRIPT })).catch(() => {
|
|
2568
|
+
});
|
|
2569
|
+
}
|
|
2487
2570
|
const originalNewPage = context.newPage.bind(context);
|
|
2488
2571
|
context.newPage = async (...args) => {
|
|
2489
2572
|
const page = await originalNewPage(...args);
|
|
@@ -83,6 +83,33 @@ declare const EXPECT_CAPTURE_EXPRESSIONS: ReadonlySet<string>;
|
|
|
83
83
|
* Returns the first quoted string after the role, or null if none found.
|
|
84
84
|
*/
|
|
85
85
|
declare function extractAccessibleName(ariaSnapshot: string | null): string | null;
|
|
86
|
+
/** Tags whose accessible name comes from an associated `<label>`. */
|
|
87
|
+
declare const FORM_FIELD_TAGS: ReadonlySet<string>;
|
|
88
|
+
/**
|
|
89
|
+
* Roles whose accessible name is computed from the element's own content. For
|
|
90
|
+
* these, an element carrying neither `aria-label` nor `aria-labelledby` is
|
|
91
|
+
* named by its trimmed text — which the probe already returns.
|
|
92
|
+
*/
|
|
93
|
+
declare const NAME_FROM_CONTENT_ROLES: ReadonlySet<string>;
|
|
94
|
+
/** The subset of a probe result the accessible-name shortcut reads. */
|
|
95
|
+
interface NameSource {
|
|
96
|
+
tagName: string;
|
|
97
|
+
attributes: Record<string, string | null>;
|
|
98
|
+
textContent: string | null;
|
|
99
|
+
labelText?: string | null;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The accessible name when the probed attributes already settle it, or null
|
|
103
|
+
* when only the browser can. Capture consults this before spending an
|
|
104
|
+
* `ariaSnapshot()` round trip on an element: in each case below the name is
|
|
105
|
+
* what the accessible-name computation would return anyway, so the round trip
|
|
106
|
+
* would buy nothing.
|
|
107
|
+
*
|
|
108
|
+
* Deliberately narrow — anything outside these cases (an `aria-labelledby`
|
|
109
|
+
* reference, an image named by `alt`, an element with no text) still goes to
|
|
110
|
+
* the browser, because a wrong name here becomes a wrong `getByRole` locator.
|
|
111
|
+
*/
|
|
112
|
+
declare function exactAccessibleName(attrs: NameSource, role: string | null): string | null;
|
|
86
113
|
/** A failed locator action, used to suggest a fresh locator from the live page. */
|
|
87
114
|
interface FailedLocatorInfo {
|
|
88
115
|
method: string;
|
|
@@ -126,4 +153,4 @@ declare function suggestLocatorsFromAria(failed: FailedLocatorInfo, ariaSnapshot
|
|
|
126
153
|
*/
|
|
127
154
|
declare function captureCallerLocation(stack?: string): string | null;
|
|
128
155
|
|
|
129
|
-
export { ACTION_METHODS, CHAIN_METHODS, EXPECT_CAPTURE_EXPRESSIONS, EXPECT_METHOD, type FailedLocatorInfo, LOCATOR_CREATING_CHAINS, LOCATOR_METHODS, type LocatorSuggestion, captureCallerLocation, dedupeSnapshotsByLocation, extractAccessibleName, renderFailing, suggestLocatorsFromAria };
|
|
156
|
+
export { ACTION_METHODS, CHAIN_METHODS, EXPECT_CAPTURE_EXPRESSIONS, EXPECT_METHOD, FORM_FIELD_TAGS, type FailedLocatorInfo, LOCATOR_CREATING_CHAINS, LOCATOR_METHODS, type LocatorSuggestion, NAME_FROM_CONTENT_ROLES, type NameSource, captureCallerLocation, dedupeSnapshotsByLocation, exactAccessibleName, extractAccessibleName, renderFailing, suggestLocatorsFromAria };
|
|
@@ -35,14 +35,17 @@ __export(locator_healing_exports, {
|
|
|
35
35
|
CHAIN_METHODS: () => CHAIN_METHODS,
|
|
36
36
|
EXPECT_CAPTURE_EXPRESSIONS: () => EXPECT_CAPTURE_EXPRESSIONS,
|
|
37
37
|
EXPECT_METHOD: () => EXPECT_METHOD,
|
|
38
|
+
FORM_FIELD_TAGS: () => FORM_FIELD_TAGS,
|
|
38
39
|
INPUT_TYPE_TO_ROLE: () => INPUT_TYPE_TO_ROLE,
|
|
39
40
|
LOCATOR_CREATING_CHAINS: () => LOCATOR_CREATING_CHAINS,
|
|
40
41
|
LOCATOR_METHODS: () => LOCATOR_METHODS,
|
|
42
|
+
NAME_FROM_CONTENT_ROLES: () => NAME_FROM_CONTENT_ROLES,
|
|
41
43
|
TAG_TO_ROLE: () => TAG_TO_ROLE,
|
|
42
44
|
approximateAccessibleName: () => approximateAccessibleName,
|
|
43
45
|
captureCallerLocation: () => captureCallerLocation,
|
|
44
46
|
classifyCssStability: () => classifyCssStability,
|
|
45
47
|
dedupeSnapshotsByLocation: () => dedupeSnapshotsByLocation,
|
|
48
|
+
exactAccessibleName: () => exactAccessibleName,
|
|
46
49
|
extractAccessibleName: () => extractAccessibleName,
|
|
47
50
|
generateAlternatives: () => generateAlternatives,
|
|
48
51
|
headingLevel: () => headingLevel,
|
|
@@ -195,6 +198,7 @@ var CAPTURED_ATTRIBUTES = [
|
|
|
195
198
|
"alt",
|
|
196
199
|
"title",
|
|
197
200
|
"aria-label",
|
|
201
|
+
"aria-labelledby",
|
|
198
202
|
"aria-level",
|
|
199
203
|
"role",
|
|
200
204
|
"type",
|
|
@@ -573,6 +577,43 @@ function extractAccessibleName(ariaSnapshot) {
|
|
|
573
577
|
if (match) return match[1];
|
|
574
578
|
return null;
|
|
575
579
|
}
|
|
580
|
+
var FORM_FIELD_TAGS = /* @__PURE__ */ new Set(["input", "select", "textarea"]);
|
|
581
|
+
var NAME_FROM_CONTENT_ROLES = /* @__PURE__ */ new Set([
|
|
582
|
+
"button",
|
|
583
|
+
"cell",
|
|
584
|
+
"checkbox",
|
|
585
|
+
"columnheader",
|
|
586
|
+
"gridcell",
|
|
587
|
+
"heading",
|
|
588
|
+
"link",
|
|
589
|
+
"menuitem",
|
|
590
|
+
"menuitemcheckbox",
|
|
591
|
+
"menuitemradio",
|
|
592
|
+
"option",
|
|
593
|
+
"radio",
|
|
594
|
+
"row",
|
|
595
|
+
"rowheader",
|
|
596
|
+
"switch",
|
|
597
|
+
"tab",
|
|
598
|
+
"tooltip",
|
|
599
|
+
"treeitem"
|
|
600
|
+
]);
|
|
601
|
+
var PROBE_TEXT_CAP = 80;
|
|
602
|
+
var PROBE_LABEL_TEXT_CAP = 120;
|
|
603
|
+
function exactAccessibleName(attrs, role) {
|
|
604
|
+
if (attrs.attributes["aria-labelledby"]) return null;
|
|
605
|
+
const ariaLabel = attrs.attributes["aria-label"];
|
|
606
|
+
if (ariaLabel) return ariaLabel;
|
|
607
|
+
if (FORM_FIELD_TAGS.has(attrs.tagName)) {
|
|
608
|
+
const labelText = attrs.labelText;
|
|
609
|
+
return labelText && labelText.length < PROBE_LABEL_TEXT_CAP ? labelText : null;
|
|
610
|
+
}
|
|
611
|
+
if (role && NAME_FROM_CONTENT_ROLES.has(role)) {
|
|
612
|
+
const text = attrs.textContent;
|
|
613
|
+
return text && text.length < PROBE_TEXT_CAP ? text : null;
|
|
614
|
+
}
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
576
617
|
var NAME_BASED_METHODS = /* @__PURE__ */ new Set([
|
|
577
618
|
"getByText",
|
|
578
619
|
"getByRole",
|
|
@@ -707,14 +748,17 @@ function captureCallerLocation(stack = new Error().stack ?? "") {
|
|
|
707
748
|
CHAIN_METHODS,
|
|
708
749
|
EXPECT_CAPTURE_EXPRESSIONS,
|
|
709
750
|
EXPECT_METHOD,
|
|
751
|
+
FORM_FIELD_TAGS,
|
|
710
752
|
INPUT_TYPE_TO_ROLE,
|
|
711
753
|
LOCATOR_CREATING_CHAINS,
|
|
712
754
|
LOCATOR_METHODS,
|
|
755
|
+
NAME_FROM_CONTENT_ROLES,
|
|
713
756
|
TAG_TO_ROLE,
|
|
714
757
|
approximateAccessibleName,
|
|
715
758
|
captureCallerLocation,
|
|
716
759
|
classifyCssStability,
|
|
717
760
|
dedupeSnapshotsByLocation,
|
|
761
|
+
exactAccessibleName,
|
|
718
762
|
extractAccessibleName,
|
|
719
763
|
generateAlternatives,
|
|
720
764
|
headingLevel,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@piwitests/reporter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "Playwright reporter that streams results, traces and HTML reports to a Piwi Dashboard instance",
|
|
5
5
|
"url": "https://github.com/PiwiTests/platform",
|
|
6
6
|
"homepage": "https://piwitests.github.io",
|
|
@@ -53,6 +53,8 @@
|
|
|
53
53
|
"reporter:test:integration": "npm run reporter:build && playwright test --config=tests/integration/playwright.config.ts",
|
|
54
54
|
"reporter:test:integration:ai": "npm run reporter:build && node tests/integration/ai/run.mjs",
|
|
55
55
|
"reporter:test:integration:ai:live": "npm run reporter:build && playwright test --config=tests/integration/ai/live.config.ts",
|
|
56
|
+
"reporter:bench": "npm run reporter:build && node tests/bench/run.mjs",
|
|
57
|
+
"reporter:bench:micro": "vitest bench --run",
|
|
56
58
|
"test": "npm run reporter:test",
|
|
57
59
|
"prepublishOnly": "npm run reporter:build"
|
|
58
60
|
},
|
|
@@ -60,6 +62,9 @@
|
|
|
60
62
|
"dist/",
|
|
61
63
|
"templates/"
|
|
62
64
|
],
|
|
65
|
+
"engines": {
|
|
66
|
+
"node": ">=20"
|
|
67
|
+
},
|
|
63
68
|
"peerDependencies": {
|
|
64
69
|
"@playwright/test": "^1.61.1"
|
|
65
70
|
},
|