/** * Copyright (c) Microsoft Corporation. * * Licensed under the Apache License, Version 2.0 (the 'License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* eslint-disable no-console */ import path from 'path'; import fs from 'fs'; import { colors } from 'playwright-core/lib/utilsBundle'; import { spawnAsync } from 'playwright-core/lib/utils/spawnAsync'; import * as utils from 'playwright-core/lib/utils'; import { getPlaywrightVersion } from 'playwright-core/lib/common/userAgent'; import * as dockerApi from './dockerApi'; import type { TestRunnerPlugin } from '../plugins'; import type { FullConfig, Reporter, Suite } from '../../types/testReporter'; const VRT_IMAGE_DISTRO = 'focal'; const VRT_IMAGE_NAME = `playwright:local-${getPlaywrightVersion()}-${VRT_IMAGE_DISTRO}`; const VRT_CONTAINER_NAME = `playwright-${getPlaywrightVersion()}-${VRT_IMAGE_DISTRO}`; const VRT_CONTAINER_LABEL_NAME = 'dev.playwright.vrt-service.version'; const VRT_CONTAINER_LABEL_VALUE = '1'; export async function startPlaywrightContainer() { await checkDockerEngineIsRunningOrDie(); let info = await containerInfo(); if (!info) { process.stdout.write(`Starting docker container... `); const time = Date.now(); info = await ensurePlaywrightContainerOrDie(); const deltaMs = (Date.now() - time); console.log('Done in ' + (deltaMs / 1000).toFixed(1) + 's'); } console.log([ `- View screen:`, ` ${info.vncSession}`, `- Run tests with browsers inside container:`, ` npx playwright docker test`, `- Stop background container *manually* when you are done working with tests:`, ` npx playwright docker stop`, ].join('\n')); } export async function stopAllPlaywrightContainers() { await checkDockerEngineIsRunningOrDie(); const allContainers = await dockerApi.listContainers(); const vrtContainers = allContainers.filter(container => container.labels[VRT_CONTAINER_LABEL_NAME] === VRT_CONTAINER_LABEL_VALUE); await Promise.all(vrtContainers.map(container => dockerApi.stopContainer({ containerId: container.containerId, waitUntil: 'removed', }))); } export async function deletePlaywrightImage() { await checkDockerEngineIsRunningOrDie(); const dockerImage = await findDockerImage(VRT_IMAGE_NAME); if (!dockerImage) return; if (await containerInfo()) await stopAllPlaywrightContainers(); await dockerApi.removeImage(dockerImage.imageId); } export async function buildPlaywrightImage() { await checkDockerEngineIsRunningOrDie(); const isDevelopmentMode = getPlaywrightVersion().includes('next'); let baseImageName = `mcr.microsoft.com/playwright:v${getPlaywrightVersion()}-${VRT_IMAGE_DISTRO}`; // 1. Build or pull base image. if (isDevelopmentMode) { // Use our docker build scripts in development mode! if (!process.env.PWTEST_DOCKER_BASE_IMAGE) { const arch = process.arch === 'arm64' ? '--arm64' : '--amd64'; console.error(utils.wrapInASCIIBox([ `You are in DEVELOPMENT mode!`, ``, `1. Build local base image`, ` ./utils/docker/build.sh ${arch} ${VRT_IMAGE_DISTRO} playwright:localbuild`, `2. Use the local base to build VRT image:`, ` PWTEST_DOCKER_BASE_IMAGE=playwright:localbuild npx playwright docker build`, ].join('\n'), 1)); process.exit(1); } baseImageName = process.env.PWTEST_DOCKER_BASE_IMAGE; } else { const { code } = await spawnAsync('docker', ['pull', baseImageName], { stdio: 'inherit' }); if (code !== 0) throw new Error('Failed to pull docker image!'); } // 2. Find pulled docker image const dockerImage = await findDockerImage(baseImageName); if (!dockerImage) throw new Error(`Failed to pull ${baseImageName}`); // 3. Launch container and install VNC in it console.log(`Building ${VRT_IMAGE_NAME}...`); const buildScriptText = await fs.promises.readFile(path.join(__dirname, 'build_docker_image.sh'), 'utf8'); const containerId = await dockerApi.launchContainer({ imageId: dockerImage.imageId, autoRemove: false, command: ['/bin/bash', '-c', buildScriptText], waitUntil: 'not-running', }); // 4. Commit a new image based on the launched container with installed VNC & noVNC. const [vrtRepo, vrtTag] = VRT_IMAGE_NAME.split(':'); await dockerApi.commitContainer({ containerId, repo: vrtRepo, tag: vrtTag, entrypoint: '/entrypoint.sh', env: { 'DISPLAY_NUM': '99', 'DISPLAY': ':99', }, }); await dockerApi.removeContainer(containerId); console.log(`Done!`); } export const dockerPlugin: TestRunnerPlugin = { name: 'playwright:docker', async setup(config: FullConfig, configDir: string, rootSuite: Suite, reporter: Reporter) { if (!process.env.PLAYWRIGHT_DOCKER) return; const print = (text: string) => reporter.onStdOut?.(text); const println = (text: string) => reporter.onStdOut?.(text + '\n'); println(colors.dim('Using docker container to run browsers.')); await checkDockerEngineIsRunningOrDie(); let info = await containerInfo(); if (!info) { print(colors.dim(`Starting docker container... `)); const time = Date.now(); info = await ensurePlaywrightContainerOrDie(); const deltaMs = (Date.now() - time); println(colors.dim('Done in ' + (deltaMs / 1000).toFixed(1) + 's')); println(colors.dim('The Docker container will keep running after tests finished.')); println(colors.dim('Stop manually using:')); println(colors.dim(' npx playwright docker stop')); } println(colors.dim(`View screen: ${info.vncSession}`)); println(''); process.env.PW_TEST_CONNECT_WS_ENDPOINT = info.wsEndpoint; process.env.PW_TEST_CONNECT_HEADERS = JSON.stringify({ 'x-playwright-proxy': '*', }); }, }; interface ContainerInfo { wsEndpoint: string; vncSession: string; } async function containerInfo(): Promise { const allContainers = await dockerApi.listContainers(); const pwDockerImage = await findDockerImage(VRT_IMAGE_NAME); const container = allContainers.find(container => container.imageId === pwDockerImage?.imageId && container.state === 'running'); if (!container) return undefined; const logLines = await dockerApi.getContainerLogs(container.containerId); const containerUrlToHostUrl = (address: string) => { const url = new URL(address); const portBinding = container.portBindings.find(binding => binding.containerPort === +url.port); if (!portBinding) return undefined; url.host = portBinding.ip; url.port = portBinding.hostPort + ''; return url.toString(); }; const WS_LINE_PREFIX = 'Listening on ws://'; const webSocketLine = logLines.find(line => line.startsWith(WS_LINE_PREFIX)); const NOVNC_LINE_PREFIX = 'novnc is listening on '; const novncLine = logLines.find(line => line.startsWith(NOVNC_LINE_PREFIX)); if (!novncLine || !webSocketLine) return undefined; const wsEndpoint = containerUrlToHostUrl('ws://' + webSocketLine.substring(WS_LINE_PREFIX.length)); const vncSession = containerUrlToHostUrl(novncLine.substring(NOVNC_LINE_PREFIX.length)); return wsEndpoint && vncSession ? { wsEndpoint, vncSession } : undefined; } async function ensurePlaywrightContainerOrDie(): Promise { const pwImage = await findDockerImage(VRT_IMAGE_NAME); if (!pwImage) { console.error('\n' + utils.wrapInASCIIBox([ `Failed to find local docker image.`, `Please build local docker image with the following command:`, ``, ` npx playwright docker build`, ``, `<3 Playwright Team`, ].join('\n'), 1)); process.exit(1); } let info = await containerInfo(); if (info) return info; await dockerApi.launchContainer({ imageId: pwImage.imageId, name: VRT_CONTAINER_NAME, autoRemove: true, ports: [5400, 7900], labels: { [VRT_CONTAINER_LABEL_NAME]: VRT_CONTAINER_LABEL_VALUE, }, }); // Wait for the service to become available. const startTime = Date.now(); const timeouts = [0, 100, 100, 200, 500, 1000]; do { await new Promise(x => setTimeout(x, timeouts.shift() ?? 1000)); info = await containerInfo(); } while (!info && Date.now() < startTime + 60000); if (!info) throw new Error('Failed to launch docker container!'); return info; } async function checkDockerEngineIsRunningOrDie() { if (await dockerApi.checkEngineRunning()) return; console.error(utils.wrapInASCIIBox([ `Docker is not running!`, `Please install and launch docker:`, ``, ` https://docs.docker.com/get-docker`, ``, ].join('\n'), 1)); process.exit(1); } async function findDockerImage(imageName: string): Promise { const images = await dockerApi.listImages(); return images.find(image => image.names.includes(imageName)); }