@ricsam/isolate-playwright 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,14 +19,14 @@ Run browser automation scripts without a test framework:
19
19
  ```typescript
20
20
  import { createRuntime } from "@ricsam/isolate-runtime";
21
21
  import { chromium } from "playwright";
22
+ import { defaultPlaywrightHandler } from "@ricsam/isolate-playwright/client";
22
23
 
23
24
  const browser = await chromium.launch({ headless: true });
24
25
  const page = await browser.newPage();
25
26
 
26
27
  const runtime = await createRuntime({
27
28
  playwright: {
28
- page,
29
- baseUrl: "https://example.com",
29
+ handler: defaultPlaywrightHandler(page),
30
30
  console: true, // Print browser console logs to stdout
31
31
  },
32
32
  });
@@ -53,6 +53,7 @@ For tests, enable `testEnvironment` which provides `describe`, `it`, and `expect
53
53
  ```typescript
54
54
  import { createRuntime } from "@ricsam/isolate-runtime";
55
55
  import { chromium } from "playwright";
56
+ import { defaultPlaywrightHandler } from "@ricsam/isolate-playwright/client";
56
57
 
57
58
  const browser = await chromium.launch({ headless: true });
58
59
  const page = await browser.newPage();
@@ -60,10 +61,14 @@ const page = await browser.newPage();
60
61
  const runtime = await createRuntime({
61
62
  testEnvironment: true, // Provides describe, it, expect
62
63
  playwright: {
63
- page,
64
- baseUrl: "https://example.com",
65
- onBrowserConsoleLog: (entry) => console.log("[browser]", entry.level, entry.stdout),
66
- onNetworkRequest: (info) => console.log("Request:", info.url),
64
+ handler: defaultPlaywrightHandler(page),
65
+ onEvent: (event) => {
66
+ if (event.type === "browserConsoleLog") {
67
+ console.log("[browser]", event.level, event.stdout);
68
+ } else if (event.type === "networkRequest") {
69
+ console.log("Request:", event.url);
70
+ }
71
+ },
67
72
  },
68
73
  });
69
74
 
@@ -113,10 +118,15 @@ await setupTestEnvironment(context);
113
118
  const handle = await setupPlaywright(context, {
114
119
  page,
115
120
  timeout: 30000,
116
- baseUrl: "https://example.com",
117
- onNetworkRequest: (info) => console.log("Request:", info.url),
118
- onNetworkResponse: (info) => console.log("Response:", info.status),
119
- onBrowserConsoleLog: (entry) => console.log(`[${entry.level}]`, entry.stdout),
121
+ onEvent: (event) => {
122
+ if (event.type === "networkRequest") {
123
+ console.log("Request:", event.url);
124
+ } else if (event.type === "networkResponse") {
125
+ console.log("Response:", event.status);
126
+ } else if (event.type === "browserConsoleLog") {
127
+ console.log(`[${event.level}]`, event.stdout);
128
+ }
129
+ },
120
130
  });
121
131
 
122
132
  // Load and run untrusted test code
@@ -143,30 +153,32 @@ await browser.close();
143
153
 
144
154
  ## Handler-based API (for Remote Execution)
145
155
 
146
- For daemon/client architectures where the browser runs on the client:
156
+ For daemon/client architectures where the browser runs on the client, use the
157
+ handler-first contract (`playwright.handler`):
147
158
 
148
159
  ```typescript
149
- import { createPlaywrightHandler, setupPlaywright, type PlaywrightCallback } from "@ricsam/isolate-playwright";
160
+ import { defaultPlaywrightHandler, setupPlaywright, type PlaywrightCallback } from "@ricsam/isolate-playwright";
150
161
  import { chromium } from "playwright";
151
162
 
152
163
  // On the client: create handler from page
153
164
  const browser = await chromium.launch();
154
165
  const page = await browser.newPage();
