@jackwener/opencli 1.7.7 → 1.7.9
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 +49 -14
- package/README.zh-CN.md +30 -10
- package/cli-manifest.json +782 -55
- package/clis/36kr/news.js +1 -1
- package/clis/amazon/discussion.js +37 -6
- package/clis/amazon/discussion.test.js +147 -32
- package/clis/apple-podcasts/commands.test.js +4 -4
- package/clis/apple-podcasts/episodes.js +1 -1
- package/clis/apple-podcasts/search.js +1 -1
- package/clis/apple-podcasts/top.js +1 -1
- package/clis/arxiv/paper.js +1 -1
- package/clis/arxiv/search.js +1 -1
- package/clis/band/mentions.js +3 -3
- package/clis/bbc/news.js +1 -1
- package/clis/bilibili/subtitle.js +2 -2
- package/clis/bloomberg/businessweek.js +1 -1
- package/clis/bloomberg/economics.js +1 -1
- package/clis/bloomberg/industries.js +1 -1
- package/clis/bloomberg/main.js +1 -1
- package/clis/bloomberg/markets.js +1 -1
- package/clis/bloomberg/opinions.js +1 -1
- package/clis/bloomberg/politics.js +1 -1
- package/clis/bloomberg/tech.js +1 -1
- package/clis/boss/search.js +49 -8
- package/clis/boss/search.test.js +78 -0
- package/clis/boss/send.js +3 -3
- package/clis/chatgpt/image.js +37 -8
- package/clis/chatgpt/image.test.js +92 -0
- package/clis/chatgpt/utils.js +39 -6
- package/clis/chatgpt/utils.test.js +63 -0
- package/clis/chatgpt-app/ask.js +4 -20
- package/clis/chatgpt-app/ax.js +135 -2
- package/clis/chatgpt-app/ax.test.js +35 -0
- package/clis/chatgpt-app/model.js +1 -1
- package/clis/chatgpt-app/new.js +1 -1
- package/clis/chatgpt-app/read.js +1 -1
- package/clis/chatgpt-app/send.js +3 -22
- package/clis/chatgpt-app/status.js +1 -1
- package/clis/chatwise/ask.js +2 -2
- package/clis/chatwise/model.js +2 -2
- package/clis/chatwise/send.js +2 -2
- package/clis/claude/ask.js +128 -0
- package/clis/claude/ask.test.js +338 -0
- package/clis/claude/commands.test.js +118 -0
- package/clis/claude/detail.js +29 -0
- package/clis/claude/history.js +31 -0
- package/clis/claude/new.js +21 -0
- package/clis/claude/read.js +24 -0
- package/clis/claude/send.js +41 -0
- package/clis/claude/status.js +24 -0
- package/clis/claude/utils.js +440 -0
- package/clis/claude/utils.test.js +148 -0
- package/clis/codex/ask.js +2 -2
- package/clis/codex/send.js +2 -2
- package/clis/ctrip/search.js +1 -1
- package/clis/ctrip/search.test.js +4 -4
- package/clis/cursor/ask.js +2 -2
- package/clis/cursor/composer.js +2 -2
- package/clis/cursor/send.js +2 -2
- package/clis/deepseek/ask.js +49 -10
- package/clis/deepseek/ask.test.js +150 -3
- package/clis/deepseek/utils.js +60 -22
- package/clis/deepseek/utils.test.js +124 -5
- package/clis/doubao/utils.js +53 -11
- package/clis/doubao/utils.test.js +22 -2
- package/clis/eastmoney/announcement.js +1 -1
- package/clis/eastmoney/convertible.js +1 -1
- package/clis/eastmoney/etf.js +1 -1
- package/clis/eastmoney/holders.js +1 -1
- package/clis/eastmoney/index-board.js +1 -1
- package/clis/eastmoney/kline.js +1 -1
- package/clis/eastmoney/kuaixun.js +1 -1
- package/clis/eastmoney/longhu.js +1 -1
- package/clis/eastmoney/money-flow.js +1 -1
- package/clis/eastmoney/northbound.js +1 -1
- package/clis/eastmoney/quote.js +1 -1
- package/clis/eastmoney/rank.js +1 -1
- package/clis/eastmoney/sectors.js +1 -1
- package/clis/facebook/marketplace-inbox.js +83 -0
- package/clis/facebook/marketplace-listings.js +83 -0
- package/clis/facebook/marketplace.test.js +91 -0
- package/clis/google/news.js +1 -1
- package/clis/google/suggest.js +1 -1
- package/clis/google/trends.js +1 -1
- package/clis/google-scholar/cite.js +74 -0
- package/clis/google-scholar/cite.test.js +47 -0
- package/clis/google-scholar/profile.js +92 -0
- package/clis/google-scholar/profile.test.js +49 -0
- package/clis/google-scholar/search.js +1 -1
- package/clis/google-scholar/search.test.js +15 -0
- package/clis/hf/top.js +1 -1
- package/clis/jd/item.js +679 -47
- package/clis/jd/item.test.js +318 -7
- package/clis/jd/item.test.ts +517 -0
- package/clis/lesswrong/comments.js +1 -1
- package/clis/lesswrong/curated.js +1 -1
- package/clis/lesswrong/frontpage.js +1 -1
- package/clis/lesswrong/new.js +1 -1
- package/clis/lesswrong/read.js +1 -1
- package/clis/lesswrong/sequences.js +1 -1
- package/clis/lesswrong/shortform.js +1 -1
- package/clis/lesswrong/tag.js +1 -1
- package/clis/lesswrong/tags.js +1 -1
- package/clis/lesswrong/top-month.js +1 -1
- package/clis/lesswrong/top-week.js +1 -1
- package/clis/lesswrong/top-year.js +1 -1
- package/clis/lesswrong/top.js +1 -1
- package/clis/lesswrong/user-posts.js +1 -1
- package/clis/lesswrong/user.js +1 -1
- package/clis/paperreview/commands.test.js +6 -6
- package/clis/paperreview/feedback.js +1 -1
- package/clis/paperreview/review.js +1 -1
- package/clis/paperreview/submit.js +1 -1
- package/clis/powerchina/search.js +250 -0
- package/clis/powerchina/search.test.js +67 -0
- package/clis/producthunt/posts.js +1 -1
- package/clis/producthunt/today.js +1 -1
- package/clis/sinablog/search.js +1 -1
- package/clis/sinafinance/news.js +1 -1
- package/clis/sinafinance/stock.js +6 -3
- package/clis/sinafinance/stock.test.js +59 -0
- package/clis/spotify/spotify.js +6 -6
- package/clis/substack/search.js +1 -1
- package/clis/toutiao/articles.js +80 -0
- package/clis/toutiao/articles.test.js +30 -0
- package/clis/twitter/followers.js +2 -2
- package/clis/twitter/following.js +224 -73
- package/clis/twitter/following.test.js +277 -0
- package/clis/twitter/post.js +184 -47
- package/clis/twitter/post.test.js +114 -34
- package/clis/uiverse/_shared.js +63 -4
- package/clis/uiverse/_shared.test.js +7 -0
- package/clis/uiverse/code.js +1 -0
- package/clis/uiverse/navigation.test.js +12 -0
- package/clis/uiverse/preview.js +1 -0
- package/clis/web/read.js +319 -81
- package/clis/web/read.test.js +221 -5
- package/clis/weibo/favorites.js +169 -0
- package/clis/weibo/favorites.test.js +114 -0
- package/clis/weibo/publish.js +282 -0
- package/clis/weibo/publish.test.js +183 -0
- package/clis/weixin/create-draft.js +225 -0
- package/clis/weixin/drafts.js +65 -0
- package/clis/weixin/drafts.test.js +65 -0
- package/clis/weread/ranking.js +1 -1
- package/clis/weread/search-regression.test.js +8 -8
- package/clis/weread/search.js +1 -1
- package/clis/wikipedia/random.js +1 -1
- package/clis/wikipedia/search.js +1 -1
- package/clis/wikipedia/summary.js +1 -1
- package/clis/wikipedia/trending.js +1 -1
- package/clis/xianyu/chat.js +3 -3
- package/clis/xianyu/item.js +2 -2
- package/clis/xianyu/item.test.js +3 -3
- package/clis/xiaohongshu/search.js +17 -2
- package/clis/xiaohongshu/search.test.js +37 -1
- package/clis/xiaoyuzhou/download.js +1 -1
- package/clis/xiaoyuzhou/download.test.js +3 -3
- package/clis/xiaoyuzhou/episode.js +1 -1
- package/clis/xiaoyuzhou/podcast-episodes.js +1 -1
- package/clis/xiaoyuzhou/podcast-episodes.test.js +2 -2
- package/clis/xiaoyuzhou/podcast.js +1 -1
- package/clis/xiaoyuzhou/transcript.js +1 -1
- package/clis/xiaoyuzhou/transcript.test.js +5 -5
- package/clis/yollomi/models.js +1 -1
- package/clis/youtube/channel.js +24 -1
- package/clis/youtube/channel.test.js +59 -0
- package/clis/zhihu/answer.js +21 -162
- package/clis/zhihu/answer.test.js +26 -53
- package/clis/zhihu/collection.js +197 -0
- package/clis/zhihu/collection.test.js +290 -0
- package/clis/zhihu/collections.js +127 -0
- package/clis/zhihu/collections.test.js +182 -0
- package/clis/zhihu/comment.js +24 -305
- package/clis/zhihu/comment.test.js +31 -35
- package/clis/zhihu/favorite.js +44 -182
- package/clis/zhihu/favorite.test.js +30 -167
- package/clis/zhihu/follow.js +25 -56
- package/clis/zhihu/follow.test.js +20 -23
- package/clis/zhihu/like.js +22 -67
- package/clis/zhihu/like.test.js +19 -42
- package/clis/zhihu/search.js +3 -2
- package/clis/zhihu/write-shared.js +8 -1
- package/clis/zhihu/write-shared.test.js +1 -0
- package/clis/zlibrary/commands.test.js +75 -0
- package/clis/zlibrary/info.js +47 -0
- package/clis/zlibrary/search.js +46 -0
- package/clis/zlibrary/utils.js +136 -0
- package/dist/src/adapter-source.d.ts +11 -0
- package/dist/src/adapter-source.js +24 -0
- package/dist/src/adapter-source.test.js +29 -0
- package/dist/src/browser/base-page.d.ts +3 -1
- package/dist/src/browser/base-page.js +76 -1
- package/dist/src/browser/base-page.test.d.ts +1 -0
- package/dist/src/browser/base-page.test.js +74 -0
- package/dist/src/browser/bridge.d.ts +1 -0
- package/dist/src/browser/bridge.js +36 -9
- package/dist/src/browser/cdp.d.ts +1 -0
- package/dist/src/browser/cdp.js +3 -3
- package/dist/src/browser/daemon-client.d.ts +38 -4
- package/dist/src/browser/daemon-client.js +24 -7
- package/dist/src/browser/daemon-client.test.js +49 -0
- package/dist/src/browser/errors.js +3 -0
- package/dist/src/browser/errors.test.js +3 -0
- package/dist/src/browser/network-cache.d.ts +1 -0
- package/dist/src/browser/page.d.ts +3 -1
- package/dist/src/browser/page.js +10 -2
- package/dist/src/browser/profile.d.ts +14 -0
- package/dist/src/browser/profile.js +85 -0
- package/dist/src/build-manifest.d.ts +2 -0
- package/dist/src/build-manifest.js +13 -3
- package/dist/src/build-manifest.test.js +20 -2
- package/dist/src/cli.d.ts +6 -0
- package/dist/src/cli.js +462 -32
- package/dist/src/cli.test.js +209 -2
- package/dist/src/commanderAdapter.js +29 -9
- package/dist/src/commanderAdapter.test.js +78 -2
- package/dist/src/commands/daemon.js +6 -0
- package/dist/src/completion-shared.js +1 -2
- package/dist/src/completion.test.js +3 -2
- package/dist/src/daemon.js +125 -41
- package/dist/src/doctor.d.ts +4 -6
- package/dist/src/doctor.js +80 -22
- package/dist/src/doctor.test.js +82 -0
- package/dist/src/engine.test.js +6 -5
- package/dist/src/errors.d.ts +14 -8
- package/dist/src/errors.js +36 -30
- package/dist/src/errors.test.js +5 -5
- package/dist/src/execution.d.ts +4 -0
- package/dist/src/execution.js +173 -25
- package/dist/src/execution.test.js +171 -1
- package/dist/src/main.js +10 -0
- package/dist/src/observation/artifact.d.ts +16 -0
- package/dist/src/observation/artifact.js +260 -0
- package/dist/src/observation/artifact.test.d.ts +1 -0
- package/dist/src/observation/artifact.test.js +121 -0
- package/dist/src/observation/events.d.ts +89 -0
- package/dist/src/observation/events.js +1 -0
- package/dist/src/observation/index.d.ts +7 -0
- package/dist/src/observation/index.js +7 -0
- package/dist/src/observation/manager.d.ts +9 -0
- package/dist/src/observation/manager.js +27 -0
- package/dist/src/observation/manager.test.d.ts +1 -0
- package/dist/src/observation/manager.test.js +13 -0
- package/dist/src/observation/redaction.d.ts +11 -0
- package/dist/src/observation/redaction.js +81 -0
- package/dist/src/observation/redaction.test.d.ts +1 -0
- package/dist/src/observation/redaction.test.js +32 -0
- package/dist/src/observation/retention.d.ts +32 -0
- package/dist/src/observation/retention.js +160 -0
- package/dist/src/observation/retention.test.d.ts +1 -0
- package/dist/src/observation/retention.test.js +118 -0
- package/dist/src/observation/ring-buffer.d.ts +22 -0
- package/dist/src/observation/ring-buffer.js +45 -0
- package/dist/src/observation/ring-buffer.test.d.ts +1 -0
- package/dist/src/observation/ring-buffer.test.js +22 -0
- package/dist/src/observation/session.d.ts +25 -0
- package/dist/src/observation/session.js +50 -0
- package/dist/src/pipeline/executor.test.js +1 -0
- package/dist/src/pipeline/steps/download.test.js +1 -0
- package/dist/src/pipeline/steps/fetch.js +1 -21
- package/dist/src/pipeline/steps/fetch.test.js +6 -12
- package/dist/src/plugin-scaffold.js +1 -1
- package/dist/src/plugin-scaffold.test.js +1 -1
- package/dist/src/registry.d.ts +40 -9
- package/dist/src/registry.js +3 -1
- package/dist/src/runtime-detect.d.ts +10 -0
- package/dist/src/runtime-detect.js +19 -0
- package/dist/src/runtime-detect.test.js +12 -1
- package/dist/src/runtime.d.ts +2 -0
- package/dist/src/runtime.js +1 -0
- package/dist/src/types.d.ts +22 -0
- package/dist/src/update-check.d.ts +31 -1
- package/dist/src/update-check.js +62 -16
- package/dist/src/update-check.test.js +86 -1
- package/package.json +1 -1
- package/dist/src/diagnostic.d.ts +0 -63
- package/dist/src/diagnostic.js +0 -292
- package/dist/src/diagnostic.test.js +0 -302
- /package/dist/src/{diagnostic.test.d.ts → adapter-source.test.d.ts} +0 -0
package/clis/cursor/composer.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
-
import {
|
|
2
|
+
import { selectorError } from '@jackwener/opencli/errors';
|
|
3
3
|
export const composerCommand = cli({
|
|
4
4
|
site: 'cursor',
|
|
5
5
|
name: 'composer',
|
|
@@ -27,7 +27,7 @@ export const composerCommand = cli({
|
|
|
27
27
|
return true;
|
|
28
28
|
})(${JSON.stringify(textToInsert)})`);
|
|
29
29
|
if (!typed) {
|
|
30
|
-
throw
|
|
30
|
+
throw selectorError('Cursor Composer input element', 'Could not find Cursor Composer input element after pressing Cmd+I.');
|
|
31
31
|
}
|
|
32
32
|
await page.wait(0.5);
|
|
33
33
|
await page.pressKey('Enter');
|
package/clis/cursor/send.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
-
import {
|
|
2
|
+
import { selectorError } from '@jackwener/opencli/errors';
|
|
3
3
|
export const sendCommand = cli({
|
|
4
4
|
site: 'cursor',
|
|
5
5
|
name: 'send',
|
|
@@ -24,7 +24,7 @@ export const sendCommand = cli({
|
|
|
24
24
|
return true;
|
|
25
25
|
})(${JSON.stringify(textToInsert)})`);
|
|
26
26
|
if (!injected) {
|
|
27
|
-
throw
|
|
27
|
+
throw selectorError('Cursor Composer input element');
|
|
28
28
|
}
|
|
29
29
|
// Submit the command. In Cursor, Enter usually submits the chat.
|
|
30
30
|
await page.wait(0.5);
|
package/clis/deepseek/ask.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
-
import { CommandExecutionError } from '@jackwener/opencli/errors';
|
|
2
|
+
import { CliError, CommandExecutionError, EXIT_CODES } from '@jackwener/opencli/errors';
|
|
3
3
|
import {
|
|
4
4
|
DEEPSEEK_DOMAIN, DEEPSEEK_URL, ensureOnDeepSeek, selectModel, setFeature,
|
|
5
5
|
sendMessage, sendWithFile, getBubbleCount, waitForResponse, parseBoolFlag, withRetry,
|
|
@@ -18,7 +18,7 @@ export const askCommand = cli({
|
|
|
18
18
|
{ name: 'prompt', positional: true, required: true, help: 'Prompt to send' },
|
|
19
19
|
{ name: 'timeout', type: 'int', default: 120, help: 'Max seconds to wait for response' },
|
|
20
20
|
{ name: 'new', type: 'boolean', default: false, help: 'Start a new chat before sending' },
|
|
21
|
-
{ name: 'model', default: 'instant', choices: ['instant', 'expert'], help: 'Model to use: instant or
|
|
21
|
+
{ name: 'model', default: 'instant', choices: ['instant', 'expert', 'vision'], help: 'Model to use: instant, expert, or vision' },
|
|
22
22
|
{ name: 'think', type: 'boolean', default: false, help: 'Enable DeepThink mode' },
|
|
23
23
|
{ name: 'search', type: 'boolean', default: false, help: 'Enable web search' },
|
|
24
24
|
{ name: 'file', help: 'Attach a file (PDF, image, text) with the prompt' },
|
|
@@ -35,26 +35,65 @@ export const askCommand = cli({
|
|
|
35
35
|
await page.goto(DEEPSEEK_URL);
|
|
36
36
|
await page.wait(3);
|
|
37
37
|
} else {
|
|
38
|
-
await ensureOnDeepSeek(page);
|
|
38
|
+
const navigated = await ensureOnDeepSeek(page);
|
|
39
|
+
if (navigated) {
|
|
40
|
+
// Workspace was recycled; try to resume the most recent
|
|
41
|
+
// conversation instead of starting a new one.
|
|
42
|
+
await page.evaluate(`(() => {
|
|
43
|
+
var link = document.querySelector('a[href*="/a/chat/s/"]');
|
|
44
|
+
if (link) link.click();
|
|
45
|
+
})()`);
|
|
46
|
+
await page.wait(2);
|
|
47
|
+
}
|
|
39
48
|
}
|
|
40
49
|
|
|
41
50
|
await page.wait(2);
|
|
42
51
|
|
|
52
|
+
// Model selector is only available on the new-chat page, not inside
|
|
53
|
+
// an existing conversation. Skip it when we resumed a prior thread.
|
|
54
|
+
const currentUrl = await page.evaluate('window.location.href') || '';
|
|
55
|
+
const inConversation = currentUrl.includes('/a/chat/s/');
|
|
56
|
+
const modelExplicit = kwargs.__opencliOptionSources?.model === 'cli';
|
|
57
|
+
|
|
43
58
|
const wantModel = kwargs.model || 'instant';
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
59
|
+
if (inConversation && modelExplicit) {
|
|
60
|
+
throw new CliError(
|
|
61
|
+
'ARGUMENT',
|
|
62
|
+
`Cannot switch to ${wantModel} model inside an existing conversation.`,
|
|
63
|
+
'Re-run with --new to start a fresh chat before selecting a model.',
|
|
64
|
+
EXIT_CODES.USAGE_ERROR,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!inConversation) {
|
|
69
|
+
const modelResult = await withRetry(() => selectModel(page, wantModel));
|
|
70
|
+
if (!modelResult?.ok) {
|
|
71
|
+
throw new CommandExecutionError(`Could not switch to ${wantModel} model`);
|
|
72
|
+
}
|
|
73
|
+
if (modelResult?.toggled) await page.wait(0.5);
|
|
47
74
|
}
|
|
48
|
-
if (modelResult?.toggled) await page.wait(0.5);
|
|
49
75
|
|
|
50
76
|
const thinkResult = await withRetry(() => setFeature(page, 'DeepThink', wantThink));
|
|
51
77
|
if (!thinkResult?.ok && wantThink) {
|
|
52
78
|
throw new CommandExecutionError('Could not enable DeepThink');
|
|
53
79
|
}
|
|
54
80
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
81
|
+
if (wantModel === 'vision' && wantSearch) {
|
|
82
|
+
throw new CliError(
|
|
83
|
+
'ARGUMENT',
|
|
84
|
+
'DeepSeek vision mode does not support --search.',
|
|
85
|
+
'Run without --search, or use --model instant/expert for web search.',
|
|
86
|
+
EXIT_CODES.USAGE_ERROR,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Vision mode does not have the search toggle.
|
|
91
|
+
let searchResult;
|
|
92
|
+
if (wantModel !== 'vision') {
|
|
93
|
+
searchResult = await withRetry(() => setFeature(page, 'Search', wantSearch));
|
|
94
|
+
if (!searchResult?.ok && wantSearch) {
|
|
95
|
+
throw new CommandExecutionError('Could not enable Search');
|
|
96
|
+
}
|
|
58
97
|
}
|
|
59
98
|
|
|
60
99
|
if (thinkResult?.toggled || searchResult?.toggled) await page.wait(0.5);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { CommandExecutionError } from '@jackwener/opencli/errors';
|
|
2
|
+
import { CliError, CommandExecutionError, EXIT_CODES } from '@jackwener/opencli/errors';
|
|
3
3
|
|
|
4
4
|
const {
|
|
5
5
|
mockEnsureOnDeepSeek,
|
|
@@ -43,11 +43,13 @@ describe('deepseek ask --file', () => {
|
|
|
43
43
|
const page = {
|
|
44
44
|
wait: vi.fn().mockResolvedValue(undefined),
|
|
45
45
|
goto: vi.fn().mockResolvedValue(undefined),
|
|
46
|
+
evaluate: vi.fn().mockResolvedValue('https://chat.deepseek.com/'),
|
|
46
47
|
};
|
|
47
48
|
|
|
48
49
|
beforeEach(() => {
|
|
49
50
|
vi.clearAllMocks();
|
|
50
|
-
|
|
51
|
+
page.evaluate.mockResolvedValue('https://chat.deepseek.com/');
|
|
52
|
+
mockEnsureOnDeepSeek.mockResolvedValue(false);
|
|
51
53
|
mockSelectModel.mockResolvedValue({ ok: true, toggled: false });
|
|
52
54
|
mockSetFeature.mockResolvedValue({ ok: true, toggled: false });
|
|
53
55
|
mockSendWithFile.mockResolvedValue({ ok: true });
|
|
@@ -90,11 +92,13 @@ describe('deepseek ask --think', () => {
|
|
|
90
92
|
const page = {
|
|
91
93
|
wait: vi.fn().mockResolvedValue(undefined),
|
|
92
94
|
goto: vi.fn().mockResolvedValue(undefined),
|
|
95
|
+
evaluate: vi.fn().mockResolvedValue('https://chat.deepseek.com/'),
|
|
93
96
|
};
|
|
94
97
|
|
|
95
98
|
beforeEach(() => {
|
|
96
99
|
vi.clearAllMocks();
|
|
97
|
-
|
|
100
|
+
page.evaluate.mockResolvedValue('https://chat.deepseek.com/');
|
|
101
|
+
mockEnsureOnDeepSeek.mockResolvedValue(false);
|
|
98
102
|
mockSelectModel.mockResolvedValue({ ok: true, toggled: false });
|
|
99
103
|
mockSetFeature.mockResolvedValue({ ok: true, toggled: false });
|
|
100
104
|
mockSendMessage.mockResolvedValue({ ok: true });
|
|
@@ -163,3 +167,146 @@ describe('deepseek ask --think', () => {
|
|
|
163
167
|
expect(Object.keys(rows[0])).toEqual(['response']);
|
|
164
168
|
});
|
|
165
169
|
});
|
|
170
|
+
|
|
171
|
+
describe('deepseek ask conversation resume', () => {
|
|
172
|
+
const page = {
|
|
173
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
174
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
175
|
+
evaluate: vi.fn(),
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
beforeEach(() => {
|
|
179
|
+
vi.clearAllMocks();
|
|
180
|
+
mockSetFeature.mockResolvedValue({ ok: true, toggled: false });
|
|
181
|
+
mockSendMessage.mockResolvedValue({ ok: true });
|
|
182
|
+
mockGetBubbleCount.mockResolvedValue(2);
|
|
183
|
+
mockWaitForResponse.mockResolvedValue('follow-up reply');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('resumes the most recent conversation and skips model selection', async () => {
|
|
187
|
+
mockEnsureOnDeepSeek.mockResolvedValue(true);
|
|
188
|
+
// first evaluate: sidebar resume click (returns undefined)
|
|
189
|
+
page.evaluate.mockResolvedValueOnce(undefined);
|
|
190
|
+
// second evaluate: URL check (now inside a conversation)
|
|
191
|
+
page.evaluate.mockResolvedValueOnce('https://chat.deepseek.com/a/chat/s/abc-123');
|
|
192
|
+
|
|
193
|
+
const rows = await askCommand.func(page, {
|
|
194
|
+
prompt: 'follow up',
|
|
195
|
+
timeout: 120,
|
|
196
|
+
new: false,
|
|
197
|
+
model: 'instant',
|
|
198
|
+
think: false,
|
|
199
|
+
search: false,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
expect(rows).toEqual([{ response: 'follow-up reply' }]);
|
|
203
|
+
expect(mockSelectModel).not.toHaveBeenCalled();
|
|
204
|
+
expect(mockSendMessage).toHaveBeenCalled();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('skips model selection when already inside an existing conversation', async () => {
|
|
208
|
+
mockEnsureOnDeepSeek.mockResolvedValue(false);
|
|
209
|
+
page.evaluate.mockResolvedValue('https://chat.deepseek.com/a/chat/s/abc-123');
|
|
210
|
+
|
|
211
|
+
const rows = await askCommand.func(page, {
|
|
212
|
+
prompt: 'continue',
|
|
213
|
+
timeout: 120,
|
|
214
|
+
new: false,
|
|
215
|
+
model: 'expert',
|
|
216
|
+
think: false,
|
|
217
|
+
search: false,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
expect(rows).toEqual([{ response: 'follow-up reply' }]);
|
|
221
|
+
expect(mockSelectModel).not.toHaveBeenCalled();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('fails fast when --model is explicitly requested inside an existing conversation', async () => {
|
|
225
|
+
mockEnsureOnDeepSeek.mockResolvedValue(false);
|
|
226
|
+
page.evaluate.mockResolvedValue('https://chat.deepseek.com/a/chat/s/abc-123');
|
|
227
|
+
|
|
228
|
+
await expect(askCommand.func(page, {
|
|
229
|
+
prompt: 'continue',
|
|
230
|
+
timeout: 120,
|
|
231
|
+
new: false,
|
|
232
|
+
model: 'expert',
|
|
233
|
+
think: false,
|
|
234
|
+
search: false,
|
|
235
|
+
__opencliOptionSources: { model: 'cli' },
|
|
236
|
+
})).rejects.toMatchObject(new CliError(
|
|
237
|
+
'ARGUMENT',
|
|
238
|
+
'Cannot switch to expert model inside an existing conversation.',
|
|
239
|
+
'Re-run with --new to start a fresh chat before selecting a model.',
|
|
240
|
+
EXIT_CODES.USAGE_ERROR,
|
|
241
|
+
));
|
|
242
|
+
|
|
243
|
+
expect(mockSelectModel).not.toHaveBeenCalled();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('still selects model when no conversation to resume', async () => {
|
|
247
|
+
mockEnsureOnDeepSeek.mockResolvedValue(true);
|
|
248
|
+
mockSelectModel.mockResolvedValue({ ok: true, toggled: false });
|
|
249
|
+
// first evaluate: sidebar resume click (no link found)
|
|
250
|
+
page.evaluate.mockResolvedValueOnce(undefined);
|
|
251
|
+
// second evaluate: URL check (still on root page)
|
|
252
|
+
page.evaluate.mockResolvedValueOnce('https://chat.deepseek.com/');
|
|
253
|
+
|
|
254
|
+
const rows = await askCommand.func(page, {
|
|
255
|
+
prompt: 'hello',
|
|
256
|
+
timeout: 120,
|
|
257
|
+
new: false,
|
|
258
|
+
model: 'instant',
|
|
259
|
+
think: false,
|
|
260
|
+
search: false,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
expect(rows).toEqual([{ response: 'follow-up reply' }]);
|
|
264
|
+
expect(mockSelectModel).toHaveBeenCalled();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('skips search toggle in vision mode when search is not requested', async () => {
|
|
268
|
+
mockEnsureOnDeepSeek.mockResolvedValue(false);
|
|
269
|
+
mockSelectModel.mockResolvedValue({ ok: true, toggled: false });
|
|
270
|
+
mockSetFeature.mockResolvedValue({ ok: true, toggled: false });
|
|
271
|
+
mockSendMessage.mockResolvedValue({ ok: true });
|
|
272
|
+
mockGetBubbleCount.mockResolvedValue(0);
|
|
273
|
+
mockWaitForResponse.mockResolvedValue('vision reply');
|
|
274
|
+
page.evaluate.mockResolvedValue('https://chat.deepseek.com/');
|
|
275
|
+
|
|
276
|
+
const rows = await askCommand.func(page, {
|
|
277
|
+
prompt: 'describe',
|
|
278
|
+
timeout: 120,
|
|
279
|
+
new: false,
|
|
280
|
+
model: 'vision',
|
|
281
|
+
think: false,
|
|
282
|
+
search: false,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
expect(rows).toEqual([{ response: 'vision reply' }]);
|
|
286
|
+
expect(mockSetFeature).toHaveBeenCalledTimes(1);
|
|
287
|
+
expect(mockSetFeature).toHaveBeenCalledWith(expect.anything(), 'DeepThink', false);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('fails fast instead of silently ignoring --search in vision mode', async () => {
|
|
291
|
+
mockEnsureOnDeepSeek.mockResolvedValue(false);
|
|
292
|
+
mockSelectModel.mockResolvedValue({ ok: true, toggled: false });
|
|
293
|
+
page.evaluate.mockResolvedValue('https://chat.deepseek.com/');
|
|
294
|
+
|
|
295
|
+
await expect(askCommand.func(page, {
|
|
296
|
+
prompt: 'describe',
|
|
297
|
+
timeout: 120,
|
|
298
|
+
new: false,
|
|
299
|
+
model: 'vision',
|
|
300
|
+
think: false,
|
|
301
|
+
search: true,
|
|
302
|
+
})).rejects.toMatchObject(new CliError(
|
|
303
|
+
'ARGUMENT',
|
|
304
|
+
'DeepSeek vision mode does not support --search.',
|
|
305
|
+
'Run without --search, or use --model instant/expert for web search.',
|
|
306
|
+
EXIT_CODES.USAGE_ERROR,
|
|
307
|
+
));
|
|
308
|
+
|
|
309
|
+
expect(mockSendMessage).not.toHaveBeenCalled();
|
|
310
|
+
expect(mockSendWithFile).not.toHaveBeenCalled();
|
|
311
|
+
});
|
|
312
|
+
});
|
package/clis/deepseek/utils.js
CHANGED
|
@@ -15,10 +15,10 @@ export async function isOnDeepSeek(page) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export async function ensureOnDeepSeek(page) {
|
|
18
|
-
if (
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
if (await isOnDeepSeek(page)) return false;
|
|
19
|
+
await page.goto(DEEPSEEK_URL);
|
|
20
|
+
await page.wait(3);
|
|
21
|
+
return true;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export async function getPageState(page) {
|
|
@@ -40,9 +40,10 @@ export async function selectModel(page, modelName) {
|
|
|
40
40
|
return page.evaluate(`(() => {
|
|
41
41
|
var radios = document.querySelectorAll('div[role="radio"]');
|
|
42
42
|
if (radios.length === 0) return { ok: false };
|
|
43
|
-
var
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
var name = '${modelName}'.toLowerCase();
|
|
44
|
+
var index = name === 'instant' ? 0 : name === 'expert' ? 1 : name === 'vision' ? 2 : -1;
|
|
45
|
+
if (index < 0 || index >= radios.length) return { ok: false };
|
|
46
|
+
var target = radios[index];
|
|
46
47
|
var alreadySelected = target.getAttribute('aria-checked') === 'true';
|
|
47
48
|
if (!alreadySelected) target.click();
|
|
48
49
|
return { ok: true, toggled: !alreadySelected };
|
|
@@ -74,14 +75,18 @@ export async function sendMessage(page, prompt) {
|
|
|
74
75
|
document.execCommand('insertText', false, ${promptJson});
|
|
75
76
|
await new Promise(r => setTimeout(r, 800));
|
|
76
77
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
78
|
+
// Find the send button: last non-toggle button in the textarea's container
|
|
79
|
+
var container = box.parentElement;
|
|
80
|
+
while (container && !container.querySelector('div[role="button"]')) {
|
|
81
|
+
container = container.parentElement;
|
|
82
|
+
}
|
|
83
|
+
if (container) {
|
|
84
|
+
var btns = container.querySelectorAll('div[role="button"]:not(.ds-toggle-button)');
|
|
85
|
+
var sendBtn = btns[btns.length - 1];
|
|
86
|
+
if (sendBtn && sendBtn.getAttribute('aria-disabled') === 'false'
|
|
87
|
+
&& sendBtn.querySelectorAll('svg').length > 0) {
|
|
88
|
+
sendBtn.click();
|
|
89
|
+
return { ok: true };
|
|
85
90
|
}
|
|
86
91
|
}
|
|
87
92
|
|
|
@@ -252,8 +257,7 @@ export async function getConversationList(page) {
|
|
|
252
257
|
const items = [];
|
|
253
258
|
const links = document.querySelectorAll('a[href*="/a/chat/s/"]');
|
|
254
259
|
links.forEach((link, i) => {
|
|
255
|
-
const
|
|
256
|
-
const title = titleEl ? titleEl.textContent.trim() : '';
|
|
260
|
+
const title = (link.innerText || '').trim().split('\\n')[0].trim();
|
|
257
261
|
const href = link.getAttribute('href') || '';
|
|
258
262
|
const idMatch = href.match(/\\/s\\/([a-f0-9-]+)/);
|
|
259
263
|
items.push({
|
|
@@ -274,9 +278,18 @@ async function waitForFilePreview(page, fileName) {
|
|
|
274
278
|
for (let attempt = 0; attempt < 8; attempt++) {
|
|
275
279
|
await page.wait(2);
|
|
276
280
|
const ready = await page.evaluate(`(() => {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
.some((el)
|
|
281
|
+
var name = ${JSON.stringify(fileName)};
|
|
282
|
+
var hasFileName = Array.from(document.querySelectorAll('div'))
|
|
283
|
+
.some(function(el) { return el.children.length === 0 && (el.textContent || '').trim() === name; });
|
|
284
|
+
if (hasFileName) return true;
|
|
285
|
+
// Vision mode shows an image thumbnail, not filename text. Require
|
|
286
|
+
// a preview-like node here; send-button readiness is checked later.
|
|
287
|
+
var box = document.querySelector('${TEXTAREA_SELECTOR}');
|
|
288
|
+
if (!box) return false;
|
|
289
|
+
var c = box.parentElement;
|
|
290
|
+
while (c && !c.querySelector('div[role="button"]')) c = c.parentElement;
|
|
291
|
+
if (!c) return false;
|
|
292
|
+
return !!c.querySelector('img[src], canvas, video, [style*="background-image"], [class*="preview"], [class*="upload"]');
|
|
280
293
|
})()`);
|
|
281
294
|
if (ready) return true;
|
|
282
295
|
}
|
|
@@ -315,7 +328,7 @@ export async function sendWithFile(page, filePath, prompt) {
|
|
|
315
328
|
uploaded = true;
|
|
316
329
|
} catch (err) {
|
|
317
330
|
const msg = String(err?.message || err);
|
|
318
|
-
if (!msg.includes('Unknown action') && !msg.includes('not supported')) {
|
|
331
|
+
if (!msg.includes('Unknown action') && !msg.includes('not supported') && !msg.includes('Not allowed')) {
|
|
319
332
|
throw err;
|
|
320
333
|
}
|
|
321
334
|
}
|
|
@@ -342,7 +355,8 @@ export async function sendWithFile(page, filePath, prompt) {
|
|
|
342
355
|
}
|
|
343
356
|
|
|
344
357
|
inp.files = dt.files;
|
|
345
|
-
inp
|
|
358
|
+
// Use inp.files, not dt.files; assignment transfers ownership
|
|
359
|
+
inp[propsKey].onChange({ target: { files: inp.files } });
|
|
346
360
|
return { ok: true };
|
|
347
361
|
})()`);
|
|
348
362
|
if (fallbackResult && !fallbackResult.ok) return fallbackResult;
|
|
@@ -351,6 +365,30 @@ export async function sendWithFile(page, filePath, prompt) {
|
|
|
351
365
|
const ready = await waitForFilePreview(page, fileName);
|
|
352
366
|
if (!ready) return { ok: false, reason: 'file preview did not appear' };
|
|
353
367
|
|
|
368
|
+
// File preview appears immediately but send button stays disabled until
|
|
369
|
+
// the server upload finishes. Wait for it.
|
|
370
|
+
let sendEnabled = false;
|
|
371
|
+
for (let tick = 0; tick < 15; tick++) {
|
|
372
|
+
const enabled = await page.evaluate(`(() => {
|
|
373
|
+
var box = document.querySelector('${TEXTAREA_SELECTOR}');
|
|
374
|
+
if (!box) return false;
|
|
375
|
+
var c = box.parentElement;
|
|
376
|
+
while (c && !c.querySelector('div[role="button"]')) c = c.parentElement;
|
|
377
|
+
if (!c) return false;
|
|
378
|
+
var btns = c.querySelectorAll('div[role="button"]:not(.ds-toggle-button)');
|
|
379
|
+
var last = btns[btns.length - 1];
|
|
380
|
+
return !!(last && last.getAttribute('aria-disabled') === 'false');
|
|
381
|
+
})()`);
|
|
382
|
+
if (enabled) {
|
|
383
|
+
sendEnabled = true;
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
await page.wait(1);
|
|
387
|
+
}
|
|
388
|
+
if (!sendEnabled) {
|
|
389
|
+
return { ok: false, reason: 'send button did not enable after upload' };
|
|
390
|
+
}
|
|
391
|
+
|
|
354
392
|
return sendMessage(page, prompt);
|
|
355
393
|
}
|
|
356
394
|
|
|
@@ -107,15 +107,36 @@ describe('deepseek sendWithFile', () => {
|
|
|
107
107
|
setFileInput: vi.fn().mockResolvedValue(undefined),
|
|
108
108
|
wait: vi.fn().mockResolvedValue(undefined),
|
|
109
109
|
evaluate: vi.fn()
|
|
110
|
-
.mockResolvedValueOnce(undefined)
|
|
111
|
-
.mockResolvedValueOnce(true)
|
|
112
|
-
.mockResolvedValueOnce(
|
|
110
|
+
.mockResolvedValueOnce(undefined) // sidebar collapse
|
|
111
|
+
.mockResolvedValueOnce(true) // waitForFilePreview
|
|
112
|
+
.mockResolvedValueOnce(true) // send button enabled check
|
|
113
|
+
.mockResolvedValueOnce({ ok: true }), // sendMessage
|
|
113
114
|
};
|
|
114
115
|
|
|
115
116
|
const result = await sendWithFile(page, filePath, 'summarize this');
|
|
116
117
|
|
|
117
|
-
expect(result).toEqual({ ok: true });
|
|
118
|
-
|
|
118
|
+
expect(result).toEqual({ ok: true }); expect(page.setFileInput).toHaveBeenCalledWith([filePath], 'input[type="file"]');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('fails closed when upload preview appears but send button never enables', async () => {
|
|
122
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-deepseek-'));
|
|
123
|
+
tempDirs.push(dir);
|
|
124
|
+
const filePath = path.join(dir, 'report.txt');
|
|
125
|
+
fs.writeFileSync(filePath, 'hello');
|
|
126
|
+
|
|
127
|
+
const page = {
|
|
128
|
+
setFileInput: vi.fn().mockResolvedValue(undefined),
|
|
129
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
130
|
+
evaluate: vi.fn()
|
|
131
|
+
.mockResolvedValueOnce(undefined) // sidebar collapse
|
|
132
|
+
.mockResolvedValueOnce(true) // waitForFilePreview
|
|
133
|
+
.mockResolvedValue(false), // send button never enables
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const result = await sendWithFile(page, filePath, 'summarize this');
|
|
137
|
+
|
|
138
|
+
expect(result).toEqual({ ok: false, reason: 'send button did not enable after upload' });
|
|
139
|
+
expect(page.evaluate).toHaveBeenCalledTimes(17);
|
|
119
140
|
});
|
|
120
141
|
});
|
|
121
142
|
|
|
@@ -142,4 +163,102 @@ describe('deepseek selectModel', () => {
|
|
|
142
163
|
expect(result).toEqual({ ok: false });
|
|
143
164
|
expect(instantRadio.click).not.toHaveBeenCalled();
|
|
144
165
|
});
|
|
166
|
+
|
|
167
|
+
it('selects the correct radio for each model', async () => {
|
|
168
|
+
const radios = [0, 1, 2].map(() => ({
|
|
169
|
+
getAttribute: vi.fn(() => 'false'),
|
|
170
|
+
click: vi.fn(),
|
|
171
|
+
}));
|
|
172
|
+
global.document = {
|
|
173
|
+
querySelectorAll: vi.fn(() => radios),
|
|
174
|
+
};
|
|
175
|
+
const page = {
|
|
176
|
+
evaluate: vi.fn(async (script) => eval(script)),
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
await selectModel(page, 'instant');
|
|
180
|
+
expect(radios[0].click).toHaveBeenCalled();
|
|
181
|
+
expect(radios[1].click).not.toHaveBeenCalled();
|
|
182
|
+
expect(radios[2].click).not.toHaveBeenCalled();
|
|
183
|
+
|
|
184
|
+
radios.forEach(r => r.click.mockClear());
|
|
185
|
+
await selectModel(page, 'expert');
|
|
186
|
+
expect(radios[1].click).toHaveBeenCalled();
|
|
187
|
+
|
|
188
|
+
radios.forEach(r => r.click.mockClear());
|
|
189
|
+
await selectModel(page, 'vision');
|
|
190
|
+
expect(radios[2].click).toHaveBeenCalled();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('rejects unknown model names', async () => {
|
|
194
|
+
const radios = [0, 1, 2].map(() => ({
|
|
195
|
+
getAttribute: vi.fn(() => 'false'),
|
|
196
|
+
click: vi.fn(),
|
|
197
|
+
}));
|
|
198
|
+
global.document = {
|
|
199
|
+
querySelectorAll: vi.fn(() => radios),
|
|
200
|
+
};
|
|
201
|
+
const page = {
|
|
202
|
+
evaluate: vi.fn(async (script) => eval(script)),
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const result = await selectModel(page, 'turbo');
|
|
206
|
+
expect(result).toEqual({ ok: false });
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe('deepseek sendWithFile Not allowed fallback', () => {
|
|
211
|
+
const tempDirs = [];
|
|
212
|
+
|
|
213
|
+
afterEach(() => {
|
|
214
|
+
vi.restoreAllMocks();
|
|
215
|
+
while (tempDirs.length) {
|
|
216
|
+
fs.rmSync(tempDirs.pop(), { recursive: true, force: true });
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('falls back to DataTransfer when setFileInput throws Not allowed', async () => {
|
|
221
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-deepseek-'));
|
|
222
|
+
tempDirs.push(dir);
|
|
223
|
+
const filePath = path.join(dir, 'image.png');
|
|
224
|
+
fs.writeFileSync(filePath, 'fake-png');
|
|
225
|
+
|
|
226
|
+
const page = {
|
|
227
|
+
setFileInput: vi.fn().mockRejectedValue(new Error('Not allowed')),
|
|
228
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
229
|
+
evaluate: vi.fn()
|
|
230
|
+
.mockResolvedValueOnce(undefined) // sidebar collapse
|
|
231
|
+
.mockResolvedValueOnce({ ok: true }) // DataTransfer fallback
|
|
232
|
+
.mockResolvedValueOnce(true) // waitForFilePreview
|
|
233
|
+
.mockResolvedValueOnce(true) // send button enabled
|
|
234
|
+
.mockResolvedValueOnce({ ok: true }),// sendMessage
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const result = await sendWithFile(page, filePath, 'describe');
|
|
238
|
+
|
|
239
|
+
expect(page.setFileInput).toHaveBeenCalled();
|
|
240
|
+
expect(page.evaluate).toHaveBeenCalledTimes(5);
|
|
241
|
+
expect(result).toEqual({ ok: true });
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('does not treat send-button enablement alone as image upload proof', async () => {
|
|
245
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-deepseek-'));
|
|
246
|
+
tempDirs.push(dir);
|
|
247
|
+
const filePath = path.join(dir, 'image.png');
|
|
248
|
+
fs.writeFileSync(filePath, 'fake-png');
|
|
249
|
+
|
|
250
|
+
const page = {
|
|
251
|
+
setFileInput: vi.fn().mockResolvedValue(undefined),
|
|
252
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
253
|
+
evaluate: vi.fn()
|
|
254
|
+
.mockResolvedValueOnce(undefined) // sidebar collapse
|
|
255
|
+
.mockResolvedValue(false), // no filename / thumbnail preview
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const result = await sendWithFile(page, filePath, 'describe');
|
|
259
|
+
|
|
260
|
+
expect(result).toEqual({ ok: false, reason: 'file preview did not appear' });
|
|
261
|
+
expect(page.evaluate.mock.calls[1][0]).toContain('img[src], canvas, video');
|
|
262
|
+
expect(page.evaluate.mock.calls[1][0]).not.toContain("aria-disabled') === 'false'");
|
|
263
|
+
});
|
|
145
264
|
});
|