@ricsam/isolate-playwright
v0.1.14
Published
Playwright bridge for running browser tests in isolated-vm V8 sandbox
Downloads
1,542
Maintainers
Readme
@ricsam/isolate-playwright
Playwright bridge for running browser automation in a V8 sandbox. Execute untrusted Playwright code against a real browser page while keeping the logic isolated.
Installation
npm add @ricsam/isolate-playwright playwrightUsage with isolate-runtime (Recommended)
The easiest way to use this package is through @ricsam/isolate-runtime:
Script Mode (No Tests)
Run browser automation scripts without a test framework:
import { createRuntime } from "@ricsam/isolate-runtime";
import { chromium } from "playwright";
import { defaultPlaywrightHandler } from "@ricsam/isolate-playwright/client";
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
const runtime = await createRuntime({
playwright: {
handler: defaultPlaywrightHandler(page),
console: true, // Print browser console logs to stdout
},
});
// Run a script - page is available, but expect is not
await runtime.eval(`
await page.goto("/");
const title = await page.title();
console.log("Page title:", title);
`);
// Get collected network data
const data = runtime.playwright.getCollectedData();
console.log("Network requests:", data.networkRequests.length);
await runtime.dispose();
await browser.close();Test Mode (With Test Framework)
For tests, enable testEnvironment which provides describe, it, and expect. Playwright extends expect with locator matchers:
import { createRuntime } from "@ricsam/isolate-runtime";
import { chromium } from "playwright";
import { defaultPlaywrightHandler } from "@ricsam/isolate-playwright/client";
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
const runtime = await createRuntime({
testEnvironment: true, // Provides describe, it, expect
playwright: {
handler: defaultPlaywrightHandler(page),
onEvent: (event) => {
if (event.type === "browserConsoleLog") {
console.log("[browser]", event.level, event.stdout);
} else if (event.type === "networkRequest") {
console.log("Request:", event.url);
}
},
},
});
await runtime.eval(`
describe("homepage", () => {
it("loads correctly", async () => {
await page.goto("/");
const heading = page.getByRole("heading", { name: "Example Domain" });
await expect(heading).toBeVisible(); // Locator matcher from playwright
expect(await page.title()).toBe("Example Domain"); // Primitive matcher from test-environment
});
});
`);
// Run tests using test-environment
const results = await runtime.testEnvironment.runTests();
console.log(`${results.passed}/${results.total} tests passed`);
// Get collected browser data
const data = runtime.playwright.getCollectedData();
console.log("Browser console logs:", data.browserConsoleLogs);
await runtime.dispose();
await browser.close();Low-level Usage (Direct ivm)
For advanced use cases with direct isolated-vm access:
import ivm from "isolated-vm";
import { chromium } from "playwright";
import { setupPlaywright } from "@ricsam/isolate-playwright";
import { setupTestEnvironment, runTests } from "@ricsam/isolate-test-environment";
const browser = await chromium.launch();
const page = await browser.newPage();
const isolate = new ivm.Isolate();
const context = await isolate.createContext();
// Setup test-environment first (provides describe, it, expect)
await setupTestEnvironment(context);
// Then setup playwright (extends expect with locator matchers)
const handle = await setupPlaywright(context, {
page,
timeout: 30000,
onEvent: (event) => {
if (event.type === "networkRequest") {
console.log("Request:", event.url);
} else if (event.type === "networkResponse") {
console.log("Response:", event.status);
} else if (event.type === "browserConsoleLog") {
console.log(`[${event.level}]`, event.stdout);
}
},
});
// Load and run untrusted test code
await context.eval(`
describe("homepage", () => {
it("loads correctly", async () => {
await page.goto("/");
const heading = page.getByRole("heading", { name: "Example Domain" });
await expect(heading).toBeVisible();
});
});
`);
// Run tests
const results = await runTests(context);
console.log(`${results.passed}/${results.total} tests passed`);
// Cleanup
handle.dispose();
context.release();
isolate.dispose();
await browser.close();Handler-based API (for Remote Execution)
For daemon/client architectures where the browser runs on the client, use the
handler-first contract (playwright.handler):
import { defaultPlaywrightHandler, setupPlaywright, type PlaywrightCallback } from "@ricsam/isolate-playwright";
import { chromium } from "playwright";
// On the client: create handler from page
const browser = await chromium.launch();
const page = await browser.newPage();
const handler: PlaywrightCallback = defaultPlaywrightHandler(page, {
timeout: 30000,
});
// On the daemon: setup playwright with handler (instead of page)
const handle = await setupPlaywright(context, {
handler,
onEvent: (event) => sendToClient("playwright-event", event),
});Injected Globals (in isolate)
page- Page object with navigation and locator methodscontext- BrowserContext object withnewPage(), cookie methodsbrowser- Browser object withnewContext()methodLocator- Locator class for element interactionsexpect- Extended with locator matchers (only if test-environment is loaded first)
Page Methods
page.goto(url, options?)- Navigate to URLpage.reload()- Reload pagepage.goBack()- Navigate backpage.goForward()- Navigate forwardpage.url()- Get current URL (sync)page.title()- Get page titlepage.content()- Get page HTMLpage.click(selector)- Click element (shorthand)page.fill(selector, value)- Fill input (shorthand)page.waitForSelector(selector, options?)- Wait for elementpage.waitForTimeout(ms)- Wait for millisecondspage.waitForLoadState(state?)- Wait for load statepage.waitForURL(url, options?)- Wait for URL matchpage.evaluate(script, arg?)- Evaluate JS in browser contextpage.locator(selector)- Get locator by CSS selectorpage.getByRole(role, options?)- Get locator by ARIA rolepage.getByText(text)- Get locator by text contentpage.getByLabel(label)- Get locator by labelpage.getByPlaceholder(text)- Get locator by placeholderpage.getByTestId(id)- Get locator by test IDpage.screenshot(options?)- Take screenshot, returns base64page.pdf(options?)- Generate PDF (Chromium only), returns base64page.request.get(url)- HTTP GET request with page cookiespage.request.post(url, options?)- HTTP POST request with page cookiespage.context()- Get the context object for this pagepage.close()- Close the page
Context Methods
context.newPage()- Create a new page (requirescreatePagecallback)context.close()- Close the contextcontext.cookies(urls?)- Get cookiescontext.addCookies(cookies)- Add cookiescontext.clearCookies()- Clear cookies
Browser Methods
browser.newContext(options?)- Create a new context (requirescreateContextcallback)
Locator Methods
click(),dblclick(),hover(),focus()fill(text),type(text),clear(),press(key)check(),uncheck(),selectOption(value)setInputFiles(files)- Set files for file input (paths or inline data)screenshot(options?)- Take element screenshot, returns base64textContent(),inputValue(),getAttribute(name)isVisible(),isEnabled(),isChecked(),count()nth(index),first(),last()- Get specific matching elementlocator(selector)- Chain with another selectorgetByRole(),getByText(),getByLabel(), etc. - Chain with getBy* methods
Expect Matchers (for Locators)
These matchers are available when using playwright with test-environment:
toBeVisible(options?),toBeEnabled(options?),toBeChecked(options?)toContainText(text, options?),toHaveValue(value, options?)- All matchers support
.notmodifier - All matchers support
{ timeout: number }option
Handle Methods
dispose()- Clean up event listenersgetBrowserConsoleLogs()- Get captured browser console logsgetNetworkRequests()- Get captured network requestsgetNetworkResponses()- Get captured network responsesclearCollected()- Clear all collected data
Setup Options
@ricsam/isolate-runtime and @ricsam/isolate-client expose a handler-first
public contract (playwright.handler). The page field below is for low-level
setupPlaywright(...) usage.
interface PlaywrightSetupOptions {
page?: Page; // Direct page object (for local use)
handler?: PlaywrightCallback; // Handler callback (for remote use)
timeout?: number; // Default timeout for operations
console?: boolean; // Route browser console logs through console handler
onEvent?: (event: PlaywrightEvent) => void; // Unified event callback
// Security callbacks for file operations
readFile?: (filePath: string) => Promise<FileData> | FileData;
writeFile?: (filePath: string, data: Buffer) => Promise<void> | void;
// Multi-page lifecycle callbacks
createPage?: (context: BrowserContext) => Promise<Page> | Page;
createContext?: (options?: BrowserContextOptions) => Promise<BrowserContext> | BrowserContext;
}
interface FileData {
name: string; // File name
mimeType: string; // MIME type
buffer: Buffer; // File contents
}
type PlaywrightEvent =
| { type: "browserConsoleLog"; level: string; stdout: string; timestamp: number }
| { type: "networkRequest"; url: string; method: string; headers: Record<string, string>; ... }
| { type: "networkResponse"; url: string; status: number; headers: Record<string, string>; ... };Multi-Page Testing
For tests that need multiple pages or contexts, provide the createPage and/or createContext callbacks:
import { createRuntime } from "@ricsam/isolate-runtime";
import { chromium } from "playwright";
import { defaultPlaywrightHandler } from "@ricsam/isolate-playwright/client";
const browser = await chromium.launch({ headless: true });
const browserContext = await browser.newContext();
const page = await browserContext.newPage();
const runtime = await createRuntime({
testEnvironment: true,
playwright: {
handler: defaultPlaywrightHandler(page, {
// Called when isolate code calls context.newPage(); receive the BrowserContext and call context.newPage()
createPage: async (context) => context.newPage(),
// Called when isolate code calls browser.newContext()
createContext: async (options) => browser.newContext(options),
}),
},
});
await runtime.eval(`
describe("multi-page tests", () => {
it("can work with multiple pages", async () => {
// Create a second page in the same context
const page2 = await context.newPage();
// Navigate both pages
await page.goto("https://example.com/page1");
await page2.goto("https://example.com/page2");
// Each page maintains its own state
expect(page.url()).toContain("page1");
expect(page2.url()).toContain("page2");
// Interact with elements on different pages
await page.locator("#button1").click();
await page2.locator("#button2").click();
await page2.close();
});
it("can work with multiple contexts", async () => {
// Create an isolated context (separate cookies, storage)
const ctx2 = await browser.newContext();
const page2 = await ctx2.newPage();
await page2.goto("https://example.com");
// Cookies are isolated between contexts
await context.addCookies([{ name: "test", value: "1", domain: "example.com", path: "/" }]);
const ctx1Cookies = await context.cookies();
const ctx2Cookies = await ctx2.cookies();
expect(ctx1Cookies.some(c => c.name === "test")).toBe(true);
expect(ctx2Cookies.some(c => c.name === "test")).toBe(false);
await ctx2.close();
});
});
`);
const results = await runtime.testEnvironment.runTests();
await runtime.dispose();
await browser.close();File Operations
Screenshots and PDFs
Screenshots and PDFs return base64-encoded data by default. To save to disk, provide a writeFile callback:
const handle = await setupPlaywright(context, {
page,
writeFile: async (filePath, data) => {
// Validate and write file
await fs.writeFile(filePath, data);
},
});
// In isolate code:
await context.eval(`
// Returns base64, no file written
const base64 = await page.screenshot();
// Returns base64 AND calls writeFile callback
const base64WithSave = await page.screenshot({ path: '/output/screenshot.png' });
// PDF works the same way
const pdfBase64 = await page.pdf({ path: '/output/document.pdf' });
`);With handler-first runtime APIs (createRuntime({ playwright: { handler } })), provide
writeFile when creating the handler:
const handler = defaultPlaywrightHandler(page, {
writeFile: async (filePath, data) => {
await fs.writeFile(filePath, data);
},
});File Uploads (setInputFiles)
File uploads support these input shapes:
"/uploads/document.pdf"["/uploads/file1.pdf", "/uploads/file2.pdf"]{ name, mimeType, buffer }[{ name, mimeType, buffer }, ...][](clear files)
Mixing paths and inline objects in the same array throws an error.
In page-mode (setupPlaywright(context, { page, ... })), provide readFile in
setupPlaywright options:
const handle = await setupPlaywright(context, {
page,
readFile: async (filePath) => {
const buffer = await fs.readFile(filePath);
return {
name: path.basename(filePath),
mimeType: 'application/octet-stream',
buffer,
};
},
});
// In isolate code:
await context.eval(`
// Inline data object - no callback needed
await page.locator('#upload').setInputFiles({
name: 'single.txt',
mimeType: 'text/plain',
buffer: new TextEncoder().encode('Hello!'),
});
// Inline data array - no callback needed
await page.locator('#upload').setInputFiles([{
name: 'test.txt',
mimeType: 'text/plain',
buffer: new TextEncoder().encode('Hello!'),
}]);
// File path - calls readFile callback
await page.locator('#upload').setInputFiles('/uploads/document.pdf');
// Multiple files
await page.locator('#upload').setInputFiles([
'/uploads/file1.pdf',
'/uploads/file2.pdf',
]);
// Clear files
await page.locator('#upload').setInputFiles([]);
`);With handler-first runtime APIs (createRuntime({ playwright: { handler } })),
provide readFile when creating the handler:
const handler = defaultPlaywrightHandler(page, {
readFile: async (filePath) => {
const buffer = await fs.readFile(filePath);
return {
name: path.basename(filePath),
mimeType: 'application/octet-stream',
buffer,
};
},
});License
MIT
