Skip to content

Commit db2a52d

Browse files
authored
feat: add location information for annotations (#36542)
1 parent bbf198a commit db2a52d

File tree

19 files changed

+191
-102
lines changed

19 files changed

+191
-102
lines changed

docs/src/test-api/class-testinfo.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ test('basic test', async ({ page }, testInfo) => {
1919
- type: <[Array]<[Object]>>
2020
- `type` <[string]> Annotation type, for example `'skip'` or `'fail'`.
2121
- `description` ?<[string]> Optional description.
22+
- `location` ?<[Location]> Optional location in the source where the annotation is added.
2223

2324
The list of annotations applicable to the current test. Includes annotations from the test, annotations from all [`method: Test.describe`] groups the test belongs to and file-level annotations for the test file.
2425

docs/src/test-reporter-api/class-testcase.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
- type: <[Array]<[Object]>>
1010
- `type` <[string]> Annotation type, for example `'skip'` or `'fail'`.
1111
- `description` ?<[string]> Optional description.
12+
- `location` ?<[Location]> Optional location in the source where the annotation is added.
1213

1314
[`property: TestResult.annotations`] of the last test run.
1415

docs/src/test-reporter-api/class-testresult.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ The list of files or buffers attached during the test execution through [`proper
1919
- type: <[Array]<[Object]>>
2020
- `type` <[string]> Annotation type, for example `'skip'` or `'fail'`.
2121
- `description` ?<[string]> Optional description.
22+
- `location` ?<[Location]> Optional location in the source where the annotation is added.
2223

2324
The list of annotations applicable to the current test. Includes:
2425
* annotations defined on the test or suite via [`method: Test.(call)`] and [`method: Test.describe`];

docs/src/test-reporter-api/class-teststep.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ List of steps inside this step.
5858
- type: <[Array]<[Object]>>
5959
- `type` <[string]> Annotation type, for example `'skip'`.
6060
- `description` ?<[string]> Optional description.
61+
- `location` ?<[Location]> Optional location in the source where the annotation is added.
6162

6263
The list of annotations applicable to the current test step.
6364

packages/playwright/src/common/testType.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export class TestTypeImpl {
102102
details = fnOrDetails;
103103
}
104104

105-
const validatedDetails = validateTestDetails(details);
105+
const validatedDetails = validateTestDetails(details, location);
106106
const test = new TestCase(title, body, this, location);
107107
test._requireFile = suite._requireFile;
108108
test.annotations.push(...validatedDetails.annotations);
@@ -112,9 +112,9 @@ export class TestTypeImpl {
112112
if (type === 'only' || type === 'fail.only')
113113
test._only = true;
114114
if (type === 'skip' || type === 'fixme' || type === 'fail')
115-
test.annotations.push({ type });
115+
test.annotations.push({ type, location });
116116
else if (type === 'fail.only')
117-
test.annotations.push({ type: 'fail' });
117+
test.annotations.push({ type: 'fail', location });
118118
}
119119

120120
private _describe(type: 'default' | 'only' | 'serial' | 'serial.only' | 'parallel' | 'parallel.only' | 'skip' | 'fixme', location: Location, titleOrFn: string | Function, fnOrDetails?: TestDetails | Function, fn?: Function) {
@@ -141,7 +141,7 @@ export class TestTypeImpl {
141141
body = fn!;
142142
}
143143

144-
const validatedDetails = validateTestDetails(details);
144+
const validatedDetails = validateTestDetails(details, location);
145145
const child = new Suite(title, 'describe');
146146
child._requireFile = suite._requireFile;
147147
child.location = location;
@@ -156,7 +156,7 @@ export class TestTypeImpl {
156156
if (type === 'parallel' || type === 'parallel.only')
157157
child._parallelMode = 'parallel';
158158
if (type === 'skip' || type === 'fixme')
159-
child._staticAnnotations.push({ type });
159+
child._staticAnnotations.push({ type, location });
160160

161161
for (let parent: Suite | undefined = suite; parent; parent = parent.parent) {
162162
if (parent._parallelMode === 'serial' && child._parallelMode === 'parallel')
@@ -227,7 +227,7 @@ export class TestTypeImpl {
227227
if (modifierArgs.length >= 1 && !modifierArgs[0])
228228
return;
229229
const description = modifierArgs[1];
230-
suite._staticAnnotations.push({ type, description });
230+
suite._staticAnnotations.push({ type, description, location });
231231
}
232232
return;
233233
}
@@ -237,7 +237,7 @@ export class TestTypeImpl {
237237
throw new Error(`test.${type}() can only be called inside test, describe block or fixture`);
238238
if (typeof modifierArgs[0] === 'function')
239239
throw new Error(`test.${type}() with a function can only be called inside describe block`);
240-
testInfo[type](...modifierArgs as [any, any]);
240+
testInfo._modifier(type, location, modifierArgs as [any, any]);
241241
}
242242

243243
private _setTimeout(location: Location, timeout: number) {
@@ -270,7 +270,7 @@ export class TestTypeImpl {
270270
let result: Awaited<ReturnType<typeof raceAgainstDeadline<T>>> | undefined = undefined;
271271
result = await raceAgainstDeadline(async () => {
272272
try {
273-
return await step.info._runStepBody(expectation === 'skip', body);
273+
return await step.info._runStepBody(expectation === 'skip', body, step.location);
274274
} catch (e) {
275275
// If the step timed out, the test fixtures will tear down, which in turn
276276
// will abort unfinished actions in the step body. Record such errors here.
@@ -309,8 +309,9 @@ function throwIfRunningInsideJest() {
309309
}
310310
}
311311

312-
function validateTestDetails(details: TestDetails) {
313-
const annotations = Array.isArray(details.annotation) ? details.annotation : (details.annotation ? [details.annotation] : []);
312+
function validateTestDetails(details: TestDetails, location: Location) {
313+
const originalAnnotations = Array.isArray(details.annotation) ? details.annotation : (details.annotation ? [details.annotation] : []);
314+
const annotations = originalAnnotations.map(annotation => ({ ...annotation, location }));
314315
const tags = Array.isArray(details.tag) ? details.tag : (details.tag ? [details.tag] : []);
315316
for (const tag of tags) {
316317
if (tag[0] !== '@')

packages/playwright/src/isomorphic/teleReceiver.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,8 +355,9 @@ export class TeleReporterReceiver {
355355
if (!!payload.attachments)
356356
result.attachments = this._parseAttachments(payload.attachments);
357357
if (payload.annotations) {
358+
this._absoluteAnnotationLocationsInplace(payload.annotations);
358359
result.annotations = payload.annotations;
359-
test.annotations = result.annotations;
360+
test.annotations = payload.annotations;
360361
}
361362
this._reporter.onTestEnd?.(test, result);
362363
// Free up the memory as won't see these step ids.
@@ -499,9 +500,17 @@ export class TeleReporterReceiver {
499500
test.retries = payload.retries;
500501
test.tags = payload.tags ?? [];
501502
test.annotations = payload.annotations ?? [];
503+
this._absoluteAnnotationLocationsInplace(test.annotations);
502504
return test;
503505
}
504506

507+
private _absoluteAnnotationLocationsInplace(annotations: TestAnnotation[]) {
508+
for (const annotation of annotations) {
509+
if (annotation.location)
510+
annotation.location = this._absoluteLocation(annotation.location);
511+
}
512+
}
513+
505514
private _absoluteLocation(location: reporterTypes.Location): reporterTypes.Location;
506515
private _absoluteLocation(location?: reporterTypes.Location): reporterTypes.Location | undefined;
507516
private _absoluteLocation(location: reporterTypes.Location | undefined): reporterTypes.Location | undefined {

packages/playwright/src/reporters/html.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,15 @@ class HtmlBuilder {
502502

503503
private _serializeAnnotations(annotations: api.TestCase['annotations']): TestAnnotation[] {
504504
// Annotations can be pushed directly, with a wrong type.
505-
return annotations.map(a => ({ type: a.type, description: a.description === undefined ? undefined : String(a.description) }));
505+
return annotations.map(a => ({
506+
type: a.type,
507+
description: a.description === undefined ? undefined : String(a.description),
508+
location: a.location ? {
509+
file: a.location.file,
510+
line: a.location.line,
511+
column: a.location.column,
512+
} : undefined,
513+
}));
506514
}
507515

508516
private _createTestResult(test: api.TestCase, result: api.TestResult): TestResult {

packages/playwright/src/reporters/merge.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { TeleReporterReceiver } from '../isomorphic/teleReceiver';
2626
import { createReporters } from '../runner/reporters';
2727
import { relativeFilePath } from '../util';
2828

29-
import type { ReporterDescription } from '../../types/test';
29+
import type { ReporterDescription, TestAnnotation } from '../../types/test';
3030
import type { TestError } from '../../types/testReporter';
3131
import type { FullConfigInternal } from '../common/config';
3232
import type { BlobReportMetadata, JsonAttachment, JsonConfig, JsonEvent, JsonFullResult, JsonLocation, JsonOnConfigureEvent, JsonOnEndEvent, JsonOnProjectEvent, JsonProject, JsonSuite, JsonTestCase } from '../isomorphic/teleReceiver';
@@ -484,7 +484,10 @@ class PathSeparatorPatcher {
484484
return;
485485
}
486486
if (jsonEvent.method === 'onTestEnd') {
487+
const test = jsonEvent.params.test;
488+
test.annotations?.forEach(annotation => this._updateAnnotationLocation(annotation));
487489
const testResult = jsonEvent.params.result;
490+
testResult.annotations?.forEach(annotation => this._updateAnnotationLocation(annotation));
488491
testResult.errors.forEach(error => this._updateErrorLocations(error));
489492
(testResult.attachments ?? []).forEach(attachment => {
490493
if (attachment.path)
@@ -500,6 +503,7 @@ class PathSeparatorPatcher {
500503
if (jsonEvent.method === 'onStepEnd') {
501504
const step = jsonEvent.params.step;
502505
this._updateErrorLocations(step.error);
506+
step.annotations?.forEach(annotation => this._updateAnnotationLocation(annotation));
503507
return;
504508
}
505509
if (jsonEvent.method === 'onAttach') {
@@ -524,10 +528,12 @@ class PathSeparatorPatcher {
524528
if (isFileSuite)
525529
suite.title = this._updatePath(suite.title);
526530
for (const entry of suite.entries) {
527-
if ('testId' in entry)
531+
if ('testId' in entry) {
528532
this._updateLocation(entry.location);
529-
else
533+
entry.annotations?.forEach(annotation => this._updateAnnotationLocation(annotation));
534+
} else {
530535
this._updateSuite(entry);
536+
}
531537
}
532538
}
533539

@@ -538,6 +544,10 @@ class PathSeparatorPatcher {
538544
}
539545
}
540546

547+
private _updateAnnotationLocation(annotation: TestAnnotation) {
548+
this._updateLocation(annotation.location);
549+
}
550+
541551
private _updateLocation(location?: JsonLocation) {
542552
if (location)
543553
location.file = this._updatePath(location.file);

packages/playwright/src/reporters/teleEmitter.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { serializeRegexPatterns } from '../isomorphic/teleReceiver';
2222

2323
import type { ReporterV2 } from './reporterV2';
2424
import type * as reporterTypes from '../../types/testReporter';
25+
import type { TestAnnotation } from '../../types/test';
2526
import type * as teleReceiver from '../isomorphic/teleReceiver';
2627

2728
export type TeleReporterEmitterOptions = {
@@ -224,7 +225,7 @@ export class TeleReporterEmitter implements ReporterV2 {
224225
retries: test.retries,
225226
tags: test.tags,
226227
repeatEachIndex: test.repeatEachIndex,
227-
annotations: test.annotations,
228+
annotations: this._relativeAnnotationLocations(test.annotations),
228229
};
229230
}
230231

@@ -244,7 +245,7 @@ export class TeleReporterEmitter implements ReporterV2 {
244245
duration: result.duration,
245246
status: result.status,
246247
errors: result.errors,
247-
annotations: result.annotations?.length ? result.annotations : undefined,
248+
annotations: result.annotations?.length ? this._relativeAnnotationLocations(result.annotations) : undefined,
248249
};
249250
}
250251

@@ -294,10 +295,17 @@ export class TeleReporterEmitter implements ReporterV2 {
294295
duration: step.duration,
295296
error: step.error,
296297
attachments: step.attachments.length ? step.attachments.map(a => result.attachments.indexOf(a)) : undefined,
297-
annotations: step.annotations.length ? step.annotations : undefined,
298+
annotations: step.annotations.length ? this._relativeAnnotationLocations(step.annotations) : undefined,
298299
};
299300
}
300301

302+
private _relativeAnnotationLocations(annotations: TestAnnotation[]): TestAnnotation[] {
303+
return annotations.map(annotation => ({
304+
...annotation,
305+
location: annotation.location ? this._relativeLocation(annotation.location) : undefined,
306+
}));
307+
}
308+
301309
private _relativeLocation(location: reporterTypes.Location): reporterTypes.Location;
302310
private _relativeLocation(location?: reporterTypes.Location): reporterTypes.Location | undefined;
303311
private _relativeLocation(location: reporterTypes.Location | undefined): reporterTypes.Location | undefined {

packages/playwright/src/worker/testInfo.ts

Lines changed: 27 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { TimeoutManager, TimeoutManagerError, kMaxDeadline } from './timeoutMana
2323
import { addSuffixToFilePath, filteredStackTrace, getContainedPath, normalizeAndSaveAttachment, sanitizeFilePathBeforeExtension, trimLongString, windowsFilesystemFriendlyLength } from '../util';
2424
import { TestTracing } from './testTracing';
2525
import { testInfoError } from './util';
26+
import { wrapFunctionWithLocation } from '../transform/transform';
2627

2728
import type { RunnableDescription } from './timeoutManager';
2829
import type { FullProject, TestInfo, TestStatus, TestStepInfo, TestAnnotation } from '../../types/test';
@@ -80,6 +81,12 @@ export class TestInfoImpl implements TestInfo {
8081
_hasUnhandledError = false;
8182
_allowSkips = false;
8283

84+
// ------------ Main methods ------------
85+
skip: (arg?: any, description?: string) => void;
86+
fixme: (arg?: any, description?: string) => void;
87+
fail: (arg?: any, description?: string) => void;
88+
slow: (arg?: any, description?: string) => void;
89+
8390
// ------------ TestInfo fields ------------
8491
readonly testId: string;
8592
readonly repeatEachIndex: number;
@@ -205,9 +212,14 @@ export class TestInfoImpl implements TestInfo {
205212
};
206213

207214
this._tracing = new TestTracing(this, workerParams.artifactsDir);
215+
216+
this.skip = wrapFunctionWithLocation((location, ...args) => this._modifier('skip', location, args));
217+
this.fixme = wrapFunctionWithLocation((location, ...args) => this._modifier('fixme', location, args));
218+
this.fail = wrapFunctionWithLocation((location, ...args) => this._modifier('fail', location, args));
219+
this.slow = wrapFunctionWithLocation((location, ...args) => this._modifier('slow', location, args));
208220
}
209221

210-
private _modifier(type: 'skip' | 'fail' | 'fixme' | 'slow', modifierArgs: [arg?: any, description?: string]) {
222+
_modifier(type: 'skip' | 'fail' | 'fixme' | 'slow', location: Location, modifierArgs: [arg?: any, description?: string]) {
211223
if (typeof modifierArgs[1] === 'function') {
212224
throw new Error([
213225
'It looks like you are calling test.skip() inside the test and pass a callback.',
@@ -222,7 +234,7 @@ export class TestInfoImpl implements TestInfo {
222234
return;
223235

224236
const description = modifierArgs[1];
225-
this.annotations.push({ type, description });
237+
this.annotations.push({ type, description, location });
226238
if (type === 'slow') {
227239
this._timeoutManager.slow();
228240
} else if (type === 'skip' || type === 'fixme') {
@@ -567,22 +579,6 @@ export class TestInfoImpl implements TestInfo {
567579
return this._resolveSnapshotPaths(kind, name.length <= 1 ? name[0] : name, 'dontUpdateSnapshotIndex').absoluteSnapshotPath;
568580
}
569581

570-
skip(...args: [arg?: any, description?: string]) {
571-
this._modifier('skip', args);
572-
}
573-
574-
fixme(...args: [arg?: any, description?: string]) {
575-
this._modifier('fixme', args);
576-
}
577-
578-
fail(...args: [arg?: any, description?: string]) {
579-
this._modifier('fail', args);
580-
}
581-
582-
slow(...args: [arg?: any, description?: string]) {
583-
this._modifier('slow', args);
584-
}
585-
586582
setTimeout(timeout: number) {
587583
this._timeoutManager.setTimeout(timeout);
588584
}
@@ -594,14 +590,25 @@ export class TestStepInfoImpl implements TestStepInfo {
594590
private _testInfo: TestInfoImpl;
595591
private _stepId: string;
596592

593+
skip: (arg?: any, description?: string) => void;
594+
597595
constructor(testInfo: TestInfoImpl, stepId: string) {
598596
this._testInfo = testInfo;
599597
this._stepId = stepId;
598+
this.skip = wrapFunctionWithLocation((location: Location, ...args: unknown[]) => {
599+
// skip();
600+
// skip(condition: boolean, description: string);
601+
if (args.length > 0 && !args[0])
602+
return;
603+
const description = args[1] as (string|undefined);
604+
this.annotations.push({ type: 'skip', description, location });
605+
throw new StepSkipError(description);
606+
});
600607
}
601608

602-
async _runStepBody<T>(skip: boolean, body: (step: TestStepInfo) => T | Promise<T>) {
609+
async _runStepBody<T>(skip: boolean, body: (step: TestStepInfo) => T | Promise<T>, location?: Location) {
603610
if (skip) {
604-
this.annotations.push({ type: 'skip' });
611+
this.annotations.push({ type: 'skip', location });
605612
return undefined as T;
606613
}
607614
try {
@@ -620,16 +627,6 @@ export class TestStepInfoImpl implements TestStepInfo {
620627
async attach(name: string, options?: { body?: string | Buffer; contentType?: string; path?: string; }): Promise<void> {
621628
this._attachToStep(await normalizeAndSaveAttachment(this._testInfo.outputPath(), name, options));
622629
}
623-
624-
skip(...args: unknown[]) {
625-
// skip();
626-
// skip(condition: boolean, description: string);
627-
if (args.length > 0 && !args[0])
628-
return;
629-
const description = args[1] as (string|undefined);
630-
this.annotations.push({ type: 'skip', description });
631-
throw new StepSkipError(description);
632-
}
633630
}
634631

635632
export class TestSkipError extends Error {

0 commit comments

Comments
 (0)