csspilot 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/bin.js +301 -23
  2. package/dist/index.js +301 -23
  3. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -30388,7 +30388,11 @@ var ACTIONS = [
30388
30388
  "assert",
30389
30389
  "extract",
30390
30390
  "screenshot",
30391
- "request"
30391
+ "request",
30392
+ "callMethod",
30393
+ "mockWxApi",
30394
+ "trigger",
30395
+ "setData"
30392
30396
  ];
30393
30397
  var locatorSchema = external_exports.strictObject({
30394
30398
  text: external_exports.string().min(1).optional(),
@@ -30416,7 +30420,25 @@ var stepSchema = external_exports.strictObject({
30416
30420
  variable: external_exports.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/, "\u53D8\u91CF\u540D\u9700\u4EE5\u5B57\u6BCD\u6216\u4E0B\u5212\u7EBF\u5F00\u5934,\u4EC5\u542B\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u4E0B\u5212\u7EBF").optional(),
30417
30421
  timeout: external_exports.number().int().positive().optional(),
30418
30422
  /** UI 动作触发的网络请求精确次数断言 */
30419
- expectRequests: external_exports.array(requestExpectationSchema).min(1).optional()
30423
+ expectRequests: external_exports.array(requestExpectationSchema).min(1).optional(),
30424
+ /** callMethod:要调用的 Page / 组件方法名(仅 miniapp;页面级调用不写 locator) */
30425
+ methodName: external_exports.string().min(1).optional(),
30426
+ /** callMethod 的入参(支持 ${var} 模板) */
30427
+ args: external_exports.array(external_exports.unknown()).optional(),
30428
+ /** mockWxApi:要 mock 的 wx.* API 名(如 showModal / request) */
30429
+ wxApi: external_exports.string().min(1).optional(),
30430
+ /** mockWxApi:mock 返回值(对象按原样注入;capture 时真实调用先上报再返回 mock 值) */
30431
+ result: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
30432
+ /** mockWxApi:捕获真实调用参数的变量名(${<capture>.options} / ${<capture>.called}) */
30433
+ capture: external_exports.string().min(1).optional(),
30434
+ /** trigger:触发的事件类型(如 getphonenumber;空 detail 即"用户拒绝/取消"类分支) */
30435
+ eventType: external_exports.string().min(1).optional(),
30436
+ /** trigger:作为事件 e.detail 注入(值支持 ${var} 模板) */
30437
+ detail: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
30438
+ /** setData:写入页面 data 的补丁(值支持 ${var} 模板) */
30439
+ patch: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
30440
+ /** assert:页面 data 路径断言(与 locator 互斥,仅 miniapp) */
30441
+ dataPath: external_exports.string().min(1).optional()
30420
30442
  });
30421
30443
  var datasetReferenceSchema = external_exports.strictObject({
30422
30444
  file: external_exports.string().min(1),
@@ -30489,7 +30511,11 @@ var ADAPTER_SUPPORT = {
30489
30511
  assert: ["web", "miniapp", "api"],
30490
30512
  extract: ["web", "miniapp", "api"],
30491
30513
  screenshot: ["web", "miniapp"],
30492
- request: ["api"]
30514
+ request: ["api"],
30515
+ callMethod: ["miniapp"],
30516
+ mockWxApi: ["miniapp"],
30517
+ trigger: ["miniapp"],
30518
+ setData: ["miniapp"]
30493
30519
  };
30494
30520
  var LOCATOR_REQUIRED = /* @__PURE__ */ new Set(["click", "input", "select", "assert", "extract"]);
30495
30521
  var VALUE_REQUIRED = /* @__PURE__ */ new Set(["input", "select"]);
@@ -30502,7 +30528,16 @@ var FIELD_ALLOWED = {
30502
30528
  timeout: ["wait"],
30503
30529
  method: ["request"],
30504
30530
  headers: ["request"],
30505
- body: ["request"]
30531
+ body: ["request"],
30532
+ methodName: ["callMethod"],
30533
+ args: ["callMethod"],
30534
+ wxApi: ["mockWxApi"],
30535
+ result: ["mockWxApi"],
30536
+ capture: ["mockWxApi"],
30537
+ eventType: ["trigger"],
30538
+ detail: ["trigger"],
30539
+ patch: ["setData"],
30540
+ dataPath: ["assert"]
30506
30541
  };
30507
30542
  var VARIABLE_REF = /\$\{([A-Za-z_][A-Za-z0-9_.]*)\}/g;
30508
30543
  function validateCase(data) {
@@ -30566,6 +30601,23 @@ function checkStep(step, at, declared, issues) {
30566
30601
  });
30567
30602
  }
30568
30603
  }
30604
+ if (step.action === "mockWxApi") {
30605
+ if (!step.wxApi) {
30606
+ issues.push({ path: `${at}.wxApi`, code: "semantic", message: "mockWxApi \u5FC5\u987B\u63D0\u4F9B wxApi(\u8981 mock \u7684 wx.* API \u540D)" });
30607
+ }
30608
+ if (!step.result) {
30609
+ issues.push({ path: `${at}.result`, code: "semantic", message: "mockWxApi \u5FC5\u987B\u63D0\u4F9B result(mock \u8FD4\u56DE\u503C)" });
30610
+ }
30611
+ }
30612
+ if (step.action === "trigger" && !step.eventType) {
30613
+ issues.push({ path: `${at}.eventType`, code: "semantic", message: "trigger \u5FC5\u987B\u63D0\u4F9B eventType(\u89E6\u53D1\u7684\u4E8B\u4EF6\u7C7B\u578B)" });
30614
+ }
30615
+ if (step.action === "setData" && (!step.patch || Object.keys(step.patch).length === 0)) {
30616
+ issues.push({ path: `${at}.patch`, code: "semantic", message: "setData \u5FC5\u987B\u63D0\u4F9B patch(\u5199\u5165\u9875\u9762 data \u7684\u8865\u4E01)" });
30617
+ }
30618
+ if (step.dataPath !== void 0 && step.target !== "miniapp") {
30619
+ issues.push({ path: `${at}.dataPath`, code: "semantic", message: "dataPath \u65AD\u8A00\u4EC5 miniapp \u652F\u6301(web \u7AEF\u7528 locator)" });
30620
+ }
30569
30621
  if (step.action === "navigate" && !step.url) {
30570
30622
  issues.push({ path: `${at}.url`, code: "semantic", message: "navigate \u5FC5\u987B\u63D0\u4F9B url" });
30571
30623
  }
@@ -30597,6 +30649,13 @@ function checkStep(step, at, declared, issues) {
30597
30649
  if (step.action === "wait" && !step.locator && step.timeout === void 0) {
30598
30650
  issues.push({ path: at, code: "semantic", message: "wait \u9700\u8981\u63D0\u4F9B locator(\u7B49\u5F85\u5143\u7D20)\u6216 timeout(\u6BEB\u79D2)" });
30599
30651
  }
30652
+ if (step.action === "callMethod" && !step.methodName) {
30653
+ issues.push({
30654
+ path: `${at}.methodName`,
30655
+ code: "semantic",
30656
+ message: "callMethod \u5FC5\u987B\u63D0\u4F9B methodName(\u9875\u9762\u6216\u7EC4\u4EF6\u7684\u65B9\u6CD5\u540D)"
30657
+ });
30658
+ }
30600
30659
  if (step.expectRequests) {
30601
30660
  if (step.target === "api" || !REQUEST_EXPECTATION_ACTIONS.has(step.action)) {
30602
30661
  issues.push({
@@ -30638,11 +30697,17 @@ function checkVariableRefs(step, at, declared, issues) {
30638
30697
  collect("value", step.value);
30639
30698
  collect("expected", step.expected);
30640
30699
  collect("url", step.url);
30700
+ collect("methodName", step.methodName);
30641
30701
  collect("locator.text", step.locator?.text);
30642
30702
  collect("locator.css", step.locator?.css);
30643
30703
  if (typeof step.body === "string") collect("body", step.body);
30644
30704
  else if (step.body) collect("body", JSON.stringify(step.body));
30645
30705
  for (const headerValue of Object.values(step.headers ?? {})) collect("headers", headerValue);
30706
+ if (Array.isArray(step.args)) {
30707
+ step.args.forEach((arg, index) => {
30708
+ if (typeof arg === "string") collect(`args[${index}]`, arg);
30709
+ });
30710
+ }
30646
30711
  for (const [index, expectation] of (step.expectRequests ?? []).entries()) {
30647
30712
  collect(`expectRequests[${index}].urlContains`, expectation.urlContains);
30648
30713
  }
@@ -31870,6 +31935,26 @@ var ExecutionContext = class {
31870
31935
  }
31871
31936
  this.variables.set("account", rawValue);
31872
31937
  }
31938
+ /**
31939
+ * 注入结构化对象(如 mockWxApi 的捕获结果):按点号路径展开为平铺字符串变量。
31940
+ * 例如 seedObject('showModalCall', { called: true, args: { content: 'x' } }) 后,
31941
+ * `${showModalCall.called}` 与 `${showModalCall.args.content}` 均可解析。
31942
+ */
31943
+ seedObject(name, value) {
31944
+ const flatten = (prefix, input2) => {
31945
+ if (input2 !== null && typeof input2 === "object" && !Array.isArray(input2)) {
31946
+ const entries = Object.entries(input2);
31947
+ if (entries.length === 0) {
31948
+ this.variables.set(prefix, "");
31949
+ return;
31950
+ }
31951
+ for (const [key, item] of entries) flatten(`${prefix}.${key}`, item);
31952
+ return;
31953
+ }
31954
+ this.variables.set(prefix, input2 === void 0 ? "" : String(input2));
31955
+ };
31956
+ flatten(name, value);
31957
+ }
31873
31958
  };
31874
31959
 
31875
31960
  // ../../packages/execution-engine/src/lifecycle/index.ts
@@ -32028,7 +32113,14 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
32028
32113
  case "wait":
32029
32114
  await adapter.wait(locator, step.timeout);
32030
32115
  break;
32031
- case "assert":
32116
+ case "assert": {
32117
+ if (step.dataPath !== void 0) {
32118
+ if (!adapter.assertData) {
32119
+ throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 dataPath \u65AD\u8A00`);
32120
+ }
32121
+ await adapter.assertData(context.resolve(step.dataPath), context.resolve(step.expected ?? ""));
32122
+ break;
32123
+ }
32032
32124
  if (step.target === "api") {
32033
32125
  if (!adapter.assertResponse) throw new TestPilotError("UNSUPPORTED_ACTION", "\u5F53\u524D adapter \u4E0D\u652F\u6301 api \u65AD\u8A00");
32034
32126
  await adapter.assertResponse(context.resolve(step.expected ?? ""));
@@ -32036,6 +32128,7 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
32036
32128
  await adapter.assert(locator, context.resolve(step.expected ?? ""));
32037
32129
  }
32038
32130
  break;
32131
+ }
32039
32132
  case "extract": {
32040
32133
  const value = step.target === "api" ? await extractApiResponse(adapter, context.resolve(step.value ?? "")) : await adapter.extract(locator);
32041
32134
  if (step.variable) {
@@ -32075,6 +32168,42 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
32075
32168
  }
32076
32169
  break;
32077
32170
  }
32171
+ case "callMethod": {
32172
+ if (typeof adapter.callMethod !== "function") {
32173
+ throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 callMethod`);
32174
+ }
32175
+ const methodName = context.resolve(step.methodName ?? "");
32176
+ if (!methodName) throw new TestPilotError("MISSING_METHOD_NAME", "callMethod \u5FC5\u987B\u63D0\u4F9B methodName");
32177
+ const callArgs = (step.args ?? []).map((arg) => resolveDeep(arg, context));
32178
+ await adapter.callMethod(locator, methodName, callArgs);
32179
+ break;
32180
+ }
32181
+ case "mockWxApi": {
32182
+ if (typeof adapter.mockWxApi !== "function") {
32183
+ throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 mockWxApi`);
32184
+ }
32185
+ await adapter.mockWxApi({
32186
+ api: context.resolve(step.wxApi ?? ""),
32187
+ result: step.result ? resolveDeep(step.result, context) : void 0,
32188
+ capture: step.capture
32189
+ });
32190
+ break;
32191
+ }
32192
+ case "trigger": {
32193
+ if (typeof adapter.trigger !== "function") {
32194
+ throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 trigger`);
32195
+ }
32196
+ if (!locator) throw new TestPilotError("MISSING_LOCATOR", "trigger \u5FC5\u987B\u63D0\u4F9B locator(\u8981\u89E6\u53D1\u4E8B\u4EF6\u7684\u7EC4\u4EF6)");
32197
+ await adapter.trigger(locator, context.resolve(step.eventType ?? ""), step.detail ? resolveDeep(step.detail, context) : void 0);
32198
+ break;
32199
+ }
32200
+ case "setData": {
32201
+ if (typeof adapter.setData !== "function") {
32202
+ throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 setData`);
32203
+ }
32204
+ await adapter.setData(resolveDeep(step.patch ?? {}, context));
32205
+ break;
32206
+ }
32078
32207
  default: {
32079
32208
  const exhaustive = step.action;
32080
32209
  throw new TestPilotError("UNSUPPORTED_ACTION", `\u4E0D\u652F\u6301\u7684 action:${String(exhaustive)}`);
@@ -32263,6 +32392,12 @@ var ExecutionEngine = class {
32263
32392
  result = await executeStep(step, index, adapter, context, artifacts, {
32264
32393
  screenshotName: `${executionId}-${phase}-${String(index).padStart(2, "0")}`
32265
32394
  });
32395
+ if (step.capture !== void 0 && typeof adapter.readWxApiCaptures === "function") {
32396
+ const captures = adapter.readWxApiCaptures();
32397
+ if (captures && captures.calls.length > 0) {
32398
+ context.seedObject(step.capture, captures.calls[0]?.options ?? null);
32399
+ }
32400
+ }
32266
32401
  } catch (err) {
32267
32402
  result = {
32268
32403
  index,
@@ -32384,6 +32519,9 @@ var ExecutionEngine = class {
32384
32519
  );
32385
32520
  }
32386
32521
  }
32522
+ for (const [, adapter] of evidenceAdapters) {
32523
+ await adapter.restoreWxMock?.().catch(() => void 0);
32524
+ }
32387
32525
  await bus.emit({
32388
32526
  type: "case-finished",
32389
32527
  runId,
@@ -32547,6 +32685,10 @@ import { mkdtemp, readFile as readFile6, rm } from "node:fs/promises";
32547
32685
  import { tmpdir } from "node:os";
32548
32686
  import { join as join16 } from "node:path";
32549
32687
  import { errors } from "playwright";
32688
+ var TEXT_WAIT_MS = 1e4;
32689
+ function delay2(ms) {
32690
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
32691
+ }
32550
32692
  var PlaywrightAdapter = class {
32551
32693
  constructor(bundle, options = {}) {
32552
32694
  this.bundle = bundle;
@@ -32637,16 +32779,29 @@ var PlaywrightAdapter = class {
32637
32779
  }
32638
32780
  async assert(locator, expected) {
32639
32781
  const target = await this.toLocator(locator);
32640
- await this.coded(() => target.waitFor({ state: "visible" }));
32641
- const actual = (await this.coded(() => target.innerText())).trim();
32642
32782
  const wanted = expected.trim();
32643
- if (!actual.includes(wanted)) {
32644
- throw new TestPilotError("ASSERTION_FAILED", `\u65AD\u8A00\u5931\u8D25:\u671F\u671B\u5305\u542B "${wanted}",\u5B9E\u9645\u4E3A "${actual.slice(0, 200)}"`);
32783
+ const startedAt = Date.now();
32784
+ for (; ; ) {
32785
+ await this.coded(() => target.waitFor({ state: "visible" }));
32786
+ const actual = (await this.coded(() => target.innerText())).trim();
32787
+ if (actual.includes(wanted)) return;
32788
+ if (Date.now() - startedAt > TEXT_WAIT_MS) {
32789
+ throw new TestPilotError(
32790
+ "ASSERTION_FAILED",
32791
+ `\u65AD\u8A00\u5931\u8D25(\u7B49\u5F85 ${TEXT_WAIT_MS}ms):\u671F\u671B\u5305\u542B "${wanted}",\u5B9E\u9645\u4E3A "${actual.slice(0, 200)}"`
32792
+ );
32793
+ }
32794
+ await delay2(300);
32645
32795
  }
32646
32796
  }
32647
32797
  async extract(locator) {
32648
32798
  const target = await this.toLocator(locator);
32649
- return this.coded(() => target.innerText()).then((text) => text.trim());
32799
+ const startedAt = Date.now();
32800
+ for (; ; ) {
32801
+ const text = (await this.coded(() => target.innerText())).trim();
32802
+ if (text.length > 0 || Date.now() - startedAt > TEXT_WAIT_MS) return text;
32803
+ await delay2(300);
32804
+ }
32650
32805
  }
32651
32806
  async screenshot() {
32652
32807
  return (await this.ensurePage()).screenshot();
@@ -32711,6 +32866,8 @@ import net from "node:net";
32711
32866
  var AUTO_PORT_FALLBACK_WAIT_MS = 9e4;
32712
32867
  var NAVIGATE_RETRY_WINDOW_MS = 45e3;
32713
32868
  var ELEMENT_WAIT_MS = 1e4;
32869
+ var TEXT_WAIT_MS2 = 1e4;
32870
+ var PAGE_READY_WAIT_MS = 3e4;
32714
32871
  var CONNECTION_LOST = /connection closed|not connected/i;
32715
32872
  var automatorLauncher = {
32716
32873
  launch: async (options) => {
@@ -32724,7 +32881,7 @@ var automatorLauncher = {
32724
32881
  }
32725
32882
  }
32726
32883
  };
32727
- function delay2(ms) {
32884
+ function delay3(ms) {
32728
32885
  return new Promise((resolve7) => setTimeout(resolve7, ms));
32729
32886
  }
32730
32887
  async function connectWithRetry(wsEndpoint, timeoutMs) {
@@ -32735,7 +32892,7 @@ async function connectWithRetry(wsEndpoint, timeoutMs) {
32735
32892
  return await automator.connect({ wsEndpoint });
32736
32893
  } catch (err) {
32737
32894
  lastError = err;
32738
- await delay2(1e3);
32895
+ await delay3(1e3);
32739
32896
  }
32740
32897
  }
32741
32898
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
@@ -32804,14 +32961,31 @@ var MiniAppAdapter = class {
32804
32961
  await this.runWithReconnect(async () => {
32805
32962
  await (await this.getProgram()).reLaunch(url2);
32806
32963
  });
32964
+ await this.waitForPageReady();
32807
32965
  return;
32808
32966
  } catch (err) {
32809
32967
  lastError = err;
32810
- await delay2(3e3);
32968
+ await delay3(3e3);
32811
32969
  }
32812
32970
  }
32813
32971
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
32814
32972
  }
32973
+ /** 等待页面就绪:模拟器编译期间 currentPage / data 会失败或挂起,轮询至可响应 */
32974
+ async waitForPageReady(timeoutMs = PAGE_READY_WAIT_MS) {
32975
+ const startedAt = Date.now();
32976
+ for (; ; ) {
32977
+ const page = await (await this.getProgram()).currentPage();
32978
+ const data = await page.data().catch(() => void 0);
32979
+ if (data !== void 0 && data !== null) return;
32980
+ if (Date.now() - startedAt > timeoutMs) {
32981
+ throw new TestPilotError(
32982
+ "PAGE_READY_TIMEOUT",
32983
+ `\u7B49\u5F85\u9875\u9762\u5C31\u7EEA\u8D85\u65F6(${timeoutMs}ms):\u6A21\u62DF\u5668\u53EF\u80FD\u4ECD\u5728\u7F16\u8BD1,\u6216\u9879\u76EE\u672A\u88AB\u4FE1\u4EFB`
32984
+ );
32985
+ }
32986
+ await delay3(300);
32987
+ }
32988
+ }
32815
32989
  async click(locator) {
32816
32990
  await this.runWithReconnect(async () => (await this.element(locator)).tap());
32817
32991
  }
@@ -32834,17 +33008,100 @@ var MiniAppAdapter = class {
32834
33008
  async assert(locator, expected) {
32835
33009
  await this.runWithReconnect(async () => {
32836
33010
  const element = await this.element(locator);
32837
- const actual = (await element.text()).trim();
32838
33011
  const wanted = expected.trim();
32839
- if (!actual.includes(wanted)) {
32840
- throw new TestPilotError("ASSERTION_FAILED", `\u65AD\u8A00\u5931\u8D25:\u671F\u671B\u5305\u542B "${wanted}",\u5B9E\u9645\u4E3A "${actual.slice(0, 200)}"`);
33012
+ const startedAt = Date.now();
33013
+ for (; ; ) {
33014
+ const actual = (await element.text()).trim();
33015
+ if (actual.includes(wanted)) return;
33016
+ if (Date.now() - startedAt > TEXT_WAIT_MS2) {
33017
+ throw new TestPilotError(
33018
+ "ASSERTION_FAILED",
33019
+ `\u65AD\u8A00\u5931\u8D25(\u7B49\u5F85 ${TEXT_WAIT_MS2}ms):\u671F\u671B\u5305\u542B "${wanted}",\u5B9E\u9645\u4E3A "${actual.slice(0, 200)}"`
33020
+ );
33021
+ }
33022
+ await delay3(300);
32841
33023
  }
32842
33024
  });
32843
33025
  }
32844
33026
  async extract(locator) {
32845
33027
  return this.runWithReconnect(async () => {
32846
33028
  const element = await this.element(locator);
32847
- return (await element.text()).trim();
33029
+ const startedAt = Date.now();
33030
+ for (; ; ) {
33031
+ const text = (await element.text()).trim();
33032
+ if (text.length > 0 || Date.now() - startedAt > TEXT_WAIT_MS2) return text;
33033
+ await delay3(300);
33034
+ }
33035
+ });
33036
+ }
33037
+ /** 调用 Page / 组件方法;不传 locator 时为页面级调用(Page 实例方法) */
33038
+ async callMethod(locator, methodName, args = []) {
33039
+ return this.runWithReconnect(async () => {
33040
+ if (locator) {
33041
+ const element = await this.element(locator);
33042
+ return element.callMethod(methodName, ...args);
33043
+ }
33044
+ const page = await (await this.getProgram()).currentPage();
33045
+ return page.callMethod(methodName, ...args);
33046
+ });
33047
+ }
33048
+ // ---- miniapp 深度测试(V0.2)----
33049
+ wxMock;
33050
+ /** mock 任意 wx.* API:拦截调用并返回 mock 值;capture 记录真实调用参数(最近调用在前) */
33051
+ async mockWxApi(options) {
33052
+ if (this.wxMock?.api === options.api) await this.restoreWxMock();
33053
+ const program2 = await this.getProgram();
33054
+ const captured = [];
33055
+ this.wxMock = { api: options.api, captured };
33056
+ const callbackName = options.capture ? `__tpWxCapture_${options.capture}` : void 0;
33057
+ if (callbackName) {
33058
+ await program2.exposeFunction(callbackName, (payload) => {
33059
+ if (payload !== null && typeof payload === "object") {
33060
+ captured.push(payload);
33061
+ }
33062
+ });
33063
+ }
33064
+ await program2.mockWxMethod(
33065
+ options.api,
33066
+ function(invokeOptions, cbName) {
33067
+ const callbacks = globalThis;
33068
+ callbacks[cbName]?.({ options: invokeOptions });
33069
+ return options.result ?? this.origin(invokeOptions);
33070
+ },
33071
+ callbackName
33072
+ );
33073
+ }
33074
+ /** 读取 mock 期间捕获的调用参数(最近调用在前) */
33075
+ readCapturedWxApiCalls(api) {
33076
+ return this.wxMock?.api === api ? [...this.wxMock.captured].reverse() : [];
33077
+ }
33078
+ /** 恢复 mock 的 wx API(用例结束由引擎调用) */
33079
+ async restoreWxMock() {
33080
+ const mock = this.wxMock;
33081
+ if (!mock) return;
33082
+ this.wxMock = void 0;
33083
+ if (!this.program) return;
33084
+ await this.program.restoreWxMethod(mock.api).catch(() => void 0);
33085
+ }
33086
+ /** 触发组件事件(trigger;detail 作为 e.detail 注入,空 detail 即"用户拒绝/取消"类分支) */
33087
+ async trigger(locator, eventType, detail) {
33088
+ await this.runWithReconnect(async () => {
33089
+ const element = await this.element(locator);
33090
+ await element.trigger(eventType, detail);
33091
+ });
33092
+ }
33093
+ /** 读取页面 data(点号路径,缺省为整个 data 对象) */
33094
+ async getData(dataPath) {
33095
+ return this.runWithReconnect(async () => {
33096
+ const page = await (await this.getProgram()).currentPage();
33097
+ return page.data(dataPath);
33098
+ });
33099
+ }
33100
+ /** 写入页面 data 补丁(setData 驱动 onShow/onPullDownRefresh 等真实链路) */
33101
+ async setData(patch) {
33102
+ await this.runWithReconnect(async () => {
33103
+ const page = await (await this.getProgram()).currentPage();
33104
+ await page.setData(patch);
32848
33105
  });
32849
33106
  }
32850
33107
  async screenshot() {
@@ -32937,7 +33194,7 @@ var MiniAppAdapter = class {
32937
33194
  if (Date.now() - startedAt > ELEMENT_WAIT_MS) {
32938
33195
  throw new TestPilotError("ELEMENT_NOT_FOUND", `\u7B49\u5F85\u5143\u7D20\u8D85\u65F6(${ELEMENT_WAIT_MS}ms):${selector}`);
32939
33196
  }
32940
- await delay2(500);
33197
+ await delay3(500);
32941
33198
  }
32942
33199
  }
32943
33200
  };
@@ -33417,7 +33674,7 @@ async function collectEnvVarRefs(root) {
33417
33674
  }
33418
33675
 
33419
33676
  // src/commands/init/project-detector.ts
33420
- import { existsSync as existsSync6, readFileSync as readFileSync2 } from "node:fs";
33677
+ import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "node:fs";
33421
33678
  import { basename, join as join22 } from "node:path";
33422
33679
 
33423
33680
  // src/lib/git.ts
@@ -33473,10 +33730,31 @@ function detectProjectType(root) {
33473
33730
  if (isWeb) return "web";
33474
33731
  return "unknown";
33475
33732
  }
33733
+ function detectLanguageFromSources(root) {
33734
+ const SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "miniprogram_npm", "dist", ".git"]);
33735
+ const found = { ts: false, js: false };
33736
+ const walk = (dir, depth) => {
33737
+ if (depth > 4 || found.ts && found.js) return;
33738
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
33739
+ if (entry.name.startsWith(".")) continue;
33740
+ const full = join22(dir, entry.name);
33741
+ if (entry.isDirectory()) {
33742
+ if (!SKIP_DIRS.has(entry.name)) walk(full, depth + 1);
33743
+ continue;
33744
+ }
33745
+ if (/\.(ts|tsx)$/.test(entry.name)) found.ts = true;
33746
+ else if (/\.(js|cjs|mjs|wxml)$/.test(entry.name)) found.js = true;
33747
+ }
33748
+ };
33749
+ walk(root, 0);
33750
+ if (found.ts) return "TypeScript";
33751
+ if (found.js) return "JavaScript";
33752
+ return "Unknown";
33753
+ }
33476
33754
  async function detectProject(root) {
33477
33755
  const pkg = readPackageJson(root);
33478
33756
  const type = detectProjectType(root);
33479
- const language = existsSync6(join22(root, "tsconfig.json")) ? "TypeScript" : pkg || existsSync6(join22(root, "src")) ? "JavaScript" : "Unknown";
33757
+ const language = existsSync6(join22(root, "tsconfig.json")) ? "TypeScript" : pkg || existsSync6(join22(root, "src")) ? "JavaScript" : detectLanguageFromSources(root);
33480
33758
  const packageManager = existsSync6(join22(root, "pnpm-lock.yaml")) ? "pnpm" : existsSync6(join22(root, "yarn.lock")) ? "yarn" : existsSync6(join22(root, "package-lock.json")) ? "npm" : "npx";
33481
33759
  const existingE2eDirs = E2E_DIR_CANDIDATES.filter((dir) => existsSync6(join22(root, dir)));
33482
33760
  return {
@@ -33956,7 +34234,7 @@ function randomId() {
33956
34234
  }
33957
34235
 
33958
34236
  // src/commands/init/test-structure.ts
33959
- import { existsSync as existsSync9, readdirSync as readdirSync2 } from "node:fs";
34237
+ import { existsSync as existsSync9, readdirSync as readdirSync3 } from "node:fs";
33960
34238
  import { mkdir as mkdir14, writeFile as writeFile12 } from "node:fs/promises";
33961
34239
  import { join as join26 } from "node:path";
33962
34240
  async function initTestStructure(root, _info) {
@@ -33976,7 +34254,7 @@ async function initTestStructure(root, _info) {
33976
34254
  await mkdir14(dir, { recursive: true });
33977
34255
  }
33978
34256
  for (const [name, dir] of Object.entries(dirs)) {
33979
- if (!existed[name] && readdirSync2(dir).length === 0) {
34257
+ if (!existed[name] && readdirSync3(dir).length === 0) {
33980
34258
  await writeFile12(join26(dir, ".gitkeep"), "", "utf8").catch(() => void 0);
33981
34259
  }
33982
34260
  }
@@ -34351,7 +34629,7 @@ function makeValidateCommand() {
34351
34629
  }
34352
34630
 
34353
34631
  // src/cli.ts
34354
- var CLI_VERSION = "0.7.0";
34632
+ var CLI_VERSION = "0.7.1";
34355
34633
  async function createCli() {
34356
34634
  const program2 = new Command();
34357
34635
  program2.name("csspilot").description("TestPilot \u2014 AI Native \u8DE8\u7AEF\u4E1A\u52A1\u6D4B\u8BD5\u57FA\u7840\u8BBE\u65BD CLI").version(CLI_VERSION, "-v, --version", "output the current version").option("-V", "output the current version", () => {
package/dist/index.js CHANGED
@@ -30387,7 +30387,11 @@ var ACTIONS = [
30387
30387
  "assert",
30388
30388
  "extract",
30389
30389
  "screenshot",
30390
- "request"
30390
+ "request",
30391
+ "callMethod",
30392
+ "mockWxApi",
30393
+ "trigger",
30394
+ "setData"
30391
30395
  ];
30392
30396
  var locatorSchema = external_exports.strictObject({
30393
30397
  text: external_exports.string().min(1).optional(),
@@ -30415,7 +30419,25 @@ var stepSchema = external_exports.strictObject({
30415
30419
  variable: external_exports.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/, "\u53D8\u91CF\u540D\u9700\u4EE5\u5B57\u6BCD\u6216\u4E0B\u5212\u7EBF\u5F00\u5934,\u4EC5\u542B\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u4E0B\u5212\u7EBF").optional(),
30416
30420
  timeout: external_exports.number().int().positive().optional(),
30417
30421
  /** UI 动作触发的网络请求精确次数断言 */
30418
- expectRequests: external_exports.array(requestExpectationSchema).min(1).optional()
30422
+ expectRequests: external_exports.array(requestExpectationSchema).min(1).optional(),
30423
+ /** callMethod:要调用的 Page / 组件方法名(仅 miniapp;页面级调用不写 locator) */
30424
+ methodName: external_exports.string().min(1).optional(),
30425
+ /** callMethod 的入参(支持 ${var} 模板) */
30426
+ args: external_exports.array(external_exports.unknown()).optional(),
30427
+ /** mockWxApi:要 mock 的 wx.* API 名(如 showModal / request) */
30428
+ wxApi: external_exports.string().min(1).optional(),
30429
+ /** mockWxApi:mock 返回值(对象按原样注入;capture 时真实调用先上报再返回 mock 值) */
30430
+ result: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
30431
+ /** mockWxApi:捕获真实调用参数的变量名(${<capture>.options} / ${<capture>.called}) */
30432
+ capture: external_exports.string().min(1).optional(),
30433
+ /** trigger:触发的事件类型(如 getphonenumber;空 detail 即"用户拒绝/取消"类分支) */
30434
+ eventType: external_exports.string().min(1).optional(),
30435
+ /** trigger:作为事件 e.detail 注入(值支持 ${var} 模板) */
30436
+ detail: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
30437
+ /** setData:写入页面 data 的补丁(值支持 ${var} 模板) */
30438
+ patch: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
30439
+ /** assert:页面 data 路径断言(与 locator 互斥,仅 miniapp) */
30440
+ dataPath: external_exports.string().min(1).optional()
30419
30441
  });
30420
30442
  var datasetReferenceSchema = external_exports.strictObject({
30421
30443
  file: external_exports.string().min(1),
@@ -30488,7 +30510,11 @@ var ADAPTER_SUPPORT = {
30488
30510
  assert: ["web", "miniapp", "api"],
30489
30511
  extract: ["web", "miniapp", "api"],
30490
30512
  screenshot: ["web", "miniapp"],
30491
- request: ["api"]
30513
+ request: ["api"],
30514
+ callMethod: ["miniapp"],
30515
+ mockWxApi: ["miniapp"],
30516
+ trigger: ["miniapp"],
30517
+ setData: ["miniapp"]
30492
30518
  };
30493
30519
  var LOCATOR_REQUIRED = /* @__PURE__ */ new Set(["click", "input", "select", "assert", "extract"]);
30494
30520
  var VALUE_REQUIRED = /* @__PURE__ */ new Set(["input", "select"]);
@@ -30501,7 +30527,16 @@ var FIELD_ALLOWED = {
30501
30527
  timeout: ["wait"],
30502
30528
  method: ["request"],
30503
30529
  headers: ["request"],
30504
- body: ["request"]
30530
+ body: ["request"],
30531
+ methodName: ["callMethod"],
30532
+ args: ["callMethod"],
30533
+ wxApi: ["mockWxApi"],
30534
+ result: ["mockWxApi"],
30535
+ capture: ["mockWxApi"],
30536
+ eventType: ["trigger"],
30537
+ detail: ["trigger"],
30538
+ patch: ["setData"],
30539
+ dataPath: ["assert"]
30505
30540
  };
30506
30541
  var VARIABLE_REF = /\$\{([A-Za-z_][A-Za-z0-9_.]*)\}/g;
30507
30542
  function validateCase(data) {
@@ -30565,6 +30600,23 @@ function checkStep(step, at, declared, issues) {
30565
30600
  });
30566
30601
  }
30567
30602
  }
30603
+ if (step.action === "mockWxApi") {
30604
+ if (!step.wxApi) {
30605
+ issues.push({ path: `${at}.wxApi`, code: "semantic", message: "mockWxApi \u5FC5\u987B\u63D0\u4F9B wxApi(\u8981 mock \u7684 wx.* API \u540D)" });
30606
+ }
30607
+ if (!step.result) {
30608
+ issues.push({ path: `${at}.result`, code: "semantic", message: "mockWxApi \u5FC5\u987B\u63D0\u4F9B result(mock \u8FD4\u56DE\u503C)" });
30609
+ }
30610
+ }
30611
+ if (step.action === "trigger" && !step.eventType) {
30612
+ issues.push({ path: `${at}.eventType`, code: "semantic", message: "trigger \u5FC5\u987B\u63D0\u4F9B eventType(\u89E6\u53D1\u7684\u4E8B\u4EF6\u7C7B\u578B)" });
30613
+ }
30614
+ if (step.action === "setData" && (!step.patch || Object.keys(step.patch).length === 0)) {
30615
+ issues.push({ path: `${at}.patch`, code: "semantic", message: "setData \u5FC5\u987B\u63D0\u4F9B patch(\u5199\u5165\u9875\u9762 data \u7684\u8865\u4E01)" });
30616
+ }
30617
+ if (step.dataPath !== void 0 && step.target !== "miniapp") {
30618
+ issues.push({ path: `${at}.dataPath`, code: "semantic", message: "dataPath \u65AD\u8A00\u4EC5 miniapp \u652F\u6301(web \u7AEF\u7528 locator)" });
30619
+ }
30568
30620
  if (step.action === "navigate" && !step.url) {
30569
30621
  issues.push({ path: `${at}.url`, code: "semantic", message: "navigate \u5FC5\u987B\u63D0\u4F9B url" });
30570
30622
  }
@@ -30596,6 +30648,13 @@ function checkStep(step, at, declared, issues) {
30596
30648
  if (step.action === "wait" && !step.locator && step.timeout === void 0) {
30597
30649
  issues.push({ path: at, code: "semantic", message: "wait \u9700\u8981\u63D0\u4F9B locator(\u7B49\u5F85\u5143\u7D20)\u6216 timeout(\u6BEB\u79D2)" });
30598
30650
  }
30651
+ if (step.action === "callMethod" && !step.methodName) {
30652
+ issues.push({
30653
+ path: `${at}.methodName`,
30654
+ code: "semantic",
30655
+ message: "callMethod \u5FC5\u987B\u63D0\u4F9B methodName(\u9875\u9762\u6216\u7EC4\u4EF6\u7684\u65B9\u6CD5\u540D)"
30656
+ });
30657
+ }
30599
30658
  if (step.expectRequests) {
30600
30659
  if (step.target === "api" || !REQUEST_EXPECTATION_ACTIONS.has(step.action)) {
30601
30660
  issues.push({
@@ -30637,11 +30696,17 @@ function checkVariableRefs(step, at, declared, issues) {
30637
30696
  collect("value", step.value);
30638
30697
  collect("expected", step.expected);
30639
30698
  collect("url", step.url);
30699
+ collect("methodName", step.methodName);
30640
30700
  collect("locator.text", step.locator?.text);
30641
30701
  collect("locator.css", step.locator?.css);
30642
30702
  if (typeof step.body === "string") collect("body", step.body);
30643
30703
  else if (step.body) collect("body", JSON.stringify(step.body));
30644
30704
  for (const headerValue of Object.values(step.headers ?? {})) collect("headers", headerValue);
30705
+ if (Array.isArray(step.args)) {
30706
+ step.args.forEach((arg, index) => {
30707
+ if (typeof arg === "string") collect(`args[${index}]`, arg);
30708
+ });
30709
+ }
30645
30710
  for (const [index, expectation] of (step.expectRequests ?? []).entries()) {
30646
30711
  collect(`expectRequests[${index}].urlContains`, expectation.urlContains);
30647
30712
  }
@@ -31869,6 +31934,26 @@ var ExecutionContext = class {
31869
31934
  }
31870
31935
  this.variables.set("account", rawValue);
31871
31936
  }
31937
+ /**
31938
+ * 注入结构化对象(如 mockWxApi 的捕获结果):按点号路径展开为平铺字符串变量。
31939
+ * 例如 seedObject('showModalCall', { called: true, args: { content: 'x' } }) 后,
31940
+ * `${showModalCall.called}` 与 `${showModalCall.args.content}` 均可解析。
31941
+ */
31942
+ seedObject(name, value) {
31943
+ const flatten = (prefix, input2) => {
31944
+ if (input2 !== null && typeof input2 === "object" && !Array.isArray(input2)) {
31945
+ const entries = Object.entries(input2);
31946
+ if (entries.length === 0) {
31947
+ this.variables.set(prefix, "");
31948
+ return;
31949
+ }
31950
+ for (const [key, item] of entries) flatten(`${prefix}.${key}`, item);
31951
+ return;
31952
+ }
31953
+ this.variables.set(prefix, input2 === void 0 ? "" : String(input2));
31954
+ };
31955
+ flatten(name, value);
31956
+ }
31872
31957
  };
31873
31958
 
31874
31959
  // ../../packages/execution-engine/src/lifecycle/index.ts
@@ -32027,7 +32112,14 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
32027
32112
  case "wait":
32028
32113
  await adapter.wait(locator, step.timeout);
32029
32114
  break;
32030
- case "assert":
32115
+ case "assert": {
32116
+ if (step.dataPath !== void 0) {
32117
+ if (!adapter.assertData) {
32118
+ throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 dataPath \u65AD\u8A00`);
32119
+ }
32120
+ await adapter.assertData(context.resolve(step.dataPath), context.resolve(step.expected ?? ""));
32121
+ break;
32122
+ }
32031
32123
  if (step.target === "api") {
32032
32124
  if (!adapter.assertResponse) throw new TestPilotError("UNSUPPORTED_ACTION", "\u5F53\u524D adapter \u4E0D\u652F\u6301 api \u65AD\u8A00");
32033
32125
  await adapter.assertResponse(context.resolve(step.expected ?? ""));
@@ -32035,6 +32127,7 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
32035
32127
  await adapter.assert(locator, context.resolve(step.expected ?? ""));
32036
32128
  }
32037
32129
  break;
32130
+ }
32038
32131
  case "extract": {
32039
32132
  const value = step.target === "api" ? await extractApiResponse(adapter, context.resolve(step.value ?? "")) : await adapter.extract(locator);
32040
32133
  if (step.variable) {
@@ -32074,6 +32167,42 @@ async function executeStep(step, index, adapter, context, artifacts, options) {
32074
32167
  }
32075
32168
  break;
32076
32169
  }
32170
+ case "callMethod": {
32171
+ if (typeof adapter.callMethod !== "function") {
32172
+ throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 callMethod`);
32173
+ }
32174
+ const methodName = context.resolve(step.methodName ?? "");
32175
+ if (!methodName) throw new TestPilotError("MISSING_METHOD_NAME", "callMethod \u5FC5\u987B\u63D0\u4F9B methodName");
32176
+ const callArgs = (step.args ?? []).map((arg) => resolveDeep(arg, context));
32177
+ await adapter.callMethod(locator, methodName, callArgs);
32178
+ break;
32179
+ }
32180
+ case "mockWxApi": {
32181
+ if (typeof adapter.mockWxApi !== "function") {
32182
+ throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 mockWxApi`);
32183
+ }
32184
+ await adapter.mockWxApi({
32185
+ api: context.resolve(step.wxApi ?? ""),
32186
+ result: step.result ? resolveDeep(step.result, context) : void 0,
32187
+ capture: step.capture
32188
+ });
32189
+ break;
32190
+ }
32191
+ case "trigger": {
32192
+ if (typeof adapter.trigger !== "function") {
32193
+ throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 trigger`);
32194
+ }
32195
+ if (!locator) throw new TestPilotError("MISSING_LOCATOR", "trigger \u5FC5\u987B\u63D0\u4F9B locator(\u8981\u89E6\u53D1\u4E8B\u4EF6\u7684\u7EC4\u4EF6)");
32196
+ await adapter.trigger(locator, context.resolve(step.eventType ?? ""), step.detail ? resolveDeep(step.detail, context) : void 0);
32197
+ break;
32198
+ }
32199
+ case "setData": {
32200
+ if (typeof adapter.setData !== "function") {
32201
+ throw new TestPilotError("UNSUPPORTED_ACTION", `\u5F53\u524D ${step.target} adapter \u4E0D\u652F\u6301 setData`);
32202
+ }
32203
+ await adapter.setData(resolveDeep(step.patch ?? {}, context));
32204
+ break;
32205
+ }
32077
32206
  default: {
32078
32207
  const exhaustive = step.action;
32079
32208
  throw new TestPilotError("UNSUPPORTED_ACTION", `\u4E0D\u652F\u6301\u7684 action:${String(exhaustive)}`);
@@ -32262,6 +32391,12 @@ var ExecutionEngine = class {
32262
32391
  result = await executeStep(step, index, adapter, context, artifacts, {
32263
32392
  screenshotName: `${executionId}-${phase}-${String(index).padStart(2, "0")}`
32264
32393
  });
32394
+ if (step.capture !== void 0 && typeof adapter.readWxApiCaptures === "function") {
32395
+ const captures = adapter.readWxApiCaptures();
32396
+ if (captures && captures.calls.length > 0) {
32397
+ context.seedObject(step.capture, captures.calls[0]?.options ?? null);
32398
+ }
32399
+ }
32265
32400
  } catch (err) {
32266
32401
  result = {
32267
32402
  index,
@@ -32383,6 +32518,9 @@ var ExecutionEngine = class {
32383
32518
  );
32384
32519
  }
32385
32520
  }
32521
+ for (const [, adapter] of evidenceAdapters) {
32522
+ await adapter.restoreWxMock?.().catch(() => void 0);
32523
+ }
32386
32524
  await bus.emit({
32387
32525
  type: "case-finished",
32388
32526
  runId,
@@ -32546,6 +32684,10 @@ import { mkdtemp, readFile as readFile6, rm } from "node:fs/promises";
32546
32684
  import { tmpdir } from "node:os";
32547
32685
  import { join as join16 } from "node:path";
32548
32686
  import { errors } from "playwright";
32687
+ var TEXT_WAIT_MS = 1e4;
32688
+ function delay2(ms) {
32689
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
32690
+ }
32549
32691
  var PlaywrightAdapter = class {
32550
32692
  constructor(bundle, options = {}) {
32551
32693
  this.bundle = bundle;
@@ -32636,16 +32778,29 @@ var PlaywrightAdapter = class {
32636
32778
  }
32637
32779
  async assert(locator, expected) {
32638
32780
  const target = await this.toLocator(locator);
32639
- await this.coded(() => target.waitFor({ state: "visible" }));
32640
- const actual = (await this.coded(() => target.innerText())).trim();
32641
32781
  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)}"`);
32782
+ const startedAt = Date.now();
32783
+ for (; ; ) {
32784
+ await this.coded(() => target.waitFor({ state: "visible" }));
32785
+ const actual = (await this.coded(() => target.innerText())).trim();
32786
+ if (actual.includes(wanted)) return;
32787
+ if (Date.now() - startedAt > TEXT_WAIT_MS) {
32788
+ throw new TestPilotError(
32789
+ "ASSERTION_FAILED",
32790
+ `\u65AD\u8A00\u5931\u8D25(\u7B49\u5F85 ${TEXT_WAIT_MS}ms):\u671F\u671B\u5305\u542B "${wanted}",\u5B9E\u9645\u4E3A "${actual.slice(0, 200)}"`
32791
+ );
32792
+ }
32793
+ await delay2(300);
32644
32794
  }
32645
32795
  }
32646
32796
  async extract(locator) {
32647
32797
  const target = await this.toLocator(locator);
32648
- return this.coded(() => target.innerText()).then((text) => text.trim());
32798
+ const startedAt = Date.now();
32799
+ for (; ; ) {
32800
+ const text = (await this.coded(() => target.innerText())).trim();
32801
+ if (text.length > 0 || Date.now() - startedAt > TEXT_WAIT_MS) return text;
32802
+ await delay2(300);
32803
+ }
32649
32804
  }
32650
32805
  async screenshot() {
32651
32806
  return (await this.ensurePage()).screenshot();
@@ -32710,6 +32865,8 @@ import net from "node:net";
32710
32865
  var AUTO_PORT_FALLBACK_WAIT_MS = 9e4;
32711
32866
  var NAVIGATE_RETRY_WINDOW_MS = 45e3;
32712
32867
  var ELEMENT_WAIT_MS = 1e4;
32868
+ var TEXT_WAIT_MS2 = 1e4;
32869
+ var PAGE_READY_WAIT_MS = 3e4;
32713
32870
  var CONNECTION_LOST = /connection closed|not connected/i;
32714
32871
  var automatorLauncher = {
32715
32872
  launch: async (options) => {
@@ -32723,7 +32880,7 @@ var automatorLauncher = {
32723
32880
  }
32724
32881
  }
32725
32882
  };
32726
- function delay2(ms) {
32883
+ function delay3(ms) {
32727
32884
  return new Promise((resolve7) => setTimeout(resolve7, ms));
32728
32885
  }
32729
32886
  async function connectWithRetry(wsEndpoint, timeoutMs) {
@@ -32734,7 +32891,7 @@ async function connectWithRetry(wsEndpoint, timeoutMs) {
32734
32891
  return await automator.connect({ wsEndpoint });
32735
32892
  } catch (err) {
32736
32893
  lastError = err;
32737
- await delay2(1e3);
32894
+ await delay3(1e3);
32738
32895
  }
32739
32896
  }
32740
32897
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
@@ -32803,14 +32960,31 @@ var MiniAppAdapter = class {
32803
32960
  await this.runWithReconnect(async () => {
32804
32961
  await (await this.getProgram()).reLaunch(url2);
32805
32962
  });
32963
+ await this.waitForPageReady();
32806
32964
  return;
32807
32965
  } catch (err) {
32808
32966
  lastError = err;
32809
- await delay2(3e3);
32967
+ await delay3(3e3);
32810
32968
  }
32811
32969
  }
32812
32970
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
32813
32971
  }
32972
+ /** 等待页面就绪:模拟器编译期间 currentPage / data 会失败或挂起,轮询至可响应 */
32973
+ async waitForPageReady(timeoutMs = PAGE_READY_WAIT_MS) {
32974
+ const startedAt = Date.now();
32975
+ for (; ; ) {
32976
+ const page = await (await this.getProgram()).currentPage();
32977
+ const data = await page.data().catch(() => void 0);
32978
+ if (data !== void 0 && data !== null) return;
32979
+ if (Date.now() - startedAt > timeoutMs) {
32980
+ throw new TestPilotError(
32981
+ "PAGE_READY_TIMEOUT",
32982
+ `\u7B49\u5F85\u9875\u9762\u5C31\u7EEA\u8D85\u65F6(${timeoutMs}ms):\u6A21\u62DF\u5668\u53EF\u80FD\u4ECD\u5728\u7F16\u8BD1,\u6216\u9879\u76EE\u672A\u88AB\u4FE1\u4EFB`
32983
+ );
32984
+ }
32985
+ await delay3(300);
32986
+ }
32987
+ }
32814
32988
  async click(locator) {
32815
32989
  await this.runWithReconnect(async () => (await this.element(locator)).tap());
32816
32990
  }
@@ -32833,17 +33007,100 @@ var MiniAppAdapter = class {
32833
33007
  async assert(locator, expected) {
32834
33008
  await this.runWithReconnect(async () => {
32835
33009
  const element = await this.element(locator);
32836
- const actual = (await element.text()).trim();
32837
33010
  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)}"`);
33011
+ const startedAt = Date.now();
33012
+ for (; ; ) {
33013
+ const actual = (await element.text()).trim();
33014
+ if (actual.includes(wanted)) return;
33015
+ if (Date.now() - startedAt > TEXT_WAIT_MS2) {
33016
+ throw new TestPilotError(
33017
+ "ASSERTION_FAILED",
33018
+ `\u65AD\u8A00\u5931\u8D25(\u7B49\u5F85 ${TEXT_WAIT_MS2}ms):\u671F\u671B\u5305\u542B "${wanted}",\u5B9E\u9645\u4E3A "${actual.slice(0, 200)}"`
33019
+ );
33020
+ }
33021
+ await delay3(300);
32840
33022
  }
32841
33023
  });
32842
33024
  }
32843
33025
  async extract(locator) {
32844
33026
  return this.runWithReconnect(async () => {
32845
33027
  const element = await this.element(locator);
32846
- return (await element.text()).trim();
33028
+ const startedAt = Date.now();
33029
+ for (; ; ) {
33030
+ const text = (await element.text()).trim();
33031
+ if (text.length > 0 || Date.now() - startedAt > TEXT_WAIT_MS2) return text;
33032
+ await delay3(300);
33033
+ }
33034
+ });
33035
+ }
33036
+ /** 调用 Page / 组件方法;不传 locator 时为页面级调用(Page 实例方法) */
33037
+ async callMethod(locator, methodName, args = []) {
33038
+ return this.runWithReconnect(async () => {
33039
+ if (locator) {
33040
+ const element = await this.element(locator);
33041
+ return element.callMethod(methodName, ...args);
33042
+ }
33043
+ const page = await (await this.getProgram()).currentPage();
33044
+ return page.callMethod(methodName, ...args);
33045
+ });
33046
+ }
33047
+ // ---- miniapp 深度测试(V0.2)----
33048
+ wxMock;
33049
+ /** mock 任意 wx.* API:拦截调用并返回 mock 值;capture 记录真实调用参数(最近调用在前) */
33050
+ async mockWxApi(options) {
33051
+ if (this.wxMock?.api === options.api) await this.restoreWxMock();
33052
+ const program2 = await this.getProgram();
33053
+ const captured = [];
33054
+ this.wxMock = { api: options.api, captured };
33055
+ const callbackName = options.capture ? `__tpWxCapture_${options.capture}` : void 0;
33056
+ if (callbackName) {
33057
+ await program2.exposeFunction(callbackName, (payload) => {
33058
+ if (payload !== null && typeof payload === "object") {
33059
+ captured.push(payload);
33060
+ }
33061
+ });
33062
+ }
33063
+ await program2.mockWxMethod(
33064
+ options.api,
33065
+ function(invokeOptions, cbName) {
33066
+ const callbacks = globalThis;
33067
+ callbacks[cbName]?.({ options: invokeOptions });
33068
+ return options.result ?? this.origin(invokeOptions);
33069
+ },
33070
+ callbackName
33071
+ );
33072
+ }
33073
+ /** 读取 mock 期间捕获的调用参数(最近调用在前) */
33074
+ readCapturedWxApiCalls(api) {
33075
+ return this.wxMock?.api === api ? [...this.wxMock.captured].reverse() : [];
33076
+ }
33077
+ /** 恢复 mock 的 wx API(用例结束由引擎调用) */
33078
+ async restoreWxMock() {
33079
+ const mock = this.wxMock;
33080
+ if (!mock) return;
33081
+ this.wxMock = void 0;
33082
+ if (!this.program) return;
33083
+ await this.program.restoreWxMethod(mock.api).catch(() => void 0);
33084
+ }
33085
+ /** 触发组件事件(trigger;detail 作为 e.detail 注入,空 detail 即"用户拒绝/取消"类分支) */
33086
+ async trigger(locator, eventType, detail) {
33087
+ await this.runWithReconnect(async () => {
33088
+ const element = await this.element(locator);
33089
+ await element.trigger(eventType, detail);
33090
+ });
33091
+ }
33092
+ /** 读取页面 data(点号路径,缺省为整个 data 对象) */
33093
+ async getData(dataPath) {
33094
+ return this.runWithReconnect(async () => {
33095
+ const page = await (await this.getProgram()).currentPage();
33096
+ return page.data(dataPath);
33097
+ });
33098
+ }
33099
+ /** 写入页面 data 补丁(setData 驱动 onShow/onPullDownRefresh 等真实链路) */
33100
+ async setData(patch) {
33101
+ await this.runWithReconnect(async () => {
33102
+ const page = await (await this.getProgram()).currentPage();
33103
+ await page.setData(patch);
32847
33104
  });
32848
33105
  }
32849
33106
  async screenshot() {
@@ -32936,7 +33193,7 @@ var MiniAppAdapter = class {
32936
33193
  if (Date.now() - startedAt > ELEMENT_WAIT_MS) {
32937
33194
  throw new TestPilotError("ELEMENT_NOT_FOUND", `\u7B49\u5F85\u5143\u7D20\u8D85\u65F6(${ELEMENT_WAIT_MS}ms):${selector}`);
32938
33195
  }
32939
- await delay2(500);
33196
+ await delay3(500);
32940
33197
  }
32941
33198
  }
32942
33199
  };
