tt-help-cli-ycl 1.4.169 → 1.4.171
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 +1 -1
- package/scripts/test-subscription-allocator.mjs +220 -0
- package/scripts/test-subscription-auto-alloc-ui.mjs +208 -0
- package/scripts/test-subscription-auto-alloc.mjs +259 -0
- package/scripts/test-subscription-center-e2e.mjs +134 -0
- package/scripts/test-subscription-center-smoke.mjs +154 -0
- package/scripts/test-subscription-center-ui.mjs +111 -0
- package/scripts/vnc-login.sh +68 -0
- package/src/cli/info.js +6 -1
- package/src/lib/browser/cdp.js +7 -1
- package/src/watch/public/app.js +586 -15
- package/src/watch/public/index.html +70 -3
- package/src/watch/subscription-allocator.js +255 -0
- package/src/watch/subscription-center.js +590 -0
- package/src/watch/watchdog-server.js +210 -0
- package/src/watchdog/agent.js +5 -0
- package/scripts/test-api-interceptor-429.mjs +0 -122
- package/scripts/test-api-interceptor-captcha.mjs +0 -176
- package/scripts/test-claim-fix.mjs +0 -216
- package/scripts/test-clash-discover.mjs +0 -126
- package/scripts/test-clash-hysteresis.mjs +0 -131
- package/scripts/test-clash-multichannel.mjs +0 -120
- package/scripts/test-client-error-monitor.mjs +0 -261
- package/scripts/test-country-fix.mjs +0 -341
- package/scripts/test-daemon-failover.mjs +0 -301
- package/scripts/test-daemon-switch-history.mjs +0 -141
- package/scripts/test-daemon-whitelist.mjs +0 -308
- package/scripts/test-explore-captcha-wait.mjs +0 -170
- package/scripts/test-photo-post.mjs +0 -156
- package/scripts/test-precheck-perf.mjs +0 -270
- package/scripts/test-precise-index.mjs +0 -114
- package/scripts/test-probe-live.mjs +0 -83
- package/scripts/test-probe.mjs +0 -315
- package/scripts/test-product-fill-cdp.mjs +0 -336
- package/scripts/test-product-fill-db.mjs +0 -216
- package/scripts/test-product-fill-http.mjs +0 -117
- package/scripts/test-proxy-switch-restart.mjs +0 -198
- package/scripts/test-refill-order.mjs +0 -218
- package/scripts/test-sample-all-fail.mjs +0 -142
- package/scripts/test-seller-any-country.mjs +0 -167
- package/scripts/test-stats-smoke.mjs +0 -43
- package/scripts/test-upgrade-spawn.mjs +0 -179
- package/scripts/test-webserver-error-classify.mjs +0 -409
- package/scripts/test-wmi-free-upgrade.mjs +0 -98
- package/src/cli/test-real-attach.js +0 -0
- package/src/results/user-videos-bar.lar.lar.moeta.json +0 -37
package/scripts/test-probe.mjs
DELETED
|
@@ -1,315 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 探测模块单元测试(离线,不连浏览器)
|
|
3
|
-
*
|
|
4
|
-
* 覆盖:
|
|
5
|
-
* - common: classifyError / withTimeout / cleanMessage / makePointResult
|
|
6
|
-
* - targets: DEFAULT_TARGETS / resolveTargets
|
|
7
|
-
* - index: POINT_NAMES / normalizePoints / runProbe 空端口与异常端口
|
|
8
|
-
* - points: comment 无视频 skipped、follow 未登录 skipped、POINT_NAME 注册
|
|
9
|
-
*
|
|
10
|
-
* 运行: node scripts/test-probe.mjs
|
|
11
|
-
*/
|
|
12
|
-
import assert from "node:assert/strict";
|
|
13
|
-
import {
|
|
14
|
-
classifyError,
|
|
15
|
-
withTimeout,
|
|
16
|
-
cleanMessage,
|
|
17
|
-
makePointResult,
|
|
18
|
-
isTransientKind,
|
|
19
|
-
retryOnTransient,
|
|
20
|
-
} from "../src/lib/probe/common.js";
|
|
21
|
-
import { DEFAULT_TARGETS, resolveTargets } from "../src/lib/probe/targets.js";
|
|
22
|
-
import {
|
|
23
|
-
POINTS,
|
|
24
|
-
POINT_NAMES,
|
|
25
|
-
normalizePoints,
|
|
26
|
-
runProbe,
|
|
27
|
-
} from "../src/lib/probe/index.js";
|
|
28
|
-
import {
|
|
29
|
-
POINT_NAME as COMMENT_POINT_NAME,
|
|
30
|
-
probeComment,
|
|
31
|
-
} from "../src/lib/probe/points/comment.js";
|
|
32
|
-
import {
|
|
33
|
-
POINT_NAME as FOLLOW_POINT_NAME,
|
|
34
|
-
probeFollow,
|
|
35
|
-
} from "../src/lib/probe/points/follow.js";
|
|
36
|
-
|
|
37
|
-
let passed = 0;
|
|
38
|
-
const failures = [];
|
|
39
|
-
const allTests = [];
|
|
40
|
-
function test(name, fn) {
|
|
41
|
-
allTests.push(
|
|
42
|
-
Promise.resolve().then(() => {
|
|
43
|
-
try {
|
|
44
|
-
fn();
|
|
45
|
-
passed++;
|
|
46
|
-
console.log(`✓ ${name}`);
|
|
47
|
-
} catch (e) {
|
|
48
|
-
failures.push({ name, error: e });
|
|
49
|
-
console.error(`✗ ${name}: ${e.message}`);
|
|
50
|
-
}
|
|
51
|
-
}),
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
function testAsync(name, fn) {
|
|
55
|
-
allTests.push(
|
|
56
|
-
Promise.resolve().then(async () => {
|
|
57
|
-
try {
|
|
58
|
-
await fn();
|
|
59
|
-
passed++;
|
|
60
|
-
console.log(`✓ ${name}`);
|
|
61
|
-
} catch (e) {
|
|
62
|
-
failures.push({ name, error: e });
|
|
63
|
-
console.error(`✗ ${name}: ${e.message}`);
|
|
64
|
-
}
|
|
65
|
-
}),
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// ── common.classifyError ──
|
|
70
|
-
test("classifyError: 被封 → blocked", () => {
|
|
71
|
-
assert.equal(classifyError(new Error("被封: abc")).kind, "blocked");
|
|
72
|
-
});
|
|
73
|
-
test("classifyError: CDN限流 → rate_limited", () => {
|
|
74
|
-
assert.equal(classifyError(new Error("CDN限流: abc")).kind, "rate_limited");
|
|
75
|
-
});
|
|
76
|
-
test("classifyError: Access Denied → rate_limited", () => {
|
|
77
|
-
assert.equal(classifyError("Access Denied").kind, "rate_limited");
|
|
78
|
-
});
|
|
79
|
-
test("classifyError: 验证码 → captcha", () => {
|
|
80
|
-
assert.equal(classifyError("验证码出现").kind, "captcha");
|
|
81
|
-
});
|
|
82
|
-
test("classifyError: captcha → captcha", () => {
|
|
83
|
-
assert.equal(classifyError("captcha verify").kind, "captcha");
|
|
84
|
-
});
|
|
85
|
-
test("classifyError: Timeout → timeout", () => {
|
|
86
|
-
assert.equal(classifyError(new Error("Timeout 123")).kind, "timeout");
|
|
87
|
-
});
|
|
88
|
-
test("classifyError: 超时 → timeout", () => {
|
|
89
|
-
assert.equal(classifyError("请求超时").kind, "timeout");
|
|
90
|
-
});
|
|
91
|
-
test("classifyError: login_required → login_required", () => {
|
|
92
|
-
assert.equal(classifyError("login_required").kind, "login_required");
|
|
93
|
-
});
|
|
94
|
-
test("classifyError: 未登录 → login_required", () => {
|
|
95
|
-
assert.equal(classifyError("请先登录").kind, "login_required");
|
|
96
|
-
});
|
|
97
|
-
test("classifyError: region → region_blocked", () => {
|
|
98
|
-
assert.equal(
|
|
99
|
-
classifyError("not available in your region").kind,
|
|
100
|
-
"region_blocked",
|
|
101
|
-
);
|
|
102
|
-
});
|
|
103
|
-
test("classifyError: 普通错误 → error", () => {
|
|
104
|
-
assert.equal(classifyError("boom something").kind, "error");
|
|
105
|
-
});
|
|
106
|
-
test("classifyError: 空 → error + 未知错误", () => {
|
|
107
|
-
const c = classifyError(null);
|
|
108
|
-
assert.equal(c.kind, "error");
|
|
109
|
-
assert.equal(c.message, "未知错误");
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
// ── common.cleanMessage ──
|
|
113
|
-
test("cleanMessage: 去 ANSI + 首行 + 限长", () => {
|
|
114
|
-
const out = cleanMessage("\x1b[31mline1\x1b[0m\nline2\n".repeat(50));
|
|
115
|
-
assert.ok(!out.includes("\x1b"));
|
|
116
|
-
assert.ok(!out.includes("\n"));
|
|
117
|
-
assert.ok(out.length <= 300);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
// ── common.withTimeout ──
|
|
121
|
-
testAsync("withTimeout: 正常完成返回结果", async () => {
|
|
122
|
-
const r = await withTimeout(Promise.resolve("ok"), 2000, "t");
|
|
123
|
-
assert.equal(r, "ok");
|
|
124
|
-
});
|
|
125
|
-
testAsync("withTimeout: 超时 reject", async () => {
|
|
126
|
-
let timedOut = false;
|
|
127
|
-
try {
|
|
128
|
-
await withTimeout(new Promise(() => {}), 30, "超时测试");
|
|
129
|
-
} catch (e) {
|
|
130
|
-
timedOut = true;
|
|
131
|
-
assert.ok(e.message.includes("超时测试"));
|
|
132
|
-
}
|
|
133
|
-
assert.ok(timedOut, "应超时 reject");
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
// ── common.makePointResult ──
|
|
137
|
-
test("makePointResult: 默认空 → kind=empty", () => {
|
|
138
|
-
const r = makePointResult({ ok: false });
|
|
139
|
-
assert.equal(r.kind, "empty");
|
|
140
|
-
assert.equal(r.count, 0);
|
|
141
|
-
assert.equal(r.skipped, false);
|
|
142
|
-
});
|
|
143
|
-
test("makePointResult: ok → kind=null + extra 合并", () => {
|
|
144
|
-
const r = makePointResult({ ok: true, count: 3, extra: { a: 1 } });
|
|
145
|
-
assert.equal(r.kind, null);
|
|
146
|
-
assert.equal(r.a, 1);
|
|
147
|
-
});
|
|
148
|
-
test("makePointResult: skipped + 显式 kind", () => {
|
|
149
|
-
const r = makePointResult({
|
|
150
|
-
ok: false,
|
|
151
|
-
skipped: true,
|
|
152
|
-
skipReason: "no_video",
|
|
153
|
-
kind: "login_required",
|
|
154
|
-
});
|
|
155
|
-
assert.equal(r.kind, "login_required");
|
|
156
|
-
assert.equal(r.skipReason, "no_video");
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
// ── common.isTransientKind / retryOnTransient ──
|
|
160
|
-
test("isTransientKind: 瞬时型为 true", () => {
|
|
161
|
-
for (const k of ["timeout", "empty", "error", "waf_challenge"]) {
|
|
162
|
-
assert.equal(isTransientKind(k), true, `${k} 应为瞬时`);
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
test("isTransientKind: 确定型与空为 false", () => {
|
|
166
|
-
for (const k of [
|
|
167
|
-
"blocked",
|
|
168
|
-
"rate_limited",
|
|
169
|
-
"captcha",
|
|
170
|
-
"login_required",
|
|
171
|
-
"region_blocked",
|
|
172
|
-
null,
|
|
173
|
-
undefined,
|
|
174
|
-
"",
|
|
175
|
-
]) {
|
|
176
|
-
assert.equal(isTransientKind(k), false, `${k} 不应为瞬时`);
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
testAsync("retryOnTransient: 成功即返回(只调 1 次)", async () => {
|
|
180
|
-
let calls = 0;
|
|
181
|
-
const r = await retryOnTransient(async () => {
|
|
182
|
-
calls++;
|
|
183
|
-
return makePointResult({ ok: true, count: 3 });
|
|
184
|
-
}, { retries: 2, retryDelayMs: 0 });
|
|
185
|
-
assert.equal(r.ok, true);
|
|
186
|
-
assert.equal(calls, 1);
|
|
187
|
-
});
|
|
188
|
-
testAsync("retryOnTransient: 确定性失败不重试(只调 1 次)", async () => {
|
|
189
|
-
let calls = 0;
|
|
190
|
-
const r = await retryOnTransient(async () => {
|
|
191
|
-
calls++;
|
|
192
|
-
return makePointResult({ ok: false, kind: "blocked" });
|
|
193
|
-
}, { retries: 2, retryDelayMs: 0 });
|
|
194
|
-
assert.equal(r.ok, false);
|
|
195
|
-
assert.equal(r.kind, "blocked");
|
|
196
|
-
assert.equal(calls, 1);
|
|
197
|
-
});
|
|
198
|
-
testAsync("retryOnTransient: 瞬时失败后第 2 次成功", async () => {
|
|
199
|
-
let calls = 0;
|
|
200
|
-
const r = await retryOnTransient(async () => {
|
|
201
|
-
calls++;
|
|
202
|
-
if (calls === 1) return makePointResult({ ok: false, kind: "waf_challenge" });
|
|
203
|
-
return makePointResult({ ok: true, count: 5 });
|
|
204
|
-
}, { retries: 2, retryDelayMs: 0 });
|
|
205
|
-
assert.equal(r.ok, true);
|
|
206
|
-
assert.equal(calls, 2);
|
|
207
|
-
});
|
|
208
|
-
testAsync("retryOnTransient: 瞬时失败耗尽返回最后结果", async () => {
|
|
209
|
-
let calls = 0;
|
|
210
|
-
const r = await retryOnTransient(async () => {
|
|
211
|
-
calls++;
|
|
212
|
-
return makePointResult({ ok: false, kind: "timeout" });
|
|
213
|
-
}, { retries: 2, retryDelayMs: 0 });
|
|
214
|
-
assert.equal(r.ok, false);
|
|
215
|
-
assert.equal(r.kind, "timeout");
|
|
216
|
-
assert.equal(calls, 3); // 1 次初始 + 2 次重试
|
|
217
|
-
});
|
|
218
|
-
testAsync("retryOnTransient: 重试间有间隔延迟", async () => {
|
|
219
|
-
const t0 = Date.now();
|
|
220
|
-
let calls = 0;
|
|
221
|
-
await retryOnTransient(
|
|
222
|
-
async () => {
|
|
223
|
-
calls++;
|
|
224
|
-
return makePointResult({ ok: false, kind: "empty" });
|
|
225
|
-
},
|
|
226
|
-
{ retries: 1, retryDelayMs: 150 },
|
|
227
|
-
);
|
|
228
|
-
assert.equal(calls, 2);
|
|
229
|
-
assert.ok(Date.now() - t0 >= 150, "应有重试间隔");
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
// ── targets ──
|
|
233
|
-
test("targets: 默认值", () => {
|
|
234
|
-
assert.equal(DEFAULT_TARGETS.author, "tiktok");
|
|
235
|
-
assert.equal(DEFAULT_TARGETS.searchQuery, "dog");
|
|
236
|
-
assert.equal(DEFAULT_TARGETS.video, null);
|
|
237
|
-
});
|
|
238
|
-
test("targets: resolveTargets 覆盖 + 保留默认", () => {
|
|
239
|
-
const t = resolveTargets({ author: "foo", video: "x" });
|
|
240
|
-
assert.equal(t.author, "foo");
|
|
241
|
-
assert.equal(t.searchQuery, "dog");
|
|
242
|
-
assert.equal(t.video, "x");
|
|
243
|
-
});
|
|
244
|
-
test("targets: resolveTargets 空覆盖 → 全默认", () => {
|
|
245
|
-
const t = resolveTargets();
|
|
246
|
-
assert.deepEqual(t, DEFAULT_TARGETS);
|
|
247
|
-
});
|
|
248
|
-
test("targets: resolveTargets null → 全默认", () => {
|
|
249
|
-
const t = resolveTargets(null);
|
|
250
|
-
assert.deepEqual(t, DEFAULT_TARGETS);
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
// ── index.normalizePoints ──
|
|
254
|
-
test("normalizePoints: 空 → 全部功能点", () => {
|
|
255
|
-
assert.deepEqual(normalizePoints(), [...POINT_NAMES]);
|
|
256
|
-
assert.deepEqual(normalizePoints([]), [...POINT_NAMES]);
|
|
257
|
-
assert.deepEqual(normalizePoints(null), [...POINT_NAMES]);
|
|
258
|
-
});
|
|
259
|
-
test("normalizePoints: 全非法 → 全部功能点", () => {
|
|
260
|
-
assert.deepEqual(normalizePoints(["bad", "xxx"]), [...POINT_NAMES]);
|
|
261
|
-
});
|
|
262
|
-
test("normalizePoints: 过滤非法 + 去重 + 保序", () => {
|
|
263
|
-
assert.deepEqual(normalizePoints(["video", "video", "bad", "comment"]), [
|
|
264
|
-
"video",
|
|
265
|
-
"comment",
|
|
266
|
-
]);
|
|
267
|
-
});
|
|
268
|
-
test("POINTS 注册表完整", () => {
|
|
269
|
-
assert.equal(POINT_NAMES.length, 5);
|
|
270
|
-
for (const n of POINT_NAMES) assert.equal(typeof POINTS[n], "function");
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
// ── points 注册与离线可判定分支 ──
|
|
274
|
-
test("points: 各模块 POINT_NAME 常量正确", () => {
|
|
275
|
-
assert.equal(COMMENT_POINT_NAME, "comment");
|
|
276
|
-
assert.equal(FOLLOW_POINT_NAME, "follow");
|
|
277
|
-
});
|
|
278
|
-
testAsync("probeComment: 无视频 → skipped(no_video)", async () => {
|
|
279
|
-
const r = await probeComment(null, {
|
|
280
|
-
target: { video: null },
|
|
281
|
-
videoHref: null,
|
|
282
|
-
});
|
|
283
|
-
assert.equal(r.skipped, true);
|
|
284
|
-
assert.equal(r.skipReason, "no_video");
|
|
285
|
-
assert.equal(r.ok, false);
|
|
286
|
-
});
|
|
287
|
-
testAsync("probeFollow: 未登录 → skipped(not_logged_in)", async () => {
|
|
288
|
-
const r = await probeFollow(null, { target: {}, loggedIn: false });
|
|
289
|
-
assert.equal(r.skipped, true);
|
|
290
|
-
assert.equal(r.skipReason, "not_logged_in");
|
|
291
|
-
assert.equal(r.kind, "login_required");
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
// ── runProbe 空端口 / 非法端口 ──
|
|
295
|
-
testAsync("runProbe: 空端口 → 空结果结构", async () => {
|
|
296
|
-
const r = await runProbe({ ports: [], log: () => {} });
|
|
297
|
-
assert.deepEqual(r.ports, []);
|
|
298
|
-
assert.ok(Array.isArray(r.points));
|
|
299
|
-
assert.equal(typeof r.checkedAt, "string");
|
|
300
|
-
assert.equal(typeof r.durationMs, "number");
|
|
301
|
-
});
|
|
302
|
-
testAsync("runProbe: 非法端口被过滤", async () => {
|
|
303
|
-
const r = await runProbe({ ports: [0, -1, NaN, "abc"], log: () => {} });
|
|
304
|
-
assert.deepEqual(r.ports, []);
|
|
305
|
-
});
|
|
306
|
-
// 注:真实端口连接(存在/不存在端口)属集成测试范畴(会启动/连接 Edge),见 scripts/test-probe-live.mjs
|
|
307
|
-
|
|
308
|
-
// ── 汇总(等全部测试完成) ──
|
|
309
|
-
await Promise.all(allTests);
|
|
310
|
-
console.log(`\n${passed} 通过, ${failures.length} 失败`);
|
|
311
|
-
if (failures.length > 0) {
|
|
312
|
-
for (const f of failures)
|
|
313
|
-
console.error(` FAIL: ${f.name}\n ${f.error.stack}`);
|
|
314
|
-
process.exit(1);
|
|
315
|
-
}
|
|
@@ -1,336 +0,0 @@
|
|
|
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
|
-
// 含多 SKU 的 PDP HTML(结构与线上实测一致:sku_id 19位 / property_pairs / sku_quantity)
|
|
36
|
-
const VALID_HTML_WITH_SKUS =
|
|
37
|
-
'<html><body><script>{"product_model":{"name":"Test Product","product_id":"123456","seller_id":"789"},"sold_count":"962","review_ratings":{"overall_score":4.8,"review_count":"174"},"skus":[{"sku_id":"1731140428076257811","sku_name":"810003369352","sku_status":1,"property_pairs":[{"sku_property_id":"7003574686483515141","sku_property_value_id":"6996631003314030337","sku_property_name":"Shade","sku_property_value_name":"Wanderlust"}],"sku_quantity":{"warehouse_id":"7676336681096611597","available_quantity":106},"sku_stock_status":1},{"sku_id":"1729888889849549331","sku_name":"810003367204","sku_status":1,"property_pairs":[{"sku_property_name":"Shade","sku_property_value_name":"Ballerina"}],"sku_quantity":{"warehouse_id":"7676336681096611597","available_quantity":2017},"sku_stock_status":1}]}</script></body></html>';
|
|
38
|
-
|
|
39
|
-
console.log("\n[1] isEmptyProductDetail 全空判定");
|
|
40
|
-
check("全 null → true", () => {
|
|
41
|
-
assert.strictEqual(
|
|
42
|
-
isEmptyProductDetail({
|
|
43
|
-
rating: null,
|
|
44
|
-
reviewCount: null,
|
|
45
|
-
soldCount: null,
|
|
46
|
-
productName: null,
|
|
47
|
-
ratingBreakdown: null,
|
|
48
|
-
productId: null,
|
|
49
|
-
sellerId: null,
|
|
50
|
-
}),
|
|
51
|
-
true,
|
|
52
|
-
);
|
|
53
|
-
});
|
|
54
|
-
check("缺失字段(undefined)→ true", () =>
|
|
55
|
-
assert.strictEqual(isEmptyProductDetail({}), true),
|
|
56
|
-
);
|
|
57
|
-
check("有 rating → false", () =>
|
|
58
|
-
assert.strictEqual(isEmptyProductDetail({ rating: 4.7 }), false),
|
|
59
|
-
);
|
|
60
|
-
check("有 soldCount → false", () =>
|
|
61
|
-
assert.strictEqual(isEmptyProductDetail({ soldCount: 296 }), false),
|
|
62
|
-
);
|
|
63
|
-
check("null/undefined 混入 → true", () =>
|
|
64
|
-
assert.strictEqual(
|
|
65
|
-
isEmptyProductDetail({ rating: null, soldCount: undefined }),
|
|
66
|
-
true,
|
|
67
|
-
),
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
console.log("\n[2] parseProductHtml 空壳页抛可重试错误");
|
|
71
|
-
check("纯空 HTML → 抛 Empty product page", () => {
|
|
72
|
-
assert.throws(
|
|
73
|
-
() => parseProductHtml("<html><body></body></html>"),
|
|
74
|
-
/Empty product page/,
|
|
75
|
-
);
|
|
76
|
-
});
|
|
77
|
-
check("风控降级页(无商品标识)→ 抛 Empty product page", () => {
|
|
78
|
-
assert.throws(
|
|
79
|
-
() =>
|
|
80
|
-
parseProductHtml(
|
|
81
|
-
"<html><head><title>TikTok Shop</title></head><body>Please verify you are a human</body></html>",
|
|
82
|
-
),
|
|
83
|
-
/Empty product page/,
|
|
84
|
-
);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
console.log("\n[3] parseProductHtml 正常 PDP 页面解析");
|
|
88
|
-
const detail = parseProductHtml(VALID_HTML);
|
|
89
|
-
check("rating 解析正确", () => assert.strictEqual(detail.rating, 4.7));
|
|
90
|
-
check("reviewCount 解析正确", () =>
|
|
91
|
-
assert.strictEqual(detail.reviewCount, 3303),
|
|
92
|
-
);
|
|
93
|
-
check("soldCount 解析正确", () => assert.strictEqual(detail.soldCount, 296));
|
|
94
|
-
check("productName 解析正确", () =>
|
|
95
|
-
assert.strictEqual(detail.productName, "Test Product"),
|
|
96
|
-
);
|
|
97
|
-
check("productId/sellerId 解析正确", () => {
|
|
98
|
-
assert.strictEqual(detail.productId, "123456");
|
|
99
|
-
assert.strictEqual(detail.sellerId, "789");
|
|
100
|
-
});
|
|
101
|
-
check("解析结果非全空", () =>
|
|
102
|
-
assert.strictEqual(isEmptyProductDetail(detail), false),
|
|
103
|
-
);
|
|
104
|
-
|
|
105
|
-
console.log("\n[3b] parseProductHtml 多 SKU 提取");
|
|
106
|
-
const skuDetail = parseProductHtml(VALID_HTML_WITH_SKUS);
|
|
107
|
-
check("原有字段不受 skus 影响(rating)", () =>
|
|
108
|
-
assert.strictEqual(skuDetail.rating, 4.8),
|
|
109
|
-
);
|
|
110
|
-
check("原有字段不受 skus 影响(soldCount)", () =>
|
|
111
|
-
assert.strictEqual(skuDetail.soldCount, 962),
|
|
112
|
-
);
|
|
113
|
-
check(
|
|
114
|
-
"skus 数量 = 2",
|
|
115
|
-
() =>
|
|
116
|
-
assert.strictEqual(Array.isArray(skuDetail.skus), true) &&
|
|
117
|
-
assert.strictEqual(skuDetail.skus.length, 2),
|
|
118
|
-
);
|
|
119
|
-
check("skuId 19位字符串保精度", () =>
|
|
120
|
-
assert.strictEqual(skuDetail.skus[0].skuId, "1731140428076257811"),
|
|
121
|
-
);
|
|
122
|
-
check("specs 展开 property_pairs", () =>
|
|
123
|
-
assert.strictEqual(skuDetail.skus[0].specs, "Shade: Wanderlust"),
|
|
124
|
-
);
|
|
125
|
-
check(
|
|
126
|
-
"库存 available_quantity 提取",
|
|
127
|
-
() =>
|
|
128
|
-
assert.strictEqual(skuDetail.skus[0].stock, 106) &&
|
|
129
|
-
assert.strictEqual(skuDetail.skus[1].stock, 2017),
|
|
130
|
-
);
|
|
131
|
-
check("warehouseId 字符串化", () =>
|
|
132
|
-
assert.strictEqual(skuDetail.skus[0].warehouseId, "7676336681096611597"),
|
|
133
|
-
);
|
|
134
|
-
check("stockStatus 提取", () =>
|
|
135
|
-
assert.strictEqual(skuDetail.skus[0].stockStatus, 1),
|
|
136
|
-
);
|
|
137
|
-
check("无 skus 段 → skus = null", () =>
|
|
138
|
-
assert.strictEqual(parseProductHtml(VALID_HTML).skus, null),
|
|
139
|
-
);
|
|
140
|
-
check("skus 为 null 时不影响 isEmptyProductDetail", () => {
|
|
141
|
-
const d = parseProductHtml(VALID_HTML);
|
|
142
|
-
return assert.strictEqual(isEmptyProductDetail(d), false);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
console.log("\n[4] parseProductHtml 验证码页抛 captcha 错误");
|
|
146
|
-
check("captcha-verify-container → 抛 captcha", () => {
|
|
147
|
-
assert.throws(
|
|
148
|
-
() =>
|
|
149
|
-
parseProductHtml(
|
|
150
|
-
'<html><body><div class="captcha-verify-container"></div></body></html>',
|
|
151
|
-
),
|
|
152
|
-
/Blocked by TikTok/,
|
|
153
|
-
);
|
|
154
|
-
});
|
|
155
|
-
check("Security Check 标题 → 抛 captcha", () => {
|
|
156
|
-
assert.throws(
|
|
157
|
-
() => parseProductHtml("<html><title>Security Check</title></html>"),
|
|
158
|
-
/Blocked by TikTok/,
|
|
159
|
-
);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
console.log("\n[5] parseArgs --profiles 参数解析");
|
|
163
|
-
const { parseArgs } = await import("../src/lib/args.js");
|
|
164
|
-
const origArgv = process.argv;
|
|
165
|
-
try {
|
|
166
|
-
process.argv = [
|
|
167
|
-
"node",
|
|
168
|
-
"tt-help",
|
|
169
|
-
"product-fill",
|
|
170
|
-
"--profiles",
|
|
171
|
-
"us/9223",
|
|
172
|
-
"-l",
|
|
173
|
-
"3",
|
|
174
|
-
];
|
|
175
|
-
const parsed = parseArgs();
|
|
176
|
-
check("单对国家小写转大写", () =>
|
|
177
|
-
assert.deepStrictEqual(parsed.productFillProfiles, [
|
|
178
|
-
{ country: "US", port: 9223 },
|
|
179
|
-
]),
|
|
180
|
-
);
|
|
181
|
-
check("subcommand = product-fill", () =>
|
|
182
|
-
assert.strictEqual(parsed.subcommand, "product-fill"),
|
|
183
|
-
);
|
|
184
|
-
check("-l 3 生效", () => assert.strictEqual(parsed.productFillLimit, 3));
|
|
185
|
-
check("--task-delay 未传时默认 5", () =>
|
|
186
|
-
assert.strictEqual(parsed.productFillTaskDelay, 5),
|
|
187
|
-
);
|
|
188
|
-
|
|
189
|
-
process.argv = [
|
|
190
|
-
"node",
|
|
191
|
-
"tt-help",
|
|
192
|
-
"product-fill",
|
|
193
|
-
"--profiles",
|
|
194
|
-
"US/9223",
|
|
195
|
-
"--task-delay",
|
|
196
|
-
"30",
|
|
197
|
-
];
|
|
198
|
-
const parsedDelay = parseArgs();
|
|
199
|
-
check("--task-delay 30 生效", () =>
|
|
200
|
-
assert.strictEqual(parsedDelay.productFillTaskDelay, 30),
|
|
201
|
-
);
|
|
202
|
-
|
|
203
|
-
process.argv = [
|
|
204
|
-
"node",
|
|
205
|
-
"tt-help",
|
|
206
|
-
"product-fill",
|
|
207
|
-
"--profiles",
|
|
208
|
-
"US/9223",
|
|
209
|
-
"--task-delay",
|
|
210
|
-
"0",
|
|
211
|
-
];
|
|
212
|
-
const parsedDelayZero = parseArgs();
|
|
213
|
-
check("--task-delay 0 生效(关闭任务间延迟)", () =>
|
|
214
|
-
assert.strictEqual(parsedDelayZero.productFillTaskDelay, 0),
|
|
215
|
-
);
|
|
216
|
-
|
|
217
|
-
process.argv = [
|
|
218
|
-
"node",
|
|
219
|
-
"tt-help",
|
|
220
|
-
"product-fill",
|
|
221
|
-
"--profiles",
|
|
222
|
-
"US/9223,JP/1222",
|
|
223
|
-
];
|
|
224
|
-
const parsedMulti = parseArgs();
|
|
225
|
-
check("多对解析", () =>
|
|
226
|
-
assert.deepStrictEqual(parsedMulti.productFillProfiles, [
|
|
227
|
-
{ country: "US", port: 9223 },
|
|
228
|
-
{ country: "JP", port: 1222 },
|
|
229
|
-
]),
|
|
230
|
-
);
|
|
231
|
-
|
|
232
|
-
process.argv = ["node", "tt-help", "product-fill", "--profiles", "us/abc,xx"];
|
|
233
|
-
const parsedBad = parseArgs();
|
|
234
|
-
check("非法段被丢弃", () =>
|
|
235
|
-
assert.deepStrictEqual(parsedBad.productFillProfiles, []),
|
|
236
|
-
);
|
|
237
|
-
} finally {
|
|
238
|
-
process.argv = origArgv;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
console.log("\n[6] 浏览器断连判定(网络层错误不重启,防误杀 Edge)");
|
|
242
|
-
const { isBrowserDisconnected, isNetworkError, isCDPPortReady } =
|
|
243
|
-
await import("../src/cli/product-fill.js");
|
|
244
|
-
|
|
245
|
-
const onlineBrowser = { isConnected: () => true };
|
|
246
|
-
const offlineBrowser = { isConnected: () => false };
|
|
247
|
-
|
|
248
|
-
check("net::ERR_CONNECTION_CLOSED + 浏览器在线 → 不断连", () => {
|
|
249
|
-
assert.strictEqual(
|
|
250
|
-
isBrowserDisconnected(
|
|
251
|
-
onlineBrowser,
|
|
252
|
-
new Error("page.goto: net::ERR_CONNECTION_CLOSED at https://..."),
|
|
253
|
-
),
|
|
254
|
-
false,
|
|
255
|
-
);
|
|
256
|
-
});
|
|
257
|
-
check("net::ERR_CONNECTION_RESET + 浏览器在线 → 不断连", () => {
|
|
258
|
-
assert.strictEqual(
|
|
259
|
-
isBrowserDisconnected(
|
|
260
|
-
onlineBrowser,
|
|
261
|
-
new Error("net::ERR_CONNECTION_RESET"),
|
|
262
|
-
),
|
|
263
|
-
false,
|
|
264
|
-
);
|
|
265
|
-
});
|
|
266
|
-
check("net::ERR_PROXY_CONNECTION_FAILED + 浏览器在线 → 不断连", () => {
|
|
267
|
-
assert.strictEqual(
|
|
268
|
-
isBrowserDisconnected(
|
|
269
|
-
onlineBrowser,
|
|
270
|
-
new Error("net::ERR_PROXY_CONNECTION_FAILED"),
|
|
271
|
-
),
|
|
272
|
-
false,
|
|
273
|
-
);
|
|
274
|
-
});
|
|
275
|
-
check("isConnected()=false → 断连", () => {
|
|
276
|
-
assert.strictEqual(
|
|
277
|
-
isBrowserDisconnected(offlineBrowser, new Error("any error")),
|
|
278
|
-
true,
|
|
279
|
-
);
|
|
280
|
-
});
|
|
281
|
-
check("Target closed + 在线 → 断连", () => {
|
|
282
|
-
assert.strictEqual(
|
|
283
|
-
isBrowserDisconnected(onlineBrowser, new Error("Target closed")),
|
|
284
|
-
true,
|
|
285
|
-
);
|
|
286
|
-
});
|
|
287
|
-
check("Browser has been closed + 在线 → 断连", () => {
|
|
288
|
-
assert.strictEqual(
|
|
289
|
-
isBrowserDisconnected(onlineBrowser, new Error("Browser has been closed")),
|
|
290
|
-
true,
|
|
291
|
-
);
|
|
292
|
-
});
|
|
293
|
-
check("Target page... closed + 在线 → 断连", () => {
|
|
294
|
-
assert.strictEqual(
|
|
295
|
-
isBrowserDisconnected(
|
|
296
|
-
onlineBrowser,
|
|
297
|
-
new Error("Target page, context or browser has been closed"),
|
|
298
|
-
),
|
|
299
|
-
true,
|
|
300
|
-
);
|
|
301
|
-
});
|
|
302
|
-
check("Protocol error + 在线 → 不断连(交给错误计数处理)", () => {
|
|
303
|
-
assert.strictEqual(
|
|
304
|
-
isBrowserDisconnected(onlineBrowser, new Error("Protocol error")),
|
|
305
|
-
false,
|
|
306
|
-
);
|
|
307
|
-
});
|
|
308
|
-
check("无错误参数 → 不断连", () => {
|
|
309
|
-
assert.strictEqual(isBrowserDisconnected(onlineBrowser, null), false);
|
|
310
|
-
});
|
|
311
|
-
check("isNetworkError(net::ERR_*) → true", () => {
|
|
312
|
-
assert.strictEqual(
|
|
313
|
-
isNetworkError(new Error("net::ERR_CONNECTION_CLOSED")),
|
|
314
|
-
true,
|
|
315
|
-
);
|
|
316
|
-
});
|
|
317
|
-
check("isNetworkError(普通错误) → false", () => {
|
|
318
|
-
assert.strictEqual(isNetworkError(new Error("Timeout")), false);
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
console.log("\n[7] CDP 端口就绪探测(isCDPPortReady)");
|
|
322
|
-
// 未监听端口应快速返回 false(本机无 Edge 监听该端口)
|
|
323
|
-
const probeResult = await isCDPPortReady(65531);
|
|
324
|
-
if (probeResult === false) {
|
|
325
|
-
passed++;
|
|
326
|
-
console.log(" ✅ 未监听端口 → false");
|
|
327
|
-
} else {
|
|
328
|
-
console.error(
|
|
329
|
-
" ❌ 未监听端口 → false\n 期望 false,实得 " + probeResult,
|
|
330
|
-
);
|
|
331
|
-
process.exitCode = 1;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
console.log(
|
|
335
|
-
`\n结果: ${passed} 通过${process.exitCode ? ", 有失败" : ", 0 失败"}`,
|
|
336
|
-
);
|