@jackwener/opencli 1.7.18 → 1.7.20
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/README.md +18 -17
- package/README.zh-CN.md +16 -18
- package/cli-manifest.json +311 -186
- package/clis/ctrip/ctrip.test.js +486 -1
- package/clis/ctrip/flight.js +136 -0
- package/clis/ctrip/hotel-search.js +132 -0
- package/clis/ctrip/utils.js +298 -0
- package/clis/google/search.js +16 -6
- package/clis/google-scholar/search.js +20 -5
- package/clis/google-scholar/search.test.js +35 -2
- package/clis/reddit/home.js +117 -0
- package/clis/reddit/home.test.js +127 -0
- package/clis/reddit/read.js +400 -54
- package/clis/reddit/read.test.js +315 -12
- package/clis/reddit/subreddit-info.js +117 -0
- package/clis/reddit/subreddit-info.test.js +163 -0
- package/clis/reddit/whoami.js +84 -0
- package/clis/reddit/whoami.test.js +105 -0
- package/clis/rednote/search.js +6 -2
- package/clis/twitter/bookmark-folder.js +8 -4
- package/clis/twitter/bookmark-folder.test.js +59 -1
- package/clis/twitter/bookmarks.js +12 -4
- package/clis/twitter/bookmarks.test.js +205 -0
- package/clis/twitter/followers.js +20 -5
- package/clis/twitter/followers.test.js +44 -0
- package/clis/twitter/following.js +36 -20
- package/clis/twitter/following.test.js +60 -8
- package/clis/twitter/likes.js +28 -13
- package/clis/twitter/likes.test.js +111 -1
- package/clis/twitter/list-add.js +128 -204
- package/clis/twitter/list-add.test.js +97 -1
- package/clis/twitter/list-tweets.js +13 -4
- package/clis/twitter/list-tweets.test.js +48 -0
- package/clis/twitter/lists.js +5 -2
- package/clis/twitter/post.js +23 -4
- package/clis/twitter/post.test.js +30 -0
- package/clis/twitter/profile.js +16 -8
- package/clis/twitter/profile.test.js +39 -0
- package/clis/twitter/reply.js +133 -10
- package/clis/twitter/reply.test.js +55 -0
- package/clis/twitter/search.js +188 -170
- package/clis/twitter/search.test.js +96 -258
- package/clis/twitter/shared.js +167 -16
- package/clis/twitter/shared.test.js +102 -1
- package/clis/twitter/timeline.js +3 -1
- package/clis/twitter/tweets.js +147 -51
- package/clis/twitter/tweets.test.js +238 -1
- package/clis/xiaohongshu/comments.js +23 -2
- package/clis/xiaohongshu/comments.test.js +63 -1
- package/clis/xiaohongshu/search.js +168 -13
- package/clis/xiaohongshu/search.test.js +82 -8
- package/clis/xueqiu/earnings-date.js +2 -2
- package/clis/xueqiu/kline.js +2 -2
- package/clis/xueqiu/utils.js +19 -0
- package/clis/xueqiu/utils.test.js +26 -0
- package/clis/zhihu/answer-detail.js +233 -0
- package/clis/zhihu/answer-detail.test.js +330 -0
- package/clis/zhihu/question.js +44 -10
- package/clis/zhihu/question.test.js +78 -1
- package/clis/zhihu/recommend.js +103 -0
- package/clis/zhihu/recommend.test.js +143 -0
- package/dist/src/browser/base-page.d.ts +3 -2
- package/dist/src/browser/base-page.test.js +2 -2
- package/dist/src/browser/cdp.js +3 -3
- package/dist/src/browser/daemon-client.d.ts +1 -0
- package/dist/src/browser/daemon-client.js +3 -0
- package/dist/src/browser/daemon-client.test.js +20 -0
- package/dist/src/browser/page.d.ts +3 -2
- package/dist/src/browser/page.js +4 -4
- package/dist/src/browser/page.test.js +31 -0
- package/dist/src/browser/utils.d.ts +10 -0
- package/dist/src/browser/utils.js +37 -0
- package/dist/src/browser/utils.test.d.ts +1 -0
- package/dist/src/browser/utils.test.js +29 -0
- package/dist/src/cli-argv-preprocess.d.ts +37 -0
- package/dist/src/cli-argv-preprocess.js +131 -0
- package/dist/src/cli-argv-preprocess.test.d.ts +1 -0
- package/dist/src/cli-argv-preprocess.test.js +130 -0
- package/dist/src/cli.js +131 -89
- package/dist/src/cli.test.js +34 -28
- package/dist/src/commands/daemon.js +6 -7
- package/dist/src/daemon-utils.d.ts +18 -0
- package/dist/src/daemon-utils.js +37 -0
- package/dist/src/daemon.d.ts +1 -1
- package/dist/src/daemon.js +44 -13
- package/dist/src/daemon.test.js +42 -1
- package/dist/src/doctor.js +15 -16
- package/dist/src/download/progress.js +15 -11
- package/dist/src/download/progress.test.d.ts +1 -0
- package/dist/src/download/progress.test.js +25 -0
- package/dist/src/electron-apps.js +0 -1
- package/dist/src/electron-apps.test.js +1 -0
- package/dist/src/execution.js +1 -3
- package/dist/src/execution.test.js +4 -16
- package/dist/src/external-clis.yaml +12 -3
- package/dist/src/external.d.ts +4 -0
- package/dist/src/external.js +3 -0
- package/dist/src/external.test.js +24 -1
- package/dist/src/help.d.ts +16 -1
- package/dist/src/help.js +50 -8
- package/dist/src/help.test.js +5 -1
- package/dist/src/logger.js +8 -9
- package/dist/src/main.js +16 -0
- package/dist/src/output.js +4 -5
- package/dist/src/runtime-detect.d.ts +1 -1
- package/dist/src/runtime-detect.js +1 -1
- package/dist/src/runtime-detect.test.js +3 -2
- package/dist/src/tui.d.ts +0 -1
- package/dist/src/tui.js +9 -22
- package/dist/src/types.d.ts +3 -1
- package/dist/src/update-check.js +4 -5
- package/package.json +5 -4
- package/clis/notion/export.js +0 -32
- package/clis/notion/favorites.js +0 -85
- package/clis/notion/new.js +0 -35
- package/clis/notion/read.js +0 -31
- package/clis/notion/search.js +0 -47
- package/clis/notion/sidebar.js +0 -42
- package/clis/notion/status.js +0 -17
- package/clis/notion/write.js +0 -41
|
@@ -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
|
-
|
|
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
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
{
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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.
|
|
85
|
-
expect(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
106
|
-
|
|
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
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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:
|
|
172
|
-
legacy: {
|
|
173
|
-
|
|
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: '
|
|
238
|
-
expect(result).
|
|
239
|
-
expect(
|
|
240
|
-
expect(evaluate
|
|
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
|
|
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(
|
|
418
|
-
.mockResolvedValueOnce(
|
|
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
|
|
436
|
-
|
|
437
|
-
expect(
|
|
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.
|
|
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(
|
|
491
|
-
.mockResolvedValueOnce(
|
|
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
|
|
503
|
-
expect(
|
|
340
|
+
const searchFetch = evaluate.mock.calls[1][0];
|
|
341
|
+
expect(searchFetch).toContain('\\"rawQuery\\":\\"from:alice\\"');
|
|
504
342
|
});
|
|
505
343
|
});
|