@unotest/mobile 0.8.3 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/write-e2e-test.md +23 -14
- package/CHANGELOG.md +25 -0
- package/README.md +4 -3
- package/bin/{mcp.js → unotest-mobile.js} +9 -0
- package/dist/dsl/mobile-dsl-language-service.d.ts +12 -0
- package/dist/dsl/mobile-dsl-language-service.js +1056 -0
- package/dist/mcp/server.js +1095 -2886
- package/dist/runner/cli.js +4247 -4991
- package/dist/runner/doctor.js +4 -3
- package/dist/runner/init.js +67 -73
- package/dist/runner/install.js +103 -89
- package/dist/runner/mobile-runner-adapter.d.ts +6 -0
- package/dist/runner/mobile-runner-adapter.js +227 -0
- package/package.json +39 -33
package/dist/mcp/server.js
CHANGED
|
@@ -163,16 +163,24 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
163
163
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
164
164
|
|
|
165
165
|
// src/config/env.ts
|
|
166
|
-
import {
|
|
166
|
+
import { applyEnvLayers as applyCoreEnvLayers } from "@unotest/core";
|
|
167
167
|
import { z } from "zod";
|
|
168
|
-
|
|
168
|
+
|
|
169
|
+
// src/runner/scenarios-dir.ts
|
|
170
|
+
import { runsDirFor, testDirFor, UNOTEST_DIR } from "@unotest/protocol";
|
|
171
|
+
var MOBILE_SUFFIX = "-mobile";
|
|
172
|
+
var MOBILE_TEST_DIR = testDirFor(MOBILE_SUFFIX);
|
|
173
|
+
var MOBILE_SCENARIOS_DIR = `${UNOTEST_DIR}/${MOBILE_TEST_DIR}`;
|
|
174
|
+
var MOBILE_RUNS_DIR = `${UNOTEST_DIR}/${runsDirFor(MOBILE_SUFFIX)}`;
|
|
175
|
+
|
|
176
|
+
// src/config/env.ts
|
|
169
177
|
var loaded = false;
|
|
170
|
-
function
|
|
178
|
+
function applyEnvLayers() {
|
|
171
179
|
if (loaded) return;
|
|
172
|
-
|
|
180
|
+
applyCoreEnvLayers(process.cwd(), MOBILE_SUFFIX);
|
|
173
181
|
loaded = true;
|
|
174
182
|
}
|
|
175
|
-
__name(
|
|
183
|
+
__name(applyEnvLayers, "applyEnvLayers");
|
|
176
184
|
var EnvSchema = z.object({
|
|
177
185
|
// APP_BUNDLE_ID — required at appLaunch / install time. We let the schema
|
|
178
186
|
// accept it as optional so commands that don't touch the app (`doctor`,
|
|
@@ -219,7 +227,10 @@ var EnvSchema = z.object({
|
|
|
219
227
|
// `exp+<APP_URL_SCHEME>://expo-development-client/?url=<METRO_URL>` after
|
|
220
228
|
// `app_launch clean: true` to bypass the launcher. (Flow not yet wired.)
|
|
221
229
|
EXPO_DEV_CLIENT: z.string().optional().transform((v) => v === "true" || v === "1"),
|
|
222
|
-
|
|
230
|
+
// Dot-prefixed + target-suffixed per the M-10 artifact convention
|
|
231
|
+
// (`.sessions-mobile`, like `.runs-mobile`): one `.gitignore` wildcard
|
|
232
|
+
// covers all runtime debris, and web/mobile artifacts never mix.
|
|
233
|
+
SESSION_LOG_PATH: z.string().default("unotest/.sessions-mobile/current.jsonl"),
|
|
223
234
|
// Explicit kill-switch for session recording. When "1"/"true", or when
|
|
224
235
|
// SESSION_LOG_PATH is empty, buildApp wires a NoopSessionRecorder. Used
|
|
225
236
|
// by evals harness and any consumer that wants the MCP server to make
|
|
@@ -228,10 +239,10 @@ var EnvSchema = z.object({
|
|
|
228
239
|
// When true, recorder writes the FULL tool result alongside the
|
|
229
240
|
// truncated preview. Off by default — snapshots can be megabytes.
|
|
230
241
|
SESSION_LOG_FULL: z.string().optional().transform((v) => v === "1" || v === "true"),
|
|
231
|
-
ARTIFACTS_DIR: z.string().default("unotest
|
|
242
|
+
ARTIFACTS_DIR: z.string().default("unotest/.artifacts-mobile"),
|
|
232
243
|
// Where ExplorationService persists per-session JSONL recording logs.
|
|
233
244
|
// Default: <ARTIFACTS_DIR>/explorations. Folded into the gitignored
|
|
234
|
-
// `unotest
|
|
245
|
+
// `unotest/.artifacts-mobile/` tree by the init template.
|
|
235
246
|
EXPLORATIONS_DIR: z.string().optional(),
|
|
236
247
|
// WDA per-slot port mapping (D-13 parallel multi-device). Stored as a
|
|
237
248
|
// comma-separated `slot=port` list, e.g. "A=8100,B=8101". Each slot present
|
|
@@ -255,7 +266,7 @@ var EnvSchema = z.object({
|
|
|
255
266
|
var cached = null;
|
|
256
267
|
function loadEnv() {
|
|
257
268
|
if (cached) return cached;
|
|
258
|
-
|
|
269
|
+
applyEnvLayers();
|
|
259
270
|
const parsed = EnvSchema.safeParse(process.env);
|
|
260
271
|
if (!parsed.success) {
|
|
261
272
|
const issues = parsed.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
@@ -2164,1963 +2175,301 @@ var TreeInspector = class {
|
|
|
2164
2175
|
}
|
|
2165
2176
|
};
|
|
2166
2177
|
|
|
2167
|
-
//
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
constructor(sizeOrElements) {
|
|
2174
|
-
if (typeof sizeOrElements === "number") {
|
|
2175
|
-
this.elements = new Array(sizeOrElements).fill(void 0);
|
|
2176
|
-
} else if (Array.isArray(sizeOrElements)) {
|
|
2177
|
-
this.elements = [...sizeOrElements];
|
|
2178
|
-
} else {
|
|
2179
|
-
this.elements = [...sizeOrElements.elements];
|
|
2180
|
-
}
|
|
2181
|
-
}
|
|
2182
|
-
get(index) {
|
|
2183
|
-
return this.elements[index];
|
|
2184
|
-
}
|
|
2185
|
-
set(index, value) {
|
|
2186
|
-
this.elements[index] = value;
|
|
2187
|
-
}
|
|
2188
|
-
asNumber() {
|
|
2189
|
-
throw new Error("Cannot cast array to number");
|
|
2190
|
-
}
|
|
2191
|
-
clone() {
|
|
2192
|
-
return new _ArrayValue(this.elements.map((e) => e.clone()));
|
|
2193
|
-
}
|
|
2194
|
-
asString() {
|
|
2195
|
-
return `[${this.elements.map((e) => e.asString()).join(", ")}]`;
|
|
2196
|
-
}
|
|
2197
|
-
toString() {
|
|
2198
|
-
return this.asString();
|
|
2199
|
-
}
|
|
2200
|
-
};
|
|
2201
|
-
|
|
2202
|
-
// vendor/dsl/parser/lib/number-value.ts
|
|
2203
|
-
var NumberValue = class _NumberValue {
|
|
2178
|
+
// src/dsl/ast-executor.ts
|
|
2179
|
+
import {
|
|
2180
|
+
ExecutionWalker
|
|
2181
|
+
} from "@unotest/dsl/executor";
|
|
2182
|
+
import { UnsupportedAstNodeError } from "@unotest/dsl/executor";
|
|
2183
|
+
var AstExecutor = class {
|
|
2204
2184
|
static {
|
|
2205
|
-
__name(this, "
|
|
2206
|
-
}
|
|
2207
|
-
static ZERO = new _NumberValue(0);
|
|
2208
|
-
value;
|
|
2209
|
-
constructor(value) {
|
|
2210
|
-
if (typeof value === "boolean") {
|
|
2211
|
-
this.value = value ? 1 : 0;
|
|
2212
|
-
} else {
|
|
2213
|
-
this.value = value;
|
|
2214
|
-
}
|
|
2215
|
-
}
|
|
2216
|
-
clone() {
|
|
2217
|
-
return new _NumberValue(this.value);
|
|
2218
|
-
}
|
|
2219
|
-
asNumber() {
|
|
2220
|
-
return this.value;
|
|
2185
|
+
__name(this, "AstExecutor");
|
|
2221
2186
|
}
|
|
2222
|
-
|
|
2223
|
-
|
|
2187
|
+
walker;
|
|
2188
|
+
constructor(registry) {
|
|
2189
|
+
this.walker = new ExecutionWalker(
|
|
2190
|
+
registry
|
|
2191
|
+
);
|
|
2224
2192
|
}
|
|
2225
|
-
|
|
2226
|
-
return this.
|
|
2193
|
+
run(program, testName, runtime, opts, helpers) {
|
|
2194
|
+
return this.walker.run(
|
|
2195
|
+
program,
|
|
2196
|
+
testName,
|
|
2197
|
+
runtime,
|
|
2198
|
+
{ resumeRetriesFailedStep: true, ...opts },
|
|
2199
|
+
helpers
|
|
2200
|
+
);
|
|
2227
2201
|
}
|
|
2228
2202
|
};
|
|
2229
2203
|
|
|
2230
|
-
//
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
["open", { execute(...args) {
|
|
2264
|
-
return NumberValue.ZERO;
|
|
2265
|
-
} }],
|
|
2266
|
-
// prettier-ignore
|
|
2267
|
-
["String", { execute(...args) {
|
|
2268
|
-
return NumberValue.ZERO;
|
|
2269
|
-
} }]
|
|
2270
|
-
]);
|
|
2271
|
-
static isExists(key) {
|
|
2272
|
-
return this.functions.has(key);
|
|
2273
|
-
}
|
|
2274
|
-
static get(key) {
|
|
2275
|
-
const f = this.functions.get(key);
|
|
2276
|
-
if (!f) {
|
|
2277
|
-
throw new TypeError(`Unknown function: ${key}`);
|
|
2278
|
-
}
|
|
2279
|
-
return f;
|
|
2280
|
-
}
|
|
2281
|
-
static set(name, value) {
|
|
2282
|
-
this.functions.set(name, value);
|
|
2283
|
-
}
|
|
2284
|
-
};
|
|
2204
|
+
// src/dsl/linter.ts
|
|
2205
|
+
import {
|
|
2206
|
+
AssignmentStatement,
|
|
2207
|
+
ArrayAccessExpression,
|
|
2208
|
+
ArrayAssignmentStatement,
|
|
2209
|
+
ArrayExpression,
|
|
2210
|
+
BinaryExpression,
|
|
2211
|
+
BlockStatement,
|
|
2212
|
+
BreakStatement,
|
|
2213
|
+
ConditionalExpression,
|
|
2214
|
+
ContinueStatement,
|
|
2215
|
+
DoWhileStatement,
|
|
2216
|
+
ForStatement,
|
|
2217
|
+
FunctionDefineStatement,
|
|
2218
|
+
FunctionStatement,
|
|
2219
|
+
FunctionalExpression,
|
|
2220
|
+
IfStatement,
|
|
2221
|
+
IncrementExpression,
|
|
2222
|
+
IncrementStatement,
|
|
2223
|
+
MemberCallExpression,
|
|
2224
|
+
MetaBlockStatement,
|
|
2225
|
+
ObjectExpression,
|
|
2226
|
+
PropertyAccessExpression,
|
|
2227
|
+
PrintStatement,
|
|
2228
|
+
ReturnStatement,
|
|
2229
|
+
StepStatement,
|
|
2230
|
+
UnaryExpression,
|
|
2231
|
+
ValueExpression,
|
|
2232
|
+
VarStatement,
|
|
2233
|
+
VariableExpression,
|
|
2234
|
+
WhileStatement
|
|
2235
|
+
} from "@unotest/dsl";
|
|
2236
|
+
import { validateDsl } from "@unotest/dsl/validator";
|
|
2285
2237
|
|
|
2286
|
-
//
|
|
2287
|
-
var
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
set(key, value) {
|
|
2299
|
-
this.properties.set(key, value);
|
|
2300
|
-
}
|
|
2301
|
-
asNumber() {
|
|
2302
|
-
return this.properties.size;
|
|
2303
|
-
}
|
|
2304
|
-
asString() {
|
|
2305
|
-
const entries = Array.from(this.properties.entries()).map(([key, value]) => `${key}: ${value.asString()}`).join(", ");
|
|
2306
|
-
return `{${entries}}`;
|
|
2307
|
-
}
|
|
2308
|
-
clone() {
|
|
2309
|
-
const clonedProperties = /* @__PURE__ */ new Map();
|
|
2310
|
-
this.properties.forEach((value, key) => {
|
|
2311
|
-
clonedProperties.set(key, value.clone());
|
|
2312
|
-
});
|
|
2313
|
-
return new _ObjectValue(clonedProperties);
|
|
2314
|
-
}
|
|
2315
|
-
getProperties() {
|
|
2316
|
-
return this.properties;
|
|
2317
|
-
}
|
|
2238
|
+
// src/dsl/mobile-dsl-registry.ts
|
|
2239
|
+
var ARG_KIND = {
|
|
2240
|
+
// Mobile's loose types map onto the engine's lenient predicates —
|
|
2241
|
+
// variables / helper-call results of unknown kind always pass, like the
|
|
2242
|
+
// old E6 "only flag the unambiguous" policy. `selector` maps to the
|
|
2243
|
+
// STRICT locator kind: mobile selectors only come from selector
|
|
2244
|
+
// functions (getByTestId/getByText/…), never bare strings.
|
|
2245
|
+
string: "stringLike",
|
|
2246
|
+
number: "number",
|
|
2247
|
+
boolean: "boolLike",
|
|
2248
|
+
selector: "strictLocatorLike",
|
|
2249
|
+
any: "any"
|
|
2318
2250
|
};
|
|
2319
|
-
|
|
2320
|
-
//
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
__name(this, "StringValue");
|
|
2324
|
-
}
|
|
2325
|
-
value;
|
|
2326
|
-
constructor(value) {
|
|
2327
|
-
this.value = value;
|
|
2328
|
-
}
|
|
2329
|
-
clone() {
|
|
2330
|
-
return new _StringValue(this.value);
|
|
2331
|
-
}
|
|
2332
|
-
asNumber() {
|
|
2333
|
-
try {
|
|
2334
|
-
return parseFloat(this.value);
|
|
2335
|
-
} catch (error) {
|
|
2336
|
-
return 0;
|
|
2337
|
-
}
|
|
2338
|
-
}
|
|
2339
|
-
asString() {
|
|
2340
|
-
return this.value;
|
|
2341
|
-
}
|
|
2342
|
-
toString() {
|
|
2343
|
-
return this.asString();
|
|
2344
|
-
}
|
|
2251
|
+
var ARG_KIND_OVERRIDES = {
|
|
2252
|
+
// apiCall(method, path, jsonBody?, jsonHeaders?) — body/headers must be
|
|
2253
|
+
// JSON strings; replaces the old local E4 check.
|
|
2254
|
+
apiCall: { 2: "jsonObjectString", 3: "jsonObjectString" }
|
|
2345
2255
|
};
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
body;
|
|
2354
|
-
constructor(argNames, body) {
|
|
2355
|
-
this.argNames = argNames;
|
|
2356
|
-
this.body = body;
|
|
2357
|
-
}
|
|
2358
|
-
getArgsCount() {
|
|
2359
|
-
return this.argNames.length;
|
|
2360
|
-
}
|
|
2361
|
-
getArgsName(index) {
|
|
2362
|
-
if (index < 0 || index >= this.argNames.length) {
|
|
2363
|
-
return "";
|
|
2364
|
-
}
|
|
2365
|
-
return this.argNames[index];
|
|
2366
|
-
}
|
|
2367
|
-
execute(...args) {
|
|
2368
|
-
try {
|
|
2369
|
-
this.body.execute();
|
|
2370
|
-
return NumberValue.ZERO;
|
|
2371
|
-
} catch (e) {
|
|
2372
|
-
if (e instanceof ReturnStatement) {
|
|
2373
|
-
return e.getResult();
|
|
2374
|
-
}
|
|
2375
|
-
throw e;
|
|
2376
|
-
}
|
|
2377
|
-
}
|
|
2256
|
+
var RETURN_KIND = {
|
|
2257
|
+
void: "void",
|
|
2258
|
+
string: "string",
|
|
2259
|
+
number: "number",
|
|
2260
|
+
boolean: "bool",
|
|
2261
|
+
selector: "locator",
|
|
2262
|
+
any: "unknown"
|
|
2378
2263
|
};
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2264
|
+
function contractFor(fn) {
|
|
2265
|
+
const required = fn.minArgs ?? fn.argTypes.length;
|
|
2266
|
+
const overrides = ARG_KIND_OVERRIDES[fn.name] ?? {};
|
|
2267
|
+
return {
|
|
2268
|
+
name: fn.name,
|
|
2269
|
+
signature: {
|
|
2270
|
+
args: fn.argTypes.map((t, i) => ({
|
|
2271
|
+
label: `arg${i + 1}`,
|
|
2272
|
+
kind: overrides[i] ?? ARG_KIND[t],
|
|
2273
|
+
required: i < required
|
|
2274
|
+
})),
|
|
2275
|
+
...fn.variadic ? { variadic: true } : {}
|
|
2276
|
+
},
|
|
2277
|
+
support: "visual",
|
|
2278
|
+
returns: { mode: "fixed", kind: RETURN_KIND[fn.returnType] },
|
|
2279
|
+
// Mobile's frozen subset (D-4) forbids method chains entirely — the
|
|
2280
|
+
// slim linter rejects MemberCallExpression before the engine ever
|
|
2281
|
+
// sees a chain, so no contract is chainable.
|
|
2282
|
+
locatorCapability: fn.returnType === "selector" ? "locator" : "none"
|
|
2283
|
+
};
|
|
2284
|
+
}
|
|
2285
|
+
__name(contractFor, "contractFor");
|
|
2286
|
+
function helperContract(name) {
|
|
2287
|
+
return {
|
|
2288
|
+
name,
|
|
2289
|
+
signature: { args: [], variadic: true },
|
|
2290
|
+
support: "visual",
|
|
2291
|
+
returns: { mode: "fixed", kind: "unknown" },
|
|
2292
|
+
locatorCapability: "none",
|
|
2293
|
+
trusted: true
|
|
2294
|
+
};
|
|
2295
|
+
}
|
|
2296
|
+
__name(helperContract, "helperContract");
|
|
2297
|
+
var MobileDslRegistry = class {
|
|
2382
2298
|
static {
|
|
2383
|
-
__name(this, "
|
|
2384
|
-
}
|
|
2385
|
-
static stack = [];
|
|
2386
|
-
static variables = /* @__PURE__ */ new Map([
|
|
2387
|
-
["PI", new NumberValue(Math.PI)],
|
|
2388
|
-
["E", new NumberValue(Math.E)]
|
|
2389
|
-
]);
|
|
2390
|
-
static push() {
|
|
2391
|
-
const clone = /* @__PURE__ */ new Map([]);
|
|
2392
|
-
for (const [key, value] of this.variables.entries()) {
|
|
2393
|
-
clone.set(key, value);
|
|
2394
|
-
}
|
|
2395
|
-
this.stack.push(clone);
|
|
2299
|
+
__name(this, "MobileDslRegistry");
|
|
2396
2300
|
}
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2301
|
+
byName = /* @__PURE__ */ new Map();
|
|
2302
|
+
constructor(functions, userFunctionNames = []) {
|
|
2303
|
+
for (const fn of functions) this.byName.set(fn.name, contractFor(fn));
|
|
2304
|
+
for (const name of userFunctionNames) {
|
|
2305
|
+
if (!this.byName.has(name)) this.byName.set(name, helperContract(name));
|
|
2401
2306
|
}
|
|
2402
|
-
this.variables = v;
|
|
2403
2307
|
}
|
|
2404
|
-
|
|
2405
|
-
return this.
|
|
2308
|
+
resolve(name) {
|
|
2309
|
+
return this.byName.get(name) ?? null;
|
|
2406
2310
|
}
|
|
2407
|
-
|
|
2408
|
-
return this.
|
|
2311
|
+
resolveContract(name) {
|
|
2312
|
+
return this.resolve(name);
|
|
2409
2313
|
}
|
|
2410
|
-
|
|
2411
|
-
this.
|
|
2314
|
+
inferReturnKind(expression, _session) {
|
|
2315
|
+
const contract = this.resolveContract(expression.name);
|
|
2316
|
+
if (!contract || contract.returns.mode !== "fixed") return "unknown";
|
|
2317
|
+
if (contract.returns.kind === "void") return "unknown";
|
|
2318
|
+
return contract.returns.kind;
|
|
2412
2319
|
}
|
|
2413
2320
|
};
|
|
2321
|
+
var MOBILE_DISABLED_ENGINE_RULES = [
|
|
2322
|
+
"unsupported-statement",
|
|
2323
|
+
"unsupported-expression",
|
|
2324
|
+
"for-shape",
|
|
2325
|
+
"var-shape",
|
|
2326
|
+
"if-shape",
|
|
2327
|
+
"string-concat",
|
|
2328
|
+
"semantic-loss",
|
|
2329
|
+
"list-arg",
|
|
2330
|
+
"wrapped-format"
|
|
2331
|
+
];
|
|
2414
2332
|
|
|
2415
|
-
//
|
|
2416
|
-
var
|
|
2333
|
+
// src/dsl/function-registry.ts
|
|
2334
|
+
var FunctionRegistry = class {
|
|
2417
2335
|
static {
|
|
2418
|
-
__name(this, "
|
|
2419
|
-
}
|
|
2420
|
-
variable;
|
|
2421
|
-
indexes;
|
|
2422
|
-
token;
|
|
2423
|
-
constructor(variable, indexes, token) {
|
|
2424
|
-
this.variable = variable;
|
|
2425
|
-
this.indexes = indexes;
|
|
2426
|
-
this.token = token;
|
|
2427
|
-
}
|
|
2428
|
-
eval() {
|
|
2429
|
-
const i = this.lastIndex();
|
|
2430
|
-
const arr = this.getArray();
|
|
2431
|
-
return arr.get(i);
|
|
2432
|
-
}
|
|
2433
|
-
lastIndex() {
|
|
2434
|
-
return this.index(this.indexes.length - 1);
|
|
2435
|
-
}
|
|
2436
|
-
index(index) {
|
|
2437
|
-
return this.indexes[index].eval().asNumber();
|
|
2438
|
-
}
|
|
2439
|
-
getArray() {
|
|
2440
|
-
let arr = this.consumeArray(Variables.get(this.variable));
|
|
2441
|
-
const last = this.indexes.length - 1;
|
|
2442
|
-
for (let i = 0; i < last; i++) {
|
|
2443
|
-
arr = this.consumeArray(arr.get(this.index(i)));
|
|
2444
|
-
}
|
|
2445
|
-
return arr;
|
|
2336
|
+
__name(this, "FunctionRegistry");
|
|
2446
2337
|
}
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2338
|
+
map = /* @__PURE__ */ new Map();
|
|
2339
|
+
register(fn) {
|
|
2340
|
+
if (this.map.has(fn.name)) {
|
|
2341
|
+
throw new Error(`Duplicate DSL function: ${fn.name}`);
|
|
2450
2342
|
}
|
|
2451
|
-
|
|
2452
|
-
}
|
|
2453
|
-
toString() {
|
|
2454
|
-
return `${this.variable + this.indexes}`;
|
|
2455
|
-
}
|
|
2456
|
-
};
|
|
2457
|
-
|
|
2458
|
-
// vendor/dsl/parser/ast/array-assignment-statement.ts
|
|
2459
|
-
var ArrayAssignmentStatement = class {
|
|
2460
|
-
static {
|
|
2461
|
-
__name(this, "ArrayAssignmentStatement");
|
|
2343
|
+
this.map.set(fn.name, fn);
|
|
2462
2344
|
}
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
token;
|
|
2466
|
-
constructor(array, expression, token) {
|
|
2467
|
-
this.array = array;
|
|
2468
|
-
this.expression = expression;
|
|
2469
|
-
this.token = token;
|
|
2345
|
+
has(name) {
|
|
2346
|
+
return this.map.has(name);
|
|
2470
2347
|
}
|
|
2471
|
-
|
|
2472
|
-
|
|
2348
|
+
get(name) {
|
|
2349
|
+
return this.map.get(name);
|
|
2473
2350
|
}
|
|
2474
|
-
|
|
2475
|
-
return
|
|
2351
|
+
names() {
|
|
2352
|
+
return [...this.map.keys()].sort();
|
|
2476
2353
|
}
|
|
2477
2354
|
};
|
|
2478
2355
|
|
|
2479
|
-
//
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
const
|
|
2492
|
-
|
|
2493
|
-
}
|
|
2494
|
-
toString() {
|
|
2495
|
-
return `${this.elements.toString()}`;
|
|
2496
|
-
}
|
|
2356
|
+
// src/dsl/functions/alerts.ts
|
|
2357
|
+
function asString(x, fn, idx) {
|
|
2358
|
+
if (typeof x !== "string") throw new Error(`${fn}(): arg ${idx} must be a string. Got ${typeof x}.`);
|
|
2359
|
+
return x;
|
|
2360
|
+
}
|
|
2361
|
+
__name(asString, "asString");
|
|
2362
|
+
var acceptAlert = {
|
|
2363
|
+
name: "acceptAlert",
|
|
2364
|
+
argTypes: ["string"],
|
|
2365
|
+
returnType: "void",
|
|
2366
|
+
minArgs: 0,
|
|
2367
|
+
invoke: /* @__PURE__ */ __name(async (runtime, buttonArg) => {
|
|
2368
|
+
const button = buttonArg !== void 0 ? asString(buttonArg, "acceptAlert", 0) : void 0;
|
|
2369
|
+
await runtime.driver.acceptAlert(runtime.currentDeviceSlot, button);
|
|
2370
|
+
}, "invoke")
|
|
2497
2371
|
};
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
expression;
|
|
2506
|
-
token;
|
|
2507
|
-
constructor(variable, expression, token) {
|
|
2508
|
-
this.variable = variable;
|
|
2509
|
-
this.expression = expression;
|
|
2510
|
-
this.token = token;
|
|
2511
|
-
}
|
|
2512
|
-
execute() {
|
|
2513
|
-
const result = this.expression.eval();
|
|
2514
|
-
Variables.set(this.variable, result);
|
|
2515
|
-
}
|
|
2516
|
-
toString() {
|
|
2517
|
-
return `${this.variable} = ${this.expression}`;
|
|
2518
|
-
}
|
|
2372
|
+
var dismissAlert = {
|
|
2373
|
+
name: "dismissAlert",
|
|
2374
|
+
argTypes: [],
|
|
2375
|
+
returnType: "void",
|
|
2376
|
+
invoke: /* @__PURE__ */ __name(async (runtime) => {
|
|
2377
|
+
await runtime.driver.dismissAlert(runtime.currentDeviceSlot);
|
|
2378
|
+
}, "invoke")
|
|
2519
2379
|
};
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
operation;
|
|
2529
|
-
token;
|
|
2530
|
-
constructor(operator, expr1, expr2, token) {
|
|
2531
|
-
this.expr1 = expr1;
|
|
2532
|
-
this.expr2 = expr2;
|
|
2533
|
-
this.operation = operator;
|
|
2534
|
-
this.token = token;
|
|
2535
|
-
}
|
|
2536
|
-
eval() {
|
|
2537
|
-
const value1 = this.expr1.eval();
|
|
2538
|
-
if (value1 instanceof StringValue || value1 instanceof ArrayValue) {
|
|
2539
|
-
const string1 = value1.asString();
|
|
2540
|
-
const string2 = this.expr2.eval().asString();
|
|
2541
|
-
switch (this.operation) {
|
|
2542
|
-
case "+":
|
|
2543
|
-
return new StringValue(string1 + string2);
|
|
2544
|
-
default:
|
|
2545
|
-
throw new Error("Unrecognized operator for string value");
|
|
2546
|
-
}
|
|
2547
|
-
}
|
|
2548
|
-
const number1 = this.expr1.eval().asNumber();
|
|
2549
|
-
const number2 = this.expr2.eval().asNumber();
|
|
2550
|
-
switch (this.operation) {
|
|
2551
|
-
case "-":
|
|
2552
|
-
return new NumberValue(number1 - number2);
|
|
2553
|
-
case "+":
|
|
2554
|
-
return new NumberValue(number1 + number2);
|
|
2555
|
-
case "/":
|
|
2556
|
-
return new NumberValue(number1 / number2);
|
|
2557
|
-
case "*":
|
|
2558
|
-
default:
|
|
2559
|
-
return new NumberValue(number1 * number2);
|
|
2560
|
-
}
|
|
2561
|
-
}
|
|
2562
|
-
toString() {
|
|
2563
|
-
return `${this.expr1} ${this.operation} ${this.expr2}`;
|
|
2564
|
-
}
|
|
2380
|
+
var readAlert = {
|
|
2381
|
+
name: "readAlert",
|
|
2382
|
+
argTypes: [],
|
|
2383
|
+
returnType: "string",
|
|
2384
|
+
invoke: /* @__PURE__ */ __name(async (runtime) => {
|
|
2385
|
+
const { text } = await runtime.driver.readAlert(runtime.currentDeviceSlot);
|
|
2386
|
+
return text;
|
|
2387
|
+
}, "invoke")
|
|
2565
2388
|
};
|
|
2389
|
+
var ALERT_FUNCTIONS = [acceptAlert, dismissAlert, readAlert];
|
|
2566
2390
|
|
|
2567
|
-
//
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
}
|
|
2572
|
-
statements = [];
|
|
2573
|
-
token;
|
|
2574
|
-
constructor(token) {
|
|
2575
|
-
this.token = token;
|
|
2576
|
-
}
|
|
2577
|
-
add(statement) {
|
|
2578
|
-
this.statements.push(statement);
|
|
2391
|
+
// src/dsl/functions/asserts.ts
|
|
2392
|
+
function asSelector(x, fn, idx) {
|
|
2393
|
+
if (typeof x !== "object" || x === null) {
|
|
2394
|
+
throw new Error(`${fn}(): arg ${idx} must be a Selector. Got ${typeof x}.`);
|
|
2579
2395
|
}
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2396
|
+
return x;
|
|
2397
|
+
}
|
|
2398
|
+
__name(asSelector, "asSelector");
|
|
2399
|
+
function asNumber(x, fn, idx) {
|
|
2400
|
+
if (typeof x !== "number") throw new Error(`${fn}(): arg ${idx} must be a number. Got ${typeof x}.`);
|
|
2401
|
+
return x;
|
|
2402
|
+
}
|
|
2403
|
+
__name(asNumber, "asNumber");
|
|
2404
|
+
var assertEqual = {
|
|
2405
|
+
name: "assertEqual",
|
|
2406
|
+
argTypes: ["any", "any"],
|
|
2407
|
+
returnType: "void",
|
|
2408
|
+
invoke: /* @__PURE__ */ __name((_runtime, actual, expected) => {
|
|
2409
|
+
if (actual !== expected) {
|
|
2410
|
+
throw new Error(
|
|
2411
|
+
`assertEqual failed: actual ${JSON.stringify(actual)} !== expected ${JSON.stringify(expected)}`
|
|
2412
|
+
);
|
|
2583
2413
|
}
|
|
2584
|
-
}
|
|
2585
|
-
toString() {
|
|
2586
|
-
return `${this.statements.join("\n")}`;
|
|
2587
|
-
}
|
|
2414
|
+
}, "invoke")
|
|
2588
2415
|
};
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
}
|
|
2416
|
+
var assertVisible = {
|
|
2417
|
+
name: "assertVisible",
|
|
2418
|
+
argTypes: ["selector"],
|
|
2419
|
+
returnType: "void",
|
|
2420
|
+
invoke: /* @__PURE__ */ __name(async (runtime, selectorArg) => {
|
|
2421
|
+
const selector = asSelector(selectorArg, "assertVisible", 0);
|
|
2422
|
+
const tree = await runtime.driver.a11yTree(runtime.currentDeviceSlot);
|
|
2423
|
+
const r = runtime.selectorResolver.resolve(tree, selector);
|
|
2424
|
+
if (!r.ok) {
|
|
2425
|
+
throw new Error(
|
|
2426
|
+
`assertVisible failed: ${r.reason}
|
|
2427
|
+
selector: ${JSON.stringify(selector)}
|
|
2428
|
+
candidates: ${JSON.stringify(r.candidates).slice(0, 600)}`
|
|
2429
|
+
);
|
|
2430
|
+
}
|
|
2431
|
+
}, "invoke")
|
|
2606
2432
|
};
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
const value1 = this.expr1.eval();
|
|
2625
|
-
const value2 = this.expr2.eval();
|
|
2626
|
-
let number1;
|
|
2627
|
-
let number2;
|
|
2628
|
-
if (value1 instanceof StringValue) {
|
|
2629
|
-
number1 = number1 = value1.asString().localeCompare(value2.asString());
|
|
2630
|
-
number2 = 0;
|
|
2631
|
-
} else {
|
|
2632
|
-
number1 = this.expr1.eval().asNumber();
|
|
2633
|
-
number2 = this.expr2.eval().asNumber();
|
|
2433
|
+
var assertCount = {
|
|
2434
|
+
name: "assertCount",
|
|
2435
|
+
argTypes: ["selector", "number"],
|
|
2436
|
+
returnType: "void",
|
|
2437
|
+
invoke: /* @__PURE__ */ __name(async (runtime, selectorArg, expectedArg) => {
|
|
2438
|
+
const selector = asSelector(selectorArg, "assertCount", 0);
|
|
2439
|
+
const expected = asNumber(expectedArg, "assertCount", 1);
|
|
2440
|
+
const tree = await runtime.driver.a11yTree(runtime.currentDeviceSlot);
|
|
2441
|
+
let count = 0;
|
|
2442
|
+
while (true) {
|
|
2443
|
+
const probe = { ...selector, ordinal: count };
|
|
2444
|
+
const r = runtime.selectorResolver.resolve(tree, probe);
|
|
2445
|
+
if (!r.ok) break;
|
|
2446
|
+
count++;
|
|
2447
|
+
if (count > 1e3) {
|
|
2448
|
+
throw new Error(`assertCount: aborting after 1000 \u2014 selector too broad`);
|
|
2449
|
+
}
|
|
2634
2450
|
}
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
break;
|
|
2640
|
-
case "<=" /* LTE */:
|
|
2641
|
-
result = number1 <= number2;
|
|
2642
|
-
break;
|
|
2643
|
-
case ">" /* GT */:
|
|
2644
|
-
result = number1 > number2;
|
|
2645
|
-
break;
|
|
2646
|
-
case ">=" /* GTE */:
|
|
2647
|
-
result = number1 >= number2;
|
|
2648
|
-
break;
|
|
2649
|
-
case "!=" /* NEQ */:
|
|
2650
|
-
result = number1 != number2;
|
|
2651
|
-
break;
|
|
2652
|
-
case "&&" /* AND */:
|
|
2653
|
-
result = number1 != 0 && number2 != 0;
|
|
2654
|
-
break;
|
|
2655
|
-
case "||" /* OR */:
|
|
2656
|
-
result = number1 != 0 || number2 != 0;
|
|
2657
|
-
break;
|
|
2658
|
-
case "==" /* EQ */:
|
|
2659
|
-
default:
|
|
2660
|
-
result = number1 === number2;
|
|
2451
|
+
if (count !== expected) {
|
|
2452
|
+
throw new Error(
|
|
2453
|
+
`assertCount failed: ${JSON.stringify(selector)} matched ${count} time(s), expected ${expected}`
|
|
2454
|
+
);
|
|
2661
2455
|
}
|
|
2662
|
-
|
|
2663
|
-
}
|
|
2664
|
-
toString() {
|
|
2665
|
-
return `${this.expr1} ${this.operation} ${this.expr2}`;
|
|
2666
|
-
}
|
|
2456
|
+
}, "invoke")
|
|
2667
2457
|
};
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
this.token = token;
|
|
2458
|
+
async function assertEnabledState(runtime, selectorArg, expected, fnName) {
|
|
2459
|
+
const selector = asSelector(selectorArg, fnName, 0);
|
|
2460
|
+
const tree = await runtime.driver.a11yTree(runtime.currentDeviceSlot);
|
|
2461
|
+
const r = runtime.selectorResolver.resolve(tree, selector);
|
|
2462
|
+
if (!r.ok) {
|
|
2463
|
+
throw new Error(
|
|
2464
|
+
`${fnName} failed: selector did not match. ${r.reason}
|
|
2465
|
+
selector: ${JSON.stringify(selector)}`
|
|
2466
|
+
);
|
|
2678
2467
|
}
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
}
|
|
2685
|
-
};
|
|
2686
|
-
|
|
2687
|
-
// vendor/dsl/parser/ast/do-while-statement.ts
|
|
2688
|
-
var DoWhileStatement = class {
|
|
2689
|
-
static {
|
|
2690
|
-
__name(this, "DoWhileStatement");
|
|
2691
|
-
}
|
|
2692
|
-
condition;
|
|
2693
|
-
statement;
|
|
2694
|
-
token;
|
|
2695
|
-
constructor(expression, statement, token) {
|
|
2696
|
-
this.condition = expression;
|
|
2697
|
-
this.statement = statement;
|
|
2698
|
-
this.token = token;
|
|
2699
|
-
}
|
|
2700
|
-
execute() {
|
|
2701
|
-
do {
|
|
2702
|
-
try {
|
|
2703
|
-
this.statement.execute();
|
|
2704
|
-
} catch (e) {
|
|
2705
|
-
if (e instanceof BreakStatement) {
|
|
2706
|
-
break;
|
|
2707
|
-
} else if (e instanceof ContinueStatement) {
|
|
2708
|
-
} else {
|
|
2709
|
-
throw e;
|
|
2710
|
-
}
|
|
2711
|
-
}
|
|
2712
|
-
} while (this.condition.eval().asNumber() != 0);
|
|
2713
|
-
}
|
|
2714
|
-
toString() {
|
|
2715
|
-
return `do {
|
|
2716
|
-
${this.statement}
|
|
2717
|
-
}
|
|
2718
|
-
while (${this.condition})
|
|
2719
|
-
`;
|
|
2720
|
-
}
|
|
2721
|
-
};
|
|
2722
|
-
|
|
2723
|
-
// vendor/dsl/parser/ast/for-statement.ts
|
|
2724
|
-
var ForStatement = class {
|
|
2725
|
-
static {
|
|
2726
|
-
__name(this, "ForStatement");
|
|
2727
|
-
}
|
|
2728
|
-
initialization;
|
|
2729
|
-
termination;
|
|
2730
|
-
increment;
|
|
2731
|
-
block;
|
|
2732
|
-
token;
|
|
2733
|
-
constructor(initialization, termination, increment, block, token) {
|
|
2734
|
-
this.initialization = initialization;
|
|
2735
|
-
this.termination = termination;
|
|
2736
|
-
this.increment = increment;
|
|
2737
|
-
this.block = block;
|
|
2738
|
-
this.token = token;
|
|
2739
|
-
}
|
|
2740
|
-
execute() {
|
|
2741
|
-
for (this.initialization.execute(); this.termination.eval().asNumber() != 0; this.increment.execute()) {
|
|
2742
|
-
try {
|
|
2743
|
-
this.block.execute();
|
|
2744
|
-
} catch (e) {
|
|
2745
|
-
if (e instanceof BreakStatement) {
|
|
2746
|
-
break;
|
|
2747
|
-
} else if (e instanceof ContinueStatement) {
|
|
2748
|
-
} else {
|
|
2749
|
-
throw e;
|
|
2750
|
-
}
|
|
2751
|
-
}
|
|
2752
|
-
}
|
|
2753
|
-
}
|
|
2754
|
-
toString() {
|
|
2755
|
-
return `for (${this.initialization}; ${this.termination}; ${this.increment})
|
|
2756
|
-
{
|
|
2757
|
-
${this.block}
|
|
2758
|
-
}`;
|
|
2759
|
-
}
|
|
2760
|
-
};
|
|
2761
|
-
|
|
2762
|
-
// vendor/dsl/parser/ast/function-define-statement.ts
|
|
2763
|
-
var FunctionDefineStatement = class {
|
|
2764
|
-
static {
|
|
2765
|
-
__name(this, "FunctionDefineStatement");
|
|
2766
|
-
}
|
|
2767
|
-
name;
|
|
2768
|
-
argNames;
|
|
2769
|
-
body;
|
|
2770
|
-
token;
|
|
2771
|
-
constructor(name, argNames, body, token) {
|
|
2772
|
-
this.name = name;
|
|
2773
|
-
this.argNames = argNames;
|
|
2774
|
-
this.body = body;
|
|
2775
|
-
this.token = token;
|
|
2776
|
-
}
|
|
2777
|
-
execute() {
|
|
2778
|
-
Functions.set(this.name, new UserDefineFunction(this.argNames, this.body));
|
|
2779
|
-
}
|
|
2780
|
-
toString() {
|
|
2781
|
-
const argsString = this.argNames.map((arg) => arg.toString()).join(", ");
|
|
2782
|
-
return `function ${this.name}(${argsString})`;
|
|
2783
|
-
}
|
|
2784
|
-
};
|
|
2785
|
-
|
|
2786
|
-
// vendor/dsl/parser/ast/function-statement.ts
|
|
2787
|
-
var FunctionStatement = class {
|
|
2788
|
-
static {
|
|
2789
|
-
__name(this, "FunctionStatement");
|
|
2790
|
-
}
|
|
2791
|
-
functionalExpression;
|
|
2792
|
-
token;
|
|
2793
|
-
constructor(func, token) {
|
|
2794
|
-
this.functionalExpression = func;
|
|
2795
|
-
this.token = token;
|
|
2796
|
-
}
|
|
2797
|
-
execute() {
|
|
2798
|
-
this.functionalExpression.eval();
|
|
2799
|
-
}
|
|
2800
|
-
toString() {
|
|
2801
|
-
return `${this.functionalExpression.toString()}`;
|
|
2802
|
-
}
|
|
2803
|
-
};
|
|
2804
|
-
|
|
2805
|
-
// vendor/dsl/parser/ast/functional-expression.ts
|
|
2806
|
-
var FunctionalExpression = class {
|
|
2807
|
-
static {
|
|
2808
|
-
__name(this, "FunctionalExpression");
|
|
2809
|
-
}
|
|
2810
|
-
name;
|
|
2811
|
-
arguments;
|
|
2812
|
-
token;
|
|
2813
|
-
constructor(name, argument, token) {
|
|
2814
|
-
this.name = name;
|
|
2815
|
-
this.arguments = argument;
|
|
2816
|
-
this.token = token;
|
|
2817
|
-
}
|
|
2818
|
-
addArgument(argument) {
|
|
2819
|
-
return this.arguments.push(argument);
|
|
2820
|
-
}
|
|
2821
|
-
eval() {
|
|
2822
|
-
const values = this.arguments.map((v) => v.eval());
|
|
2823
|
-
const size = values.length;
|
|
2824
|
-
const func = Functions.get(this.name);
|
|
2825
|
-
if (func instanceof UserDefineFunction) {
|
|
2826
|
-
const userFunction = func;
|
|
2827
|
-
if (values.length !== userFunction.getArgsCount()) {
|
|
2828
|
-
throw new Error(`Invalid argument count for function ${this.name}: expected ${size}`);
|
|
2829
|
-
}
|
|
2830
|
-
Variables.push();
|
|
2831
|
-
for (let i = 0; i < size; i++) {
|
|
2832
|
-
Variables.set(userFunction.getArgsName(i), values[i]);
|
|
2833
|
-
}
|
|
2834
|
-
const result = userFunction.execute(...values);
|
|
2835
|
-
Variables.pop();
|
|
2836
|
-
return result;
|
|
2837
|
-
}
|
|
2838
|
-
return func.execute(...values);
|
|
2839
|
-
}
|
|
2840
|
-
toString() {
|
|
2841
|
-
const argsString = this.arguments.map((arg) => arg.toString()).join(", ");
|
|
2842
|
-
return `${this.name}(${argsString})`;
|
|
2843
|
-
}
|
|
2844
|
-
};
|
|
2845
|
-
|
|
2846
|
-
// vendor/dsl/parser/ast/if-statement.ts
|
|
2847
|
-
var IfStatement = class {
|
|
2848
|
-
static {
|
|
2849
|
-
__name(this, "IfStatement");
|
|
2850
|
-
}
|
|
2851
|
-
expression;
|
|
2852
|
-
ifStatement;
|
|
2853
|
-
elseStatement;
|
|
2854
|
-
token;
|
|
2855
|
-
constructor(expression, ifStatement, elseStatement, token) {
|
|
2856
|
-
this.expression = expression;
|
|
2857
|
-
this.ifStatement = ifStatement;
|
|
2858
|
-
this.elseStatement = elseStatement;
|
|
2859
|
-
this.token = token;
|
|
2860
|
-
}
|
|
2861
|
-
execute() {
|
|
2862
|
-
const result = this.expression.eval().asNumber();
|
|
2863
|
-
if (result != 0) {
|
|
2864
|
-
this.ifStatement.execute();
|
|
2865
|
-
} else if (this.elseStatement != null) {
|
|
2866
|
-
this.elseStatement.execute();
|
|
2867
|
-
}
|
|
2868
|
-
}
|
|
2869
|
-
toString() {
|
|
2870
|
-
let result = `if ${this.expression} ${this.ifStatement}`;
|
|
2871
|
-
if (this.elseStatement != null) {
|
|
2872
|
-
result += ` else ${this.elseStatement}`;
|
|
2873
|
-
}
|
|
2874
|
-
return result.toString();
|
|
2875
|
-
}
|
|
2876
|
-
};
|
|
2877
|
-
|
|
2878
|
-
// vendor/dsl/parser/ast/increment-expression.ts
|
|
2879
|
-
var IncrementExpression = class {
|
|
2880
|
-
static {
|
|
2881
|
-
__name(this, "IncrementExpression");
|
|
2882
|
-
}
|
|
2883
|
-
variable;
|
|
2884
|
-
type;
|
|
2885
|
-
token;
|
|
2886
|
-
constructor(variable, type, token) {
|
|
2887
|
-
this.variable = variable;
|
|
2888
|
-
this.type = type;
|
|
2889
|
-
this.token = token;
|
|
2890
|
-
}
|
|
2891
|
-
eval() {
|
|
2892
|
-
const varName = this.variable.getName();
|
|
2893
|
-
const currentValue = Variables.get(varName);
|
|
2894
|
-
if (!(currentValue instanceof NumberValue)) {
|
|
2895
|
-
throw new Error(`Cannot increment/decrement non-numeric value`);
|
|
2896
|
-
}
|
|
2897
|
-
const currentNumber = currentValue.asNumber();
|
|
2898
|
-
let newNumber;
|
|
2899
|
-
switch (this.type) {
|
|
2900
|
-
case 0 /* PREFIX_INCREMENT */:
|
|
2901
|
-
newNumber = currentNumber + 1;
|
|
2902
|
-
Variables.set(varName, new NumberValue(newNumber));
|
|
2903
|
-
return new NumberValue(newNumber);
|
|
2904
|
-
case 1 /* PREFIX_DECREMENT */:
|
|
2905
|
-
newNumber = currentNumber - 1;
|
|
2906
|
-
Variables.set(varName, new NumberValue(newNumber));
|
|
2907
|
-
return new NumberValue(newNumber);
|
|
2908
|
-
case 2 /* POSTFIX_INCREMENT */:
|
|
2909
|
-
Variables.set(varName, new NumberValue(currentNumber + 1));
|
|
2910
|
-
return new NumberValue(currentNumber);
|
|
2911
|
-
case 3 /* POSTFIX_DECREMENT */:
|
|
2912
|
-
Variables.set(varName, new NumberValue(currentNumber - 1));
|
|
2913
|
-
return new NumberValue(currentNumber);
|
|
2914
|
-
}
|
|
2915
|
-
}
|
|
2916
|
-
};
|
|
2917
|
-
|
|
2918
|
-
// vendor/dsl/parser/ast/increment-statement.ts
|
|
2919
|
-
var IncrementStatement = class {
|
|
2920
|
-
static {
|
|
2921
|
-
__name(this, "IncrementStatement");
|
|
2922
|
-
}
|
|
2923
|
-
expression;
|
|
2924
|
-
token;
|
|
2925
|
-
constructor(expression, token) {
|
|
2926
|
-
this.expression = expression;
|
|
2927
|
-
this.token = token;
|
|
2928
|
-
}
|
|
2929
|
-
execute() {
|
|
2930
|
-
this.expression.eval();
|
|
2931
|
-
}
|
|
2932
|
-
};
|
|
2933
|
-
|
|
2934
|
-
// vendor/dsl/parser/ast/meta-block-statement.ts
|
|
2935
|
-
var MetaBlockStatement = class {
|
|
2936
|
-
static {
|
|
2937
|
-
__name(this, "MetaBlockStatement");
|
|
2938
|
-
}
|
|
2939
|
-
name;
|
|
2940
|
-
argNames;
|
|
2941
|
-
body;
|
|
2942
|
-
token;
|
|
2943
|
-
constructor(name, argNames, body, token) {
|
|
2944
|
-
this.name = name;
|
|
2945
|
-
this.argNames = argNames;
|
|
2946
|
-
this.body = body;
|
|
2947
|
-
this.token = token;
|
|
2948
|
-
}
|
|
2949
|
-
execute() {
|
|
2950
|
-
}
|
|
2951
|
-
toString() {
|
|
2952
|
-
const argsString = this.argNames.map((arg) => arg.toString()).join(", ");
|
|
2953
|
-
return `//@${this.name}(${argsString})
|
|
2954
|
-
${this.body}
|
|
2955
|
-
//@end-${this.name}`;
|
|
2956
|
-
}
|
|
2957
|
-
};
|
|
2958
|
-
|
|
2959
|
-
// vendor/dsl/parser/ast/object-expression.ts
|
|
2960
|
-
var ObjectExpression = class {
|
|
2961
|
-
static {
|
|
2962
|
-
__name(this, "ObjectExpression");
|
|
2963
|
-
}
|
|
2964
|
-
properties;
|
|
2965
|
-
token;
|
|
2966
|
-
constructor(properties = /* @__PURE__ */ new Map(), token) {
|
|
2967
|
-
this.properties = properties;
|
|
2968
|
-
this.token = token;
|
|
2969
|
-
}
|
|
2970
|
-
eval() {
|
|
2971
|
-
const evaluatedProperties = /* @__PURE__ */ new Map();
|
|
2972
|
-
this.properties.forEach((expression, key) => {
|
|
2973
|
-
evaluatedProperties.set(key, expression.eval());
|
|
2974
|
-
});
|
|
2975
|
-
return new ObjectValue(evaluatedProperties);
|
|
2976
|
-
}
|
|
2977
|
-
toString() {
|
|
2978
|
-
const entries = Array.from(this.properties.entries()).map(([key, value]) => `${key}: ${value.toString()}`).join(", ");
|
|
2979
|
-
return `{${entries}}`;
|
|
2980
|
-
}
|
|
2981
|
-
};
|
|
2982
|
-
|
|
2983
|
-
// vendor/dsl/parser/ast/print-statement.ts
|
|
2984
|
-
var PrintStatement = class {
|
|
2985
|
-
static {
|
|
2986
|
-
__name(this, "PrintStatement");
|
|
2987
|
-
}
|
|
2988
|
-
expression;
|
|
2989
|
-
token;
|
|
2990
|
-
constructor(expression, token) {
|
|
2991
|
-
this.expression = expression;
|
|
2992
|
-
this.token = token;
|
|
2993
|
-
}
|
|
2994
|
-
execute() {
|
|
2995
|
-
const result = this.expression.eval().asString();
|
|
2996
|
-
console.log(`${result}`);
|
|
2997
|
-
}
|
|
2998
|
-
toString() {
|
|
2999
|
-
return `print ${this.expression}`;
|
|
3000
|
-
}
|
|
3001
|
-
};
|
|
3002
|
-
|
|
3003
|
-
// vendor/dsl/parser/ast/return-statement.ts
|
|
3004
|
-
var ReturnStatement = class extends Error {
|
|
3005
|
-
static {
|
|
3006
|
-
__name(this, "ReturnStatement");
|
|
3007
|
-
}
|
|
3008
|
-
expression;
|
|
3009
|
-
result;
|
|
3010
|
-
token;
|
|
3011
|
-
constructor(expression, token) {
|
|
3012
|
-
super("ContinueStatement");
|
|
3013
|
-
this.expression = expression;
|
|
3014
|
-
this.token = token;
|
|
3015
|
-
}
|
|
3016
|
-
getResult() {
|
|
3017
|
-
if (!this.result) {
|
|
3018
|
-
throw new Error("Not execute ReturnStatement");
|
|
3019
|
-
}
|
|
3020
|
-
return this.result;
|
|
3021
|
-
}
|
|
3022
|
-
execute() {
|
|
3023
|
-
this.result = this.expression.eval();
|
|
3024
|
-
throw this;
|
|
3025
|
-
}
|
|
3026
|
-
toString() {
|
|
3027
|
-
return `return`;
|
|
3028
|
-
}
|
|
3029
|
-
};
|
|
3030
|
-
|
|
3031
|
-
// vendor/dsl/parser/ast/unary-expression.ts
|
|
3032
|
-
var UnaryExpression = class {
|
|
3033
|
-
static {
|
|
3034
|
-
__name(this, "UnaryExpression");
|
|
3035
|
-
}
|
|
3036
|
-
expr1;
|
|
3037
|
-
operation;
|
|
3038
|
-
token;
|
|
3039
|
-
constructor(operator, expr1, token) {
|
|
3040
|
-
this.operation = operator;
|
|
3041
|
-
this.expr1 = expr1;
|
|
3042
|
-
this.token = token;
|
|
3043
|
-
}
|
|
3044
|
-
eval() {
|
|
3045
|
-
switch (this.operation) {
|
|
3046
|
-
case "-":
|
|
3047
|
-
return new NumberValue(-this.expr1.eval().asNumber());
|
|
3048
|
-
case "+":
|
|
3049
|
-
default:
|
|
3050
|
-
return new NumberValue(this.expr1.eval().asNumber());
|
|
3051
|
-
}
|
|
3052
|
-
}
|
|
3053
|
-
toString() {
|
|
3054
|
-
return `${this.operation}${this.expr1}`;
|
|
3055
|
-
}
|
|
3056
|
-
};
|
|
3057
|
-
|
|
3058
|
-
// vendor/dsl/parser/token-type.ts
|
|
3059
|
-
var TokenType = /* @__PURE__ */ ((TokenType2) => {
|
|
3060
|
-
TokenType2[TokenType2["NUMBER"] = 0] = "NUMBER";
|
|
3061
|
-
TokenType2[TokenType2["WORD"] = 1] = "WORD";
|
|
3062
|
-
TokenType2[TokenType2["TEXT"] = 2] = "TEXT";
|
|
3063
|
-
TokenType2[TokenType2["VAR"] = 3] = "VAR";
|
|
3064
|
-
TokenType2[TokenType2["PRINT"] = 4] = "PRINT";
|
|
3065
|
-
TokenType2[TokenType2["IF"] = 5] = "IF";
|
|
3066
|
-
TokenType2[TokenType2["ELSE"] = 6] = "ELSE";
|
|
3067
|
-
TokenType2[TokenType2["WHILE"] = 7] = "WHILE";
|
|
3068
|
-
TokenType2[TokenType2["FOR"] = 8] = "FOR";
|
|
3069
|
-
TokenType2[TokenType2["DO"] = 9] = "DO";
|
|
3070
|
-
TokenType2[TokenType2["BREAK"] = 10] = "BREAK";
|
|
3071
|
-
TokenType2[TokenType2["CONTINUE"] = 11] = "CONTINUE";
|
|
3072
|
-
TokenType2[TokenType2["FUNCTION"] = 12] = "FUNCTION";
|
|
3073
|
-
TokenType2[TokenType2["RETURN"] = 13] = "RETURN";
|
|
3074
|
-
TokenType2[TokenType2["PLUS"] = 14] = "PLUS";
|
|
3075
|
-
TokenType2[TokenType2["PLUSPLUS"] = 15] = "PLUSPLUS";
|
|
3076
|
-
TokenType2[TokenType2["MINUS"] = 16] = "MINUS";
|
|
3077
|
-
TokenType2[TokenType2["MINUSMINUS"] = 17] = "MINUSMINUS";
|
|
3078
|
-
TokenType2[TokenType2["STAR"] = 18] = "STAR";
|
|
3079
|
-
TokenType2[TokenType2["SLASH"] = 19] = "SLASH";
|
|
3080
|
-
TokenType2[TokenType2["EQ"] = 20] = "EQ";
|
|
3081
|
-
TokenType2[TokenType2["EQEQ"] = 21] = "EQEQ";
|
|
3082
|
-
TokenType2[TokenType2["EXCL"] = 22] = "EXCL";
|
|
3083
|
-
TokenType2[TokenType2["EXCLEQ"] = 23] = "EXCLEQ";
|
|
3084
|
-
TokenType2[TokenType2["LT"] = 24] = "LT";
|
|
3085
|
-
TokenType2[TokenType2["LTEQ"] = 25] = "LTEQ";
|
|
3086
|
-
TokenType2[TokenType2["GT"] = 26] = "GT";
|
|
3087
|
-
TokenType2[TokenType2["GTEQ"] = 27] = "GTEQ";
|
|
3088
|
-
TokenType2[TokenType2["BAR"] = 28] = "BAR";
|
|
3089
|
-
TokenType2[TokenType2["BARBAR"] = 29] = "BARBAR";
|
|
3090
|
-
TokenType2[TokenType2["AMP"] = 30] = "AMP";
|
|
3091
|
-
TokenType2[TokenType2["AMPAMP"] = 31] = "AMPAMP";
|
|
3092
|
-
TokenType2[TokenType2["LPAREN"] = 32] = "LPAREN";
|
|
3093
|
-
TokenType2[TokenType2["RPAREN"] = 33] = "RPAREN";
|
|
3094
|
-
TokenType2[TokenType2["LBRACE"] = 34] = "LBRACE";
|
|
3095
|
-
TokenType2[TokenType2["RBRACE"] = 35] = "RBRACE";
|
|
3096
|
-
TokenType2[TokenType2["LBRACKET"] = 36] = "LBRACKET";
|
|
3097
|
-
TokenType2[TokenType2["RBRACKET"] = 37] = "RBRACKET";
|
|
3098
|
-
TokenType2[TokenType2["COMMA"] = 38] = "COMMA";
|
|
3099
|
-
TokenType2[TokenType2["COLON"] = 39] = "COLON";
|
|
3100
|
-
TokenType2[TokenType2["SEMICOLON"] = 40] = "SEMICOLON";
|
|
3101
|
-
TokenType2[TokenType2["META_BLOCK"] = 41] = "META_BLOCK";
|
|
3102
|
-
TokenType2[TokenType2["CLOSE_META_BLOCK"] = 42] = "CLOSE_META_BLOCK";
|
|
3103
|
-
TokenType2[TokenType2["EOF"] = 43] = "EOF";
|
|
3104
|
-
return TokenType2;
|
|
3105
|
-
})(TokenType || {});
|
|
3106
|
-
|
|
3107
|
-
// vendor/dsl/parser/ast/value-expression.ts
|
|
3108
|
-
var ValueExpression = class {
|
|
3109
|
-
static {
|
|
3110
|
-
__name(this, "ValueExpression");
|
|
3111
|
-
}
|
|
3112
|
-
value;
|
|
3113
|
-
token;
|
|
3114
|
-
constructor(value, token) {
|
|
3115
|
-
this.token = token;
|
|
3116
|
-
if (token.getType() === 0 /* NUMBER */) {
|
|
3117
|
-
this.value = new NumberValue(parseFloat(value));
|
|
3118
|
-
return;
|
|
3119
|
-
}
|
|
3120
|
-
if (token.getType() === 2 /* TEXT */) {
|
|
3121
|
-
this.value = new StringValue(value);
|
|
3122
|
-
return;
|
|
3123
|
-
}
|
|
3124
|
-
throw new Error(`Unrecognized value. ${token}`);
|
|
3125
|
-
}
|
|
3126
|
-
eval() {
|
|
3127
|
-
return this.value;
|
|
3128
|
-
}
|
|
3129
|
-
toString() {
|
|
3130
|
-
return `${this.value.asString()}`;
|
|
3131
|
-
}
|
|
3132
|
-
};
|
|
3133
|
-
|
|
3134
|
-
// vendor/dsl/parser/lib/undefined-value.ts
|
|
3135
|
-
var UndefinedValue = class _UndefinedValue {
|
|
3136
|
-
static {
|
|
3137
|
-
__name(this, "UndefinedValue");
|
|
3138
|
-
}
|
|
3139
|
-
static UNDEFINED = new _UndefinedValue();
|
|
3140
|
-
clone() {
|
|
3141
|
-
return _UndefinedValue.UNDEFINED;
|
|
3142
|
-
}
|
|
3143
|
-
asNumber() {
|
|
3144
|
-
return NaN;
|
|
3145
|
-
}
|
|
3146
|
-
asString() {
|
|
3147
|
-
return "undefined";
|
|
3148
|
-
}
|
|
3149
|
-
toString() {
|
|
3150
|
-
return this.asString();
|
|
3151
|
-
}
|
|
3152
|
-
};
|
|
3153
|
-
|
|
3154
|
-
// vendor/dsl/parser/ast/var-statement.ts
|
|
3155
|
-
var VarStatement = class {
|
|
3156
|
-
static {
|
|
3157
|
-
__name(this, "VarStatement");
|
|
3158
|
-
}
|
|
3159
|
-
declarators;
|
|
3160
|
-
token;
|
|
3161
|
-
constructor(declarators, token) {
|
|
3162
|
-
this.declarators = declarators;
|
|
3163
|
-
this.token = token;
|
|
3164
|
-
}
|
|
3165
|
-
execute() {
|
|
3166
|
-
for (const declarator of this.declarators) {
|
|
3167
|
-
if (declarator.init) {
|
|
3168
|
-
const value = declarator.init.eval();
|
|
3169
|
-
Variables.set(declarator.name, value);
|
|
3170
|
-
} else {
|
|
3171
|
-
Variables.set(declarator.name, UndefinedValue.UNDEFINED);
|
|
3172
|
-
}
|
|
3173
|
-
}
|
|
3174
|
-
}
|
|
3175
|
-
toString() {
|
|
3176
|
-
const decls = this.declarators.map((d) => d.init ? `${d.name} = ${d.init}` : d.name).join(", ");
|
|
3177
|
-
return `var ${decls}`;
|
|
3178
|
-
}
|
|
3179
|
-
};
|
|
3180
|
-
|
|
3181
|
-
// vendor/dsl/parser/ast/variable-expression.ts
|
|
3182
|
-
var VariableExpression = class {
|
|
3183
|
-
static {
|
|
3184
|
-
__name(this, "VariableExpression");
|
|
3185
|
-
}
|
|
3186
|
-
name;
|
|
3187
|
-
token;
|
|
3188
|
-
constructor(name, token) {
|
|
3189
|
-
this.name = name;
|
|
3190
|
-
this.token = token;
|
|
3191
|
-
}
|
|
3192
|
-
eval() {
|
|
3193
|
-
if (!Variables.isExists(this.name)) {
|
|
3194
|
-
throw new Error(`Constant ${this.name} does not exist!`);
|
|
3195
|
-
}
|
|
3196
|
-
return Variables.get(this.name);
|
|
3197
|
-
}
|
|
3198
|
-
getName() {
|
|
3199
|
-
return this.name;
|
|
3200
|
-
}
|
|
3201
|
-
toString() {
|
|
3202
|
-
return `${this.name}`;
|
|
3203
|
-
}
|
|
3204
|
-
};
|
|
3205
|
-
|
|
3206
|
-
// vendor/dsl/parser/ast/while-statement.ts
|
|
3207
|
-
var WhileStatement = class {
|
|
3208
|
-
static {
|
|
3209
|
-
__name(this, "WhileStatement");
|
|
3210
|
-
}
|
|
3211
|
-
condition;
|
|
3212
|
-
statement;
|
|
3213
|
-
token;
|
|
3214
|
-
constructor(expression, statement, token) {
|
|
3215
|
-
this.condition = expression;
|
|
3216
|
-
this.statement = statement;
|
|
3217
|
-
this.token = token;
|
|
3218
|
-
}
|
|
3219
|
-
execute() {
|
|
3220
|
-
while (this.condition.eval().asNumber() != 0) {
|
|
3221
|
-
try {
|
|
3222
|
-
this.statement.execute();
|
|
3223
|
-
} catch (e) {
|
|
3224
|
-
if (e instanceof BreakStatement) {
|
|
3225
|
-
break;
|
|
3226
|
-
} else if (e instanceof ContinueStatement) {
|
|
3227
|
-
} else {
|
|
3228
|
-
throw e;
|
|
3229
|
-
}
|
|
3230
|
-
}
|
|
3231
|
-
}
|
|
3232
|
-
}
|
|
3233
|
-
toString() {
|
|
3234
|
-
return `while (${this.condition})
|
|
3235
|
-
{
|
|
3236
|
-
${this.statement}
|
|
3237
|
-
}`;
|
|
3238
|
-
}
|
|
3239
|
-
};
|
|
3240
|
-
|
|
3241
|
-
// src/dsl/return-signal.ts
|
|
3242
|
-
var ReturnSignal = class extends Error {
|
|
3243
|
-
constructor(value) {
|
|
3244
|
-
super("ReturnSignal");
|
|
3245
|
-
this.value = value;
|
|
3246
|
-
this.name = "ReturnSignal";
|
|
3247
|
-
}
|
|
3248
|
-
value;
|
|
3249
|
-
static {
|
|
3250
|
-
__name(this, "ReturnSignal");
|
|
3251
|
-
}
|
|
3252
|
-
};
|
|
3253
|
-
|
|
3254
|
-
// src/dsl/user-function-resolver.ts
|
|
3255
|
-
var UserFunctionResolver = class {
|
|
3256
|
-
static {
|
|
3257
|
-
__name(this, "UserFunctionResolver");
|
|
3258
|
-
}
|
|
3259
|
-
fns = /* @__PURE__ */ new Map();
|
|
3260
|
-
register(fnDef, source) {
|
|
3261
|
-
if (this.fns.has(fnDef.name)) {
|
|
3262
|
-
const existing = this.fns.get(fnDef.name);
|
|
3263
|
-
const existingTok = existing.token?.getLine?.();
|
|
3264
|
-
throw new Error(
|
|
3265
|
-
`Duplicate user function "${fnDef.name}"` + (existingTok ? ` (already defined at line ${existingTok}${source ? ` in ${source}` : ""})` : "")
|
|
3266
|
-
);
|
|
3267
|
-
}
|
|
3268
|
-
this.fns.set(fnDef.name, fnDef);
|
|
3269
|
-
}
|
|
3270
|
-
get(name) {
|
|
3271
|
-
return this.fns.get(name);
|
|
3272
|
-
}
|
|
3273
|
-
has(name) {
|
|
3274
|
-
return this.fns.has(name);
|
|
3275
|
-
}
|
|
3276
|
-
names() {
|
|
3277
|
-
return [...this.fns.keys()].sort();
|
|
3278
|
-
}
|
|
3279
|
-
};
|
|
3280
|
-
|
|
3281
|
-
// src/dsl/ast-executor.ts
|
|
3282
|
-
var UnsupportedAstNodeError = class extends Error {
|
|
3283
|
-
static {
|
|
3284
|
-
__name(this, "UnsupportedAstNodeError");
|
|
3285
|
-
}
|
|
3286
|
-
constructor(nodeType, line, col) {
|
|
3287
|
-
super(`Unsupported AST node "${nodeType}" at ${line}:${col} \u2014 not allowed by MVP DSL subset (D-4)`);
|
|
3288
|
-
this.name = "UnsupportedAstNodeError";
|
|
3289
|
-
}
|
|
3290
|
-
};
|
|
3291
|
-
var DEFAULT_MAX_CALL_DEPTH = 32;
|
|
3292
|
-
function isBlockMappable(name) {
|
|
3293
|
-
return name.startsWith("test_") || name.startsWith("flow_");
|
|
3294
|
-
}
|
|
3295
|
-
__name(isBlockMappable, "isBlockMappable");
|
|
3296
|
-
var AstExecutor = class {
|
|
3297
|
-
constructor(registry) {
|
|
3298
|
-
this.registry = registry;
|
|
3299
|
-
}
|
|
3300
|
-
registry;
|
|
3301
|
-
static {
|
|
3302
|
-
__name(this, "AstExecutor");
|
|
3303
|
-
}
|
|
3304
|
-
/**
|
|
3305
|
-
* Run the test function `testName` from the parsed AST `program`.
|
|
3306
|
-
*
|
|
3307
|
-
* @param program entry scenario AST
|
|
3308
|
-
* @param testName name of the entry test_/flow_ function
|
|
3309
|
-
* @param runtime per-run state container
|
|
3310
|
-
* @param opts execution options (budgets, mode)
|
|
3311
|
-
* @param helpers optional helper-file ASTs (Stage 2); their top-level
|
|
3312
|
-
* FunctionDefineStatements are registered alongside
|
|
3313
|
-
* the entry's helpers in the user-function resolver
|
|
3314
|
-
*/
|
|
3315
|
-
async *run(program, testName, runtime, opts, helpers) {
|
|
3316
|
-
const start = Date.now();
|
|
3317
|
-
let stepsExecuted = 0;
|
|
3318
|
-
const userFns = new UserFunctionResolver();
|
|
3319
|
-
const programs = [program, ...helpers ?? []];
|
|
3320
|
-
for (const p of programs) {
|
|
3321
|
-
for (const stmt of p.statements) {
|
|
3322
|
-
if (stmt instanceof FunctionDefineStatement) {
|
|
3323
|
-
userFns.register(stmt);
|
|
3324
|
-
}
|
|
3325
|
-
}
|
|
3326
|
-
}
|
|
3327
|
-
const entry = userFns.get(testName);
|
|
3328
|
-
if (!entry) {
|
|
3329
|
-
const error = {
|
|
3330
|
-
message: `Test function "${testName}" not found in scenario AST`
|
|
3331
|
-
};
|
|
3332
|
-
return { outcome: "failed", stepsExecuted, durationMs: Date.now() - start, error };
|
|
3333
|
-
}
|
|
3334
|
-
if (!(entry.body instanceof BlockStatement)) {
|
|
3335
|
-
const error = {
|
|
3336
|
-
message: `Test "${testName}" body is not a BlockStatement (got ${entry.body.constructor.name})`
|
|
3337
|
-
};
|
|
3338
|
-
return { outcome: "failed", stepsExecuted, durationMs: Date.now() - start, error };
|
|
3339
|
-
}
|
|
3340
|
-
if (!isBlockMappable(testName)) {
|
|
3341
|
-
const error = {
|
|
3342
|
-
message: `Entry function "${testName}" must start with test_ or flow_`
|
|
3343
|
-
};
|
|
3344
|
-
return { outcome: "failed", stepsExecuted, durationMs: Date.now() - start, error };
|
|
3345
|
-
}
|
|
3346
|
-
const getSteps = /* @__PURE__ */ __name(() => stepsExecuted, "getSteps");
|
|
3347
|
-
const setSteps = /* @__PURE__ */ __name((n) => {
|
|
3348
|
-
stepsExecuted = n;
|
|
3349
|
-
}, "setSteps");
|
|
3350
|
-
const callStack = [];
|
|
3351
|
-
try {
|
|
3352
|
-
yield* this.runBlock(entry.body, runtime, opts, getSteps, setSteps, start, userFns, callStack);
|
|
3353
|
-
return { outcome: "completed", stepsExecuted, durationMs: Date.now() - start };
|
|
3354
|
-
} catch (e) {
|
|
3355
|
-
const error = toExecutionError(e);
|
|
3356
|
-
return { outcome: "failed", stepsExecuted, durationMs: Date.now() - start, error };
|
|
3357
|
-
}
|
|
3358
|
-
}
|
|
3359
|
-
// ---- internals ---------------------------------------------------------
|
|
3360
|
-
async *runBlock(block, runtime, opts, getSteps, setSteps, runStartMs, userFns, callStack) {
|
|
3361
|
-
for (const stmt of block.statements) {
|
|
3362
|
-
yield* this.runStatement(stmt, runtime, opts, getSteps, setSteps, runStartMs, userFns, callStack);
|
|
3363
|
-
}
|
|
3364
|
-
}
|
|
3365
|
-
async *runStatement(stmt, runtime, opts, getSteps, setSteps, runStartMs, userFns, callStack) {
|
|
3366
|
-
const { line, col } = locationOf(stmt);
|
|
3367
|
-
const stackSnap = callStack.length === 0 ? void 0 : [...callStack];
|
|
3368
|
-
yield { kind: "step-start", node: stmt, line, col, callStack: stackSnap };
|
|
3369
|
-
let attempted = false;
|
|
3370
|
-
while (true) {
|
|
3371
|
-
this.checkBudgets(opts, getSteps(), runStartMs);
|
|
3372
|
-
try {
|
|
3373
|
-
yield* this.execStatement(stmt, runtime, opts, getSteps, setSteps, runStartMs, userFns, callStack);
|
|
3374
|
-
setSteps(getSteps() + 1);
|
|
3375
|
-
yield { kind: "step-end", node: stmt, line, col, ok: true, callStack: stackSnap };
|
|
3376
|
-
if (opts.mode === "step") yield { kind: "paused", reason: "step", line, col };
|
|
3377
|
-
return;
|
|
3378
|
-
} catch (e) {
|
|
3379
|
-
if (e instanceof ReturnSignal) throw e;
|
|
3380
|
-
const error = toExecutionError(e);
|
|
3381
|
-
yield { kind: "step-end", node: stmt, line, col, ok: false, error, callStack: stackSnap };
|
|
3382
|
-
if (!opts.pauseOnFailure) throw e;
|
|
3383
|
-
yield { kind: "paused", reason: "failure", line, col };
|
|
3384
|
-
attempted = true;
|
|
3385
|
-
if (attempted) continue;
|
|
3386
|
-
return;
|
|
3387
|
-
}
|
|
3388
|
-
}
|
|
3389
|
-
}
|
|
3390
|
-
async *execStatement(stmt, runtime, opts, getSteps, setSteps, runStartMs, userFns, callStack) {
|
|
3391
|
-
if (stmt instanceof BlockStatement) {
|
|
3392
|
-
yield* this.runBlock(stmt, runtime, opts, getSteps, setSteps, runStartMs, userFns, callStack);
|
|
3393
|
-
return;
|
|
3394
|
-
}
|
|
3395
|
-
if (stmt instanceof AssignmentStatement) {
|
|
3396
|
-
const value = yield* this.evalExpression(stmt.expression, runtime, opts, getSteps, setSteps, runStartMs, userFns, callStack);
|
|
3397
|
-
runtime.setVar(stmt.variable, value);
|
|
3398
|
-
return;
|
|
3399
|
-
}
|
|
3400
|
-
if (stmt instanceof FunctionStatement) {
|
|
3401
|
-
yield* this.evalExpression(
|
|
3402
|
-
stmt.functionalExpression,
|
|
3403
|
-
runtime,
|
|
3404
|
-
opts,
|
|
3405
|
-
getSteps,
|
|
3406
|
-
setSteps,
|
|
3407
|
-
runStartMs,
|
|
3408
|
-
userFns,
|
|
3409
|
-
callStack
|
|
3410
|
-
);
|
|
3411
|
-
return;
|
|
3412
|
-
}
|
|
3413
|
-
if (stmt instanceof IfStatement) {
|
|
3414
|
-
const cond = yield* this.evalExpression(stmt.expression, runtime, opts, getSteps, setSteps, runStartMs, userFns, callStack);
|
|
3415
|
-
if (isTruthy(cond)) {
|
|
3416
|
-
yield* this.execStatement(stmt.ifStatement, runtime, opts, getSteps, setSteps, runStartMs, userFns, callStack);
|
|
3417
|
-
} else if (stmt.elseStatement) {
|
|
3418
|
-
yield* this.execStatement(stmt.elseStatement, runtime, opts, getSteps, setSteps, runStartMs, userFns, callStack);
|
|
3419
|
-
}
|
|
3420
|
-
return;
|
|
3421
|
-
}
|
|
3422
|
-
if (stmt instanceof MetaBlockStatement) {
|
|
3423
|
-
yield* this.execStatement(stmt.body, runtime, opts, getSteps, setSteps, runStartMs, userFns, callStack);
|
|
3424
|
-
return;
|
|
3425
|
-
}
|
|
3426
|
-
if (stmt instanceof ReturnStatement) {
|
|
3427
|
-
const expr = stmt.expression;
|
|
3428
|
-
const value = yield* this.evalExpression(expr, runtime, opts, getSteps, setSteps, runStartMs, userFns, callStack);
|
|
3429
|
-
throw new ReturnSignal(value);
|
|
3430
|
-
}
|
|
3431
|
-
if (stmt instanceof FunctionDefineStatement) {
|
|
3432
|
-
const { line: line2, col: col2 } = locationOf(stmt);
|
|
3433
|
-
throw new UnsupportedAstNodeError("nested FunctionDefineStatement", line2, col2);
|
|
3434
|
-
}
|
|
3435
|
-
if (stmt instanceof VarStatement) {
|
|
3436
|
-
const { line: line2, col: col2 } = locationOf(stmt);
|
|
3437
|
-
throw new UnsupportedAstNodeError("VarStatement (use bare assignment instead)", line2, col2);
|
|
3438
|
-
}
|
|
3439
|
-
if (stmt instanceof ForStatement || stmt instanceof WhileStatement) {
|
|
3440
|
-
const { line: line2, col: col2 } = locationOf(stmt);
|
|
3441
|
-
throw new UnsupportedAstNodeError(
|
|
3442
|
-
stmt instanceof ForStatement ? "ForStatement" : "WhileStatement",
|
|
3443
|
-
line2,
|
|
3444
|
-
col2
|
|
3445
|
-
);
|
|
3446
|
-
}
|
|
3447
|
-
const { line, col } = locationOf(stmt);
|
|
3448
|
-
throw new UnsupportedAstNodeError(stmt.constructor.name, line, col);
|
|
3449
|
-
}
|
|
3450
|
-
async *evalExpression(expr, runtime, opts, getSteps, setSteps, runStartMs, userFns, callStack) {
|
|
3451
|
-
if (expr instanceof ValueExpression) {
|
|
3452
|
-
const v = expr.value;
|
|
3453
|
-
if (v instanceof StringValue) return v.asString();
|
|
3454
|
-
if (v instanceof NumberValue) return v.asNumber();
|
|
3455
|
-
const { line: line2, col: col2 } = locationOf(expr);
|
|
3456
|
-
throw new UnsupportedAstNodeError(
|
|
3457
|
-
`ValueExpression payload ${v.constructor.name}`,
|
|
3458
|
-
line2,
|
|
3459
|
-
col2
|
|
3460
|
-
);
|
|
3461
|
-
}
|
|
3462
|
-
if (expr instanceof VariableExpression) {
|
|
3463
|
-
if (expr.name === "true") return true;
|
|
3464
|
-
if (expr.name === "false") return false;
|
|
3465
|
-
if (expr.name === "null") return null;
|
|
3466
|
-
return runtime.getVar(expr.name);
|
|
3467
|
-
}
|
|
3468
|
-
if (expr instanceof FunctionalExpression) {
|
|
3469
|
-
const args = [];
|
|
3470
|
-
for (const a of expr.arguments) {
|
|
3471
|
-
const v = yield* this.evalExpression(a, runtime, opts, getSteps, setSteps, runStartMs, userFns, callStack);
|
|
3472
|
-
args.push(v);
|
|
3473
|
-
}
|
|
3474
|
-
const userFn = userFns.get(expr.name);
|
|
3475
|
-
if (userFn) {
|
|
3476
|
-
return yield* this.callUserFunction(
|
|
3477
|
-
userFn,
|
|
3478
|
-
args,
|
|
3479
|
-
runtime,
|
|
3480
|
-
opts,
|
|
3481
|
-
getSteps,
|
|
3482
|
-
setSteps,
|
|
3483
|
-
runStartMs,
|
|
3484
|
-
userFns,
|
|
3485
|
-
callStack
|
|
3486
|
-
);
|
|
3487
|
-
}
|
|
3488
|
-
const fn = this.registry.get(expr.name);
|
|
3489
|
-
if (!fn) {
|
|
3490
|
-
const { line: line2, col: col2 } = locationOf(expr);
|
|
3491
|
-
throw new Error(`Unknown DSL function "${expr.name}" at ${line2}:${col2}`);
|
|
3492
|
-
}
|
|
3493
|
-
return await fn.invoke(runtime, ...args);
|
|
3494
|
-
}
|
|
3495
|
-
if (expr instanceof BinaryExpression) {
|
|
3496
|
-
const l = yield* this.evalExpression(expr.expr1, runtime, opts, getSteps, setSteps, runStartMs, userFns, callStack);
|
|
3497
|
-
const r = yield* this.evalExpression(expr.expr2, runtime, opts, getSteps, setSteps, runStartMs, userFns, callStack);
|
|
3498
|
-
return applyBinaryOp(expr.operation, l, r);
|
|
3499
|
-
}
|
|
3500
|
-
if (expr instanceof ArrayExpression) {
|
|
3501
|
-
const out = [];
|
|
3502
|
-
for (const e of expr.elements) {
|
|
3503
|
-
const v = yield* this.evalExpression(e, runtime, opts, getSteps, setSteps, runStartMs, userFns, callStack);
|
|
3504
|
-
out.push(v);
|
|
3505
|
-
}
|
|
3506
|
-
return out;
|
|
3507
|
-
}
|
|
3508
|
-
const { line, col } = locationOf(expr);
|
|
3509
|
-
throw new UnsupportedAstNodeError(`expression ${expr.constructor.name}`, line, col);
|
|
3510
|
-
}
|
|
3511
|
-
/**
|
|
3512
|
-
* Invoke a user-defined function. enterCall (maxCallDepth budget), pushScope,
|
|
3513
|
-
* bind args, runBlock body, catch ReturnSignal → return value, popScope,
|
|
3514
|
-
* exitCall. NOTE: does NOT increment stepsExecuted on the call itself —
|
|
3515
|
-
* `stepsExecuted == count of emitted step-events`, invariant preserved.
|
|
3516
|
-
*/
|
|
3517
|
-
async *callUserFunction(fnDef, args, runtime, opts, getSteps, setSteps, runStartMs, userFns, callStack) {
|
|
3518
|
-
runtime.enterCall(opts.maxCallDepth ?? DEFAULT_MAX_CALL_DEPTH);
|
|
3519
|
-
callStack.push(fnDef.name);
|
|
3520
|
-
try {
|
|
3521
|
-
runtime.pushScope();
|
|
3522
|
-
try {
|
|
3523
|
-
for (let i = 0; i < fnDef.argNames.length; i++) {
|
|
3524
|
-
runtime.setVar(fnDef.argNames[i], args[i] ?? null);
|
|
3525
|
-
}
|
|
3526
|
-
if (!(fnDef.body instanceof BlockStatement)) {
|
|
3527
|
-
throw new Error(`User function "${fnDef.name}" body is not a BlockStatement`);
|
|
3528
|
-
}
|
|
3529
|
-
try {
|
|
3530
|
-
yield* this.runBlock(fnDef.body, runtime, opts, getSteps, setSteps, runStartMs, userFns, callStack);
|
|
3531
|
-
return null;
|
|
3532
|
-
} catch (e) {
|
|
3533
|
-
if (e instanceof ReturnSignal) return e.value ?? null;
|
|
3534
|
-
throw e;
|
|
3535
|
-
}
|
|
3536
|
-
} finally {
|
|
3537
|
-
runtime.popScope();
|
|
3538
|
-
}
|
|
3539
|
-
} finally {
|
|
3540
|
-
callStack.pop();
|
|
3541
|
-
runtime.exitCall();
|
|
3542
|
-
}
|
|
3543
|
-
}
|
|
3544
|
-
checkBudgets(opts, steps, runStartMs) {
|
|
3545
|
-
if (opts.maxSteps !== void 0 && steps >= opts.maxSteps) {
|
|
3546
|
-
throw new Error(`maxSteps budget (${opts.maxSteps}) exhausted`);
|
|
3547
|
-
}
|
|
3548
|
-
if (opts.maxDurationMs !== void 0 && Date.now() - runStartMs >= opts.maxDurationMs) {
|
|
3549
|
-
throw new Error(`maxDurationMs budget (${opts.maxDurationMs}ms) exhausted`);
|
|
3550
|
-
}
|
|
3551
|
-
}
|
|
3552
|
-
};
|
|
3553
|
-
function locationOf(node) {
|
|
3554
|
-
const tok = node.token;
|
|
3555
|
-
if (!tok) return { line: 0, col: 0 };
|
|
3556
|
-
return {
|
|
3557
|
-
line: tok.getLine ? tok.getLine() : 0,
|
|
3558
|
-
col: tok.getColumn ? tok.getColumn() : 0
|
|
3559
|
-
};
|
|
3560
|
-
}
|
|
3561
|
-
__name(locationOf, "locationOf");
|
|
3562
|
-
function isTruthy(v) {
|
|
3563
|
-
if (typeof v === "number") return v !== 0;
|
|
3564
|
-
if (typeof v === "boolean") return v;
|
|
3565
|
-
if (typeof v === "string") return v.length > 0;
|
|
3566
|
-
return v != null;
|
|
3567
|
-
}
|
|
3568
|
-
__name(isTruthy, "isTruthy");
|
|
3569
|
-
function applyBinaryOp(op, l, r) {
|
|
3570
|
-
const ln = typeof l === "number" ? l : Number(l);
|
|
3571
|
-
const rn = typeof r === "number" ? r : Number(r);
|
|
3572
|
-
switch (op) {
|
|
3573
|
-
case "+":
|
|
3574
|
-
if (typeof l === "string" || typeof r === "string") return String(l) + String(r);
|
|
3575
|
-
return ln + rn;
|
|
3576
|
-
case "-":
|
|
3577
|
-
return ln - rn;
|
|
3578
|
-
case "*":
|
|
3579
|
-
return ln * rn;
|
|
3580
|
-
case "/":
|
|
3581
|
-
return ln / rn;
|
|
3582
|
-
case "==":
|
|
3583
|
-
return l === r || Number.isFinite(ln) && Number.isFinite(rn) && ln === rn;
|
|
3584
|
-
case "!=":
|
|
3585
|
-
return !(l === r || Number.isFinite(ln) && Number.isFinite(rn) && ln === rn);
|
|
3586
|
-
case "<":
|
|
3587
|
-
return ln < rn;
|
|
3588
|
-
case "<=":
|
|
3589
|
-
return ln <= rn;
|
|
3590
|
-
case ">":
|
|
3591
|
-
return ln > rn;
|
|
3592
|
-
case ">=":
|
|
3593
|
-
return ln >= rn;
|
|
3594
|
-
case "&&":
|
|
3595
|
-
return Boolean(l) && Boolean(r);
|
|
3596
|
-
case "||":
|
|
3597
|
-
return Boolean(l) || Boolean(r);
|
|
3598
|
-
default:
|
|
3599
|
-
throw new Error(`Unsupported binary operator "${op}"`);
|
|
3600
|
-
}
|
|
3601
|
-
}
|
|
3602
|
-
__name(applyBinaryOp, "applyBinaryOp");
|
|
3603
|
-
function toExecutionError(e) {
|
|
3604
|
-
if (e instanceof Error) {
|
|
3605
|
-
return { message: e.message, stack: e.stack ?? "" };
|
|
3606
|
-
}
|
|
3607
|
-
return { message: String(e) };
|
|
3608
|
-
}
|
|
3609
|
-
__name(toExecutionError, "toExecutionError");
|
|
3610
|
-
|
|
3611
|
-
// src/dsl/linter.ts
|
|
3612
|
-
var ASSERT_PREFIX = "assert";
|
|
3613
|
-
var UI_CALL_NAMES = /* @__PURE__ */ new Set(["tap", "type", "swipe", "pressKey", "waitFor", "appLaunch", "openDeeplink"]);
|
|
3614
|
-
var DslLinter = class {
|
|
3615
|
-
constructor(deps) {
|
|
3616
|
-
this.deps = deps;
|
|
3617
|
-
}
|
|
3618
|
-
deps;
|
|
3619
|
-
static {
|
|
3620
|
-
__name(this, "DslLinter");
|
|
3621
|
-
}
|
|
3622
|
-
/**
|
|
3623
|
-
* Lint a parsed scenario AST.
|
|
3624
|
-
*
|
|
3625
|
-
* @param ast entry-scenario AST
|
|
3626
|
-
* @param source raw source — when present enables E7 header check
|
|
3627
|
-
* @param options cross-cutting context (user-function names, etc.)
|
|
3628
|
-
*/
|
|
3629
|
-
lint(ast, source, options = {}) {
|
|
3630
|
-
const diags = [];
|
|
3631
|
-
const lines = source !== void 0 ? source.split("\n") : null;
|
|
3632
|
-
const userFns = options.userFunctionNames ?? /* @__PURE__ */ new Set();
|
|
3633
|
-
for (const stmt of ast.statements) {
|
|
3634
|
-
if (stmt instanceof FunctionDefineStatement && isBlockMappable2(stmt.name)) {
|
|
3635
|
-
if (lines !== null) this.checkBlocklyHeader(stmt, lines, diags);
|
|
3636
|
-
this.lintFunctionBody(
|
|
3637
|
-
stmt,
|
|
3638
|
-
diags,
|
|
3639
|
-
userFns,
|
|
3640
|
-
/*allowReturn*/
|
|
3641
|
-
false
|
|
3642
|
-
);
|
|
3643
|
-
} else if (stmt instanceof FunctionDefineStatement) {
|
|
3644
|
-
this.lintFunctionBody(
|
|
3645
|
-
stmt,
|
|
3646
|
-
diags,
|
|
3647
|
-
userFns,
|
|
3648
|
-
/*allowReturn*/
|
|
3649
|
-
true
|
|
3650
|
-
);
|
|
3651
|
-
} else if (stmt instanceof MetaBlockStatement) {
|
|
3652
|
-
} else {
|
|
3653
|
-
diags.push(diag("error", "E2", `Only function definitions allowed at top level (got ${stmt.constructor.name})`, stmt));
|
|
3654
|
-
}
|
|
3655
|
-
}
|
|
3656
|
-
return diags;
|
|
3657
|
-
}
|
|
3658
|
-
/**
|
|
3659
|
-
* Cross-file E8 check — duplicate user-function names + shadowing
|
|
3660
|
-
* built-ins. Run ONCE per scenario load (not per file) using the
|
|
3661
|
-
* full origin list from ScenarioLoader.userFunctionOrigins.
|
|
3662
|
-
*
|
|
3663
|
-
* Each diagnostic is anchored to a specific origin (with path/line/col),
|
|
3664
|
-
* so production callers print "_helpers/seed.js:12:1 [E8] ...".
|
|
3665
|
-
*/
|
|
3666
|
-
lintCrossFile(origins) {
|
|
3667
|
-
const diags = [];
|
|
3668
|
-
const byName = /* @__PURE__ */ new Map();
|
|
3669
|
-
for (const o of origins) {
|
|
3670
|
-
const list = byName.get(o.name) ?? [];
|
|
3671
|
-
list.push(o);
|
|
3672
|
-
byName.set(o.name, list);
|
|
3673
|
-
}
|
|
3674
|
-
for (const [name, list] of byName) {
|
|
3675
|
-
if (this.deps.registry.has(name)) {
|
|
3676
|
-
for (const o of list) {
|
|
3677
|
-
diags.push({
|
|
3678
|
-
severity: "error",
|
|
3679
|
-
code: "E8",
|
|
3680
|
-
message: `User function "${name}" shadows a built-in DSL function. Rename it.`,
|
|
3681
|
-
line: o.line,
|
|
3682
|
-
col: o.col,
|
|
3683
|
-
source: o.source
|
|
3684
|
-
});
|
|
3685
|
-
}
|
|
3686
|
-
continue;
|
|
3687
|
-
}
|
|
3688
|
-
if (list.length > 1) {
|
|
3689
|
-
const where = list.map((o) => `${o.source}:${o.line}`).join(", ");
|
|
3690
|
-
for (const o of list) {
|
|
3691
|
-
diags.push({
|
|
3692
|
-
severity: "error",
|
|
3693
|
-
code: "E8",
|
|
3694
|
-
message: `Duplicate user function "${name}". Defined at: ${where}`,
|
|
3695
|
-
line: o.line,
|
|
3696
|
-
col: o.col,
|
|
3697
|
-
source: o.source
|
|
3698
|
-
});
|
|
3699
|
-
}
|
|
3700
|
-
}
|
|
3701
|
-
}
|
|
3702
|
-
return diags;
|
|
3703
|
-
}
|
|
3704
|
-
/**
|
|
3705
|
-
* Lint a helper file. Same rules as scenario, but:
|
|
3706
|
-
* - No E7 header check (helpers aren't block-mappable)
|
|
3707
|
-
* - `return` allowed in any function body
|
|
3708
|
-
* - All diagnostics tagged with helper.path
|
|
3709
|
-
*/
|
|
3710
|
-
lintHelper(helper, options = {}) {
|
|
3711
|
-
const diags = [];
|
|
3712
|
-
const userFns = options.userFunctionNames ?? /* @__PURE__ */ new Set();
|
|
3713
|
-
for (const stmt of helper.ast.statements) {
|
|
3714
|
-
if (stmt instanceof FunctionDefineStatement) {
|
|
3715
|
-
if (isBlockMappable2(stmt.name)) {
|
|
3716
|
-
this.lintFunctionBody(
|
|
3717
|
-
stmt,
|
|
3718
|
-
diags,
|
|
3719
|
-
userFns,
|
|
3720
|
-
/*allowReturn*/
|
|
3721
|
-
false
|
|
3722
|
-
);
|
|
3723
|
-
} else {
|
|
3724
|
-
this.lintFunctionBody(
|
|
3725
|
-
stmt,
|
|
3726
|
-
diags,
|
|
3727
|
-
userFns,
|
|
3728
|
-
/*allowReturn*/
|
|
3729
|
-
true
|
|
3730
|
-
);
|
|
3731
|
-
}
|
|
3732
|
-
} else if (stmt instanceof MetaBlockStatement) {
|
|
3733
|
-
} else {
|
|
3734
|
-
diags.push(diag("error", "E2", `Only function definitions allowed at top level (got ${stmt.constructor.name})`, stmt));
|
|
3735
|
-
}
|
|
3736
|
-
}
|
|
3737
|
-
return diags.map((d) => ({ ...d, source: helper.path }));
|
|
3738
|
-
}
|
|
3739
|
-
// ---- internals --------------------------------------------------------
|
|
3740
|
-
checkBlocklyHeader(fn, lines, diags) {
|
|
3741
|
-
const fnLine = fn.token?.getLine?.() ?? 0;
|
|
3742
|
-
if (fnLine <= 3) {
|
|
3743
|
-
diags.push(diag("error", "E7", `${fn.name}: missing 3-comment Blockly header (// id-<slug>, // <name>, // #<color>) immediately above the function definition`, fn));
|
|
3744
|
-
return;
|
|
3745
|
-
}
|
|
3746
|
-
const idLine = lines[fnLine - 4];
|
|
3747
|
-
const nameLine = lines[fnLine - 3];
|
|
3748
|
-
const colorLine = lines[fnLine - 2];
|
|
3749
|
-
const allComments = [idLine, nameLine, colorLine].every(
|
|
3750
|
-
(l) => typeof l === "string" && l.trimStart().startsWith("//")
|
|
3751
|
-
);
|
|
3752
|
-
if (!allComments) {
|
|
3753
|
-
diags.push(diag("error", "E7", `${fn.name}: 3 comment lines must immediately precede the function (// id-<slug>, // <name>, // #<color>) \u2014 no blank line in between`, fn));
|
|
3754
|
-
}
|
|
3755
|
-
}
|
|
3756
|
-
lintFunctionBody(fn, diags, userFns, allowReturn) {
|
|
3757
|
-
if (!(fn.body instanceof BlockStatement)) {
|
|
3758
|
-
diags.push(diag("error", "E2", `Function "${fn.name}" body must be a block`, fn));
|
|
3759
|
-
return;
|
|
3760
|
-
}
|
|
3761
|
-
let firstUiSeen = false;
|
|
3762
|
-
let firstAssertSeen = false;
|
|
3763
|
-
const recordCall = /* @__PURE__ */ __name((name) => {
|
|
3764
|
-
if (UI_CALL_NAMES.has(name)) firstUiSeen = true;
|
|
3765
|
-
if (name.startsWith(ASSERT_PREFIX)) firstAssertSeen = true;
|
|
3766
|
-
}, "recordCall");
|
|
3767
|
-
for (const stmt of fn.body.statements) {
|
|
3768
|
-
this.checkStatement(stmt, diags, recordCall, () => firstUiSeen, userFns, allowReturn);
|
|
3769
|
-
}
|
|
3770
|
-
void firstAssertSeen;
|
|
3771
|
-
}
|
|
3772
|
-
checkStatement(stmt, diags, recordCall, isFirstUiSeen, userFns, allowReturn) {
|
|
3773
|
-
if (stmt instanceof ReturnStatement) {
|
|
3774
|
-
if (!allowReturn) {
|
|
3775
|
-
diags.push(diag("error", "E2", `'return' is not allowed inside test_*/flow_* function bodies (only in helpers)`, stmt));
|
|
3776
|
-
return;
|
|
3777
|
-
}
|
|
3778
|
-
const expr = stmt.expression;
|
|
3779
|
-
if (expr) this.checkExpression(expr, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
|
|
3780
|
-
return;
|
|
3781
|
-
}
|
|
3782
|
-
if (this.isForbiddenStatement(stmt)) {
|
|
3783
|
-
diags.push(diag("error", "E2", `Forbidden statement type "${stmt.constructor.name}" (D-4 subset)`, stmt));
|
|
3784
|
-
return;
|
|
3785
|
-
}
|
|
3786
|
-
if (stmt instanceof AssignmentStatement) {
|
|
3787
|
-
this.checkExpression(stmt.expression, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
|
|
3788
|
-
return;
|
|
3789
|
-
}
|
|
3790
|
-
if (stmt instanceof FunctionStatement) {
|
|
3791
|
-
this.checkCall(stmt.functionalExpression, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
|
|
3792
|
-
return;
|
|
3793
|
-
}
|
|
3794
|
-
if (stmt instanceof IfStatement) {
|
|
3795
|
-
this.checkExpression(stmt.expression, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
|
|
3796
|
-
this.checkStatement(stmt.ifStatement, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
|
|
3797
|
-
if (stmt.elseStatement) this.checkStatement(stmt.elseStatement, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
|
|
3798
|
-
return;
|
|
3799
|
-
}
|
|
3800
|
-
if (stmt instanceof BlockStatement) {
|
|
3801
|
-
for (const s of stmt.statements) this.checkStatement(s, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
|
|
3802
|
-
return;
|
|
3803
|
-
}
|
|
3804
|
-
if (stmt instanceof FunctionDefineStatement) {
|
|
3805
|
-
diags.push(diag("error", "E2", "Nested function definitions are not allowed", stmt));
|
|
3806
|
-
return;
|
|
3807
|
-
}
|
|
3808
|
-
if (stmt instanceof MetaBlockStatement) return;
|
|
3809
|
-
diags.push(diag("error", "E2", `Unsupported statement "${stmt.constructor.name}"`, stmt));
|
|
3810
|
-
}
|
|
3811
|
-
checkExpression(expr, diags, recordCall, isFirstUiSeen, userFns, allowReturn) {
|
|
3812
|
-
if (expr instanceof FunctionalExpression) {
|
|
3813
|
-
this.checkCall(expr, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
|
|
3814
|
-
return;
|
|
3815
|
-
}
|
|
3816
|
-
if (expr instanceof BinaryExpression) {
|
|
3817
|
-
this.checkExpression(expr.expr1, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
|
|
3818
|
-
this.checkExpression(expr.expr2, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
|
|
3819
|
-
return;
|
|
3820
|
-
}
|
|
3821
|
-
if (expr instanceof ArrayExpression) {
|
|
3822
|
-
for (const e of expr.elements) this.checkExpression(e, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
|
|
3823
|
-
return;
|
|
3824
|
-
}
|
|
3825
|
-
if (expr instanceof ValueExpression || expr instanceof VariableExpression) return;
|
|
3826
|
-
if (this.isForbiddenExpression(expr)) {
|
|
3827
|
-
diags.push(diag("error", "E2", `Forbidden expression "${expr.constructor.name}" (D-4 subset)`, expr));
|
|
3828
|
-
return;
|
|
3829
|
-
}
|
|
3830
|
-
diags.push(diag("error", "E2", `Unsupported expression "${expr.constructor.name}"`, expr));
|
|
3831
|
-
}
|
|
3832
|
-
checkCall(call, diags, recordCall, isFirstUiSeen, userFns, allowReturn) {
|
|
3833
|
-
const name = call.name;
|
|
3834
|
-
if (!this.deps.registry.has(name) && !userFns.has(name)) {
|
|
3835
|
-
diags.push(diag("error", "E1", `Unknown DSL function "${name}"`, call));
|
|
3836
|
-
}
|
|
3837
|
-
const fn = this.deps.registry.get(name);
|
|
3838
|
-
if (fn) {
|
|
3839
|
-
this.checkArity(call, fn, diags);
|
|
3840
|
-
this.checkArgTypes(call, fn, diags);
|
|
3841
|
-
}
|
|
3842
|
-
if (name === "setDevice" && call.arguments.length > 0) {
|
|
3843
|
-
const arg0 = call.arguments[0];
|
|
3844
|
-
if (arg0 instanceof ValueExpression) {
|
|
3845
|
-
const val = arg0.value.asString?.call(arg0.value);
|
|
3846
|
-
if (typeof val === "string" && !this.deps.knownSlots.includes(val)) {
|
|
3847
|
-
diags.push(
|
|
3848
|
-
diag("error", "E3", `setDevice("${val}"): unknown slot. Known: ${this.deps.knownSlots.join(", ") || "(none)"}`, call)
|
|
3849
|
-
);
|
|
3850
|
-
}
|
|
3851
|
-
}
|
|
3852
|
-
}
|
|
3853
|
-
if (name === "apiCall" && call.arguments.length > 0) {
|
|
3854
|
-
const jsonArgIdx = 2;
|
|
3855
|
-
const jsonArg = call.arguments[jsonArgIdx];
|
|
3856
|
-
if (jsonArg instanceof ValueExpression) {
|
|
3857
|
-
const val = jsonArg.value.asString?.call(jsonArg.value);
|
|
3858
|
-
if (typeof val === "string") {
|
|
3859
|
-
try {
|
|
3860
|
-
JSON.parse(val);
|
|
3861
|
-
} catch (e) {
|
|
3862
|
-
diags.push(
|
|
3863
|
-
diag("error", "E4", `${name}: arg ${jsonArgIdx} is not valid JSON \u2014 ${e.message}`, call)
|
|
3864
|
-
);
|
|
3865
|
-
}
|
|
3866
|
-
}
|
|
3867
|
-
}
|
|
3868
|
-
}
|
|
3869
|
-
if (name.startsWith(ASSERT_PREFIX) && !isFirstUiSeen()) {
|
|
3870
|
-
diags.push(diag("warning", "W5", `${name}() called before any UI action \u2014 typo or wrong order?`, call));
|
|
3871
|
-
}
|
|
3872
|
-
recordCall(name);
|
|
3873
|
-
for (const arg of call.arguments) {
|
|
3874
|
-
this.checkExpression(arg, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
|
|
3875
|
-
}
|
|
3876
|
-
}
|
|
3877
|
-
checkArity(call, fn, diags) {
|
|
3878
|
-
const max = fn.argTypes.length;
|
|
3879
|
-
const min = fn.minArgs ?? max;
|
|
3880
|
-
const got = call.arguments.length;
|
|
3881
|
-
if (got < min) {
|
|
3882
|
-
diags.push(
|
|
3883
|
-
diag(
|
|
3884
|
-
"error",
|
|
3885
|
-
"E5",
|
|
3886
|
-
`${fn.name}(): expected ${min === max ? `${min}` : `at least ${min}`} arg${min === 1 ? "" : "s"}, got ${got}`,
|
|
3887
|
-
call
|
|
3888
|
-
)
|
|
3889
|
-
);
|
|
3890
|
-
return;
|
|
3891
|
-
}
|
|
3892
|
-
if (!fn.variadic && got > max) {
|
|
3893
|
-
diags.push(
|
|
3894
|
-
diag(
|
|
3895
|
-
"error",
|
|
3896
|
-
"E5",
|
|
3897
|
-
`${fn.name}(): expected at most ${max} arg${max === 1 ? "" : "s"}, got ${got}`,
|
|
3898
|
-
call
|
|
3899
|
-
)
|
|
3900
|
-
);
|
|
3901
|
-
}
|
|
3902
|
-
}
|
|
3903
|
-
/**
|
|
3904
|
-
* For each provided arg up to argTypes.length, infer its static type and
|
|
3905
|
-
* compare to the declared signature. Only emits E6 when the inferred type
|
|
3906
|
-
* is UNAMBIGUOUS — we never flag "any" sources (variables, helper-call
|
|
3907
|
-
* results, BinaryExpression, ArrayExpression).
|
|
3908
|
-
*/
|
|
3909
|
-
checkArgTypes(call, fn, diags) {
|
|
3910
|
-
const limit = Math.min(call.arguments.length, fn.argTypes.length);
|
|
3911
|
-
for (let i = 0; i < limit; i++) {
|
|
3912
|
-
const expected = fn.argTypes[i];
|
|
3913
|
-
if (expected === "any") continue;
|
|
3914
|
-
const inferred = this.inferExprType(call.arguments[i]);
|
|
3915
|
-
if (inferred === "any") continue;
|
|
3916
|
-
if (inferred === expected) continue;
|
|
3917
|
-
if (expected === "boolean" && inferred === "number" || expected === "number" && inferred === "boolean") {
|
|
3918
|
-
continue;
|
|
3919
|
-
}
|
|
3920
|
-
diags.push(
|
|
3921
|
-
diag(
|
|
3922
|
-
"error",
|
|
3923
|
-
"E6",
|
|
3924
|
-
`${fn.name}(): arg ${i} expected ${expected}, got ${describeInferred(call.arguments[i], inferred)}`,
|
|
3925
|
-
call
|
|
3926
|
-
)
|
|
3927
|
-
);
|
|
3928
|
-
}
|
|
3929
|
-
}
|
|
3930
|
-
/** Best-effort static type for an expression node. "any" = unknown. */
|
|
3931
|
-
inferExprType(expr) {
|
|
3932
|
-
if (expr instanceof ValueExpression) {
|
|
3933
|
-
const v = expr.value;
|
|
3934
|
-
if (v instanceof StringValue) return "string";
|
|
3935
|
-
if (v instanceof NumberValue) return "number";
|
|
3936
|
-
return "any";
|
|
3937
|
-
}
|
|
3938
|
-
if (expr instanceof VariableExpression) {
|
|
3939
|
-
if (expr.name === "true" || expr.name === "false") return "boolean";
|
|
3940
|
-
return "any";
|
|
3941
|
-
}
|
|
3942
|
-
if (expr instanceof FunctionalExpression) {
|
|
3943
|
-
const callee = this.deps.registry.get(expr.name);
|
|
3944
|
-
return callee ? callee.returnType : "any";
|
|
3945
|
-
}
|
|
3946
|
-
return "any";
|
|
3947
|
-
}
|
|
3948
|
-
isForbiddenStatement(stmt) {
|
|
3949
|
-
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;
|
|
3950
|
-
}
|
|
3951
|
-
isForbiddenExpression(expr) {
|
|
3952
|
-
return expr instanceof IncrementExpression || expr instanceof UnaryExpression || expr instanceof ConditionalExpression || expr instanceof ObjectExpression || expr instanceof ArrayAccessExpression;
|
|
3953
|
-
}
|
|
3954
|
-
};
|
|
3955
|
-
function isBlockMappable2(name) {
|
|
3956
|
-
return name.startsWith("test_") || name.startsWith("flow_");
|
|
3957
|
-
}
|
|
3958
|
-
__name(isBlockMappable2, "isBlockMappable");
|
|
3959
|
-
function describeInferred(expr, inferred) {
|
|
3960
|
-
if (expr instanceof FunctionalExpression) {
|
|
3961
|
-
return `${inferred} (${expr.name}() returns ${inferred})`;
|
|
3962
|
-
}
|
|
3963
|
-
if (expr instanceof ValueExpression) {
|
|
3964
|
-
return `${inferred} literal`;
|
|
3965
|
-
}
|
|
3966
|
-
if (expr instanceof VariableExpression && (expr.name === "true" || expr.name === "false")) {
|
|
3967
|
-
return `boolean literal \`${expr.name}\``;
|
|
3968
|
-
}
|
|
3969
|
-
return inferred;
|
|
3970
|
-
}
|
|
3971
|
-
__name(describeInferred, "describeInferred");
|
|
3972
|
-
function diag(severity, code, message, node) {
|
|
3973
|
-
const tok = node.token;
|
|
3974
|
-
return {
|
|
3975
|
-
severity,
|
|
3976
|
-
code,
|
|
3977
|
-
message,
|
|
3978
|
-
line: tok?.getLine ? tok.getLine() : 0,
|
|
3979
|
-
col: tok?.getColumn ? tok.getColumn() : 0
|
|
3980
|
-
};
|
|
3981
|
-
}
|
|
3982
|
-
__name(diag, "diag");
|
|
3983
|
-
|
|
3984
|
-
// src/dsl/function-registry.ts
|
|
3985
|
-
var FunctionRegistry = class {
|
|
3986
|
-
static {
|
|
3987
|
-
__name(this, "FunctionRegistry");
|
|
3988
|
-
}
|
|
3989
|
-
map = /* @__PURE__ */ new Map();
|
|
3990
|
-
register(fn) {
|
|
3991
|
-
if (this.map.has(fn.name)) {
|
|
3992
|
-
throw new Error(`Duplicate DSL function: ${fn.name}`);
|
|
3993
|
-
}
|
|
3994
|
-
this.map.set(fn.name, fn);
|
|
3995
|
-
}
|
|
3996
|
-
has(name) {
|
|
3997
|
-
return this.map.has(name);
|
|
3998
|
-
}
|
|
3999
|
-
get(name) {
|
|
4000
|
-
return this.map.get(name);
|
|
4001
|
-
}
|
|
4002
|
-
names() {
|
|
4003
|
-
return [...this.map.keys()].sort();
|
|
4004
|
-
}
|
|
4005
|
-
};
|
|
4006
|
-
|
|
4007
|
-
// src/dsl/functions/alerts.ts
|
|
4008
|
-
function asString(x, fn, idx) {
|
|
4009
|
-
if (typeof x !== "string") throw new Error(`${fn}(): arg ${idx} must be a string. Got ${typeof x}.`);
|
|
4010
|
-
return x;
|
|
4011
|
-
}
|
|
4012
|
-
__name(asString, "asString");
|
|
4013
|
-
var acceptAlert = {
|
|
4014
|
-
name: "acceptAlert",
|
|
4015
|
-
argTypes: ["string"],
|
|
4016
|
-
returnType: "void",
|
|
4017
|
-
minArgs: 0,
|
|
4018
|
-
invoke: /* @__PURE__ */ __name(async (runtime, buttonArg) => {
|
|
4019
|
-
const button = buttonArg !== void 0 ? asString(buttonArg, "acceptAlert", 0) : void 0;
|
|
4020
|
-
await runtime.driver.acceptAlert(runtime.currentDeviceSlot, button);
|
|
4021
|
-
}, "invoke")
|
|
4022
|
-
};
|
|
4023
|
-
var dismissAlert = {
|
|
4024
|
-
name: "dismissAlert",
|
|
4025
|
-
argTypes: [],
|
|
4026
|
-
returnType: "void",
|
|
4027
|
-
invoke: /* @__PURE__ */ __name(async (runtime) => {
|
|
4028
|
-
await runtime.driver.dismissAlert(runtime.currentDeviceSlot);
|
|
4029
|
-
}, "invoke")
|
|
4030
|
-
};
|
|
4031
|
-
var readAlert = {
|
|
4032
|
-
name: "readAlert",
|
|
4033
|
-
argTypes: [],
|
|
4034
|
-
returnType: "string",
|
|
4035
|
-
invoke: /* @__PURE__ */ __name(async (runtime) => {
|
|
4036
|
-
const { text } = await runtime.driver.readAlert(runtime.currentDeviceSlot);
|
|
4037
|
-
return text;
|
|
4038
|
-
}, "invoke")
|
|
4039
|
-
};
|
|
4040
|
-
var ALERT_FUNCTIONS = [acceptAlert, dismissAlert, readAlert];
|
|
4041
|
-
|
|
4042
|
-
// src/dsl/functions/asserts.ts
|
|
4043
|
-
function asSelector(x, fn, idx) {
|
|
4044
|
-
if (typeof x !== "object" || x === null) {
|
|
4045
|
-
throw new Error(`${fn}(): arg ${idx} must be a Selector. Got ${typeof x}.`);
|
|
4046
|
-
}
|
|
4047
|
-
return x;
|
|
4048
|
-
}
|
|
4049
|
-
__name(asSelector, "asSelector");
|
|
4050
|
-
function asNumber(x, fn, idx) {
|
|
4051
|
-
if (typeof x !== "number") throw new Error(`${fn}(): arg ${idx} must be a number. Got ${typeof x}.`);
|
|
4052
|
-
return x;
|
|
4053
|
-
}
|
|
4054
|
-
__name(asNumber, "asNumber");
|
|
4055
|
-
var assertEqual = {
|
|
4056
|
-
name: "assertEqual",
|
|
4057
|
-
argTypes: ["any", "any"],
|
|
4058
|
-
returnType: "void",
|
|
4059
|
-
invoke: /* @__PURE__ */ __name((_runtime, actual, expected) => {
|
|
4060
|
-
if (actual !== expected) {
|
|
4061
|
-
throw new Error(
|
|
4062
|
-
`assertEqual failed: actual ${JSON.stringify(actual)} !== expected ${JSON.stringify(expected)}`
|
|
4063
|
-
);
|
|
4064
|
-
}
|
|
4065
|
-
}, "invoke")
|
|
4066
|
-
};
|
|
4067
|
-
var assertVisible = {
|
|
4068
|
-
name: "assertVisible",
|
|
4069
|
-
argTypes: ["selector"],
|
|
4070
|
-
returnType: "void",
|
|
4071
|
-
invoke: /* @__PURE__ */ __name(async (runtime, selectorArg) => {
|
|
4072
|
-
const selector = asSelector(selectorArg, "assertVisible", 0);
|
|
4073
|
-
const tree = await runtime.driver.a11yTree(runtime.currentDeviceSlot);
|
|
4074
|
-
const r = runtime.selectorResolver.resolve(tree, selector);
|
|
4075
|
-
if (!r.ok) {
|
|
4076
|
-
throw new Error(
|
|
4077
|
-
`assertVisible failed: ${r.reason}
|
|
4078
|
-
selector: ${JSON.stringify(selector)}
|
|
4079
|
-
candidates: ${JSON.stringify(r.candidates).slice(0, 600)}`
|
|
4080
|
-
);
|
|
4081
|
-
}
|
|
4082
|
-
}, "invoke")
|
|
4083
|
-
};
|
|
4084
|
-
var assertCount = {
|
|
4085
|
-
name: "assertCount",
|
|
4086
|
-
argTypes: ["selector", "number"],
|
|
4087
|
-
returnType: "void",
|
|
4088
|
-
invoke: /* @__PURE__ */ __name(async (runtime, selectorArg, expectedArg) => {
|
|
4089
|
-
const selector = asSelector(selectorArg, "assertCount", 0);
|
|
4090
|
-
const expected = asNumber(expectedArg, "assertCount", 1);
|
|
4091
|
-
const tree = await runtime.driver.a11yTree(runtime.currentDeviceSlot);
|
|
4092
|
-
let count = 0;
|
|
4093
|
-
while (true) {
|
|
4094
|
-
const probe = { ...selector, ordinal: count };
|
|
4095
|
-
const r = runtime.selectorResolver.resolve(tree, probe);
|
|
4096
|
-
if (!r.ok) break;
|
|
4097
|
-
count++;
|
|
4098
|
-
if (count > 1e3) {
|
|
4099
|
-
throw new Error(`assertCount: aborting after 1000 \u2014 selector too broad`);
|
|
4100
|
-
}
|
|
4101
|
-
}
|
|
4102
|
-
if (count !== expected) {
|
|
4103
|
-
throw new Error(
|
|
4104
|
-
`assertCount failed: ${JSON.stringify(selector)} matched ${count} time(s), expected ${expected}`
|
|
4105
|
-
);
|
|
4106
|
-
}
|
|
4107
|
-
}, "invoke")
|
|
4108
|
-
};
|
|
4109
|
-
async function assertEnabledState(runtime, selectorArg, expected, fnName) {
|
|
4110
|
-
const selector = asSelector(selectorArg, fnName, 0);
|
|
4111
|
-
const tree = await runtime.driver.a11yTree(runtime.currentDeviceSlot);
|
|
4112
|
-
const r = runtime.selectorResolver.resolve(tree, selector);
|
|
4113
|
-
if (!r.ok) {
|
|
4114
|
-
throw new Error(
|
|
4115
|
-
`${fnName} failed: selector did not match. ${r.reason}
|
|
4116
|
-
selector: ${JSON.stringify(selector)}`
|
|
4117
|
-
);
|
|
4118
|
-
}
|
|
4119
|
-
const node = r.node;
|
|
4120
|
-
if (node.enabled === void 0) {
|
|
4121
|
-
throw new Error(
|
|
4122
|
-
`${fnName} failed: matched node does not report 'enabled' (likely a non-interactive widget or RN-side accessibility gap). Selector: ${JSON.stringify(selector)}`
|
|
4123
|
-
);
|
|
2468
|
+
const node = r.node;
|
|
2469
|
+
if (node.enabled === void 0) {
|
|
2470
|
+
throw new Error(
|
|
2471
|
+
`${fnName} failed: matched node does not report 'enabled' (likely a non-interactive widget or RN-side accessibility gap). Selector: ${JSON.stringify(selector)}`
|
|
2472
|
+
);
|
|
4124
2473
|
}
|
|
4125
2474
|
if (node.enabled !== expected) {
|
|
4126
2475
|
throw new Error(
|
|
@@ -4494,47 +2843,319 @@ var pressKey = {
|
|
|
4494
2843
|
if (key !== "back" && key !== "home" && key !== "enter" && key !== "escape") {
|
|
4495
2844
|
throw new Error(`pressKey(): key must be 'back'|'home'|'enter'|'escape'. Got "${key}".`);
|
|
4496
2845
|
}
|
|
4497
|
-
await runtime.driver.pressKey(runtime.currentDeviceSlot, key);
|
|
4498
|
-
}, "invoke")
|
|
4499
|
-
};
|
|
4500
|
-
var waitFor = {
|
|
4501
|
-
name: "waitFor",
|
|
4502
|
-
argTypes: ["selector", "number"],
|
|
4503
|
-
returnType: "void",
|
|
4504
|
-
minArgs: 1,
|
|
4505
|
-
invoke: /* @__PURE__ */ __name(async (runtime, selectorArg, timeoutMsArg) => {
|
|
4506
|
-
const selector = asSelector2(selectorArg, "waitFor", 0);
|
|
4507
|
-
const timeoutMs = timeoutMsArg === void 0 ? runtime.envConfig.defaultWaitForTimeoutMs : asNumber2(timeoutMsArg, "waitFor", 1);
|
|
4508
|
-
await pollUntilFound(runtime, selector, timeoutMs);
|
|
4509
|
-
}, "invoke")
|
|
4510
|
-
};
|
|
4511
|
-
var pause = {
|
|
4512
|
-
name: "pause",
|
|
4513
|
-
argTypes: ["number"],
|
|
4514
|
-
returnType: "void",
|
|
4515
|
-
invoke: /* @__PURE__ */ __name(async (_runtime, msArg) => {
|
|
4516
|
-
const ms = asNumber2(msArg, "pause", 0);
|
|
4517
|
-
await wait(ms);
|
|
4518
|
-
}, "invoke")
|
|
2846
|
+
await runtime.driver.pressKey(runtime.currentDeviceSlot, key);
|
|
2847
|
+
}, "invoke")
|
|
2848
|
+
};
|
|
2849
|
+
var waitFor = {
|
|
2850
|
+
name: "waitFor",
|
|
2851
|
+
argTypes: ["selector", "number"],
|
|
2852
|
+
returnType: "void",
|
|
2853
|
+
minArgs: 1,
|
|
2854
|
+
invoke: /* @__PURE__ */ __name(async (runtime, selectorArg, timeoutMsArg) => {
|
|
2855
|
+
const selector = asSelector2(selectorArg, "waitFor", 0);
|
|
2856
|
+
const timeoutMs = timeoutMsArg === void 0 ? runtime.envConfig.defaultWaitForTimeoutMs : asNumber2(timeoutMsArg, "waitFor", 1);
|
|
2857
|
+
await pollUntilFound(runtime, selector, timeoutMs);
|
|
2858
|
+
}, "invoke")
|
|
2859
|
+
};
|
|
2860
|
+
var pause = {
|
|
2861
|
+
name: "pause",
|
|
2862
|
+
argTypes: ["number"],
|
|
2863
|
+
returnType: "void",
|
|
2864
|
+
invoke: /* @__PURE__ */ __name(async (_runtime, msArg) => {
|
|
2865
|
+
const ms = asNumber2(msArg, "pause", 0);
|
|
2866
|
+
await wait(ms);
|
|
2867
|
+
}, "invoke")
|
|
2868
|
+
};
|
|
2869
|
+
var UI_FUNCTIONS = [tap, type_, swipe, pressKey, waitFor, pause];
|
|
2870
|
+
|
|
2871
|
+
// src/dsl/functions/index.ts
|
|
2872
|
+
var ALL_DSL_FUNCTIONS = [
|
|
2873
|
+
...DEVICE_FUNCTIONS,
|
|
2874
|
+
...UI_FUNCTIONS,
|
|
2875
|
+
...SELECTOR_FUNCTIONS,
|
|
2876
|
+
...ALERT_FUNCTIONS,
|
|
2877
|
+
...DATA_FUNCTIONS,
|
|
2878
|
+
...ASSERT_FUNCTIONS,
|
|
2879
|
+
...TIME_FUNCTIONS
|
|
2880
|
+
];
|
|
2881
|
+
function buildDefaultRegistry() {
|
|
2882
|
+
const reg = new FunctionRegistry();
|
|
2883
|
+
for (const fn of ALL_DSL_FUNCTIONS) reg.register(fn);
|
|
2884
|
+
return reg;
|
|
2885
|
+
}
|
|
2886
|
+
__name(buildDefaultRegistry, "buildDefaultRegistry");
|
|
2887
|
+
|
|
2888
|
+
// src/dsl/linter.ts
|
|
2889
|
+
var ASSERT_PREFIX = "assert";
|
|
2890
|
+
var UI_CALL_NAMES = /* @__PURE__ */ new Set(["tap", "type", "swipe", "pressKey", "waitFor", "appLaunch", "openDeeplink"]);
|
|
2891
|
+
var DslLinter = class {
|
|
2892
|
+
constructor(deps) {
|
|
2893
|
+
this.deps = deps;
|
|
2894
|
+
}
|
|
2895
|
+
deps;
|
|
2896
|
+
static {
|
|
2897
|
+
__name(this, "DslLinter");
|
|
2898
|
+
}
|
|
2899
|
+
/**
|
|
2900
|
+
* Lint a parsed scenario AST: mobile-specific pass + the shared
|
|
2901
|
+
* validator engine over the same tree.
|
|
2902
|
+
*
|
|
2903
|
+
* @param ast entry-scenario AST
|
|
2904
|
+
* @param source raw source — the engine's validation session reads it
|
|
2905
|
+
* @param options cross-cutting context (user-function names, etc.)
|
|
2906
|
+
*/
|
|
2907
|
+
lint(ast, source, options = {}) {
|
|
2908
|
+
const diags = [];
|
|
2909
|
+
const userFns = options.userFunctionNames ?? /* @__PURE__ */ new Set();
|
|
2910
|
+
for (const stmt of ast.statements) {
|
|
2911
|
+
if (stmt instanceof FunctionDefineStatement && isBlockMappable(stmt.name)) {
|
|
2912
|
+
this.lintFunctionBody(
|
|
2913
|
+
stmt,
|
|
2914
|
+
diags,
|
|
2915
|
+
userFns,
|
|
2916
|
+
/*allowReturn*/
|
|
2917
|
+
false
|
|
2918
|
+
);
|
|
2919
|
+
} else if (stmt instanceof FunctionDefineStatement) {
|
|
2920
|
+
this.lintFunctionBody(
|
|
2921
|
+
stmt,
|
|
2922
|
+
diags,
|
|
2923
|
+
userFns,
|
|
2924
|
+
/*allowReturn*/
|
|
2925
|
+
true
|
|
2926
|
+
);
|
|
2927
|
+
} else if (stmt instanceof MetaBlockStatement) {
|
|
2928
|
+
} else {
|
|
2929
|
+
diags.push(diag("error", "E2", `Only function definitions allowed at top level (got ${stmt.constructor.name})`, stmt));
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
diags.push(...this.runEngine(ast, source ?? "", userFns));
|
|
2933
|
+
return diags;
|
|
2934
|
+
}
|
|
2935
|
+
/**
|
|
2936
|
+
* Cross-file E8 check — duplicate user-function names + shadowing
|
|
2937
|
+
* built-ins. Run ONCE per scenario load (not per file) using the
|
|
2938
|
+
* full origin list from ScenarioLoader.userFunctionOrigins.
|
|
2939
|
+
*
|
|
2940
|
+
* Each diagnostic is anchored to a specific origin (with path/line/col),
|
|
2941
|
+
* so production callers print "_helpers/seed.js:12:1 [E8] ...".
|
|
2942
|
+
*/
|
|
2943
|
+
lintCrossFile(origins) {
|
|
2944
|
+
const diags = [];
|
|
2945
|
+
const byName = /* @__PURE__ */ new Map();
|
|
2946
|
+
for (const o of origins) {
|
|
2947
|
+
const list = byName.get(o.name) ?? [];
|
|
2948
|
+
list.push(o);
|
|
2949
|
+
byName.set(o.name, list);
|
|
2950
|
+
}
|
|
2951
|
+
for (const [name, list] of byName) {
|
|
2952
|
+
if (this.deps.registry.has(name)) {
|
|
2953
|
+
for (const o of list) {
|
|
2954
|
+
diags.push({
|
|
2955
|
+
severity: "error",
|
|
2956
|
+
code: "E8",
|
|
2957
|
+
message: `User function "${name}" shadows a built-in DSL function. Rename it.`,
|
|
2958
|
+
line: o.line,
|
|
2959
|
+
col: o.col,
|
|
2960
|
+
source: o.source
|
|
2961
|
+
});
|
|
2962
|
+
}
|
|
2963
|
+
continue;
|
|
2964
|
+
}
|
|
2965
|
+
if (list.length > 1) {
|
|
2966
|
+
const where = list.map((o) => `${o.source}:${o.line}`).join(", ");
|
|
2967
|
+
for (const o of list) {
|
|
2968
|
+
diags.push({
|
|
2969
|
+
severity: "error",
|
|
2970
|
+
code: "E8",
|
|
2971
|
+
message: `Duplicate user function "${name}". Defined at: ${where}`,
|
|
2972
|
+
line: o.line,
|
|
2973
|
+
col: o.col,
|
|
2974
|
+
source: o.source
|
|
2975
|
+
});
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
return diags;
|
|
2980
|
+
}
|
|
2981
|
+
/**
|
|
2982
|
+
* Lint a helper file. Same two layers as a scenario, but `return` is
|
|
2983
|
+
* allowed in any non-entry function body and all diagnostics are
|
|
2984
|
+
* tagged with helper.path.
|
|
2985
|
+
*/
|
|
2986
|
+
lintHelper(helper, options = {}) {
|
|
2987
|
+
const diags = [];
|
|
2988
|
+
const userFns = options.userFunctionNames ?? /* @__PURE__ */ new Set();
|
|
2989
|
+
for (const stmt of helper.ast.statements) {
|
|
2990
|
+
if (stmt instanceof FunctionDefineStatement) {
|
|
2991
|
+
if (isBlockMappable(stmt.name)) {
|
|
2992
|
+
this.lintFunctionBody(
|
|
2993
|
+
stmt,
|
|
2994
|
+
diags,
|
|
2995
|
+
userFns,
|
|
2996
|
+
/*allowReturn*/
|
|
2997
|
+
false
|
|
2998
|
+
);
|
|
2999
|
+
} else {
|
|
3000
|
+
this.lintFunctionBody(
|
|
3001
|
+
stmt,
|
|
3002
|
+
diags,
|
|
3003
|
+
userFns,
|
|
3004
|
+
/*allowReturn*/
|
|
3005
|
+
true
|
|
3006
|
+
);
|
|
3007
|
+
}
|
|
3008
|
+
} else if (stmt instanceof MetaBlockStatement) {
|
|
3009
|
+
} else {
|
|
3010
|
+
diags.push(diag("error", "E2", `Only function definitions allowed at top level (got ${stmt.constructor.name})`, stmt));
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
diags.push(...this.runEngine(helper.ast, helper.source ?? "", userFns));
|
|
3014
|
+
return diags.map((d) => ({ ...d, source: helper.path }));
|
|
3015
|
+
}
|
|
3016
|
+
// ---- shared engine ------------------------------------------------------
|
|
3017
|
+
/** Run the `@unotest/dsl/validator` engine with the mobile contracts.
|
|
3018
|
+
* Engine diagnostics map 1:1 into mobile `Diagnostic` rows with
|
|
3019
|
+
* `validator:<rule>` codes. */
|
|
3020
|
+
runEngine(ast, source, userFns) {
|
|
3021
|
+
const registry = new MobileDslRegistry(ALL_DSL_FUNCTIONS, userFns);
|
|
3022
|
+
return validateDsl(ast, source, false, registry, {
|
|
3023
|
+
disabledRules: MOBILE_DISABLED_ENGINE_RULES
|
|
3024
|
+
}).map((d) => ({
|
|
3025
|
+
severity: d.severity,
|
|
3026
|
+
code: `validator:${d.rule}`,
|
|
3027
|
+
message: d.message,
|
|
3028
|
+
line: d.line ?? 0,
|
|
3029
|
+
col: d.column ?? 0
|
|
3030
|
+
}));
|
|
3031
|
+
}
|
|
3032
|
+
// ---- mobile-specific pass ----------------------------------------------
|
|
3033
|
+
lintFunctionBody(fn, diags, userFns, allowReturn) {
|
|
3034
|
+
if (!(fn.body instanceof BlockStatement)) {
|
|
3035
|
+
diags.push(diag("error", "E2", `Function "${fn.name}" body must be a block`, fn));
|
|
3036
|
+
return;
|
|
3037
|
+
}
|
|
3038
|
+
let firstUiSeen = false;
|
|
3039
|
+
const recordCall = /* @__PURE__ */ __name((name) => {
|
|
3040
|
+
if (UI_CALL_NAMES.has(name)) firstUiSeen = true;
|
|
3041
|
+
}, "recordCall");
|
|
3042
|
+
for (const stmt of fn.body.statements) {
|
|
3043
|
+
this.checkStatement(stmt, diags, recordCall, () => firstUiSeen, userFns, allowReturn);
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
checkStatement(stmt, diags, recordCall, isFirstUiSeen, userFns, allowReturn) {
|
|
3047
|
+
if (stmt instanceof ReturnStatement) {
|
|
3048
|
+
if (!allowReturn) {
|
|
3049
|
+
diags.push(diag("error", "E2", `'return' is not allowed inside test_*/flow_* function bodies (only in helpers)`, stmt));
|
|
3050
|
+
return;
|
|
3051
|
+
}
|
|
3052
|
+
const expr = stmt.expression;
|
|
3053
|
+
if (expr) this.checkExpression(expr, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
|
|
3054
|
+
return;
|
|
3055
|
+
}
|
|
3056
|
+
if (this.isForbiddenStatement(stmt)) {
|
|
3057
|
+
diags.push(diag("error", "E2", `Forbidden statement type "${stmt.constructor.name}" (D-4 subset)`, stmt));
|
|
3058
|
+
return;
|
|
3059
|
+
}
|
|
3060
|
+
if (stmt instanceof AssignmentStatement) {
|
|
3061
|
+
this.checkExpression(stmt.expression, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
|
|
3062
|
+
return;
|
|
3063
|
+
}
|
|
3064
|
+
if (stmt instanceof FunctionStatement) {
|
|
3065
|
+
const call = stmt.functionalExpression;
|
|
3066
|
+
if (!(call instanceof FunctionalExpression)) {
|
|
3067
|
+
diags.push(diag("error", "E2", `Method chains are not allowed (D-4 subset)`, stmt));
|
|
3068
|
+
return;
|
|
3069
|
+
}
|
|
3070
|
+
this.checkCall(call, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
|
|
3071
|
+
return;
|
|
3072
|
+
}
|
|
3073
|
+
if (stmt instanceof StepStatement) {
|
|
3074
|
+
this.checkStatement(stmt.body, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
|
|
3075
|
+
return;
|
|
3076
|
+
}
|
|
3077
|
+
if (stmt instanceof IfStatement) {
|
|
3078
|
+
this.checkExpression(stmt.expression, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
|
|
3079
|
+
this.checkStatement(stmt.ifStatement, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
|
|
3080
|
+
if (stmt.elseStatement) this.checkStatement(stmt.elseStatement, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
|
|
3081
|
+
return;
|
|
3082
|
+
}
|
|
3083
|
+
if (stmt instanceof BlockStatement) {
|
|
3084
|
+
for (const s of stmt.statements) this.checkStatement(s, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
|
|
3085
|
+
return;
|
|
3086
|
+
}
|
|
3087
|
+
if (stmt instanceof FunctionDefineStatement) {
|
|
3088
|
+
diags.push(diag("error", "E2", "Nested function definitions are not allowed", stmt));
|
|
3089
|
+
return;
|
|
3090
|
+
}
|
|
3091
|
+
if (stmt instanceof MetaBlockStatement) return;
|
|
3092
|
+
diags.push(diag("error", "E2", `Unsupported statement "${stmt.constructor.name}"`, stmt));
|
|
3093
|
+
}
|
|
3094
|
+
checkExpression(expr, diags, recordCall, isFirstUiSeen, userFns, allowReturn) {
|
|
3095
|
+
if (expr instanceof FunctionalExpression) {
|
|
3096
|
+
this.checkCall(expr, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
|
|
3097
|
+
return;
|
|
3098
|
+
}
|
|
3099
|
+
if (expr instanceof BinaryExpression) {
|
|
3100
|
+
this.checkExpression(expr.expr1, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
|
|
3101
|
+
this.checkExpression(expr.expr2, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
|
|
3102
|
+
return;
|
|
3103
|
+
}
|
|
3104
|
+
if (expr instanceof ArrayExpression) {
|
|
3105
|
+
for (const e of expr.elements) this.checkExpression(e, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
|
|
3106
|
+
return;
|
|
3107
|
+
}
|
|
3108
|
+
if (expr instanceof ValueExpression || expr instanceof VariableExpression) return;
|
|
3109
|
+
if (this.isForbiddenExpression(expr)) {
|
|
3110
|
+
diags.push(diag("error", "E2", `Forbidden expression "${expr.constructor.name}" (D-4 subset)`, expr));
|
|
3111
|
+
return;
|
|
3112
|
+
}
|
|
3113
|
+
diags.push(diag("error", "E2", `Unsupported expression "${expr.constructor.name}"`, expr));
|
|
3114
|
+
}
|
|
3115
|
+
checkCall(call, diags, recordCall, isFirstUiSeen, userFns, allowReturn) {
|
|
3116
|
+
const name = call.name;
|
|
3117
|
+
if (name === "setDevice" && call.arguments.length > 0) {
|
|
3118
|
+
const arg0 = call.arguments[0];
|
|
3119
|
+
if (arg0 instanceof ValueExpression) {
|
|
3120
|
+
const val = arg0.value.asString?.call(arg0.value);
|
|
3121
|
+
if (typeof val === "string" && !this.deps.knownSlots.includes(val)) {
|
|
3122
|
+
diags.push(
|
|
3123
|
+
diag("error", "E3", `setDevice("${val}"): unknown slot. Known: ${this.deps.knownSlots.join(", ") || "(none)"}`, call)
|
|
3124
|
+
);
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
if (name.startsWith(ASSERT_PREFIX) && !isFirstUiSeen()) {
|
|
3129
|
+
diags.push(diag("warning", "W5", `${name}() called before any UI action \u2014 typo or wrong order?`, call));
|
|
3130
|
+
}
|
|
3131
|
+
recordCall(name);
|
|
3132
|
+
for (const arg of call.arguments) {
|
|
3133
|
+
this.checkExpression(arg, diags, recordCall, isFirstUiSeen, userFns, allowReturn);
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
isForbiddenStatement(stmt) {
|
|
3137
|
+
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;
|
|
3138
|
+
}
|
|
3139
|
+
isForbiddenExpression(expr) {
|
|
3140
|
+
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.
|
|
3141
|
+
expr instanceof MemberCallExpression || expr instanceof PropertyAccessExpression;
|
|
3142
|
+
}
|
|
4519
3143
|
};
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
// src/dsl/functions/index.ts
|
|
4523
|
-
var ALL_DSL_FUNCTIONS = [
|
|
4524
|
-
...DEVICE_FUNCTIONS,
|
|
4525
|
-
...UI_FUNCTIONS,
|
|
4526
|
-
...SELECTOR_FUNCTIONS,
|
|
4527
|
-
...ALERT_FUNCTIONS,
|
|
4528
|
-
...DATA_FUNCTIONS,
|
|
4529
|
-
...ASSERT_FUNCTIONS,
|
|
4530
|
-
...TIME_FUNCTIONS
|
|
4531
|
-
];
|
|
4532
|
-
function buildDefaultRegistry() {
|
|
4533
|
-
const reg = new FunctionRegistry();
|
|
4534
|
-
for (const fn of ALL_DSL_FUNCTIONS) reg.register(fn);
|
|
4535
|
-
return reg;
|
|
3144
|
+
function isBlockMappable(name) {
|
|
3145
|
+
return name.startsWith("test_") || name.startsWith("flow_");
|
|
4536
3146
|
}
|
|
4537
|
-
__name(
|
|
3147
|
+
__name(isBlockMappable, "isBlockMappable");
|
|
3148
|
+
function diag(severity, code, message, node) {
|
|
3149
|
+
const tok = node.token;
|
|
3150
|
+
return {
|
|
3151
|
+
severity,
|
|
3152
|
+
code,
|
|
3153
|
+
message,
|
|
3154
|
+
line: tok?.getLine ? tok.getLine() : 0,
|
|
3155
|
+
col: tok?.getColumn ? tok.getColumn() : 0
|
|
3156
|
+
};
|
|
3157
|
+
}
|
|
3158
|
+
__name(diag, "diag");
|
|
4538
3159
|
|
|
4539
3160
|
// src/dsl/test-runtime-manager.ts
|
|
4540
3161
|
var TestRuntimeManager = class {
|
|
@@ -4695,849 +3316,77 @@ var TestRuntimeManager = class {
|
|
|
4695
3316
|
if (!active.ttlTimer) return;
|
|
4696
3317
|
const clear = this.deps.clearTimeoutFn ?? clearTimeout;
|
|
4697
3318
|
clear(active.ttlTimer);
|
|
4698
|
-
active.ttlTimer = null;
|
|
4699
|
-
}
|
|
4700
|
-
requireActive(id) {
|
|
4701
|
-
const a = this.runtimes.get(id);
|
|
4702
|
-
if (!a) throw new Error(`Runtime "${id}" not found (already aborted or never started)`);
|
|
4703
|
-
return a;
|
|
4704
|
-
}
|
|
4705
|
-
};
|
|
4706
|
-
|
|
4707
|
-
// src/runner/fs-scenario-repository.ts
|
|
4708
|
-
import { readFileSync as readFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync } from "fs";
|
|
4709
|
-
import { readdir } from "fs/promises";
|
|
4710
|
-
import { dirname as dirname2, resolve, join as join2, sep } from "path";
|
|
4711
|
-
var FileSystemScenarioRepository = class {
|
|
4712
|
-
static {
|
|
4713
|
-
__name(this, "FileSystemScenarioRepository");
|
|
4714
|
-
}
|
|
4715
|
-
scenariosDir;
|
|
4716
|
-
constructor(deps) {
|
|
4717
|
-
this.scenariosDir = resolve(deps.scenariosDir);
|
|
4718
|
-
}
|
|
4719
|
-
async list() {
|
|
4720
|
-
const out = [];
|
|
4721
|
-
await this.walk(this.scenariosDir, "", out);
|
|
4722
|
-
return out.sort();
|
|
4723
|
-
}
|
|
4724
|
-
async walk(dir, relPrefix, out) {
|
|
4725
|
-
const entries = await readdir(dir, { withFileTypes: true });
|
|
4726
|
-
for (const entry of entries) {
|
|
4727
|
-
if (entry.name.startsWith("_") || entry.name.startsWith(".")) continue;
|
|
4728
|
-
const abs = join2(dir, entry.name);
|
|
4729
|
-
const rel = relPrefix === "" ? entry.name : `${relPrefix}/${entry.name}`;
|
|
4730
|
-
if (entry.isDirectory()) {
|
|
4731
|
-
await this.walk(abs, rel, out);
|
|
4732
|
-
} else if (entry.isFile() && entry.name.endsWith(".js")) {
|
|
4733
|
-
out.push(rel.slice(0, -".js".length));
|
|
4734
|
-
}
|
|
4735
|
-
}
|
|
4736
|
-
}
|
|
4737
|
-
async load(name) {
|
|
4738
|
-
const path = this.pathFor(name);
|
|
4739
|
-
const source = readFileSync2(path, "utf8");
|
|
4740
|
-
return { name, source };
|
|
4741
|
-
}
|
|
4742
|
-
async has(name) {
|
|
4743
|
-
return existsSync2(this.pathFor(name));
|
|
4744
|
-
}
|
|
4745
|
-
async save(name, source, opts) {
|
|
4746
|
-
const path = this.pathFor(name);
|
|
4747
|
-
if (existsSync2(path) && !opts?.overwrite) {
|
|
4748
|
-
throw new Error(`scenario "${name}" already exists at ${path}`);
|
|
4749
|
-
}
|
|
4750
|
-
mkdirSync2(dirname2(path), { recursive: true });
|
|
4751
|
-
writeFileSync(path, source);
|
|
4752
|
-
return path;
|
|
4753
|
-
}
|
|
4754
|
-
pathFor(name) {
|
|
4755
|
-
if (name.includes("..") || name.startsWith("/") || name.includes("\\")) {
|
|
4756
|
-
throw new Error(`Invalid scenario name "${name}"`);
|
|
4757
|
-
}
|
|
4758
|
-
const relPath = name.split("/").join(sep) + ".js";
|
|
4759
|
-
return join2(this.scenariosDir, relPath);
|
|
4760
|
-
}
|
|
4761
|
-
};
|
|
4762
|
-
|
|
4763
|
-
// vendor/dsl/parser/token.ts
|
|
4764
|
-
var Token = class {
|
|
4765
|
-
static {
|
|
4766
|
-
__name(this, "Token");
|
|
4767
|
-
}
|
|
4768
|
-
text;
|
|
4769
|
-
type;
|
|
4770
|
-
column;
|
|
4771
|
-
line;
|
|
4772
|
-
constructor(tokenType, text, column, line) {
|
|
4773
|
-
this.type = tokenType;
|
|
4774
|
-
this.text = text;
|
|
4775
|
-
this.column = column;
|
|
4776
|
-
this.line = line;
|
|
4777
|
-
}
|
|
4778
|
-
getText() {
|
|
4779
|
-
return this.text;
|
|
4780
|
-
}
|
|
4781
|
-
getType() {
|
|
4782
|
-
return this.type;
|
|
4783
|
-
}
|
|
4784
|
-
getLine() {
|
|
4785
|
-
return this.line;
|
|
4786
|
-
}
|
|
4787
|
-
getColumn() {
|
|
4788
|
-
return this.column;
|
|
4789
|
-
}
|
|
4790
|
-
// Переопределение метода toString
|
|
4791
|
-
toString() {
|
|
4792
|
-
return `Token(type: ${TokenType[this.type]}, text: "${this.text}", line: ${this.line}, column: ${this.column})`;
|
|
4793
|
-
}
|
|
4794
|
-
};
|
|
4795
|
-
|
|
4796
|
-
// vendor/dsl/parser/tools.ts
|
|
4797
|
-
function isNumber(char) {
|
|
4798
|
-
const num = Number(char);
|
|
4799
|
-
return !isNaN(num) && char.trim() === num.toString();
|
|
4800
|
-
}
|
|
4801
|
-
__name(isNumber, "isNumber");
|
|
4802
|
-
function isLetter(char) {
|
|
4803
|
-
return /^[a-zA-Zа-яА-ЯёЁ]$/.test(char);
|
|
4804
|
-
}
|
|
4805
|
-
__name(isLetter, "isLetter");
|
|
4806
|
-
function isLetterInWord(char) {
|
|
4807
|
-
return /^[a-zA-Zа-яА-ЯёЁ0-9_$]$/.test(char);
|
|
4808
|
-
}
|
|
4809
|
-
__name(isLetterInWord, "isLetterInWord");
|
|
4810
|
-
|
|
4811
|
-
// vendor/dsl/parser/lexer.ts
|
|
4812
|
-
var Lexer = class {
|
|
4813
|
-
static {
|
|
4814
|
-
__name(this, "Lexer");
|
|
4815
|
-
}
|
|
4816
|
-
OPERATOR_CHARS = "+-*/(){}[]=<>!&|,:;";
|
|
4817
|
-
OPERATORS = /* @__PURE__ */ new Map([
|
|
4818
|
-
["+", 14 /* PLUS */],
|
|
4819
|
-
["++", 15 /* PLUSPLUS */],
|
|
4820
|
-
["-", 16 /* MINUS */],
|
|
4821
|
-
["--", 17 /* MINUSMINUS */],
|
|
4822
|
-
["*", 18 /* STAR */],
|
|
4823
|
-
["/", 19 /* SLASH */],
|
|
4824
|
-
["(", 32 /* LPAREN */],
|
|
4825
|
-
[")", 33 /* RPAREN */],
|
|
4826
|
-
["{", 34 /* LBRACE */],
|
|
4827
|
-
["}", 35 /* RBRACE */],
|
|
4828
|
-
["[", 36 /* LBRACKET */],
|
|
4829
|
-
["]", 37 /* RBRACKET */],
|
|
4830
|
-
["=", 20 /* EQ */],
|
|
4831
|
-
["<", 24 /* LT */],
|
|
4832
|
-
[">", 26 /* GT */],
|
|
4833
|
-
[",", 38 /* COMMA */],
|
|
4834
|
-
[":", 39 /* COLON */],
|
|
4835
|
-
[";", 40 /* SEMICOLON */],
|
|
4836
|
-
["!", 22 /* EXCL */],
|
|
4837
|
-
["&", 30 /* AMP */],
|
|
4838
|
-
["|", 28 /* BAR */],
|
|
4839
|
-
["==", 21 /* EQEQ */],
|
|
4840
|
-
["!=", 23 /* EXCLEQ */],
|
|
4841
|
-
["<=", 25 /* LTEQ */],
|
|
4842
|
-
[">=", 27 /* GTEQ */],
|
|
4843
|
-
["&&", 31 /* AMPAMP */],
|
|
4844
|
-
["||", 29 /* BARBAR */]
|
|
4845
|
-
]);
|
|
4846
|
-
input;
|
|
4847
|
-
length;
|
|
4848
|
-
tokens = [];
|
|
4849
|
-
pos = 0;
|
|
4850
|
-
column = 1;
|
|
4851
|
-
line = 1;
|
|
4852
|
-
constructor(input) {
|
|
4853
|
-
this.input = input;
|
|
4854
|
-
this.length = input.length;
|
|
4855
|
-
}
|
|
4856
|
-
tokenize() {
|
|
4857
|
-
while (this.pos < this.length) {
|
|
4858
|
-
const current = this.peek(0);
|
|
4859
|
-
if (isNumber(current)) {
|
|
4860
|
-
this.tokenizeNumber();
|
|
4861
|
-
} else if (isLetter(current)) {
|
|
4862
|
-
this.tokenizeWord();
|
|
4863
|
-
} else if (current === '"' || current === "'") {
|
|
4864
|
-
this.tokenizeText(current);
|
|
4865
|
-
} else if (this.peekGroup(3) === "//@") {
|
|
4866
|
-
this.tokenizeMetaWord();
|
|
4867
|
-
} else if (this.OPERATOR_CHARS.indexOf(current) !== -1) {
|
|
4868
|
-
this.tokenizeOperator();
|
|
4869
|
-
} else {
|
|
4870
|
-
this.next();
|
|
4871
|
-
}
|
|
4872
|
-
}
|
|
4873
|
-
return this.tokens;
|
|
4874
|
-
}
|
|
4875
|
-
next() {
|
|
4876
|
-
const currentChar = this.input.charAt(this.pos);
|
|
4877
|
-
this.pos++;
|
|
4878
|
-
if (currentChar == "\n") {
|
|
4879
|
-
this.line++;
|
|
4880
|
-
this.column = 1;
|
|
4881
|
-
} else {
|
|
4882
|
-
this.column++;
|
|
4883
|
-
}
|
|
4884
|
-
return this.peek(0);
|
|
4885
|
-
}
|
|
4886
|
-
peek(relativePosition) {
|
|
4887
|
-
const position = this.pos + relativePosition;
|
|
4888
|
-
if (position >= this.length) {
|
|
4889
|
-
return "\0";
|
|
4890
|
-
}
|
|
4891
|
-
return this.input.charAt(position);
|
|
4892
|
-
}
|
|
4893
|
-
peekGroup(count) {
|
|
4894
|
-
const buffer = [];
|
|
4895
|
-
for (let i = 0; i < count; i++) {
|
|
4896
|
-
buffer.push(this.peek(i));
|
|
4897
|
-
}
|
|
4898
|
-
return buffer.join("");
|
|
4899
|
-
}
|
|
4900
|
-
addToken(type, text, column, line) {
|
|
4901
|
-
this.tokens.push(new Token(type, text, column - 1, line));
|
|
4902
|
-
}
|
|
4903
|
-
tokenizeNumber() {
|
|
4904
|
-
const column = this.column;
|
|
4905
|
-
const line = this.line;
|
|
4906
|
-
const buffer = [];
|
|
4907
|
-
let current = this.peek(0);
|
|
4908
|
-
while (current !== "\0") {
|
|
4909
|
-
if (current == ".") {
|
|
4910
|
-
if (buffer.indexOf(".") !== -1) {
|
|
4911
|
-
throw new Error("Invalid float number");
|
|
4912
|
-
}
|
|
4913
|
-
} else if (!isNumber(current)) {
|
|
4914
|
-
break;
|
|
4915
|
-
}
|
|
4916
|
-
buffer.push(current);
|
|
4917
|
-
current = this.next();
|
|
4918
|
-
}
|
|
4919
|
-
this.addToken(0 /* NUMBER */, buffer.join(""), column, line);
|
|
4920
|
-
}
|
|
4921
|
-
consumeWord() {
|
|
4922
|
-
const buffer = [];
|
|
4923
|
-
let current = this.peek(0);
|
|
4924
|
-
while (current !== "\0") {
|
|
4925
|
-
if (!isLetterInWord(current)) {
|
|
4926
|
-
break;
|
|
4927
|
-
}
|
|
4928
|
-
buffer.push(current);
|
|
4929
|
-
current = this.next();
|
|
4930
|
-
}
|
|
4931
|
-
return buffer.join("");
|
|
4932
|
-
}
|
|
4933
|
-
tokenizeMetaWord() {
|
|
4934
|
-
const column = this.column;
|
|
4935
|
-
const line = this.line;
|
|
4936
|
-
this.next();
|
|
4937
|
-
this.next();
|
|
4938
|
-
this.next();
|
|
4939
|
-
const word = this.consumeWord();
|
|
4940
|
-
if (word.startsWith("end")) {
|
|
4941
|
-
this.addToken(42 /* CLOSE_META_BLOCK */, word, column, line);
|
|
4942
|
-
} else {
|
|
4943
|
-
this.addToken(41 /* META_BLOCK */, word, column, line);
|
|
4944
|
-
}
|
|
4945
|
-
}
|
|
4946
|
-
tokenizeWord() {
|
|
4947
|
-
const column = this.column;
|
|
4948
|
-
const line = this.line;
|
|
4949
|
-
const word = this.consumeWord();
|
|
4950
|
-
switch (word) {
|
|
4951
|
-
case "var":
|
|
4952
|
-
this.addToken(3 /* VAR */, "", column, line);
|
|
4953
|
-
break;
|
|
4954
|
-
case "print":
|
|
4955
|
-
this.addToken(4 /* PRINT */, "", column, line);
|
|
4956
|
-
break;
|
|
4957
|
-
case "if":
|
|
4958
|
-
this.addToken(5 /* IF */, "", column, line);
|
|
4959
|
-
break;
|
|
4960
|
-
case "else":
|
|
4961
|
-
this.addToken(6 /* ELSE */, "", column, line);
|
|
4962
|
-
break;
|
|
4963
|
-
case "for":
|
|
4964
|
-
this.addToken(8 /* FOR */, "", column, line);
|
|
4965
|
-
break;
|
|
4966
|
-
case "while":
|
|
4967
|
-
this.addToken(7 /* WHILE */, "", column, line);
|
|
4968
|
-
break;
|
|
4969
|
-
case "do":
|
|
4970
|
-
this.addToken(9 /* DO */, "", column, line);
|
|
4971
|
-
break;
|
|
4972
|
-
case "break":
|
|
4973
|
-
this.addToken(10 /* BREAK */, "", column, line);
|
|
4974
|
-
break;
|
|
4975
|
-
case "continue":
|
|
4976
|
-
this.addToken(11 /* CONTINUE */, "", column, line);
|
|
4977
|
-
break;
|
|
4978
|
-
case "function":
|
|
4979
|
-
this.addToken(12 /* FUNCTION */, "", column, line);
|
|
4980
|
-
break;
|
|
4981
|
-
case "return":
|
|
4982
|
-
this.addToken(13 /* RETURN */, "", column, line);
|
|
4983
|
-
break;
|
|
4984
|
-
default:
|
|
4985
|
-
this.addToken(1 /* WORD */, word, column, line);
|
|
4986
|
-
}
|
|
4987
|
-
}
|
|
4988
|
-
tokenizeText(char) {
|
|
4989
|
-
this.next();
|
|
4990
|
-
const column = this.column;
|
|
4991
|
-
const line = this.line;
|
|
4992
|
-
const buffer = [];
|
|
4993
|
-
let current = this.peek(0);
|
|
4994
|
-
while (current !== "\0") {
|
|
4995
|
-
if (current == "\\") {
|
|
4996
|
-
current = this.next();
|
|
4997
|
-
switch (current) {
|
|
4998
|
-
case char:
|
|
4999
|
-
current = this.next();
|
|
5000
|
-
buffer.push(char);
|
|
5001
|
-
continue;
|
|
5002
|
-
case "n":
|
|
5003
|
-
current = this.next();
|
|
5004
|
-
buffer.push("\n");
|
|
5005
|
-
continue;
|
|
5006
|
-
case "t":
|
|
5007
|
-
current = this.next();
|
|
5008
|
-
buffer.push(" ");
|
|
5009
|
-
continue;
|
|
5010
|
-
}
|
|
5011
|
-
buffer.push("\\");
|
|
5012
|
-
continue;
|
|
5013
|
-
}
|
|
5014
|
-
if (current == char) {
|
|
5015
|
-
break;
|
|
5016
|
-
}
|
|
5017
|
-
buffer.push(current);
|
|
5018
|
-
current = this.next();
|
|
5019
|
-
}
|
|
5020
|
-
if (current === "\0") {
|
|
5021
|
-
throw new Error("Missing close quote in string literal");
|
|
5022
|
-
}
|
|
5023
|
-
this.next();
|
|
5024
|
-
const text = buffer.join("");
|
|
5025
|
-
this.addToken(2 /* TEXT */, text, column, line);
|
|
5026
|
-
}
|
|
5027
|
-
tokenizeComment() {
|
|
5028
|
-
let current = this.peek(0);
|
|
5029
|
-
while ("\r\n\0".indexOf(current) == -1) {
|
|
5030
|
-
current = this.next();
|
|
5031
|
-
}
|
|
5032
|
-
}
|
|
5033
|
-
tokenizeMultilineComment() {
|
|
5034
|
-
let current = this.peek(0);
|
|
5035
|
-
while (current != "\0") {
|
|
5036
|
-
if (current == "*" && this.peek(1) == "/") break;
|
|
5037
|
-
current = this.next();
|
|
5038
|
-
}
|
|
5039
|
-
if (current == "\0") throw new Error("Missing close tag in comment");
|
|
5040
|
-
this.next();
|
|
5041
|
-
this.next();
|
|
5042
|
-
}
|
|
5043
|
-
tokenizeOperator() {
|
|
5044
|
-
let current = this.peek(0);
|
|
5045
|
-
if (current == "/") {
|
|
5046
|
-
if (this.peek(1) == "/") {
|
|
5047
|
-
this.next();
|
|
5048
|
-
this.next();
|
|
5049
|
-
this.tokenizeComment();
|
|
5050
|
-
return;
|
|
5051
|
-
} else if (this.peek(1) == "*") {
|
|
5052
|
-
this.next();
|
|
5053
|
-
this.next();
|
|
5054
|
-
this.tokenizeMultilineComment();
|
|
5055
|
-
return;
|
|
5056
|
-
}
|
|
5057
|
-
}
|
|
5058
|
-
const buffer = [];
|
|
5059
|
-
while (current !== "\0") {
|
|
5060
|
-
const text2 = buffer.join("");
|
|
5061
|
-
const column = this.column;
|
|
5062
|
-
const line = this.line;
|
|
5063
|
-
if (!this.OPERATORS.has(text2 + current) && text2 != "") {
|
|
5064
|
-
const type = this.OPERATORS.get(text2);
|
|
5065
|
-
if (!type) {
|
|
5066
|
-
throw new Error(`Invalid tokenizeOperator ${text2}`);
|
|
5067
|
-
}
|
|
5068
|
-
this.addToken(type, "", column, line);
|
|
5069
|
-
return;
|
|
5070
|
-
}
|
|
5071
|
-
buffer.push(current);
|
|
5072
|
-
current = this.next();
|
|
5073
|
-
}
|
|
5074
|
-
const text = buffer.join("");
|
|
5075
|
-
if (text !== "") {
|
|
5076
|
-
const type = this.OPERATORS.get(text);
|
|
5077
|
-
if (type) {
|
|
5078
|
-
this.addToken(type, "", this.column, this.line);
|
|
5079
|
-
return;
|
|
5080
|
-
}
|
|
5081
|
-
}
|
|
5082
|
-
throw new Error("Invalid tokenizeOperator: unexpected end of input");
|
|
5083
|
-
}
|
|
5084
|
-
};
|
|
5085
|
-
|
|
5086
|
-
// vendor/dsl/parser/parser.ts
|
|
5087
|
-
var Parser = class _Parser {
|
|
5088
|
-
static {
|
|
5089
|
-
__name(this, "Parser");
|
|
5090
|
-
}
|
|
5091
|
-
EOF = new Token(43 /* EOF */, "", -1, -1);
|
|
5092
|
-
tokens = [];
|
|
5093
|
-
pos = 0;
|
|
5094
|
-
size;
|
|
5095
|
-
constructor(tokens) {
|
|
5096
|
-
this.tokens = tokens;
|
|
5097
|
-
this.size = tokens.length;
|
|
5098
|
-
}
|
|
5099
|
-
parse() {
|
|
5100
|
-
const current = this.get(0);
|
|
5101
|
-
const result = new BlockStatement(current);
|
|
5102
|
-
while (!this.match(43 /* EOF */)) {
|
|
5103
|
-
result.add(this.statement());
|
|
5104
|
-
this.match(40 /* SEMICOLON */);
|
|
5105
|
-
}
|
|
5106
|
-
return result;
|
|
5107
|
-
}
|
|
5108
|
-
statementOrBlock() {
|
|
5109
|
-
if (this.get(0).getType() === 34 /* LBRACE */) {
|
|
5110
|
-
return this.block();
|
|
5111
|
-
}
|
|
5112
|
-
return this.statement();
|
|
5113
|
-
}
|
|
5114
|
-
blockMetaBlock(name) {
|
|
5115
|
-
const current = this.get(0);
|
|
5116
|
-
const block = new BlockStatement(current);
|
|
5117
|
-
while (!this.match(42 /* CLOSE_META_BLOCK */)) {
|
|
5118
|
-
block.add(this.statement());
|
|
5119
|
-
this.match(40 /* SEMICOLON */);
|
|
5120
|
-
}
|
|
5121
|
-
return block;
|
|
5122
|
-
}
|
|
5123
|
-
metaBlock() {
|
|
5124
|
-
const current = this.get(0);
|
|
5125
|
-
const name = this.consume(41 /* META_BLOCK */).getText();
|
|
5126
|
-
this.consume(32 /* LPAREN */);
|
|
5127
|
-
const argNames = [];
|
|
5128
|
-
while (!this.match(33 /* RPAREN */)) {
|
|
5129
|
-
argNames.push(this.consume(2 /* TEXT */).getText());
|
|
5130
|
-
this.match(38 /* COMMA */);
|
|
5131
|
-
}
|
|
5132
|
-
const body = this.blockMetaBlock(name);
|
|
5133
|
-
return new MetaBlockStatement(name, argNames, body, current);
|
|
5134
|
-
}
|
|
5135
|
-
block() {
|
|
5136
|
-
const current = this.get(0);
|
|
5137
|
-
const block = new BlockStatement(current);
|
|
5138
|
-
this.consume(34 /* LBRACE */);
|
|
5139
|
-
while (!this.match(35 /* RBRACE */)) {
|
|
5140
|
-
block.add(this.statement());
|
|
5141
|
-
this.match(40 /* SEMICOLON */);
|
|
5142
|
-
}
|
|
5143
|
-
return block;
|
|
5144
|
-
}
|
|
5145
|
-
statement() {
|
|
5146
|
-
const current = this.get(0);
|
|
5147
|
-
if (this.match(3 /* VAR */)) {
|
|
5148
|
-
return this.varStatement(current);
|
|
5149
|
-
}
|
|
5150
|
-
if (this.match(4 /* PRINT */)) {
|
|
5151
|
-
return new PrintStatement(this.expression(), current);
|
|
5152
|
-
}
|
|
5153
|
-
if (this.match(5 /* IF */)) {
|
|
5154
|
-
return this.ifElse(current);
|
|
5155
|
-
}
|
|
5156
|
-
if (this.match(7 /* WHILE */)) {
|
|
5157
|
-
return this.whileStatement(current);
|
|
5158
|
-
}
|
|
5159
|
-
if (this.match(9 /* DO */)) {
|
|
5160
|
-
return this.doWhileStatement(current);
|
|
5161
|
-
}
|
|
5162
|
-
if (this.match(10 /* BREAK */)) {
|
|
5163
|
-
return new BreakStatement(current);
|
|
5164
|
-
}
|
|
5165
|
-
if (this.match(11 /* CONTINUE */)) {
|
|
5166
|
-
return new ContinueStatement(current);
|
|
5167
|
-
}
|
|
5168
|
-
if (this.match(13 /* RETURN */)) {
|
|
5169
|
-
return new ReturnStatement(this.expression(), current);
|
|
5170
|
-
}
|
|
5171
|
-
if (this.match(8 /* FOR */)) {
|
|
5172
|
-
return this.forStatement(current);
|
|
5173
|
-
}
|
|
5174
|
-
if (this.match(12 /* FUNCTION */)) {
|
|
5175
|
-
return this.functionDefine(current);
|
|
5176
|
-
}
|
|
5177
|
-
if (this.lookMatch(0, 41 /* META_BLOCK */)) {
|
|
5178
|
-
return this.metaBlock();
|
|
5179
|
-
}
|
|
5180
|
-
if (this.lookMatch(0, 1 /* WORD */) && this.lookMatch(1, 32 /* LPAREN */)) {
|
|
5181
|
-
return new FunctionStatement(this.functionExpression(current), current);
|
|
5182
|
-
}
|
|
5183
|
-
if (this.match(15 /* PLUSPLUS */)) {
|
|
5184
|
-
const variable = new VariableExpression(this.consume(1 /* WORD */).getText(), current);
|
|
5185
|
-
return new IncrementStatement(
|
|
5186
|
-
new IncrementExpression(variable, 0 /* PREFIX_INCREMENT */, current),
|
|
5187
|
-
current
|
|
5188
|
-
);
|
|
5189
|
-
}
|
|
5190
|
-
if (this.match(17 /* MINUSMINUS */)) {
|
|
5191
|
-
const variable = new VariableExpression(this.consume(1 /* WORD */).getText(), current);
|
|
5192
|
-
return new IncrementStatement(
|
|
5193
|
-
new IncrementExpression(variable, 1 /* PREFIX_DECREMENT */, current),
|
|
5194
|
-
current
|
|
5195
|
-
);
|
|
5196
|
-
}
|
|
5197
|
-
if (this.lookMatch(0, 1 /* WORD */) && (this.lookMatch(1, 15 /* PLUSPLUS */) || this.lookMatch(1, 17 /* MINUSMINUS */))) {
|
|
5198
|
-
const varToken = this.consume(1 /* WORD */);
|
|
5199
|
-
const varName = varToken.getText();
|
|
5200
|
-
if (this.match(15 /* PLUSPLUS */)) {
|
|
5201
|
-
throw new Error(
|
|
5202
|
-
`Postfix increment '${varName}++' is not supported. Use '${varName} = ${varName} + 1' instead. ${varToken.toString()}`
|
|
5203
|
-
);
|
|
5204
|
-
} else if (this.match(17 /* MINUSMINUS */)) {
|
|
5205
|
-
throw new Error(
|
|
5206
|
-
`Postfix decrement '${varName}--' is not supported. Use '${varName} = ${varName} - 1' instead. ${varToken.toString()}`
|
|
5207
|
-
);
|
|
5208
|
-
}
|
|
5209
|
-
}
|
|
5210
|
-
return this.assignmentStatement();
|
|
5211
|
-
}
|
|
5212
|
-
static RESERVED_WORDS = /* @__PURE__ */ new Set([
|
|
5213
|
-
"true",
|
|
5214
|
-
"false",
|
|
5215
|
-
"null",
|
|
5216
|
-
"undefined",
|
|
5217
|
-
"let",
|
|
5218
|
-
"const",
|
|
5219
|
-
"class",
|
|
5220
|
-
"new",
|
|
5221
|
-
"delete",
|
|
5222
|
-
"typeof",
|
|
5223
|
-
"instanceof",
|
|
5224
|
-
"in",
|
|
5225
|
-
"of",
|
|
5226
|
-
"import",
|
|
5227
|
-
"export",
|
|
5228
|
-
"default",
|
|
5229
|
-
"extends",
|
|
5230
|
-
"super",
|
|
5231
|
-
"this",
|
|
5232
|
-
"void",
|
|
5233
|
-
"throw",
|
|
5234
|
-
"try",
|
|
5235
|
-
"catch",
|
|
5236
|
-
"finally",
|
|
5237
|
-
"switch",
|
|
5238
|
-
"case",
|
|
5239
|
-
"with",
|
|
5240
|
-
"debugger",
|
|
5241
|
-
"yield",
|
|
5242
|
-
"await"
|
|
5243
|
-
]);
|
|
5244
|
-
varStatement(token) {
|
|
5245
|
-
const declarators = [];
|
|
5246
|
-
do {
|
|
5247
|
-
const nameToken = this.consume(1 /* WORD */);
|
|
5248
|
-
const name = nameToken.getText();
|
|
5249
|
-
if (_Parser.RESERVED_WORDS.has(name)) {
|
|
5250
|
-
throw new Error(
|
|
5251
|
-
`Variable name '${name}' is a reserved JavaScript keyword. Use a different name. ${nameToken.toString()}`
|
|
5252
|
-
);
|
|
5253
|
-
}
|
|
5254
|
-
let init = null;
|
|
5255
|
-
if (this.match(20 /* EQ */)) {
|
|
5256
|
-
init = this.expression();
|
|
5257
|
-
}
|
|
5258
|
-
declarators.push({ name, init });
|
|
5259
|
-
} while (this.match(38 /* COMMA */));
|
|
5260
|
-
return new VarStatement(declarators, token);
|
|
5261
|
-
}
|
|
5262
|
-
assignmentStatement() {
|
|
5263
|
-
const current = this.get(0);
|
|
5264
|
-
if (this.lookMatch(0, 1 /* WORD */) && this.lookMatch(1, 20 /* EQ */)) {
|
|
5265
|
-
const variable = this.consume(1 /* WORD */).getText();
|
|
5266
|
-
this.consume(20 /* EQ */);
|
|
5267
|
-
return new AssignmentStatement(variable, this.expression(), current);
|
|
5268
|
-
}
|
|
5269
|
-
if (this.lookMatch(0, 1 /* WORD */) && this.lookMatch(1, 36 /* LBRACKET */)) {
|
|
5270
|
-
const array = this.elementExpression(current);
|
|
5271
|
-
this.consume(20 /* EQ */);
|
|
5272
|
-
return new ArrayAssignmentStatement(array, this.expression(), current);
|
|
5273
|
-
}
|
|
5274
|
-
throw new Error(`Unknown statement ${this.get(0)}`);
|
|
5275
|
-
}
|
|
5276
|
-
ifElse(token) {
|
|
5277
|
-
const condition = this.expression();
|
|
5278
|
-
const ifStatement = this.statementOrBlock();
|
|
5279
|
-
let elseStatement;
|
|
5280
|
-
if (this.match(6 /* ELSE */)) {
|
|
5281
|
-
elseStatement = this.statementOrBlock();
|
|
5282
|
-
} else {
|
|
5283
|
-
elseStatement = null;
|
|
5284
|
-
}
|
|
5285
|
-
return new IfStatement(condition, ifStatement, elseStatement, token);
|
|
5286
|
-
}
|
|
5287
|
-
doWhileStatement(token) {
|
|
5288
|
-
const statement = this.statementOrBlock();
|
|
5289
|
-
this.consume(7 /* WHILE */);
|
|
5290
|
-
const condition = this.expression();
|
|
5291
|
-
return new DoWhileStatement(condition, statement, token);
|
|
5292
|
-
}
|
|
5293
|
-
whileStatement(token) {
|
|
5294
|
-
const condition = this.expression();
|
|
5295
|
-
const statement = this.statementOrBlock();
|
|
5296
|
-
return new WhileStatement(condition, statement, token);
|
|
5297
|
-
}
|
|
5298
|
-
forStatement(token) {
|
|
5299
|
-
this.consume(32 /* LPAREN */);
|
|
5300
|
-
let initialization;
|
|
5301
|
-
if (this.match(3 /* VAR */)) {
|
|
5302
|
-
throw new Error(
|
|
5303
|
-
`'var' in for-loop initialization is not supported by the Blockly converter. Use plain assignment: 'for (k = 1; k <= N; k = k + 1)'. ${this.get(-1).toString()}`
|
|
5304
|
-
);
|
|
5305
|
-
} else {
|
|
5306
|
-
initialization = this.assignmentStatement();
|
|
5307
|
-
}
|
|
5308
|
-
this.consume(40 /* SEMICOLON */);
|
|
5309
|
-
const termination = this.expression();
|
|
5310
|
-
this.consume(40 /* SEMICOLON */);
|
|
5311
|
-
const increment = this.statement();
|
|
5312
|
-
this.consume(33 /* RPAREN */);
|
|
5313
|
-
const block = this.statementOrBlock();
|
|
5314
|
-
return new ForStatement(initialization, termination, increment, block, token);
|
|
5315
|
-
}
|
|
5316
|
-
functionDefine(token) {
|
|
5317
|
-
const name = this.consume(1 /* WORD */).getText();
|
|
5318
|
-
this.consume(32 /* LPAREN */);
|
|
5319
|
-
const argNames = [];
|
|
5320
|
-
while (!this.match(33 /* RPAREN */)) {
|
|
5321
|
-
argNames.push(this.consume(1 /* WORD */).getText());
|
|
5322
|
-
this.match(38 /* COMMA */);
|
|
5323
|
-
}
|
|
5324
|
-
const body = this.statementOrBlock();
|
|
5325
|
-
return new FunctionDefineStatement(name, argNames, body, token);
|
|
5326
|
-
}
|
|
5327
|
-
functionExpression(token) {
|
|
5328
|
-
const name = this.consume(1 /* WORD */).getText();
|
|
5329
|
-
this.consume(32 /* LPAREN */);
|
|
5330
|
-
const func = new FunctionalExpression(name, [], token);
|
|
5331
|
-
while (!this.match(33 /* RPAREN */)) {
|
|
5332
|
-
func.addArgument(this.expression());
|
|
5333
|
-
this.match(38 /* COMMA */);
|
|
5334
|
-
}
|
|
5335
|
-
return func;
|
|
5336
|
-
}
|
|
5337
|
-
elementExpression(token) {
|
|
5338
|
-
const variable = this.consume(1 /* WORD */).getText();
|
|
5339
|
-
const indexes = [];
|
|
5340
|
-
do {
|
|
5341
|
-
this.consume(36 /* LBRACKET */);
|
|
5342
|
-
indexes.push(this.expression());
|
|
5343
|
-
this.consume(37 /* RBRACKET */);
|
|
5344
|
-
} while (this.lookMatch(0, 36 /* LBRACKET */));
|
|
5345
|
-
return new ArrayAccessExpression(variable, indexes, token);
|
|
5346
|
-
}
|
|
5347
|
-
arrayExpression(token) {
|
|
5348
|
-
this.consume(36 /* LBRACKET */);
|
|
5349
|
-
const elements = [];
|
|
5350
|
-
while (!this.match(37 /* RBRACKET */)) {
|
|
5351
|
-
elements.push(this.expression());
|
|
5352
|
-
this.match(38 /* COMMA */);
|
|
5353
|
-
}
|
|
5354
|
-
return new ArrayExpression(elements, token);
|
|
5355
|
-
}
|
|
5356
|
-
objectExpression(token) {
|
|
5357
|
-
this.consume(34 /* LBRACE */);
|
|
5358
|
-
const properties = /* @__PURE__ */ new Map();
|
|
5359
|
-
while (!this.match(35 /* RBRACE */)) {
|
|
5360
|
-
let key;
|
|
5361
|
-
if (this.lookMatch(0, 1 /* WORD */)) {
|
|
5362
|
-
key = this.consume(1 /* WORD */).getText();
|
|
5363
|
-
} else if (this.lookMatch(0, 2 /* TEXT */)) {
|
|
5364
|
-
key = this.consume(2 /* TEXT */).getText();
|
|
5365
|
-
} else {
|
|
5366
|
-
throw new Error("Expected property key (word or string)");
|
|
5367
|
-
}
|
|
5368
|
-
this.consume(39 /* COLON */);
|
|
5369
|
-
const value = this.expression();
|
|
5370
|
-
properties.set(key, value);
|
|
5371
|
-
this.match(38 /* COMMA */);
|
|
5372
|
-
}
|
|
5373
|
-
return new ObjectExpression(properties, token);
|
|
5374
|
-
}
|
|
5375
|
-
expression() {
|
|
5376
|
-
return this.logicalOr();
|
|
3319
|
+
active.ttlTimer = null;
|
|
5377
3320
|
}
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
result = new ConditionalExpression("||" /* OR */, result, this.logicalAnd(), current);
|
|
5383
|
-
}
|
|
5384
|
-
return result;
|
|
3321
|
+
requireActive(id) {
|
|
3322
|
+
const a = this.runtimes.get(id);
|
|
3323
|
+
if (!a) throw new Error(`Runtime "${id}" not found (already aborted or never started)`);
|
|
3324
|
+
return a;
|
|
5385
3325
|
}
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
3326
|
+
};
|
|
3327
|
+
|
|
3328
|
+
// src/runner/fs-scenario-repository.ts
|
|
3329
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync } from "fs";
|
|
3330
|
+
import { readdir } from "fs/promises";
|
|
3331
|
+
import { dirname as dirname2, resolve, join as join2, sep } from "path";
|
|
3332
|
+
var FileSystemScenarioRepository = class {
|
|
3333
|
+
static {
|
|
3334
|
+
__name(this, "FileSystemScenarioRepository");
|
|
5393
3335
|
}
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
if (this.match(21 /* EQEQ */)) {
|
|
5398
|
-
return new ConditionalExpression("==" /* EQ */, result, this.conditional(), current);
|
|
5399
|
-
}
|
|
5400
|
-
if (this.match(23 /* EXCLEQ */)) {
|
|
5401
|
-
return new ConditionalExpression("!=" /* NEQ */, result, this.conditional(), current);
|
|
5402
|
-
}
|
|
5403
|
-
return result;
|
|
3336
|
+
scenariosDir;
|
|
3337
|
+
constructor(deps) {
|
|
3338
|
+
this.scenariosDir = resolve(deps.scenariosDir);
|
|
5404
3339
|
}
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
if (this.match(24 /* LT */)) {
|
|
5410
|
-
result = new ConditionalExpression("<" /* LT */, result, this.additive(), current);
|
|
5411
|
-
continue;
|
|
5412
|
-
}
|
|
5413
|
-
if (this.match(25 /* LTEQ */)) {
|
|
5414
|
-
result = new ConditionalExpression("<=" /* LTE */, result, this.additive(), current);
|
|
5415
|
-
continue;
|
|
5416
|
-
}
|
|
5417
|
-
if (this.match(26 /* GT */)) {
|
|
5418
|
-
result = new ConditionalExpression(">" /* GT */, result, this.additive(), current);
|
|
5419
|
-
continue;
|
|
5420
|
-
}
|
|
5421
|
-
if (this.match(27 /* GTEQ */)) {
|
|
5422
|
-
result = new ConditionalExpression(">=" /* GTE */, result, this.additive(), current);
|
|
5423
|
-
continue;
|
|
5424
|
-
}
|
|
5425
|
-
}
|
|
5426
|
-
return result;
|
|
3340
|
+
async list() {
|
|
3341
|
+
const out = [];
|
|
3342
|
+
await this.walk(this.scenariosDir, "", out);
|
|
3343
|
+
return out.sort();
|
|
5427
3344
|
}
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
if (
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
result = new BinaryExpression("-", result, this.multiplicate(), opToken);
|
|
5439
|
-
}
|
|
5440
|
-
return result;
|
|
5441
|
-
}
|
|
5442
|
-
multiplicate() {
|
|
5443
|
-
let result = this.unary();
|
|
5444
|
-
while (this.lookMatch(0, 18 /* STAR */) || this.lookMatch(0, 19 /* SLASH */)) {
|
|
5445
|
-
if (this.match(18 /* STAR */)) {
|
|
5446
|
-
const opToken2 = this.get(-1);
|
|
5447
|
-
result = new BinaryExpression("*", result, this.unary(), opToken2);
|
|
5448
|
-
continue;
|
|
3345
|
+
async walk(dir, relPrefix, out) {
|
|
3346
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
3347
|
+
for (const entry of entries) {
|
|
3348
|
+
if (entry.name.startsWith("_") || entry.name.startsWith(".")) continue;
|
|
3349
|
+
const abs = join2(dir, entry.name);
|
|
3350
|
+
const rel = relPrefix === "" ? entry.name : `${relPrefix}/${entry.name}`;
|
|
3351
|
+
if (entry.isDirectory()) {
|
|
3352
|
+
await this.walk(abs, rel, out);
|
|
3353
|
+
} else if (entry.isFile() && entry.name.endsWith(".js")) {
|
|
3354
|
+
out.push(rel.slice(0, -".js".length));
|
|
5449
3355
|
}
|
|
5450
|
-
this.consume(19 /* SLASH */);
|
|
5451
|
-
const opToken = this.get(-1);
|
|
5452
|
-
result = new BinaryExpression("/", result, this.unary(), opToken);
|
|
5453
|
-
}
|
|
5454
|
-
return result;
|
|
5455
|
-
}
|
|
5456
|
-
unary() {
|
|
5457
|
-
const current = this.get(0);
|
|
5458
|
-
if (this.match(16 /* MINUS */)) {
|
|
5459
|
-
return new UnaryExpression("-", this.primary(), current);
|
|
5460
|
-
}
|
|
5461
|
-
if (this.match(15 /* PLUSPLUS */)) {
|
|
5462
|
-
const variable = new VariableExpression(this.consume(1 /* WORD */).getText(), current);
|
|
5463
|
-
return new IncrementExpression(variable, 0 /* PREFIX_INCREMENT */, current);
|
|
5464
3356
|
}
|
|
5465
|
-
if (this.match(17 /* MINUSMINUS */)) {
|
|
5466
|
-
const variable = new VariableExpression(this.consume(1 /* WORD */).getText(), current);
|
|
5467
|
-
return new IncrementExpression(variable, 1 /* PREFIX_DECREMENT */, current);
|
|
5468
|
-
}
|
|
5469
|
-
return this.primary();
|
|
5470
3357
|
}
|
|
5471
|
-
|
|
5472
|
-
const
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
}
|
|
5476
|
-
if (this.lookMatch(0, 1 /* WORD */) && this.lookMatch(1, 32 /* LPAREN */)) {
|
|
5477
|
-
return this.functionExpression(current);
|
|
5478
|
-
}
|
|
5479
|
-
if (this.lookMatch(0, 1 /* WORD */) && this.lookMatch(1, 36 /* LBRACKET */)) {
|
|
5480
|
-
return this.elementExpression(current);
|
|
5481
|
-
}
|
|
5482
|
-
if (this.lookMatch(0, 36 /* LBRACKET */)) {
|
|
5483
|
-
return this.arrayExpression(current);
|
|
5484
|
-
}
|
|
5485
|
-
if (this.lookMatch(0, 34 /* LBRACE */)) {
|
|
5486
|
-
return this.objectExpression(current);
|
|
5487
|
-
}
|
|
5488
|
-
if (this.match(1 /* WORD */)) {
|
|
5489
|
-
const variable = new VariableExpression(current.getText(), current);
|
|
5490
|
-
if (this.match(15 /* PLUSPLUS */)) {
|
|
5491
|
-
throw new Error(
|
|
5492
|
-
`Postfix increment '${current.getText()}++' is not supported. Use '${current.getText()} = ${current.getText()} + 1' instead. ${current.toString()}`
|
|
5493
|
-
);
|
|
5494
|
-
}
|
|
5495
|
-
if (this.match(17 /* MINUSMINUS */)) {
|
|
5496
|
-
throw new Error(
|
|
5497
|
-
`Postfix decrement '${current.getText()}--' is not supported. Use '${current.getText()} = ${current.getText()} - 1' instead. ${current.toString()}`
|
|
5498
|
-
);
|
|
5499
|
-
}
|
|
5500
|
-
return variable;
|
|
5501
|
-
}
|
|
5502
|
-
if (this.match(2 /* TEXT */)) {
|
|
5503
|
-
return new ValueExpression(current.getText(), current);
|
|
5504
|
-
}
|
|
5505
|
-
if (this.match(32 /* LPAREN */)) {
|
|
5506
|
-
const result = this.expression();
|
|
5507
|
-
this.match(33 /* RPAREN */);
|
|
5508
|
-
return result;
|
|
5509
|
-
}
|
|
5510
|
-
throw new Error(`Invalid token ${current}`);
|
|
3358
|
+
async load(name) {
|
|
3359
|
+
const path = this.pathFor(name);
|
|
3360
|
+
const source = readFileSync2(path, "utf8");
|
|
3361
|
+
return { name, source };
|
|
5511
3362
|
}
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
if (type != current.getType()) {
|
|
5515
|
-
throw new Error(`Token ${current} doesn't match token type ${TokenType[type]}`);
|
|
5516
|
-
}
|
|
5517
|
-
this.pos++;
|
|
5518
|
-
return current;
|
|
3363
|
+
async has(name) {
|
|
3364
|
+
return existsSync2(this.pathFor(name));
|
|
5519
3365
|
}
|
|
5520
|
-
|
|
5521
|
-
const
|
|
5522
|
-
if (
|
|
5523
|
-
|
|
3366
|
+
async save(name, source, opts) {
|
|
3367
|
+
const path = this.pathFor(name);
|
|
3368
|
+
if (existsSync2(path) && !opts?.overwrite) {
|
|
3369
|
+
throw new Error(`scenario "${name}" already exists at ${path}`);
|
|
5524
3370
|
}
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
lookMatch(pos, type) {
|
|
5529
|
-
return this.get(pos).getType() == type;
|
|
3371
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
3372
|
+
writeFileSync(path, source);
|
|
3373
|
+
return path;
|
|
5530
3374
|
}
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
return this.EOF;
|
|
3375
|
+
pathFor(name) {
|
|
3376
|
+
if (name.includes("..") || name.startsWith("/") || name.includes("\\")) {
|
|
3377
|
+
throw new Error(`Invalid scenario name "${name}"`);
|
|
5535
3378
|
}
|
|
5536
|
-
|
|
3379
|
+
const relPath = name.split("/").join(sep) + ".js";
|
|
3380
|
+
return join2(this.scenariosDir, relPath);
|
|
5537
3381
|
}
|
|
5538
3382
|
};
|
|
5539
3383
|
|
|
5540
3384
|
// src/runner/scenario-loader.ts
|
|
3385
|
+
import { Lexer } from "@unotest/dsl";
|
|
3386
|
+
import { Parser } from "@unotest/dsl";
|
|
3387
|
+
import {
|
|
3388
|
+
FunctionDefineStatement as FunctionDefineStatement2
|
|
3389
|
+
} from "@unotest/dsl";
|
|
5541
3390
|
var ScenarioLoader = class {
|
|
5542
3391
|
constructor(repo, helpers) {
|
|
5543
3392
|
this.repo = repo;
|
|
@@ -5570,7 +3419,7 @@ function resolveEntryFunctionName(scenarioName, prefix = "test") {
|
|
|
5570
3419
|
__name(resolveEntryFunctionName, "resolveEntryFunctionName");
|
|
5571
3420
|
function collectOrigins(program, sourcePath, out) {
|
|
5572
3421
|
for (const stmt of program.statements) {
|
|
5573
|
-
if (stmt instanceof
|
|
3422
|
+
if (stmt instanceof FunctionDefineStatement2) {
|
|
5574
3423
|
const tok = stmt.token;
|
|
5575
3424
|
out.push({
|
|
5576
3425
|
name: stmt.name,
|
|
@@ -5587,6 +3436,8 @@ __name(collectOrigins, "collectOrigins");
|
|
|
5587
3436
|
import { readdir as readdir2, readFile } from "fs/promises";
|
|
5588
3437
|
import { existsSync as existsSync3 } from "fs";
|
|
5589
3438
|
import { join as join3, relative } from "path";
|
|
3439
|
+
import { Lexer as Lexer2 } from "@unotest/dsl";
|
|
3440
|
+
import { Parser as Parser2 } from "@unotest/dsl";
|
|
5590
3441
|
var FileSystemHelperRepository = class {
|
|
5591
3442
|
static {
|
|
5592
3443
|
__name(this, "FileSystemHelperRepository");
|
|
@@ -5603,8 +3454,8 @@ var FileSystemHelperRepository = class {
|
|
|
5603
3454
|
const out = [];
|
|
5604
3455
|
for (const absolutePath of files) {
|
|
5605
3456
|
const source = await readFile(absolutePath, "utf8");
|
|
5606
|
-
const tokens = new
|
|
5607
|
-
const ast = new
|
|
3457
|
+
const tokens = new Lexer2(source).tokenize();
|
|
3458
|
+
const ast = new Parser2(tokens).parse();
|
|
5608
3459
|
out.push({
|
|
5609
3460
|
path: relative(this.scenariosDir, absolutePath),
|
|
5610
3461
|
absolutePath,
|
|
@@ -5893,14 +3744,15 @@ var DslViewService = class {
|
|
|
5893
3744
|
generate(session, log) {
|
|
5894
3745
|
const warnings = [];
|
|
5895
3746
|
const lines = [];
|
|
5896
|
-
lines.push(
|
|
3747
|
+
lines.push(`// ${session.title ?? session.scenarioName}`);
|
|
5897
3748
|
lines.push(`function test_${session.scenarioName.replace(/[^a-zA-Z0-9_]/g, "_")}() {`);
|
|
5898
|
-
lines.push(`
|
|
5899
|
-
|
|
3749
|
+
lines.push(` step("setup", () => {`);
|
|
3750
|
+
lines.push(` setDevice(${JSON.stringify(session.device)});`);
|
|
3751
|
+
let openSection = "setup";
|
|
5900
3752
|
for (const entry of log.entries) {
|
|
5901
3753
|
if (entry.section !== openSection) {
|
|
5902
|
-
|
|
5903
|
-
lines.push(`
|
|
3754
|
+
lines.push(" });");
|
|
3755
|
+
lines.push(` step(${JSON.stringify(entry.section)}, () => {`);
|
|
5904
3756
|
openSection = entry.section;
|
|
5905
3757
|
}
|
|
5906
3758
|
const result = renderEntry(entry);
|
|
@@ -5910,7 +3762,7 @@ var DslViewService = class {
|
|
|
5910
3762
|
type: "NO_DSL_PRIMITIVE",
|
|
5911
3763
|
message: result.reason
|
|
5912
3764
|
});
|
|
5913
|
-
lines.push(`
|
|
3765
|
+
lines.push(` // SKIPPED (${result.reason}) \u2014 replace with getByTestId/getByText/getByLabel`);
|
|
5914
3766
|
continue;
|
|
5915
3767
|
}
|
|
5916
3768
|
if (entry.stability === "fragile") {
|
|
@@ -5923,22 +3775,13 @@ var DslViewService = class {
|
|
|
5923
3775
|
for (const w of result.warnings ?? []) {
|
|
5924
3776
|
warnings.push({ ...w, entryId: entry.entryId });
|
|
5925
3777
|
}
|
|
5926
|
-
lines.push("
|
|
3778
|
+
lines.push(" " + result.stmt);
|
|
5927
3779
|
}
|
|
5928
|
-
|
|
3780
|
+
lines.push(" });");
|
|
5929
3781
|
lines.push("}");
|
|
5930
3782
|
return { draftDsl: lines.join("\n") + "\n", warnings };
|
|
5931
3783
|
}
|
|
5932
3784
|
};
|
|
5933
|
-
function renderHeader(session) {
|
|
5934
|
-
const title = session.title ?? session.scenarioName;
|
|
5935
|
-
return [
|
|
5936
|
-
`// id-${session.scenarioName}`,
|
|
5937
|
-
`// ${title}`,
|
|
5938
|
-
`// #4287f5`
|
|
5939
|
-
].join("\n");
|
|
5940
|
-
}
|
|
5941
|
-
__name(renderHeader, "renderHeader");
|
|
5942
3785
|
function renderEntry(entry) {
|
|
5943
3786
|
const sel = /* @__PURE__ */ __name((s) => {
|
|
5944
3787
|
const out = formatSelector(s);
|
|
@@ -6199,7 +4042,7 @@ async function buildApp(opts = {}) {
|
|
|
6199
4042
|
await Promise.allSettled(slots.map((slot) => active.testRuntime.driver.shutdown(slot)));
|
|
6200
4043
|
}, "onAbort")
|
|
6201
4044
|
});
|
|
6202
|
-
const scenariosDirAbs = resolve3(opts.scenariosDir ??
|
|
4045
|
+
const scenariosDirAbs = resolve3(opts.scenariosDir ?? MOBILE_SCENARIOS_DIR);
|
|
6203
4046
|
const scenarioRepository = opts.scenarioRepository ?? new FileSystemScenarioRepository({ scenariosDir: scenariosDirAbs });
|
|
6204
4047
|
const helperRepository = opts.helperRepository ?? new FileSystemHelperRepository({ scenariosDir: scenariosDirAbs });
|
|
6205
4048
|
const scenarioLoader = new ScenarioLoader(scenarioRepository, helperRepository);
|
|
@@ -7030,37 +4873,7 @@ var AppInstallTool = class extends BaseTool {
|
|
|
7030
4873
|
import { z as z5 } from "zod";
|
|
7031
4874
|
|
|
7032
4875
|
// src/dsl/variable-scope.ts
|
|
7033
|
-
|
|
7034
|
-
static {
|
|
7035
|
-
__name(this, "VariableScope");
|
|
7036
|
-
}
|
|
7037
|
-
stack = [];
|
|
7038
|
-
current = /* @__PURE__ */ new Map();
|
|
7039
|
-
push() {
|
|
7040
|
-
this.stack.push(new Map(this.current));
|
|
7041
|
-
}
|
|
7042
|
-
pop() {
|
|
7043
|
-
const popped = this.stack.pop();
|
|
7044
|
-
if (popped === void 0) throw new Error("VariableScope.pop(): underflow (more pops than pushes)");
|
|
7045
|
-
this.current = popped;
|
|
7046
|
-
}
|
|
7047
|
-
get(name) {
|
|
7048
|
-
return this.current.get(name);
|
|
7049
|
-
}
|
|
7050
|
-
has(name) {
|
|
7051
|
-
return this.current.has(name);
|
|
7052
|
-
}
|
|
7053
|
-
set(name, value) {
|
|
7054
|
-
this.current.set(name, value);
|
|
7055
|
-
}
|
|
7056
|
-
toObject() {
|
|
7057
|
-
return Object.fromEntries(this.current.entries());
|
|
7058
|
-
}
|
|
7059
|
-
/** Depth of the push-stack. 0 at construction; +1 per push, -1 per pop. */
|
|
7060
|
-
depth() {
|
|
7061
|
-
return this.stack.length;
|
|
7062
|
-
}
|
|
7063
|
-
};
|
|
4876
|
+
import { VariableScope } from "@unotest/dsl/executor";
|
|
7064
4877
|
|
|
7065
4878
|
// src/dsl/runtime.ts
|
|
7066
4879
|
var TestRuntime = class {
|
|
@@ -7180,7 +4993,7 @@ var RunTestTool = class extends BaseTool {
|
|
|
7180
4993
|
"run_test",
|
|
7181
4994
|
{
|
|
7182
4995
|
title: "Run a DSL scenario (with optional step/pause modes)",
|
|
7183
|
-
description: "Loads unotest/e2e/<name>.js, parses + lints, then runs the function 'test_<name>' through AstExecutor. mode='auto' runs to completion or failure; mode='step' pauses after every statement. pauseOnFailure (default true) keeps simulators and WDA sessions alive on error so they can be inspected.",
|
|
4996
|
+
description: "Loads unotest/e2e-mobile/<name>.js, parses + lints, then runs the function 'test_<name>' through AstExecutor. mode='auto' runs to completion or failure; mode='step' pauses after every statement. pauseOnFailure (default true) keeps simulators and WDA sessions alive on error so they can be inspected.",
|
|
7184
4997
|
inputSchema: {
|
|
7185
4998
|
name: z5.string(),
|
|
7186
4999
|
mode: z5.enum(["auto", "step"]).optional(),
|
|
@@ -7208,7 +5021,7 @@ var RunTestTool = class extends BaseTool {
|
|
|
7208
5021
|
if (!loaded2.userFunctionNames.has(testFn)) {
|
|
7209
5022
|
return this.failJson({
|
|
7210
5023
|
status: "entry_not_found",
|
|
7211
|
-
message: `Entry function "${testFn}" not found in ${name}.js. Convention: unotest/e2e/${name}.js \u2192 function ${testFn}().`
|
|
5024
|
+
message: `Entry function "${testFn}" not found in ${name}.js. Convention: unotest/e2e-mobile/${name}.js \u2192 function ${testFn}().`
|
|
7212
5025
|
});
|
|
7213
5026
|
}
|
|
7214
5027
|
const runId = `e2e-${randomUUID2().slice(0, 8)}`;
|
|
@@ -7347,19 +5160,404 @@ var ListRuntimesTool = class extends BaseTool {
|
|
|
7347
5160
|
}
|
|
7348
5161
|
};
|
|
7349
5162
|
|
|
7350
|
-
// src/mcp/tools/
|
|
5163
|
+
// src/mcp/tools/attach.tool.ts
|
|
5164
|
+
import { z as z6 } from "zod";
|
|
5165
|
+
|
|
5166
|
+
// src/mcp/attach-runtime.ts
|
|
5167
|
+
import {
|
|
5168
|
+
COMMANDS_FILE,
|
|
5169
|
+
HEARTBEAT_FILE,
|
|
5170
|
+
RUNTIME_FILE
|
|
5171
|
+
} from "@unotest/protocol";
|
|
5172
|
+
import { appendFile, mkdir, readFile as readFile2 } from "fs/promises";
|
|
5173
|
+
import { existsSync as existsSync6, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
|
|
5174
|
+
import { dirname as dirname4, join as join6 } from "path";
|
|
5175
|
+
var HEARTBEAT_STALE_MS = 3e4;
|
|
5176
|
+
function runDir(projectRoot, runId) {
|
|
5177
|
+
return join6(projectRoot, MOBILE_RUNS_DIR, runId);
|
|
5178
|
+
}
|
|
5179
|
+
__name(runDir, "runDir");
|
|
5180
|
+
async function readMobileRuntimeFile(projectRoot, runId) {
|
|
5181
|
+
try {
|
|
5182
|
+
return JSON.parse(
|
|
5183
|
+
await readFile2(join6(runDir(projectRoot, runId), RUNTIME_FILE), "utf8")
|
|
5184
|
+
);
|
|
5185
|
+
} catch {
|
|
5186
|
+
return null;
|
|
5187
|
+
}
|
|
5188
|
+
}
|
|
5189
|
+
__name(readMobileRuntimeFile, "readMobileRuntimeFile");
|
|
5190
|
+
async function findShareableMobileRun(projectRoot, nowFn = Date.now) {
|
|
5191
|
+
const root = join6(projectRoot, MOBILE_RUNS_DIR);
|
|
5192
|
+
if (!existsSync6(root)) return null;
|
|
5193
|
+
const now = nowFn();
|
|
5194
|
+
let best = null;
|
|
5195
|
+
for (const name of readdirSync3(root)) {
|
|
5196
|
+
const hb = join6(root, name, HEARTBEAT_FILE);
|
|
5197
|
+
if (!existsSync6(hb)) continue;
|
|
5198
|
+
let mtimeMs = 0;
|
|
5199
|
+
try {
|
|
5200
|
+
mtimeMs = statSync3(hb).mtimeMs;
|
|
5201
|
+
} catch {
|
|
5202
|
+
continue;
|
|
5203
|
+
}
|
|
5204
|
+
if (now - mtimeMs > HEARTBEAT_STALE_MS) continue;
|
|
5205
|
+
const snap = await readMobileRuntimeFile(projectRoot, name);
|
|
5206
|
+
if (!snap?.deviceSlot) continue;
|
|
5207
|
+
if (snap.state === "completed" || snap.state === "failed" || snap.state === "aborted") {
|
|
5208
|
+
continue;
|
|
5209
|
+
}
|
|
5210
|
+
if (!best || mtimeMs > best.mtimeMs) best = { runtimeId: name, snap, mtimeMs };
|
|
5211
|
+
}
|
|
5212
|
+
return best ? { runtimeId: best.runtimeId, snap: best.snap } : null;
|
|
5213
|
+
}
|
|
5214
|
+
__name(findShareableMobileRun, "findShareableMobileRun");
|
|
5215
|
+
async function appendDbgCommand(projectRoot, runId, cmd) {
|
|
5216
|
+
const path = join6(runDir(projectRoot, runId), COMMANDS_FILE);
|
|
5217
|
+
await mkdir(dirname4(path), { recursive: true });
|
|
5218
|
+
await appendFile(path, JSON.stringify(cmd) + "\n", "utf8");
|
|
5219
|
+
}
|
|
5220
|
+
__name(appendDbgCommand, "appendDbgCommand");
|
|
5221
|
+
function nextCommandId() {
|
|
5222
|
+
return Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 8);
|
|
5223
|
+
}
|
|
5224
|
+
__name(nextCommandId, "nextCommandId");
|
|
5225
|
+
var AttachState = class {
|
|
5226
|
+
static {
|
|
5227
|
+
__name(this, "AttachState");
|
|
5228
|
+
}
|
|
5229
|
+
runId = null;
|
|
5230
|
+
slot = null;
|
|
5231
|
+
attach(runId, slot) {
|
|
5232
|
+
this.runId = runId;
|
|
5233
|
+
this.slot = slot;
|
|
5234
|
+
}
|
|
5235
|
+
detach() {
|
|
5236
|
+
this.runId = null;
|
|
5237
|
+
this.slot = null;
|
|
5238
|
+
}
|
|
5239
|
+
attachedRunId() {
|
|
5240
|
+
return this.runId;
|
|
5241
|
+
}
|
|
5242
|
+
attachedSlot() {
|
|
5243
|
+
return this.slot;
|
|
5244
|
+
}
|
|
5245
|
+
};
|
|
5246
|
+
|
|
5247
|
+
// src/mcp/tools/attach.tool.ts
|
|
5248
|
+
var AttachBaseTool = class extends BaseTool {
|
|
5249
|
+
constructor(services, attach, projectRoot) {
|
|
5250
|
+
super(services);
|
|
5251
|
+
this.attach = attach;
|
|
5252
|
+
this.projectRoot = projectRoot;
|
|
5253
|
+
}
|
|
5254
|
+
attach;
|
|
5255
|
+
projectRoot;
|
|
5256
|
+
static {
|
|
5257
|
+
__name(this, "AttachBaseTool");
|
|
5258
|
+
}
|
|
5259
|
+
};
|
|
5260
|
+
var GetActiveContextTool = class extends AttachBaseTool {
|
|
5261
|
+
static {
|
|
5262
|
+
__name(this, "GetActiveContextTool");
|
|
5263
|
+
}
|
|
5264
|
+
register(server) {
|
|
5265
|
+
server.registerTool(
|
|
5266
|
+
"get_active_context",
|
|
5267
|
+
{
|
|
5268
|
+
title: "Active mobile context",
|
|
5269
|
+
description: "Reports whether you are attached to a live mobile debug session and, if not, whether one is JOINABLE. When a human is debugging a scenario in the viewer (Debug mode), `attachable` names its runtimeId + device slot + state \u2014 call attach_debug_session to co-drive the SAME device, then a11y_tree / screenshot that slot. Returns attachable=null when no live debug session exists (start your own with run_test).",
|
|
5270
|
+
inputSchema: {}
|
|
5271
|
+
},
|
|
5272
|
+
async () => this.tracked("get_active_context", {}, async () => {
|
|
5273
|
+
const attachedRunId = this.attach.attachedRunId();
|
|
5274
|
+
const found = await findShareableMobileRun(this.projectRoot);
|
|
5275
|
+
return this.okJson({
|
|
5276
|
+
attached: attachedRunId ? { runtimeId: attachedRunId, slot: this.attach.attachedSlot() } : null,
|
|
5277
|
+
attachable: found ? {
|
|
5278
|
+
runtimeId: found.runtimeId,
|
|
5279
|
+
slot: found.snap.deviceSlot ?? null,
|
|
5280
|
+
state: found.snap.state,
|
|
5281
|
+
wheel: found.snap.wheel ?? "human"
|
|
5282
|
+
} : null
|
|
5283
|
+
});
|
|
5284
|
+
})
|
|
5285
|
+
);
|
|
5286
|
+
}
|
|
5287
|
+
};
|
|
5288
|
+
var AttachDebugSessionTool = class extends AttachBaseTool {
|
|
5289
|
+
static {
|
|
5290
|
+
__name(this, "AttachDebugSessionTool");
|
|
5291
|
+
}
|
|
5292
|
+
register(server) {
|
|
5293
|
+
server.registerTool(
|
|
5294
|
+
"attach_debug_session",
|
|
5295
|
+
{
|
|
5296
|
+
title: "Attach to a mobile debug session",
|
|
5297
|
+
description: "Join a human's live mobile debug run so you BOTH drive the SAME device (collaborative debugging). Omit runtimeId to auto-pick the single live debug session. Afterwards a11y_tree / screenshot / find_element on the run's device slot show what the human is debugging; take_wheel before you act. The run must be started in Debug mode (the viewer's Debug button) \u2014 a normal run advertises no device slot. Call detach_debug_session to disengage.",
|
|
5298
|
+
inputSchema: { runtimeId: z6.string().optional() }
|
|
5299
|
+
},
|
|
5300
|
+
async ({ runtimeId }) => this.tracked("attach_debug_session", { runtimeId }, async () => {
|
|
5301
|
+
let id = runtimeId;
|
|
5302
|
+
let snap = id ? await readMobileRuntimeFile(this.projectRoot, id) : null;
|
|
5303
|
+
if (!id) {
|
|
5304
|
+
const found = await findShareableMobileRun(this.projectRoot);
|
|
5305
|
+
if (!found) {
|
|
5306
|
+
return this.okJson({
|
|
5307
|
+
status: "no_session",
|
|
5308
|
+
reason: "no live mobile debug session to attach to \u2014 start one from the viewer's Debug button, or run_test here."
|
|
5309
|
+
});
|
|
5310
|
+
}
|
|
5311
|
+
id = found.runtimeId;
|
|
5312
|
+
snap = found.snap;
|
|
5313
|
+
}
|
|
5314
|
+
if (!snap) {
|
|
5315
|
+
return this.okJson({
|
|
5316
|
+
status: "no_session",
|
|
5317
|
+
reason: `no runtime.json for "${id}" \u2014 the run may have ended.`
|
|
5318
|
+
});
|
|
5319
|
+
}
|
|
5320
|
+
if (!snap.deviceSlot) {
|
|
5321
|
+
return this.okJson({
|
|
5322
|
+
status: "not_attachable",
|
|
5323
|
+
runtimeId: id,
|
|
5324
|
+
reason: "this run advertises no device slot \u2014 only Debug-mode runs are attachable."
|
|
5325
|
+
});
|
|
5326
|
+
}
|
|
5327
|
+
this.attach.attach(id, snap.deviceSlot);
|
|
5328
|
+
return this.okJson({
|
|
5329
|
+
status: "attached",
|
|
5330
|
+
runtimeId: id,
|
|
5331
|
+
slot: snap.deviceSlot,
|
|
5332
|
+
state: snap.state,
|
|
5333
|
+
wheel: snap.wheel ?? "human"
|
|
5334
|
+
});
|
|
5335
|
+
})
|
|
5336
|
+
);
|
|
5337
|
+
}
|
|
5338
|
+
};
|
|
5339
|
+
var DetachDebugSessionTool = class extends AttachBaseTool {
|
|
5340
|
+
static {
|
|
5341
|
+
__name(this, "DetachDebugSessionTool");
|
|
5342
|
+
}
|
|
5343
|
+
register(server) {
|
|
5344
|
+
server.registerTool(
|
|
5345
|
+
"detach_debug_session",
|
|
5346
|
+
{
|
|
5347
|
+
title: "Detach from the mobile debug session",
|
|
5348
|
+
description: "Disengage from the live debug session you joined with attach_debug_session. Does not stop the human's run \u2014 only clears your attachment so the device tools no longer default to its slot.",
|
|
5349
|
+
inputSchema: {}
|
|
5350
|
+
},
|
|
5351
|
+
async () => this.tracked("detach_debug_session", {}, async () => {
|
|
5352
|
+
const was = this.attach.attachedRunId();
|
|
5353
|
+
this.attach.detach();
|
|
5354
|
+
return this.okJson({ status: "detached", wasAttachedTo: was });
|
|
5355
|
+
})
|
|
5356
|
+
);
|
|
5357
|
+
}
|
|
5358
|
+
};
|
|
5359
|
+
var WheelTool = class extends AttachBaseTool {
|
|
5360
|
+
constructor(services, attach, projectRoot, owner) {
|
|
5361
|
+
super(services, attach, projectRoot);
|
|
5362
|
+
this.owner = owner;
|
|
5363
|
+
}
|
|
5364
|
+
owner;
|
|
5365
|
+
static {
|
|
5366
|
+
__name(this, "WheelTool");
|
|
5367
|
+
}
|
|
5368
|
+
register(server) {
|
|
5369
|
+
const name = this.owner === "agent" ? "take_wheel" : "release_wheel";
|
|
5370
|
+
server.registerTool(
|
|
5371
|
+
name,
|
|
5372
|
+
{
|
|
5373
|
+
title: name === "take_wheel" ? "Take the wheel" : "Release the wheel",
|
|
5374
|
+
description: this.owner === "agent" ? "Take the collaborative control token so you may act on the human's debug device. The viewer shows an 'Agent driving' badge and the human can take it back any time. Omit runtimeId to use the session you attached to." : "Release the control token back to the human (the host). Omit runtimeId to use the session you attached to.",
|
|
5375
|
+
inputSchema: { runtimeId: z6.string().optional() }
|
|
5376
|
+
},
|
|
5377
|
+
async ({ runtimeId }) => this.tracked(name, { runtimeId }, async () => {
|
|
5378
|
+
const id = runtimeId ?? this.attach.attachedRunId();
|
|
5379
|
+
if (!id) {
|
|
5380
|
+
return this.okJson({
|
|
5381
|
+
status: "no_target",
|
|
5382
|
+
reason: "not attached \u2014 call attach_debug_session first, or pass runtimeId."
|
|
5383
|
+
});
|
|
5384
|
+
}
|
|
5385
|
+
await appendDbgCommand(this.projectRoot, id, {
|
|
5386
|
+
id: nextCommandId(),
|
|
5387
|
+
cmd: "wheel",
|
|
5388
|
+
owner: this.owner
|
|
5389
|
+
});
|
|
5390
|
+
return this.okJson({ status: "ok", runtimeId: id, owner: this.owner });
|
|
5391
|
+
})
|
|
5392
|
+
);
|
|
5393
|
+
}
|
|
5394
|
+
};
|
|
5395
|
+
|
|
5396
|
+
// src/mcp/tools/failure.tool.ts
|
|
7351
5397
|
import { z as z7 } from "zod";
|
|
7352
5398
|
|
|
5399
|
+
// src/mcp/failure-bundle-reader.ts
|
|
5400
|
+
import {
|
|
5401
|
+
PICKER_SNAPSHOT_FILE,
|
|
5402
|
+
RUN_MANIFEST_FILE
|
|
5403
|
+
} from "@unotest/protocol";
|
|
5404
|
+
import { existsSync as existsSync7, readdirSync as readdirSync4 } from "fs";
|
|
5405
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
5406
|
+
import { join as join7 } from "path";
|
|
5407
|
+
var FAILURE_SCREENSHOT_FILE = "screenshot.png";
|
|
5408
|
+
function runDir2(projectRoot, runId) {
|
|
5409
|
+
return join7(projectRoot, MOBILE_RUNS_DIR, runId);
|
|
5410
|
+
}
|
|
5411
|
+
__name(runDir2, "runDir");
|
|
5412
|
+
async function readManifest(projectRoot, runId) {
|
|
5413
|
+
try {
|
|
5414
|
+
return JSON.parse(
|
|
5415
|
+
await readFile3(join7(runDir2(projectRoot, runId), RUN_MANIFEST_FILE), "utf8")
|
|
5416
|
+
);
|
|
5417
|
+
} catch {
|
|
5418
|
+
return null;
|
|
5419
|
+
}
|
|
5420
|
+
}
|
|
5421
|
+
__name(readManifest, "readManifest");
|
|
5422
|
+
async function listMobileFailures(projectRoot) {
|
|
5423
|
+
const root = join7(projectRoot, MOBILE_RUNS_DIR);
|
|
5424
|
+
if (!existsSync7(root)) return [];
|
|
5425
|
+
const out = [];
|
|
5426
|
+
for (const runId of readdirSync4(root)) {
|
|
5427
|
+
const snap = await readMobileRuntimeFile(projectRoot, runId);
|
|
5428
|
+
if (!snap) continue;
|
|
5429
|
+
const failed = snap.state === "failed" || snap.state === "paused-failure" || snap.lastFailure != null;
|
|
5430
|
+
if (!failed) continue;
|
|
5431
|
+
const manifest = await readManifest(projectRoot, runId);
|
|
5432
|
+
out.push({
|
|
5433
|
+
runId,
|
|
5434
|
+
scenario: manifest?.ref ?? snap.scenario ?? runId,
|
|
5435
|
+
state: snap.state,
|
|
5436
|
+
error: snap.lastFailure?.error ?? null,
|
|
5437
|
+
location: snap.lastFailure ? { line: snap.lastFailure.line, col: snap.lastFailure.col } : null,
|
|
5438
|
+
hasScreenshot: existsSync7(join7(runDir2(projectRoot, runId), FAILURE_SCREENSHOT_FILE)),
|
|
5439
|
+
hasOutline: existsSync7(join7(runDir2(projectRoot, runId), PICKER_SNAPSHOT_FILE))
|
|
5440
|
+
});
|
|
5441
|
+
}
|
|
5442
|
+
return out;
|
|
5443
|
+
}
|
|
5444
|
+
__name(listMobileFailures, "listMobileFailures");
|
|
5445
|
+
async function readFailureScreenshot(projectRoot, runId) {
|
|
5446
|
+
const path = join7(runDir2(projectRoot, runId), FAILURE_SCREENSHOT_FILE);
|
|
5447
|
+
if (!existsSync7(path)) return null;
|
|
5448
|
+
try {
|
|
5449
|
+
return await readFile3(path);
|
|
5450
|
+
} catch {
|
|
5451
|
+
return null;
|
|
5452
|
+
}
|
|
5453
|
+
}
|
|
5454
|
+
__name(readFailureScreenshot, "readFailureScreenshot");
|
|
5455
|
+
async function readFailureOutline(projectRoot, runId) {
|
|
5456
|
+
const path = join7(runDir2(projectRoot, runId), PICKER_SNAPSHOT_FILE);
|
|
5457
|
+
if (!existsSync7(path)) return null;
|
|
5458
|
+
try {
|
|
5459
|
+
return await readFile3(path, "utf8");
|
|
5460
|
+
} catch {
|
|
5461
|
+
return null;
|
|
5462
|
+
}
|
|
5463
|
+
}
|
|
5464
|
+
__name(readFailureOutline, "readFailureOutline");
|
|
5465
|
+
|
|
5466
|
+
// src/mcp/tools/failure.tool.ts
|
|
5467
|
+
var FailureBaseTool = class extends BaseTool {
|
|
5468
|
+
constructor(services, projectRoot) {
|
|
5469
|
+
super(services);
|
|
5470
|
+
this.projectRoot = projectRoot;
|
|
5471
|
+
}
|
|
5472
|
+
projectRoot;
|
|
5473
|
+
static {
|
|
5474
|
+
__name(this, "FailureBaseTool");
|
|
5475
|
+
}
|
|
5476
|
+
};
|
|
5477
|
+
var ListFailuresTool = class extends FailureBaseTool {
|
|
5478
|
+
static {
|
|
5479
|
+
__name(this, "ListFailuresTool");
|
|
5480
|
+
}
|
|
5481
|
+
register(server) {
|
|
5482
|
+
server.registerTool(
|
|
5483
|
+
"list_failures",
|
|
5484
|
+
{
|
|
5485
|
+
title: "List failed runs",
|
|
5486
|
+
description: "Lists recent runs that ended in (or are paused at) a failure, with the scenario name, error message, and failing line:col. Each entry says whether a failure screenshot and an a11y outline are available \u2014 fetch them with get_failure_screenshot / get_failure_a11y by runtimeId.",
|
|
5487
|
+
inputSchema: {}
|
|
5488
|
+
},
|
|
5489
|
+
async () => this.tracked("list_failures", {}, async () => {
|
|
5490
|
+
return this.okJson({ failures: await listMobileFailures(this.projectRoot) });
|
|
5491
|
+
})
|
|
5492
|
+
);
|
|
5493
|
+
}
|
|
5494
|
+
};
|
|
5495
|
+
var GetFailureScreenshotTool = class extends FailureBaseTool {
|
|
5496
|
+
static {
|
|
5497
|
+
__name(this, "GetFailureScreenshotTool");
|
|
5498
|
+
}
|
|
5499
|
+
register(server) {
|
|
5500
|
+
server.registerTool(
|
|
5501
|
+
"get_failure_screenshot",
|
|
5502
|
+
{
|
|
5503
|
+
title: "Failure screenshot",
|
|
5504
|
+
description: "Returns the device screenshot captured at the failure point of a run (as inline MCP image content). Pass the runtimeId from list_failures. Returns a not_found status when the run has no failure screenshot.",
|
|
5505
|
+
inputSchema: { runtimeId: z7.string() }
|
|
5506
|
+
},
|
|
5507
|
+
async ({ runtimeId }) => this.tracked("get_failure_screenshot", { runtimeId }, async () => {
|
|
5508
|
+
const png = await readFailureScreenshot(this.projectRoot, runtimeId);
|
|
5509
|
+
if (!png) {
|
|
5510
|
+
return this.okJson({ status: "not_found", runtimeId });
|
|
5511
|
+
}
|
|
5512
|
+
return {
|
|
5513
|
+
content: [
|
|
5514
|
+
{
|
|
5515
|
+
type: "image",
|
|
5516
|
+
data: png.toString("base64"),
|
|
5517
|
+
mimeType: "image/png"
|
|
5518
|
+
}
|
|
5519
|
+
]
|
|
5520
|
+
};
|
|
5521
|
+
})
|
|
5522
|
+
);
|
|
5523
|
+
}
|
|
5524
|
+
};
|
|
5525
|
+
var GetFailureA11yTool = class extends FailureBaseTool {
|
|
5526
|
+
static {
|
|
5527
|
+
__name(this, "GetFailureA11yTool");
|
|
5528
|
+
}
|
|
5529
|
+
register(server) {
|
|
5530
|
+
server.registerTool(
|
|
5531
|
+
"get_failure_a11y",
|
|
5532
|
+
{
|
|
5533
|
+
title: "Failure a11y outline",
|
|
5534
|
+
description: "Returns the accessibility outline of the device at the failure/pause point of a run (the same compact outline as a11y_tree). Pass the runtimeId from list_failures. Returns a not_found status when no outline was captured.",
|
|
5535
|
+
inputSchema: { runtimeId: z7.string() }
|
|
5536
|
+
},
|
|
5537
|
+
async ({ runtimeId }) => this.tracked("get_failure_a11y", { runtimeId }, async () => {
|
|
5538
|
+
const outline = await readFailureOutline(this.projectRoot, runtimeId);
|
|
5539
|
+
if (outline === null) {
|
|
5540
|
+
return this.okJson({ status: "not_found", runtimeId });
|
|
5541
|
+
}
|
|
5542
|
+
return this.ok(outline);
|
|
5543
|
+
})
|
|
5544
|
+
);
|
|
5545
|
+
}
|
|
5546
|
+
};
|
|
5547
|
+
|
|
5548
|
+
// src/mcp/tools/explore.tool.ts
|
|
5549
|
+
import { z as z9 } from "zod";
|
|
5550
|
+
|
|
7353
5551
|
// src/mcp/tools/selector-param.ts
|
|
7354
|
-
import { z as
|
|
5552
|
+
import { z as z8 } from "zod";
|
|
7355
5553
|
var SelectorShape = {
|
|
7356
|
-
testId:
|
|
7357
|
-
text:
|
|
7358
|
-
label:
|
|
7359
|
-
ordinal:
|
|
7360
|
-
pointPercent:
|
|
7361
|
-
x:
|
|
7362
|
-
y:
|
|
5554
|
+
testId: z8.string().optional(),
|
|
5555
|
+
text: z8.string().optional(),
|
|
5556
|
+
label: z8.string().optional(),
|
|
5557
|
+
ordinal: z8.number().int().nonnegative().optional(),
|
|
5558
|
+
pointPercent: z8.object({
|
|
5559
|
+
x: z8.number().min(0).max(1),
|
|
5560
|
+
y: z8.number().min(0).max(1)
|
|
7363
5561
|
}).optional()
|
|
7364
5562
|
};
|
|
7365
5563
|
function coerceJsonObject(v) {
|
|
@@ -7371,14 +5569,14 @@ function coerceJsonObject(v) {
|
|
|
7371
5569
|
}
|
|
7372
5570
|
}
|
|
7373
5571
|
__name(coerceJsonObject, "coerceJsonObject");
|
|
7374
|
-
var SelectorParam =
|
|
5572
|
+
var SelectorParam = z8.preprocess(
|
|
7375
5573
|
coerceJsonObject,
|
|
7376
|
-
|
|
5574
|
+
z8.object(SelectorShape).refine(
|
|
7377
5575
|
(s) => Boolean(s.testId || s.text || s.label || s.pointPercent),
|
|
7378
5576
|
{ message: "selector requires one of testId/text/label/pointPercent" }
|
|
7379
5577
|
)
|
|
7380
5578
|
);
|
|
7381
|
-
var SelectorParamOptional =
|
|
5579
|
+
var SelectorParamOptional = z8.preprocess(coerceJsonObject, z8.object(SelectorShape).optional());
|
|
7382
5580
|
|
|
7383
5581
|
// src/mcp/tools/explore.tool.ts
|
|
7384
5582
|
var ACTION_ENUM = [
|
|
@@ -7395,22 +5593,22 @@ var ACTION_ENUM = [
|
|
|
7395
5593
|
var KEY_ENUM = ["back", "home", "enter", "escape"];
|
|
7396
5594
|
var DIRECTION_ENUM = ["up", "down", "left", "right"];
|
|
7397
5595
|
var stepShape = {
|
|
7398
|
-
explorationId:
|
|
7399
|
-
action:
|
|
7400
|
-
device:
|
|
5596
|
+
explorationId: z9.string().optional(),
|
|
5597
|
+
action: z9.enum(ACTION_ENUM),
|
|
5598
|
+
device: z9.string().optional(),
|
|
7401
5599
|
selector: SelectorParamOptional,
|
|
7402
|
-
value:
|
|
7403
|
-
key:
|
|
7404
|
-
direction:
|
|
5600
|
+
value: z9.string().optional(),
|
|
5601
|
+
key: z9.enum(KEY_ENUM).optional(),
|
|
5602
|
+
direction: z9.enum(DIRECTION_ENUM).optional(),
|
|
7405
5603
|
from: SelectorParamOptional,
|
|
7406
|
-
url:
|
|
7407
|
-
bundleId:
|
|
7408
|
-
clean:
|
|
7409
|
-
timeoutMs:
|
|
7410
|
-
optional:
|
|
7411
|
-
button:
|
|
7412
|
-
description:
|
|
7413
|
-
section:
|
|
5604
|
+
url: z9.string().optional(),
|
|
5605
|
+
bundleId: z9.string().optional(),
|
|
5606
|
+
clean: z9.boolean().optional(),
|
|
5607
|
+
timeoutMs: z9.number().int().positive().optional(),
|
|
5608
|
+
optional: z9.boolean().optional(),
|
|
5609
|
+
button: z9.string().optional(),
|
|
5610
|
+
description: z9.string().optional(),
|
|
5611
|
+
section: z9.string().optional()
|
|
7414
5612
|
};
|
|
7415
5613
|
function validateRecordingArgs(args) {
|
|
7416
5614
|
if (!args.section) return "section required when recording";
|
|
@@ -7497,10 +5695,10 @@ var ExploreStartTool = class extends BaseTool {
|
|
|
7497
5695
|
title: "Start a recording session",
|
|
7498
5696
|
description: "Begins an ExplorationSession. Returns { explorationId }. Subsequent explore_step calls that include this id will be recorded into an ActionLog; calls without it are ad-hoc. Device slot ('A' or 'B') is fixed for the session. No auto-launch: the first explore_step must do the app_launch / open_deeplink so it appears in the generated test.",
|
|
7499
5697
|
inputSchema: {
|
|
7500
|
-
scenario_name:
|
|
7501
|
-
device:
|
|
7502
|
-
title:
|
|
7503
|
-
description:
|
|
5698
|
+
scenario_name: z9.string().min(1).describe("Logical name; later used by save_exploration_as_test as the test file name."),
|
|
5699
|
+
device: z9.string().min(1).describe("Slot from SIM_POOL, e.g. 'A'."),
|
|
5700
|
+
title: z9.string().optional(),
|
|
5701
|
+
description: z9.string().optional()
|
|
7504
5702
|
}
|
|
7505
5703
|
},
|
|
7506
5704
|
async ({ scenario_name, device, title, description }) => this.tracked(
|
|
@@ -7536,7 +5734,7 @@ var ExploreStopTool = class extends BaseTool {
|
|
|
7536
5734
|
title: "Stop a recording session",
|
|
7537
5735
|
description: "Marks the session as stopped. The device stays running (no teardown). After stop, generate_dsl_from_exploration / save_exploration_as_test remain callable; further explore_step calls with this id will fail.",
|
|
7538
5736
|
inputSchema: {
|
|
7539
|
-
explorationId:
|
|
5737
|
+
explorationId: z9.string().min(1)
|
|
7540
5738
|
}
|
|
7541
5739
|
},
|
|
7542
5740
|
async ({ explorationId }) => this.tracked("explore_stop", { explorationId }, async () => {
|
|
@@ -7563,7 +5761,7 @@ var ExploreStateTool = class extends BaseTool {
|
|
|
7563
5761
|
title: "Inspect the recording session log",
|
|
7564
5762
|
description: "Returns the session status and all recorded ActionEntries. Each entry carries its per-entry `stability` annotation (stable | fragile). The full warnings list (FRAGILE_LOCATOR / NO_DSL_PRIMITIVE / BUNDLE_ID_IGNORED) is NOT returned here \u2014 it is derived on demand by generate_dsl_from_exploration.",
|
|
7565
5763
|
inputSchema: {
|
|
7566
|
-
explorationId:
|
|
5764
|
+
explorationId: z9.string().min(1)
|
|
7567
5765
|
}
|
|
7568
5766
|
},
|
|
7569
5767
|
async ({ explorationId }) => this.tracked("explore_state", { explorationId }, async () => {
|
|
@@ -7707,8 +5905,8 @@ var ExploreRemoveStepTool = class extends BaseTool {
|
|
|
7707
5905
|
title: "Remove an entry from the recording session log",
|
|
7708
5906
|
description: "Deletes the entry by entryId. Returns { removed: true } or { removed: false } if not found.",
|
|
7709
5907
|
inputSchema: {
|
|
7710
|
-
explorationId:
|
|
7711
|
-
entryId:
|
|
5908
|
+
explorationId: z9.string().min(1),
|
|
5909
|
+
entryId: z9.string().min(1)
|
|
7712
5910
|
}
|
|
7713
5911
|
},
|
|
7714
5912
|
async ({ explorationId, entryId }) => this.tracked("explore_remove_step", { explorationId, entryId }, async () => {
|
|
@@ -7729,9 +5927,9 @@ var GenerateDslFromExplorationTool = class extends BaseTool {
|
|
|
7729
5927
|
"generate_dsl_from_exploration",
|
|
7730
5928
|
{
|
|
7731
5929
|
title: "Render the session log as a DSL test",
|
|
7732
|
-
description:
|
|
5930
|
+
description: 'Returns { draftDsl, warnings } from the current ActionLog. Pure function \u2014 does not mutate the log. Warnings: FRAGILE_LOCATOR (selector lacks stable identifier), NO_DSL_PRIMITIVE (entry skipped \u2014 selector shape has no DSL function), BUNDLE_ID_IGNORED (app_launch with explicit bundleId \u2014 DSL appLaunch reads it from APP_BUNDLE_ID env). Adjacent same-section entries group into one step("section", () => { ... }) block - the mandatory test_* body structure.',
|
|
7733
5931
|
inputSchema: {
|
|
7734
|
-
explorationId:
|
|
5932
|
+
explorationId: z9.string().min(1)
|
|
7735
5933
|
}
|
|
7736
5934
|
},
|
|
7737
5935
|
async ({ explorationId }) => this.tracked("generate_dsl_from_exploration", { explorationId }, async () => {
|
|
@@ -7753,12 +5951,12 @@ var SaveExplorationAsTestTool = class extends BaseTool {
|
|
|
7753
5951
|
"save_exploration_as_test",
|
|
7754
5952
|
{
|
|
7755
5953
|
title: "Persist the generated DSL as a runnable scenario",
|
|
7756
|
-
description: "Renders the ActionLog as DSL and writes it to unotest/e2e/<scenarioName>.js. Returns { path, actionCount, warnings }. Policy: by default, NO_DSL_PRIMITIVE warnings block the save \u2014 pass force: true to write anyway with `// SKIPPED \u2026` comments inline. If the file already exists, pass overwrite: true to replace it.",
|
|
5954
|
+
description: "Renders the ActionLog as DSL and writes it to unotest/e2e-mobile/<scenarioName>.js. Returns { path, actionCount, warnings }. Policy: by default, NO_DSL_PRIMITIVE warnings block the save \u2014 pass force: true to write anyway with `// SKIPPED \u2026` comments inline. If the file already exists, pass overwrite: true to replace it.",
|
|
7757
5955
|
inputSchema: {
|
|
7758
|
-
explorationId:
|
|
7759
|
-
scenarioName:
|
|
7760
|
-
overwrite:
|
|
7761
|
-
force:
|
|
5956
|
+
explorationId: z9.string().min(1),
|
|
5957
|
+
scenarioName: z9.string().min(1),
|
|
5958
|
+
overwrite: z9.boolean().optional(),
|
|
5959
|
+
force: z9.boolean().optional()
|
|
7762
5960
|
}
|
|
7763
5961
|
},
|
|
7764
5962
|
async ({ explorationId, scenarioName, overwrite, force }) => this.tracked(
|
|
@@ -7847,6 +6045,17 @@ function registerAllTools(server, services) {
|
|
|
7847
6045
|
new InspectRuntimeTool(dbgServices).register(server);
|
|
7848
6046
|
new AbortRuntimeTool(dbgServices).register(server);
|
|
7849
6047
|
new ListRuntimesTool(dbgServices).register(server);
|
|
6048
|
+
const attachState = new AttachState();
|
|
6049
|
+
const attachSlice = { sessionRecorder: services.sessionRecorder };
|
|
6050
|
+
const projectRoot = process.cwd();
|
|
6051
|
+
new GetActiveContextTool(attachSlice, attachState, projectRoot).register(server);
|
|
6052
|
+
new AttachDebugSessionTool(attachSlice, attachState, projectRoot).register(server);
|
|
6053
|
+
new DetachDebugSessionTool(attachSlice, attachState, projectRoot).register(server);
|
|
6054
|
+
new WheelTool(attachSlice, attachState, projectRoot, "agent").register(server);
|
|
6055
|
+
new WheelTool(attachSlice, attachState, projectRoot, "human").register(server);
|
|
6056
|
+
new ListFailuresTool(attachSlice, projectRoot).register(server);
|
|
6057
|
+
new GetFailureScreenshotTool(attachSlice, projectRoot).register(server);
|
|
6058
|
+
new GetFailureA11yTool(attachSlice, projectRoot).register(server);
|
|
7850
6059
|
new AppInstallTool({
|
|
7851
6060
|
envConfig: services.envConfig,
|
|
7852
6061
|
logger: services.logger,
|