diff options
Diffstat (limited to 'test/automation/src/playwrightBrowser.ts')
-rw-r--r-- | test/automation/src/playwrightBrowser.ts | 168 |
1 files changed, 168 insertions, 0 deletions
diff --git a/test/automation/src/playwrightBrowser.ts b/test/automation/src/playwrightBrowser.ts new file mode 100644 index 00000000000..f4e28668ab0 --- /dev/null +++ b/test/automation/src/playwrightBrowser.ts @@ -0,0 +1,168 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as playwright from '@playwright/test'; +import { ChildProcess, spawn } from 'child_process'; +import { join } from 'path'; +import * as mkdirp from 'mkdirp'; +import { URI } from 'vscode-uri'; +import { Logger, measureAndLog } from './logger'; +import type { LaunchOptions } from './code'; +import { PlaywrightDriver } from './playwrightDriver'; + +const root = join(__dirname, '..', '..', '..'); + +let port = 9000; + +export async function launch(options: LaunchOptions): Promise<{ serverProcess: ChildProcess; driver: PlaywrightDriver }> { + + // Launch server + const { serverProcess, endpoint } = await launchServer(options); + + // Launch browser + const { browser, context, page } = await launchBrowser(options, endpoint); + + return { + serverProcess, + driver: new PlaywrightDriver(browser, context, page, serverProcess, options) + }; +} + +async function launchServer(options: LaunchOptions) { + const { userDataDir, codePath, extensionsPath, logger, logsPath } = options; + const codeServerPath = codePath ?? process.env.VSCODE_REMOTE_SERVER_PATH; + const agentFolder = userDataDir; + await measureAndLog(mkdirp(agentFolder), `mkdirp(${agentFolder})`, logger); + + const env = { + VSCODE_REMOTE_SERVER_PATH: codeServerPath, + ...process.env + }; + + const args = [ + '--disable-telemetry', + '--disable-workspace-trust', + `--port${port++}`, + '--enable-smoke-test-driver', + `--extensions-dir=${extensionsPath}`, + `--server-data-dir=${agentFolder}`, + '--accept-server-license-terms', + `--logsPath=${logsPath}` + ]; + + if (options.verbose) { + args.push('--log=trace'); + } + + let serverLocation: string | undefined; + if (codeServerPath) { + const { serverApplicationName } = require(join(codeServerPath, 'product.json')); + serverLocation = join(codeServerPath, 'bin', `${serverApplicationName}${process.platform === 'win32' ? '.cmd' : ''}`); + + logger.log(`Starting built server from '${serverLocation}'`); + } else { + serverLocation = join(root, `scripts/code-server.${process.platform === 'win32' ? 'bat' : 'sh'}`); + + logger.log(`Starting server out of sources from '${serverLocation}'`); + } + + logger.log(`Storing log files into '${logsPath}'`); + + logger.log(`Command line: '${serverLocation}' ${args.join(' ')}`); + const serverProcess = spawn( + serverLocation, + args, + { env } + ); + + logger.log(`Started server for browser smoke tests (pid: ${serverProcess.pid})`); + + return { + serverProcess, + endpoint: await measureAndLog(waitForEndpoint(serverProcess, logger), 'waitForEndpoint(serverProcess)', logger) + }; +} + +async function launchBrowser(options: LaunchOptions, endpoint: string) { + const { logger, workspacePath, tracing, headless } = options; + + const browser = await measureAndLog(playwright[options.browser ?? 'chromium'].launch({ headless: headless ?? false }), 'playwright#launch', logger); + browser.on('disconnected', () => logger.log(`Playwright: browser disconnected`)); + + const context = await measureAndLog(browser.newContext(), 'browser.newContext', logger); + + if (tracing) { + try { + await measureAndLog(context.tracing.start({ screenshots: true, /* remaining options are off for perf reasons */ }), 'context.tracing.start()', logger); + } catch (error) { + logger.log(`Playwright (Browser): Failed to start playwright tracing (${error})`); // do not fail the build when this fails + } + } + + const page = await measureAndLog(context.newPage(), 'context.newPage()', logger); + await measureAndLog(page.setViewportSize({ width: 1200, height: 800 }), 'page.setViewportSize', logger); + + if (options.verbose) { + context.on('page', () => logger.log(`Playwright (Browser): context.on('page')`)); + context.on('requestfailed', e => logger.log(`Playwright (Browser): context.on('requestfailed') [${e.failure()?.errorText} for ${e.url()}]`)); + + page.on('console', e => logger.log(`Playwright (Browser): window.on('console') [${e.text()}]`)); + page.on('dialog', () => logger.log(`Playwright (Browser): page.on('dialog')`)); + page.on('domcontentloaded', () => logger.log(`Playwright (Browser): page.on('domcontentloaded')`)); + page.on('load', () => logger.log(`Playwright (Browser): page.on('load')`)); + page.on('popup', () => logger.log(`Playwright (Browser): page.on('popup')`)); + page.on('framenavigated', () => logger.log(`Playwright (Browser): page.on('framenavigated')`)); + page.on('requestfailed', e => logger.log(`Playwright (Browser): page.on('requestfailed') [${e.failure()?.errorText} for ${e.url()}]`)); + } + + page.on('pageerror', async (error) => logger.log(`Playwright (Browser) ERROR: page error: ${error}`)); + page.on('crash', () => logger.log('Playwright (Browser) ERROR: page crash')); + page.on('close', () => logger.log('Playwright (Browser): page close')); + page.on('response', async (response) => { + if (response.status() >= 400) { + logger.log(`Playwright (Browser) ERROR: HTTP status ${response.status()} for ${response.url()}`); + } + }); + + const payloadParam = `[${[ + '["enableProposedApi",""]', + '["skipWelcome", "true"]', + '["skipReleaseNotes", "true"]', + `["logLevel","${options.verbose ? 'trace' : 'info'}"]` + ].join(',')}]`; + + await measureAndLog(page.goto(`${endpoint}&${workspacePath.endsWith('.code-workspace') ? 'workspace' : 'folder'}=${URI.file(workspacePath!).path}&payload=${payloadParam}`), 'page.goto()', logger); + + return { browser, context, page }; +} + +function waitForEndpoint(server: ChildProcess, logger: Logger): Promise<string> { + return new Promise<string>((resolve, reject) => { + let endpointFound = false; + + server.stdout?.on('data', data => { + if (!endpointFound) { + logger.log(`[server] stdout: ${data}`); // log until endpoint found to diagnose issues + } + + const matches = data.toString('ascii').match(/Web UI available at (.+)/); + if (matches !== null) { + endpointFound = true; + + resolve(matches[1]); + } + }); + + server.stderr?.on('data', error => { + if (!endpointFound) { + logger.log(`[server] stderr: ${error}`); // log until endpoint found to diagnose issues + } + + if (error.toString().indexOf('EADDRINUSE') !== -1) { + reject(new Error(error)); + } + }); + }); +} |