@jackwener/opencli 1.7.3 → 1.7.5
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 +81 -59
- package/README.zh-CN.md +93 -67
- package/cli-manifest.json +5015 -2975
- package/clis/antigravity/serve.js +71 -25
- package/clis/baidu-scholar/search.js +87 -0
- package/clis/baidu-scholar/search.test.js +23 -0
- package/clis/bilibili/favorite.js +18 -13
- package/clis/binance/depth.js +3 -4
- package/clis/boss/utils.js +2 -3
- package/clis/chatgpt-app/ax.js +6 -3
- package/clis/deepseek/ask.js +74 -0
- package/clis/deepseek/history.js +25 -0
- package/clis/deepseek/new.js +20 -0
- package/clis/deepseek/read.js +22 -0
- package/clis/deepseek/status.js +24 -0
- package/clis/deepseek/utils.js +208 -0
- package/clis/douban/search.js +1 -0
- package/clis/douban/search.test.js +11 -0
- package/clis/douban/subject.js +20 -93
- package/clis/douban/subject.test.js +11 -0
- package/clis/douban/utils.js +250 -8
- package/clis/douban/utils.test.js +179 -4
- package/clis/doubao/utils.js +319 -130
- package/clis/doubao/utils.test.js +241 -2
- package/clis/eastmoney/_secid.js +78 -0
- package/clis/eastmoney/announcement.js +52 -0
- package/clis/eastmoney/convertible.js +73 -0
- package/clis/eastmoney/etf.js +65 -0
- package/clis/eastmoney/holders.js +78 -0
- package/clis/eastmoney/hot-rank.js +50 -0
- package/clis/eastmoney/hot-rank.test.js +59 -0
- package/clis/eastmoney/index-board.js +96 -0
- package/clis/eastmoney/kline.js +87 -0
- package/clis/eastmoney/kuaixun.js +54 -0
- package/clis/eastmoney/longhu.js +67 -0
- package/clis/eastmoney/money-flow.js +78 -0
- package/clis/eastmoney/northbound.js +57 -0
- package/clis/eastmoney/quote.js +107 -0
- package/clis/eastmoney/rank.js +94 -0
- package/clis/eastmoney/sectors.js +76 -0
- package/clis/google-scholar/search.js +58 -0
- package/clis/google-scholar/search.test.js +23 -0
- package/clis/gov-law/commands.test.js +39 -0
- package/clis/gov-law/recent.js +22 -0
- package/clis/gov-law/search.js +41 -0
- package/clis/gov-law/shared.js +51 -0
- package/clis/gov-policy/commands.test.js +27 -0
- package/clis/gov-policy/recent.js +47 -0
- package/clis/gov-policy/search.js +48 -0
- package/clis/grok/image.test.ts +107 -0
- package/clis/grok/image.ts +356 -0
- package/clis/nowcoder/companies.js +23 -0
- package/clis/nowcoder/creators.js +27 -0
- package/clis/nowcoder/detail.js +61 -0
- package/clis/nowcoder/experience.js +36 -0
- package/clis/nowcoder/hot.js +24 -0
- package/clis/nowcoder/jobs.js +21 -0
- package/clis/nowcoder/notifications.js +29 -0
- package/clis/nowcoder/papers.js +40 -0
- package/clis/nowcoder/practice.js +37 -0
- package/clis/nowcoder/recommend.js +30 -0
- package/clis/nowcoder/referral.js +39 -0
- package/clis/nowcoder/salary.js +40 -0
- package/clis/nowcoder/search.js +49 -0
- package/clis/nowcoder/suggest.js +33 -0
- package/clis/nowcoder/topics.js +27 -0
- package/clis/nowcoder/trending.js +25 -0
- package/clis/tdx/hot-rank.js +47 -0
- package/clis/tdx/hot-rank.test.js +59 -0
- package/clis/ths/hot-rank.js +49 -0
- package/clis/ths/hot-rank.test.js +64 -0
- package/clis/twitter/bookmarks.js +2 -1
- package/clis/twitter/list-add.js +337 -0
- package/clis/twitter/list-add.test.js +15 -0
- package/clis/twitter/list-remove.js +297 -0
- package/clis/twitter/list-remove.test.js +14 -0
- package/clis/twitter/list-tweets.js +185 -0
- package/clis/twitter/list-tweets.test.js +108 -0
- package/clis/twitter/lists.js +134 -47
- package/clis/twitter/lists.test.js +105 -38
- package/clis/uiverse/_shared.js +368 -0
- package/clis/uiverse/_shared.test.js +55 -0
- package/clis/uiverse/code.js +47 -0
- package/clis/uiverse/preview.js +71 -0
- package/clis/wanfang/search.js +66 -0
- package/clis/wanfang/search.test.js +23 -0
- package/clis/web/read.js +1 -1
- package/clis/weixin/download.js +3 -2
- package/clis/xiaohongshu/comments.js +2 -2
- package/clis/xiaohongshu/comments.test.js +46 -25
- package/clis/xiaohongshu/download.js +6 -7
- package/clis/xiaohongshu/download.test.js +17 -5
- package/clis/xiaohongshu/note-helpers.js +46 -12
- package/clis/xiaohongshu/note.js +3 -5
- package/clis/xiaohongshu/note.test.js +52 -25
- package/clis/xiaohongshu/publish.js +149 -28
- package/clis/xiaohongshu/publish.test.js +319 -6
- package/clis/xiaoyuzhou/auth.js +303 -0
- package/clis/xiaoyuzhou/auth.test.js +124 -0
- package/clis/xiaoyuzhou/download.js +53 -0
- package/clis/xiaoyuzhou/download.test.js +135 -0
- package/clis/xiaoyuzhou/episode.js +9 -4
- package/clis/xiaoyuzhou/podcast-episodes.js +15 -11
- package/clis/xiaoyuzhou/podcast.js +9 -4
- package/clis/xiaoyuzhou/transcript.js +76 -0
- package/clis/xiaoyuzhou/transcript.test.js +195 -0
- package/clis/xiaoyuzhou/utils.js +0 -40
- package/clis/xiaoyuzhou/utils.test.js +15 -75
- package/clis/youtube/feed.js +120 -0
- package/clis/youtube/history.js +118 -0
- package/clis/youtube/like.js +62 -0
- package/clis/youtube/playlist.js +97 -0
- package/clis/youtube/subscribe.js +71 -0
- package/clis/youtube/subscriptions.js +57 -0
- package/clis/youtube/unlike.js +62 -0
- package/clis/youtube/unsubscribe.js +71 -0
- package/clis/youtube/utils.js +122 -0
- package/clis/youtube/utils.test.js +32 -1
- package/clis/youtube/watch-later.js +76 -0
- package/clis/zsxq/dynamics.js +1 -1
- package/clis/zsxq/utils.js +6 -3
- package/clis/zsxq/utils.test.js +31 -0
- package/dist/src/browser/base-page.d.ts +1 -1
- package/dist/src/browser/base-page.js +25 -5
- package/dist/src/browser/bridge.d.ts +3 -0
- package/dist/src/browser/bridge.js +52 -15
- package/dist/src/browser/cdp.js +2 -1
- package/dist/src/browser/daemon-client.d.ts +7 -4
- package/dist/src/browser/daemon-client.js +6 -1
- package/dist/src/browser/daemon-client.test.js +40 -1
- package/dist/src/browser/dom-snapshot.js +20 -3
- package/dist/src/browser/page.d.ts +18 -5
- package/dist/src/browser/page.js +96 -15
- package/dist/src/browser/page.test.js +158 -1
- package/dist/src/browser/target-errors.d.ts +23 -0
- package/dist/src/browser/target-errors.js +29 -0
- package/dist/src/browser/target-errors.test.js +61 -0
- package/dist/src/browser/target-resolver.d.ts +57 -0
- package/dist/src/browser/target-resolver.js +298 -0
- package/dist/src/browser/target-resolver.test.js +43 -0
- package/dist/src/browser.test.js +38 -1
- package/dist/src/cli.js +272 -187
- package/dist/src/cli.test.js +167 -90
- package/dist/src/commanderAdapter.d.ts +0 -1
- package/dist/src/commanderAdapter.js +2 -16
- package/dist/src/commanderAdapter.test.js +1 -1
- package/dist/src/commands/daemon.d.ts +4 -2
- package/dist/src/commands/daemon.js +22 -2
- package/dist/src/commands/daemon.test.js +65 -2
- package/dist/src/completion-shared.js +2 -5
- package/dist/src/daemon.js +10 -0
- package/dist/src/doctor.d.ts +1 -0
- package/dist/src/doctor.js +32 -9
- package/dist/src/doctor.test.js +28 -12
- package/dist/src/download/article-download.d.ts +1 -0
- package/dist/src/download/article-download.js +3 -0
- package/dist/src/download/article-download.test.js +39 -0
- package/dist/src/external-clis.yaml +2 -2
- package/dist/src/logger.d.ts +2 -2
- package/dist/src/logger.js +3 -3
- package/dist/src/output.js +1 -5
- package/dist/src/output.test.js +0 -21
- package/dist/src/pipeline/steps/transform.js +1 -1
- package/dist/src/pipeline/template.d.ts +1 -0
- package/dist/src/pipeline/template.js +11 -3
- package/dist/src/pipeline/template.test.js +3 -0
- package/dist/src/pipeline/transform.test.js +14 -0
- package/dist/src/plugin.d.ts +8 -9
- package/dist/src/plugin.js +24 -28
- package/dist/src/plugin.test.js +16 -60
- package/dist/src/registry.d.ts +1 -0
- package/dist/src/registry.js +3 -2
- package/dist/src/registry.test.js +22 -0
- package/dist/src/types.d.ts +15 -6
- package/package.json +1 -1
- package/clis/twitter/lists-parser.js +0 -77
- package/clis/twitter/lists.d.ts +0 -5
- package/dist/src/cascade.d.ts +0 -46
- package/dist/src/cascade.js +0 -135
- package/dist/src/explore.d.ts +0 -99
- package/dist/src/explore.js +0 -402
- package/dist/src/generate-verified.d.ts +0 -105
- package/dist/src/generate-verified.js +0 -696
- package/dist/src/generate-verified.test.js +0 -925
- package/dist/src/generate.d.ts +0 -46
- package/dist/src/generate.js +0 -117
- package/dist/src/record.d.ts +0 -96
- package/dist/src/record.js +0 -657
- package/dist/src/record.test.js +0 -293
- package/dist/src/skill-generate.d.ts +0 -30
- package/dist/src/skill-generate.js +0 -75
- package/dist/src/skill-generate.test.js +0 -173
- package/dist/src/synthesize.d.ts +0 -97
- package/dist/src/synthesize.js +0 -208
- /package/dist/src/{generate-verified.test.d.ts → browser/target-errors.test.d.ts} +0 -0
- /package/dist/src/{record.test.d.ts → browser/target-resolver.test.d.ts} +0 -0
- /package/dist/src/{skill-generate.test.d.ts → download/article-download.test.d.ts} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { extractJsonAssignmentFromHtml, prepareYoutubeApiPage } from './utils.js';
|
|
2
|
+
import { extractJsonAssignmentFromHtml, extractSubscriptionChannel, prepareYoutubeApiPage } from './utils.js';
|
|
3
3
|
describe('youtube utils', () => {
|
|
4
4
|
it('extractJsonAssignmentFromHtml parses bootstrap objects with nested braces in strings', () => {
|
|
5
5
|
const html = `
|
|
@@ -34,4 +34,35 @@ describe('youtube utils', () => {
|
|
|
34
34
|
expect(page.goto).toHaveBeenCalledWith('https://www.youtube.com', { waitUntil: 'none' });
|
|
35
35
|
expect(page.wait).toHaveBeenCalledWith(2);
|
|
36
36
|
});
|
|
37
|
+
it('extractSubscriptionChannel prefers explicit handle and subscriber count fields', () => {
|
|
38
|
+
expect(extractSubscriptionChannel({
|
|
39
|
+
title: { simpleText: 'OpenAI' },
|
|
40
|
+
channelHandleText: { runs: [{ text: '@openai' }] },
|
|
41
|
+
subscriberCountText: { simpleText: '1.23M subscribers' },
|
|
42
|
+
videoCountText: { simpleText: '123 videos' },
|
|
43
|
+
navigationEndpoint: { browseEndpoint: { canonicalBaseUrl: '/channel/UC123' } },
|
|
44
|
+
channelId: 'UC123',
|
|
45
|
+
})).toEqual({
|
|
46
|
+
name: 'OpenAI',
|
|
47
|
+
handle: '@openai',
|
|
48
|
+
subscribers: '1.23M subscribers',
|
|
49
|
+
url: 'https://www.youtube.com/channel/UC123',
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
it('extractSubscriptionChannel falls back when handle/count fields are overloaded', () => {
|
|
53
|
+
expect(extractSubscriptionChannel({
|
|
54
|
+
title: {
|
|
55
|
+
runs: [{ text: 'OpenAI' }],
|
|
56
|
+
},
|
|
57
|
+
subscriberCountText: { simpleText: '@openai' },
|
|
58
|
+
videoCountText: { simpleText: '1.23M subscribers' },
|
|
59
|
+
navigationEndpoint: { browseEndpoint: { canonicalBaseUrl: '/@openai' } },
|
|
60
|
+
channelId: 'UC123',
|
|
61
|
+
})).toEqual({
|
|
62
|
+
name: 'OpenAI',
|
|
63
|
+
handle: '@openai',
|
|
64
|
+
subscribers: '1.23M subscribers',
|
|
65
|
+
url: 'https://www.youtube.com/@openai',
|
|
66
|
+
});
|
|
67
|
+
});
|
|
37
68
|
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YouTube watch-later — the user's Watch Later queue.
|
|
3
|
+
* Navigates to /playlist?list=WL and reads ytInitialData directly.
|
|
4
|
+
*/
|
|
5
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
6
|
+
import { FETCH_BROWSE_FN, extractPlaylistVideos } from './utils.js';
|
|
7
|
+
import { CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
8
|
+
|
|
9
|
+
cli({
|
|
10
|
+
site: 'youtube',
|
|
11
|
+
name: 'watch-later',
|
|
12
|
+
description: 'Get your YouTube Watch Later queue',
|
|
13
|
+
domain: 'www.youtube.com',
|
|
14
|
+
strategy: Strategy.COOKIE,
|
|
15
|
+
args: [
|
|
16
|
+
{ name: 'limit', type: 'int', default: 50, help: 'Max videos to return (default 50, max 200)' },
|
|
17
|
+
],
|
|
18
|
+
columns: ['rank', 'title', 'channel', 'duration', 'views', 'published', 'url'],
|
|
19
|
+
func: async (page, kwargs) => {
|
|
20
|
+
const limit = Math.min(kwargs.limit || 50, 200);
|
|
21
|
+
await page.goto('https://www.youtube.com/playlist?list=WL');
|
|
22
|
+
await page.wait(3);
|
|
23
|
+
const data = await page.evaluate(`
|
|
24
|
+
(async () => {
|
|
25
|
+
const d = window.ytInitialData;
|
|
26
|
+
if (!d) return { error: 'YouTube data not found — are you logged in?' };
|
|
27
|
+
|
|
28
|
+
const limit = ${limit};
|
|
29
|
+
const cfg = window.ytcfg?.data_ || {};
|
|
30
|
+
const apiKey = cfg.INNERTUBE_API_KEY;
|
|
31
|
+
const context = cfg.INNERTUBE_CONTEXT;
|
|
32
|
+
|
|
33
|
+
const header = d.header?.playlistHeaderRenderer;
|
|
34
|
+
const title = header?.title?.simpleText || 'Watch Later';
|
|
35
|
+
const stats = (header?.stats || [])
|
|
36
|
+
.map(s => s.runs?.map(r => r.text)?.join('') || s.simpleText || '')
|
|
37
|
+
.filter(Boolean);
|
|
38
|
+
|
|
39
|
+
const tabs = d.contents?.twoColumnBrowseResultsRenderer?.tabs || [];
|
|
40
|
+
let listContents = tabs[0]?.tabRenderer?.content?.sectionListRenderer?.contents?.[0]?.itemSectionRenderer?.contents?.[0]?.playlistVideoListRenderer?.contents || [];
|
|
41
|
+
|
|
42
|
+
${FETCH_BROWSE_FN}
|
|
43
|
+
|
|
44
|
+
const extractVideos = ${extractPlaylistVideos.toString()};
|
|
45
|
+
|
|
46
|
+
let videos = extractVideos(listContents);
|
|
47
|
+
|
|
48
|
+
let contItem = listContents[listContents.length - 1];
|
|
49
|
+
while (videos.length < limit && contItem?.continuationItemRenderer && apiKey && context) {
|
|
50
|
+
const token = contItem.continuationItemRenderer?.continuationEndpoint?.continuationCommand?.token;
|
|
51
|
+
if (!token) break;
|
|
52
|
+
const contData = await fetchBrowse(apiKey, { context, continuation: token });
|
|
53
|
+
if (contData.error) break;
|
|
54
|
+
const newItems = contData.onResponseReceivedActions?.[0]?.appendContinuationItemsAction?.continuationItems || [];
|
|
55
|
+
if (!newItems.length) break;
|
|
56
|
+
videos = videos.concat(extractVideos(newItems));
|
|
57
|
+
contItem = newItems[newItems.length - 1];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { title, stats, videos: videos.slice(0, limit) };
|
|
61
|
+
})()
|
|
62
|
+
`);
|
|
63
|
+
if (!data || typeof data !== 'object') {
|
|
64
|
+
throw new CommandExecutionError('Failed to fetch Watch Later — make sure you are logged into YouTube');
|
|
65
|
+
}
|
|
66
|
+
if (data.error) {
|
|
67
|
+
throw new CommandExecutionError(String(data.error));
|
|
68
|
+
}
|
|
69
|
+
if (!data.videos?.length) {
|
|
70
|
+
throw new EmptyResultError('youtube watch-later');
|
|
71
|
+
}
|
|
72
|
+
const statsStr = (data.stats || []).join(' | ');
|
|
73
|
+
process.stderr.write(`${data.title} ${statsStr}\n`);
|
|
74
|
+
return data.videos;
|
|
75
|
+
},
|
|
76
|
+
});
|
package/clis/zsxq/dynamics.js
CHANGED
|
@@ -37,7 +37,7 @@ cli({
|
|
|
37
37
|
time: d.create_time || topic.create_time || '',
|
|
38
38
|
group: topic.group?.name || '',
|
|
39
39
|
author: getTopicAuthor(topic),
|
|
40
|
-
title: getTopicText(topic)
|
|
40
|
+
title: getTopicText(topic),
|
|
41
41
|
comments: topic.comments_count ?? 0,
|
|
42
42
|
likes: topic.likes_count ?? 0,
|
|
43
43
|
url: getTopicUrl(topic.topic_id),
|
package/clis/zsxq/utils.js
CHANGED
|
@@ -186,8 +186,11 @@ export function getTopicAuthor(topic) {
|
|
|
186
186
|
'');
|
|
187
187
|
}
|
|
188
188
|
export function getTopicText(topic) {
|
|
189
|
+
const title = (topic.title || '').replace(/\s+/g, ' ').trim();
|
|
190
|
+
return title || getTopicContent(topic);
|
|
191
|
+
}
|
|
192
|
+
export function getTopicContent(topic) {
|
|
189
193
|
const primary = [
|
|
190
|
-
topic.title,
|
|
191
194
|
topic.talk?.text,
|
|
192
195
|
topic.question?.text,
|
|
193
196
|
topic.answer?.text,
|
|
@@ -218,8 +221,8 @@ export function toTopicRow(topic) {
|
|
|
218
221
|
type: topic.type || '',
|
|
219
222
|
group: topic.group?.name || '',
|
|
220
223
|
author: getTopicAuthor(topic),
|
|
221
|
-
title: getTopicText(topic)
|
|
222
|
-
content:
|
|
224
|
+
title: getTopicText(topic),
|
|
225
|
+
content: getTopicContent(topic),
|
|
223
226
|
comments: topic.comments_count ?? comments.length ?? 0,
|
|
224
227
|
likes: topic.likes_count ?? 0,
|
|
225
228
|
readers: topic.readers_count ?? topic.reading_count ?? 0,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { getTopicText, toTopicRow } from './utils.js';
|
|
3
|
+
|
|
4
|
+
describe('zsxq utils', () => {
|
|
5
|
+
it('keeps title and content separate when both fields exist', () => {
|
|
6
|
+
const topic = {
|
|
7
|
+
topic_id: '123',
|
|
8
|
+
title: 'A full title that should not be truncated',
|
|
9
|
+
talk: { text: 'This is the full body text.' },
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
expect(getTopicText(topic)).toBe('A full title that should not be truncated');
|
|
13
|
+
expect(toTopicRow(topic)).toMatchObject({
|
|
14
|
+
title: 'A full title that should not be truncated',
|
|
15
|
+
content: 'This is the full body text.',
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('falls back to body text for title when explicit title is absent', () => {
|
|
20
|
+
const topic = {
|
|
21
|
+
topic_id: '456',
|
|
22
|
+
talk: { text: 'Body-only topic text should still appear as the title preview.' },
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
expect(getTopicText(topic)).toBe('Body-only topic text should still appear as the title preview.');
|
|
26
|
+
expect(toTopicRow(topic)).toMatchObject({
|
|
27
|
+
title: 'Body-only topic text should still appear as the title preview.',
|
|
28
|
+
content: 'Body-only topic text should still appear as the title preview.',
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -33,7 +33,7 @@ export declare abstract class BasePage implements IPage {
|
|
|
33
33
|
}): Promise<BrowserCookie[]>;
|
|
34
34
|
abstract screenshot(options?: ScreenshotOptions): Promise<string>;
|
|
35
35
|
abstract tabs(): Promise<unknown[]>;
|
|
36
|
-
abstract selectTab(
|
|
36
|
+
abstract selectTab(target: number | string): Promise<void>;
|
|
37
37
|
click(ref: string): Promise<void>;
|
|
38
38
|
/** Override in subclasses with CDP native click support */
|
|
39
39
|
protected tryNativeClick(_x: number, _y: number): Promise<boolean>;
|
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
* Subclasses implement the transport-specific methods: goto, evaluate,
|
|
9
9
|
* getCookies, screenshot, tabs, etc.
|
|
10
10
|
*/
|
|
11
|
-
import { generateSnapshotJs,
|
|
12
|
-
import {
|
|
11
|
+
import { generateSnapshotJs, getFormStateJs } from './dom-snapshot.js';
|
|
12
|
+
import { pressKeyJs, waitForTextJs, waitForCaptureJs, waitForSelectorJs, scrollJs, autoScrollJs, networkRequestsJs, waitForDomStableJs, } from './dom-helpers.js';
|
|
13
|
+
import { resolveTargetJs, clickResolvedJs, typeResolvedJs, scrollResolvedJs } from './target-resolver.js';
|
|
14
|
+
import { TargetError } from './target-errors.js';
|
|
13
15
|
import { formatSnapshot } from '../snapshotFormatter.js';
|
|
14
16
|
export class BasePage {
|
|
15
17
|
_lastUrl = null;
|
|
@@ -36,7 +38,13 @@ export class BasePage {
|
|
|
36
38
|
}
|
|
37
39
|
// ── Shared DOM helper implementations ──
|
|
38
40
|
async click(ref) {
|
|
39
|
-
|
|
41
|
+
// Phase 1: Resolve target with fingerprint verification
|
|
42
|
+
const resolution = await this.evaluate(resolveTargetJs(ref));
|
|
43
|
+
if (!resolution.ok) {
|
|
44
|
+
throw new TargetError(resolution);
|
|
45
|
+
}
|
|
46
|
+
// Phase 2: Execute click on resolved element
|
|
47
|
+
const result = await this.evaluate(clickResolvedJs());
|
|
40
48
|
// Backwards compat: old format returned 'clicked' string
|
|
41
49
|
if (typeof result === 'string' || result == null)
|
|
42
50
|
return;
|
|
@@ -56,13 +64,25 @@ export class BasePage {
|
|
|
56
64
|
return false;
|
|
57
65
|
}
|
|
58
66
|
async typeText(ref, text) {
|
|
59
|
-
|
|
67
|
+
// Phase 1: Resolve target with fingerprint verification
|
|
68
|
+
const resolution = await this.evaluate(resolveTargetJs(ref));
|
|
69
|
+
if (!resolution.ok) {
|
|
70
|
+
throw new TargetError(resolution);
|
|
71
|
+
}
|
|
72
|
+
// Phase 2: Execute type on resolved element
|
|
73
|
+
await this.evaluate(typeResolvedJs(text));
|
|
60
74
|
}
|
|
61
75
|
async pressKey(key) {
|
|
62
76
|
await this.evaluate(pressKeyJs(key));
|
|
63
77
|
}
|
|
64
78
|
async scrollTo(ref) {
|
|
65
|
-
|
|
79
|
+
// Phase 1: Resolve target with fingerprint verification
|
|
80
|
+
const resolution = await this.evaluate(resolveTargetJs(ref));
|
|
81
|
+
if (!resolution.ok) {
|
|
82
|
+
throw new TargetError(resolution);
|
|
83
|
+
}
|
|
84
|
+
// Phase 2: Scroll to resolved element
|
|
85
|
+
return this.evaluate(scrollResolvedJs());
|
|
66
86
|
}
|
|
67
87
|
async getFormState() {
|
|
68
88
|
return (await this.evaluate(getFormStateJs()));
|
|
@@ -15,9 +15,12 @@ export declare class BrowserBridge implements IBrowserFactory {
|
|
|
15
15
|
connect(opts?: {
|
|
16
16
|
timeout?: number;
|
|
17
17
|
workspace?: string;
|
|
18
|
+
idleTimeout?: number;
|
|
18
19
|
}): Promise<IPage>;
|
|
19
20
|
close(): Promise<void>;
|
|
20
21
|
private _ensureDaemon;
|
|
22
|
+
/** Poll until daemon is fully stopped (port released). */
|
|
23
|
+
private _waitForDaemonStop;
|
|
21
24
|
/** Poll getDaemonHealth() until state is 'ready' or deadline is reached. */
|
|
22
25
|
private _pollUntilReady;
|
|
23
26
|
}
|
|
@@ -6,9 +6,10 @@ import { fileURLToPath } from 'node:url';
|
|
|
6
6
|
import * as path from 'node:path';
|
|
7
7
|
import * as fs from 'node:fs';
|
|
8
8
|
import { Page } from './page.js';
|
|
9
|
-
import { getDaemonHealth } from './daemon-client.js';
|
|
9
|
+
import { getDaemonHealth, requestDaemonShutdown } from './daemon-client.js';
|
|
10
10
|
import { DEFAULT_DAEMON_PORT } from '../constants.js';
|
|
11
11
|
import { BrowserConnectError } from '../errors.js';
|
|
12
|
+
import { PKG_VERSION } from '../version.js';
|
|
12
13
|
const DAEMON_SPAWN_TIMEOUT = 10000; // 10s to wait for daemon + extension
|
|
13
14
|
/**
|
|
14
15
|
* Browser factory: manages daemon lifecycle and provides IPage instances.
|
|
@@ -32,7 +33,7 @@ export class BrowserBridge {
|
|
|
32
33
|
this._state = 'connecting';
|
|
33
34
|
try {
|
|
34
35
|
await this._ensureDaemon(opts.timeout);
|
|
35
|
-
this._page = new Page(opts.workspace);
|
|
36
|
+
this._page = new Page(opts.workspace, opts.idleTimeout);
|
|
36
37
|
this._state = 'connected';
|
|
37
38
|
return this._page;
|
|
38
39
|
}
|
|
@@ -57,18 +58,42 @@ export class BrowserBridge {
|
|
|
57
58
|
// Fast path: everything ready
|
|
58
59
|
if (health.state === 'ready')
|
|
59
60
|
return;
|
|
60
|
-
// Daemon running but no extension
|
|
61
|
+
// Daemon running but no extension
|
|
61
62
|
if (health.state === 'no-extension') {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
// Detect stale daemon: version mismatch OR missing daemonVersion (pre-version daemon)
|
|
64
|
+
const daemonVersion = health.status?.daemonVersion;
|
|
65
|
+
const isStale = !daemonVersion || daemonVersion !== PKG_VERSION;
|
|
66
|
+
if (isStale) {
|
|
67
|
+
// Stale daemon — restart it so extension gets a fresh WebSocket endpoint
|
|
68
|
+
const reason = daemonVersion
|
|
69
|
+
? `v${daemonVersion} ≠ v${PKG_VERSION}`
|
|
70
|
+
: `pre-version daemon, CLI is v${PKG_VERSION}`;
|
|
71
|
+
if (process.env.OPENCLI_VERBOSE || process.stderr.isTTY) {
|
|
72
|
+
process.stderr.write(`⚠️ Stale daemon detected (${reason}). Restarting...\n`);
|
|
73
|
+
}
|
|
74
|
+
const shutdownAccepted = await requestDaemonShutdown();
|
|
75
|
+
const portReleased = shutdownAccepted && await this._waitForDaemonStop(3000);
|
|
76
|
+
if (!portReleased) {
|
|
77
|
+
// Stale daemon replacement failed — don't blindly spawn on an occupied port
|
|
78
|
+
throw new BrowserConnectError('Stale daemon could not be replaced', `A stale daemon (${reason}) is running but did not shut down.\n` +
|
|
79
|
+
' Run manually: opencli daemon stop && opencli doctor', 'daemon-not-running');
|
|
80
|
+
}
|
|
81
|
+
// Port released — fall through to spawn a fresh daemon
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
// Same version — wait for extension to connect
|
|
85
|
+
if (process.env.OPENCLI_VERBOSE || process.stderr.isTTY) {
|
|
86
|
+
process.stderr.write('⏳ Waiting for Chrome/Chromium extension to connect...\n');
|
|
87
|
+
process.stderr.write(' Make sure Chrome or Chromium is open and the OpenCLI extension is enabled.\n');
|
|
88
|
+
}
|
|
89
|
+
if (await this._pollUntilReady(timeoutMs))
|
|
90
|
+
return;
|
|
91
|
+
throw new BrowserConnectError('Browser Bridge extension not connected', 'Make sure Chrome/Chromium is open and the extension is enabled.\n' +
|
|
92
|
+
'If the extension is installed, try: opencli daemon stop && opencli doctor\n' +
|
|
93
|
+
'If not installed:\n' +
|
|
94
|
+
' 1. Download: https://github.com/jackwener/opencli/releases\n' +
|
|
95
|
+
' 2. Open chrome://extensions → Developer Mode → Load unpacked', 'extension-not-connected');
|
|
65
96
|
}
|
|
66
|
-
if (await this._pollUntilReady(timeoutMs))
|
|
67
|
-
return;
|
|
68
|
-
throw new BrowserConnectError('Browser Bridge extension not connected', 'Install the Browser Bridge:\n' +
|
|
69
|
-
' 1. Download: https://github.com/jackwener/opencli/releases\n' +
|
|
70
|
-
' 2. In Chrome or Chromium, open chrome://extensions → Developer Mode → Load unpacked\n' +
|
|
71
|
-
' Then run: opencli doctor', 'extension-not-connected');
|
|
72
97
|
}
|
|
73
98
|
// No daemon — spawn one
|
|
74
99
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -94,13 +119,25 @@ export class BrowserBridge {
|
|
|
94
119
|
return;
|
|
95
120
|
const finalHealth = await getDaemonHealth();
|
|
96
121
|
if (finalHealth.state === 'no-extension') {
|
|
97
|
-
throw new BrowserConnectError('Browser Bridge extension not connected', '
|
|
122
|
+
throw new BrowserConnectError('Browser Bridge extension not connected', 'Make sure Chrome/Chromium is open and the extension is enabled.\n' +
|
|
123
|
+
'If the extension is installed, try: opencli daemon stop && opencli doctor\n' +
|
|
124
|
+
'If not installed:\n' +
|
|
98
125
|
' 1. Download: https://github.com/jackwener/opencli/releases\n' +
|
|
99
|
-
' 2.
|
|
100
|
-
' Then run: opencli doctor', 'extension-not-connected');
|
|
126
|
+
' 2. Open chrome://extensions → Developer Mode → Load unpacked', 'extension-not-connected');
|
|
101
127
|
}
|
|
102
128
|
throw new BrowserConnectError('Failed to start opencli daemon', `Try running manually:\n node ${daemonPath}\nMake sure port ${DEFAULT_DAEMON_PORT} is available.`, 'daemon-not-running');
|
|
103
129
|
}
|
|
130
|
+
/** Poll until daemon is fully stopped (port released). */
|
|
131
|
+
async _waitForDaemonStop(timeoutMs) {
|
|
132
|
+
const deadline = Date.now() + timeoutMs;
|
|
133
|
+
while (Date.now() < deadline) {
|
|
134
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
135
|
+
const h = await getDaemonHealth();
|
|
136
|
+
if (h.state === 'stopped')
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
104
141
|
/** Poll getDaemonHealth() until state is 'ready' or deadline is reached. */
|
|
105
142
|
async _pollUntilReady(timeoutMs) {
|
|
106
143
|
const deadline = Date.now() + timeoutMs;
|
package/dist/src/browser/cdp.js
CHANGED
|
@@ -255,6 +255,7 @@ class CDPPage extends BasePage {
|
|
|
255
255
|
});
|
|
256
256
|
this._networkCapturing = true;
|
|
257
257
|
}
|
|
258
|
+
return true;
|
|
258
259
|
}
|
|
259
260
|
async readNetworkCapture() {
|
|
260
261
|
// Await all in-flight body fetches so entries have responsePreview populated
|
|
@@ -295,7 +296,7 @@ class CDPPage extends BasePage {
|
|
|
295
296
|
async tabs() {
|
|
296
297
|
return [];
|
|
297
298
|
}
|
|
298
|
-
async selectTab(
|
|
299
|
+
async selectTab(_target) {
|
|
299
300
|
// Not supported in direct CDP mode
|
|
300
301
|
}
|
|
301
302
|
}
|
|
@@ -6,11 +6,9 @@
|
|
|
6
6
|
import type { BrowserSessionInfo } from '../types.js';
|
|
7
7
|
export interface DaemonCommand {
|
|
8
8
|
id: string;
|
|
9
|
-
action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'sessions' | 'set-file-input' | 'insert-text' | 'bind-current' | 'network-capture-start' | 'network-capture-read' | 'cdp';
|
|
10
|
-
/** Target page identity (targetId). Cross-layer contract
|
|
9
|
+
action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'sessions' | 'set-file-input' | 'insert-text' | 'bind-current' | 'network-capture-start' | 'network-capture-read' | 'cdp' | 'frames';
|
|
10
|
+
/** Target page identity (targetId). Cross-layer contract with the extension. */
|
|
11
11
|
page?: string;
|
|
12
|
-
/** @deprecated Legacy tab ID — use `page` (targetId) instead. */
|
|
13
|
-
tabId?: number;
|
|
14
12
|
code?: string;
|
|
15
13
|
workspace?: string;
|
|
16
14
|
url?: string;
|
|
@@ -34,6 +32,10 @@ export interface DaemonCommand {
|
|
|
34
32
|
cdpParams?: Record<string, unknown>;
|
|
35
33
|
/** When true, automation windows are created in the foreground */
|
|
36
34
|
windowFocused?: boolean;
|
|
35
|
+
/** Custom idle timeout in seconds for this workspace session. Overrides the default. */
|
|
36
|
+
idleTimeout?: number;
|
|
37
|
+
/** Frame index for cross-frame operations (0-based, from 'frames' action) */
|
|
38
|
+
frameIndex?: number;
|
|
37
39
|
}
|
|
38
40
|
export interface DaemonResult {
|
|
39
41
|
id: string;
|
|
@@ -47,6 +49,7 @@ export interface DaemonStatus {
|
|
|
47
49
|
ok: boolean;
|
|
48
50
|
pid: number;
|
|
49
51
|
uptime: number;
|
|
52
|
+
daemonVersion?: string;
|
|
50
53
|
extensionConnected: boolean;
|
|
51
54
|
extensionVersion?: string;
|
|
52
55
|
extensionCompatRange?: string;
|
|
@@ -11,7 +11,7 @@ const DAEMON_URL = `http://127.0.0.1:${DAEMON_PORT}`;
|
|
|
11
11
|
const OPENCLI_HEADERS = { 'X-OpenCLI': '1' };
|
|
12
12
|
let _idCounter = 0;
|
|
13
13
|
function generateId() {
|
|
14
|
-
return `cmd_${Date.now()}_${++_idCounter}`;
|
|
14
|
+
return `cmd_${process.pid}_${Date.now()}_${++_idCounter}`;
|
|
15
15
|
}
|
|
16
16
|
async function requestDaemon(pathname, init) {
|
|
17
17
|
const { timeout = 2000, headers, ...rest } = init ?? {};
|
|
@@ -85,6 +85,11 @@ async function sendCommandRaw(action, params) {
|
|
|
85
85
|
});
|
|
86
86
|
const result = (await res.json());
|
|
87
87
|
if (!result.ok) {
|
|
88
|
+
const isDuplicateCommandId = res.status === 409
|
|
89
|
+
|| (result.error ?? '').includes('Duplicate command id');
|
|
90
|
+
if (isDuplicateCommandId && attempt < maxRetries) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
88
93
|
const advice = classifyBrowserError(new Error(result.error ?? ''));
|
|
89
94
|
if (advice.retryable && attempt < maxRetries) {
|
|
90
95
|
await sleep(advice.delayMs);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { fetchDaemonStatus, getDaemonHealth, requestDaemonShutdown, } from './daemon-client.js';
|
|
2
|
+
import { fetchDaemonStatus, getDaemonHealth, requestDaemonShutdown, sendCommand, } from './daemon-client.js';
|
|
3
3
|
describe('daemon-client', () => {
|
|
4
4
|
beforeEach(() => {
|
|
5
5
|
vi.stubGlobal('fetch', vi.fn());
|
|
@@ -78,4 +78,43 @@ describe('daemon-client', () => {
|
|
|
78
78
|
});
|
|
79
79
|
await expect(getDaemonHealth()).resolves.toEqual({ state: 'ready', status });
|
|
80
80
|
});
|
|
81
|
+
it('sendCommand includes the current pid in generated command ids', async () => {
|
|
82
|
+
vi.spyOn(Date, 'now').mockReturnValue(1_763_000_000_000);
|
|
83
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
84
|
+
status: 200,
|
|
85
|
+
json: () => Promise.resolve({ id: 'server', ok: true, data: 'ok' }),
|
|
86
|
+
});
|
|
87
|
+
await expect(sendCommand('exec', { code: '1 + 1' })).resolves.toBe('ok');
|
|
88
|
+
await expect(sendCommand('exec', { code: '2 + 2' })).resolves.toBe('ok');
|
|
89
|
+
const ids = vi.mocked(fetch).mock.calls.map(([, init]) => {
|
|
90
|
+
const body = JSON.parse(String(init?.body));
|
|
91
|
+
return body.id;
|
|
92
|
+
});
|
|
93
|
+
expect(ids).toHaveLength(2);
|
|
94
|
+
expect(ids[0]).toMatch(new RegExp(`^cmd_${process.pid}_1763000000000_\\d+$`));
|
|
95
|
+
expect(ids[1]).toMatch(new RegExp(`^cmd_${process.pid}_1763000000000_\\d+$`));
|
|
96
|
+
expect(ids[0]).not.toBe(ids[1]);
|
|
97
|
+
});
|
|
98
|
+
it('sendCommand retries with a new id when daemon reports a duplicate pending id', async () => {
|
|
99
|
+
vi.spyOn(Date, 'now').mockReturnValue(1_763_000_000_123);
|
|
100
|
+
const fetchMock = vi.mocked(fetch);
|
|
101
|
+
fetchMock
|
|
102
|
+
.mockResolvedValueOnce({
|
|
103
|
+
ok: false,
|
|
104
|
+
status: 409,
|
|
105
|
+
json: () => Promise.resolve({ ok: false, error: 'Duplicate command id already pending; retry' }),
|
|
106
|
+
})
|
|
107
|
+
.mockResolvedValueOnce({
|
|
108
|
+
ok: true,
|
|
109
|
+
status: 200,
|
|
110
|
+
json: () => Promise.resolve({ id: 'server', ok: true, data: 42 }),
|
|
111
|
+
});
|
|
112
|
+
await expect(sendCommand('exec', { code: '6 * 7' })).resolves.toBe(42);
|
|
113
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
114
|
+
const ids = fetchMock.mock.calls.map(([, init]) => {
|
|
115
|
+
const body = JSON.parse(String(init?.body));
|
|
116
|
+
return body.id;
|
|
117
|
+
});
|
|
118
|
+
expect(ids[0]).not.toBe(ids[1]);
|
|
119
|
+
});
|
|
81
120
|
});
|
|
@@ -575,7 +575,9 @@ export function generateSnapshotJs(opts = {}) {
|
|
|
575
575
|
const lines = [];
|
|
576
576
|
const hiddenInteractives = [];
|
|
577
577
|
const currentHashes = [];
|
|
578
|
+
const refIdentity = {};
|
|
578
579
|
let iframeCount = 0;
|
|
580
|
+
let crossOriginIndex = 0;
|
|
579
581
|
|
|
580
582
|
function walk(el, depth, parentPropagatingRect) {
|
|
581
583
|
if (depth > MAX_DEPTH) return false;
|
|
@@ -709,11 +711,20 @@ export function generateSnapshotJs(opts = {}) {
|
|
|
709
711
|
// Scroll marker
|
|
710
712
|
if (isScrollable && !interactive) line += '|scroll|';
|
|
711
713
|
|
|
712
|
-
// Interactive index + data-ref
|
|
714
|
+
// Interactive index + data-ref + fingerprint
|
|
713
715
|
if (interactive) {
|
|
714
716
|
interactiveIndex++;
|
|
715
717
|
if (ANNOTATE_REFS) el.setAttribute('data-opencli-ref', '' + interactiveIndex);
|
|
716
718
|
line += isScrollable ? '|scroll[' + interactiveIndex + ']|' : '[' + interactiveIndex + ']';
|
|
719
|
+
// Store fingerprint for stale-ref detection
|
|
720
|
+
refIdentity['' + interactiveIndex] = {
|
|
721
|
+
tag: tag,
|
|
722
|
+
role: el.getAttribute('role') || '',
|
|
723
|
+
text: (el.textContent || '').trim().slice(0, 30),
|
|
724
|
+
ariaLabel: el.getAttribute('aria-label') || '',
|
|
725
|
+
id: el.id || '',
|
|
726
|
+
testId: el.getAttribute('data-testid') || el.getAttribute('data-test') || '',
|
|
727
|
+
};
|
|
717
728
|
}
|
|
718
729
|
|
|
719
730
|
// Tag + attributes
|
|
@@ -747,7 +758,9 @@ export function generateSnapshotJs(opts = {}) {
|
|
|
747
758
|
const doc = el.contentDocument;
|
|
748
759
|
if (!doc || !doc.body) {
|
|
749
760
|
const attrs = serializeAttrs(el);
|
|
750
|
-
|
|
761
|
+
const frameLabel = '[F' + crossOriginIndex + ']';
|
|
762
|
+
lines.push(indent + '|iframe|' + frameLabel + '<iframe' + (attrs ? ' ' + attrs : '') + ' /> (cross-origin, use: opencli browser frames + browser eval --frame <index>)');
|
|
763
|
+
crossOriginIndex++;
|
|
751
764
|
return false;
|
|
752
765
|
}
|
|
753
766
|
iframeCount++;
|
|
@@ -760,7 +773,9 @@ export function generateSnapshotJs(opts = {}) {
|
|
|
760
773
|
return has;
|
|
761
774
|
} catch {
|
|
762
775
|
const attrs = serializeAttrs(el);
|
|
763
|
-
|
|
776
|
+
const frameLabel = '[F' + crossOriginIndex + ']';
|
|
777
|
+
lines.push(indent + '|iframe|' + frameLabel + '<iframe' + (attrs ? ' ' + attrs : '') + ' /> (blocked, use: opencli browser frames + browser eval --frame <index>)');
|
|
778
|
+
crossOriginIndex++;
|
|
764
779
|
return false;
|
|
765
780
|
}
|
|
766
781
|
}
|
|
@@ -797,6 +812,8 @@ export function generateSnapshotJs(opts = {}) {
|
|
|
797
812
|
|
|
798
813
|
// Store hashes on window for next diff snapshot
|
|
799
814
|
try { window.__opencli_prev_hashes = JSON.stringify(currentHashes); } catch {}
|
|
815
|
+
// Store ref identity map for stale-ref detection by target resolver
|
|
816
|
+
try { window.__opencli_ref_identity = refIdentity; } catch {}
|
|
800
817
|
|
|
801
818
|
return lines.join('\\n');
|
|
802
819
|
})()
|
|
@@ -15,9 +15,12 @@ import { BasePage } from './base-page.js';
|
|
|
15
15
|
*/
|
|
16
16
|
export declare class Page extends BasePage {
|
|
17
17
|
private readonly workspace;
|
|
18
|
-
|
|
18
|
+
private readonly _idleTimeout;
|
|
19
|
+
constructor(workspace?: string, idleTimeout?: number);
|
|
19
20
|
/** Active page identity (targetId), set after navigate and used in all subsequent commands */
|
|
20
21
|
private _page;
|
|
22
|
+
private _networkCaptureUnsupported;
|
|
23
|
+
private _networkCaptureWarned;
|
|
21
24
|
/** Helper: spread workspace into command params */
|
|
22
25
|
private _wsOpt;
|
|
23
26
|
/** Helper: spread workspace + page identity into command params */
|
|
@@ -28,8 +31,9 @@ export declare class Page extends BasePage {
|
|
|
28
31
|
}): Promise<void>;
|
|
29
32
|
/** Get the active page identity (targetId) */
|
|
30
33
|
getActivePage(): string | undefined;
|
|
31
|
-
/**
|
|
32
|
-
|
|
34
|
+
/** Bind this Page instance to a specific page identity (targetId). */
|
|
35
|
+
setActivePage(page?: string): void;
|
|
36
|
+
private _markUnsupportedNetworkCapture;
|
|
33
37
|
evaluate(js: string): Promise<unknown>;
|
|
34
38
|
getCookies(opts?: {
|
|
35
39
|
domain?: string;
|
|
@@ -38,12 +42,14 @@ export declare class Page extends BasePage {
|
|
|
38
42
|
/** Close the automation window in the extension */
|
|
39
43
|
closeWindow(): Promise<void>;
|
|
40
44
|
tabs(): Promise<unknown[]>;
|
|
41
|
-
|
|
45
|
+
newTab(url?: string): Promise<string | undefined>;
|
|
46
|
+
closeTab(target?: number | string): Promise<void>;
|
|
47
|
+
selectTab(target: number | string): Promise<void>;
|
|
42
48
|
/**
|
|
43
49
|
* Capture a screenshot via CDP Page.captureScreenshot.
|
|
44
50
|
*/
|
|
45
51
|
screenshot(options?: ScreenshotOptions): Promise<string>;
|
|
46
|
-
startNetworkCapture(pattern?: string): Promise<
|
|
52
|
+
startNetworkCapture(pattern?: string): Promise<boolean>;
|
|
47
53
|
readNetworkCapture(): Promise<unknown[]>;
|
|
48
54
|
/**
|
|
49
55
|
* Set local file paths on a file input element via CDP DOM.setFileInputFiles.
|
|
@@ -52,6 +58,13 @@ export declare class Page extends BasePage {
|
|
|
52
58
|
*/
|
|
53
59
|
setFileInput(files: string[], selector?: string): Promise<void>;
|
|
54
60
|
insertText(text: string): Promise<void>;
|
|
61
|
+
frames(): Promise<Array<{
|
|
62
|
+
index: number;
|
|
63
|
+
frameId: string;
|
|
64
|
+
url: string;
|
|
65
|
+
name: string;
|
|
66
|
+
}>>;
|
|
67
|
+
evaluateInFrame(js: string, frameIndex: number): Promise<unknown>;
|
|
55
68
|
cdp(method: string, params?: Record<string, unknown>): Promise<unknown>;
|
|
56
69
|
/** CDP native click fallback — called when JS el.click() fails */
|
|
57
70
|
protected tryNativeClick(x: number, y: number): Promise<boolean>;
|