@validate.qa/runner 1.0.72 → 1.0.74

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 +188 -47
  2. package/dist/cli.mjs +188 -47
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -5854,6 +5854,29 @@ function normalizeTtsVoiceId(voiceId) {
5854
5854
  return null;
5855
5855
  return LEGACY_TTS_VOICE_ID_MAP[trimmed] ?? trimmed;
5856
5856
  }
5857
+ function normalizeTtsModel(model) {
5858
+ if (!model)
5859
+ return null;
5860
+ const trimmed = model.trim();
5861
+ if (!trimmed)
5862
+ return null;
5863
+ return LEGACY_TTS_MODEL_MAP[trimmed] ?? trimmed;
5864
+ }
5865
+ function ttsFallbackModel(primary) {
5866
+ const normalized = normalizeTtsModel(primary);
5867
+ if (!normalized)
5868
+ return null;
5869
+ return TTS_FALLBACK_MODEL_MAP[normalized] ?? null;
5870
+ }
5871
+ function resolveTtsModelChain(model) {
5872
+ const normalized = normalizeTtsModel(model);
5873
+ const primary = normalized && normalized in TTS_FALLBACK_MODEL_MAP ? normalized : DEFAULT_TTS_MODEL;
5874
+ const chain = [primary];
5875
+ const fallback = TTS_FALLBACK_MODEL_MAP[primary];
5876
+ if (fallback && fallback !== primary)
5877
+ chain.push(fallback);
5878
+ return chain;
5879
+ }
5857
5880
  function sttModelsForProvider(provider) {
5858
5881
  if (provider === "openai") {
5859
5882
  return STT_MODEL_OPTIONS.filter((m) => m.provider === "openai");
@@ -5901,16 +5924,18 @@ function isValidTtsVoiceId(value) {
5901
5924
  return TTS_VOICE_OPTIONS.some((v) => v.id === normalized);
5902
5925
  }
5903
5926
  function isValidTtsModel(value) {
5904
- if (!value)
5927
+ const normalized = normalizeTtsModel(value);
5928
+ if (!normalized)
5905
5929
  return false;
5906
- return TTS_MODEL_OPTIONS.some((m) => m.id === value);
5930
+ return TTS_MODEL_OPTIONS.some((m) => m.id === normalized);
5907
5931
  }
5908
- var VOICE_PREVIEW_SAMPLE, DEFAULT_TTS_MODEL, DEFAULT_TTS_VOICE_ID, DEFAULT_TTS_SPEED, LEGACY_TTS_VOICE_ID_MAP, DEFAULT_STT_MODEL_BY_PROVIDER, STT_PROVIDER_OPTIONS, STT_MODEL_OPTIONS, TTS_MODEL_OPTIONS, TTS_VOICE_OPTIONS;
5932
+ var VOICE_PREVIEW_SAMPLE, DEFAULT_TTS_MODEL, DEFAULT_TTS_TURBO_MODEL, DEFAULT_TTS_VOICE_ID, DEFAULT_TTS_SPEED, LEGACY_TTS_VOICE_ID_MAP, LEGACY_TTS_MODEL_MAP, TTS_FALLBACK_MODEL_MAP, DEFAULT_STT_MODEL_BY_PROVIDER, STT_PROVIDER_OPTIONS, STT_MODEL_OPTIONS, TTS_MODEL_OPTIONS, TTS_VOICE_OPTIONS;
5909
5933
  var init_voice_config = __esm({
5910
5934
  "../shared/dist/voice-config.js"() {
5911
5935
  "use strict";
5912
5936
  VOICE_PREVIEW_SAMPLE = "Hello! I am your Validate Copilot assistant. I can help you run tests, review failures, and generate new coverage for your project.";
5913
- DEFAULT_TTS_MODEL = "speech-2.6-hd";
5937
+ DEFAULT_TTS_MODEL = "speech-2.8-hd";
5938
+ DEFAULT_TTS_TURBO_MODEL = "speech-2.8-turbo";
5914
5939
  DEFAULT_TTS_VOICE_ID = "English_Graceful_Lady";
5915
5940
  DEFAULT_TTS_SPEED = 1;
5916
5941
  LEGACY_TTS_VOICE_ID_MAP = {
@@ -5919,6 +5944,18 @@ var init_voice_config = __esm({
5919
5944
  English_Deep_Voice_Man: "English_ManWithDeepVoice",
5920
5945
  English_CasualMan: "English_Jovialman"
5921
5946
  };
5947
+ LEGACY_TTS_MODEL_MAP = {
5948
+ "speech-2.6-hd": "speech-2.8-hd",
5949
+ "speech-2.6-turbo": "speech-2.8-turbo",
5950
+ "speech-02-hd": "speech-2.8-hd",
5951
+ "speech-02-turbo": "speech-2.8-turbo",
5952
+ "speech-01-hd": "speech-2.8-hd",
5953
+ "speech-01-turbo": "speech-2.8-turbo"
5954
+ };
5955
+ TTS_FALLBACK_MODEL_MAP = {
5956
+ "speech-2.8-hd": "speech-2.6-hd",
5957
+ "speech-2.8-turbo": "speech-2.6-turbo"
5958
+ };
5922
5959
  DEFAULT_STT_MODEL_BY_PROVIDER = {
5923
5960
  groq: "whisper-large-v3-turbo",
5924
5961
  openai: "whisper-1"
@@ -5971,49 +6008,13 @@ var init_voice_config = __esm({
5971
6008
  id: "speech-2.8-hd",
5972
6009
  label: "Speech 2.8 HD",
5973
6010
  provider: "minimax",
5974
- description: "Latest high-definition model with interjection tags (recommended)."
6011
+ description: "High-definition quality (default). Auto-falls back to Speech 2.6 HD."
5975
6012
  },
5976
6013
  {
5977
6014
  id: "speech-2.8-turbo",
5978
6015
  label: "Speech 2.8 Turbo",
5979
6016
  provider: "minimax",
5980
- description: "Latest low-latency model with interjection tags."
5981
- },
5982
- {
5983
- id: "speech-2.6-hd",
5984
- label: "Speech 2.6 HD",
5985
- provider: "minimax",
5986
- description: "High-definition MiniMax voice model."
5987
- },
5988
- {
5989
- id: "speech-2.6-turbo",
5990
- label: "Speech 2.6 Turbo",
5991
- provider: "minimax",
5992
- description: "Lower-latency Speech 2.6 variant."
5993
- },
5994
- {
5995
- id: "speech-02-hd",
5996
- label: "Speech 02 HD",
5997
- provider: "minimax",
5998
- description: "Previous-generation HD model."
5999
- },
6000
- {
6001
- id: "speech-02-turbo",
6002
- label: "Speech 02 Turbo",
6003
- provider: "minimax",
6004
- description: "Lower-latency Speech 02 variant."
6005
- },
6006
- {
6007
- id: "speech-01-hd",
6008
- label: "Speech 01 HD",
6009
- provider: "minimax",
6010
- description: "Legacy HD model."
6011
- },
6012
- {
6013
- id: "speech-01-turbo",
6014
- label: "Speech 01 Turbo",
6015
- provider: "minimax",
6016
- description: "Legacy fast model."
6017
+ description: "Lower latency. Auto-falls back to Speech 2.6 Turbo."
6017
6018
  }
6018
6019
  ];
6019
6020
  TTS_VOICE_OPTIONS = [
@@ -6297,6 +6298,7 @@ __export(dist_exports, {
6297
6298
  DEFAULT_STT_MODEL_BY_PROVIDER: () => DEFAULT_STT_MODEL_BY_PROVIDER,
6298
6299
  DEFAULT_TTS_MODEL: () => DEFAULT_TTS_MODEL,
6299
6300
  DEFAULT_TTS_SPEED: () => DEFAULT_TTS_SPEED,
6301
+ DEFAULT_TTS_TURBO_MODEL: () => DEFAULT_TTS_TURBO_MODEL,
6300
6302
  DEFAULT_TTS_VOICE_ID: () => DEFAULT_TTS_VOICE_ID,
6301
6303
  DEFAULT_UI_VARIANT: () => DEFAULT_UI_VARIANT,
6302
6304
  EMAIL_FROM_IDENTITIES: () => EMAIL_FROM_IDENTITIES,
@@ -6306,6 +6308,7 @@ __export(dist_exports, {
6306
6308
  FLOW_SERVER_TYPES: () => FLOW_SERVER_TYPES,
6307
6309
  GROUPABLE_CALL_TYPES: () => GROUPABLE_CALL_TYPES,
6308
6310
  GROUP_LABELS: () => GROUP_LABELS,
6311
+ LEGACY_TTS_MODEL_MAP: () => LEGACY_TTS_MODEL_MAP,
6309
6312
  LEGACY_TTS_VOICE_ID_MAP: () => LEGACY_TTS_VOICE_ID_MAP,
6310
6313
  MAX_CREDIT_TOPUP_CENTS: () => MAX_CREDIT_TOPUP_CENTS,
6311
6314
  MAX_DEEPLINK_LENGTH: () => MAX_DEEPLINK_LENGTH,
@@ -6339,6 +6342,7 @@ __export(dist_exports, {
6339
6342
  TEST_USER_ENV_KEYS: () => TEST_USER_ENV_KEYS,
6340
6343
  TRANSIENT_CONTROL_ROLES: () => TRANSIENT_CONTROL_ROLES,
6341
6344
  TRIAL_DURATION_DAYS: () => TRIAL_DURATION_DAYS,
6345
+ TTS_FALLBACK_MODEL_MAP: () => TTS_FALLBACK_MODEL_MAP,
6342
6346
  TTS_MODEL_OPTIONS: () => TTS_MODEL_OPTIONS,
6343
6347
  TTS_VOICE_OPTIONS: () => TTS_VOICE_OPTIONS,
6344
6348
  UI_VARIANTS: () => UI_VARIANTS,
@@ -6441,6 +6445,7 @@ __export(dist_exports, {
6441
6445
  mobilePlatformFromMedium: () => mobilePlatformFromMedium,
6442
6446
  mobileWanderTraceEnabled: () => mobileWanderTraceEnabled,
6443
6447
  normalizeControlName: () => normalizeControlName,
6448
+ normalizeTtsModel: () => normalizeTtsModel,
6444
6449
  normalizeTtsVoiceId: () => normalizeTtsVoiceId,
6445
6450
  otherMobilePlatform: () => otherMobilePlatform,
6446
6451
  parenDepthDelta: () => parenDepthDelta,
@@ -6459,6 +6464,7 @@ __export(dist_exports, {
6459
6464
  resolveMobileStepValuePlaceholders: () => resolveMobileStepValuePlaceholders,
6460
6465
  resolvePlatformLocatorBundle: () => resolvePlatformLocatorBundle,
6461
6466
  resolveSttModelForProvider: () => resolveSttModelForProvider,
6467
+ resolveTtsModelChain: () => resolveTtsModelChain,
6462
6468
  safeCurrentUrl: () => safeCurrentUrl,
6463
6469
  safeOrigin: () => safeOrigin,
6464
6470
  sanitizeSttErrorMessage: () => sanitizeSttErrorMessage,
@@ -6481,6 +6487,7 @@ __export(dist_exports, {
6481
6487
  summarizeMobileReadiness: () => summarizeMobileReadiness,
6482
6488
  summarizeRunHealth: () => summarizeRunHealth,
6483
6489
  toDbPricingPlan: () => toDbPricingPlan,
6490
+ ttsFallbackModel: () => ttsFallbackModel,
6484
6491
  validateAssertionStrength: () => validateAssertionStrength
6485
6492
  });
6486
6493
  var init_dist = __esm({
@@ -353007,7 +353014,7 @@ var require_package4 = __commonJS({
353007
353014
  "package.json"(exports2, module2) {
353008
353015
  module2.exports = {
353009
353016
  name: "@validate.qa/runner",
353010
- version: "1.0.72",
353017
+ version: "1.0.74",
353011
353018
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
353012
353019
  bin: {
353013
353020
  "validate-runner": "dist/cli.js"
@@ -354671,6 +354678,47 @@ async function clickCssInCurrentContext(sessionPath, css) {
354671
354678
  body: "{}"
354672
354679
  });
354673
354680
  }
354681
+ async function clickByVisibleTextInCurrentContext(sessionPath, text) {
354682
+ const script = `
354683
+ var t = String(arguments[0]).trim().replace(/\\s+/g, ' ');
354684
+ var norm = function (e) { return (e.textContent || e.value || '').trim().replace(/\\s+/g, ' '); };
354685
+ var vis = function (e) { var r = e.getBoundingClientRect(); return r.width > 0 && r.height > 0; };
354686
+ var all = Array.prototype.slice.call(document.body.getElementsByTagName('*'));
354687
+ var match = all.filter(function (e) { return norm(e) === t && vis(e); });
354688
+ // Deepest-only: drop any match that contains another match (same text).
354689
+ var deepest = match.filter(function (e) {
354690
+ return !match.some(function (o) { return o !== e && e.contains(o); });
354691
+ });
354692
+ if (deepest.length !== 1) { return deepest.length; }
354693
+ var el = deepest[0];
354694
+ var interactive = function (e) {
354695
+ var tag = e.tagName;
354696
+ if (tag === 'BUTTON' || tag === 'A' || tag === 'INPUT' || tag === 'SELECT' || tag === 'SUMMARY') { return true; }
354697
+ var role = e.getAttribute && e.getAttribute('role');
354698
+ if (role === 'button' || role === 'tab' || role === 'link' || role === 'menuitem' || role === 'option') { return true; }
354699
+ if (e.hasAttribute && (e.hasAttribute('onclick') || (e.hasAttribute('tabindex') && e.getAttribute('tabindex') !== '-1'))) { return true; }
354700
+ try { return window.getComputedStyle(e).cursor === 'pointer'; } catch (err) { return false; }
354701
+ };
354702
+ // Prefer the outermost interactive ancestor that wraps ONLY this text.
354703
+ var target = el;
354704
+ var hop = el;
354705
+ for (var i = 0; i < 5 && hop && hop !== document.body; i++) {
354706
+ if (norm(hop) !== t) { break; }
354707
+ if (interactive(hop)) { target = hop; }
354708
+ hop = hop.parentElement;
354709
+ }
354710
+ target.click();
354711
+ return 1;
354712
+ `;
354713
+ const result = await appiumRequest(`${sessionPath}/execute/sync`, {
354714
+ method: "POST",
354715
+ body: JSON.stringify({ script, args: [text] })
354716
+ });
354717
+ const count = readValue(result);
354718
+ if (count !== 1) {
354719
+ throw new Error(`WEBVIEW text click found ${count === 0 || count === void 0 ? "no" : String(count)} actionable match(es) for "${text}"`);
354720
+ }
354721
+ }
354674
354722
  async function typeCssInCurrentContext(sessionPath, css, text) {
354675
354723
  const elResult = await appiumRequest(`${sessionPath}/element`, {
354676
354724
  method: "POST",
@@ -357029,6 +357077,48 @@ function stepWebViewContext(step) {
357029
357077
  const meta = step.metadata && typeof step.metadata === "object" ? step.metadata : null;
357030
357078
  return typeof meta?.webviewContext === "string" && meta.webviewContext.trim() ? meta.webviewContext.trim() : null;
357031
357079
  }
357080
+ var DOM_ID_RE = /^[A-Za-z_][\w-]*$/;
357081
+ var SYSTEM_RESOURCE_ID_RE = /^(?:android|com\.android\.|com\.google\.android\.)/i;
357082
+ function webViewCssFallbackFromNativeStep(step) {
357083
+ if (stepWebViewCss(step)) return null;
357084
+ const target = step.target;
357085
+ if (!target) return null;
357086
+ const bundle = target.locatorBundle;
357087
+ const resourceId = typeof bundle?.resourceId === "string" ? bundle.resourceId.trim() : "";
357088
+ const accessibilityId = typeof bundle?.accessibilityId === "string" ? bundle.accessibilityId.trim() : "";
357089
+ const primary = typeof target.primary === "string" ? target.primary.trim() : "";
357090
+ const fromResourceId = (raw) => {
357091
+ if (!raw) return null;
357092
+ if (raw.includes(":id/")) {
357093
+ if (SYSTEM_RESOURCE_ID_RE.test(raw)) return null;
357094
+ const local = raw.slice(raw.indexOf(":id/") + 4);
357095
+ return DOM_ID_RE.test(local) ? local : null;
357096
+ }
357097
+ return DOM_ID_RE.test(raw) ? raw : null;
357098
+ };
357099
+ const domId = fromResourceId(resourceId) ?? fromResourceId(accessibilityId) ?? (primary && !/[=~/[\].\s]/.test(primary) && DOM_ID_RE.test(primary) ? primary : null);
357100
+ return domId ? `#${domId}` : null;
357101
+ }
357102
+ function webViewTextFallbackFromNativeStep(step) {
357103
+ if (stepWebViewCss(step)) return null;
357104
+ if (step.action !== "tap") return null;
357105
+ const target = step.target;
357106
+ if (!target) return null;
357107
+ const bundle = target.locatorBundle;
357108
+ const text = typeof bundle?.text === "string" && bundle.text.trim() ? bundle.text.trim() : typeof bundle?.contentDesc === "string" && bundle.contentDesc.trim() ? bundle.contentDesc.trim() : typeof target.primary === "string" && target.primary.trim() && !/[=~/[\]]|:id\//.test(target.primary) ? target.primary.trim() : "";
357109
+ const normalized = text.replace(/\s+/g, " ");
357110
+ if (!normalized || normalized.length > 40 || /[\n\r]/.test(text)) return null;
357111
+ if (SYSTEM_PERMISSION_BUTTON_RE.test(text)) return null;
357112
+ return normalized;
357113
+ }
357114
+ async function hasLiveWebViewContext(sessionId) {
357115
+ try {
357116
+ const contexts = await listAppContexts(`/session/${sessionId}`);
357117
+ return contexts.some((c) => /^WEBVIEW/i.test(c));
357118
+ } catch {
357119
+ return false;
357120
+ }
357121
+ }
357032
357122
  var CONTAINER_ONLY_ENTRY_ID_RE = /(?:^|[:/])id\/(webview|content|decor|decor_content_parent|action_bar_root|statusBarBackground|navigationBarBackground)$|^(?:main-content|root|app|app-root|__next|react-root|application)$/i;
357033
357123
  function isContainerOnlyEntryTarget(target) {
357034
357124
  const primary = target?.primary?.trim() ?? "";
@@ -357116,6 +357206,7 @@ function isWebViewHydrationTransient(err) {
357116
357206
  return /no such element|NoSuchElement|element not interactable|stale element|WEBVIEW step requires|Empty page dictionary|not connectable|no WEBVIEW context|chrome not reachable/i.test(message);
357117
357207
  }
357118
357208
  var WEBVIEW_HYDRATION_TIMEOUT_MS = Number(process.env.VALIDATEQA_WEBVIEW_HYDRATION_TIMEOUT_MS) || 3e4;
357209
+ var PERMISSION_DIALOG_TIMEOUT_MS = Number(process.env.VALIDATEQA_PERMISSION_DIALOG_TIMEOUT_MS) || 3e3;
357119
357210
  async function executeInWebViewContextWithHydrationRetry(sessionId, step, css, body, timeoutMs) {
357120
357211
  const deadline = Date.now() + Math.max(1e3, timeoutMs);
357121
357212
  for (; ; ) {
@@ -357128,6 +357219,17 @@ async function executeInWebViewContextWithHydrationRetry(sessionId, step, css, b
357128
357219
  }
357129
357220
  }
357130
357221
  }
357222
+ var SYSTEM_PERMISSION_BUTTON_RE = /com\.android\.permissioncontroller:id\/permission_(allow|deny)|com\.android\.packageinstaller:id\/permission_|:id\/permission_allow_(foreground_only|one_time)_button|^(?:com\.apple\.springboard|SpringBoard)/i;
357223
+ function isSystemPermissionButtonTarget(target) {
357224
+ if (!target) return false;
357225
+ const bundle = target.locatorBundle;
357226
+ const candidates = [
357227
+ target.primary,
357228
+ typeof bundle?.resourceId === "string" ? bundle.resourceId : "",
357229
+ typeof bundle?.xpath === "string" ? bundle.xpath : ""
357230
+ ];
357231
+ return candidates.some((c) => typeof c === "string" && SYSTEM_PERMISSION_BUTTON_RE.test(c));
357232
+ }
357131
357233
  async function executeTap(sessionId, step, platform3, isCrossPlatform = false) {
357132
357234
  const webCss = stepWebViewCss(step);
357133
357235
  if (webCss) {
@@ -357140,10 +357242,11 @@ async function executeTap(sessionId, step, platform3, isCrossPlatform = false) {
357140
357242
  );
357141
357243
  return;
357142
357244
  }
357245
+ const isPermissionTap = isSystemPermissionButtonTarget(step.target);
357143
357246
  const bounds = boundsForRun(step, platform3, isCrossPlatform);
357144
357247
  if (step.target) {
357145
357248
  try {
357146
- const elementId = await findElement(sessionId, step.target, readStepTimeout(step), {
357249
+ const elementId = await findElement(sessionId, step.target, isPermissionTap ? Math.min(readStepTimeout(step) ?? DEFAULT_STEP_TIMEOUT_MS, PERMISSION_DIALOG_TIMEOUT_MS) : readStepTimeout(step), {
357147
357250
  platform: platform3,
357148
357251
  recordedBounds: bounds
357149
357252
  });
@@ -357153,7 +357256,23 @@ async function executeTap(sessionId, step, platform3, isCrossPlatform = false) {
357153
357256
  });
357154
357257
  return;
357155
357258
  } catch (error2) {
357156
- if (isTransportError(error2) || !bounds || buildLocatorStrategies(step.target, platform3).length > 0) throw error2;
357259
+ if (isTransportError(error2) || error2 instanceof MobileAmbiguousMatchError) throw error2;
357260
+ if (isPermissionTap) return;
357261
+ const cssFallback = webViewCssFallbackFromNativeStep(step);
357262
+ const textFallback = cssFallback ? null : webViewTextFallbackFromNativeStep(step);
357263
+ if ((cssFallback || textFallback) && await hasLiveWebViewContext(sessionId)) {
357264
+ try {
357265
+ if (cssFallback) {
357266
+ await executeInWebViewContext(sessionId, step, cssFallback, (sessionPath, css) => clickCssInCurrentContext(sessionPath, css));
357267
+ } else if (textFallback) {
357268
+ await executeInWebViewContext(sessionId, step, textFallback, (sessionPath) => clickByVisibleTextInCurrentContext(sessionPath, textFallback));
357269
+ }
357270
+ return;
357271
+ } catch (webErr) {
357272
+ if (isTransportError(webErr)) throw webErr;
357273
+ }
357274
+ }
357275
+ if (!bounds || buildLocatorStrategies(step.target, platform3).length > 0) throw error2;
357157
357276
  }
357158
357277
  }
357159
357278
  if (!bounds) {
@@ -357211,6 +357330,18 @@ async function executeType(sessionId, step, context) {
357211
357330
  return;
357212
357331
  } catch (error2) {
357213
357332
  if (isTransportError(error2)) throw error2;
357333
+ if (!focusEstablished && !keyboardSubmitOnly) {
357334
+ const cssFallback = webViewCssFallbackFromNativeStep(step);
357335
+ if (cssFallback && await hasLiveWebViewContext(sessionId)) {
357336
+ try {
357337
+ context.log?.(` Native locate failed; retrying as WebView ${cssFallback} (hybrid mirror fallback)`);
357338
+ await executeInWebViewContext(sessionId, step, cssFallback, (sessionPath2, css) => typeCssInCurrentContext(sessionPath2, css, value));
357339
+ return;
357340
+ } catch (webErr) {
357341
+ if (isTransportError(webErr)) throw webErr;
357342
+ }
357343
+ }
357344
+ }
357214
357345
  if (context.platform === "ANDROID" && focusEstablished) {
357215
357346
  try {
357216
357347
  context.log?.(" Appium active-element send failed; using Android focused-keyboard input fallback");
@@ -363045,7 +363176,11 @@ ${finalVerifyResult.logs ?? ""}`;
363045
363176
  surface: "mobile",
363046
363177
  mobilePlatform: target.platform,
363047
363178
  ...assignedDeviceId ? { deviceId: assignedDeviceId } : {},
363048
- ...leasedDeviceName ? { deviceName: leasedDeviceName } : {}
363179
+ ...leasedDeviceName ? { deviceName: leasedDeviceName } : {},
363180
+ // A mobile AI heal spends its first phase in model analysis with NO
363181
+ // Appium session, so the dashboard mirror is legitimately dark — say
363182
+ // so instead of leaving an eternal "mirror starting" placeholder.
363183
+ ...runMode === "heal" ? { note: "Heal Phase 1 \u2014 reproducing & analyzing the failure (the device mirror attaches when the live phase starts)" } : {}
363049
363184
  });
363050
363185
  requestLiveBrowserHeartbeat(true);
363051
363186
  const mobileSteps = run2.steps ?? [];
@@ -363145,7 +363280,11 @@ ${finalVerifyResult.logs ?? ""}`;
363145
363280
  // Surface heal liveness: a Phase-1/Phase-2 heal can legitimately run
363146
363281
  // for many minutes of model calls with zero device traffic, which
363147
363282
  // previously printed nothing between the start banner and the verdict.
363148
- onProgress: (message) => log.info(`\u{1FA7A} ${message}`),
363283
+ onProgress: (message) => {
363284
+ log.info(`\u{1FA7A} ${message}`);
363285
+ liveBrowserHandle.patch({ note: `\u{1FA7A} ${message}`.slice(0, 180) });
363286
+ requestLiveBrowserHeartbeat();
363287
+ },
363149
363288
  selectorRegistryContext: run2.selectorRegistryContext ?? null,
363150
363289
  runLiveHeal: async (a) => {
363151
363290
  let liveSession = null;
@@ -363197,6 +363336,8 @@ ${finalVerifyResult.logs ?? ""}`;
363197
363336
  };
363198
363337
  }
363199
363338
  log.info("\u{1FA7A} Phase 2 (live device) \u2014 opening the live observation session...");
363339
+ liveBrowserHandle.patch({ note: "Heal Phase 2 \u2014 opening the live device session (mirror attaching)" });
363340
+ requestLiveBrowserHeartbeat(true);
363200
363341
  try {
363201
363342
  await openLiveSession();
363202
363343
  const liveDriver = {
package/dist/cli.mjs CHANGED
@@ -5859,6 +5859,29 @@ function normalizeTtsVoiceId(voiceId) {
5859
5859
  return null;
5860
5860
  return LEGACY_TTS_VOICE_ID_MAP[trimmed] ?? trimmed;
5861
5861
  }
5862
+ function normalizeTtsModel(model) {
5863
+ if (!model)
5864
+ return null;
5865
+ const trimmed = model.trim();
5866
+ if (!trimmed)
5867
+ return null;
5868
+ return LEGACY_TTS_MODEL_MAP[trimmed] ?? trimmed;
5869
+ }
5870
+ function ttsFallbackModel(primary) {
5871
+ const normalized = normalizeTtsModel(primary);
5872
+ if (!normalized)
5873
+ return null;
5874
+ return TTS_FALLBACK_MODEL_MAP[normalized] ?? null;
5875
+ }
5876
+ function resolveTtsModelChain(model) {
5877
+ const normalized = normalizeTtsModel(model);
5878
+ const primary = normalized && normalized in TTS_FALLBACK_MODEL_MAP ? normalized : DEFAULT_TTS_MODEL;
5879
+ const chain = [primary];
5880
+ const fallback = TTS_FALLBACK_MODEL_MAP[primary];
5881
+ if (fallback && fallback !== primary)
5882
+ chain.push(fallback);
5883
+ return chain;
5884
+ }
5862
5885
  function sttModelsForProvider(provider) {
5863
5886
  if (provider === "openai") {
5864
5887
  return STT_MODEL_OPTIONS.filter((m) => m.provider === "openai");
@@ -5906,16 +5929,18 @@ function isValidTtsVoiceId(value) {
5906
5929
  return TTS_VOICE_OPTIONS.some((v) => v.id === normalized);
5907
5930
  }
5908
5931
  function isValidTtsModel(value) {
5909
- if (!value)
5932
+ const normalized = normalizeTtsModel(value);
5933
+ if (!normalized)
5910
5934
  return false;
5911
- return TTS_MODEL_OPTIONS.some((m) => m.id === value);
5935
+ return TTS_MODEL_OPTIONS.some((m) => m.id === normalized);
5912
5936
  }
5913
- var VOICE_PREVIEW_SAMPLE, DEFAULT_TTS_MODEL, DEFAULT_TTS_VOICE_ID, DEFAULT_TTS_SPEED, LEGACY_TTS_VOICE_ID_MAP, DEFAULT_STT_MODEL_BY_PROVIDER, STT_PROVIDER_OPTIONS, STT_MODEL_OPTIONS, TTS_MODEL_OPTIONS, TTS_VOICE_OPTIONS;
5937
+ var VOICE_PREVIEW_SAMPLE, DEFAULT_TTS_MODEL, DEFAULT_TTS_TURBO_MODEL, DEFAULT_TTS_VOICE_ID, DEFAULT_TTS_SPEED, LEGACY_TTS_VOICE_ID_MAP, LEGACY_TTS_MODEL_MAP, TTS_FALLBACK_MODEL_MAP, DEFAULT_STT_MODEL_BY_PROVIDER, STT_PROVIDER_OPTIONS, STT_MODEL_OPTIONS, TTS_MODEL_OPTIONS, TTS_VOICE_OPTIONS;
5914
5938
  var init_voice_config = __esm({
5915
5939
  "../shared/dist/voice-config.js"() {
5916
5940
  "use strict";
5917
5941
  VOICE_PREVIEW_SAMPLE = "Hello! I am your Validate Copilot assistant. I can help you run tests, review failures, and generate new coverage for your project.";
5918
- DEFAULT_TTS_MODEL = "speech-2.6-hd";
5942
+ DEFAULT_TTS_MODEL = "speech-2.8-hd";
5943
+ DEFAULT_TTS_TURBO_MODEL = "speech-2.8-turbo";
5919
5944
  DEFAULT_TTS_VOICE_ID = "English_Graceful_Lady";
5920
5945
  DEFAULT_TTS_SPEED = 1;
5921
5946
  LEGACY_TTS_VOICE_ID_MAP = {
@@ -5924,6 +5949,18 @@ var init_voice_config = __esm({
5924
5949
  English_Deep_Voice_Man: "English_ManWithDeepVoice",
5925
5950
  English_CasualMan: "English_Jovialman"
5926
5951
  };
5952
+ LEGACY_TTS_MODEL_MAP = {
5953
+ "speech-2.6-hd": "speech-2.8-hd",
5954
+ "speech-2.6-turbo": "speech-2.8-turbo",
5955
+ "speech-02-hd": "speech-2.8-hd",
5956
+ "speech-02-turbo": "speech-2.8-turbo",
5957
+ "speech-01-hd": "speech-2.8-hd",
5958
+ "speech-01-turbo": "speech-2.8-turbo"
5959
+ };
5960
+ TTS_FALLBACK_MODEL_MAP = {
5961
+ "speech-2.8-hd": "speech-2.6-hd",
5962
+ "speech-2.8-turbo": "speech-2.6-turbo"
5963
+ };
5927
5964
  DEFAULT_STT_MODEL_BY_PROVIDER = {
5928
5965
  groq: "whisper-large-v3-turbo",
5929
5966
  openai: "whisper-1"
@@ -5976,49 +6013,13 @@ var init_voice_config = __esm({
5976
6013
  id: "speech-2.8-hd",
5977
6014
  label: "Speech 2.8 HD",
5978
6015
  provider: "minimax",
5979
- description: "Latest high-definition model with interjection tags (recommended)."
6016
+ description: "High-definition quality (default). Auto-falls back to Speech 2.6 HD."
5980
6017
  },
5981
6018
  {
5982
6019
  id: "speech-2.8-turbo",
5983
6020
  label: "Speech 2.8 Turbo",
5984
6021
  provider: "minimax",
5985
- description: "Latest low-latency model with interjection tags."
5986
- },
5987
- {
5988
- id: "speech-2.6-hd",
5989
- label: "Speech 2.6 HD",
5990
- provider: "minimax",
5991
- description: "High-definition MiniMax voice model."
5992
- },
5993
- {
5994
- id: "speech-2.6-turbo",
5995
- label: "Speech 2.6 Turbo",
5996
- provider: "minimax",
5997
- description: "Lower-latency Speech 2.6 variant."
5998
- },
5999
- {
6000
- id: "speech-02-hd",
6001
- label: "Speech 02 HD",
6002
- provider: "minimax",
6003
- description: "Previous-generation HD model."
6004
- },
6005
- {
6006
- id: "speech-02-turbo",
6007
- label: "Speech 02 Turbo",
6008
- provider: "minimax",
6009
- description: "Lower-latency Speech 02 variant."
6010
- },
6011
- {
6012
- id: "speech-01-hd",
6013
- label: "Speech 01 HD",
6014
- provider: "minimax",
6015
- description: "Legacy HD model."
6016
- },
6017
- {
6018
- id: "speech-01-turbo",
6019
- label: "Speech 01 Turbo",
6020
- provider: "minimax",
6021
- description: "Legacy fast model."
6022
+ description: "Lower latency. Auto-falls back to Speech 2.6 Turbo."
6022
6023
  }
6023
6024
  ];
6024
6025
  TTS_VOICE_OPTIONS = [
@@ -6302,6 +6303,7 @@ __export(dist_exports, {
6302
6303
  DEFAULT_STT_MODEL_BY_PROVIDER: () => DEFAULT_STT_MODEL_BY_PROVIDER,
6303
6304
  DEFAULT_TTS_MODEL: () => DEFAULT_TTS_MODEL,
6304
6305
  DEFAULT_TTS_SPEED: () => DEFAULT_TTS_SPEED,
6306
+ DEFAULT_TTS_TURBO_MODEL: () => DEFAULT_TTS_TURBO_MODEL,
6305
6307
  DEFAULT_TTS_VOICE_ID: () => DEFAULT_TTS_VOICE_ID,
6306
6308
  DEFAULT_UI_VARIANT: () => DEFAULT_UI_VARIANT,
6307
6309
  EMAIL_FROM_IDENTITIES: () => EMAIL_FROM_IDENTITIES,
@@ -6311,6 +6313,7 @@ __export(dist_exports, {
6311
6313
  FLOW_SERVER_TYPES: () => FLOW_SERVER_TYPES,
6312
6314
  GROUPABLE_CALL_TYPES: () => GROUPABLE_CALL_TYPES,
6313
6315
  GROUP_LABELS: () => GROUP_LABELS,
6316
+ LEGACY_TTS_MODEL_MAP: () => LEGACY_TTS_MODEL_MAP,
6314
6317
  LEGACY_TTS_VOICE_ID_MAP: () => LEGACY_TTS_VOICE_ID_MAP,
6315
6318
  MAX_CREDIT_TOPUP_CENTS: () => MAX_CREDIT_TOPUP_CENTS,
6316
6319
  MAX_DEEPLINK_LENGTH: () => MAX_DEEPLINK_LENGTH,
@@ -6344,6 +6347,7 @@ __export(dist_exports, {
6344
6347
  TEST_USER_ENV_KEYS: () => TEST_USER_ENV_KEYS,
6345
6348
  TRANSIENT_CONTROL_ROLES: () => TRANSIENT_CONTROL_ROLES,
6346
6349
  TRIAL_DURATION_DAYS: () => TRIAL_DURATION_DAYS,
6350
+ TTS_FALLBACK_MODEL_MAP: () => TTS_FALLBACK_MODEL_MAP,
6347
6351
  TTS_MODEL_OPTIONS: () => TTS_MODEL_OPTIONS,
6348
6352
  TTS_VOICE_OPTIONS: () => TTS_VOICE_OPTIONS,
6349
6353
  UI_VARIANTS: () => UI_VARIANTS,
@@ -6446,6 +6450,7 @@ __export(dist_exports, {
6446
6450
  mobilePlatformFromMedium: () => mobilePlatformFromMedium,
6447
6451
  mobileWanderTraceEnabled: () => mobileWanderTraceEnabled,
6448
6452
  normalizeControlName: () => normalizeControlName,
6453
+ normalizeTtsModel: () => normalizeTtsModel,
6449
6454
  normalizeTtsVoiceId: () => normalizeTtsVoiceId,
6450
6455
  otherMobilePlatform: () => otherMobilePlatform,
6451
6456
  parenDepthDelta: () => parenDepthDelta,
@@ -6464,6 +6469,7 @@ __export(dist_exports, {
6464
6469
  resolveMobileStepValuePlaceholders: () => resolveMobileStepValuePlaceholders,
6465
6470
  resolvePlatformLocatorBundle: () => resolvePlatformLocatorBundle,
6466
6471
  resolveSttModelForProvider: () => resolveSttModelForProvider,
6472
+ resolveTtsModelChain: () => resolveTtsModelChain,
6467
6473
  safeCurrentUrl: () => safeCurrentUrl,
6468
6474
  safeOrigin: () => safeOrigin,
6469
6475
  sanitizeSttErrorMessage: () => sanitizeSttErrorMessage,
@@ -6486,6 +6492,7 @@ __export(dist_exports, {
6486
6492
  summarizeMobileReadiness: () => summarizeMobileReadiness,
6487
6493
  summarizeRunHealth: () => summarizeRunHealth,
6488
6494
  toDbPricingPlan: () => toDbPricingPlan,
6495
+ ttsFallbackModel: () => ttsFallbackModel,
6489
6496
  validateAssertionStrength: () => validateAssertionStrength
6490
6497
  });
6491
6498
  var init_dist = __esm({
@@ -353012,7 +353019,7 @@ var require_package4 = __commonJS({
353012
353019
  "package.json"(exports, module) {
353013
353020
  module.exports = {
353014
353021
  name: "@validate.qa/runner",
353015
- version: "1.0.72",
353022
+ version: "1.0.74",
353016
353023
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
353017
353024
  bin: {
353018
353025
  "validate-runner": "dist/cli.js"
@@ -354676,6 +354683,47 @@ async function clickCssInCurrentContext(sessionPath, css) {
354676
354683
  body: "{}"
354677
354684
  });
354678
354685
  }
354686
+ async function clickByVisibleTextInCurrentContext(sessionPath, text) {
354687
+ const script = `
354688
+ var t = String(arguments[0]).trim().replace(/\\s+/g, ' ');
354689
+ var norm = function (e) { return (e.textContent || e.value || '').trim().replace(/\\s+/g, ' '); };
354690
+ var vis = function (e) { var r = e.getBoundingClientRect(); return r.width > 0 && r.height > 0; };
354691
+ var all = Array.prototype.slice.call(document.body.getElementsByTagName('*'));
354692
+ var match = all.filter(function (e) { return norm(e) === t && vis(e); });
354693
+ // Deepest-only: drop any match that contains another match (same text).
354694
+ var deepest = match.filter(function (e) {
354695
+ return !match.some(function (o) { return o !== e && e.contains(o); });
354696
+ });
354697
+ if (deepest.length !== 1) { return deepest.length; }
354698
+ var el = deepest[0];
354699
+ var interactive = function (e) {
354700
+ var tag = e.tagName;
354701
+ if (tag === 'BUTTON' || tag === 'A' || tag === 'INPUT' || tag === 'SELECT' || tag === 'SUMMARY') { return true; }
354702
+ var role = e.getAttribute && e.getAttribute('role');
354703
+ if (role === 'button' || role === 'tab' || role === 'link' || role === 'menuitem' || role === 'option') { return true; }
354704
+ if (e.hasAttribute && (e.hasAttribute('onclick') || (e.hasAttribute('tabindex') && e.getAttribute('tabindex') !== '-1'))) { return true; }
354705
+ try { return window.getComputedStyle(e).cursor === 'pointer'; } catch (err) { return false; }
354706
+ };
354707
+ // Prefer the outermost interactive ancestor that wraps ONLY this text.
354708
+ var target = el;
354709
+ var hop = el;
354710
+ for (var i = 0; i < 5 && hop && hop !== document.body; i++) {
354711
+ if (norm(hop) !== t) { break; }
354712
+ if (interactive(hop)) { target = hop; }
354713
+ hop = hop.parentElement;
354714
+ }
354715
+ target.click();
354716
+ return 1;
354717
+ `;
354718
+ const result = await appiumRequest(`${sessionPath}/execute/sync`, {
354719
+ method: "POST",
354720
+ body: JSON.stringify({ script, args: [text] })
354721
+ });
354722
+ const count = readValue(result);
354723
+ if (count !== 1) {
354724
+ throw new Error(`WEBVIEW text click found ${count === 0 || count === void 0 ? "no" : String(count)} actionable match(es) for "${text}"`);
354725
+ }
354726
+ }
354679
354727
  async function typeCssInCurrentContext(sessionPath, css, text) {
354680
354728
  const elResult = await appiumRequest(`${sessionPath}/element`, {
354681
354729
  method: "POST",
@@ -357037,6 +357085,48 @@ function stepWebViewContext(step) {
357037
357085
  const meta = step.metadata && typeof step.metadata === "object" ? step.metadata : null;
357038
357086
  return typeof meta?.webviewContext === "string" && meta.webviewContext.trim() ? meta.webviewContext.trim() : null;
357039
357087
  }
357088
+ var DOM_ID_RE = /^[A-Za-z_][\w-]*$/;
357089
+ var SYSTEM_RESOURCE_ID_RE = /^(?:android|com\.android\.|com\.google\.android\.)/i;
357090
+ function webViewCssFallbackFromNativeStep(step) {
357091
+ if (stepWebViewCss(step)) return null;
357092
+ const target = step.target;
357093
+ if (!target) return null;
357094
+ const bundle = target.locatorBundle;
357095
+ const resourceId = typeof bundle?.resourceId === "string" ? bundle.resourceId.trim() : "";
357096
+ const accessibilityId = typeof bundle?.accessibilityId === "string" ? bundle.accessibilityId.trim() : "";
357097
+ const primary = typeof target.primary === "string" ? target.primary.trim() : "";
357098
+ const fromResourceId = (raw) => {
357099
+ if (!raw) return null;
357100
+ if (raw.includes(":id/")) {
357101
+ if (SYSTEM_RESOURCE_ID_RE.test(raw)) return null;
357102
+ const local = raw.slice(raw.indexOf(":id/") + 4);
357103
+ return DOM_ID_RE.test(local) ? local : null;
357104
+ }
357105
+ return DOM_ID_RE.test(raw) ? raw : null;
357106
+ };
357107
+ const domId = fromResourceId(resourceId) ?? fromResourceId(accessibilityId) ?? (primary && !/[=~/[\].\s]/.test(primary) && DOM_ID_RE.test(primary) ? primary : null);
357108
+ return domId ? `#${domId}` : null;
357109
+ }
357110
+ function webViewTextFallbackFromNativeStep(step) {
357111
+ if (stepWebViewCss(step)) return null;
357112
+ if (step.action !== "tap") return null;
357113
+ const target = step.target;
357114
+ if (!target) return null;
357115
+ const bundle = target.locatorBundle;
357116
+ const text = typeof bundle?.text === "string" && bundle.text.trim() ? bundle.text.trim() : typeof bundle?.contentDesc === "string" && bundle.contentDesc.trim() ? bundle.contentDesc.trim() : typeof target.primary === "string" && target.primary.trim() && !/[=~/[\]]|:id\//.test(target.primary) ? target.primary.trim() : "";
357117
+ const normalized = text.replace(/\s+/g, " ");
357118
+ if (!normalized || normalized.length > 40 || /[\n\r]/.test(text)) return null;
357119
+ if (SYSTEM_PERMISSION_BUTTON_RE.test(text)) return null;
357120
+ return normalized;
357121
+ }
357122
+ async function hasLiveWebViewContext(sessionId) {
357123
+ try {
357124
+ const contexts = await listAppContexts(`/session/${sessionId}`);
357125
+ return contexts.some((c) => /^WEBVIEW/i.test(c));
357126
+ } catch {
357127
+ return false;
357128
+ }
357129
+ }
357040
357130
  var CONTAINER_ONLY_ENTRY_ID_RE = /(?:^|[:/])id\/(webview|content|decor|decor_content_parent|action_bar_root|statusBarBackground|navigationBarBackground)$|^(?:main-content|root|app|app-root|__next|react-root|application)$/i;
357041
357131
  function isContainerOnlyEntryTarget(target) {
357042
357132
  const primary = target?.primary?.trim() ?? "";
@@ -357124,6 +357214,7 @@ function isWebViewHydrationTransient(err) {
357124
357214
  return /no such element|NoSuchElement|element not interactable|stale element|WEBVIEW step requires|Empty page dictionary|not connectable|no WEBVIEW context|chrome not reachable/i.test(message);
357125
357215
  }
357126
357216
  var WEBVIEW_HYDRATION_TIMEOUT_MS = Number(process.env.VALIDATEQA_WEBVIEW_HYDRATION_TIMEOUT_MS) || 3e4;
357217
+ var PERMISSION_DIALOG_TIMEOUT_MS = Number(process.env.VALIDATEQA_PERMISSION_DIALOG_TIMEOUT_MS) || 3e3;
357127
357218
  async function executeInWebViewContextWithHydrationRetry(sessionId, step, css, body, timeoutMs) {
357128
357219
  const deadline = Date.now() + Math.max(1e3, timeoutMs);
357129
357220
  for (; ; ) {
@@ -357136,6 +357227,17 @@ async function executeInWebViewContextWithHydrationRetry(sessionId, step, css, b
357136
357227
  }
357137
357228
  }
357138
357229
  }
357230
+ var SYSTEM_PERMISSION_BUTTON_RE = /com\.android\.permissioncontroller:id\/permission_(allow|deny)|com\.android\.packageinstaller:id\/permission_|:id\/permission_allow_(foreground_only|one_time)_button|^(?:com\.apple\.springboard|SpringBoard)/i;
357231
+ function isSystemPermissionButtonTarget(target) {
357232
+ if (!target) return false;
357233
+ const bundle = target.locatorBundle;
357234
+ const candidates = [
357235
+ target.primary,
357236
+ typeof bundle?.resourceId === "string" ? bundle.resourceId : "",
357237
+ typeof bundle?.xpath === "string" ? bundle.xpath : ""
357238
+ ];
357239
+ return candidates.some((c) => typeof c === "string" && SYSTEM_PERMISSION_BUTTON_RE.test(c));
357240
+ }
357139
357241
  async function executeTap(sessionId, step, platform3, isCrossPlatform = false) {
357140
357242
  const webCss = stepWebViewCss(step);
357141
357243
  if (webCss) {
@@ -357148,10 +357250,11 @@ async function executeTap(sessionId, step, platform3, isCrossPlatform = false) {
357148
357250
  );
357149
357251
  return;
357150
357252
  }
357253
+ const isPermissionTap = isSystemPermissionButtonTarget(step.target);
357151
357254
  const bounds = boundsForRun(step, platform3, isCrossPlatform);
357152
357255
  if (step.target) {
357153
357256
  try {
357154
- const elementId = await findElement(sessionId, step.target, readStepTimeout(step), {
357257
+ const elementId = await findElement(sessionId, step.target, isPermissionTap ? Math.min(readStepTimeout(step) ?? DEFAULT_STEP_TIMEOUT_MS, PERMISSION_DIALOG_TIMEOUT_MS) : readStepTimeout(step), {
357155
357258
  platform: platform3,
357156
357259
  recordedBounds: bounds
357157
357260
  });
@@ -357161,7 +357264,23 @@ async function executeTap(sessionId, step, platform3, isCrossPlatform = false) {
357161
357264
  });
357162
357265
  return;
357163
357266
  } catch (error2) {
357164
- if (isTransportError(error2) || !bounds || buildLocatorStrategies(step.target, platform3).length > 0) throw error2;
357267
+ if (isTransportError(error2) || error2 instanceof MobileAmbiguousMatchError) throw error2;
357268
+ if (isPermissionTap) return;
357269
+ const cssFallback = webViewCssFallbackFromNativeStep(step);
357270
+ const textFallback = cssFallback ? null : webViewTextFallbackFromNativeStep(step);
357271
+ if ((cssFallback || textFallback) && await hasLiveWebViewContext(sessionId)) {
357272
+ try {
357273
+ if (cssFallback) {
357274
+ await executeInWebViewContext(sessionId, step, cssFallback, (sessionPath, css) => clickCssInCurrentContext(sessionPath, css));
357275
+ } else if (textFallback) {
357276
+ await executeInWebViewContext(sessionId, step, textFallback, (sessionPath) => clickByVisibleTextInCurrentContext(sessionPath, textFallback));
357277
+ }
357278
+ return;
357279
+ } catch (webErr) {
357280
+ if (isTransportError(webErr)) throw webErr;
357281
+ }
357282
+ }
357283
+ if (!bounds || buildLocatorStrategies(step.target, platform3).length > 0) throw error2;
357165
357284
  }
357166
357285
  }
357167
357286
  if (!bounds) {
@@ -357219,6 +357338,18 @@ async function executeType(sessionId, step, context) {
357219
357338
  return;
357220
357339
  } catch (error2) {
357221
357340
  if (isTransportError(error2)) throw error2;
357341
+ if (!focusEstablished && !keyboardSubmitOnly) {
357342
+ const cssFallback = webViewCssFallbackFromNativeStep(step);
357343
+ if (cssFallback && await hasLiveWebViewContext(sessionId)) {
357344
+ try {
357345
+ context.log?.(` Native locate failed; retrying as WebView ${cssFallback} (hybrid mirror fallback)`);
357346
+ await executeInWebViewContext(sessionId, step, cssFallback, (sessionPath2, css) => typeCssInCurrentContext(sessionPath2, css, value));
357347
+ return;
357348
+ } catch (webErr) {
357349
+ if (isTransportError(webErr)) throw webErr;
357350
+ }
357351
+ }
357352
+ }
357222
357353
  if (context.platform === "ANDROID" && focusEstablished) {
357223
357354
  try {
357224
357355
  context.log?.(" Appium active-element send failed; using Android focused-keyboard input fallback");
@@ -363053,7 +363184,11 @@ ${finalVerifyResult.logs ?? ""}`;
363053
363184
  surface: "mobile",
363054
363185
  mobilePlatform: target.platform,
363055
363186
  ...assignedDeviceId ? { deviceId: assignedDeviceId } : {},
363056
- ...leasedDeviceName ? { deviceName: leasedDeviceName } : {}
363187
+ ...leasedDeviceName ? { deviceName: leasedDeviceName } : {},
363188
+ // A mobile AI heal spends its first phase in model analysis with NO
363189
+ // Appium session, so the dashboard mirror is legitimately dark — say
363190
+ // so instead of leaving an eternal "mirror starting" placeholder.
363191
+ ...runMode === "heal" ? { note: "Heal Phase 1 \u2014 reproducing & analyzing the failure (the device mirror attaches when the live phase starts)" } : {}
363057
363192
  });
363058
363193
  requestLiveBrowserHeartbeat(true);
363059
363194
  const mobileSteps = run2.steps ?? [];
@@ -363153,7 +363288,11 @@ ${finalVerifyResult.logs ?? ""}`;
363153
363288
  // Surface heal liveness: a Phase-1/Phase-2 heal can legitimately run
363154
363289
  // for many minutes of model calls with zero device traffic, which
363155
363290
  // previously printed nothing between the start banner and the verdict.
363156
- onProgress: (message) => log.info(`\u{1FA7A} ${message}`),
363291
+ onProgress: (message) => {
363292
+ log.info(`\u{1FA7A} ${message}`);
363293
+ liveBrowserHandle.patch({ note: `\u{1FA7A} ${message}`.slice(0, 180) });
363294
+ requestLiveBrowserHeartbeat();
363295
+ },
363157
363296
  selectorRegistryContext: run2.selectorRegistryContext ?? null,
363158
363297
  runLiveHeal: async (a) => {
363159
363298
  let liveSession = null;
@@ -363205,6 +363344,8 @@ ${finalVerifyResult.logs ?? ""}`;
363205
363344
  };
363206
363345
  }
363207
363346
  log.info("\u{1FA7A} Phase 2 (live device) \u2014 opening the live observation session...");
363347
+ liveBrowserHandle.patch({ note: "Heal Phase 2 \u2014 opening the live device session (mirror attaching)" });
363348
+ requestLiveBrowserHeartbeat(true);
363208
363349
  try {
363209
363350
  await openLiveSession();
363210
363351
  const liveDriver = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@validate.qa/runner",
3
- "version": "1.0.72",
3
+ "version": "1.0.74",
4
4
  "description": "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
5
5
  "bin": {
6
6
  "validate-runner": "dist/cli.js"