@jackwener/opencli 1.7.14 → 1.7.16
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 +9 -6
- package/README.zh-CN.md +9 -6
- package/cli-manifest.json +374 -74
- package/clis/bilibili/subtitle.js +1 -1
- package/clis/chatgpt/ask.js +2 -1
- package/clis/chatgpt/detail.js +6 -1
- package/clis/chatgpt/read.js +2 -1
- package/clis/chatgpt/send.js +2 -1
- package/clis/chatgpt/utils.js +54 -12
- package/clis/chatgpt/utils.test.js +36 -1
- package/clis/claude/ask.js +22 -7
- package/clis/claude/detail.js +9 -2
- package/clis/claude/new.js +8 -2
- package/clis/claude/read.js +2 -1
- package/clis/claude/send.js +8 -3
- package/clis/claude/utils.js +27 -4
- package/clis/deepseek/ask.js +21 -8
- package/clis/deepseek/detail.js +9 -1
- package/clis/deepseek/new.js +13 -2
- package/clis/deepseek/read.js +2 -1
- package/clis/deepseek/utils.js +8 -1
- package/clis/dianping/cityResolver.js +185 -0
- package/clis/dianping/dianping.test.js +154 -0
- package/clis/dianping/search.js +6 -3
- package/clis/douyin/_shared/browser-fetch.js +14 -2
- package/clis/douyin/_shared/browser-fetch.test.js +13 -0
- package/clis/douyin/stats.js +1 -1
- package/clis/douyin/update.js +1 -1
- package/clis/jike/search.js +1 -1
- package/clis/linkedin/search.js +8 -11
- package/clis/maimai/search-talents.js +10 -6
- package/clis/openreview/author.js +58 -0
- package/clis/openreview/openreview.test.js +83 -1
- package/clis/openreview/utils.js +14 -0
- package/clis/reddit/comment.js +1 -0
- package/clis/reddit/frontpage.js +1 -0
- package/clis/reddit/popular.js +1 -0
- package/clis/reddit/read.js +2 -0
- package/clis/reddit/read.test.js +4 -0
- package/clis/reddit/save.js +1 -0
- package/clis/reddit/saved.js +1 -0
- package/clis/reddit/search.js +2 -1
- package/clis/reddit/subreddit.js +2 -1
- package/clis/reddit/subscribe.js +1 -0
- package/clis/reddit/upvote.js +1 -0
- package/clis/reddit/upvoted.js +1 -0
- package/clis/reddit/user-comments.js +2 -1
- package/clis/reddit/user-posts.js +2 -1
- package/clis/reddit/user.js +2 -1
- package/clis/twitter/article.js +9 -5
- package/clis/twitter/bookmark-folder.js +187 -0
- package/clis/twitter/bookmark-folder.test.js +337 -0
- package/clis/twitter/bookmark-folders.js +115 -0
- package/clis/twitter/bookmark-folders.test.js +152 -0
- package/clis/twitter/bookmark.js +15 -6
- package/clis/twitter/bookmark.test.js +74 -0
- package/clis/twitter/bookmarks.js +10 -10
- package/clis/twitter/delete.js +11 -35
- package/clis/twitter/delete.test.js +21 -9
- package/clis/twitter/download.js +6 -5
- package/clis/twitter/followers.js +10 -3
- package/clis/twitter/following.js +14 -11
- package/clis/twitter/following.test.js +2 -1
- package/clis/twitter/hide-reply.js +24 -5
- package/clis/twitter/hide-reply.test.js +76 -0
- package/clis/twitter/like.js +21 -11
- package/clis/twitter/like.test.js +73 -0
- package/clis/twitter/likes.js +11 -11
- package/clis/twitter/list-add.js +8 -7
- package/clis/twitter/list-add.test.js +23 -1
- package/clis/twitter/list-remove.js +8 -7
- package/clis/twitter/list-remove.test.js +23 -1
- package/clis/twitter/list-tweets.js +9 -9
- package/clis/twitter/lists.js +6 -8
- package/clis/twitter/notifications.js +3 -2
- package/clis/twitter/profile.js +11 -7
- package/clis/twitter/quote.js +60 -32
- package/clis/twitter/quote.test.js +96 -8
- package/clis/twitter/reply.js +24 -178
- package/clis/twitter/reply.test.js +29 -11
- package/clis/twitter/retweet.js +9 -14
- package/clis/twitter/retweet.test.js +5 -1
- package/clis/twitter/search.js +176 -23
- package/clis/twitter/search.test.js +266 -1
- package/clis/twitter/shared.js +43 -0
- package/clis/twitter/shared.test.js +107 -1
- package/clis/twitter/thread.js +11 -11
- package/clis/twitter/timeline.js +13 -13
- package/clis/twitter/trending.js +4 -4
- package/clis/twitter/tweets.js +8 -9
- package/clis/twitter/unbookmark.js +13 -6
- package/clis/twitter/unbookmark.test.js +73 -0
- package/clis/twitter/unlike.js +6 -13
- package/clis/twitter/unlike.test.js +5 -2
- package/clis/twitter/unretweet.js +9 -14
- package/clis/twitter/unretweet.test.js +5 -1
- package/clis/twitter/utils.js +286 -0
- package/clis/twitter/utils.test.js +169 -0
- package/clis/youtube/like.js +6 -2
- package/clis/youtube/subscribe.js +6 -2
- package/clis/youtube/unlike.js +6 -2
- package/clis/youtube/unsubscribe.js +6 -2
- package/clis/youtube/utils.js +19 -13
- package/clis/youtube/utils.test.js +17 -1
- package/dist/src/browser/ax-snapshot.d.ts +37 -0
- package/dist/src/browser/ax-snapshot.js +217 -0
- package/dist/src/browser/ax-snapshot.test.d.ts +1 -0
- package/dist/src/browser/ax-snapshot.test.js +91 -0
- package/dist/src/browser/base-page.d.ts +51 -0
- package/dist/src/browser/base-page.js +545 -2
- package/dist/src/browser/base-page.test.js +520 -4
- package/dist/src/browser/bridge.d.ts +1 -0
- package/dist/src/browser/bridge.js +1 -1
- package/dist/src/browser/cdp-click-fixture.test.d.ts +1 -0
- package/dist/src/browser/cdp-click-fixture.test.js +87 -0
- package/dist/src/browser/cdp.d.ts +1 -0
- package/dist/src/browser/cdp.js +5 -0
- package/dist/src/browser/cdp.test.js +1 -0
- package/dist/src/browser/daemon-client.d.ts +5 -3
- package/dist/src/browser/daemon-client.js +6 -3
- package/dist/src/browser/daemon-client.test.js +10 -0
- package/dist/src/browser/find.d.ts +9 -1
- package/dist/src/browser/find.js +219 -0
- package/dist/src/browser/find.test.js +61 -1
- package/dist/src/browser/page.d.ts +4 -2
- package/dist/src/browser/page.js +18 -1
- package/dist/src/browser/page.test.js +28 -0
- package/dist/src/browser/target-errors.d.ts +3 -1
- package/dist/src/browser/target-errors.js +2 -0
- package/dist/src/browser/target-resolver.d.ts +14 -0
- package/dist/src/browser/target-resolver.js +28 -0
- package/dist/src/browser/visual-refs.d.ts +11 -0
- package/dist/src/browser/visual-refs.js +108 -0
- package/dist/src/build-manifest.d.ts +23 -0
- package/dist/src/build-manifest.js +34 -0
- package/dist/src/build-manifest.test.js +108 -1
- package/dist/src/cli.js +630 -60
- package/dist/src/cli.test.js +731 -1
- package/dist/src/commanderAdapter.js +7 -0
- package/dist/src/doctor.js +2 -2
- package/dist/src/doctor.test.js +4 -4
- package/dist/src/execution.d.ts +2 -0
- package/dist/src/execution.js +31 -6
- package/dist/src/execution.test.js +43 -16
- package/dist/src/external-clis.yaml +24 -0
- package/dist/src/help.d.ts +33 -0
- package/dist/src/help.js +174 -0
- package/dist/src/main.js +4 -14
- package/dist/src/runtime.d.ts +3 -0
- package/dist/src/runtime.js +1 -0
- package/dist/src/types.d.ts +83 -1
- package/package.json +1 -1
- package/scripts/typed-error-lint-baseline.json +18 -18
|
@@ -21,9 +21,12 @@ class ActionPage extends BasePage {
|
|
|
21
21
|
withArgsResults = [];
|
|
22
22
|
scripts = [];
|
|
23
23
|
withArgs = [];
|
|
24
|
+
screenshotCalls = [];
|
|
24
25
|
nativeType;
|
|
25
26
|
insertText;
|
|
26
27
|
nativeKeyPress;
|
|
28
|
+
nativeClick;
|
|
29
|
+
setFileInput;
|
|
27
30
|
cdp;
|
|
28
31
|
async goto() { }
|
|
29
32
|
async evaluate(js) {
|
|
@@ -36,7 +39,10 @@ class ActionPage extends BasePage {
|
|
|
36
39
|
return this.withArgsResults.shift() ?? null;
|
|
37
40
|
}
|
|
38
41
|
async getCookies() { return []; }
|
|
39
|
-
async screenshot() {
|
|
42
|
+
async screenshot(options = {}) {
|
|
43
|
+
this.screenshotCalls.push(options);
|
|
44
|
+
return 'shot';
|
|
45
|
+
}
|
|
40
46
|
async tabs() { return []; }
|
|
41
47
|
async selectTab() { }
|
|
42
48
|
}
|
|
@@ -98,6 +104,23 @@ describe('BasePage.fetchJson', () => {
|
|
|
98
104
|
});
|
|
99
105
|
});
|
|
100
106
|
});
|
|
107
|
+
describe('BasePage annotatedScreenshot', () => {
|
|
108
|
+
it('refreshes DOM refs, captures with a temporary visual overlay, and cleans up', async () => {
|
|
109
|
+
const page = new ActionPage();
|
|
110
|
+
page.results = [
|
|
111
|
+
'snapshot',
|
|
112
|
+
'["hash"]',
|
|
113
|
+
{ annotated: 1, truncated: false },
|
|
114
|
+
true,
|
|
115
|
+
];
|
|
116
|
+
await expect(page.annotatedScreenshot({ path: '/tmp/opencli.png', annotate: true })).resolves.toBe('shot');
|
|
117
|
+
expect(page.scripts[0]).toContain('const VIEWPORT_EXPAND = 0');
|
|
118
|
+
expect(page.scripts[2]).toContain('__opencli_visual_ref_overlay');
|
|
119
|
+
expect(page.scripts[2]).toContain('[data-opencli-ref]');
|
|
120
|
+
expect(page.scripts[3]).toContain('__opencli_visual_ref_overlay');
|
|
121
|
+
expect(page.screenshotCalls).toEqual([{ path: '/tmp/opencli.png', annotate: false }]);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
101
124
|
describe('BasePage native input routing', () => {
|
|
102
125
|
it('types rich-editor text via native Input.insertText when available', async () => {
|
|
103
126
|
const page = new ActionPage();
|
|
@@ -212,19 +235,512 @@ describe('BasePage native input routing', () => {
|
|
|
212
235
|
expect(err).toBeInstanceOf(TargetError);
|
|
213
236
|
expect(err.code).toBe('not_editable');
|
|
214
237
|
});
|
|
215
|
-
it('uses CDP DOM scrollIntoViewIfNeeded before
|
|
238
|
+
it('uses CDP DOM scrollIntoViewIfNeeded before measuring rect for click', async () => {
|
|
216
239
|
const page = new ActionPage();
|
|
240
|
+
page.nativeClick = vi.fn().mockResolvedValue(undefined);
|
|
217
241
|
page.cdp = vi.fn()
|
|
218
242
|
.mockResolvedValueOnce({})
|
|
219
243
|
.mockResolvedValueOnce({ root: { nodeId: 1 } })
|
|
220
244
|
.mockResolvedValueOnce({ nodeId: 9 })
|
|
221
245
|
.mockResolvedValueOnce({});
|
|
222
|
-
page.results = [resolveOk, {
|
|
223
|
-
page.withArgsResults = [{ ok: true }, undefined];
|
|
246
|
+
page.results = [resolveOk, { x: 50, y: 100, w: 200, h: 32, visible: true }];
|
|
247
|
+
page.withArgsResults = [{ ok: true, multiple: false, accept: 'application/pdf' }, undefined];
|
|
224
248
|
await page.click('#save');
|
|
225
249
|
expect(page.cdp).toHaveBeenCalledWith('DOM.scrollIntoViewIfNeeded', { nodeId: 9 });
|
|
250
|
+
// After CDP scroll, boundingRectResolvedJs runs with skipScroll=true.
|
|
226
251
|
expect(page.scripts.at(-1)).toContain('if (false) el.scrollIntoView');
|
|
227
252
|
});
|
|
253
|
+
it('clicks via CDP Input.dispatchMouseEvent when rect is visible', async () => {
|
|
254
|
+
const page = new ActionPage();
|
|
255
|
+
page.nativeClick = vi.fn().mockResolvedValue(undefined);
|
|
256
|
+
page.results = [resolveOk, { x: 50, y: 100, w: 200, h: 32, visible: true }];
|
|
257
|
+
await page.click('#category');
|
|
258
|
+
expect(page.nativeClick).toHaveBeenCalledWith(50, 100);
|
|
259
|
+
expect(page.nativeClick).toHaveBeenCalledTimes(1);
|
|
260
|
+
expect(page.scripts).toHaveLength(2);
|
|
261
|
+
expect(page.scripts[1]).toContain('getBoundingClientRect');
|
|
262
|
+
expect(page.scripts.join('\n')).not.toContain('el.click()');
|
|
263
|
+
});
|
|
264
|
+
it('clicks AX snapshot refs through backend node coordinates without DOM resolver', async () => {
|
|
265
|
+
const page = new ActionPage();
|
|
266
|
+
page.nativeClick = vi.fn().mockResolvedValue(undefined);
|
|
267
|
+
page.cdp = vi.fn(async (method) => {
|
|
268
|
+
if (method === 'Accessibility.getFullAXTree') {
|
|
269
|
+
return {
|
|
270
|
+
nodes: [
|
|
271
|
+
{ nodeId: '1', role: { value: 'RootWebArea' }, name: { value: 'Demo' }, childIds: ['2'] },
|
|
272
|
+
{ nodeId: '2', role: { value: 'button' }, name: { value: 'Submit' }, backendDOMNodeId: 10 },
|
|
273
|
+
],
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
if (method === 'Page.getFrameTree') {
|
|
277
|
+
return {
|
|
278
|
+
frameTree: { frame: { id: 'root', url: 'https://app.example/' } },
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
if (method === 'DOM.getBoxModel') {
|
|
282
|
+
return { model: { content: [10, 20, 50, 20, 50, 40, 10, 40] } };
|
|
283
|
+
}
|
|
284
|
+
return {};
|
|
285
|
+
});
|
|
286
|
+
await expect(page.snapshot({ source: 'ax' })).resolves.toContain('[1]button "Submit"');
|
|
287
|
+
await expect(page.click('1')).resolves.toEqual({ matches_n: 1, match_level: 'exact' });
|
|
288
|
+
expect(page.cdp).toHaveBeenNthCalledWith(1, 'Accessibility.enable', {});
|
|
289
|
+
expect(page.cdp).toHaveBeenNthCalledWith(2, 'Accessibility.getFullAXTree', {});
|
|
290
|
+
expect(page.cdp).toHaveBeenCalledWith('Accessibility.getFullAXTree', {});
|
|
291
|
+
expect(page.cdp).toHaveBeenCalledWith('Page.getFrameTree', {});
|
|
292
|
+
expect(page.cdp).toHaveBeenCalledWith('DOM.getBoxModel', { backendNodeId: 10 });
|
|
293
|
+
expect(page.nativeClick).toHaveBeenCalledWith(30, 30);
|
|
294
|
+
expect(page.scripts).toHaveLength(0);
|
|
295
|
+
});
|
|
296
|
+
it('adds same-origin iframe AX refs and clicks them by frame-scoped backend node', async () => {
|
|
297
|
+
const page = new ActionPage();
|
|
298
|
+
page.nativeClick = vi.fn().mockResolvedValue(undefined);
|
|
299
|
+
page.cdp = vi.fn(async (method, params) => {
|
|
300
|
+
if (method === 'Accessibility.getFullAXTree' && params?.frameId === 'same-frame') {
|
|
301
|
+
return {
|
|
302
|
+
nodes: [
|
|
303
|
+
{ nodeId: '1', role: { value: 'RootWebArea' }, name: { value: 'Embedded' }, childIds: ['2'] },
|
|
304
|
+
{ nodeId: '2', role: { value: 'button' }, name: { value: 'Frame Save' }, backendDOMNodeId: 20 },
|
|
305
|
+
],
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
if (method === 'Accessibility.getFullAXTree' && params?.frameId === 'cross-frame') {
|
|
309
|
+
return {
|
|
310
|
+
nodes: [
|
|
311
|
+
{ nodeId: '1', role: { value: 'RootWebArea' }, name: { value: 'Cross' }, childIds: ['2'] },
|
|
312
|
+
{ nodeId: '2', role: { value: 'button' }, name: { value: 'Cross Save' }, backendDOMNodeId: 30 },
|
|
313
|
+
],
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
if (method === 'Accessibility.getFullAXTree') {
|
|
317
|
+
return {
|
|
318
|
+
nodes: [
|
|
319
|
+
{ nodeId: '1', role: { value: 'RootWebArea' }, name: { value: 'Demo' }, childIds: ['2'] },
|
|
320
|
+
{ nodeId: '2', role: { value: 'button' }, name: { value: 'Main Save' }, backendDOMNodeId: 10 },
|
|
321
|
+
],
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
if (method === 'Page.getFrameTree') {
|
|
325
|
+
return {
|
|
326
|
+
frameTree: {
|
|
327
|
+
frame: { id: 'root', url: 'https://app.example/' },
|
|
328
|
+
childFrames: [
|
|
329
|
+
{ frame: { id: 'same-frame', url: 'https://app.example/embed' } },
|
|
330
|
+
{ frame: { id: 'cross-frame', url: 'https://other.example/embed' } },
|
|
331
|
+
],
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
if (method === 'DOM.getBoxModel') {
|
|
336
|
+
return { model: { content: [100, 200, 140, 200, 140, 220, 100, 220] } };
|
|
337
|
+
}
|
|
338
|
+
return {};
|
|
339
|
+
});
|
|
340
|
+
const snapshot = await page.snapshot({ source: 'ax' });
|
|
341
|
+
expect(snapshot).toContain('[1]button "Main Save"');
|
|
342
|
+
expect(snapshot).toContain('frame "https://app.example/embed":');
|
|
343
|
+
expect(snapshot).toContain('[2]button "Frame Save"');
|
|
344
|
+
expect(snapshot).toContain('frame "https://other.example/embed":');
|
|
345
|
+
expect(snapshot).toContain('[3]button "Cross Save"');
|
|
346
|
+
await expect(page.click('2')).resolves.toEqual({ matches_n: 1, match_level: 'exact' });
|
|
347
|
+
expect(page.cdp).toHaveBeenCalledWith('Accessibility.getFullAXTree', { frameId: 'same-frame' });
|
|
348
|
+
expect(page.cdp).toHaveBeenCalledWith('Accessibility.enable', { frameId: 'cross-frame', sessionId: 'target', targetUrl: 'https://other.example/embed' });
|
|
349
|
+
expect(page.cdp).toHaveBeenCalledWith('Accessibility.getFullAXTree', { frameId: 'cross-frame', sessionId: 'target', targetUrl: 'https://other.example/embed' });
|
|
350
|
+
expect(page.cdp).toHaveBeenCalledWith('DOM.getBoxModel', { backendNodeId: 20 });
|
|
351
|
+
expect(page.nativeClick).toHaveBeenCalledWith(120, 210);
|
|
352
|
+
});
|
|
353
|
+
it('clicks cross-origin AX refs through a frame-target CDP route', async () => {
|
|
354
|
+
const page = new ActionPage();
|
|
355
|
+
page.nativeClick = vi.fn().mockResolvedValue(undefined);
|
|
356
|
+
page.cdp = vi.fn(async (method, params) => {
|
|
357
|
+
if (method === 'Page.getFrameTree') {
|
|
358
|
+
return {
|
|
359
|
+
frameTree: {
|
|
360
|
+
frame: { id: 'root', url: 'https://app.example/' },
|
|
361
|
+
childFrames: [{ frame: { id: 'cross-frame', url: 'https://other.example/embed' } }],
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
if (method === 'Accessibility.getFullAXTree' && params?.sessionId === 'target') {
|
|
366
|
+
return {
|
|
367
|
+
nodes: [
|
|
368
|
+
{ nodeId: '1', role: { value: 'RootWebArea' }, childIds: ['2'] },
|
|
369
|
+
{ nodeId: '2', role: { value: 'button' }, name: { value: 'Cross Save' }, backendDOMNodeId: 99 },
|
|
370
|
+
],
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
if (method === 'Accessibility.getFullAXTree') {
|
|
374
|
+
return { nodes: [{ nodeId: '1', role: { value: 'RootWebArea' } }] };
|
|
375
|
+
}
|
|
376
|
+
if (method === 'DOM.getBoxModel') {
|
|
377
|
+
return { model: { content: [300, 400, 340, 400, 340, 420, 300, 420] } };
|
|
378
|
+
}
|
|
379
|
+
return {};
|
|
380
|
+
});
|
|
381
|
+
const snapshot = await page.snapshot({ source: 'ax' });
|
|
382
|
+
expect(snapshot).toContain('[1]button "Cross Save"');
|
|
383
|
+
await expect(page.click('1')).resolves.toEqual({ matches_n: 1, match_level: 'exact' });
|
|
384
|
+
expect(page.cdp).toHaveBeenCalledWith('Accessibility.enable', { frameId: 'cross-frame', sessionId: 'target', targetUrl: 'https://other.example/embed' });
|
|
385
|
+
expect(page.cdp).toHaveBeenCalledWith('Accessibility.getFullAXTree', { frameId: 'cross-frame', sessionId: 'target', targetUrl: 'https://other.example/embed' });
|
|
386
|
+
expect(page.cdp).toHaveBeenCalledWith('DOM.getBoxModel', { backendNodeId: 99, frameId: 'cross-frame', sessionId: 'target', targetUrl: 'https://other.example/embed' });
|
|
387
|
+
expect(page.nativeClick).toHaveBeenCalledWith(320, 410);
|
|
388
|
+
});
|
|
389
|
+
it('enables Accessibility in cross-origin frame target sessions before stale AX recovery', async () => {
|
|
390
|
+
const page = new ActionPage();
|
|
391
|
+
page.nativeClick = vi.fn().mockResolvedValue(undefined);
|
|
392
|
+
let crossAxCalls = 0;
|
|
393
|
+
page.cdp = vi.fn(async (method, params) => {
|
|
394
|
+
if (method === 'Page.getFrameTree') {
|
|
395
|
+
return {
|
|
396
|
+
frameTree: {
|
|
397
|
+
frame: { id: 'root', url: 'https://app.example/' },
|
|
398
|
+
childFrames: [{ frame: { id: 'cross-frame', url: 'https://other.example/embed' } }],
|
|
399
|
+
},
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
if (method === 'Accessibility.getFullAXTree' && params?.sessionId === 'target') {
|
|
403
|
+
crossAxCalls++;
|
|
404
|
+
const backendDOMNodeId = crossAxCalls === 1 ? 99 : 100;
|
|
405
|
+
return {
|
|
406
|
+
nodes: [
|
|
407
|
+
{ nodeId: '1', role: { value: 'RootWebArea' }, childIds: ['2'] },
|
|
408
|
+
{ nodeId: '2', role: { value: 'button' }, name: { value: 'Cross Save' }, backendDOMNodeId },
|
|
409
|
+
],
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
if (method === 'Accessibility.getFullAXTree') {
|
|
413
|
+
return { nodes: [{ nodeId: '1', role: { value: 'RootWebArea' } }] };
|
|
414
|
+
}
|
|
415
|
+
if (method === 'DOM.getBoxModel') {
|
|
416
|
+
if (params?.backendNodeId === 99)
|
|
417
|
+
throw new Error('No node with given id found');
|
|
418
|
+
return { model: { content: [300, 400, 340, 400, 340, 420, 300, 420] } };
|
|
419
|
+
}
|
|
420
|
+
return {};
|
|
421
|
+
});
|
|
422
|
+
await page.snapshot({ source: 'ax' });
|
|
423
|
+
await expect(page.click('1')).resolves.toEqual({ matches_n: 1, match_level: 'reidentified' });
|
|
424
|
+
expect(page.cdp).toHaveBeenCalledWith('Accessibility.enable', { frameId: 'cross-frame', sessionId: 'target', targetUrl: 'https://other.example/embed' });
|
|
425
|
+
expect(page.cdp).toHaveBeenCalledWith('Accessibility.getFullAXTree', { frameId: 'cross-frame', sessionId: 'target', targetUrl: 'https://other.example/embed' });
|
|
426
|
+
expect(page.cdp).toHaveBeenCalledWith('DOM.getBoxModel', { backendNodeId: 99, frameId: 'cross-frame', sessionId: 'target', targetUrl: 'https://other.example/embed' });
|
|
427
|
+
expect(page.cdp).toHaveBeenCalledWith('DOM.getBoxModel', { backendNodeId: 100, frameId: 'cross-frame', sessionId: 'target', targetUrl: 'https://other.example/embed' });
|
|
428
|
+
expect(page.nativeClick).toHaveBeenCalledWith(320, 410);
|
|
429
|
+
});
|
|
430
|
+
it('recovers stale iframe AX refs inside the original frame', async () => {
|
|
431
|
+
const page = new ActionPage();
|
|
432
|
+
page.nativeClick = vi.fn().mockResolvedValue(undefined);
|
|
433
|
+
let iframeAxCalls = 0;
|
|
434
|
+
page.cdp = vi.fn(async (method, params) => {
|
|
435
|
+
if (method === 'Page.getFrameTree') {
|
|
436
|
+
return {
|
|
437
|
+
frameTree: {
|
|
438
|
+
frame: { id: 'root', url: 'https://app.example/' },
|
|
439
|
+
childFrames: [{ frame: { id: 'same-frame', url: 'https://app.example/embed' } }],
|
|
440
|
+
},
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
if (method === 'Accessibility.getFullAXTree' && params?.frameId === 'same-frame') {
|
|
444
|
+
iframeAxCalls++;
|
|
445
|
+
return {
|
|
446
|
+
nodes: [
|
|
447
|
+
{ nodeId: '1', role: { value: 'RootWebArea' }, childIds: ['2'] },
|
|
448
|
+
{ nodeId: '2', role: { value: 'button' }, name: { value: 'Frame Save' }, backendDOMNodeId: iframeAxCalls === 1 ? 20 : 42 },
|
|
449
|
+
],
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
if (method === 'Accessibility.getFullAXTree') {
|
|
453
|
+
return {
|
|
454
|
+
nodes: [
|
|
455
|
+
{ nodeId: '1', role: { value: 'RootWebArea' }, childIds: ['2'] },
|
|
456
|
+
{ nodeId: '2', role: { value: 'button' }, name: { value: 'Main Save' }, backendDOMNodeId: 10 },
|
|
457
|
+
],
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
if (method === 'DOM.getBoxModel') {
|
|
461
|
+
if (params?.backendNodeId === 20)
|
|
462
|
+
throw new Error('No node with given id found');
|
|
463
|
+
return { model: { content: [200, 300, 240, 300, 240, 320, 200, 320] } };
|
|
464
|
+
}
|
|
465
|
+
return {};
|
|
466
|
+
});
|
|
467
|
+
await page.snapshot({ source: 'ax' });
|
|
468
|
+
await expect(page.click('2')).resolves.toEqual({ matches_n: 1, match_level: 'reidentified' });
|
|
469
|
+
expect(page.cdp).toHaveBeenCalledWith('Accessibility.enable', {});
|
|
470
|
+
expect(page.cdp).toHaveBeenCalledWith('Accessibility.getFullAXTree', { frameId: 'same-frame' });
|
|
471
|
+
expect(page.cdp).toHaveBeenCalledWith('DOM.getBoxModel', { backendNodeId: 20 });
|
|
472
|
+
expect(page.cdp).toHaveBeenCalledWith('DOM.getBoxModel', { backendNodeId: 42 });
|
|
473
|
+
expect(page.nativeClick).toHaveBeenCalledWith(220, 310);
|
|
474
|
+
});
|
|
475
|
+
it('recovers stale AX refs by role/name/nth before clicking', async () => {
|
|
476
|
+
const page = new ActionPage();
|
|
477
|
+
page.nativeClick = vi.fn().mockResolvedValue(undefined);
|
|
478
|
+
let axCalls = 0;
|
|
479
|
+
page.cdp = vi.fn(async (method, params) => {
|
|
480
|
+
if (method === 'Accessibility.getFullAXTree') {
|
|
481
|
+
axCalls++;
|
|
482
|
+
return {
|
|
483
|
+
nodes: [
|
|
484
|
+
{ nodeId: '1', role: { value: 'RootWebArea' }, name: { value: 'Demo' }, childIds: ['2'] },
|
|
485
|
+
{ nodeId: '2', role: { value: 'button' }, name: { value: 'Submit' }, backendDOMNodeId: axCalls === 1 ? 10 : 42 },
|
|
486
|
+
],
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
if (method === 'DOM.getBoxModel') {
|
|
490
|
+
if (params?.backendNodeId === 10)
|
|
491
|
+
throw new Error('No node with given id found');
|
|
492
|
+
return { model: { content: [100, 200, 140, 200, 140, 220, 100, 220] } };
|
|
493
|
+
}
|
|
494
|
+
return {};
|
|
495
|
+
});
|
|
496
|
+
await page.snapshot({ source: 'ax' });
|
|
497
|
+
await expect(page.click('1')).resolves.toEqual({ matches_n: 1, match_level: 'reidentified' });
|
|
498
|
+
expect(page.cdp).toHaveBeenCalledWith('DOM.getBoxModel', { backendNodeId: 10 });
|
|
499
|
+
expect(page.cdp).toHaveBeenCalledWith('DOM.getBoxModel', { backendNodeId: 42 });
|
|
500
|
+
expect(page.nativeClick).toHaveBeenCalledWith(120, 210);
|
|
501
|
+
});
|
|
502
|
+
it('recovers AX refs across 10 repeated React-style rerenders', async () => {
|
|
503
|
+
const page = new ActionPage();
|
|
504
|
+
page.nativeClick = vi.fn().mockResolvedValue(undefined);
|
|
505
|
+
let currentBackendId = 100;
|
|
506
|
+
const staleBackendIds = new Set();
|
|
507
|
+
page.cdp = vi.fn(async (method, params) => {
|
|
508
|
+
if (method === 'Accessibility.getFullAXTree') {
|
|
509
|
+
return {
|
|
510
|
+
nodes: [
|
|
511
|
+
{ nodeId: '1', role: { value: 'RootWebArea' }, name: { value: 'Demo' }, childIds: ['2'] },
|
|
512
|
+
{ nodeId: '2', role: { value: 'button' }, name: { value: 'Submit' }, backendDOMNodeId: currentBackendId },
|
|
513
|
+
],
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
if (method === 'DOM.getBoxModel') {
|
|
517
|
+
const id = params?.backendNodeId;
|
|
518
|
+
if (staleBackendIds.has(id))
|
|
519
|
+
throw new Error('No node with given id found');
|
|
520
|
+
return { model: { content: [id, id, id + 20, id, id + 20, id + 10, id, id + 10] } };
|
|
521
|
+
}
|
|
522
|
+
return {};
|
|
523
|
+
});
|
|
524
|
+
await page.snapshot({ source: 'ax' });
|
|
525
|
+
for (let i = 0; i < 10; i++) {
|
|
526
|
+
staleBackendIds.add(currentBackendId);
|
|
527
|
+
currentBackendId += 1;
|
|
528
|
+
await expect(page.click('1')).resolves.toEqual({ matches_n: 1, match_level: 'reidentified' });
|
|
529
|
+
}
|
|
530
|
+
expect(page.nativeClick).toHaveBeenCalledTimes(10);
|
|
531
|
+
});
|
|
532
|
+
it('falls back to JS el.click() when nativeClick is unavailable', async () => {
|
|
533
|
+
const page = new ActionPage();
|
|
534
|
+
page.results = [
|
|
535
|
+
resolveOk,
|
|
536
|
+
{ x: 50, y: 100, w: 200, h: 32, visible: true },
|
|
537
|
+
{ status: 'clicked', x: 50, y: 100 },
|
|
538
|
+
];
|
|
539
|
+
await page.click('#save');
|
|
540
|
+
expect(page.scripts).toHaveLength(3);
|
|
541
|
+
expect(page.scripts[1]).toContain('getBoundingClientRect');
|
|
542
|
+
expect(page.scripts[2]).toContain('el.click()');
|
|
543
|
+
});
|
|
544
|
+
it('falls back to JS el.click() when rect is zero-area', async () => {
|
|
545
|
+
const page = new ActionPage();
|
|
546
|
+
page.nativeClick = vi.fn().mockResolvedValue(undefined);
|
|
547
|
+
page.results = [
|
|
548
|
+
resolveOk,
|
|
549
|
+
{ x: 0, y: 0, w: 0, h: 0, visible: false },
|
|
550
|
+
{ status: 'clicked', x: 0, y: 0 },
|
|
551
|
+
];
|
|
552
|
+
await page.click('#hidden');
|
|
553
|
+
expect(page.nativeClick).not.toHaveBeenCalled();
|
|
554
|
+
expect(page.scripts).toHaveLength(3);
|
|
555
|
+
expect(page.scripts[2]).toContain('el.click()');
|
|
556
|
+
});
|
|
557
|
+
it('retries CDP click when JS path throws but yields coordinates', async () => {
|
|
558
|
+
const page = new ActionPage();
|
|
559
|
+
const nativeClick = vi.fn()
|
|
560
|
+
.mockRejectedValueOnce(new Error('cdp transient'))
|
|
561
|
+
.mockResolvedValueOnce(undefined);
|
|
562
|
+
page.nativeClick = nativeClick;
|
|
563
|
+
page.results = [
|
|
564
|
+
resolveOk,
|
|
565
|
+
{ x: 10, y: 20, w: 100, h: 30, visible: true },
|
|
566
|
+
{ status: 'js_failed', x: 10, y: 20, error: 'click intercepted' },
|
|
567
|
+
];
|
|
568
|
+
await page.click('#flaky');
|
|
569
|
+
expect(nativeClick).toHaveBeenCalledTimes(2);
|
|
570
|
+
expect(nativeClick).toHaveBeenNthCalledWith(1, 10, 20);
|
|
571
|
+
expect(nativeClick).toHaveBeenNthCalledWith(2, 10, 20);
|
|
572
|
+
});
|
|
573
|
+
it('hovers via native CDP mouseMoved when coordinates are available', async () => {
|
|
574
|
+
const page = new ActionPage();
|
|
575
|
+
page.cdp = vi.fn().mockResolvedValue({});
|
|
576
|
+
page.results = [resolveOk, { x: 70, y: 80, w: 100, h: 20, visible: true }];
|
|
577
|
+
await expect(page.hover('#menu')).resolves.toEqual({ matches_n: 1, match_level: 'exact' });
|
|
578
|
+
expect(page.cdp).toHaveBeenCalledWith('Input.dispatchMouseEvent', { type: 'mouseMoved', x: 70, y: 80 });
|
|
579
|
+
expect(page.scripts.at(-1)).toContain('getBoundingClientRect');
|
|
580
|
+
});
|
|
581
|
+
it('focuses through CDP DOM.focus when available', async () => {
|
|
582
|
+
const page = new ActionPage();
|
|
583
|
+
page.cdp = vi.fn(async (method) => {
|
|
584
|
+
if (method === 'DOM.getDocument')
|
|
585
|
+
return { root: { nodeId: 1 } };
|
|
586
|
+
if (method === 'DOM.querySelector')
|
|
587
|
+
return { nodeId: 9 };
|
|
588
|
+
return {};
|
|
589
|
+
});
|
|
590
|
+
page.results = [resolveOk, true];
|
|
591
|
+
page.withArgsResults = [{ ok: true }, undefined];
|
|
592
|
+
await expect(page.focus('#email')).resolves.toEqual({ focused: true, matches_n: 1, match_level: 'exact' });
|
|
593
|
+
expect(page.cdp).toHaveBeenCalledWith('DOM.focus', { nodeId: 9 });
|
|
594
|
+
});
|
|
595
|
+
it('verifies CDP focus and falls back to DOM focus when focus did not stick', async () => {
|
|
596
|
+
const page = new ActionPage();
|
|
597
|
+
page.cdp = vi.fn(async (method) => {
|
|
598
|
+
if (method === 'DOM.getDocument')
|
|
599
|
+
return { root: { nodeId: 1 } };
|
|
600
|
+
if (method === 'DOM.querySelector')
|
|
601
|
+
return { nodeId: 9 };
|
|
602
|
+
return {};
|
|
603
|
+
});
|
|
604
|
+
page.results = [resolveOk, false, true];
|
|
605
|
+
page.withArgsResults = [{ ok: true }, undefined];
|
|
606
|
+
await expect(page.focus('#email')).resolves.toEqual({ focused: true, matches_n: 1, match_level: 'exact' });
|
|
607
|
+
expect(page.cdp).toHaveBeenCalledWith('DOM.focus', { nodeId: 9 });
|
|
608
|
+
expect(page.scripts.at(-2)).toContain('document.activeElement === el');
|
|
609
|
+
expect(page.scripts.at(-1)).toContain('el.focus');
|
|
610
|
+
});
|
|
611
|
+
it('double-clicks via native CDP mouse events', async () => {
|
|
612
|
+
const page = new ActionPage();
|
|
613
|
+
page.cdp = vi.fn().mockResolvedValue({});
|
|
614
|
+
page.results = [resolveOk, { x: 20, y: 30, w: 100, h: 20, visible: true }];
|
|
615
|
+
await expect(page.dblClick('#row')).resolves.toEqual({ matches_n: 1, match_level: 'exact' });
|
|
616
|
+
expect(page.cdp).toHaveBeenCalledWith('Input.dispatchMouseEvent', { type: 'mouseMoved', x: 20, y: 30 });
|
|
617
|
+
expect(page.cdp).toHaveBeenCalledWith('Input.dispatchMouseEvent', { type: 'mousePressed', x: 20, y: 30, button: 'left', clickCount: 2 });
|
|
618
|
+
expect(page.cdp).toHaveBeenCalledWith('Input.dispatchMouseEvent', { type: 'mouseReleased', x: 20, y: 30, button: 'left', clickCount: 2 });
|
|
619
|
+
});
|
|
620
|
+
it('checks a checkbox only when its current state differs', async () => {
|
|
621
|
+
const page = new ActionPage();
|
|
622
|
+
page.nativeClick = vi.fn().mockResolvedValue(undefined);
|
|
623
|
+
page.results = [
|
|
624
|
+
resolveOk,
|
|
625
|
+
{ ok: true, checked: false, disabled: false, kind: 'checkbox' },
|
|
626
|
+
resolveOk,
|
|
627
|
+
{ x: 20, y: 30, w: 40, h: 20, visible: true },
|
|
628
|
+
{ ok: true, checked: true, disabled: false, kind: 'checkbox' },
|
|
629
|
+
];
|
|
630
|
+
await expect(page.setChecked('#agree', true)).resolves.toEqual({
|
|
631
|
+
checked: true,
|
|
632
|
+
changed: true,
|
|
633
|
+
matches_n: 1,
|
|
634
|
+
match_level: 'exact',
|
|
635
|
+
kind: 'checkbox',
|
|
636
|
+
});
|
|
637
|
+
expect(page.nativeClick).toHaveBeenCalledWith(20, 30);
|
|
638
|
+
});
|
|
639
|
+
it('does not click a checkbox that already has the requested state', async () => {
|
|
640
|
+
const page = new ActionPage();
|
|
641
|
+
page.nativeClick = vi.fn().mockResolvedValue(undefined);
|
|
642
|
+
page.results = [resolveOk, { ok: true, checked: false, disabled: false, kind: 'checkbox' }];
|
|
643
|
+
await expect(page.setChecked('#agree', false)).resolves.toEqual({
|
|
644
|
+
checked: false,
|
|
645
|
+
changed: false,
|
|
646
|
+
matches_n: 1,
|
|
647
|
+
match_level: 'exact',
|
|
648
|
+
kind: 'checkbox',
|
|
649
|
+
});
|
|
650
|
+
expect(page.nativeClick).not.toHaveBeenCalled();
|
|
651
|
+
});
|
|
652
|
+
it('rejects non-checkable targets with a structured error', async () => {
|
|
653
|
+
const page = new ActionPage();
|
|
654
|
+
page.results = [resolveOk, { ok: false, reason: 'not_checkable', tag: 'button' }];
|
|
655
|
+
const err = await page.setChecked('button', true).catch((error) => error);
|
|
656
|
+
expect(err).toBeInstanceOf(TargetError);
|
|
657
|
+
expect(err.code).toBe('not_checkable');
|
|
658
|
+
});
|
|
659
|
+
it('rejects attempts to uncheck a radio button directly', async () => {
|
|
660
|
+
const page = new ActionPage();
|
|
661
|
+
page.results = [resolveOk, { ok: true, checked: true, disabled: false, kind: 'radio' }];
|
|
662
|
+
const err = await page.setChecked('#radio', false).catch((error) => error);
|
|
663
|
+
expect(err).toBeInstanceOf(TargetError);
|
|
664
|
+
expect(err.code).toBe('not_checkable');
|
|
665
|
+
expect(err.hint).toContain('Select another radio');
|
|
666
|
+
});
|
|
667
|
+
it('treats ARIA radio controls like radio buttons', async () => {
|
|
668
|
+
const page = new ActionPage();
|
|
669
|
+
page.results = [resolveOk, { ok: true, checked: true, disabled: false, kind: 'menuitemradio' }];
|
|
670
|
+
const err = await page.setChecked('[role="menuitemradio"]', false).catch((error) => error);
|
|
671
|
+
expect(err).toBeInstanceOf(TargetError);
|
|
672
|
+
expect(err.code).toBe('not_checkable');
|
|
673
|
+
expect(err.hint).toContain('Select another radio');
|
|
674
|
+
});
|
|
675
|
+
it('uploads files through setFileInput using a temporary marker selector', async () => {
|
|
676
|
+
const page = new ActionPage();
|
|
677
|
+
page.setFileInput = vi.fn().mockResolvedValue(undefined);
|
|
678
|
+
page.results = [
|
|
679
|
+
resolveOk,
|
|
680
|
+
['receipt.pdf'],
|
|
681
|
+
];
|
|
682
|
+
page.withArgsResults = [{ ok: true, multiple: false, accept: 'application/pdf' }, undefined];
|
|
683
|
+
await expect(page.uploadFiles('#file', ['/tmp/receipt.pdf'])).resolves.toEqual({
|
|
684
|
+
uploaded: true,
|
|
685
|
+
files: 1,
|
|
686
|
+
file_names: ['receipt.pdf'],
|
|
687
|
+
target: '#file',
|
|
688
|
+
matches_n: 1,
|
|
689
|
+
match_level: 'exact',
|
|
690
|
+
multiple: false,
|
|
691
|
+
accept: 'application/pdf',
|
|
692
|
+
});
|
|
693
|
+
expect(page.setFileInput).toHaveBeenCalledWith(['/tmp/receipt.pdf'], expect.stringMatching(/data-opencli-upload-target/));
|
|
694
|
+
expect(page.withArgs.at(0)).toMatchObject({ markerAttr: 'data-opencli-upload-target' });
|
|
695
|
+
expect(page.withArgs.at(-1)).toMatchObject({ markerAttr: 'data-opencli-upload-target' });
|
|
696
|
+
});
|
|
697
|
+
it('rejects non-file-input upload targets with a structured error', async () => {
|
|
698
|
+
const page = new ActionPage();
|
|
699
|
+
page.setFileInput = vi.fn().mockResolvedValue(undefined);
|
|
700
|
+
page.results = [resolveOk];
|
|
701
|
+
page.withArgsResults = [{ ok: false, tag: 'button', type: '' }];
|
|
702
|
+
const err = await page.uploadFiles('button', ['/tmp/receipt.pdf']).catch((error) => error);
|
|
703
|
+
expect(err).toBeInstanceOf(TargetError);
|
|
704
|
+
expect(err.code).toBe('not_file_input');
|
|
705
|
+
expect(page.setFileInput).not.toHaveBeenCalled();
|
|
706
|
+
});
|
|
707
|
+
it('rejects multiple files for a single-file input before mutating files', async () => {
|
|
708
|
+
const page = new ActionPage();
|
|
709
|
+
page.setFileInput = vi.fn().mockResolvedValue(undefined);
|
|
710
|
+
page.results = [resolveOk];
|
|
711
|
+
page.withArgsResults = [{ ok: true, multiple: false, accept: '' }, undefined];
|
|
712
|
+
const err = await page.uploadFiles('#file', ['/tmp/a.pdf', '/tmp/b.pdf']).catch((error) => error);
|
|
713
|
+
expect(err).toBeInstanceOf(TargetError);
|
|
714
|
+
expect(err.code).toBe('not_file_input');
|
|
715
|
+
expect(page.setFileInput).not.toHaveBeenCalled();
|
|
716
|
+
});
|
|
717
|
+
it('drags between two resolved element centers via native CDP mouse events', async () => {
|
|
718
|
+
const page = new ActionPage();
|
|
719
|
+
page.cdp = vi.fn().mockResolvedValue({});
|
|
720
|
+
page.results = [
|
|
721
|
+
resolveOk,
|
|
722
|
+
{ x: 10, y: 20, w: 30, h: 20, visible: true },
|
|
723
|
+
{ ok: true, matches_n: 2, match_level: 'stable' },
|
|
724
|
+
{
|
|
725
|
+
source: { x: 10, y: 20, w: 30, h: 20, visible: true },
|
|
726
|
+
target: { x: 110, y: 120, w: 40, h: 30, visible: true },
|
|
727
|
+
},
|
|
728
|
+
];
|
|
729
|
+
await expect(page.drag('#card', '.lane', { to: { nth: 1 } })).resolves.toEqual({
|
|
730
|
+
dragged: true,
|
|
731
|
+
source: '#card',
|
|
732
|
+
target: '.lane',
|
|
733
|
+
source_matches_n: 1,
|
|
734
|
+
target_matches_n: 2,
|
|
735
|
+
source_match_level: 'exact',
|
|
736
|
+
target_match_level: 'stable',
|
|
737
|
+
});
|
|
738
|
+
expect(page.cdp).toHaveBeenCalledWith('Input.dispatchMouseEvent', { type: 'mouseMoved', x: 10, y: 20 });
|
|
739
|
+
expect(page.cdp).toHaveBeenCalledWith('Input.dispatchMouseEvent', { type: 'mousePressed', x: 10, y: 20, button: 'left', clickCount: 1 });
|
|
740
|
+
expect(page.cdp).toHaveBeenCalledWith('Input.dispatchMouseEvent', { type: 'mouseMoved', x: 60, y: 70, button: 'left', buttons: 1 });
|
|
741
|
+
expect(page.cdp).toHaveBeenCalledWith('Input.dispatchMouseEvent', { type: 'mouseMoved', x: 110, y: 120, button: 'left', buttons: 1 });
|
|
742
|
+
expect(page.cdp).toHaveBeenCalledWith('Input.dispatchMouseEvent', { type: 'mouseReleased', x: 110, y: 120, button: 'left', clickCount: 1 });
|
|
743
|
+
});
|
|
228
744
|
it('presses key chords through native CDP key events when available', async () => {
|
|
229
745
|
const page = new ActionPage();
|
|
230
746
|
page.nativeKeyPress = vi.fn().mockResolvedValue(undefined);
|
|
@@ -32,7 +32,7 @@ export class BrowserBridge {
|
|
|
32
32
|
try {
|
|
33
33
|
const contextId = opts.contextId ?? resolveProfileContextId();
|
|
34
34
|
await this._ensureDaemon(opts.timeout, contextId);
|
|
35
|
-
this._page = new Page(opts.workspace, opts.idleTimeout, contextId);
|
|
35
|
+
this._page = new Page(opts.workspace, opts.idleTimeout, contextId, opts.windowMode);
|
|
36
36
|
this._state = 'connected';
|
|
37
37
|
return this._page;
|
|
38
38
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|