@piwitests/reporter 0.18.2 → 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.
@@ -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
- const probe = target.evaluate(probeElementAttrs, CAPTURED_ATTRS_ARG);
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.18.2",
4
- "description": "Playwright reporter for sending test results to Piwi Dashboard",
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",