@sovovs/bycli 1.8.0 → 2.0.0

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.
Files changed (509) hide show
  1. package/cli-manifest.json +0 -61
  2. package/dist/src/adapter-shadow.js +2 -2
  3. package/dist/src/browser/analyze.d.ts +10 -99
  4. package/dist/src/browser/analyze.js +10 -226
  5. package/dist/src/browser/cdp.js +64 -7
  6. package/dist/src/browser/navigation-guard.d.ts +78 -0
  7. package/dist/src/browser/navigation-guard.js +142 -0
  8. package/dist/src/browser/navigation-guard.test.d.ts +1 -0
  9. package/dist/src/browser/url-policy.d.ts +89 -0
  10. package/dist/src/browser/url-policy.js +311 -0
  11. package/dist/src/browser/url-policy.test.d.ts +1 -0
  12. package/dist/src/browser/verify-fixture.js +2 -2
  13. package/dist/src/cli.js +10 -13
  14. package/dist/src/config-paths.d.ts +19 -0
  15. package/dist/src/config-paths.js +31 -0
  16. package/dist/src/daemon.js +240 -0
  17. package/dist/src/discovery.d.ts +5 -3
  18. package/dist/src/discovery.js +8 -6
  19. package/dist/src/execution.js +8 -3
  20. package/dist/src/main.js +18 -2
  21. package/dist/src/plugin.js +11 -8
  22. package/dist/src/recorder/highlevel/analyze.d.ts +29 -0
  23. package/dist/src/recorder/highlevel/analyze.js +91 -0
  24. package/dist/src/recorder/highlevel/init.d.ts +125 -0
  25. package/dist/src/recorder/highlevel/init.js +279 -0
  26. package/dist/src/recorder/highlevel/init.test.d.ts +1 -0
  27. package/dist/src/recorder/highlevel/verify.d.ts +73 -0
  28. package/dist/src/recorder/highlevel/verify.js +74 -0
  29. package/dist/src/recorder/http/analyze-runner.d.ts +28 -0
  30. package/dist/src/recorder/http/analyze-runner.js +78 -0
  31. package/dist/src/recorder/http/analyze-runner.test.d.ts +1 -0
  32. package/dist/src/recorder/http/handlers.d.ts +51 -0
  33. package/dist/src/recorder/http/handlers.js +219 -0
  34. package/dist/src/recorder/http/wrapper-config.d.ts +20 -0
  35. package/dist/src/recorder/http/wrapper-config.js +82 -0
  36. package/dist/src/recorder/http/wrapper-envelope.d.ts +31 -0
  37. package/dist/src/recorder/http/wrapper-envelope.js +51 -0
  38. package/dist/src/recorder/http/wrapper-gates.d.ts +16 -0
  39. package/dist/src/recorder/http/wrapper-gates.js +28 -0
  40. package/dist/src/recorder/http/wrapper-metrics.d.ts +1 -0
  41. package/dist/src/recorder/http/wrapper-metrics.js +5 -0
  42. package/dist/src/recorder/http/wrapper-registry.d.ts +50 -0
  43. package/dist/src/recorder/http/wrapper-registry.js +98 -0
  44. package/dist/src/recorder/http/wrapper-server.d.ts +59 -0
  45. package/dist/src/recorder/http/wrapper-server.js +240 -0
  46. package/dist/src/recorder/http/wrapper.test.d.ts +1 -0
  47. package/dist/src/recorder/observability/logger.d.ts +5 -0
  48. package/dist/src/recorder/observability/logger.js +13 -0
  49. package/dist/src/recorder/runner/config.d.ts +34 -0
  50. package/dist/src/recorder/runner/config.js +84 -0
  51. package/dist/src/recorder/runner/config.test.d.ts +1 -0
  52. package/dist/src/recorder/runner/reap.d.ts +79 -0
  53. package/dist/src/recorder/runner/reap.js +230 -0
  54. package/dist/src/recorder/runner/reap.test.d.ts +1 -0
  55. package/dist/src/recorder/runner/runner-port.d.ts +95 -0
  56. package/dist/src/recorder/runner/runner-port.js +518 -0
  57. package/dist/src/recorder/runner/runner-port.test.d.ts +1 -0
  58. package/dist/src/recorder/runner/session-keys.d.ts +28 -0
  59. package/dist/src/recorder/runner/session-keys.js +72 -0
  60. package/dist/src/recorder/runner/session-keys.test.d.ts +1 -0
  61. package/dist/src/recorder/runner/verify-runner-main.d.ts +95 -0
  62. package/dist/src/recorder/runner/verify-runner-main.js +275 -0
  63. package/dist/src/recorder/runner/verify-runner-main.test.d.ts +1 -0
  64. package/package.json +8 -5
  65. package/scripts/m1-daemon-navigate-drive.mjs +138 -0
  66. package/scripts/m1-extension-spike.mjs +237 -0
  67. package/scripts/m1-live-spike.mjs +156 -0
  68. package/scripts/m6b-connect-back-spike.sh +31 -0
  69. package/scripts/recorder.sh +186 -0
  70. package/clis/12306/utils.test.js +0 -331
  71. package/clis/1688/assets.test.js +0 -39
  72. package/clis/1688/download.test.js +0 -31
  73. package/clis/1688/item.test.js +0 -67
  74. package/clis/1688/search.test.js +0 -75
  75. package/clis/1688/shared.test.js +0 -57
  76. package/clis/1688/store.test.js +0 -62
  77. package/clis/36kr/article.test.js +0 -46
  78. package/clis/36kr/hot.test.js +0 -15
  79. package/clis/36kr/news.test.js +0 -85
  80. package/clis/51job/utils.test.js +0 -69
  81. package/clis/_atlassian/shared.test.js +0 -170
  82. package/clis/aibase/news.test.js +0 -59
  83. package/clis/amazon/bestsellers.test.js +0 -29
  84. package/clis/amazon/discussion.test.js +0 -124
  85. package/clis/amazon/offer.test.js +0 -29
  86. package/clis/amazon/product.test.js +0 -24
  87. package/clis/amazon/rankings.test.js +0 -41
  88. package/clis/amazon/search.test.js +0 -22
  89. package/clis/amazon/shared.test.js +0 -44
  90. package/clis/apple-podcasts/commands.test.js +0 -119
  91. package/clis/apple-podcasts/utils.test.js +0 -57
  92. package/clis/arxiv/arxiv.test.js +0 -112
  93. package/clis/baidu-scholar/search.test.js +0 -23
  94. package/clis/barchart/greeks.test.js +0 -138
  95. package/clis/bilibili/comment.test.js +0 -153
  96. package/clis/bilibili/comments.test.js +0 -142
  97. package/clis/bilibili/dynamic.test.js +0 -68
  98. package/clis/bilibili/hot.test.js +0 -17
  99. package/clis/bilibili/subtitle.test.js +0 -196
  100. package/clis/bilibili/summary.test.js +0 -210
  101. package/clis/bilibili/utils.test.js +0 -61
  102. package/clis/bilibili/video.test.js +0 -132
  103. package/clis/binance/commands.test.js +0 -70
  104. package/clis/bloomberg/utils.test.js +0 -129
  105. package/clis/booking/booking.test.js +0 -356
  106. package/clis/boss/chatlist.test.js +0 -211
  107. package/clis/boss/chatmsg.test.js +0 -230
  108. package/clis/boss/search.test.js +0 -78
  109. package/clis/boss/utils.test.js +0 -34
  110. package/clis/brave/search.test.js +0 -76
  111. package/clis/chaoxing/utils.test.js +0 -45
  112. package/clis/chatgpt/commands.test.js +0 -45
  113. package/clis/chatgpt/envelope.test.js +0 -108
  114. package/clis/chatgpt/image.test.js +0 -188
  115. package/clis/chatgpt/utils.test.js +0 -330
  116. package/clis/chatgpt-app/ax.test.js +0 -35
  117. package/clis/chatwise/composer.test.js +0 -186
  118. package/clis/chess/analyze.test.js +0 -79
  119. package/clis/chess/game.test.js +0 -178
  120. package/clis/chess/games.test.js +0 -164
  121. package/clis/chess/stats.test.js +0 -79
  122. package/clis/chess/utils.test.js +0 -230
  123. package/clis/claude/ask.test.js +0 -338
  124. package/clis/claude/commands.test.js +0 -118
  125. package/clis/claude/utils.test.js +0 -148
  126. package/clis/cnki/search.test.js +0 -18
  127. package/clis/codex/sidebar.test.js +0 -329
  128. package/clis/coingecko/coingecko.test.js +0 -109
  129. package/clis/confluence/commands.test.js +0 -195
  130. package/clis/coupang/coupang.test.js +0 -159
  131. package/clis/coupang/utils.test.js +0 -62
  132. package/clis/ctrip/ctrip.test.js +0 -719
  133. package/clis/dblp/dblp.test.js +0 -397
  134. package/clis/deepseek/ask.test.js +0 -312
  135. package/clis/deepseek/detail.test.js +0 -81
  136. package/clis/deepseek/send.test.js +0 -107
  137. package/clis/deepseek/utils.test.js +0 -370
  138. package/clis/defillama/defillama.test.js +0 -99
  139. package/clis/devto/devto.test.js +0 -236
  140. package/clis/dianping/dianping.test.js +0 -578
  141. package/clis/douban/download.test.js +0 -170
  142. package/clis/douban/movie-hot.test.js +0 -14
  143. package/clis/douban/search.test.js +0 -11
  144. package/clis/douban/subject.test.js +0 -11
  145. package/clis/douban/utils.test.js +0 -438
  146. package/clis/doubao/detail.test.js +0 -42
  147. package/clis/doubao/history.test.js +0 -37
  148. package/clis/doubao/utils.test.js +0 -344
  149. package/clis/douyin/_shared/browser-fetch.test.js +0 -64
  150. package/clis/douyin/_shared/creation-id.test.js +0 -22
  151. package/clis/douyin/_shared/imagex-upload.test.js +0 -87
  152. package/clis/douyin/_shared/sts2.test.js +0 -27
  153. package/clis/douyin/_shared/text-extra.test.js +0 -37
  154. package/clis/douyin/_shared/timing.test.js +0 -28
  155. package/clis/douyin/_shared/tos-upload-short-read.test.js +0 -83
  156. package/clis/douyin/_shared/tos-upload.test.js +0 -229
  157. package/clis/douyin/_shared/transcode.test.js +0 -91
  158. package/clis/douyin/_shared/vod-upload.test.js +0 -38
  159. package/clis/douyin/activities.test.js +0 -55
  160. package/clis/douyin/collections.test.js +0 -45
  161. package/clis/douyin/delete.test.js +0 -100
  162. package/clis/douyin/draft.test.js +0 -335
  163. package/clis/douyin/drafts.test.js +0 -11
  164. package/clis/douyin/hashtag.test.js +0 -171
  165. package/clis/douyin/location.test.js +0 -23
  166. package/clis/douyin/profile.test.js +0 -46
  167. package/clis/douyin/publish-upload-id.test.js +0 -170
  168. package/clis/douyin/publish.test.js +0 -38
  169. package/clis/douyin/stats.test.js +0 -22
  170. package/clis/douyin/update.test.js +0 -11
  171. package/clis/douyin/user-videos.test.js +0 -151
  172. package/clis/douyin/videos.test.js +0 -54
  173. package/clis/duckduckgo/search.test.js +0 -128
  174. package/clis/duckduckgo/suggest.test.js +0 -66
  175. package/clis/eastmoney/hot-rank.test.js +0 -59
  176. package/clis/endoflife/endoflife.test.js +0 -51
  177. package/clis/facebook/feed.test.js +0 -169
  178. package/clis/facebook/marketplace.test.js +0 -91
  179. package/clis/facebook/notifications.test.js +0 -458
  180. package/clis/facebook/search.test.js +0 -55
  181. package/clis/flathub/flathub.test.js +0 -90
  182. package/clis/flomo/memos.test.js +0 -144
  183. package/clis/gemini/ask.test.js +0 -100
  184. package/clis/gemini/deep-research-result.test.js +0 -155
  185. package/clis/gemini/deep-research.test.js +0 -181
  186. package/clis/gemini/reply-state.test.js +0 -671
  187. package/clis/gemini/utils.test.js +0 -198
  188. package/clis/geogebra/geogebra.test.js +0 -175
  189. package/clis/gitee/search.test.js +0 -65
  190. package/clis/gitee/user.test.js +0 -63
  191. package/clis/google/utils.test.js +0 -75
  192. package/clis/google-scholar/cite.test.js +0 -47
  193. package/clis/google-scholar/profile.test.js +0 -49
  194. package/clis/google-scholar/search.test.js +0 -71
  195. package/clis/goproxy/goproxy.test.js +0 -103
  196. package/clis/gov-law/commands.test.js +0 -39
  197. package/clis/gov-policy/gov-policy.test.js +0 -224
  198. package/clis/grok/ask.test.js +0 -29
  199. package/clis/grok/utils.test.js +0 -103
  200. package/clis/hackernews/hackernews.test.js +0 -132
  201. package/clis/hf/hf.test.js +0 -16
  202. package/clis/hupu/hot.test.js +0 -224
  203. package/clis/imdb/utils.test.js +0 -88
  204. package/clis/indeed/indeed.test.js +0 -375
  205. package/clis/instagram/_shared/private-publish.test.js +0 -705
  206. package/clis/instagram/_shared/protocol-capture.test.js +0 -114
  207. package/clis/instagram/download.test.js +0 -118
  208. package/clis/instagram/note.test.js +0 -53
  209. package/clis/instagram/post.test.js +0 -1647
  210. package/clis/instagram/reel.test.js +0 -167
  211. package/clis/instagram/story.test.js +0 -134
  212. package/clis/jd/commands.test.js +0 -41
  213. package/clis/jd/item.test.js +0 -354
  214. package/clis/jianyu/search.test.js +0 -153
  215. package/clis/jianyu/shared/procurement-contract.test.js +0 -72
  216. package/clis/jianyu/shared/procurement-detail.test.js +0 -84
  217. package/clis/jike/read.test.js +0 -86
  218. package/clis/jira/commands.test.js +0 -287
  219. package/clis/juejin/search.js +0 -123
  220. package/clis/juejin/search.test.js +0 -98
  221. package/clis/lesswrong/frontpage.test.js +0 -37
  222. package/clis/lichess/lichess.test.js +0 -85
  223. package/clis/linkedin/connect.test.js +0 -213
  224. package/clis/linkedin/inbox.test.js +0 -152
  225. package/clis/linkedin/job-detail.test.js +0 -38
  226. package/clis/linkedin/jobs-preferences.test.js +0 -43
  227. package/clis/linkedin/people-search.test.js +0 -216
  228. package/clis/linkedin/post-analytics.test.js +0 -40
  229. package/clis/linkedin/posts.test.js +0 -40
  230. package/clis/linkedin/profile-analytics.test.js +0 -67
  231. package/clis/linkedin/profile-experience.test.js +0 -152
  232. package/clis/linkedin/profile-projects.test.js +0 -111
  233. package/clis/linkedin/profile-read.test.js +0 -77
  234. package/clis/linkedin/safe-send.test.js +0 -204
  235. package/clis/linkedin/salesnav-inbox.test.js +0 -113
  236. package/clis/linkedin/salesnav-message.test.js +0 -172
  237. package/clis/linkedin/salesnav-search.test.js +0 -76
  238. package/clis/linkedin/salesnav-thread.test.js +0 -79
  239. package/clis/linkedin/search.test.js +0 -222
  240. package/clis/linkedin/sent-invitations.test.js +0 -62
  241. package/clis/linkedin/services-read.test.js +0 -105
  242. package/clis/linkedin/thread-snapshot.test.js +0 -89
  243. package/clis/linkedin/timeline.test.js +0 -81
  244. package/clis/linkedin-learning/course.test.js +0 -114
  245. package/clis/linkedin-learning/search.test.js +0 -144
  246. package/clis/linkedin-learning/trending.test.js +0 -123
  247. package/clis/linux-do/feed.test.js +0 -153
  248. package/clis/linux-do/topic-content.test.js +0 -59
  249. package/clis/lobsters/lobsters.test.js +0 -169
  250. package/clis/notebooklm/add-source.test.js +0 -97
  251. package/clis/notebooklm/compat.test.js +0 -16
  252. package/clis/notebooklm/create.test.js +0 -58
  253. package/clis/notebooklm/generate-audio.test.js +0 -63
  254. package/clis/notebooklm/generate-slides.test.js +0 -75
  255. package/clis/notebooklm/history.test.js +0 -58
  256. package/clis/notebooklm/note-list.test.js +0 -56
  257. package/clis/notebooklm/notes-get.test.js +0 -72
  258. package/clis/notebooklm/open.test.js +0 -63
  259. package/clis/notebooklm/rpc.test.js +0 -131
  260. package/clis/notebooklm/source-fulltext.test.js +0 -106
  261. package/clis/notebooklm/source-get.test.js +0 -84
  262. package/clis/notebooklm/source-guide.test.js +0 -104
  263. package/clis/notebooklm/summary.test.js +0 -78
  264. package/clis/notebooklm/utils.test.js +0 -449
  265. package/clis/notebooklm/write-note.test.js +0 -70
  266. package/clis/nuget/nuget.test.js +0 -111
  267. package/clis/oeis/oeis.test.js +0 -88
  268. package/clis/ones/task-helpers.test.js +0 -12
  269. package/clis/ones/worklog.test.js +0 -20
  270. package/clis/openfda/openfda.test.js +0 -114
  271. package/clis/openreview/openreview.test.js +0 -427
  272. package/clis/osv/osv.test.js +0 -97
  273. package/clis/paperreview/commands.test.js +0 -243
  274. package/clis/paperreview/utils.test.js +0 -49
  275. package/clis/pixiv/download.test.js +0 -87
  276. package/clis/pixiv/illusts.test.js +0 -99
  277. package/clis/pixiv/search.test.js +0 -83
  278. package/clis/powerchina/search.test.js +0 -93
  279. package/clis/producthunt/utils.test.js +0 -64
  280. package/clis/pubmed/pubmed.test.js +0 -276
  281. package/clis/quark/utils.test.js +0 -58
  282. package/clis/qwen/utils.test.js +0 -45
  283. package/clis/reddit/extract-media.test.js +0 -149
  284. package/clis/reddit/frontpage.test.js +0 -34
  285. package/clis/reddit/home.test.js +0 -170
  286. package/clis/reddit/hot.test.js +0 -32
  287. package/clis/reddit/popular.test.js +0 -26
  288. package/clis/reddit/read.test.js +0 -335
  289. package/clis/reddit/reply.test.js +0 -89
  290. package/clis/reddit/search.test.js +0 -26
  291. package/clis/reddit/subreddit-info.test.js +0 -163
  292. package/clis/reddit/subreddit.test.js +0 -31
  293. package/clis/reddit/subscribed.test.js +0 -168
  294. package/clis/reddit/whoami.test.js +0 -105
  295. package/clis/rednote/navigation.test.js +0 -26
  296. package/clis/rednote/rednote.test.js +0 -222
  297. package/clis/rest-countries/rest-countries.test.js +0 -83
  298. package/clis/reuters/reuters.test.js +0 -299
  299. package/clis/rfc/rfc.test.js +0 -74
  300. package/clis/sinafinance/stock.test.js +0 -59
  301. package/clis/spotify/utils.test.js +0 -67
  302. package/clis/stackoverflow/stackoverflow.test.js +0 -404
  303. package/clis/steam/steam.test.js +0 -46
  304. package/clis/substack/utils.test.js +0 -44
  305. package/clis/suno/commands.test.js +0 -188
  306. package/clis/suno/download.test.js +0 -151
  307. package/clis/suno/generate.test.js +0 -252
  308. package/clis/suno/utils.test.js +0 -329
  309. package/clis/taobao/commands.test.js +0 -41
  310. package/clis/tdx/hot-rank.test.js +0 -59
  311. package/clis/ths/hot-rank.test.js +0 -64
  312. package/clis/tieba/commands.test.js +0 -79
  313. package/clis/tieba/utils.test.js +0 -290
  314. package/clis/tiktok/creator-videos.test.js +0 -113
  315. package/clis/tiktok/refactor.test.js +0 -389
  316. package/clis/tiktok/write-refactor.test.js +0 -370
  317. package/clis/toutiao/toutiao.test.js +0 -378
  318. package/clis/tvmaze/tvmaze.test.js +0 -93
  319. package/clis/twitter/bookmark-folder.test.js +0 -395
  320. package/clis/twitter/bookmark-folders.test.js +0 -152
  321. package/clis/twitter/bookmark.test.js +0 -74
  322. package/clis/twitter/bookmarks.test.js +0 -205
  323. package/clis/twitter/delete.test.js +0 -85
  324. package/clis/twitter/device-follow.test.js +0 -287
  325. package/clis/twitter/download.test.js +0 -457
  326. package/clis/twitter/followers.test.js +0 -62
  327. package/clis/twitter/following.test.js +0 -338
  328. package/clis/twitter/hide-reply.test.js +0 -76
  329. package/clis/twitter/like.test.js +0 -73
  330. package/clis/twitter/likes.test.js +0 -220
  331. package/clis/twitter/list-add.test.js +0 -133
  332. package/clis/twitter/list-create.test.js +0 -169
  333. package/clis/twitter/list-remove.test.js +0 -110
  334. package/clis/twitter/list-tweets.test.js +0 -196
  335. package/clis/twitter/lists.test.js +0 -253
  336. package/clis/twitter/post.test.js +0 -258
  337. package/clis/twitter/profile.test.js +0 -39
  338. package/clis/twitter/quote.test.js +0 -194
  339. package/clis/twitter/reply.test.js +0 -237
  340. package/clis/twitter/retweet.test.js +0 -73
  341. package/clis/twitter/search.test.js +0 -349
  342. package/clis/twitter/shared.test.js +0 -794
  343. package/clis/twitter/thread.test.js +0 -58
  344. package/clis/twitter/timeline.test.js +0 -104
  345. package/clis/twitter/trending.test.js +0 -15
  346. package/clis/twitter/tweets.test.js +0 -362
  347. package/clis/twitter/unbookmark.test.js +0 -73
  348. package/clis/twitter/unlike.test.js +0 -75
  349. package/clis/twitter/unretweet.test.js +0 -73
  350. package/clis/twitter/utils.test.js +0 -169
  351. package/clis/uisdc/news.test.js +0 -66
  352. package/clis/uiverse/_shared.test.js +0 -62
  353. package/clis/uiverse/navigation.test.js +0 -12
  354. package/clis/upwork/upwork.test.js +0 -566
  355. package/clis/wanfang/search.test.js +0 -23
  356. package/clis/web/read.test.js +0 -392
  357. package/clis/weibo/delete.test.js +0 -94
  358. package/clis/weibo/envelope.test.js +0 -85
  359. package/clis/weibo/favorites.test.js +0 -114
  360. package/clis/weibo/publish.test.js +0 -192
  361. package/clis/weibo/user-posts.test.js +0 -92
  362. package/clis/weibo/utils.test.js +0 -36
  363. package/clis/weixin/drafts.test.js +0 -69
  364. package/clis/weixin/search.test.js +0 -227
  365. package/clis/weread/ai-outline.test.js +0 -83
  366. package/clis/weread/book-search.test.js +0 -242
  367. package/clis/weread/commands.test.js +0 -398
  368. package/clis/weread/private-api-regression.test.js +0 -369
  369. package/clis/weread/search-regression.test.js +0 -494
  370. package/clis/weread/utils.test.js +0 -128
  371. package/clis/weread-official/commands.test.js +0 -385
  372. package/clis/weread-official/utils.test.js +0 -242
  373. package/clis/wikidata/wikidata.test.js +0 -83
  374. package/clis/wikipedia/trending.test.js +0 -57
  375. package/clis/wttr/wttr.test.js +0 -84
  376. package/clis/xianyu/chat.test.js +0 -84
  377. package/clis/xianyu/im.test.js +0 -253
  378. package/clis/xianyu/item.test.js +0 -56
  379. package/clis/xianyu/publish.test.js +0 -220
  380. package/clis/xianyu/search.test.js +0 -17
  381. package/clis/xiaoe/xiaoe.test.js +0 -486
  382. package/clis/xiaohongshu/comments.test.js +0 -258
  383. package/clis/xiaohongshu/creator-note-detail.test.js +0 -451
  384. package/clis/xiaohongshu/creator-notes-summary.test.js +0 -94
  385. package/clis/xiaohongshu/creator-notes.test.js +0 -281
  386. package/clis/xiaohongshu/creator-stats.test.js +0 -24
  387. package/clis/xiaohongshu/delete-note.test.js +0 -172
  388. package/clis/xiaohongshu/download.test.js +0 -316
  389. package/clis/xiaohongshu/navigation.test.js +0 -34
  390. package/clis/xiaohongshu/note.test.js +0 -249
  391. package/clis/xiaohongshu/publish.test.js +0 -608
  392. package/clis/xiaohongshu/search.test.js +0 -325
  393. package/clis/xiaohongshu/user-helpers.test.js +0 -160
  394. package/clis/xiaoyuzhou/auth.test.js +0 -124
  395. package/clis/xiaoyuzhou/download.test.js +0 -135
  396. package/clis/xiaoyuzhou/podcast-episodes.test.js +0 -78
  397. package/clis/xiaoyuzhou/transcript.test.js +0 -195
  398. package/clis/xiaoyuzhou/utils.test.js +0 -39
  399. package/clis/xueqiu/comments.test.js +0 -696
  400. package/clis/xueqiu/danjuan-utils.test.js +0 -41
  401. package/clis/xueqiu/utils.test.js +0 -26
  402. package/clis/yahoo/search.test.js +0 -94
  403. package/clis/youdao/note.test.js +0 -99
  404. package/clis/youtube/channel.test.js +0 -59
  405. package/clis/youtube/feed.test.js +0 -131
  406. package/clis/youtube/transcript-group.test.js +0 -99
  407. package/clis/youtube/transcript.test.js +0 -296
  408. package/clis/youtube/utils.test.js +0 -84
  409. package/clis/yuanbao/ask.test.js +0 -124
  410. package/clis/yuanbao/new.test.js +0 -30
  411. package/clis/yuanbao/shared.test.js +0 -80
  412. package/clis/zhihu/answer-comments.test.js +0 -287
  413. package/clis/zhihu/answer-detail.test.js +0 -338
  414. package/clis/zhihu/answer.test.js +0 -54
  415. package/clis/zhihu/collection.test.js +0 -337
  416. package/clis/zhihu/collections.test.js +0 -182
  417. package/clis/zhihu/comment.test.js +0 -50
  418. package/clis/zhihu/download.test.js +0 -12
  419. package/clis/zhihu/favorite.test.js +0 -59
  420. package/clis/zhihu/follow.test.js +0 -42
  421. package/clis/zhihu/like.test.js +0 -41
  422. package/clis/zhihu/question.test.js +0 -251
  423. package/clis/zhihu/recommend.test.js +0 -143
  424. package/clis/zhihu/search.test.js +0 -198
  425. package/clis/zhihu/target.test.js +0 -77
  426. package/clis/zhihu/text.test.js +0 -24
  427. package/clis/zhihu/write-shared.test.js +0 -176
  428. package/clis/zlibrary/commands.test.js +0 -65
  429. package/clis/zsxq/search.test.js +0 -24
  430. package/clis/zsxq/topic.test.js +0 -30
  431. package/clis/zsxq/topics.test.js +0 -24
  432. package/clis/zsxq/utils.test.js +0 -31
  433. package/dist/src/adapter-shadow.test.js +0 -49
  434. package/dist/src/adapter-source.test.js +0 -29
  435. package/dist/src/browser/analyze.test.js +0 -166
  436. package/dist/src/browser/article-extract.e2e.test.js +0 -105
  437. package/dist/src/browser/article-extract.test.js +0 -94
  438. package/dist/src/browser/ax-snapshot.test.js +0 -91
  439. package/dist/src/browser/base-page.test.js +0 -758
  440. package/dist/src/browser/cdp-click-fixture.test.js +0 -87
  441. package/dist/src/browser/cdp.test.js +0 -79
  442. package/dist/src/browser/compound.test.js +0 -175
  443. package/dist/src/browser/daemon-client.test.js +0 -199
  444. package/dist/src/browser/dom-helpers.test.js +0 -106
  445. package/dist/src/browser/dom-snapshot.test.js +0 -313
  446. package/dist/src/browser/errors.test.js +0 -60
  447. package/dist/src/browser/extract.test.js +0 -129
  448. package/dist/src/browser/find.test.js +0 -180
  449. package/dist/src/browser/html-tree.test.js +0 -181
  450. package/dist/src/browser/network-cache.test.js +0 -58
  451. package/dist/src/browser/network-key.test.js +0 -49
  452. package/dist/src/browser/page.test.js +0 -378
  453. package/dist/src/browser/shape-filter.test.js +0 -101
  454. package/dist/src/browser/shape.test.js +0 -82
  455. package/dist/src/browser/stealth.test.js +0 -134
  456. package/dist/src/browser/target-errors.test.js +0 -94
  457. package/dist/src/browser/target-resolver.test.js +0 -118
  458. package/dist/src/browser/utils.test.js +0 -29
  459. package/dist/src/browser/verify-fixture.test.js +0 -219
  460. package/dist/src/browser.test.js +0 -232
  461. package/dist/src/build-manifest.test.js +0 -341
  462. package/dist/src/capabilityRouting.test.js +0 -63
  463. package/dist/src/cli-argv-preprocess.test.js +0 -209
  464. package/dist/src/cli.test.js +0 -2803
  465. package/dist/src/commanderAdapter.test.js +0 -352
  466. package/dist/src/commands/daemon.test.js +0 -219
  467. package/dist/src/completion.test.js +0 -24
  468. package/dist/src/convention-audit.test.js +0 -247
  469. package/dist/src/daemon.test.js +0 -60
  470. package/dist/src/doctor.test.js +0 -263
  471. package/dist/src/download/article-download.test.js +0 -235
  472. package/dist/src/download/index.test.js +0 -140
  473. package/dist/src/download/media-download.test.js +0 -112
  474. package/dist/src/download/progress.test.js +0 -36
  475. package/dist/src/electron-apps.test.js +0 -49
  476. package/dist/src/engine.test.js +0 -312
  477. package/dist/src/errors.test.js +0 -109
  478. package/dist/src/execution.test.js +0 -623
  479. package/dist/src/extension-manifest-regression.test.js +0 -12
  480. package/dist/src/external.test.js +0 -146
  481. package/dist/src/help.test.js +0 -58
  482. package/dist/src/hooks.test.js +0 -92
  483. package/dist/src/interceptor.test.js +0 -81
  484. package/dist/src/launcher.test.js +0 -138
  485. package/dist/src/node-network.test.js +0 -55
  486. package/dist/src/observation/artifact.test.js +0 -121
  487. package/dist/src/observation/manager.test.js +0 -13
  488. package/dist/src/observation/redaction.test.js +0 -32
  489. package/dist/src/observation/retention.test.js +0 -118
  490. package/dist/src/observation/ring-buffer.test.js +0 -22
  491. package/dist/src/output.test.js +0 -53
  492. package/dist/src/package-exports.test.js +0 -112
  493. package/dist/src/pipeline/executor.test.js +0 -182
  494. package/dist/src/pipeline/steps/download.test.js +0 -102
  495. package/dist/src/pipeline/steps/fetch.test.js +0 -117
  496. package/dist/src/pipeline/template.test.js +0 -175
  497. package/dist/src/pipeline/transform.test.js +0 -133
  498. package/dist/src/plugin-manifest.test.js +0 -179
  499. package/dist/src/plugin-scaffold.test.js +0 -83
  500. package/dist/src/plugin.test.js +0 -1324
  501. package/dist/src/registry.test.js +0 -209
  502. package/dist/src/runtime-detect.test.js +0 -39
  503. package/dist/src/serialization.test.js +0 -78
  504. package/dist/src/snapshotFormatter.test.js +0 -521
  505. package/dist/src/tui.test.js +0 -19
  506. package/dist/src/update-check.test.js +0 -116
  507. package/dist/src/utils.test.js +0 -155
  508. package/dist/src/validate.test.js +0 -90
  509. package/dist/src/weixin-download.test.js +0 -45
