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/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, ".env"));
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(["click", "input", "select", "assert", "extract"]);
30494
- var VALUE_REQUIRED = /* @__PURE__ */ new Set(["input", "select"]);
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) && !step.value) {
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 exec2 of executions) {
31467
- let agg = byCase.get(exec2.identity);
31670
+ for (const exec3 of executions) {
31671
+ let agg = byCase.get(exec3.identity);
31468
31672
  if (!agg) {
31469
31673
  agg = {
31470
- caseId: exec2.caseId,
31471
- rowId: exec2.rowId,
31472
- caseName: exec2.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(exec2.identity, agg);
31685
+ byCase.set(exec3.identity, agg);
31482
31686
  }
31483
31687
  agg.runs++;
31484
- if (exec2.status === "passed") agg.passed++;
31485
- else if (exec2.status === "failed") agg.failed++;
31486
- for (const platform of exec2.platforms) {
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((exec2) => exec2.identity === identity);
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((exec2) => exec2.status === "failed" && exec2.category === item.id).length
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((exec2) => exec2.status === "blocked").length,
31509
- inconclusive: executions.filter((exec2) => exec2.status === "inconclusive").length
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((exec2) => exec2.status === "passed").length / executions.length : 0,
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, context.resolve(step.value ?? ""));
32262
+ await adapter.input(locator, resolveValueText(step.value, context));
32023
32263
  break;
32024
32264
  case "select":
32025
- await adapter.select(locator, context.resolve(step.value ?? ""));
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, context.resolve(step.value ?? "")) : await adapter.extract(locator);
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 writeFile9 } from "node:fs/promises";
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 writeFile9(join15(runDir, RESULT_FILE), JSON.stringify(summary, null, 2), "utf8");
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
- if (!actual.includes(wanted)) {
32643
- throw new TestPilotError("ASSERTION_FAILED", `\u65AD\u8A00\u5931\u8D25:\u671F\u671B\u5305\u542B "${wanted}",\u5B9E\u9645\u4E3A "${actual.slice(0, 200)}"`);
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
- return this.coded(() => target.innerText()).then((text) => text.trim());
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 delay2(ms) {
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 delay2(1e3);
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 delay2(3e3);
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
- if (!actual.includes(wanted)) {
32839
- throw new TestPilotError("ASSERTION_FAILED", `\u65AD\u8A00\u5931\u8D25:\u671F\u671B\u5305\u542B "${wanted}",\u5B9E\u9645\u4E3A "${actual.slice(0, 200)}"`);
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
- return (await element.text()).trim();
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 delay2(500);
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(readFileSync(file2, "utf8"));
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 readFileSync2 } from "node:fs";
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(readFileSync2(file2, "utf8"));
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" : "Unknown";
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 readFileSync3 } from "node:fs";
33507
- import { access as access2, cp, mkdir as mkdir12, writeFile as writeFile10 } from "node:fs/promises";
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(readFileSync3(manifestPath, "utf8"))?.[1];
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 writeFile10(file2, render(), "utf8");
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\u8FD0\u884C\u73AF\u5883(.env / CI Secret)\u4E2D\u8BBE\u7F6E")
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 writeFile11 } from "node:fs/promises";
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 renderDotEnvExample() {
33848
- return `# TestPilot \u73AF\u5883\u53D8\u91CF\u793A\u4F8B:\u590D\u5236\u4E3A .env \u5E76\u586B\u5165\u771F\u5B9E\u503C(.env \u542B\u654F\u611F\u4FE1\u606F,\u4E0D\u8981\u63D0\u4EA4\u5230 Git)
33849
- # \u4F18\u5148\u7EA7:\u771F\u5B9E\u73AF\u5883\u53D8\u91CF > .env \u6587\u4EF6;testpilot.yaml \u4E2D\u7684 \${VAR} \u5F15\u7528\u8FD0\u884C\u65F6\u81EA\u52A8\u6CE8\u5165
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 writeFile11(gitignorePath, `# ${comment}
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 writeFile11(gitignorePath, `${content}${separator}# ${comment}
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 writeFile11(yamlPath, renderTestpilotYaml(info), "utf8");
34659
+ await writeFile12(yamlPath, renderTestpilotYaml(info), "utf8");
33889
34660
  yaml = "created";
33890
34661
  }
33891
- const envExamplePath = join25(root, ".env.example");
33892
- let envExample = "reused";
33893
- if (!await exists4(envExamplePath)) {
33894
- await writeFile11(envExamplePath, renderDotEnvExample(), "utf8");
33895
- envExample = "created";
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, ".env");
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 writeFile11(keep, "", "utf8");
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
- envExample,
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 readdirSync2 } from "node:fs";
33959
- import { mkdir as mkdir14, writeFile as writeFile12 } from "node:fs/promises";
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] && readdirSync2(dir).length === 0) {
33979
- await writeFile12(join26(dir, ".gitkeep"), "", "utf8").catch(() => void 0);
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.envExample === "created" ? "\u2713 \u73AF\u5883\u53D8\u91CF\u793A\u4F8B .env.example(\u590D\u5236\u4E3A .env \u586B\u5165\u771F\u5B9E\u503C)" : "\u2713 \u73AF\u5883\u53D8\u91CF\u793A\u4F8B .env.example(\u5DF2\u5B58\u5728,\u4FDD\u7559)"
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
- exec(command, () => resolvePromise());
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 writeFile13 } from "node:fs/promises";
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.example;\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) => {
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
- await writeFile13(join29(root, ".env.example"), renderDotEnvExample(), "utf8");
34307
- console.log("\u2713 .env.example \u5DF2\u540C\u6B65\u4E3A\u5F53\u524D\u7248\u672C\u7684\u793A\u4F8B(.env \u4E0D\u53D7\u5F71\u54CD)");
34308
- const gitignore = await ensureGitignoreEntry(root, ".env");
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" ? "\u2713 .gitignore \u5DF2\u5FFD\u7565 .env" : "\u2713 .gitignore \u5DF2\u8FFD\u52A0\u5FFD\u7565 .env"
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/ \u4E0E .env.example \u7684\u53D8\u66F4,\u56E2\u961F\u4E0E AI \u5373\u53EF\u4F7F\u7528\u6700\u65B0\u80FD\u529B");
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.0";
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", () => {