Merge branch 'main' into feat-extend-custom-asymmetric-matchers
This commit is contained in:
commit
4fc4b76b6c
6
.github/workflows/tests_bidi.yml
vendored
6
.github/workflows/tests_bidi.yml
vendored
|
|
@ -26,7 +26,7 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
channel: [bidi-chromium, bidi-firefox-beta]
|
channel: [bidi-chromium, bidi-firefox-nightly]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
|
|
@ -38,8 +38,8 @@ jobs:
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- run: npx playwright install --with-deps chromium
|
- run: npx playwright install --with-deps chromium
|
||||||
if: matrix.channel == 'bidi-chromium'
|
if: matrix.channel == 'bidi-chromium'
|
||||||
- run: npx -y @puppeteer/browsers install firefox@beta
|
- run: npx -y @puppeteer/browsers install firefox@nightly
|
||||||
if: matrix.channel == 'bidi-firefox-beta'
|
if: matrix.channel == 'bidi-firefox-nightly'
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run biditest -- --project=${{ matrix.channel }}*
|
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run biditest -- --project=${{ matrix.channel }}*
|
||||||
env:
|
env:
|
||||||
|
|
|
||||||
|
|
@ -397,7 +397,7 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro
|
||||||
process.stdout.write('\n-------------8<-------------\n');
|
process.stdout.write('\n-------------8<-------------\n');
|
||||||
const autoExitCondition = process.env.PWTEST_CLI_AUTO_EXIT_WHEN;
|
const autoExitCondition = process.env.PWTEST_CLI_AUTO_EXIT_WHEN;
|
||||||
if (autoExitCondition && text.includes(autoExitCondition))
|
if (autoExitCondition && text.includes(autoExitCondition))
|
||||||
Promise.all(context.pages().map(async p => p.close()));
|
closeBrowser();
|
||||||
};
|
};
|
||||||
// Make sure we exit abnormally when browser crashes.
|
// Make sure we exit abnormally when browser crashes.
|
||||||
const logs: string[] = [];
|
const logs: string[] = [];
|
||||||
|
|
@ -504,7 +504,7 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro
|
||||||
if (hasPage)
|
if (hasPage)
|
||||||
return;
|
return;
|
||||||
// Avoid the error when the last page is closed because the browser has been closed.
|
// Avoid the error when the last page is closed because the browser has been closed.
|
||||||
closeBrowser().catch(e => null);
|
closeBrowser().catch(() => {});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
process.on('SIGINT', async () => {
|
process.on('SIGINT', async () => {
|
||||||
|
|
@ -560,7 +560,7 @@ async function open(options: Options, url: string | undefined, language: string)
|
||||||
|
|
||||||
async function codegen(options: Options & { target: string, output?: string, testIdAttribute?: string }, url: string | undefined) {
|
async function codegen(options: Options & { target: string, output?: string, testIdAttribute?: string }, url: string | undefined) {
|
||||||
const { target: language, output: outputFile, testIdAttribute: testIdAttributeName } = options;
|
const { target: language, output: outputFile, testIdAttribute: testIdAttributeName } = options;
|
||||||
const tracesDir = path.join(os.tmpdir(), `recorder-trace-${Date.now()}`);
|
const tracesDir = path.join(os.tmpdir(), `playwright-recorder-trace-${Date.now()}`);
|
||||||
const { context, launchOptions, contextOptions } = await launchContext(options, {
|
const { context, launchOptions, contextOptions } = await launchContext(options, {
|
||||||
headless: !!process.env.PWTEST_CLI_HEADLESS,
|
headless: !!process.env.PWTEST_CLI_HEADLESS,
|
||||||
executablePath: process.env.PWTEST_CLI_EXECUTABLE_PATH,
|
executablePath: process.env.PWTEST_CLI_EXECUTABLE_PATH,
|
||||||
|
|
@ -574,6 +574,7 @@ async function codegen(options: Options & { target: string, output?: string, tes
|
||||||
device: options.device,
|
device: options.device,
|
||||||
saveStorage: options.saveStorage,
|
saveStorage: options.saveStorage,
|
||||||
mode: 'recording',
|
mode: 'recording',
|
||||||
|
codegenMode: process.env.PW_RECORDER_IS_TRACE_VIEWER ? 'trace-events' : 'actions',
|
||||||
testIdAttributeName,
|
testIdAttributeName,
|
||||||
outputFile: outputFile ? path.resolve(outputFile) : undefined,
|
outputFile: outputFile ? path.resolve(outputFile) : undefined,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -220,7 +220,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
||||||
}
|
}
|
||||||
// If the page is closed or unrouteAll() was called without waiting and interception disabled,
|
// If the page is closed or unrouteAll() was called without waiting and interception disabled,
|
||||||
// the method will throw an error - silence it.
|
// the method will throw an error - silence it.
|
||||||
await route._innerContinue(true).catch(() => {});
|
await route._innerContinue(true /* isFallback */).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
async _onWebSocketRoute(webSocketRoute: network.WebSocketRoute) {
|
async _onWebSocketRoute(webSocketRoute: network.WebSocketRoute) {
|
||||||
|
|
@ -492,17 +492,8 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
||||||
await this._closedPromise;
|
await this._closedPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _enableRecorder(params: {
|
async _enableRecorder(params: channels.BrowserContextEnableRecorderParams) {
|
||||||
language: string,
|
await this._channel.enableRecorder(params);
|
||||||
launchOptions?: LaunchOptions,
|
|
||||||
contextOptions?: BrowserContextOptions,
|
|
||||||
device?: string,
|
|
||||||
saveStorage?: string,
|
|
||||||
mode?: 'recording' | 'inspecting',
|
|
||||||
testIdAttributeName?: string,
|
|
||||||
outputFile?: string,
|
|
||||||
}) {
|
|
||||||
await this._channel.recorderSupplementEnable(params);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
|
||||||
_logger: Logger | undefined;
|
_logger: Logger | undefined;
|
||||||
readonly _instrumentation: ClientInstrumentation;
|
readonly _instrumentation: ClientInstrumentation;
|
||||||
private _eventToSubscriptionMapping: Map<string, string> = new Map();
|
private _eventToSubscriptionMapping: Map<string, string> = new Map();
|
||||||
|
private _isInternalType = false;
|
||||||
_wasCollected: boolean = false;
|
_wasCollected: boolean = false;
|
||||||
|
|
||||||
constructor(parent: ChannelOwner | Connection, type: string, guid: string, initializer: channels.InitializerTraits<T>) {
|
constructor(parent: ChannelOwner | Connection, type: string, guid: string, initializer: channels.InitializerTraits<T>) {
|
||||||
|
|
@ -61,6 +62,10 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
|
||||||
this._initializer = initializer;
|
this._initializer = initializer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected markAsInternalType() {
|
||||||
|
this._isInternalType = true;
|
||||||
|
}
|
||||||
|
|
||||||
_setEventToSubscriptionMapping(mapping: Map<string, string>) {
|
_setEventToSubscriptionMapping(mapping: Map<string, string>) {
|
||||||
this._eventToSubscriptionMapping = mapping;
|
this._eventToSubscriptionMapping = mapping;
|
||||||
}
|
}
|
||||||
|
|
@ -173,7 +178,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
|
||||||
let apiName: string | undefined = stackTrace.apiName;
|
let apiName: string | undefined = stackTrace.apiName;
|
||||||
const frames: channels.StackFrame[] = stackTrace.frames;
|
const frames: channels.StackFrame[] = stackTrace.frames;
|
||||||
|
|
||||||
isInternal = isInternal || this._type === 'LocalUtils';
|
isInternal = isInternal || this._isInternalType;
|
||||||
if (isInternal)
|
if (isInternal)
|
||||||
apiName = undefined;
|
apiName = undefined;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ export class LocalUtils extends ChannelOwner<channels.LocalUtilsChannel> {
|
||||||
|
|
||||||
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.LocalUtilsInitializer) {
|
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.LocalUtilsInitializer) {
|
||||||
super(parent, type, guid, initializer);
|
super(parent, type, guid, initializer);
|
||||||
|
this.markAsInternalType();
|
||||||
this.devices = {};
|
this.devices = {};
|
||||||
for (const { name, descriptor } of initializer.deviceDescriptors)
|
for (const { name, descriptor } of initializer.deviceDescriptors)
|
||||||
this.devices[name] = descriptor;
|
this.devices[name] = descriptor;
|
||||||
|
|
|
||||||
|
|
@ -299,6 +299,7 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
|
||||||
|
|
||||||
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.RouteInitializer) {
|
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.RouteInitializer) {
|
||||||
super(parent, type, guid, initializer);
|
super(parent, type, guid, initializer);
|
||||||
|
this.markAsInternalType();
|
||||||
}
|
}
|
||||||
|
|
||||||
request(): Request {
|
request(): Request {
|
||||||
|
|
@ -325,7 +326,7 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
|
||||||
|
|
||||||
async abort(errorCode?: string) {
|
async abort(errorCode?: string) {
|
||||||
await this._handleRoute(async () => {
|
await this._handleRoute(async () => {
|
||||||
await this._raceWithTargetClose(this._channel.abort({ requestUrl: this.request()._initializer.url, errorCode }));
|
await this._raceWithTargetClose(this._channel.abort({ errorCode }));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -409,7 +410,6 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
|
||||||
headers['content-length'] = String(length);
|
headers['content-length'] = String(length);
|
||||||
|
|
||||||
await this._raceWithTargetClose(this._channel.fulfill({
|
await this._raceWithTargetClose(this._channel.fulfill({
|
||||||
requestUrl: this.request()._initializer.url,
|
|
||||||
status: statusOption || 200,
|
status: statusOption || 200,
|
||||||
headers: headersObjectToArray(headers),
|
headers: headersObjectToArray(headers),
|
||||||
body,
|
body,
|
||||||
|
|
@ -421,7 +421,7 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
|
||||||
async continue(options: FallbackOverrides = {}) {
|
async continue(options: FallbackOverrides = {}) {
|
||||||
await this._handleRoute(async () => {
|
await this._handleRoute(async () => {
|
||||||
this.request()._applyFallbackOverrides(options);
|
this.request()._applyFallbackOverrides(options);
|
||||||
await this._innerContinue();
|
await this._innerContinue(false /* isFallback */);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -436,18 +436,15 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
|
||||||
chain.resolve(done);
|
chain.resolve(done);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _innerContinue(internal = false) {
|
async _innerContinue(isFallback: boolean) {
|
||||||
const options = this.request()._fallbackOverridesForContinue();
|
const options = this.request()._fallbackOverridesForContinue();
|
||||||
return await this._wrapApiCall(async () => {
|
return await this._raceWithTargetClose(this._channel.continue({
|
||||||
await this._raceWithTargetClose(this._channel.continue({
|
|
||||||
requestUrl: this.request()._initializer.url,
|
|
||||||
url: options.url,
|
url: options.url,
|
||||||
method: options.method,
|
method: options.method,
|
||||||
headers: options.headers ? headersObjectToArray(options.headers) : undefined,
|
headers: options.headers ? headersObjectToArray(options.headers) : undefined,
|
||||||
postData: options.postDataBuffer,
|
postData: options.postDataBuffer,
|
||||||
isFallback: internal,
|
isFallback,
|
||||||
}));
|
}));
|
||||||
}, !!internal);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,20 +31,18 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
|
||||||
|
|
||||||
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.TracingInitializer) {
|
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.TracingInitializer) {
|
||||||
super(parent, type, guid, initializer);
|
super(parent, type, guid, initializer);
|
||||||
|
this.markAsInternalType();
|
||||||
}
|
}
|
||||||
|
|
||||||
async start(options: { name?: string, title?: string, snapshots?: boolean, screenshots?: boolean, sources?: boolean, _live?: boolean } = {}) {
|
async start(options: { name?: string, title?: string, snapshots?: boolean, screenshots?: boolean, sources?: boolean, _live?: boolean } = {}) {
|
||||||
this._includeSources = !!options.sources;
|
this._includeSources = !!options.sources;
|
||||||
const traceName = await this._wrapApiCall(async () => {
|
|
||||||
await this._channel.tracingStart({
|
await this._channel.tracingStart({
|
||||||
name: options.name,
|
name: options.name,
|
||||||
snapshots: options.snapshots,
|
snapshots: options.snapshots,
|
||||||
screenshots: options.screenshots,
|
screenshots: options.screenshots,
|
||||||
live: options._live,
|
live: options._live,
|
||||||
});
|
});
|
||||||
const response = await this._channel.tracingStartChunk({ name: options.name, title: options.title });
|
const { traceName } = await this._channel.tracingStartChunk({ name: options.name, title: options.title });
|
||||||
return response.traceName;
|
|
||||||
}, true);
|
|
||||||
await this._startCollectingStacks(traceName);
|
await this._startCollectingStacks(traceName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -63,16 +61,12 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
|
||||||
}
|
}
|
||||||
|
|
||||||
async stopChunk(options: { path?: string } = {}) {
|
async stopChunk(options: { path?: string } = {}) {
|
||||||
await this._wrapApiCall(async () => {
|
|
||||||
await this._doStopChunk(options.path);
|
await this._doStopChunk(options.path);
|
||||||
}, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async stop(options: { path?: string } = {}) {
|
async stop(options: { path?: string } = {}) {
|
||||||
await this._wrapApiCall(async () => {
|
|
||||||
await this._doStopChunk(options.path);
|
await this._doStopChunk(options.path);
|
||||||
await this._channel.tracingStop();
|
await this._channel.tracingStop();
|
||||||
}, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _doStopChunk(filePath: string | undefined) {
|
private async _doStopChunk(filePath: string | undefined) {
|
||||||
|
|
|
||||||
|
|
@ -965,9 +965,10 @@ scheme.BrowserContextStorageStateResult = tObject({
|
||||||
});
|
});
|
||||||
scheme.BrowserContextPauseParams = tOptional(tObject({}));
|
scheme.BrowserContextPauseParams = tOptional(tObject({}));
|
||||||
scheme.BrowserContextPauseResult = tOptional(tObject({}));
|
scheme.BrowserContextPauseResult = tOptional(tObject({}));
|
||||||
scheme.BrowserContextRecorderSupplementEnableParams = tObject({
|
scheme.BrowserContextEnableRecorderParams = tObject({
|
||||||
language: tOptional(tString),
|
language: tOptional(tString),
|
||||||
mode: tOptional(tEnum(['inspecting', 'recording'])),
|
mode: tOptional(tEnum(['inspecting', 'recording'])),
|
||||||
|
codegenMode: tOptional(tEnum(['actions', 'trace-events'])),
|
||||||
pauseOnNextStatement: tOptional(tBoolean),
|
pauseOnNextStatement: tOptional(tBoolean),
|
||||||
testIdAttributeName: tOptional(tString),
|
testIdAttributeName: tOptional(tString),
|
||||||
launchOptions: tOptional(tAny),
|
launchOptions: tOptional(tAny),
|
||||||
|
|
@ -977,7 +978,7 @@ scheme.BrowserContextRecorderSupplementEnableParams = tObject({
|
||||||
outputFile: tOptional(tString),
|
outputFile: tOptional(tString),
|
||||||
omitCallTracking: tOptional(tBoolean),
|
omitCallTracking: tOptional(tBoolean),
|
||||||
});
|
});
|
||||||
scheme.BrowserContextRecorderSupplementEnableResult = tOptional(tObject({}));
|
scheme.BrowserContextEnableRecorderResult = tOptional(tObject({}));
|
||||||
scheme.BrowserContextNewCDPSessionParams = tObject({
|
scheme.BrowserContextNewCDPSessionParams = tObject({
|
||||||
page: tOptional(tChannel(['Page'])),
|
page: tOptional(tChannel(['Page'])),
|
||||||
frame: tOptional(tChannel(['Frame'])),
|
frame: tOptional(tChannel(['Frame'])),
|
||||||
|
|
@ -2115,7 +2116,6 @@ scheme.RouteRedirectNavigationRequestParams = tObject({
|
||||||
scheme.RouteRedirectNavigationRequestResult = tOptional(tObject({}));
|
scheme.RouteRedirectNavigationRequestResult = tOptional(tObject({}));
|
||||||
scheme.RouteAbortParams = tObject({
|
scheme.RouteAbortParams = tObject({
|
||||||
errorCode: tOptional(tString),
|
errorCode: tOptional(tString),
|
||||||
requestUrl: tString,
|
|
||||||
});
|
});
|
||||||
scheme.RouteAbortResult = tOptional(tObject({}));
|
scheme.RouteAbortResult = tOptional(tObject({}));
|
||||||
scheme.RouteContinueParams = tObject({
|
scheme.RouteContinueParams = tObject({
|
||||||
|
|
@ -2123,7 +2123,6 @@ scheme.RouteContinueParams = tObject({
|
||||||
method: tOptional(tString),
|
method: tOptional(tString),
|
||||||
headers: tOptional(tArray(tType('NameValue'))),
|
headers: tOptional(tArray(tType('NameValue'))),
|
||||||
postData: tOptional(tBinary),
|
postData: tOptional(tBinary),
|
||||||
requestUrl: tString,
|
|
||||||
isFallback: tBoolean,
|
isFallback: tBoolean,
|
||||||
});
|
});
|
||||||
scheme.RouteContinueResult = tOptional(tObject({}));
|
scheme.RouteContinueResult = tOptional(tObject({}));
|
||||||
|
|
@ -2133,7 +2132,6 @@ scheme.RouteFulfillParams = tObject({
|
||||||
body: tOptional(tString),
|
body: tOptional(tString),
|
||||||
isBase64: tOptional(tBoolean),
|
isBase64: tOptional(tBoolean),
|
||||||
fetchResponseUid: tOptional(tString),
|
fetchResponseUid: tOptional(tString),
|
||||||
requestUrl: tString,
|
|
||||||
});
|
});
|
||||||
scheme.RouteFulfillResult = tOptional(tObject({}));
|
scheme.RouteFulfillResult = tOptional(tObject({}));
|
||||||
scheme.WebSocketRouteInitializer = tObject({
|
scheme.WebSocketRouteInitializer = tObject({
|
||||||
|
|
|
||||||
|
|
@ -131,15 +131,15 @@ export abstract class BrowserContext extends SdkObject {
|
||||||
|
|
||||||
// When PWDEBUG=1, show inspector for each context.
|
// When PWDEBUG=1, show inspector for each context.
|
||||||
if (debugMode() === 'inspector')
|
if (debugMode() === 'inspector')
|
||||||
await Recorder.show(this, RecorderApp.factory(this), { pauseOnNextStatement: true });
|
await Recorder.show('actions', this, RecorderApp.factory(this), { pauseOnNextStatement: true });
|
||||||
|
|
||||||
// When paused, show inspector.
|
// When paused, show inspector.
|
||||||
if (this._debugger.isPaused())
|
if (this._debugger.isPaused())
|
||||||
Recorder.showInspector(this, RecorderApp.factory(this));
|
Recorder.showInspectorNoReply(this, RecorderApp.factory(this));
|
||||||
|
|
||||||
this._debugger.on(Debugger.Events.PausedStateChanged, () => {
|
this._debugger.on(Debugger.Events.PausedStateChanged, () => {
|
||||||
if (this._debugger.isPaused())
|
if (this._debugger.isPaused())
|
||||||
Recorder.showInspector(this, RecorderApp.factory(this));
|
Recorder.showInspectorNoReply(this, RecorderApp.factory(this));
|
||||||
});
|
});
|
||||||
|
|
||||||
if (debugMode() === 'console')
|
if (debugMode() === 'console')
|
||||||
|
|
@ -525,7 +525,7 @@ export abstract class BrowserContext extends SdkObject {
|
||||||
const internalMetadata = serverSideCallMetadata();
|
const internalMetadata = serverSideCallMetadata();
|
||||||
const page = await this.newPage(internalMetadata);
|
const page = await this.newPage(internalMetadata);
|
||||||
await page._setServerRequestInterceptor(handler => {
|
await page._setServerRequestInterceptor(handler => {
|
||||||
handler.fulfill({ body: '<html></html>', requestUrl: handler.request().url() }).catch(() => {});
|
handler.fulfill({ body: '<html></html>' }).catch(() => {});
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
for (const origin of originsToSave) {
|
for (const origin of originsToSave) {
|
||||||
|
|
@ -559,7 +559,7 @@ export abstract class BrowserContext extends SdkObject {
|
||||||
isServerSide: false,
|
isServerSide: false,
|
||||||
});
|
});
|
||||||
await page._setServerRequestInterceptor(handler => {
|
await page._setServerRequestInterceptor(handler => {
|
||||||
handler.fulfill({ body: '<html></html>', requestUrl: handler.request().url() }).catch(() => {});
|
handler.fulfill({ body: '<html></html>' }).catch(() => {});
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -594,7 +594,7 @@ export abstract class BrowserContext extends SdkObject {
|
||||||
const internalMetadata = serverSideCallMetadata();
|
const internalMetadata = serverSideCallMetadata();
|
||||||
const page = await this.newPage(internalMetadata);
|
const page = await this.newPage(internalMetadata);
|
||||||
await page._setServerRequestInterceptor(handler => {
|
await page._setServerRequestInterceptor(handler => {
|
||||||
handler.fulfill({ body: '<html></html>', requestUrl: handler.request().url() }).catch(() => {});
|
handler.fulfill({ body: '<html></html>' }).catch(() => {});
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
for (const originState of state.origins) {
|
for (const originState of state.origins) {
|
||||||
|
|
|
||||||
|
|
@ -197,7 +197,7 @@ export class DebugController extends SdkObject {
|
||||||
const contexts = new Set<BrowserContext>();
|
const contexts = new Set<BrowserContext>();
|
||||||
for (const page of this._playwright.allPages())
|
for (const page of this._playwright.allPages())
|
||||||
contexts.add(page.context());
|
contexts.add(page.context());
|
||||||
const result = await Promise.all([...contexts].map(c => Recorder.show(c, () => Promise.resolve(new InspectingRecorderApp(this)), { omitCallTracking: true })));
|
const result = await Promise.all([...contexts].map(c => Recorder.showInspector(c, { omitCallTracking: true }, () => Promise.resolve(new InspectingRecorderApp(this)))));
|
||||||
return result.filter(Boolean) as Recorder[];
|
return result.filter(Boolean) as Recorder[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ export class Dialog extends SdkObject {
|
||||||
this._onHandle = onHandle;
|
this._onHandle = onHandle;
|
||||||
this._defaultValue = defaultValue || '';
|
this._defaultValue = defaultValue || '';
|
||||||
this._page._frameManager.dialogDidOpen(this);
|
this._page._frameManager.dialogDidOpen(this);
|
||||||
|
this.instrumentation.onDialog(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
page() {
|
page() {
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,6 @@ import { serializeError } from '../errors';
|
||||||
import { ElementHandleDispatcher } from './elementHandlerDispatcher';
|
import { ElementHandleDispatcher } from './elementHandlerDispatcher';
|
||||||
import { RecorderInTraceViewer } from '../recorder/recorderInTraceViewer';
|
import { RecorderInTraceViewer } from '../recorder/recorderInTraceViewer';
|
||||||
import { RecorderApp } from '../recorder/recorderApp';
|
import { RecorderApp } from '../recorder/recorderApp';
|
||||||
import type { IRecorderAppFactory } from '../recorder/recorderFrontend';
|
|
||||||
import { WebSocketRouteDispatcher } from './webSocketRouteDispatcher';
|
import { WebSocketRouteDispatcher } from './webSocketRouteDispatcher';
|
||||||
|
|
||||||
export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channels.BrowserContextChannel, DispatcherScope> implements channels.BrowserContextChannel {
|
export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channels.BrowserContextChannel, DispatcherScope> implements channels.BrowserContextChannel {
|
||||||
|
|
@ -301,21 +300,18 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
|
||||||
await this._context.close(params);
|
await this._context.close(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
async recorderSupplementEnable(params: channels.BrowserContextRecorderSupplementEnableParams): Promise<void> {
|
async enableRecorder(params: channels.BrowserContextEnableRecorderParams): Promise<void> {
|
||||||
let factory: IRecorderAppFactory;
|
if (params.codegenMode === 'trace-events') {
|
||||||
if (process.env.PW_RECORDER_IS_TRACE_VIEWER) {
|
|
||||||
factory = RecorderInTraceViewer.factory(this._context);
|
|
||||||
await this._context.tracing.start({
|
await this._context.tracing.start({
|
||||||
name: 'trace',
|
name: 'trace',
|
||||||
snapshots: true,
|
snapshots: true,
|
||||||
screenshots: false,
|
screenshots: true,
|
||||||
live: true,
|
live: true,
|
||||||
});
|
});
|
||||||
await this._context.tracing.startChunk({ name: 'trace', title: 'trace' });
|
await Recorder.show('trace-events', this._context, RecorderInTraceViewer.factory(this._context), params);
|
||||||
} else {
|
} else {
|
||||||
factory = RecorderApp.factory(this._context);
|
await Recorder.show('actions', this._context, RecorderApp.factory(this._context), params);
|
||||||
}
|
}
|
||||||
await Recorder.show(this._context, factory, params);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async pause(params: channels.BrowserContextPauseParams, metadata: CallMetadata) {
|
async pause(params: channels.BrowserContextPauseParams, metadata: CallMetadata) {
|
||||||
|
|
|
||||||
|
|
@ -35,16 +35,25 @@ export class Download {
|
||||||
this._suggestedFilename = suggestedFilename;
|
this._suggestedFilename = suggestedFilename;
|
||||||
page._browserContext._downloads.add(this);
|
page._browserContext._downloads.add(this);
|
||||||
if (suggestedFilename !== undefined)
|
if (suggestedFilename !== undefined)
|
||||||
this._page.emit(Page.Events.Download, this);
|
this._fireDownloadEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
page(): Page {
|
||||||
|
return this._page;
|
||||||
}
|
}
|
||||||
|
|
||||||
_filenameSuggested(suggestedFilename: string) {
|
_filenameSuggested(suggestedFilename: string) {
|
||||||
assert(this._suggestedFilename === undefined);
|
assert(this._suggestedFilename === undefined);
|
||||||
this._suggestedFilename = suggestedFilename;
|
this._suggestedFilename = suggestedFilename;
|
||||||
this._page.emit(Page.Events.Download, this);
|
this._fireDownloadEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
suggestedFilename(): string {
|
suggestedFilename(): string {
|
||||||
return this._suggestedFilename!;
|
return this._suggestedFilename!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _fireDownloadEvent() {
|
||||||
|
this._page.instrumentation.onDownload(this._page, this);
|
||||||
|
this._page.emit(Page.Events.Download, this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,8 @@ export type Attribution = {
|
||||||
};
|
};
|
||||||
|
|
||||||
import type { CallMetadata } from '@protocol/callMetadata';
|
import type { CallMetadata } from '@protocol/callMetadata';
|
||||||
|
import type { Dialog } from './dialog';
|
||||||
|
import type { Download } from './download';
|
||||||
export type { CallMetadata } from '@protocol/callMetadata';
|
export type { CallMetadata } from '@protocol/callMetadata';
|
||||||
|
|
||||||
export class SdkObject extends EventEmitter {
|
export class SdkObject extends EventEmitter {
|
||||||
|
|
@ -62,6 +64,8 @@ export interface Instrumentation {
|
||||||
onPageClose(page: Page): void;
|
onPageClose(page: Page): void;
|
||||||
onBrowserOpen(browser: Browser): void;
|
onBrowserOpen(browser: Browser): void;
|
||||||
onBrowserClose(browser: Browser): void;
|
onBrowserClose(browser: Browser): void;
|
||||||
|
onDialog(dialog: Dialog): void;
|
||||||
|
onDownload(page: Page, download: Download): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InstrumentationListener {
|
export interface InstrumentationListener {
|
||||||
|
|
@ -73,6 +77,8 @@ export interface InstrumentationListener {
|
||||||
onPageClose?(page: Page): void;
|
onPageClose?(page: Page): void;
|
||||||
onBrowserOpen?(browser: Browser): void;
|
onBrowserOpen?(browser: Browser): void;
|
||||||
onBrowserClose?(browser: Browser): void;
|
onBrowserClose?(browser: Browser): void;
|
||||||
|
onDialog?(dialog: Dialog): void;
|
||||||
|
onDownload?(page: Page, download: Download): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createInstrumentation(): Instrumentation {
|
export function createInstrumentation(): Instrumentation {
|
||||||
|
|
|
||||||
|
|
@ -45,32 +45,35 @@ export class Recorder implements InstrumentationListener, IRecorder {
|
||||||
private _omitCallTracking = false;
|
private _omitCallTracking = false;
|
||||||
private _currentLanguage: Language;
|
private _currentLanguage: Language;
|
||||||
|
|
||||||
static showInspector(context: BrowserContext, recorderAppFactory: IRecorderAppFactory) {
|
static async showInspector(context: BrowserContext, params: channels.BrowserContextEnableRecorderParams, recorderAppFactory: IRecorderAppFactory) {
|
||||||
const params: channels.BrowserContextRecorderSupplementEnableParams = {};
|
|
||||||
if (isUnderTest())
|
if (isUnderTest())
|
||||||
params.language = process.env.TEST_INSPECTOR_LANGUAGE;
|
params.language = process.env.TEST_INSPECTOR_LANGUAGE;
|
||||||
Recorder.show(context, recorderAppFactory, params).catch(() => {});
|
return await Recorder.show('actions', context, recorderAppFactory, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
static show(context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextRecorderSupplementEnableParams = {}): Promise<Recorder> {
|
static showInspectorNoReply(context: BrowserContext, recorderAppFactory: IRecorderAppFactory) {
|
||||||
|
Recorder.showInspector(context, {}, recorderAppFactory).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
static show(codegenMode: 'actions' | 'trace-events', context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextEnableRecorderParams): Promise<Recorder> {
|
||||||
let recorderPromise = (context as any)[recorderSymbol] as Promise<Recorder>;
|
let recorderPromise = (context as any)[recorderSymbol] as Promise<Recorder>;
|
||||||
if (!recorderPromise) {
|
if (!recorderPromise) {
|
||||||
recorderPromise = Recorder._create(context, recorderAppFactory, params);
|
recorderPromise = Recorder._create(codegenMode, context, recorderAppFactory, params);
|
||||||
(context as any)[recorderSymbol] = recorderPromise;
|
(context as any)[recorderSymbol] = recorderPromise;
|
||||||
}
|
}
|
||||||
return recorderPromise;
|
return recorderPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async _create(context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextRecorderSupplementEnableParams = {}): Promise<Recorder> {
|
private static async _create(codegenMode: 'actions' | 'trace-events', context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextEnableRecorderParams = {}): Promise<Recorder> {
|
||||||
const recorder = new Recorder(context, params);
|
const recorder = new Recorder(codegenMode, context, params);
|
||||||
const recorderApp = await recorderAppFactory(recorder);
|
const recorderApp = await recorderAppFactory(recorder);
|
||||||
await recorder._install(recorderApp);
|
await recorder._install(recorderApp);
|
||||||
return recorder;
|
return recorder;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams) {
|
constructor(codegenMode: 'actions' | 'trace-events', context: BrowserContext, params: channels.BrowserContextEnableRecorderParams) {
|
||||||
this._mode = params.mode || 'none';
|
this._mode = params.mode || 'none';
|
||||||
this._contextRecorder = new ContextRecorder(context, params, {});
|
this._contextRecorder = new ContextRecorder(codegenMode, context, params, {});
|
||||||
this._context = context;
|
this._context = context;
|
||||||
this._omitCallTracking = !!params.omitCallTracking;
|
this._omitCallTracking = !!params.omitCallTracking;
|
||||||
this._debugger = context.debugger();
|
this._debugger = context.debugger();
|
||||||
|
|
|
||||||
|
|
@ -48,14 +48,14 @@ export class ContextRecorder extends EventEmitter {
|
||||||
private _lastDialogOrdinal = -1;
|
private _lastDialogOrdinal = -1;
|
||||||
private _lastDownloadOrdinal = -1;
|
private _lastDownloadOrdinal = -1;
|
||||||
private _context: BrowserContext;
|
private _context: BrowserContext;
|
||||||
private _params: channels.BrowserContextRecorderSupplementEnableParams;
|
private _params: channels.BrowserContextEnableRecorderParams;
|
||||||
private _delegate: ContextRecorderDelegate;
|
private _delegate: ContextRecorderDelegate;
|
||||||
private _recorderSources: Source[];
|
private _recorderSources: Source[];
|
||||||
private _throttledOutputFile: ThrottledFile | null = null;
|
private _throttledOutputFile: ThrottledFile | null = null;
|
||||||
private _orderedLanguages: LanguageGenerator[] = [];
|
private _orderedLanguages: LanguageGenerator[] = [];
|
||||||
private _listeners: RegisteredListener[] = [];
|
private _listeners: RegisteredListener[] = [];
|
||||||
|
|
||||||
constructor(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams, delegate: ContextRecorderDelegate) {
|
constructor(codegenMode: 'actions' | 'trace-events', context: BrowserContext, params: channels.BrowserContextEnableRecorderParams, delegate: ContextRecorderDelegate) {
|
||||||
super();
|
super();
|
||||||
this._context = context;
|
this._context = context;
|
||||||
this._params = params;
|
this._params = params;
|
||||||
|
|
@ -73,8 +73,8 @@ export class ContextRecorder extends EventEmitter {
|
||||||
saveStorage: params.saveStorage,
|
saveStorage: params.saveStorage,
|
||||||
};
|
};
|
||||||
|
|
||||||
const collection = new RecorderCollection(context, this._pageAliases, params.mode === 'recording');
|
this._collection = new RecorderCollection(codegenMode, context, this._pageAliases);
|
||||||
collection.on('change', (actions: ActionInContext[]) => {
|
this._collection.on('change', (actions: ActionInContext[]) => {
|
||||||
this._recorderSources = [];
|
this._recorderSources = [];
|
||||||
for (const languageGenerator of this._orderedLanguages) {
|
for (const languageGenerator of this._orderedLanguages) {
|
||||||
const { header, footer, actionTexts, text } = generateCode(actions, languageGenerator, languageGeneratorOptions);
|
const { header, footer, actionTexts, text } = generateCode(actions, languageGenerator, languageGeneratorOptions);
|
||||||
|
|
@ -103,7 +103,7 @@ export class ContextRecorder extends EventEmitter {
|
||||||
this._listeners.push(eventsHelper.addEventListener(process, 'exit', () => {
|
this._listeners.push(eventsHelper.addEventListener(process, 'exit', () => {
|
||||||
this._throttledOutputFile?.flush();
|
this._throttledOutputFile?.flush();
|
||||||
}));
|
}));
|
||||||
this._collection = collection;
|
this.setEnabled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
setOutput(codegenId: string, outputFile?: string) {
|
setOutput(codegenId: string, outputFile?: string) {
|
||||||
|
|
@ -145,6 +145,10 @@ export class ContextRecorder extends EventEmitter {
|
||||||
|
|
||||||
setEnabled(enabled: boolean) {
|
setEnabled(enabled: boolean) {
|
||||||
this._collection.setEnabled(enabled);
|
this._collection.setEnabled(enabled);
|
||||||
|
if (enabled)
|
||||||
|
this._context.tracing.startChunk({ name: 'trace', title: 'trace' }).catch(() => {});
|
||||||
|
else
|
||||||
|
this._context.tracing.stopChunk({ mode: 'discard' }).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
dispose() {
|
dispose() {
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,6 @@ export class RecorderApp extends EventEmitter implements IRecorderApp {
|
||||||
const file = require.resolve('../../vite/recorder/' + uri);
|
const file = require.resolve('../../vite/recorder/' + uri);
|
||||||
fs.promises.readFile(file).then(buffer => {
|
fs.promises.readFile(file).then(buffer => {
|
||||||
route.fulfill({
|
route.fulfill({
|
||||||
requestUrl: route.request().url(),
|
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: [
|
headers: [
|
||||||
{ name: 'Content-Type', value: mime.getType(path.extname(file)) || 'application/octet-stream' }
|
{ name: 'Content-Type', value: mime.getType(path.extname(file)) || 'application/octet-stream' }
|
||||||
|
|
@ -162,8 +161,10 @@ export class RecorderApp extends EventEmitter implements IRecorderApp {
|
||||||
}).toString(), { isFunction: true }, sources).catch(() => {});
|
}).toString(), { isFunction: true }, sources).catch(() => {});
|
||||||
|
|
||||||
// Testing harness for runCLI mode.
|
// Testing harness for runCLI mode.
|
||||||
if (process.env.PWTEST_CLI_IS_UNDER_TEST && sources.length)
|
if (process.env.PWTEST_CLI_IS_UNDER_TEST && sources.length) {
|
||||||
(process as any)._didSetSourcesForTest(sources[0].text);
|
if ((process as any)._didSetSourcesForTest(sources[0].text))
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setSelector(selector: string, userGesture?: boolean): Promise<void> {
|
async setSelector(selector: string, userGesture?: boolean): Promise<void> {
|
||||||
|
|
|
||||||
|
|
@ -29,17 +29,16 @@ import type { BrowserContext } from '../browserContext';
|
||||||
|
|
||||||
export class RecorderCollection extends EventEmitter {
|
export class RecorderCollection extends EventEmitter {
|
||||||
private _actions: ActionInContext[] = [];
|
private _actions: ActionInContext[] = [];
|
||||||
private _enabled: boolean;
|
private _enabled = false;
|
||||||
private _pageAliases: Map<Page, string>;
|
private _pageAliases: Map<Page, string>;
|
||||||
private _context: BrowserContext;
|
private _context: BrowserContext;
|
||||||
|
|
||||||
constructor(context: BrowserContext, pageAliases: Map<Page, string>, enabled: boolean) {
|
constructor(codegenMode: 'actions' | 'trace-events', context: BrowserContext, pageAliases: Map<Page, string>) {
|
||||||
super();
|
super();
|
||||||
this._context = context;
|
this._context = context;
|
||||||
this._enabled = enabled;
|
|
||||||
this._pageAliases = pageAliases;
|
this._pageAliases = pageAliases;
|
||||||
|
|
||||||
if (process.env.PW_RECORDER_IS_TRACE_VIEWER) {
|
if (codegenMode === 'trace-events') {
|
||||||
this._context.tracing.onMemoryEvents(events => {
|
this._context.tracing.onMemoryEvents(events => {
|
||||||
this._actions = traceEventsToAction(events);
|
this._actions = traceEventsToAction(events);
|
||||||
this._fireChange();
|
this._fireChange();
|
||||||
|
|
|
||||||
|
|
@ -21,77 +21,97 @@ import type { IRecorder, IRecorderApp, IRecorderAppFactory } from './recorderFro
|
||||||
import { installRootRedirect, openTraceViewerApp, startTraceViewerServer } from '../trace/viewer/traceViewer';
|
import { installRootRedirect, openTraceViewerApp, startTraceViewerServer } from '../trace/viewer/traceViewer';
|
||||||
import type { TraceViewerServerOptions } from '../trace/viewer/traceViewer';
|
import type { TraceViewerServerOptions } from '../trace/viewer/traceViewer';
|
||||||
import type { BrowserContext } from '../browserContext';
|
import type { BrowserContext } from '../browserContext';
|
||||||
import { gracefullyProcessExitDoNotHang } from '../../utils/processLauncher';
|
import type { HttpServer, Transport } from '../../utils/httpServer';
|
||||||
import type { Transport } from '../../utils/httpServer';
|
import type { Page } from '../page';
|
||||||
|
import { ManualPromise } from '../../utils/manualPromise';
|
||||||
|
|
||||||
export class RecorderInTraceViewer extends EventEmitter implements IRecorderApp {
|
export class RecorderInTraceViewer extends EventEmitter implements IRecorderApp {
|
||||||
readonly wsEndpointForTest: string | undefined;
|
readonly wsEndpointForTest: string | undefined;
|
||||||
private _recorder: IRecorder;
|
private _transport: RecorderTransport;
|
||||||
private _transport: Transport;
|
private _tracePage: Page;
|
||||||
|
private _traceServer: HttpServer;
|
||||||
|
|
||||||
static factory(context: BrowserContext): IRecorderAppFactory {
|
static factory(context: BrowserContext): IRecorderAppFactory {
|
||||||
return async (recorder: IRecorder) => {
|
return async (recorder: IRecorder) => {
|
||||||
const transport = new RecorderTransport();
|
const transport = new RecorderTransport();
|
||||||
const trace = path.join(context._browser.options.tracesDir, 'trace');
|
const trace = path.join(context._browser.options.tracesDir, 'trace');
|
||||||
const wsEndpointForTest = await openApp(trace, { transport, headless: !context._browser.options.headful });
|
const { wsEndpointForTest, tracePage, traceServer } = await openApp(trace, { transport, headless: !context._browser.options.headful });
|
||||||
return new RecorderInTraceViewer(context, recorder, transport, wsEndpointForTest);
|
return new RecorderInTraceViewer(transport, tracePage, traceServer, wsEndpointForTest);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(context: BrowserContext, recorder: IRecorder, transport: Transport, wsEndpointForTest: string | undefined) {
|
constructor(transport: RecorderTransport, tracePage: Page, traceServer: HttpServer, wsEndpointForTest: string | undefined) {
|
||||||
super();
|
super();
|
||||||
this._recorder = recorder;
|
|
||||||
this._transport = transport;
|
this._transport = transport;
|
||||||
|
this._tracePage = tracePage;
|
||||||
|
this._traceServer = traceServer;
|
||||||
this.wsEndpointForTest = wsEndpointForTest;
|
this.wsEndpointForTest = wsEndpointForTest;
|
||||||
|
this._tracePage.once('close', () => {
|
||||||
|
this.close();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async close(): Promise<void> {
|
async close(): Promise<void> {
|
||||||
this._transport.sendEvent?.('close', {});
|
await this._tracePage.context().close({ reason: 'Recorder window closed' });
|
||||||
|
await this._traceServer.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
async setPaused(paused: boolean): Promise<void> {
|
async setPaused(paused: boolean): Promise<void> {
|
||||||
this._transport.sendEvent?.('setPaused', { paused });
|
this._transport.deliverEvent('setPaused', { paused });
|
||||||
}
|
}
|
||||||
|
|
||||||
async setMode(mode: Mode): Promise<void> {
|
async setMode(mode: Mode): Promise<void> {
|
||||||
this._transport.sendEvent?.('setMode', { mode });
|
this._transport.deliverEvent('setMode', { mode });
|
||||||
}
|
}
|
||||||
|
|
||||||
async setFile(file: string): Promise<void> {
|
async setFile(file: string): Promise<void> {
|
||||||
this._transport.sendEvent?.('setFileIfNeeded', { file });
|
this._transport.deliverEvent('setFileIfNeeded', { file });
|
||||||
}
|
}
|
||||||
|
|
||||||
async setSelector(selector: string, userGesture?: boolean): Promise<void> {
|
async setSelector(selector: string, userGesture?: boolean): Promise<void> {
|
||||||
this._transport.sendEvent?.('setSelector', { selector, userGesture });
|
this._transport.deliverEvent('setSelector', { selector, userGesture });
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateCallLogs(callLogs: CallLog[]): Promise<void> {
|
async updateCallLogs(callLogs: CallLog[]): Promise<void> {
|
||||||
this._transport.sendEvent?.('updateCallLogs', { callLogs });
|
this._transport.deliverEvent('updateCallLogs', { callLogs });
|
||||||
}
|
}
|
||||||
|
|
||||||
async setSources(sources: Source[]): Promise<void> {
|
async setSources(sources: Source[]): Promise<void> {
|
||||||
this._transport.sendEvent?.('setSources', { sources });
|
this._transport.deliverEvent('setSources', { sources });
|
||||||
|
if (process.env.PWTEST_CLI_IS_UNDER_TEST && sources.length) {
|
||||||
|
if ((process as any)._didSetSourcesForTest(sources[0].text))
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openApp(trace: string, options?: TraceViewerServerOptions & { headless?: boolean }): Promise<string | undefined> {
|
async function openApp(trace: string, options?: TraceViewerServerOptions & { headless?: boolean }): Promise<{ wsEndpointForTest: string | undefined, tracePage: Page, traceServer: HttpServer }> {
|
||||||
const server = await startTraceViewerServer(options);
|
const traceServer = await startTraceViewerServer(options);
|
||||||
await installRootRedirect(server, [trace], { ...options, webApp: 'recorder.html' });
|
await installRootRedirect(traceServer, [trace], { ...options, webApp: 'recorder.html' });
|
||||||
const page = await openTraceViewerApp(server.urlPrefix('precise'), 'chromium', options);
|
const page = await openTraceViewerApp(traceServer.urlPrefix('precise'), 'chromium', options);
|
||||||
page.on('close', () => gracefullyProcessExitDoNotHang(0));
|
return { wsEndpointForTest: page.context()._browser.options.wsEndpoint, tracePage: page, traceServer };
|
||||||
return page.context()._browser.options.wsEndpoint;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class RecorderTransport implements Transport {
|
class RecorderTransport implements Transport {
|
||||||
|
private _connected = new ManualPromise<void>();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async dispatch(method: string, params: any) {
|
onconnect() {
|
||||||
|
this._connected.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
async dispatch(method: string, params: any): Promise<any> {
|
||||||
}
|
}
|
||||||
|
|
||||||
onclose() {
|
onclose() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deliverEvent(method: string, params: any) {
|
||||||
|
this._connected.then(() => this.sendEvent?.(method, params));
|
||||||
|
}
|
||||||
|
|
||||||
sendEvent?: (method: string, params: any) => void;
|
sendEvent?: (method: string, params: any) => void;
|
||||||
close?: () => void;
|
close?: () => void;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ import type * as trace from '@trace/trace';
|
||||||
import { fromKeyboardModifiers, toKeyboardModifiers } from '../codegen/language';
|
import { fromKeyboardModifiers, toKeyboardModifiers } from '../codegen/language';
|
||||||
import { serializeExpectedTextValues } from '../../utils/expectUtils';
|
import { serializeExpectedTextValues } from '../../utils/expectUtils';
|
||||||
import { createGuid, monotonicTime } from '../../utils';
|
import { createGuid, monotonicTime } from '../../utils';
|
||||||
import { serializeValue } from '../../protocol/serializers';
|
import { parseSerializedValue, serializeValue } from '../../protocol/serializers';
|
||||||
import type { SmartKeyboardModifier } from '../types';
|
import type { SmartKeyboardModifier } from '../types';
|
||||||
|
|
||||||
export function metadataToCallLog(metadata: CallMetadata, status: CallLogStatus): CallLog {
|
export function metadataToCallLog(metadata: CallMetadata, status: CallLogStatus): CallLog {
|
||||||
|
|
@ -158,7 +158,7 @@ export function traceParamsForAction(actionInContext: ActionInContext): { method
|
||||||
const params: channels.FrameExpectParams = {
|
const params: channels.FrameExpectParams = {
|
||||||
selector: action.selector,
|
selector: action.selector,
|
||||||
expression: 'to.be.checked',
|
expression: 'to.be.checked',
|
||||||
isNot: action.checked,
|
isNot: !action.checked,
|
||||||
};
|
};
|
||||||
return { method: 'expect', params };
|
return { method: 'expect', params };
|
||||||
}
|
}
|
||||||
|
|
@ -166,7 +166,7 @@ export function traceParamsForAction(actionInContext: ActionInContext): { method
|
||||||
const params: channels.FrameExpectParams = {
|
const params: channels.FrameExpectParams = {
|
||||||
selector,
|
selector,
|
||||||
expression: 'to.have.text',
|
expression: 'to.have.text',
|
||||||
expectedText: serializeExpectedTextValues([action.text], { matchSubstring: true, normalizeWhiteSpace: true }),
|
expectedText: serializeExpectedTextValues([action.text], { matchSubstring: action.substring, normalizeWhiteSpace: true }),
|
||||||
isNot: false,
|
isNot: false,
|
||||||
};
|
};
|
||||||
return { method: 'expect', params };
|
return { method: 'expect', params };
|
||||||
|
|
@ -193,12 +193,12 @@ export function traceParamsForAction(actionInContext: ActionInContext): { method
|
||||||
|
|
||||||
export function callMetadataForAction(pageAliases: Map<Page, string>, actionInContext: ActionInContext): { callMetadata: CallMetadata, mainFrame: Frame } {
|
export function callMetadataForAction(pageAliases: Map<Page, string>, actionInContext: ActionInContext): { callMetadata: CallMetadata, mainFrame: Frame } {
|
||||||
const mainFrame = mainFrameForAction(pageAliases, actionInContext);
|
const mainFrame = mainFrameForAction(pageAliases, actionInContext);
|
||||||
const { action } = actionInContext;
|
|
||||||
const { method, params } = traceParamsForAction(actionInContext);
|
const { method, params } = traceParamsForAction(actionInContext);
|
||||||
|
|
||||||
const callMetadata: CallMetadata = {
|
const callMetadata: CallMetadata = {
|
||||||
id: `call@${createGuid()}`,
|
id: `call@${createGuid()}`,
|
||||||
stepId: `recorder@${createGuid()}`,
|
stepId: `recorder@${createGuid()}`,
|
||||||
apiName: 'frame.' + action.name,
|
apiName: 'page.' + method,
|
||||||
objectId: mainFrame.guid,
|
objectId: mainFrame.guid,
|
||||||
pageId: mainFrame._page.guid,
|
pageId: mainFrame._page.guid,
|
||||||
frameId: mainFrame.guid,
|
frameId: mainFrame.guid,
|
||||||
|
|
@ -215,13 +215,23 @@ export function callMetadataForAction(pageAliases: Map<Page, string>, actionInCo
|
||||||
export function traceEventsToAction(events: trace.TraceEvent[]): ActionInContext[] {
|
export function traceEventsToAction(events: trace.TraceEvent[]): ActionInContext[] {
|
||||||
const result: ActionInContext[] = [];
|
const result: ActionInContext[] = [];
|
||||||
const pageAliases = new Map<string, string>();
|
const pageAliases = new Map<string, string>();
|
||||||
|
let lastDownloadOrdinal = 0;
|
||||||
|
let lastDialogOrdinal = 0;
|
||||||
|
|
||||||
|
const addSignal = (signal: actions.Signal) => {
|
||||||
|
const lastAction = result[result.length - 1];
|
||||||
|
if (!lastAction)
|
||||||
|
return;
|
||||||
|
lastAction.action.signals.push(signal);
|
||||||
|
};
|
||||||
|
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
if (event.type === 'event' && event.class === 'BrowserContext' && event.method === 'page') {
|
if (event.type === 'event' && event.class === 'BrowserContext') {
|
||||||
const pageAlias = 'page' + pageAliases.size;
|
const { method, params } = event;
|
||||||
pageAliases.set(event.params.pageId, pageAlias);
|
if (method === 'page') {
|
||||||
const lastAction = result[result.length - 1];
|
const pageAlias = 'page' + (pageAliases.size || '');
|
||||||
lastAction.action.signals.push({
|
pageAliases.set(params.pageId, pageAlias);
|
||||||
|
addSignal({
|
||||||
name: 'popup',
|
name: 'popup',
|
||||||
popupAlias: pageAlias,
|
popupAlias: pageAlias,
|
||||||
});
|
});
|
||||||
|
|
@ -237,7 +247,7 @@ export function traceEventsToAction(events: trace.TraceEvent[]): ActionInContext
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.type === 'event' && event.class === 'BrowserContext' && event.method === 'pageClosed') {
|
if (method === 'pageClosed') {
|
||||||
const pageAlias = pageAliases.get(event.params.pageId) || 'page';
|
const pageAlias = pageAliases.get(event.params.pageId) || 'page';
|
||||||
result.push({
|
result.push({
|
||||||
frame: { pageAlias, framePath: [] },
|
frame: { pageAlias, framePath: [] },
|
||||||
|
|
@ -250,6 +260,28 @@ export function traceEventsToAction(events: trace.TraceEvent[]): ActionInContext
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (method === 'download') {
|
||||||
|
const downloadAlias = lastDownloadOrdinal ? String(lastDownloadOrdinal) : '';
|
||||||
|
++lastDownloadOrdinal;
|
||||||
|
addSignal({
|
||||||
|
name: 'download',
|
||||||
|
downloadAlias,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === 'dialog') {
|
||||||
|
const dialogAlias = lastDialogOrdinal ? String(lastDialogOrdinal) : '';
|
||||||
|
++lastDialogOrdinal;
|
||||||
|
addSignal({
|
||||||
|
name: 'dialog',
|
||||||
|
dialogAlias,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (event.type !== 'before' || !event.pageId)
|
if (event.type !== 'before' || !event.pageId)
|
||||||
continue;
|
continue;
|
||||||
if (!event.stepId?.startsWith('recorder@'))
|
if (!event.stepId?.startsWith('recorder@'))
|
||||||
|
|
@ -389,6 +421,67 @@ export function traceEventsToAction(events: trace.TraceEvent[]): ActionInContext
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (method === 'expect') {
|
||||||
|
const params = untypedParams as channels.FrameExpectParams;
|
||||||
|
if (params.expression === 'to.have.text') {
|
||||||
|
const entry = params.expectedText?.[0];
|
||||||
|
result.push({
|
||||||
|
frame: { pageAlias, framePath: [] },
|
||||||
|
action: {
|
||||||
|
name: 'assertText',
|
||||||
|
selector: params.selector,
|
||||||
|
signals: [],
|
||||||
|
text: entry?.string!,
|
||||||
|
substring: !!entry?.matchSubstring,
|
||||||
|
},
|
||||||
|
timestamp: event.startTime
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.expression === 'to.have.value') {
|
||||||
|
result.push({
|
||||||
|
frame: { pageAlias, framePath: [] },
|
||||||
|
action: {
|
||||||
|
name: 'assertValue',
|
||||||
|
selector: params.selector,
|
||||||
|
signals: [],
|
||||||
|
value: parseSerializedValue(params.expectedValue!.value, params.expectedValue!.handles),
|
||||||
|
},
|
||||||
|
timestamp: event.startTime
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.expression === 'to.be.checked') {
|
||||||
|
result.push({
|
||||||
|
frame: { pageAlias, framePath: [] },
|
||||||
|
action: {
|
||||||
|
name: 'assertChecked',
|
||||||
|
selector: params.selector,
|
||||||
|
signals: [],
|
||||||
|
checked: !params.isNot,
|
||||||
|
},
|
||||||
|
timestamp: event.startTime
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.expression === 'to.be.visible') {
|
||||||
|
result.push({
|
||||||
|
frame: { pageAlias, framePath: [] },
|
||||||
|
action: {
|
||||||
|
name: 'assertVisible',
|
||||||
|
selector: params.selector,
|
||||||
|
signals: [],
|
||||||
|
},
|
||||||
|
timestamp: event.startTime
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,8 @@ import { Snapshotter } from './snapshotter';
|
||||||
import type { ConsoleMessage } from '../../console';
|
import type { ConsoleMessage } from '../../console';
|
||||||
import { Dispatcher } from '../../dispatchers/dispatcher';
|
import { Dispatcher } from '../../dispatchers/dispatcher';
|
||||||
import { serializeError } from '../../errors';
|
import { serializeError } from '../../errors';
|
||||||
|
import type { Dialog } from '../../dialog';
|
||||||
|
import type { Download } from '../../download';
|
||||||
|
|
||||||
const version: trace.VERSION = 7;
|
const version: trace.VERSION = 7;
|
||||||
|
|
||||||
|
|
@ -454,6 +456,28 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
||||||
this._appendTraceEvent(event);
|
this._appendTraceEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onDialog(dialog: Dialog) {
|
||||||
|
const event: trace.EventTraceEvent = {
|
||||||
|
type: 'event',
|
||||||
|
time: monotonicTime(),
|
||||||
|
class: 'BrowserContext',
|
||||||
|
method: 'dialog',
|
||||||
|
params: { pageId: dialog.page().guid, type: dialog.type(), message: dialog.message(), defaultValue: dialog.defaultValue() },
|
||||||
|
};
|
||||||
|
this._appendTraceEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
onDownload(page: Page, download: Download) {
|
||||||
|
const event: trace.EventTraceEvent = {
|
||||||
|
type: 'event',
|
||||||
|
time: monotonicTime(),
|
||||||
|
class: 'BrowserContext',
|
||||||
|
method: 'download',
|
||||||
|
params: { pageId: page.guid, url: download.url, suggestedFilename: download.suggestedFilename() },
|
||||||
|
};
|
||||||
|
this._appendTraceEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
onPageOpen(page: Page) {
|
onPageOpen(page: Page) {
|
||||||
const event: trace.EventTraceEvent = {
|
const event: trace.EventTraceEvent = {
|
||||||
type: 'event',
|
type: 'event',
|
||||||
|
|
|
||||||
|
|
@ -223,6 +223,9 @@ class StdinServer implements Transport {
|
||||||
process.stdin.on('close', () => gracefullyProcessExitDoNotHang(0));
|
process.stdin.on('close', () => gracefullyProcessExitDoNotHang(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onconnect() {
|
||||||
|
}
|
||||||
|
|
||||||
async dispatch(method: string, params: any) {
|
async dispatch(method: string, params: any) {
|
||||||
if (method === 'initialize') {
|
if (method === 'initialize') {
|
||||||
if (this._traceUrl)
|
if (this._traceUrl)
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,9 @@ export type ServerRouteHandler = (request: http.IncomingMessage, response: http.
|
||||||
|
|
||||||
export type Transport = {
|
export type Transport = {
|
||||||
sendEvent?: (method: string, params: any) => void;
|
sendEvent?: (method: string, params: any) => void;
|
||||||
dispatch: (method: string, params: any) => Promise<any>;
|
|
||||||
close?: () => void;
|
close?: () => void;
|
||||||
|
onconnect: () => void;
|
||||||
|
dispatch: (method: string, params: any) => Promise<any>;
|
||||||
onclose: () => void;
|
onclose: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -82,6 +83,7 @@ export class HttpServer {
|
||||||
this._wsGuid = guid || createGuid();
|
this._wsGuid = guid || createGuid();
|
||||||
const wss = new wsServer({ server: this._server, path: '/' + this._wsGuid });
|
const wss = new wsServer({ server: this._server, path: '/' + this._wsGuid });
|
||||||
wss.on('connection', ws => {
|
wss.on('connection', ws => {
|
||||||
|
transport.onconnect();
|
||||||
transport.sendEvent = (method, params) => ws.send(JSON.stringify({ method, params }));
|
transport.sendEvent = (method, params) => ws.send(JSON.stringify({ method, params }));
|
||||||
transport.close = () => ws.close();
|
transport.close = () => ws.close();
|
||||||
ws.on('message', async message => {
|
ws.on('message', async message => {
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,7 @@ export class TestServerDispatcher implements TestServerInterface {
|
||||||
constructor(configLocation: ConfigLocation) {
|
constructor(configLocation: ConfigLocation) {
|
||||||
this._configLocation = configLocation;
|
this._configLocation = configLocation;
|
||||||
this.transport = {
|
this.transport = {
|
||||||
|
onconnect: () => {},
|
||||||
dispatch: (method, params) => (this as any)[method](params),
|
dispatch: (method, params) => (this as any)[method](params),
|
||||||
onclose: () => {
|
onclose: () => {
|
||||||
if (this._closeOnDisconnect)
|
if (this._closeOnDisconnect)
|
||||||
|
|
|
||||||
|
|
@ -1526,7 +1526,7 @@ export interface BrowserContextChannel extends BrowserContextEventTarget, EventT
|
||||||
setOffline(params: BrowserContextSetOfflineParams, metadata?: CallMetadata): Promise<BrowserContextSetOfflineResult>;
|
setOffline(params: BrowserContextSetOfflineParams, metadata?: CallMetadata): Promise<BrowserContextSetOfflineResult>;
|
||||||
storageState(params?: BrowserContextStorageStateParams, metadata?: CallMetadata): Promise<BrowserContextStorageStateResult>;
|
storageState(params?: BrowserContextStorageStateParams, metadata?: CallMetadata): Promise<BrowserContextStorageStateResult>;
|
||||||
pause(params?: BrowserContextPauseParams, metadata?: CallMetadata): Promise<BrowserContextPauseResult>;
|
pause(params?: BrowserContextPauseParams, metadata?: CallMetadata): Promise<BrowserContextPauseResult>;
|
||||||
recorderSupplementEnable(params: BrowserContextRecorderSupplementEnableParams, metadata?: CallMetadata): Promise<BrowserContextRecorderSupplementEnableResult>;
|
enableRecorder(params: BrowserContextEnableRecorderParams, metadata?: CallMetadata): Promise<BrowserContextEnableRecorderResult>;
|
||||||
newCDPSession(params: BrowserContextNewCDPSessionParams, metadata?: CallMetadata): Promise<BrowserContextNewCDPSessionResult>;
|
newCDPSession(params: BrowserContextNewCDPSessionParams, metadata?: CallMetadata): Promise<BrowserContextNewCDPSessionResult>;
|
||||||
harStart(params: BrowserContextHarStartParams, metadata?: CallMetadata): Promise<BrowserContextHarStartResult>;
|
harStart(params: BrowserContextHarStartParams, metadata?: CallMetadata): Promise<BrowserContextHarStartResult>;
|
||||||
harExport(params: BrowserContextHarExportParams, metadata?: CallMetadata): Promise<BrowserContextHarExportResult>;
|
harExport(params: BrowserContextHarExportParams, metadata?: CallMetadata): Promise<BrowserContextHarExportResult>;
|
||||||
|
|
@ -1766,9 +1766,10 @@ export type BrowserContextStorageStateResult = {
|
||||||
export type BrowserContextPauseParams = {};
|
export type BrowserContextPauseParams = {};
|
||||||
export type BrowserContextPauseOptions = {};
|
export type BrowserContextPauseOptions = {};
|
||||||
export type BrowserContextPauseResult = void;
|
export type BrowserContextPauseResult = void;
|
||||||
export type BrowserContextRecorderSupplementEnableParams = {
|
export type BrowserContextEnableRecorderParams = {
|
||||||
language?: string,
|
language?: string,
|
||||||
mode?: 'inspecting' | 'recording',
|
mode?: 'inspecting' | 'recording',
|
||||||
|
codegenMode?: 'actions' | 'trace-events',
|
||||||
pauseOnNextStatement?: boolean,
|
pauseOnNextStatement?: boolean,
|
||||||
testIdAttributeName?: string,
|
testIdAttributeName?: string,
|
||||||
launchOptions?: any,
|
launchOptions?: any,
|
||||||
|
|
@ -1778,9 +1779,10 @@ export type BrowserContextRecorderSupplementEnableParams = {
|
||||||
outputFile?: string,
|
outputFile?: string,
|
||||||
omitCallTracking?: boolean,
|
omitCallTracking?: boolean,
|
||||||
};
|
};
|
||||||
export type BrowserContextRecorderSupplementEnableOptions = {
|
export type BrowserContextEnableRecorderOptions = {
|
||||||
language?: string,
|
language?: string,
|
||||||
mode?: 'inspecting' | 'recording',
|
mode?: 'inspecting' | 'recording',
|
||||||
|
codegenMode?: 'actions' | 'trace-events',
|
||||||
pauseOnNextStatement?: boolean,
|
pauseOnNextStatement?: boolean,
|
||||||
testIdAttributeName?: string,
|
testIdAttributeName?: string,
|
||||||
launchOptions?: any,
|
launchOptions?: any,
|
||||||
|
|
@ -1790,7 +1792,7 @@ export type BrowserContextRecorderSupplementEnableOptions = {
|
||||||
outputFile?: string,
|
outputFile?: string,
|
||||||
omitCallTracking?: boolean,
|
omitCallTracking?: boolean,
|
||||||
};
|
};
|
||||||
export type BrowserContextRecorderSupplementEnableResult = void;
|
export type BrowserContextEnableRecorderResult = void;
|
||||||
export type BrowserContextNewCDPSessionParams = {
|
export type BrowserContextNewCDPSessionParams = {
|
||||||
page?: PageChannel,
|
page?: PageChannel,
|
||||||
frame?: FrameChannel,
|
frame?: FrameChannel,
|
||||||
|
|
@ -3769,7 +3771,6 @@ export type RouteRedirectNavigationRequestOptions = {
|
||||||
export type RouteRedirectNavigationRequestResult = void;
|
export type RouteRedirectNavigationRequestResult = void;
|
||||||
export type RouteAbortParams = {
|
export type RouteAbortParams = {
|
||||||
errorCode?: string,
|
errorCode?: string,
|
||||||
requestUrl: string,
|
|
||||||
};
|
};
|
||||||
export type RouteAbortOptions = {
|
export type RouteAbortOptions = {
|
||||||
errorCode?: string,
|
errorCode?: string,
|
||||||
|
|
@ -3780,7 +3781,6 @@ export type RouteContinueParams = {
|
||||||
method?: string,
|
method?: string,
|
||||||
headers?: NameValue[],
|
headers?: NameValue[],
|
||||||
postData?: Binary,
|
postData?: Binary,
|
||||||
requestUrl: string,
|
|
||||||
isFallback: boolean,
|
isFallback: boolean,
|
||||||
};
|
};
|
||||||
export type RouteContinueOptions = {
|
export type RouteContinueOptions = {
|
||||||
|
|
@ -3796,7 +3796,6 @@ export type RouteFulfillParams = {
|
||||||
body?: string,
|
body?: string,
|
||||||
isBase64?: boolean,
|
isBase64?: boolean,
|
||||||
fetchResponseUid?: string,
|
fetchResponseUid?: string,
|
||||||
requestUrl: string,
|
|
||||||
};
|
};
|
||||||
export type RouteFulfillOptions = {
|
export type RouteFulfillOptions = {
|
||||||
status?: number,
|
status?: number,
|
||||||
|
|
|
||||||
|
|
@ -1187,7 +1187,7 @@ BrowserContext:
|
||||||
pause:
|
pause:
|
||||||
experimental: True
|
experimental: True
|
||||||
|
|
||||||
recorderSupplementEnable:
|
enableRecorder:
|
||||||
experimental: True
|
experimental: True
|
||||||
parameters:
|
parameters:
|
||||||
language: string?
|
language: string?
|
||||||
|
|
@ -1196,6 +1196,11 @@ BrowserContext:
|
||||||
literals:
|
literals:
|
||||||
- inspecting
|
- inspecting
|
||||||
- recording
|
- recording
|
||||||
|
codegenMode:
|
||||||
|
type: enum?
|
||||||
|
literals:
|
||||||
|
- actions
|
||||||
|
- trace-events
|
||||||
pauseOnNextStatement: boolean?
|
pauseOnNextStatement: boolean?
|
||||||
testIdAttributeName: string?
|
testIdAttributeName: string?
|
||||||
launchOptions: json?
|
launchOptions: json?
|
||||||
|
|
@ -2941,7 +2946,6 @@ Route:
|
||||||
abort:
|
abort:
|
||||||
parameters:
|
parameters:
|
||||||
errorCode: string?
|
errorCode: string?
|
||||||
requestUrl: string
|
|
||||||
|
|
||||||
continue:
|
continue:
|
||||||
parameters:
|
parameters:
|
||||||
|
|
@ -2951,7 +2955,6 @@ Route:
|
||||||
type: array?
|
type: array?
|
||||||
items: NameValue
|
items: NameValue
|
||||||
postData: binary?
|
postData: binary?
|
||||||
requestUrl: string
|
|
||||||
isFallback: boolean
|
isFallback: boolean
|
||||||
|
|
||||||
fulfill:
|
fulfill:
|
||||||
|
|
@ -2964,7 +2967,6 @@ Route:
|
||||||
body: string?
|
body: string?
|
||||||
isBase64: boolean?
|
isBase64: boolean?
|
||||||
fetchResponseUid: string?
|
fetchResponseUid: string?
|
||||||
requestUrl: string
|
|
||||||
|
|
||||||
|
|
||||||
WebSocketRoute:
|
WebSocketRoute:
|
||||||
|
|
|
||||||
|
|
@ -47,12 +47,14 @@ async function loadTrace(traceUrl: string, traceFileName: string | null, clientI
|
||||||
}
|
}
|
||||||
set.add(traceUrl);
|
set.add(traceUrl);
|
||||||
|
|
||||||
|
const isRecorderMode = traceUrl.includes('/playwright-recorder-trace-');
|
||||||
|
|
||||||
const traceModel = new TraceModel();
|
const traceModel = new TraceModel();
|
||||||
try {
|
try {
|
||||||
// Allow 10% to hop from sw to page.
|
// Allow 10% to hop from sw to page.
|
||||||
const [fetchProgress, unzipProgress] = splitProgress(progress, [0.5, 0.4, 0.1]);
|
const [fetchProgress, unzipProgress] = splitProgress(progress, [0.5, 0.4, 0.1]);
|
||||||
const backend = traceUrl.endsWith('json') ? new FetchTraceModelBackend(traceUrl) : new ZipTraceModelBackend(traceUrl, fetchProgress);
|
const backend = traceUrl.endsWith('json') ? new FetchTraceModelBackend(traceUrl) : new ZipTraceModelBackend(traceUrl, fetchProgress);
|
||||||
await traceModel.load(backend, unzipProgress);
|
await traceModel.load(backend, isRecorderMode, unzipProgress);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { parseClientSideCallMetadata } from '../../../packages/playwright-core/src/utils/isomorphic/traceUtils';
|
import { parseClientSideCallMetadata } from '../../../packages/playwright-core/src/utils/isomorphic/traceUtils';
|
||||||
import type { ContextEntry } from './entries';
|
import type { ActionEntry, ContextEntry } from './entries';
|
||||||
import { createEmptyContext } from './entries';
|
import { createEmptyContext } from './entries';
|
||||||
import { SnapshotStorage } from './snapshotStorage';
|
import { SnapshotStorage } from './snapshotStorage';
|
||||||
import { TraceModernizer } from './traceModernizer';
|
import { TraceModernizer } from './traceModernizer';
|
||||||
|
|
@ -38,7 +38,7 @@ export class TraceModel {
|
||||||
constructor() {
|
constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async load(backend: TraceModelBackend, unzipProgress: (done: number, total: number) => void) {
|
async load(backend: TraceModelBackend, isRecorderMode: boolean, unzipProgress: (done: number, total: number) => void) {
|
||||||
this._backend = backend;
|
this._backend = backend;
|
||||||
|
|
||||||
const ordinals: string[] = [];
|
const ordinals: string[] = [];
|
||||||
|
|
@ -72,7 +72,8 @@ export class TraceModel {
|
||||||
modernizer.appendTrace(network);
|
modernizer.appendTrace(network);
|
||||||
unzipProgress(++done, total);
|
unzipProgress(++done, total);
|
||||||
|
|
||||||
contextEntry.actions = modernizer.actions().sort((a1, a2) => a1.startTime - a2.startTime);
|
const actions = modernizer.actions().sort((a1, a2) => a1.startTime - a2.startTime);
|
||||||
|
contextEntry.actions = isRecorderMode ? collapseActionsForRecorder(actions) : actions;
|
||||||
|
|
||||||
if (!backend.isLive()) {
|
if (!backend.isLive()) {
|
||||||
// Terminate actions w/o after event gracefully.
|
// Terminate actions w/o after event gracefully.
|
||||||
|
|
@ -133,3 +134,19 @@ function stripEncodingFromContentType(contentType: string) {
|
||||||
return charset[1];
|
return charset[1];
|
||||||
return contentType;
|
return contentType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function collapseActionsForRecorder(actions: ActionEntry[]): ActionEntry[] {
|
||||||
|
const result: ActionEntry[] = [];
|
||||||
|
for (const action of actions) {
|
||||||
|
const lastAction = result[result.length - 1];
|
||||||
|
const isSameAction = lastAction && lastAction.method === action.method && lastAction.pageId === action.pageId;
|
||||||
|
const isSameSelector = lastAction && 'selector' in lastAction.params && 'selector' in action.params && action.params.selector === lastAction.params.selector;
|
||||||
|
const shouldMerge = isSameAction && (action.method === 'goto' || (action.method === 'fill' && isSameSelector));
|
||||||
|
if (!shouldMerge) {
|
||||||
|
result.push(action);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
result[result.length - 1] = action;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,8 @@
|
||||||
|
|
||||||
.call-section {
|
.call-section {
|
||||||
padding-left: 6px;
|
padding-left: 6px;
|
||||||
|
padding-top: 2px;
|
||||||
|
margin-top: 2px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
|
@ -53,9 +55,8 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
line-height: 18px;
|
line-height: 20px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
max-height: 18px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.call-line:not(:hover) .toolbar-button.copy {
|
.call-line:not(:hover) .toolbar-button.copy {
|
||||||
|
|
@ -64,7 +65,8 @@
|
||||||
|
|
||||||
.call-line .toolbar-button.copy {
|
.call-line .toolbar-button.copy {
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
transform: scale(0.8);
|
margin-top: -2px;
|
||||||
|
margin-bottom: -2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.call-value {
|
.call-value {
|
||||||
|
|
|
||||||
|
|
@ -45,8 +45,6 @@ export const RecorderView: React.FunctionComponent = () => {
|
||||||
connection.setMode('recording');
|
connection.setMode('recording');
|
||||||
}, [connection]);
|
}, [connection]);
|
||||||
|
|
||||||
window.playwrightSourcesEchoForTest = sources;
|
|
||||||
|
|
||||||
return <div className='vbox workbench-loader'>
|
return <div className='vbox workbench-loader'>
|
||||||
<TraceView
|
<TraceView
|
||||||
traceLocation={trace}
|
traceLocation={trace}
|
||||||
|
|
@ -104,6 +102,7 @@ export const TraceView: React.FC<{
|
||||||
showSourcesFirst={true}
|
showSourcesFirst={true}
|
||||||
fallbackLocation={fallbackLocation}
|
fallbackLocation={fallbackLocation}
|
||||||
isLive={true}
|
isLive={true}
|
||||||
|
hideTimeline={true}
|
||||||
/>;
|
/>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -165,6 +164,7 @@ class Connection {
|
||||||
if (method === 'setSources') {
|
if (method === 'setSources') {
|
||||||
const { sources } = params as { sources: Source[] };
|
const { sources } = params as { sources: Source[] };
|
||||||
this._options.setSources(sources);
|
this._options.setSources(sources);
|
||||||
|
window.playwrightSourcesEchoForTest = sources;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,6 @@ export const UIModeView: React.FC<{}> = ({
|
||||||
const [singleWorker, setSingleWorker] = React.useState(queryParams.workers === '1');
|
const [singleWorker, setSingleWorker] = React.useState(queryParams.workers === '1');
|
||||||
const [showBrowser, setShowBrowser] = React.useState(queryParams.headed);
|
const [showBrowser, setShowBrowser] = React.useState(queryParams.headed);
|
||||||
const [updateSnapshots, setUpdateSnapshots] = React.useState(queryParams.updateSnapshots === 'all');
|
const [updateSnapshots, setUpdateSnapshots] = React.useState(queryParams.updateSnapshots === 'all');
|
||||||
const [showRouteActions, setShowRouteActions] = useSetting('show-route-actions', true);
|
|
||||||
const [darkMode, setDarkMode] = useDarkModeSetting();
|
const [darkMode, setDarkMode] = useDarkModeSetting();
|
||||||
const [showScreenshot, setShowScreenshot] = useSetting('screenshot-instead-of-snapshot', false);
|
const [showScreenshot, setShowScreenshot] = useSetting('screenshot-instead-of-snapshot', false);
|
||||||
|
|
||||||
|
|
@ -526,7 +525,6 @@ export const UIModeView: React.FC<{}> = ({
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
{settingsVisible && <SettingsView settings={[
|
{settingsVisible && <SettingsView settings={[
|
||||||
{ value: darkMode, set: setDarkMode, title: 'Dark mode' },
|
{ value: darkMode, set: setDarkMode, title: 'Dark mode' },
|
||||||
{ value: showRouteActions, set: setShowRouteActions, title: 'Show route actions' },
|
|
||||||
{ value: showScreenshot, set: setShowScreenshot, title: 'Show screenshot instead of snapshot' },
|
{ value: showScreenshot, set: setShowScreenshot, title: 'Show screenshot instead of snapshot' },
|
||||||
]} />}
|
]} />}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ import type { ErrorDescription } from './errorsTab';
|
||||||
import type { ConsoleEntry } from './consoleTab';
|
import type { ConsoleEntry } from './consoleTab';
|
||||||
import { ConsoleTab, useConsoleTabModel } from './consoleTab';
|
import { ConsoleTab, useConsoleTabModel } from './consoleTab';
|
||||||
import type * as modelUtil from './modelUtil';
|
import type * as modelUtil from './modelUtil';
|
||||||
import { isRouteAction } from './modelUtil';
|
|
||||||
import { NetworkTab, useNetworkTabModel } from './networkTab';
|
import { NetworkTab, useNetworkTabModel } from './networkTab';
|
||||||
import { SnapshotTab } from './snapshotTab';
|
import { SnapshotTab } from './snapshotTab';
|
||||||
import { SourceTab } from './sourceTab';
|
import { SourceTab } from './sourceTab';
|
||||||
|
|
@ -50,6 +49,7 @@ export const Workbench: React.FunctionComponent<{
|
||||||
rootDir?: string,
|
rootDir?: string,
|
||||||
fallbackLocation?: modelUtil.SourceLocation,
|
fallbackLocation?: modelUtil.SourceLocation,
|
||||||
isLive?: boolean,
|
isLive?: boolean,
|
||||||
|
hideTimeline?: boolean,
|
||||||
status?: UITestStatus,
|
status?: UITestStatus,
|
||||||
annotations?: { type: string; description?: string; }[];
|
annotations?: { type: string; description?: string; }[];
|
||||||
inert?: boolean,
|
inert?: boolean,
|
||||||
|
|
@ -57,7 +57,7 @@ export const Workbench: React.FunctionComponent<{
|
||||||
onOpenExternally?: (location: modelUtil.SourceLocation) => void,
|
onOpenExternally?: (location: modelUtil.SourceLocation) => void,
|
||||||
revealSource?: boolean,
|
revealSource?: boolean,
|
||||||
showSettings?: boolean,
|
showSettings?: boolean,
|
||||||
}> = ({ model, showSourcesFirst, rootDir, fallbackLocation, isLive, status, annotations, inert, openPage, onOpenExternally, revealSource, showSettings }) => {
|
}> = ({ model, showSourcesFirst, rootDir, fallbackLocation, isLive, hideTimeline, status, annotations, inert, openPage, onOpenExternally, revealSource, showSettings }) => {
|
||||||
const [selectedCallId, setSelectedCallId] = React.useState<string | undefined>(undefined);
|
const [selectedCallId, setSelectedCallId] = React.useState<string | undefined>(undefined);
|
||||||
const [revealedError, setRevealedError] = React.useState<ErrorDescription | undefined>(undefined);
|
const [revealedError, setRevealedError] = React.useState<ErrorDescription | undefined>(undefined);
|
||||||
|
|
||||||
|
|
@ -70,13 +70,8 @@ export const Workbench: React.FunctionComponent<{
|
||||||
const [highlightedLocator, setHighlightedLocator] = React.useState<string>('');
|
const [highlightedLocator, setHighlightedLocator] = React.useState<string>('');
|
||||||
const [selectedTime, setSelectedTime] = React.useState<Boundaries | undefined>();
|
const [selectedTime, setSelectedTime] = React.useState<Boundaries | undefined>();
|
||||||
const [sidebarLocation, setSidebarLocation] = useSetting<'bottom' | 'right'>('propertiesSidebarLocation', 'bottom');
|
const [sidebarLocation, setSidebarLocation] = useSetting<'bottom' | 'right'>('propertiesSidebarLocation', 'bottom');
|
||||||
const [showRouteActions, setShowRouteActions] = useSetting('show-route-actions', true);
|
|
||||||
const [showScreenshot, setShowScreenshot] = useSetting('screenshot-instead-of-snapshot', false);
|
const [showScreenshot, setShowScreenshot] = useSetting('screenshot-instead-of-snapshot', false);
|
||||||
|
|
||||||
const filteredActions = React.useMemo(() => {
|
|
||||||
return (model?.actions || []).filter(action => showRouteActions || !isRouteAction(action));
|
|
||||||
}, [model, showRouteActions]);
|
|
||||||
|
|
||||||
const setSelectedAction = React.useCallback((action: modelUtil.ActionTraceEventInContext | undefined) => {
|
const setSelectedAction = React.useCallback((action: modelUtil.ActionTraceEventInContext | undefined) => {
|
||||||
setSelectedCallId(action?.callId);
|
setSelectedCallId(action?.callId);
|
||||||
setRevealedError(undefined);
|
setRevealedError(undefined);
|
||||||
|
|
@ -291,7 +286,7 @@ export const Workbench: React.FunctionComponent<{
|
||||||
</div>}
|
</div>}
|
||||||
<ActionList
|
<ActionList
|
||||||
sdkLanguage={sdkLanguage}
|
sdkLanguage={sdkLanguage}
|
||||||
actions={filteredActions}
|
actions={model?.actions || []}
|
||||||
selectedAction={model ? selectedAction : undefined}
|
selectedAction={model ? selectedAction : undefined}
|
||||||
selectedTime={selectedTime}
|
selectedTime={selectedTime}
|
||||||
setSelectedTime={setSelectedTime}
|
setSelectedTime={setSelectedTime}
|
||||||
|
|
@ -311,13 +306,12 @@ export const Workbench: React.FunctionComponent<{
|
||||||
id: 'settings',
|
id: 'settings',
|
||||||
title: 'Settings',
|
title: 'Settings',
|
||||||
component: <SettingsView settings={[
|
component: <SettingsView settings={[
|
||||||
{ value: showRouteActions, set: setShowRouteActions, title: 'Show route actions' },
|
|
||||||
{ value: showScreenshot, set: setShowScreenshot, title: 'Show screenshot instead of snapshot' }
|
{ value: showScreenshot, set: setShowScreenshot, title: 'Show screenshot instead of snapshot' }
|
||||||
]}/>,
|
]}/>,
|
||||||
};
|
};
|
||||||
|
|
||||||
return <div className='vbox workbench' {...(inert ? { inert: 'true' } : {})}>
|
return <div className='vbox workbench' {...(inert ? { inert: 'true' } : {})}>
|
||||||
<Timeline
|
{!hideTimeline && <Timeline
|
||||||
model={model}
|
model={model}
|
||||||
consoleEntries={consoleModel.entries}
|
consoleEntries={consoleModel.entries}
|
||||||
boundaries={boundaries}
|
boundaries={boundaries}
|
||||||
|
|
@ -328,7 +322,7 @@ export const Workbench: React.FunctionComponent<{
|
||||||
sdkLanguage={sdkLanguage}
|
sdkLanguage={sdkLanguage}
|
||||||
selectedTime={selectedTime}
|
selectedTime={selectedTime}
|
||||||
setSelectedTime={setSelectedTime}
|
setSelectedTime={setSelectedTime}
|
||||||
/>
|
/>}
|
||||||
<SplitView
|
<SplitView
|
||||||
sidebarSize={250}
|
sidebarSize={250}
|
||||||
orientation={sidebarLocation === 'bottom' ? 'vertical' : 'horizontal'} settingName='propertiesSidebar'
|
orientation={sidebarLocation === 'bottom' ? 'vertical' : 'horizontal'} settingName='propertiesSidebar'
|
||||||
|
|
|
||||||
1949
tests/bidi/expectations/bidi-firefox-nightly-library.txt
Normal file
1949
tests/bidi/expectations/bidi-firefox-nightly-library.txt
Normal file
File diff suppressed because it is too large
Load diff
1968
tests/bidi/expectations/bidi-firefox-nightly-page.txt
Normal file
1968
tests/bidi/expectations/bidi-firefox-nightly-page.txt
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -21,6 +21,7 @@ import * as playwrightLibrary from 'playwright-core';
|
||||||
|
|
||||||
export type TestModeWorkerOptions = {
|
export type TestModeWorkerOptions = {
|
||||||
mode: TestModeName;
|
mode: TestModeName;
|
||||||
|
codegenMode: 'trace-events' | 'actions';
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TestModeTestFixtures = {
|
export type TestModeTestFixtures = {
|
||||||
|
|
@ -48,6 +49,7 @@ export const testModeTest = test.extend<TestModeTestFixtures, TestModeWorkerOpti
|
||||||
await run(playwright);
|
await run(playwright);
|
||||||
await testMode.teardown();
|
await testMode.teardown();
|
||||||
}, { scope: 'worker' }],
|
}, { scope: 'worker' }],
|
||||||
|
codegenMode: ['actions', { scope: 'worker', option: true }],
|
||||||
|
|
||||||
toImplInWorkerScope: [async ({ playwright }, use) => {
|
toImplInWorkerScope: [async ({ playwright }, use) => {
|
||||||
await use((playwright as any)._toImpl);
|
await use((playwright as any)._toImpl);
|
||||||
|
|
|
||||||
|
|
@ -71,10 +71,12 @@ class TraceViewerPage {
|
||||||
return await this.page.waitForSelector(`.list-view-entry:has-text("${action}") .action-icons`);
|
return await this.page.waitForSelector(`.list-view-entry:has-text("${action}") .action-icons`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@step
|
||||||
async selectAction(title: string, ordinal: number = 0) {
|
async selectAction(title: string, ordinal: number = 0) {
|
||||||
await this.page.locator(`.action-title:has-text("${title}")`).nth(ordinal).click();
|
await this.page.locator(`.action-title:has-text("${title}")`).nth(ordinal).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@step
|
||||||
async selectSnapshot(name: string) {
|
async selectSnapshot(name: string) {
|
||||||
await this.page.click(`.snapshot-tab .tabbed-pane-tab-label:has-text("${name}")`);
|
await this.page.click(`.snapshot-tab .tabbed-pane-tab-label:has-text("${name}")`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -160,7 +160,7 @@ export async function parseTraceRaw(file: string): Promise<{ events: any[], reso
|
||||||
export async function parseTrace(file: string): Promise<{ resources: Map<string, Buffer>, events: (EventTraceEvent | ConsoleMessageTraceEvent)[], actions: ActionTraceEvent[], apiNames: string[], traceModel: TraceModel, model: MultiTraceModel, actionTree: string[], errors: string[] }> {
|
export async function parseTrace(file: string): Promise<{ resources: Map<string, Buffer>, events: (EventTraceEvent | ConsoleMessageTraceEvent)[], actions: ActionTraceEvent[], apiNames: string[], traceModel: TraceModel, model: MultiTraceModel, actionTree: string[], errors: string[] }> {
|
||||||
const backend = new TraceBackend(file);
|
const backend = new TraceBackend(file);
|
||||||
const traceModel = new TraceModel();
|
const traceModel = new TraceModel();
|
||||||
await traceModel.load(backend, () => {});
|
await traceModel.load(backend, false, () => {});
|
||||||
const model = new MultiTraceModel(traceModel.contextEntries);
|
const model = new MultiTraceModel(traceModel.contextEntries);
|
||||||
const { rootItem } = buildActionTree(model.actions);
|
const { rootItem } = buildActionTree(model.actions);
|
||||||
const actionTree: string[] = [];
|
const actionTree: string[] = [];
|
||||||
|
|
|
||||||
|
|
@ -337,7 +337,8 @@ await page.GetByRole(AriaRole.Button, new() { Name = "click me" }).ClickAsync();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should record open in a new tab with url', async ({ openRecorder, browserName }) => {
|
test('should record open in a new tab with url', async ({ openRecorder, browserName, codegenMode }) => {
|
||||||
|
test.skip(codegenMode === 'trace-events');
|
||||||
const { page, recorder } = await openRecorder();
|
const { page, recorder } = await openRecorder();
|
||||||
await recorder.setContentAndWait(`<a href="about:blank?foo">link</a>`);
|
await recorder.setContentAndWait(`<a href="about:blank?foo">link</a>`);
|
||||||
|
|
||||||
|
|
@ -490,7 +491,8 @@ await page1.GotoAsync("about:blank?foo");`);
|
||||||
await recorder.waitForOutput('JavaScript', `await page.goto('${server.PREFIX}/page2.html');`);
|
await recorder.waitForOutput('JavaScript', `await page.goto('${server.PREFIX}/page2.html');`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should --save-trace', async ({ runCLI }, testInfo) => {
|
test('should --save-trace', async ({ runCLI, codegenMode }, testInfo) => {
|
||||||
|
test.skip(codegenMode === 'trace-events');
|
||||||
const traceFileName = testInfo.outputPath('trace.zip');
|
const traceFileName = testInfo.outputPath('trace.zip');
|
||||||
const cli = runCLI([`--save-trace=${traceFileName}`], {
|
const cli = runCLI([`--save-trace=${traceFileName}`], {
|
||||||
autoExitWhen: ' ',
|
autoExitWhen: ' ',
|
||||||
|
|
@ -499,7 +501,8 @@ await page1.GotoAsync("about:blank?foo");`);
|
||||||
expect(fs.existsSync(traceFileName)).toBeTruthy();
|
expect(fs.existsSync(traceFileName)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should save assets via SIGINT', async ({ runCLI, platform }, testInfo) => {
|
test('should save assets via SIGINT', async ({ runCLI, platform, codegenMode }, testInfo) => {
|
||||||
|
test.skip(codegenMode === 'trace-events');
|
||||||
test.skip(platform === 'win32', 'SIGINT not supported on Windows');
|
test.skip(platform === 'win32', 'SIGINT not supported on Windows');
|
||||||
|
|
||||||
const traceFileName = testInfo.outputPath('trace.zip');
|
const traceFileName = testInfo.outputPath('trace.zip');
|
||||||
|
|
|
||||||
|
|
@ -67,17 +67,30 @@ export const test = contextTest.extend<CLITestArgs>({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
runCLI: async ({ childProcess, browserName, channel, headless, mode, launchOptions }, run, testInfo) => {
|
runCLI: async ({ childProcess, browserName, channel, headless, mode, launchOptions, codegenMode }, run, testInfo) => {
|
||||||
testInfo.skip(mode.startsWith('service'));
|
testInfo.skip(mode.startsWith('service'));
|
||||||
|
|
||||||
await run((cliArgs, { autoExitWhen } = {}) => {
|
await run((cliArgs, { autoExitWhen } = {}) => {
|
||||||
return new CLIMock(childProcess, browserName, channel, headless, cliArgs, launchOptions.executablePath, autoExitWhen);
|
return new CLIMock(childProcess, {
|
||||||
|
browserName,
|
||||||
|
channel,
|
||||||
|
headless,
|
||||||
|
args: cliArgs,
|
||||||
|
executablePath: launchOptions.executablePath,
|
||||||
|
autoExitWhen,
|
||||||
|
codegenMode
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
openRecorder: async ({ context, recorderPageGetter }, run) => {
|
openRecorder: async ({ context, recorderPageGetter, codegenMode }, run) => {
|
||||||
await run(async (options?: { testIdAttributeName?: string }) => {
|
await run(async (options?: { testIdAttributeName?: string }) => {
|
||||||
await (context as any)._enableRecorder({ language: 'javascript', mode: 'recording', ...options });
|
await (context as any)._enableRecorder({
|
||||||
|
language: 'javascript',
|
||||||
|
mode: 'recording',
|
||||||
|
codegenMode,
|
||||||
|
...options
|
||||||
|
});
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
return { page, recorder: new Recorder(page, await recorderPageGetter()) };
|
return { page, recorder: new Recorder(page, await recorderPageGetter()) };
|
||||||
});
|
});
|
||||||
|
|
@ -205,23 +218,24 @@ class Recorder {
|
||||||
class CLIMock {
|
class CLIMock {
|
||||||
process: TestChildProcess;
|
process: TestChildProcess;
|
||||||
|
|
||||||
constructor(childProcess: CommonFixtures['childProcess'], browserName: string, channel: string | undefined, headless: boolean | undefined, args: string[], executablePath: string | undefined, autoExitWhen: string | undefined) {
|
constructor(childProcess: CommonFixtures['childProcess'], options: { browserName: string, channel: string | undefined, headless: boolean | undefined, args: string[], executablePath: string | undefined, autoExitWhen: string | undefined, codegenMode?: 'trace-events' | 'actions'}) {
|
||||||
const nodeArgs = [
|
const nodeArgs = [
|
||||||
'node',
|
'node',
|
||||||
path.join(__dirname, '..', '..', '..', 'packages', 'playwright-core', 'cli.js'),
|
path.join(__dirname, '..', '..', '..', 'packages', 'playwright-core', 'cli.js'),
|
||||||
'codegen',
|
'codegen',
|
||||||
...args,
|
...options.args,
|
||||||
`--browser=${browserName}`,
|
`--browser=${options.browserName}`,
|
||||||
];
|
];
|
||||||
if (channel)
|
if (options.channel)
|
||||||
nodeArgs.push(`--channel=${channel}`);
|
nodeArgs.push(`--channel=${options.channel}`);
|
||||||
this.process = childProcess({
|
this.process = childProcess({
|
||||||
command: nodeArgs,
|
command: nodeArgs,
|
||||||
env: {
|
env: {
|
||||||
PWTEST_CLI_AUTO_EXIT_WHEN: autoExitWhen,
|
PW_RECORDER_IS_TRACE_VIEWER: options.codegenMode === 'trace-events' ? '1' : undefined,
|
||||||
|
PWTEST_CLI_AUTO_EXIT_WHEN: options.autoExitWhen,
|
||||||
PWTEST_CLI_IS_UNDER_TEST: '1',
|
PWTEST_CLI_IS_UNDER_TEST: '1',
|
||||||
PWTEST_CLI_HEADLESS: headless ? '1' : undefined,
|
PWTEST_CLI_HEADLESS: options.headless ? '1' : undefined,
|
||||||
PWTEST_CLI_EXECUTABLE_PATH: executablePath,
|
PWTEST_CLI_EXECUTABLE_PATH: options.executablePath,
|
||||||
DEBUG: (process.env.DEBUG ?? '') + ',pw:browser*',
|
DEBUG: (process.env.DEBUG ?? '') + ',pw:browser*',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -107,10 +107,8 @@ for (const browserName of browserNames) {
|
||||||
console.error(`Using executable at ${executablePath}`);
|
console.error(`Using executable at ${executablePath}`);
|
||||||
const devtools = process.env.DEVTOOLS === '1';
|
const devtools = process.env.DEVTOOLS === '1';
|
||||||
const testIgnore: RegExp[] = browserNames.filter(b => b !== browserName).map(b => new RegExp(b));
|
const testIgnore: RegExp[] = browserNames.filter(b => b !== browserName).map(b => new RegExp(b));
|
||||||
for (const folder of ['library', 'page']) {
|
|
||||||
config.projects.push({
|
const projectTemplate: typeof config.projects[0] = {
|
||||||
name: `${browserName}-${folder}`,
|
|
||||||
testDir: path.join(testDir, folder),
|
|
||||||
testIgnore,
|
testIgnore,
|
||||||
snapshotPathTemplate: `{testDir}/{testFileDir}/{testFileName}-snapshots/{arg}-${browserName}{ext}`,
|
snapshotPathTemplate: `{testDir}/{testFileDir}/{testFileName}-snapshots/{arg}-${browserName}{ext}`,
|
||||||
use: {
|
use: {
|
||||||
|
|
@ -141,9 +139,31 @@ for (const browserName of browserNames) {
|
||||||
video: !!video,
|
video: !!video,
|
||||||
trace: !!trace,
|
trace: !!trace,
|
||||||
clock: process.env.PW_CLOCK ? 'clock-' + process.env.PW_CLOCK : undefined,
|
clock: process.env.PW_CLOCK ? 'clock-' + process.env.PW_CLOCK : undefined,
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
config.projects.push({
|
||||||
|
name: `${browserName}-library`,
|
||||||
|
testDir: path.join(testDir, 'library'),
|
||||||
|
...projectTemplate,
|
||||||
|
});
|
||||||
|
|
||||||
|
config.projects.push({
|
||||||
|
name: `${browserName}-page`,
|
||||||
|
testDir: path.join(testDir, 'page'),
|
||||||
|
...projectTemplate,
|
||||||
|
});
|
||||||
|
|
||||||
|
config.projects.push({
|
||||||
|
name: `${browserName}-codegen-mode-trace`,
|
||||||
|
testDir: path.join(testDir, 'library'),
|
||||||
|
testMatch: '**/cli-codegen-*.spec.ts',
|
||||||
|
...projectTemplate,
|
||||||
|
use: {
|
||||||
|
...projectTemplate.use,
|
||||||
|
codegenMode: 'trace-events',
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,9 @@ test('should complain about newer version of trace in old viewer', async ({ show
|
||||||
test('should properly synchronize local and remote time', async ({ showTraceViewer, asset }, testInfo) => {
|
test('should properly synchronize local and remote time', async ({ showTraceViewer, asset }, testInfo) => {
|
||||||
const traceViewer = await showTraceViewer([asset('trace-remote-time-diff.zip')]);
|
const traceViewer = await showTraceViewer([asset('trace-remote-time-diff.zip')]);
|
||||||
// The total duration should be sub 10s, rather than 16h.
|
// The total duration should be sub 10s, rather than 16h.
|
||||||
await expect(traceViewer.page.locator('.timeline-time').last()).toHaveText('8.5s');
|
await expect.poll(async () =>
|
||||||
|
parseInt(await traceViewer.page.locator('.timeline-time').last().innerText(), 10)
|
||||||
|
).toBeLessThan(10);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should contain action info', async ({ showTraceViewer }) => {
|
test('should contain action info', async ({ showTraceViewer }) => {
|
||||||
|
|
@ -964,28 +966,6 @@ test('should open two trace files of the same test', async ({ context, page, req
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should include requestUrl in route.fulfill', async ({ page, runAndTrace, browserName }) => {
|
|
||||||
await page.route('**/*', route => {
|
|
||||||
void route.fulfill({
|
|
||||||
status: 200,
|
|
||||||
headers: {
|
|
||||||
'content-type': 'text/html'
|
|
||||||
},
|
|
||||||
body: 'Hello there!'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
const traceViewer = await runAndTrace(async () => {
|
|
||||||
await page.goto('http://test.com');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Render snapshot, check expectations.
|
|
||||||
await traceViewer.selectAction('route.fulfill');
|
|
||||||
await traceViewer.page.locator('.tabbed-pane-tab-label', { hasText: 'Call' }).click();
|
|
||||||
const callLine = traceViewer.page.locator('.call-line');
|
|
||||||
await expect(callLine.getByText('status')).toContainText('200');
|
|
||||||
await expect(callLine.getByText('requestUrl')).toContainText('http://test.com');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should not crash with broken locator', async ({ page, runAndTrace, server }) => {
|
test('should not crash with broken locator', async ({ page, runAndTrace, server }) => {
|
||||||
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/21832' });
|
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/21832' });
|
||||||
const traceViewer = await runAndTrace(async () => {
|
const traceViewer = await runAndTrace(async () => {
|
||||||
|
|
@ -999,37 +979,6 @@ test('should not crash with broken locator', async ({ page, runAndTrace, server
|
||||||
await expect(header).toBeVisible();
|
await expect(header).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should include requestUrl in route.continue', async ({ page, runAndTrace, server }) => {
|
|
||||||
await page.route('**/*', route => {
|
|
||||||
void route.continue({ url: server.EMPTY_PAGE });
|
|
||||||
});
|
|
||||||
const traceViewer = await runAndTrace(async () => {
|
|
||||||
await page.goto('http://test.com');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Render snapshot, check expectations.
|
|
||||||
await traceViewer.selectAction('route.continue');
|
|
||||||
await traceViewer.page.locator('.tabbed-pane-tab-label', { hasText: 'Call' }).click();
|
|
||||||
const callLine = traceViewer.page.locator('.call-line');
|
|
||||||
await expect(callLine.getByText('requestUrl')).toContainText('http://test.com');
|
|
||||||
await expect(callLine.getByText(/^url:.*/)).toContainText(server.EMPTY_PAGE);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should include requestUrl in route.abort', async ({ page, runAndTrace, server }) => {
|
|
||||||
await page.route('**/*', route => {
|
|
||||||
void route.abort();
|
|
||||||
});
|
|
||||||
const traceViewer = await runAndTrace(async () => {
|
|
||||||
await page.goto('http://test.com').catch(() => {});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Render snapshot, check expectations.
|
|
||||||
await traceViewer.selectAction('route.abort');
|
|
||||||
await traceViewer.page.locator('.tabbed-pane-tab-label', { hasText: 'Call' }).click();
|
|
||||||
const callLine = traceViewer.page.locator('.call-line');
|
|
||||||
await expect(callLine.getByText('requestUrl')).toContainText('http://test.com');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should serve overridden request', async ({ page, runAndTrace, server }) => {
|
test('should serve overridden request', async ({ page, runAndTrace, server }) => {
|
||||||
server.setRoute('/custom.css', (req, res) => {
|
server.setRoute('/custom.css', (req, res) => {
|
||||||
res.writeHead(200, {
|
res.writeHead(200, {
|
||||||
|
|
@ -1416,7 +1365,7 @@ test('should show correct request start time', {
|
||||||
expect(parseMillis(start)).toBeLessThan(1000);
|
expect(parseMillis(start)).toBeLessThan(1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should allow hiding route actions', {
|
test('should not record route actions', {
|
||||||
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30970' },
|
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30970' },
|
||||||
}, async ({ page, runAndTrace, server }) => {
|
}, async ({ page, runAndTrace, server }) => {
|
||||||
const traceViewer = await runAndTrace(async () => {
|
const traceViewer = await runAndTrace(async () => {
|
||||||
|
|
@ -1426,28 +1375,9 @@ test('should allow hiding route actions', {
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Routes are visible by default.
|
|
||||||
await expect(traceViewer.actionTitles).toHaveText([
|
await expect(traceViewer.actionTitles).toHaveText([
|
||||||
/page.route/,
|
/page.route/,
|
||||||
/page.goto.*empty.html/,
|
/page.goto.*empty.html/,
|
||||||
/route.fulfill/,
|
|
||||||
]);
|
|
||||||
|
|
||||||
await traceViewer.page.getByText('Settings').click();
|
|
||||||
await expect(traceViewer.page.getByRole('checkbox', { name: 'Show route actions' })).toBeChecked();
|
|
||||||
await traceViewer.page.getByRole('checkbox', { name: 'Show route actions' }).uncheck();
|
|
||||||
await traceViewer.page.getByText('Actions', { exact: true }).click();
|
|
||||||
await expect(traceViewer.actionTitles).toHaveText([
|
|
||||||
/page.goto.*empty.html/,
|
|
||||||
]);
|
|
||||||
|
|
||||||
await traceViewer.page.getByText('Settings').click();
|
|
||||||
await traceViewer.page.getByRole('checkbox', { name: 'Show route actions' }).check();
|
|
||||||
await traceViewer.page.getByText('Actions', { exact: true }).click();
|
|
||||||
await expect(traceViewer.actionTitles).toHaveText([
|
|
||||||
/page.route/,
|
|
||||||
/page.goto.*empty.html/,
|
|
||||||
/route.fulfill/,
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -66,10 +66,9 @@ test('should collect trace with resources, but no js', async ({ context, page, s
|
||||||
|
|
||||||
test('should use the correct apiName for event driven callbacks', async ({ context, page, server }, testInfo) => {
|
test('should use the correct apiName for event driven callbacks', async ({ context, page, server }, testInfo) => {
|
||||||
await context.tracing.start();
|
await context.tracing.start();
|
||||||
|
// route.* calls should not be included in the trace
|
||||||
await page.route('**/empty.html', route => route.continue());
|
await page.route('**/empty.html', route => route.continue());
|
||||||
// page.goto -> page.route should be included in the trace since its handled.
|
|
||||||
await page.goto(server.PREFIX + '/empty.html');
|
await page.goto(server.PREFIX + '/empty.html');
|
||||||
// page.route -> internalContinue should not be included in the trace since it was handled by Playwright internally.
|
|
||||||
await page.goto(server.PREFIX + '/grid.html');
|
await page.goto(server.PREFIX + '/grid.html');
|
||||||
|
|
||||||
// The default internal dialog handler should not provide an action.
|
// The default internal dialog handler should not provide an action.
|
||||||
|
|
@ -87,7 +86,6 @@ test('should use the correct apiName for event driven callbacks', async ({ conte
|
||||||
expect(actions).toEqual([
|
expect(actions).toEqual([
|
||||||
'page.route',
|
'page.route',
|
||||||
'page.goto',
|
'page.goto',
|
||||||
'route.continue',
|
|
||||||
'page.goto',
|
'page.goto',
|
||||||
'page.evaluate',
|
'page.evaluate',
|
||||||
'page.reload',
|
'page.reload',
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,71 @@ it('clicking checkbox should activate it', async ({ page, browserName, headless,
|
||||||
expect(nodeName).toBe('INPUT');
|
expect(nodeName).toBe('INPUT');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('tab should cycle between single input and browser', {
|
||||||
|
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32339' }
|
||||||
|
}, async ({ page, browserName, headless }) => {
|
||||||
|
it.fixme(browserName === 'chromium' && (!headless || !!process.env.PLAYWRIGHT_CHROMIUM_USE_HEADLESS_NEW),
|
||||||
|
'Chromium in headful mode keeps input focused.');
|
||||||
|
it.fixme(browserName !== 'chromium');
|
||||||
|
await page.setContent(`<label for="input1">input1</label>
|
||||||
|
<input id="input1">
|
||||||
|
<script>
|
||||||
|
{
|
||||||
|
window.focusEvents = [];
|
||||||
|
const input = document.getElementById('input1');
|
||||||
|
input.addEventListener('blur', () => focusEvents.push('blur'));
|
||||||
|
input.addEventListener('focus', () => focusEvents.push('focus'));
|
||||||
|
}
|
||||||
|
</script>`);
|
||||||
|
expect(await page.evaluate(() => document.activeElement.tagName)).toBe('BODY');
|
||||||
|
await page.keyboard.press('Tab');
|
||||||
|
expect(await page.evaluate(() => document.activeElement.id)).toBe('input1');
|
||||||
|
expect(await page.evaluate(() => (window as any).focusEvents)).toEqual(['focus']);
|
||||||
|
await page.keyboard.press('Tab');
|
||||||
|
expect(await page.evaluate(() => document.activeElement.tagName)).toBe('BODY');
|
||||||
|
expect(await page.evaluate(() => (window as any).focusEvents)).toEqual(['focus', 'blur']);
|
||||||
|
await page.keyboard.press('Tab');
|
||||||
|
expect(await page.evaluate(() => document.activeElement.id)).toBe('input1');
|
||||||
|
expect(await page.evaluate(() => (window as any).focusEvents)).toEqual(['focus', 'blur', 'focus']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('tab should cycle between document elements and browser', {
|
||||||
|
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32339' }
|
||||||
|
}, async ({ page, browserName, headless }) => {
|
||||||
|
it.fixme(browserName === 'chromium' && (!headless || !!process.env.PLAYWRIGHT_CHROMIUM_USE_HEADLESS_NEW),
|
||||||
|
'Chromium in headful mode keeps last input focused.');
|
||||||
|
it.fixme(browserName !== 'chromium');
|
||||||
|
await page.setContent(`
|
||||||
|
<input id="input1">
|
||||||
|
<input id="input2">
|
||||||
|
<script>
|
||||||
|
window.focusEvents = [];
|
||||||
|
{
|
||||||
|
const input = document.getElementById('input1');
|
||||||
|
input.addEventListener('blur', () => focusEvents.push('blur1'));
|
||||||
|
input.addEventListener('focus', () => focusEvents.push('focus1'));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const input = document.getElementById('input2');
|
||||||
|
input.addEventListener('blur', () => focusEvents.push('blur2'));
|
||||||
|
input.addEventListener('focus', () => focusEvents.push('focus2'));
|
||||||
|
}
|
||||||
|
</script>`);
|
||||||
|
expect(await page.evaluate(() => document.activeElement.tagName)).toBe('BODY');
|
||||||
|
await page.keyboard.press('Tab');
|
||||||
|
expect(await page.evaluate(() => document.activeElement.id)).toBe('input1');
|
||||||
|
expect(await page.evaluate(() => (window as any).focusEvents)).toEqual(['focus1']);
|
||||||
|
await page.keyboard.press('Tab');
|
||||||
|
expect(await page.evaluate(() => document.activeElement.id)).toBe('input2');
|
||||||
|
expect(await page.evaluate(() => (window as any).focusEvents)).toEqual(['focus1', 'blur1', 'focus2']);
|
||||||
|
await page.keyboard.press('Tab');
|
||||||
|
expect(await page.evaluate(() => document.activeElement.tagName)).toBe('BODY');
|
||||||
|
expect(await page.evaluate(() => (window as any).focusEvents)).toEqual(['focus1', 'blur1', 'focus2', 'blur2']);
|
||||||
|
await page.keyboard.press('Tab');
|
||||||
|
expect(await page.evaluate(() => document.activeElement.id)).toBe('input1');
|
||||||
|
expect(await page.evaluate(() => (window as any).focusEvents)).toEqual(['focus1', 'blur1', 'focus2', 'blur2', 'focus1']);
|
||||||
|
});
|
||||||
|
|
||||||
it('keeps focus on element when attempting to focus a non-focusable element', async ({ page }) => {
|
it('keeps focus on element when attempting to focus a non-focusable element', async ({ page }) => {
|
||||||
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/14254' });
|
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/14254' });
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1216,7 +1216,6 @@ test('should not nest top level expect into unfinished api calls ', {
|
||||||
' browserContext.newPage',
|
' browserContext.newPage',
|
||||||
'page.route',
|
'page.route',
|
||||||
'page.goto',
|
'page.goto',
|
||||||
'route.fetch',
|
|
||||||
'expect.toBeVisible',
|
'expect.toBeVisible',
|
||||||
'page.unrouteAll',
|
'page.unrouteAll',
|
||||||
'After Hooks',
|
'After Hooks',
|
||||||
|
|
|
||||||
|
|
@ -546,37 +546,6 @@ fixture | fixture: browser
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not nest page.continue inside page.goto steps', async ({ runInlineTest }) => {
|
|
||||||
const result = await runInlineTest({
|
|
||||||
'reporter.ts': stepIndentReporter,
|
|
||||||
'playwright.config.ts': `module.exports = { reporter: './reporter', };`,
|
|
||||||
'a.test.ts': `
|
|
||||||
import { test, expect } from '@playwright/test';
|
|
||||||
test('pass', async ({ page }) => {
|
|
||||||
await page.route('**/*', route => route.fulfill('<html></html>'));
|
|
||||||
await page.goto('http://localhost:1234');
|
|
||||||
});
|
|
||||||
`
|
|
||||||
}, { reporter: '' });
|
|
||||||
|
|
||||||
expect(result.exitCode).toBe(0);
|
|
||||||
expect(result.output).toBe(`
|
|
||||||
hook |Before Hooks
|
|
||||||
fixture | fixture: browser
|
|
||||||
pw:api | browserType.launch
|
|
||||||
fixture | fixture: context
|
|
||||||
pw:api | browser.newContext
|
|
||||||
fixture | fixture: page
|
|
||||||
pw:api | browserContext.newPage
|
|
||||||
pw:api |page.route @ a.test.ts:4
|
|
||||||
pw:api |page.goto(http://localhost:1234) @ a.test.ts:5
|
|
||||||
pw:api |route.fulfill @ a.test.ts:4
|
|
||||||
hook |After Hooks
|
|
||||||
fixture | fixture: page
|
|
||||||
fixture | fixture: context
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should not propagate errors from within toPass', async ({ runInlineTest }) => {
|
test('should not propagate errors from within toPass', async ({ runInlineTest }) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'reporter.ts': stepIndentReporter,
|
'reporter.ts': stepIndentReporter,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue