@ricsam/isolate-playwright 0.1.12 → 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,13 +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
+ handler: defaultPlaywrightHandler(page),
29
30
  console: true, // Print browser console logs to stdout
30
31
  },
31
32
  });
@@ -52,6 +53,7 @@ For tests, enable `testEnvironment` which provides `describe`, `it`, and `expect
52
53
  ```typescript
53
54
  import { createRuntime } from "@ricsam/isolate-runtime";
54
55
  import { chromium } from "playwright";
56
+ import { defaultPlaywrightHandler } from "@ricsam/isolate-playwright/client";
55
57
 
56
58
  const browser = await chromium.launch({ headless: true });
57
59
  const page = await browser.newPage();
@@ -59,9 +61,14 @@ const page = await browser.newPage();
59
61
  const runtime = await createRuntime({
60
62
  testEnvironment: true, // Provides describe, it, expect
61
63
  playwright: {
62
- page,
63
- onBrowserConsoleLog: (entry) => console.log("[browser]", entry.level, entry.stdout),
64
- 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
+ },
65
72
  },
66
73
  });
67
74
 
@@ -111,9 +118,15 @@ await setupTestEnvironment(context);
111
118
  const handle = await setupPlaywright(context, {
112
119
  page,
113
120
  timeout: 30000,
114
- onNetworkRequest: (info) => console.log("Request:", info.url),
115
- onNetworkResponse: (info) => console.log("Response:", info.status),
116
- 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
+ },
117
130
  });
118
131
 
119
132
  // Load and run untrusted test code
@@ -140,29 +153,32 @@ await browser.close();
140
153
 
141
154
  ## Handler-based API (for Remote Execution)
142
155
 
143
- 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`):
144
158
 
145
159
  ```typescript
146
- import { createPlaywrightHandler, setupPlaywright, type PlaywrightCallback } from "@ricsam/isolate-playwright";
160
+ import { defaultPlaywrightHandler, setupPlaywright, type PlaywrightCallback } from "@ricsam/isolate-playwright";
147
161
  import { chromium } from "playwright";
148
162
 
149
163
  // On the client: create handler from page
150
164
  const browser = await chromium.launch();
151
165
  const page = await browser.newPage();
152
- const handler: PlaywrightCallback = createPlaywrightHandler(page, {
166
+ const handler: PlaywrightCallback = defaultPlaywrightHandler(page, {
153
167
  timeout: 30000,
154
168
  });
155
169
 
156
170
  // On the daemon: setup playwright with handler (instead of page)
157
171
  const handle = await setupPlaywright(context, {
158
- handler, // Handler callback instead of direct page
159
- onBrowserConsoleLog: (entry) => sendToClient("browserConsoleLog", entry),
172
+ handler,
173
+ onEvent: (event) => sendToClient("playwright-event", event),
160
174
  });
161
175
  ```
162
176
 
163
177
  ## Injected Globals (in isolate)
164
178
 
165
179
  - `page` - Page object with navigation and locator methods
180
+ - `context` - BrowserContext object with `newPage()`, cookie methods
181
+ - `browser` - Browser object with `newContext()` method
166
182
  - `Locator` - Locator class for element interactions
167
183
  - `expect` - Extended with locator matchers (only if test-environment is loaded first)
168
184
 
@@ -170,6 +186,8 @@ const handle = await setupPlaywright(context, {
170
186
 
171
187
  - `page.goto(url, options?)` - Navigate to URL
172
188
  - `page.reload()` - Reload page
189
+ - `page.goBack()` - Navigate back
190
+ - `page.goForward()` - Navigate forward
173
191
  - `page.url()` - Get current URL (sync)
174
192
  - `page.title()` - Get page title
175
193
  - `page.content()` - Get page HTML
@@ -178,24 +196,45 @@ const handle = await setupPlaywright(context, {
178
196
  - `page.waitForSelector(selector, options?)` - Wait for element
179
197
  - `page.waitForTimeout(ms)` - Wait for milliseconds
180
198
  - `page.waitForLoadState(state?)` - Wait for load state
181
- - `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
182
201
  - `page.locator(selector)` - Get locator by CSS selector
183
202
  - `page.getByRole(role, options?)` - Get locator by ARIA role
184
203
  - `page.getByText(text)` - Get locator by text content
185
204
  - `page.getByLabel(label)` - Get locator by label
186
205
  - `page.getByPlaceholder(text)` - Get locator by placeholder
187
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
188
209
  - `page.request.get(url)` - HTTP GET request with page cookies
189
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)
190
225
 
191
226
  ## Locator Methods
192
227
 
193
228
  - `click()`, `dblclick()`, `hover()`, `focus()`
194
229
  - `fill(text)`, `type(text)`, `clear()`, `press(key)`
195
230
  - `check()`, `uncheck()`, `selectOption(value)`
196
- - `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)`
197
234
  - `isVisible()`, `isEnabled()`, `isChecked()`, `count()`
198
- - `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
199
238
 
200
239
  ## Expect Matchers (for Locators)
201
240
 
@@ -216,6 +255,10 @@ These matchers are available when using playwright with test-environment:
216
255
 
217
256
  ## Setup Options
218
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
+
219
262
  ```typescript
220
263
  interface PlaywrightSetupOptions {
221
264
  page?: Page; // Direct page object (for local use)
@@ -223,6 +266,18 @@ interface PlaywrightSetupOptions {
223
266
  timeout?: number; // Default timeout for operations
224
267
  console?: boolean; // Route browser console logs through console handler
225
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
226
281
  }
227
282
 
228
283
  type PlaywrightEvent =
@@ -231,6 +286,145 @@ type PlaywrightEvent =
231
286
  | { type: "networkResponse"; url: string; status: number; headers: Record<string, string>; ... };
232
287
  ```
233
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
+
234
428
  ## License
235
429
 
236
430
  MIT