@validate.qa/runner 1.0.9 → 1.0.12

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 +108 -28
  2. package/dist/cli.mjs +108 -28
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -263,14 +263,16 @@ var require_ai_provider = __commonJS({
263
263
  { id: "minimax-m2.7", label: "MiniMax M2.7", provider: "minimax", description: "Fast M2.7 tier (Highspeed); auto-fails over to standard M2.7, ~200K ctx", contextWindow: 2e5 },
264
264
  { id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6", provider: "anthropic", description: "Anthropic - fast & intelligent, 200k ctx", contextWindow: 2e5 },
265
265
  { id: "nvidia-nemotron-nano", label: "NVIDIA Nemotron Nano", provider: "nvidia", description: "NVIDIA NIM - Nemotron 30B Nano, reasoning model, 128k ctx", contextWindow: 128e3 },
266
- { id: "nvidia-kimi-k2.6", label: "NVIDIA Kimi K2.6", provider: "nvidia", description: "NVIDIA NIM - Moonshot Kimi K2.6, multimodal coding and agentic model, 256k ctx", contextWindow: 256e3 }
266
+ { id: "nvidia-kimi-k2.6", label: "NVIDIA Kimi K2.6", provider: "nvidia", description: "NVIDIA NIM - Moonshot Kimi K2.6, multimodal coding and agentic model, 256k ctx", contextWindow: 256e3 },
267
+ { id: "deepseek-v4-flash", label: "DeepSeek V4 Flash", provider: "deepseek", description: "DeepSeek - V4 Flash, OpenAI-compatible, 1M ctx", contextWindow: 1e6 }
267
268
  ];
268
269
  var PROVIDER_DEFAULT_MODEL = {
269
270
  openai: "gpt-4o",
270
271
  xai: "grok-build-0.1",
271
272
  anthropic: "claude-sonnet-4-6",
272
273
  minimax: "minimax-m3",
273
- nvidia: "nvidia-nemotron-nano"
274
+ nvidia: "nvidia-nemotron-nano",
275
+ deepseek: "deepseek-v4-flash"
274
276
  };
275
277
  function getContextConfig(modelId) {
276
278
  const info = exports2.AI_MODEL_CATALOG.find((m) => m.id === modelId);
@@ -321,7 +323,8 @@ var require_ai_provider = __commonJS({
321
323
  "minimax-m2.7-highspeed": { strong: "MiniMax-M2.7-highspeed", fast: "MiniMax-M2.7-highspeed", agent: "MiniMax-M2.7-highspeed" },
322
324
  "claude-sonnet-4-6": { strong: "claude-sonnet-4-6", fast: "claude-sonnet-4-6", agent: "claude-sonnet-4-6" },
323
325
  "nvidia-nemotron-nano": { strong: "nvidia/nemotron-3-nano-30b-a3b", fast: "nvidia/nemotron-3-nano-30b-a3b", agent: "nvidia/nemotron-3-nano-30b-a3b" },
324
- "nvidia-kimi-k2.6": { strong: "moonshotai/kimi-k2.6", fast: "moonshotai/kimi-k2.6", agent: "moonshotai/kimi-k2.6" }
326
+ "nvidia-kimi-k2.6": { strong: "moonshotai/kimi-k2.6", fast: "moonshotai/kimi-k2.6", agent: "moonshotai/kimi-k2.6" },
327
+ "deepseek-v4-flash": { strong: "deepseek-v4-flash", fast: "deepseek-v4-flash", agent: "deepseek-v4-flash" }
325
328
  };
326
329
  var _requestModelStorage = new async_hooks_1.AsyncLocalStorage();
327
330
  function withModelContext(modelId, fn) {
@@ -383,6 +386,9 @@ var require_ai_provider = __commonJS({
383
386
  if (modelId === "nvidia/nemotron-3-nano-30b-a3b") {
384
387
  return { chat_template_kwargs: { enable_thinking: false } };
385
388
  }
389
+ if (modelId === "deepseek-v4-flash") {
390
+ return { thinking: { type: "disabled" } };
391
+ }
386
392
  return {};
387
393
  }
388
394
  exports2.MINIMAX_FALLBACK_CHAIN = ["MiniMax-M3", "MiniMax-M2.7-highspeed", "MiniMax-M2.7"];
@@ -484,6 +490,11 @@ var require_ai_provider = __commonJS({
484
490
  if (!apiKey)
485
491
  throw new Error("NVIDIA_API_KEY is required when using an NVIDIA model");
486
492
  client = new openai_1.default({ apiKey, baseURL: process.env.NVIDIA_API_BASE ?? "https://integrate.api.nvidia.com/v1" });
493
+ } else if (provider === "deepseek") {
494
+ const apiKey = process.env.DEEPSEEK_API_KEY;
495
+ if (!apiKey)
496
+ throw new Error("DEEPSEEK_API_KEY is required when using a DeepSeek model");
497
+ client = new openai_1.default({ apiKey, baseURL: process.env.DEEPSEEK_API_BASE ?? "https://api.deepseek.com" });
487
498
  } else {
488
499
  const apiKey = process.env.OPENAI_API_KEY;
489
500
  if (!apiKey)
@@ -517,6 +528,8 @@ var require_ai_provider = __commonJS({
517
528
  return !!process.env.ANTHROPIC_API_KEY?.trim();
518
529
  if (provider === "nvidia")
519
530
  return !!process.env.NVIDIA_API_KEY?.trim();
531
+ if (provider === "deepseek")
532
+ return !!process.env.DEEPSEEK_API_KEY?.trim();
520
533
  return false;
521
534
  }
522
535
  function getAvailableModels() {
@@ -1544,6 +1557,21 @@ function buildAppiumCode(testName, target, steps) {
1544
1557
  ` await driver.keys(text);`,
1545
1558
  ` }`,
1546
1559
  `}`,
1560
+ ``,
1561
+ `async function __textCandidates(el: WebdriverIO.Element) {`,
1562
+ ` const __values: string[] = [];`,
1563
+ ` const __push = (__value: unknown) => {`,
1564
+ ` if (typeof __value !== 'string') return;`,
1565
+ ` const __trimmed = __value.trim();`,
1566
+ ` if (__trimmed.length === 0 || __values.includes(__trimmed)) return;`,
1567
+ ` __values.push(__trimmed);`,
1568
+ ` };`,
1569
+ ` __push(await el.getText().catch(() => ''));`,
1570
+ ` for (const __attr of ['text', 'content-desc', 'contentDescription', 'name', 'label', 'value']) {`,
1571
+ ` __push(await el.getAttribute(__attr).catch(() => ''));`,
1572
+ ` }`,
1573
+ ` return __values;`,
1574
+ `}`,
1547
1575
  ...hasSwipe ? [
1548
1576
  ``,
1549
1577
  `// Read the device window with a few retries (matches the runtime executor).`,
@@ -1630,8 +1658,8 @@ function buildAppiumCode(testName, target, steps) {
1630
1658
  } else if (step.action === "assertText" && hasSelector) {
1631
1659
  lines.push(` {`);
1632
1660
  lines.push(` const __expected = ${toJsStringLiteral(step.assertion ?? "")};`);
1633
- lines.push(` const __actual = await (await findFirst(driver, ${candidatesLiteral})).getText();`);
1634
- lines.push(` if (!__actual.includes(__expected)) {`);
1661
+ lines.push(` const __actual = await __textCandidates(await findFirst(driver, ${candidatesLiteral}));`);
1662
+ lines.push(` if (!__actual.some((__value) => __value.includes(__expected))) {`);
1635
1663
  lines.push(` throw new Error('Assertion failed: expected text to contain ' + JSON.stringify(__expected) + ' but got ' + JSON.stringify(__actual));`);
1636
1664
  lines.push(` }`);
1637
1665
  lines.push(` }`);
@@ -311576,6 +311604,13 @@ var require_mobile_discovery_agent = __commonJS({
311576
311604
  var mobile_generator_js_1 = require_mobile_generator();
311577
311605
  var mobile_exploration_state_js_1 = require_mobile_exploration_state();
311578
311606
  var discover_planner_js_1 = require_discover_planner();
311607
+ function checkpointErrorMessage(err) {
311608
+ return err instanceof Error ? err.message : String(err);
311609
+ }
311610
+ function isTerminalDiscoveryCheckpointError(err) {
311611
+ const message = checkpointErrorMessage(err);
311612
+ return /Discovery plan checkpoint rejected|Discovery run is .*not RUNNING|not RUNNING|cancelled/i.test(message);
311613
+ }
311579
311614
  var EXPLORE_BUDGET_DEEP = 60;
311580
311615
  var EXPLORE_BUDGET_SURVEY = 40;
311581
311616
  function platformForMedium(medium) {
@@ -311706,14 +311741,22 @@ Plan: ${planResult.scenarios.length} scenarios, ${planResult.featureGroups.lengt
311706
311741
  const plans = toPlans(scenarios);
311707
311742
  const coverageGaps = planResult.coverageGaps && planResult.coverageGaps.length > 0 ? planResult.coverageGaps : void 0;
311708
311743
  if (callbacks?.onPlanReady) {
311709
- await callbacks.onPlanReady({
311710
- scenarios,
311711
- featureGroups,
311712
- fallbackUsed: planResult.fallbackUsed ?? false,
311713
- fallbackReason: planResult.fallbackReason
311714
- }).then(() => log2("Plan checkpoint: scenario plan POSTed to server")).catch((err) => {
311715
- log2(`Plan checkpoint POST failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
311716
- });
311744
+ try {
311745
+ await callbacks.onPlanReady({
311746
+ scenarios,
311747
+ featureGroups,
311748
+ fallbackUsed: planResult.fallbackUsed ?? false,
311749
+ fallbackReason: planResult.fallbackReason
311750
+ });
311751
+ log2("Plan checkpoint: scenario plan POSTed to server");
311752
+ } catch (err) {
311753
+ const message = checkpointErrorMessage(err);
311754
+ if (isTerminalDiscoveryCheckpointError(err)) {
311755
+ log2(`Plan checkpoint rejected for inactive discovery run: ${message}`);
311756
+ throw err;
311757
+ }
311758
+ log2(`Plan checkpoint POST failed (non-fatal): ${message}`);
311759
+ }
311717
311760
  }
311718
311761
  if (isSurvey) {
311719
311762
  const duration2 = (Date.now() - startTime) / 1e3;
@@ -326288,7 +326331,7 @@ var require_package4 = __commonJS({
326288
326331
  "package.json"(exports2, module2) {
326289
326332
  module2.exports = {
326290
326333
  name: "@validate.qa/runner",
326291
- version: "1.0.9",
326334
+ version: "1.0.12",
326292
326335
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
326293
326336
  bin: {
326294
326337
  "validate-runner": "dist/cli.js"
@@ -327604,6 +327647,7 @@ async function startMobileNetworkCapture(options) {
327604
327647
  // src/services/appium-executor.ts
327605
327648
  var DEFAULT_STEP_TIMEOUT_MS = 1e4;
327606
327649
  var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process2.execFile);
327650
+ var MOBILE_TEXT_ATTRIBUTE_NAMES = ["text", "content-desc", "contentDescription", "name", "label", "value"];
327607
327651
  var WebDriverTransportError = class extends Error {
327608
327652
  isTransport = true;
327609
327653
  constructor(message) {
@@ -328150,9 +328194,29 @@ async function executeAssertText(sessionId, step) {
328150
328194
  const elementId = await findElement(sessionId, step.target, readStepTimeout(step));
328151
328195
  const result = await wdRequest(`/session/${sessionId}/element/${elementId}/text`);
328152
328196
  const actualText = typeof result?.value === "string" ? result.value : "";
328153
- if (!actualText.includes(step.assertion)) {
328154
- throw new Error(`Text assertion failed: expected "${step.assertion}" to be contained in "${actualText}"`);
328197
+ const textCandidates = await readElementTextCandidates(sessionId, elementId, actualText);
328198
+ if (!textCandidates.some((value) => value.includes(step.assertion))) {
328199
+ throw new Error(`Text assertion failed: expected "${step.assertion}" to be contained in ${JSON.stringify(textCandidates)}`);
328200
+ }
328201
+ }
328202
+ async function readElementTextCandidates(sessionId, elementId, actualText) {
328203
+ const values = [];
328204
+ const push = (value) => {
328205
+ if (typeof value !== "string") return;
328206
+ const trimmed = value.trim();
328207
+ if (trimmed.length === 0 || values.includes(trimmed)) return;
328208
+ values.push(trimmed);
328209
+ };
328210
+ push(actualText);
328211
+ for (const attr of MOBILE_TEXT_ATTRIBUTE_NAMES) {
328212
+ try {
328213
+ const result = await wdRequest(`/session/${sessionId}/element/${elementId}/attribute/${encodeURIComponent(attr)}`);
328214
+ push(result?.value);
328215
+ } catch (err) {
328216
+ if (isTransportError(err)) throw err;
328217
+ }
328155
328218
  }
328219
+ return values;
328156
328220
  }
328157
328221
  async function executeMobileTest(options) {
328158
328222
  const logLines = [];
@@ -332040,23 +332104,39 @@ ${finalVerifyResult.logs ?? ""}`;
332040
332104
  log.fail("ERROR - No mobile steps");
332041
332105
  return;
332042
332106
  }
332107
+ const maxRetries = typeof run2.retryCount === "number" && Number.isFinite(run2.retryCount) ? Math.max(0, Math.min(2, Math.floor(run2.retryCount))) : 0;
332043
332108
  setExecutorBusy(true);
332044
- let result2;
332109
+ let result2 = null;
332110
+ const attemptLogs = [];
332045
332111
  try {
332046
- result2 = await executeMobileTest({
332047
- steps: mobileSteps,
332048
- target: {
332049
- appId: target.appId,
332050
- platform: target.platform,
332051
- platformVersion: target.platformVersion,
332052
- recordedDeviceId: target.recordedDeviceId,
332053
- launchMode: target.launchMode,
332054
- deeplink: target.deeplink
332055
- }
332056
- });
332112
+ for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
332113
+ if (attempt > 0) {
332114
+ log.info(`Retrying mobile test after failed Appium step (${attempt}/${maxRetries})`);
332115
+ }
332116
+ result2 = await executeMobileTest({
332117
+ steps: mobileSteps,
332118
+ target: {
332119
+ appId: target.appId,
332120
+ platform: target.platform,
332121
+ platformVersion: target.platformVersion,
332122
+ recordedDeviceId: target.recordedDeviceId,
332123
+ launchMode: target.launchMode,
332124
+ deeplink: target.deeplink
332125
+ }
332126
+ });
332127
+ attemptLogs.push(`--- Appium attempt ${attempt + 1}/${maxRetries + 1} ---
332128
+ ${result2.logs}`);
332129
+ if (result2.passed || result2.environmental || attempt >= maxRetries) break;
332130
+ }
332057
332131
  } finally {
332058
332132
  setExecutorBusy(false);
332059
332133
  }
332134
+ if (!result2) {
332135
+ throw new Error("Appium execution did not produce a result.");
332136
+ }
332137
+ if (attemptLogs.length > 1) {
332138
+ result2 = { ...result2, logs: attemptLogs.join("\n\n") };
332139
+ }
332060
332140
  const duration2 = Date.now() - startTime;
332061
332141
  const status = result2.environmental ? "ERROR" : result2.passed ? "PASSED" : "FAILED";
332062
332142
  const reportedError = result2.environmental ? `CONFIG_ISSUE: ${result2.environmental}` : result2.error ?? void 0;
package/dist/cli.mjs CHANGED
@@ -268,14 +268,16 @@ var require_ai_provider = __commonJS({
268
268
  { id: "minimax-m2.7", label: "MiniMax M2.7", provider: "minimax", description: "Fast M2.7 tier (Highspeed); auto-fails over to standard M2.7, ~200K ctx", contextWindow: 2e5 },
269
269
  { id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6", provider: "anthropic", description: "Anthropic - fast & intelligent, 200k ctx", contextWindow: 2e5 },
270
270
  { id: "nvidia-nemotron-nano", label: "NVIDIA Nemotron Nano", provider: "nvidia", description: "NVIDIA NIM - Nemotron 30B Nano, reasoning model, 128k ctx", contextWindow: 128e3 },
271
- { id: "nvidia-kimi-k2.6", label: "NVIDIA Kimi K2.6", provider: "nvidia", description: "NVIDIA NIM - Moonshot Kimi K2.6, multimodal coding and agentic model, 256k ctx", contextWindow: 256e3 }
271
+ { id: "nvidia-kimi-k2.6", label: "NVIDIA Kimi K2.6", provider: "nvidia", description: "NVIDIA NIM - Moonshot Kimi K2.6, multimodal coding and agentic model, 256k ctx", contextWindow: 256e3 },
272
+ { id: "deepseek-v4-flash", label: "DeepSeek V4 Flash", provider: "deepseek", description: "DeepSeek - V4 Flash, OpenAI-compatible, 1M ctx", contextWindow: 1e6 }
272
273
  ];
273
274
  var PROVIDER_DEFAULT_MODEL = {
274
275
  openai: "gpt-4o",
275
276
  xai: "grok-build-0.1",
276
277
  anthropic: "claude-sonnet-4-6",
277
278
  minimax: "minimax-m3",
278
- nvidia: "nvidia-nemotron-nano"
279
+ nvidia: "nvidia-nemotron-nano",
280
+ deepseek: "deepseek-v4-flash"
279
281
  };
280
282
  function getContextConfig(modelId) {
281
283
  const info = exports.AI_MODEL_CATALOG.find((m) => m.id === modelId);
@@ -326,7 +328,8 @@ var require_ai_provider = __commonJS({
326
328
  "minimax-m2.7-highspeed": { strong: "MiniMax-M2.7-highspeed", fast: "MiniMax-M2.7-highspeed", agent: "MiniMax-M2.7-highspeed" },
327
329
  "claude-sonnet-4-6": { strong: "claude-sonnet-4-6", fast: "claude-sonnet-4-6", agent: "claude-sonnet-4-6" },
328
330
  "nvidia-nemotron-nano": { strong: "nvidia/nemotron-3-nano-30b-a3b", fast: "nvidia/nemotron-3-nano-30b-a3b", agent: "nvidia/nemotron-3-nano-30b-a3b" },
329
- "nvidia-kimi-k2.6": { strong: "moonshotai/kimi-k2.6", fast: "moonshotai/kimi-k2.6", agent: "moonshotai/kimi-k2.6" }
331
+ "nvidia-kimi-k2.6": { strong: "moonshotai/kimi-k2.6", fast: "moonshotai/kimi-k2.6", agent: "moonshotai/kimi-k2.6" },
332
+ "deepseek-v4-flash": { strong: "deepseek-v4-flash", fast: "deepseek-v4-flash", agent: "deepseek-v4-flash" }
330
333
  };
331
334
  var _requestModelStorage = new async_hooks_1.AsyncLocalStorage();
332
335
  function withModelContext(modelId, fn) {
@@ -388,6 +391,9 @@ var require_ai_provider = __commonJS({
388
391
  if (modelId === "nvidia/nemotron-3-nano-30b-a3b") {
389
392
  return { chat_template_kwargs: { enable_thinking: false } };
390
393
  }
394
+ if (modelId === "deepseek-v4-flash") {
395
+ return { thinking: { type: "disabled" } };
396
+ }
391
397
  return {};
392
398
  }
393
399
  exports.MINIMAX_FALLBACK_CHAIN = ["MiniMax-M3", "MiniMax-M2.7-highspeed", "MiniMax-M2.7"];
@@ -489,6 +495,11 @@ var require_ai_provider = __commonJS({
489
495
  if (!apiKey)
490
496
  throw new Error("NVIDIA_API_KEY is required when using an NVIDIA model");
491
497
  client = new openai_1.default({ apiKey, baseURL: process.env.NVIDIA_API_BASE ?? "https://integrate.api.nvidia.com/v1" });
498
+ } else if (provider === "deepseek") {
499
+ const apiKey = process.env.DEEPSEEK_API_KEY;
500
+ if (!apiKey)
501
+ throw new Error("DEEPSEEK_API_KEY is required when using a DeepSeek model");
502
+ client = new openai_1.default({ apiKey, baseURL: process.env.DEEPSEEK_API_BASE ?? "https://api.deepseek.com" });
492
503
  } else {
493
504
  const apiKey = process.env.OPENAI_API_KEY;
494
505
  if (!apiKey)
@@ -522,6 +533,8 @@ var require_ai_provider = __commonJS({
522
533
  return !!process.env.ANTHROPIC_API_KEY?.trim();
523
534
  if (provider === "nvidia")
524
535
  return !!process.env.NVIDIA_API_KEY?.trim();
536
+ if (provider === "deepseek")
537
+ return !!process.env.DEEPSEEK_API_KEY?.trim();
525
538
  return false;
526
539
  }
527
540
  function getAvailableModels() {
@@ -1549,6 +1562,21 @@ function buildAppiumCode(testName, target, steps) {
1549
1562
  ` await driver.keys(text);`,
1550
1563
  ` }`,
1551
1564
  `}`,
1565
+ ``,
1566
+ `async function __textCandidates(el: WebdriverIO.Element) {`,
1567
+ ` const __values: string[] = [];`,
1568
+ ` const __push = (__value: unknown) => {`,
1569
+ ` if (typeof __value !== 'string') return;`,
1570
+ ` const __trimmed = __value.trim();`,
1571
+ ` if (__trimmed.length === 0 || __values.includes(__trimmed)) return;`,
1572
+ ` __values.push(__trimmed);`,
1573
+ ` };`,
1574
+ ` __push(await el.getText().catch(() => ''));`,
1575
+ ` for (const __attr of ['text', 'content-desc', 'contentDescription', 'name', 'label', 'value']) {`,
1576
+ ` __push(await el.getAttribute(__attr).catch(() => ''));`,
1577
+ ` }`,
1578
+ ` return __values;`,
1579
+ `}`,
1552
1580
  ...hasSwipe ? [
1553
1581
  ``,
1554
1582
  `// Read the device window with a few retries (matches the runtime executor).`,
@@ -1635,8 +1663,8 @@ function buildAppiumCode(testName, target, steps) {
1635
1663
  } else if (step.action === "assertText" && hasSelector) {
1636
1664
  lines.push(` {`);
1637
1665
  lines.push(` const __expected = ${toJsStringLiteral(step.assertion ?? "")};`);
1638
- lines.push(` const __actual = await (await findFirst(driver, ${candidatesLiteral})).getText();`);
1639
- lines.push(` if (!__actual.includes(__expected)) {`);
1666
+ lines.push(` const __actual = await __textCandidates(await findFirst(driver, ${candidatesLiteral}));`);
1667
+ lines.push(` if (!__actual.some((__value) => __value.includes(__expected))) {`);
1640
1668
  lines.push(` throw new Error('Assertion failed: expected text to contain ' + JSON.stringify(__expected) + ' but got ' + JSON.stringify(__actual));`);
1641
1669
  lines.push(` }`);
1642
1670
  lines.push(` }`);
@@ -311581,6 +311609,13 @@ var require_mobile_discovery_agent = __commonJS({
311581
311609
  var mobile_generator_js_1 = require_mobile_generator();
311582
311610
  var mobile_exploration_state_js_1 = require_mobile_exploration_state();
311583
311611
  var discover_planner_js_1 = require_discover_planner();
311612
+ function checkpointErrorMessage(err) {
311613
+ return err instanceof Error ? err.message : String(err);
311614
+ }
311615
+ function isTerminalDiscoveryCheckpointError(err) {
311616
+ const message = checkpointErrorMessage(err);
311617
+ return /Discovery plan checkpoint rejected|Discovery run is .*not RUNNING|not RUNNING|cancelled/i.test(message);
311618
+ }
311584
311619
  var EXPLORE_BUDGET_DEEP = 60;
311585
311620
  var EXPLORE_BUDGET_SURVEY = 40;
311586
311621
  function platformForMedium(medium) {
@@ -311711,14 +311746,22 @@ Plan: ${planResult.scenarios.length} scenarios, ${planResult.featureGroups.lengt
311711
311746
  const plans = toPlans(scenarios);
311712
311747
  const coverageGaps = planResult.coverageGaps && planResult.coverageGaps.length > 0 ? planResult.coverageGaps : void 0;
311713
311748
  if (callbacks?.onPlanReady) {
311714
- await callbacks.onPlanReady({
311715
- scenarios,
311716
- featureGroups,
311717
- fallbackUsed: planResult.fallbackUsed ?? false,
311718
- fallbackReason: planResult.fallbackReason
311719
- }).then(() => log2("Plan checkpoint: scenario plan POSTed to server")).catch((err) => {
311720
- log2(`Plan checkpoint POST failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
311721
- });
311749
+ try {
311750
+ await callbacks.onPlanReady({
311751
+ scenarios,
311752
+ featureGroups,
311753
+ fallbackUsed: planResult.fallbackUsed ?? false,
311754
+ fallbackReason: planResult.fallbackReason
311755
+ });
311756
+ log2("Plan checkpoint: scenario plan POSTed to server");
311757
+ } catch (err) {
311758
+ const message = checkpointErrorMessage(err);
311759
+ if (isTerminalDiscoveryCheckpointError(err)) {
311760
+ log2(`Plan checkpoint rejected for inactive discovery run: ${message}`);
311761
+ throw err;
311762
+ }
311763
+ log2(`Plan checkpoint POST failed (non-fatal): ${message}`);
311764
+ }
311722
311765
  }
311723
311766
  if (isSurvey) {
311724
311767
  const duration2 = (Date.now() - startTime) / 1e3;
@@ -326293,7 +326336,7 @@ var require_package4 = __commonJS({
326293
326336
  "package.json"(exports, module) {
326294
326337
  module.exports = {
326295
326338
  name: "@validate.qa/runner",
326296
- version: "1.0.9",
326339
+ version: "1.0.12",
326297
326340
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
326298
326341
  bin: {
326299
326342
  "validate-runner": "dist/cli.js"
@@ -327612,6 +327655,7 @@ async function startMobileNetworkCapture(options) {
327612
327655
  // src/services/appium-executor.ts
327613
327656
  var DEFAULT_STEP_TIMEOUT_MS = 1e4;
327614
327657
  var execFileAsync2 = promisify2(execFile2);
327658
+ var MOBILE_TEXT_ATTRIBUTE_NAMES = ["text", "content-desc", "contentDescription", "name", "label", "value"];
327615
327659
  var WebDriverTransportError = class extends Error {
327616
327660
  isTransport = true;
327617
327661
  constructor(message) {
@@ -328158,9 +328202,29 @@ async function executeAssertText(sessionId, step) {
328158
328202
  const elementId = await findElement(sessionId, step.target, readStepTimeout(step));
328159
328203
  const result = await wdRequest(`/session/${sessionId}/element/${elementId}/text`);
328160
328204
  const actualText = typeof result?.value === "string" ? result.value : "";
328161
- if (!actualText.includes(step.assertion)) {
328162
- throw new Error(`Text assertion failed: expected "${step.assertion}" to be contained in "${actualText}"`);
328205
+ const textCandidates = await readElementTextCandidates(sessionId, elementId, actualText);
328206
+ if (!textCandidates.some((value) => value.includes(step.assertion))) {
328207
+ throw new Error(`Text assertion failed: expected "${step.assertion}" to be contained in ${JSON.stringify(textCandidates)}`);
328208
+ }
328209
+ }
328210
+ async function readElementTextCandidates(sessionId, elementId, actualText) {
328211
+ const values = [];
328212
+ const push = (value) => {
328213
+ if (typeof value !== "string") return;
328214
+ const trimmed = value.trim();
328215
+ if (trimmed.length === 0 || values.includes(trimmed)) return;
328216
+ values.push(trimmed);
328217
+ };
328218
+ push(actualText);
328219
+ for (const attr of MOBILE_TEXT_ATTRIBUTE_NAMES) {
328220
+ try {
328221
+ const result = await wdRequest(`/session/${sessionId}/element/${elementId}/attribute/${encodeURIComponent(attr)}`);
328222
+ push(result?.value);
328223
+ } catch (err) {
328224
+ if (isTransportError(err)) throw err;
328225
+ }
328163
328226
  }
328227
+ return values;
328164
328228
  }
328165
328229
  async function executeMobileTest(options) {
328166
328230
  const logLines = [];
@@ -332048,23 +332112,39 @@ ${finalVerifyResult.logs ?? ""}`;
332048
332112
  log.fail("ERROR - No mobile steps");
332049
332113
  return;
332050
332114
  }
332115
+ const maxRetries = typeof run2.retryCount === "number" && Number.isFinite(run2.retryCount) ? Math.max(0, Math.min(2, Math.floor(run2.retryCount))) : 0;
332051
332116
  setExecutorBusy(true);
332052
- let result2;
332117
+ let result2 = null;
332118
+ const attemptLogs = [];
332053
332119
  try {
332054
- result2 = await executeMobileTest({
332055
- steps: mobileSteps,
332056
- target: {
332057
- appId: target.appId,
332058
- platform: target.platform,
332059
- platformVersion: target.platformVersion,
332060
- recordedDeviceId: target.recordedDeviceId,
332061
- launchMode: target.launchMode,
332062
- deeplink: target.deeplink
332063
- }
332064
- });
332120
+ for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
332121
+ if (attempt > 0) {
332122
+ log.info(`Retrying mobile test after failed Appium step (${attempt}/${maxRetries})`);
332123
+ }
332124
+ result2 = await executeMobileTest({
332125
+ steps: mobileSteps,
332126
+ target: {
332127
+ appId: target.appId,
332128
+ platform: target.platform,
332129
+ platformVersion: target.platformVersion,
332130
+ recordedDeviceId: target.recordedDeviceId,
332131
+ launchMode: target.launchMode,
332132
+ deeplink: target.deeplink
332133
+ }
332134
+ });
332135
+ attemptLogs.push(`--- Appium attempt ${attempt + 1}/${maxRetries + 1} ---
332136
+ ${result2.logs}`);
332137
+ if (result2.passed || result2.environmental || attempt >= maxRetries) break;
332138
+ }
332065
332139
  } finally {
332066
332140
  setExecutorBusy(false);
332067
332141
  }
332142
+ if (!result2) {
332143
+ throw new Error("Appium execution did not produce a result.");
332144
+ }
332145
+ if (attemptLogs.length > 1) {
332146
+ result2 = { ...result2, logs: attemptLogs.join("\n\n") };
332147
+ }
332068
332148
  const duration2 = Date.now() - startTime;
332069
332149
  const status = result2.environmental ? "ERROR" : result2.passed ? "PASSED" : "FAILED";
332070
332150
  const reportedError = result2.environmental ? `CONFIG_ISSUE: ${result2.environmental}` : result2.error ?? void 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@validate.qa/runner",
3
- "version": "1.0.9",
3
+ "version": "1.0.12",
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"