@jackwener/opencli 1.7.18 → 1.7.19

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 (95) hide show
  1. package/README.md +7 -8
  2. package/README.zh-CN.md +7 -8
  3. package/cli-manifest.json +305 -9
  4. package/clis/ctrip/ctrip.test.js +486 -1
  5. package/clis/ctrip/flight.js +136 -0
  6. package/clis/ctrip/hotel-search.js +132 -0
  7. package/clis/ctrip/utils.js +298 -0
  8. package/clis/google/search.js +16 -6
  9. package/clis/google-scholar/search.js +20 -5
  10. package/clis/google-scholar/search.test.js +35 -2
  11. package/clis/reddit/home.js +117 -0
  12. package/clis/reddit/home.test.js +127 -0
  13. package/clis/reddit/read.js +400 -54
  14. package/clis/reddit/read.test.js +315 -12
  15. package/clis/reddit/subreddit-info.js +117 -0
  16. package/clis/reddit/subreddit-info.test.js +163 -0
  17. package/clis/reddit/whoami.js +84 -0
  18. package/clis/reddit/whoami.test.js +105 -0
  19. package/clis/rednote/search.js +6 -2
  20. package/clis/twitter/bookmark-folder.js +3 -1
  21. package/clis/twitter/bookmarks.js +3 -1
  22. package/clis/twitter/followers.js +20 -5
  23. package/clis/twitter/followers.test.js +44 -0
  24. package/clis/twitter/following.js +36 -20
  25. package/clis/twitter/following.test.js +60 -8
  26. package/clis/twitter/likes.js +28 -13
  27. package/clis/twitter/likes.test.js +111 -1
  28. package/clis/twitter/list-add.js +128 -204
  29. package/clis/twitter/list-add.test.js +97 -1
  30. package/clis/twitter/list-tweets.js +13 -4
  31. package/clis/twitter/list-tweets.test.js +48 -0
  32. package/clis/twitter/lists.js +5 -2
  33. package/clis/twitter/post.js +23 -4
  34. package/clis/twitter/post.test.js +30 -0
  35. package/clis/twitter/profile.js +16 -8
  36. package/clis/twitter/profile.test.js +39 -0
  37. package/clis/twitter/reply.js +133 -10
  38. package/clis/twitter/reply.test.js +55 -0
  39. package/clis/twitter/search.js +188 -170
  40. package/clis/twitter/search.test.js +96 -258
  41. package/clis/twitter/shared.js +167 -16
  42. package/clis/twitter/shared.test.js +102 -1
  43. package/clis/twitter/timeline.js +3 -1
  44. package/clis/twitter/tweets.js +147 -51
  45. package/clis/twitter/tweets.test.js +238 -1
  46. package/clis/xiaohongshu/comments.js +23 -2
  47. package/clis/xiaohongshu/comments.test.js +63 -1
  48. package/clis/xiaohongshu/search.js +168 -13
  49. package/clis/xiaohongshu/search.test.js +82 -8
  50. package/clis/xueqiu/earnings-date.js +2 -2
  51. package/clis/xueqiu/kline.js +2 -2
  52. package/clis/xueqiu/utils.js +19 -0
  53. package/clis/xueqiu/utils.test.js +26 -0
  54. package/clis/zhihu/answer-detail.js +233 -0
  55. package/clis/zhihu/answer-detail.test.js +330 -0
  56. package/clis/zhihu/question.js +44 -10
  57. package/clis/zhihu/question.test.js +78 -1
  58. package/clis/zhihu/recommend.js +103 -0
  59. package/clis/zhihu/recommend.test.js +143 -0
  60. package/dist/src/browser/base-page.d.ts +3 -2
  61. package/dist/src/browser/base-page.test.js +2 -2
  62. package/dist/src/browser/cdp.js +3 -3
  63. package/dist/src/browser/page.d.ts +3 -2
  64. package/dist/src/browser/page.js +4 -4
  65. package/dist/src/browser/page.test.js +31 -0
  66. package/dist/src/browser/utils.d.ts +10 -0
  67. package/dist/src/browser/utils.js +37 -0
  68. package/dist/src/browser/utils.test.d.ts +1 -0
  69. package/dist/src/browser/utils.test.js +29 -0
  70. package/dist/src/cli-argv-preprocess.d.ts +37 -0
  71. package/dist/src/cli-argv-preprocess.js +131 -0
  72. package/dist/src/cli-argv-preprocess.test.d.ts +1 -0
  73. package/dist/src/cli-argv-preprocess.test.js +130 -0
  74. package/dist/src/cli.js +123 -86
  75. package/dist/src/cli.test.js +33 -28
  76. package/dist/src/commands/daemon.js +6 -7
  77. package/dist/src/doctor.js +15 -16
  78. package/dist/src/download/progress.js +15 -11
  79. package/dist/src/download/progress.test.d.ts +1 -0
  80. package/dist/src/download/progress.test.js +25 -0
  81. package/dist/src/execution.js +1 -3
  82. package/dist/src/execution.test.js +4 -16
  83. package/dist/src/help.d.ts +11 -0
  84. package/dist/src/help.js +46 -5
  85. package/dist/src/logger.js +8 -9
  86. package/dist/src/main.js +16 -0
  87. package/dist/src/output.js +4 -5
  88. package/dist/src/runtime-detect.d.ts +1 -1
  89. package/dist/src/runtime-detect.js +1 -1
  90. package/dist/src/runtime-detect.test.js +3 -2
  91. package/dist/src/tui.d.ts +0 -1
  92. package/dist/src/tui.js +9 -22
  93. package/dist/src/types.d.ts +3 -1
  94. package/dist/src/update-check.js +4 -5
  95. package/package.json +5 -4
