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 { .chip-body > .tree-item {
max-width: 600px;
line-height: 38px; line-height: 38px;
} }

View File

@ -217,10 +217,11 @@ const StepTreeItem: React.FC<{
<span style={{ float: 'right' }}>{msToString(step.duration)}</span> <span style={{ float: 'right' }}>{msToString(step.duration)}</span>
{statusIcon(step.error ? 'failed' : 'passed')} {statusIcon(step.error ? 'failed' : 'passed')}
<span>{step.title}</span> <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>); const children = step.steps.map((s, i) => <StepTreeItem key={i} step={s} depth={depth + 1}></StepTreeItem>);
if (step.error) if (step.snippet)
children.unshift(<ErrorMessage key={-1} error={step.error}></ErrorMessage>); children.unshift(<ErrorMessage key='line' error={step.snippet}></ErrorMessage>);
return children; return children;
} : undefined} depth={depth}></TreeItem>; } : undefined} depth={depth}></TreeItem>;
}; };

View File

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

View File

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

View File

@ -306,8 +306,9 @@ export class WorkerRunner extends EventEmitter {
this.emit('stepEnd', payload); this.emit('stepEnd', payload);
} }
}; };
// Sanitize location that comes from userland. const hasLocation = data.location && !data.location.file.includes('@playwright');
const location = data.location ? { file: data.location.file, line: data.location.line, column: data.location.column } : undefined; // 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 = { const payload: StepBeginPayload = {
testId, testId,
stepId, stepId,