@ricsam/isolate-playwright 0.1.12 → 0.1.14

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.
@@ -1,445 +1,151 @@
1
1
  // packages/playwright/src/index.ts
2
2
  import ivm from "isolated-vm";
3
- function getLocator(page, selectorType, selectorValue, optionsJson) {
4
- const options = optionsJson ? JSON.parse(optionsJson) : undefined;
5
- const nthIndex = options?.nth;
6
- const roleOptions = options ? { ...options } : undefined;
7
- if (roleOptions) {
8
- delete roleOptions.nth;
9
- delete roleOptions.filter;
10
- if (roleOptions.name && typeof roleOptions.name === "object" && roleOptions.name.$regex) {
11
- roleOptions.name = new RegExp(roleOptions.name.$regex, roleOptions.name.$flags);
12
- }
13
- }
14
- let locator;
15
- switch (selectorType) {
16
- case "css":
17
- locator = page.locator(selectorValue);
18
- break;
19
- case "role":
20
- locator = page.getByRole(selectorValue, roleOptions && Object.keys(roleOptions).length > 0 ? roleOptions : undefined);
21
- break;
22
- case "text":
23
- locator = page.getByText(selectorValue);
24
- break;
25
- case "label":
26
- locator = page.getByLabel(selectorValue);
27
- break;
28
- case "placeholder":
29
- locator = page.getByPlaceholder(selectorValue);
30
- break;
31
- case "testId":
32
- locator = page.getByTestId(selectorValue);
33
- break;
34
- case "or": {
35
- const [firstInfo, secondInfo] = JSON.parse(selectorValue);
36
- const first = getLocator(page, firstInfo[0], firstInfo[1], firstInfo[2]);
37
- const second = getLocator(page, secondInfo[0], secondInfo[1], secondInfo[2]);
38
- locator = first.or(second);
39
- break;
40
- }
41
- default:
42
- locator = page.locator(selectorValue);
43
- }
44
- if (nthIndex !== undefined) {
45
- locator = locator.nth(nthIndex);
46
- }
47
- if (options?.filter) {
48
- const filterOpts = { ...options.filter };
49
- if (filterOpts.hasText && typeof filterOpts.hasText === "object" && filterOpts.hasText.$regex) {
50
- filterOpts.hasText = new RegExp(filterOpts.hasText.$regex, filterOpts.hasText.$flags);
51
- }
52
- if (filterOpts.hasNotText && typeof filterOpts.hasNotText === "object" && filterOpts.hasNotText.$regex) {
53
- filterOpts.hasNotText = new RegExp(filterOpts.hasNotText.$regex, filterOpts.hasNotText.$flags);
54
- }
55
- locator = locator.filter(filterOpts);
56
- }
57
- return locator;
58
- }
59
- async function executeLocatorAction(locator, action, actionArg, timeout) {
60
- switch (action) {
61
- case "click":
62
- await locator.click({ timeout });
63
- return null;
64
- case "dblclick":
65
- await locator.dblclick({ timeout });
66
- return null;
67
- case "fill":
68
- await locator.fill(String(actionArg ?? ""), { timeout });
69
- return null;
70
- case "type":
71
- await locator.pressSequentially(String(actionArg ?? ""), { timeout });
72
- return null;
73
- case "check":
74
- await locator.check({ timeout });
75
- return null;
76
- case "uncheck":
77
- await locator.uncheck({ timeout });
78
- return null;
79
- case "selectOption":
80
- await locator.selectOption(String(actionArg ?? ""), { timeout });
81
- return null;
82
- case "clear":
83
- await locator.clear({ timeout });
84
- return null;
85
- case "press":
86
- await locator.press(String(actionArg ?? ""), { timeout });
87
- return null;
88
- case "hover":
89
- await locator.hover({ timeout });
90
- return null;
91
- case "focus":
92
- await locator.focus({ timeout });
93
- return null;
94
- case "getText":
95
- return await locator.textContent({ timeout });
96
- case "getValue":
97
- return await locator.inputValue({ timeout });
98
- case "isVisible":
99
- return await locator.isVisible();
100
- case "isEnabled":
101
- return await locator.isEnabled();
102
- case "isChecked":
103
- return await locator.isChecked();
104
- case "count":
105
- return await locator.count();
106
- case "getAttribute":
107
- return await locator.getAttribute(String(actionArg ?? ""), { timeout });
108
- case "isDisabled":
109
- return await locator.isDisabled();
110
- case "isHidden":
111
- return await locator.isHidden();
112
- case "innerHTML":
113
- return await locator.innerHTML({ timeout });
114
- case "innerText":
115
- return await locator.innerText({ timeout });
116
- case "allTextContents":
117
- return await locator.allTextContents();
118
- case "allInnerTexts":
119
- return await locator.allInnerTexts();
120
- case "waitFor": {
121
- const opts = actionArg && typeof actionArg === "object" ? actionArg : {};
122
- await locator.waitFor({ state: opts.state, timeout: opts.timeout ?? timeout });
123
- return null;
124
- }
125
- case "boundingBox":
126
- return await locator.boundingBox({ timeout });
127
- default:
128
- throw new Error(`Unknown action: ${action}`);
129
- }
130
- }
131
- async function executeExpectAssertion(locator, matcher, expected, negated, timeout) {
132
- switch (matcher) {
133
- case "toBeVisible": {
134
- const isVisible = await locator.isVisible();
135
- if (negated) {
136
- if (isVisible)
137
- throw new Error("Expected element to not be visible, but it was visible");
138
- } else {
139
- if (!isVisible)
140
- throw new Error("Expected element to be visible, but it was not");
141
- }
142
- break;
143
- }
144
- case "toContainText": {
145
- const text = await locator.textContent({ timeout });
146
- let matches;
147
- let expectedDisplay;
148
- if (expected && typeof expected === "object" && expected.$regex) {
149
- const regex = new RegExp(expected.$regex, expected.$flags);
150
- matches = regex.test(text ?? "");
151
- expectedDisplay = String(regex);
152
- } else {
153
- matches = text?.includes(String(expected)) ?? false;
154
- expectedDisplay = String(expected);
155
- }
156
- if (negated) {
157
- if (matches)
158
- throw new Error(`Expected text to not contain ${expectedDisplay}, but got "${text}"`);
159
- } else {
160
- if (!matches)
161
- throw new Error(`Expected text to contain ${expectedDisplay}, but got "${text}"`);
162
- }
163
- break;
164
- }
165
- case "toHaveValue": {
166
- const value = await locator.inputValue({ timeout });
167
- const matches = value === String(expected);
168
- if (negated) {
169
- if (matches)
170
- throw new Error(`Expected value to not be "${expected}", but it was`);
171
- } else {
172
- if (!matches)
173
- throw new Error(`Expected value to be "${expected}", but got "${value}"`);
174
- }
175
- break;
176
- }
177
- case "toBeEnabled": {
178
- const isEnabled = await locator.isEnabled();
179
- if (negated) {
180
- if (isEnabled)
181
- throw new Error("Expected element to be disabled, but it was enabled");
182
- } else {
183
- if (!isEnabled)
184
- throw new Error("Expected element to be enabled, but it was disabled");
185
- }
186
- break;
187
- }
188
- case "toBeChecked": {
189
- const isChecked = await locator.isChecked();
190
- if (negated) {
191
- if (isChecked)
192
- throw new Error("Expected element to not be checked, but it was checked");
193
- } else {
194
- if (!isChecked)
195
- throw new Error("Expected element to be checked, but it was not");
196
- }
197
- break;
198
- }
199
- case "toHaveAttribute": {
200
- const { name, value } = expected;
201
- const actual = await locator.getAttribute(name, { timeout });
202
- if (value instanceof RegExp || value && typeof value === "object" && value.$regex) {
203
- const regex = value.$regex ? new RegExp(value.$regex, value.$flags) : value;
204
- const matches = regex.test(actual ?? "");
205
- if (negated) {
206
- if (matches)
207
- throw new Error(`Expected attribute "${name}" to not match ${regex}, but got "${actual}"`);
208
- } else {
209
- if (!matches)
210
- throw new Error(`Expected attribute "${name}" to match ${regex}, but got "${actual}"`);
211
- }
212
- } else {
213
- const matches = actual === String(value);
214
- if (negated) {
215
- if (matches)
216
- throw new Error(`Expected attribute "${name}" to not be "${value}", but it was`);
217
- } else {
218
- if (!matches)
219
- throw new Error(`Expected attribute "${name}" to be "${value}", but got "${actual}"`);
220
- }
221
- }
222
- break;
223
- }
224
- case "toHaveText": {
225
- const text = await locator.textContent({ timeout }) ?? "";
226
- let matches;
227
- let expectedDisplay;
228
- if (expected && typeof expected === "object" && expected.$regex) {
229
- const regex = new RegExp(expected.$regex, expected.$flags);
230
- matches = regex.test(text);
231
- expectedDisplay = String(regex);
232
- } else {
233
- matches = text === String(expected);
234
- expectedDisplay = JSON.stringify(expected);
235
- }
236
- if (negated) {
237
- if (matches)
238
- throw new Error(`Expected text to not be ${expectedDisplay}, but got "${text}"`);
239
- } else {
240
- if (!matches)
241
- throw new Error(`Expected text to be ${expectedDisplay}, but got "${text}"`);
242
- }
243
- break;
244
- }
245
- case "toHaveCount": {
246
- const count = await locator.count();
247
- const expectedCount = Number(expected);
248
- if (negated) {
249
- if (count === expectedCount)
250
- throw new Error(`Expected count to not be ${expectedCount}, but it was`);
251
- } else {
252
- if (count !== expectedCount)
253
- throw new Error(`Expected count to be ${expectedCount}, but got ${count}`);
254
- }
255
- break;
256
- }
257
- case "toBeHidden": {
258
- const isHidden = await locator.isHidden();
259
- if (negated) {
260
- if (isHidden)
261
- throw new Error("Expected element to not be hidden, but it was hidden");
262
- } else {
263
- if (!isHidden)
264
- throw new Error("Expected element to be hidden, but it was not");
265
- }
266
- break;
267
- }
268
- case "toBeDisabled": {
269
- const isDisabled = await locator.isDisabled();
270
- if (negated) {
271
- if (isDisabled)
272
- throw new Error("Expected element to not be disabled, but it was disabled");
273
- } else {
274
- if (!isDisabled)
275
- throw new Error("Expected element to be disabled, but it was not");
276
- }
277
- break;
278
- }
279
- case "toBeFocused": {
280
- const isFocused = await locator.evaluate((el) => document.activeElement === el).catch(() => false);
281
- if (negated) {
282
- if (isFocused)
283
- throw new Error("Expected element to not be focused, but it was focused");
284
- } else {
285
- if (!isFocused)
286
- throw new Error("Expected element to be focused, but it was not");
287
- }
288
- break;
289
- }
290
- case "toBeEmpty": {
291
- const text = await locator.textContent({ timeout });
292
- const value = await locator.inputValue({ timeout }).catch(() => null);
293
- const isEmpty = value !== null ? value === "" : (text ?? "") === "";
294
- if (negated) {
295
- if (isEmpty)
296
- throw new Error("Expected element to not be empty, but it was");
297
- } else {
298
- if (!isEmpty)
299
- throw new Error("Expected element to be empty, but it was not");
300
- }
301
- break;
302
- }
303
- default:
304
- throw new Error(`Unknown matcher: ${matcher}`);
305
- }
306
- }
307
- function createPlaywrightHandler(page, options) {
308
- const timeout = options?.timeout ?? 30000;
3
+ import {
4
+ DEFAULT_PLAYWRIGHT_HANDLER_META
5
+ } from "./types.mjs";
6
+ import {
7
+ createPlaywrightHandler,
8
+ defaultPlaywrightHandler,
9
+ getDefaultPlaywrightHandlerMetadata
10
+ } from "./handler.mjs";
11
+ import {
12
+ createPlaywrightHandler as createPlaywrightHandler2,
13
+ getDefaultPlaywrightHandlerMetadata as getDefaultPlaywrightHandlerMetadata2
14
+ } from "./handler.mjs";
15
+ function wrapHandlerWithPredicateSupport(handler, evaluatePredicate, defaultTimeout) {
309
16
  return async (op) => {
310
- try {
311
- switch (op.type) {
312
- case "goto": {
313
- const [url, waitUntil] = op.args;
314
- await page.goto(url, {
315
- timeout,
316
- waitUntil: waitUntil ?? "load"
317
- });
318
- return { ok: true };
319
- }
320
- case "reload":
321
- await page.reload({ timeout });
322
- return { ok: true };
323
- case "url":
324
- return { ok: true, value: page.url() };
325
- case "title":
326
- return { ok: true, value: await page.title() };
327
- case "content":
328
- return { ok: true, value: await page.content() };
329
- case "waitForSelector": {
330
- const [selector, optionsJson] = op.args;
331
- const opts = optionsJson ? JSON.parse(optionsJson) : {};
332
- await page.waitForSelector(selector, { timeout, ...opts });
333
- return { ok: true };
334
- }
335
- case "waitForTimeout": {
336
- const [ms] = op.args;
337
- await page.waitForTimeout(ms);
338
- return { ok: true };
339
- }
340
- case "waitForLoadState": {
341
- const [state] = op.args;
342
- await page.waitForLoadState(state ?? "load", { timeout });
343
- return { ok: true };
344
- }
345
- case "evaluate": {
346
- const [script, arg] = op.args;
347
- if (op.args.length > 1) {
348
- const fn = new Function("return (" + script + ")")();
349
- const result2 = await page.evaluate(fn, arg);
350
- return { ok: true, value: result2 };
351
- }
352
- const result = await page.evaluate(script);
353
- return { ok: true, value: result };
354
- }
355
- case "locatorAction": {
356
- const [selectorType, selectorValue, roleOptions, action, actionArg] = op.args;
357
- const locator = getLocator(page, selectorType, selectorValue, roleOptions);
358
- const result = await executeLocatorAction(locator, action, actionArg, timeout);
359
- return { ok: true, value: result };
360
- }
361
- case "expectLocator": {
362
- const [selectorType, selectorValue, roleOptions, matcher, expected, negated, customTimeout] = op.args;
363
- const locator = getLocator(page, selectorType, selectorValue, roleOptions);
364
- const effectiveTimeout = customTimeout ?? timeout;
365
- await executeExpectAssertion(locator, matcher, expected, negated ?? false, effectiveTimeout);
366
- return { ok: true };
367
- }
368
- case "request": {
369
- const [url, method, data, headers] = op.args;
370
- const requestOptions = {
371
- timeout
372
- };
373
- if (headers) {
374
- requestOptions.headers = headers;
17
+ switch (op.type) {
18
+ case "waitForURLPredicate": {
19
+ const [predicateId, customTimeout, waitUntil] = op.args;
20
+ const effectiveTimeout = customTimeout ?? defaultTimeout;
21
+ const startTime = Date.now();
22
+ const pollInterval = 100;
23
+ while (true) {
24
+ const urlResult = await handler({ type: "url", args: [], pageId: op.pageId, contextId: op.contextId });
25
+ if (urlResult.ok) {
26
+ try {
27
+ if (evaluatePredicate(predicateId, urlResult.value)) {
28
+ return { ok: true };
29
+ }
30
+ } catch (e) {
31
+ const error = e;
32
+ return { ok: false, error: { name: error.name, message: error.message } };
33
+ }
375
34
  }
376
- if (data !== undefined && data !== null) {
377
- requestOptions.data = data;
35
+ if (effectiveTimeout > 0 && Date.now() - startTime >= effectiveTimeout) {
36
+ return { ok: false, error: { name: "Error", message: `Timeout ${effectiveTimeout}ms exceeded waiting for URL` } };
378
37
  }
379
- const response = await page.request.fetch(url, {
380
- method,
381
- ...requestOptions
38
+ await new Promise((r) => setTimeout(r, pollInterval));
39
+ }
40
+ }
41
+ case "waitForResponsePredicateFinish": {
42
+ const [initialListenerId, predicateId, customTimeout] = op.args;
43
+ const effectiveTimeout = customTimeout ?? defaultTimeout;
44
+ const broadMatcher = { type: "regex", value: { $regex: ".*", $flags: "" } };
45
+ const startTime = Date.now();
46
+ let currentListenerId = initialListenerId;
47
+ while (true) {
48
+ const finishResult = await handler({
49
+ type: "waitForResponseFinish",
50
+ args: [currentListenerId],
51
+ pageId: op.pageId,
52
+ contextId: op.contextId
382
53
  });
383
- const text = await response.text();
384
- let json = null;
54
+ if (!finishResult.ok)
55
+ return finishResult;
56
+ const responseData = finishResult.value;
385
57
  try {
386
- json = JSON.parse(text);
387
- } catch {}
388
- return {
389
- ok: true,
390
- value: {
391
- status: response.status(),
392
- ok: response.ok(),
393
- headers: response.headers(),
394
- text,
395
- json,
396
- body: null
58
+ const serialized = {
59
+ method: "",
60
+ headers: Object.entries(responseData.headers || {}),
61
+ url: responseData.url,
62
+ status: responseData.status,
63
+ statusText: responseData.statusText,
64
+ body: responseData.text || ""
65
+ };
66
+ if (evaluatePredicate(predicateId, serialized)) {
67
+ return finishResult;
397
68
  }
398
- };
399
- }
400
- case "goBack": {
401
- const [waitUntil] = op.args;
402
- await page.goBack({
403
- timeout,
404
- waitUntil: waitUntil ?? "load"
69
+ } catch (e) {
70
+ const error = e;
71
+ return { ok: false, error: { name: error.name, message: error.message } };
72
+ }
73
+ if (effectiveTimeout > 0 && Date.now() - startTime >= effectiveTimeout) {
74
+ return { ok: false, error: { name: "Error", message: `Timeout ${effectiveTimeout}ms exceeded waiting for response` } };
75
+ }
76
+ const remainingTimeout = effectiveTimeout > 0 ? Math.max(1, effectiveTimeout - (Date.now() - startTime)) : effectiveTimeout;
77
+ const nextStartResult = await handler({
78
+ type: "waitForResponseStart",
79
+ args: [broadMatcher, remainingTimeout],
80
+ pageId: op.pageId,
81
+ contextId: op.contextId
405
82
  });
406
- return { ok: true };
83
+ if (!nextStartResult.ok)
84
+ return nextStartResult;
85
+ currentListenerId = nextStartResult.value.listenerId;
407
86
  }
408
- case "goForward": {
409
- const [waitUntil] = op.args;
410
- await page.goForward({
411
- timeout,
412
- waitUntil: waitUntil ?? "load"
87
+ }
88
+ case "waitForRequestPredicateFinish": {
89
+ const [initialListenerId, predicateId, customTimeout] = op.args;
90
+ const effectiveTimeout = customTimeout ?? defaultTimeout;
91
+ const broadMatcher = { type: "regex", value: { $regex: ".*", $flags: "" } };
92
+ const startTime = Date.now();
93
+ let currentListenerId = initialListenerId;
94
+ while (true) {
95
+ const finishResult = await handler({
96
+ type: "waitForRequestFinish",
97
+ args: [currentListenerId],
98
+ pageId: op.pageId,
99
+ contextId: op.contextId
413
100
  });
414
- return { ok: true };
415
- }
416
- case "waitForURL": {
417
- const [url, customTimeout, waitUntil] = op.args;
418
- await page.waitForURL(url, {
419
- timeout: customTimeout ?? timeout,
420
- waitUntil: waitUntil ?? undefined
101
+ if (!finishResult.ok)
102
+ return finishResult;
103
+ const requestData = finishResult.value;
104
+ try {
105
+ const serialized = {
106
+ method: requestData.method,
107
+ headers: Object.entries(requestData.headers || {}),
108
+ url: requestData.url,
109
+ body: requestData.postData || ""
110
+ };
111
+ if (evaluatePredicate(predicateId, serialized)) {
112
+ return finishResult;
113
+ }
114
+ } catch (e) {
115
+ const error = e;
116
+ return { ok: false, error: { name: error.name, message: error.message } };
117
+ }
118
+ if (effectiveTimeout > 0 && Date.now() - startTime >= effectiveTimeout) {
119
+ return { ok: false, error: { name: "Error", message: `Timeout ${effectiveTimeout}ms exceeded waiting for request` } };
120
+ }
121
+ const remainingTimeout = effectiveTimeout > 0 ? Math.max(1, effectiveTimeout - (Date.now() - startTime)) : effectiveTimeout;
122
+ const nextStartResult = await handler({
123
+ type: "waitForRequestStart",
124
+ args: [broadMatcher, remainingTimeout],
125
+ pageId: op.pageId,
126
+ contextId: op.contextId
421
127
  });
422
- return { ok: true };
128
+ if (!nextStartResult.ok)
129
+ return nextStartResult;
130
+ currentListenerId = nextStartResult.value.listenerId;
423
131
  }
424
- case "clearCookies": {
425
- await page.context().clearCookies();
426
- return { ok: true };
427
- }
428
- default:
429
- return { ok: false, error: { name: "Error", message: `Unknown operation: ${op.type}` } };
430
132
  }
431
- } catch (err) {
432
- const error = err;
433
- return { ok: false, error: { name: error.name, message: error.message } };
133
+ default:
134
+ return handler(op);
434
135
  }
435
136
  };
436
137
  }
437
138
  async function setupPlaywright(context, options) {
438
139
  const timeout = options.timeout ?? 30000;
439
- const page = "page" in options ? options.page : undefined;
140
+ const explicitPage = "page" in options ? options.page : undefined;
440
141
  const handler = "handler" in options ? options.handler : undefined;
441
- const effectiveHandler = handler ?? (page ? createPlaywrightHandler(page, { timeout }) : undefined);
442
- if (!effectiveHandler) {
142
+ const handlerMetadata = handler ? getDefaultPlaywrightHandlerMetadata2(handler) : undefined;
143
+ const page = explicitPage ?? handlerMetadata?.page;
144
+ const createPage = "createPage" in options ? options.createPage : undefined;
145
+ const createContext = "createContext" in options ? options.createContext : undefined;
146
+ const readFile = "readFile" in options ? options.readFile : undefined;
147
+ const writeFile = "writeFile" in options ? options.writeFile : undefined;
148
+ if (!handler && !page) {
443
149
  throw new Error("Either page or handler must be provided to setupPlaywright");
444
150
  }
445
151
  const browserConsoleLogs = [];
@@ -518,15 +224,10 @@ async function setupPlaywright(context, options) {
518
224
  page.on("response", responseHandler);
519
225
  page.on("console", consoleHandler);
520
226
  }
521
- global.setSync("__Playwright_handler_ref", new ivm.Reference(async (opJson) => {
522
- const op = JSON.parse(opJson);
523
- const result = await effectiveHandler(op);
524
- return JSON.stringify(result);
525
- }));
526
227
  context.evalSync(`
527
228
  (function() {
528
- globalThis.__pw_invoke = async function(type, args) {
529
- const op = JSON.stringify({ type, args });
229
+ globalThis.__pw_invoke_sync = function(type, args, options) {
230
+ const op = JSON.stringify({ type, args, pageId: options?.pageId, contextId: options?.contextId });
530
231
  const resultJson = __Playwright_handler_ref.applySyncPromise(undefined, [op]);
531
232
  const result = JSON.parse(resultJson);
532
233
  if (result.ok) {
@@ -537,215 +238,607 @@ async function setupPlaywright(context, options) {
537
238
  throw error;
538
239
  }
539
240
  };
241
+ globalThis.__pw_invoke = async function(type, args, options) {
242
+ return globalThis.__pw_invoke_sync(type, args, options);
243
+ };
540
244
  })();
541
245
  `);
542
246
  context.evalSync(`
543
247
  (function() {
544
- let __pw_currentUrl = '';
545
- globalThis.page = {
248
+ const __pw_predicates = new Map();
249
+ let __pw_next_id = 0;
250
+ globalThis.__pw_register_predicate = function(fn) {
251
+ const id = __pw_next_id++;
252
+ __pw_predicates.set(id, fn);
253
+ return id;
254
+ };
255
+ globalThis.__pw_unregister_predicate = function(id) {
256
+ __pw_predicates.delete(id);
257
+ };
258
+ globalThis.__pw_evaluate_predicate = function(id, data) {
259
+ const fn = __pw_predicates.get(id);
260
+ if (!fn) throw new Error('Predicate not found: ' + id);
261
+ const result = fn(data);
262
+ if (result && typeof result === 'object' && typeof result.then === 'function') {
263
+ throw new Error('Async predicates are not supported. Use a synchronous predicate function.');
264
+ }
265
+ return !!result;
266
+ };
267
+ })();
268
+ `);
269
+ const evaluatePredicateRef = context.global.getSync("__pw_evaluate_predicate", { reference: true });
270
+ const evaluatePredicateFn = (predicateId, data) => {
271
+ return evaluatePredicateRef.applySync(undefined, [new ivm.ExternalCopy(predicateId).copyInto(), new ivm.ExternalCopy(data).copyInto()]);
272
+ };
273
+ let effectiveHandler;
274
+ if (handler && handlerMetadata?.page) {
275
+ effectiveHandler = createPlaywrightHandler2(handlerMetadata.page, {
276
+ ...handlerMetadata.options,
277
+ evaluatePredicate: evaluatePredicateFn
278
+ });
279
+ } else if (handler) {
280
+ effectiveHandler = wrapHandlerWithPredicateSupport(handler, evaluatePredicateFn, timeout);
281
+ } else if (page) {
282
+ effectiveHandler = createPlaywrightHandler2(page, {
283
+ timeout,
284
+ readFile,
285
+ writeFile,
286
+ createPage,
287
+ createContext,
288
+ evaluatePredicate: evaluatePredicateFn
289
+ });
290
+ } else {
291
+ throw new Error("Either page or handler must be provided to setupPlaywright");
292
+ }
293
+ global.setSync("__Playwright_handler_ref", new ivm.Reference(async (opJson) => {
294
+ const op = JSON.parse(opJson);
295
+ const result = await effectiveHandler(op);
296
+ return JSON.stringify(result);
297
+ }));
298
+ context.evalSync(`
299
+ (function() {
300
+ // IsolatePage class - represents a page with a specific pageId
301
+ class IsolatePage {
302
+ #pageId; #contextId;
303
+ constructor(pageId, contextId) {
304
+ this.#pageId = pageId;
305
+ this.#contextId = contextId;
306
+ }
307
+ get __isPage() { return true; }
308
+ get __pageId() { return this.#pageId; }
309
+ get __contextId() { return this.#contextId; }
310
+
546
311
  async goto(url, options) {
547
- const result = await __pw_invoke("goto", [url, options?.waitUntil || null]);
548
- const resolvedUrl = await __pw_invoke("url", []);
549
- __pw_currentUrl = resolvedUrl || url;
550
- return result;
551
- },
312
+ await __pw_invoke("goto", [url, options?.waitUntil || null], { pageId: this.#pageId });
313
+ }
552
314
  async reload() {
553
- const result = await __pw_invoke("reload", []);
554
- const resolvedUrl = await __pw_invoke("url", []);
555
- if (resolvedUrl) __pw_currentUrl = resolvedUrl;
556
- return result;
557
- },
558
- url() {
559
- return __pw_currentUrl;
560
- },
561
- async title() {
562
- return __pw_invoke("title", []);
563
- },
564
- async content() {
565
- return __pw_invoke("content", []);
566
- },
315
+ await __pw_invoke("reload", [], { pageId: this.#pageId });
316
+ }
317
+ url() { return __pw_invoke_sync("url", [], { pageId: this.#pageId }); }
318
+ async title() { return __pw_invoke("title", [], { pageId: this.#pageId }); }
319
+ async content() { return __pw_invoke("content", [], { pageId: this.#pageId }); }
567
320
  async waitForSelector(selector, options) {
568
- return __pw_invoke("waitForSelector", [selector, options ? JSON.stringify(options) : null]);
569
- },
570
- async waitForTimeout(ms) {
571
- return __pw_invoke("waitForTimeout", [ms]);
572
- },
573
- async waitForLoadState(state) {
574
- return __pw_invoke("waitForLoadState", [state || null]);
575
- },
321
+ return __pw_invoke("waitForSelector", [selector, options ? JSON.stringify(options) : null], { pageId: this.#pageId });
322
+ }
323
+ async waitForTimeout(ms) { return __pw_invoke("waitForTimeout", [ms], { pageId: this.#pageId }); }
324
+ async waitForLoadState(state) { return __pw_invoke("waitForLoadState", [state || null], { pageId: this.#pageId }); }
576
325
  async evaluate(script, arg) {
577
326
  const hasArg = arguments.length > 1;
578
327
  if (hasArg) {
579
328
  const serialized = typeof script === "function" ? script.toString() : script;
580
- return __pw_invoke("evaluate", [serialized, arg]);
329
+ return __pw_invoke("evaluate", [serialized, arg], { pageId: this.#pageId });
581
330
  }
582
331
  const serialized = typeof script === "function" ? "(" + script.toString() + ")()" : script;
583
- return __pw_invoke("evaluate", [serialized]);
584
- },
585
- locator(selector) { return new Locator("css", selector, null); },
332
+ return __pw_invoke("evaluate", [serialized], { pageId: this.#pageId });
333
+ }
334
+ locator(selector) { return new Locator("css", selector, null, this.#pageId); }
586
335
  getByRole(role, options) {
587
336
  if (options) {
588
337
  const serialized = { ...options };
589
- // Use duck-typing RegExp detection (instanceof fails across isolated-vm boundary)
590
338
  const name = options.name;
591
339
  if (name && typeof name === 'object' && typeof name.source === 'string' && typeof name.flags === 'string') {
592
340
  serialized.name = { $regex: name.source, $flags: name.flags };
593
341
  }
594
- return new Locator("role", role, JSON.stringify(serialized));
342
+ return new Locator("role", role, JSON.stringify(serialized), this.#pageId);
595
343
  }
596
- return new Locator("role", role, null);
597
- },
598
- getByText(text) { return new Locator("text", text, null); },
599
- getByLabel(label) { return new Locator("label", label, null); },
600
- getByPlaceholder(p) { return new Locator("placeholder", p, null); },
601
- getByTestId(id) { return new Locator("testId", id, null); },
344
+ return new Locator("role", role, null, this.#pageId);
345
+ }
346
+ getByText(text) { return new Locator("text", text, null, this.#pageId); }
347
+ getByLabel(label) { return new Locator("label", label, null, this.#pageId); }
348
+ getByPlaceholder(p) { return new Locator("placeholder", p, null, this.#pageId); }
349
+ getByTestId(id) { return new Locator("testId", id, null, this.#pageId); }
350
+ getByAltText(alt) { return new Locator("altText", alt, null, this.#pageId); }
351
+ getByTitle(title) { return new Locator("title", title, null, this.#pageId); }
352
+ frameLocator(selector) {
353
+ const pageId = this.#pageId;
354
+ return {
355
+ locator(innerSelector) { return new Locator("frame", JSON.stringify([["css", selector, null], ["css", innerSelector, null]]), null, pageId); },
356
+ getByRole(role, options) { return new Locator("frame", JSON.stringify([["css", selector, null], ["role", role, options ? JSON.stringify(options) : null]]), null, pageId); },
357
+ getByText(text) { return new Locator("frame", JSON.stringify([["css", selector, null], ["text", text, null]]), null, pageId); },
358
+ getByLabel(label) { return new Locator("frame", JSON.stringify([["css", selector, null], ["label", label, null]]), null, pageId); },
359
+ getByPlaceholder(placeholder) { return new Locator("frame", JSON.stringify([["css", selector, null], ["placeholder", placeholder, null]]), null, pageId); },
360
+ getByTestId(testId) { return new Locator("frame", JSON.stringify([["css", selector, null], ["testId", testId, null]]), null, pageId); },
361
+ getByAltText(alt) { return new Locator("frame", JSON.stringify([["css", selector, null], ["altText", alt, null]]), null, pageId); },
362
+ getByTitle(title) { return new Locator("frame", JSON.stringify([["css", selector, null], ["title", title, null]]), null, pageId); },
363
+ };
364
+ }
602
365
  async goBack(options) {
603
- await __pw_invoke("goBack", [options?.waitUntil || null]);
604
- const resolvedUrl = await __pw_invoke("url", []);
605
- if (resolvedUrl) __pw_currentUrl = resolvedUrl;
606
- },
366
+ await __pw_invoke("goBack", [options?.waitUntil || null], { pageId: this.#pageId });
367
+ }
607
368
  async goForward(options) {
608
- await __pw_invoke("goForward", [options?.waitUntil || null]);
609
- const resolvedUrl = await __pw_invoke("url", []);
610
- if (resolvedUrl) __pw_currentUrl = resolvedUrl;
611
- },
369
+ await __pw_invoke("goForward", [options?.waitUntil || null], { pageId: this.#pageId });
370
+ }
612
371
  async waitForURL(url, options) {
613
- return __pw_invoke("waitForURL", [url, options?.timeout || null, options?.waitUntil || null]);
614
- },
615
- context() {
372
+ if (typeof url === 'function') {
373
+ const predicateId = __pw_register_predicate(url);
374
+ try {
375
+ await __pw_invoke("waitForURLPredicate", [predicateId, options?.timeout || null, options?.waitUntil || null], { pageId: this.#pageId });
376
+ } finally {
377
+ __pw_unregister_predicate(predicateId);
378
+ }
379
+ return;
380
+ }
381
+ let serializedUrl;
382
+ if (typeof url === 'string') {
383
+ serializedUrl = { type: 'string', value: url };
384
+ } else if (url && typeof url === 'object' && typeof url.source === 'string' && typeof url.flags === 'string') {
385
+ serializedUrl = { type: 'regex', value: { $regex: url.source, $flags: url.flags } };
386
+ } else {
387
+ serializedUrl = url;
388
+ }
389
+ return __pw_invoke("waitForURL", [serializedUrl, options?.timeout || null, options?.waitUntil || null], { pageId: this.#pageId });
390
+ }
391
+ waitForRequest(urlOrPredicate, options) {
392
+ if (typeof urlOrPredicate === 'function') {
393
+ const userPredicate = urlOrPredicate;
394
+ const wrappedPredicate = (data) => {
395
+ const requestLike = {
396
+ url: () => data.url,
397
+ method: () => data.method,
398
+ headers: () => Object.fromEntries(data.headers),
399
+ headersArray: () => data.headers.map(h => ({ name: h[0], value: h[1] })),
400
+ postData: () => data.body || null,
401
+ };
402
+ return userPredicate(requestLike);
403
+ };
404
+ const predicateId = __pw_register_predicate(wrappedPredicate);
405
+ const pageId = this.#pageId;
406
+ // Start listening immediately (before the user triggers the request)
407
+ const broadMatcher = { type: 'regex', value: { $regex: '.*', $flags: '' } };
408
+ const startResult = __pw_invoke_sync("waitForRequestStart", [broadMatcher, options?.timeout || null], { pageId });
409
+ const listenerId = startResult.listenerId;
410
+ return {
411
+ then(resolve, reject) {
412
+ try {
413
+ const r = __pw_invoke_sync("waitForRequestPredicateFinish", [listenerId, predicateId, options?.timeout || null], { pageId });
414
+ resolve({ url: () => r.url, method: () => r.method, headers: () => r.headers, postData: () => r.postData });
415
+ } catch(e) { reject(e); } finally { __pw_unregister_predicate(predicateId); }
416
+ }
417
+ };
418
+ }
419
+ let serializedMatcher;
420
+ if (typeof urlOrPredicate === 'string') {
421
+ serializedMatcher = { type: 'string', value: urlOrPredicate };
422
+ } else if (urlOrPredicate && typeof urlOrPredicate === 'object'
423
+ && typeof urlOrPredicate.source === 'string'
424
+ && typeof urlOrPredicate.flags === 'string') {
425
+ serializedMatcher = { type: 'regex', value: { $regex: urlOrPredicate.source, $flags: urlOrPredicate.flags } };
426
+ } else {
427
+ throw new Error('waitForRequest requires a URL string, RegExp, or predicate function');
428
+ }
429
+ const startResult = __pw_invoke_sync("waitForRequestStart", [serializedMatcher, options?.timeout || null], { pageId: this.#pageId });
430
+ const listenerId = startResult.listenerId;
431
+ const pageId = this.#pageId;
616
432
  return {
617
- async clearCookies() {
618
- return __pw_invoke("clearCookies", []);
433
+ then(resolve, reject) {
434
+ try {
435
+ const r = __pw_invoke_sync("waitForRequestFinish", [listenerId], { pageId });
436
+ resolve({ url: () => r.url, method: () => r.method, headers: () => r.headers, postData: () => r.postData });
437
+ } catch(e) { reject(e); }
619
438
  }
620
439
  };
621
- },
622
- async click(selector) { return this.locator(selector).click(); },
623
- async fill(selector, value) { return this.locator(selector).fill(value); },
624
- request: {
625
- async fetch(url, options) {
626
- const result = await __pw_invoke("request", [url, options?.method || "GET", options?.data, options?.headers]);
440
+ }
441
+ waitForResponse(urlOrPredicate, options) {
442
+ if (typeof urlOrPredicate === 'function') {
443
+ const userPredicate = urlOrPredicate;
444
+ const wrappedPredicate = (data) => {
445
+ const responseLike = {
446
+ url: () => data.url,
447
+ status: () => data.status,
448
+ statusText: () => data.statusText,
449
+ headers: () => Object.fromEntries(data.headers),
450
+ headersArray: () => data.headers.map(h => ({ name: h[0], value: h[1] })),
451
+ ok: () => data.status >= 200 && data.status < 300,
452
+ };
453
+ return userPredicate(responseLike);
454
+ };
455
+ const predicateId = __pw_register_predicate(wrappedPredicate);
456
+ const pageId = this.#pageId;
457
+ // Start listening immediately (before the user triggers the response)
458
+ const broadMatcher = { type: 'regex', value: { $regex: '.*', $flags: '' } };
459
+ const startResult = __pw_invoke_sync("waitForResponseStart", [broadMatcher, options?.timeout || null], { pageId });
460
+ const listenerId = startResult.listenerId;
627
461
  return {
628
- status: () => result.status,
629
- ok: () => result.ok,
630
- headers: () => result.headers,
631
- json: async () => result.json,
632
- text: async () => result.text,
633
- body: async () => result.body,
462
+ then(resolve, reject) {
463
+ try {
464
+ const r = __pw_invoke_sync("waitForResponsePredicateFinish", [listenerId, predicateId, options?.timeout || null], { pageId });
465
+ resolve({
466
+ url: () => r.url, status: () => r.status, statusText: () => r.statusText,
467
+ headers: () => r.headers, headersArray: () => r.headersArray,
468
+ ok: () => r.ok, json: async () => r.json, text: async () => r.text, body: async () => r.body,
469
+ });
470
+ } catch(e) { reject(e); } finally { __pw_unregister_predicate(predicateId); }
471
+ }
634
472
  };
635
- },
636
- async get(url, options) {
637
- return this.fetch(url, { ...options, method: "GET" });
638
- },
639
- async post(url, options) {
640
- return this.fetch(url, { ...options, method: "POST" });
641
- },
642
- async put(url, options) {
643
- return this.fetch(url, { ...options, method: "PUT" });
644
- },
645
- async delete(url, options) {
646
- return this.fetch(url, { ...options, method: "DELETE" });
647
- },
648
- },
473
+ }
474
+ let serializedMatcher;
475
+ if (typeof urlOrPredicate === 'string') {
476
+ serializedMatcher = { type: 'string', value: urlOrPredicate };
477
+ } else if (urlOrPredicate && typeof urlOrPredicate === 'object'
478
+ && typeof urlOrPredicate.source === 'string'
479
+ && typeof urlOrPredicate.flags === 'string') {
480
+ serializedMatcher = { type: 'regex', value: { $regex: urlOrPredicate.source, $flags: urlOrPredicate.flags } };
481
+ } else {
482
+ throw new Error('waitForResponse requires a URL string, RegExp, or predicate function');
483
+ }
484
+ const startResult = __pw_invoke_sync("waitForResponseStart", [serializedMatcher, options?.timeout || null], { pageId: this.#pageId });
485
+ const listenerId = startResult.listenerId;
486
+ const pageId = this.#pageId;
487
+ return {
488
+ then(resolve, reject) {
489
+ try {
490
+ const r = __pw_invoke_sync("waitForResponseFinish", [listenerId], { pageId });
491
+ resolve({
492
+ url: () => r.url, status: () => r.status, statusText: () => r.statusText,
493
+ headers: () => r.headers, headersArray: () => r.headersArray,
494
+ ok: () => r.ok, json: async () => r.json, text: async () => r.text, body: async () => r.body,
495
+ });
496
+ } catch(e) { reject(e); }
497
+ }
498
+ };
499
+ }
500
+ context() {
501
+ const contextId = this.#contextId;
502
+ return new IsolateContext(contextId);
503
+ }
504
+ async click(selector) { return this.locator(selector).click(); }
505
+ async fill(selector, value) { return this.locator(selector).fill(value); }
506
+ async textContent(selector) { return this.locator(selector).textContent(); }
507
+ async innerText(selector) { return this.locator(selector).innerText(); }
508
+ async innerHTML(selector) { return this.locator(selector).innerHTML(); }
509
+ async getAttribute(selector, name) { return this.locator(selector).getAttribute(name); }
510
+ async inputValue(selector) { return this.locator(selector).inputValue(); }
511
+ async isVisible(selector) { return this.locator(selector).isVisible(); }
512
+ async isEnabled(selector) { return this.locator(selector).isEnabled(); }
513
+ async isChecked(selector) { return this.locator(selector).isChecked(); }
514
+ async isHidden(selector) { return this.locator(selector).isHidden(); }
515
+ async isDisabled(selector) { return this.locator(selector).isDisabled(); }
516
+ async screenshot(options) { return __pw_invoke("screenshot", [options || {}], { pageId: this.#pageId }); }
517
+ async setViewportSize(size) { return __pw_invoke("setViewportSize", [size], { pageId: this.#pageId }); }
518
+ async viewportSize() { return __pw_invoke("viewportSize", [], { pageId: this.#pageId }); }
519
+ async emulateMedia(options) { return __pw_invoke("emulateMedia", [options], { pageId: this.#pageId }); }
520
+ async setExtraHTTPHeaders(headers) { return __pw_invoke("setExtraHTTPHeaders", [headers], { pageId: this.#pageId }); }
521
+ async bringToFront() { return __pw_invoke("bringToFront", [], { pageId: this.#pageId }); }
522
+ async close() { return __pw_invoke("close", [], { pageId: this.#pageId }); }
523
+ async isClosed() { return __pw_invoke("isClosed", [], { pageId: this.#pageId }); }
524
+ async pdf(options) { return __pw_invoke("pdf", [options || {}], { pageId: this.#pageId }); }
525
+ async pause() { return __pw_invoke("pause", [], { pageId: this.#pageId }); }
526
+ async frames() { return __pw_invoke("frames", [], { pageId: this.#pageId }); }
527
+ async mainFrame() { return __pw_invoke("mainFrame", [], { pageId: this.#pageId }); }
528
+ get keyboard() {
529
+ const pageId = this.#pageId;
530
+ return {
531
+ async type(text, options) { return __pw_invoke("keyboardType", [text, options], { pageId }); },
532
+ async press(key, options) { return __pw_invoke("keyboardPress", [key, options], { pageId }); },
533
+ async down(key) { return __pw_invoke("keyboardDown", [key], { pageId }); },
534
+ async up(key) { return __pw_invoke("keyboardUp", [key], { pageId }); },
535
+ async insertText(text) { return __pw_invoke("keyboardInsertText", [text], { pageId }); }
536
+ };
537
+ }
538
+ get mouse() {
539
+ const pageId = this.#pageId;
540
+ return {
541
+ async move(x, y, options) { return __pw_invoke("mouseMove", [x, y, options], { pageId }); },
542
+ async click(x, y, options) { return __pw_invoke("mouseClick", [x, y, options], { pageId }); },
543
+ async down(options) { return __pw_invoke("mouseDown", [options], { pageId }); },
544
+ async up(options) { return __pw_invoke("mouseUp", [options], { pageId }); },
545
+ async wheel(deltaX, deltaY) { return __pw_invoke("mouseWheel", [deltaX, deltaY], { pageId }); }
546
+ };
547
+ }
548
+ get request() {
549
+ const pageId = this.#pageId;
550
+ return {
551
+ async fetch(url, options) {
552
+ const result = await __pw_invoke("request", [url, options?.method || "GET", options?.data, options?.headers], { pageId });
553
+ return {
554
+ status: () => result.status,
555
+ ok: () => result.ok,
556
+ headers: () => result.headers,
557
+ json: async () => result.json,
558
+ text: async () => result.text,
559
+ body: async () => result.body,
560
+ };
561
+ },
562
+ async get(url, options) { return this.fetch(url, { ...options, method: "GET" }); },
563
+ async post(url, options) { return this.fetch(url, { ...options, method: "POST" }); },
564
+ async put(url, options) { return this.fetch(url, { ...options, method: "PUT" }); },
565
+ async delete(url, options) { return this.fetch(url, { ...options, method: "DELETE" }); },
566
+ };
567
+ }
568
+ }
569
+ globalThis.IsolatePage = IsolatePage;
570
+
571
+ // IsolateContext class - represents a browser context with a specific contextId
572
+ class IsolateContext {
573
+ #contextId;
574
+ constructor(contextId) { this.#contextId = contextId; }
575
+ get __contextId() { return this.#contextId; }
576
+
577
+ async newPage() {
578
+ const result = await __pw_invoke("newPage", [], { contextId: this.#contextId });
579
+ return new IsolatePage(result.pageId, this.#contextId);
580
+ }
581
+ async close() { return __pw_invoke("closeContext", [], { contextId: this.#contextId }); }
582
+ async clearCookies() { return __pw_invoke("clearCookies", [], { contextId: this.#contextId }); }
583
+ async addCookies(cookies) { return __pw_invoke("addCookies", [cookies], { contextId: this.#contextId }); }
584
+ async cookies(urls) { return __pw_invoke("cookies", [urls], { contextId: this.#contextId }); }
585
+ }
586
+ globalThis.IsolateContext = IsolateContext;
587
+
588
+ // browser global - for creating new contexts
589
+ globalThis.browser = {
590
+ async newContext(options) {
591
+ const result = await __pw_invoke("newContext", [options || null]);
592
+ return new IsolateContext(result.contextId);
593
+ }
649
594
  };
595
+
596
+ // context global - represents the default context
597
+ globalThis.context = new IsolateContext("ctx_0");
598
+
599
+ // page global - represents the default page
600
+ globalThis.page = new IsolatePage("page_0", "ctx_0");
650
601
  })();
651
602
  `);
652
603
  context.evalSync(`
653
604
  (function() {
605
+ // Helper to serialize options including RegExp
606
+ function serializeOptions(options) {
607
+ if (!options) return null;
608
+ const serialized = { ...options };
609
+ if (options.name && typeof options.name === 'object' && typeof options.name.source === 'string' && typeof options.name.flags === 'string') {
610
+ serialized.name = { $regex: options.name.source, $flags: options.name.flags };
611
+ }
612
+ return JSON.stringify(serialized);
613
+ }
614
+
615
+ const INPUT_FILES_VALIDATION_ERROR =
616
+ "setInputFiles() expects a file path string, an array of file path strings, " +
617
+ "a single inline file object ({ name, mimeType, buffer }), or an array of inline file objects.";
618
+
619
+ function isInlineFileObject(value) {
620
+ return !!value
621
+ && typeof value === 'object'
622
+ && typeof value.name === 'string'
623
+ && typeof value.mimeType === 'string'
624
+ && 'buffer' in value;
625
+ }
626
+
627
+ function encodeInlineFileBuffer(buffer) {
628
+ if (typeof buffer === 'string') {
629
+ return buffer;
630
+ }
631
+ let bytes;
632
+ if (buffer instanceof ArrayBuffer) {
633
+ bytes = new Uint8Array(buffer);
634
+ } else if (ArrayBuffer.isView(buffer)) {
635
+ bytes = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
636
+ } else {
637
+ throw new Error(
638
+ "setInputFiles() inline file buffer must be a base64 string, ArrayBuffer, or TypedArray."
639
+ );
640
+ }
641
+ let binary = '';
642
+ for (let i = 0; i < bytes.length; i++) {
643
+ binary += String.fromCharCode(bytes[i]);
644
+ }
645
+ return btoa(binary);
646
+ }
647
+
648
+ function serializeInlineFile(file) {
649
+ return {
650
+ name: file.name,
651
+ mimeType: file.mimeType,
652
+ buffer: encodeInlineFileBuffer(file.buffer),
653
+ };
654
+ }
655
+
656
+ function normalizeSetInputFilesArg(files) {
657
+ if (typeof files === 'string') {
658
+ return files;
659
+ }
660
+ if (isInlineFileObject(files)) {
661
+ return serializeInlineFile(files);
662
+ }
663
+ if (!Array.isArray(files)) {
664
+ throw new Error(INPUT_FILES_VALIDATION_ERROR);
665
+ }
666
+ if (files.length === 0) {
667
+ return [];
668
+ }
669
+
670
+ let hasPaths = false;
671
+ let hasInline = false;
672
+ const inlineFiles = [];
673
+
674
+ for (const file of files) {
675
+ if (typeof file === 'string') {
676
+ hasPaths = true;
677
+ continue;
678
+ }
679
+ if (isInlineFileObject(file)) {
680
+ hasInline = true;
681
+ inlineFiles.push(serializeInlineFile(file));
682
+ continue;
683
+ }
684
+ throw new Error(INPUT_FILES_VALIDATION_ERROR);
685
+ }
686
+
687
+ if (hasPaths && hasInline) {
688
+ throw new Error(
689
+ "setInputFiles() does not support mixing file paths and inline file objects in the same array."
690
+ );
691
+ }
692
+ return hasInline ? inlineFiles : files;
693
+ }
694
+
654
695
  class Locator {
655
- #type; #value; #options;
656
- constructor(type, value, options) {
696
+ #type; #value; #options; #pageId;
697
+ constructor(type, value, options, pageId) {
657
698
  this.#type = type;
658
699
  this.#value = value;
659
700
  this.#options = options;
701
+ this.#pageId = pageId || "page_0";
660
702
  }
661
703
 
662
704
  _getInfo() { return [this.#type, this.#value, this.#options]; }
705
+ _getPageId() { return this.#pageId; }
706
+
707
+ // Helper to create a chained locator
708
+ _chain(childType, childValue, childOptions) {
709
+ const parentInfo = this._getInfo();
710
+ const childInfo = [childType, childValue, childOptions];
711
+ return new Locator("chained", JSON.stringify([parentInfo, childInfo]), null, this.#pageId);
712
+ }
663
713
 
664
714
  async click() {
665
- return __pw_invoke("locatorAction", [...this._getInfo(), "click", null]);
715
+ return __pw_invoke("locatorAction", [...this._getInfo(), "click", null], { pageId: this.#pageId });
666
716
  }
667
717
  async dblclick() {
668
- return __pw_invoke("locatorAction", [...this._getInfo(), "dblclick", null]);
718
+ return __pw_invoke("locatorAction", [...this._getInfo(), "dblclick", null], { pageId: this.#pageId });
669
719
  }
670
720
  async fill(text) {
671
- return __pw_invoke("locatorAction", [...this._getInfo(), "fill", text]);
721
+ return __pw_invoke("locatorAction", [...this._getInfo(), "fill", text], { pageId: this.#pageId });
672
722
  }
673
723
  async type(text) {
674
- return __pw_invoke("locatorAction", [...this._getInfo(), "type", text]);
724
+ return __pw_invoke("locatorAction", [...this._getInfo(), "type", text], { pageId: this.#pageId });
675
725
  }
676
726
  async check() {
677
- return __pw_invoke("locatorAction", [...this._getInfo(), "check", null]);
727
+ return __pw_invoke("locatorAction", [...this._getInfo(), "check", null], { pageId: this.#pageId });
678
728
  }
679
729
  async uncheck() {
680
- return __pw_invoke("locatorAction", [...this._getInfo(), "uncheck", null]);
730
+ return __pw_invoke("locatorAction", [...this._getInfo(), "uncheck", null], { pageId: this.#pageId });
681
731
  }
682
732
  async selectOption(value) {
683
- return __pw_invoke("locatorAction", [...this._getInfo(), "selectOption", value]);
733
+ return __pw_invoke("locatorAction", [...this._getInfo(), "selectOption", value], { pageId: this.#pageId });
684
734
  }
685
735
  async clear() {
686
- return __pw_invoke("locatorAction", [...this._getInfo(), "clear", null]);
736
+ return __pw_invoke("locatorAction", [...this._getInfo(), "clear", null], { pageId: this.#pageId });
687
737
  }
688
738
  async press(key) {
689
- return __pw_invoke("locatorAction", [...this._getInfo(), "press", key]);
739
+ return __pw_invoke("locatorAction", [...this._getInfo(), "press", key], { pageId: this.#pageId });
690
740
  }
691
741
  async hover() {
692
- return __pw_invoke("locatorAction", [...this._getInfo(), "hover", null]);
742
+ return __pw_invoke("locatorAction", [...this._getInfo(), "hover", null], { pageId: this.#pageId });
693
743
  }
694
744
  async focus() {
695
- return __pw_invoke("locatorAction", [...this._getInfo(), "focus", null]);
745
+ return __pw_invoke("locatorAction", [...this._getInfo(), "focus", null], { pageId: this.#pageId });
696
746
  }
697
747
  async textContent() {
698
- return __pw_invoke("locatorAction", [...this._getInfo(), "getText", null]);
748
+ return __pw_invoke("locatorAction", [...this._getInfo(), "getText", null], { pageId: this.#pageId });
699
749
  }
700
750
  async inputValue() {
701
- return __pw_invoke("locatorAction", [...this._getInfo(), "getValue", null]);
751
+ return __pw_invoke("locatorAction", [...this._getInfo(), "getValue", null], { pageId: this.#pageId });
702
752
  }
703
753
  async isVisible() {
704
- return __pw_invoke("locatorAction", [...this._getInfo(), "isVisible", null]);
754
+ return __pw_invoke("locatorAction", [...this._getInfo(), "isVisible", null], { pageId: this.#pageId });
705
755
  }
706
756
  async isEnabled() {
707
- return __pw_invoke("locatorAction", [...this._getInfo(), "isEnabled", null]);
757
+ return __pw_invoke("locatorAction", [...this._getInfo(), "isEnabled", null], { pageId: this.#pageId });
708
758
  }
709
759
  async isChecked() {
710
- return __pw_invoke("locatorAction", [...this._getInfo(), "isChecked", null]);
760
+ return __pw_invoke("locatorAction", [...this._getInfo(), "isChecked", null], { pageId: this.#pageId });
711
761
  }
712
762
  async count() {
713
- return __pw_invoke("locatorAction", [...this._getInfo(), "count", null]);
763
+ return __pw_invoke("locatorAction", [...this._getInfo(), "count", null], { pageId: this.#pageId });
714
764
  }
715
765
  async getAttribute(name) {
716
- return __pw_invoke("locatorAction", [...this._getInfo(), "getAttribute", name]);
766
+ return __pw_invoke("locatorAction", [...this._getInfo(), "getAttribute", name], { pageId: this.#pageId });
717
767
  }
718
768
  async isDisabled() {
719
- return __pw_invoke("locatorAction", [...this._getInfo(), "isDisabled", null]);
769
+ return __pw_invoke("locatorAction", [...this._getInfo(), "isDisabled", null], { pageId: this.#pageId });
720
770
  }
721
771
  async isHidden() {
722
- return __pw_invoke("locatorAction", [...this._getInfo(), "isHidden", null]);
772
+ return __pw_invoke("locatorAction", [...this._getInfo(), "isHidden", null], { pageId: this.#pageId });
723
773
  }
724
774
  async innerHTML() {
725
- return __pw_invoke("locatorAction", [...this._getInfo(), "innerHTML", null]);
775
+ return __pw_invoke("locatorAction", [...this._getInfo(), "innerHTML", null], { pageId: this.#pageId });
726
776
  }
727
777
  async innerText() {
728
- return __pw_invoke("locatorAction", [...this._getInfo(), "innerText", null]);
778
+ return __pw_invoke("locatorAction", [...this._getInfo(), "innerText", null], { pageId: this.#pageId });
729
779
  }
730
780
  async allTextContents() {
731
- return __pw_invoke("locatorAction", [...this._getInfo(), "allTextContents", null]);
781
+ return __pw_invoke("locatorAction", [...this._getInfo(), "allTextContents", null], { pageId: this.#pageId });
732
782
  }
733
783
  async allInnerTexts() {
734
- return __pw_invoke("locatorAction", [...this._getInfo(), "allInnerTexts", null]);
784
+ return __pw_invoke("locatorAction", [...this._getInfo(), "allInnerTexts", null], { pageId: this.#pageId });
735
785
  }
736
786
  async waitFor(options) {
737
- return __pw_invoke("locatorAction", [...this._getInfo(), "waitFor", options || {}]);
787
+ return __pw_invoke("locatorAction", [...this._getInfo(), "waitFor", options || {}], { pageId: this.#pageId });
738
788
  }
739
789
  async boundingBox() {
740
- return __pw_invoke("locatorAction", [...this._getInfo(), "boundingBox", null]);
790
+ return __pw_invoke("locatorAction", [...this._getInfo(), "boundingBox", null], { pageId: this.#pageId });
791
+ }
792
+ async setInputFiles(files) {
793
+ const serializedFiles = normalizeSetInputFilesArg(files);
794
+ return __pw_invoke("locatorAction", [...this._getInfo(), "setInputFiles", serializedFiles], { pageId: this.#pageId });
795
+ }
796
+ async screenshot(options) {
797
+ const base64 = await __pw_invoke("locatorAction", [...this._getInfo(), "screenshot", options || {}], { pageId: this.#pageId });
798
+ return base64;
799
+ }
800
+ async dragTo(target) {
801
+ const targetInfo = target._getInfo();
802
+ return __pw_invoke("locatorAction", [...this._getInfo(), "dragTo", targetInfo], { pageId: this.#pageId });
803
+ }
804
+ async scrollIntoViewIfNeeded() {
805
+ return __pw_invoke("locatorAction", [...this._getInfo(), "scrollIntoViewIfNeeded", null], { pageId: this.#pageId });
806
+ }
807
+ async highlight() {
808
+ return __pw_invoke("locatorAction", [...this._getInfo(), "highlight", null], { pageId: this.#pageId });
809
+ }
810
+ async evaluate(fn, arg) {
811
+ const fnString = typeof fn === 'function' ? fn.toString() : fn;
812
+ return __pw_invoke("locatorAction", [...this._getInfo(), "evaluate", [fnString, arg]], { pageId: this.#pageId });
813
+ }
814
+ async evaluateAll(fn, arg) {
815
+ const fnString = typeof fn === 'function' ? fn.toString() : fn;
816
+ return __pw_invoke("locatorAction", [...this._getInfo(), "evaluateAll", [fnString, arg]], { pageId: this.#pageId });
741
817
  }
742
818
  locator(selector) {
743
- const parentSelector = this.#type === 'css' ? this.#value : null;
744
- if (parentSelector) {
745
- return new Locator("css", parentSelector + " " + selector, this.#options);
746
- }
747
- // For non-css locators, use css with the combined approach
748
- return new Locator("css", selector, this.#options);
819
+ return this._chain("css", selector, null);
820
+ }
821
+ // Chaining: getBy* methods within a locator
822
+ getByRole(role, options) {
823
+ return this._chain("role", role, serializeOptions(options));
824
+ }
825
+ getByText(text) {
826
+ return this._chain("text", text, null);
827
+ }
828
+ getByLabel(label) {
829
+ return this._chain("label", label, null);
830
+ }
831
+ getByPlaceholder(placeholder) {
832
+ return this._chain("placeholder", placeholder, null);
833
+ }
834
+ getByTestId(testId) {
835
+ return this._chain("testId", testId, null);
836
+ }
837
+ getByAltText(altText) {
838
+ return this._chain("altText", altText, null);
839
+ }
840
+ getByTitle(title) {
841
+ return this._chain("title", title, null);
749
842
  }
750
843
  async all() {
751
844
  const n = await this.count();
@@ -757,7 +850,7 @@ async function setupPlaywright(context, options) {
757
850
  }
758
851
  nth(index) {
759
852
  const existingOpts = this.#options ? JSON.parse(this.#options) : {};
760
- return new Locator(this.#type, this.#value, JSON.stringify({ ...existingOpts, nth: index }));
853
+ return new Locator(this.#type, this.#value, JSON.stringify({ ...existingOpts, nth: index }), this.#pageId);
761
854
  }
762
855
  first() {
763
856
  return this.nth(0);
@@ -777,13 +870,28 @@ async function setupPlaywright(context, options) {
777
870
  if (hasNotText && typeof hasNotText === 'object' && typeof hasNotText.source === 'string' && typeof hasNotText.flags === 'string') {
778
871
  serializedFilter.hasNotText = { $regex: hasNotText.source, $flags: hasNotText.flags };
779
872
  }
780
- return new Locator(this.#type, this.#value, JSON.stringify({ ...existingOpts, filter: serializedFilter }));
873
+ // Serialize has/hasNot locators using duck-typing
874
+ const has = options.has;
875
+ if (has && typeof has === 'object' && typeof has._getInfo === 'function') {
876
+ serializedFilter.has = { $locator: has._getInfo() };
877
+ }
878
+ const hasNot = options.hasNot;
879
+ if (hasNot && typeof hasNot === 'object' && typeof hasNot._getInfo === 'function') {
880
+ serializedFilter.hasNot = { $locator: hasNot._getInfo() };
881
+ }
882
+ return new Locator(this.#type, this.#value, JSON.stringify({ ...existingOpts, filter: serializedFilter }), this.#pageId);
781
883
  }
782
884
  or(other) {
783
885
  // Create a composite locator that matches either this or other
784
886
  const thisInfo = this._getInfo();
785
887
  const otherInfo = other._getInfo();
786
- return new Locator("or", JSON.stringify([thisInfo, otherInfo]), null);
888
+ return new Locator("or", JSON.stringify([thisInfo, otherInfo]), null, this.#pageId);
889
+ }
890
+ and(other) {
891
+ // Create a composite locator that matches both this and other
892
+ const thisInfo = this._getInfo();
893
+ const otherInfo = other._getInfo();
894
+ return new Locator("and", JSON.stringify([thisInfo, otherInfo]), null, this.#pageId);
787
895
  }
788
896
  }
789
897
  globalThis.Locator = Locator;
@@ -794,84 +902,157 @@ async function setupPlaywright(context, options) {
794
902
  // Helper to create locator matchers
795
903
  function createLocatorMatchers(locator, baseMatchers) {
796
904
  const info = locator._getInfo();
905
+ const pageId = locator._getPageId ? locator._getPageId() : "page_0";
906
+
907
+ // Helper for serializing regex values
908
+ function serializeExpected(expected) {
909
+ if (expected instanceof RegExp) {
910
+ return { $regex: expected.source, $flags: expected.flags };
911
+ }
912
+ return expected;
913
+ }
797
914
 
798
915
  const locatorMatchers = {
799
916
  async toBeVisible(options) {
800
- return __pw_invoke("expectLocator", [...info, "toBeVisible", null, false, options?.timeout]);
917
+ return __pw_invoke("expectLocator", [...info, "toBeVisible", null, false, options?.timeout], { pageId });
801
918
  },
802
919
  async toContainText(expected, options) {
803
- const serialized = expected instanceof RegExp ? { $regex: expected.source, $flags: expected.flags } : expected;
804
- return __pw_invoke("expectLocator", [...info, "toContainText", serialized, false, options?.timeout]);
920
+ return __pw_invoke("expectLocator", [...info, "toContainText", serializeExpected(expected), false, options?.timeout], { pageId });
805
921
  },
806
922
  async toHaveValue(expected, options) {
807
- return __pw_invoke("expectLocator", [...info, "toHaveValue", expected, false, options?.timeout]);
923
+ return __pw_invoke("expectLocator", [...info, "toHaveValue", expected, false, options?.timeout], { pageId });
808
924
  },
809
925
  async toBeEnabled(options) {
810
- return __pw_invoke("expectLocator", [...info, "toBeEnabled", null, false, options?.timeout]);
926
+ return __pw_invoke("expectLocator", [...info, "toBeEnabled", null, false, options?.timeout], { pageId });
811
927
  },
812
928
  async toBeChecked(options) {
813
- return __pw_invoke("expectLocator", [...info, "toBeChecked", null, false, options?.timeout]);
929
+ return __pw_invoke("expectLocator", [...info, "toBeChecked", null, false, options?.timeout], { pageId });
814
930
  },
815
931
  async toHaveAttribute(name, value, options) {
816
- return __pw_invoke("expectLocator", [...info, "toHaveAttribute", { name, value }, false, options?.timeout]);
932
+ return __pw_invoke("expectLocator", [...info, "toHaveAttribute", { name, value: serializeExpected(value) }, false, options?.timeout], { pageId });
817
933
  },
818
934
  async toHaveText(expected, options) {
819
- const serialized = expected instanceof RegExp ? { $regex: expected.source, $flags: expected.flags } : expected;
820
- return __pw_invoke("expectLocator", [...info, "toHaveText", serialized, false, options?.timeout]);
935
+ return __pw_invoke("expectLocator", [...info, "toHaveText", serializeExpected(expected), false, options?.timeout], { pageId });
821
936
  },
822
937
  async toHaveCount(count, options) {
823
- return __pw_invoke("expectLocator", [...info, "toHaveCount", count, false, options?.timeout]);
938
+ return __pw_invoke("expectLocator", [...info, "toHaveCount", count, false, options?.timeout], { pageId });
824
939
  },
825
940
  async toBeHidden(options) {
826
- return __pw_invoke("expectLocator", [...info, "toBeHidden", null, false, options?.timeout]);
941
+ return __pw_invoke("expectLocator", [...info, "toBeHidden", null, false, options?.timeout], { pageId });
827
942
  },
828
943
  async toBeDisabled(options) {
829
- return __pw_invoke("expectLocator", [...info, "toBeDisabled", null, false, options?.timeout]);
944
+ return __pw_invoke("expectLocator", [...info, "toBeDisabled", null, false, options?.timeout], { pageId });
830
945
  },
831
946
  async toBeFocused(options) {
832
- return __pw_invoke("expectLocator", [...info, "toBeFocused", null, false, options?.timeout]);
947
+ return __pw_invoke("expectLocator", [...info, "toBeFocused", null, false, options?.timeout], { pageId });
833
948
  },
834
949
  async toBeEmpty(options) {
835
- return __pw_invoke("expectLocator", [...info, "toBeEmpty", null, false, options?.timeout]);
950
+ return __pw_invoke("expectLocator", [...info, "toBeEmpty", null, false, options?.timeout], { pageId });
951
+ },
952
+ // New matchers
953
+ async toBeAttached(options) {
954
+ return __pw_invoke("expectLocator", [...info, "toBeAttached", null, false, options?.timeout], { pageId });
955
+ },
956
+ async toBeEditable(options) {
957
+ return __pw_invoke("expectLocator", [...info, "toBeEditable", null, false, options?.timeout], { pageId });
958
+ },
959
+ async toHaveClass(expected, options) {
960
+ return __pw_invoke("expectLocator", [...info, "toHaveClass", serializeExpected(expected), false, options?.timeout], { pageId });
961
+ },
962
+ async toContainClass(expected, options) {
963
+ return __pw_invoke("expectLocator", [...info, "toContainClass", expected, false, options?.timeout], { pageId });
964
+ },
965
+ async toHaveId(expected, options) {
966
+ return __pw_invoke("expectLocator", [...info, "toHaveId", expected, false, options?.timeout], { pageId });
967
+ },
968
+ async toBeInViewport(options) {
969
+ return __pw_invoke("expectLocator", [...info, "toBeInViewport", null, false, options?.timeout], { pageId });
970
+ },
971
+ async toHaveCSS(name, value, options) {
972
+ return __pw_invoke("expectLocator", [...info, "toHaveCSS", { name, value: serializeExpected(value) }, false, options?.timeout], { pageId });
973
+ },
974
+ async toHaveJSProperty(name, value, options) {
975
+ return __pw_invoke("expectLocator", [...info, "toHaveJSProperty", { name, value }, false, options?.timeout], { pageId });
976
+ },
977
+ async toHaveAccessibleName(expected, options) {
978
+ return __pw_invoke("expectLocator", [...info, "toHaveAccessibleName", serializeExpected(expected), false, options?.timeout], { pageId });
979
+ },
980
+ async toHaveAccessibleDescription(expected, options) {
981
+ return __pw_invoke("expectLocator", [...info, "toHaveAccessibleDescription", serializeExpected(expected), false, options?.timeout], { pageId });
982
+ },
983
+ async toHaveRole(expected, options) {
984
+ return __pw_invoke("expectLocator", [...info, "toHaveRole", expected, false, options?.timeout], { pageId });
836
985
  },
837
986
  not: {
838
987
  async toBeVisible(options) {
839
- return __pw_invoke("expectLocator", [...info, "toBeVisible", null, true, options?.timeout]);
988
+ return __pw_invoke("expectLocator", [...info, "toBeVisible", null, true, options?.timeout], { pageId });
840
989
  },
841
990
  async toContainText(expected, options) {
842
- const serialized = expected instanceof RegExp ? { $regex: expected.source, $flags: expected.flags } : expected;
843
- return __pw_invoke("expectLocator", [...info, "toContainText", serialized, true, options?.timeout]);
991
+ return __pw_invoke("expectLocator", [...info, "toContainText", serializeExpected(expected), true, options?.timeout], { pageId });
844
992
  },
845
993
  async toHaveValue(expected, options) {
846
- return __pw_invoke("expectLocator", [...info, "toHaveValue", expected, true, options?.timeout]);
994
+ return __pw_invoke("expectLocator", [...info, "toHaveValue", expected, true, options?.timeout], { pageId });
847
995
  },
848
996
  async toBeEnabled(options) {
849
- return __pw_invoke("expectLocator", [...info, "toBeEnabled", null, true, options?.timeout]);
997
+ return __pw_invoke("expectLocator", [...info, "toBeEnabled", null, true, options?.timeout], { pageId });
850
998
  },
851
999
  async toBeChecked(options) {
852
- return __pw_invoke("expectLocator", [...info, "toBeChecked", null, true, options?.timeout]);
1000
+ return __pw_invoke("expectLocator", [...info, "toBeChecked", null, true, options?.timeout], { pageId });
853
1001
  },
854
1002
  async toHaveAttribute(name, value, options) {
855
- return __pw_invoke("expectLocator", [...info, "toHaveAttribute", { name, value }, true, options?.timeout]);
1003
+ return __pw_invoke("expectLocator", [...info, "toHaveAttribute", { name, value: serializeExpected(value) }, true, options?.timeout], { pageId });
856
1004
  },
857
1005
  async toHaveText(expected, options) {
858
- const serialized = expected instanceof RegExp ? { $regex: expected.source, $flags: expected.flags } : expected;
859
- return __pw_invoke("expectLocator", [...info, "toHaveText", serialized, true, options?.timeout]);
1006
+ return __pw_invoke("expectLocator", [...info, "toHaveText", serializeExpected(expected), true, options?.timeout], { pageId });
860
1007
  },
861
1008
  async toHaveCount(count, options) {
862
- return __pw_invoke("expectLocator", [...info, "toHaveCount", count, true, options?.timeout]);
1009
+ return __pw_invoke("expectLocator", [...info, "toHaveCount", count, true, options?.timeout], { pageId });
863
1010
  },
864
1011
  async toBeHidden(options) {
865
- return __pw_invoke("expectLocator", [...info, "toBeHidden", null, true, options?.timeout]);
1012
+ return __pw_invoke("expectLocator", [...info, "toBeHidden", null, true, options?.timeout], { pageId });
866
1013
  },
867
1014
  async toBeDisabled(options) {
868
- return __pw_invoke("expectLocator", [...info, "toBeDisabled", null, true, options?.timeout]);
1015
+ return __pw_invoke("expectLocator", [...info, "toBeDisabled", null, true, options?.timeout], { pageId });
869
1016
  },
870
1017
  async toBeFocused(options) {
871
- return __pw_invoke("expectLocator", [...info, "toBeFocused", null, true, options?.timeout]);
1018
+ return __pw_invoke("expectLocator", [...info, "toBeFocused", null, true, options?.timeout], { pageId });
872
1019
  },
873
1020
  async toBeEmpty(options) {
874
- return __pw_invoke("expectLocator", [...info, "toBeEmpty", null, true, options?.timeout]);
1021
+ return __pw_invoke("expectLocator", [...info, "toBeEmpty", null, true, options?.timeout], { pageId });
1022
+ },
1023
+ // New negated matchers
1024
+ async toBeAttached(options) {
1025
+ return __pw_invoke("expectLocator", [...info, "toBeAttached", null, true, options?.timeout], { pageId });
1026
+ },
1027
+ async toBeEditable(options) {
1028
+ return __pw_invoke("expectLocator", [...info, "toBeEditable", null, true, options?.timeout], { pageId });
1029
+ },
1030
+ async toHaveClass(expected, options) {
1031
+ return __pw_invoke("expectLocator", [...info, "toHaveClass", serializeExpected(expected), true, options?.timeout], { pageId });
1032
+ },
1033
+ async toContainClass(expected, options) {
1034
+ return __pw_invoke("expectLocator", [...info, "toContainClass", expected, true, options?.timeout], { pageId });
1035
+ },
1036
+ async toHaveId(expected, options) {
1037
+ return __pw_invoke("expectLocator", [...info, "toHaveId", expected, true, options?.timeout], { pageId });
1038
+ },
1039
+ async toBeInViewport(options) {
1040
+ return __pw_invoke("expectLocator", [...info, "toBeInViewport", null, true, options?.timeout], { pageId });
1041
+ },
1042
+ async toHaveCSS(name, value, options) {
1043
+ return __pw_invoke("expectLocator", [...info, "toHaveCSS", { name, value: serializeExpected(value) }, true, options?.timeout], { pageId });
1044
+ },
1045
+ async toHaveJSProperty(name, value, options) {
1046
+ return __pw_invoke("expectLocator", [...info, "toHaveJSProperty", { name, value }, true, options?.timeout], { pageId });
1047
+ },
1048
+ async toHaveAccessibleName(expected, options) {
1049
+ return __pw_invoke("expectLocator", [...info, "toHaveAccessibleName", serializeExpected(expected), true, options?.timeout], { pageId });
1050
+ },
1051
+ async toHaveAccessibleDescription(expected, options) {
1052
+ return __pw_invoke("expectLocator", [...info, "toHaveAccessibleDescription", serializeExpected(expected), true, options?.timeout], { pageId });
1053
+ },
1054
+ async toHaveRole(expected, options) {
1055
+ return __pw_invoke("expectLocator", [...info, "toHaveRole", expected, true, options?.timeout], { pageId });
875
1056
  },
876
1057
  }
877
1058
  };
@@ -887,6 +1068,44 @@ async function setupPlaywright(context, options) {
887
1068
  return locatorMatchers;
888
1069
  }
889
1070
 
1071
+ // Helper to create page matchers
1072
+ function createPageMatchers(page, baseMatchers) {
1073
+ const pageId = page.__pageId || "page_0";
1074
+
1075
+ function serializeExpected(expected) {
1076
+ if (expected instanceof RegExp) {
1077
+ return { $regex: expected.source, $flags: expected.flags };
1078
+ }
1079
+ return expected;
1080
+ }
1081
+
1082
+ const pageMatchers = {
1083
+ async toHaveURL(expected, options) {
1084
+ return __pw_invoke("expectPage", ["toHaveURL", serializeExpected(expected), false, options?.timeout], { pageId });
1085
+ },
1086
+ async toHaveTitle(expected, options) {
1087
+ return __pw_invoke("expectPage", ["toHaveTitle", serializeExpected(expected), false, options?.timeout], { pageId });
1088
+ },
1089
+ not: {
1090
+ async toHaveURL(expected, options) {
1091
+ return __pw_invoke("expectPage", ["toHaveURL", serializeExpected(expected), true, options?.timeout], { pageId });
1092
+ },
1093
+ async toHaveTitle(expected, options) {
1094
+ return __pw_invoke("expectPage", ["toHaveTitle", serializeExpected(expected), true, options?.timeout], { pageId });
1095
+ },
1096
+ }
1097
+ };
1098
+
1099
+ if (baseMatchers) {
1100
+ return {
1101
+ ...baseMatchers,
1102
+ ...pageMatchers,
1103
+ not: { ...baseMatchers.not, ...pageMatchers.not }
1104
+ };
1105
+ }
1106
+ return pageMatchers;
1107
+ }
1108
+
890
1109
  // Only extend expect if test-environment already defined it
891
1110
  if (typeof globalThis.expect === 'function') {
892
1111
  const originalExpect = globalThis.expect;
@@ -896,6 +1115,10 @@ async function setupPlaywright(context, options) {
896
1115
  if (actual && actual.constructor && actual.constructor.name === 'Locator') {
897
1116
  return createLocatorMatchers(actual, baseMatchers);
898
1117
  }
1118
+ // If actual is the page object (IsolatePage), add page-specific matchers
1119
+ if (actual && actual.__isPage === true) {
1120
+ return createPageMatchers(actual, baseMatchers);
1121
+ }
899
1122
  return baseMatchers;
900
1123
  };
901
1124
  }
@@ -931,7 +1154,10 @@ async function setupPlaywright(context, options) {
931
1154
  }
932
1155
  export {
933
1156
  setupPlaywright,
934
- createPlaywrightHandler
1157
+ getDefaultPlaywrightHandlerMetadata,
1158
+ defaultPlaywrightHandler,
1159
+ createPlaywrightHandler,
1160
+ DEFAULT_PLAYWRIGHT_HANDLER_META
935
1161
  };
936
1162
 
937
- //# debugId=6B90A150912AD17864756E2164756E21
1163
+ //# debugId=917AA0A37B759B7B64756E2164756E21