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