@piwitests/reporter 0.43.0 → 0.45.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/cli/index.js CHANGED
@@ -1752,8 +1752,12 @@ function samePage(a, b) {
1752
1752
  const kb = pageKey(b);
1753
1753
  return ka === null || kb === null || ka === kb;
1754
1754
  }
1755
+ function viewportLine(viewport) {
1756
+ return ` await page.setViewportSize({ width: ${Math.round(viewport.width)}, height: ${Math.round(viewport.height)} });`;
1757
+ }
1755
1758
  function renderSpec(session, options = {}) {
1756
1759
  const { steps } = session;
1760
+ const viewportAt = new Map((session.viewports ?? []).map((v) => [v.step, v]));
1757
1761
  const title = options.title ?? "recorded flow";
1758
1762
  const catalog = (options.catalog ?? []).filter(callIdentifiersAreSafe);
1759
1763
  const ctx = {
@@ -1781,6 +1785,8 @@ function renderSpec(session, options = {}) {
1781
1785
  let sawGoto = false;
1782
1786
  while (pos < steps.length) {
1783
1787
  const step = steps[pos];
1788
+ const viewport = pos > 0 ? viewportAt.get(pos) : void 0;
1789
+ if (viewport) bodyLines.push(viewportLine(viewport));
1784
1790
  stepStarts[pos] = bodyLines.length;
1785
1791
  if (step.action === "goto") {
1786
1792
  bodyLines.push(...renderRawStep(step, pos, ctx));
@@ -1788,7 +1794,8 @@ function renderSpec(session, options = {}) {
1788
1794
  pos++;
1789
1795
  continue;
1790
1796
  }
1791
- const match = catalog.length > 0 ? matchFunctionAt(steps, pos, catalog) : null;
1797
+ const found = catalog.length > 0 ? matchFunctionAt(steps, pos, catalog) : null;
1798
+ const match = found && ![...viewportAt.keys()].some((at) => at > pos && at <= Math.max(...found.matchedIndices)) ? found : null;
1792
1799
  if (match) {
1793
1800
  bodyLines.push(renderFunctionCall(match));
1794
1801
  usedEntries.push(match.entry);
@@ -1803,11 +1810,13 @@ function renderSpec(session, options = {}) {
1803
1810
  checkNextPage(pos);
1804
1811
  pos++;
1805
1812
  }
1806
- let bodyOffset = 0;
1807
- if (!sawGoto && session.startUrl) {
1808
- bodyLines.unshift(` await page.goto(${quote2(urlForCode(session.startUrl, ctx))});`);
1809
- bodyOffset = 1;
1810
- }
1813
+ const start = viewportAt.get(0);
1814
+ const opening = [
1815
+ ...start ? [viewportLine(start)] : [],
1816
+ ...!sawGoto && session.startUrl ? [` await page.goto(${quote2(urlForCode(session.startUrl, ctx))});`] : []
1817
+ ];
1818
+ bodyLines.unshift(...opening);
1819
+ const bodyOffset = opening.length;
1811
1820
  const importLines = renderImports(usedEntries);
1812
1821
  const instantiationLines = renderInstantiations(usedEntries);
1813
1822
  const failLine = expectFailLine(options);
@@ -1825,7 +1834,7 @@ function renderSpec(session, options = {}) {
1825
1834
  };
1826
1835
  }
1827
1836
  const details = renderDetails(options);
1828
- const opening = details.length > 0 ? [`test(${quote2(title)}, {`, ...details, `}, async ({ page }) => {`] : [`test(${quote2(title)}, async ({ page }) => {`];
1837
+ const testOpening = details.length > 0 ? [`test(${quote2(title)}, {`, ...details, `}, async ({ page }) => {`] : [`test(${quote2(title)}, async ({ page }) => {`];
1829
1838
  const lines = [
1830
1839
  // Package name interpolated rather than adjoining the preceding keyword
1831
1840
  // directly, so this generated-code line does not itself look like a real
@@ -1833,12 +1842,12 @@ function renderSpec(session, options = {}) {
1833
1842
  `import { test, expect } from ${quote2(options.testImport?.trim() || "@playwright/test")};`,
1834
1843
  ...importLines,
1835
1844
  ``,
1836
- ...opening,
1845
+ ...testOpening,
1837
1846
  ...testBody,
1838
1847
  `});`,
1839
1848
  ``
1840
1849
  ];
1841
- const header = 1 + importLines.length + 1 + opening.length;
1850
+ const header = 1 + importLines.length + 1 + testOpening.length;
1842
1851
  return { code: lines.join("\n"), matchedSpans, warnings: ctx.warnings, stepLines: linesFrom(header) };
1843
1852
  }
1844
1853
 
@@ -1850,7 +1859,12 @@ var STEPS_LIMITS = {
1850
1859
  /** Typed values, expected values, URLs, notes. */
1851
1860
  valueLength: 2e3,
1852
1861
  /** Titles, names, texts, locators. */
1853
- textLength: 500
1862
+ textLength: 500,
1863
+ viewports: 50,
1864
+ /** The widest and tallest viewport, in CSS pixels. */
1865
+ viewportSize: 1e4,
1866
+ /** The browser's zoom factors a viewport may name, as browsers allow them. */
1867
+ zoom: { min: 0.25, max: 5 }
1854
1868
  };
1855
1869
  var ACTIONS = /* @__PURE__ */ new Set([
1856
1870
  "goto",
@@ -1895,13 +1909,14 @@ function toStepsDocument(session, meta = {}) {
1895
1909
  origin,
1896
1910
  recordedAt: session.startedAt,
1897
1911
  note: meta.note ?? null,
1898
- steps: session.steps.map((step) => ({
1912
+ steps: session.steps.map(({ view: _view, ...step }) => ({
1899
1913
  ...step,
1900
1914
  target: step.target ? withoutElementKey(step.target) : null,
1901
1915
  ...step.dropTarget ? { dropTarget: withoutElementKey(step.dropTarget) } : {},
1902
1916
  pageUrl: toPath(step.pageUrl, origin),
1903
1917
  value: step.action === "goto" && step.value ? toPath(step.value, origin) : step.value
1904
- }))
1918
+ })),
1919
+ ...session.viewports?.length ? { viewports: session.viewports.map((v) => ({ ...v })) } : {}
1905
1920
  };
1906
1921
  }
1907
1922
  function withoutElementKey(target) {
@@ -1915,7 +1930,12 @@ function sessionFromSteps(doc, origin = doc.origin) {
1915
1930
  value: step.action === "goto" && step.value ? toAbsolute(step.value, origin) : step.value
1916
1931
  }));
1917
1932
  const firstGoto = steps.find((s) => s.action === "goto");
1918
- return { steps, startedAt: doc.recordedAt, startUrl: firstGoto?.value ?? steps[0]?.pageUrl ?? origin ?? "" };
1933
+ return {
1934
+ steps,
1935
+ startedAt: doc.recordedAt,
1936
+ startUrl: firstGoto?.value ?? steps[0]?.pageUrl ?? origin ?? "",
1937
+ ...doc.viewports?.length ? { viewports: doc.viewports } : {}
1938
+ };
1919
1939
  }
1920
1940
  function isObject(v) {
1921
1941
  return typeof v === "object" && v !== null && !Array.isArray(v);
@@ -2001,6 +2021,44 @@ function checkAssertion(c, v, path19) {
2001
2021
  note: c.text(v.note, `${path19}.note`, STEPS_LIMITS.valueLength, true)
2002
2022
  };
2003
2023
  }
2024
+ function checkViewports(c, v, stepCount) {
2025
+ if (v == null) return [];
2026
+ if (!Array.isArray(v)) {
2027
+ c.fail("viewports", "must be a list");
2028
+ return [];
2029
+ }
2030
+ if (v.length > STEPS_LIMITS.viewports) {
2031
+ c.fail("viewports", `holds more than ${STEPS_LIMITS.viewports}`);
2032
+ return [];
2033
+ }
2034
+ const out = [];
2035
+ v.forEach((entry, i) => {
2036
+ const at = `viewports[${i}]`;
2037
+ if (!isObject(entry)) return c.fail(at, "must be an object");
2038
+ const { step, width, height } = entry;
2039
+ if (!Number.isInteger(step) || step < 0 || step >= stepCount)
2040
+ return c.fail(`${at}.step`, "must be the index of a step");
2041
+ for (const [name, size] of [
2042
+ ["width", width],
2043
+ ["height", height]
2044
+ ]) {
2045
+ if (!Number.isInteger(size) || size < 1 || size > STEPS_LIMITS.viewportSize)
2046
+ return c.fail(`${at}.${name}`, `must be a whole number of pixels from 1 to ${STEPS_LIMITS.viewportSize}`);
2047
+ }
2048
+ const { zoom } = entry;
2049
+ if (zoom != null && (typeof zoom !== "number" || !(zoom >= STEPS_LIMITS.zoom.min && zoom <= STEPS_LIMITS.zoom.max)))
2050
+ return c.fail(`${at}.zoom`, `must be a zoom factor from ${STEPS_LIMITS.zoom.min} to ${STEPS_LIMITS.zoom.max}`);
2051
+ const last = out[out.length - 1];
2052
+ if (last && last.step >= step) return c.fail(`${at}.step`, "must come after the step before it");
2053
+ out.push({
2054
+ step,
2055
+ width,
2056
+ height,
2057
+ ...zoom != null && zoom !== 1 ? { zoom } : {}
2058
+ });
2059
+ });
2060
+ return out;
2061
+ }
2004
2062
  function checkStep(c, v, path19) {
2005
2063
  if (!isObject(v)) {
2006
2064
  c.fail(path19, "must be an object");
@@ -2072,13 +2130,15 @@ function parseSteps(input) {
2072
2130
  const originText = c.text(value.origin, "origin", STEPS_LIMITS.textLength, true);
2073
2131
  const origin = originText ? originOf2(originText) : null;
2074
2132
  if (originText && !origin) c.fail("origin", "must be an http(s) origin");
2133
+ const viewports = checkViewports(c, value.viewports, steps.length);
2075
2134
  doc = {
2076
2135
  v: STEPS_VERSION,
2077
2136
  title: c.text(value.title, "title", STEPS_LIMITS.textLength, true),
2078
2137
  origin,
2079
2138
  recordedAt: value.recordedAt == null ? 0 : c.number(value.recordedAt, "recordedAt"),
2080
2139
  note: c.text(value.note, "note", STEPS_LIMITS.valueLength, true),
2081
- steps
2140
+ steps,
2141
+ ...viewports.length > 0 ? { viewports } : {}
2082
2142
  };
2083
2143
  }
2084
2144
  return c.errors.length > 0 ? { ok: false, errors: c.errors } : { ok: true, steps: doc };
@@ -2454,6 +2514,8 @@ var DEFAULTS = {
2454
2514
  // Off by default: JavaScript coverage slows the page, so a scheduled job opts in.
2455
2515
  captureCodeReach: false,
2456
2516
  captureServerTraces: true,
2517
+ captureResources: true,
2518
+ leakCheck: "report",
2457
2519
  sampleAriaOnPass: true,
2458
2520
  uploadManifest: true,
2459
2521
  defaultCapture: true,
@@ -2494,6 +2556,8 @@ var PIWI_ENV_KEYS = {
2494
2556
  captureCodeReach: "PIWI_CAPTURE_CODE_REACH",
2495
2557
  codeReachRoots: "PIWI_CODE_REACH_ROOTS",
2496
2558
  captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
2559
+ captureResources: "PIWI_CAPTURE_RESOURCES",
2560
+ leakCheck: "PIWI_LEAK_CHECK",
2497
2561
  sampleAriaOnPass: "PIWI_SAMPLE_ARIA_ON_PASS",
2498
2562
  uploadManifest: "PIWI_UPLOAD_MANIFEST",
2499
2563
  defaultCapture: "PIWI_DEFAULT_CAPTURE",
@@ -2545,6 +2609,8 @@ var ENV_FALLBACK_SPECS = [
2545
2609
  { option: "capturePageInventory", env: PIWI_ENV_KEYS.capturePageInventory, kind: "bool" },
2546
2610
  { option: "captureCodeReach", env: PIWI_ENV_KEYS.captureCodeReach, kind: "bool" },
2547
2611
  { option: "captureServerTraces", env: PIWI_ENV_KEYS.captureServerTraces, kind: "bool" },
2612
+ { option: "captureResources", env: PIWI_ENV_KEYS.captureResources, kind: "bool" },
2613
+ { option: "leakCheck", env: PIWI_ENV_KEYS.leakCheck, kind: "string" },
2548
2614
  { option: "sampleAriaOnPass", env: PIWI_ENV_KEYS.sampleAriaOnPass, kind: "bool" },
2549
2615
  { option: "uploadManifest", env: PIWI_ENV_KEYS.uploadManifest, kind: "bool" },
2550
2616
  { option: "defaultCapture", env: PIWI_ENV_KEYS.defaultCapture, kind: "bool" },
@@ -3404,6 +3470,10 @@ function formatGateResult(result) {
3404
3470
  ` ${facts.quarantinedFailures} failing ${facts.quarantinedFailures === 1 ? "test is" : "tests are"} quarantined and did not count`
3405
3471
  );
3406
3472
  }
3473
+ if (facts.resources?.reported) {
3474
+ const { leaks, newLeaks } = facts.resources;
3475
+ lines.push(` ${leaks.length} ${leaks.length === 1 ? "leak" : "leaks"}, ${newLeaks.length} new`);
3476
+ }
3407
3477
  for (const violation of result.violations) lines.push(` \u2716 ${violation.message}`);
3408
3478
  for (const warning of result.warnings ?? []) lines.push(` \u26A0 ${warning}`);
3409
3479
  lines.push(` ${facts.runUrl}`);
@@ -3439,6 +3509,8 @@ Policy (at least one is required):
3439
3509
  --fail-on-new-cluster Fail when this run introduced a new failure cluster
3440
3510
  --fail-on-flaky Fail when this run contains any flaky test
3441
3511
  --require-selection <key> Fail when a test the named selection matches did not run or failed
3512
+ --max-leaks <n> Fail when the run left more than n browsers, contexts or pages open (per opening line)
3513
+ --max-new-leaks <n> Fail when more than n of those were never seen on the base branch
3442
3514
 
3443
3515
  Warn-only (reported, never fails the build):
3444
3516
  --max-uncovered-changes <n> Warn when more than n changed files have no observed test reach
@@ -3495,7 +3567,9 @@ function parseGateArgs(argv, env) {
3495
3567
  maxQuarantined: readCount(argv, "--max-quarantined"),
3496
3568
  failOnNewCluster: argv.includes("--fail-on-new-cluster"),
3497
3569
  failOnFlaky: argv.includes("--fail-on-flaky"),
3498
- requireSelection: readOption4(argv, "--require-selection")
3570
+ requireSelection: readOption4(argv, "--require-selection"),
3571
+ maxLeaks: readCount(argv, "--max-leaks"),
3572
+ maxNewLeaks: readCount(argv, "--max-new-leaks")
3499
3573
  };
3500
3574
  return {
3501
3575
  serverUrl,
@@ -83,6 +83,8 @@ var DEFAULTS = {
83
83
  // Off by default: JavaScript coverage slows the page, so a scheduled job opts in.
84
84
  captureCodeReach: false,
85
85
  captureServerTraces: true,
86
+ captureResources: true,
87
+ leakCheck: "report",
86
88
  sampleAriaOnPass: true,
87
89
  uploadManifest: true,
88
90
  defaultCapture: true,
@@ -123,6 +125,8 @@ var PIWI_ENV_KEYS = {
123
125
  captureCodeReach: "PIWI_CAPTURE_CODE_REACH",
124
126
  codeReachRoots: "PIWI_CODE_REACH_ROOTS",
125
127
  captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
128
+ captureResources: "PIWI_CAPTURE_RESOURCES",
129
+ leakCheck: "PIWI_LEAK_CHECK",
126
130
  sampleAriaOnPass: "PIWI_SAMPLE_ARIA_ON_PASS",
127
131
  uploadManifest: "PIWI_UPLOAD_MANIFEST",
128
132
  defaultCapture: "PIWI_DEFAULT_CAPTURE",
@@ -168,6 +172,8 @@ var ENV_FALLBACK_SPECS = [
168
172
  { option: "capturePageInventory", env: PIWI_ENV_KEYS.capturePageInventory, kind: "bool" },
169
173
  { option: "captureCodeReach", env: PIWI_ENV_KEYS.captureCodeReach, kind: "bool" },
170
174
  { option: "captureServerTraces", env: PIWI_ENV_KEYS.captureServerTraces, kind: "bool" },
175
+ { option: "captureResources", env: PIWI_ENV_KEYS.captureResources, kind: "bool" },
176
+ { option: "leakCheck", env: PIWI_ENV_KEYS.leakCheck, kind: "string" },
171
177
  { option: "sampleAriaOnPass", env: PIWI_ENV_KEYS.sampleAriaOnPass, kind: "bool" },
172
178
  { option: "uploadManifest", env: PIWI_ENV_KEYS.uploadManifest, kind: "bool" },
173
179
  { option: "defaultCapture", env: PIWI_ENV_KEYS.defaultCapture, kind: "bool" },
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { FullConfig, Suite, TestCase, TestResult, FullResult } from '@playwright/test/reporter';
2
2
  import { PlaywrightTestConfig, Locator, TestType, Page, Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions } from '@playwright/test';
3
3
  export { PlaywrightTestConfig } from '@playwright/test';
4
- export { PiwiFixtures, extendPiwiFixtures, piwiFixtures } from './internal/capture/capture-fixtures.js';
4
+ export { PiwiFixtures, PiwiWorkerFixtures, extendPiwiFixtures, piwiFixtures } from './internal/capture/capture-fixtures.js';
5
5
  import '@piwitests/picker-dom';
6
6
  import '@piwitests/core/wire';
7
7
  import '@piwitests/core/code-reach';
@@ -100,6 +100,35 @@ interface PiwiDashboardOptions {
100
100
  * `false`. Can also be forced off with `PIWI_CAPTURE_SERVER_TRACES=false`.
101
101
  */
102
102
  captureServerTraces?: boolean;
103
+ /**
104
+ * Keep a ledger of the browsers, contexts, pages and API request contexts each
105
+ * test opens, and list at the end of the run the ones left open after the
106
+ * scope that created them ended (a context a test never closed, a `beforeAll`
107
+ * context no `afterAll` closes, a browser a test launched itself), the pages
108
+ * opened and never used, and the pages, listeners and route handlers piling up
109
+ * on a page or context that lives across tests. Each finding names the line or
110
+ * the fixture that opened the object. Rides the capture fixtures and costs a
111
+ * few bookkeeping steps per test. Also samples the run's processes and the
112
+ * machine, with or without the fixtures, and prints what the run cost: CPU by
113
+ * process, time spent waiting for a CPU, peak memory and disk. The findings,
114
+ * the run's cost and each test's go to the dashboard with the run, on its
115
+ * Resources tab. Defaults to `true`. Set to `false` (or
116
+ * `PIWI_CAPTURE_RESOURCES=false`) to turn both off.
117
+ */
118
+ captureResources?: boolean;
119
+ /**
120
+ * What the capture fixtures do when a test leaves open a browser, context,
121
+ * page or API request context that the test itself opened (its body or its
122
+ * `beforeEach`/`afterEach` hooks; a fixture's objects are the fixture's):
123
+ * `'report'` lists it in the end-of-run summary, `'fail'` also fails the test
124
+ * with the line that opened it, and `'close'` closes it when the test ends and
125
+ * still lists it. A `beforeAll` object left open is only ever reported. Each
126
+ * test is judged when it ends, so a test that hands what it opened to the next
127
+ * one fails under `'fail'` and loses it under `'close'`: open such objects in
128
+ * `beforeAll` and close them in `afterAll`. Defaults to `'report'`. Can also
129
+ * be set with `PIWI_LEAK_CHECK`.
130
+ */
131
+ leakCheck?: 'report' | 'fail' | 'close';
103
132
  /**
104
133
  * Sample the ARIA snapshot at the end of a *passing* test, so a later failure
105
134
  * can be diffed against the page as it last looked when green. Rate-limited by
@@ -375,6 +404,20 @@ declare class PiwiDashboardReporter {
375
404
  private filterDetails;
376
405
  /** Configured `maxFailures` (0 = unlimited) — disambiguates an interrupted run's unrun reason. */
377
406
  private maxFailures;
407
+ /** The resource censuses the capture fixtures attached, one per test attempt. */
408
+ private resourceCensuses;
409
+ /** Lifecycle steps of the tests that ran without the fixtures. */
410
+ private lifecycleTallies;
411
+ /** The user fixtures set up by the tests that left a page unused, by test id. */
412
+ private fixturesByTest;
413
+ /** The file workers append their shutdown census to, set in `onBegin`. */
414
+ private resourcesFile;
415
+ /** Samples the run's processes and the machine from `onBegin` to `onEnd`. */
416
+ private sampler;
417
+ /** Bytes of the files the tests attached, by kind. */
418
+ private artifactBytes;
419
+ /** The run's resource report for the dashboard, built in `onEnd`. */
420
+ private resourceReport;
378
421
  private httpClient;
379
422
  private uploader;
380
423
  private fileHandler;
@@ -408,6 +451,17 @@ declare class PiwiDashboardReporter {
408
451
  onStepEnd(test: TestCase | undefined, _result: TestResult | undefined, step: any): void;
409
452
  /** Playwright reporter hook: called when an individual test finishes */
410
453
  onTestEnd(test: TestCase, result: TestResult): void;
454
+ /**
455
+ * Add up the files a test attached, and keep its resource census; for a test
456
+ * that ran without the capture fixtures, tally the browsers and contexts its
457
+ * steps opened and closed.
458
+ */
459
+ private collectResources;
460
+ /**
461
+ * Print what the run left open or opened for nothing, from the censuses and
462
+ * the workers' shutdown file, then what the run cost the machine.
463
+ */
464
+ private reportResources;
411
465
  /**
412
466
  * Synthesize `didnotrun` cases for tests Playwright planned but never reported
413
467
  * (no `onTestEnd`) — typically because `maxFailures` cut the run short. These