@jackwener/opencli 1.6.0 → 1.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +27 -45
- package/README.zh-CN.md +32 -34
- package/autoresearch/browse-tasks.json +18 -20
- package/autoresearch/commands/debug.ts +163 -0
- package/autoresearch/commands/fix.ts +145 -0
- package/autoresearch/commands/plan.ts +88 -0
- package/autoresearch/commands/run.ts +138 -0
- package/autoresearch/config.ts +82 -0
- package/autoresearch/engine.ts +359 -0
- package/autoresearch/eval-all.ts +127 -0
- package/autoresearch/eval-browse.ts +1 -1
- package/autoresearch/eval-publish.ts +238 -0
- package/autoresearch/eval-save.ts +249 -0
- package/autoresearch/eval-skill.ts +14 -8
- package/autoresearch/eval-v2ex.ts +220 -0
- package/autoresearch/eval-zhihu.ts +230 -0
- package/autoresearch/logger.ts +69 -0
- package/autoresearch/presets/combined-reliability.ts +27 -0
- package/autoresearch/presets/index.ts +23 -0
- package/autoresearch/presets/operate-reliability.ts +24 -0
- package/autoresearch/presets/save-reliability.ts +26 -0
- package/autoresearch/presets/skill-quality.ts +20 -0
- package/autoresearch/presets/v2ex-reliability.ts +24 -0
- package/autoresearch/presets/zhihu-reliability.ts +25 -0
- package/autoresearch/publish-tasks.json +345 -0
- package/autoresearch/run-save.sh +11 -0
- package/autoresearch/save-adapters/xhs-explore-deep.ts +64 -0
- package/autoresearch/save-adapters/xhs-note-comments.ts +61 -0
- package/autoresearch/save-adapters/xhs-search-full.ts +62 -0
- package/autoresearch/save-adapters/zhihu-hot-detail.ts +52 -0
- package/autoresearch/save-adapters/zhihu-question-full.ts +57 -0
- package/autoresearch/save-adapters/zhihu-search-detail.ts +53 -0
- package/autoresearch/save-tasks.json +281 -0
- package/autoresearch/v2ex-tasks.json +899 -0
- package/autoresearch/zhihu-tasks.json +848 -0
- package/bun.lock +615 -0
- package/dist/browser/base-page.d.ts +4 -2
- package/dist/browser/base-page.js +37 -4
- package/dist/browser/bridge.js +10 -8
- package/dist/browser/cdp.js +2 -6
- package/dist/browser/daemon-client.d.ts +11 -1
- package/dist/browser/daemon-client.js +3 -0
- package/dist/browser/dom-helpers.d.ts +4 -2
- package/dist/browser/dom-helpers.js +42 -31
- package/dist/browser/dom-snapshot.js +23 -1
- package/dist/browser/page.d.ts +7 -2
- package/dist/browser/page.js +112 -30
- package/dist/browser.test.js +1 -1
- package/dist/build-manifest.d.ts +1 -0
- package/dist/build-manifest.js +1 -0
- package/dist/cli-manifest.json +1133 -182
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +48 -7
- package/dist/cli.test.d.ts +1 -0
- package/dist/cli.test.js +88 -0
- package/dist/clis/1688/item.d.ts +70 -0
- package/dist/clis/1688/item.js +187 -0
- package/dist/clis/1688/item.test.d.ts +1 -0
- package/dist/clis/1688/item.test.js +67 -0
- package/dist/clis/1688/search.d.ts +56 -0
- package/dist/clis/1688/search.js +309 -0
- package/dist/clis/1688/search.test.d.ts +1 -0
- package/dist/clis/1688/search.test.js +75 -0
- package/dist/clis/1688/shared.d.ts +112 -0
- package/dist/clis/1688/shared.js +514 -0
- package/dist/clis/1688/shared.test.d.ts +1 -0
- package/dist/clis/1688/shared.test.js +57 -0
- package/dist/clis/1688/store.d.ts +45 -0
- package/dist/clis/1688/store.js +226 -0
- package/dist/clis/1688/store.test.d.ts +1 -0
- package/dist/clis/1688/store.test.js +62 -0
- package/dist/clis/amazon/bestsellers.d.ts +0 -20
- package/dist/clis/amazon/bestsellers.js +6 -129
- package/dist/clis/amazon/bestsellers.test.js +12 -3
- package/dist/clis/amazon/movers-shakers.d.ts +1 -0
- package/dist/clis/amazon/movers-shakers.js +7 -0
- package/dist/clis/amazon/new-releases.d.ts +1 -0
- package/dist/clis/amazon/new-releases.js +7 -0
- package/dist/clis/amazon/rankings.d.ts +59 -0
- package/dist/clis/amazon/rankings.js +226 -0
- package/dist/clis/amazon/rankings.test.d.ts +1 -0
- package/dist/clis/amazon/rankings.test.js +41 -0
- package/dist/clis/amazon/shared.d.ts +11 -0
- package/dist/clis/amazon/shared.js +121 -11
- package/dist/clis/amazon/shared.test.js +11 -0
- package/dist/clis/bilibili/comments.js +2 -2
- package/dist/clis/bilibili/comments.test.js +3 -2
- package/dist/clis/bilibili/download.js +2 -1
- package/dist/clis/bilibili/subtitle.js +4 -3
- package/dist/clis/bilibili/subtitle.test.js +2 -1
- package/dist/clis/bilibili/utils.d.ts +5 -0
- package/dist/clis/bilibili/utils.js +30 -0
- package/dist/clis/bilibili/utils.test.d.ts +1 -0
- package/dist/clis/bilibili/utils.test.js +17 -0
- package/dist/clis/douban/marks.js +1 -1
- package/dist/clis/douban/subject.yaml +50 -19
- package/dist/clis/doubao/utils.js +32 -12
- package/dist/clis/douyin/_shared/browser-fetch.test.js +0 -1
- package/dist/clis/douyin/_shared/transcode.test.js +0 -2
- package/dist/clis/douyin/draft.test.js +0 -2
- package/dist/clis/facebook/search.test.js +0 -2
- package/dist/clis/gemini/ask.js +9 -3
- package/dist/clis/gemini/ask.test.d.ts +1 -0
- package/dist/clis/gemini/ask.test.js +100 -0
- package/dist/clis/gemini/reply-state.test.d.ts +1 -0
- package/dist/clis/gemini/reply-state.test.js +641 -0
- package/dist/clis/gemini/utils.d.ts +44 -1
- package/dist/clis/gemini/utils.js +528 -61
- package/dist/clis/gemini/utils.test.js +149 -2
- package/dist/clis/hupu/detail.d.ts +1 -0
- package/dist/clis/hupu/detail.js +72 -0
- package/dist/clis/hupu/hot.yaml +43 -0
- package/dist/clis/hupu/like.d.ts +1 -0
- package/dist/clis/hupu/like.js +75 -0
- package/dist/clis/hupu/reply.d.ts +1 -0
- package/dist/clis/hupu/reply.js +71 -0
- package/dist/clis/hupu/search.d.ts +1 -0
- package/dist/clis/hupu/search.js +59 -0
- package/dist/clis/hupu/unlike.d.ts +1 -0
- package/dist/clis/hupu/unlike.js +75 -0
- package/dist/clis/hupu/utils.d.ts +20 -0
- package/dist/clis/hupu/utils.js +319 -0
- package/dist/clis/instagram/_shared/private-publish.d.ts +138 -0
- package/dist/clis/instagram/_shared/private-publish.js +1030 -0
- package/dist/clis/instagram/_shared/private-publish.test.d.ts +1 -0
- package/dist/clis/instagram/_shared/private-publish.test.js +705 -0
- package/dist/clis/instagram/_shared/protocol-capture.d.ts +26 -0
- package/dist/clis/instagram/_shared/protocol-capture.js +282 -0
- package/dist/clis/instagram/_shared/protocol-capture.test.d.ts +1 -0
- package/dist/clis/instagram/_shared/protocol-capture.test.js +114 -0
- package/dist/clis/instagram/_shared/runtime-info.d.ts +9 -0
- package/dist/clis/instagram/_shared/runtime-info.js +81 -0
- package/dist/clis/instagram/note.d.ts +1 -0
- package/dist/clis/instagram/note.js +222 -0
- package/dist/clis/instagram/note.test.d.ts +1 -0
- package/dist/clis/instagram/note.test.js +81 -0
- package/dist/clis/instagram/post.d.ts +4 -0
- package/dist/clis/instagram/post.js +1496 -0
- package/dist/clis/instagram/post.test.d.ts +1 -0
- package/dist/clis/instagram/post.test.js +1647 -0
- package/dist/clis/instagram/reel.d.ts +1 -0
- package/dist/clis/instagram/reel.js +826 -0
- package/dist/clis/instagram/reel.test.d.ts +1 -0
- package/dist/clis/instagram/reel.test.js +167 -0
- package/dist/clis/instagram/story.d.ts +1 -0
- package/dist/clis/instagram/story.js +115 -0
- package/dist/clis/instagram/story.test.d.ts +1 -0
- package/dist/clis/instagram/story.test.js +167 -0
- package/dist/clis/sinafinance/stock-rank.d.ts +4 -0
- package/dist/clis/sinafinance/stock-rank.js +65 -0
- package/dist/clis/substack/utils.test.js +0 -2
- package/dist/clis/twitter/post.js +72 -45
- package/dist/clis/twitter/post.test.d.ts +1 -0
- package/dist/clis/twitter/post.test.js +116 -0
- package/dist/clis/twitter/reply.d.ts +12 -0
- package/dist/clis/twitter/reply.js +257 -35
- package/dist/clis/twitter/reply.test.d.ts +1 -0
- package/dist/clis/twitter/reply.test.js +151 -0
- package/dist/clis/twitter/search.js +67 -5
- package/dist/clis/twitter/search.test.js +83 -5
- package/dist/clis/xianyu/chat.d.ts +7 -0
- package/dist/clis/xianyu/chat.js +146 -0
- package/dist/clis/xianyu/chat.test.d.ts +1 -0
- package/dist/clis/xianyu/chat.test.js +15 -0
- package/dist/clis/xianyu/item.d.ts +7 -0
- package/dist/clis/xianyu/item.js +152 -0
- package/dist/clis/xianyu/item.test.d.ts +1 -0
- package/dist/clis/xianyu/item.test.js +56 -0
- package/dist/clis/xianyu/search.d.ts +10 -0
- package/dist/clis/xianyu/search.js +134 -0
- package/dist/clis/xianyu/search.test.d.ts +1 -0
- package/dist/clis/xianyu/search.test.js +17 -0
- package/dist/clis/xianyu/utils.d.ts +1 -0
- package/dist/clis/xianyu/utils.js +8 -0
- package/dist/clis/xiaoe/catalog.yaml +129 -0
- package/dist/clis/xiaoe/content.yaml +43 -0
- package/dist/clis/xiaoe/courses.yaml +73 -0
- package/dist/clis/xiaoe/detail.yaml +39 -0
- package/dist/clis/xiaoe/play-url.yaml +124 -0
- package/dist/clis/xiaohongshu/comments.test.js +0 -2
- package/dist/clis/xiaohongshu/creator-note-detail.test.js +0 -2
- package/dist/clis/xiaohongshu/creator-notes.test.js +0 -2
- package/dist/clis/xiaohongshu/download.test.js +0 -2
- package/dist/clis/xiaohongshu/note.test.js +0 -2
- package/dist/clis/xiaohongshu/publish.test.js +0 -2
- package/dist/clis/xiaohongshu/search.js +29 -20
- package/dist/clis/xiaohongshu/search.test.js +56 -48
- package/dist/clis/yuanbao/ask.d.ts +21 -0
- package/dist/clis/yuanbao/ask.js +427 -0
- package/dist/clis/yuanbao/ask.test.d.ts +1 -0
- package/dist/clis/yuanbao/ask.test.js +124 -0
- package/dist/clis/yuanbao/new.d.ts +1 -0
- package/dist/clis/yuanbao/new.js +70 -0
- package/dist/clis/yuanbao/new.test.d.ts +1 -0
- package/dist/clis/yuanbao/new.test.js +30 -0
- package/dist/clis/yuanbao/shared.d.ts +13 -0
- package/dist/clis/yuanbao/shared.js +49 -0
- package/dist/clis/zhihu/question.js +30 -19
- package/dist/clis/zhihu/question.test.js +34 -16
- package/dist/commanderAdapter.js +8 -4
- package/dist/commanderAdapter.test.js +42 -0
- package/dist/completion.js +3 -1
- package/dist/completion.test.d.ts +1 -0
- package/dist/completion.test.js +23 -0
- package/dist/doctor.js +1 -1
- package/dist/electron-apps.d.ts +2 -0
- package/dist/electron-apps.js +7 -1
- package/dist/errors.js +1 -1
- package/dist/execution.js +25 -35
- package/dist/explore.js +1 -1
- package/dist/launcher.d.ts +4 -0
- package/dist/launcher.js +64 -8
- package/dist/launcher.test.js +88 -7
- package/dist/output.d.ts +2 -0
- package/dist/output.js +10 -1
- package/dist/output.test.d.ts +0 -3
- package/dist/output.test.js +59 -92
- package/dist/pipeline/executor.test.js +0 -2
- package/dist/pipeline/steps/download.test.js +0 -2
- package/dist/registry.d.ts +2 -0
- package/dist/serialization.d.ts +1 -0
- package/dist/serialization.js +1 -0
- package/dist/types.d.ts +9 -2
- package/docs/.vitepress/config.mts +4 -0
- package/docs/adapters/browser/1688.md +52 -0
- package/docs/adapters/browser/36kr.md +2 -1
- package/docs/adapters/browser/doubao.md +5 -1
- package/docs/adapters/browser/hupu.md +53 -0
- package/docs/adapters/browser/sinafinance.md +32 -2
- package/docs/adapters/browser/weibo.md +6 -1
- package/docs/adapters/browser/wikipedia.md +2 -0
- package/docs/adapters/browser/xianyu.md +42 -0
- package/docs/adapters/browser/xiaoe.md +44 -0
- package/docs/adapters/browser/yuanbao.md +64 -0
- package/docs/adapters/index.md +14 -5
- package/docs/comparison.md +1 -1
- package/docs/developer/ai-workflow.md +2 -2
- package/docs/developer/contributing.md +1 -1
- package/docs/developer/testing.md +2 -0
- package/docs/guide/plugins.md +1 -0
- package/docs/guide/troubleshooting.md +11 -0
- package/docs/superpowers/specs/2026-04-03-v2ex-autoresearch-design.md +41 -0
- package/docs/zh/guide/plugins.md +1 -0
- package/extension/dist/background.js +1127 -0
- package/extension/src/background.test.ts +39 -0
- package/extension/src/background.ts +223 -34
- package/extension/src/cdp.ts +194 -4
- package/extension/src/protocol.ts +22 -1
- package/package.json +3 -2
- package/scripts/postinstall.js +1 -1
- package/skills/opencli-explorer/SKILL.md +1 -1
- package/skills/opencli-oneshot/SKILL.md +2 -2
- package/skills/opencli-operate/SKILL.md +120 -27
- package/skills/opencli-usage/SKILL.md +31 -20
- package/skills/opencli-usage/browser.md +114 -16
- package/skills/opencli-usage/public-api.md +32 -3
- package/skills/smart-search/SKILL.md +156 -0
- package/skills/smart-search/references/sources-ai.md +74 -0
- package/skills/smart-search/references/sources-info.md +43 -0
- package/skills/smart-search/references/sources-media.md +50 -0
- package/skills/smart-search/references/sources-other.md +42 -0
- package/skills/smart-search/references/sources-shopping.md +31 -0
- package/skills/smart-search/references/sources-social.md +51 -0
- package/skills/smart-search/references/sources-tech.md +42 -0
- package/skills/smart-search/references/sources-travel.md +20 -0
- package/src/browser/base-page.ts +41 -6
- package/src/browser/bridge.ts +11 -8
- package/src/browser/cdp.ts +1 -8
- package/src/browser/daemon-client.ts +11 -1
- package/src/browser/dom-helpers.ts +43 -31
- package/src/browser/dom-snapshot.ts +23 -1
- package/src/browser/page.ts +115 -31
- package/src/browser.test.ts +1 -1
- package/src/build-manifest.ts +2 -0
- package/src/cli.test.ts +133 -0
- package/src/cli.ts +73 -11
- package/src/clis/1688/item.test.ts +69 -0
- package/src/clis/1688/item.ts +282 -0
- package/src/clis/1688/search.test.ts +81 -0
- package/src/clis/1688/search.ts +402 -0
- package/src/clis/1688/shared.test.ts +75 -0
- package/src/clis/1688/shared.ts +623 -0
- package/src/clis/1688/store.test.ts +69 -0
- package/src/clis/1688/store.ts +300 -0
- package/src/clis/amazon/bestsellers.test.ts +12 -3
- package/src/clis/amazon/bestsellers.ts +6 -178
- package/src/clis/amazon/movers-shakers.ts +8 -0
- package/src/clis/amazon/new-releases.ts +8 -0
- package/src/clis/amazon/rankings.test.ts +47 -0
- package/src/clis/amazon/rankings.ts +312 -0
- package/src/clis/amazon/shared.test.ts +16 -0
- package/src/clis/amazon/shared.ts +134 -12
- package/src/clis/bilibili/comments.test.ts +4 -3
- package/src/clis/bilibili/comments.ts +2 -2
- package/src/clis/bilibili/download.ts +2 -1
- package/src/clis/bilibili/subtitle.test.ts +2 -1
- package/src/clis/bilibili/subtitle.ts +4 -3
- package/src/clis/bilibili/utils.test.ts +21 -0
- package/src/clis/bilibili/utils.ts +27 -0
- package/src/clis/douban/marks.ts +1 -1
- package/src/clis/douban/subject.yaml +50 -19
- package/src/clis/doubao/utils.ts +32 -12
- package/src/clis/douyin/_shared/browser-fetch.test.ts +0 -1
- package/src/clis/douyin/_shared/transcode.test.ts +0 -2
- package/src/clis/douyin/draft.test.ts +0 -2
- package/src/clis/facebook/search.test.ts +0 -2
- package/src/clis/gemini/ask.test.ts +116 -0
- package/src/clis/gemini/ask.ts +10 -3
- package/src/clis/gemini/reply-state.test.ts +708 -0
- package/src/clis/gemini/utils.test.ts +184 -2
- package/src/clis/gemini/utils.ts +588 -60
- package/src/clis/hupu/detail.ts +126 -0
- package/src/clis/hupu/hot.yaml +43 -0
- package/src/clis/hupu/like.ts +76 -0
- package/src/clis/hupu/reply.ts +76 -0
- package/src/clis/hupu/search.ts +95 -0
- package/src/clis/hupu/unlike.ts +76 -0
- package/src/clis/hupu/utils.ts +381 -0
- package/src/clis/instagram/_shared/private-publish.test.ts +827 -0
- package/src/clis/instagram/_shared/private-publish.ts +1303 -0
- package/src/clis/instagram/_shared/protocol-capture.test.ts +148 -0
- package/src/clis/instagram/_shared/protocol-capture.ts +321 -0
- package/src/clis/instagram/_shared/runtime-info.ts +91 -0
- package/src/clis/instagram/note.test.ts +96 -0
- package/src/clis/instagram/note.ts +254 -0
- package/src/clis/instagram/post.test.ts +1716 -0
- package/src/clis/instagram/post.ts +1620 -0
- package/src/clis/instagram/reel.test.ts +191 -0
- package/src/clis/instagram/reel.ts +886 -0
- package/src/clis/instagram/story.test.ts +191 -0
- package/src/clis/instagram/story.ts +151 -0
- package/src/clis/sinafinance/stock-rank.ts +68 -0
- package/src/clis/substack/utils.test.ts +0 -2
- package/src/clis/twitter/post.test.ts +157 -0
- package/src/clis/twitter/post.ts +82 -48
- package/src/clis/twitter/reply.test.ts +177 -0
- package/src/clis/twitter/reply.ts +285 -39
- package/src/clis/twitter/search.test.ts +88 -5
- package/src/clis/twitter/search.ts +68 -5
- package/src/clis/xianyu/chat.test.ts +20 -0
- package/src/clis/xianyu/chat.ts +175 -0
- package/src/clis/xianyu/item.test.ts +67 -0
- package/src/clis/xianyu/item.ts +172 -0
- package/src/clis/xianyu/search.test.ts +22 -0
- package/src/clis/xianyu/search.ts +151 -0
- package/src/clis/xianyu/utils.ts +9 -0
- package/src/clis/xiaoe/catalog.yaml +129 -0
- package/src/clis/xiaoe/content.yaml +43 -0
- package/src/clis/xiaoe/courses.yaml +73 -0
- package/src/clis/xiaoe/detail.yaml +39 -0
- package/src/clis/xiaoe/play-url.yaml +124 -0
- package/src/clis/xiaohongshu/comments.test.ts +0 -2
- package/src/clis/xiaohongshu/creator-note-detail.test.ts +0 -2
- package/src/clis/xiaohongshu/creator-notes.test.ts +0 -2
- package/src/clis/xiaohongshu/download.test.ts +0 -2
- package/src/clis/xiaohongshu/note.test.ts +0 -2
- package/src/clis/xiaohongshu/publish.test.ts +0 -2
- package/src/clis/xiaohongshu/search.test.ts +59 -48
- package/src/clis/xiaohongshu/search.ts +31 -21
- package/src/clis/yuanbao/ask.test.ts +156 -0
- package/src/clis/yuanbao/ask.ts +522 -0
- package/src/clis/yuanbao/new.test.ts +36 -0
- package/src/clis/yuanbao/new.ts +81 -0
- package/src/clis/yuanbao/shared.ts +57 -0
- package/src/clis/zhihu/question.test.ts +42 -17
- package/src/clis/zhihu/question.ts +31 -26
- package/src/commanderAdapter.test.ts +51 -0
- package/src/commanderAdapter.ts +8 -4
- package/src/completion.test.ts +30 -0
- package/src/completion.ts +3 -1
- package/src/doctor.ts +1 -1
- package/src/electron-apps.ts +9 -1
- package/src/errors.ts +1 -1
- package/src/execution.ts +26 -30
- package/src/explore.ts +1 -1
- package/src/launcher.test.ts +121 -7
- package/src/launcher.ts +87 -9
- package/src/output.test.ts +50 -90
- package/src/output.ts +10 -1
- package/src/pipeline/executor.test.ts +0 -2
- package/src/pipeline/steps/download.test.ts +0 -2
- package/src/registry.ts +2 -0
- package/src/serialization.ts +2 -0
- package/src/types.ts +9 -2
- package/tests/e2e/browser-auth.test.ts +9 -0
- package/CLI-EXPLORER.md +0 -724
- package/CLI-ONESHOT.md +0 -216
- package/SKILL.md +0 -59
|
@@ -0,0 +1,1647 @@
|
|
|
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 '../../errors.js';
|
|
6
|
+
import { getRegistry } from '../../registry.js';
|
|
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(), 'opencli-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(), 'opencli-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.OPENCLI_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?.timeoutSeconds).toBe(300);
|
|
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('__opencli_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-opencli-ig-upload-index'))
|
|
418
|
+
return { ok: true, selectors: ['[data-opencli-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-opencli-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-opencli-ig-upload-index'))
|
|
458
|
+
return { ok: true, selectors: ['[data-opencli-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-opencli-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-opencli-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 opencli',
|
|
561
|
+
});
|
|
562
|
+
expect(page.goto).toHaveBeenCalledWith('https://www.instagram.com/');
|
|
563
|
+
expect(page.setFileInput).toHaveBeenCalledWith([imagePath], '[data-opencli-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-opencli-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-opencli-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 OPENCLI_INSTAGRAM_CAPTURE is enabled', async () => {
|
|
604
|
+
process.env.OPENCLI_INSTAGRAM_CAPTURE = '1';
|
|
605
|
+
const imagePath = createTempImage('capture-enabled.jpg');
|
|
606
|
+
const evaluate = vi.fn(async (js) => {
|
|
607
|
+
if (js.includes('__opencli_ig_protocol_capture') && js.includes('PATCH_GUARD'))
|
|
608
|
+
return { ok: true };
|
|
609
|
+
if (js.includes('const data = Array.isArray(window[') && js.includes('__opencli_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-opencli-ig-upload-index'))
|
|
617
|
+
return { ok: true, selectors: ['[data-opencli-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('__opencli_ig_protocol_capture') && js.includes('PATCH_GUARD'))).toBe(true);
|
|
638
|
+
expect(evaluateCalls.some((js) => js.includes('const data = Array.isArray(window[') && js.includes('__opencli_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.OPENCLI_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-opencli-ig-upload-index'))
|
|
658
|
+
return { ok: true, selectors: ['[data-opencli-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-opencli-ig-upload-index'))
|
|
708
|
+
return { ok: true, selectors: ['[data-opencli-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-opencli-ig-upload-index'))
|
|
762
|
+
return { ok: true, selectors: ['[data-opencli-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-opencli-ig-upload-index'))
|
|
800
|
+
return { ok: true, selectors: ['[data-opencli-ig-upload-index="0"]'] };
|
|
801
|
+
if (js.includes('__opencliInstagramUpload_') && 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-opencli-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-opencli-ig-upload-index'))
|
|
849
|
+
return { ok: true, selectors: ['[data-opencli-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 opencli',
|
|
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-opencli-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-opencli-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-opencli-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-opencli-ig-upload-index'))
|
|
994
|
+
return { ok: true, selectors: ['[data-opencli-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-opencli-ig-upload-index'))
|
|
1041
|
+
return { ok: true, selectors: ['[data-opencli-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-opencli-ig-upload-index'))
|
|
1103
|
+
return { ok: true, selectors: ['[data-opencli-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-opencli-ig-upload-index'))
|
|
1153
|
+
return { ok: true, selectors: ['[data-opencli-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('__opencli_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('__opencli_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-opencli-ig-upload-index'))
|
|
1212
|
+
return { ok: true, selectors: ['[data-opencli-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-opencli-ig-upload-index="0"]'))
|
|
1257
|
+
.mockResolvedValueOnce(undefined);
|
|
1258
|
+
const page = createPageMock(withInitialDialogDismiss([
|
|
1259
|
+
{ ok: true },
|
|
1260
|
+
{ ok: true, selectors: ['[data-opencli-ig-upload-index="0"]'] },
|
|
1261
|
+
{ ok: true, selectors: ['[data-opencli-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-opencli-ig-upload-index="0"]'] },
|
|
1294
|
+
{ ok: true, selectors: ['[data-opencli-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-opencli-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-opencli-ig-upload-index'))
|
|
1361
|
+
return { ok: true, selectors: ['[data-opencli-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-opencli-ig-upload-index="0"]', '[data-opencli-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-opencli-ig-upload-index="0"]');
|
|
1430
|
+
expect(setFileInput).toHaveBeenNthCalledWith(2, [imagePath], '[data-opencli-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-opencli-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-opencli-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-opencli-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('__opencli_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-opencli-ig-upload-index'))
|
|
1528
|
+
return { ok: true, selectors: ['[data-opencli-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-opencli-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-opencli-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
|
+
});
|