@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.js
CHANGED
|
@@ -2236,6 +2236,18 @@ function selectorForValue(value) {
|
|
|
2236
2236
|
function isIdentityMobileXpath(value) {
|
|
2237
2237
|
return /@[A-Za-z_:][-A-Za-z0-9_:.]*\s*=/.test(value) && !/(?:contains|starts-with)\s*\(/i.test(value);
|
|
2238
2238
|
}
|
|
2239
|
+
function xpathLiteral(value) {
|
|
2240
|
+
if (!value.includes("'"))
|
|
2241
|
+
return `'${value}'`;
|
|
2242
|
+
if (!value.includes('"'))
|
|
2243
|
+
return `"${value}"`;
|
|
2244
|
+
const parts = value.split("'").map((p) => `'${p}'`);
|
|
2245
|
+
return `concat(${parts.join(`, "'", `)})`;
|
|
2246
|
+
}
|
|
2247
|
+
function identityTextXpath(value) {
|
|
2248
|
+
const lit = xpathLiteral(value);
|
|
2249
|
+
return `//*[@text=${lit} or @content-desc=${lit} or @label=${lit} or @name=${lit} or @value=${lit}]`;
|
|
2250
|
+
}
|
|
2239
2251
|
function mobileLocatorBundleHasIdentity(bundle) {
|
|
2240
2252
|
if (!bundle)
|
|
2241
2253
|
return false;
|
|
@@ -2262,6 +2274,13 @@ function selectorCandidatesForStep(step) {
|
|
|
2262
2274
|
candidates.push(`~${bundle.accessibilityId}`);
|
|
2263
2275
|
if (nonEmpty(bundle.resourceId))
|
|
2264
2276
|
candidates.push(`id=${bundle.resourceId}`);
|
|
2277
|
+
const isGateStep = step.action === "waitForElement" || step.action === "assertVisible" || step.action === "assertText";
|
|
2278
|
+
const textIdentity = isGateStep || !isEditableMobileClassName(bundle.className) ? bundle.text : void 0;
|
|
2279
|
+
for (const value of [textIdentity, bundle.contentDesc]) {
|
|
2280
|
+
const trimmed = nonEmpty(value);
|
|
2281
|
+
if (trimmed)
|
|
2282
|
+
candidates.push(identityTextXpath(trimmed));
|
|
2283
|
+
}
|
|
2265
2284
|
if (nonEmpty(bundle.xpath))
|
|
2266
2285
|
candidates.push(bundle.xpath);
|
|
2267
2286
|
}
|
|
@@ -2710,7 +2729,7 @@ function uniq(values) {
|
|
|
2710
2729
|
}
|
|
2711
2730
|
return result;
|
|
2712
2731
|
}
|
|
2713
|
-
function
|
|
2732
|
+
function xpathLiteral2(value) {
|
|
2714
2733
|
if (!value.includes("'"))
|
|
2715
2734
|
return `'${value}'`;
|
|
2716
2735
|
if (!value.includes('"'))
|
|
@@ -2718,7 +2737,7 @@ function xpathLiteral(value) {
|
|
|
2718
2737
|
return `concat(${value.split("'").map((part) => `'${part}'`).join(`, "'", `)})`;
|
|
2719
2738
|
}
|
|
2720
2739
|
function mobileTextXPath(value, platform3) {
|
|
2721
|
-
const literal =
|
|
2740
|
+
const literal = xpathLiteral2(value);
|
|
2722
2741
|
if (platform3 === "IOS") {
|
|
2723
2742
|
return `//*[@name=${literal} or @label=${literal} or @value=${literal}]`;
|
|
2724
2743
|
}
|
|
@@ -3866,6 +3885,43 @@ function accumulateStatements(lines) {
|
|
|
3866
3885
|
}
|
|
3867
3886
|
return out;
|
|
3868
3887
|
}
|
|
3888
|
+
function classifyMobileAssertValue(value, context = {}) {
|
|
3889
|
+
const text = value.trim();
|
|
3890
|
+
if (!text)
|
|
3891
|
+
return null;
|
|
3892
|
+
if (/^\{\{[A-Z0-9_]+\}\}$/.test(text))
|
|
3893
|
+
return null;
|
|
3894
|
+
const t1 = matchFirst(TIER1_PATTERNS, text);
|
|
3895
|
+
if (t1)
|
|
3896
|
+
return { tier: 1, reason: t1[1] };
|
|
3897
|
+
const authIdentityValues = [];
|
|
3898
|
+
if (context.authenticatedUserEmail)
|
|
3899
|
+
authIdentityValues.push(context.authenticatedUserEmail);
|
|
3900
|
+
if (context.authenticatedUserName && context.authenticatedUserName.length >= 3) {
|
|
3901
|
+
authIdentityValues.push(context.authenticatedUserName);
|
|
3902
|
+
}
|
|
3903
|
+
for (const auth of authIdentityValues) {
|
|
3904
|
+
if (auth.length >= 3 && text.toLowerCase().includes(auth.toLowerCase())) {
|
|
3905
|
+
return { tier: 2, reason: "asserts the authenticated user identity \u2014 fails under any other test account" };
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
3908
|
+
const knownValues = [
|
|
3909
|
+
...context.knownUserInputs ?? [],
|
|
3910
|
+
...context.scenarioCreatedValues ?? []
|
|
3911
|
+
].filter((v) => {
|
|
3912
|
+
for (const auth of authIdentityValues) {
|
|
3913
|
+
if (auth.length >= 3 && v.toLowerCase().includes(auth.toLowerCase()))
|
|
3914
|
+
return false;
|
|
3915
|
+
}
|
|
3916
|
+
return true;
|
|
3917
|
+
});
|
|
3918
|
+
if (containsKnownValue(text, knownValues))
|
|
3919
|
+
return null;
|
|
3920
|
+
const t2 = matchFirst(TIER2_PATTERNS, text);
|
|
3921
|
+
if (t2)
|
|
3922
|
+
return { tier: 2, reason: t2[1] };
|
|
3923
|
+
return null;
|
|
3924
|
+
}
|
|
3869
3925
|
function parenDepthDelta(text) {
|
|
3870
3926
|
let depth = 0;
|
|
3871
3927
|
let quote = null;
|
|
@@ -6039,6 +6095,7 @@ __export(dist_exports, {
|
|
|
6039
6095
|
classifyAssertionBlock: () => classifyAssertionBlock,
|
|
6040
6096
|
classifyAssertionLine: () => classifyAssertionLine,
|
|
6041
6097
|
classifyErrorMessage: () => classifyErrorMessage,
|
|
6098
|
+
classifyMobileAssertValue: () => classifyMobileAssertValue,
|
|
6042
6099
|
classifyTestHealth: () => classifyTestHealth,
|
|
6043
6100
|
coldSessionConfigMessage: () => coldSessionConfigMessage,
|
|
6044
6101
|
computeMobileSwipePointers: () => computeMobileSwipePointers,
|
|
@@ -73624,6 +73681,7 @@ var require_inbox_tools = __commonJS({
|
|
|
73624
73681
|
exports2.buildInboxPromptBlock = buildInboxPromptBlock;
|
|
73625
73682
|
exports2.guardVerificationCodeFabrication = guardVerificationCodeFabrication;
|
|
73626
73683
|
exports2.hasRegisterIntent = hasRegisterIntent;
|
|
73684
|
+
exports2.hasSignupTestIntent = hasSignupTestIntent;
|
|
73627
73685
|
exports2.resolveInboxRegistration = resolveInboxRegistration;
|
|
73628
73686
|
exports2.applyInboxRegistrationIdentity = applyInboxRegistrationIdentity;
|
|
73629
73687
|
exports2.isThrowawayRegisterSeedEmail = isThrowawayRegisterSeedEmail;
|
|
@@ -74167,6 +74225,9 @@ This project asked to REGISTER a new account, but this runner CANNOT receive ema
|
|
|
74167
74225
|
const v = variableContext.allTestVariables?.DISCOVERY_SHOULD_REGISTER ?? variableContext.publicTestVariables?.DISCOVERY_SHOULD_REGISTER ?? "";
|
|
74168
74226
|
return String(v).trim().toLowerCase() === "true";
|
|
74169
74227
|
}
|
|
74228
|
+
function hasSignupTestIntent(variableContext) {
|
|
74229
|
+
return variableContext?.allTestVariables?.DISCOVERY_ALLOW_SIGNUP_TESTS !== "false";
|
|
74230
|
+
}
|
|
74170
74231
|
function resolveInboxRegistration(opts) {
|
|
74171
74232
|
const config = (0, verification_inbox_js_1.loadVerificationInboxConfig)(opts.env ?? process.env);
|
|
74172
74233
|
if (!config)
|
|
@@ -80879,6 +80940,45 @@ ${constraints.join("\n")}`);
|
|
|
80879
80940
|
}
|
|
80880
80941
|
});
|
|
80881
80942
|
|
|
80943
|
+
// ../runner-core/dist/services/discovery-lint-flags.js
|
|
80944
|
+
var require_discovery_lint_flags = __commonJS({
|
|
80945
|
+
"../runner-core/dist/services/discovery-lint-flags.js"(exports2) {
|
|
80946
|
+
"use strict";
|
|
80947
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
80948
|
+
exports2.structuralLocatorLintMode = structuralLocatorLintMode;
|
|
80949
|
+
exports2.isPwMethodPreflightEnabled = isPwMethodPreflightEnabled;
|
|
80950
|
+
exports2.isVqaLoginDedupeEnabled = isVqaLoginDedupeEnabled;
|
|
80951
|
+
exports2.mobileSelectorQualityLintMode = mobileSelectorQualityLintMode;
|
|
80952
|
+
exports2.isTestgenEmailVerifyEnabled = isTestgenEmailVerifyEnabled;
|
|
80953
|
+
function strictOn(value) {
|
|
80954
|
+
return value === "true" || value === "1";
|
|
80955
|
+
}
|
|
80956
|
+
function structuralLocatorLintMode() {
|
|
80957
|
+
const v = (process.env.DISCOVERY_STRUCTURAL_LOCATOR_LINT || "").trim().toLowerCase();
|
|
80958
|
+
if (v === "warn")
|
|
80959
|
+
return "warn";
|
|
80960
|
+
if (v === "true" || v === "1" || v === "enforce")
|
|
80961
|
+
return "enforce";
|
|
80962
|
+
return "off";
|
|
80963
|
+
}
|
|
80964
|
+
function isPwMethodPreflightEnabled() {
|
|
80965
|
+
return strictOn(process.env.DISCOVERY_PW_METHOD_PREFLIGHT);
|
|
80966
|
+
}
|
|
80967
|
+
function isVqaLoginDedupeEnabled() {
|
|
80968
|
+
return strictOn(process.env.DISCOVERY_VQALOGIN_DEDUPE);
|
|
80969
|
+
}
|
|
80970
|
+
function mobileSelectorQualityLintMode() {
|
|
80971
|
+
const v = (process.env.MOBILE_SELECTOR_QUALITY_LINT || "").trim().toLowerCase();
|
|
80972
|
+
if (v === "true" || v === "1" || v === "enforce")
|
|
80973
|
+
return "enforce";
|
|
80974
|
+
return "off";
|
|
80975
|
+
}
|
|
80976
|
+
function isTestgenEmailVerifyEnabled() {
|
|
80977
|
+
return strictOn(process.env.DISCOVERY_TESTGEN_EMAIL_VERIFY);
|
|
80978
|
+
}
|
|
80979
|
+
}
|
|
80980
|
+
});
|
|
80981
|
+
|
|
80882
80982
|
// ../runner-core/dist/services/mobile/mobile-generator.js
|
|
80883
80983
|
var require_mobile_generator = __commonJS({
|
|
80884
80984
|
"../runner-core/dist/services/mobile/mobile-generator.js"(exports2) {
|
|
@@ -80898,6 +80998,7 @@ var require_mobile_generator = __commonJS({
|
|
|
80898
80998
|
var ai_queue_js_1 = require_ai_queue();
|
|
80899
80999
|
var ai_retry_js_1 = require_ai_retry();
|
|
80900
81000
|
var mobile_session_keepalive_js_1 = require_mobile_session_keepalive();
|
|
81001
|
+
var discovery_lint_flags_js_1 = require_discovery_lint_flags();
|
|
80901
81002
|
var mobile_source_parser_js_1 = require_mobile_source_parser();
|
|
80902
81003
|
var mobile_source_parser_js_2 = require_mobile_source_parser();
|
|
80903
81004
|
var mobile_observation_js_1 = require_mobile_observation();
|
|
@@ -81067,7 +81168,15 @@ var require_mobile_generator = __commonJS({
|
|
|
81067
81168
|
properties: {
|
|
81068
81169
|
name: { type: "string", description: 'Descriptive test name (e.g. "Login - Happy Path").' },
|
|
81069
81170
|
description: { type: "string", description: "One-line description of what the test proves." },
|
|
81070
|
-
verified: { type: "boolean", description: "True only if you asserted the outcome LIVE with an assert tool." }
|
|
81171
|
+
verified: { type: "boolean", description: "True only if you asserted the outcome LIVE with an assert tool." },
|
|
81172
|
+
suspectedBug: {
|
|
81173
|
+
type: "boolean",
|
|
81174
|
+
description: "True when the app itself misbehaved (crash, error screen, broken flow) and the scenario could not behave as specified. Requires bugDescription and at least one honored assert proving what you observed."
|
|
81175
|
+
},
|
|
81176
|
+
bugDescription: {
|
|
81177
|
+
type: "string",
|
|
81178
|
+
description: "What is broken in the APP (not the test) \u2014 concrete screen + observed vs expected."
|
|
81179
|
+
}
|
|
81071
81180
|
},
|
|
81072
81181
|
required: ["name", "description", "verified"]
|
|
81073
81182
|
}
|
|
@@ -81313,6 +81422,8 @@ var require_mobile_generator = __commonJS({
|
|
|
81313
81422
|
- Assert the OUTCOME of the flow (the new screen / new content), not the control you just tapped.
|
|
81314
81423
|
- Assert only DURABLE elements \u2014 screen chrome that exists on every visit (tab labels, headers, nav bars, the entity you just created). NEVER assert one-time / data-dependent UI: empty-state CTAs, first-run or onboarding prompts, or counters \u2014 they vanish as soon as the account has data, and the test then fails forever on a healthy app.
|
|
81315
81424
|
- Do NOT trigger irreversible or destructive side effects \u2014 real payments; sending messages to others; and ESPECIALLY account/data deletion, deactivation, or any "type DELETE to confirm" gate (the runner hard-blocks destructive taps and confirmation keystrokes). Note them and assert what you safely can.
|
|
81425
|
+
- STATE OWNERSHIP for mutation scenarios (edit / rename / archive / remove): CREATE the record first in this same recording, then act on the row YOU created \u2014 never mutate pre-existing data (it may not exist on a clean install, so the replay fails forever, and another test may depend on it). If the app offers no way to create one, do NOT act on someone else's data: assert what you can safely prove and finish with verified:false, explaining why in the description.
|
|
81426
|
+
- If the APP itself misbehaves (crash, error screen, a flow that cannot proceed as specified), do not force a green path: assert the failure you observed (e.g. the error text), then call finish_generate with suspectedBug:true and a concrete bugDescription of what is broken in the app.
|
|
81316
81427
|
- Stay inside this app.${targetLine} Do not open or use app stores, browsers, email apps, settings, or any other app. If an action leaves the target app, the runner will relaunch the target app and reject that action.
|
|
81317
81428
|
- NEVER tap the phone's home screen / launcher (app icons, "Predicted app" chips, search bars, widgets). Those are NOT part of the app under test \u2014 tapping them is junk that poisons the test. If the app ever backgrounds or you find yourself on the launcher, use mobile_relaunch to cold-start the app; do NOT tap a launcher icon or app-prediction to get back in.
|
|
81318
81429
|
- Do NOT re-do a sequence you already completed. If you filled a form then navigated away, do NOT come back and fill it again \u2014 the scenario needs ONE clean pass. Filling the login form, going back, and re-filling it is exactly the kind of wandering that produces a bloated, broken test.
|
|
@@ -81418,6 +81529,7 @@ Start by calling mobile_snapshot, then execute the steps above \u2014 the minimu
|
|
|
81418
81529
|
let assertCount = 0;
|
|
81419
81530
|
let finish = null;
|
|
81420
81531
|
let finishIntentRejections = 0;
|
|
81532
|
+
let finishStrengthRejections = 0;
|
|
81421
81533
|
let emptyTurnsAfterFinishReject = 0;
|
|
81422
81534
|
let latest = null;
|
|
81423
81535
|
const getLatest = () => latest;
|
|
@@ -81910,31 +82022,49 @@ ${statePacket}`, redactionCredentials) },
|
|
|
81910
82022
|
const claimedVerified = Boolean(toolArgs.verified);
|
|
81911
82023
|
const intentReason = scenarioIntentMismatchReason(scenario, actions);
|
|
81912
82024
|
const verifiedWithoutAssert = claimedVerified && assertCount === 0;
|
|
81913
|
-
const
|
|
81914
|
-
|
|
81915
|
-
|
|
82025
|
+
const intentStyleReject = finishIntentRejections < MAX_FINISH_INTENT_REJECTIONS && iteration < maxIterations && (intentReason !== null || verifiedWithoutAssert);
|
|
82026
|
+
let strengthRejectReason = null;
|
|
82027
|
+
if (!intentStyleReject && claimedVerified && assertCount > 0 && intentReason === null && finishStrengthRejections < 1 && iteration < maxIterations) {
|
|
82028
|
+
const verdict = evaluateMobileAssertionStrength(actions);
|
|
82029
|
+
if (verdict.severity === "block") {
|
|
82030
|
+
strengthRejectReason = verdict.reason;
|
|
82031
|
+
}
|
|
82032
|
+
}
|
|
82033
|
+
if (intentStyleReject || strengthRejectReason !== null) {
|
|
82034
|
+
if (intentStyleReject)
|
|
82035
|
+
finishIntentRejections += 1;
|
|
82036
|
+
if (strengthRejectReason !== null)
|
|
82037
|
+
finishStrengthRejections += 1;
|
|
81916
82038
|
const reasons = [];
|
|
81917
82039
|
if (intentReason)
|
|
81918
82040
|
reasons.push(intentReason);
|
|
81919
82041
|
if (verifiedWithoutAssert) {
|
|
81920
82042
|
reasons.push("claimed verified:true but never called mobile_assert_visible / mobile_assert_text");
|
|
81921
82043
|
}
|
|
82044
|
+
if (strengthRejectReason !== null) {
|
|
82045
|
+
reasons.push(`assertion strength: ${strengthRejectReason} \u2014 your only live proof re-checks a control you already used. Assert the scenario OUTCOME instead: an element on the resulting screen, the created/updated row, or the success/error message.`);
|
|
82046
|
+
}
|
|
81922
82047
|
const reasonText = reasons.join("; ");
|
|
81923
|
-
log2(` finish_generate REJECTED (${finishIntentRejections}/${MAX_FINISH_INTENT_REJECTIONS}): ${reasonText}`);
|
|
82048
|
+
log2(` finish_generate REJECTED (intent ${finishIntentRejections}/${MAX_FINISH_INTENT_REJECTIONS}, strength ${finishStrengthRejections}/1): ${reasonText}`);
|
|
81924
82049
|
pushResult({
|
|
81925
82050
|
ok: false,
|
|
81926
82051
|
error: `Cannot finish yet \u2014 the recorded steps do not complete this scenario: ${reasonText}. Continue driving the app (fill fields, commit forms, navigate, assert the REAL outcome), then call finish_generate again. Do NOT re-open menus or thrash Back.`,
|
|
81927
82052
|
intentMismatch: intentReason,
|
|
81928
82053
|
verifiedWithoutAssert,
|
|
81929
|
-
|
|
81930
|
-
|
|
82054
|
+
...strengthRejectReason !== null ? { strengthBlock: strengthRejectReason } : {},
|
|
82055
|
+
rejection: finishIntentRejections + finishStrengthRejections,
|
|
82056
|
+
maxRejections: MAX_FINISH_INTENT_REJECTIONS + 1
|
|
81931
82057
|
});
|
|
81932
82058
|
continue;
|
|
81933
82059
|
}
|
|
81934
82060
|
finish = {
|
|
81935
82061
|
name: redactCredentialValues(nonEmpty3(String(toolArgs.name ?? "")) ?? scenario.name, redactionCredentials),
|
|
81936
82062
|
description: redactCredentialValues(nonEmpty3(String(toolArgs.description ?? "")) ?? scenario.goal, redactionCredentials),
|
|
81937
|
-
verified: claimedVerified
|
|
82063
|
+
verified: claimedVerified,
|
|
82064
|
+
...toolArgs.suspectedBug === true ? {
|
|
82065
|
+
suspectedBug: true,
|
|
82066
|
+
bugDescription: redactCredentialValues(nonEmpty3(String(toolArgs.bugDescription ?? "")) ?? "", redactionCredentials)
|
|
82067
|
+
} : {}
|
|
81938
82068
|
};
|
|
81939
82069
|
log2(` finish_generate: "${finish.name}" (claimed verified: ${finish.verified}, asserts: ${assertCount})`);
|
|
81940
82070
|
pushResult({ acknowledged: true });
|
|
@@ -81998,6 +82128,7 @@ ${statePacket}`, redactionCredentials) },
|
|
|
81998
82128
|
ok: false,
|
|
81999
82129
|
error: `Unknown element ref "${String(ref)}". Call mobile_snapshot first; refs are only valid for the latest snapshot.`
|
|
82000
82130
|
});
|
|
82131
|
+
const recordedTypeValues = () => recordedActions.filter((action) => action.type === "TYPE" && typeof action.value === "string" && action.value.length > 0).map((action) => action.value);
|
|
82001
82132
|
const actionPayload = (result, absorbed) => {
|
|
82002
82133
|
const blocked = Boolean(absorbed.externalBlocked || absorbed.transientExternal);
|
|
82003
82134
|
return {
|
|
@@ -82398,7 +82529,8 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82398
82529
|
latest,
|
|
82399
82530
|
screenName,
|
|
82400
82531
|
platform: platform3,
|
|
82401
|
-
recordAction
|
|
82532
|
+
recordAction,
|
|
82533
|
+
typedValues: recordedTypeValues
|
|
82402
82534
|
});
|
|
82403
82535
|
if (!recorded.ok)
|
|
82404
82536
|
return recorded;
|
|
@@ -82467,6 +82599,15 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82467
82599
|
error: `Live text assertion failed: expected ${JSON.stringify(expected)} was not present on ref ${element.ref}. Re-snapshot after the outcome appears and assert the text that is actually visible.`
|
|
82468
82600
|
};
|
|
82469
82601
|
}
|
|
82602
|
+
const volatileExpected = (0, shared_1.classifyMobileAssertValue)(expected, {
|
|
82603
|
+
knownUserInputs: recordedTypeValues()
|
|
82604
|
+
});
|
|
82605
|
+
if (volatileExpected) {
|
|
82606
|
+
return {
|
|
82607
|
+
ok: false,
|
|
82608
|
+
error: `Expected text ${JSON.stringify(expected)} is ${volatileExpected.tier === 1 ? "transient one-shot UI" : "session-dynamic content"} (${volatileExpected.reason}) \u2014 it changes between runs, so the test would fail forever on a healthy app. Assert durable post-state instead: an element unique to the destination screen, a stable label, or text your flow itself entered.`
|
|
82609
|
+
};
|
|
82610
|
+
}
|
|
82470
82611
|
const metadata = metadataForElement(element, screenName(), platform3);
|
|
82471
82612
|
const expectedNeedle = expected.trim().toLowerCase();
|
|
82472
82613
|
const identityValues = [element.accessibilityId, element.resourceId].map((value) => value?.trim().toLowerCase()).filter((value) => Boolean(value));
|
|
@@ -82522,6 +82663,17 @@ ${statePacket}`, redactionCredentials) },
|
|
|
82522
82663
|
if (!accessibilityId && !text) {
|
|
82523
82664
|
return { ok: false, error: "mobile_assert_visible needs a ref (preferred), accessibilityId, or text." };
|
|
82524
82665
|
}
|
|
82666
|
+
if (!accessibilityId && text) {
|
|
82667
|
+
const volatileIdentity = (0, shared_1.classifyMobileAssertValue)(text, {
|
|
82668
|
+
knownUserInputs: ctx.typedValues?.() ?? []
|
|
82669
|
+
});
|
|
82670
|
+
if (volatileIdentity) {
|
|
82671
|
+
return {
|
|
82672
|
+
ok: false,
|
|
82673
|
+
error: `${JSON.stringify(text)} is ${volatileIdentity.tier === 1 ? "transient one-shot UI" : "session-dynamic content"} (${volatileIdentity.reason}) \u2014 a text identity must be durable to resolve at replay. Assert a stable element of the destination screen instead (prefer a ref from mobile_snapshot).`
|
|
82674
|
+
};
|
|
82675
|
+
}
|
|
82676
|
+
}
|
|
82525
82677
|
const directTarget = accessibilityId ?? text;
|
|
82526
82678
|
if (directTarget && isStructuralAssertionIdentity(directTarget)) {
|
|
82527
82679
|
return {
|
|
@@ -83157,7 +83309,21 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83157
83309
|
if (claimedVerified && !verified) {
|
|
83158
83310
|
log2(" downgraded verified -> false: model claimed verified but called zero assert tools.");
|
|
83159
83311
|
}
|
|
83160
|
-
const
|
|
83312
|
+
const baseStrengthVerdict = evaluateMobileAssertionStrength(actions);
|
|
83313
|
+
const bugDescription = recording.finish?.bugDescription?.trim() ?? "";
|
|
83314
|
+
const bugWitnessHonored = process.env.DISCOVERY_BUG_WITNESS_ENABLED === "true" && recording.finish?.suspectedBug === true && bugDescription.length >= 12 && recording.assertCount > 0;
|
|
83315
|
+
if (recording.finish?.suspectedBug === true && !bugWitnessHonored) {
|
|
83316
|
+
log2(" suspectedBug declared but NOT honored: " + (process.env.DISCOVERY_BUG_WITNESS_ENABLED !== "true" ? "bug-witness channel disabled (DISCOVERY_BUG_WITNESS_ENABLED)." : bugDescription.length < 12 ? "bugDescription too short." : "no honored assert backs the observation."));
|
|
83317
|
+
}
|
|
83318
|
+
const strengthVerdict = bugWitnessHonored ? {
|
|
83319
|
+
...baseStrengthVerdict,
|
|
83320
|
+
ok: false,
|
|
83321
|
+
severity: "route-bug",
|
|
83322
|
+
reason: `Model declared an app bug during generation: ${bugDescription}`
|
|
83323
|
+
} : baseStrengthVerdict;
|
|
83324
|
+
if (bugWitnessHonored) {
|
|
83325
|
+
log2(` bug-witness honored \u2014 routing as app bug: ${bugDescription}`);
|
|
83326
|
+
}
|
|
83161
83327
|
if (strengthVerdict.severity === "block" || strengthVerdict.severity === "route-bug") {
|
|
83162
83328
|
if (verified) {
|
|
83163
83329
|
log2(` downgraded verified -> false: assertion strength ${strengthVerdict.severity} \u2014 ${strengthVerdict.reason}`);
|
|
@@ -83173,7 +83339,11 @@ ${statePacket}`, redactionCredentials) },
|
|
|
83173
83339
|
verified = false;
|
|
83174
83340
|
}
|
|
83175
83341
|
const platformTag2 = platform3 === "IOS" ? "@ios" : "@android";
|
|
83176
|
-
const
|
|
83342
|
+
const selectorLintEnforced = (0, discovery_lint_flags_js_1.mobileSelectorQualityLintMode)() === "enforce" && selectorQuality.summary.high > 0;
|
|
83343
|
+
if (selectorLintEnforced && verified) {
|
|
83344
|
+
log2(` selector-quality lint (enforce): ${selectorQuality.summary.high} high-severity locator issue(s) \u2014 tagging @needs-review.`);
|
|
83345
|
+
}
|
|
83346
|
+
const reviewTag = verified && !selectorLintEnforced ? "@verified" : "@needs-review";
|
|
83177
83347
|
const tags = ["@discovered", "@mobile", platformTag2, reviewTag];
|
|
83178
83348
|
if (strengthVerdict.severity === "downgrade")
|
|
83179
83349
|
tags.push("@smoke");
|
|
@@ -86253,38 +86423,6 @@ var require_run_callback_proxy = __commonJS({
|
|
|
86253
86423
|
}
|
|
86254
86424
|
});
|
|
86255
86425
|
|
|
86256
|
-
// ../runner-core/dist/services/discovery-lint-flags.js
|
|
86257
|
-
var require_discovery_lint_flags = __commonJS({
|
|
86258
|
-
"../runner-core/dist/services/discovery-lint-flags.js"(exports2) {
|
|
86259
|
-
"use strict";
|
|
86260
|
-
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
86261
|
-
exports2.structuralLocatorLintMode = structuralLocatorLintMode;
|
|
86262
|
-
exports2.isPwMethodPreflightEnabled = isPwMethodPreflightEnabled;
|
|
86263
|
-
exports2.isVqaLoginDedupeEnabled = isVqaLoginDedupeEnabled;
|
|
86264
|
-
exports2.isTestgenEmailVerifyEnabled = isTestgenEmailVerifyEnabled;
|
|
86265
|
-
function strictOn(value) {
|
|
86266
|
-
return value === "true" || value === "1";
|
|
86267
|
-
}
|
|
86268
|
-
function structuralLocatorLintMode() {
|
|
86269
|
-
const v = (process.env.DISCOVERY_STRUCTURAL_LOCATOR_LINT || "").trim().toLowerCase();
|
|
86270
|
-
if (v === "warn")
|
|
86271
|
-
return "warn";
|
|
86272
|
-
if (v === "true" || v === "1" || v === "enforce")
|
|
86273
|
-
return "enforce";
|
|
86274
|
-
return "off";
|
|
86275
|
-
}
|
|
86276
|
-
function isPwMethodPreflightEnabled() {
|
|
86277
|
-
return strictOn(process.env.DISCOVERY_PW_METHOD_PREFLIGHT);
|
|
86278
|
-
}
|
|
86279
|
-
function isVqaLoginDedupeEnabled() {
|
|
86280
|
-
return strictOn(process.env.DISCOVERY_VQALOGIN_DEDUPE);
|
|
86281
|
-
}
|
|
86282
|
-
function isTestgenEmailVerifyEnabled() {
|
|
86283
|
-
return strictOn(process.env.DISCOVERY_TESTGEN_EMAIL_VERIFY);
|
|
86284
|
-
}
|
|
86285
|
-
}
|
|
86286
|
-
});
|
|
86287
|
-
|
|
86288
86426
|
// ../runner-core/dist/services/auth-recipe.js
|
|
86289
86427
|
var require_auth_recipe = __commonJS({
|
|
86290
86428
|
"../runner-core/dist/services/auth-recipe.js"(exports2) {
|
|
@@ -95115,6 +95253,8 @@ var require_discover_generator = __commonJS({
|
|
|
95115
95253
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
95116
95254
|
exports2.MIN_BUG_DESCRIPTION_LENGTH = exports2.VALID_PLAYWRIGHT_GETBY_METHODS = exports2.BLOCKED_MCP_TOOLS_SCENARIO_GEN = void 0;
|
|
95117
95255
|
exports2.scenarioLooksLikeRegistration = scenarioLooksLikeRegistration;
|
|
95256
|
+
exports2.signupFeatureTestingAllowed = signupFeatureTestingAllowed;
|
|
95257
|
+
exports2.scenarioTouchesSignupAuthSurface = scenarioTouchesSignupAuthSurface;
|
|
95118
95258
|
exports2.filterMcpToolsForScenarioGen = filterMcpToolsForScenarioGen;
|
|
95119
95259
|
exports2.detectScenarioSignals = detectScenarioSignals;
|
|
95120
95260
|
exports2.scenarioStepsCoverAuth = scenarioStepsCoverAuth;
|
|
@@ -95202,6 +95342,19 @@ EMIT (the code you put in finish_generate) \u2014 do NOT hardcode the throwaway
|
|
|
95202
95342
|
- Tag the test \`@email-verify\`. Emit EXACTLY ONE test() in this file (one registration per run).
|
|
95203
95343
|
- 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.`;
|
|
95204
95344
|
}
|
|
95345
|
+
function signupFeatureTestingAllowed(credentials) {
|
|
95346
|
+
if (!(0, discovery_lint_flags_js_1.isTestgenEmailVerifyEnabled)() || !(0, verification_inbox_js_1.loadVerificationInboxConfig)())
|
|
95347
|
+
return false;
|
|
95348
|
+
return credentials?.DISCOVERY_ALLOW_SIGNUP_TESTS !== "false";
|
|
95349
|
+
}
|
|
95350
|
+
var SIGNUP_AUTH_ROUTE_RE = /\/(sign-?up|verify-?email|email-?verification|create-?account|confirm-?email)\b/i;
|
|
95351
|
+
function scenarioTouchesSignupAuthSurface(scenario, pages) {
|
|
95352
|
+
const routes = [scenario.entryRoute, ...scenario.targetPages ?? []].filter((r) => !!r);
|
|
95353
|
+
if (routes.some((r) => SIGNUP_AUTH_ROUTE_RE.test(r)))
|
|
95354
|
+
return true;
|
|
95355
|
+
const authRoutes = new Set(pages.filter((p) => (p.pageType ?? "").toUpperCase() === "AUTH").map((p) => p.route));
|
|
95356
|
+
return routes.some((r) => authRoutes.has(r));
|
|
95357
|
+
}
|
|
95205
95358
|
exports2.BLOCKED_MCP_TOOLS_SCENARIO_GEN = /* @__PURE__ */ new Set([
|
|
95206
95359
|
"browser_install",
|
|
95207
95360
|
"browser_close",
|
|
@@ -98313,9 +98466,16 @@ ${tailBlock}`;
|
|
|
98313
98466
|
extraAuthEvidence: !!opts?.recipe || hasConfiguredLoginCredentials(credentials)
|
|
98314
98467
|
});
|
|
98315
98468
|
const baseSkeletonHadRegression = tags.includes("@regression");
|
|
98469
|
+
const selfProvisioningSignupScenario = signupFeatureTestingAllowed(credentials) && scenarioLooksLikeRegistration(scenario) && scenarioTouchesSignupAuthSurface(scenario, siteMap.pages);
|
|
98470
|
+
if (selfProvisioningSignupScenario) {
|
|
98471
|
+
for (const prereqTag of ["@needs-auth", "@inline-login", "@auth-guarded"]) {
|
|
98472
|
+
const idx = tags.indexOf(prereqTag);
|
|
98473
|
+
if (idx !== -1)
|
|
98474
|
+
tags.splice(idx, 1);
|
|
98475
|
+
}
|
|
98476
|
+
}
|
|
98316
98477
|
const hasVerifiedRecipe = !!opts?.recipe && tags.includes("@needs-auth");
|
|
98317
|
-
const
|
|
98318
|
-
const inboxGenConfig = (0, discovery_lint_flags_js_1.isTestgenEmailVerifyEnabled)() && projectHasRegisterIntent && !hasVerifiedRecipe && scenarioLooksLikeRegistration(scenario) ? (0, verification_inbox_js_1.loadVerificationInboxConfig)() : null;
|
|
98478
|
+
const inboxGenConfig = selfProvisioningSignupScenario ? (0, verification_inbox_js_1.loadVerificationInboxConfig)() : null;
|
|
98319
98479
|
let genInboxState = null;
|
|
98320
98480
|
let genInboxCredentials = credentials;
|
|
98321
98481
|
let genInboxSecret = null;
|
|
@@ -99262,7 +99422,8 @@ Scenario: "${scenario.name}" (${scenario.type})`);
|
|
|
99262
99422
|
} catch {
|
|
99263
99423
|
}
|
|
99264
99424
|
const recipe = authRecipe;
|
|
99265
|
-
const
|
|
99425
|
+
const selfProvisioningSignupScenario = signupFeatureTestingAllowed(credentials) && scenarioLooksLikeRegistration(scenario) && scenarioTouchesSignupAuthSurface(scenario, siteMap.pages);
|
|
99426
|
+
const replayLogin = !selfProvisioningSignupScenario && shouldReplayAuthRecipe(scenario.targetPages, siteMap.pages, recipe);
|
|
99266
99427
|
let effectiveRecipe = null;
|
|
99267
99428
|
if (replayLogin && recipe) {
|
|
99268
99429
|
const planLeaks = findSensitiveLiteralLeaks(JSON.stringify(recipe.replayPlan ?? []), credentials);
|
|
@@ -99369,7 +99530,8 @@ Scenario: "${scenario.name}" (${scenario.type})`);
|
|
|
99369
99530
|
return;
|
|
99370
99531
|
}
|
|
99371
99532
|
let effectiveRecipe = null;
|
|
99372
|
-
|
|
99533
|
+
const selfProvisioningSignupScenario = signupFeatureTestingAllowed(credentials) && scenarioLooksLikeRegistration(scenario) && scenarioTouchesSignupAuthSurface(scenario, siteMap.pages);
|
|
99534
|
+
if (!selfProvisioningSignupScenario && shouldReplayAuthRecipe(scenario.targetPages, siteMap.pages, authRecipe) && authRecipe) {
|
|
99373
99535
|
const planLeaks = findSensitiveLiteralLeaks(JSON.stringify(authRecipe.replayPlan ?? []), credentials);
|
|
99374
99536
|
if (planLeaks.length > 0) {
|
|
99375
99537
|
scenarioLogs.push(` Warning: login replay plan contains literal credential value(s) for ${planLeaks.join(", ")} \u2014 falling back to model-authored login for "${scenario.name}"`);
|
|
@@ -335170,6 +335332,7 @@ var require_discover_agent = __commonJS({
|
|
|
335170
335332
|
var discover_ai_planner_js_1 = require_discover_ai_planner();
|
|
335171
335333
|
var discovery_lint_flags_js_1 = require_discovery_lint_flags();
|
|
335172
335334
|
var verification_inbox_js_1 = require_verification_inbox();
|
|
335335
|
+
var inbox_tools_js_1 = require_inbox_tools();
|
|
335173
335336
|
var sitemap_builder_js_1 = require_sitemap_builder();
|
|
335174
335337
|
Object.defineProperty(exports2, "buildInMemorySiteMap", { enumerable: true, get: function() {
|
|
335175
335338
|
return sitemap_builder_js_1.buildInMemorySiteMap;
|
|
@@ -335183,11 +335346,16 @@ var require_discover_agent = __commonJS({
|
|
|
335183
335346
|
var canvas_detector_js_1 = require_canvas_detector();
|
|
335184
335347
|
var auth_recipe_js_1 = require_auth_recipe();
|
|
335185
335348
|
function plannerEmailVerifyArmed(ctx) {
|
|
335186
|
-
if (!(0, discovery_lint_flags_js_1.isTestgenEmailVerifyEnabled)())
|
|
335187
|
-
return false;
|
|
335188
|
-
if (ctx.variableContext?.allTestVariables?.DISCOVERY_SHOULD_REGISTER !== "true")
|
|
335349
|
+
if (!(0, discovery_lint_flags_js_1.isTestgenEmailVerifyEnabled)() || !(0, verification_inbox_js_1.loadVerificationInboxConfig)())
|
|
335189
335350
|
return false;
|
|
335190
|
-
return (0,
|
|
335351
|
+
return (0, inbox_tools_js_1.hasSignupTestIntent)(ctx.variableContext);
|
|
335352
|
+
}
|
|
335353
|
+
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;
|
|
335354
|
+
var PLANNER_AUTH_ROUTE_RE = /\/(auth|login|sign-?in|sign-?up|signup|verify-?email|create-?account)\b/i;
|
|
335355
|
+
function featureLooksLikeAuth(featureName, focusRoutes) {
|
|
335356
|
+
if (featureName && PLANNER_AUTH_NAME_RE.test(featureName))
|
|
335357
|
+
return true;
|
|
335358
|
+
return (focusRoutes ?? []).some((r) => PLANNER_AUTH_ROUTE_RE.test(r));
|
|
335191
335359
|
}
|
|
335192
335360
|
async function runCanvasPreflight(ctx, sharedBrowser, log2) {
|
|
335193
335361
|
const cached = ctx.appRenderType ?? null;
|
|
@@ -335839,6 +336007,11 @@ Explore complete: ${exploreResult.pagesDiscovered} pages, ${exploreResult.elemen
|
|
|
335839
336007
|
authRedirectRoute: (0, shared_1.parseBoolFlag)(process.env.AUTH_RECIPE_STAMP) ? ctx.authRecipe?.redirectRoute ?? void 0 : void 0,
|
|
335840
336008
|
// Email-verify capability — only for scenario-producing modes; a survey emits
|
|
335841
336009
|
// featureGroups (no scenarios), so keep its prompt free of the round-trip block.
|
|
336010
|
+
// This site serves survey + FULL mode. Full mode is app-wide (no single feature
|
|
336011
|
+
// identity here), so we deliberately do NOT apply the auth-feature scope: the
|
|
336012
|
+
// per-scenario generator gates (scenarioLooksLikeRegistration +
|
|
336013
|
+
// scenarioTouchesSignupAuthSurface) still isolate non-auth scenarios. The
|
|
336014
|
+
// auth-feature scope IS applied at the DEEP call site where ctx.featureName is known.
|
|
335842
336015
|
emailVerifyArmed: mode !== "survey" && plannerEmailVerifyArmed(ctx),
|
|
335843
336016
|
testBudget
|
|
335844
336017
|
}, {
|
|
@@ -336427,9 +336600,10 @@ Plan (reused): ${resumeSavedScenarios.length} scenarios from the paused run.`);
|
|
|
336427
336600
|
// Email-verify capability: when armed, let the planner plan the full
|
|
336428
336601
|
// register→verify and forgot-password→reset round trips to completion
|
|
336429
336602
|
// (see the planner's "## Verification Inbox Available" block). Armed on the
|
|
336430
|
-
// SAME project-level predicate the generator uses (flag +
|
|
336603
|
+
// SAME project-level predicate the generator uses (flag + consent +
|
|
336431
336604
|
// configured inbox), so the planner never promises what generation can't do.
|
|
336432
|
-
|
|
336605
|
+
// Scoped to AUTH features: a non-auth feature has no signup surface to test.
|
|
336606
|
+
emailVerifyArmed: plannerEmailVerifyArmed(ctx) && featureLooksLikeAuth(ctx.featureName, augmentedFeatureFocusAreas),
|
|
336433
336607
|
testBudget: featureTestBudget
|
|
336434
336608
|
}, {
|
|
336435
336609
|
modelOverride: ctx.planModel ?? ctx.generateModel,
|
|
@@ -347747,12 +347921,7 @@ Write valid JSON only (no markdown fences inside files). Be exhaustive.`;
|
|
|
347747
347921
|
const o = value;
|
|
347748
347922
|
if (!isNonEmptyString(o.name))
|
|
347749
347923
|
return { ok: false, reason: "name: missing" };
|
|
347750
|
-
|
|
347751
|
-
return { ok: false, reason: "appiumCode: missing" };
|
|
347752
|
-
const code = o.appiumCode;
|
|
347753
|
-
if (!/webdriverio|remote\s*\(|driver\s*\./i.test(code)) {
|
|
347754
|
-
return { ok: false, reason: "appiumCode: not a WebdriverIO/Appium script" };
|
|
347755
|
-
}
|
|
347924
|
+
const code = isNonEmptyString(o.appiumCode) ? o.appiumCode : "";
|
|
347756
347925
|
if (!Array.isArray(o.steps) || o.steps.length === 0) {
|
|
347757
347926
|
return { ok: false, reason: "steps: missing or empty" };
|
|
347758
347927
|
}
|
|
@@ -351055,7 +351224,7 @@ var require_package4 = __commonJS({
|
|
|
351055
351224
|
"package.json"(exports2, module2) {
|
|
351056
351225
|
module2.exports = {
|
|
351057
351226
|
name: "@validate.qa/runner",
|
|
351058
|
-
version: "1.0.
|
|
351227
|
+
version: "1.0.67",
|
|
351059
351228
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
351060
351229
|
bin: {
|
|
351061
351230
|
"validate-runner": "dist/cli.js"
|
|
@@ -351451,6 +351620,9 @@ function sanitizeAppiumLogLine(line) {
|
|
|
351451
351620
|
}
|
|
351452
351621
|
function setAppiumWorkloadActive(active) {
|
|
351453
351622
|
activeWorkloadCount = active ? activeWorkloadCount + 1 : Math.max(0, activeWorkloadCount - 1);
|
|
351623
|
+
if (activeWorkloadCount === 0) {
|
|
351624
|
+
consecutiveHealthFailures = 0;
|
|
351625
|
+
}
|
|
351454
351626
|
}
|
|
351455
351627
|
function shouldRestartAfterHealthFailure(workloadCount, failureCount) {
|
|
351456
351628
|
return workloadCount <= 0 && failureCount >= HEALTH_FAILURES_BEFORE_RESTART;
|
|
@@ -351946,17 +352118,31 @@ function rectMatchesIntendedBounds(rect, bounds) {
|
|
|
351946
352118
|
return rectCenterInBounds || boundsCenterInRect;
|
|
351947
352119
|
}
|
|
351948
352120
|
async function resolveTypeTargetElementId(sessionPath, bounds) {
|
|
351949
|
-
const
|
|
352121
|
+
const rawBox = bounds !== void 0 ? normaliseBounds(bounds) : null;
|
|
352122
|
+
const box = rawBox && rawBox.width > 0 && rawBox.height > 0 ? rawBox : null;
|
|
351950
352123
|
let elementId = await getActiveElementId(sessionPath);
|
|
351951
|
-
|
|
352124
|
+
let rejectedElementId = null;
|
|
352125
|
+
let rejectedForMismatch = false;
|
|
352126
|
+
if (elementId && box) {
|
|
351952
352127
|
const rect = await getElementRect(sessionPath, elementId);
|
|
351953
352128
|
if (!rect || !rectMatchesIntendedBounds(rect, box)) {
|
|
352129
|
+
rejectedElementId = elementId;
|
|
352130
|
+
rejectedForMismatch = !!rect;
|
|
351954
352131
|
elementId = null;
|
|
351955
352132
|
}
|
|
351956
352133
|
}
|
|
351957
352134
|
if (!elementId && bounds !== void 0 && await tapBoundsCenter(sessionPath, bounds)) {
|
|
351958
352135
|
await new Promise((resolve2) => setTimeout(resolve2, MOBILE_FOCUS_SETTLE_MS));
|
|
351959
352136
|
elementId = await getActiveElementId(sessionPath);
|
|
352137
|
+
if (elementId && box) {
|
|
352138
|
+
const rect = await getElementRect(sessionPath, elementId);
|
|
352139
|
+
const provablyWrong = rect ? !rectMatchesIntendedBounds(rect, box) : rejectedForMismatch && elementId === rejectedElementId;
|
|
352140
|
+
if (provablyWrong) {
|
|
352141
|
+
throw new Error(
|
|
352142
|
+
"Could not focus the intended field: a different element kept keyboard focus after tapping the target bounds. Tap the field directly, then retry."
|
|
352143
|
+
);
|
|
352144
|
+
}
|
|
352145
|
+
}
|
|
351960
352146
|
}
|
|
351961
352147
|
return elementId;
|
|
351962
352148
|
}
|
|
@@ -354532,6 +354718,13 @@ function buildLocatorStrategies(target, platform3) {
|
|
|
354532
354718
|
if (bundle) {
|
|
354533
354719
|
if (bundle.accessibilityId) strategies.push({ using: "accessibility id", value: bundle.accessibilityId });
|
|
354534
354720
|
if (bundle.resourceId) strategies.push({ using: "id", value: bundle.resourceId });
|
|
354721
|
+
for (const value of [
|
|
354722
|
+
isEditableMobileClassName(bundle.className) ? void 0 : bundle.text,
|
|
354723
|
+
bundle.contentDesc
|
|
354724
|
+
]) {
|
|
354725
|
+
const trimmed = value?.trim();
|
|
354726
|
+
if (trimmed) strategies.push({ using: "xpath", value: identityTextXpath2(trimmed) });
|
|
354727
|
+
}
|
|
354535
354728
|
if (bundle.xpath) strategies.push({ using: "xpath", value: bundle.xpath });
|
|
354536
354729
|
}
|
|
354537
354730
|
if (!(platform3 && hasPlatformLocator(target, platform3))) {
|
|
@@ -354549,14 +354742,14 @@ function dedupeStrategies(strategies) {
|
|
|
354549
354742
|
(strategy, index, list) => list.findIndex((candidate) => candidate.using === strategy.using && candidate.value === strategy.value) === index
|
|
354550
354743
|
);
|
|
354551
354744
|
}
|
|
354552
|
-
function
|
|
354745
|
+
function xpathLiteral3(value) {
|
|
354553
354746
|
if (!value.includes("'")) return `'${value}'`;
|
|
354554
354747
|
if (!value.includes('"')) return `"${value}"`;
|
|
354555
354748
|
const parts = value.split("'").map((p) => `'${p}'`);
|
|
354556
354749
|
return `concat(${parts.join(`, "'", `)})`;
|
|
354557
354750
|
}
|
|
354558
|
-
function
|
|
354559
|
-
const lit =
|
|
354751
|
+
function identityTextXpath2(value) {
|
|
354752
|
+
const lit = xpathLiteral3(value);
|
|
354560
354753
|
return `//*[@text=${lit} or @content-desc=${lit} or @label=${lit} or @name=${lit} or @value=${lit}]`;
|
|
354561
354754
|
}
|
|
354562
354755
|
function isIdentityXpath(value) {
|
|
@@ -354576,7 +354769,7 @@ function buildIdentityLocatorStrategies(target, platform3) {
|
|
|
354576
354769
|
if (bundle.resourceId) strategies.push({ using: "id", value: bundle.resourceId });
|
|
354577
354770
|
for (const value of [bundle.text, bundle.contentDesc]) {
|
|
354578
354771
|
const trimmed = value?.trim();
|
|
354579
|
-
if (trimmed) strategies.push({ using: "xpath", value:
|
|
354772
|
+
if (trimmed) strategies.push({ using: "xpath", value: identityTextXpath2(trimmed) });
|
|
354580
354773
|
}
|
|
354581
354774
|
if (bundle.xpath && isIdentityXpath(bundle.xpath)) strategies.push({ using: "xpath", value: bundle.xpath });
|
|
354582
354775
|
}
|
|
@@ -355056,6 +355249,19 @@ async function readElementTextCandidates(sessionId, elementId, actualText) {
|
|
|
355056
355249
|
}
|
|
355057
355250
|
return values;
|
|
355058
355251
|
}
|
|
355252
|
+
var controlPortSlotsByDevice = /* @__PURE__ */ new Map();
|
|
355253
|
+
function acquireControlPortSlot(deviceId) {
|
|
355254
|
+
const existing = controlPortSlotsByDevice.get(deviceId);
|
|
355255
|
+
if (existing !== void 0) return existing;
|
|
355256
|
+
const taken = new Set(controlPortSlotsByDevice.values());
|
|
355257
|
+
let slot = 0;
|
|
355258
|
+
while (taken.has(slot)) slot += 1;
|
|
355259
|
+
controlPortSlotsByDevice.set(deviceId, slot);
|
|
355260
|
+
return slot;
|
|
355261
|
+
}
|
|
355262
|
+
function releaseControlPortSlot(deviceId) {
|
|
355263
|
+
controlPortSlotsByDevice.delete(deviceId);
|
|
355264
|
+
}
|
|
355059
355265
|
async function executeMobileTest(options) {
|
|
355060
355266
|
const logLines = [];
|
|
355061
355267
|
const log2 = (msg) => logLines.push(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}`);
|
|
@@ -355075,6 +355281,7 @@ async function executeMobileTest(options) {
|
|
|
355075
355281
|
let captureStopped = false;
|
|
355076
355282
|
let recordingStarted = false;
|
|
355077
355283
|
let recordingStopped = false;
|
|
355284
|
+
let acquiredSlotDeviceId = null;
|
|
355078
355285
|
let video;
|
|
355079
355286
|
let failingSource;
|
|
355080
355287
|
const stopCapture = async () => {
|
|
@@ -355177,7 +355384,8 @@ async function executeMobileTest(options) {
|
|
|
355177
355384
|
log2(`Warning: Could not retrieve installed apps list. Proceeding with test execution.`);
|
|
355178
355385
|
}
|
|
355179
355386
|
log2(`Starting Appium session for ${options.target.appId} on ${selectedDevice.name}...`);
|
|
355180
|
-
const leasedPortOffset =
|
|
355387
|
+
const leasedPortOffset = acquireControlPortSlot(selectedDevice.id);
|
|
355388
|
+
acquiredSlotDeviceId = selectedDevice.id;
|
|
355181
355389
|
try {
|
|
355182
355390
|
sessionId = await createSessionWithRetry({
|
|
355183
355391
|
appId: options.target.appId,
|
|
@@ -355454,6 +355662,7 @@ async function executeMobileTest(options) {
|
|
|
355454
355662
|
await deleteSession(sessionId);
|
|
355455
355663
|
log2("Session destroyed");
|
|
355456
355664
|
}
|
|
355665
|
+
if (acquiredSlotDeviceId) releaseControlPortSlot(acquiredSlotDeviceId);
|
|
355457
355666
|
}
|
|
355458
355667
|
}
|
|
355459
355668
|
async function createMobileDiscoverySession(target, options) {
|
|
@@ -355499,21 +355708,28 @@ async function createMobileDiscoverySession(target, options) {
|
|
|
355499
355708
|
isPhysical: selectedDevice.isPhysical === true,
|
|
355500
355709
|
platformVersion: selectedDevice.platformVersion
|
|
355501
355710
|
});
|
|
355502
|
-
const
|
|
355503
|
-
|
|
355504
|
-
|
|
355505
|
-
|
|
355506
|
-
|
|
355507
|
-
|
|
355508
|
-
|
|
355509
|
-
|
|
355510
|
-
|
|
355711
|
+
const discoveryPortSlotDeviceId = selectedDevice.id;
|
|
355712
|
+
let sessionId;
|
|
355713
|
+
try {
|
|
355714
|
+
sessionId = await createSessionWithRetry({
|
|
355715
|
+
appId: target.appId,
|
|
355716
|
+
platform: target.platform,
|
|
355717
|
+
deviceId: selectedDevice.id,
|
|
355718
|
+
platformVersion: target.platformVersion ?? selectedDevice.platformVersion,
|
|
355719
|
+
isPhysical: selectedDevice.isPhysical,
|
|
355720
|
+
portOffset: acquireControlPortSlot(discoveryPortSlotDeviceId)
|
|
355721
|
+
});
|
|
355722
|
+
} catch (err) {
|
|
355723
|
+
releaseControlPortSlot(discoveryPortSlotDeviceId);
|
|
355724
|
+
throw err;
|
|
355725
|
+
}
|
|
355511
355726
|
if (target.launchMode === "DEEP_LINK" && target.deeplink) {
|
|
355512
355727
|
try {
|
|
355513
355728
|
await openDeepLink(sessionId, target.platform, target.deeplink, target.appId);
|
|
355514
355729
|
} catch (err) {
|
|
355515
355730
|
await deleteSession(sessionId).catch(() => {
|
|
355516
355731
|
});
|
|
355732
|
+
releaseControlPortSlot(discoveryPortSlotDeviceId);
|
|
355517
355733
|
throw new Error(
|
|
355518
355734
|
`Deep-link launch failed for mobile discovery: ${err instanceof Error ? err.message : String(err)}`
|
|
355519
355735
|
);
|
|
@@ -355535,6 +355751,7 @@ async function createMobileDiscoverySession(target, options) {
|
|
|
355535
355751
|
} catch {
|
|
355536
355752
|
}
|
|
355537
355753
|
await deleteSession(sessionId);
|
|
355754
|
+
releaseControlPortSlot(discoveryPortSlotDeviceId);
|
|
355538
355755
|
}
|
|
355539
355756
|
};
|
|
355540
355757
|
}
|
|
@@ -355552,14 +355769,14 @@ async function readElementAttribute(sessionId, elementId, name) {
|
|
|
355552
355769
|
}
|
|
355553
355770
|
function harvestFallbackXpath(platform3, attrs) {
|
|
355554
355771
|
if (platform3 === "IOS") {
|
|
355555
|
-
if (attrs.name) return `//*[@name=${
|
|
355772
|
+
if (attrs.name) return `//*[@name=${xpathLiteral3(attrs.name)}]`;
|
|
355556
355773
|
const text = attrs.label ?? attrs.value;
|
|
355557
|
-
if (text) return
|
|
355774
|
+
if (text) return identityTextXpath2(text);
|
|
355558
355775
|
return "//*";
|
|
355559
355776
|
}
|
|
355560
|
-
if (attrs.resourceId) return `//*[@resource-id=${
|
|
355561
|
-
if (attrs.text) return
|
|
355562
|
-
if (attrs.contentDesc) return `//*[@content-desc=${
|
|
355777
|
+
if (attrs.resourceId) return `//*[@resource-id=${xpathLiteral3(attrs.resourceId)}]`;
|
|
355778
|
+
if (attrs.text) return identityTextXpath2(attrs.text);
|
|
355779
|
+
if (attrs.contentDesc) return `//*[@content-desc=${xpathLiteral3(attrs.contentDesc)}]`;
|
|
355563
355780
|
return "//*";
|
|
355564
355781
|
}
|
|
355565
355782
|
async function readNativeElementBundle(sessionId, elementId, platform3) {
|
|
@@ -355912,6 +356129,7 @@ var sessionStartInFlight = null;
|
|
|
355912
356129
|
var sessionReaperTimer = null;
|
|
355913
356130
|
var mirrorClientsEmptyAt = null;
|
|
355914
356131
|
var mirrorSessionOwner = null;
|
|
356132
|
+
var actionInFlight = false;
|
|
355915
356133
|
var recordingWorkloadActive = false;
|
|
355916
356134
|
function setRecordingWorkloadActive(active) {
|
|
355917
356135
|
if (recordingWorkloadActive === active) return;
|
|
@@ -356141,7 +356359,11 @@ async function handleHealth(_req, res) {
|
|
|
356141
356359
|
// 409 — it must never be inferred from server-side global run counts, which
|
|
356142
356360
|
// include unrelated projects and runners.
|
|
356143
356361
|
executorBusy: executorBusyCount > 0,
|
|
356144
|
-
|
|
356362
|
+
// TRUE also during the multi-second session-START window: the claim loop
|
|
356363
|
+
// reads this to keep test runs off the recording device, and the Appium
|
|
356364
|
+
// session id only lands AFTER `POST /session` returns — without the
|
|
356365
|
+
// in-flight latch a run admitted in that window fights the recording.
|
|
356366
|
+
recordingActive: !!state.appiumSessionId && mirrorSessionOwner === "recording" || sessionStartInFlight !== null
|
|
356145
356367
|
});
|
|
356146
356368
|
}
|
|
356147
356369
|
function realDeviceCapabilities2(platform3) {
|
|
@@ -356219,7 +356441,11 @@ async function handleDevices(_req, res) {
|
|
|
356219
356441
|
sendJSON(res, 200, {
|
|
356220
356442
|
devices,
|
|
356221
356443
|
executorBusy: executorBusyCount > 0,
|
|
356222
|
-
|
|
356444
|
+
// TRUE also during the multi-second session-START window: the claim loop
|
|
356445
|
+
// reads this to keep test runs off the recording device, and the Appium
|
|
356446
|
+
// session id only lands AFTER `POST /session` returns — without the
|
|
356447
|
+
// in-flight latch a run admitted in that window fights the recording.
|
|
356448
|
+
recordingActive: !!state.appiumSessionId && mirrorSessionOwner === "recording" || sessionStartInFlight !== null
|
|
356223
356449
|
});
|
|
356224
356450
|
}
|
|
356225
356451
|
async function handleSessionStart(req, res) {
|
|
@@ -356308,6 +356534,8 @@ async function startSession(validated, res) {
|
|
|
356308
356534
|
state.platform = targetPlatform === "IOS" ? "IOS" : "ANDROID";
|
|
356309
356535
|
mirrorSessionOwner = "recording";
|
|
356310
356536
|
setRecordingWorkloadActive(true);
|
|
356537
|
+
mirrorClientsEmptyAt = Date.now();
|
|
356538
|
+
ensureSessionReaper();
|
|
356311
356539
|
if (launchMode === "DEEP_LINK" && deeplink) {
|
|
356312
356540
|
try {
|
|
356313
356541
|
const args = targetPlatform === "IOS" ? [{ url: deeplink, bundleId: appId }] : [{ url: deeplink, package: appId }];
|
|
@@ -356661,6 +356889,14 @@ async function handleAction(req, res) {
|
|
|
356661
356889
|
sendJSON(res, 400, { error: "No active Appium session" });
|
|
356662
356890
|
return;
|
|
356663
356891
|
}
|
|
356892
|
+
if (mirrorSessionOwner !== "recording") {
|
|
356893
|
+
sendJSON(res, 409, { error: "No recording session owns the device (a runner-driven session is active)." });
|
|
356894
|
+
return;
|
|
356895
|
+
}
|
|
356896
|
+
if (actionInFlight) {
|
|
356897
|
+
sendJSON(res, 409, { error: "Another action is still executing. Wait for it to finish." });
|
|
356898
|
+
return;
|
|
356899
|
+
}
|
|
356664
356900
|
validateSessionId(state.appiumSessionId);
|
|
356665
356901
|
const body = await parseBody(req);
|
|
356666
356902
|
const type = typeof body.type === "string" ? body.type : null;
|
|
@@ -356672,6 +356908,7 @@ async function handleAction(req, res) {
|
|
|
356672
356908
|
sendJSON(res, 400, { error: "Unknown or unsupported action type" });
|
|
356673
356909
|
return;
|
|
356674
356910
|
}
|
|
356911
|
+
actionInFlight = true;
|
|
356675
356912
|
try {
|
|
356676
356913
|
const sessionPath = `/session/${state.appiumSessionId}`;
|
|
356677
356914
|
switch (type) {
|
|
@@ -356744,15 +356981,23 @@ async function handleAction(req, res) {
|
|
|
356744
356981
|
sendJSON(res, 400, { error: `Unknown action type: ${type}` });
|
|
356745
356982
|
return;
|
|
356746
356983
|
}
|
|
356747
|
-
|
|
356984
|
+
await waitForUiSettle(sessionPath);
|
|
356985
|
+
const screenshot = await getScreenshot2();
|
|
356986
|
+
let source = await getPageSource2();
|
|
356987
|
+
if (source === null) {
|
|
356988
|
+
source = await getPageSource2();
|
|
356989
|
+
}
|
|
356748
356990
|
sendJSON(res, 200, {
|
|
356749
356991
|
ok: true,
|
|
356750
356992
|
screenshot: screenshot ?? null,
|
|
356751
356993
|
source: source ?? null,
|
|
356994
|
+
...source === null || source === void 0 ? { sourceUnavailable: true } : {},
|
|
356752
356995
|
timestamp: Date.now()
|
|
356753
356996
|
});
|
|
356754
356997
|
} catch (err) {
|
|
356755
356998
|
sendJSON(res, 500, { error: `Action failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
356999
|
+
} finally {
|
|
357000
|
+
actionInFlight = false;
|
|
356756
357001
|
}
|
|
356757
357002
|
}
|
|
356758
357003
|
async function handleScreenshot(_req, res) {
|
|
@@ -356990,7 +357235,11 @@ function getBridgeState() {
|
|
|
356990
357235
|
tunnelUrl: state.tunnelUrl,
|
|
356991
357236
|
tunnelToken: state.tunnelToken,
|
|
356992
357237
|
hasActiveSession: !!state.appiumSessionId,
|
|
356993
|
-
|
|
357238
|
+
// TRUE also during the multi-second session-START window: the claim loop
|
|
357239
|
+
// reads this to keep test runs off the recording device, and the Appium
|
|
357240
|
+
// session id only lands AFTER `POST /session` returns — without the
|
|
357241
|
+
// in-flight latch a run admitted in that window fights the recording.
|
|
357242
|
+
recordingActive: !!state.appiumSessionId && mirrorSessionOwner === "recording" || sessionStartInFlight !== null,
|
|
356994
357243
|
platform: state.platform
|
|
356995
357244
|
};
|
|
356996
357245
|
}
|