-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
build: run playwright browsers in a container for visual regression t…
…esting (#2821) --------- Co-authored-by: Jeremias Peier <[email protected]>
- Loading branch information
1 parent
4674a61
commit ebded24
Showing
12 changed files
with
230 additions
and
125 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
30 changes: 30 additions & 0 deletions
30
tools/visual-regression-testing/prepare-failed-screenshots-artifact.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { cpSync, existsSync, mkdirSync, writeFileSync, type CopySyncOptions } from 'node:fs'; | ||
|
||
import * as glob from 'glob'; | ||
|
||
// When visual regression tests have failed, we only want to pack the relevant screenshots | ||
// into the artifact transfered to the secure workflow, as uploading and downloading the full | ||
// baseline would take far longer. | ||
// Due to this we copy the necessary screenshots to /dist/screenshots-artifact which will | ||
// be moved to /dist/screenshots in the secure workflow. | ||
|
||
const screenshotDir = new URL('../../dist/screenshots/', import.meta.url); | ||
const artifactDir = new URL('../../dist/screenshots-artifact/', import.meta.url); | ||
const copyOptions: CopySyncOptions = { force: true, recursive: true }; | ||
mkdirSync(artifactDir, { recursive: true }); | ||
writeFileSync(new URL('./.keep', artifactDir), '', 'utf8'); | ||
|
||
const failedDirs = glob.sync('*/failed/', { cwd: screenshotDir }); | ||
for (const failedDir of failedDirs.map((d) => `./${d}`)) { | ||
cpSync(new URL(failedDir, screenshotDir), new URL(failedDir, artifactDir), copyOptions); | ||
} | ||
|
||
const failedFiles = glob | ||
.sync('*/failed/**/*.png', { cwd: artifactDir, ignore: '**/*-diff.png' }) | ||
.map((p) => p.replace('/failed/', '/baseline/')); | ||
for (const failedFile of failedFiles.map((f) => `./${f}`)) { | ||
const baselineFile = new URL(failedFile, screenshotDir); | ||
if (existsSync(baselineFile)) { | ||
cpSync(baselineFile, new URL(failedFile, artifactDir), copyOptions); | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
3 changes: 0 additions & 3 deletions
3
tools/visual-regression-testing/testing.Dockerfile.dockerignore
This file was deleted.
Oops, something went wrong.
56 changes: 56 additions & 0 deletions
56
tools/web-test-runner/configure-container-playwright-browser.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import type { PlaywrightLauncher, ProductType } from '@web/test-runner-playwright'; | ||
import type { Browser, LaunchOptions } from 'playwright'; | ||
import * as playwright from 'playwright'; | ||
|
||
import { playwrightWebsocketAddress } from './container-playwright-browser-plugin.js'; | ||
|
||
interface PlaywrightLauncherPrivate { | ||
// eslint-disable-next-line @typescript-eslint/naming-convention | ||
__connectBrowserPromise: Promise<Browser> | undefined; | ||
browser: Browser | undefined; | ||
product: ProductType; | ||
launchOptions: LaunchOptions; | ||
} | ||
|
||
/** | ||
* Configure playwright browser execution to run the browsers in a container. | ||
* | ||
* @see https://github.com/microsoft/playwright/issues/26482 | ||
* @param browser The playwright browser launcher. | ||
*/ | ||
export function configureRemotePlaywrightBrowser(browser: PlaywrightLauncher): void { | ||
// The original implementation calls launch instead of connect, | ||
// so we overwrite the original method with this variant which | ||
// calls connect with the websocket endpoint. | ||
async function getOrStartBrowser(this: PlaywrightLauncherPrivate): Promise<Browser> { | ||
if (this.__connectBrowserPromise) { | ||
return this.__connectBrowserPromise; | ||
} | ||
|
||
if (!this.browser || !this.browser?.isConnected()) { | ||
this.__connectBrowserPromise = (async () => { | ||
// eslint-disable-next-line import-x/namespace | ||
const browser = await playwright[this.product].connect(playwrightWebsocketAddress, { | ||
headers: { 'x-playwright-launch-options': JSON.stringify(this.launchOptions) }, | ||
}); | ||
return browser; | ||
})(); | ||
const browser = await this.__connectBrowserPromise; | ||
this.browser = browser; | ||
this.__connectBrowserPromise = undefined; | ||
} | ||
return this.browser; | ||
} | ||
(browser as any).getOrStartBrowser = getOrStartBrowser; | ||
|
||
// Inside the container the dev server from `@web/test-runner` is not available | ||
// so we need to adapt the address to point to the host machine. | ||
const startSession = browser.startSession; | ||
browser.startSession = async function (sessionId: string, url: string): Promise<void> { | ||
await startSession.call( | ||
this, | ||
sessionId, | ||
url.replace('http://localhost:', 'http://host.containers.internal:'), | ||
); | ||
}; | ||
} |
85 changes: 85 additions & 0 deletions
85
tools/web-test-runner/container-playwright-browser-plugin.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import { execSync, spawn } from 'node:child_process'; | ||
import { readFileSync } from 'node:fs'; | ||
import { platform } from 'node:os'; | ||
|
||
import type { TestRunnerPlugin } from '@web/test-runner'; | ||
|
||
function executableIsAvailable(name: string): string | null { | ||
try { | ||
execSync(`${platform().startsWith('win') ? 'where' : 'which'} ${name}`, { encoding: 'utf8' }); | ||
return name; | ||
} catch (error) { | ||
return null; | ||
} | ||
} | ||
|
||
const containerCmd = | ||
process.env.CONTAINER_CMD ?? executableIsAvailable('podman') ?? executableIsAvailable('docker'); | ||
const pkgJson = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')); | ||
const playwrightVersion = pkgJson.devDependencies.playwright; | ||
const port = 3000; | ||
export const playwrightWebsocketAddress = `ws://localhost:${port}`; | ||
// See https://github.com/microsoft/playwright/issues/26482 | ||
export const startPlaywrightServerCommand = [ | ||
containerCmd ?? 'docker', | ||
'run', | ||
'-p', | ||
`${port}:${port}`, | ||
'--rm', | ||
'--init', | ||
'--workdir=/home/pwuser', | ||
'--entrypoint=/bin/sh', | ||
`mcr.microsoft.com/playwright:v${playwrightVersion}-jammy`, | ||
`-c`, | ||
`npx -y playwright@${playwrightVersion} run-server --port ${port} --host 0.0.0.0`, | ||
]; | ||
|
||
// Reference: https://github.com/remcovaes/web-test-runner-vite-plugin | ||
export function containerPlaywrightBrowserPlugin(): TestRunnerPlugin { | ||
let abortController: AbortController; | ||
|
||
if (!containerCmd) { | ||
console.log('Either docker or podman need to be installed!'); | ||
process.exit(1); | ||
} | ||
|
||
return { | ||
name: 'remote-playwright-browser-plugin', | ||
|
||
async serverStart({ logger }) { | ||
logger.log('Starting playwright browsers in a container'); | ||
abortController = new AbortController(); | ||
await new Promise<void>((resolve, reject) => { | ||
let id: NodeJS.Timeout | undefined = undefined; | ||
spawn(startPlaywrightServerCommand[0], startPlaywrightServerCommand.slice(1), { | ||
signal: abortController.signal, | ||
}).on('error', (err) => { | ||
clearInterval(id); | ||
reject(err); | ||
}); | ||
id = setInterval(() => { | ||
const timeout = AbortSignal.timeout(950); | ||
const ws = new WebSocket(playwrightWebsocketAddress); | ||
timeout.addEventListener('abort', () => ws.close()); | ||
ws.addEventListener( | ||
'open', | ||
() => { | ||
console.log('Playwright container is ready'); | ||
ws.close(); | ||
clearInterval(id); | ||
resolve(); | ||
}, | ||
{ signal: timeout }, | ||
); | ||
}, 1000); | ||
AbortSignal.timeout(60000).addEventListener('abort', () => { | ||
clearInterval(id); | ||
reject('Failed to start container'); | ||
}); | ||
}); | ||
}, | ||
async serverStop() { | ||
abortController.abort(); | ||
}, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { spawn } from 'node:child_process'; | ||
|
||
import { startPlaywrightServerCommand } from './container-playwright-browser-plugin.js'; | ||
|
||
const abortController = new AbortController(); | ||
spawn(startPlaywrightServerCommand[0], startPlaywrightServerCommand.slice(1), { | ||
stdio: 'inherit', | ||
signal: abortController.signal, | ||
}); | ||
|
||
process.on('SIGINT', () => { | ||
abortController.abort(); | ||
process.exit(0); | ||
}); |
Oops, something went wrong.