@jackwener/opencli 1.7.22 → 1.8.1
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 +35 -194
- package/README.zh-CN.md +42 -260
- package/cli-manifest.json +8160 -4392
- package/clis/12306/me.js +73 -0
- package/clis/12306/orders.js +96 -0
- package/clis/12306/passengers.js +90 -0
- package/clis/12306/price.js +166 -0
- package/clis/12306/stations.js +66 -0
- package/clis/12306/train.js +91 -0
- package/clis/12306/trains.js +119 -0
- package/clis/12306/utils.js +272 -0
- package/clis/12306/utils.test.js +331 -0
- package/clis/36kr/article.js +6 -3
- package/clis/36kr/article.test.js +46 -0
- package/clis/_atlassian/shared.js +577 -0
- package/clis/_atlassian/shared.test.js +170 -0
- package/clis/apple-podcasts/commands.test.js +20 -0
- package/clis/apple-podcasts/search.js +2 -2
- package/clis/barchart/greeks.js +144 -56
- package/clis/barchart/greeks.test.js +138 -0
- package/clis/bilibili/comment.js +125 -0
- package/clis/bilibili/comment.test.js +153 -0
- package/clis/bilibili/comments.js +116 -21
- package/clis/bilibili/comments.test.js +77 -18
- package/clis/bilibili/subtitle.js +76 -31
- package/clis/bilibili/subtitle.test.js +156 -9
- package/clis/bilibili/summary.js +167 -0
- package/clis/bilibili/summary.test.js +210 -0
- package/clis/bilibili/utils.js +63 -5
- package/clis/bilibili/utils.test.js +45 -1
- package/clis/booking/booking.test.js +356 -0
- package/clis/booking/search.js +351 -0
- package/clis/chatgpt/envelope.test.js +108 -0
- package/clis/chatgpt/image.js +2 -2
- package/clis/chatgpt/image.test.js +6 -0
- package/clis/chatgpt/utils.js +148 -41
- package/clis/chatgpt/utils.test.js +92 -2
- package/clis/chess/analyze.js +35 -0
- package/clis/chess/analyze.test.js +79 -0
- package/clis/chess/game.js +114 -0
- package/clis/chess/game.test.js +178 -0
- package/clis/chess/games.js +67 -0
- package/clis/chess/games.test.js +164 -0
- package/clis/chess/stats.js +32 -0
- package/clis/chess/stats.test.js +79 -0
- package/clis/chess/utils.js +170 -0
- package/clis/chess/utils.test.js +230 -0
- package/clis/confluence/commands.test.js +195 -0
- package/clis/confluence/create.js +39 -0
- package/clis/confluence/page.js +23 -0
- package/clis/confluence/search.js +34 -0
- package/clis/confluence/shared.js +173 -0
- package/clis/confluence/update.js +38 -0
- package/clis/douyin/_shared/browser-fetch.js +44 -20
- package/clis/douyin/_shared/browser-fetch.test.js +22 -1
- package/clis/douyin/_shared/evaluate-result.js +16 -0
- package/clis/douyin/_shared/tos-upload.js +105 -69
- package/clis/douyin/_shared/vod-upload.js +212 -0
- package/clis/douyin/_shared/vod-upload.test.js +38 -0
- package/clis/douyin/delete.js +137 -4
- package/clis/douyin/delete.test.js +90 -1
- package/clis/douyin/hashtag.js +84 -23
- package/clis/douyin/hashtag.test.js +113 -0
- package/clis/douyin/publish-upload-id.test.js +170 -0
- package/clis/douyin/publish.js +88 -42
- package/clis/douyin/user-videos.js +9 -2
- package/clis/douyin/user-videos.test.js +43 -0
- package/clis/flomo/memos.js +228 -0
- package/clis/flomo/memos.test.js +144 -0
- package/clis/geogebra/add-circle.js +46 -0
- package/clis/geogebra/add-line.js +35 -0
- package/clis/geogebra/add-point.js +27 -0
- package/clis/geogebra/add-polygon.js +25 -0
- package/clis/geogebra/eval.js +35 -0
- package/clis/geogebra/geogebra.test.js +175 -0
- package/clis/geogebra/hexagon.js +62 -0
- package/clis/geogebra/info.js +72 -0
- package/clis/geogebra/list.js +35 -0
- package/clis/geogebra/triangle.js +60 -0
- package/clis/geogebra/utils.js +271 -0
- package/clis/gitee/search.js +2 -2
- package/clis/gitee/search.test.js +65 -0
- package/clis/jike/post.js +27 -17
- package/clis/jike/read.test.js +86 -0
- package/clis/jike/topic.js +32 -19
- package/clis/jike/user.js +33 -20
- package/clis/jira/attachments.js +28 -0
- package/clis/jira/commands.test.js +287 -0
- package/clis/jira/comments.js +28 -0
- package/clis/jira/issue.js +28 -0
- package/clis/jira/links.js +28 -0
- package/clis/jira/search.js +47 -0
- package/clis/jira/shared.js +256 -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/frontpage.test.js +37 -0
- 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/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/linkedin/connect.js +401 -0
- package/clis/linkedin/connect.test.js +213 -0
- package/clis/linkedin/inbox.js +234 -0
- package/clis/linkedin/inbox.test.js +152 -0
- package/clis/linkedin/job-detail.js +167 -0
- package/clis/linkedin/job-detail.test.js +38 -0
- package/clis/linkedin/jobs-preferences.js +113 -0
- package/clis/linkedin/jobs-preferences.test.js +43 -0
- package/clis/linkedin/people-search.js +262 -0
- package/clis/linkedin/people-search.test.js +216 -0
- package/clis/linkedin/post-analytics.js +74 -0
- package/clis/linkedin/post-analytics.test.js +40 -0
- package/clis/linkedin/posts-core.js +241 -0
- package/clis/linkedin/posts.js +22 -0
- package/clis/linkedin/posts.test.js +40 -0
- package/clis/linkedin/profile-analytics.js +104 -0
- package/clis/linkedin/profile-analytics.test.js +67 -0
- package/clis/linkedin/profile-experience.js +671 -0
- package/clis/linkedin/profile-experience.test.js +152 -0
- package/clis/linkedin/profile-projects.js +311 -0
- package/clis/linkedin/profile-projects.test.js +111 -0
- package/clis/linkedin/profile-read.js +148 -0
- package/clis/linkedin/profile-read.test.js +77 -0
- package/clis/linkedin/safe-send.js +357 -0
- package/clis/linkedin/safe-send.test.js +204 -0
- package/clis/linkedin/salesnav-inbox.js +210 -0
- package/clis/linkedin/salesnav-inbox.test.js +113 -0
- package/clis/linkedin/salesnav-message.js +360 -0
- package/clis/linkedin/salesnav-message.test.js +172 -0
- package/clis/linkedin/salesnav-search.js +186 -0
- package/clis/linkedin/salesnav-search.test.js +76 -0
- package/clis/linkedin/salesnav-thread.js +212 -0
- package/clis/linkedin/salesnav-thread.test.js +79 -0
- package/clis/linkedin/sent-invitations.js +92 -0
- package/clis/linkedin/sent-invitations.test.js +62 -0
- package/clis/linkedin/services-read.js +213 -0
- package/clis/linkedin/services-read.test.js +105 -0
- package/clis/linkedin/shared.js +124 -0
- package/clis/linkedin/thread-snapshot.js +214 -0
- package/clis/linkedin/thread-snapshot.test.js +89 -0
- package/clis/linkedin/timeline.js +14 -7
- package/clis/linkedin-learning/course.js +138 -0
- package/clis/linkedin-learning/course.test.js +114 -0
- package/clis/linkedin-learning/search.js +155 -0
- package/clis/linkedin-learning/search.test.js +144 -0
- package/clis/linkedin-learning/trending.js +133 -0
- package/clis/linkedin-learning/trending.test.js +123 -0
- package/clis/notebooklm/add-source.js +269 -0
- package/clis/notebooklm/add-source.test.js +97 -0
- package/clis/notebooklm/create.js +76 -0
- package/clis/notebooklm/create.test.js +58 -0
- package/clis/notebooklm/generate-audio.js +91 -0
- package/clis/notebooklm/generate-audio.test.js +63 -0
- package/clis/notebooklm/generate-slides.js +106 -0
- package/clis/notebooklm/generate-slides.test.js +75 -0
- package/clis/notebooklm/open.test.js +10 -10
- package/clis/notebooklm/rpc.js +20 -6
- package/clis/notebooklm/rpc.test.js +27 -1
- package/clis/notebooklm/utils.js +100 -24
- package/clis/notebooklm/utils.test.js +60 -1
- package/clis/notebooklm/write-note.js +103 -0
- package/clis/notebooklm/write-note.test.js +70 -0
- package/clis/pixiv/detail.js +41 -34
- package/clis/pixiv/detail.test.js +93 -0
- package/clis/pixiv/user.js +36 -31
- package/clis/pixiv/user.test.js +100 -0
- package/clis/pixiv/utils.js +56 -7
- package/clis/powerchina/search.js +3 -3
- package/clis/powerchina/search.test.js +27 -1
- package/clis/reddit/extract-media.test.js +149 -0
- package/clis/reddit/frontpage.js +47 -9
- package/clis/reddit/frontpage.test.js +34 -0
- package/clis/reddit/home.js +31 -1
- package/clis/reddit/home.test.js +46 -3
- package/clis/reddit/hot.js +32 -1
- package/clis/reddit/hot.test.js +15 -1
- package/clis/reddit/popular.js +39 -1
- package/clis/reddit/popular.test.js +26 -0
- package/clis/reddit/saved.js +1 -1
- package/clis/reddit/search.js +38 -1
- package/clis/reddit/search.test.js +26 -0
- package/clis/reddit/subreddit.js +52 -7
- package/clis/reddit/subreddit.test.js +31 -0
- package/clis/reddit/subscribed.js +165 -0
- package/clis/reddit/subscribed.test.js +168 -0
- package/clis/reddit/upvoted.js +1 -1
- package/clis/suno/commands.test.js +188 -0
- package/clis/suno/download.js +140 -0
- package/clis/suno/download.test.js +151 -0
- package/clis/suno/generate.js +231 -0
- package/clis/suno/generate.test.js +252 -0
- package/clis/suno/list.js +79 -0
- package/clis/suno/status.js +63 -0
- package/clis/suno/utils.js +549 -0
- package/clis/suno/utils.test.js +329 -0
- package/clis/twitter/device-follow.js +193 -0
- package/clis/twitter/device-follow.test.js +287 -0
- package/clis/twitter/download.js +443 -73
- package/clis/twitter/download.test.js +457 -0
- package/clis/twitter/followers.js +6 -2
- package/clis/twitter/followers.test.js +19 -1
- package/clis/twitter/following.js +14 -5
- package/clis/twitter/following.test.js +29 -0
- package/clis/twitter/likes.js +12 -4
- package/clis/twitter/likes.test.js +26 -1
- package/clis/twitter/list-add.js +1 -1
- package/clis/twitter/list-create.js +155 -0
- package/clis/twitter/list-create.test.js +169 -0
- package/clis/twitter/list-remove.js +13 -6
- package/clis/twitter/list-remove.test.js +74 -0
- package/clis/twitter/list-tweets.js +6 -2
- package/clis/twitter/list-tweets.test.js +41 -1
- package/clis/twitter/lists.js +31 -4
- package/clis/twitter/lists.test.js +152 -16
- package/clis/twitter/notifications.js +4 -4
- package/clis/twitter/post.js +62 -4
- package/clis/twitter/post.test.js +35 -3
- package/clis/twitter/profile.js +81 -28
- package/clis/twitter/profile.test.js +113 -2
- package/clis/twitter/quote.js +9 -4
- package/clis/twitter/reply.js +13 -10
- package/clis/twitter/reply.test.js +41 -0
- package/clis/twitter/search.js +7 -3
- package/clis/twitter/search.test.js +41 -0
- package/clis/twitter/shared.js +155 -0
- package/clis/twitter/shared.test.js +465 -1
- package/clis/twitter/thread.js +10 -2
- package/clis/twitter/thread.test.js +58 -0
- package/clis/twitter/timeline.js +6 -2
- package/clis/twitter/timeline.test.js +2 -0
- package/clis/twitter/tweets.js +3 -2
- package/clis/twitter/tweets.test.js +1 -1
- package/clis/twitter/utils.js +53 -16
- package/clis/upwork/detail.js +132 -0
- package/clis/upwork/feed.js +109 -0
- package/clis/upwork/search.js +115 -0
- package/clis/upwork/upwork.test.js +566 -0
- package/clis/upwork/utils.js +323 -0
- package/clis/weibo/delete.js +172 -0
- package/clis/weibo/delete.test.js +94 -0
- package/clis/weibo/publish.js +37 -14
- package/clis/weibo/publish.test.js +14 -5
- package/clis/weibo/user-posts.js +234 -0
- package/clis/weibo/user-posts.test.js +92 -0
- package/clis/weread/book-search.js +438 -0
- package/clis/weread/book-search.test.js +242 -0
- package/clis/weread/search-regression.test.js +98 -11
- package/clis/weread/search.js +32 -9
- package/clis/weread-official/book.js +135 -0
- package/clis/weread-official/commands.test.js +385 -0
- package/clis/weread-official/discover.js +107 -0
- package/clis/weread-official/list-apis.js +95 -0
- package/clis/weread-official/notes.js +171 -0
- package/clis/weread-official/readdata.js +158 -0
- package/clis/weread-official/review.js +93 -0
- package/clis/weread-official/search.js +106 -0
- package/clis/weread-official/shelf.js +97 -0
- package/clis/weread-official/utils.js +293 -0
- package/clis/weread-official/utils.test.js +242 -0
- package/clis/wikipedia/trending.js +7 -3
- package/clis/wikipedia/trending.test.js +57 -0
- package/clis/xianyu/chat.js +24 -109
- package/clis/xianyu/chat.test.js +5 -0
- package/clis/xianyu/im.js +322 -0
- package/clis/xianyu/im.test.js +253 -0
- package/clis/xianyu/inbox.js +96 -0
- package/clis/xianyu/messages.js +91 -0
- package/clis/xianyu/reply.js +82 -0
- package/clis/xiaohongshu/creator-note-detail.js +166 -28
- package/clis/xiaohongshu/creator-note-detail.test.js +196 -36
- package/clis/xiaohongshu/creator-notes-summary.js +2 -1
- package/clis/xiaohongshu/creator-notes-summary.test.js +7 -0
- package/clis/xiaohongshu/creator-notes.js +252 -2
- package/clis/xiaohongshu/creator-notes.test.js +90 -1
- package/clis/xiaohongshu/creator-stats.js +2 -1
- package/clis/xiaohongshu/creator-stats.test.js +24 -0
- package/clis/xiaohongshu/delete-note.js +260 -0
- package/clis/xiaohongshu/delete-note.test.js +172 -0
- package/clis/xiaohongshu/download.js +97 -39
- package/clis/xiaohongshu/download.test.js +201 -0
- package/clis/xiaohongshu/publish.js +48 -8
- package/clis/xiaohongshu/publish.test.js +65 -10
- package/clis/xiaohongshu/user-helpers.test.js +41 -0
- package/clis/xiaohongshu/user.js +27 -4
- package/clis/xiaoyuzhou/download.js +1 -1
- package/clis/xiaoyuzhou/transcript.js +1 -1
- package/clis/youdao/note.js +258 -0
- package/clis/youdao/note.test.js +99 -0
- package/clis/youtube/transcript.js +397 -24
- package/clis/youtube/transcript.test.js +196 -6
- package/clis/zhihu/answer-comments.js +280 -0
- package/clis/zhihu/answer-comments.test.js +287 -0
- package/clis/zhihu/answer-detail.js +2 -19
- package/clis/zhihu/answer-detail.test.js +8 -0
- package/clis/zhihu/collection.js +17 -16
- package/clis/zhihu/collection.test.js +50 -3
- package/clis/zhihu/download.js +1 -1
- package/clis/zhihu/question.js +42 -17
- package/clis/zhihu/question.test.js +113 -11
- package/clis/zhihu/search.js +195 -43
- package/clis/zhihu/search.test.js +198 -0
- package/clis/zhihu/text.js +29 -0
- package/clis/zhihu/text.test.js +24 -0
- package/dist/src/browser/errors.js +4 -2
- package/dist/src/browser/errors.test.js +6 -0
- package/dist/src/browser/network-cache.js +13 -1
- package/dist/src/browser/network-cache.test.js +17 -0
- package/dist/src/browser/page.js +30 -4
- package/dist/src/browser/page.test.js +42 -0
- package/dist/src/browser/utils.d.ts +1 -1
- package/dist/src/cli-argv-preprocess.d.ts +26 -0
- package/dist/src/cli-argv-preprocess.js +138 -0
- package/dist/src/cli-argv-preprocess.test.js +79 -0
- package/dist/src/convention-audit.js +15 -8
- package/dist/src/convention-audit.test.js +21 -0
- package/dist/src/download/index.js +13 -1
- package/dist/src/download/index.test.js +23 -1
- package/dist/src/download/media-download.js +15 -2
- package/dist/src/download/media-download.test.d.ts +1 -0
- package/dist/src/download/media-download.test.js +112 -0
- package/dist/src/download/progress.js +2 -2
- package/dist/src/download/progress.test.js +12 -1
- package/dist/src/electron-apps.js +1 -1
- package/dist/src/electron-apps.test.js +7 -2
- package/dist/src/errors.d.ts +17 -0
- package/dist/src/errors.js +22 -0
- package/dist/src/external-clis.yaml +8 -0
- package/dist/src/main.js +14 -2
- package/dist/src/output.js +11 -1
- package/dist/src/output.test.js +6 -0
- package/dist/src/registry.js +1 -0
- package/dist/src/registry.test.js +11 -0
- package/dist/src/utils.d.ts +43 -0
- package/dist/src/utils.js +97 -0
- package/dist/src/utils.test.d.ts +1 -0
- package/dist/src/utils.test.js +155 -0
- package/package.json +8 -2
- package/scripts/silent-column-drop-baseline.json +0 -52
- package/scripts/typed-error-lint-baseline.json +28 -380
- package/clis/slock/_utils.js +0 -12
package/clis/chatgpt/utils.js
CHANGED
|
@@ -74,7 +74,6 @@ function buildComposerLocatorScript() {
|
|
|
74
74
|
};
|
|
75
75
|
|
|
76
76
|
findComposer.toString = () => 'findComposer';
|
|
77
|
-
return { findComposer, markerAttr };
|
|
78
77
|
`;
|
|
79
78
|
}
|
|
80
79
|
|
|
@@ -103,6 +102,50 @@ export function requirePositiveInt(value, flagLabel, hint) {
|
|
|
103
102
|
return value;
|
|
104
103
|
}
|
|
105
104
|
|
|
105
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
106
|
+
// page.evaluate envelope helpers.
|
|
107
|
+
//
|
|
108
|
+
// The browser bridge wraps every `page.evaluate(...)` return value in a
|
|
109
|
+
// `{ session, data }` envelope. Adapters that read `.length` or
|
|
110
|
+
// `Array.isArray(payload)` directly on the envelope silently see "no data" —
|
|
111
|
+
// this matches the failure mode fixed for xiaohongshu/rednote (#1561) and
|
|
112
|
+
// weibo (#1568).
|
|
113
|
+
//
|
|
114
|
+
// `unwrapEvaluateResult` is a defensive ternary: it unwraps when the payload
|
|
115
|
+
// looks like an envelope, otherwise passes the value through unchanged so
|
|
116
|
+
// older bridge versions and primitive return values still work.
|
|
117
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
118
|
+
export function unwrapEvaluateResult(payload) {
|
|
119
|
+
if (payload && !Array.isArray(payload) && typeof payload === 'object' && 'session' in payload && 'data' in payload) {
|
|
120
|
+
return payload.data;
|
|
121
|
+
}
|
|
122
|
+
return payload;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function requireArrayEvaluateResult(payload, label) {
|
|
126
|
+
if (!Array.isArray(payload)) {
|
|
127
|
+
if (payload && typeof payload === 'object' && 'error' in payload) {
|
|
128
|
+
throw new CommandExecutionError(`${label}: ${String(payload.error)}`);
|
|
129
|
+
}
|
|
130
|
+
throw new CommandExecutionError(`${label} returned malformed extraction payload`);
|
|
131
|
+
}
|
|
132
|
+
return payload;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function requireObjectEvaluateResult(payload, label) {
|
|
136
|
+
if (!payload || Array.isArray(payload) || typeof payload !== 'object') {
|
|
137
|
+
throw new CommandExecutionError(`${label} returned malformed extraction payload`);
|
|
138
|
+
}
|
|
139
|
+
return payload;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function requireBooleanEvaluateResult(payload, label) {
|
|
143
|
+
if (typeof payload !== 'boolean') {
|
|
144
|
+
throw new CommandExecutionError(`${label} returned malformed extraction payload`);
|
|
145
|
+
}
|
|
146
|
+
return payload;
|
|
147
|
+
}
|
|
148
|
+
|
|
106
149
|
export function parseChatGPTConversationId(value) {
|
|
107
150
|
const raw = String(value ?? '').trim();
|
|
108
151
|
const match = raw.match(/(?:^|\/c\/)([A-Za-z0-9_-]{8,})(?:[/?#]|$)/);
|
|
@@ -115,7 +158,7 @@ export function parseChatGPTConversationId(value) {
|
|
|
115
158
|
}
|
|
116
159
|
|
|
117
160
|
export async function currentChatGPTUrl(page) {
|
|
118
|
-
const url = await page.evaluate('window.location.href').catch(() => '');
|
|
161
|
+
const url = unwrapEvaluateResult(await page.evaluate('window.location.href').catch(() => ''));
|
|
119
162
|
return typeof url === 'string' ? url : '';
|
|
120
163
|
}
|
|
121
164
|
|
|
@@ -161,7 +204,7 @@ export async function startNewChat(page) {
|
|
|
161
204
|
}
|
|
162
205
|
|
|
163
206
|
export async function getPageState(page) {
|
|
164
|
-
return await page.evaluate(`(() => {
|
|
207
|
+
return requireObjectEvaluateResult(unwrapEvaluateResult(await page.evaluate(`(() => {
|
|
165
208
|
const isVisible = (el) => {
|
|
166
209
|
if (!(el instanceof HTMLElement)) return false;
|
|
167
210
|
const style = window.getComputedStyle(el);
|
|
@@ -187,7 +230,7 @@ export async function getPageState(page) {
|
|
|
187
230
|
isLoggedIn: hasComposer || !!userMenu || !hasLoginGate,
|
|
188
231
|
hasLoginGate,
|
|
189
232
|
};
|
|
190
|
-
})()`);
|
|
233
|
+
})()`)), 'chatgpt page state');
|
|
191
234
|
}
|
|
192
235
|
|
|
193
236
|
export async function ensureChatGPTLogin(page, message = 'ChatGPT requires a logged-in browser session.') {
|
|
@@ -258,7 +301,7 @@ export async function sendChatGPTMessage(page, text) {
|
|
|
258
301
|
// findComposer() retries inside a single CDP call, so no fixed sleep is
|
|
259
302
|
// needed before reading the composer.
|
|
260
303
|
|
|
261
|
-
const typeResult = await page.evaluate(`
|
|
304
|
+
const typeResult = requireBooleanEvaluateResult(unwrapEvaluateResult(await page.evaluate(`
|
|
262
305
|
(() => {
|
|
263
306
|
${buildComposerLocatorScript()}
|
|
264
307
|
const composer = findComposer();
|
|
@@ -276,8 +319,8 @@ export async function sendChatGPTMessage(page, text) {
|
|
|
276
319
|
composer.dispatchEvent(new Event('change', { bubbles: true }));
|
|
277
320
|
return true;
|
|
278
321
|
})()
|
|
279
|
-
`);
|
|
280
|
-
|
|
322
|
+
`)), 'chatgpt composer readiness');
|
|
323
|
+
|
|
281
324
|
if (!typeResult) return false;
|
|
282
325
|
|
|
283
326
|
// Use page.type() which is Playwright's native method
|
|
@@ -304,7 +347,7 @@ export async function sendChatGPTMessage(page, text) {
|
|
|
304
347
|
let sent = null;
|
|
305
348
|
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
306
349
|
await page.wait(0.5);
|
|
307
|
-
sent = await page.evaluate(`
|
|
350
|
+
sent = requireObjectEvaluateResult(unwrapEvaluateResult(await page.evaluate(`
|
|
308
351
|
(() => {
|
|
309
352
|
const isUsable = (button) => button
|
|
310
353
|
&& !button.disabled
|
|
@@ -318,7 +361,7 @@ export async function sendChatGPTMessage(page, text) {
|
|
|
318
361
|
: btns.find(b => labels.includes(b.getAttribute('aria-label') || '') && isUsable(b));
|
|
319
362
|
return { sendBtnFound: !!sendBtn };
|
|
320
363
|
})()
|
|
321
|
-
`);
|
|
364
|
+
`)), 'chatgpt send button readiness');
|
|
322
365
|
if (sent?.sendBtnFound) break;
|
|
323
366
|
}
|
|
324
367
|
|
|
@@ -339,7 +382,7 @@ export async function sendChatGPTMessage(page, text) {
|
|
|
339
382
|
}
|
|
340
383
|
|
|
341
384
|
export async function getVisibleMessages(page) {
|
|
342
|
-
const result = await page.evaluate(`(() => {
|
|
385
|
+
const result = requireArrayEvaluateResult(unwrapEvaluateResult(await page.evaluate(`(() => {
|
|
343
386
|
const isVisible = (el) => {
|
|
344
387
|
if (!(el instanceof HTMLElement)) return false;
|
|
345
388
|
const style = window.getComputedStyle(el);
|
|
@@ -385,8 +428,7 @@ export async function getVisibleMessages(page) {
|
|
|
385
428
|
rows.push({ role, text, html });
|
|
386
429
|
}
|
|
387
430
|
return rows;
|
|
388
|
-
})()`);
|
|
389
|
-
if (!Array.isArray(result)) return [];
|
|
431
|
+
})()`)), 'chatgpt visible messages');
|
|
390
432
|
return result.map((item, index) => ({
|
|
391
433
|
Index: index + 1,
|
|
392
434
|
Role: item?.role === 'Assistant' ? 'Assistant' : 'User',
|
|
@@ -448,7 +490,7 @@ export async function getConversationList(page) {
|
|
|
448
490
|
// so the previous standalone 2 s settle is redundant.
|
|
449
491
|
await ensureOnChatGPT(page);
|
|
450
492
|
|
|
451
|
-
const openSidebar = await page.evaluate(`(() => {
|
|
493
|
+
const openSidebar = requireBooleanEvaluateResult(unwrapEvaluateResult(await page.evaluate(`(() => {
|
|
452
494
|
const button = Array.from(document.querySelectorAll('button'))
|
|
453
495
|
.find((node) => /open sidebar/i.test(node.getAttribute('aria-label') || ''));
|
|
454
496
|
if (button instanceof HTMLElement) {
|
|
@@ -456,7 +498,7 @@ export async function getConversationList(page) {
|
|
|
456
498
|
return true;
|
|
457
499
|
}
|
|
458
500
|
return false;
|
|
459
|
-
})()`);
|
|
501
|
+
})()`)), 'chatgpt sidebar open state');
|
|
460
502
|
if (openSidebar) {
|
|
461
503
|
try {
|
|
462
504
|
await page.wait({ selector: CONVERSATION_LINK_SELECTOR, timeout: 3 });
|
|
@@ -480,7 +522,7 @@ export async function getConversationList(page) {
|
|
|
480
522
|
}
|
|
481
523
|
|
|
482
524
|
async function extractConversationLinks(page) {
|
|
483
|
-
const items = await page.evaluate(`(() => {
|
|
525
|
+
const items = requireArrayEvaluateResult(unwrapEvaluateResult(await page.evaluate(`(() => {
|
|
484
526
|
const isVisible = (el) => {
|
|
485
527
|
if (!(el instanceof HTMLElement)) return false;
|
|
486
528
|
const style = window.getComputedStyle(el);
|
|
@@ -505,15 +547,13 @@ async function extractConversationLinks(page) {
|
|
|
505
547
|
});
|
|
506
548
|
}
|
|
507
549
|
return rows;
|
|
508
|
-
})()`);
|
|
509
|
-
return
|
|
510
|
-
? items.map((item, index) => ({
|
|
550
|
+
})()`)), 'chatgpt conversation link extraction');
|
|
551
|
+
return items.map((item, index) => ({
|
|
511
552
|
Index: index + 1,
|
|
512
553
|
Id: String(item?.Id || ''),
|
|
513
554
|
Title: String(item?.Title || '(untitled)').trim() || '(untitled)',
|
|
514
555
|
Url: String(item?.Url || ''),
|
|
515
|
-
})).filter((item) => item.Id)
|
|
516
|
-
: [];
|
|
556
|
+
})).filter((item) => item.Id);
|
|
517
557
|
}
|
|
518
558
|
|
|
519
559
|
function imageMimeFromPath(filePath) {
|
|
@@ -556,7 +596,7 @@ async function waitForChatGPTUploadPreview(page, fileNames) {
|
|
|
556
596
|
const namesJson = JSON.stringify(fileNames);
|
|
557
597
|
for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
558
598
|
await page.wait(1);
|
|
559
|
-
const ready = await page.evaluate(`
|
|
599
|
+
const ready = requireBooleanEvaluateResult(unwrapEvaluateResult(await page.evaluate(`
|
|
560
600
|
(() => {
|
|
561
601
|
const names = ${namesJson};
|
|
562
602
|
const text = document.body ? (document.body.innerText || '') : '';
|
|
@@ -572,7 +612,7 @@ async function waitForChatGPTUploadPreview(page, fileNames) {
|
|
|
572
612
|
const previewNodes = scope.querySelectorAll('img[src], canvas, video, [style*="background-image"], [data-testid*="attachment"], [data-testid*="upload"], [class*="attachment"], [class*="upload"]');
|
|
573
613
|
return previewNodes.length >= names.length;
|
|
574
614
|
})()
|
|
575
|
-
`);
|
|
615
|
+
`)), 'chatgpt upload preview detection');
|
|
576
616
|
if (ready) return true;
|
|
577
617
|
}
|
|
578
618
|
return false;
|
|
@@ -606,7 +646,7 @@ export async function uploadChatGPTImages(page, imagePaths) {
|
|
|
606
646
|
mime: imageMimeFromPath(absPath),
|
|
607
647
|
base64: fs.default.readFileSync(absPath).toString('base64'),
|
|
608
648
|
}));
|
|
609
|
-
const fallbackResult = await page.evaluate(`
|
|
649
|
+
const fallbackResult = requireObjectEvaluateResult(unwrapEvaluateResult(await page.evaluate(`
|
|
610
650
|
(() => {
|
|
611
651
|
const files = ${JSON.stringify(files)};
|
|
612
652
|
const input = document.querySelector('input[type="file"]');
|
|
@@ -642,7 +682,7 @@ export async function uploadChatGPTImages(page, imagePaths) {
|
|
|
642
682
|
}
|
|
643
683
|
return { ok: true };
|
|
644
684
|
})()
|
|
645
|
-
`);
|
|
685
|
+
`)), 'chatgpt image upload fallback');
|
|
646
686
|
if (fallbackResult && !fallbackResult.ok) return fallbackResult;
|
|
647
687
|
}
|
|
648
688
|
|
|
@@ -656,21 +696,21 @@ export async function uploadChatGPTImages(page, imagePaths) {
|
|
|
656
696
|
* Check if ChatGPT is still generating a response.
|
|
657
697
|
*/
|
|
658
698
|
export async function isGenerating(page) {
|
|
659
|
-
return await page.evaluate(`
|
|
699
|
+
return requireBooleanEvaluateResult(unwrapEvaluateResult(await page.evaluate(`
|
|
660
700
|
(() => {
|
|
661
701
|
return Array.from(document.querySelectorAll('button')).some(b => {
|
|
662
702
|
const label = b.getAttribute('aria-label') || '';
|
|
663
703
|
return label === 'Stop generating' || label.includes('Thinking');
|
|
664
704
|
});
|
|
665
705
|
})()
|
|
666
|
-
`);
|
|
706
|
+
`)), 'chatgpt generation state');
|
|
667
707
|
}
|
|
668
708
|
|
|
669
709
|
/**
|
|
670
710
|
* Get visible image URLs from the ChatGPT page (excluding profile/avatar images).
|
|
671
711
|
*/
|
|
672
712
|
export async function getChatGPTVisibleImageUrls(page) {
|
|
673
|
-
return await page.evaluate(`
|
|
713
|
+
return requireArrayEvaluateResult(unwrapEvaluateResult(await page.evaluate(`
|
|
674
714
|
(() => {
|
|
675
715
|
const isVisible = (el) => {
|
|
676
716
|
if (!(el instanceof HTMLElement)) return false;
|
|
@@ -680,32 +720,78 @@ export async function getChatGPTVisibleImageUrls(page) {
|
|
|
680
720
|
return rect.width > 32 && rect.height > 32;
|
|
681
721
|
};
|
|
682
722
|
|
|
723
|
+
const urls = [];
|
|
724
|
+
const seen = new Set();
|
|
725
|
+
const normalizeUrl = (value) => {
|
|
726
|
+
const raw = String(value || '').trim();
|
|
727
|
+
if (!raw || raw === 'none') return '';
|
|
728
|
+
if (/^(?:https?:|blob:|data:)/i.test(raw)) return raw;
|
|
729
|
+
try {
|
|
730
|
+
return new URL(raw, window.location.href).href;
|
|
731
|
+
} catch {
|
|
732
|
+
return raw;
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
const addUrl = (value) => {
|
|
736
|
+
const src = normalizeUrl(value);
|
|
737
|
+
if (!src || seen.has(src)) return;
|
|
738
|
+
seen.add(src);
|
|
739
|
+
urls.push(src);
|
|
740
|
+
};
|
|
741
|
+
const isDecorative = (el, src = '') => {
|
|
742
|
+
const alt = (el.getAttribute('alt') || '').toLowerCase();
|
|
743
|
+
const cls = String(el.className || '').toLowerCase();
|
|
744
|
+
const testId = (el.getAttribute('data-testid') || '').toLowerCase();
|
|
745
|
+
const label = (el.getAttribute('aria-label') || '').toLowerCase();
|
|
746
|
+
const text = [alt, cls, testId, label, src.toLowerCase()].join(' ');
|
|
747
|
+
return /avatar|profile|logo|icon/.test(text);
|
|
748
|
+
};
|
|
749
|
+
|
|
683
750
|
const imgs = Array.from(document.querySelectorAll('img')).filter(img =>
|
|
684
751
|
img instanceof HTMLImageElement && isVisible(img)
|
|
685
752
|
);
|
|
686
753
|
|
|
687
|
-
const urls = [];
|
|
688
|
-
const seen = new Set();
|
|
689
|
-
|
|
690
754
|
for (const img of imgs) {
|
|
691
755
|
const src = img.currentSrc || img.src || '';
|
|
692
|
-
const alt = (img.getAttribute('alt') || '').toLowerCase();
|
|
693
|
-
const cls = (img.className || '').toLowerCase();
|
|
694
756
|
const width = img.naturalWidth || img.width || 0;
|
|
695
757
|
const height = img.naturalHeight || img.height || 0;
|
|
696
758
|
|
|
697
759
|
if (!src) continue;
|
|
698
|
-
if (
|
|
699
|
-
if (cls.includes('avatar') || cls.includes('profile') || cls.includes('icon')) continue;
|
|
760
|
+
if (isDecorative(img, src)) continue;
|
|
700
761
|
if (width < 128 && height < 128) continue;
|
|
701
|
-
|
|
762
|
+
addUrl(src);
|
|
763
|
+
}
|
|
702
764
|
|
|
703
|
-
|
|
704
|
-
|
|
765
|
+
// ChatGPT occasionally renders generated images as CSS background
|
|
766
|
+
// thumbnails instead of plain <img> nodes. Treat visible, large
|
|
767
|
+
// background images as generated-image candidates too.
|
|
768
|
+
for (const el of Array.from(document.querySelectorAll('[style*="background-image"], [style*="background"]'))) {
|
|
769
|
+
if (!(el instanceof HTMLElement) || !isVisible(el) || isDecorative(el)) continue;
|
|
770
|
+
const rect = el.getBoundingClientRect();
|
|
771
|
+
if (rect.width < 128 && rect.height < 128) continue;
|
|
772
|
+
const backgroundImage = window.getComputedStyle(el).backgroundImage || '';
|
|
773
|
+
for (const match of backgroundImage.matchAll(/url\\((['"]?)(.*?)\\1\\)/g)) {
|
|
774
|
+
const src = match[2];
|
|
775
|
+
if (!src || isDecorative(el, src)) continue;
|
|
776
|
+
addUrl(src);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// Some image experiences render to a canvas. Returning the data URL
|
|
781
|
+
// lets the downstream asset exporter save it without needing a DOM
|
|
782
|
+
// selector to rediscover the canvas.
|
|
783
|
+
for (const canvas of Array.from(document.querySelectorAll('canvas'))) {
|
|
784
|
+
if (!(canvas instanceof HTMLCanvasElement) || !isVisible(canvas) || isDecorative(canvas)) continue;
|
|
785
|
+
const width = canvas.width || canvas.getBoundingClientRect().width || 0;
|
|
786
|
+
const height = canvas.height || canvas.getBoundingClientRect().height || 0;
|
|
787
|
+
if (width < 128 && height < 128) continue;
|
|
788
|
+
try {
|
|
789
|
+
addUrl(canvas.toDataURL('image/png'));
|
|
790
|
+
} catch { }
|
|
705
791
|
}
|
|
706
792
|
return urls;
|
|
707
793
|
})()
|
|
708
|
-
`);
|
|
794
|
+
`)), 'chatgpt visible image url extraction');
|
|
709
795
|
}
|
|
710
796
|
|
|
711
797
|
/**
|
|
@@ -723,7 +809,7 @@ export async function waitForChatGPTImages(page, beforeUrls, timeoutSeconds, con
|
|
|
723
809
|
|
|
724
810
|
let currentUrl = '';
|
|
725
811
|
if (convUrl && convUrl.includes('/c/')) {
|
|
726
|
-
currentUrl = await page.evaluate('window.location.href').catch(() => '');
|
|
812
|
+
currentUrl = unwrapEvaluateResult(await page.evaluate('window.location.href').catch(() => ''));
|
|
727
813
|
if (currentUrl && !isSameChatGPTConversation(currentUrl, convUrl)) {
|
|
728
814
|
await page.goto(convUrl);
|
|
729
815
|
await page.wait(3);
|
|
@@ -766,6 +852,7 @@ export const __test__ = {
|
|
|
766
852
|
SEND_BUTTON_FALLBACK_SELECTORS,
|
|
767
853
|
SEND_BUTTON_LABELS,
|
|
768
854
|
CLOSE_SIDEBAR_LABELS,
|
|
855
|
+
buildComposerLocatorScript,
|
|
769
856
|
isSameChatGPTConversation,
|
|
770
857
|
parseChatGPTConversationId,
|
|
771
858
|
imageMimeFromPath,
|
|
@@ -776,7 +863,7 @@ export const __test__ = {
|
|
|
776
863
|
*/
|
|
777
864
|
export async function getChatGPTImageAssets(page, urls) {
|
|
778
865
|
const urlsJson = JSON.stringify(urls);
|
|
779
|
-
return await page.evaluate(`
|
|
866
|
+
return requireArrayEvaluateResult(unwrapEvaluateResult(await page.evaluate(`
|
|
780
867
|
(async (targetUrls) => {
|
|
781
868
|
const blobToDataUrl = (blob) => new Promise((resolve, reject) => {
|
|
782
869
|
const reader = new FileReader();
|
|
@@ -809,6 +896,26 @@ export async function getChatGPTImageAssets(page, urls) {
|
|
|
809
896
|
if (img) {
|
|
810
897
|
width = img.naturalWidth || img.width || 0;
|
|
811
898
|
height = img.naturalHeight || img.height || 0;
|
|
899
|
+
} else {
|
|
900
|
+
const backgroundEl = Array.from(document.querySelectorAll('[style*="background-image"], [style*="background"]')).find(el => {
|
|
901
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
902
|
+
const backgroundImage = window.getComputedStyle(el).backgroundImage || '';
|
|
903
|
+
return Array.from(backgroundImage.matchAll(/url\\((['"]?)(.*?)\\1\\)/g)).some(match => {
|
|
904
|
+
const raw = String(match[2] || '').trim();
|
|
905
|
+
if (!raw) return false;
|
|
906
|
+
if (raw === targetUrl) return true;
|
|
907
|
+
try {
|
|
908
|
+
return new URL(raw, window.location.href).href === targetUrl;
|
|
909
|
+
} catch {
|
|
910
|
+
return false;
|
|
911
|
+
}
|
|
912
|
+
});
|
|
913
|
+
});
|
|
914
|
+
if (backgroundEl) {
|
|
915
|
+
const rect = backgroundEl.getBoundingClientRect();
|
|
916
|
+
width = Math.round(rect.width || 0);
|
|
917
|
+
height = Math.round(rect.height || 0);
|
|
918
|
+
}
|
|
812
919
|
}
|
|
813
920
|
|
|
814
921
|
try {
|
|
@@ -850,5 +957,5 @@ export async function getChatGPTImageAssets(page, urls) {
|
|
|
850
957
|
|
|
851
958
|
return results;
|
|
852
959
|
})(${urlsJson})
|
|
853
|
-
|
|
960
|
+
`)), 'chatgpt image asset export');
|
|
854
961
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { JSDOM } from 'jsdom';
|
|
4
5
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
-
import { __test__, prepareChatGPTImagePaths, sendChatGPTMessage, uploadChatGPTImages, waitForChatGPTImages } from './utils.js';
|
|
6
|
+
import { __test__, getChatGPTImageAssets, getChatGPTVisibleImageUrls, prepareChatGPTImagePaths, sendChatGPTMessage, uploadChatGPTImages, waitForChatGPTImages } from './utils.js';
|
|
6
7
|
|
|
7
8
|
const tempDirs = [];
|
|
8
9
|
|
|
@@ -88,6 +89,25 @@ describe('chatgpt conversation id parsing', () => {
|
|
|
88
89
|
});
|
|
89
90
|
|
|
90
91
|
describe('chatgpt send selectors', () => {
|
|
92
|
+
it('inlines the composer locator without returning before caller code runs', () => {
|
|
93
|
+
const dom = new JSDOM('<!doctype html><div id="prompt-textarea" contenteditable="true"></div>', {
|
|
94
|
+
url: 'https://chatgpt.com/',
|
|
95
|
+
runScripts: 'outside-only',
|
|
96
|
+
});
|
|
97
|
+
const composer = dom.window.document.querySelector('#prompt-textarea');
|
|
98
|
+
composer.getBoundingClientRect = () => ({ width: 320, height: 48 });
|
|
99
|
+
|
|
100
|
+
const result = dom.window.eval(`
|
|
101
|
+
(() => {
|
|
102
|
+
${__test__.buildComposerLocatorScript()}
|
|
103
|
+
const composer = findComposer();
|
|
104
|
+
return !!composer && composer.getAttribute(markerAttr) === '1';
|
|
105
|
+
})()
|
|
106
|
+
`);
|
|
107
|
+
|
|
108
|
+
expect(result).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
91
111
|
it('keeps locale-independent send-button selector before aria-label fallbacks', async () => {
|
|
92
112
|
const page = {
|
|
93
113
|
wait: vi.fn().mockResolvedValue(undefined),
|
|
@@ -143,6 +163,73 @@ describe('chatgpt send selectors', () => {
|
|
|
143
163
|
});
|
|
144
164
|
});
|
|
145
165
|
|
|
166
|
+
describe('chatgpt generated image detection', () => {
|
|
167
|
+
function createDomPage(html, setup = () => {}) {
|
|
168
|
+
const dom = new JSDOM(html, {
|
|
169
|
+
url: 'https://chatgpt.com/c/demo',
|
|
170
|
+
runScripts: 'outside-only',
|
|
171
|
+
});
|
|
172
|
+
setup(dom.window);
|
|
173
|
+
return {
|
|
174
|
+
evaluate: vi.fn((script) => Promise.resolve(dom.window.eval(String(script)))),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
it('detects visible CSS background images when ChatGPT does not render a plain img', async () => {
|
|
179
|
+
const page = createDomPage(`
|
|
180
|
+
<!doctype html>
|
|
181
|
+
<main>
|
|
182
|
+
<div class="avatar" style="background-image: url('https://chatgpt.com/avatar.png')"></div>
|
|
183
|
+
<button data-testid="generated-image" style="background-image: url('/backend-api/generated/foo.webp')"></button>
|
|
184
|
+
</main>
|
|
185
|
+
`, (window) => {
|
|
186
|
+
for (const el of window.document.querySelectorAll('div, button')) {
|
|
187
|
+
el.getBoundingClientRect = () => ({ width: 512, height: 512 });
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
await expect(getChatGPTVisibleImageUrls(page)).resolves.toEqual([
|
|
192
|
+
'https://chatgpt.com/backend-api/generated/foo.webp',
|
|
193
|
+
]);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('detects visible generated canvases as data URLs', async () => {
|
|
197
|
+
const page = createDomPage('<!doctype html><canvas width="512" height="512"></canvas>', (window) => {
|
|
198
|
+
const canvas = window.document.querySelector('canvas');
|
|
199
|
+
canvas.getBoundingClientRect = () => ({ width: 512, height: 512 });
|
|
200
|
+
canvas.toDataURL = () => 'data:image/png;base64,ZmFrZQ==';
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
await expect(getChatGPTVisibleImageUrls(page)).resolves.toEqual([
|
|
204
|
+
'data:image/png;base64,ZmFrZQ==',
|
|
205
|
+
]);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('exports assets for generated CSS background images', async () => {
|
|
209
|
+
const imageUrl = 'https://chatgpt.com/backend-api/generated/foo.webp';
|
|
210
|
+
const page = createDomPage(`
|
|
211
|
+
<!doctype html>
|
|
212
|
+
<button style="background-image: url('/backend-api/generated/foo.webp')"></button>
|
|
213
|
+
`, (window) => {
|
|
214
|
+
const button = window.document.querySelector('button');
|
|
215
|
+
button.getBoundingClientRect = () => ({ width: 512, height: 512 });
|
|
216
|
+
window.fetch = vi.fn().mockResolvedValue({
|
|
217
|
+
ok: true,
|
|
218
|
+
blob: async () => new window.Blob(['fake-image'], { type: 'image/webp' }),
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
await expect(getChatGPTImageAssets(page, [imageUrl])).resolves.toEqual([
|
|
223
|
+
expect.objectContaining({
|
|
224
|
+
url: imageUrl,
|
|
225
|
+
mimeType: 'image/webp',
|
|
226
|
+
width: 512,
|
|
227
|
+
height: 512,
|
|
228
|
+
}),
|
|
229
|
+
]);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
146
233
|
describe('chatgpt image upload helper', () => {
|
|
147
234
|
it('validates local images without a browser page', async () => {
|
|
148
235
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-chatgpt-'));
|
|
@@ -218,7 +305,10 @@ describe('chatgpt image upload helper', () => {
|
|
|
218
305
|
setFileInput: vi.fn().mockRejectedValue(new Error('No element found')),
|
|
219
306
|
wait: vi.fn().mockResolvedValue(undefined),
|
|
220
307
|
evaluate: vi.fn((script) => {
|
|
221
|
-
|
|
308
|
+
if (String(script).includes('new DataTransfer()')) {
|
|
309
|
+
return Promise.resolve({ ok: true });
|
|
310
|
+
}
|
|
311
|
+
return Promise.resolve(true);
|
|
222
312
|
}),
|
|
223
313
|
};
|
|
224
314
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Open a Chess.com game in the browser's analysis view. Thin wrapper:
|
|
3
|
+
* navigates the bound session to the `/analysis` form of the game URL
|
|
4
|
+
* and reports the resolved page URL.
|
|
5
|
+
*/
|
|
6
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
7
|
+
import { CommandExecutionError } from '@jackwener/opencli/errors';
|
|
8
|
+
import { parseGameUrl } from './utils.js';
|
|
9
|
+
|
|
10
|
+
cli({
|
|
11
|
+
site: 'chess',
|
|
12
|
+
name: 'analyze',
|
|
13
|
+
access: 'read',
|
|
14
|
+
description: 'Open a Chess.com game in the browser analysis board',
|
|
15
|
+
domain: 'www.chess.com',
|
|
16
|
+
strategy: Strategy.UI,
|
|
17
|
+
browser: true,
|
|
18
|
+
navigateBefore: false,
|
|
19
|
+
args: [
|
|
20
|
+
{ name: 'game-url', type: 'string', required: true, positional: true, help: 'Full game URL, e.g. https://www.chess.com/game/live/168842570216' },
|
|
21
|
+
],
|
|
22
|
+
columns: ['kind', 'game_id', 'analysis_url'],
|
|
23
|
+
func: async (page, kwargs) => {
|
|
24
|
+
if (!page) throw new CommandExecutionError('Browser session required for chess analyze');
|
|
25
|
+
const { kind, id } = parseGameUrl(kwargs['game-url']);
|
|
26
|
+
const analysisUrl = `https://www.chess.com/analysis/game/${kind}/${id}`;
|
|
27
|
+
try {
|
|
28
|
+
await page.goto(analysisUrl);
|
|
29
|
+
await page.wait(2);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
throw new CommandExecutionError(`Failed to open Chess.com analysis board: ${error?.message || error}`);
|
|
32
|
+
}
|
|
33
|
+
return [{ kind, game_id: id, analysis_url: analysisUrl }];
|
|
34
|
+
},
|
|
35
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { getRegistry } from '@jackwener/opencli/registry';
|
|
3
|
+
import { ArgumentError, CommandExecutionError } from '@jackwener/opencli/errors';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { dirname, resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import './analyze.js';
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const manifestPath = resolve(__dirname, '../../cli-manifest.json');
|
|
11
|
+
|
|
12
|
+
function loadManifestCommand(name) {
|
|
13
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
14
|
+
return manifest.find(cmd => cmd.site === 'chess' && cmd.name === name);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function makePage() {
|
|
18
|
+
return {
|
|
19
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
20
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('chess analyze command', () => {
|
|
25
|
+
it('navigates to /analysis/game/<kind>/<id> and reports the URL', async () => {
|
|
26
|
+
const cmd = getRegistry().get('chess/analyze');
|
|
27
|
+
const page = makePage();
|
|
28
|
+
const rows = await cmd.func(page, { 'game-url': 'https://www.chess.com/game/live/42' });
|
|
29
|
+
expect(rows).toEqual([{ kind: 'live', game_id: '42', analysis_url: 'https://www.chess.com/analysis/game/live/42' }]);
|
|
30
|
+
expect(page.goto).toHaveBeenCalledWith('https://www.chess.com/analysis/game/live/42');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('preserves daily kind in the analysis URL', async () => {
|
|
34
|
+
const cmd = getRegistry().get('chess/analyze');
|
|
35
|
+
const page = makePage();
|
|
36
|
+
const rows = await cmd.func(page, { 'game-url': 'https://www.chess.com/game/daily/123' });
|
|
37
|
+
expect(rows[0].analysis_url).toBe('https://www.chess.com/analysis/game/daily/123');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('rejects invalid URL with ArgumentError before navigation', async () => {
|
|
41
|
+
const cmd = getRegistry().get('chess/analyze');
|
|
42
|
+
const page = makePage();
|
|
43
|
+
await expect(cmd.func(page, { 'game-url': 'not-a-url' })).rejects.toBeInstanceOf(ArgumentError);
|
|
44
|
+
expect(page.goto).not.toHaveBeenCalled();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('throws CommandExecutionError without a browser page', async () => {
|
|
48
|
+
const cmd = getRegistry().get('chess/analyze');
|
|
49
|
+
await expect(cmd.func(null, { 'game-url': 'https://www.chess.com/game/live/42' }))
|
|
50
|
+
.rejects.toBeInstanceOf(CommandExecutionError);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('throws CommandExecutionError when browser navigation fails', async () => {
|
|
54
|
+
const cmd = getRegistry().get('chess/analyze');
|
|
55
|
+
const page = makePage();
|
|
56
|
+
page.goto.mockRejectedValue(new Error('navigation failed'));
|
|
57
|
+
await expect(cmd.func(page, { 'game-url': 'https://www.chess.com/game/live/42' }))
|
|
58
|
+
.rejects.toBeInstanceOf(CommandExecutionError);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('registers with the expected columns + browser flag', () => {
|
|
62
|
+
const cmd = getRegistry().get('chess/analyze');
|
|
63
|
+
expect(cmd?.columns).toEqual(['kind', 'game_id', 'analysis_url']);
|
|
64
|
+
expect(cmd?.browser).toBe(true);
|
|
65
|
+
expect(cmd?.navigateBefore).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('build manifest keeps analyze pre-navigation disabled and game source attribution stable', () => {
|
|
69
|
+
expect(loadManifestCommand('analyze')).toMatchObject({
|
|
70
|
+
navigateBefore: false,
|
|
71
|
+
modulePath: 'chess/analyze.js',
|
|
72
|
+
sourceFile: 'chess/analyze.js',
|
|
73
|
+
});
|
|
74
|
+
expect(loadManifestCommand('game')).toMatchObject({
|
|
75
|
+
modulePath: 'chess/game.js',
|
|
76
|
+
sourceFile: 'chess/game.js',
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|