155
- const handler: PlaywrightCallback = createPlaywrightHandler(page, {
166
+ const handler: PlaywrightCallback = defaultPlaywrightHandler(page, {
156
167
  timeout: 30000,
157
- baseUrl: "https://example.com",
158
168
  });
159
169
 
160
170
  // On the daemon: setup playwright with handler (instead of page)
161
171
  const handle = await setupPlaywright(context, {
162
- handler, // Handler callback instead of direct page
163
- onBrowserConsoleLog: (entry) => sendToClient("browserConsoleLog", entry),
172
+ handler,
173
+ onEvent: (event) => sendToClient("playwright-event", event),
164
174
  });
165
175
  ```
166
176
 
167
177
  ## Injected Globals (in isolate)
168
178
 
169
179
  - `page` - Page object with navigation and locator methods
180
+ - `context` - BrowserContext object with `newPage()`, cookie methods
181
+ - `browser` - Browser object with `newContext()` method
170
182
  - `Locator` - Locator class for element interactions
171
183
  - `expect` - Extended with locator matchers (only if test-environment is loaded first)
172
184
 
@@ -174,6 +186,8 @@ const handle = await setupPlaywright(context, {
174
186
 
175
187
  - `page.goto(url, options?)` - Navigate to URL
176
188
  - `page.reload()` - Reload page
189
+ - `page.goBack()` - Navigate back
190
+ - `page.goForward()` - Navigate forward
177
191
  - `page.url()` - Get current URL (sync)
178
192
  - `page.title()` - Get page title
179
193
  - `page.content()` - Get page HTML
@@ -182,24 +196,45 @@ const handle = await setupPlaywright(context, {
182
196
  - `page.waitForSelector(selector, options?)` - Wait for element
183
197
  - `page.waitForTimeout(ms)` - Wait for milliseconds
184
198
  - `page.waitForLoadState(state?)` - Wait for load state
185
- - `page.evaluate(script)` - Evaluate JS in browser context
199
+ - `page.waitForURL(url, options?)` - Wait for URL match
200
+ - `page.evaluate(script, arg?)` - Evaluate JS in browser context
186
201
  - `page.locator(selector)` - Get locator by CSS selector
187
202
  - `page.getByRole(role, options?)` - Get locator by ARIA role
188
203
  - `page.getByText(text)` - Get locator by text content
189
204
  - `page.getByLabel(label)` - Get locator by label
190
205
  - `page.getByPlaceholder(text)` - Get locator by placeholder
191
206
  - `page.getByTestId(id)` - Get locator by test ID
207
+ - `page.screenshot(options?)` - Take screenshot, returns base64
208
+ - `page.pdf(options?)` - Generate PDF (Chromium only), returns base64
192
209
  - `page.request.get(url)` - HTTP GET request with page cookies
193
210
  - `page.request.post(url, options?)` - HTTP POST request with page cookies
211
+ - `page.context()` - Get the context object for this page
212
+ - `page.close()` - Close the page
213
+
214
+ ## Context Methods
215
+
216
+ - `context.newPage()` - Create a new page (requires `createPage` callback)
217
+ - `context.close()` - Close the context
218
+ - `context.cookies(urls?)` - Get cookies
219
+ - `context.addCookies(cookies)` - Add cookies
220
+ - `context.clearCookies()` - Clear cookies
221
+
222
+ ## Browser Methods
223
+
224
+ - `browser.newContext(options?)` - Create a new context (requires `createContext` callback)
194
225
 
195
226
  ## Locator Methods
196
227
 
197
228
  - `click()`, `dblclick()`, `hover()`, `focus()`
198
229
  - `fill(text)`, `type(text)`, `clear()`, `press(key)`
199
230
  - `check()`, `uncheck()`, `selectOption(value)`
200
- - `textContent()`, `inputValue()`
231
+ - `setInputFiles(files)` - Set files for file input (paths or inline data)
232
+ - `screenshot(options?)` - Take element screenshot, returns base64
233
+ - `textContent()`, `inputValue()`, `getAttribute(name)`
201
234
  - `isVisible()`, `isEnabled()`, `isChecked()`, `count()`
202
- - `nth(index)` - Get nth matching element
235
+ - `nth(index)`, `first()`, `last()` - Get specific matching element
236
+ - `locator(selector)` - Chain with another selector
237
+ - `getByRole()`, `getByText()`, `getByLabel()`, etc. - Chain with getBy* methods
203
238
 
204
239
  ## Expect Matchers (for Locators)
205
240
 
@@ -220,14 +255,29 @@ These matchers are available when using playwright with test-environment:
220
255
 
221
256
  ## Setup Options
222
257
 
258
+ `@ricsam/isolate-runtime` and `@ricsam/isolate-client` expose a handler-first
259
+ public contract (`playwright.handler`). The `page` field below is for low-level
260
+ `setupPlaywright(...)` usage.
261
+
223
262
  ```typescript
224
263
  interface PlaywrightSetupOptions {
225
264
  page?: Page; // Direct page object (for local use)
226
265
  handler?: PlaywrightCallback; // Handler callback (for remote use)
227
266
  timeout?: number; // Default timeout for operations
228
- baseUrl?: string; // Base URL for relative navigation
229
267
  console?: boolean; // Route browser console logs through console handler
230
268
  onEvent?: (event: PlaywrightEvent) => void; // Unified event callback
269
+ // Security callbacks for file operations
270
+ readFile?: (filePath: string) => Promise<FileData> | FileData;
271
+ writeFile?: (filePath: string, data: Buffer) => Promise<void> | void;
272
+ // Multi-page lifecycle callbacks
273
+ createPage?: (context: BrowserContext) => Promise<Page> | Page;
274
+ createContext?: (options?: BrowserContextOptions) => Promise<BrowserContext> | BrowserContext;
275
+ }
276
+
277
+ interface FileData {
278
+ name: string; // File name
279
+ mimeType: string; // MIME type
280
+ buffer: Buffer; // File contents
231
281
  }
232
282
 
233
283
  type PlaywrightEvent =
@@ -236,6 +286,145 @@ type PlaywrightEvent =
236
286
  | { type: "networkResponse"; url: string; status: number; headers: Record<string, string>; ... };
237
287
  ```
238
288
 
289
+ ## Multi-Page Testing
290
+
291
+ For tests that need multiple pages or contexts, provide the `createPage` and/or `createContext` callbacks:
292
+
293
+ ```typescript
294
+ import { createRuntime } from "@ricsam/isolate-runtime";
295
+ import { chromium } from "playwright";
296
+ import { defaultPlaywrightHandler } from "@ricsam/isolate-playwright/client";
297
+
298
+ const browser = await chromium.launch({ headless: true });
299
+ const browserContext = await browser.newContext();
300
+ const page = await browserContext.newPage();
301
+
302
+ const runtime = await createRuntime({
303
+ testEnvironment: true,
304
+ playwright: {
305
+ handler: defaultPlaywrightHandler(page, {
306
+ // Called when isolate code calls context.newPage(); receive the BrowserContext and call context.newPage()
307
+ createPage: async (context) => context.newPage(),
308
+ // Called when isolate code calls browser.newContext()
309
+ createContext: async (options) => browser.newContext(options),
310
+ }),
311
+ },
312
+ });
313
+
314
+ await runtime.eval(`
315
+ describe("multi-page tests", () => {
316
+ it("can work with multiple pages", async () => {
317
+ // Create a second page in the same context
318
+ const page2 = await context.newPage();
319
+
320
+ // Navigate both pages
321
+ await page.goto("https://example.com/page1");
322
+ await page2.goto("https://example.com/page2");
323
+
324
+ // Each page maintains its own state
325
+ expect(page.url()).toContain("page1");
326
+ expect(page2.url()).toContain("page2");
327
+
328
+ // Interact with elements on different pages
329
+ await page.locator("#button1").click();
330
+ await page2.locator("#button2").click();
331
+
332
+ await page2.close();
333
+ });
334
+
335
+ it("can work with multiple contexts", async () => {
336
+ // Create an isolated context (separate cookies, storage)
337
+ const ctx2 = await browser.newContext();
338
+ const page2 = await ctx2.newPage();
339
+
340
+ await page2.goto("https://example.com");
341
+
342
+ // Cookies are isolated between contexts
343
+ await context.addCookies([{ name: "test", value: "1", domain: "example.com", path: "/" }]);
344
+ const ctx1Cookies = await context.cookies();
345
+ const ctx2Cookies = await ctx2.cookies();
346
+
347
+ expect(ctx1Cookies.some(c => c.name === "test")).toBe(true);
348
+ expect(ctx2Cookies.some(c => c.name === "test")).toBe(false);
349
+
350
+ await ctx2.close();
351
+ });
352
+ });
353
+ `);
354
+
355
+ const results = await runtime.testEnvironment.runTests();
356
+ await runtime.dispose();
357
+ await browser.close();
358
+ ```
359
+
360
+ ## File Operations
361
+
362
+ ### Screenshots and PDFs
363
+
364
+ Screenshots and PDFs return base64-encoded data by default. To save to disk, provide a `writeFile` callback:
365
+
366
+ ```typescript
367
+ const handle = await setupPlaywright(context, {
368
+ page,
369
+ writeFile: async (filePath, data) => {
370
+ // Validate and write file
371
+ await fs.writeFile(filePath, data);
372
+ },
373
+ });
374
+
375
+ // In isolate code:
376
+ await context.eval(`
377
+ // Returns base64, no file written
378
+ const base64 = await page.screenshot();
379
+
380
+ // Returns base64 AND calls writeFile callback
381
+ const base64WithSave = await page.screenshot({ path: '/output/screenshot.png' });
382
+
383
+ // PDF works the same way
384
+ const pdfBase64 = await page.pdf({ path: '/output/document.pdf' });
385
+ `);
386
+ ```
387
+
388
+ ### File Uploads (setInputFiles)
389
+
390
+ File uploads support both inline data and file paths:
391
+
392
+ ```typescript
393
+ const handle = await setupPlaywright(context, {
394
+ page,
395
+ readFile: async (filePath) => {
396
+ const buffer = await fs.readFile(filePath);
397
+ return {
398
+ name: path.basename(filePath),
399
+ mimeType: 'application/octet-stream',
400
+ buffer,
401
+ };
402
+ },
403
+ });
404
+
405
+ // In isolate code:
406
+ await context.eval(`
407
+ // Inline data - no callback needed
408
+ await page.locator('#upload').setInputFiles([{
409
+ name: 'test.txt',
410
+ mimeType: 'text/plain',
411
+ buffer: new TextEncoder().encode('Hello!'),
412
+ }]);
413
+
414
+ // File path - calls readFile callback
415
+ await page.locator('#upload').setInputFiles('/uploads/document.pdf');
416
+
417
+ // Multiple files
418
+ await page.locator('#upload').setInputFiles([
419
+ '/uploads/file1.pdf',
420
+ '/uploads/file2.pdf',
421
+ ]);
422
+
423
+ // Clear files
424
+ await page.locator('#upload').setInputFiles([]);
425
+ `);
426
+ ```
427
+
239
428
  ## License
240
429
 
241
430
  MIT