playwright 1.55.0-alpha-2025-08-06 → 1.55.0-alpha-2025-08-08

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.

Potentially problematic release.


This version of playwright might be problematic. Click here for more details.

@@ -49,6 +49,8 @@ class FullConfigInternal {
49
49
  this.projects = [];
50
50
  this.cliArgs = [];
51
51
  this.cliListOnly = false;
52
+ this.preOnlyTestFilters = [];
53
+ this.postShardTestFilters = [];
52
54
  this.defineConfigWasUsed = false;
53
55
  this.globalSetups = [];
54
56
  this.globalTeardowns = [];
@@ -90,7 +90,7 @@ class FixturePool {
90
90
  continue;
91
91
  }
92
92
  } else if (previous) {
93
- options = { auto: previous.auto, scope: previous.scope, option: previous.option, timeout: previous.timeout, customTitle: previous.customTitle, box: previous.box };
93
+ options = { auto: previous.auto, scope: previous.scope, option: previous.option, timeout: previous.timeout, customTitle: previous.customTitle };
94
94
  } else if (!options) {
95
95
  options = { auto: false, scope: "test", option: false, timeout: void 0 };
96
96
  }
@@ -31,10 +31,8 @@ __export(suiteUtils_exports, {
31
31
  applyRepeatEachIndex: () => applyRepeatEachIndex,
32
32
  bindFileSuiteToProject: () => bindFileSuiteToProject,
33
33
  filterByFocusedLine: () => filterByFocusedLine,
34
- filterByTestIds: () => filterByTestIds,
35
34
  filterOnly: () => filterOnly,
36
35
  filterSuite: () => filterSuite,
37
- filterSuiteWithOnlySemantics: () => filterSuiteWithOnlySemantics,
38
36
  filterTestsRemoveEmptySuites: () => filterTestsRemoveEmptySuites
39
37
  });
40
38
  module.exports = __toCommonJS(suiteUtils_exports);
@@ -127,11 +125,6 @@ function filterByFocusedLine(suite, focusedTestFileLines) {
127
125
  const testFilter = (test) => testFileLineMatches(test.location.file, test.location.line, test.location.column);
128
126
  return filterSuite(suite, suiteFilter, testFilter);
129
127
  }
130
- function filterByTestIds(suite, testIdMatcher) {
131
- if (!testIdMatcher)
132
- return;
133
- filterTestsRemoveEmptySuites(suite, (test) => testIdMatcher(test.id));
134
- }
135
128
  function createFileMatcherFromFilter(filter) {
136
129
  const fileMatcher = (0, import_util.createFileMatcher)(filter.re || filter.exact || "");
137
130
  return (testFileName, testLine, testColumn) => fileMatcher(testFileName) && (filter.line === testLine || filter.line === null) && (filter.column === testColumn || filter.column === null);
@@ -141,9 +134,7 @@ function createFileMatcherFromFilter(filter) {
141
134
  applyRepeatEachIndex,
142
135
  bindFileSuiteToProject,
143
136
  filterByFocusedLine,
144
- filterByTestIds,
145
137
  filterOnly,
146
138
  filterSuite,
147
- filterSuiteWithOnlySemantics,
148
139
  filterTestsRemoveEmptySuites
149
140
  });
package/lib/index.js CHANGED
@@ -61,20 +61,20 @@ if (process["__pw_initiator__"]) {
61
61
  process["__pw_initiator__"] = new Error().stack;
62
62
  }
63
63
  const playwrightFixtures = {
64
- defaultBrowserType: ["chromium", { scope: "worker", option: true }],
65
- browserName: [({ defaultBrowserType }, use) => use(defaultBrowserType), { scope: "worker", option: true }],
64
+ defaultBrowserType: ["chromium", { scope: "worker", option: true, box: true }],
65
+ browserName: [({ defaultBrowserType }, use) => use(defaultBrowserType), { scope: "worker", option: true, box: true }],
66
66
  playwright: [async ({}, use) => {
67
67
  await use(require("playwright-core"));
68
68
  }, { scope: "worker", box: true }],
69
- headless: [({ launchOptions }, use) => use(launchOptions.headless ?? true), { scope: "worker", option: true }],
70
- channel: [({ launchOptions }, use) => use(launchOptions.channel), { scope: "worker", option: true }],
71
- launchOptions: [{}, { scope: "worker", option: true }],
69
+ headless: [({ launchOptions }, use) => use(launchOptions.headless ?? true), { scope: "worker", option: true, box: true }],
70
+ channel: [({ launchOptions }, use) => use(launchOptions.channel), { scope: "worker", option: true, box: true }],
71
+ launchOptions: [{}, { scope: "worker", option: true, box: true }],
72
72
  connectOptions: [async ({ _optionConnectOptions }, use) => {
73
73
  await use(connectOptionsFromEnv() || _optionConnectOptions);
74
- }, { scope: "worker", option: true }],
75
- screenshot: ["off", { scope: "worker", option: true }],
76
- video: ["off", { scope: "worker", option: true }],
77
- trace: ["off", { scope: "worker", option: true }],
74
+ }, { scope: "worker", option: true, box: true }],
75
+ screenshot: ["off", { scope: "worker", option: true, box: true }],
76
+ video: ["off", { scope: "worker", option: true, box: true }],
77
+ trace: ["off", { scope: "worker", option: true, box: true }],
78
78
  _browserOptions: [async ({ playwright, headless, channel, launchOptions }, use) => {
79
79
  const options = {
80
80
  handleSIGINT: false,
@@ -103,45 +103,41 @@ const playwrightFixtures = {
103
103
  }
104
104
  });
105
105
  await use(browser2);
106
- await browser2._wrapApiCall(async () => {
107
- await browser2.close({ reason: "Test ended." });
108
- }, { internal: true });
106
+ await browser2.close({ reason: "Test ended." });
109
107
  return;
110
108
  }
111
109
  const browser = await playwright[browserName].launch();
112
110
  await use(browser);
