@validate.qa/runner 1.0.66 → 1.0.67
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 +286 -77
- package/dist/cli.mjs +286 -77
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2236,6 +2236,18 @@ function selectorForValue(value) {
|
|
|
2236
2236
|
function isIdentityMobileXpath(value) {
|
|
2237
2237
|
return /@[A-Za-z_:][-A-Za-z0-9_:.]*\s*=/.test(value) && !/(?:contains|starts-with)\s*\(/i.test(value);
|
|
2238
2238
|
}
|
|
2239
|
+
function xpathLiteral(value) {
|
|
2240
|
+
if (!value.includes("'"))
|
|
2241
|
+
return `'${value}'`;
|
|
2242
|
+
if (!value.includes('"'))
|
|
2243
|
+
return `"${value}"`;
|
|
2244
|
+
const parts = value.split("'").map((p) => `'${p}'`);
|
|
2245
|
+
return `concat(${parts.join(`, "'", `)})`;
|
|
2246
|
+
}
|
|
2247
|
+
function identityTextXpath(value) {
|
|
2248
|
+
const lit = xpathLiteral(value);
|
|
2249
|
+
return `//*[@text=${lit} or @content-desc=${lit} or @label=${lit} or @name=${lit} or @value=${lit}]`;
|
|
2250
|
+
}
|
|
2239
2251
|
function mobileLocatorBundleHasIdentity(bundle) {
|
|
2240
2252
|
if (!bundle)
|
|
2241
2253
|
return false;
|
|
@@ -2262,6 +2274,13 @@ function selectorCandidatesForStep(step) {
|
|
|
2262
2274
|
candidates.push(`~${bundle.accessibilityId}`);
|
|
2263
2275
|
if (nonEmpty(bundle.resourceId))
|
|
2264
2276
|
candidates.push(`id=${bundle.resourceId}`);
|
|
2277
|
+
const isGateStep = step.action === "waitForElement" || step.action === "assertVisible" || step.action === "assertText";
|
|
2278
|
+
const textIdentity = isGateStep || !isEditableMobileClassName(bundle.className) ? bundle.text : void 0;
|
|
2279
|
+
for (const value of [textIdentity, bundle.contentDesc]) {
|
|
2280
|
+
const trimmed = nonEmpty(value);
|
|
2281
|
+
if (trimmed)
|
|
2282
|
+
candidates.push(identityTextXpath(trimmed));
|
|
2283
|
+
}
|
|
2265
2284
|
if (nonEmpty(bundle.xpath))
|
|
2266
2285
|
candidates.push(bundle.xpath);
|
|
2267
2286
|
}
|
|
@@ -2710,7 +2729,7 @@ function uniq(values) {
|
|
|
2710
2729
|
}
|
|
2711
2730
|
return result;
|
|
2712
2731
|
}
|
|
2713
|
-
function
|
|
2732
|
+
function xpathLiteral2(value) {
|
|
2714
2733
|
if (!value.includes("'"))
|
|
2715
2734
|
return `'${value}'`;
|
|
2716
2735
|
if (!value.includes('"'))
|
|
@@ -2718,7 +2737,7 @@ function xpathLiteral(value) {
|
|
|
2718
2737
|
return `concat(${value.split("'").map((part) => `'${part}'`).join(`, "'", `)})`;
|
|
2719
2738
|
}
|
|
2720
2739
|
function mobileTextXPath(value, platform3) {
|
|
2721
|
-
const literal =
|
|
2740
|
+
const literal = xpathLiteral2(value);
|
|
2722
2741
|
if (platform3 === "IOS") {
|
|
2723
2742
|
return `//*[@name=${literal} or @label=${literal} or @value=${literal}]`;
|
|
2724
2743
|
}
|
|
@@ -3866,6 +3885,43 @@ function accumulateStatements(lines) {
|
|
|
3866
3885
|
}
|
|
3867
3886
|
return out;
|
|
3868
3887
|
}
|
|
3888
|
+
function classifyMobileAssertValue(value, context = {}) {
|
|
3889
|
+
const text = value.trim();
|
|
3890
|
+
if (!text)
|
|
3891
|
+
return null;
|
|
3892
|
+
if (/^\{\{[A-Z0-9_]+\}\}$/.test(text))
|
|
3893
|
+
return null;
|
|
3894
|
+
const t1 = matchFirst(TIER1_PATTERNS, text);
|
|
3895
|
+
if (t1)
|
|
3896
|
+
return { tier: 1, reason: t1[1] };
|
|
3897
|
+
const authIdentityValues = [];
|
|
3898
|
+
if (context.authenticatedUserEmail)
|
|
3899
|
+
authIdentityValues.push(context.authenticatedUserEmail);
|
|
3900
|
+
if (context.authenticatedUserName && context.authenticatedUserName.length >= 3) {
|
|
3901
|
+
authIdentityValues.push(context.authenticatedUserName);
|
|
3902
|
+
}
|
|
3903
|
+
for (const auth of authIdentityValues) {
|
|
3904
|
+
if (auth.length >= 3 && text.toLowerCase().includes(auth.toLowerCase())) {
|
|
3905
|
+
return { tier: 2, reason: "asserts the authenticated user identity \u2014 fails under any other test account" };
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
3908
|
+
const knownValues = [
|
|
3909
|
+
...context.knownUserInputs ?? [],
|
|
3910
|
+
...context.scenarioCreatedValues ?? []
|
|
3911
|
+
].filter((v) => {
|
|
3912
|
+
for (const auth of authIdentityValues) {
|
|
3913
|
+
if (auth.length >= 3 && v.toLowerCase().includes(auth.toLowerCase()))
|
|
3914
|
+
return false;
|
|
3915
|
+
}
|
|
3916
|
+
return true;
|
|
3917
|
+
});
|
|
3918
|
+
if (containsKnownValue(text, knownValues))
|
|
3919
|
+
return null;
|
|
3920
|
+
const t2 = matchFirst(TIER2_PATTERNS, text);
|
|
3921
|
+
if (t2)
|
|
3922
|
+
return { tier: 2, reason: t2[1] };
|
|
3923
|
+
return null;
|
|
3924
|
+
}
|
|
3869
3925
|
function parenDepthDelta(text) {
|
|
3870
3926
|
let depth = 0;
|
|
3871
3927
|
let quote = null;
|
|
@@ -6039,6 +6095,7 @@ __export(dist_exports, {
|
|
|
6039
6095
|
classifyAssertionBlock: () => classifyAssertionBlock,
|
|
6040
6096
|
classifyAssertionLine: () => classifyAssertionLine,
|
|
6041
6097
|
classifyErrorMessage: () => classifyErrorMessage,
|
|
6098
|
+
classifyMobileAssertValue: () => classifyMobileAssertValue,
|
|
6042
6099
|
classifyTestHealth: () => classifyTestHealth,
|
|
6043
6100
|
coldSessionConfigMessage: () => coldSessionConfigMessage,
|
|
6044
6101
|
computeMobileSwipePointers: () => computeMobileSwipePointers,
|
|
@@ -80883,6 +80940,45 @@ ${constraints.join("\n")}`);
|
|
|
80883
80940
|
}
|
|
80884
80941
|
});
|
|
80885
80942
|
|
|
80943
|
+
// ../runner-core/dist/services/discovery-lint-flags.js
|
|
80944
|
+
var require_discovery_lint_flags = __commonJS({
|
|
80945
|
+
"../runner-core/dist/services/discovery-lint-flags.js"(exports2) {
|
|
80946
|
+
"use strict";
|
|
80947
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
80948
|
+
exports2.structuralLocatorLintMode = structuralLocatorLintMode;
|
|
80949
|
+
exports2.isPwMethodPreflightEnabled = isPwMethodPreflightEnabled;
|
|
80950
|
+
exports2.isVqaLoginDedupeEnabled = isVqaLoginDedupeEnabled;
|
|
80951
|
+
exports2.mobileSelectorQualityLintMode = mobileSelectorQualityLintMode;
|
|
80952
|
+
exports2.isTestgenEmailVerifyEnabled = isTestgenEmailVerifyEnabled;
|
|
80953
|
+
function strictOn(value) {
|
|
80954
|
+
return value === "true" || value === "1";
|
|
80955
|
+
}
|
|
80956
|
+
function structuralLocatorLintMode() {
|
|
80957
|
+
const v = (process.env.DISCOVERY_STRUCTURAL_LOCATOR_LINT || "").trim().toLowerCase();
|
|
80958
|
+
if (v === "warn")
|
|
80959
|
+
return "warn";
|
|
80960
|
+
if (v === "true" || v === "1" || v === "enforce")
|
|
80961
|
+
return "enforce";
|
|
80962
|
+
return "off";
|
|
80963
|
+
}
|
|
80964
|
+
function isPwMethodPreflightEnabled() {
|
|
80965
|
+
return strictOn(process.env.DISCOVERY_PW_METHOD_PREFLIGHT);
|
|
80966
|
+
}
|
|
80967
|
+
function isVqaLoginDedupeEnabled() {
|
|
80968
|
+
return strictOn(process.env.DISCOVERY_VQALOGIN_DEDUPE);
|
|
80969
|
+
}
|
|
80970
|
+
function mobileSelectorQualityLintMode() {
|
|
80971
|
+
const v = (process.env.MOBILE_SELECTOR_QUALITY_LINT || "").trim().toLowerCase();
|
|
80972
|
+
if (v === "true" || v === "1" || v === "enforce")
|
|
80973
|
+
return "enforce";
|
|
80974
|
+
return "off";
|
|
80975
|
+
}
|
|
80976
|
+
function isTestgenEmailVerifyEnabled() {
|
|
80977
|
+
return strictOn(process.env.DISCOVERY_TESTGEN_EMAIL_VERIFY);
|
|
80978
|
+
}
|
|
80979
|
+
}
|
|
80980
|
+
});
|
|
80981
|
+
|
|
80886
80982
|
// ../runner-core/dist/services/mobile/mobile-generator.js
|
|
80887
80983
|
var require_mobile_generator = __commonJS({
|
|
80888
80984
|
"../runner-core/dist/services/mobile/mobile-generator.js"(exports2) {
|
|
@@ -80902,6 +80998,7 @@ var require_mobile_generator = __commonJS({
|
|
|
80902
80998
|
var ai_queue_js_1 = require_ai_queue();
|
|
80903
80999
|
var ai_retry_js_1 = require_ai_retry();
|
|
80904
81000
|
var mobile_session_keepalive_js_1 = require_mobile_session_keepalive();
|
|
81001
|
+
var discovery_lint_flags_js_1 = require_discovery_lint_flags();
|
|
80905
81002
|
var mobile_source_parser_js_1 = require_mobile_source_parser();
|
|
80906
81003
|
var mobile_source_parser_js_2 = require_mobile_source_parser();
|
|
80907
81004
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
@@ -81071,7 +81168,15 @@ var require_mobile_generator = __commonJS({
|
|
|
81071
81168
|
properties: {
|
|
81072
81169
|
name: { type: "string", description: 'Descriptive test name (e.g. "Login - Happy Path").' },
|
|
81073
81170
|
description: { type: "string", description: "One-line description of what the test proves." },
|
|
81074
|
-
verified: { type: "boolean", description: "True only if you asserted the outcome LIVE with an assert tool." }
|
|
81171
|
+
verified: { type: "boolean", description: "True only if you asserted the outcome LIVE with an assert tool." },
|
|
81172
|
+
suspectedBug: {
|
|
81173
|
+
type: "boolean",
|
|
81174
|
+
description: "True when the app itself misbehaved (crash, error screen, broken flow) and the scenario could not behave as specified. Requires bugDescription and at least one honored assert proving what you observed."
|
|
81175
|
+
},
|
|
81176
|
+
bugDescription: {
|
|
81177
|
+
type: "string",
|
|
81178
|
+
description: "What is broken in the APP (not the test) \u2014 concrete screen + observed vs expected."
|
|
81179
|
+
}
|
|
81075
81180
|
},
|
|
81076
81181
|
required: ["name", "description", "verified"]
|
|
81077
81182
|
}
|
|
@@ -81317,6 +81422,8 @@ var require_mobile_generator = __commonJS({
|
|
|
81317
81422
|
- Assert the OUTCOME of the flow (the new screen / new content), not the control you just tapped.
|
|
81318
81423
|
- Assert only DURABLE elements \u2014 screen chrome that exists on every visit (tab labels, headers, nav bars, the entity you just created). NEVER assert one-time / data-dependent UI: empty-state CTAs, first-run or onboarding prompts, or counters \u2014 they vanish as soon as the account has data, and the test then fails forever on a healthy app.
|
|
81319
81424
|
- Do NOT trigger irreversible or destructive side effects \u2014 real payments; sending messages to others; and ESPECIALLY account/data deletion, deactivation, or any "type DELETE to confirm" gate (the runner hard-blocks destructive taps and confirmation keystrokes). Note them and assert what you safely can.
|
|
81425
|
+
- STATE OWNERSHIP for mutation scenarios (edit / rename / archive / remove): CREATE the record first in this same recording, then act on the row YOU created \u2014 never mutate pre-existing data (it may not exist on a clean install, so the replay fails forever, and another test may depend on it). If the app offers no way to create one, do NOT act on someone else's data: assert what you can safely prove and finish with verified:false, explaining why in the description.
|
|
81426
|
+
- If the APP itself misbehaves (crash, error screen, a flow that cannot proceed as specified), do not force a green path: assert the failure you observed (e.g. the error text), then call finish_generate with suspectedBug:true and a concrete bugDescription of what is broken in the app.
|
|
81320
81427
|
- Stay inside this app.${targetLine} Do not open or use app stores, browsers, email apps, settings, or any other app. If an action leaves the target app, the runner will relaunch the target app and reject that action.
|
|
81321
81428
|
- NEVER tap the phone's home screen / launcher (app icons, "Predicted app" chips, search bars, widgets). Those are NOT part of the app under test \u2014 tapping them is junk that poisons the test. If the app ever backgrounds or you find yourself on the launcher, use mobile_relaunch to cold-start the app; do NOT tap a launcher icon or app-prediction to get back in.
|
|
81322
81429
|
- Do NOT re-do a sequence you already completed. If you filled a form then navigated away, do NOT come back and fill it again \u2014 the scenario needs ONE clean pass. Filling the login form, going back, and re-filling it is exactly the kind of wandering that produces a bloated, broken test.
|
|
@@ -81422,6 +81529,7 @@ Start by calling mobile_snapshot, then execute the steps above \u2014 the minimu
|
|
|
81422
81529
|
let assertCount = 0;
|
|
81423
81530
|
let finish = null;
|
|
81424
81531
|
let finishIntentRejections = 0;
|
|
81532
|
+
let finishStrengthRejections = 0;
|
|
81425
81533
|
let emptyTurnsAfterFinishReject = 0;
|
|
81426
81534
|
let latest = null;
|
|
81427
81535
|
const getLatest = () => latest;
|
|
@@ -81914,31 +82022,49 @@ ${statePacket}`, redactionCredentials) },
|
|
|
81914
82022
|
const claimedVerified = Boolean(toolArgs.verified);
|
|
81915
82023
|
const intentReason = scenarioIntentMismatchReason(scenario, actions);
|
|
81916
82024
|
const verifiedWithoutAssert = claimedVerified && assertCount === 0;
|
|
81917
|
-
const
|
|
81918
|
-
|
|
81919
|
-
|
|
82025
|
+
const intentStyleReject = finishIntentRejections < MAX_FINISH_INTENT_REJECTIONS && iteration < maxIterations && (intentReason !== null || verifiedWithoutAssert);
|
|
82026
|
+
let strengthRejectReason = null;
|
|
82027
|
+
if (!intentStyleReject && claimedVerified && assertCount > 0 && intentReason === null && finishStrengthRejections < 1 && iteration < maxIterations) {
|
|
82028
|
+
const verdict = evaluateMobileAssertionStrength(actions);
|
|
82029
|
+
if (verdict.severity === "block") {
|
|
82030
|
+
strengthRejectReason = verdict.reason;
|
|
82031
|
+
}
|
|
82032
|
+
}
|
|
82033
|
+
if (intentStyleReject || strengthRejectReason !== null) {
|
|
82034
|
+
if (intentStyleReject)
|
|
82035
|
+
finishIntentRejections += 1;
|
|
82036
|
+
if (strengthRejectReason !== null)
|
|
82037
|
+
finishStrengthRejections += 1;
|
|
81920
82038
|
const reasons = [];
|
|
81921
82039
|
if (intentReason)
|
|
81922
82040
|
reasons.push(intentReason);
|
|
81923
82041
|
if (verifiedWithoutAssert) {
|
|
81924
82042
|
reasons.push("claimed verified:true but never called mobile_assert_visible / mobile_assert_text");
|
|
81925
82043
|
}
|
|
82044
|
+
if (strengthRejectReason !== null) {
|
|
82045
|
+
reasons.push(`assertion strength: ${strengthRejectReason} \u2014 your only live proof re-checks a control you already used. Assert the scenario OUTCOME instead: an element on the resulting screen, the created/updated row, or the success/error message.`);
|
|
82046
|
+
}
|
|
81926
82047
|
const reasonText = reasons.join("; ");
|
|
81927
|
-
log2(` finish_generate REJECTED (${finishIntentRejections}/${MAX_FINISH_INTENT_REJECTIONS}): ${reasonText}`);
|
|
82048
|
+
log2(` finish_generate REJECTED (intent ${finishIntentRejections}/${MAX_FINISH_INTENT_REJECTIONS}, strength ${finishStrengthRejections}/1): ${reasonText}`);
|
|
81928
82049
|
pushResult({
|
|
81929
82050
|
ok: false,
|
|
81930
82051
|
error: `Cannot finish yet \u2014 the recorded steps do not complete this scenario: ${reasonText}. Continue driving the app (fill fields, commit forms, navigate, assert the REAL outcome), then call finish_generate again. Do NOT re-open menus or thrash Back.`,
|
|
81931
82052
|
intentMismatch: intentReason,
|
|
81932
82053
|
verifiedWithoutAssert,
|
|
81933
|
-
|
|
81934
|
-
|
|
82054
|
+
...strengthRejectReason !== null ? { strengthBlock: strengthRejectReason } : {},
|
|
82055
|
+
rejection: finishIntentRejections + finishStrengthRejections,
|
|
82056
|
+
maxRejections: MAX_FINISH_INTENT_REJECTIONS + 1
|
|
81935
82057
|
});
|
|
81936
82058
|
continue;
|
|
81937
82059
|
}
|
|
81938
82060
|
finish = {
|
|
81939
82061
|
name: redactCredentialValues(nonEmpty3(String(toolArgs.name ?? "")) ?? scenario.name, redactionCredentials),
|
|
81940
82062
|
description: redactCredentialValues(nonEmpty3(String(toolArgs.description ?? "")) ?? scenario.goal, redactionCredentials),
|
|
81941
|
-
verified: claimedVerified
|
|
82063
|
+
verified: claimedVerified,
|
|
82064
|
+
...toolArgs.suspectedBug === true ? {
|
|
82065
|
+
suspectedBug: true,
|
|
82066
|
+
bugDescription: redactCredentialValues(nonEmpty3(String(toolArgs.bugDescription ?? "")) ?? "", redactionCredentials)
|
|
82067
|
+
} : {}
|
|
81942
82068
|
};
|
|
81943
82069
|
log2(` finish_generate: "${finish.name}" (claimed verified: ${finish.verified}, asserts: ${assertCount})`);
|
|
81944
82070
|
pushResult({ acknowledged: true });
|
|
@@ -82002,6 +82128,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82002
82128
|
ok: false,
|
|
82003
82129
|
error: `Unknown element ref "${String(ref)}". Call mobile_snapshot first; refs are only valid for the latest snapshot.`
|
|
82004
82130
|
});
|
|
82131
|
+
const recordedTypeValues = () => recordedActions.filter((action) => action.type === "TYPE" && typeof action.value === "string" && action.value.length > 0).map((action) => action.value);
|
|
82005
82132
|
const actionPayload = (result, absorbed) => {
|
|
82006
82133
|
const blocked = Boolean(absorbed.externalBlocked || absorbed.transientExternal);
|
|
82007
82134
|
return {
|
|
@@ -82402,7 +82529,8 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82402
82529
|
latest,
|
|
82403
82530
|
screenName,
|
|
82404
82531
|
platform: platform3,
|
|
82405
|
-
recordAction
|
|
82532
|
+
recordAction,
|
|
82533
|
+
typedValues: recordedTypeValues
|
|
82406
82534
|
});
|
|
82407
82535
|
if (!recorded.ok)
|
|
82408
82536
|
return recorded;
|
|
@@ -82471,6 +82599,15 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82471
82599
|
error: `Live text assertion failed: expected ${JSON.stringify(expected)} was not present on ref ${element.ref}. Re-snapshot after the outcome appears and assert the text that is actually visible.`
|
|
82472
82600
|
};
|
|
82473
82601
|
}
|
|
82602
|
+
const volatileExpected = (0, shared_1.classifyMobileAssertValue)(expected, {
|
|
82603
|
+
knownUserInputs: recordedTypeValues()
|
|
82604
|
+
});
|
|
82605
|
+
if (volatileExpected) {
|
|
82606
|
+
return {
|
|
82607
|
+
ok: false,
|
|
82608
|
+
error: `Expected text ${JSON.stringify(expected)} is ${volatileExpected.tier === 1 ? "transient one-shot UI" : "session-dynamic content"} (${volatileExpected.reason}) \u2014 it changes between runs, so the test would fail forever on a healthy app. Assert durable post-state instead: an element unique to the destination screen, a stable label, or text your flow itself entered.`
|
|
82609
|
+
};
|
|
82610
|
+
}
|
|
82474
82611
|
const metadata = metadataForElement(element, screenName(), platform3);
|
|
82475
82612
|
const expectedNeedle = expected.trim().toLowerCase();
|
|
82476
82613
|
const identityValues = [element.accessibilityId, element.resourceId].map((value) => value?.trim().toLowerCase()).filter((value) => Boolean(value));
|
|
@@ -82526,6 +82663,17 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82526
82663
|
if (!accessibilityId && !text) {
|
|
82527
82664
|
return { ok: false, error: "mobile_assert_visible needs a ref (preferred), accessibilityId, or text." };
|
|
82528
82665
|
}
|
|
82666
|
+
if (!accessibilityId && text) {
|
|
82667
|
+
const volatileIdentity = (0, shared_1.classifyMobileAssertValue)(text, {
|
|
82668
|
+
knownUserInputs: ctx.typedValues?.() ?? []
|
|
82669
|
+
});
|
|
82670
|
+
if (volatileIdentity) {
|
|
82671
|
+
return {
|
|
82672
|
+
ok: false,
|
|
82673
|
+
error: `${JSON.stringify(text)} is ${volatileIdentity.tier === 1 ? "transient one-shot UI" : "session-dynamic content"} (${volatileIdentity.reason}) \u2014 a text identity must be durable to resolve at replay. Assert a stable element of the destination screen instead (prefer a ref from mobile_snapshot).`
|
|
82674
|
+
};
|
|
82675
|
+
}
|
|
82676
|
+
}
|
|
82529
82677
|
const directTarget = accessibilityId ?? text;
|
|
82530
82678
|
if (directTarget && isStructuralAssertionIdentity(directTarget)) {
|
|
82531
82679
|
return {
|
|
@@ -83161,7 +83309,21 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83161
83309
|
if (claimedVerified && !verified) {
|
|
83162
83310
|
log2(" downgraded verified -> false: model claimed verified but called zero assert tools.");
|
|
83163
83311
|
}
|
|
83164
|
-
const
|
|
83312
|
+
const baseStrengthVerdict = evaluateMobileAssertionStrength(actions);
|
|
83313
|
+
const bugDescription = recording.finish?.bugDescription?.trim() ?? "";
|
|
83314
|
+
const bugWitnessHonored = process.env.DISCOVERY_BUG_WITNESS_ENABLED === "true" && recording.finish?.suspectedBug === true && bugDescription.length >= 12 && recording.assertCount > 0;
|
|
83315
|
+
if (recording.finish?.suspectedBug === true && !bugWitnessHonored) {
|
|
83316
|
+
log2(" suspectedBug declared but NOT honored: " + (process.env.DISCOVERY_BUG_WITNESS_ENABLED !== "true" ? "bug-witness channel disabled (DISCOVERY_BUG_WITNESS_ENABLED)." : bugDescription.length < 12 ? "bugDescription too short." : "no honored assert backs the observation."));
|
|
83317
|
+
}
|
|
83318
|
+
const strengthVerdict = bugWitnessHonored ? {
|
|
83319
|
+
...baseStrengthVerdict,
|
|
83320
|
+
ok: false,
|
|
83321
|
+
severity: "route-bug",
|
|
83322
|
+
reason: `Model declared an app bug during generation: ${bugDescription}`
|
|
83323
|
+
} : baseStrengthVerdict;
|
|
83324
|
+
if (bugWitnessHonored) {
|
|
83325
|
+
log2(` bug-witness honored \u2014 routing as app bug: ${bugDescription}`);
|
|
83326
|
+
}
|
|
83165
83327
|
if (strengthVerdict.severity === "block" || strengthVerdict.severity === "route-bug") {
|
|
83166
83328
|
if (verified) {
|
|
83167
83329
|
log2(` downgraded verified -> false: assertion strength ${strengthVerdict.severity} \u2014 ${strengthVerdict.reason}`);
|
|
@@ -83177,7 +83339,11 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83177
83339
|
verified = false;
|
|
83178
83340
|
}
|
|
83179
83341
|
const platformTag2 = platform3 === "IOS" ? "@ios" : "@android";
|
|
83180
|
-
const
|
|
83342
|
+
const selectorLintEnforced = (0, discovery_lint_flags_js_1.mobileSelectorQualityLintMode)() === "enforce" && selectorQuality.summary.high > 0;
|
|
83343
|
+
if (selectorLintEnforced && verified) {
|
|
83344
|
+
log2(` selector-quality lint (enforce): ${selectorQuality.summary.high} high-severity locator issue(s) \u2014 tagging @needs-review.`);
|
|
83345
|
+
}
|
|
83346
|
+
const reviewTag = verified && !selectorLintEnforced ? "@verified" : "@needs-review";
|
|
83181
83347
|
const tags = ["@discovered", "@mobile", platformTag2, reviewTag];
|
|
83182
83348
|
if (strengthVerdict.severity === "downgrade")
|
|
83183
83349
|
tags.push("@smoke");
|
|
@@ -86257,38 +86423,6 @@ var require_run_callback_proxy = __commonJS({
|
|
|
86257
86423
|
}
|
|
86258
86424
|
});
|
|
86259
86425
|
|
|
86260
|
-
// ../runner-core/dist/services/discovery-lint-flags.js
|
|
86261
|
-
var require_discovery_lint_flags = __commonJS({
|
|
86262
|
-
"../runner-core/dist/services/discovery-lint-flags.js"(exports2) {
|
|
86263
|
-
"use strict";
|
|
86264
|
-
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
86265
|
-
exports2.structuralLocatorLintMode = structuralLocatorLintMode;
|
|
86266
|
-
exports2.isPwMethodPreflightEnabled = isPwMethodPreflightEnabled;
|
|
86267
|
-
exports2.isVqaLoginDedupeEnabled = isVqaLoginDedupeEnabled;
|
|
86268
|
-
exports2.isTestgenEmailVerifyEnabled = isTestgenEmailVerifyEnabled;
|
|
86269
|
-
function strictOn(value) {
|
|
86270
|
-
return value === "true" || value === "1";
|
|
86271
|
-
}
|
|
86272
|
-
function structuralLocatorLintMode() {
|
|
86273
|
-
const v = (process.env.DISCOVERY_STRUCTURAL_LOCATOR_LINT || "").trim().toLowerCase();
|
|
86274
|
-
if (v === "warn")
|
|
86275
|
-
return "warn";
|
|
86276
|
-
if (v === "true" || v === "1" || v === "enforce")
|
|
86277
|
-
return "enforce";
|
|
86278
|
-
return "off";
|
|
86279
|
-
}
|
|
86280
|
-
function isPwMethodPreflightEnabled() {
|
|
86281
|
-
return strictOn(process.env.DISCOVERY_PW_METHOD_PREFLIGHT);
|
|
86282
|
-
}
|
|
86283
|
-
function isVqaLoginDedupeEnabled() {
|
|
86284
|
-
return strictOn(process.env.DISCOVERY_VQALOGIN_DEDUPE);
|
|
86285
|
-
}
|
|
86286
|
-
function isTestgenEmailVerifyEnabled() {
|
|
86287
|
-
return strictOn(process.env.DISCOVERY_TESTGEN_EMAIL_VERIFY);
|
|
86288
|
-
}
|
|
86289
|
-
}
|
|
86290
|
-
});
|
|
86291
|
-
|
|
86292
86426
|
// ../runner-core/dist/services/auth-recipe.js
|
|
86293
86427
|
var require_auth_recipe = __commonJS({
|
|
86294
86428
|
"../runner-core/dist/services/auth-recipe.js"(exports2) {
|
|
@@ -347787,12 +347921,7 @@ Write valid JSON only (no markdown fences inside files). Be exhaustive.`;
|
|
|
347787
347921
|
const o = value;
|
|
347788
347922
|
if (!isNonEmptyString(o.name))
|
|
347789
347923
|
return { ok: false, reason: "name: missing" };
|
|
347790
|
-
|
|
347791
|
-
return { ok: false, reason: "appiumCode: missing" };
|
|
347792
|
-
const code = o.appiumCode;
|
|
347793
|
-
if (!/webdriverio|remote\s*\(|driver\s*\./i.test(code)) {
|
|
347794
|
-
return { ok: false, reason: "appiumCode: not a WebdriverIO/Appium script" };
|
|
347795
|
-
}
|
|
347924
|
+
const code = isNonEmptyString(o.appiumCode) ? o.appiumCode : "";
|
|
347796
347925
|
if (!Array.isArray(o.steps) || o.steps.length === 0) {
|
|
347797
347926
|
return { ok: false, reason: "steps: missing or empty" };
|
|
347798
347927
|
}
|
|
@@ -351095,7 +351224,7 @@ var require_package4 = __commonJS({
|
|
|
351095
351224
|
"package.json"(exports2, module2) {
|
|
351096
351225
|
module2.exports = {
|
|
351097
351226
|
name: "@validate.qa/runner",
|
|
351098
|
-
version: "1.0.
|
|
351227
|
+
version: "1.0.67",
|
|
351099
351228
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
351100
351229
|
bin: {
|
|
351101
351230
|
"validate-runner": "dist/cli.js"
|
|
@@ -351491,6 +351620,9 @@ function sanitizeAppiumLogLine(line) {
|
|
|
351491
351620
|
}
|
|
351492
351621
|
function setAppiumWorkloadActive(active) {
|
|
351493
351622
|
activeWorkloadCount = active ? activeWorkloadCount + 1 : Math.max(0, activeWorkloadCount - 1);
|
|
351623
|
+
if (activeWorkloadCount === 0) {
|
|
351624
|
+
consecutiveHealthFailures = 0;
|
|
351625
|
+
}
|
|
351494
351626
|
}
|
|
351495
351627
|
function shouldRestartAfterHealthFailure(workloadCount, failureCount) {
|
|
351496
351628
|
return workloadCount <= 0 && failureCount >= HEALTH_FAILURES_BEFORE_RESTART;
|
|
@@ -351986,17 +352118,31 @@ function rectMatchesIntendedBounds(rect, bounds) {
|
|
|
351986
352118
|
return rectCenterInBounds || boundsCenterInRect;
|
|
351987
352119
|
}
|
|
351988
352120
|
async function resolveTypeTargetElementId(sessionPath, bounds) {
|
|
351989
|
-
const
|
|
352121
|
+
const rawBox = bounds !== void 0 ? normaliseBounds(bounds) : null;
|
|
352122
|
+
const box = rawBox && rawBox.width > 0 && rawBox.height > 0 ? rawBox : null;
|
|
351990
352123
|
let elementId = await getActiveElementId(sessionPath);
|
|
351991
|
-
|
|
352124
|
+
let rejectedElementId = null;
|
|
352125
|
+
let rejectedForMismatch = false;
|
|
352126
|
+
if (elementId && box) {
|
|
351992
352127
|
const rect = await getElementRect(sessionPath, elementId);
|
|
351993
352128
|
if (!rect || !rectMatchesIntendedBounds(rect, box)) {
|
|
352129
|
+
rejectedElementId = elementId;
|
|
352130
|
+
rejectedForMismatch = !!rect;
|
|
351994
352131
|
elementId = null;
|
|
351995
352132
|
}
|
|
351996
352133
|
}
|
|
351997
352134
|
if (!elementId && bounds !== void 0 && await tapBoundsCenter(sessionPath, bounds)) {
|
|
351998
352135
|
await new Promise((resolve2) => setTimeout(resolve2, MOBILE_FOCUS_SETTLE_MS));
|
|
351999
352136
|
elementId = await getActiveElementId(sessionPath);
|
|
352137
|
+
if (elementId && box) {
|
|
352138
|
+
const rect = await getElementRect(sessionPath, elementId);
|
|
352139
|
+
const provablyWrong = rect ? !rectMatchesIntendedBounds(rect, box) : rejectedForMismatch && elementId === rejectedElementId;
|
|
352140
|
+
if (provablyWrong) {
|
|
352141
|
+
throw new Error(
|
|
352142
|
+
"Could not focus the intended field: a different element kept keyboard focus after tapping the target bounds. Tap the field directly, then retry."
|
|
352143
|
+
);
|
|
352144
|
+
}
|
|
352145
|
+
}
|
|
352000
352146
|
}
|
|
352001
352147
|
return elementId;
|
|
352002
352148
|
}
|
|
@@ -354572,6 +354718,13 @@ function buildLocatorStrategies(target, platform3) {
|
|
|
354572
354718
|
if (bundle) {
|
|
354573
354719
|
if (bundle.accessibilityId) strategies.push({ using: "accessibility id", value: bundle.accessibilityId });
|
|
354574
354720
|
if (bundle.resourceId) strategies.push({ using: "id", value: bundle.resourceId });
|
|
354721
|
+
for (const value of [
|
|
354722
|
+
isEditableMobileClassName(bundle.className) ? void 0 : bundle.text,
|
|
354723
|
+
bundle.contentDesc
|
|
354724
|
+
]) {
|
|
354725
|
+
const trimmed = value?.trim();
|
|
354726
|
+
if (trimmed) strategies.push({ using: "xpath", value: identityTextXpath2(trimmed) });
|
|
354727
|
+
}
|
|
354575
354728
|
if (bundle.xpath) strategies.push({ using: "xpath", value: bundle.xpath });
|
|
354576
354729
|
}
|
|
354577
354730
|
if (!(platform3 && hasPlatformLocator(target, platform3))) {
|
|
@@ -354589,14 +354742,14 @@ function dedupeStrategies(strategies) {
|
|
|
354589
354742
|
(strategy, index, list) => list.findIndex((candidate) => candidate.using === strategy.using && candidate.value === strategy.value) === index
|
|
354590
354743
|
);
|
|
354591
354744
|
}
|
|
354592
|
-
function
|
|
354745
|
+
function xpathLiteral3(value) {
|
|
354593
354746
|
if (!value.includes("'")) return `'${value}'`;
|
|
354594
354747
|
if (!value.includes('"')) return `"${value}"`;
|
|
354595
354748
|
const parts = value.split("'").map((p) => `'${p}'`);
|
|
354596
354749
|
return `concat(${parts.join(`, "'", `)})`;
|
|
354597
354750
|
}
|
|
354598
|
-
function
|
|
354599
|
-
const lit =
|
|
354751
|
+
function identityTextXpath2(value) {
|
|
354752
|
+
const lit = xpathLiteral3(value);
|
|
354600
354753
|
return `//*[@text=${lit} or @content-desc=${lit} or @label=${lit} or @name=${lit} or @value=${lit}]`;
|
|
354601
354754
|
}
|
|
354602
354755
|
function isIdentityXpath(value) {
|
|
@@ -354616,7 +354769,7 @@ function buildIdentityLocatorStrategies(target, platform3) {
|
|
|
354616
354769
|
if (bundle.resourceId) strategies.push({ using: "id", value: bundle.resourceId });
|
|
354617
354770
|
for (const value of [bundle.text, bundle.contentDesc]) {
|
|
354618
354771
|
const trimmed = value?.trim();
|
|
354619
|
-
if (trimmed) strategies.push({ using: "xpath", value:
|
|
354772
|
+
if (trimmed) strategies.push({ using: "xpath", value: identityTextXpath2(trimmed) });
|
|
354620
354773
|
}
|
|
354621
354774
|
if (bundle.xpath && isIdentityXpath(bundle.xpath)) strategies.push({ using: "xpath", value: bundle.xpath });
|
|
354622
354775
|
}
|
|
@@ -355096,6 +355249,19 @@ async function readElementTextCandidates(sessionId, elementId, actualText) {
|
|
|
355096
355249
|
}
|
|
355097
355250
|
return values;
|
|
355098
355251
|
}
|
|
355252
|
+
var controlPortSlotsByDevice = /* @__PURE__ */ new Map();
|
|
355253
|
+
function acquireControlPortSlot(deviceId) {
|
|
355254
|
+
const existing = controlPortSlotsByDevice.get(deviceId);
|
|
355255
|
+
if (existing !== void 0) return existing;
|
|
355256
|
+
const taken = new Set(controlPortSlotsByDevice.values());
|
|
355257
|
+
let slot = 0;
|
|
355258
|
+
while (taken.has(slot)) slot += 1;
|
|
355259
|
+
controlPortSlotsByDevice.set(deviceId, slot);
|
|
355260
|
+
return slot;
|
|
355261
|
+
}
|
|
355262
|
+
function releaseControlPortSlot(deviceId) {
|
|
355263
|
+
controlPortSlotsByDevice.delete(deviceId);
|
|
355264
|
+
}
|
|
355099
355265
|
async function executeMobileTest(options) {
|
|
355100
355266
|
const logLines = [];
|
|
355101
355267
|
const log2 = (msg) => logLines.push(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}`);
|
|
@@ -355115,6 +355281,7 @@ async function executeMobileTest(options) {
|
|
|
355115
355281
|
let captureStopped = false;
|
|
355116
355282
|
let recordingStarted = false;
|
|
355117
355283
|
let recordingStopped = false;
|
|
355284
|
+
let acquiredSlotDeviceId = null;
|
|
355118
355285
|
let video;
|
|
355119
355286
|
let failingSource;
|
|
355120
355287
|
const stopCapture = async () => {
|
|
@@ -355217,7 +355384,8 @@ async function executeMobileTest(options) {
|
|
|
355217
355384
|
log2(`Warning: Could not retrieve installed apps list. Proceeding with test execution.`);
|
|
355218
355385
|
}
|
|
355219
355386
|
log2(`Starting Appium session for ${options.target.appId} on ${selectedDevice.name}...`);
|
|
355220
|
-
const leasedPortOffset =
|
|
355387
|
+
const leasedPortOffset = acquireControlPortSlot(selectedDevice.id);
|
|
355388
|
+
acquiredSlotDeviceId = selectedDevice.id;
|
|
355221
355389
|
try {
|
|
355222
355390
|
sessionId = await createSessionWithRetry({
|
|
355223
355391
|
appId: options.target.appId,
|
|
@@ -355494,6 +355662,7 @@ async function executeMobileTest(options) {
|
|
|
355494
355662
|
await deleteSession(sessionId);
|
|
355495
355663
|
log2("Session destroyed");
|
|
355496
355664
|
}
|
|
355665
|
+
if (acquiredSlotDeviceId) releaseControlPortSlot(acquiredSlotDeviceId);
|
|
355497
355666
|
}
|
|
355498
355667
|
}
|
|
355499
355668
|
async function createMobileDiscoverySession(target, options) {
|
|
@@ -355539,21 +355708,28 @@ async function createMobileDiscoverySession(target, options) {
|
|
|
355539
355708
|
isPhysical: selectedDevice.isPhysical === true,
|
|
355540
355709
|
platformVersion: selectedDevice.platformVersion
|
|
355541
355710
|
});
|
|
355542
|
-
const
|
|
355543
|
-
|
|
355544
|
-
|
|
355545
|
-
|
|
355546
|
-
|
|
355547
|
-
|
|
355548
|
-
|
|
355549
|
-
|
|
355550
|
-
|
|
355711
|
+
const discoveryPortSlotDeviceId = selectedDevice.id;
|
|
355712
|
+
let sessionId;
|
|
355713
|
+
try {
|
|
355714
|
+
sessionId = await createSessionWithRetry({
|
|
355715
|
+
appId: target.appId,
|
|
355716
|
+
platform: target.platform,
|
|
355717
|
+
deviceId: selectedDevice.id,
|
|
355718
|
+
platformVersion: target.platformVersion ?? selectedDevice.platformVersion,
|
|
355719
|
+
isPhysical: selectedDevice.isPhysical,
|
|
355720
|
+
portOffset: acquireControlPortSlot(discoveryPortSlotDeviceId)
|
|
355721
|
+
});
|
|
355722
|
+
} catch (err) {
|
|
355723
|
+
releaseControlPortSlot(discoveryPortSlotDeviceId);
|
|
355724
|
+
throw err;
|
|
355725
|
+
}
|
|
355551
355726
|
if (target.launchMode === "DEEP_LINK" && target.deeplink) {
|
|
355552
355727
|
try {
|
|
355553
355728
|
await openDeepLink(sessionId, target.platform, target.deeplink, target.appId);
|
|
355554
355729
|
} catch (err) {
|
|
355555
355730
|
await deleteSession(sessionId).catch(() => {
|
|
355556
355731
|
});
|
|
355732
|
+
releaseControlPortSlot(discoveryPortSlotDeviceId);
|
|
355557
355733
|
throw new Error(
|
|
355558
355734
|
`Deep-link launch failed for mobile discovery: ${err instanceof Error ? err.message : String(err)}`
|
|
355559
355735
|
);
|
|
@@ -355575,6 +355751,7 @@ async function createMobileDiscoverySession(target, options) {
|
|
|
355575
355751
|
} catch {
|
|
355576
355752
|
}
|
|
355577
355753
|
await deleteSession(sessionId);
|
|
355754
|
+
releaseControlPortSlot(discoveryPortSlotDeviceId);
|
|
355578
355755
|
}
|
|
355579
355756
|
};
|
|
355580
355757
|
}
|
|
@@ -355592,14 +355769,14 @@ async function readElementAttribute(sessionId, elementId, name) {
|
|
|
355592
355769
|
}
|
|
355593
355770
|
function harvestFallbackXpath(platform3, attrs) {
|
|
355594
355771
|
if (platform3 === "IOS") {
|
|
355595
|
-
if (attrs.name) return `//*[@name=${
|
|
355772
|
+
if (attrs.name) return `//*[@name=${xpathLiteral3(attrs.name)}]`;
|
|
355596
355773
|
const text = attrs.label ?? attrs.value;
|
|
355597
|
-
if (text) return
|
|
355774
|
+
if (text) return identityTextXpath2(text);
|
|
355598
355775
|
return "//*";
|
|
355599
355776
|
}
|
|
355600
|
-
if (attrs.resourceId) return `//*[@resource-id=${
|
|
355601
|
-
if (attrs.text) return
|
|
355602
|
-
if (attrs.contentDesc) return `//*[@content-desc=${
|
|
355777
|
+
if (attrs.resourceId) return `//*[@resource-id=${xpathLiteral3(attrs.resourceId)}]`;
|
|
355778
|
+
if (attrs.text) return identityTextXpath2(attrs.text);
|
|
355779
|
+
if (attrs.contentDesc) return `//*[@content-desc=${xpathLiteral3(attrs.contentDesc)}]`;
|
|
355603
355780
|
return "//*";
|
|
355604
355781
|
}
|
|
355605
355782
|
async function readNativeElementBundle(sessionId, elementId, platform3) {
|
|
@@ -355952,6 +356129,7 @@ var sessionStartInFlight = null;
|
|
|
355952
356129
|
var sessionReaperTimer = null;
|
|
355953
356130
|
var mirrorClientsEmptyAt = null;
|
|
355954
356131
|
var mirrorSessionOwner = null;
|
|
356132
|
+
var actionInFlight = false;
|
|
355955
356133
|
var recordingWorkloadActive = false;
|
|
355956
356134
|
function setRecordingWorkloadActive(active) {
|
|
355957
356135
|
if (recordingWorkloadActive === active) return;
|
|
@@ -356181,7 +356359,11 @@ async function handleHealth(_req, res) {
|
|
|
356181
356359
|
// 409 — it must never be inferred from server-side global run counts, which
|
|
356182
356360
|
// include unrelated projects and runners.
|
|
356183
356361
|
executorBusy: executorBusyCount > 0,
|
|
356184
|
-
|
|
356362
|
+
// TRUE also during the multi-second session-START window: the claim loop
|
|
356363
|
+
// reads this to keep test runs off the recording device, and the Appium
|
|
356364
|
+
// session id only lands AFTER `POST /session` returns — without the
|
|
356365
|
+
// in-flight latch a run admitted in that window fights the recording.
|
|
356366
|
+
recordingActive: !!state.appiumSessionId && mirrorSessionOwner === "recording" || sessionStartInFlight !== null
|
|
356185
356367
|
});
|
|
356186
356368
|
}
|
|
356187
356369
|
function realDeviceCapabilities2(platform3) {
|
|
@@ -356259,7 +356441,11 @@ async function handleDevices(_req, res) {
|
|
|
356259
356441
|
sendJSON(res, 200, {
|
|
356260
356442
|
devices,
|
|
356261
356443
|
executorBusy: executorBusyCount > 0,
|
|
356262
|
-
|
|
356444
|
+
// TRUE also during the multi-second session-START window: the claim loop
|
|
356445
|
+
// reads this to keep test runs off the recording device, and the Appium
|
|
356446
|
+
// session id only lands AFTER `POST /session` returns — without the
|
|
356447
|
+
// in-flight latch a run admitted in that window fights the recording.
|
|
356448
|
+
recordingActive: !!state.appiumSessionId && mirrorSessionOwner === "recording" || sessionStartInFlight !== null
|
|
356263
356449
|
});
|
|
356264
356450
|
}
|
|
356265
356451
|
async function handleSessionStart(req, res) {
|
|
@@ -356348,6 +356534,8 @@ async function startSession(validated, res) {
|
|
|
356348
356534
|
state.platform = targetPlatform === "IOS" ? "IOS" : "ANDROID";
|
|
356349
356535
|
mirrorSessionOwner = "recording";
|
|
356350
356536
|
setRecordingWorkloadActive(true);
|
|
356537
|
+
mirrorClientsEmptyAt = Date.now();
|
|
356538
|
+
ensureSessionReaper();
|
|
356351
356539
|
if (launchMode === "DEEP_LINK" && deeplink) {
|
|
356352
356540
|
try {
|
|
356353
356541
|
const args = targetPlatform === "IOS" ? [{ url: deeplink, bundleId: appId }] : [{ url: deeplink, package: appId }];
|
|
@@ -356701,6 +356889,14 @@ async function handleAction(req, res) {
|
|
|
356701
356889
|
sendJSON(res, 400, { error: "No active Appium session" });
|
|
356702
356890
|
return;
|
|
356703
356891
|
}
|
|
356892
|
+
if (mirrorSessionOwner !== "recording") {
|
|
356893
|
+
sendJSON(res, 409, { error: "No recording session owns the device (a runner-driven session is active)." });
|
|
356894
|
+
return;
|
|
356895
|
+
}
|
|
356896
|
+
if (actionInFlight) {
|
|
356897
|
+
sendJSON(res, 409, { error: "Another action is still executing. Wait for it to finish." });
|
|
356898
|
+
return;
|
|
356899
|
+
}
|
|
356704
356900
|
validateSessionId(state.appiumSessionId);
|
|
356705
356901
|
const body = await parseBody(req);
|
|
356706
356902
|
const type = typeof body.type === "string" ? body.type : null;
|
|
@@ -356712,6 +356908,7 @@ async function handleAction(req, res) {
|
|
|
356712
356908
|
sendJSON(res, 400, { error: "Unknown or unsupported action type" });
|
|
356713
356909
|
return;
|
|
356714
356910
|
}
|
|
356911
|
+
actionInFlight = true;
|
|
356715
356912
|
try {
|
|
356716
356913
|
const sessionPath = `/session/${state.appiumSessionId}`;
|
|
356717
356914
|
switch (type) {
|
|
@@ -356784,15 +356981,23 @@ async function handleAction(req, res) {
|
|
|
356784
356981
|
sendJSON(res, 400, { error: `Unknown action type: ${type}` });
|
|
356785
356982
|
return;
|
|
356786
356983
|
}
|
|
356787
|
-
|
|
356984
|
+
await waitForUiSettle(sessionPath);
|
|
356985
|
+
const screenshot = await getScreenshot2();
|
|
356986
|
+
let source = await getPageSource2();
|
|
356987
|
+
if (source === null) {
|
|
356988
|
+
source = await getPageSource2();
|
|
356989
|
+
}
|
|
356788
356990
|
sendJSON(res, 200, {
|
|
356789
356991
|
ok: true,
|
|
356790
356992
|
screenshot: screenshot ?? null,
|
|
356791
356993
|
source: source ?? null,
|
|
356994
|
+
...source === null || source === void 0 ? { sourceUnavailable: true } : {},
|
|
356792
356995
|
timestamp: Date.now()
|
|
356793
356996
|
});
|
|
356794
356997
|
} catch (err) {
|
|
356795
356998
|
sendJSON(res, 500, { error: `Action failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
356999
|
+
} finally {
|
|
357000
|
+
actionInFlight = false;
|
|
356796
357001
|
}
|
|
356797
357002
|
}
|
|
356798
357003
|
async function handleScreenshot(_req, res) {
|
|
@@ -357030,7 +357235,11 @@ function getBridgeState() {
|
|
|
357030
357235
|
tunnelUrl: state.tunnelUrl,
|
|
357031
357236
|
tunnelToken: state.tunnelToken,
|
|
357032
357237
|
hasActiveSession: !!state.appiumSessionId,
|
|
357033
|
-
|
|
357238
|
+
// TRUE also during the multi-second session-START window: the claim loop
|
|
357239
|
+
// reads this to keep test runs off the recording device, and the Appium
|
|
357240
|
+
// session id only lands AFTER `POST /session` returns — without the
|
|
357241
|
+
// in-flight latch a run admitted in that window fights the recording.
|
|
357242
|
+
recordingActive: !!state.appiumSessionId && mirrorSessionOwner === "recording" || sessionStartInFlight !== null,
|
|
357034
357243
|
platform: state.platform
|
|
357035
357244
|
};
|
|
357036
357245
|
}
|