playwright 1.54.0-alpha-2025-07-07 → 1.54.0-alpha-2025-07-09

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.

@@ -237,7 +237,11 @@ class TestCase extends Base {
237
237
  return status === "expected" || status === "flaky" || status === "skipped";
238
238
  }
239
239
  get tags() {
240
- return this._grepTitle().match(/@[\S]+/g) || [];
240
+ const titleTags = this._grepBaseTitlePath().join(" ").match(/@[\S]+/g) || [];
241
+ return [
242
+ ...titleTags,
243
+ ...this._tags
244
+ ];
241
245
  }
242
246
  _serialize() {
243
247
  return {
@@ -298,10 +302,14 @@ class TestCase extends Base {
298
302
  this.results.push(result);
299
303
  return result;
300
304
  }
301
- _grepTitle() {
305
+ _grepBaseTitlePath() {
302
306
  const path = [];
303
307
  this.parent._collectGrepTitlePath(path);
304
308
  path.push(this.title);
309
+ return path;
310
+ }
311
+ _grepTitleWithTags() {
312
+ const path = this._grepBaseTitlePath();
305
313
  path.push(...this._tags);
306
314
  return path.join(" ");
307
315
  }
@@ -96,7 +96,7 @@ class TestTypeImpl {
96
96
  body = fn;
97
97
  details = fnOrDetails;
98
98
  }
99
- const validatedDetails = validateTestDetails(details);
99
+ const validatedDetails = validateTestDetails(details, location);
100
100
  const test = new import_test.TestCase(title, body, this, location);
101
101
  test._requireFile = suite._requireFile;
102
102
  test.annotations.push(...validatedDetails.annotations);
@@ -105,9 +105,9 @@ class TestTypeImpl {
105
105
  if (type === "only" || type === "fail.only")
106
106
  test._only = true;
107
107
  if (type === "skip" || type === "fixme" || type === "fail")
108
- test.annotations.push({ type });
108
+ test.annotations.push({ type, location });
109
109
  else if (type === "fail.only")
110
- test.annotations.push({ type: "fail" });
110
+ test.annotations.push({ type: "fail", location });
111
111
  }
112
112
  _describe(type, location, titleOrFn, fnOrDetails, fn) {
113
113
  throwIfRunningInsideJest();
@@ -130,7 +130,7 @@ class TestTypeImpl {
130
130
  details = fnOrDetails;
131
131
  body = fn;
132
132
  }
133
- const validatedDetails = validateTestDetails(details);
133
+ const validatedDetails = validateTestDetails(details, location);
134
134
  const child = new import_test.Suite(title, "describe");
135
135
  child._requireFile = suite._requireFile;
136
136
  child.location = location;
@@ -144,7 +144,7 @@ class TestTypeImpl {
144
144
  if (type === "parallel" || type === "parallel.only")
145
145
  child._parallelMode = "parallel";
146
146
  if (type === "skip" || type === "fixme")
147
- child._staticAnnotations.push({ type });
147
+ child._staticAnnotations.push({ type, location });
148
148
  for (let parent = suite; parent; parent = parent.parent) {
149
149
  if (parent._parallelMode === "serial" && child._parallelMode === "parallel")
150
150
  throw new Error("describe.parallel cannot be nested inside describe.serial");
@@ -203,7 +203,7 @@ class TestTypeImpl {
203
203
  if (modifierArgs.length >= 1 && !modifierArgs[0])
204
204
  return;
205
205
  const description = modifierArgs[1];
206
- suite._staticAnnotations.push({ type, description });
206
+ suite._staticAnnotations.push({ type, description, location });
207
207
  }
208
208
  return;
209
209
  }
@@ -212,7 +212,7 @@ class TestTypeImpl {
212
212
  throw new Error(`test.${type}() can only be called inside test, describe block or fixture`);
213
213
  if (typeof modifierArgs[0] === "function")
214
214
  throw new Error(`test.${type}() with a function can only be called inside describe block`);
215
- testInfo[type](...modifierArgs);
215
+ testInfo._modifier(type, location, modifierArgs);
216
216
  }
217
217
  _setTimeout(location, timeout) {
218
218
  const suite = (0, import_globals.currentlyLoadingFileSuite)();
@@ -241,7 +241,7 @@ class TestTypeImpl {
241
241
  let result = void 0;
242
242
  result = await (0, import_utils.raceAgainstDeadline)(async () => {
243
243
  try {
244
- return await step.info._runStepBody(expectation === "skip", body);
244
+ return await step.info._runStepBody(expectation === "skip", body, step.location);
245
245
  } catch (e) {
246
246
  if (result?.timedOut)
247
247
  testInfo._failWithError(e);
@@ -276,8 +276,9 @@ See https://playwright.dev/docs/intro for more information about Playwright Test
276
276
  );
277
277
  }
278
278
  }
279
- function validateTestDetails(details) {
280
- const annotations = Array.isArray(details.annotation) ? details.annotation : details.annotation ? [details.annotation] : [];
279
+ function validateTestDetails(details, location) {
280
+ const originalAnnotations = Array.isArray(details.annotation) ? details.annotation : details.annotation ? [details.annotation] : [];
281
+ const annotations = originalAnnotations.map((annotation) => ({ ...annotation, location }));
281
282
  const tags = Array.isArray(details.tag) ? details.tag : details.tag ? [details.tag] : [];
282
283
  for (const tag of tags) {
283
284
  if (tag[0] !== "@")
@@ -128,8 +128,9 @@ class TeleReporterReceiver {
128
128
  if (!!payload.attachments)
129
129
  result.attachments = this._parseAttachments(payload.attachments);
130
130
  if (payload.annotations) {
131
+ this._absoluteAnnotationLocationsInplace(payload.annotations);
131
132
  result.annotations = payload.annotations;
132
- test.annotations = result.annotations;
133
+ test.annotations = payload.annotations;
133
134
  }
134
135
  this._reporter.onTestEnd?.(test, result);
135
136
  result._stepMap = /* @__PURE__ */ new Map();
@@ -257,8 +258,15 @@ class TeleReporterReceiver {
257
258
  test.retries = payload.retries;
258
259
  test.tags = payload.tags ?? [];
259
260
  test.annotations = payload.annotations ?? [];
261
+ this._absoluteAnnotationLocationsInplace(test.annotations);
260
262
  return test;
261
263
  }
264
+ _absoluteAnnotationLocationsInplace(annotations) {
265
+ for (const annotation of annotations) {
266
+ if (annotation.location)
267
+ annotation.location = this._absoluteLocation(annotation.location);
268
+ }
269
+ }
262
270
  _absoluteLocation(location) {
263
271
  if (!location)
264
272
  return location;
@@ -60,6 +60,7 @@ class SnapshotHelper {
60
60
  this.options = options;
61
61
  name = nameFromOptions;
62
62
  }
63
+ this.name = Array.isArray(name) ? name.join(import_path.default.sep) : name || "";
63
64
  const resolvedPaths = testInfo._resolveSnapshotPaths(matcherName === "toHaveScreenshot" ? "screenshot" : "snapshot", name, "updateSnapshotIndex", anonymousSnapshotExtension);
64
65
  this.expectedPath = resolvedPaths.absoluteSnapshotPath;
65
66
  this.attachmentBaseName = resolvedPaths.relativeOutputPath;
@@ -140,26 +141,25 @@ class SnapshotHelper {
140
141
  }
141
142
  handleDifferent(actual, expected, previous, diff, header, diffError, log, step) {
142
143
  const output = [`${header}${indent(diffError, " ")}`];
144
+ if (this.name) {
145
+ output.push("");
146
+ output.push(` Snapshot: ${this.name}`);
147
+ }
143
148
  if (expected !== void 0) {
144
149
  writeFileSync(this.legacyExpectedPath, expected);
145
150
  step?._attachToStep({ name: (0, import_util.addSuffixToFilePath)(this.attachmentBaseName, "-expected"), contentType: this.mimeType, path: this.expectedPath });
146
- output.push(`
147
- Expected: ${import_utils2.colors.yellow(this.expectedPath)}`);
148
151
  }
149
152
  if (previous !== void 0) {
150
153
  writeFileSync(this.previousPath, previous);
151
154
  step?._attachToStep({ name: (0, import_util.addSuffixToFilePath)(this.attachmentBaseName, "-previous"), contentType: this.mimeType, path: this.previousPath });
152
- output.push(`Previous: ${import_utils2.colors.yellow(this.previousPath)}`);
153
155
  }
154
156
  if (actual !== void 0) {
155
157
  writeFileSync(this.actualPath, actual);
156
158
  step?._attachToStep({ name: (0, import_util.addSuffixToFilePath)(this.attachmentBaseName, "-actual"), contentType: this.mimeType, path: this.actualPath });
157
- output.push(`Received: ${import_utils2.colors.yellow(this.actualPath)}`);
158
159
  }
159
160
  if (diff !== void 0) {
160
161
  writeFileSync(this.diffPath, diff);
161
162
  step?._attachToStep({ name: (0, import_util.addSuffixToFilePath)(this.attachmentBaseName, "-diff"), contentType: this.mimeType, path: this.diffPath });
162
- output.push(` Diff: ${import_utils2.colors.yellow(this.diffPath)}`);
163
163
  }
164
164
  if (log?.length)
165
165
  output.push((0, import_util.callLogText)(log));
@@ -307,8 +307,9 @@ function formatFailure(screen, config, test, index) {
307
307
  resultLines.push(screen.colors.gray(separator(screen, ` Retry #${result.retry}`)));
308
308
  }
309
309
  resultLines.push(...errors.map((error) => "\n" + error.message));
310
- for (let i = 0; i < result.attachments.length; ++i) {
311
- const attachment = result.attachments[i];
310
+ const attachmentGroups = groupAttachments(result.attachments);
311
+ for (let i = 0; i < attachmentGroups.length; ++i) {
312
+ const attachment = attachmentGroups[i];
312
313
  if (attachment.name === "error-context" && attachment.path) {
313
314
  resultLines.push("");
314
315
  resultLines.push(screen.colors.dim(` Error Context: ${relativeFilePath(screen, config, attachment.path)}`));
@@ -320,15 +321,30 @@ function formatFailure(screen, config, test, index) {
320
321
  if (!attachment.path && !hasPrintableContent)
321
322
  continue;
322
323
  resultLines.push("");
323
- resultLines.push(screen.colors.cyan(separator(screen, ` attachment #${i + 1}: ${attachment.name} (${attachment.contentType})`)));
324
- if (attachment.path) {
325
- const relativePath = import_path.default.relative(process.cwd(), attachment.path);
326
- resultLines.push(screen.colors.cyan(` ${relativePath}`));
324
+ resultLines.push(screen.colors.dim(separator(screen, ` attachment #${i + 1}: ${screen.colors.bold(attachment.name)} (${attachment.contentType})`)));
325
+ if (attachment.actual?.path) {
326
+ if (attachment.expected?.path) {
327
+ const expectedPath = relativeFilePath(screen, config, attachment.expected.path);
328
+ resultLines.push(screen.colors.dim(` Expected: ${expectedPath}`));
329
+ }
330
+ const actualPath = relativeFilePath(screen, config, attachment.actual.path);
331
+ resultLines.push(screen.colors.dim(` Received: ${actualPath}`));
332
+ if (attachment.previous?.path) {
333
+ const previousPath = relativeFilePath(screen, config, attachment.previous.path);
334
+ resultLines.push(screen.colors.dim(` Previous: ${previousPath}`));
335
+ }
336
+ if (attachment.diff?.path) {
337
+ const diffPath = relativeFilePath(screen, config, attachment.diff.path);
338
+ resultLines.push(screen.colors.dim(` Diff: ${diffPath}`));
339
+ }
340
+ } else if (attachment.path) {
341
+ const relativePath = relativeFilePath(screen, config, attachment.path);
342
+ resultLines.push(screen.colors.dim(` ${relativePath}`));
327
343
  if (attachment.name === "trace") {
328
344
  const packageManagerCommand = (0, import_utils.getPackageManagerExecCommand)();
329
- resultLines.push(screen.colors.cyan(` Usage:`));
345
+ resultLines.push(screen.colors.dim(` Usage:`));
330
346
  resultLines.push("");
331
- resultLines.push(screen.colors.cyan(` ${packageManagerCommand} playwright show-trace ${quotePathIfNeeded(relativePath)}`));
347
+ resultLines.push(screen.colors.dim(` ${packageManagerCommand} playwright show-trace ${quotePathIfNeeded(relativePath)}`));
332
348
  resultLines.push("");
333
349
  }
334
350
  } else {
@@ -337,10 +353,10 @@ function formatFailure(screen, config, test, index) {
337
353
  if (text.length > 300)
338
354
  text = text.slice(0, 300) + "...";
339
355
  for (const line of text.split("\n"))
340
- resultLines.push(screen.colors.cyan(` ${line}`));
356
+ resultLines.push(screen.colors.dim(` ${line}`));
341
357
  }
342
358
  }
343
- resultLines.push(screen.colors.cyan(separator(screen, " ")));
359
+ resultLines.push(screen.colors.dim(separator(screen, " ")));
344
360
  }
345
361
  lines.push(...resultLines);
346
362
  }
@@ -460,7 +476,7 @@ function separator(screen, text = "") {
460
476
  if (text)
461
477
  text += " ";
462
478
  const columns = Math.min(100, screen.ttyWidth || 100);
463
- return text + screen.colors.dim("\u2500".repeat(Math.max(0, columns - text.length)));
479
+ return text + screen.colors.dim("\u2500".repeat(Math.max(0, columns - (0, import_util.stripAnsiEscapes)(text).length)));
464
480
  }
465
481
  function indent(lines, tab) {
466
482
  return lines.replace(/^(?=.+$)/gm, tab);
@@ -537,6 +553,37 @@ function resolveOutputFile(reporterName, options) {
537
553
  outputFile = import_path.default.resolve(outputDir, reportName);
538
554
  return { outputFile, outputDir };
539
555
  }
556
+ function groupAttachments(attachments) {
557
+ const result = [];
558
+ const attachmentsByPrefix = /* @__PURE__ */ new Map();
559
+ for (const attachment of attachments) {
560
+ if (!attachment.path) {
561
+ result.push(attachment);
562
+ continue;
563
+ }
564
+ const match = attachment.name.match(/^(.*)-(expected|actual|diff|previous)(\.[^.]+)?$/);
565
+ if (!match) {
566
+ result.push(attachment);
567
+ continue;
568
+ }
569
+ const [, name, category] = match;
570
+ let group = attachmentsByPrefix.get(name);
571
+ if (!group) {
572
+ group = { ...attachment, name };
573
+ attachmentsByPrefix.set(name, group);
574
+ result.push(group);
575
+ }
576
+ if (category === "expected")
577
+ group.expected = attachment;
578
+ else if (category === "actual")
579
+ group.actual = attachment;
580
+ else if (category === "diff")
581
+ group.diff = attachment;
582
+ else if (category === "previous")
583
+ group.previous = attachment;
584
+ }
585
+ return result;
586
+ }
540
587
  // Annotate the CommonJS export names for ESM import in node:
541
588
  0 && (module.exports = {
542
589
  TerminalReporter,
@@ -107,7 +107,13 @@ class HtmlReporter {
107
107
  async onEnd(result) {
108
108
  const projectSuites = this.suite.suites;
109
109
  await (0, import_utils.removeFolders)([this._outputFolder]);
110
- const builder = new HtmlBuilder(this.config, this._outputFolder, this._attachmentsBaseURL, process.env.PLAYWRIGHT_HTML_TITLE || this._options.title, this._options.snippets);
110
+ let noSnippets;
111
+ if (process.env.PLAYWRIGHT_HTML_NO_SNIPPETS === "false" || process.env.PLAYWRIGHT_HTML_NO_SNIPPETS === "0")
112
+ noSnippets = false;
113
+ else if (process.env.PLAYWRIGHT_HTML_NO_SNIPPETS)
114
+ noSnippets = true;
115
+ noSnippets = noSnippets || this._options.noSnippets;
116
+ const builder = new HtmlBuilder(this.config, this._outputFolder, this._attachmentsBaseURL, process.env.PLAYWRIGHT_HTML_TITLE || this._options.title, noSnippets);
111
117
  this._buildResult = await builder.build(this.config.metadata, projectSuites, result, this._topLevelErrors);
112
118
  }
113
119
  async onExit() {
@@ -191,12 +197,12 @@ function startHtmlReportServer(folder) {
191
197
  return server;
192
198
  }
193
199
  class HtmlBuilder {
194
- constructor(config, outputDir, attachmentsBaseURL, title, snippets = true) {
200
+ constructor(config, outputDir, attachmentsBaseURL, title, noSnippets = false) {
195
201
  this._stepsInFile = new import_utils.MultiMap();
196
202
  this._hasTraces = false;
197
203
  this._config = config;
198
204
  this._reportFolder = outputDir;
199
- this._snippets = snippets;
205
+ this._noSnippets = noSnippets;
200
206
  import_fs.default.mkdirSync(this._reportFolder, { recursive: true });
201
207
  this._dataZipFile = new import_zipBundle.yazl.ZipFile();
202
208
  this._attachmentsBaseURL = attachmentsBaseURL;
@@ -225,7 +231,7 @@ class HtmlBuilder {
225
231
  }
226
232
  }
227
233
  }
228
- if (this._snippets)
234
+ if (!this._noSnippets)
229
235
  createSnippets(this._stepsInFile);
230
236
  let ok = true;
231
237
  for (const [fileId, { testFile, testFileSummary }] of data) {
@@ -431,7 +437,15 @@ class HtmlBuilder {
431
437
  }).filter(Boolean);
432
438
  }
433
439
  _serializeAnnotations(annotations) {
434
- return annotations.map((a) => ({ type: a.type, description: a.description === void 0 ? void 0 : String(a.description) }));
440
+ return annotations.map((a) => ({
441
+ type: a.type,
442
+ description: a.description === void 0 ? void 0 : String(a.description),
443
+ location: a.location ? {
444
+ file: a.location.file,
445
+ line: a.location.line,
446
+ column: a.location.column
447
+ } : void 0
448
+ }));
435
449
  }
436
450
  _createTestResult(test, result) {
437
451
  return {
@@ -406,7 +406,10 @@ class PathSeparatorPatcher {
406
406
  return;
407
407
  }
408
408
  if (jsonEvent.method === "onTestEnd") {
409
+ const test = jsonEvent.params.test;
410
+ test.annotations?.forEach((annotation) => this._updateAnnotationLocation(annotation));
409
411
  const testResult = jsonEvent.params.result;
412
+ testResult.annotations?.forEach((annotation) => this._updateAnnotationLocation(annotation));
410
413
  testResult.errors.forEach((error) => this._updateErrorLocations(error));
411
414
  (testResult.attachments ?? []).forEach((attachment) => {
412
415
  if (attachment.path)
@@ -422,6 +425,7 @@ class PathSeparatorPatcher {
422
425
  if (jsonEvent.method === "onStepEnd") {
423
426
  const step = jsonEvent.params.step;
424
427
  this._updateErrorLocations(step.error);
428
+ step.annotations?.forEach((annotation) => this._updateAnnotationLocation(annotation));
425
429
  return;
426
430
  }
427
431
  if (jsonEvent.method === "onAttach") {
@@ -444,10 +448,12 @@ class PathSeparatorPatcher {
444
448
  if (isFileSuite)
445
449
  suite.title = this._updatePath(suite.title);
446
450
  for (const entry of suite.entries) {
447
- if ("testId" in entry)
451
+ if ("testId" in entry) {
448
452
  this._updateLocation(entry.location);
449
- else
453
+ entry.annotations?.forEach((annotation) => this._updateAnnotationLocation(annotation));
454
+ } else {
450
455
  this._updateSuite(entry);
456
+ }
451
457
  }
452
458
  }
453
459
  _updateErrorLocations(error) {
@@ -456,6 +462,9 @@ class PathSeparatorPatcher {
456
462
  error = error.cause;
457
463
  }
458
464
  }
465
+ _updateAnnotationLocation(annotation) {
466
+ this._updateLocation(annotation.location);
467
+ }
459
468
  _updateLocation(location) {
460
469
  if (location)
461
470
  location.file = this._updatePath(location.file);
@@ -204,7 +204,7 @@ class TeleReporterEmitter {
204
204
  retries: test.retries,
205
205
  tags: test.tags,
206
206
  repeatEachIndex: test.repeatEachIndex,
207
- annotations: test.annotations
207
+ annotations: this._relativeAnnotationLocations(test.annotations)
208
208
  };
209
209
  }
210
210
  _serializeResultStart(result) {
@@ -222,7 +222,7 @@ class TeleReporterEmitter {
222
222
  duration: result.duration,
223
223
  status: result.status,
224
224
  errors: result.errors,
225
- annotations: result.annotations?.length ? result.annotations : void 0
225
+ annotations: result.annotations?.length ? this._relativeAnnotationLocations(result.annotations) : void 0
226
226
  };
227
227
  }
228
228
  _sendNewAttachments(result, testId) {
@@ -266,9 +266,15 @@ class TeleReporterEmitter {
266
266
  duration: step.duration,
267
267
  error: step.error,
268
268
  attachments: step.attachments.length ? step.attachments.map((a) => result.attachments.indexOf(a)) : void 0,
269
- annotations: step.annotations.length ? step.annotations : void 0
269
+ annotations: step.annotations.length ? this._relativeAnnotationLocations(step.annotations) : void 0
270
270
  };
271
271
  }
272
+ _relativeAnnotationLocations(annotations) {
273
+ return annotations.map((annotation) => ({
274
+ ...annotation,
275
+ location: annotation.location ? this._relativeLocation(annotation.location) : void 0
276
+ }));
277
+ }
272
278
  _relativeLocation(location) {
273
279
  if (!location)
274
280
  return location;
@@ -184,7 +184,7 @@ function createProjectSuite(project, fileSuites) {
184
184
  const grepMatcher = (0, import_util.createTitleMatcher)(project.project.grep);
185
185
  const grepInvertMatcher = project.project.grepInvert ? (0, import_util.createTitleMatcher)(project.project.grepInvert) : null;
186
186
  (0, import_suiteUtils.filterTestsRemoveEmptySuites)(projectSuite, (test) => {
187
- const grepTitle = test._grepTitle();
187
+ const grepTitle = test._grepTitleWithTags();
188
188
  if (grepInvertMatcher?.(grepTitle))
189
189
  return false;
190
190
  return grepMatcher(grepTitle);
@@ -200,7 +200,7 @@ function filterProjectSuite(projectSuite, options) {
200
200
  if (options.testIdMatcher)
201
201
  (0, import_suiteUtils.filterByTestIds)(result, options.testIdMatcher);
202
202
  (0, import_suiteUtils.filterTestsRemoveEmptySuites)(result, (test) => {
203
- if (options.cliTitleMatcher && !options.cliTitleMatcher(test._grepTitle()))
203
+ if (options.cliTitleMatcher && !options.cliTitleMatcher(test._grepTitleWithTags()))
204
204
  return false;
205
205
  if (options.additionalFileMatcher && !options.additionalFileMatcher(test.location.file))
206
206
  return false;
@@ -41,6 +41,7 @@ var import_timeoutManager = require("./timeoutManager");
41
41
  var import_util = require("../util");
42
42
  var import_testTracing = require("./testTracing");
43
43
  var import_util2 = require("./util");
44
+ var import_transform = require("../transform/transform");
44
45
  class TestInfoImpl {
45
46
  constructor(configInternal, projectInternal, workerParams, test, retry, onStepBegin, onStepEnd, onAttach) {
46
47
  this._snapshotNames = { lastAnonymousSnapshotIndex: 0, lastNamedSnapshotIndex: {} };
@@ -110,6 +111,10 @@ class TestInfoImpl {
110
111
  return this.attachments.length;
111
112
  };
112
113
  this._tracing = new import_testTracing.TestTracing(this, workerParams.artifactsDir);
114
+ this.skip = (0, import_transform.wrapFunctionWithLocation)((location, ...args) => this._modifier("skip", location, args));
115
+ this.fixme = (0, import_transform.wrapFunctionWithLocation)((location, ...args) => this._modifier("fixme", location, args));
116
+ this.fail = (0, import_transform.wrapFunctionWithLocation)((location, ...args) => this._modifier("fail", location, args));
117
+ this.slow = (0, import_transform.wrapFunctionWithLocation)((location, ...args) => this._modifier("slow", location, args));
113
118
  }
114
119
  get error() {
115
120
  return this.errors[0];
@@ -135,7 +140,7 @@ class TestInfoImpl {
135
140
  static _defaultDeadlineForMatcher(timeout) {
136
141
  return { deadline: timeout ? (0, import_utils.monotonicTime)() + timeout : 0, timeoutMessage: `Timeout ${timeout}ms exceeded while waiting on the predicate` };
137
142
  }
138
- _modifier(type, modifierArgs) {
143
+ _modifier(type, location, modifierArgs) {
139
144
  if (typeof modifierArgs[1] === "function") {
140
145
  throw new Error([
141
146
  "It looks like you are calling test.skip() inside the test and pass a callback.",
@@ -148,7 +153,7 @@ class TestInfoImpl {
148
153
  if (modifierArgs.length >= 1 && !modifierArgs[0])
149
154
  return;
150
155
  const description = modifierArgs[1];
151
- this.annotations.push({ type, description });
156
+ this.annotations.push({ type, description, location });
152
157
  if (type === "slow") {
153
158
  this._timeoutManager.slow();
154
159
  } else if (type === "skip" || type === "fixme") {
@@ -418,18 +423,6 @@ ${(0, import_utils.stringifyStackFrames)(step.boxedStack).join("\n")}`;
418
423
  throw new Error(`testInfo.snapshotPath: unknown kind "${kind}", must be one of "snapshot", "screenshot" or "aria"`);
419
424
  return this._resolveSnapshotPaths(kind, name.length <= 1 ? name[0] : name, "dontUpdateSnapshotIndex").absoluteSnapshotPath;
420
425
  }
421
- skip(...args) {
422
- this._modifier("skip", args);
423
- }
424
- fixme(...args) {
425
- this._modifier("fixme", args);
426
- }
427
- fail(...args) {
428
- this._modifier("fail", args);
429
- }
430
- slow(...args) {
431
- this._modifier("slow", args);
432
- }
433
426
  setTimeout(timeout) {
434
427
  this._timeoutManager.setTimeout(timeout);
435
428
  }
@@ -439,10 +432,17 @@ class TestStepInfoImpl {
439
432
  this.annotations = [];
440
433
  this._testInfo = testInfo;
441
434
  this._stepId = stepId;
435
+ this.skip = (0, import_transform.wrapFunctionWithLocation)((location, ...args) => {
436
+ if (args.length > 0 && !args[0])
437
+ return;
438
+ const description = args[1];
439
+ this.annotations.push({ type: "skip", description, location });
440
+ throw new StepSkipError(description);
441
+ });
442
442
  }
443
- async _runStepBody(skip, body) {
443
+ async _runStepBody(skip, body, location) {
444
444
  if (skip) {
445
- this.annotations.push({ type: "skip" });
445
+ this.annotations.push({ type: "skip", location });
446
446
  return void 0;
447
447
  }
448
448
  try {
@@ -459,13 +459,6 @@ class TestStepInfoImpl {
459
459
  async attach(name, options) {
460
460
  this._attachToStep(await (0, import_util.normalizeAndSaveAttachment)(this._testInfo.outputPath(), name, options));
461
461
  }
462
- skip(...args) {
463
- if (args.length > 0 && !args[0])
464
- return;
465
- const description = args[1];
466
- this.annotations.push({ type: "skip", description });
467
- throw new StepSkipError(description);
468
- }
469
462
  }
470
463
  class TestSkipError extends Error {
471
464
  }
@@ -391,7 +391,7 @@ class WorkerMain extends import_process.ProcessRunner {
391
391
  continue;
392
392
  const fn = async (fixtures) => {
393
393
  const result = await modifier.fn(fixtures);
394
- testInfo[modifier.type](!!result, modifier.description);
394
+ testInfo._modifier(modifier.type, modifier.location, [!!result, modifier.description]);
395
395
  };
396
396
  (0, import_fixtures.inheritFixtureNames)(modifier.fn, fn);
397
397
  runnables.push({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playwright",
3
- "version": "1.54.0-alpha-2025-07-07",
3
+ "version": "1.54.0-alpha-2025-07-09",
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.54.0-alpha-2025-07-07"
59
+ "playwright-core": "1.54.0-alpha-2025-07-09"
60
60
  },
61
61
  "optionalDependencies": {
62
62
  "fsevents": "2.3.2"
package/types/test.d.ts CHANGED
@@ -22,7 +22,7 @@ export type BlobReporterOptions = { outputDir?: string, fileName?: string };
22
22
  export type ListReporterOptions = { printSteps?: boolean };
23
23
  export type JUnitReporterOptions = { outputFile?: string, stripANSIControlSequences?: boolean, includeProjectInTestName?: boolean };
24
24
  export type JsonReporterOptions = { outputFile?: string };
25
- export type HtmlReporterOptions = { outputFolder?: string, open?: 'always' | 'never' | 'on-failure', host?: string, port?: number, attachmentsBaseURL?: string, title?: string, snippets?: boolean };
25
+ export type HtmlReporterOptions = { outputFolder?: string, open?: 'always' | 'never' | 'on-failure', host?: string, port?: number, attachmentsBaseURL?: string, title?: string, noSnippets?: boolean };
26
26
 
27
27
  export type ReporterDescription = Readonly<
28
28
  ['blob'] | ['blob', BlobReporterOptions] |
@@ -2325,6 +2325,11 @@ export interface TestInfo {
2325
2325
  * Optional description.
2326
2326
  */
2327
2327
  description?: string;
2328
+
2329
+ /**
2330
+ * Optional location in the source where the annotation is added.
2331
+ */
2332
+ location?: Location;
2328
2333
  }>;
2329
2334
 
2330
2335
  /**
@@ -2575,7 +2580,9 @@ export type TestDetailsAnnotation = {
2575
2580
  description?: string;
2576
2581
  };
2577
2582
 
2578
- export type TestAnnotation = TestDetailsAnnotation;
2583
+ export type TestAnnotation = TestDetailsAnnotation & {
2584
+ location?: Location;
2585
+ };
2579
2586
 
2580
2587
  export type TestDetails = {
2581
2588
  tag?: string | string[];
@@ -451,6 +451,11 @@ export interface TestCase {
451
451
  * Optional description.
452
452
  */
453
453
  description?: string;
454
+
455
+ /**
456
+ * Optional location in the source where the annotation is added.
457
+ */
458
+ location?: Location;
454
459
  }>;
455
460
 
456
461
  /**
@@ -607,6 +612,11 @@ export interface TestResult {
607
612
  * Optional description.
608
613
  */
609
614
  description?: string;
615
+
616
+ /**
617
+ * Optional location in the source where the annotation is added.
618
+ */
619
+ location?: Location;
610
620
  }>;
611
621
 
612
622
  /**
@@ -722,6 +732,11 @@ export interface TestStep {
722
732
  * Optional description.
723
733
  */
724
734
  description?: string;
735
+
736
+ /**
737
+ * Optional location in the source where the annotation is added.
738
+ */
739
+ location?: Location;
725
740
  }>;
726
741
 
727
742
  /**