113
- await browser._wrapApiCall(async () => {
114
- await browser.close({ reason: "Test ended." });
115
- }, { internal: true });
111
+ await browser.close({ reason: "Test ended." });
116
112
  }, { scope: "worker", timeout: 0 }],
117
- acceptDownloads: [({ contextOptions }, use) => use(contextOptions.acceptDownloads ?? true), { option: true }],
118
- bypassCSP: [({ contextOptions }, use) => use(contextOptions.bypassCSP ?? false), { option: true }],
119
- colorScheme: [({ contextOptions }, use) => use(contextOptions.colorScheme === void 0 ? "light" : contextOptions.colorScheme), { option: true }],
120
- deviceScaleFactor: [({ contextOptions }, use) => use(contextOptions.deviceScaleFactor), { option: true }],
121
- extraHTTPHeaders: [({ contextOptions }, use) => use(contextOptions.extraHTTPHeaders), { option: true }],
122
- geolocation: [({ contextOptions }, use) => use(contextOptions.geolocation), { option: true }],
123
- hasTouch: [({ contextOptions }, use) => use(contextOptions.hasTouch ?? false), { option: true }],
124
- httpCredentials: [({ contextOptions }, use) => use(contextOptions.httpCredentials), { option: true }],
125
- ignoreHTTPSErrors: [({ contextOptions }, use) => use(contextOptions.ignoreHTTPSErrors ?? false), { option: true }],
126
- isMobile: [({ contextOptions }, use) => use(contextOptions.isMobile ?? false), { option: true }],
127
- javaScriptEnabled: [({ contextOptions }, use) => use(contextOptions.javaScriptEnabled ?? true), { option: true }],
128
- locale: [({ contextOptions }, use) => use(contextOptions.locale ?? "en-US"), { option: true }],
129
- offline: [({ contextOptions }, use) => use(contextOptions.offline ?? false), { option: true }],
130
- permissions: [({ contextOptions }, use) => use(contextOptions.permissions), { option: true }],
131
- proxy: [({ contextOptions }, use) => use(contextOptions.proxy), { option: true }],
132
- storageState: [({ contextOptions }, use) => use(contextOptions.storageState), { option: true }],
133
- clientCertificates: [({ contextOptions }, use) => use(contextOptions.clientCertificates), { option: true }],
134
- timezoneId: [({ contextOptions }, use) => use(contextOptions.timezoneId), { option: true }],
135
- userAgent: [({ contextOptions }, use) => use(contextOptions.userAgent), { option: true }],
136
- viewport: [({ contextOptions }, use) => use(contextOptions.viewport === void 0 ? { width: 1280, height: 720 } : contextOptions.viewport), { option: true }],
137
- actionTimeout: [0, { option: true }],
138
- testIdAttribute: ["data-testid", { option: true }],
139
- navigationTimeout: [0, { option: true }],
113
+ acceptDownloads: [({ contextOptions }, use) => use(contextOptions.acceptDownloads ?? true), { option: true, box: true }],
114
+ bypassCSP: [({ contextOptions }, use) => use(contextOptions.bypassCSP ?? false), { option: true, box: true }],
115
+ colorScheme: [({ contextOptions }, use) => use(contextOptions.colorScheme === void 0 ? "light" : contextOptions.colorScheme), { option: true, box: true }],
116
+ deviceScaleFactor: [({ contextOptions }, use) => use(contextOptions.deviceScaleFactor), { option: true, box: true }],
117
+ extraHTTPHeaders: [({ contextOptions }, use) => use(contextOptions.extraHTTPHeaders), { option: true, box: true }],
118
+ geolocation: [({ contextOptions }, use) => use(contextOptions.geolocation), { option: true, box: true }],
119
+ hasTouch: [({ contextOptions }, use) => use(contextOptions.hasTouch ?? false), { option: true, box: true }],
120
+ httpCredentials: [({ contextOptions }, use) => use(contextOptions.httpCredentials), { option: true, box: true }],
121
+ ignoreHTTPSErrors: [({ contextOptions }, use) => use(contextOptions.ignoreHTTPSErrors ?? false), { option: true, box: true }],
122
+ isMobile: [({ contextOptions }, use) => use(contextOptions.isMobile ?? false), { option: true, box: true }],
123
+ javaScriptEnabled: [({ contextOptions }, use) => use(contextOptions.javaScriptEnabled ?? true), { option: true, box: true }],
124
+ locale: [({ contextOptions }, use) => use(contextOptions.locale ?? "en-US"), { option: true, box: true }],
125
+ offline: [({ contextOptions }, use) => use(contextOptions.offline ?? false), { option: true, box: true }],
126
+ permissions: [({ contextOptions }, use) => use(contextOptions.permissions), { option: true, box: true }],
127
+ proxy: [({ contextOptions }, use) => use(contextOptions.proxy), { option: true, box: true }],
128
+ storageState: [({ contextOptions }, use) => use(contextOptions.storageState), { option: true, box: true }],
129
+ clientCertificates: [({ contextOptions }, use) => use(contextOptions.clientCertificates), { option: true, box: true }],
130
+ timezoneId: [({ contextOptions }, use) => use(contextOptions.timezoneId), { option: true, box: true }],
131
+ userAgent: [({ contextOptions }, use) => use(contextOptions.userAgent), { option: true, box: true }],
132
+ viewport: [({ contextOptions }, use) => use(contextOptions.viewport === void 0 ? { width: 1280, height: 720 } : contextOptions.viewport), { option: true, box: true }],
133
+ actionTimeout: [0, { option: true, box: true }],
134
+ testIdAttribute: ["data-testid", { option: true, box: true }],
135
+ navigationTimeout: [0, { option: true, box: true }],
140
136
  baseURL: [async ({}, use) => {
141
137
  await use(process.env.PLAYWRIGHT_TEST_BASE_URL);
142
- }, { option: true }],
143
- serviceWorkers: [({ contextOptions }, use) => use(contextOptions.serviceWorkers ?? "allow"), { option: true }],
144
- contextOptions: [{}, { option: true }],
138
+ }, { option: true, box: true }],
139
+ serviceWorkers: [({ contextOptions }, use) => use(contextOptions.serviceWorkers ?? "allow"), { option: true, box: true }],
140
+ contextOptions: [{}, { option: true, box: true }],
145
141
  _combinedContextOptions: [async ({
146
142
  acceptDownloads,
147
143
  bypassCSP,
@@ -221,7 +217,7 @@ const playwrightFixtures = {
221
217
  if (testIdAttribute)
222
218
  playwrightLibrary.selectors.setTestIdAttribute(testIdAttribute);
223
219
  testInfo.snapshotSuffix = process.platform;
224
- if ((0, import_utils.debugMode)())
220
+ if ((0, import_utils.debugMode)() === "inspector")
225
221
  testInfo._setDebugMode();
226
222
  playwright._defaultContextOptions = _combinedContextOptions;
227
223
  playwright._defaultContextTimeout = actionTimeout || 0;
@@ -310,6 +306,7 @@ const playwrightFixtures = {
310
306
  const videoMode = normalizeVideoMode(video);
311
307
  const captureVideo = shouldCaptureVideo(videoMode, testInfo) && !_reuseContext;
312
308
  const contexts = /* @__PURE__ */ new Map();
309
+ let counter = 0;
313
310
  await use(async (options) => {
314
311
  const hook = testInfoImpl._currentHookType();
315
312
  if (hook === "beforeAll" || hook === "afterAll") {
@@ -326,10 +323,6 @@ const playwrightFixtures = {
326
323
  }
327
324
  } : {};
328
325
  const context = await browser.newContext({ ...videoOptions, ...options });
329
- const contextData = { pagesWithVideo: [] };
330
- contexts.set(context, contextData);
331
- if (captureVideo)
332
- context.on("page", (page) => contextData.pagesWithVideo.push(page));
333
326
  if (process.env.PW_CLOCK === "frozen") {
334
327
  await context._wrapApiCall(async () => {
335
328
  await context.clock.install({ time: 0 });
@@ -340,33 +333,39 @@ const playwrightFixtures = {
340
333
  await context.clock.install({ time: 0 });
341
334
  }, { internal: true });
342
335
  }
343
- return context;
344
- });
345
- let counter = 0;
346
- const closeReason = testInfo.status === "timedOut" ? "Test timeout of " + testInfo.timeout + "ms exceeded." : "Test ended.";
347
- await Promise.all([...contexts.keys()].map(async (context) => {
348
- await context._wrapApiCall(async () => {
336
+ let closed = false;
337
+ const close = async () => {
338
+ if (closed)
339
+ return;
340
+ closed = true;
341
+ const closeReason = testInfo.status === "timedOut" ? "Test timeout of " + testInfo.timeout + "ms exceeded." : "Test ended.";
349
342
  await context.close({ reason: closeReason });
350
- }, { internal: true });
351
- const testFailed = testInfo.status !== testInfo.expectedStatus;
352
- const preserveVideo = captureVideo && (videoMode === "on" || testFailed && videoMode === "retain-on-failure" || videoMode === "on-first-retry" && testInfo.retry === 1);
353
- if (preserveVideo) {
354
- const { pagesWithVideo: pagesForVideo } = contexts.get(context);
355
- const videos = pagesForVideo.map((p) => p.video()).filter(Boolean);
356
- await Promise.all(videos.map(async (v) => {
357
- try {
358
- const savedPath = testInfo.outputPath(`video${counter ? "-" + counter : ""}.webm`);
359
- ++counter;
360
- await v.saveAs(savedPath);
361
- testInfo.attachments.push({ name: "video", path: savedPath, contentType: "video/webm" });
362
- } catch (e) {
363
- }
364
- }));
365
- }
366
- }));
343
+ const testFailed = testInfo.status !== testInfo.expectedStatus;
344
+ const preserveVideo = captureVideo && (videoMode === "on" || testFailed && videoMode === "retain-on-failure" || videoMode === "on-first-retry" && testInfo.retry === 1);
345
+ if (preserveVideo) {
346
+ const { pagesWithVideo: pagesForVideo } = contexts.get(context);
347
+ const videos = pagesForVideo.map((p) => p.video()).filter((video2) => !!video2);
348
+ await Promise.all(videos.map(async (v) => {
349
+ try {
350
+ const savedPath = testInfo.outputPath(`video${counter ? "-" + counter : ""}.webm`);
351
+ ++counter;
352
+ await v.saveAs(savedPath);
353
+ testInfo.attachments.push({ name: "video", path: savedPath, contentType: "video/webm" });
354
+ } catch (e) {
355
+ }
356
+ }));
357
+ }
358
+ };
359
+ const contextData = { close, pagesWithVideo: [] };
360
+ if (captureVideo)
361
+ context.on("page", (page) => contextData.pagesWithVideo.push(page));
362
+ contexts.set(context, contextData);
363
+ return { context, close };
364
+ });
365
+ await Promise.all([...contexts.values()].map((data) => data.close()));
367
366
  }, { scope: "test", title: "context", box: true }],
368
- _optionContextReuseMode: ["none", { scope: "worker", option: true }],
369
- _optionConnectOptions: [void 0, { scope: "worker", option: true }],
367
+ _optionContextReuseMode: ["none", { scope: "worker", option: true, box: true }],
368
+ _optionConnectOptions: [void 0, { scope: "worker", option: true, box: true }],
370
369
  _reuseContext: [async ({ video, _optionContextReuseMode }, use) => {
371
370
  let mode = _optionContextReuseMode;
372
371
  if (process.env.PW_TEST_REUSE_CONTEXT)
@@ -374,17 +373,19 @@ const playwrightFixtures = {
374
373
  const reuse = mode === "when-possible" && normalizeVideoMode(video) === "off";
375
374
  await use(reuse);
376
375
  }, { scope: "worker", title: "context", box: true }],
377
- context: async ({ playwright, browser, _reuseContext, _contextFactory }, use, testInfo) => {
378
- attachConnectedHeaderIfNeeded(testInfo, browser);
376
+ context: async ({ browser, _reuseContext, _contextFactory }, use, testInfo) => {
377
+ const browserImpl = browser;
378
+ attachConnectedHeaderIfNeeded(testInfo, browserImpl);
379
379
  if (!_reuseContext) {
380
- await use(await _contextFactory());
380
+ const { context: context2, close } = await _contextFactory();
381
+ await use(context2);
382
+ await close();
381
383
  return;
382
384
  }
383
- const defaultContextOptions = playwright.chromium._defaultContextOptions;
384
- const context = await browser._newContextForReuse(defaultContextOptions);
385
+ const context = await browserImpl._wrapApiCall(() => browserImpl._newContextForReuse(), { internal: true });
385
386
  await use(context);
386
387
  const closeReason = testInfo.status === "timedOut" ? "Test timeout of " + testInfo.timeout + "ms exceeded." : "Test ended.";
387
- await browser._disconnectFromReusedContext(closeReason);
388
+ await browserImpl._wrapApiCall(() => browserImpl._disconnectFromReusedContext(closeReason), { internal: true });
388
389
  },
389
390
  page: async ({ context, _reuseContext }, use) => {
390
391
  if (!_reuseContext) {
@@ -548,7 +549,9 @@ class ArtifactsRecorder {
548
549
  const screenshotOptions = typeof screenshot === "string" ? void 0 : screenshot;
549
550
  this._startedCollectingArtifacts = Symbol("startedCollectingArtifacts");
550
551
  this._screenshotRecorder = new SnapshotRecorder(this, normalizeScreenshotMode(screenshot), "screenshot", "image/png", ".png", async (page, path2) => {
551
- await page.screenshot({ ...screenshotOptions, timeout: 5e3, path: path2, caret: "initial" });
552
+ await page._wrapApiCall(async () => {
553
+ await page.screenshot({ ...screenshotOptions, timeout: 5e3, path: path2, caret: "initial" });
554
+ }, { internal: true });
552
555
  });
553
556
  }
554
557
  async willStartTest(testInfo) {
@@ -560,10 +563,10 @@ class ArtifactsRecorder {
560
563
  await Promise.all(existingApiRequests.map((c) => this.didCreateRequestContext(c)));
561
564
  }
562
565
  async didCreateBrowserContext(context) {
563
- await this._startTraceChunkOnContextCreation(context.tracing);
566
+ await this._startTraceChunkOnContextCreation(context, context.tracing);
564
567
  }
565
568
  async willCloseBrowserContext(context) {
566
- await this._stopTracing(context.tracing);
569
+ await this._stopTracing(context, context.tracing);
567
570
  await this._screenshotRecorder.captureTemporary(context);
568
571
  await this._takePageSnapshot(context);
569
572
  }
@@ -578,17 +581,17 @@ class ArtifactsRecorder {
578
581
  if (!page)
579
582
  return;
580
583
  try {
581
- this._pageSnapshot = await page._snapshotForAI({ timeout: 5e3 });
584
+ await page._wrapApiCall(async () => {
585
+ this._pageSnapshot = await page._snapshotForAI({ timeout: 5e3 });
586
+ }, { internal: true });
582
587
  } catch {
583
588
  }
584
589
  }
585
590
  async didCreateRequestContext(context) {
586
- const tracing2 = context._tracing;
587
- await this._startTraceChunkOnContextCreation(tracing2);
591
+ await this._startTraceChunkOnContextCreation(context, context._tracing);
588
592
  }
589
593
  async willCloseRequestContext(context) {
590
- const tracing2 = context._tracing;
591
- await this._stopTracing(tracing2);
594
+ await this._stopTracing(context, context._tracing);
592
595
  }
593
596
  async didFinishTestFunction() {
594
597
  await this._screenshotRecorder.maybeCapture();
@@ -598,10 +601,9 @@ class ArtifactsRecorder {
598
601
  const leftoverContexts = this._playwright._allContexts();
599
602
  const leftoverApiRequests = Array.from(this._playwright.request._contexts);
600
603
  await Promise.all(leftoverContexts.map(async (context2) => {
601
- await this._stopTracing(context2.tracing);
604
+ await this._stopTracing(context2, context2.tracing);
602
605
  }).concat(leftoverApiRequests.map(async (context2) => {
603
- const tracing2 = context2._tracing;
604
- await this._stopTracing(tracing2);
606
+ await this._stopTracing(context2, context2._tracing);
605
607
  })));
606
608
  await this._screenshotRecorder.persistTemporary();
607
609
  const context = leftoverContexts[0];
@@ -624,30 +626,34 @@ class ArtifactsRecorder {
624
626
  }, void 0);
625
627
  }
626
628
  }
627
- async _startTraceChunkOnContextCreation(tracing2) {
628
- const options = this._testInfo._tracing.traceOptions();
629
- if (options) {
630
- const title = this._testInfo._tracing.traceTitle();
631
- const name = this._testInfo._tracing.generateNextTraceRecordingName();
632
- if (!tracing2[kTracingStarted]) {
633
- await tracing2.start({ ...options, title, name });
634
- tracing2[kTracingStarted] = true;
629
+ async _startTraceChunkOnContextCreation(channelOwner, tracing2) {
630
+ await channelOwner._wrapApiCall(async () => {
631
+ const options = this._testInfo._tracing.traceOptions();
632
+ if (options) {
633
+ const title = this._testInfo._tracing.traceTitle();
634
+ const name = this._testInfo._tracing.generateNextTraceRecordingName();
635
+ if (!tracing2[kTracingStarted]) {
636
+ await tracing2.start({ ...options, title, name });
637
+ tracing2[kTracingStarted] = true;
638
+ } else {
639
+ await tracing2.startChunk({ title, name });
640
+ }
635
641
  } else {
636
- await tracing2.startChunk({ title, name });
637
- }
638
- } else {
639
- if (tracing2[kTracingStarted]) {
640
- tracing2[kTracingStarted] = false;
641
- await tracing2.stop();
642
+ if (tracing2[kTracingStarted]) {
643
+ tracing2[kTracingStarted] = false;
644
+ await tracing2.stop();
645
+ }
642
646
  }
643
- }
647
+ }, { internal: true });
644
648
  }
645
- async _stopTracing(tracing2) {
646
- if (tracing2[this._startedCollectingArtifacts])
647
- return;
648
- tracing2[this._startedCollectingArtifacts] = true;
649
- if (this._testInfo._tracing.traceOptions() && tracing2[kTracingStarted])
650
- await tracing2.stopChunk({ path: this._testInfo._tracing.maybeGenerateNextTraceRecordingPath() });
649
+ async _stopTracing(channelOwner, tracing2) {
650
+ await channelOwner._wrapApiCall(async () => {
651
+ if (tracing2[this._startedCollectingArtifacts])
652
+ return;
653
+ tracing2[this._startedCollectingArtifacts] = true;
654
+ if (this._testInfo._tracing.traceOptions() && tracing2[kTracingStarted])
655
+ await tracing2.stopChunk({ path: this._testInfo._tracing.maybeGenerateNextTraceRecordingPath() });
656
+ }, { internal: true });
651
657
  }
652
658
  }
653
659
  function renderTitle(type, method, params, title) {
package/lib/program.js CHANGED
@@ -172,6 +172,7 @@ async function runTests(args, opts) {
172
172
  config.cliProjectFilter = opts.project || void 0;
173
173
  config.cliPassWithNoTests = !!opts.passWithNoTests;
174
174
  config.cliLastFailed = !!opts.lastFailed;
175
+ config.cliFilterFile = opts.filter ? import_path.default.resolve(process.cwd(), opts.filter) : void 0;
175
176
  (0, import_projectUtils.filterProjects)(config.projects, config.cliProjectFilter);
176
177
  if (opts.ui || opts.uiHost || opts.uiPort) {
177
178
  if (opts.onlyChanged)
@@ -346,6 +347,7 @@ const testOptions = [
346
347
  ["-c, --config <file>", { description: `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"` }],
347
348
  ["--debug", { description: `Run tests with Playwright Inspector. Shortcut for "PWDEBUG=1" environment variable and "--timeout=0 --max-failures=1 --headed --workers=1" options` }],
348
349
  ["--fail-on-flaky-tests", { description: `Fail if any test is flagged as flaky (default: false)` }],
350
+ ["--filter <file>", { description: `Path to a test filter file. See documentation for details.` }],
349
351
  ["--forbid-only", { description: `Fail if test.only is called (default: false)` }],
350
352
  ["--fully-parallel", { description: `Run all tests in parallel (default: false)` }],
351
353
  ["--global-timeout <timeout>", { description: `Maximum time this test suite can run in milliseconds (default: unlimited)` }],
@@ -46,7 +46,8 @@ class LastRunReporter {
46
46
  return;
47
47
  try {
48
48
  const lastRunInfo = JSON.parse(await import_fs.default.promises.readFile(this._lastRunFile, "utf8"));
49
- this._config.lastFailedTestIdMatcher = (id) => lastRunInfo.failedTests.includes(id);
49
+ const failedTestIds = new Set(lastRunInfo.failedTests);
50
+ this._config.postShardTestFilters.push((test) => failedTestIds.has(test.id));
50
51
  } catch {
51
52
  }
52
53
  }
@@ -62,10 +63,12 @@ class LastRunReporter {
62
63
  async onEnd(result) {
63
64
  if (!this._lastRunFile || this._config.cliListOnly)
64
65
  return;
66
+ const lastRunInfo = {
67
+ status: result.status,
68
+ failedTests: this._suite?.allTests().filter((t) => !t.ok()).map((t) => t.id) || []
69
+ };
65
70
  await import_fs.default.promises.mkdir(import_path.default.dirname(this._lastRunFile), { recursive: true });
66
- const failedTests = this._suite?.allTests().filter((t) => !t.ok()).map((t) => t.id);
67
- const lastRunReport = JSON.stringify({ status: result.status, failedTests }, void 0, 2);
68
- await import_fs.default.promises.writeFile(this._lastRunFile, lastRunReport);
71
+ await import_fs.default.promises.writeFile(this._lastRunFile, JSON.stringify(lastRunInfo, void 0, 2));
69
72
  }
70
73
  }
71
74
  // Annotate the CommonJS export names for ESM import in node:
@@ -112,7 +112,7 @@ async function loadFileSuites(testRun, mode, errors) {
112
112
  testRun.projectSuites.set(project, suites);
113
113
  }
114
114
  }
115
- async function createRootSuite(testRun, errors, shouldFilterOnly, additionalFileMatcher) {
115
+ async function createRootSuite(testRun, errors, shouldFilterOnly) {
116
116
  const config = testRun.config;
117
117
  const rootSuite = new import_test.Suite("", "root");
118
118
  const projectSuites = /* @__PURE__ */ new Map();
@@ -125,7 +125,7 @@ async function createRootSuite(testRun, errors, shouldFilterOnly, additionalFile
125
125
  for (const [project, fileSuites] of testRun.projectSuites) {
126
126
  const projectSuite = createProjectSuite(project, fileSuites);
127
127
  projectSuites.set(project, projectSuite);
128
- const filteredProjectSuite = filterProjectSuite(projectSuite, { cliFileFilters, cliTitleMatcher, testIdMatcher: config.testIdMatcher, additionalFileMatcher });
128
+ const filteredProjectSuite = filterProjectSuite(projectSuite, { cliFileFilters, cliTitleMatcher, testFilters: config.preOnlyTestFilters });
129
129
  filteredProjectSuites.set(project, filteredProjectSuite);
130
130
  }
131
131
  }
@@ -166,8 +166,8 @@ async function createRootSuite(testRun, errors, shouldFilterOnly, additionalFile
166
166
  }
167
167
  (0, import_suiteUtils.filterTestsRemoveEmptySuites)(rootSuite, (test) => testsInThisShard.has(test));
168
168
  }
169
- if (config.lastFailedTestIdMatcher)
170
- (0, import_suiteUtils.filterByTestIds)(rootSuite, config.lastFailedTestIdMatcher);
169
+ if (config.postShardTestFilters.length)
170
+ (0, import_suiteUtils.filterTestsRemoveEmptySuites)(rootSuite, (test) => config.postShardTestFilters.every((filter) => filter(test)));
171
171
  {
172
172
  const projectClosure2 = new Map((0, import_projectUtils.buildProjectsClosure)(rootSuite.suites.map((suite) => suite._fullProject)));
173
173
  for (const [project, level] of projectClosure2.entries()) {
@@ -192,17 +192,15 @@ function createProjectSuite(project, fileSuites) {
192
192
  return projectSuite;
193
193
  }
194
194
  function filterProjectSuite(projectSuite, options) {
195
- if (!options.cliFileFilters.length && !options.cliTitleMatcher && !options.testIdMatcher && !options.additionalFileMatcher)
195
+ if (!options.cliFileFilters.length && !options.cliTitleMatcher && !options.testFilters.length)
196
196
  return projectSuite;
197
197
  const result = projectSuite._deepClone();
198
198
  if (options.cliFileFilters.length)
199
199
  (0, import_suiteUtils.filterByFocusedLine)(result, options.cliFileFilters);
200
- if (options.testIdMatcher)
201
- (0, import_suiteUtils.filterByTestIds)(result, options.testIdMatcher);
202
200
  (0, import_suiteUtils.filterTestsRemoveEmptySuites)(result, (test) => {
203
- if (options.cliTitleMatcher && !options.cliTitleMatcher(test._grepTitleWithTags()))
201
+ if (!options.testFilters.every((filter) => filter(test)))
204
202
  return false;
205
- if (options.additionalFileMatcher && !options.additionalFileMatcher(test.location.file))
203
+ if (options.cliTitleMatcher && !options.cliTitleMatcher(test._grepTitleWithTags()))
206
204
  return false;
207
205
  return true;
208
206
  });
@@ -243,12 +243,20 @@ function createLoadTask(mode, options) {
243
243
  for (const plugin of testRun.config.plugins)
244
244
  await plugin.instance?.populateDependencies?.();
245
245
  }
246
- let cliOnlyChangedMatcher = void 0;
247
246
  if (testRun.config.cliOnlyChanged) {
248
247
  const changedFiles = await (0, import_vcs.detectChangedTestFiles)(testRun.config.cliOnlyChanged, testRun.config.configDir);
249
- cliOnlyChangedMatcher = (file) => changedFiles.has(file);
248
+ testRun.config.preOnlyTestFilters.push((test) => changedFiles.has(test.location.file));
250
249
  }
251
- testRun.rootSuite = await (0, import_loadUtils.createRootSuite)(testRun, options.failOnLoadErrors ? errors : softErrors, !!options.filterOnly, cliOnlyChangedMatcher);
250
+ if (testRun.config.cliFilterFile) {
251
+ try {
252
+ testRun.config.preOnlyTestFilters.push(await (0, import_util2.loadTestFilterFile)(testRun.config.cliFilterFile));
253
+ } catch (error) {
254
+ if (options.failOnLoadErrors)
255
+ throw error;
256
+ softErrors.push(error);
257
+ }
258
+ }
259
+ testRun.rootSuite = await (0, import_loadUtils.createRootSuite)(testRun, options.failOnLoadErrors ? errors : softErrors, !!options.filterOnly);
252
260
  testRun.failureTracker.onRootSuite(testRun.rootSuite);
253
261
  if (options.failOnLoadErrors && !testRun.rootSuite.allTests().length && !testRun.config.cliPassWithNoTests && !testRun.config.config.shard && !testRun.config.cliOnlyChanged) {
254
262
  if (testRun.config.cliArgs.length) {
@@ -286,14 +286,17 @@ class TestServerDispatcher {
286
286
  const config = await this._loadConfigOrReportError(new import_internalReporter.InternalReporter([wireReporter]), overrides);
287
287
  if (!config)
288
288
  return { status: "failed" };
289
- const testIdSet = params.testIds ? new Set(params.testIds) : null;
290
289
  config.cliListOnly = false;
291
290
  config.cliPassWithNoTests = true;
292
291
  config.cliArgs = params.locations || [];
293
292
  config.cliGrep = params.grep;
294
293
  config.cliGrepInvert = params.grepInvert;
295
294
  config.cliProjectFilter = params.projects?.length ? params.projects : void 0;
296
- config.testIdMatcher = testIdSet ? (id) => testIdSet.has(id) : void 0;
295
+ config.preOnlyTestFilters = [];
296
+ if (params.testIds) {
297
+ const testIdSet = new Set(params.testIds);
298
+ config.preOnlyTestFilters.push((test) => testIdSet.has(test.id));
299
+ }
297
300
  const configReporters = await (0, import_reporters.createReporters)(config, "test", true);
298
301
  const reporter = new import_internalReporter.InternalReporter([...configReporters, wireReporter]);
299
302
  const stop = new import_utils.ManualPromise();
package/lib/util.js CHANGED
@@ -47,6 +47,7 @@ __export(util_exports, {
47
47
  formatLocation: () => formatLocation,
48
48
  getContainedPath: () => getContainedPath,
49
49
  getPackageJsonPath: () => getPackageJsonPath,
50
+ loadTestFilterFile: () => loadTestFilterFile,
50
51
  mergeObjects: () => mergeObjects,
51
52
  normalizeAndSaveAttachment: () => normalizeAndSaveAttachment,
52
53
  relativeFilePath: () => relativeFilePath,
@@ -390,6 +391,22 @@ function stepTitle(category, title) {
390
391
  return `[${category}] ${title}`;
391
392
  }
392
393
  }
394
+ async function loadTestFilterFile(filePath) {
395
+ try {
396
+ const data = JSON.parse(await import_fs.default.promises.readFile(filePath, "utf8"));
397
+ if (!data || typeof data !== "object" || !Array.isArray(data.titlePath) || !data.titlePath.every((path2) => Array.isArray(path2)))
398
+ throw new Error(`Wrong test filter file format`);
399
+ const toId = (titlePath) => {
400
+ const [project, file, ...titles] = titlePath;
401
+ return `${project}${(0, import_utils.toPosixPath)(file)}${titles.join("")}`;
402
+ };
403
+ const ids = new Set(data.titlePath.map((titlePath) => toId(titlePath)));
404
+ return (test) => ids.has(toId(test.titlePath()));
405
+ } catch (error) {
406
+ (0, import_utils.rewriteErrorMessage)(error, `Failed to read test filter file "${filePath}": ${error.message}`);
407
+ throw error;
408
+ }
409
+ }
393
410
  // Annotate the CommonJS export names for ESM import in node:
394
411
  0 && (module.exports = {
395
412
  addSuffixToFilePath,
@@ -411,6 +428,7 @@ function stepTitle(category, title) {
411
428
  formatLocation,
412
429
  getContainedPath,
413
430
  getPackageJsonPath,
431
+ loadTestFilterFile,
414
432
  mergeObjects,
415
433
  normalizeAndSaveAttachment,
416
434
  relativeFilePath,
@@ -32,11 +32,12 @@ class Fixture {
32
32
  this.runner = runner;
33
33
  this.registration = registration;
34
34
  this.value = null;
35
- const shouldGenerateStep = !this.registration.box && !this.registration.option;
36
35
  const isUserFixture = this.registration.location && (0, import_util.filterStackFile)(this.registration.location.file);
37
36
  const title = this.registration.customTitle || this.registration.name;
38
37
  const location = isUserFixture ? this.registration.location : void 0;
39
- this._stepInfo = shouldGenerateStep ? { title, category: "fixture", location } : void 0;
38
+ this._stepInfo = { title, category: "fixture", location };
39
+ if (this.registration.box)
40
+ this._stepInfo.visibility = isUserFixture ? "hidden" : "internal";
40
41
  this._setupDescription = {
41
42
  title,
42
43
  phase: "setup",
@@ -54,11 +55,9 @@ class Fixture {
54
55
  this.value = this.registration.fn;
55
56
  return;
56
57
  }
57
- const run = () => testInfo._runWithTimeout({ ...runnable, fixture: this._setupDescription }, () => this._setupInternal(testInfo));
58
- if (this._stepInfo)
59
- await testInfo._runAsStep(this._stepInfo, run);
60
- else
61
- await run();
58
+ await testInfo._runAsStep(this._stepInfo, async () => {
59
+ await testInfo._runWithTimeout({ ...runnable, fixture: this._setupDescription }, () => this._setupInternal(testInfo));
60
+ });
62
61
  }
63
62
  async _setupInternal(testInfo) {
64
63
  const params = {};
@@ -107,11 +106,9 @@ class Fixture {
107
106
  try {
108
107
  const fixtureRunnable = { ...runnable, fixture: this._teardownDescription };
109
108
  if (!testInfo._timeoutManager.isTimeExhaustedFor(fixtureRunnable)) {
110
- const run = () => testInfo._runWithTimeout(fixtureRunnable, () => this._teardownInternal());
111
- if (this._stepInfo)
112
- await testInfo._runAsStep(this._stepInfo, run);
113
- else
114
- await run();
109
+ await testInfo._runAsStep(this._stepInfo, async () => {
110
+ await testInfo._runWithTimeout(fixtureRunnable, () => this._teardownInternal());
111
+ });
115
112
  }
116
113
  } finally {
117
114
  for (const dep of this._deps)
@@ -185,18 +185,22 @@ class TestInfoImpl {
185
185
  parentStep = this._parentStep();
186
186
  }
187
187
  const filteredStack = (0, import_util.filteredStackTrace)((0, import_utils.captureRawStack)());
188
- data.boxedStack = parentStep?.boxedStack;
189
- if (!data.boxedStack && data.box) {
190
- data.boxedStack = filteredStack.slice(1);
191
- data.location = data.location || data.boxedStack[0];
188
+ let boxedStack = parentStep?.boxedStack;
189
+ let location = data.location;
190
+ if (!boxedStack && data.box) {
191
+ boxedStack = filteredStack.slice(1);
192
+ location = location || boxedStack[0];
192
193
  }
193
- data.location = data.location || filteredStack[0];
194
- const attachmentIndices = [];
194
+ location = location || filteredStack[0];
195
+ const visibility = parentStep?.visibility === "internal" || data.visibility === "internal" ? "internal" : parentStep?.visibility === "hidden" || data.visibility === "hidden" ? "hidden" : "default";
195
196
  const step = {
196
- stepId,
197
197
  ...data,
198
+ stepId,
199
+ visibility,
200
+ boxedStack,
201
+ location,
198
202
  steps: [],
199
- attachmentIndices,
203
+ attachmentIndices: [],
200
204
  info: new TestStepInfoImpl(this, stepId, data.title, parentStep?.info),
201
205
  complete: (result) => {
202
206
  if (step.endWallTime)
@@ -206,9 +210,9 @@ class TestInfoImpl {
206
210
  if (typeof result.error === "object" && !result.error?.[stepSymbol])
207
211
  result.error[stepSymbol] = step;
208
212
  const error = (0, import_util2.testInfoError)(result.error);
209
- if (data.boxedStack)
213
+ if (step.boxedStack)
210
214
  error.stack = `${error.message}
211
- ${(0, import_utils.stringifyStackFrames)(data.boxedStack).join("\n")}`;
215
+ ${(0, import_utils.stringifyStackFrames)(step.boxedStack).join("\n")}`;
212
216
  step.error = error;
213
217
  }
214
218
  if (!step.error) {
@@ -220,39 +224,50 @@ ${(0, import_utils.stringifyStackFrames)(data.boxedStack).join("\n")}`;
220
224
  }
221
225
  }
222
226
  }
223
- const payload2 = {
224
- testId: this.testId,
225
- stepId,
226
- wallTime: step.endWallTime,
227
- error: step.error,
228
- suggestedRebaseline: result.suggestedRebaseline,
229
- annotations: step.info.annotations
230
- };
231
- this._onStepEnd(payload2);
232
- const errorForTrace = step.error ? { name: "", message: step.error.message || "", stack: step.error.stack } : void 0;
233
- const attachments = attachmentIndices.map((i) => this.attachments[i]);
234
- this._tracing.appendAfterActionForStep(stepId, errorForTrace, attachments, step.info.annotations);
227
+ if (visibility === "default") {
228
+ const payload = {
229
+ testId: this.testId,
230
+ stepId,
231
+ wallTime: step.endWallTime,
232
+ error: step.error,
233
+ suggestedRebaseline: result.suggestedRebaseline,
234
+ annotations: step.info.annotations
235
+ };
236
+ this._onStepEnd(payload);
237
+ }
238
+ if (visibility !== "internal") {
239
+ const errorForTrace = step.error ? { name: "", message: step.error.message || "", stack: step.error.stack } : void 0;
240
+ const attachments = step.attachmentIndices.map((i) => this.attachments[i]);
241
+ this._tracing.appendAfterActionForStep(stepId, errorForTrace, attachments, step.info.annotations);
242
+ }
235
243
  }
236
244
  };
237
245
  const parentStepList = parentStep ? parentStep.steps : this._steps;
238
246
  parentStepList.push(step);
239
247
  this._stepMap.set(stepId, step);
240
- const payload = {
241
- testId: this.testId,
242
- stepId,
243
- parentStepId: parentStep ? parentStep.stepId : void 0,
244
- title: data.title,
245
- category: data.category,
246
- wallTime: Date.now(),
247
- location: data.location
248
- };
249
- this._onStepBegin(payload);
250
- this._tracing.appendBeforeActionForStep(stepId, parentStep?.stepId, {
251
- title: data.title,
252
- category: data.category,
253
- params: data.params,
254
- stack: data.location ? [data.location] : []
255
- });
248
+ if (visibility === "default") {
249
+ const payload = {
250
+ testId: this.testId,
251
+ stepId,
252
+ parentStepId: parentStep ? parentStep.stepId : void 0,
253
+ title: step.title,
254
+ category: step.category,
255
+ wallTime: Date.now(),
256
+ location: step.location
257
+ };
258
+ this._onStepBegin(payload);
259
+ }
260
+ if (visibility !== "internal") {
261
+ this._tracing.appendBeforeActionForStep({
262
+ stepId,
263
+ parentId: parentStep?.stepId,
264
+ title: step.title,
265
+ category: step.category,
266
+ params: step.params,
267
+ stack: step.location ? [step.location] : [],
268
+ visibility: visibility === "hidden" ? "hidden" : void 0
269
+ });
270
+ }
256
271
  return step;
257
272
  }
258
273
  _interrupt() {
@@ -327,9 +342,9 @@ ${(0, import_utils.stringifyStackFrames)(step.boxedStack).join("\n")}`;
327
342
  if (stepId) {
328
343
  this._stepMap.get(stepId).attachmentIndices.push(index);
329
344
  } else {
330
- const callId = `attach@${(0, import_utils.createGuid)()}`;
331
- this._tracing.appendBeforeActionForStep(callId, void 0, { title: attachment.name, category: "test.attach", stack: [] });
332
- this._tracing.appendAfterActionForStep(callId, void 0, [attachment]);
345
+ const stepId2 = `attach@${(0, import_utils.createGuid)()}`;
346
+ this._tracing.appendBeforeActionForStep({ stepId: stepId2, title: attachment.name, category: "test.attach", stack: [] });
347
+ this._tracing.appendAfterActionForStep(stepId2, void 0, [attachment]);
333
348
  }
334
349
  this._onAttach({
335
350
  testId: this.testId,
@@ -221,18 +221,19 @@ class TestTracing {
221
221
  base64: typeof chunk === "string" ? void 0 : chunk.toString("base64")
222
222
  });
223
223
  }
224
- appendBeforeActionForStep(callId, parentId, options) {
224
+ appendBeforeActionForStep(options) {
225
225
  this._appendTraceEvent({
226
226
  type: "before",
227
- callId,
228
- stepId: callId,
229
- parentId,
227
+ callId: options.stepId,
228
+ stepId: options.stepId,
229
+ parentId: options.parentId,
230
230
  startTime: (0, import_utils.monotonicTime)(),
231
231
  class: "Test",
232
- method: "step",
232
+ method: options.category,
233
233
  title: (0, import_util.stepTitle)(options.category, options.title),
234
234
  params: Object.fromEntries(Object.entries(options.params || {}).map(([name, value]) => [name, generatePreview(value)])),
235
- stack: options.stack
235
+ stack: options.stack,
236
+ visibility: options.visibility
236
237
  });
237
238
  }
238
239
  appendAfterActionForStep(callId, error, attachments = [], annotations) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playwright",
3
- "version": "1.55.0-alpha-2025-08-06",
3
+ "version": "1.55.0-alpha-2025-08-08",
4
4
  "description": "A high-level API to automate web browsers",
5
5
  "repository": {
6
6
  "type": "git",
@@ -56,7 +56,7 @@
56
56
  },
57
57
  "license": "Apache-2.0",
58
58
  "dependencies": {
59
- "playwright-core": "1.55.0-alpha-2025-08-06"
59
+ "playwright-core": "1.55.0-alpha-2025-08-08"
60
60
  },
61
61
  "optionalDependencies": {
62
62
  "fsevents": "2.3.2"