@testmuai/playwright-bindings 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +51 -0
  3. package/dist/capability.d.ts +60 -0
  4. package/dist/capability.d.ts.map +1 -0
  5. package/dist/capability.js +212 -0
  6. package/dist/capability.js.map +1 -0
  7. package/dist/config.d.ts +12 -0
  8. package/dist/config.d.ts.map +1 -0
  9. package/dist/config.js +16 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/configure.d.ts +30 -0
  12. package/dist/configure.d.ts.map +1 -0
  13. package/dist/configure.js +53 -0
  14. package/dist/configure.js.map +1 -0
  15. package/dist/errors.d.ts +4 -0
  16. package/dist/errors.d.ts.map +1 -0
  17. package/dist/errors.js +7 -0
  18. package/dist/errors.js.map +1 -0
  19. package/dist/healPatch.d.ts +11 -0
  20. package/dist/healPatch.d.ts.map +1 -0
  21. package/dist/healPatch.js +82 -0
  22. package/dist/healPatch.js.map +1 -0
  23. package/dist/helpers/assertion.d.ts +23 -0
  24. package/dist/helpers/assertion.d.ts.map +1 -0
  25. package/dist/helpers/assertion.js +87 -0
  26. package/dist/helpers/assertion.js.map +1 -0
  27. package/dist/helpers/drag.d.ts +3 -0
  28. package/dist/helpers/drag.d.ts.map +1 -0
  29. package/dist/helpers/drag.js +7 -0
  30. package/dist/helpers/drag.js.map +1 -0
  31. package/dist/helpers/executeApi.d.ts +34 -0
  32. package/dist/helpers/executeApi.d.ts.map +1 -0
  33. package/dist/helpers/executeApi.js +83 -0
  34. package/dist/helpers/executeApi.js.map +1 -0
  35. package/dist/helpers/executeDb.d.ts +9 -0
  36. package/dist/helpers/executeDb.d.ts.map +1 -0
  37. package/dist/helpers/executeDb.js +23 -0
  38. package/dist/helpers/executeDb.js.map +1 -0
  39. package/dist/helpers/executeJs.d.ts +16 -0
  40. package/dist/helpers/executeJs.d.ts.map +1 -0
  41. package/dist/helpers/executeJs.js +58 -0
  42. package/dist/helpers/executeJs.js.map +1 -0
  43. package/dist/helpers/kaneCli.d.ts +2 -0
  44. package/dist/helpers/kaneCli.d.ts.map +1 -0
  45. package/dist/helpers/kaneCli.js +5 -0
  46. package/dist/helpers/kaneCli.js.map +1 -0
  47. package/dist/helpers/math.d.ts +6 -0
  48. package/dist/helpers/math.d.ts.map +1 -0
  49. package/dist/helpers/math.js +70 -0
  50. package/dist/helpers/math.js.map +1 -0
  51. package/dist/helpers/network.d.ts +23 -0
  52. package/dist/helpers/network.d.ts.map +1 -0
  53. package/dist/helpers/network.js +67 -0
  54. package/dist/helpers/network.js.map +1 -0
  55. package/dist/helpers/smartui.d.ts +3 -0
  56. package/dist/helpers/smartui.d.ts.map +1 -0
  57. package/dist/helpers/smartui.js +11 -0
  58. package/dist/helpers/smartui.js.map +1 -0
  59. package/dist/helpers/tabs.d.ts +3 -0
  60. package/dist/helpers/tabs.d.ts.map +1 -0
  61. package/dist/helpers/tabs.js +13 -0
  62. package/dist/helpers/tabs.js.map +1 -0
  63. package/dist/helpers/vision.d.ts +48 -0
  64. package/dist/helpers/vision.d.ts.map +1 -0
  65. package/dist/helpers/vision.js +169 -0
  66. package/dist/helpers/vision.js.map +1 -0
  67. package/dist/helpers/wait.d.ts +3 -0
  68. package/dist/helpers/wait.d.ts.map +1 -0
  69. package/dist/helpers/wait.js +21 -0
  70. package/dist/helpers/wait.js.map +1 -0
  71. package/dist/http.d.ts +12 -0
  72. package/dist/http.d.ts.map +1 -0
  73. package/dist/http.js +29 -0
  74. package/dist/http.js.map +1 -0
  75. package/dist/index.d.ts +56 -0
  76. package/dist/index.d.ts.map +1 -0
  77. package/dist/index.js +34 -0
  78. package/dist/index.js.map +1 -0
  79. package/dist/log.d.ts +22 -0
  80. package/dist/log.d.ts.map +1 -0
  81. package/dist/log.js +43 -0
  82. package/dist/log.js.map +1 -0
  83. package/dist/reporters/index.d.ts +10 -0
  84. package/dist/reporters/index.d.ts.map +1 -0
  85. package/dist/reporters/index.js +7 -0
  86. package/dist/reporters/index.js.map +1 -0
  87. package/dist/reporters/local.d.ts +11 -0
  88. package/dist/reporters/local.d.ts.map +1 -0
  89. package/dist/reporters/local.js +29 -0
  90. package/dist/reporters/local.js.map +1 -0
  91. package/dist/reporters/lt.d.ts +15 -0
  92. package/dist/reporters/lt.d.ts.map +1 -0
  93. package/dist/reporters/lt.js +48 -0
  94. package/dist/reporters/lt.js.map +1 -0
  95. package/dist/reporters/null.d.ts +10 -0
  96. package/dist/reporters/null.d.ts.map +1 -0
  97. package/dist/reporters/null.js +9 -0
  98. package/dist/reporters/null.js.map +1 -0
  99. package/dist/session.d.ts +2 -0
  100. package/dist/session.d.ts.map +1 -0
  101. package/dist/session.js +61 -0
  102. package/dist/session.js.map +1 -0
  103. package/dist/step.d.ts +14 -0
  104. package/dist/step.d.ts.map +1 -0
  105. package/dist/step.js +36 -0
  106. package/dist/step.js.map +1 -0
  107. package/dist/test.d.ts +10 -0
  108. package/dist/test.d.ts.map +1 -0
  109. package/dist/test.js +5 -0
  110. package/dist/test.js.map +1 -0
  111. package/dist/testConfig.d.ts +11 -0
  112. package/dist/testConfig.d.ts.map +1 -0
  113. package/dist/testConfig.js +82 -0
  114. package/dist/testConfig.js.map +1 -0
  115. package/dist/vars.d.ts +61 -0
  116. package/dist/vars.d.ts.map +1 -0
  117. package/dist/vars.js +656 -0
  118. package/dist/vars.js.map +1 -0
  119. package/package.json +63 -0
