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