feat(api): expose step location (#9602)
This commit is contained in:
parent
4977edcaf3
commit
e37660b068
|
|
@ -17,6 +17,11 @@ Step category to differentiate steps with different origin and verbosity. Built-
|
|||
|
||||
Running time in milliseconds.
|
||||
|
||||
## property: TestStep.location
|
||||
- type: <[void]|[Location]>
|
||||
|
||||
Location in the source where the step is defined.
|
||||
|
||||
## property: TestStep.error
|
||||
- type: <[void]|[TestError]>
|
||||
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
|
|||
if (validator) {
|
||||
return (params: any) => {
|
||||
if (callCookie && csi) {
|
||||
callCookie.userObject = csi.onApiCallBegin(renderCallWithParams(stackTrace!.apiName!, params)).userObject;
|
||||
callCookie.userObject = csi.onApiCallBegin(renderCallWithParams(stackTrace!.apiName!, params), stackTrace).userObject;
|
||||
csi = undefined;
|
||||
}
|
||||
return this._connection.sendMessageToServer(this, prop, validator(params, ''), stackTrace);
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
import * as channels from '../protocol/channels';
|
||||
import type { Size } from '../common/types';
|
||||
import { ParsedStackTrace } from '../utils/stackTrace';
|
||||
export { Size, Point, Rect, Quad, URLMatch, TimeoutOptions, HeadersArray } from '../common/types';
|
||||
|
||||
type LoggerSeverity = 'verbose' | 'info' | 'warning' | 'error';
|
||||
|
|
@ -26,7 +27,7 @@ export interface Logger {
|
|||
}
|
||||
|
||||
export interface ClientSideInstrumentation {
|
||||
onApiCallBegin(apiCall: string): { userObject: any };
|
||||
onApiCallBegin(apiCall: string, stackTrace: ParsedStackTrace | null): { userObject: any };
|
||||
onApiCallEnd(userData: { userObject: any }, error?: Error): any;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@
|
|||
"pirates": "^4.0.1",
|
||||
"pixelmatch": "^5.2.1",
|
||||
"playwright-core": "=1.16.0-next",
|
||||
"source-map-support": "^0.4.18"
|
||||
"source-map-support": "^0.4.18",
|
||||
"stack-utils": "^2.0.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -150,6 +150,7 @@ export class Dispatcher {
|
|||
startTime: new Date(params.wallTime),
|
||||
duration: 0,
|
||||
steps: [],
|
||||
location: params.location,
|
||||
data: {},
|
||||
};
|
||||
steps.set(params.stepId, step);
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
|
||||
import expectLibrary from 'expect';
|
||||
import path from 'path';
|
||||
import {
|
||||
INVERTED_COLOR,
|
||||
RECEIVED_COLOR,
|
||||
|
|
@ -46,6 +47,9 @@ import type { Expect, TestError } from './types';
|
|||
import matchers from 'expect/build/matchers';
|
||||
import { currentTestInfo } from './globals';
|
||||
import { serializeError } from './util';
|
||||
import StackUtils from 'stack-utils';
|
||||
|
||||
const stackUtils = new StackUtils();
|
||||
|
||||
// #region
|
||||
// Mirrored from https://github.com/facebook/jest/blob/f13abff8df9a0e1148baf3584bcde6d1b479edc7/packages/expect/src/print.ts
|
||||
|
|
@ -124,7 +128,9 @@ function wrap(matcherName: string, matcher: any) {
|
|||
// at Object.throwingMatcher [as toHaveText] (...)
|
||||
// at <test function> (...)
|
||||
const stackLines = new Error().stack!.split('\n').slice(INTERNAL_STACK_LENGTH + 1);
|
||||
const frame = stackLines[0] ? stackUtils.parseLine(stackLines[0]) : undefined;
|
||||
const step = testInfo._addStep({
|
||||
location: frame && frame.file ? { file: path.resolve(process.cwd(), frame.file), line: frame.line || 0, column: frame.column || 0 } : undefined,
|
||||
category: 'expect',
|
||||
title: `expect${this.isNot ? '.not' : ''}.${matcherName}`,
|
||||
canHaveChildren: true,
|
||||
|
|
|
|||
|
|
@ -298,14 +298,16 @@ export const test = _baseTest.extend<TestFixtures, WorkerAndFileFixtures>({
|
|||
await context.tracing.stop();
|
||||
}
|
||||
(context as any)._csi = {
|
||||
onApiCallBegin: (apiCall: string) => {
|
||||
onApiCallBegin: (apiCall: string, stackTrace: ParsedStackTrace | null) => {
|
||||
if (apiCall.startsWith('expect.'))
|
||||
return { userObject: null };
|
||||
const step = (testInfo as any)._addStep({
|
||||
const testInfoImpl = testInfo as any;
|
||||
const step = testInfoImpl._addStep({
|
||||
location: stackTrace?.frames[0],
|
||||
category: 'pw:api',
|
||||
title: apiCall,
|
||||
canHaveChildren: false,
|
||||
forceNoParent: false,
|
||||
forceNoParent: false
|
||||
});
|
||||
return { userObject: step };
|
||||
},
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ export type StepBeginPayload = {
|
|||
canHaveChildren: boolean;
|
||||
forceNoParent: boolean;
|
||||
wallTime: number; // milliseconds since unix epoch
|
||||
location?: { file: string, line: number, column: number };
|
||||
};
|
||||
|
||||
export type StepEndPayload = {
|
||||
|
|
|
|||
|
|
@ -189,6 +189,7 @@ export class TestTypeImpl {
|
|||
const step = testInfo._addStep({
|
||||
category: 'test.step',
|
||||
title,
|
||||
location,
|
||||
canHaveChildren: true,
|
||||
forceNoParent: false
|
||||
});
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export interface TestStepInternal {
|
|||
category: string;
|
||||
canHaveChildren: boolean;
|
||||
forceNoParent: boolean;
|
||||
location?: Location;
|
||||
}
|
||||
|
||||
export interface TestInfoImpl extends TestInfo {
|
||||
|
|
|
|||
|
|
@ -306,10 +306,13 @@ export class WorkerRunner extends EventEmitter {
|
|||
this.emit('stepEnd', payload);
|
||||
}
|
||||
};
|
||||
// Sanitize location that comes from userland.
|
||||
const location = data.location ? { file: data.location.file, line: data.location.line, column: data.location.column } : undefined;
|
||||
const payload: StepBeginPayload = {
|
||||
testId,
|
||||
stepId,
|
||||
...data,
|
||||
location,
|
||||
wallTime: Date.now(),
|
||||
};
|
||||
if (reportEvents)
|
||||
|
|
|
|||
|
|
@ -230,6 +230,10 @@ export interface TestStep {
|
|||
* Returns a list of step titles from the root step down to this step.
|
||||
*/
|
||||
titlePath(): string[];
|
||||
/**
|
||||
* Location in the source where the step is defined.
|
||||
*/
|
||||
location?: Location;
|
||||
/**
|
||||
* Parent step, if any.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import * as os from 'os';
|
|||
import { RemoteServer, RemoteServerOptions } from './remoteServer';
|
||||
import { baseTest, CommonWorkerFixtures } from './baseTest';
|
||||
import { CommonFixtures } from './commonFixtures';
|
||||
import { ParsedStackTrace } from 'playwright-core/src/utils/stackTrace';
|
||||
|
||||
type PlaywrightWorkerOptions = {
|
||||
executablePath: LaunchOptions['executablePath'];
|
||||
|
|
@ -147,11 +148,12 @@ export const playwrightFixtures: Fixtures<PlaywrightTestOptions & PlaywrightTest
|
|||
if (trace)
|
||||
await context.tracing.start({ screenshots: true, snapshots: true });
|
||||
(context as any)._csi = {
|
||||
onApiCallBegin: (apiCall: string) => {
|
||||
onApiCallBegin: (apiCall: string, stackTrace: ParsedStackTrace | null) => {
|
||||
if (apiCall.startsWith('expect.'))
|
||||
return { userObject: null };
|
||||
const testInfoImpl = testInfo as any;
|
||||
const step = testInfoImpl._addStep({
|
||||
location: stackTrace?.frames[0],
|
||||
category: 'pw:api',
|
||||
title: apiCall,
|
||||
canHaveChildren: false,
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ class Reporter {
|
|||
duration: undefined,
|
||||
parent: undefined,
|
||||
data: undefined,
|
||||
location: undefined,
|
||||
steps: step.steps.length ? step.steps.map(s => this.distillStep(s)) : undefined,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,11 @@ class Reporter {
|
|||
duration: undefined,
|
||||
parent: undefined,
|
||||
data: undefined,
|
||||
location: step.location ? {
|
||||
file: step.location.file.substring(step.location.file.lastIndexOf(require('path').sep) + 1).replace('.js', '.ts'),
|
||||
line: step.location.line ? typeof step.location.line : 0,
|
||||
column: step.location.column ? typeof step.location.column : 0
|
||||
} : undefined,
|
||||
steps: step.steps.length ? step.steps.map(s => this.distillStep(s)) : undefined,
|
||||
};
|
||||
}
|
||||
|
|
@ -83,6 +88,11 @@ test('should report api step hierarchy', async ({ runInlineTest }) => {
|
|||
steps: [
|
||||
{
|
||||
category: 'pw:api',
|
||||
location: {
|
||||
column: 'number',
|
||||
file: 'index.ts',
|
||||
line: 'number',
|
||||
},
|
||||
title: 'browserContext.newPage',
|
||||
},
|
||||
],
|
||||
|
|
@ -90,13 +100,28 @@ test('should report api step hierarchy', async ({ runInlineTest }) => {
|
|||
{
|
||||
category: 'test.step',
|
||||
title: 'outer step 1',
|
||||
location: {
|
||||
column: 'number',
|
||||
file: 'a.test.ts',
|
||||
line: 'number',
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
category: 'test.step',
|
||||
location: {
|
||||
column: 'number',
|
||||
file: 'a.test.ts',
|
||||
line: 'number',
|
||||
},
|
||||
title: 'inner step 1.1',
|
||||
},
|
||||
{
|
||||
category: 'test.step',
|
||||
location: {
|
||||
column: 'number',
|
||||
file: 'a.test.ts',
|
||||
line: 'number',
|
||||
},
|
||||
title: 'inner step 1.2',
|
||||
},
|
||||
],
|
||||
|
|
@ -104,13 +129,28 @@ test('should report api step hierarchy', async ({ runInlineTest }) => {
|
|||
{
|
||||
category: 'test.step',
|
||||
title: 'outer step 2',
|
||||
location: {
|
||||
column: 'number',
|
||||
file: 'a.test.ts',
|
||||
line: 'number',
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
category: 'test.step',
|
||||
location: {
|
||||
column: 'number',
|
||||
file: 'a.test.ts',
|
||||
line: 'number',
|
||||
},
|
||||
title: 'inner step 2.1',
|
||||
},
|
||||
{
|
||||
category: 'test.step',
|
||||
location: {
|
||||
column: 'number',
|
||||
file: 'a.test.ts',
|
||||
line: 'number',
|
||||
},
|
||||
title: 'inner step 2.2',
|
||||
},
|
||||
],
|
||||
|
|
@ -121,6 +161,11 @@ test('should report api step hierarchy', async ({ runInlineTest }) => {
|
|||
steps: [
|
||||
{
|
||||
category: 'pw:api',
|
||||
location: {
|
||||
column: 'number',
|
||||
file: 'index.ts',
|
||||
line: 'number',
|
||||
},
|
||||
title: 'browserContext.close',
|
||||
},
|
||||
],
|
||||
|
|
@ -156,12 +201,22 @@ test('should not report nested after hooks', async ({ runInlineTest }) => {
|
|||
{
|
||||
category: 'pw:api',
|
||||
title: 'browserContext.newPage',
|
||||
location: {
|
||||
column: 'number',
|
||||
file: 'index.ts',
|
||||
line: 'number',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: 'test.step',
|
||||
title: 'my step',
|
||||
location: {
|
||||
column: 'number',
|
||||
file: 'a.test.ts',
|
||||
line: 'number',
|
||||
},
|
||||
},
|
||||
{
|
||||
category: 'hook',
|
||||
|
|
@ -170,6 +225,11 @@ test('should not report nested after hooks', async ({ runInlineTest }) => {
|
|||
{
|
||||
category: 'pw:api',
|
||||
title: 'browserContext.close',
|
||||
location: {
|
||||
column: 'number',
|
||||
file: 'index.ts',
|
||||
line: 'number',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -230,3 +290,64 @@ test('should report test.step from fixtures', async ({ runInlineTest }) => {
|
|||
`%% end After Hooks`,
|
||||
]);
|
||||
});
|
||||
|
||||
test('should report expect step locations', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'reporter.ts': stepHierarchyReporter,
|
||||
'playwright.config.ts': `
|
||||
module.exports = {
|
||||
reporter: './reporter',
|
||||
};
|
||||
`,
|
||||
'a.test.ts': `
|
||||
const { test } = pwt;
|
||||
test('pass', async ({ page }) => {
|
||||
expect(true).toBeTruthy();
|
||||
});
|
||||
`
|
||||
}, { reporter: '', workers: 1 });
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
const objects = result.output.split('\n').filter(line => line.startsWith('%% ')).map(line => line.substring(3).trim()).filter(Boolean).map(line => JSON.parse(line));
|
||||
expect(objects).toEqual([
|
||||
{
|
||||
category: 'hook',
|
||||
title: 'Before Hooks',
|
||||
steps: [
|
||||
{
|
||||
category: 'pw:api',
|
||||
location: {
|
||||
column: 'number',
|
||||
file: 'index.ts',
|
||||
line: 'number',
|
||||
},
|
||||
title: 'browserContext.newPage',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: 'expect',
|
||||
title: 'expect.toBeTruthy',
|
||||
location: {
|
||||
column: 'number',
|
||||
file: 'a.test.ts',
|
||||
line: 'number',
|
||||
},
|
||||
},
|
||||
{
|
||||
category: 'hook',
|
||||
title: 'After Hooks',
|
||||
steps: [
|
||||
{
|
||||
category: 'pw:api',
|
||||
location: {
|
||||
column: 'number',
|
||||
file: 'index.ts',
|
||||
line: 'number',
|
||||
},
|
||||
title: 'browserContext.close',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ export interface TestResult {
|
|||
export interface TestStep {
|
||||
title: string;
|
||||
titlePath(): string[];
|
||||
location?: Location;
|
||||
parent?: TestStep;
|
||||
category: string,
|
||||
startTime: Date;
|
||||
|
|
|
|||
Loading…
Reference in a new issue