feat(api): expose step location UI (#9605)

This commit is contained in:
Pavel Feldman 2021-10-18 21:14:01 -08:00 committed by GitHub
parent c06a6e1f63
commit bccd4c8906
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 25 additions and 20 deletions

View File

@ -90,7 +90,6 @@ svg {
}
.chip-body > .tree-item {
max-width: 600px;
line-height: 38px;
}

View File

@ -217,10 +217,11 @@ const StepTreeItem: React.FC<{
<span style={{ float: 'right' }}>{msToString(step.duration)}</span>
{statusIcon(step.error ? 'failed' : 'passed')}
<span>{step.title}</span>
</span>} loadChildren={step.steps.length + (step.error ? 1 : 0) ? () => {
{step.location && <span className='test-summary-path'> {step.location.file}:{step.location.line}</span>}
</span>} loadChildren={step.steps.length + (step.snippet ? 1 : 0) ? () => {
const children = step.steps.map((s, i) => <StepTreeItem key={i} step={s} depth={depth + 1}></StepTreeItem>);
if (step.error)
children.unshift(<ErrorMessage key={-1} error={step.error}></ErrorMessage>);
if (step.snippet)
children.unshift(<ErrorMessage key='line' error={step.snippet}></ErrorMessage>);
return children;
} : undefined} depth={depth}></TreeItem>;
};

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
import { codeFrameColumns } from '@babel/code-frame';
import { BabelCodeFrameOptions, codeFrameColumns } from '@babel/code-frame';
import colors from 'colors/safe';
import fs from 'fs';
import milliseconds from 'ms';
@ -337,7 +337,7 @@ export function formatError(error: TestError, highlightCode: boolean, file?: str
positionInFile = position;
tokens.push(message);
const codeFrame = generateCodeFrame(highlightCode, file, position);
const codeFrame = generateCodeFrame({ highlightCode }, file, position);
if (codeFrame) {
tokens.push('');
tokens.push(codeFrame);
@ -365,7 +365,7 @@ function indent(lines: string, tab: string) {
return lines.replace(/^(?=.+$)/gm, tab);
}
function generateCodeFrame(highlightCode: boolean, file?: string, position?: PositionInFile): string | undefined {
export function generateCodeFrame(options: BabelCodeFrameOptions, file?: string, position?: PositionInFile): string | undefined {
if (!position || !file)
return;
@ -373,7 +373,7 @@ function generateCodeFrame(highlightCode: boolean, file?: string, position?: Pos
const codeFrame = codeFrameColumns(
source,
{ start: position },
{ highlightCode }
options
);
return codeFrame;

View File

@ -90,7 +90,8 @@ export type TestStep = {
title: string;
startTime: string;
duration: number;
log?: string[];
location?: Location;
snippet?: string;
error?: string;
steps: TestStep[];
};
@ -365,8 +366,9 @@ class HtmlBuilder {
title: step.title,
startTime: step.startTime,
duration: step.duration,
snippet: step.snippet,
steps: step.steps.map(s => this._createTestStep(s)),
log: step.log,
location: step.location,
error: step.error
};
}

View File

@ -20,7 +20,7 @@ import { FullProject } from '../types';
import { FullConfig, Location, Suite, TestCase, TestResult, TestStatus, TestStep } from '../../types/testReporter';
import { assert, calculateSha1 } from 'playwright-core/src/utils/utils';
import { sanitizeForFilePath } from '../util';
import { formatResultFailure } from './base';
import { formatResultFailure, generateCodeFrame } from './base';
import { toPosixPath, serializePatterns } from './json';
export type JsonLocation = Location;
@ -93,7 +93,8 @@ export type JsonTestStep = {
duration: number;
error?: JsonError;
steps: JsonTestStep[];
log?: string[];
location?: Location;
snippet?: string;
};
class RawReporter {
@ -159,18 +160,18 @@ class RawReporter {
fileId,
location,
suites: suite.suites.map(s => this._serializeSuite(s)),
tests: suite.tests.map(t => this._serializeTest(t, fileId, location.file)),
tests: suite.tests.map(t => this._serializeTest(t, fileId)),
};
}
private _serializeTest(test: TestCase, fileId: string, fileName: string): JsonTestCase {
private _serializeTest(test: TestCase, fileId: string): JsonTestCase {
const [, projectName, , ...titles] = test.titlePath();
const testIdExpression = `project:${projectName}|path:${titles.join('>')}`;
const testId = fileId + '-' + calculateSha1(testIdExpression);
return {
testId,
title: test.title,
location: this._relativeLocation(test.location),
location: this._relativeLocation(test.location)!,
expectedStatus: test.expectedStatus,
timeout: test.timeout,
annotations: test.annotations,
@ -202,8 +203,9 @@ class RawReporter {
startTime: step.startTime.toISOString(),
duration: step.duration,
error: step.error?.message,
location: this._relativeLocation(step.location),
steps: this._serializeSteps(test, step.steps),
log: step.data.log || undefined,
snippet: step.location ? generateCodeFrame({ highlightCode: true, linesBelow: 1, linesAbove: 1 }, step.location.file, step.location) : undefined
};
});
}
@ -248,9 +250,9 @@ class RawReporter {
};
}
private _relativeLocation(location: Location | undefined): Location {
private _relativeLocation(location: Location | undefined): Location | undefined {
if (!location)
return { file: '', line: 0, column: 0 };
return undefined;
const file = toPosixPath(path.relative(this.config.rootDir, location.file));
return {
file,

View File

@ -306,8 +306,9 @@ 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 hasLocation = data.location && !data.location.file.includes('@playwright');
// Sanitize location that comes from user land, it might have extra properties.
const location = data.location && hasLocation ? { file: data.location.file, line: data.location.line, column: data.location.column } : undefined;
const payload: StepBeginPayload = {
testId,
stepId,