tt-help-cli-ycl 1.4.145 → 1.4.147

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tt-help-cli-ycl",
3
- "version": "1.4.145",
3
+ "version": "1.4.147",
4
4
  "description": "TikTok user & video data scraper - extract ttSeller, verified, locationCreated from HTML source",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,292 @@
1
+ /**
2
+ * product-fill CDP 模式 + 空数据防御测试
3
+ *
4
+ * 覆盖:
5
+ * 1. isEmptyProductDetail 全空判定
6
+ * 2. parseProductHtml 空壳页抛可重试错误(防全 null 当成功回写)
7
+ * 3. parseProductHtml 正常 PDP 页面解析
8
+ * 4. parseProductHtml 验证码页抛 captcha 错误
9
+ * 5. parseArgs --cdp 参数解析(HTTP 模式默认不启用)
10
+ *
11
+ * 运行: node scripts/test-product-fill-cdp.mjs
12
+ */
13
+ import assert from "node:assert";
14
+ import {
15
+ isEmptyProductDetail,
16
+ parseProductHtml,
17
+ } from "../src/lib/shop-scraper.mjs";
18
+
19
+ let passed = 0;
20
+ function check(name, fn) {
21
+ try {
22
+ fn();
23
+ passed++;
24
+ console.log(` ✅ ${name}`);
25
+ } catch (e) {
26
+ console.error(` ❌ ${name}\n ${e.message}`);
27
+ process.exitCode = 1;
28
+ }
29
+ }
30
+
31
+ // 最小可解析的 PDP HTML:含 product_model / sold_count / review_ratings 标识
32
+ const VALID_HTML =
33
+ '<html><body><script>{"product_model":{"name":"Test Product","product_id":"123456","seller_id":"789"},"sold_count":"296","review_ratings":{"overall_score":4.7,"review_count":"3303","rating_result":[5,4,3,2,1]}}</script></body></html>';
34
+
35
+ console.log("\n[1] isEmptyProductDetail 全空判定");
36
+ check("全 null → true", () => {
37
+ assert.strictEqual(
38
+ isEmptyProductDetail({
39
+ rating: null,
40
+ reviewCount: null,
41
+ soldCount: null,
42
+ productName: null,
43
+ ratingBreakdown: null,
44
+ productId: null,
45
+ sellerId: null,
46
+ }),
47
+ true,
48
+ );
49
+ });
50
+ check("缺失字段(undefined)→ true", () =>
51
+ assert.strictEqual(isEmptyProductDetail({}), true),
52
+ );
53
+ check("有 rating → false", () =>
54
+ assert.strictEqual(isEmptyProductDetail({ rating: 4.7 }), false),
55
+ );
56
+ check("有 soldCount → false", () =>
57
+ assert.strictEqual(isEmptyProductDetail({ soldCount: 296 }), false),
58
+ );
59
+ check("null/undefined 混入 → true", () =>
60
+ assert.strictEqual(
61
+ isEmptyProductDetail({ rating: null, soldCount: undefined }),
62
+ true,
63
+ ),
64
+ );
65
+
66
+ console.log("\n[2] parseProductHtml 空壳页抛可重试错误");
67
+ check("纯空 HTML → 抛 Empty product page", () => {
68
+ assert.throws(
69
+ () => parseProductHtml("<html><body></body></html>"),
70
+ /Empty product page/,
71
+ );
72
+ });
73
+ check("风控降级页(无商品标识)→ 抛 Empty product page", () => {
74
+ assert.throws(
75
+ () =>
76
+ parseProductHtml(
77
+ "<html><head><title>TikTok Shop</title></head><body>Please verify you are a human</body></html>",
78
+ ),
79
+ /Empty product page/,
80
+ );
81
+ });
82
+
83
+ console.log("\n[3] parseProductHtml 正常 PDP 页面解析");
84
+ const detail = parseProductHtml(VALID_HTML);
85
+ check("rating 解析正确", () => assert.strictEqual(detail.rating, 4.7));
86
+ check("reviewCount 解析正确", () =>
87
+ assert.strictEqual(detail.reviewCount, 3303),
88
+ );
89
+ check("soldCount 解析正确", () => assert.strictEqual(detail.soldCount, 296));
90
+ check("productName 解析正确", () =>
91
+ assert.strictEqual(detail.productName, "Test Product"),
92
+ );
93
+ check("productId/sellerId 解析正确", () => {
94
+ assert.strictEqual(detail.productId, "123456");
95
+ assert.strictEqual(detail.sellerId, "789");
96
+ });
97
+ check("解析结果非全空", () =>
98
+ assert.strictEqual(isEmptyProductDetail(detail), false),
99
+ );
100
+
101
+ console.log("\n[4] parseProductHtml 验证码页抛 captcha 错误");
102
+ check("captcha-verify-container → 抛 captcha", () => {
103
+ assert.throws(
104
+ () =>
105
+ parseProductHtml(
106
+ '<html><body><div class="captcha-verify-container"></div></body></html>',
107
+ ),
108
+ /Blocked by TikTok/,
109
+ );
110
+ });
111
+ check("Security Check 标题 → 抛 captcha", () => {
112
+ assert.throws(
113
+ () => parseProductHtml("<html><title>Security Check</title></html>"),
114
+ /Blocked by TikTok/,
115
+ );
116
+ });
117
+
118
+ console.log("\n[5] parseArgs --profiles 参数解析");
119
+ const { parseArgs } = await import("../src/lib/args.js");
120
+ const origArgv = process.argv;
121
+ try {
122
+ process.argv = [
123
+ "node",
124
+ "tt-help",
125
+ "product-fill",
126
+ "--profiles",
127
+ "us/9223",
128
+ "-l",
129
+ "3",
130
+ ];
131
+ const parsed = parseArgs();
132
+ check("单对国家小写转大写", () =>
133
+ assert.deepStrictEqual(parsed.productFillProfiles, [
134
+ { country: "US", port: 9223 },
135
+ ]),
136
+ );
137
+ check("subcommand = product-fill", () =>
138
+ assert.strictEqual(parsed.subcommand, "product-fill"),
139
+ );
140
+ check("-l 3 生效", () => assert.strictEqual(parsed.productFillLimit, 3));
141
+ check("--task-delay 未传时默认 5", () =>
142
+ assert.strictEqual(parsed.productFillTaskDelay, 5),
143
+ );
144
+
145
+ process.argv = [
146
+ "node",
147
+ "tt-help",
148
+ "product-fill",
149
+ "--profiles",
150
+ "US/9223",
151
+ "--task-delay",
152
+ "30",
153
+ ];
154
+ const parsedDelay = parseArgs();
155
+ check("--task-delay 30 生效", () =>
156
+ assert.strictEqual(parsedDelay.productFillTaskDelay, 30),
157
+ );
158
+
159
+ process.argv = [
160
+ "node",
161
+ "tt-help",
162
+ "product-fill",
163
+ "--profiles",
164
+ "US/9223",
165
+ "--task-delay",
166
+ "0",
167
+ ];
168
+ const parsedDelayZero = parseArgs();
169
+ check("--task-delay 0 生效(关闭任务间延迟)", () =>
170
+ assert.strictEqual(parsedDelayZero.productFillTaskDelay, 0),
171
+ );
172
+
173
+ process.argv = [
174
+ "node",
175
+ "tt-help",
176
+ "product-fill",
177
+ "--profiles",
178
+ "US/9223,JP/1222",
179
+ ];
180
+ const parsedMulti = parseArgs();
181
+ check("多对解析", () =>
182
+ assert.deepStrictEqual(parsedMulti.productFillProfiles, [
183
+ { country: "US", port: 9223 },
184
+ { country: "JP", port: 1222 },
185
+ ]),
186
+ );
187
+
188
+ process.argv = ["node", "tt-help", "product-fill", "--profiles", "us/abc,xx"];
189
+ const parsedBad = parseArgs();
190
+ check("非法段被丢弃", () =>
191
+ assert.deepStrictEqual(parsedBad.productFillProfiles, []),
192
+ );
193
+ } finally {
194
+ process.argv = origArgv;
195
+ }
196
+
197
+ console.log("\n[6] 浏览器断连判定(网络层错误不重启,防误杀 Edge)");
198
+ const { isBrowserDisconnected, isNetworkError, isCDPPortReady } =
199
+ await import("../src/cli/product-fill.js");
200
+
201
+ const onlineBrowser = { isConnected: () => true };
202
+ const offlineBrowser = { isConnected: () => false };
203
+
204
+ check("net::ERR_CONNECTION_CLOSED + 浏览器在线 → 不断连", () => {
205
+ assert.strictEqual(
206
+ isBrowserDisconnected(
207
+ onlineBrowser,
208
+ new Error("page.goto: net::ERR_CONNECTION_CLOSED at https://..."),
209
+ ),
210
+ false,
211
+ );
212
+ });
213
+ check("net::ERR_CONNECTION_RESET + 浏览器在线 → 不断连", () => {
214
+ assert.strictEqual(
215
+ isBrowserDisconnected(
216
+ onlineBrowser,
217
+ new Error("net::ERR_CONNECTION_RESET"),
218
+ ),
219
+ false,
220
+ );
221
+ });
222
+ check("net::ERR_PROXY_CONNECTION_FAILED + 浏览器在线 → 不断连", () => {
223
+ assert.strictEqual(
224
+ isBrowserDisconnected(
225
+ onlineBrowser,
226
+ new Error("net::ERR_PROXY_CONNECTION_FAILED"),
227
+ ),
228
+ false,
229
+ );
230
+ });
231
+ check("isConnected()=false → 断连", () => {
232
+ assert.strictEqual(
233
+ isBrowserDisconnected(offlineBrowser, new Error("any error")),
234
+ true,
235
+ );
236
+ });
237
+ check("Target closed + 在线 → 断连", () => {
238
+ assert.strictEqual(
239
+ isBrowserDisconnected(onlineBrowser, new Error("Target closed")),
240
+ true,
241
+ );
242
+ });
243
+ check("Browser has been closed + 在线 → 断连", () => {
244
+ assert.strictEqual(
245
+ isBrowserDisconnected(onlineBrowser, new Error("Browser has been closed")),
246
+ true,
247
+ );
248
+ });
249
+ check("Target page... closed + 在线 → 断连", () => {
250
+ assert.strictEqual(
251
+ isBrowserDisconnected(
252
+ onlineBrowser,
253
+ new Error("Target page, context or browser has been closed"),
254
+ ),
255
+ true,
256
+ );
257
+ });
258
+ check("Protocol error + 在线 → 不断连(交给错误计数处理)", () => {
259
+ assert.strictEqual(
260
+ isBrowserDisconnected(onlineBrowser, new Error("Protocol error")),
261
+ false,
262
+ );
263
+ });
264
+ check("无错误参数 → 不断连", () => {
265
+ assert.strictEqual(isBrowserDisconnected(onlineBrowser, null), false);
266
+ });
267
+ check("isNetworkError(net::ERR_*) → true", () => {
268
+ assert.strictEqual(
269
+ isNetworkError(new Error("net::ERR_CONNECTION_CLOSED")),
270
+ true,
271
+ );
272
+ });
273
+ check("isNetworkError(普通错误) → false", () => {
274
+ assert.strictEqual(isNetworkError(new Error("Timeout")), false);
275
+ });
276
+
277
+ console.log("\n[7] CDP 端口就绪探测(isCDPPortReady)");
278
+ // 未监听端口应快速返回 false(本机无 Edge 监听该端口)
279
+ const probeResult = await isCDPPortReady(65531);
280
+ if (probeResult === false) {
281
+ passed++;
282
+ console.log(" ✅ 未监听端口 → false");
283
+ } else {
284
+ console.error(
285
+ " ❌ 未监听端口 → false\n 期望 false,实得 " + probeResult,
286
+ );
287
+ process.exitCode = 1;
288
+ }
289
+
290
+ console.log(
291
+ `\n结果: ${passed} 通过${process.exitCode ? ", 有失败" : ", 0 失败"}`,
292
+ );