@@ -1,1647 +0,0 @@
1
- import * as fs from 'node:fs';
2
- import * as os from 'node:os';
3
- import * as path from 'node:path';
4
- import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
- import { AuthRequiredError, CommandExecutionError } from '@sovovs/bycli/errors';
6
- import { getRegistry } from '@sovovs/bycli/registry';
7
- import * as privatePublish from './_shared/private-publish.js';
8
- import { buildClickActionJs, buildEnsureComposerOpenJs, buildInspectUploadStageJs, buildPublishStatusProbeJs } from './post.js';
9
- import './post.js';
10
- const tempDirs = [];
11
- function createTempImage(name = 'demo.jpg', bytes = Buffer.from([0xff, 0xd8, 0xff, 0xd9])) {
12
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'bycli-instagram-post-'));
13
- tempDirs.push(dir);
14
- const filePath = path.join(dir, name);
15
- fs.writeFileSync(filePath, bytes);
16
- return filePath;
17
- }
18
- function createTempVideo(name = 'demo.mp4', bytes = Buffer.from('video')) {
19
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'bycli-instagram-post-video-'));
20
- tempDirs.push(dir);
21
- const filePath = path.join(dir, name);
22
- fs.writeFileSync(filePath, bytes);
23
- return filePath;
24
- }
25
- function withInitialDialogDismiss(results) {
26
- return [{ ok: false }, ...results];
27
- }
28
- function createPageMock(evaluateResults, overrides = {}) {
29
- const evaluate = vi.fn();
30
- for (const result of evaluateResults) {
31
- evaluate.mockResolvedValueOnce(result);
32
- }
33
- return {
34
- goto: vi.fn().mockResolvedValue(undefined),
35
- evaluate,
36
- getCookies: vi.fn().mockResolvedValue([]),
37
- snapshot: vi.fn().mockResolvedValue(undefined),
38
- click: vi.fn().mockResolvedValue(undefined),
39
- typeText: vi.fn().mockResolvedValue(undefined),
40
- pressKey: vi.fn().mockResolvedValue(undefined),
41
- scrollTo: vi.fn().mockResolvedValue(undefined),
42
- getFormState: vi.fn().mockResolvedValue({ forms: [], orphanFields: [] }),
43
- wait: vi.fn().mockResolvedValue(undefined),
44
- tabs: vi.fn().mockResolvedValue([]),
45
- closeTab: vi.fn().mockResolvedValue(undefined),
46
- newTab: vi.fn().mockResolvedValue(undefined),
47
- selectTab: vi.fn().mockResolvedValue(undefined),
48
- networkRequests: vi.fn().mockResolvedValue([]),
49
- consoleMessages: vi.fn().mockResolvedValue([]),
50
- scroll: vi.fn().mockResolvedValue(undefined),
51
- autoScroll: vi.fn().mockResolvedValue(undefined),
52
- installInterceptor: vi.fn().mockResolvedValue(undefined),
53
- getInterceptedRequests: vi.fn().mockResolvedValue([]),
54
- waitForCapture: vi.fn().mockResolvedValue(undefined),
55
- screenshot: vi.fn().mockResolvedValue(''),
56
- setFileInput: vi.fn().mockResolvedValue(undefined),
57
- insertText: undefined,
58
- getCurrentUrl: vi.fn().mockResolvedValue(null),
59
- ...overrides,
60
- };
61
- }
62
- afterAll(() => {
63
- for (const dir of tempDirs) {
64
- fs.rmSync(dir, { recursive: true, force: true });
65
- }
66
- delete process.env.BYCLI_INSTAGRAM_CAPTURE;
67
- });
68
- describe('instagram auth detection', () => {
69
- it('does not treat generic homepage text containing "log in" as an auth failure', () => {
70
- const globalState = globalThis;
71
- const originalDocument = globalState.document;
72
- const originalWindow = globalState.window;
73
- globalState.document = {
74
- body: { innerText: 'Suggested for you Log in to see more content' },
75
- querySelector: () => null,
76
- querySelectorAll: () => [],
77
- };
78
- globalState.window = { location: { pathname: '/' } };
79
- try {
80
- expect(eval(buildEnsureComposerOpenJs())).toEqual({ ok: true });
81
- }
82
- finally {
83
- globalState.document = originalDocument;
84
- globalState.window = originalWindow;
85
- }
86
- });
87
- });
88
- describe('instagram publish status detection', () => {
89
- it('does not treat unrelated page text as share failure while the sharing dialog is still visible', () => {
90
- const globalState = globalThis;
91
- class MockHTMLElement {
92
- }
93
- const visibleDialog = new MockHTMLElement();
94
- visibleDialog.textContent = 'Sharing';
95
- visibleDialog.querySelector = () => null;
96
- visibleDialog.getBoundingClientRect = () => ({ width: 100, height: 100 });
97
- const originalDocument = globalState.document;
98
- const originalWindow = globalState.window;
99
- const originalHTMLElement = globalState.HTMLElement;
100
- globalState.HTMLElement = MockHTMLElement;
101
- globalState.document = {
102
- querySelectorAll: (selector) => selector === '[role="dialog"]' ? [visibleDialog] : [],
103
- };
104
- globalState.window = {
105
- location: { href: 'https://www.instagram.com/' },
106
- getComputedStyle: () => ({ display: 'block', visibility: 'visible' }),
107
- };
108
- try {
109
- expect(eval(buildPublishStatusProbeJs())).toEqual({
110
- ok: false,
111
- failed: false,
112
- settled: false,
113
- url: '',
114
- });
115
- }
116
- finally {
117
- globalState.document = originalDocument;
118
- globalState.window = originalWindow;
119
- globalState.HTMLElement = originalHTMLElement;
120
- }
121
- });
122
- it('does not treat a stale visible error dialog as share failure while sharing is still in progress', () => {
123
- const globalState = globalThis;
124
- class MockHTMLElement {
125
- }
126
- const sharingDialog = new MockHTMLElement();
127
- sharingDialog.textContent = 'Sharing';
128
- sharingDialog.querySelector = () => null;
129
- sharingDialog.getBoundingClientRect = () => ({ width: 100, height: 100 });
130
- const staleErrorDialog = new MockHTMLElement();
131
- staleErrorDialog.textContent = 'Something went wrong. Please try again. Try again';
132
- staleErrorDialog.querySelector = () => null;
133
- staleErrorDialog.getBoundingClientRect = () => ({ width: 100, height: 100 });
134
- const originalDocument = globalState.document;
135
- const originalWindow = globalState.window;
136
- const originalHTMLElement = globalState.HTMLElement;
137
- globalState.HTMLElement = MockHTMLElement;
138
- globalState.document = {
139
- querySelectorAll: (selector) => selector === '[role="dialog"]' ? [sharingDialog, staleErrorDialog] : [],
140
- };
141
- globalState.window = {
142
- location: { href: 'https://www.instagram.com/' },
143
- getComputedStyle: () => ({ display: 'block', visibility: 'visible' }),
144
- };
145
- try {
146
- expect(eval(buildPublishStatusProbeJs())).toEqual({
147
- ok: false,
148
- failed: false,
149
- settled: false,
150
- url: '',
151
- });
152
- }
153
- finally {
154
- globalState.document = originalDocument;
155
- globalState.window = originalWindow;
156
- globalState.HTMLElement = originalHTMLElement;
157
- }
158
- });
159
- it('prefers explicit post-shared success over stale visible error text', () => {
160
- const globalState = globalThis;
161
- class MockHTMLElement {
162
- }
163
- const sharedDialog = new MockHTMLElement();
164
- sharedDialog.textContent = 'Post shared Your post has been shared.';
165
- sharedDialog.querySelector = () => null;
166
- sharedDialog.getBoundingClientRect = () => ({ width: 100, height: 100 });
167
- const staleErrorDialog = new MockHTMLElement();
168
- staleErrorDialog.textContent = 'Something went wrong. Please try again. Try again';
169
- staleErrorDialog.querySelector = () => null;
170
- staleErrorDialog.getBoundingClientRect = () => ({ width: 100, height: 100 });
171
- const originalDocument = globalState.document;
172
- const originalWindow = globalState.window;
173
- const originalHTMLElement = globalState.HTMLElement;
174
- globalState.HTMLElement = MockHTMLElement;
175
- globalState.document = {
176
- querySelectorAll: (selector) => selector === '[role="dialog"]' ? [sharedDialog, staleErrorDialog] : [],
177
- };
178
- globalState.window = {
179
- location: { href: 'https://www.instagram.com/' },
180
- getComputedStyle: () => ({ display: 'block', visibility: 'visible' }),
181
- };
182
- try {
183
- expect(eval(buildPublishStatusProbeJs())).toEqual({
184
- ok: true,
185
- failed: false,
186
- settled: false,
187
- url: '',
188
- });
189
- }
190
- finally {
191
- globalState.document = originalDocument;
192
- globalState.window = originalWindow;
193
- globalState.HTMLElement = originalHTMLElement;
194
- }
195
- });
196
- });
197
- describe('instagram click action detection', () => {
198
- it('matches aria-label-only Next buttons in the media dialog', () => {
199
- const globalState = globalThis;
200
- class MockHTMLElement {
201
- textContent = '';
202
- ariaLabel = '';
203
- clicked = false;
204
- querySelectorAll = (_selector) => [];
205
- querySelector = (_selector) => null;
206
- getAttribute(name) {
207
- if (name === 'aria-label')
208
- return this.ariaLabel || null;
209
- return null;
210
- }
211
- getBoundingClientRect() {
212
- return { width: 100, height: 40 };
213
- }
214
- click() {
215
- this.clicked = true;
216
- }
217
- }
218
- const nextButton = new MockHTMLElement();
219
- nextButton.ariaLabel = 'Next';
220
- const dialog = new MockHTMLElement();
221
- dialog.textContent = 'Crop Back Select crop Open media gallery';
222
- dialog.querySelector = (selector) => selector === 'input[type="file"]' ? {} : null;
223
- dialog.querySelectorAll = (selector) => selector === 'button, div[role="button"]' ? [nextButton] : [];
224
- const body = new MockHTMLElement();
225
- const originalDocument = globalState.document;
226
- const originalWindow = globalState.window;
227
- const originalHTMLElement = globalState.HTMLElement;
228
- globalState.HTMLElement = MockHTMLElement;
229
- globalState.document = {
230
- body,
231
- querySelectorAll: (selector) => selector === '[role="dialog"]' ? [dialog] : [],
232
- };
233
- globalState.window = {
234
- getComputedStyle: () => ({ display: 'block', visibility: 'visible' }),
235
- };
236
- try {
237
- expect(eval(buildClickActionJs(['Next', '下一步'], 'media'))).toEqual({
238
- ok: true,
239
- label: 'Next',
240
- });
241
- expect(nextButton.clicked).toBe(true);
242
- }
243
- finally {
244
- globalState.document = originalDocument;
245
- globalState.window = originalWindow;
246
- globalState.HTMLElement = originalHTMLElement;
247
- }
248
- });
249
- it('does not click a body-level Next button when media scope has no matching dialog controls', () => {
250
- const globalState = globalThis;
251
- class MockHTMLElement {
252
- textContent = '';
253
- ariaLabel = '';
254
- clicked = false;
255
- children = [];
256
- querySelectorAll = (_selector) => this.children;
257
- querySelector = (_selector) => null;
258
- getAttribute(name) {
259
- if (name === 'aria-label')
260
- return this.ariaLabel || null;
261
- return null;
262
- }
263
- getBoundingClientRect() {
264
- return { width: 100, height: 40 };
265
- }
266
- click() {
267
- this.clicked = true;
268
- }
269
- }
270
- const bodyNext = new MockHTMLElement();
271
- bodyNext.ariaLabel = 'Next';
272
- const errorDialog = new MockHTMLElement();
273
- errorDialog.textContent = 'Something went wrong Try again';
274
- errorDialog.children = [];
275
- const body = new MockHTMLElement();
276
- body.children = [bodyNext];
277
- const originalDocument = globalState.document;
278
- const originalWindow = globalState.window;
279
- const originalHTMLElement = globalState.HTMLElement;
280
- globalState.HTMLElement = MockHTMLElement;
281
- globalState.document = {
282
- body,
283
- querySelectorAll: (selector) => selector === '[role="dialog"]' ? [errorDialog] : [],
284
- };
285
- globalState.window = {
286
- getComputedStyle: () => ({ display: 'block', visibility: 'visible' }),
287
- };
288
- try {
289
- expect(eval(buildClickActionJs(['Next', '下一步'], 'media'))).toEqual({ ok: false });
290
- expect(bodyNext.clicked).toBe(false);
291
- }
292
- finally {
293
- globalState.document = originalDocument;
294
- globalState.window = originalWindow;
295
- globalState.HTMLElement = originalHTMLElement;
296
- }
297
- });
298
- });
299
- describe('instagram upload stage detection', () => {
300
- it('does not treat a body-level Next button as upload preview when the visible dialog is an error', () => {
301
- const globalState = globalThis;
302
- class MockHTMLElement {
303
- textContent = '';
304
- ariaLabel = '';
305
- children = [];
306
- querySelectorAll = (_selector) => this.children;
307
- querySelector = (_selector) => null;
308
- getAttribute(name) {
309
- if (name === 'aria-label')
310
- return this.ariaLabel || null;
311
- return null;
312
- }
313
- getBoundingClientRect() {
314
- return { width: 100, height: 40 };
315
- }
316
- }
317
- const bodyNext = new MockHTMLElement();
318
- bodyNext.ariaLabel = 'Next';
319
- const errorDialog = new MockHTMLElement();
320
- errorDialog.textContent = 'Something went wrong. Please try again. Try again';
321
- const body = new MockHTMLElement();
322
- body.children = [bodyNext];
323
- const originalDocument = globalState.document;
324
- const originalWindow = globalState.window;
325
- const originalHTMLElement = globalState.HTMLElement;
326
- globalState.HTMLElement = MockHTMLElement;
327
- globalState.document = {
328
- body,
329
- querySelectorAll: (selector) => {
330
- if (selector === '[role="dialog"]')
331
- return [errorDialog];
332
- return [];
333
- },
334
- };
335
- globalState.window = {
336
- getComputedStyle: () => ({ display: 'block', visibility: 'visible' }),
337
- };
338
- try {
339
- expect(eval(buildInspectUploadStageJs())).toEqual({
340
- state: 'failed',
341
- detail: 'Something went wrong. Please try again. Try again',
342
- });
343
- }
344
- finally {
345
- globalState.document = originalDocument;
346
- globalState.window = originalWindow;
347
- globalState.HTMLElement = originalHTMLElement;
348
- }
349
- });
350
- });
351
- describe('instagram post registration', () => {
352
- beforeEach(() => {
353
- vi.spyOn(privatePublish, 'resolveInstagramPrivatePublishConfig').mockResolvedValue({
354
- apiContext: {
355
- asbdId: '',
356
- csrfToken: 'csrf-token',
357
- igAppId: '936619743392459',
358
- igWwwClaim: '',
359
- instagramAjax: '1036523242',
360
- webSessionId: '',
361
- },
362
- jazoest: '22047',
363
- });
364
- vi.spyOn(privatePublish, 'publishImagesViaPrivateApi').mockRejectedValue(new CommandExecutionError('Instagram private publish configure failed: 400 {"message":"fallback to ui"}'));
365
- });
366
- afterEach(() => {
367
- vi.restoreAllMocks();
368
- });
369
- it('registers the post command with a required-value media arg', () => {
370
- const cmd = getRegistry().get('instagram/post');
371
- expect(cmd).toBeDefined();
372
- expect(cmd?.browser).toBe(true);
373
- expect(cmd?.args.some((arg) => arg.name === 'timeout' && arg.default === 300)).toBe(true);
374
- expect(cmd?.args.some((arg) => arg.name === 'media' && !arg.required && arg.valueRequired)).toBe(true);
375
- expect(cmd?.args.some((arg) => arg.name === 'content' && !arg.required && arg.positional)).toBe(true);
376
- });
377
- it('prefers the private route by default and returns without touching UI upload steps when private publish succeeds', async () => {
378
- const imagePath = createTempImage('private-default.jpg');
379
- const privateSpy = vi.spyOn(privatePublish, 'publishImagesViaPrivateApi').mockResolvedValueOnce({
380
- code: 'PRIVATEDEFAULT123',
381
- uploadIds: ['111'],
382
- });
383
- const evaluate = vi.fn(async (js) => {
384
- if (js.includes('sharing') && js.includes('create new post'))
385
- return { ok: false };
386
- if (js.includes('window.location?.pathname'))
387
- return { ok: true };
388
- if (js.includes('const data = Array.isArray(window[') && js.includes('__bycli_ig_protocol_capture'))
389
- return { data: [], errors: [] };
390
- return { ok: true };
391
- });
392
- const page = createPageMock([], {
393
- evaluate,
394
- getCookies: vi.fn().mockResolvedValue([{ name: 'csrftoken', value: 'csrf-token', domain: 'instagram.com' }]),
395
- });
396
- const cmd = getRegistry().get('instagram/post');
397
- const result = await cmd.func(page, { media: imagePath, content: 'private default' });
398
- expect(privateSpy).toHaveBeenCalledTimes(1);
399
- expect(page.setFileInput).not.toHaveBeenCalled();
400
- expect(result).toEqual([
401
- {
402
- status: '✅ Posted',
403
- detail: 'Single image post shared successfully',
404
- url: 'https://www.instagram.com/p/PRIVATEDEFAULT123/',
405
- },
406
- ]);
407
- privateSpy.mockRestore();
408
- });
409
- it('falls back to the UI route when the default private route fails safely before publishing', async () => {
410
- const imagePath = createTempImage('private-fallback-ui.jpg');
411
- const privateSpy = vi.spyOn(privatePublish, 'publishImagesViaPrivateApi').mockRejectedValueOnce(new CommandExecutionError('Instagram private publish configure_sidecar failed: 400 {"message":"Uploaded image is invalid"}'));
412
- const evaluate = vi.fn(async (js) => {
413
- if (js.includes('sharing') && js.includes('create new post'))
414
- return { ok: false };
415
- if (js.includes('window.location?.pathname'))
416
- return { ok: true };
417
- if (js.includes('data-bycli-ig-upload-index'))
418
- return { ok: true, selectors: ['[data-bycli-ig-upload-index="0"]'] };
419
- if (js.includes("dispatchEvent(new Event('input'"))
420
- return { ok: true };
421
- if (js.includes('const hasPreviewUi ='))
422
- return { ok: true, state: 'preview' };
423
- if (js.includes("scope === 'media'"))
424
- return { ok: true, label: 'Next' };
425
- if (js.includes("scope === 'caption'"))
426
- return { ok: true, label: 'Share' };
427
- if (js.includes('post shared') && js.includes('your post has been shared'))
428
- return { ok: true, url: 'https://www.instagram.com/p/PRIVATEFALLBACK123/' };
429
- return { ok: true };
430
- });
431
- const page = createPageMock([], {
432
- evaluate,
433
- getCookies: vi.fn().mockResolvedValue([{ name: 'csrftoken', value: 'csrf-token', domain: 'instagram.com' }]),
434
- });
435
- const cmd = getRegistry().get('instagram/post');
436
- const result = await cmd.func(page, { media: imagePath, content: 'private fallback' });
437
- expect(privateSpy).toHaveBeenCalledTimes(1);
438
- expect(page.setFileInput).toHaveBeenCalledWith([imagePath], '[data-bycli-ig-upload-index="0"]');
439
- expect(result).toEqual([
440
- {
441
- status: '✅ Posted',
442
- detail: 'Single image post shared successfully',
443
- url: 'https://www.instagram.com/p/PRIVATEFALLBACK123/',
444
- },
445
- ]);
446
- privateSpy.mockRestore();
447
- });
448
- it('falls back to the UI route for mixed-media posts when the private route fails safely before publishing', async () => {
449
- const imagePath = createTempImage('mixed-fallback-image.jpg');
450
- const videoPath = createTempVideo('mixed-fallback-video.mp4');
451
- const privateSpy = vi.spyOn(privatePublish, 'publishMediaViaPrivateApi').mockRejectedValueOnce(new CommandExecutionError('Instagram private publish configure_sidecar failed: 400 {"message":"fallback to ui"}'));
452
- const evaluate = vi.fn(async (js) => {
453
- if (js.includes('sharing') && js.includes('create new post'))
454
- return { ok: false };
455
- if (js.includes('window.location?.pathname'))
456
- return { ok: true };
457
- if (js.includes('data-bycli-ig-upload-index'))
458
- return { ok: true, selectors: ['[data-bycli-ig-upload-index="0"]'] };
459
- if (js.includes("dispatchEvent(new Event('input'"))
460
- return { ok: true };
461
- if (js.includes('const hasPreviewUi ='))
462
- return { ok: true, state: 'preview' };
463
- if (js.includes("scope === 'media'"))
464
- return { ok: true, label: 'Next' };
465
- if (js.includes("scope === 'caption'"))
466
- return { ok: true, label: 'Share' };
467
- if (js.includes('post shared') && js.includes('your post has been shared'))
468
- return { ok: true, url: 'https://www.instagram.com/p/MIXEDFALLBACK123/' };
469
- return { ok: true };
470
- });
471
- const page = createPageMock([], {
472
- evaluate,
473
- getCookies: vi.fn().mockResolvedValue([{ name: 'csrftoken', value: 'csrf-token', domain: 'instagram.com' }]),
474
- });
475
- const cmd = getRegistry().get('instagram/post');
476
- const result = await cmd.func(page, {
477
- media: `${imagePath},${videoPath}`,
478
- content: 'mixed ui fallback',
479
- });
480
- expect(privateSpy).toHaveBeenCalledTimes(1);
481
- expect(page.setFileInput).toHaveBeenCalledWith([imagePath, videoPath], '[data-bycli-ig-upload-index="0"]');
482
- expect(result).toEqual([
483
- {
484
- status: '✅ Posted',
485
- detail: '2-item mixed-media carousel post shared successfully',
486
- url: 'https://www.instagram.com/p/MIXEDFALLBACK123/',
487
- },
488
- ]);
489
- privateSpy.mockRestore();
490
- });
491
- it('prefers the private route by default for mixed-media posts and preserves input order', async () => {
492
- const imagePath = createTempImage('mixed-default.jpg');
493
- const videoPath = createTempVideo('mixed-default.mp4');
494
- const privateSpy = vi.spyOn(privatePublish, 'publishMediaViaPrivateApi').mockResolvedValueOnce({
495
- code: 'MIXEDPRIVATE123',
496
- uploadIds: ['111', '222'],
497
- });
498
- const page = createPageMock([], {
499
- evaluate: vi.fn(async () => ({ ok: true })),
500
- getCookies: vi.fn().mockResolvedValue([{ name: 'csrftoken', value: 'csrf-token', domain: 'instagram.com' }]),
501
- });
502
- const cmd = getRegistry().get('instagram/post');
503
- const result = await cmd.func(page, {
504
- media: `${imagePath},${videoPath}`,
505
- content: 'mixed private default',
506
- });
507
- expect(privateSpy).toHaveBeenCalledWith(expect.objectContaining({
508
- mediaItems: [
509
- { type: 'image', filePath: imagePath },
510
- { type: 'video', filePath: videoPath },
511
- ],
512
- caption: 'mixed private default',
513
- }));
514
- expect(page.setFileInput).not.toHaveBeenCalled();
515
- expect(result).toEqual([
516
- {
517
- status: '✅ Posted',
518
- detail: '2-item mixed-media carousel post shared successfully',
519
- url: 'https://www.instagram.com/p/MIXEDPRIVATE123/',
520
- },
521
- ]);
522
- privateSpy.mockRestore();
523
- });
524
- it('rejects missing --media before browser work', async () => {
525
- const page = createPageMock([]);
526
- const cmd = getRegistry().get('instagram/post');
527
- await expect(cmd.func(page, {
528
- content: 'missing media',
529
- })).rejects.toThrow('Argument "media" is required.');
530
- });
531
- it('rejects empty or invalid --media inputs', async () => {
532
- const imagePath = createTempImage('invalid-media-image.jpg');
533
- const page = createPageMock([]);
534
- const cmd = getRegistry().get('instagram/post');
535
- await expect(cmd.func(page, {
536
- media: '',
537
- })).rejects.toThrow('Argument "media" is required.');
538
- await expect(cmd.func(page, {
539
- media: `${imagePath},/tmp/does-not-exist.mp4`,
540
- })).rejects.toThrow('Media file not found');
541
- });
542
- it('uploads a single image, fills caption, and shares the post', async () => {
543
- const imagePath = createTempImage();
544
- const page = createPageMock(withInitialDialogDismiss([
545
- { ok: true },
546
- { ok: true, selectors: ['[data-bycli-ig-upload-index="0"]'] },
547
- { ok: true },
548
- { ok: true },
549
- { ok: false },
550
- { ok: true, label: 'Next' },
551
- { ok: true },
552
- { ok: true },
553
- { ok: true },
554
- { ok: true, label: 'Share' },
555
- { ok: true, url: 'https://www.instagram.com/p/ABC123xyz/' },
556
- ]));
557
- const cmd = getRegistry().get('instagram/post');
558
- const result = await cmd.func(page, {
559
- media: imagePath,
560
- content: 'hello from bycli',
561
- });
562
- expect(page.goto).toHaveBeenCalledWith('https://www.instagram.com/');
563
- expect(page.setFileInput).toHaveBeenCalledWith([imagePath], '[data-bycli-ig-upload-index="0"]');
564
- expect(page.evaluate.mock.calls.some((args) => String(args[0]).includes("dispatchEvent(new Event('change'"))).toBe(true);
565
- expect(result).toEqual([
566
- {
567
- status: '✅ Posted',
568
- detail: 'Single image post shared successfully',
569
- url: 'https://www.instagram.com/p/ABC123xyz/',
570
- },
571
- ]);
572
- });
573
- it('uploads multiple images as a carousel and shares the post', async () => {
574
- const firstImagePath = createTempImage('carousel-1.jpg');
575
- const secondImagePath = createTempImage('carousel-2.jpg');
576
- const page = createPageMock(withInitialDialogDismiss([
577
- { ok: true },
578
- { ok: true, selectors: ['[data-bycli-ig-upload-index="0"]'] },
579
- { ok: true },
580
- { ok: true },
581
- { ok: false },
582
- { ok: true, label: 'Next' },
583
- { ok: true },
584
- { ok: true },
585
- { ok: true },
586
- { ok: true, label: 'Share' },
587
- { ok: true, url: 'https://www.instagram.com/p/CAROUSEL123/' },
588
- ]));
589
- const cmd = getRegistry().get('instagram/post');
590
- const result = await cmd.func(page, {
591
- media: `${firstImagePath},${secondImagePath}`,
592
- content: 'hello carousel',
593
- });
594
- expect(page.setFileInput).toHaveBeenCalledWith([firstImagePath, secondImagePath], '[data-bycli-ig-upload-index="0"]');
595
- expect(result).toEqual([
596
- {
597
- status: '✅ Posted',
598
- detail: '2-image carousel post shared successfully',
599
- url: 'https://www.instagram.com/p/CAROUSEL123/',
600
- },
601
- ]);
602
- });
603
- it('installs and dumps protocol capture when BYCLI_INSTAGRAM_CAPTURE is enabled', async () => {
604
- process.env.BYCLI_INSTAGRAM_CAPTURE = '1';
605
- const imagePath = createTempImage('capture-enabled.jpg');
606
- const evaluate = vi.fn(async (js) => {
607
- if (js.includes('__bycli_ig_protocol_capture') && js.includes('PATCH_GUARD'))
608
- return { ok: true };
609
- if (js.includes('const data = Array.isArray(window[') && js.includes('__bycli_ig_protocol_capture')) {
610
- return { data: [], errors: [] };
611
- }
612
- if (js.includes('sharing') && js.includes('create new post'))
613
- return { ok: false };
614
- if (js.includes('window.location?.pathname'))
615
- return { ok: true };
616
- if (js.includes('data-bycli-ig-upload-index'))
617
- return { ok: true, selectors: ['[data-bycli-ig-upload-index="0"]'] };
618
- if (js.includes("dispatchEvent(new Event('input'"))
619
- return { ok: true };
620
- if (js.includes('const hasPreviewUi ='))
621
- return { ok: true, state: 'preview' };
622
- if (js.includes("scope === 'media'"))
623
- return { ok: true, label: 'Next' };
624
- if (js.includes("scope === 'caption'"))
625
- return { ok: true, label: 'Share' };
626
- if (js.includes('post shared') && js.includes('your post has been shared'))
627
- return { ok: true, url: 'https://www.instagram.com/p/CAPTURE123/' };
628
- return { ok: true };
629
- });
630
- const page = createPageMock([], { evaluate });
631
- const cmd = getRegistry().get('instagram/post');
632
- const result = await cmd.func(page, {
633
- media: imagePath,
634
- content: 'capture enabled',
635
- });
636
- const evaluateCalls = evaluate.mock.calls.map((args) => String(args[0]));
637
- expect(evaluateCalls.some((js) => js.includes('__bycli_ig_protocol_capture') && js.includes('PATCH_GUARD'))).toBe(true);
638
- expect(evaluateCalls.some((js) => js.includes('const data = Array.isArray(window[') && js.includes('__bycli_ig_protocol_capture'))).toBe(true);
639
- expect(result).toEqual([
640
- {
641
- status: '✅ Posted',
642
- detail: 'Single image post shared successfully',
643
- url: 'https://www.instagram.com/p/CAPTURE123/',
644
- },
645
- ]);
646
- delete process.env.BYCLI_INSTAGRAM_CAPTURE;
647
- });
648
- it('retries media Next when preview is visible before the button becomes clickable', async () => {
649
- const firstImagePath = createTempImage('carousel-delay-1.jpg');
650
- const secondImagePath = createTempImage('carousel-delay-2.jpg');
651
- let nextAttempts = 0;
652
- const evaluate = vi.fn(async (js) => {
653
- if (js.includes('sharing') && js.includes('create new post'))
654
- return { ok: false };
655
- if (js.includes('window.location?.pathname'))
656
- return { ok: true };
657
- if (js.includes('data-bycli-ig-upload-index'))
658
- return { ok: true, selectors: ['[data-bycli-ig-upload-index="0"]'] };
659
- if (js.includes("dispatchEvent(new Event('input'"))
660
- return { ok: true };
661
- if (js.includes('const hasVisibleButtonInDialogs'))
662
- return { state: 'preview', detail: 'Crop Back Next Select crop' };
663
- if (js.includes("dialogText.includes('write a caption')") || js.includes("const editable = document.querySelector('textarea, [contenteditable=\"true\"]');")) {
664
- return { ok: nextAttempts >= 2 };
665
- }
666
- if (js.includes("!labels.includes(text) && !labels.includes(aria)")) {
667
- nextAttempts += 1;
668
- if (nextAttempts === 1)
669
- return { ok: false };
670
- return { ok: true, label: 'Next' };
671
- }
672
- if (js.includes('ClipboardEvent') && js.includes('textarea'))
673
- return { ok: true, mode: 'textarea' };
674
- if (js.includes('readLexicalText'))
675
- return { ok: true };
676
- if (js.includes('post shared') && js.includes('your post has been shared'))
677
- return { ok: true, url: 'https://www.instagram.com/p/CAROUSELRETRY123/' };
678
- return { ok: true };
679
- });
680
- const page = createPageMock([], { evaluate });
681
- const cmd = getRegistry().get('instagram/post');
682
- const result = await cmd.func(page, {
683
- media: `${firstImagePath},${secondImagePath}`,
684
- content: 'hello delayed carousel',
685
- });
686
- expect(result).toEqual([
687
- {
688
- status: '✅ Posted',
689
- detail: '2-image carousel post shared successfully',
690
- url: 'https://www.instagram.com/p/CAROUSELRETRY123/',
691
- },
692
- ]);
693
- });
694
- it('retries the whole carousel flow when preview briefly appears and then degrades into an upload error before Next is usable', async () => {
695
- const firstImagePath = createTempImage('carousel-race-1.jpg');
696
- const secondImagePath = createTempImage('carousel-race-2.jpg');
697
- let composerRuns = 0;
698
- let uploadStageChecks = 0;
699
- let secondAttemptAdvanced = false;
700
- const evaluate = vi.fn(async (js) => {
701
- if (js.includes('sharing') && js.includes('create new post'))
702
- return { ok: false };
703
- if (js.includes('window.location?.pathname')) {
704
- composerRuns += 1;
705
- return { ok: true };
706
- }
707
- if (js.includes('data-bycli-ig-upload-index'))
708
- return { ok: true, selectors: ['[data-bycli-ig-upload-index="0"]'] };
709
- if (js.includes("dispatchEvent(new Event('input'"))
710
- return { ok: true };
711
- if (js.includes('const hasVisibleButtonInDialogs')) {
712
- uploadStageChecks += 1;
713
- if (composerRuns === 1 && uploadStageChecks === 1) {
714
- return { state: 'preview', detail: 'Crop Back Next Select crop' };
715
- }
716
- if (composerRuns === 1) {
717
- return { state: 'failed', detail: 'Something went wrong. Please try again.' };
718
- }
719
- return { state: 'preview', detail: 'Crop Back Next Select crop' };
720
- }
721
- if (js.includes("dialogText.includes('write a caption')") || js.includes("const editable = document.querySelector('textarea, [contenteditable=\"true\"]');")) {
722
- return { ok: composerRuns >= 2 && secondAttemptAdvanced };
723
- }
724
- if (js.includes("!labels.includes(text) && !labels.includes(aria)")) {
725
- if (composerRuns === 1)
726
- return { ok: false };
727
- secondAttemptAdvanced = true;
728
- return { ok: true, label: 'Next' };
729
- }
730
- if (js.includes('button[aria-label="Close"]'))
731
- return { ok: true };
732
- if (js.includes('ClipboardEvent') && js.includes('textarea'))
733
- return { ok: true, mode: 'textarea' };
734
- if (js.includes('readLexicalText'))
735
- return { ok: true };
736
- if (js.includes('post shared') && js.includes('your post has been shared'))
737
- return { ok: true, url: 'https://www.instagram.com/p/CAROUSELFRESH123/' };
738
- return { ok: true };
739
- });
740
- const page = createPageMock([], { evaluate });
741
- const cmd = getRegistry().get('instagram/post');
742
- const result = await cmd.func(page, {
743
- media: `${firstImagePath},${secondImagePath}`,
744
- content: 'hello recovered carousel',
745
- });
746
- expect(result).toEqual([
747
- {
748
- status: '✅ Posted',
749
- detail: '2-image carousel post shared successfully',
750
- url: 'https://www.instagram.com/p/CAROUSELFRESH123/',
751
- },
752
- ]);
753
- });
754
- it('uploads a single image and shares it without a caption when content is omitted', async () => {
755
- const imagePath = createTempImage('no-caption.jpg');
756
- const evaluate = vi.fn(async (js) => {
757
- if (js.includes('sharing') && js.includes('create new post'))
758
- return { ok: false };
759
- if (js.includes('window.location?.pathname'))
760
- return { ok: true };
761
- if (js.includes('data-bycli-ig-upload-index'))
762
- return { ok: true, selectors: ['[data-bycli-ig-upload-index="0"]'] };
763
- if (js.includes("dispatchEvent(new Event('input'"))
764
- return { ok: true };
765
- if (js.includes('const hasPreviewUi ='))
766
- return { ok: true, state: 'preview' };
767
- if (js.includes("scope === 'media'"))
768
- return { ok: true, label: 'Next' };
769
- if (js.includes("scope === 'caption'"))
770
- return { ok: true, label: 'Share' };
771
- if (js.includes('const editable = document.querySelector(\'textarea, [contenteditable="true"]\');'))
772
- return { ok: true };
773
- if (js.includes('post shared') && js.includes('your post has been shared'))
774
- return { ok: true, url: 'https://www.instagram.com/p/NOCAPTION123/' };
775
- return { ok: false };
776
- });
777
- const page = createPageMock([], { evaluate });
778
- const cmd = getRegistry().get('instagram/post');
779
- const result = await cmd.func(page, {
780
- media: imagePath,
781
- });
782
- const evaluateCalls = page.evaluate.mock.calls.map((args) => String(args[0]));
783
- expect(evaluateCalls.some((js) => js.includes('Write a caption'))).toBe(false);
784
- expect(result).toEqual([
785
- {
786
- status: '✅ Posted',
787
- detail: 'Single image post shared successfully',
788
- url: 'https://www.instagram.com/p/NOCAPTION123/',
789
- },
790
- ]);
791
- });
792
- it('falls back to browser-side file injection when the extension does not support set-file-input', async () => {
793
- const imagePath = createTempImage('legacy-extension.jpg');
794
- const evaluate = vi.fn(async (js) => {
795
- if (js.includes('sharing') && js.includes('create new post'))
796
- return { ok: false };
797
- if (js.includes('window.location?.pathname'))
798
- return { ok: true };
799
- if (js.includes('data-bycli-ig-upload-index'))
800
- return { ok: true, selectors: ['[data-bycli-ig-upload-index="0"]'] };
801
- if (js.includes('__bycliInstagramUpload_') && js.includes('] = [];'))
802
- return { ok: true };
803
- if (js.includes('parts.push(chunk)'))
804
- return { ok: true, count: 1 };
805
- if (js.includes('File input not found for fallback injection'))
806
- return { ok: true, count: 1 };
807
- if (js.includes('const hasPreviewUi ='))
808
- return { ok: true, state: 'preview' };
809
- if (js.includes("scope === 'caption'"))
810
- return { ok: true, label: 'Share' };
811
- if (js.includes("scope === 'media'"))
812
- return { ok: true, label: 'Next' };
813
- if (js.includes('labels.includes(text)'))
814
- return { ok: false };
815
- if (js.includes('ClipboardEvent') && js.includes('textarea'))
816
- return { ok: true, mode: 'textarea' };
817
- if (js.includes('readLexicalText'))
818
- return { ok: true };
819
- if (js.includes('couldn') && js.includes('your post has been shared'))
820
- return { ok: true, url: 'https://www.instagram.com/p/LEGACY123/' };
821
- return { ok: true };
822
- });
823
- const page = createPageMock([], {
824
- evaluate,
825
- setFileInput: vi.fn().mockRejectedValue(new Error('Unknown action: set-file-input')),
826
- });
827
- const cmd = getRegistry().get('instagram/post');
828
- const result = await cmd.func(page, {
829
- media: imagePath,
830
- content: 'legacy bridge fallback',
831
- });
832
- expect(page.setFileInput).toHaveBeenCalledWith([imagePath], '[data-bycli-ig-upload-index="0"]');
833
- expect(result).toEqual([
834
- {
835
- status: '✅ Posted',
836
- detail: 'Single image post shared successfully',
837
- url: 'https://www.instagram.com/p/LEGACY123/',
838
- },
839
- ]);
840
- });
841
- it('chunks large legacy fallback uploads instead of embedding the whole image in one evaluate payload', async () => {
842
- const imagePath = createTempImage('legacy-large.jpg', Buffer.alloc(900 * 1024, 1));
843
- const evaluate = vi.fn(async (js) => {
844
- if (js.includes('sharing') && js.includes('create new post'))
845
- return { ok: false };
846
- if (js.includes('window.location?.pathname'))
847
- return { ok: true };
848
- if (js.includes('data-bycli-ig-upload-index'))
849
- return { ok: true, selectors: ['[data-bycli-ig-upload-index="0"]'] };
850
- if (js.includes('window[') && js.includes('] = [];'))
851
- return { ok: true };
852
- if (js.includes('parts.push(chunk)'))
853
- return { ok: true, count: 1 };
854
- if (js.includes('File input not found for fallback injection'))
855
- return { ok: true, count: 1 };
856
- if (js.includes('const hasPreviewUi ='))
857
- return { ok: true, state: 'preview' };
858
- if (js.includes("scope === 'caption'"))
859
- return { ok: true, label: 'Share' };
860
- if (js.includes("scope === 'media'"))
861
- return { ok: true, label: 'Next' };
862
- if (js.includes('labels.includes(text)'))
863
- return { ok: false };
864
- if (js.includes('ClipboardEvent') && js.includes('textarea'))
865
- return { ok: true, mode: 'textarea' };
866
- if (js.includes('readLexicalText'))
867
- return { ok: true };
868
- if (js.includes('couldn') && js.includes('your post has been shared'))
869
- return { ok: true, url: 'https://www.instagram.com/p/LARGELEGACY123/' };
870
- return { ok: true };
871
- });
872
- const page = createPageMock([], {
873
- evaluate,
874
- setFileInput: vi.fn().mockRejectedValue(new Error('Unknown action: set-file-input')),
875
- });
876
- const cmd = getRegistry().get('instagram/post');
877
- const result = await cmd.func(page, {
878
- media: imagePath,
879
- content: 'legacy large bridge fallback',
880
- });
881
- const chunkCalls = evaluate.mock.calls.filter((args) => String(args[0]).includes('parts.push(chunk)'));
882
- expect(chunkCalls.length).toBeGreaterThan(1);
883
- expect(result).toEqual([
884
- {
885
- status: '✅ Posted',
886
- detail: 'Single image post shared successfully',
887
- url: 'https://www.instagram.com/p/LARGELEGACY123/',
888
- },
889
- ]);
890
- });
891
- it('fails clearly when Browser Bridge file upload support is unavailable', async () => {
892
- const imagePath = createTempImage('missing-bridge.jpg');
893
- const page = createPageMock([], { setFileInput: undefined });
894
- const cmd = getRegistry().get('instagram/post');
895
- await expect(cmd.func(page, {
896
- media: imagePath,
897
- content: 'hello from bycli',
898
- })).rejects.toThrow(CommandExecutionError);
899
- });
900
- it('maps login-gated composer access to AuthRequiredError', async () => {
901
- const imagePath = createTempImage('auth.jpg');
902
- const page = createPageMock(withInitialDialogDismiss([
903
- { ok: false, reason: 'auth' },
904
- ]));
905
- const cmd = getRegistry().get('instagram/post');
906
- await expect(cmd.func(page, {
907
- media: imagePath,
908
- content: 'login required',
909
- })).rejects.toThrow(AuthRequiredError);
910
- });
911
- it('captures a debug screenshot when the upload preview never appears', async () => {
912
- const imagePath = createTempImage('no-preview.jpg');
913
- const page = createPageMock(withInitialDialogDismiss([
914
- { ok: true },
915
- { ok: true, selectors: ['[data-bycli-ig-upload-index="0"]'] },
916
- { ok: true },
917
- { ok: false },
918
- { ok: false },
919
- { ok: false },
920
- { ok: false },
921
- { ok: false },
922
- { ok: false },
923
- { ok: false },
924
- { ok: false },
925
- { ok: false },
926
- { ok: false },
927
- { ok: false },
928
- { ok: false },
929
- { ok: false },
930
- ]));
931
- const cmd = getRegistry().get('instagram/post');
932
- await expect(cmd.func(page, {
933
- media: imagePath,
934
- content: 'preview missing',
935
- })).rejects.toThrow('Instagram image preview did not appear after upload');
936
- expect(page.screenshot).toHaveBeenCalledWith({ path: '/tmp/instagram_post_preview_debug.png' });
937
- });
938
- it('fails clearly when Instagram shows an upload-stage error dialog', async () => {
939
- const imagePath = createTempImage('upload-error.jpg');
940
- const page = createPageMock(withInitialDialogDismiss([
941
- { ok: true },
942
- { ok: true, selectors: ['[data-bycli-ig-upload-index="0"]'] },
943
- { ok: true },
944
- { ok: false, state: 'failed', detail: 'Something went wrong. Please try again.' },
945
- ]));
946
- const cmd = getRegistry().get('instagram/post');
947
- await expect(cmd.func(page, {
948
- media: imagePath,
949
- content: 'upload should fail clearly',
950
- })).rejects.toThrow('Instagram image upload failed');
951
- });
952
- it('treats crop/next preview UI as success even if stale error text is still visible', async () => {
953
- const imagePath = createTempImage('upload-preview-wins.jpg');
954
- const page = createPageMock(withInitialDialogDismiss([
955
- { ok: true },
956
- { ok: true, selectors: ['[data-bycli-ig-upload-index="0"]'] },
957
- { ok: true },
958
- {
959
- ok: false,
960
- state: 'preview',
961
- detail: 'Something went wrong. Please try again. Crop Back Next Select crop Select zoom Open media gallery',
962
- },
963
- { ok: false },
964
- { ok: true, label: 'Next' },
965
- { ok: true },
966
- { ok: true },
967
- { ok: true },
968
- { ok: true, label: 'Share' },
969
- { ok: true, url: 'https://www.instagram.com/p/PREVIEWWINS123/' },
970
- ]));
971
- const cmd = getRegistry().get('instagram/post');
972
- const result = await cmd.func(page, {
973
- media: imagePath,
974
- content: 'preview state wins over stale error text',
975
- });
976
- expect(result).toEqual([
977
- {
978
- status: '✅ Posted',
979
- detail: 'Single image post shared successfully',
980
- url: 'https://www.instagram.com/p/PREVIEWWINS123/',
981
- },
982
- ]);
983
- });
984
- it('retries the same upload selector once after an upload-stage error and can still succeed', async () => {
985
- const imagePath = createTempImage('upload-retry.jpg');
986
- const setFileInput = vi.fn().mockResolvedValue(undefined);
987
- let uploadProbeCount = 0;
988
- const evaluate = vi.fn(async (js) => {
989
- if (js.includes('sharing') && js.includes('create new post'))
990
- return { ok: false };
991
- if (js.includes('window.location?.pathname'))
992
- return { ok: true };
993
- if (js.includes('data-bycli-ig-upload-index'))
994
- return { ok: true, selectors: ['[data-bycli-ig-upload-index="0"]'] };
995
- if (js.includes("dispatchEvent(new Event('input'"))
996
- return { ok: true };
997
- if (js.includes('const failed =') && js.includes('const hasCaption =')) {
998
- uploadProbeCount += 1;
999
- return uploadProbeCount === 1
1000
- ? { ok: false, state: 'failed', detail: 'Something went wrong. Please try again.' }
1001
- : { ok: true, state: 'preview' };
1002
- }
1003
- if (js.includes('button[aria-label="Close"]'))
1004
- return { ok: true };
1005
- if (js.includes("scope === 'media'"))
1006
- return { ok: true, label: 'Next' };
1007
- if (js.includes('ClipboardEvent') && js.includes('textarea'))
1008
- return { ok: true, mode: 'textarea' };
1009
- if (js.includes('readLexicalText'))
1010
- return { ok: true };
1011
- if (js.includes("scope === 'caption'"))
1012
- return { ok: true, label: 'Share' };
1013
- if (js.includes('post shared') && js.includes('your post has been shared'))
1014
- return { ok: true, url: 'https://www.instagram.com/p/UPLOADRETRY123/' };
1015
- return { ok: true };
1016
- });
1017
- const page = createPageMock([], { setFileInput, evaluate });
1018
- const cmd = getRegistry().get('instagram/post');
1019
- const result = await cmd.func(page, {
1020
- media: imagePath,
1021
- content: 'upload retry succeeds',
1022
- });
1023
- expect(setFileInput).toHaveBeenCalledTimes(1);
1024
- expect(result).toEqual([
1025
- {
1026
- status: '✅ Posted',
1027
- detail: 'Single image post shared successfully',
1028
- url: 'https://www.instagram.com/p/UPLOADRETRY123/',
1029
- },
1030
- ]);
1031
- });
1032
- it('clicks upload Try again in-place before resetting the whole flow when Instagram shows an upload error dialog', async () => {
1033
- const imagePath = createTempImage('upload-inline-retry.jpg');
1034
- let uploadProbeCount = 0;
1035
- const evaluate = vi.fn(async (js) => {
1036
- if (js.includes('sharing') && js.includes('create new post'))
1037
- return { ok: false };
1038
- if (js.includes('window.location?.pathname'))
1039
- return { ok: true };
1040
- if (js.includes('data-bycli-ig-upload-index'))
1041
- return { ok: true, selectors: ['[data-bycli-ig-upload-index="0"]'] };
1042
- if (js.includes("dispatchEvent(new Event('input'"))
1043
- return { ok: true };
1044
- if (js.includes('const hasVisibleButtonInDialogs')) {
1045
- uploadProbeCount += 1;
1046
- return uploadProbeCount === 1
1047
- ? { state: 'failed', detail: 'Something went wrong. Please try again.' }
1048
- : { state: 'preview', detail: 'Crop Back Next Select crop' };
1049
- }
1050
- if (js.includes('something went wrong') && js.includes('label === \'try again\''))
1051
- return { ok: true };
1052
- if (js.includes("dialogText.includes('write a caption')") || js.includes("const editable = document.querySelector('textarea, [contenteditable=\"true\"]');")) {
1053
- return { ok: true };
1054
- }
1055
- if (js.includes("!labels.includes(text) && !labels.includes(aria)")) {
1056
- if (js.includes('"Share"'))
1057
- return { ok: true, label: 'Share' };
1058
- return { ok: true, label: 'Next' };
1059
- }
1060
- if (js.includes('ClipboardEvent') && js.includes('textarea'))
1061
- return { ok: true, mode: 'textarea' };
1062
- if (js.includes('readLexicalText'))
1063
- return { ok: true };
1064
- if (js.includes('post shared') && js.includes('your post has been shared'))
1065
- return { ok: true, url: 'https://www.instagram.com/p/UPLOADINLINERETRY123/' };
1066
- return { ok: true };
1067
- });
1068
- const page = createPageMock([], { evaluate });
1069
- const cmd = getRegistry().get('instagram/post');
1070
- const result = await cmd.func(page, {
1071
- media: imagePath,
1072
- content: 'upload inline retry succeeds',
1073
- });
1074
- expect(result).toEqual([
1075
- {
1076
- status: '✅ Posted',
1077
- detail: 'Single image post shared successfully',
1078
- url: 'https://www.instagram.com/p/UPLOADINLINERETRY123/',
1079
- },
1080
- ]);
1081
- });
1082
- it('retries max-size carousel upload failures beyond the expanded large-carousel budget before succeeding', async () => {
1083
- const paths = [
1084
- createTempImage('carousel-10-1.jpg'),
1085
- createTempImage('carousel-10-2.jpg'),
1086
- createTempImage('carousel-10-3.jpg'),
1087
- createTempImage('carousel-10-4.jpg'),
1088
- createTempImage('carousel-10-5.jpg'),
1089
- createTempImage('carousel-10-6.jpg'),
1090
- createTempImage('carousel-10-7.jpg'),
1091
- createTempImage('carousel-10-8.jpg'),
1092
- createTempImage('carousel-10-9.jpg'),
1093
- createTempImage('carousel-10-10.jpg'),
1094
- ];
1095
- const setFileInput = vi.fn().mockResolvedValue(undefined);
1096
- let uploadProbeCount = 0;
1097
- const evaluate = vi.fn(async (js) => {
1098
- if (js.includes('sharing') && js.includes('create new post'))
1099
- return { ok: false };
1100
- if (js.includes('window.location?.pathname'))
1101
- return { ok: true };
1102
- if (js.includes('data-bycli-ig-upload-index'))
1103
- return { ok: true, selectors: ['[data-bycli-ig-upload-index="0"]'] };
1104
- if (js.includes("dispatchEvent(new Event('input'"))
1105
- return { ok: true };
1106
- if (js.includes('const hasVisibleButtonInDialogs')) {
1107
- uploadProbeCount += 1;
1108
- if (uploadProbeCount <= 16) {
1109
- return { state: 'failed', detail: 'Something went wrong. Please try again.' };
1110
- }
1111
- return { state: 'preview', detail: 'Crop Back Next Select crop' };
1112
- }
1113
- if (js.includes('button[aria-label="Close"]'))
1114
- return { ok: true };
1115
- if (js.includes("dialogText.includes('write a caption')") || js.includes("const editable = document.querySelector('textarea, [contenteditable=\"true\"]');")) {
1116
- return { ok: true };
1117
- }
1118
- if (js.includes("!labels.includes(text) && !labels.includes(aria)")) {
1119
- if (js.includes('"Share"'))
1120
- return { ok: true, label: 'Share' };
1121
- return { ok: true, label: 'Next' };
1122
- }
1123
- if (js.includes('post shared') && js.includes('your post has been shared'))
1124
- return { ok: true, url: 'https://www.instagram.com/p/CAROUSEL10RETRY123/' };
1125
- return { ok: true };
1126
- });
1127
- const page = createPageMock([], { setFileInput, evaluate });
1128
- const cmd = getRegistry().get('instagram/post');
1129
- const result = await cmd.func(page, {
1130
- media: paths.join(','),
1131
- });
1132
- expect(setFileInput).toHaveBeenCalledTimes(5);
1133
- expect(result).toEqual([
1134
- {
1135
- status: '✅ Posted',
1136
- detail: '10-image carousel post shared successfully',
1137
- url: 'https://www.instagram.com/p/CAROUSEL10RETRY123/',
1138
- },
1139
- ]);
1140
- });
1141
- it('forces a fresh home reload before retrying after an upload-stage error', async () => {
1142
- const imagePath = createTempImage('upload-fresh-reload.jpg');
1143
- const gotoUrls = [];
1144
- const goto = vi.fn(async (url) => {
1145
- gotoUrls.push(String(url));
1146
- });
1147
- let uploadProbeCount = 0;
1148
- let advancedToCaption = false;
1149
- const evaluate = vi.fn(async (js) => {
1150
- if (js.includes('window.location?.pathname'))
1151
- return { ok: true };
1152
- if (js.includes('data-bycli-ig-upload-index'))
1153
- return { ok: true, selectors: ['[data-bycli-ig-upload-index="0"]'] };
1154
- if (js.includes("dispatchEvent(new Event('input'"))
1155
- return { ok: true };
1156
- if (js.includes("dialogText.includes('write a caption')") || js.includes("const editable = document.querySelector('textarea, [contenteditable=\"true\"]');")) {
1157
- return { ok: advancedToCaption };
1158
- }
1159
- if (js.includes('const hasPreviewUi =')) {
1160
- uploadProbeCount += 1;
1161
- if (uploadProbeCount === 1) {
1162
- return { ok: false, state: 'failed', detail: 'Something went wrong. Please try again.' };
1163
- }
1164
- return gotoUrls.some((url) => url.includes('__bycli_reset='))
1165
- ? { ok: true, state: 'preview' }
1166
- : { ok: false, state: 'failed', detail: 'Something went wrong. Please try again.' };
1167
- }
1168
- if (js.includes('button[aria-label="Close"]'))
1169
- return { ok: false };
1170
- if (js.includes("scope === 'media'")) {
1171
- advancedToCaption = true;
1172
- return { ok: true, label: 'Next' };
1173
- }
1174
- if (js.includes('ClipboardEvent') && js.includes('textarea'))
1175
- return { ok: true, mode: 'textarea' };
1176
- if (js.includes('readLexicalText'))
1177
- return { ok: true };
1178
- if (js.includes("scope === 'caption'"))
1179
- return { ok: true, label: 'Share' };
1180
- if (js.includes('post shared') && js.includes('your post has been shared'))
1181
- return { ok: true, url: 'https://www.instagram.com/p/FRESHRELOAD123/' };
1182
- return { ok: false };
1183
- });
1184
- const page = createPageMock([], {
1185
- goto,
1186
- evaluate,
1187
- setFileInput: vi.fn().mockResolvedValue(undefined),
1188
- });
1189
- const cmd = getRegistry().get('instagram/post');
1190
- const result = await cmd.func(page, {
1191
- media: imagePath,
1192
- content: 'fresh reload after upload failure',
1193
- });
1194
- expect(gotoUrls.some((url) => url.includes('__bycli_reset='))).toBe(true);
1195
- expect(result).toEqual([
1196
- {
1197
- status: '✅ Posted',
1198
- detail: 'Single image post shared successfully',
1199
- url: 'https://www.instagram.com/p/FRESHRELOAD123/',
1200
- },
1201
- ]);
1202
- });
1203
- it('retries the share action in-place when Instagram shows a visible try-again share failure dialog', async () => {
1204
- const imagePath = createTempImage('share-retry.jpg');
1205
- let shareStatusChecks = 0;
1206
- const evaluate = vi.fn(async (js) => {
1207
- if (js.includes('sharing') && js.includes('create new post'))
1208
- return { ok: false };
1209
- if (js.includes('window.location?.pathname'))
1210
- return { ok: true };
1211
- if (js.includes('data-bycli-ig-upload-index'))
1212
- return { ok: true, selectors: ['[data-bycli-ig-upload-index="0"]'] };
1213
- if (js.includes("dispatchEvent(new Event('input'"))
1214
- return { ok: true };
1215
- if (js.includes('const hasVisibleButtonInDialogs'))
1216
- return { state: 'preview', detail: 'Crop Back Next Select crop' };
1217
- if (js.includes("dialogText.includes('write a caption')") || js.includes("const editable = document.querySelector('textarea, [contenteditable=\"true\"]');")) {
1218
- return { ok: true };
1219
- }
1220
- if (js.includes("!labels.includes(text) && !labels.includes(aria)")) {
1221
- if (js.includes('"Share"'))
1222
- return { ok: true, label: 'Share' };
1223
- return { ok: true, label: 'Next' };
1224
- }
1225
- if (js.includes('ClipboardEvent') && js.includes('textarea'))
1226
- return { ok: true, mode: 'textarea' };
1227
- if (js.includes('readLexicalText'))
1228
- return { ok: true };
1229
- if (js.includes('post shared') && js.includes('your post has been shared')) {
1230
- shareStatusChecks += 1;
1231
- return shareStatusChecks === 1
1232
- ? { ok: false, failed: true, settled: false, url: '' }
1233
- : { ok: true, failed: false, settled: false, url: 'https://www.instagram.com/p/SHARERETRY123/' };
1234
- }
1235
- if (js.includes('post couldn') && js.includes('try again'))
1236
- return { ok: true };
1237
- return { ok: true };
1238
- });
1239
- const page = createPageMock([], { evaluate });
1240
- const cmd = getRegistry().get('instagram/post');
1241
- const result = await cmd.func(page, {
1242
- media: imagePath,
1243
- content: 'share retry succeeds',
1244
- });
1245
- expect(result).toEqual([
1246
- {
1247
- status: '✅ Posted',
1248
- detail: 'Single image post shared successfully',
1249
- url: 'https://www.instagram.com/p/SHARERETRY123/',
1250
- },
1251
- ]);
1252
- });
1253
- it('re-resolves the upload input when the tagged selector goes stale before setFileInput runs', async () => {
1254
- const imagePath = createTempImage('stale-selector.jpg');
1255
- const setFileInput = vi.fn()
1256
- .mockRejectedValueOnce(new Error('No element found matching selector: [data-bycli-ig-upload-index="0"]'))
1257
- .mockResolvedValueOnce(undefined);
1258
- const page = createPageMock(withInitialDialogDismiss([
1259
- { ok: true },
1260
- { ok: true, selectors: ['[data-bycli-ig-upload-index="0"]'] },
1261
- { ok: true, selectors: ['[data-bycli-ig-upload-index="0"]'] },
1262
- { ok: true },
1263
- { ok: true },
1264
- { ok: false },
1265
- { ok: true, label: 'Next' },
1266
- { ok: true },
1267
- { ok: true },
1268
- { ok: true },
1269
- { ok: true, label: 'Share' },
1270
- { ok: true, url: 'https://www.instagram.com/p/STALE123/' },
1271
- ]), { setFileInput });
1272
- const cmd = getRegistry().get('instagram/post');
1273
- const result = await cmd.func(page, {
1274
- media: imagePath,
1275
- content: 'stale selector recovery',
1276
- });
1277
- expect(setFileInput).toHaveBeenCalledTimes(2);
1278
- expect(result).toEqual([
1279
- {
1280
- status: '✅ Posted',
1281
- detail: 'Single image post shared successfully',
1282
- url: 'https://www.instagram.com/p/STALE123/',
1283
- },
1284
- ]);
1285
- });
1286
- it('re-resolves the upload input when CDP loses the matched file-input node before setFileInput runs', async () => {
1287
- const imagePath = createTempImage('stale-node-id.jpg');
1288
- const setFileInput = vi.fn()
1289
- .mockRejectedValueOnce(new Error('{"code":-32000,"message":"Could not find node with given id"}'))
1290
- .mockResolvedValueOnce(undefined);
1291
- const page = createPageMock(withInitialDialogDismiss([
1292
- { ok: true },
1293
- { ok: true, selectors: ['[data-bycli-ig-upload-index="0"]'] },
1294
- { ok: true, selectors: ['[data-bycli-ig-upload-index="0"]'] },
1295
- { ok: true },
1296
- { ok: true },
1297
- { ok: false },
1298
- { ok: true, label: 'Next' },
1299
- { ok: true },
1300
- { ok: true },
1301
- { ok: true },
1302
- { ok: true, label: 'Share' },
1303
- { ok: true, url: 'https://www.instagram.com/p/STALEID123/' },
1304
- ]), { setFileInput });
1305
- const cmd = getRegistry().get('instagram/post');
1306
- const result = await cmd.func(page, {
1307
- media: imagePath,
1308
- content: 'stale node id recovery',
1309
- });
1310
- expect(setFileInput).toHaveBeenCalledTimes(2);
1311
- expect(result).toEqual([
1312
- {
1313
- status: '✅ Posted',
1314
- detail: 'Single image post shared successfully',
1315
- url: 'https://www.instagram.com/p/STALEID123/',
1316
- },
1317
- ]);
1318
- });
1319
- it('retries opening the home composer instead of navigating to the broken /create/select route', async () => {
1320
- const imagePath = createTempImage('retry-composer.jpg');
1321
- const page = createPageMock(withInitialDialogDismiss([
1322
- { ok: true },
1323
- { ok: false },
1324
- { ok: true },
1325
- { ok: true, selectors: ['[data-bycli-ig-upload-index="0"]'] },
1326
- { ok: true },
1327
- { ok: true },
1328
- { ok: false },
1329
- { ok: true, label: 'Next' },
1330
- { ok: true },
1331
- { ok: true },
1332
- { ok: true },
1333
- { ok: true, label: 'Share' },
1334
- { ok: true, url: 'https://www.instagram.com/p/FALLBACK123/' },
1335
- ]));
1336
- const cmd = getRegistry().get('instagram/post');
1337
- const result = await cmd.func(page, {
1338
- media: imagePath,
1339
- content: 'retry composer',
1340
- });
1341
- const gotoCalls = page.goto.mock.calls.map((args) => String(args[0]));
1342
- expect(gotoCalls.every((url) => !url.includes('/create/select'))).toBe(true);
1343
- expect(gotoCalls.some((url) => url === 'https://www.instagram.com/')).toBe(true);
1344
- expect(result).toEqual([
1345
- {
1346
- status: '✅ Posted',
1347
- detail: 'Single image post shared successfully',
1348
- url: 'https://www.instagram.com/p/FALLBACK123/',
1349
- },
1350
- ]);
1351
- });
1352
- it('clicks Next twice when Instagram shows an intermediate preview step before the caption editor', async () => {
1353
- const imagePath = createTempImage('double-next.jpg');
1354
- let nextClicks = 0;
1355
- const evaluate = vi.fn(async (js) => {
1356
- if (js.includes('sharing') && js.includes('create new post'))
1357
- return { ok: false };
1358
- if (js.includes('window.location?.pathname'))
1359
- return { ok: true };
1360
- if (js.includes('data-bycli-ig-upload-index'))
1361
- return { ok: true, selectors: ['[data-bycli-ig-upload-index="0"]'] };
1362
- if (js.includes("dispatchEvent(new Event('input'"))
1363
- return { ok: true };
1364
- if (js.includes('const hasVisibleButtonInDialogs'))
1365
- return { state: 'preview', detail: 'Crop Back Next Select crop' };
1366
- if (js.includes("dialogText.includes('write a caption')") || js.includes("const editable = document.querySelector('textarea, [contenteditable=\"true\"]');")) {
1367
- return { ok: nextClicks >= 2 };
1368
- }
1369
- if (js.includes("!labels.includes(text) && !labels.includes(aria)")) {
1370
- if (js.includes('"Share"'))
1371
- return { ok: true, label: 'Share' };
1372
- nextClicks += 1;
1373
- return { ok: true, label: 'Next' };
1374
- }
1375
- if (js.includes('ClipboardEvent') && js.includes('textarea'))
1376
- return { ok: true, mode: 'textarea' };
1377
- if (js.includes('readLexicalText'))
1378
- return { ok: true };
1379
- if (js.includes('post shared') && js.includes('your post has been shared'))
1380
- return { ok: true, url: 'https://www.instagram.com/p/DOUBLE123/' };
1381
- return { ok: true };
1382
- });
1383
- const page = createPageMock([], { evaluate });
1384
- const cmd = getRegistry().get('instagram/post');
1385
- const result = await cmd.func(page, {
1386
- media: imagePath,
1387
- content: 'double next flow',
1388
- });
1389
- expect(result).toEqual([
1390
- {
1391
- status: '✅ Posted',
1392
- detail: 'Single image post shared successfully',
1393
- url: 'https://www.instagram.com/p/DOUBLE123/',
1394
- },
1395
- ]);
1396
- });
1397
- it('tries the next upload input when the first candidate never opens the preview', async () => {
1398
- const imagePath = createTempImage('second-input.jpg');
1399
- const setFileInput = vi.fn()
1400
- .mockResolvedValueOnce(undefined)
1401
- .mockResolvedValueOnce(undefined);
1402
- const page = createPageMock(withInitialDialogDismiss([
1403
- { ok: true },
1404
- { ok: true, selectors: ['[data-bycli-ig-upload-index="0"]', '[data-bycli-ig-upload-index="1"]'] },
1405
- { ok: true },
1406
- { ok: false },
1407
- { ok: false },
1408
- { ok: false },
1409
- { ok: false },
1410
- { ok: false },
1411
- { ok: false },
1412
- { ok: false },
1413
- { ok: false },
1414
- { ok: true },
1415
- { ok: true },
1416
- { ok: false },
1417
- { ok: true, label: 'Next' },
1418
- { ok: true },
1419
- { ok: true },
1420
- { ok: true },
1421
- { ok: true, label: 'Share' },
1422
- { ok: true, url: 'https://www.instagram.com/p/SECOND123/' },
1423
- ]), { setFileInput });
1424
- const cmd = getRegistry().get('instagram/post');
1425
- const result = await cmd.func(page, {
1426
- media: imagePath,
1427
- content: 'second input works',
1428
- });
1429
- expect(setFileInput).toHaveBeenNthCalledWith(1, [imagePath], '[data-bycli-ig-upload-index="0"]');
1430
- expect(setFileInput).toHaveBeenNthCalledWith(2, [imagePath], '[data-bycli-ig-upload-index="1"]');
1431
- expect(result).toEqual([
1432
- {
1433
- status: '✅ Posted',
1434
- detail: 'Single image post shared successfully',
1435
- url: 'https://www.instagram.com/p/SECOND123/',
1436
- },
1437
- ]);
1438
- });
1439
- it('fails fast when Instagram reports that the post could not be shared', async () => {
1440
- const imagePath = createTempImage('share-failed.jpg');
1441
- const page = createPageMock(withInitialDialogDismiss([
1442
- { ok: true },
1443
- { ok: true, selectors: ['[data-bycli-ig-upload-index="0"]'] },
1444
- { ok: true },
1445
- { ok: true },
1446
- { ok: false },
1447
- { ok: true, label: 'Next' },
1448
- { ok: true },
1449
- { ok: true },
1450
- { ok: true },
1451
- { ok: true, label: 'Share' },
1452
- { ok: false, failed: true, url: '' },
1453
- ]));
1454
- const cmd = getRegistry().get('instagram/post');
1455
- await expect(cmd.func(page, {
1456
- media: imagePath,
1457
- content: 'share should fail',
1458
- })).rejects.toThrow('Instagram post share failed');
1459
- });
1460
- it('keeps waiting across the full publish timeout window instead of fast-forwarding after 30 polls', async () => {
1461
- const imagePath = createTempImage('slow-share.jpg');
1462
- const page = createPageMock(withInitialDialogDismiss([
1463
- { ok: true },
1464
- { ok: true, selectors: ['[data-bycli-ig-upload-index="0"]'] },
1465
- { ok: true },
1466
- { ok: true },
1467
- { ok: false },
1468
- { ok: true, label: 'Next' },
1469
- { ok: true },
1470
- { ok: true },
1471
- { ok: true },
1472
- { ok: true, label: 'Share' },
1473
- ...Array.from({ length: 35 }, () => ({ ok: false, failed: false, settled: false, url: '' })),
1474
- { ok: true, url: 'https://www.instagram.com/p/SLOWSHARE123/' },
1475
- ]));
1476
- const cmd = getRegistry().get('instagram/post');
1477
- const result = await cmd.func(page, {
1478
- media: imagePath,
1479
- content: 'slow share eventually succeeds',
1480
- });
1481
- const waitCalls = page.wait.mock.calls.filter((args) => args[0]?.time === 1);
1482
- expect(waitCalls.length).toBeGreaterThanOrEqual(35);
1483
- expect(result).toEqual([
1484
- {
1485
- status: '✅ Posted',
1486
- detail: 'Single image post shared successfully',
1487
- url: 'https://www.instagram.com/p/SLOWSHARE123/',
1488
- },
1489
- ]);
1490
- });
1491
- it('does not retry the upload flow after Share has already been clicked', async () => {
1492
- const imagePath = createTempImage('no-duplicate-retry.jpg');
1493
- const setFileInput = vi.fn().mockResolvedValue(undefined);
1494
- const page = createPageMock(withInitialDialogDismiss([
1495
- { ok: true },
1496
- { ok: true, selectors: ['[data-bycli-ig-upload-index="0"]'] },
1497
- { ok: true },
1498
- { ok: true },
1499
- { ok: false },
1500
- { ok: true, label: 'Next' },
1501
- { ok: true },
1502
- { ok: true },
1503
- { ok: true },
1504
- { ok: true, label: 'Share' },
1505
- ...Array.from({ length: 30 }, () => ({ ok: false, failed: false, url: '' })),
1506
- ]), { setFileInput });
1507
- const cmd = getRegistry().get('instagram/post');
1508
- await expect(cmd.func(page, {
1509
- media: imagePath,
1510
- content: 'share observation stalled',
1511
- })).rejects.toThrow('Instagram post share confirmation did not appear');
1512
- expect(setFileInput).toHaveBeenCalledTimes(1);
1513
- });
1514
- it('recovers the latest post URL from the current logged-in profile when success does not navigate to /p/', async () => {
1515
- const imagePath = createTempImage('url-recovery.jpg');
1516
- const evaluate = vi.fn(async (js) => {
1517
- if (js.includes('const data = Array.isArray(window[') && js.includes('__bycli_ig_protocol_capture')) {
1518
- return { data: [], errors: [] };
1519
- }
1520
- if (js.includes('fetch(') && js.includes('/api/v1/users/') && js.includes('X-IG-App-ID')) {
1521
- return js.includes('dynamic-runtime-app-id')
1522
- ? { ok: true, username: 'tsezi_ray' }
1523
- : { ok: false };
1524
- }
1525
- if (js.includes('window.location?.pathname'))
1526
- return { ok: true };
1527
- if (js.includes('data-bycli-ig-upload-index'))
1528
- return { ok: true, selectors: ['[data-bycli-ig-upload-index="0"]'] };
1529
- if (js.includes("dispatchEvent(new Event('input'"))
1530
- return { ok: true };
1531
- if (js.includes('const hasPreviewUi ='))
1532
- return { ok: true, state: 'preview' };
1533
- if (js.includes("scope === 'media'"))
1534
- return { ok: true, label: 'Next' };
1535
- if (js.includes("scope === 'caption'"))
1536
- return { ok: true, label: 'Share' };
1537
- if (js.includes('post shared') && js.includes('your post has been shared'))
1538
- return { ok: true, url: '' };
1539
- if (js.includes('const hrefs = Array.from(document.querySelectorAll(\'a[href*="/p/"]\'))')) {
1540
- const calls = evaluate.mock.calls.filter(([script]) => typeof script === 'string' && script.includes('const hrefs = Array.from(document.querySelectorAll(\'a[href*="/p/"]\'))')).length;
1541
- return calls === 1
1542
- ? { ok: true, hrefs: ['/tsezi_ray/p/PINNED111/', '/tsezi_ray/p/OLD222/'] }
1543
- : { ok: true, hrefs: ['/tsezi_ray/p/PINNED111/', '/tsezi_ray/p/OLD222/', '/tsezi_ray/p/RECOVER123/'] };
1544
- }
1545
- if (js.includes('document.documentElement?.outerHTML')) {
1546
- return {
1547
- appId: 'dynamic-runtime-app-id',
1548
- csrfToken: 'csrf-token',
1549
- instagramAjax: 'dynamic-rollout',
1550
- };
1551
- }
1552
- return { ok: true };
1553
- });
1554
- const page = createPageMock([], {
1555
- evaluate,
1556
- getCookies: vi.fn().mockResolvedValue([{ name: 'ds_user_id', value: '61236465677', domain: 'instagram.com' }]),
1557
- });
1558
- const cmd = getRegistry().get('instagram/post');
1559
- const result = await cmd.func(page, {
1560
- media: imagePath,
1561
- content: 'url recovery',
1562
- });
1563
- expect(result).toEqual([
1564
- {
1565
- status: '✅ Posted',
1566
- detail: 'Single image post shared successfully',
1567
- url: 'https://www.instagram.com/tsezi_ray/p/RECOVER123/',
1568
- },
1569
- ]);
1570
- });
1571
- it('treats a closed composer as a successful share and falls back to URL recovery', async () => {
1572
- const imagePath = createTempImage('share-settled.jpg');
1573
- const page = createPageMock([
1574
- { appId: 'dynamic-runtime-app-id', csrfToken: 'csrf-token', instagramAjax: 'dynamic-rollout' },
1575
- { ok: true, username: 'tsezi_ray' },
1576
- { ok: true, hrefs: ['/p/OLD111/'] },
1577
- { ok: false },
1578
- { ok: true },
1579
- { ok: true, selectors: ['[data-bycli-ig-upload-index="0"]'] },
1580
- { ok: true },
1581
- { ok: true },
1582
- { ok: false },
1583
- { ok: true, label: 'Next' },
1584
- { ok: true },
1585
- { ok: true },
1586
- { ok: true },
1587
- { ok: true, label: 'Share' },
1588
- { ok: false, failed: false, settled: true, url: '' },
1589
- { ok: false, failed: false, settled: true, url: '' },
1590
- { ok: false, failed: false, settled: true, url: '' },
1591
- { appId: 'dynamic-runtime-app-id', csrfToken: 'csrf-token', instagramAjax: 'dynamic-rollout' },
1592
- { ok: true, username: 'tsezi_ray' },
1593
- { ok: true, hrefs: ['/p/OLD111/', '/p/RECOVER789/'] },
1594
- ], {
1595
- getCookies: vi.fn().mockResolvedValue([{ name: 'ds_user_id', value: '61236465677', domain: 'instagram.com' }]),
1596
- });
1597
- const cmd = getRegistry().get('instagram/post');
1598
- const result = await cmd.func(page, {
1599
- media: imagePath,
1600
- content: 'share settled recovery',
1601
- });
1602
- expect(result).toEqual([
1603
- {
1604
- status: '✅ Posted',
1605
- detail: 'Single image post shared successfully',
1606
- url: 'https://www.instagram.com/p/RECOVER789/',
1607
- },
1608
- ]);
1609
- });
1610
- it('accepts standard /p/... profile links during URL recovery', async () => {
1611
- const imagePath = createTempImage('url-recovery-standard-shape.jpg');
1612
- const page = createPageMock([
1613
- { appId: 'dynamic-runtime-app-id', csrfToken: 'csrf-token', instagramAjax: 'dynamic-rollout' },
1614
- { ok: true, username: 'tsezi_ray' },
1615
- { ok: true, hrefs: ['/p/PINNED111/', '/p/OLD222/'] },
1616
- { ok: false },
1617
- { ok: true },
1618
- { ok: true, selectors: ['[data-bycli-ig-upload-index="0"]'] },
1619
- { ok: true },
1620
- { ok: true },
1621
- { ok: false },
1622
- { ok: true, label: 'Next' },
1623
- { ok: true },
1624
- { ok: true },
1625
- { ok: true },
1626
- { ok: true, label: 'Share' },
1627
- { ok: true, url: '' },
1628
- { appId: 'dynamic-runtime-app-id', csrfToken: 'csrf-token', instagramAjax: 'dynamic-rollout' },
1629
- { ok: true, username: 'tsezi_ray' },
1630
- { ok: true, hrefs: ['/p/PINNED111/', '/p/OLD222/', '/p/RECOVER456/'] },
1631
- ], {
1632
- getCookies: vi.fn().mockResolvedValue([{ name: 'ds_user_id', value: '61236465677', domain: 'instagram.com' }]),
1633
- });
1634
- const cmd = getRegistry().get('instagram/post');
1635
- const result = await cmd.func(page, {
1636
- media: imagePath,
1637
- content: 'url recovery standard shape',
1638
- });
1639
- expect(result).toEqual([
1640
- {
1641
- status: '✅ Posted',
1642
- detail: 'Single image post shared successfully',
1643
- url: 'https://www.instagram.com/p/RECOVER456/',
1644
- },
1645
- ]);
1646
- });
1647
- });