2022-09-09 15:25:42 -07:00
|
|
|
/**
|
|
|
|
* 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';
|
2022-09-13 16:14:49 -07:00
|
|
|
import { colors } from 'playwright-core/lib/utilsBundle';
|
2022-09-09 15:25:42 -07:00
|
|
|
import { spawnAsync } from 'playwright-core/lib/utils/spawnAsync';
|
|
|
|
import * as utils from 'playwright-core/lib/utils';
|
|
|
|
import { getPlaywrightVersion } from 'playwright-core/lib/common/userAgent';
|
2022-09-13 10:55:11 -07:00
|
|
|
import * as dockerApi from './dockerApi';
|
2022-09-13 17:05:37 -07:00
|
|
|
import type { TestRunnerPlugin } from '../plugins';
|
|
|
|
import type { FullConfig, Reporter, Suite } from '../../types/testReporter';
|
2022-09-09 15:25:42 -07:00
|
|
|
|
|
|
|
const VRT_IMAGE_DISTRO = 'focal';
|
|
|
|
const VRT_IMAGE_NAME = `playwright:local-${getPlaywrightVersion()}-${VRT_IMAGE_DISTRO}`;
|
|
|
|
const VRT_CONTAINER_NAME = `playwright-${getPlaywrightVersion()}-${VRT_IMAGE_DISTRO}`;
|
|
|
|
|
2022-09-13 16:14:49 -07:00
|
|
|
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 stopPlaywrightContainer() {
|
|
|
|
await checkDockerEngineIsRunningOrDie();
|
|
|
|
|
|
|
|
const container = await findRunningDockerContainer();
|
|
|
|
if (!container)
|
|
|
|
return;
|
|
|
|
await dockerApi.stopContainer({
|
|
|
|
containerId: container.containerId,
|
|
|
|
waitUntil: 'removed',
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function deletePlaywrightImage() {
|
|
|
|
await checkDockerEngineIsRunningOrDie();
|
|
|
|
|
2022-09-09 15:25:42 -07:00
|
|
|
const dockerImage = await findDockerImage(VRT_IMAGE_NAME);
|
|
|
|
if (!dockerImage)
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (await containerInfo())
|
2022-09-13 16:14:49 -07:00
|
|
|
await stopPlaywrightContainer();
|
2022-09-13 10:55:11 -07:00
|
|
|
await dockerApi.removeImage(dockerImage.imageId);
|
2022-09-09 15:25:42 -07:00
|
|
|
}
|
|
|
|
|
2022-09-13 16:14:49 -07:00
|
|
|
export async function buildPlaywrightImage() {
|
|
|
|
await checkDockerEngineIsRunningOrDie();
|
|
|
|
|
2022-09-09 15:25:42 -07:00
|
|
|
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');
|
2022-09-13 10:55:11 -07:00
|
|
|
const containerId = await dockerApi.launchContainer({
|
|
|
|
imageId: dockerImage.imageId,
|
2022-09-09 15:25:42 -07:00
|
|
|
autoRemove: false,
|
|
|
|
command: ['/bin/bash', '-c', buildScriptText],
|
2022-09-13 10:55:11 -07:00
|
|
|
waitUntil: 'not-running',
|
2022-09-09 15:25:42 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
// 4. Commit a new image based on the launched container with installed VNC & noVNC.
|
|
|
|
const [vrtRepo, vrtTag] = VRT_IMAGE_NAME.split(':');
|
2022-09-13 10:55:11 -07:00
|
|
|
await dockerApi.commitContainer({
|
|
|
|
containerId,
|
|
|
|
repo: vrtRepo,
|
|
|
|
tag: vrtTag,
|
|
|
|
entrypoint: '/entrypoint.sh',
|
|
|
|
env: {
|
|
|
|
'DISPLAY_NUM': '99',
|
|
|
|
'DISPLAY': ':99',
|
|
|
|
},
|
2022-09-09 15:25:42 -07:00
|
|
|
});
|
2022-09-13 10:55:11 -07:00
|
|
|
await dockerApi.removeContainer(containerId);
|
2022-09-09 15:25:42 -07:00
|
|
|
console.log(`Done!`);
|
|
|
|
}
|
|
|
|
|
2022-09-13 17:05:37 -07:00
|
|
|
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': '*',
|
|
|
|
});
|
|
|
|
},
|
|
|
|
};
|
2022-09-13 16:14:49 -07:00
|
|
|
|
2022-09-09 15:25:42 -07:00
|
|
|
interface ContainerInfo {
|
|
|
|
wsEndpoint: string;
|
|
|
|
vncSession: string;
|
|
|
|
}
|
|
|
|
|
2022-09-13 16:14:49 -07:00
|
|
|
async function containerInfo(): Promise<ContainerInfo|undefined> {
|
2022-09-13 13:23:04 -07:00
|
|
|
const container = await findRunningDockerContainer();
|
|
|
|
if (!container)
|
2022-09-09 15:25:42 -07:00
|
|
|
return undefined;
|
2022-09-13 13:23:04 -07:00
|
|
|
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();
|
|
|
|
};
|
|
|
|
|
2022-09-09 15:25:42 -07:00
|
|
|
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));
|
2022-09-13 13:23:04 -07:00
|
|
|
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;
|
2022-09-09 15:25:42 -07:00
|
|
|
}
|
|
|
|
|
2022-09-13 16:14:49 -07:00
|
|
|
async function ensurePlaywrightContainerOrDie(): Promise<ContainerInfo> {
|
2022-09-09 15:25:42 -07:00
|
|
|
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;
|
|
|
|
|
2022-09-13 10:55:11 -07:00
|
|
|
await dockerApi.launchContainer({
|
|
|
|
imageId: pwImage.imageId,
|
2022-09-09 15:25:42 -07:00
|
|
|
name: VRT_CONTAINER_NAME,
|
|
|
|
autoRemove: true,
|
|
|
|
ports: [5400, 7900],
|
|
|
|
});
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
|
2022-09-13 16:14:49 -07:00
|
|
|
async function checkDockerEngineIsRunningOrDie() {
|
2022-09-13 10:55:11 -07:00
|
|
|
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);
|
2022-09-09 15:25:42 -07:00
|
|
|
}
|
|
|
|
|
2022-09-13 10:55:11 -07:00
|
|
|
async function findDockerImage(imageName: string): Promise<dockerApi.DockerImage|undefined> {
|
|
|
|
const images = await dockerApi.listImages();
|
|
|
|
return images.find(image => image.names.includes(imageName));
|
2022-09-09 15:25:42 -07:00
|
|
|
}
|
|
|
|
|
2022-09-13 13:23:04 -07:00
|
|
|
async function findRunningDockerContainer(): Promise<dockerApi.DockerContainer|undefined> {
|
2022-09-13 10:55:11 -07:00
|
|
|
const containers = await dockerApi.listContainers();
|
2022-09-09 15:25:42 -07:00
|
|
|
const dockerImage = await findDockerImage(VRT_IMAGE_NAME);
|
2022-09-13 10:55:11 -07:00
|
|
|
const container = dockerImage ? containers.find(container => container.imageId === dockerImage.imageId) : undefined;
|
2022-09-13 13:23:04 -07:00
|
|
|
return container?.state === 'running' ? container : undefined;
|
2022-09-09 15:25:42 -07:00
|
|
|
}
|
|
|
|
|