@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.mjs
CHANGED
|
@@ -2241,6 +2241,18 @@ function selectorForValue(value) {
|
|
|
2241
2241
|
function isIdentityMobileXpath(value) {
|
|
2242
2242
|
return /@[A-Za-z_:][-A-Za-z0-9_:.]*\s*=/.test(value) && !/(?:contains|starts-with)\s*\(/i.test(value);
|
|
2243
2243
|
}
|
|
2244
|
+
function xpathLiteral(value) {
|
|
2245
|
+
if (!value.includes("'"))
|
|
2246
|
+
return `'${value}'`;
|
|
2247
|
+
if (!value.includes('"'))
|
|
2248
|
+
return `"${value}"`;
|
|
2249
|
+
const parts = value.split("'").map((p) => `'${p}'`);
|
|
2250
|
+
return `concat(${parts.join(`, "'", `)})`;
|
|
2251
|
+
}
|
|
2252
|
+
function identityTextXpath(value) {
|
|
2253
|
+
const lit = xpathLiteral(value);
|
|
2254
|
+
return `//*[@text=${lit} or @content-desc=${lit} or @label=${lit} or @name=${lit} or @value=${lit}]`;
|
|
2255
|
+
}
|
|
2244
2256
|
function mobileLocatorBundleHasIdentity(bundle) {
|
|
2245
2257
|
if (!bundle)
|
|
2246
2258
|
return false;
|
|
@@ -2267,6 +2279,13 @@ function selectorCandidatesForStep(step) {
|
|
|
2267
2279
|
candidates.push(`~${bundle.accessibilityId}`);
|
|
2268
2280
|
if (nonEmpty(bundle.resourceId))
|
|
2269
2281
|
candidates.push(`id=${bundle.resourceId}`);
|
|
2282
|
+
const isGateStep = step.action === "waitForElement" || step.action === "assertVisible" || step.action === "assertText";
|
|
2283
|
+
const textIdentity = isGateStep || !isEditableMobileClassName(bundle.className) ? bundle.text : void 0;
|
|
2284
|
+
for (const value of [textIdentity, bundle.contentDesc]) {
|
|
2285
|
+
const trimmed = nonEmpty(value);
|
|
2286
|
+
if (trimmed)
|
|
2287
|
+
candidates.push(identityTextXpath(trimmed));
|
|
2288
|
+
}
|
|
2270
2289
|
if (nonEmpty(bundle.xpath))
|
|
2271
2290
|
candidates.push(bundle.xpath);
|
|
2272
2291
|
}
|
|
@@ -2715,7 +2734,7 @@ function uniq(values) {
|
|
|
2715
2734
|
}
|
|
2716
2735
|
return result;
|
|
2717
2736
|
}
|
|
2718
|
-
function
|
|
2737
|
+
function xpathLiteral2(value) {
|
|
2719
2738
|
if (!value.includes("'"))
|
|
2720
2739
|
return `'${value}'`;
|
|
2721
2740
|
if (!value.includes('"'))
|
|
@@ -2723,7 +2742,7 @@ function xpathLiteral(value) {
|
|
|
2723
2742
|
return `concat(${value.split("'").map((part) => `'${part}'`).join(`, "'", `)})`;
|
|
2724
2743
|
}
|
|
2725
2744
|
function mobileTextXPath(value, platform3) {
|
|
2726
|
-
const literal =
|
|
2745
|
+
const literal = xpathLiteral2(value);
|
|
2727
2746
|
if (platform3 === "IOS") {
|
|
2728
2747
|
return `//*[@name=${literal} or @label=${literal} or @value=${literal}]`;
|
|
2729
2748
|
}
|
|
@@ -3871,6 +3890,43 @@ function accumulateStatements(lines) {
|
|
|
3871
3890
|
}
|
|
3872
3891
|
return out;
|
|
3873
3892
|
}
|
|
3893
|
+
function classifyMobileAssertValue(value, context = {}) {
|
|
3894
|
+
const text = value.trim();
|
|
3895
|
+
if (!text)
|
|
3896
|
+
return null;
|
|
3897
|
+
if (/^\{\{[A-Z0-9_]+\}\}$/.test(text))
|
|
3898
|
+
return null;
|
|
3899
|
+
const t1 = matchFirst(TIER1_PATTERNS, text);
|
|
3900
|
+
if (t1)
|
|
3901
|
+
return { tier: 1, reason: t1[1] };
|
|
3902
|
+
const authIdentityValues = [];
|
|
3903
|
+
if (context.authenticatedUserEmail)
|
|
3904
|
+
authIdentityValues.push(context.authenticatedUserEmail);
|
|
3905
|
+
if (context.authenticatedUserName && context.authenticatedUserName.length >= 3) {
|
|
3906
|
+
authIdentityValues.push(context.authenticatedUserName);
|
|
3907
|
+
}
|
|
3908
|
+
for (const auth of authIdentityValues) {
|
|
3909
|
+
if (auth.length >= 3 && text.toLowerCase().includes(auth.toLowerCase())) {
|
|
3910
|
+
return { tier: 2, reason: "asserts the authenticated user identity \u2014 fails under any other test account" };
|
|
3911
|
+
}
|
|
3912
|
+
}
|
|
3913
|
+
const knownValues = [
|
|
3914
|
+
...context.knownUserInputs ?? [],
|
|
3915
|
+
...context.scenarioCreatedValues ?? []
|
|
3916
|
+
].filter((v) => {
|
|
3917
|
+
for (const auth of authIdentityValues) {
|
|
3918
|
+
if (auth.length >= 3 && v.toLowerCase().includes(auth.toLowerCase()))
|
|
3919
|
+
return false;
|
|
3920
|
+
}
|
|
3921
|
+
return true;
|
|
3922
|
+
});
|
|
3923
|
+
if (containsKnownValue(text, knownValues))
|
|
3924
|
+
return null;
|
|
3925
|
+
const t2 = matchFirst(TIER2_PATTERNS, text);
|
|
3926
|
+
if (t2)
|
|
3927
|
+
return { tier: 2, reason: t2[1] };
|
|
3928
|
+
return null;
|
|
3929
|
+
}
|
|
3874
3930
|
function parenDepthDelta(text) {
|
|
3875
3931
|
let depth = 0;
|
|
3876
3932
|
let quote = null;
|
|
@@ -6044,6 +6100,7 @@ __export(dist_exports, {
|
|
|
6044
6100
|
classifyAssertionBlock: () => classifyAssertionBlock,
|
|
6045
6101
|
classifyAssertionLine: () => classifyAssertionLine,
|
|
6046
6102
|
classifyErrorMessage: () => classifyErrorMessage,
|
|
6103
|
+
classifyMobileAssertValue: () => classifyMobileAssertValue,
|
|
6047
6104
|
classifyTestHealth: () => classifyTestHealth,
|
|
6048
6105
|
coldSessionConfigMessage: () => coldSessionConfigMessage,
|
|
6049
6106
|
computeMobileSwipePointers: () => computeMobileSwipePointers,
|
|
@@ -80888,6 +80945,45 @@ ${constraints.join("\n")}`);
|
|
|
80888
80945
|
}
|
|
80889
80946
|
});
|
|
80890
80947
|
|
|
80948
|
+
// ../runner-core/dist/services/discovery-lint-flags.js
|
|
80949
|
+
var require_discovery_lint_flags = __commonJS({
|
|
80950
|
+
"../runner-core/dist/services/discovery-lint-flags.js"(exports) {
|
|
80951
|
+
"use strict";
|
|
80952
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
80953
|
+
exports.structuralLocatorLintMode = structuralLocatorLintMode;
|
|
80954
|
+
exports.isPwMethodPreflightEnabled = isPwMethodPreflightEnabled;
|
|
80955
|
+
exports.isVqaLoginDedupeEnabled = isVqaLoginDedupeEnabled;
|
|
80956
|
+
exports.mobileSelectorQualityLintMode = mobileSelectorQualityLintMode;
|
|
80957
|
+
exports.isTestgenEmailVerifyEnabled = isTestgenEmailVerifyEnabled;
|
|
80958
|
+
function strictOn(value) {
|
|
80959
|
+
return value === "true" || value === "1";
|
|
80960
|
+
}
|
|
80961
|
+
function structuralLocatorLintMode() {
|
|
80962
|
+
const v = (process.env.DISCOVERY_STRUCTURAL_LOCATOR_LINT || "").trim().toLowerCase();
|
|
80963
|
+
if (v === "warn")
|
|
80964
|
+
return "warn";
|
|
80965
|
+
if (v === "true" || v === "1" || v === "enforce")
|
|
80966
|
+
return "enforce";
|
|
80967
|
+
return "off";
|
|
80968
|
+
}
|
|
80969
|
+
function isPwMethodPreflightEnabled() {
|
|
80970
|
+
return strictOn(process.env.DISCOVERY_PW_METHOD_PREFLIGHT);
|
|
80971
|
+
}
|
|
80972
|
+
function isVqaLoginDedupeEnabled() {
|
|
80973
|
+
return strictOn(process.env.DISCOVERY_VQALOGIN_DEDUPE);
|
|
80974
|
+
}
|
|
80975
|
+
function mobileSelectorQualityLintMode() {
|
|
80976
|
+
const v = (process.env.MOBILE_SELECTOR_QUALITY_LINT || "").trim().toLowerCase();
|
|
80977
|
+
if (v === "true" || v === "1" || v === "enforce")
|
|
80978
|
+
return "enforce";
|
|
80979
|
+
return "off";
|
|
80980
|
+
}
|
|
80981
|
+
function isTestgenEmailVerifyEnabled() {
|
|
80982
|
+
return strictOn(process.env.DISCOVERY_TESTGEN_EMAIL_VERIFY);
|
|
80983
|
+
}
|
|
80984
|
+
}
|
|
80985
|
+
});
|
|
80986
|
+
|
|
80891
80987
|
// ../runner-core/dist/services/mobile/mobile-generator.js
|
|
80892
80988
|
var require_mobile_generator = __commonJS({
|
|
80893
80989
|
"../runner-core/dist/services/mobile/mobile-generator.js"(exports) {
|
|
@@ -80907,6 +81003,7 @@ var require_mobile_generator = __commonJS({
|
|
|
80907
81003
|
var ai_queue_js_1 = require_ai_queue();
|
|
80908
81004
|
var ai_retry_js_1 = require_ai_retry();
|
|
80909
81005
|
var mobile_session_keepalive_js_1 = require_mobile_session_keepalive();
|
|
81006
|
+
var discovery_lint_flags_js_1 = require_discovery_lint_flags();
|
|
80910
81007
|
var mobile_source_parser_js_1 = require_mobile_source_parser();
|
|
80911
81008
|
var mobile_source_parser_js_2 = require_mobile_source_parser();
|
|
80912
81009
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
@@ -81076,7 +81173,15 @@ var require_mobile_generator = __commonJS({
|
|
|
81076
81173
|
properties: {
|
|
81077
81174
|
name: { type: "string", description: 'Descriptive test name (e.g. "Login - Happy Path").' },
|
|
81078
81175
|
description: { type: "string", description: "One-line description of what the test proves." },
|
|
81079
|
-
verified: { type: "boolean", description: "True only if you asserted the outcome LIVE with an assert tool." }
|
|
81176
|
+
verified: { type: "boolean", description: "True only if you asserted the outcome LIVE with an assert tool." },
|
|
81177
|
+
suspectedBug: {
|
|
81178
|
+
type: "boolean",
|
|
81179
|
+
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."
|
|
81180
|
+
},
|
|
81181
|
+
bugDescription: {
|
|
81182
|
+
type: "string",
|
|
81183
|
+
description: "What is broken in the APP (not the test) \u2014 concrete screen + observed vs expected."
|
|
81184
|
+
}
|
|
81080
81185
|
},
|
|
81081
81186
|
required: ["name", "description", "verified"]
|
|
81082
81187
|
}
|
|
@@ -81322,6 +81427,8 @@ var require_mobile_generator = __commonJS({
|
|
|
81322
81427
|
- Assert the OUTCOME of the flow (the new screen / new content), not the control you just tapped.
|
|
81323
81428
|
- 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.
|
|
81324
81429
|
- 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.
|
|
81430
|
+
- 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.
|
|
81431
|
+
- 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.
|
|
81325
81432
|
- 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.
|
|
81326
81433
|
- 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.
|
|
81327
81434
|
- 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.
|
|
@@ -81427,6 +81534,7 @@ Start by calling mobile_snapshot, then execute the steps above \u2014 the minimu
|
|
|
81427
81534
|
let assertCount = 0;
|
|
81428
81535
|
let finish = null;
|
|
81429
81536
|
let finishIntentRejections = 0;
|
|
81537
|
+
let finishStrengthRejections = 0;
|
|
81430
81538
|
let emptyTurnsAfterFinishReject = 0;
|
|
81431
81539
|
let latest = null;
|
|
81432
81540
|
const getLatest = () => latest;
|
|
@@ -81919,31 +82027,49 @@ ${statePacket}`, redactionCredentials) },
|
|
|
81919
82027
|
const claimedVerified = Boolean(toolArgs.verified);
|
|
81920
82028
|
const intentReason = scenarioIntentMismatchReason(scenario, actions);
|
|
81921
82029
|
const verifiedWithoutAssert = claimedVerified && assertCount === 0;
|
|
81922
|
-
const
|
|
81923
|
-
|
|
81924
|
-
|
|
82030
|
+
const intentStyleReject = finishIntentRejections < MAX_FINISH_INTENT_REJECTIONS && iteration < maxIterations && (intentReason !== null || verifiedWithoutAssert);
|
|
82031
|
+
let strengthRejectReason = null;
|
|
82032
|
+
if (!intentStyleReject && claimedVerified && assertCount > 0 && intentReason === null && finishStrengthRejections < 1 && iteration < maxIterations) {
|
|
82033
|
+
const verdict = evaluateMobileAssertionStrength(actions);
|
|
82034
|
+
if (verdict.severity === "block") {
|
|
82035
|
+
strengthRejectReason = verdict.reason;
|
|
82036
|
+
}
|
|
82037
|
+
}
|
|
82038
|
+
if (intentStyleReject || strengthRejectReason !== null) {
|
|
82039
|
+
if (intentStyleReject)
|
|
82040
|
+
finishIntentRejections += 1;
|
|
82041
|
+
if (strengthRejectReason !== null)
|
|
82042
|
+
finishStrengthRejections += 1;
|
|
81925
82043
|
const reasons = [];
|
|
81926
82044
|
if (intentReason)
|
|
81927
82045
|
reasons.push(intentReason);
|
|
81928
82046
|
if (verifiedWithoutAssert) {
|
|
81929
82047
|
reasons.push("claimed verified:true but never called mobile_assert_visible / mobile_assert_text");
|
|
81930
82048
|
}
|
|
82049
|
+
if (strengthRejectReason !== null) {
|
|
82050
|
+
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.`);
|
|
82051
|
+
}
|
|
81931
82052
|
const reasonText = reasons.join("; ");
|
|
81932
|
-
log2(` finish_generate REJECTED (${finishIntentRejections}/${MAX_FINISH_INTENT_REJECTIONS}): ${reasonText}`);
|
|
82053
|
+
log2(` finish_generate REJECTED (intent ${finishIntentRejections}/${MAX_FINISH_INTENT_REJECTIONS}, strength ${finishStrengthRejections}/1): ${reasonText}`);
|
|
81933
82054
|
pushResult({
|
|
81934
82055
|
ok: false,
|
|
81935
82056
|
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.`,
|
|
81936
82057
|
intentMismatch: intentReason,
|
|
81937
82058
|
verifiedWithoutAssert,
|
|
81938
|
-
|
|
81939
|
-
|
|
82059
|
+
...strengthRejectReason !== null ? { strengthBlock: strengthRejectReason } : {},
|
|
82060
|
+
rejection: finishIntentRejections + finishStrengthRejections,
|
|
82061
|
+
maxRejections: MAX_FINISH_INTENT_REJECTIONS + 1
|
|
81940
82062
|
});
|
|
81941
82063
|
continue;
|
|
81942
82064
|
}
|
|
81943
82065
|
finish = {
|
|
81944
82066
|
name: redactCredentialValues(nonEmpty3(String(toolArgs.name ?? "")) ?? scenario.name, redactionCredentials),
|
|
81945
82067
|
description: redactCredentialValues(nonEmpty3(String(toolArgs.description ?? "")) ?? scenario.goal, redactionCredentials),
|
|
81946
|
-
verified: claimedVerified
|
|
82068
|
+
verified: claimedVerified,
|
|
82069
|
+
...toolArgs.suspectedBug === true ? {
|
|
82070
|
+
suspectedBug: true,
|
|
82071
|
+
bugDescription: redactCredentialValues(nonEmpty3(String(toolArgs.bugDescription ?? "")) ?? "", redactionCredentials)
|
|
82072
|
+
} : {}
|
|
81947
82073
|
};
|
|
81948
82074
|
log2(` finish_generate: "${finish.name}" (claimed verified: ${finish.verified}, asserts: ${assertCount})`);
|
|
81949
82075
|
pushResult({ acknowledged: true });
|
|
@@ -82007,6 +82133,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82007
82133
|
ok: false,
|
|
82008
82134
|
error: `Unknown element ref "${String(ref)}". Call mobile_snapshot first; refs are only valid for the latest snapshot.`
|
|
82009
82135
|
});
|
|
82136
|
+
const recordedTypeValues = () => recordedActions.filter((action) => action.type === "TYPE" && typeof action.value === "string" && action.value.length > 0).map((action) => action.value);
|
|
82010
82137
|
const actionPayload = (result, absorbed) => {
|
|
82011
82138
|
const blocked = Boolean(absorbed.externalBlocked || absorbed.transientExternal);
|
|
82012
82139
|
return {
|
|
@@ -82407,7 +82534,8 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82407
82534
|
latest,
|
|
82408
82535
|
screenName,
|
|
82409
82536
|
platform: platform3,
|
|
82410
|
-
recordAction
|
|
82537
|
+
recordAction,
|
|
82538
|
+
typedValues: recordedTypeValues
|
|
82411
82539
|
});
|
|
82412
82540
|
if (!recorded.ok)
|
|
82413
82541
|
return recorded;
|
|
@@ -82476,6 +82604,15 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82476
82604
|
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.`
|
|
82477
82605
|
};
|
|
82478
82606
|
}
|
|
82607
|
+
const volatileExpected = (0, shared_1.classifyMobileAssertValue)(expected, {
|
|
82608
|
+
knownUserInputs: recordedTypeValues()
|
|
82609
|
+
});
|
|
82610
|
+
if (volatileExpected) {
|
|
82611
|
+
return {
|
|
82612
|
+
ok: false,
|
|
82613
|
+
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.`
|
|
82614
|
+
};
|
|
82615
|
+
}
|
|
82479
82616
|
const metadata = metadataForElement(element, screenName(), platform3);
|
|
82480
82617
|
const expectedNeedle = expected.trim().toLowerCase();
|
|
82481
82618
|
const identityValues = [element.accessibilityId, element.resourceId].map((value) => value?.trim().toLowerCase()).filter((value) => Boolean(value));
|
|
@@ -82531,6 +82668,17 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82531
82668
|
if (!accessibilityId && !text) {
|
|
82532
82669
|
return { ok: false, error: "mobile_assert_visible needs a ref (preferred), accessibilityId, or text." };
|
|
82533
82670
|
}
|
|
82671
|
+
if (!accessibilityId && text) {
|
|
82672
|
+
const volatileIdentity = (0, shared_1.classifyMobileAssertValue)(text, {
|
|
82673
|
+
knownUserInputs: ctx.typedValues?.() ?? []
|
|
82674
|
+
});
|
|
82675
|
+
if (volatileIdentity) {
|
|
82676
|
+
return {
|
|
82677
|
+
ok: false,
|
|
82678
|
+
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).`
|
|
82679
|
+
};
|
|
82680
|
+
}
|
|
82681
|
+
}
|
|
82534
82682
|
const directTarget = accessibilityId ?? text;
|
|
82535
82683
|
if (directTarget && isStructuralAssertionIdentity(directTarget)) {
|
|
82536
82684
|
return {
|
|
@@ -83166,7 +83314,21 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83166
83314
|
if (claimedVerified && !verified) {
|
|
83167
83315
|
log2(" downgraded verified -> false: model claimed verified but called zero assert tools.");
|
|
83168
83316
|
}
|
|
83169
|
-
const
|
|
83317
|
+
const baseStrengthVerdict = evaluateMobileAssertionStrength(actions);
|
|
83318
|
+
const bugDescription = recording.finish?.bugDescription?.trim() ?? "";
|
|
83319
|
+
const bugWitnessHonored = process.env.DISCOVERY_BUG_WITNESS_ENABLED === "true" && recording.finish?.suspectedBug === true && bugDescription.length >= 12 && recording.assertCount > 0;
|
|
83320
|
+
if (recording.finish?.suspectedBug === true && !bugWitnessHonored) {
|
|
83321
|
+
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."));
|
|
83322
|
+
}
|
|
83323
|
+
const strengthVerdict = bugWitnessHonored ? {
|
|
83324
|
+
...baseStrengthVerdict,
|
|
83325
|
+
ok: false,
|
|
83326
|
+
severity: "route-bug",
|
|
83327
|
+
reason: `Model declared an app bug during generation: ${bugDescription}`
|
|
83328
|
+
} : baseStrengthVerdict;
|
|
83329
|
+
if (bugWitnessHonored) {
|
|
83330
|
+
log2(` bug-witness honored \u2014 routing as app bug: ${bugDescription}`);
|
|
83331
|
+
}
|
|
83170
83332
|
if (strengthVerdict.severity === "block" || strengthVerdict.severity === "route-bug") {
|
|
83171
83333
|
if (verified) {
|
|
83172
83334
|
log2(` downgraded verified -> false: assertion strength ${strengthVerdict.severity} \u2014 ${strengthVerdict.reason}`);
|
|
@@ -83182,7 +83344,11 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83182
83344
|
verified = false;
|
|
83183
83345
|
}
|
|
83184
83346
|
const platformTag2 = platform3 === "IOS" ? "@ios" : "@android";
|
|
83185
|
-
const
|
|
83347
|
+
const selectorLintEnforced = (0, discovery_lint_flags_js_1.mobileSelectorQualityLintMode)() === "enforce" && selectorQuality.summary.high > 0;
|
|
83348
|
+
if (selectorLintEnforced && verified) {
|
|
83349
|
+
log2(` selector-quality lint (enforce): ${selectorQuality.summary.high} high-severity locator issue(s) \u2014 tagging @needs-review.`);
|
|
83350
|
+
}
|
|
83351
|
+
const reviewTag = verified && !selectorLintEnforced ? "@verified" : "@needs-review";
|
|
83186
83352
|
const tags = ["@discovered", "@mobile", platformTag2, reviewTag];
|
|
83187
83353
|
if (strengthVerdict.severity === "downgrade")
|
|
83188
83354
|
tags.push("@smoke");
|
|
@@ -86262,38 +86428,6 @@ var require_run_callback_proxy = __commonJS({
|
|
|
86262
86428
|
}
|
|
86263
86429
|
});
|
|
86264
86430
|
|
|
86265
|
-
// ../runner-core/dist/services/discovery-lint-flags.js
|
|
86266
|
-
var require_discovery_lint_flags = __commonJS({
|
|
86267
|
-
"../runner-core/dist/services/discovery-lint-flags.js"(exports) {
|
|
86268
|
-
"use strict";
|
|
86269
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
86270
|
-
exports.structuralLocatorLintMode = structuralLocatorLintMode;
|
|
86271
|
-
exports.isPwMethodPreflightEnabled = isPwMethodPreflightEnabled;
|
|
86272
|
-
exports.isVqaLoginDedupeEnabled = isVqaLoginDedupeEnabled;
|
|
86273
|
-
exports.isTestgenEmailVerifyEnabled = isTestgenEmailVerifyEnabled;
|
|
86274
|
-
function strictOn(value) {
|
|
86275
|
-
return value === "true" || value === "1";
|
|
86276
|
-
}
|
|
86277
|
-
function structuralLocatorLintMode() {
|
|
86278
|
-
const v = (process.env.DISCOVERY_STRUCTURAL_LOCATOR_LINT || "").trim().toLowerCase();
|
|
86279
|
-
if (v === "warn")
|
|
86280
|
-
return "warn";
|
|
86281
|
-
if (v === "true" || v === "1" || v === "enforce")
|
|
86282
|
-
return "enforce";
|
|
86283
|
-
return "off";
|
|
86284
|
-
}
|
|
86285
|
-
function isPwMethodPreflightEnabled() {
|
|
86286
|
-
return strictOn(process.env.DISCOVERY_PW_METHOD_PREFLIGHT);
|
|
86287
|
-
}
|
|
86288
|
-
function isVqaLoginDedupeEnabled() {
|
|
86289
|
-
return strictOn(process.env.DISCOVERY_VQALOGIN_DEDUPE);
|
|
86290
|
-
}
|
|
86291
|
-
function isTestgenEmailVerifyEnabled() {
|
|
86292
|
-
return strictOn(process.env.DISCOVERY_TESTGEN_EMAIL_VERIFY);
|
|
86293
|
-
}
|
|
86294
|
-
}
|
|
86295
|
-
});
|
|
86296
|
-
|
|
86297
86431
|
// ../runner-core/dist/services/auth-recipe.js
|
|
86298
86432
|
var require_auth_recipe = __commonJS({
|
|
86299
86433
|
"../runner-core/dist/services/auth-recipe.js"(exports) {
|
|
@@ -347792,12 +347926,7 @@ Write valid JSON only (no markdown fences inside files). Be exhaustive.`;
|
|
|
347792
347926
|
const o = value;
|
|
347793
347927
|
if (!isNonEmptyString(o.name))
|
|
347794
347928
|
return { ok: false, reason: "name: missing" };
|
|
347795
|
-
|
|
347796
|
-
return { ok: false, reason: "appiumCode: missing" };
|
|
347797
|
-
const code = o.appiumCode;
|
|
347798
|
-
if (!/webdriverio|remote\s*\(|driver\s*\./i.test(code)) {
|
|
347799
|
-
return { ok: false, reason: "appiumCode: not a WebdriverIO/Appium script" };
|
|
347800
|
-
}
|
|
347929
|
+
const code = isNonEmptyString(o.appiumCode) ? o.appiumCode : "";
|
|
347801
347930
|
if (!Array.isArray(o.steps) || o.steps.length === 0) {
|
|
347802
347931
|
return { ok: false, reason: "steps: missing or empty" };
|
|
347803
347932
|
}
|
|
@@ -351100,7 +351229,7 @@ var require_package4 = __commonJS({
|
|
|
351100
351229
|
"package.json"(exports, module) {
|
|
351101
351230
|
module.exports = {
|
|
351102
351231
|
name: "@validate.qa/runner",
|
|
351103
|
-
version: "1.0.
|
|
351232
|
+
version: "1.0.67",
|
|
351104
351233
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
351105
351234
|
bin: {
|
|
351106
351235
|
"validate-runner": "dist/cli.js"
|
|
@@ -351496,6 +351625,9 @@ function sanitizeAppiumLogLine(line) {
|
|
|
351496
351625
|
}
|
|
351497
351626
|
function setAppiumWorkloadActive(active) {
|
|
351498
351627
|
activeWorkloadCount = active ? activeWorkloadCount + 1 : Math.max(0, activeWorkloadCount - 1);
|
|
351628
|
+
if (activeWorkloadCount === 0) {
|
|
351629
|
+
consecutiveHealthFailures = 0;
|
|
351630
|
+
}
|
|
351499
351631
|
}
|
|
351500
351632
|
function shouldRestartAfterHealthFailure(workloadCount, failureCount) {
|
|
351501
351633
|
return workloadCount <= 0 && failureCount >= HEALTH_FAILURES_BEFORE_RESTART;
|
|
@@ -351991,17 +352123,31 @@ function rectMatchesIntendedBounds(rect, bounds) {
|
|
|
351991
352123
|
return rectCenterInBounds || boundsCenterInRect;
|
|
351992
352124
|
}
|
|
351993
352125
|
async function resolveTypeTargetElementId(sessionPath, bounds) {
|
|
351994
|
-
const
|
|
352126
|
+
const rawBox = bounds !== void 0 ? normaliseBounds(bounds) : null;
|
|
352127
|
+
const box = rawBox && rawBox.width > 0 && rawBox.height > 0 ? rawBox : null;
|
|
351995
352128
|
let elementId = await getActiveElementId(sessionPath);
|
|
351996
|
-
|
|
352129
|
+
let rejectedElementId = null;
|
|
352130
|
+
let rejectedForMismatch = false;
|
|
352131
|
+
if (elementId && box) {
|
|
351997
352132
|
const rect = await getElementRect(sessionPath, elementId);
|
|
351998
352133
|
if (!rect || !rectMatchesIntendedBounds(rect, box)) {
|
|
352134
|
+
rejectedElementId = elementId;
|
|
352135
|
+
rejectedForMismatch = !!rect;
|
|
351999
352136
|
elementId = null;
|
|
352000
352137
|
}
|
|
352001
352138
|
}
|
|
352002
352139
|
if (!elementId && bounds !== void 0 && await tapBoundsCenter(sessionPath, bounds)) {
|
|
352003
352140
|
await new Promise((resolve2) => setTimeout(resolve2, MOBILE_FOCUS_SETTLE_MS));
|
|
352004
352141
|
elementId = await getActiveElementId(sessionPath);
|
|
352142
|
+
if (elementId && box) {
|
|
352143
|
+
const rect = await getElementRect(sessionPath, elementId);
|
|
352144
|
+
const provablyWrong = rect ? !rectMatchesIntendedBounds(rect, box) : rejectedForMismatch && elementId === rejectedElementId;
|
|
352145
|
+
if (provablyWrong) {
|
|
352146
|
+
throw new Error(
|
|
352147
|
+
"Could not focus the intended field: a different element kept keyboard focus after tapping the target bounds. Tap the field directly, then retry."
|
|
352148
|
+
);
|
|
352149
|
+
}
|
|
352150
|
+
}
|
|
352005
352151
|
}
|
|
352006
352152
|
return elementId;
|
|
352007
352153
|
}
|
|
@@ -354580,6 +354726,13 @@ function buildLocatorStrategies(target, platform3) {
|
|
|
354580
354726
|
if (bundle) {
|
|
354581
354727
|
if (bundle.accessibilityId) strategies.push({ using: "accessibility id", value: bundle.accessibilityId });
|
|
354582
354728
|
if (bundle.resourceId) strategies.push({ using: "id", value: bundle.resourceId });
|
|
354729
|
+
for (const value of [
|
|
354730
|
+
isEditableMobileClassName(bundle.className) ? void 0 : bundle.text,
|
|
354731
|
+
bundle.contentDesc
|
|
354732
|
+
]) {
|
|
354733
|
+
const trimmed = value?.trim();
|
|
354734
|
+
if (trimmed) strategies.push({ using: "xpath", value: identityTextXpath2(trimmed) });
|
|
354735
|
+
}
|
|
354583
354736
|
if (bundle.xpath) strategies.push({ using: "xpath", value: bundle.xpath });
|
|
354584
354737
|
}
|
|
354585
354738
|
if (!(platform3 && hasPlatformLocator(target, platform3))) {
|
|
@@ -354597,14 +354750,14 @@ function dedupeStrategies(strategies) {
|
|
|
354597
354750
|
(strategy, index, list) => list.findIndex((candidate) => candidate.using === strategy.using && candidate.value === strategy.value) === index
|
|
354598
354751
|
);
|
|
354599
354752
|
}
|
|
354600
|
-
function
|
|
354753
|
+
function xpathLiteral3(value) {
|
|
354601
354754
|
if (!value.includes("'")) return `'${value}'`;
|
|
354602
354755
|
if (!value.includes('"')) return `"${value}"`;
|
|
354603
354756
|
const parts = value.split("'").map((p) => `'${p}'`);
|
|
354604
354757
|
return `concat(${parts.join(`, "'", `)})`;
|
|
354605
354758
|
}
|
|
354606
|
-
function
|
|
354607
|
-
const lit =
|
|
354759
|
+
function identityTextXpath2(value) {
|
|
354760
|
+
const lit = xpathLiteral3(value);
|
|
354608
354761
|
return `//*[@text=${lit} or @content-desc=${lit} or @label=${lit} or @name=${lit} or @value=${lit}]`;
|
|
354609
354762
|
}
|
|
354610
354763
|
function isIdentityXpath(value) {
|
|
@@ -354624,7 +354777,7 @@ function buildIdentityLocatorStrategies(target, platform3) {
|
|
|
354624
354777
|
if (bundle.resourceId) strategies.push({ using: "id", value: bundle.resourceId });
|
|
354625
354778
|
for (const value of [bundle.text, bundle.contentDesc]) {
|
|
354626
354779
|
const trimmed = value?.trim();
|
|
354627
|
-
if (trimmed) strategies.push({ using: "xpath", value:
|
|
354780
|
+
if (trimmed) strategies.push({ using: "xpath", value: identityTextXpath2(trimmed) });
|
|
354628
354781
|
}
|
|
354629
354782
|
if (bundle.xpath && isIdentityXpath(bundle.xpath)) strategies.push({ using: "xpath", value: bundle.xpath });
|
|
354630
354783
|
}
|
|
@@ -355104,6 +355257,19 @@ async function readElementTextCandidates(sessionId, elementId, actualText) {
|
|
|
355104
355257
|
}
|
|
355105
355258
|
return values;
|
|
355106
355259
|
}
|
|
355260
|
+
var controlPortSlotsByDevice = /* @__PURE__ */ new Map();
|
|
355261
|
+
function acquireControlPortSlot(deviceId) {
|
|
355262
|
+
const existing = controlPortSlotsByDevice.get(deviceId);
|
|
355263
|
+
if (existing !== void 0) return existing;
|
|
355264
|
+
const taken = new Set(controlPortSlotsByDevice.values());
|
|
355265
|
+
let slot = 0;
|
|
355266
|
+
while (taken.has(slot)) slot += 1;
|
|
355267
|
+
controlPortSlotsByDevice.set(deviceId, slot);
|
|
355268
|
+
return slot;
|
|
355269
|
+
}
|
|
355270
|
+
function releaseControlPortSlot(deviceId) {
|
|
355271
|
+
controlPortSlotsByDevice.delete(deviceId);
|
|
355272
|
+
}
|
|
355107
355273
|
async function executeMobileTest(options) {
|
|
355108
355274
|
const logLines = [];
|
|
355109
355275
|
const log2 = (msg) => logLines.push(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}`);
|
|
@@ -355123,6 +355289,7 @@ async function executeMobileTest(options) {
|
|
|
355123
355289
|
let captureStopped = false;
|
|
355124
355290
|
let recordingStarted = false;
|
|
355125
355291
|
let recordingStopped = false;
|
|
355292
|
+
let acquiredSlotDeviceId = null;
|
|
355126
355293
|
let video;
|
|
355127
355294
|
let failingSource;
|
|
355128
355295
|
const stopCapture = async () => {
|
|
@@ -355225,7 +355392,8 @@ async function executeMobileTest(options) {
|
|
|
355225
355392
|
log2(`Warning: Could not retrieve installed apps list. Proceeding with test execution.`);
|
|
355226
355393
|
}
|
|
355227
355394
|
log2(`Starting Appium session for ${options.target.appId} on ${selectedDevice.name}...`);
|
|
355228
|
-
const leasedPortOffset =
|
|
355395
|
+
const leasedPortOffset = acquireControlPortSlot(selectedDevice.id);
|
|
355396
|
+
acquiredSlotDeviceId = selectedDevice.id;
|
|
355229
355397
|
try {
|
|
355230
355398
|
sessionId = await createSessionWithRetry({
|
|
355231
355399
|
appId: options.target.appId,
|
|
@@ -355502,6 +355670,7 @@ async function executeMobileTest(options) {
|
|
|
355502
355670
|
await deleteSession(sessionId);
|
|
355503
355671
|
log2("Session destroyed");
|
|
355504
355672
|
}
|
|
355673
|
+
if (acquiredSlotDeviceId) releaseControlPortSlot(acquiredSlotDeviceId);
|
|
355505
355674
|
}
|
|
355506
355675
|
}
|
|
355507
355676
|
async function createMobileDiscoverySession(target, options) {
|
|
@@ -355547,21 +355716,28 @@ async function createMobileDiscoverySession(target, options) {
|
|
|
355547
355716
|
isPhysical: selectedDevice.isPhysical === true,
|
|
355548
355717
|
platformVersion: selectedDevice.platformVersion
|
|
355549
355718
|
});
|
|
355550
|
-
const
|
|
355551
|
-
|
|
355552
|
-
|
|
355553
|
-
|
|
355554
|
-
|
|
355555
|
-
|
|
355556
|
-
|
|
355557
|
-
|
|
355558
|
-
|
|
355719
|
+
const discoveryPortSlotDeviceId = selectedDevice.id;
|
|
355720
|
+
let sessionId;
|
|
355721
|
+
try {
|
|
355722
|
+
sessionId = await createSessionWithRetry({
|
|
355723
|
+
appId: target.appId,
|
|
355724
|
+
platform: target.platform,
|
|
355725
|
+
deviceId: selectedDevice.id,
|
|
355726
|
+
platformVersion: target.platformVersion ?? selectedDevice.platformVersion,
|
|
355727
|
+
isPhysical: selectedDevice.isPhysical,
|
|
355728
|
+
portOffset: acquireControlPortSlot(discoveryPortSlotDeviceId)
|
|
355729
|
+
});
|
|
355730
|
+
} catch (err) {
|
|
355731
|
+
releaseControlPortSlot(discoveryPortSlotDeviceId);
|
|
355732
|
+
throw err;
|
|
355733
|
+
}
|
|
355559
355734
|
if (target.launchMode === "DEEP_LINK" && target.deeplink) {
|
|
355560
355735
|
try {
|
|
355561
355736
|
await openDeepLink(sessionId, target.platform, target.deeplink, target.appId);
|
|
355562
355737
|
} catch (err) {
|
|
355563
355738
|
await deleteSession(sessionId).catch(() => {
|
|
355564
355739
|
});
|
|
355740
|
+
releaseControlPortSlot(discoveryPortSlotDeviceId);
|
|
355565
355741
|
throw new Error(
|
|
355566
355742
|
`Deep-link launch failed for mobile discovery: ${err instanceof Error ? err.message : String(err)}`
|
|
355567
355743
|
);
|
|
@@ -355583,6 +355759,7 @@ async function createMobileDiscoverySession(target, options) {
|
|
|
355583
355759
|
} catch {
|
|
355584
355760
|
}
|
|
355585
355761
|
await deleteSession(sessionId);
|
|
355762
|
+
releaseControlPortSlot(discoveryPortSlotDeviceId);
|
|
355586
355763
|
}
|
|
355587
355764
|
};
|
|
355588
355765
|
}
|
|
@@ -355600,14 +355777,14 @@ async function readElementAttribute(sessionId, elementId, name) {
|
|
|
355600
355777
|
}
|
|
355601
355778
|
function harvestFallbackXpath(platform3, attrs) {
|
|
355602
355779
|
if (platform3 === "IOS") {
|
|
355603
|
-
if (attrs.name) return `//*[@name=${
|
|
355780
|
+
if (attrs.name) return `//*[@name=${xpathLiteral3(attrs.name)}]`;
|
|
355604
355781
|
const text = attrs.label ?? attrs.value;
|
|
355605
|
-
if (text) return
|
|
355782
|
+
if (text) return identityTextXpath2(text);
|
|
355606
355783
|
return "//*";
|
|
355607
355784
|
}
|
|
355608
|
-
if (attrs.resourceId) return `//*[@resource-id=${
|
|
355609
|
-
if (attrs.text) return
|
|
355610
|
-
if (attrs.contentDesc) return `//*[@content-desc=${
|
|
355785
|
+
if (attrs.resourceId) return `//*[@resource-id=${xpathLiteral3(attrs.resourceId)}]`;
|
|
355786
|
+
if (attrs.text) return identityTextXpath2(attrs.text);
|
|
355787
|
+
if (attrs.contentDesc) return `//*[@content-desc=${xpathLiteral3(attrs.contentDesc)}]`;
|
|
355611
355788
|
return "//*";
|
|
355612
355789
|
}
|
|
355613
355790
|
async function readNativeElementBundle(sessionId, elementId, platform3) {
|
|
@@ -355960,6 +356137,7 @@ var sessionStartInFlight = null;
|
|
|
355960
356137
|
var sessionReaperTimer = null;
|
|
355961
356138
|
var mirrorClientsEmptyAt = null;
|
|
355962
356139
|
var mirrorSessionOwner = null;
|
|
356140
|
+
var actionInFlight = false;
|
|
355963
356141
|
var recordingWorkloadActive = false;
|
|
355964
356142
|
function setRecordingWorkloadActive(active) {
|
|
355965
356143
|
if (recordingWorkloadActive === active) return;
|
|
@@ -356189,7 +356367,11 @@ async function handleHealth(_req, res) {
|
|
|
356189
356367
|
// 409 — it must never be inferred from server-side global run counts, which
|
|
356190
356368
|
// include unrelated projects and runners.
|
|
356191
356369
|
executorBusy: executorBusyCount > 0,
|
|
356192
|
-
|
|
356370
|
+
// TRUE also during the multi-second session-START window: the claim loop
|
|
356371
|
+
// reads this to keep test runs off the recording device, and the Appium
|
|
356372
|
+
// session id only lands AFTER `POST /session` returns — without the
|
|
356373
|
+
// in-flight latch a run admitted in that window fights the recording.
|
|
356374
|
+
recordingActive: !!state.appiumSessionId && mirrorSessionOwner === "recording" || sessionStartInFlight !== null
|
|
356193
356375
|
});
|
|
356194
356376
|
}
|
|
356195
356377
|
function realDeviceCapabilities2(platform3) {
|
|
@@ -356267,7 +356449,11 @@ async function handleDevices(_req, res) {
|
|
|
356267
356449
|
sendJSON(res, 200, {
|
|
356268
356450
|
devices,
|
|
356269
356451
|
executorBusy: executorBusyCount > 0,
|
|
356270
|
-
|
|
356452
|
+
// TRUE also during the multi-second session-START window: the claim loop
|
|
356453
|
+
// reads this to keep test runs off the recording device, and the Appium
|
|
356454
|
+
// session id only lands AFTER `POST /session` returns — without the
|
|
356455
|
+
// in-flight latch a run admitted in that window fights the recording.
|
|
356456
|
+
recordingActive: !!state.appiumSessionId && mirrorSessionOwner === "recording" || sessionStartInFlight !== null
|
|
356271
356457
|
});
|
|
356272
356458
|
}
|
|
356273
356459
|
async function handleSessionStart(req, res) {
|
|
@@ -356356,6 +356542,8 @@ async function startSession(validated, res) {
|
|
|
356356
356542
|
state.platform = targetPlatform === "IOS" ? "IOS" : "ANDROID";
|
|
356357
356543
|
mirrorSessionOwner = "recording";
|
|
356358
356544
|
setRecordingWorkloadActive(true);
|
|
356545
|
+
mirrorClientsEmptyAt = Date.now();
|
|
356546
|
+
ensureSessionReaper();
|
|
356359
356547
|
if (launchMode === "DEEP_LINK" && deeplink) {
|
|
356360
356548
|
try {
|
|
356361
356549
|
const args = targetPlatform === "IOS" ? [{ url: deeplink, bundleId: appId }] : [{ url: deeplink, package: appId }];
|
|
@@ -356709,6 +356897,14 @@ async function handleAction(req, res) {
|
|
|
356709
356897
|
sendJSON(res, 400, { error: "No active Appium session" });
|
|
356710
356898
|
return;
|
|
356711
356899
|
}
|
|
356900
|
+
if (mirrorSessionOwner !== "recording") {
|
|
356901
|
+
sendJSON(res, 409, { error: "No recording session owns the device (a runner-driven session is active)." });
|
|
356902
|
+
return;
|
|
356903
|
+
}
|
|
356904
|
+
if (actionInFlight) {
|
|
356905
|
+
sendJSON(res, 409, { error: "Another action is still executing. Wait for it to finish." });
|
|
356906
|
+
return;
|
|
356907
|
+
}
|
|
356712
356908
|
validateSessionId(state.appiumSessionId);
|
|
356713
356909
|
const body = await parseBody(req);
|
|
356714
356910
|
const type = typeof body.type === "string" ? body.type : null;
|
|
@@ -356720,6 +356916,7 @@ async function handleAction(req, res) {
|
|
|
356720
356916
|
sendJSON(res, 400, { error: "Unknown or unsupported action type" });
|
|
356721
356917
|
return;
|
|
356722
356918
|
}
|
|
356919
|
+
actionInFlight = true;
|
|
356723
356920
|
try {
|
|
356724
356921
|
const sessionPath = `/session/${state.appiumSessionId}`;
|
|
356725
356922
|
switch (type) {
|
|
@@ -356792,15 +356989,23 @@ async function handleAction(req, res) {
|
|
|
356792
356989
|
sendJSON(res, 400, { error: `Unknown action type: ${type}` });
|
|
356793
356990
|
return;
|
|
356794
356991
|
}
|
|
356795
|
-
|
|
356992
|
+
await waitForUiSettle(sessionPath);
|
|
356993
|
+
const screenshot = await getScreenshot2();
|
|
356994
|
+
let source = await getPageSource2();
|
|
356995
|
+
if (source === null) {
|
|
356996
|
+
source = await getPageSource2();
|
|
356997
|
+
}
|
|
356796
356998
|
sendJSON(res, 200, {
|
|
356797
356999
|
ok: true,
|
|
356798
357000
|
screenshot: screenshot ?? null,
|
|
356799
357001
|
source: source ?? null,
|
|
357002
|
+
...source === null || source === void 0 ? { sourceUnavailable: true } : {},
|
|
356800
357003
|
timestamp: Date.now()
|
|
356801
357004
|
});
|
|
356802
357005
|
} catch (err) {
|
|
356803
357006
|
sendJSON(res, 500, { error: `Action failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
357007
|
+
} finally {
|
|
357008
|
+
actionInFlight = false;
|
|
356804
357009
|
}
|
|
356805
357010
|
}
|
|
356806
357011
|
async function handleScreenshot(_req, res) {
|
|
@@ -357038,7 +357243,11 @@ function getBridgeState() {
|
|
|
357038
357243
|
tunnelUrl: state.tunnelUrl,
|
|
357039
357244
|
tunnelToken: state.tunnelToken,
|
|
357040
357245
|
hasActiveSession: !!state.appiumSessionId,
|
|
357041
|
-
|
|
357246
|
+
// TRUE also during the multi-second session-START window: the claim loop
|
|
357247
|
+
// reads this to keep test runs off the recording device, and the Appium
|
|
357248
|
+
// session id only lands AFTER `POST /session` returns — without the
|
|
357249
|
+
// in-flight latch a run admitted in that window fights the recording.
|
|
357250
|
+
recordingActive: !!state.appiumSessionId && mirrorSessionOwner === "recording" || sessionStartInFlight !== null,
|
|
357042
357251
|
platform: state.platform
|
|
357043
357252
|
};
|
|
357044
357253
|
}
|