@validate.qa/runner 1.0.8 → 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.
- package/dist/cli.js +150 -39
- package/dist/cli.mjs +150 -39
- 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}))
|
|
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(` }`);
|
|
@@ -12038,7 +12066,7 @@ var require_kill_tree = __commonJS({
|
|
|
12038
12066
|
var node_child_process_1 = require("child_process");
|
|
12039
12067
|
var node_util_1 = require("util");
|
|
12040
12068
|
var node_fs_1 = require("fs");
|
|
12041
|
-
var
|
|
12069
|
+
var execFileAsync3 = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
12042
12070
|
var PS_TIMEOUT_MS = 2e3;
|
|
12043
12071
|
var PS_BUFFER = 1024 * 1024;
|
|
12044
12072
|
async function gracefulCloseWithTreeKill(options) {
|
|
@@ -12092,7 +12120,7 @@ var require_kill_tree = __commonJS({
|
|
|
12092
12120
|
timer.unref?.();
|
|
12093
12121
|
let stdout;
|
|
12094
12122
|
try {
|
|
12095
|
-
const result = await
|
|
12123
|
+
const result = await execFileAsync3("ps", ["-A", "-o", "pid=,ppid=,command="], {
|
|
12096
12124
|
maxBuffer: PS_BUFFER,
|
|
12097
12125
|
signal: controller.signal
|
|
12098
12126
|
});
|
|
@@ -12130,7 +12158,7 @@ var require_kill_tree = __commonJS({
|
|
|
12130
12158
|
timer.unref?.();
|
|
12131
12159
|
let stdout;
|
|
12132
12160
|
try {
|
|
12133
|
-
const result = await
|
|
12161
|
+
const result = await execFileAsync3("ps", ["-A", "-o", "pid=,ppid=,command="], {
|
|
12134
12162
|
maxBuffer: PS_BUFFER,
|
|
12135
12163
|
signal: controller.signal
|
|
12136
12164
|
});
|
|
@@ -12207,7 +12235,7 @@ var require_kill_tree = __commonJS({
|
|
|
12207
12235
|
timer.unref?.();
|
|
12208
12236
|
let stdout;
|
|
12209
12237
|
try {
|
|
12210
|
-
const result = await
|
|
12238
|
+
const result = await execFileAsync3("ps", ["-A", "-o", "pid=,ppid="], {
|
|
12211
12239
|
maxBuffer: PS_BUFFER,
|
|
12212
12240
|
signal: controller.signal
|
|
12213
12241
|
});
|
|
@@ -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
|
-
|
|
311710
|
-
|
|
311711
|
-
|
|
311712
|
-
|
|
311713
|
-
|
|
311714
|
-
|
|
311715
|
-
|
|
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;
|
|
@@ -321095,7 +321138,7 @@ var require_update = __commonJS({
|
|
|
321095
321138
|
var require_opts_arg = __commonJS({
|
|
321096
321139
|
"../../node_modules/tar/node_modules/mkdirp/lib/opts-arg.js"(exports2, module2) {
|
|
321097
321140
|
"use strict";
|
|
321098
|
-
var { promisify:
|
|
321141
|
+
var { promisify: promisify3 } = require("util");
|
|
321099
321142
|
var fs = require("fs");
|
|
321100
321143
|
var optsArg = (opts) => {
|
|
321101
321144
|
if (!opts)
|
|
@@ -321109,9 +321152,9 @@ var require_opts_arg = __commonJS({
|
|
|
321109
321152
|
else
|
|
321110
321153
|
throw new TypeError("invalid options argument");
|
|
321111
321154
|
opts.mkdir = opts.mkdir || opts.fs.mkdir || fs.mkdir;
|
|
321112
|
-
opts.mkdirAsync =
|
|
321155
|
+
opts.mkdirAsync = promisify3(opts.mkdir);
|
|
321113
321156
|
opts.stat = opts.stat || opts.fs.stat || fs.stat;
|
|
321114
|
-
opts.statAsync =
|
|
321157
|
+
opts.statAsync = promisify3(opts.stat);
|
|
321115
321158
|
opts.statSync = opts.statSync || opts.fs.statSync || fs.statSync;
|
|
321116
321159
|
opts.mkdirSync = opts.mkdirSync || opts.fs.mkdirSync || fs.mkdirSync;
|
|
321117
321160
|
return opts;
|
|
@@ -325204,7 +325247,7 @@ var require_process_inventory = __commonJS({
|
|
|
325204
325247
|
var node_child_process_1 = require("child_process");
|
|
325205
325248
|
var node_os_1 = __importDefault(require("os"));
|
|
325206
325249
|
var node_util_1 = require("util");
|
|
325207
|
-
var
|
|
325250
|
+
var execFileAsync3 = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
325208
325251
|
var DEFAULT_LIMIT = 60;
|
|
325209
325252
|
var DEFAULT_TIMEOUT_MS = 800;
|
|
325210
325253
|
var MAX_COMMAND_LENGTH = 320;
|
|
@@ -325266,7 +325309,7 @@ var require_process_inventory = __commonJS({
|
|
|
325266
325309
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
325267
325310
|
timer.unref?.();
|
|
325268
325311
|
try {
|
|
325269
|
-
const { stdout } = await
|
|
325312
|
+
const { stdout } = await execFileAsync3(command, args, {
|
|
325270
325313
|
maxBuffer: 1024 * 1024,
|
|
325271
325314
|
signal: controller.signal
|
|
325272
325315
|
});
|
|
@@ -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.
|
|
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"
|
|
@@ -326421,6 +326464,8 @@ function shouldUseAuthState(tags, playwrightCode) {
|
|
|
326421
326464
|
|
|
326422
326465
|
// src/services/appium-executor.ts
|
|
326423
326466
|
init_dist();
|
|
326467
|
+
var import_node_child_process2 = require("child_process");
|
|
326468
|
+
var import_node_util2 = require("util");
|
|
326424
326469
|
var import_runner_core8 = __toESM(require_dist2());
|
|
326425
326470
|
|
|
326426
326471
|
// src/services/appium-lifecycle.ts
|
|
@@ -327601,6 +327646,8 @@ async function startMobileNetworkCapture(options) {
|
|
|
327601
327646
|
|
|
327602
327647
|
// src/services/appium-executor.ts
|
|
327603
327648
|
var DEFAULT_STEP_TIMEOUT_MS = 1e4;
|
|
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"];
|
|
327604
327651
|
var WebDriverTransportError = class extends Error {
|
|
327605
327652
|
isTransport = true;
|
|
327606
327653
|
constructor(message) {
|
|
@@ -327922,6 +327969,18 @@ async function sendKeysToFocusedInput(sessionId, value) {
|
|
|
327922
327969
|
body: JSON.stringify({ text: value, value: Array.from(value) })
|
|
327923
327970
|
});
|
|
327924
327971
|
}
|
|
327972
|
+
function encodeAndroidInputText(value) {
|
|
327973
|
+
return value.replace(/%/g, "%25").replace(/\s/g, "%s");
|
|
327974
|
+
}
|
|
327975
|
+
async function sendAndroidTextToFocusedInput(deviceId, value) {
|
|
327976
|
+
if (!deviceId) {
|
|
327977
|
+
throw new Error("Android keyboard fallback requires a device id");
|
|
327978
|
+
}
|
|
327979
|
+
await execFileAsync2("adb", ["-s", deviceId, "shell", "input", "text", encodeAndroidInputText(value)], {
|
|
327980
|
+
timeout: 1e4,
|
|
327981
|
+
windowsHide: true
|
|
327982
|
+
});
|
|
327983
|
+
}
|
|
327925
327984
|
async function trySendElementValue(sessionId, elementId, value) {
|
|
327926
327985
|
try {
|
|
327927
327986
|
await wdRequest(`/session/${sessionId}/element/${elementId}/value`, {
|
|
@@ -327970,7 +328029,7 @@ async function executeTap(sessionId, step) {
|
|
|
327970
328029
|
}
|
|
327971
328030
|
await tapCoordinates(sessionId, bounds);
|
|
327972
328031
|
}
|
|
327973
|
-
async function executeType(sessionId, step) {
|
|
328032
|
+
async function executeType(sessionId, step, context) {
|
|
327974
328033
|
if (step.value === void 0 || step.value === null || step.value === "") return;
|
|
327975
328034
|
const value = String(step.value);
|
|
327976
328035
|
const bounds = boundsFromStep(step);
|
|
@@ -328019,6 +328078,7 @@ async function executeType(sessionId, step) {
|
|
|
328019
328078
|
if (bounds) {
|
|
328020
328079
|
if (!tappedBounds) {
|
|
328021
328080
|
await tapCoordinates(sessionId, bounds);
|
|
328081
|
+
tappedBounds = true;
|
|
328022
328082
|
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
328023
328083
|
}
|
|
328024
328084
|
try {
|
|
@@ -328029,6 +328089,17 @@ async function executeType(sessionId, step) {
|
|
|
328029
328089
|
lastInputError = error2;
|
|
328030
328090
|
}
|
|
328031
328091
|
}
|
|
328092
|
+
if (context.platform === "ANDROID" && tappedBounds) {
|
|
328093
|
+
try {
|
|
328094
|
+
context.log?.(" Appium active-element send failed; using Android focused-keyboard input fallback");
|
|
328095
|
+
await sendAndroidTextToFocusedInput(context.deviceId, value);
|
|
328096
|
+
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
328097
|
+
return;
|
|
328098
|
+
} catch (error2) {
|
|
328099
|
+
if (isTransportError(error2)) throw error2;
|
|
328100
|
+
lastInputError = error2;
|
|
328101
|
+
}
|
|
328102
|
+
}
|
|
328032
328103
|
throw new Error(`TYPE step could not send text to the focused target${lastInputError instanceof Error ? `: ${lastInputError.message}` : ""}`);
|
|
328033
328104
|
}
|
|
328034
328105
|
async function executeClear(sessionId, step) {
|
|
@@ -328123,9 +328194,29 @@ async function executeAssertText(sessionId, step) {
|
|
|
328123
328194
|
const elementId = await findElement(sessionId, step.target, readStepTimeout(step));
|
|
328124
328195
|
const result = await wdRequest(`/session/${sessionId}/element/${elementId}/text`);
|
|
328125
328196
|
const actualText = typeof result?.value === "string" ? result.value : "";
|
|
328126
|
-
|
|
328127
|
-
|
|
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
|
+
}
|
|
328128
328218
|
}
|
|
328219
|
+
return values;
|
|
328129
328220
|
}
|
|
328130
328221
|
async function executeMobileTest(options) {
|
|
328131
328222
|
const logLines = [];
|
|
@@ -328250,7 +328341,11 @@ async function executeMobileTest(options) {
|
|
|
328250
328341
|
await executeTap(sessionId, step);
|
|
328251
328342
|
break;
|
|
328252
328343
|
case "type":
|
|
328253
|
-
await executeType(sessionId, step
|
|
328344
|
+
await executeType(sessionId, step, {
|
|
328345
|
+
platform: options.target.platform,
|
|
328346
|
+
deviceId: selectedDevice.id,
|
|
328347
|
+
log: log2
|
|
328348
|
+
});
|
|
328254
328349
|
break;
|
|
328255
328350
|
case "clear":
|
|
328256
328351
|
await executeClear(sessionId, step);
|
|
@@ -332009,23 +332104,39 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
332009
332104
|
log.fail("ERROR - No mobile steps");
|
|
332010
332105
|
return;
|
|
332011
332106
|
}
|
|
332107
|
+
const maxRetries = typeof run2.retryCount === "number" && Number.isFinite(run2.retryCount) ? Math.max(0, Math.min(2, Math.floor(run2.retryCount))) : 0;
|
|
332012
332108
|
setExecutorBusy(true);
|
|
332013
|
-
let result2;
|
|
332109
|
+
let result2 = null;
|
|
332110
|
+
const attemptLogs = [];
|
|
332014
332111
|
try {
|
|
332015
|
-
|
|
332016
|
-
|
|
332017
|
-
|
|
332018
|
-
|
|
332019
|
-
|
|
332020
|
-
|
|
332021
|
-
|
|
332022
|
-
|
|
332023
|
-
|
|
332024
|
-
|
|
332025
|
-
|
|
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
|
+
}
|
|
332026
332131
|
} finally {
|
|
332027
332132
|
setExecutorBusy(false);
|
|
332028
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
|
+
}
|
|
332029
332140
|
const duration2 = Date.now() - startTime;
|
|
332030
332141
|
const status = result2.environmental ? "ERROR" : result2.passed ? "PASSED" : "FAILED";
|
|
332031
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}))
|
|
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(` }`);
|
|
@@ -12043,7 +12071,7 @@ var require_kill_tree = __commonJS({
|
|
|
12043
12071
|
var node_child_process_1 = __require("child_process");
|
|
12044
12072
|
var node_util_1 = __require("util");
|
|
12045
12073
|
var node_fs_1 = __require("fs");
|
|
12046
|
-
var
|
|
12074
|
+
var execFileAsync3 = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
12047
12075
|
var PS_TIMEOUT_MS = 2e3;
|
|
12048
12076
|
var PS_BUFFER = 1024 * 1024;
|
|
12049
12077
|
async function gracefulCloseWithTreeKill(options) {
|
|
@@ -12097,7 +12125,7 @@ var require_kill_tree = __commonJS({
|
|
|
12097
12125
|
timer.unref?.();
|
|
12098
12126
|
let stdout;
|
|
12099
12127
|
try {
|
|
12100
|
-
const result = await
|
|
12128
|
+
const result = await execFileAsync3("ps", ["-A", "-o", "pid=,ppid=,command="], {
|
|
12101
12129
|
maxBuffer: PS_BUFFER,
|
|
12102
12130
|
signal: controller.signal
|
|
12103
12131
|
});
|
|
@@ -12135,7 +12163,7 @@ var require_kill_tree = __commonJS({
|
|
|
12135
12163
|
timer.unref?.();
|
|
12136
12164
|
let stdout;
|
|
12137
12165
|
try {
|
|
12138
|
-
const result = await
|
|
12166
|
+
const result = await execFileAsync3("ps", ["-A", "-o", "pid=,ppid=,command="], {
|
|
12139
12167
|
maxBuffer: PS_BUFFER,
|
|
12140
12168
|
signal: controller.signal
|
|
12141
12169
|
});
|
|
@@ -12212,7 +12240,7 @@ var require_kill_tree = __commonJS({
|
|
|
12212
12240
|
timer.unref?.();
|
|
12213
12241
|
let stdout;
|
|
12214
12242
|
try {
|
|
12215
|
-
const result = await
|
|
12243
|
+
const result = await execFileAsync3("ps", ["-A", "-o", "pid=,ppid="], {
|
|
12216
12244
|
maxBuffer: PS_BUFFER,
|
|
12217
12245
|
signal: controller.signal
|
|
12218
12246
|
});
|
|
@@ -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
|
-
|
|
311715
|
-
|
|
311716
|
-
|
|
311717
|
-
|
|
311718
|
-
|
|
311719
|
-
|
|
311720
|
-
|
|
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;
|
|
@@ -321100,7 +321143,7 @@ var require_update = __commonJS({
|
|
|
321100
321143
|
var require_opts_arg = __commonJS({
|
|
321101
321144
|
"../../node_modules/tar/node_modules/mkdirp/lib/opts-arg.js"(exports, module) {
|
|
321102
321145
|
"use strict";
|
|
321103
|
-
var { promisify:
|
|
321146
|
+
var { promisify: promisify3 } = __require("util");
|
|
321104
321147
|
var fs = __require("fs");
|
|
321105
321148
|
var optsArg = (opts) => {
|
|
321106
321149
|
if (!opts)
|
|
@@ -321114,9 +321157,9 @@ var require_opts_arg = __commonJS({
|
|
|
321114
321157
|
else
|
|
321115
321158
|
throw new TypeError("invalid options argument");
|
|
321116
321159
|
opts.mkdir = opts.mkdir || opts.fs.mkdir || fs.mkdir;
|
|
321117
|
-
opts.mkdirAsync =
|
|
321160
|
+
opts.mkdirAsync = promisify3(opts.mkdir);
|
|
321118
321161
|
opts.stat = opts.stat || opts.fs.stat || fs.stat;
|
|
321119
|
-
opts.statAsync =
|
|
321162
|
+
opts.statAsync = promisify3(opts.stat);
|
|
321120
321163
|
opts.statSync = opts.statSync || opts.fs.statSync || fs.statSync;
|
|
321121
321164
|
opts.mkdirSync = opts.mkdirSync || opts.fs.mkdirSync || fs.mkdirSync;
|
|
321122
321165
|
return opts;
|
|
@@ -325209,7 +325252,7 @@ var require_process_inventory = __commonJS({
|
|
|
325209
325252
|
var node_child_process_1 = __require("child_process");
|
|
325210
325253
|
var node_os_1 = __importDefault(__require("os"));
|
|
325211
325254
|
var node_util_1 = __require("util");
|
|
325212
|
-
var
|
|
325255
|
+
var execFileAsync3 = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
325213
325256
|
var DEFAULT_LIMIT = 60;
|
|
325214
325257
|
var DEFAULT_TIMEOUT_MS = 800;
|
|
325215
325258
|
var MAX_COMMAND_LENGTH = 320;
|
|
@@ -325271,7 +325314,7 @@ var require_process_inventory = __commonJS({
|
|
|
325271
325314
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
325272
325315
|
timer.unref?.();
|
|
325273
325316
|
try {
|
|
325274
|
-
const { stdout } = await
|
|
325317
|
+
const { stdout } = await execFileAsync3(command, args, {
|
|
325275
325318
|
maxBuffer: 1024 * 1024,
|
|
325276
325319
|
signal: controller.signal
|
|
325277
325320
|
});
|
|
@@ -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.
|
|
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"
|
|
@@ -326427,6 +326470,8 @@ function shouldUseAuthState(tags, playwrightCode) {
|
|
|
326427
326470
|
// src/services/appium-executor.ts
|
|
326428
326471
|
init_dist();
|
|
326429
326472
|
var import_runner_core8 = __toESM(require_dist2());
|
|
326473
|
+
import { execFile as execFile2 } from "child_process";
|
|
326474
|
+
import { promisify as promisify2 } from "util";
|
|
326430
326475
|
|
|
326431
326476
|
// src/services/appium-lifecycle.ts
|
|
326432
326477
|
import { spawn, execFileSync } from "child_process";
|
|
@@ -327609,6 +327654,8 @@ async function startMobileNetworkCapture(options) {
|
|
|
327609
327654
|
|
|
327610
327655
|
// src/services/appium-executor.ts
|
|
327611
327656
|
var DEFAULT_STEP_TIMEOUT_MS = 1e4;
|
|
327657
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
327658
|
+
var MOBILE_TEXT_ATTRIBUTE_NAMES = ["text", "content-desc", "contentDescription", "name", "label", "value"];
|
|
327612
327659
|
var WebDriverTransportError = class extends Error {
|
|
327613
327660
|
isTransport = true;
|
|
327614
327661
|
constructor(message) {
|
|
@@ -327930,6 +327977,18 @@ async function sendKeysToFocusedInput(sessionId, value) {
|
|
|
327930
327977
|
body: JSON.stringify({ text: value, value: Array.from(value) })
|
|
327931
327978
|
});
|
|
327932
327979
|
}
|
|
327980
|
+
function encodeAndroidInputText(value) {
|
|
327981
|
+
return value.replace(/%/g, "%25").replace(/\s/g, "%s");
|
|
327982
|
+
}
|
|
327983
|
+
async function sendAndroidTextToFocusedInput(deviceId, value) {
|
|
327984
|
+
if (!deviceId) {
|
|
327985
|
+
throw new Error("Android keyboard fallback requires a device id");
|
|
327986
|
+
}
|
|
327987
|
+
await execFileAsync2("adb", ["-s", deviceId, "shell", "input", "text", encodeAndroidInputText(value)], {
|
|
327988
|
+
timeout: 1e4,
|
|
327989
|
+
windowsHide: true
|
|
327990
|
+
});
|
|
327991
|
+
}
|
|
327933
327992
|
async function trySendElementValue(sessionId, elementId, value) {
|
|
327934
327993
|
try {
|
|
327935
327994
|
await wdRequest(`/session/${sessionId}/element/${elementId}/value`, {
|
|
@@ -327978,7 +328037,7 @@ async function executeTap(sessionId, step) {
|
|
|
327978
328037
|
}
|
|
327979
328038
|
await tapCoordinates(sessionId, bounds);
|
|
327980
328039
|
}
|
|
327981
|
-
async function executeType(sessionId, step) {
|
|
328040
|
+
async function executeType(sessionId, step, context) {
|
|
327982
328041
|
if (step.value === void 0 || step.value === null || step.value === "") return;
|
|
327983
328042
|
const value = String(step.value);
|
|
327984
328043
|
const bounds = boundsFromStep(step);
|
|
@@ -328027,6 +328086,7 @@ async function executeType(sessionId, step) {
|
|
|
328027
328086
|
if (bounds) {
|
|
328028
328087
|
if (!tappedBounds) {
|
|
328029
328088
|
await tapCoordinates(sessionId, bounds);
|
|
328089
|
+
tappedBounds = true;
|
|
328030
328090
|
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
328031
328091
|
}
|
|
328032
328092
|
try {
|
|
@@ -328037,6 +328097,17 @@ async function executeType(sessionId, step) {
|
|
|
328037
328097
|
lastInputError = error2;
|
|
328038
328098
|
}
|
|
328039
328099
|
}
|
|
328100
|
+
if (context.platform === "ANDROID" && tappedBounds) {
|
|
328101
|
+
try {
|
|
328102
|
+
context.log?.(" Appium active-element send failed; using Android focused-keyboard input fallback");
|
|
328103
|
+
await sendAndroidTextToFocusedInput(context.deviceId, value);
|
|
328104
|
+
await new Promise((resolve) => setTimeout(resolve, MOBILE_FOCUS_SETTLE_MS));
|
|
328105
|
+
return;
|
|
328106
|
+
} catch (error2) {
|
|
328107
|
+
if (isTransportError(error2)) throw error2;
|
|
328108
|
+
lastInputError = error2;
|
|
328109
|
+
}
|
|
328110
|
+
}
|
|
328040
328111
|
throw new Error(`TYPE step could not send text to the focused target${lastInputError instanceof Error ? `: ${lastInputError.message}` : ""}`);
|
|
328041
328112
|
}
|
|
328042
328113
|
async function executeClear(sessionId, step) {
|
|
@@ -328131,9 +328202,29 @@ async function executeAssertText(sessionId, step) {
|
|
|
328131
328202
|
const elementId = await findElement(sessionId, step.target, readStepTimeout(step));
|
|
328132
328203
|
const result = await wdRequest(`/session/${sessionId}/element/${elementId}/text`);
|
|
328133
328204
|
const actualText = typeof result?.value === "string" ? result.value : "";
|
|
328134
|
-
|
|
328135
|
-
|
|
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
|
+
}
|
|
328136
328226
|
}
|
|
328227
|
+
return values;
|
|
328137
328228
|
}
|
|
328138
328229
|
async function executeMobileTest(options) {
|
|
328139
328230
|
const logLines = [];
|
|
@@ -328258,7 +328349,11 @@ async function executeMobileTest(options) {
|
|
|
328258
328349
|
await executeTap(sessionId, step);
|
|
328259
328350
|
break;
|
|
328260
328351
|
case "type":
|
|
328261
|
-
await executeType(sessionId, step
|
|
328352
|
+
await executeType(sessionId, step, {
|
|
328353
|
+
platform: options.target.platform,
|
|
328354
|
+
deviceId: selectedDevice.id,
|
|
328355
|
+
log: log2
|
|
328356
|
+
});
|
|
328262
328357
|
break;
|
|
328263
328358
|
case "clear":
|
|
328264
328359
|
await executeClear(sessionId, step);
|
|
@@ -332017,23 +332112,39 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
332017
332112
|
log.fail("ERROR - No mobile steps");
|
|
332018
332113
|
return;
|
|
332019
332114
|
}
|
|
332115
|
+
const maxRetries = typeof run2.retryCount === "number" && Number.isFinite(run2.retryCount) ? Math.max(0, Math.min(2, Math.floor(run2.retryCount))) : 0;
|
|
332020
332116
|
setExecutorBusy(true);
|
|
332021
|
-
let result2;
|
|
332117
|
+
let result2 = null;
|
|
332118
|
+
const attemptLogs = [];
|
|
332022
332119
|
try {
|
|
332023
|
-
|
|
332024
|
-
|
|
332025
|
-
|
|
332026
|
-
|
|
332027
|
-
|
|
332028
|
-
|
|
332029
|
-
|
|
332030
|
-
|
|
332031
|
-
|
|
332032
|
-
|
|
332033
|
-
|
|
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
|
+
}
|
|
332034
332139
|
} finally {
|
|
332035
332140
|
setExecutorBusy(false);
|
|
332036
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
|
+
}
|
|
332037
332148
|
const duration2 = Date.now() - startTime;
|
|
332038
332149
|
const status = result2.environmental ? "ERROR" : result2.passed ? "PASSED" : "FAILED";
|
|
332039
332150
|
const reportedError = result2.environmental ? `CONFIG_ISSUE: ${result2.environmental}` : result2.error ?? void 0;
|
package/package.json
CHANGED