@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
package/dist/src/cli.test.js
CHANGED
|
@@ -337,6 +337,236 @@ describe('createProgram root help descriptions', () => {
|
|
|
337
337
|
registry.set(key, value);
|
|
338
338
|
}
|
|
339
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).toEqual(expect.arrayContaining([
|
|
353
|
+
expect.objectContaining({
|
|
354
|
+
name: 'workspace',
|
|
355
|
+
flags: '--workspace <name>',
|
|
356
|
+
takes_value: 'required',
|
|
357
|
+
}),
|
|
358
|
+
expect.objectContaining({
|
|
359
|
+
name: 'window',
|
|
360
|
+
flags: '--window <mode>',
|
|
361
|
+
takes_value: 'required',
|
|
362
|
+
}),
|
|
363
|
+
expect.objectContaining({
|
|
364
|
+
name: 'keepTab',
|
|
365
|
+
flags: '--keep-tab <bool>',
|
|
366
|
+
takes_value: 'required',
|
|
367
|
+
}),
|
|
368
|
+
]));
|
|
369
|
+
expect(data.global_options).toEqual(expect.arrayContaining([
|
|
370
|
+
expect.objectContaining({
|
|
371
|
+
name: 'version',
|
|
372
|
+
flags: '-V, --version',
|
|
373
|
+
}),
|
|
374
|
+
expect.objectContaining({
|
|
375
|
+
name: 'profile',
|
|
376
|
+
flags: '--profile <name>',
|
|
377
|
+
takes_value: 'required',
|
|
378
|
+
}),
|
|
379
|
+
]));
|
|
380
|
+
const click = data.commands.find((cmd) => cmd.name === 'click');
|
|
381
|
+
expect(click).toMatchObject({
|
|
382
|
+
command: 'opencli browser click',
|
|
383
|
+
usage: 'opencli browser click [target] [options]',
|
|
384
|
+
positionals: [{ name: 'target' }],
|
|
385
|
+
});
|
|
386
|
+
expect(click.command_options.map((option) => option.name)).toEqual(['role', 'name', 'label', 'text', 'testid', 'nth', 'tab']);
|
|
387
|
+
const tabList = data.commands.find((cmd) => cmd.name === 'tab list');
|
|
388
|
+
expect(tabList).toMatchObject({
|
|
389
|
+
command: 'opencli browser tab list',
|
|
390
|
+
usage: 'opencli browser tab list [options]',
|
|
391
|
+
command_options: [],
|
|
392
|
+
});
|
|
393
|
+
const getText = data.commands.find((cmd) => cmd.name === 'get text');
|
|
394
|
+
expect(getText).toMatchObject({
|
|
395
|
+
command: 'opencli browser get text',
|
|
396
|
+
positionals: [{ name: 'target' }],
|
|
397
|
+
});
|
|
398
|
+
expect(data.structured_help).toMatchObject({
|
|
399
|
+
formats: ['yaml', 'json'],
|
|
400
|
+
usage: 'opencli browser --help -f yaml',
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
finally {
|
|
404
|
+
process.argv = argv;
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
it('renders nested browser parent structured help for a subtree', () => {
|
|
408
|
+
const argv = process.argv;
|
|
409
|
+
try {
|
|
410
|
+
const program = createProgram('', '');
|
|
411
|
+
const browser = program.commands.find(cmd => cmd.name() === 'browser');
|
|
412
|
+
const tab = browser.commands.find(cmd => cmd.name() === 'tab');
|
|
413
|
+
expect(tab).toBeTruthy();
|
|
414
|
+
process.argv = ['node', 'opencli', 'browser', 'tab', '--help', '-f', 'yaml'];
|
|
415
|
+
const data = yaml.load(tab.helpInformation());
|
|
416
|
+
expect(data).toMatchObject({
|
|
417
|
+
namespace: 'browser',
|
|
418
|
+
group: 'tab',
|
|
419
|
+
command: 'opencli browser tab',
|
|
420
|
+
usage: 'opencli browser tab <command> [args] [options]',
|
|
421
|
+
command_count: 4,
|
|
422
|
+
});
|
|
423
|
+
expect(data.commands.map((cmd) => cmd.name)).toEqual([
|
|
424
|
+
'tab close',
|
|
425
|
+
'tab list',
|
|
426
|
+
'tab new',
|
|
427
|
+
'tab select',
|
|
428
|
+
]);
|
|
429
|
+
expect(data.commands.find((cmd) => cmd.name === 'tab close')).toMatchObject({
|
|
430
|
+
command: 'opencli browser tab close',
|
|
431
|
+
usage: 'opencli browser tab close [targetId] [options]',
|
|
432
|
+
positionals: [{ name: 'targetId', help: 'Target tab/page identity returned by "browser open", "browser tab new", or "browser tab list"' }],
|
|
433
|
+
});
|
|
434
|
+
expect(data.namespace_options.map((option) => option.name)).toEqual(['workspace', 'window', 'keepTab']);
|
|
435
|
+
expect(data.structured_help).toMatchObject({
|
|
436
|
+
usage: 'opencli browser tab --help -f yaml',
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
finally {
|
|
440
|
+
process.argv = argv;
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
it('renders browser command structured help without needing the full namespace dump', () => {
|
|
444
|
+
const argv = process.argv;
|
|
445
|
+
try {
|
|
446
|
+
const program = createProgram('', '');
|
|
447
|
+
const browser = program.commands.find(cmd => cmd.name() === 'browser');
|
|
448
|
+
const click = browser.commands.find(cmd => cmd.name() === 'click');
|
|
449
|
+
expect(click).toBeTruthy();
|
|
450
|
+
process.argv = ['node', 'opencli', 'browser', 'click', '--help', '-f', 'yaml'];
|
|
451
|
+
const data = yaml.load(click.helpInformation());
|
|
452
|
+
expect(data).toMatchObject({
|
|
453
|
+
namespace: 'browser',
|
|
454
|
+
name: 'click',
|
|
455
|
+
command: 'opencli browser click',
|
|
456
|
+
usage: 'opencli browser click [target] [options]',
|
|
457
|
+
positionals: [{ name: 'target' }],
|
|
458
|
+
structured_help: {
|
|
459
|
+
usage: 'opencli browser click --help -f yaml',
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
expect(data.command_options.map((option) => option.name)).toEqual(['role', 'name', 'label', 'text', 'testid', 'nth', 'tab']);
|
|
463
|
+
expect(data.namespace_options.map((option) => option.name)).toEqual(['workspace', 'window', 'keepTab']);
|
|
464
|
+
expect(data.global_options.map((option) => option.name)).toContain('profile');
|
|
465
|
+
}
|
|
466
|
+
finally {
|
|
467
|
+
process.argv = argv;
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
it('renders daemon namespace structured help with leaves and global options', () => {
|
|
471
|
+
const argv = process.argv;
|
|
472
|
+
try {
|
|
473
|
+
const program = createProgram('', '');
|
|
474
|
+
const daemon = program.commands.find(cmd => cmd.name() === 'daemon');
|
|
475
|
+
expect(daemon).toBeTruthy();
|
|
476
|
+
process.argv = ['node', 'opencli', 'daemon', '--help', '-f', 'yaml'];
|
|
477
|
+
const data = yaml.load(daemon.helpInformation());
|
|
478
|
+
expect(data).toMatchObject({
|
|
479
|
+
namespace: 'daemon',
|
|
480
|
+
command: 'opencli daemon',
|
|
481
|
+
usage: 'opencli daemon <command> [args] [options]',
|
|
482
|
+
description: 'Manage the opencli daemon',
|
|
483
|
+
command_count: 3,
|
|
484
|
+
namespace_options: [],
|
|
485
|
+
structured_help: { usage: 'opencli daemon --help -f yaml' },
|
|
486
|
+
});
|
|
487
|
+
expect(data.commands.map((cmd) => cmd.name)).toEqual(['restart', 'status', 'stop']);
|
|
488
|
+
expect(data.global_options.map((option) => option.name)).toEqual(expect.arrayContaining(['version', 'profile']));
|
|
489
|
+
}
|
|
490
|
+
finally {
|
|
491
|
+
process.argv = argv;
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
it('renders plugin namespace structured help with positional + option leaves', () => {
|
|
495
|
+
const argv = process.argv;
|
|
496
|
+
try {
|
|
497
|
+
const program = createProgram('', '');
|
|
498
|
+
const plugin = program.commands.find(cmd => cmd.name() === 'plugin');
|
|
499
|
+
expect(plugin).toBeTruthy();
|
|
500
|
+
process.argv = ['node', 'opencli', 'plugin', '--help', '-f', 'yaml'];
|
|
501
|
+
const data = yaml.load(plugin.helpInformation());
|
|
502
|
+
expect(data).toMatchObject({
|
|
503
|
+
namespace: 'plugin',
|
|
504
|
+
command: 'opencli plugin',
|
|
505
|
+
description: 'Manage opencli plugins',
|
|
506
|
+
namespace_options: [],
|
|
507
|
+
});
|
|
508
|
+
expect(data.commands.map((cmd) => cmd.name)).toEqual(['create', 'install', 'list', 'uninstall', 'update']);
|
|
509
|
+
const update = data.commands.find((cmd) => cmd.name === 'update');
|
|
510
|
+
expect(update).toMatchObject({
|
|
511
|
+
usage: 'opencli plugin update [name] [options]',
|
|
512
|
+
positionals: [{ name: 'name' }],
|
|
513
|
+
});
|
|
514
|
+
expect(update.command_options.map((option) => option.name)).toEqual(['all']);
|
|
515
|
+
}
|
|
516
|
+
finally {
|
|
517
|
+
process.argv = argv;
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
it('renders adapter namespace structured help preserving original description after applyRootSubcommandSummaries', () => {
|
|
521
|
+
const argv = process.argv;
|
|
522
|
+
try {
|
|
523
|
+
const program = createProgram('', '');
|
|
524
|
+
const adapter = program.commands.find(cmd => cmd.name() === 'adapter');
|
|
525
|
+
expect(adapter).toBeTruthy();
|
|
526
|
+
process.argv = ['node', 'opencli', 'adapter', '--help', '-f', 'yaml'];
|
|
527
|
+
const data = yaml.load(adapter.helpInformation());
|
|
528
|
+
// applyRootSubcommandSummaries() rewrites .description() to a child-name listing;
|
|
529
|
+
// structured help must surface the original product description via the snapshot.
|
|
530
|
+
expect(data.description).toBe('Manage CLI adapters');
|
|
531
|
+
expect(data.commands.map((cmd) => cmd.name)).toEqual(['eject', 'reset', 'status']);
|
|
532
|
+
const reset = data.commands.find((cmd) => cmd.name === 'reset');
|
|
533
|
+
expect(reset).toMatchObject({
|
|
534
|
+
usage: 'opencli adapter reset [site] [options]',
|
|
535
|
+
positionals: [{ name: 'site' }],
|
|
536
|
+
});
|
|
537
|
+
expect(reset.command_options.map((option) => option.name)).toEqual(['all']);
|
|
538
|
+
}
|
|
539
|
+
finally {
|
|
540
|
+
process.argv = argv;
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
it('renders profile namespace structured help including required positionals', () => {
|
|
544
|
+
const argv = process.argv;
|
|
545
|
+
try {
|
|
546
|
+
const program = createProgram('', '');
|
|
547
|
+
const profile = program.commands.find(cmd => cmd.name() === 'profile');
|
|
548
|
+
expect(profile).toBeTruthy();
|
|
549
|
+
process.argv = ['node', 'opencli', 'profile', '--help', '-f', 'yaml'];
|
|
550
|
+
const data = yaml.load(profile.helpInformation());
|
|
551
|
+
expect(data).toMatchObject({
|
|
552
|
+
namespace: 'profile',
|
|
553
|
+
description: 'Manage Browser Bridge Chrome profiles',
|
|
554
|
+
command_count: 3,
|
|
555
|
+
});
|
|
556
|
+
expect(data.commands.map((cmd) => cmd.name)).toEqual(['list', 'rename', 'use']);
|
|
557
|
+
const rename = data.commands.find((cmd) => cmd.name === 'rename');
|
|
558
|
+
expect(rename).toMatchObject({
|
|
559
|
+
usage: 'opencli profile rename <contextId> <alias> [options]',
|
|
560
|
+
positionals: [
|
|
561
|
+
{ name: 'contextId', required: true },
|
|
562
|
+
{ name: 'alias', required: true },
|
|
563
|
+
],
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
finally {
|
|
567
|
+
process.argv = argv;
|
|
568
|
+
}
|
|
569
|
+
});
|
|
340
570
|
});
|
|
341
571
|
describe('resolveBrowserVerifyInvocation', () => {
|
|
342
572
|
it('prefers the built entry declared in package metadata', () => {
|
|
@@ -607,6 +837,8 @@ describe('browser tab targeting commands', () => {
|
|
|
607
837
|
stderrSpy.mockClear();
|
|
608
838
|
mockBrowserConnect.mockClear();
|
|
609
839
|
mockBrowserClose.mockReset().mockResolvedValue(undefined);
|
|
840
|
+
delete process.env.OPENCLI_WINDOW;
|
|
841
|
+
delete process.env.OPENCLI_KEEP_TAB;
|
|
610
842
|
mockBindTab.mockReset().mockResolvedValue({
|
|
611
843
|
workspace: 'bound:default',
|
|
612
844
|
page: 'tab-2',
|
|
@@ -636,7 +868,17 @@ describe('browser tab targeting commands', () => {
|
|
|
636
868
|
{ index: 0, frameId: 'frame-1', url: 'https://x.example/embed', name: 'x-embed' },
|
|
637
869
|
]),
|
|
638
870
|
evaluateInFrame: vi.fn().mockResolvedValue('inside frame'),
|
|
871
|
+
screenshot: vi.fn().mockResolvedValue('base64-shot'),
|
|
872
|
+
annotatedScreenshot: vi.fn().mockResolvedValue('annotated-base64-shot'),
|
|
639
873
|
readNetworkCapture: vi.fn().mockResolvedValue([]),
|
|
874
|
+
closeWindow: vi.fn().mockResolvedValue(undefined),
|
|
875
|
+
waitForDownload: vi.fn().mockResolvedValue({
|
|
876
|
+
downloaded: true,
|
|
877
|
+
filename: '/tmp/receipt.pdf',
|
|
878
|
+
url: 'https://app.example/receipt.pdf',
|
|
879
|
+
state: 'complete',
|
|
880
|
+
elapsedMs: 10,
|
|
881
|
+
}),
|
|
640
882
|
};
|
|
641
883
|
});
|
|
642
884
|
function lastJsonLog() {
|
|
@@ -672,8 +914,90 @@ describe('browser tab targeting commands', () => {
|
|
|
672
914
|
it('runs browser commands against an explicit bound workspace', async () => {
|
|
673
915
|
const program = createProgram('', '');
|
|
674
916
|
await program.parseAsync(['node', 'opencli', 'browser', '--workspace', 'bound:default', 'state']);
|
|
675
|
-
expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 30, workspace: 'bound:default' });
|
|
917
|
+
expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 30, workspace: 'bound:default', windowMode: 'foreground' });
|
|
918
|
+
expect(browserState.page?.snapshot).toHaveBeenCalled();
|
|
919
|
+
});
|
|
920
|
+
it('passes browser --window through Commander options without relying on env pre-processing', async () => {
|
|
921
|
+
const program = createProgram('', '');
|
|
922
|
+
await program.parseAsync(['node', 'opencli', 'browser', '--window', 'background', 'state']);
|
|
923
|
+
expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 30, workspace: 'browser:default', windowMode: 'background' });
|
|
924
|
+
expect(browserState.page?.snapshot).toHaveBeenCalled();
|
|
925
|
+
});
|
|
926
|
+
it('releases non-bound browser tab leases when --keep-tab=false', async () => {
|
|
927
|
+
const program = createProgram('', '');
|
|
928
|
+
await program.parseAsync(['node', 'opencli', 'browser', '--keep-tab', 'false', 'state']);
|
|
676
929
|
expect(browserState.page?.snapshot).toHaveBeenCalled();
|
|
930
|
+
expect(browserState.page?.closeWindow).toHaveBeenCalled();
|
|
931
|
+
});
|
|
932
|
+
it('does not auto-release explicit bound workspaces when --keep-tab=false', async () => {
|
|
933
|
+
const program = createProgram('', '');
|
|
934
|
+
await program.parseAsync(['node', 'opencli', 'browser', '--workspace', 'bound:default', '--keep-tab', 'false', 'state']);
|
|
935
|
+
expect(browserState.page?.snapshot).toHaveBeenCalled();
|
|
936
|
+
expect(browserState.page?.closeWindow).not.toHaveBeenCalled();
|
|
937
|
+
expect(stderrSpy.mock.calls.flat().join('')).toContain('--window/--keep-tab ignored for bound:* workspaces');
|
|
938
|
+
});
|
|
939
|
+
it('passes the opt-in AX source to browser state', async () => {
|
|
940
|
+
const program = createProgram('', '');
|
|
941
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'state', '--source', 'ax']);
|
|
942
|
+
expect(browserState.page?.snapshot).toHaveBeenCalledWith({ viewportExpand: 2000, source: 'ax' });
|
|
943
|
+
});
|
|
944
|
+
it('prints DOM vs AX snapshot metrics without changing default state output', async () => {
|
|
945
|
+
browserState.page = {
|
|
946
|
+
...browserState.page,
|
|
947
|
+
snapshot: vi.fn(async (opts) => {
|
|
948
|
+
if (opts?.source === 'ax') {
|
|
949
|
+
return 'source: ax\n---\n[1]button "Save"\nframe "https://app.example/embed":\n [2]button "Frame Save"\n---\ninteractive: 2';
|
|
950
|
+
}
|
|
951
|
+
return 'URL: https://app.example\n[1] button "Save"';
|
|
952
|
+
}),
|
|
953
|
+
};
|
|
954
|
+
const program = createProgram('', '');
|
|
955
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'state', '--compare-sources']);
|
|
956
|
+
expect(browserState.page?.snapshot).toHaveBeenCalledWith({ viewportExpand: 2000, source: 'dom' });
|
|
957
|
+
expect(browserState.page?.snapshot).toHaveBeenCalledWith({ viewportExpand: 2000, source: 'ax' });
|
|
958
|
+
const out = lastJsonLog();
|
|
959
|
+
expect(out.url).toBe('https://one.example');
|
|
960
|
+
expect(out.sources.dom).toMatchObject({ ok: true, refs: 1, frame_sections: 0 });
|
|
961
|
+
expect(out.sources.ax).toMatchObject({ ok: true, refs: 2, frame_sections: 1, interactive: 2 });
|
|
962
|
+
});
|
|
963
|
+
it('keeps compare-sources usable when one observation backend fails', async () => {
|
|
964
|
+
browserState.page = {
|
|
965
|
+
...browserState.page,
|
|
966
|
+
snapshot: vi.fn(async (opts) => {
|
|
967
|
+
if (opts?.source === 'ax')
|
|
968
|
+
throw new Error('AX unavailable');
|
|
969
|
+
return '[1] button "Save"';
|
|
970
|
+
}),
|
|
971
|
+
};
|
|
972
|
+
const program = createProgram('', '');
|
|
973
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'state', '--compare-sources']);
|
|
974
|
+
const out = lastJsonLog();
|
|
975
|
+
expect(out.sources.dom).toMatchObject({ ok: true, refs: 1 });
|
|
976
|
+
expect(out.sources.ax).toMatchObject({
|
|
977
|
+
ok: false,
|
|
978
|
+
error: { message: 'AX unavailable' },
|
|
979
|
+
});
|
|
980
|
+
});
|
|
981
|
+
it('rejects unknown browser state sources before touching the page', async () => {
|
|
982
|
+
const program = createProgram('', '');
|
|
983
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'state', '--source', 'magic']);
|
|
984
|
+
expect(browserState.page?.snapshot).not.toHaveBeenCalled();
|
|
985
|
+
const out = lastJsonLog();
|
|
986
|
+
expect(out.error.code).toBe('invalid_source');
|
|
987
|
+
expect(process.exitCode).toBeDefined();
|
|
988
|
+
});
|
|
989
|
+
it('captures annotated screenshots through the visual ref overlay path', async () => {
|
|
990
|
+
const program = createProgram('', '');
|
|
991
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'screenshot', '--annotate']);
|
|
992
|
+
expect(browserState.page?.annotatedScreenshot).toHaveBeenCalledWith({
|
|
993
|
+
fullPage: false,
|
|
994
|
+
annotate: true,
|
|
995
|
+
width: undefined,
|
|
996
|
+
height: undefined,
|
|
997
|
+
format: 'png',
|
|
998
|
+
});
|
|
999
|
+
expect(browserState.page?.screenshot).not.toHaveBeenCalled();
|
|
1000
|
+
expect(consoleLogSpy).toHaveBeenLastCalledWith('annotated-base64-shot');
|
|
677
1001
|
});
|
|
678
1002
|
it('blocks history navigation on bound workspaces unless explicitly allowed', async () => {
|
|
679
1003
|
browserState.page = {
|
|
@@ -1036,6 +1360,55 @@ describe('browser tab targeting commands', () => {
|
|
|
1036
1360
|
expect(bufferReads).toBe(2);
|
|
1037
1361
|
expect(out.matched.url).toBe('https://target.example/api/target');
|
|
1038
1362
|
});
|
|
1363
|
+
it('browser wait download delegates to the Browser Bridge download observer', async () => {
|
|
1364
|
+
browserState.page = {
|
|
1365
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
1366
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
1367
|
+
waitForDownload: vi.fn().mockResolvedValue({
|
|
1368
|
+
downloaded: true,
|
|
1369
|
+
filename: '/tmp/receipt.pdf',
|
|
1370
|
+
url: 'https://app.example/receipt.pdf',
|
|
1371
|
+
state: 'complete',
|
|
1372
|
+
elapsedMs: 10,
|
|
1373
|
+
}),
|
|
1374
|
+
setActivePage: vi.fn(),
|
|
1375
|
+
getActivePage: vi.fn().mockReturnValue('tab-1'),
|
|
1376
|
+
getCurrentUrl: vi.fn().mockResolvedValue('https://target.example'),
|
|
1377
|
+
tabs: vi.fn().mockResolvedValue([{ index: 0, page: 'tab-1', url: 'https://target.example', title: 'Target', active: true }]),
|
|
1378
|
+
};
|
|
1379
|
+
const program = createProgram('', '');
|
|
1380
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'wait', 'download', 'receipt', '--timeout', '900']);
|
|
1381
|
+
expect(browserState.page?.waitForDownload).toHaveBeenCalledWith('receipt', 900);
|
|
1382
|
+
expect(lastJsonLog()).toEqual({
|
|
1383
|
+
downloaded: true,
|
|
1384
|
+
filename: '/tmp/receipt.pdf',
|
|
1385
|
+
url: 'https://app.example/receipt.pdf',
|
|
1386
|
+
state: 'complete',
|
|
1387
|
+
elapsedMs: 10,
|
|
1388
|
+
});
|
|
1389
|
+
});
|
|
1390
|
+
it('browser wait download reports an error envelope when no matching download completes', async () => {
|
|
1391
|
+
browserState.page = {
|
|
1392
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
1393
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
1394
|
+
waitForDownload: vi.fn().mockResolvedValue({
|
|
1395
|
+
downloaded: false,
|
|
1396
|
+
state: 'interrupted',
|
|
1397
|
+
error: 'No download matched "receipt" within 900ms',
|
|
1398
|
+
elapsedMs: 900,
|
|
1399
|
+
}),
|
|
1400
|
+
setActivePage: vi.fn(),
|
|
1401
|
+
getActivePage: vi.fn().mockReturnValue('tab-1'),
|
|
1402
|
+
getCurrentUrl: vi.fn().mockResolvedValue('https://target.example'),
|
|
1403
|
+
tabs: vi.fn().mockResolvedValue([{ index: 0, page: 'tab-1', url: 'https://target.example', title: 'Target', active: true }]),
|
|
1404
|
+
};
|
|
1405
|
+
const program = createProgram('', '');
|
|
1406
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'wait', 'download', 'receipt', '--timeout', '900']);
|
|
1407
|
+
const out = lastJsonLog();
|
|
1408
|
+
expect(out.error.code).toBe('download_not_seen');
|
|
1409
|
+
expect(out.download.elapsedMs).toBe(900);
|
|
1410
|
+
expect(process.exitCode).toBeDefined();
|
|
1411
|
+
});
|
|
1039
1412
|
});
|
|
1040
1413
|
describe('browser network command', () => {
|
|
1041
1414
|
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
@@ -1687,6 +2060,25 @@ describe('browser find command', () => {
|
|
|
1687
2060
|
expect(out.entries[1].ref).toBe(17);
|
|
1688
2061
|
expect(process.exitCode).toBeUndefined();
|
|
1689
2062
|
});
|
|
2063
|
+
it('finds elements by semantic role/name without requiring CSS', async () => {
|
|
2064
|
+
browserState.page.evaluate.mockResolvedValueOnce({
|
|
2065
|
+
matches_n: 1,
|
|
2066
|
+
entries: [
|
|
2067
|
+
{ nth: 0, ref: 9, tag: 'button', role: 'button', text: 'Save', attrs: {}, visible: true },
|
|
2068
|
+
],
|
|
2069
|
+
});
|
|
2070
|
+
const program = createProgram('', '');
|
|
2071
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'find', '--role', 'button', '--name', 'Save']);
|
|
2072
|
+
const js = browserState.page.evaluate.mock.calls[0][0];
|
|
2073
|
+
expect(js).toContain('CRITERIA');
|
|
2074
|
+
expect(js).toContain('function accessibleName');
|
|
2075
|
+
expect(lastJsonLog()).toEqual({
|
|
2076
|
+
matches_n: 1,
|
|
2077
|
+
entries: [
|
|
2078
|
+
{ nth: 0, ref: 9, tag: 'button', role: 'button', text: 'Save', attrs: {}, visible: true },
|
|
2079
|
+
],
|
|
2080
|
+
});
|
|
2081
|
+
});
|
|
1690
2082
|
it('forwards --limit / --text-max into the generated JS', async () => {
|
|
1691
2083
|
browserState.page.evaluate.mockResolvedValueOnce({ matches_n: 0, entries: [] });
|
|
1692
2084
|
const program = createProgram('', '');
|
|
@@ -1742,6 +2134,40 @@ describe('browser get text/value/attributes commands', () => {
|
|
|
1742
2134
|
await program.parseAsync(['node', 'opencli', 'browser', 'get', 'text', '7']);
|
|
1743
2135
|
expect(lastJsonLog()).toEqual({ value: 'Hello world', matches_n: 1, match_level: 'exact' });
|
|
1744
2136
|
});
|
|
2137
|
+
it('resolves a semantic locator to a ref before get text', async () => {
|
|
2138
|
+
const evalMock = browserState.page.evaluate;
|
|
2139
|
+
evalMock.mockResolvedValueOnce({
|
|
2140
|
+
matches_n: 1,
|
|
2141
|
+
entries: [
|
|
2142
|
+
{ nth: 0, ref: 12, tag: 'button', role: 'button', text: 'Save', attrs: {}, visible: true },
|
|
2143
|
+
],
|
|
2144
|
+
});
|
|
2145
|
+
evalMock.mockResolvedValueOnce({ ok: true, matches_n: 1, match_level: 'exact' });
|
|
2146
|
+
evalMock.mockResolvedValueOnce('Save');
|
|
2147
|
+
const program = createProgram('', '');
|
|
2148
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'get', 'text', '--role', 'button', '--name', 'Save']);
|
|
2149
|
+
expect(evalMock.mock.calls[0][0]).toContain('function accessibleName');
|
|
2150
|
+
expect(evalMock.mock.calls[1][0]).toContain('const ref = "12"');
|
|
2151
|
+
expect(lastJsonLog()).toEqual({ value: 'Save', matches_n: 1, match_level: 'exact' });
|
|
2152
|
+
});
|
|
2153
|
+
it('reports total_matches when semantic get reads the first of multiple matches', async () => {
|
|
2154
|
+
const evalMock = browserState.page.evaluate;
|
|
2155
|
+
evalMock.mockResolvedValueOnce({
|
|
2156
|
+
matches_n: 3,
|
|
2157
|
+
entries: [
|
|
2158
|
+
{ nth: 0, ref: 12, tag: 'button', role: 'button', text: 'Save', attrs: {}, visible: true },
|
|
2159
|
+
{ nth: 1, ref: 13, tag: 'button', role: 'button', text: 'Save draft', attrs: {}, visible: true },
|
|
2160
|
+
{ nth: 2, ref: 14, tag: 'button', role: 'button', text: 'Save copy', attrs: {}, visible: true },
|
|
2161
|
+
],
|
|
2162
|
+
});
|
|
2163
|
+
evalMock.mockResolvedValueOnce({ ok: true, matches_n: 1, match_level: 'exact' });
|
|
2164
|
+
evalMock.mockResolvedValueOnce('Save');
|
|
2165
|
+
const program = createProgram('', '');
|
|
2166
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'get', 'text', '--role', 'button', '--name', 'Save']);
|
|
2167
|
+
expect(evalMock.mock.calls[0][0]).toContain('const LIMIT = 6');
|
|
2168
|
+
expect(evalMock.mock.calls[1][0]).toContain('const ref = "12"');
|
|
2169
|
+
expect(lastJsonLog()).toEqual({ value: 'Save', matches_n: 1, match_level: 'exact', total_matches: 3 });
|
|
2170
|
+
});
|
|
1745
2171
|
it('reports matches_n on multi-match CSS (read path: first match wins)', async () => {
|
|
1746
2172
|
const evalMock = browserState.page.evaluate;
|
|
1747
2173
|
evalMock.mockResolvedValueOnce({ ok: true, matches_n: 3, match_level: 'exact' });
|
|
@@ -1797,6 +2223,28 @@ describe('browser click/type commands', () => {
|
|
|
1797
2223
|
const { lastJsonLog } = installSelectorFirstTestHarness('click-type', () => ({
|
|
1798
2224
|
evaluate: vi.fn().mockResolvedValue(false),
|
|
1799
2225
|
click: vi.fn().mockResolvedValue({ matches_n: 1, match_level: 'exact' }),
|
|
2226
|
+
dblClick: vi.fn().mockResolvedValue({ matches_n: 1, match_level: 'exact' }),
|
|
2227
|
+
hover: vi.fn().mockResolvedValue({ matches_n: 1, match_level: 'exact' }),
|
|
2228
|
+
focus: vi.fn().mockResolvedValue({ focused: true, matches_n: 1, match_level: 'exact' }),
|
|
2229
|
+
setChecked: vi.fn().mockResolvedValue({ checked: true, changed: true, matches_n: 1, match_level: 'exact', kind: 'checkbox' }),
|
|
2230
|
+
uploadFiles: vi.fn().mockResolvedValue({
|
|
2231
|
+
uploaded: true,
|
|
2232
|
+
files: 1,
|
|
2233
|
+
file_names: ['receipt.pdf'],
|
|
2234
|
+
target: '#file',
|
|
2235
|
+
matches_n: 1,
|
|
2236
|
+
match_level: 'exact',
|
|
2237
|
+
multiple: false,
|
|
2238
|
+
}),
|
|
2239
|
+
drag: vi.fn().mockResolvedValue({
|
|
2240
|
+
dragged: true,
|
|
2241
|
+
source: '#card',
|
|
2242
|
+
target: '#lane',
|
|
2243
|
+
source_matches_n: 1,
|
|
2244
|
+
target_matches_n: 1,
|
|
2245
|
+
source_match_level: 'exact',
|
|
2246
|
+
target_match_level: 'exact',
|
|
2247
|
+
}),
|
|
1800
2248
|
typeText: vi.fn().mockResolvedValue({ matches_n: 1, match_level: 'exact' }),
|
|
1801
2249
|
fillText: vi.fn().mockResolvedValue({
|
|
1802
2250
|
filled: true,
|
|
@@ -1817,6 +2265,163 @@ describe('browser click/type commands', () => {
|
|
|
1817
2265
|
expect(browserState.page.click).toHaveBeenCalledWith('#save', {});
|
|
1818
2266
|
expect(lastJsonLog()).toEqual({ clicked: true, target: '#save', matches_n: 1, match_level: 'exact' });
|
|
1819
2267
|
});
|
|
2268
|
+
it('clicks a unique semantic locator without a prior state call', async () => {
|
|
2269
|
+
browserState.page.evaluate.mockResolvedValueOnce({
|
|
2270
|
+
matches_n: 1,
|
|
2271
|
+
entries: [
|
|
2272
|
+
{ nth: 0, ref: 23, tag: 'button', role: 'button', text: 'Submit', attrs: {}, visible: true },
|
|
2273
|
+
],
|
|
2274
|
+
});
|
|
2275
|
+
browserState.page.click.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
|
|
2276
|
+
const program = createProgram('', '');
|
|
2277
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'click', '--role', 'button', '--name', 'Submit']);
|
|
2278
|
+
expect(browserState.page.click).toHaveBeenCalledWith('23', {});
|
|
2279
|
+
expect(lastJsonLog()).toEqual({ clicked: true, target: '23', matches_n: 1, match_level: 'exact' });
|
|
2280
|
+
});
|
|
2281
|
+
it('rejects ambiguous semantic locators before write actions', async () => {
|
|
2282
|
+
browserState.page.evaluate.mockResolvedValueOnce({
|
|
2283
|
+
matches_n: 2,
|
|
2284
|
+
entries: [
|
|
2285
|
+
{ nth: 0, ref: 1, tag: 'button', role: 'button', text: 'Save', attrs: {}, visible: true },
|
|
2286
|
+
{ nth: 1, ref: 2, tag: 'button', role: 'button', text: 'Save draft', attrs: {}, visible: true },
|
|
2287
|
+
],
|
|
2288
|
+
});
|
|
2289
|
+
const program = createProgram('', '');
|
|
2290
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'click', '--role', 'button', '--name', 'Save']);
|
|
2291
|
+
const err = lastJsonLog().error;
|
|
2292
|
+
expect(err.code).toBe('semantic_ambiguous');
|
|
2293
|
+
expect(err.matches_n).toBe(2);
|
|
2294
|
+
expect(browserState.page.click).not.toHaveBeenCalled();
|
|
2295
|
+
expect(process.exitCode).toBeDefined();
|
|
2296
|
+
});
|
|
2297
|
+
it('hover: resolves a semantic locator before moving the mouse', async () => {
|
|
2298
|
+
browserState.page.evaluate.mockResolvedValueOnce({
|
|
2299
|
+
matches_n: 1,
|
|
2300
|
+
entries: [
|
|
2301
|
+
{ nth: 0, ref: 31, tag: 'button', role: 'button', text: 'Settings', attrs: {}, visible: true },
|
|
2302
|
+
],
|
|
2303
|
+
});
|
|
2304
|
+
const program = createProgram('', '');
|
|
2305
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'hover', '--role', 'button', '--name', 'Settings']);
|
|
2306
|
+
expect(browserState.page.hover).toHaveBeenCalledWith('31', {});
|
|
2307
|
+
expect(lastJsonLog()).toEqual({ hovered: true, target: '31', matches_n: 1, match_level: 'exact' });
|
|
2308
|
+
});
|
|
2309
|
+
it('check: resolves a semantic locator before setting checked state', async () => {
|
|
2310
|
+
browserState.page.evaluate.mockResolvedValueOnce({
|
|
2311
|
+
matches_n: 1,
|
|
2312
|
+
entries: [
|
|
2313
|
+
{ nth: 0, ref: 32, tag: 'input', role: 'checkbox', text: 'Accept', attrs: {}, visible: true },
|
|
2314
|
+
],
|
|
2315
|
+
});
|
|
2316
|
+
browserState.page.setChecked.mockResolvedValueOnce({ checked: true, changed: false, matches_n: 1, match_level: 'exact', kind: 'checkbox' });
|
|
2317
|
+
const program = createProgram('', '');
|
|
2318
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'check', '--role', 'checkbox', '--name', 'Accept']);
|
|
2319
|
+
expect(browserState.page.setChecked).toHaveBeenCalledWith('32', true, {});
|
|
2320
|
+
expect(lastJsonLog()).toEqual({ checked: true, changed: false, target: '32', matches_n: 1, match_level: 'exact', kind: 'checkbox' });
|
|
2321
|
+
});
|
|
2322
|
+
it('upload: treats the first positional as a file when using semantic locator flags', async () => {
|
|
2323
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-upload-semantic-'));
|
|
2324
|
+
const file = path.join(dir, 'receipt.pdf');
|
|
2325
|
+
fs.writeFileSync(file, 'pdf');
|
|
2326
|
+
browserState.page.evaluate.mockResolvedValueOnce({
|
|
2327
|
+
matches_n: 1,
|
|
2328
|
+
entries: [
|
|
2329
|
+
{ nth: 0, ref: 33, tag: 'input', role: 'button', text: 'Upload receipt', attrs: {}, visible: true },
|
|
2330
|
+
],
|
|
2331
|
+
});
|
|
2332
|
+
browserState.page.uploadFiles.mockResolvedValueOnce({
|
|
2333
|
+
uploaded: true,
|
|
2334
|
+
files: 1,
|
|
2335
|
+
file_names: ['receipt.pdf'],
|
|
2336
|
+
target: '33',
|
|
2337
|
+
matches_n: 1,
|
|
2338
|
+
match_level: 'exact',
|
|
2339
|
+
multiple: false,
|
|
2340
|
+
});
|
|
2341
|
+
const program = createProgram('', '');
|
|
2342
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'upload', '--role', 'button', '--name', 'Upload receipt', file]);
|
|
2343
|
+
expect(browserState.page.uploadFiles).toHaveBeenCalledWith('33', [file], {});
|
|
2344
|
+
expect(lastJsonLog()).toMatchObject({ uploaded: true, target: '33', files: 1 });
|
|
2345
|
+
});
|
|
2346
|
+
it('type: treats the first positional as text when using semantic locator flags', async () => {
|
|
2347
|
+
browserState.page.evaluate
|
|
2348
|
+
.mockResolvedValueOnce({
|
|
2349
|
+
matches_n: 1,
|
|
2350
|
+
entries: [
|
|
2351
|
+
{ nth: 0, ref: 34, tag: 'input', role: 'textbox', text: '', attrs: {}, visible: true },
|
|
2352
|
+
],
|
|
2353
|
+
})
|
|
2354
|
+
.mockResolvedValueOnce(false);
|
|
2355
|
+
browserState.page.click.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
|
|
2356
|
+
browserState.page.typeText.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
|
|
2357
|
+
const program = createProgram('', '');
|
|
2358
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'type', '--label', 'Email', 'me@example.com']);
|
|
2359
|
+
expect(browserState.page.click).toHaveBeenCalledWith('34', {});
|
|
2360
|
+
expect(browserState.page.typeText).toHaveBeenCalledWith('34', 'me@example.com', {});
|
|
2361
|
+
expect(lastJsonLog()).toMatchObject({ typed: true, target: '34', text: 'me@example.com' });
|
|
2362
|
+
});
|
|
2363
|
+
it('fill: treats the first positional as text when using semantic locator flags', async () => {
|
|
2364
|
+
browserState.page.evaluate.mockResolvedValueOnce({
|
|
2365
|
+
matches_n: 1,
|
|
2366
|
+
entries: [
|
|
2367
|
+
{ nth: 0, ref: 35, tag: 'input', role: 'textbox', text: '', attrs: {}, visible: true },
|
|
2368
|
+
],
|
|
2369
|
+
});
|
|
2370
|
+
browserState.page.fillText.mockResolvedValueOnce({
|
|
2371
|
+
filled: true,
|
|
2372
|
+
verified: true,
|
|
2373
|
+
expected: 'me@example.com',
|
|
2374
|
+
actual: 'me@example.com',
|
|
2375
|
+
length: 14,
|
|
2376
|
+
matches_n: 1,
|
|
2377
|
+
match_level: 'exact',
|
|
2378
|
+
});
|
|
2379
|
+
const program = createProgram('', '');
|
|
2380
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'fill', '--label', 'Email', 'me@example.com']);
|
|
2381
|
+
expect(browserState.page.fillText).toHaveBeenCalledWith('35', 'me@example.com', {});
|
|
2382
|
+
expect(lastJsonLog()).toMatchObject({ filled: true, verified: true, target: '35', text: 'me@example.com' });
|
|
2383
|
+
});
|
|
2384
|
+
it('drag: resolves source and target from prefixed semantic locators', async () => {
|
|
2385
|
+
browserState.page.evaluate
|
|
2386
|
+
.mockResolvedValueOnce({
|
|
2387
|
+
matches_n: 1,
|
|
2388
|
+
entries: [
|
|
2389
|
+
{ nth: 0, ref: 40, tag: 'div', role: 'button', text: 'Card A', attrs: {}, visible: true },
|
|
2390
|
+
],
|
|
2391
|
+
})
|
|
2392
|
+
.mockResolvedValueOnce({
|
|
2393
|
+
matches_n: 1,
|
|
2394
|
+
entries: [
|
|
2395
|
+
{ nth: 0, ref: 41, tag: 'div', role: 'region', text: 'Done', attrs: {}, visible: true },
|
|
2396
|
+
],
|
|
2397
|
+
});
|
|
2398
|
+
browserState.page.drag.mockResolvedValueOnce({
|
|
2399
|
+
dragged: true,
|
|
2400
|
+
source: '40',
|
|
2401
|
+
target: '41',
|
|
2402
|
+
source_matches_n: 1,
|
|
2403
|
+
target_matches_n: 1,
|
|
2404
|
+
source_match_level: 'exact',
|
|
2405
|
+
target_match_level: 'exact',
|
|
2406
|
+
});
|
|
2407
|
+
const program = createProgram('', '');
|
|
2408
|
+
await program.parseAsync([
|
|
2409
|
+
'node',
|
|
2410
|
+
'opencli',
|
|
2411
|
+
'browser',
|
|
2412
|
+
'drag',
|
|
2413
|
+
'--from-role',
|
|
2414
|
+
'button',
|
|
2415
|
+
'--from-name',
|
|
2416
|
+
'Card A',
|
|
2417
|
+
'--to-role',
|
|
2418
|
+
'region',
|
|
2419
|
+
'--to-name',
|
|
2420
|
+
'Done',
|
|
2421
|
+
]);
|
|
2422
|
+
expect(browserState.page.drag).toHaveBeenCalledWith('40', '41', { from: {}, to: {} });
|
|
2423
|
+
expect(lastJsonLog()).toMatchObject({ dragged: true, source: '40', target: '41' });
|
|
2424
|
+
});
|
|
1820
2425
|
it('surfaces match_level=stable when resolver falls back to fingerprint match', async () => {
|
|
1821
2426
|
browserState.page.click.mockResolvedValueOnce({ matches_n: 1, match_level: 'stable' });
|
|
1822
2427
|
const program = createProgram('', '');
|
|
@@ -1863,6 +2468,116 @@ describe('browser click/type commands', () => {
|
|
|
1863
2468
|
expect(browserState.page.click).not.toHaveBeenCalled();
|
|
1864
2469
|
expect(process.exitCode).toBeDefined();
|
|
1865
2470
|
});
|
|
2471
|
+
it('hover: delegates to page.hover and emits a structured envelope', async () => {
|
|
2472
|
+
browserState.page.hover.mockResolvedValueOnce({ matches_n: 2, match_level: 'exact' });
|
|
2473
|
+
const program = createProgram('', '');
|
|
2474
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'hover', '.menu', '--nth', '1']);
|
|
2475
|
+
expect(browserState.page.hover).toHaveBeenCalledWith('.menu', { nth: 1 });
|
|
2476
|
+
expect(lastJsonLog()).toEqual({ hovered: true, target: '.menu', matches_n: 2, match_level: 'exact' });
|
|
2477
|
+
});
|
|
2478
|
+
it('focus: delegates to page.focus and reports whether the element took focus', async () => {
|
|
2479
|
+
browserState.page.focus.mockResolvedValueOnce({ focused: true, matches_n: 1, match_level: 'stable' });
|
|
2480
|
+
const program = createProgram('', '');
|
|
2481
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'focus', '7']);
|
|
2482
|
+
expect(browserState.page.focus).toHaveBeenCalledWith('7', {});
|
|
2483
|
+
expect(lastJsonLog()).toEqual({ focused: true, target: '7', matches_n: 1, match_level: 'stable' });
|
|
2484
|
+
});
|
|
2485
|
+
it('dblclick: delegates to page.dblClick and emits a structured envelope', async () => {
|
|
2486
|
+
browserState.page.dblClick.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
|
|
2487
|
+
const program = createProgram('', '');
|
|
2488
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'dblclick', '#row']);
|
|
2489
|
+
expect(browserState.page.dblClick).toHaveBeenCalledWith('#row', {});
|
|
2490
|
+
expect(lastJsonLog()).toEqual({ dblclicked: true, target: '#row', matches_n: 1, match_level: 'exact' });
|
|
2491
|
+
});
|
|
2492
|
+
it('check: ensures target is checked through page.setChecked', async () => {
|
|
2493
|
+
browserState.page.setChecked.mockResolvedValueOnce({
|
|
2494
|
+
checked: true,
|
|
2495
|
+
changed: true,
|
|
2496
|
+
matches_n: 2,
|
|
2497
|
+
match_level: 'exact',
|
|
2498
|
+
kind: 'checkbox',
|
|
2499
|
+
});
|
|
2500
|
+
const program = createProgram('', '');
|
|
2501
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'check', '.todo', '--nth', '1']);
|
|
2502
|
+
expect(browserState.page.setChecked).toHaveBeenCalledWith('.todo', true, { nth: 1 });
|
|
2503
|
+
expect(lastJsonLog()).toEqual({ checked: true, changed: true, target: '.todo', matches_n: 2, match_level: 'exact', kind: 'checkbox' });
|
|
2504
|
+
});
|
|
2505
|
+
it('uncheck: ensures target is unchecked through page.setChecked', async () => {
|
|
2506
|
+
browserState.page.setChecked.mockResolvedValueOnce({
|
|
2507
|
+
checked: false,
|
|
2508
|
+
changed: false,
|
|
2509
|
+
matches_n: 1,
|
|
2510
|
+
match_level: 'stable',
|
|
2511
|
+
kind: 'checkbox',
|
|
2512
|
+
});
|
|
2513
|
+
const program = createProgram('', '');
|
|
2514
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'uncheck', '#subscribe']);
|
|
2515
|
+
expect(browserState.page.setChecked).toHaveBeenCalledWith('#subscribe', false, {});
|
|
2516
|
+
expect(lastJsonLog()).toEqual({ checked: false, changed: false, target: '#subscribe', matches_n: 1, match_level: 'stable', kind: 'checkbox' });
|
|
2517
|
+
});
|
|
2518
|
+
it('upload: validates local files and delegates to page.uploadFiles', async () => {
|
|
2519
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-upload-'));
|
|
2520
|
+
const file = path.join(dir, 'receipt.pdf');
|
|
2521
|
+
fs.writeFileSync(file, 'pdf');
|
|
2522
|
+
browserState.page.uploadFiles.mockResolvedValueOnce({
|
|
2523
|
+
uploaded: true,
|
|
2524
|
+
files: 1,
|
|
2525
|
+
file_names: ['receipt.pdf'],
|
|
2526
|
+
target: '#file',
|
|
2527
|
+
matches_n: 1,
|
|
2528
|
+
match_level: 'exact',
|
|
2529
|
+
multiple: false,
|
|
2530
|
+
});
|
|
2531
|
+
const program = createProgram('', '');
|
|
2532
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'upload', '#file', file]);
|
|
2533
|
+
expect(browserState.page.uploadFiles).toHaveBeenCalledWith('#file', [file], {});
|
|
2534
|
+
expect(lastJsonLog()).toEqual({
|
|
2535
|
+
uploaded: true,
|
|
2536
|
+
files: 1,
|
|
2537
|
+
file_names: ['receipt.pdf'],
|
|
2538
|
+
target: '#file',
|
|
2539
|
+
matches_n: 1,
|
|
2540
|
+
match_level: 'exact',
|
|
2541
|
+
multiple: false,
|
|
2542
|
+
});
|
|
2543
|
+
});
|
|
2544
|
+
it('upload: rejects missing files before touching the page', async () => {
|
|
2545
|
+
const program = createProgram('', '');
|
|
2546
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'upload', '#file', '/tmp/opencli-missing-file']);
|
|
2547
|
+
expect(lastJsonLog().error.code).toBe('file_not_found');
|
|
2548
|
+
expect(browserState.page.uploadFiles).not.toHaveBeenCalled();
|
|
2549
|
+
expect(process.exitCode).toBeDefined();
|
|
2550
|
+
});
|
|
2551
|
+
it('drag: delegates to page.drag and forwards source/target nth options', async () => {
|
|
2552
|
+
browserState.page.drag.mockResolvedValueOnce({
|
|
2553
|
+
dragged: true,
|
|
2554
|
+
source: '.card',
|
|
2555
|
+
target: '.lane',
|
|
2556
|
+
source_matches_n: 3,
|
|
2557
|
+
target_matches_n: 2,
|
|
2558
|
+
source_match_level: 'exact',
|
|
2559
|
+
target_match_level: 'stable',
|
|
2560
|
+
});
|
|
2561
|
+
const program = createProgram('', '');
|
|
2562
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'drag', '.card', '.lane', '--from-nth', '2', '--to-nth', '1']);
|
|
2563
|
+
expect(browserState.page.drag).toHaveBeenCalledWith('.card', '.lane', { from: { nth: 2 }, to: { nth: 1 } });
|
|
2564
|
+
expect(lastJsonLog()).toEqual({
|
|
2565
|
+
dragged: true,
|
|
2566
|
+
source: '.card',
|
|
2567
|
+
target: '.lane',
|
|
2568
|
+
source_matches_n: 3,
|
|
2569
|
+
target_matches_n: 2,
|
|
2570
|
+
source_match_level: 'exact',
|
|
2571
|
+
target_match_level: 'stable',
|
|
2572
|
+
});
|
|
2573
|
+
});
|
|
2574
|
+
it('drag: rejects malformed --from-nth before touching the page', async () => {
|
|
2575
|
+
const program = createProgram('', '');
|
|
2576
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'drag', '.card', '.lane', '--from-nth', 'abc']);
|
|
2577
|
+
expect(lastJsonLog().error.code).toBe('usage_error');
|
|
2578
|
+
expect(browserState.page.drag).not.toHaveBeenCalled();
|
|
2579
|
+
expect(process.exitCode).toBeDefined();
|
|
2580
|
+
});
|
|
1866
2581
|
it('type: clicks, waits, then typeText — emits {typed, text, target, matches_n, match_level, autocomplete}', async () => {
|
|
1867
2582
|
browserState.page.click.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
|
|
1868
2583
|
browserState.page.typeText.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
|
|
@@ -1996,6 +2711,21 @@ describe('browser select command', () => {
|
|
|
1996
2711
|
expect(err.available).toEqual(['US', 'CA']);
|
|
1997
2712
|
expect(process.exitCode).toBeDefined();
|
|
1998
2713
|
});
|
|
2714
|
+
it('select: treats the first positional as option when using semantic locator flags', async () => {
|
|
2715
|
+
const evalMock = browserState.page.evaluate;
|
|
2716
|
+
evalMock
|
|
2717
|
+
.mockResolvedValueOnce({
|
|
2718
|
+
matches_n: 1,
|
|
2719
|
+
entries: [
|
|
2720
|
+
{ nth: 0, ref: 36, tag: 'select', role: 'combobox', text: 'Country', attrs: {}, visible: true },
|
|
2721
|
+
],
|
|
2722
|
+
})
|
|
2723
|
+
.mockResolvedValueOnce({ ok: true, matches_n: 1, match_level: 'exact' })
|
|
2724
|
+
.mockResolvedValueOnce({ selected: 'Uruguay' });
|
|
2725
|
+
const program = createProgram('', '');
|
|
2726
|
+
await program.parseAsync(['node', 'opencli', 'browser', 'select', '--label', 'Country', 'Uruguay']);
|
|
2727
|
+
expect(lastJsonLog()).toEqual({ selected: 'Uruguay', target: '36', matches_n: 1, match_level: 'exact' });
|
|
2728
|
+
});
|
|
1999
2729
|
it('surfaces selector_ambiguous from the resolver before calling selectResolvedJs', async () => {
|
|
2000
2730
|
browserState.page.evaluate.mockResolvedValueOnce({
|
|
2001
2731
|
ok: false,
|