feat: allow opening multiple html reporters and trace viewers (#17636)

This makes `HttpServer` accept `preferredPort` option that will first
try to listen on that port, and if that port is already in use, listen
on some available port instead.

Fixes #17201.
This commit is contained in:
Dmitry Gozman 2022-09-27 12:45:42 -07:00 committed by GitHub
parent bfd38bf7df
commit 3409a37f77
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 57 additions and 16 deletions

View File

@ -393,7 +393,7 @@ export class GridServer {
}
async start(port?: number) {
await this._server.start(port);
await this._server.start({ port });
}
gridURL(): string {

View File

@ -26,7 +26,7 @@ import { serverSideCallMetadata } from '../../instrumentation';
import { createPlaywright } from '../../playwright';
import { ProgressController } from '../../progress';
export async function showTraceViewer(traceUrls: string[], browserName: string, headless = false, port?: number): Promise<BrowserContext | undefined> {
export async function showTraceViewer(traceUrls: string[], browserName: string, headless = false, preferredPort?: number): Promise<BrowserContext | undefined> {
for (const traceUrl of traceUrls) {
if (!traceUrl.startsWith('http://') && !traceUrl.startsWith('https://') && !fs.existsSync(traceUrl)) {
// eslint-disable-next-line no-console
@ -49,7 +49,7 @@ export async function showTraceViewer(traceUrls: string[], browserName: string,
return server.serveFile(request, response, absolutePath);
});
const urlPrefix = await server.start(port);
const urlPrefix = await server.start({ preferredPort });
const traceViewerPlaywright = createPlaywright('javascript', true);
const traceViewerBrowser = isUnderTest() ? 'chromium' : browserName;

View File

@ -20,6 +20,7 @@ import path from 'path';
import { mime, wsServer } from '../utilsBundle';
import type { WebSocketServer } from '../utilsBundle';
import { assert } from './';
import { ManualPromise } from './manualPromise';
export type ServerRouteHandler = (request: http.IncomingMessage, response: http.ServerResponse) => boolean;
@ -51,15 +52,43 @@ export class HttpServer {
return this._port;
}
async start(port?: number, host = 'localhost'): Promise<string> {
private async _tryStart(port: number | undefined, host: string) {
const errorPromise = new ManualPromise();
const errorListener = (error: Error) => errorPromise.reject(error);
this._server.on('error', errorListener);
try {
this._server.listen(port, host);
await Promise.race([
new Promise(cb => this._server!.once('listening', cb)),
errorPromise,
]);
} finally {
this._server.removeListener('error', errorListener);
}
}
async start(options: { port?: number, preferredPort?: number, host?: string } = {}): Promise<string> {
assert(!this._started, 'server already started');
this._started = true;
this._server.on('connection', socket => {
this._activeSockets.add(socket);
socket.once('close', () => this._activeSockets.delete(socket));
});
this._server.listen(port, host);
await new Promise(cb => this._server!.once('listening', cb));
const host = options.host || 'localhost';
if (options.preferredPort) {
try {
await this._tryStart(options.preferredPort, host);
} catch (e) {
if (!e || !e.message || !e.message.includes('EADDRINUSE'))
throw e;
await this._tryStart(undefined, host);
}
} else {
await this._tryStart(options.port, host);
}
const address = this._server.address();
assert(address, 'Could not bind server socket');
if (!this._urlPrefix) {

View File

@ -149,7 +149,7 @@ function standaloneDefaultFolder(): string {
return reportFolderFromEnv() ?? defaultReportFolder(process.cwd());
}
export async function showHTMLReport(reportFolder: string | undefined, host: string = 'localhost', port: number = 9223, testId?: string) {
export async function showHTMLReport(reportFolder: string | undefined, host: string = 'localhost', port?: number, testId?: string) {
const folder = reportFolder ?? standaloneDefaultFolder();
try {
assert(fs.statSync(folder).isDirectory());
@ -159,7 +159,7 @@ export async function showHTMLReport(reportFolder: string | undefined, host: str
return;
}
const server = startHtmlReportServer(folder);
let url = await server.start(port, host);
let url = await server.start({ port, host, preferredPort: port ? undefined : 9223 });
console.log('');
console.log(colors.cyan(` Serving HTML report at ${url}. Press Ctrl+C to quit.`));
if (testId)

View File

@ -29,7 +29,7 @@ type BaseWorkerFixtures = {
};
export type TraceViewerFixtures = {
showTraceViewer: (trace: string[]) => Promise<TraceViewerPage>;
showTraceViewer: (trace: string[], preferredPort?: number) => Promise<TraceViewerPage>;
runAndTrace: (body: () => Promise<void>) => Promise<TraceViewerPage>;
};
@ -110,15 +110,19 @@ class TraceViewerPage {
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);
const browsers: Browser[] = [];
const contextImpls: any[] = [];
await use(async (traces: string[], preferredPort?: number) => {
const contextImpl = await showTraceViewer(traces, browserName, headless, preferredPort);
const browser = await playwright.chromium.connectOverCDP(contextImpl._browser.options.wsEndpoint);
browsers.push(browser);
contextImpls.push(contextImpl);
return new TraceViewerPage(browser.contexts()[0].pages()[0]);
});
await browser?.close();
await contextImpl?._browser.close();
for (const browser of browsers)
await browser.close();
for (const contextImpl of contextImpls)
await contextImpl._browser.close();
},
runAndTrace: async ({ context, showTraceViewer }, use, testInfo) => {

View File

@ -78,6 +78,14 @@ test('should show empty trace viewer', async ({ showTraceViewer }, testInfo) =>
await expect(traceViewer.page).toHaveTitle('Playwright Trace Viewer');
});
test('should open two trace viewers', async ({ showTraceViewer }, testInfo) => {
const preferredPort = testInfo.workerIndex + 48321;
const traceViewer1 = await showTraceViewer([testInfo.outputPath()], preferredPort);
await expect(traceViewer1.page).toHaveTitle('Playwright Trace Viewer');
const traceViewer2 = await showTraceViewer([testInfo.outputPath()], preferredPort);
await expect(traceViewer2.page).toHaveTitle('Playwright Trace Viewer');
});
test('should open simple trace viewer', async ({ showTraceViewer }) => {
const traceViewer = await showTraceViewer([traceFile]);
await expect(traceViewer.actionTitles).toHaveText([