@validate.qa/runner 1.0.77 → 1.0.79
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 +496 -48
- package/dist/cli.mjs +494 -46
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1888,6 +1888,56 @@ 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_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_STALE_MS = readEnvInt("LIVE_FRAME_STALE_MS", 3e4);
|
|
1927
|
+
LIVE_FRAME_MAX_SESSIONS_PER_PROJECT = readEnvInt("LIVE_FRAME_MAX_SESSIONS_PER_PROJECT", 32);
|
|
1928
|
+
MOBILE_MJPEG_SETTINGS = {
|
|
1929
|
+
/** Screenshots per second the broadcaster emits (both drivers: range 1-60). */
|
|
1930
|
+
framerate: readEnvInt("MOBILE_MJPEG_FRAMERATE", 10),
|
|
1931
|
+
/** Downscale percentage. UiAutomator2 defaults to 50, XCUITest to 100. */
|
|
1932
|
+
scalingFactor: readEnvInt("MOBILE_MJPEG_SCALING_FACTOR", 50),
|
|
1933
|
+
/** JPEG compression factor (both drivers: range 1-100). */
|
|
1934
|
+
quality: readEnvInt("MOBILE_MJPEG_QUALITY", 50)
|
|
1935
|
+
};
|
|
1936
|
+
MOBILE_MJPEG_FIRST_FRAME_TIMEOUT_MS = readEnvInt("MOBILE_MJPEG_FIRST_FRAME_TIMEOUT_MS", 6e3);
|
|
1937
|
+
MOBILE_MJPEG_MAX_FRAME_BYTES = readEnvInt("MOBILE_MJPEG_MAX_FRAME_BYTES", 8 * 1024 * 1024);
|
|
1938
|
+
}
|
|
1939
|
+
});
|
|
1940
|
+
|
|
1891
1941
|
// ../shared/dist/project-key.js
|
|
1892
1942
|
function isValidProjectKey(key) {
|
|
1893
1943
|
return typeof key === "string" && PROJECT_KEY_PATTERN.test(key) && !RESERVED_PROJECT_KEYS.has(key);
|
|
@@ -6310,6 +6360,14 @@ __export(dist_exports, {
|
|
|
6310
6360
|
GROUP_LABELS: () => GROUP_LABELS,
|
|
6311
6361
|
LEGACY_TTS_MODEL_MAP: () => LEGACY_TTS_MODEL_MAP,
|
|
6312
6362
|
LEGACY_TTS_VOICE_ID_MAP: () => LEGACY_TTS_VOICE_ID_MAP,
|
|
6363
|
+
LIVE_FRAME_JPEG_QUALITY: () => LIVE_FRAME_JPEG_QUALITY,
|
|
6364
|
+
LIVE_FRAME_MAX_DIMENSION: () => LIVE_FRAME_MAX_DIMENSION,
|
|
6365
|
+
LIVE_FRAME_MAX_SESSIONS_PER_PROJECT: () => LIVE_FRAME_MAX_SESSIONS_PER_PROJECT,
|
|
6366
|
+
LIVE_FRAME_MIN_PUSH_INTERVAL_MS: () => LIVE_FRAME_MIN_PUSH_INTERVAL_MS,
|
|
6367
|
+
LIVE_FRAME_PUSH_MAX_BASE64_BYTES: () => LIVE_FRAME_PUSH_MAX_BASE64_BYTES,
|
|
6368
|
+
LIVE_FRAME_STALE_MS: () => LIVE_FRAME_STALE_MS,
|
|
6369
|
+
LIVE_FRAME_UNFOCUSED_INTERVAL_MS: () => LIVE_FRAME_UNFOCUSED_INTERVAL_MS,
|
|
6370
|
+
LIVE_THUMBNAIL_MAX_BASE64_BYTES: () => LIVE_THUMBNAIL_MAX_BASE64_BYTES,
|
|
6313
6371
|
MAX_CREDIT_TOPUP_CENTS: () => MAX_CREDIT_TOPUP_CENTS,
|
|
6314
6372
|
MAX_DEEPLINK_LENGTH: () => MAX_DEEPLINK_LENGTH,
|
|
6315
6373
|
MIN_CREDIT_TOPUP_CENTS: () => MIN_CREDIT_TOPUP_CENTS,
|
|
@@ -6321,6 +6379,9 @@ __export(dist_exports, {
|
|
|
6321
6379
|
MOBILE_DEFAULT_SWIPE_DISTANCE: () => MOBILE_DEFAULT_SWIPE_DISTANCE,
|
|
6322
6380
|
MOBILE_FOCUS_SETTLE_MS: () => MOBILE_FOCUS_SETTLE_MS,
|
|
6323
6381
|
MOBILE_MAX_SWIPE_DISTANCE: () => MOBILE_MAX_SWIPE_DISTANCE,
|
|
6382
|
+
MOBILE_MJPEG_FIRST_FRAME_TIMEOUT_MS: () => MOBILE_MJPEG_FIRST_FRAME_TIMEOUT_MS,
|
|
6383
|
+
MOBILE_MJPEG_MAX_FRAME_BYTES: () => MOBILE_MJPEG_MAX_FRAME_BYTES,
|
|
6384
|
+
MOBILE_MJPEG_SETTINGS: () => MOBILE_MJPEG_SETTINGS,
|
|
6324
6385
|
MOBILE_PLATFORMS: () => MOBILE_PLATFORMS,
|
|
6325
6386
|
MOBILE_POST_ACTION_SETTLE_MS: () => MOBILE_POST_ACTION_SETTLE_MS,
|
|
6326
6387
|
MOBILE_SCREENSHOT_INTERVAL_MS: () => MOBILE_SCREENSHOT_INTERVAL_MS,
|
|
@@ -6433,6 +6494,7 @@ __export(dist_exports, {
|
|
|
6433
6494
|
isTestReadyForPlatform: () => isTestReadyForPlatform,
|
|
6434
6495
|
isTrialExpired: () => isTrialExpired,
|
|
6435
6496
|
isUIVariant: () => isUIVariant,
|
|
6497
|
+
isValidLiveFrame: () => isValidLiveFrame,
|
|
6436
6498
|
isValidMobileAppId: () => isValidMobileAppId,
|
|
6437
6499
|
isValidProjectKey: () => isValidProjectKey,
|
|
6438
6500
|
isValidSttProvider: () => isValidSttProvider,
|
|
@@ -6495,6 +6557,7 @@ var init_dist = __esm({
|
|
|
6495
6557
|
"use strict";
|
|
6496
6558
|
init_types();
|
|
6497
6559
|
init_constants();
|
|
6560
|
+
init_live_frame();
|
|
6498
6561
|
init_project_key();
|
|
6499
6562
|
init_mobile_test_generation();
|
|
6500
6563
|
init_mobile_step_healer();
|
|
@@ -86290,11 +86353,25 @@ var require_api_call_redaction = __commonJS({
|
|
|
86290
86353
|
const lower = name.toLowerCase();
|
|
86291
86354
|
return headers.find((h) => h.name?.toLowerCase() === lower)?.value;
|
|
86292
86355
|
}
|
|
86356
|
+
var ENUM_LIKE_FIELD_NAME = /status|state|phase|role|tier|kind|mode|level|category|priority|visibility|^type$|_type$/i;
|
|
86357
|
+
var NEVER_SAMPLE_FIELD_NAME = /password|token|secret|api.?key|auth|bearer|credential|session|email|phone|address|ssn/i;
|
|
86358
|
+
var ENUM_LIKE_VALUE = /^[A-Za-z][A-Za-z0-9_-]{0,29}$/;
|
|
86359
|
+
function enumSampleHint(key, val) {
|
|
86360
|
+
if (typeof val !== "string")
|
|
86361
|
+
return null;
|
|
86362
|
+
if (NEVER_SAMPLE_FIELD_NAME.test(key))
|
|
86363
|
+
return null;
|
|
86364
|
+
if (!ENUM_LIKE_FIELD_NAME.test(key))
|
|
86365
|
+
return null;
|
|
86366
|
+
if (!ENUM_LIKE_VALUE.test(val))
|
|
86367
|
+
return null;
|
|
86368
|
+
return val;
|
|
86369
|
+
}
|
|
86293
86370
|
function extractBodyShape(body) {
|
|
86294
86371
|
if (!body)
|
|
86295
86372
|
return void 0;
|
|
86296
86373
|
try {
|
|
86297
|
-
let describeValue = function(val, depth) {
|
|
86374
|
+
let describeValue = function(val, depth, key) {
|
|
86298
86375
|
if (val === null)
|
|
86299
86376
|
return "null";
|
|
86300
86377
|
if (Array.isArray(val)) {
|
|
@@ -86311,10 +86388,16 @@ var require_api_call_redaction = __commonJS({
|
|
|
86311
86388
|
const subFields = Object.keys(v).slice(0, 15).join(",");
|
|
86312
86389
|
return subFields ? `${k}:{${subFields}}` : k;
|
|
86313
86390
|
}
|
|
86314
|
-
|
|
86391
|
+
const hint = enumSampleHint(k, v);
|
|
86392
|
+
return hint ? `${k}(e.g. "${hint}")` : k;
|
|
86315
86393
|
});
|
|
86316
86394
|
return `object{${fieldDescs.join(",")}}`;
|
|
86317
86395
|
}
|
|
86396
|
+
if (key) {
|
|
86397
|
+
const hint = enumSampleHint(key, val);
|
|
86398
|
+
if (hint)
|
|
86399
|
+
return `string (e.g. "${hint}")`;
|
|
86400
|
+
}
|
|
86318
86401
|
return typeof val;
|
|
86319
86402
|
};
|
|
86320
86403
|
const parsed = JSON.parse(body);
|
|
@@ -86322,7 +86405,7 @@ var require_api_call_redaction = __commonJS({
|
|
|
86322
86405
|
return void 0;
|
|
86323
86406
|
const shape = {};
|
|
86324
86407
|
for (const [key, val] of Object.entries(parsed)) {
|
|
86325
|
-
shape[key] = describeValue(val, 1);
|
|
86408
|
+
shape[key] = describeValue(val, 1, key);
|
|
86326
86409
|
}
|
|
86327
86410
|
return Object.keys(shape).length > 0 ? shape : void 0;
|
|
86328
86411
|
} catch {
|
|
@@ -87937,6 +88020,7 @@ var require_playwright_native = __commonJS({
|
|
|
87937
88020
|
var PACKAGE_DIR = (0, path_1.resolve)(__dirname, "..", "..");
|
|
87938
88021
|
var RUNS_DIR = process.env.RUNS_DIR || (0, path_1.join)(PACKAGE_DIR, ".qa-runs");
|
|
87939
88022
|
var PW_CLI = require.resolve("@playwright/test/cli");
|
|
88023
|
+
var LIVE_BROWSER_STATE_POLL_INTERVAL_MS = 500;
|
|
87940
88024
|
async function executeTestViaNativePlaywright2(options) {
|
|
87941
88025
|
const { playwrightCode, baseUrl, testName, testVariables, authStateCapturePath, storageStatePath, extraHttpHeaders, authStandingMode, validateContext, liveBrowserHandle, onLiveBrowserSnapshot } = options;
|
|
87942
88026
|
const logs2 = [];
|
|
@@ -88026,7 +88110,7 @@ test.beforeEach(async ({ page }, testInfo) => {
|
|
|
88026
88110
|
await __qaLiveBrowserCapture();
|
|
88027
88111
|
__qaLiveBrowserInterval = setInterval(() => {
|
|
88028
88112
|
void __qaLiveBrowserCapture?.();
|
|
88029
|
-
},
|
|
88113
|
+
}, ${LIVE_BROWSER_STATE_POLL_INTERVAL_MS});
|
|
88030
88114
|
});
|
|
88031
88115
|
|
|
88032
88116
|
test.afterEach(async () => {
|
|
@@ -88443,7 +88527,7 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
|
|
|
88443
88527
|
};
|
|
88444
88528
|
const interval = setInterval(() => {
|
|
88445
88529
|
void poll();
|
|
88446
|
-
},
|
|
88530
|
+
}, LIVE_BROWSER_STATE_POLL_INTERVAL_MS);
|
|
88447
88531
|
interval.unref?.();
|
|
88448
88532
|
void poll();
|
|
88449
88533
|
return async () => {
|
|
@@ -97641,6 +97725,12 @@ ${testOpen}`;
|
|
|
97641
97725
|
}
|
|
97642
97726
|
}
|
|
97643
97727
|
}
|
|
97728
|
+
const droppedByCalls = codeForMatcherScan.match(/\.get(?:Role|Text|Label|Placeholder|TestId|AltText|Title)\s*\(/g) ?? [];
|
|
97729
|
+
for (const call of droppedByCalls) {
|
|
97730
|
+
const methodName = call.replace(/[\s(]+$/, "").slice(1);
|
|
97731
|
+
logs2?.push(` \u2717 Rejected "${t.name}": "${methodName}" is not a real Playwright method \u2014 likely "${methodName.replace("get", "getBy")}" with "By" dropped`);
|
|
97732
|
+
return false;
|
|
97733
|
+
}
|
|
97644
97734
|
if (code.includes("// TODO")) {
|
|
97645
97735
|
logs2?.push(` \u26A0 "${t.name}" contains TODO comment \u2014 marking as unverified`);
|
|
97646
97736
|
t.tags = t.tags.filter((tag) => tag !== "@verified");
|
|
@@ -352078,16 +352168,16 @@ var require_live_browser_registry = __commonJS({
|
|
|
352078
352168
|
"use strict";
|
|
352079
352169
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
352080
352170
|
exports2.liveBrowserRegistry = void 0;
|
|
352171
|
+
var shared_1 = (init_dist(), __toCommonJS(dist_exports));
|
|
352081
352172
|
var THUMBNAIL_INTERVAL_MS = parseInt(process.env.LIVE_BROWSER_THUMBNAIL_INTERVAL_MS ?? "4000", 10) || 4e3;
|
|
352082
352173
|
var THUMBNAIL_QUALITY = parseInt(process.env.LIVE_BROWSER_THUMBNAIL_QUALITY ?? "60", 10) || 60;
|
|
352083
|
-
var BROWSER_THUMBNAIL_MAX_BASE64_BYTES = parseInt(process.env.LIVE_BROWSER_THUMBNAIL_MAX_BASE64_BYTES ?? "400000", 10) || 4e5;
|
|
352084
|
-
var MOBILE_THUMBNAIL_MAX_BASE64_BYTES = parseInt(process.env.LIVE_MOBILE_THUMBNAIL_MAX_BASE64_BYTES ?? "4500000", 10) || 45e5;
|
|
352085
352174
|
var STALE_SESSION_MS = parseInt(process.env.LIVE_BROWSER_STALE_MS ?? `${10 * 60 * 1e3}`, 10) || 10 * 60 * 1e3;
|
|
352086
352175
|
var MAX_SESSIONS = parseInt(process.env.LIVE_BROWSER_MAX_SESSIONS ?? "32", 10) || 32;
|
|
352087
352176
|
var LiveBrowserRegistry = class {
|
|
352088
352177
|
entries = /* @__PURE__ */ new Map();
|
|
352089
352178
|
snapshotTimer = null;
|
|
352090
352179
|
capturing = false;
|
|
352180
|
+
publisher = null;
|
|
352091
352181
|
/** Register a new session. Returns a handle the caller must hold and
|
|
352092
352182
|
* ultimately call .unregister() on. */
|
|
352093
352183
|
register(meta) {
|
|
@@ -352101,7 +352191,10 @@ var require_live_browser_registry = __commonJS({
|
|
|
352101
352191
|
lastUpdatedAt: now,
|
|
352102
352192
|
page: null,
|
|
352103
352193
|
isCapturing: false,
|
|
352104
|
-
pendingCapture: false
|
|
352194
|
+
pendingCapture: false,
|
|
352195
|
+
screencast: null,
|
|
352196
|
+
screencastStarting: false,
|
|
352197
|
+
lastPublishedAt: 0
|
|
352105
352198
|
};
|
|
352106
352199
|
this.entries.set(meta.sessionId, entry);
|
|
352107
352200
|
this.ensureSnapshotLoop();
|
|
@@ -352114,19 +352207,40 @@ var require_live_browser_registry = __commonJS({
|
|
|
352114
352207
|
unregister: () => this.unregister(meta.sessionId)
|
|
352115
352208
|
};
|
|
352116
352209
|
}
|
|
352210
|
+
/**
|
|
352211
|
+
* Install the real-time frame sink. Idempotent; the runner calls this once at
|
|
352212
|
+
* startup, before any session registers.
|
|
352213
|
+
*/
|
|
352214
|
+
setFramePublisher(publisher) {
|
|
352215
|
+
this.publisher = publisher;
|
|
352216
|
+
}
|
|
352117
352217
|
unregister(sessionId) {
|
|
352118
352218
|
const entry = this.entries.get(sessionId);
|
|
352119
352219
|
if (!entry)
|
|
352120
352220
|
return;
|
|
352121
|
-
|
|
352122
|
-
this.entries.delete(sessionId);
|
|
352221
|
+
this.dropEntry(sessionId, entry);
|
|
352123
352222
|
if (this.entries.size === 0)
|
|
352124
352223
|
this.stopSnapshotLoop();
|
|
352125
352224
|
}
|
|
352225
|
+
/**
|
|
352226
|
+
* The ONLY way an entry leaves `entries`. Centralised because a screencast
|
|
352227
|
+
* outlives the map otherwise: its frame handler closes over the entry rather
|
|
352228
|
+
* than looking it up, so an entry deleted without stopScreencast() leaves
|
|
352229
|
+
* Chromium encoding and acking frames for a page nobody is watching, holding
|
|
352230
|
+
* the CDP session and Page alive. Eviction and the staleness sweep both used
|
|
352231
|
+
* to delete directly and leaked exactly that way.
|
|
352232
|
+
*/
|
|
352233
|
+
dropEntry(sessionId, entry) {
|
|
352234
|
+
void this.stopScreencast(entry);
|
|
352235
|
+
entry.page = null;
|
|
352236
|
+
this.entries.delete(sessionId);
|
|
352237
|
+
}
|
|
352126
352238
|
setPage(sessionId, page) {
|
|
352127
352239
|
const entry = this.entries.get(sessionId);
|
|
352128
352240
|
if (!entry)
|
|
352129
352241
|
return;
|
|
352242
|
+
if (entry.screencast && entry.screencast.page !== page)
|
|
352243
|
+
void this.stopScreencast(entry);
|
|
352130
352244
|
entry.page = page;
|
|
352131
352245
|
entry.lastUpdatedAt = Date.now();
|
|
352132
352246
|
if (page) {
|
|
@@ -352136,6 +352250,119 @@ var require_live_browser_registry = __commonJS({
|
|
|
352136
352250
|
entry.currentUrl = url;
|
|
352137
352251
|
} catch {
|
|
352138
352252
|
}
|
|
352253
|
+
void this.startScreencast(entry, page);
|
|
352254
|
+
}
|
|
352255
|
+
}
|
|
352256
|
+
/**
|
|
352257
|
+
* Begin a CDP screencast for this page. Chromium then pushes a JPEG on every
|
|
352258
|
+
* paint, already downscaled to LIVE_FRAME_MAX_DIMENSION — which is the only
|
|
352259
|
+
* way this runner can downscale at all, since it ships no image library.
|
|
352260
|
+
*
|
|
352261
|
+
* Best-effort by design: any failure leaves `screencast` null and the polling
|
|
352262
|
+
* screenshot loop keeps serving the entry exactly as before.
|
|
352263
|
+
*/
|
|
352264
|
+
async startScreencast(entry, page) {
|
|
352265
|
+
if (!page || entry.screencast || entry.screencastStarting)
|
|
352266
|
+
return;
|
|
352267
|
+
if (page.isClosed?.())
|
|
352268
|
+
return;
|
|
352269
|
+
entry.screencastStarting = true;
|
|
352270
|
+
try {
|
|
352271
|
+
const client = await page.context().newCDPSession(page);
|
|
352272
|
+
const binding = { client, page, stopped: false };
|
|
352273
|
+
client.on("Page.screencastFrame", (params) => {
|
|
352274
|
+
void client.send("Page.screencastFrameAck", { sessionId: params.sessionId }).catch(() => {
|
|
352275
|
+
});
|
|
352276
|
+
if (binding.stopped)
|
|
352277
|
+
return;
|
|
352278
|
+
if (this.entries.get(entry.meta.sessionId) !== entry)
|
|
352279
|
+
return;
|
|
352280
|
+
if (typeof params.data !== "string" || params.data.length === 0)
|
|
352281
|
+
return;
|
|
352282
|
+
const width = params.metadata?.deviceWidth;
|
|
352283
|
+
const height = params.metadata?.deviceHeight;
|
|
352284
|
+
this.acceptFrame(entry, params.data, "image/jpeg", width, height);
|
|
352285
|
+
});
|
|
352286
|
+
page.once("close", () => {
|
|
352287
|
+
void this.stopScreencast(entry);
|
|
352288
|
+
});
|
|
352289
|
+
page.once("crash", () => {
|
|
352290
|
+
void this.stopScreencast(entry);
|
|
352291
|
+
});
|
|
352292
|
+
await client.send("Page.enable");
|
|
352293
|
+
await client.send("Page.startScreencast", {
|
|
352294
|
+
format: "jpeg",
|
|
352295
|
+
quality: shared_1.LIVE_FRAME_JPEG_QUALITY,
|
|
352296
|
+
// Absolute bounds, aspect-preserving — Chromium does the resize.
|
|
352297
|
+
maxWidth: shared_1.LIVE_FRAME_MAX_DIMENSION,
|
|
352298
|
+
maxHeight: shared_1.LIVE_FRAME_MAX_DIMENSION,
|
|
352299
|
+
everyNthFrame: 1
|
|
352300
|
+
});
|
|
352301
|
+
if (this.entries.get(entry.meta.sessionId) !== entry || entry.page !== page) {
|
|
352302
|
+
binding.stopped = true;
|
|
352303
|
+
await client.send("Page.stopScreencast").catch(() => {
|
|
352304
|
+
});
|
|
352305
|
+
await client.detach().catch(() => {
|
|
352306
|
+
});
|
|
352307
|
+
return;
|
|
352308
|
+
}
|
|
352309
|
+
entry.screencast = binding;
|
|
352310
|
+
} catch {
|
|
352311
|
+
} finally {
|
|
352312
|
+
entry.screencastStarting = false;
|
|
352313
|
+
}
|
|
352314
|
+
}
|
|
352315
|
+
async stopScreencast(entry) {
|
|
352316
|
+
const binding = entry.screencast;
|
|
352317
|
+
if (!binding || binding.stopped)
|
|
352318
|
+
return;
|
|
352319
|
+
binding.stopped = true;
|
|
352320
|
+
entry.screencast = null;
|
|
352321
|
+
try {
|
|
352322
|
+
await binding.client.send("Page.stopScreencast");
|
|
352323
|
+
} catch {
|
|
352324
|
+
}
|
|
352325
|
+
try {
|
|
352326
|
+
await binding.client.detach();
|
|
352327
|
+
} catch {
|
|
352328
|
+
}
|
|
352329
|
+
}
|
|
352330
|
+
/**
|
|
352331
|
+
* Record a freshly captured frame and forward it to the real-time publisher.
|
|
352332
|
+
* The stored copy (used by the heartbeat fallback) is only replaced when it
|
|
352333
|
+
* fits the heartbeat budget, but the publisher — which has a far larger budget
|
|
352334
|
+
* of its own — always sees the frame.
|
|
352335
|
+
*/
|
|
352336
|
+
acceptFrame(entry, base64, mimeType, width, height) {
|
|
352337
|
+
const now = Date.now();
|
|
352338
|
+
entry.lastUpdatedAt = now;
|
|
352339
|
+
if (base64.length <= shared_1.LIVE_THUMBNAIL_MAX_BASE64_BYTES) {
|
|
352340
|
+
entry.thumbnailJpegBase64 = base64;
|
|
352341
|
+
entry.thumbnailCapturedAt = now;
|
|
352342
|
+
if (width !== void 0)
|
|
352343
|
+
entry.thumbnailWidth = width;
|
|
352344
|
+
if (height !== void 0)
|
|
352345
|
+
entry.thumbnailHeight = height;
|
|
352346
|
+
}
|
|
352347
|
+
const publisher = this.publisher;
|
|
352348
|
+
if (!publisher)
|
|
352349
|
+
return;
|
|
352350
|
+
if (now - entry.lastPublishedAt < shared_1.LIVE_FRAME_MIN_PUSH_INTERVAL_MS)
|
|
352351
|
+
return;
|
|
352352
|
+
entry.lastPublishedAt = now;
|
|
352353
|
+
try {
|
|
352354
|
+
publisher({
|
|
352355
|
+
sessionId: entry.meta.sessionId,
|
|
352356
|
+
projectId: entry.meta.projectId,
|
|
352357
|
+
runId: entry.meta.runId,
|
|
352358
|
+
capturedAt: now,
|
|
352359
|
+
mimeType,
|
|
352360
|
+
width,
|
|
352361
|
+
height,
|
|
352362
|
+
surface: entry.meta.surface,
|
|
352363
|
+
base64
|
|
352364
|
+
});
|
|
352365
|
+
} catch {
|
|
352139
352366
|
}
|
|
352140
352367
|
}
|
|
352141
352368
|
setCurrentUrl(sessionId, url) {
|
|
@@ -352147,7 +352374,7 @@ var require_live_browser_registry = __commonJS({
|
|
|
352147
352374
|
const previousUrl = entry.currentUrl;
|
|
352148
352375
|
entry.currentUrl = url;
|
|
352149
352376
|
entry.lastUpdatedAt = Date.now();
|
|
352150
|
-
if (url && previousUrl && url !== previousUrl) {
|
|
352377
|
+
if (url && previousUrl && url !== previousUrl && !entry.screencast) {
|
|
352151
352378
|
entry.pendingCapture = true;
|
|
352152
352379
|
const schedule = (delay) => {
|
|
352153
352380
|
setTimeout(() => {
|
|
@@ -352168,6 +352395,11 @@ var require_live_browser_registry = __commonJS({
|
|
|
352168
352395
|
schedule(100);
|
|
352169
352396
|
}
|
|
352170
352397
|
}
|
|
352398
|
+
/**
|
|
352399
|
+
* Ingest a frame captured outside this module — the mobile mirror and the
|
|
352400
|
+
* spawned `playwright test` child (whose Page lives in another process, so no
|
|
352401
|
+
* CDP screencast is reachable from here) both arrive this way.
|
|
352402
|
+
*/
|
|
352171
352403
|
updateSnapshot(sessionId, snapshot) {
|
|
352172
352404
|
const entry = this.entries.get(sessionId);
|
|
352173
352405
|
if (!entry)
|
|
@@ -352176,12 +352408,10 @@ var require_live_browser_registry = __commonJS({
|
|
|
352176
352408
|
entry.currentUrl = snapshot.currentUrl;
|
|
352177
352409
|
}
|
|
352178
352410
|
const thumbnail = snapshot.thumbnailJpegBase64;
|
|
352179
|
-
|
|
352180
|
-
|
|
352181
|
-
|
|
352182
|
-
entry
|
|
352183
|
-
entry.thumbnailWidth = typeof snapshot.thumbnailWidth === "number" && Number.isFinite(snapshot.thumbnailWidth) ? snapshot.thumbnailWidth : void 0;
|
|
352184
|
-
entry.thumbnailHeight = typeof snapshot.thumbnailHeight === "number" && Number.isFinite(snapshot.thumbnailHeight) ? snapshot.thumbnailHeight : void 0;
|
|
352411
|
+
if (thumbnail) {
|
|
352412
|
+
const width = typeof snapshot.thumbnailWidth === "number" && Number.isFinite(snapshot.thumbnailWidth) ? snapshot.thumbnailWidth : void 0;
|
|
352413
|
+
const height = typeof snapshot.thumbnailHeight === "number" && Number.isFinite(snapshot.thumbnailHeight) ? snapshot.thumbnailHeight : void 0;
|
|
352414
|
+
this.acceptFrame(entry, thumbnail, snapshot.mimeType ?? "image/jpeg", width, height);
|
|
352185
352415
|
}
|
|
352186
352416
|
entry.lastUpdatedAt = Date.now();
|
|
352187
352417
|
}
|
|
@@ -352224,9 +352454,11 @@ var require_live_browser_registry = __commonJS({
|
|
|
352224
352454
|
async captureAllNow() {
|
|
352225
352455
|
await this.captureAll();
|
|
352226
352456
|
}
|
|
352227
|
-
/** Stop the snapshot loop and clear all entries. Idempotent. */
|
|
352457
|
+
/** Stop the snapshot loop, tear down every screencast, and clear all entries. Idempotent. */
|
|
352228
352458
|
shutdown() {
|
|
352229
352459
|
this.stopSnapshotLoop();
|
|
352460
|
+
for (const entry of this.entries.values())
|
|
352461
|
+
void this.stopScreencast(entry);
|
|
352230
352462
|
this.entries.clear();
|
|
352231
352463
|
}
|
|
352232
352464
|
ensureSnapshotLoop() {
|
|
@@ -352254,15 +352486,13 @@ var require_live_browser_registry = __commonJS({
|
|
|
352254
352486
|
}
|
|
352255
352487
|
}
|
|
352256
352488
|
if (oldestKey) {
|
|
352257
|
-
this.entries.get(oldestKey)
|
|
352258
|
-
this.entries.delete(oldestKey);
|
|
352489
|
+
this.dropEntry(oldestKey, this.entries.get(oldestKey));
|
|
352259
352490
|
}
|
|
352260
352491
|
}
|
|
352261
352492
|
sweepStale(now) {
|
|
352262
352493
|
for (const [key, entry] of this.entries) {
|
|
352263
352494
|
if (now - entry.lastUpdatedAt > STALE_SESSION_MS) {
|
|
352264
|
-
|
|
352265
|
-
this.entries.delete(key);
|
|
352495
|
+
this.dropEntry(key, entry);
|
|
352266
352496
|
}
|
|
352267
352497
|
}
|
|
352268
352498
|
}
|
|
@@ -352303,6 +352533,11 @@ var require_live_browser_registry = __commonJS({
|
|
|
352303
352533
|
entry.currentUrl = url;
|
|
352304
352534
|
} catch {
|
|
352305
352535
|
}
|
|
352536
|
+
if (entry.screencast) {
|
|
352537
|
+
if (entry.thumbnailJpegBase64)
|
|
352538
|
+
entry.thumbnailCapturedAt = Date.now();
|
|
352539
|
+
return;
|
|
352540
|
+
}
|
|
352306
352541
|
const buf = await page.screenshot({
|
|
352307
352542
|
type: "jpeg",
|
|
352308
352543
|
quality: THUMBNAIL_QUALITY,
|
|
@@ -352310,17 +352545,8 @@ var require_live_browser_registry = __commonJS({
|
|
|
352310
352545
|
timeout: 3e3,
|
|
352311
352546
|
scale: "css"
|
|
352312
352547
|
});
|
|
352313
|
-
const base64 = buf.toString("base64");
|
|
352314
|
-
if (base64.length > BROWSER_THUMBNAIL_MAX_BASE64_BYTES) {
|
|
352315
|
-
return;
|
|
352316
|
-
}
|
|
352317
|
-
entry.thumbnailJpegBase64 = base64;
|
|
352318
|
-
entry.thumbnailCapturedAt = Date.now();
|
|
352319
352548
|
const viewport = page.viewportSize?.();
|
|
352320
|
-
|
|
352321
|
-
entry.thumbnailWidth = viewport.width;
|
|
352322
|
-
entry.thumbnailHeight = viewport.height;
|
|
352323
|
-
}
|
|
352549
|
+
this.acceptFrame(entry, buf.toString("base64"), "image/jpeg", viewport?.width, viewport?.height);
|
|
352324
352550
|
} catch {
|
|
352325
352551
|
} finally {
|
|
352326
352552
|
entry.isCapturing = false;
|
|
@@ -353034,7 +353260,7 @@ var require_package4 = __commonJS({
|
|
|
353034
353260
|
"package.json"(exports2, module2) {
|
|
353035
353261
|
module2.exports = {
|
|
353036
353262
|
name: "@validate.qa/runner",
|
|
353037
|
-
version: "1.0.
|
|
353263
|
+
version: "1.0.79",
|
|
353038
353264
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
353039
353265
|
bin: {
|
|
353040
353266
|
"validate-runner": "dist/cli.js"
|
|
@@ -353826,6 +354052,24 @@ async function getScreenshot(sessionPath) {
|
|
|
353826
354052
|
return null;
|
|
353827
354053
|
}
|
|
353828
354054
|
}
|
|
354055
|
+
async function applyMjpegSettings(sessionPath, settings) {
|
|
354056
|
+
try {
|
|
354057
|
+
await appiumRequest(`${sessionPath}/appium/settings`, {
|
|
354058
|
+
method: "POST",
|
|
354059
|
+
body: JSON.stringify({
|
|
354060
|
+
settings: {
|
|
354061
|
+
mjpegServerFramerate: settings.framerate,
|
|
354062
|
+
mjpegScalingFactor: settings.scalingFactor,
|
|
354063
|
+
mjpegServerScreenshotQuality: settings.quality
|
|
354064
|
+
}
|
|
354065
|
+
})
|
|
354066
|
+
});
|
|
354067
|
+
return true;
|
|
354068
|
+
} catch (err) {
|
|
354069
|
+
if (isAppiumDriverDeadError(err)) throw err;
|
|
354070
|
+
return false;
|
|
354071
|
+
}
|
|
354072
|
+
}
|
|
353829
354073
|
async function getPageSource(sessionPath) {
|
|
353830
354074
|
try {
|
|
353831
354075
|
const value = readValue(await appiumRequest(`${sessionPath}/source`));
|
|
@@ -356334,7 +356578,15 @@ function mobileParallelSessionCapabilities(platform3, portOffset = 0, deviceId =
|
|
|
356334
356578
|
return (Number.isFinite(parsed) ? parsed : fallback) + offset;
|
|
356335
356579
|
};
|
|
356336
356580
|
if (platform3 === "ANDROID") {
|
|
356337
|
-
return {
|
|
356581
|
+
return {
|
|
356582
|
+
"appium:systemPort": readPort("APPIUM_SYSTEM_PORT", 8200),
|
|
356583
|
+
// Host port that UiAutomator2 forwards the on-device MJPEG broadcaster
|
|
356584
|
+
// (device port 7810) to. Without this the live mirror has to poll
|
|
356585
|
+
// WebDriver /screenshot for full-resolution PNGs, which the runner cannot
|
|
356586
|
+
// downscale itself. Base differs from iOS's 9100 so a concurrent
|
|
356587
|
+
// Android+iOS pair can never collide on a shared slot offset.
|
|
356588
|
+
"appium:mjpegServerPort": readPort("APPIUM_ANDROID_MJPEG_SERVER_PORT", 7810)
|
|
356589
|
+
};
|
|
356338
356590
|
}
|
|
356339
356591
|
const safeDevice = deviceId.replace(/[^A-Za-z0-9._-]+/g, "-") || `device-${offset}`;
|
|
356340
356592
|
const derivedDataRoot = env.APPIUM_DERIVED_DATA_PATH?.trim() || (0, import_node_path2.join)((0, import_node_os2.tmpdir)(), "validateqa-wda");
|
|
@@ -357589,6 +357841,15 @@ function acquireControlPortSlot(deviceId) {
|
|
|
357589
357841
|
function releaseControlPortSlot(deviceId) {
|
|
357590
357842
|
controlPortSlotsByDevice.delete(deviceId);
|
|
357591
357843
|
}
|
|
357844
|
+
function getControlPortSlot(deviceId) {
|
|
357845
|
+
return controlPortSlotsByDevice.get(deviceId);
|
|
357846
|
+
}
|
|
357847
|
+
function mobileMjpegPortForDevice(platform3, deviceId, env = process.env) {
|
|
357848
|
+
const slot = getControlPortSlot(deviceId);
|
|
357849
|
+
if (slot === void 0) return null;
|
|
357850
|
+
const port = mobileParallelSessionCapabilities(platform3, slot, deviceId, env)["appium:mjpegServerPort"];
|
|
357851
|
+
return typeof port === "number" && Number.isFinite(port) ? port : null;
|
|
357852
|
+
}
|
|
357592
357853
|
var PINNED_DEVICE_SNAPSHOT_TTL_MS = 25 * 6e4;
|
|
357593
357854
|
var pinnedDeviceSnapshot = null;
|
|
357594
357855
|
async function executeMobileTest(options) {
|
|
@@ -358527,12 +358788,106 @@ function decideMobileDeviceGate(input) {
|
|
|
358527
358788
|
}
|
|
358528
358789
|
|
|
358529
358790
|
// src/services/mobile-bridge.ts
|
|
358530
|
-
var
|
|
358791
|
+
var import_http2 = require("http");
|
|
358531
358792
|
var import_child_process4 = require("child_process");
|
|
358532
358793
|
var import_crypto = require("crypto");
|
|
358533
358794
|
var import_os4 = require("os");
|
|
358534
358795
|
var import_path4 = require("path");
|
|
358535
358796
|
var import_fs3 = require("fs");
|
|
358797
|
+
|
|
358798
|
+
// src/services/mobile/mjpeg-stream.ts
|
|
358799
|
+
var import_http = require("http");
|
|
358800
|
+
init_dist();
|
|
358801
|
+
var JPEG_SOI = Buffer.from([255, 216]);
|
|
358802
|
+
var JPEG_EOI = Buffer.from([255, 217]);
|
|
358803
|
+
function startMjpegStream(options) {
|
|
358804
|
+
const { port, host = "127.0.0.1", onFrame, onError } = options;
|
|
358805
|
+
let stopped = false;
|
|
358806
|
+
let req = null;
|
|
358807
|
+
let response = null;
|
|
358808
|
+
let buffer = Buffer.alloc(0);
|
|
358809
|
+
const fail = (error2) => {
|
|
358810
|
+
if (stopped) return;
|
|
358811
|
+
stopped = true;
|
|
358812
|
+
try {
|
|
358813
|
+
req?.destroy();
|
|
358814
|
+
} catch {
|
|
358815
|
+
}
|
|
358816
|
+
try {
|
|
358817
|
+
response?.destroy();
|
|
358818
|
+
} catch {
|
|
358819
|
+
}
|
|
358820
|
+
buffer = Buffer.alloc(0);
|
|
358821
|
+
onError?.(error2);
|
|
358822
|
+
};
|
|
358823
|
+
const consume = (chunk) => {
|
|
358824
|
+
if (stopped) return;
|
|
358825
|
+
buffer = buffer.length === 0 ? chunk : Buffer.concat([buffer, chunk]);
|
|
358826
|
+
for (; ; ) {
|
|
358827
|
+
const start = buffer.indexOf(JPEG_SOI);
|
|
358828
|
+
if (start < 0) {
|
|
358829
|
+
if (buffer.length > 1) buffer = buffer.subarray(buffer.length - 1);
|
|
358830
|
+
return;
|
|
358831
|
+
}
|
|
358832
|
+
const end = buffer.indexOf(JPEG_EOI, start + JPEG_SOI.length);
|
|
358833
|
+
if (end < 0) {
|
|
358834
|
+
if (start > 0) buffer = buffer.subarray(start);
|
|
358835
|
+
if (buffer.length > MOBILE_MJPEG_MAX_FRAME_BYTES) buffer = Buffer.alloc(0);
|
|
358836
|
+
return;
|
|
358837
|
+
}
|
|
358838
|
+
const frameEnd = end + JPEG_EOI.length;
|
|
358839
|
+
const frame = buffer.subarray(start, frameEnd);
|
|
358840
|
+
buffer = buffer.subarray(frameEnd);
|
|
358841
|
+
try {
|
|
358842
|
+
onFrame(frame);
|
|
358843
|
+
} catch {
|
|
358844
|
+
}
|
|
358845
|
+
}
|
|
358846
|
+
};
|
|
358847
|
+
try {
|
|
358848
|
+
req = (0, import_http.request)(
|
|
358849
|
+
{ host, port, path: "/", method: "GET", headers: { Connection: "keep-alive" } },
|
|
358850
|
+
(res) => {
|
|
358851
|
+
response = res;
|
|
358852
|
+
if (stopped) {
|
|
358853
|
+
res.destroy();
|
|
358854
|
+
return;
|
|
358855
|
+
}
|
|
358856
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
358857
|
+
fail(new Error(`MJPEG broadcaster returned HTTP ${res.statusCode}`));
|
|
358858
|
+
res.destroy();
|
|
358859
|
+
return;
|
|
358860
|
+
}
|
|
358861
|
+
res.on("data", (chunk) => consume(chunk));
|
|
358862
|
+
res.on("end", () => fail(new Error("MJPEG stream ended")));
|
|
358863
|
+
res.on("error", (err) => fail(err));
|
|
358864
|
+
}
|
|
358865
|
+
);
|
|
358866
|
+
req.on("error", (err) => fail(err));
|
|
358867
|
+
req.setTimeout(1e4, () => fail(new Error("MJPEG connect timeout")));
|
|
358868
|
+
req.end();
|
|
358869
|
+
} catch (err) {
|
|
358870
|
+
fail(err instanceof Error ? err : new Error(String(err)));
|
|
358871
|
+
}
|
|
358872
|
+
return {
|
|
358873
|
+
stop: () => {
|
|
358874
|
+
if (stopped) return;
|
|
358875
|
+
stopped = true;
|
|
358876
|
+
try {
|
|
358877
|
+
req?.destroy();
|
|
358878
|
+
} catch {
|
|
358879
|
+
}
|
|
358880
|
+
try {
|
|
358881
|
+
response?.destroy();
|
|
358882
|
+
} catch {
|
|
358883
|
+
}
|
|
358884
|
+
buffer = Buffer.alloc(0);
|
|
358885
|
+
},
|
|
358886
|
+
isActive: () => !stopped
|
|
358887
|
+
};
|
|
358888
|
+
}
|
|
358889
|
+
|
|
358890
|
+
// src/services/mobile-bridge.ts
|
|
358536
358891
|
init_dist();
|
|
358537
358892
|
var MOBILE_BRIDGE_PORT2 = 9515;
|
|
358538
358893
|
var MAX_REQUEST_BODY_BYTES = 64 * 1024;
|
|
@@ -359086,16 +359441,66 @@ function writeFrameToChannelClient(ch, client, frame) {
|
|
|
359086
359441
|
} catch {
|
|
359087
359442
|
}
|
|
359088
359443
|
}
|
|
359444
|
+
function broadcastChannelFrame(ch, frame) {
|
|
359445
|
+
ch.lastFrame = frame;
|
|
359446
|
+
for (const client of ch.clients) writeFrameToChannelClient(ch, client, frame);
|
|
359447
|
+
}
|
|
359448
|
+
function tryStartChannelMjpeg(ch) {
|
|
359449
|
+
if (ch.mjpeg || ch.mjpegStarting || ch.mjpegUnavailable) return;
|
|
359450
|
+
const port = mobileMjpegPortForDevice(ch.platform, ch.deviceId);
|
|
359451
|
+
if (port === null) {
|
|
359452
|
+
ch.mjpegUnavailable = true;
|
|
359453
|
+
return;
|
|
359454
|
+
}
|
|
359455
|
+
ch.mjpegStarting = true;
|
|
359456
|
+
const handle = startMjpegStream({
|
|
359457
|
+
port,
|
|
359458
|
+
onFrame: (jpeg) => {
|
|
359459
|
+
const isOwned = ch.mjpeg === handle;
|
|
359460
|
+
const isCandidate = ch.mjpeg === null && ch.mjpegStarting;
|
|
359461
|
+
if (!isOwned && !isCandidate) return;
|
|
359462
|
+
ch.mjpegLastFrameAt = Date.now();
|
|
359463
|
+
ch.nullTicks = 0;
|
|
359464
|
+
if (isCandidate) {
|
|
359465
|
+
ch.mjpeg = handle;
|
|
359466
|
+
ch.mjpegStarting = false;
|
|
359467
|
+
if (ch.pollTimer) {
|
|
359468
|
+
clearInterval(ch.pollTimer);
|
|
359469
|
+
ch.pollTimer = null;
|
|
359470
|
+
}
|
|
359471
|
+
}
|
|
359472
|
+
broadcastChannelFrame(ch, {
|
|
359473
|
+
timestamp: ch.mjpegLastFrameAt,
|
|
359474
|
+
screenshot: jpeg.toString("base64"),
|
|
359475
|
+
source: null,
|
|
359476
|
+
mimeType: "image/jpeg"
|
|
359477
|
+
});
|
|
359478
|
+
},
|
|
359479
|
+
onError: () => {
|
|
359480
|
+
const wasActive = ch.mjpeg === handle;
|
|
359481
|
+
ch.mjpeg = null;
|
|
359482
|
+
ch.mjpegStarting = false;
|
|
359483
|
+
ch.mjpegUnavailable = true;
|
|
359484
|
+
if (wasActive && ch.clients.size > 0) ensureChannelPolling(ch);
|
|
359485
|
+
}
|
|
359486
|
+
});
|
|
359487
|
+
const firstFrameDeadline = setTimeout(() => {
|
|
359488
|
+
if (ch.mjpeg) return;
|
|
359489
|
+
ch.mjpegStarting = false;
|
|
359490
|
+
ch.mjpegUnavailable = true;
|
|
359491
|
+
handle.stop();
|
|
359492
|
+
if (ch.clients.size > 0) ensureChannelPolling(ch);
|
|
359493
|
+
}, MOBILE_MJPEG_FIRST_FRAME_TIMEOUT_MS);
|
|
359494
|
+
firstFrameDeadline.unref?.();
|
|
359495
|
+
}
|
|
359089
359496
|
async function pollRunnerChannel(ch) {
|
|
359090
359497
|
if (ch.clients.size === 0 || ch.polling) return;
|
|
359498
|
+
if (ch.mjpeg) return;
|
|
359091
359499
|
ch.polling = true;
|
|
359092
359500
|
try {
|
|
359093
359501
|
validateSessionId(ch.sessionId);
|
|
359094
|
-
const
|
|
359095
|
-
|
|
359096
|
-
getPageSource(`/session/${ch.sessionId}`)
|
|
359097
|
-
]);
|
|
359098
|
-
if (!screenshot && !source) {
|
|
359502
|
+
const screenshot = await getScreenshot(`/session/${ch.sessionId}`);
|
|
359503
|
+
if (!screenshot) {
|
|
359099
359504
|
ch.nullTicks++;
|
|
359100
359505
|
if (ch.nullTicks >= MIRROR_DEAD_SESSION_TICKS) {
|
|
359101
359506
|
const alive = await isSessionAlive(ch.sessionId);
|
|
@@ -359108,32 +359513,45 @@ async function pollRunnerChannel(ch) {
|
|
|
359108
359513
|
} else {
|
|
359109
359514
|
ch.nullTicks = 0;
|
|
359110
359515
|
}
|
|
359111
|
-
|
|
359112
|
-
|
|
359113
|
-
|
|
359516
|
+
broadcastChannelFrame(ch, {
|
|
359517
|
+
timestamp: Date.now(),
|
|
359518
|
+
screenshot: screenshot ?? null,
|
|
359519
|
+
source: null,
|
|
359520
|
+
mimeType: "image/png"
|
|
359521
|
+
});
|
|
359114
359522
|
} catch {
|
|
359115
359523
|
} finally {
|
|
359116
359524
|
ch.polling = false;
|
|
359117
359525
|
}
|
|
359118
359526
|
}
|
|
359119
359527
|
function ensureChannelPolling(ch) {
|
|
359120
|
-
|
|
359528
|
+
tryStartChannelMjpeg(ch);
|
|
359529
|
+
if (ch.mjpeg || ch.pollTimer) return;
|
|
359121
359530
|
ch.pollTimer = setInterval(() => {
|
|
359122
359531
|
void pollRunnerChannel(ch);
|
|
359123
359532
|
}, MOBILE_SCREENSHOT_INTERVAL_MS);
|
|
359124
359533
|
ch.pollTimer.unref?.();
|
|
359125
359534
|
}
|
|
359126
359535
|
function stopChannelPollingIfIdle(ch) {
|
|
359127
|
-
if (ch.clients.size
|
|
359536
|
+
if (ch.clients.size > 0) return;
|
|
359537
|
+
if (ch.pollTimer) {
|
|
359128
359538
|
clearInterval(ch.pollTimer);
|
|
359129
359539
|
ch.pollTimer = null;
|
|
359130
359540
|
}
|
|
359541
|
+
if (ch.mjpeg) {
|
|
359542
|
+
ch.mjpeg.stop();
|
|
359543
|
+
ch.mjpeg = null;
|
|
359544
|
+
}
|
|
359131
359545
|
}
|
|
359132
359546
|
function closeRunnerChannel(ch) {
|
|
359133
359547
|
if (ch.pollTimer) {
|
|
359134
359548
|
clearInterval(ch.pollTimer);
|
|
359135
359549
|
ch.pollTimer = null;
|
|
359136
359550
|
}
|
|
359551
|
+
if (ch.mjpeg) {
|
|
359552
|
+
ch.mjpeg.stop();
|
|
359553
|
+
ch.mjpeg = null;
|
|
359554
|
+
}
|
|
359137
359555
|
for (const client of ch.clients) {
|
|
359138
359556
|
try {
|
|
359139
359557
|
client.end();
|
|
@@ -359530,7 +359948,7 @@ async function startMobileBridge(options) {
|
|
|
359530
359948
|
}
|
|
359531
359949
|
killStaleBridge();
|
|
359532
359950
|
return new Promise((resolve2, reject) => {
|
|
359533
|
-
server2 = (0,
|
|
359951
|
+
server2 = (0, import_http2.createServer)(route);
|
|
359534
359952
|
server2.on("error", (err) => {
|
|
359535
359953
|
if (err.code === "EADDRINUSE") {
|
|
359536
359954
|
reject(new Error(`Port ${port} already in use. Another bridge instance may be running.`));
|
|
@@ -359718,12 +360136,24 @@ function attachMirrorSession(sessionId, platform3, deviceId) {
|
|
|
359718
360136
|
lastFrame: null,
|
|
359719
360137
|
pollTimer: null,
|
|
359720
360138
|
nullTicks: 0,
|
|
359721
|
-
polling: false
|
|
360139
|
+
polling: false,
|
|
360140
|
+
mjpeg: null,
|
|
360141
|
+
mjpegLastFrameAt: 0,
|
|
360142
|
+
mjpegStarting: false,
|
|
360143
|
+
mjpegUnavailable: false
|
|
359722
360144
|
};
|
|
359723
360145
|
ch.sessionId = sessionId;
|
|
359724
360146
|
ch.platform = platform3;
|
|
359725
360147
|
ch.nullTicks = 0;
|
|
360148
|
+
if (ch.mjpeg) {
|
|
360149
|
+
ch.mjpeg.stop();
|
|
360150
|
+
ch.mjpeg = null;
|
|
360151
|
+
}
|
|
360152
|
+
ch.mjpegUnavailable = false;
|
|
360153
|
+
ch.mjpegStarting = false;
|
|
359726
360154
|
runnerChannels.set(deviceId, ch);
|
|
360155
|
+
void applyMjpegSettings(`/session/${sessionId}`, MOBILE_MJPEG_SETTINGS).catch(() => {
|
|
360156
|
+
});
|
|
359727
360157
|
if (ch.clients.size > 0) {
|
|
359728
360158
|
ensureChannelPolling(ch);
|
|
359729
360159
|
void pollRunnerChannel(ch);
|
|
@@ -362907,7 +363337,9 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
362907
363337
|
liveBrowserHandle.updateSnapshot({
|
|
362908
363338
|
currentUrl: mobileTarget.appId,
|
|
362909
363339
|
thumbnailJpegBase64: base64,
|
|
362910
|
-
thumbnailCapturedAt: Date.now()
|
|
363340
|
+
thumbnailCapturedAt: Date.now(),
|
|
363341
|
+
// Appium's screenshot endpoint returns PNG despite the field name.
|
|
363342
|
+
mimeType: "image/png"
|
|
362911
363343
|
});
|
|
362912
363344
|
requestLiveBrowserHeartbeat();
|
|
362913
363345
|
},
|
|
@@ -364152,6 +364584,22 @@ async function startRunner(opts) {
|
|
|
364152
364584
|
} catch {
|
|
364153
364585
|
}
|
|
364154
364586
|
};
|
|
364587
|
+
const liveFramePushInFlight = /* @__PURE__ */ new Set();
|
|
364588
|
+
const publishLiveFrame = (frame) => {
|
|
364589
|
+
if (!frame.projectId) return;
|
|
364590
|
+
if (liveFramePushInFlight.has(frame.sessionId)) return;
|
|
364591
|
+
liveFramePushInFlight.add(frame.sessionId);
|
|
364592
|
+
void fetch(`${serverUrl}/api/runner/live-frame`, {
|
|
364593
|
+
method: "POST",
|
|
364594
|
+
headers,
|
|
364595
|
+
body: JSON.stringify(frame),
|
|
364596
|
+
signal: AbortSignal.timeout(5e3)
|
|
364597
|
+
}).catch(() => {
|
|
364598
|
+
}).finally(() => {
|
|
364599
|
+
liveFramePushInFlight.delete(frame.sessionId);
|
|
364600
|
+
});
|
|
364601
|
+
};
|
|
364602
|
+
import_runner_core11.liveBrowserRegistry.setFramePublisher(publishLiveFrame);
|
|
364155
364603
|
let liveBrowserHeartbeatInFlight = false;
|
|
364156
364604
|
let liveBrowserHeartbeatPending = false;
|
|
364157
364605
|
let lastLiveBrowserHeartbeatAt = 0;
|