@validate.qa/runner 1.0.81 → 1.0.83

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +231 -94
  2. package/dist/cli.mjs +231 -94
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -105530,7 +105530,7 @@ var require_typescript = __commonJS({
105530
105530
  walkUpParenthesizedTypesAndGetParentAndChild: () => walkUpParenthesizedTypesAndGetParentAndChild,
105531
105531
  whitespaceOrMapCommentRegExp: () => whitespaceOrMapCommentRegExp,
105532
105532
  writeCommentRange: () => writeCommentRange,
105533
- writeFile: () => writeFile2,
105533
+ writeFile: () => writeFile3,
105534
105534
  writeFileEnsuringDirectories: () => writeFileEnsuringDirectories,
105535
105535
  zipWith: () => zipWith
105536
105536
  });
@@ -111862,7 +111862,7 @@ ${lanes.join("\n")}
111862
111862
  writeOutputIsTTY() {
111863
111863
  return process.stdout.isTTY;
111864
111864
  },
111865
- readFile: readFile2,
111865
+ readFile: readFile3,
111866
111866
  writeFile: writeFile22,
111867
111867
  watchFile: watchFile2,
111868
111868
  watchDirectory,
@@ -112068,7 +112068,7 @@ ${lanes.join("\n")}
112068
112068
  callback
112069
112069
  );
112070
112070
  }