@@ -33416,7 +33673,7 @@ async function collectEnvVarRefs(root) {
33416
33673
  }
33417
33674
 
33418
33675
  // src/commands/init/project-detector.ts
33419
- import { existsSync as existsSync6, readFileSync as readFileSync2 } from "node:fs";
33676
+ import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "node:fs";
33420
33677
  import { basename, join as join22 } from "node:path";
33421
33678
 
33422
33679
  // src/lib/git.ts
@@ -33472,10 +33729,31 @@ function detectProjectType(root) {
33472
33729
  if (isWeb) return "web";
33473
33730
  return "unknown";
33474
33731
  }
33732
+ function detectLanguageFromSources(root) {
33733
+ const SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "miniprogram_npm", "dist", ".git"]);
33734
+ const found = { ts: false, js: false };
33735
+ const walk = (dir, depth) => {
33736
+ if (depth > 4 || found.ts && found.js) return;
33737
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
33738
+ if (entry.name.startsWith(".")) continue;
33739
+ const full = join22(dir, entry.name);
33740
+ if (entry.isDirectory()) {
33741
+ if (!SKIP_DIRS.has(entry.name)) walk(full, depth + 1);
33742
+ continue;
33743
+ }
33744
+ if (/\.(ts|tsx)$/.test(entry.name)) found.ts = true;
33745
+ else if (/\.(js|cjs|mjs|wxml)$/.test(entry.name)) found.js = true;
33746
+ }
33747
+ };
33748
+ walk(root, 0);
33749
+ if (found.ts) return "TypeScript";
33750
+ if (found.js) return "JavaScript";
33751
+ return "Unknown";
33752
+ }
33475
33753
  async function detectProject(root) {
33476
33754
  const pkg = readPackageJson(root);
33477
33755
  const type = detectProjectType(root);
33478
- const language = existsSync6(join22(root, "tsconfig.json")) ? "TypeScript" : pkg || existsSync6(join22(root, "src")) ? "JavaScript" : "Unknown";
33756
+ const language = existsSync6(join22(root, "tsconfig.json")) ? "TypeScript" : pkg || existsSync6(join22(root, "src")) ? "JavaScript" : detectLanguageFromSources(root);
33479
33757
  const packageManager = existsSync6(join22(root, "pnpm-lock.yaml")) ? "pnpm" : existsSync6(join22(root, "yarn.lock")) ? "yarn" : existsSync6(join22(root, "package-lock.json")) ? "npm" : "npx";
33480
33758
  const existingE2eDirs = E2E_DIR_CANDIDATES.filter((dir) => existsSync6(join22(root, dir)));
33481
33759
  return {
@@ -33955,7 +34233,7 @@ function randomId() {
33955
34233
  }
33956
34234
 
33957
34235
  // src/commands/init/test-structure.ts
33958
- import { existsSync as existsSync9, readdirSync as readdirSync2 } from "node:fs";
34236
+ import { existsSync as existsSync9, readdirSync as readdirSync3 } from "node:fs";
33959
34237
  import { mkdir as mkdir14, writeFile as writeFile12 } from "node:fs/promises";
33960
34238
  import { join as join26 } from "node:path";
33961
34239
  async function initTestStructure(root, _info) {
@@ -33975,7 +34253,7 @@ async function initTestStructure(root, _info) {
33975
34253
  await mkdir14(dir, { recursive: true });
33976
34254
  }
33977
34255
  for (const [name, dir] of Object.entries(dirs)) {
33978
- if (!existed[name] && readdirSync2(dir).length === 0) {
34256
+ if (!existed[name] && readdirSync3(dir).length === 0) {
33979
34257
  await writeFile12(join26(dir, ".gitkeep"), "", "utf8").catch(() => void 0);
33980
34258
  }
33981
34259
  }
@@ -34350,7 +34628,7 @@ function makeValidateCommand() {
34350
34628
  }
34351
34629
 
34352
34630
  // src/cli.ts
34353
- var CLI_VERSION = "0.7.0";
34631
+ var CLI_VERSION = "0.7.1";
34354
34632
  async function createCli() {
34355
34633
  const program2 = new Command();
34356
34634
  program2.name("csspilot").description("TestPilot \u2014 AI Native \u8DE8\u7AEF\u4E1A\u52A1\u6D4B\u8BD5\u57FA\u7840\u8BBE\u65BD CLI").version(CLI_VERSION, "-v, --version", "output the current version").option("-V", "output the current version", () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "csspilot",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "TestPilot — AI Native 跨端业务测试基础设施 CLI",
5
5
  "license": "MIT",
6
6
  "repository": {