@validate.qa/runner 1.0.78 → 1.0.80
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 +649 -56
- package/dist/cli.mjs +647 -54
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1888,6 +1888,57 @@ var init_constants = __esm({
|
|
|
1888
1888
|
}
|
|
1889
1889
|
});
|
|
1890
1890
|
|
|
1891
|
+
// ../shared/dist/live-frame.js
|
|
1892
|
+
function isValidLiveFrame(frame) {
|
|
1893
|
+
if (!frame || typeof frame !== "object")
|
|
1894
|
+
return false;
|
|
1895
|
+
const f = frame;
|
|
1896
|
+
if (typeof f.sessionId !== "string" || f.sessionId.length === 0 || f.sessionId.length > 200)
|
|
1897
|
+
return false;
|
|
1898
|
+
if (typeof f.projectId !== "string" || f.projectId.length === 0 || f.projectId.length > 200)
|
|
1899
|
+
return false;
|
|
1900
|
+
if (typeof f.capturedAt !== "number" || !Number.isFinite(f.capturedAt))
|
|
1901
|
+
return false;
|
|
1902
|
+
if (f.mimeType !== "image/jpeg" && f.mimeType !== "image/png")
|
|
1903
|
+
return false;
|
|
1904
|
+
if (typeof f.base64 !== "string" || f.base64.length === 0)
|
|
1905
|
+
return false;
|
|
1906
|
+
if (f.base64.length > LIVE_FRAME_PUSH_MAX_BASE64_BYTES)
|
|
1907
|
+
return false;
|
|
1908
|
+
return true;
|
|
1909
|
+
}
|
|
1910
|
+
var readEnvInt, LIVE_FRAME_PUSH_MAX_BASE64_BYTES, LIVE_THUMBNAIL_MAX_BASE64_BYTES, LIVE_FRAME_MAX_DIMENSION, LIVE_FRAME_JPEG_QUALITY, LIVE_FRAME_MIN_PUSH_INTERVAL_MS, LIVE_FRAME_UNFOCUSED_INTERVAL_MS, LIVE_FRAME_IDLE_PUSH_INTERVAL_MS, LIVE_FRAME_STALE_MS, LIVE_FRAME_MAX_SESSIONS_PER_PROJECT, MOBILE_MJPEG_SETTINGS, MOBILE_MJPEG_FIRST_FRAME_TIMEOUT_MS, MOBILE_MJPEG_MAX_FRAME_BYTES;
|
|
1911
|
+
var init_live_frame = __esm({
|
|
1912
|
+
"../shared/dist/live-frame.js"() {
|
|
1913
|
+
"use strict";
|
|
1914
|
+
readEnvInt = (name, fallback) => {
|
|
1915
|
+
if (typeof process === "undefined")
|
|
1916
|
+
return fallback;
|
|
1917
|
+
const parsed = parseInt(process.env?.[name] ?? "", 10);
|
|
1918
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
1919
|
+
};
|
|
1920
|
+
LIVE_FRAME_PUSH_MAX_BASE64_BYTES = readEnvInt("LIVE_FRAME_PUSH_MAX_BASE64_BYTES", 45e5);
|
|
1921
|
+
LIVE_THUMBNAIL_MAX_BASE64_BYTES = readEnvInt("LIVE_THUMBNAIL_MAX_BASE64_BYTES", 4e5);
|
|
1922
|
+
LIVE_FRAME_MAX_DIMENSION = readEnvInt("LIVE_FRAME_MAX_DIMENSION", 900);
|
|
1923
|
+
LIVE_FRAME_JPEG_QUALITY = readEnvInt("LIVE_FRAME_JPEG_QUALITY", 55);
|
|
1924
|
+
LIVE_FRAME_MIN_PUSH_INTERVAL_MS = readEnvInt("LIVE_FRAME_MIN_PUSH_INTERVAL_MS", 125);
|
|
1925
|
+
LIVE_FRAME_UNFOCUSED_INTERVAL_MS = readEnvInt("LIVE_FRAME_UNFOCUSED_INTERVAL_MS", 2e3);
|
|
1926
|
+
LIVE_FRAME_IDLE_PUSH_INTERVAL_MS = readEnvInt("LIVE_FRAME_IDLE_PUSH_INTERVAL_MS", 5e3);
|
|
1927
|
+
LIVE_FRAME_STALE_MS = readEnvInt("LIVE_FRAME_STALE_MS", 3e4);
|
|
1928
|
+
LIVE_FRAME_MAX_SESSIONS_PER_PROJECT = readEnvInt("LIVE_FRAME_MAX_SESSIONS_PER_PROJECT", 32);
|
|
1929
|
+
MOBILE_MJPEG_SETTINGS = {
|
|
1930
|
+
/** Screenshots per second the broadcaster emits (both drivers: range 1-60). */
|
|
1931
|
+
framerate: readEnvInt("MOBILE_MJPEG_FRAMERATE", 10),
|
|
1932
|
+
/** Downscale percentage. UiAutomator2 defaults to 50, XCUITest to 100. */
|
|
1933
|
+
scalingFactor: readEnvInt("MOBILE_MJPEG_SCALING_FACTOR", 50),
|
|
1934
|
+
/** JPEG compression factor (both drivers: range 1-100). */
|
|
1935
|
+
quality: readEnvInt("MOBILE_MJPEG_QUALITY", 50)
|
|
1936
|
+
};
|
|
1937
|
+
MOBILE_MJPEG_FIRST_FRAME_TIMEOUT_MS = readEnvInt("MOBILE_MJPEG_FIRST_FRAME_TIMEOUT_MS", 6e3);
|
|
1938
|
+
MOBILE_MJPEG_MAX_FRAME_BYTES = readEnvInt("MOBILE_MJPEG_MAX_FRAME_BYTES", 8 * 1024 * 1024);
|
|
1939
|
+
}
|
|
1940
|
+
});
|
|
1941
|
+
|
|
1891
1942
|
// ../shared/dist/project-key.js
|
|
1892
1943
|
function isValidProjectKey(key) {
|
|
1893
1944
|
return typeof key === "string" && PROJECT_KEY_PATTERN.test(key) && !RESERVED_PROJECT_KEYS.has(key);
|
|
@@ -6310,6 +6361,15 @@ __export(dist_exports, {
|
|
|
6310
6361
|
GROUP_LABELS: () => GROUP_LABELS,
|
|
6311
6362
|
LEGACY_TTS_MODEL_MAP: () => LEGACY_TTS_MODEL_MAP,
|
|
6312
6363
|
LEGACY_TTS_VOICE_ID_MAP: () => LEGACY_TTS_VOICE_ID_MAP,
|
|
6364
|
+
LIVE_FRAME_IDLE_PUSH_INTERVAL_MS: () => LIVE_FRAME_IDLE_PUSH_INTERVAL_MS,
|
|
6365
|
+
LIVE_FRAME_JPEG_QUALITY: () => LIVE_FRAME_JPEG_QUALITY,
|
|
6366
|
+
LIVE_FRAME_MAX_DIMENSION: () => LIVE_FRAME_MAX_DIMENSION,
|
|
6367
|
+
LIVE_FRAME_MAX_SESSIONS_PER_PROJECT: () => LIVE_FRAME_MAX_SESSIONS_PER_PROJECT,
|
|
6368
|
+
LIVE_FRAME_MIN_PUSH_INTERVAL_MS: () => LIVE_FRAME_MIN_PUSH_INTERVAL_MS,
|
|
6369
|
+
LIVE_FRAME_PUSH_MAX_BASE64_BYTES: () => LIVE_FRAME_PUSH_MAX_BASE64_BYTES,
|
|
6370
|
+
LIVE_FRAME_STALE_MS: () => LIVE_FRAME_STALE_MS,
|
|
6371
|
+
LIVE_FRAME_UNFOCUSED_INTERVAL_MS: () => LIVE_FRAME_UNFOCUSED_INTERVAL_MS,
|
|
6372
|
+
LIVE_THUMBNAIL_MAX_BASE64_BYTES: () => LIVE_THUMBNAIL_MAX_BASE64_BYTES,
|
|
6313
6373
|
MAX_CREDIT_TOPUP_CENTS: () => MAX_CREDIT_TOPUP_CENTS,
|
|
6314
6374
|
MAX_DEEPLINK_LENGTH: () => MAX_DEEPLINK_LENGTH,
|
|
6315
6375
|
MIN_CREDIT_TOPUP_CENTS: () => MIN_CREDIT_TOPUP_CENTS,
|
|
@@ -6321,6 +6381,9 @@ __export(dist_exports, {
|
|
|
6321
6381
|
MOBILE_DEFAULT_SWIPE_DISTANCE: () => MOBILE_DEFAULT_SWIPE_DISTANCE,
|
|
6322
6382
|
MOBILE_FOCUS_SETTLE_MS: () => MOBILE_FOCUS_SETTLE_MS,
|
|
6323
6383
|
MOBILE_MAX_SWIPE_DISTANCE: () => MOBILE_MAX_SWIPE_DISTANCE,
|
|
6384
|
+
MOBILE_MJPEG_FIRST_FRAME_TIMEOUT_MS: () => MOBILE_MJPEG_FIRST_FRAME_TIMEOUT_MS,
|
|
6385
|
+
MOBILE_MJPEG_MAX_FRAME_BYTES: () => MOBILE_MJPEG_MAX_FRAME_BYTES,
|
|
6386
|
+
MOBILE_MJPEG_SETTINGS: () => MOBILE_MJPEG_SETTINGS,
|
|
6324
6387
|
MOBILE_PLATFORMS: () => MOBILE_PLATFORMS,
|
|
6325
6388
|
MOBILE_POST_ACTION_SETTLE_MS: () => MOBILE_POST_ACTION_SETTLE_MS,
|
|
6326
6389
|
MOBILE_SCREENSHOT_INTERVAL_MS: () => MOBILE_SCREENSHOT_INTERVAL_MS,
|
|
@@ -6433,6 +6496,7 @@ __export(dist_exports, {
|
|
|
6433
6496
|
isTestReadyForPlatform: () => isTestReadyForPlatform,
|
|
6434
6497
|
isTrialExpired: () => isTrialExpired,
|
|
6435
6498
|
isUIVariant: () => isUIVariant,
|
|
6499
|
+
isValidLiveFrame: () => isValidLiveFrame,
|
|
6436
6500
|
isValidMobileAppId: () => isValidMobileAppId,
|
|
6437
6501
|
isValidProjectKey: () => isValidProjectKey,
|
|
6438
6502
|
isValidSttProvider: () => isValidSttProvider,
|
|
@@ -6495,6 +6559,7 @@ var init_dist = __esm({
|
|
|
6495
6559
|
"use strict";
|
|
6496
6560
|
init_types();
|
|
6497
6561
|
init_constants();
|
|
6562
|
+
init_live_frame();
|
|
6498
6563
|
init_project_key();
|
|
6499
6564
|
init_mobile_test_generation();
|
|
6500
6565
|
init_mobile_step_healer();
|
|
@@ -87957,6 +88022,7 @@ var require_playwright_native = __commonJS({
|
|
|
87957
88022
|
var PACKAGE_DIR = (0, path_1.resolve)(__dirname, "..", "..");
|
|
87958
88023
|
var RUNS_DIR = process.env.RUNS_DIR || (0, path_1.join)(PACKAGE_DIR, ".qa-runs");
|
|
87959
88024
|
var PW_CLI = require.resolve("@playwright/test/cli");
|
|
88025
|
+
var LIVE_BROWSER_STATE_POLL_INTERVAL_MS = 500;
|
|
87960
88026
|
async function executeTestViaNativePlaywright2(options) {
|
|
87961
88027
|
const { playwrightCode, baseUrl, testName, testVariables, authStateCapturePath, storageStatePath, extraHttpHeaders, authStandingMode, validateContext, liveBrowserHandle, onLiveBrowserSnapshot } = options;
|
|
87962
88028
|
const logs2 = [];
|
|
@@ -88046,7 +88112,7 @@ test.beforeEach(async ({ page }, testInfo) => {
|
|
|
88046
88112
|
await __qaLiveBrowserCapture();
|
|
88047
88113
|
__qaLiveBrowserInterval = setInterval(() => {
|
|
88048
88114
|
void __qaLiveBrowserCapture?.();
|
|
88049
|
-
},
|
|
88115
|
+
}, ${LIVE_BROWSER_STATE_POLL_INTERVAL_MS});
|
|
88050
88116
|
});
|
|
88051
88117
|
|
|
88052
88118
|
test.afterEach(async () => {
|
|
@@ -88463,7 +88529,7 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
|
|
|
88463
88529
|
};
|
|
88464
88530
|
const interval = setInterval(() => {
|
|
88465
88531
|
void poll();
|
|
88466
|
-
},
|
|
88532
|
+
}, LIVE_BROWSER_STATE_POLL_INTERVAL_MS);
|
|
88467
88533
|
interval.unref?.();
|
|
88468
88534
|
void poll();
|
|
88469
88535
|
return async () => {
|
|
@@ -320987,6 +321053,7 @@ var require_primary_action_ledger = __commonJS({
|
|
|
320987
321053
|
exports2.isLedgerActive = isLedgerActive;
|
|
320988
321054
|
exports2.isGateEnforcing = isGateEnforcing;
|
|
320989
321055
|
exports2.isSteeringActive = isSteeringActive;
|
|
321056
|
+
exports2.isPrimaryActionCoverageGapEnabled = isPrimaryActionCoverageGapEnabled;
|
|
320990
321057
|
exports2.leadingVerb = leadingVerb;
|
|
320991
321058
|
exports2.classifyPrimaryAction = classifyPrimaryAction;
|
|
320992
321059
|
exports2.containerScopeOf = containerScopeOf;
|
|
@@ -321001,6 +321068,8 @@ var require_primary_action_ledger = __commonJS({
|
|
|
321001
321068
|
exports2.classifyActionOutcome = classifyActionOutcome;
|
|
321002
321069
|
exports2.detectPrimaryActions = detectPrimaryActions;
|
|
321003
321070
|
exports2.summarizeLedger = summarizeLedger;
|
|
321071
|
+
exports2.summarizeUnexercisedPrimaries = summarizeUnexercisedPrimaries;
|
|
321072
|
+
exports2.buildUnexercisedPrimaryCoverageGaps = buildUnexercisedPrimaryCoverageGaps;
|
|
321004
321073
|
var reveal_diff_js_1 = require_reveal_diff();
|
|
321005
321074
|
var exploration_parser_js_1 = require_exploration_parser();
|
|
321006
321075
|
function getPrimaryActionGateMode(env = process.env) {
|
|
@@ -321020,6 +321089,10 @@ var require_primary_action_ledger = __commonJS({
|
|
|
321020
321089
|
function isSteeringActive(mode) {
|
|
321021
321090
|
return mode === "advisory" || mode === "enforce";
|
|
321022
321091
|
}
|
|
321092
|
+
function isPrimaryActionCoverageGapEnabled(env = process.env) {
|
|
321093
|
+
const raw = (env.DISCOVERY_PRIMARY_ACTION_COVERAGE_GAP ?? "").trim().toLowerCase();
|
|
321094
|
+
return raw === "1" || raw === "true" || raw === "on" || raw === "enabled";
|
|
321095
|
+
}
|
|
321023
321096
|
exports2.PRIMARY_ACTION_VERB_RE = /^(run|analyze|analyse|generate|regenerate|process|submit|save|create|send|upload|search|export|import|calculate|recalculate|compute|convert|start|scan|translate|transcribe|render|publish|sync|deploy|enrich|summar(?:ize|ise))\b/i;
|
|
321024
321097
|
exports2.AMBIGUOUS_ACTION_VERB_RE = /^(continue|next|apply|go|proceed|confirm|update|add|edit|ok|done|finish|complete|approve|enable|activate)\b/i;
|
|
321025
321098
|
exports2.DESTRUCTIVE_ACTION_VERB_RE = /^(delete|remove|archive|reset|disconnect|deactivate|destroy|purge|revoke|discard|drop|wipe|erase|unsubscribe|cancel|clear|trash)\b/i;
|
|
@@ -321283,6 +321356,47 @@ var require_primary_action_ledger = __commonJS({
|
|
|
321283
321356
|
}
|
|
321284
321357
|
return `actions=${c.total} primary{detected=${c.detected} attempted=${c.attempted} captured=${c.captured} blocked=${c.blocked} exempt=${c.exempt}} ambiguous=${c.ambiguous}`;
|
|
321285
321358
|
}
|
|
321359
|
+
function summarizeUnexercisedPrimaries(ledger, routeInScope, cap = 12) {
|
|
321360
|
+
const out = [];
|
|
321361
|
+
for (const e of getBlockingActionDebt(ledger, routeInScope)) {
|
|
321362
|
+
if (e.status !== "DETECTED" && e.status !== "ATTEMPTED")
|
|
321363
|
+
continue;
|
|
321364
|
+
out.push({ label: e.label, route: e.route, containerScope: e.containerScope, status: e.status });
|
|
321365
|
+
if (out.length >= cap)
|
|
321366
|
+
break;
|
|
321367
|
+
}
|
|
321368
|
+
return out;
|
|
321369
|
+
}
|
|
321370
|
+
var REGEXP_META_RE = /[.*+?^${}()|[\]\\]/g;
|
|
321371
|
+
function labelCoveredByPlan(normLabel, plannedLower) {
|
|
321372
|
+
if (!normLabel.includes(" "))
|
|
321373
|
+
return false;
|
|
321374
|
+
const escaped = normLabel.replace(REGEXP_META_RE, "\\$&");
|
|
321375
|
+
return new RegExp(`(^|\\W)${escaped}($|\\W)`).test(plannedLower);
|
|
321376
|
+
}
|
|
321377
|
+
function buildUnexercisedPrimaryCoverageGaps(unexercised, plannedScenarioText, nowIso) {
|
|
321378
|
+
const gaps = [];
|
|
321379
|
+
const seen = /* @__PURE__ */ new Set();
|
|
321380
|
+
const planned = plannedScenarioText.toLowerCase();
|
|
321381
|
+
for (const a of unexercised) {
|
|
321382
|
+
const norm = normLabelKey(a.label);
|
|
321383
|
+
const key = `${norm}|${a.route}`;
|
|
321384
|
+
if (seen.has(key))
|
|
321385
|
+
continue;
|
|
321386
|
+
seen.add(key);
|
|
321387
|
+
if (labelCoveredByPlan(norm, planned))
|
|
321388
|
+
continue;
|
|
321389
|
+
const clickedNote = a.status === "ATTEMPTED" ? "clicked, but no result state was captured" : "never clicked";
|
|
321390
|
+
gaps.push({
|
|
321391
|
+
scenarioName: `Exercise "${a.label}"`,
|
|
321392
|
+
reason: `Primary action "${a.label}" was detected on ${a.route} but never exercised to an observed result (${clickedNote}).`,
|
|
321393
|
+
suggestion: `Exercise "${a.label}" to a captured result \u2014 open its result modal/panel and capture it BEFORE closing \u2014 or schedule a deep run scoped to this action, so a real test can be authored.`,
|
|
321394
|
+
unmetPrerequisites: [],
|
|
321395
|
+
detectedAt: nowIso
|
|
321396
|
+
});
|
|
321397
|
+
}
|
|
321398
|
+
return gaps;
|
|
321399
|
+
}
|
|
321286
321400
|
}
|
|
321287
321401
|
});
|
|
321288
321402
|
|
|
@@ -322771,6 +322885,9 @@ var require_exploration_prompt_builder = __commonJS({
|
|
|
322771
322885
|
"These are the feature's core actions \u2014 what it EXISTS to do. You are on a PROVISIONED TEST ACCOUNT, so exercising them is the point of this exploration. For each: click it, wait for it to finish (an async job can take 10-60s \u2014 keep waiting and capturing, do not give up early), then capture_page the result (a new panel, row, score, status, or route change).",
|
|
322772
322886
|
`Spending in-app COMPUTE credits is expected and budgeted \u2014 running an Analyze / Generate / Run / Process ONCE to observe its output is exactly what you should do; an "N of M used" usage or AI-credit meter is NOT a reason to skip. These were auto-screened, but YOU are the final check: if you can see a specific listed action actually charges REAL money at a checkout (Buy / Upgrade / Subscribe), makes an irreversible or destructive change (Delete / Wipe / Deploy to production), or sends a message to real people (Send email / Invite), SKIP just that one \u2014 note it as blocked with the reason and move on. Don't finish until every other action below has produced an observed outcome:`
|
|
322773
322887
|
];
|
|
322888
|
+
if ((0, primary_action_ledger_js_1.isPrimaryActionCoverageGapEnabled)()) {
|
|
322889
|
+
out.push("If the result opens in a modal, dialog, or panel, call capture_page WHILE IT IS STILL OPEN \u2014 do not close or dismiss it first, or the result state is never recorded and the action does not count as covered. An action reachable only from inside this feature's own result modal is in-scope: exercise it to a captured result before closing, even if it generates content that belongs to another builder.");
|
|
322890
|
+
}
|
|
322774
322891
|
for (const e of debt.slice(0, 8)) {
|
|
322775
322892
|
const note = e.status === "ATTEMPTED" ? " \u2014 clicked but no result captured yet" : "";
|
|
322776
322893
|
out.push(` - "${e.label}" on ${e.route}${note}`);
|
|
@@ -326591,6 +326708,7 @@ ${inbox_tools_js_1.EMAIL_VERIFICATION_UNAVAILABLE_OVERRIDE_BLOCK}` : ""}`;
|
|
|
326591
326708
|
const primaryActionMode = (0, primary_action_ledger_js_1.getPrimaryActionGateMode)();
|
|
326592
326709
|
const primaryActionLedgerActive = (0, primary_action_ledger_js_1.isLedgerActive)(primaryActionMode) && options?.mode === "deep";
|
|
326593
326710
|
const primaryActionEnforcing = primaryActionLedgerActive && (0, primary_action_ledger_js_1.isGateEnforcing)(primaryActionMode);
|
|
326711
|
+
const primaryActionCoverageGapEnabled = primaryActionLedgerActive && (0, primary_action_ledger_js_1.isPrimaryActionCoverageGapEnabled)();
|
|
326594
326712
|
const prevRouteSignatures = /* @__PURE__ */ new Map();
|
|
326595
326713
|
const prevInteractionCountsByRoute = /* @__PURE__ */ new Map();
|
|
326596
326714
|
let previousCaptureRoute = null;
|
|
@@ -326601,6 +326719,16 @@ ${inbox_tools_js_1.EMAIL_VERIFICATION_UNAVAILABLE_OVERRIDE_BLOCK}` : ""}`;
|
|
|
326601
326719
|
let finishBounce = (0, survey_link_frontier_js_1.initialFinishBounceState)();
|
|
326602
326720
|
let lastLandedPageUrl = null;
|
|
326603
326721
|
const state2 = (0, exploration_state_js_1.createExplorationState)();
|
|
326722
|
+
const buildUnexercisedPrimarySummary = () => {
|
|
326723
|
+
if (!primaryActionCoverageGapEnabled)
|
|
326724
|
+
return void 0;
|
|
326725
|
+
const fa = options?.focusAreas ?? [];
|
|
326726
|
+
if (fa.length === 0)
|
|
326727
|
+
return void 0;
|
|
326728
|
+
const otherRoutes = options?.featureContext?.otherFeatures?.flatMap((f) => f.pages);
|
|
326729
|
+
const summary2 = (0, primary_action_ledger_js_1.summarizeUnexercisedPrimaries)(state2.actionLedger, (r) => (0, exploration_parser_js_1.routeIsWithinFocusAreas)(r, fa, otherRoutes), 12);
|
|
326730
|
+
return summary2.length > 0 ? summary2 : void 0;
|
|
326731
|
+
};
|
|
326604
326732
|
function ingestLinksFromText(rawText) {
|
|
326605
326733
|
if (!linkFrontierEnabled || !rawText || !rawText.includes("- /url:"))
|
|
326606
326734
|
return;
|
|
@@ -328831,6 +328959,7 @@ Exploration ended after ${iteration} iterations without calling finish_explorati
|
|
|
328831
328959
|
summary: state2.summary || summary,
|
|
328832
328960
|
collectedPages: state2.collectedPages,
|
|
328833
328961
|
collectedTransitions: state2.collectedTransitions,
|
|
328962
|
+
unexercisedPrimaryActions: buildUnexercisedPrimarySummary(),
|
|
328834
328963
|
iterationTimeline: iterationTimeline.length > 0 ? iterationTimeline : void 0
|
|
328835
328964
|
};
|
|
328836
328965
|
}
|
|
@@ -328925,6 +329054,7 @@ Fatal error: ${msg}`);
|
|
|
328925
329054
|
summary: `Fatal error: ${msg}`,
|
|
328926
329055
|
collectedPages: state2.collectedPages,
|
|
328927
329056
|
collectedTransitions: state2.collectedTransitions,
|
|
329057
|
+
unexercisedPrimaryActions: buildUnexercisedPrimarySummary(),
|
|
328928
329058
|
iterationTimeline: iterationTimeline.length > 0 ? iterationTimeline : void 0,
|
|
328929
329059
|
truncated: true,
|
|
328930
329060
|
truncatedReason,
|
|
@@ -337111,6 +337241,7 @@ var require_discover_agent = __commonJS({
|
|
|
337111
337241
|
var discover_planner_js_1 = require_discover_planner();
|
|
337112
337242
|
var discover_generator_js_1 = require_discover_generator();
|
|
337113
337243
|
var discover_ai_planner_js_1 = require_discover_ai_planner();
|
|
337244
|
+
var primary_action_ledger_js_1 = require_primary_action_ledger();
|
|
337114
337245
|
var discovery_lint_flags_js_1 = require_discovery_lint_flags();
|
|
337115
337246
|
var verification_inbox_js_1 = require_verification_inbox();
|
|
337116
337247
|
var inbox_tools_js_1 = require_inbox_tools();
|
|
@@ -338413,6 +338544,16 @@ Plan: ${planResult.scenarios.length} scenarios (budget: ${featureTestBudget})`);
|
|
|
338413
338544
|
checkpointFallbackReason = planResult.fallbackReason;
|
|
338414
338545
|
plannerWarning = planResult.warning;
|
|
338415
338546
|
plannerCoverageGaps = planResult.coverageGaps;
|
|
338547
|
+
const unexercisedPrimaries = exploreResult.unexercisedPrimaryActions ?? [];
|
|
338548
|
+
if (unexercisedPrimaries.length > 0) {
|
|
338549
|
+
const plannedText = planResult.scenarios.map((s) => `${s.name} ${s.goal ?? ""} ${(s.targetPages ?? []).join(" ")}`).join(" ");
|
|
338550
|
+
const ledgerGaps = (0, primary_action_ledger_js_1.buildUnexercisedPrimaryCoverageGaps)(unexercisedPrimaries, plannedText, (/* @__PURE__ */ new Date()).toISOString());
|
|
338551
|
+
if (ledgerGaps.length > 0) {
|
|
338552
|
+
plannerCoverageGaps = [...plannerCoverageGaps ?? [], ...ledgerGaps];
|
|
338553
|
+
}
|
|
338554
|
+
const suppressed = unexercisedPrimaries.length - ledgerGaps.length;
|
|
338555
|
+
logs2.push(` [primary-action-ledger] ${unexercisedPrimaries.length} in-scope primary action(s) un-exercised; surfaced ${ledgerGaps.length} as coverage gap(s)${suppressed > 0 ? ` (${suppressed} already covered by the plan / deduped)` : ""}`);
|
|
338556
|
+
}
|
|
338416
338557
|
}
|
|
338417
338558
|
if (callbackOptions?.onPlanReady) {
|
|
338418
338559
|
try {
|
|
@@ -352104,16 +352245,16 @@ var require_live_browser_registry = __commonJS({
|
|
|
352104
352245
|
"use strict";
|
|
352105
352246
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
352106
352247
|
exports2.liveBrowserRegistry = void 0;
|
|
352248
|
+
var shared_1 = (init_dist(), __toCommonJS(dist_exports));
|
|
352107
352249
|
var THUMBNAIL_INTERVAL_MS = parseInt(process.env.LIVE_BROWSER_THUMBNAIL_INTERVAL_MS ?? "4000", 10) || 4e3;
|
|
352108
352250
|
var THUMBNAIL_QUALITY = parseInt(process.env.LIVE_BROWSER_THUMBNAIL_QUALITY ?? "60", 10) || 60;
|
|
352109
|
-
var BROWSER_THUMBNAIL_MAX_BASE64_BYTES = parseInt(process.env.LIVE_BROWSER_THUMBNAIL_MAX_BASE64_BYTES ?? "400000", 10) || 4e5;
|
|
352110
|
-
var MOBILE_THUMBNAIL_MAX_BASE64_BYTES = parseInt(process.env.LIVE_MOBILE_THUMBNAIL_MAX_BASE64_BYTES ?? "4500000", 10) || 45e5;
|
|
352111
352251
|
var STALE_SESSION_MS = parseInt(process.env.LIVE_BROWSER_STALE_MS ?? `${10 * 60 * 1e3}`, 10) || 10 * 60 * 1e3;
|
|
352112
352252
|
var MAX_SESSIONS = parseInt(process.env.LIVE_BROWSER_MAX_SESSIONS ?? "32", 10) || 32;
|
|
352113
352253
|
var LiveBrowserRegistry = class {
|
|
352114
352254
|
entries = /* @__PURE__ */ new Map();
|
|
352115
352255
|
snapshotTimer = null;
|
|
352116
352256
|
capturing = false;
|
|
352257
|
+
publisher = null;
|
|
352117
352258
|
/** Register a new session. Returns a handle the caller must hold and
|
|
352118
352259
|
* ultimately call .unregister() on. */
|
|
352119
352260
|
register(meta) {
|
|
@@ -352127,7 +352268,10 @@ var require_live_browser_registry = __commonJS({
|
|
|
352127
352268
|
lastUpdatedAt: now,
|
|
352128
352269
|
page: null,
|
|
352129
352270
|
isCapturing: false,
|
|
352130
|
-
pendingCapture: false
|
|
352271
|
+
pendingCapture: false,
|
|
352272
|
+
screencast: null,
|
|
352273
|
+
screencastStarting: false,
|
|
352274
|
+
lastPublishedAt: 0
|
|
352131
352275
|
};
|
|
352132
352276
|
this.entries.set(meta.sessionId, entry);
|
|
352133
352277
|
this.ensureSnapshotLoop();
|
|
@@ -352140,19 +352284,40 @@ var require_live_browser_registry = __commonJS({
|
|
|
352140
352284
|
unregister: () => this.unregister(meta.sessionId)
|
|
352141
352285
|
};
|
|
352142
352286
|
}
|
|
352287
|
+
/**
|
|
352288
|
+
* Install the real-time frame sink. Idempotent; the runner calls this once at
|
|
352289
|
+
* startup, before any session registers.
|
|
352290
|
+
*/
|
|
352291
|
+
setFramePublisher(publisher) {
|
|
352292
|
+
this.publisher = publisher;
|
|
352293
|
+
}
|
|
352143
352294
|
unregister(sessionId) {
|
|
352144
352295
|
const entry = this.entries.get(sessionId);
|
|
352145
352296
|
if (!entry)
|
|
352146
352297
|
return;
|
|
352147
|
-
|
|
352148
|
-
this.entries.delete(sessionId);
|
|
352298
|
+
this.dropEntry(sessionId, entry);
|
|
352149
352299
|
if (this.entries.size === 0)
|
|
352150
352300
|
this.stopSnapshotLoop();
|
|
352151
352301
|
}
|
|
352302
|
+
/**
|
|
352303
|
+
* The ONLY way an entry leaves `entries`. Centralised because a screencast
|
|
352304
|
+
* outlives the map otherwise: its frame handler closes over the entry rather
|
|
352305
|
+
* than looking it up, so an entry deleted without stopScreencast() leaves
|
|
352306
|
+
* Chromium encoding and acking frames for a page nobody is watching, holding
|
|
352307
|
+
* the CDP session and Page alive. Eviction and the staleness sweep both used
|
|
352308
|
+
* to delete directly and leaked exactly that way.
|
|
352309
|
+
*/
|
|
352310
|
+
dropEntry(sessionId, entry) {
|
|
352311
|
+
void this.stopScreencast(entry);
|
|
352312
|
+
entry.page = null;
|
|
352313
|
+
this.entries.delete(sessionId);
|
|
352314
|
+
}
|
|
352152
352315
|
setPage(sessionId, page) {
|
|
352153
352316
|
const entry = this.entries.get(sessionId);
|
|
352154
352317
|
if (!entry)
|
|
352155
352318
|
return;
|
|
352319
|
+
if (entry.screencast && entry.screencast.page !== page)
|
|
352320
|
+
void this.stopScreencast(entry);
|
|
352156
352321
|
entry.page = page;
|
|
352157
352322
|
entry.lastUpdatedAt = Date.now();
|
|
352158
352323
|
if (page) {
|
|
@@ -352162,6 +352327,119 @@ var require_live_browser_registry = __commonJS({
|
|
|
352162
352327
|
entry.currentUrl = url;
|
|
352163
352328
|
} catch {
|
|
352164
352329
|
}
|
|
352330
|
+
void this.startScreencast(entry, page);
|
|
352331
|
+
}
|
|
352332
|
+
}
|
|
352333
|
+
/**
|
|
352334
|
+
* Begin a CDP screencast for this page. Chromium then pushes a JPEG on every
|
|
352335
|
+
* paint, already downscaled to LIVE_FRAME_MAX_DIMENSION — which is the only
|
|
352336
|
+
* way this runner can downscale at all, since it ships no image library.
|
|
352337
|
+
*
|
|
352338
|
+
* Best-effort by design: any failure leaves `screencast` null and the polling
|
|
352339
|
+
* screenshot loop keeps serving the entry exactly as before.
|
|
352340
|
+
*/
|
|
352341
|
+
async startScreencast(entry, page) {
|
|
352342
|
+
if (!page || entry.screencast || entry.screencastStarting)
|
|
352343
|
+
return;
|
|
352344
|
+
if (page.isClosed?.())
|
|
352345
|
+
return;
|
|
352346
|
+
entry.screencastStarting = true;
|
|
352347
|
+
try {
|
|
352348
|
+
const client = await page.context().newCDPSession(page);
|
|
352349
|
+
const binding = { client, page, stopped: false };
|
|
352350
|
+
client.on("Page.screencastFrame", (params) => {
|
|
352351
|
+
void client.send("Page.screencastFrameAck", { sessionId: params.sessionId }).catch(() => {
|
|
352352
|
+
});
|
|
352353
|
+
if (binding.stopped)
|
|
352354
|
+
return;
|
|
352355
|
+
if (this.entries.get(entry.meta.sessionId) !== entry)
|
|
352356
|
+
return;
|
|
352357
|
+
if (typeof params.data !== "string" || params.data.length === 0)
|
|
352358
|
+
return;
|
|
352359
|
+
const width = params.metadata?.deviceWidth;
|
|
352360
|
+
const height = params.metadata?.deviceHeight;
|
|
352361
|
+
this.acceptFrame(entry, params.data, "image/jpeg", width, height);
|
|
352362
|
+
});
|
|
352363
|
+
page.once("close", () => {
|
|
352364
|
+
void this.stopScreencast(entry);
|
|
352365
|
+
});
|
|
352366
|
+
page.once("crash", () => {
|
|
352367
|
+
void this.stopScreencast(entry);
|
|
352368
|
+
});
|
|
352369
|
+
await client.send("Page.enable");
|
|
352370
|
+
await client.send("Page.startScreencast", {
|
|
352371
|
+
format: "jpeg",
|
|
352372
|
+
quality: shared_1.LIVE_FRAME_JPEG_QUALITY,
|
|
352373
|
+
// Absolute bounds, aspect-preserving — Chromium does the resize.
|
|
352374
|
+
maxWidth: shared_1.LIVE_FRAME_MAX_DIMENSION,
|
|
352375
|
+
maxHeight: shared_1.LIVE_FRAME_MAX_DIMENSION,
|
|
352376
|
+
everyNthFrame: 1
|
|
352377
|
+
});
|
|
352378
|
+
if (this.entries.get(entry.meta.sessionId) !== entry || entry.page !== page) {
|
|
352379
|
+
binding.stopped = true;
|
|
352380
|
+
await client.send("Page.stopScreencast").catch(() => {
|
|
352381
|
+
});
|
|
352382
|
+
await client.detach().catch(() => {
|
|
352383
|
+
});
|
|
352384
|
+
return;
|
|
352385
|
+
}
|
|
352386
|
+
entry.screencast = binding;
|
|
352387
|
+
} catch {
|
|
352388
|
+
} finally {
|
|
352389
|
+
entry.screencastStarting = false;
|
|
352390
|
+
}
|
|
352391
|
+
}
|
|
352392
|
+
async stopScreencast(entry) {
|
|
352393
|
+
const binding = entry.screencast;
|
|
352394
|
+
if (!binding || binding.stopped)
|
|
352395
|
+
return;
|
|
352396
|
+
binding.stopped = true;
|
|
352397
|
+
entry.screencast = null;
|
|
352398
|
+
try {
|
|
352399
|
+
await binding.client.send("Page.stopScreencast");
|
|
352400
|
+
} catch {
|
|
352401
|
+
}
|
|
352402
|
+
try {
|
|
352403
|
+
await binding.client.detach();
|
|
352404
|
+
} catch {
|
|
352405
|
+
}
|
|
352406
|
+
}
|
|
352407
|
+
/**
|
|
352408
|
+
* Record a freshly captured frame and forward it to the real-time publisher.
|
|
352409
|
+
* The stored copy (used by the heartbeat fallback) is only replaced when it
|
|
352410
|
+
* fits the heartbeat budget, but the publisher — which has a far larger budget
|
|
352411
|
+
* of its own — always sees the frame.
|
|
352412
|
+
*/
|
|
352413
|
+
acceptFrame(entry, base64, mimeType, width, height) {
|
|
352414
|
+
const now = Date.now();
|
|
352415
|
+
entry.lastUpdatedAt = now;
|
|
352416
|
+
if (base64.length <= shared_1.LIVE_THUMBNAIL_MAX_BASE64_BYTES) {
|
|
352417
|
+
entry.thumbnailJpegBase64 = base64;
|
|
352418
|
+
entry.thumbnailCapturedAt = now;
|
|
352419
|
+
if (width !== void 0)
|
|
352420
|
+
entry.thumbnailWidth = width;
|
|
352421
|
+
if (height !== void 0)
|
|
352422
|
+
entry.thumbnailHeight = height;
|
|
352423
|
+
}
|
|
352424
|
+
const publisher = this.publisher;
|
|
352425
|
+
if (!publisher)
|
|
352426
|
+
return;
|
|
352427
|
+
if (now - entry.lastPublishedAt < shared_1.LIVE_FRAME_MIN_PUSH_INTERVAL_MS)
|
|
352428
|
+
return;
|
|
352429
|
+
entry.lastPublishedAt = now;
|
|
352430
|
+
try {
|
|
352431
|
+
publisher({
|
|
352432
|
+
sessionId: entry.meta.sessionId,
|
|
352433
|
+
projectId: entry.meta.projectId,
|
|
352434
|
+
runId: entry.meta.runId,
|
|
352435
|
+
capturedAt: now,
|
|
352436
|
+
mimeType,
|
|
352437
|
+
width,
|
|
352438
|
+
height,
|
|
352439
|
+
surface: entry.meta.surface,
|
|
352440
|
+
base64
|
|
352441
|
+
});
|
|
352442
|
+
} catch {
|
|
352165
352443
|
}
|
|
352166
352444
|
}
|
|
352167
352445
|
setCurrentUrl(sessionId, url) {
|
|
@@ -352173,7 +352451,7 @@ var require_live_browser_registry = __commonJS({
|
|
|
352173
352451
|
const previousUrl = entry.currentUrl;
|
|
352174
352452
|
entry.currentUrl = url;
|
|
352175
352453
|
entry.lastUpdatedAt = Date.now();
|
|
352176
|
-
if (url && previousUrl && url !== previousUrl) {
|
|
352454
|
+
if (url && previousUrl && url !== previousUrl && !entry.screencast) {
|
|
352177
352455
|
entry.pendingCapture = true;
|
|
352178
352456
|
const schedule = (delay) => {
|
|
352179
352457
|
setTimeout(() => {
|
|
@@ -352194,6 +352472,11 @@ var require_live_browser_registry = __commonJS({
|
|
|
352194
352472
|
schedule(100);
|
|
352195
352473
|
}
|
|
352196
352474
|
}
|
|
352475
|
+
/**
|
|
352476
|
+
* Ingest a frame captured outside this module — the mobile mirror and the
|
|
352477
|
+
* spawned `playwright test` child (whose Page lives in another process, so no
|
|
352478
|
+
* CDP screencast is reachable from here) both arrive this way.
|
|
352479
|
+
*/
|
|
352197
352480
|
updateSnapshot(sessionId, snapshot) {
|
|
352198
352481
|
const entry = this.entries.get(sessionId);
|
|
352199
352482
|
if (!entry)
|
|
@@ -352202,12 +352485,10 @@ var require_live_browser_registry = __commonJS({
|
|
|
352202
352485
|
entry.currentUrl = snapshot.currentUrl;
|
|
352203
352486
|
}
|
|
352204
352487
|
const thumbnail = snapshot.thumbnailJpegBase64;
|
|
352205
|
-
|
|
352206
|
-
|
|
352207
|
-
|
|
352208
|
-
entry
|
|
352209
|
-
entry.thumbnailWidth = typeof snapshot.thumbnailWidth === "number" && Number.isFinite(snapshot.thumbnailWidth) ? snapshot.thumbnailWidth : void 0;
|
|
352210
|
-
entry.thumbnailHeight = typeof snapshot.thumbnailHeight === "number" && Number.isFinite(snapshot.thumbnailHeight) ? snapshot.thumbnailHeight : void 0;
|
|
352488
|
+
if (thumbnail) {
|
|
352489
|
+
const width = typeof snapshot.thumbnailWidth === "number" && Number.isFinite(snapshot.thumbnailWidth) ? snapshot.thumbnailWidth : void 0;
|
|
352490
|
+
const height = typeof snapshot.thumbnailHeight === "number" && Number.isFinite(snapshot.thumbnailHeight) ? snapshot.thumbnailHeight : void 0;
|
|
352491
|
+
this.acceptFrame(entry, thumbnail, snapshot.mimeType ?? "image/jpeg", width, height);
|
|
352211
352492
|
}
|
|
352212
352493
|
entry.lastUpdatedAt = Date.now();
|
|
352213
352494
|
}
|
|
@@ -352250,9 +352531,11 @@ var require_live_browser_registry = __commonJS({
|
|
|
352250
352531
|
async captureAllNow() {
|
|
352251
352532
|
await this.captureAll();
|
|
352252
352533
|
}
|
|
352253
|
-
/** Stop the snapshot loop and clear all entries. Idempotent. */
|
|
352534
|
+
/** Stop the snapshot loop, tear down every screencast, and clear all entries. Idempotent. */
|
|
352254
352535
|
shutdown() {
|
|
352255
352536
|
this.stopSnapshotLoop();
|
|
352537
|
+
for (const entry of this.entries.values())
|
|
352538
|
+
void this.stopScreencast(entry);
|
|
352256
352539
|
this.entries.clear();
|
|
352257
352540
|
}
|
|
352258
352541
|
ensureSnapshotLoop() {
|
|
@@ -352280,15 +352563,13 @@ var require_live_browser_registry = __commonJS({
|
|
|
352280
352563
|
}
|
|
352281
352564
|
}
|
|
352282
352565
|
if (oldestKey) {
|
|
352283
|
-
this.entries.get(oldestKey)
|
|
352284
|
-
this.entries.delete(oldestKey);
|
|
352566
|
+
this.dropEntry(oldestKey, this.entries.get(oldestKey));
|
|
352285
352567
|
}
|
|
352286
352568
|
}
|
|
352287
352569
|
sweepStale(now) {
|
|
352288
352570
|
for (const [key, entry] of this.entries) {
|
|
352289
352571
|
if (now - entry.lastUpdatedAt > STALE_SESSION_MS) {
|
|
352290
|
-
|
|
352291
|
-
this.entries.delete(key);
|
|
352572
|
+
this.dropEntry(key, entry);
|
|
352292
352573
|
}
|
|
352293
352574
|
}
|
|
352294
352575
|
}
|
|
@@ -352329,6 +352610,11 @@ var require_live_browser_registry = __commonJS({
|
|
|
352329
352610
|
entry.currentUrl = url;
|
|
352330
352611
|
} catch {
|
|
352331
352612
|
}
|
|
352613
|
+
if (entry.screencast) {
|
|
352614
|
+
if (entry.thumbnailJpegBase64)
|
|
352615
|
+
entry.thumbnailCapturedAt = Date.now();
|
|
352616
|
+
return;
|
|
352617
|
+
}
|
|
352332
352618
|
const buf = await page.screenshot({
|
|
352333
352619
|
type: "jpeg",
|
|
352334
352620
|
quality: THUMBNAIL_QUALITY,
|
|
@@ -352336,17 +352622,8 @@ var require_live_browser_registry = __commonJS({
|
|
|
352336
352622
|
timeout: 3e3,
|
|
352337
352623
|
scale: "css"
|
|
352338
352624
|
});
|
|
352339
|
-
const base64 = buf.toString("base64");
|
|
352340
|
-
if (base64.length > BROWSER_THUMBNAIL_MAX_BASE64_BYTES) {
|
|
352341
|
-
return;
|
|
352342
|
-
}
|
|
352343
|
-
entry.thumbnailJpegBase64 = base64;
|
|
352344
|
-
entry.thumbnailCapturedAt = Date.now();
|
|
352345
352625
|
const viewport = page.viewportSize?.();
|
|
352346
|
-
|
|
352347
|
-
entry.thumbnailWidth = viewport.width;
|
|
352348
|
-
entry.thumbnailHeight = viewport.height;
|
|
352349
|
-
}
|
|
352626
|
+
this.acceptFrame(entry, buf.toString("base64"), "image/jpeg", viewport?.width, viewport?.height);
|
|
352350
352627
|
} catch {
|
|
352351
352628
|
} finally {
|
|
352352
352629
|
entry.isCapturing = false;
|
|
@@ -353060,7 +353337,7 @@ var require_package4 = __commonJS({
|
|
|
353060
353337
|
"package.json"(exports2, module2) {
|
|
353061
353338
|
module2.exports = {
|
|
353062
353339
|
name: "@validate.qa/runner",
|
|
353063
|
-
version: "1.0.
|
|
353340
|
+
version: "1.0.80",
|
|
353064
353341
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
353065
353342
|
bin: {
|
|
353066
353343
|
"validate-runner": "dist/cli.js"
|
|
@@ -353329,6 +353606,9 @@ async function enforceRunnerUpToDate(currentVersion, opts = {}) {
|
|
|
353329
353606
|
return result;
|
|
353330
353607
|
}
|
|
353331
353608
|
|
|
353609
|
+
// src/runner.ts
|
|
353610
|
+
init_dist();
|
|
353611
|
+
|
|
353332
353612
|
// src/services/appium-executor.ts
|
|
353333
353613
|
init_dist();
|
|
353334
353614
|
|
|
@@ -353852,6 +354132,24 @@ async function getScreenshot(sessionPath) {
|
|
|
353852
354132
|
return null;
|
|
353853
354133
|
}
|
|
353854
354134
|
}
|
|
354135
|
+
async function applyMjpegSettings(sessionPath, settings) {
|
|
354136
|
+
try {
|
|
354137
|
+
await appiumRequest(`${sessionPath}/appium/settings`, {
|
|
354138
|
+
method: "POST",
|
|
354139
|
+
body: JSON.stringify({
|
|
354140
|
+
settings: {
|
|
354141
|
+
mjpegServerFramerate: settings.framerate,
|
|
354142
|
+
mjpegScalingFactor: settings.scalingFactor,
|
|
354143
|
+
mjpegServerScreenshotQuality: settings.quality
|
|
354144
|
+
}
|
|
354145
|
+
})
|
|
354146
|
+
});
|
|
354147
|
+
return true;
|
|
354148
|
+
} catch (err) {
|
|
354149
|
+
if (isAppiumDriverDeadError(err)) throw err;
|
|
354150
|
+
return false;
|
|
354151
|
+
}
|
|
354152
|
+
}
|
|
353855
354153
|
async function getPageSource(sessionPath) {
|
|
353856
354154
|
try {
|
|
353857
354155
|
const value = readValue(await appiumRequest(`${sessionPath}/source`));
|
|
@@ -356360,7 +356658,15 @@ function mobileParallelSessionCapabilities(platform3, portOffset = 0, deviceId =
|
|
|
356360
356658
|
return (Number.isFinite(parsed) ? parsed : fallback) + offset;
|
|
356361
356659
|
};
|
|
356362
356660
|
if (platform3 === "ANDROID") {
|
|
356363
|
-
return {
|
|
356661
|
+
return {
|
|
356662
|
+
"appium:systemPort": readPort("APPIUM_SYSTEM_PORT", 8200),
|
|
356663
|
+
// Host port that UiAutomator2 forwards the on-device MJPEG broadcaster
|
|
356664
|
+
// (device port 7810) to. Without this the live mirror has to poll
|
|
356665
|
+
// WebDriver /screenshot for full-resolution PNGs, which the runner cannot
|
|
356666
|
+
// downscale itself. Base differs from iOS's 9100 so a concurrent
|
|
356667
|
+
// Android+iOS pair can never collide on a shared slot offset.
|
|
356668
|
+
"appium:mjpegServerPort": readPort("APPIUM_ANDROID_MJPEG_SERVER_PORT", 7810)
|
|
356669
|
+
};
|
|
356364
356670
|
}
|
|
356365
356671
|
const safeDevice = deviceId.replace(/[^A-Za-z0-9._-]+/g, "-") || `device-${offset}`;
|
|
356366
356672
|
const derivedDataRoot = env.APPIUM_DERIVED_DATA_PATH?.trim() || (0, import_node_path2.join)((0, import_node_os2.tmpdir)(), "validateqa-wda");
|
|
@@ -357615,6 +357921,15 @@ function acquireControlPortSlot(deviceId) {
|
|
|
357615
357921
|
function releaseControlPortSlot(deviceId) {
|
|
357616
357922
|
controlPortSlotsByDevice.delete(deviceId);
|
|
357617
357923
|
}
|
|
357924
|
+
function getControlPortSlot(deviceId) {
|
|
357925
|
+
return controlPortSlotsByDevice.get(deviceId);
|
|
357926
|
+
}
|
|
357927
|
+
function mobileMjpegPortForDevice(platform3, deviceId, env = process.env) {
|
|
357928
|
+
const slot = getControlPortSlot(deviceId);
|
|
357929
|
+
if (slot === void 0) return null;
|
|
357930
|
+
const port = mobileParallelSessionCapabilities(platform3, slot, deviceId, env)["appium:mjpegServerPort"];
|
|
357931
|
+
return typeof port === "number" && Number.isFinite(port) ? port : null;
|
|
357932
|
+
}
|
|
357618
357933
|
var PINNED_DEVICE_SNAPSHOT_TTL_MS = 25 * 6e4;
|
|
357619
357934
|
var pinnedDeviceSnapshot = null;
|
|
357620
357935
|
async function executeMobileTest(options) {
|
|
@@ -358553,14 +358868,109 @@ function decideMobileDeviceGate(input) {
|
|
|
358553
358868
|
}
|
|
358554
358869
|
|
|
358555
358870
|
// src/services/mobile-bridge.ts
|
|
358556
|
-
var
|
|
358871
|
+
var import_http2 = require("http");
|
|
358557
358872
|
var import_child_process4 = require("child_process");
|
|
358558
358873
|
var import_crypto = require("crypto");
|
|
358559
358874
|
var import_os4 = require("os");
|
|
358560
358875
|
var import_path4 = require("path");
|
|
358561
358876
|
var import_fs3 = require("fs");
|
|
358877
|
+
|
|
358878
|
+
// src/services/mobile/mjpeg-stream.ts
|
|
358879
|
+
var import_http = require("http");
|
|
358880
|
+
init_dist();
|
|
358881
|
+
var JPEG_SOI = Buffer.from([255, 216]);
|
|
358882
|
+
var JPEG_EOI = Buffer.from([255, 217]);
|
|
358883
|
+
function startMjpegStream(options) {
|
|
358884
|
+
const { port, host = "127.0.0.1", onFrame, onError } = options;
|
|
358885
|
+
let stopped = false;
|
|
358886
|
+
let req = null;
|
|
358887
|
+
let response = null;
|
|
358888
|
+
let buffer = Buffer.alloc(0);
|
|
358889
|
+
const fail = (error2) => {
|
|
358890
|
+
if (stopped) return;
|
|
358891
|
+
stopped = true;
|
|
358892
|
+
try {
|
|
358893
|
+
req?.destroy();
|
|
358894
|
+
} catch {
|
|
358895
|
+
}
|
|
358896
|
+
try {
|
|
358897
|
+
response?.destroy();
|
|
358898
|
+
} catch {
|
|
358899
|
+
}
|
|
358900
|
+
buffer = Buffer.alloc(0);
|
|
358901
|
+
onError?.(error2);
|
|
358902
|
+
};
|
|
358903
|
+
const consume = (chunk) => {
|
|
358904
|
+
if (stopped) return;
|
|
358905
|
+
buffer = buffer.length === 0 ? chunk : Buffer.concat([buffer, chunk]);
|
|
358906
|
+
for (; ; ) {
|
|
358907
|
+
const start = buffer.indexOf(JPEG_SOI);
|
|
358908
|
+
if (start < 0) {
|
|
358909
|
+
if (buffer.length > 1) buffer = buffer.subarray(buffer.length - 1);
|
|
358910
|
+
return;
|
|
358911
|
+
}
|
|
358912
|
+
const end = buffer.indexOf(JPEG_EOI, start + JPEG_SOI.length);
|
|
358913
|
+
if (end < 0) {
|
|
358914
|
+
if (start > 0) buffer = buffer.subarray(start);
|
|
358915
|
+
if (buffer.length > MOBILE_MJPEG_MAX_FRAME_BYTES) buffer = Buffer.alloc(0);
|
|
358916
|
+
return;
|
|
358917
|
+
}
|
|
358918
|
+
const frameEnd = end + JPEG_EOI.length;
|
|
358919
|
+
const frame = buffer.subarray(start, frameEnd);
|
|
358920
|
+
buffer = buffer.subarray(frameEnd);
|
|
358921
|
+
try {
|
|
358922
|
+
onFrame(frame);
|
|
358923
|
+
} catch {
|
|
358924
|
+
}
|
|
358925
|
+
}
|
|
358926
|
+
};
|
|
358927
|
+
try {
|
|
358928
|
+
req = (0, import_http.request)(
|
|
358929
|
+
{ host, port, path: "/", method: "GET", headers: { Connection: "keep-alive" } },
|
|
358930
|
+
(res) => {
|
|
358931
|
+
response = res;
|
|
358932
|
+
if (stopped) {
|
|
358933
|
+
res.destroy();
|
|
358934
|
+
return;
|
|
358935
|
+
}
|
|
358936
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
358937
|
+
fail(new Error(`MJPEG broadcaster returned HTTP ${res.statusCode}`));
|
|
358938
|
+
res.destroy();
|
|
358939
|
+
return;
|
|
358940
|
+
}
|
|
358941
|
+
res.on("data", (chunk) => consume(chunk));
|
|
358942
|
+
res.on("end", () => fail(new Error("MJPEG stream ended")));
|
|
358943
|
+
res.on("error", (err) => fail(err));
|
|
358944
|
+
}
|
|
358945
|
+
);
|
|
358946
|
+
req.on("error", (err) => fail(err));
|
|
358947
|
+
req.setTimeout(1e4, () => fail(new Error("MJPEG connect timeout")));
|
|
358948
|
+
req.end();
|
|
358949
|
+
} catch (err) {
|
|
358950
|
+
fail(err instanceof Error ? err : new Error(String(err)));
|
|
358951
|
+
}
|
|
358952
|
+
return {
|
|
358953
|
+
stop: () => {
|
|
358954
|
+
if (stopped) return;
|
|
358955
|
+
stopped = true;
|
|
358956
|
+
try {
|
|
358957
|
+
req?.destroy();
|
|
358958
|
+
} catch {
|
|
358959
|
+
}
|
|
358960
|
+
try {
|
|
358961
|
+
response?.destroy();
|
|
358962
|
+
} catch {
|
|
358963
|
+
}
|
|
358964
|
+
buffer = Buffer.alloc(0);
|
|
358965
|
+
},
|
|
358966
|
+
isActive: () => !stopped
|
|
358967
|
+
};
|
|
358968
|
+
}
|
|
358969
|
+
|
|
358970
|
+
// src/services/mobile-bridge.ts
|
|
358562
358971
|
init_dist();
|
|
358563
358972
|
var MOBILE_BRIDGE_PORT2 = 9515;
|
|
358973
|
+
var RELAY_DEMAND_POLL_MS = 2e3;
|
|
358564
358974
|
var MAX_REQUEST_BODY_BYTES = 64 * 1024;
|
|
358565
358975
|
var MAX_TEXT_INPUT_LENGTH = 4e3;
|
|
358566
358976
|
var MAX_APP_ID_LENGTH = 255;
|
|
@@ -359072,6 +359482,18 @@ async function handleSessionStop(_req, res) {
|
|
|
359072
359482
|
sendJSON(res, 200, { ok: true });
|
|
359073
359483
|
}
|
|
359074
359484
|
var runnerChannels = /* @__PURE__ */ new Map();
|
|
359485
|
+
var relayDemandTimer = null;
|
|
359486
|
+
function setMirrorRelay(deviceId, relay) {
|
|
359487
|
+
const ch = runnerChannels.get(deviceId);
|
|
359488
|
+
if (!ch) return;
|
|
359489
|
+
ch.relay = relay;
|
|
359490
|
+
if (relay) {
|
|
359491
|
+
ensureRelayDemandSupervisor();
|
|
359492
|
+
if (hasChannelDemand(ch)) ensureChannelPolling(ch);
|
|
359493
|
+
} else {
|
|
359494
|
+
stopChannelCaptureIfIdle(ch);
|
|
359495
|
+
}
|
|
359496
|
+
}
|
|
359075
359497
|
function readDeviceIdParam(req) {
|
|
359076
359498
|
try {
|
|
359077
359499
|
const url = new URL(req.url || "", "http://localhost");
|
|
@@ -359103,7 +359525,7 @@ function writeFrameToChannelClient(ch, client, frame) {
|
|
|
359103
359525
|
} catch {
|
|
359104
359526
|
}
|
|
359105
359527
|
ch.clients.delete(client);
|
|
359106
|
-
|
|
359528
|
+
stopChannelCaptureIfIdle(ch);
|
|
359107
359529
|
return;
|
|
359108
359530
|
}
|
|
359109
359531
|
client.write(`data: ${JSON.stringify(frame)}
|
|
@@ -359112,16 +359534,95 @@ function writeFrameToChannelClient(ch, client, frame) {
|
|
|
359112
359534
|
} catch {
|
|
359113
359535
|
}
|
|
359114
359536
|
}
|
|
359537
|
+
function broadcastChannelFrame(ch, frame) {
|
|
359538
|
+
ch.lastFrame = frame;
|
|
359539
|
+
for (const client of ch.clients) writeFrameToChannelClient(ch, client, frame);
|
|
359540
|
+
if (ch.relay && frame.screenshot) {
|
|
359541
|
+
try {
|
|
359542
|
+
ch.relay.onFrame(frame.screenshot, frame.mimeType ?? "image/png");
|
|
359543
|
+
} catch {
|
|
359544
|
+
}
|
|
359545
|
+
}
|
|
359546
|
+
}
|
|
359547
|
+
function tryStartChannelMjpeg(ch) {
|
|
359548
|
+
if (ch.mjpeg || ch.mjpegStarting || ch.mjpegUnavailable) return;
|
|
359549
|
+
const port = mobileMjpegPortForDevice(ch.platform, ch.deviceId);
|
|
359550
|
+
if (port === null) {
|
|
359551
|
+
ch.mjpegUnavailable = true;
|
|
359552
|
+
return;
|
|
359553
|
+
}
|
|
359554
|
+
ch.mjpegStarting = true;
|
|
359555
|
+
const handle = startMjpegStream({
|
|
359556
|
+
port,
|
|
359557
|
+
onFrame: (jpeg) => {
|
|
359558
|
+
const isOwned = ch.mjpeg === handle;
|
|
359559
|
+
const isCandidate = ch.mjpeg === null && ch.mjpegStarting;
|
|
359560
|
+
if (!isOwned && !isCandidate) return;
|
|
359561
|
+
ch.mjpegLastFrameAt = Date.now();
|
|
359562
|
+
ch.nullTicks = 0;
|
|
359563
|
+
if (isCandidate) {
|
|
359564
|
+
ch.mjpeg = handle;
|
|
359565
|
+
ch.mjpegStarting = false;
|
|
359566
|
+
if (ch.pollTimer) {
|
|
359567
|
+
clearInterval(ch.pollTimer);
|
|
359568
|
+
ch.pollTimer = null;
|
|
359569
|
+
}
|
|
359570
|
+
}
|
|
359571
|
+
broadcastChannelFrame(ch, {
|
|
359572
|
+
timestamp: ch.mjpegLastFrameAt,
|
|
359573
|
+
screenshot: jpeg.toString("base64"),
|
|
359574
|
+
source: null,
|
|
359575
|
+
mimeType: "image/jpeg"
|
|
359576
|
+
});
|
|
359577
|
+
},
|
|
359578
|
+
onError: () => {
|
|
359579
|
+
const wasActive = ch.mjpeg === handle;
|
|
359580
|
+
ch.mjpeg = null;
|
|
359581
|
+
ch.mjpegStarting = false;
|
|
359582
|
+
ch.mjpegUnavailable = true;
|
|
359583
|
+
if (wasActive && hasChannelDemand(ch)) ensureChannelPolling(ch);
|
|
359584
|
+
}
|
|
359585
|
+
});
|
|
359586
|
+
const firstFrameDeadline = setTimeout(() => {
|
|
359587
|
+
if (ch.mjpeg) return;
|
|
359588
|
+
ch.mjpegStarting = false;
|
|
359589
|
+
ch.mjpegUnavailable = true;
|
|
359590
|
+
handle.stop();
|
|
359591
|
+
if (hasChannelDemand(ch)) ensureChannelPolling(ch);
|
|
359592
|
+
}, MOBILE_MJPEG_FIRST_FRAME_TIMEOUT_MS);
|
|
359593
|
+
firstFrameDeadline.unref?.();
|
|
359594
|
+
}
|
|
359595
|
+
function hasChannelDemand(ch) {
|
|
359596
|
+
if (ch.clients.size > 0) return true;
|
|
359597
|
+
if (!ch.relay) return false;
|
|
359598
|
+
try {
|
|
359599
|
+
return ch.relay.wanted();
|
|
359600
|
+
} catch {
|
|
359601
|
+
return false;
|
|
359602
|
+
}
|
|
359603
|
+
}
|
|
359604
|
+
function ensureRelayDemandSupervisor() {
|
|
359605
|
+
if (relayDemandTimer) return;
|
|
359606
|
+
relayDemandTimer = setInterval(() => {
|
|
359607
|
+
for (const ch of [...runnerChannels.values()]) {
|
|
359608
|
+
if (hasChannelDemand(ch)) ensureChannelPolling(ch);
|
|
359609
|
+
else stopChannelCaptureIfIdle(ch);
|
|
359610
|
+
}
|
|
359611
|
+
if (runnerChannels.size === 0 && relayDemandTimer) {
|
|
359612
|
+
clearInterval(relayDemandTimer);
|
|
359613
|
+
relayDemandTimer = null;
|
|
359614
|
+
}
|
|
359615
|
+
}, RELAY_DEMAND_POLL_MS);
|
|
359616
|
+
relayDemandTimer.unref?.();
|
|
359617
|
+
}
|
|
359115
359618
|
async function pollRunnerChannel(ch) {
|
|
359116
|
-
if (ch
|
|
359619
|
+
if (!hasChannelDemand(ch) || ch.polling) return;
|
|
359620
|
+
if (ch.mjpeg) return;
|
|
359117
359621
|
ch.polling = true;
|
|
359118
359622
|
try {
|
|
359119
359623
|
validateSessionId(ch.sessionId);
|
|
359120
|
-
const
|
|
359121
|
-
|
|
359122
|
-
getPageSource(`/session/${ch.sessionId}`)
|
|
359123
|
-
]);
|
|
359124
|
-
if (!screenshot && !source) {
|
|
359624
|
+
const screenshot = await getScreenshot(`/session/${ch.sessionId}`);
|
|
359625
|
+
if (!screenshot) {
|
|
359125
359626
|
ch.nullTicks++;
|
|
359126
359627
|
if (ch.nullTicks >= MIRROR_DEAD_SESSION_TICKS) {
|
|
359127
359628
|
const alive = await isSessionAlive(ch.sessionId);
|
|
@@ -359134,32 +359635,47 @@ async function pollRunnerChannel(ch) {
|
|
|
359134
359635
|
} else {
|
|
359135
359636
|
ch.nullTicks = 0;
|
|
359136
359637
|
}
|
|
359137
|
-
|
|
359138
|
-
|
|
359139
|
-
|
|
359638
|
+
broadcastChannelFrame(ch, {
|
|
359639
|
+
timestamp: Date.now(),
|
|
359640
|
+
screenshot: screenshot ?? null,
|
|
359641
|
+
source: null,
|
|
359642
|
+
mimeType: "image/png"
|
|
359643
|
+
});
|
|
359140
359644
|
} catch {
|
|
359141
359645
|
} finally {
|
|
359142
359646
|
ch.polling = false;
|
|
359143
359647
|
}
|
|
359144
359648
|
}
|
|
359145
359649
|
function ensureChannelPolling(ch) {
|
|
359146
|
-
if (ch
|
|
359650
|
+
if (!hasChannelDemand(ch)) return;
|
|
359651
|
+
tryStartChannelMjpeg(ch);
|
|
359652
|
+
if (ch.mjpeg || ch.pollTimer) return;
|
|
359147
359653
|
ch.pollTimer = setInterval(() => {
|
|
359148
359654
|
void pollRunnerChannel(ch);
|
|
359149
359655
|
}, MOBILE_SCREENSHOT_INTERVAL_MS);
|
|
359150
359656
|
ch.pollTimer.unref?.();
|
|
359151
359657
|
}
|
|
359152
|
-
function
|
|
359153
|
-
if (ch
|
|
359658
|
+
function stopChannelCaptureIfIdle(ch) {
|
|
359659
|
+
if (hasChannelDemand(ch)) return;
|
|
359660
|
+
if (ch.pollTimer) {
|
|
359154
359661
|
clearInterval(ch.pollTimer);
|
|
359155
359662
|
ch.pollTimer = null;
|
|
359156
359663
|
}
|
|
359664
|
+
if (ch.mjpeg) {
|
|
359665
|
+
ch.mjpeg.stop();
|
|
359666
|
+
ch.mjpeg = null;
|
|
359667
|
+
}
|
|
359157
359668
|
}
|
|
359158
359669
|
function closeRunnerChannel(ch) {
|
|
359159
359670
|
if (ch.pollTimer) {
|
|
359160
359671
|
clearInterval(ch.pollTimer);
|
|
359161
359672
|
ch.pollTimer = null;
|
|
359162
359673
|
}
|
|
359674
|
+
if (ch.mjpeg) {
|
|
359675
|
+
ch.mjpeg.stop();
|
|
359676
|
+
ch.mjpeg = null;
|
|
359677
|
+
}
|
|
359678
|
+
ch.relay = null;
|
|
359163
359679
|
for (const client of ch.clients) {
|
|
359164
359680
|
try {
|
|
359165
359681
|
client.end();
|
|
@@ -359168,7 +359684,7 @@ function closeRunnerChannel(ch) {
|
|
|
359168
359684
|
}
|
|
359169
359685
|
ch.clients.clear();
|
|
359170
359686
|
ch.lastFrame = null;
|
|
359171
|
-
runnerChannels.delete(ch.deviceId);
|
|
359687
|
+
if (runnerChannels.get(ch.deviceId) === ch) runnerChannels.delete(ch.deviceId);
|
|
359172
359688
|
}
|
|
359173
359689
|
function closeAllRunnerChannels() {
|
|
359174
359690
|
for (const ch of [...runnerChannels.values()]) closeRunnerChannel(ch);
|
|
@@ -359190,7 +359706,7 @@ async function serveRunnerMirror(req, res, ch) {
|
|
|
359190
359706
|
}
|
|
359191
359707
|
req.on("close", () => {
|
|
359192
359708
|
ch.clients.delete(res);
|
|
359193
|
-
|
|
359709
|
+
stopChannelCaptureIfIdle(ch);
|
|
359194
359710
|
});
|
|
359195
359711
|
}
|
|
359196
359712
|
async function handleMirror(req, res) {
|
|
@@ -359556,7 +360072,7 @@ async function startMobileBridge(options) {
|
|
|
359556
360072
|
}
|
|
359557
360073
|
killStaleBridge();
|
|
359558
360074
|
return new Promise((resolve2, reject) => {
|
|
359559
|
-
server2 = (0,
|
|
360075
|
+
server2 = (0, import_http2.createServer)(route);
|
|
359560
360076
|
server2.on("error", (err) => {
|
|
359561
360077
|
if (err.code === "EADDRINUSE") {
|
|
359562
360078
|
reject(new Error(`Port ${port} already in use. Another bridge instance may be running.`));
|
|
@@ -359744,13 +360260,26 @@ function attachMirrorSession(sessionId, platform3, deviceId) {
|
|
|
359744
360260
|
lastFrame: null,
|
|
359745
360261
|
pollTimer: null,
|
|
359746
360262
|
nullTicks: 0,
|
|
359747
|
-
polling: false
|
|
360263
|
+
polling: false,
|
|
360264
|
+
mjpeg: null,
|
|
360265
|
+
mjpegLastFrameAt: 0,
|
|
360266
|
+
mjpegStarting: false,
|
|
360267
|
+
mjpegUnavailable: false,
|
|
360268
|
+
relay: null
|
|
359748
360269
|
};
|
|
359749
360270
|
ch.sessionId = sessionId;
|
|
359750
360271
|
ch.platform = platform3;
|
|
359751
360272
|
ch.nullTicks = 0;
|
|
360273
|
+
if (ch.mjpeg) {
|
|
360274
|
+
ch.mjpeg.stop();
|
|
360275
|
+
ch.mjpeg = null;
|
|
360276
|
+
}
|
|
360277
|
+
ch.mjpegUnavailable = false;
|
|
360278
|
+
ch.mjpegStarting = false;
|
|
359752
360279
|
runnerChannels.set(deviceId, ch);
|
|
359753
|
-
|
|
360280
|
+
void applyMjpegSettings(`/session/${sessionId}`, MOBILE_MJPEG_SETTINGS).catch(() => {
|
|
360281
|
+
});
|
|
360282
|
+
if (hasChannelDemand(ch)) {
|
|
359754
360283
|
ensureChannelPolling(ch);
|
|
359755
360284
|
void pollRunnerChannel(ch);
|
|
359756
360285
|
}
|
|
@@ -360722,6 +361251,13 @@ function attachMobileDiscoveryNetworkContext(calls, result, appId) {
|
|
|
360722
361251
|
}));
|
|
360723
361252
|
}
|
|
360724
361253
|
var runnerPackage = require_package4();
|
|
361254
|
+
var MIRROR_RELAY_FRESH_MS = 3e3;
|
|
361255
|
+
var liveFrameWatchedByProject = /* @__PURE__ */ new Map();
|
|
361256
|
+
var liveFrameIdleProbeAt = /* @__PURE__ */ new Map();
|
|
361257
|
+
var LIVE_FRAME_IDLE_PROBE_MAX_ENTRIES = 256;
|
|
361258
|
+
function isLiveFrameWatched(projectId) {
|
|
361259
|
+
return !projectId || liveFrameWatchedByProject.get(projectId) !== false;
|
|
361260
|
+
}
|
|
360725
361261
|
var SECRET_VAR_PATTERN = /password|secret|token|api[_]?key|pass\b|credential|bearer|pat\b/i;
|
|
360726
361262
|
var IS_CLOUD = parseBoolFlag(process.env.RUNNER_CLOUD);
|
|
360727
361263
|
var RUNNER_NAME = runnerPackage.name ?? "@validate.qa/runner";
|
|
@@ -361499,6 +362035,22 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
361499
362035
|
testCaseId: run2.testCaseId ?? void 0
|
|
361500
362036
|
});
|
|
361501
362037
|
requestLiveBrowserHeartbeat(true);
|
|
362038
|
+
let lastMirrorRelayFrameAt = 0;
|
|
362039
|
+
const attachMirrorWithRelay = (sessionId, platform3, deviceId) => {
|
|
362040
|
+
attachMirrorSession(sessionId, platform3, deviceId);
|
|
362041
|
+
if (!deviceId) return;
|
|
362042
|
+
setMirrorRelay(deviceId, {
|
|
362043
|
+
onFrame: (base64, mimeType) => {
|
|
362044
|
+
lastMirrorRelayFrameAt = Date.now();
|
|
362045
|
+
liveBrowserHandle.updateSnapshot({
|
|
362046
|
+
thumbnailJpegBase64: base64,
|
|
362047
|
+
thumbnailCapturedAt: Date.now(),
|
|
362048
|
+
mimeType
|
|
362049
|
+
});
|
|
362050
|
+
},
|
|
362051
|
+
wanted: () => isLiveFrameWatched(run2.projectId)
|
|
362052
|
+
});
|
|
362053
|
+
};
|
|
361502
362054
|
const liveBrowserHeartbeatInterval = setInterval(() => {
|
|
361503
362055
|
liveBrowserHandle.patch({});
|
|
361504
362056
|
requestLiveBrowserHeartbeat();
|
|
@@ -362402,7 +362954,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
362402
362954
|
deeplink: harvestTarget.deeplink
|
|
362403
362955
|
},
|
|
362404
362956
|
onSessionReady: (sessionId, deviceId, sessionPlatform) => {
|
|
362405
|
-
|
|
362957
|
+
attachMirrorWithRelay(sessionId, sessionPlatform, deviceId);
|
|
362406
362958
|
const name = getCachedDeviceSnapshot().find((d) => d.id === deviceId)?.name;
|
|
362407
362959
|
liveBrowserHandle.patch({ deviceId, ...name ? { deviceName: name } : {} });
|
|
362408
362960
|
requestLiveBrowserHeartbeat(true);
|
|
@@ -362872,7 +363424,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
362872
363424
|
}
|
|
362873
363425
|
);
|
|
362874
363426
|
mobileOnLog(`Appium session created: ${session.sessionId} on device ${session.deviceId}`);
|
|
362875
|
-
|
|
363427
|
+
attachMirrorWithRelay(session.sessionId, platform3, session.deviceId);
|
|
362876
363428
|
const driver = new MobileBridgeDriver({
|
|
362877
363429
|
sessionId: session.sessionId,
|
|
362878
363430
|
platform: platform3,
|
|
@@ -362909,7 +363461,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
362909
363461
|
throw new Error("discovery teardown started during session recovery \u2014 replacement session closed");
|
|
362910
363462
|
}
|
|
362911
363463
|
session = replacement;
|
|
362912
|
-
|
|
363464
|
+
attachMirrorWithRelay(replacement.sessionId, platform3, replacement.deviceId);
|
|
362913
363465
|
liveBrowserHandle.patch({
|
|
362914
363466
|
deviceId: replacement.deviceId,
|
|
362915
363467
|
deviceName: replacement.deviceName
|
|
@@ -362930,10 +363482,16 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
362930
363482
|
// UI a reliable mobile fallback when the direct bridge SSE is
|
|
362931
363483
|
// still opening or blocked by the user's network/tunnel.
|
|
362932
363484
|
onScreenshot: (base64) => {
|
|
363485
|
+
if (Date.now() - lastMirrorRelayFrameAt < MIRROR_RELAY_FRESH_MS) {
|
|
363486
|
+
liveBrowserHandle.updateSnapshot({ currentUrl: mobileTarget.appId });
|
|
363487
|
+
return;
|
|
363488
|
+
}
|
|
362933
363489
|
liveBrowserHandle.updateSnapshot({
|
|
362934
363490
|
currentUrl: mobileTarget.appId,
|
|
362935
363491
|
thumbnailJpegBase64: base64,
|
|
362936
|
-
thumbnailCapturedAt: Date.now()
|
|
363492
|
+
thumbnailCapturedAt: Date.now(),
|
|
363493
|
+
// Appium's screenshot endpoint returns PNG despite the field name.
|
|
363494
|
+
mimeType: "image/png"
|
|
362937
363495
|
});
|
|
362938
363496
|
requestLiveBrowserHeartbeat();
|
|
362939
363497
|
},
|
|
@@ -363400,7 +363958,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
363400
363958
|
deeplink: target.deeplink
|
|
363401
363959
|
});
|
|
363402
363960
|
liveSession = opened;
|
|
363403
|
-
|
|
363961
|
+
attachMirrorWithRelay(opened.sessionId, target.platform, opened.deviceId);
|
|
363404
363962
|
bridgeDriver = new MobileBridgeDriver({
|
|
363405
363963
|
sessionId: opened.sessionId,
|
|
363406
363964
|
platform: target.platform,
|
|
@@ -363590,7 +364148,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
363590
364148
|
// Stream this run to its OWN per-device dashboard tile (keyed by the
|
|
363591
364149
|
// leased device id) so N parallel Appium runs each show a live mirror.
|
|
363592
364150
|
onSessionReady: (sessionId, deviceId, sessionPlatform) => {
|
|
363593
|
-
|
|
364151
|
+
attachMirrorWithRelay(sessionId, sessionPlatform, deviceId);
|
|
363594
364152
|
requestLiveBrowserHeartbeat(true);
|
|
363595
364153
|
},
|
|
363596
364154
|
onSessionEnding: (deviceId) => {
|
|
@@ -364178,6 +364736,41 @@ async function startRunner(opts) {
|
|
|
364178
364736
|
} catch {
|
|
364179
364737
|
}
|
|
364180
364738
|
};
|
|
364739
|
+
const liveFramePushInFlight = /* @__PURE__ */ new Set();
|
|
364740
|
+
const publishLiveFrame = (frame) => {
|
|
364741
|
+
if (!frame.projectId) return;
|
|
364742
|
+
if (liveFramePushInFlight.has(frame.sessionId)) return;
|
|
364743
|
+
if (!isLiveFrameWatched(frame.projectId)) {
|
|
364744
|
+
const lastProbeAt = liveFrameIdleProbeAt.get(frame.sessionId) ?? 0;
|
|
364745
|
+
if (frame.capturedAt - lastProbeAt < LIVE_FRAME_IDLE_PUSH_INTERVAL_MS) return;
|
|
364746
|
+
if (liveFrameIdleProbeAt.size >= LIVE_FRAME_IDLE_PROBE_MAX_ENTRIES) {
|
|
364747
|
+
const expiredBefore = frame.capturedAt - LIVE_FRAME_IDLE_PUSH_INTERVAL_MS;
|
|
364748
|
+
for (const [sessionId, probedAt] of liveFrameIdleProbeAt) {
|
|
364749
|
+
if (probedAt >= expiredBefore) break;
|
|
364750
|
+
liveFrameIdleProbeAt.delete(sessionId);
|
|
364751
|
+
}
|
|
364752
|
+
}
|
|
364753
|
+
liveFrameIdleProbeAt.set(frame.sessionId, frame.capturedAt);
|
|
364754
|
+
}
|
|
364755
|
+
liveFramePushInFlight.add(frame.sessionId);
|
|
364756
|
+
const frameProjectId = frame.projectId;
|
|
364757
|
+
void fetch(`${serverUrl}/api/runner/live-frame`, {
|
|
364758
|
+
method: "POST",
|
|
364759
|
+
headers,
|
|
364760
|
+
body: JSON.stringify(frame),
|
|
364761
|
+
signal: AbortSignal.timeout(5e3)
|
|
364762
|
+
}).then(async (res) => {
|
|
364763
|
+
if (!res.ok) return;
|
|
364764
|
+
const ack = await res.json().catch(() => null);
|
|
364765
|
+
if (ack && typeof ack.watching === "boolean") {
|
|
364766
|
+
liveFrameWatchedByProject.set(frameProjectId, ack.watching);
|
|
364767
|
+
}
|
|
364768
|
+
}).catch(() => {
|
|
364769
|
+
}).finally(() => {
|
|
364770
|
+
liveFramePushInFlight.delete(frame.sessionId);
|
|
364771
|
+
});
|
|
364772
|
+
};
|
|
364773
|
+
import_runner_core11.liveBrowserRegistry.setFramePublisher(publishLiveFrame);
|
|
364181
364774
|
let liveBrowserHeartbeatInFlight = false;
|
|
364182
364775
|
let liveBrowserHeartbeatPending = false;
|
|
364183
364776
|
let lastLiveBrowserHeartbeatAt = 0;
|