diff --git a/src/cli/cli.ts b/src/cli/cli.ts index c385ff1ed1..048c685509 100755 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -18,14 +18,12 @@ /* eslint-disable no-console */ -import extract from 'extract-zip'; import fs from 'fs'; import os from 'os'; import path from 'path'; -import rimraf from 'rimraf'; import program from 'commander'; import { runDriver, runServer, printApiJson, launchBrowserServer, installBrowsers } from './driver'; -import { TraceViewer } from '../server/trace/viewer/traceViewer'; +import { showTraceViewer } from '../server/trace/viewer/traceViewer'; import * as playwright from '../..'; import { BrowserContext } from '../client/browserContext'; import { Browser } from '../client/browser'; @@ -584,31 +582,3 @@ function commandWithOpenOptions(command: string, description: string, options: a .option('--user-agent ', 'specify user agent string') .option('--viewport-size ', 'specify browser viewport size in pixels, for example "1280, 720"'); } - -export async function showTraceViewer(tracePath: string, browserName: string) { - let stat; - try { - stat = fs.statSync(tracePath); - } catch (e) { - console.log(`No such file or directory: ${tracePath}`); - return; - } - - if (stat.isDirectory()) { - const traceViewer = new TraceViewer(tracePath, browserName); - await traceViewer.show(); - return; - } - - const zipFile = tracePath; - const dir = fs.mkdtempSync(path.join(os.tmpdir(), `playwright-trace`)); - process.on('exit', () => rimraf.sync(dir)); - try { - await extract(zipFile, { dir: dir }); - } catch (e) { - console.log(`Invalid trace file: ${zipFile}`); - return; - } - const traceViewer = new TraceViewer(dir, browserName); - await traceViewer.show(); -} diff --git a/src/dispatchers/frameDispatcher.ts b/src/dispatchers/frameDispatcher.ts index 74f4c705e3..f998cf8381 100644 --- a/src/dispatchers/frameDispatcher.ts +++ b/src/dispatchers/frameDispatcher.ts @@ -30,11 +30,17 @@ export class FrameDispatcher extends Dispatcher(frame.parentFrame()), + parentFrame: FrameDispatcher.fromNullable(scope, frame.parentFrame()), loadStates: Array.from(frame._subtreeLifecycleEvents), }); this._frame = frame; diff --git a/src/server/trace/viewer/traceViewer.ts b/src/server/trace/viewer/traceViewer.ts index 68d465add2..45fa9209e1 100644 --- a/src/server/trace/viewer/traceViewer.ts +++ b/src/server/trace/viewer/traceViewer.ts @@ -14,8 +14,11 @@ * limitations under the License. */ +import extract from 'extract-zip'; import fs from 'fs'; +import os from 'os'; import path from 'path'; +import rimraf from 'rimraf'; import { createPlaywright } from '../../playwright'; import { PersistentSnapshotStorage, TraceModel } from './traceModel'; import { TraceEvent } from '../common/traceEvents'; @@ -25,6 +28,7 @@ import * as consoleApiSource from '../../../generated/consoleApiSource'; import { isUnderTest } from '../../../utils/utils'; import { internalCallMetadata } from '../../instrumentation'; import { ProgressController } from '../../progress'; +import { BrowserContext } from '../../browserContext'; export class TraceViewer { private _server: HttpServer; @@ -112,7 +116,7 @@ export class TraceViewer { this._server.routePrefix('/sha1/', sha1Handler); } - async show() { + async show(headless: boolean): Promise { const urlPrefix = await this._server.start(); const traceViewerPlaywright = createPlaywright(true); @@ -127,7 +131,7 @@ export class TraceViewer { sdkLanguage: 'javascript', args, noDefaultViewport: true, - headless: !!process.env.PWTEST_CLI_HEADLESS, + headless, useWebSocket: isUnderTest() }); @@ -137,7 +141,35 @@ export class TraceViewer { }); await context.extendInjectedScript('main', consoleApiSource.source); const [page] = context.pages(); - page.on('close', () => process.exit(0)); + page.on('close', () => context.close(internalCallMetadata()).catch(() => {})); await page.mainFrame().goto(internalCallMetadata(), urlPrefix + '/traceviewer/traceViewer/index.html'); + return context; } } + +export async function showTraceViewer(tracePath: string, browserName: string, headless = false): Promise { + let stat; + try { + stat = fs.statSync(tracePath); + } catch (e) { + console.log(`No such file or directory: ${tracePath}`); // eslint-disable-line no-console + return; + } + + if (stat.isDirectory()) { + const traceViewer = new TraceViewer(tracePath, browserName); + return await traceViewer.show(headless); + } + + const zipFile = tracePath; + const dir = fs.mkdtempSync(path.join(os.tmpdir(), `playwright-trace`)); + process.on('exit', () => rimraf.sync(dir)); + try { + await extract(zipFile, { dir }); + } catch (e) { + console.log(`Invalid trace file: ${zipFile}`); // eslint-disable-line no-console + return; + } + const traceViewer = new TraceViewer(dir, browserName); + return await traceViewer.show(headless); +} diff --git a/src/web/traceViewer/ui/workbench.tsx b/src/web/traceViewer/ui/workbench.tsx index 24af1cffa9..a821dd5b16 100644 --- a/src/web/traceViewer/ui/workbench.tsx +++ b/src/web/traceViewer/ui/workbench.tsx @@ -38,6 +38,8 @@ export const Workbench: React.FunctionComponent<{ const [highlightedAction, setHighlightedAction] = React.useState(); let context = useAsyncMemo(async () => { + if (!debugName) + return emptyContext; return (await fetch(`/context/${debugName}`).then(response => response.json())) as ContextEntry; }, [debugName], emptyContext); diff --git a/tests/chromium/trace-viewer/trace-viewer.spec.ts b/tests/chromium/trace-viewer/trace-viewer.spec.ts new file mode 100644 index 0000000000..08bf703a08 --- /dev/null +++ b/tests/chromium/trace-viewer/trace-viewer.spec.ts @@ -0,0 +1,86 @@ +/** + * 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 path from 'path'; +import type { Browser, Page } from '../../../index'; +import { showTraceViewer } from '../../../lib/server/trace/viewer/traceViewer'; +import { playwrightTest } from '../../config/browserTest'; +import { expect } from '../../config/test-runner'; + +class TraceViewerPage { + constructor(public page: Page) {} + + async actionTitles() { + await this.page.waitForSelector('.action-title'); + return await this.page.$$eval('.action-title', ee => ee.map(e => e.textContent)); + } + + async selectAction(title: string) { + await this.page.click(`.action-title:text("${title}")`); + } + + async logLines() { + return await this.page.$$eval('.log-line', ee => ee.map(e => e.textContent)); + } +} + +const test = playwrightTest.extend<{ showTraceViewer: (trace: string) => Promise }>({ + showTraceViewer: async ({ browserType, browserName, headless }, use) => { + let browser: Browser; + let contextImpl: any; + await use(async (trace: string) => { + contextImpl = await showTraceViewer(trace, browserName, headless); + browser = await browserType.connectOverCDP({ endpointURL: contextImpl._browser.options.wsEndpoint }); + return new TraceViewerPage(browser.contexts()[0].pages()[0]); + }); + await browser.close(); + await contextImpl._browser.close(); + } +}); + +let traceFile: string; + +test.beforeAll(async ({ browser }, workerInfo) => { + const context = await browser.newContext(); + await context.tracing.start({ name: 'test', screenshots: true, snapshots: true }); + const page = await context.newPage(); + await page.goto('data:text/html,Hello world'); + await page.setContent(''); + await page.click('"Click"'); + await page.close(); + traceFile = path.join(workerInfo.project.outputDir, 'trace.zip'); + await context.tracing.stop({ path: traceFile }); +}); + +test('should show empty trace viewer', async ({ showTraceViewer }, testInfo) => { + const traceViewer = await showTraceViewer(testInfo.outputPath()); + expect(await traceViewer.page.title()).toBe('Playwright Trace Viewer'); +}); + +test('should open simple trace viewer', async ({ showTraceViewer }) => { + const traceViewer = await showTraceViewer(traceFile); + expect(await traceViewer.actionTitles()).toEqual(['page.goto', 'page.setContent', 'page.click']); +}); + +test('should contain action log', async ({ showTraceViewer }) => { + const traceViewer = await showTraceViewer(traceFile); + await traceViewer.selectAction('page.click'); + + const logLines = await traceViewer.logLines(); + expect(logLines.length).toBeGreaterThan(10); + expect(logLines).toContain('attempting click action'); + expect(logLines).toContain(' click action done'); +});