csspilot 0.6.0 → 0.7.1
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/dist/bin.js +530 -91
- package/dist/index.js +530 -91
- package/dist/skills/testpilot/SKILL.md +84 -82
- package/dist/skills/testpilot/rules/case-design.md +47 -41
- package/dist/skills/testpilot/workflows/create-case.md +33 -20
- package/dist/skills/testpilot/workflows/test-requirements-review.md +143 -103
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8102,7 +8102,7 @@ var require_composer = __commonJS({
|
|
|
8102
8102
|
var node_process = __require("process");
|
|
8103
8103
|
var directives = require_directives();
|
|
8104
8104
|
var Document = require_Document();
|
|
8105
|
-
var
|
|
8105
|
+
var errors2 = require_errors();
|
|
8106
8106
|
var identity = require_identity();
|
|
8107
8107
|
var composeDoc = require_compose_doc();
|
|
8108
8108
|
var resolveEnd = require_resolve_end();
|
|
@@ -8149,9 +8149,9 @@ var require_composer = __commonJS({
|
|
|
8149
8149
|
this.onError = (source, code, message, warning) => {
|
|
8150
8150
|
const pos = getErrorPos(source);
|
|
8151
8151
|
if (warning)
|
|
8152
|
-
this.warnings.push(new
|
|
8152
|
+
this.warnings.push(new errors2.YAMLWarning(pos, code, message));
|
|
8153
8153
|
else
|
|
8154
|
-
this.errors.push(new
|
|
8154
|
+
this.errors.push(new errors2.YAMLParseError(pos, code, message));
|
|
8155
8155
|
};
|
|
8156
8156
|
this.directives = new directives.Directives({ version: options.version || "1.2" });
|
|
8157
8157
|
this.options = options;
|
|
@@ -8249,7 +8249,7 @@ ${cb}` : comment;
|
|
|
8249
8249
|
break;
|
|
8250
8250
|
case "error": {
|
|
8251
8251
|
const msg = token.source ? `${token.message}: ${JSON.stringify(token.source)}` : token.message;
|
|
8252
|
-
const error62 = new
|
|
8252
|
+
const error62 = new errors2.YAMLParseError(getErrorPos(token), "UNEXPECTED_TOKEN", msg);
|
|
8253
8253
|
if (this.atDirectives || !this.doc)
|
|
8254
8254
|
this.errors.push(error62);
|
|
8255
8255
|
else
|
|
@@ -8259,7 +8259,7 @@ ${cb}` : comment;
|
|
|
8259
8259
|
case "doc-end": {
|
|
8260
8260
|
if (!this.doc) {
|
|
8261
8261
|
const msg = "Unexpected doc-end without preceding document";
|
|
8262
|
-
this.errors.push(new
|
|
8262
|
+
this.errors.push(new errors2.YAMLParseError(getErrorPos(token), "UNEXPECTED_TOKEN", msg));
|
|
8263
8263
|
break;
|
|
8264
8264
|
}
|
|
8265
8265
|
this.doc.directives.docEnd = true;
|
|
@@ -8274,7 +8274,7 @@ ${end.comment}` : end.comment;
|
|
|
8274
8274
|
break;
|
|
8275
8275
|
}
|
|
8276
8276
|
default:
|
|
8277
|
-
this.errors.push(new
|
|
8277
|
+
this.errors.push(new errors2.YAMLParseError(getErrorPos(token), "UNEXPECTED_TOKEN", `Unsupported token ${token.type}`));
|
|
8278
8278
|
}
|
|
8279
8279
|
}
|
|
8280
8280
|
/**
|
|
@@ -8309,7 +8309,7 @@ var require_cst_scalar = __commonJS({
|
|
|
8309
8309
|
"use strict";
|
|
8310
8310
|
var resolveBlockScalar = require_resolve_block_scalar();
|
|
8311
8311
|
var resolveFlowScalar = require_resolve_flow_scalar();
|
|
8312
|
-
var
|
|
8312
|
+
var errors2 = require_errors();
|
|
8313
8313
|
var stringifyString = require_stringifyString();
|
|
8314
8314
|
function resolveAsScalar(token, strict = true, onError) {
|
|
8315
8315
|
if (token) {
|
|
@@ -8318,7 +8318,7 @@ var require_cst_scalar = __commonJS({
|
|
|
8318
8318
|
if (onError)
|
|
8319
8319
|
onError(offset, code, message);
|
|
8320
8320
|
else
|
|
8321
|
-
throw new
|
|
8321
|
+
throw new errors2.YAMLParseError([offset, offset + 1], code, message);
|
|
8322
8322
|
};
|
|
8323
8323
|
switch (token.type) {
|
|
8324
8324
|
case "scalar":
|
|
@@ -10213,7 +10213,7 @@ var require_public_api = __commonJS({
|
|
|
10213
10213
|
"use strict";
|
|
10214
10214
|
var composer = require_composer();
|
|
10215
10215
|
var Document = require_Document();
|
|
10216
|
-
var
|
|
10216
|
+
var errors2 = require_errors();
|
|
10217
10217
|
var log = require_log();
|
|
10218
10218
|
var identity = require_identity();
|
|
10219
10219
|
var lineCounter = require_line_counter();
|
|
@@ -10230,8 +10230,8 @@ var require_public_api = __commonJS({
|
|
|
10230
10230
|
const docs = Array.from(composer$1.compose(parser$1.parse(source)));
|
|
10231
10231
|
if (prettyErrors && lineCounter2)
|
|
10232
10232
|
for (const doc of docs) {
|
|
10233
|
-
doc.errors.forEach(
|
|
10234
|
-
doc.warnings.forEach(
|
|
10233
|
+
doc.errors.forEach(errors2.prettifyError(source, lineCounter2));
|
|
10234
|
+
doc.warnings.forEach(errors2.prettifyError(source, lineCounter2));
|
|
10235
10235
|
}
|
|
10236
10236
|
if (docs.length > 0)
|
|
10237
10237
|
return docs;
|
|
@@ -10246,13 +10246,13 @@ var require_public_api = __commonJS({
|
|
|
10246
10246
|
if (!doc)
|
|
10247
10247
|
doc = _doc;
|
|
10248
10248
|
else if (doc.options.logLevel !== "silent") {
|
|
10249
|
-
doc.errors.push(new
|
|
10249
|
+
doc.errors.push(new errors2.YAMLParseError(_doc.range.slice(0, 2), "MULTIPLE_DOCS", "Source contains multiple documents; please use YAML.parseAllDocuments()"));
|
|
10250
10250
|
break;
|
|
10251
10251
|
}
|
|
10252
10252
|
}
|
|
10253
10253
|
if (prettyErrors && lineCounter2) {
|
|
10254
|
-
doc.errors.forEach(
|
|
10255
|
-
doc.warnings.forEach(
|
|
10254
|
+
doc.errors.forEach(errors2.prettifyError(source, lineCounter2));
|
|
10255
|
+
doc.warnings.forEach(errors2.prettifyError(source, lineCounter2));
|
|
10256
10256
|
}
|
|
10257
10257
|
return doc;
|
|
10258
10258
|
}
|
|
@@ -10311,7 +10311,7 @@ var require_dist = __commonJS({
|
|
|
10311
10311
|
var composer = require_composer();
|
|
10312
10312
|
var Document = require_Document();
|
|
10313
10313
|
var Schema = require_Schema();
|
|
10314
|
-
var
|
|
10314
|
+
var errors2 = require_errors();
|
|
10315
10315
|
var Alias = require_Alias();
|
|
10316
10316
|
var identity = require_identity();
|
|
10317
10317
|
var Pair = require_Pair();
|
|
@@ -10327,9 +10327,9 @@ var require_dist = __commonJS({
|
|
|
10327
10327
|
exports.Composer = composer.Composer;
|
|
10328
10328
|
exports.Document = Document.Document;
|
|
10329
10329
|
exports.Schema = Schema.Schema;
|
|
10330
|
-
exports.YAMLError =
|
|
10331
|
-
exports.YAMLParseError =
|
|
10332
|
-
exports.YAMLWarning =
|
|
10330
|
+
exports.YAMLError = errors2.YAMLError;
|
|
10331
|
+
exports.YAMLParseError = errors2.YAMLParseError;
|
|
10332
|
+
exports.YAMLWarning = errors2.YAMLWarning;
|
|
10333
10333
|
exports.Alias = Alias.Alias;
|
|
10334
10334
|
exports.isAlias = identity.isAlias;
|
|
10335
10335
|
exports.isCollection = identity.isCollection;
|
|
@@ -10462,6 +10462,28 @@ var TestPilotError = class extends Error {
|
|
|
10462
10462
|
}
|
|
10463
10463
|
};
|
|
10464
10464
|
|
|
10465
|
+
// ../../packages/core/src/verdict.ts
|
|
10466
|
+
var INFRA_CODES = /* @__PURE__ */ new Set([
|
|
10467
|
+
"ENVIRONMENT_UNAVAILABLE",
|
|
10468
|
+
"ADAPTER_NOT_FOUND",
|
|
10469
|
+
"UNSUPPORTED_ACTION",
|
|
10470
|
+
"FIXTURE_MISSING",
|
|
10471
|
+
"ACCOUNT_UNAVAILABLE",
|
|
10472
|
+
"CONFIG_MISSING"
|
|
10473
|
+
]);
|
|
10474
|
+
var UNVERIFIED_CODES = /* @__PURE__ */ new Set(["PREREQ_VARIABLE_MISSING"]);
|
|
10475
|
+
function verdictFromErrorCode(code) {
|
|
10476
|
+
if (code !== void 0 && INFRA_CODES.has(code)) return "infra";
|
|
10477
|
+
if (code !== void 0 && UNVERIFIED_CODES.has(code)) return "unverified";
|
|
10478
|
+
return "product";
|
|
10479
|
+
}
|
|
10480
|
+
function caseStatusFromErrorCode(code) {
|
|
10481
|
+
const verdict = verdictFromErrorCode(code);
|
|
10482
|
+
if (verdict === "infra") return "blocked";
|
|
10483
|
+
if (verdict === "unverified") return "inconclusive";
|
|
10484
|
+
return "failed";
|
|
10485
|
+
}
|
|
10486
|
+
|
|
10465
10487
|
// ../../packages/core/src/constants/index.ts
|
|
10466
10488
|
import { join as join2 } from "node:path";
|
|
10467
10489
|
var TESTPILOT_DIR = ".testpilot";
|
|
@@ -30365,7 +30387,11 @@ var ACTIONS = [
|
|
|
30365
30387
|
"assert",
|
|
30366
30388
|
"extract",
|
|
30367
30389
|
"screenshot",
|
|
30368
|
-
"request"
|
|
30390
|
+
"request",
|
|
30391
|
+
"callMethod",
|
|
30392
|
+
"mockWxApi",
|
|
30393
|
+
"trigger",
|
|
30394
|
+
"setData"
|
|
30369
30395
|
];
|
|
30370
30396
|
var locatorSchema = external_exports.strictObject({
|
|
30371
30397
|
text: external_exports.string().min(1).optional(),
|
|
@@ -30393,7 +30419,25 @@ var stepSchema = external_exports.strictObject({
|
|
|
30393
30419
|
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(),
|
|
30394
30420
|
timeout: external_exports.number().int().positive().optional(),
|
|
30395
30421
|
/** UI 动作触发的网络请求精确次数断言 */
|
|
30396
|
-
expectRequests: external_exports.array(requestExpectationSchema).min(1).optional()
|
|
30422
|
+
expectRequests: external_exports.array(requestExpectationSchema).min(1).optional(),
|
|
30423
|
+
/** callMethod:要调用的 Page / 组件方法名(仅 miniapp;页面级调用不写 locator) */
|
|
30424
|
+
methodName: external_exports.string().min(1).optional(),
|
|
30425
|
+
/** callMethod 的入参(支持 ${var} 模板) */
|
|
30426
|
+
args: external_exports.array(external_exports.unknown()).optional(),
|
|
30427
|
+
/** mockWxApi:要 mock 的 wx.* API 名(如 showModal / request) */
|
|
30428
|
+
wxApi: external_exports.string().min(1).optional(),
|
|
30429
|
+
/** mockWxApi:mock 返回值(对象按原样注入;capture 时真实调用先上报再返回 mock 值) */
|
|
30430
|
+
result: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
|
|
30431
|
+
/** mockWxApi:捕获真实调用参数的变量名(${<capture>.options} / ${<capture>.called}) */
|
|
30432
|
+
capture: external_exports.string().min(1).optional(),
|
|
30433
|
+
/** trigger:触发的事件类型(如 getphonenumber;空 detail 即"用户拒绝/取消"类分支) */
|
|
30434
|
+
eventType: external_exports.string().min(1).optional(),
|
|
30435
|
+
/** trigger:作为事件 e.detail 注入(值支持 ${var} 模板) */
|
|
30436
|
+
detail: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
|
|
30437
|
+
/** setData:写入页面 data 的补丁(值支持 ${var} 模板) */
|
|
30438
|
+
patch: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
|
|
30439
|
+
/** assert:页面 data 路径断言(与 locator 互斥,仅 miniapp) */
|
|
30440
|
+
dataPath: external_exports.string().min(1).optional()
|
|
30397
30441
|
});
|
|
30398
30442
|
var datasetReferenceSchema = external_exports.strictObject({
|
|
30399
30443
|
file: external_exports.string().min(1),
|
|
@@ -30466,7 +30510,11 @@ var ADAPTER_SUPPORT = {
|
|
|
30466
30510
|
assert: ["web", "miniapp", "api"],
|
|
30467
30511
|
extract: ["web", "miniapp", "api"],
|
|
30468
30512
|
screenshot: ["web", "miniapp"],
|
|
30469
|
-
request: ["api"]
|
|
30513
|
+
request: ["api"],
|
|
30514
|
+
callMethod: ["miniapp"],
|
|
30515
|
+
mockWxApi: ["miniapp"],
|
|
30516
|
+
trigger: ["miniapp"],
|
|
30517
|
+
setData: ["miniapp"]
|
|
30470
30518
|
};
|
|
30471
30519
|
var LOCATOR_REQUIRED = /* @__PURE__ */ new Set(["click", "input", "select", "assert", "extract"]);
|
|
30472
30520
|
var VALUE_REQUIRED = /* @__PURE__ */ new Set(["input", "select"]);
|
|
@@ -30479,7 +30527,16 @@ var FIELD_ALLOWED = {
|
|
|
30479
30527
|
timeout: ["wait"],
|
|
30480
30528
|
method: ["request"],
|
|
30481
30529
|
headers: ["request"],
|
|
30482
|
-
body: ["request"]
|
|
30530
|
+
body: ["request"],
|
|
30531
|
+
methodName: ["callMethod"],
|
|
30532
|
+
args: ["callMethod"],
|
|
30533
|
+
wxApi: ["mockWxApi"],
|
|
30534
|
+
result: ["mockWxApi"],
|
|
30535
|
+
capture: ["mockWxApi"],
|
|
30536
|
+
eventType: ["trigger"],
|
|
30537
|
+
detail: ["trigger"],
|
|
30538
|
+
patch: ["setData"],
|
|
30539
|
+
dataPath: ["assert"]
|
|
30483
30540
|
};
|
|
30484
30541
|
var VARIABLE_REF = /\$\{([A-Za-z_][A-Za-z0-9_.]*)\}/g;
|
|
30485
30542
|
function validateCase(data) {
|
|
@@ -30543,6 +30600,23 @@ function checkStep(step, at, declared, issues) {
|
|
|
30543
30600
|
});
|
|
30544
30601
|
}
|
|
30545
30602
|
}
|
|
30603
|
+
if (step.action === "mockWxApi") {
|
|
30604
|
+
if (!step.wxApi) {
|
|
30605
|
+
issues.push({ path: `${at}.wxApi`, code: "semantic", message: "mockWxApi \u5FC5\u987B\u63D0\u4F9B wxApi(\u8981 mock \u7684 wx.* API \u540D)" });
|
|
30606
|
+
}
|
|
30607
|
+
if (!step.result) {
|
|
30608
|
+
issues.push({ path: `${at}.result`, code: "semantic", message: "mockWxApi \u5FC5\u987B\u63D0\u4F9B result(mock \u8FD4\u56DE\u503C)" });
|
|
30609
|
+
}
|
|
30610
|
+
}
|
|
30611
|
+
if (step.action === "trigger" && !step.eventType) {
|
|
30612
|
+
issues.push({ path: `${at}.eventType`, code: "semantic", message: "trigger \u5FC5\u987B\u63D0\u4F9B eventType(\u89E6\u53D1\u7684\u4E8B\u4EF6\u7C7B\u578B)" });
|
|
30613
|
+
}
|
|
30614
|
+
if (step.action === "setData" && (!step.patch || Object.keys(step.patch).length === 0)) {
|
|
30615
|
+
issues.push({ path: `${at}.patch`, code: "semantic", message: "setData \u5FC5\u987B\u63D0\u4F9B patch(\u5199\u5165\u9875\u9762 data \u7684\u8865\u4E01)" });
|
|
30616
|
+
}
|
|
30617
|
+
if (step.dataPath !== void 0 && step.target !== "miniapp") {
|
|
30618
|
+
issues.push({ path: `${at}.dataPath`, code: "semantic", message: "dataPath \u65AD\u8A00\u4EC5 miniapp \u652F\u6301(web \u7AEF\u7528 locator)" });
|
|
30619
|
+
}
|
|
30546
30620
|
if (step.action === "navigate" && !step.url) {
|
|
30547
30621
|
issues.push({ path: `${at}.url`, code: "semantic", message: "navigate \u5FC5\u987B\u63D0\u4F9B url" });
|
|
30548
30622
|
}
|
|
@@ -30574,6 +30648,13 @@ function checkStep(step, at, declared, issues) {
|
|
|
30574
30648
|
if (step.action === "wait" && !step.locator && step.timeout === void 0) {
|
|
30575
30649
|
issues.push({ path: at, code: "semantic", message: "wait \u9700\u8981\u63D0\u4F9B locator(\u7B49\u5F85\u5143\u7D20)\u6216 timeout(\u6BEB\u79D2)" });
|
|
30576
30650
|
}
|
|
30651
|
+
if (step.action === "callMethod" && !step.methodName) {
|
|
30652
|
+
issues.push({
|
|
30653
|
+
path: `${at}.methodName`,
|
|
30654
|
+
code: "semantic",
|
|
30655
|
+
message: "callMethod \u5FC5\u987B\u63D0\u4F9B methodName(\u9875\u9762\u6216\u7EC4\u4EF6\u7684\u65B9\u6CD5\u540D)"
|
|
30656
|
+
});
|
|
30657
|
+
}
|
|
30577
30658
|
if (step.expectRequests) {
|
|
30578
30659
|
if (step.target === "api" || !REQUEST_EXPECTATION_ACTIONS.has(step.action)) {
|
|
30579
30660
|
issues.push({
|
|
@@ -30615,11 +30696,17 @@ function checkVariableRefs(step, at, declared, issues) {
|
|
|
30615
30696
|
collect("value", step.value);
|
|
30616
30697
|
collect("expected", step.expected);
|
|
30617
30698
|
collect("url", step.url);
|
|
30699
|
+
collect("methodName", step.methodName);
|
|
30618
30700
|
collect("locator.text", step.locator?.text);
|
|
30619
30701
|
collect("locator.css", step.locator?.css);
|
|
30620
30702
|
if (typeof step.body === "string") collect("body", step.body);
|
|
30621
30703
|
else if (step.body) collect("body", JSON.stringify(step.body));
|
|
30622
30704
|
for (const headerValue of Object.values(step.headers ?? {})) collect("headers", headerValue);
|
|
30705
|
+
if (Array.isArray(step.args)) {
|
|
30706
|
+
step.args.forEach((arg, index) => {
|
|
30707
|
+
if (typeof arg === "string") collect(`args[${index}]`, arg);
|
|
30708
|
+
});
|
|
30709
|
+
}
|
|
30623
30710
|
for (const [index, expectation] of (step.expectRequests ?? []).entries()) {
|
|
30624
30711
|
collect(`expectRequests[${index}].urlContains`, expectation.urlContains);
|
|
30625
30712
|
}
|
|
@@ -30716,6 +30803,9 @@ async function loadCaseDataFile(reference, options) {
|
|
|
30716
30803
|
const [realDataDir, realTarget] = await Promise.all([
|
|
30717
30804
|
realpath(dataDir).catch(() => dataDir),
|
|
30718
30805
|
realpath(target).catch((error62) => {
|
|
30806
|
+
if (error62 instanceof Error && error62.code === "ENOENT") {
|
|
30807
|
+
throw new TestPilotError("FIXTURE_MISSING", `\u6570\u636E\u6587\u4EF6\u4E0D\u5B58\u5728:${reference}`);
|
|
30808
|
+
}
|
|
30719
30809
|
throw new Error(
|
|
30720
30810
|
`\u8BFB\u53D6\u6570\u636E\u6587\u4EF6\u5931\u8D25:${reference}:${error62 instanceof Error ? error62.message : String(error62)}`
|
|
30721
30811
|
);
|
|
@@ -30943,6 +31033,9 @@ async function validateCaseDataSource(source, file2, root) {
|
|
|
30943
31033
|
);
|
|
30944
31034
|
return validation;
|
|
30945
31035
|
} catch (error62) {
|
|
31036
|
+
if (error62 instanceof TestPilotError && error62.code === "FIXTURE_MISSING") {
|
|
31037
|
+
return validation;
|
|
31038
|
+
}
|
|
30946
31039
|
return {
|
|
30947
31040
|
ok: false,
|
|
30948
31041
|
data: validation.data,
|
|
@@ -30991,6 +31084,24 @@ function classifyFailure(message) {
|
|
|
30991
31084
|
}
|
|
30992
31085
|
return "other";
|
|
30993
31086
|
}
|
|
31087
|
+
var CODE_CATEGORIES = {
|
|
31088
|
+
ASSERTION_FAILED: "assert",
|
|
31089
|
+
REQUEST_COUNT_MISMATCH: "assert",
|
|
31090
|
+
STATUS_MISMATCH: "assert",
|
|
31091
|
+
STEP_TIMEOUT: "assert",
|
|
31092
|
+
ELEMENT_NOT_FOUND: "element",
|
|
31093
|
+
ADAPTER_NOT_FOUND: "adapter",
|
|
31094
|
+
UNSUPPORTED_ACTION: "adapter",
|
|
31095
|
+
CONFIG_MISSING: "adapter",
|
|
31096
|
+
ENVIRONMENT_UNAVAILABLE: "env",
|
|
31097
|
+
FIXTURE_MISSING: "env",
|
|
31098
|
+
ACCOUNT_UNAVAILABLE: "env",
|
|
31099
|
+
PREREQ_VARIABLE_MISSING: "other"
|
|
31100
|
+
};
|
|
31101
|
+
function classifyFromCode(code) {
|
|
31102
|
+
if (code === void 0) return void 0;
|
|
31103
|
+
return CODE_CATEGORIES[code];
|
|
31104
|
+
}
|
|
30994
31105
|
|
|
30995
31106
|
// ../../packages/reporter/src/history.ts
|
|
30996
31107
|
import { readFile as readFile5, readdir as readdir2 } from "node:fs/promises";
|
|
@@ -31048,7 +31159,7 @@ async function writeReports(runDir, summary, options = {}) {
|
|
|
31048
31159
|
const cases = summary.cases.map((item) => {
|
|
31049
31160
|
const categories = /* @__PURE__ */ new Set();
|
|
31050
31161
|
const steps = item.steps.map((step) => {
|
|
31051
|
-
const category = step.status === "failed" ? classifyFailure(step.error) : void 0;
|
|
31162
|
+
const category = classifyFromCode(step.errorCode) ?? (step.status === "failed" ? classifyFailure(step.error) : void 0);
|
|
31052
31163
|
if (category) {
|
|
31053
31164
|
categories.add(category);
|
|
31054
31165
|
failures.push({
|
|
@@ -31056,7 +31167,8 @@ async function writeReports(runDir, summary, options = {}) {
|
|
|
31056
31167
|
label: categoryLabel(category),
|
|
31057
31168
|
caseId: item.caseId,
|
|
31058
31169
|
index: step.index,
|
|
31059
|
-
message: step.error ?? ""
|
|
31170
|
+
message: step.error ?? "",
|
|
31171
|
+
verdict: verdictFromErrorCode(step.errorCode ?? item.errorCode)
|
|
31060
31172
|
});
|
|
31061
31173
|
}
|
|
31062
31174
|
return { ...step, category };
|
|
@@ -31079,6 +31191,20 @@ async function writeReports(runDir, summary, options = {}) {
|
|
|
31079
31191
|
await writeFile4(htmlPath, renderHtml(data), "utf8");
|
|
31080
31192
|
return { json: jsonPath, html: htmlPath };
|
|
31081
31193
|
}
|
|
31194
|
+
var VERDICT_GROUPS = [
|
|
31195
|
+
{ verdict: "product", title: "\u4EA7\u54C1\u5931\u8D25" },
|
|
31196
|
+
{ verdict: "infra", title: "\u73AF\u5883\u4E0E\u524D\u7F6E(\u975E\u4EA7\u54C1\u56DE\u5F52)" },
|
|
31197
|
+
{ verdict: "unverified", title: "\u672A\u9A8C\u8BC1" }
|
|
31198
|
+
];
|
|
31199
|
+
function verdictGroupsHtml(data) {
|
|
31200
|
+
return VERDICT_GROUPS.map((group) => {
|
|
31201
|
+
const items = data.failures.filter((failure2) => failure2.verdict === group.verdict);
|
|
31202
|
+
const list = items.length > 0 ? items.map(
|
|
31203
|
+
(failure2) => `<li><code>${escapeHtml(failure2.caseId)}</code> #${failure2.index} ${escapeHtml(failure2.message.slice(0, 160))}</li>`
|
|
31204
|
+
).join("\n") : '<li class="muted">\u65E0</li>';
|
|
31205
|
+
return `<div class="verdict-group"><h3>${escapeHtml(group.title)}(${items.length})</h3><ul>${list}</ul></div>`;
|
|
31206
|
+
}).join("\n");
|
|
31207
|
+
}
|
|
31082
31208
|
function categoryChips(data) {
|
|
31083
31209
|
const counts = /* @__PURE__ */ new Map();
|
|
31084
31210
|
for (const failure2 of data.failures) {
|
|
@@ -31104,19 +31230,20 @@ function renderHtml(data) {
|
|
|
31104
31230
|
const trendSvg = renderTrend(data);
|
|
31105
31231
|
const casesHtml = groupedCasesHtml(summary.cases);
|
|
31106
31232
|
const chips = categoryChips(data);
|
|
31233
|
+
const verdictHtml = verdictGroupsHtml(data);
|
|
31107
31234
|
return `<!doctype html>
|
|
31108
31235
|
<html lang="zh-CN">
|
|
31109
31236
|
<head>
|
|
31110
31237
|
<meta charset="utf-8" />
|
|
31111
31238
|
<title>TestPilot Report ${escapeHtml(summary.runId)}</title>
|
|
31112
31239
|
<style>
|
|
31113
|
-
:root { --ok:#1a7f37; --fail:#cf222e; --warn:#9a6700; --skip:#6e7781; --line:#d0d7de; --muted:#59636e; }
|
|
31240
|
+
:root { --ok:#1a7f37; --fail:#cf222e; --warn:#9a6700; --skip:#6e7781; --blocked:#8250df; --inconclusive:#9a6700; --line:#d0d7de; --muted:#59636e; }
|
|
31114
31241
|
* { box-sizing: border-box; }
|
|
31115
31242
|
body { font-family: system-ui, "Segoe UI", sans-serif; margin: 0; color: #1f2328; background: #f6f8fa; }
|
|
31116
31243
|
.wrap { max-width: 1080px; margin: 0 auto; padding: 20px 16px 48px; }
|
|
31117
31244
|
h1 { font-size: 20px; margin: 8px 0; } h2 { font-size: 15px; margin: 24px 0 8px; }
|
|
31118
31245
|
.badge { display: inline-block; padding: 2px 10px; border-radius: 10px; color: #fff; font-size: 12px; }
|
|
31119
|
-
.passed { background: var(--ok); } .failed { background: var(--fail); } .warning { background: var(--warn); } .skipped { background: var(--skip); }
|
|
31246
|
+
.passed { background: var(--ok); } .failed { background: var(--fail); } .warning { background: var(--warn); } .skipped { background: var(--skip); } .blocked { background: var(--blocked); } .inconclusive { background: var(--inconclusive); }
|
|
31120
31247
|
code { background: #eef1f4; padding: 1px 5px; border-radius: 4px; font-size: 12px; }
|
|
31121
31248
|
.cards { display: flex; gap: 12px; flex-wrap: wrap; margin: 14px 0; }
|
|
31122
31249
|
.card { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: 10px 16px; min-width: 130px; }
|
|
@@ -31143,6 +31270,11 @@ function renderHtml(data) {
|
|
|
31143
31270
|
.muted { color: var(--muted); }
|
|
31144
31271
|
.hide { display: none !important; }
|
|
31145
31272
|
.meta { color: var(--muted); font-size: 12px; margin: 6px 0; }
|
|
31273
|
+
.verdicts { display: flex; gap: 14px; flex-wrap: wrap; }
|
|
31274
|
+
.verdict-group { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: 10px 14px; flex: 1; min-width: 220px; }
|
|
31275
|
+
.verdict-group h3 { font-size: 13px; margin: 0 0 6px; }
|
|
31276
|
+
.verdict-group ul { margin: 0; padding-left: 18px; font-size: 12px; }
|
|
31277
|
+
.verdict-group li { margin: 3px 0; }
|
|
31146
31278
|
</style>
|
|
31147
31279
|
</head>
|
|
31148
31280
|
<body>
|
|
@@ -31163,6 +31295,9 @@ function renderHtml(data) {
|
|
|
31163
31295
|
<h2>\u5386\u53F2\u8D8B\u52BF(\u6700\u8FD1 ${data.history.length + 1} \u6B21\u8FD0\u884C)</h2>
|
|
31164
31296
|
<div class="trend">${trendSvg}</div>
|
|
31165
31297
|
|
|
31298
|
+
<h2>\u5931\u8D25\u4E09\u6876</h2>
|
|
31299
|
+
<div class="verdicts">${verdictHtml}</div>
|
|
31300
|
+
|
|
31166
31301
|
<h2>\u5931\u8D25\u5206\u7C7B</h2>
|
|
31167
31302
|
<div class="toolbar" id="cat-chips">${chips}</div>
|
|
31168
31303
|
|
|
@@ -31371,7 +31506,7 @@ function buildSummaryData(runs) {
|
|
|
31371
31506
|
for (const item of summary.cases) {
|
|
31372
31507
|
const platforms = [...new Set(item.steps.map((step) => step.target))];
|
|
31373
31508
|
const failedStep = item.steps.find((step) => step.status === "failed");
|
|
31374
|
-
const category = item.status === "failed" ? classifyFailure(failedStep?.error) : void 0;
|
|
31509
|
+
const category = classifyFromCode(item.errorCode ?? failedStep?.errorCode) ?? (item.status === "failed" ? classifyFailure(failedStep?.error) : void 0);
|
|
31375
31510
|
executions.push({
|
|
31376
31511
|
runId,
|
|
31377
31512
|
caseId: item.caseId,
|
|
@@ -31434,6 +31569,10 @@ function buildSummaryData(runs) {
|
|
|
31434
31569
|
})).filter((item) => item.count > 0);
|
|
31435
31570
|
const durations = runs.map((item) => item.summary.durationMs).filter((value) => typeof value === "number");
|
|
31436
31571
|
return {
|
|
31572
|
+
buckets: {
|
|
31573
|
+
blocked: executions.filter((exec2) => exec2.status === "blocked").length,
|
|
31574
|
+
inconclusive: executions.filter((exec2) => exec2.status === "inconclusive").length
|
|
31575
|
+
},
|
|
31437
31576
|
window: {
|
|
31438
31577
|
runs: runs.length,
|
|
31439
31578
|
from: runPoints[0]?.startedAt,
|
|
@@ -31518,7 +31657,7 @@ function renderSummaryHtml(data) {
|
|
|
31518
31657
|
th.num, td.num { text-align: right; }
|
|
31519
31658
|
code { background: #eef1f4; padding: 1px 5px; border-radius: 4px; font-size: 12px; cursor: pointer; }
|
|
31520
31659
|
.badge { display: inline-block; padding: 1px 9px; border-radius: 10px; color: #fff; font-size: 11px; }
|
|
31521
|
-
.passed { background: #1a7f37; } .failed { background: #cf222e; } .unknown { background: #6e7781; }
|
|
31660
|
+
.passed { background: #1a7f37; } .failed { background: #cf222e; } .unknown { background: #6e7781; } .blocked { background: #8250df; } .inconclusive { background: #9a6700; }
|
|
31522
31661
|
.toolbar { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin: 10px 0; }
|
|
31523
31662
|
.chip { border: 1px solid var(--line); background: #fff; border-radius: 14px; padding: 3px 12px; font-size: 12px; cursor: pointer; }
|
|
31524
31663
|
.chip.active { background: #0969da; color: #fff; border-color: #0969da; }
|
|
@@ -31544,6 +31683,8 @@ function renderSummaryHtml(data) {
|
|
|
31544
31683
|
<div class="card"><b>${rateText}</b><span>\u6574\u4F53\u901A\u8FC7\u7387</span></div>
|
|
31545
31684
|
<div class="card"><b>${(kpis.avgDurationMs / 1e3).toFixed(1)}s</b><span>\u5E73\u5747\u8FD0\u884C\u8017\u65F6</span></div>
|
|
31546
31685
|
<div class="card"><b>${kpis.flakyCount}</b><span>Flaky \u7528\u4F8B</span></div>
|
|
31686
|
+
${data.buckets.blocked > 0 ? `<div class="card"><b>${data.buckets.blocked} \u2298</b><span>\u73AF\u5883/\u524D\u7F6E\u963B\u585E(\u975E\u4EA7\u54C1)</span></div>` : ""}
|
|
31687
|
+
${data.buckets.inconclusive > 0 ? `<div class="card"><b>${data.buckets.inconclusive} ?</b><span>\u672A\u9A8C\u8BC1(\u8BC1\u636E\u4E0D\u8DB3)</span></div>` : ""}
|
|
31547
31688
|
</div>
|
|
31548
31689
|
|
|
31549
31690
|
<h2>\u8D8B\u52BF(\u6BCF\u6B21\u8FD0\u884C\u7ED3\u679C\u4E0E\u901A\u8FC7\u7387,\u60AC\u6D6E\u770B\u8BE6\u60C5)</h2>
|
|
@@ -31752,14 +31893,23 @@ var ExecutionContext = class {
|
|
|
31752
31893
|
}
|
|
31753
31894
|
}
|
|
31754
31895
|
/**
|
|
31755
|
-
* 将字符串中的 ${name}
|
|
31756
|
-
*
|
|
31896
|
+
* 将字符串中的 ${name} 替换为已提取的变量值;未定义的引用抛 TestPilotError——
|
|
31897
|
+
* 校验阶段已静态拦截"从未声明"的引用,运行时未命中即数据未兑现
|
|
31898
|
+
* (前置 extract 未执行或 fixture/dataset/凭据缺失),按三态归 unverified/account 桶。
|
|
31899
|
+
* 凭据桶仅限 `account` 整值或 `account.*` 字段;用户变量撞 account 前缀(如 accountId)
|
|
31900
|
+
* 不误归凭据桶。名字允许点号,用于引擎注入的 account.username 一类凭据变量
|
|
31901
|
+
* (用户 extract 变量名不含点号)。
|
|
31757
31902
|
*/
|
|
31758
31903
|
resolve(template) {
|
|
31759
|
-
return template.replace(
|
|
31760
|
-
|
|
31761
|
-
(
|
|
31762
|
-
|
|
31904
|
+
return template.replace(/\$\{([A-Za-z_][A-Za-z0-9_.]*)\}/g, (_match, name) => {
|
|
31905
|
+
const value = this.variables.get(name);
|
|
31906
|
+
if (value !== void 0) return value;
|
|
31907
|
+
const isAccount = name === "account" || name.startsWith("account.");
|
|
31908
|
+
throw new TestPilotError(
|
|
31909
|
+
isAccount ? "ACCOUNT_UNAVAILABLE" : "PREREQ_VARIABLE_MISSING",
|
|
31910
|
+
`\u53D8\u91CF "${name}" \u672A\u5B9A\u4E49(\u524D\u7F6E extract \u672A\u6267\u884C\u6216 fixture/dataset/\u51ED\u636E\u7F3A\u5931)`
|
|
31911
|
+
);
|
|
31912
|
+
});
|
|
31763
31913
|
}
|
|
31764
31914
|
/**
|
|
31765
31915
|
* 注入 accountRef 凭据:值为 JSON 对象时按 `${account.<字段>}` 展开为字符串,
|
|
@@ -31784,6 +31934,26 @@ var ExecutionContext = class {
|
|
|
31784
31934
|
}
|
|
31785
31935
|
this.variables.set("account", rawValue);
|
|
31786
31936
|
}
|
|
31937
|
+
/**
|
|
31938
|
+
* 注入结构化对象(如 mockWxApi 的捕获结果):按点号路径展开为平铺字符串变量。
|
|
31939
|
+
* 例如 seedObject('showModalCall', { called: true, args: { content: 'x' } }) 后,
|
|
31940
|
+
* `${showModalCall.called}` 与 `${showModalCall.args.content}` 均可解析。
|
|
31941
|
+
*/
|
|
31942
|
+
seedObject(name, value) {
|
|
31943
|
+
const flatten = (prefix, input2) => {
|
|
31944
|
+
if (input2 !== null && typeof input2 === "object" && !Array.isArray(input2)) {
|
|
31945
|
+
const entries = Object.entries(input2);
|
|
31946
|
+
if (entries.length === 0) {
|
|
31947
|
+
this.variables.set(prefix, "");
|
|
31948
|
+
return;
|
|
31949
|
+
}
|
|
31950
|
+
for (const [key, item] of entries) flatten(`${prefix}.${key}`, item);
|
|
31951
|
+
return;
|
|
31952
|
+
}
|
|
31953
|
+
this.variables.set(prefix, input2 === void 0 ? "" : String(input2));
|
|
31954
|
+
};
|
|
31955
|
+
flatten(name, value);
|
|
31956
|
+
}
|
|
31787
31957
|
};
|
|
31788
31958
|
|
|
31789
31959
|
// ../../packages/execution-engine/src/lifecycle/index.ts
|
|
@@ -31918,7 +32088,7 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
|
|
|
31918
32088
|
} : void 0;
|
|
31919
32089
|
if (step.expectRequests) {
|
|
31920
32090
|
if (!adapter.startRequestCapture || !adapter.stopRequestCapture) {
|
|
31921
|
-
throw new
|
|
32091
|
+
throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301\u8BF7\u6C42\u6B21\u6570\u65AD\u8A00`);
|
|
31922
32092
|
}
|
|
31923
32093
|
await adapter.startRequestCapture();
|
|
31924
32094
|
captureStarted = true;
|
|
@@ -31942,14 +32112,22 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
|
|
|
31942
32112
|
case "wait":
|
|
31943
32113
|
await adapter.wait(locator, step.timeout);
|
|
31944
32114
|
break;
|
|
31945
|
-
case "assert":
|
|
32115
|
+
case "assert": {
|
|
32116
|
+
if (step.dataPath !== void 0) {
|
|
32117
|
+
if (!adapter.assertData) {
|
|
32118
|
+
throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 dataPath \u65AD\u8A00`);
|
|
32119
|
+
}
|
|
32120
|
+
await adapter.assertData(context.resolve(step.dataPath), context.resolve(step.expected ?? ""));
|
|
32121
|
+
break;
|
|
32122
|
+
}
|
|
31946
32123
|
if (step.target === "api") {
|
|
31947
|
-
if (!adapter.assertResponse) throw new
|
|
32124
|
+
if (!adapter.assertResponse) throw new TestPilotError("UNSUPPORTED_ACTION", "\u5F53\u524D adapter \u4E0D\u652F\u6301 api \u65AD\u8A00");
|
|
31948
32125
|
await adapter.assertResponse(context.resolve(step.expected ?? ""));
|
|
31949
32126
|
} else {
|
|
31950
32127
|
await adapter.assert(locator, context.resolve(step.expected ?? ""));
|
|
31951
32128
|
}
|
|
31952
32129
|
break;
|
|
32130
|
+
}
|
|
31953
32131
|
case "extract": {
|
|
31954
32132
|
const value = step.target === "api" ? await extractApiResponse(adapter, context.resolve(step.value ?? "")) : await adapter.extract(locator);
|
|
31955
32133
|
if (step.variable) {
|
|
@@ -31964,7 +32142,7 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
|
|
|
31964
32142
|
break;
|
|
31965
32143
|
}
|
|
31966
32144
|
case "request": {
|
|
31967
|
-
if (!adapter.request) throw new
|
|
32145
|
+
if (!adapter.request) throw new TestPilotError("UNSUPPORTED_ACTION", "\u5F53\u524D adapter \u4E0D\u652F\u6301 api request");
|
|
31968
32146
|
const method = step.method ?? "GET";
|
|
31969
32147
|
const url2 = context.resolve(step.url ?? "");
|
|
31970
32148
|
const headers = resolveHeaders(step.headers, context);
|
|
@@ -31981,16 +32159,53 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
|
|
|
31981
32159
|
if (step.expected !== void 0) {
|
|
31982
32160
|
const expectedStatus = context.resolve(step.expected);
|
|
31983
32161
|
if (String(response.status) !== expectedStatus) {
|
|
31984
|
-
throw new
|
|
32162
|
+
throw new TestPilotError(
|
|
32163
|
+
"STATUS_MISMATCH",
|
|
31985
32164
|
`HTTP \u72B6\u6001\u7801 ${response.status} ${response.statusText},\u671F\u671B ${expectedStatus}:${snippet(response.bodyText, 200)}`
|
|
31986
32165
|
);
|
|
31987
32166
|
}
|
|
31988
32167
|
}
|
|
31989
32168
|
break;
|
|
31990
32169
|
}
|
|
32170
|
+
case "callMethod": {
|
|
32171
|
+
if (typeof adapter.callMethod !== "function") {
|
|
32172
|
+
throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 callMethod`);
|
|
32173
|
+
}
|
|
32174
|
+
const methodName = context.resolve(step.methodName ?? "");
|
|
32175
|
+
if (!methodName) throw new TestPilotError("MISSING_METHOD_NAME", "callMethod \u5FC5\u987B\u63D0\u4F9B methodName");
|
|
32176
|
+
const callArgs = (step.args ?? []).map((arg) => resolveDeep(arg, context));
|
|
32177
|
+
await adapter.callMethod(locator, methodName, callArgs);
|
|
32178
|
+
break;
|
|
32179
|
+
}
|
|
32180
|
+
case "mockWxApi": {
|
|
32181
|
+
if (typeof adapter.mockWxApi !== "function") {
|
|
32182
|
+
throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 mockWxApi`);
|
|
32183
|
+
}
|
|
32184
|
+
await adapter.mockWxApi({
|
|
32185
|
+
api: context.resolve(step.wxApi ?? ""),
|
|
32186
|
+
result: step.result ? resolveDeep(step.result, context) : void 0,
|
|
32187
|
+
capture: step.capture
|
|
32188
|
+
});
|
|
32189
|
+
break;
|
|
32190
|
+
}
|
|
32191
|
+
case "trigger": {
|
|
32192
|
+
if (typeof adapter.trigger !== "function") {
|
|
32193
|
+
throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 trigger`);
|
|
32194
|
+
}
|
|
32195
|
+
if (!locator) throw new TestPilotError("MISSING_LOCATOR", "trigger \u5FC5\u987B\u63D0\u4F9B locator(\u8981\u89E6\u53D1\u4E8B\u4EF6\u7684\u7EC4\u4EF6)");
|
|
32196
|
+
await adapter.trigger(locator, context.resolve(step.eventType ?? ""), step.detail ? resolveDeep(step.detail, context) : void 0);
|
|
32197
|
+
break;
|
|
32198
|
+
}
|
|
32199
|
+
case "setData": {
|
|
32200
|
+
if (typeof adapter.setData !== "function") {
|
|
32201
|
+
throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 setData`);
|
|
32202
|
+
}
|
|
32203
|
+
await adapter.setData(resolveDeep(step.patch ?? {}, context));
|
|
32204
|
+
break;
|
|
32205
|
+
}
|
|
31991
32206
|
default: {
|
|
31992
32207
|
const exhaustive = step.action;
|
|
31993
|
-
throw new
|
|
32208
|
+
throw new TestPilotError("UNSUPPORTED_ACTION", `\u4E0D\u652F\u6301\u7684 action:${String(exhaustive)}`);
|
|
31994
32209
|
}
|
|
31995
32210
|
}
|
|
31996
32211
|
if (step.expectRequests) {
|
|
@@ -32001,7 +32216,8 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
|
|
|
32001
32216
|
result.requestAssertions = buildRequestAssertions(step, requests, context);
|
|
32002
32217
|
const mismatches = result.requestAssertions.filter((item) => item.actualCount !== item.count);
|
|
32003
32218
|
if (mismatches.length > 0) {
|
|
32004
|
-
throw new
|
|
32219
|
+
throw new TestPilotError(
|
|
32220
|
+
"REQUEST_COUNT_MISMATCH",
|
|
32005
32221
|
mismatches.map(
|
|
32006
32222
|
(item) => `\u8BF7\u6C42\u6B21\u6570\u4E0D\u5339\u914D:${item.method ?? "*"} ${item.urlContains},\u5B9E\u9645 ${item.actualCount},\u671F\u671B ${item.count}`
|
|
32007
32223
|
).join("; ")
|
|
@@ -32018,6 +32234,7 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
|
|
|
32018
32234
|
}
|
|
32019
32235
|
result.status = "failed";
|
|
32020
32236
|
result.error = err instanceof Error ? err.message : String(err);
|
|
32237
|
+
result.errorCode = err instanceof TestPilotError ? err.code : void 0;
|
|
32021
32238
|
try {
|
|
32022
32239
|
const data = await adapter.screenshot();
|
|
32023
32240
|
result.screenshot = await artifacts.saveStepScreenshot(`${options.screenshotName}-failed.png`, data);
|
|
@@ -32028,8 +32245,8 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
|
|
|
32028
32245
|
return result;
|
|
32029
32246
|
}
|
|
32030
32247
|
async function extractApiResponse(adapter, path) {
|
|
32031
|
-
if (!adapter.extractResponse) throw new
|
|
32032
|
-
if (!path) throw new
|
|
32248
|
+
if (!adapter.extractResponse) throw new TestPilotError("UNSUPPORTED_ACTION", "\u5F53\u524D adapter \u4E0D\u652F\u6301 api \u63D0\u53D6");
|
|
32249
|
+
if (!path) throw new TestPilotError("UNSUPPORTED_ACTION", "api \u7AEF extract \u9700\u8981\u63D0\u4F9B value(\u70B9\u53F7 JSON path)");
|
|
32033
32250
|
return adapter.extractResponse(path);
|
|
32034
32251
|
}
|
|
32035
32252
|
function resolveHeaders(headers, context) {
|
|
@@ -32103,6 +32320,7 @@ var ExecutionEngine = class {
|
|
|
32103
32320
|
const warnings = [];
|
|
32104
32321
|
const evidenceAdapters = /* @__PURE__ */ new Map();
|
|
32105
32322
|
let caseError;
|
|
32323
|
+
let caseErrorCode;
|
|
32106
32324
|
const executionId = safeArtifactName(`${data.id}-${rowId}`);
|
|
32107
32325
|
await bus.emit({
|
|
32108
32326
|
type: "case-started",
|
|
@@ -32173,6 +32391,12 @@ var ExecutionEngine = class {
|
|
|
32173
32391
|
result = await executeStep(step, index, adapter, context, artifacts, {
|
|
32174
32392
|
screenshotName: `${executionId}-${phase}-${String(index).padStart(2, "0")}`
|
|
32175
32393
|
});
|
|
32394
|
+
if (step.capture !== void 0 && typeof adapter.readWxApiCaptures === "function") {
|
|
32395
|
+
const captures = adapter.readWxApiCaptures();
|
|
32396
|
+
if (captures && captures.calls.length > 0) {
|
|
32397
|
+
context.seedObject(step.capture, captures.calls[0]?.options ?? null);
|
|
32398
|
+
}
|
|
32399
|
+
}
|
|
32176
32400
|
} catch (err) {
|
|
32177
32401
|
result = {
|
|
32178
32402
|
index,
|
|
@@ -32180,7 +32404,8 @@ var ExecutionEngine = class {
|
|
|
32180
32404
|
action: step.action,
|
|
32181
32405
|
status: "failed",
|
|
32182
32406
|
durationMs: 0,
|
|
32183
|
-
error: err instanceof Error ? err.message : String(err)
|
|
32407
|
+
error: err instanceof Error ? err.message : String(err),
|
|
32408
|
+
errorCode: err instanceof TestPilotError ? err.code : void 0
|
|
32184
32409
|
};
|
|
32185
32410
|
}
|
|
32186
32411
|
result.phase = phase;
|
|
@@ -32209,6 +32434,7 @@ var ExecutionEngine = class {
|
|
|
32209
32434
|
if (result.status === "failed") {
|
|
32210
32435
|
passed = false;
|
|
32211
32436
|
caseError = result.error;
|
|
32437
|
+
caseErrorCode = result.errorCode;
|
|
32212
32438
|
for (const [skipIndex, skippedStep] of phaseSteps.entries()) {
|
|
32213
32439
|
if (skipIndex <= index) continue;
|
|
32214
32440
|
steps.push({
|
|
@@ -32225,24 +32451,42 @@ var ExecutionEngine = class {
|
|
|
32225
32451
|
}
|
|
32226
32452
|
return passed;
|
|
32227
32453
|
};
|
|
32228
|
-
|
|
32229
|
-
|
|
32230
|
-
|
|
32231
|
-
} else {
|
|
32232
|
-
for (const [index, step] of data.steps.entries()) {
|
|
32454
|
+
if (options.blockedError) {
|
|
32455
|
+
const first = data.setup?.[0] ?? data.steps[0];
|
|
32456
|
+
if (first) {
|
|
32233
32457
|
steps.push({
|
|
32234
|
-
index,
|
|
32235
|
-
phase: "
|
|
32236
|
-
target:
|
|
32237
|
-
action:
|
|
32238
|
-
status: "
|
|
32239
|
-
durationMs: 0
|
|
32458
|
+
index: 0,
|
|
32459
|
+
phase: "setup",
|
|
32460
|
+
target: first.target,
|
|
32461
|
+
action: first.action,
|
|
32462
|
+
status: "failed",
|
|
32463
|
+
durationMs: 0,
|
|
32464
|
+
error: options.blockedError.message,
|
|
32465
|
+
errorCode: options.blockedError.code
|
|
32240
32466
|
});
|
|
32241
32467
|
}
|
|
32468
|
+
caseError = options.blockedError.message;
|
|
32469
|
+
caseErrorCode = options.blockedError.code;
|
|
32470
|
+
} else {
|
|
32471
|
+
const setupPassed = await runPhase("setup", data.setup ?? []);
|
|
32472
|
+
if (setupPassed) {
|
|
32473
|
+
await runPhase("steps", data.steps);
|
|
32474
|
+
} else {
|
|
32475
|
+
for (const [index, step] of data.steps.entries()) {
|
|
32476
|
+
steps.push({
|
|
32477
|
+
index,
|
|
32478
|
+
phase: "steps",
|
|
32479
|
+
target: step.target,
|
|
32480
|
+
action: step.action,
|
|
32481
|
+
status: "skipped",
|
|
32482
|
+
durationMs: 0
|
|
32483
|
+
});
|
|
32484
|
+
}
|
|
32485
|
+
}
|
|
32486
|
+
await runPhase("teardown", data.teardown ?? []);
|
|
32242
32487
|
}
|
|
32243
|
-
await runPhase("teardown", data.teardown ?? []);
|
|
32244
32488
|
const finishedAt = /* @__PURE__ */ new Date();
|
|
32245
|
-
const status = caseError ?
|
|
32489
|
+
const status = caseError ? caseStatusFromErrorCode(caseErrorCode) : "passed";
|
|
32246
32490
|
const caseResult = {
|
|
32247
32491
|
caseId: data.id,
|
|
32248
32492
|
caseName: data.name,
|
|
@@ -32256,6 +32500,7 @@ var ExecutionEngine = class {
|
|
|
32256
32500
|
durationMs: finishedAt.getTime() - startedAt.getTime()
|
|
32257
32501
|
};
|
|
32258
32502
|
if (caseError) caseResult.error = caseError;
|
|
32503
|
+
if (caseErrorCode) caseResult.errorCode = caseErrorCode;
|
|
32259
32504
|
if (warnings.length > 0) caseResult.warnings = warnings;
|
|
32260
32505
|
for (const [target, adapter] of evidenceAdapters) {
|
|
32261
32506
|
if (!adapter.stopEvidence) continue;
|
|
@@ -32273,6 +32518,9 @@ var ExecutionEngine = class {
|
|
|
32273
32518
|
);
|
|
32274
32519
|
}
|
|
32275
32520
|
}
|
|
32521
|
+
for (const [, adapter] of evidenceAdapters) {
|
|
32522
|
+
await adapter.restoreWxMock?.().catch(() => void 0);
|
|
32523
|
+
}
|
|
32276
32524
|
await bus.emit({
|
|
32277
32525
|
type: "case-finished",
|
|
32278
32526
|
runId,
|
|
@@ -32299,6 +32547,8 @@ function buildRunSummary(runId, startedAt, cases, options = {}) {
|
|
|
32299
32547
|
passed: cases.filter((item) => item.status === "passed").length,
|
|
32300
32548
|
failed: cases.filter((item) => item.status === "failed").length,
|
|
32301
32549
|
skipped: cases.filter((item) => item.status === "skipped").length,
|
|
32550
|
+
...cases.some((item) => item.status === "blocked") ? { blocked: cases.filter((item) => item.status === "blocked").length } : {},
|
|
32551
|
+
...cases.some((item) => item.status === "inconclusive") ? { inconclusive: cases.filter((item) => item.status === "inconclusive").length } : {},
|
|
32302
32552
|
warnings: 0,
|
|
32303
32553
|
steps: 0,
|
|
32304
32554
|
stepsPassed: 0,
|
|
@@ -32323,7 +32573,8 @@ function buildRunSummary(runId, startedAt, cases, options = {}) {
|
|
|
32323
32573
|
startedAt: startedAt.toISOString(),
|
|
32324
32574
|
finishedAt: finishedAt.toISOString(),
|
|
32325
32575
|
durationMs: finishedAt.getTime() - startedAt.getTime(),
|
|
32326
|
-
|
|
32576
|
+
// run 状态按三桶优先级推导:产品失败 > 证据不足 > 环境阻塞 > 通过 > 跳过
|
|
32577
|
+
status: totals.failed > 0 ? "failed" : (totals.inconclusive ?? 0) > 0 ? "inconclusive" : (totals.blocked ?? 0) > 0 ? "blocked" : totals.passed > 0 || totals.cases === 0 ? "passed" : "skipped",
|
|
32327
32578
|
...options.cancelled ? { cancelled: true } : {},
|
|
32328
32579
|
cases,
|
|
32329
32580
|
totals
|
|
@@ -32375,6 +32626,7 @@ var TestRunner = class {
|
|
|
32375
32626
|
rowIndex: item.rowIndex ?? 0,
|
|
32376
32627
|
initialVariables: item.initialVariables ?? {},
|
|
32377
32628
|
missingDependencies: item.missingDependencies ?? [],
|
|
32629
|
+
blockedError: item.blockedError,
|
|
32378
32630
|
artifacts,
|
|
32379
32631
|
bus,
|
|
32380
32632
|
resolver: this.resolver,
|
|
@@ -32431,6 +32683,11 @@ import { existsSync as existsSync3 } from "node:fs";
|
|
|
32431
32683
|
import { mkdtemp, readFile as readFile6, rm } from "node:fs/promises";
|
|
32432
32684
|
import { tmpdir } from "node:os";
|
|
32433
32685
|
import { join as join16 } from "node:path";
|
|
32686
|
+
import { errors } from "playwright";
|
|
32687
|
+
var TEXT_WAIT_MS = 1e4;
|
|
32688
|
+
function delay2(ms) {
|
|
32689
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
32690
|
+
}
|
|
32434
32691
|
var PlaywrightAdapter = class {
|
|
32435
32692
|
constructor(bundle, options = {}) {
|
|
32436
32693
|
this.bundle = bundle;
|
|
@@ -32487,36 +32744,63 @@ var PlaywrightAdapter = class {
|
|
|
32487
32744
|
const options = this.options;
|
|
32488
32745
|
const page = await this.ensurePage();
|
|
32489
32746
|
const target = options.baseUrl && !/^https?:\/\//i.test(url2) ? new URL(url2, options.baseUrl).toString() : url2;
|
|
32490
|
-
|
|
32747
|
+
try {
|
|
32748
|
+
await page.goto(target);
|
|
32749
|
+
} catch (err) {
|
|
32750
|
+
if (err instanceof errors.TimeoutError) {
|
|
32751
|
+
throw new TestPilotError("STEP_TIMEOUT", err.message, { cause: err });
|
|
32752
|
+
}
|
|
32753
|
+
if (err instanceof Error && err.message.includes("net::ERR")) {
|
|
32754
|
+
throw new TestPilotError("ENVIRONMENT_UNAVAILABLE", err.message, { cause: err });
|
|
32755
|
+
}
|
|
32756
|
+
throw err;
|
|
32757
|
+
}
|
|
32491
32758
|
}
|
|
32492
32759
|
async click(locator) {
|
|
32493
|
-
|
|
32760
|
+
const target = await this.toLocator(locator);
|
|
32761
|
+
await this.coded(() => target.click());
|
|
32494
32762
|
}
|
|
32495
32763
|
async input(locator, value) {
|
|
32496
|
-
|
|
32764
|
+
const target = await this.toLocator(locator);
|
|
32765
|
+
await this.coded(() => target.fill(value));
|
|
32497
32766
|
}
|
|
32498
32767
|
async select(locator, value) {
|
|
32499
|
-
|
|
32768
|
+
const target = await this.toLocator(locator);
|
|
32769
|
+
await this.coded(() => target.selectOption(value));
|
|
32500
32770
|
}
|
|
32501
32771
|
async wait(locator, timeoutMs) {
|
|
32502
32772
|
const page = await this.ensurePage();
|
|
32503
32773
|
if (locator) {
|
|
32504
|
-
await this.toLocatorOn(page, locator).waitFor({ state: "visible", timeout: timeoutMs });
|
|
32774
|
+
await this.coded(() => this.toLocatorOn(page, locator).waitFor({ state: "visible", timeout: timeoutMs }));
|
|
32505
32775
|
return;
|
|
32506
32776
|
}
|
|
32507
32777
|
await page.waitForTimeout(timeoutMs ?? 1e3);
|
|
32508
32778
|
}
|
|
32509
32779
|
async assert(locator, expected) {
|
|
32510
32780
|
const target = await this.toLocator(locator);
|
|
32511
|
-
await target.waitFor({ state: "visible" });
|
|
32512
|
-
const actual = (await target.innerText()).trim();
|
|
32513
32781
|
const wanted = expected.trim();
|
|
32514
|
-
|
|
32515
|
-
|
|
32782
|
+
const startedAt = Date.now();
|
|
32783
|
+
for (; ; ) {
|
|
32784
|
+
await this.coded(() => target.waitFor({ state: "visible" }));
|
|
32785
|
+
const actual = (await this.coded(() => target.innerText())).trim();
|
|
32786
|
+
if (actual.includes(wanted)) return;
|
|
32787
|
+
if (Date.now() - startedAt > TEXT_WAIT_MS) {
|
|
32788
|
+
throw new TestPilotError(
|
|
32789
|
+
"ASSERTION_FAILED",
|
|
32790
|
+
`\u65AD\u8A00\u5931\u8D25(\u7B49\u5F85 ${TEXT_WAIT_MS}ms):\u671F\u671B\u5305\u542B "${wanted}",\u5B9E\u9645\u4E3A "${actual.slice(0, 200)}"`
|
|
32791
|
+
);
|
|
32792
|
+
}
|
|
32793
|
+
await delay2(300);
|
|
32516
32794
|
}
|
|
32517
32795
|
}
|
|
32518
32796
|
async extract(locator) {
|
|
32519
|
-
|
|
32797
|
+
const target = await this.toLocator(locator);
|
|
32798
|
+
const startedAt = Date.now();
|
|
32799
|
+
for (; ; ) {
|
|
32800
|
+
const text = (await this.coded(() => target.innerText())).trim();
|
|
32801
|
+
if (text.length > 0 || Date.now() - startedAt > TEXT_WAIT_MS) return text;
|
|
32802
|
+
await delay2(300);
|
|
32803
|
+
}
|
|
32520
32804
|
}
|
|
32521
32805
|
async screenshot() {
|
|
32522
32806
|
return (await this.ensurePage()).screenshot();
|
|
@@ -32534,6 +32818,17 @@ var PlaywrightAdapter = class {
|
|
|
32534
32818
|
async toLocator(locator) {
|
|
32535
32819
|
return this.toLocatorOn(await this.ensurePage(), locator);
|
|
32536
32820
|
}
|
|
32821
|
+
/** 定位类操作的统一兜底:等待超时按「元素没等到」归类(可能是回归也可能是 selector 腐烂,不允许静默) */
|
|
32822
|
+
async coded(fn) {
|
|
32823
|
+
try {
|
|
32824
|
+
return await fn();
|
|
32825
|
+
} catch (err) {
|
|
32826
|
+
if (err instanceof errors.TimeoutError) {
|
|
32827
|
+
throw new TestPilotError("ELEMENT_NOT_FOUND", err.message, { cause: err });
|
|
32828
|
+
}
|
|
32829
|
+
throw err;
|
|
32830
|
+
}
|
|
32831
|
+
}
|
|
32537
32832
|
toLocatorOn(page, locator) {
|
|
32538
32833
|
if (locator.text !== void 0) {
|
|
32539
32834
|
return page.getByText(locator.text, { exact: true });
|
|
@@ -32570,6 +32865,8 @@ import net from "node:net";
|
|
|
32570
32865
|
var AUTO_PORT_FALLBACK_WAIT_MS = 9e4;
|
|
32571
32866
|
var NAVIGATE_RETRY_WINDOW_MS = 45e3;
|
|
32572
32867
|
var ELEMENT_WAIT_MS = 1e4;
|
|
32868
|
+
var TEXT_WAIT_MS2 = 1e4;
|
|
32869
|
+
var PAGE_READY_WAIT_MS = 3e4;
|
|
32573
32870
|
var CONNECTION_LOST = /connection closed|not connected/i;
|
|
32574
32871
|
var automatorLauncher = {
|
|
32575
32872
|
launch: async (options) => {
|
|
@@ -32583,7 +32880,7 @@ var automatorLauncher = {
|
|
|
32583
32880
|
}
|
|
32584
32881
|
}
|
|
32585
32882
|
};
|
|
32586
|
-
function
|
|
32883
|
+
function delay3(ms) {
|
|
32587
32884
|
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
32588
32885
|
}
|
|
32589
32886
|
async function connectWithRetry(wsEndpoint, timeoutMs) {
|
|
@@ -32594,7 +32891,7 @@ async function connectWithRetry(wsEndpoint, timeoutMs) {
|
|
|
32594
32891
|
return await automator.connect({ wsEndpoint });
|
|
32595
32892
|
} catch (err) {
|
|
32596
32893
|
lastError = err;
|
|
32597
|
-
await
|
|
32894
|
+
await delay3(1e3);
|
|
32598
32895
|
}
|
|
32599
32896
|
}
|
|
32600
32897
|
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
@@ -32611,12 +32908,12 @@ function startCliAuto(cliPath, projectPath, port) {
|
|
|
32611
32908
|
child.stderr?.on("data", (chunk) => {
|
|
32612
32909
|
output2 += chunk.toString("utf8");
|
|
32613
32910
|
});
|
|
32614
|
-
child.once("error", (err) => rejectPromise(new
|
|
32911
|
+
child.once("error", (err) => rejectPromise(new TestPilotError("ENVIRONMENT_UNAVAILABLE", `\u62C9\u8D77\u5F00\u53D1\u8005\u5DE5\u5177 cli \u5931\u8D25:${err.message}`)));
|
|
32615
32912
|
child.once("exit", (code) => {
|
|
32616
32913
|
if (code === 0) {
|
|
32617
32914
|
resolvePromise();
|
|
32618
32915
|
} else {
|
|
32619
|
-
rejectPromise(new
|
|
32916
|
+
rejectPromise(new TestPilotError("ENVIRONMENT_UNAVAILABLE", `cli auto \u9000\u51FA\u7801 ${code}:${output2.slice(-400)}`));
|
|
32620
32917
|
}
|
|
32621
32918
|
});
|
|
32622
32919
|
const startedAt = Date.now();
|
|
@@ -32629,7 +32926,7 @@ function startCliAuto(cliPath, projectPath, port) {
|
|
|
32629
32926
|
socket.once("error", () => {
|
|
32630
32927
|
socket.destroy();
|
|
32631
32928
|
if (Date.now() - startedAt > AUTO_PORT_FALLBACK_WAIT_MS) {
|
|
32632
|
-
rejectPromise(new
|
|
32929
|
+
rejectPromise(new TestPilotError("ENVIRONMENT_UNAVAILABLE", `\u7B49\u5F85\u5F00\u53D1\u8005\u5DE5\u5177\u81EA\u52A8\u5316\u7AEF\u53E3 ${port} \u8D85\u65F6;\u8BF7\u786E\u8BA4:1) \u5DE5\u5177\u5DF2\u767B\u5F55 2) \u8BBE\u7F6E->\u5B89\u5168\u8BBE\u7F6E->\u670D\u52A1\u7AEF\u53E3\u5DF2\u5F00\u542F`));
|
|
32633
32930
|
} else {
|
|
32634
32931
|
setTimeout(poll, 500);
|
|
32635
32932
|
}
|
|
@@ -32663,14 +32960,31 @@ var MiniAppAdapter = class {
|
|
|
32663
32960
|
await this.runWithReconnect(async () => {
|
|
32664
32961
|
await (await this.getProgram()).reLaunch(url2);
|
|
32665
32962
|
});
|
|
32963
|
+
await this.waitForPageReady();
|
|
32666
32964
|
return;
|
|
32667
32965
|
} catch (err) {
|
|
32668
32966
|
lastError = err;
|
|
32669
|
-
await
|
|
32967
|
+
await delay3(3e3);
|
|
32670
32968
|
}
|
|
32671
32969
|
}
|
|
32672
32970
|
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
32673
32971
|
}
|
|
32972
|
+
/** 等待页面就绪:模拟器编译期间 currentPage / data 会失败或挂起,轮询至可响应 */
|
|
32973
|
+
async waitForPageReady(timeoutMs = PAGE_READY_WAIT_MS) {
|
|
32974
|
+
const startedAt = Date.now();
|
|
32975
|
+
for (; ; ) {
|
|
32976
|
+
const page = await (await this.getProgram()).currentPage();
|
|
32977
|
+
const data = await page.data().catch(() => void 0);
|
|
32978
|
+
if (data !== void 0 && data !== null) return;
|
|
32979
|
+
if (Date.now() - startedAt > timeoutMs) {
|
|
32980
|
+
throw new TestPilotError(
|
|
32981
|
+
"PAGE_READY_TIMEOUT",
|
|
32982
|
+
`\u7B49\u5F85\u9875\u9762\u5C31\u7EEA\u8D85\u65F6(${timeoutMs}ms):\u6A21\u62DF\u5668\u53EF\u80FD\u4ECD\u5728\u7F16\u8BD1,\u6216\u9879\u76EE\u672A\u88AB\u4FE1\u4EFB`
|
|
32983
|
+
);
|
|
32984
|
+
}
|
|
32985
|
+
await delay3(300);
|
|
32986
|
+
}
|
|
32987
|
+
}
|
|
32674
32988
|
async click(locator) {
|
|
32675
32989
|
await this.runWithReconnect(async () => (await this.element(locator)).tap());
|
|
32676
32990
|
}
|
|
@@ -32678,7 +32992,7 @@ var MiniAppAdapter = class {
|
|
|
32678
32992
|
await this.runWithReconnect(async () => (await this.element(locator)).input(value));
|
|
32679
32993
|
}
|
|
32680
32994
|
async select() {
|
|
32681
|
-
throw new
|
|
32995
|
+
throw new TestPilotError("UNSUPPORTED_ACTION", "\u5C0F\u7A0B\u5E8F\u7AEF\u6682\u4E0D\u652F\u6301 select(picker \u4EA4\u4E92),\u8BF7\u6539\u7528 click \u5B8C\u6210\u9009\u62E9");
|
|
32682
32996
|
}
|
|
32683
32997
|
async wait(locator, timeoutMs) {
|
|
32684
32998
|
await this.runWithReconnect(async () => {
|
|
@@ -32693,17 +33007,100 @@ var MiniAppAdapter = class {
|
|
|
32693
33007
|
async assert(locator, expected) {
|
|
32694
33008
|
await this.runWithReconnect(async () => {
|
|
32695
33009
|
const element = await this.element(locator);
|
|
32696
|
-
const actual = (await element.text()).trim();
|
|
32697
33010
|
const wanted = expected.trim();
|
|
32698
|
-
|
|
32699
|
-
|
|
33011
|
+
const startedAt = Date.now();
|
|
33012
|
+
for (; ; ) {
|
|
33013
|
+
const actual = (await element.text()).trim();
|
|
33014
|
+
if (actual.includes(wanted)) return;
|
|
33015
|
+
if (Date.now() - startedAt > TEXT_WAIT_MS2) {
|
|
33016
|
+
throw new TestPilotError(
|
|
33017
|
+
"ASSERTION_FAILED",
|
|
33018
|
+
`\u65AD\u8A00\u5931\u8D25(\u7B49\u5F85 ${TEXT_WAIT_MS2}ms):\u671F\u671B\u5305\u542B "${wanted}",\u5B9E\u9645\u4E3A "${actual.slice(0, 200)}"`
|
|
33019
|
+
);
|
|
33020
|
+
}
|
|
33021
|
+
await delay3(300);
|
|
32700
33022
|
}
|
|
32701
33023
|
});
|
|
32702
33024
|
}
|
|
32703
33025
|
async extract(locator) {
|
|
32704
33026
|
return this.runWithReconnect(async () => {
|
|
32705
33027
|
const element = await this.element(locator);
|
|
32706
|
-
|
|
33028
|
+
const startedAt = Date.now();
|
|
33029
|
+
for (; ; ) {
|
|
33030
|
+
const text = (await element.text()).trim();
|
|
33031
|
+
if (text.length > 0 || Date.now() - startedAt > TEXT_WAIT_MS2) return text;
|
|
33032
|
+
await delay3(300);
|
|
33033
|
+
}
|
|
33034
|
+
});
|
|
33035
|
+
}
|
|
33036
|
+
/** 调用 Page / 组件方法;不传 locator 时为页面级调用(Page 实例方法) */
|
|
33037
|
+
async callMethod(locator, methodName, args = []) {
|
|
33038
|
+
return this.runWithReconnect(async () => {
|
|
33039
|
+
if (locator) {
|
|
33040
|
+
const element = await this.element(locator);
|
|
33041
|
+
return element.callMethod(methodName, ...args);
|
|
33042
|
+
}
|
|
33043
|
+
const page = await (await this.getProgram()).currentPage();
|
|
33044
|
+
return page.callMethod(methodName, ...args);
|
|
33045
|
+
});
|
|
33046
|
+
}
|
|
33047
|
+
// ---- miniapp 深度测试(V0.2)----
|
|
33048
|
+
wxMock;
|
|
33049
|
+
/** mock 任意 wx.* API:拦截调用并返回 mock 值;capture 记录真实调用参数(最近调用在前) */
|
|
33050
|
+
async mockWxApi(options) {
|
|
33051
|
+
if (this.wxMock?.api === options.api) await this.restoreWxMock();
|
|
33052
|
+
const program2 = await this.getProgram();
|
|
33053
|
+
const captured = [];
|
|
33054
|
+
this.wxMock = { api: options.api, captured };
|
|
33055
|
+
const callbackName = options.capture ? `__tpWxCapture_${options.capture}` : void 0;
|
|
33056
|
+
if (callbackName) {
|
|
33057
|
+
await program2.exposeFunction(callbackName, (payload) => {
|
|
33058
|
+
if (payload !== null && typeof payload === "object") {
|
|
33059
|
+
captured.push(payload);
|
|
33060
|
+
}
|
|
33061
|
+
});
|
|
33062
|
+
}
|
|
33063
|
+
await program2.mockWxMethod(
|
|
33064
|
+
options.api,
|
|
33065
|
+
function(invokeOptions, cbName) {
|
|
33066
|
+
const callbacks = globalThis;
|
|
33067
|
+
callbacks[cbName]?.({ options: invokeOptions });
|
|
33068
|
+
return options.result ?? this.origin(invokeOptions);
|
|
33069
|
+
},
|
|
33070
|
+
callbackName
|
|
33071
|
+
);
|
|
33072
|
+
}
|
|
33073
|
+
/** 读取 mock 期间捕获的调用参数(最近调用在前) */
|
|
33074
|
+
readCapturedWxApiCalls(api) {
|
|
33075
|
+
return this.wxMock?.api === api ? [...this.wxMock.captured].reverse() : [];
|
|
33076
|
+
}
|
|
33077
|
+
/** 恢复 mock 的 wx API(用例结束由引擎调用) */
|
|
33078
|
+
async restoreWxMock() {
|
|
33079
|
+
const mock = this.wxMock;
|
|
33080
|
+
if (!mock) return;
|
|
33081
|
+
this.wxMock = void 0;
|
|
33082
|
+
if (!this.program) return;
|
|
33083
|
+
await this.program.restoreWxMethod(mock.api).catch(() => void 0);
|
|
33084
|
+
}
|
|
33085
|
+
/** 触发组件事件(trigger;detail 作为 e.detail 注入,空 detail 即"用户拒绝/取消"类分支) */
|
|
33086
|
+
async trigger(locator, eventType, detail) {
|
|
33087
|
+
await this.runWithReconnect(async () => {
|
|
33088
|
+
const element = await this.element(locator);
|
|
33089
|
+
await element.trigger(eventType, detail);
|
|
33090
|
+
});
|
|
33091
|
+
}
|
|
33092
|
+
/** 读取页面 data(点号路径,缺省为整个 data 对象) */
|
|
33093
|
+
async getData(dataPath) {
|
|
33094
|
+
return this.runWithReconnect(async () => {
|
|
33095
|
+
const page = await (await this.getProgram()).currentPage();
|
|
33096
|
+
return page.data(dataPath);
|
|
33097
|
+
});
|
|
33098
|
+
}
|
|
33099
|
+
/** 写入页面 data 补丁(setData 驱动 onShow/onPullDownRefresh 等真实链路) */
|
|
33100
|
+
async setData(patch) {
|
|
33101
|
+
await this.runWithReconnect(async () => {
|
|
33102
|
+
const page = await (await this.getProgram()).currentPage();
|
|
33103
|
+
await page.setData(patch);
|
|
32707
33104
|
});
|
|
32708
33105
|
}
|
|
32709
33106
|
async screenshot() {
|
|
@@ -32794,9 +33191,9 @@ var MiniAppAdapter = class {
|
|
|
32794
33191
|
const element = await page.$(selector);
|
|
32795
33192
|
if (element) return element;
|
|
32796
33193
|
if (Date.now() - startedAt > ELEMENT_WAIT_MS) {
|
|
32797
|
-
throw new
|
|
33194
|
+
throw new TestPilotError("ELEMENT_NOT_FOUND", `\u7B49\u5F85\u5143\u7D20\u8D85\u65F6(${ELEMENT_WAIT_MS}ms):${selector}`);
|
|
32798
33195
|
}
|
|
32799
|
-
await
|
|
33196
|
+
await delay3(500);
|
|
32800
33197
|
}
|
|
32801
33198
|
}
|
|
32802
33199
|
};
|
|
@@ -32872,7 +33269,7 @@ var ApiAdapter = class {
|
|
|
32872
33269
|
signal: AbortSignal.timeout(this.options.timeoutMs ?? 15e3)
|
|
32873
33270
|
});
|
|
32874
33271
|
} catch (err) {
|
|
32875
|
-
throw new
|
|
33272
|
+
throw new TestPilotError("ENVIRONMENT_UNAVAILABLE", `API \u8BF7\u6C42\u5931\u8D25 ${request.method.toUpperCase()} ${url2}:${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
32876
33273
|
}
|
|
32877
33274
|
const bodyText = await res.text();
|
|
32878
33275
|
let json2;
|
|
@@ -32894,7 +33291,7 @@ var ApiAdapter = class {
|
|
|
32894
33291
|
if (!this.last) throw new Error("\u5C1A\u672A\u53D1\u8D77\u4EFB\u4F55 API \u8BF7\u6C42,\u65E0\u6CD5\u65AD\u8A00\u54CD\u5E94");
|
|
32895
33292
|
if (!this.last.bodyText.includes(expected)) {
|
|
32896
33293
|
const snippet2 = this.last.bodyText.slice(0, 200);
|
|
32897
|
-
throw new
|
|
33294
|
+
throw new TestPilotError("ASSERTION_FAILED", `\u54CD\u5E94\u4F53\u4E0D\u5305\u542B "${expected}":HTTP ${this.last.status} ${snippet2}`);
|
|
32898
33295
|
}
|
|
32899
33296
|
}
|
|
32900
33297
|
async extractResponse(path) {
|
|
@@ -33023,7 +33420,23 @@ async function runCases(options = {}) {
|
|
|
33023
33420
|
variables: config2.variables,
|
|
33024
33421
|
accounts: options.accounts
|
|
33025
33422
|
}
|
|
33026
|
-
)
|
|
33423
|
+
).catch((err) => {
|
|
33424
|
+
if (err instanceof TestPilotError && err.code === "FIXTURE_MISSING") {
|
|
33425
|
+
return [
|
|
33426
|
+
{
|
|
33427
|
+
data: item.case,
|
|
33428
|
+
file: item.file,
|
|
33429
|
+
templateId: item.case.id,
|
|
33430
|
+
rowId: "default",
|
|
33431
|
+
rowIndex: 0,
|
|
33432
|
+
initialVariables: {},
|
|
33433
|
+
missingDependencies: [],
|
|
33434
|
+
blockedError: { code: err.code, message: err.message }
|
|
33435
|
+
}
|
|
33436
|
+
];
|
|
33437
|
+
}
|
|
33438
|
+
throw err;
|
|
33439
|
+
})
|
|
33027
33440
|
)
|
|
33028
33441
|
)).flat();
|
|
33029
33442
|
const summary = await runner.run(
|
|
@@ -33260,7 +33673,7 @@ async function collectEnvVarRefs(root) {
|
|
|
33260
33673
|
}
|
|
33261
33674
|
|
|
33262
33675
|
// src/commands/init/project-detector.ts
|
|
33263
|
-
import { existsSync as existsSync6, readFileSync as readFileSync2 } from "node:fs";
|
|
33676
|
+
import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
33264
33677
|
import { basename, join as join22 } from "node:path";
|
|
33265
33678
|
|
|
33266
33679
|
// src/lib/git.ts
|
|
@@ -33316,10 +33729,31 @@ function detectProjectType(root) {
|
|
|
33316
33729
|
if (isWeb) return "web";
|
|
33317
33730
|
return "unknown";
|
|
33318
33731
|
}
|
|
33732
|
+
function detectLanguageFromSources(root) {
|
|
33733
|
+
const SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "miniprogram_npm", "dist", ".git"]);
|
|
33734
|
+
const found = { ts: false, js: false };
|
|
33735
|
+
const walk = (dir, depth) => {
|
|
33736
|
+
if (depth > 4 || found.ts && found.js) return;
|
|
33737
|
+
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
33738
|
+
if (entry.name.startsWith(".")) continue;
|
|
33739
|
+
const full = join22(dir, entry.name);
|
|
33740
|
+
if (entry.isDirectory()) {
|
|
33741
|
+
if (!SKIP_DIRS.has(entry.name)) walk(full, depth + 1);
|
|
33742
|
+
continue;
|
|
33743
|
+
}
|
|
33744
|
+
if (/\.(ts|tsx)$/.test(entry.name)) found.ts = true;
|
|
33745
|
+
else if (/\.(js|cjs|mjs|wxml)$/.test(entry.name)) found.js = true;
|
|
33746
|
+
}
|
|
33747
|
+
};
|
|
33748
|
+
walk(root, 0);
|
|
33749
|
+
if (found.ts) return "TypeScript";
|
|
33750
|
+
if (found.js) return "JavaScript";
|
|
33751
|
+
return "Unknown";
|
|
33752
|
+
}
|
|
33319
33753
|
async function detectProject(root) {
|
|
33320
33754
|
const pkg = readPackageJson(root);
|
|
33321
33755
|
const type = detectProjectType(root);
|
|
33322
|
-
const language = existsSync6(join22(root, "tsconfig.json")) ? "TypeScript" : pkg || existsSync6(join22(root, "src")) ? "JavaScript" :
|
|
33756
|
+
const language = existsSync6(join22(root, "tsconfig.json")) ? "TypeScript" : pkg || existsSync6(join22(root, "src")) ? "JavaScript" : detectLanguageFromSources(root);
|
|
33323
33757
|
const packageManager = existsSync6(join22(root, "pnpm-lock.yaml")) ? "pnpm" : existsSync6(join22(root, "yarn.lock")) ? "yarn" : existsSync6(join22(root, "package-lock.json")) ? "npm" : "npx";
|
|
33324
33758
|
const existingE2eDirs = E2E_DIR_CANDIDATES.filter((dir) => existsSync6(join22(root, dir)));
|
|
33325
33759
|
return {
|
|
@@ -33799,7 +34233,7 @@ function randomId() {
|
|
|
33799
34233
|
}
|
|
33800
34234
|
|
|
33801
34235
|
// src/commands/init/test-structure.ts
|
|
33802
|
-
import { existsSync as existsSync9, readdirSync as
|
|
34236
|
+
import { existsSync as existsSync9, readdirSync as readdirSync3 } from "node:fs";
|
|
33803
34237
|
import { mkdir as mkdir14, writeFile as writeFile12 } from "node:fs/promises";
|
|
33804
34238
|
import { join as join26 } from "node:path";
|
|
33805
34239
|
async function initTestStructure(root, _info) {
|
|
@@ -33819,7 +34253,7 @@ async function initTestStructure(root, _info) {
|
|
|
33819
34253
|
await mkdir14(dir, { recursive: true });
|
|
33820
34254
|
}
|
|
33821
34255
|
for (const [name, dir] of Object.entries(dirs)) {
|
|
33822
|
-
if (!existed[name] &&
|
|
34256
|
+
if (!existed[name] && readdirSync3(dir).length === 0) {
|
|
33823
34257
|
await writeFile12(join26(dir, ".gitkeep"), "", "utf8").catch(() => void 0);
|
|
33824
34258
|
}
|
|
33825
34259
|
}
|
|
@@ -33989,6 +34423,11 @@ function openInBrowser(file2) {
|
|
|
33989
34423
|
|
|
33990
34424
|
// src/commands/run.ts
|
|
33991
34425
|
import { join as join28 } from "node:path";
|
|
34426
|
+
function exitCodeForSummary(status) {
|
|
34427
|
+
if (status === "failed" || status === "inconclusive") return 1;
|
|
34428
|
+
if (status === "blocked") return 2;
|
|
34429
|
+
return 0;
|
|
34430
|
+
}
|
|
33992
34431
|
function makeRunCommand() {
|
|
33993
34432
|
return new Command("run").description("\u6267\u884C\u6D4B\u8BD5\u7528\u4F8B(\u9ED8\u8BA4\u8FD0\u884C testpilot.yaml \u914D\u7F6E\u7684 Case)").argument("[paths...]", "\u7528\u4F8B\u6587\u4EF6\u6216\u76EE\u5F55,\u9ED8\u8BA4\u6309\u9879\u76EE\u914D\u7F6E").option("--tag <tag>", "\u6309\u6807\u7B7E\u8FC7\u6EE4\u7528\u4F8B").action(async (paths, options) => {
|
|
33994
34433
|
const client = new TestPilotClient();
|
|
@@ -34010,11 +34449,11 @@ function makeRunCommand() {
|
|
|
34010
34449
|
].filter(Boolean).join(",");
|
|
34011
34450
|
console.log("");
|
|
34012
34451
|
console.log(
|
|
34013
|
-
`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})` : ""}`
|
|
34452
|
+
`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} \u2298${summary.totals.blocked ?? 0} ?${summary.totals.inconclusive ?? 0} \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})` : ""}`
|
|
34014
34453
|
);
|
|
34015
34454
|
console.log(`\u4EA7\u7269\u76EE\u5F55:${join28(".testpilot", "artifacts", "runs", summary.runId)}`);
|
|
34016
34455
|
console.log("\u67E5\u770B\u62A5\u544A:npx csspilot report");
|
|
34017
|
-
|
|
34456
|
+
process.exitCode = exitCodeForSummary(summary.status);
|
|
34018
34457
|
} catch (err) {
|
|
34019
34458
|
console.error(err instanceof Error ? err.message : err);
|
|
34020
34459
|
process.exitCode = 1;
|
|
@@ -34189,7 +34628,7 @@ function makeValidateCommand() {
|
|
|
34189
34628
|
}
|
|
34190
34629
|
|
|
34191
34630
|
// src/cli.ts
|
|
34192
|
-
var CLI_VERSION = "0.
|
|
34631
|
+
var CLI_VERSION = "0.7.1";
|
|
34193
34632
|
async function createCli() {
|
|
34194
34633
|
const program2 = new Command();
|
|
34195
34634
|
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", () => {
|