@jackwener/opencli 1.7.8 → 1.7.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. package/README.md +49 -14
  2. package/README.zh-CN.md +30 -10
  3. package/cli-manifest.json +646 -30
  4. package/clis/36kr/news.js +1 -1
  5. package/clis/apple-podcasts/commands.test.js +4 -4
  6. package/clis/apple-podcasts/episodes.js +1 -1
  7. package/clis/apple-podcasts/search.js +1 -1
  8. package/clis/apple-podcasts/top.js +1 -1
  9. package/clis/arxiv/paper.js +1 -1
  10. package/clis/arxiv/search.js +1 -1
  11. package/clis/band/mentions.js +3 -3
  12. package/clis/bbc/news.js +1 -1
  13. package/clis/bilibili/subtitle.js +2 -2
  14. package/clis/bloomberg/businessweek.js +1 -1
  15. package/clis/bloomberg/economics.js +1 -1
  16. package/clis/bloomberg/industries.js +1 -1
  17. package/clis/bloomberg/main.js +1 -1
  18. package/clis/bloomberg/markets.js +1 -1
  19. package/clis/bloomberg/opinions.js +1 -1
  20. package/clis/bloomberg/politics.js +1 -1
  21. package/clis/bloomberg/tech.js +1 -1
  22. package/clis/boss/search.js +49 -8
  23. package/clis/boss/search.test.js +78 -0
  24. package/clis/boss/send.js +3 -3
  25. package/clis/chatgpt/image.js +37 -8
  26. package/clis/chatgpt/image.test.js +92 -0
  27. package/clis/chatgpt/utils.js +39 -6
  28. package/clis/chatgpt/utils.test.js +63 -0
  29. package/clis/chatgpt-app/ask.js +1 -1
  30. package/clis/chatgpt-app/ax.js +4 -2
  31. package/clis/chatgpt-app/ax.test.js +12 -0
  32. package/clis/chatgpt-app/model.js +1 -1
  33. package/clis/chatgpt-app/new.js +1 -1
  34. package/clis/chatgpt-app/read.js +1 -1
  35. package/clis/chatgpt-app/send.js +1 -1
  36. package/clis/chatgpt-app/status.js +1 -1
  37. package/clis/chatwise/ask.js +2 -2
  38. package/clis/chatwise/model.js +2 -2
  39. package/clis/chatwise/send.js +2 -2
  40. package/clis/claude/ask.js +128 -0
  41. package/clis/claude/ask.test.js +338 -0
  42. package/clis/claude/commands.test.js +118 -0
  43. package/clis/claude/detail.js +29 -0
  44. package/clis/claude/history.js +31 -0
  45. package/clis/claude/new.js +21 -0
  46. package/clis/claude/read.js +24 -0
  47. package/clis/claude/send.js +41 -0
  48. package/clis/claude/status.js +24 -0
  49. package/clis/claude/utils.js +440 -0
  50. package/clis/claude/utils.test.js +148 -0
  51. package/clis/codex/ask.js +2 -2
  52. package/clis/codex/send.js +2 -2
  53. package/clis/ctrip/search.js +1 -1
  54. package/clis/ctrip/search.test.js +4 -4
  55. package/clis/cursor/ask.js +2 -2
  56. package/clis/cursor/composer.js +2 -2
  57. package/clis/cursor/send.js +2 -2
  58. package/clis/deepseek/ask.js +17 -4
  59. package/clis/deepseek/ask.test.js +46 -0
  60. package/clis/deepseek/utils.js +55 -16
  61. package/clis/deepseek/utils.test.js +124 -5
  62. package/clis/doubao/utils.js +53 -11
  63. package/clis/doubao/utils.test.js +22 -2
  64. package/clis/eastmoney/announcement.js +1 -1
  65. package/clis/eastmoney/convertible.js +1 -1
  66. package/clis/eastmoney/etf.js +1 -1
  67. package/clis/eastmoney/holders.js +1 -1
  68. package/clis/eastmoney/index-board.js +1 -1
  69. package/clis/eastmoney/kline.js +1 -1
  70. package/clis/eastmoney/kuaixun.js +1 -1
  71. package/clis/eastmoney/longhu.js +1 -1
  72. package/clis/eastmoney/money-flow.js +1 -1
  73. package/clis/eastmoney/northbound.js +1 -1
  74. package/clis/eastmoney/quote.js +1 -1
  75. package/clis/eastmoney/rank.js +1 -1
  76. package/clis/eastmoney/sectors.js +1 -1
  77. package/clis/facebook/marketplace-inbox.js +83 -0
  78. package/clis/facebook/marketplace-listings.js +83 -0
  79. package/clis/facebook/marketplace.test.js +91 -0
  80. package/clis/google/news.js +1 -1
  81. package/clis/google/suggest.js +1 -1
  82. package/clis/google/trends.js +1 -1
  83. package/clis/google-scholar/cite.js +74 -0
  84. package/clis/google-scholar/cite.test.js +47 -0
  85. package/clis/google-scholar/profile.js +92 -0
  86. package/clis/google-scholar/profile.test.js +49 -0
  87. package/clis/google-scholar/search.js +1 -1
  88. package/clis/google-scholar/search.test.js +15 -0
  89. package/clis/hf/top.js +1 -1
  90. package/clis/instagram/collection-create.js +57 -0
  91. package/clis/instagram/saved.js +21 -7
  92. package/clis/jd/item.js +679 -47
  93. package/clis/jd/item.test.js +318 -7
  94. package/clis/jd/item.test.ts +517 -0
  95. package/clis/lesswrong/comments.js +1 -1
  96. package/clis/lesswrong/curated.js +1 -1
  97. package/clis/lesswrong/frontpage.js +1 -1
  98. package/clis/lesswrong/new.js +1 -1
  99. package/clis/lesswrong/read.js +1 -1
  100. package/clis/lesswrong/sequences.js +1 -1
  101. package/clis/lesswrong/shortform.js +1 -1
  102. package/clis/lesswrong/tag.js +1 -1
  103. package/clis/lesswrong/tags.js +1 -1
  104. package/clis/lesswrong/top-month.js +1 -1
  105. package/clis/lesswrong/top-week.js +1 -1
  106. package/clis/lesswrong/top-year.js +1 -1
  107. package/clis/lesswrong/top.js +1 -1
  108. package/clis/lesswrong/user-posts.js +1 -1
  109. package/clis/lesswrong/user.js +1 -1
  110. package/clis/paperreview/commands.test.js +6 -6
  111. package/clis/paperreview/feedback.js +1 -1
  112. package/clis/paperreview/review.js +1 -1
  113. package/clis/paperreview/submit.js +1 -1
  114. package/clis/producthunt/posts.js +1 -1
  115. package/clis/producthunt/today.js +1 -1
  116. package/clis/sinablog/search.js +1 -1
  117. package/clis/sinafinance/news.js +1 -1
  118. package/clis/sinafinance/stock.js +1 -1
  119. package/clis/sinafinance/stock.test.js +2 -2
  120. package/clis/spotify/spotify.js +6 -6
  121. package/clis/substack/search.js +1 -1
  122. package/clis/toutiao/articles.js +5 -6
  123. package/clis/toutiao/articles.test.js +22 -15
  124. package/clis/twitter/followers.js +2 -2
  125. package/clis/twitter/following.js +224 -73
  126. package/clis/twitter/following.test.js +277 -0
  127. package/clis/twitter/post.js +184 -47
  128. package/clis/twitter/post.test.js +114 -34
  129. package/clis/uiverse/_shared.js +63 -4
  130. package/clis/uiverse/_shared.test.js +7 -0
  131. package/clis/uiverse/code.js +1 -0
  132. package/clis/uiverse/navigation.test.js +12 -0
  133. package/clis/uiverse/preview.js +1 -0
  134. package/clis/web/read.js +319 -81
  135. package/clis/web/read.test.js +221 -5
  136. package/clis/weibo/favorites.js +169 -0
  137. package/clis/weibo/favorites.test.js +114 -0
  138. package/clis/weibo/publish.js +282 -0
  139. package/clis/weibo/publish.test.js +183 -0
  140. package/clis/weread/ranking.js +1 -1
  141. package/clis/weread/search-regression.test.js +8 -8
  142. package/clis/weread/search.js +1 -1
  143. package/clis/wikipedia/random.js +1 -1
  144. package/clis/wikipedia/search.js +1 -1
  145. package/clis/wikipedia/summary.js +1 -1
  146. package/clis/wikipedia/trending.js +1 -1
  147. package/clis/xianyu/chat.js +3 -3
  148. package/clis/xianyu/item.js +2 -2
  149. package/clis/xianyu/item.test.js +3 -3
  150. package/clis/xiaohongshu/search.js +17 -2
  151. package/clis/xiaohongshu/search.test.js +37 -1
  152. package/clis/xiaoyuzhou/download.js +1 -1
  153. package/clis/xiaoyuzhou/download.test.js +3 -3
  154. package/clis/xiaoyuzhou/episode.js +1 -1
  155. package/clis/xiaoyuzhou/podcast-episodes.js +1 -1
  156. package/clis/xiaoyuzhou/podcast-episodes.test.js +2 -2
  157. package/clis/xiaoyuzhou/podcast.js +1 -1
  158. package/clis/xiaoyuzhou/transcript.js +1 -1
  159. package/clis/xiaoyuzhou/transcript.test.js +5 -5
  160. package/clis/yollomi/models.js +1 -1
  161. package/clis/youtube/channel.js +24 -1
  162. package/clis/youtube/channel.test.js +59 -0
  163. package/clis/zhihu/answer.js +21 -162
  164. package/clis/zhihu/answer.test.js +26 -53
  165. package/clis/zhihu/collection.js +197 -0
  166. package/clis/zhihu/collection.test.js +290 -0
  167. package/clis/zhihu/collections.js +127 -0
  168. package/clis/zhihu/collections.test.js +182 -0
  169. package/clis/zhihu/comment.js +24 -305
  170. package/clis/zhihu/comment.test.js +31 -35
  171. package/clis/zhihu/favorite.js +44 -182
  172. package/clis/zhihu/favorite.test.js +30 -167
  173. package/clis/zhihu/follow.js +25 -56
  174. package/clis/zhihu/follow.test.js +20 -23
  175. package/clis/zhihu/like.js +22 -67
  176. package/clis/zhihu/like.test.js +19 -42
  177. package/clis/zhihu/search.js +3 -2
  178. package/clis/zhihu/write-shared.js +8 -1
  179. package/clis/zhihu/write-shared.test.js +1 -0
  180. package/clis/zlibrary/commands.test.js +75 -0
  181. package/clis/zlibrary/info.js +47 -0
  182. package/clis/zlibrary/search.js +46 -0
  183. package/clis/zlibrary/utils.js +136 -0
  184. package/dist/src/adapter-source.d.ts +11 -0
  185. package/dist/src/adapter-source.js +24 -0
  186. package/dist/src/adapter-source.test.js +29 -0
  187. package/dist/src/browser/base-page.d.ts +3 -1
  188. package/dist/src/browser/base-page.js +76 -1
  189. package/dist/src/browser/base-page.test.d.ts +1 -0
  190. package/dist/src/browser/base-page.test.js +74 -0
  191. package/dist/src/browser/bridge.d.ts +1 -2
  192. package/dist/src/browser/bridge.js +40 -41
  193. package/dist/src/browser/cdp.d.ts +1 -0
  194. package/dist/src/browser/cdp.js +3 -3
  195. package/dist/src/browser/daemon-client.d.ts +38 -4
  196. package/dist/src/browser/daemon-client.js +24 -7
  197. package/dist/src/browser/daemon-client.test.js +49 -0
  198. package/dist/src/browser/daemon-lifecycle.d.ts +23 -0
  199. package/dist/src/browser/daemon-lifecycle.js +67 -0
  200. package/dist/src/browser/daemon-version.d.ts +4 -0
  201. package/dist/src/browser/daemon-version.js +12 -0
  202. package/dist/src/browser/errors.js +3 -0
  203. package/dist/src/browser/errors.test.js +3 -0
  204. package/dist/src/browser/network-cache.d.ts +1 -0
  205. package/dist/src/browser/page.d.ts +3 -1
  206. package/dist/src/browser/page.js +10 -2
  207. package/dist/src/browser/profile.d.ts +14 -0
  208. package/dist/src/browser/profile.js +85 -0
  209. package/dist/src/build-manifest.d.ts +2 -0
  210. package/dist/src/build-manifest.js +13 -3
  211. package/dist/src/build-manifest.test.js +20 -2
  212. package/dist/src/cli.d.ts +6 -0
  213. package/dist/src/cli.js +477 -35
  214. package/dist/src/cli.test.js +303 -2
  215. package/dist/src/commanderAdapter.js +17 -9
  216. package/dist/src/commanderAdapter.test.js +67 -2
  217. package/dist/src/commands/daemon.d.ts +2 -0
  218. package/dist/src/commands/daemon.js +42 -1
  219. package/dist/src/commands/daemon.test.js +103 -2
  220. package/dist/src/completion-shared.js +1 -2
  221. package/dist/src/completion.test.js +3 -2
  222. package/dist/src/daemon.js +125 -41
  223. package/dist/src/doctor.d.ts +5 -6
  224. package/dist/src/doctor.js +77 -19
  225. package/dist/src/doctor.test.js +117 -0
  226. package/dist/src/engine.test.js +6 -5
  227. package/dist/src/errors.d.ts +14 -8
  228. package/dist/src/errors.js +36 -30
  229. package/dist/src/errors.test.js +5 -5
  230. package/dist/src/execution.d.ts +4 -0
  231. package/dist/src/execution.js +173 -25
  232. package/dist/src/execution.test.js +171 -1
  233. package/dist/src/main.js +10 -0
  234. package/dist/src/observation/artifact.d.ts +16 -0
  235. package/dist/src/observation/artifact.js +260 -0
  236. package/dist/src/observation/artifact.test.d.ts +1 -0
  237. package/dist/src/observation/artifact.test.js +121 -0
  238. package/dist/src/observation/events.d.ts +89 -0
  239. package/dist/src/observation/events.js +1 -0
  240. package/dist/src/observation/index.d.ts +7 -0
  241. package/dist/src/observation/index.js +7 -0
  242. package/dist/src/observation/manager.d.ts +9 -0
  243. package/dist/src/observation/manager.js +27 -0
  244. package/dist/src/observation/manager.test.d.ts +1 -0
  245. package/dist/src/observation/manager.test.js +13 -0
  246. package/dist/src/observation/redaction.d.ts +11 -0
  247. package/dist/src/observation/redaction.js +81 -0
  248. package/dist/src/observation/redaction.test.d.ts +1 -0
  249. package/dist/src/observation/redaction.test.js +32 -0
  250. package/dist/src/observation/retention.d.ts +32 -0
  251. package/dist/src/observation/retention.js +160 -0
  252. package/dist/src/observation/retention.test.d.ts +1 -0
  253. package/dist/src/observation/retention.test.js +118 -0
  254. package/dist/src/observation/ring-buffer.d.ts +22 -0
  255. package/dist/src/observation/ring-buffer.js +45 -0
  256. package/dist/src/observation/ring-buffer.test.d.ts +1 -0
  257. package/dist/src/observation/ring-buffer.test.js +22 -0
  258. package/dist/src/observation/session.d.ts +25 -0
  259. package/dist/src/observation/session.js +50 -0
  260. package/dist/src/pipeline/executor.test.js +1 -0
  261. package/dist/src/pipeline/steps/download.test.js +1 -0
  262. package/dist/src/pipeline/steps/fetch.js +1 -21
  263. package/dist/src/pipeline/steps/fetch.test.js +6 -12
  264. package/dist/src/plugin-scaffold.js +1 -1
  265. package/dist/src/plugin-scaffold.test.js +1 -1
  266. package/dist/src/registry.d.ts +40 -9
  267. package/dist/src/registry.js +3 -1
  268. package/dist/src/runtime-detect.d.ts +10 -0
  269. package/dist/src/runtime-detect.js +19 -0
  270. package/dist/src/runtime-detect.test.js +12 -1
  271. package/dist/src/runtime.d.ts +2 -0
  272. package/dist/src/runtime.js +1 -0
  273. package/dist/src/types.d.ts +22 -0
  274. package/dist/src/update-check.d.ts +31 -1
  275. package/dist/src/update-check.js +62 -16
  276. package/dist/src/update-check.test.js +86 -1
  277. package/package.json +1 -1
  278. package/dist/src/diagnostic.d.ts +0 -63
  279. package/dist/src/diagnostic.js +0 -292
  280. package/dist/src/diagnostic.test.js +0 -302
  281. /package/dist/src/{diagnostic.test.d.ts → adapter-source.test.d.ts} +0 -0
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, it, vi } from 'vitest';
2
2
  import { getRegistry } from '@jackwener/opencli/registry';
