feat(tracing): pack sources to trace on the driver side (#10815)
This commit is contained in:
parent
a52c6219a7
commit
976af162b0
|
|
@ -24,6 +24,7 @@ import { isSafeCloseError, kBrowserClosedError } from '../utils/errors';
|
||||||
import * as api from '../../types/types';
|
import * as api from '../../types/types';
|
||||||
import { CDPSession } from './cdpSession';
|
import { CDPSession } from './cdpSession';
|
||||||
import type { BrowserType } from './browserType';
|
import type { BrowserType } from './browserType';
|
||||||
|
import { LocalUtils } from './localUtils';
|
||||||
|
|
||||||
export class Browser extends ChannelOwner<channels.BrowserChannel> implements api.Browser {
|
export class Browser extends ChannelOwner<channels.BrowserChannel> implements api.Browser {
|
||||||
readonly _contexts = new Set<BrowserContext>();
|
readonly _contexts = new Set<BrowserContext>();
|
||||||
|
|
@ -32,6 +33,7 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> implements ap
|
||||||
_shouldCloseConnectionOnClose = false;
|
_shouldCloseConnectionOnClose = false;
|
||||||
private _browserType!: BrowserType;
|
private _browserType!: BrowserType;
|
||||||
readonly _name: string;
|
readonly _name: string;
|
||||||
|
_localUtils!: LocalUtils;
|
||||||
|
|
||||||
static from(browser: channels.BrowserChannel): Browser {
|
static from(browser: channels.BrowserChannel): Browser {
|
||||||
return (browser as any)._object;
|
return (browser as any)._object;
|
||||||
|
|
@ -62,6 +64,7 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> implements ap
|
||||||
this._contexts.add(context);
|
this._contexts.add(context);
|
||||||
context._logger = options.logger || this._logger;
|
context._logger = options.logger || this._logger;
|
||||||
context._setBrowserType(this._browserType);
|
context._setBrowserType(this._browserType);
|
||||||
|
context._localUtils = this._localUtils;
|
||||||
await this._browserType._onDidCreateContext?.(context);
|
await this._browserType._onDidCreateContext?.(context);
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,12 +38,14 @@ import type { BrowserType } from './browserType';
|
||||||
import { Artifact } from './artifact';
|
import { Artifact } from './artifact';
|
||||||
import { APIRequestContext } from './fetch';
|
import { APIRequestContext } from './fetch';
|
||||||
import { createInstrumentation } from './clientInstrumentation';
|
import { createInstrumentation } from './clientInstrumentation';
|
||||||
|
import { LocalUtils } from './localUtils';
|
||||||
|
|
||||||
export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel> implements api.BrowserContext {
|
export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel> implements api.BrowserContext {
|
||||||
_pages = new Set<Page>();
|
_pages = new Set<Page>();
|
||||||
private _routes: network.RouteHandler[] = [];
|
private _routes: network.RouteHandler[] = [];
|
||||||
readonly _browser: Browser | null = null;
|
readonly _browser: Browser | null = null;
|
||||||
private _browserType: BrowserType | undefined;
|
private _browserType: BrowserType | undefined;
|
||||||
|
_localUtils!: LocalUtils;
|
||||||
readonly _bindings = new Map<string, (source: structs.BindingSource, ...args: any[]) => any>();
|
readonly _bindings = new Map<string, (source: structs.BindingSource, ...args: any[]) => any>();
|
||||||
_timeoutSettings = new TimeoutSettings();
|
_timeoutSettings = new TimeoutSettings();
|
||||||
_ownerPage: Page | undefined;
|
_ownerPage: Page | undefined;
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
|
||||||
const browser = Browser.from((await this._channel.launch(launchOptions)).browser);
|
const browser = Browser.from((await this._channel.launch(launchOptions)).browser);
|
||||||
browser._logger = logger;
|
browser._logger = logger;
|
||||||
browser._setBrowserType(this);
|
browser._setBrowserType(this);
|
||||||
|
browser._localUtils = this._playwright._utils;
|
||||||
return browser;
|
return browser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -108,6 +109,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
|
||||||
context._options = contextParams;
|
context._options = contextParams;
|
||||||
context._logger = logger;
|
context._logger = logger;
|
||||||
context._setBrowserType(this);
|
context._setBrowserType(this);
|
||||||
|
context._localUtils = this._playwright._utils;
|
||||||
await this._onDidCreateContext?.(context);
|
await this._onDidCreateContext?.(context);
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
@ -172,6 +174,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
|
||||||
browser._logger = logger;
|
browser._logger = logger;
|
||||||
browser._shouldCloseConnectionOnClose = true;
|
browser._shouldCloseConnectionOnClose = true;
|
||||||
browser._setBrowserType((playwright as any)[browser._name]);
|
browser._setBrowserType((playwright as any)[browser._name]);
|
||||||
|
browser._localUtils = this._playwright._utils;
|
||||||
browser.on(Events.Browser.Disconnected, closePipe);
|
browser.on(Events.Browser.Disconnected, closePipe);
|
||||||
fulfill(browser);
|
fulfill(browser);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -216,6 +219,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
|
||||||
browser._contexts.add(BrowserContext.from(result.defaultContext));
|
browser._contexts.add(BrowserContext.from(result.defaultContext));
|
||||||
browser._logger = logger;
|
browser._logger = logger;
|
||||||
browser._setBrowserType(this);
|
browser._setBrowserType(this);
|
||||||
|
browser._localUtils = this._playwright._utils;
|
||||||
return browser;
|
return browser;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ import { Artifact } from './artifact';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import { JsonPipe } from './jsonPipe';
|
import { JsonPipe } from './jsonPipe';
|
||||||
import { APIRequestContext } from './fetch';
|
import { APIRequestContext } from './fetch';
|
||||||
|
import { LocalUtils } from './localUtils';
|
||||||
|
|
||||||
class Root extends ChannelOwner<channels.RootChannel> {
|
class Root extends ChannelOwner<channels.RootChannel> {
|
||||||
constructor(connection: Connection) {
|
constructor(connection: Connection) {
|
||||||
|
|
@ -229,6 +230,9 @@ export class Connection extends EventEmitter {
|
||||||
case 'JsonPipe':
|
case 'JsonPipe':
|
||||||
result = new JsonPipe(parent, type, guid, initializer);
|
result = new JsonPipe(parent, type, guid, initializer);
|
||||||
break;
|
break;
|
||||||
|
case 'LocalUtils':
|
||||||
|
result = new LocalUtils(parent, type, guid, initializer);
|
||||||
|
break;
|
||||||
case 'Page':
|
case 'Page':
|
||||||
result = new Page(parent, type, guid, initializer);
|
result = new Page(parent, type, guid, initializer);
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
32
packages/playwright-core/src/client/localUtils.ts
Normal file
32
packages/playwright-core/src/client/localUtils.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as channels from '../protocol/channels';
|
||||||
|
import { ChannelOwner } from './channelOwner';
|
||||||
|
|
||||||
|
export class LocalUtils extends ChannelOwner<channels.LocalUtilsChannel> {
|
||||||
|
static from(channel: channels.LocalUtilsChannel): LocalUtils {
|
||||||
|
return (channel as any)._object;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.LocalUtilsInitializer) {
|
||||||
|
super(parent, type, guid, initializer);
|
||||||
|
}
|
||||||
|
|
||||||
|
async zip(zipFile: string, entries: channels.NameValue[]): Promise<void> {
|
||||||
|
await this._channel.zip({ zipFile, entries });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -25,6 +25,7 @@ import { BrowserType } from './browserType';
|
||||||
import { ChannelOwner } from './channelOwner';
|
import { ChannelOwner } from './channelOwner';
|
||||||
import { Electron } from './electron';
|
import { Electron } from './electron';
|
||||||
import { APIRequest } from './fetch';
|
import { APIRequest } from './fetch';
|
||||||
|
import { LocalUtils } from './localUtils';
|
||||||
import { Selectors, SelectorsOwner } from './selectors';
|
import { Selectors, SelectorsOwner } from './selectors';
|
||||||
import { Size } from './types';
|
import { Size } from './types';
|
||||||
const dnsLookupAsync = util.promisify(dns.lookup);
|
const dnsLookupAsync = util.promisify(dns.lookup);
|
||||||
|
|
@ -49,6 +50,7 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel> {
|
||||||
selectors: Selectors;
|
selectors: Selectors;
|
||||||
readonly request: APIRequest;
|
readonly request: APIRequest;
|
||||||
readonly errors: { TimeoutError: typeof TimeoutError };
|
readonly errors: { TimeoutError: typeof TimeoutError };
|
||||||
|
_utils: LocalUtils;
|
||||||
private _sockets = new Map<string, net.Socket>();
|
private _sockets = new Map<string, net.Socket>();
|
||||||
private _redirectPortForTest: number | undefined;
|
private _redirectPortForTest: number | undefined;
|
||||||
|
|
||||||
|
|
@ -68,6 +70,7 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel> {
|
||||||
this.devices[name] = descriptor;
|
this.devices[name] = descriptor;
|
||||||
this.selectors = new Selectors();
|
this.selectors = new Selectors();
|
||||||
this.errors = { TimeoutError };
|
this.errors = { TimeoutError };
|
||||||
|
this._utils = LocalUtils.from(initializer.utils);
|
||||||
|
|
||||||
const selectorsOwner = SelectorsOwner.from(initializer.selectors);
|
const selectorsOwner = SelectorsOwner.from(initializer.selectors);
|
||||||
this.selectors._addChannel(selectorsOwner);
|
this.selectors._addChannel(selectorsOwner);
|
||||||
|
|
|
||||||
|
|
@ -16,17 +16,11 @@
|
||||||
|
|
||||||
import * as api from '../../types/types';
|
import * as api from '../../types/types';
|
||||||
import * as channels from '../protocol/channels';
|
import * as channels from '../protocol/channels';
|
||||||
|
import { ParsedStackTrace } from '../utils/stackTrace';
|
||||||
|
import { calculateSha1 } from '../utils/utils';
|
||||||
import { Artifact } from './artifact';
|
import { Artifact } from './artifact';
|
||||||
import { BrowserContext } from './browserContext';
|
import { BrowserContext } from './browserContext';
|
||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
import yauzl from 'yauzl';
|
|
||||||
import yazl from 'yazl';
|
|
||||||
import { assert, calculateSha1 } from '../utils/utils';
|
|
||||||
import { ManualPromise } from '../utils/async';
|
|
||||||
import EventEmitter from 'events';
|
|
||||||
import { ClientInstrumentationListener } from './clientInstrumentation';
|
import { ClientInstrumentationListener } from './clientInstrumentation';
|
||||||
import { ParsedStackTrace } from '../utils/stackTrace';
|
|
||||||
|
|
||||||
export class Tracing implements api.Tracing {
|
export class Tracing implements api.Tracing {
|
||||||
private _context: BrowserContext;
|
private _context: BrowserContext;
|
||||||
|
|
@ -72,80 +66,26 @@ export class Tracing implements api.Tracing {
|
||||||
const sources = this._sources;
|
const sources = this._sources;
|
||||||
this._sources = new Set();
|
this._sources = new Set();
|
||||||
this._context._instrumentation!.removeListener(this._instrumentationListener);
|
this._context._instrumentation!.removeListener(this._instrumentationListener);
|
||||||
const skipCompress = !this._context._connection.isRemote();
|
const isLocal = !this._context._connection.isRemote();
|
||||||
const result = await channel.tracingStopChunk({ save: !!filePath, skipCompress });
|
|
||||||
|
const result = await channel.tracingStopChunk({ save: !!filePath, skipCompress: isLocal });
|
||||||
if (!filePath) {
|
if (!filePath) {
|
||||||
// Not interested in artifacts.
|
// Not interested in artifacts.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we don't have anything locally and we run against remote Playwright, compress on remote side.
|
const sourceEntries: channels.NameValue[] = [];
|
||||||
if (!skipCompress && !sources) {
|
for (const value of sources)
|
||||||
|
sourceEntries.push({ name: 'resources/src@' + calculateSha1(value) + '.txt', value });
|
||||||
|
|
||||||
|
if (!isLocal) {
|
||||||
|
// We run against remote Playwright, compress on remote side.
|
||||||
const artifact = Artifact.from(result.artifact!);
|
const artifact = Artifact.from(result.artifact!);
|
||||||
await artifact.saveAs(filePath);
|
await artifact.saveAs(filePath);
|
||||||
await artifact.delete();
|
await artifact.delete();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We either have sources to append or we were running locally, compress on client side
|
if (isLocal || sourceEntries)
|
||||||
|
await this._context._localUtils.zip(filePath, sourceEntries.concat(result.entries));
|
||||||
const promise = new ManualPromise<void>();
|
|
||||||
const zipFile = new yazl.ZipFile();
|
|
||||||
(zipFile as any as EventEmitter).on('error', error => promise.reject(error));
|
|
||||||
|
|
||||||
// Add sources.
|
|
||||||
if (sources) {
|
|
||||||
for (const source of sources) {
|
|
||||||
try {
|
|
||||||
if (fs.statSync(source).isFile())
|
|
||||||
zipFile.addFile(source, 'resources/src@' + calculateSha1(source) + '.txt');
|
|
||||||
} catch (e) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
|
|
||||||
if (skipCompress) {
|
|
||||||
// Local scenario, compress the entries.
|
|
||||||
for (const entry of result.entries!)
|
|
||||||
zipFile.addFile(entry.value, entry.name);
|
|
||||||
zipFile.end(undefined, () => {
|
|
||||||
zipFile.outputStream.pipe(fs.createWriteStream(filePath)).on('close', () => promise.resolve());
|
|
||||||
});
|
|
||||||
return promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remote scenario, repack.
|
|
||||||
const artifact = Artifact.from(result.artifact!);
|
|
||||||
const tmpPath = filePath! + '.tmp';
|
|
||||||
await artifact.saveAs(tmpPath);
|
|
||||||
await artifact.delete();
|
|
||||||
|
|
||||||
yauzl.open(tmpPath!, (err, inZipFile) => {
|
|
||||||
if (err) {
|
|
||||||
promise.reject(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
assert(inZipFile);
|
|
||||||
let pendingEntries = inZipFile.entryCount;
|
|
||||||
inZipFile.on('entry', entry => {
|
|
||||||
inZipFile.openReadStream(entry, (err, readStream) => {
|
|
||||||
if (err) {
|
|
||||||
promise.reject(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
zipFile.addReadStream(readStream!, entry.fileName);
|
|
||||||
if (--pendingEntries === 0) {
|
|
||||||
zipFile.end(undefined, () => {
|
|
||||||
zipFile.outputStream.pipe(fs.createWriteStream(filePath)).on('close', () => {
|
|
||||||
fs.promises.unlink(tmpPath).then(() => {
|
|
||||||
promise.resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return promise;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the 'License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import EventEmitter from 'events';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import yauzl from 'yauzl';
|
||||||
|
import yazl from 'yazl';
|
||||||
|
import * as channels from '../protocol/channels';
|
||||||
|
import { ManualPromise } from '../utils/async';
|
||||||
|
import { assert, createGuid } from '../utils/utils';
|
||||||
|
import { Dispatcher, DispatcherScope } from './dispatcher';
|
||||||
|
|
||||||
|
export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.LocalUtilsChannel> implements channels.LocalUtilsChannel {
|
||||||
|
_type_LocalUtils: boolean;
|
||||||
|
constructor(scope: DispatcherScope) {
|
||||||
|
super(scope, { guid: 'localUtils@' + createGuid() }, 'LocalUtils', {});
|
||||||
|
this._type_LocalUtils = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async zip(params: channels.LocalUtilsZipParams, metadata?: channels.Metadata): Promise<void> {
|
||||||
|
const promise = new ManualPromise<void>();
|
||||||
|
const zipFile = new yazl.ZipFile();
|
||||||
|
(zipFile as any as EventEmitter).on('error', error => promise.reject(error));
|
||||||
|
|
||||||
|
for (const entry of params.entries) {
|
||||||
|
try {
|
||||||
|
if (fs.statSync(entry.value).isFile())
|
||||||
|
zipFile.addFile(entry.value, entry.name);
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(params.zipFile)) {
|
||||||
|
// New file, just compress the entries.
|
||||||
|
await fs.promises.mkdir(path.dirname(params.zipFile), { recursive: true });
|
||||||
|
zipFile.end(undefined, () => {
|
||||||
|
zipFile.outputStream.pipe(fs.createWriteStream(params.zipFile)).on('close', () => promise.resolve());
|
||||||
|
});
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
// File already exists. Repack and add new entries.
|
||||||
|
const tempFile = params.zipFile + '.tmp';
|
||||||
|
await fs.promises.rename(params.zipFile, tempFile);
|
||||||
|
|
||||||
|
yauzl.open(tempFile, (err, inZipFile) => {
|
||||||
|
if (err) {
|
||||||
|
promise.reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
assert(inZipFile);
|
||||||
|
let pendingEntries = inZipFile.entryCount;
|
||||||
|
inZipFile.on('entry', entry => {
|
||||||
|
inZipFile.openReadStream(entry, (err, readStream) => {
|
||||||
|
if (err) {
|
||||||
|
promise.reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
zipFile.addReadStream(readStream!, entry.fileName);
|
||||||
|
if (--pendingEntries === 0) {
|
||||||
|
zipFile.end(undefined, () => {
|
||||||
|
zipFile.outputStream.pipe(fs.createWriteStream(params.zipFile)).on('close', () => {
|
||||||
|
fs.promises.unlink(tempFile).then(() => {
|
||||||
|
promise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -26,6 +26,7 @@ import { AndroidDispatcher } from './androidDispatcher';
|
||||||
import { BrowserTypeDispatcher } from './browserTypeDispatcher';
|
import { BrowserTypeDispatcher } from './browserTypeDispatcher';
|
||||||
import { Dispatcher, DispatcherScope } from './dispatcher';
|
import { Dispatcher, DispatcherScope } from './dispatcher';
|
||||||
import { ElectronDispatcher } from './electronDispatcher';
|
import { ElectronDispatcher } from './electronDispatcher';
|
||||||
|
import { LocalUtilsDispatcher } from './localUtilsDispatcher';
|
||||||
import { APIRequestContextDispatcher } from './networkDispatchers';
|
import { APIRequestContextDispatcher } from './networkDispatchers';
|
||||||
import { SelectorsDispatcher } from './selectorsDispatcher';
|
import { SelectorsDispatcher } from './selectorsDispatcher';
|
||||||
|
|
||||||
|
|
@ -43,6 +44,7 @@ export class PlaywrightDispatcher extends Dispatcher<Playwright, channels.Playwr
|
||||||
webkit: new BrowserTypeDispatcher(scope, playwright.webkit),
|
webkit: new BrowserTypeDispatcher(scope, playwright.webkit),
|
||||||
android: new AndroidDispatcher(scope, playwright.android),
|
android: new AndroidDispatcher(scope, playwright.android),
|
||||||
electron: new ElectronDispatcher(scope, playwright.electron),
|
electron: new ElectronDispatcher(scope, playwright.electron),
|
||||||
|
utils: new LocalUtilsDispatcher(scope),
|
||||||
deviceDescriptors,
|
deviceDescriptors,
|
||||||
selectors: customSelectors || new SelectorsDispatcher(scope, playwright.selectors),
|
selectors: customSelectors || new SelectorsDispatcher(scope, playwright.selectors),
|
||||||
preLaunchedBrowser,
|
preLaunchedBrowser,
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ export type InitializerTraits<T> =
|
||||||
T extends SelectorsChannel ? SelectorsInitializer :
|
T extends SelectorsChannel ? SelectorsInitializer :
|
||||||
T extends PlaywrightChannel ? PlaywrightInitializer :
|
T extends PlaywrightChannel ? PlaywrightInitializer :
|
||||||
T extends RootChannel ? RootInitializer :
|
T extends RootChannel ? RootInitializer :
|
||||||
|
T extends LocalUtilsChannel ? LocalUtilsInitializer :
|
||||||
T extends APIRequestContextChannel ? APIRequestContextInitializer :
|
T extends APIRequestContextChannel ? APIRequestContextInitializer :
|
||||||
object;
|
object;
|
||||||
|
|
||||||
|
|
@ -84,6 +85,7 @@ export type EventsTraits<T> =
|
||||||
T extends SelectorsChannel ? SelectorsEvents :
|
T extends SelectorsChannel ? SelectorsEvents :
|
||||||
T extends PlaywrightChannel ? PlaywrightEvents :
|
T extends PlaywrightChannel ? PlaywrightEvents :
|
||||||
T extends RootChannel ? RootEvents :
|
T extends RootChannel ? RootEvents :
|
||||||
|
T extends LocalUtilsChannel ? LocalUtilsEvents :
|
||||||
T extends APIRequestContextChannel ? APIRequestContextEvents :
|
T extends APIRequestContextChannel ? APIRequestContextEvents :
|
||||||
undefined;
|
undefined;
|
||||||
|
|
||||||
|
|
@ -117,6 +119,7 @@ export type EventTargetTraits<T> =
|
||||||
T extends SelectorsChannel ? SelectorsEventTarget :
|
T extends SelectorsChannel ? SelectorsEventTarget :
|
||||||
T extends PlaywrightChannel ? PlaywrightEventTarget :
|
T extends PlaywrightChannel ? PlaywrightEventTarget :
|
||||||
T extends RootChannel ? RootEventTarget :
|
T extends RootChannel ? RootEventTarget :
|
||||||
|
T extends LocalUtilsChannel ? LocalUtilsEventTarget :
|
||||||
T extends APIRequestContextChannel ? APIRequestContextEventTarget :
|
T extends APIRequestContextChannel ? APIRequestContextEventTarget :
|
||||||
undefined;
|
undefined;
|
||||||
|
|
||||||
|
|
@ -346,6 +349,26 @@ export type APIResponse = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LifecycleEvent = 'load' | 'domcontentloaded' | 'networkidle' | 'commit';
|
export type LifecycleEvent = 'load' | 'domcontentloaded' | 'networkidle' | 'commit';
|
||||||
|
// ----------- LocalUtils -----------
|
||||||
|
export type LocalUtilsInitializer = {};
|
||||||
|
export interface LocalUtilsEventTarget {
|
||||||
|
}
|
||||||
|
export interface LocalUtilsChannel extends LocalUtilsEventTarget, Channel {
|
||||||
|
_type_LocalUtils: boolean;
|
||||||
|
zip(params: LocalUtilsZipParams, metadata?: Metadata): Promise<LocalUtilsZipResult>;
|
||||||
|
}
|
||||||
|
export type LocalUtilsZipParams = {
|
||||||
|
zipFile: string,
|
||||||
|
entries: NameValue[],
|
||||||
|
};
|
||||||
|
export type LocalUtilsZipOptions = {
|
||||||
|
|
||||||
|
};
|
||||||
|
export type LocalUtilsZipResult = void;
|
||||||
|
|
||||||
|
export interface LocalUtilsEvents {
|
||||||
|
}
|
||||||
|
|
||||||
// ----------- Root -----------
|
// ----------- Root -----------
|
||||||
export type RootInitializer = {};
|
export type RootInitializer = {};
|
||||||
export interface RootEventTarget {
|
export interface RootEventTarget {
|
||||||
|
|
@ -374,6 +397,7 @@ export type PlaywrightInitializer = {
|
||||||
webkit: BrowserTypeChannel,
|
webkit: BrowserTypeChannel,
|
||||||
android: AndroidChannel,
|
android: AndroidChannel,
|
||||||
electron: ElectronChannel,
|
electron: ElectronChannel,
|
||||||
|
utils: LocalUtilsChannel,
|
||||||
deviceDescriptors: {
|
deviceDescriptors: {
|
||||||
name: string,
|
name: string,
|
||||||
descriptor: {
|
descriptor: {
|
||||||
|
|
|
||||||
|
|
@ -415,6 +415,18 @@ ContextOptions:
|
||||||
path: string
|
path: string
|
||||||
strictSelectors: boolean?
|
strictSelectors: boolean?
|
||||||
|
|
||||||
|
LocalUtils:
|
||||||
|
type: interface
|
||||||
|
|
||||||
|
commands:
|
||||||
|
|
||||||
|
zip:
|
||||||
|
parameters:
|
||||||
|
zipFile: string
|
||||||
|
entries:
|
||||||
|
type: array
|
||||||
|
items: NameValue
|
||||||
|
|
||||||
Root:
|
Root:
|
||||||
type: interface
|
type: interface
|
||||||
|
|
||||||
|
|
@ -435,6 +447,7 @@ Playwright:
|
||||||
webkit: BrowserType
|
webkit: BrowserType
|
||||||
android: Android
|
android: Android
|
||||||
electron: Electron
|
electron: Electron
|
||||||
|
utils: LocalUtils
|
||||||
deviceDescriptors:
|
deviceDescriptors:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
|
|
|
||||||
|
|
@ -189,6 +189,10 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
||||||
headers: tArray(tType('NameValue')),
|
headers: tArray(tType('NameValue')),
|
||||||
});
|
});
|
||||||
scheme.LifecycleEvent = tEnum(['load', 'domcontentloaded', 'networkidle', 'commit']);
|
scheme.LifecycleEvent = tEnum(['load', 'domcontentloaded', 'networkidle', 'commit']);
|
||||||
|
scheme.LocalUtilsZipParams = tObject({
|
||||||
|
zipFile: tString,
|
||||||
|
entries: tArray(tType('NameValue')),
|
||||||
|
});
|
||||||
scheme.RootInitializeParams = tObject({
|
scheme.RootInitializeParams = tObject({
|
||||||
sdkLanguage: tString,
|
sdkLanguage: tString,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,12 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { playwrightTest as test, expect } from './config/browserTest';
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { getUserAgent } from 'playwright-core/lib/utils/utils';
|
import { getUserAgent } from 'playwright-core/lib/utils/utils';
|
||||||
import WebSocket from 'ws';
|
import WebSocket from 'ws';
|
||||||
import { suppressCertificateWarning } from './config/utils';
|
import { expect, playwrightTest as test } from './config/browserTest';
|
||||||
|
import { parseTrace, suppressCertificateWarning } from './config/utils';
|
||||||
|
|
||||||
test.slow(true, 'All connect tests are slow');
|
test.slow(true, 'All connect tests are slow');
|
||||||
|
|
||||||
|
|
@ -531,3 +531,26 @@ test('should save har', async ({ browserType, startRemoteServer, server }, testI
|
||||||
expect(entry.pageref).toBe(log.pages[0].id);
|
expect(entry.pageref).toBe(log.pages[0].id);
|
||||||
expect(entry.request.url).toBe(server.EMPTY_PAGE);
|
expect(entry.request.url).toBe(server.EMPTY_PAGE);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should record trace with sources', async ({ browserType, startRemoteServer, server }, testInfo) => {
|
||||||
|
const remoteServer = await startRemoteServer();
|
||||||
|
const browser = await browserType.connect(remoteServer.wsEndpoint());
|
||||||
|
const context = await browser.newContext();
|
||||||
|
const page = await context.newPage();
|
||||||
|
|
||||||
|
await context.tracing.start({ sources: true });
|
||||||
|
await page.goto(server.EMPTY_PAGE);
|
||||||
|
await page.setContent('<button>Click</button>');
|
||||||
|
await page.click('"Click"');
|
||||||
|
await context.tracing.stop({ path: testInfo.outputPath('trace1.zip') });
|
||||||
|
|
||||||
|
await context.close();
|
||||||
|
await browser.close();
|
||||||
|
|
||||||
|
const { resources } = await parseTrace(testInfo.outputPath('trace1.zip'));
|
||||||
|
const sourceNames = Array.from(resources.keys()).filter(k => k.endsWith('.txt'));
|
||||||
|
expect(sourceNames.length).toBe(1);
|
||||||
|
const sourceFile = resources.get(sourceNames[0]);
|
||||||
|
const thisFile = await fs.promises.readFile(__filename);
|
||||||
|
expect(sourceFile).toEqual(thisFile);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ it('should scope context handles', async ({ browserType, server }) => {
|
||||||
{ _guid: 'browser', objects: [] }
|
{ _guid: 'browser', objects: [] }
|
||||||
] },
|
] },
|
||||||
{ _guid: 'electron', objects: [] },
|
{ _guid: 'electron', objects: [] },
|
||||||
|
{ _guid: 'localUtils', objects: [] },
|
||||||
{ _guid: 'Playwright', objects: [] },
|
{ _guid: 'Playwright', objects: [] },
|
||||||
{ _guid: 'selectors', objects: [] },
|
{ _guid: 'selectors', objects: [] },
|
||||||
]
|
]
|
||||||
|
|
@ -65,6 +66,7 @@ it('should scope context handles', async ({ browserType, server }) => {
|
||||||
] },
|
] },
|
||||||
] },
|
] },
|
||||||
{ _guid: 'electron', objects: [] },
|
{ _guid: 'electron', objects: [] },
|
||||||
|
{ _guid: 'localUtils', objects: [] },
|
||||||
{ _guid: 'Playwright', objects: [] },
|
{ _guid: 'Playwright', objects: [] },
|
||||||
{ _guid: 'selectors', objects: [] },
|
{ _guid: 'selectors', objects: [] },
|
||||||
]
|
]
|
||||||
|
|
@ -89,6 +91,7 @@ it('should scope CDPSession handles', async ({ browserType, browserName }) => {
|
||||||
{ _guid: 'browser', objects: [] }
|
{ _guid: 'browser', objects: [] }
|
||||||
] },
|
] },
|
||||||
{ _guid: 'electron', objects: [] },
|
{ _guid: 'electron', objects: [] },
|
||||||
|
{ _guid: 'localUtils', objects: [] },
|
||||||
{ _guid: 'Playwright', objects: [] },
|
{ _guid: 'Playwright', objects: [] },
|
||||||
{ _guid: 'selectors', objects: [] },
|
{ _guid: 'selectors', objects: [] },
|
||||||
]
|
]
|
||||||
|
|
@ -108,6 +111,7 @@ it('should scope CDPSession handles', async ({ browserType, browserName }) => {
|
||||||
] },
|
] },
|
||||||
] },
|
] },
|
||||||
{ _guid: 'electron', objects: [] },
|
{ _guid: 'electron', objects: [] },
|
||||||
|
{ _guid: 'localUtils', objects: [] },
|
||||||
{ _guid: 'Playwright', objects: [] },
|
{ _guid: 'Playwright', objects: [] },
|
||||||
{ _guid: 'selectors', objects: [] },
|
{ _guid: 'selectors', objects: [] },
|
||||||
]
|
]
|
||||||
|
|
@ -128,6 +132,7 @@ it('should scope browser handles', async ({ browserType }) => {
|
||||||
{ _guid: 'browser-type', objects: [] },
|
{ _guid: 'browser-type', objects: [] },
|
||||||
{ _guid: 'browser-type', objects: [] },
|
{ _guid: 'browser-type', objects: [] },
|
||||||
{ _guid: 'electron', objects: [] },
|
{ _guid: 'electron', objects: [] },
|
||||||
|
{ _guid: 'localUtils', objects: [] },
|
||||||
{ _guid: 'Playwright', objects: [] },
|
{ _guid: 'Playwright', objects: [] },
|
||||||
{ _guid: 'selectors', objects: [] },
|
{ _guid: 'selectors', objects: [] },
|
||||||
]
|
]
|
||||||
|
|
@ -152,6 +157,7 @@ it('should scope browser handles', async ({ browserType }) => {
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{ _guid: 'electron', objects: [] },
|
{ _guid: 'electron', objects: [] },
|
||||||
|
{ _guid: 'localUtils', objects: [] },
|
||||||
{ _guid: 'Playwright', objects: [] },
|
{ _guid: 'Playwright', objects: [] },
|
||||||
{ _guid: 'selectors', objects: [] },
|
{ _guid: 'selectors', objects: [] },
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
import { expect } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
import type { Frame, Page } from 'playwright-core';
|
import type { Frame, Page } from 'playwright-core';
|
||||||
|
import { ZipFileSystem } from '../../packages/playwright-core/lib/utils/vfs';
|
||||||
|
|
||||||
export async function attachFrame(page: Page, frameId: string, url: string): Promise<Frame> {
|
export async function attachFrame(page: Page, frameId: string, url: string): Promise<Frame> {
|
||||||
const handle = await page.evaluateHandle(async ({ frameId, url }) => {
|
const handle = await page.evaluateHandle(async ({ frameId, url }) => {
|
||||||
|
|
@ -87,4 +88,26 @@ export function suppressCertificateWarning() {
|
||||||
}
|
}
|
||||||
return originalEmitWarning.call(process, warning, ...args);
|
return originalEmitWarning.call(process, warning, ...args);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function parseTrace(file: string): Promise<{ events: any[], resources: Map<string, Buffer> }> {
|
||||||
|
const zipFS = new ZipFileSystem(file);
|
||||||
|
const resources = new Map<string, Buffer>();
|
||||||
|
for (const entry of await zipFS.entries())
|
||||||
|
resources.set(entry, await zipFS.read(entry));
|
||||||
|
zipFS.close();
|
||||||
|
|
||||||
|
const events = [];
|
||||||
|
for (const line of resources.get('trace.trace').toString().split('\n')) {
|
||||||
|
if (line)
|
||||||
|
events.push(JSON.parse(line));
|
||||||
|
}
|
||||||
|
for (const line of resources.get('trace.network').toString().split('\n')) {
|
||||||
|
if (line)
|
||||||
|
events.push(JSON.parse(line));
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
events,
|
||||||
|
resources,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,11 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { expect, contextTest as test, browserTest } from './config/browserTest';
|
import fs from 'fs';
|
||||||
import { ZipFileSystem } from '../packages/playwright-core/lib/utils/vfs';
|
|
||||||
import jpeg from 'jpeg-js';
|
import jpeg from 'jpeg-js';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import { browserTest, contextTest as test, expect } from './config/browserTest';
|
||||||
|
import { parseTrace } from './config/utils';
|
||||||
|
|
||||||
test.skip(({ trace }) => trace === 'on');
|
test.skip(({ trace }) => trace === 'on');
|
||||||
|
|
||||||
|
|
@ -131,6 +132,21 @@ test('should collect two traces', async ({ context, page, server }, testInfo) =>
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should collect sources', async ({ context, page, server }, testInfo) => {
|
||||||
|
await context.tracing.start({ sources: true });
|
||||||
|
await page.goto(server.EMPTY_PAGE);
|
||||||
|
await page.setContent('<button>Click</button>');
|
||||||
|
await page.click('"Click"');
|
||||||
|
await context.tracing.stop({ path: testInfo.outputPath('trace1.zip') });
|
||||||
|
|
||||||
|
const { resources } = await parseTrace(testInfo.outputPath('trace1.zip'));
|
||||||
|
const sourceNames = Array.from(resources.keys()).filter(k => k.endsWith('.txt'));
|
||||||
|
expect(sourceNames.length).toBe(1);
|
||||||
|
const sourceFile = resources.get(sourceNames[0]);
|
||||||
|
const thisFile = await fs.promises.readFile(__filename);
|
||||||
|
expect(sourceFile).toEqual(thisFile);
|
||||||
|
});
|
||||||
|
|
||||||
test('should not stall on dialogs', async ({ page, context, server }) => {
|
test('should not stall on dialogs', async ({ page, context, server }) => {
|
||||||
await context.tracing.start({ screenshots: true, snapshots: true });
|
await context.tracing.start({ screenshots: true, snapshots: true });
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
|
|
@ -342,28 +358,6 @@ test('should hide internal stack frames in expect', async ({ context, page }, te
|
||||||
expect(relativeStack(action)).toEqual(['tracing.spec.ts']);
|
expect(relativeStack(action)).toEqual(['tracing.spec.ts']);
|
||||||
});
|
});
|
||||||
|
|
||||||
async function parseTrace(file: string): Promise<{ events: any[], resources: Map<string, Buffer> }> {
|
|
||||||
const zipFS = new ZipFileSystem(file);
|
|
||||||
const resources = new Map<string, Buffer>();
|
|
||||||
for (const entry of await zipFS.entries())
|
|
||||||
resources.set(entry, await zipFS.read(entry));
|
|
||||||
zipFS.close();
|
|
||||||
|
|
||||||
const events = [];
|
|
||||||
for (const line of resources.get('trace.trace').toString().split('\n')) {
|
|
||||||
if (line)
|
|
||||||
events.push(JSON.parse(line));
|
|
||||||
}
|
|
||||||
for (const line of resources.get('trace.network').toString().split('\n')) {
|
|
||||||
if (line)
|
|
||||||
events.push(JSON.parse(line));
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
events,
|
|
||||||
resources,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function expectRed(pixels: Buffer, offset: number) {
|
function expectRed(pixels: Buffer, offset: number) {
|
||||||
const r = pixels.readUInt8(offset);
|
const r = pixels.readUInt8(offset);
|
||||||
const g = pixels.readUInt8(offset + 1);
|
const g = pixels.readUInt8(offset + 1);
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ async function innerCheckDeps(root, checkDepsFile) {
|
||||||
}
|
}
|
||||||
const importPath = path.resolve(path.dirname(fileName), importName) + '.ts';
|
const importPath = path.resolve(path.dirname(fileName), importName) + '.ts';
|
||||||
if (checkDepsFile && !allowImport(fileName, importPath))
|
if (checkDepsFile && !allowImport(fileName, importPath))
|
||||||
errors.push(`Disallowed import from ${path.relative(root, fileName)} to ${path.relative(root, importPath)}`);
|
errors.push(`Disallowed import ${path.relative(root, importPath)} in ${path.relative(root, fileName)}`);
|
||||||
if (checkDepsFile && !allowExternalImport(fileName, importPath, importName))
|
if (checkDepsFile && !allowExternalImport(fileName, importPath, importName))
|
||||||
errors.push(`Disallowed external dependency ${importName} from ${path.relative(root, fileName)}`);
|
errors.push(`Disallowed external dependency ${importName} from ${path.relative(root, fileName)}`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue