mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(api): expose step location UI (#9605)
This commit is contained in:
parent
c06a6e1f63
commit
bccd4c8906
@ -90,7 +90,6 @@ svg {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chip-body > .tree-item {
|
.chip-body > .tree-item {
|
||||||
max-width: 600px;
|
|
||||||
line-height: 38px;
|
line-height: 38px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>;
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user