@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.mjs CHANGED
@@ -105535,7 +105535,7 @@ var require_typescript = __commonJS({
105535
105535
  walkUpParenthesizedTypesAndGetParentAndChild: () => walkUpParenthesizedTypesAndGetParentAndChild,
105536
105536
  whitespaceOrMapCommentRegExp: () => whitespaceOrMapCommentRegExp,
105537
105537
  writeCommentRange: () => writeCommentRange,
105538
- writeFile: () => writeFile2,
105538
+ writeFile: () => writeFile3,
105539
105539
  writeFileEnsuringDirectories: () => writeFileEnsuringDirectories,
105540
105540
  zipWith: () => zipWith
105541
105541
  });
@@ -111867,7 +111867,7 @@ ${lanes.join("\n")}
111867
111867
  writeOutputIsTTY() {
111868
111868
  return process.stdout.isTTY;
111869
111869
  },
111870
- readFile: readFile2,
111870
+ readFile: readFile3,
111871
111871
  writeFile: writeFile22,
111872
111872
  watchFile: watchFile2,
111873
111873
  watchDirectory,
@@ -112073,7 +112073,7 @@ ${lanes.join("\n")}
112073
112073
  callback
112074
112074
  );
112075
112075
  }
112076
- function readFile2(fileName, _encoding) {
112076
+ function readFile3(fileName, _encoding) {
112077
112077
  let buffer;
112078
112078
  try {
112079
112079
  buffer = _fs.readFileSync(fileName);
@@ -124178,7 +124178,7 @@ ${lanes.join("\n")}
124178
124178
  sourceFilePath = isSourceFileInCommonSourceDirectory ? sourceFilePath.substring(commonSourceDirectory.length) : sourceFilePath;
124179
124179
  return combinePaths(newDirPath, sourceFilePath);
124180
124180
  }
124181
- function writeFile2(host, diagnostics, fileName, text, writeByteOrderMark, sourceFiles, data) {
124181
+ function writeFile3(host, diagnostics, fileName, text, writeByteOrderMark, sourceFiles, data) {
124182
124182
  host.writeFile(
124183
124183
  fileName,
124184
124184
  text,
@@ -148629,7 +148629,7 @@ ${lanes.join("\n")}
148629
148629
  const possibleOption = getSpellingSuggestion(unknownOption, diagnostics.optionDeclarations, getOptionName);
148630
148630
  return possibleOption ? createDiagnosticForNodeInSourceFileOrCompilerDiagnostic(sourceFile, node, diagnostics.unknownDidYouMeanDiagnostic, unknownOptionErrorText || unknownOption, possibleOption.name) : createDiagnosticForNodeInSourceFileOrCompilerDiagnostic(sourceFile, node, diagnostics.unknownOptionDiagnostic, unknownOptionErrorText || unknownOption);
148631
148631
  }
148632
- function parseCommandLineWorker(diagnostics, commandLine, readFile2) {
148632
+ function parseCommandLineWorker(diagnostics, commandLine, readFile3) {
148633
148633
  const options = {};
148634
148634
  let watchOptions;
148635
148635
  const fileNames = [];
@@ -148677,7 +148677,7 @@ ${lanes.join("\n")}
148677
148677
  }
148678
148678
  }
148679
148679
  function parseResponseFile(fileName) {
148680
- const text = tryReadFile(fileName, readFile2 || ((fileName2) => sys.readFile(fileName2)));
148680
+ const text = tryReadFile(fileName, readFile3 || ((fileName2) => sys.readFile(fileName2)));
148681
148681
  if (!isString(text)) {
148682
148682
  errors2.push(text);
148683
148683
  return;
@@ -148780,8 +148780,8 @@ ${lanes.join("\n")}
148780
148780
  unknownDidYouMeanDiagnostic: Diagnostics.Unknown_compiler_option_0_Did_you_mean_1,
148781
148781
  optionTypeMismatchDiagnostic: Diagnostics.Compiler_option_0_expects_an_argument
148782
148782
  };
148783
- function parseCommandLine(commandLine, readFile2) {
148784
- return parseCommandLineWorker(compilerOptionsDidYouMeanDiagnostics, commandLine, readFile2);
148783
+ function parseCommandLine(commandLine, readFile3) {
148784
+ return parseCommandLineWorker(compilerOptionsDidYouMeanDiagnostics, commandLine, readFile3);
148785
148785
  }
148786
148786
  function getOptionFromName(optionName, allowShort) {
148787
148787
  return getOptionDeclarationFromName(getOptionsNameMap, optionName, allowShort);
@@ -148863,8 +148863,8 @@ ${lanes.join("\n")}
148863
148863
  watchOptionsToExtend
148864
148864
  );
148865
148865
  }
148866
- function readConfigFile(fileName, readFile2) {
148867
- const textOrDiagnostic = tryReadFile(fileName, readFile2);
148866
+ function readConfigFile(fileName, readFile3) {
148867
+ const textOrDiagnostic = tryReadFile(fileName, readFile3);
148868
148868
  return isString(textOrDiagnostic) ? parseConfigFileTextToJson(fileName, textOrDiagnostic) : { config: {}, error: textOrDiagnostic };
148869
148869
  }
148870
148870
  function parseConfigFileTextToJson(fileName, jsonText) {
@@ -148879,14 +148879,14 @@ ${lanes.join("\n")}
148879
148879
  error: jsonSourceFile.parseDiagnostics.length ? jsonSourceFile.parseDiagnostics[0] : void 0
148880
148880
  };
148881
148881
  }
148882
- function readJsonConfigFile(fileName, readFile2) {
148883
- const textOrDiagnostic = tryReadFile(fileName, readFile2);
148882
+ function readJsonConfigFile(fileName, readFile3) {
148883
+ const textOrDiagnostic = tryReadFile(fileName, readFile3);
148884
148884
  return isString(textOrDiagnostic) ? parseJsonText(fileName, textOrDiagnostic) : { fileName, parseDiagnostics: [textOrDiagnostic] };
148885
148885
  }
148886
- function tryReadFile(fileName, readFile2) {
148886
+ function tryReadFile(fileName, readFile3) {
148887
148887
  let text;
148888
148888
  try {
148889
- text = readFile2(fileName);
148889
+ text = readFile3(fileName);
148890
148890
  } catch (e) {
148891
148891
  return createCompilerDiagnostic(Diagnostics.Cannot_read_file_0_Colon_1, fileName, e.message);
148892
148892
  }
@@ -232929,7 +232929,7 @@ ${lanes.join("\n")}
232929
232929
  return;
232930
232930
  }
232931
232931
  const buildInfo = host.getBuildInfo() || { version };
232932
- writeFile2(
232932
+ writeFile3(
232933
232933
  host,
232934
232934
  emitterDiagnostics,
232935
232935
  buildInfoPath,
@@ -233141,7 +233141,7 @@ ${lanes.join("\n")}
233141
233141
  }
233142
233142
  if (sourceMapFilePath) {
233143
233143
  const sourceMap = sourceMapGenerator.toString();
233144
- writeFile2(
233144
+ writeFile3(
233145
233145
  host,
233146
233146
  emitterDiagnostics,
233147
233147
  sourceMapFilePath,
@@ -233156,7 +233156,7 @@ ${lanes.join("\n")}
233156
233156
  }
233157
233157
  const text = writer.getText();
233158
233158
  const data = { sourceMapUrlPos, diagnostics: transform2.diagnostics };
233159
- writeFile2(host, emitterDiagnostics, jsFilePath, text, !!compilerOptions.emitBOM, sourceFiles, data);
233159
+ writeFile3(host, emitterDiagnostics, jsFilePath, text, !!compilerOptions.emitBOM, sourceFiles, data);
233160
233160
  writer.clear();
233161
233161
  return !data.skippedDtsWrite;
233162
233162
  }
@@ -238544,12 +238544,12 @@ ${lanes.join("\n")}
238544
238544
  function createCompilerHost(options, setParentNodes) {
238545
238545
  return createCompilerHostWorker(options, setParentNodes);
238546
238546
  }
238547
- function createGetSourceFile(readFile2, setParentNodes) {
238547
+ function createGetSourceFile(readFile3, setParentNodes) {
238548
238548
  return (fileName, languageVersionOrOptions, onError) => {
238549
238549
  let text;
238550
238550
  try {
238551
238551
  mark("beforeIORead");
238552
- text = readFile2(fileName);
238552
+ text = readFile3(fileName);
238553
238553
  mark("afterIORead");
238554
238554
  measure("I/O Read", "beforeIORead", "afterIORead");
238555
238555
  } catch (e) {
@@ -239453,7 +239453,7 @@ ${lanes.join("\n")}
239453
239453
  getRedirectFromOutput,
239454
239454
  forEachResolvedProjectReference: forEachResolvedProjectReference2
239455
239455
  });
239456
- const readFile2 = host.readFile.bind(host);
239456
+ const readFile3 = host.readFile.bind(host);
239457
239457
  (_e = tracing) == null ? void 0 : _e.push(tracing.Phase.Program, "shouldProgramCreateNewSourceFiles", { hasOldProgram: !!oldProgram });
239458
239458
  const shouldCreateNewSourceFile = shouldProgramCreateNewSourceFiles(oldProgram, options);
239459
239459
  (_f = tracing) == null ? void 0 : _f.pop();
@@ -239679,7 +239679,7 @@ ${lanes.join("\n")}
239679
239679
  shouldTransformImportCall,
239680
239680
  emitBuildInfo,
239681
239681
  fileExists,
239682
- readFile: readFile2,
239682
+ readFile: readFile3,
239683
239683
  directoryExists,
239684
239684
  getSymlinkCache,
239685
239685
  realpath: (_o = host.realpath) == null ? void 0 : _o.bind(host),
@@ -301038,7 +301038,7 @@ ${options.prefix}` : "\n" : options.prefix
301038
301038
  walkUpParenthesizedTypesAndGetParentAndChild: () => walkUpParenthesizedTypesAndGetParentAndChild,
301039
301039
  whitespaceOrMapCommentRegExp: () => whitespaceOrMapCommentRegExp,
301040
301040
  writeCommentRange: () => writeCommentRange,
301041
- writeFile: () => writeFile2,
301041
+ writeFile: () => writeFile3,
301042
301042
  writeFileEnsuringDirectories: () => writeFileEnsuringDirectories,
301043
301043
  zipWith: () => zipWith
301044
301044
  });
@@ -347540,7 +347540,7 @@ var require_mkdir = __commonJS({
347540
347540
  const gid = opt.gid;
347541
347541
  const doChown = typeof uid === "number" && typeof gid === "number" && (uid !== opt.processUid || gid !== opt.processGid);
347542
347542
  const preserve = opt.preserve;
347543
- const unlink2 = opt.unlink;
347543
+ const unlink3 = opt.unlink;
347544
347544
  const cache = opt.cache;
347545
347545
  const cwd = normPath(opt.cwd);
347546
347546
  const done = (er, created) => {
@@ -347568,33 +347568,33 @@ var require_mkdir = __commonJS({
347568
347568
  }
347569
347569
  const sub = normPath(path.relative(cwd, dir));
347570
347570
  const parts = sub.split("/");
347571
- mkdir_(cwd, parts, mode, cache, unlink2, cwd, null, done);
347571
+ mkdir_(cwd, parts, mode, cache, unlink3, cwd, null, done);
347572
347572
  };
347573
- var mkdir_ = (base2, parts, mode, cache, unlink2, cwd, created, cb) => {
347573
+ var mkdir_ = (base2, parts, mode, cache, unlink3, cwd, created, cb) => {
347574
347574
  if (!parts.length) {
347575
347575
  return cb(null, created);
347576
347576
  }
347577
347577
  const p = parts.shift();
347578
347578
  const part = normPath(path.resolve(base2 + "/" + p));
347579
347579
  if (cGet(cache, part)) {
347580
- return mkdir_(part, parts, mode, cache, unlink2, cwd, created, cb);
347580
+ return mkdir_(part, parts, mode, cache, unlink3, cwd, created, cb);
347581
347581
  }
347582
- fs.mkdir(part, mode, onmkdir(part, parts, mode, cache, unlink2, cwd, created, cb));
347582
+ fs.mkdir(part, mode, onmkdir(part, parts, mode, cache, unlink3, cwd, created, cb));
347583
347583
  };
347584
- var onmkdir = (part, parts, mode, cache, unlink2, cwd, created, cb) => (er) => {
347584
+ var onmkdir = (part, parts, mode, cache, unlink3, cwd, created, cb) => (er) => {
347585
347585
  if (er) {
347586
347586
  fs.lstat(part, (statEr, st) => {
347587
347587
  if (statEr) {
347588
347588
  statEr.path = statEr.path && normPath(statEr.path);
347589
347589
  cb(statEr);
347590
347590
  } else if (st.isDirectory()) {
347591
- mkdir_(part, parts, mode, cache, unlink2, cwd, created, cb);
347592
- } else if (unlink2) {
347591
+ mkdir_(part, parts, mode, cache, unlink3, cwd, created, cb);
347592
+ } else if (unlink3) {
347593
347593
  fs.unlink(part, (er2) => {
347594
347594
  if (er2) {
347595
347595
  return cb(er2);
347596
347596
  }
347597
- fs.mkdir(part, mode, onmkdir(part, parts, mode, cache, unlink2, cwd, created, cb));
347597
+ fs.mkdir(part, mode, onmkdir(part, parts, mode, cache, unlink3, cwd, created, cb));
347598
347598
  });
347599
347599
  } else if (st.isSymbolicLink()) {
347600
347600
  return cb(new SymlinkError(part, part + "/" + parts.join("/")));
@@ -347604,7 +347604,7 @@ var require_mkdir = __commonJS({
347604
347604
  });
347605
347605
  } else {
347606
347606
  created = created || part;
347607
- mkdir_(part, parts, mode, cache, unlink2, cwd, created, cb);
347607
+ mkdir_(part, parts, mode, cache, unlink3, cwd, created, cb);
347608
347608
  }
347609
347609
  };
347610
347610
  var checkCwdSync = (dir) => {
@@ -347629,7 +347629,7 @@ var require_mkdir = __commonJS({
347629
347629
  const gid = opt.gid;
347630
347630
  const doChown = typeof uid === "number" && typeof gid === "number" && (uid !== opt.processUid || gid !== opt.processGid);
347631
347631
  const preserve = opt.preserve;
347632
- const unlink2 = opt.unlink;
347632
+ const unlink3 = opt.unlink;
347633
347633
  const cache = opt.cache;
347634
347634
  const cwd = normPath(opt.cwd);
347635
347635
  const done = (created2) => {
@@ -347668,7 +347668,7 @@ var require_mkdir = __commonJS({
347668
347668
  if (st.isDirectory()) {
347669
347669
  cSet(cache, part, true);
347670
347670
  continue;
347671
- } else if (unlink2) {
347671
+ } else if (unlink3) {
347672
347672
  fs.unlinkSync(part);
347673
347673
  fs.mkdirSync(part, mode);
347674
347674
  created = created || part;
@@ -353342,7 +353342,7 @@ var require_package4 = __commonJS({
353342
353342
  "package.json"(exports, module) {
353343
353343
  module.exports = {
353344
353344
  name: "@validate.qa/runner",
353345
- version: "1.0.81",
353345
+ version: "1.0.83",
353346
353346
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
353347
353347
  bin: {
353348
353348
  "validate-runner": "dist/cli.js"
@@ -355291,6 +355291,7 @@ import { execFile as execFile3 } from "child_process";
355291
355291
  import { promisify as promisify3 } from "util";
355292
355292
  import { tmpdir as tmpdir3 } from "os";
355293
355293
  import { join as join5 } from "path";
355294
+ import { readFile as readFile2, unlink as unlink2, writeFile as writeFile2 } from "fs/promises";
355294
355295
 
355295
355296
  // src/services/device-discovery.ts
355296
355297
  import { execFileSync as execFileSync2 } from "child_process";
@@ -356727,6 +356728,12 @@ async function createSession(target) {
356727
356728
  // whole survey. The runner owns session teardown explicitly, so a long
356728
356729
  // idle window is safe. Env-overridable.
356729
356730
  "appium:newCommandTimeout": MOBILE_NEW_COMMAND_TIMEOUT_SECONDS,
356731
+ // Expo / universal-link "Open in App?" system sheets and permission dialogs
356732
+ // otherwise sit on top of the app and can stall WDA snapshot/command proxy
356733
+ // for the entire discovery session (observed: 20+ min hung XCUITest with
356734
+ // ECONNREFUSED on wdaLocalPort). Auto-accept keeps the AUT interactive.
356735
+ // Override with APPIUM_IOS_AUTO_ACCEPT_ALERTS=false for explicit dialog tests.
356736
+ ...process.env.APPIUM_IOS_AUTO_ACCEPT_ALERTS !== "false" ? { "appium:autoAcceptAlerts": true } : {},
356730
356737
  // Xcode 27+ relocates Simulator.app out of <DEVELOPER_DIR>/Applications, so
356731
356738
  // XCUITest's default "open Simulator UI" step fails ("Simulator.app does not
356732
356739
  // exist") on this toolchain. The simulator is already booted and driven
@@ -356982,20 +356989,102 @@ async function startScreenRecording(sessionId, platform3, log2) {
356982
356989
  if (platform3 === "ANDROID") {
356983
356990
  options.bitRate = readPositiveIntEnv("VALIDATEQA_MOBILE_RECORDING_BITRATE", 75e4, 1e5, 4e6);
356984
356991
  } else {
356985
- options.videoQuality = process.env.VALIDATEQA_MOBILE_RECORDING_IOS_QUALITY || "low";
356992
+ options.videoType = process.env.VALIDATEQA_MOBILE_RECORDING_IOS_VIDEO_TYPE || "libx264";
356993
+ options.videoQuality = process.env.VALIDATEQA_MOBILE_RECORDING_IOS_QUALITY || "medium";
356994
+ options.videoFilters = process.env.VALIDATEQA_MOBILE_RECORDING_IOS_FILTERS || "scale=trunc(iw/2)*2:trunc(ih/2)*2";
356986
356995
  }
356987
356996
  try {
356988
356997
  await wdRequest(`/session/${sessionId}/appium/start_recording_screen`, {
356989
356998
  method: "POST",
356990
356999
  body: JSON.stringify({ options })
356991
357000
  });
356992
- log2(`[mobile-video] screen recording started (${timeLimit}s max)`);
357001
+ log2(`[mobile-video] screen recording started (${timeLimit}s max${platform3 === "IOS" ? `, type=${options.videoType}` : ""})`);
356993
357002
  return true;
356994
357003
  } catch (err) {
357004
+ if (platform3 === "IOS" && options.videoType) {
357005
+ try {
357006
+ const fallback = {
357007
+ timeLimit: String(timeLimit),
357008
+ forceRestart: true,
357009
+ videoQuality: process.env.VALIDATEQA_MOBILE_RECORDING_IOS_QUALITY || "medium"
357010
+ };
357011
+ await wdRequest(`/session/${sessionId}/appium/start_recording_screen`, {
357012
+ method: "POST",
357013
+ body: JSON.stringify({ options: fallback })
357014
+ });
357015
+ log2(`[mobile-video] screen recording started with quality-only fallback (${timeLimit}s max); will re-encode on stop`);
357016
+ return true;
357017
+ } catch (fallbackErr) {
357018
+ log2(`[mobile-video] screen recording unavailable: ${fallbackErr instanceof Error ? fallbackErr.message : String(fallbackErr)}`);
357019
+ return false;
357020
+ }
357021
+ }
356995
357022
  log2(`[mobile-video] screen recording unavailable: ${err instanceof Error ? err.message : String(err)}`);
356996
357023
  return false;
356997
357024
  }
356998
357025
  }
357026
+ async function ensureBrowserPlayableMp4Base64(base64Video, log2) {
357027
+ if (process.env.VALIDATEQA_MOBILE_VIDEO_REENCODE === "0") {
357028
+ return base64Video;
357029
+ }
357030
+ const inPath = join5(tmpdir3(), `validateqa-mobile-rec-in-${Date.now()}-${Math.random().toString(36).slice(2)}.mp4`);
357031
+ const outPath = join5(tmpdir3(), `validateqa-mobile-rec-out-${Date.now()}-${Math.random().toString(36).slice(2)}.mp4`);
357032
+ try {
357033
+ await writeFile2(inPath, Buffer.from(base64Video, "base64"));
357034
+ try {
357035
+ const { stdout } = await execFileAsync3("ffprobe", [
357036
+ "-v",
357037
+ "error",
357038
+ "-select_streams",
357039
+ "v:0",
357040
+ "-show_entries",
357041
+ "stream=codec_name",
357042
+ "-of",
357043
+ "csv=p=0",
357044
+ inPath
357045
+ ], { timeout: 15e3 });
357046
+ const codec = stdout.trim().toLowerCase();
357047
+ if (codec === "h264" || codec === "avc1") {
357048
+ log2(`[mobile-video] recording already H.264 (${codec}); skipping re-encode`);
357049
+ return base64Video;
357050
+ }
357051
+ log2(`[mobile-video] re-encoding ${codec || "unknown"} \u2192 H.264 for browser preview`);
357052
+ } catch {
357053
+ log2("[mobile-video] codec probe failed; attempting H.264 re-encode anyway");
357054
+ }
357055
+ await execFileAsync3("ffmpeg", [
357056
+ "-y",
357057
+ "-i",
357058
+ inPath,
357059
+ "-vf",
357060
+ "scale=trunc(iw/2)*2:trunc(ih/2)*2",
357061
+ "-c:v",
357062
+ "libx264",
357063
+ "-pix_fmt",
357064
+ "yuv420p",
357065
+ "-preset",
357066
+ "veryfast",
357067
+ "-crf",
357068
+ "28",
357069
+ "-movflags",
357070
+ "+faststart",
357071
+ "-an",
357072
+ outPath
357073
+ ], { timeout: 12e4 });
357074
+ const encoded = await readFile2(outPath);
357075
+ if (encoded.length < 100) {
357076
+ log2("[mobile-video] re-encode produced empty file; keeping original");
357077
+ return base64Video;
357078
+ }
357079
+ log2(`[mobile-video] re-encoded for browser (${Math.round(encoded.length / 1024)}KB)`);
357080
+ return encoded.toString("base64");
357081
+ } catch (err) {
357082
+ log2(`[mobile-video] re-encode skipped/failed: ${err instanceof Error ? err.message : String(err)}`);
357083
+ return base64Video;
357084
+ } finally {
357085
+ await Promise.allSettled([unlink2(inPath), unlink2(outPath)]);
357086
+ }
357087
+ }
356999
357088
  async function stopScreenRecording(sessionId, log2) {
357000
357089
  try {
357001
357090
  const result = await wdRequest(`/session/${sessionId}/appium/stop_recording_screen`, {
@@ -357008,7 +357097,7 @@ async function stopScreenRecording(sessionId, log2) {
357008
357097
  return void 0;
357009
357098
  }
357010
357099
  log2(`[mobile-video] screen recording captured (${Math.round(value.length / 1024)}KB base64)`);
357011
- return value;
357100
+ return ensureBrowserPlayableMp4Base64(value, log2);
357012
357101
  } catch (err) {
357013
357102
  log2(`[mobile-video] screen recording stop failed: ${err instanceof Error ? err.message : String(err)}`);
357014
357103
  return void 0;
@@ -357093,10 +357182,17 @@ function xpathLiteral3(value) {
357093
357182
  const parts = value.split("'").map((p) => `'${p}'`);
357094
357183
  return `concat(${parts.join(`, "'", `)})`;
357095
357184
  }
357185
+ var XPATH_ASCII_UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
357186
+ var XPATH_ASCII_LOWER = "abcdefghijklmnopqrstuvwxyz";
357096
357187
  function identityTextXpath2(value) {
357097
357188
  const lit = xpathLiteral3(value);
357098
357189
  return `//*[@text=${lit} or @content-desc=${lit} or @label=${lit} or @name=${lit} or @value=${lit}]`;
357099
357190
  }
357191
+ function identityTextXpathCaseInsensitive(value) {
357192
+ const lit = xpathLiteral3(value.toLowerCase());
357193
+ const fold = (attr) => `translate(@${attr}, '${XPATH_ASCII_UPPER}', '${XPATH_ASCII_LOWER}')=${lit}`;
357194
+ return `//*[${fold("text")} or ${fold("content-desc")} or ${fold("label")} or ${fold("name")} or ${fold("value")}]`;
357195
+ }
357100
357196
  function iosPredicateLiteral(value) {
357101
357197
  return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
357102
357198
  }
@@ -357104,19 +357200,28 @@ function iosTextIdentityPredicate(value) {
357104
357200
  const lit = iosPredicateLiteral(value);
357105
357201
  return `label == ${lit} OR name == ${lit} OR value == ${lit}`;
357106
357202
  }
357203
+ function iosTextIdentityPredicateCaseInsensitive(value) {
357204
+ const lit = iosPredicateLiteral(value);
357205
+ return `label ==[c] ${lit} OR name ==[c] ${lit} OR value ==[c] ${lit}`;
357206
+ }
357107
357207
  function textIdentityStrategies(value, platform3) {
357108
357208
  const strategies = [];
357109
357209
  if (platform3 === "IOS") {
357110
357210
  strategies.push({ using: "-ios predicate string", value: iosTextIdentityPredicate(value) });
357211
+ strategies.push({ using: "-ios predicate string", value: iosTextIdentityPredicateCaseInsensitive(value) });
357111
357212
  }
357112
357213
  strategies.push({ using: "xpath", value: identityTextXpath2(value) });
357214
+ strategies.push({ using: "xpath", value: identityTextXpathCaseInsensitive(value) });
357113
357215
  return strategies;
357114
357216
  }
357115
357217
  function isIdentityIosPredicate(value) {
357116
- return /(?:^|[\s(])(?:name|label|value)\s*==\s*"/.test(value) && !/\b(?:CONTAINS|BEGINSWITH|ENDSWITH|LIKE|MATCHES|IN)\b/i.test(value);
357218
+ return /(?:^|[\s(])(?:name|label|value)\s*==(?:\[[cC]\])?\s*"/.test(value) && !/\b(?:CONTAINS|BEGINSWITH|ENDSWITH|LIKE|MATCHES|IN)\b/i.test(value);
357117
357219
  }
357118
357220
  function isIdentityXpath(value) {
357119
- return /@[A-Za-z_:][-A-Za-z0-9_:.]*\s*=/.test(value) && !/(?:contains|starts-with)\s*\(/i.test(value);
357221
+ if (/(?:contains|starts-with)\s*\(/i.test(value)) return false;
357222
+ if (/@[A-Za-z_:][-A-Za-z0-9_:.]*\s*=/.test(value)) return true;
357223
+ if (/translate\s*\(\s*@[A-Za-z_:][-A-Za-z0-9_:.]*\s*,/.test(value) && /\)\s*=/.test(value)) return true;
357224
+ return false;
357120
357225
  }
357121
357226
  function isIdentityStrategy(strategy) {
357122
357227
  if (strategy.using === "accessibility id" || strategy.using === "id") return true;
@@ -357939,7 +358044,12 @@ function mobileMjpegPortForDevice(platform3, deviceId, env = process.env) {
357939
358044
  return typeof port === "number" && Number.isFinite(port) ? port : null;
357940
358045
  }
357941
358046
  var PINNED_DEVICE_SNAPSHOT_TTL_MS = 25 * 6e4;
357942
- var pinnedDeviceSnapshot = null;
358047
+ var pinnedDeviceSnapshots = /* @__PURE__ */ new Map();
358048
+ function prunePinnedDeviceSnapshots(now = Date.now()) {
358049
+ for (const [id, entry] of pinnedDeviceSnapshots) {
358050
+ if (now - entry.at >= PINNED_DEVICE_SNAPSHOT_TTL_MS) pinnedDeviceSnapshots.delete(id);
358051
+ }
358052
+ }
357943
358053
  async function executeMobileTest(options) {
357944
358054
  const logLines = [];
357945
358055
  const log2 = (msg) => logLines.push(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}`);
@@ -358014,8 +358124,10 @@ async function executeMobileTest(options) {
358014
358124
  }
358015
358125
  const platformFilter = options.target.platform;
358016
358126
  const pinnedDeviceId = options.target.assignedDeviceId;
358017
- const candidateSnapshot = pinnedDeviceSnapshot;
358018
- const reusablePinnedSnapshot = pinnedDeviceId && candidateSnapshot && candidateSnapshot.deviceId === pinnedDeviceId && Date.now() - candidateSnapshot.at < PINNED_DEVICE_SNAPSHOT_TTL_MS ? candidateSnapshot : null;
358127
+ const now = Date.now();
358128
+ prunePinnedDeviceSnapshots(now);
358129
+ const candidateSnapshot = pinnedDeviceId ? pinnedDeviceSnapshots.get(pinnedDeviceId) : void 0;
358130
+ const reusablePinnedSnapshot = pinnedDeviceId && candidateSnapshot && now - candidateSnapshot.at < PINNED_DEVICE_SNAPSHOT_TTL_MS ? candidateSnapshot : null;
358019
358131
  if (reusablePinnedSnapshot) {
358020
358132
  assertDeviceNotQuarantined(reusablePinnedSnapshot.device.id);
358021
358133
  selectedDevice = reusablePinnedSnapshot.device;
@@ -358031,22 +358143,24 @@ async function executeMobileTest(options) {
358031
358143
  `No ${platformFilter} ${platformFilter === "IOS" ? "simulator" : "emulator"} booted. Start one via ${platformFilter === "IOS" ? "Simulator.app" : "Android Studio"} first.`
358032
358144
  );
358033
358145
  }
358034
- if (options.target.assignedDeviceId) {
358035
- selectedDevice = matchingDevices.find((d) => d.id === options.target.assignedDeviceId);
358036
- if (!selectedDevice) {
358037
- throw new Error(
358038
- `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.`
358039
- );
358040
- }
358041
- }
358042
- if (!selectedDevice && options.target.recordedDeviceId) {
358043
- selectedDevice = matchingDevices.find((d) => d.id === options.target.recordedDeviceId);
358146
+ if (!options.target.assignedDeviceId) {
358147
+ throw new Error(
358148
+ "CONFIG_ISSUE: mobile test requires an assigned device lease; refusing unpinned session create."
358149
+ );
358044
358150
  }
358151
+ selectedDevice = matchingDevices.find((d) => d.id === options.target.assignedDeviceId);
358045
358152
  if (!selectedDevice) {
358046
- selectedDevice = matchingDevices[0];
358153
+ throw new Error(
358154
+ `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.`
358155
+ );
358047
358156
  }
358048
358157
  log2(`Using device: ${selectedDevice.name} (${selectedDevice.id})`);
358049
- pinnedDeviceSnapshot = options.target.assignedDeviceId ? { deviceId: options.target.assignedDeviceId, device: selectedDevice, at: Date.now() } : null;
358158
+ if (options.target.assignedDeviceId) {
358159
+ pinnedDeviceSnapshots.set(options.target.assignedDeviceId, {
358160
+ device: selectedDevice,
358161
+ at: Date.now()
358162
+ });
358163
+ }
358050
358164
  }
358051
358165
  try {
358052
358166
  capture = await startMobileNetworkCapture({
@@ -358382,20 +358496,16 @@ async function createMobileDiscoverySession(target, options) {
358382
358496
  `No ${platformFilter} ${platformFilter === "IOS" ? "simulator" : "emulator"} booted. Start one via ${platformFilter === "IOS" ? "Simulator.app" : "Android Studio"} first.`
358383
358497
  );
358384
358498
  }
358385
- let selectedDevice;
358386
- if (target.assignedDeviceId) {
358387
- selectedDevice = matchingDevices.find((d) => d.id === target.assignedDeviceId);
358388
- if (!selectedDevice) {
358389
- throw new Error(
358390
- `CONFIG_ISSUE: assigned ${platformFilter} device ${target.assignedDeviceId} is no longer booted for mobile discovery; will retry.`
358391
- );
358392
- }
358393
- }
358394
- if (!selectedDevice && target.recordedDeviceId) {
358395
- selectedDevice = matchingDevices.find((d) => d.id === target.recordedDeviceId);
358499
+ if (!target.assignedDeviceId) {
358500
+ throw new Error(
358501
+ "CONFIG_ISSUE: mobile discovery requires an assigned device lease; refusing unpinned session create."
358502
+ );
358396
358503
  }
358504
+ const selectedDevice = matchingDevices.find((d) => d.id === target.assignedDeviceId);
358397
358505
  if (!selectedDevice) {
358398
- selectedDevice = matchingDevices[0];
358506
+ throw new Error(
358507
+ `CONFIG_ISSUE: assigned ${platformFilter} device ${target.assignedDeviceId} is no longer booted for mobile discovery; will retry.`
358508
+ );
358399
358509
  }
358400
358510
  if (selectedDevice.installedApps && selectedDevice.installedApps.length > 0 && !selectedDevice.installedApps.includes(target.appId)) {
358401
358511
  throw new Error(
@@ -358837,6 +358947,10 @@ function computeMobileCapacity(input) {
358837
358947
  }
358838
358948
  return Math.max(0, cap);
358839
358949
  }
358950
+ function discoveryKindOf(ctx) {
358951
+ if (!isNativeMobileDiscoveryContext(ctx)) return null;
358952
+ return ctx?.mode === "survey" ? "survey" : "deep";
358953
+ }
358840
358954
  function decideMobileDeviceGate(input) {
358841
358955
  const {
358842
358956
  runMode,
@@ -358847,8 +358961,10 @@ function decideMobileDeviceGate(input) {
358847
358961
  readyDevices,
358848
358962
  capacity,
358849
358963
  freeDeviceId,
358850
- isDiscovery,
358851
- activeMobileDiscoveries
358964
+ discoveryKind,
358965
+ activeMobileSurveys,
358966
+ activeMobileDeeps,
358967
+ maxMobileDeeps
358852
358968
  } = input;
358853
358969
  if (!platform3) {
358854
358970
  return { claim: false, disposition: "pending", reason: "mobile run has no supported target platform" };
@@ -358860,8 +358976,18 @@ function decideMobileDeviceGate(input) {
358860
358976
  reason: `no booted ${platform3 === "IOS" ? "iOS" : "Android"} device for mobile run`
358861
358977
  };
358862
358978
  }
358863
- if (isDiscovery && activeMobileDiscoveries >= 1) {
358864
- return { claim: false, disposition: "defer", reason: "native discovery serialized to the shared live-mirror" };
358979
+ if (discoveryKind === "survey" && activeMobileSurveys >= 1) {
358980
+ return { claim: false, disposition: "defer", reason: "native survey serialized to 1 concurrent" };
358981
+ }
358982
+ if (discoveryKind === "deep") {
358983
+ const deepCap = Math.max(0, maxMobileDeeps);
358984
+ if (activeMobileDeeps >= deepCap) {
358985
+ return {
358986
+ claim: false,
358987
+ disposition: "defer",
358988
+ reason: `native deep discovery capacity reached: ${activeMobileDeeps}/${deepCap}`
358989
+ };
358990
+ }
358865
358991
  }
358866
358992
  const isHealForHeldTest = runMode === "heal" && !!testCaseId && healHoldTestCaseIds.has(testCaseId);
358867
358993
  const reservedForHeals = isHealForHeldTest ? Math.max(0, healHoldTestCaseIds.size - 1) : healHoldTestCaseIds.size;
@@ -361269,11 +361395,12 @@ var RUNNER_BUILD_ID = process.env.VALIDATE_RUNNER_BUILD_ID ?? process.env.RAILWA
361269
361395
  var DEFAULT_MAX_CONCURRENT = IS_CLOUD ? 4 : 40;
361270
361396
  var DEFAULT_TYPE_LIMITS = {
361271
361397
  discovery: IS_CLOUD ? 2 : 15,
361272
- // Native mobile discovery is single-device (one Appium session per runner), so
361273
- // it is always capped at 1 regardless of cloud/local — the shared-device mutex
361274
- // (setExecutorBusy + getBridgeState().hasActiveSession) already serializes it,
361275
- // and this static limit documents that ceiling for the resource governor.
361276
- "mobile-discovery": 1,
361398
+ // Max concurrent native deep discoveries on one local runner (survey is always
361399
+ // 1 via decideMobileDeviceGate). Applied by the device gate as maxMobileDeeps
361400
+ // (MAX_MOBILE_DISCOVERY); the resource-governor type key is run.mode='discovery'
361401
+ // and does NOT read this entry — do not treat TYPE_LIMITS['mobile-discovery'] as
361402
+ // the live governor. Env default 2 enables two deeps when two free devices exist.
361403
+ "mobile-discovery": 2,
361277
361404
  heal: IS_CLOUD ? 2 : 8,
361278
361405
  "regression-suite": IS_CLOUD ? 1 : 2,
361279
361406
  "system-regression-suite": IS_CLOUD ? 1 : 2,
@@ -361293,7 +361420,7 @@ function envInt(name, fallback) {
361293
361420
  }
361294
361421
  var MOBILE_DEVICE_CEILING = Math.max(0, envInt("MOBILE_DEVICE_CEILING", 0));
361295
361422
  var MOBILE_HEAL_HOLD_MS = Math.max(0, envInt("MOBILE_HEAL_HOLD_MS", 2e4));
361296
- var MOBILE_RUN_WATCHDOG_MS = Math.max(0, envInt("MOBILE_RUN_WATCHDOG_MS", 30 * 6e4));
361423
+ var MOBILE_RUN_WATCHDOG_MS = Math.max(0, envInt("MOBILE_RUN_WATCHDOG_MS", 60 * 6e4));
361297
361424
  var MOBILE_RUN_ABORT_GRACE_MS = Math.max(0, envInt("MOBILE_RUN_ABORT_GRACE_MS", 2 * 6e4));
361298
361425
  var MOBILE_CANCEL_POLL_MS = Math.max(0, envInt("MOBILE_CANCEL_POLL_MS", 15e3));
361299
361426
  var MobileRunWatchdog = class {
@@ -362922,10 +363049,10 @@ ${finalVerifyResult.logs ?? ""}`;
362922
363049
  log.fail("ERROR - malformed locator-discovery context");
362923
363050
  return;
362924
363051
  }
362925
- if (getBridgeState().hasActiveSession) {
363052
+ if (getBridgeState().recordingActive) {
362926
363053
  await postResult(opts.serverUrl, headers, run2.runId, {
362927
363054
  status: "ERROR",
362928
- error: "CONFIG_ISSUE: the shared mobile device is busy with an active recording session; retry once it ends."
363055
+ error: "CONFIG_ISSUE: a mobile recording session is active; retry once it ends."
362929
363056
  });
362930
363057
  log.fail("ERROR - device busy (recording) for locator discovery");
362931
363058
  return;
@@ -363354,6 +363481,11 @@ ${finalVerifyResult.logs ?? ""}`;
363354
363481
  if (!mobileTarget || !mobileTarget.appId) {
363355
363482
  throw new Error("Native mobile discovery requires a target with an appId (bundleId/appPackage).");
363356
363483
  }
363484
+ if (!assignedDeviceId) {
363485
+ throw new Error(
363486
+ "CONFIG_ISSUE: mobile discovery was claimed without a device lease; refusing unpinned session create."
363487
+ );
363488
+ }
363357
363489
  const targetPlatform = mobileTarget.platform;
363358
363490
  const platform3 = mobileMedium === "IOS_NATIVE" ? "IOS" : mobileMedium === "ANDROID_NATIVE" ? "ANDROID" : targetPlatform === "IOS" || targetPlatform === "ANDROID" ? targetPlatform : null;
363359
363491
  if (!platform3) {
@@ -363362,8 +363494,8 @@ ${finalVerifyResult.logs ?? ""}`;
363362
363494
  if (mobileMedium === "IOS_NATIVE" && targetPlatform === "ANDROID" || mobileMedium === "ANDROID_NATIVE" && targetPlatform === "IOS") {
363363
363495
  throw new Error("Native mobile discovery medium does not match target.platform.");
363364
363496
  }
363365
- if (getBridgeState().hasActiveSession) {
363366
- throw new Error("CONFIG_ISSUE: the shared mobile device is busy with an active recording session; mobile discovery will retry once it ends.");
363497
+ if (getBridgeState().recordingActive) {
363498
+ throw new Error("CONFIG_ISSUE: a mobile recording session is active; mobile discovery will retry once it ends.");
363367
363499
  }
363368
363500
  const mobileProgressLines = [];
363369
363501
  const mobileOnLog = (line) => {
@@ -363841,10 +363973,10 @@ ${finalVerifyResult.logs ?? ""}`;
363841
363973
  return;
363842
363974
  }
363843
363975
  if (runMode === "heal" && run2.mobileAiHeal === true) {
363844
- if (getBridgeState().hasActiveSession) {
363976
+ if (getBridgeState().recordingActive) {
363845
363977
  await postResult(opts.serverUrl, headers, run2.runId, {
363846
363978
  status: "ERROR",
363847
- error: "CONFIG_ISSUE: the shared mobile device is busy with an active recording session; the AI heal will retry once it ends.",
363979
+ error: "CONFIG_ISSUE: a mobile recording session is active; the AI heal will retry once it ends.",
363848
363980
  duration: Date.now() - startTime
363849
363981
  });
363850
363982
  log.warn("Skipped mobile AI heal (device busy with a recording)");
@@ -364582,7 +364714,9 @@ async function startRunner(opts) {
364582
364714
  };
364583
364715
  const leasedDeviceIds = /* @__PURE__ */ new Map();
364584
364716
  const activeMobileRuns = /* @__PURE__ */ new Map();
364585
- let activeMobileDiscoveries = 0;
364717
+ let activeMobileSurveys = 0;
364718
+ let activeMobileDeeps = 0;
364719
+ const MAX_MOBILE_DEEPS = Math.max(0, envInt("MAX_MOBILE_DISCOVERY", DEFAULT_TYPE_LIMITS["mobile-discovery"]));
364586
364720
  const mobileHealHold = /* @__PURE__ */ new Map();
364587
364721
  const pruneMobileHealHolds = () => {
364588
364722
  if (mobileHealHold.size === 0) return;
@@ -364927,7 +365061,7 @@ async function startRunner(opts) {
364927
365061
  if (activeRuns >= MAX_CONCURRENT) break;
364928
365062
  const runMode = run2.mode ?? "run";
364929
365063
  const isMobileOp = enableMobile && isMobileDeviceOp(run2);
364930
- const isDiscovery = isMobileOp && isNativeMobileDiscoveryContext(run2.discoveryContext);
365064
+ const discoveryKind = isMobileOp ? discoveryKindOf(run2.discoveryContext) : null;
364931
365065
  let leasedDeviceId = null;
364932
365066
  let mobileLeaseError;
364933
365067
  const runProject = projectKey(run2);
@@ -364946,7 +365080,7 @@ async function startRunner(opts) {
364946
365080
  logDeferredOnce(run2.runId, ` Deferred ${run2.runId} (${admission.reason})`);
364947
365081
  continue;
364948
365082
  }
364949
- if (enableMobile && run2.framework === "APPIUM" && getBridgeState().recordingActive) {
365083
+ if (enableMobile && isMobileOp && getBridgeState().recordingActive) {
364950
365084
  logDeferredOnce(run2.runId, ` Deferred ${run2.runId} (mobile recording session active)`);
364951
365085
  continue;
364952
365086
  }
@@ -364962,10 +365096,6 @@ async function startRunner(opts) {
364962
365096
  logDeferredOnce(run2.runId, ` Left PENDING ${run2.runId} (no booted ${discoveryMedium === "IOS_NATIVE" ? "iOS" : "Android"} device for native mobile discovery)`);
364963
365097
  continue;
364964
365098
  }
364965
- if (getBridgeState().hasActiveSession) {
364966
- logDeferredOnce(run2.runId, ` Deferred ${run2.runId} (mobile recording session active on shared device)`);
364967
- continue;
364968
- }
364969
365099
  }
364970
365100
  const mobilePlatform = isMobileOp ? mobilePlatformOf(run2) : null;
364971
365101
  if (isMobileOp && mobilePlatform) {
@@ -365007,13 +365137,18 @@ async function startRunner(opts) {
365007
365137
  readyDevices: virtual + physical,
365008
365138
  capacity,
365009
365139
  freeDeviceId,
365010
- isDiscovery,
365011
- activeMobileDiscoveries
365140
+ discoveryKind,
365141
+ activeMobileSurveys,
365142
+ activeMobileDeeps,
365143
+ maxMobileDeeps: MAX_MOBILE_DEEPS
365012
365144
  });
365013
365145
  if (!decision.claim) {
365014
365146
  logDeferredOnce(run2.runId, ` ${decision.disposition === "pending" ? "Left PENDING" : "Deferred"} ${run2.runId} (${decision.reason})`);
365015
365147
  continue;
365016
365148
  }
365149
+ log.dim(
365150
+ ` [mobile-gate] admit ${discoveryKind ?? runMode} device=${freeDeviceId} surveys=${activeMobileSurveys} deeps=${activeMobileDeeps}/${MAX_MOBILE_DEEPS} ops=${getActiveMobileOps(mobilePlatform)}/${capacity}`
365151
+ );
365017
365152
  leasedDeviceId = freeDeviceId;
365018
365153
  leasedDeviceIds.set(freeDeviceId, run2.runId);
365019
365154
  }
@@ -365041,7 +365176,8 @@ async function startRunner(opts) {
365041
365176
  incrementProject(runProject);
365042
365177
  if (isMobileOp) {
365043
365178
  if (mobilePlatform) bumpActiveMobileOps(mobilePlatform, 1);
365044
- if (isDiscovery) activeMobileDiscoveries++;
365179
+ if (discoveryKind === "survey") activeMobileSurveys++;
365180
+ else if (discoveryKind === "deep") activeMobileDeeps++;
365045
365181
  if (runMode === "heal" && run2.testCaseId) mobileHealHold.delete(run2.testCaseId);
365046
365182
  }
365047
365183
  if (claimedThisPoll > 0 && staggerMs > 0) {
@@ -365054,7 +365190,8 @@ async function startRunner(opts) {
365054
365190
  decrementProject(runProject);
365055
365191
  if (isMobileOp) {
365056
365192
  if (mobilePlatform) bumpActiveMobileOps(mobilePlatform, -1);
365057
- if (isDiscovery) activeMobileDiscoveries--;
365193
+ if (discoveryKind === "survey") activeMobileSurveys = Math.max(0, activeMobileSurveys - 1);
365194
+ else if (discoveryKind === "deep") activeMobileDeeps = Math.max(0, activeMobileDeeps - 1);
365058
365195
  if (leasedDeviceId) leasedDeviceIds.delete(leasedDeviceId);
365059
365196
  activeMobileRuns.delete(run2.runId);
365060
365197
  }