@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.
- package/dist/cli.js +108 -28
- package/dist/cli.mjs +108 -28
- 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(` }`);
|
|
@@ -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;
|
|
@@ -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"
|
|
@@ -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
|
-
|
|
328154
|
-
|
|
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
|
-
|
|
332047
|
-
|
|
332048
|
-
|
|
332049
|
-
|
|
332050
|
-
|
|
332051
|
-
|
|
332052
|
-
|
|
332053
|
-
|
|
332054
|
-
|
|
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}))
|
|
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
|
-
|
|
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;
|
|
@@ -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"
|
|
@@ -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
|
-
|
|
328162
|
-
|
|
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
|
-
|
|
332055
|
-
|
|
332056
|
-
|
|
332057
|
-
|
|
332058
|
-
|
|
332059
|
-
|
|
332060
|
-
|
|
332061
|
-
|
|
332062
|
-
|
|
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