package/dist/vars.js ADDED
@@ -0,0 +1,656 @@
1
+ /**
2
+ * Variable resolution for testmu.
3
+ *
4
+ * v(template) resolves {{...}} and ${...} placeholders:
5
+ * - Pure {{x}} → direct lookup from userStore (type-preserving); unresolved → ""
6
+ * - Embedded "prefix {{x}} suffix" → string interpolation; unresolved → ""
7
+ * - ${x} → testParams lookup, then userStore fallback (type-preserving when pure)
8
+ * - Non-string / non-template values → returned as-is
9
+ *
10
+ * vAsync(template) additionally resolves:
11
+ * - {{global.X}} → ATMS API or TESTMU_VAR_X env fallback
12
+ * - {{environment.X}} → ATMS API (with ENVIRONMENT_ID) or TESTMU_VAR_X env fallback
13
+ * - {{secrets.cat.KEY}} → process.env[KEY]
14
+ * - {{smart.X}} → local computation (no HTTP)
15
+ * - plain {{x}} / ${x} → delegates to sync v()
16
+ */
17
+ import { authenticator } from "otplib";
18
+ import { fetchWithAuth, ltAuthHeader } from "./http.js";
19
+ import { _registerConfigureHook } from "./configure.js";
20
+ // ---------------------------------------------------------------------------
21
+ // Internal stores (module-level singletons, reset via _resetStore())
22
+ // ---------------------------------------------------------------------------
23
+ const userStore = new Map();
24
+ const testParamsStore = new Map();
25
+ const globalVariables = [];
26
+ // ---------------------------------------------------------------------------
27
+ // Regex — non-greedy, matches Python's re.compile(r"\{\{(.+?)\}\}")
28
+ // ---------------------------------------------------------------------------
29
+ const MUSTACHE_RE = /\{\{(.+?)\}\}/;
30
+ const MUSTACHE_RE_G = /\{\{(.+?)\}\}/g;
31
+ const DOLLAR_RE = /\$\{(.+?)\}/;
32
+ const DOLLAR_RE_G = /\$\{(.+?)\}/g;
33
+ // Matches a single bracket segment like "[0]" or a mixed segment like "key[0]"
34
+ const BRACKET_RE = /^(\w+)\[(\d+)\]$/;
35
+ const BRACKET_ONLY_RE = /^\[(\d+)\]$/;
36
+ function applyTransform(value, t) {
37
+ switch (t) {
38
+ case "upper":
39
+ return String(value).toUpperCase();
40
+ case "lower":
41
+ return String(value).toLowerCase();
42
+ case "trim":
43
+ return String(value).trim();
44
+ case "str":
45
+ return String(value);
46
+ case "int": {
47
+ const n = Number(value);
48
+ if (!Number.isInteger(n) || isNaN(n))
49
+ throw new Error(`int transform: '${value}' is not an integer`);
50
+ return n;
51
+ }
52
+ case "float": {
53
+ const n = Number(value);
54
+ if (isNaN(n))
55
+ throw new Error(`float transform: '${value}' is not a number`);
56
+ return n;
57
+ }
58
+ // matches Python: str(value).lower() in ("true", "1", "yes")
59
+ case "bool": {
60
+ if (typeof value === "boolean")
61
+ return value;
62
+ return ["true", "1", "yes"].includes(String(value).toLowerCase());
63
+ }
64
+ }
65
+ }
66
+ /** Store a variable value into the given scope (default: user variables). */
67
+ export function setVar(name, value, opts = {}) {
68
+ const scope = opts.scope ?? "user";
69
+ if (scope === "testParams") {
70
+ testParamsStore.set(name, value);
71
+ }
72
+ else {
73
+ userStore.set(name, value);
74
+ }
75
+ }
76
+ /** Reset all stores (for testing). */
77
+ export function _resetStore() {
78
+ userStore.clear();
79
+ testParamsStore.clear();
80
+ globalVariables.length = 0;
81
+ }
82
+ /** Expose globalVariables for later tasks. */
83
+ export function _getGlobalVariables() {
84
+ return globalVariables;
85
+ }
86
+ /** Return the user variable store as a plain object (for executeJs injection). */
87
+ export function _getUserStore() {
88
+ return Object.fromEntries(userStore);
89
+ }
90
+ /** Append a global variable entry (called by configure() in Task 15). */
91
+ export function _addGlobalVariable(g) {
92
+ globalVariables.push(g);
93
+ }
94
+ /**
95
+ * Resolve a template value.
96
+ *
97
+ * - Non-string → returned as-is.
98
+ * - No {{...}} or ${...} → returned as-is.
99
+ * - Pure {{x}} → type-preserving lookup; unresolved → "".
100
+ * - Pure ${x} → testParams then userStore lookup; unresolved → "".
101
+ * - Embedded (contains other text) → string interpolation; unresolved → "".
102
+ */
103
+ export function v(template, opts = {}) {
104
+ // Non-string: pass through unchanged (JS idiom — differs from Python's None→"")
105
+ if (typeof template !== "string")
106
+ return template;
107
+ // Fast-path: no template patterns at all
108
+ if (!MUSTACHE_RE.test(template) && !DOLLAR_RE.test(template)) {
109
+ return template;
110
+ }
111
+ let result;
112
+ // Pure {{x}} — type-preserving; unresolved → ""
113
+ const pureMustacheName = extractPure(template, MUSTACHE_RE);
114
+ if (pureMustacheName !== null) {
115
+ const smart = maybeSmart(pureMustacheName);
116
+ result = smart.isSmart ? smart.value : resolveUserName(pureMustacheName);
117
+ return (opts.transform ? applyTransform(result, opts.transform) : result);
118
+ }
119
+ // Pure ${x} — type-preserving; unresolved → ""
120
+ const pureDollarName = extractPure(template, DOLLAR_RE);
121
+ if (pureDollarName !== null) {
122
+ result = lookupTestParams(pureDollarName);
123
+ return (opts.transform ? applyTransform(result, opts.transform) : result);
124
+ }
125
+ // Embedded template — string interpolation
126
+ let out = template.replace(MUSTACHE_RE_G, (_m, name) => {
127
+ const trimmed = name.trim();
128
+ const smart = maybeSmart(trimmed);
129
+ if (smart.isSmart)
130
+ return smart.value;
131
+ const val = resolveUserName(trimmed);
132
+ return val === "" ? "" : String(val);
133
+ });
134
+ out = out.replace(DOLLAR_RE_G, (_m, name) => {
135
+ const val = lookupTestParams(name.trim());
136
+ return val === "" ? "" : String(val);
137
+ });
138
+ result = out;
139
+ return (opts.transform ? applyTransform(result, opts.transform) : result);
140
+ }
141
+ // ---------------------------------------------------------------------------
142
+ // Dot-path / bracket traversal helpers
143
+ // ---------------------------------------------------------------------------
144
+ /** Sentinel that means "key not found" — distinct from a stored undefined/null. */
145
+ const MISSING = Symbol("testmu-missing");
146
+ /**
147
+ * If val is a JSON-string that parses to a dict or list, return the parsed value.
148
+ * Otherwise return val unchanged. Mirrors Python's _parse_if_string.
149
+ */
150
+ function parseIfJsonString(val) {
151
+ if (typeof val !== "string")
152
+ return val;
153
+ const s = val.trim();
154
+ if (s.startsWith("{") || s.startsWith("[")) {
155
+ try {
156
+ const parsed = JSON.parse(s);
157
+ if (typeof parsed === "object" && parsed !== null)
158
+ return parsed;
159
+ }
160
+ catch {
161
+ // not valid JSON — leave as string
162
+ }
163
+ }
164
+ return val;
165
+ }
166
+ /**
167
+ * Walk into `obj` using a dot/bracket path string.
168
+ *
169
+ * Examples:
170
+ * "address.city" on { address: { city: "Paris" } } → "Paris"
171
+ * "[1].id" on [{ id: 1 }, { id: 2 }] → 2
172
+ * "users[0].name" on { users: [{ name: "A" }] } → "A"
173
+ *
174
+ * Returns MISSING when the path cannot be fully navigated.
175
+ * Parses JSON strings at each step before descending.
176
+ */
177
+ function applyPath(value, path) {
178
+ const rawSegments = path.split(".");
179
+ const segments = rawSegments.filter((s) => s.length > 0);
180
+ let obj = parseIfJsonString(value);
181
+ for (const seg of segments) {
182
+ obj = parseIfJsonString(obj);
183
+ if (obj === null || obj === undefined)
184
+ return MISSING;
185
+ // Bracket-only segment: "[N]"
186
+ const bracketOnly = BRACKET_ONLY_RE.exec(seg);
187
+ if (bracketOnly) {
188
+ const idx = Number(bracketOnly[1]);
189
+ if (!Array.isArray(obj))
190
+ return MISSING;
191
+ if (idx < 0 || idx >= obj.length)
192
+ return MISSING;
193
+ obj = obj[idx];
194
+ continue;
195
+ }
196
+ // Mixed segment: "key[N]"
197
+ const mixed = BRACKET_RE.exec(seg);
198
+ if (mixed) {
199
+ const key = mixed[1];
200
+ const idx = Number(mixed[2]);
201
+ if (typeof obj !== "object" || Array.isArray(obj))
202
+ return MISSING;
203
+ const record = obj;
204
+ if (key === undefined || !(key in record))
205
+ return MISSING;
206
+ const inner = parseIfJsonString(record[key]);
207
+ if (!Array.isArray(inner))
208
+ return MISSING;
209
+ if (idx < 0 || idx >= inner.length)
210
+ return MISSING;
211
+ obj = inner[idx];
212
+ continue;
213
+ }
214
+ // Plain key or integer index
215
+ if (Array.isArray(obj)) {
216
+ const idx = Number(seg);
217
+ if (!Number.isInteger(idx) || isNaN(idx))
218
+ return MISSING;
219
+ if (idx < 0 || idx >= obj.length)
220
+ return MISSING;
221
+ obj = obj[idx];
222
+ }
223
+ else if (typeof obj === "object") {
224
+ const record = obj;
225
+ if (!(seg in record))
226
+ return MISSING;
227
+ obj = record[seg];
228
+ }
229
+ else {
230
+ return MISSING;
231
+ }
232
+ }
233
+ return obj;
234
+ }
235
+ /**
236
+ * Resolve a variable name that may contain dot-paths or bracket notation.
237
+ *
238
+ * Matches Python's _resolve_single for plain variables:
239
+ * 1. If name contains "." or "[", attempt dot-path traversal first:
240
+ * - Extract root key (before first "." or "[")
241
+ * - If root exists in store, traverse the rest of the path
242
+ * - If traversal fails (MISSING), fall back to flat key lookup
243
+ * 2. Otherwise, direct flat key lookup.
244
+ *
245
+ * Returns the resolved value or "" when nothing found.
246
+ */
247
+ function resolveUserName(name) {
248
+ const hasDot = name.includes(".");
249
+ const hasBracket = name.includes("[");
250
+ if (hasDot || hasBracket) {
251
+ // Extract root: everything before the first "." or "["
252
+ const firstDot = name.indexOf(".");
253
+ const firstBracket = name.indexOf("[");
254
+ let splitAt;
255
+ if (firstDot === -1)
256
+ splitAt = firstBracket;
257
+ else if (firstBracket === -1)
258
+ splitAt = firstDot;
259
+ else
260
+ splitAt = Math.min(firstDot, firstBracket);
261
+ const rootKey = name.slice(0, splitAt);
262
+ // Rest starts after "." or at "["; we pass the remainder including any "[" on the root
263
+ const afterRoot = name.slice(splitAt);
264
+ // Strip leading "." to get the path to pass to applyPath
265
+ const rest = afterRoot.startsWith(".") ? afterRoot.slice(1) : afterRoot;
266
+ if (userStore.has(rootKey) && rest.length > 0) {
267
+ const rootVal = userStore.get(rootKey);
268
+ const walked = applyPath(rootVal, rest);
269
+ if (walked !== MISSING)
270
+ return walked;
271
+ }
272
+ }
273
+ // Flat key lookup (also the fallback when root is absent or traversal fails)
274
+ if (userStore.has(name))
275
+ return userStore.get(name);
276
+ return "";
277
+ }
278
+ // ---------------------------------------------------------------------------
279
+ // Smart variables — {{smart.X}} — computed, matching Python _resolve_smart
280
+ // ---------------------------------------------------------------------------
281
+ const MONTH_NAMES = [
282
+ "January",
283
+ "February",
284
+ "March",
285
+ "April",
286
+ "May",
287
+ "June",
288
+ "July",
289
+ "August",
290
+ "September",
291
+ "October",
292
+ "November",
293
+ "December",
294
+ ];
295
+ // JS getDay(): Sunday=0..Saturday=6
296
+ const DAY_NAMES = [
297
+ "Sunday",
298
+ "Monday",
299
+ "Tuesday",
300
+ "Wednesday",
301
+ "Thursday",
302
+ "Friday",
303
+ "Saturday",
304
+ ];
305
+ // Python: ascii_letters + digits (a-z A-Z 0-9)
306
+ const ALPHANUM = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
307
+ // Python: ascii_lowercase + digits
308
+ const LOWERDIG = "abcdefghijklmnopqrstuvwxyz0123456789";
309
+ function pad2(n) {
310
+ return n < 10 ? `0${n}` : String(n);
311
+ }
312
+ function fmtDate(d) {
313
+ return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
314
+ }
315
+ function randInt(lo, hi) {
316
+ return Math.floor(Math.random() * (hi - lo + 1)) + lo;
317
+ }
318
+ function randStr(len, chars) {
319
+ let s = "";
320
+ for (let i = 0; i < len; i++)
321
+ s += chars[randInt(0, chars.length - 1)];
322
+ return s;
323
+ }
324
+ function startOfWeek(d) {
325
+ // Python: now - timedelta(days=now.weekday()); weekday() is Mon=0..Sun=6
326
+ const jsDow = d.getDay(); // Sun=0..Sat=6
327
+ const pyDow = jsDow === 0 ? 6 : jsDow - 1; // Mon=0..Sun=6
328
+ const r = new Date(d);
329
+ r.setDate(d.getDate() - pyDow);
330
+ return r;
331
+ }
332
+ function endOfWeek(d) {
333
+ const jsDow = d.getDay();
334
+ const pyDow = jsDow === 0 ? 6 : jsDow - 1;
335
+ const r = new Date(d);
336
+ r.setDate(d.getDate() + (6 - pyDow));
337
+ return r;
338
+ }
339
+ function daysInMonth(year, month0) {
340
+ // month0 is 0-indexed; new Date(y, m+1, 0) gives last day of month m
341
+ return new Date(year, month0 + 1, 0).getDate();
342
+ }
343
+ function computeSmartVariable(name) {
344
+ const now = new Date();
345
+ switch (name) {
346
+ // Date/time (Python strftime format strings)
347
+ case "current_date":
348
+ return fmtDate(now); // %Y-%m-%d
349
+ case "current_day":
350
+ return DAY_NAMES[now.getDay()]; // %A e.g. "Monday"
351
+ case "current_month_number":
352
+ return pad2(now.getMonth() + 1); // %m e.g. "03"
353
+ case "current_year":
354
+ return String(now.getFullYear()); // %Y
355
+ case "current_month":
356
+ return MONTH_NAMES[now.getMonth()]; // %B e.g. "March"
357
+ case "current_hour":
358
+ return pad2(now.getHours()); // %H
359
+ case "current_minute":
360
+ return pad2(now.getMinutes()); // %M
361
+ case "current_timestamp":
362
+ return `${fmtDate(now)} ${pad2(now.getHours())}:${pad2(now.getMinutes())}:${pad2(now.getSeconds())}`; // %Y-%m-%d %H:%M:%S
363
+ case "current_timezone":
364
+ return Intl.DateTimeFormat().resolvedOptions().timeZone; // time.strftime("%Z")
365
+ // Date arithmetic
366
+ case "next_day": {
367
+ const d = new Date(now);
368
+ d.setDate(d.getDate() + 1);
369
+ return fmtDate(d);
370
+ }
371
+ case "previous_day": {
372
+ const d = new Date(now);
373
+ d.setDate(d.getDate() - 1);
374
+ return fmtDate(d);
375
+ }
376
+ case "start_of_week":
377
+ return fmtDate(startOfWeek(now));
378
+ case "end_of_week":
379
+ return fmtDate(endOfWeek(now));
380
+ case "start_of_month":
381
+ return fmtDate(new Date(now.getFullYear(), now.getMonth(), 1));
382
+ case "end_of_month":
383
+ return fmtDate(new Date(now.getFullYear(), now.getMonth(), daysInMonth(now.getFullYear(), now.getMonth())));
384
+ // Random (Python-exact ranges)
385
+ case "random_int":
386
+ return String(randInt(100, 999)); // randint(100, 999)
387
+ case "random_float":
388
+ return String(Math.round((Math.random() * 99 + 1) * 100) / 100); // round(uniform(1,100), 2)
389
+ case "random_string_8":
390
+ return randStr(8, ALPHANUM);
391
+ case "random_string_56":
392
+ return randStr(56, ALPHANUM);
393
+ case "random_email":
394
+ return `${randStr(10, LOWERDIG)}@example.com`;
395
+ case "random_phone":
396
+ return String(randInt(1_000_000_000, 9_999_999_999)); // randint(1e9, 9.999...e9)
397
+ // Static geo (exact literals from Python)
398
+ case "latitude":
399
+ return "13.2257";
400
+ case "longitude":
401
+ return "77.5750";
402
+ case "country":
403
+ return "India";
404
+ case "city":
405
+ return "Doddaballapura";
406
+ case "ip_address":
407
+ return "143.110.182.88";
408
+ // Env-backed system info (Python: os.getenv("LT_USERNAME", ""), etc.)
409
+ case "user_name":
410
+ return process.env["LT_USERNAME"] ?? "";
411
+ case "os_type":
412
+ // falls through
413
+ case "os":
414
+ return process.env["smart_os"] ?? "";
415
+ case "os_version":
416
+ return process.env["smart_os_version"] ?? "";
417
+ case "browser_name":
418
+ return process.env["smart_browser_name"] ?? "";
419
+ case "browser_version":
420
+ return process.env["smart_browser_version"] ?? "";
421
+ default:
422
+ return "";
423
+ }
424
+ }
425
+ /** If name starts with "smart.", compute the smart variable. */
426
+ function maybeSmart(name) {
427
+ if (!name.startsWith("smart."))
428
+ return { isSmart: false };
429
+ return {
430
+ isSmart: true,
431
+ value: computeSmartVariable(name.slice("smart.".length)),
432
+ };
433
+ }
434
+ // ---------------------------------------------------------------------------
435
+ // Internal helpers
436
+ // ---------------------------------------------------------------------------
437
+ /**
438
+ * If `template` is exactly one placeholder matching `re`, return the captured
439
+ * group (trimmed). Otherwise return null.
440
+ *
441
+ * Uses an anchored regex so "{{a}} {{b}}" is NOT considered pure.
442
+ *
443
+ * noUncheckedIndexedAccess: m[1] is string|undefined — use `m[1] ?? null`.
444
+ */
445
+ function extractPure(template, re) {
446
+ const anchored = new RegExp(`^${re.source}$`);
447
+ const m = template.match(anchored);
448
+ if (!m)
449
+ return null;
450
+ const captured = m[1];
451
+ return captured !== undefined ? captured.trim() : null;
452
+ }
453
+ /** Look up a name from userStore. Returns "" if not found. */
454
+ function lookupUser(name) {
455
+ if (userStore.has(name))
456
+ return userStore.get(name);
457
+ return "";
458
+ }
459
+ /**
460
+ * Look up a name from testParamsStore, falling back to userStore.
461
+ * Returns "" if not found in either (matches Python: _test_params.get(name, _variable_store.get(name, ""))).
462
+ */
463
+ function lookupTestParams(name) {
464
+ if (testParamsStore.has(name))
465
+ return testParamsStore.get(name);
466
+ if (userStore.has(name))
467
+ return userStore.get(name);
468
+ return "";
469
+ }
470
+ /**
471
+ * GET {ATMS_URL}/api/v1/variables/{name}?environment_id={id}
472
+ *
473
+ * Matches Python _atms_get_variable: always includes environment_id (0 for global).
474
+ * Response: resp.json()["data"] — then .value from that dict.
475
+ * ATMS_URL has no default (empty string if unset) — mirrors Python.
476
+ */
477
+ async function atmsGetVariable(name, environmentId = 0) {
478
+ const base = (process.env["ATMS_URL"] ?? "").replace(/\/$/, "");
479
+ const url = `${base}/api/v1/variables/${encodeURIComponent(name)}?environment_id=${environmentId}`;
480
+ const resp = await fetchWithAuth(url, { method: "GET" });
481
+ if (!resp.ok)
482
+ throw new Error(`ATMS variable fetch failed for '${name}': ${resp.status}`);
483
+ const body = (await resp.json());
484
+ return body.data.value;
485
+ }
486
+ /**
487
+ * Resolve a special async variable prefix: global., environment., secrets.
488
+ * Throws a descriptive error when resolution fails.
489
+ */
490
+ async function resolveSpecialAsync(name) {
491
+ // {{global.X}} → ATMS (environment_id=0) or TESTMU_VAR_X fallback
492
+ if (name.startsWith("global.")) {
493
+ const varName = name.slice("global.".length);
494
+ if (!ltAuthHeader()) {
495
+ const envVal = process.env[`TESTMU_VAR_${varName}`];
496
+ if (envVal === undefined) {
497
+ throw new Error(`Cannot resolve {{global.${varName}}}: no LT credentials and no TESTMU_VAR_${varName} env var set`);
498
+ }
499
+ return envVal;
500
+ }
501
+ try {
502
+ return await atmsGetVariable(varName, 0);
503
+ }
504
+ catch (err) {
505
+ const envVal = process.env[`TESTMU_VAR_${varName}`];
506
+ if (envVal !== undefined)
507
+ return envVal;
508
+ throw new Error(`Cannot resolve {{global.${varName}}}: ATMS failed and no TESTMU_VAR_${varName} env var set`);
509
+ }
510
+ }
511
+ // {{environment.X}} → ATMS with ENVIRONMENT_ID or TESTMU_VAR_X fallback
512
+ if (name.startsWith("environment.")) {
513
+ const varName = name.slice("environment.".length);
514
+ if (!ltAuthHeader()) {
515
+ const envVal = process.env[`TESTMU_VAR_${varName}`];
516
+ if (envVal === undefined) {
517
+ throw new Error(`Cannot resolve {{environment.${varName}}}: no LT credentials and no TESTMU_VAR_${varName} env var set`);
518
+ }
519
+ return envVal;
520
+ }
521
+ try {
522
+ const envId = parseInt(process.env["ENVIRONMENT_ID"] ?? "0", 10);
523
+ return await atmsGetVariable(varName, envId);
524
+ }
525
+ catch (err) {
526
+ const envVal = process.env[`TESTMU_VAR_${varName}`];
527
+ if (envVal !== undefined)
528
+ return envVal;
529
+ throw new Error(`Cannot resolve {{environment.${varName}}}: ATMS failed and no TESTMU_VAR_${varName} env var set`);
530
+ }
531
+ }
532
+ // {{secrets.cat.KEY}} → process.env[KEY]
533
+ if (name.startsWith("secrets.")) {
534
+ const parts = name.split(".");
535
+ const key = parts[parts.length - 1];
536
+ if (!key)
537
+ throw new Error(`Invalid secrets path: ${name}`);
538
+ const val = process.env[key];
539
+ if (val === undefined)
540
+ throw new Error(`Cannot resolve {{${name}}}: env var '${key}' not set`);
541
+ return val;
542
+ }
543
+ // {{totp.X}} → ATMS seed + otplib generate, or TESTMU_TOTP_X env fallback (also a seed)
544
+ if (name.startsWith("totp.")) {
545
+ const varName = name.slice("totp.".length);
546
+ let seed;
547
+ if (!ltAuthHeader()) {
548
+ const envVal = process.env[`TESTMU_TOTP_${varName}`];
549
+ if (envVal === undefined) {
550
+ throw new Error(`Cannot resolve {{totp.${varName}}}: no LT credentials and no TESTMU_TOTP_${varName} env var set`);
551
+ }
552
+ seed = envVal;
553
+ }
554
+ else {
555
+ // GET {ATMS_URL}/api/v1/variables/totp/{name} — resp.json()["data"] is the seed string
556
+ const base = (process.env["ATMS_URL"] ?? "").replace(/\/$/, "");
557
+ const url = `${base}/api/v1/variables/totp/${encodeURIComponent(varName)}`;
558
+ const resp = await fetchWithAuth(url, { method: "GET" });
559
+ if (!resp.ok)
560
+ throw new Error(`TOTP seed fetch failed for '${varName}': ${resp.status}`);
561
+ const body = (await resp.json());
562
+ seed = body.data;
563
+ }
564
+ return authenticator.generate(seed);
565
+ }
566
+ throw new Error(`Unknown special variable prefix: ${name}`);
567
+ }
568
+ /**
569
+ * Async variable resolution — extends v() with HTTP-backed lookups.
570
+ *
571
+ * - {{global.X}} → ATMS or TESTMU_VAR_X fallback
572
+ * - {{environment.X}} → ATMS (with ENVIRONMENT_ID) or TESTMU_VAR_X fallback
573
+ * - {{secrets.cat.KEY}} → process.env[KEY]
574
+ * - {{smart.X}} → local computation (no HTTP)
575
+ * - plain {{x}} / ${x} → delegates to sync v()
576
+ */
577
+ export async function vAsync(template, opts = {}) {
578
+ if (typeof template !== "string")
579
+ return template;
580
+ let result;
581
+ // Pure single-placeholder: {{name}}
582
+ const pureName = extractPure(template, MUSTACHE_RE);
583
+ if (pureName !== null) {
584
+ if (pureName.startsWith("smart.")) {
585
+ result = computeSmartVariable(pureName.slice("smart.".length));
586
+ }
587
+ else if (pureName.startsWith("global.") ||
588
+ pureName.startsWith("environment.") ||
589
+ pureName.startsWith("secrets.") ||
590
+ pureName.startsWith("totp.")) {
591
+ result = await resolveSpecialAsync(pureName);
592
+ }
593
+ else {
594
+ // Delegate to sync v() for plain variables (without transform — apply below)
595
+ result = v(template);
596
+ }
597
+ return (opts.transform ? applyTransform(result, opts.transform) : result);
598
+ }
599
+ // Pure ${x} — delegate sync (without transform — apply below)
600
+ const pureDollar = extractPure(template, DOLLAR_RE);
601
+ if (pureDollar !== null) {
602
+ result = v(template);
603
+ return (opts.transform ? applyTransform(result, opts.transform) : result);
604
+ }
605
+ // Embedded template — resolve {{...}} placeholders asynchronously
606
+ if (!MUSTACHE_RE.test(template) && !DOLLAR_RE.test(template)) {
607
+ return template;
608
+ }
609
+ const matches = [...template.matchAll(MUSTACHE_RE_G)];
610
+ const replacements = [];
611
+ for (const match of matches) {
612
+ const inner = (match[1] ?? "").trim();
613
+ if (inner.startsWith("smart.")) {
614
+ replacements.push(computeSmartVariable(inner.slice("smart.".length)));
615
+ }
616
+ else if (inner.startsWith("global.") ||
617
+ inner.startsWith("environment.") ||
618
+ inner.startsWith("secrets.") ||
619
+ inner.startsWith("totp.")) {
620
+ replacements.push(await resolveSpecialAsync(inner));
621
+ }
622
+ else {
623
+ const sync = v(`{{${inner}}}`);
624
+ replacements.push(sync === "" ? "" : String(sync));
625
+ }
626
+ }
627
+ let out = template;
628
+ for (let i = 0; i < matches.length; i++) {
629
+ out = out.replace(matches[i][0], replacements[i]);
630
+ }
631
+ // ${...} through sync path
632
+ out = out.replace(DOLLAR_RE_G, (_m, name) => {
633
+ const val = lookupTestParams(name.trim());
634
+ return val === "" ? "" : String(val);
635
+ });
636
+ result = out;
637
+ return (opts.transform ? applyTransform(result, opts.transform) : result);
638
+ }
639
+ // ---------------------------------------------------------------------------
640
+ // configure() → vars store wiring (registered once at module load)
641
+ // ---------------------------------------------------------------------------
642
+ _registerConfigureHook((opts) => {
643
+ if (opts.variables) {
644
+ for (const [k, val] of Object.entries(opts.variables))
645
+ setVar(k, val);
646
+ }
647
+ if (opts.testParams) {
648
+ for (const [k, val] of Object.entries(opts.testParams))
649
+ setVar(k, val, { scope: "testParams" });
650
+ }
651
+ if (opts.globalVariables) {
652
+ for (const g of opts.globalVariables)
653
+ _addGlobalVariable(g);
654
+ }
655
+ });
656
+ //# sourceMappingURL=vars.js.map