UNPKG

@web/test-runner-chrome

Version:
235 lines (206 loc) 7.79 kB
import * as puppeteerCore from 'puppeteer-core'; import { Browser, Page, LaunchOptions, launch as puppeteerCoreLaunch, BrowserContext, } from 'puppeteer-core'; import { BrowserLauncher, TestRunnerCoreConfig } from '@web/test-runner-core'; import { findExecutablePath } from './findExecutablePath.js'; import { ChromeLauncherPage } from './ChromeLauncherPage.js'; function capitalize(str: string) { return `${str[0].toUpperCase()}${str.substring(1)}`; } const errorHelp = 'This could be because of a mismatch between the version of puppeteer and Chrome or Chromium. ' + 'Try updating either of them, or adjust the executablePath option to point to another browser installation. ' + 'Use the --puppeteer flag to run tests with bundled compatible version of Chromium.'; interface CreateArgs { browser: Browser; config: TestRunnerCoreConfig; } export type CreateBrowserContextFn = (args: CreateArgs) => BrowserContext | Promise<BrowserContext>; export type CreatePageFn = (args: CreateArgs & { context: BrowserContext }) => Promise<Page>; export class ChromeLauncher implements BrowserLauncher { public name: string; public type = 'puppeteer'; public concurrency?: number; private launchOptions: LaunchOptions; private customPuppeteer?: typeof puppeteerCore; private createBrowserContextFn: CreateBrowserContextFn; private createPageFn: CreatePageFn; private config?: TestRunnerCoreConfig; private testFiles?: string[]; private browser?: Browser; private browserContext?: BrowserContext; private debugBrowser?: Browser; private debugBrowserContext?: BrowserContext; private cachedExecutablePath?: string; private activePages = new Map<string, ChromeLauncherPage>(); private activeDebugPages = new Map<string, ChromeLauncherPage>(); private inactivePages: ChromeLauncherPage[] = []; private __startBrowserPromise?: Promise<{ browser: Browser; context: BrowserContext }>; constructor( launchOptions: LaunchOptions, createBrowserContextFn: CreateBrowserContextFn, createPageFn: CreatePageFn, customPuppeteer?: typeof puppeteerCore, concurrency?: number, ) { this.launchOptions = launchOptions; this.customPuppeteer = customPuppeteer; this.createBrowserContextFn = createBrowserContextFn; this.createPageFn = createPageFn; this.concurrency = concurrency; if (!customPuppeteer) { // without a custom puppeteer, we use the locally installed chrome this.name = 'Chrome'; } else if (!this.launchOptions?.browser || this.launchOptions.browser === 'chrome') { // with puppeteer we use the a packaged chromium, puppeteer calls it chrome but we // should call it chromium to avoid confusion this.name = 'Chromium'; } else { // otherwise take the browser name directly this.name = capitalize(this.launchOptions.browser); } } async initialize(config: TestRunnerCoreConfig, testFiles: string[]) { this.config = config; this.testFiles = testFiles; } launchBrowser(options: LaunchOptions = {}) { const mergedOptions: LaunchOptions = { headless: true, ...this.launchOptions, ...options, }; if (this.customPuppeteer) { // launch using a custom puppeteer instance return this.customPuppeteer.launch(mergedOptions).catch(error => { if (mergedOptions.browser === 'firefox') { console.warn( '\nUsing puppeteer with firefox is experimental.\n' + 'Check the docs at https://github.com/modernweb-dev/web/tree/master/packages/test-runner-puppeteer' + ' to learn how to set it up.\n', ); } throw error; }); } // launch using puppeteer-core, connecting to an installed browser // add a default executable path if the user did not provide any if (!mergedOptions.executablePath) { if (!this.cachedExecutablePath) { this.cachedExecutablePath = findExecutablePath(); } mergedOptions.executablePath = this.cachedExecutablePath; } return puppeteerCoreLaunch(mergedOptions).catch(error => { console.error(''); console.error( `Failed to launch local browser installed at ${mergedOptions.executablePath}. ${errorHelp}`, ); console.error(''); throw error; }); } async startBrowser(options: LaunchOptions = {}) { const browser = await this.launchBrowser(options); const context = await this.createBrowserContextFn({ config: this.config!, browser }); return { browser, context }; } async stop() { if (this.browser?.isConnected()) { await this.browser.close(); } if (this.debugBrowser?.isConnected()) { await this.debugBrowser.close(); } } async startSession(sessionId: string, url: string) { const { browser, context } = await this.getOrStartBrowser(); let page: ChromeLauncherPage; if (this.inactivePages.length === 0) { page = await this.createNewPage(browser, context); } else { page = this.inactivePages.pop()!; } this.activePages.set(sessionId, page); await page.runSession(url, !!this.config?.coverage); } isActive(sessionId: string) { return this.activePages.has(sessionId); } getBrowserUrl(sessionId: string) { return this.getPage(sessionId).url(); } async startDebugSession(sessionId: string, url: string) { if (!this.debugBrowser || !this.debugBrowserContext) { this.debugBrowser = await this.launchBrowser({ devtools: true, headless: false }); this.debugBrowserContext = await this.createBrowserContextFn({ config: this.config!, browser: this.debugBrowser, }); } const page = await this.createNewPage(this.debugBrowser, this.debugBrowserContext); this.activeDebugPages.set(sessionId, page); page.puppeteerPage.on('close', () => { this.activeDebugPages.delete(sessionId); }); await page.runSession(url, true); } async createNewPage(browser: Browser, context: BrowserContext) { const puppeteerPagePromise = this.createPageFn({ config: this.config!, browser, context, }).catch(error => { if (!this.customPuppeteer) { console.error(`Failed to create page with puppeteer. ${errorHelp}`); } throw error; }); return new ChromeLauncherPage( this.config!, this.testFiles!, this.launchOptions?.browser ?? 'chromium', await puppeteerPagePromise, ); } async stopSession(sessionId: string) { const page = this.activePages.get(sessionId); if (!page) { throw new Error(`No page for session ${sessionId}`); } if (page.puppeteerPage.isClosed()) { throw new Error(`Session ${sessionId} is already stopped`); } const result = await page.stopSession(); this.activePages.delete(sessionId); this.inactivePages.push(page); return result; } private async getOrStartBrowser(): Promise<{ browser: Browser; context: BrowserContext }> { if (this.__startBrowserPromise) { return this.__startBrowserPromise; } if (!this.browser || !this.browser?.isConnected() || !this.browserContext) { this.__startBrowserPromise = this.startBrowser(); const { browser, context } = await this.__startBrowserPromise; this.browser = browser; this.browserContext = context; this.__startBrowserPromise = undefined; } return { browser: this.browser, context: this.browserContext }; } getPage(sessionId: string) { const page = this.activePages.get(sessionId)?.puppeteerPage ?? this.activeDebugPages.get(sessionId)?.puppeteerPage; if (!page) { throw new Error(`Could not find a page for session ${sessionId}`); } return page; } }