Skip to content
Merged
1 change: 1 addition & 0 deletions docs/src/test-api/class-testinfo.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ test('basic test', async ({ page }, testInfo) => {
- type: <[Array]<[Object]>>
- `type` <[string]> Annotation type, for example `'skip'` or `'fail'`.
- `description` ?<[string]> Optional description.
- `location` ?<[Location]> Optional location in the source where the annotation is added.

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.

Expand Down
1 change: 1 addition & 0 deletions docs/src/test-reporter-api/class-testcase.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- type: <[Array]<[Object]>>
- `type` <[string]> Annotation type, for example `'skip'` or `'fail'`.
- `description` ?<[string]> Optional description.
- `location` ?<[Location]> Optional location in the source where the annotation is added.

The list of annotations applicable to the current test. Includes:
* annotations defined on the test or suite via [`method: Test.(call)`] and [`method: Test.describe`];
Expand Down
1 change: 1 addition & 0 deletions docs/src/test-reporter-api/class-testresult.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ The list of files or buffers attached during the test execution through [`proper
- type: <[Array]<[Object]>>
- `type` <[string]> Annotation type, for example `'skip'` or `'fail'`.
- `description` ?<[string]> Optional description.
- `location` ?<[Location]> Optional location in the source where the annotation is added.

The list of annotations appended during test execution. Includes:
* annotations implicitly added by methods [`method: Test.skip`], [`method: Test.fixme`] and [`method: Test.fail`] during test execution;
Expand Down
1 change: 1 addition & 0 deletions docs/src/test-reporter-api/class-teststep.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ List of steps inside this step.
- type: <[Array]<[Object]>>
- `type` <[string]> Annotation type, for example `'skip'`.
- `description` ?<[string]> Optional description.
- `location` ?<[Location]> Optional location in the source where the annotation is added.

The list of annotations applicable to the current test step.

Expand Down
3 changes: 2 additions & 1 deletion packages/html-reporter/src/testCaseView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
limitations under the License.
*/

import type { TestCase, TestAnnotation, TestCaseSummary } from './types';
import type { TestAnnotation } from '@playwright/test';
import type { TestCase, TestCaseSummary } from './types';
import * as React from 'react';
import { TabbedPane } from './tabbedPane';
import { AutoChip } from './chip';
Expand Down
4 changes: 1 addition & 3 deletions packages/html-reporter/src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import type { Metadata } from '@playwright/test';
import type { TestAnnotation, Metadata } from '@playwright/test';

export type Stats = {
total: number;
Expand Down Expand Up @@ -59,8 +59,6 @@ export type TestFileSummary = {
stats: Stats;
};

export type TestAnnotation = { type: string, description?: string };

export type TestCaseSummary = {
testId: string,
title: string;
Expand Down
1 change: 0 additions & 1 deletion packages/playwright/src/common/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ export type FixturesWithLocation = {
fixtures: Fixtures;
location: Location;
};
export type Annotation = { type: string, description?: string };

export const defaultTimeout = 30000;

Expand Down
7 changes: 4 additions & 3 deletions packages/playwright/src/common/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@
import { rootTestType } from './testType';
import { computeTestCaseOutcome } from '../isomorphic/teleReceiver';

import type { Annotation, FixturesWithLocation, FullProjectInternal } from './config';
import type { FixturesWithLocation, FullProjectInternal } from './config';
import type { FixturePool } from './fixtures';
import type { TestTypeImpl } from './testType';
import type { TestAnnotation } from '../../types/test';
import type * as reporterTypes from '../../types/testReporter';
import type { FullProject, Location } from '../../types/testReporter';

Expand Down Expand Up @@ -50,7 +51,7 @@ export class Suite extends Base {
_timeout: number | undefined;
_retries: number | undefined;
// Annotations known statically before running the test, e.g. `test.describe.skip()` or `test.describe({ annotation }, body)`.
_staticAnnotations: Annotation[] = [];
_staticAnnotations: TestAnnotation[] = [];
// Explicitly declared tags that are not a part of the title.
_tags: string[] = [];
_modifiers: Modifier[] = [];
Expand Down Expand Up @@ -252,7 +253,7 @@ export class TestCase extends Base implements reporterTypes.TestCase {

expectedStatus: reporterTypes.TestStatus = 'passed';
timeout = 0;
annotations: Annotation[] = [];
annotations: TestAnnotation[] = [];
retries = 0;
repeatEachIndex = 0;

Expand Down
19 changes: 10 additions & 9 deletions packages/playwright/src/common/testType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export class TestTypeImpl {
details = fnOrDetails;
}

const validatedDetails = validateTestDetails(details);
const validatedDetails = validateTestDetails(details, location);
const test = new TestCase(title, body, this, location);
test._requireFile = suite._requireFile;
test.annotations.push(...validatedDetails.annotations);
Expand All @@ -114,9 +114,9 @@ export class TestTypeImpl {
if (type === 'only' || type === 'fail.only')
test._only = true;
if (type === 'skip' || type === 'fixme' || type === 'fail')
test.annotations.push({ type });
test.annotations.push({ type, location });
else if (type === 'fail.only')
test.annotations.push({ type: 'fail' });
test.annotations.push({ type: 'fail', location });
}

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

const validatedDetails = validateTestDetails(details);
const validatedDetails = validateTestDetails(details, location);
const child = new Suite(title, 'describe');
child._requireFile = suite._requireFile;
child.location = location;
Expand All @@ -158,7 +158,7 @@ export class TestTypeImpl {
if (type === 'parallel' || type === 'parallel.only')
child._parallelMode = 'parallel';
if (type === 'skip' || type === 'fixme')
child._staticAnnotations.push({ type });
child._staticAnnotations.push({ type, location });

for (let parent: Suite | undefined = suite; parent; parent = parent.parent) {
if (parent._parallelMode === 'serial' && child._parallelMode === 'parallel')
Expand Down Expand Up @@ -229,7 +229,7 @@ export class TestTypeImpl {
if (modifierArgs.length >= 1 && !modifierArgs[0])
return;
const description = modifierArgs[1];
suite._staticAnnotations.push({ type, description });
suite._staticAnnotations.push({ type, description, location });
}
return;
}
Expand Down Expand Up @@ -276,7 +276,7 @@ export class TestTypeImpl {
let result: Awaited<ReturnType<typeof raceAgainstDeadline<T>>> | undefined = undefined;
result = await raceAgainstDeadline(async () => {
try {
return await step.info._runStepBody(expectation === 'skip', body);
return await step.info._runStepBody(expectation === 'skip', body, step.location);
} catch (e) {
// If the step timed out, the test fixtures will tear down, which in turn
// will abort unfinished actions in the step body. Record such errors here.
Expand Down Expand Up @@ -315,8 +315,9 @@ function throwIfRunningInsideJest() {
}
}

function validateTestDetails(details: TestDetails) {
const annotations = Array.isArray(details.annotation) ? details.annotation : (details.annotation ? [details.annotation] : []);
function validateTestDetails(details: TestDetails, location: Location) {
const originalAnnotations = Array.isArray(details.annotation) ? details.annotation : (details.annotation ? [details.annotation] : []);
const annotations = originalAnnotations.map(annotation => ({ ...annotation, location }));
const tags = Array.isArray(details.tag) ? details.tag : (details.tag ? [details.tag] : []);
for (const tag of tags) {
if (tag[0] !== '@')
Expand Down
13 changes: 6 additions & 7 deletions packages/playwright/src/isomorphic/teleReceiver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@
* limitations under the License.
*/

import type { Metadata } from '../../types/test';
import type { Metadata, TestAnnotation } from '../../types/test';
Comment thread
agg23 marked this conversation as resolved.
import type * as reporterTypes from '../../types/testReporter';
import type { Annotation } from '../common/config';
import type { ReporterV2 } from '../reporters/reporterV2';

export type StringIntern = (s: string) => string;
Expand Down Expand Up @@ -68,14 +67,14 @@ export type JsonTestCase = {
retries: number;
tags?: string[];
repeatEachIndex: number;
annotations?: Annotation[];
annotations?: TestAnnotation[];
};

export type JsonTestEnd = {
testId: string;
expectedStatus: reporterTypes.TestStatus;
timeout: number;
annotations: Annotation[];
annotations: TestAnnotation[];
};

export type JsonTestResultStart = {
Expand All @@ -94,7 +93,7 @@ export type JsonTestResultEnd = {
status: reporterTypes.TestStatus;
errors: reporterTypes.TestError[];
attachments: JsonAttachment[];
annotations?: Annotation[];
annotations?: TestAnnotation[];
};

export type JsonTestStepStart = {
Expand All @@ -111,7 +110,7 @@ export type JsonTestStepEnd = {
duration: number;
error?: reporterTypes.TestError;
attachments?: number[]; // index of JsonTestResultEnd.attachments
annotations?: Annotation[];
annotations?: TestAnnotation[];
};

export type JsonFullResult = {
Expand Down Expand Up @@ -477,7 +476,7 @@ export class TeleTestCase implements reporterTypes.TestCase {

expectedStatus: reporterTypes.TestStatus = 'passed';
timeout = 0;
annotations: Annotation[] = [];
annotations: TestAnnotation[] = [];
retries = 0;
tags: string[] = [];
repeatEachIndex = 0;
Expand Down
4 changes: 2 additions & 2 deletions packages/playwright/src/reporters/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ import { codeFrameColumns } from '../transform/babelBundle';
import { resolveReporterOutputPath, stripAnsiEscapes } from '../util';

import type { ReporterV2 } from './reporterV2';
import type { Metadata } from '../../types/test';
import type { Metadata, TestAnnotation } from '../../types/test';
import type * as api from '../../types/testReporter';
import type { HTMLReport, Stats, TestAttachment, TestCase, TestCaseSummary, TestFile, TestFileSummary, TestResult, TestStep, TestAnnotation } from '@html-reporter/types';
import type { HTMLReport, Stats, TestAttachment, TestCase, TestCaseSummary, TestFile, TestFileSummary, TestResult, TestStep } from '@html-reporter/types';
import type { ZipFile } from 'playwright-core/lib/zipBundle';
import type { TransformCallback } from 'stream';

Expand Down
12 changes: 12 additions & 0 deletions packages/playwright/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,18 @@ export function filteredStackTrace(rawStack: RawStack): StackFrame[] {
return frames;
}

export function filteredLocation(rawStack: RawStack): Location | undefined {
const frame = filteredStackTrace(rawStack)[0] as StackFrame | undefined;
if (!frame)
return undefined;
return {
file: frame.file,
line: frame.line,
column: frame.column
};
}


export function serializeError(error: Error | any): TestInfoErrorImpl {
if (error instanceof Error)
return filterStackTrace(error);
Expand Down
22 changes: 13 additions & 9 deletions packages/playwright/src/worker/testInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ import path from 'path';
import { captureRawStack, monotonicTime, sanitizeForFilePath, stringifyStackFrames, currentZone } from 'playwright-core/lib/utils';

import { TimeoutManager, TimeoutManagerError, kMaxDeadline } from './timeoutManager';
import { filteredStackTrace, getContainedPath, normalizeAndSaveAttachment, trimLongString, windowsFilesystemFriendlyLength } from '../util';
import { filteredLocation, filteredStackTrace, getContainedPath, normalizeAndSaveAttachment, trimLongString, windowsFilesystemFriendlyLength } from '../util';
import { TestTracing } from './testTracing';
import { testInfoError } from './util';
import { FloatingPromiseScope } from './floatingPromiseScope';

import type { RunnableDescription } from './timeoutManager';
import type { FullProject, TestInfo, TestStatus, TestStepInfo } from '../../types/test';
import type { FullProject, TestAnnotation, TestInfo, TestStatus, TestStepInfo } from '../../types/test';
import type { FullConfig, Location } from '../../types/testReporter';
import type { Annotation, FullConfigInternal, FullProjectInternal } from '../common/config';
import type { FullConfigInternal, FullProjectInternal } from '../common/config';
import type { AttachmentPayload, StepBeginPayload, StepEndPayload, TestInfoErrorImpl, WorkerInitParams } from '../common/ipc';
import type { TestCase } from '../common/test';
import type { StackFrame } from '@protocol/channels';
Expand Down Expand Up @@ -92,7 +92,7 @@ export class TestInfoImpl implements TestInfo {
readonly fn: Function;
expectedStatus: TestStatus;
duration: number = 0;
readonly annotations: Annotation[] = [];
readonly annotations: TestAnnotation[] = [];
readonly attachments: TestInfo['attachments'] = [];
status: TestStatus = 'passed';
snapshotSuffix: string = '';
Expand Down Expand Up @@ -217,7 +217,10 @@ export class TestInfoImpl implements TestInfo {
return;

const description = modifierArgs[1];
this.annotations.push({ type, description });
const callLocation = filteredLocation(captureRawStack());
Comment thread
agg23 marked this conversation as resolved.
Outdated
const baseLocation = this.column !== undefined && this.line !== undefined && this.file !== undefined ? { column: this.column, line: this.line, file: this.file } : undefined;
const location = callLocation ? callLocation : baseLocation;
this.annotations.push({ type, description, location });
if (type === 'slow') {
this._timeoutManager.slow();
} else if (type === 'skip' || type === 'fixme') {
Expand Down Expand Up @@ -503,7 +506,7 @@ export class TestInfoImpl implements TestInfo {
}

export class TestStepInfoImpl implements TestStepInfo {
annotations: Annotation[] = [];
annotations: TestAnnotation[] = [];

private _testInfo: TestInfoImpl;
private _stepId: string;
Expand All @@ -513,9 +516,9 @@ export class TestStepInfoImpl implements TestStepInfo {
this._stepId = stepId;
}

async _runStepBody<T>(skip: boolean, body: (step: TestStepInfo) => T | Promise<T>) {
async _runStepBody<T>(skip: boolean, body: (step: TestStepInfo) => T | Promise<T>, location?: Location) {
if (skip) {
this.annotations.push({ type: 'skip' });
this.annotations.push({ type: 'skip', location });
return undefined as T;
}
try {
Expand All @@ -541,7 +544,8 @@ export class TestStepInfoImpl implements TestStepInfo {
if (args.length > 0 && !args[0])
return;
const description = args[1] as (string|undefined);
this.annotations.push({ type: 'skip', description });
const location = filteredLocation(captureRawStack());
this.annotations.push({ type: 'skip', description, location });
throw new StepSkipError(description);
}
}
Expand Down
13 changes: 7 additions & 6 deletions packages/playwright/src/worker/workerMain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@ import { loadTestFile } from '../common/testLoader';

import type { TimeSlot } from './timeoutManager';
import type { Location } from '../../types/testReporter';
import type { Annotation, FullConfigInternal, FullProjectInternal } from '../common/config';
import type { FullConfigInternal, FullProjectInternal } from '../common/config';
import type { DonePayload, RunPayload, TeardownErrorsPayload, TestBeginPayload, TestEndPayload, TestInfoErrorImpl, WorkerInitParams } from '../common/ipc';
import type { Suite, TestCase } from '../common/test';
import type { TestAnnotation } from '../../types/test';

export class WorkerMain extends ProcessRunner {
private _params: WorkerInitParams;
Expand All @@ -60,7 +61,7 @@ export class WorkerMain extends ProcessRunner {
// Suites that had their beforeAll hooks, but not afterAll hooks executed.
// These suites still need afterAll hooks to be executed for the proper cleanup.
// Contains dynamic annotations originated by modifiers with a callback, e.g. `test.skip(() => true)`.
private _activeSuites = new Map<Suite, Annotation[]>();
private _activeSuites = new Map<Suite, TestAnnotation[]>();

constructor(params: WorkerInitParams) {
super();
Expand Down Expand Up @@ -264,7 +265,7 @@ export class WorkerMain extends ProcessRunner {
stepEndPayload => this.dispatchEvent('stepEnd', stepEndPayload),
attachment => this.dispatchEvent('attach', attachment));

const processAnnotation = (annotation: Annotation) => {
const processAnnotation = (annotation: TestAnnotation) => {
testInfo.annotations.push(annotation);
switch (annotation.type) {
case 'fixme':
Expand Down Expand Up @@ -529,12 +530,12 @@ export class WorkerMain extends ProcessRunner {
private async _runBeforeAllHooksForSuite(suite: Suite, testInfo: TestInfoImpl) {
if (this._activeSuites.has(suite))
return;
const extraAnnotations: Annotation[] = [];
const extraAnnotations: TestAnnotation[] = [];
this._activeSuites.set(suite, extraAnnotations);
await this._runAllHooksForSuite(suite, testInfo, 'beforeAll', extraAnnotations);
}

private async _runAllHooksForSuite(suite: Suite, testInfo: TestInfoImpl, type: 'beforeAll' | 'afterAll', extraAnnotations?: Annotation[]) {
private async _runAllHooksForSuite(suite: Suite, testInfo: TestInfoImpl, type: 'beforeAll' | 'afterAll', extraAnnotations?: TestAnnotation[]) {
// Always run all the hooks, and capture the first error.
let firstError: Error | undefined;
for (const hook of this._collectHooksAndModifiers(suite, type, testInfo)) {
Expand Down Expand Up @@ -614,7 +615,7 @@ function buildTestBeginPayload(testInfo: TestInfoImpl): TestBeginPayload {
};
}

function buildTestEndPayload(testInfo: TestInfoImpl, staticAnnotations: Set<Annotation>): TestEndPayload {
function buildTestEndPayload(testInfo: TestInfoImpl, staticAnnotations: Set<TestAnnotation>): TestEndPayload {
return {
testId: testInfo.testId,
duration: testInfo.duration,
Expand Down
Loading