Merge branch 'main' into sharding-algorithm
This commit is contained in:
commit
ea72517529
|
|
@ -451,7 +451,7 @@ The title of the currently running test as passed to `test(title, testFunction)`
|
||||||
* since: v1.10
|
* since: v1.10
|
||||||
- type: <[Array]<[string]>>
|
- type: <[Array]<[string]>>
|
||||||
|
|
||||||
The full title path starting with the project.
|
The full title path starting with the test file name.
|
||||||
|
|
||||||
## property: TestInfo.workerIndex
|
## property: TestInfo.workerIndex
|
||||||
* since: v1.10
|
* since: v1.10
|
||||||
|
|
|
||||||
|
|
@ -21,13 +21,13 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "firefox-beta",
|
"name": "firefox-beta",
|
||||||
"revision": "1462",
|
"revision": "1463",
|
||||||
"installByDefault": false,
|
"installByDefault": false,
|
||||||
"browserVersion": "130.0b2"
|
"browserVersion": "131.0b2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "webkit",
|
"name": "webkit",
|
||||||
"revision": "2071",
|
"revision": "2072",
|
||||||
"installByDefault": true,
|
"installByDefault": true,
|
||||||
"revisionOverrides": {
|
"revisionOverrides": {
|
||||||
"mac10.14": "1446",
|
"mac10.14": "1446",
|
||||||
|
|
|
||||||
|
|
@ -898,7 +898,7 @@ class FrameSession {
|
||||||
const buffer = Buffer.from(payload.data, 'base64');
|
const buffer = Buffer.from(payload.data, 'base64');
|
||||||
this._page.emit(Page.Events.ScreencastFrame, {
|
this._page.emit(Page.Events.ScreencastFrame, {
|
||||||
buffer,
|
buffer,
|
||||||
timestamp: payload.metadata.timestamp,
|
frameSwapWallTime: payload.metadata.timestamp ? payload.metadata.timestamp * 1000 : undefined,
|
||||||
width: payload.metadata.deviceWidth,
|
width: payload.metadata.deviceWidth,
|
||||||
height: payload.metadata.deviceHeight,
|
height: payload.metadata.deviceHeight,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ export class VideoRecorder {
|
||||||
private constructor(page: Page, ffmpegPath: string, progress: Progress) {
|
private constructor(page: Page, ffmpegPath: string, progress: Progress) {
|
||||||
this._progress = progress;
|
this._progress = progress;
|
||||||
this._ffmpegPath = ffmpegPath;
|
this._ffmpegPath = ffmpegPath;
|
||||||
page.on(Page.Events.ScreencastFrame, frame => this.writeFrame(frame.buffer, frame.timestamp));
|
page.on(Page.Events.ScreencastFrame, frame => this.writeFrame(frame.buffer, frame.frameSwapWallTime / 1000));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _launch(options: types.PageScreencastOptions) {
|
private async _launch(options: types.PageScreencastOptions) {
|
||||||
|
|
|
||||||
|
|
@ -846,6 +846,8 @@ export class PageBinding {
|
||||||
const handle = await context.evaluateHandle(takeHandle, { name, seq }).catch(e => null);
|
const handle = await context.evaluateHandle(takeHandle, { name, seq }).catch(e => null);
|
||||||
result = await binding.playwrightFunction({ frame: context.frame, page, context: page._browserContext }, handle);
|
result = await binding.playwrightFunction({ frame: context.frame, page, context: page._browserContext }, handle);
|
||||||
} else {
|
} else {
|
||||||
|
if (!Array.isArray(serializedArgs))
|
||||||
|
throw new Error(`serializedArgs is not an array. This can happen when Array.prototype.toJSON is defined incorrectly`);
|
||||||
const args = serializedArgs!.map(a => parseEvaluationResultValue(a));
|
const args = serializedArgs!.map(a => parseEvaluationResultValue(a));
|
||||||
result = await binding.playwrightFunction({ frame: context.frame, page, context: page._browserContext }, ...args);
|
result = await binding.playwrightFunction({ frame: context.frame, page, context: page._browserContext }, ...args);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,7 @@ export class Snapshotter {
|
||||||
html: data.html,
|
html: data.html,
|
||||||
viewport: data.viewport,
|
viewport: data.viewport,
|
||||||
timestamp: monotonicTime(),
|
timestamp: monotonicTime(),
|
||||||
|
wallTime: data.wallTime,
|
||||||
collectionTime: data.collectionTime,
|
collectionTime: data.collectionTime,
|
||||||
resourceOverrides: [],
|
resourceOverrides: [],
|
||||||
isMainFrame: page.mainFrame() === frame
|
isMainFrame: page.mainFrame() === frame
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export type SnapshotData = {
|
||||||
}[],
|
}[],
|
||||||
viewport: { width: number, height: number },
|
viewport: { width: number, height: number },
|
||||||
url: string,
|
url: string,
|
||||||
timestamp: number,
|
wallTime: number,
|
||||||
collectionTime: number,
|
collectionTime: number,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -572,7 +572,7 @@ export function frameSnapshotStreamer(snapshotStreamer: string, removeNoScript:
|
||||||
height: window.innerHeight,
|
height: window.innerHeight,
|
||||||
},
|
},
|
||||||
url: location.href,
|
url: location.href,
|
||||||
timestamp,
|
wallTime: Date.now(),
|
||||||
collectionTime: 0,
|
collectionTime: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -589,7 +589,7 @@ export function frameSnapshotStreamer(snapshotStreamer: string, removeNoScript:
|
||||||
result.resourceOverrides.push({ url, content, contentType: 'text/css' },);
|
result.resourceOverrides.push({ url, content, contentType: 'text/css' },);
|
||||||
}
|
}
|
||||||
|
|
||||||
result.collectionTime = performance.now() - result.timestamp;
|
result.collectionTime = performance.now() - timestamp;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -472,7 +472,8 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
||||||
sha1,
|
sha1,
|
||||||
width: params.width,
|
width: params.width,
|
||||||
height: params.height,
|
height: params.height,
|
||||||
timestamp: monotonicTime()
|
timestamp: monotonicTime(),
|
||||||
|
frameSwapWallTime: params.frameSwapWallTime,
|
||||||
};
|
};
|
||||||
// Make sure to write the screencast frame before adding a reference to it.
|
// Make sure to write the screencast frame before adding a reference to it.
|
||||||
this._appendResource(sha1, params.buffer);
|
this._appendResource(sha1, params.buffer);
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,13 @@ export function transformObject(value: any, mapping: (v: any) => { result: any }
|
||||||
result.push(transformObject(item, mapping));
|
result.push(transformObject(item, mapping));
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
if (value?.__pw_type === 'jsx' && typeof value.type === 'function') {
|
||||||
|
throw new Error([
|
||||||
|
`Component "${value.type.name}" cannot be mounted.`,
|
||||||
|
`Most likely, this component is defined in the test file. Create a test story instead.`,
|
||||||
|
`For more information, see https://playwright.dev/docs/test-components#test-stories.`,
|
||||||
|
].join('\n'));
|
||||||
|
}
|
||||||
const result2: any = {};
|
const result2: any = {};
|
||||||
for (const [key, prop] of Object.entries(value))
|
for (const [key, prop] of Object.entries(value))
|
||||||
result2[key] = transformObject(prop, mapping);
|
result2[key] = transformObject(prop, mapping);
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ export class FullConfigInternal {
|
||||||
cliListOnly = false;
|
cliListOnly = false;
|
||||||
cliPassWithNoTests?: boolean;
|
cliPassWithNoTests?: boolean;
|
||||||
cliFailOnFlakyTests?: boolean;
|
cliFailOnFlakyTests?: boolean;
|
||||||
|
cliLastFailed?: boolean;
|
||||||
testIdMatcher?: Matcher;
|
testIdMatcher?: Matcher;
|
||||||
defineConfigWasUsed = false;
|
defineConfigWasUsed = false;
|
||||||
shardingMode: ShardingMode;
|
shardingMode: ShardingMode;
|
||||||
|
|
|
||||||
|
|
@ -133,12 +133,12 @@ export class TeleReporterReceiver {
|
||||||
public isListing = false;
|
public isListing = false;
|
||||||
private _rootSuite: TeleSuite;
|
private _rootSuite: TeleSuite;
|
||||||
private _options: TeleReporterReceiverOptions;
|
private _options: TeleReporterReceiverOptions;
|
||||||
private _reporter: Partial<ReporterV2>;
|
private _reporter: ReporterV2;
|
||||||
private _tests = new Map<string, TeleTestCase>();
|
private _tests = new Map<string, TeleTestCase>();
|
||||||
private _rootDir!: string;
|
private _rootDir!: string;
|
||||||
private _config!: reporterTypes.FullConfig;
|
private _config!: reporterTypes.FullConfig;
|
||||||
|
|
||||||
constructor(reporter: Partial<ReporterV2>, options: TeleReporterReceiverOptions = {}) {
|
constructor(reporter: ReporterV2, options: TeleReporterReceiverOptions = {}) {
|
||||||
this._rootSuite = new TeleSuite('', 'root');
|
this._rootSuite = new TeleSuite('', 'root');
|
||||||
this._options = options;
|
this._options = options;
|
||||||
this._reporter = reporter;
|
this._reporter = reporter;
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,7 @@ export class TeleSuiteUpdater {
|
||||||
// To work around that, have a dedicated per-run receiver that will only have
|
// To work around that, have a dedicated per-run receiver that will only have
|
||||||
// suite for a single test run, and hence will have correct total.
|
// suite for a single test run, and hence will have correct total.
|
||||||
this._lastRunReceiver = new TeleReporterReceiver({
|
this._lastRunReceiver = new TeleReporterReceiver({
|
||||||
|
version: () => 'v2',
|
||||||
onBegin: (suite: reporterTypes.Suite) => {
|
onBegin: (suite: reporterTypes.Suite) => {
|
||||||
this._lastRunTestCount = suite.allTests().length;
|
this._lastRunTestCount = suite.allTests().length;
|
||||||
this._lastRunReceiver = undefined;
|
this._lastRunReceiver = undefined;
|
||||||
|
|
@ -127,20 +128,13 @@ export class TeleSuiteUpdater {
|
||||||
|
|
||||||
onError: (error: reporterTypes.TestError) => this._handleOnError(error),
|
onError: (error: reporterTypes.TestError) => this._handleOnError(error),
|
||||||
|
|
||||||
printsToStdio: () => {
|
printsToStdio: () => false,
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
onStdOut: () => { },
|
|
||||||
onStdErr: () => { },
|
|
||||||
onExit: () => { },
|
|
||||||
onStepBegin: () => { },
|
|
||||||
onStepEnd: () => { },
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
processGlobalReport(report: any[]) {
|
processGlobalReport(report: any[]) {
|
||||||
const receiver = new TeleReporterReceiver({
|
const receiver = new TeleReporterReceiver({
|
||||||
|
version: () => 'v2',
|
||||||
onConfigure: (c: reporterTypes.FullConfig) => {
|
onConfigure: (c: reporterTypes.FullConfig) => {
|
||||||
this.config = c;
|
this.config = c;
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
import type { Command } from 'playwright-core/lib/utilsBundle';
|
import type { Command } from 'playwright-core/lib/utilsBundle';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { Runner, readLastRunInfo } from './runner/runner';
|
import { Runner } from './runner/runner';
|
||||||
import { stopProfiling, startProfiling, gracefullyProcessExitDoNotHang } from 'playwright-core/lib/utils';
|
import { stopProfiling, startProfiling, gracefullyProcessExitDoNotHang } from 'playwright-core/lib/utils';
|
||||||
import { serializeError } from './util';
|
import { serializeError } from './util';
|
||||||
import { showHTMLReport } from './reporters/html';
|
import { showHTMLReport } from './reporters/html';
|
||||||
|
|
@ -208,15 +208,6 @@ async function runTests(args: string[], opts: { [key: string]: any }) {
|
||||||
if (!config)
|
if (!config)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (opts.lastFailed || config.shardingMode === 'duration-round-robin') {
|
|
||||||
const lastRunInfo = await readLastRunInfo(config);
|
|
||||||
if (opts.lastFailed)
|
|
||||||
config.testIdMatcher = id => lastRunInfo.failedTests.includes(id);
|
|
||||||
|
|
||||||
if (config.shardingMode === 'duration-round-robin')
|
|
||||||
config.lastRunInfo = lastRunInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
config.cliArgs = args;
|
config.cliArgs = args;
|
||||||
config.cliGrep = opts.grep as string | undefined;
|
config.cliGrep = opts.grep as string | undefined;
|
||||||
config.cliOnlyChanged = opts.onlyChanged === true ? 'HEAD' : opts.onlyChanged;
|
config.cliOnlyChanged = opts.onlyChanged === true ? 'HEAD' : opts.onlyChanged;
|
||||||
|
|
@ -225,6 +216,7 @@ async function runTests(args: string[], opts: { [key: string]: any }) {
|
||||||
config.cliProjectFilter = opts.project || undefined;
|
config.cliProjectFilter = opts.project || undefined;
|
||||||
config.cliPassWithNoTests = !!opts.passWithNoTests;
|
config.cliPassWithNoTests = !!opts.passWithNoTests;
|
||||||
config.cliFailOnFlakyTests = !!opts.failOnFlakyTests;
|
config.cliFailOnFlakyTests = !!opts.failOnFlakyTests;
|
||||||
|
config.cliLastFailed = !!opts.lastFailed;
|
||||||
|
|
||||||
const runner = new Runner(config);
|
const runner = new Runner(config);
|
||||||
const status = await runner.runAllTests();
|
const status = await runner.runAllTests();
|
||||||
|
|
|
||||||
|
|
@ -123,9 +123,6 @@ export class BaseReporter implements ReporterV2 {
|
||||||
(result as any)[kOutputSymbol].push(output);
|
(result as any)[kOutputSymbol].push(output);
|
||||||
}
|
}
|
||||||
|
|
||||||
onTestBegin(test: TestCase, result: TestResult): void {
|
|
||||||
}
|
|
||||||
|
|
||||||
onTestEnd(test: TestCase, result: TestResult) {
|
onTestEnd(test: TestCase, result: TestResult) {
|
||||||
if (result.status !== 'skipped' && result.status !== test.expectedStatus)
|
if (result.status !== 'skipped' && result.status !== test.expectedStatus)
|
||||||
++this._failureCount;
|
++this._failureCount;
|
||||||
|
|
@ -146,19 +143,6 @@ export class BaseReporter implements ReporterV2 {
|
||||||
this.result = result;
|
this.result = result;
|
||||||
}
|
}
|
||||||
|
|
||||||
onStepBegin(test: TestCase, result: TestResult, step: TestStep): void {
|
|
||||||
}
|
|
||||||
|
|
||||||
onStepEnd(test: TestCase, result: TestResult, step: TestStep): void {
|
|
||||||
}
|
|
||||||
|
|
||||||
async onExit() {
|
|
||||||
}
|
|
||||||
|
|
||||||
printsToStdio() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fitToScreen(line: string, prefix?: string): string {
|
protected fitToScreen(line: string, prefix?: string): string {
|
||||||
if (!ttyWidth) {
|
if (!ttyWidth) {
|
||||||
// Guard against the case where we cannot determine available width.
|
// Guard against the case where we cannot determine available width.
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,6 @@ import type { FullResult, TestCase, TestResult, Suite, TestError } from '../../t
|
||||||
class DotReporter extends BaseReporter {
|
class DotReporter extends BaseReporter {
|
||||||
private _counter = 0;
|
private _counter = 0;
|
||||||
|
|
||||||
override printsToStdio() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
override onBegin(suite: Suite) {
|
override onBegin(suite: Suite) {
|
||||||
super.onBegin(suite);
|
super.onBegin(suite);
|
||||||
console.log(this.generateStartingMessage());
|
console.log(this.generateStartingMessage());
|
||||||
|
|
|
||||||
|
|
@ -15,49 +15,15 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ReporterV2 } from './reporterV2';
|
import type { ReporterV2 } from './reporterV2';
|
||||||
import type { FullConfig, TestCase, TestError, TestResult, FullResult, TestStep, Suite } from '../../types/testReporter';
|
|
||||||
|
|
||||||
class EmptyReporter implements ReporterV2 {
|
class EmptyReporter implements ReporterV2 {
|
||||||
onConfigure(config: FullConfig) {
|
version(): 'v2' {
|
||||||
}
|
return 'v2';
|
||||||
|
|
||||||
onBegin(suite: Suite) {
|
|
||||||
}
|
|
||||||
|
|
||||||
onTestBegin(test: TestCase, result: TestResult) {
|
|
||||||
}
|
|
||||||
|
|
||||||
onStdOut(chunk: string | Buffer, test?: TestCase, result?: TestResult) {
|
|
||||||
}
|
|
||||||
|
|
||||||
onStdErr(chunk: string | Buffer, test?: TestCase, result?: TestResult) {
|
|
||||||
}
|
|
||||||
|
|
||||||
onTestEnd(test: TestCase, result: TestResult) {
|
|
||||||
}
|
|
||||||
|
|
||||||
async onEnd(result: FullResult) {
|
|
||||||
}
|
|
||||||
|
|
||||||
async onExit() {
|
|
||||||
}
|
|
||||||
|
|
||||||
onError(error: TestError) {
|
|
||||||
}
|
|
||||||
|
|
||||||
onStepBegin(test: TestCase, result: TestResult, step: TestStep) {
|
|
||||||
}
|
|
||||||
|
|
||||||
onStepEnd(test: TestCase, result: TestResult, step: TestStep) {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
printsToStdio() {
|
printsToStdio() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
version(): 'v2' {
|
|
||||||
return 'v2';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EmptyReporter;
|
export default EmptyReporter;
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ class GitHubLogger {
|
||||||
export class GitHubReporter extends BaseReporter {
|
export class GitHubReporter extends BaseReporter {
|
||||||
githubLogger = new GitHubLogger();
|
githubLogger = new GitHubLogger();
|
||||||
|
|
||||||
override printsToStdio() {
|
printsToStdio() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ import type { ZipFile } from 'playwright-core/lib/zipBundle';
|
||||||
import { yazl } from 'playwright-core/lib/zipBundle';
|
import { yazl } from 'playwright-core/lib/zipBundle';
|
||||||
import { mime } from 'playwright-core/lib/utilsBundle';
|
import { mime } from 'playwright-core/lib/utilsBundle';
|
||||||
import type { HTMLReport, Stats, TestAttachment, TestCase, TestCaseSummary, TestFile, TestFileSummary, TestResult, TestStep } from '@html-reporter/types';
|
import type { HTMLReport, Stats, TestAttachment, TestCase, TestCaseSummary, TestFile, TestFileSummary, TestResult, TestStep } from '@html-reporter/types';
|
||||||
import EmptyReporter from './empty';
|
import type { ReporterV2 } from './reporterV2';
|
||||||
|
|
||||||
type TestEntry = {
|
type TestEntry = {
|
||||||
testCase: TestCase;
|
testCase: TestCase;
|
||||||
|
|
@ -55,7 +55,7 @@ type HtmlReporterOptions = {
|
||||||
_isTestServer?: boolean;
|
_isTestServer?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
class HtmlReporter extends EmptyReporter {
|
class HtmlReporter implements ReporterV2 {
|
||||||
private config!: FullConfig;
|
private config!: FullConfig;
|
||||||
private suite!: Suite;
|
private suite!: Suite;
|
||||||
private _options: HtmlReporterOptions;
|
private _options: HtmlReporterOptions;
|
||||||
|
|
@ -68,19 +68,22 @@ class HtmlReporter extends EmptyReporter {
|
||||||
private _topLevelErrors: TestError[] = [];
|
private _topLevelErrors: TestError[] = [];
|
||||||
|
|
||||||
constructor(options: HtmlReporterOptions) {
|
constructor(options: HtmlReporterOptions) {
|
||||||
super();
|
|
||||||
this._options = options;
|
this._options = options;
|
||||||
}
|
}
|
||||||
|
|
||||||
override printsToStdio() {
|
version(): 'v2' {
|
||||||
|
return 'v2';
|
||||||
|
}
|
||||||
|
|
||||||
|
printsToStdio() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
override onConfigure(config: FullConfig) {
|
onConfigure(config: FullConfig) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
override onBegin(suite: Suite) {
|
onBegin(suite: Suite) {
|
||||||
const { outputFolder, open, attachmentsBaseURL, host, port } = this._resolveOptions();
|
const { outputFolder, open, attachmentsBaseURL, host, port } = this._resolveOptions();
|
||||||
this._outputFolder = outputFolder;
|
this._outputFolder = outputFolder;
|
||||||
this._open = open;
|
this._open = open;
|
||||||
|
|
@ -122,18 +125,18 @@ class HtmlReporter extends EmptyReporter {
|
||||||
return !!relativePath && !relativePath.startsWith('..') && !path.isAbsolute(relativePath);
|
return !!relativePath && !relativePath.startsWith('..') && !path.isAbsolute(relativePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
override onError(error: TestError): void {
|
onError(error: TestError): void {
|
||||||
this._topLevelErrors.push(error);
|
this._topLevelErrors.push(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
override async onEnd(result: FullResult) {
|
async onEnd(result: FullResult) {
|
||||||
const projectSuites = this.suite.suites;
|
const projectSuites = this.suite.suites;
|
||||||
await removeFolders([this._outputFolder]);
|
await removeFolders([this._outputFolder]);
|
||||||
const builder = new HtmlBuilder(this.config, this._outputFolder, this._attachmentsBaseURL);
|
const builder = new HtmlBuilder(this.config, this._outputFolder, this._attachmentsBaseURL);
|
||||||
this._buildResult = await builder.build(this.config.metadata, projectSuites, result, this._topLevelErrors);
|
this._buildResult = await builder.build(this.config.metadata, projectSuites, result, this._topLevelErrors);
|
||||||
}
|
}
|
||||||
|
|
||||||
override async onExit() {
|
async onExit() {
|
||||||
if (process.env.CI || !this._buildResult)
|
if (process.env.CI || !this._buildResult)
|
||||||
return;
|
return;
|
||||||
const { ok, singleTestId } = this._buildResult;
|
const { ok, singleTestId } = this._buildResult;
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import type { ReporterV2 } from './reporterV2';
|
||||||
import { monotonicTime } from 'playwright-core/lib/utils';
|
import { monotonicTime } from 'playwright-core/lib/utils';
|
||||||
import { Multiplexer } from './multiplexer';
|
import { Multiplexer } from './multiplexer';
|
||||||
|
|
||||||
export class InternalReporter {
|
export class InternalReporter implements ReporterV2 {
|
||||||
private _reporter: ReporterV2;
|
private _reporter: ReporterV2;
|
||||||
private _didBegin = false;
|
private _didBegin = false;
|
||||||
private _config!: FullConfig;
|
private _config!: FullConfig;
|
||||||
|
|
@ -42,29 +42,29 @@ export class InternalReporter {
|
||||||
this._config = config;
|
this._config = config;
|
||||||
this._startTime = new Date();
|
this._startTime = new Date();
|
||||||
this._monotonicStartTime = monotonicTime();
|
this._monotonicStartTime = monotonicTime();
|
||||||
this._reporter.onConfigure(config);
|
this._reporter.onConfigure?.(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
onBegin(suite: Suite) {
|
onBegin(suite: Suite) {
|
||||||
this._didBegin = true;
|
this._didBegin = true;
|
||||||
this._reporter.onBegin(suite);
|
this._reporter.onBegin?.(suite);
|
||||||
}
|
}
|
||||||
|
|
||||||
onTestBegin(test: TestCase, result: TestResult) {
|
onTestBegin(test: TestCase, result: TestResult) {
|
||||||
this._reporter.onTestBegin(test, result);
|
this._reporter.onTestBegin?.(test, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
onStdOut(chunk: string | Buffer, test?: TestCase, result?: TestResult) {
|
onStdOut(chunk: string | Buffer, test?: TestCase, result?: TestResult) {
|
||||||
this._reporter.onStdOut(chunk, test, result);
|
this._reporter.onStdOut?.(chunk, test, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
onStdErr(chunk: string | Buffer, test?: TestCase, result?: TestResult) {
|
onStdErr(chunk: string | Buffer, test?: TestCase, result?: TestResult) {
|
||||||
this._reporter.onStdErr(chunk, test, result);
|
this._reporter.onStdErr?.(chunk, test, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
onTestEnd(test: TestCase, result: TestResult) {
|
onTestEnd(test: TestCase, result: TestResult) {
|
||||||
this._addSnippetToTestErrors(test, result);
|
this._addSnippetToTestErrors(test, result);
|
||||||
this._reporter.onTestEnd(test, result);
|
this._reporter.onTestEnd?.(test, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onEnd(result: { status: FullResult['status'] }) {
|
async onEnd(result: { status: FullResult['status'] }) {
|
||||||
|
|
@ -72,7 +72,7 @@ export class InternalReporter {
|
||||||
// onBegin was not reported, emit it.
|
// onBegin was not reported, emit it.
|
||||||
this.onBegin(new Suite('', 'root'));
|
this.onBegin(new Suite('', 'root'));
|
||||||
}
|
}
|
||||||
return await this._reporter.onEnd({
|
return await this._reporter.onEnd?.({
|
||||||
...result,
|
...result,
|
||||||
startTime: this._startTime!,
|
startTime: this._startTime!,
|
||||||
duration: monotonicTime() - this._monotonicStartTime!,
|
duration: monotonicTime() - this._monotonicStartTime!,
|
||||||
|
|
@ -80,25 +80,25 @@ export class InternalReporter {
|
||||||
}
|
}
|
||||||
|
|
||||||
async onExit() {
|
async onExit() {
|
||||||
await this._reporter.onExit();
|
await this._reporter.onExit?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
onError(error: TestError) {
|
onError(error: TestError) {
|
||||||
addLocationAndSnippetToError(this._config, error);
|
addLocationAndSnippetToError(this._config, error);
|
||||||
this._reporter.onError(error);
|
this._reporter.onError?.(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
onStepBegin(test: TestCase, result: TestResult, step: TestStep) {
|
onStepBegin(test: TestCase, result: TestResult, step: TestStep) {
|
||||||
this._reporter.onStepBegin(test, result, step);
|
this._reporter.onStepBegin?.(test, result, step);
|
||||||
}
|
}
|
||||||
|
|
||||||
onStepEnd(test: TestCase, result: TestResult, step: TestStep) {
|
onStepEnd(test: TestCase, result: TestResult, step: TestStep) {
|
||||||
this._addSnippetToStepError(test, step);
|
this._addSnippetToStepError(test, step);
|
||||||
this._reporter.onStepEnd(test, result, step);
|
this._reporter.onStepEnd?.(test, result, step);
|
||||||
}
|
}
|
||||||
|
|
||||||
printsToStdio() {
|
printsToStdio() {
|
||||||
return this._reporter.printsToStdio();
|
return this._reporter.printsToStdio ? this._reporter.printsToStdio() : true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _addSnippetToTestErrors(test: TestCase, result: TestResult) {
|
private _addSnippetToTestErrors(test: TestCase, result: TestResult) {
|
||||||
|
|
|
||||||
|
|
@ -20,41 +20,44 @@ import type { FullConfig, TestCase, Suite, TestResult, TestError, TestStep, Full
|
||||||
import { formatError, prepareErrorStack, resolveOutputFile } from './base';
|
import { formatError, prepareErrorStack, resolveOutputFile } from './base';
|
||||||
import { MultiMap, toPosixPath } from 'playwright-core/lib/utils';
|
import { MultiMap, toPosixPath } from 'playwright-core/lib/utils';
|
||||||
import { getProjectId } from '../common/config';
|
import { getProjectId } from '../common/config';
|
||||||
import EmptyReporter from './empty';
|
import type { ReporterV2 } from './reporterV2';
|
||||||
|
|
||||||
type JSONOptions = {
|
type JSONOptions = {
|
||||||
outputFile?: string,
|
outputFile?: string,
|
||||||
configDir: string,
|
configDir: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
class JSONReporter extends EmptyReporter {
|
class JSONReporter implements ReporterV2 {
|
||||||
config!: FullConfig;
|
config!: FullConfig;
|
||||||
suite!: Suite;
|
suite!: Suite;
|
||||||
private _errors: TestError[] = [];
|
private _errors: TestError[] = [];
|
||||||
private _resolvedOutputFile: string | undefined;
|
private _resolvedOutputFile: string | undefined;
|
||||||
|
|
||||||
constructor(options: JSONOptions) {
|
constructor(options: JSONOptions) {
|
||||||
super();
|
|
||||||
this._resolvedOutputFile = resolveOutputFile('JSON', options)?.outputFile;
|
this._resolvedOutputFile = resolveOutputFile('JSON', options)?.outputFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
override printsToStdio() {
|
version(): 'v2' {
|
||||||
|
return 'v2';
|
||||||
|
}
|
||||||
|
|
||||||
|
printsToStdio() {
|
||||||
return !this._resolvedOutputFile;
|
return !this._resolvedOutputFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
override onConfigure(config: FullConfig) {
|
onConfigure(config: FullConfig) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
override onBegin(suite: Suite) {
|
onBegin(suite: Suite) {
|
||||||
this.suite = suite;
|
this.suite = suite;
|
||||||
}
|
}
|
||||||
|
|
||||||
override onError(error: TestError): void {
|
onError(error: TestError): void {
|
||||||
this._errors.push(error);
|
this._errors.push(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
override async onEnd(result: FullResult) {
|
async onEnd(result: FullResult) {
|
||||||
await outputReport(this._serializeReport(result), this._resolvedOutputFile);
|
await outputReport(this._serializeReport(result), this._resolvedOutputFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@ import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import type { FullConfig, FullResult, Suite, TestCase } from '../../types/testReporter';
|
import type { FullConfig, FullResult, Suite, TestCase } from '../../types/testReporter';
|
||||||
import { formatFailure, resolveOutputFile, stripAnsiEscapes } from './base';
|
import { formatFailure, resolveOutputFile, stripAnsiEscapes } from './base';
|
||||||
import EmptyReporter from './empty';
|
|
||||||
import { getAsBooleanFromENV } from 'playwright-core/lib/utils';
|
import { getAsBooleanFromENV } from 'playwright-core/lib/utils';
|
||||||
|
import type { ReporterV2 } from './reporterV2';
|
||||||
|
|
||||||
type JUnitOptions = {
|
type JUnitOptions = {
|
||||||
outputFile?: string,
|
outputFile?: string,
|
||||||
|
|
@ -29,7 +29,7 @@ type JUnitOptions = {
|
||||||
configDir: string,
|
configDir: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
class JUnitReporter extends EmptyReporter {
|
class JUnitReporter implements ReporterV2 {
|
||||||
private config!: FullConfig;
|
private config!: FullConfig;
|
||||||
private configDir: string;
|
private configDir: string;
|
||||||
private suite!: Suite;
|
private suite!: Suite;
|
||||||
|
|
@ -42,27 +42,30 @@ class JUnitReporter extends EmptyReporter {
|
||||||
private includeProjectInTestName = false;
|
private includeProjectInTestName = false;
|
||||||
|
|
||||||
constructor(options: JUnitOptions) {
|
constructor(options: JUnitOptions) {
|
||||||
super();
|
|
||||||
this.stripANSIControlSequences = getAsBooleanFromENV('PLAYWRIGHT_JUNIT_STRIP_ANSI', !!options.stripANSIControlSequences);
|
this.stripANSIControlSequences = getAsBooleanFromENV('PLAYWRIGHT_JUNIT_STRIP_ANSI', !!options.stripANSIControlSequences);
|
||||||
this.includeProjectInTestName = getAsBooleanFromENV('PLAYWRIGHT_JUNIT_INCLUDE_PROJECT_IN_TEST_NAME', !!options.includeProjectInTestName);
|
this.includeProjectInTestName = getAsBooleanFromENV('PLAYWRIGHT_JUNIT_INCLUDE_PROJECT_IN_TEST_NAME', !!options.includeProjectInTestName);
|
||||||
this.configDir = options.configDir;
|
this.configDir = options.configDir;
|
||||||
this.resolvedOutputFile = resolveOutputFile('JUNIT', options)?.outputFile;
|
this.resolvedOutputFile = resolveOutputFile('JUNIT', options)?.outputFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
override printsToStdio() {
|
version(): 'v2' {
|
||||||
|
return 'v2';
|
||||||
|
}
|
||||||
|
|
||||||
|
printsToStdio() {
|
||||||
return !this.resolvedOutputFile;
|
return !this.resolvedOutputFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
override onConfigure(config: FullConfig) {
|
onConfigure(config: FullConfig) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
override onBegin(suite: Suite) {
|
onBegin(suite: Suite) {
|
||||||
this.suite = suite;
|
this.suite = suite;
|
||||||
this.timestamp = new Date();
|
this.timestamp = new Date();
|
||||||
}
|
}
|
||||||
|
|
||||||
override async onEnd(result: FullResult) {
|
async onEnd(result: FullResult) {
|
||||||
const children: XMLEntry[] = [];
|
const children: XMLEntry[] = [];
|
||||||
for (const projectSuite of this.suite.suites) {
|
for (const projectSuite of this.suite.suites) {
|
||||||
for (const fileSuite of projectSuite.suites)
|
for (const fileSuite of projectSuite.suites)
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,6 @@ class LineReporter extends BaseReporter {
|
||||||
private _lastTest: TestCase | undefined;
|
private _lastTest: TestCase | undefined;
|
||||||
private _didBegin = false;
|
private _didBegin = false;
|
||||||
|
|
||||||
override printsToStdio() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
override onBegin(suite: Suite) {
|
override onBegin(suite: Suite) {
|
||||||
super.onBegin(suite);
|
super.onBegin(suite);
|
||||||
const startingMessage = this.generateStartingMessage();
|
const startingMessage = this.generateStartingMessage();
|
||||||
|
|
@ -66,20 +62,17 @@ class LineReporter extends BaseReporter {
|
||||||
console.log();
|
console.log();
|
||||||
}
|
}
|
||||||
|
|
||||||
override onTestBegin(test: TestCase, result: TestResult) {
|
onTestBegin(test: TestCase, result: TestResult) {
|
||||||
super.onTestBegin(test, result);
|
|
||||||
++this._current;
|
++this._current;
|
||||||
this._updateLine(test, result, undefined);
|
this._updateLine(test, result, undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
override onStepBegin(test: TestCase, result: TestResult, step: TestStep) {
|
onStepBegin(test: TestCase, result: TestResult, step: TestStep) {
|
||||||
super.onStepBegin(test, result, step);
|
|
||||||
if (step.category === 'test.step')
|
if (step.category === 'test.step')
|
||||||
this._updateLine(test, result, step);
|
this._updateLine(test, result, step);
|
||||||
}
|
}
|
||||||
|
|
||||||
override onStepEnd(test: TestCase, result: TestResult, step: TestStep) {
|
onStepEnd(test: TestCase, result: TestResult, step: TestStep) {
|
||||||
super.onStepEnd(test, result, step);
|
|
||||||
if (step.category === 'test.step')
|
if (step.category === 'test.step')
|
||||||
this._updateLine(test, result, step.parent);
|
this._updateLine(test, result, step.parent);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,10 +39,6 @@ class ListReporter extends BaseReporter {
|
||||||
this._printSteps = getAsBooleanFromENV('PLAYWRIGHT_LIST_PRINT_STEPS', options.printSteps);
|
this._printSteps = getAsBooleanFromENV('PLAYWRIGHT_LIST_PRINT_STEPS', options.printSteps);
|
||||||
}
|
}
|
||||||
|
|
||||||
override printsToStdio() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
override onBegin(suite: Suite) {
|
override onBegin(suite: Suite) {
|
||||||
super.onBegin(suite);
|
super.onBegin(suite);
|
||||||
const startingMessage = this.generateStartingMessage();
|
const startingMessage = this.generateStartingMessage();
|
||||||
|
|
@ -52,9 +48,7 @@ class ListReporter extends BaseReporter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override onTestBegin(test: TestCase, result: TestResult) {
|
onTestBegin(test: TestCase, result: TestResult) {
|
||||||
super.onTestBegin(test, result);
|
|
||||||
|
|
||||||
const index = String(this._resultIndex.size + 1);
|
const index = String(this._resultIndex.size + 1);
|
||||||
this._resultIndex.set(result, index);
|
this._resultIndex.set(result, index);
|
||||||
|
|
||||||
|
|
@ -88,8 +82,7 @@ class ListReporter extends BaseReporter {
|
||||||
return stepIndex;
|
return stepIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
override onStepBegin(test: TestCase, result: TestResult, step: TestStep) {
|
onStepBegin(test: TestCase, result: TestResult, step: TestStep) {
|
||||||
super.onStepBegin(test, result, step);
|
|
||||||
if (step.category !== 'test.step')
|
if (step.category !== 'test.step')
|
||||||
return;
|
return;
|
||||||
const testIndex = this._resultIndex.get(result) || '';
|
const testIndex = this._resultIndex.get(result) || '';
|
||||||
|
|
@ -108,8 +101,7 @@ class ListReporter extends BaseReporter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override onStepEnd(test: TestCase, result: TestResult, step: TestStep) {
|
onStepEnd(test: TestCase, result: TestResult, step: TestStep) {
|
||||||
super.onStepEnd(test, result, step);
|
|
||||||
if (step.category !== 'test.step')
|
if (step.category !== 'test.step')
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ class MarkdownReporter extends BaseReporter {
|
||||||
this._options = options;
|
this._options = options;
|
||||||
}
|
}
|
||||||
|
|
||||||
override printsToStdio() {
|
printsToStdio() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,37 +31,37 @@ export class Multiplexer implements ReporterV2 {
|
||||||
|
|
||||||
onConfigure(config: FullConfig) {
|
onConfigure(config: FullConfig) {
|
||||||
for (const reporter of this._reporters)
|
for (const reporter of this._reporters)
|
||||||
wrap(() => reporter.onConfigure(config));
|
wrap(() => reporter.onConfigure?.(config));
|
||||||
}
|
}
|
||||||
|
|
||||||
onBegin(suite: Suite) {
|
onBegin(suite: Suite) {
|
||||||
for (const reporter of this._reporters)
|
for (const reporter of this._reporters)
|
||||||
wrap(() => reporter.onBegin(suite));
|
wrap(() => reporter.onBegin?.(suite));
|
||||||
}
|
}
|
||||||
|
|
||||||
onTestBegin(test: TestCase, result: TestResult) {
|
onTestBegin(test: TestCase, result: TestResult) {
|
||||||
for (const reporter of this._reporters)
|
for (const reporter of this._reporters)
|
||||||
wrap(() => reporter.onTestBegin(test, result));
|
wrap(() => reporter.onTestBegin?.(test, result));
|
||||||
}
|
}
|
||||||
|
|
||||||
onStdOut(chunk: string | Buffer, test?: TestCase, result?: TestResult) {
|
onStdOut(chunk: string | Buffer, test?: TestCase, result?: TestResult) {
|
||||||
for (const reporter of this._reporters)
|
for (const reporter of this._reporters)
|
||||||
wrap(() => reporter.onStdOut(chunk, test, result));
|
wrap(() => reporter.onStdOut?.(chunk, test, result));
|
||||||
}
|
}
|
||||||
|
|
||||||
onStdErr(chunk: string | Buffer, test?: TestCase, result?: TestResult) {
|
onStdErr(chunk: string | Buffer, test?: TestCase, result?: TestResult) {
|
||||||
for (const reporter of this._reporters)
|
for (const reporter of this._reporters)
|
||||||
wrap(() => reporter.onStdErr(chunk, test, result));
|
wrap(() => reporter.onStdErr?.(chunk, test, result));
|
||||||
}
|
}
|
||||||
|
|
||||||
onTestEnd(test: TestCase, result: TestResult) {
|
onTestEnd(test: TestCase, result: TestResult) {
|
||||||
for (const reporter of this._reporters)
|
for (const reporter of this._reporters)
|
||||||
wrap(() => reporter.onTestEnd(test, result));
|
wrap(() => reporter.onTestEnd?.(test, result));
|
||||||
}
|
}
|
||||||
|
|
||||||
async onEnd(result: FullResult) {
|
async onEnd(result: FullResult) {
|
||||||
for (const reporter of this._reporters) {
|
for (const reporter of this._reporters) {
|
||||||
const outResult = await wrapAsync(() => reporter.onEnd(result));
|
const outResult = await wrapAsync(() => reporter.onEnd?.(result));
|
||||||
if (outResult?.status)
|
if (outResult?.status)
|
||||||
result.status = outResult.status;
|
result.status = outResult.status;
|
||||||
}
|
}
|
||||||
|
|
@ -70,28 +70,28 @@ export class Multiplexer implements ReporterV2 {
|
||||||
|
|
||||||
async onExit() {
|
async onExit() {
|
||||||
for (const reporter of this._reporters)
|
for (const reporter of this._reporters)
|
||||||
await wrapAsync(() => reporter.onExit());
|
await wrapAsync(() => reporter.onExit?.());
|
||||||
}
|
}
|
||||||
|
|
||||||
onError(error: TestError) {
|
onError(error: TestError) {
|
||||||
for (const reporter of this._reporters)
|
for (const reporter of this._reporters)
|
||||||
wrap(() => reporter.onError(error));
|
wrap(() => reporter.onError?.(error));
|
||||||
}
|
}
|
||||||
|
|
||||||
onStepBegin(test: TestCase, result: TestResult, step: TestStep) {
|
onStepBegin(test: TestCase, result: TestResult, step: TestStep) {
|
||||||
for (const reporter of this._reporters)
|
for (const reporter of this._reporters)
|
||||||
wrap(() => reporter.onStepBegin(test, result, step));
|
wrap(() => reporter.onStepBegin?.(test, result, step));
|
||||||
}
|
}
|
||||||
|
|
||||||
onStepEnd(test: TestCase, result: TestResult, step: TestStep) {
|
onStepEnd(test: TestCase, result: TestResult, step: TestStep) {
|
||||||
for (const reporter of this._reporters)
|
for (const reporter of this._reporters)
|
||||||
wrap(() => reporter.onStepEnd(test, result, step));
|
wrap(() => reporter.onStepEnd?.(test, result, step));
|
||||||
}
|
}
|
||||||
|
|
||||||
printsToStdio(): boolean {
|
printsToStdio(): boolean {
|
||||||
return this._reporters.some(r => {
|
return this._reporters.some(r => {
|
||||||
let prints = true;
|
let prints = false;
|
||||||
wrap(() => prints = r.printsToStdio());
|
wrap(() => prints = r.printsToStdio ? r.printsToStdio() : true);
|
||||||
return prints;
|
return prints;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,18 +17,18 @@
|
||||||
import type { FullConfig, TestCase, TestError, TestResult, FullResult, TestStep, Reporter, Suite } from '../../types/testReporter';
|
import type { FullConfig, TestCase, TestError, TestResult, FullResult, TestStep, Reporter, Suite } from '../../types/testReporter';
|
||||||
|
|
||||||
export interface ReporterV2 {
|
export interface ReporterV2 {
|
||||||
onConfigure(config: FullConfig): void;
|
onConfigure?(config: FullConfig): void;
|
||||||
onBegin(suite: Suite): void;
|
onBegin?(suite: Suite): void;
|
||||||
onTestBegin(test: TestCase, result: TestResult): void;
|
onTestBegin?(test: TestCase, result: TestResult): void;
|
||||||
onStdOut(chunk: string | Buffer, test?: TestCase, result?: TestResult): void;
|
onStdOut?(chunk: string | Buffer, test?: TestCase, result?: TestResult): void;
|
||||||
onStdErr(chunk: string | Buffer, test?: TestCase, result?: TestResult): void;
|
onStdErr?(chunk: string | Buffer, test?: TestCase, result?: TestResult): void;
|
||||||
onTestEnd(test: TestCase, result: TestResult): void;
|
onTestEnd?(test: TestCase, result: TestResult): void;
|
||||||
onEnd(result: FullResult): Promise<{ status?: FullResult['status'] } | undefined | void> | void;
|
onEnd?(result: FullResult): Promise<{ status?: FullResult['status'] } | undefined | void> | void;
|
||||||
onExit(): void | Promise<void>;
|
onExit?(): void | Promise<void>;
|
||||||
onError(error: TestError): void;
|
onError?(error: TestError): void;
|
||||||
onStepBegin(test: TestCase, result: TestResult, step: TestStep): void;
|
onStepBegin?(test: TestCase, result: TestResult, step: TestStep): void;
|
||||||
onStepEnd(test: TestCase, result: TestResult, step: TestStep): void;
|
onStepEnd?(test: TestCase, result: TestResult, step: TestStep): void;
|
||||||
printsToStdio(): boolean;
|
printsToStdio?(): boolean;
|
||||||
version(): 'v2';
|
version(): 'v2';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -145,9 +145,6 @@ export class TeleReporterEmitter implements ReporterV2 {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async onExit() {
|
|
||||||
}
|
|
||||||
|
|
||||||
printsToStdio() {
|
printsToStdio() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -198,17 +198,17 @@ export class Dispatcher {
|
||||||
worker.on('stdOut', (params: TestOutputPayload) => {
|
worker.on('stdOut', (params: TestOutputPayload) => {
|
||||||
const { chunk, test, result } = handleOutput(params);
|
const { chunk, test, result } = handleOutput(params);
|
||||||
result?.stdout.push(chunk);
|
result?.stdout.push(chunk);
|
||||||
this._reporter.onStdOut(chunk, test, result);
|
this._reporter.onStdOut?.(chunk, test, result);
|
||||||
});
|
});
|
||||||
worker.on('stdErr', (params: TestOutputPayload) => {
|
worker.on('stdErr', (params: TestOutputPayload) => {
|
||||||
const { chunk, test, result } = handleOutput(params);
|
const { chunk, test, result } = handleOutput(params);
|
||||||
result?.stderr.push(chunk);
|
result?.stderr.push(chunk);
|
||||||
this._reporter.onStdErr(chunk, test, result);
|
this._reporter.onStdErr?.(chunk, test, result);
|
||||||
});
|
});
|
||||||
worker.on('teardownErrors', (params: TeardownErrorsPayload) => {
|
worker.on('teardownErrors', (params: TeardownErrorsPayload) => {
|
||||||
this._failureTracker.onWorkerError();
|
this._failureTracker.onWorkerError();
|
||||||
for (const error of params.fatalErrors)
|
for (const error of params.fatalErrors)
|
||||||
this._reporter.onError(error);
|
this._reporter.onError?.(error);
|
||||||
});
|
});
|
||||||
worker.on('exit', () => {
|
worker.on('exit', () => {
|
||||||
const producedEnv = this._producedEnvByProjectId.get(testGroup.projectId) || {};
|
const producedEnv = this._producedEnvByProjectId.get(testGroup.projectId) || {};
|
||||||
|
|
@ -257,7 +257,7 @@ class JobDispatcher {
|
||||||
result.parallelIndex = this._parallelIndex;
|
result.parallelIndex = this._parallelIndex;
|
||||||
result.workerIndex = this._workerIndex;
|
result.workerIndex = this._workerIndex;
|
||||||
result.startTime = new Date(params.startWallTime);
|
result.startTime = new Date(params.startWallTime);
|
||||||
this._reporter.onTestBegin(test, result);
|
this._reporter.onTestBegin?.(test, result);
|
||||||
this._currentlyRunning = { test, result };
|
this._currentlyRunning = { test, result };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -323,7 +323,7 @@ class JobDispatcher {
|
||||||
};
|
};
|
||||||
steps.set(params.stepId, step);
|
steps.set(params.stepId, step);
|
||||||
(parentStep || result).steps.push(step);
|
(parentStep || result).steps.push(step);
|
||||||
this._reporter.onStepBegin(test, result, step);
|
this._reporter.onStepBegin?.(test, result, step);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onStepEnd(params: StepEndPayload) {
|
private _onStepEnd(params: StepEndPayload) {
|
||||||
|
|
@ -335,14 +335,14 @@ class JobDispatcher {
|
||||||
const { result, steps, test } = data;
|
const { result, steps, test } = data;
|
||||||
const step = steps.get(params.stepId);
|
const step = steps.get(params.stepId);
|
||||||
if (!step) {
|
if (!step) {
|
||||||
this._reporter.onStdErr('Internal error: step end without step begin: ' + params.stepId, test, result);
|
this._reporter.onStdErr?.('Internal error: step end without step begin: ' + params.stepId, test, result);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
step.duration = params.wallTime - step.startTime.getTime();
|
step.duration = params.wallTime - step.startTime.getTime();
|
||||||
if (params.error)
|
if (params.error)
|
||||||
step.error = params.error;
|
step.error = params.error;
|
||||||
steps.delete(params.stepId);
|
steps.delete(params.stepId);
|
||||||
this._reporter.onStepEnd(test, result, step);
|
this._reporter.onStepEnd?.(test, result, step);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onAttach(params: AttachmentPayload) {
|
private _onAttach(params: AttachmentPayload) {
|
||||||
|
|
@ -368,7 +368,7 @@ class JobDispatcher {
|
||||||
result = runData.result;
|
result = runData.result;
|
||||||
} else {
|
} else {
|
||||||
result = test._appendTestResult();
|
result = test._appendTestResult();
|
||||||
this._reporter.onTestBegin(test, result);
|
this._reporter.onTestBegin?.(test, result);
|
||||||
}
|
}
|
||||||
result.errors = [...errors];
|
result.errors = [...errors];
|
||||||
result.error = result.errors[0];
|
result.error = result.errors[0];
|
||||||
|
|
@ -392,7 +392,7 @@ class JobDispatcher {
|
||||||
// Let's just fail the test run.
|
// Let's just fail the test run.
|
||||||
this._failureTracker.onWorkerError();
|
this._failureTracker.onWorkerError();
|
||||||
for (const error of errors)
|
for (const error of errors)
|
||||||
this._reporter.onError(error);
|
this._reporter.onError?.(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -526,7 +526,7 @@ class JobDispatcher {
|
||||||
if (allTestsSkipped && !this._failureTracker.hasReachedMaxFailures()) {
|
if (allTestsSkipped && !this._failureTracker.hasReachedMaxFailures()) {
|
||||||
for (const test of this._job.tests) {
|
for (const test of this._job.tests) {
|
||||||
const result = test._appendTestResult();
|
const result = test._appendTestResult();
|
||||||
this._reporter.onTestBegin(test, result);
|
this._reporter.onTestBegin?.(test, result);
|
||||||
result.status = 'skipped';
|
result.status = 'skipped';
|
||||||
this._reportTestEnd(test, result);
|
this._reportTestEnd(test, result);
|
||||||
}
|
}
|
||||||
|
|
@ -540,13 +540,13 @@ class JobDispatcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
private _reportTestEnd(test: TestCase, result: TestResult) {
|
private _reportTestEnd(test: TestCase, result: TestResult) {
|
||||||
this._reporter.onTestEnd(test, result);
|
this._reporter.onTestEnd?.(test, result);
|
||||||
const hadMaxFailures = this._failureTracker.hasReachedMaxFailures();
|
const hadMaxFailures = this._failureTracker.hasReachedMaxFailures();
|
||||||
this._failureTracker.onTestEnd(test, result);
|
this._failureTracker.onTestEnd(test, result);
|
||||||
if (this._failureTracker.hasReachedMaxFailures()) {
|
if (this._failureTracker.hasReachedMaxFailures()) {
|
||||||
this._stopCallback();
|
this._stopCallback();
|
||||||
if (!hadMaxFailures)
|
if (!hadMaxFailures)
|
||||||
this._reporter.onError({ message: colors.red(`Testing stopped early after ${this._failureTracker.maxFailures()} maximum allowed failures.`) });
|
this._reporter.onError?.({ message: colors.red(`Testing stopped early after ${this._failureTracker.maxFailures()} maximum allowed failures.`) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
71
packages/playwright/src/runner/lastRun.ts
Normal file
71
packages/playwright/src/runner/lastRun.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
/**
|
||||||
|
* Copyright Microsoft Corporation. All rights reserved.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import type { FullResult, Suite } from '../../types/testReporter';
|
||||||
|
import { filterProjects } from './projectUtils';
|
||||||
|
import type { FullConfigInternal } from '../common/config';
|
||||||
|
import type { ReporterV2 } from '../reporters/reporterV2';
|
||||||
|
|
||||||
|
type LastRunInfo = {
|
||||||
|
status: FullResult['status'];
|
||||||
|
failedTests: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export class LastRunReporter implements ReporterV2 {
|
||||||
|
private _config: FullConfigInternal;
|
||||||
|
private _lastRunFile: string | undefined;
|
||||||
|
private _suite: Suite | undefined;
|
||||||
|
|
||||||
|
constructor(config: FullConfigInternal) {
|
||||||
|
this._config = config;
|
||||||
|
const [project] = filterProjects(config.projects, config.cliProjectFilter);
|
||||||
|
if (project)
|
||||||
|
this._lastRunFile = path.join(project.project.outputDir, '.last-run.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
async filterLastFailed() {
|
||||||
|
if (!this._lastRunFile)
|
||||||
|
return;
|
||||||
|
try {
|
||||||
|
const lastRunInfo = JSON.parse(await fs.promises.readFile(this._lastRunFile, 'utf8')) as LastRunInfo;
|
||||||
|
this._config.testIdMatcher = id => lastRunInfo.failedTests.includes(id);
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
version(): 'v2' {
|
||||||
|
return 'v2';
|
||||||
|
}
|
||||||
|
|
||||||
|
printsToStdio() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onBegin(suite: Suite) {
|
||||||
|
this._suite = suite;
|
||||||
|
}
|
||||||
|
|
||||||
|
async onEnd(result: FullResult) {
|
||||||
|
if (!this._lastRunFile || this._config.cliListOnly)
|
||||||
|
return;
|
||||||
|
await fs.promises.mkdir(path.dirname(this._lastRunFile), { recursive: true });
|
||||||
|
const failedTests = this._suite?.allTests().filter(t => !t.ok()).map(t => t.id);
|
||||||
|
const lastRunReport = JSON.stringify({ status: result.status, failedTests }, undefined, 2);
|
||||||
|
await fs.promises.writeFile(this._lastRunFile, lastRunReport);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -68,7 +68,7 @@ export async function createReporters(config: FullConfigInternal, mode: 'list' |
|
||||||
reporters.push(wrapReporterAsV2(new reporterConstructor(runOptions)));
|
reporters.push(wrapReporterAsV2(new reporterConstructor(runOptions)));
|
||||||
}
|
}
|
||||||
|
|
||||||
const someReporterPrintsToStdio = reporters.some(r => r.printsToStdio());
|
const someReporterPrintsToStdio = reporters.some(r => r.printsToStdio ? r.printsToStdio() : true);
|
||||||
if (reporters.length && !someReporterPrintsToStdio) {
|
if (reporters.length && !someReporterPrintsToStdio) {
|
||||||
// Add a line/dot/list-mode reporter for convenience.
|
// Add a line/dot/list-mode reporter for convenience.
|
||||||
// Important to put it first, just in case some other reporter stalls onEnd.
|
// Important to put it first, just in case some other reporter stalls onEnd.
|
||||||
|
|
@ -100,16 +100,15 @@ interface ErrorCollectingReporter extends ReporterV2 {
|
||||||
|
|
||||||
export function createErrorCollectingReporter(writeToConsole?: boolean): ErrorCollectingReporter {
|
export function createErrorCollectingReporter(writeToConsole?: boolean): ErrorCollectingReporter {
|
||||||
const errors: TestError[] = [];
|
const errors: TestError[] = [];
|
||||||
const reporterV2 = wrapReporterAsV2({
|
return {
|
||||||
|
version: () => 'v2',
|
||||||
onError(error: TestError) {
|
onError(error: TestError) {
|
||||||
errors.push(error);
|
errors.push(error);
|
||||||
if (writeToConsole)
|
if (writeToConsole)
|
||||||
process.stdout.write(formatError(error, colors.enabled).message + '\n');
|
process.stdout.write(formatError(error, colors.enabled).message + '\n');
|
||||||
}
|
},
|
||||||
});
|
errors: () => errors,
|
||||||
const reporter = reporterV2 as ErrorCollectingReporter;
|
};
|
||||||
reporter.errors = () => errors;
|
|
||||||
return reporter;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function reporterOptions(config: FullConfigInternal, mode: 'list' | 'test' | 'merge', isTestServer: boolean) {
|
function reporterOptions(config: FullConfigInternal, mode: 'list' | 'test' | 'merge', isTestServer: boolean) {
|
||||||
|
|
@ -140,14 +139,18 @@ function computeCommandHash(config: FullConfigInternal) {
|
||||||
return parts.join('-');
|
return parts.join('-');
|
||||||
}
|
}
|
||||||
|
|
||||||
class ListModeReporter extends EmptyReporter {
|
class ListModeReporter implements ReporterV2 {
|
||||||
private config!: FullConfig;
|
private config!: FullConfig;
|
||||||
|
|
||||||
override onConfigure(config: FullConfig) {
|
version(): 'v2' {
|
||||||
|
return 'v2';
|
||||||
|
}
|
||||||
|
|
||||||
|
onConfigure(config: FullConfig) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
override onBegin(suite: Suite): void {
|
onBegin(suite: Suite): void {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log(`Listing tests:`);
|
console.log(`Listing tests:`);
|
||||||
const tests = suite.allTests();
|
const tests = suite.allTests();
|
||||||
|
|
@ -165,12 +168,8 @@ class ListModeReporter extends EmptyReporter {
|
||||||
console.log(`Total: ${tests.length} ${tests.length === 1 ? 'test' : 'tests'} in ${files.size} ${files.size === 1 ? 'file' : 'files'}`);
|
console.log(`Total: ${tests.length} ${tests.length === 1 ? 'test' : 'tests'} in ${files.size} ${files.size === 1 ? 'file' : 'files'}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
override onError(error: TestError) {
|
onError(error: TestError) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error('\n' + formatError(error, false).message);
|
console.error('\n' + formatError(error, false).message);
|
||||||
}
|
}
|
||||||
|
|
||||||
override printsToStdio(): boolean {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,6 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
import { monotonicTime } from 'playwright-core/lib/utils';
|
import { monotonicTime } from 'playwright-core/lib/utils';
|
||||||
import type { FullResult, TestError } from '../../types/testReporter';
|
import type { FullResult, TestError } from '../../types/testReporter';
|
||||||
import { webServerPluginsForConfig } from '../plugins/webServerPlugin';
|
import { webServerPluginsForConfig } from '../plugins/webServerPlugin';
|
||||||
|
|
@ -26,6 +24,7 @@ import { TestRun, createTaskRunner, createTaskRunnerForClearCache, createTaskRun
|
||||||
import type { FullConfigInternal } from '../common/config';
|
import type { FullConfigInternal } from '../common/config';
|
||||||
import { affectedTestFiles } from '../transform/compilationCache';
|
import { affectedTestFiles } from '../transform/compilationCache';
|
||||||
import { InternalReporter } from '../reporters/internalReporter';
|
import { InternalReporter } from '../reporters/internalReporter';
|
||||||
|
import { LastRunReporter } from './lastRun';
|
||||||
|
|
||||||
type ProjectConfigWithFiles = {
|
type ProjectConfigWithFiles = {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -76,7 +75,11 @@ export class Runner {
|
||||||
webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p }));
|
webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p }));
|
||||||
|
|
||||||
const reporters = await createReporters(config, listOnly ? 'list' : 'test', false);
|
const reporters = await createReporters(config, listOnly ? 'list' : 'test', false);
|
||||||
const reporter = new InternalReporter(reporters);
|
const lastRun = new LastRunReporter(config);
|
||||||
|
if (config.cliLastFailed)
|
||||||
|
await lastRun.filterLastFailed();
|
||||||
|
|
||||||
|
const reporter = new InternalReporter([...reporters, lastRun]);
|
||||||
const taskRunner = listOnly ? createTaskRunnerForList(
|
const taskRunner = listOnly ? createTaskRunnerForList(
|
||||||
config,
|
config,
|
||||||
reporter,
|
reporter,
|
||||||
|
|
@ -140,22 +143,3 @@ export class Runner {
|
||||||
return { status };
|
return { status };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LastRunInfo = {
|
|
||||||
status: FullResult['status'];
|
|
||||||
failedTests: string[];
|
|
||||||
testDurations?: { [testId: string]: number };
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function readLastRunInfo(config: FullConfigInternal): Promise<LastRunInfo> {
|
|
||||||
const [project] = filterProjects(config.projects, config.cliProjectFilter);
|
|
||||||
if (!project)
|
|
||||||
return { status: 'passed', failedTests: [] };
|
|
||||||
const outputDir = project.project.outputDir;
|
|
||||||
try {
|
|
||||||
const lastRunReportFile = config.lastRunFile || path.join(outputDir, '.last-run.json');
|
|
||||||
return JSON.parse(await fs.promises.readFile(lastRunReportFile, 'utf8')) as LastRunInfo;
|
|
||||||
} catch {
|
|
||||||
}
|
|
||||||
return { status: 'passed', failedTests: [] };
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -159,7 +159,7 @@ export function createTaskRunnerForClearCache(config: FullConfigInternal, report
|
||||||
function createReportBeginTask(): Task<TestRun> {
|
function createReportBeginTask(): Task<TestRun> {
|
||||||
return {
|
return {
|
||||||
setup: async (reporter, { rootSuite }) => {
|
setup: async (reporter, { rootSuite }) => {
|
||||||
reporter.onBegin(rootSuite!);
|
reporter.onBegin?.(rootSuite!);
|
||||||
},
|
},
|
||||||
teardown: async ({}) => {},
|
teardown: async ({}) => {},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
2
packages/playwright/types/test.d.ts
vendored
2
packages/playwright/types/test.d.ts
vendored
|
|
@ -8224,7 +8224,7 @@ export interface TestInfo {
|
||||||
title: string;
|
title: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The full title path starting with the project.
|
* The full title path starting with the test file name.
|
||||||
*/
|
*/
|
||||||
titlePath: Array<string>;
|
titlePath: Array<string>;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ export type PageEntry = {
|
||||||
screencastFrames: {
|
screencastFrames: {
|
||||||
sha1: string,
|
sha1: string,
|
||||||
timestamp: number,
|
timestamp: number,
|
||||||
|
frameSwapWallTime?: number,
|
||||||
width: number,
|
width: number,
|
||||||
height: number,
|
height: number,
|
||||||
}[];
|
}[];
|
||||||
|
|
|
||||||
|
|
@ -346,6 +346,8 @@ function snapshotScript(...targetIds: (string | undefined)[]) {
|
||||||
if (search.get('pointX') && search.get('pointY')) {
|
if (search.get('pointX') && search.get('pointY')) {
|
||||||
const pointX = +search.get('pointX')!;
|
const pointX = +search.get('pointX')!;
|
||||||
const pointY = +search.get('pointY')!;
|
const pointY = +search.get('pointY')!;
|
||||||
|
const hasInputTarget = search.has('hasInputTarget');
|
||||||
|
const isTopFrame = window.location.pathname.match(/\/page@[a-z0-9]+$/);
|
||||||
const hasTargetElements = targetElements.length > 0;
|
const hasTargetElements = targetElements.length > 0;
|
||||||
const roots = document.documentElement ? [document.documentElement] : [];
|
const roots = document.documentElement ? [document.documentElement] : [];
|
||||||
for (const target of (hasTargetElements ? targetElements : roots)) {
|
for (const target of (hasTargetElements ? targetElements : roots)) {
|
||||||
|
|
@ -370,7 +372,8 @@ function snapshotScript(...targetIds: (string | undefined)[]) {
|
||||||
pointElement.style.left = centerX + 'px';
|
pointElement.style.left = centerX + 'px';
|
||||||
pointElement.style.top = centerY + 'px';
|
pointElement.style.top = centerY + 'px';
|
||||||
// "Warning symbol" indicates that action point is not 100% correct.
|
// "Warning symbol" indicates that action point is not 100% correct.
|
||||||
if (Math.abs(centerX - pointX) >= 10 || Math.abs(centerY - pointY) >= 10) {
|
// Note that action point is relative to the top frame, so we can only compare in the top frame.
|
||||||
|
if (isTopFrame && (Math.abs(centerX - pointX) >= 10 || Math.abs(centerY - pointY) >= 10)) {
|
||||||
const warningElement = document.createElement('x-pw-pointer-warning');
|
const warningElement = document.createElement('x-pw-pointer-warning');
|
||||||
warningElement.textContent = '⚠';
|
warningElement.textContent = '⚠';
|
||||||
warningElement.style.fontSize = '19px';
|
warningElement.style.fontSize = '19px';
|
||||||
|
|
@ -380,13 +383,14 @@ function snapshotScript(...targetIds: (string | undefined)[]) {
|
||||||
pointElement.appendChild(warningElement);
|
pointElement.appendChild(warningElement);
|
||||||
pointElement.setAttribute('title', kPointerWarningTitle);
|
pointElement.setAttribute('title', kPointerWarningTitle);
|
||||||
}
|
}
|
||||||
} else {
|
document.documentElement.appendChild(pointElement);
|
||||||
|
} else if (isTopFrame && !hasInputTarget) {
|
||||||
// For actions without a target element, e.g. page.mouse.move(),
|
// For actions without a target element, e.g. page.mouse.move(),
|
||||||
// show the point at the recorder location.
|
// show the point at the recorded location, which is relative to the top frame.
|
||||||
pointElement.style.left = pointX + 'px';
|
pointElement.style.left = pointX + 'px';
|
||||||
pointElement.style.top = pointY + 'px';
|
pointElement.style.top = pointY + 'px';
|
||||||
|
document.documentElement.appendChild(pointElement);
|
||||||
}
|
}
|
||||||
document.documentElement.appendChild(pointElement);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ export class SnapshotServer {
|
||||||
viewport: snapshot.viewport(),
|
viewport: snapshot.viewport(),
|
||||||
url: snapshot.snapshot().frameUrl,
|
url: snapshot.snapshot().frameUrl,
|
||||||
timestamp: snapshot.snapshot().timestamp,
|
timestamp: snapshot.snapshot().timestamp,
|
||||||
|
wallTime: snapshot.snapshot().wallTime,
|
||||||
} : {
|
} : {
|
||||||
error: 'No snapshot found'
|
error: 'No snapshot found'
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -31,12 +31,12 @@ import { TabbedPaneTab } from '@web/components/tabbedPane';
|
||||||
import { BrowserFrame } from './browserFrame';
|
import { BrowserFrame } from './browserFrame';
|
||||||
import { ClickPointer } from './clickPointer';
|
import { ClickPointer } from './clickPointer';
|
||||||
|
|
||||||
function findClosest<T extends { timestamp: number }>(items: T[], target: number) {
|
function findClosest<T>(items: T[], metric: (v: T) => number, target: number) {
|
||||||
return items.find((item, index) => {
|
return items.find((item, index) => {
|
||||||
if (index === items.length - 1)
|
if (index === items.length - 1)
|
||||||
return true;
|
return true;
|
||||||
const next = items[index + 1];
|
const next = items[index + 1];
|
||||||
return Math.abs(item.timestamp - target) < Math.abs(next.timestamp - target);
|
return Math.abs(metric(item) - target) < Math.abs(metric(next) - target);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -55,7 +55,7 @@ export const SnapshotTab: React.FunctionComponent<{
|
||||||
const [snapshotTab, setSnapshotTab] = React.useState<'action'|'before'|'after'>('action');
|
const [snapshotTab, setSnapshotTab] = React.useState<'action'|'before'|'after'>('action');
|
||||||
const [showScreenshotInsteadOfSnapshot] = useSetting('screenshot-instead-of-snapshot', false);
|
const [showScreenshotInsteadOfSnapshot] = useSetting('screenshot-instead-of-snapshot', false);
|
||||||
|
|
||||||
type Snapshot = { action: ActionTraceEvent, snapshotName: string, point?: { x: number, y: number } };
|
type Snapshot = { action: ActionTraceEvent, snapshotName: string, point?: { x: number, y: number }, hasInputTarget?: boolean };
|
||||||
const { snapshots } = React.useMemo(() => {
|
const { snapshots } = React.useMemo(() => {
|
||||||
if (!action)
|
if (!action)
|
||||||
return { snapshots: {} };
|
return { snapshots: {} };
|
||||||
|
|
@ -68,7 +68,7 @@ export const SnapshotTab: React.FunctionComponent<{
|
||||||
beforeSnapshot = a?.afterSnapshot ? { action: a, snapshotName: a?.afterSnapshot } : undefined;
|
beforeSnapshot = a?.afterSnapshot ? { action: a, snapshotName: a?.afterSnapshot } : undefined;
|
||||||
}
|
}
|
||||||
const afterSnapshot: Snapshot | undefined = action.afterSnapshot ? { action, snapshotName: action.afterSnapshot } : beforeSnapshot;
|
const afterSnapshot: Snapshot | undefined = action.afterSnapshot ? { action, snapshotName: action.afterSnapshot } : beforeSnapshot;
|
||||||
const actionSnapshot: Snapshot | undefined = action.inputSnapshot ? { action, snapshotName: action.inputSnapshot } : afterSnapshot;
|
const actionSnapshot: Snapshot | undefined = action.inputSnapshot ? { action, snapshotName: action.inputSnapshot, hasInputTarget: true } : afterSnapshot;
|
||||||
if (actionSnapshot)
|
if (actionSnapshot)
|
||||||
actionSnapshot.point = action.point;
|
actionSnapshot.point = action.point;
|
||||||
return { snapshots: { action: actionSnapshot, before: beforeSnapshot, after: afterSnapshot } };
|
return { snapshots: { action: actionSnapshot, before: beforeSnapshot, after: afterSnapshot } };
|
||||||
|
|
@ -85,6 +85,8 @@ export const SnapshotTab: React.FunctionComponent<{
|
||||||
if (snapshot.point) {
|
if (snapshot.point) {
|
||||||
params.set('pointX', String(snapshot.point.x));
|
params.set('pointX', String(snapshot.point.x));
|
||||||
params.set('pointY', String(snapshot.point.y));
|
params.set('pointY', String(snapshot.point.y));
|
||||||
|
if (snapshot.hasInputTarget)
|
||||||
|
params.set('hasInputTarget', '1');
|
||||||
}
|
}
|
||||||
const snapshotUrl = new URL(`snapshot/${snapshot.action.pageId}?${params.toString()}`, window.location.href).toString();
|
const snapshotUrl = new URL(`snapshot/${snapshot.action.pageId}?${params.toString()}`, window.location.href).toString();
|
||||||
const snapshotInfoUrl = new URL(`snapshotInfo/${snapshot.action.pageId}?${params.toString()}`, window.location.href).toString();
|
const snapshotInfoUrl = new URL(`snapshotInfo/${snapshot.action.pageId}?${params.toString()}`, window.location.href).toString();
|
||||||
|
|
@ -95,6 +97,8 @@ export const SnapshotTab: React.FunctionComponent<{
|
||||||
if (snapshot.point) {
|
if (snapshot.point) {
|
||||||
popoutParams.set('pointX', String(snapshot.point.x));
|
popoutParams.set('pointX', String(snapshot.point.x));
|
||||||
popoutParams.set('pointY', String(snapshot.point.y));
|
popoutParams.set('pointY', String(snapshot.point.y));
|
||||||
|
if (snapshot.hasInputTarget)
|
||||||
|
params.set('hasInputTarget', '1');
|
||||||
}
|
}
|
||||||
const popoutUrl = new URL(`snapshot.html?${popoutParams.toString()}`, window.location.href).toString();
|
const popoutUrl = new URL(`snapshot.html?${popoutParams.toString()}`, window.location.href).toString();
|
||||||
return { snapshots, snapshotInfoUrl, snapshotUrl, popoutUrl, point: snapshot.point };
|
return { snapshots, snapshotInfoUrl, snapshotUrl, popoutUrl, point: snapshot.point };
|
||||||
|
|
@ -102,7 +106,7 @@ export const SnapshotTab: React.FunctionComponent<{
|
||||||
|
|
||||||
const iframeRef0 = React.useRef<HTMLIFrameElement>(null);
|
const iframeRef0 = React.useRef<HTMLIFrameElement>(null);
|
||||||
const iframeRef1 = React.useRef<HTMLIFrameElement>(null);
|
const iframeRef1 = React.useRef<HTMLIFrameElement>(null);
|
||||||
const [snapshotInfo, setSnapshotInfo] = React.useState<{ viewport: typeof kDefaultViewport, url: string, timestamp?: number }>({ viewport: kDefaultViewport, url: '', timestamp: undefined });
|
const [snapshotInfo, setSnapshotInfo] = React.useState<{ viewport: typeof kDefaultViewport, url: string, timestamp?: number, wallTime?: undefined }>({ viewport: kDefaultViewport, url: '' });
|
||||||
const loadingRef = React.useRef({ iteration: 0, visibleIframe: 0 });
|
const loadingRef = React.useRef({ iteration: 0, visibleIframe: 0 });
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
|
@ -111,7 +115,7 @@ export const SnapshotTab: React.FunctionComponent<{
|
||||||
const newVisibleIframe = 1 - loadingRef.current.visibleIframe;
|
const newVisibleIframe = 1 - loadingRef.current.visibleIframe;
|
||||||
loadingRef.current.iteration = thisIteration;
|
loadingRef.current.iteration = thisIteration;
|
||||||
|
|
||||||
const newSnapshotInfo = { url: '', viewport: kDefaultViewport, timestamp: undefined };
|
const newSnapshotInfo = { url: '', viewport: kDefaultViewport, timestamp: undefined, wallTime: undefined };
|
||||||
if (snapshotInfoUrl) {
|
if (snapshotInfoUrl) {
|
||||||
const response = await fetch(snapshotInfoUrl);
|
const response = await fetch(snapshotInfoUrl);
|
||||||
const info = await response.json();
|
const info = await response.json();
|
||||||
|
|
@ -119,6 +123,7 @@ export const SnapshotTab: React.FunctionComponent<{
|
||||||
newSnapshotInfo.url = info.url;
|
newSnapshotInfo.url = info.url;
|
||||||
newSnapshotInfo.viewport = info.viewport;
|
newSnapshotInfo.viewport = info.viewport;
|
||||||
newSnapshotInfo.timestamp = info.timestamp;
|
newSnapshotInfo.timestamp = info.timestamp;
|
||||||
|
newSnapshotInfo.wallTime = info.wallTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -170,10 +175,13 @@ export const SnapshotTab: React.FunctionComponent<{
|
||||||
const page = action ? pageForAction(action) : undefined;
|
const page = action ? pageForAction(action) : undefined;
|
||||||
const screencastFrame = React.useMemo(
|
const screencastFrame = React.useMemo(
|
||||||
() => {
|
() => {
|
||||||
|
if (snapshotInfo.wallTime && page?.screencastFrames[0]?.frameSwapWallTime)
|
||||||
|
return findClosest(page.screencastFrames, frame => frame.frameSwapWallTime!, snapshotInfo.wallTime);
|
||||||
|
|
||||||
if (snapshotInfo.timestamp && page?.screencastFrames)
|
if (snapshotInfo.timestamp && page?.screencastFrames)
|
||||||
return findClosest(page.screencastFrames, snapshotInfo.timestamp);
|
return findClosest(page.screencastFrames, frame => frame.timestamp, snapshotInfo.timestamp);
|
||||||
},
|
},
|
||||||
[page?.screencastFrames, snapshotInfo.timestamp]
|
[page?.screencastFrames, snapshotInfo.timestamp, snapshotInfo.wallTime]
|
||||||
);
|
);
|
||||||
|
|
||||||
return <div
|
return <div
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ export const TraceView: React.FC<{
|
||||||
if (pollTimer.current)
|
if (pollTimer.current)
|
||||||
clearTimeout(pollTimer.current);
|
clearTimeout(pollTimer.current);
|
||||||
};
|
};
|
||||||
}, [outputDir, item, setModel, counter, setCounter]);
|
}, [outputDir, item, setModel, counter, setCounter, pathSeparator]);
|
||||||
|
|
||||||
return <Workbench
|
return <Workbench
|
||||||
key='workbench'
|
key='workbench'
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ export type FrameSnapshot = {
|
||||||
frameId: string,
|
frameId: string,
|
||||||
frameUrl: string,
|
frameUrl: string,
|
||||||
timestamp: number,
|
timestamp: number,
|
||||||
|
wallTime?: number,
|
||||||
collectionTime: number,
|
collectionTime: number,
|
||||||
doctype?: string,
|
doctype?: string,
|
||||||
html: NodeSnapshot,
|
html: NodeSnapshot,
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ export type ScreencastFrameTraceEvent = {
|
||||||
width: number,
|
width: number,
|
||||||
height: number,
|
height: number,
|
||||||
timestamp: number,
|
timestamp: number,
|
||||||
|
frameSwapWallTime?: number,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BeforeActionTraceEvent = {
|
export type BeforeActionTraceEvent = {
|
||||||
|
|
|
||||||
76
tests/bidi/expectationReporter.ts
Normal file
76
tests/bidi/expectationReporter.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {
|
||||||
|
FullConfig, FullResult, Reporter, Suite, TestCase
|
||||||
|
} from '@playwright/test/reporter';
|
||||||
|
import fs from 'fs';
|
||||||
|
import { projectExpectationPath } from './expectationUtil';
|
||||||
|
|
||||||
|
type ReporterOptions = {
|
||||||
|
rebase?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ExpectationReporter implements Reporter {
|
||||||
|
private _suite: Suite;
|
||||||
|
private _options: ReporterOptions;
|
||||||
|
|
||||||
|
constructor(options: ReporterOptions) {
|
||||||
|
this._options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
onBegin(config: FullConfig, suite: Suite) {
|
||||||
|
this._suite = suite;
|
||||||
|
}
|
||||||
|
|
||||||
|
onEnd(result: FullResult) {
|
||||||
|
if (!this._options.rebase)
|
||||||
|
return;
|
||||||
|
for (const project of this._suite.suites)
|
||||||
|
this._updateProjectExpectations(project);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _updateProjectExpectations(project: Suite) {
|
||||||
|
const results = project.allTests().map(test => {
|
||||||
|
const outcome = getOutcome(test);
|
||||||
|
const line = `${test.titlePath().slice(1).join(' › ')} [${outcome}]`;
|
||||||
|
return line;
|
||||||
|
});
|
||||||
|
const outputFile = projectExpectationPath(project.title);
|
||||||
|
console.log('Writing new expectations to', outputFile);
|
||||||
|
fs.writeFileSync(outputFile, results.join('\n'));
|
||||||
|
}
|
||||||
|
|
||||||
|
printsToStdio(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOutcome(test: TestCase): 'unknown' | 'flaky' | 'pass' | 'fail' | 'timeout' {
|
||||||
|
if (test.results.length === 0)
|
||||||
|
return 'unknown';
|
||||||
|
if (test.results.every(r => r.status === 'timedOut'))
|
||||||
|
return 'timeout';
|
||||||
|
if (test.outcome() === 'expected')
|
||||||
|
return 'pass';
|
||||||
|
if (test.outcome() === 'unexpected')
|
||||||
|
return 'fail';
|
||||||
|
if (test.outcome() === 'flaky')
|
||||||
|
return 'flaky';
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ExpectationReporter;
|
||||||
50
tests/bidi/expectationUtil.ts
Normal file
50
tests/bidi/expectationUtil.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import type { TestInfo } from 'playwright/test';
|
||||||
|
|
||||||
|
type ShouldSkipPredicate = (info: TestInfo) => boolean;
|
||||||
|
|
||||||
|
export async function parseBidiExpectations(projectName: string): Promise<ShouldSkipPredicate> {
|
||||||
|
const filePath = projectExpectationPath(projectName);
|
||||||
|
try {
|
||||||
|
await fs.promises.access(filePath);
|
||||||
|
} catch (e) {
|
||||||
|
return () => false;
|
||||||
|
}
|
||||||
|
const content = await fs.promises.readFile(filePath);
|
||||||
|
const pairs = content.toString().split('\n').map(line => {
|
||||||
|
const match = /(?<titlePath>.+) \[(?<expectation>[^\]]+)\]$/.exec(line);
|
||||||
|
if (!match) {
|
||||||
|
console.error('Bad expectation line: ' + line);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return [match.groups!.titlePath, match.groups!.expectation];
|
||||||
|
}).filter(Boolean) as [string, string][];
|
||||||
|
const expectationsMap = new Map(pairs);
|
||||||
|
|
||||||
|
return (info: TestInfo) => {
|
||||||
|
const key = [info.project.name, ...info.titlePath].join(' › ');
|
||||||
|
const expectation = expectationsMap.get(key);
|
||||||
|
return expectation === 'fail' || expectation === 'timeout';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function projectExpectationPath(project: string): string {
|
||||||
|
return path.join(__dirname, 'expectations', project + '.txt');
|
||||||
|
}
|
||||||
1910
tests/bidi/expectations/bidi-chromium-library.txt
Normal file
1910
tests/bidi/expectations/bidi-chromium-library.txt
Normal file
File diff suppressed because it is too large
Load diff
1966
tests/bidi/expectations/bidi-chromium-page.txt
Normal file
1966
tests/bidi/expectations/bidi-chromium-page.txt
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -36,7 +36,8 @@ const reporters = () => {
|
||||||
['json', { outputFile: path.join(outputDir, 'report.json') }],
|
['json', { outputFile: path.join(outputDir, 'report.json') }],
|
||||||
['blob', { fileName: `${process.env.PWTEST_BOT_NAME}.zip` }],
|
['blob', { fileName: `${process.env.PWTEST_BOT_NAME}.zip` }],
|
||||||
] : [
|
] : [
|
||||||
['html', { open: 'on-failure' }]
|
['html', { open: 'on-failure' }],
|
||||||
|
['./expectationReporter', { rebase: false }],
|
||||||
];
|
];
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { test, expect } from '@playwright/experimental-ct-react';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import EmptyFragment from '@/components/EmptyFragment';
|
import EmptyFragment from '@/components/EmptyFragment';
|
||||||
import { ComponentAsProp } from '@/components/ComponentAsProp';
|
import { ComponentAsProp } from '@/components/ComponentAsProp';
|
||||||
|
import DefaultChildren from '@/components/DefaultChildren';
|
||||||
|
|
||||||
test('render props', async ({ mount }) => {
|
test('render props', async ({ mount }) => {
|
||||||
const component = await mount(<Button title="Submit" />);
|
const component = await mount(<Button title="Submit" />);
|
||||||
|
|
@ -31,3 +32,17 @@ test('render an empty component', async ({ mount, page }) => {
|
||||||
expect(await component.textContent()).toBe('');
|
expect(await component.textContent()).toBe('');
|
||||||
await expect(component).toHaveText('');
|
await expect(component).toHaveText('');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function MyInlineComponent({ value }: { value: string }) {
|
||||||
|
return <>Hello {value}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('render inline component with an error', async ({ mount }) => {
|
||||||
|
await expect(mount(<MyInlineComponent value="Max" />)).rejects.toThrow('Component "MyInlineComponent" cannot be mounted.');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('render inline component with an error if its nested', async ({ mount }) => {
|
||||||
|
await expect(mount(<DefaultChildren>
|
||||||
|
<MyInlineComponent value="Max" />
|
||||||
|
</DefaultChildren>)).rejects.toThrow('Component "MyInlineComponent" cannot be mounted.');
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ import { baseTest } from './baseTest';
|
||||||
import { type RemoteServerOptions, type PlaywrightServer, RunServer, RemoteServer } from './remoteServer';
|
import { type RemoteServerOptions, type PlaywrightServer, RunServer, RemoteServer } from './remoteServer';
|
||||||
import type { Log } from '../../packages/trace/src/har';
|
import type { Log } from '../../packages/trace/src/har';
|
||||||
import { parseHar } from '../config/utils';
|
import { parseHar } from '../config/utils';
|
||||||
|
import { parseBidiExpectations as parseBidiProjectExpectations } from '../bidi/expectationUtil';
|
||||||
|
import type { TestInfo } from '@playwright/test';
|
||||||
|
|
||||||
export type BrowserTestWorkerFixtures = PageWorkerFixtures & {
|
export type BrowserTestWorkerFixtures = PageWorkerFixtures & {
|
||||||
browserVersion: string;
|
browserVersion: string;
|
||||||
|
|
@ -33,6 +35,7 @@ export type BrowserTestWorkerFixtures = PageWorkerFixtures & {
|
||||||
browserType: BrowserType;
|
browserType: BrowserType;
|
||||||
isAndroid: boolean;
|
isAndroid: boolean;
|
||||||
isElectron: boolean;
|
isElectron: boolean;
|
||||||
|
bidiTestSkipPredicate: (info: TestInfo) => boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface StartRemoteServer {
|
interface StartRemoteServer {
|
||||||
|
|
@ -46,6 +49,7 @@ type BrowserTestTestFixtures = PageTestFixtures & {
|
||||||
startRemoteServer: StartRemoteServer;
|
startRemoteServer: StartRemoteServer;
|
||||||
contextFactory: (options?: BrowserContextOptions) => Promise<BrowserContext>;
|
contextFactory: (options?: BrowserContextOptions) => Promise<BrowserContext>;
|
||||||
pageWithHar(options?: { outputPath?: string, content?: 'embed' | 'attach' | 'omit', omitContent?: boolean }): Promise<{ context: BrowserContext, page: Page, getLog: () => Promise<Log>, getZip: () => Promise<Map<string, Buffer>> }>
|
pageWithHar(options?: { outputPath?: string, content?: 'embed' | 'attach' | 'omit', omitContent?: boolean }): Promise<{ context: BrowserContext, page: Page, getLog: () => Promise<Log>, getZip: () => Promise<Map<string, Buffer>> }>
|
||||||
|
autoSkipBidiTest: void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const test = baseTest.extend<BrowserTestTestFixtures, BrowserTestWorkerFixtures>({
|
const test = baseTest.extend<BrowserTestTestFixtures, BrowserTestWorkerFixtures>({
|
||||||
|
|
@ -165,7 +169,18 @@ const test = baseTest.extend<BrowserTestTestFixtures, BrowserTestWorkerFixtures>
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
await use(pageWithHar);
|
await use(pageWithHar);
|
||||||
}
|
},
|
||||||
|
|
||||||
|
bidiTestSkipPredicate: [async ({ }, run) => {
|
||||||
|
const filter = await parseBidiProjectExpectations(test.info().project.name);
|
||||||
|
await run(filter);
|
||||||
|
}, { scope: 'worker' }],
|
||||||
|
|
||||||
|
autoSkipBidiTest: [async ({ bidiTestSkipPredicate }, run) => {
|
||||||
|
if (bidiTestSkipPredicate(test.info()))
|
||||||
|
test.skip(true);
|
||||||
|
await run();
|
||||||
|
}, { auto: true, scope: 'test' }],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const playwrightTest = test;
|
export const playwrightTest = test;
|
||||||
|
|
|
||||||
|
|
@ -1496,3 +1496,28 @@ test('should handle case where neither snapshots nor screenshots exist', async (
|
||||||
await expect(screenshot).not.toBeVisible();
|
await expect(screenshot).not.toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should show only one pointer with multilevel iframes', async ({ page, runAndTrace, server, browserName }) => {
|
||||||
|
test.fixme(browserName !== 'chromium', 'Elements in iframe are not marked');
|
||||||
|
|
||||||
|
server.setRoute('/level-0.html', (req, res) => {
|
||||||
|
res.writeHead(200);
|
||||||
|
res.end(`<iframe src="/level-1.html" style="position: absolute; left: 100px"></iframe>`);
|
||||||
|
});
|
||||||
|
server.setRoute('/level-1.html', (req, res) => {
|
||||||
|
res.writeHead(200);
|
||||||
|
res.end(`<iframe src="/level-2.html"></iframe>`);
|
||||||
|
});
|
||||||
|
server.setRoute('/level-2.html', (req, res) => {
|
||||||
|
res.writeHead(200);
|
||||||
|
res.end(`<button>Click me</button>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
const traceViewer = await runAndTrace(async () => {
|
||||||
|
await page.goto(server.PREFIX + '/level-0.html');
|
||||||
|
await page.frameLocator('iframe').frameLocator('iframe').locator('button').click({ position: { x: 5, y: 5 } });
|
||||||
|
});
|
||||||
|
const snapshotFrame = await traceViewer.snapshotFrame('locator.click');
|
||||||
|
await expect.soft(snapshotFrame.locator('x-pw-pointer')).not.toBeAttached();
|
||||||
|
await expect.soft(snapshotFrame.frameLocator('iframe').locator('x-pw-pointer')).not.toBeAttached();
|
||||||
|
await expect.soft(snapshotFrame.frameLocator('iframe').frameLocator('iframe').locator('x-pw-pointer')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -300,3 +300,12 @@ it('should work with busted Array.prototype.map/push', async ({ page, server })
|
||||||
await page.exposeFunction('add', (a, b) => a + b);
|
await page.exposeFunction('add', (a, b) => a + b);
|
||||||
expect(await page.evaluate('add(5, 6)')).toBe(11);
|
expect(await page.evaluate('add(5, 6)')).toBe(11);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should fail with busted Array.prototype.toJSON', async ({ page }) => {
|
||||||
|
await page.evaluateHandle(() => (Array.prototype as any).toJSON = () => '"[]"');
|
||||||
|
|
||||||
|
await page.exposeFunction('add', (a, b) => a + b);
|
||||||
|
await expect(() => page.evaluate(`add(5, 6)`)).rejects.toThrowError('serializedArgs is not an array. This can happen when Array.prototype.toJSON is defined incorrectly');
|
||||||
|
|
||||||
|
expect.soft(await page.evaluate(() => ([] as any).toJSON())).toBe('"[]"');
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue