Skip to content

feat: playwright extension #1764

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

Merged
merged 15 commits into from
May 13, 2025
5 changes: 5 additions & 0 deletions .changeset/itchy-frogs-care.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"trigger.dev": patch
---

Log images sizes for self-hosted deploys
5 changes: 5 additions & 0 deletions .changeset/witty-donkeys-unite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@trigger.dev/build": patch
---

Add playwright extension
6 changes: 6 additions & 0 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ jobs:
unitTests:
name: "🧪 Unit Tests"
runs-on: ubuntu-latest
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
steps:
- name: 🔧 Disable IPv6
run: |
Expand Down Expand Up @@ -52,10 +54,14 @@ jobs:

# ..to avoid rate limits when pulling images
- name: 🐳 Login to DockerHub
if: ${{ env.DOCKERHUB_USERNAME }}
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: 🐳 Skipping DockerHub login (no secrets available)
if: ${{ !env.DOCKERHUB_USERNAME }}
run: echo "DockerHub login skipped because secrets are not available."

- name: 📥 Download deps
run: pnpm install --frozen-lockfile
Expand Down
1 change: 1 addition & 0 deletions docs/config/extensions/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Trigger.dev provides a set of built-in extensions that you can use to customize
| :-------------------------------------------------------------------- | :----------------------------------------------------------------------------- |
| [prismaExtension](/config/extensions/prismaExtension) | Using prisma in your Trigger.dev tasks |
| [pythonExtension](/config/extensions/pythonExtension) | Execute Python scripts in your project |
| [playwright](/config/extensions/playwright) | Use Playwright in your Trigger.dev tasks |
| [puppeteer](/config/extensions/puppeteer) | Use Puppeteer in your Trigger.dev tasks |
| [ffmpeg](/config/extensions/ffmpeg) | Use FFmpeg in your Trigger.dev tasks |
| [aptGet](/config/extensions/aptGet) | Install system packages in your build image |
Expand Down
169 changes: 169 additions & 0 deletions docs/config/extensions/playwright.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
---
title: "Playwright"
sidebarTitle: "playwright"
description: "Use the playwright build extension to use Playwright with Trigger.dev"
---

