/** * Copyright (c) Microsoft Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import type { Fixtures, FrameLocator, Locator, Page, Browser, BrowserContext } from '@playwright/test'; import { step } from './baseTest'; import path from 'path'; import { CommonFixtures, TestChildProcess } from './commonFixtures'; type BaseTestFixtures = CommonFixtures & { context: BrowserContext; }; type BaseWorkerFixtures = { headless: boolean; browser: Browser; browserName: 'chromium' | 'firefox' | 'webkit'; playwright: typeof import('@playwright/test'); }; export type TraceViewerFixtures = { showTraceViewer: (trace: string | undefined, options?: {host?: string, port?: number, stdin?: boolean}) => Promise; runAndTrace: (body: () => Promise, optsOverrides?: Parameters[0]) => Promise; }; class TraceViewerPage { actionTitles: Locator; actionsTree: Locator; callLines: Locator; consoleLines: Locator; logLines: Locator; errorMessages: Locator; consoleLineMessages: Locator; consoleStacks: Locator; networkRequests: Locator; metadataTab: Locator; snapshotContainer: Locator; sourceCodeTab: Locator; networkTab: Locator; settingsDialog: Locator; themeSetting: Locator; displayCanvasContentSetting: Locator; constructor(public page: Page, public process: TestChildProcess) { this.actionTitles = page.locator('.action-title'); this.actionsTree = page.getByTestId('actions-tree'); this.callLines = page.locator('.call-tab .call-line'); this.logLines = page.getByRole('listbox', { name: 'Log entries' }).getByRole('option'); this.consoleLines = page.getByRole('tabpanel', { name: 'Console' }).getByRole('option'); this.consoleLineMessages = page.locator('.console-line-message'); this.errorMessages = page.locator('.error-message'); this.consoleStacks = page.locator('.console-stack'); this.networkRequests = page.getByRole('listbox', { name: 'Network requests' }).getByRole('option'); this.snapshotContainer = page.locator('.snapshot-container iframe.snapshot-visible[name=snapshot]'); this.metadataTab = page.getByRole('tabpanel', { name: 'Metadata' }); this.sourceCodeTab = page.getByRole('tabpanel', { name: 'Source' }); this.networkTab = page.getByRole('tabpanel', { name: 'Network' }); this.settingsDialog = page.getByTestId('settings-toolbar-dialog'); this.themeSetting = this.settingsDialog.getByRole('combobox', { name: 'Theme' }); this.displayCanvasContentSetting = page.locator('.setting').getByText('Display canvas content'); } @step async showAllActions() { await this.page.getByRole('button', { name: 'Filter actions' }).click(); await this.page.locator('.setting').getByText('Network routes').click(); await this.page.locator('.setting').getByText('Getters').click(); await this.page.locator('.setting').getByText('Configuration').click(); await this.page.getByRole('button', { name: 'Filter actions' }).click(); } stackFrames(options: { selected?: boolean } = {}) { return this.page.getByRole('listbox', { name: 'Stack trace' }).getByRole('option', options); } actionIconsText(action: string) { const entry = this.actionsTree.getByRole('treeitem', { name: action }); return entry.locator('.action-icon-value').filter({ visible: true }); } actionIcons(action: string) { return this.actionsTree.getByRole('treeitem', { name: action }).locator('.action-icons').filter({ visible: true }); } @step async expandAction(title: string) { await this.actionsTree.getByRole('treeitem', { name: title }).locator('.codicon-chevron-right').click(); } @step async selectAction(title: string, ordinal: number = 0) { await this.actionsTree.getByTitle(title).nth(ordinal).click(); } @step async hoverAction(title: string, ordinal: number = 0) { await this.actionsTree.getByRole('treeitem', { name: title }).nth(ordinal).hover(); } @step async selectSnapshot(name: string) { await this.page.getByRole('tab', { name }).click(); } async showErrorsTab() { await this.page.getByRole('tab', { name: 'Errors' }).click(); } async showConsoleTab() { await this.page.getByRole('tab', { name: 'Console' }).click(); } async showSourceTab() { await this.page.getByRole('tab', { name: 'Source' }).click(); } async showNetworkTab() { await this.page.getByRole('tab', { name: 'Network' }).click(); } async showMetadataTab() { await this.page.getByRole('tab', { name: 'Metadata' }).click(); } async showSettings() { await this.page.getByRole('button', { name: 'Settings' }).click(); } @step async snapshotFrame(actionName: string, ordinal: number = 0, hasSubframe: boolean = false): Promise { await this.selectAction(actionName, ordinal); while (this.page.frames().length < (hasSubframe ? 4 : 3)) await this.page.waitForEvent('frameattached'); return this.page.frameLocator('iframe.snapshot-visible[name=snapshot]'); } } export const traceViewerFixtures: Fixtures = { showTraceViewer: async ({ playwright, childProcess, browserName }, use, testInfo) => { const browsers: Browser[] = []; const tracings: any[] = []; await use(async (trace: string | undefined, { host, port, stdin } = {}) => { const command = [ 'node', path.join(__dirname, '../../packages/playwright-core/cli.js'), 'show-trace', '--port', '' + (port ?? '0'), ]; if (host) command.push('--host', host); if (stdin) command.push('--stdin'); if (trace) command.push(trace); const cp = childProcess({ command }); await cp.waitForOutput('Listening on'); const browser = await playwright.chromium.launch({ ...(browserName === 'chromium' ? {} : { channel: 'chromium' }), executablePath: process.env.CRPATH, // without this, setting FFPATH makes us launch Firefox with Chromium args }); browsers.push(browser); const page = await browser.newPage(); if (process.env.PWTEST_DEBUG_TRACE_VIEWER) { const tracing = page.context().tracing; await tracing.start({ snapshots: true, screenshots: true }); tracings.push(tracing); } const url = cp.output.match(/Listening on (http:\/\/[^\s]+)/)![1]; await page.goto(url); return new TraceViewerPage(page, cp); }); for (const [index, tracing] of tracings.entries()) { const path = testInfo.outputPath(`viewer-trace-${index}.zip`); await tracing.stop({ path }); await testInfo.attach(`viewer-trace-${index}.zip`, { path, contentType: 'application/zip' }); } for (const browser of browsers) await browser.close(); }, runAndTrace: async ({ context, showTraceViewer }, use, testInfo) => { await use(async (body: () => Promise, optsOverrides = {}) => { const traceFile = testInfo.outputPath('trace.zip'); await context.tracing.start({ snapshots: true, screenshots: true, sources: true, ...optsOverrides }); await body(); await context.tracing.stop({ path: traceFile }); if (process.env.PWTEST_DEBUG_TRACE_VIEWER) await testInfo.attach('recorded-trace.zip', { path: traceFile, contentType: 'application/zip' }); return showTraceViewer(traceFile); }); }, };