@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 +209 -15
- package/dist/cjs/client.cjs +801 -24
- package/dist/cjs/client.cjs.map +3 -3
- package/dist/cjs/index.cjs +1094 -173
- package/dist/cjs/index.cjs.map +3 -3
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/types.cjs +14 -1
- package/dist/cjs/types.cjs.map +4 -3
- package/dist/mjs/client.mjs +803 -24
- package/dist/mjs/client.mjs.map +3 -3
- package/dist/mjs/index.mjs +1096 -173
- package/dist/mjs/index.mjs.map +3 -3
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/types.mjs +6 -1
- package/dist/mjs/types.mjs.map +4 -3
- package/dist/types/client.d.ts +13 -6
- package/dist/types/index.d.ts +33 -16
- package/dist/types/types.d.ts +62 -10
- package/package.json +1 -1
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
|
-
|
|
64
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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 {
|
|
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 =
|
|
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,
|
|
159
|
-
|
|
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.
|
|
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
|
-
- `
|
|
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
|
|
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
|