mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: introduce "instrumentation" that is used for debug and trace (#3775)
This commit is contained in:
parent
25fe115719
commit
675ce00432
61
src/debug/debugController.ts
Normal file
61
src/debug/debugController.ts
Normal file
@ -0,0 +1,61 @@
|
||||
/**
|
||||
* 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 { BrowserContext } from '../server/browserContext';
|
||||
import * as frames from '../server/frames';
|
||||
import * as js from '../server/javascript';
|
||||
import { Page } from '../server/page';
|
||||
import { InstrumentingAgent } from '../server/instrumentation';
|
||||
import type DebugScript from './injected/debugScript';
|
||||
import { Progress } from '../server/progress';
|
||||
import { isDebugMode } from '../utils/utils';
|
||||
import * as debugScriptSource from '../generated/debugScriptSource';
|
||||
|
||||
const debugScriptSymbol = Symbol('debugScript');
|
||||
|
||||
export class DebugController implements InstrumentingAgent {
|
||||
private async ensureInstalledInFrame(frame: frames.Frame) {
|
||||
try {
|
||||
const mainContext = await frame._mainContext();
|
||||
if ((mainContext as any)[debugScriptSymbol])
|
||||
return;
|
||||
(mainContext as any)[debugScriptSymbol] = true;
|
||||
const objectId = await mainContext._delegate.rawEvaluate(`new (${debugScriptSource.source})()`);
|
||||
const debugScript = new js.JSHandle(mainContext, 'object', objectId) as js.JSHandle<DebugScript>;
|
||||
const injectedScript = await mainContext.injectedScript();
|
||||
await debugScript.evaluate((debugScript, injectedScript) => {
|
||||
debugScript.initialize(injectedScript, { console: true });
|
||||
}, injectedScript);
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
async onContextCreated(context: BrowserContext): Promise<void> {
|
||||
if (!isDebugMode())
|
||||
return;
|
||||
context.on(BrowserContext.Events.Page, (page: Page) => {
|
||||
for (const frame of page.frames())
|
||||
this.ensureInstalledInFrame(frame);
|
||||
page.on(Page.Events.FrameNavigated, frame => this.ensureInstalledInFrame(frame));
|
||||
});
|
||||
}
|
||||
|
||||
async onContextDestroyed(context: BrowserContext): Promise<void> {
|
||||
}
|
||||
|
||||
async onBeforePageAction(page: Page, progress: Progress): Promise<void> {
|
||||
}
|
||||
}
|
||||
@ -14,8 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ParsedSelector, parseSelector } from '../../common/selectorParser';
|
||||
import type InjectedScript from '../../injected/injectedScript';
|
||||
import { ParsedSelector, parseSelector } from '../../server/common/selectorParser';
|
||||
import type InjectedScript from '../../server/injected/injectedScript';
|
||||
import { html } from './html';
|
||||
|
||||
export class ConsoleAPI {
|
||||
@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { ConsoleAPI } from './consoleApi';
|
||||
import type InjectedScript from '../../injected/injectedScript';
|
||||
import type InjectedScript from '../../server/injected/injectedScript';
|
||||
|
||||
export default class DebugScript {
|
||||
consoleAPI: ConsoleAPI | undefined;
|
||||
@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const InlineSource = require('../../injected/webpack-inline-source-plugin');
|
||||
const InlineSource = require('../../server/injected/webpack-inline-source-plugin');
|
||||
|
||||
module.exports = {
|
||||
entry: path.join(__dirname, 'debugScript.ts'),
|
||||
@ -38,9 +38,9 @@ module.exports = {
|
||||
output: {
|
||||
libraryTarget: 'var',
|
||||
filename: 'debugScriptSource.js',
|
||||
path: path.resolve(__dirname, '../../../../lib/server/injected/packed')
|
||||
path: path.resolve(__dirname, '../../../lib/server/injected/packed')
|
||||
},
|
||||
plugins: [
|
||||
new InlineSource(path.join(__dirname, '..', '..', '..', 'generated', 'debugScriptSource.ts')),
|
||||
new InlineSource(path.join(__dirname, '..', '..', 'generated', 'debugScriptSource.ts')),
|
||||
]
|
||||
};
|
||||
@ -21,8 +21,12 @@ import { PlaywrightDispatcher } from './dispatchers/playwrightDispatcher';
|
||||
import { Connection } from './client/connection';
|
||||
import { BrowserServerLauncherImpl } from './browserServerImpl';
|
||||
import { isDevMode } from './utils/utils';
|
||||
import { instrumentingAgents } from './server/instrumentation';
|
||||
import { DebugController } from './debug/debugController';
|
||||
|
||||
export function setupInProcess(playwright: PlaywrightImpl): PlaywrightAPI {
|
||||
instrumentingAgents.add(new DebugController());
|
||||
|
||||
const clientConnection = new Connection();
|
||||
const dispatcherConnection = new DispatcherConnection();
|
||||
|
||||
|
||||
@ -20,6 +20,10 @@ import { Playwright } from './server/playwright';
|
||||
import { PlaywrightDispatcher } from './dispatchers/playwrightDispatcher';
|
||||
import { Electron } from './server/electron/electron';
|
||||
import { gracefullyCloseAll } from './server/processLauncher';
|
||||
import { instrumentingAgents } from './server/instrumentation';
|
||||
import { DebugController } from './debug/debugController';
|
||||
|
||||
instrumentingAgents.add(new DebugController());
|
||||
|
||||
const dispatcherConnection = new DispatcherConnection();
|
||||
const transport = new Transport(process.stdout, process.stdin);
|
||||
|
||||
@ -27,10 +27,8 @@ import { Download } from './download';
|
||||
import { Browser } from './browser';
|
||||
import { EventEmitter } from 'events';
|
||||
import { Progress } from './progress';
|
||||
import { DebugController } from './debug/debugController';
|
||||
import { isDebugMode } from '../utils/utils';
|
||||
import { Snapshotter, SnapshotterDelegate } from './snapshotter';
|
||||
import { Selectors, serverSelectors } from './selectors';
|
||||
import { instrumentingAgents } from './instrumentation';
|
||||
|
||||
export class Screencast {
|
||||
readonly page: Page;
|
||||
@ -70,7 +68,6 @@ export abstract class BrowserContext extends EventEmitter {
|
||||
readonly _browser: Browser;
|
||||
readonly _browserContextId: string | undefined;
|
||||
private _selectors?: Selectors;
|
||||
_snapshotter?: Snapshotter;
|
||||
|
||||
constructor(browser: Browser, options: types.BrowserContextOptions, browserContextId: string | undefined) {
|
||||
super();
|
||||
@ -90,14 +87,8 @@ export abstract class BrowserContext extends EventEmitter {
|
||||
}
|
||||
|
||||
async _initialize() {
|
||||
if (isDebugMode())
|
||||
new DebugController(this);
|
||||
}
|
||||
|
||||
// Used by test runner.
|
||||
async _initSnapshotter(delegate: SnapshotterDelegate): Promise<Snapshotter> {
|
||||
this._snapshotter = new Snapshotter(this, delegate);
|
||||
return this._snapshotter;
|
||||
for (const agent of instrumentingAgents)
|
||||
await agent.onContextCreated(this);
|
||||
}
|
||||
|
||||
_browserClosed() {
|
||||
@ -250,8 +241,8 @@ export abstract class BrowserContext extends EventEmitter {
|
||||
this._closedStatus = 'closing';
|
||||
await this._doClose();
|
||||
await Promise.all([...this._downloads].map(d => d.delete()));
|
||||
if (this._snapshotter)
|
||||
this._snapshotter._dispose();
|
||||
for (const agent of instrumentingAgents)
|
||||
await agent.onContextDestroyed(this);
|
||||
this._didCloseInternal();
|
||||
}
|
||||
await this._closePromise;
|
||||
|
||||
@ -1,39 +0,0 @@
|
||||
/**
|
||||
* 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 { BrowserContext } from '../browserContext';
|
||||
import * as frames from '../frames';
|
||||
import * as js from '../javascript';
|
||||
import { Page } from '../page';
|
||||
import type DebugScript from './injected/debugScript';
|
||||
|
||||
export class DebugController {
|
||||
constructor(context: BrowserContext) {
|
||||
context.on(BrowserContext.Events.Page, (page: Page) => {
|
||||
for (const frame of page.frames())
|
||||
this.ensureInstalledInFrame(frame);
|
||||
page.on(Page.Events.FrameNavigated, frame => this.ensureInstalledInFrame(frame));
|
||||
});
|
||||
}
|
||||
|
||||
private async ensureInstalledInFrame(frame: frames.Frame): Promise<js.JSHandle<DebugScript> | undefined> {
|
||||
try {
|
||||
const mainContext = await frame._mainContext();
|
||||
return await mainContext.createDebugScript({ console: true });
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -18,13 +18,11 @@ import * as frames from './frames';
|
||||
import { assert } from '../utils/utils';
|
||||
import type { InjectedScript, InjectedScriptPoll } from './injected/injectedScript';
|
||||
import * as injectedScriptSource from '../generated/injectedScriptSource';
|
||||
import * as debugScriptSource from '../generated/debugScriptSource';
|
||||
import * as js from './javascript';
|
||||
import { Page } from './page';
|
||||
import { SelectorInfo } from './selectors';
|
||||
import * as types from './types';
|
||||
import { Progress } from './progress';
|
||||
import type DebugScript from './debug/injected/debugScript';
|
||||
import { FatalDOMError, RetargetableDOMError } from './common/domErrors';
|
||||
|
||||
export class FrameExecutionContext extends js.ExecutionContext {
|
||||
@ -92,18 +90,6 @@ export class FrameExecutionContext extends js.ExecutionContext {
|
||||
return this._injectedScriptPromise;
|
||||
}
|
||||
|
||||
createDebugScript(options: { console?: boolean }): Promise<js.JSHandle<DebugScript> | undefined> {
|
||||
if (!this._debugScriptPromise) {
|
||||
const source = `new (${debugScriptSource.source})()`;
|
||||
this._debugScriptPromise = this._delegate.rawEvaluate(source).then(objectId => new js.JSHandle(this, 'object', objectId)).then(async debugScript => {
|
||||
const injectedScript = await this.injectedScript();
|
||||
await debugScript.evaluate((debugScript: DebugScript, { injectedScript, options }) => debugScript.initialize(injectedScript, options), { injectedScript, options });
|
||||
return debugScript;
|
||||
}).catch(e => undefined);
|
||||
}
|
||||
return this._debugScriptPromise;
|
||||
}
|
||||
|
||||
async doSlowMo() {
|
||||
return this.frame._page._doSlowMo();
|
||||
}
|
||||
|
||||
27
src/server/instrumentation.ts
Normal file
27
src/server/instrumentation.ts
Normal file
@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 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 { BrowserContext } from './browserContext';
|
||||
import { Page } from './page';
|
||||
import { Progress } from './progress';
|
||||
|
||||
export interface InstrumentingAgent {
|
||||
onContextCreated(context: BrowserContext): Promise<void>;
|
||||
onContextDestroyed(context: BrowserContext): Promise<void>;
|
||||
onBeforePageAction(page: Page, progress: Progress): Promise<void>;
|
||||
}
|
||||
|
||||
export const instrumentingAgents = new Set<InstrumentingAgent>();
|
||||
@ -32,6 +32,7 @@ import { Progress, runAbortableTask } from './progress';
|
||||
import { assert, isError } from '../utils/utils';
|
||||
import { debugLogger } from '../utils/debugLogger';
|
||||
import { Selectors } from './selectors';
|
||||
import { instrumentingAgents } from './instrumentation';
|
||||
|
||||
export interface PageDelegate {
|
||||
readonly rawMouse: input.RawMouse;
|
||||
@ -199,8 +200,8 @@ export class Page extends EventEmitter {
|
||||
|
||||
async _runAbortableTask<T>(task: (progress: Progress) => Promise<T>, timeout: number): Promise<T> {
|
||||
return runAbortableTask(async progress => {
|
||||
if (this._browserContext._snapshotter)
|
||||
await this._browserContext._snapshotter._doSnapshot(progress, this, 'progress');
|
||||
for (const agent of instrumentingAgents)
|
||||
await agent.onBeforePageAction(this, progress);
|
||||
return task(progress);
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
@ -14,15 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { BrowserContext } from './browserContext';
|
||||
import { Page } from './page';
|
||||
import * as network from './network';
|
||||
import { helper, RegisteredListener } from './helper';
|
||||
import { Progress, runAbortableTask } from './progress';
|
||||
import { BrowserContext } from '../server/browserContext';
|
||||
import { Page } from '../server/page';
|
||||
import * as network from '../server/network';
|
||||
import { helper, RegisteredListener } from '../server/helper';
|
||||
import { Progress } from '../server/progress';
|
||||
import { debugLogger } from '../utils/debugLogger';
|
||||
import { Frame } from './frames';
|
||||
import * as js from './javascript';
|
||||
import * as types from './types';
|
||||
import { Frame } from '../server/frames';
|
||||
import * as js from '../server/javascript';
|
||||
import { SnapshotData, takeSnapshotInFrame } from './snapshotterInjected';
|
||||
import { assert, calculateSha1, createGuid } from '../utils/utils';
|
||||
|
||||
@ -53,11 +52,9 @@ export type PageSnapshot = {
|
||||
};
|
||||
|
||||
export interface SnapshotterDelegate {
|
||||
onContextCreated(context: BrowserContext): void;
|
||||
onContextDestroyed(context: BrowserContext): void;
|
||||
onBlob(context: BrowserContext, blob: SnapshotterBlob): void;
|
||||
onResource(context: BrowserContext, resource: SanpshotterResource): void;
|
||||
onSnapshot(context: BrowserContext, snapshot: PageSnapshot): void;
|
||||
onBlob(blob: SnapshotterBlob): void;
|
||||
onResource(resource: SanpshotterResource): void;
|
||||
onSnapshot(snapshot: PageSnapshot): void;
|
||||
}
|
||||
|
||||
export class Snapshotter {
|
||||
@ -71,25 +68,17 @@ export class Snapshotter {
|
||||
this._eventListeners = [
|
||||
helper.addEventListener(this._context, BrowserContext.Events.Page, this._onPage.bind(this)),
|
||||
];
|
||||
this._delegate.onContextCreated(this._context);
|
||||
}
|
||||
|
||||
async captureSnapshot(page: Page, options: types.TimeoutOptions & { label?: string } = {}): Promise<void> {
|
||||
return runAbortableTask(async progress => {
|
||||
await this._doSnapshot(progress, page, options.label || 'snapshot');
|
||||
}, page._timeoutSettings.timeout(options));
|
||||
}
|
||||
|
||||
_dispose() {
|
||||
dispose() {
|
||||
helper.removeEventListeners(this._eventListeners);
|
||||
this._delegate.onContextDestroyed(this._context);
|
||||
}
|
||||
|
||||
async _doSnapshot(progress: Progress, page: Page, label: string): Promise<void> {
|
||||
async takeSnapshot(progress: Progress, page: Page, label: string): Promise<void> {
|
||||
assert(page.context() === this._context);
|
||||
const snapshot = await this._snapshotPage(progress, page, label);
|
||||
if (snapshot)
|
||||
this._delegate.onSnapshot(this._context, snapshot);
|
||||
this._delegate.onSnapshot(snapshot);
|
||||
}
|
||||
|
||||
private _onPage(page: Page) {
|
||||
@ -124,9 +113,9 @@ export class Snapshotter {
|
||||
responseHeaders: response.headers(),
|
||||
sha1,
|
||||
};
|
||||
this._delegate.onResource(this._context, resource);
|
||||
this._delegate.onResource(resource);
|
||||
if (body)
|
||||
this._delegate.onBlob(this._context, { sha1, buffer: body });
|
||||
this._delegate.onBlob({ sha1, buffer: body });
|
||||
}
|
||||
|
||||
private async _snapshotPage(progress: Progress, page: Page, label: string): Promise<PageSnapshot | null> {
|
||||
@ -214,7 +203,7 @@ export class Snapshotter {
|
||||
for (const { url, content } of data.resourceOverrides) {
|
||||
const buffer = Buffer.from(content);
|
||||
const sha1 = calculateSha1(buffer);
|
||||
this._delegate.onBlob(this._context, { sha1, buffer });
|
||||
this._delegate.onBlob({ sha1, buffer });
|
||||
snapshot.resourceOverrides.push({ url, sha1 });
|
||||
}
|
||||
|
||||
@ -14,8 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type * as snapshotter from '../server/snapshotter';
|
||||
|
||||
export type ContextCreatedTraceEvent = {
|
||||
type: 'context-created',
|
||||
browserName: string,
|
||||
@ -46,6 +44,3 @@ export type SnapshotTraceEvent = {
|
||||
label: string,
|
||||
sha1: string,
|
||||
};
|
||||
|
||||
export type FrameSnapshot = snapshotter.FrameSnapshot;
|
||||
export type PageSnapshot = snapshotter.PageSnapshot;
|
||||
|
||||
@ -17,7 +17,8 @@
|
||||
import * as path from 'path';
|
||||
import * as util from 'util';
|
||||
import * as fs from 'fs';
|
||||
import { NetworkResourceTraceEvent, SnapshotTraceEvent, ContextCreatedTraceEvent, ContextDestroyedTraceEvent, FrameSnapshot, PageSnapshot } from './traceTypes';
|
||||
import type { NetworkResourceTraceEvent, SnapshotTraceEvent, ContextCreatedTraceEvent, ContextDestroyedTraceEvent } from './traceTypes';
|
||||
import type { FrameSnapshot, PageSnapshot } from './snapshotter';
|
||||
import type { Browser, BrowserContext, Frame, Page, Route } from '../client/api';
|
||||
import type { Playwright } from '../client/playwright';
|
||||
|
||||
|
||||
@ -15,59 +15,96 @@
|
||||
*/
|
||||
|
||||
import type { BrowserContext } from '../server/browserContext';
|
||||
import type { PageSnapshot, SanpshotterResource, SnapshotterBlob, SnapshotterDelegate } from '../server/snapshotter';
|
||||
import type { PageSnapshot, SanpshotterResource, SnapshotterBlob, SnapshotterDelegate } from './snapshotter';
|
||||
import { ContextCreatedTraceEvent, ContextDestroyedTraceEvent, NetworkResourceTraceEvent, SnapshotTraceEvent } from './traceTypes';
|
||||
import * as path from 'path';
|
||||
import * as util from 'util';
|
||||
import * as fs from 'fs';
|
||||
import { calculateSha1, createGuid, mkdirIfNeeded, monotonicTime } from '../utils/utils';
|
||||
import { InstrumentingAgent, instrumentingAgents } from '../server/instrumentation';
|
||||
import { Page } from '../server/page';
|
||||
import { Progress, runAbortableTask } from '../server/progress';
|
||||
import { Snapshotter } from './snapshotter';
|
||||
import * as types from '../server/types';
|
||||
|
||||
const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs));
|
||||
const fsAppendFileAsync = util.promisify(fs.appendFile.bind(fs));
|
||||
const fsAccessAsync = util.promisify(fs.access.bind(fs));
|
||||
|
||||
export class Tracer implements SnapshotterDelegate {
|
||||
private _contextIds = new Map<BrowserContext, string>();
|
||||
export class Tracer implements InstrumentingAgent {
|
||||
private _contextTracers = new Map<BrowserContext, ContextTracer>();
|
||||
|
||||
constructor() {
|
||||
instrumentingAgents.add(this);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
instrumentingAgents.delete(this);
|
||||
}
|
||||
|
||||
traceContext(context: BrowserContext, traceStorageDir: string, traceFile: string) {
|
||||
const contextTracer = new ContextTracer(context, traceStorageDir, traceFile);
|
||||
this._contextTracers.set(context, contextTracer);
|
||||
}
|
||||
|
||||
async captureSnapshot(page: Page, options: types.TimeoutOptions & { label?: string } = {}): Promise<void> {
|
||||
return runAbortableTask(async progress => {
|
||||
const contextTracer = this._contextTracers.get(page.context());
|
||||
if (contextTracer)
|
||||
await contextTracer._snapshotter.takeSnapshot(progress, page, options.label || 'snapshot');
|
||||
}, page._timeoutSettings.timeout(options));
|
||||
}
|
||||
|
||||
async onContextCreated(context: BrowserContext): Promise<void> {
|
||||
}
|
||||
|
||||
async onContextDestroyed(context: BrowserContext): Promise<void> {
|
||||
const contextTracer = this._contextTracers.get(context);
|
||||
if (contextTracer) {
|
||||
await contextTracer.dispose();
|
||||
this._contextTracers.delete(context);
|
||||
}
|
||||
}
|
||||
|
||||
async onBeforePageAction(page: Page, progress: Progress): Promise<void> {
|
||||
const contextTracer = this._contextTracers.get(page.context());
|
||||
if (contextTracer)
|
||||
await contextTracer._snapshotter.takeSnapshot(progress, page, 'progress');
|
||||
}
|
||||
}
|
||||
|
||||
class ContextTracer implements SnapshotterDelegate {
|
||||
private _contextId: string;
|
||||
private _traceStoragePromise: Promise<string>;
|
||||
private _appendEventChain: Promise<string>;
|
||||
private _writeArtifactChain: Promise<void>;
|
||||
readonly _snapshotter: Snapshotter;
|
||||
|
||||
constructor(traceStorageDir: string, traceFile: string) {
|
||||
constructor(context: BrowserContext, traceStorageDir: string, traceFile: string) {
|
||||
this._contextId = 'context@' + createGuid();
|
||||
this._traceStoragePromise = mkdirIfNeeded(path.join(traceStorageDir, 'sha1')).then(() => traceStorageDir);
|
||||
this._appendEventChain = mkdirIfNeeded(traceFile).then(() => traceFile);
|
||||
this._writeArtifactChain = Promise.resolve();
|
||||
}
|
||||
|
||||
onContextCreated(context: BrowserContext): void {
|
||||
const contextId = 'context@' + createGuid();
|
||||
this._contextIds.set(context, contextId);
|
||||
const event: ContextCreatedTraceEvent = {
|
||||
type: 'context-created',
|
||||
browserName: context._browser._options.name,
|
||||
contextId,
|
||||
contextId: this._contextId,
|
||||
isMobile: !!context._options.isMobile,
|
||||
deviceScaleFactor: context._options.deviceScaleFactor || 1,
|
||||
viewportSize: context._options.viewport || undefined,
|
||||
};
|
||||
this._appendTraceEvent(event);
|
||||
this._snapshotter = new Snapshotter(context, this);
|
||||
}
|
||||
|
||||
onContextDestroyed(context: BrowserContext): void {
|
||||
const event: ContextDestroyedTraceEvent = {
|
||||
type: 'context-destroyed',
|
||||
contextId: this._contextIds.get(context)!,
|
||||
};
|
||||
this._appendTraceEvent(event);
|
||||
}
|
||||
|
||||
onBlob(context: BrowserContext, blob: SnapshotterBlob): void {
|
||||
onBlob(blob: SnapshotterBlob): void {
|
||||
this._writeArtifact(blob.sha1, blob.buffer);
|
||||
}
|
||||
|
||||
onResource(context: BrowserContext, resource: SanpshotterResource): void {
|
||||
onResource(resource: SanpshotterResource): void {
|
||||
const event: NetworkResourceTraceEvent = {
|
||||
type: 'resource',
|
||||
contextId: this._contextIds.get(context)!,
|
||||
contextId: this._contextId,
|
||||
frameId: resource.frameId,
|
||||
url: resource.url,
|
||||
contentType: resource.contentType,
|
||||
@ -77,12 +114,12 @@ export class Tracer implements SnapshotterDelegate {
|
||||
this._appendTraceEvent(event);
|
||||
}
|
||||
|
||||
onSnapshot(context: BrowserContext, snapshot: PageSnapshot): void {
|
||||
onSnapshot(snapshot: PageSnapshot): void {
|
||||
const buffer = Buffer.from(JSON.stringify(snapshot));
|
||||
const sha1 = calculateSha1(buffer);
|
||||
const event: SnapshotTraceEvent = {
|
||||
type: 'snapshot',
|
||||
contextId: this._contextIds.get(context)!,
|
||||
contextId: this._contextId,
|
||||
label: snapshot.label,
|
||||
sha1,
|
||||
};
|
||||
@ -91,6 +128,13 @@ export class Tracer implements SnapshotterDelegate {
|
||||
}
|
||||
|
||||
async dispose() {
|
||||
this._snapshotter.dispose();
|
||||
const event: ContextDestroyedTraceEvent = {
|
||||
type: 'context-destroyed',
|
||||
contextId: this._contextId,
|
||||
};
|
||||
this._appendTraceEvent(event);
|
||||
|
||||
// Ensure all writes are finished.
|
||||
await this._appendEventChain;
|
||||
await this._writeArtifactChain;
|
||||
|
||||
@ -137,7 +137,14 @@ registerWorkerFixture('playwright', async ({browserName}, test) => {
|
||||
spawnedProcess.stderr.destroy();
|
||||
await teardownCoverage();
|
||||
} else {
|
||||
await test(require('../index'));
|
||||
const playwright = require('../index');
|
||||
if (options.TRACING) {
|
||||
const tracerFactory = require('../lib/trace/tracer').Tracer;
|
||||
playwright.__tracer = new tracerFactory();
|
||||
}
|
||||
await test(playwright);
|
||||
if (playwright.__tracer)
|
||||
playwright.__tracer.dispose();
|
||||
await teardownCoverage();
|
||||
}
|
||||
|
||||
@ -148,7 +155,6 @@ registerWorkerFixture('playwright', async ({browserName}, test) => {
|
||||
await fs.promises.mkdir(path.dirname(coveragePath), { recursive: true });
|
||||
await fs.promises.writeFile(coveragePath, JSON.stringify(coverageJSON, undefined, 2), 'utf8');
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
registerWorkerFixture('toImpl', async ({playwright}, test) => {
|
||||
@ -182,25 +188,21 @@ registerWorkerFixture('golden', async ({browserName}, test) => {
|
||||
await test(p => path.join(browserName, p));
|
||||
});
|
||||
|
||||
registerFixture('context', async ({browser, toImpl}, runTest, info) => {
|
||||
registerFixture('context', async ({browser, playwright, toImpl}, runTest, info) => {
|
||||
const context = await browser.newContext();
|
||||
const { test, config } = info;
|
||||
if (options.TRACING) {
|
||||
if ((playwright as any).__tracer) {
|
||||
const traceStorageDir = path.join(config.outputDir, 'trace-storage');
|
||||
const relativePath = path.relative(config.testDir, test.file).replace(/\.spec\.[jt]s/, '');
|
||||
const sanitizedTitle = test.title.replace(/[^\w\d]+/g, '_');
|
||||
const traceFile = path.join(config.outputDir, relativePath, sanitizedTitle + '.trace');
|
||||
const tracerFactory = require('../lib/trace/tracer').Tracer;
|
||||
(context as any).__tracer = new tracerFactory(traceStorageDir, traceFile);
|
||||
(context as any).__snapshotter = await toImpl(context)._initSnapshotter((context as any).__tracer);
|
||||
(playwright as any).__tracer.traceContext(toImpl(context), traceStorageDir, traceFile);
|
||||
}
|
||||
await runTest(context);
|
||||
await context.close();
|
||||
if ((context as any).__tracer)
|
||||
await (context as any).__tracer.dispose();
|
||||
});
|
||||
|
||||
registerFixture('page', async ({context, toImpl}, runTest, info) => {
|
||||
registerFixture('page', async ({context, playwright, toImpl}, runTest, info) => {
|
||||
const page = await context.newPage();
|
||||
await runTest(page);
|
||||
const { test, config, result } = info;
|
||||
@ -209,8 +211,8 @@ registerFixture('page', async ({context, toImpl}, runTest, info) => {
|
||||
const sanitizedTitle = test.title.replace(/[^\w\d]+/g, '_');
|
||||
const assetPath = path.join(config.outputDir, relativePath, sanitizedTitle) + '-failed.png';
|
||||
await page.screenshot({ timeout: 5000, path: assetPath });
|
||||
if ((context as any).__snapshotter)
|
||||
await (context as any).__snapshotter.captureSnapshot(toImpl(page), { timeout: 5000, label: 'Test Failed' });
|
||||
if ((playwright as any).__tracer)
|
||||
await (playwright as any).__tracer.captureSnapshot(toImpl(page), { timeout: 5000, label: 'Test Failed' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@ import { it, options } from './playwright.fixtures';
|
||||
|
||||
it('should not throw', test => {
|
||||
test.skip(!options.TRACING);
|
||||
}, async ({page, server, context, toImpl}) => {
|
||||
}, async ({page, server, playwright, toImpl}) => {
|
||||
await page.goto(server.PREFIX + '/snapshot/snapshot-with-css.html');
|
||||
await (context as any).__snapshotter.captureSnapshot(toImpl(page), { timeout: 5000, label: 'snapshot' });
|
||||
await (playwright as any).__tracer.captureSnapshot(toImpl(page), { timeout: 5000, label: 'snapshot' });
|
||||
});
|
||||
|
||||
@ -103,7 +103,6 @@ DEPS['src/server/'] = [
|
||||
// Can depend on any files in these subdirectories.
|
||||
'src/server/common/**',
|
||||
'src/server/injected/**',
|
||||
'src/server/debug/**',
|
||||
];
|
||||
|
||||
// No dependencies for code shared between node and page.
|
||||
@ -117,7 +116,10 @@ DEPS['src/server/electron/'] = [...DEPS['src/server/'], 'src/server/chromium/'];
|
||||
DEPS['src/server/playwright.ts'] = [...DEPS['src/server/'], 'src/server/chromium/', 'src/server/webkit/', 'src/server/firefox/'];
|
||||
DEPS['src/server.ts'] = DEPS['src/inprocess.ts'] = DEPS['src/browserServerImpl.ts'] = ['src/**'];
|
||||
|
||||
// Tracing depends on client and server, but nothing should depend on tracing.
|
||||
// Tracing is a client/server plugin, nothing should depend on it.
|
||||
DEPS['src/trace/'] = ['src/utils/', 'src/client/**', 'src/server/**'];
|
||||
|
||||
// Debug is a server plugin, nothing should depend on it.
|
||||
DEPS['src/debug/'] = ['src/utils/', 'src/generated/', 'src/server/**', 'src/debug/**'];
|
||||
|
||||
checkDeps();
|
||||
|
||||
@ -20,7 +20,7 @@ const path = require('path');
|
||||
const files = [
|
||||
path.join('src', 'server', 'injected', 'injectedScript.webpack.config.js'),
|
||||
path.join('src', 'server', 'injected', 'utilityScript.webpack.config.js'),
|
||||
path.join('src', 'server', 'debug', 'injected', 'debugScript.webpack.config.js'),
|
||||
path.join('src', 'debug', 'injected', 'debugScript.webpack.config.js'),
|
||||
];
|
||||
|
||||
function runOne(runner, file) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user