@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 +209 -20
- package/dist/cjs/client.cjs +866 -22
- package/dist/cjs/client.cjs.map +3 -3
- package/dist/cjs/index.cjs +1116 -181
- 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 +868 -22
- package/dist/mjs/client.mjs.map +3 -3
- package/dist/mjs/index.mjs +1118 -181
- 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 -7
- package/dist/types/index.d.ts +33 -20
- package/dist/types/types.d.ts +62 -13
- package/package.json +1 -1
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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 {
|
|
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 =
|
|
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,
|
|
163
|
-
|
|
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.
|
|
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
|
-
- `
|
|
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
|
|
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
|