@@ -2,71 +2,67 @@ import { describe, expect, it, vi } from 'vitest';
2
2
  import { getRegistry } from '@jackwener/opencli/registry';
3
3
  import { __test__ } from './search.js';
4
4
 
5
- const { buildSearchQuery, resolveSearchFParam, HAS_CHOICES, EXCLUDE_CHOICES, PRODUCT_CHOICES, EXCLUDE_TO_OPERATOR, PRODUCT_TO_F_PARAM, FROM_USER_PATTERN } = __test__;
5
+ const { buildSearchQuery, resolveSearchFParam, resolveSearchProduct, buildSearchTimelineRequest, parseSearchTimeline, HAS_CHOICES, EXCLUDE_CHOICES, PRODUCT_CHOICES, EXCLUDE_TO_OPERATOR, PRODUCT_TO_F_PARAM, FROM_USER_PATTERN } = __test__;
6
6
  describe('twitter search command', () => {
7
- it('retries transient SPA navigation failures before giving up', async () => {
7
+ function makeSearchPage(data) {
8
+ return {
9
+ getCookies: vi.fn().mockResolvedValue([{ name: 'ct0', value: 'csrf' }]),
10
+ goto: vi.fn().mockResolvedValue(undefined),
11
+ evaluate: vi.fn()
12
+ .mockResolvedValueOnce(null) // resolveTwitterQueryId fallback
13
+ .mockResolvedValueOnce(data),
14
+ };
15
+ }
16
+
17
+ it('fetches SearchTimeline directly instead of relying on SPA navigation', async () => {
8
18
  const command = getRegistry().get('twitter/search');
9
19
  expect(command?.func).toBeTypeOf('function');
10
- const evaluate = vi.fn()
11
- .mockResolvedValueOnce(undefined)
12
- .mockResolvedValueOnce('/explore')
13
- .mockResolvedValueOnce(undefined)
14
- .mockResolvedValueOnce('/search');
15
- const page = {
16
- goto: vi.fn().mockResolvedValue(undefined),
17
- wait: vi.fn().mockResolvedValue(undefined),
18
- installInterceptor: vi.fn().mockResolvedValue(undefined),
19
- evaluate,
20
- autoScroll: vi.fn().mockResolvedValue(undefined),
21
- getInterceptedRequests: vi.fn().mockResolvedValue([
22
- {
23
- data: {
24
- search_by_raw_query: {
25
- search_timeline: {
26
- timeline: {
27
- instructions: [
20
+ const page = makeSearchPage({
21
+ data: {
22
+ search_by_raw_query: {
23
+ search_timeline: {
24
+ timeline: {
25
+ instructions: [
26
+ {
27
+ type: 'TimelineAddEntries',
28
+ entries: [
28
29
  {
29
- type: 'TimelineAddEntries',
30
- entries: [
31
- {
32
- entryId: 'tweet-1',
33
- content: {
34
- itemContent: {
35
- tweet_results: {
36
- result: {
37
- rest_id: '1',
38
- legacy: {
39
- full_text: 'hello world',
40
- favorite_count: 7,
41
- created_at: 'Thu Mar 26 10:30:00 +0000 2026',
42
- },
43
- core: {
44
- user_results: {
45
- result: {
46
- core: {
47
- screen_name: 'alice',
48
- },
49
- },
30
+ entryId: 'tweet-1',
31
+ content: {
32
+ itemContent: {
33
+ tweet_results: {
34
+ result: {
35
+ rest_id: '1',
36
+ legacy: {
37
+ full_text: 'hello world',
38
+ favorite_count: 7,
39
+ created_at: 'Thu Mar 26 10:30:00 +0000 2026',
40
+ },
41
+ core: {
42
+ user_results: {
43
+ result: {
44
+ core: {
45
+ screen_name: 'alice',
50
46
  },
51
47
  },
52
- views: {
53
- count: '12',
54
- },
55
48
  },
56
49
  },
50
+ views: {
51
+ count: '12',
52
+ },
57
53
  },
58
54
  },
59
55
  },
60
- ],
56
+ },
61
57
  },
62
58
  ],
63
59
  },
64
- },
60
+ ],
65
61
  },
66
62
  },
67
63
  },
68
- ]),
69
- };
64
+ },
65
+ });
70
66
  const result = await command.func(page, { query: 'from:alice', filter: 'top', limit: 5 });
71
67
  expect(result).toEqual([
72
68
  {
@@ -81,112 +77,62 @@ describe('twitter search command', () => {
81
77
  media_urls: [],
82
78
  },
83
79
  ]);
84
- expect(page.installInterceptor).toHaveBeenCalledWith('SearchTimeline');
85
- expect(evaluate).toHaveBeenCalledTimes(4);
86
- });
87
- it('uses f=live in search URL when filter is live', async () => {
88
- const command = getRegistry().get('twitter/search');
89
- const evaluate = vi.fn()
90
- .mockResolvedValueOnce(undefined)
91
- .mockResolvedValueOnce('/search');
92
- const page = {
93
- goto: vi.fn().mockResolvedValue(undefined),
94
- wait: vi.fn().mockResolvedValue(undefined),
95
- installInterceptor: vi.fn().mockResolvedValue(undefined),
96
- evaluate,
97
- autoScroll: vi.fn().mockResolvedValue(undefined),
98
- getInterceptedRequests: vi.fn().mockResolvedValue([]),
99
- };
100
- await command.func(page, { query: 'breaking news', filter: 'live', limit: 5 });
101
- const pushStateCall = evaluate.mock.calls[0][0];
102
- expect(pushStateCall).toContain('f=live');
103
- expect(pushStateCall).toContain(encodeURIComponent('breaking news'));
80
+ expect(page.getCookies).toHaveBeenCalledWith({ url: 'https://x.com' });
81
+ expect(page.goto).toHaveBeenCalledWith('https://x.com/home', { waitUntil: 'load', settleMs: 1000 });
82
+ const searchFetch = page.evaluate.mock.calls[1][0];
83
+ expect(searchFetch).toContain('/SearchTimeline');
84
+ expect(searchFetch).toContain("method: 'POST'");
85
+ expect(searchFetch).toContain('\\"rawQuery\\":\\"from:alice\\"');
104
86
  });
105
- it('uses f=top in search URL when filter is top', async () => {
106
- const command = getRegistry().get('twitter/search');
107
- const evaluate = vi.fn()
108
- .mockResolvedValueOnce(undefined)
109
- .mockResolvedValueOnce('/search');
110
- const page = {
111
- goto: vi.fn().mockResolvedValue(undefined),
112
- wait: vi.fn().mockResolvedValue(undefined),
113
- installInterceptor: vi.fn().mockResolvedValue(undefined),
114
- evaluate,
115
- autoScroll: vi.fn().mockResolvedValue(undefined),
116
- getInterceptedRequests: vi.fn().mockResolvedValue([]),
117
- };
118
- await command.func(page, { query: 'test', filter: 'top', limit: 5 });
119
- const pushStateCall = evaluate.mock.calls[0][0];
120
- expect(pushStateCall).toContain('f=top');
121
- });
122
- it('falls back to top when filter is omitted', async () => {
87
+
88
+ it('uses the requested GraphQL product', async () => {
123
89
  const command = getRegistry().get('twitter/search');
124
- const evaluate = vi.fn()
125
- .mockResolvedValueOnce(undefined)
126
- .mockResolvedValueOnce('/search');
127
- const page = {
128
- goto: vi.fn().mockResolvedValue(undefined),
129
- wait: vi.fn().mockResolvedValue(undefined),
130
- installInterceptor: vi.fn().mockResolvedValue(undefined),
131
- evaluate,
132
- autoScroll: vi.fn().mockResolvedValue(undefined),
133
- getInterceptedRequests: vi.fn().mockResolvedValue([]),
134
- };
135
- await command.func(page, { query: 'test', limit: 5 });
136
- const pushStateCall = evaluate.mock.calls[0][0];
137
- expect(pushStateCall).toContain('f=top');
90
+ const page = makeSearchPage({ data: { search_by_raw_query: { search_timeline: { timeline: { instructions: [] } } } } });
91
+ await command.func(page, { query: 'cats', product: 'videos', limit: 5 });
92
+ expect(page.evaluate.mock.calls[1][0]).toContain('\\"product\\":\\"Videos\\"');
138
93
  });
139
- it('falls back to search input when pushState fails twice', async () => {
94
+
95
+ it('paginates past the old five-page cap until the requested limit is reached', async () => {
140
96
  const command = getRegistry().get('twitter/search');
141
- expect(command?.func).toBeTypeOf('function');
142
- const evaluate = vi.fn()
143
- .mockResolvedValueOnce(undefined) // pushState attempt 1
144
- .mockResolvedValueOnce('/explore') // pathname check 1 — not /search
145
- .mockResolvedValueOnce(undefined) // pushState attempt 2
146
- .mockResolvedValueOnce('/explore') // pathname check 2 — still not /search
147
- .mockResolvedValueOnce({ ok: true }) // search input fallback succeeds
148
- .mockResolvedValueOnce('/search'); // pathname check after fallback
97
+ let pageIndex = 0;
149
98
  const page = {
99
+ getCookies: vi.fn().mockResolvedValue([{ name: 'ct0', value: 'csrf' }]),
150
100
  goto: vi.fn().mockResolvedValue(undefined),
151
- wait: vi.fn().mockResolvedValue(undefined),
152
- installInterceptor: vi.fn().mockResolvedValue(undefined),
153
- evaluate,
154
- autoScroll: vi.fn().mockResolvedValue(undefined),
155
- getInterceptedRequests: vi.fn().mockResolvedValue([
156
- {
101
+ evaluate: vi.fn().mockImplementation(async () => {
102
+ if (pageIndex === 0) {
103
+ pageIndex += 1;
104
+ return null;
105
+ }
106
+ const id = String(pageIndex);
107
+ pageIndex += 1;
108
+ return {
157
109
  data: {
158
110
  search_by_raw_query: {
159
111
  search_timeline: {
160
112
  timeline: {
161
113
  instructions: [
162
114
  {
163
- type: 'TimelineAddEntries',
164
115
  entries: [
165
116
  {
166
- entryId: 'tweet-99',
167
117
  content: {
168
118
  itemContent: {
169
119
  tweet_results: {
170
120
  result: {
171
- rest_id: '99',
172
- legacy: {
173
- full_text: 'fallback works',
174
- favorite_count: 3,
175
- created_at: 'Wed Apr 02 12:00:00 +0000 2026',
176
- },
177
- core: {
178
- user_results: {
179
- result: {
180
- core: { screen_name: 'bob' },
181
- },
182
- },
183
- },
184
- views: { count: '5' },
121
+ rest_id: id,
122
+ legacy: { full_text: `tweet ${id}`, created_at: 'now' },
123
+ core: { user_results: { result: { core: { screen_name: 'alice' } } } },
185
124
  },
186
125
  },
187
126
  },
188
127
  },
189
128
  },
129
+ {
130
+ content: {
131
+ entryType: 'TimelineTimelineCursor',
132
+ cursorType: 'Bottom',
133
+ value: `cursor-${id}`,
134
+ },
135
+ },
190
136
  ],
191
137
  },
192
138
  ],
@@ -194,100 +140,13 @@ describe('twitter search command', () => {
194
140
  },
195
141
  },
196
142
  },
197
- },
198
- ]),
199
- };
200
- const result = await command.func(page, { query: 'test fallback', filter: 'top', limit: 5 });
201
- expect(result).toEqual([
202
- {
203
- id: '99',
204
- author: 'bob',
205
- text: 'fallback works',
206
- created_at: 'Wed Apr 02 12:00:00 +0000 2026',
207
- likes: 3,
208
- views: '5',
209
- url: 'https://x.com/i/status/99',
210
- has_media: false,
211
- media_urls: [],
212
- },
213
- ]);
214
- // 6 evaluate calls: 2x pushState + 2x pathname check + 1x fallback + 1x pathname check
215
- expect(evaluate).toHaveBeenCalledTimes(6);
216
- expect(page.autoScroll).toHaveBeenCalled();
217
- });
218
- it('clicks the requested product tab after fallback navigation when f= param is absent', async () => {
219
- const command = getRegistry().get('twitter/search');
220
- expect(command?.func).toBeTypeOf('function');
221
- const evaluate = vi.fn()
222
- .mockResolvedValueOnce(undefined) // pushState attempt 1
223
- .mockResolvedValueOnce('/explore')
224
- .mockResolvedValueOnce(undefined) // pushState attempt 2
225
- .mockResolvedValueOnce('/explore')
226
- .mockResolvedValueOnce({ ok: true }) // search input fallback
227
- .mockResolvedValueOnce('/search')
228
- .mockResolvedValueOnce(true); // product tab click
229
- const page = {
230
- goto: vi.fn().mockResolvedValue(undefined),
231
- wait: vi.fn().mockResolvedValue(undefined),
232
- installInterceptor: vi.fn().mockResolvedValue(undefined),
233
- evaluate,
234
- autoScroll: vi.fn().mockResolvedValue(undefined),
235
- getInterceptedRequests: vi.fn().mockResolvedValue([]),
143
+ };
144
+ }),
236
145
  };
237
- const result = await command.func(page, { query: 'cats', product: 'photos', limit: 5 });
238
- expect(result).toEqual([]);
239
- expect(evaluate).toHaveBeenCalledTimes(7);
240
- expect(evaluate.mock.calls[6][0]).toContain('Photos');
241
- expect(page.autoScroll).toHaveBeenCalled();
242
- });
243
- it('throws when fallback navigation cannot select the requested product tab', async () => {
244
- const command = getRegistry().get('twitter/search');
245
- expect(command?.func).toBeTypeOf('function');
246
- const evaluate = vi.fn()
247
- .mockResolvedValueOnce(undefined) // pushState attempt 1
248
- .mockResolvedValueOnce('/explore')
249
- .mockResolvedValueOnce(undefined) // pushState attempt 2
250
- .mockResolvedValueOnce('/explore')
251
- .mockResolvedValueOnce({ ok: true }) // search input fallback
252
- .mockResolvedValueOnce('/search')
253
- .mockResolvedValueOnce(false); // requested tab missing
254
- const page = {
255
- goto: vi.fn().mockResolvedValue(undefined),
256
- wait: vi.fn().mockResolvedValue(undefined),
257
- installInterceptor: vi.fn().mockResolvedValue(undefined),
258
- evaluate,
259
- autoScroll: vi.fn().mockResolvedValue(undefined),
260
- getInterceptedRequests: vi.fn(),
261
- };
262
- await expect(command.func(page, { query: 'cats', product: 'videos', limit: 5 }))
263
- .rejects
264
- .toThrow(/could not select the requested product tab: video/);
265
- expect(page.autoScroll).not.toHaveBeenCalled();
266
- expect(page.getInterceptedRequests).not.toHaveBeenCalled();
267
- });
268
- it('throws with the final path after both attempts fail', async () => {
269
- const command = getRegistry().get('twitter/search');
270
- expect(command?.func).toBeTypeOf('function');
271
- const evaluate = vi.fn()
272
- .mockResolvedValueOnce(undefined) // pushState attempt 1
273
- .mockResolvedValueOnce('/explore') // pathname check 1
274
- .mockResolvedValueOnce(undefined) // pushState attempt 2
275
- .mockResolvedValueOnce('/login') // pathname check 2
276
- .mockResolvedValueOnce({ ok: false }); // search input fallback
277
- const page = {
278
- goto: vi.fn().mockResolvedValue(undefined),
279
- wait: vi.fn().mockResolvedValue(undefined),
280
- installInterceptor: vi.fn().mockResolvedValue(undefined),
281
- evaluate,
282
- autoScroll: vi.fn().mockResolvedValue(undefined),
283
- getInterceptedRequests: vi.fn(),
284
- };
285
- await expect(command.func(page, { query: 'from:alice', filter: 'top', limit: 5 }))
286
- .rejects
287
- .toThrow('Final path: /login');
288
- expect(page.autoScroll).not.toHaveBeenCalled();
289
- expect(page.getInterceptedRequests).not.toHaveBeenCalled();
290
- expect(evaluate).toHaveBeenCalledTimes(5);
146
+ const result = await command.func(page, { query: 'opencli', limit: 7 });
147
+ expect(result).toHaveLength(7);
148
+ expect(result.map((row) => row.id)).toEqual(['1', '2', '3', '4', '5', '6', '7']);
149
+ expect(page.evaluate).toHaveBeenCalledTimes(8);
291
150
  });
292
151
  });
293
152
 
@@ -411,18 +270,15 @@ describe('twitter search filter helpers', () => {
411
270
  });
412
271
 
413
272
  describe('twitter search end-to-end with new filters', () => {
414
- it('encodes the composed query and product=live into the f= URL param', async () => {
273
+ it('encodes the composed query and product=live into the GraphQL request', async () => {
415
274
  const command = getRegistry().get('twitter/search');
416
275
  const evaluate = vi.fn()
417
- .mockResolvedValueOnce(undefined)
418
- .mockResolvedValueOnce('/search');
276
+ .mockResolvedValueOnce(null)
277
+ .mockResolvedValueOnce({ data: { search_by_raw_query: { search_timeline: { timeline: { instructions: [] } } } } });
419
278
  const page = {
279
+ getCookies: vi.fn().mockResolvedValue([{ name: 'ct0', value: 'csrf' }]),
420
280
  goto: vi.fn().mockResolvedValue(undefined),
421
- wait: vi.fn().mockResolvedValue(undefined),
422
- installInterceptor: vi.fn().mockResolvedValue(undefined),
423
281
  evaluate,
424
- autoScroll: vi.fn().mockResolvedValue(undefined),
425
- getInterceptedRequests: vi.fn().mockResolvedValue([]),
426
282
  };
427
283
  await command.func(page, {
428
284
  query: 'breaking news',
@@ -432,37 +288,26 @@ describe('twitter search end-to-end with new filters', () => {
432
288
  product: 'live',
433
289
  limit: 5,
434
290
  });
435
- const pushStateCall = evaluate.mock.calls[0][0];
436
- // f=live wins because --product=live trumps the default --filter
437
- expect(pushStateCall).toContain('f=live');
438
- // composed query should be percent-encoded inside the URL
439
- const encoded = encodeURIComponent('breaking news from:alice filter:images -filter:nativeretweets');
440
- expect(pushStateCall).toContain(encoded);
291
+ const searchFetch = evaluate.mock.calls[1][0];
292
+ expect(searchFetch).toContain('\\"product\\":\\"Latest\\"');
293
+ expect(searchFetch).toContain('\\"rawQuery\\":\\"breaking news from:alice filter:images -filter:nativeretweets\\"');
441
294
  });
442
295
  it('throws ArgumentError when query and all filters are empty', async () => {
443
296
  const command = getRegistry().get('twitter/search');
444
297
  const page = {
445
298
  goto: vi.fn().mockResolvedValue(undefined),
446
- wait: vi.fn().mockResolvedValue(undefined),
447
- installInterceptor: vi.fn().mockResolvedValue(undefined),
448
299
  evaluate: vi.fn(),
449
- autoScroll: vi.fn().mockResolvedValue(undefined),
450
- getInterceptedRequests: vi.fn(),
451
300
  };
452
301
  await expect(command.func(page, { query: ' ', limit: 5 }))
453
302
  .rejects
454
303
  .toThrow(/empty/i);
455
- expect(page.installInterceptor).not.toHaveBeenCalled();
304
+ expect(page.goto).not.toHaveBeenCalled();
456
305
  });
457
306
  it('throws ArgumentError for invalid --from before navigation', async () => {
458
307
  const command = getRegistry().get('twitter/search');
459
308
  const page = {
460
309
  goto: vi.fn(),
461
- wait: vi.fn(),
462
- installInterceptor: vi.fn(),
463
310
  evaluate: vi.fn(),
464
- autoScroll: vi.fn(),
465
- getInterceptedRequests: vi.fn(),
466
311
  };
467
312
  await expect(command.func(page, { query: 'hi', from: 'alice filter:links', limit: 5 }))
468
313
  .rejects
@@ -473,11 +318,7 @@ describe('twitter search end-to-end with new filters', () => {
473
318
  const command = getRegistry().get('twitter/search');
474
319
  const page = {
475
320
  goto: vi.fn(),
476
- wait: vi.fn(),
477
- installInterceptor: vi.fn(),
478
321
  evaluate: vi.fn(),
479
- autoScroll: vi.fn(),
480
- getInterceptedRequests: vi.fn(),
481
322
  };
482
323
  await expect(command.func(page, { query: 'hi', limit: 0 }))
483
324
  .rejects
@@ -487,19 +328,16 @@ describe('twitter search end-to-end with new filters', () => {
487
328
  it('runs with only filters set (empty <query>)', async () => {
488
329
  const command = getRegistry().get('twitter/search');
489
330
  const evaluate = vi.fn()
490
- .mockResolvedValueOnce(undefined)
491
- .mockResolvedValueOnce('/search');
331
+ .mockResolvedValueOnce(null)
332
+ .mockResolvedValueOnce({ data: { search_by_raw_query: { search_timeline: { timeline: { instructions: [] } } } } });
492
333
  const page = {
334
+ getCookies: vi.fn().mockResolvedValue([{ name: 'ct0', value: 'csrf' }]),
493
335
  goto: vi.fn().mockResolvedValue(undefined),
494
- wait: vi.fn().mockResolvedValue(undefined),
495
- installInterceptor: vi.fn().mockResolvedValue(undefined),
496
336
  evaluate,
497
- autoScroll: vi.fn().mockResolvedValue(undefined),
498
- getInterceptedRequests: vi.fn().mockResolvedValue([]),
499
337
  };
500
338
  const result = await command.func(page, { query: '', from: 'alice', limit: 5 });
501
339
  expect(result).toEqual([]);
502
- const pushStateCall = evaluate.mock.calls[0][0];
503
- expect(pushStateCall).toContain(encodeURIComponent('from:alice'));
340
+ const searchFetch = evaluate.mock.calls[1][0];
341
+ expect(searchFetch).toContain('\\"rawQuery\\":\\"from:alice\\"');
504
342
  });
505
343
  });