Path: blob/main/test/automation/src/playwrightBrowser.ts
5241 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as playwright from '@playwright/test';6import { ChildProcess, spawn } from 'child_process';7import { join } from 'path';8import * as fs from 'fs';9import { URI } from 'vscode-uri';10import { Logger, measureAndLog } from './logger';11import type { LaunchOptions } from './code';12import { PlaywrightDriver } from './playwrightDriver';1314const root = join(__dirname, '..', '..', '..');1516let port = 9000;1718export async function launch(options: LaunchOptions): Promise<{ serverProcess: ChildProcess; driver: PlaywrightDriver }> {1920// Launch server21const { serverProcess, endpoint } = await launchServer(options);2223// Launch browser24const { browser, context, page, pageLoadedPromise } = await launchBrowser(options, endpoint);2526return {27serverProcess,28driver: new PlaywrightDriver(browser, context, page, serverProcess, pageLoadedPromise, options)29};30}3132async function launchServer(options: LaunchOptions) {33const { userDataDir, codePath, extensionsPath, logger, logsPath } = options;34const serverLogsPath = join(logsPath, 'server');35const codeServerPath = codePath ?? process.env.VSCODE_REMOTE_SERVER_PATH;36const agentFolder = userDataDir;3738const env = {39VSCODE_REMOTE_SERVER_PATH: codeServerPath,40...process.env41};4243const args = [44'--disable-telemetry',45'--disable-experiments',46'--disable-workspace-trust',47`--port=${port++}`,48'--enable-smoke-test-driver',49'--accept-server-license-terms',50`--logsPath=${serverLogsPath}`51];52if (agentFolder) {53await measureAndLog(() => fs.promises.mkdir(agentFolder, { recursive: true }), `mkdirp(${agentFolder})`, logger);54args.push(`--server-data-dir=${agentFolder}`);55}56if (extensionsPath) {57args.push(`--extensions-dir=${extensionsPath}`);58}5960if (options.verbose) {61args.push('--log=trace');62}63if (options.extensionDevelopmentPath) {64args.push(`--extensionDevelopmentPath=${options.extensionDevelopmentPath}`);65}6667let serverLocation: string | undefined;68if (codeServerPath) {69const { serverApplicationName } = require(join(codeServerPath, 'product.json'));70serverLocation = join(codeServerPath, 'bin', `${serverApplicationName}${process.platform === 'win32' ? '.cmd' : ''}`);7172logger.log(`Starting built server from '${serverLocation}'`);73} else {74serverLocation = join(root, `scripts/code-server.${process.platform === 'win32' ? 'bat' : 'sh'}`);7576logger.log(`Starting server out of sources from '${serverLocation}'`);77}7879logger.log(`Storing log files into '${serverLogsPath}'`);8081logger.log(`Command line: '${serverLocation}' ${args.join(' ')}`);82const shell: boolean = (process.platform === 'win32');83const serverProcess = spawn(84serverLocation,85args,86{ env, shell }87);8889logger.log(`Started server for browser smoke tests (pid: ${serverProcess.pid})`);9091return {92serverProcess,93endpoint: await measureAndLog(() => waitForEndpoint(serverProcess, logger), 'waitForEndpoint(serverProcess)', logger)94};95}9697async function launchBrowser(options: LaunchOptions, endpoint: string) {98const { logger, workspacePath, tracing, snapshots, headless } = options;99100const playwrightImpl = options.playwright ?? playwright;101const [browserType, browserChannel] = (options.browser ?? 'chromium').split('-');102const browser = await measureAndLog(() => playwrightImpl[browserType as unknown as 'chromium' | 'webkit' | 'firefox'].launch({103headless: headless ?? false,104timeout: 0,105channel: browserChannel,106}), 'playwright#launch', logger);107108browser.on('disconnected', () => logger.log(`Playwright: browser disconnected`));109110const context = await measureAndLog(111() => browser.newContext({112recordVideo: options.videosPath113? {114dir: options.videosPath,115size: { width: 1920, height: 1080 }116} : undefined,117}),118'browser.newContext',119logger120);121122if (tracing) {123try {124await measureAndLog(() => context.tracing.start({ screenshots: true, snapshots }), 'context.tracing.start()', logger);125} catch (error) {126logger.log(`Playwright (Browser): Failed to start playwright tracing (${error})`); // do not fail the build when this fails127}128}129130const page = await measureAndLog(() => context.newPage(), 'context.newPage()', logger);131await measureAndLog(() => page.setViewportSize({ width: 1440, height: 900 }), 'page.setViewportSize', logger);132133if (options.verbose) {134context.on('page', () => logger.log(`Playwright (Browser): context.on('page')`));135context.on('requestfailed', e => logger.log(`Playwright (Browser): context.on('requestfailed') [${e.failure()?.errorText} for ${e.url()}]`));136137page.on('console', e => logger.log(`Playwright (Browser): window.on('console') [${e.text()}]`));138page.on('dialog', () => logger.log(`Playwright (Browser): page.on('dialog')`));139page.on('domcontentloaded', () => logger.log(`Playwright (Browser): page.on('domcontentloaded')`));140page.on('load', () => logger.log(`Playwright (Browser): page.on('load')`));141page.on('popup', () => logger.log(`Playwright (Browser): page.on('popup')`));142page.on('framenavigated', () => logger.log(`Playwright (Browser): page.on('framenavigated')`));143page.on('requestfailed', e => logger.log(`Playwright (Browser): page.on('requestfailed') [${e.failure()?.errorText} for ${e.url()}]`));144}145146page.on('pageerror', async (error) => logger.log(`Playwright (Browser) ERROR: page error: ${error}`));147page.on('crash', () => logger.log('Playwright (Browser) ERROR: page crash'));148page.on('close', () => logger.log('Playwright (Browser): page close'));149page.on('response', async (response) => {150if (response.status() >= 400) {151logger.log(`Playwright (Browser) ERROR: HTTP status ${response.status()} for ${response.url()}`);152}153});154155const payloadParam = `[${[156'["enableProposedApi",""]',157'["skipWelcome", "true"]',158'["skipReleaseNotes", "true"]',159`["logLevel","${options.verbose ? 'trace' : 'info'}"]`160].join(',')}]`;161162// Build URL with optional workspace path163let url = `${endpoint}&`;164if (workspacePath) {165const workspaceParam = workspacePath.endsWith('.code-workspace') ? 'workspace' : 'folder';166url += `${workspaceParam}=${URI.file(workspacePath).path}&`;167}168url += `payload=${payloadParam}`;169170const gotoPromise = measureAndLog(() => page.goto(url), 'page.goto()', logger);171const pageLoadedPromise = page.waitForLoadState('load');172173await gotoPromise;174175return { browser, context, page, pageLoadedPromise };176}177178function waitForEndpoint(server: ChildProcess, logger: Logger): Promise<string> {179return new Promise<string>((resolve, reject) => {180let endpointFound = false;181182server.stdout?.on('data', data => {183if (!endpointFound) {184logger.log(`[server] stdout: ${data}`); // log until endpoint found to diagnose issues185}186187const matches = data.toString('ascii').match(/Web UI available at (.+)/);188if (matches !== null) {189endpointFound = true;190191resolve(matches[1]);192}193});194195server.stderr?.on('data', error => {196if (!endpointFound) {197logger.log(`[server] stderr: ${error}`); // log until endpoint found to diagnose issues198}199200if (error.toString().indexOf('EADDRINUSE') !== -1) {201reject(new Error(error));202}203});204});205}206207208