@validate.qa/runner 1.0.65 → 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 +336 -87
- package/dist/cli.mjs +336 -87
- 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,
|
|
@@ -73629,6 +73686,7 @@ var require_inbox_tools = __commonJS({
|
|
|
73629
73686
|
exports.buildInboxPromptBlock = buildInboxPromptBlock;
|
|
73630
73687
|
exports.guardVerificationCodeFabrication = guardVerificationCodeFabrication;
|
|
73631
73688
|
exports.hasRegisterIntent = hasRegisterIntent;
|
|
73689
|
+
exports.hasSignupTestIntent = hasSignupTestIntent;
|
|
73632
73690
|
exports.resolveInboxRegistration = resolveInboxRegistration;
|
|
73633
73691
|
exports.applyInboxRegistrationIdentity = applyInboxRegistrationIdentity;
|
|
73634
73692
|
exports.isThrowawayRegisterSeedEmail = isThrowawayRegisterSeedEmail;
|
|
@@ -74172,6 +74230,9 @@ This project asked to REGISTER a new account, but this runner CANNOT receive ema
|
|
|
74172
74230
|
const v = variableContext.allTestVariables?.DISCOVERY_SHOULD_REGISTER ?? variableContext.publicTestVariables?.DISCOVERY_SHOULD_REGISTER ?? "";
|
|
74173
74231
|
return String(v).trim().toLowerCase() === "true";
|
|
74174
74232
|
}
|
|
74233
|
+
function hasSignupTestIntent(variableContext) {
|
|
74234
|
+
return variableContext?.allTestVariables?.DISCOVERY_ALLOW_SIGNUP_TESTS !== "false";
|
|
74235
|
+
}
|
|
74175
74236
|
function resolveInboxRegistration(opts) {
|
|
74176
74237
|
const config = (0, verification_inbox_js_1.loadVerificationInboxConfig)(opts.env ?? process.env);
|
|
74177
74238
|
if (!config)
|
|
@@ -80884,6 +80945,45 @@ ${constraints.join("\n")}`);
|
|
|
80884
80945
|
}
|
|
80885
80946
|
});
|
|
80886
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
|
+
|
|
80887
80987
|
// ../runner-core/dist/services/mobile/mobile-generator.js
|
|
80888
80988
|
var require_mobile_generator = __commonJS({
|
|
80889
80989
|
"../runner-core/dist/services/mobile/mobile-generator.js"(exports) {
|
|
@@ -80903,6 +81003,7 @@ var require_mobile_generator = __commonJS({
|
|
|
80903
81003
|
var ai_queue_js_1 = require_ai_queue();
|
|
80904
81004
|
var ai_retry_js_1 = require_ai_retry();
|
|
80905
81005
|
var mobile_session_keepalive_js_1 = require_mobile_session_keepalive();
|
|
81006
|
+
var discovery_lint_flags_js_1 = require_discovery_lint_flags();
|
|
80906
81007
|
var mobile_source_parser_js_1 = require_mobile_source_parser();
|
|
80907
81008
|
var mobile_source_parser_js_2 = require_mobile_source_parser();
|
|
80908
81009
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
@@ -81072,7 +81173,15 @@ var require_mobile_generator = __commonJS({
|
|
|
81072
81173
|
properties: {
|
|
81073
81174
|
name: { type: "string", description: 'Descriptive test name (e.g. "Login - Happy Path").' },
|
|
81074
81175
|
description: { type: "string", description: "One-line description of what the test proves." },
|
|
81075
|
-
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
|
+
}
|
|
81076
81185
|
},
|
|
81077
81186
|
required: ["name", "description", "verified"]
|
|
81078
81187
|
}
|
|
@@ -81318,6 +81427,8 @@ var require_mobile_generator = __commonJS({
|
|
|
81318
81427
|
- Assert the OUTCOME of the flow (the new screen / new content), not the control you just tapped.
|
|
81319
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.
|
|
81320
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.
|
|
81321
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.
|
|
81322
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.
|
|
81323
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.
|
|
@@ -81423,6 +81534,7 @@ Start by calling mobile_snapshot, then execute the steps above \u2014 the minimu
|
|
|
81423
81534
|
let assertCount = 0;
|
|
81424
81535
|
let finish = null;
|
|
81425
81536
|
let finishIntentRejections = 0;
|
|
81537
|
+
let finishStrengthRejections = 0;
|
|
81426
81538
|
let emptyTurnsAfterFinishReject = 0;
|
|
81427
81539
|
let latest = null;
|
|
81428
81540
|
const getLatest = () => latest;
|
|
@@ -81915,31 +82027,49 @@ ${statePacket}`, redactionCredentials) },
|
|
|
81915
82027
|
const claimedVerified = Boolean(toolArgs.verified);
|
|
81916
82028
|
const intentReason = scenarioIntentMismatchReason(scenario, actions);
|
|
81917
82029
|
const verifiedWithoutAssert = claimedVerified && assertCount === 0;
|
|
81918
|
-
const
|
|
81919
|
-
|
|
81920
|
-
|
|
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;
|
|
81921
82043
|
const reasons = [];
|
|
81922
82044
|
if (intentReason)
|
|
81923
82045
|
reasons.push(intentReason);
|
|
81924
82046
|
if (verifiedWithoutAssert) {
|
|
81925
82047
|
reasons.push("claimed verified:true but never called mobile_assert_visible / mobile_assert_text");
|
|
81926
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
|
+
}
|
|
81927
82052
|
const reasonText = reasons.join("; ");
|
|
81928
|
-
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}`);
|
|
81929
82054
|
pushResult({
|
|
81930
82055
|
ok: false,
|
|
81931
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.`,
|
|
81932
82057
|
intentMismatch: intentReason,
|
|
81933
82058
|
verifiedWithoutAssert,
|
|
81934
|
-
|
|
81935
|
-
|
|
82059
|
+
...strengthRejectReason !== null ? { strengthBlock: strengthRejectReason } : {},
|
|
82060
|
+
rejection: finishIntentRejections + finishStrengthRejections,
|
|
82061
|
+
maxRejections: MAX_FINISH_INTENT_REJECTIONS + 1
|
|
81936
82062
|
});
|
|
81937
82063
|
continue;
|
|
81938
82064
|
}
|
|
81939
82065
|
finish = {
|
|
81940
82066
|
name: redactCredentialValues(nonEmpty3(String(toolArgs.name ?? "")) ?? scenario.name, redactionCredentials),
|
|
81941
82067
|
description: redactCredentialValues(nonEmpty3(String(toolArgs.description ?? "")) ?? scenario.goal, redactionCredentials),
|
|
81942
|
-
verified: claimedVerified
|
|
82068
|
+
verified: claimedVerified,
|
|
82069
|
+
...toolArgs.suspectedBug === true ? {
|
|
82070
|
+
suspectedBug: true,
|
|
82071
|
+
bugDescription: redactCredentialValues(nonEmpty3(String(toolArgs.bugDescription ?? "")) ?? "", redactionCredentials)
|
|
82072
|
+
} : {}
|
|
81943
82073
|
};
|
|
81944
82074
|
log2(` finish_generate: "${finish.name}" (claimed verified: ${finish.verified}, asserts: ${assertCount})`);
|
|
81945
82075
|
pushResult({ acknowledged: true });
|
|
@@ -82003,6 +82133,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82003
82133
|
ok: false,
|
|
82004
82134
|
error: `Unknown element ref "${String(ref)}". Call mobile_snapshot first; refs are only valid for the latest snapshot.`
|
|
82005
82135
|
});
|
|
82136
|
+
const recordedTypeValues = () => recordedActions.filter((action) => action.type === "TYPE" && typeof action.value === "string" && action.value.length > 0).map((action) => action.value);
|
|
82006
82137
|
const actionPayload = (result, absorbed) => {
|
|
82007
82138
|
const blocked = Boolean(absorbed.externalBlocked || absorbed.transientExternal);
|
|
82008
82139
|
return {
|
|
@@ -82403,7 +82534,8 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82403
82534
|
latest,
|
|
82404
82535
|
screenName,
|
|
82405
82536
|
platform: platform3,
|
|
82406
|
-
recordAction
|
|
82537
|
+
recordAction,
|
|
82538
|
+
typedValues: recordedTypeValues
|
|
82407
82539
|
});
|
|
82408
82540
|
if (!recorded.ok)
|
|
82409
82541
|
return recorded;
|
|
@@ -82472,6 +82604,15 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82472
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.`
|
|
82473
82605
|
};
|
|
82474
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
|
+
}
|
|
82475
82616
|
const metadata = metadataForElement(element, screenName(), platform3);
|
|
82476
82617
|
const expectedNeedle = expected.trim().toLowerCase();
|
|
82477
82618
|
const identityValues = [element.accessibilityId, element.resourceId].map((value) => value?.trim().toLowerCase()).filter((value) => Boolean(value));
|
|
@@ -82527,6 +82668,17 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82527
82668
|
if (!accessibilityId && !text) {
|
|
82528
82669
|
return { ok: false, error: "mobile_assert_visible needs a ref (preferred), accessibilityId, or text." };
|
|
82529
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
|
+
}
|
|
82530
82682
|
const directTarget = accessibilityId ?? text;
|
|
82531
82683
|
if (directTarget && isStructuralAssertionIdentity(directTarget)) {
|
|
82532
82684
|
return {
|
|
@@ -83162,7 +83314,21 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83162
83314
|
if (claimedVerified && !verified) {
|
|
83163
83315
|
log2(" downgraded verified -> false: model claimed verified but called zero assert tools.");
|
|
83164
83316
|
}
|
|
83165
|
-
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
|
+
}
|
|
83166
83332
|
if (strengthVerdict.severity === "block" || strengthVerdict.severity === "route-bug") {
|
|
83167
83333
|
if (verified) {
|
|
83168
83334
|
log2(` downgraded verified -> false: assertion strength ${strengthVerdict.severity} \u2014 ${strengthVerdict.reason}`);
|
|
@@ -83178,7 +83344,11 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83178
83344
|
verified = false;
|
|
83179
83345
|
}
|
|
83180
83346
|
const platformTag2 = platform3 === "IOS" ? "@ios" : "@android";
|
|
83181
|
-
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";
|
|
83182
83352
|
const tags = ["@discovered", "@mobile", platformTag2, reviewTag];
|
|
83183
83353
|
if (strengthVerdict.severity === "downgrade")
|
|
83184
83354
|
tags.push("@smoke");
|
|
@@ -86258,38 +86428,6 @@ var require_run_callback_proxy = __commonJS({
|
|
|
86258
86428
|
}
|
|
86259
86429
|
});
|
|
86260
86430
|
|
|
86261
|
-
// ../runner-core/dist/services/discovery-lint-flags.js
|
|
86262
|
-
var require_discovery_lint_flags = __commonJS({
|
|
86263
|
-
"../runner-core/dist/services/discovery-lint-flags.js"(exports) {
|
|
86264
|
-
"use strict";
|
|
86265
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
86266
|
-
exports.structuralLocatorLintMode = structuralLocatorLintMode;
|
|
86267
|
-
exports.isPwMethodPreflightEnabled = isPwMethodPreflightEnabled;
|
|
86268
|
-
exports.isVqaLoginDedupeEnabled = isVqaLoginDedupeEnabled;
|
|
86269
|
-
exports.isTestgenEmailVerifyEnabled = isTestgenEmailVerifyEnabled;
|
|
86270
|
-
function strictOn(value) {
|
|
86271
|
-
return value === "true" || value === "1";
|
|
86272
|
-
}
|
|
86273
|
-
function structuralLocatorLintMode() {
|
|
86274
|
-
const v = (process.env.DISCOVERY_STRUCTURAL_LOCATOR_LINT || "").trim().toLowerCase();
|
|
86275
|
-
if (v === "warn")
|
|
86276
|
-
return "warn";
|
|
86277
|
-
if (v === "true" || v === "1" || v === "enforce")
|
|
86278
|
-
return "enforce";
|
|
86279
|
-
return "off";
|
|
86280
|
-
}
|
|
86281
|
-
function isPwMethodPreflightEnabled() {
|
|
86282
|
-
return strictOn(process.env.DISCOVERY_PW_METHOD_PREFLIGHT);
|
|
86283
|
-
}
|
|
86284
|
-
function isVqaLoginDedupeEnabled() {
|
|
86285
|
-
return strictOn(process.env.DISCOVERY_VQALOGIN_DEDUPE);
|
|
86286
|
-
}
|
|
86287
|
-
function isTestgenEmailVerifyEnabled() {
|
|
86288
|
-
return strictOn(process.env.DISCOVERY_TESTGEN_EMAIL_VERIFY);
|
|
86289
|
-
}
|
|
86290
|
-
}
|
|
86291
|
-
});
|
|
86292
|
-
|
|
86293
86431
|
// ../runner-core/dist/services/auth-recipe.js
|
|
86294
86432
|
var require_auth_recipe = __commonJS({
|
|
86295
86433
|
"../runner-core/dist/services/auth-recipe.js"(exports) {
|
|
@@ -95120,6 +95258,8 @@ var require_discover_generator = __commonJS({
|
|
|
95120
95258
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
95121
95259
|
exports.MIN_BUG_DESCRIPTION_LENGTH = exports.VALID_PLAYWRIGHT_GETBY_METHODS = exports.BLOCKED_MCP_TOOLS_SCENARIO_GEN = void 0;
|
|
95122
95260
|
exports.scenarioLooksLikeRegistration = scenarioLooksLikeRegistration;
|
|
95261
|
+
exports.signupFeatureTestingAllowed = signupFeatureTestingAllowed;
|
|
95262
|
+
exports.scenarioTouchesSignupAuthSurface = scenarioTouchesSignupAuthSurface;
|
|
95123
95263
|
exports.filterMcpToolsForScenarioGen = filterMcpToolsForScenarioGen;
|
|
95124
95264
|
exports.detectScenarioSignals = detectScenarioSignals;
|
|
95125
95265
|
exports.scenarioStepsCoverAuth = scenarioStepsCoverAuth;
|
|
@@ -95207,6 +95347,19 @@ EMIT (the code you put in finish_generate) \u2014 do NOT hardcode the throwaway
|
|
|
95207
95347
|
- Tag the test \`@email-verify\`. Emit EXACTLY ONE test() in this file (one registration per run).
|
|
95208
95348
|
- End with a DURABLE assertion of the post-verification signed-in state (e.g. a Logout control, an account/dashboard landmark) in your OWN test body \u2014 the helper call is not itself a proof.`;
|
|
95209
95349
|
}
|
|
95350
|
+
function signupFeatureTestingAllowed(credentials) {
|
|
95351
|
+
if (!(0, discovery_lint_flags_js_1.isTestgenEmailVerifyEnabled)() || !(0, verification_inbox_js_1.loadVerificationInboxConfig)())
|
|
95352
|
+
return false;
|
|
95353
|
+
return credentials?.DISCOVERY_ALLOW_SIGNUP_TESTS !== "false";
|
|
95354
|
+
}
|
|
95355
|
+
var SIGNUP_AUTH_ROUTE_RE = /\/(sign-?up|verify-?email|email-?verification|create-?account|confirm-?email)\b/i;
|
|
95356
|
+
function scenarioTouchesSignupAuthSurface(scenario, pages) {
|
|
95357
|
+
const routes = [scenario.entryRoute, ...scenario.targetPages ?? []].filter((r) => !!r);
|
|
95358
|
+
if (routes.some((r) => SIGNUP_AUTH_ROUTE_RE.test(r)))
|
|
95359
|
+
return true;
|
|
95360
|
+
const authRoutes = new Set(pages.filter((p) => (p.pageType ?? "").toUpperCase() === "AUTH").map((p) => p.route));
|
|
95361
|
+
return routes.some((r) => authRoutes.has(r));
|
|
95362
|
+
}
|
|
95210
95363
|
exports.BLOCKED_MCP_TOOLS_SCENARIO_GEN = /* @__PURE__ */ new Set([
|
|
95211
95364
|
"browser_install",
|
|
95212
95365
|
"browser_close",
|
|
@@ -98318,9 +98471,16 @@ ${tailBlock}`;
|
|
|
98318
98471
|
extraAuthEvidence: !!opts?.recipe || hasConfiguredLoginCredentials(credentials)
|
|
98319
98472
|
});
|
|
98320
98473
|
const baseSkeletonHadRegression = tags.includes("@regression");
|
|
98474
|
+
const selfProvisioningSignupScenario = signupFeatureTestingAllowed(credentials) && scenarioLooksLikeRegistration(scenario) && scenarioTouchesSignupAuthSurface(scenario, siteMap.pages);
|
|
98475
|
+
if (selfProvisioningSignupScenario) {
|
|
98476
|
+
for (const prereqTag of ["@needs-auth", "@inline-login", "@auth-guarded"]) {
|
|
98477
|
+
const idx = tags.indexOf(prereqTag);
|
|
98478
|
+
if (idx !== -1)
|
|
98479
|
+
tags.splice(idx, 1);
|
|
98480
|
+
}
|
|
98481
|
+
}
|
|
98321
98482
|
const hasVerifiedRecipe = !!opts?.recipe && tags.includes("@needs-auth");
|
|
98322
|
-
const
|
|
98323
|
-
const inboxGenConfig = (0, discovery_lint_flags_js_1.isTestgenEmailVerifyEnabled)() && projectHasRegisterIntent && !hasVerifiedRecipe && scenarioLooksLikeRegistration(scenario) ? (0, verification_inbox_js_1.loadVerificationInboxConfig)() : null;
|
|
98483
|
+
const inboxGenConfig = selfProvisioningSignupScenario ? (0, verification_inbox_js_1.loadVerificationInboxConfig)() : null;
|
|
98324
98484
|
let genInboxState = null;
|
|
98325
98485
|
let genInboxCredentials = credentials;
|
|
98326
98486
|
let genInboxSecret = null;
|
|
@@ -99267,7 +99427,8 @@ Scenario: "${scenario.name}" (${scenario.type})`);
|
|
|
99267
99427
|
} catch {
|
|
99268
99428
|
}
|
|
99269
99429
|
const recipe = authRecipe;
|
|
99270
|
-
const
|
|
99430
|
+
const selfProvisioningSignupScenario = signupFeatureTestingAllowed(credentials) && scenarioLooksLikeRegistration(scenario) && scenarioTouchesSignupAuthSurface(scenario, siteMap.pages);
|
|
99431
|
+
const replayLogin = !selfProvisioningSignupScenario && shouldReplayAuthRecipe(scenario.targetPages, siteMap.pages, recipe);
|
|
99271
99432
|
let effectiveRecipe = null;
|
|
99272
99433
|
if (replayLogin && recipe) {
|
|
99273
99434
|
const planLeaks = findSensitiveLiteralLeaks(JSON.stringify(recipe.replayPlan ?? []), credentials);
|
|
@@ -99374,7 +99535,8 @@ Scenario: "${scenario.name}" (${scenario.type})`);
|
|
|
99374
99535
|
return;
|
|
99375
99536
|
}
|
|
99376
99537
|
let effectiveRecipe = null;
|
|
99377
|
-
|
|
99538
|
+
const selfProvisioningSignupScenario = signupFeatureTestingAllowed(credentials) && scenarioLooksLikeRegistration(scenario) && scenarioTouchesSignupAuthSurface(scenario, siteMap.pages);
|
|
99539
|
+
if (!selfProvisioningSignupScenario && shouldReplayAuthRecipe(scenario.targetPages, siteMap.pages, authRecipe) && authRecipe) {
|
|
99378
99540
|
const planLeaks = findSensitiveLiteralLeaks(JSON.stringify(authRecipe.replayPlan ?? []), credentials);
|
|
99379
99541
|
if (planLeaks.length > 0) {
|
|
99380
99542
|
scenarioLogs.push(` Warning: login replay plan contains literal credential value(s) for ${planLeaks.join(", ")} \u2014 falling back to model-authored login for "${scenario.name}"`);
|
|
@@ -335175,6 +335337,7 @@ var require_discover_agent = __commonJS({
|
|
|
335175
335337
|
var discover_ai_planner_js_1 = require_discover_ai_planner();
|
|
335176
335338
|
var discovery_lint_flags_js_1 = require_discovery_lint_flags();
|
|
335177
335339
|
var verification_inbox_js_1 = require_verification_inbox();
|
|
335340
|
+
var inbox_tools_js_1 = require_inbox_tools();
|
|
335178
335341
|
var sitemap_builder_js_1 = require_sitemap_builder();
|
|
335179
335342
|
Object.defineProperty(exports, "buildInMemorySiteMap", { enumerable: true, get: function() {
|
|
335180
335343
|
return sitemap_builder_js_1.buildInMemorySiteMap;
|
|
@@ -335188,11 +335351,16 @@ var require_discover_agent = __commonJS({
|
|
|
335188
335351
|
var canvas_detector_js_1 = require_canvas_detector();
|
|
335189
335352
|
var auth_recipe_js_1 = require_auth_recipe();
|
|
335190
335353
|
function plannerEmailVerifyArmed(ctx) {
|
|
335191
|
-
if (!(0, discovery_lint_flags_js_1.isTestgenEmailVerifyEnabled)())
|
|
335192
|
-
return false;
|
|
335193
|
-
if (ctx.variableContext?.allTestVariables?.DISCOVERY_SHOULD_REGISTER !== "true")
|
|
335354
|
+
if (!(0, discovery_lint_flags_js_1.isTestgenEmailVerifyEnabled)() || !(0, verification_inbox_js_1.loadVerificationInboxConfig)())
|
|
335194
335355
|
return false;
|
|
335195
|
-
return (0,
|
|
335356
|
+
return (0, inbox_tools_js_1.hasSignupTestIntent)(ctx.variableContext);
|
|
335357
|
+
}
|
|
335358
|
+
var PLANNER_AUTH_NAME_RE = /\b(auth|authentication|log[\s-]?in|sign[\s-]?in|sign[\s-]?up|signup|account|password|verify[\s-]?email)\b/i;
|
|
335359
|
+
var PLANNER_AUTH_ROUTE_RE = /\/(auth|login|sign-?in|sign-?up|signup|verify-?email|create-?account)\b/i;
|
|
335360
|
+
function featureLooksLikeAuth(featureName, focusRoutes) {
|
|
335361
|
+
if (featureName && PLANNER_AUTH_NAME_RE.test(featureName))
|
|
335362
|
+
return true;
|
|
335363
|
+
return (focusRoutes ?? []).some((r) => PLANNER_AUTH_ROUTE_RE.test(r));
|
|
335196
335364
|
}
|
|
335197
335365
|
async function runCanvasPreflight(ctx, sharedBrowser, log2) {
|
|
335198
335366
|
const cached = ctx.appRenderType ?? null;
|
|
@@ -335844,6 +336012,11 @@ Explore complete: ${exploreResult.pagesDiscovered} pages, ${exploreResult.elemen
|
|
|
335844
336012
|
authRedirectRoute: (0, shared_1.parseBoolFlag)(process.env.AUTH_RECIPE_STAMP) ? ctx.authRecipe?.redirectRoute ?? void 0 : void 0,
|
|
335845
336013
|
// Email-verify capability — only for scenario-producing modes; a survey emits
|
|
335846
336014
|
// featureGroups (no scenarios), so keep its prompt free of the round-trip block.
|
|
336015
|
+
// This site serves survey + FULL mode. Full mode is app-wide (no single feature
|
|
336016
|
+
// identity here), so we deliberately do NOT apply the auth-feature scope: the
|
|
336017
|
+
// per-scenario generator gates (scenarioLooksLikeRegistration +
|
|
336018
|
+
// scenarioTouchesSignupAuthSurface) still isolate non-auth scenarios. The
|
|
336019
|
+
// auth-feature scope IS applied at the DEEP call site where ctx.featureName is known.
|
|
335847
336020
|
emailVerifyArmed: mode !== "survey" && plannerEmailVerifyArmed(ctx),
|
|
335848
336021
|
testBudget
|
|
335849
336022
|
}, {
|
|
@@ -336432,9 +336605,10 @@ Plan (reused): ${resumeSavedScenarios.length} scenarios from the paused run.`);
|
|
|
336432
336605
|
// Email-verify capability: when armed, let the planner plan the full
|
|
336433
336606
|
// register→verify and forgot-password→reset round trips to completion
|
|
336434
336607
|
// (see the planner's "## Verification Inbox Available" block). Armed on the
|
|
336435
|
-
// SAME project-level predicate the generator uses (flag +
|
|
336608
|
+
// SAME project-level predicate the generator uses (flag + consent +
|
|
336436
336609
|
// configured inbox), so the planner never promises what generation can't do.
|
|
336437
|
-
|
|
336610
|
+
// Scoped to AUTH features: a non-auth feature has no signup surface to test.
|
|
336611
|
+
emailVerifyArmed: plannerEmailVerifyArmed(ctx) && featureLooksLikeAuth(ctx.featureName, augmentedFeatureFocusAreas),
|
|
336438
336612
|
testBudget: featureTestBudget
|
|
336439
336613
|
}, {
|
|
336440
336614
|
modelOverride: ctx.planModel ?? ctx.generateModel,
|
|
@@ -347752,12 +347926,7 @@ Write valid JSON only (no markdown fences inside files). Be exhaustive.`;
|
|
|
347752
347926
|
const o = value;
|
|
347753
347927
|
if (!isNonEmptyString(o.name))
|
|
347754
347928
|
return { ok: false, reason: "name: missing" };
|
|
347755
|
-
|
|
347756
|
-
return { ok: false, reason: "appiumCode: missing" };
|
|
347757
|
-
const code = o.appiumCode;
|
|
347758
|
-
if (!/webdriverio|remote\s*\(|driver\s*\./i.test(code)) {
|
|
347759
|
-
return { ok: false, reason: "appiumCode: not a WebdriverIO/Appium script" };
|
|
347760
|
-
}
|
|
347929
|
+
const code = isNonEmptyString(o.appiumCode) ? o.appiumCode : "";
|
|
347761
347930
|
if (!Array.isArray(o.steps) || o.steps.length === 0) {
|
|
347762
347931
|
return { ok: false, reason: "steps: missing or empty" };
|
|
347763
347932
|
}
|
|
@@ -351060,7 +351229,7 @@ var require_package4 = __commonJS({
|
|
|
351060
351229
|
"package.json"(exports, module) {
|
|
351061
351230
|
module.exports = {
|
|
351062
351231
|
name: "@validate.qa/runner",
|
|
351063
|
-
version: "1.0.
|
|
351232
|
+
version: "1.0.67",
|
|
351064
351233
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
351065
351234
|
bin: {
|
|
351066
351235
|
"validate-runner": "dist/cli.js"
|
|
@@ -351456,6 +351625,9 @@ function sanitizeAppiumLogLine(line) {
|
|
|
351456
351625
|
}
|
|
351457
351626
|
function setAppiumWorkloadActive(active) {
|
|
351458
351627
|
activeWorkloadCount = active ? activeWorkloadCount + 1 : Math.max(0, activeWorkloadCount - 1);
|
|
351628
|
+
if (activeWorkloadCount === 0) {
|
|
351629
|
+
consecutiveHealthFailures = 0;
|
|
351630
|
+
}
|
|
351459
351631
|
}
|
|
351460
351632
|
function shouldRestartAfterHealthFailure(workloadCount, failureCount) {
|
|
351461
351633
|
return workloadCount <= 0 && failureCount >= HEALTH_FAILURES_BEFORE_RESTART;
|
|
@@ -351951,17 +352123,31 @@ function rectMatchesIntendedBounds(rect, bounds) {
|
|
|
351951
352123
|
return rectCenterInBounds || boundsCenterInRect;
|
|
351952
352124
|
}
|
|
351953
352125
|
async function resolveTypeTargetElementId(sessionPath, bounds) {
|
|
351954
|
-
const
|
|
352126
|
+
const rawBox = bounds !== void 0 ? normaliseBounds(bounds) : null;
|
|
352127
|
+
const box = rawBox && rawBox.width > 0 && rawBox.height > 0 ? rawBox : null;
|
|
351955
352128
|
let elementId = await getActiveElementId(sessionPath);
|
|
351956
|
-
|
|
352129
|
+
let rejectedElementId = null;
|
|
352130
|
+
let rejectedForMismatch = false;
|
|
352131
|
+
if (elementId && box) {
|
|
351957
352132
|
const rect = await getElementRect(sessionPath, elementId);
|
|
351958
352133
|
if (!rect || !rectMatchesIntendedBounds(rect, box)) {
|
|
352134
|
+
rejectedElementId = elementId;
|
|
352135
|
+
rejectedForMismatch = !!rect;
|
|
351959
352136
|
elementId = null;
|
|
351960
352137
|
}
|
|
351961
352138
|
}
|
|
351962
352139
|
if (!elementId && bounds !== void 0 && await tapBoundsCenter(sessionPath, bounds)) {
|
|
351963
352140
|
await new Promise((resolve2) => setTimeout(resolve2, MOBILE_FOCUS_SETTLE_MS));
|
|
351964
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
|
+
}
|
|
351965
352151
|
}
|
|
351966
352152
|
return elementId;
|
|
351967
352153
|
}
|
|
@@ -354540,6 +354726,13 @@ function buildLocatorStrategies(target, platform3) {
|
|
|
354540
354726
|
if (bundle) {
|
|
354541
354727
|
if (bundle.accessibilityId) strategies.push({ using: "accessibility id", value: bundle.accessibilityId });
|
|
354542
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
|
+
}
|
|
354543
354736
|
if (bundle.xpath) strategies.push({ using: "xpath", value: bundle.xpath });
|
|
354544
354737
|
}
|
|
354545
354738
|
if (!(platform3 && hasPlatformLocator(target, platform3))) {
|
|
@@ -354557,14 +354750,14 @@ function dedupeStrategies(strategies) {
|
|
|
354557
354750
|
(strategy, index, list) => list.findIndex((candidate) => candidate.using === strategy.using && candidate.value === strategy.value) === index
|
|
354558
354751
|
);
|
|
354559
354752
|
}
|
|
354560
|
-
function
|
|
354753
|
+
function xpathLiteral3(value) {
|
|
354561
354754
|
if (!value.includes("'")) return `'${value}'`;
|
|
354562
354755
|
if (!value.includes('"')) return `"${value}"`;
|
|
354563
354756
|
const parts = value.split("'").map((p) => `'${p}'`);
|
|
354564
354757
|
return `concat(${parts.join(`, "'", `)})`;
|
|
354565
354758
|
}
|
|
354566
|
-
function
|
|
354567
|
-
const lit =
|
|
354759
|
+
function identityTextXpath2(value) {
|
|
354760
|
+
const lit = xpathLiteral3(value);
|
|
354568
354761
|
return `//*[@text=${lit} or @content-desc=${lit} or @label=${lit} or @name=${lit} or @value=${lit}]`;
|
|
354569
354762
|
}
|
|
354570
354763
|
function isIdentityXpath(value) {
|
|
@@ -354584,7 +354777,7 @@ function buildIdentityLocatorStrategies(target, platform3) {
|
|
|
354584
354777
|
if (bundle.resourceId) strategies.push({ using: "id", value: bundle.resourceId });
|
|
354585
354778
|
for (const value of [bundle.text, bundle.contentDesc]) {
|
|
354586
354779
|
const trimmed = value?.trim();
|
|
354587
|
-
if (trimmed) strategies.push({ using: "xpath", value:
|
|
354780
|
+
if (trimmed) strategies.push({ using: "xpath", value: identityTextXpath2(trimmed) });
|
|
354588
354781
|
}
|
|
354589
354782
|
if (bundle.xpath && isIdentityXpath(bundle.xpath)) strategies.push({ using: "xpath", value: bundle.xpath });
|
|
354590
354783
|
}
|
|
@@ -355064,6 +355257,19 @@ async function readElementTextCandidates(sessionId, elementId, actualText) {
|
|
|
355064
355257
|
}
|
|
355065
355258
|
return values;
|
|
355066
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
|
+
}
|
|
355067
355273
|
async function executeMobileTest(options) {
|
|
355068
355274
|
const logLines = [];
|
|
355069
355275
|
const log2 = (msg) => logLines.push(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}`);
|
|
@@ -355083,6 +355289,7 @@ async function executeMobileTest(options) {
|
|
|
355083
355289
|
let captureStopped = false;
|
|
355084
355290
|
let recordingStarted = false;
|
|
355085
355291
|
let recordingStopped = false;
|
|
355292
|
+
let acquiredSlotDeviceId = null;
|
|
355086
355293
|
let video;
|
|
355087
355294
|
let failingSource;
|
|
355088
355295
|
const stopCapture = async () => {
|
|
@@ -355185,7 +355392,8 @@ async function executeMobileTest(options) {
|
|
|
355185
355392
|
log2(`Warning: Could not retrieve installed apps list. Proceeding with test execution.`);
|
|
355186
355393
|
}
|
|
355187
355394
|
log2(`Starting Appium session for ${options.target.appId} on ${selectedDevice.name}...`);
|
|
355188
|
-
const leasedPortOffset =
|
|
355395
|
+
const leasedPortOffset = acquireControlPortSlot(selectedDevice.id);
|
|
355396
|
+
acquiredSlotDeviceId = selectedDevice.id;
|
|
355189
355397
|
try {
|
|
355190
355398
|
sessionId = await createSessionWithRetry({
|
|
355191
355399
|
appId: options.target.appId,
|
|
@@ -355462,6 +355670,7 @@ async function executeMobileTest(options) {
|
|
|
355462
355670
|
await deleteSession(sessionId);
|
|
355463
355671
|
log2("Session destroyed");
|
|
355464
355672
|
}
|
|
355673
|
+
if (acquiredSlotDeviceId) releaseControlPortSlot(acquiredSlotDeviceId);
|
|
355465
355674
|
}
|
|
355466
355675
|
}
|
|
355467
355676
|
async function createMobileDiscoverySession(target, options) {
|
|
@@ -355507,21 +355716,28 @@ async function createMobileDiscoverySession(target, options) {
|
|
|
355507
355716
|
isPhysical: selectedDevice.isPhysical === true,
|
|
355508
355717
|
platformVersion: selectedDevice.platformVersion
|
|
355509
355718
|
});
|
|
355510
|
-
const
|
|
355511
|
-
|
|
355512
|
-
|
|
355513
|
-
|
|
355514
|
-
|
|
355515
|
-
|
|
355516
|
-
|
|
355517
|
-
|
|
355518
|
-
|
|
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
|
+
}
|
|
355519
355734
|
if (target.launchMode === "DEEP_LINK" && target.deeplink) {
|
|
355520
355735
|
try {
|
|
355521
355736
|
await openDeepLink(sessionId, target.platform, target.deeplink, target.appId);
|
|
355522
355737
|
} catch (err) {
|
|
355523
355738
|
await deleteSession(sessionId).catch(() => {
|
|
355524
355739
|
});
|
|
355740
|
+
releaseControlPortSlot(discoveryPortSlotDeviceId);
|
|
355525
355741
|
throw new Error(
|
|
355526
355742
|
`Deep-link launch failed for mobile discovery: ${err instanceof Error ? err.message : String(err)}`
|
|
355527
355743
|
);
|
|
@@ -355543,6 +355759,7 @@ async function createMobileDiscoverySession(target, options) {
|
|
|
355543
355759
|
} catch {
|
|
355544
355760
|
}
|
|
355545
355761
|
await deleteSession(sessionId);
|
|
355762
|
+
releaseControlPortSlot(discoveryPortSlotDeviceId);
|
|
355546
355763
|
}
|
|
355547
355764
|
};
|
|
355548
355765
|
}
|
|
@@ -355560,14 +355777,14 @@ async function readElementAttribute(sessionId, elementId, name) {
|
|
|
355560
355777
|
}
|
|
355561
355778
|
function harvestFallbackXpath(platform3, attrs) {
|
|
355562
355779
|
if (platform3 === "IOS") {
|
|
355563
|
-
if (attrs.name) return `//*[@name=${
|
|
355780
|
+
if (attrs.name) return `//*[@name=${xpathLiteral3(attrs.name)}]`;
|
|
355564
355781
|
const text = attrs.label ?? attrs.value;
|
|
355565
|
-
if (text) return
|
|
355782
|
+
if (text) return identityTextXpath2(text);
|
|
355566
355783
|
return "//*";
|
|
355567
355784
|
}
|
|
355568
|
-
if (attrs.resourceId) return `//*[@resource-id=${
|
|
355569
|
-
if (attrs.text) return
|
|
355570
|
-
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)}]`;
|
|
355571
355788
|
return "//*";
|
|
355572
355789
|
}
|
|
355573
355790
|
async function readNativeElementBundle(sessionId, elementId, platform3) {
|
|
@@ -355920,6 +356137,7 @@ var sessionStartInFlight = null;
|
|
|
355920
356137
|
var sessionReaperTimer = null;
|
|
355921
356138
|
var mirrorClientsEmptyAt = null;
|
|
355922
356139
|
var mirrorSessionOwner = null;
|
|
356140
|
+
var actionInFlight = false;
|
|
355923
356141
|
var recordingWorkloadActive = false;
|
|
355924
356142
|
function setRecordingWorkloadActive(active) {
|
|
355925
356143
|
if (recordingWorkloadActive === active) return;
|
|
@@ -356149,7 +356367,11 @@ async function handleHealth(_req, res) {
|
|
|
356149
356367
|
// 409 — it must never be inferred from server-side global run counts, which
|
|
356150
356368
|
// include unrelated projects and runners.
|
|
356151
356369
|
executorBusy: executorBusyCount > 0,
|
|
356152
|
-
|
|
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
|
|
356153
356375
|
});
|
|
356154
356376
|
}
|
|
356155
356377
|
function realDeviceCapabilities2(platform3) {
|
|
@@ -356227,7 +356449,11 @@ async function handleDevices(_req, res) {
|
|
|
356227
356449
|
sendJSON(res, 200, {
|
|
356228
356450
|
devices,
|
|
356229
356451
|
executorBusy: executorBusyCount > 0,
|
|
356230
|
-
|
|
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
|
|
356231
356457
|
});
|
|
356232
356458
|
}
|
|
356233
356459
|
async function handleSessionStart(req, res) {
|
|
@@ -356316,6 +356542,8 @@ async function startSession(validated, res) {
|
|
|
356316
356542
|
state.platform = targetPlatform === "IOS" ? "IOS" : "ANDROID";
|
|
356317
356543
|
mirrorSessionOwner = "recording";
|
|
356318
356544
|
setRecordingWorkloadActive(true);
|
|
356545
|
+
mirrorClientsEmptyAt = Date.now();
|
|
356546
|
+
ensureSessionReaper();
|
|
356319
356547
|
if (launchMode === "DEEP_LINK" && deeplink) {
|
|
356320
356548
|
try {
|
|
356321
356549
|
const args = targetPlatform === "IOS" ? [{ url: deeplink, bundleId: appId }] : [{ url: deeplink, package: appId }];
|
|
@@ -356669,6 +356897,14 @@ async function handleAction(req, res) {
|
|
|
356669
356897
|
sendJSON(res, 400, { error: "No active Appium session" });
|
|
356670
356898
|
return;
|
|
356671
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
|
+
}
|
|
356672
356908
|
validateSessionId(state.appiumSessionId);
|
|
356673
356909
|
const body = await parseBody(req);
|
|
356674
356910
|
const type = typeof body.type === "string" ? body.type : null;
|
|
@@ -356680,6 +356916,7 @@ async function handleAction(req, res) {
|
|
|
356680
356916
|
sendJSON(res, 400, { error: "Unknown or unsupported action type" });
|
|
356681
356917
|
return;
|
|
356682
356918
|
}
|
|
356919
|
+
actionInFlight = true;
|
|
356683
356920
|
try {
|
|
356684
356921
|
const sessionPath = `/session/${state.appiumSessionId}`;
|
|
356685
356922
|
switch (type) {
|
|
@@ -356752,15 +356989,23 @@ async function handleAction(req, res) {
|
|
|
356752
356989
|
sendJSON(res, 400, { error: `Unknown action type: ${type}` });
|
|
356753
356990
|
return;
|
|
356754
356991
|
}
|
|
356755
|
-
|
|
356992
|
+
await waitForUiSettle(sessionPath);
|
|
356993
|
+
const screenshot = await getScreenshot2();
|
|
356994
|
+
let source = await getPageSource2();
|
|
356995
|
+
if (source === null) {
|
|
356996
|
+
source = await getPageSource2();
|
|
356997
|
+
}
|
|
356756
356998
|
sendJSON(res, 200, {
|
|
356757
356999
|
ok: true,
|
|
356758
357000
|
screenshot: screenshot ?? null,
|
|
356759
357001
|
source: source ?? null,
|
|
357002
|
+
...source === null || source === void 0 ? { sourceUnavailable: true } : {},
|
|
356760
357003
|
timestamp: Date.now()
|
|
356761
357004
|
});
|
|
356762
357005
|
} catch (err) {
|
|
356763
357006
|
sendJSON(res, 500, { error: `Action failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
357007
|
+
} finally {
|
|
357008
|
+
actionInFlight = false;
|
|
356764
357009
|
}
|
|
356765
357010
|
}
|
|
356766
357011
|
async function handleScreenshot(_req, res) {
|
|
@@ -356998,7 +357243,11 @@ function getBridgeState() {
|
|
|
356998
357243
|
tunnelUrl: state.tunnelUrl,
|
|
356999
357244
|
tunnelToken: state.tunnelToken,
|
|
357000
357245
|
hasActiveSession: !!state.appiumSessionId,
|
|
357001
|
-
|
|
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,
|
|
357002
357251
|
platform: state.platform
|
|
357003
357252
|
};
|
|
357004
357253
|
}
|