@jackwener/opencli 1.7.13 → 1.7.15
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/cli-manifest.json +326 -44
- package/clis/bilibili/subtitle.js +1 -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/reddit/search.js +1 -1
- package/clis/reddit/subreddit.js +1 -1
- package/clis/reddit/user-comments.js +1 -1
- package/clis/reddit/user-posts.js +1 -1
- package/clis/reddit/user.js +1 -1
- package/clis/twitter/article.js +2 -1
- package/clis/twitter/bookmark-folder.js +189 -0
- package/clis/twitter/bookmark-folder.test.js +334 -0
- package/clis/twitter/bookmark-folders.js +117 -0
- package/clis/twitter/bookmark-folders.test.js +150 -0
- package/clis/twitter/bookmark.js +15 -6
- package/clis/twitter/bookmark.test.js +74 -0
- package/clis/twitter/bookmarks.js +7 -5
- package/clis/twitter/delete.js +11 -35
- package/clis/twitter/delete.test.js +21 -9
- package/clis/twitter/download.js +5 -5
- package/clis/twitter/followers.js +9 -3
- package/clis/twitter/following.js +11 -5
- 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 +8 -6
- package/clis/twitter/list-add.js +4 -4
- package/clis/twitter/list-remove.js +4 -4
- package/clis/twitter/list-tweets.js +6 -4
- package/clis/twitter/lists.js +3 -3
- package/clis/twitter/notifications.js +2 -2
- package/clis/twitter/profile.js +4 -3
- package/clis/twitter/quote.js +167 -0
- package/clis/twitter/quote.test.js +194 -0
- package/clis/twitter/reply.js +24 -178
- package/clis/twitter/reply.test.js +29 -11
- package/clis/twitter/retweet.js +94 -0
- package/clis/twitter/retweet.test.js +73 -0
- package/clis/twitter/search.js +175 -23
- package/clis/twitter/search.test.js +266 -1
- package/clis/twitter/shared.js +81 -0
- package/clis/twitter/shared.test.js +134 -1
- package/clis/twitter/thread.js +6 -4
- package/clis/twitter/timeline.js +8 -6
- package/clis/twitter/tweets.js +5 -3
- package/clis/twitter/unbookmark.js +13 -6
- package/clis/twitter/unbookmark.test.js +73 -0
- package/clis/twitter/unlike.js +80 -0
- package/clis/twitter/unlike.test.js +75 -0
- package/clis/twitter/unretweet.js +94 -0
- package/clis/twitter/unretweet.test.js +73 -0
- package/clis/twitter/utils.js +286 -0
- package/clis/twitter/utils.test.js +169 -0
- 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.js +47 -45
- 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.js +5 -0
- package/dist/src/browser/cdp.test.js +1 -0
- package/dist/src/browser/daemon-client.d.ts +3 -1
- 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 +2 -1
- package/dist/src/browser/page.js +13 -0
- 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/browser.test.js +18 -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 +560 -58
- package/dist/src/cli.test.js +689 -1
- package/dist/src/commanderAdapter.js +23 -4
- package/dist/src/help.d.ts +36 -0
- package/dist/src/help.js +301 -5
- package/dist/src/types.d.ts +82 -0
- package/package.json +1 -1
- package/scripts/typed-error-lint-baseline.json +18 -18
package/dist/src/cli.test.js
CHANGED
|
@@ -223,6 +223,7 @@ describe('createProgram root help descriptions', () => {
|
|
|
223
223
|
strategy: Strategy.PUBLIC,
|
|
224
224
|
browser: false,
|
|
225
225
|
args: [{ name: 'limit', type: 'int', default: 20, help: 'Number of videos' }],
|
|
226
|
+
columns: ['title', 'url'],
|
|
226
227
|
});
|
|
227
228
|
const program = createProgram('', '');
|
|
228
229
|
const site = program.commands.find(cmd => cmd.name() === 'bilibili');
|
|
@@ -235,10 +236,13 @@ describe('createProgram root help descriptions', () => {
|
|
|
235
236
|
name: 'hot',
|
|
236
237
|
access: 'read',
|
|
237
238
|
description: 'Bilibili hot videos',
|
|
239
|
+
browser: false,
|
|
238
240
|
example: 'opencli bilibili hot -f yaml',
|
|
239
|
-
|
|
241
|
+
command_options: [{ name: 'limit', type: 'int', default: 20 }],
|
|
242
|
+
columns: ['title', 'url'],
|
|
240
243
|
},
|
|
241
244
|
]);
|
|
245
|
+
expect(data.commands[0]).not.toHaveProperty('args');
|
|
242
246
|
}
|
|
243
247
|
finally {
|
|
244
248
|
process.argv = argv;
|
|
@@ -247,6 +251,212 @@ describe('createProgram root help descriptions', () => {
|
|
|
247
251
|
registry.set(key, value);
|
|
248
252
|
}
|
|
249
253
|
});
|
|
254
|
+
it('renders per-site text help without per-command common option noise', () => {
|
|
255
|
+
const registry = getRegistry();
|
|
256
|
+
const snapshot = new Map(registry);
|
|
257
|
+
registry.clear();
|
|
258
|
+
try {
|
|
259
|
+
cli({
|
|
260
|
+
site: 'bilibili',
|
|
261
|
+
name: 'hot',
|
|
262
|
+
access: 'read',
|
|
263
|
+
description: 'Bilibili hot videos',
|
|
264
|
+
strategy: Strategy.PUBLIC,
|
|
265
|
+
browser: false,
|
|
266
|
+
args: [{ name: 'limit', type: 'int', default: 20, help: 'Number of videos' }],
|
|
267
|
+
});
|
|
268
|
+
cli({
|
|
269
|
+
site: 'bilibili',
|
|
270
|
+
name: 'video',
|
|
271
|
+
access: 'read',
|
|
272
|
+
description: 'Read one video',
|
|
273
|
+
domain: 'www.bilibili.com',
|
|
274
|
+
strategy: Strategy.PUBLIC,
|
|
275
|
+
browser: true,
|
|
276
|
+
args: [{ name: 'bvid', positional: true, required: true, help: 'Video id' }],
|
|
277
|
+
});
|
|
278
|
+
const program = createProgram('', '');
|
|
279
|
+
const site = program.commands.find(cmd => cmd.name() === 'bilibili');
|
|
280
|
+
expect(site).toBeTruthy();
|
|
281
|
+
const help = site.helpInformation();
|
|
282
|
+
expect(help).toContain('hot [options] [read] Bilibili hot videos');
|
|
283
|
+
expect(help).toContain('video <bvid> [read] Read one video');
|
|
284
|
+
expect(help).toContain('hot [options]');
|
|
285
|
+
expect(help).not.toContain('video <bvid> [options]');
|
|
286
|
+
expect(help).not.toContain('\nOptions:');
|
|
287
|
+
expect(help).toContain('Common options:');
|
|
288
|
+
expect(help).toContain('-f, --format <fmt>');
|
|
289
|
+
expect(help).toContain('--trace <mode>');
|
|
290
|
+
expect(help).toContain('get all command args/options in one structured response');
|
|
291
|
+
}
|
|
292
|
+
finally {
|
|
293
|
+
registry.clear();
|
|
294
|
+
for (const [key, value] of snapshot)
|
|
295
|
+
registry.set(key, value);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
it('separates command args from common options in structured help', () => {
|
|
299
|
+
const registry = getRegistry();
|
|
300
|
+
const snapshot = new Map(registry);
|
|
301
|
+
const argv = process.argv;
|
|
302
|
+
registry.clear();
|
|
303
|
+
try {
|
|
304
|
+
cli({
|
|
305
|
+
site: 'bilibili',
|
|
306
|
+
name: 'video',
|
|
307
|
+
access: 'read',
|
|
308
|
+
description: 'Read one video',
|
|
309
|
+
strategy: Strategy.PUBLIC,
|
|
310
|
+
domain: 'www.bilibili.com',
|
|
311
|
+
browser: true,
|
|
312
|
+
args: [
|
|
313
|
+
{ name: 'bvid', positional: true, required: true, help: 'Video id' },
|
|
314
|
+
{ name: 'with-comments', type: 'boolean', default: false, help: 'Include comments' },
|
|
315
|
+
],
|
|
316
|
+
columns: ['title', 'url'],
|
|
317
|
+
});
|
|
318
|
+
const program = createProgram('', '');
|
|
319
|
+
const site = program.commands.find(cmd => cmd.name() === 'bilibili');
|
|
320
|
+
const command = site.commands.find(cmd => cmd.name() === 'video');
|
|
321
|
+
expect(command).toBeTruthy();
|
|
322
|
+
process.argv = ['node', 'opencli', 'bilibili', 'video', '--help', '-f', 'yaml'];
|
|
323
|
+
const data = yaml.load(command.helpInformation());
|
|
324
|
+
expect(data.usage).toBe('opencli bilibili video <bvid> [options]');
|
|
325
|
+
expect(data.browser).toBe(true);
|
|
326
|
+
expect(data.domain).toBe('www.bilibili.com');
|
|
327
|
+
expect(data.positionals).toMatchObject([{ name: 'bvid', positional: true, required: true }]);
|
|
328
|
+
expect(data.command_options).toMatchObject([{ name: 'with-comments', default: false }]);
|
|
329
|
+
expect(data.common_options.map((option) => option.name)).toEqual(['format', 'trace', 'verbose', 'help']);
|
|
330
|
+
expect(data.columns).toEqual(['title', 'url']);
|
|
331
|
+
expect(data).not.toHaveProperty('args');
|
|
332
|
+
}
|
|
333
|
+
finally {
|
|
334
|
+
process.argv = argv;
|
|
335
|
+
registry.clear();
|
|
336
|
+
for (const [key, value] of snapshot)
|
|
337
|
+
registry.set(key, value);
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
it('renders browser namespace structured help from Commander metadata', () => {
|
|
341
|
+
const argv = process.argv;
|
|
342
|
+
try {
|
|
343
|
+
const program = createProgram('', '');
|
|
344
|
+
const browser = program.commands.find(cmd => cmd.name() === 'browser');
|
|
345
|
+
expect(browser).toBeTruthy();
|
|
346
|
+
process.argv = ['node', 'opencli', 'browser', '--help', '-f', 'yaml'];
|
|
347
|
+
const data = yaml.load(browser.helpInformation());
|
|
348
|
+
expect(data.namespace).toBe('browser');
|
|
349
|
+
expect(data.command).toBe('opencli browser');
|
|
350
|
+
expect(data.description).toBe('Browser control — navigate, click, type, extract, wait (no LLM needed)');
|
|
351
|
+
expect(data.command_count).toBeGreaterThan(20);
|
|
352
|
+
expect(data.namespace_options).toMatchObject([
|
|
353
|
+
{
|
|
354
|
+
name: 'workspace',
|
|
355
|
+
flags: '--workspace <name>',
|
|
356
|
+
takes_value: 'required',
|
|
357
|
+
},
|
|
358
|
+
]);
|
|
359
|
+
expect(data.global_options).toEqual(expect.arrayContaining([
|
|
360
|
+
expect.objectContaining({
|
|
361
|
+
name: 'version',
|
|
362
|
+
flags: '-V, --version',
|
|
363
|
+
}),
|
|
364
|
+
expect.objectContaining({
|
|
365
|
+
name: 'profile',
|
|
366
|
+
flags: '--profile <name>',
|
|
367
|
+
takes_value: 'required',
|
|
368
|
+
}),
|
|
369
|
+
]));
|
|
370
|
+
const click = data.commands.find((cmd) => cmd.name === 'click');
|
|
371
|
+
expect(click).toMatchObject({
|
|
372
|
+
command: 'opencli browser click',
|
|
373
|
+
usage: 'opencli browser click [target] [options]',
|
|
374
|
+
positionals: [{ name: 'target' }],
|
|
375
|
+
});
|
|
376
|
+
expect(click.command_options.map((option) => option.name)).toEqual(['role', 'name', 'label', 'text', 'testid', 'nth', 'tab']);
|
|
377
|
+
const tabList = data.commands.find((cmd) => cmd.name === 'tab list');
|
|
378
|
+
expect(tabList).toMatchObject({
|
|
379
|
+
command: 'opencli browser tab list',
|
|
380
|
+
usage: 'opencli browser tab list [options]',
|
|
381
|
+
command_options: [],
|
|
382
|
+
});
|
|
383
|
+
const getText = data.commands.find((cmd) => cmd.name === 'get text');
|
|
384
|
+
expect(getText).toMatchObject({
|
|
385
|
+
command: 'opencli browser get text',
|
|
386
|
+
positionals: [{ name: 'target' }],
|
|
387
|
+
});
|
|
388
|
+
expect(data.structured_help).toMatchObject({
|
|
389
|
+
formats: ['yaml', 'json'],
|
|
390
|
+
usage: 'opencli browser --help -f yaml',
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
finally {
|
|
394
|
+
process.argv = argv;
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
it('renders nested browser parent structured help for a subtree', () => {
|
|
398
|
+
const argv = process.argv;
|
|
399
|
+
try {
|
|
400
|
+
const program = createProgram('', '');
|
|
401
|
+
const browser = program.commands.find(cmd => cmd.name() === 'browser');
|
|
402
|
+
const tab = browser.commands.find(cmd => cmd.name() === 'tab');
|
|
403
|
+
expect(tab).toBeTruthy();
|
|
404
|
+
process.argv = ['node', 'opencli', 'browser', 'tab', '--help', '-f', 'yaml'];
|
|
405
|
+
const data = yaml.load(tab.helpInformation());
|
|
406
|
+
expect(data).toMatchObject({
|
|
407
|
+
namespace: 'browser',
|
|
408
|
+
group: 'tab',
|
|
409
|
+
command: 'opencli browser tab',
|
|
410
|
+
usage: 'opencli browser tab <command> [args] [options]',
|
|
411
|
+
command_count: 4,
|
|
412
|
+
});
|
|
413
|
+
expect(data.commands.map((cmd) => cmd.name)).toEqual([
|
|
414
|
+
'tab close',
|
|
415
|
+
'tab list',
|
|
416
|
+
'tab new',
|
|
417
|
+
'tab select',
|
|
418
|
+
]);
|
|
419
|
+
expect(data.commands.find((cmd) => cmd.name === 'tab close')).toMatchObject({
|
|
420
|
+
command: 'opencli browser tab close',
|
|
421
|
+
usage: 'opencli browser tab close [targetId] [options]',
|
|
422
|
+
positionals: [{ name: 'targetId', help: 'Target tab/page identity returned by "browser open", "browser tab new", or "browser tab list"' }],
|
|
423
|
+
});
|
|
424
|
+
expect(data.namespace_options.map((option) => option.name)).toEqual(['workspace']);
|
|
425
|
+
expect(data.structured_help).toMatchObject({
|
|
426
|
+
usage: 'opencli browser tab --help -f yaml',
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
finally {
|
|
430
|
+
process.argv = argv;
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
it('renders browser command structured help without needing the full namespace dump', () => {
|
|
434
|
+
const argv = process.argv;
|
|
435
|
+
try {
|
|
436
|
+
const program = createProgram('', '');
|
|
437
|
+
const browser = program.commands.find(cmd => cmd.name() === 'browser');
|
|
438
|
+
const click = browser.commands.find(cmd => cmd.name() === 'click');
|
|
439
|
+
expect(click).toBeTruthy();
|
|
440
|
+
process.argv = ['node', 'opencli', 'browser', 'click', '--help', '-f', 'yaml'];
|
|
441
|
+
const data = yaml.load(click.helpInformation());
|
|
442
|
+
expect(data).toMatchObject({
|
|
443
|
+
namespace: 'browser',
|
|
444
|
+
name: 'click',
|
|
445
|
+
command: 'opencli browser click',
|
|
446
|
+
usage: 'opencli browser click [target] [options]',
|
|
447
|
+
positionals: [{ name: 'target' }],
|
|
448
|
+
structured_help: {
|
|
449
|
+
usage: 'opencli browser click --help -f yaml',
|
|
450
|
+
},
|
|
451
|
+
});
|
|
452
|
+
expect(data.command_options.map((option) => option.name)).toEqual(['role', 'name', 'label', 'text', 'testid', 'nth', 'tab']);
|
|
453
|
+
expect(data.namespace_options.map((option) => option.name)).toEqual(['workspace']);
|
|
454
|
+
expect(data.global_options.map((option) => option.name)).toContain('profile');
|
|
455
|
+
}
|
|
456
|
+
finally {
|
|
457
|
+
process.argv = argv;
|
|
458
|
+
}
|
|
459
|
+
});
|
|
250
460
|
});
|
|
251
461
|
describe('resolveBrowserVerifyInvocation', () => {
|
|
252
462
|
it('prefers the built entry declared in package metadata', () => {
|
|
@@ -546,7 +756,16 @@ describe('browser tab targeting commands', () => {
|
|
|
546
756
|
{ index: 0, frameId: 'frame-1', url: 'https://x.example/embed', name: 'x-embed' },
|
|
547
757
|
]),
|
|
548
758
|
evaluateInFrame: vi.fn().mockResolvedValue('inside frame'),
|
|
759
|
+
screenshot: vi.fn().mockResolvedValue('base64-shot'),
|
|
760
|
+
annotatedScreenshot: vi.fn().mockResolvedValue('annotated-base64-shot'),
|
|
549
761
|
readNetworkCapture: vi.fn().mockResolvedValue([]),
|
|
762
|
+
waitForDownload: vi.fn().mockResolvedValue({
|
|
763
|
+
downloaded: true,
|
|
764
|
+
filename: '/tmp/receipt.pdf',
|
|
765
|
+
url: 'https://app.example/receipt.pdf',
|
|
766
|
+
state: 'complete',
|
|
767
|
+
elapsedMs: 10,
|
|
768
|
+
}),
|
|
550
769
|
};
|
|
551
770
|
});
|
|
552
771
|
function lastJsonLog() {
|
|
@@ -585,6 +804,69 @@ describe('browser tab targeting commands', () => {
|
|
|
585
804
|
expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 30, workspace: 'bound:default' });
|
|
586
805
|
expect(browserState.page?.snapshot).toHaveBeenCalled();
|
|
587
806
|
});
|
|
807
|
+
it('passes the opt-in AX source to browser state', async () => {
|
|
808
|
+
const program = createProgram('', '');
|
|
809
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'state', '--source', 'ax']);
|
|
810
|
+
expect(browserState.page?.snapshot).toHaveBeenCalledWith({ viewportExpand: 2000, source: 'ax' });
|
|
811
|
+
});
|
|
812
|
+
it('prints DOM vs AX snapshot metrics without changing default state output', async () => {
|
|
813
|
+
browserState.page = {
|
|
814
|
+
...browserState.page,
|
|
815
|
+
snapshot: vi.fn(async (opts) => {
|
|
816
|
+
if (opts?.source === 'ax') {
|
|
817
|
+
return 'source: ax\n---\n[1]button "Save"\nframe "https://app.example/embed":\n [2]button "Frame Save"\n---\ninteractive: 2';
|
|
818
|
+
}
|
|
819
|
+
return 'URL: https://app.example\n[1] button "Save"';
|
|
820
|
+
}),
|
|
821
|
+
};
|
|
822
|
+
const program = createProgram('', '');
|
|
823
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'state', '--compare-sources']);
|
|
824
|
+
expect(browserState.page?.snapshot).toHaveBeenCalledWith({ viewportExpand: 2000, source: 'dom' });
|
|
825
|
+
expect(browserState.page?.snapshot).toHaveBeenCalledWith({ viewportExpand: 2000, source: 'ax' });
|
|
826
|
+
const out = lastJsonLog();
|
|
827
|
+
expect(out.url).toBe('https://one.example');
|
|
828
|
+
expect(out.sources.dom).toMatchObject({ ok: true, refs: 1, frame_sections: 0 });
|
|
829
|
+
expect(out.sources.ax).toMatchObject({ ok: true, refs: 2, frame_sections: 1, interactive: 2 });
|
|
830
|
+
});
|
|
831
|
+
it('keeps compare-sources usable when one observation backend fails', async () => {
|
|
832
|
+
browserState.page = {
|
|
833
|
+
...browserState.page,
|
|
834
|
+
snapshot: vi.fn(async (opts) => {
|
|
835
|
+
if (opts?.source === 'ax')
|
|
836
|
+
throw new Error('AX unavailable');
|
|
837
|
+
return '[1] button "Save"';
|
|
838
|
+
}),
|
|
839
|
+
};
|
|
840
|
+
const program = createProgram('', '');
|
|
841
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'state', '--compare-sources']);
|
|
842
|
+
const out = lastJsonLog();
|
|
843
|
+
expect(out.sources.dom).toMatchObject({ ok: true, refs: 1 });
|
|
844
|
+
expect(out.sources.ax).toMatchObject({
|
|
845
|
+
ok: false,
|
|
846
|
+
error: { message: 'AX unavailable' },
|
|
847
|
+
});
|
|
848
|
+
});
|
|
849
|
+
it('rejects unknown browser state sources before touching the page', async () => {
|
|
850
|
+
const program = createProgram('', '');
|
|
851
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'state', '--source', 'magic']);
|
|
852
|
+
expect(browserState.page?.snapshot).not.toHaveBeenCalled();
|
|
853
|
+
const out = lastJsonLog();
|
|
854
|
+
expect(out.error.code).toBe('invalid_source');
|
|
855
|
+
expect(process.exitCode).toBeDefined();
|
|
856
|
+
});
|
|
857
|
+
it('captures annotated screenshots through the visual ref overlay path', async () => {
|
|
858
|
+
const program = createProgram('', '');
|
|
859
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'screenshot', '--annotate']);
|
|
860
|
+
expect(browserState.page?.annotatedScreenshot).toHaveBeenCalledWith({
|
|
861
|
+
fullPage: false,
|
|
862
|
+
annotate: true,
|
|
863
|
+
width: undefined,
|
|
864
|
+
height: undefined,
|
|
865
|
+
format: 'png',
|
|
866
|
+
});
|
|
867
|
+
expect(browserState.page?.screenshot).not.toHaveBeenCalled();
|
|
868
|
+
expect(consoleLogSpy).toHaveBeenLastCalledWith('annotated-base64-shot');
|
|
869
|
+
});
|
|
588
870
|
it('blocks history navigation on bound workspaces unless explicitly allowed', async () => {
|
|
589
871
|
browserState.page = {
|
|
590
872
|
...browserState.page,
|
|
@@ -946,6 +1228,55 @@ describe('browser tab targeting commands', () => {
|
|
|
946
1228
|
expect(bufferReads).toBe(2);
|
|
947
1229
|
expect(out.matched.url).toBe('https://target.example/api/target');
|
|
948
1230
|
});
|
|
1231
|
+
it('browser wait download delegates to the Browser Bridge download observer', async () => {
|
|
1232
|
+
browserState.page = {
|
|
1233
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
1234
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
1235
|
+
waitForDownload: vi.fn().mockResolvedValue({
|
|
1236
|
+
downloaded: true,
|
|
1237
|
+
filename: '/tmp/receipt.pdf',
|
|
1238
|
+
url: 'https://app.example/receipt.pdf',
|
|
1239
|
+
state: 'complete',
|
|
1240
|
+
elapsedMs: 10,
|
|
1241
|
+
}),
|
|
1242
|
+
setActivePage: vi.fn(),
|
|
1243
|
+
getActivePage: vi.fn().mockReturnValue('tab-1'),
|
|
1244
|
+
getCurrentUrl: vi.fn().mockResolvedValue('https://target.example'),
|
|
1245
|
+
tabs: vi.fn().mockResolvedValue([{ index: 0, page: 'tab-1', url: 'https://target.example', title: 'Target', active: true }]),
|
|
1246
|
+
};
|
|
1247
|
+
const program = createProgram('', '');
|
|
1248
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'wait', 'download', 'receipt', '--timeout', '900']);
|
|
1249
|
+
expect(browserState.page?.waitForDownload).toHaveBeenCalledWith('receipt', 900);
|
|
1250
|
+
expect(lastJsonLog()).toEqual({
|
|
1251
|
+
downloaded: true,
|
|
1252
|
+
filename: '/tmp/receipt.pdf',
|
|
1253
|
+
url: 'https://app.example/receipt.pdf',
|
|
1254
|
+
state: 'complete',
|
|
1255
|
+
elapsedMs: 10,
|
|
1256
|
+
});
|
|
1257
|
+
});
|
|
1258
|
+
it('browser wait download reports an error envelope when no matching download completes', async () => {
|
|
1259
|
+
browserState.page = {
|
|
1260
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
1261
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
1262
|
+
waitForDownload: vi.fn().mockResolvedValue({
|
|
1263
|
+
downloaded: false,
|
|
1264
|
+
state: 'interrupted',
|
|
1265
|
+
error: 'No download matched "receipt" within 900ms',
|
|
1266
|
+
elapsedMs: 900,
|
|
1267
|
+
}),
|
|
1268
|
+
setActivePage: vi.fn(),
|
|
1269
|
+
getActivePage: vi.fn().mockReturnValue('tab-1'),
|
|
1270
|
+
getCurrentUrl: vi.fn().mockResolvedValue('https://target.example'),
|
|
1271
|
+
tabs: vi.fn().mockResolvedValue([{ index: 0, page: 'tab-1', url: 'https://target.example', title: 'Target', active: true }]),
|
|
1272
|
+
};
|
|
1273
|
+
const program = createProgram('', '');
|
|
1274
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'wait', 'download', 'receipt', '--timeout', '900']);
|
|
1275
|
+
const out = lastJsonLog();
|
|
1276
|
+
expect(out.error.code).toBe('download_not_seen');
|
|
1277
|
+
expect(out.download.elapsedMs).toBe(900);
|
|
1278
|
+
expect(process.exitCode).toBeDefined();
|
|
1279
|
+
});
|
|
949
1280
|
});
|
|
950
1281
|
describe('browser network command', () => {
|
|
951
1282
|
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
@@ -1597,6 +1928,25 @@ describe('browser find command', () => {
|
|
|
1597
1928
|
expect(out.entries[1].ref).toBe(17);
|
|
1598
1929
|
expect(process.exitCode).toBeUndefined();
|
|
1599
1930
|
});
|
|
1931
|
+
it('finds elements by semantic role/name without requiring CSS', async () => {
|
|
1932
|
+
browserState.page.evaluate.mockResolvedValueOnce({
|
|
1933
|
+
matches_n: 1,
|
|
1934
|
+
entries: [
|
|
1935
|
+
{ nth: 0, ref: 9, tag: 'button', role: 'button', text: 'Save', attrs: {}, visible: true },
|
|
1936
|
+
],
|
|
1937
|
+
});
|
|
1938
|
+
const program = createProgram('', '');
|
|
1939
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'find', '--role', 'button', '--name', 'Save']);
|
|
1940
|
+
const js = browserState.page.evaluate.mock.calls[0][0];
|
|
1941
|
+
expect(js).toContain('CRITERIA');
|
|
1942
|
+
expect(js).toContain('function accessibleName');
|
|
1943
|
+
expect(lastJsonLog()).toEqual({
|
|
1944
|
+
matches_n: 1,
|
|
1945
|
+
entries: [
|
|
1946
|
+
{ nth: 0, ref: 9, tag: 'button', role: 'button', text: 'Save', attrs: {}, visible: true },
|
|
1947
|
+
],
|
|
1948
|
+
});
|
|
1949
|
+
});
|
|
1600
1950
|
it('forwards --limit / --text-max into the generated JS', async () => {
|
|
1601
1951
|
browserState.page.evaluate.mockResolvedValueOnce({ matches_n: 0, entries: [] });
|
|
1602
1952
|
const program = createProgram('', '');
|
|
@@ -1652,6 +2002,40 @@ describe('browser get text/value/attributes commands', () => {
|
|
|
1652
2002
|
await program.parseAsync(['node', 'opencli', 'browser', 'get', 'text', '7']);
|
|
1653
2003
|
expect(lastJsonLog()).toEqual({ value: 'Hello world', matches_n: 1, match_level: 'exact' });
|
|
1654
2004
|
});
|
|
2005
|
+
it('resolves a semantic locator to a ref before get text', async () => {
|
|
2006
|
+
const evalMock = browserState.page.evaluate;
|
|
2007
|
+
evalMock.mockResolvedValueOnce({
|
|
2008
|
+
matches_n: 1,
|
|
2009
|
+
entries: [
|
|
2010
|
+
{ nth: 0, ref: 12, tag: 'button', role: 'button', text: 'Save', attrs: {}, visible: true },
|
|
2011
|
+
],
|
|
2012
|
+
});
|
|
2013
|
+
evalMock.mockResolvedValueOnce({ ok: true, matches_n: 1, match_level: 'exact' });
|
|
2014
|
+
evalMock.mockResolvedValueOnce('Save');
|
|
2015
|
+
const program = createProgram('', '');
|
|
2016
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'get', 'text', '--role', 'button', '--name', 'Save']);
|
|
2017
|
+
expect(evalMock.mock.calls[0][0]).toContain('function accessibleName');
|
|
2018
|
+
expect(evalMock.mock.calls[1][0]).toContain('const ref = "12"');
|
|
2019
|
+
expect(lastJsonLog()).toEqual({ value: 'Save', matches_n: 1, match_level: 'exact' });
|
|
2020
|
+
});
|
|
2021
|
+
it('reports total_matches when semantic get reads the first of multiple matches', async () => {
|
|
2022
|
+
const evalMock = browserState.page.evaluate;
|
|
2023
|
+
evalMock.mockResolvedValueOnce({
|
|
2024
|
+
matches_n: 3,
|
|
2025
|
+
entries: [
|
|
2026
|
+
{ nth: 0, ref: 12, tag: 'button', role: 'button', text: 'Save', attrs: {}, visible: true },
|
|
2027
|
+
{ nth: 1, ref: 13, tag: 'button', role: 'button', text: 'Save draft', attrs: {}, visible: true },
|
|
2028
|
+
{ nth: 2, ref: 14, tag: 'button', role: 'button', text: 'Save copy', attrs: {}, visible: true },
|
|
2029
|
+
],
|
|
2030
|
+
});
|
|
2031
|
+
evalMock.mockResolvedValueOnce({ ok: true, matches_n: 1, match_level: 'exact' });
|
|
2032
|
+
evalMock.mockResolvedValueOnce('Save');
|
|
2033
|
+
const program = createProgram('', '');
|
|
2034
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'get', 'text', '--role', 'button', '--name', 'Save']);
|
|
2035
|
+
expect(evalMock.mock.calls[0][0]).toContain('const LIMIT = 6');
|
|
2036
|
+
expect(evalMock.mock.calls[1][0]).toContain('const ref = "12"');
|
|
2037
|
+
expect(lastJsonLog()).toEqual({ value: 'Save', matches_n: 1, match_level: 'exact', total_matches: 3 });
|
|
2038
|
+
});
|
|
1655
2039
|
it('reports matches_n on multi-match CSS (read path: first match wins)', async () => {
|
|
1656
2040
|
const evalMock = browserState.page.evaluate;
|
|
1657
2041
|
evalMock.mockResolvedValueOnce({ ok: true, matches_n: 3, match_level: 'exact' });
|
|
@@ -1707,6 +2091,28 @@ describe('browser click/type commands', () => {
|
|
|
1707
2091
|
const { lastJsonLog } = installSelectorFirstTestHarness('click-type', () => ({
|
|
1708
2092
|
evaluate: vi.fn().mockResolvedValue(false),
|
|
1709
2093
|
click: vi.fn().mockResolvedValue({ matches_n: 1, match_level: 'exact' }),
|
|
2094
|
+
dblClick: vi.fn().mockResolvedValue({ matches_n: 1, match_level: 'exact' }),
|
|
2095
|
+
hover: vi.fn().mockResolvedValue({ matches_n: 1, match_level: 'exact' }),
|
|
2096
|
+
focus: vi.fn().mockResolvedValue({ focused: true, matches_n: 1, match_level: 'exact' }),
|
|
2097
|
+
setChecked: vi.fn().mockResolvedValue({ checked: true, changed: true, matches_n: 1, match_level: 'exact', kind: 'checkbox' }),
|
|
2098
|
+
uploadFiles: vi.fn().mockResolvedValue({
|
|
2099
|
+
uploaded: true,
|
|
2100
|
+
files: 1,
|
|
2101
|
+
file_names: ['receipt.pdf'],
|
|
2102
|
+
target: '#file',
|
|
2103
|
+
matches_n: 1,
|
|
2104
|
+
match_level: 'exact',
|
|
2105
|
+
multiple: false,
|
|
2106
|
+
}),
|
|
2107
|
+
drag: vi.fn().mockResolvedValue({
|
|
2108
|
+
dragged: true,
|
|
2109
|
+
source: '#card',
|
|
2110
|
+
target: '#lane',
|
|
2111
|
+
source_matches_n: 1,
|
|
2112
|
+
target_matches_n: 1,
|
|
2113
|
+
source_match_level: 'exact',
|
|
2114
|
+
target_match_level: 'exact',
|
|
2115
|
+
}),
|
|
1710
2116
|
typeText: vi.fn().mockResolvedValue({ matches_n: 1, match_level: 'exact' }),
|
|
1711
2117
|
fillText: vi.fn().mockResolvedValue({
|
|
1712
2118
|
filled: true,
|
|
@@ -1727,6 +2133,163 @@ describe('browser click/type commands', () => {
|
|
|
1727
2133
|
expect(browserState.page.click).toHaveBeenCalledWith('#save', {});
|
|
1728
2134
|
expect(lastJsonLog()).toEqual({ clicked: true, target: '#save', matches_n: 1, match_level: 'exact' });
|
|
1729
2135
|
});
|
|
2136
|
+
it('clicks a unique semantic locator without a prior state call', async () => {
|
|
2137
|
+
browserState.page.evaluate.mockResolvedValueOnce({
|
|
2138
|
+
matches_n: 1,
|
|
2139
|
+
entries: [
|
|
2140
|
+
{ nth: 0, ref: 23, tag: 'button', role: 'button', text: 'Submit', attrs: {}, visible: true },
|
|
2141
|
+
],
|
|
2142
|
+
});
|
|
2143
|
+
browserState.page.click.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
|
|
2144
|
+
const program = createProgram('', '');
|
|
2145
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'click', '--role', 'button', '--name', 'Submit']);
|
|
2146
|
+
expect(browserState.page.click).toHaveBeenCalledWith('23', {});
|
|
2147
|
+
expect(lastJsonLog()).toEqual({ clicked: true, target: '23', matches_n: 1, match_level: 'exact' });
|
|
2148
|
+
});
|
|
2149
|
+
it('rejects ambiguous semantic locators before write actions', async () => {
|
|
2150
|
+
browserState.page.evaluate.mockResolvedValueOnce({
|
|
2151
|
+
matches_n: 2,
|
|
2152
|
+
entries: [
|
|
2153
|
+
{ nth: 0, ref: 1, tag: 'button', role: 'button', text: 'Save', attrs: {}, visible: true },
|
|
2154
|
+
{ nth: 1, ref: 2, tag: 'button', role: 'button', text: 'Save draft', attrs: {}, visible: true },
|
|
2155
|
+
],
|
|
2156
|
+
});
|
|
2157
|
+
const program = createProgram('', '');
|
|
2158
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'click', '--role', 'button', '--name', 'Save']);
|
|
2159
|
+
const err = lastJsonLog().error;
|
|
2160
|
+
expect(err.code).toBe('semantic_ambiguous');
|
|
2161
|
+
expect(err.matches_n).toBe(2);
|
|
2162
|
+
expect(browserState.page.click).not.toHaveBeenCalled();
|
|
2163
|
+
expect(process.exitCode).toBeDefined();
|
|
2164
|
+
});
|
|
2165
|
+
it('hover: resolves a semantic locator before moving the mouse', async () => {
|
|
2166
|
+
browserState.page.evaluate.mockResolvedValueOnce({
|
|
2167
|
+
matches_n: 1,
|
|
2168
|
+
entries: [
|
|
2169
|
+
{ nth: 0, ref: 31, tag: 'button', role: 'button', text: 'Settings', attrs: {}, visible: true },
|
|
2170
|
+
],
|
|
2171
|
+
});
|
|
2172
|
+
const program = createProgram('', '');
|
|
2173
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'hover', '--role', 'button', '--name', 'Settings']);
|
|
2174
|
+
expect(browserState.page.hover).toHaveBeenCalledWith('31', {});
|
|
2175
|
+
expect(lastJsonLog()).toEqual({ hovered: true, target: '31', matches_n: 1, match_level: 'exact' });
|
|
2176
|
+
});
|
|
2177
|
+
it('check: resolves a semantic locator before setting checked state', async () => {
|
|
2178
|
+
browserState.page.evaluate.mockResolvedValueOnce({
|
|
2179
|
+
matches_n: 1,
|
|
2180
|
+
entries: [
|
|
2181
|
+
{ nth: 0, ref: 32, tag: 'input', role: 'checkbox', text: 'Accept', attrs: {}, visible: true },
|
|
2182
|
+
],
|
|
2183
|
+
});
|
|
2184
|
+
browserState.page.setChecked.mockResolvedValueOnce({ checked: true, changed: false, matches_n: 1, match_level: 'exact', kind: 'checkbox' });
|
|
2185
|
+
const program = createProgram('', '');
|
|
2186
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'check', '--role', 'checkbox', '--name', 'Accept']);
|
|
2187
|
+
expect(browserState.page.setChecked).toHaveBeenCalledWith('32', true, {});
|
|
2188
|
+
expect(lastJsonLog()).toEqual({ checked: true, changed: false, target: '32', matches_n: 1, match_level: 'exact', kind: 'checkbox' });
|
|
2189
|
+
});
|
|
2190
|
+
it('upload: treats the first positional as a file when using semantic locator flags', async () => {
|
|
2191
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-upload-semantic-'));
|
|
2192
|
+
const file = path.join(dir, 'receipt.pdf');
|
|
2193
|
+
fs.writeFileSync(file, 'pdf');
|
|
2194
|
+
browserState.page.evaluate.mockResolvedValueOnce({
|
|
2195
|
+
matches_n: 1,
|
|
2196
|
+
entries: [
|
|
2197
|
+
{ nth: 0, ref: 33, tag: 'input', role: 'button', text: 'Upload receipt', attrs: {}, visible: true },
|
|
2198
|
+
],
|
|
2199
|
+
});
|
|
2200
|
+
browserState.page.uploadFiles.mockResolvedValueOnce({
|
|
2201
|
+
uploaded: true,
|
|
2202
|
+
files: 1,
|
|
2203
|
+
file_names: ['receipt.pdf'],
|
|
2204
|
+
target: '33',
|
|
2205
|
+
matches_n: 1,
|
|
2206
|
+
match_level: 'exact',
|
|
2207
|
+
multiple: false,
|
|
2208
|
+
});
|
|
2209
|
+
const program = createProgram('', '');
|
|
2210
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'upload', '--role', 'button', '--name', 'Upload receipt', file]);
|
|
2211
|
+
expect(browserState.page.uploadFiles).toHaveBeenCalledWith('33', [file], {});
|
|
2212
|
+
expect(lastJsonLog()).toMatchObject({ uploaded: true, target: '33', files: 1 });
|
|
2213
|
+
});
|
|
2214
|
+
it('type: treats the first positional as text when using semantic locator flags', async () => {
|
|
2215
|
+
browserState.page.evaluate
|
|
2216
|
+
.mockResolvedValueOnce({
|
|
2217
|
+
matches_n: 1,
|
|
2218
|
+
entries: [
|
|
2219
|
+
{ nth: 0, ref: 34, tag: 'input', role: 'textbox', text: '', attrs: {}, visible: true },
|
|
2220
|
+
],
|
|
2221
|
+
})
|
|
2222
|
+
.mockResolvedValueOnce(false);
|
|
2223
|
+
browserState.page.click.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
|
|
2224
|
+
browserState.page.typeText.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
|
|
2225
|
+
const program = createProgram('', '');
|
|
2226
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'type', '--label', 'Email', 'me@example.com']);
|
|
2227
|
+
expect(browserState.page.click).toHaveBeenCalledWith('34', {});
|
|
2228
|
+
expect(browserState.page.typeText).toHaveBeenCalledWith('34', 'me@example.com', {});
|
|
2229
|
+
expect(lastJsonLog()).toMatchObject({ typed: true, target: '34', text: 'me@example.com' });
|
|
2230
|
+
});
|
|
2231
|
+
it('fill: treats the first positional as text when using semantic locator flags', async () => {
|
|
2232
|
+
browserState.page.evaluate.mockResolvedValueOnce({
|
|
2233
|
+
matches_n: 1,
|
|
2234
|
+
entries: [
|
|
2235
|
+
{ nth: 0, ref: 35, tag: 'input', role: 'textbox', text: '', attrs: {}, visible: true },
|
|
2236
|
+
],
|
|
2237
|
+
});
|
|
2238
|
+
browserState.page.fillText.mockResolvedValueOnce({
|
|
2239
|
+
filled: true,
|
|
2240
|
+
verified: true,
|
|
2241
|
+
expected: 'me@example.com',
|
|
2242
|
+
actual: 'me@example.com',
|
|
2243
|
+
length: 14,
|
|
2244
|
+
matches_n: 1,
|
|
2245
|
+
match_level: 'exact',
|
|
2246
|
+
});
|
|
2247
|
+
const program = createProgram('', '');
|
|
2248
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'fill', '--label', 'Email', 'me@example.com']);
|
|
2249
|
+
expect(browserState.page.fillText).toHaveBeenCalledWith('35', 'me@example.com', {});
|
|
2250
|
+
expect(lastJsonLog()).toMatchObject({ filled: true, verified: true, target: '35', text: 'me@example.com' });
|
|
2251
|
+
});
|
|
2252
|
+
it('drag: resolves source and target from prefixed semantic locators', async () => {
|
|
2253
|
+
browserState.page.evaluate
|
|
2254
|
+
.mockResolvedValueOnce({
|
|
2255
|
+
matches_n: 1,
|
|
2256
|
+
entries: [
|
|
2257
|
+
{ nth: 0, ref: 40, tag: 'div', role: 'button', text: 'Card A', attrs: {}, visible: true },
|
|
2258
|
+
],
|
|
2259
|
+
})
|
|
2260
|
+
.mockResolvedValueOnce({
|
|
2261
|
+
matches_n: 1,
|
|
2262
|
+
entries: [
|
|
2263
|
+
{ nth: 0, ref: 41, tag: 'div', role: 'region', text: 'Done', attrs: {}, visible: true },
|
|
2264
|
+
],
|
|
2265
|
+
});
|
|
2266
|
+
browserState.page.drag.mockResolvedValueOnce({
|
|
2267
|
+
dragged: true,
|
|
2268
|
+
source: '40',
|
|
2269
|
+
target: '41',
|
|
2270
|
+
source_matches_n: 1,
|
|
2271
|
+
target_matches_n: 1,
|
|
2272
|
+
source_match_level: 'exact',
|
|
2273
|
+
target_match_level: 'exact',
|
|
2274
|
+
});
|
|
2275
|
+
const program = createProgram('', '');
|
|
2276
|
+
await program.parseAsync([
|
|
2277
|
+
'node',
|
|
2278
|
+
'opencli',
|
|
2279
|
+
'browser',
|
|
2280
|
+
'drag',
|
|
2281
|
+
'--from-role',
|
|
2282
|
+
'button',
|
|
2283
|
+
'--from-name',
|
|
2284
|
+
'Card A',
|
|
2285
|
+
'--to-role',
|
|
2286
|
+
'region',
|
|
2287
|
+
'--to-name',
|
|
2288
|
+
'Done',
|
|
2289
|
+
]);
|
|
2290
|
+
expect(browserState.page.drag).toHaveBeenCalledWith('40', '41', { from: {}, to: {} });
|
|
2291
|
+
expect(lastJsonLog()).toMatchObject({ dragged: true, source: '40', target: '41' });
|
|
2292
|
+
});
|
|
1730
2293
|
it('surfaces match_level=stable when resolver falls back to fingerprint match', async () => {
|
|
1731
2294
|
browserState.page.click.mockResolvedValueOnce({ matches_n: 1, match_level: 'stable' });
|
|
1732
2295
|
const program = createProgram('', '');
|
|
@@ -1773,6 +2336,116 @@ describe('browser click/type commands', () => {
|
|
|
1773
2336
|
expect(browserState.page.click).not.toHaveBeenCalled();
|
|
1774
2337
|
expect(process.exitCode).toBeDefined();
|
|
1775
2338
|
});
|
|
2339
|
+
it('hover: delegates to page.hover and emits a structured envelope', async () => {
|
|
2340
|
+
browserState.page.hover.mockResolvedValueOnce({ matches_n: 2, match_level: 'exact' });
|
|
2341
|
+
const program = createProgram('', '');
|
|
2342
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'hover', '.menu', '--nth', '1']);
|
|
2343
|
+
expect(browserState.page.hover).toHaveBeenCalledWith('.menu', { nth: 1 });
|
|
2344
|
+
expect(lastJsonLog()).toEqual({ hovered: true, target: '.menu', matches_n: 2, match_level: 'exact' });
|
|
2345
|
+
});
|
|
2346
|
+
it('focus: delegates to page.focus and reports whether the element took focus', async () => {
|
|
2347
|
+
browserState.page.focus.mockResolvedValueOnce({ focused: true, matches_n: 1, match_level: 'stable' });
|
|
2348
|
+
const program = createProgram('', '');
|
|
2349
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'focus', '7']);
|
|
2350
|
+
expect(browserState.page.focus).toHaveBeenCalledWith('7', {});
|
|
2351
|
+
expect(lastJsonLog()).toEqual({ focused: true, target: '7', matches_n: 1, match_level: 'stable' });
|
|
2352
|
+
});
|
|
2353
|
+
it('dblclick: delegates to page.dblClick and emits a structured envelope', async () => {
|
|
2354
|
+
browserState.page.dblClick.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
|
|
2355
|
+
const program = createProgram('', '');
|
|
2356
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'dblclick', '#row']);
|
|
2357
|
+
expect(browserState.page.dblClick).toHaveBeenCalledWith('#row', {});
|
|
2358
|
+
expect(lastJsonLog()).toEqual({ dblclicked: true, target: '#row', matches_n: 1, match_level: 'exact' });
|
|
2359
|
+
});
|
|
2360
|
+
it('check: ensures target is checked through page.setChecked', async () => {
|
|
2361
|
+
browserState.page.setChecked.mockResolvedValueOnce({
|
|
2362
|
+
checked: true,
|
|
2363
|
+
changed: true,
|
|
2364
|
+
matches_n: 2,
|
|
2365
|
+
match_level: 'exact',
|
|
2366
|
+
kind: 'checkbox',
|
|
2367
|
+
});
|
|
2368
|
+
const program = createProgram('', '');
|
|
2369
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'check', '.todo', '--nth', '1']);
|
|
2370
|
+
expect(browserState.page.setChecked).toHaveBeenCalledWith('.todo', true, { nth: 1 });
|
|
2371
|
+
expect(lastJsonLog()).toEqual({ checked: true, changed: true, target: '.todo', matches_n: 2, match_level: 'exact', kind: 'checkbox' });
|
|
2372
|
+
});
|
|
2373
|
+
it('uncheck: ensures target is unchecked through page.setChecked', async () => {
|
|
2374
|
+
browserState.page.setChecked.mockResolvedValueOnce({
|
|
2375
|
+
checked: false,
|
|
2376
|
+
changed: false,
|
|
2377
|
+
matches_n: 1,
|
|
2378
|
+
match_level: 'stable',
|
|
2379
|
+
kind: 'checkbox',
|
|
2380
|
+
});
|
|
2381
|
+
const program = createProgram('', '');
|
|
2382
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'uncheck', '#subscribe']);
|
|
2383
|
+
expect(browserState.page.setChecked).toHaveBeenCalledWith('#subscribe', false, {});
|
|
2384
|
+
expect(lastJsonLog()).toEqual({ checked: false, changed: false, target: '#subscribe', matches_n: 1, match_level: 'stable', kind: 'checkbox' });
|
|
2385
|
+
});
|
|
2386
|
+
it('upload: validates local files and delegates to page.uploadFiles', async () => {
|
|
2387
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-upload-'));
|
|
2388
|
+
const file = path.join(dir, 'receipt.pdf');
|
|
2389
|
+
fs.writeFileSync(file, 'pdf');
|
|
2390
|
+
browserState.page.uploadFiles.mockResolvedValueOnce({
|
|
2391
|
+
uploaded: true,
|
|
2392
|
+
files: 1,
|
|
2393
|
+
file_names: ['receipt.pdf'],
|
|
2394
|
+
target: '#file',
|
|
2395
|
+
matches_n: 1,
|
|
2396
|
+
match_level: 'exact',
|
|
2397
|
+
multiple: false,
|
|
2398
|
+
});
|
|
2399
|
+
const program = createProgram('', '');
|
|
2400
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'upload', '#file', file]);
|
|
2401
|
+
expect(browserState.page.uploadFiles).toHaveBeenCalledWith('#file', [file], {});
|
|
2402
|
+
expect(lastJsonLog()).toEqual({
|
|
2403
|
+
uploaded: true,
|
|
2404
|
+
files: 1,
|
|
2405
|
+
file_names: ['receipt.pdf'],
|
|
2406
|
+
target: '#file',
|
|
2407
|
+
matches_n: 1,
|
|
2408
|
+
match_level: 'exact',
|
|
2409
|
+
multiple: false,
|
|
2410
|
+
});
|
|
2411
|
+
});
|
|
2412
|
+
it('upload: rejects missing files before touching the page', async () => {
|
|
2413
|
+
const program = createProgram('', '');
|
|
2414
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'upload', '#file', '/tmp/opencli-missing-file']);
|
|
2415
|
+
expect(lastJsonLog().error.code).toBe('file_not_found');
|
|
2416
|
+
expect(browserState.page.uploadFiles).not.toHaveBeenCalled();
|
|
2417
|
+
expect(process.exitCode).toBeDefined();
|
|
2418
|
+
});
|
|
2419
|
+
it('drag: delegates to page.drag and forwards source/target nth options', async () => {
|
|
2420
|
+
browserState.page.drag.mockResolvedValueOnce({
|
|
2421
|
+
dragged: true,
|
|
2422
|
+
source: '.card',
|
|
2423
|
+
target: '.lane',
|
|
2424
|
+
source_matches_n: 3,
|
|
2425
|
+
target_matches_n: 2,
|
|
2426
|
+
source_match_level: 'exact',
|
|
2427
|
+
target_match_level: 'stable',
|
|
2428
|
+
});
|
|
2429
|
+
const program = createProgram('', '');
|
|
2430
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'drag', '.card', '.lane', '--from-nth', '2', '--to-nth', '1']);
|
|
2431
|
+
expect(browserState.page.drag).toHaveBeenCalledWith('.card', '.lane', { from: { nth: 2 }, to: { nth: 1 } });
|
|
2432
|
+
expect(lastJsonLog()).toEqual({
|
|
2433
|
+
dragged: true,
|
|
2434
|
+
source: '.card',
|
|
2435
|
+
target: '.lane',
|
|
2436
|
+
source_matches_n: 3,
|
|
2437
|
+
target_matches_n: 2,
|
|
2438
|
+
source_match_level: 'exact',
|
|
2439
|
+
target_match_level: 'stable',
|
|
2440
|
+
});
|
|
2441
|
+
});
|
|
2442
|
+
it('drag: rejects malformed --from-nth before touching the page', async () => {
|
|
2443
|
+
const program = createProgram('', '');
|
|
2444
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'drag', '.card', '.lane', '--from-nth', 'abc']);
|
|
2445
|
+
expect(lastJsonLog().error.code).toBe('usage_error');
|
|
2446
|
+
expect(browserState.page.drag).not.toHaveBeenCalled();
|
|
2447
|
+
expect(process.exitCode).toBeDefined();
|
|
2448
|
+
});
|
|
1776
2449
|
it('type: clicks, waits, then typeText — emits {typed, text, target, matches_n, match_level, autocomplete}', async () => {
|
|
1777
2450
|
browserState.page.click.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
|
|
1778
2451
|
browserState.page.typeText.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
|
|
@@ -1906,6 +2579,21 @@ describe('browser select command', () => {
|
|
|
1906
2579
|
expect(err.available).toEqual(['US', 'CA']);
|
|
1907
2580
|
expect(process.exitCode).toBeDefined();
|
|
1908
2581
|
});
|
|
2582
|
+
it('select: treats the first positional as option when using semantic locator flags', async () => {
|
|
2583
|
+
const evalMock = browserState.page.evaluate;
|
|
2584
|
+
evalMock
|
|
2585
|
+
.mockResolvedValueOnce({
|
|
2586
|
+
matches_n: 1,
|
|
2587
|
+
entries: [
|
|
2588
|
+
{ nth: 0, ref: 36, tag: 'select', role: 'combobox', text: 'Country', attrs: {}, visible: true },
|
|
2589
|
+
],
|
|
2590
|
+
})
|
|
2591
|
+
.mockResolvedValueOnce({ ok: true, matches_n: 1, match_level: 'exact' })
|
|
2592
|
+
.mockResolvedValueOnce({ selected: 'Uruguay' });
|
|
2593
|
+
const program = createProgram('', '');
|
|
2594
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'select', '--label', 'Country', 'Uruguay']);
|
|
2595
|
+
expect(lastJsonLog()).toEqual({ selected: 'Uruguay', target: '36', matches_n: 1, match_level: 'exact' });
|
|
2596
|
+
});
|
|
1909
2597
|
it('surfaces selector_ambiguous from the resolver before calling selectResolvedJs', async () => {
|
|
1910
2598
|
browserState.page.evaluate.mockResolvedValueOnce({
|
|
1911
2599
|
ok: false,
|