fix(electron): better support for custom schemas (#13329)
This commit is contained in:
parent
67989e01d1
commit
6ca58e18cb
|
|
@ -333,7 +333,7 @@ export function frameSnapshotStreamer(snapshotStreamer: string) {
|
||||||
expectValue(cssText);
|
expectValue(cssText);
|
||||||
// Compensate for the extra 'cssText' text node.
|
// Compensate for the extra 'cssText' text node.
|
||||||
extraNodes++;
|
extraNodes++;
|
||||||
return checkAndReturn(['style', {}, cssText]);
|
return checkAndReturn([nodeName, {}, cssText]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const attrs: { [attr: string]: string } = {};
|
const attrs: { [attr: string]: string } = {};
|
||||||
|
|
|
||||||
|
|
@ -40,10 +40,16 @@ export class SnapshotRenderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): RenderedFrameSnapshot {
|
render(): RenderedFrameSnapshot {
|
||||||
const visit = (n: NodeSnapshot, snapshotIndex: number): string => {
|
const visit = (n: NodeSnapshot, snapshotIndex: number, parentTag: string | undefined): string => {
|
||||||
// Text node.
|
// Text node.
|
||||||
if (typeof n === 'string')
|
if (typeof n === 'string') {
|
||||||
return escapeText(n);
|
const text = escapeText(n);
|
||||||
|
// Best-effort Electron support: rewrite custom protocol in url() links in stylesheets.
|
||||||
|
// Old snapshotter was sending lower-case.
|
||||||
|
if (parentTag === 'STYLE' || parentTag === 'style')
|
||||||
|
return rewriteURLsInStyleSheetForCustomProtocol(text);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
if (!(n as any)._string) {
|
if (!(n as any)._string) {
|
||||||
if (Array.isArray(n[0])) {
|
if (Array.isArray(n[0])) {
|
||||||
|
|
@ -53,7 +59,7 @@ export class SnapshotRenderer {
|
||||||
const nodes = snapshotNodes(this._snapshots[referenceIndex]);
|
const nodes = snapshotNodes(this._snapshots[referenceIndex]);
|
||||||
const nodeIndex = n[0][1];
|
const nodeIndex = n[0][1];
|
||||||
if (nodeIndex >= 0 && nodeIndex < nodes.length)
|
if (nodeIndex >= 0 && nodeIndex < nodes.length)
|
||||||
(n as any)._string = visit(nodes[nodeIndex], referenceIndex);
|
(n as any)._string = visit(nodes[nodeIndex], referenceIndex, parentTag);
|
||||||
}
|
}
|
||||||
} else if (typeof n[0] === 'string') {
|
} else if (typeof n[0] === 'string') {
|
||||||
// Element node.
|
// Element node.
|
||||||
|
|
@ -68,7 +74,7 @@ export class SnapshotRenderer {
|
||||||
}
|
}
|
||||||
builder.push('>');
|
builder.push('>');
|
||||||
for (let i = 2; i < n.length; i++)
|
for (let i = 2; i < n.length; i++)
|
||||||
builder.push(visit(n[i], snapshotIndex));
|
builder.push(visit(n[i], snapshotIndex, n[0]));
|
||||||
if (!autoClosing.has(n[0]))
|
if (!autoClosing.has(n[0]))
|
||||||
builder.push('</', n[0], '>');
|
builder.push('</', n[0], '>');
|
||||||
(n as any)._string = builder.join('');
|
(n as any)._string = builder.join('');
|
||||||
|
|
@ -81,7 +87,7 @@ export class SnapshotRenderer {
|
||||||
};
|
};
|
||||||
|
|
||||||
const snapshot = this._snapshot;
|
const snapshot = this._snapshot;
|
||||||
let html = visit(snapshot.html, this._index);
|
let html = visit(snapshot.html, this._index, undefined);
|
||||||
if (!html)
|
if (!html)
|
||||||
return { html: '', pageId: snapshot.pageId, frameId: snapshot.frameId, index: this._index };
|
return { html: '', pageId: snapshot.pageId, frameId: snapshot.frameId, index: this._index };
|
||||||
|
|
||||||
|
|
@ -275,8 +281,12 @@ function snapshotScript() {
|
||||||
return `\n(${applyPlaywrightAttributes.toString()})()`;
|
return `\n(${applyPlaywrightAttributes.toString()})()`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const schemas = ['about:', 'blob:', 'data:', 'file:', 'ftp:', 'http:', 'https:', 'mailto:', 'sftp:', 'ws:', 'wss:' ];
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Best-effort Electron support: rewrite custom protocol in DOM.
|
||||||
|
* vscode-file://vscode-app/ -> https://pw-vscode-file--vscode-app/
|
||||||
|
*/
|
||||||
|
const schemas = ['about:', 'blob:', 'data:', 'file:', 'ftp:', 'http:', 'https:', 'mailto:', 'sftp:', 'ws:', 'wss:' ];
|
||||||
const kLegacyBlobPrefix = 'http://playwright.bloburl/#';
|
const kLegacyBlobPrefix = 'http://playwright.bloburl/#';
|
||||||
|
|
||||||
export function rewriteURLForCustomProtocol(href: string): string {
|
export function rewriteURLForCustomProtocol(href: string): string {
|
||||||
|
|
@ -298,9 +308,24 @@ export function rewriteURLForCustomProtocol(href: string): string {
|
||||||
// Rewrite blob and custom schemas.
|
// Rewrite blob and custom schemas.
|
||||||
const prefix = 'pw-' + url.protocol.slice(0, url.protocol.length - 1);
|
const prefix = 'pw-' + url.protocol.slice(0, url.protocol.length - 1);
|
||||||
url.protocol = 'https:';
|
url.protocol = 'https:';
|
||||||
url.hostname = url.hostname ? `${prefix}.${url.hostname}` : prefix;
|
url.hostname = url.hostname ? `${prefix}--${url.hostname}` : prefix;
|
||||||
return url.toString();
|
return url.toString();
|
||||||
} catch {
|
} catch {
|
||||||
return href;
|
return href;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Best-effort Electron support: rewrite custom protocol in inline stylesheets.
|
||||||
|
* vscode-file://vscode-app/ -> https://pw-vscode-file--vscode-app/
|
||||||
|
*/
|
||||||
|
const urlInCSSRegex = /url\(['"]?([\w-]+:)\/\//ig;
|
||||||
|
|
||||||
|
function rewriteURLsInStyleSheetForCustomProtocol(text: string): string {
|
||||||
|
return text.replace(urlInCSSRegex, (match: string, protocol: string) => {
|
||||||
|
const isBlob = protocol === 'blob:';
|
||||||
|
if (!isBlob && schemas.includes(protocol))
|
||||||
|
return match;
|
||||||
|
return match.replace(protocol + '//', `https://pw-${protocol.slice(0, -1)}--`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
133
tests/config/traceViewerFixtures.ts
Normal file
133
tests/config/traceViewerFixtures.ts
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
/**
|
||||||
|
* 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, Frame, Locator, Page, Browser, BrowserContext } from '@playwright/test';
|
||||||
|
import { showTraceViewer } from '../../packages/playwright-core/lib/server/trace/viewer/traceViewer';
|
||||||
|
|
||||||
|
type BaseTestFixtures = {
|
||||||
|
context: BrowserContext;
|
||||||
|
};
|
||||||
|
|
||||||
|
type BaseWorkerFixtures = {
|
||||||
|
headless: boolean;
|
||||||
|
browser: Browser;
|
||||||
|
browserName: 'chromium' | 'firefox' | 'webkit';
|
||||||
|
playwright: typeof import('@playwright/test');
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TraceViewerFixtures = {
|
||||||
|
showTraceViewer: (trace: string[]) => Promise<TraceViewerPage>;
|
||||||
|
runAndTrace: (body: () => Promise<void>) => Promise<TraceViewerPage>;
|
||||||
|
};
|
||||||
|
|
||||||
|
class TraceViewerPage {
|
||||||
|
actionTitles: Locator;
|
||||||
|
callLines: Locator;
|
||||||
|
consoleLines: Locator;
|
||||||
|
consoleLineMessages: Locator;
|
||||||
|
consoleStacks: Locator;
|
||||||
|
stackFrames: Locator;
|
||||||
|
networkRequests: Locator;
|
||||||
|
snapshotContainer: Locator;
|
||||||
|
|
||||||
|
constructor(public page: Page) {
|
||||||
|
this.actionTitles = page.locator('.action-title');
|
||||||
|
this.callLines = page.locator('.call-line');
|
||||||
|
this.consoleLines = page.locator('.console-line');
|
||||||
|
this.consoleLineMessages = page.locator('.console-line-message');
|
||||||
|
this.consoleStacks = page.locator('.console-stack');
|
||||||
|
this.stackFrames = page.locator('.stack-trace-frame');
|
||||||
|
this.networkRequests = page.locator('.network-request-title');
|
||||||
|
this.snapshotContainer = page.locator('.snapshot-container');
|
||||||
|
}
|
||||||
|
|
||||||
|
async actionIconsText(action: string) {
|
||||||
|
const entry = await this.page.waitForSelector(`.action-entry:has-text("${action}")`);
|
||||||
|
await entry.waitForSelector('.action-icon-value:visible');
|
||||||
|
return await entry.$$eval('.action-icon-value:visible', ee => ee.map(e => e.textContent));
|
||||||
|
}
|
||||||
|
|
||||||
|
async actionIcons(action: string) {
|
||||||
|
return await this.page.waitForSelector(`.action-entry:has-text("${action}") .action-icons`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async selectAction(title: string, ordinal: number = 0) {
|
||||||
|
await this.page.locator(`.action-title:has-text("${title}")`).nth(ordinal).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async selectSnapshot(name: string) {
|
||||||
|
await this.page.click(`.snapshot-tab .tab-label:has-text("${name}")`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async showConsoleTab() {
|
||||||
|
await this.page.click('text="Console"');
|
||||||
|
}
|
||||||
|
|
||||||
|
async showSourceTab() {
|
||||||
|
await this.page.click('text="Source"');
|
||||||
|
}
|
||||||
|
|
||||||
|
async showNetworkTab() {
|
||||||
|
await this.page.click('text="Network"');
|
||||||
|
}
|
||||||
|
|
||||||
|
async eventBars() {
|
||||||
|
await this.page.waitForSelector('.timeline-bar.event:visible');
|
||||||
|
const list = await this.page.$$eval('.timeline-bar.event:visible', ee => ee.map(e => e.className));
|
||||||
|
const set = new Set<string>();
|
||||||
|
for (const item of list) {
|
||||||
|
for (const className of item.split(' '))
|
||||||
|
set.add(className);
|
||||||
|
}
|
||||||
|
const result = [...set];
|
||||||
|
return result.sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
async snapshotFrame(actionName: string, ordinal: number = 0, hasSubframe: boolean = false): Promise<Frame> {
|
||||||
|
const existing = this.page.mainFrame().childFrames()[0];
|
||||||
|
await Promise.all([
|
||||||
|
existing ? existing.waitForNavigation() as any : Promise.resolve(),
|
||||||
|
this.selectAction(actionName, ordinal),
|
||||||
|
]);
|
||||||
|
while (this.page.frames().length < (hasSubframe ? 3 : 2))
|
||||||
|
await this.page.waitForEvent('frameattached');
|
||||||
|
return this.page.mainFrame().childFrames()[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const traceViewerFixtures: Fixtures<TraceViewerFixtures, {}, BaseTestFixtures, BaseWorkerFixtures> = {
|
||||||
|
showTraceViewer: async ({ playwright, browserName, headless }, use) => {
|
||||||
|
let browser: Browser;
|
||||||
|
let contextImpl: any;
|
||||||
|
await use(async (traces: string[]) => {
|
||||||
|
contextImpl = await showTraceViewer(traces, browserName, headless);
|
||||||
|
browser = await playwright.chromium.connectOverCDP(contextImpl._browser.options.wsEndpoint);
|
||||||
|
return new TraceViewerPage(browser.contexts()[0].pages()[0]);
|
||||||
|
});
|
||||||
|
await browser?.close();
|
||||||
|
await contextImpl?._browser.close();
|
||||||
|
},
|
||||||
|
|
||||||
|
runAndTrace: async ({ context, showTraceViewer }, use, testInfo) => {
|
||||||
|
await use(async (body: () => Promise<void>) => {
|
||||||
|
const traceFile = testInfo.outputPath('trace.zip');
|
||||||
|
await context.tracing.start({ snapshots: true, screenshots: true, sources: true });
|
||||||
|
await body();
|
||||||
|
await context.tracing.stop({ path: traceFile });
|
||||||
|
return showTraceViewer([traceFile]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
3
tests/electron/assets/imported.css
Normal file
3
tests/electron/assets/imported.css
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
button {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
15
tests/electron/assets/index.html
Normal file
15
tests/electron/assets/index.html
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="vscode-file://style.css">
|
||||||
|
<style>
|
||||||
|
@import url("vscode-file://imported.css");
|
||||||
|
button {
|
||||||
|
background: url(background.png);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<button>Click me</button>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
3
tests/electron/assets/style.css
Normal file
3
tests/electron/assets/style.css
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
button {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,11 @@
|
||||||
const { app } = require('electron');
|
const { app, protocol } = require('electron');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
app.on('window-all-closed', e => e.preventDefault());
|
app.on('window-all-closed', e => e.preventDefault());
|
||||||
|
|
||||||
|
app.whenReady().then(() => {
|
||||||
|
protocol.registerFileProtocol('vscode-file', (request, callback) => {
|
||||||
|
const url = request.url.substring('vscode-file'.length + 3);
|
||||||
|
callback({ path: path.join(__dirname, 'assets', url) });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
47
tests/electron/electron-tracing.spec.ts
Normal file
47
tests/electron/electron-tracing.spec.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
/**
|
||||||
|
* 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 { electronTest as test, expect } from './electronTest';
|
||||||
|
|
||||||
|
test.skip(({ trace }) => trace === 'on');
|
||||||
|
// test.slow();
|
||||||
|
|
||||||
|
test('should record trace', async ({ newWindow, server, runAndTrace }) => {
|
||||||
|
const traceViewer = await runAndTrace(async () => {
|
||||||
|
const window = await newWindow();
|
||||||
|
await window.goto(server.PREFIX + '/input/button.html');
|
||||||
|
await window.click('button');
|
||||||
|
expect(await window.evaluate('result')).toBe('Clicked');
|
||||||
|
});
|
||||||
|
await expect(traceViewer.actionTitles).toHaveText([
|
||||||
|
/page.goto/,
|
||||||
|
/page.click/,
|
||||||
|
/page.evaluate/,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should support custom protocol', async ({ electronApp, newWindow, server, runAndTrace }) => {
|
||||||
|
const window = await newWindow();
|
||||||
|
await electronApp.evaluate(async ({ BrowserWindow }) => {
|
||||||
|
BrowserWindow.getAllWindows()[0].loadURL('vscode-file://index.html');
|
||||||
|
});
|
||||||
|
const traceViewer = await runAndTrace(async () => {
|
||||||
|
await window.click('button');
|
||||||
|
});
|
||||||
|
const frame = await traceViewer.snapshotFrame('page.click');
|
||||||
|
await expect(frame.locator('button')).toHaveCSS('color', 'rgb(255, 0, 0)');
|
||||||
|
await expect(frame.locator('button')).toHaveCSS('font-weight', '700');
|
||||||
|
});
|
||||||
|
|
@ -16,8 +16,9 @@
|
||||||
|
|
||||||
import { baseTest } from '../config/baseTest';
|
import { baseTest } from '../config/baseTest';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { ElectronApplication, Page } from 'playwright-core';
|
import { ElectronApplication, Page } from '@playwright/test';
|
||||||
import { PageTestFixtures, PageWorkerFixtures } from '../page/pageTestApi';
|
import { PageTestFixtures, PageWorkerFixtures } from '../page/pageTestApi';
|
||||||
|
import { traceViewerFixtures, TraceViewerFixtures } from '../config/traceViewerFixtures';
|
||||||
export { expect } from '@playwright/test';
|
export { expect } from '@playwright/test';
|
||||||
|
|
||||||
type ElectronTestFixtures = PageTestFixtures & {
|
type ElectronTestFixtures = PageTestFixtures & {
|
||||||
|
|
@ -27,7 +28,7 @@ type ElectronTestFixtures = PageTestFixtures & {
|
||||||
|
|
||||||
const electronVersion = require('electron/package.json').version;
|
const electronVersion = require('electron/package.json').version;
|
||||||
|
|
||||||
export const electronTest = baseTest.extend<ElectronTestFixtures, PageWorkerFixtures>({
|
export const electronTest = baseTest.extend<TraceViewerFixtures>(traceViewerFixtures).extend<ElectronTestFixtures, PageWorkerFixtures>({
|
||||||
browserVersion: [electronVersion, { scope: 'worker' }],
|
browserVersion: [electronVersion, { scope: 'worker' }],
|
||||||
browserMajorVersion: [Number(electronVersion.split('.')[0]), { scope: 'worker' }],
|
browserMajorVersion: [Number(electronVersion.split('.')[0]), { scope: 'worker' }],
|
||||||
isAndroid: [false, { scope: 'worker' }],
|
isAndroid: [false, { scope: 'worker' }],
|
||||||
|
|
@ -71,4 +72,8 @@ export const electronTest = baseTest.extend<ElectronTestFixtures, PageWorkerFixt
|
||||||
page: async ({ newWindow }, run) => {
|
page: async ({ newWindow }, run) => {
|
||||||
await run(await newWindow());
|
await run(await newWindow());
|
||||||
},
|
},
|
||||||
|
|
||||||
|
context: async ({ electronApp }, run) => {
|
||||||
|
await run(electronApp.context());
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -68,11 +68,11 @@ it.describe('snapshots', () => {
|
||||||
it('should respect inline CSSOM change', async ({ page, toImpl, snapshotter }) => {
|
it('should respect inline CSSOM change', async ({ page, toImpl, snapshotter }) => {
|
||||||
await page.setContent('<style>button { color: red; }</style><button>Hello</button>');
|
await page.setContent('<style>button { color: red; }</style><button>Hello</button>');
|
||||||
const snapshot1 = await snapshotter.captureSnapshot(toImpl(page), 'snapshot1');
|
const snapshot1 = await snapshotter.captureSnapshot(toImpl(page), 'snapshot1');
|
||||||
expect(distillSnapshot(snapshot1)).toBe('<style>button { color: red; }</style><BUTTON>Hello</BUTTON>');
|
expect(distillSnapshot(snapshot1)).toBe('<STYLE>button { color: red; }</STYLE><BUTTON>Hello</BUTTON>');
|
||||||
|
|
||||||
await page.evaluate(() => { (document.styleSheets[0].cssRules[0] as any).style.color = 'blue'; });
|
await page.evaluate(() => { (document.styleSheets[0].cssRules[0] as any).style.color = 'blue'; });
|
||||||
const snapshot2 = await snapshotter.captureSnapshot(toImpl(page), 'snapshot2');
|
const snapshot2 = await snapshotter.captureSnapshot(toImpl(page), 'snapshot2');
|
||||||
expect(distillSnapshot(snapshot2)).toBe('<style>button { color: blue; }</style><BUTTON>Hello</BUTTON>');
|
expect(distillSnapshot(snapshot2)).toBe('<STYLE>button { color: blue; }</STYLE><BUTTON>Hello</BUTTON>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should respect node removal', async ({ page, toImpl, snapshotter }) => {
|
it('should respect node removal', async ({ page, toImpl, snapshotter }) => {
|
||||||
|
|
|
||||||
|
|
@ -14,110 +14,12 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { TraceViewerFixtures, traceViewerFixtures } from '../config/traceViewerFixtures';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import type { Browser, Frame, Locator, Page } from 'playwright-core';
|
import { expect, playwrightTest } from '../config/browserTest';
|
||||||
import { showTraceViewer } from '../../packages/playwright-core/lib/server/trace/viewer/traceViewer';
|
|
||||||
import { playwrightTest, expect } from '../config/browserTest';
|
|
||||||
|
|
||||||
class TraceViewerPage {
|
const test = playwrightTest.extend<TraceViewerFixtures>(traceViewerFixtures);
|
||||||
actionTitles: Locator;
|
|
||||||
callLines: Locator;
|
|
||||||
consoleLines: Locator;
|
|
||||||
consoleLineMessages: Locator;
|
|
||||||
consoleStacks: Locator;
|
|
||||||
stackFrames: Locator;
|
|
||||||
networkRequests: Locator;
|
|
||||||
snapshotContainer: Locator;
|
|
||||||
|
|
||||||
constructor(public page: Page) {
|
|
||||||
this.actionTitles = page.locator('.action-title');
|
|
||||||
this.callLines = page.locator('.call-line');
|
|
||||||
this.consoleLines = page.locator('.console-line');
|
|
||||||
this.consoleLineMessages = page.locator('.console-line-message');
|
|
||||||
this.consoleStacks = page.locator('.console-stack');
|
|
||||||
this.stackFrames = page.locator('.stack-trace-frame');
|
|
||||||
this.networkRequests = page.locator('.network-request-title');
|
|
||||||
this.snapshotContainer = page.locator('.snapshot-container');
|
|
||||||
}
|
|
||||||
|
|
||||||
async actionIconsText(action: string) {
|
|
||||||
const entry = await this.page.waitForSelector(`.action-entry:has-text("${action}")`);
|
|
||||||
await entry.waitForSelector('.action-icon-value:visible');
|
|
||||||
return await entry.$$eval('.action-icon-value:visible', ee => ee.map(e => e.textContent));
|
|
||||||
}
|
|
||||||
|
|
||||||
async actionIcons(action: string) {
|
|
||||||
return await this.page.waitForSelector(`.action-entry:has-text("${action}") .action-icons`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async selectAction(title: string, ordinal: number = 0) {
|
|
||||||
await this.page.locator(`.action-title:has-text("${title}")`).nth(ordinal).click();
|
|
||||||
}
|
|
||||||
|
|
||||||
async selectSnapshot(name: string) {
|
|
||||||
await this.page.click(`.snapshot-tab .tab-label:has-text("${name}")`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async showConsoleTab() {
|
|
||||||
await this.page.click('text="Console"');
|
|
||||||
}
|
|
||||||
|
|
||||||
async showSourceTab() {
|
|
||||||
await this.page.click('text="Source"');
|
|
||||||
}
|
|
||||||
|
|
||||||
async showNetworkTab() {
|
|
||||||
await this.page.click('text="Network"');
|
|
||||||
}
|
|
||||||
|
|
||||||
async eventBars() {
|
|
||||||
await this.page.waitForSelector('.timeline-bar.event:visible');
|
|
||||||
const list = await this.page.$$eval('.timeline-bar.event:visible', ee => ee.map(e => e.className));
|
|
||||||
const set = new Set<string>();
|
|
||||||
for (const item of list) {
|
|
||||||
for (const className of item.split(' '))
|
|
||||||
set.add(className);
|
|
||||||
}
|
|
||||||
const result = [...set];
|
|
||||||
return result.sort();
|
|
||||||
}
|
|
||||||
|
|
||||||
async snapshotFrame(actionName: string, ordinal: number = 0, hasSubframe: boolean = false): Promise<Frame> {
|
|
||||||
const existing = this.page.mainFrame().childFrames()[0];
|
|
||||||
await Promise.all([
|
|
||||||
existing ? existing.waitForNavigation() as any : Promise.resolve(),
|
|
||||||
this.selectAction(actionName, ordinal),
|
|
||||||
]);
|
|
||||||
while (this.page.frames().length < (hasSubframe ? 3 : 2))
|
|
||||||
await this.page.waitForEvent('frameattached');
|
|
||||||
return this.page.mainFrame().childFrames()[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const test = playwrightTest.extend<{ showTraceViewer: (trace: string[]) => Promise<TraceViewerPage>, runAndTrace: (body: () => Promise<void>) => Promise<TraceViewerPage> }>({
|
|
||||||
showTraceViewer: async ({ playwright, browserName, headless }, use) => {
|
|
||||||
let browser: Browser;
|
|
||||||
let contextImpl: any;
|
|
||||||
await use(async (traces: string[]) => {
|
|
||||||
contextImpl = await showTraceViewer(traces, browserName, headless);
|
|
||||||
browser = await playwright.chromium.connectOverCDP({ endpointURL: contextImpl._browser.options.wsEndpoint });
|
|
||||||
return new TraceViewerPage(browser.contexts()[0].pages()[0]);
|
|
||||||
});
|
|
||||||
await browser?.close();
|
|
||||||
await contextImpl?._browser.close();
|
|
||||||
},
|
|
||||||
|
|
||||||
runAndTrace: async ({ context, showTraceViewer }, use, testInfo) => {
|
|
||||||
await use(async (body: () => Promise<void>) => {
|
|
||||||
const traceFile = testInfo.outputPath('trace.zip');
|
|
||||||
await context.tracing.start({ snapshots: true, screenshots: true, sources: true });
|
|
||||||
await body();
|
|
||||||
await context.tracing.stop({ path: traceFile });
|
|
||||||
return showTraceViewer([traceFile]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test.skip(({ trace }) => trace === 'on');
|
test.skip(({ trace }) => trace === 'on');
|
||||||
test.slow();
|
test.slow();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue