csspilot 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -7
- package/dist/bin.js +880 -93
- package/dist/index.js +880 -93
- package/dist/skills/testpilot/manifest.yaml +4 -1
- package/dist/skills/testpilot/rules/case-design.md +47 -47
- package/dist/skills/testpilot/rules/dsl.md +121 -13
- package/dist/skills/testpilot/workflows/create-case.md +33 -33
- package/dist/skills/testpilot/workflows/run-case.md +1 -1
- package/dist/skills/testpilot/workflows/test-requirements-review.md +43 -6
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -30210,9 +30210,10 @@ var configSchema = external_exports.strictObject({
|
|
|
30210
30210
|
function interpolateEnvRefs(value) {
|
|
30211
30211
|
return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (match, name) => process.env[name] ?? match);
|
|
30212
30212
|
}
|
|
30213
|
+
var ENV_TESTING_FILE = ".env.testing";
|
|
30213
30214
|
function loadProjectEnvFile(root = process.cwd()) {
|
|
30214
30215
|
try {
|
|
30215
|
-
process.loadEnvFile(join3(root,
|
|
30216
|
+
process.loadEnvFile(join3(root, ENV_TESTING_FILE));
|
|
30216
30217
|
} catch {
|
|
30217
30218
|
}
|
|
30218
30219
|
}
|
|
@@ -30278,8 +30279,8 @@ async function loadTestpilotConfig(cwd = process.cwd()) {
|
|
|
30278
30279
|
}
|
|
30279
30280
|
|
|
30280
30281
|
// ../../packages/core/src/wechat-devtools.ts
|
|
30281
|
-
import { execFile } from "node:child_process";
|
|
30282
|
-
import { existsSync, readdirSync } from "node:fs";
|
|
30282
|
+
import { exec, execFile } from "node:child_process";
|
|
30283
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
30283
30284
|
import { join as join4 } from "node:path";
|
|
30284
30285
|
var CLI_NAMES = ["cli.bat", "cli"];
|
|
30285
30286
|
var REGISTRY_UNINSTALL_KEYS = [
|
|
@@ -30366,6 +30367,42 @@ function execToString(command, args) {
|
|
|
30366
30367
|
});
|
|
30367
30368
|
});
|
|
30368
30369
|
}
|
|
30370
|
+
async function isWechatDevToolsLoggedIn(cliPath) {
|
|
30371
|
+
const output2 = await execCliToString(cliPath, ["islogin"]).catch(() => void 0);
|
|
30372
|
+
if (output2 === void 0) return void 0;
|
|
30373
|
+
if (/is\s*login\s*[:=]?\s*(true|yes|1)\b/i.test(output2)) return true;
|
|
30374
|
+
if (/is\s*login\s*[:=]?\s*(false|no|0)\b/i.test(output2)) return false;
|
|
30375
|
+
if (/\btrue\b/i.test(output2)) return true;
|
|
30376
|
+
if (/\bfalse\b/i.test(output2)) return false;
|
|
30377
|
+
return void 0;
|
|
30378
|
+
}
|
|
30379
|
+
function execCliToString(cliPath, args) {
|
|
30380
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
30381
|
+
const quote = (value) => /\s/.test(value) ? `"${value}"` : value;
|
|
30382
|
+
exec(
|
|
30383
|
+
`"${cliPath}" ${args.map(quote).join(" ")}`,
|
|
30384
|
+
{ windowsHide: true, timeout: 3e4, encoding: "buffer" },
|
|
30385
|
+
(err, stdout, stderr) => {
|
|
30386
|
+
const text = `${stdout.toString("utf8")}${stderr.toString("utf8")}`.trim();
|
|
30387
|
+
if (err && !text) {
|
|
30388
|
+
rejectPromise(err);
|
|
30389
|
+
return;
|
|
30390
|
+
}
|
|
30391
|
+
resolvePromise(text);
|
|
30392
|
+
}
|
|
30393
|
+
);
|
|
30394
|
+
});
|
|
30395
|
+
}
|
|
30396
|
+
function readMiniappProjectAppid(projectPath) {
|
|
30397
|
+
const configPath = join4(projectPath, "project.config.json");
|
|
30398
|
+
if (!existsSync(configPath)) return void 0;
|
|
30399
|
+
try {
|
|
30400
|
+
const parsed = JSON.parse(readFileSync(configPath, "utf8"));
|
|
30401
|
+
return { appid: typeof parsed.appid === "string" ? parsed.appid : void 0, configPath };
|
|
30402
|
+
} catch {
|
|
30403
|
+
return void 0;
|
|
30404
|
+
}
|
|
30405
|
+
}
|
|
30369
30406
|
|
|
30370
30407
|
// ../../packages/sdk/src/client.ts
|
|
30371
30408
|
import { isAbsolute as isAbsolute4, join as join19 } from "node:path";
|
|
@@ -30387,8 +30424,20 @@ var ACTIONS = [
|
|
|
30387
30424
|
"assert",
|
|
30388
30425
|
"extract",
|
|
30389
30426
|
"screenshot",
|
|
30390
|
-
"request"
|
|
30427
|
+
"request",
|
|
30428
|
+
"callMethod",
|
|
30429
|
+
"mockWxApi",
|
|
30430
|
+
"trigger",
|
|
30431
|
+
"setData",
|
|
30432
|
+
"longpress",
|
|
30433
|
+
"callWxApi",
|
|
30434
|
+
"touch",
|
|
30435
|
+
"scroll",
|
|
30436
|
+
"swipeTo",
|
|
30437
|
+
"slideTo",
|
|
30438
|
+
"moveTo"
|
|
30391
30439
|
];
|
|
30440
|
+
var TOUCH_PHASES = ["touchstart", "touchmove", "touchend"];
|
|
30392
30441
|
var locatorSchema = external_exports.strictObject({
|
|
30393
30442
|
text: external_exports.string().min(1).optional(),
|
|
30394
30443
|
css: external_exports.string().min(1).optional()
|
|
@@ -30404,7 +30453,7 @@ var stepSchema = external_exports.strictObject({
|
|
|
30404
30453
|
action: external_exports.enum(ACTIONS),
|
|
30405
30454
|
locator: locatorSchema.optional(),
|
|
30406
30455
|
url: external_exports.string().min(1).optional(),
|
|
30407
|
-
value: external_exports.string().optional(),
|
|
30456
|
+
value: external_exports.union([external_exports.string(), external_exports.number()]).optional(),
|
|
30408
30457
|
expected: external_exports.string().optional(),
|
|
30409
30458
|
/** api target:request 的 HTTP 方法(默认 GET) */
|
|
30410
30459
|
method: external_exports.enum(HTTP_METHODS).optional(),
|
|
@@ -30415,7 +30464,44 @@ var stepSchema = external_exports.strictObject({
|
|
|
30415
30464
|
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(),
|
|
30416
30465
|
timeout: external_exports.number().int().positive().optional(),
|
|
30417
30466
|
/** UI 动作触发的网络请求精确次数断言 */
|
|
30418
|
-
expectRequests: external_exports.array(requestExpectationSchema).min(1).optional()
|
|
30467
|
+
expectRequests: external_exports.array(requestExpectationSchema).min(1).optional(),
|
|
30468
|
+
/** callMethod:要调用的 Page / 组件方法名(仅 miniapp;页面级调用不写 locator) */
|
|
30469
|
+
methodName: external_exports.string().min(1).optional(),
|
|
30470
|
+
/** callMethod 的入参(支持 ${var} 模板) */
|
|
30471
|
+
args: external_exports.array(external_exports.unknown()).optional(),
|
|
30472
|
+
/** mockWxApi:要 mock 的 wx.* API 名(如 showModal / request) */
|
|
30473
|
+
wxApi: external_exports.string().min(1).optional(),
|
|
30474
|
+
/** mockWxApi:mock 返回值(对象按原样注入;capture 时真实调用先上报再返回 mock 值) */
|
|
30475
|
+
result: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
|
|
30476
|
+
/** mockWxApi:捕获真实调用参数的变量名(${<capture>.options} / ${<capture>.called}) */
|
|
30477
|
+
capture: external_exports.string().min(1).optional(),
|
|
30478
|
+
/** trigger:触发的事件类型(如 getphonenumber;空 detail 即"用户拒绝/取消"类分支) */
|
|
30479
|
+
eventType: external_exports.string().min(1).optional(),
|
|
30480
|
+
/** trigger:作为事件 e.detail 注入(值支持 ${var} 模板) */
|
|
30481
|
+
detail: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
|
|
30482
|
+
/** setData:写入页面 data 的补丁(值支持 ${var} 模板);提供 locator 时为组件级 setData */
|
|
30483
|
+
patch: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
|
|
30484
|
+
/** assert:页面 data 路径断言(与 locator/attribute/property/count 互斥,仅 miniapp) */
|
|
30485
|
+
dataPath: external_exports.string().min(1).optional(),
|
|
30486
|
+
/** assert:元素 attribute 名(与 expected 组合做属性断言;web/miniapp) */
|
|
30487
|
+
attribute: external_exports.string().min(1).optional(),
|
|
30488
|
+
/** assert:元素 property 名(自定义组件实例属性;仅 miniapp) */
|
|
30489
|
+
property: external_exports.string().min(1).optional(),
|
|
30490
|
+
/** assert:匹配元素的精确数量(0 即"元素不存在"断言;与 expected/attribute/property/dataPath 互斥) */
|
|
30491
|
+
count: external_exports.number().int().nonnegative().optional(),
|
|
30492
|
+
/** touch:触摸阶段 */
|
|
30493
|
+
phase: external_exports.enum(TOUCH_PHASES).optional(),
|
|
30494
|
+
/** touch:触摸点数组(元素如 [{identifier: 0, x: 100, y: 200}],值支持 ${var} 模板) */
|
|
30495
|
+
touches: external_exports.array(external_exports.record(external_exports.string(), external_exports.unknown())).min(1).optional(),
|
|
30496
|
+
/** touch:changedTouches 触摸点数组;缺省复用 touches */
|
|
30497
|
+
changedTouches: external_exports.array(external_exports.record(external_exports.string(), external_exports.unknown())).optional(),
|
|
30498
|
+
/** scroll:滚动位置(scroll-view 内滚动提供 locator;缺省滚动页面) */
|
|
30499
|
+
scrollTop: external_exports.number().optional(),
|
|
30500
|
+
/** swipeTo:swiper 目标页索引 */
|
|
30501
|
+
index: external_exports.number().int().nonnegative().optional(),
|
|
30502
|
+
/** moveTo:movable-view 目标坐标 */
|
|
30503
|
+
x: external_exports.number().optional(),
|
|
30504
|
+
y: external_exports.number().optional()
|
|
30419
30505
|
});
|
|
30420
30506
|
var datasetReferenceSchema = external_exports.strictObject({
|
|
30421
30507
|
file: external_exports.string().min(1),
|
|
@@ -30488,20 +30574,61 @@ var ADAPTER_SUPPORT = {
|
|
|
30488
30574
|
assert: ["web", "miniapp", "api"],
|
|
30489
30575
|
extract: ["web", "miniapp", "api"],
|
|
30490
30576
|
screenshot: ["web", "miniapp"],
|
|
30491
|
-
request: ["api"]
|
|
30577
|
+
request: ["api"],
|
|
30578
|
+
callMethod: ["miniapp"],
|
|
30579
|
+
mockWxApi: ["miniapp"],
|
|
30580
|
+
trigger: ["miniapp"],
|
|
30581
|
+
setData: ["miniapp"],
|
|
30582
|
+
longpress: ["web", "miniapp"],
|
|
30583
|
+
callWxApi: ["miniapp"],
|
|
30584
|
+
touch: ["miniapp"],
|
|
30585
|
+
scroll: ["miniapp"],
|
|
30586
|
+
swipeTo: ["miniapp"],
|
|
30587
|
+
slideTo: ["miniapp"],
|
|
30588
|
+
moveTo: ["miniapp"]
|
|
30492
30589
|
};
|
|
30493
|
-
var LOCATOR_REQUIRED = /* @__PURE__ */ new Set([
|
|
30494
|
-
|
|
30590
|
+
var LOCATOR_REQUIRED = /* @__PURE__ */ new Set([
|
|
30591
|
+
"click",
|
|
30592
|
+
"input",
|
|
30593
|
+
"select",
|
|
30594
|
+
"assert",
|
|
30595
|
+
"extract",
|
|
30596
|
+
"longpress",
|
|
30597
|
+
"touch",
|
|
30598
|
+
"swipeTo",
|
|
30599
|
+
"slideTo",
|
|
30600
|
+
"moveTo"
|
|
30601
|
+
]);
|
|
30602
|
+
var VALUE_REQUIRED = /* @__PURE__ */ new Set(["input", "select", "slideTo"]);
|
|
30495
30603
|
var REQUEST_EXPECTATION_ACTIONS = /* @__PURE__ */ new Set(["navigate", "click", "input", "select"]);
|
|
30496
30604
|
var FIELD_ALLOWED = {
|
|
30497
30605
|
url: ["navigate", "request"],
|
|
30498
|
-
value: ["input", "select", "extract"],
|
|
30606
|
+
value: ["input", "select", "extract", "slideTo"],
|
|
30499
30607
|
expected: ["assert", "request"],
|
|
30500
|
-
variable: ["extract"],
|
|
30608
|
+
variable: ["extract", "callWxApi"],
|
|
30501
30609
|
timeout: ["wait"],
|
|
30502
30610
|
method: ["request"],
|
|
30503
30611
|
headers: ["request"],
|
|
30504
|
-
body: ["request"]
|
|
30612
|
+
body: ["request"],
|
|
30613
|
+
methodName: ["callMethod"],
|
|
30614
|
+
args: ["callMethod", "callWxApi"],
|
|
30615
|
+
wxApi: ["mockWxApi", "callWxApi"],
|
|
30616
|
+
result: ["mockWxApi"],
|
|
30617
|
+
capture: ["mockWxApi"],
|
|
30618
|
+
eventType: ["trigger"],
|
|
30619
|
+
detail: ["trigger"],
|
|
30620
|
+
patch: ["setData"],
|
|
30621
|
+
dataPath: ["assert"],
|
|
30622
|
+
attribute: ["assert"],
|
|
30623
|
+
property: ["assert"],
|
|
30624
|
+
count: ["assert"],
|
|
30625
|
+
phase: ["touch"],
|
|
30626
|
+
touches: ["touch"],
|
|
30627
|
+
changedTouches: ["touch"],
|
|
30628
|
+
scrollTop: ["scroll"],
|
|
30629
|
+
index: ["swipeTo"],
|
|
30630
|
+
x: ["moveTo"],
|
|
30631
|
+
y: ["moveTo"]
|
|
30505
30632
|
};
|
|
30506
30633
|
var VARIABLE_REF = /\$\{([A-Za-z_][A-Za-z0-9_.]*)\}/g;
|
|
30507
30634
|
function validateCase(data) {
|
|
@@ -30565,19 +30692,82 @@ function checkStep(step, at, declared, issues) {
|
|
|
30565
30692
|
});
|
|
30566
30693
|
}
|
|
30567
30694
|
}
|
|
30695
|
+
if (step.action === "callWxApi" && !step.wxApi) {
|
|
30696
|
+
issues.push({ path: `${at}.wxApi`, code: "semantic", message: "callWxApi \u5FC5\u987B\u63D0\u4F9B wxApi(\u8981\u4E3B\u52A8\u8C03\u7528\u7684 wx.* API \u540D,\u5982 setStorageSync)" });
|
|
30697
|
+
}
|
|
30698
|
+
if (step.action === "touch") {
|
|
30699
|
+
if (!step.phase) {
|
|
30700
|
+
issues.push({ path: `${at}.phase`, code: "semantic", message: "touch \u5FC5\u987B\u63D0\u4F9B phase(touchstart / touchmove / touchend)" });
|
|
30701
|
+
}
|
|
30702
|
+
if (!step.touches || step.touches.length === 0) {
|
|
30703
|
+
issues.push({ path: `${at}.touches`, code: "semantic", message: "touch \u5FC5\u987B\u63D0\u4F9B touches(\u89E6\u6478\u70B9\u6570\u7EC4,\u5982 [{identifier: 0, x: 0, y: 0}])" });
|
|
30704
|
+
}
|
|
30705
|
+
}
|
|
30706
|
+
if (step.action === "scroll" && step.scrollTop === void 0) {
|
|
30707
|
+
issues.push({ path: `${at}.scrollTop`, code: "semantic", message: "scroll \u5FC5\u987B\u63D0\u4F9B scrollTop(\u6EDA\u52A8\u4F4D\u7F6E\u50CF\u7D20)" });
|
|
30708
|
+
}
|
|
30709
|
+
if (step.action === "swipeTo" && step.index === void 0) {
|
|
30710
|
+
issues.push({ path: `${at}.index`, code: "semantic", message: "swipeTo \u5FC5\u987B\u63D0\u4F9B index(swiper \u76EE\u6807\u9875\u7D22\u5F15)" });
|
|
30711
|
+
}
|
|
30712
|
+
if (step.action === "moveTo" && (step.x === void 0 || step.y === void 0)) {
|
|
30713
|
+
issues.push({ path: `${at}.x`, code: "semantic", message: "moveTo \u5FC5\u987B\u63D0\u4F9B x \u4E0E y(movable-view \u76EE\u6807\u5750\u6807)" });
|
|
30714
|
+
}
|
|
30715
|
+
if (step.action === "assert") {
|
|
30716
|
+
const modes = [
|
|
30717
|
+
step.dataPath !== void 0,
|
|
30718
|
+
step.attribute !== void 0,
|
|
30719
|
+
step.property !== void 0,
|
|
30720
|
+
step.count !== void 0
|
|
30721
|
+
].filter(Boolean).length;
|
|
30722
|
+
if (modes > 1) {
|
|
30723
|
+
issues.push({ path: `${at}.assert`, code: "semantic", message: "assert \u7684 dataPath / attribute / property / count \u53EA\u80FD\u63D0\u4F9B\u5176\u4E00(\u7F3A\u7701\u4E3A\u5143\u7D20\u6587\u672C\u65AD\u8A00)" });
|
|
30724
|
+
}
|
|
30725
|
+
if (step.property !== void 0 && step.target !== "miniapp") {
|
|
30726
|
+
issues.push({ path: `${at}.property`, code: "semantic", message: "property \u65AD\u8A00\u4EC5 miniapp \u652F\u6301(\u81EA\u5B9A\u4E49\u7EC4\u4EF6\u5B9E\u4F8B\u5C5E\u6027)" });
|
|
30727
|
+
}
|
|
30728
|
+
if ((step.attribute !== void 0 || step.property !== void 0) && !step.locator) {
|
|
30729
|
+
issues.push({ path: `${at}.locator`, code: "semantic", message: "attribute / property \u65AD\u8A00\u5FC5\u987B\u63D0\u4F9B locator" });
|
|
30730
|
+
}
|
|
30731
|
+
if (step.count !== void 0 && !step.locator) {
|
|
30732
|
+
issues.push({ path: `${at}.locator`, code: "semantic", message: "count \u65AD\u8A00\u5FC5\u987B\u63D0\u4F9B locator(\u7EDF\u8BA1\u5339\u914D\u5143\u7D20)" });
|
|
30733
|
+
}
|
|
30734
|
+
if (step.count !== void 0 && step.expected !== void 0) {
|
|
30735
|
+
issues.push({ path: `${at}.assert`, code: "semantic", message: "count \u65AD\u8A00\u4EE5 count \u4E3A\u671F\u671B\u503C,\u4E0D\u9700\u8981 expected" });
|
|
30736
|
+
}
|
|
30737
|
+
if (step.target === "api" && (step.attribute !== void 0 || step.property !== void 0 || step.count !== void 0)) {
|
|
30738
|
+
issues.push({ path: `${at}.assert`, code: "semantic", message: "api \u7AEF assert \u4EC5\u652F\u6301 expected(\u54CD\u5E94\u4F53\u5305\u542B)" });
|
|
30739
|
+
}
|
|
30740
|
+
if (step.dataPath !== void 0 && step.target !== "miniapp") {
|
|
30741
|
+
issues.push({ path: `${at}.dataPath`, code: "semantic", message: "dataPath \u65AD\u8A00\u4EC5 miniapp \u652F\u6301(web \u7AEF\u7528 locator)" });
|
|
30742
|
+
}
|
|
30743
|
+
}
|
|
30744
|
+
if (step.action === "mockWxApi") {
|
|
30745
|
+
if (!step.wxApi) {
|
|
30746
|
+
issues.push({ path: `${at}.wxApi`, code: "semantic", message: "mockWxApi \u5FC5\u987B\u63D0\u4F9B wxApi(\u8981 mock \u7684 wx.* API \u540D)" });
|
|
30747
|
+
}
|
|
30748
|
+
if (!step.result) {
|
|
30749
|
+
issues.push({ path: `${at}.result`, code: "semantic", message: "mockWxApi \u5FC5\u987B\u63D0\u4F9B result(mock \u8FD4\u56DE\u503C)" });
|
|
30750
|
+
}
|
|
30751
|
+
}
|
|
30752
|
+
if (step.action === "trigger" && !step.eventType) {
|
|
30753
|
+
issues.push({ path: `${at}.eventType`, code: "semantic", message: "trigger \u5FC5\u987B\u63D0\u4F9B eventType(\u89E6\u53D1\u7684\u4E8B\u4EF6\u7C7B\u578B)" });
|
|
30754
|
+
}
|
|
30755
|
+
if (step.action === "setData" && (!step.patch || Object.keys(step.patch).length === 0)) {
|
|
30756
|
+
issues.push({ path: `${at}.patch`, code: "semantic", message: "setData \u5FC5\u987B\u63D0\u4F9B patch(\u5199\u5165\u9875\u9762 data \u7684\u8865\u4E01)" });
|
|
30757
|
+
}
|
|
30568
30758
|
if (step.action === "navigate" && !step.url) {
|
|
30569
30759
|
issues.push({ path: `${at}.url`, code: "semantic", message: "navigate \u5FC5\u987B\u63D0\u4F9B url" });
|
|
30570
30760
|
}
|
|
30571
30761
|
if (step.action === "request" && !step.url) {
|
|
30572
30762
|
issues.push({ path: `${at}.url`, code: "semantic", message: "request \u5FC5\u987B\u63D0\u4F9B url(\u76F8\u5BF9\u8DEF\u5F84\u6309 api.baseUrl \u89E3\u6790)" });
|
|
30573
30763
|
}
|
|
30574
|
-
if (VALUE_REQUIRED.has(step.action) &&
|
|
30764
|
+
if (VALUE_REQUIRED.has(step.action) && (step.value === void 0 || step.value === "")) {
|
|
30575
30765
|
issues.push({ path: `${at}.value`, code: "semantic", message: `action "${step.action}" \u5FC5\u987B\u63D0\u4F9B value` });
|
|
30576
30766
|
}
|
|
30577
30767
|
if (step.action === "extract" && step.target === "api" && !step.value) {
|
|
30578
30768
|
issues.push({ path: `${at}.value`, code: "semantic", message: "api \u7AEF extract \u5FC5\u987B\u63D0\u4F9B value(\u70B9\u53F7 JSON path,\u5982 data.orderId)" });
|
|
30579
30769
|
}
|
|
30580
|
-
if (step.action === "assert" && !step.expected) {
|
|
30770
|
+
if (step.action === "assert" && step.count === void 0 && !step.expected) {
|
|
30581
30771
|
issues.push({ path: `${at}.expected`, code: "semantic", message: "assert \u5FC5\u987B\u63D0\u4F9B expected" });
|
|
30582
30772
|
}
|
|
30583
30773
|
if (step.action === "extract") {
|
|
@@ -30596,6 +30786,13 @@ function checkStep(step, at, declared, issues) {
|
|
|
30596
30786
|
if (step.action === "wait" && !step.locator && step.timeout === void 0) {
|
|
30597
30787
|
issues.push({ path: at, code: "semantic", message: "wait \u9700\u8981\u63D0\u4F9B locator(\u7B49\u5F85\u5143\u7D20)\u6216 timeout(\u6BEB\u79D2)" });
|
|
30598
30788
|
}
|
|
30789
|
+
if (step.action === "callMethod" && !step.methodName) {
|
|
30790
|
+
issues.push({
|
|
30791
|
+
path: `${at}.methodName`,
|
|
30792
|
+
code: "semantic",
|
|
30793
|
+
message: "callMethod \u5FC5\u987B\u63D0\u4F9B methodName(\u9875\u9762\u6216\u7EC4\u4EF6\u7684\u65B9\u6CD5\u540D)"
|
|
30794
|
+
});
|
|
30795
|
+
}
|
|
30599
30796
|
if (step.expectRequests) {
|
|
30600
30797
|
if (step.target === "api" || !REQUEST_EXPECTATION_ACTIONS.has(step.action)) {
|
|
30601
30798
|
issues.push({
|
|
@@ -30610,6 +30807,7 @@ function checkStep(step, at, declared, issues) {
|
|
|
30610
30807
|
function checkLocator(step, at, issues) {
|
|
30611
30808
|
const locator = step.locator;
|
|
30612
30809
|
if (!locator) {
|
|
30810
|
+
if (step.action === "assert" && step.dataPath !== void 0) return;
|
|
30613
30811
|
if (LOCATOR_REQUIRED.has(step.action)) {
|
|
30614
30812
|
issues.push({
|
|
30615
30813
|
path: `${at}.locator`,
|
|
@@ -30634,14 +30832,20 @@ function checkVariableRefs(step, at, declared, issues) {
|
|
|
30634
30832
|
if (match[1]) refs.push([field, match[1]]);
|
|
30635
30833
|
}
|
|
30636
30834
|
};
|
|
30637
|
-
collect("value", step.value);
|
|
30835
|
+
collect("value", step.value === void 0 ? void 0 : String(step.value));
|
|
30638
30836
|
collect("expected", step.expected);
|
|
30639
30837
|
collect("url", step.url);
|
|
30838
|
+
collect("methodName", step.methodName);
|
|
30640
30839
|
collect("locator.text", step.locator?.text);
|
|
30641
30840
|
collect("locator.css", step.locator?.css);
|
|
30642
30841
|
if (typeof step.body === "string") collect("body", step.body);
|
|
30643
30842
|
else if (step.body) collect("body", JSON.stringify(step.body));
|
|
30644
30843
|
for (const headerValue of Object.values(step.headers ?? {})) collect("headers", headerValue);
|
|
30844
|
+
if (Array.isArray(step.args)) {
|
|
30845
|
+
step.args.forEach((arg, index) => {
|
|
30846
|
+
if (typeof arg === "string") collect(`args[${index}]`, arg);
|
|
30847
|
+
});
|
|
30848
|
+
}
|
|
30645
30849
|
for (const [index, expectation] of (step.expectRequests ?? []).entries()) {
|
|
30646
30850
|
collect(`expectRequests[${index}].urlContains`, expectation.urlContains);
|
|
30647
30851
|
}
|
|
@@ -31463,13 +31667,13 @@ function buildSummaryData(runs) {
|
|
|
31463
31667
|
});
|
|
31464
31668
|
}
|
|
31465
31669
|
const byCase = /* @__PURE__ */ new Map();
|
|
31466
|
-
for (const
|
|
31467
|
-
let agg = byCase.get(
|
|
31670
|
+
for (const exec3 of executions) {
|
|
31671
|
+
let agg = byCase.get(exec3.identity);
|
|
31468
31672
|
if (!agg) {
|
|
31469
31673
|
agg = {
|
|
31470
|
-
caseId:
|
|
31471
|
-
rowId:
|
|
31472
|
-
caseName:
|
|
31674
|
+
caseId: exec3.caseId,
|
|
31675
|
+
rowId: exec3.rowId,
|
|
31676
|
+
caseName: exec3.caseName,
|
|
31473
31677
|
platforms: [],
|
|
31474
31678
|
runs: 0,
|
|
31475
31679
|
passed: 0,
|
|
@@ -31478,18 +31682,18 @@ function buildSummaryData(runs) {
|
|
|
31478
31682
|
lastStatus: "unknown",
|
|
31479
31683
|
flaky: false
|
|
31480
31684
|
};
|
|
31481
|
-
byCase.set(
|
|
31685
|
+
byCase.set(exec3.identity, agg);
|
|
31482
31686
|
}
|
|
31483
31687
|
agg.runs++;
|
|
31484
|
-
if (
|
|
31485
|
-
else if (
|
|
31486
|
-
for (const platform of
|
|
31688
|
+
if (exec3.status === "passed") agg.passed++;
|
|
31689
|
+
else if (exec3.status === "failed") agg.failed++;
|
|
31690
|
+
for (const platform of exec3.platforms) {
|
|
31487
31691
|
if (!agg.platforms.includes(platform)) agg.platforms.push(platform);
|
|
31488
31692
|
}
|
|
31489
31693
|
}
|
|
31490
31694
|
for (const agg of byCase.values()) {
|
|
31491
31695
|
const identity = agg.rowId && agg.rowId !== "default" ? `${agg.caseId}#${agg.rowId}` : agg.caseId;
|
|
31492
|
-
const last = [...executions].reverse().find((
|
|
31696
|
+
const last = [...executions].reverse().find((exec3) => exec3.identity === identity);
|
|
31493
31697
|
agg.lastStatus = last?.status ?? "unknown";
|
|
31494
31698
|
agg.lastCategory = last?.category;
|
|
31495
31699
|
agg.passRate = agg.runs > 0 ? agg.passed / agg.runs : 0;
|
|
@@ -31500,13 +31704,13 @@ function buildSummaryData(runs) {
|
|
|
31500
31704
|
const categories = FAILURE_CATEGORIES.map((item) => ({
|
|
31501
31705
|
id: item.id,
|
|
31502
31706
|
label: item.label,
|
|
31503
|
-
count: executions.filter((
|
|
31707
|
+
count: executions.filter((exec3) => exec3.status === "failed" && exec3.category === item.id).length
|
|
31504
31708
|
})).filter((item) => item.count > 0);
|
|
31505
31709
|
const durations = runs.map((item) => item.summary.durationMs).filter((value) => typeof value === "number");
|
|
31506
31710
|
return {
|
|
31507
31711
|
buckets: {
|
|
31508
|
-
blocked: executions.filter((
|
|
31509
|
-
inconclusive: executions.filter((
|
|
31712
|
+
blocked: executions.filter((exec3) => exec3.status === "blocked").length,
|
|
31713
|
+
inconclusive: executions.filter((exec3) => exec3.status === "inconclusive").length
|
|
31510
31714
|
},
|
|
31511
31715
|
window: {
|
|
31512
31716
|
runs: runs.length,
|
|
@@ -31516,7 +31720,7 @@ function buildSummaryData(runs) {
|
|
|
31516
31720
|
kpis: {
|
|
31517
31721
|
runs: runs.length,
|
|
31518
31722
|
executions: executions.length,
|
|
31519
|
-
overallRate: executions.length > 0 ? executions.filter((
|
|
31723
|
+
overallRate: executions.length > 0 ? executions.filter((exec3) => exec3.status === "passed").length / executions.length : 0,
|
|
31520
31724
|
avgDurationMs: durations.length > 0 ? durations.reduce((sum, value) => sum + value, 0) / durations.length : 0,
|
|
31521
31725
|
flakyCount: flaky.length
|
|
31522
31726
|
},
|
|
@@ -31720,7 +31924,7 @@ import { readFile as readFile7, readdir as readdir4 } from "node:fs/promises";
|
|
|
31720
31924
|
import { isAbsolute as isAbsolute3, join as join17 } from "node:path";
|
|
31721
31925
|
|
|
31722
31926
|
// ../../packages/execution-engine/src/artifacts/index.ts
|
|
31723
|
-
import { mkdir as mkdir9 } from "node:fs/promises";
|
|
31927
|
+
import { mkdir as mkdir9, writeFile as writeFile9 } from "node:fs/promises";
|
|
31724
31928
|
import { join as join13 } from "node:path";
|
|
31725
31929
|
|
|
31726
31930
|
// ../../packages/evidence/src/log.ts
|
|
@@ -31811,6 +32015,12 @@ var ArtifactManager = class {
|
|
|
31811
32015
|
await saveTrace(this.tracesDir, fileName, data);
|
|
31812
32016
|
return `traces/${fileName}`;
|
|
31813
32017
|
}
|
|
32018
|
+
/** 保存失败用例的运行时诊断(console 尾部/异常/页面栈/systemInfo),返回相对 run 目录的路径 */
|
|
32019
|
+
async saveDiagnostics(fileName, data) {
|
|
32020
|
+
await mkdir9(this.logsDir, { recursive: true });
|
|
32021
|
+
await writeFile9(join13(this.logsDir, fileName), JSON.stringify(data, null, 2), "utf8");
|
|
32022
|
+
return `logs/${fileName}`;
|
|
32023
|
+
}
|
|
31814
32024
|
};
|
|
31815
32025
|
|
|
31816
32026
|
// ../../packages/execution-engine/src/context/index.ts
|
|
@@ -31869,6 +32079,26 @@ var ExecutionContext = class {
|
|
|
31869
32079
|
}
|
|
31870
32080
|
this.variables.set("account", rawValue);
|
|
31871
32081
|
}
|
|
32082
|
+
/**
|
|
32083
|
+
* 注入结构化对象(如 mockWxApi 的捕获结果):按点号路径展开为平铺字符串变量。
|
|
32084
|
+
* 例如 seedObject('showModalCall', { called: true, args: { content: 'x' } }) 后,
|
|
32085
|
+
* `${showModalCall.called}` 与 `${showModalCall.args.content}` 均可解析。
|
|
32086
|
+
*/
|
|
32087
|
+
seedObject(name, value) {
|
|
32088
|
+
const flatten = (prefix, input2) => {
|
|
32089
|
+
if (input2 !== null && typeof input2 === "object" && !Array.isArray(input2)) {
|
|
32090
|
+
const entries = Object.entries(input2);
|
|
32091
|
+
if (entries.length === 0) {
|
|
32092
|
+
this.variables.set(prefix, "");
|
|
32093
|
+
return;
|
|
32094
|
+
}
|
|
32095
|
+
for (const [key, item] of entries) flatten(`${prefix}.${key}`, item);
|
|
32096
|
+
return;
|
|
32097
|
+
}
|
|
32098
|
+
this.variables.set(prefix, input2 === void 0 ? "" : String(input2));
|
|
32099
|
+
};
|
|
32100
|
+
flatten(name, value);
|
|
32101
|
+
}
|
|
31872
32102
|
};
|
|
31873
32103
|
|
|
31874
32104
|
// ../../packages/execution-engine/src/lifecycle/index.ts
|
|
@@ -31930,6 +32160,14 @@ function redactHeaders(headers) {
|
|
|
31930
32160
|
return redacted;
|
|
31931
32161
|
}
|
|
31932
32162
|
|
|
32163
|
+
// ../../packages/adapter-core/src/locator.ts
|
|
32164
|
+
function describeLocator(locator) {
|
|
32165
|
+
if (!locator) return "-";
|
|
32166
|
+
if (locator.text !== void 0) return `text="${locator.text}"`;
|
|
32167
|
+
if (locator.css !== void 0) return `css="${locator.css}"`;
|
|
32168
|
+
return "-";
|
|
32169
|
+
}
|
|
32170
|
+
|
|
31933
32171
|
// ../../packages/adapter-core/src/session.ts
|
|
31934
32172
|
var AdapterSession = class {
|
|
31935
32173
|
constructor(target, factory) {
|
|
@@ -31986,6 +32224,8 @@ var AdapterResolver = class {
|
|
|
31986
32224
|
|
|
31987
32225
|
// ../../packages/execution-engine/src/engine/step-executor.ts
|
|
31988
32226
|
var SNIPPET_LIMIT = 400;
|
|
32227
|
+
var ASSERT_POLL_MS = 1e4;
|
|
32228
|
+
var ABSENT_SETTLE_MS = 2e3;
|
|
31989
32229
|
async function executeStep(step, index, adapter, context, artifacts, options) {
|
|
31990
32230
|
const startedAt = Date.now();
|
|
31991
32231
|
const result = {
|
|
@@ -32019,15 +32259,33 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
|
|
|
32019
32259
|
await adapter.click(locator);
|
|
32020
32260
|
break;
|
|
32021
32261
|
case "input":
|
|
32022
|
-
await adapter.input(locator,
|
|
32262
|
+
await adapter.input(locator, resolveValueText(step.value, context));
|
|
32023
32263
|
break;
|
|
32024
32264
|
case "select":
|
|
32025
|
-
await adapter.select(locator,
|
|
32265
|
+
await adapter.select(locator, resolveValueText(step.value, context));
|
|
32026
32266
|
break;
|
|
32027
32267
|
case "wait":
|
|
32028
32268
|
await adapter.wait(locator, step.timeout);
|
|
32029
32269
|
break;
|
|
32030
|
-
case "assert":
|
|
32270
|
+
case "assert": {
|
|
32271
|
+
if (step.count !== void 0) {
|
|
32272
|
+
if (!adapter.countElements) {
|
|
32273
|
+
throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 count \u65AD\u8A00`);
|
|
32274
|
+
}
|
|
32275
|
+
await assertElementCount(adapter, locator, step.count);
|
|
32276
|
+
break;
|
|
32277
|
+
}
|
|
32278
|
+
if (step.attribute !== void 0 || step.property !== void 0) {
|
|
32279
|
+
await assertElementAttribute(adapter, locator, step, context);
|
|
32280
|
+
break;
|
|
32281
|
+
}
|
|
32282
|
+
if (step.dataPath !== void 0) {
|
|
32283
|
+
if (!adapter.assertData) {
|
|
32284
|
+
throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 dataPath \u65AD\u8A00`);
|
|
32285
|
+
}
|
|
32286
|
+
await adapter.assertData(context.resolve(step.dataPath), context.resolve(step.expected ?? ""));
|
|
32287
|
+
break;
|
|
32288
|
+
}
|
|
32031
32289
|
if (step.target === "api") {
|
|
32032
32290
|
if (!adapter.assertResponse) throw new TestPilotError("UNSUPPORTED_ACTION", "\u5F53\u524D adapter \u4E0D\u652F\u6301 api \u65AD\u8A00");
|
|
32033
32291
|
await adapter.assertResponse(context.resolve(step.expected ?? ""));
|
|
@@ -32035,8 +32293,9 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
|
|
|
32035
32293
|
await adapter.assert(locator, context.resolve(step.expected ?? ""));
|
|
32036
32294
|
}
|
|
32037
32295
|
break;
|
|
32296
|
+
}
|
|
32038
32297
|
case "extract": {
|
|
32039
|
-
const value = step.target === "api" ? await extractApiResponse(adapter,
|
|
32298
|
+
const value = step.target === "api" ? await extractApiResponse(adapter, resolveValueText(step.value, context)) : await adapter.extract(locator);
|
|
32040
32299
|
if (step.variable) {
|
|
32041
32300
|
context.set(step.variable, value);
|
|
32042
32301
|
result.extracted = { [step.variable]: value };
|
|
@@ -32074,6 +32333,104 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
|
|
|
32074
32333
|
}
|
|
32075
32334
|
break;
|
|
32076
32335
|
}
|
|
32336
|
+
case "callMethod": {
|
|
32337
|
+
if (typeof adapter.callMethod !== "function") {
|
|
32338
|
+
throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 callMethod`);
|
|
32339
|
+
}
|
|
32340
|
+
const methodName = context.resolve(step.methodName ?? "");
|
|
32341
|
+
if (!methodName) throw new TestPilotError("MISSING_METHOD_NAME", "callMethod \u5FC5\u987B\u63D0\u4F9B methodName");
|
|
32342
|
+
const callArgs = (step.args ?? []).map((arg) => resolveDeep(arg, context));
|
|
32343
|
+
await adapter.callMethod(locator, methodName, callArgs);
|
|
32344
|
+
break;
|
|
32345
|
+
}
|
|
32346
|
+
case "mockWxApi": {
|
|
32347
|
+
if (typeof adapter.mockWxApi !== "function") {
|
|
32348
|
+
throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 mockWxApi`);
|
|
32349
|
+
}
|
|
32350
|
+
await adapter.mockWxApi({
|
|
32351
|
+
api: context.resolve(step.wxApi ?? ""),
|
|
32352
|
+
result: step.result ? resolveDeep(step.result, context) : void 0,
|
|
32353
|
+
capture: step.capture
|
|
32354
|
+
});
|
|
32355
|
+
break;
|
|
32356
|
+
}
|
|
32357
|
+
case "trigger": {
|
|
32358
|
+
if (typeof adapter.trigger !== "function") {
|
|
32359
|
+
throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 trigger`);
|
|
32360
|
+
}
|
|
32361
|
+
if (!locator) throw new TestPilotError("MISSING_LOCATOR", "trigger \u5FC5\u987B\u63D0\u4F9B locator(\u8981\u89E6\u53D1\u4E8B\u4EF6\u7684\u7EC4\u4EF6)");
|
|
32362
|
+
await adapter.trigger(locator, context.resolve(step.eventType ?? ""), step.detail ? resolveDeep(step.detail, context) : void 0);
|
|
32363
|
+
break;
|
|
32364
|
+
}
|
|
32365
|
+
case "setData": {
|
|
32366
|
+
if (typeof adapter.setData !== "function") {
|
|
32367
|
+
throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 setData`);
|
|
32368
|
+
}
|
|
32369
|
+
await adapter.setData(resolveDeep(step.patch ?? {}, context), locator);
|
|
32370
|
+
break;
|
|
32371
|
+
}
|
|
32372
|
+
case "longpress": {
|
|
32373
|
+
if (typeof adapter.longpress !== "function") {
|
|
32374
|
+
throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 longpress`);
|
|
32375
|
+
}
|
|
32376
|
+
await adapter.longpress(locator);
|
|
32377
|
+
break;
|
|
32378
|
+
}
|
|
32379
|
+
case "callWxApi": {
|
|
32380
|
+
if (typeof adapter.callWxApi !== "function") {
|
|
32381
|
+
throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 callWxApi`);
|
|
32382
|
+
}
|
|
32383
|
+
const wxApi = context.resolve(step.wxApi ?? "");
|
|
32384
|
+
if (!wxApi) throw new TestPilotError("MISSING_WX_API", "callWxApi \u5FC5\u987B\u63D0\u4F9B wxApi");
|
|
32385
|
+
const callArgs = (step.args ?? []).map((arg) => resolveDeep(arg, context));
|
|
32386
|
+
const returned = await adapter.callWxApi(wxApi, callArgs);
|
|
32387
|
+
if (step.variable) {
|
|
32388
|
+
const captured = typeof returned === "string" ? returned : JSON.stringify(returned) ?? "";
|
|
32389
|
+
context.set(step.variable, captured);
|
|
32390
|
+
result.extracted = { [step.variable]: captured };
|
|
32391
|
+
}
|
|
32392
|
+
break;
|
|
32393
|
+
}
|
|
32394
|
+
case "touch": {
|
|
32395
|
+
if (typeof adapter.touch !== "function") {
|
|
32396
|
+
throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 touch`);
|
|
32397
|
+
}
|
|
32398
|
+
if (!locator) throw new TestPilotError("MISSING_LOCATOR", "touch \u5FC5\u987B\u63D0\u4F9B locator");
|
|
32399
|
+
const touches = (step.touches ?? []).map((point) => resolveDeep(point, context));
|
|
32400
|
+
const changed = (step.changedTouches ?? step.touches ?? []).map(
|
|
32401
|
+
(point) => resolveDeep(point, context)
|
|
32402
|
+
);
|
|
32403
|
+
await adapter.touch(locator, step.phase ?? "", touches, changed);
|
|
32404
|
+
break;
|
|
32405
|
+
}
|
|
32406
|
+
case "scroll": {
|
|
32407
|
+
if (typeof adapter.scroll !== "function") {
|
|
32408
|
+
throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 scroll`);
|
|
32409
|
+
}
|
|
32410
|
+
await adapter.scroll(locator, step.scrollTop ?? 0);
|
|
32411
|
+
break;
|
|
32412
|
+
}
|
|
32413
|
+
case "swipeTo": {
|
|
32414
|
+
if (typeof adapter.swipeTo !== "function") {
|
|
32415
|
+
throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 swipeTo`);
|
|
32416
|
+
}
|
|
32417
|
+
await adapter.swipeTo(locator, step.index ?? 0);
|
|
32418
|
+
break;
|
|
32419
|
+
}
|
|
32420
|
+
case "slideTo": {
|
|
32421
|
+
if (typeof adapter.slideTo !== "function") {
|
|
32422
|
+
throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 slideTo`);
|
|
32423
|
+
}
|
|
32424
|
+
await adapter.slideTo(locator, Number(resolveValueText(step.value, context)));
|
|
32425
|
+
break;
|
|
32426
|
+
}
|
|
32427
|
+
case "moveTo": {
|
|
32428
|
+
if (typeof adapter.moveTo !== "function") {
|
|
32429
|
+
throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 moveTo`);
|
|
32430
|
+
}
|
|
32431
|
+
await adapter.moveTo(locator, step.x ?? 0, step.y ?? 0);
|
|
32432
|
+
break;
|
|
32433
|
+
}
|
|
32077
32434
|
default: {
|
|
32078
32435
|
const exhaustive = step.action;
|
|
32079
32436
|
throw new TestPilotError("UNSUPPORTED_ACTION", `\u4E0D\u652F\u6301\u7684 action:${String(exhaustive)}`);
|
|
@@ -32120,6 +32477,42 @@ async function extractApiResponse(adapter, path) {
|
|
|
32120
32477
|
if (!path) throw new TestPilotError("UNSUPPORTED_ACTION", "api \u7AEF extract \u9700\u8981\u63D0\u4F9B value(\u70B9\u53F7 JSON path)");
|
|
32121
32478
|
return adapter.extractResponse(path);
|
|
32122
32479
|
}
|
|
32480
|
+
function resolveValueText(value, context) {
|
|
32481
|
+
return value === void 0 ? "" : context.resolve(String(value));
|
|
32482
|
+
}
|
|
32483
|
+
async function assertElementCount(adapter, locator, expected) {
|
|
32484
|
+
const windowMs = expected > 0 ? ASSERT_POLL_MS : ABSENT_SETTLE_MS;
|
|
32485
|
+
const startedAt = Date.now();
|
|
32486
|
+
for (; ; ) {
|
|
32487
|
+
const actual = await adapter.countElements(locator);
|
|
32488
|
+
if (actual === expected) return;
|
|
32489
|
+
if (Date.now() - startedAt > windowMs) {
|
|
32490
|
+
throw new TestPilotError(
|
|
32491
|
+
"ASSERTION_FAILED",
|
|
32492
|
+
`\u5143\u7D20\u6570\u91CF\u65AD\u8A00\u5931\u8D25(\u7B49\u5F85 ${windowMs}ms):${describeLocator(locator)} \u5B9E\u9645 ${actual},\u671F\u671B ${expected}`
|
|
32493
|
+
);
|
|
32494
|
+
}
|
|
32495
|
+
await delay(300);
|
|
32496
|
+
}
|
|
32497
|
+
}
|
|
32498
|
+
async function assertElementAttribute(adapter, locator, step, context) {
|
|
32499
|
+
const useProperty = step.property !== void 0;
|
|
32500
|
+
const name = (useProperty ? step.property : step.attribute) ?? "";
|
|
32501
|
+
const wanted = context.resolve(step.expected ?? "").trim();
|
|
32502
|
+
const startedAt = Date.now();
|
|
32503
|
+
for (; ; ) {
|
|
32504
|
+
const actual = useProperty ? await adapter.getElementProperty(locator, name) : await adapter.getElementAttribute(locator, name);
|
|
32505
|
+
const actualText = actual === void 0 || actual === null ? "<absent>" : String(actual);
|
|
32506
|
+
if (actualText.trim() === wanted) return;
|
|
32507
|
+
if (Date.now() - startedAt > ASSERT_POLL_MS) {
|
|
32508
|
+
throw new TestPilotError(
|
|
32509
|
+
"ASSERTION_FAILED",
|
|
32510
|
+
`\u5C5E\u6027\u65AD\u8A00\u5931\u8D25(\u7B49\u5F85 ${ASSERT_POLL_MS}ms):${describeLocator(locator)} \u7684 ${useProperty ? "property" : "attribute"} "${name}" \u671F\u671B "${wanted}",\u5B9E\u9645\u4E3A "${actualText.slice(0, 200)}"`
|
|
32511
|
+
);
|
|
32512
|
+
}
|
|
32513
|
+
await delay(300);
|
|
32514
|
+
}
|
|
32515
|
+
}
|
|
32123
32516
|
function resolveHeaders(headers, context) {
|
|
32124
32517
|
if (!headers) return void 0;
|
|
32125
32518
|
const resolved = {};
|
|
@@ -32262,6 +32655,12 @@ var ExecutionEngine = class {
|
|
|
32262
32655
|
result = await executeStep(step, index, adapter, context, artifacts, {
|
|
32263
32656
|
screenshotName: `${executionId}-${phase}-${String(index).padStart(2, "0")}`
|
|
32264
32657
|
});
|
|
32658
|
+
if (step.capture !== void 0 && typeof adapter.readWxApiCaptures === "function") {
|
|
32659
|
+
const captures = adapter.readWxApiCaptures();
|
|
32660
|
+
if (captures && captures.calls.length > 0) {
|
|
32661
|
+
context.seedObject(step.capture, captures.calls[0]?.options ?? null);
|
|
32662
|
+
}
|
|
32663
|
+
}
|
|
32265
32664
|
} catch (err) {
|
|
32266
32665
|
result = {
|
|
32267
32666
|
index,
|
|
@@ -32383,6 +32782,27 @@ var ExecutionEngine = class {
|
|
|
32383
32782
|
);
|
|
32384
32783
|
}
|
|
32385
32784
|
}
|
|
32785
|
+
for (const [, adapter] of evidenceAdapters) {
|
|
32786
|
+
await adapter.restoreWxMock?.().catch(() => void 0);
|
|
32787
|
+
}
|
|
32788
|
+
if (status !== "passed") {
|
|
32789
|
+
for (const [target, adapter] of evidenceAdapters) {
|
|
32790
|
+
if (typeof adapter.readDiagnostics !== "function") continue;
|
|
32791
|
+
try {
|
|
32792
|
+
const diagnostics = await adapter.readDiagnostics();
|
|
32793
|
+
const hasContent = diagnostics.consoleLogs.length > 0 || diagnostics.exceptions.length > 0 || (diagnostics.pageStack?.length ?? 0) > 0 || diagnostics.systemInfo !== void 0;
|
|
32794
|
+
if (!hasContent) continue;
|
|
32795
|
+
caseResult.diagnostics = await artifacts.saveDiagnostics(
|
|
32796
|
+
`${executionId}-${target}.json`,
|
|
32797
|
+
diagnostics
|
|
32798
|
+
);
|
|
32799
|
+
} catch (err) {
|
|
32800
|
+
await artifacts.logger.info(
|
|
32801
|
+
`diagnostics dump failed for ${target}: ${err instanceof Error ? err.message : String(err)}`
|
|
32802
|
+
);
|
|
32803
|
+
}
|
|
32804
|
+
}
|
|
32805
|
+
}
|
|
32386
32806
|
await bus.emit({
|
|
32387
32807
|
type: "case-finished",
|
|
32388
32808
|
runId,
|
|
@@ -32444,7 +32864,7 @@ function buildRunSummary(runId, startedAt, cases, options = {}) {
|
|
|
32444
32864
|
}
|
|
32445
32865
|
|
|
32446
32866
|
// ../../packages/execution-engine/src/engine/test-runner.ts
|
|
32447
|
-
import { writeFile as
|
|
32867
|
+
import { writeFile as writeFile10 } from "node:fs/promises";
|
|
32448
32868
|
import { join as join15, resolve as resolve6 } from "node:path";
|
|
32449
32869
|
var TestRunner = class {
|
|
32450
32870
|
resolver;
|
|
@@ -32508,7 +32928,7 @@ var TestRunner = class {
|
|
|
32508
32928
|
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
32509
32929
|
});
|
|
32510
32930
|
await artifacts.logger.info(`run ${runId} finished: ${summary.status}`);
|
|
32511
|
-
await
|
|
32931
|
+
await writeFile10(join15(runDir, RESULT_FILE), JSON.stringify(summary, null, 2), "utf8");
|
|
32512
32932
|
return summary;
|
|
32513
32933
|
}
|
|
32514
32934
|
};
|
|
@@ -32546,6 +32966,11 @@ import { mkdtemp, readFile as readFile6, rm } from "node:fs/promises";
|
|
|
32546
32966
|
import { tmpdir } from "node:os";
|
|
32547
32967
|
import { join as join16 } from "node:path";
|
|
32548
32968
|
import { errors } from "playwright";
|
|
32969
|
+
var TEXT_WAIT_MS = 1e4;
|
|
32970
|
+
var LONG_PRESS_MS = 600;
|
|
32971
|
+
function delay2(ms) {
|
|
32972
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
32973
|
+
}
|
|
32549
32974
|
var PlaywrightAdapter = class {
|
|
32550
32975
|
constructor(bundle, options = {}) {
|
|
32551
32976
|
this.bundle = bundle;
|
|
@@ -32618,6 +33043,21 @@ var PlaywrightAdapter = class {
|
|
|
32618
33043
|
const target = await this.toLocator(locator);
|
|
32619
33044
|
await this.coded(() => target.click());
|
|
32620
33045
|
}
|
|
33046
|
+
/** 长按元素:web 端没有原生 longpress,以按住 LONG_PRESS_MS 的 click 近似 */
|
|
33047
|
+
async longpress(locator) {
|
|
33048
|
+
const target = await this.toLocator(locator);
|
|
33049
|
+
await this.coded(() => target.click({ delay: LONG_PRESS_MS }));
|
|
33050
|
+
}
|
|
33051
|
+
/** 统计匹配元素数量(单次读取,不等待;重试语义由引擎负责) */
|
|
33052
|
+
async countElements(locator) {
|
|
33053
|
+
const target = await this.toLocator(locator);
|
|
33054
|
+
return target.count();
|
|
33055
|
+
}
|
|
33056
|
+
/** 读取元素 attribute(不存在时为 null) */
|
|
33057
|
+
async getElementAttribute(locator, name) {
|
|
33058
|
+
const target = await this.toLocator(locator);
|
|
33059
|
+
return this.coded(() => target.getAttribute(name));
|
|
33060
|
+
}
|
|
32621
33061
|
async input(locator, value) {
|
|
32622
33062
|
const target = await this.toLocator(locator);
|
|
32623
33063
|
await this.coded(() => target.fill(value));
|
|
@@ -32636,16 +33076,29 @@ var PlaywrightAdapter = class {
|
|
|
32636
33076
|
}
|
|
32637
33077
|
async assert(locator, expected) {
|
|
32638
33078
|
const target = await this.toLocator(locator);
|
|
32639
|
-
await this.coded(() => target.waitFor({ state: "visible" }));
|
|
32640
|
-
const actual = (await this.coded(() => target.innerText())).trim();
|
|
32641
33079
|
const wanted = expected.trim();
|
|
32642
|
-
|
|
32643
|
-
|
|
33080
|
+
const startedAt = Date.now();
|
|
33081
|
+
for (; ; ) {
|
|
33082
|
+
await this.coded(() => target.waitFor({ state: "visible" }));
|
|
33083
|
+
const actual = (await this.coded(() => target.innerText())).trim();
|
|
33084
|
+
if (actual.includes(wanted)) return;
|
|
33085
|
+
if (Date.now() - startedAt > TEXT_WAIT_MS) {
|
|
33086
|
+
throw new TestPilotError(
|
|
33087
|
+
"ASSERTION_FAILED",
|
|
33088
|
+
`\u65AD\u8A00\u5931\u8D25(\u7B49\u5F85 ${TEXT_WAIT_MS}ms):\u671F\u671B\u5305\u542B "${wanted}",\u5B9E\u9645\u4E3A "${actual.slice(0, 200)}"`
|
|
33089
|
+
);
|
|
33090
|
+
}
|
|
33091
|
+
await delay2(300);
|
|
32644
33092
|
}
|
|
32645
33093
|
}
|
|
32646
33094
|
async extract(locator) {
|
|
32647
33095
|
const target = await this.toLocator(locator);
|
|
32648
|
-
|
|
33096
|
+
const startedAt = Date.now();
|
|
33097
|
+
for (; ; ) {
|
|
33098
|
+
const text = (await this.coded(() => target.innerText())).trim();
|
|
33099
|
+
if (text.length > 0 || Date.now() - startedAt > TEXT_WAIT_MS) return text;
|
|
33100
|
+
await delay2(300);
|
|
33101
|
+
}
|
|
32649
33102
|
}
|
|
32650
33103
|
async screenshot() {
|
|
32651
33104
|
return (await this.ensurePage()).screenshot();
|
|
@@ -32710,7 +33163,11 @@ import net from "node:net";
|
|
|
32710
33163
|
var AUTO_PORT_FALLBACK_WAIT_MS = 9e4;
|
|
32711
33164
|
var NAVIGATE_RETRY_WINDOW_MS = 45e3;
|
|
32712
33165
|
var ELEMENT_WAIT_MS = 1e4;
|
|
33166
|
+
var TEXT_WAIT_MS2 = 1e4;
|
|
33167
|
+
var PAGE_READY_WAIT_MS = 3e4;
|
|
32713
33168
|
var CONNECTION_LOST = /connection closed|not connected/i;
|
|
33169
|
+
var DIAGNOSTICS_BUFFER_MAX = 200;
|
|
33170
|
+
var CONSOLE_TEXT_LIMIT = 500;
|
|
32714
33171
|
var automatorLauncher = {
|
|
32715
33172
|
launch: async (options) => {
|
|
32716
33173
|
try {
|
|
@@ -32723,9 +33180,23 @@ var automatorLauncher = {
|
|
|
32723
33180
|
}
|
|
32724
33181
|
}
|
|
32725
33182
|
};
|
|
32726
|
-
function
|
|
33183
|
+
function delay3(ms) {
|
|
32727
33184
|
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
32728
33185
|
}
|
|
33186
|
+
function serializeConsoleArg(value) {
|
|
33187
|
+
if (typeof value === "string") return value;
|
|
33188
|
+
try {
|
|
33189
|
+
return JSON.stringify(value) ?? String(value);
|
|
33190
|
+
} catch {
|
|
33191
|
+
return String(value);
|
|
33192
|
+
}
|
|
33193
|
+
}
|
|
33194
|
+
function stringifyDataValue(value) {
|
|
33195
|
+
if (value === void 0 || value === null) return "";
|
|
33196
|
+
if (typeof value === "string") return value;
|
|
33197
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
33198
|
+
return String(value);
|
|
33199
|
+
}
|
|
32729
33200
|
async function connectWithRetry(wsEndpoint, timeoutMs) {
|
|
32730
33201
|
const startedAt = Date.now();
|
|
32731
33202
|
let lastError;
|
|
@@ -32734,7 +33205,7 @@ async function connectWithRetry(wsEndpoint, timeoutMs) {
|
|
|
32734
33205
|
return await automator.connect({ wsEndpoint });
|
|
32735
33206
|
} catch (err) {
|
|
32736
33207
|
lastError = err;
|
|
32737
|
-
await
|
|
33208
|
+
await delay3(1e3);
|
|
32738
33209
|
}
|
|
32739
33210
|
}
|
|
32740
33211
|
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
@@ -32786,6 +33257,10 @@ var MiniAppAdapter = class {
|
|
|
32786
33257
|
target = "miniapp";
|
|
32787
33258
|
program;
|
|
32788
33259
|
requestCapture;
|
|
33260
|
+
/** console / exception 环形缓冲(失败取证的数据源);重连后继续累积 */
|
|
33261
|
+
consoleLogs = [];
|
|
33262
|
+
exceptions = [];
|
|
33263
|
+
diagnosticsAttachedTo;
|
|
32789
33264
|
/** 拉起开发者工具并建立自动化连接;需幂等 */
|
|
32790
33265
|
async launch() {
|
|
32791
33266
|
if (this.program) return;
|
|
@@ -32794,6 +33269,7 @@ var MiniAppAdapter = class {
|
|
|
32794
33269
|
};
|
|
32795
33270
|
if (this.config.cliPath) launchOptions.cliPath = this.config.cliPath;
|
|
32796
33271
|
this.program = await this.launcher.launch(launchOptions);
|
|
33272
|
+
this.attachDiagnostics(this.program);
|
|
32797
33273
|
}
|
|
32798
33274
|
async navigate(url2) {
|
|
32799
33275
|
const startedAt = Date.now();
|
|
@@ -32803,14 +33279,31 @@ var MiniAppAdapter = class {
|
|
|
32803
33279
|
await this.runWithReconnect(async () => {
|
|
32804
33280
|
await (await this.getProgram()).reLaunch(url2);
|
|
32805
33281
|
});
|
|
33282
|
+
await this.waitForPageReady();
|
|
32806
33283
|
return;
|
|
32807
33284
|
} catch (err) {
|
|
32808
33285
|
lastError = err;
|
|
32809
|
-
await
|
|
33286
|
+
await delay3(3e3);
|
|
32810
33287
|
}
|
|
32811
33288
|
}
|
|
32812
33289
|
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
32813
33290
|
}
|
|
33291
|
+
/** 等待页面就绪:模拟器编译期间 currentPage / data 会失败或挂起,轮询至可响应 */
|
|
33292
|
+
async waitForPageReady(timeoutMs = PAGE_READY_WAIT_MS) {
|
|
33293
|
+
const startedAt = Date.now();
|
|
33294
|
+
for (; ; ) {
|
|
33295
|
+
const page = await (await this.getProgram()).currentPage();
|
|
33296
|
+
const data = await page.data().catch(() => void 0);
|
|
33297
|
+
if (data !== void 0 && data !== null) return;
|
|
33298
|
+
if (Date.now() - startedAt > timeoutMs) {
|
|
33299
|
+
throw new TestPilotError(
|
|
33300
|
+
"PAGE_READY_TIMEOUT",
|
|
33301
|
+
`\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`
|
|
33302
|
+
);
|
|
33303
|
+
}
|
|
33304
|
+
await delay3(300);
|
|
33305
|
+
}
|
|
33306
|
+
}
|
|
32814
33307
|
async click(locator) {
|
|
32815
33308
|
await this.runWithReconnect(async () => (await this.element(locator)).tap());
|
|
32816
33309
|
}
|
|
@@ -32833,19 +33326,240 @@ var MiniAppAdapter = class {
|
|
|
32833
33326
|
async assert(locator, expected) {
|
|
32834
33327
|
await this.runWithReconnect(async () => {
|
|
32835
33328
|
const element = await this.element(locator);
|
|
32836
|
-
const actual = (await element.text()).trim();
|
|
32837
33329
|
const wanted = expected.trim();
|
|
32838
|
-
|
|
32839
|
-
|
|
33330
|
+
const startedAt = Date.now();
|
|
33331
|
+
for (; ; ) {
|
|
33332
|
+
const actual = (await element.text()).trim();
|
|
33333
|
+
if (actual.includes(wanted)) return;
|
|
33334
|
+
if (Date.now() - startedAt > TEXT_WAIT_MS2) {
|
|
33335
|
+
throw new TestPilotError(
|
|
33336
|
+
"ASSERTION_FAILED",
|
|
33337
|
+
`\u65AD\u8A00\u5931\u8D25(\u7B49\u5F85 ${TEXT_WAIT_MS2}ms):\u671F\u671B\u5305\u542B "${wanted}",\u5B9E\u9645\u4E3A "${actual.slice(0, 200)}"`
|
|
33338
|
+
);
|
|
33339
|
+
}
|
|
33340
|
+
await delay3(300);
|
|
32840
33341
|
}
|
|
32841
33342
|
});
|
|
32842
33343
|
}
|
|
32843
33344
|
async extract(locator) {
|
|
32844
33345
|
return this.runWithReconnect(async () => {
|
|
32845
33346
|
const element = await this.element(locator);
|
|
32846
|
-
|
|
33347
|
+
const startedAt = Date.now();
|
|
33348
|
+
for (; ; ) {
|
|
33349
|
+
const text = (await element.text()).trim();
|
|
33350
|
+
if (text.length > 0 || Date.now() - startedAt > TEXT_WAIT_MS2) return text;
|
|
33351
|
+
await delay3(300);
|
|
33352
|
+
}
|
|
33353
|
+
});
|
|
33354
|
+
}
|
|
33355
|
+
/** 调用 Page / 组件方法;不传 locator 时为页面级调用(Page 实例方法) */
|
|
33356
|
+
async callMethod(locator, methodName, args = []) {
|
|
33357
|
+
return this.runWithReconnect(async () => {
|
|
33358
|
+
if (locator) {
|
|
33359
|
+
const element = await this.element(locator);
|
|
33360
|
+
return element.callMethod(methodName, ...args);
|
|
33361
|
+
}
|
|
33362
|
+
const page = await (await this.getProgram()).currentPage();
|
|
33363
|
+
return page.callMethod(methodName, ...args);
|
|
33364
|
+
});
|
|
33365
|
+
}
|
|
33366
|
+
// ---- miniapp 深度测试(V0.2)----
|
|
33367
|
+
wxMock;
|
|
33368
|
+
/** mock 任意 wx.* API:拦截调用并返回 mock 值;capture 记录真实调用参数(最近调用在前) */
|
|
33369
|
+
async mockWxApi(options) {
|
|
33370
|
+
if (this.wxMock?.api === options.api) await this.restoreWxMock();
|
|
33371
|
+
const program2 = await this.getProgram();
|
|
33372
|
+
const captured = [];
|
|
33373
|
+
this.wxMock = { api: options.api, captured };
|
|
33374
|
+
const callbackName = options.capture ? `__tpWxCapture_${options.capture}` : void 0;
|
|
33375
|
+
if (callbackName) {
|
|
33376
|
+
await program2.exposeFunction(callbackName, (payload) => {
|
|
33377
|
+
if (payload !== null && typeof payload === "object") {
|
|
33378
|
+
captured.push(payload);
|
|
33379
|
+
}
|
|
33380
|
+
});
|
|
33381
|
+
}
|
|
33382
|
+
await program2.mockWxMethod(
|
|
33383
|
+
options.api,
|
|
33384
|
+
function(invokeOptions, cbName) {
|
|
33385
|
+
const callbacks = globalThis;
|
|
33386
|
+
callbacks[cbName]?.({ options: invokeOptions });
|
|
33387
|
+
return options.result ?? this.origin(invokeOptions);
|
|
33388
|
+
},
|
|
33389
|
+
callbackName
|
|
33390
|
+
);
|
|
33391
|
+
}
|
|
33392
|
+
/** 读取 mock 期间捕获的调用参数(引擎在带 capture 的 mockWxApi step 后调用;最近调用在前) */
|
|
33393
|
+
readWxApiCaptures() {
|
|
33394
|
+
if (!this.wxMock) return void 0;
|
|
33395
|
+
return { name: this.wxMock.api, calls: [...this.wxMock.captured].reverse() };
|
|
33396
|
+
}
|
|
33397
|
+
/** @deprecated 引擎已改用 readWxApiCaptures();保留按 API 名读取的旧入口 */
|
|
33398
|
+
readCapturedWxApiCalls(api) {
|
|
33399
|
+
return this.wxMock?.api === api ? [...this.wxMock.captured].reverse() : [];
|
|
33400
|
+
}
|
|
33401
|
+
/** 恢复 mock 的 wx API(用例结束由引擎调用) */
|
|
33402
|
+
async restoreWxMock() {
|
|
33403
|
+
const mock = this.wxMock;
|
|
33404
|
+
if (!mock) return;
|
|
33405
|
+
this.wxMock = void 0;
|
|
33406
|
+
if (!this.program) return;
|
|
33407
|
+
await this.program.restoreWxMethod(mock.api).catch(() => void 0);
|
|
33408
|
+
}
|
|
33409
|
+
/** 触发组件事件(trigger;detail 作为 e.detail 注入,空 detail 即"用户拒绝/取消"类分支) */
|
|
33410
|
+
async trigger(locator, eventType, detail) {
|
|
33411
|
+
await this.runWithReconnect(async () => {
|
|
33412
|
+
const element = await this.element(locator);
|
|
33413
|
+
await element.trigger(eventType, detail);
|
|
33414
|
+
});
|
|
33415
|
+
}
|
|
33416
|
+
/** 读取页面 data(点号路径,缺省为整个 data 对象) */
|
|
33417
|
+
async getData(dataPath) {
|
|
33418
|
+
return this.runWithReconnect(async () => {
|
|
33419
|
+
const page = await (await this.getProgram()).currentPage();
|
|
33420
|
+
return page.data(dataPath);
|
|
33421
|
+
});
|
|
33422
|
+
}
|
|
33423
|
+
/** 写入 data 补丁(setData 驱动 onShow/onPullDownRefresh 等真实链路);locator 提供时为组件级 */
|
|
33424
|
+
async setData(patch, locator) {
|
|
33425
|
+
await this.runWithReconnect(async () => {
|
|
33426
|
+
if (locator) {
|
|
33427
|
+
await (await this.element(locator)).setData(patch);
|
|
33428
|
+
return;
|
|
33429
|
+
}
|
|
33430
|
+
const page = await (await this.getProgram()).currentPage();
|
|
33431
|
+
await page.setData(patch);
|
|
32847
33432
|
});
|
|
32848
33433
|
}
|
|
33434
|
+
/** 断言页面 data 路径的值包含 expected(数据绑定未完成时轮询等待) */
|
|
33435
|
+
async assertData(dataPath, expected) {
|
|
33436
|
+
await this.runWithReconnect(async () => {
|
|
33437
|
+
const page = await (await this.getProgram()).currentPage();
|
|
33438
|
+
const wanted = expected.trim();
|
|
33439
|
+
const startedAt = Date.now();
|
|
33440
|
+
for (; ; ) {
|
|
33441
|
+
const actual = stringifyDataValue(await page.data(dataPath));
|
|
33442
|
+
if (actual.includes(wanted)) return;
|
|
33443
|
+
if (Date.now() - startedAt > TEXT_WAIT_MS2) {
|
|
33444
|
+
throw new TestPilotError(
|
|
33445
|
+
"ASSERTION_FAILED",
|
|
33446
|
+
`\u9875\u9762 data \u65AD\u8A00\u5931\u8D25(\u7B49\u5F85 ${TEXT_WAIT_MS2}ms):data.${dataPath} \u671F\u671B\u5305\u542B "${wanted}",\u5B9E\u9645\u4E3A "${actual.slice(0, 200)}"`
|
|
33447
|
+
);
|
|
33448
|
+
}
|
|
33449
|
+
await delay3(300);
|
|
33450
|
+
}
|
|
33451
|
+
});
|
|
33452
|
+
}
|
|
33453
|
+
// ---- miniapp 交互扩展(对照官方 automator Element / MiniProgram API)----
|
|
33454
|
+
/** 长按元素 */
|
|
33455
|
+
async longpress(locator) {
|
|
33456
|
+
await this.runWithReconnect(async () => (await this.element(locator)).longpress());
|
|
33457
|
+
}
|
|
33458
|
+
/** 主动调用 wx.* API(如 setStorageSync / clearStorageSync 造前置状态) */
|
|
33459
|
+
async callWxApi(api, args = []) {
|
|
33460
|
+
return this.runWithReconnect(async () => {
|
|
33461
|
+
const program2 = await this.getProgram();
|
|
33462
|
+
if (typeof program2.callWxMethod !== "function") {
|
|
33463
|
+
throw new TestPilotError(
|
|
33464
|
+
"UNSUPPORTED_ACTION",
|
|
33465
|
+
"\u5F53\u524D miniprogram-automator \u4E0D\u652F\u6301 callWxMethod,\u8BF7\u5347\u7EA7\u5FAE\u4FE1\u5F00\u53D1\u8005\u5DE5\u5177\u540E\u91CD\u8BD5"
|
|
33466
|
+
);
|
|
33467
|
+
}
|
|
33468
|
+
return program2.callWxMethod(api, ...args);
|
|
33469
|
+
});
|
|
33470
|
+
}
|
|
33471
|
+
/** 派发触摸事件(touchstart / touchmove / touchend) */
|
|
33472
|
+
async touch(locator, phase, touches, changedTouches) {
|
|
33473
|
+
await this.runWithReconnect(async () => {
|
|
33474
|
+
const element = await this.element(locator);
|
|
33475
|
+
const options = { touches: [...touches], changedTouches: [...changedTouches] };
|
|
33476
|
+
if (phase === "touchstart") await element.touchstart(options);
|
|
33477
|
+
else if (phase === "touchmove") await element.touchmove(options);
|
|
33478
|
+
else if (phase === "touchend") await element.touchend(options);
|
|
33479
|
+
else throw new TestPilotError("UNSUPPORTED_ACTION", `\u672A\u77E5\u89E6\u6478\u9636\u6BB5:${phase}`);
|
|
33480
|
+
});
|
|
33481
|
+
}
|
|
33482
|
+
/** 滚动:locator 提供时滚动 scroll-view 元素,缺省滚动页面 */
|
|
33483
|
+
async scroll(locator, scrollTop) {
|
|
33484
|
+
await this.runWithReconnect(async () => {
|
|
33485
|
+
if (locator) {
|
|
33486
|
+
await (await this.element(locator)).scrollTo(scrollTop);
|
|
33487
|
+
return;
|
|
33488
|
+
}
|
|
33489
|
+
const program2 = await this.getProgram();
|
|
33490
|
+
if (typeof program2.pageScrollTo !== "function") {
|
|
33491
|
+
throw new TestPilotError(
|
|
33492
|
+
"UNSUPPORTED_ACTION",
|
|
33493
|
+
"\u5F53\u524D miniprogram-automator \u4E0D\u652F\u6301 pageScrollTo,\u8BF7\u5347\u7EA7\u5FAE\u4FE1\u5F00\u53D1\u8005\u5DE5\u5177\u540E\u91CD\u8BD5"
|
|
33494
|
+
);
|
|
33495
|
+
}
|
|
33496
|
+
await program2.pageScrollTo(scrollTop);
|
|
33497
|
+
});
|
|
33498
|
+
}
|
|
33499
|
+
/** swiper 切换到指定页 */
|
|
33500
|
+
async swipeTo(locator, index) {
|
|
33501
|
+
await this.runWithReconnect(async () => (await this.element(locator)).swipeTo(index));
|
|
33502
|
+
}
|
|
33503
|
+
/** slider 滑到指定值 */
|
|
33504
|
+
async slideTo(locator, value) {
|
|
33505
|
+
await this.runWithReconnect(async () => (await this.element(locator)).slideTo(value));
|
|
33506
|
+
}
|
|
33507
|
+
/** movable-view 移动到指定坐标 */
|
|
33508
|
+
async moveTo(locator, x, y) {
|
|
33509
|
+
await this.runWithReconnect(async () => (await this.element(locator)).moveTo(x, y));
|
|
33510
|
+
}
|
|
33511
|
+
/** 统计匹配元素数量(单次读取;存在性/计数断言的重试语义由引擎负责) */
|
|
33512
|
+
async countElements(locator) {
|
|
33513
|
+
return this.runWithReconnect(async () => {
|
|
33514
|
+
const page = await (await this.getProgram()).currentPage();
|
|
33515
|
+
const elements = await page.$$(this.cssSelector(locator));
|
|
33516
|
+
return elements.length;
|
|
33517
|
+
});
|
|
33518
|
+
}
|
|
33519
|
+
/** 读取元素 attribute(不存在时为 undefined/null) */
|
|
33520
|
+
async getElementAttribute(locator, name) {
|
|
33521
|
+
return this.runWithReconnect(async () => (await this.element(locator)).attribute(name));
|
|
33522
|
+
}
|
|
33523
|
+
/** 读取元素 property(miniapp 自定义组件实例属性) */
|
|
33524
|
+
async getElementProperty(locator, name) {
|
|
33525
|
+
return this.runWithReconnect(async () => (await this.element(locator)).property(name));
|
|
33526
|
+
}
|
|
33527
|
+
/** 导出运行时诊断:console 尾部 / 异常 / 页面栈 / systemInfo(失败取证由引擎落盘) */
|
|
33528
|
+
async readDiagnostics() {
|
|
33529
|
+
const program2 = this.program;
|
|
33530
|
+
const pageStack = await program2?.pageStack?.().catch(() => void 0);
|
|
33531
|
+
const systemInfo = await program2?.systemInfo?.().catch(() => void 0);
|
|
33532
|
+
return {
|
|
33533
|
+
consoleLogs: [...this.consoleLogs],
|
|
33534
|
+
exceptions: [...this.exceptions],
|
|
33535
|
+
pageStack,
|
|
33536
|
+
systemInfo
|
|
33537
|
+
};
|
|
33538
|
+
}
|
|
33539
|
+
/** 订阅小程序 console / exception 事件(每个 program 实例只挂一次) */
|
|
33540
|
+
attachDiagnostics(program2) {
|
|
33541
|
+
if (this.diagnosticsAttachedTo === program2 || typeof program2.on !== "function") return;
|
|
33542
|
+
this.diagnosticsAttachedTo = program2;
|
|
33543
|
+
program2.on("console", (payload) => {
|
|
33544
|
+
const record2 = payload ?? {};
|
|
33545
|
+
const args = Array.isArray(record2.args) ? record2.args : [];
|
|
33546
|
+
this.pushConsoleLog({
|
|
33547
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
33548
|
+
type: typeof record2.type === "string" ? record2.type : "log",
|
|
33549
|
+
text: args.map(serializeConsoleArg).join(" ").slice(0, CONSOLE_TEXT_LIMIT)
|
|
33550
|
+
});
|
|
33551
|
+
});
|
|
33552
|
+
program2.on("exception", (payload) => {
|
|
33553
|
+
const record2 = payload ?? {};
|
|
33554
|
+
const message = typeof record2.message === "string" ? record2.message : String(payload);
|
|
33555
|
+
this.exceptions.push(message.slice(0, CONSOLE_TEXT_LIMIT));
|
|
33556
|
+
if (this.exceptions.length > DIAGNOSTICS_BUFFER_MAX) this.exceptions.shift();
|
|
33557
|
+
});
|
|
33558
|
+
}
|
|
33559
|
+
pushConsoleLog(entry) {
|
|
33560
|
+
this.consoleLogs.push(entry);
|
|
33561
|
+
if (this.consoleLogs.length > DIAGNOSTICS_BUFFER_MAX) this.consoleLogs.shift();
|
|
33562
|
+
}
|
|
32849
33563
|
async screenshot() {
|
|
32850
33564
|
return this.runWithReconnect(async () => {
|
|
32851
33565
|
const base643 = await (await this.getProgram()).screenshot();
|
|
@@ -32936,7 +33650,7 @@ var MiniAppAdapter = class {
|
|
|
32936
33650
|
if (Date.now() - startedAt > ELEMENT_WAIT_MS) {
|
|
32937
33651
|
throw new TestPilotError("ELEMENT_NOT_FOUND", `\u7B49\u5F85\u5143\u7D20\u8D85\u65F6(${ELEMENT_WAIT_MS}ms):${selector}`);
|
|
32938
33652
|
}
|
|
32939
|
-
await
|
|
33653
|
+
await delay3(500);
|
|
32940
33654
|
}
|
|
32941
33655
|
}
|
|
32942
33656
|
};
|
|
@@ -33359,7 +34073,7 @@ var TestPilotClient = class {
|
|
|
33359
34073
|
};
|
|
33360
34074
|
|
|
33361
34075
|
// src/commands/init/adapter-detector.ts
|
|
33362
|
-
import { existsSync as existsSync5, readFileSync } from "node:fs";
|
|
34076
|
+
import { existsSync as existsSync5, readFileSync as readFileSync2 } from "node:fs";
|
|
33363
34077
|
import { join as join20 } from "node:path";
|
|
33364
34078
|
async function detectAdapters(root, projectType) {
|
|
33365
34079
|
const required2 = projectType === "hybrid" ? ["playwright", "wechatide"] : projectType === "wechat-miniapp" ? ["wechatide"] : projectType === "web" ? ["playwright"] : [];
|
|
@@ -33386,7 +34100,7 @@ function readPackageJsonDeps(root) {
|
|
|
33386
34100
|
const file2 = join20(root, "package.json");
|
|
33387
34101
|
if (!existsSync5(file2)) return void 0;
|
|
33388
34102
|
try {
|
|
33389
|
-
const pkg = JSON.parse(
|
|
34103
|
+
const pkg = JSON.parse(readFileSync2(file2, "utf8"));
|
|
33390
34104
|
return { ...pkg.dependencies, ...pkg.devDependencies };
|
|
33391
34105
|
} catch {
|
|
33392
34106
|
return void 0;
|
|
@@ -33416,7 +34130,7 @@ async function collectEnvVarRefs(root) {
|
|
|
33416
34130
|
}
|
|
33417
34131
|
|
|
33418
34132
|
// src/commands/init/project-detector.ts
|
|
33419
|
-
import { existsSync as existsSync6, readFileSync as
|
|
34133
|
+
import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "node:fs";
|
|
33420
34134
|
import { basename, join as join22 } from "node:path";
|
|
33421
34135
|
|
|
33422
34136
|
// src/lib/git.ts
|
|
@@ -33457,7 +34171,7 @@ function readPackageJson(root) {
|
|
|
33457
34171
|
const file2 = join22(root, "package.json");
|
|
33458
34172
|
if (!existsSync6(file2)) return void 0;
|
|
33459
34173
|
try {
|
|
33460
|
-
return JSON.parse(
|
|
34174
|
+
return JSON.parse(readFileSync3(file2, "utf8"));
|
|
33461
34175
|
} catch {
|
|
33462
34176
|
return void 0;
|
|
33463
34177
|
}
|
|
@@ -33472,10 +34186,31 @@ function detectProjectType(root) {
|
|
|
33472
34186
|
if (isWeb) return "web";
|
|
33473
34187
|
return "unknown";
|
|
33474
34188
|
}
|
|
34189
|
+
function detectLanguageFromSources(root) {
|
|
34190
|
+
const SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "miniprogram_npm", "dist", ".git"]);
|
|
34191
|
+
const found = { ts: false, js: false };
|
|
34192
|
+
const walk = (dir, depth) => {
|
|
34193
|
+
if (depth > 4 || found.ts && found.js) return;
|
|
34194
|
+
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
34195
|
+
if (entry.name.startsWith(".")) continue;
|
|
34196
|
+
const full = join22(dir, entry.name);
|
|
34197
|
+
if (entry.isDirectory()) {
|
|
34198
|
+
if (!SKIP_DIRS.has(entry.name)) walk(full, depth + 1);
|
|
34199
|
+
continue;
|
|
34200
|
+
}
|
|
34201
|
+
if (/\.(ts|tsx)$/.test(entry.name)) found.ts = true;
|
|
34202
|
+
else if (/\.(js|cjs|mjs|wxml)$/.test(entry.name)) found.js = true;
|
|
34203
|
+
}
|
|
34204
|
+
};
|
|
34205
|
+
walk(root, 0);
|
|
34206
|
+
if (found.ts) return "TypeScript";
|
|
34207
|
+
if (found.js) return "JavaScript";
|
|
34208
|
+
return "Unknown";
|
|
34209
|
+
}
|
|
33475
34210
|
async function detectProject(root) {
|
|
33476
34211
|
const pkg = readPackageJson(root);
|
|
33477
34212
|
const type = detectProjectType(root);
|
|
33478
|
-
const language = existsSync6(join22(root, "tsconfig.json")) ? "TypeScript" : pkg || existsSync6(join22(root, "src")) ? "JavaScript" :
|
|
34213
|
+
const language = existsSync6(join22(root, "tsconfig.json")) ? "TypeScript" : pkg || existsSync6(join22(root, "src")) ? "JavaScript" : detectLanguageFromSources(root);
|
|
33479
34214
|
const packageManager = existsSync6(join22(root, "pnpm-lock.yaml")) ? "pnpm" : existsSync6(join22(root, "yarn.lock")) ? "yarn" : existsSync6(join22(root, "package-lock.json")) ? "npm" : "npx";
|
|
33480
34215
|
const existingE2eDirs = E2E_DIR_CANDIDATES.filter((dir) => existsSync6(join22(root, dir)));
|
|
33481
34216
|
return {
|
|
@@ -33503,8 +34238,8 @@ function adaptersFor(type) {
|
|
|
33503
34238
|
}
|
|
33504
34239
|
|
|
33505
34240
|
// src/commands/init/skill-installer.ts
|
|
33506
|
-
import { existsSync as existsSync7, readFileSync as
|
|
33507
|
-
import { access as access2, cp, mkdir as mkdir12, writeFile as
|
|
34241
|
+
import { existsSync as existsSync7, readFileSync as readFileSync4 } from "node:fs";
|
|
34242
|
+
import { access as access2, cp, mkdir as mkdir12, writeFile as writeFile11 } from "node:fs/promises";
|
|
33508
34243
|
import { dirname as dirname9, join as join23 } from "node:path";
|
|
33509
34244
|
import { fileURLToPath } from "node:url";
|
|
33510
34245
|
var CORE_FILES = ["SKILL.md", "manifest.yaml"];
|
|
@@ -33520,7 +34255,7 @@ async function exists3(path) {
|
|
|
33520
34255
|
function readManifestVersion(dir) {
|
|
33521
34256
|
const manifestPath = join23(dir, "manifest.yaml");
|
|
33522
34257
|
if (!existsSync7(manifestPath)) return void 0;
|
|
33523
|
-
return /^version:\s*([^\s#]+)/m.exec(
|
|
34258
|
+
return /^version:\s*([^\s#]+)/m.exec(readFileSync4(manifestPath, "utf8"))?.[1];
|
|
33524
34259
|
}
|
|
33525
34260
|
function resolveSkillSourceDir() {
|
|
33526
34261
|
const fromEnv = process.env.TESTPILOT_SKILL_DIR;
|
|
@@ -33589,7 +34324,7 @@ async function installSkill(root, info, adapters) {
|
|
|
33589
34324
|
const file2 = join23(refsDir, name);
|
|
33590
34325
|
if (await exists3(file2)) continue;
|
|
33591
34326
|
await mkdir12(refsDir, { recursive: true });
|
|
33592
|
-
await
|
|
34327
|
+
await writeFile11(file2, render(), "utf8");
|
|
33593
34328
|
references = "created";
|
|
33594
34329
|
}
|
|
33595
34330
|
return coreVersion ? { core, coreVersion, references, dest } : { core, references, dest };
|
|
@@ -33708,9 +34443,44 @@ async function runDoctor(root) {
|
|
|
33708
34443
|
(item) => item.available ? ok(`${item.id}(${item.detail})`) : item.required ? warn(`${item.id} \u4E0D\u53EF\u7528`, item.detail) : warn(`${item.id} \u672A\u5B89\u88C5(\u53EF\u9009)`, item.detail)
|
|
33709
34444
|
)
|
|
33710
34445
|
);
|
|
34446
|
+
const miniappProjectPath = config2?.miniapp?.projectPath;
|
|
34447
|
+
if (miniappProjectPath) {
|
|
34448
|
+
const miniappItems = [];
|
|
34449
|
+
const cliPath = process.env.WECHAT_DEVTOOLS_CLI ?? config2?.miniapp?.cliPath ?? await findWechatDevToolsCli();
|
|
34450
|
+
if (!cliPath) {
|
|
34451
|
+
miniappItems.push(
|
|
34452
|
+
warn("\u5FAE\u4FE1\u5F00\u53D1\u8005\u5DE5\u5177 CLI \u672A\u627E\u5230", "\u5B89\u88C5\u5FAE\u4FE1\u5F00\u53D1\u8005\u5DE5\u5177,\u6216\u8BBE\u7F6E WECHAT_DEVTOOLS_CLI \u6307\u5411 cli")
|
|
34453
|
+
);
|
|
34454
|
+
} else {
|
|
34455
|
+
miniappItems.push(ok(`\u5F00\u53D1\u8005\u5DE5\u5177 CLI(${cliPath})`));
|
|
34456
|
+
const loggedIn = await isWechatDevToolsLoggedIn(cliPath);
|
|
34457
|
+
if (loggedIn === true) miniappItems.push(ok("\u5F00\u53D1\u8005\u5DE5\u5177\u5DF2\u767B\u5F55"));
|
|
34458
|
+
else if (loggedIn === false)
|
|
34459
|
+
miniappItems.push(
|
|
34460
|
+
warn("\u5F00\u53D1\u8005\u5DE5\u5177\u672A\u767B\u5F55", "\u6253\u5F00\u5DE5\u5177\u626B\u7801\u767B\u5F55;CI \u65E0\u4EBA\u503C\u5B88\u7684\u767B\u5F55\u6001\u7EF4\u62A4\u89C1 docs/miniapp-testing.md")
|
|
34461
|
+
);
|
|
34462
|
+
else
|
|
34463
|
+
miniappItems.push(
|
|
34464
|
+
warn("\u5F00\u53D1\u8005\u5DE5\u5177\u767B\u5F55\u6001\u672A\u77E5", "cli islogin \u65E0\u6709\u6548\u8F93\u51FA;\u8BF7\u786E\u8BA4\u5DE5\u5177\u5DF2\u767B\u5F55\u4E14 \u5B89\u5168\u8BBE\u7F6E->\u670D\u52A1\u7AEF\u53E3 \u5DF2\u5F00\u542F")
|
|
34465
|
+
);
|
|
34466
|
+
}
|
|
34467
|
+
const appidInfo = readMiniappProjectAppid(miniappProjectPath);
|
|
34468
|
+
if (!appidInfo) {
|
|
34469
|
+
miniappItems.push(warn("project.config.json \u7F3A\u5931", `${miniappProjectPath} \u4E0B\u672A\u627E\u5230,\u81EA\u52A8\u5316\u65E0\u6CD5\u5B9A\u4F4D\u9879\u76EE`));
|
|
34470
|
+
} else if (!appidInfo.appid) {
|
|
34471
|
+
miniappItems.push(warn("project.config.json \u7F3A\u5C11 appid", "\u81EA\u52A8\u5316\u4E0E\u9884\u89C8\u80FD\u529B\u53D7\u9650,\u8BF7\u586B\u5199\u771F\u5B9E appid"));
|
|
34472
|
+
} else if (appidInfo.appid === "touristappid") {
|
|
34473
|
+
miniappItems.push(
|
|
34474
|
+
warn("appid \u4E3A touristappid(\u6E38\u5BA2\u6A21\u5F0F)", "\u90E8\u5206\u81EA\u52A8\u5316/\u9884\u89C8\u80FD\u529B\u53D7\u9650,\u8BF7\u5728 project.config.json \u586B\u5199\u771F\u5B9E appid")
|
|
34475
|
+
);
|
|
34476
|
+
} else {
|
|
34477
|
+
miniappItems.push(ok(`\u5C0F\u7A0B\u5E8F appid(${appidInfo.appid})`));
|
|
34478
|
+
}
|
|
34479
|
+
push("Miniapp IDE", miniappItems);
|
|
34480
|
+
}
|
|
33711
34481
|
const refs = await collectEnvVarRefs(root);
|
|
33712
34482
|
const envItems = refs.map(
|
|
33713
|
-
(name) => process.env[name] !== void 0 ? ok(name) : error62(`${name} missing`, "\u5728\
|
|
34483
|
+
(name) => process.env[name] !== void 0 ? ok(name) : error62(`${name} missing`, "\u5728 .env.testing(\u65E7\u7248\u672C\u4E3A .env,\u8BF7\u8FC1\u79FB)\u6216 CI Secret \u4E2D\u8BBE\u7F6E")
|
|
33714
34484
|
);
|
|
33715
34485
|
const serverConfigured = (process.env.TESTPILOT_SERVER_URL ?? config2?.server?.baseUrl) !== void 0;
|
|
33716
34486
|
const accountRefs = [
|
|
@@ -33785,7 +34555,7 @@ function makeDoctorCommand() {
|
|
|
33785
34555
|
import { join as join27 } from "node:path";
|
|
33786
34556
|
|
|
33787
34557
|
// src/commands/init/config-initializer.ts
|
|
33788
|
-
import { access as access3, mkdir as mkdir13, readFile as readFile10, writeFile as
|
|
34558
|
+
import { access as access3, mkdir as mkdir13, readFile as readFile10, writeFile as writeFile12 } from "node:fs/promises";
|
|
33789
34559
|
import { basename as basename2, join as join25 } from "node:path";
|
|
33790
34560
|
async function exists4(path) {
|
|
33791
34561
|
try {
|
|
@@ -33817,7 +34587,7 @@ environment:
|
|
|
33817
34587
|
staging:
|
|
33818
34588
|
baseUrl: \${STAGING_BASE_URL}
|
|
33819
34589
|
|
|
33820
|
-
# Case \u53EF\u89C1\u7684\u9879\u76EE\u53D8\u91CF\u767D\u540D\u5355(\u503C\u6765\u81EA .env / CI Secret;Case \u7528 \${variable.<name>} \u5F15\u7528)
|
|
34590
|
+
# Case \u53EF\u89C1\u7684\u9879\u76EE\u53D8\u91CF\u767D\u540D\u5355(\u503C\u6765\u81EA .env.testing / CI Secret;Case \u7528 \${variable.<name>} \u5F15\u7528)
|
|
33821
34591
|
# variables:
|
|
33822
34592
|
# ppmProjectId: \${PPM_PROJECT_ID}
|
|
33823
34593
|
|
|
@@ -33834,19 +34604,20 @@ ${adapters}
|
|
|
33834
34604
|
# server:
|
|
33835
34605
|
# baseUrl: http://127.0.0.1:3000
|
|
33836
34606
|
|
|
33837
|
-
# Web \u7AEF:navigate \u76F8\u5BF9 url \u7684\u57FA\u51C6\u5730\u5740(\u6BCF\u4EBA\u7684\u5730\u5740\u4E0D\u540C,\u503C\u653E .env)
|
|
34607
|
+
# Web \u7AEF:navigate \u76F8\u5BF9 url \u7684\u57FA\u51C6\u5730\u5740(\u6BCF\u4EBA\u7684\u5730\u5740\u4E0D\u540C,\u503C\u653E .env.testing)
|
|
33838
34608
|
# web:
|
|
33839
34609
|
# baseUrl: \${WEB_BASE_URL}
|
|
33840
34610
|
|
|
33841
|
-
# \u5C0F\u7A0B\u5E8F\u7AEF:\u586B\u5165\u5C0F\u7A0B\u5E8F\u9879\u76EE\u76EE\u5F55\u540E\u5373\u53EF\u6267\u884C target: miniapp \u7684\u6B65\u9AA4(\u8DEF\u5F84\u6BCF\u4EBA\u4E0D\u540C,\u8D70 .env)
|
|
34611
|
+
# \u5C0F\u7A0B\u5E8F\u7AEF:\u586B\u5165\u5C0F\u7A0B\u5E8F\u9879\u76EE\u76EE\u5F55\u540E\u5373\u53EF\u6267\u884C target: miniapp \u7684\u6B65\u9AA4(\u8DEF\u5F84\u6BCF\u4EBA\u4E0D\u540C,\u8D70 .env.testing)
|
|
33842
34612
|
# miniapp:
|
|
33843
34613
|
# projectPath: \${MINIAPP_PROJECT_PATH}
|
|
33844
34614
|
# cliPath: \${MINIAPP_CLI_PATH}
|
|
33845
34615
|
`;
|
|
33846
34616
|
}
|
|
33847
|
-
function
|
|
33848
|
-
return `# TestPilot \u73AF\u5883\u53D8\u91CF\
|
|
33849
|
-
# \
|
|
34617
|
+
function renderDotEnvTesting() {
|
|
34618
|
+
return `# TestPilot \u6D4B\u8BD5\u73AF\u5883\u53D8\u91CF(CLI \u81EA\u52A8\u52A0\u8F7D\u672C\u6587\u4EF6,\u76F4\u63A5\u586B\u5165\u771F\u5B9E\u503C\u5373\u53EF,\u65E0\u9700\u590D\u5236)
|
|
34619
|
+
# \u53EA\u653E\u6D4B\u8BD5\u76F8\u5173\u53D8\u91CF,\u4E0E\u524D\u7AEF\u9879\u76EE\u81EA\u8EAB\u7684 .env \u4E92\u4E0D\u5F71\u54CD;\u672C\u6587\u4EF6\u542B\u6D4B\u8BD5\u51ED\u636E,\u5DF2\u52A0\u5165 .gitignore
|
|
34620
|
+
# \u4F18\u5148\u7EA7:\u771F\u5B9E\u73AF\u5883\u53D8\u91CF > \u672C\u6587\u4EF6;testpilot.yaml \u4E2D\u7684 \${VAR} \u5F15\u7528\u8FD0\u884C\u65F6\u81EA\u52A8\u6CE8\u5165
|
|
33850
34621
|
|
|
33851
34622
|
# \u73AF\u5883\u914D\u7F6E(environment \u6BB5)
|
|
33852
34623
|
TEST_BASE_URL=http://127.0.0.1:8080
|
|
@@ -33866,7 +34637,7 @@ STAGING_BASE_URL=https://staging.example.com
|
|
|
33866
34637
|
async function ensureGitignoreEntry(root, entry, comment = "TestPilot \u672C\u5730\u73AF\u5883\u53D8\u91CF") {
|
|
33867
34638
|
const gitignorePath = join25(root, ".gitignore");
|
|
33868
34639
|
if (!await exists4(gitignorePath)) {
|
|
33869
|
-
await
|
|
34640
|
+
await writeFile12(gitignorePath, `# ${comment}
|
|
33870
34641
|
${entry}
|
|
33871
34642
|
`, "utf8");
|
|
33872
34643
|
return "created";
|
|
@@ -33876,7 +34647,7 @@ ${entry}
|
|
|
33876
34647
|
return "exists";
|
|
33877
34648
|
}
|
|
33878
34649
|
const separator = content.endsWith("\n") || content === "" ? "" : "\n";
|
|
33879
|
-
await
|
|
34650
|
+
await writeFile12(gitignorePath, `${content}${separator}# ${comment}
|
|
33880
34651
|
${entry}
|
|
33881
34652
|
`, "utf8");
|
|
33882
34653
|
return "updated";
|
|
@@ -33885,21 +34656,22 @@ async function initTestPilotConfig(root, info, adapters) {
|
|
|
33885
34656
|
const yamlPath = join25(root, "testpilot.yaml");
|
|
33886
34657
|
let yaml = "reused";
|
|
33887
34658
|
if (!await exists4(yamlPath)) {
|
|
33888
|
-
await
|
|
34659
|
+
await writeFile12(yamlPath, renderTestpilotYaml(info), "utf8");
|
|
33889
34660
|
yaml = "created";
|
|
33890
34661
|
}
|
|
33891
|
-
const
|
|
33892
|
-
let
|
|
33893
|
-
if (!await exists4(
|
|
33894
|
-
await
|
|
33895
|
-
|
|
34662
|
+
const envTestingPath = join25(root, ENV_TESTING_FILE);
|
|
34663
|
+
let envTesting = "reused";
|
|
34664
|
+
if (!await exists4(envTestingPath)) {
|
|
34665
|
+
await writeFile12(envTestingPath, renderDotEnvTesting(), "utf8");
|
|
34666
|
+
envTesting = "created";
|
|
33896
34667
|
}
|
|
33897
|
-
const gitignore = await ensureGitignoreEntry(root,
|
|
34668
|
+
const gitignore = await ensureGitignoreEntry(root, ENV_TESTING_FILE);
|
|
34669
|
+
await ensureGitignoreEntry(root, ".env");
|
|
33898
34670
|
const runtimeDir = join25(root, ".testpilot", "artifacts");
|
|
33899
34671
|
await mkdir13(runtimeDir, { recursive: true });
|
|
33900
34672
|
const keep = join25(runtimeDir, ".gitkeep");
|
|
33901
34673
|
if (!await exists4(keep)) {
|
|
33902
|
-
await
|
|
34674
|
+
await writeFile12(keep, "", "utf8");
|
|
33903
34675
|
}
|
|
33904
34676
|
const existing = await readProjectLink(root);
|
|
33905
34677
|
const defaultAdapter = defaultAdapterFor(info.type);
|
|
@@ -33923,7 +34695,7 @@ async function initTestPilotConfig(root, info, adapters) {
|
|
|
33923
34695
|
runtimeDir: join25(root, ".testpilot"),
|
|
33924
34696
|
projectLink: link,
|
|
33925
34697
|
projectLinkFile,
|
|
33926
|
-
|
|
34698
|
+
envTesting,
|
|
33927
34699
|
gitignore
|
|
33928
34700
|
};
|
|
33929
34701
|
}
|
|
@@ -33955,8 +34727,8 @@ function randomId() {
|
|
|
33955
34727
|
}
|
|
33956
34728
|
|
|
33957
34729
|
// src/commands/init/test-structure.ts
|
|
33958
|
-
import { existsSync as existsSync9, readdirSync as
|
|
33959
|
-
import { mkdir as mkdir14, writeFile as
|
|
34730
|
+
import { existsSync as existsSync9, readdirSync as readdirSync3 } from "node:fs";
|
|
34731
|
+
import { mkdir as mkdir14, writeFile as writeFile13 } from "node:fs/promises";
|
|
33960
34732
|
import { join as join26 } from "node:path";
|
|
33961
34733
|
async function initTestStructure(root, _info) {
|
|
33962
34734
|
const dirs = {
|
|
@@ -33975,8 +34747,8 @@ async function initTestStructure(root, _info) {
|
|
|
33975
34747
|
await mkdir14(dir, { recursive: true });
|
|
33976
34748
|
}
|
|
33977
34749
|
for (const [name, dir] of Object.entries(dirs)) {
|
|
33978
|
-
if (!existed[name] &&
|
|
33979
|
-
await
|
|
34750
|
+
if (!existed[name] && readdirSync3(dir).length === 0) {
|
|
34751
|
+
await writeFile13(join26(dir, ".gitkeep"), "", "utf8").catch(() => void 0);
|
|
33980
34752
|
}
|
|
33981
34753
|
}
|
|
33982
34754
|
return {
|
|
@@ -34015,10 +34787,10 @@ async function runInit(root, options = {}) {
|
|
|
34015
34787
|
const serverMode = config2.projectLink.serverUrl ? `Server:${config2.projectLink.serverUrl}` : "\u672C\u5730\u6A21\u5F0F";
|
|
34016
34788
|
console.log(`\u2713 \u9879\u76EE\u5173\u8054 ${config2.projectLink.projectId}(${serverMode})`);
|
|
34017
34789
|
console.log(
|
|
34018
|
-
config2.
|
|
34790
|
+
config2.envTesting === "created" ? "\u2713 \u6D4B\u8BD5\u73AF\u5883\u53D8\u91CF .env.testing(\u76F4\u63A5\u586B\u5165\u771F\u5B9E\u503C,CLI \u81EA\u52A8\u52A0\u8F7D)" : "\u2713 \u6D4B\u8BD5\u73AF\u5883\u53D8\u91CF .env.testing(\u5DF2\u5B58\u5728,\u4FDD\u7559)"
|
|
34019
34791
|
);
|
|
34020
34792
|
console.log(
|
|
34021
|
-
config2.gitignore === "exists" ? "\u2713 Git \u5FFD\u7565 .env(\u5DF2\u5728 .gitignore)" : `\u2713 Git \u5FFD\u7565 .env(\u5DF2\u5199\u5165 .gitignore)`
|
|
34793
|
+
config2.gitignore === "exists" ? "\u2713 Git \u5FFD\u7565 .env.testing(\u5DF2\u5728 .gitignore)" : `\u2713 Git \u5FFD\u7565 .env.testing(\u5DF2\u5199\u5165 .gitignore)`
|
|
34022
34794
|
);
|
|
34023
34795
|
const testStructure = await initTestStructure(root, project);
|
|
34024
34796
|
const reuseNote = project.existingE2eDirs.filter((dir) => dir !== "tests/e2e");
|
|
@@ -34110,7 +34882,7 @@ function makeListCommand() {
|
|
|
34110
34882
|
}
|
|
34111
34883
|
|
|
34112
34884
|
// src/commands/report.ts
|
|
34113
|
-
import { exec } from "node:child_process";
|
|
34885
|
+
import { exec as exec2 } from "node:child_process";
|
|
34114
34886
|
function makeReportCommand() {
|
|
34115
34887
|
return new Command("report").description("\u751F\u6210\u6D4B\u8BD5\u62A5\u544A(JSON + HTML)").option("--run <id>", "\u8FD0\u884C\u8BB0\u5F55 ID,\u9ED8\u8BA4\u6700\u8FD1\u4E00\u6B21").option("--open", "\u751F\u6210\u540E\u5728\u6D4F\u89C8\u5668\u6253\u5F00", false).option("--summary", "\u751F\u6210\u8DE8 run \u6C47\u603B\u62A5\u544A(summary.html + summary.json)", false).option("--last <n>", "\u6C47\u603B\u7EDF\u8BA1\u7A97\u53E3:\u6700\u8FD1 N \u6B21\u8FD0\u884C", "30").action(async (options) => {
|
|
34116
34888
|
const client = new TestPilotClient();
|
|
@@ -34139,7 +34911,7 @@ function makeReportCommand() {
|
|
|
34139
34911
|
function openInBrowser(file2) {
|
|
34140
34912
|
const command = process.platform === "win32" ? `start "" "${file2}"` : process.platform === "darwin" ? `open "${file2}"` : `xdg-open "${file2}"`;
|
|
34141
34913
|
return new Promise((resolvePromise) => {
|
|
34142
|
-
|
|
34914
|
+
exec2(command, () => resolvePromise());
|
|
34143
34915
|
});
|
|
34144
34916
|
}
|
|
34145
34917
|
|
|
@@ -34284,10 +35056,13 @@ function makeSyncMetadataCommand() {
|
|
|
34284
35056
|
}
|
|
34285
35057
|
|
|
34286
35058
|
// src/commands/update.ts
|
|
34287
|
-
import { writeFile as
|
|
35059
|
+
import { readFile as readFile11, writeFile as writeFile14 } from "node:fs/promises";
|
|
34288
35060
|
import { join as join29 } from "node:path";
|
|
35061
|
+
function varNames(content) {
|
|
35062
|
+
return new Set([...content.matchAll(/^#?\s*([A-Z][A-Z0-9_]+)=/gm)].map((match) => match[1]));
|
|
35063
|
+
}
|
|
34289
35064
|
function makeUpdateCommand() {
|
|
34290
|
-
const update = new Command("update").description("\u540C\u6B65 TestPilot \u63A5\u5165\u7269\u5230\u5F53\u524D CLI \u7248\u672C(Skill / .env.
|
|
35065
|
+
const update = new Command("update").description("\u540C\u6B65 TestPilot \u63A5\u5165\u7269\u5230\u5F53\u524D CLI \u7248\u672C(Skill / .env.testing;\u4E0D\u89E6\u78B0 .env \u4E0E\u7528\u4F8B)").option("--force", "Skill \u7248\u672C\u4E00\u81F4\u65F6\u4E5F\u5F3A\u5236\u8986\u76D6\u672C\u5730\u4FEE\u6539", false).action(async (options) => {
|
|
34291
35066
|
const root = process.cwd();
|
|
34292
35067
|
const dest = join29(".agents", "skills", "testpilot");
|
|
34293
35068
|
const skill = await syncSkillFiles(root, { force: options.force });
|
|
@@ -34303,13 +35078,25 @@ function makeUpdateCommand() {
|
|
|
34303
35078
|
default:
|
|
34304
35079
|
console.log(`\u2713 Skill \u5DF2\u662F\u6700\u65B0 v${skill.bundledVersion ?? "?"}`);
|
|
34305
35080
|
}
|
|
34306
|
-
|
|
34307
|
-
|
|
34308
|
-
const
|
|
35081
|
+
const envPath = join29(root, ENV_TESTING_FILE);
|
|
35082
|
+
const template = renderDotEnvTesting();
|
|
35083
|
+
const existing = await readFile11(envPath, "utf8").catch(() => void 0);
|
|
35084
|
+
if (existing === void 0) {
|
|
35085
|
+
await writeFile14(envPath, template, "utf8");
|
|
35086
|
+
console.log(`\u2713 ${ENV_TESTING_FILE} \u5DF2\u751F\u6210(\u76F4\u63A5\u586B\u5165\u771F\u5B9E\u503C,CLI \u81EA\u52A8\u52A0\u8F7D)`);
|
|
35087
|
+
} else {
|
|
35088
|
+
console.log(`\u2713 ${ENV_TESTING_FILE} \u5DF2\u5B58\u5728,\u4FDD\u7559\u7528\u6237\u5185\u5BB9`);
|
|
35089
|
+
const missing = [...varNames(template)].filter((name) => !varNames(existing).has(name));
|
|
35090
|
+
if (missing.length > 0) {
|
|
35091
|
+
console.log(` \u63D0\u793A:\u5F53\u524D\u7248\u672C\u6A21\u677F\u65B0\u589E\u53D8\u91CF,\u53EF\u6309\u9700\u8865\u5165:${missing.join(", ")}`);
|
|
35092
|
+
}
|
|
35093
|
+
}
|
|
35094
|
+
const gitignore = await ensureGitignoreEntry(root, ENV_TESTING_FILE);
|
|
35095
|
+
await ensureGitignoreEntry(root, ".env");
|
|
34309
35096
|
console.log(
|
|
34310
|
-
gitignore === "exists" ?
|
|
35097
|
+
gitignore === "exists" ? `\u2713 .gitignore \u5DF2\u5FFD\u7565 ${ENV_TESTING_FILE}` : `\u2713 .gitignore \u5DF2\u8FFD\u52A0\u5FFD\u7565 ${ENV_TESTING_FILE}`
|
|
34311
35098
|
);
|
|
34312
|
-
console.log("\u63D0\u793A:\u63D0\u4EA4 .agents/skills/ \
|
|
35099
|
+
console.log("\u63D0\u793A:\u63D0\u4EA4 .agents/skills/ \u7684\u53D8\u66F4,\u56E2\u961F\u4E0E AI \u5373\u53EF\u4F7F\u7528\u6700\u65B0\u80FD\u529B");
|
|
34313
35100
|
});
|
|
34314
35101
|
return update;
|
|
34315
35102
|
}
|
|
@@ -34350,7 +35137,7 @@ function makeValidateCommand() {
|
|
|
34350
35137
|
}
|
|
34351
35138
|
|
|
34352
35139
|
// src/cli.ts
|
|
34353
|
-
var CLI_VERSION = "0.7.
|
|
35140
|
+
var CLI_VERSION = "0.7.2";
|
|
34354
35141
|
async function createCli() {
|
|
34355
35142
|
const program2 = new Command();
|
|
34356
35143
|
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", () => {
|