@unotest/mobile 0.8.3 → 0.10.0

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.
@@ -0,0 +1,1056 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+
4
+ // src/dsl/mobile-dsl-language-service.ts
5
+ import { Lexer } from "@unotest/dsl";
6
+ import { Parser } from "@unotest/dsl";
7
+
8
+ // src/dsl/function-registry.ts
9
+ var FunctionRegistry = class {
10
+ static {
11
+ __name(this, "FunctionRegistry");
12
+ }
13
+ map = /* @__PURE__ */ new Map();
14
+ register(fn) {
15
+ if (this.map.has(fn.name)) {
16
+ throw new Error(`Duplicate DSL function: ${fn.name}`);
17
+ }
18
+ this.map.set(fn.name, fn);
19
+ }
20
+ has(name) {
21
+ return this.map.has(name);
22
+ }
23
+ get(name) {
24
+ return this.map.get(name);
25
+ }
26
+ names() {
27
+ return [...this.map.keys()].sort();
28
+ }
29
+ };
30
+
31
+ // src/dsl/functions/alerts.ts
32
+ function asString(x, fn, idx) {
33
+ if (typeof x !== "string") throw new Error(`${fn}(): arg ${idx} must be a string. Got ${typeof x}.`);
34
+ return x;
35
+ }
36
+ __name(asString, "asString");
37
+ var acceptAlert = {
38
+ name: "acceptAlert",
39
+ argTypes: ["string"],
40
+ returnType: "void",
41
+ minArgs: 0,
42
+ invoke: /* @__PURE__ */ __name(async (runtime, buttonArg) => {
43
+ const button = buttonArg !== void 0 ? asString(buttonArg, "acceptAlert", 0) : void 0;
44
+ await runtime.driver.acceptAlert(runtime.currentDeviceSlot, button);
45
+ }, "invoke")
46
+ };
47
+ var dismissAlert = {
48
+ name: "dismissAlert",
49
+ argTypes: [],
50
+ returnType: "void",
51
+ invoke: /* @__PURE__ */ __name(async (runtime) => {
52
+ await runtime.driver.dismissAlert(runtime.currentDeviceSlot);
53
+ }, "invoke")
54
+ };
55
+ var readAlert = {
56
+ name: "readAlert",
57
+ argTypes: [],
58
+ returnType: "string",
59
+ invoke: /* @__PURE__ */ __name(async (runtime) => {
60
+ const { text } = await runtime.driver.readAlert(runtime.currentDeviceSlot);
61
+ return text;
62
+ }, "invoke")
63
+ };
64
+ var ALERT_FUNCTIONS = [acceptAlert, dismissAlert, readAlert];
65
+
66
+ // src/dsl/functions/asserts.ts
67
+ function asSelector(x, fn, idx) {
68
+ if (typeof x !== "object" || x === null) {
69
+ throw new Error(`${fn}(): arg ${idx} must be a Selector. Got ${typeof x}.`);
70
+ }
71
+ return x;
72
+ }
73
+ __name(asSelector, "asSelector");
74
+ function asNumber(x, fn, idx) {
75
+ if (typeof x !== "number") throw new Error(`${fn}(): arg ${idx} must be a number. Got ${typeof x}.`);
76
+ return x;
77
+ }
78
+ __name(asNumber, "asNumber");
79
+ var assertEqual = {
80
+ name: "assertEqual",
81
+ argTypes: ["any", "any"],
82
+ returnType: "void",
83
+ invoke: /* @__PURE__ */ __name((_runtime, actual, expected) => {
84
+ if (actual !== expected) {
85
+ throw new Error(
86
+ `assertEqual failed: actual ${JSON.stringify(actual)} !== expected ${JSON.stringify(expected)}`
87
+ );
88
+ }
89
+ }, "invoke")
90
+ };
91
+ var assertVisible = {
92
+ name: "assertVisible",
93
+ argTypes: ["selector"],
94
+ returnType: "void",
95
+ invoke: /* @__PURE__ */ __name(async (runtime, selectorArg) => {
96
+ const selector = asSelector(selectorArg, "assertVisible", 0);
97
+ const tree = await runtime.driver.a11yTree(runtime.currentDeviceSlot);
98
+ const r = runtime.selectorResolver.resolve(tree, selector);
99
+ if (!r.ok) {
100
+ throw new Error(
101
+ `assertVisible failed: ${r.reason}
102
+ selector: ${JSON.stringify(selector)}
103
+ candidates: ${JSON.stringify(r.candidates).slice(0, 600)}`
104
+ );
105
+ }
106
+ }, "invoke")
107
+ };
108
+ var assertCount = {
109
+ name: "assertCount",
110
+ argTypes: ["selector", "number"],
111
+ returnType: "void",
112
+ invoke: /* @__PURE__ */ __name(async (runtime, selectorArg, expectedArg) => {
113
+ const selector = asSelector(selectorArg, "assertCount", 0);
114
+ const expected = asNumber(expectedArg, "assertCount", 1);
115
+ const tree = await runtime.driver.a11yTree(runtime.currentDeviceSlot);
116
+ let count = 0;
117
+ while (true) {
118
+ const probe = { ...selector, ordinal: count };
119
+ const r = runtime.selectorResolver.resolve(tree, probe);
120
+ if (!r.ok) break;
121
+ count++;
122
+ if (count > 1e3) {
123
+ throw new Error(`assertCount: aborting after 1000 \u2014 selector too broad`);
124
+ }
125
+ }
126
+ if (count !== expected) {
127
+ throw new Error(
128
+ `assertCount failed: ${JSON.stringify(selector)} matched ${count} time(s), expected ${expected}`
129
+ );
130
+ }
131
+ }, "invoke")
132
+ };
133
+ async function assertEnabledState(runtime, selectorArg, expected, fnName) {
134
+ const selector = asSelector(selectorArg, fnName, 0);
135
+ const tree = await runtime.driver.a11yTree(runtime.currentDeviceSlot);
136
+ const r = runtime.selectorResolver.resolve(tree, selector);
137
+ if (!r.ok) {
138
+ throw new Error(
139
+ `${fnName} failed: selector did not match. ${r.reason}
140
+ selector: ${JSON.stringify(selector)}`
141
+ );
142
+ }
143
+ const node = r.node;
144
+ if (node.enabled === void 0) {
145
+ throw new Error(
146
+ `${fnName} failed: matched node does not report 'enabled' (likely a non-interactive widget or RN-side accessibility gap). Selector: ${JSON.stringify(selector)}`
147
+ );
148
+ }
149
+ if (node.enabled !== expected) {
150
+ throw new Error(
151
+ `${fnName} failed: node.enabled=${node.enabled}, expected ${expected}. Selector: ${JSON.stringify(selector)}`
152
+ );
153
+ }
154
+ }
155
+ __name(assertEnabledState, "assertEnabledState");
156
+ var assertEnabled = {
157
+ name: "assertEnabled",
158
+ argTypes: ["selector"],
159
+ returnType: "void",
160
+ invoke: /* @__PURE__ */ __name(async (runtime, selectorArg) => {
161
+ await assertEnabledState(runtime, selectorArg, true, "assertEnabled");
162
+ }, "invoke")
163
+ };
164
+ var assertDisabled = {
165
+ name: "assertDisabled",
166
+ argTypes: ["selector"],
167
+ returnType: "void",
168
+ invoke: /* @__PURE__ */ __name(async (runtime, selectorArg) => {
169
+ await assertEnabledState(runtime, selectorArg, false, "assertDisabled");
170
+ }, "invoke")
171
+ };
172
+ var ASSERT_FUNCTIONS = [
173
+ assertEqual,
174
+ assertVisible,
175
+ assertCount,
176
+ assertEnabled,
177
+ assertDisabled
178
+ ];
179
+
180
+ // src/dsl/functions/data.ts
181
+ function asString2(x, fn, idx) {
182
+ if (typeof x !== "string") throw new Error(`${fn}(): arg ${idx} must be a string. Got ${typeof x}.`);
183
+ return x;
184
+ }
185
+ __name(asString2, "asString");
186
+ function toSqlParam(v) {
187
+ if (v === null || v === void 0) return null;
188
+ if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") return v;
189
+ throw new Error(`dbQuery/dbExec arg type unsupported: ${typeof v}`);
190
+ }
191
+ __name(toSqlParam, "toSqlParam");
192
+ var dbQuery = {
193
+ name: "dbQuery",
194
+ argTypes: ["string"],
195
+ returnType: "string",
196
+ variadic: true,
197
+ invoke: /* @__PURE__ */ __name(async (runtime, sqlArg, ...rest) => {
198
+ const sql = asString2(sqlArg, "dbQuery", 0);
199
+ const params = rest.map((a, i) => {
200
+ try {
201
+ return toSqlParam(a);
202
+ } catch (e) {
203
+ throw new Error(`dbQuery: arg ${i + 1} ${e.message}`);
204
+ }
205
+ });
206
+ runtime.logger.debug(`dbQuery ${sql.slice(0, 200)} params=${JSON.stringify(params).slice(0, 200)}`);
207
+ const rows = await runtime.db.query(sql, params);
208
+ if (rows.length === 0) return "";
209
+ const firstKey = Object.keys(rows[0])[0];
210
+ if (!firstKey) return "";
211
+ const cell = rows[0][firstKey];
212
+ return cell == null ? "" : String(cell);
213
+ }, "invoke")
214
+ };
215
+ var dbExec = {
216
+ name: "dbExec",
217
+ argTypes: ["string"],
218
+ returnType: "string",
219
+ variadic: true,
220
+ invoke: /* @__PURE__ */ __name(async (runtime, sqlArg, ...rest) => {
221
+ const sql = asString2(sqlArg, "dbExec", 0);
222
+ const params = rest.map((a, i) => {
223
+ try {
224
+ return toSqlParam(a);
225
+ } catch (e) {
226
+ throw new Error(`dbExec: arg ${i + 1} ${e.message}`);
227
+ }
228
+ });
229
+ runtime.logger.debug(`dbExec ${sql.slice(0, 200)} params=${JSON.stringify(params).slice(0, 200)}`);
230
+ const result = await runtime.db.exec(sql, params);
231
+ if (result.rows.length === 0) return "";
232
+ const firstRow = result.rows[0];
233
+ const firstKey = Object.keys(firstRow)[0];
234
+ if (!firstKey) return "";
235
+ const cell = firstRow[firstKey];
236
+ return cell == null ? "" : String(cell);
237
+ }, "invoke")
238
+ };
239
+ var shell = {
240
+ name: "shell",
241
+ argTypes: ["string"],
242
+ returnType: "string",
243
+ variadic: true,
244
+ invoke: /* @__PURE__ */ __name(async (runtime, cmdArg, ...rest) => {
245
+ const cmd = asString2(cmdArg, "shell", 0);
246
+ const args = rest.map((a, i) => {
247
+ if (a === null || a === void 0) return "";
248
+ if (typeof a === "string") return a;
249
+ if (typeof a === "number" || typeof a === "boolean") return String(a);
250
+ throw new Error(`shell: arg ${i + 1} must be string|number|boolean|null. Got ${typeof a}.`);
251
+ });
252
+ runtime.logger.debug(`shell ${cmd} ${args.join(" ")}`);
253
+ const result = await runtime.shell.exec(cmd, args);
254
+ return result.stdout;
255
+ }, "invoke")
256
+ };
257
+ var apiCall = {
258
+ name: "apiCall",
259
+ argTypes: ["string", "string", "string"],
260
+ returnType: "string",
261
+ minArgs: 2,
262
+ invoke: /* @__PURE__ */ __name(async (runtime, methodArg, pathArg, bodyArg) => {
263
+ const method = asString2(methodArg, "apiCall", 0).toUpperCase();
264
+ const path = asString2(pathArg, "apiCall", 1);
265
+ if (method !== "GET" && method !== "POST" && method !== "PUT" && method !== "PATCH" && method !== "DELETE") {
266
+ throw new Error(`apiCall(): unsupported method "${method}"`);
267
+ }
268
+ let body;
269
+ if (bodyArg !== void 0) {
270
+ const bodyJson = asString2(bodyArg, "apiCall", 2);
271
+ try {
272
+ body = JSON.parse(bodyJson);
273
+ } catch (e) {
274
+ throw new Error(`apiCall(): body arg must be valid JSON. ${e.message}`);
275
+ }
276
+ }
277
+ runtime.logger.debug(`apiCall ${method} ${path}`);
278
+ const opts = { method, path };
279
+ if (body !== void 0) opts.body = body;
280
+ const resp = await runtime.api.call(opts);
281
+ if (!resp.ok) {
282
+ throw new Error(`apiCall ${method} ${path} \u2192 HTTP ${resp.status}: ${resp.raw.slice(0, 500)}`);
283
+ }
284
+ return resp.raw;
285
+ }, "invoke")
286
+ };
287
+ var DATA_FUNCTIONS = [dbQuery, dbExec, apiCall, shell];
288
+
289
+ // src/dsl/functions/device.ts
290
+ function asString3(x, fn, idx) {
291
+ if (typeof x !== "string") throw new Error(`${fn}(): arg ${idx} must be a string. Got ${typeof x}.`);
292
+ return x;
293
+ }
294
+ __name(asString3, "asString");
295
+ function asBoolean(x, fn, idx) {
296
+ if (typeof x !== "boolean") throw new Error(`${fn}(): arg ${idx} must be a boolean. Got ${typeof x}.`);
297
+ return x;
298
+ }
299
+ __name(asBoolean, "asBoolean");
300
+ var setDevice = {
301
+ name: "setDevice",
302
+ argTypes: ["string"],
303
+ returnType: "void",
304
+ invoke: /* @__PURE__ */ __name((runtime, slotArg) => {
305
+ const slot = asString3(slotArg, "setDevice", 0);
306
+ runtime.setCurrentDeviceSlot(slot);
307
+ }, "invoke")
308
+ };
309
+ var appLaunch = {
310
+ name: "appLaunch",
311
+ argTypes: ["boolean"],
312
+ returnType: "void",
313
+ minArgs: 0,
314
+ invoke: /* @__PURE__ */ __name(async (runtime, cleanArg) => {
315
+ const clean = cleanArg !== void 0 ? asBoolean(cleanArg, "appLaunch", 0) : false;
316
+ await runtime.driver.appLaunch(runtime.currentDeviceSlot, { clean });
317
+ }, "invoke")
318
+ };
319
+ var openDeeplink = {
320
+ name: "openDeeplink",
321
+ argTypes: ["string"],
322
+ returnType: "void",
323
+ invoke: /* @__PURE__ */ __name(async (runtime, urlArg) => {
324
+ const url = asString3(urlArg, "openDeeplink", 0);
325
+ await runtime.driver.openDeeplink(runtime.currentDeviceSlot, url);
326
+ }, "invoke")
327
+ };
328
+ var DEVICE_FUNCTIONS = [setDevice, appLaunch, openDeeplink];
329
+
330
+ // src/dsl/functions/selectors.ts
331
+ function assertSelector(x, fnName, argIndex) {
332
+ if (typeof x !== "object" || x === null) {
333
+ throw new Error(
334
+ `${fnName}(): arg ${argIndex} must be a Selector (from getByTestId/getByText/getByLabel/ordinal/near). Got ${typeof x}.`
335
+ );
336
+ }
337
+ return x;
338
+ }
339
+ __name(assertSelector, "assertSelector");
340
+ function assertString(x, fnName, argIndex) {
341
+ if (typeof x !== "string") {
342
+ throw new Error(`${fnName}(): arg ${argIndex} must be a string. Got ${typeof x}.`);
343
+ }
344
+ return x;
345
+ }
346
+ __name(assertString, "assertString");
347
+ function assertNumber(x, fnName, argIndex) {
348
+ if (typeof x !== "number") {
349
+ throw new Error(`${fnName}(): arg ${argIndex} must be a number. Got ${typeof x}.`);
350
+ }
351
+ return x;
352
+ }
353
+ __name(assertNumber, "assertNumber");
354
+ var getByTestId = {
355
+ name: "getByTestId",
356
+ argTypes: ["string"],
357
+ returnType: "selector",
358
+ invoke: /* @__PURE__ */ __name((_rt, testId) => {
359
+ return { testId: assertString(testId, "getByTestId", 0) };
360
+ }, "invoke")
361
+ };
362
+ var getByText = {
363
+ name: "getByText",
364
+ argTypes: ["string"],
365
+ returnType: "selector",
366
+ invoke: /* @__PURE__ */ __name((_rt, text) => {
367
+ return { text: assertString(text, "getByText", 0) };
368
+ }, "invoke")
369
+ };
370
+ var getByLabel = {
371
+ name: "getByLabel",
372
+ argTypes: ["string"],
373
+ returnType: "selector",
374
+ invoke: /* @__PURE__ */ __name((_rt, label) => {
375
+ return { label: assertString(label, "getByLabel", 0) };
376
+ }, "invoke")
377
+ };
378
+ var ordinal = {
379
+ name: "ordinal",
380
+ argTypes: ["selector", "number"],
381
+ returnType: "selector",
382
+ invoke: /* @__PURE__ */ __name((_rt, target, n) => {
383
+ return { ...assertSelector(target, "ordinal", 0), ordinal: assertNumber(n, "ordinal", 1) };
384
+ }, "invoke")
385
+ };
386
+ var near = {
387
+ name: "near",
388
+ argTypes: ["selector", "selector", "number"],
389
+ returnType: "selector",
390
+ minArgs: 2,
391
+ invoke: /* @__PURE__ */ __name((_rt, target, anchor, maxDist) => {
392
+ const t = assertSelector(target, "near", 0);
393
+ const a = assertSelector(anchor, "near", 1);
394
+ const near2 = { anchor: a };
395
+ if (maxDist !== void 0) near2.maxDistancePx = assertNumber(maxDist, "near", 2);
396
+ return { ...t, near: near2 };
397
+ }, "invoke")
398
+ };
399
+ var SELECTOR_FUNCTIONS = [getByTestId, getByText, getByLabel, ordinal, near];
400
+
401
+ // src/dsl/functions/time.ts
402
+ function pad2(n) {
403
+ return n < 10 ? `0${n}` : String(n);
404
+ }
405
+ __name(pad2, "pad2");
406
+ function isoDate(ms) {
407
+ const d = new Date(ms);
408
+ return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`;
409
+ }
410
+ __name(isoDate, "isoDate");
411
+ var todayFn = {
412
+ name: "today",
413
+ argTypes: [],
414
+ returnType: "string",
415
+ invoke: /* @__PURE__ */ __name((runtime) => isoDate(runtime.now()), "invoke")
416
+ };
417
+ var daysFromNow = {
418
+ name: "daysFromNow",
419
+ argTypes: ["number"],
420
+ returnType: "string",
421
+ invoke: /* @__PURE__ */ __name((runtime, nArg) => {
422
+ if (typeof nArg !== "number" || !Number.isFinite(nArg)) {
423
+ throw new Error(`daysFromNow(): arg 0 must be a finite number. Got ${nArg}.`);
424
+ }
425
+ const dayMs = 24 * 60 * 60 * 1e3;
426
+ return isoDate(runtime.now() + Math.trunc(nArg) * dayMs);
427
+ }, "invoke")
428
+ };
429
+ var nowMs = {
430
+ name: "nowMs",
431
+ argTypes: [],
432
+ returnType: "number",
433
+ invoke: /* @__PURE__ */ __name((runtime) => runtime.now(), "invoke")
434
+ };
435
+ var TIME_FUNCTIONS = [todayFn, daysFromNow, nowMs];
436
+
437
+ // src/dsl/functions/ui.ts
438
+ import { setTimeout as wait } from "timers/promises";
439
+ function asSelector2(x, fn, idx) {
440
+ if (typeof x !== "object" || x === null) {
441
+ throw new Error(`${fn}(): arg ${idx} must be a Selector (use getByTestId/Text/Label). Got ${typeof x}.`);
442
+ }
443
+ return x;
444
+ }
445
+ __name(asSelector2, "asSelector");
446
+ function asString4(x, fn, idx) {
447
+ if (typeof x !== "string") throw new Error(`${fn}(): arg ${idx} must be a string. Got ${typeof x}.`);
448
+ return x;
449
+ }
450
+ __name(asString4, "asString");
451
+ function asNumber2(x, fn, idx) {
452
+ if (typeof x !== "number") throw new Error(`${fn}(): arg ${idx} must be a number. Got ${typeof x}.`);
453
+ return x;
454
+ }
455
+ __name(asNumber2, "asNumber");
456
+ async function pollUntilFound(runtime, selector, timeoutMs) {
457
+ const deadline = Date.now() + timeoutMs;
458
+ let lastFailure = null;
459
+ while (Date.now() < deadline) {
460
+ const tree = await runtime.driver.a11yTree(runtime.currentDeviceSlot);
461
+ const r = runtime.selectorResolver.resolve(tree, selector);
462
+ if (r.ok) return;
463
+ lastFailure = { reason: r.reason, candidates: r.candidates };
464
+ await wait(150);
465
+ }
466
+ throw new Error(
467
+ `Selector not found within ${timeoutMs}ms on slot "${runtime.currentDeviceSlot}": ${JSON.stringify(selector)}
468
+ reason: ${lastFailure?.reason ?? "unknown"}
469
+ candidates: ${JSON.stringify(lastFailure?.candidates ?? [], null, 2).slice(0, 600)}`
470
+ );
471
+ }
472
+ __name(pollUntilFound, "pollUntilFound");
473
+ var tap = {
474
+ name: "tap",
475
+ argTypes: ["selector"],
476
+ returnType: "void",
477
+ invoke: /* @__PURE__ */ __name(async (runtime, selectorArg) => {
478
+ const selector = asSelector2(selectorArg, "tap", 0);
479
+ await pollUntilFound(runtime, selector, runtime.envConfig.defaultActionWaitMs);
480
+ await runtime.driver.tap(runtime.currentDeviceSlot, selector);
481
+ }, "invoke")
482
+ };
483
+ var type_ = {
484
+ name: "type",
485
+ argTypes: ["selector", "string"],
486
+ returnType: "void",
487
+ invoke: /* @__PURE__ */ __name(async (runtime, selectorArg, textArg) => {
488
+ const selector = asSelector2(selectorArg, "type", 0);
489
+ const text = asString4(textArg, "type", 1);
490
+ await pollUntilFound(runtime, selector, runtime.envConfig.defaultActionWaitMs);
491
+ await runtime.driver.type(runtime.currentDeviceSlot, selector, text);
492
+ }, "invoke")
493
+ };
494
+ var swipe = {
495
+ name: "swipe",
496
+ argTypes: ["string", "selector"],
497
+ returnType: "void",
498
+ minArgs: 1,
499
+ invoke: /* @__PURE__ */ __name(async (runtime, directionArg, fromArg) => {
500
+ const direction = asString4(directionArg, "swipe", 0);
501
+ if (direction !== "up" && direction !== "down" && direction !== "left" && direction !== "right") {
502
+ throw new Error(`swipe(): direction must be 'up'|'down'|'left'|'right'. Got "${direction}".`);
503
+ }
504
+ let from;
505
+ if (fromArg !== void 0) {
506
+ from = asSelector2(fromArg, "swipe", 1);
507
+ await pollUntilFound(runtime, from, runtime.envConfig.defaultActionWaitMs);
508
+ }
509
+ await runtime.driver.swipe(runtime.currentDeviceSlot, direction, from);
510
+ }, "invoke")
511
+ };
512
+ var pressKey = {
513
+ name: "pressKey",
514
+ argTypes: ["string"],
515
+ returnType: "void",
516
+ invoke: /* @__PURE__ */ __name(async (runtime, keyArg) => {
517
+ const key = asString4(keyArg, "pressKey", 0);
518
+ if (key !== "back" && key !== "home" && key !== "enter" && key !== "escape") {
519
+ throw new Error(`pressKey(): key must be 'back'|'home'|'enter'|'escape'. Got "${key}".`);
520
+ }
521
+ await runtime.driver.pressKey(runtime.currentDeviceSlot, key);
522
+ }, "invoke")
523
+ };
524
+ var waitFor = {
525
+ name: "waitFor",
526
+ argTypes: ["selector", "number"],
527
+ returnType: "void",
528
+ minArgs: 1,
529
+ invoke: /* @__PURE__ */ __name(async (runtime, selectorArg, timeoutMsArg) => {
530
+ const selector = asSelector2(selectorArg, "waitFor", 0);
531
+ const timeoutMs = timeoutMsArg === void 0 ? runtime.envConfig.defaultWaitForTimeoutMs : asNumber2(timeoutMsArg, "waitFor", 1);
532
+ await pollUntilFound(runtime, selector, timeoutMs);
533
+ }, "invoke")
534
+ };
535
+ var pause = {
536
+ name: "pause",
537
+ argTypes: ["number"],
538
+ returnType: "void",
539
+ invoke: /* @__PURE__ */ __name(async (_runtime, msArg) => {
540
+ const ms = asNumber2(msArg, "pause", 0);
541
+ await wait(ms);
542
+ }, "invoke")
543
+ };
544
+ var UI_FUNCTIONS = [tap, type_, swipe, pressKey, waitFor, pause];
545
+
546
+ // src/dsl/functions/index.ts
547
+ var ALL_DSL_FUNCTIONS = [
548
+ ...DEVICE_FUNCTIONS,
549
+ ...UI_FUNCTIONS,
550
+ ...SELECTOR_FUNCTIONS,
551
+ ...ALERT_FUNCTIONS,
552
+ ...DATA_FUNCTIONS,
553
+ ...ASSERT_FUNCTIONS,
554
+ ...TIME_FUNCTIONS
555
+ ];
556
+ function buildDefaultRegistry() {
557
+ const reg = new FunctionRegistry();
558
+ for (const fn of ALL_DSL_FUNCTIONS) reg.register(fn);
559
+ return reg;
560
+ }
561
+ __name(buildDefaultRegistry, "buildDefaultRegistry");
562
+
563
+ // src/dsl/linter.ts
564
+ import {
565
+ AssignmentStatement,
566
+ ArrayAccessExpression,
567
+ ArrayAssignmentStatement,
568
+ ArrayExpression,
569
+ BinaryExpression,
570
+ BlockStatement,
571
+ BreakStatement,
572
+ ConditionalExpression,
573
+ ContinueStatement,
574
+ DoWhileStatement,
575
+ ForStatement,
576
+ FunctionDefineStatement,
577
+ FunctionStatement,
578
+ FunctionalExpression,
579
+ IfStatement,
580
+ IncrementExpression,
581
+ IncrementStatement,
582
+ MemberCallExpression,
583
+ MetaBlockStatement,
584
+ ObjectExpression,
585
+ PropertyAccessExpression,
586
+ PrintStatement,
587
+ ReturnStatement,
588
+ StepStatement,
589
+ UnaryExpression,
590
+ ValueExpression,
591
+ VarStatement,
592
+ VariableExpression,
593
+ WhileStatement
594
+ } from "@unotest/dsl";
595
+ import { validateDsl } from "@unotest/dsl/validator";
596
+
597
+ // src/dsl/mobile-dsl-registry.ts
598
+ var ARG_KIND = {
599
+ // Mobile's loose types map onto the engine's lenient predicates —
600
+ // variables / helper-call results of unknown kind always pass, like the
601
+ // old E6 "only flag the unambiguous" policy. `selector` maps to the
602
+ // STRICT locator kind: mobile selectors only come from selector
603
+ // functions (getByTestId/getByText/…), never bare strings.
604
+ string: "stringLike",
605
+ number: "number",
606
+ boolean: "boolLike",
607
+ selector: "strictLocatorLike",
608
+ any: "any"
609
+ };
610
+ var ARG_KIND_OVERRIDES = {
611
+ // apiCall(method, path, jsonBody?, jsonHeaders?) — body/headers must be
612
+ // JSON strings; replaces the old local E4 check.
613
+ apiCall: { 2: "jsonObjectString", 3: "jsonObjectString" }
614
+ };
615
+ var RETURN_KIND = {
616
+ void: "void",
617
+ string: "string",
618
+ number: "number",
619
+ boolean: "bool",
620
+ selector: "locator",
621
+ any: "unknown"
622
+ };
623
+ function contractFor(fn) {
624
+ const required = fn.minArgs ?? fn.argTypes.length;
625
+ const overrides = ARG_KIND_OVERRIDES[fn.name] ?? {};
626
+ return {
627
+ name: fn.name,
628
+ signature: {
629
+ args: fn.argTypes.map((t, i) => ({
630
+ label: `arg${i + 1}`,
631
+ kind: overrides[i] ?? ARG_KIND[t],
632
+ required: i < required
633
+ })),
634
+ ...fn.variadic ? { variadic: true } : {}
635
+ },
636
+ support: "visual",
637
+ returns: { mode: "fixed", kind: RETURN_KIND[fn.returnType] },
638
+ // Mobile's frozen subset (D-4) forbids method chains entirely — the
639
+ // slim linter rejects MemberCallExpression before the engine ever
640
+ // sees a chain, so no contract is chainable.
641
+ locatorCapability: fn.returnType === "selector" ? "locator" : "none"
642
+ };
643
+ }
644
+ __name(contractFor, "contractFor");
645
+ function helperContract(name) {
646
+ return {
647
+ name,
648
+ signature: { args: [], variadic: true },
649
+ support: "visual",
650
+ returns: { mode: "fixed", kind: "unknown" },
651
+ locatorCapability: "none",
652
+ trusted: true
653
+ };
654
+ }
655
+ __name(helperContract, "helperContract");
656
+ var MobileDslRegistry = class {
657
+ static {
658
+ __name(this, "MobileDslRegistry");
659
+ }
660
+ byName = /* @__PURE__ */ new Map();
661
+ constructor(functions, userFunctionNames = []) {
662
+ for (const fn of functions) this.byName.set(fn.name, contractFor(fn));
663
+ for (const name of userFunctionNames) {
664
+ if (!this.byName.has(name)) this.byName.set(name, helperContract(name));
665
+ }
666
+ }
667
+ resolve(name) {
668
+ return this.byName.get(name) ?? null;
669
+ }
670
+ resolveContract(name) {
671
+ return this.resolve(name);
672
+ }
673
+ inferReturnKind(expression, _session) {
674
+ const contract = this.resolveContract(expression.name);
675
+ if (!contract || contract.returns.mode !== "fixed") return "unknown";
676
+ if (contract.returns.kind === "void") return "unknown";
677
+ return contract.returns.kind;
678
+ }
679
+ };
680
+ var MOBILE_DISABLED_ENGINE_RULES = [
681
+ "unsupported-statement",
682
+ "unsupported-expression",
683
+ "for-shape",
684
+ "var-shape",
685
+ "if-shape",
686
+ "string-concat",
687
+ "semantic-loss",
688
+ "list-arg",
689
+ "wrapped-format"
690
+ ];
691
+
692
+ // src/dsl/linter.ts
693
+ var ASSERT_PREFIX = "assert";
694
+ var UI_CALL_NAMES = /* @__PURE__ */ new Set(["tap", "type", "swipe", "pressKey", "waitFor", "appLaunch", "openDeeplink"]);
695
+ var DslLinter = class {
696
+ constructor(deps) {
697
+ this.deps = deps;
698
+ }
699
+ deps;
700
+ static {
701
+ __name(this, "DslLinter");
702
+ }
703
+ /**
704
+ * Lint a parsed scenario AST: mobile-specific pass + the shared
705
+ * validator engine over the same tree.
706
+ *
707
+ * @param ast entry-scenario AST
708
+ * @param source raw source — the engine's validation session reads it
709
+ * @param options cross-cutting context (user-function names, etc.)
710
+ */
711
+ lint(ast, source, options = {}) {
712
+ const diags = [];
713
+ const userFns = options.userFunctionNames ?? /* @__PURE__ */ new Set();
714
+ for (const stmt of ast.statements) {
715
+ if (stmt instanceof FunctionDefineStatement && isBlockMappable(stmt.name)) {
716
+ this.lintFunctionBody(
717
+ stmt,
718
+ diags,
719
+ userFns,
720
+ /*allowReturn*/
721
+ false
722
+ );
723
+ } else if (stmt instanceof FunctionDefineStatement) {
724
+ this.lintFunctionBody(
725
+ stmt,
726
+ diags,
727
+ userFns,
728
+ /*allowReturn*/
729
+ true
730
+ );
731
+ } else if (stmt instanceof MetaBlockStatement) {
732
+ } else {
733
+ diags.push(diag("error", "E2", `Only function definitions allowed at top level (got ${stmt.constructor.name})`, stmt));
734
+ }
735
+ }
736
+ diags.push(...this.runEngine(ast, source ?? "", userFns));
737
+ return diags;
738
+ }
739
+ /**
740
+ * Cross-file E8 check — duplicate user-function names + shadowing
741
+ * built-ins. Run ONCE per scenario load (not per file) using the
742
+ * full origin list from ScenarioLoader.userFunctionOrigins.
743
+ *
744
+ * Each diagnostic is anchored to a specific origin (with path/line/col),
745
+ * so production callers print "_helpers/seed.js:12:1 [E8] ...".
746
+ */
747
+ lintCrossFile(origins) {
748
+ const diags = [];
749
+ const byName = /* @__PURE__ */ new Map();
750
+ for (const o of origins) {
751
+ const list = byName.get(o.name) ?? [];
752
+ list.push(o);
753
+ byName.set(o.name, list);
754
+ }
755
+ for (const [name, list] of byName) {
756
+ if (this.deps.registry.has(name)) {
757
+ for (const o of list) {
758
+ diags.push({
759
+ severity: "error",
760
+ code: "E8",
761
+ message: `User function "${name}" shadows a built-in DSL function. Rename it.`,
762
+ line: o.line,
763
+ col: o.col,
764
+ source: o.source
765
+ });
766
+ }
767
+ continue;
768
+ }
769
+ if (list.length > 1) {
770
+ const where = list.map((o) => `${o.source}:${o.line}`).join(", ");
771
+ for (const o of list) {
772
+ diags.push({
773
+ severity: "error",
774
+ code: "E8",
775
+ message: `Duplicate user function "${name}". Defined at: ${where}`,
776
+ line: o.line,
777
+ col: o.col,
778
+ source: o.source
779
+ });
780
+ }
781
+ }
782
+ }
783
+ return diags;
784
+ }
785
+ /**
786
+ * Lint a helper file. Same two layers as a scenario, but `return` is
787
+ * allowed in any non-entry function body and all diagnostics are
788
+ * tagged with helper.path.
789
+ */
790
+ lintHelper(helper, options = {}) {
791
+ const diags = [];
792
+ const userFns = options.userFunctionNames ?? /* @__PURE__ */ new Set();
793
+ for (const stmt of helper.ast.statements) {
794
+ if (stmt instanceof FunctionDefineStatement) {
795
+ if (isBlockMappable(stmt.name)) {
796
+ this.lintFunctionBody(
797
+ stmt,
798
+ diags,
799
+ userFns,
800
+ /*allowReturn*/
801
+ false
802
+ );
803
+ } else {
804
+ this.lintFunctionBody(
805
+ stmt,
806
+ diags,
807
+ userFns,
808
+ /*allowReturn*/
809
+ true
810
+ );
811
+ }
812
+ } else if (stmt instanceof MetaBlockStatement) {
813
+ } else {
814
+ diags.push(diag("error", "E2", `Only function definitions allowed at top level (got ${stmt.constructor.name})`, stmt));
815
+ }
816
+ }
817
+ diags.push(...this.runEngine(helper.ast, helper.source ?? "", userFns));
818
+ return diags.map((d) => ({ ...d, source: helper.path }));
819
+ }
820
+ // ---- shared engine ------------------------------------------------------
821
+ /** Run the `@unotest/dsl/validator` engine with the mobile contracts.
822
+ * Engine diagnostics map 1:1 into mobile `Diagnostic` rows with
823
+ * `validator:<rule>` codes. */
824
+ runEngine(ast, source, userFns) {
825
+ const registry = new MobileDslRegistry(ALL_DSL_FUNCTIONS, userFns);
826
+ return validateDsl(ast, source, false, registry, {
827
+ disabledRules: MOBILE_DISABLED_ENGINE_RULES
828
+ }).map((d) => ({
829
+ severity: d.severity,
830
+ code: `validator:${d.rule}`,
831
+ message: d.message,
832
+ line: d.line ?? 0,
833
+ col: d.column ?? 0
834
+ }));
835
+ }
836
+ // ---- mobile-specific pass ----------------------------------------------
837
+ lintFunctionBody(fn, diags, userFns, allowReturn) {
838
+ if (!(fn.body instanceof BlockStatement)) {
839
+ diags.push(diag("error", "E2", `Function "${fn.name}" body must be a block`, fn));
840
+ return;
841
+ }
842
+ let firstUiSeen = false;
843
+ const recordCall = /* @__PURE__ */ __name((name) => {
844
+ if (UI_CALL_NAMES.has(name)) firstUiSeen = true;
845
+ }, "recordCall");
846
+ for (const stmt of fn.body.statements) {
847
+ this.checkStatement(stmt, diags, recordCall, () => firstUiSeen, userFns, allowReturn);
848
+ }
849
+ }
850
+ checkStatement(stmt, diags, recordCall, isFirstUiSeen, userFns, allowReturn) {
851
+ if (stmt instanceof ReturnStatement) {
852
+ if (!allowReturn) {
853
+ diags.push(diag("error", "E2", `'return' is not allowed inside test_*/flow_* function bodies (only in helpers)`, stmt));
854
+ return;
855
+ }
856
+ const expr = stmt.expression;
857
+ if (expr) this.checkExpression(expr, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
858
+ return;
859
+ }
860
+ if (this.isForbiddenStatement(stmt)) {
861
+ diags.push(diag("error", "E2", `Forbidden statement type "${stmt.constructor.name}" (D-4 subset)`, stmt));
862
+ return;
863
+ }
864
+ if (stmt instanceof AssignmentStatement) {
865
+ this.checkExpression(stmt.expression, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
866
+ return;
867
+ }
868
+ if (stmt instanceof FunctionStatement) {
869
+ const call = stmt.functionalExpression;
870
+ if (!(call instanceof FunctionalExpression)) {
871
+ diags.push(diag("error", "E2", `Method chains are not allowed (D-4 subset)`, stmt));
872
+ return;
873
+ }
874
+ this.checkCall(call, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
875
+ return;
876
+ }
877
+ if (stmt instanceof StepStatement) {
878
+ this.checkStatement(stmt.body, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
879
+ return;
880
+ }
881
+ if (stmt instanceof IfStatement) {
882
+ this.checkExpression(stmt.expression, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
883
+ this.checkStatement(stmt.ifStatement, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
884
+ if (stmt.elseStatement) this.checkStatement(stmt.elseStatement, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
885
+ return;
886
+ }
887
+ if (stmt instanceof BlockStatement) {
888
+ for (const s of stmt.statements) this.checkStatement(s, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
889
+ return;
890
+ }
891
+ if (stmt instanceof FunctionDefineStatement) {
892
+ diags.push(diag("error", "E2", "Nested function definitions are not allowed", stmt));
893
+ return;
894
+ }
895
+ if (stmt instanceof MetaBlockStatement) return;
896
+ diags.push(diag("error", "E2", `Unsupported statement "${stmt.constructor.name}"`, stmt));
897
+ }
898
+ checkExpression(expr, diags, recordCall, isFirstUiSeen, userFns, allowReturn) {
899
+ if (expr instanceof FunctionalExpression) {
900
+ this.checkCall(expr, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
901
+ return;
902
+ }
903
+ if (expr instanceof BinaryExpression) {
904
+ this.checkExpression(expr.expr1, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
905
+ this.checkExpression(expr.expr2, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
906
+ return;
907
+ }
908
+ if (expr instanceof ArrayExpression) {
909
+ for (const e of expr.elements) this.checkExpression(e, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
910
+ return;
911
+ }
912
+ if (expr instanceof ValueExpression || expr instanceof VariableExpression) return;
913
+ if (this.isForbiddenExpression(expr)) {
914
+ diags.push(diag("error", "E2", `Forbidden expression "${expr.constructor.name}" (D-4 subset)`, expr));
915
+ return;
916
+ }
917
+ diags.push(diag("error", "E2", `Unsupported expression "${expr.constructor.name}"`, expr));
918
+ }
919
+ checkCall(call, diags, recordCall, isFirstUiSeen, userFns, allowReturn) {
920
+ const name = call.name;
921
+ if (name === "setDevice" && call.arguments.length > 0) {
922
+ const arg0 = call.arguments[0];
923
+ if (arg0 instanceof ValueExpression) {
924
+ const val = arg0.value.asString?.call(arg0.value);
925
+ if (typeof val === "string" && !this.deps.knownSlots.includes(val)) {
926
+ diags.push(
927
+ diag("error", "E3", `setDevice("${val}"): unknown slot. Known: ${this.deps.knownSlots.join(", ") || "(none)"}`, call)
928
+ );
929
+ }
930
+ }
931
+ }
932
+ if (name.startsWith(ASSERT_PREFIX) && !isFirstUiSeen()) {
933
+ diags.push(diag("warning", "W5", `${name}() called before any UI action \u2014 typo or wrong order?`, call));
934
+ }
935
+ recordCall(name);
936
+ for (const arg of call.arguments) {
937
+ this.checkExpression(arg, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
938
+ }
939
+ }
940
+ isForbiddenStatement(stmt) {
941
+ return stmt instanceof ForStatement || stmt instanceof WhileStatement || stmt instanceof DoWhileStatement || stmt instanceof VarStatement || stmt instanceof PrintStatement || stmt instanceof BreakStatement || stmt instanceof ContinueStatement || stmt instanceof IncrementStatement || stmt instanceof ArrayAssignmentStatement;
942
+ }
943
+ isForbiddenExpression(expr) {
944
+ return expr instanceof IncrementExpression || expr instanceof UnaryExpression || expr instanceof ConditionalExpression || expr instanceof ObjectExpression || expr instanceof ArrayAccessExpression || // Post-vendor @unotest/dsl extensions the mobile frozen subset rejects.
945
+ expr instanceof MemberCallExpression || expr instanceof PropertyAccessExpression;
946
+ }
947
+ };
948
+ function isBlockMappable(name) {
949
+ return name.startsWith("test_") || name.startsWith("flow_");
950
+ }
951
+ __name(isBlockMappable, "isBlockMappable");
952
+ function diag(severity, code, message, node) {
953
+ const tok = node.token;
954
+ return {
955
+ severity,
956
+ code,
957
+ message,
958
+ line: tok?.getLine ? tok.getLine() : 0,
959
+ col: tok?.getColumn ? tok.getColumn() : 0
960
+ };
961
+ }
962
+ __name(diag, "diag");
963
+
964
+ // src/dsl/mobile-dsl-language-service.ts
965
+ var SELECTOR_FNS = /* @__PURE__ */ new Set([
966
+ "getByTestId",
967
+ "getByText",
968
+ "getByLabel",
969
+ "ordinal",
970
+ "near"
971
+ ]);
972
+ var WAIT_FNS = /* @__PURE__ */ new Set(["waitFor", "pause"]);
973
+ var STATE_FNS = /* @__PURE__ */ new Set(["dbQuery", "dbExec", "apiCall", "shell"]);
974
+ var READ_FNS = /* @__PURE__ */ new Set(["today", "daysFromNow", "nowMs"]);
975
+ function categoryFor(name) {
976
+ if (name.startsWith("assert")) return "assertion";
977
+ if (SELECTOR_FNS.has(name)) return "selector";
978
+ if (WAIT_FNS.has(name)) return "wait";
979
+ if (STATE_FNS.has(name)) return "state";
980
+ if (READ_FNS.has(name)) return "read";
981
+ return "action";
982
+ }
983
+ __name(categoryFor, "categoryFor");
984
+ function vocabEntryFor(fn) {
985
+ const required = fn.minArgs ?? fn.argTypes.length;
986
+ const args = fn.argTypes.map((kind, i) => ({
987
+ label: `arg${i + 1}`,
988
+ kind,
989
+ required: i < required
990
+ }));
991
+ const placeholders = args.map((a, i) => `\${${i + 1}:${a.label}}`).join(", ");
992
+ const sigArgs = args.map((a) => a.required ? a.label : `${a.label}?`).join(", ");
993
+ return {
994
+ name: fn.name,
995
+ category: categoryFor(fn.name),
996
+ args,
997
+ returns: fn.returnType,
998
+ insertText: `${fn.name}(${placeholders})`,
999
+ signature: `${fn.name}(${sigArgs})`,
1000
+ locatorCapability: fn.returnType === "selector" ? "locator" : "none"
1001
+ };
1002
+ }
1003
+ __name(vocabEntryFor, "vocabEntryFor");
1004
+ function toDslDiagnostic(d) {
1005
+ return {
1006
+ severity: d.severity,
1007
+ line: d.line,
1008
+ column: d.col,
1009
+ message: d.message,
1010
+ rule: `linter:${d.code}`,
1011
+ source: "lint"
1012
+ };
1013
+ }
1014
+ __name(toDslDiagnostic, "toDslDiagnostic");
1015
+ var MobileDslLanguageService = class {
1016
+ static {
1017
+ __name(this, "MobileDslLanguageService");
1018
+ }
1019
+ vocab;
1020
+ registry = buildDefaultRegistry();
1021
+ constructor() {
1022
+ this.vocab = ALL_DSL_FUNCTIONS.map(vocabEntryFor).sort(
1023
+ (a, b) => a.name.localeCompare(b.name)
1024
+ );
1025
+ }
1026
+ getVocab() {
1027
+ return this.vocab;
1028
+ }
1029
+ validate(source, ctx) {
1030
+ let ast;
1031
+ try {
1032
+ ast = new Parser(new Lexer(source).tokenize()).parse();
1033
+ } catch (e) {
1034
+ return [
1035
+ {
1036
+ severity: "error",
1037
+ line: 1,
1038
+ column: 1,
1039
+ message: e instanceof Error ? e.message : String(e),
1040
+ rule: "parse",
1041
+ source: "parse"
1042
+ }
1043
+ ];
1044
+ }
1045
+ const userFunctionNames = new Set(ctx?.helperNames ?? []);
1046
+ const linter = new DslLinter({ registry: this.registry, knownSlots: [] });
1047
+ return linter.lint(ast, source, { userFunctionNames }).filter((d) => d.code !== "E3").map(toDslDiagnostic);
1048
+ }
1049
+ };
1050
+ var mobileDslLanguageService = new MobileDslLanguageService();
1051
+ var mobile_dsl_language_service_default = mobileDslLanguageService;
1052
+ export {
1053
+ MobileDslLanguageService,
1054
+ mobile_dsl_language_service_default as default,
1055
+ mobileDslLanguageService
1056
+ };