@weapp-vite/miniprogram-automator 1.0.2 → 1.0.4

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.d.mts CHANGED
@@ -202,6 +202,12 @@ interface IScreenshotOptions {
202
202
  interface IAuditsOptions {
203
203
  path?: string;
204
204
  }
205
+ interface IToolCompileOptions {
206
+ force?: boolean;
207
+ }
208
+ interface IToolClearCacheOptions {
209
+ clean: 'all' | 'auth' | 'compile' | 'file' | 'network' | 'session' | 'storage';
210
+ }
205
211
  type AutomatorCallable = (...args: any[]) => any;
206
212
  /** MiniProgram 的实现。 */
207
213
  declare class MiniProgram extends EventEmitter {
@@ -211,11 +217,11 @@ declare class MiniProgram extends EventEmitter {
211
217
  private nativeIns?;
212
218
  constructor(connection: Connection);
213
219
  pageStack(): Promise<any>;
214
- navigateTo(url: string): Promise<Page>;
215
- redirectTo(url: string): Promise<Page>;
216
- navigateBack(): Promise<Page>;
217
- reLaunch(url: string): Promise<Page>;
218
- switchTab(url: string): Promise<Page>;
220
+ navigateTo(url: string): Promise<any>;
221
+ redirectTo(url: string): Promise<any>;
222
+ navigateBack(): Promise<any>;
223
+ reLaunch(url: string): Promise<any>;
224
+ switchTab(url: string): Promise<any>;
219
225
  currentPage(): Promise<Page>;
220
226
  systemInfo(): Promise<any>;
221
227
  callWxMethod(method: string, ...args: any[]): Promise<any>;
@@ -234,6 +240,22 @@ declare class MiniProgram extends EventEmitter {
234
240
  checkVersion(): Promise<void>;
235
241
  screenshot(options?: IScreenshotOptions): Promise<any>;
236
242
  testAccounts(): Promise<any>;
243
+ /**
244
+ * @description 获取开发者工具 Tool 域基础信息。
245
+ */
246
+ toolInfo(): Promise<any>;
247
+ /**
248
+ * @description 触发开发者工具执行一次项目编译。
249
+ */
250
+ compile(options?: IToolCompileOptions): Promise<any>;
251
+ /**
252
+ * @description 清理开发者工具缓存。
253
+ */
254
+ clearCache(options: IToolClearCacheOptions): Promise<any>;
255
+ /**
256
+ * @description 调用开发者工具 Tool 域协议方法。
257
+ */
258
+ tool(method: string, params?: Record<string, any>): Promise<any>;
237
259
  stopAudits(options?: IAuditsOptions): Promise<any>;
238
260
  getTicket(): Promise<any>;
239
261
  setTicket(ticket: string): Promise<void>;
@@ -247,7 +269,6 @@ declare class MiniProgram extends EventEmitter {
247
269
  }
248
270
  //#endregion
249
271
  //#region src/Launcher.d.ts
250
- /** IConnectOptions 的类型定义。 */
251
272
  interface IConnectOptions {
252
273
  wsEndpoint: string;
253
274
  }
package/dist/index.mjs CHANGED
@@ -577,11 +577,11 @@ var Connection = class Connection extends EventEmitter {
577
577
  //#endregion
578
578
  //#region src/headless.ts
579
579
  async function launchHeadlessAutomator(options) {
580
- return await (await import("./launch-Bd3TZy1I.mjs")).launch({ projectPath: options.projectPath });
580
+ return await (await import("./launch-Didv0lMX.mjs")).launch({ projectPath: options.projectPath });
581
581
  }
582
582
  //#endregion
583
583
  //#region package.json
584
- var version = "1.0.2";
584
+ var version = "1.0.4";
585
585
  //#endregion
586
586
  //#region src/Native.ts
587
587
  /** Native 的实现。 */
@@ -770,6 +770,11 @@ function extractPluginId(p) {
770
770
  //#endregion
771
771
  //#region src/MiniProgram.ts
772
772
  const CLOSE_STEP_TIMEOUT = 2e3;
773
+ const CURRENT_PAGE_RETRIES = 3;
774
+ const CURRENT_PAGE_RETRY_DELAY = 400;
775
+ const CHANGE_ROUTE_READY_TIMEOUT = 15e3;
776
+ const CHANGE_ROUTE_POLL_DELAY = 500;
777
+ const CHANGE_ROUTE_DEBUG_ENABLED = process.env.WEAPP_VITE_E2E_CHANGE_ROUTE_DEBUG === "1";
773
778
  function sleep(ms) {
774
779
  return new Promise((resolve) => setTimeout(resolve, ms));
775
780
  }
@@ -792,6 +797,16 @@ function isFnStr(value) {
792
797
  const trimmed = trim(value);
793
798
  return startWith(trimmed, "function") || startWith(trimmed, "() =>");
794
799
  }
800
+ function isCurrentPageProtocolTimeout(error) {
801
+ return error instanceof Error && "code" in error && error.code === "DEVTOOLS_PROTOCOL_TIMEOUT" && "method" in error && error.method === "App.getCurrentPage";
802
+ }
803
+ function normalizeRoutePath(value) {
804
+ return String(value ?? "").split("?", 1)[0].replace(/^\/+/, "").replace(/\/+$/, "");
805
+ }
806
+ function logChangeRouteDebug(message) {
807
+ if (!CHANGE_ROUTE_DEBUG_ENABLED) return;
808
+ process.stdout.write(`[automator:changeRoute] ${message}\n`);
809
+ }
795
810
  /** MiniProgram 的实现。 */
796
811
  var MiniProgram = class extends EventEmitter {
797
812
  appBindings = /* @__PURE__ */ new Map();
@@ -830,12 +845,20 @@ var MiniProgram = class extends EventEmitter {
830
845
  return await this.changeRoute("switchTab", url);
831
846
  }
832
847
  async currentPage() {
833
- const { pageId, path, query } = await this.send("App.getCurrentPage");
834
- return Page.create(this.connection, {
835
- id: pageId,
836
- path,
837
- query
838
- }, this.pageMap);
848
+ let lastError;
849
+ for (let attempt = 1; attempt <= CURRENT_PAGE_RETRIES; attempt += 1) try {
850
+ const { pageId, path, query } = await this.send("App.getCurrentPage");
851
+ return Page.create(this.connection, {
852
+ id: pageId,
853
+ path,
854
+ query
855
+ }, this.pageMap);
856
+ } catch (error) {
857
+ lastError = error;
858
+ if (!isCurrentPageProtocolTimeout(error) || attempt >= CURRENT_PAGE_RETRIES) throw error;
859
+ await sleep(CURRENT_PAGE_RETRY_DELAY);
860
+ }
861
+ throw lastError;
839
862
  }
840
863
  async systemInfo() {
841
864
  return await this.callWxMethod("getSystemInfoSync");
@@ -951,6 +974,30 @@ var MiniProgram = class extends EventEmitter {
951
974
  async testAccounts() {
952
975
  return (await this.send("Tool.getTestAccounts")).accounts;
953
976
  }
977
+ /**
978
+ * @description 获取开发者工具 Tool 域基础信息。
979
+ */
980
+ async toolInfo() {
981
+ return await this.send("Tool.getInfo");
982
+ }
983
+ /**
984
+ * @description 触发开发者工具执行一次项目编译。
985
+ */
986
+ async compile(options = {}) {
987
+ return await this.send("Tool.compile", options);
988
+ }
989
+ /**
990
+ * @description 清理开发者工具缓存。
991
+ */
992
+ async clearCache(options) {
993
+ return await this.send("Tool.clearCache", options);
994
+ }
995
+ /**
996
+ * @description 调用开发者工具 Tool 域协议方法。
997
+ */
998
+ async tool(method, params = {}) {
999
+ return await this.send(`Tool.${method}`, params);
1000
+ }
954
1001
  async stopAudits(options = {}) {
955
1002
  const result = await this.send("Tool.stopAudits");
956
1003
  if (options.path) await fs.writeFile(options.path, result.report, "utf8");
@@ -971,12 +1018,50 @@ var MiniProgram = class extends EventEmitter {
971
1018
  }
972
1019
  async changeRoute(method, url) {
973
1020
  const currentPage = await this.currentPage();
1021
+ logChangeRouteDebug(`start method=${method} url=${url ?? "<none>"} current=${currentPage?.path ?? "<none>"}`);
974
1022
  if (currentPage && isPluginPath(currentPage.path)) {
975
1023
  const pluginId = extractPluginId(currentPage.path);
976
1024
  await this.callPluginWxMethod(pluginId, method, { url });
977
1025
  } else await this.callWxMethod(method, { url });
978
- await sleep(3e3);
979
- return await this.currentPage();
1026
+ const expectedRoute = normalizeRoutePath(url);
1027
+ const startedAt = Date.now();
1028
+ let lastPage;
1029
+ let lastError;
1030
+ while (Date.now() - startedAt <= CHANGE_ROUTE_READY_TIMEOUT) {
1031
+ try {
1032
+ const page = await this.currentPage();
1033
+ lastPage = page;
1034
+ logChangeRouteDebug(`poll method=${method} url=${url ?? "<none>"} current=${page?.path ?? "<none>"}`);
1035
+ if (!expectedRoute || normalizeRoutePath(page?.path) === expectedRoute) {
1036
+ logChangeRouteDebug(`ready method=${method} url=${url ?? "<none>"} current=${page?.path ?? "<none>"}`);
1037
+ return page;
1038
+ }
1039
+ } catch (error) {
1040
+ lastError = error;
1041
+ logChangeRouteDebug(`poll-error method=${method} url=${url ?? "<none>"} error=${error instanceof Error ? error.message : String(error)}`);
1042
+ }
1043
+ try {
1044
+ const stackTop = (await this.pageStack()).at(-1);
1045
+ if (stackTop) {
1046
+ lastPage = stackTop;
1047
+ logChangeRouteDebug(`stack method=${method} url=${url ?? "<none>"} current=${stackTop.path}`);
1048
+ if (!expectedRoute || normalizeRoutePath(stackTop.path) === expectedRoute) {
1049
+ logChangeRouteDebug(`stack-ready method=${method} url=${url ?? "<none>"} current=${stackTop.path}`);
1050
+ return stackTop;
1051
+ }
1052
+ }
1053
+ } catch (error) {
1054
+ lastError = error;
1055
+ logChangeRouteDebug(`stack-error method=${method} url=${url ?? "<none>"} error=${error instanceof Error ? error.message : String(error)}`);
1056
+ }
1057
+ await sleep(CHANGE_ROUTE_POLL_DELAY);
1058
+ }
1059
+ if (lastPage) {
1060
+ logChangeRouteDebug(`timeout method=${method} url=${url ?? "<none>"} current=${lastPage.path}`);
1061
+ throw new Error(`Timed out waiting route ${expectedRoute || "<current>"} after ${method}; current page: ${lastPage.path}`);
1062
+ }
1063
+ logChangeRouteDebug(`timeout method=${method} url=${url ?? "<none>"} current=<none> error=${lastError instanceof Error ? lastError.message : String(lastError)}`);
1064
+ throw lastError instanceof Error ? lastError : /* @__PURE__ */ new Error(`Timed out waiting route ${expectedRoute || "<current>"} after ${method}`);
980
1065
  }
981
1066
  async send(method, params = {}) {
982
1067
  return await this.connection.send(method, params);
@@ -1005,6 +1090,7 @@ const DEFAULT_TIMEOUT = 3e4;
1005
1090
  const DEFAULT_RUNTIME_PROVIDER_ENV = "WEAPP_VITE_AUTOMATOR_RUNTIME_PROVIDER";
1006
1091
  const LEGACY_RUNTIME_PROVIDER_ENV = "WEAPP_VITE_E2E_RUNTIME_PROVIDER";
1007
1092
  const EXTENSION_CONTEXT_INVALIDATED_RE = /Extension context invalidated/i;
1093
+ const WINDOWS_BATCH_CLI_RE = /\.(?:bat|cmd)$/i;
1008
1094
  let localhostListenPatched = false;
1009
1095
  function isExtensionContextInvalidatedError(error) {
1010
1096
  return error instanceof Error && EXTENSION_CONTEXT_INVALIDATED_RE.test(error.message);
@@ -1026,6 +1112,25 @@ function patchNetListenToLoopback() {
1026
1112
  return rawListen.apply(this, args);
1027
1113
  };
1028
1114
  }
1115
+ /** IConnectOptions 的类型定义。 */
1116
+ function shouldUseWindowsCommandShell(cliPath) {
1117
+ return isWindows && WINDOWS_BATCH_CLI_RE.test(cliPath);
1118
+ }
1119
+ function escapeWindowsCmdArg(arg) {
1120
+ const escaped = arg.replace(/"/g, "\"\"").replace(/%/g, "%%");
1121
+ return /[\s"&<>^|()]/.test(arg) ? `"${escaped}"` : escaped;
1122
+ }
1123
+ function resolveWindowsBatchSpawn(cliPath, args) {
1124
+ return {
1125
+ file: process.env.ComSpec || "cmd.exe",
1126
+ args: [
1127
+ "/d",
1128
+ "/s",
1129
+ "/c",
1130
+ `"${[cliPath, ...args].map(escapeWindowsCmdArg).join(" ")}"`
1131
+ ]
1132
+ };
1133
+ }
1029
1134
  function resolveRuntimeProvider(options) {
1030
1135
  return options.runtimeProvider || process.env[DEFAULT_RUNTIME_PROVIDER_ENV] || process.env[LEGACY_RUNTIME_PROVIDER_ENV] || "devtools";
1031
1136
  }
@@ -1045,7 +1150,8 @@ var Launcher = class {
1045
1150
  if (!await import("node:fs/promises").then((fs) => fs.access(projectPath).then(() => true).catch(() => false))) throw new Error(`Project path ${projectPath} doesn't exist`);
1046
1151
  if (!isEmpty(projectConfig)) await this.extendProjectConfig(projectConfig, projectPath);
1047
1152
  let processError = null;
1048
- let exited = false;
1153
+ let processExitCode = null;
1154
+ let processSignal = null;
1049
1155
  args = [
1050
1156
  ...args,
1051
1157
  "auto",
@@ -1058,17 +1164,25 @@ var Launcher = class {
1058
1164
  else if (ticket) args.push("--ticket", ticket);
1059
1165
  if (trustProject) args.push("--trust-project");
1060
1166
  try {
1061
- const child = spawn(cliPath, args, {
1167
+ const spawnTarget = shouldUseWindowsCommandShell(cliPath) ? resolveWindowsBatchSpawn(cliPath, args) : {
1168
+ file: cliPath,
1169
+ args
1170
+ };
1171
+ const child = spawn(spawnTarget.file, spawnTarget.args, {
1062
1172
  stdio: "ignore",
1063
- cwd: cwd || void 0
1173
+ cwd: cwd || void 0,
1174
+ ...shouldUseWindowsCommandShell(cliPath) ? {
1175
+ windowsHide: true,
1176
+ windowsVerbatimArguments: true
1177
+ } : {}
1064
1178
  });
1065
1179
  child.on("error", (error) => {
1066
1180
  processError = error;
1067
1181
  });
1068
- child.on("exit", () => {
1069
- setTimeout(() => {
1070
- exited = true;
1071
- }, 15e3);
1182
+ child.on("exit", (code, signal) => {
1183
+ processExitCode = code;
1184
+ processSignal = signal;
1185
+ if (code !== 0 || signal) processError = /* @__PURE__ */ new Error(`DevTools cli exited unexpectedly with code ${code ?? "null"}${signal ? ` and signal ${signal}` : ""}`);
1072
1186
  });
1073
1187
  child.unref();
1074
1188
  } catch (error) {
@@ -1078,7 +1192,7 @@ var Launcher = class {
1078
1192
  let lastConnectError = null;
1079
1193
  await waitUntil(async () => {
1080
1194
  try {
1081
- if (processError || exited) return true;
1195
+ if (processError) return true;
1082
1196
  const candidate = await this.connectTool({ wsEndpoint: `ws://127.0.0.1:${port}` });
1083
1197
  try {
1084
1198
  await candidate.checkVersion();
@@ -1098,7 +1212,7 @@ var Launcher = class {
1098
1212
  if (!miniProgram) {
1099
1213
  if (processError) throw new Error("Failed to launch wechat web devTools, please make sure cliPath is correctly specified");
1100
1214
  if (lastConnectError) throw lastConnectError;
1101
- if (exited) throw new Error("Failed to launch wechat web devTools, please make sure http port is open");
1215
+ if (processExitCode !== null || processSignal) throw new Error("Failed to launch wechat web devTools, please make sure http port is open");
1102
1216
  throw new Error("Failed connecting to devtools websocket endpoint");
1103
1217
  }
1104
1218
  const resolvedMiniProgram = miniProgram;
@@ -27,9 +27,12 @@ function createAppInstance(definition) {
27
27
  return instance;
28
28
  }
29
29
  //#endregion
30
- //#region ../../mpcore/packages/simulator/src/runtime/componentInstance.ts
30
+ //#region ../../mpcore/packages/simulator/src/runtime/componentInstance/shared.ts
31
31
  const ARRAY_INDEX_PATH_RE$1 = /\[(\d+)\]/g;
32
32
  const ARRAY_INDEX_SEGMENT_RE$1 = /^\d+$/;
33
+ function cloneObject$1(value) {
34
+ return JSON.parse(JSON.stringify(value));
35
+ }
33
36
  function bindFunction$1(target, key, value) {
34
37
  if (typeof value !== "function") {
35
38
  target[key] = value;
@@ -37,14 +40,14 @@ function bindFunction$1(target, key, value) {
37
40
  }
38
41
  target[key] = (...args) => value.apply(target, args);
39
42
  }
40
- function cloneObject$1(value) {
41
- return JSON.parse(JSON.stringify(value));
42
- }
43
43
  function cloneValue$1(value) {
44
44
  if (Array.isArray(value)) return JSON.parse(JSON.stringify(value));
45
45
  if (value && typeof value === "object") return cloneObject$1(value);
46
46
  return value;
47
47
  }
48
+ function cloneRecord(value) {
49
+ return cloneObject$1(value);
50
+ }
48
51
  function parseDataPath$1(path) {
49
52
  return path.replace(ARRAY_INDEX_PATH_RE$1, ".$1").split(".").map((segment) => segment.trim()).filter(Boolean);
50
53
  }
@@ -70,13 +73,70 @@ function assignByPath$1(target, path, value) {
70
73
  const normalizedLeafSegment = isArrayIndexSegment$1(leafSegment) ? Number(leafSegment) : leafSegment;
71
74
  current[normalizedLeafSegment] = value;
72
75
  }
76
+ function mergeRecord(...records) {
77
+ return Object.assign({}, ...records.filter(Boolean));
78
+ }
79
+ //#endregion
80
+ //#region ../../mpcore/packages/simulator/src/runtime/componentInstance/observers.ts
81
+ function runPropertyObservers(definition, instance, changedKeys, previousProperties) {
82
+ const propOptions = definition.properties;
83
+ if (!propOptions || typeof propOptions !== "object" || Array.isArray(propOptions)) return;
84
+ for (const changedKey of changedKeys) {
85
+ const option = propOptions[changedKey];
86
+ if (!option || typeof option !== "object" || Array.isArray(option)) continue;
87
+ const observer = option.observer;
88
+ if (typeof observer !== "function") continue;
89
+ observer.call(instance, instance.properties[changedKey], previousProperties[changedKey]);
90
+ }
91
+ }
92
+ function normalizeObserverPattern(pattern) {
93
+ return pattern.split(",").map((item) => item.trim()).filter(Boolean);
94
+ }
95
+ function resolveObservedValue(instance, pattern) {
96
+ if (pattern === "**") return {
97
+ ...instance.properties,
98
+ ...instance.data
99
+ };
100
+ const segments = parseDataPath$1(pattern);
101
+ if (segments.length === 0) return;
102
+ const [rootSegment, ...restSegments] = segments;
103
+ let current = (Object.hasOwn(instance.properties, rootSegment) ? instance.properties : instance.data)?.[rootSegment];
104
+ for (const segment of restSegments) {
105
+ const normalizedSegment = isArrayIndexSegment$1(segment) ? Number(segment) : segment;
106
+ current = current?.[normalizedSegment];
107
+ }
108
+ return current;
109
+ }
110
+ function matchObserverPattern(pattern, changedKeys) {
111
+ if (pattern === "**") return changedKeys.length > 0;
112
+ const patterns = normalizeObserverPattern(pattern);
113
+ return changedKeys.some((changedKey) => {
114
+ return patterns.some((item) => {
115
+ if (item === "**") return true;
116
+ if (changedKey === item) return true;
117
+ return changedKey.startsWith(`${item}.`) || changedKey.startsWith(`${item}[`);
118
+ });
119
+ });
120
+ }
121
+ function runComponentObservers(definition, instance, changedKeys, previousProperties = {}) {
122
+ if (changedKeys.length === 0) return;
123
+ runPropertyObservers(definition, instance, changedKeys, previousProperties);
124
+ const observers = definition.observers;
125
+ if (observers && typeof observers === "object" && !Array.isArray(observers)) for (const [pattern, handler] of Object.entries(observers)) {
126
+ if (typeof handler !== "function" || !matchObserverPattern(pattern, changedKeys)) continue;
127
+ const args = normalizeObserverPattern(pattern).map((item) => resolveObservedValue(instance, item));
128
+ handler.call(instance, ...args);
129
+ }
130
+ }
131
+ //#endregion
132
+ //#region ../../mpcore/packages/simulator/src/runtime/componentInstance/properties.ts
73
133
  function resolveInitialData$1(definition) {
74
134
  const rawData = definition.data;
75
135
  if (typeof rawData === "function") {
76
136
  const next = rawData.call(definition);
77
- return next && typeof next === "object" && !Array.isArray(next) ? cloneObject$1(next) : {};
137
+ return next && typeof next === "object" && !Array.isArray(next) ? cloneRecord(next) : {};
78
138
  }
79
- return rawData && typeof rawData === "object" && !Array.isArray(rawData) ? cloneObject$1(rawData) : {};
139
+ return rawData && typeof rawData === "object" && !Array.isArray(rawData) ? cloneRecord(rawData) : {};
80
140
  }
81
141
  function isBehaviorDefinition(value) {
82
142
  return !!value && typeof value === "object" && !Array.isArray(value) && value.__isHeadlessBehavior__ === true;
@@ -91,9 +151,6 @@ function flattenBehaviors(definition) {
91
151
  }
92
152
  return flattened;
93
153
  }
94
- function mergeRecord(...records) {
95
- return Object.assign({}, ...records.filter(Boolean));
96
- }
97
154
  function normalizeComponentDefinition(definition) {
98
155
  const behaviors = flattenBehaviors(definition);
99
156
  if (behaviors.length === 0) return definition;
@@ -200,61 +257,13 @@ function resolvePropertyDefaultValue(option) {
200
257
  if (!option || typeof option !== "object" || Array.isArray(option) || !("value" in option)) return;
201
258
  return cloneValue$1(option.value);
202
259
  }
203
- function runPropertyObservers(definition, instance, changedKeys, previousProperties) {
204
- const propOptions = definition.properties;
205
- if (!propOptions || typeof propOptions !== "object" || Array.isArray(propOptions)) return;
206
- for (const changedKey of changedKeys) {
207
- const option = propOptions[changedKey];
208
- if (!option || typeof option !== "object" || Array.isArray(option)) continue;
209
- const observer = option.observer;
210
- if (typeof observer !== "function") continue;
211
- observer.call(instance, instance.properties[changedKey], previousProperties[changedKey]);
212
- }
213
- }
214
- function normalizeObserverPattern(pattern) {
215
- return pattern.split(",").map((item) => item.trim()).filter(Boolean);
216
- }
217
- function resolveObservedValue(instance, pattern) {
218
- if (pattern === "**") return {
219
- ...instance.properties,
220
- ...instance.data
221
- };
222
- const segments = parseDataPath$1(pattern);
223
- if (segments.length === 0) return;
224
- const [rootSegment, ...restSegments] = segments;
225
- let current = (Object.hasOwn(instance.properties, rootSegment) ? instance.properties : instance.data)?.[rootSegment];
226
- for (const segment of restSegments) {
227
- const normalizedSegment = isArrayIndexSegment$1(segment) ? Number(segment) : segment;
228
- current = current?.[normalizedSegment];
229
- }
230
- return current;
231
- }
232
- function matchObserverPattern(pattern, changedKeys) {
233
- if (pattern === "**") return changedKeys.length > 0;
234
- const patterns = normalizeObserverPattern(pattern);
235
- return changedKeys.some((changedKey) => {
236
- return patterns.some((item) => {
237
- if (item === "**") return true;
238
- if (changedKey === item) return true;
239
- return changedKey.startsWith(`${item}.`) || changedKey.startsWith(`${item}[`);
240
- });
241
- });
242
- }
243
- function runComponentObservers(definition, instance, changedKeys, previousProperties = {}) {
244
- if (changedKeys.length === 0) return;
245
- runPropertyObservers(definition, instance, changedKeys, previousProperties);
246
- const observers = definition.observers;
247
- if (observers && typeof observers === "object" && !Array.isArray(observers)) for (const [pattern, handler] of Object.entries(observers)) {
248
- if (typeof handler !== "function" || !matchObserverPattern(pattern, changedKeys)) continue;
249
- const args = normalizeObserverPattern(pattern).map((item) => resolveObservedValue(instance, item));
250
- handler.call(instance, ...args);
251
- }
252
- }
253
260
  function normalizeComponentPropertyValue(definition, key, rawValue) {
254
261
  const option = definition.properties?.[key];
255
262
  if (rawValue == null) return resolvePropertyDefaultValue(option);
256
263
  return coerceComponentPropertyValue(rawValue, option);
257
264
  }
265
+ //#endregion
266
+ //#region ../../mpcore/packages/simulator/src/runtime/componentInstance/index.ts
258
267
  function createComponentInstance(options) {
259
268
  const definition = normalizeComponentDefinition(options.definition);
260
269
  const instance = {
@@ -2830,9 +2839,11 @@ function parseDocument(data, options) {
2830
2839
  return handler.root;
2831
2840
  }
2832
2841
  //#endregion
2833
- //#region ../../mpcore/packages/simulator/src/runtime/render.ts
2834
- const LEADING_SLASH_RE$4 = /^\/+/;
2842
+ //#region ../../mpcore/packages/simulator/src/runtime/render/shared.ts
2835
2843
  const TEMPLATE_INTERPOLATION_RE$1 = /\{\{([^{}]+)\}\}/g;
2844
+ const DATASET_NAME_RE$3 = /-([a-z])/g;
2845
+ const BRACKET_INDEX_RE = /\[(\d+)\]/g;
2846
+ const LEADING_SLASH_RE$4 = /^\/+/;
2836
2847
  const EVENT_BINDING_ATTRS = [
2837
2848
  "bindtap",
2838
2849
  "bind:tap",
@@ -2855,8 +2866,6 @@ const STRUCTURAL_ATTRS = [
2855
2866
  "wx:key"
2856
2867
  ];
2857
2868
  const WX_ELSE_ATTRS = new Set(["wx:elif", "wx:else"]);
2858
- const DATASET_NAME_RE$3 = /-([a-z])/g;
2859
- const BRACKET_INDEX_RE = /\[(\d+)\]/g;
2860
2869
  const CLASS_SPLIT_RE = /\s+/;
2861
2870
  const JS_FILE_RE = /\.js$/;
2862
2871
  function isMustacheOnly(value) {
@@ -2964,21 +2973,46 @@ function resolveComponentAttributeValue(value, scope) {
2964
2973
  }
2965
2974
  return interpolateTemplate$1(value, scope.data);
2966
2975
  }
2967
- function resolveUsingComponents(ownerJsonPath, ownerFilePath) {
2968
- try {
2969
- const usingComponents = JSON.parse(fs.readFileSync(ownerJsonPath, "utf8")).usingComponents;
2970
- if (!usingComponents || typeof usingComponents !== "object" || Array.isArray(usingComponents)) return /* @__PURE__ */ new Map();
2971
- const resolved = /* @__PURE__ */ new Map();
2972
- for (const [alias, rawPath] of Object.entries(usingComponents)) {
2973
- if (typeof rawPath !== "string") continue;
2974
- const basePath = rawPath.startsWith("/") ? rawPath.replace(LEADING_SLASH_RE$4, "") : path.posix.normalize(path.posix.join(path.posix.dirname(ownerFilePath), rawPath));
2975
- resolved.set(alias, basePath.replace(LEADING_SLASH_RE$4, ""));
2976
+ function applyNodeBindings(node, scope) {
2977
+ if (!isTagNode(node)) {
2978
+ if (node.type === "text" && typeof node.data === "string") node.data = interpolateTemplate$1(node.data, scope.data);
2979
+ return;
2980
+ }
2981
+ node.attribs ??= {};
2982
+ node.attribs["data-sim-scope"] = scope.getScopeId();
2983
+ for (const key of STRUCTURAL_ATTRS) delete node.attribs[key];
2984
+ for (const [key, value] of Object.entries({ ...node.attribs })) {
2985
+ if (EVENT_BINDING_ATTRS.includes(key)) {
2986
+ node.attribs["data-sim-tap"] = value;
2987
+ continue;
2976
2988
  }
2977
- return resolved;
2978
- } catch {
2979
- return /* @__PURE__ */ new Map();
2989
+ node.attribs[key] = typeof value === "string" ? String(resolveAttributeValue(value, scope)) : String(value);
2980
2990
  }
2981
2991
  }
2992
+ function createMergedScopeData(pageData, componentProperties, componentData) {
2993
+ return {
2994
+ ...pageData,
2995
+ ...componentProperties,
2996
+ ...componentData
2997
+ };
2998
+ }
2999
+ function evaluateConditionalBranch(node, scope) {
3000
+ const condition = node.attribs?.["wx:if"] ?? node.attribs?.["wx:elif"];
3001
+ if (condition == null) return true;
3002
+ return Boolean(resolveRawValueByPath(scope.data, condition));
3003
+ }
3004
+ function createLoopScope(scope, itemName, indexName, item, index) {
3005
+ return {
3006
+ ...scope,
3007
+ data: {
3008
+ ...scope.data,
3009
+ [indexName]: index,
3010
+ [itemName]: item
3011
+ }
3012
+ };
3013
+ }
3014
+ //#endregion
3015
+ //#region ../../mpcore/packages/simulator/src/runtime/render/component.ts
2982
3016
  function resolveComponentRegistryEntry(context, ownerJsonPath, ownerFilePath, alias) {
2983
3017
  const componentBasePath = resolveUsingComponents(ownerJsonPath, ownerFilePath).get(alias);
2984
3018
  if (!componentBasePath) return null;
@@ -2993,6 +3027,21 @@ function resolveComponentRegistryEntry(context, ownerJsonPath, ownerFilePath, al
2993
3027
  absoluteTemplatePath
2994
3028
  };
2995
3029
  }
3030
+ function resolveUsingComponents(ownerJsonPath, ownerFilePath) {
3031
+ try {
3032
+ const usingComponents = JSON.parse(fs.readFileSync(ownerJsonPath, "utf8")).usingComponents;
3033
+ if (!usingComponents || typeof usingComponents !== "object" || Array.isArray(usingComponents)) return /* @__PURE__ */ new Map();
3034
+ const resolved = /* @__PURE__ */ new Map();
3035
+ for (const [alias, rawPath] of Object.entries(usingComponents)) {
3036
+ if (typeof rawPath !== "string") continue;
3037
+ const basePath = rawPath.startsWith("/") ? rawPath.replace(LEADING_SLASH_RE$4, "") : path.posix.normalize(path.posix.join(path.posix.dirname(ownerFilePath), rawPath));
3038
+ resolved.set(alias, basePath.replace(LEADING_SLASH_RE$4, ""));
3039
+ }
3040
+ return resolved;
3041
+ } catch {
3042
+ return /* @__PURE__ */ new Map();
3043
+ }
3044
+ }
2996
3045
  function collectComponentEventBindings(hostNode) {
2997
3046
  const eventBindings = /* @__PURE__ */ new Map();
2998
3047
  for (const [key, value] of Object.entries(hostNode.attribs ?? {})) {
@@ -3043,22 +3092,6 @@ function buildComponentTrigger(componentScopeId, context, hostNode) {
3043
3092
  }
3044
3093
  };
3045
3094
  }
3046
- function applyNodeBindings(node, scope) {
3047
- if (!isTagNode(node)) {
3048
- if (node.type === "text" && typeof node.data === "string") node.data = interpolateTemplate$1(node.data, scope.data);
3049
- return;
3050
- }
3051
- node.attribs ??= {};
3052
- node.attribs["data-sim-scope"] = scope.getScopeId();
3053
- for (const key of STRUCTURAL_ATTRS) delete node.attribs[key];
3054
- for (const [key, value] of Object.entries({ ...node.attribs })) {
3055
- if (EVENT_BINDING_ATTRS.includes(key)) {
3056
- node.attribs["data-sim-tap"] = value;
3057
- continue;
3058
- }
3059
- node.attribs[key] = typeof value === "string" ? String(resolveAttributeValue(value, scope)) : String(value);
3060
- }
3061
- }
3062
3095
  function syncComponentProperties(instance, definition, nextProperties, bindingExpressions, changedPageKeys) {
3063
3096
  const changedRootKeys = [];
3064
3097
  const previousProperties = {};
@@ -3081,31 +3114,62 @@ function syncComponentProperties(instance, definition, nextProperties, bindingEx
3081
3114
  if (changedRootKeys.length === 0) return;
3082
3115
  runComponentObservers(definition, instance, changedRootKeys, previousProperties);
3083
3116
  }
3084
- function createMergedScopeData(pageData, componentProperties, componentData) {
3117
+ function createComponentScope(clonedNode, scope, componentScopeId, componentInstance) {
3118
+ const ownerScopeId = scope.getScopeId().includes("/") ? scope.getScopeId() : void 0;
3085
3119
  return {
3086
- ...pageData,
3087
- ...componentProperties,
3088
- ...componentData
3120
+ alias: clonedNode.name,
3121
+ classList: String(clonedNode.attribs?.class ?? "").split(CLASS_SPLIT_RE).map((item) => item.trim()).filter(Boolean),
3122
+ data: createMergedScopeData(scope.data, componentInstance.properties, componentInstance.data),
3123
+ dataset: collectDataset$3(clonedNode),
3124
+ eventBindings: collectComponentEventBindings(clonedNode),
3125
+ getMethod: (methodName) => {
3126
+ const method = componentInstance?.[methodName];
3127
+ return typeof method === "function" ? method : void 0;
3128
+ },
3129
+ getScopeId: () => componentScopeId,
3130
+ hostId: typeof clonedNode.attribs?.id === "string" ? clonedNode.attribs.id : void 0,
3131
+ id: typeof clonedNode.attribs?.id === "string" ? clonedNode.attribs.id : void 0,
3132
+ listenerScopeId: scope.getScopeId(),
3133
+ ownerScopeId
3089
3134
  };
3090
3135
  }
3091
- function isTruthy(value) {
3092
- return Boolean(value);
3093
- }
3094
- function createLoopScope(scope, itemName, indexName, item, index) {
3136
+ function resolveComponentProperties(clonedNode, scope) {
3137
+ const nextProperties = {};
3138
+ const bindingExpressions = {};
3139
+ for (const [key, value] of Object.entries(clonedNode.attribs ?? {})) {
3140
+ if (key.startsWith("bind")) continue;
3141
+ if (isMustacheOnly(String(value))) bindingExpressions[key] = String(value).trim().slice(2, -2).trim();
3142
+ nextProperties[key] = resolveComponentAttributeValue(String(value), scope);
3143
+ }
3095
3144
  return {
3096
- ...scope,
3097
- data: {
3098
- ...scope.data,
3099
- [indexName]: index,
3100
- [itemName]: item
3101
- }
3145
+ nextProperties,
3146
+ bindingExpressions
3102
3147
  };
3103
3148
  }
3104
- function evaluateConditionalBranch(node, scope) {
3105
- const condition = node.attribs?.["wx:if"] ?? node.attribs?.["wx:elif"];
3106
- if (condition == null) return true;
3107
- return isTruthy(resolveRawValueByPath(scope.data, condition));
3149
+ function createRuntimeComponentInstance(componentScopeId, context, clonedNode, componentEntry, nextProperties, ownerScopeId) {
3150
+ const componentInstance = createComponentInstance({
3151
+ definition: componentEntry.definition,
3152
+ properties: nextProperties,
3153
+ triggerEvent: buildComponentTrigger(componentScopeId, context, clonedNode)
3154
+ });
3155
+ componentInstance.createIntersectionObserver = (options) => context.session.createIntersectionObserver(componentInstance, options);
3156
+ componentInstance.createMediaQueryObserver = () => context.session.createMediaQueryObserver(componentInstance);
3157
+ componentInstance.selectComponent = (selector) => context.session.selectComponentWithin(componentScopeId, selector);
3158
+ componentInstance.selectAllComponents = (selector) => context.session.selectAllComponentsWithin(componentScopeId, selector);
3159
+ componentInstance.selectOwnerComponent = () => ownerScopeId ? context.session.selectOwnerComponent(componentScopeId) : null;
3160
+ runComponentLifecycle(componentInstance, "created");
3161
+ runComponentObservers(componentInstance.__definition__ ?? componentEntry.definition, componentInstance, Object.keys(nextProperties), {});
3162
+ componentInstance.__propertySnapshots = Object.fromEntries(Object.entries(componentInstance.properties).map(([key, propertyValue]) => [key, cloneValue$1(propertyValue)]));
3163
+ runComponentLifecycle(componentInstance, "attached");
3164
+ context.componentCache.set(componentScopeId, componentInstance);
3165
+ return componentInstance;
3166
+ }
3167
+ function renderRuntimeComponentTemplate(context, componentEntry, renderNodeTree, componentScope, componentScopeId, seenComponentScopes) {
3168
+ const componentDocument = parseTemplateDocument(readTemplateSource(componentEntry.absoluteTemplatePath));
3169
+ return renderNodeTree((componentDocument.children ?? [])[0] ?? componentDocument, componentScope, context, path.resolve(context.project.miniprogramRootPath, `${componentEntry.filePath.replace(JS_FILE_RE, "")}.json`), componentEntry.filePath, componentScopeId, seenComponentScopes);
3108
3170
  }
3171
+ //#endregion
3172
+ //#region ../../mpcore/packages/simulator/src/runtime/render/index.ts
3109
3173
  function expandNodeByFor(node, scope) {
3110
3174
  const forExpression = node.attribs?.["wx:for"];
3111
3175
  if (!forExpression) return [{
@@ -3175,51 +3239,14 @@ function renderNodeTree(node, scope, context, ownerJsonPath, ownerFilePath, inst
3175
3239
  if (componentEntry) {
3176
3240
  const componentScopeId = `${instancePath}/${clonedNode.name}`;
3177
3241
  const ownerScopeId = scope.getScopeId().includes("/") ? scope.getScopeId() : void 0;
3178
- const nextProperties = {};
3179
- const bindingExpressions = {};
3180
- for (const [key, value] of Object.entries(clonedNode.attribs ?? {})) {
3181
- if (key.startsWith("bind")) continue;
3182
- if (isMustacheOnly(String(value))) bindingExpressions[key] = String(value).trim().slice(2, -2).trim();
3183
- nextProperties[key] = resolveComponentAttributeValue(String(value), scope);
3184
- }
3242
+ const { nextProperties, bindingExpressions } = resolveComponentProperties(clonedNode, scope);
3185
3243
  let componentInstance = context.componentCache.get(componentScopeId);
3186
- if (!componentInstance) {
3187
- componentInstance = createComponentInstance({
3188
- definition: componentEntry.definition,
3189
- properties: nextProperties,
3190
- triggerEvent: buildComponentTrigger(componentScopeId, context, clonedNode)
3191
- });
3192
- componentInstance.createIntersectionObserver = (options) => context.session.createIntersectionObserver(componentInstance, options);
3193
- componentInstance.createMediaQueryObserver = () => context.session.createMediaQueryObserver(componentInstance);
3194
- componentInstance.selectComponent = (selector) => context.session.selectComponentWithin(componentScopeId, selector);
3195
- componentInstance.selectAllComponents = (selector) => context.session.selectAllComponentsWithin(componentScopeId, selector);
3196
- componentInstance.selectOwnerComponent = () => ownerScopeId ? context.session.selectOwnerComponent(componentScopeId) : null;
3197
- runComponentLifecycle(componentInstance, "created");
3198
- runComponentObservers(componentInstance.__definition__ ?? componentEntry.definition, componentInstance, Object.keys(nextProperties), {});
3199
- componentInstance.__propertySnapshots = Object.fromEntries(Object.entries(componentInstance.properties).map(([key, propertyValue]) => [key, cloneValue$1(propertyValue)]));
3200
- runComponentLifecycle(componentInstance, "attached");
3201
- context.componentCache.set(componentScopeId, componentInstance);
3202
- } else syncComponentProperties(componentInstance, componentInstance.__definition__ ?? componentEntry.definition, nextProperties, bindingExpressions, context.changedPageKeys);
3244
+ if (!componentInstance) componentInstance = createRuntimeComponentInstance(componentScopeId, context, clonedNode, componentEntry, nextProperties, ownerScopeId);
3245
+ else syncComponentProperties(componentInstance, componentInstance.__definition__ ?? componentEntry.definition, nextProperties, bindingExpressions, context.changedPageKeys);
3203
3246
  seenComponentScopes.add(componentScopeId);
3204
- const componentScope = {
3205
- alias: clonedNode.name,
3206
- classList: String(clonedNode.attribs?.class ?? "").split(CLASS_SPLIT_RE).map((item) => item.trim()).filter(Boolean),
3207
- data: createMergedScopeData(scope.data, componentInstance.properties, componentInstance.data),
3208
- dataset: collectDataset$3(clonedNode),
3209
- eventBindings: collectComponentEventBindings(clonedNode),
3210
- getMethod: (methodName) => {
3211
- const method = componentInstance?.[methodName];
3212
- return typeof method === "function" ? method : void 0;
3213
- },
3214
- getScopeId: () => componentScopeId,
3215
- hostId: typeof clonedNode.attribs?.id === "string" ? clonedNode.attribs.id : void 0,
3216
- id: typeof clonedNode.attribs?.id === "string" ? clonedNode.attribs.id : void 0,
3217
- listenerScopeId: scope.getScopeId(),
3218
- ownerScopeId
3219
- };
3247
+ const componentScope = createComponentScope(clonedNode, scope, componentScopeId, componentInstance);
3220
3248
  context.componentScopes.set(componentScopeId, componentScope);
3221
- const componentDocument = parseTemplateDocument(readTemplateSource(componentEntry.absoluteTemplatePath));
3222
- const renderedComponentRoot = renderNodeTree((componentDocument.children ?? [])[0] ?? componentDocument, componentScope, context, path.resolve(context.project.miniprogramRootPath, `${componentEntry.filePath.replace(JS_FILE_RE, "")}.json`), componentEntry.filePath, componentScopeId, seenComponentScopes);
3249
+ const renderedComponentRoot = renderRuntimeComponentTemplate(context, componentEntry, renderNodeTree, componentScope, componentScopeId, seenComponentScopes);
3223
3250
  if (renderedComponentRoot.attribs) renderedComponentRoot.attribs["data-sim-component"] = clonedNode.name;
3224
3251
  if (!componentInstance.__ready__) {
3225
3252
  runComponentLifecycle(componentInstance, "ready");
@@ -3291,7 +3318,7 @@ function registerComponentDefinition(registries, definition) {
3291
3318
  return definition;
3292
3319
  }
3293
3320
  //#endregion
3294
- //#region ../../mpcore/packages/simulator/src/host/wx.ts
3321
+ //#region ../../mpcore/packages/simulator/src/host/wx/index.ts
3295
3322
  function invokeWxApi(operation, option) {
3296
3323
  try {
3297
3324
  const result = operation();
@@ -3534,10 +3561,7 @@ function createHeadlessWx(driver) {
3534
3561
  chooseMedia: (option) => invokeWxApi(() => driver.chooseMedia(option ?? {}), option),
3535
3562
  chooseVideo: (option) => invokeWxApi(() => driver.chooseVideo(option ?? {}), option),
3536
3563
  compressImage: (option) => invokeWxApi(() => driver.compressImage(option), option),
3537
- clearStorage: (option) => invokeWxApi(() => {
3538
- driver.clearStorageSync();
3539
- return { errMsg: "clearStorage:ok" };
3540
- }, option),
3564
+ clearStorage: (option) => invokeWxApi(() => ({ errMsg: (driver.clearStorageSync(), "clearStorage:ok") }), option),
3541
3565
  clearStorageSync: () => driver.clearStorageSync(),
3542
3566
  createAnimation: (option) => driver.createAnimation(option),
3543
3567
  createCanvasContext: (canvasId, component) => driver.createCanvasContext(canvasId, component),
@@ -5515,10 +5539,12 @@ function inferImageType(filePath, fileContent) {
5515
5539
  }
5516
5540
  function inferImageSize(fileContent) {
5517
5541
  try {
5518
- const payload = JSON.parse(fileContent);
5542
+ const config = JSON.parse(fileContent)?.config;
5543
+ const destHeight = config && "destHeight" in config ? config.destHeight : void 0;
5544
+ const destWidth = config && "destWidth" in config ? config.destWidth : void 0;
5519
5545
  return {
5520
- height: payload?.config?.destHeight ?? payload?.config?.height ?? 0,
5521
- width: payload?.config?.destWidth ?? payload?.config?.width ?? 0
5546
+ height: destHeight ?? config?.height ?? 0,
5547
+ width: destWidth ?? config?.width ?? 0
5522
5548
  };
5523
5549
  } catch {
5524
5550
  return {
@@ -6299,7 +6325,7 @@ function createHeadlessWxState() {
6299
6325
  width
6300
6326
  };
6301
6327
  }),
6302
- type: mediaTypes.length > 1 ? "mix" : mediaTypes[0]
6328
+ type: mediaTypes.length > 1 ? "mix" : mediaTypes[0] ?? "image"
6303
6329
  };
6304
6330
  },
6305
6331
  chooseVideo(option) {
@@ -7826,7 +7852,7 @@ var HeadlessTestingPageHandle = class {
7826
7852
  }
7827
7853
  };
7828
7854
  //#endregion
7829
- //#region ../../mpcore/packages/simulator/src/testing/sessionHandle.ts
7855
+ //#region ../../mpcore/packages/simulator/src/testing/sessionHandle/shared.ts
7830
7856
  const LEADING_SLASH_RE = /^\/+/;
7831
7857
  const DEFAULT_WAIT_TIMEOUT = 1e3;
7832
7858
  const DEFAULT_WAIT_INTERVAL = 10;
@@ -7837,6 +7863,28 @@ function resolvePageScopeId(scopeId) {
7837
7863
  const pagePathIndex = scopeId.indexOf("/page/");
7838
7864
  return pagePathIndex >= 0 ? scopeId.slice(0, pagePathIndex) : scopeId;
7839
7865
  }
7866
+ function normalizeNonEmptyInput(value, label) {
7867
+ const normalizedValue = value.trim();
7868
+ if (!normalizedValue) throw new Error(`${label} must be a non-empty string in headless testing runtime.`);
7869
+ return normalizedValue;
7870
+ }
7871
+ async function waitForDelay(ms = 0) {
7872
+ if (ms <= 0) return;
7873
+ await new Promise((resolve) => setTimeout(resolve, ms));
7874
+ }
7875
+ async function pollUntil(check, errorMessage, options = {}) {
7876
+ const timeout = Number.isFinite(options.timeout) ? Math.max(0, Math.trunc(options.timeout ?? DEFAULT_WAIT_TIMEOUT)) : DEFAULT_WAIT_TIMEOUT;
7877
+ const interval = Number.isFinite(options.interval) ? Math.max(1, Math.trunc(options.interval ?? DEFAULT_WAIT_INTERVAL)) : DEFAULT_WAIT_INTERVAL;
7878
+ const deadline = Date.now() + timeout;
7879
+ while (true) {
7880
+ const result = await check();
7881
+ if (result != null) return result;
7882
+ if (Date.now() >= deadline) throw new Error(errorMessage);
7883
+ await waitForDelay(interval);
7884
+ }
7885
+ }
7886
+ //#endregion
7887
+ //#region ../../mpcore/packages/simulator/src/testing/sessionHandle/scope.ts
7840
7888
  var HeadlessTestingScopeHandle = class HeadlessTestingScopeHandle {
7841
7889
  constructor(scopeId, project, session) {
7842
7890
  this.scopeId = scopeId;
@@ -7848,8 +7896,7 @@ var HeadlessTestingScopeHandle = class HeadlessTestingScopeHandle {
7848
7896
  return scopeId ? new HeadlessTestingScopeHandle(scopeId, this.project, this.session) : null;
7849
7897
  }
7850
7898
  async callMethod(methodName, ...args) {
7851
- const normalizedMethodName = methodName.trim();
7852
- if (!normalizedMethodName) throw new Error("Method name must be a non-empty string in headless testing runtime.");
7899
+ const normalizedMethodName = normalizeNonEmptyInput(methodName, "Method name");
7853
7900
  return this.session.callScopeMethodDirect(this.scopeId, normalizedMethodName, ...args);
7854
7901
  }
7855
7902
  async snapshot() {
@@ -7866,75 +7913,37 @@ var HeadlessTestingScopeHandle = class HeadlessTestingScopeHandle {
7866
7913
  return this.createScopeHandle(this.session.selectOwnerComponent(this.scopeId));
7867
7914
  }
7868
7915
  async waitForOwnerComponent(options = {}) {
7869
- const timeout = Number.isFinite(options.timeout) ? Math.max(0, Math.trunc(options.timeout ?? 1e3)) : 1e3;
7870
- const interval = Number.isFinite(options.interval) ? Math.max(1, Math.trunc(options.interval ?? 10)) : 10;
7871
- const deadline = Date.now() + timeout;
7872
- while (true) {
7873
- const owner = await this.ownerComponent();
7874
- if (owner) return owner;
7875
- if (Date.now() >= deadline) throw new Error("Timed out waiting for owner component in headless testing runtime.");
7876
- await new Promise((resolve) => setTimeout(resolve, interval));
7877
- }
7916
+ return await pollUntil(async () => await this.ownerComponent(), "Timed out waiting for owner component in headless testing runtime.", options);
7878
7917
  }
7879
7918
  async selectComponent(selector) {
7880
- const normalizedSelector = selector.trim();
7881
- if (!normalizedSelector) throw new Error("Selector must be a non-empty string in headless testing runtime.");
7919
+ const normalizedSelector = normalizeNonEmptyInput(selector, "Selector");
7882
7920
  const component = this.session.selectComponentWithin(this.scopeId, normalizedSelector);
7883
7921
  return this.createScopeHandle(component);
7884
7922
  }
7885
7923
  async selectAllComponents(selector) {
7886
- const normalizedSelector = selector.trim();
7887
- if (!normalizedSelector) throw new Error("Selector must be a non-empty string in headless testing runtime.");
7924
+ const normalizedSelector = normalizeNonEmptyInput(selector, "Selector");
7888
7925
  return this.session.selectAllComponentsWithin(this.scopeId, normalizedSelector).map((component) => this.createScopeHandle(component)).filter((handle) => Boolean(handle));
7889
7926
  }
7890
7927
  async waitForComponent(selector, options = {}) {
7891
- const normalizedSelector = selector.trim();
7892
- if (!normalizedSelector) throw new Error("Selector must be a non-empty string in headless testing runtime.");
7893
- const timeout = Number.isFinite(options.timeout) ? Math.max(0, Math.trunc(options.timeout ?? 1e3)) : 1e3;
7894
- const interval = Number.isFinite(options.interval) ? Math.max(1, Math.trunc(options.interval ?? 10)) : 10;
7895
- const deadline = Date.now() + timeout;
7896
- while (true) {
7897
- const component = await this.selectComponent(normalizedSelector);
7898
- if (component) return component;
7899
- if (Date.now() >= deadline) throw new Error(`Timed out waiting for component "${normalizedSelector}" in headless testing runtime.`);
7900
- await new Promise((resolve) => setTimeout(resolve, interval));
7901
- }
7928
+ const normalizedSelector = normalizeNonEmptyInput(selector, "Selector");
7929
+ return await pollUntil(async () => await this.selectComponent(normalizedSelector), `Timed out waiting for component "${normalizedSelector}" in headless testing runtime.`, options);
7902
7930
  }
7903
7931
  async waitForComponents(selector, count = 1, options = {}) {
7904
- const normalizedSelector = selector.trim();
7905
- if (!normalizedSelector) throw new Error("Selector must be a non-empty string in headless testing runtime.");
7932
+ const normalizedSelector = normalizeNonEmptyInput(selector, "Selector");
7906
7933
  const normalizedCount = Number.isFinite(count) ? Math.max(1, Math.trunc(count)) : 1;
7907
- const timeout = Number.isFinite(options.timeout) ? Math.max(0, Math.trunc(options.timeout ?? 1e3)) : 1e3;
7908
- const interval = Number.isFinite(options.interval) ? Math.max(1, Math.trunc(options.interval ?? 10)) : 10;
7909
- const deadline = Date.now() + timeout;
7910
- while (true) {
7934
+ return await pollUntil(async () => {
7911
7935
  const components = await this.selectAllComponents(normalizedSelector);
7912
- if (components.length >= normalizedCount) return components;
7913
- if (Date.now() >= deadline) throw new Error(`Timed out waiting for ${normalizedCount} component(s) matching "${normalizedSelector}" in headless testing runtime.`);
7914
- await new Promise((resolve) => setTimeout(resolve, interval));
7915
- }
7936
+ return components.length >= normalizedCount ? components : null;
7937
+ }, `Timed out waiting for ${normalizedCount} component(s) matching "${normalizedSelector}" in headless testing runtime.`, options);
7916
7938
  }
7917
7939
  };
7940
+ //#endregion
7941
+ //#region ../../mpcore/packages/simulator/src/testing/sessionHandle/index.ts
7918
7942
  var HeadlessTestingSessionHandle = class {
7919
7943
  constructor(project, session) {
7920
7944
  this.project = project;
7921
7945
  this.session = session;
7922
7946
  }
7923
- async waitFor(ms = 0) {
7924
- if (ms <= 0) return;
7925
- await new Promise((resolve) => setTimeout(resolve, ms));
7926
- }
7927
- async pollUntil(check, errorMessage, options = {}) {
7928
- const timeout = Number.isFinite(options.timeout) ? Math.max(0, Math.trunc(options.timeout ?? DEFAULT_WAIT_TIMEOUT)) : DEFAULT_WAIT_TIMEOUT;
7929
- const interval = Number.isFinite(options.interval) ? Math.max(1, Math.trunc(options.interval ?? DEFAULT_WAIT_INTERVAL)) : DEFAULT_WAIT_INTERVAL;
7930
- const deadline = Date.now() + timeout;
7931
- while (true) {
7932
- const result = await check();
7933
- if (result != null) return result;
7934
- if (Date.now() >= deadline) throw new Error(errorMessage);
7935
- await this.waitFor(interval);
7936
- }
7937
- }
7938
7947
  async close() {}
7939
7948
  async currentPage() {
7940
7949
  const current = this.session.getCurrentPages().at(-1);
@@ -7944,32 +7953,27 @@ var HeadlessTestingSessionHandle = class {
7944
7953
  return this.session.getCurrentPages().map((page) => new HeadlessTestingPageHandle(this.project, page, this.session));
7945
7954
  }
7946
7955
  async scopeSnapshot(scopeId) {
7947
- const normalizedScopeId = scopeId.trim();
7948
- if (!normalizedScopeId) throw new Error("Scope id must be a non-empty string in headless testing runtime.");
7956
+ const normalizedScopeId = normalizeNonEmptyInput(scopeId, "Scope id");
7949
7957
  return this.session.getScopeSnapshot(normalizedScopeId);
7950
7958
  }
7951
7959
  async selectComponent(selector) {
7952
- const normalizedSelector = selector.trim();
7953
- if (!normalizedSelector) throw new Error("Selector must be a non-empty string in headless testing runtime.");
7960
+ const normalizedSelector = normalizeNonEmptyInput(selector, "Selector");
7954
7961
  const component = this.session.selectComponent(normalizedSelector);
7955
7962
  const scopeId = this.session.getScopeIdForComponent(component);
7956
7963
  return scopeId ? new HeadlessTestingScopeHandle(scopeId, this.project, this.session) : null;
7957
7964
  }
7958
7965
  async selectAllComponents(selector) {
7959
- const normalizedSelector = selector.trim();
7960
- if (!normalizedSelector) throw new Error("Selector must be a non-empty string in headless testing runtime.");
7966
+ const normalizedSelector = normalizeNonEmptyInput(selector, "Selector");
7961
7967
  return this.session.selectAllComponents(normalizedSelector).map((component) => this.session.getScopeIdForComponent(component)).filter((scopeId) => Boolean(scopeId)).map((scopeId) => new HeadlessTestingScopeHandle(scopeId, this.project, this.session));
7962
7968
  }
7963
7969
  async waitForComponent(selector, options = {}) {
7964
- const normalizedSelector = selector.trim();
7965
- if (!normalizedSelector) throw new Error("Selector must be a non-empty string in headless testing runtime.");
7966
- return await this.pollUntil(async () => await this.selectComponent(normalizedSelector), `Timed out waiting for component "${normalizedSelector}" in headless testing runtime.`, options);
7970
+ const normalizedSelector = normalizeNonEmptyInput(selector, "Selector");
7971
+ return await pollUntil(async () => await this.selectComponent(normalizedSelector), `Timed out waiting for component "${normalizedSelector}" in headless testing runtime.`, options);
7967
7972
  }
7968
7973
  async waitForComponents(selector, count = 1, options = {}) {
7969
- const normalizedSelector = selector.trim();
7970
- if (!normalizedSelector) throw new Error("Selector must be a non-empty string in headless testing runtime.");
7974
+ const normalizedSelector = normalizeNonEmptyInput(selector, "Selector");
7971
7975
  const normalizedCount = Number.isFinite(count) ? Math.max(1, Math.trunc(count)) : 1;
7972
- return await this.pollUntil(async () => {
7976
+ return await pollUntil(async () => {
7973
7977
  const components = await this.selectAllComponents(normalizedSelector);
7974
7978
  return components.length >= normalizedCount ? components : null;
7975
7979
  }, `Timed out waiting for ${normalizedCount} component(s) matching "${normalizedSelector}" in headless testing runtime.`, options);
@@ -7981,7 +7985,7 @@ var HeadlessTestingSessionHandle = class {
7981
7985
  async waitForCurrentPage(route, options = {}) {
7982
7986
  const normalizedRoute = route?.trim();
7983
7987
  if (normalizedRoute != null && !normalizedRoute) throw new Error("Route must be a non-empty string in headless testing runtime.");
7984
- return await this.pollUntil(async () => {
7988
+ return await pollUntil(async () => {
7985
7989
  const currentPageInstance = this.session.getCurrentPages().at(-1);
7986
7990
  if (!currentPageInstance) return null;
7987
7991
  const current = new HeadlessTestingPageHandle(this.project, currentPageInstance, this.session);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@weapp-vite/miniprogram-automator",
3
3
  "type": "module",
4
- "version": "1.0.2",
4
+ "version": "1.0.4",
5
5
  "description": "完全兼容微信 miniprogram-automator 的现代化替代实现",
6
6
  "author": "ice breaker <1324318532@qq.com>",
7
7
  "license": "MIT",