3
3
  import './post.js';
4
+
4
5
  vi.mock('node:fs', async (importOriginal) => {
5
6
  const actual = await importOriginal();
6
7
  return {
@@ -12,6 +13,7 @@ vi.mock('node:fs', async (importOriginal) => {
12
13
  }),
13
14
  };
14
15
  });
16
+
15
17
  vi.mock('node:path', async (importOriginal) => {
16
18
  const actual = await importOriginal();
17
19
  return {
@@ -23,92 +25,170 @@ vi.mock('node:path', async (importOriginal) => {
23
25
  }),
24
26
  };
25
27
  });
26
- function makePage(overrides = {}) {
28
+
29
+ function makePage(evaluateResults = [], overrides = {}) {
30
+ const evaluate = vi.fn();
31
+ for (const result of evaluateResults) {
32
+ evaluate.mockResolvedValueOnce(result);
33
+ }
34
+ evaluate.mockResolvedValue({ ok: true });
35
+
27
36
  return {
28
37
  goto: vi.fn().mockResolvedValue(undefined),
29
38
  wait: vi.fn().mockResolvedValue(undefined),
30
- evaluate: vi.fn().mockResolvedValue({ ok: true }),
39
+ evaluate,
31
40
  setFileInput: vi.fn().mockResolvedValue(undefined),
41
+ insertText: vi.fn().mockResolvedValue(undefined),
32
42
  ...overrides,
33
43
  };
34
44
  }
45
+
35
46
  describe('twitter post command', () => {
36
47
  const getCommand = () => getRegistry().get('twitter/post');
37
- it('posts text-only tweet successfully', async () => {
48
+
49
+ it('posts text-only tweet successfully through the current compose route', async () => {
38
50
  const command = getCommand();
39
- const page = makePage({
40
- evaluate: vi.fn()
41
- .mockResolvedValueOnce({ ok: true })
42
- .mockResolvedValueOnce({ ok: true, message: 'Tweet posted successfully.' }),
43
- });
51
+ const page = makePage([
52
+ { ok: true }, // focus composer
53
+ { ok: true }, // verify native insertText
54
+ { ok: true }, // click post
55
+ { ok: true, message: 'Tweet posted successfully.' }, // verify submit completed
56
+ ]);
57
+
44
58
  const result = await command.func(page, { text: 'hello world' });
59
+
45
60
  expect(result).toEqual([{ status: 'success', message: 'Tweet posted successfully.', text: 'hello world' }]);
46
- expect(page.goto).toHaveBeenCalledWith('https://x.com/compose/tweet');
61
+ expect(page.goto).toHaveBeenCalledWith('https://x.com/compose/post', { waitUntil: 'load', settleMs: 2500 });
62
+ expect(page.wait).toHaveBeenNthCalledWith(1, { selector: '[data-testid="tweetTextarea_0"]', timeout: 15 });
63
+ expect(page.insertText).toHaveBeenCalledWith('hello world');
47
64
  });
65
+
48
66
  it('returns failed when text area not found', async () => {
49
67
  const command = getCommand();
50
- const page = makePage({
51
- evaluate: vi.fn()
52
- .mockResolvedValueOnce({ ok: false, message: 'Could not find the tweet composer text area.' }),
53
- });
68
+ const page = makePage([
69
+ { ok: false, message: 'Could not find the tweet composer text area. Are you logged in?' },
70
+ ]);
71
+
54
72
  const result = await command.func(page, { text: 'hello' });
55
- expect(result).toEqual([{ status: 'failed', message: 'Could not find the tweet composer text area.', text: 'hello' }]);
73
+
74
+ expect(result).toEqual([{ status: 'failed', message: 'Could not find the tweet composer text area. Are you logged in?', text: 'hello' }]);
75
+ expect(page.insertText).not.toHaveBeenCalled();
56
76
  });
77
+
57
78
  it('throws when more than 4 images', async () => {
58
79
  const command = getCommand();
59
80
  const page = makePage();
60
81
  await expect(command.func(page, { text: 'hi', images: 'a.png,b.png,c.png,d.png,e.png' })).rejects.toThrow('Too many images: 5 (max 4)');
61
82
  });
83
+
62
84
  it('throws when image file does not exist', async () => {
63
85
  const command = getCommand();
64
86
  const page = makePage();
65
87
  await expect(command.func(page, { text: 'hi', images: 'missing.png' })).rejects.toThrow('Not a valid file');
66
88
  });
89
+
67
90
  it('throws on unsupported image format', async () => {
68
91
  const command = getCommand();
69
92
  const page = makePage();
70
93
  await expect(command.func(page, { text: 'hi', images: 'photo.bmp' })).rejects.toThrow('Unsupported image format');
71
94
  });
95
+
72
96
  it('throws when page.setFileInput is not available', async () => {
73
97
  const command = getCommand();
74
- const page = makePage({
75
- evaluate: vi.fn().mockResolvedValueOnce({ ok: true }),
76
- setFileInput: undefined,
77
- });
98
+ const page = makePage([], { setFileInput: undefined });
78
99
  await expect(command.func(page, { text: 'hi', images: 'a.png' })).rejects.toThrow('Browser extension does not support file upload');
79
100
  });
80
- it('posts with images when upload completes', async () => {
101
+
102
+ it('uploads images before inserting text so media re-renders cannot erase the tweet text', async () => {
81
103
  const command = getCommand();
82
- const page = makePage({
83
- evaluate: vi.fn()
84
- .mockResolvedValueOnce({ ok: true }) // type text
85
- .mockResolvedValueOnce(true) // upload polling returns true
86
- .mockResolvedValueOnce({ ok: true, message: 'Tweet posted successfully.' }), // click post
87
- });
104
+ const page = makePage([
105
+ { ok: true, previewCount: 2 }, // upload polling returns true
106
+ { ok: true }, // focus composer
107
+ { ok: true }, // verify native insertText
108
+ { ok: true }, // click post
109
+ { ok: true, message: 'Tweet posted successfully.' }, // verify submit completed
110
+ ]);
111
+
88
112
  const result = await command.func(page, { text: 'with images', images: 'a.png,b.png' });
113
+
89
114
  expect(result).toEqual([{ status: 'success', message: 'Tweet posted successfully.', text: 'with images' }]);
90
- expect(page.setFileInput).toHaveBeenCalled();
91
- const uploadScript = page.evaluate.mock.calls[1][0];
115
+ expect(page.wait).toHaveBeenNthCalledWith(2, { selector: 'input[type="file"][data-testid="fileInput"]', timeout: 20 });
116
+ expect(page.setFileInput).toHaveBeenCalledWith(['/abs/a.png', '/abs/b.png'], 'input[type="file"][data-testid="fileInput"]');
117
+ expect(page.insertText).toHaveBeenCalledWith('with images');
118
+ expect(page.setFileInput.mock.invocationCallOrder[0]).toBeLessThan(page.insertText.mock.invocationCallOrder[0]);
119
+
120
+ const uploadScript = page.evaluate.mock.calls[0][0];
92
121
  expect(uploadScript).toContain('[data-testid="attachments"]');
93
- expect(uploadScript).toContain('[role="group"]');
122
+ expect(uploadScript).toContain('buttonReady');
94
123
  });
124
+
125
+ it('prefers nativeType when available because bridge insert-text can miss Draft.js after media upload', async () => {
126
+ const command = getCommand();
127
+ const nativeType = vi.fn().mockResolvedValue(undefined);
128
+ const page = makePage([
129
+ { ok: true }, // focus composer
130
+ { ok: true }, // verify nativeType
131
+ { ok: true }, // click post
132
+ { ok: true, message: 'Tweet posted successfully.' },
133
+ ], { nativeType });
134
+
135
+ const result = await command.func(page, { text: 'native type' });
136
+
137
+ expect(result).toEqual([{ status: 'success', message: 'Tweet posted successfully.', text: 'native type' }]);
138
+ expect(nativeType).toHaveBeenCalledWith('native type');
139
+ expect(page.insertText).not.toHaveBeenCalled();
140
+ });
141
+
142
+ it('treats X success toast as completed even if stale composer nodes remain', async () => {
143
+ const command = getCommand();
144
+ const page = makePage([
145
+ { ok: true }, // focus composer
146
+ { ok: true }, // verify native insertText
147
+ { ok: true }, // click post
148
+ { ok: true, message: 'Tweet posted successfully.' }, // verify submit completed
149
+ ]);
150
+
151
+ await command.func(page, { text: 'toast success' });
152
+
153
+ const submitScript = page.evaluate.mock.calls[3][0];
154
+ expect(submitScript).toContain('successToast');
155
+ expect(submitScript).toContain('your post was sent');
156
+ });
157
+
95
158
  it('returns failed when image upload times out', async () => {
96
159
  const command = getCommand();
97
- const page = makePage({
98
- evaluate: vi.fn()
99
- .mockResolvedValueOnce({ ok: true })
100
- .mockResolvedValueOnce(false),
101
- });
160
+ const page = makePage([
161
+ { ok: false, message: 'Image upload timed out (30s).' },
162
+ ]);
163
+
102
164
  const result = await command.func(page, { text: 'timeout', images: 'a.png' });
165
+
103
166
  expect(result).toEqual([{ status: 'failed', message: 'Image upload timed out (30s).', text: 'timeout' }]);
167
+ expect(page.insertText).not.toHaveBeenCalled();
168
+ });
169
+
170
+ it('falls back to DOM insertion when native insertText is unavailable', async () => {
171
+ const command = getCommand();
172
+ const page = makePage([
173
+ { ok: true }, // focus composer
174
+ { ok: true }, // fallback DOM insertion
175
+ { ok: true }, // click post
176
+ { ok: true, message: 'Tweet posted successfully.' },
177
+ ], { insertText: undefined });
178
+
179
+ const result = await command.func(page, { text: 'fallback text' });
180
+
181
+ expect(result).toEqual([{ status: 'success', message: 'Tweet posted successfully.', text: 'fallback text' }]);
182
+ expect(page.evaluate.mock.calls[1][0]).toContain("execCommand('insertText'");
104
183
  });
184
+
105
185
  it('validates images before navigating to compose page', async () => {
106
186
  const command = getCommand();
107
187
  const page = makePage();
108
188
  await expect(command.func(page, { text: 'hi', images: 'missing.png' })).rejects.toThrow('Not a valid file');
109
- // Should NOT have navigated since validation happens first
110
189
  expect(page.goto).not.toHaveBeenCalled();
111
190
  });
191
+
112
192
  it('throws when no browser session', async () => {
113
193
  const command = getCommand();
114
194
  await expect(command.func(null, { text: 'hi' })).rejects.toThrow('Browser session required for twitter post');
@@ -273,13 +273,52 @@ export function parseHtmlRootSignature(html) {
273
273
  };
274
274
  }
275
275
 
276
+ export function getPreviewFallbackTags(signature) {
277
+ const tag = String(signature?.tag || '').toLowerCase();
278
+ if (!tag) return ['label', 'button', 'a', 'div'];
279
+ if (tag === 'input') return ['input', 'label', 'button', 'a', 'div'];
280
+ return [tag];
281
+ }
282
+
276
283
  export async function locatePreviewElement(page, html) {
277
284
  const signature = parseHtmlRootSignature(html);
278
285
  const raw = await page.evaluate(`(async () => {
279
286
  const sig = ${JSON.stringify(signature)};
280
287
  const viewportWidth = window.innerWidth;
281
288
  const viewportHeight = window.innerHeight;
282
- const fallbackTags = sig.tag ? [sig.tag] : ['label', 'button', 'a', 'div'];
289
+ const fallbackTags = ${JSON.stringify(getPreviewFallbackTags(signature))};
290
+
291
+ const getSearchRoots = () => {
292
+ const roots = [document];
293
+ const seen = new Set([document]);
294
+ for (let index = 0; index < roots.length; index += 1) {
295
+ const root = roots[index];
296
+ const nodes = root.querySelectorAll ? Array.from(root.querySelectorAll('*')) : [];
297
+ for (const node of nodes) {
298
+ const shadowRoot = node.shadowRoot;
299
+ if (shadowRoot && !seen.has(shadowRoot)) {
300
+ seen.add(shadowRoot);
301
+ roots.push(shadowRoot);
302
+ }
303
+ }
304
+ }
305
+ return roots;
306
+ };
307
+
308
+ const queryAcrossRoots = (selector, limit = 200) => {
309
+ const results = [];
310
+ const seen = new Set();
311
+ for (const root of getSearchRoots()) {
312
+ const matches = root.querySelectorAll ? Array.from(root.querySelectorAll(selector)) : [];
313
+ for (const match of matches) {
314
+ if (seen.has(match)) continue;
315
+ seen.add(match);
316
+ results.push(match);
317
+ if (results.length >= limit) return results;
318
+ }
319
+ }
320
+ return results;
321
+ };
283
322
 
284
323
  const isVisible = (element) => {
285
324
  if (!element || !(element instanceof Element)) return false;
@@ -292,6 +331,7 @@ export async function locatePreviewElement(page, html) {
292
331
 
293
332
  const scoreCandidate = (element, source) => {
294
333
  const rect = element.getBoundingClientRect();
334
+ const style = window.getComputedStyle(element);
295
335
  const centerX = rect.x + rect.width / 2;
296
336
  const centerY = rect.y + rect.height / 2;
297
337
  const area = rect.width * rect.height;
@@ -299,10 +339,15 @@ export async function locatePreviewElement(page, html) {
299
339
  if (sig.tag && element.tagName.toLowerCase() === sig.tag) score += 30;
300
340
  if (sig.id && element.id === sig.id) score += 120;
301
341
  if (sig.classes.length && sig.classes.every((className) => element.classList.contains(className))) score += 120;
342
+ if (sig.tag === 'input' && source === 'shadow-host') score += 220;
343
+ if (sig.tag === 'input' && element.tagName.toLowerCase() === 'label') score += 80;
344
+ if (element.id && element.id.includes('shadow-root')) score += 80;
302
345
  if (centerX <= viewportWidth * 0.65) score += 40;
303
346
  if (centerY <= viewportHeight * 0.6) score += 40;
304
347
  if (area <= viewportWidth * viewportHeight * 0.2) score += 30;
305
348
  if (area <= viewportWidth * viewportHeight * 0.05) score += 20;
349
+ if (style.position === 'fixed') score -= 180;
350
+ if (rect.height <= 24 && rect.width >= viewportWidth * 0.8) score -= 120;
306
351
  return {
307
352
  source,
308
353
  tag: element.tagName.toLowerCase(),
@@ -327,11 +372,25 @@ export async function locatePreviewElement(page, html) {
327
372
  candidates.push(scoreCandidate(element, source));
328
373
  };
329
374
 
330
- if (sig.id) collect(document.getElementById(sig.id), 'id');
331
- if (sig.classes.length) collect(document.querySelector('.' + sig.classes.join('.')), 'classes');
375
+ if (sig.id) {
376
+ for (const element of queryAcrossRoots('#' + CSS.escape(sig.id), 20)) {
377
+ collect(element, 'id');
378
+ }
379
+ }
380
+ if (sig.classes.length) {
381
+ const classSelector = '.' + sig.classes.map((className) => CSS.escape(className)).join('.');
382
+ for (const element of queryAcrossRoots(classSelector, 20)) {
383
+ collect(element, 'classes');
384
+ }
385
+ }
386
+ if (sig.tag === 'input') {
387
+ for (const element of queryAcrossRoots('#shadow-root-div-ready,[id*="shadow-root"]', 20)) {
388
+ collect(element, 'shadow-host');
389
+ }
390
+ }
332
391
 
333
392
  for (const tagName of fallbackTags) {
334
- const tagNodes = Array.from(document.querySelectorAll(tagName));
393
+ const tagNodes = queryAcrossRoots(tagName, 200);
335
394
  for (const node of tagNodes.slice(0, 200)) {
336
395
  collect(node, 'tag:' + tagName);
337
396
  }
@@ -3,6 +3,7 @@ import {
3
3
  UIVERSE_BASE_URL,
4
4
  parseComponentInput,
5
5
  parseHtmlRootSignature,
6
+ getPreviewFallbackTags,
6
7
  inferLanguage,
7
8
  getCodeLength,
8
9
  } from './_shared.js';
@@ -39,6 +40,12 @@ describe('uiverse shared helpers', () => {
39
40
  expect(parseHtmlRootSignature('')).toEqual({ tag: null, id: null, classes: [] });
40
41
  });
41
42
 
43
+ it('uses broader preview fallback tags for input roots', () => {
44
+ expect(getPreviewFallbackTags({ tag: 'input' })).toEqual(['input', 'label', 'button', 'a', 'div']);
45
+ expect(getPreviewFallbackTags({ tag: 'label' })).toEqual(['label']);
46
+ expect(getPreviewFallbackTags({ tag: null })).toEqual(['label', 'button', 'a', 'div']);
47
+ });
48
+
42
49
  it('infers the language from target and metadata', () => {
43
50
  expect(inferLanguage('react', {})).toBe('tsx');
44
51
  expect(inferLanguage('vue', {})).toBe('vue');
@@ -14,6 +14,7 @@ cli({
14
14
  domain: 'uiverse.io',
15
15
  strategy: Strategy.PUBLIC,
16
16
  browser: true,
17
+ navigateBefore: 'https://uiverse.io',
17
18
  args: [
18
19
  { name: 'input', type: 'str', required: true, positional: true, help: 'Uiverse URL or author/slug identifier' },
19
20
  { name: 'target', type: 'str', required: true, choices: ['html', 'css', 'react', 'vue'], help: 'Code target to export' },
@@ -0,0 +1,12 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { getRegistry } from '@jackwener/opencli/registry';
3
+ import './code.js';
4
+ import './preview.js';
5
+
6
+ describe('uiverse navigateBefore hardening', () => {
7
+ it.each(['uiverse/code', 'uiverse/preview'])('%s starts from uiverse home instead of about:blank', (name) => {
8
+ const cmd = getRegistry().get(name);
9
+ expect(cmd).toBeDefined();
10
+ expect(cmd.navigateBefore).toBe('https://uiverse.io');
11
+ });
12
+ });
@@ -14,6 +14,7 @@ cli({
14
14
  domain: 'uiverse.io',
15
15
  strategy: Strategy.PUBLIC,
16
16
  browser: true,
17
+ navigateBefore: 'https://uiverse.io',
17
18
  args: [
18
19
  { name: 'input', type: 'str', required: true, positional: true, help: 'Uiverse URL or author/slug identifier' },
19
20
  { name: 'output', type: 'str', required: false, help: 'Output image path (defaults to a temp file)' },