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) { async start(port?: number) {
await this._server.start(port); await this._server.start({ port });
} }
gridURL(): string { gridURL(): string {

View File

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

View File

@ -20,6 +20,7 @@ import path from 'path';
import { mime, wsServer } from '../utilsBundle'; import { mime, wsServer } from '../utilsBundle';
import type { WebSocketServer } from '../utilsBundle'; import type { WebSocketServer } from '../utilsBundle';
import { assert } from './'; import { assert } from './';
import { ManualPromise } from './manualPromise';
export type ServerRouteHandler = (request: http.IncomingMessage, response: http.ServerResponse) => boolean; export type ServerRouteHandler = (request: http.IncomingMessage, response: http.ServerResponse) => boolean;
@ -51,15 +52,43 @@ export class HttpServer {
return this._port; 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'); assert(!this._started, 'server already started');
this._started = true; this._started = true;
this._server.on('connection', socket => { this._server.on('connection', socket => {
this._activeSockets.add(socket); this._activeSockets.add(socket);
socket.once('close', () => this._activeSockets.delete(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(); const address = this._server.address();
assert(address, 'Could not bind server socket'); assert(address, 'Could not bind server socket');
if (!this._urlPrefix) { if (!this._urlPrefix) {

View File

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

View File

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