112071
- function readFile2(fileName, _encoding) {
112071
+ function readFile3(fileName, _encoding) {
112072
112072
  let buffer;
112073
112073
  try {
112074
112074
  buffer = _fs.readFileSync(fileName);
@@ -124173,7 +124173,7 @@ ${lanes.join("\n")}
124173
124173
  sourceFilePath = isSourceFileInCommonSourceDirectory ? sourceFilePath.substring(commonSourceDirectory.length) : sourceFilePath;
124174
124174
  return combinePaths(newDirPath, sourceFilePath);
124175
124175
  }
124176
- function writeFile2(host, diagnostics, fileName, text, writeByteOrderMark, sourceFiles, data) {
124176
+ function writeFile3(host, diagnostics, fileName, text, writeByteOrderMark, sourceFiles, data) {
124177
124177
  host.writeFile(
124178
124178
  fileName,
124179
124179
  text,
@@ -148624,7 +148624,7 @@ ${lanes.join("\n")}
148624
148624
  const possibleOption = getSpellingSuggestion(unknownOption, diagnostics.optionDeclarations, getOptionName);
148625
148625
  return possibleOption ? createDiagnosticForNodeInSourceFileOrCompilerDiagnostic(sourceFile, node, diagnostics.unknownDidYouMeanDiagnostic, unknownOptionErrorText || unknownOption, possibleOption.name) : createDiagnosticForNodeInSourceFileOrCompilerDiagnostic(sourceFile, node, diagnostics.unknownOptionDiagnostic, unknownOptionErrorText || unknownOption);
148626
148626
  }
148627
- function parseCommandLineWorker(diagnostics, commandLine, readFile2) {
148627
+ function parseCommandLineWorker(diagnostics, commandLine, readFile3) {
148628
148628
  const options = {};
148629
148629
  let watchOptions;
148630
148630
  const fileNames = [];
@@ -148672,7 +148672,7 @@ ${lanes.join("\n")}
148672
148672
  }
148673
148673
  }
148674
148674
  function parseResponseFile(fileName) {
148675
- const text = tryReadFile(fileName, readFile2 || ((fileName2) => sys.readFile(fileName2)));
148675
+ const text = tryReadFile(fileName, readFile3 || ((fileName2) => sys.readFile(fileName2)));
148676
148676
  if (!isString(text)) {
148677
148677
  errors2.push(text);
148678
148678
  return;
@@ -148775,8 +148775,8 @@ ${lanes.join("\n")}
148775
148775
  unknownDidYouMeanDiagnostic: Diagnostics.Unknown_compiler_option_0_Did_you_mean_1,
148776
148776
  optionTypeMismatchDiagnostic: Diagnostics.Compiler_option_0_expects_an_argument
148777
148777
  };
148778
- function parseCommandLine(commandLine, readFile2) {
148779
- return parseCommandLineWorker(compilerOptionsDidYouMeanDiagnostics, commandLine, readFile2);
148778
+ function parseCommandLine(commandLine, readFile3) {
148779
+ return parseCommandLineWorker(compilerOptionsDidYouMeanDiagnostics, commandLine, readFile3);
148780
148780
  }
148781
148781
  function getOptionFromName(optionName, allowShort) {
148782
148782
  return getOptionDeclarationFromName(getOptionsNameMap, optionName, allowShort);
@@ -148858,8 +148858,8 @@ ${lanes.join("\n")}
148858
148858
  watchOptionsToExtend
148859
148859
  );
148860
148860
  }
148861
- function readConfigFile(fileName, readFile2) {
148862
- const textOrDiagnostic = tryReadFile(fileName, readFile2);
148861
+ function readConfigFile(fileName, readFile3) {
148862
+ const textOrDiagnostic = tryReadFile(fileName, readFile3);
148863
148863
  return isString(textOrDiagnostic) ? parseConfigFileTextToJson(fileName, textOrDiagnostic) : { config: {}, error: textOrDiagnostic };
148864
148864
  }
148865
148865
  function parseConfigFileTextToJson(fileName, jsonText) {
@@ -148874,14 +148874,14 @@ ${lanes.join("\n")}
148874
148874
  error: jsonSourceFile.parseDiagnostics.length ? jsonSourceFile.parseDiagnostics[0] : void 0
148875
148875
  };
148876
148876
  }
148877
- function readJsonConfigFile(fileName, readFile2) {
148878
- const textOrDiagnostic = tryReadFile(fileName, readFile2);
148877
+ function readJsonConfigFile(fileName, readFile3) {
148878
+ const textOrDiagnostic = tryReadFile(fileName, readFile3);
148879
148879
  return isString(textOrDiagnostic) ? parseJsonText(fileName, textOrDiagnostic) : { fileName, parseDiagnostics: [textOrDiagnostic] };
148880
148880
  }
148881
- function tryReadFile(fileName, readFile2) {
148881
+ function tryReadFile(fileName, readFile3) {
148882
148882
  let text;
148883
148883
  try {
148884
- text = readFile2(fileName);
148884
+ text = readFile3(fileName);
148885
148885
  } catch (e) {
148886
148886
  return createCompilerDiagnostic(Diagnostics.Cannot_read_file_0_Colon_1, fileName, e.message);
148887
148887
  }
@@ -232924,7 +232924,7 @@ ${lanes.join("\n")}
232924
232924
  return;
232925
232925
  }
232926
232926
  const buildInfo = host.getBuildInfo() || { version };
232927
- writeFile2(
232927
+ writeFile3(
232928
232928
  host,
232929
232929
  emitterDiagnostics,
232930
232930
  buildInfoPath,
@@ -233136,7 +233136,7 @@ ${lanes.join("\n")}
233136
233136
  }
233137
233137
  if (sourceMapFilePath) {
233138
233138
  const sourceMap = sourceMapGenerator.toString();
233139
- writeFile2(
233139
+ writeFile3(
233140
233140
  host,
233141
233141
  emitterDiagnostics,
233142
233142
  sourceMapFilePath,
@@ -233151,7 +233151,7 @@ ${lanes.join("\n")}
233151
233151
  }
233152
233152
  const text = writer.getText();
233153
233153
  const data = { sourceMapUrlPos, diagnostics: transform2.diagnostics };
233154
- writeFile2(host, emitterDiagnostics, jsFilePath, text, !!compilerOptions.emitBOM, sourceFiles, data);
233154
+ writeFile3(host, emitterDiagnostics, jsFilePath, text, !!compilerOptions.emitBOM, sourceFiles, data);
233155
233155
  writer.clear();
233156
233156
  return !data.skippedDtsWrite;
233157
233157
  }
@@ -238539,12 +238539,12 @@ ${lanes.join("\n")}
238539
238539
  function createCompilerHost(options, setParentNodes) {
238540
238540
  return createCompilerHostWorker(options, setParentNodes);
238541
238541
  }
238542
- function createGetSourceFile(readFile2, setParentNodes) {
238542
+ function createGetSourceFile(readFile3, setParentNodes) {
238543
238543
  return (fileName, languageVersionOrOptions, onError) => {
238544
238544
  let text;
238545
238545
  try {
238546
238546
  mark("beforeIORead");
238547
- text = readFile2(fileName);
238547
+ text = readFile3(fileName);
238548
238548
  mark("afterIORead");
238549
238549
  measure("I/O Read", "beforeIORead", "afterIORead");
238550
238550
  } catch (e) {
@@ -239448,7 +239448,7 @@ ${lanes.join("\n")}
239448
239448
  getRedirectFromOutput,
239449
239449
  forEachResolvedProjectReference: forEachResolvedProjectReference2
239450
239450
  });
239451
- const readFile2 = host.readFile.bind(host);
239451
+ const readFile3 = host.readFile.bind(host);
239452
239452
  (_e = tracing) == null ? void 0 : _e.push(tracing.Phase.Program, "shouldProgramCreateNewSourceFiles", { hasOldProgram: !!oldProgram });
239453
239453
  const shouldCreateNewSourceFile = shouldProgramCreateNewSourceFiles(oldProgram, options);
239454
239454
  (_f = tracing) == null ? void 0 : _f.pop();
@@ -239674,7 +239674,7 @@ ${lanes.join("\n")}
239674
239674
  shouldTransformImportCall,
239675
239675
  emitBuildInfo,
239676
239676
  fileExists,
239677
- readFile: readFile2,
239677
+ readFile: readFile3,
239678
239678
  directoryExists,
239679
239679
  getSymlinkCache,
239680
239680
  realpath: (_o = host.realpath) == null ? void 0 : _o.bind(host),
@@ -301033,7 +301033,7 @@ ${options.prefix}` : "\n" : options.prefix
301033
301033
  walkUpParenthesizedTypesAndGetParentAndChild: () => walkUpParenthesizedTypesAndGetParentAndChild,
301034
301034
  whitespaceOrMapCommentRegExp: () => whitespaceOrMapCommentRegExp,
301035
301035
  writeCommentRange: () => writeCommentRange,
301036
- writeFile: () => writeFile2,
301036
+ writeFile: () => writeFile3,
301037
301037
  writeFileEnsuringDirectories: () => writeFileEnsuringDirectories,
301038
301038
  zipWith: () => zipWith
301039
301039
  });
@@ -347535,7 +347535,7 @@ var require_mkdir = __commonJS({
347535
347535
  const gid = opt.gid;
347536
347536
  const doChown = typeof uid === "number" && typeof gid === "number" && (uid !== opt.processUid || gid !== opt.processGid);
347537
347537
  const preserve = opt.preserve;
347538
- const unlink2 = opt.unlink;
347538
+ const unlink3 = opt.unlink;
347539
347539
  const cache = opt.cache;
347540
347540
  const cwd = normPath(opt.cwd);
347541
347541
  const done = (er, created) => {
@@ -347563,33 +347563,33 @@ var require_mkdir = __commonJS({
347563
347563
  }
347564
347564
  const sub = normPath(path.relative(cwd, dir));
347565
347565
  const parts = sub.split("/");
347566
- mkdir_(cwd, parts, mode, cache, unlink2, cwd, null, done);
347566
+ mkdir_(cwd, parts, mode, cache, unlink3, cwd, null, done);
347567
347567
  };
347568
- var mkdir_ = (base2, parts, mode, cache, unlink2, cwd, created, cb) => {
347568
+ var mkdir_ = (base2, parts, mode, cache, unlink3, cwd, created, cb) => {
347569
347569
  if (!parts.length) {
347570
347570
  return cb(null, created);
347571
347571
  }
347572
347572
  const p = parts.shift();
347573
347573
  const part = normPath(path.resolve(base2 + "/" + p));
347574
347574
  if (cGet(cache, part)) {
347575
- return mkdir_(part, parts, mode, cache, unlink2, cwd, created, cb);
347575
+ return mkdir_(part, parts, mode, cache, unlink3, cwd, created, cb);
347576
347576
  }
347577
- fs.mkdir(part, mode, onmkdir(part, parts, mode, cache, unlink2, cwd, created, cb));
347577
+ fs.mkdir(part, mode, onmkdir(part, parts, mode, cache, unlink3, cwd, created, cb));
347578
347578
  };
347579
- var onmkdir = (part, parts, mode, cache, unlink2, cwd, created, cb) => (er) => {
347579
+ var onmkdir = (part, parts, mode, cache, unlink3, cwd, created, cb) => (er) => {
347580
347580
  if (er) {
347581
347581
  fs.lstat(part, (statEr, st) => {
347582
347582
  if (statEr) {
347583
347583
  statEr.path = statEr.path && normPath(statEr.path);
347584
347584
  cb(statEr);
347585
347585
  } else if (st.isDirectory()) {
347586
- mkdir_(part, parts, mode, cache, unlink2, cwd, created, cb);
347587
- } else if (unlink2) {
347586
+ mkdir_(part, parts, mode, cache, unlink3, cwd, created, cb);
347587
+ } else if (unlink3) {
347588
347588
  fs.unlink(part, (er2) => {
347589
347589
  if (er2) {
347590
347590
  return cb(er2);
347591
347591
  }
347592
- fs.mkdir(part, mode, onmkdir(part, parts, mode, cache, unlink2, cwd, created, cb));
347592
+ fs.mkdir(part, mode, onmkdir(part, parts, mode, cache, unlink3, cwd, created, cb));
347593
347593
  });
347594
347594
  } else if (st.isSymbolicLink()) {
347595
347595
  return cb(new SymlinkError(part, part + "/" + parts.join("/")));
@@ -347599,7 +347599,7 @@ var require_mkdir = __commonJS({
347599
347599
  });
347600
347600
  } else {
347601
347601
  created = created || part;
347602
- mkdir_(part, parts, mode, cache, unlink2, cwd, created, cb);
347602
+ mkdir_(part, parts, mode, cache, unlink3, cwd, created, cb);
347603
347603
  }
347604
347604
  };
347605
347605
  var checkCwdSync = (dir) => {
@@ -347624,7 +347624,7 @@ var require_mkdir = __commonJS({
347624
347624
  const gid = opt.gid;
347625
347625
  const doChown = typeof uid === "number" && typeof gid === "number" && (uid !== opt.processUid || gid !== opt.processGid);
347626
347626
  const preserve = opt.preserve;
347627
- const unlink2 = opt.unlink;
347627
+ const unlink3 = opt.unlink;
347628
347628
  const cache = opt.cache;
347629
347629
  const cwd = normPath(opt.cwd);
347630
347630
  const done = (created2) => {
@@ -347663,7 +347663,7 @@ var require_mkdir = __commonJS({
347663
347663
  if (st.isDirectory()) {
347664
347664
  cSet(cache, part, true);
347665
347665
  continue;
347666
- } else if (unlink2) {
347666
+ } else if (unlink3) {
347667
347667
  fs.unlinkSync(part);
347668
347668
  fs.mkdirSync(part, mode);
347669
347669
  created = created || part;
@@ -353337,7 +353337,7 @@ var require_package4 = __commonJS({
353337
353337
  "package.json"(exports2, module2) {
353338
353338
  module2.exports = {
353339
353339
  name: "@validate.qa/runner",
353340
- version: "1.0.81",
353340
+ version: "1.0.83",
353341
353341
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
353342
353342
  bin: {
353343
353343
  "validate-runner": "dist/cli.js"
@@ -355285,6 +355285,7 @@ var import_node_child_process2 = require("child_process");
355285
355285
  var import_node_util2 = require("util");
355286
355286
  var import_node_os2 = require("os");
355287
355287
  var import_node_path2 = require("path");
355288
+ var import_promises3 = require("fs/promises");
355288
355289
  var import_runner_core8 = __toESM(require_dist2());
355289
355290
 
355290
355291
  // src/services/device-discovery.ts
@@ -356719,6 +356720,12 @@ async function createSession(target) {
356719
356720
  // whole survey. The runner owns session teardown explicitly, so a long
356720
356721
  // idle window is safe. Env-overridable.
356721
356722
  "appium:newCommandTimeout": MOBILE_NEW_COMMAND_TIMEOUT_SECONDS,
356723
+ // Expo / universal-link "Open in App?" system sheets and permission dialogs
356724
+ // otherwise sit on top of the app and can stall WDA snapshot/command proxy
356725
+ // for the entire discovery session (observed: 20+ min hung XCUITest with
356726
+ // ECONNREFUSED on wdaLocalPort). Auto-accept keeps the AUT interactive.
356727
+ // Override with APPIUM_IOS_AUTO_ACCEPT_ALERTS=false for explicit dialog tests.
356728
+ ...process.env.APPIUM_IOS_AUTO_ACCEPT_ALERTS !== "false" ? { "appium:autoAcceptAlerts": true } : {},
356722
356729
  // Xcode 27+ relocates Simulator.app out of <DEVELOPER_DIR>/Applications, so
356723
356730
  // XCUITest's default "open Simulator UI" step fails ("Simulator.app does not
356724
356731
  // exist") on this toolchain. The simulator is already booted and driven
@@ -356974,20 +356981,102 @@ async function startScreenRecording(sessionId, platform3, log2) {
356974
356981
  if (platform3 === "ANDROID") {
356975
356982
  options.bitRate = readPositiveIntEnv("VALIDATEQA_MOBILE_RECORDING_BITRATE", 75e4, 1e5, 4e6);
356976
356983
  } else {
356977
- options.videoQuality = process.env.VALIDATEQA_MOBILE_RECORDING_IOS_QUALITY || "low";
356984
+ options.videoType = process.env.VALIDATEQA_MOBILE_RECORDING_IOS_VIDEO_TYPE || "libx264";
356985
+ options.videoQuality = process.env.VALIDATEQA_MOBILE_RECORDING_IOS_QUALITY || "medium";
356986
+ options.videoFilters = process.env.VALIDATEQA_MOBILE_RECORDING_IOS_FILTERS || "scale=trunc(iw/2)*2:trunc(ih/2)*2";
356978
356987
  }
356979
356988
  try {
356980
356989
  await wdRequest(`/session/${sessionId}/appium/start_recording_screen`, {
356981
356990
  method: "POST",
356982
356991
  body: JSON.stringify({ options })
356983
356992
  });
356984
- log2(`[mobile-video] screen recording started (${timeLimit}s max)`);
356993
+ log2(`[mobile-video] screen recording started (${timeLimit}s max${platform3 === "IOS" ? `, type=${options.videoType}` : ""})`);
356985
356994
  return true;
356986
356995
  } catch (err) {
356996
+ if (platform3 === "IOS" && options.videoType) {
356997
+ try {
356998
+ const fallback = {
356999
+ timeLimit: String(timeLimit),
357000
+ forceRestart: true,
357001
+ videoQuality: process.env.VALIDATEQA_MOBILE_RECORDING_IOS_QUALITY || "medium"
357002
+ };
357003
+ await wdRequest(`/session/${sessionId}/appium/start_recording_screen`, {
357004
+ method: "POST",
357005
+ body: JSON.stringify({ options: fallback })
357006
+ });
357007
+ log2(`[mobile-video] screen recording started with quality-only fallback (${timeLimit}s max); will re-encode on stop`);
357008
+ return true;
357009
+ } catch (fallbackErr) {
357010
+ log2(`[mobile-video] screen recording unavailable: ${fallbackErr instanceof Error ? fallbackErr.message : String(fallbackErr)}`);
357011
+ return false;
357012
+ }
357013
+ }
356987
357014
  log2(`[mobile-video] screen recording unavailable: ${err instanceof Error ? err.message : String(err)}`);
356988
357015
  return false;
356989
357016
  }
356990
357017
  }
357018
+ async function ensureBrowserPlayableMp4Base64(base64Video, log2) {
357019
+ if (process.env.VALIDATEQA_MOBILE_VIDEO_REENCODE === "0") {
357020
+ return base64Video;
357021
+ }
357022
+ const inPath = (0, import_node_path2.join)((0, import_node_os2.tmpdir)(), `validateqa-mobile-rec-in-${Date.now()}-${Math.random().toString(36).slice(2)}.mp4`);
357023
+ const outPath = (0, import_node_path2.join)((0, import_node_os2.tmpdir)(), `validateqa-mobile-rec-out-${Date.now()}-${Math.random().toString(36).slice(2)}.mp4`);
357024
+ try {
357025
+ await (0, import_promises3.writeFile)(inPath, Buffer.from(base64Video, "base64"));
357026
+ try {
357027
+ const { stdout } = await execFileAsync3("ffprobe", [
357028
+ "-v",
357029
+ "error",
357030
+ "-select_streams",
357031
+ "v:0",
357032
+ "-show_entries",
357033
+ "stream=codec_name",
357034
+ "-of",
357035
+ "csv=p=0",
357036
+ inPath
357037
+ ], { timeout: 15e3 });
357038
+ const codec = stdout.trim().toLowerCase();
357039
+ if (codec === "h264" || codec === "avc1") {
357040
+ log2(`[mobile-video] recording already H.264 (${codec}); skipping re-encode`);
357041
+ return base64Video;
357042
+ }
357043
+ log2(`[mobile-video] re-encoding ${codec || "unknown"} \u2192 H.264 for browser preview`);
357044
+ } catch {
357045
+ log2("[mobile-video] codec probe failed; attempting H.264 re-encode anyway");
357046
+ }
357047
+ await execFileAsync3("ffmpeg", [
357048
+ "-y",
357049
+ "-i",
357050
+ inPath,
357051
+ "-vf",
357052
+ "scale=trunc(iw/2)*2:trunc(ih/2)*2",
357053
+ "-c:v",
357054
+ "libx264",
357055
+ "-pix_fmt",
357056
+ "yuv420p",
357057
+ "-preset",
357058
+ "veryfast",
357059
+ "-crf",
357060
+ "28",
357061
+ "-movflags",
357062
+ "+faststart",
357063
+ "-an",
357064
+ outPath
357065
+ ], { timeout: 12e4 });
357066
+ const encoded = await (0, import_promises3.readFile)(outPath);
357067
+ if (encoded.length < 100) {
357068
+ log2("[mobile-video] re-encode produced empty file; keeping original");
357069
+ return base64Video;
357070
+ }
357071
+ log2(`[mobile-video] re-encoded for browser (${Math.round(encoded.length / 1024)}KB)`);
357072
+ return encoded.toString("base64");
357073
+ } catch (err) {
357074
+ log2(`[mobile-video] re-encode skipped/failed: ${err instanceof Error ? err.message : String(err)}`);
357075
+ return base64Video;
357076
+ } finally {
357077
+ await Promise.allSettled([(0, import_promises3.unlink)(inPath), (0, import_promises3.unlink)(outPath)]);
357078
+ }
357079
+ }
356991
357080
  async function stopScreenRecording(sessionId, log2) {
356992
357081
  try {
356993
357082
  const result = await wdRequest(`/session/${sessionId}/appium/stop_recording_screen`, {
@@ -357000,7 +357089,7 @@ async function stopScreenRecording(sessionId, log2) {
357000
357089
  return void 0;
357001
357090
  }
357002
357091
  log2(`[mobile-video] screen recording captured (${Math.round(value.length / 1024)}KB base64)`);
357003
- return value;
357092
+ return ensureBrowserPlayableMp4Base64(value, log2);
357004
357093
  } catch (err) {
357005
357094
  log2(`[mobile-video] screen recording stop failed: ${err instanceof Error ? err.message : String(err)}`);
357006
357095
  return void 0;
@@ -357085,10 +357174,17 @@ function xpathLiteral3(value) {
357085
357174
  const parts = value.split("'").map((p) => `'${p}'`);
357086
357175
  return `concat(${parts.join(`, "'", `)})`;
357087
357176
  }
357177
+ var XPATH_ASCII_UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
357178
+ var XPATH_ASCII_LOWER = "abcdefghijklmnopqrstuvwxyz";
357088
357179
  function identityTextXpath2(value) {
357089
357180
  const lit = xpathLiteral3(value);
357090
357181
  return `//*[@text=${lit} or @content-desc=${lit} or @label=${lit} or @name=${lit} or @value=${lit}]`;
357091
357182
  }
357183
+ function identityTextXpathCaseInsensitive(value) {
357184
+ const lit = xpathLiteral3(value.toLowerCase());
357185
+ const fold = (attr) => `translate(@${attr}, '${XPATH_ASCII_UPPER}', '${XPATH_ASCII_LOWER}')=${lit}`;
357186
+ return `//*[${fold("text")} or ${fold("content-desc")} or ${fold("label")} or ${fold("name")} or ${fold("value")}]`;
357187
+ }
357092
357188
  function iosPredicateLiteral(value) {
357093
357189
  return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
357094
357190
  }
@@ -357096,19 +357192,28 @@ function iosTextIdentityPredicate(value) {
357096
357192
  const lit = iosPredicateLiteral(value);
357097
357193
  return `label == ${lit} OR name == ${lit} OR value == ${lit}`;
357098
357194
  }
357195
+ function iosTextIdentityPredicateCaseInsensitive(value) {
357196
+ const lit = iosPredicateLiteral(value);
357197
+ return `label ==[c] ${lit} OR name ==[c] ${lit} OR value ==[c] ${lit}`;
357198
+ }
357099
357199
  function textIdentityStrategies(value, platform3) {
357100
357200
  const strategies = [];
357101
357201
  if (platform3 === "IOS") {
357102
357202
  strategies.push({ using: "-ios predicate string", value: iosTextIdentityPredicate(value) });
357203
+ strategies.push({ using: "-ios predicate string", value: iosTextIdentityPredicateCaseInsensitive(value) });
357103
357204
  }
357104
357205
  strategies.push({ using: "xpath", value: identityTextXpath2(value) });
357206
+ strategies.push({ using: "xpath", value: identityTextXpathCaseInsensitive(value) });
357105
357207
  return strategies;
357106
357208
  }
357107
357209
  function isIdentityIosPredicate(value) {
357108
- return /(?:^|[\s(])(?:name|label|value)\s*==\s*"/.test(value) && !/\b(?:CONTAINS|BEGINSWITH|ENDSWITH|LIKE|MATCHES|IN)\b/i.test(value);
357210
+ return /(?:^|[\s(])(?:name|label|value)\s*==(?:\[[cC]\])?\s*"/.test(value) && !/\b(?:CONTAINS|BEGINSWITH|ENDSWITH|LIKE|MATCHES|IN)\b/i.test(value);
357109
357211
  }
357110
357212
  function isIdentityXpath(value) {
357111
- return /@[A-Za-z_:][-A-Za-z0-9_:.]*\s*=/.test(value) && !/(?:contains|starts-with)\s*\(/i.test(value);
357213
+ if (/(?:contains|starts-with)\s*\(/i.test(value)) return false;
357214
+ if (/@[A-Za-z_:][-A-Za-z0-9_:.]*\s*=/.test(value)) return true;
357215
+ if (/translate\s*\(\s*@[A-Za-z_:][-A-Za-z0-9_:.]*\s*,/.test(value) && /\)\s*=/.test(value)) return true;
357216
+ return false;
357112
357217
  }
357113
357218
  function isIdentityStrategy(strategy) {
357114
357219
  if (strategy.using === "accessibility id" || strategy.using === "id") return true;
@@ -357931,7 +358036,12 @@ function mobileMjpegPortForDevice(platform3, deviceId, env = process.env) {
357931
358036
  return typeof port === "number" && Number.isFinite(port) ? port : null;
357932
358037
  }
357933
358038
  var PINNED_DEVICE_SNAPSHOT_TTL_MS = 25 * 6e4;
357934
- var pinnedDeviceSnapshot = null;
358039
+ var pinnedDeviceSnapshots = /* @__PURE__ */ new Map();
358040
+ function prunePinnedDeviceSnapshots(now = Date.now()) {
358041
+ for (const [id, entry] of pinnedDeviceSnapshots) {
358042
+ if (now - entry.at >= PINNED_DEVICE_SNAPSHOT_TTL_MS) pinnedDeviceSnapshots.delete(id);
358043
+ }
358044
+ }
357935
358045
  async function executeMobileTest(options) {
357936
358046
  const logLines = [];
357937
358047
  const log2 = (msg) => logLines.push(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}`);
@@ -358006,8 +358116,10 @@ async function executeMobileTest(options) {
358006
358116
  }
358007
358117
  const platformFilter = options.target.platform;
358008
358118
  const pinnedDeviceId = options.target.assignedDeviceId;
358009
- const candidateSnapshot = pinnedDeviceSnapshot;
358010
- const reusablePinnedSnapshot = pinnedDeviceId && candidateSnapshot && candidateSnapshot.deviceId === pinnedDeviceId && Date.now() - candidateSnapshot.at < PINNED_DEVICE_SNAPSHOT_TTL_MS ? candidateSnapshot : null;
358119
+ const now = Date.now();
358120
+ prunePinnedDeviceSnapshots(now);
358121
+ const candidateSnapshot = pinnedDeviceId ? pinnedDeviceSnapshots.get(pinnedDeviceId) : void 0;
358122
+ const reusablePinnedSnapshot = pinnedDeviceId && candidateSnapshot && now - candidateSnapshot.at < PINNED_DEVICE_SNAPSHOT_TTL_MS ? candidateSnapshot : null;
358011
358123
  if (reusablePinnedSnapshot) {
358012
358124
  assertDeviceNotQuarantined(reusablePinnedSnapshot.device.id);
358013
358125
  selectedDevice = reusablePinnedSnapshot.device;
@@ -358023,22 +358135,24 @@ async function executeMobileTest(options) {
358023
358135
  `No ${platformFilter} ${platformFilter === "IOS" ? "simulator" : "emulator"} booted. Start one via ${platformFilter === "IOS" ? "Simulator.app" : "Android Studio"} first.`
358024
358136
  );
358025
358137
  }
358026
- if (options.target.assignedDeviceId) {
358027
- selectedDevice = matchingDevices.find((d) => d.id === options.target.assignedDeviceId);
358028
- if (!selectedDevice) {
358029
- throw new Error(
358030
- `Assigned ${platformFilter} device ${options.target.assignedDeviceId} is no longer booted/available (booted: ${matchingDevices.map((d) => d.id).join(", ") || "none"}). Refusing to fall back to another device.`
358031
- );
358032
- }
358033
- }
358034
- if (!selectedDevice && options.target.recordedDeviceId) {
358035
- selectedDevice = matchingDevices.find((d) => d.id === options.target.recordedDeviceId);
358138
+ if (!options.target.assignedDeviceId) {
358139
+ throw new Error(
358140
+ "CONFIG_ISSUE: mobile test requires an assigned device lease; refusing unpinned session create."
358141
+ );
358036
358142
  }
358143
+ selectedDevice = matchingDevices.find((d) => d.id === options.target.assignedDeviceId);
358037
358144
  if (!selectedDevice) {
358038
- selectedDevice = matchingDevices[0];
358145
+ throw new Error(
358146
+ `Assigned ${platformFilter} device ${options.target.assignedDeviceId} is no longer booted/available (booted: ${matchingDevices.map((d) => d.id).join(", ") || "none"}). Refusing to fall back to another device.`
358147
+ );
358039
358148
  }
358040
358149
  log2(`Using device: ${selectedDevice.name} (${selectedDevice.id})`);
358041
- pinnedDeviceSnapshot = options.target.assignedDeviceId ? { deviceId: options.target.assignedDeviceId, device: selectedDevice, at: Date.now() } : null;
358150
+ if (options.target.assignedDeviceId) {
358151
+ pinnedDeviceSnapshots.set(options.target.assignedDeviceId, {
358152
+ device: selectedDevice,
358153
+ at: Date.now()
358154
+ });
358155
+ }
358042
358156
  }
358043
358157
  try {
358044
358158
  capture = await startMobileNetworkCapture({
@@ -358374,20 +358488,16 @@ async function createMobileDiscoverySession(target, options) {
358374
358488
  `No ${platformFilter} ${platformFilter === "IOS" ? "simulator" : "emulator"} booted. Start one via ${platformFilter === "IOS" ? "Simulator.app" : "Android Studio"} first.`
358375
358489
  );
358376
358490
  }
358377
- let selectedDevice;
358378
- if (target.assignedDeviceId) {
358379
- selectedDevice = matchingDevices.find((d) => d.id === target.assignedDeviceId);
358380
- if (!selectedDevice) {
358381
- throw new Error(
358382
- `CONFIG_ISSUE: assigned ${platformFilter} device ${target.assignedDeviceId} is no longer booted for mobile discovery; will retry.`
358383
- );
358384
- }
358385
- }
358386
- if (!selectedDevice && target.recordedDeviceId) {
358387
- selectedDevice = matchingDevices.find((d) => d.id === target.recordedDeviceId);
358491
+ if (!target.assignedDeviceId) {
358492
+ throw new Error(
358493
+ "CONFIG_ISSUE: mobile discovery requires an assigned device lease; refusing unpinned session create."
358494
+ );
358388
358495
  }
358496
+ const selectedDevice = matchingDevices.find((d) => d.id === target.assignedDeviceId);
358389
358497
  if (!selectedDevice) {
358390
- selectedDevice = matchingDevices[0];
358498
+ throw new Error(
358499
+ `CONFIG_ISSUE: assigned ${platformFilter} device ${target.assignedDeviceId} is no longer booted for mobile discovery; will retry.`
358500
+ );
358391
358501
  }
358392
358502
  if (selectedDevice.installedApps && selectedDevice.installedApps.length > 0 && !selectedDevice.installedApps.includes(target.appId)) {
358393
358503
  throw new Error(
@@ -358829,6 +358939,10 @@ function computeMobileCapacity(input) {
358829
358939
  }
358830
358940
  return Math.max(0, cap);
358831
358941
  }
358942
+ function discoveryKindOf(ctx) {
358943
+ if (!isNativeMobileDiscoveryContext(ctx)) return null;
358944
+ return ctx?.mode === "survey" ? "survey" : "deep";
358945
+ }
358832
358946
  function decideMobileDeviceGate(input) {
358833
358947
  const {
358834
358948
  runMode,
@@ -358839,8 +358953,10 @@ function decideMobileDeviceGate(input) {
358839
358953
  readyDevices,
358840
358954
  capacity,
358841
358955
  freeDeviceId,
358842
- isDiscovery,
358843
- activeMobileDiscoveries
358956
+ discoveryKind,
358957
+ activeMobileSurveys,
358958
+ activeMobileDeeps,
358959
+ maxMobileDeeps
358844
358960
  } = input;
358845
358961
  if (!platform3) {
358846
358962
  return { claim: false, disposition: "pending", reason: "mobile run has no supported target platform" };
@@ -358852,8 +358968,18 @@ function decideMobileDeviceGate(input) {
358852
358968
  reason: `no booted ${platform3 === "IOS" ? "iOS" : "Android"} device for mobile run`
358853
358969
  };
358854
358970
  }
358855
- if (isDiscovery && activeMobileDiscoveries >= 1) {
358856
- return { claim: false, disposition: "defer", reason: "native discovery serialized to the shared live-mirror" };
358971
+ if (discoveryKind === "survey" && activeMobileSurveys >= 1) {
358972
+ return { claim: false, disposition: "defer", reason: "native survey serialized to 1 concurrent" };
358973
+ }
358974
+ if (discoveryKind === "deep") {
358975
+ const deepCap = Math.max(0, maxMobileDeeps);
358976
+ if (activeMobileDeeps >= deepCap) {
358977
+ return {
358978
+ claim: false,
358979
+ disposition: "defer",
358980
+ reason: `native deep discovery capacity reached: ${activeMobileDeeps}/${deepCap}`
358981
+ };
358982
+ }
358857
358983
  }
358858
358984
  const isHealForHeldTest = runMode === "heal" && !!testCaseId && healHoldTestCaseIds.has(testCaseId);
358859
358985
  const reservedForHeals = isHealForHeldTest ? Math.max(0, healHoldTestCaseIds.size - 1) : healHoldTestCaseIds.size;
@@ -361261,11 +361387,12 @@ var RUNNER_BUILD_ID = process.env.VALIDATE_RUNNER_BUILD_ID ?? process.env.RAILWA
361261
361387
  var DEFAULT_MAX_CONCURRENT = IS_CLOUD ? 4 : 40;
361262
361388
  var DEFAULT_TYPE_LIMITS = {
361263
361389
  discovery: IS_CLOUD ? 2 : 15,
361264
- // Native mobile discovery is single-device (one Appium session per runner), so
361265
- // it is always capped at 1 regardless of cloud/local — the shared-device mutex
361266
- // (setExecutorBusy + getBridgeState().hasActiveSession) already serializes it,
361267
- // and this static limit documents that ceiling for the resource governor.
361268
- "mobile-discovery": 1,
361390
+ // Max concurrent native deep discoveries on one local runner (survey is always
361391
+ // 1 via decideMobileDeviceGate). Applied by the device gate as maxMobileDeeps
361392
+ // (MAX_MOBILE_DISCOVERY); the resource-governor type key is run.mode='discovery'
361393
+ // and does NOT read this entry — do not treat TYPE_LIMITS['mobile-discovery'] as
361394
+ // the live governor. Env default 2 enables two deeps when two free devices exist.
361395
+ "mobile-discovery": 2,
361269
361396
  heal: IS_CLOUD ? 2 : 8,
361270
361397
  "regression-suite": IS_CLOUD ? 1 : 2,
361271
361398
  "system-regression-suite": IS_CLOUD ? 1 : 2,
@@ -361285,7 +361412,7 @@ function envInt(name, fallback) {
361285
361412
  }
361286
361413
  var MOBILE_DEVICE_CEILING = Math.max(0, envInt("MOBILE_DEVICE_CEILING", 0));
361287
361414
  var MOBILE_HEAL_HOLD_MS = Math.max(0, envInt("MOBILE_HEAL_HOLD_MS", 2e4));
361288
- var MOBILE_RUN_WATCHDOG_MS = Math.max(0, envInt("MOBILE_RUN_WATCHDOG_MS", 30 * 6e4));
361415
+ var MOBILE_RUN_WATCHDOG_MS = Math.max(0, envInt("MOBILE_RUN_WATCHDOG_MS", 60 * 6e4));
361289
361416
  var MOBILE_RUN_ABORT_GRACE_MS = Math.max(0, envInt("MOBILE_RUN_ABORT_GRACE_MS", 2 * 6e4));
361290
361417
  var MOBILE_CANCEL_POLL_MS = Math.max(0, envInt("MOBILE_CANCEL_POLL_MS", 15e3));
361291
361418
  var MobileRunWatchdog = class {
@@ -362914,10 +363041,10 @@ ${finalVerifyResult.logs ?? ""}`;
362914
363041
  log.fail("ERROR - malformed locator-discovery context");
362915
363042
  return;
362916
363043
  }
362917
- if (getBridgeState().hasActiveSession) {
363044
+ if (getBridgeState().recordingActive) {
362918
363045
  await postResult(opts.serverUrl, headers, run2.runId, {
362919
363046
  status: "ERROR",
362920
- error: "CONFIG_ISSUE: the shared mobile device is busy with an active recording session; retry once it ends."
363047
+ error: "CONFIG_ISSUE: a mobile recording session is active; retry once it ends."
362921
363048
  });
362922
363049
  log.fail("ERROR - device busy (recording) for locator discovery");
362923
363050
  return;
@@ -363346,6 +363473,11 @@ ${finalVerifyResult.logs ?? ""}`;
363346
363473
  if (!mobileTarget || !mobileTarget.appId) {
363347
363474
  throw new Error("Native mobile discovery requires a target with an appId (bundleId/appPackage).");
363348
363475
  }
363476
+ if (!assignedDeviceId) {
363477
+ throw new Error(
363478
+ "CONFIG_ISSUE: mobile discovery was claimed without a device lease; refusing unpinned session create."
363479
+ );
363480
+ }
363349
363481
  const targetPlatform = mobileTarget.platform;
363350
363482
  const platform3 = mobileMedium === "IOS_NATIVE" ? "IOS" : mobileMedium === "ANDROID_NATIVE" ? "ANDROID" : targetPlatform === "IOS" || targetPlatform === "ANDROID" ? targetPlatform : null;
363351
363483
  if (!platform3) {
@@ -363354,8 +363486,8 @@ ${finalVerifyResult.logs ?? ""}`;
363354
363486
  if (mobileMedium === "IOS_NATIVE" && targetPlatform === "ANDROID" || mobileMedium === "ANDROID_NATIVE" && targetPlatform === "IOS") {
363355
363487
  throw new Error("Native mobile discovery medium does not match target.platform.");
363356
363488
  }
363357
- if (getBridgeState().hasActiveSession) {
363358
- throw new Error("CONFIG_ISSUE: the shared mobile device is busy with an active recording session; mobile discovery will retry once it ends.");
363489
+ if (getBridgeState().recordingActive) {
363490
+ throw new Error("CONFIG_ISSUE: a mobile recording session is active; mobile discovery will retry once it ends.");
363359
363491
  }
363360
363492
  const mobileProgressLines = [];
363361
363493
  const mobileOnLog = (line) => {
@@ -363833,10 +363965,10 @@ ${finalVerifyResult.logs ?? ""}`;
363833
363965
  return;
363834
363966
  }
363835
363967
  if (runMode === "heal" && run2.mobileAiHeal === true) {
363836
- if (getBridgeState().hasActiveSession) {
363968
+ if (getBridgeState().recordingActive) {
363837
363969
  await postResult(opts.serverUrl, headers, run2.runId, {
363838
363970
  status: "ERROR",
363839
- error: "CONFIG_ISSUE: the shared mobile device is busy with an active recording session; the AI heal will retry once it ends.",
363971
+ error: "CONFIG_ISSUE: a mobile recording session is active; the AI heal will retry once it ends.",
363840
363972
  duration: Date.now() - startTime
363841
363973
  });
363842
363974
  log.warn("Skipped mobile AI heal (device busy with a recording)");
@@ -364574,7 +364706,9 @@ async function startRunner(opts) {
364574
364706
  };
364575
364707
  const leasedDeviceIds = /* @__PURE__ */ new Map();
364576
364708
  const activeMobileRuns = /* @__PURE__ */ new Map();
364577
- let activeMobileDiscoveries = 0;
364709
+ let activeMobileSurveys = 0;
364710
+ let activeMobileDeeps = 0;
364711
+ const MAX_MOBILE_DEEPS = Math.max(0, envInt("MAX_MOBILE_DISCOVERY", DEFAULT_TYPE_LIMITS["mobile-discovery"]));
364578
364712
  const mobileHealHold = /* @__PURE__ */ new Map();
364579
364713
  const pruneMobileHealHolds = () => {
364580
364714
  if (mobileHealHold.size === 0) return;
@@ -364919,7 +365053,7 @@ async function startRunner(opts) {
364919
365053
  if (activeRuns >= MAX_CONCURRENT) break;
364920
365054
  const runMode = run2.mode ?? "run";
364921
365055
  const isMobileOp = enableMobile && isMobileDeviceOp(run2);
364922
- const isDiscovery = isMobileOp && isNativeMobileDiscoveryContext(run2.discoveryContext);
365056
+ const discoveryKind = isMobileOp ? discoveryKindOf(run2.discoveryContext) : null;
364923
365057
  let leasedDeviceId = null;
364924
365058
  let mobileLeaseError;
364925
365059
  const runProject = projectKey(run2);
@@ -364938,7 +365072,7 @@ async function startRunner(opts) {
364938
365072
  logDeferredOnce(run2.runId, ` Deferred ${run2.runId} (${admission.reason})`);
364939
365073
  continue;
364940
365074
  }
364941
- if (enableMobile && run2.framework === "APPIUM" && getBridgeState().recordingActive) {
365075
+ if (enableMobile && isMobileOp && getBridgeState().recordingActive) {
364942
365076
  logDeferredOnce(run2.runId, ` Deferred ${run2.runId} (mobile recording session active)`);
364943
365077
  continue;
364944
365078
  }
@@ -364954,10 +365088,6 @@ async function startRunner(opts) {
364954
365088
  logDeferredOnce(run2.runId, ` Left PENDING ${run2.runId} (no booted ${discoveryMedium === "IOS_NATIVE" ? "iOS" : "Android"} device for native mobile discovery)`);
364955
365089
  continue;
364956
365090
  }
364957
- if (getBridgeState().hasActiveSession) {
364958
- logDeferredOnce(run2.runId, ` Deferred ${run2.runId} (mobile recording session active on shared device)`);
364959
- continue;
364960
- }
364961
365091
  }
364962
365092
  const mobilePlatform = isMobileOp ? mobilePlatformOf(run2) : null;
364963
365093
  if (isMobileOp && mobilePlatform) {
@@ -364999,13 +365129,18 @@ async function startRunner(opts) {
364999
365129
  readyDevices: virtual + physical,
365000
365130
  capacity,
365001
365131
  freeDeviceId,
365002
- isDiscovery,
365003
- activeMobileDiscoveries
365132
+ discoveryKind,
365133
+ activeMobileSurveys,
365134
+ activeMobileDeeps,
365135
+ maxMobileDeeps: MAX_MOBILE_DEEPS
365004
365136
  });
365005
365137
  if (!decision.claim) {
365006
365138
  logDeferredOnce(run2.runId, ` ${decision.disposition === "pending" ? "Left PENDING" : "Deferred"} ${run2.runId} (${decision.reason})`);
365007
365139
  continue;
365008
365140
  }
365141
+ log.dim(
365142
+ ` [mobile-gate] admit ${discoveryKind ?? runMode} device=${freeDeviceId} surveys=${activeMobileSurveys} deeps=${activeMobileDeeps}/${MAX_MOBILE_DEEPS} ops=${getActiveMobileOps(mobilePlatform)}/${capacity}`
365143
+ );
365009
365144
  leasedDeviceId = freeDeviceId;
365010
365145
  leasedDeviceIds.set(freeDeviceId, run2.runId);
365011
365146
  }
@@ -365033,7 +365168,8 @@ async function startRunner(opts) {
365033
365168
  incrementProject(runProject);
365034
365169
  if (isMobileOp) {
365035
365170
  if (mobilePlatform) bumpActiveMobileOps(mobilePlatform, 1);
365036
- if (isDiscovery) activeMobileDiscoveries++;
365171
+ if (discoveryKind === "survey") activeMobileSurveys++;
365172
+ else if (discoveryKind === "deep") activeMobileDeeps++;
365037
365173
  if (runMode === "heal" && run2.testCaseId) mobileHealHold.delete(run2.testCaseId);
365038
365174
  }
365039
365175
  if (claimedThisPoll > 0 && staggerMs > 0) {
@@ -365046,7 +365182,8 @@ async function startRunner(opts) {
365046
365182
  decrementProject(runProject);
365047
365183
  if (isMobileOp) {
365048
365184
  if (mobilePlatform) bumpActiveMobileOps(mobilePlatform, -1);
365049
- if (isDiscovery) activeMobileDiscoveries--;
365185
+ if (discoveryKind === "survey") activeMobileSurveys = Math.max(0, activeMobileSurveys - 1);
365186
+ else if (discoveryKind === "deep") activeMobileDeeps = Math.max(0, activeMobileDeeps - 1);
365050
365187
  if (leasedDeviceId) leasedDeviceIds.delete(leasedDeviceId);
365051
365188
  activeMobileRuns.delete(run2.runId);
365052
365189
  }