@sovovs/bycli 1.8.0 → 2.1.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 (539) hide show
  1. package/cli-manifest.json +0 -61
  2. package/clis/twitter/search.js +3 -3
  3. package/dist/src/adapter-shadow.js +2 -2
  4. package/dist/src/browser/analyze.d.ts +10 -99
  5. package/dist/src/browser/analyze.js +10 -226
  6. package/dist/src/browser/cdp.js +67 -7
  7. package/dist/src/browser/daemon-client.d.ts +2 -0
  8. package/dist/src/browser/daemon-client.js +1 -1
  9. package/dist/src/browser/extension-capabilities.d.ts +13 -0
  10. package/dist/src/browser/extension-capabilities.js +22 -0
  11. package/dist/src/browser/extension-capabilities.test.d.ts +1 -0
  12. package/dist/src/browser/extension-version-metadata.test.d.ts +1 -0
  13. package/dist/src/browser/navigation-guard.d.ts +78 -0
  14. package/dist/src/browser/navigation-guard.js +142 -0
  15. package/dist/src/browser/navigation-guard.test.d.ts +1 -0
  16. package/dist/src/browser/page.d.ts +1 -0
  17. package/dist/src/browser/page.js +20 -1
  18. package/dist/src/browser/url-policy.d.ts +89 -0
  19. package/dist/src/browser/url-policy.js +311 -0
  20. package/dist/src/browser/url-policy.test.d.ts +1 -0
  21. package/dist/src/browser/verify-fixture.js +2 -2
  22. package/dist/src/build-manifest.js +4 -2
  23. package/dist/src/capabilityRouting.d.ts +3 -2
  24. package/dist/src/capabilityRouting.js +10 -2
  25. package/dist/src/cli.js +11 -14
  26. package/dist/src/commanderAdapter.js +5 -5
  27. package/dist/src/config-paths.d.ts +19 -0
  28. package/dist/src/config-paths.js +31 -0
  29. package/dist/src/daemon.js +256 -0
  30. package/dist/src/discovery.d.ts +10 -3
  31. package/dist/src/discovery.js +20 -10
  32. package/dist/src/discovery.test.d.ts +1 -0
  33. package/dist/src/execution.d.ts +5 -0
  34. package/dist/src/execution.js +272 -48
  35. package/dist/src/help.js +8 -8
  36. package/dist/src/main.js +18 -2
  37. package/dist/src/manifest-schema.d.ts +9 -0
  38. package/dist/src/manifest-schema.js +162 -0
  39. package/dist/src/manifest-schema.test.d.ts +1 -0
  40. package/dist/src/manifest-types.d.ts +1 -1
  41. package/dist/src/observation/redaction.js +10 -4
  42. package/dist/src/plugin.js +11 -8
  43. package/dist/src/recorder/highlevel/analyze.d.ts +29 -0
  44. package/dist/src/recorder/highlevel/analyze.js +91 -0
  45. package/dist/src/recorder/highlevel/init.d.ts +125 -0
  46. package/dist/src/recorder/highlevel/init.js +279 -0
  47. package/dist/src/recorder/highlevel/init.test.d.ts +1 -0
  48. package/dist/src/recorder/highlevel/verify.d.ts +73 -0
  49. package/dist/src/recorder/highlevel/verify.js +74 -0
  50. package/dist/src/recorder/http/analyze-runner.d.ts +28 -0
  51. package/dist/src/recorder/http/analyze-runner.js +78 -0
  52. package/dist/src/recorder/http/analyze-runner.test.d.ts +1 -0
  53. package/dist/src/recorder/http/handlers.d.ts +51 -0
  54. package/dist/src/recorder/http/handlers.js +219 -0
  55. package/dist/src/recorder/http/wrapper-config.d.ts +20 -0
  56. package/dist/src/recorder/http/wrapper-config.js +82 -0
  57. package/dist/src/recorder/http/wrapper-envelope.d.ts +31 -0
  58. package/dist/src/recorder/http/wrapper-envelope.js +51 -0
  59. package/dist/src/recorder/http/wrapper-gates.d.ts +16 -0
  60. package/dist/src/recorder/http/wrapper-gates.js +28 -0
  61. package/dist/src/recorder/http/wrapper-metrics.d.ts +1 -0
  62. package/dist/src/recorder/http/wrapper-metrics.js +5 -0
  63. package/dist/src/recorder/http/wrapper-registry.d.ts +50 -0
  64. package/dist/src/recorder/http/wrapper-registry.js +98 -0
  65. package/dist/src/recorder/http/wrapper-server.d.ts +59 -0
  66. package/dist/src/recorder/http/wrapper-server.js +240 -0
  67. package/dist/src/recorder/http/wrapper.test.d.ts +1 -0
  68. package/dist/src/recorder/observability/logger.d.ts +5 -0
  69. package/dist/src/recorder/observability/logger.js +13 -0
  70. package/dist/src/recorder/runner/config.d.ts +34 -0
  71. package/dist/src/recorder/runner/config.js +84 -0
  72. package/dist/src/recorder/runner/config.test.d.ts +1 -0
  73. package/dist/src/recorder/runner/reap.d.ts +79 -0
  74. package/dist/src/recorder/runner/reap.js +230 -0
  75. package/dist/src/recorder/runner/reap.test.d.ts +1 -0
  76. package/dist/src/recorder/runner/runner-port.d.ts +95 -0
  77. package/dist/src/recorder/runner/runner-port.js +518 -0
  78. package/dist/src/recorder/runner/runner-port.test.d.ts +1 -0
  79. package/dist/src/recorder/runner/session-keys.d.ts +28 -0
  80. package/dist/src/recorder/runner/session-keys.js +72 -0
  81. package/dist/src/recorder/runner/session-keys.test.d.ts +1 -0
  82. package/dist/src/recorder/runner/verify-runner-main.d.ts +96 -0
  83. package/dist/src/recorder/runner/verify-runner-main.js +292 -0
  84. package/dist/src/recorder/runner/verify-runner-main.test.d.ts +1 -0
  85. package/dist/src/registry-api.d.ts +1 -1
  86. package/dist/src/registry-api.types.test.d.ts +1 -0
  87. package/dist/src/registry-transaction.d.ts +42 -0
  88. package/dist/src/registry-transaction.js +194 -0
  89. package/dist/src/registry-transaction.test.d.ts +1 -0
  90. package/dist/src/registry.d.ts +58 -16
  91. package/dist/src/registry.js +131 -15
  92. package/dist/src/serialization.d.ts +1 -1
  93. package/dist/src/serialization.js +3 -3
  94. package/dist/src/types.d.ts +2 -0
  95. package/package.json +8 -5
  96. package/scripts/m1-daemon-navigate-drive.mjs +138 -0
  97. package/scripts/m1-extension-spike.mjs +237 -0
  98. package/scripts/m1-live-spike.mjs +156 -0
  99. package/scripts/m6b-connect-back-spike.sh +31 -0
  100. package/clis/12306/utils.test.js +0 -331
  101. package/clis/1688/assets.test.js +0 -39
  102. package/clis/1688/download.test.js +0 -31
  103. package/clis/1688/item.test.js +0 -67
  104. package/clis/1688/search.test.js +0 -75
  105. package/clis/1688/shared.test.js +0 -57
  106. package/clis/1688/store.test.js +0 -62
  107. package/clis/36kr/article.test.js +0 -46
  108. package/clis/36kr/hot.test.js +0 -15
  109. package/clis/36kr/news.test.js +0 -85
  110. package/clis/51job/utils.test.js +0 -69
  111. package/clis/_atlassian/shared.test.js +0 -170
  112. package/clis/aibase/news.test.js +0 -59
  113. package/clis/amazon/bestsellers.test.js +0 -29
  114. package/clis/amazon/discussion.test.js +0 -124
  115. package/clis/amazon/offer.test.js +0 -29
  116. package/clis/amazon/product.test.js +0 -24
  117. package/clis/amazon/rankings.test.js +0 -41
  118. package/clis/amazon/search.test.js +0 -22
  119. package/clis/amazon/shared.test.js +0 -44
  120. package/clis/apple-podcasts/commands.test.js +0 -119
  121. package/clis/apple-podcasts/utils.test.js +0 -57
  122. package/clis/arxiv/arxiv.test.js +0 -112
  123. package/clis/baidu-scholar/search.test.js +0 -23
  124. package/clis/barchart/greeks.test.js +0 -138
  125. package/clis/bilibili/comment.test.js +0 -153
  126. package/clis/bilibili/comments.test.js +0 -142
  127. package/clis/bilibili/dynamic.test.js +0 -68
  128. package/clis/bilibili/hot.test.js +0 -17
  129. package/clis/bilibili/subtitle.test.js +0 -196
  130. package/clis/bilibili/summary.test.js +0 -210
  131. package/clis/bilibili/utils.test.js +0 -61
  132. package/clis/bilibili/video.test.js +0 -132
  133. package/clis/binance/commands.test.js +0 -70
  134. package/clis/bloomberg/utils.test.js +0 -129
  135. package/clis/booking/booking.test.js +0 -356
  136. package/clis/boss/chatlist.test.js +0 -211
  137. package/clis/boss/chatmsg.test.js +0 -230
  138. package/clis/boss/search.test.js +0 -78
  139. package/clis/boss/utils.test.js +0 -34
  140. package/clis/brave/search.test.js +0 -76
  141. package/clis/chaoxing/utils.test.js +0 -45
  142. package/clis/chatgpt/commands.test.js +0 -45
  143. package/clis/chatgpt/envelope.test.js +0 -108
  144. package/clis/chatgpt/image.test.js +0 -188
  145. package/clis/chatgpt/utils.test.js +0 -330
  146. package/clis/chatgpt-app/ax.test.js +0 -35
  147. package/clis/chatwise/composer.test.js +0 -186
  148. package/clis/chess/analyze.test.js +0 -79
  149. package/clis/chess/game.test.js +0 -178
  150. package/clis/chess/games.test.js +0 -164
  151. package/clis/chess/stats.test.js +0 -79
  152. package/clis/chess/utils.test.js +0 -230
  153. package/clis/claude/ask.test.js +0 -338
  154. package/clis/claude/commands.test.js +0 -118
  155. package/clis/claude/utils.test.js +0 -148
  156. package/clis/cnki/search.test.js +0 -18
  157. package/clis/codex/sidebar.test.js +0 -329
  158. package/clis/coingecko/coingecko.test.js +0 -109
  159. package/clis/confluence/commands.test.js +0 -195
  160. package/clis/coupang/coupang.test.js +0 -159
  161. package/clis/coupang/utils.test.js +0 -62
  162. package/clis/ctrip/ctrip.test.js +0 -719
  163. package/clis/dblp/dblp.test.js +0 -397
  164. package/clis/deepseek/ask.test.js +0 -312
  165. package/clis/deepseek/detail.test.js +0 -81
  166. package/clis/deepseek/send.test.js +0 -107
  167. package/clis/deepseek/utils.test.js +0 -370
  168. package/clis/defillama/defillama.test.js +0 -99
  169. package/clis/devto/devto.test.js +0 -236
  170. package/clis/dianping/dianping.test.js +0 -578
  171. package/clis/douban/download.test.js +0 -170
  172. package/clis/douban/movie-hot.test.js +0 -14
  173. package/clis/douban/search.test.js +0 -11
  174. package/clis/douban/subject.test.js +0 -11
  175. package/clis/douban/utils.test.js +0 -438
  176. package/clis/doubao/detail.test.js +0 -42
  177. package/clis/doubao/history.test.js +0 -37
  178. package/clis/doubao/utils.test.js +0 -344
  179. package/clis/douyin/_shared/browser-fetch.test.js +0 -64
  180. package/clis/douyin/_shared/creation-id.test.js +0 -22
  181. package/clis/douyin/_shared/imagex-upload.test.js +0 -87
  182. package/clis/douyin/_shared/sts2.test.js +0 -27
  183. package/clis/douyin/_shared/text-extra.test.js +0 -37
  184. package/clis/douyin/_shared/timing.test.js +0 -28
  185. package/clis/douyin/_shared/tos-upload-short-read.test.js +0 -83
  186. package/clis/douyin/_shared/tos-upload.test.js +0 -229
  187. package/clis/douyin/_shared/transcode.test.js +0 -91
  188. package/clis/douyin/_shared/vod-upload.test.js +0 -38
  189. package/clis/douyin/activities.test.js +0 -55
  190. package/clis/douyin/collections.test.js +0 -45
  191. package/clis/douyin/delete.test.js +0 -100
  192. package/clis/douyin/draft.test.js +0 -335
  193. package/clis/douyin/drafts.test.js +0 -11
  194. package/clis/douyin/hashtag.test.js +0 -171
  195. package/clis/douyin/location.test.js +0 -23
  196. package/clis/douyin/profile.test.js +0 -46
  197. package/clis/douyin/publish-upload-id.test.js +0 -170
  198. package/clis/douyin/publish.test.js +0 -38
  199. package/clis/douyin/stats.test.js +0 -22
  200. package/clis/douyin/update.test.js +0 -11
  201. package/clis/douyin/user-videos.test.js +0 -151
  202. package/clis/douyin/videos.test.js +0 -54
  203. package/clis/duckduckgo/search.test.js +0 -128
  204. package/clis/duckduckgo/suggest.test.js +0 -66
  205. package/clis/eastmoney/hot-rank.test.js +0 -59
  206. package/clis/endoflife/endoflife.test.js +0 -51
  207. package/clis/facebook/feed.test.js +0 -169
  208. package/clis/facebook/marketplace.test.js +0 -91
  209. package/clis/facebook/notifications.test.js +0 -458
  210. package/clis/facebook/search.test.js +0 -55
  211. package/clis/flathub/flathub.test.js +0 -90
  212. package/clis/flomo/memos.test.js +0 -144
  213. package/clis/gemini/ask.test.js +0 -100
  214. package/clis/gemini/deep-research-result.test.js +0 -155
  215. package/clis/gemini/deep-research.test.js +0 -181
  216. package/clis/gemini/reply-state.test.js +0 -671
  217. package/clis/gemini/utils.test.js +0 -198
  218. package/clis/geogebra/geogebra.test.js +0 -175
  219. package/clis/gitee/search.test.js +0 -65
  220. package/clis/gitee/user.test.js +0 -63
  221. package/clis/google/utils.test.js +0 -75
  222. package/clis/google-scholar/cite.test.js +0 -47
  223. package/clis/google-scholar/profile.test.js +0 -49
  224. package/clis/google-scholar/search.test.js +0 -71
  225. package/clis/goproxy/goproxy.test.js +0 -103
  226. package/clis/gov-law/commands.test.js +0 -39
  227. package/clis/gov-policy/gov-policy.test.js +0 -224
  228. package/clis/grok/ask.test.js +0 -29
  229. package/clis/grok/utils.test.js +0 -103
  230. package/clis/hackernews/hackernews.test.js +0 -132
  231. package/clis/hf/hf.test.js +0 -16
  232. package/clis/hupu/hot.test.js +0 -224
  233. package/clis/imdb/utils.test.js +0 -88
  234. package/clis/indeed/indeed.test.js +0 -375
  235. package/clis/instagram/_shared/private-publish.test.js +0 -705
  236. package/clis/instagram/_shared/protocol-capture.test.js +0 -114
  237. package/clis/instagram/download.test.js +0 -118
  238. package/clis/instagram/note.test.js +0 -53
  239. package/clis/instagram/post.test.js +0 -1647
  240. package/clis/instagram/reel.test.js +0 -167
  241. package/clis/instagram/story.test.js +0 -134
  242. package/clis/jd/commands.test.js +0 -41
  243. package/clis/jd/item.test.js +0 -354
  244. package/clis/jianyu/search.test.js +0 -153
  245. package/clis/jianyu/shared/procurement-contract.test.js +0 -72
  246. package/clis/jianyu/shared/procurement-detail.test.js +0 -84
  247. package/clis/jike/read.test.js +0 -86
  248. package/clis/jira/commands.test.js +0 -287
  249. package/clis/juejin/search.js +0 -123
  250. package/clis/juejin/search.test.js +0 -98
  251. package/clis/lesswrong/frontpage.test.js +0 -37
  252. package/clis/lichess/lichess.test.js +0 -85
  253. package/clis/linkedin/connect.test.js +0 -213
  254. package/clis/linkedin/inbox.test.js +0 -152
  255. package/clis/linkedin/job-detail.test.js +0 -38
  256. package/clis/linkedin/jobs-preferences.test.js +0 -43
  257. package/clis/linkedin/people-search.test.js +0 -216
  258. package/clis/linkedin/post-analytics.test.js +0 -40
  259. package/clis/linkedin/posts.test.js +0 -40
  260. package/clis/linkedin/profile-analytics.test.js +0 -67
  261. package/clis/linkedin/profile-experience.test.js +0 -152
  262. package/clis/linkedin/profile-projects.test.js +0 -111
  263. package/clis/linkedin/profile-read.test.js +0 -77
  264. package/clis/linkedin/safe-send.test.js +0 -204
  265. package/clis/linkedin/salesnav-inbox.test.js +0 -113
  266. package/clis/linkedin/salesnav-message.test.js +0 -172
  267. package/clis/linkedin/salesnav-search.test.js +0 -76
  268. package/clis/linkedin/salesnav-thread.test.js +0 -79
  269. package/clis/linkedin/search.test.js +0 -222
  270. package/clis/linkedin/sent-invitations.test.js +0 -62
  271. package/clis/linkedin/services-read.test.js +0 -105
  272. package/clis/linkedin/thread-snapshot.test.js +0 -89
  273. package/clis/linkedin/timeline.test.js +0 -81
  274. package/clis/linkedin-learning/course.test.js +0 -114
  275. package/clis/linkedin-learning/search.test.js +0 -144
  276. package/clis/linkedin-learning/trending.test.js +0 -123
  277. package/clis/linux-do/feed.test.js +0 -153
  278. package/clis/linux-do/topic-content.test.js +0 -59
  279. package/clis/lobsters/lobsters.test.js +0 -169
  280. package/clis/notebooklm/add-source.test.js +0 -97
  281. package/clis/notebooklm/compat.test.js +0 -16
  282. package/clis/notebooklm/create.test.js +0 -58
  283. package/clis/notebooklm/generate-audio.test.js +0 -63
  284. package/clis/notebooklm/generate-slides.test.js +0 -75
  285. package/clis/notebooklm/history.test.js +0 -58
  286. package/clis/notebooklm/note-list.test.js +0 -56
  287. package/clis/notebooklm/notes-get.test.js +0 -72
  288. package/clis/notebooklm/open.test.js +0 -63
  289. package/clis/notebooklm/rpc.test.js +0 -131
  290. package/clis/notebooklm/source-fulltext.test.js +0 -106
  291. package/clis/notebooklm/source-get.test.js +0 -84
  292. package/clis/notebooklm/source-guide.test.js +0 -104
  293. package/clis/notebooklm/summary.test.js +0 -78
  294. package/clis/notebooklm/utils.test.js +0 -449
  295. package/clis/notebooklm/write-note.test.js +0 -70
  296. package/clis/nuget/nuget.test.js +0 -111
  297. package/clis/oeis/oeis.test.js +0 -88
  298. package/clis/ones/task-helpers.test.js +0 -12
  299. package/clis/ones/worklog.test.js +0 -20
  300. package/clis/openfda/openfda.test.js +0 -114
  301. package/clis/openreview/openreview.test.js +0 -427
  302. package/clis/osv/osv.test.js +0 -97
  303. package/clis/paperreview/commands.test.js +0 -243
  304. package/clis/paperreview/utils.test.js +0 -49
  305. package/clis/pixiv/download.test.js +0 -87
  306. package/clis/pixiv/illusts.test.js +0 -99
  307. package/clis/pixiv/search.test.js +0 -83
  308. package/clis/powerchina/search.test.js +0 -93
  309. package/clis/producthunt/utils.test.js +0 -64
  310. package/clis/pubmed/pubmed.test.js +0 -276
  311. package/clis/quark/utils.test.js +0 -58
  312. package/clis/qwen/utils.test.js +0 -45
  313. package/clis/reddit/extract-media.test.js +0 -149
  314. package/clis/reddit/frontpage.test.js +0 -34
  315. package/clis/reddit/home.test.js +0 -170
  316. package/clis/reddit/hot.test.js +0 -32
  317. package/clis/reddit/popular.test.js +0 -26
  318. package/clis/reddit/read.test.js +0 -335
  319. package/clis/reddit/reply.test.js +0 -89
  320. package/clis/reddit/search.test.js +0 -26
  321. package/clis/reddit/subreddit-info.test.js +0 -163
  322. package/clis/reddit/subreddit.test.js +0 -31
  323. package/clis/reddit/subscribed.test.js +0 -168
  324. package/clis/reddit/whoami.test.js +0 -105
  325. package/clis/rednote/navigation.test.js +0 -26
  326. package/clis/rednote/rednote.test.js +0 -222
  327. package/clis/rest-countries/rest-countries.test.js +0 -83
  328. package/clis/reuters/reuters.test.js +0 -299
  329. package/clis/rfc/rfc.test.js +0 -74
  330. package/clis/sinafinance/stock.test.js +0 -59
  331. package/clis/spotify/utils.test.js +0 -67
  332. package/clis/stackoverflow/stackoverflow.test.js +0 -404
  333. package/clis/steam/steam.test.js +0 -46
  334. package/clis/substack/utils.test.js +0 -44
  335. package/clis/suno/commands.test.js +0 -188
  336. package/clis/suno/download.test.js +0 -151
  337. package/clis/suno/generate.test.js +0 -252
  338. package/clis/suno/utils.test.js +0 -329
  339. package/clis/taobao/commands.test.js +0 -41
  340. package/clis/tdx/hot-rank.test.js +0 -59
  341. package/clis/ths/hot-rank.test.js +0 -64
  342. package/clis/tieba/commands.test.js +0 -79
  343. package/clis/tieba/utils.test.js +0 -290
  344. package/clis/tiktok/creator-videos.test.js +0 -113
  345. package/clis/tiktok/refactor.test.js +0 -389
  346. package/clis/tiktok/write-refactor.test.js +0 -370
  347. package/clis/toutiao/toutiao.test.js +0 -378
  348. package/clis/tvmaze/tvmaze.test.js +0 -93
  349. package/clis/twitter/bookmark-folder.test.js +0 -395
  350. package/clis/twitter/bookmark-folders.test.js +0 -152
  351. package/clis/twitter/bookmark.test.js +0 -74
  352. package/clis/twitter/bookmarks.test.js +0 -205
  353. package/clis/twitter/delete.test.js +0 -85
  354. package/clis/twitter/device-follow.test.js +0 -287
  355. package/clis/twitter/download.test.js +0 -457
  356. package/clis/twitter/followers.test.js +0 -62
  357. package/clis/twitter/following.test.js +0 -338
  358. package/clis/twitter/hide-reply.test.js +0 -76
  359. package/clis/twitter/like.test.js +0 -73
  360. package/clis/twitter/likes.test.js +0 -220
  361. package/clis/twitter/list-add.test.js +0 -133
  362. package/clis/twitter/list-create.test.js +0 -169
  363. package/clis/twitter/list-remove.test.js +0 -110
  364. package/clis/twitter/list-tweets.test.js +0 -196
  365. package/clis/twitter/lists.test.js +0 -253
  366. package/clis/twitter/post.test.js +0 -258
  367. package/clis/twitter/profile.test.js +0 -39
  368. package/clis/twitter/quote.test.js +0 -194
  369. package/clis/twitter/reply.test.js +0 -237
  370. package/clis/twitter/retweet.test.js +0 -73
  371. package/clis/twitter/search.test.js +0 -349
  372. package/clis/twitter/shared.test.js +0 -794
  373. package/clis/twitter/thread.test.js +0 -58
  374. package/clis/twitter/timeline.test.js +0 -104
  375. package/clis/twitter/trending.test.js +0 -15
  376. package/clis/twitter/tweets.test.js +0 -362
  377. package/clis/twitter/unbookmark.test.js +0 -73
  378. package/clis/twitter/unlike.test.js +0 -75
  379. package/clis/twitter/unretweet.test.js +0 -73
  380. package/clis/twitter/utils.test.js +0 -169
  381. package/clis/uisdc/news.test.js +0 -66
  382. package/clis/uiverse/_shared.test.js +0 -62
  383. package/clis/uiverse/navigation.test.js +0 -12
  384. package/clis/upwork/upwork.test.js +0 -566
  385. package/clis/wanfang/search.test.js +0 -23
  386. package/clis/web/read.test.js +0 -392
  387. package/clis/weibo/delete.test.js +0 -94
  388. package/clis/weibo/envelope.test.js +0 -85
  389. package/clis/weibo/favorites.test.js +0 -114
  390. package/clis/weibo/publish.test.js +0 -192
  391. package/clis/weibo/user-posts.test.js +0 -92
  392. package/clis/weibo/utils.test.js +0 -36
  393. package/clis/weixin/drafts.test.js +0 -69
  394. package/clis/weixin/search.test.js +0 -227
  395. package/clis/weread/ai-outline.test.js +0 -83
  396. package/clis/weread/book-search.test.js +0 -242
  397. package/clis/weread/commands.test.js +0 -398
  398. package/clis/weread/private-api-regression.test.js +0 -369
  399. package/clis/weread/search-regression.test.js +0 -494
  400. package/clis/weread/utils.test.js +0 -128
  401. package/clis/weread-official/commands.test.js +0 -385
  402. package/clis/weread-official/utils.test.js +0 -242
  403. package/clis/wikidata/wikidata.test.js +0 -83
  404. package/clis/wikipedia/trending.test.js +0 -57
  405. package/clis/wttr/wttr.test.js +0 -84
  406. package/clis/xianyu/chat.test.js +0 -84
  407. package/clis/xianyu/im.test.js +0 -253
  408. package/clis/xianyu/item.test.js +0 -56
  409. package/clis/xianyu/publish.test.js +0 -220
  410. package/clis/xianyu/search.test.js +0 -17
  411. package/clis/xiaoe/xiaoe.test.js +0 -486
  412. package/clis/xiaohongshu/comments.test.js +0 -258
  413. package/clis/xiaohongshu/creator-note-detail.test.js +0 -451
  414. package/clis/xiaohongshu/creator-notes-summary.test.js +0 -94
  415. package/clis/xiaohongshu/creator-notes.test.js +0 -281
  416. package/clis/xiaohongshu/creator-stats.test.js +0 -24
  417. package/clis/xiaohongshu/delete-note.test.js +0 -172
  418. package/clis/xiaohongshu/download.test.js +0 -316
  419. package/clis/xiaohongshu/navigation.test.js +0 -34
  420. package/clis/xiaohongshu/note.test.js +0 -249
  421. package/clis/xiaohongshu/publish.test.js +0 -608
  422. package/clis/xiaohongshu/search.test.js +0 -325
  423. package/clis/xiaohongshu/user-helpers.test.js +0 -160
  424. package/clis/xiaoyuzhou/auth.test.js +0 -124
  425. package/clis/xiaoyuzhou/download.test.js +0 -135
  426. package/clis/xiaoyuzhou/podcast-episodes.test.js +0 -78
  427. package/clis/xiaoyuzhou/transcript.test.js +0 -195
  428. package/clis/xiaoyuzhou/utils.test.js +0 -39
  429. package/clis/xueqiu/comments.test.js +0 -696
  430. package/clis/xueqiu/danjuan-utils.test.js +0 -41
  431. package/clis/xueqiu/utils.test.js +0 -26
  432. package/clis/yahoo/search.test.js +0 -94
  433. package/clis/youdao/note.test.js +0 -99
  434. package/clis/youtube/channel.test.js +0 -59
  435. package/clis/youtube/feed.test.js +0 -131
  436. package/clis/youtube/transcript-group.test.js +0 -99
  437. package/clis/youtube/transcript.test.js +0 -296
  438. package/clis/youtube/utils.test.js +0 -84
  439. package/clis/yuanbao/ask.test.js +0 -124
  440. package/clis/yuanbao/new.test.js +0 -30
  441. package/clis/yuanbao/shared.test.js +0 -80
  442. package/clis/zhihu/answer-comments.test.js +0 -287
  443. package/clis/zhihu/answer-detail.test.js +0 -338
  444. package/clis/zhihu/answer.test.js +0 -54
  445. package/clis/zhihu/collection.test.js +0 -337
  446. package/clis/zhihu/collections.test.js +0 -182
  447. package/clis/zhihu/comment.test.js +0 -50
  448. package/clis/zhihu/download.test.js +0 -12
  449. package/clis/zhihu/favorite.test.js +0 -59
  450. package/clis/zhihu/follow.test.js +0 -42
  451. package/clis/zhihu/like.test.js +0 -41
  452. package/clis/zhihu/question.test.js +0 -251
  453. package/clis/zhihu/recommend.test.js +0 -143
  454. package/clis/zhihu/search.test.js +0 -198
  455. package/clis/zhihu/target.test.js +0 -77
  456. package/clis/zhihu/text.test.js +0 -24
  457. package/clis/zhihu/write-shared.test.js +0 -176
  458. package/clis/zlibrary/commands.test.js +0 -65
  459. package/clis/zsxq/search.test.js +0 -24
  460. package/clis/zsxq/topic.test.js +0 -30
  461. package/clis/zsxq/topics.test.js +0 -24
  462. package/clis/zsxq/utils.test.js +0 -31
  463. package/dist/src/adapter-shadow.test.js +0 -49
  464. package/dist/src/adapter-source.test.js +0 -29
  465. package/dist/src/browser/analyze.test.js +0 -166
  466. package/dist/src/browser/article-extract.e2e.test.js +0 -105
  467. package/dist/src/browser/article-extract.test.js +0 -94
  468. package/dist/src/browser/ax-snapshot.test.js +0 -91
  469. package/dist/src/browser/base-page.test.js +0 -758
  470. package/dist/src/browser/cdp-click-fixture.test.js +0 -87
  471. package/dist/src/browser/cdp.test.js +0 -79
  472. package/dist/src/browser/compound.test.js +0 -175
  473. package/dist/src/browser/daemon-client.test.js +0 -199
  474. package/dist/src/browser/dom-helpers.test.js +0 -106
  475. package/dist/src/browser/dom-snapshot.test.js +0 -313
  476. package/dist/src/browser/errors.test.js +0 -60
  477. package/dist/src/browser/extract.test.js +0 -129
  478. package/dist/src/browser/find.test.js +0 -180
  479. package/dist/src/browser/html-tree.test.js +0 -181
  480. package/dist/src/browser/network-cache.test.js +0 -58
  481. package/dist/src/browser/network-key.test.js +0 -49
  482. package/dist/src/browser/page.test.js +0 -378
  483. package/dist/src/browser/shape-filter.test.js +0 -101
  484. package/dist/src/browser/shape.test.js +0 -82
  485. package/dist/src/browser/stealth.test.js +0 -134
  486. package/dist/src/browser/target-errors.test.js +0 -94
  487. package/dist/src/browser/target-resolver.test.js +0 -118
  488. package/dist/src/browser/utils.test.js +0 -29
  489. package/dist/src/browser/verify-fixture.test.js +0 -219
  490. package/dist/src/browser.test.js +0 -232
  491. package/dist/src/build-manifest.test.js +0 -341
  492. package/dist/src/capabilityRouting.test.js +0 -63
  493. package/dist/src/cli-argv-preprocess.test.js +0 -209
  494. package/dist/src/cli.test.js +0 -2803
  495. package/dist/src/commanderAdapter.test.js +0 -352
  496. package/dist/src/commands/daemon.test.js +0 -219
  497. package/dist/src/completion.test.js +0 -24
  498. package/dist/src/convention-audit.test.js +0 -247
  499. package/dist/src/daemon.test.js +0 -60
  500. package/dist/src/doctor.test.js +0 -263
  501. package/dist/src/download/article-download.test.js +0 -235
  502. package/dist/src/download/index.test.js +0 -140
  503. package/dist/src/download/media-download.test.js +0 -112
  504. package/dist/src/download/progress.test.js +0 -36
  505. package/dist/src/electron-apps.test.js +0 -49
  506. package/dist/src/engine.test.js +0 -312
  507. package/dist/src/errors.test.js +0 -109
  508. package/dist/src/execution.test.js +0 -623
  509. package/dist/src/extension-manifest-regression.test.js +0 -12
  510. package/dist/src/external.test.js +0 -146
  511. package/dist/src/help.test.js +0 -58
  512. package/dist/src/hooks.test.js +0 -92
  513. package/dist/src/interceptor.test.js +0 -81
  514. package/dist/src/launcher.test.js +0 -138
  515. package/dist/src/node-network.test.js +0 -55
  516. package/dist/src/observation/artifact.test.js +0 -121
  517. package/dist/src/observation/manager.test.js +0 -13
  518. package/dist/src/observation/redaction.test.js +0 -32
  519. package/dist/src/observation/retention.test.js +0 -118
  520. package/dist/src/observation/ring-buffer.test.js +0 -22
  521. package/dist/src/output.test.js +0 -53
  522. package/dist/src/package-exports.test.js +0 -112
  523. package/dist/src/pipeline/executor.test.js +0 -182
  524. package/dist/src/pipeline/steps/download.test.js +0 -102
  525. package/dist/src/pipeline/steps/fetch.test.js +0 -117
  526. package/dist/src/pipeline/template.test.js +0 -175
  527. package/dist/src/pipeline/transform.test.js +0 -133
  528. package/dist/src/plugin-manifest.test.js +0 -179
  529. package/dist/src/plugin-scaffold.test.js +0 -83
  530. package/dist/src/plugin.test.js +0 -1324
  531. package/dist/src/registry.test.js +0 -209
  532. package/dist/src/runtime-detect.test.js +0 -39
  533. package/dist/src/serialization.test.js +0 -78
  534. package/dist/src/snapshotFormatter.test.js +0 -521
  535. package/dist/src/tui.test.js +0 -19
  536. package/dist/src/update-check.test.js +0 -116
  537. package/dist/src/utils.test.js +0 -155
  538. package/dist/src/validate.test.js +0 -90
  539. 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
- });