feat(api): expose step location (#9602)

This commit is contained in:
Pavel Feldman 2021-10-18 20:06:18 -08:00 committed by GitHub
parent 4977edcaf3
commit e37660b068
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 158 additions and 7 deletions

View file

@ -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]>

View file

@ -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);

View file

@ -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;
}

View file

@ -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"
}
}

View file

@ -150,6 +150,7 @@ export class Dispatcher {
startTime: new Date(params.wallTime),
duration: 0,
steps: [],
location: params.location,
data: {},
};
steps.set(params.stepId, step);

View file

@ -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,

View file

@ -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 };
},

View file

@ -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 = {

View file

@ -189,6 +189,7 @@ export class TestTypeImpl {
const step = testInfo._addStep({
category: 'test.step',
title,
location,
canHaveChildren: true,
forceNoParent: false
});

View file

@ -31,6 +31,7 @@ export interface TestStepInternal {
category: string;
canHaveChildren: boolean;
forceNoParent: boolean;
location?: Location;
}
export interface TestInfoImpl extends TestInfo {

View file

@ -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)

View file

@ -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.
*/

View file

@ -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,

View file

@ -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,
};
}

View file

@ -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',
},
],
},
]);
});

View file

@ -61,6 +61,7 @@ export interface TestResult {
export interface TestStep {
title: string;
titlePath(): string[];
location?: Location;
parent?: TestStep;
category: string,
startTime: Date;