@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.mjs
CHANGED
|
@@ -1893,6 +1893,57 @@ var init_constants = __esm({
|
|
|
1893
1893
|
}
|
|
1894
1894
|
});
|
|
1895
1895
|
|
|
1896
|
+
// ../shared/dist/live-frame.js
|
|
1897
|
+
function isValidLiveFrame(frame) {
|
|
1898
|
+
if (!frame || typeof frame !== "object")
|
|
1899
|
+
return false;
|
|
1900
|
+
const f = frame;
|
|
1901
|
+
if (typeof f.sessionId !== "string" || f.sessionId.length === 0 || f.sessionId.length > 200)
|
|
1902
|
+
return false;
|
|
1903
|
+
if (typeof f.projectId !== "string" || f.projectId.length === 0 || f.projectId.length > 200)
|
|
1904
|
+
return false;
|
|
1905
|
+
if (typeof f.capturedAt !== "number" || !Number.isFinite(f.capturedAt))
|
|
1906
|
+
return false;
|
|
1907
|
+
if (f.mimeType !== "image/jpeg" && f.mimeType !== "image/png")
|
|
1908
|
+
return false;
|
|
1909
|
+
if (typeof f.base64 !== "string" || f.base64.length === 0)
|
|
1910
|
+
return false;
|
|
1911
|
+
if (f.base64.length > LIVE_FRAME_PUSH_MAX_BASE64_BYTES)
|
|
1912
|
+
return false;
|
|
1913
|
+
return true;
|
|
1914
|
+
}
|
|
1915
|
+
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;
|
|
1916
|
+
var init_live_frame = __esm({
|
|
1917
|
+
"../shared/dist/live-frame.js"() {
|
|
1918
|
+
"use strict";
|
|
1919
|
+
readEnvInt = (name, fallback) => {
|
|
1920
|
+
if (typeof process === "undefined")
|
|
1921
|
+
return fallback;
|
|
1922
|
+
const parsed = parseInt(process.env?.[name] ?? "", 10);
|
|
1923
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
1924
|
+
};
|
|
1925
|
+
LIVE_FRAME_PUSH_MAX_BASE64_BYTES = readEnvInt("LIVE_FRAME_PUSH_MAX_BASE64_BYTES", 45e5);
|
|
1926
|
+
LIVE_THUMBNAIL_MAX_BASE64_BYTES = readEnvInt("LIVE_THUMBNAIL_MAX_BASE64_BYTES", 4e5);
|
|
1927
|
+
LIVE_FRAME_MAX_DIMENSION = readEnvInt("LIVE_FRAME_MAX_DIMENSION", 900);
|
|
1928
|
+
LIVE_FRAME_JPEG_QUALITY = readEnvInt("LIVE_FRAME_JPEG_QUALITY", 55);
|
|
1929
|
+
LIVE_FRAME_MIN_PUSH_INTERVAL_MS = readEnvInt("LIVE_FRAME_MIN_PUSH_INTERVAL_MS", 125);
|
|
1930
|
+
LIVE_FRAME_UNFOCUSED_INTERVAL_MS = readEnvInt("LIVE_FRAME_UNFOCUSED_INTERVAL_MS", 2e3);
|
|
1931
|
+
LIVE_FRAME_IDLE_PUSH_INTERVAL_MS = readEnvInt("LIVE_FRAME_IDLE_PUSH_INTERVAL_MS", 5e3);
|
|
1932
|
+
LIVE_FRAME_STALE_MS = readEnvInt("LIVE_FRAME_STALE_MS", 3e4);
|
|
1933
|
+
LIVE_FRAME_MAX_SESSIONS_PER_PROJECT = readEnvInt("LIVE_FRAME_MAX_SESSIONS_PER_PROJECT", 32);
|
|
1934
|
+
MOBILE_MJPEG_SETTINGS = {
|
|
1935
|
+
/** Screenshots per second the broadcaster emits (both drivers: range 1-60). */
|
|
1936
|
+
framerate: readEnvInt("MOBILE_MJPEG_FRAMERATE", 10),
|
|
1937
|
+
/** Downscale percentage. UiAutomator2 defaults to 50, XCUITest to 100. */
|
|
1938
|
+
scalingFactor: readEnvInt("MOBILE_MJPEG_SCALING_FACTOR", 50),
|
|
1939
|
+
/** JPEG compression factor (both drivers: range 1-100). */
|
|
1940
|
+
quality: readEnvInt("MOBILE_MJPEG_QUALITY", 50)
|
|
1941
|
+
};
|
|
1942
|
+
MOBILE_MJPEG_FIRST_FRAME_TIMEOUT_MS = readEnvInt("MOBILE_MJPEG_FIRST_FRAME_TIMEOUT_MS", 6e3);
|
|
1943
|
+
MOBILE_MJPEG_MAX_FRAME_BYTES = readEnvInt("MOBILE_MJPEG_MAX_FRAME_BYTES", 8 * 1024 * 1024);
|
|
1944
|
+
}
|
|
1945
|
+
});
|
|
1946
|
+
|
|
1896
1947
|
// ../shared/dist/project-key.js
|
|
1897
1948
|
function isValidProjectKey(key) {
|
|
1898
1949
|
return typeof key === "string" && PROJECT_KEY_PATTERN.test(key) && !RESERVED_PROJECT_KEYS.has(key);
|
|
@@ -6315,6 +6366,15 @@ __export(dist_exports, {
|
|
|
6315
6366
|
GROUP_LABELS: () => GROUP_LABELS,
|
|
6316
6367
|
LEGACY_TTS_MODEL_MAP: () => LEGACY_TTS_MODEL_MAP,
|
|
6317
6368
|
LEGACY_TTS_VOICE_ID_MAP: () => LEGACY_TTS_VOICE_ID_MAP,
|
|
6369
|
+
LIVE_FRAME_IDLE_PUSH_INTERVAL_MS: () => LIVE_FRAME_IDLE_PUSH_INTERVAL_MS,
|
|
6370
|
+
LIVE_FRAME_JPEG_QUALITY: () => LIVE_FRAME_JPEG_QUALITY,
|
|
6371
|
+
LIVE_FRAME_MAX_DIMENSION: () => LIVE_FRAME_MAX_DIMENSION,
|
|
6372
|
+
LIVE_FRAME_MAX_SESSIONS_PER_PROJECT: () => LIVE_FRAME_MAX_SESSIONS_PER_PROJECT,
|
|
6373
|
+
LIVE_FRAME_MIN_PUSH_INTERVAL_MS: () => LIVE_FRAME_MIN_PUSH_INTERVAL_MS,
|
|
6374
|
+
LIVE_FRAME_PUSH_MAX_BASE64_BYTES: () => LIVE_FRAME_PUSH_MAX_BASE64_BYTES,
|
|
6375
|
+
LIVE_FRAME_STALE_MS: () => LIVE_FRAME_STALE_MS,
|
|
6376
|
+
LIVE_FRAME_UNFOCUSED_INTERVAL_MS: () => LIVE_FRAME_UNFOCUSED_INTERVAL_MS,
|
|
6377
|
+
LIVE_THUMBNAIL_MAX_BASE64_BYTES: () => LIVE_THUMBNAIL_MAX_BASE64_BYTES,
|
|
6318
6378
|
MAX_CREDIT_TOPUP_CENTS: () => MAX_CREDIT_TOPUP_CENTS,
|
|
6319
6379
|
MAX_DEEPLINK_LENGTH: () => MAX_DEEPLINK_LENGTH,
|
|
6320
6380
|
MIN_CREDIT_TOPUP_CENTS: () => MIN_CREDIT_TOPUP_CENTS,
|
|
@@ -6326,6 +6386,9 @@ __export(dist_exports, {
|
|
|
6326
6386
|
MOBILE_DEFAULT_SWIPE_DISTANCE: () => MOBILE_DEFAULT_SWIPE_DISTANCE,
|
|
6327
6387
|
MOBILE_FOCUS_SETTLE_MS: () => MOBILE_FOCUS_SETTLE_MS,
|
|
6328
6388
|
MOBILE_MAX_SWIPE_DISTANCE: () => MOBILE_MAX_SWIPE_DISTANCE,
|
|
6389
|
+
MOBILE_MJPEG_FIRST_FRAME_TIMEOUT_MS: () => MOBILE_MJPEG_FIRST_FRAME_TIMEOUT_MS,
|
|
6390
|
+
MOBILE_MJPEG_MAX_FRAME_BYTES: () => MOBILE_MJPEG_MAX_FRAME_BYTES,
|
|
6391
|
+
MOBILE_MJPEG_SETTINGS: () => MOBILE_MJPEG_SETTINGS,
|
|
6329
6392
|
MOBILE_PLATFORMS: () => MOBILE_PLATFORMS,
|
|
6330
6393
|
MOBILE_POST_ACTION_SETTLE_MS: () => MOBILE_POST_ACTION_SETTLE_MS,
|
|
6331
6394
|
MOBILE_SCREENSHOT_INTERVAL_MS: () => MOBILE_SCREENSHOT_INTERVAL_MS,
|
|
@@ -6438,6 +6501,7 @@ __export(dist_exports, {
|
|
|
6438
6501
|
isTestReadyForPlatform: () => isTestReadyForPlatform,
|
|
6439
6502
|
isTrialExpired: () => isTrialExpired,
|
|
6440
6503
|
isUIVariant: () => isUIVariant,
|
|
6504
|
+
isValidLiveFrame: () => isValidLiveFrame,
|
|
6441
6505
|
isValidMobileAppId: () => isValidMobileAppId,
|
|
6442
6506
|
isValidProjectKey: () => isValidProjectKey,
|
|
6443
6507
|
isValidSttProvider: () => isValidSttProvider,
|
|
@@ -6500,6 +6564,7 @@ var init_dist = __esm({
|
|
|
6500
6564
|
"use strict";
|
|
6501
6565
|
init_types();
|
|
6502
6566
|
init_constants();
|
|
6567
|
+
init_live_frame();
|
|
6503
6568
|
init_project_key();
|
|
6504
6569
|
init_mobile_test_generation();
|
|
6505
6570
|
init_mobile_step_healer();
|
|
@@ -87962,6 +88027,7 @@ var require_playwright_native = __commonJS({
|
|
|
87962
88027
|
var PACKAGE_DIR = (0, path_1.resolve)(__dirname, "..", "..");
|
|
87963
88028
|
var RUNS_DIR = process.env.RUNS_DIR || (0, path_1.join)(PACKAGE_DIR, ".qa-runs");
|
|
87964
88029
|
var PW_CLI = __require.resolve("@playwright/test/cli");
|
|
88030
|
+
var LIVE_BROWSER_STATE_POLL_INTERVAL_MS = 500;
|
|
87965
88031
|
async function executeTestViaNativePlaywright2(options) {
|
|
87966
88032
|
const { playwrightCode, baseUrl, testName, testVariables, authStateCapturePath, storageStatePath, extraHttpHeaders, authStandingMode, validateContext, liveBrowserHandle, onLiveBrowserSnapshot } = options;
|
|
87967
88033
|
const logs2 = [];
|
|
@@ -88051,7 +88117,7 @@ test.beforeEach(async ({ page }, testInfo) => {
|
|
|
88051
88117
|
await __qaLiveBrowserCapture();
|
|
88052
88118
|
__qaLiveBrowserInterval = setInterval(() => {
|
|
88053
88119
|
void __qaLiveBrowserCapture?.();
|
|
88054
|
-
},
|
|
88120
|
+
}, ${LIVE_BROWSER_STATE_POLL_INTERVAL_MS});
|
|
88055
88121
|
});
|
|
88056
88122
|
|
|
88057
88123
|
test.afterEach(async () => {
|
|
@@ -88468,7 +88534,7 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
|
|
|
88468
88534
|
};
|
|
88469
88535
|
const interval = setInterval(() => {
|
|
88470
88536
|
void poll();
|
|
88471
|
-
},
|
|
88537
|
+
}, LIVE_BROWSER_STATE_POLL_INTERVAL_MS);
|
|
88472
88538
|
interval.unref?.();
|
|
88473
88539
|
void poll();
|
|
88474
88540
|
return async () => {
|
|
@@ -320992,6 +321058,7 @@ var require_primary_action_ledger = __commonJS({
|
|
|
320992
321058
|
exports.isLedgerActive = isLedgerActive;
|
|
320993
321059
|
exports.isGateEnforcing = isGateEnforcing;
|
|
320994
321060
|
exports.isSteeringActive = isSteeringActive;
|
|
321061
|
+
exports.isPrimaryActionCoverageGapEnabled = isPrimaryActionCoverageGapEnabled;
|
|
320995
321062
|
exports.leadingVerb = leadingVerb;
|
|
320996
321063
|
exports.classifyPrimaryAction = classifyPrimaryAction;
|
|
320997
321064
|
exports.containerScopeOf = containerScopeOf;
|
|
@@ -321006,6 +321073,8 @@ var require_primary_action_ledger = __commonJS({
|
|
|
321006
321073
|
exports.classifyActionOutcome = classifyActionOutcome;
|
|
321007
321074
|
exports.detectPrimaryActions = detectPrimaryActions;
|
|
321008
321075
|
exports.summarizeLedger = summarizeLedger;
|
|
321076
|
+
exports.summarizeUnexercisedPrimaries = summarizeUnexercisedPrimaries;
|
|
321077
|
+
exports.buildUnexercisedPrimaryCoverageGaps = buildUnexercisedPrimaryCoverageGaps;
|
|
321009
321078
|
var reveal_diff_js_1 = require_reveal_diff();
|
|
321010
321079
|
var exploration_parser_js_1 = require_exploration_parser();
|
|
321011
321080
|
function getPrimaryActionGateMode(env = process.env) {
|
|
@@ -321025,6 +321094,10 @@ var require_primary_action_ledger = __commonJS({
|
|
|
321025
321094
|
function isSteeringActive(mode) {
|
|
321026
321095
|
return mode === "advisory" || mode === "enforce";
|
|
321027
321096
|
}
|
|
321097
|
+
function isPrimaryActionCoverageGapEnabled(env = process.env) {
|
|
321098
|
+
const raw = (env.DISCOVERY_PRIMARY_ACTION_COVERAGE_GAP ?? "").trim().toLowerCase();
|
|
321099
|
+
return raw === "1" || raw === "true" || raw === "on" || raw === "enabled";
|
|
321100
|
+
}
|
|
321028
321101
|
exports.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;
|
|
321029
321102
|
exports.AMBIGUOUS_ACTION_VERB_RE = /^(continue|next|apply|go|proceed|confirm|update|add|edit|ok|done|finish|complete|approve|enable|activate)\b/i;
|
|
321030
321103
|
exports.DESTRUCTIVE_ACTION_VERB_RE = /^(delete|remove|archive|reset|disconnect|deactivate|destroy|purge|revoke|discard|drop|wipe|erase|unsubscribe|cancel|clear|trash)\b/i;
|
|
@@ -321288,6 +321361,47 @@ var require_primary_action_ledger = __commonJS({
|
|
|
321288
321361
|
}
|
|
321289
321362
|
return `actions=${c.total} primary{detected=${c.detected} attempted=${c.attempted} captured=${c.captured} blocked=${c.blocked} exempt=${c.exempt}} ambiguous=${c.ambiguous}`;
|
|
321290
321363
|
}
|
|
321364
|
+
function summarizeUnexercisedPrimaries(ledger, routeInScope, cap = 12) {
|
|
321365
|
+
const out = [];
|
|
321366
|
+
for (const e of getBlockingActionDebt(ledger, routeInScope)) {
|
|
321367
|
+
if (e.status !== "DETECTED" && e.status !== "ATTEMPTED")
|
|
321368
|
+
continue;
|
|
321369
|
+
out.push({ label: e.label, route: e.route, containerScope: e.containerScope, status: e.status });
|
|
321370
|
+
if (out.length >= cap)
|
|
321371
|
+
break;
|
|
321372
|
+
}
|
|
321373
|
+
return out;
|
|
321374
|
+
}
|
|
321375
|
+
var REGEXP_META_RE = /[.*+?^${}()|[\]\\]/g;
|
|
321376
|
+
function labelCoveredByPlan(normLabel, plannedLower) {
|
|
321377
|
+
if (!normLabel.includes(" "))
|
|
321378
|
+
return false;
|
|
321379
|
+
const escaped = normLabel.replace(REGEXP_META_RE, "\\$&");
|
|
321380
|
+
return new RegExp(`(^|\\W)${escaped}($|\\W)`).test(plannedLower);
|
|
321381
|
+
}
|
|
321382
|
+
function buildUnexercisedPrimaryCoverageGaps(unexercised, plannedScenarioText, nowIso) {
|
|
321383
|
+
const gaps = [];
|
|
321384
|
+
const seen = /* @__PURE__ */ new Set();
|
|
321385
|
+
const planned = plannedScenarioText.toLowerCase();
|
|
321386
|
+
for (const a of unexercised) {
|
|
321387
|
+
const norm = normLabelKey(a.label);
|
|
321388
|
+
const key = `${norm}|${a.route}`;
|
|
321389
|
+
if (seen.has(key))
|
|
321390
|
+
continue;
|
|
321391
|
+
seen.add(key);
|
|
321392
|
+
if (labelCoveredByPlan(norm, planned))
|
|
321393
|
+
continue;
|
|
321394
|
+
const clickedNote = a.status === "ATTEMPTED" ? "clicked, but no result state was captured" : "never clicked";
|
|
321395
|
+
gaps.push({
|
|
321396
|
+
scenarioName: `Exercise "${a.label}"`,
|
|
321397
|
+
reason: `Primary action "${a.label}" was detected on ${a.route} but never exercised to an observed result (${clickedNote}).`,
|
|
321398
|
+
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.`,
|
|
321399
|
+
unmetPrerequisites: [],
|
|
321400
|
+
detectedAt: nowIso
|
|
321401
|
+
});
|
|
321402
|
+
}
|
|
321403
|
+
return gaps;
|
|
321404
|
+
}
|
|
321291
321405
|
}
|
|
321292
321406
|
});
|
|
321293
321407
|
|
|
@@ -322776,6 +322890,9 @@ var require_exploration_prompt_builder = __commonJS({
|
|
|
322776
322890
|
"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).",
|
|
322777
322891
|
`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:`
|
|
322778
322892
|
];
|
|
322893
|
+
if ((0, primary_action_ledger_js_1.isPrimaryActionCoverageGapEnabled)()) {
|
|
322894
|
+
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.");
|
|
322895
|
+
}
|
|
322779
322896
|
for (const e of debt.slice(0, 8)) {
|
|
322780
322897
|
const note = e.status === "ATTEMPTED" ? " \u2014 clicked but no result captured yet" : "";
|
|
322781
322898
|
out.push(` - "${e.label}" on ${e.route}${note}`);
|
|
@@ -326596,6 +326713,7 @@ ${inbox_tools_js_1.EMAIL_VERIFICATION_UNAVAILABLE_OVERRIDE_BLOCK}` : ""}`;
|
|
|
326596
326713
|
const primaryActionMode = (0, primary_action_ledger_js_1.getPrimaryActionGateMode)();
|
|
326597
326714
|
const primaryActionLedgerActive = (0, primary_action_ledger_js_1.isLedgerActive)(primaryActionMode) && options?.mode === "deep";
|
|
326598
326715
|
const primaryActionEnforcing = primaryActionLedgerActive && (0, primary_action_ledger_js_1.isGateEnforcing)(primaryActionMode);
|
|
326716
|
+
const primaryActionCoverageGapEnabled = primaryActionLedgerActive && (0, primary_action_ledger_js_1.isPrimaryActionCoverageGapEnabled)();
|
|
326599
326717
|
const prevRouteSignatures = /* @__PURE__ */ new Map();
|
|
326600
326718
|
const prevInteractionCountsByRoute = /* @__PURE__ */ new Map();
|
|
326601
326719
|
let previousCaptureRoute = null;
|
|
@@ -326606,6 +326724,16 @@ ${inbox_tools_js_1.EMAIL_VERIFICATION_UNAVAILABLE_OVERRIDE_BLOCK}` : ""}`;
|
|
|
326606
326724
|
let finishBounce = (0, survey_link_frontier_js_1.initialFinishBounceState)();
|
|
326607
326725
|
let lastLandedPageUrl = null;
|
|
326608
326726
|
const state2 = (0, exploration_state_js_1.createExplorationState)();
|
|
326727
|
+
const buildUnexercisedPrimarySummary = () => {
|
|
326728
|
+
if (!primaryActionCoverageGapEnabled)
|
|
326729
|
+
return void 0;
|
|
326730
|
+
const fa = options?.focusAreas ?? [];
|
|
326731
|
+
if (fa.length === 0)
|
|
326732
|
+
return void 0;
|
|
326733
|
+
const otherRoutes = options?.featureContext?.otherFeatures?.flatMap((f) => f.pages);
|
|
326734
|
+
const summary2 = (0, primary_action_ledger_js_1.summarizeUnexercisedPrimaries)(state2.actionLedger, (r) => (0, exploration_parser_js_1.routeIsWithinFocusAreas)(r, fa, otherRoutes), 12);
|
|
326735
|
+
return summary2.length > 0 ? summary2 : void 0;
|
|
326736
|
+
};
|
|
326609
326737
|
function ingestLinksFromText(rawText) {
|
|
326610
326738
|
if (!linkFrontierEnabled || !rawText || !rawText.includes("- /url:"))
|
|
326611
326739
|
return;
|
|
@@ -328836,6 +328964,7 @@ Exploration ended after ${iteration} iterations without calling finish_explorati
|
|
|
328836
328964
|
summary: state2.summary || summary,
|
|
328837
328965
|
collectedPages: state2.collectedPages,
|
|
328838
328966
|
collectedTransitions: state2.collectedTransitions,
|
|
328967
|
+
unexercisedPrimaryActions: buildUnexercisedPrimarySummary(),
|
|
328839
328968
|
iterationTimeline: iterationTimeline.length > 0 ? iterationTimeline : void 0
|
|
328840
328969
|
};
|
|
328841
328970
|
}
|
|
@@ -328930,6 +329059,7 @@ Fatal error: ${msg}`);
|
|
|
328930
329059
|
summary: `Fatal error: ${msg}`,
|
|
328931
329060
|
collectedPages: state2.collectedPages,
|
|
328932
329061
|
collectedTransitions: state2.collectedTransitions,
|
|
329062
|
+
unexercisedPrimaryActions: buildUnexercisedPrimarySummary(),
|
|
328933
329063
|
iterationTimeline: iterationTimeline.length > 0 ? iterationTimeline : void 0,
|
|
328934
329064
|
truncated: true,
|
|
328935
329065
|
truncatedReason,
|
|
@@ -337116,6 +337246,7 @@ var require_discover_agent = __commonJS({
|
|
|
337116
337246
|
var discover_planner_js_1 = require_discover_planner();
|
|
337117
337247
|
var discover_generator_js_1 = require_discover_generator();
|
|
337118
337248
|
var discover_ai_planner_js_1 = require_discover_ai_planner();
|
|
337249
|
+
var primary_action_ledger_js_1 = require_primary_action_ledger();
|
|
337119
337250
|
var discovery_lint_flags_js_1 = require_discovery_lint_flags();
|
|
337120
337251
|
var verification_inbox_js_1 = require_verification_inbox();
|
|
337121
337252
|
var inbox_tools_js_1 = require_inbox_tools();
|
|
@@ -338418,6 +338549,16 @@ Plan: ${planResult.scenarios.length} scenarios (budget: ${featureTestBudget})`);
|
|
|
338418
338549
|
checkpointFallbackReason = planResult.fallbackReason;
|
|
338419
338550
|
plannerWarning = planResult.warning;
|
|
338420
338551
|
plannerCoverageGaps = planResult.coverageGaps;
|
|
338552
|
+
const unexercisedPrimaries = exploreResult.unexercisedPrimaryActions ?? [];
|
|
338553
|
+
if (unexercisedPrimaries.length > 0) {
|
|
338554
|
+
const plannedText = planResult.scenarios.map((s) => `${s.name} ${s.goal ?? ""} ${(s.targetPages ?? []).join(" ")}`).join(" ");
|
|
338555
|
+
const ledgerGaps = (0, primary_action_ledger_js_1.buildUnexercisedPrimaryCoverageGaps)(unexercisedPrimaries, plannedText, (/* @__PURE__ */ new Date()).toISOString());
|
|
338556
|
+
if (ledgerGaps.length > 0) {
|
|
338557
|
+
plannerCoverageGaps = [...plannerCoverageGaps ?? [], ...ledgerGaps];
|
|
338558
|
+
}
|
|
338559
|
+
const suppressed = unexercisedPrimaries.length - ledgerGaps.length;
|
|
338560
|
+
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)` : ""}`);
|
|
338561
|
+
}
|
|
338421
338562
|
}
|
|
338422
338563
|
if (callbackOptions?.onPlanReady) {
|
|
338423
338564
|
try {
|
|
@@ -352109,16 +352250,16 @@ var require_live_browser_registry = __commonJS({
|
|
|
352109
352250
|
"use strict";
|
|
352110
352251
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
352111
352252
|
exports.liveBrowserRegistry = void 0;
|
|
352253
|
+
var shared_1 = (init_dist(), __toCommonJS(dist_exports));
|
|
352112
352254
|
var THUMBNAIL_INTERVAL_MS = parseInt(process.env.LIVE_BROWSER_THUMBNAIL_INTERVAL_MS ?? "4000", 10) || 4e3;
|
|
352113
352255
|
var THUMBNAIL_QUALITY = parseInt(process.env.LIVE_BROWSER_THUMBNAIL_QUALITY ?? "60", 10) || 60;
|
|
352114
|
-
var BROWSER_THUMBNAIL_MAX_BASE64_BYTES = parseInt(process.env.LIVE_BROWSER_THUMBNAIL_MAX_BASE64_BYTES ?? "400000", 10) || 4e5;
|
|
352115
|
-
var MOBILE_THUMBNAIL_MAX_BASE64_BYTES = parseInt(process.env.LIVE_MOBILE_THUMBNAIL_MAX_BASE64_BYTES ?? "4500000", 10) || 45e5;
|
|
352116
352256
|
var STALE_SESSION_MS = parseInt(process.env.LIVE_BROWSER_STALE_MS ?? `${10 * 60 * 1e3}`, 10) || 10 * 60 * 1e3;
|
|
352117
352257
|
var MAX_SESSIONS = parseInt(process.env.LIVE_BROWSER_MAX_SESSIONS ?? "32", 10) || 32;
|
|
352118
352258
|
var LiveBrowserRegistry = class {
|
|
352119
352259
|
entries = /* @__PURE__ */ new Map();
|
|
352120
352260
|
snapshotTimer = null;
|
|
352121
352261
|
capturing = false;
|
|
352262
|
+
publisher = null;
|
|
352122
352263
|
/** Register a new session. Returns a handle the caller must hold and
|
|
352123
352264
|
* ultimately call .unregister() on. */
|
|
352124
352265
|
register(meta) {
|
|
@@ -352132,7 +352273,10 @@ var require_live_browser_registry = __commonJS({
|
|
|
352132
352273
|
lastUpdatedAt: now,
|
|
352133
352274
|
page: null,
|
|
352134
352275
|
isCapturing: false,
|
|
352135
|
-
pendingCapture: false
|
|
352276
|
+
pendingCapture: false,
|
|
352277
|
+
screencast: null,
|
|
352278
|
+
screencastStarting: false,
|
|
352279
|
+
lastPublishedAt: 0
|
|
352136
352280
|
};
|
|
352137
352281
|
this.entries.set(meta.sessionId, entry);
|
|
352138
352282
|
this.ensureSnapshotLoop();
|
|
@@ -352145,19 +352289,40 @@ var require_live_browser_registry = __commonJS({
|
|
|
352145
352289
|
unregister: () => this.unregister(meta.sessionId)
|
|
352146
352290
|
};
|
|
352147
352291
|
}
|
|
352292
|
+
/**
|
|
352293
|
+
* Install the real-time frame sink. Idempotent; the runner calls this once at
|
|
352294
|
+
* startup, before any session registers.
|
|
352295
|
+
*/
|
|
352296
|
+
setFramePublisher(publisher) {
|
|
352297
|
+
this.publisher = publisher;
|
|
352298
|
+
}
|
|
352148
352299
|
unregister(sessionId) {
|
|
352149
352300
|
const entry = this.entries.get(sessionId);
|
|
352150
352301
|
if (!entry)
|
|
352151
352302
|
return;
|
|
352152
|
-
|
|
352153
|
-
this.entries.delete(sessionId);
|
|
352303
|
+
this.dropEntry(sessionId, entry);
|
|
352154
352304
|
if (this.entries.size === 0)
|
|
352155
352305
|
this.stopSnapshotLoop();
|
|
352156
352306
|
}
|
|
352307
|
+
/**
|
|
352308
|
+
* The ONLY way an entry leaves `entries`. Centralised because a screencast
|
|
352309
|
+
* outlives the map otherwise: its frame handler closes over the entry rather
|
|
352310
|
+
* than looking it up, so an entry deleted without stopScreencast() leaves
|
|
352311
|
+
* Chromium encoding and acking frames for a page nobody is watching, holding
|
|
352312
|
+
* the CDP session and Page alive. Eviction and the staleness sweep both used
|
|
352313
|
+
* to delete directly and leaked exactly that way.
|
|
352314
|
+
*/
|
|
352315
|
+
dropEntry(sessionId, entry) {
|
|
352316
|
+
void this.stopScreencast(entry);
|
|
352317
|
+
entry.page = null;
|
|
352318
|
+
this.entries.delete(sessionId);
|
|
352319
|
+
}
|
|
352157
352320
|
setPage(sessionId, page) {
|
|
352158
352321
|
const entry = this.entries.get(sessionId);
|
|
352159
352322
|
if (!entry)
|
|
352160
352323
|
return;
|
|
352324
|
+
if (entry.screencast && entry.screencast.page !== page)
|
|
352325
|
+
void this.stopScreencast(entry);
|
|
352161
352326
|
entry.page = page;
|
|
352162
352327
|
entry.lastUpdatedAt = Date.now();
|
|
352163
352328
|
if (page) {
|
|
@@ -352167,6 +352332,119 @@ var require_live_browser_registry = __commonJS({
|
|
|
352167
352332
|
entry.currentUrl = url;
|
|
352168
352333
|
} catch {
|
|
352169
352334
|
}
|
|
352335
|
+
void this.startScreencast(entry, page);
|
|
352336
|
+
}
|
|
352337
|
+
}
|
|
352338
|
+
/**
|
|
352339
|
+
* Begin a CDP screencast for this page. Chromium then pushes a JPEG on every
|
|
352340
|
+
* paint, already downscaled to LIVE_FRAME_MAX_DIMENSION — which is the only
|
|
352341
|
+
* way this runner can downscale at all, since it ships no image library.
|
|
352342
|
+
*
|
|
352343
|
+
* Best-effort by design: any failure leaves `screencast` null and the polling
|
|
352344
|
+
* screenshot loop keeps serving the entry exactly as before.
|
|
352345
|
+
*/
|
|
352346
|
+
async startScreencast(entry, page) {
|
|
352347
|
+
if (!page || entry.screencast || entry.screencastStarting)
|
|
352348
|
+
return;
|
|
352349
|
+
if (page.isClosed?.())
|
|
352350
|
+
return;
|
|
352351
|
+
entry.screencastStarting = true;
|
|
352352
|
+
try {
|
|
352353
|
+
const client = await page.context().newCDPSession(page);
|
|
352354
|
+
const binding = { client, page, stopped: false };
|
|
352355
|
+
client.on("Page.screencastFrame", (params) => {
|
|
352356
|
+
void client.send("Page.screencastFrameAck", { sessionId: params.sessionId }).catch(() => {
|
|
352357
|
+
});
|
|
352358
|
+
if (binding.stopped)
|
|
352359
|
+
return;
|
|
352360
|
+
if (this.entries.get(entry.meta.sessionId) !== entry)
|
|
352361
|
+
return;
|
|
352362
|
+
if (typeof params.data !== "string" || params.data.length === 0)
|
|
352363
|
+
return;
|
|
352364
|
+
const width = params.metadata?.deviceWidth;
|
|
352365
|
+
const height = params.metadata?.deviceHeight;
|
|
352366
|
+
this.acceptFrame(entry, params.data, "image/jpeg", width, height);
|
|
352367
|
+
});
|
|
352368
|
+
page.once("close", () => {
|
|
352369
|
+
void this.stopScreencast(entry);
|
|
352370
|
+
});
|
|
352371
|
+
page.once("crash", () => {
|
|
352372
|
+
void this.stopScreencast(entry);
|
|
352373
|
+
});
|
|
352374
|
+
await client.send("Page.enable");
|
|
352375
|
+
await client.send("Page.startScreencast", {
|
|
352376
|
+
format: "jpeg",
|
|
352377
|
+
quality: shared_1.LIVE_FRAME_JPEG_QUALITY,
|
|
352378
|
+
// Absolute bounds, aspect-preserving — Chromium does the resize.
|
|
352379
|
+
maxWidth: shared_1.LIVE_FRAME_MAX_DIMENSION,
|
|
352380
|
+
maxHeight: shared_1.LIVE_FRAME_MAX_DIMENSION,
|
|
352381
|
+
everyNthFrame: 1
|
|
352382
|
+
});
|
|
352383
|
+
if (this.entries.get(entry.meta.sessionId) !== entry || entry.page !== page) {
|
|
352384
|
+
binding.stopped = true;
|
|
352385
|
+
await client.send("Page.stopScreencast").catch(() => {
|
|
352386
|
+
});
|
|
352387
|
+
await client.detach().catch(() => {
|
|
352388
|
+
});
|
|
352389
|
+
return;
|
|
352390
|
+
}
|
|
352391
|
+
entry.screencast = binding;
|
|
352392
|
+
} catch {
|
|
352393
|
+
} finally {
|
|
352394
|
+
entry.screencastStarting = false;
|
|
352395
|
+
}
|
|
352396
|
+
}
|
|
352397
|
+
async stopScreencast(entry) {
|
|
352398
|
+
const binding = entry.screencast;
|
|
352399
|
+
if (!binding || binding.stopped)
|
|
352400
|
+
return;
|
|
352401
|
+
binding.stopped = true;
|
|
352402
|
+
entry.screencast = null;
|
|
352403
|
+
try {
|
|
352404
|
+
await binding.client.send("Page.stopScreencast");
|
|
352405
|
+
} catch {
|
|
352406
|
+
}
|
|
352407
|
+
try {
|
|
352408
|
+
await binding.client.detach();
|
|
352409
|
+
} catch {
|
|
352410
|
+
}
|
|
352411
|
+
}
|
|
352412
|
+
/**
|
|
352413
|
+
* Record a freshly captured frame and forward it to the real-time publisher.
|
|
352414
|
+
* The stored copy (used by the heartbeat fallback) is only replaced when it
|
|
352415
|
+
* fits the heartbeat budget, but the publisher — which has a far larger budget
|
|
352416
|
+
* of its own — always sees the frame.
|
|
352417
|
+
*/
|
|
352418
|
+
acceptFrame(entry, base64, mimeType, width, height) {
|
|
352419
|
+
const now = Date.now();
|
|
352420
|
+
entry.lastUpdatedAt = now;
|
|
352421
|
+
if (base64.length <= shared_1.LIVE_THUMBNAIL_MAX_BASE64_BYTES) {
|
|
352422
|
+
entry.thumbnailJpegBase64 = base64;
|
|
352423
|
+
entry.thumbnailCapturedAt = now;
|
|
352424
|
+
if (width !== void 0)
|
|
352425
|
+
entry.thumbnailWidth = width;
|
|
352426
|
+
if (height !== void 0)
|
|
352427
|
+
entry.thumbnailHeight = height;
|
|
352428
|
+
}
|
|
352429
|
+
const publisher = this.publisher;
|
|
352430
|
+
if (!publisher)
|
|
352431
|
+
return;
|
|
352432
|
+
if (now - entry.lastPublishedAt < shared_1.LIVE_FRAME_MIN_PUSH_INTERVAL_MS)
|
|
352433
|
+
return;
|
|
352434
|
+
entry.lastPublishedAt = now;
|
|
352435
|
+
try {
|
|
352436
|
+
publisher({
|
|
352437
|
+
sessionId: entry.meta.sessionId,
|
|
352438
|
+
projectId: entry.meta.projectId,
|
|
352439
|
+
runId: entry.meta.runId,
|
|
352440
|
+
capturedAt: now,
|
|
352441
|
+
mimeType,
|
|
352442
|
+
width,
|
|
352443
|
+
height,
|
|
352444
|
+
surface: entry.meta.surface,
|
|
352445
|
+
base64
|
|
352446
|
+
});
|
|
352447
|
+
} catch {
|
|
352170
352448
|
}
|
|
352171
352449
|
}
|
|
352172
352450
|
setCurrentUrl(sessionId, url) {
|
|
@@ -352178,7 +352456,7 @@ var require_live_browser_registry = __commonJS({
|
|
|
352178
352456
|
const previousUrl = entry.currentUrl;
|
|
352179
352457
|
entry.currentUrl = url;
|
|
352180
352458
|
entry.lastUpdatedAt = Date.now();
|
|
352181
|
-
if (url && previousUrl && url !== previousUrl) {
|
|
352459
|
+
if (url && previousUrl && url !== previousUrl && !entry.screencast) {
|
|
352182
352460
|
entry.pendingCapture = true;
|
|
352183
352461
|
const schedule = (delay) => {
|
|
352184
352462
|
setTimeout(() => {
|
|
@@ -352199,6 +352477,11 @@ var require_live_browser_registry = __commonJS({
|
|
|
352199
352477
|
schedule(100);
|
|
352200
352478
|
}
|
|
352201
352479
|
}
|
|
352480
|
+
/**
|
|
352481
|
+
* Ingest a frame captured outside this module — the mobile mirror and the
|
|
352482
|
+
* spawned `playwright test` child (whose Page lives in another process, so no
|
|
352483
|
+
* CDP screencast is reachable from here) both arrive this way.
|
|
352484
|
+
*/
|
|
352202
352485
|
updateSnapshot(sessionId, snapshot) {
|
|
352203
352486
|
const entry = this.entries.get(sessionId);
|
|
352204
352487
|
if (!entry)
|
|
@@ -352207,12 +352490,10 @@ var require_live_browser_registry = __commonJS({
|
|
|
352207
352490
|
entry.currentUrl = snapshot.currentUrl;
|
|
352208
352491
|
}
|
|
352209
352492
|
const thumbnail = snapshot.thumbnailJpegBase64;
|
|
352210
|
-
|
|
352211
|
-
|
|
352212
|
-
|
|
352213
|
-
entry
|
|
352214
|
-
entry.thumbnailWidth = typeof snapshot.thumbnailWidth === "number" && Number.isFinite(snapshot.thumbnailWidth) ? snapshot.thumbnailWidth : void 0;
|
|
352215
|
-
entry.thumbnailHeight = typeof snapshot.thumbnailHeight === "number" && Number.isFinite(snapshot.thumbnailHeight) ? snapshot.thumbnailHeight : void 0;
|
|
352493
|
+
if (thumbnail) {
|
|
352494
|
+
const width = typeof snapshot.thumbnailWidth === "number" && Number.isFinite(snapshot.thumbnailWidth) ? snapshot.thumbnailWidth : void 0;
|
|
352495
|
+
const height = typeof snapshot.thumbnailHeight === "number" && Number.isFinite(snapshot.thumbnailHeight) ? snapshot.thumbnailHeight : void 0;
|
|
352496
|
+
this.acceptFrame(entry, thumbnail, snapshot.mimeType ?? "image/jpeg", width, height);
|
|
352216
352497
|
}
|
|
352217
352498
|
entry.lastUpdatedAt = Date.now();
|
|
352218
352499
|
}
|
|
@@ -352255,9 +352536,11 @@ var require_live_browser_registry = __commonJS({
|
|
|
352255
352536
|
async captureAllNow() {
|
|
352256
352537
|
await this.captureAll();
|
|
352257
352538
|
}
|
|
352258
|
-
/** Stop the snapshot loop and clear all entries. Idempotent. */
|
|
352539
|
+
/** Stop the snapshot loop, tear down every screencast, and clear all entries. Idempotent. */
|
|
352259
352540
|
shutdown() {
|
|
352260
352541
|
this.stopSnapshotLoop();
|
|
352542
|
+
for (const entry of this.entries.values())
|
|
352543
|
+
void this.stopScreencast(entry);
|
|
352261
352544
|
this.entries.clear();
|
|
352262
352545
|
}
|
|
352263
352546
|
ensureSnapshotLoop() {
|
|
@@ -352285,15 +352568,13 @@ var require_live_browser_registry = __commonJS({
|
|
|
352285
352568
|
}
|
|
352286
352569
|
}
|
|
352287
352570
|
if (oldestKey) {
|
|
352288
|
-
this.entries.get(oldestKey)
|
|
352289
|
-
this.entries.delete(oldestKey);
|
|
352571
|
+
this.dropEntry(oldestKey, this.entries.get(oldestKey));
|
|
352290
352572
|
}
|
|
352291
352573
|
}
|
|
352292
352574
|
sweepStale(now) {
|
|
352293
352575
|
for (const [key, entry] of this.entries) {
|
|
352294
352576
|
if (now - entry.lastUpdatedAt > STALE_SESSION_MS) {
|
|
352295
|
-
|
|
352296
|
-
this.entries.delete(key);
|
|
352577
|
+
this.dropEntry(key, entry);
|
|
352297
352578
|
}
|
|
352298
352579
|
}
|
|
352299
352580
|
}
|
|
@@ -352334,6 +352615,11 @@ var require_live_browser_registry = __commonJS({
|
|
|
352334
352615
|
entry.currentUrl = url;
|
|
352335
352616
|
} catch {
|
|
352336
352617
|
}
|
|
352618
|
+
if (entry.screencast) {
|
|
352619
|
+
if (entry.thumbnailJpegBase64)
|
|
352620
|
+
entry.thumbnailCapturedAt = Date.now();
|
|
352621
|
+
return;
|
|
352622
|
+
}
|
|
352337
352623
|
const buf = await page.screenshot({
|
|
352338
352624
|
type: "jpeg",
|
|
352339
352625
|
quality: THUMBNAIL_QUALITY,
|
|
@@ -352341,17 +352627,8 @@ var require_live_browser_registry = __commonJS({
|
|
|
352341
352627
|
timeout: 3e3,
|
|
352342
352628
|
scale: "css"
|
|
352343
352629
|
});
|
|
352344
|
-
const base64 = buf.toString("base64");
|
|
352345
|
-
if (base64.length > BROWSER_THUMBNAIL_MAX_BASE64_BYTES) {
|
|
352346
|
-
return;
|
|
352347
|
-
}
|
|
352348
|
-
entry.thumbnailJpegBase64 = base64;
|
|
352349
|
-
entry.thumbnailCapturedAt = Date.now();
|
|
352350
352630
|
const viewport = page.viewportSize?.();
|
|
352351
|
-
|
|
352352
|
-
entry.thumbnailWidth = viewport.width;
|
|
352353
|
-
entry.thumbnailHeight = viewport.height;
|
|
352354
|
-
}
|
|
352631
|
+
this.acceptFrame(entry, buf.toString("base64"), "image/jpeg", viewport?.width, viewport?.height);
|
|
352355
352632
|
} catch {
|
|
352356
352633
|
} finally {
|
|
352357
352634
|
entry.isCapturing = false;
|
|
@@ -353065,7 +353342,7 @@ var require_package4 = __commonJS({
|
|
|
353065
353342
|
"package.json"(exports, module) {
|
|
353066
353343
|
module.exports = {
|
|
353067
353344
|
name: "@validate.qa/runner",
|
|
353068
|
-
version: "1.0.
|
|
353345
|
+
version: "1.0.80",
|
|
353069
353346
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
353070
353347
|
bin: {
|
|
353071
353348
|
"validate-runner": "dist/cli.js"
|
|
@@ -353334,6 +353611,9 @@ async function enforceRunnerUpToDate(currentVersion, opts = {}) {
|
|
|
353334
353611
|
return result;
|
|
353335
353612
|
}
|
|
353336
353613
|
|
|
353614
|
+
// src/runner.ts
|
|
353615
|
+
init_dist();
|
|
353616
|
+
|
|
353337
353617
|
// src/services/appium-executor.ts
|
|
353338
353618
|
init_dist();
|
|
353339
353619
|
|
|
@@ -353857,6 +354137,24 @@ async function getScreenshot(sessionPath) {
|
|
|
353857
354137
|
return null;
|
|
353858
354138
|
}
|
|
353859
354139
|
}
|
|
354140
|
+
async function applyMjpegSettings(sessionPath, settings) {
|
|
354141
|
+
try {
|
|
354142
|
+
await appiumRequest(`${sessionPath}/appium/settings`, {
|
|
354143
|
+
method: "POST",
|
|
354144
|
+
body: JSON.stringify({
|
|
354145
|
+
settings: {
|
|
354146
|
+
mjpegServerFramerate: settings.framerate,
|
|
354147
|
+
mjpegScalingFactor: settings.scalingFactor,
|
|
354148
|
+
mjpegServerScreenshotQuality: settings.quality
|
|
354149
|
+
}
|
|
354150
|
+
})
|
|
354151
|
+
});
|
|
354152
|
+
return true;
|
|
354153
|
+
} catch (err) {
|
|
354154
|
+
if (isAppiumDriverDeadError(err)) throw err;
|
|
354155
|
+
return false;
|
|
354156
|
+
}
|
|
354157
|
+
}
|
|
353860
354158
|
async function getPageSource(sessionPath) {
|
|
353861
354159
|
try {
|
|
353862
354160
|
const value = readValue(await appiumRequest(`${sessionPath}/source`));
|
|
@@ -356368,7 +356666,15 @@ function mobileParallelSessionCapabilities(platform3, portOffset = 0, deviceId =
|
|
|
356368
356666
|
return (Number.isFinite(parsed) ? parsed : fallback) + offset;
|
|
356369
356667
|
};
|
|
356370
356668
|
if (platform3 === "ANDROID") {
|
|
356371
|
-
return {
|
|
356669
|
+
return {
|
|
356670
|
+
"appium:systemPort": readPort("APPIUM_SYSTEM_PORT", 8200),
|
|
356671
|
+
// Host port that UiAutomator2 forwards the on-device MJPEG broadcaster
|
|
356672
|
+
// (device port 7810) to. Without this the live mirror has to poll
|
|
356673
|
+
// WebDriver /screenshot for full-resolution PNGs, which the runner cannot
|
|
356674
|
+
// downscale itself. Base differs from iOS's 9100 so a concurrent
|
|
356675
|
+
// Android+iOS pair can never collide on a shared slot offset.
|
|
356676
|
+
"appium:mjpegServerPort": readPort("APPIUM_ANDROID_MJPEG_SERVER_PORT", 7810)
|
|
356677
|
+
};
|
|
356372
356678
|
}
|
|
356373
356679
|
const safeDevice = deviceId.replace(/[^A-Za-z0-9._-]+/g, "-") || `device-${offset}`;
|
|
356374
356680
|
const derivedDataRoot = env.APPIUM_DERIVED_DATA_PATH?.trim() || join5(tmpdir3(), "validateqa-wda");
|
|
@@ -357623,6 +357929,15 @@ function acquireControlPortSlot(deviceId) {
|
|
|
357623
357929
|
function releaseControlPortSlot(deviceId) {
|
|
357624
357930
|
controlPortSlotsByDevice.delete(deviceId);
|
|
357625
357931
|
}
|
|
357932
|
+
function getControlPortSlot(deviceId) {
|
|
357933
|
+
return controlPortSlotsByDevice.get(deviceId);
|
|
357934
|
+
}
|
|
357935
|
+
function mobileMjpegPortForDevice(platform3, deviceId, env = process.env) {
|
|
357936
|
+
const slot = getControlPortSlot(deviceId);
|
|
357937
|
+
if (slot === void 0) return null;
|
|
357938
|
+
const port = mobileParallelSessionCapabilities(platform3, slot, deviceId, env)["appium:mjpegServerPort"];
|
|
357939
|
+
return typeof port === "number" && Number.isFinite(port) ? port : null;
|
|
357940
|
+
}
|
|
357626
357941
|
var PINNED_DEVICE_SNAPSHOT_TTL_MS = 25 * 6e4;
|
|
357627
357942
|
var pinnedDeviceSnapshot = null;
|
|
357628
357943
|
async function executeMobileTest(options) {
|
|
@@ -358567,8 +358882,103 @@ import { randomBytes, timingSafeEqual } from "crypto";
|
|
|
358567
358882
|
import { homedir as homedir2 } from "os";
|
|
358568
358883
|
import { join as join6, dirname } from "path";
|
|
358569
358884
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
358885
|
+
|
|
358886
|
+
// src/services/mobile/mjpeg-stream.ts
|
|
358887
|
+
init_dist();
|
|
358888
|
+
import { request as httpRequest } from "http";
|
|
358889
|
+
var JPEG_SOI = Buffer.from([255, 216]);
|
|
358890
|
+
var JPEG_EOI = Buffer.from([255, 217]);
|
|
358891
|
+
function startMjpegStream(options) {
|
|
358892
|
+
const { port, host = "127.0.0.1", onFrame, onError } = options;
|
|
358893
|
+
let stopped = false;
|
|
358894
|
+
let req = null;
|
|
358895
|
+
let response = null;
|
|
358896
|
+
let buffer = Buffer.alloc(0);
|
|
358897
|
+
const fail = (error2) => {
|
|
358898
|
+
if (stopped) return;
|
|
358899
|
+
stopped = true;
|
|
358900
|
+
try {
|
|
358901
|
+
req?.destroy();
|
|
358902
|
+
} catch {
|
|
358903
|
+
}
|
|
358904
|
+
try {
|
|
358905
|
+
response?.destroy();
|
|
358906
|
+
} catch {
|
|
358907
|
+
}
|
|
358908
|
+
buffer = Buffer.alloc(0);
|
|
358909
|
+
onError?.(error2);
|
|
358910
|
+
};
|
|
358911
|
+
const consume = (chunk) => {
|
|
358912
|
+
if (stopped) return;
|
|
358913
|
+
buffer = buffer.length === 0 ? chunk : Buffer.concat([buffer, chunk]);
|
|
358914
|
+
for (; ; ) {
|
|
358915
|
+
const start = buffer.indexOf(JPEG_SOI);
|
|
358916
|
+
if (start < 0) {
|
|
358917
|
+
if (buffer.length > 1) buffer = buffer.subarray(buffer.length - 1);
|
|
358918
|
+
return;
|
|
358919
|
+
}
|
|
358920
|
+
const end = buffer.indexOf(JPEG_EOI, start + JPEG_SOI.length);
|
|
358921
|
+
if (end < 0) {
|
|
358922
|
+
if (start > 0) buffer = buffer.subarray(start);
|
|
358923
|
+
if (buffer.length > MOBILE_MJPEG_MAX_FRAME_BYTES) buffer = Buffer.alloc(0);
|
|
358924
|
+
return;
|
|
358925
|
+
}
|
|
358926
|
+
const frameEnd = end + JPEG_EOI.length;
|
|
358927
|
+
const frame = buffer.subarray(start, frameEnd);
|
|
358928
|
+
buffer = buffer.subarray(frameEnd);
|
|
358929
|
+
try {
|
|
358930
|
+
onFrame(frame);
|
|
358931
|
+
} catch {
|
|
358932
|
+
}
|
|
358933
|
+
}
|
|
358934
|
+
};
|
|
358935
|
+
try {
|
|
358936
|
+
req = httpRequest(
|
|
358937
|
+
{ host, port, path: "/", method: "GET", headers: { Connection: "keep-alive" } },
|
|
358938
|
+
(res) => {
|
|
358939
|
+
response = res;
|
|
358940
|
+
if (stopped) {
|
|
358941
|
+
res.destroy();
|
|
358942
|
+
return;
|
|
358943
|
+
}
|
|
358944
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
358945
|
+
fail(new Error(`MJPEG broadcaster returned HTTP ${res.statusCode}`));
|
|
358946
|
+
res.destroy();
|
|
358947
|
+
return;
|
|
358948
|
+
}
|
|
358949
|
+
res.on("data", (chunk) => consume(chunk));
|
|
358950
|
+
res.on("end", () => fail(new Error("MJPEG stream ended")));
|
|
358951
|
+
res.on("error", (err) => fail(err));
|
|
358952
|
+
}
|
|
358953
|
+
);
|
|
358954
|
+
req.on("error", (err) => fail(err));
|
|
358955
|
+
req.setTimeout(1e4, () => fail(new Error("MJPEG connect timeout")));
|
|
358956
|
+
req.end();
|
|
358957
|
+
} catch (err) {
|
|
358958
|
+
fail(err instanceof Error ? err : new Error(String(err)));
|
|
358959
|
+
}
|
|
358960
|
+
return {
|
|
358961
|
+
stop: () => {
|
|
358962
|
+
if (stopped) return;
|
|
358963
|
+
stopped = true;
|
|
358964
|
+
try {
|
|
358965
|
+
req?.destroy();
|
|
358966
|
+
} catch {
|
|
358967
|
+
}
|
|
358968
|
+
try {
|
|
358969
|
+
response?.destroy();
|
|
358970
|
+
} catch {
|
|
358971
|
+
}
|
|
358972
|
+
buffer = Buffer.alloc(0);
|
|
358973
|
+
},
|
|
358974
|
+
isActive: () => !stopped
|
|
358975
|
+
};
|
|
358976
|
+
}
|
|
358977
|
+
|
|
358978
|
+
// src/services/mobile-bridge.ts
|
|
358570
358979
|
init_dist();
|
|
358571
358980
|
var MOBILE_BRIDGE_PORT2 = 9515;
|
|
358981
|
+
var RELAY_DEMAND_POLL_MS = 2e3;
|
|
358572
358982
|
var MAX_REQUEST_BODY_BYTES = 64 * 1024;
|
|
358573
358983
|
var MAX_TEXT_INPUT_LENGTH = 4e3;
|
|
358574
358984
|
var MAX_APP_ID_LENGTH = 255;
|
|
@@ -359080,6 +359490,18 @@ async function handleSessionStop(_req, res) {
|
|
|
359080
359490
|
sendJSON(res, 200, { ok: true });
|
|
359081
359491
|
}
|
|
359082
359492
|
var runnerChannels = /* @__PURE__ */ new Map();
|
|
359493
|
+
var relayDemandTimer = null;
|
|
359494
|
+
function setMirrorRelay(deviceId, relay) {
|
|
359495
|
+
const ch = runnerChannels.get(deviceId);
|
|
359496
|
+
if (!ch) return;
|
|
359497
|
+
ch.relay = relay;
|
|
359498
|
+
if (relay) {
|
|
359499
|
+
ensureRelayDemandSupervisor();
|
|
359500
|
+
if (hasChannelDemand(ch)) ensureChannelPolling(ch);
|
|
359501
|
+
} else {
|
|
359502
|
+
stopChannelCaptureIfIdle(ch);
|
|
359503
|
+
}
|
|
359504
|
+
}
|
|
359083
359505
|
function readDeviceIdParam(req) {
|
|
359084
359506
|
try {
|
|
359085
359507
|
const url = new URL(req.url || "", "http://localhost");
|
|
@@ -359111,7 +359533,7 @@ function writeFrameToChannelClient(ch, client, frame) {
|
|
|
359111
359533
|
} catch {
|
|
359112
359534
|
}
|
|
359113
359535
|
ch.clients.delete(client);
|
|
359114
|
-
|
|
359536
|
+
stopChannelCaptureIfIdle(ch);
|
|
359115
359537
|
return;
|
|
359116
359538
|
}
|
|
359117
359539
|
client.write(`data: ${JSON.stringify(frame)}
|
|
@@ -359120,16 +359542,95 @@ function writeFrameToChannelClient(ch, client, frame) {
|
|
|
359120
359542
|
} catch {
|
|
359121
359543
|
}
|
|
359122
359544
|
}
|
|
359545
|
+
function broadcastChannelFrame(ch, frame) {
|
|
359546
|
+
ch.lastFrame = frame;
|
|
359547
|
+
for (const client of ch.clients) writeFrameToChannelClient(ch, client, frame);
|
|
359548
|
+
if (ch.relay && frame.screenshot) {
|
|
359549
|
+
try {
|
|
359550
|
+
ch.relay.onFrame(frame.screenshot, frame.mimeType ?? "image/png");
|
|
359551
|
+
} catch {
|
|
359552
|
+
}
|
|
359553
|
+
}
|
|
359554
|
+
}
|
|
359555
|
+
function tryStartChannelMjpeg(ch) {
|
|
359556
|
+
if (ch.mjpeg || ch.mjpegStarting || ch.mjpegUnavailable) return;
|
|
359557
|
+
const port = mobileMjpegPortForDevice(ch.platform, ch.deviceId);
|
|
359558
|
+
if (port === null) {
|
|
359559
|
+
ch.mjpegUnavailable = true;
|
|
359560
|
+
return;
|
|
359561
|
+
}
|
|
359562
|
+
ch.mjpegStarting = true;
|
|
359563
|
+
const handle = startMjpegStream({
|
|
359564
|
+
port,
|
|
359565
|
+
onFrame: (jpeg) => {
|
|
359566
|
+
const isOwned = ch.mjpeg === handle;
|
|
359567
|
+
const isCandidate = ch.mjpeg === null && ch.mjpegStarting;
|
|
359568
|
+
if (!isOwned && !isCandidate) return;
|
|
359569
|
+
ch.mjpegLastFrameAt = Date.now();
|
|
359570
|
+
ch.nullTicks = 0;
|
|
359571
|
+
if (isCandidate) {
|
|
359572
|
+
ch.mjpeg = handle;
|
|
359573
|
+
ch.mjpegStarting = false;
|
|
359574
|
+
if (ch.pollTimer) {
|
|
359575
|
+
clearInterval(ch.pollTimer);
|
|
359576
|
+
ch.pollTimer = null;
|
|
359577
|
+
}
|
|
359578
|
+
}
|
|
359579
|
+
broadcastChannelFrame(ch, {
|
|
359580
|
+
timestamp: ch.mjpegLastFrameAt,
|
|
359581
|
+
screenshot: jpeg.toString("base64"),
|
|
359582
|
+
source: null,
|
|
359583
|
+
mimeType: "image/jpeg"
|
|
359584
|
+
});
|
|
359585
|
+
},
|
|
359586
|
+
onError: () => {
|
|
359587
|
+
const wasActive = ch.mjpeg === handle;
|
|
359588
|
+
ch.mjpeg = null;
|
|
359589
|
+
ch.mjpegStarting = false;
|
|
359590
|
+
ch.mjpegUnavailable = true;
|
|
359591
|
+
if (wasActive && hasChannelDemand(ch)) ensureChannelPolling(ch);
|
|
359592
|
+
}
|
|
359593
|
+
});
|
|
359594
|
+
const firstFrameDeadline = setTimeout(() => {
|
|
359595
|
+
if (ch.mjpeg) return;
|
|
359596
|
+
ch.mjpegStarting = false;
|
|
359597
|
+
ch.mjpegUnavailable = true;
|
|
359598
|
+
handle.stop();
|
|
359599
|
+
if (hasChannelDemand(ch)) ensureChannelPolling(ch);
|
|
359600
|
+
}, MOBILE_MJPEG_FIRST_FRAME_TIMEOUT_MS);
|
|
359601
|
+
firstFrameDeadline.unref?.();
|
|
359602
|
+
}
|
|
359603
|
+
function hasChannelDemand(ch) {
|
|
359604
|
+
if (ch.clients.size > 0) return true;
|
|
359605
|
+
if (!ch.relay) return false;
|
|
359606
|
+
try {
|
|
359607
|
+
return ch.relay.wanted();
|
|
359608
|
+
} catch {
|
|
359609
|
+
return false;
|
|
359610
|
+
}
|
|
359611
|
+
}
|
|
359612
|
+
function ensureRelayDemandSupervisor() {
|
|
359613
|
+
if (relayDemandTimer) return;
|
|
359614
|
+
relayDemandTimer = setInterval(() => {
|
|
359615
|
+
for (const ch of [...runnerChannels.values()]) {
|
|
359616
|
+
if (hasChannelDemand(ch)) ensureChannelPolling(ch);
|
|
359617
|
+
else stopChannelCaptureIfIdle(ch);
|
|
359618
|
+
}
|
|
359619
|
+
if (runnerChannels.size === 0 && relayDemandTimer) {
|
|
359620
|
+
clearInterval(relayDemandTimer);
|
|
359621
|
+
relayDemandTimer = null;
|
|
359622
|
+
}
|
|
359623
|
+
}, RELAY_DEMAND_POLL_MS);
|
|
359624
|
+
relayDemandTimer.unref?.();
|
|
359625
|
+
}
|
|
359123
359626
|
async function pollRunnerChannel(ch) {
|
|
359124
|
-
if (ch
|
|
359627
|
+
if (!hasChannelDemand(ch) || ch.polling) return;
|
|
359628
|
+
if (ch.mjpeg) return;
|
|
359125
359629
|
ch.polling = true;
|
|
359126
359630
|
try {
|
|
359127
359631
|
validateSessionId(ch.sessionId);
|
|
359128
|
-
const
|
|
359129
|
-
|
|
359130
|
-
getPageSource(`/session/${ch.sessionId}`)
|
|
359131
|
-
]);
|
|
359132
|
-
if (!screenshot && !source) {
|
|
359632
|
+
const screenshot = await getScreenshot(`/session/${ch.sessionId}`);
|
|
359633
|
+
if (!screenshot) {
|
|
359133
359634
|
ch.nullTicks++;
|
|
359134
359635
|
if (ch.nullTicks >= MIRROR_DEAD_SESSION_TICKS) {
|
|
359135
359636
|
const alive = await isSessionAlive(ch.sessionId);
|
|
@@ -359142,32 +359643,47 @@ async function pollRunnerChannel(ch) {
|
|
|
359142
359643
|
} else {
|
|
359143
359644
|
ch.nullTicks = 0;
|
|
359144
359645
|
}
|
|
359145
|
-
|
|
359146
|
-
|
|
359147
|
-
|
|
359646
|
+
broadcastChannelFrame(ch, {
|
|
359647
|
+
timestamp: Date.now(),
|
|
359648
|
+
screenshot: screenshot ?? null,
|
|
359649
|
+
source: null,
|
|
359650
|
+
mimeType: "image/png"
|
|
359651
|
+
});
|
|
359148
359652
|
} catch {
|
|
359149
359653
|
} finally {
|
|
359150
359654
|
ch.polling = false;
|
|
359151
359655
|
}
|
|
359152
359656
|
}
|
|
359153
359657
|
function ensureChannelPolling(ch) {
|
|
359154
|
-
if (ch
|
|
359658
|
+
if (!hasChannelDemand(ch)) return;
|
|
359659
|
+
tryStartChannelMjpeg(ch);
|
|
359660
|
+
if (ch.mjpeg || ch.pollTimer) return;
|
|
359155
359661
|
ch.pollTimer = setInterval(() => {
|
|
359156
359662
|
void pollRunnerChannel(ch);
|
|
359157
359663
|
}, MOBILE_SCREENSHOT_INTERVAL_MS);
|
|
359158
359664
|
ch.pollTimer.unref?.();
|
|
359159
359665
|
}
|
|
359160
|
-
function
|
|
359161
|
-
if (ch
|
|
359666
|
+
function stopChannelCaptureIfIdle(ch) {
|
|
359667
|
+
if (hasChannelDemand(ch)) return;
|
|
359668
|
+
if (ch.pollTimer) {
|
|
359162
359669
|
clearInterval(ch.pollTimer);
|
|
359163
359670
|
ch.pollTimer = null;
|
|
359164
359671
|
}
|
|
359672
|
+
if (ch.mjpeg) {
|
|
359673
|
+
ch.mjpeg.stop();
|
|
359674
|
+
ch.mjpeg = null;
|
|
359675
|
+
}
|
|
359165
359676
|
}
|
|
359166
359677
|
function closeRunnerChannel(ch) {
|
|
359167
359678
|
if (ch.pollTimer) {
|
|
359168
359679
|
clearInterval(ch.pollTimer);
|
|
359169
359680
|
ch.pollTimer = null;
|
|
359170
359681
|
}
|
|
359682
|
+
if (ch.mjpeg) {
|
|
359683
|
+
ch.mjpeg.stop();
|
|
359684
|
+
ch.mjpeg = null;
|
|
359685
|
+
}
|
|
359686
|
+
ch.relay = null;
|
|
359171
359687
|
for (const client of ch.clients) {
|
|
359172
359688
|
try {
|
|
359173
359689
|
client.end();
|
|
@@ -359176,7 +359692,7 @@ function closeRunnerChannel(ch) {
|
|
|
359176
359692
|
}
|
|
359177
359693
|
ch.clients.clear();
|
|
359178
359694
|
ch.lastFrame = null;
|
|
359179
|
-
runnerChannels.delete(ch.deviceId);
|
|
359695
|
+
if (runnerChannels.get(ch.deviceId) === ch) runnerChannels.delete(ch.deviceId);
|
|
359180
359696
|
}
|
|
359181
359697
|
function closeAllRunnerChannels() {
|
|
359182
359698
|
for (const ch of [...runnerChannels.values()]) closeRunnerChannel(ch);
|
|
@@ -359198,7 +359714,7 @@ async function serveRunnerMirror(req, res, ch) {
|
|
|
359198
359714
|
}
|
|
359199
359715
|
req.on("close", () => {
|
|
359200
359716
|
ch.clients.delete(res);
|
|
359201
|
-
|
|
359717
|
+
stopChannelCaptureIfIdle(ch);
|
|
359202
359718
|
});
|
|
359203
359719
|
}
|
|
359204
359720
|
async function handleMirror(req, res) {
|
|
@@ -359752,13 +360268,26 @@ function attachMirrorSession(sessionId, platform3, deviceId) {
|
|
|
359752
360268
|
lastFrame: null,
|
|
359753
360269
|
pollTimer: null,
|
|
359754
360270
|
nullTicks: 0,
|
|
359755
|
-
polling: false
|
|
360271
|
+
polling: false,
|
|
360272
|
+
mjpeg: null,
|
|
360273
|
+
mjpegLastFrameAt: 0,
|
|
360274
|
+
mjpegStarting: false,
|
|
360275
|
+
mjpegUnavailable: false,
|
|
360276
|
+
relay: null
|
|
359756
360277
|
};
|
|
359757
360278
|
ch.sessionId = sessionId;
|
|
359758
360279
|
ch.platform = platform3;
|
|
359759
360280
|
ch.nullTicks = 0;
|
|
360281
|
+
if (ch.mjpeg) {
|
|
360282
|
+
ch.mjpeg.stop();
|
|
360283
|
+
ch.mjpeg = null;
|
|
360284
|
+
}
|
|
360285
|
+
ch.mjpegUnavailable = false;
|
|
360286
|
+
ch.mjpegStarting = false;
|
|
359760
360287
|
runnerChannels.set(deviceId, ch);
|
|
359761
|
-
|
|
360288
|
+
void applyMjpegSettings(`/session/${sessionId}`, MOBILE_MJPEG_SETTINGS).catch(() => {
|
|
360289
|
+
});
|
|
360290
|
+
if (hasChannelDemand(ch)) {
|
|
359762
360291
|
ensureChannelPolling(ch);
|
|
359763
360292
|
void pollRunnerChannel(ch);
|
|
359764
360293
|
}
|
|
@@ -360730,6 +361259,13 @@ function attachMobileDiscoveryNetworkContext(calls, result, appId) {
|
|
|
360730
361259
|
}));
|
|
360731
361260
|
}
|
|
360732
361261
|
var runnerPackage = require_package4();
|
|
361262
|
+
var MIRROR_RELAY_FRESH_MS = 3e3;
|
|
361263
|
+
var liveFrameWatchedByProject = /* @__PURE__ */ new Map();
|
|
361264
|
+
var liveFrameIdleProbeAt = /* @__PURE__ */ new Map();
|
|
361265
|
+
var LIVE_FRAME_IDLE_PROBE_MAX_ENTRIES = 256;
|
|
361266
|
+
function isLiveFrameWatched(projectId) {
|
|
361267
|
+
return !projectId || liveFrameWatchedByProject.get(projectId) !== false;
|
|
361268
|
+
}
|
|
360733
361269
|
var SECRET_VAR_PATTERN = /password|secret|token|api[_]?key|pass\b|credential|bearer|pat\b/i;
|
|
360734
361270
|
var IS_CLOUD = parseBoolFlag(process.env.RUNNER_CLOUD);
|
|
360735
361271
|
var RUNNER_NAME = runnerPackage.name ?? "@validate.qa/runner";
|
|
@@ -361507,6 +362043,22 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
361507
362043
|
testCaseId: run2.testCaseId ?? void 0
|
|
361508
362044
|
});
|
|
361509
362045
|
requestLiveBrowserHeartbeat(true);
|
|
362046
|
+
let lastMirrorRelayFrameAt = 0;
|
|
362047
|
+
const attachMirrorWithRelay = (sessionId, platform3, deviceId) => {
|
|
362048
|
+
attachMirrorSession(sessionId, platform3, deviceId);
|
|
362049
|
+
if (!deviceId) return;
|
|
362050
|
+
setMirrorRelay(deviceId, {
|
|
362051
|
+
onFrame: (base64, mimeType) => {
|
|
362052
|
+
lastMirrorRelayFrameAt = Date.now();
|
|
362053
|
+
liveBrowserHandle.updateSnapshot({
|
|
362054
|
+
thumbnailJpegBase64: base64,
|
|
362055
|
+
thumbnailCapturedAt: Date.now(),
|
|
362056
|
+
mimeType
|
|
362057
|
+
});
|
|
362058
|
+
},
|
|
362059
|
+
wanted: () => isLiveFrameWatched(run2.projectId)
|
|
362060
|
+
});
|
|
362061
|
+
};
|
|
361510
362062
|
const liveBrowserHeartbeatInterval = setInterval(() => {
|
|
361511
362063
|
liveBrowserHandle.patch({});
|
|
361512
362064
|
requestLiveBrowserHeartbeat();
|
|
@@ -362410,7 +362962,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
362410
362962
|
deeplink: harvestTarget.deeplink
|
|
362411
362963
|
},
|
|
362412
362964
|
onSessionReady: (sessionId, deviceId, sessionPlatform) => {
|
|
362413
|
-
|
|
362965
|
+
attachMirrorWithRelay(sessionId, sessionPlatform, deviceId);
|
|
362414
362966
|
const name = getCachedDeviceSnapshot().find((d) => d.id === deviceId)?.name;
|
|
362415
362967
|
liveBrowserHandle.patch({ deviceId, ...name ? { deviceName: name } : {} });
|
|
362416
362968
|
requestLiveBrowserHeartbeat(true);
|
|
@@ -362880,7 +363432,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
362880
363432
|
}
|
|
362881
363433
|
);
|
|
362882
363434
|
mobileOnLog(`Appium session created: ${session.sessionId} on device ${session.deviceId}`);
|
|
362883
|
-
|
|
363435
|
+
attachMirrorWithRelay(session.sessionId, platform3, session.deviceId);
|
|
362884
363436
|
const driver = new MobileBridgeDriver({
|
|
362885
363437
|
sessionId: session.sessionId,
|
|
362886
363438
|
platform: platform3,
|
|
@@ -362917,7 +363469,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
362917
363469
|
throw new Error("discovery teardown started during session recovery \u2014 replacement session closed");
|
|
362918
363470
|
}
|
|
362919
363471
|
session = replacement;
|
|
362920
|
-
|
|
363472
|
+
attachMirrorWithRelay(replacement.sessionId, platform3, replacement.deviceId);
|
|
362921
363473
|
liveBrowserHandle.patch({
|
|
362922
363474
|
deviceId: replacement.deviceId,
|
|
362923
363475
|
deviceName: replacement.deviceName
|
|
@@ -362938,10 +363490,16 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
362938
363490
|
// UI a reliable mobile fallback when the direct bridge SSE is
|
|
362939
363491
|
// still opening or blocked by the user's network/tunnel.
|
|
362940
363492
|
onScreenshot: (base64) => {
|
|
363493
|
+
if (Date.now() - lastMirrorRelayFrameAt < MIRROR_RELAY_FRESH_MS) {
|
|
363494
|
+
liveBrowserHandle.updateSnapshot({ currentUrl: mobileTarget.appId });
|
|
363495
|
+
return;
|
|
363496
|
+
}
|
|
362941
363497
|
liveBrowserHandle.updateSnapshot({
|
|
362942
363498
|
currentUrl: mobileTarget.appId,
|
|
362943
363499
|
thumbnailJpegBase64: base64,
|
|
362944
|
-
thumbnailCapturedAt: Date.now()
|
|
363500
|
+
thumbnailCapturedAt: Date.now(),
|
|
363501
|
+
// Appium's screenshot endpoint returns PNG despite the field name.
|
|
363502
|
+
mimeType: "image/png"
|
|
362945
363503
|
});
|
|
362946
363504
|
requestLiveBrowserHeartbeat();
|
|
362947
363505
|
},
|
|
@@ -363408,7 +363966,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
363408
363966
|
deeplink: target.deeplink
|
|
363409
363967
|
});
|
|
363410
363968
|
liveSession = opened;
|
|
363411
|
-
|
|
363969
|
+
attachMirrorWithRelay(opened.sessionId, target.platform, opened.deviceId);
|
|
363412
363970
|
bridgeDriver = new MobileBridgeDriver({
|
|
363413
363971
|
sessionId: opened.sessionId,
|
|
363414
363972
|
platform: target.platform,
|
|
@@ -363598,7 +364156,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
363598
364156
|
// Stream this run to its OWN per-device dashboard tile (keyed by the
|
|
363599
364157
|
// leased device id) so N parallel Appium runs each show a live mirror.
|
|
363600
364158
|
onSessionReady: (sessionId, deviceId, sessionPlatform) => {
|
|
363601
|
-
|
|
364159
|
+
attachMirrorWithRelay(sessionId, sessionPlatform, deviceId);
|
|
363602
364160
|
requestLiveBrowserHeartbeat(true);
|
|
363603
364161
|
},
|
|
363604
364162
|
onSessionEnding: (deviceId) => {
|
|
@@ -364186,6 +364744,41 @@ async function startRunner(opts) {
|
|
|
364186
364744
|
} catch {
|
|
364187
364745
|
}
|
|
364188
364746
|
};
|
|
364747
|
+
const liveFramePushInFlight = /* @__PURE__ */ new Set();
|
|
364748
|
+
const publishLiveFrame = (frame) => {
|
|
364749
|
+
if (!frame.projectId) return;
|
|
364750
|
+
if (liveFramePushInFlight.has(frame.sessionId)) return;
|
|
364751
|
+
if (!isLiveFrameWatched(frame.projectId)) {
|
|
364752
|
+
const lastProbeAt = liveFrameIdleProbeAt.get(frame.sessionId) ?? 0;
|
|
364753
|
+
if (frame.capturedAt - lastProbeAt < LIVE_FRAME_IDLE_PUSH_INTERVAL_MS) return;
|
|
364754
|
+
if (liveFrameIdleProbeAt.size >= LIVE_FRAME_IDLE_PROBE_MAX_ENTRIES) {
|
|
364755
|
+
const expiredBefore = frame.capturedAt - LIVE_FRAME_IDLE_PUSH_INTERVAL_MS;
|
|
364756
|
+
for (const [sessionId, probedAt] of liveFrameIdleProbeAt) {
|
|
364757
|
+
if (probedAt >= expiredBefore) break;
|
|
364758
|
+
liveFrameIdleProbeAt.delete(sessionId);
|
|
364759
|
+
}
|
|
364760
|
+
}
|
|
364761
|
+
liveFrameIdleProbeAt.set(frame.sessionId, frame.capturedAt);
|
|
364762
|
+
}
|
|
364763
|
+
liveFramePushInFlight.add(frame.sessionId);
|
|
364764
|
+
const frameProjectId = frame.projectId;
|
|
364765
|
+
void fetch(`${serverUrl}/api/runner/live-frame`, {
|
|
364766
|
+
method: "POST",
|
|
364767
|
+
headers,
|
|
364768
|
+
body: JSON.stringify(frame),
|
|
364769
|
+
signal: AbortSignal.timeout(5e3)
|
|
364770
|
+
}).then(async (res) => {
|
|
364771
|
+
if (!res.ok) return;
|
|
364772
|
+
const ack = await res.json().catch(() => null);
|
|
364773
|
+
if (ack && typeof ack.watching === "boolean") {
|
|
364774
|
+
liveFrameWatchedByProject.set(frameProjectId, ack.watching);
|
|
364775
|
+
}
|
|
364776
|
+
}).catch(() => {
|
|
364777
|
+
}).finally(() => {
|
|
364778
|
+
liveFramePushInFlight.delete(frame.sessionId);
|
|
364779
|
+
});
|
|
364780
|
+
};
|
|
364781
|
+
import_runner_core11.liveBrowserRegistry.setFramePublisher(publishLiveFrame);
|
|
364189
364782
|
let liveBrowserHeartbeatInFlight = false;
|
|
364190
364783
|
let liveBrowserHeartbeatPending = false;
|
|
364191
364784
|
let lastLiveBrowserHeartbeatAt = 0;
|