Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: introduce docker integration #16382

Merged
merged 15 commits into from
Sep 9, 2022
47 changes: 47 additions & 0 deletions docs/src/docker.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,50 @@ The image will be tagged as `playwright:localbuild-focal` and could be run as:
```
docker run --rm -it playwright:localbuild /bin/bash
```

## (Experimental) Playwright Test Docker Integration
aslushnikov marked this conversation as resolved.
Show resolved Hide resolved
aslushnikov marked this conversation as resolved.
Show resolved Hide resolved
* langs: js

Playwright Test now ships an **experimental** Docker integration.
With this integration, **only** browser binaries are running inside a Docker container,
while all the code is still running on the host operating system.

Docker container provides a consistent environment, eliminating browser rendering
differences across platforms. Playwright Test will automatically proxy host network traffic
into the container, so browsers can access servers running on the host.

:::note
Docker integration requires Docker installed & running on your computer.
See https://docs.docker.com/get-docker/
:::

:::note
If you use [Docker Desktop](https://www.docker.com/products/docker-desktop/), make sure to increase
default CPU and mem limit for better performance.
:::

Docker integration usage:

1. Build a local Docker image that will be used to run containers. This step
needs to be done only once.
```bash js
npx playwright docker build
```

2. Run Docker container in the background.
```bash js
npx playwright docker start
```

3. Run tests inside Docker container. Note that this command accepts all the same arguments
as a regular `npx playwright test` command.
```bash js
npx playwright docker test
```

Note that this command will detect running Docker container, and auto-launch it if needed.

4. Finally, stop Docker container when it is no longer needed.
```bash js
npx playwright docker stop
```
aslushnikov marked this conversation as resolved.
Show resolved Hide resolved
1 change: 1 addition & 0 deletions packages/playwright-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"./lib/grid/gridServer": "./lib/grid/gridServer.js",
"./lib/outofprocess": "./lib/outofprocess.js",
"./lib/utils": "./lib/utils/index.js",
"./lib/common/userAgent": "./lib/common/userAgent.js",
"./lib/utils/comparators": "./lib/utils/comparators.js",
"./lib/utils/eventsHelper": "./lib/utils/eventsHelper.js",
"./lib/utils/fileUtils": "./lib/utils/fileUtils.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright-core/src/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ program
program
.command('show-trace [trace...]')
.option('-b, --browser <browserType>', 'browser to use, one of cr, chromium, ff, firefox, wk, webkit', 'chromium')
.description('Show trace viewer')
.description('show trace viewer')
.action(function(traces, options) {
if (options.browser === 'cr')
options.browser = 'chromium';
Expand Down
1 change: 1 addition & 0 deletions packages/playwright-test/src/DEPS.list
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[*]
./utilsBundle.ts
docker/
matchers/
reporters/
third_party/
Expand Down
90 changes: 83 additions & 7 deletions packages/playwright-test/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@
/* eslint-disable no-console */

import type { Command } from 'playwright-core/lib/utilsBundle';
import * as docker from './docker/docker';
import fs from 'fs';
import url from 'url';
import path from 'path';
import { colors } from 'playwright-core/lib/utilsBundle';
import { Runner, builtInReporters, kDefaultConfigFiles } from './runner';
import type { ConfigCLIOverrides } from './runner';
import { stopProfiling, startProfiling } from './profiler';
Expand All @@ -29,14 +31,67 @@ import { baseFullConfig, defaultTimeout, fileIsModule } from './loader';
import type { TraceMode } from './types';

export function addTestCommands(program: Command) {
addTestCommand(program);
addTestCommand(program, false /* isDocker */);
addShowReportCommand(program);
addListFilesCommand(program);
addDockerCommand(program);
}

function addTestCommand(program: Command) {
function addDockerCommand(program: Command) {
const dockerCommand = program.command('docker')
.description(`run tests in Docker (EXPERIMENTAL)`);

dockerCommand.command('build')
.description('build local docker image')
.action(async function(options) {
await docker.ensureDockerEngineIsRunningOrDie();
await docker.buildImage();
});

dockerCommand.command('start')
.description('start docker container')
.action(async function(options) {
await docker.ensureDockerEngineIsRunningOrDie();
let info = await docker.containerInfo();
if (!info) {
process.stdout.write(`Starting docker container... `);
const time = Date.now();
info = await docker.ensureContainerOrDie();
const deltaMs = (Date.now() - time);
console.log('Done in ' + (deltaMs / 1000).toFixed(1) + 's');
}
console.log([
`- VNC session: ${info.vncSession}`,
`- Run tests with browsers inside container:`,
` npx playwright docker test`,
`- Stop container *manually* when it is no longer needed:`,
` npx playwright docker stop`,
].join('\n'));
});

dockerCommand.command('delete-image', { hidden: true })
.description('delete docker image, if any')
.action(async function(options) {
await docker.ensureDockerEngineIsRunningOrDie();
await docker.deleteImage();
});

dockerCommand.command('stop')
.description('stop docker container')
.action(async function(options) {
await docker.ensureDockerEngineIsRunningOrDie();
await docker.stopContainer();
});

addTestCommand(dockerCommand, true /* isDocker */);
}

function addTestCommand(program: Command, isDocker: boolean) {
const command = program.command('test [test-filter...]');
command.description('Run tests with Playwright Test');
if (isDocker)
command.description('run tests with Playwright Test and browsers inside docker container');
else
command.description('run tests with Playwright Test');
command.option('--browser <browser>', `Browser to use for tests, one of "all", "chromium", "firefox" or "webkit" (default: "chromium")`);
command.option('--headed', `Run tests in headed browsers (default: headless)`);
command.option('--debug', `Run tests with Playwright Inspector. Shortcut for "PWDEBUG=1" environment variable and "--timeout=0 --maxFailures=1 --headed --workers=1" options`);
Expand Down Expand Up @@ -64,6 +119,27 @@ function addTestCommand(program: Command) {
command.option('-x', `Stop after the first failure`);
command.action(async (args, opts) => {
try {
if (isDocker && !process.env.PW_TS_ESM_ON) {
console.log(colors.dim('Using docker container to run browsers.'));
await docker.ensureDockerEngineIsRunningOrDie();
let info = await docker.containerInfo();
if (!info) {
process.stdout.write(colors.dim(`Starting docker container... `));
const time = Date.now();
info = await docker.ensureContainerOrDie();
const deltaMs = (Date.now() - time);
console.log(colors.dim('Done in ' + (deltaMs / 1000).toFixed(1) + 's'));
console.log(colors.dim('The Docker container will keep running after tests finished.'));
console.log(colors.dim('Stop manually using:'));
console.log(colors.dim(' npx playwright docker stop'));
}
console.log(colors.dim(`View screen: ${info.vncSession}`));
process.env.PW_TEST_CONNECT_WS_ENDPOINT = info.wsEndpoint;
process.env.PW_TEST_CONNECT_HEADERS = JSON.stringify({
'x-playwright-proxy': '*',
});
process.env.PW_TEST_SNAPSHOT_SUFFIX = 'docker';
}
await runTests(args, opts);
} catch (e) {
console.error(e);
Expand All @@ -75,10 +151,10 @@ Arguments [test-filter...]:
Pass arguments to filter test files. Each argument is treated as a regular expression.

Examples:
$ npx playwright test my.spec.ts
$ npx playwright test some.spec.ts:42
$ npx playwright test --headed
$ npx playwright test --browser=webkit`);
$ npx playwright${isDocker ? ' docker ' : ' '}test my.spec.ts
$ npx playwright${isDocker ? ' docker ' : ' '}test some.spec.ts:42
$ npx playwright${isDocker ? ' docker ' : ' '}test --headed
$ npx playwright${isDocker ? ' docker ' : ' '}test --browser=webkit`);
}

function addListFilesCommand(program: Command) {
Expand Down
74 changes: 74 additions & 0 deletions packages/playwright-test/src/docker/build_docker_image.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
export NOVNC_REF='1.3.0'
export WEBSOCKIFY_REF='0.10.0'
export DEBIAN_FRONTEND=noninteractive

# Install FluxBox, VNC & noVNC
mkdir -p /opt/bin && chmod +x /dev/shm \
&& apt-get update && apt-get install -y unzip fluxbox x11vnc \
&& curl -L -o noVNC.zip "https://github.com/novnc/noVNC/archive/v${NOVNC_REF}.zip" \
&& unzip -x noVNC.zip \
&& rm -rf noVNC-${NOVNC_REF}/{docs,tests} \
&& mv noVNC-${NOVNC_REF} /opt/bin/noVNC \
&& cp /opt/bin/noVNC/vnc.html /opt/bin/noVNC/index.html \
&& rm noVNC.zip \
&& curl -L -o websockify.zip "https://github.com/novnc/websockify/archive/v${WEBSOCKIFY_REF}.zip" \
&& unzip -x websockify.zip \
&& rm websockify.zip \
&& rm -rf websockify-${WEBSOCKIFY_REF}/{docs,tests} \
&& mv websockify-${WEBSOCKIFY_REF} /opt/bin/noVNC/utils/websockify

# Configure FluxBox menus
mkdir /root/.fluxbox
cd /ms-playwright-agent
cat <<'EOF' | node > /root/.fluxbox/menu
const { chromium, firefox, webkit } = require('playwright-core');
console.log(`
[begin] (fluxbox)
[submenu] (Browsers) {}
[exec] (Chromium) { ${chromium.executablePath()} --no-sandbox --test-type= } <>
[exec] (Firefox) { ${firefox.executablePath()} } <>
[exec] (WebKit) { ${webkit.executablePath()} } <>
[end]
[include] (/etc/X11/fluxbox/fluxbox-menu)
[end]
`);
EOF

# Create entrypoint.sh
cat <<'EOF' > /entrypoint.sh
#!/bin/bash
set -e
SCREEN_WIDTH=1360
SCREEN_HEIGHT=1020
SCREEN_DEPTH=24
SCREEN_DPI=96
GEOMETRY="$SCREEN_WIDTH""x""$SCREEN_HEIGHT""x""$SCREEN_DEPTH"
nohup /usr/bin/xvfb-run --server-num=$DISPLAY_NUM \
--listen-tcp \
--server-args="-screen 0 "$GEOMETRY" -fbdir /var/tmp -dpi "$SCREEN_DPI" -listen tcp -noreset -ac +extension RANDR" \
/usr/bin/fluxbox -display "$DISPLAY" >/dev/null 2>&1 &
for i in $(seq 1 500); do
if xdpyinfo -display $DISPLAY >/dev/null 2>&1; then
break
fi
echo "Waiting for Xvfb..."
sleep 0.2
done
nohup x11vnc -forever -shared -rfbport 5900 -rfbportv6 5900 -display "$DISPLAY" >/dev/null 2>&1 &
nohup /opt/bin/noVNC/utils/novnc_proxy --listen 7900 --vnc localhost:5900 >/dev/null 2>&1 &
cd /ms-playwright-agent
fbsetbg -c /ms-playwright-agent/node_modules/playwright-core/lib/server/chromium/appIcon.png
NOVNC_UUID=$(cat /proc/sys/kernel/random/uuid)
echo "novnc is listening on http://127.0.0.1:7900?path=$NOVNC_UUID&resize=scale&autoconnect=1"
PW_UUID=$(cat /proc/sys/kernel/random/uuid)
npx playwright run-server --port=5400 --path=/$PW_UUID
EOF
chmod 755 /entrypoint.sh
Loading