@piwitests/reporter 0.18.1 → 0.19.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/README.md +1 -1
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +205 -0
- package/dist/global-setup-module.js +59 -12
- package/dist/index.d.ts +10 -2
- package/dist/index.js +389 -162
- package/dist/internal/capture/capture-fixtures.js +115 -45
- package/dist/internal/capture/locator-healing.d.ts +29 -1
- package/dist/internal/capture/locator-healing.js +38 -0
- package/package.json +8 -3
|
@@ -481,6 +481,34 @@ var ACTION_METHODS = [
|
|
|
481
481
|
"waitFor"
|
|
482
482
|
];
|
|
483
483
|
var LOCATOR_CREATING_CHAINS = new Set(LOCATOR_METHODS);
|
|
484
|
+
var EXPECT_METHOD = "_expect";
|
|
485
|
+
var EXPECT_CAPTURE_EXPRESSIONS = /* @__PURE__ */ new Set([
|
|
486
|
+
"to.be.attached",
|
|
487
|
+
"to.be.checked",
|
|
488
|
+
"to.be.disabled",
|
|
489
|
+
"to.be.editable",
|
|
490
|
+
"to.be.empty",
|
|
491
|
+
"to.be.enabled",
|
|
492
|
+
"to.be.focused",
|
|
493
|
+
"to.be.in.viewport",
|
|
494
|
+
"to.be.readonly",
|
|
495
|
+
"to.be.visible",
|
|
496
|
+
"to.contain.class",
|
|
497
|
+
"to.contain.text",
|
|
498
|
+
"to.have.accessible.description",
|
|
499
|
+
"to.have.accessible.error.message",
|
|
500
|
+
"to.have.accessible.name",
|
|
501
|
+
"to.have.attribute",
|
|
502
|
+
"to.have.attribute.value",
|
|
503
|
+
"to.have.class",
|
|
504
|
+
"to.have.css",
|
|
505
|
+
"to.have.id",
|
|
506
|
+
"to.have.js.property",
|
|
507
|
+
"to.have.role",
|
|
508
|
+
"to.have.text",
|
|
509
|
+
"to.have.value",
|
|
510
|
+
"to.match.aria"
|
|
511
|
+
]);
|
|
484
512
|
function extractAccessibleName(ariaSnapshot) {
|
|
485
513
|
if (!ariaSnapshot) return null;
|
|
486
514
|
const match = ariaSnapshot.match(/- \w+ "([^"]+)"/);
|
|
@@ -567,6 +595,7 @@ function suggestLocatorsFromAria(failed, ariaSnapshot) {
|
|
|
567
595
|
function captureCallerLocation(stack = new Error().stack ?? "") {
|
|
568
596
|
const lines = stack.split("\n");
|
|
569
597
|
let prevWasCaptureModule = false;
|
|
598
|
+
let selfFile = null;
|
|
570
599
|
for (let i = 1; i < lines.length; i++) {
|
|
571
600
|
const line = lines[i].trim();
|
|
572
601
|
if (!line.startsWith("at")) continue;
|
|
@@ -585,6 +614,11 @@ function captureCallerLocation(stack = new Error().stack ?? "") {
|
|
|
585
614
|
prevWasCaptureModule = false;
|
|
586
615
|
continue;
|
|
587
616
|
}
|
|
617
|
+
if (selfFile === null || file === selfFile) {
|
|
618
|
+
selfFile ??= file;
|
|
619
|
+
prevWasCaptureModule = /[\\/]locator-healing\.[a-z]+$/i.test(file);
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
588
622
|
if (/[\\/]locator-healing\.[a-z]+$/i.test(file)) {
|
|
589
623
|
prevWasCaptureModule = true;
|
|
590
624
|
continue;
|
|
@@ -1519,6 +1553,7 @@ function createSink() {
|
|
|
1519
1553
|
capturedLocators: [],
|
|
1520
1554
|
capturePromises: [],
|
|
1521
1555
|
failedLocators: [],
|
|
1556
|
+
expectCapturedLocations: /* @__PURE__ */ new Set(),
|
|
1522
1557
|
lastActivePage: null,
|
|
1523
1558
|
testInfo: null,
|
|
1524
1559
|
stashedWebVitals: null,
|
|
@@ -1922,6 +1957,53 @@ function probeElementAttrs(el, arg) {
|
|
|
1922
1957
|
ancestors
|
|
1923
1958
|
};
|
|
1924
1959
|
}
|
|
1960
|
+
function startElementCapture(sink, target, seq, callerLocation, used) {
|
|
1961
|
+
const probe = target.evaluate(probeElementAttrs, CAPTURED_ATTRS_ARG);
|
|
1962
|
+
const settledProbe = probe.then(
|
|
1963
|
+
() => void 0,
|
|
1964
|
+
() => void 0
|
|
1965
|
+
);
|
|
1966
|
+
PENDING_PROBES.add(settledProbe);
|
|
1967
|
+
settledProbe.then(() => PENDING_PROBES.delete(settledProbe));
|
|
1968
|
+
sink.capturePromises.push(settledProbe);
|
|
1969
|
+
const resolveAttrs = (async () => {
|
|
1970
|
+
let deadline;
|
|
1971
|
+
try {
|
|
1972
|
+
const attrs = await Promise.race([
|
|
1973
|
+
probe,
|
|
1974
|
+
new Promise((_, reject) => {
|
|
1975
|
+
deadline = setTimeout(() => reject(new Error("locator capture timeout")), 500);
|
|
1976
|
+
})
|
|
1977
|
+
]);
|
|
1978
|
+
const role = resolveAriaRole({ ...attrs, accessibleName: null });
|
|
1979
|
+
const isFormField = FORM_FIELD_TAGS.has(attrs.tagName);
|
|
1980
|
+
const aria = role || isFormField ? await ariaSnapshotBestEffort(target, 500) : null;
|
|
1981
|
+
const accessibleName = extractAccessibleName(aria) || approximateAccessibleName({ ...attrs, accessibleName: null });
|
|
1982
|
+
sink.capturedLocators[seq] = {
|
|
1983
|
+
location: callerLocation,
|
|
1984
|
+
used,
|
|
1985
|
+
// hasLabel/selectorCounts inform alternative generation only —
|
|
1986
|
+
// keep the stored element to the wire shape. rolePosition and
|
|
1987
|
+
// ancestors ARE wire fields: the server's renamed-element match
|
|
1988
|
+
// uses them at heal time.
|
|
1989
|
+
element: {
|
|
1990
|
+
tagName: attrs.tagName,
|
|
1991
|
+
attributes: attrs.attributes,
|
|
1992
|
+
textContent: attrs.textContent,
|
|
1993
|
+
accessibleName,
|
|
1994
|
+
center: attrs.center,
|
|
1995
|
+
...attrs.rolePosition ? { rolePosition: attrs.rolePosition } : {},
|
|
1996
|
+
...attrs.ancestors && attrs.ancestors.length > 0 ? { ancestors: attrs.ancestors } : {}
|
|
1997
|
+
},
|
|
1998
|
+
alternatives: generateAlternatives({ ...attrs, accessibleName })
|
|
1999
|
+
};
|
|
2000
|
+
} catch {
|
|
2001
|
+
} finally {
|
|
2002
|
+
clearTimeout(deadline);
|
|
2003
|
+
}
|
|
2004
|
+
})();
|
|
2005
|
+
sink.capturePromises.push(resolveAttrs);
|
|
2006
|
+
}
|
|
1925
2007
|
function wrapLocator(page, locator, originMethod, originArgs) {
|
|
1926
2008
|
return new Proxy(locator, {
|
|
1927
2009
|
get(target, prop) {
|
|
@@ -1937,6 +2019,38 @@ function wrapLocator(page, locator, originMethod, originArgs) {
|
|
|
1937
2019
|
return wrapLocator(page, next, originMethod, originArgs);
|
|
1938
2020
|
};
|
|
1939
2021
|
}
|
|
2022
|
+
if (prop === EXPECT_METHOD) {
|
|
2023
|
+
return async (...callArgs) => {
|
|
2024
|
+
const sink = currentSink;
|
|
2025
|
+
const expression = typeof callArgs[0] === "string" ? callArgs[0] : "";
|
|
2026
|
+
const isNot = Boolean(callArgs[1]?.isNot);
|
|
2027
|
+
if (!sink || isNot || !EXPECT_CAPTURE_EXPRESSIONS.has(expression)) {
|
|
2028
|
+
return fn.apply(target, callArgs);
|
|
2029
|
+
}
|
|
2030
|
+
sink.lastActivePage = page;
|
|
2031
|
+
const callerLocation = captureCallerLocation();
|
|
2032
|
+
const used = {
|
|
2033
|
+
method: originMethod,
|
|
2034
|
+
args: originArgs,
|
|
2035
|
+
raw: `${originMethod}(${JSON.stringify(originArgs)})`
|
|
2036
|
+
};
|
|
2037
|
+
const alreadyCaptured = callerLocation !== null && sink.expectCapturedLocations.has(callerLocation);
|
|
2038
|
+
let seq = -1;
|
|
2039
|
+
if (!alreadyCaptured) {
|
|
2040
|
+
seq = sink.capturedLocators.length;
|
|
2041
|
+
sink.capturedLocators.push({ location: callerLocation, used, element: null, alternatives: [] });
|
|
2042
|
+
}
|
|
2043
|
+
const result = await fn.apply(target, callArgs);
|
|
2044
|
+
const matches = result?.matches;
|
|
2045
|
+
if (matches === true && !alreadyCaptured) {
|
|
2046
|
+
if (callerLocation) sink.expectCapturedLocations.add(callerLocation);
|
|
2047
|
+
startElementCapture(sink, target, seq, callerLocation, used);
|
|
2048
|
+
} else if (matches === false) {
|
|
2049
|
+
sink.failedLocators.push({ method: originMethod, args: originArgs, location: callerLocation });
|
|
2050
|
+
}
|
|
2051
|
+
return result;
|
|
2052
|
+
};
|
|
2053
|
+
}
|
|
1940
2054
|
if (!ACTION_METHOD_SET.has(prop)) return original;
|
|
1941
2055
|
return async (...callArgs) => {
|
|
1942
2056
|
const sink = currentSink;
|
|
@@ -1962,51 +2076,7 @@ function wrapLocator(page, locator, originMethod, originArgs) {
|
|
|
1962
2076
|
sink.failedLocators.push({ method: originMethod, args: originArgs, location: callerLocation });
|
|
1963
2077
|
throw error;
|
|
1964
2078
|
}
|
|
1965
|
-
|
|
1966
|
-
const settledProbe = probe.then(
|
|
1967
|
-
() => void 0,
|
|
1968
|
-
() => void 0
|
|
1969
|
-
);
|
|
1970
|
-
PENDING_PROBES.add(settledProbe);
|
|
1971
|
-
settledProbe.then(() => PENDING_PROBES.delete(settledProbe));
|
|
1972
|
-
sink.capturePromises.push(settledProbe);
|
|
1973
|
-
const resolveAttrs = (async () => {
|
|
1974
|
-
let deadline;
|
|
1975
|
-
try {
|
|
1976
|
-
const attrs = await Promise.race([
|
|
1977
|
-
probe,
|
|
1978
|
-
new Promise((_, reject) => {
|
|
1979
|
-
deadline = setTimeout(() => reject(new Error("locator capture timeout")), 500);
|
|
1980
|
-
})
|
|
1981
|
-
]);
|
|
1982
|
-
const role = resolveAriaRole({ ...attrs, accessibleName: null });
|
|
1983
|
-
const isFormField = FORM_FIELD_TAGS.has(attrs.tagName);
|
|
1984
|
-
const aria = role || isFormField ? await ariaSnapshotBestEffort(target, 500) : null;
|
|
1985
|
-
const accessibleName = extractAccessibleName(aria) || approximateAccessibleName({ ...attrs, accessibleName: null });
|
|
1986
|
-
sink.capturedLocators[seq] = {
|
|
1987
|
-
location: callerLocation,
|
|
1988
|
-
used,
|
|
1989
|
-
// hasLabel/selectorCounts inform alternative generation only —
|
|
1990
|
-
// keep the stored element to the wire shape. rolePosition and
|
|
1991
|
-
// ancestors ARE wire fields: the server's renamed-element match
|
|
1992
|
-
// uses them at heal time.
|
|
1993
|
-
element: {
|
|
1994
|
-
tagName: attrs.tagName,
|
|
1995
|
-
attributes: attrs.attributes,
|
|
1996
|
-
textContent: attrs.textContent,
|
|
1997
|
-
accessibleName,
|
|
1998
|
-
center: attrs.center,
|
|
1999
|
-
...attrs.rolePosition ? { rolePosition: attrs.rolePosition } : {},
|
|
2000
|
-
...attrs.ancestors && attrs.ancestors.length > 0 ? { ancestors: attrs.ancestors } : {}
|
|
2001
|
-
},
|
|
2002
|
-
alternatives: generateAlternatives({ ...attrs, accessibleName })
|
|
2003
|
-
};
|
|
2004
|
-
} catch {
|
|
2005
|
-
} finally {
|
|
2006
|
-
clearTimeout(deadline);
|
|
2007
|
-
}
|
|
2008
|
-
})();
|
|
2009
|
-
sink.capturePromises.push(resolveAttrs);
|
|
2079
|
+
startElementCapture(sink, target, seq, callerLocation, used);
|
|
2010
2080
|
return result;
|
|
2011
2081
|
};
|
|
2012
2082
|
}
|
|
@@ -43,6 +43,34 @@ declare const CHAIN_METHODS: string[];
|
|
|
43
43
|
declare const ACTION_METHODS: string[];
|
|
44
44
|
/** Chain methods that create a new locator scope (origin tracks the chain call). */
|
|
45
45
|
declare const LOCATOR_CREATING_CHAINS: ReadonlySet<string>;
|
|
46
|
+
/**
|
|
47
|
+
* The private `Locator` method every web-first assertion funnels through:
|
|
48
|
+
* `expect(locator).toBeVisible()` and friends all resolve to
|
|
49
|
+
* `locator._expect(expression, { isNot, timeout, … })` on the locator instance
|
|
50
|
+
* the test passed to `expect()` — which, under the capture fixtures, is the
|
|
51
|
+
* wrapped proxy. Intercepting it is what lets assertion-only locators build
|
|
52
|
+
* healing history. The funnel is unchanged across Playwright 1.40 → current;
|
|
53
|
+
* if a future version renames it the interception silently never fires and
|
|
54
|
+
* assertions behave exactly as without the proxy.
|
|
55
|
+
*/
|
|
56
|
+
declare const EXPECT_METHOD = "_expect";
|
|
57
|
+
/**
|
|
58
|
+
* Web-first assertion expressions whose PASS proves the target element
|
|
59
|
+
* resolved — the assertion-side equivalent of a successful action, and the
|
|
60
|
+
* capture hook for locators only ever used in `expect()`.
|
|
61
|
+
*
|
|
62
|
+
* Deliberately excluded:
|
|
63
|
+
* - absence assertions (`to.be.hidden`, `to.be.detached`): passing means the
|
|
64
|
+
* element may not exist at all — nothing to probe;
|
|
65
|
+
* - multi-element assertions (`to.have.count`, the `*.array` text/class
|
|
66
|
+
* forms, `to.have.values`): the single-element probe would only hit a
|
|
67
|
+
* strict-mode violation;
|
|
68
|
+
* - page-level assertions (`to.have.title`, `to.have.url`): they run on an
|
|
69
|
+
* internal `:root` locator that carries no healing value.
|
|
70
|
+
* Negated calls (`.not.…`, `isNot: true`) are skipped at the call site — a
|
|
71
|
+
* passing negative assertion proves nothing about the element resolving.
|
|
72
|
+
*/
|
|
73
|
+
declare const EXPECT_CAPTURE_EXPRESSIONS: ReadonlySet<string>;
|
|
46
74
|
/**
|
|
47
75
|
* Extract the accessible name from a YAML-like ariaSnapshot() output.
|
|
48
76
|
*
|
|
@@ -98,4 +126,4 @@ declare function suggestLocatorsFromAria(failed: FailedLocatorInfo, ariaSnapshot
|
|
|
98
126
|
*/
|
|
99
127
|
declare function captureCallerLocation(stack?: string): string | null;
|
|
100
128
|
|
|
101
|
-
export { ACTION_METHODS, CHAIN_METHODS, type FailedLocatorInfo, LOCATOR_CREATING_CHAINS, LOCATOR_METHODS, type LocatorSuggestion, captureCallerLocation, dedupeSnapshotsByLocation, extractAccessibleName, renderFailing, suggestLocatorsFromAria };
|
|
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 };
|
|
@@ -33,6 +33,8 @@ __export(locator_healing_exports, {
|
|
|
33
33
|
ACTION_METHODS: () => ACTION_METHODS,
|
|
34
34
|
CAPTURED_ATTRIBUTES: () => CAPTURED_ATTRIBUTES,
|
|
35
35
|
CHAIN_METHODS: () => CHAIN_METHODS,
|
|
36
|
+
EXPECT_CAPTURE_EXPRESSIONS: () => EXPECT_CAPTURE_EXPRESSIONS,
|
|
37
|
+
EXPECT_METHOD: () => EXPECT_METHOD,
|
|
36
38
|
INPUT_TYPE_TO_ROLE: () => INPUT_TYPE_TO_ROLE,
|
|
37
39
|
LOCATOR_CREATING_CHAINS: () => LOCATOR_CREATING_CHAINS,
|
|
38
40
|
LOCATOR_METHODS: () => LOCATOR_METHODS,
|
|
@@ -489,6 +491,34 @@ var ACTION_METHODS = [
|
|
|
489
491
|
"waitFor"
|
|
490
492
|
];
|
|
491
493
|
var LOCATOR_CREATING_CHAINS = new Set(LOCATOR_METHODS);
|
|
494
|
+
var EXPECT_METHOD = "_expect";
|
|
495
|
+
var EXPECT_CAPTURE_EXPRESSIONS = /* @__PURE__ */ new Set([
|
|
496
|
+
"to.be.attached",
|
|
497
|
+
"to.be.checked",
|
|
498
|
+
"to.be.disabled",
|
|
499
|
+
"to.be.editable",
|
|
500
|
+
"to.be.empty",
|
|
501
|
+
"to.be.enabled",
|
|
502
|
+
"to.be.focused",
|
|
503
|
+
"to.be.in.viewport",
|
|
504
|
+
"to.be.readonly",
|
|
505
|
+
"to.be.visible",
|
|
506
|
+
"to.contain.class",
|
|
507
|
+
"to.contain.text",
|
|
508
|
+
"to.have.accessible.description",
|
|
509
|
+
"to.have.accessible.error.message",
|
|
510
|
+
"to.have.accessible.name",
|
|
511
|
+
"to.have.attribute",
|
|
512
|
+
"to.have.attribute.value",
|
|
513
|
+
"to.have.class",
|
|
514
|
+
"to.have.css",
|
|
515
|
+
"to.have.id",
|
|
516
|
+
"to.have.js.property",
|
|
517
|
+
"to.have.role",
|
|
518
|
+
"to.have.text",
|
|
519
|
+
"to.have.value",
|
|
520
|
+
"to.match.aria"
|
|
521
|
+
]);
|
|
492
522
|
function extractAccessibleName(ariaSnapshot) {
|
|
493
523
|
if (!ariaSnapshot) return null;
|
|
494
524
|
const match = ariaSnapshot.match(/- \w+ "([^"]+)"/);
|
|
@@ -575,6 +605,7 @@ function suggestLocatorsFromAria(failed, ariaSnapshot) {
|
|
|
575
605
|
function captureCallerLocation(stack = new Error().stack ?? "") {
|
|
576
606
|
const lines = stack.split("\n");
|
|
577
607
|
let prevWasCaptureModule = false;
|
|
608
|
+
let selfFile = null;
|
|
578
609
|
for (let i = 1; i < lines.length; i++) {
|
|
579
610
|
const line = lines[i].trim();
|
|
580
611
|
if (!line.startsWith("at")) continue;
|
|
@@ -593,6 +624,11 @@ function captureCallerLocation(stack = new Error().stack ?? "") {
|
|
|
593
624
|
prevWasCaptureModule = false;
|
|
594
625
|
continue;
|
|
595
626
|
}
|
|
627
|
+
if (selfFile === null || file === selfFile) {
|
|
628
|
+
selfFile ??= file;
|
|
629
|
+
prevWasCaptureModule = /[\\/]locator-healing\.[a-z]+$/i.test(file);
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
596
632
|
if (/[\\/]locator-healing\.[a-z]+$/i.test(file)) {
|
|
597
633
|
prevWasCaptureModule = true;
|
|
598
634
|
continue;
|
|
@@ -621,6 +657,8 @@ function captureCallerLocation(stack = new Error().stack ?? "") {
|
|
|
621
657
|
ACTION_METHODS,
|
|
622
658
|
CAPTURED_ATTRIBUTES,
|
|
623
659
|
CHAIN_METHODS,
|
|
660
|
+
EXPECT_CAPTURE_EXPRESSIONS,
|
|
661
|
+
EXPECT_METHOD,
|
|
624
662
|
INPUT_TYPE_TO_ROLE,
|
|
625
663
|
LOCATOR_CREATING_CHAINS,
|
|
626
664
|
LOCATOR_METHODS,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@piwitests/reporter",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Playwright reporter
|
|
3
|
+
"version": "0.19.0",
|
|
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",
|
|
7
7
|
"repository": {
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
"type": "commonjs",
|
|
15
15
|
"main": "dist/index.js",
|
|
16
16
|
"types": "dist/index.d.ts",
|
|
17
|
+
"bin": {
|
|
18
|
+
"piwi": "dist/cli/index.js"
|
|
19
|
+
},
|
|
17
20
|
"exports": {
|
|
18
21
|
".": {
|
|
19
22
|
"types": "./dist/index.d.ts",
|
|
@@ -23,9 +26,11 @@
|
|
|
23
26
|
"./package.json": "./package.json"
|
|
24
27
|
},
|
|
25
28
|
"keywords": [
|
|
29
|
+
"ci",
|
|
30
|
+
"dashboard",
|
|
26
31
|
"playwright",
|
|
32
|
+
"quality-gate",
|
|
27
33
|
"reporter",
|
|
28
|
-
"dashboard",
|
|
29
34
|
"test-results"
|
|
30
35
|
],
|
|
31
36
|
"author": "piwitests",
|