@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
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified target resolver for browser actions.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the ad-hoc 4-strategy fallback in dom-helpers.ts with a
|
|
5
|
+
* principled resolution pipeline:
|
|
6
|
+
*
|
|
7
|
+
* 1. Input classification: numeric → ref path, CSS-like → CSS path
|
|
8
|
+
* 2. Ref path: lookup by data-opencli-ref, then verify fingerprint
|
|
9
|
+
* 3. CSS path: querySelectorAll + uniqueness check
|
|
10
|
+
* 4. Structured errors: stale_ref / ambiguous / not_found
|
|
11
|
+
*
|
|
12
|
+
* All JS is generated as strings for page.evaluate() — runs in the browser.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Generate JS that resolves a target to a single DOM element.
|
|
16
|
+
*
|
|
17
|
+
* Returns a JS expression that evaluates to:
|
|
18
|
+
* { ok: true, el: Element } — success (el is assigned to `__resolved`)
|
|
19
|
+
* { ok: false, code, message, hint, candidates } — structured error
|
|
20
|
+
*
|
|
21
|
+
* The resolved element is stored in `__resolved` for the caller to use.
|
|
22
|
+
*/
|
|
23
|
+
export function resolveTargetJs(ref) {
|
|
24
|
+
const safeRef = JSON.stringify(ref);
|
|
25
|
+
return `
|
|
26
|
+
(() => {
|
|
27
|
+
const ref = ${safeRef};
|
|
28
|
+
const identity = window.__opencli_ref_identity || {};
|
|
29
|
+
|
|
30
|
+
// ── Classify input ──
|
|
31
|
+
const isNumeric = /^\\d+$/.test(ref);
|
|
32
|
+
const isCssLike = !isNumeric && /^[a-zA-Z#.\\[]/.test(ref);
|
|
33
|
+
|
|
34
|
+
if (isNumeric) {
|
|
35
|
+
// ── Ref path ──
|
|
36
|
+
let el = document.querySelector('[data-opencli-ref="' + ref + '"]');
|
|
37
|
+
if (!el) el = document.querySelector('[data-ref="' + ref + '"]');
|
|
38
|
+
|
|
39
|
+
if (!el) {
|
|
40
|
+
return {
|
|
41
|
+
ok: false,
|
|
42
|
+
code: 'not_found',
|
|
43
|
+
message: 'ref=' + ref + ' not found in DOM',
|
|
44
|
+
hint: 'The element may have been removed. Re-run \`opencli browser state\` to get a fresh snapshot.',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── Fingerprint verification (identity vector) ──
|
|
49
|
+
const fp = identity[ref];
|
|
50
|
+
if (fp) {
|
|
51
|
+
const tag = el.tagName.toLowerCase();
|
|
52
|
+
const text = (el.textContent || '').trim().slice(0, 30);
|
|
53
|
+
const role = el.getAttribute('role') || '';
|
|
54
|
+
const ariaLabel = el.getAttribute('aria-label') || '';
|
|
55
|
+
const id = el.id || '';
|
|
56
|
+
const testId = el.getAttribute('data-testid') || el.getAttribute('data-test') || '';
|
|
57
|
+
|
|
58
|
+
// Hard fail: tag must always match
|
|
59
|
+
const tagMatch = fp.tag === tag;
|
|
60
|
+
|
|
61
|
+
// Soft signals: each non-empty stored field that mismatches counts against
|
|
62
|
+
var mismatches = 0;
|
|
63
|
+
var checks = 0;
|
|
64
|
+
if (fp.id) { checks++; if (fp.id !== id) mismatches++; }
|
|
65
|
+
if (fp.testId) { checks++; if (fp.testId !== testId) mismatches++; }
|
|
66
|
+
if (fp.ariaLabel) { checks++; if (fp.ariaLabel !== ariaLabel) mismatches++; }
|
|
67
|
+
if (fp.role) { checks++; if (fp.role !== role) mismatches++; }
|
|
68
|
+
if (fp.text) {
|
|
69
|
+
checks++;
|
|
70
|
+
// Text: allow prefix match (page text can grow), but empty current text never matches
|
|
71
|
+
if (!text || (!text.startsWith(fp.text) && !fp.text.startsWith(text))) mismatches++;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Stale if tag changed, or if any uniquely identifying field (id/testId) changed,
|
|
75
|
+
// or if majority of soft signals mismatch
|
|
76
|
+
var isStale = !tagMatch;
|
|
77
|
+
if (!isStale && checks > 0) {
|
|
78
|
+
// id and testId are strong identifiers — any mismatch on these is decisive
|
|
79
|
+
if (fp.id && fp.id !== id) isStale = true;
|
|
80
|
+
else if (fp.testId && fp.testId !== testId) isStale = true;
|
|
81
|
+
// For remaining signals, stale if more than half mismatch
|
|
82
|
+
else if (mismatches > checks / 2) isStale = true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (isStale) {
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
code: 'stale_ref',
|
|
89
|
+
message: 'ref=' + ref + ' was <' + fp.tag + '>' + (fp.text ? '"' + fp.text + '"' : '')
|
|
90
|
+
+ ' but now points to <' + tag + '>' + (text ? '"' + text.slice(0, 30) + '"' : ''),
|
|
91
|
+
hint: 'The page has changed since the last snapshot. Re-run \`opencli browser state\` to refresh.',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
window.__resolved = el;
|
|
97
|
+
return { ok: true };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (isCssLike) {
|
|
101
|
+
// ── CSS selector path ──
|
|
102
|
+
let matches;
|
|
103
|
+
try {
|
|
104
|
+
matches = document.querySelectorAll(ref);
|
|
105
|
+
} catch (e) {
|
|
106
|
+
return {
|
|
107
|
+
ok: false,
|
|
108
|
+
code: 'not_found',
|
|
109
|
+
message: 'Invalid CSS selector: ' + ref,
|
|
110
|
+
hint: 'Check the selector syntax. Use ref numbers from snapshot for reliable targeting.',
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (matches.length === 0) {
|
|
115
|
+
return {
|
|
116
|
+
ok: false,
|
|
117
|
+
code: 'not_found',
|
|
118
|
+
message: 'CSS selector "' + ref + '" matched 0 elements',
|
|
119
|
+
hint: 'The element may not exist or may be hidden. Re-run \`opencli browser state\` to check.',
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (matches.length > 1) {
|
|
124
|
+
const candidates = [];
|
|
125
|
+
const limit = Math.min(matches.length, 5);
|
|
126
|
+
for (let i = 0; i < limit; i++) {
|
|
127
|
+
const m = matches[i];
|
|
128
|
+
const tag = m.tagName.toLowerCase();
|
|
129
|
+
const text = (m.textContent || '').trim().slice(0, 40);
|
|
130
|
+
const id = m.id ? '#' + m.id : '';
|
|
131
|
+
candidates.push('<' + tag + id + '>' + (text ? ' "' + text + '"' : ''));
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
ok: false,
|
|
135
|
+
code: 'ambiguous',
|
|
136
|
+
message: 'CSS selector "' + ref + '" matched ' + matches.length + ' elements',
|
|
137
|
+
hint: 'Use a more specific selector, or use ref numbers from \`opencli browser state\` snapshot.',
|
|
138
|
+
candidates: candidates,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
window.__resolved = matches[0];
|
|
143
|
+
return { ok: true };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── Unrecognized input ──
|
|
147
|
+
return {
|
|
148
|
+
ok: false,
|
|
149
|
+
code: 'not_found',
|
|
150
|
+
message: 'Cannot parse target: ' + ref,
|
|
151
|
+
hint: 'Use a numeric ref from snapshot (e.g. "12") or a CSS selector (e.g. "#submit").',
|
|
152
|
+
};
|
|
153
|
+
})()
|
|
154
|
+
`;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Generate JS for click that uses the unified resolver.
|
|
158
|
+
* Assumes resolveTargetJs has been called and __resolved is set.
|
|
159
|
+
*/
|
|
160
|
+
export function clickResolvedJs() {
|
|
161
|
+
return `
|
|
162
|
+
(() => {
|
|
163
|
+
const el = window.__resolved;
|
|
164
|
+
if (!el) throw new Error('No resolved element');
|
|
165
|
+
el.scrollIntoView({ behavior: 'instant', block: 'center' });
|
|
166
|
+
const rect = el.getBoundingClientRect();
|
|
167
|
+
const x = Math.round(rect.left + rect.width / 2);
|
|
168
|
+
const y = Math.round(rect.top + rect.height / 2);
|
|
169
|
+
try {
|
|
170
|
+
el.click();
|
|
171
|
+
return { status: 'clicked', x, y, w: Math.round(rect.width), h: Math.round(rect.height) };
|
|
172
|
+
} catch (e) {
|
|
173
|
+
return { status: 'js_failed', x, y, w: Math.round(rect.width), h: Math.round(rect.height), error: e.message };
|
|
174
|
+
}
|
|
175
|
+
})()
|
|
176
|
+
`;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Generate JS for type that uses the unified resolver.
|
|
180
|
+
*/
|
|
181
|
+
export function typeResolvedJs(text) {
|
|
182
|
+
const safeText = JSON.stringify(text);
|
|
183
|
+
return `
|
|
184
|
+
(() => {
|
|
185
|
+
const el = window.__resolved;
|
|
186
|
+
if (!el) throw new Error('No resolved element');
|
|
187
|
+
el.focus();
|
|
188
|
+
if (el.isContentEditable) {
|
|
189
|
+
const sel = window.getSelection();
|
|
190
|
+
const range = document.createRange();
|
|
191
|
+
range.selectNodeContents(el);
|
|
192
|
+
sel.removeAllRanges();
|
|
193
|
+
sel.addRange(range);
|
|
194
|
+
document.execCommand('delete', false);
|
|
195
|
+
document.execCommand('insertText', false, ${safeText});
|
|
196
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
197
|
+
} else {
|
|
198
|
+
const proto = el instanceof HTMLTextAreaElement
|
|
199
|
+
? HTMLTextAreaElement.prototype
|
|
200
|
+
: HTMLInputElement.prototype;
|
|
201
|
+
const nativeSetter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
|
|
202
|
+
if (nativeSetter) {
|
|
203
|
+
nativeSetter.call(el, ${safeText});
|
|
204
|
+
} else {
|
|
205
|
+
el.value = ${safeText};
|
|
206
|
+
}
|
|
207
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
208
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
209
|
+
}
|
|
210
|
+
return 'typed';
|
|
211
|
+
})()
|
|
212
|
+
`;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Generate JS for scrollTo that uses the unified resolver.
|
|
216
|
+
* Assumes resolveTargetJs has been called and __resolved is set.
|
|
217
|
+
*/
|
|
218
|
+
export function scrollResolvedJs() {
|
|
219
|
+
return `
|
|
220
|
+
(() => {
|
|
221
|
+
const el = window.__resolved;
|
|
222
|
+
if (!el) throw new Error('No resolved element');
|
|
223
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
|
|
224
|
+
return { scrolled: true, tag: el.tagName.toLowerCase(), text: (el.textContent || '').trim().slice(0, 80) };
|
|
225
|
+
})()
|
|
226
|
+
`;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Generate JS to get text content of resolved element.
|
|
230
|
+
*/
|
|
231
|
+
export function getTextResolvedJs() {
|
|
232
|
+
return `
|
|
233
|
+
(() => {
|
|
234
|
+
const el = window.__resolved;
|
|
235
|
+
if (!el) throw new Error('No resolved element');
|
|
236
|
+
return el.textContent?.trim() ?? null;
|
|
237
|
+
})()
|
|
238
|
+
`;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Generate JS to get value of resolved input/textarea element.
|
|
242
|
+
*/
|
|
243
|
+
export function getValueResolvedJs() {
|
|
244
|
+
return `
|
|
245
|
+
(() => {
|
|
246
|
+
const el = window.__resolved;
|
|
247
|
+
if (!el) throw new Error('No resolved element');
|
|
248
|
+
return el.value ?? null;
|
|
249
|
+
})()
|
|
250
|
+
`;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Generate JS to get all attributes of resolved element.
|
|
254
|
+
*/
|
|
255
|
+
export function getAttributesResolvedJs() {
|
|
256
|
+
return `
|
|
257
|
+
(() => {
|
|
258
|
+
const el = window.__resolved;
|
|
259
|
+
if (!el) throw new Error('No resolved element');
|
|
260
|
+
return JSON.stringify(Object.fromEntries([...el.attributes].map(a => [a.name, a.value])));
|
|
261
|
+
})()
|
|
262
|
+
`;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Generate JS to select an option on a resolved <select> element.
|
|
266
|
+
*/
|
|
267
|
+
export function selectResolvedJs(option) {
|
|
268
|
+
const safeOption = JSON.stringify(option);
|
|
269
|
+
return `
|
|
270
|
+
(() => {
|
|
271
|
+
const el = window.__resolved;
|
|
272
|
+
if (!el) throw new Error('No resolved element');
|
|
273
|
+
if (el.tagName !== 'SELECT') return { error: 'Not a <select>' };
|
|
274
|
+
const match = Array.from(el.options).find(o => o.text.trim() === ${safeOption} || o.value === ${safeOption});
|
|
275
|
+
if (!match) return { error: 'Option not found', available: Array.from(el.options).map(o => o.text.trim()) };
|
|
276
|
+
const setter = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, 'value')?.set;
|
|
277
|
+
if (setter) setter.call(el, match.value); else el.value = match.value;
|
|
278
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
279
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
280
|
+
return { selected: match.text };
|
|
281
|
+
})()
|
|
282
|
+
`;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Generate JS to check if resolved element is an autocomplete/combobox field.
|
|
286
|
+
*/
|
|
287
|
+
export function isAutocompleteResolvedJs() {
|
|
288
|
+
return `
|
|
289
|
+
(() => {
|
|
290
|
+
const el = window.__resolved;
|
|
291
|
+
if (!el) return false;
|
|
292
|
+
const role = el.getAttribute('role');
|
|
293
|
+
const ac = el.getAttribute('aria-autocomplete');
|
|
294
|
+
const list = el.getAttribute('list');
|
|
295
|
+
return role === 'combobox' || ac === 'list' || ac === 'both' || !!list;
|
|
296
|
+
})()
|
|
297
|
+
`;
|
|
298
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { resolveTargetJs } from './target-resolver.js';
|
|
3
|
+
/**
|
|
4
|
+
* Tests for the target resolver JS generator.
|
|
5
|
+
*
|
|
6
|
+
* Since resolveTargetJs() produces JS strings for browser evaluate(),
|
|
7
|
+
* we test the generated JS by running it in a simulated DOM-like context
|
|
8
|
+
* and verifying the structure of the output.
|
|
9
|
+
*/
|
|
10
|
+
describe('resolveTargetJs', () => {
|
|
11
|
+
it('generates JS that returns structured resolution for numeric ref', () => {
|
|
12
|
+
const js = resolveTargetJs('12');
|
|
13
|
+
expect(js).toContain('data-opencli-ref');
|
|
14
|
+
expect(js).toContain('__opencli_ref_identity');
|
|
15
|
+
expect(js).toContain('"12"');
|
|
16
|
+
});
|
|
17
|
+
it('generates JS that handles CSS selector input', () => {
|
|
18
|
+
const js = resolveTargetJs('#submit-btn');
|
|
19
|
+
expect(js).toContain('querySelectorAll');
|
|
20
|
+
expect(js).toContain('"#submit-btn"');
|
|
21
|
+
});
|
|
22
|
+
it('generates JS with stale_ref detection for numeric refs', () => {
|
|
23
|
+
const js = resolveTargetJs('5');
|
|
24
|
+
expect(js).toContain('stale_ref');
|
|
25
|
+
expect(js).toContain('__opencli_ref_identity');
|
|
26
|
+
});
|
|
27
|
+
it('generates JS with ambiguity detection for CSS selectors', () => {
|
|
28
|
+
const js = resolveTargetJs('.btn');
|
|
29
|
+
expect(js).toContain('ambiguous');
|
|
30
|
+
expect(js).toContain('candidates');
|
|
31
|
+
});
|
|
32
|
+
it('generates JS that rejects unrecognized input', () => {
|
|
33
|
+
const js = resolveTargetJs('???');
|
|
34
|
+
expect(js).toContain('not_found');
|
|
35
|
+
expect(js).toContain('Cannot parse target');
|
|
36
|
+
});
|
|
37
|
+
it('escapes ref value safely', () => {
|
|
38
|
+
const js = resolveTargetJs('"; alert(1); "');
|
|
39
|
+
// JSON.stringify should handle escaping
|
|
40
|
+
expect(js).not.toContain('alert(1); "');
|
|
41
|
+
expect(js).toContain('\\"');
|
|
42
|
+
});
|
|
43
|
+
});
|
package/dist/src/browser.test.js
CHANGED
|
@@ -112,13 +112,15 @@ describe('BrowserBridge state', () => {
|
|
|
112
112
|
bridge._state = 'closing';
|
|
113
113
|
await expect(bridge.connect()).rejects.toThrow('Session is closing');
|
|
114
114
|
});
|
|
115
|
-
it('fails fast when daemon is running but extension is disconnected', async () => {
|
|
115
|
+
it('fails fast when daemon is running but extension is disconnected (same version)', async () => {
|
|
116
|
+
const { PKG_VERSION } = await import('./version.js');
|
|
116
117
|
vi.spyOn(daemonClient, 'getDaemonHealth').mockResolvedValue({
|
|
117
118
|
state: 'no-extension',
|
|
118
119
|
status: {
|
|
119
120
|
ok: true,
|
|
120
121
|
pid: 1,
|
|
121
122
|
uptime: 0,
|
|
123
|
+
daemonVersion: PKG_VERSION,
|
|
122
124
|
extensionConnected: false,
|
|
123
125
|
pending: 0,
|
|
124
126
|
memoryMB: 0,
|
|
@@ -128,6 +130,41 @@ describe('BrowserBridge state', () => {
|
|
|
128
130
|
const bridge = new BrowserBridge();
|
|
129
131
|
await expect(bridge.connect({ timeout: 0.1 })).rejects.toThrow('Browser Bridge extension not connected');
|
|
130
132
|
});
|
|
133
|
+
it('attempts stale daemon replacement when daemonVersion is missing', async () => {
|
|
134
|
+
vi.spyOn(daemonClient, 'getDaemonHealth').mockResolvedValue({
|
|
135
|
+
state: 'no-extension',
|
|
136
|
+
status: {
|
|
137
|
+
ok: true,
|
|
138
|
+
pid: 1,
|
|
139
|
+
uptime: 0,
|
|
140
|
+
extensionConnected: false,
|
|
141
|
+
pending: 0,
|
|
142
|
+
memoryMB: 0,
|
|
143
|
+
port: 0,
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
vi.spyOn(daemonClient, 'requestDaemonShutdown').mockResolvedValue(false);
|
|
147
|
+
const bridge = new BrowserBridge();
|
|
148
|
+
await expect(bridge.connect({ timeout: 0.1 })).rejects.toThrow('Stale daemon could not be replaced');
|
|
149
|
+
});
|
|
150
|
+
it('attempts stale daemon replacement when daemonVersion mismatches', async () => {
|
|
151
|
+
vi.spyOn(daemonClient, 'getDaemonHealth').mockResolvedValue({
|
|
152
|
+
state: 'no-extension',
|
|
153
|
+
status: {
|
|
154
|
+
ok: true,
|
|
155
|
+
pid: 1,
|
|
156
|
+
uptime: 0,
|
|
157
|
+
daemonVersion: '0.0.1',
|
|
158
|
+
extensionConnected: false,
|
|
159
|
+
pending: 0,
|
|
160
|
+
memoryMB: 0,
|
|
161
|
+
port: 0,
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
vi.spyOn(daemonClient, 'requestDaemonShutdown').mockResolvedValue(false);
|
|
165
|
+
const bridge = new BrowserBridge();
|
|
166
|
+
await expect(bridge.connect({ timeout: 0.1 })).rejects.toThrow('Stale daemon could not be replaced');
|
|
167
|
+
});
|
|
131
168
|
});
|
|
132
169
|
describe('stealth anti-detection', () => {
|
|
133
170
|
it('generates non-empty JS string', () => {
|