csspilot 0.5.0 → 0.6.0
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 +76 -23
- package/dist/bin.js +727 -203
- package/dist/index.js +727 -203
- package/dist/skills/testpilot/SKILL.md +5 -1
- package/dist/skills/testpilot/manifest.yaml +1 -1
- package/dist/skills/testpilot/rules/assertion.md +8 -0
- package/dist/skills/testpilot/rules/case-design.md +13 -1
- package/dist/skills/testpilot/rules/dsl.md +62 -0
- package/dist/skills/testpilot/workflows/analyze-result.md +4 -2
- package/dist/skills/testpilot/workflows/create-case.md +4 -0
- package/dist/skills/testpilot/workflows/run-case.md +3 -0
- package/dist/skills/testpilot/workflows/test-requirements-review.md +46 -4
- package/dist/skills/testpilot/workflows/validate.md +4 -0
- package/package.json +11 -11
- package/LICENSE +0 -21
package/dist/bin.js
CHANGED
|
@@ -102,7 +102,7 @@ var require_visit = __commonJS({
|
|
|
102
102
|
var BREAK = Symbol("break visit");
|
|
103
103
|
var SKIP = Symbol("skip children");
|
|
104
104
|
var REMOVE = Symbol("remove node");
|
|
105
|
-
function
|
|
105
|
+
function visit3(node2, visitor) {
|
|
106
106
|
const visitor_ = initVisitor(visitor);
|
|
107
107
|
if (identity.isDocument(node2)) {
|
|
108
108
|
const cd = visit_(null, node2.contents, visitor_, Object.freeze([node2]));
|
|
@@ -111,9 +111,9 @@ var require_visit = __commonJS({
|
|
|
111
111
|
} else
|
|
112
112
|
visit_(null, node2, visitor_, Object.freeze([]));
|
|
113
113
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
visit3.BREAK = BREAK;
|
|
115
|
+
visit3.SKIP = SKIP;
|
|
116
|
+
visit3.REMOVE = REMOVE;
|
|
117
117
|
function visit_(key, node2, visitor, path) {
|
|
118
118
|
const ctrl = callVisitor(key, node2, visitor, path);
|
|
119
119
|
if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
|
|
@@ -247,7 +247,7 @@ var require_visit = __commonJS({
|
|
|
247
247
|
throw new Error(`Cannot replace node with ${pt} parent`);
|
|
248
248
|
}
|
|
249
249
|
}
|
|
250
|
-
exports.visit =
|
|
250
|
+
exports.visit = visit3;
|
|
251
251
|
exports.visitAsync = visitAsync;
|
|
252
252
|
}
|
|
253
253
|
});
|
|
@@ -257,7 +257,7 @@ var require_directives = __commonJS({
|
|
|
257
257
|
"../../node_modules/.pnpm/yaml@2.9.0/node_modules/yaml/dist/doc/directives.js"(exports) {
|
|
258
258
|
"use strict";
|
|
259
259
|
var identity = require_identity();
|
|
260
|
-
var
|
|
260
|
+
var visit3 = require_visit();
|
|
261
261
|
var escapeChars = {
|
|
262
262
|
"!": "%21",
|
|
263
263
|
",": "%2C",
|
|
@@ -401,7 +401,7 @@ var require_directives = __commonJS({
|
|
|
401
401
|
let tagNames;
|
|
402
402
|
if (doc && tagEntries.length > 0 && identity.isNode(doc.contents)) {
|
|
403
403
|
const tags = {};
|
|
404
|
-
|
|
404
|
+
visit3.visit(doc.contents, (_key, node2) => {
|
|
405
405
|
if (identity.isNode(node2) && node2.tag)
|
|
406
406
|
tags[node2.tag] = true;
|
|
407
407
|
});
|
|
@@ -428,7 +428,7 @@ var require_anchors = __commonJS({
|
|
|
428
428
|
"../../node_modules/.pnpm/yaml@2.9.0/node_modules/yaml/dist/doc/anchors.js"(exports) {
|
|
429
429
|
"use strict";
|
|
430
430
|
var identity = require_identity();
|
|
431
|
-
var
|
|
431
|
+
var visit3 = require_visit();
|
|
432
432
|
function anchorIsValid(anchor2) {
|
|
433
433
|
if (/[\x00-\x19\s,[\]{}]/.test(anchor2)) {
|
|
434
434
|
const sa = JSON.stringify(anchor2);
|
|
@@ -439,7 +439,7 @@ var require_anchors = __commonJS({
|
|
|
439
439
|
}
|
|
440
440
|
function anchorNames(root) {
|
|
441
441
|
const anchors = /* @__PURE__ */ new Set();
|
|
442
|
-
|
|
442
|
+
visit3.visit(root, {
|
|
443
443
|
Value(_key, node2) {
|
|
444
444
|
if (node2.anchor)
|
|
445
445
|
anchors.add(node2.anchor);
|
|
@@ -619,7 +619,7 @@ var require_Alias = __commonJS({
|
|
|
619
619
|
"../../node_modules/.pnpm/yaml@2.9.0/node_modules/yaml/dist/nodes/Alias.js"(exports) {
|
|
620
620
|
"use strict";
|
|
621
621
|
var anchors = require_anchors();
|
|
622
|
-
var
|
|
622
|
+
var visit3 = require_visit();
|
|
623
623
|
var identity = require_identity();
|
|
624
624
|
var Node = require_Node();
|
|
625
625
|
var toJS = require_toJS();
|
|
@@ -645,7 +645,7 @@ var require_Alias = __commonJS({
|
|
|
645
645
|
nodes = ctx.aliasResolveCache;
|
|
646
646
|
} else {
|
|
647
647
|
nodes = [];
|
|
648
|
-
|
|
648
|
+
visit3.visit(doc, {
|
|
649
649
|
Node: (_key, node2) => {
|
|
650
650
|
if (identity.isAlias(node2) || identity.hasAnchor(node2))
|
|
651
651
|
nodes.push(node2);
|
|
@@ -5564,15 +5564,15 @@ var require_cst_visit = __commonJS({
|
|
|
5564
5564
|
var BREAK = Symbol("break visit");
|
|
5565
5565
|
var SKIP = Symbol("skip children");
|
|
5566
5566
|
var REMOVE = Symbol("remove item");
|
|
5567
|
-
function
|
|
5567
|
+
function visit3(cst, visitor) {
|
|
5568
5568
|
if ("type" in cst && cst.type === "document")
|
|
5569
5569
|
cst = { start: cst.start, value: cst.value };
|
|
5570
5570
|
_visit(Object.freeze([]), cst, visitor);
|
|
5571
5571
|
}
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5572
|
+
visit3.BREAK = BREAK;
|
|
5573
|
+
visit3.SKIP = SKIP;
|
|
5574
|
+
visit3.REMOVE = REMOVE;
|
|
5575
|
+
visit3.itemAtPath = (cst, path) => {
|
|
5576
5576
|
let item = cst;
|
|
5577
5577
|
for (const [field, index] of path) {
|
|
5578
5578
|
const tok = item?.[field];
|
|
@@ -5583,8 +5583,8 @@ var require_cst_visit = __commonJS({
|
|
|
5583
5583
|
}
|
|
5584
5584
|
return item;
|
|
5585
5585
|
};
|
|
5586
|
-
|
|
5587
|
-
const parent =
|
|
5586
|
+
visit3.parentCollection = (cst, path) => {
|
|
5587
|
+
const parent = visit3.itemAtPath(cst, path.slice(0, -1));
|
|
5588
5588
|
const field = path[path.length - 1][0];
|
|
5589
5589
|
const coll = parent?.[field];
|
|
5590
5590
|
if (coll && "items" in coll)
|
|
@@ -5615,7 +5615,7 @@ var require_cst_visit = __commonJS({
|
|
|
5615
5615
|
}
|
|
5616
5616
|
return typeof ctrl === "function" ? ctrl(item, path) : ctrl;
|
|
5617
5617
|
}
|
|
5618
|
-
exports.visit =
|
|
5618
|
+
exports.visit = visit3;
|
|
5619
5619
|
}
|
|
5620
5620
|
});
|
|
5621
5621
|
|
|
@@ -7331,7 +7331,7 @@ var require_dist = __commonJS({
|
|
|
7331
7331
|
var lineCounter = require_line_counter();
|
|
7332
7332
|
var parser = require_parser();
|
|
7333
7333
|
var publicApi = require_public_api();
|
|
7334
|
-
var
|
|
7334
|
+
var visit3 = require_visit();
|
|
7335
7335
|
exports.Composer = composer.Composer;
|
|
7336
7336
|
exports.Document = Document.Document;
|
|
7337
7337
|
exports.Schema = Schema.Schema;
|
|
@@ -7359,8 +7359,8 @@ var require_dist = __commonJS({
|
|
|
7359
7359
|
exports.parseAllDocuments = publicApi.parseAllDocuments;
|
|
7360
7360
|
exports.parseDocument = publicApi.parseDocument;
|
|
7361
7361
|
exports.stringify = publicApi.stringify;
|
|
7362
|
-
exports.visit =
|
|
7363
|
-
exports.visitAsync =
|
|
7362
|
+
exports.visit = visit3.visit;
|
|
7363
|
+
exports.visitAsync = visit3.visitAsync;
|
|
7364
7364
|
}
|
|
7365
7365
|
});
|
|
7366
7366
|
|
|
@@ -15418,7 +15418,7 @@ var recursive = /* @__PURE__ */ new WeakMap();
|
|
|
15418
15418
|
var NONE = 0;
|
|
15419
15419
|
var ASSUMED = 1;
|
|
15420
15420
|
var PROVEN = 2;
|
|
15421
|
-
function isRecursive(inst, stack,
|
|
15421
|
+
function isRecursive(inst, stack, resolve7) {
|
|
15422
15422
|
const cached2 = recursive.get(inst);
|
|
15423
15423
|
if (cached2 !== void 0)
|
|
15424
15424
|
return cached2 ? PROVEN : NONE;
|
|
@@ -15428,7 +15428,7 @@ function isRecursive(inst, stack, resolve6) {
|
|
|
15428
15428
|
let result = NONE;
|
|
15429
15429
|
const check2 = (child) => {
|
|
15430
15430
|
if (result !== PROVEN && child?._zod) {
|
|
15431
|
-
const answer = isRecursive(child, stack,
|
|
15431
|
+
const answer = isRecursive(child, stack, resolve7);
|
|
15432
15432
|
if (answer > result)
|
|
15433
15433
|
result = answer;
|
|
15434
15434
|
}
|
|
@@ -15439,7 +15439,7 @@ function isRecursive(inst, stack, resolve6) {
|
|
|
15439
15439
|
const desc = Object.getOwnPropertyDescriptor(sh, key);
|
|
15440
15440
|
if (spread && !desc.enumerable)
|
|
15441
15441
|
continue;
|
|
15442
|
-
const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack,
|
|
15442
|
+
const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve7) : NONE;
|
|
15443
15443
|
if (child > answer)
|
|
15444
15444
|
answer = child;
|
|
15445
15445
|
}
|
|
@@ -15506,7 +15506,7 @@ function isRecursive(inst, stack, resolve6) {
|
|
|
15506
15506
|
break;
|
|
15507
15507
|
// `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
|
|
15508
15508
|
case "lazy": {
|
|
15509
|
-
const inner = def._cachedInner ?? (
|
|
15509
|
+
const inner = def._cachedInner ?? (resolve7 ? inst._zod.innerType : void 0);
|
|
15510
15510
|
merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
|
|
15511
15511
|
break;
|
|
15512
15512
|
}
|
|
@@ -30072,6 +30072,8 @@ var configSchema = external_exports.strictObject({
|
|
|
30072
30072
|
}).optional(),
|
|
30073
30073
|
/** 环境配置:default 指定默认环境名,其余键为环境名 -> { baseUrl } */
|
|
30074
30074
|
environment: external_exports.record(external_exports.string(), external_exports.union([external_exports.string().min(1), environmentEntrySchema])).optional(),
|
|
30075
|
+
/** Case 可见的项目变量白名单;值可通过 ${ENV_VAR} 从运行环境注入 */
|
|
30076
|
+
variables: external_exports.record(external_exports.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/), external_exports.string()).optional(),
|
|
30075
30077
|
/** 项目声明的执行端(playwright / wechatide);仅作声明,实际可用性以 doctor 检测为准 */
|
|
30076
30078
|
adapters: external_exports.array(external_exports.string().min(1)).optional(),
|
|
30077
30079
|
workspace: external_exports.strictObject({ default: external_exports.string().min(1).optional() }).optional(),
|
|
@@ -30136,6 +30138,9 @@ async function loadTestpilotConfig(cwd = process.cwd()) {
|
|
|
30136
30138
|
testDirectory: parsed.test?.directory,
|
|
30137
30139
|
projectName: parsed.project?.name,
|
|
30138
30140
|
environment,
|
|
30141
|
+
variables: parsed.variables ? Object.fromEntries(
|
|
30142
|
+
Object.entries(parsed.variables).map(([name, value]) => [name, interpolateEnvRefs(value)])
|
|
30143
|
+
) : void 0,
|
|
30139
30144
|
adapters: parsed.adapters,
|
|
30140
30145
|
workspace: parsed.workspace,
|
|
30141
30146
|
server: parsed.server ? {
|
|
@@ -30338,15 +30343,15 @@ function makeCiCommand() {
|
|
|
30338
30343
|
}
|
|
30339
30344
|
|
|
30340
30345
|
// src/commands/init/doctor.ts
|
|
30341
|
-
import { existsSync as
|
|
30346
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
30342
30347
|
import { join as join24 } from "node:path";
|
|
30343
30348
|
|
|
30344
30349
|
// ../../packages/sdk/src/client.ts
|
|
30345
|
-
import { isAbsolute as
|
|
30350
|
+
import { isAbsolute as isAbsolute4, join as join19 } from "node:path";
|
|
30346
30351
|
|
|
30347
30352
|
// ../../packages/sdk/src/cases.ts
|
|
30348
|
-
import { mkdir as mkdir3, readFile as
|
|
30349
|
-
import { dirname, extname, isAbsolute, join as join6, resolve as
|
|
30353
|
+
import { mkdir as mkdir3, readFile as readFile4, readdir, stat as stat2, writeFile as writeFile3 } from "node:fs/promises";
|
|
30354
|
+
import { dirname as dirname2, extname as extname2, isAbsolute as isAbsolute2, join as join6, resolve as resolve3 } from "node:path";
|
|
30350
30355
|
|
|
30351
30356
|
// ../../packages/dsl/src/schema/index.ts
|
|
30352
30357
|
var STEP_TARGETS = ["web", "miniapp", "api"];
|
|
@@ -30367,6 +30372,12 @@ var locatorSchema = external_exports.strictObject({
|
|
|
30367
30372
|
text: external_exports.string().min(1).optional(),
|
|
30368
30373
|
css: external_exports.string().min(1).optional()
|
|
30369
30374
|
}).describe("text \u4E0E css \u53EA\u80FD\u63D0\u4F9B\u5176\u4E00");
|
|
30375
|
+
var requestExpectationSchema = external_exports.strictObject({
|
|
30376
|
+
method: external_exports.enum(HTTP_METHODS).optional(),
|
|
30377
|
+
urlContains: external_exports.string().min(1),
|
|
30378
|
+
count: external_exports.number().int().nonnegative(),
|
|
30379
|
+
windowMs: external_exports.number().int().nonnegative().default(1e3)
|
|
30380
|
+
});
|
|
30370
30381
|
var stepSchema = external_exports.strictObject({
|
|
30371
30382
|
target: external_exports.enum(STEP_TARGETS),
|
|
30372
30383
|
action: external_exports.enum(ACTIONS),
|
|
@@ -30381,8 +30392,18 @@ var stepSchema = external_exports.strictObject({
|
|
|
30381
30392
|
/** api target:请求体;对象按 JSON 序列化,字符串原样发送(支持 ${var} 模板) */
|
|
30382
30393
|
body: external_exports.union([external_exports.string(), external_exports.record(external_exports.string(), external_exports.unknown())]).optional(),
|
|
30383
30394
|
variable: external_exports.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/, "\u53D8\u91CF\u540D\u9700\u4EE5\u5B57\u6BCD\u6216\u4E0B\u5212\u7EBF\u5F00\u5934,\u4EC5\u542B\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u4E0B\u5212\u7EBF").optional(),
|
|
30384
|
-
timeout: external_exports.number().int().positive().optional()
|
|
30395
|
+
timeout: external_exports.number().int().positive().optional(),
|
|
30396
|
+
/** UI 动作触发的网络请求精确次数断言 */
|
|
30397
|
+
expectRequests: external_exports.array(requestExpectationSchema).min(1).optional()
|
|
30398
|
+
});
|
|
30399
|
+
var datasetReferenceSchema = external_exports.strictObject({
|
|
30400
|
+
file: external_exports.string().min(1),
|
|
30401
|
+
idField: external_exports.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/, "idField \u9700\u4E3A\u5408\u6CD5\u5B57\u6BB5\u540D").default("id")
|
|
30385
30402
|
});
|
|
30403
|
+
var dependencyPathSchema = external_exports.string().regex(
|
|
30404
|
+
/^(fixture|dataset|account|variable)(?:\.[A-Za-z_][A-Za-z0-9_]*)+$/,
|
|
30405
|
+
"requires \u8DEF\u5F84\u5FC5\u987B\u4EE5 fixture/dataset/account/variable \u5F00\u5934\u5E76\u5305\u542B\u5B57\u6BB5"
|
|
30406
|
+
);
|
|
30386
30407
|
var caseSchema = external_exports.strictObject({
|
|
30387
30408
|
id: external_exports.string().regex(/^[a-z0-9][a-z0-9-]*$/, "id \u9700\u4E3A kebab-case(\u5C0F\u5199\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u8FDE\u5B57\u7B26)"),
|
|
30388
30409
|
name: external_exports.string().min(1, "name \u4E0D\u80FD\u4E3A\u7A7A"),
|
|
@@ -30394,7 +30415,12 @@ var caseSchema = external_exports.strictObject({
|
|
|
30394
30415
|
/** 引用测试账号标识;凭据由 Environment + Server/CI Secret 管理,禁止写进 Case */
|
|
30395
30416
|
accountRef: external_exports.string().min(1).optional(),
|
|
30396
30417
|
tags: external_exports.array(external_exports.string().min(1)).optional(),
|
|
30397
|
-
|
|
30418
|
+
fixtures: external_exports.array(external_exports.string().min(1)).min(1).optional(),
|
|
30419
|
+
datasets: datasetReferenceSchema.optional(),
|
|
30420
|
+
requires: external_exports.array(dependencyPathSchema).min(1).optional(),
|
|
30421
|
+
setup: external_exports.array(stepSchema).min(1).optional(),
|
|
30422
|
+
steps: external_exports.array(stepSchema).min(1, "Case \u81F3\u5C11\u5305\u542B\u4E00\u4E2A step"),
|
|
30423
|
+
teardown: external_exports.array(stepSchema).min(1).optional()
|
|
30398
30424
|
});
|
|
30399
30425
|
|
|
30400
30426
|
// ../../packages/dsl/src/parser/index.ts
|
|
@@ -30445,6 +30471,7 @@ var ADAPTER_SUPPORT = {
|
|
|
30445
30471
|
};
|
|
30446
30472
|
var LOCATOR_REQUIRED = /* @__PURE__ */ new Set(["click", "input", "select", "assert", "extract"]);
|
|
30447
30473
|
var VALUE_REQUIRED = /* @__PURE__ */ new Set(["input", "select"]);
|
|
30474
|
+
var REQUEST_EXPECTATION_ACTIONS = /* @__PURE__ */ new Set(["navigate", "click", "input", "select"]);
|
|
30448
30475
|
var FIELD_ALLOWED = {
|
|
30449
30476
|
url: ["navigate", "request"],
|
|
30450
30477
|
value: ["input", "select", "extract"],
|
|
@@ -30459,12 +30486,21 @@ var VARIABLE_REF = /\$\{([A-Za-z_][A-Za-z0-9_.]*)\}/g;
|
|
|
30459
30486
|
function validateCase(data) {
|
|
30460
30487
|
const issues = [];
|
|
30461
30488
|
const declared = /* @__PURE__ */ new Set();
|
|
30489
|
+
if (data.fixtures) declared.add("fixture");
|
|
30490
|
+
if (data.datasets) declared.add("dataset");
|
|
30491
|
+
declared.add("variable");
|
|
30462
30492
|
if (data.accountRef) {
|
|
30463
30493
|
declared.add("account");
|
|
30464
30494
|
}
|
|
30495
|
+
data.setup?.forEach((step, index) => {
|
|
30496
|
+
checkStep(step, `setup[${index}]`, declared, issues);
|
|
30497
|
+
});
|
|
30465
30498
|
data.steps.forEach((step, index) => {
|
|
30466
30499
|
checkStep(step, `steps[${index}]`, declared, issues);
|
|
30467
30500
|
});
|
|
30501
|
+
data.teardown?.forEach((step, index) => {
|
|
30502
|
+
checkStep(step, `teardown[${index}]`, declared, issues);
|
|
30503
|
+
});
|
|
30468
30504
|
return issues;
|
|
30469
30505
|
}
|
|
30470
30506
|
function validateCaseSource(source) {
|
|
@@ -30539,6 +30575,15 @@ function checkStep(step, at, declared, issues) {
|
|
|
30539
30575
|
if (step.action === "wait" && !step.locator && step.timeout === void 0) {
|
|
30540
30576
|
issues.push({ path: at, code: "semantic", message: "wait \u9700\u8981\u63D0\u4F9B locator(\u7B49\u5F85\u5143\u7D20)\u6216 timeout(\u6BEB\u79D2)" });
|
|
30541
30577
|
}
|
|
30578
|
+
if (step.expectRequests) {
|
|
30579
|
+
if (step.target === "api" || !REQUEST_EXPECTATION_ACTIONS.has(step.action)) {
|
|
30580
|
+
issues.push({
|
|
30581
|
+
path: `${at}.expectRequests`,
|
|
30582
|
+
code: "semantic",
|
|
30583
|
+
message: "expectRequests \u4EC5\u7528\u4E8E web/miniapp \u7684 navigate/click/input/select"
|
|
30584
|
+
});
|
|
30585
|
+
}
|
|
30586
|
+
}
|
|
30542
30587
|
checkVariableRefs(step, at, declared, issues);
|
|
30543
30588
|
}
|
|
30544
30589
|
function checkLocator(step, at, issues) {
|
|
@@ -30571,9 +30616,14 @@ function checkVariableRefs(step, at, declared, issues) {
|
|
|
30571
30616
|
collect("value", step.value);
|
|
30572
30617
|
collect("expected", step.expected);
|
|
30573
30618
|
collect("url", step.url);
|
|
30619
|
+
collect("locator.text", step.locator?.text);
|
|
30620
|
+
collect("locator.css", step.locator?.css);
|
|
30574
30621
|
if (typeof step.body === "string") collect("body", step.body);
|
|
30575
30622
|
else if (step.body) collect("body", JSON.stringify(step.body));
|
|
30576
30623
|
for (const headerValue of Object.values(step.headers ?? {})) collect("headers", headerValue);
|
|
30624
|
+
for (const [index, expectation] of (step.expectRequests ?? []).entries()) {
|
|
30625
|
+
collect(`expectRequests[${index}].urlContains`, expectation.urlContains);
|
|
30626
|
+
}
|
|
30577
30627
|
for (const [field, name] of refs) {
|
|
30578
30628
|
const root = name.split(".")[0] ?? name;
|
|
30579
30629
|
if (!declared.has(root)) {
|
|
@@ -30588,9 +30638,20 @@ function checkVariableRefs(step, at, declared, issues) {
|
|
|
30588
30638
|
|
|
30589
30639
|
// ../../packages/sdk/src/project.ts
|
|
30590
30640
|
import { mkdir as mkdir2, readFile as readFile2, stat, writeFile as writeFile2 } from "node:fs/promises";
|
|
30591
|
-
import {
|
|
30641
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
30642
|
+
import { dirname, join as join5, resolve } from "node:path";
|
|
30592
30643
|
function resolveRoot(root) {
|
|
30593
|
-
|
|
30644
|
+
const start = resolve(root ?? process.cwd());
|
|
30645
|
+
if (root) return start;
|
|
30646
|
+
let current = start;
|
|
30647
|
+
for (; ; ) {
|
|
30648
|
+
if (existsSync2(join5(current, "testpilot.yaml")) || existsSync2(join5(current, TESTPILOT_DIR))) {
|
|
30649
|
+
return current;
|
|
30650
|
+
}
|
|
30651
|
+
const parent = dirname(current);
|
|
30652
|
+
if (parent === current) return start;
|
|
30653
|
+
current = parent;
|
|
30654
|
+
}
|
|
30594
30655
|
}
|
|
30595
30656
|
async function loadProjectConfig(root) {
|
|
30596
30657
|
return loadTestpilotConfig(resolveRoot(root));
|
|
@@ -30639,6 +30700,164 @@ async function writeProjectLink(link, root) {
|
|
|
30639
30700
|
return file2;
|
|
30640
30701
|
}
|
|
30641
30702
|
|
|
30703
|
+
// ../../packages/sdk/src/case-data.ts
|
|
30704
|
+
var import_yaml3 = __toESM(require_dist(), 1);
|
|
30705
|
+
import { readFile as readFile3, realpath } from "node:fs/promises";
|
|
30706
|
+
import { extname, isAbsolute, relative, resolve as resolve2 } from "node:path";
|
|
30707
|
+
async function loadCaseDataFile(reference, options) {
|
|
30708
|
+
if (isAbsolute(reference)) {
|
|
30709
|
+
throw new Error(`\u6570\u636E\u6587\u4EF6\u5FC5\u987B\u4F7F\u7528\u9879\u76EE\u76F8\u5BF9\u8DEF\u5F84:${reference}`);
|
|
30710
|
+
}
|
|
30711
|
+
const root = resolve2(options.root);
|
|
30712
|
+
const dataDir = resolve2(options.dataDir);
|
|
30713
|
+
const target = resolve2(root, reference);
|
|
30714
|
+
if (!isInside(dataDir, target)) {
|
|
30715
|
+
throw new Error(`\u6570\u636E\u6587\u4EF6\u5FC5\u987B\u4F4D\u4E8E tests/e2e/data \u76EE\u5F55:${reference}`);
|
|
30716
|
+
}
|
|
30717
|
+
const [realDataDir, realTarget] = await Promise.all([
|
|
30718
|
+
realpath(dataDir).catch(() => dataDir),
|
|
30719
|
+
realpath(target).catch((error62) => {
|
|
30720
|
+
throw new Error(
|
|
30721
|
+
`\u8BFB\u53D6\u6570\u636E\u6587\u4EF6\u5931\u8D25:${reference}:${error62 instanceof Error ? error62.message : String(error62)}`
|
|
30722
|
+
);
|
|
30723
|
+
})
|
|
30724
|
+
]);
|
|
30725
|
+
if (!isInside(realDataDir, realTarget)) {
|
|
30726
|
+
throw new Error(`\u6570\u636E\u6587\u4EF6\u89E3\u6790\u540E\u8D8A\u51FA tests/e2e/data \u76EE\u5F55:${reference}`);
|
|
30727
|
+
}
|
|
30728
|
+
const source = await readFile3(realTarget, "utf8");
|
|
30729
|
+
const extension = extname(realTarget).toLowerCase();
|
|
30730
|
+
try {
|
|
30731
|
+
if (extension === ".yaml" || extension === ".yml") return (0, import_yaml3.parse)(source);
|
|
30732
|
+
if (extension === ".json") return JSON.parse(source);
|
|
30733
|
+
} catch (error62) {
|
|
30734
|
+
throw new Error(
|
|
30735
|
+
`\u89E3\u6790\u6570\u636E\u6587\u4EF6\u5931\u8D25:${reference}:${error62 instanceof Error ? error62.message : String(error62)}`
|
|
30736
|
+
);
|
|
30737
|
+
}
|
|
30738
|
+
throw new Error(`\u6570\u636E\u6587\u4EF6\u4EC5\u652F\u6301 .yaml/.yml/.json:${reference}`);
|
|
30739
|
+
}
|
|
30740
|
+
function isDataObject(value) {
|
|
30741
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
30742
|
+
}
|
|
30743
|
+
function mergeFixture(target, source, at = "fixture") {
|
|
30744
|
+
const output2 = { ...target };
|
|
30745
|
+
for (const [key, value] of Object.entries(source)) {
|
|
30746
|
+
const path = `${at}.${key}`;
|
|
30747
|
+
if (!(key in output2)) {
|
|
30748
|
+
output2[key] = value;
|
|
30749
|
+
continue;
|
|
30750
|
+
}
|
|
30751
|
+
const current = output2[key];
|
|
30752
|
+
if (isDataObject(current) && isDataObject(value)) {
|
|
30753
|
+
output2[key] = mergeFixture(current, value, path);
|
|
30754
|
+
continue;
|
|
30755
|
+
}
|
|
30756
|
+
throw new Error(`fixture \u5B57\u6BB5\u51B2\u7A81:${path}`);
|
|
30757
|
+
}
|
|
30758
|
+
return output2;
|
|
30759
|
+
}
|
|
30760
|
+
function flattenVariables(prefix, value) {
|
|
30761
|
+
const output2 = {};
|
|
30762
|
+
visit2(prefix, value, output2);
|
|
30763
|
+
return output2;
|
|
30764
|
+
}
|
|
30765
|
+
function visit2(path, value, output2) {
|
|
30766
|
+
if (value === null || value === void 0) return;
|
|
30767
|
+
if (typeof value === "object") {
|
|
30768
|
+
output2[path] = JSON.stringify(value);
|
|
30769
|
+
if (Array.isArray(value)) {
|
|
30770
|
+
value.forEach((item, index) => visit2(`${path}.${index}`, item, output2));
|
|
30771
|
+
} else {
|
|
30772
|
+
for (const [key, item] of Object.entries(value)) {
|
|
30773
|
+
visit2(`${path}.${key}`, item, output2);
|
|
30774
|
+
}
|
|
30775
|
+
}
|
|
30776
|
+
return;
|
|
30777
|
+
}
|
|
30778
|
+
output2[path] = String(value);
|
|
30779
|
+
}
|
|
30780
|
+
function isInside(base, candidate) {
|
|
30781
|
+
const path = relative(base, candidate);
|
|
30782
|
+
return path === "" || !path.startsWith("..") && !isAbsolute(path);
|
|
30783
|
+
}
|
|
30784
|
+
|
|
30785
|
+
// ../../packages/sdk/src/case-preparation.ts
|
|
30786
|
+
async function prepareCaseExecutions(item, options) {
|
|
30787
|
+
const fixture = await loadFixtures(item.data.fixtures ?? [], options);
|
|
30788
|
+
const rows = await loadDataset(item.data, options);
|
|
30789
|
+
const projectVariables = flattenVariables("variable", options.variables ?? {});
|
|
30790
|
+
const accountVariables = accountValues(item.data, options.accounts);
|
|
30791
|
+
return rows.map(({ row, rowId }, rowIndex) => {
|
|
30792
|
+
const initialVariables = {
|
|
30793
|
+
...flattenVariables("fixture", fixture),
|
|
30794
|
+
...flattenVariables("dataset", row),
|
|
30795
|
+
...projectVariables,
|
|
30796
|
+
...accountVariables
|
|
30797
|
+
};
|
|
30798
|
+
const missingDependencies = (item.data.requires ?? []).filter(
|
|
30799
|
+
(path) => isUnavailable(initialVariables[path])
|
|
30800
|
+
);
|
|
30801
|
+
return {
|
|
30802
|
+
data: item.data,
|
|
30803
|
+
file: item.file,
|
|
30804
|
+
templateId: item.data.id,
|
|
30805
|
+
rowId,
|
|
30806
|
+
rowIndex,
|
|
30807
|
+
initialVariables,
|
|
30808
|
+
missingDependencies
|
|
30809
|
+
};
|
|
30810
|
+
});
|
|
30811
|
+
}
|
|
30812
|
+
async function loadFixtures(references, options) {
|
|
30813
|
+
let merged = {};
|
|
30814
|
+
for (const reference of references) {
|
|
30815
|
+
const value = await loadCaseDataFile(reference, options);
|
|
30816
|
+
if (!isDataObject(value)) {
|
|
30817
|
+
throw new Error(`fixture \u6587\u4EF6\u6839\u5FC5\u987B\u662F\u5BF9\u8C61:${reference}`);
|
|
30818
|
+
}
|
|
30819
|
+
merged = mergeFixture(merged, value);
|
|
30820
|
+
}
|
|
30821
|
+
return merged;
|
|
30822
|
+
}
|
|
30823
|
+
async function loadDataset(data, options) {
|
|
30824
|
+
if (!data.datasets) return [{ row: {}, rowId: "default" }];
|
|
30825
|
+
const value = await loadCaseDataFile(data.datasets.file, options);
|
|
30826
|
+
if (!Array.isArray(value)) {
|
|
30827
|
+
throw new Error(`dataset \u6587\u4EF6\u6839\u5FC5\u987B\u662F\u5BF9\u8C61\u6570\u7EC4:${data.datasets.file}`);
|
|
30828
|
+
}
|
|
30829
|
+
const seen = /* @__PURE__ */ new Set();
|
|
30830
|
+
return value.map((row, index) => {
|
|
30831
|
+
if (!isDataObject(row)) {
|
|
30832
|
+
throw new Error(`dataset \u7B2C ${index + 1} \u884C\u5FC5\u987B\u662F\u5BF9\u8C61:${data.datasets.file}`);
|
|
30833
|
+
}
|
|
30834
|
+
const rawId = row[data.datasets.idField];
|
|
30835
|
+
if (typeof rawId !== "string" && typeof rawId !== "number" || String(rawId).trim() === "") {
|
|
30836
|
+
throw new Error(
|
|
30837
|
+
`dataset \u7B2C ${index + 1} \u884C\u7F3A\u5C11\u975E\u7A7A row id \u5B57\u6BB5 "${data.datasets.idField}"`
|
|
30838
|
+
);
|
|
30839
|
+
}
|
|
30840
|
+
const rowId = String(rawId);
|
|
30841
|
+
if (seen.has(rowId)) throw new Error(`dataset row id \u91CD\u590D:${rowId}`);
|
|
30842
|
+
seen.add(rowId);
|
|
30843
|
+
return { row, rowId };
|
|
30844
|
+
});
|
|
30845
|
+
}
|
|
30846
|
+
function accountValues(data, accounts) {
|
|
30847
|
+
if (!data.accountRef) return {};
|
|
30848
|
+
const raw = accounts?.[data.accountRef];
|
|
30849
|
+
if (raw === void 0) return {};
|
|
30850
|
+
try {
|
|
30851
|
+
const parsed = JSON.parse(raw);
|
|
30852
|
+
if (isDataObject(parsed)) return flattenVariables("account", parsed);
|
|
30853
|
+
} catch {
|
|
30854
|
+
}
|
|
30855
|
+
return { account: raw };
|
|
30856
|
+
}
|
|
30857
|
+
function isUnavailable(value) {
|
|
30858
|
+
return value === void 0 || value.trim() === "" || /\$\{[A-Za-z_][A-Za-z0-9_]*\}/.test(value);
|
|
30859
|
+
}
|
|
30860
|
+
|
|
30642
30861
|
// ../../packages/sdk/src/cases.ts
|
|
30643
30862
|
async function collectCaseFiles(paths) {
|
|
30644
30863
|
const files = [];
|
|
@@ -30667,19 +30886,20 @@ async function collectFromDir(dir) {
|
|
|
30667
30886
|
return found;
|
|
30668
30887
|
}
|
|
30669
30888
|
function isCaseFile(file2) {
|
|
30670
|
-
const ext =
|
|
30889
|
+
const ext = extname2(file2).toLowerCase();
|
|
30671
30890
|
return ext === ".yaml" || ext === ".yml";
|
|
30672
30891
|
}
|
|
30673
30892
|
function resolveAgainstRoot(root, file2) {
|
|
30674
|
-
return
|
|
30893
|
+
return isAbsolute2(file2) ? file2 : resolve3(resolveRoot(root), file2);
|
|
30675
30894
|
}
|
|
30676
30895
|
async function collectCases(paths, options = {}) {
|
|
30677
30896
|
const files = await collectCaseFiles(
|
|
30678
30897
|
paths.map((input2) => resolveAgainstRoot(options.root, input2))
|
|
30679
30898
|
);
|
|
30680
30899
|
const infos = [];
|
|
30900
|
+
const root = resolveRoot(options.root);
|
|
30681
30901
|
for (const file2 of files) {
|
|
30682
|
-
const validation = await validateCaseFile(file2);
|
|
30902
|
+
const validation = await validateCaseFile(file2, root);
|
|
30683
30903
|
infos.push({
|
|
30684
30904
|
file: file2,
|
|
30685
30905
|
valid: validation.ok,
|
|
@@ -30691,16 +30911,16 @@ async function collectCases(paths, options = {}) {
|
|
|
30691
30911
|
}
|
|
30692
30912
|
async function readCaseFile(file2, options = {}) {
|
|
30693
30913
|
const target = resolveAgainstRoot(options.root, file2);
|
|
30694
|
-
const source = await
|
|
30695
|
-
return { file: target, source, validation:
|
|
30914
|
+
const source = await readFile4(target, "utf8");
|
|
30915
|
+
return { file: target, source, validation: await validateCaseDataSource(source, target, resolveRoot(options.root)) };
|
|
30696
30916
|
}
|
|
30697
|
-
async function validateCaseFile(file2) {
|
|
30698
|
-
const source = await
|
|
30699
|
-
return
|
|
30917
|
+
async function validateCaseFile(file2, root) {
|
|
30918
|
+
const source = await readFile4(file2, "utf8");
|
|
30919
|
+
return validateCaseDataSource(source, file2, root);
|
|
30700
30920
|
}
|
|
30701
30921
|
async function writeCaseFile(file2, source, options = {}) {
|
|
30702
30922
|
const target = resolveAgainstRoot(options.root, file2);
|
|
30703
|
-
const validation =
|
|
30923
|
+
const validation = await validateCaseDataSource(source, target, resolveRoot(options.root));
|
|
30704
30924
|
if (!validation.ok) {
|
|
30705
30925
|
return { file: target, written: false, validation };
|
|
30706
30926
|
}
|
|
@@ -30710,18 +30930,41 @@ async function writeCaseFile(file2, source, options = {}) {
|
|
|
30710
30930
|
throw new Error(`\u7528\u4F8B\u6587\u4EF6\u5DF2\u5B58\u5728:${target}(\u8986\u76D6\u9700\u663E\u5F0F overwrite: true)`);
|
|
30711
30931
|
}
|
|
30712
30932
|
}
|
|
30713
|
-
await mkdir3(
|
|
30933
|
+
await mkdir3(dirname2(target), { recursive: true });
|
|
30714
30934
|
await writeFile3(target, source, "utf8");
|
|
30715
30935
|
return { file: target, written: true, validation };
|
|
30716
30936
|
}
|
|
30937
|
+
async function validateCaseDataSource(source, file2, root) {
|
|
30938
|
+
const validation = validateCaseSource(source);
|
|
30939
|
+
if (!validation.ok || !validation.data) return validation;
|
|
30940
|
+
try {
|
|
30941
|
+
await prepareCaseExecutions(
|
|
30942
|
+
{ data: validation.data, file: file2 },
|
|
30943
|
+
{ root, dataDir: join6(root, "tests", "e2e", "data") }
|
|
30944
|
+
);
|
|
30945
|
+
return validation;
|
|
30946
|
+
} catch (error62) {
|
|
30947
|
+
return {
|
|
30948
|
+
ok: false,
|
|
30949
|
+
data: validation.data,
|
|
30950
|
+
issues: [
|
|
30951
|
+
{
|
|
30952
|
+
path: "(root)",
|
|
30953
|
+
code: "semantic",
|
|
30954
|
+
message: error62 instanceof Error ? error62.message : String(error62)
|
|
30955
|
+
}
|
|
30956
|
+
]
|
|
30957
|
+
};
|
|
30958
|
+
}
|
|
30959
|
+
}
|
|
30717
30960
|
|
|
30718
30961
|
// ../../packages/sdk/src/reports.ts
|
|
30719
|
-
import { readFile as
|
|
30962
|
+
import { readFile as readFile8 } from "node:fs/promises";
|
|
30720
30963
|
import { join as join18 } from "node:path";
|
|
30721
30964
|
|
|
30722
30965
|
// ../../packages/reporter/src/report.ts
|
|
30723
30966
|
import { writeFile as writeFile4 } from "node:fs/promises";
|
|
30724
|
-
import { dirname as
|
|
30967
|
+
import { dirname as dirname3, join as join8, resolve as resolve4 } from "node:path";
|
|
30725
30968
|
|
|
30726
30969
|
// ../../packages/reporter/src/classify.ts
|
|
30727
30970
|
var FAILURE_CATEGORIES = [
|
|
@@ -30751,14 +30994,14 @@ function classifyFailure(message) {
|
|
|
30751
30994
|
}
|
|
30752
30995
|
|
|
30753
30996
|
// ../../packages/reporter/src/history.ts
|
|
30754
|
-
import { readFile as
|
|
30997
|
+
import { readFile as readFile5, readdir as readdir2 } from "node:fs/promises";
|
|
30755
30998
|
import { join as join7 } from "node:path";
|
|
30756
30999
|
async function collectHistory(runsRoot2, skipRunId, limit = 12) {
|
|
30757
31000
|
const entries = (await readdir2(runsRoot2).catch(() => [])).filter((name) => /^\d{8}-\d{3}$/.test(name) && name !== skipRunId).sort().slice(-limit);
|
|
30758
31001
|
const points = [];
|
|
30759
31002
|
for (const runId of entries) {
|
|
30760
31003
|
try {
|
|
30761
|
-
const summary = JSON.parse(await
|
|
31004
|
+
const summary = JSON.parse(await readFile5(join7(runsRoot2, runId, "result.json"), "utf8"));
|
|
30762
31005
|
points.push({
|
|
30763
31006
|
runId,
|
|
30764
31007
|
startedAt: summary.startedAt,
|
|
@@ -30779,12 +31022,19 @@ function toSummaryView(summary) {
|
|
|
30779
31022
|
runId: summary.runId,
|
|
30780
31023
|
status: summary.status,
|
|
30781
31024
|
durationMs: summary.durationMs,
|
|
30782
|
-
cases: {
|
|
31025
|
+
cases: {
|
|
31026
|
+
templates: summary.totals.templates ?? summary.totals.cases,
|
|
31027
|
+
passed: summary.totals.passed,
|
|
31028
|
+
failed: summary.totals.failed,
|
|
31029
|
+
skipped: summary.totals.skipped ?? 0,
|
|
31030
|
+
warnings: summary.totals.warnings ?? 0
|
|
31031
|
+
},
|
|
30783
31032
|
steps: {
|
|
30784
31033
|
total: summary.totals.steps,
|
|
30785
31034
|
passed: summary.totals.stepsPassed,
|
|
30786
31035
|
failed: summary.totals.stepsFailed,
|
|
30787
|
-
skipped: summary.totals.stepsSkipped
|
|
31036
|
+
skipped: summary.totals.stepsSkipped,
|
|
31037
|
+
warning: summary.totals.stepsWarning ?? 0
|
|
30788
31038
|
}
|
|
30789
31039
|
};
|
|
30790
31040
|
}
|
|
@@ -30792,9 +31042,9 @@ function toSummaryView(summary) {
|
|
|
30792
31042
|
// ../../packages/reporter/src/report.ts
|
|
30793
31043
|
var HISTORY_LIMIT = 12;
|
|
30794
31044
|
async function writeReports(runDir, summary, options = {}) {
|
|
30795
|
-
const dir =
|
|
31045
|
+
const dir = resolve4(runDir);
|
|
30796
31046
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
30797
|
-
const history = options.history ?? await collectHistory(
|
|
31047
|
+
const history = options.history ?? await collectHistory(dirname3(dir), summary.runId, options.historyLimit ?? HISTORY_LIMIT);
|
|
30798
31048
|
const failures = [];
|
|
30799
31049
|
const cases = summary.cases.map((item) => {
|
|
30800
31050
|
const categories = /* @__PURE__ */ new Set();
|
|
@@ -30853,7 +31103,7 @@ function renderHtml(data) {
|
|
|
30853
31103
|
const view = toSummaryView(data.summary);
|
|
30854
31104
|
const summary = data.summary;
|
|
30855
31105
|
const trendSvg = renderTrend(data);
|
|
30856
|
-
const casesHtml = summary.cases
|
|
31106
|
+
const casesHtml = groupedCasesHtml(summary.cases);
|
|
30857
31107
|
const chips = categoryChips(data);
|
|
30858
31108
|
return `<!doctype html>
|
|
30859
31109
|
<html lang="zh-CN">
|
|
@@ -30861,13 +31111,13 @@ function renderHtml(data) {
|
|
|
30861
31111
|
<meta charset="utf-8" />
|
|
30862
31112
|
<title>TestPilot Report ${escapeHtml(summary.runId)}</title>
|
|
30863
31113
|
<style>
|
|
30864
|
-
:root { --ok:#1a7f37; --fail:#cf222e; --skip:#6e7781; --line:#d0d7de; --muted:#59636e; }
|
|
31114
|
+
:root { --ok:#1a7f37; --fail:#cf222e; --warn:#9a6700; --skip:#6e7781; --line:#d0d7de; --muted:#59636e; }
|
|
30865
31115
|
* { box-sizing: border-box; }
|
|
30866
31116
|
body { font-family: system-ui, "Segoe UI", sans-serif; margin: 0; color: #1f2328; background: #f6f8fa; }
|
|
30867
31117
|
.wrap { max-width: 1080px; margin: 0 auto; padding: 20px 16px 48px; }
|
|
30868
31118
|
h1 { font-size: 20px; margin: 8px 0; } h2 { font-size: 15px; margin: 24px 0 8px; }
|
|
30869
31119
|
.badge { display: inline-block; padding: 2px 10px; border-radius: 10px; color: #fff; font-size: 12px; }
|
|
30870
|
-
.passed { background: var(--ok); } .failed { background: var(--fail); } .skipped { background: var(--skip); }
|
|
31120
|
+
.passed { background: var(--ok); } .failed { background: var(--fail); } .warning { background: var(--warn); } .skipped { background: var(--skip); }
|
|
30871
31121
|
code { background: #eef1f4; padding: 1px 5px; border-radius: 4px; font-size: 12px; }
|
|
30872
31122
|
.cards { display: flex; gap: 12px; flex-wrap: wrap; margin: 14px 0; }
|
|
30873
31123
|
.card { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: 10px 16px; min-width: 130px; }
|
|
@@ -30880,6 +31130,8 @@ function renderHtml(data) {
|
|
|
30880
31130
|
.btn { border: 1px solid var(--line); background: #fff; border-radius: 6px; padding: 4px 10px; font-size: 12px; cursor: pointer; }
|
|
30881
31131
|
#count { color: var(--muted); font-size: 12px; margin-left: auto; }
|
|
30882
31132
|
details.case { background: #fff; border: 1px solid var(--line); border-radius: 8px; margin: 10px 0; }
|
|
31133
|
+
section.template { border-left: 3px solid var(--line); padding-left: 12px; margin: 14px 0; }
|
|
31134
|
+
section.template > h3 { font-size: 14px; margin: 0 0 6px; }
|
|
30883
31135
|
details.case > summary { cursor: pointer; padding: 10px 14px; font-size: 14px; list-style: none; }
|
|
30884
31136
|
details.case > summary::-webkit-details-marker { display: none; }
|
|
30885
31137
|
details.case > summary:hover { background: #f6f8fa; }
|
|
@@ -30902,6 +31154,8 @@ function renderHtml(data) {
|
|
|
30902
31154
|
<div class="cards">
|
|
30903
31155
|
<div class="card"><b>${view.cases.passed}<span style="color:var(--ok)"> \u2713</span></b><span>\u901A\u8FC7\u7528\u4F8B</span></div>
|
|
30904
31156
|
<div class="card"><b>${view.cases.failed}<span style="color:var(--fail)"> \u2717</span></b><span>\u5931\u8D25\u7528\u4F8B</span></div>
|
|
31157
|
+
<div class="card"><b>${view.cases.skipped} \u21B7</b><span>\u8DF3\u8FC7\u6570\u636E\u884C</span></div>
|
|
31158
|
+
<div class="card"><b>${view.cases.warnings} \u26A0</b><span>\u6E05\u7406\u8B66\u544A</span></div>
|
|
30905
31159
|
<div class="card"><b>${view.steps.passed}<span style="color:var(--ok)"> \u2713</span></b><span>\u901A\u8FC7\u6B65\u9AA4</span></div>
|
|
30906
31160
|
<div class="card"><b>${view.steps.failed}<span style="color:var(--fail)"> \u2717</span></b><span>\u5931\u8D25\u6B65\u9AA4</span></div>
|
|
30907
31161
|
<div class="card"><b>${view.steps.skipped} \u21B7</b><span>\u8DF3\u8FC7\u6B65\u9AA4</span></div>
|
|
@@ -30988,26 +31242,44 @@ function renderHtml(data) {
|
|
|
30988
31242
|
</body>
|
|
30989
31243
|
</html>`;
|
|
30990
31244
|
}
|
|
31245
|
+
function groupedCasesHtml(cases) {
|
|
31246
|
+
const groups = /* @__PURE__ */ new Map();
|
|
31247
|
+
for (const item of cases) {
|
|
31248
|
+
const current = groups.get(item.caseId) ?? [];
|
|
31249
|
+
current.push(item);
|
|
31250
|
+
groups.set(item.caseId, current);
|
|
31251
|
+
}
|
|
31252
|
+
return [...groups.entries()].map(
|
|
31253
|
+
([caseId, rows]) => `<section class="template">
|
|
31254
|
+
<h3>${escapeHtml(caseId)} <span class="muted">\xB7 ${rows.length} \u4E2A\u6570\u636E\u884C</span></h3>
|
|
31255
|
+
${rows.map((item) => caseHtml(item)).join("\n")}
|
|
31256
|
+
</section>`
|
|
31257
|
+
).join("\n");
|
|
31258
|
+
}
|
|
30991
31259
|
function caseHtml(item) {
|
|
30992
31260
|
const rows = item.steps.map((step) => stepHtml(step)).join("\n");
|
|
30993
31261
|
const errorLine = item.error ? `<p class="error">${escapeHtml(item.error)}</p>` : "";
|
|
31262
|
+
const skipLine = item.skipReason ? `<p class="muted">${escapeHtml(item.skipReason)}${item.missingDependencies?.length ? `: ${escapeHtml(item.missingDependencies.join(", "))}` : ""}</p>` : "";
|
|
31263
|
+
const warningLine = item.warnings?.length ? `<p class="error">\u26A0 ${item.warnings.map(escapeHtml).join("<br />\u26A0 ")}</p>` : "";
|
|
30994
31264
|
const links = [];
|
|
30995
31265
|
if (item.video) links.push(`<a href="${escapeHtml(item.video)}">\u89C6\u9891</a>`);
|
|
30996
31266
|
if (item.trace) links.push(`<a href="${escapeHtml(item.trace)}">Trace</a>`);
|
|
30997
31267
|
const linksLine = links.length > 0 ? `<p>${links.join(" \xB7 ")}</p>` : "";
|
|
30998
|
-
const search = escapeHtml(`${item.caseId} ${item.caseName} ${item.file}`).replaceAll('"', """);
|
|
31268
|
+
const search = escapeHtml(`${item.caseId} ${item.rowId ?? "default"} ${item.caseName} ${item.file}`).replaceAll('"', """);
|
|
30999
31269
|
const cats = escapeHtml(item.categories.join(","));
|
|
31000
31270
|
return `
|
|
31001
31271
|
<details class="case" data-status="${item.status}" data-cats="${cats}" data-search="${search}">
|
|
31002
31272
|
<summary>
|
|
31003
31273
|
<span class="badge ${item.status}">${item.status}</span>
|
|
31004
|
-
<b
|
|
31274
|
+
<b>\u6570\u636E\u884C ${escapeHtml(item.rowId ?? "default")}</b> \xB7 ${escapeHtml(item.caseName)}
|
|
31005
31275
|
<span class="muted">\xB7 ${item.steps.length} \u6B65 \xB7 ${(item.durationMs / 1e3).toFixed(1)}s</span>
|
|
31006
31276
|
</summary>
|
|
31007
31277
|
${errorLine}
|
|
31278
|
+
${skipLine}
|
|
31279
|
+
${warningLine}
|
|
31008
31280
|
${linksLine}
|
|
31009
31281
|
<table>
|
|
31010
|
-
<tr><th>#</th><th>target</th><th>action</th><th>\u72B6\u6001</th><th>\u8017\u65F6</th><th>\u8BE6\u60C5</th></tr>
|
|
31282
|
+
<tr><th>#</th><th>\u9636\u6BB5</th><th>target</th><th>action</th><th>\u72B6\u6001</th><th>\u8017\u65F6</th><th>\u8BE6\u60C5</th></tr>
|
|
31011
31283
|
${rows}
|
|
31012
31284
|
</table>
|
|
31013
31285
|
</details>`;
|
|
@@ -31022,8 +31294,14 @@ function stepHtml(step) {
|
|
|
31022
31294
|
}
|
|
31023
31295
|
}
|
|
31024
31296
|
if (step.screenshot) details.push(`<a href="${escapeHtml(step.screenshot)}">\u622A\u56FE</a>`);
|
|
31297
|
+
for (const assertion of step.requestAssertions ?? []) {
|
|
31298
|
+
details.push(
|
|
31299
|
+
`\u8BF7\u6C42 ${escapeHtml(assertion.method ?? "*")} <code>${escapeHtml(assertion.urlContains)}</code>: ${assertion.actualCount}/${assertion.count}`
|
|
31300
|
+
);
|
|
31301
|
+
}
|
|
31025
31302
|
return `<tr>
|
|
31026
31303
|
<td class="num">${step.index}</td>
|
|
31304
|
+
<td>${escapeHtml(step.phase ?? "steps")}</td>
|
|
31027
31305
|
<td>${escapeHtml(step.target)}</td>
|
|
31028
31306
|
<td>${escapeHtml(step.action)}</td>
|
|
31029
31307
|
<td><span class="badge ${step.status}">${step.status}</span></td>
|
|
@@ -31080,7 +31358,7 @@ function embedJson(data) {
|
|
|
31080
31358
|
|
|
31081
31359
|
// ../../packages/reporter/src/summary-report.ts
|
|
31082
31360
|
import { mkdir as mkdir4, writeFile as writeFile5 } from "node:fs/promises";
|
|
31083
|
-
import { join as join9, resolve as
|
|
31361
|
+
import { join as join9, resolve as resolve5 } from "node:path";
|
|
31084
31362
|
function escapeHtml2(value) {
|
|
31085
31363
|
return String(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
31086
31364
|
}
|
|
@@ -31098,6 +31376,8 @@ function buildSummaryData(runs) {
|
|
|
31098
31376
|
executions.push({
|
|
31099
31377
|
runId,
|
|
31100
31378
|
caseId: item.caseId,
|
|
31379
|
+
rowId: item.rowId,
|
|
31380
|
+
identity: item.rowId && item.rowId !== "default" ? `${item.caseId}#${item.rowId}` : item.caseId,
|
|
31101
31381
|
caseName: item.caseName,
|
|
31102
31382
|
status: item.status,
|
|
31103
31383
|
category,
|
|
@@ -31115,10 +31395,11 @@ function buildSummaryData(runs) {
|
|
|
31115
31395
|
}
|
|
31116
31396
|
const byCase = /* @__PURE__ */ new Map();
|
|
31117
31397
|
for (const exec2 of executions) {
|
|
31118
|
-
let agg = byCase.get(exec2.
|
|
31398
|
+
let agg = byCase.get(exec2.identity);
|
|
31119
31399
|
if (!agg) {
|
|
31120
31400
|
agg = {
|
|
31121
31401
|
caseId: exec2.caseId,
|
|
31402
|
+
rowId: exec2.rowId,
|
|
31122
31403
|
caseName: exec2.caseName,
|
|
31123
31404
|
platforms: [],
|
|
31124
31405
|
runs: 0,
|
|
@@ -31128,17 +31409,18 @@ function buildSummaryData(runs) {
|
|
|
31128
31409
|
lastStatus: "unknown",
|
|
31129
31410
|
flaky: false
|
|
31130
31411
|
};
|
|
31131
|
-
byCase.set(exec2.
|
|
31412
|
+
byCase.set(exec2.identity, agg);
|
|
31132
31413
|
}
|
|
31133
31414
|
agg.runs++;
|
|
31134
31415
|
if (exec2.status === "passed") agg.passed++;
|
|
31135
|
-
else agg.failed++;
|
|
31416
|
+
else if (exec2.status === "failed") agg.failed++;
|
|
31136
31417
|
for (const platform of exec2.platforms) {
|
|
31137
31418
|
if (!agg.platforms.includes(platform)) agg.platforms.push(platform);
|
|
31138
31419
|
}
|
|
31139
31420
|
}
|
|
31140
31421
|
for (const agg of byCase.values()) {
|
|
31141
|
-
const
|
|
31422
|
+
const identity = agg.rowId && agg.rowId !== "default" ? `${agg.caseId}#${agg.rowId}` : agg.caseId;
|
|
31423
|
+
const last = [...executions].reverse().find((exec2) => exec2.identity === identity);
|
|
31142
31424
|
agg.lastStatus = last?.status ?? "unknown";
|
|
31143
31425
|
agg.lastCategory = last?.category;
|
|
31144
31426
|
agg.passRate = agg.runs > 0 ? agg.passed / agg.runs : 0;
|
|
@@ -31349,7 +31631,7 @@ function renderSummaryHtml(data) {
|
|
|
31349
31631
|
</html>`;
|
|
31350
31632
|
}
|
|
31351
31633
|
async function writeSummaryReport(artifactsDir, data) {
|
|
31352
|
-
const dir =
|
|
31634
|
+
const dir = resolve5(artifactsDir);
|
|
31353
31635
|
await mkdir4(dir, { recursive: true });
|
|
31354
31636
|
const htmlPath = join9(dir, "summary.html");
|
|
31355
31637
|
const jsonPath = join9(dir, "summary.json");
|
|
@@ -31359,8 +31641,8 @@ async function writeSummaryReport(artifactsDir, data) {
|
|
|
31359
31641
|
}
|
|
31360
31642
|
|
|
31361
31643
|
// ../../packages/sdk/src/runs.ts
|
|
31362
|
-
import { readFile as
|
|
31363
|
-
import { isAbsolute as
|
|
31644
|
+
import { readFile as readFile7, readdir as readdir4 } from "node:fs/promises";
|
|
31645
|
+
import { isAbsolute as isAbsolute3, join as join17 } from "node:path";
|
|
31364
31646
|
|
|
31365
31647
|
// ../../packages/execution-engine/src/artifacts/index.ts
|
|
31366
31648
|
import { mkdir as mkdir9 } from "node:fs/promises";
|
|
@@ -31368,7 +31650,7 @@ import { join as join13 } from "node:path";
|
|
|
31368
31650
|
|
|
31369
31651
|
// ../../packages/evidence/src/log.ts
|
|
31370
31652
|
import { appendFile, mkdir as mkdir5 } from "node:fs/promises";
|
|
31371
|
-
import { dirname as
|
|
31653
|
+
import { dirname as dirname4 } from "node:path";
|
|
31372
31654
|
var RunLogger = class {
|
|
31373
31655
|
constructor(filePath) {
|
|
31374
31656
|
this.filePath = filePath;
|
|
@@ -31383,7 +31665,7 @@ var RunLogger = class {
|
|
|
31383
31665
|
return this.write("ERROR", message);
|
|
31384
31666
|
}
|
|
31385
31667
|
async write(level, message) {
|
|
31386
|
-
await mkdir5(
|
|
31668
|
+
await mkdir5(dirname4(this.filePath), { recursive: true });
|
|
31387
31669
|
const line = `${(/* @__PURE__ */ new Date()).toISOString()} ${level} ${message}
|
|
31388
31670
|
`;
|
|
31389
31671
|
await appendFile(this.filePath, line, "utf8");
|
|
@@ -31392,30 +31674,30 @@ var RunLogger = class {
|
|
|
31392
31674
|
|
|
31393
31675
|
// ../../packages/evidence/src/screenshot.ts
|
|
31394
31676
|
import { mkdir as mkdir6, writeFile as writeFile6 } from "node:fs/promises";
|
|
31395
|
-
import { dirname as
|
|
31677
|
+
import { dirname as dirname5, join as join10 } from "node:path";
|
|
31396
31678
|
async function saveScreenshot(dir, fileName, data) {
|
|
31397
31679
|
const file2 = join10(dir, fileName);
|
|
31398
|
-
await mkdir6(
|
|
31680
|
+
await mkdir6(dirname5(file2), { recursive: true });
|
|
31399
31681
|
await writeFile6(file2, data);
|
|
31400
31682
|
return file2;
|
|
31401
31683
|
}
|
|
31402
31684
|
|
|
31403
31685
|
// ../../packages/evidence/src/trace.ts
|
|
31404
31686
|
import { mkdir as mkdir7, writeFile as writeFile7 } from "node:fs/promises";
|
|
31405
|
-
import { dirname as
|
|
31687
|
+
import { dirname as dirname6, join as join11 } from "node:path";
|
|
31406
31688
|
async function saveTrace(dir, fileName, data) {
|
|
31407
31689
|
const file2 = join11(dir, fileName);
|
|
31408
|
-
await mkdir7(
|
|
31690
|
+
await mkdir7(dirname6(file2), { recursive: true });
|
|
31409
31691
|
await writeFile7(file2, data);
|
|
31410
31692
|
return file2;
|
|
31411
31693
|
}
|
|
31412
31694
|
|
|
31413
31695
|
// ../../packages/evidence/src/video.ts
|
|
31414
31696
|
import { mkdir as mkdir8, writeFile as writeFile8 } from "node:fs/promises";
|
|
31415
|
-
import { dirname as
|
|
31697
|
+
import { dirname as dirname7, join as join12 } from "node:path";
|
|
31416
31698
|
async function saveVideo(dir, fileName, data) {
|
|
31417
31699
|
const file2 = join12(dir, fileName);
|
|
31418
|
-
await mkdir8(
|
|
31700
|
+
await mkdir8(dirname7(file2), { recursive: true });
|
|
31419
31701
|
await writeFile8(file2, data);
|
|
31420
31702
|
return file2;
|
|
31421
31703
|
}
|
|
@@ -31465,6 +31747,11 @@ var ExecutionContext = class {
|
|
|
31465
31747
|
get(name) {
|
|
31466
31748
|
return this.variables.get(name);
|
|
31467
31749
|
}
|
|
31750
|
+
seed(values) {
|
|
31751
|
+
for (const [name, value] of Object.entries(values)) {
|
|
31752
|
+
this.variables.set(name, value);
|
|
31753
|
+
}
|
|
31754
|
+
}
|
|
31468
31755
|
/**
|
|
31469
31756
|
* 将字符串中的 ${name} 替换为已提取的变量值;未定义的引用保持原样(校验阶段已拦截)。
|
|
31470
31757
|
* 名字允许点号,用于引擎注入的 account.username 一类凭据变量(用户 extract 变量名不含点号)。
|
|
@@ -31529,7 +31816,7 @@ async function ensureRunDir(runsRoot2, runId) {
|
|
|
31529
31816
|
|
|
31530
31817
|
// ../../packages/execution-engine/src/events/index.ts
|
|
31531
31818
|
import { appendFile as appendFile2, mkdir as mkdir11 } from "node:fs/promises";
|
|
31532
|
-
import { dirname as
|
|
31819
|
+
import { dirname as dirname8 } from "node:path";
|
|
31533
31820
|
var EventBus = class {
|
|
31534
31821
|
subscribers = [];
|
|
31535
31822
|
subscribe(subscriber) {
|
|
@@ -31543,7 +31830,7 @@ var EventBus = class {
|
|
|
31543
31830
|
};
|
|
31544
31831
|
function ndjsonSubscriber(filePath) {
|
|
31545
31832
|
return async (event) => {
|
|
31546
|
-
await mkdir11(
|
|
31833
|
+
await mkdir11(dirname8(filePath), { recursive: true });
|
|
31547
31834
|
await appendFile2(filePath, `${JSON.stringify(event)}
|
|
31548
31835
|
`, "utf8");
|
|
31549
31836
|
};
|
|
@@ -31624,11 +31911,19 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
|
|
|
31624
31911
|
status: "passed",
|
|
31625
31912
|
durationMs: 0
|
|
31626
31913
|
};
|
|
31914
|
+
let captureStarted = false;
|
|
31627
31915
|
try {
|
|
31628
31916
|
const locator = step.locator ? {
|
|
31629
31917
|
text: step.locator.text !== void 0 ? context.resolve(step.locator.text) : void 0,
|
|
31630
31918
|
css: step.locator.css !== void 0 ? context.resolve(step.locator.css) : void 0
|
|
31631
31919
|
} : void 0;
|
|
31920
|
+
if (step.expectRequests) {
|
|
31921
|
+
if (!adapter.startRequestCapture || !adapter.stopRequestCapture) {
|
|
31922
|
+
throw new Error(`\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301\u8BF7\u6C42\u6B21\u6570\u65AD\u8A00`);
|
|
31923
|
+
}
|
|
31924
|
+
await adapter.startRequestCapture();
|
|
31925
|
+
captureStarted = true;
|
|
31926
|
+
}
|
|
31632
31927
|
switch (step.action) {
|
|
31633
31928
|
case "launch":
|
|
31634
31929
|
await adapter.launch();
|
|
@@ -31699,7 +31994,29 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
|
|
|
31699
31994
|
throw new Error(`\u4E0D\u652F\u6301\u7684 action:${String(exhaustive)}`);
|
|
31700
31995
|
}
|
|
31701
31996
|
}
|
|
31997
|
+
if (step.expectRequests) {
|
|
31998
|
+
const windowMs = Math.max(...step.expectRequests.map((item) => item.windowMs));
|
|
31999
|
+
if (windowMs > 0) await delay(windowMs);
|
|
32000
|
+
const requests = await adapter.stopRequestCapture();
|
|
32001
|
+
captureStarted = false;
|
|
32002
|
+
result.requestAssertions = buildRequestAssertions(step, requests, context);
|
|
32003
|
+
const mismatches = result.requestAssertions.filter((item) => item.actualCount !== item.count);
|
|
32004
|
+
if (mismatches.length > 0) {
|
|
32005
|
+
throw new Error(
|
|
32006
|
+
mismatches.map(
|
|
32007
|
+
(item) => `\u8BF7\u6C42\u6B21\u6570\u4E0D\u5339\u914D:${item.method ?? "*"} ${item.urlContains},\u5B9E\u9645 ${item.actualCount},\u671F\u671B ${item.count}`
|
|
32008
|
+
).join("; ")
|
|
32009
|
+
);
|
|
32010
|
+
}
|
|
32011
|
+
}
|
|
31702
32012
|
} catch (err) {
|
|
32013
|
+
if (captureStarted) {
|
|
32014
|
+
try {
|
|
32015
|
+
await adapter.stopRequestCapture?.();
|
|
32016
|
+
} catch {
|
|
32017
|
+
}
|
|
32018
|
+
captureStarted = false;
|
|
32019
|
+
}
|
|
31703
32020
|
result.status = "failed";
|
|
31704
32021
|
result.error = err instanceof Error ? err.message : String(err);
|
|
31705
32022
|
try {
|
|
@@ -31744,92 +32061,195 @@ function resolveDeep(value, context) {
|
|
|
31744
32061
|
function snippet(text, limit = SNIPPET_LIMIT) {
|
|
31745
32062
|
return text.length > limit ? `${text.slice(0, limit)}\u2026` : text;
|
|
31746
32063
|
}
|
|
32064
|
+
function buildRequestAssertions(step, requests, context) {
|
|
32065
|
+
return (step.expectRequests ?? []).map((expectation) => {
|
|
32066
|
+
const method = expectation.method?.toUpperCase();
|
|
32067
|
+
const urlContains = context.resolve(expectation.urlContains);
|
|
32068
|
+
const matches = requests.filter(
|
|
32069
|
+
(request) => (!method || request.method.toUpperCase() === method) && request.url.includes(urlContains)
|
|
32070
|
+
);
|
|
32071
|
+
return {
|
|
32072
|
+
method,
|
|
32073
|
+
urlContains: expectation.urlContains,
|
|
32074
|
+
count: expectation.count,
|
|
32075
|
+
actualCount: matches.length,
|
|
32076
|
+
windowMs: expectation.windowMs,
|
|
32077
|
+
requests: matches.slice(0, 20).map((request) => ({
|
|
32078
|
+
method: request.method.toUpperCase(),
|
|
32079
|
+
url: sanitizeRequestUrl(request.url)
|
|
32080
|
+
}))
|
|
32081
|
+
};
|
|
32082
|
+
});
|
|
32083
|
+
}
|
|
32084
|
+
function sanitizeRequestUrl(url2) {
|
|
32085
|
+
const queryIndex = url2.indexOf("?");
|
|
32086
|
+
const safe = queryIndex >= 0 ? `${url2.slice(0, queryIndex)}?<redacted>` : url2;
|
|
32087
|
+
return snippet(safe, 300);
|
|
32088
|
+
}
|
|
32089
|
+
function delay(milliseconds) {
|
|
32090
|
+
return new Promise((resolve7) => setTimeout(resolve7, milliseconds));
|
|
32091
|
+
}
|
|
31747
32092
|
|
|
31748
32093
|
// ../../packages/execution-engine/src/engine/execution-engine.ts
|
|
31749
32094
|
var ExecutionEngine = class {
|
|
31750
32095
|
async runCase(data, options) {
|
|
31751
|
-
const { runId, file: file2, artifacts, bus, resolver } = options;
|
|
32096
|
+
const { runId, file: file2, artifacts, bus, resolver, rowId, rowIndex } = options;
|
|
31752
32097
|
const startedAt = /* @__PURE__ */ new Date();
|
|
31753
32098
|
const context = new ExecutionContext();
|
|
32099
|
+
context.seed(options.initialVariables);
|
|
31754
32100
|
if (data.accountRef && options.accounts?.[data.accountRef] !== void 0) {
|
|
31755
32101
|
context.seedAccount(options.accounts[data.accountRef]);
|
|
31756
32102
|
}
|
|
31757
32103
|
const steps = [];
|
|
32104
|
+
const warnings = [];
|
|
31758
32105
|
const evidenceAdapters = /* @__PURE__ */ new Map();
|
|
31759
32106
|
let caseError;
|
|
31760
|
-
|
|
31761
|
-
await
|
|
31762
|
-
|
|
32107
|
+
const executionId = safeArtifactName(`${data.id}-${rowId}`);
|
|
32108
|
+
await bus.emit({
|
|
32109
|
+
type: "case-started",
|
|
32110
|
+
runId,
|
|
32111
|
+
caseId: data.id,
|
|
32112
|
+
rowId,
|
|
32113
|
+
file: file2,
|
|
32114
|
+
ts: startedAt.toISOString()
|
|
32115
|
+
});
|
|
32116
|
+
await artifacts.logger.info(`case ${data.id}#${rowId} started (${file2})`);
|
|
32117
|
+
if (options.missingDependencies.length > 0) {
|
|
32118
|
+
const finishedAt2 = /* @__PURE__ */ new Date();
|
|
32119
|
+
const skipped = {
|
|
32120
|
+
caseId: data.id,
|
|
32121
|
+
caseName: data.name,
|
|
32122
|
+
file: file2,
|
|
32123
|
+
rowId,
|
|
32124
|
+
rowIndex,
|
|
32125
|
+
status: "skipped",
|
|
32126
|
+
skipReason: "dependency-not-ready",
|
|
32127
|
+
missingDependencies: options.missingDependencies,
|
|
32128
|
+
steps,
|
|
32129
|
+
startedAt: startedAt.toISOString(),
|
|
32130
|
+
finishedAt: finishedAt2.toISOString(),
|
|
32131
|
+
durationMs: finishedAt2.getTime() - startedAt.getTime()
|
|
32132
|
+
};
|
|
31763
32133
|
await bus.emit({
|
|
31764
|
-
type: "
|
|
32134
|
+
type: "case-finished",
|
|
31765
32135
|
runId,
|
|
31766
32136
|
caseId: data.id,
|
|
31767
|
-
|
|
31768
|
-
|
|
31769
|
-
|
|
31770
|
-
ts:
|
|
32137
|
+
rowId,
|
|
32138
|
+
status: "skipped",
|
|
32139
|
+
durationMs: skipped.durationMs,
|
|
32140
|
+
ts: finishedAt2.toISOString()
|
|
31771
32141
|
});
|
|
31772
|
-
|
|
31773
|
-
|
|
31774
|
-
|
|
31775
|
-
|
|
31776
|
-
|
|
31777
|
-
|
|
31778
|
-
|
|
31779
|
-
|
|
31780
|
-
|
|
31781
|
-
|
|
31782
|
-
|
|
32142
|
+
await artifacts.logger.info(
|
|
32143
|
+
`case ${data.id}#${rowId} skipped: missing ${options.missingDependencies.join(", ")}`
|
|
32144
|
+
);
|
|
32145
|
+
return skipped;
|
|
32146
|
+
}
|
|
32147
|
+
const runPhase = async (phase, phaseSteps) => {
|
|
32148
|
+
let passed = true;
|
|
32149
|
+
for (const [index, step] of phaseSteps.entries()) {
|
|
32150
|
+
await bus.emit({
|
|
32151
|
+
type: "step-started",
|
|
32152
|
+
runId,
|
|
32153
|
+
caseId: data.id,
|
|
32154
|
+
rowId,
|
|
32155
|
+
phase,
|
|
32156
|
+
index,
|
|
32157
|
+
target: step.target,
|
|
32158
|
+
action: step.action,
|
|
32159
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
32160
|
+
});
|
|
32161
|
+
let result;
|
|
32162
|
+
try {
|
|
32163
|
+
const adapter = await (await resolver.session(step.target)).get();
|
|
32164
|
+
if (adapter.startEvidence && !evidenceAdapters.has(step.target)) {
|
|
32165
|
+
try {
|
|
32166
|
+
await adapter.startEvidence(executionId);
|
|
32167
|
+
evidenceAdapters.set(step.target, adapter);
|
|
32168
|
+
} catch (err) {
|
|
32169
|
+
await artifacts.logger.info(
|
|
32170
|
+
`evidence start failed for ${step.target}: ${err instanceof Error ? err.message : String(err)}`
|
|
32171
|
+
);
|
|
32172
|
+
}
|
|
31783
32173
|
}
|
|
32174
|
+
result = await executeStep(step, index, adapter, context, artifacts, {
|
|
32175
|
+
screenshotName: `${executionId}-${phase}-${String(index).padStart(2, "0")}`
|
|
32176
|
+
});
|
|
32177
|
+
} catch (err) {
|
|
32178
|
+
result = {
|
|
32179
|
+
index,
|
|
32180
|
+
target: step.target,
|
|
32181
|
+
action: step.action,
|
|
32182
|
+
status: "failed",
|
|
32183
|
+
durationMs: 0,
|
|
32184
|
+
error: err instanceof Error ? err.message : String(err)
|
|
32185
|
+
};
|
|
31784
32186
|
}
|
|
31785
|
-
result =
|
|
31786
|
-
|
|
31787
|
-
|
|
31788
|
-
|
|
31789
|
-
|
|
32187
|
+
result.phase = phase;
|
|
32188
|
+
if (phase === "teardown" && result.status === "failed") {
|
|
32189
|
+
result.status = "warning";
|
|
32190
|
+
warnings.push(result.error ?? `teardown step ${index} failed`);
|
|
32191
|
+
}
|
|
32192
|
+
steps.push(result);
|
|
32193
|
+
await artifacts.logger.info(
|
|
32194
|
+
`${phase} step ${index} ${step.action} -> ${result.status}${result.error ? ` - ${result.error}` : ""}`
|
|
32195
|
+
);
|
|
32196
|
+
await bus.emit({
|
|
32197
|
+
type: "step-finished",
|
|
32198
|
+
runId,
|
|
32199
|
+
caseId: data.id,
|
|
32200
|
+
rowId,
|
|
32201
|
+
phase,
|
|
31790
32202
|
index,
|
|
31791
32203
|
target: step.target,
|
|
31792
32204
|
action: step.action,
|
|
31793
|
-
status:
|
|
31794
|
-
durationMs:
|
|
31795
|
-
error:
|
|
31796
|
-
|
|
31797
|
-
|
|
31798
|
-
|
|
31799
|
-
|
|
31800
|
-
|
|
31801
|
-
|
|
31802
|
-
|
|
31803
|
-
|
|
31804
|
-
|
|
31805
|
-
|
|
31806
|
-
|
|
31807
|
-
|
|
31808
|
-
|
|
31809
|
-
|
|
31810
|
-
|
|
31811
|
-
|
|
31812
|
-
|
|
31813
|
-
caseError = result.error;
|
|
31814
|
-
for (const [skipIndex, skippedStep] of data.steps.entries()) {
|
|
31815
|
-
if (skipIndex <= index) continue;
|
|
31816
|
-
steps.push({
|
|
31817
|
-
index: skipIndex,
|
|
31818
|
-
target: skippedStep.target,
|
|
31819
|
-
action: skippedStep.action,
|
|
31820
|
-
status: "skipped",
|
|
31821
|
-
durationMs: 0
|
|
31822
|
-
});
|
|
32205
|
+
status: result.status,
|
|
32206
|
+
durationMs: result.durationMs,
|
|
32207
|
+
error: result.error,
|
|
32208
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
32209
|
+
});
|
|
32210
|
+
if (result.status === "failed") {
|
|
32211
|
+
passed = false;
|
|
32212
|
+
caseError = result.error;
|
|
32213
|
+
for (const [skipIndex, skippedStep] of phaseSteps.entries()) {
|
|
32214
|
+
if (skipIndex <= index) continue;
|
|
32215
|
+
steps.push({
|
|
32216
|
+
index: skipIndex,
|
|
32217
|
+
phase,
|
|
32218
|
+
target: skippedStep.target,
|
|
32219
|
+
action: skippedStep.action,
|
|
32220
|
+
status: "skipped",
|
|
32221
|
+
durationMs: 0
|
|
32222
|
+
});
|
|
32223
|
+
}
|
|
32224
|
+
break;
|
|
31823
32225
|
}
|
|
31824
|
-
|
|
32226
|
+
}
|
|
32227
|
+
return passed;
|
|
32228
|
+
};
|
|
32229
|
+
const setupPassed = await runPhase("setup", data.setup ?? []);
|
|
32230
|
+
if (setupPassed) {
|
|
32231
|
+
await runPhase("steps", data.steps);
|
|
32232
|
+
} else {
|
|
32233
|
+
for (const [index, step] of data.steps.entries()) {
|
|
32234
|
+
steps.push({
|
|
32235
|
+
index,
|
|
32236
|
+
phase: "steps",
|
|
32237
|
+
target: step.target,
|
|
32238
|
+
action: step.action,
|
|
32239
|
+
status: "skipped",
|
|
32240
|
+
durationMs: 0
|
|
32241
|
+
});
|
|
31825
32242
|
}
|
|
31826
32243
|
}
|
|
32244
|
+
await runPhase("teardown", data.teardown ?? []);
|
|
31827
32245
|
const finishedAt = /* @__PURE__ */ new Date();
|
|
31828
32246
|
const status = caseError ? "failed" : "passed";
|
|
31829
32247
|
const caseResult = {
|
|
31830
32248
|
caseId: data.id,
|
|
31831
32249
|
caseName: data.name,
|
|
31832
32250
|
file: file2,
|
|
32251
|
+
rowId,
|
|
32252
|
+
rowIndex,
|
|
31833
32253
|
status,
|
|
31834
32254
|
steps,
|
|
31835
32255
|
startedAt: startedAt.toISOString(),
|
|
@@ -31837,15 +32257,16 @@ var ExecutionEngine = class {
|
|
|
31837
32257
|
durationMs: finishedAt.getTime() - startedAt.getTime()
|
|
31838
32258
|
};
|
|
31839
32259
|
if (caseError) caseResult.error = caseError;
|
|
32260
|
+
if (warnings.length > 0) caseResult.warnings = warnings;
|
|
31840
32261
|
for (const [target, adapter] of evidenceAdapters) {
|
|
31841
32262
|
if (!adapter.stopEvidence) continue;
|
|
31842
32263
|
try {
|
|
31843
|
-
const evidence = await adapter.stopEvidence(
|
|
32264
|
+
const evidence = await adapter.stopEvidence(executionId);
|
|
31844
32265
|
if (evidence.video) {
|
|
31845
|
-
caseResult.video = await artifacts.saveVideo(`${
|
|
32266
|
+
caseResult.video = await artifacts.saveVideo(`${executionId}-${target}.webm`, evidence.video);
|
|
31846
32267
|
}
|
|
31847
32268
|
if (evidence.trace) {
|
|
31848
|
-
caseResult.trace = await artifacts.saveTrace(`${
|
|
32269
|
+
caseResult.trace = await artifacts.saveTrace(`${executionId}-${target}.zip`, evidence.trace);
|
|
31849
32270
|
}
|
|
31850
32271
|
} catch (err) {
|
|
31851
32272
|
await artifacts.logger.info(
|
|
@@ -31857,33 +32278,45 @@ var ExecutionEngine = class {
|
|
|
31857
32278
|
type: "case-finished",
|
|
31858
32279
|
runId,
|
|
31859
32280
|
caseId: data.id,
|
|
32281
|
+
rowId,
|
|
31860
32282
|
status,
|
|
31861
32283
|
durationMs: finishedAt.getTime() - startedAt.getTime(),
|
|
31862
32284
|
ts: finishedAt.toISOString()
|
|
31863
32285
|
});
|
|
31864
|
-
await artifacts.logger.info(`case ${data.id} ${status}`);
|
|
32286
|
+
await artifacts.logger.info(`case ${data.id}#${rowId} ${status}`);
|
|
31865
32287
|
return caseResult;
|
|
31866
32288
|
}
|
|
31867
32289
|
};
|
|
32290
|
+
function safeArtifactName(value) {
|
|
32291
|
+
return value.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "case";
|
|
32292
|
+
}
|
|
31868
32293
|
|
|
31869
32294
|
// ../../packages/execution-engine/src/engine/result-collector.ts
|
|
31870
32295
|
function buildRunSummary(runId, startedAt, cases, options = {}) {
|
|
31871
32296
|
const finishedAt = /* @__PURE__ */ new Date();
|
|
31872
32297
|
const totals = {
|
|
32298
|
+
templates: options.templates ?? new Set(cases.map((item) => item.caseId)).size,
|
|
31873
32299
|
cases: cases.length,
|
|
31874
32300
|
passed: cases.filter((item) => item.status === "passed").length,
|
|
31875
32301
|
failed: cases.filter((item) => item.status === "failed").length,
|
|
32302
|
+
skipped: cases.filter((item) => item.status === "skipped").length,
|
|
32303
|
+
warnings: 0,
|
|
31876
32304
|
steps: 0,
|
|
31877
32305
|
stepsPassed: 0,
|
|
31878
32306
|
stepsFailed: 0,
|
|
31879
|
-
stepsSkipped: 0
|
|
32307
|
+
stepsSkipped: 0,
|
|
32308
|
+
stepsWarning: 0
|
|
31880
32309
|
};
|
|
31881
32310
|
for (const item of cases) {
|
|
31882
32311
|
for (const step of item.steps) {
|
|
31883
32312
|
totals.steps++;
|
|
31884
32313
|
if (step.status === "passed") totals.stepsPassed++;
|
|
31885
32314
|
else if (step.status === "failed") totals.stepsFailed++;
|
|
31886
|
-
else totals.stepsSkipped++;
|
|
32315
|
+
else if (step.status === "skipped") totals.stepsSkipped++;
|
|
32316
|
+
else {
|
|
32317
|
+
totals.stepsWarning++;
|
|
32318
|
+
totals.warnings++;
|
|
32319
|
+
}
|
|
31887
32320
|
}
|
|
31888
32321
|
}
|
|
31889
32322
|
return {
|
|
@@ -31891,7 +32324,7 @@ function buildRunSummary(runId, startedAt, cases, options = {}) {
|
|
|
31891
32324
|
startedAt: startedAt.toISOString(),
|
|
31892
32325
|
finishedAt: finishedAt.toISOString(),
|
|
31893
32326
|
durationMs: finishedAt.getTime() - startedAt.getTime(),
|
|
31894
|
-
status: totals.failed > 0 ? "failed" : "passed",
|
|
32327
|
+
status: totals.failed > 0 ? "failed" : totals.passed > 0 || totals.cases === 0 ? "passed" : "skipped",
|
|
31895
32328
|
...options.cancelled ? { cancelled: true } : {},
|
|
31896
32329
|
cases,
|
|
31897
32330
|
totals
|
|
@@ -31900,7 +32333,7 @@ function buildRunSummary(runId, startedAt, cases, options = {}) {
|
|
|
31900
32333
|
|
|
31901
32334
|
// ../../packages/execution-engine/src/engine/test-runner.ts
|
|
31902
32335
|
import { writeFile as writeFile9 } from "node:fs/promises";
|
|
31903
|
-
import { join as join15, resolve as
|
|
32336
|
+
import { join as join15, resolve as resolve6 } from "node:path";
|
|
31904
32337
|
var TestRunner = class {
|
|
31905
32338
|
resolver;
|
|
31906
32339
|
runsRoot;
|
|
@@ -31910,7 +32343,7 @@ var TestRunner = class {
|
|
|
31910
32343
|
accounts;
|
|
31911
32344
|
constructor(options) {
|
|
31912
32345
|
this.resolver = options.resolver;
|
|
31913
|
-
this.runsRoot =
|
|
32346
|
+
this.runsRoot = resolve6(options.runsRoot ?? join15(process.cwd(), ".testpilot", "artifacts", "runs"));
|
|
31914
32347
|
this.customRunId = options.runId;
|
|
31915
32348
|
this.extraSubscriber = options.onEvent;
|
|
31916
32349
|
this.signal = options.signal;
|
|
@@ -31938,6 +32371,11 @@ var TestRunner = class {
|
|
|
31938
32371
|
await engine.runCase(item.data, {
|
|
31939
32372
|
runId,
|
|
31940
32373
|
file: item.file,
|
|
32374
|
+
templateId: item.templateId ?? item.data.id,
|
|
32375
|
+
rowId: item.rowId ?? "default",
|
|
32376
|
+
rowIndex: item.rowIndex ?? 0,
|
|
32377
|
+
initialVariables: item.initialVariables ?? {},
|
|
32378
|
+
missingDependencies: item.missingDependencies ?? [],
|
|
31941
32379
|
artifacts,
|
|
31942
32380
|
bus,
|
|
31943
32381
|
resolver: this.resolver,
|
|
@@ -31947,7 +32385,8 @@ var TestRunner = class {
|
|
|
31947
32385
|
}
|
|
31948
32386
|
const cancelled = this.signal?.aborted === true;
|
|
31949
32387
|
await this.resolver.closeAll();
|
|
31950
|
-
const
|
|
32388
|
+
const templates = new Set(cases.map((item) => item.templateId ?? item.data.id)).size;
|
|
32389
|
+
const summary = buildRunSummary(runId, startedAt, results, { cancelled, templates });
|
|
31951
32390
|
await bus.emit({
|
|
31952
32391
|
type: "run-finished",
|
|
31953
32392
|
runId,
|
|
@@ -31989,8 +32428,8 @@ async function openCaseContext(bundle, options, evidenceTmpDir) {
|
|
|
31989
32428
|
}
|
|
31990
32429
|
|
|
31991
32430
|
// ../../packages/adapter-playwright/src/page.ts
|
|
31992
|
-
import { existsSync as
|
|
31993
|
-
import { mkdtemp, readFile as
|
|
32431
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
32432
|
+
import { mkdtemp, readFile as readFile6, rm } from "node:fs/promises";
|
|
31994
32433
|
import { tmpdir } from "node:os";
|
|
31995
32434
|
import { join as join16 } from "node:path";
|
|
31996
32435
|
var PlaywrightAdapter = class {
|
|
@@ -32000,6 +32439,7 @@ var PlaywrightAdapter = class {
|
|
|
32000
32439
|
}
|
|
32001
32440
|
target = "web";
|
|
32002
32441
|
current;
|
|
32442
|
+
requestCapture;
|
|
32003
32443
|
/** 浏览器在 create() 时已启动;context/page 按用例惰性创建,launch 保持幂等 */
|
|
32004
32444
|
async launch() {
|
|
32005
32445
|
}
|
|
@@ -32014,6 +32454,7 @@ var PlaywrightAdapter = class {
|
|
|
32014
32454
|
}
|
|
32015
32455
|
/** 结束取证:trace 落 zip、录屏在 context 关闭后完成,返回二者字节 */
|
|
32016
32456
|
async stopEvidence(_caseId) {
|
|
32457
|
+
await this.stopRequestCapture().catch(() => []);
|
|
32017
32458
|
if (!this.current) return {};
|
|
32018
32459
|
const { context, page, evidenceTmpDir } = this.current;
|
|
32019
32460
|
this.current = void 0;
|
|
@@ -32021,11 +32462,28 @@ var PlaywrightAdapter = class {
|
|
|
32021
32462
|
await context.tracing.stop({ path: traceZipPath }).catch(() => void 0);
|
|
32022
32463
|
const recordedVideoPath = await page.video()?.path().catch(() => void 0);
|
|
32023
32464
|
await context.close().catch(() => void 0);
|
|
32024
|
-
const video = recordedVideoPath &&
|
|
32025
|
-
const trace = evidenceTmpDir &&
|
|
32465
|
+
const video = recordedVideoPath && existsSync3(recordedVideoPath) ? await readFile6(recordedVideoPath) : void 0;
|
|
32466
|
+
const trace = evidenceTmpDir && existsSync3(traceZipPath) ? await readFile6(traceZipPath) : void 0;
|
|
32026
32467
|
if (evidenceTmpDir) await rm(evidenceTmpDir, { recursive: true, force: true }).catch(() => void 0);
|
|
32027
32468
|
return { video, trace };
|
|
32028
32469
|
}
|
|
32470
|
+
async startRequestCapture() {
|
|
32471
|
+
if (this.requestCapture) await this.stopRequestCapture();
|
|
32472
|
+
const page = await this.ensurePage();
|
|
32473
|
+
const requests = [];
|
|
32474
|
+
const listener = (request) => {
|
|
32475
|
+
requests.push({ method: request.method(), url: request.url() });
|
|
32476
|
+
};
|
|
32477
|
+
page.on("request", listener);
|
|
32478
|
+
this.requestCapture = { page, listener, requests };
|
|
32479
|
+
}
|
|
32480
|
+
async stopRequestCapture() {
|
|
32481
|
+
if (!this.requestCapture) return [];
|
|
32482
|
+
const capture = this.requestCapture;
|
|
32483
|
+
this.requestCapture = void 0;
|
|
32484
|
+
capture.page.off("request", capture.listener);
|
|
32485
|
+
return capture.requests;
|
|
32486
|
+
}
|
|
32029
32487
|
async navigate(url2) {
|
|
32030
32488
|
const options = this.options;
|
|
32031
32489
|
const page = await this.ensurePage();
|
|
@@ -32087,6 +32545,7 @@ var PlaywrightAdapter = class {
|
|
|
32087
32545
|
throw new Error(`\u65E0\u6548\u7684 locator:${JSON.stringify(locator)}`);
|
|
32088
32546
|
}
|
|
32089
32547
|
async closeContextQuietly() {
|
|
32548
|
+
await this.stopRequestCapture().catch(() => []);
|
|
32090
32549
|
if (!this.current) return;
|
|
32091
32550
|
const { context } = this.current;
|
|
32092
32551
|
this.current = void 0;
|
|
@@ -32125,8 +32584,8 @@ var automatorLauncher = {
|
|
|
32125
32584
|
}
|
|
32126
32585
|
}
|
|
32127
32586
|
};
|
|
32128
|
-
function
|
|
32129
|
-
return new Promise((
|
|
32587
|
+
function delay2(ms) {
|
|
32588
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
32130
32589
|
}
|
|
32131
32590
|
async function connectWithRetry(wsEndpoint, timeoutMs) {
|
|
32132
32591
|
const startedAt = Date.now();
|
|
@@ -32136,7 +32595,7 @@ async function connectWithRetry(wsEndpoint, timeoutMs) {
|
|
|
32136
32595
|
return await automator.connect({ wsEndpoint });
|
|
32137
32596
|
} catch (err) {
|
|
32138
32597
|
lastError = err;
|
|
32139
|
-
await
|
|
32598
|
+
await delay2(1e3);
|
|
32140
32599
|
}
|
|
32141
32600
|
}
|
|
32142
32601
|
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
@@ -32187,6 +32646,7 @@ var MiniAppAdapter = class {
|
|
|
32187
32646
|
}
|
|
32188
32647
|
target = "miniapp";
|
|
32189
32648
|
program;
|
|
32649
|
+
requestCapture;
|
|
32190
32650
|
/** 拉起开发者工具并建立自动化连接;需幂等 */
|
|
32191
32651
|
async launch() {
|
|
32192
32652
|
if (this.program) return;
|
|
@@ -32207,7 +32667,7 @@ var MiniAppAdapter = class {
|
|
|
32207
32667
|
return;
|
|
32208
32668
|
} catch (err) {
|
|
32209
32669
|
lastError = err;
|
|
32210
|
-
await
|
|
32670
|
+
await delay2(3e3);
|
|
32211
32671
|
}
|
|
32212
32672
|
}
|
|
32213
32673
|
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
@@ -32253,7 +32713,43 @@ var MiniAppAdapter = class {
|
|
|
32253
32713
|
return Buffer.from(base643, "base64");
|
|
32254
32714
|
});
|
|
32255
32715
|
}
|
|
32716
|
+
async startRequestCapture() {
|
|
32717
|
+
if (this.requestCapture) await this.stopRequestCapture();
|
|
32718
|
+
const program2 = await this.getProgram();
|
|
32719
|
+
const requests = [];
|
|
32720
|
+
const exposedName = `__testpilotRequest_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
32721
|
+
await program2.exposeFunction(exposedName, (value) => {
|
|
32722
|
+
if (value === null || typeof value !== "object") return;
|
|
32723
|
+
const request = value;
|
|
32724
|
+
if (typeof request.url !== "string") return;
|
|
32725
|
+
requests.push({
|
|
32726
|
+
method: typeof request.method === "string" ? request.method : "GET",
|
|
32727
|
+
url: request.url
|
|
32728
|
+
});
|
|
32729
|
+
});
|
|
32730
|
+
await program2.mockWxMethod(
|
|
32731
|
+
"request",
|
|
32732
|
+
function(options, callbackName) {
|
|
32733
|
+
const callbacks = globalThis;
|
|
32734
|
+
callbacks[callbackName]?.({
|
|
32735
|
+
method: typeof options.method === "string" ? options.method : "GET",
|
|
32736
|
+
url: typeof options.url === "string" ? options.url : ""
|
|
32737
|
+
});
|
|
32738
|
+
return this.origin(options);
|
|
32739
|
+
},
|
|
32740
|
+
exposedName
|
|
32741
|
+
);
|
|
32742
|
+
this.requestCapture = { program: program2, exposedName, requests };
|
|
32743
|
+
}
|
|
32744
|
+
async stopRequestCapture() {
|
|
32745
|
+
if (!this.requestCapture) return [];
|
|
32746
|
+
const capture = this.requestCapture;
|
|
32747
|
+
this.requestCapture = void 0;
|
|
32748
|
+
await capture.program.restoreWxMethod("request");
|
|
32749
|
+
return capture.requests;
|
|
32750
|
+
}
|
|
32256
32751
|
async close() {
|
|
32752
|
+
await this.stopRequestCapture().catch(() => []);
|
|
32257
32753
|
if (!this.program) return;
|
|
32258
32754
|
const program2 = this.program;
|
|
32259
32755
|
this.program = void 0;
|
|
@@ -32301,17 +32797,17 @@ var MiniAppAdapter = class {
|
|
|
32301
32797
|
if (Date.now() - startedAt > ELEMENT_WAIT_MS) {
|
|
32302
32798
|
throw new Error(`\u7B49\u5F85\u5143\u7D20\u8D85\u65F6(${ELEMENT_WAIT_MS}ms):${selector}`);
|
|
32303
32799
|
}
|
|
32304
|
-
await
|
|
32800
|
+
await delay2(500);
|
|
32305
32801
|
}
|
|
32306
32802
|
}
|
|
32307
32803
|
};
|
|
32308
32804
|
|
|
32309
32805
|
// ../../packages/adapter-wechatide/src/project.ts
|
|
32310
|
-
import { existsSync as
|
|
32806
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
32311
32807
|
async function resolveMiniappConfig(input2) {
|
|
32312
32808
|
const projectPath = input2.projectPath ?? process.env.TESTPILOT_MINIPROGRAM_PROJECT_PATH;
|
|
32313
32809
|
if (!projectPath) return void 0;
|
|
32314
|
-
if (!
|
|
32810
|
+
if (!existsSync4(projectPath)) {
|
|
32315
32811
|
throw new Error(`\u5C0F\u7A0B\u5E8F\u9879\u76EE\u8DEF\u5F84\u4E0D\u5B58\u5728:${projectPath}`);
|
|
32316
32812
|
}
|
|
32317
32813
|
const cliPath = input2.cliPath ?? process.env.WECHAT_DEVTOOLS_CLI ?? await findWechatDevToolsCli();
|
|
@@ -32486,7 +32982,7 @@ function createDefaultAdapters(config2) {
|
|
|
32486
32982
|
async function runCases(options = {}) {
|
|
32487
32983
|
const root = resolveRoot(options.root);
|
|
32488
32984
|
const config2 = await loadProjectConfig(root);
|
|
32489
|
-
const inputs = options.paths && options.paths.length > 0 ? options.paths.map((input2) =>
|
|
32985
|
+
const inputs = options.paths && options.paths.length > 0 ? options.paths.map((input2) => isAbsolute3(input2) ? input2 : join17(root, input2)) : [join17(root, config2.casesDir)];
|
|
32490
32986
|
const cases = await collectCases(inputs, { root });
|
|
32491
32987
|
if (cases.length === 0) {
|
|
32492
32988
|
throw new RunError("ENO_CASES", `\u672A\u627E\u5230\u7528\u4F8B\u6587\u4EF6(\u67E5\u627E:${inputs.join(", ")})`);
|
|
@@ -32518,8 +33014,23 @@ async function runCases(options = {}) {
|
|
|
32518
33014
|
signal: options.signal,
|
|
32519
33015
|
accounts: options.accounts
|
|
32520
33016
|
});
|
|
33017
|
+
const prepared = (await Promise.all(
|
|
33018
|
+
runnable.map(
|
|
33019
|
+
(item) => prepareCaseExecutions(
|
|
33020
|
+
{ data: item.case, file: item.file },
|
|
33021
|
+
{
|
|
33022
|
+
root,
|
|
33023
|
+
dataDir: join17(root, "tests", "e2e", "data"),
|
|
33024
|
+
variables: config2.variables,
|
|
33025
|
+
accounts: options.accounts
|
|
33026
|
+
}
|
|
33027
|
+
)
|
|
33028
|
+
)
|
|
33029
|
+
)).flat();
|
|
32521
33030
|
const summary = await runner.run(
|
|
32522
|
-
|
|
33031
|
+
prepared.map(
|
|
33032
|
+
(item) => item
|
|
33033
|
+
)
|
|
32523
33034
|
);
|
|
32524
33035
|
return { summary, invalid, tagFiltered, missingAccounts };
|
|
32525
33036
|
}
|
|
@@ -32543,20 +33054,20 @@ async function getRun(runId, options = {}) {
|
|
|
32543
33054
|
const base = runsRoot(options.root);
|
|
32544
33055
|
const file2 = join17(base, runId, RESULT_FILE);
|
|
32545
33056
|
try {
|
|
32546
|
-
return JSON.parse(await
|
|
33057
|
+
return JSON.parse(await readFile7(file2, "utf8"));
|
|
32547
33058
|
} catch {
|
|
32548
33059
|
throw new RunError("ENO_CASES", `\u8BFB\u53D6\u8FD0\u884C\u7ED3\u679C\u5931\u8D25:${file2}(\u8FD0\u884C\u672A\u5B8C\u6210\u6216 runId \u4E0D\u5B58\u5728)`);
|
|
32549
33060
|
}
|
|
32550
33061
|
}
|
|
32551
33062
|
async function getRunEvents(runId, options = {}) {
|
|
32552
33063
|
const file2 = join17(runsRoot(options.root), runId, "events.ndjson");
|
|
32553
|
-
const source = await
|
|
33064
|
+
const source = await readFile7(file2, "utf8").catch(() => "");
|
|
32554
33065
|
return source.split("\n").filter((line) => line.trim().length > 0).map((line) => JSON.parse(line));
|
|
32555
33066
|
}
|
|
32556
33067
|
async function readRunMeta(base, runId) {
|
|
32557
33068
|
try {
|
|
32558
33069
|
const summary = JSON.parse(
|
|
32559
|
-
await
|
|
33070
|
+
await readFile7(join17(base, runId, RESULT_FILE), "utf8")
|
|
32560
33071
|
);
|
|
32561
33072
|
return {
|
|
32562
33073
|
status: summary.status,
|
|
@@ -32580,7 +33091,7 @@ async function generateReport(runId, options = {}) {
|
|
|
32580
33091
|
const runDir = join18(resolveRoot(options.root), RUNS_DIR, id);
|
|
32581
33092
|
let summary;
|
|
32582
33093
|
try {
|
|
32583
|
-
summary = JSON.parse(await
|
|
33094
|
+
summary = JSON.parse(await readFile8(join18(runDir, "result.json"), "utf8"));
|
|
32584
33095
|
} catch {
|
|
32585
33096
|
throw new Error(`\u8BFB\u53D6\u8FD0\u884C\u7ED3\u679C\u5931\u8D25:${join18(runDir, "result.json")}(\u8FD0\u884C\u672A\u5B8C\u6210\u6216 runId \u4E0D\u5B58\u5728)`);
|
|
32586
33097
|
}
|
|
@@ -32611,13 +33122,13 @@ async function generateSummaryReport(options = {}) {
|
|
|
32611
33122
|
}
|
|
32612
33123
|
async function readReport(runId, options = {}) {
|
|
32613
33124
|
const file2 = join18(resolveRoot(options.root), RUNS_DIR, runId, "report.json");
|
|
32614
|
-
const source = await
|
|
33125
|
+
const source = await readFile8(file2, "utf8").catch(() => void 0);
|
|
32615
33126
|
return source ? JSON.parse(source) : void 0;
|
|
32616
33127
|
}
|
|
32617
33128
|
|
|
32618
33129
|
// ../../packages/sdk/src/client.ts
|
|
32619
33130
|
function resolveInput(root, input2) {
|
|
32620
|
-
return
|
|
33131
|
+
return isAbsolute4(input2) ? input2 : join19(root, input2);
|
|
32621
33132
|
}
|
|
32622
33133
|
var TestPilotClient = class {
|
|
32623
33134
|
/** 业务项目根目录(绝对路径) */
|
|
@@ -32693,13 +33204,13 @@ var TestPilotClient = class {
|
|
|
32693
33204
|
};
|
|
32694
33205
|
|
|
32695
33206
|
// src/commands/init/adapter-detector.ts
|
|
32696
|
-
import { existsSync as
|
|
33207
|
+
import { existsSync as existsSync5, readFileSync } from "node:fs";
|
|
32697
33208
|
import { join as join20 } from "node:path";
|
|
32698
33209
|
async function detectAdapters(root, projectType) {
|
|
32699
33210
|
const required2 = projectType === "hybrid" ? ["playwright", "wechatide"] : projectType === "wechat-miniapp" ? ["wechatide"] : projectType === "web" ? ["playwright"] : [];
|
|
32700
33211
|
const pkg = readPackageJsonDeps(root);
|
|
32701
33212
|
const hasPlaywrightDep = pkg !== void 0 && ("playwright" in pkg || "@playwright/test" in pkg);
|
|
32702
|
-
const playwrightInstalled = hasPlaywrightDep ||
|
|
33213
|
+
const playwrightInstalled = hasPlaywrightDep || existsSync5(join20(root, "node_modules", "playwright"));
|
|
32703
33214
|
const devtoolsCli = await findWechatDevToolsCli().catch(() => void 0);
|
|
32704
33215
|
return [
|
|
32705
33216
|
{
|
|
@@ -32718,7 +33229,7 @@ async function detectAdapters(root, projectType) {
|
|
|
32718
33229
|
}
|
|
32719
33230
|
function readPackageJsonDeps(root) {
|
|
32720
33231
|
const file2 = join20(root, "package.json");
|
|
32721
|
-
if (!
|
|
33232
|
+
if (!existsSync5(file2)) return void 0;
|
|
32722
33233
|
try {
|
|
32723
33234
|
const pkg = JSON.parse(readFileSync(file2, "utf8"));
|
|
32724
33235
|
return { ...pkg.dependencies, ...pkg.devDependencies };
|
|
@@ -32728,13 +33239,13 @@ function readPackageJsonDeps(root) {
|
|
|
32728
33239
|
}
|
|
32729
33240
|
|
|
32730
33241
|
// src/commands/init/environment-initializer.ts
|
|
32731
|
-
import { readFile as
|
|
33242
|
+
import { readFile as readFile9 } from "node:fs/promises";
|
|
32732
33243
|
import { join as join21 } from "node:path";
|
|
32733
33244
|
var ENV_REF_PATTERN = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
32734
33245
|
async function collectEnvVarRefs(root) {
|
|
32735
33246
|
let text;
|
|
32736
33247
|
try {
|
|
32737
|
-
text = await
|
|
33248
|
+
text = await readFile9(join21(root, "testpilot.yaml"), "utf8");
|
|
32738
33249
|
} catch {
|
|
32739
33250
|
return [];
|
|
32740
33251
|
}
|
|
@@ -32750,7 +33261,7 @@ async function collectEnvVarRefs(root) {
|
|
|
32750
33261
|
}
|
|
32751
33262
|
|
|
32752
33263
|
// src/commands/init/project-detector.ts
|
|
32753
|
-
import { existsSync as
|
|
33264
|
+
import { existsSync as existsSync6, readFileSync as readFileSync2 } from "node:fs";
|
|
32754
33265
|
import { basename, join as join22 } from "node:path";
|
|
32755
33266
|
|
|
32756
33267
|
// src/lib/git.ts
|
|
@@ -32789,7 +33300,7 @@ var WEB_DEP_MARKERS = [
|
|
|
32789
33300
|
var E2E_DIR_CANDIDATES = ["tests/e2e", "test/e2e", "e2e", "cypress"];
|
|
32790
33301
|
function readPackageJson(root) {
|
|
32791
33302
|
const file2 = join22(root, "package.json");
|
|
32792
|
-
if (!
|
|
33303
|
+
if (!existsSync6(file2)) return void 0;
|
|
32793
33304
|
try {
|
|
32794
33305
|
return JSON.parse(readFileSync2(file2, "utf8"));
|
|
32795
33306
|
} catch {
|
|
@@ -32799,8 +33310,8 @@ function readPackageJson(root) {
|
|
|
32799
33310
|
function detectProjectType(root) {
|
|
32800
33311
|
const pkg = readPackageJson(root);
|
|
32801
33312
|
const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
|
|
32802
|
-
const isWechat =
|
|
32803
|
-
const isWeb =
|
|
33313
|
+
const isWechat = existsSync6(join22(root, "project.config.json")) || existsSync6(join22(root, "app.json")) || existsSync6(join22(root, "miniprogram", "app.json"));
|
|
33314
|
+
const isWeb = existsSync6(join22(root, "index.html")) || WEB_DEP_MARKERS.some((marker) => deps?.[marker] !== void 0) || [".ts", ".tsx", ".js", ".jsx"].some((ext) => existsSync6(join22(root, "src", `index${ext}`)));
|
|
32804
33315
|
if (isWechat && isWeb) return "hybrid";
|
|
32805
33316
|
if (isWechat) return "wechat-miniapp";
|
|
32806
33317
|
if (isWeb) return "web";
|
|
@@ -32809,9 +33320,9 @@ function detectProjectType(root) {
|
|
|
32809
33320
|
async function detectProject(root) {
|
|
32810
33321
|
const pkg = readPackageJson(root);
|
|
32811
33322
|
const type = detectProjectType(root);
|
|
32812
|
-
const language =
|
|
32813
|
-
const packageManager =
|
|
32814
|
-
const existingE2eDirs = E2E_DIR_CANDIDATES.filter((dir) =>
|
|
33323
|
+
const language = existsSync6(join22(root, "tsconfig.json")) ? "TypeScript" : pkg || existsSync6(join22(root, "src")) ? "JavaScript" : "Unknown";
|
|
33324
|
+
const packageManager = existsSync6(join22(root, "pnpm-lock.yaml")) ? "pnpm" : existsSync6(join22(root, "yarn.lock")) ? "yarn" : existsSync6(join22(root, "package-lock.json")) ? "npm" : "npx";
|
|
33325
|
+
const existingE2eDirs = E2E_DIR_CANDIDATES.filter((dir) => existsSync6(join22(root, dir)));
|
|
32815
33326
|
return {
|
|
32816
33327
|
root,
|
|
32817
33328
|
name: pkg?.name ?? basename(root),
|
|
@@ -32821,8 +33332,8 @@ async function detectProject(root) {
|
|
|
32821
33332
|
git: await readGitInfo(root),
|
|
32822
33333
|
hasTestsE2e: existingE2eDirs.includes("tests/e2e"),
|
|
32823
33334
|
existingE2eDirs,
|
|
32824
|
-
hasTestpilotConfig:
|
|
32825
|
-
hasProjectLink:
|
|
33335
|
+
hasTestpilotConfig: existsSync6(join22(root, "testpilot.yaml")),
|
|
33336
|
+
hasProjectLink: existsSync6(join22(root, ".testpilot", "project.json"))
|
|
32826
33337
|
};
|
|
32827
33338
|
}
|
|
32828
33339
|
function defaultAdapterFor(type) {
|
|
@@ -32837,9 +33348,9 @@ function adaptersFor(type) {
|
|
|
32837
33348
|
}
|
|
32838
33349
|
|
|
32839
33350
|
// src/commands/init/skill-installer.ts
|
|
32840
|
-
import { existsSync as
|
|
33351
|
+
import { existsSync as existsSync7, readFileSync as readFileSync3 } from "node:fs";
|
|
32841
33352
|
import { access as access2, cp, mkdir as mkdir12, writeFile as writeFile10 } from "node:fs/promises";
|
|
32842
|
-
import { dirname as
|
|
33353
|
+
import { dirname as dirname9, join as join23 } from "node:path";
|
|
32843
33354
|
import { fileURLToPath } from "node:url";
|
|
32844
33355
|
var CORE_FILES = ["SKILL.md", "manifest.yaml"];
|
|
32845
33356
|
var CORE_DIRS = ["rules", "workflows"];
|
|
@@ -32853,23 +33364,23 @@ async function exists3(path) {
|
|
|
32853
33364
|
}
|
|
32854
33365
|
function readManifestVersion(dir) {
|
|
32855
33366
|
const manifestPath = join23(dir, "manifest.yaml");
|
|
32856
|
-
if (!
|
|
33367
|
+
if (!existsSync7(manifestPath)) return void 0;
|
|
32857
33368
|
return /^version:\s*([^\s#]+)/m.exec(readFileSync3(manifestPath, "utf8"))?.[1];
|
|
32858
33369
|
}
|
|
32859
33370
|
function resolveSkillSourceDir() {
|
|
32860
33371
|
const fromEnv = process.env.TESTPILOT_SKILL_DIR;
|
|
32861
33372
|
if (fromEnv) return fromEnv;
|
|
32862
33373
|
const bundled = fileURLToPath(new URL("./skills/testpilot", import.meta.url));
|
|
32863
|
-
if (
|
|
33374
|
+
if (existsSync7(bundled)) return bundled;
|
|
32864
33375
|
const inRepo = fileURLToPath(new URL("../../../../../skills/testpilot", import.meta.url));
|
|
32865
|
-
return
|
|
33376
|
+
return existsSync7(inRepo) ? inRepo : void 0;
|
|
32866
33377
|
}
|
|
32867
33378
|
async function copyCoreFiles(src, dest) {
|
|
32868
33379
|
for (const name of CORE_FILES) {
|
|
32869
33380
|
await cp(join23(src, name), join23(dest, name), { recursive: true, force: true });
|
|
32870
33381
|
}
|
|
32871
33382
|
for (const dir of CORE_DIRS) {
|
|
32872
|
-
if (
|
|
33383
|
+
if (existsSync7(join23(src, dir))) {
|
|
32873
33384
|
await cp(join23(src, dir), join23(dest, dir), { recursive: true, force: true });
|
|
32874
33385
|
}
|
|
32875
33386
|
}
|
|
@@ -32884,7 +33395,7 @@ async function syncSkillFiles(root, options = {}) {
|
|
|
32884
33395
|
"\u65E0\u6CD5\u5B9A\u4F4D TestPilot Skill \u6E90\u76EE\u5F55;\u53EF\u901A\u8FC7\u73AF\u5883\u53D8\u91CF TESTPILOT_SKILL_DIR \u6307\u5B9A skills/testpilot \u8DEF\u5F84"
|
|
32885
33396
|
);
|
|
32886
33397
|
}
|
|
32887
|
-
await mkdir12(
|
|
33398
|
+
await mkdir12(dirname9(dest), { recursive: true });
|
|
32888
33399
|
await cp(src, dest, { recursive: true });
|
|
32889
33400
|
return { core: "created", bundledVersion, dest };
|
|
32890
33401
|
}
|
|
@@ -32958,6 +33469,12 @@ function renderTestConventionsMd() {
|
|
|
32958
33469
|
6. \u4F18\u5148\u590D\u7528\u5DF2\u6709 Case
|
|
32959
33470
|
7. \u65B0\u589E Case \u5FC5\u987B\u901A\u8FC7 csspilot validate
|
|
32960
33471
|
8. Case ID \u5FC5\u987B\u552F\u4E00
|
|
33472
|
+
9. fixture/dataset \u4F7F\u7528 YAML \u6216 JSON,\u7EDF\u4E00\u4F4D\u4E8E tests/e2e/data
|
|
33473
|
+
10. \u9700\u6C42\u89C4\u5219\u77E9\u9635\u4E0E QA \u8986\u76D6\u6620\u5C04\u5199\u5165 tests/e2e/reviews/<\u9700\u6C42>-coverage.md
|
|
33474
|
+
11. \u53EA\u5728\u6B65\u9AA4\u6A21\u677F\u76F8\u540C\u65F6\u4F7F\u7528 data-driven Case,\u6BCF\u884C\u4FDD\u6301\u4E00\u4E2A\u53EF\u5224\u5B9A\u89C4\u5219/\u6570\u636E\u70B9
|
|
33475
|
+
12. requires \u53EA\u58F0\u660E\u9879\u76EE\u8BC1\u636E\u652F\u6301\u7684\u4F9D\u8D56,\u4E0D\u5F97\u5957\u7528\u8D26\u53F7/\u4F18\u60E0\u5238/PPM \u7B49\u9ED8\u8BA4\u5B57\u6BB5
|
|
33476
|
+
13. Case \u4E0D\u5F97\u8BFB\u53D6\u4EFB\u610F\u73AF\u5883\u53D8\u91CF;\u5148\u5728 testpilot.yaml variables \u767D\u540D\u5355\u6620\u5C04\u540E\u4F7F\u7528 \${variable.*}
|
|
33477
|
+
14. setup/teardown \u4F18\u5148 API;teardown \u5931\u8D25\u4F5C\u4E3A warning,\u4E0D\u6539\u53D8\u4E3B\u6D41\u7A0B\u7ED3\u679C
|
|
32961
33478
|
`;
|
|
32962
33479
|
}
|
|
32963
33480
|
function renderAdaptersMd(adapters) {
|
|
@@ -32998,8 +33515,8 @@ async function runDoctor(root) {
|
|
|
32998
33515
|
const projectItems = [
|
|
32999
33516
|
ok(`Project ${info.name}(${info.type})`),
|
|
33000
33517
|
info.git.commit ? ok(`Git repository(${info.git.branch ?? "detached"} \xB7 ${info.git.commit.slice(0, 7)})`) : warn("Git repository \u672A\u68C0\u6D4B\u5230", "Case Metadata \u65E0\u6CD5\u540C\u6B65\u5230 Server"),
|
|
33001
|
-
|
|
33002
|
-
|
|
33518
|
+
existsSync8(join24(root, ".testpilot", "project.json")) ? ok(".testpilot/project.json") : error62(".testpilot/project.json", "\u8FD0\u884C npx csspilot init \u751F\u6210"),
|
|
33519
|
+
existsSync8(join24(root, "testpilot.yaml")) ? ok("testpilot.yaml") : error62("testpilot.yaml", "\u8FD0\u884C npx csspilot init \u751F\u6210")
|
|
33003
33520
|
];
|
|
33004
33521
|
push("Project", projectItems);
|
|
33005
33522
|
const config2 = await loadTestpilotConfig(root).catch(() => void 0);
|
|
@@ -33008,8 +33525,8 @@ async function runDoctor(root) {
|
|
|
33008
33525
|
const invalidCount = cases.filter((item) => !item.valid).length;
|
|
33009
33526
|
const testDir = join24(root, "tests", "e2e");
|
|
33010
33527
|
push("Test", [
|
|
33011
|
-
|
|
33012
|
-
|
|
33528
|
+
existsSync8(testDir) ? ok("tests/e2e") : error62("tests/e2e", "\u8FD0\u884C npx csspilot init \u521B\u5EFA"),
|
|
33529
|
+
existsSync8(join24(root, casesDir)) ? ok(`cases directory(${casesDir})`) : error62(`cases directory(${casesDir})`, "\u8FD0\u884C npx csspilot init \u521B\u5EFA"),
|
|
33013
33530
|
cases.length === 0 ? warn("Case validation \u8DF3\u8FC7(\u672A\u627E\u5230\u7528\u4F8B)", "\u5728 tests/e2e/cases \u521B\u5EFA Case") : invalidCount === 0 ? ok(`Case validation(${cases.length} \u4E2A\u7528\u4F8B\u5168\u90E8\u6709\u6548)`) : error62(`Case validation(${invalidCount}/${cases.length} \u4E2A\u7528\u4F8B\u6821\u9A8C\u5931\u8D25)`, "\u8FD0\u884C npx csspilot validate \u67E5\u770B\u8BE6\u60C5")
|
|
33014
33531
|
]);
|
|
33015
33532
|
const skillDir = join24(root, ".agents", "skills", "testpilot");
|
|
@@ -33018,7 +33535,7 @@ async function runDoctor(root) {
|
|
|
33018
33535
|
const bundledSkillVersion = skillSource ? readManifestVersion(skillSource) : void 0;
|
|
33019
33536
|
const skillItems = [
|
|
33020
33537
|
installedSkillVersion ? ok(`TestPilot Skill(.agents/skills/testpilot,v${installedSkillVersion})`) : error62("TestPilot Skill", "\u8FD0\u884C npx csspilot init \u5B89\u88C5"),
|
|
33021
|
-
|
|
33538
|
+
existsSync8(join24(skillDir, "references", "project.md")) ? ok("Project testing guide(references/)") : warn("Project testing guide \u7F3A\u5931", "references/ \u7531 init \u751F\u6210;\u53EF\u91CD\u65B0\u8FD0\u884C npx csspilot init")
|
|
33022
33539
|
];
|
|
33023
33540
|
if (installedSkillVersion && bundledSkillVersion && installedSkillVersion !== bundledSkillVersion) {
|
|
33024
33541
|
skillItems.push(
|
|
@@ -33113,7 +33630,7 @@ function makeDoctorCommand() {
|
|
|
33113
33630
|
import { join as join27 } from "node:path";
|
|
33114
33631
|
|
|
33115
33632
|
// src/commands/init/config-initializer.ts
|
|
33116
|
-
import { access as access3, mkdir as mkdir13, readFile as
|
|
33633
|
+
import { access as access3, mkdir as mkdir13, readFile as readFile10, writeFile as writeFile11 } from "node:fs/promises";
|
|
33117
33634
|
import { basename as basename2, join as join25 } from "node:path";
|
|
33118
33635
|
async function exists4(path) {
|
|
33119
33636
|
try {
|
|
@@ -33145,6 +33662,10 @@ environment:
|
|
|
33145
33662
|
staging:
|
|
33146
33663
|
baseUrl: \${STAGING_BASE_URL}
|
|
33147
33664
|
|
|
33665
|
+
# Case \u53EF\u89C1\u7684\u9879\u76EE\u53D8\u91CF\u767D\u540D\u5355(\u503C\u6765\u81EA .env / CI Secret;Case \u7528 \${variable.<name>} \u5F15\u7528)
|
|
33666
|
+
# variables:
|
|
33667
|
+
# ppmProjectId: \${PPM_PROJECT_ID}
|
|
33668
|
+
|
|
33148
33669
|
# \u9879\u76EE\u58F0\u660E\u7684\u6267\u884C\u7AEF(\u5B9E\u9645\u53EF\u7528\u6027\u4EE5 csspilot doctor \u68C0\u6D4B\u4E3A\u51C6)
|
|
33149
33670
|
adapters:
|
|
33150
33671
|
${adapters}
|
|
@@ -33195,7 +33716,7 @@ ${entry}
|
|
|
33195
33716
|
`, "utf8");
|
|
33196
33717
|
return "created";
|
|
33197
33718
|
}
|
|
33198
|
-
const content = await
|
|
33719
|
+
const content = await readFile10(gitignorePath, "utf8");
|
|
33199
33720
|
if (content.split(/\r?\n/).some((item) => item.trim() === entry)) {
|
|
33200
33721
|
return "exists";
|
|
33201
33722
|
}
|
|
@@ -33279,19 +33800,21 @@ function randomId() {
|
|
|
33279
33800
|
}
|
|
33280
33801
|
|
|
33281
33802
|
// src/commands/init/test-structure.ts
|
|
33282
|
-
import { existsSync as
|
|
33803
|
+
import { existsSync as existsSync9, readdirSync as readdirSync2 } from "node:fs";
|
|
33283
33804
|
import { mkdir as mkdir14, writeFile as writeFile12 } from "node:fs/promises";
|
|
33284
33805
|
import { join as join26 } from "node:path";
|
|
33285
33806
|
async function initTestStructure(root, _info) {
|
|
33286
33807
|
const dirs = {
|
|
33287
33808
|
cases: join26(root, "tests", "e2e", "cases"),
|
|
33288
33809
|
fixtures: join26(root, "tests", "e2e", "fixtures"),
|
|
33289
|
-
data: join26(root, "tests", "e2e", "data")
|
|
33810
|
+
data: join26(root, "tests", "e2e", "data"),
|
|
33811
|
+
reviews: join26(root, "tests", "e2e", "reviews")
|
|
33290
33812
|
};
|
|
33291
33813
|
const existed = {
|
|
33292
|
-
cases:
|
|
33293
|
-
fixtures:
|
|
33294
|
-
data:
|
|
33814
|
+
cases: existsSync9(dirs.cases),
|
|
33815
|
+
fixtures: existsSync9(dirs.fixtures),
|
|
33816
|
+
data: existsSync9(dirs.data),
|
|
33817
|
+
reviews: existsSync9(dirs.reviews)
|
|
33295
33818
|
};
|
|
33296
33819
|
for (const dir of Object.values(dirs)) {
|
|
33297
33820
|
await mkdir14(dir, { recursive: true });
|
|
@@ -33304,7 +33827,8 @@ async function initTestStructure(root, _info) {
|
|
|
33304
33827
|
return {
|
|
33305
33828
|
cases: existed.cases ? "reused" : "created",
|
|
33306
33829
|
fixtures: existed.fixtures ? "reused" : "created",
|
|
33307
|
-
data: existed.data ? "reused" : "created"
|
|
33830
|
+
data: existed.data ? "reused" : "created",
|
|
33831
|
+
reviews: existed.reviews ? "reused" : "created"
|
|
33308
33832
|
};
|
|
33309
33833
|
}
|
|
33310
33834
|
|
|
@@ -33344,7 +33868,7 @@ async function runInit(root, options = {}) {
|
|
|
33344
33868
|
const testStructure = await initTestStructure(root, project);
|
|
33345
33869
|
const reuseNote = project.existingE2eDirs.filter((dir) => dir !== "tests/e2e");
|
|
33346
33870
|
console.log(
|
|
33347
|
-
testStructure.cases === "reused" ? "\u2713 \u6D4B\u8BD5\u76EE\u5F55 tests/e2e(\u5DF2\u5B58\u5728,\u590D\u7528)" : "\u2713 \u6D4B\u8BD5\u76EE\u5F55 tests/e2e/{cases,fixtures,data}"
|
|
33871
|
+
testStructure.cases === "reused" ? "\u2713 \u6D4B\u8BD5\u76EE\u5F55 tests/e2e(\u5DF2\u5B58\u5728,\u590D\u7528)" : "\u2713 \u6D4B\u8BD5\u76EE\u5F55 tests/e2e/{cases,fixtures,data,reviews}"
|
|
33348
33872
|
);
|
|
33349
33873
|
if (reuseNote.length > 0) {
|
|
33350
33874
|
console.log(` \u26A0 \u68C0\u6D4B\u5230\u5176\u4ED6 E2E \u76EE\u5F55:${reuseNote.join(", ")}(\u4FDD\u7559\u4E0D\u52A8,\u672A\u5F3A\u5236\u8FC1\u79FB)`);
|
|
@@ -33487,7 +34011,7 @@ function makeRunCommand() {
|
|
|
33487
34011
|
].filter(Boolean).join(",");
|
|
33488
34012
|
console.log("");
|
|
33489
34013
|
console.log(
|
|
33490
|
-
`Run ${summary.runId}${summary.cancelled ? "(\u5DF2\u53D6\u6D88)" : ""}: ${summary.status.toUpperCase()} \u2014 \
|
|
34014
|
+
`Run ${summary.runId}${summary.cancelled ? "(\u5DF2\u53D6\u6D88)" : ""}: ${summary.status.toUpperCase()} \u2014 \u6A21\u677F ${summary.totals.templates ?? summary.totals.cases},\u6570\u636E\u884C \u2713${summary.totals.passed} \u2717${summary.totals.failed} \u21B7${summary.totals.skipped ?? 0} \u26A0${summary.totals.warnings ?? 0},\u6B65\u9AA4 \u2713${summary.totals.stepsPassed} \u2717${summary.totals.stepsFailed} \u21B7${summary.totals.stepsSkipped} \u26A0${summary.totals.stepsWarning ?? 0}${notes ? `(${notes})` : ""}`
|
|
33491
34015
|
);
|
|
33492
34016
|
console.log(`\u4EA7\u7269\u76EE\u5F55:${join28(".testpilot", "artifacts", "runs", summary.runId)}`);
|
|
33493
34017
|
console.log("\u67E5\u770B\u62A5\u544A:npx csspilot report");
|
|
@@ -33514,12 +34038,12 @@ function consoleEventHandler(event) {
|
|
|
33514
34038
|
`);
|
|
33515
34039
|
break;
|
|
33516
34040
|
case "case-started":
|
|
33517
|
-
console.log(`\u25B6 ${event.caseId} (${event.file})`);
|
|
34041
|
+
console.log(`\u25B6 ${event.caseId}#${event.rowId ?? "default"} (${event.file})`);
|
|
33518
34042
|
break;
|
|
33519
34043
|
case "step-finished": {
|
|
33520
|
-
const mark = event.status === "passed" ? " \u2713" : event.status === "failed" ? " \u2717" : " \u21B7";
|
|
34044
|
+
const mark = event.status === "passed" ? " \u2713" : event.status === "failed" ? " \u2717" : event.status === "warning" ? " \u26A0" : " \u21B7";
|
|
33521
34045
|
console.log(
|
|
33522
|
-
`${mark} ${event.target}/${event.action} ${event.durationMs}ms${event.error ? ` \u2014 ${event.error}` : ""}`
|
|
34046
|
+
`${mark} ${event.phase ?? "steps"} ${event.target}/${event.action} ${event.durationMs}ms${event.error ? ` \u2014 ${event.error}` : ""}`
|
|
33523
34047
|
);
|
|
33524
34048
|
break;
|
|
33525
34049
|
}
|
|
@@ -33529,7 +34053,7 @@ function consoleEventHandler(event) {
|
|
|
33529
34053
|
}
|
|
33530
34054
|
|
|
33531
34055
|
// src/commands/sync-metadata.ts
|
|
33532
|
-
import { relative } from "node:path";
|
|
34056
|
+
import { relative as relative2 } from "node:path";
|
|
33533
34057
|
function makeSyncMetadataCommand() {
|
|
33534
34058
|
return new Command("sync-metadata").description("\u540C\u6B65 Case Metadata \u5FEB\u7167\u5230 TestPilot Server(CI / git push \u540E\u4F7F\u7528)").option("--server <url>", "TestPilot Server \u5730\u5740(\u9ED8\u8BA4\u53D6 project.json \u6216 testpilot.yaml)").action(async (options) => {
|
|
33535
34059
|
const client = new TestPilotClient();
|
|
@@ -33556,7 +34080,7 @@ function makeSyncMetadataCommand() {
|
|
|
33556
34080
|
const cases = infos.filter((item) => item.valid && item.case).map((item) => ({
|
|
33557
34081
|
id: item.case.id,
|
|
33558
34082
|
title: item.case.name,
|
|
33559
|
-
file:
|
|
34083
|
+
file: relative2(process.cwd(), item.file).split("\\").join("/"),
|
|
33560
34084
|
tags: item.case.tags ?? []
|
|
33561
34085
|
}));
|
|
33562
34086
|
const skipped = infos.length - cases.length;
|
|
@@ -33666,7 +34190,7 @@ function makeValidateCommand() {
|
|
|
33666
34190
|
}
|
|
33667
34191
|
|
|
33668
34192
|
// src/cli.ts
|
|
33669
|
-
var CLI_VERSION = "0.
|
|
34193
|
+
var CLI_VERSION = "0.6.0";
|
|
33670
34194
|
async function createCli() {
|
|
33671
34195
|
const program2 = new Command();
|
|
33672
34196
|
program2.name("csspilot").description("TestPilot \u2014 AI Native \u8DE8\u7AEF\u4E1A\u52A1\u6D4B\u8BD5\u57FA\u7840\u8BBE\u65BD CLI").version(CLI_VERSION, "-v, --version", "output the current version").option("-V", "output the current version", () => {
|