If you are using [Playwright](https://playwright.dev/), you should use the Playwright build extension.

- Automatically installs Playwright and required browser dependencies
- Allows you to specify which browsers to install (chromium, firefox, webkit)
- Supports headless or non-headless mode
- Lets you specify the Playwright version, or auto-detects it
- Installs only the dependencies needed for the selected browsers to optimize build time and image size

<Note>
This extension only affects the build and deploy process, not the `dev` command.
</Note>

You can use it for a simple Playwright setup like this:

```ts
import { defineConfig } from "@trigger.dev/sdk/v3";
import { playwright } from "@trigger.dev/build/extensions/playwright";

export default defineConfig({
project: "<project ref>",
// Your other config settings...
build: {
extensions: [
playwright(),
],
},
});
```

### Options

- `browsers`: Array of browsers to install. Valid values: `"chromium"`, `"firefox"`, `"webkit"`. Default: `["chromium"]`.
- `headless`: Run browsers in headless mode. Default: `true`. If set to `false`, a virtual display (Xvfb) will be set up automatically.
- `version`: Playwright version to install. If not provided, the version will be auto-detected from your dependencies (recommended).

<Warning>
Using a different version in your app than specified here will break things. We recommend not setting this option to automatically detect the version.
</Warning>

### Custom browsers and version

```ts
import { defineConfig } from "@trigger.dev/sdk/v3";
import { playwright } from "@trigger.dev/build/extensions/playwright";

export default defineConfig({
project: "<project ref>",
build: {
extensions: [
playwright({
browsers: ["chromium", "webkit"], // optional, will use ["chromium"] if not provided
version: "1.43.1", // optional, will automatically detect the version if not provided
}),
],
},
});
```

### Headless mode

By default, browsers are run in headless mode. If you need to run browsers with a UI (for example, for debugging), set `headless: false`. This will automatically set up a virtual display using Xvfb.

```ts
import { defineConfig } from "@trigger.dev/sdk/v3";
import { playwright } from "@trigger.dev/build/extensions/playwright";

export default defineConfig({
project: "<project ref>",
build: {
extensions: [
playwright({
headless: false,
}),
],
},
});
```

### Environment variables

The extension sets the following environment variables during the build:

- `PLAYWRIGHT_BROWSERS_PATH`: Set to `/ms-playwright` so Playwright finds the installed browsers
- `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD`: Set to `1` to skip browser download at runtime
- `PLAYWRIGHT_SKIP_BROWSER_VALIDATION`: Set to `1` to skip browser validation at runtime
- `DISPLAY`: Set to `:99` if `headless: false` (for Xvfb)

## Managing browser instances

To prevent issues with waits and resumes, you can use middleware and locals to manage the browser instance. This will ensure the browser is available for the whole run, and is properly cleaned up on waits, resumes, and after the run completes.

Here's an example using `chromium`, but you can adapt it for other browsers:

```ts
import { logger, tasks, locals } from "@trigger.dev/sdk";
import { chromium, type Browser } from "playwright";

// Create a locals key for the browser instance
const PlaywrightBrowserLocal = locals.create<{ browser: Browser }>("playwright-browser");

export function getBrowser() {
return locals.getOrThrow(PlaywrightBrowserLocal).browser;
}

export function setBrowser(browser: Browser) {
locals.set(PlaywrightBrowserLocal, { browser });
}

tasks.middleware("playwright-browser", async ({ next }) => {
// Launch the browser before the task runs
const browser = await chromium.launch();
setBrowser(browser);
logger.log("[chromium]: Browser launched (middleware)");

try {
await next();
} finally {
// Always close the browser after the task completes
await browser.close();
logger.log("[chromium]: Browser closed (middleware)");
}
});

tasks.onWait("playwright-browser", async () => {
// Close the browser when the run is waiting
const browser = getBrowser();
await browser.close();
logger.log("[chromium]: Browser closed (onWait)");
});

tasks.onResume("playwright-browser", async () => {
// Relaunch the browser when the run resumes
// Note: You will have to have to manually get a new browser instance in the run function
const browser = await chromium.launch();
setBrowser(browser);
logger.log("[chromium]: Browser launched (onResume)");
});
```

You can then use `getBrowser()` in your task's `run` function to access the browser instance:

```ts
export const playwrightTestTask = task({
id: "playwright-test",
run: async () => {
const browser = getBrowser();
const page = await browser.newPage();
await page.goto("https://google.com");
await page.screenshot({ path: "screenshot.png" });
await page.close();

// Waits will gracefully close the browser
await wait.for({ seconds: 10 });

// On resume, we will re-launch the browser but you will have to manually get the new instance
const newBrowser = getBrowser();
const newPage = await newBrowser.newPage();
await newPage.goto("https://playwright.dev");
await newPage.screenshot({ path: "screenshot2.png" });
await newPage.close();
},
});
```
1 change: 1 addition & 0 deletions docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"pages": [
"config/extensions/prismaExtension",
"config/extensions/pythonExtension",
"config/extensions/playwright",
"config/extensions/puppeteer",
"config/extensions/ffmpeg",
"config/extensions/aptGet",
Expand Down
17 changes: 16 additions & 1 deletion packages/build/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"./extensions/prisma": "./src/extensions/prisma.ts",
"./extensions/audioWaveform": "./src/extensions/audioWaveform.ts",
"./extensions/typescript": "./src/extensions/typescript.ts",
"./extensions/puppeteer": "./src/extensions/puppeteer.ts"
"./extensions/puppeteer": "./src/extensions/puppeteer.ts",
"./extensions/playwright": "./src/extensions/playwright.ts"
},
"sourceDialects": [
"@triggerdotdev/source"
Expand Down Expand Up @@ -57,6 +58,9 @@
],
"extensions/puppeteer": [
"dist/commonjs/extensions/puppeteer.d.ts"
],
"extensions/playwright": [
"dist/commonjs/extensions/playwright.d.ts"
]
}
},
Expand Down Expand Up @@ -173,6 +177,17 @@
"types": "./dist/commonjs/extensions/puppeteer.d.ts",
"default": "./dist/commonjs/extensions/puppeteer.js"
}
},
"./extensions/playwright": {
"import": {
"@triggerdotdev/source": "./src/extensions/playwright.ts",
"types": "./dist/esm/extensions/playwright.d.ts",
"default": "./dist/esm/extensions/playwright.js"
},
"require": {
"types": "./dist/commonjs/extensions/playwright.d.ts",
"default": "./dist/commonjs/extensions/playwright.js"
}
}
},
"main": "./dist/commonjs/index.js",
Expand Down
Loading