@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.mjs
CHANGED
|
@@ -1893,6 +1893,56 @@ 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_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_STALE_MS = readEnvInt("LIVE_FRAME_STALE_MS", 3e4);
|
|
1932
|
+
LIVE_FRAME_MAX_SESSIONS_PER_PROJECT = readEnvInt("LIVE_FRAME_MAX_SESSIONS_PER_PROJECT", 32);
|
|
1933
|
+
MOBILE_MJPEG_SETTINGS = {
|
|
1934
|
+
/** Screenshots per second the broadcaster emits (both drivers: range 1-60). */
|
|
1935
|
+
framerate: readEnvInt("MOBILE_MJPEG_FRAMERATE", 10),
|
|
1936
|
+
/** Downscale percentage. UiAutomator2 defaults to 50, XCUITest to 100. */
|
|
1937
|
+
scalingFactor: readEnvInt("MOBILE_MJPEG_SCALING_FACTOR", 50),
|
|
1938
|
+
/** JPEG compression factor (both drivers: range 1-100). */
|
|
1939
|
+
quality: readEnvInt("MOBILE_MJPEG_QUALITY", 50)
|
|
1940
|
+
};
|
|
1941
|
+
MOBILE_MJPEG_FIRST_FRAME_TIMEOUT_MS = readEnvInt("MOBILE_MJPEG_FIRST_FRAME_TIMEOUT_MS", 6e3);
|
|
1942
|
+
MOBILE_MJPEG_MAX_FRAME_BYTES = readEnvInt("MOBILE_MJPEG_MAX_FRAME_BYTES", 8 * 1024 * 1024);
|
|
1943
|
+
}
|
|
1944
|
+
});
|
|
1945
|
+
|
|
1896
1946
|
// ../shared/dist/project-key.js
|
|
1897
1947
|
function isValidProjectKey(key) {
|
|
1898
1948
|
return typeof key === "string" && PROJECT_KEY_PATTERN.test(key) && !RESERVED_PROJECT_KEYS.has(key);
|
|
@@ -6315,6 +6365,14 @@ __export(dist_exports, {
|
|
|
6315
6365
|
GROUP_LABELS: () => GROUP_LABELS,
|
|
6316
6366
|
LEGACY_TTS_MODEL_MAP: () => LEGACY_TTS_MODEL_MAP,
|
|
6317
6367
|
LEGACY_TTS_VOICE_ID_MAP: () => LEGACY_TTS_VOICE_ID_MAP,
|
|
6368
|
+
LIVE_FRAME_JPEG_QUALITY: () => LIVE_FRAME_JPEG_QUALITY,
|
|
6369
|
+
LIVE_FRAME_MAX_DIMENSION: () => LIVE_FRAME_MAX_DIMENSION,
|
|
6370
|
+
LIVE_FRAME_MAX_SESSIONS_PER_PROJECT: () => LIVE_FRAME_MAX_SESSIONS_PER_PROJECT,
|
|
6371
|
+
LIVE_FRAME_MIN_PUSH_INTERVAL_MS: () => LIVE_FRAME_MIN_PUSH_INTERVAL_MS,
|
|
6372
|
+
LIVE_FRAME_PUSH_MAX_BASE64_BYTES: () => LIVE_FRAME_PUSH_MAX_BASE64_BYTES,
|
|
6373
|
+
LIVE_FRAME_STALE_MS: () => LIVE_FRAME_STALE_MS,
|
|
6374
|
+
LIVE_FRAME_UNFOCUSED_INTERVAL_MS: () => LIVE_FRAME_UNFOCUSED_INTERVAL_MS,
|
|
6375
|
+
LIVE_THUMBNAIL_MAX_BASE64_BYTES: () => LIVE_THUMBNAIL_MAX_BASE64_BYTES,
|
|
6318
6376
|
MAX_CREDIT_TOPUP_CENTS: () => MAX_CREDIT_TOPUP_CENTS,
|
|
6319
6377
|
MAX_DEEPLINK_LENGTH: () => MAX_DEEPLINK_LENGTH,
|
|
6320
6378
|
MIN_CREDIT_TOPUP_CENTS: () => MIN_CREDIT_TOPUP_CENTS,
|
|
@@ -6326,6 +6384,9 @@ __export(dist_exports, {
|
|
|
6326
6384
|
MOBILE_DEFAULT_SWIPE_DISTANCE: () => MOBILE_DEFAULT_SWIPE_DISTANCE,
|
|
6327
6385
|
MOBILE_FOCUS_SETTLE_MS: () => MOBILE_FOCUS_SETTLE_MS,
|
|
6328
6386
|
MOBILE_MAX_SWIPE_DISTANCE: () => MOBILE_MAX_SWIPE_DISTANCE,
|
|
6387
|
+
MOBILE_MJPEG_FIRST_FRAME_TIMEOUT_MS: () => MOBILE_MJPEG_FIRST_FRAME_TIMEOUT_MS,
|
|
6388
|
+
MOBILE_MJPEG_MAX_FRAME_BYTES: () => MOBILE_MJPEG_MAX_FRAME_BYTES,
|
|
6389
|
+
MOBILE_MJPEG_SETTINGS: () => MOBILE_MJPEG_SETTINGS,
|
|
6329
6390
|
MOBILE_PLATFORMS: () => MOBILE_PLATFORMS,
|
|
6330
6391
|
MOBILE_POST_ACTION_SETTLE_MS: () => MOBILE_POST_ACTION_SETTLE_MS,
|
|
6331
6392
|
MOBILE_SCREENSHOT_INTERVAL_MS: () => MOBILE_SCREENSHOT_INTERVAL_MS,
|
|
@@ -6438,6 +6499,7 @@ __export(dist_exports, {
|
|
|
6438
6499
|
isTestReadyForPlatform: () => isTestReadyForPlatform,
|
|
6439
6500
|
isTrialExpired: () => isTrialExpired,
|
|
6440
6501
|
isUIVariant: () => isUIVariant,
|
|
6502
|
+
isValidLiveFrame: () => isValidLiveFrame,
|
|
6441
6503
|
isValidMobileAppId: () => isValidMobileAppId,
|
|
6442
6504
|
isValidProjectKey: () => isValidProjectKey,
|
|
6443
6505
|
isValidSttProvider: () => isValidSttProvider,
|
|
@@ -6500,6 +6562,7 @@ var init_dist = __esm({
|
|
|
6500
6562
|
"use strict";
|
|
6501
6563
|
init_types();
|
|
6502
6564
|
init_constants();
|
|
6565
|
+
init_live_frame();
|
|
6503
6566
|
init_project_key();
|
|
6504
6567
|
init_mobile_test_generation();
|
|
6505
6568
|
init_mobile_step_healer();
|
|
@@ -86295,11 +86358,25 @@ var require_api_call_redaction = __commonJS({
|
|
|
86295
86358
|
const lower = name.toLowerCase();
|
|
86296
86359
|
return headers.find((h) => h.name?.toLowerCase() === lower)?.value;
|
|
86297
86360
|
}
|
|
86361
|
+
var ENUM_LIKE_FIELD_NAME = /status|state|phase|role|tier|kind|mode|level|category|priority|visibility|^type$|_type$/i;
|
|
86362
|
+
var NEVER_SAMPLE_FIELD_NAME = /password|token|secret|api.?key|auth|bearer|credential|session|email|phone|address|ssn/i;
|
|
86363
|
+
var ENUM_LIKE_VALUE = /^[A-Za-z][A-Za-z0-9_-]{0,29}$/;
|
|
86364
|
+
function enumSampleHint(key, val) {
|
|
86365
|
+
if (typeof val !== "string")
|
|
86366
|
+
return null;
|
|
86367
|
+
if (NEVER_SAMPLE_FIELD_NAME.test(key))
|
|
86368
|
+
return null;
|
|
86369
|
+
if (!ENUM_LIKE_FIELD_NAME.test(key))
|
|
86370
|
+
return null;
|
|
86371
|
+
if (!ENUM_LIKE_VALUE.test(val))
|
|
86372
|
+
return null;
|
|
86373
|
+
return val;
|
|
86374
|
+
}
|
|
86298
86375
|
function extractBodyShape(body) {
|
|
86299
86376
|
if (!body)
|
|
86300
86377
|
return void 0;
|
|
86301
86378
|
try {
|
|
86302
|
-
let describeValue = function(val, depth) {
|
|
86379
|
+
let describeValue = function(val, depth, key) {
|
|
86303
86380
|
if (val === null)
|
|
86304
86381
|
return "null";
|
|
86305
86382
|
if (Array.isArray(val)) {
|
|
@@ -86316,10 +86393,16 @@ var require_api_call_redaction = __commonJS({
|
|
|
86316
86393
|
const subFields = Object.keys(v).slice(0, 15).join(",");
|
|
86317
86394
|
return subFields ? `${k}:{${subFields}}` : k;
|
|
86318
86395
|
}
|
|
86319
|
-
|
|
86396
|
+
const hint = enumSampleHint(k, v);
|
|
86397
|
+
return hint ? `${k}(e.g. "${hint}")` : k;
|
|
86320
86398
|
});
|
|
86321
86399
|
return `object{${fieldDescs.join(",")}}`;
|
|
86322
86400
|
}
|
|
86401
|
+
if (key) {
|
|
86402
|
+
const hint = enumSampleHint(key, val);
|
|
86403
|
+
if (hint)
|
|
86404
|
+
return `string (e.g. "${hint}")`;
|
|
86405
|
+
}
|
|
86323
86406
|
return typeof val;
|
|
86324
86407
|
};
|
|
86325
86408
|
const parsed = JSON.parse(body);
|
|
@@ -86327,7 +86410,7 @@ var require_api_call_redaction = __commonJS({
|
|
|
86327
86410
|
return void 0;
|
|
86328
86411
|
const shape = {};
|
|
86329
86412
|
for (const [key, val] of Object.entries(parsed)) {
|
|
86330
|
-
shape[key] = describeValue(val, 1);
|
|
86413
|
+
shape[key] = describeValue(val, 1, key);
|
|
86331
86414
|
}
|
|
86332
86415
|
return Object.keys(shape).length > 0 ? shape : void 0;
|
|
86333
86416
|
} catch {
|
|
@@ -87942,6 +88025,7 @@ var require_playwright_native = __commonJS({
|
|
|
87942
88025
|
var PACKAGE_DIR = (0, path_1.resolve)(__dirname, "..", "..");
|
|
87943
88026
|
var RUNS_DIR = process.env.RUNS_DIR || (0, path_1.join)(PACKAGE_DIR, ".qa-runs");
|
|
87944
88027
|
var PW_CLI = __require.resolve("@playwright/test/cli");
|
|
88028
|
+
var LIVE_BROWSER_STATE_POLL_INTERVAL_MS = 500;
|
|
87945
88029
|
async function executeTestViaNativePlaywright2(options) {
|
|
87946
88030
|
const { playwrightCode, baseUrl, testName, testVariables, authStateCapturePath, storageStatePath, extraHttpHeaders, authStandingMode, validateContext, liveBrowserHandle, onLiveBrowserSnapshot } = options;
|
|
87947
88031
|
const logs2 = [];
|
|
@@ -88031,7 +88115,7 @@ test.beforeEach(async ({ page }, testInfo) => {
|
|
|
88031
88115
|
await __qaLiveBrowserCapture();
|
|
88032
88116
|
__qaLiveBrowserInterval = setInterval(() => {
|
|
88033
88117
|
void __qaLiveBrowserCapture?.();
|
|
88034
|
-
},
|
|
88118
|
+
}, ${LIVE_BROWSER_STATE_POLL_INTERVAL_MS});
|
|
88035
88119
|
});
|
|
88036
88120
|
|
|
88037
88121
|
test.afterEach(async () => {
|
|
@@ -88448,7 +88532,7 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
|
|
|
88448
88532
|
};
|
|
88449
88533
|
const interval = setInterval(() => {
|
|
88450
88534
|
void poll();
|
|
88451
|
-
},
|
|
88535
|
+
}, LIVE_BROWSER_STATE_POLL_INTERVAL_MS);
|
|
88452
88536
|
interval.unref?.();
|
|
88453
88537
|
void poll();
|
|
88454
88538
|
return async () => {
|
|
@@ -97646,6 +97730,12 @@ ${testOpen}`;
|
|
|
97646
97730
|
}
|
|
97647
97731
|
}
|
|
97648
97732
|
}
|
|
97733
|
+
const droppedByCalls = codeForMatcherScan.match(/\.get(?:Role|Text|Label|Placeholder|TestId|AltText|Title)\s*\(/g) ?? [];
|
|
97734
|
+
for (const call of droppedByCalls) {
|
|
97735
|
+
const methodName = call.replace(/[\s(]+$/, "").slice(1);
|
|
97736
|
+
logs2?.push(` \u2717 Rejected "${t.name}": "${methodName}" is not a real Playwright method \u2014 likely "${methodName.replace("get", "getBy")}" with "By" dropped`);
|
|
97737
|
+
return false;
|
|
97738
|
+
}
|
|
97649
97739
|
if (code.includes("// TODO")) {
|
|
97650
97740
|
logs2?.push(` \u26A0 "${t.name}" contains TODO comment \u2014 marking as unverified`);
|
|
97651
97741
|
t.tags = t.tags.filter((tag) => tag !== "@verified");
|
|
@@ -352083,16 +352173,16 @@ var require_live_browser_registry = __commonJS({
|
|
|
352083
352173
|
"use strict";
|
|
352084
352174
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
352085
352175
|
exports.liveBrowserRegistry = void 0;
|
|
352176
|
+
var shared_1 = (init_dist(), __toCommonJS(dist_exports));
|
|
352086
352177
|
var THUMBNAIL_INTERVAL_MS = parseInt(process.env.LIVE_BROWSER_THUMBNAIL_INTERVAL_MS ?? "4000", 10) || 4e3;
|
|
352087
352178
|
var THUMBNAIL_QUALITY = parseInt(process.env.LIVE_BROWSER_THUMBNAIL_QUALITY ?? "60", 10) || 60;
|
|
352088
|
-
var BROWSER_THUMBNAIL_MAX_BASE64_BYTES = parseInt(process.env.LIVE_BROWSER_THUMBNAIL_MAX_BASE64_BYTES ?? "400000", 10) || 4e5;
|
|
352089
|
-
var MOBILE_THUMBNAIL_MAX_BASE64_BYTES = parseInt(process.env.LIVE_MOBILE_THUMBNAIL_MAX_BASE64_BYTES ?? "4500000", 10) || 45e5;
|
|
352090
352179
|
var STALE_SESSION_MS = parseInt(process.env.LIVE_BROWSER_STALE_MS ?? `${10 * 60 * 1e3}`, 10) || 10 * 60 * 1e3;
|
|
352091
352180
|
var MAX_SESSIONS = parseInt(process.env.LIVE_BROWSER_MAX_SESSIONS ?? "32", 10) || 32;
|
|
352092
352181
|
var LiveBrowserRegistry = class {
|
|
352093
352182
|
entries = /* @__PURE__ */ new Map();
|
|
352094
352183
|
snapshotTimer = null;
|
|
352095
352184
|
capturing = false;
|
|
352185
|
+
publisher = null;
|
|
352096
352186
|
/** Register a new session. Returns a handle the caller must hold and
|
|
352097
352187
|
* ultimately call .unregister() on. */
|
|
352098
352188
|
register(meta) {
|
|
@@ -352106,7 +352196,10 @@ var require_live_browser_registry = __commonJS({
|
|
|
352106
352196
|
lastUpdatedAt: now,
|
|
352107
352197
|
page: null,
|
|
352108
352198
|
isCapturing: false,
|
|
352109
|
-
pendingCapture: false
|
|
352199
|
+
pendingCapture: false,
|
|
352200
|
+
screencast: null,
|
|
352201
|
+
screencastStarting: false,
|
|
352202
|
+
lastPublishedAt: 0
|
|
352110
352203
|
};
|
|
352111
352204
|
this.entries.set(meta.sessionId, entry);
|
|
352112
352205
|
this.ensureSnapshotLoop();
|
|
@@ -352119,19 +352212,40 @@ var require_live_browser_registry = __commonJS({
|
|
|
352119
352212
|
unregister: () => this.unregister(meta.sessionId)
|
|
352120
352213
|
};
|
|
352121
352214
|
}
|
|
352215
|
+
/**
|
|
352216
|
+
* Install the real-time frame sink. Idempotent; the runner calls this once at
|
|
352217
|
+
* startup, before any session registers.
|
|
352218
|
+
*/
|
|
352219
|
+
setFramePublisher(publisher) {
|
|
352220
|
+
this.publisher = publisher;
|
|
352221
|
+
}
|
|
352122
352222
|
unregister(sessionId) {
|
|
352123
352223
|
const entry = this.entries.get(sessionId);
|
|
352124
352224
|
if (!entry)
|
|
352125
352225
|
return;
|
|
352126
|
-
|
|
352127
|
-
this.entries.delete(sessionId);
|
|
352226
|
+
this.dropEntry(sessionId, entry);
|
|
352128
352227
|
if (this.entries.size === 0)
|
|
352129
352228
|
this.stopSnapshotLoop();
|
|
352130
352229
|
}
|
|
352230
|
+
/**
|
|
352231
|
+
* The ONLY way an entry leaves `entries`. Centralised because a screencast
|
|
352232
|
+
* outlives the map otherwise: its frame handler closes over the entry rather
|
|
352233
|
+
* than looking it up, so an entry deleted without stopScreencast() leaves
|
|
352234
|
+
* Chromium encoding and acking frames for a page nobody is watching, holding
|
|
352235
|
+
* the CDP session and Page alive. Eviction and the staleness sweep both used
|
|
352236
|
+
* to delete directly and leaked exactly that way.
|
|
352237
|
+
*/
|
|
352238
|
+
dropEntry(sessionId, entry) {
|
|
352239
|
+
void this.stopScreencast(entry);
|
|
352240
|
+
entry.page = null;
|
|
352241
|
+
this.entries.delete(sessionId);
|
|
352242
|
+
}
|
|
352131
352243
|
setPage(sessionId, page) {
|
|
352132
352244
|
const entry = this.entries.get(sessionId);
|
|
352133
352245
|
if (!entry)
|
|
352134
352246
|
return;
|
|
352247
|
+
if (entry.screencast && entry.screencast.page !== page)
|
|
352248
|
+
void this.stopScreencast(entry);
|
|
352135
352249
|
entry.page = page;
|
|
352136
352250
|
entry.lastUpdatedAt = Date.now();
|
|
352137
352251
|
if (page) {
|
|
@@ -352141,6 +352255,119 @@ var require_live_browser_registry = __commonJS({
|
|
|
352141
352255
|
entry.currentUrl = url;
|
|
352142
352256
|
} catch {
|
|
352143
352257
|
}
|
|
352258
|
+
void this.startScreencast(entry, page);
|
|
352259
|
+
}
|
|
352260
|
+
}
|
|
352261
|
+
/**
|
|
352262
|
+
* Begin a CDP screencast for this page. Chromium then pushes a JPEG on every
|
|
352263
|
+
* paint, already downscaled to LIVE_FRAME_MAX_DIMENSION — which is the only
|
|
352264
|
+
* way this runner can downscale at all, since it ships no image library.
|
|
352265
|
+
*
|
|
352266
|
+
* Best-effort by design: any failure leaves `screencast` null and the polling
|
|
352267
|
+
* screenshot loop keeps serving the entry exactly as before.
|
|
352268
|
+
*/
|
|
352269
|
+
async startScreencast(entry, page) {
|
|
352270
|
+
if (!page || entry.screencast || entry.screencastStarting)
|
|
352271
|
+
return;
|
|
352272
|
+
if (page.isClosed?.())
|
|
352273
|
+
return;
|
|
352274
|
+
entry.screencastStarting = true;
|
|
352275
|
+
try {
|
|
352276
|
+
const client = await page.context().newCDPSession(page);
|
|
352277
|
+
const binding = { client, page, stopped: false };
|
|
352278
|
+
client.on("Page.screencastFrame", (params) => {
|
|
352279
|
+
void client.send("Page.screencastFrameAck", { sessionId: params.sessionId }).catch(() => {
|
|
352280
|
+
});
|
|
352281
|
+
if (binding.stopped)
|
|
352282
|
+
return;
|
|
352283
|
+
if (this.entries.get(entry.meta.sessionId) !== entry)
|
|
352284
|
+
return;
|
|
352285
|
+
if (typeof params.data !== "string" || params.data.length === 0)
|
|
352286
|
+
return;
|
|
352287
|
+
const width = params.metadata?.deviceWidth;
|
|
352288
|
+
const height = params.metadata?.deviceHeight;
|
|
352289
|
+
this.acceptFrame(entry, params.data, "image/jpeg", width, height);
|
|
352290
|
+
});
|
|
352291
|
+
page.once("close", () => {
|
|
352292
|
+
void this.stopScreencast(entry);
|
|
352293
|
+
});
|
|
352294
|
+
page.once("crash", () => {
|
|
352295
|
+
void this.stopScreencast(entry);
|
|
352296
|
+
});
|
|
352297
|
+
await client.send("Page.enable");
|
|
352298
|
+
await client.send("Page.startScreencast", {
|
|
352299
|
+
format: "jpeg",
|
|
352300
|
+
quality: shared_1.LIVE_FRAME_JPEG_QUALITY,
|
|
352301
|
+
// Absolute bounds, aspect-preserving — Chromium does the resize.
|
|
352302
|
+
maxWidth: shared_1.LIVE_FRAME_MAX_DIMENSION,
|
|
352303
|
+
maxHeight: shared_1.LIVE_FRAME_MAX_DIMENSION,
|
|
352304
|
+
everyNthFrame: 1
|
|
352305
|
+
});
|
|
352306
|
+
if (this.entries.get(entry.meta.sessionId) !== entry || entry.page !== page) {
|
|
352307
|
+
binding.stopped = true;
|
|
352308
|
+
await client.send("Page.stopScreencast").catch(() => {
|
|
352309
|
+
});
|
|
352310
|
+
await client.detach().catch(() => {
|
|
352311
|
+
});
|
|
352312
|
+
return;
|
|
352313
|
+
}
|
|
352314
|
+
entry.screencast = binding;
|
|
352315
|
+
} catch {
|
|
352316
|
+
} finally {
|
|
352317
|
+
entry.screencastStarting = false;
|
|
352318
|
+
}
|
|
352319
|
+
}
|
|
352320
|
+
async stopScreencast(entry) {
|
|
352321
|
+
const binding = entry.screencast;
|
|
352322
|
+
if (!binding || binding.stopped)
|
|
352323
|
+
return;
|
|
352324
|
+
binding.stopped = true;
|
|
352325
|
+
entry.screencast = null;
|
|
352326
|
+
try {
|
|
352327
|
+
await binding.client.send("Page.stopScreencast");
|
|
352328
|
+
} catch {
|
|
352329
|
+
}
|
|
352330
|
+
try {
|
|
352331
|
+
await binding.client.detach();
|
|
352332
|
+
} catch {
|
|
352333
|
+
}
|
|
352334
|
+
}
|
|
352335
|
+
/**
|
|
352336
|
+
* Record a freshly captured frame and forward it to the real-time publisher.
|
|
352337
|
+
* The stored copy (used by the heartbeat fallback) is only replaced when it
|
|
352338
|
+
* fits the heartbeat budget, but the publisher — which has a far larger budget
|
|
352339
|
+
* of its own — always sees the frame.
|
|
352340
|
+
*/
|
|
352341
|
+
acceptFrame(entry, base64, mimeType, width, height) {
|
|
352342
|
+
const now = Date.now();
|
|
352343
|
+
entry.lastUpdatedAt = now;
|
|
352344
|
+
if (base64.length <= shared_1.LIVE_THUMBNAIL_MAX_BASE64_BYTES) {
|
|
352345
|
+
entry.thumbnailJpegBase64 = base64;
|
|
352346
|
+
entry.thumbnailCapturedAt = now;
|
|
352347
|
+
if (width !== void 0)
|
|
352348
|
+
entry.thumbnailWidth = width;
|
|
352349
|
+
if (height !== void 0)
|
|
352350
|
+
entry.thumbnailHeight = height;
|
|
352351
|
+
}
|
|
352352
|
+
const publisher = this.publisher;
|
|
352353
|
+
if (!publisher)
|
|
352354
|
+
return;
|
|
352355
|
+
if (now - entry.lastPublishedAt < shared_1.LIVE_FRAME_MIN_PUSH_INTERVAL_MS)
|
|
352356
|
+
return;
|
|
352357
|
+
entry.lastPublishedAt = now;
|
|
352358
|
+
try {
|
|
352359
|
+
publisher({
|
|
352360
|
+
sessionId: entry.meta.sessionId,
|
|
352361
|
+
projectId: entry.meta.projectId,
|
|
352362
|
+
runId: entry.meta.runId,
|
|
352363
|
+
capturedAt: now,
|
|
352364
|
+
mimeType,
|
|
352365
|
+
width,
|
|
352366
|
+
height,
|
|
352367
|
+
surface: entry.meta.surface,
|
|
352368
|
+
base64
|
|
352369
|
+
});
|
|
352370
|
+
} catch {
|
|
352144
352371
|
}
|
|
352145
352372
|
}
|
|
352146
352373
|
setCurrentUrl(sessionId, url) {
|
|
@@ -352152,7 +352379,7 @@ var require_live_browser_registry = __commonJS({
|
|
|
352152
352379
|
const previousUrl = entry.currentUrl;
|
|
352153
352380
|
entry.currentUrl = url;
|
|
352154
352381
|
entry.lastUpdatedAt = Date.now();
|
|
352155
|
-
if (url && previousUrl && url !== previousUrl) {
|
|
352382
|
+
if (url && previousUrl && url !== previousUrl && !entry.screencast) {
|
|
352156
352383
|
entry.pendingCapture = true;
|
|
352157
352384
|
const schedule = (delay) => {
|
|
352158
352385
|
setTimeout(() => {
|
|
@@ -352173,6 +352400,11 @@ var require_live_browser_registry = __commonJS({
|
|
|
352173
352400
|
schedule(100);
|
|
352174
352401
|
}
|
|
352175
352402
|
}
|
|
352403
|
+
/**
|
|
352404
|
+
* Ingest a frame captured outside this module — the mobile mirror and the
|
|
352405
|
+
* spawned `playwright test` child (whose Page lives in another process, so no
|
|
352406
|
+
* CDP screencast is reachable from here) both arrive this way.
|
|
352407
|
+
*/
|
|
352176
352408
|
updateSnapshot(sessionId, snapshot) {
|
|
352177
352409
|
const entry = this.entries.get(sessionId);
|
|
352178
352410
|
if (!entry)
|
|
@@ -352181,12 +352413,10 @@ var require_live_browser_registry = __commonJS({
|
|
|
352181
352413
|
entry.currentUrl = snapshot.currentUrl;
|
|
352182
352414
|
}
|
|
352183
352415
|
const thumbnail = snapshot.thumbnailJpegBase64;
|
|
352184
|
-
|
|
352185
|
-
|
|
352186
|
-
|
|
352187
|
-
entry
|
|
352188
|
-
entry.thumbnailWidth = typeof snapshot.thumbnailWidth === "number" && Number.isFinite(snapshot.thumbnailWidth) ? snapshot.thumbnailWidth : void 0;
|
|
352189
|
-
entry.thumbnailHeight = typeof snapshot.thumbnailHeight === "number" && Number.isFinite(snapshot.thumbnailHeight) ? snapshot.thumbnailHeight : void 0;
|
|
352416
|
+
if (thumbnail) {
|
|
352417
|
+
const width = typeof snapshot.thumbnailWidth === "number" && Number.isFinite(snapshot.thumbnailWidth) ? snapshot.thumbnailWidth : void 0;
|
|
352418
|
+
const height = typeof snapshot.thumbnailHeight === "number" && Number.isFinite(snapshot.thumbnailHeight) ? snapshot.thumbnailHeight : void 0;
|
|
352419
|
+
this.acceptFrame(entry, thumbnail, snapshot.mimeType ?? "image/jpeg", width, height);
|
|
352190
352420
|
}
|
|
352191
352421
|
entry.lastUpdatedAt = Date.now();
|
|
352192
352422
|
}
|
|
@@ -352229,9 +352459,11 @@ var require_live_browser_registry = __commonJS({
|
|
|
352229
352459
|
async captureAllNow() {
|
|
352230
352460
|
await this.captureAll();
|
|
352231
352461
|
}
|
|
352232
|
-
/** Stop the snapshot loop and clear all entries. Idempotent. */
|
|
352462
|
+
/** Stop the snapshot loop, tear down every screencast, and clear all entries. Idempotent. */
|
|
352233
352463
|
shutdown() {
|
|
352234
352464
|
this.stopSnapshotLoop();
|
|
352465
|
+
for (const entry of this.entries.values())
|
|
352466
|
+
void this.stopScreencast(entry);
|
|
352235
352467
|
this.entries.clear();
|
|
352236
352468
|
}
|
|
352237
352469
|
ensureSnapshotLoop() {
|
|
@@ -352259,15 +352491,13 @@ var require_live_browser_registry = __commonJS({
|
|
|
352259
352491
|
}
|
|
352260
352492
|
}
|
|
352261
352493
|
if (oldestKey) {
|
|
352262
|
-
this.entries.get(oldestKey)
|
|
352263
|
-
this.entries.delete(oldestKey);
|
|
352494
|
+
this.dropEntry(oldestKey, this.entries.get(oldestKey));
|
|
352264
352495
|
}
|
|
352265
352496
|
}
|
|
352266
352497
|
sweepStale(now) {
|
|
352267
352498
|
for (const [key, entry] of this.entries) {
|
|
352268
352499
|
if (now - entry.lastUpdatedAt > STALE_SESSION_MS) {
|
|
352269
|
-
|
|
352270
|
-
this.entries.delete(key);
|
|
352500
|
+
this.dropEntry(key, entry);
|
|
352271
352501
|
}
|
|
352272
352502
|
}
|
|
352273
352503
|
}
|
|
@@ -352308,6 +352538,11 @@ var require_live_browser_registry = __commonJS({
|
|
|
352308
352538
|
entry.currentUrl = url;
|
|
352309
352539
|
} catch {
|
|
352310
352540
|
}
|
|
352541
|
+
if (entry.screencast) {
|
|
352542
|
+
if (entry.thumbnailJpegBase64)
|
|
352543
|
+
entry.thumbnailCapturedAt = Date.now();
|
|
352544
|
+
return;
|
|
352545
|
+
}
|
|
352311
352546
|
const buf = await page.screenshot({
|
|
352312
352547
|
type: "jpeg",
|
|
352313
352548
|
quality: THUMBNAIL_QUALITY,
|
|
@@ -352315,17 +352550,8 @@ var require_live_browser_registry = __commonJS({
|
|
|
352315
352550
|
timeout: 3e3,
|
|
352316
352551
|
scale: "css"
|
|
352317
352552
|
});
|
|
352318
|
-
const base64 = buf.toString("base64");
|
|
352319
|
-
if (base64.length > BROWSER_THUMBNAIL_MAX_BASE64_BYTES) {
|
|
352320
|
-
return;
|
|
352321
|
-
}
|
|
352322
|
-
entry.thumbnailJpegBase64 = base64;
|
|
352323
|
-
entry.thumbnailCapturedAt = Date.now();
|
|
352324
352553
|
const viewport = page.viewportSize?.();
|
|
352325
|
-
|
|
352326
|
-
entry.thumbnailWidth = viewport.width;
|
|
352327
|
-
entry.thumbnailHeight = viewport.height;
|
|
352328
|
-
}
|
|
352554
|
+
this.acceptFrame(entry, buf.toString("base64"), "image/jpeg", viewport?.width, viewport?.height);
|
|
352329
352555
|
} catch {
|
|
352330
352556
|
} finally {
|
|
352331
352557
|
entry.isCapturing = false;
|
|
@@ -353039,7 +353265,7 @@ var require_package4 = __commonJS({
|
|
|
353039
353265
|
"package.json"(exports, module) {
|
|
353040
353266
|
module.exports = {
|
|
353041
353267
|
name: "@validate.qa/runner",
|
|
353042
|
-
version: "1.0.
|
|
353268
|
+
version: "1.0.79",
|
|
353043
353269
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
353044
353270
|
bin: {
|
|
353045
353271
|
"validate-runner": "dist/cli.js"
|
|
@@ -353831,6 +354057,24 @@ async function getScreenshot(sessionPath) {
|
|
|
353831
354057
|
return null;
|
|
353832
354058
|
}
|
|
353833
354059
|
}
|
|
354060
|
+
async function applyMjpegSettings(sessionPath, settings) {
|
|
354061
|
+
try {
|
|
354062
|
+
await appiumRequest(`${sessionPath}/appium/settings`, {
|
|
354063
|
+
method: "POST",
|
|
354064
|
+
body: JSON.stringify({
|
|
354065
|
+
settings: {
|
|
354066
|
+
mjpegServerFramerate: settings.framerate,
|
|
354067
|
+
mjpegScalingFactor: settings.scalingFactor,
|
|
354068
|
+
mjpegServerScreenshotQuality: settings.quality
|
|
354069
|
+
}
|
|
354070
|
+
})
|
|
354071
|
+
});
|
|
354072
|
+
return true;
|
|
354073
|
+
} catch (err) {
|
|
354074
|
+
if (isAppiumDriverDeadError(err)) throw err;
|
|
354075
|
+
return false;
|
|
354076
|
+
}
|
|
354077
|
+
}
|
|
353834
354078
|
async function getPageSource(sessionPath) {
|
|
353835
354079
|
try {
|
|
353836
354080
|
const value = readValue(await appiumRequest(`${sessionPath}/source`));
|
|
@@ -356342,7 +356586,15 @@ function mobileParallelSessionCapabilities(platform3, portOffset = 0, deviceId =
|
|
|
356342
356586
|
return (Number.isFinite(parsed) ? parsed : fallback) + offset;
|
|
356343
356587
|
};
|
|
356344
356588
|
if (platform3 === "ANDROID") {
|
|
356345
|
-
return {
|
|
356589
|
+
return {
|
|
356590
|
+
"appium:systemPort": readPort("APPIUM_SYSTEM_PORT", 8200),
|
|
356591
|
+
// Host port that UiAutomator2 forwards the on-device MJPEG broadcaster
|
|
356592
|
+
// (device port 7810) to. Without this the live mirror has to poll
|
|
356593
|
+
// WebDriver /screenshot for full-resolution PNGs, which the runner cannot
|
|
356594
|
+
// downscale itself. Base differs from iOS's 9100 so a concurrent
|
|
356595
|
+
// Android+iOS pair can never collide on a shared slot offset.
|
|
356596
|
+
"appium:mjpegServerPort": readPort("APPIUM_ANDROID_MJPEG_SERVER_PORT", 7810)
|
|
356597
|
+
};
|
|
356346
356598
|
}
|
|
356347
356599
|
const safeDevice = deviceId.replace(/[^A-Za-z0-9._-]+/g, "-") || `device-${offset}`;
|
|
356348
356600
|
const derivedDataRoot = env.APPIUM_DERIVED_DATA_PATH?.trim() || join5(tmpdir3(), "validateqa-wda");
|
|
@@ -357597,6 +357849,15 @@ function acquireControlPortSlot(deviceId) {
|
|
|
357597
357849
|
function releaseControlPortSlot(deviceId) {
|
|
357598
357850
|
controlPortSlotsByDevice.delete(deviceId);
|
|
357599
357851
|
}
|
|
357852
|
+
function getControlPortSlot(deviceId) {
|
|
357853
|
+
return controlPortSlotsByDevice.get(deviceId);
|
|
357854
|
+
}
|
|
357855
|
+
function mobileMjpegPortForDevice(platform3, deviceId, env = process.env) {
|
|
357856
|
+
const slot = getControlPortSlot(deviceId);
|
|
357857
|
+
if (slot === void 0) return null;
|
|
357858
|
+
const port = mobileParallelSessionCapabilities(platform3, slot, deviceId, env)["appium:mjpegServerPort"];
|
|
357859
|
+
return typeof port === "number" && Number.isFinite(port) ? port : null;
|
|
357860
|
+
}
|
|
357600
357861
|
var PINNED_DEVICE_SNAPSHOT_TTL_MS = 25 * 6e4;
|
|
357601
357862
|
var pinnedDeviceSnapshot = null;
|
|
357602
357863
|
async function executeMobileTest(options) {
|
|
@@ -358541,6 +358802,100 @@ import { randomBytes, timingSafeEqual } from "crypto";
|
|
|
358541
358802
|
import { homedir as homedir2 } from "os";
|
|
358542
358803
|
import { join as join6, dirname } from "path";
|
|
358543
358804
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
358805
|
+
|
|
358806
|
+
// src/services/mobile/mjpeg-stream.ts
|
|
358807
|
+
init_dist();
|
|
358808
|
+
import { request as httpRequest } from "http";
|
|
358809
|
+
var JPEG_SOI = Buffer.from([255, 216]);
|
|
358810
|
+
var JPEG_EOI = Buffer.from([255, 217]);
|
|
358811
|
+
function startMjpegStream(options) {
|
|
358812
|
+
const { port, host = "127.0.0.1", onFrame, onError } = options;
|
|
358813
|
+
let stopped = false;
|
|
358814
|
+
let req = null;
|
|
358815
|
+
let response = null;
|
|
358816
|
+
let buffer = Buffer.alloc(0);
|
|
358817
|
+
const fail = (error2) => {
|
|
358818
|
+
if (stopped) return;
|
|
358819
|
+
stopped = true;
|
|
358820
|
+
try {
|
|
358821
|
+
req?.destroy();
|
|
358822
|
+
} catch {
|
|
358823
|
+
}
|
|
358824
|
+
try {
|
|
358825
|
+
response?.destroy();
|
|
358826
|
+
} catch {
|
|
358827
|
+
}
|
|
358828
|
+
buffer = Buffer.alloc(0);
|
|
358829
|
+
onError?.(error2);
|
|
358830
|
+
};
|
|
358831
|
+
const consume = (chunk) => {
|
|
358832
|
+
if (stopped) return;
|
|
358833
|
+
buffer = buffer.length === 0 ? chunk : Buffer.concat([buffer, chunk]);
|
|
358834
|
+
for (; ; ) {
|
|
358835
|
+
const start = buffer.indexOf(JPEG_SOI);
|
|
358836
|
+
if (start < 0) {
|
|
358837
|
+
if (buffer.length > 1) buffer = buffer.subarray(buffer.length - 1);
|
|
358838
|
+
return;
|
|
358839
|
+
}
|
|
358840
|
+
const end = buffer.indexOf(JPEG_EOI, start + JPEG_SOI.length);
|
|
358841
|
+
if (end < 0) {
|
|
358842
|
+
if (start > 0) buffer = buffer.subarray(start);
|
|
358843
|
+
if (buffer.length > MOBILE_MJPEG_MAX_FRAME_BYTES) buffer = Buffer.alloc(0);
|
|
358844
|
+
return;
|
|
358845
|
+
}
|
|
358846
|
+
const frameEnd = end + JPEG_EOI.length;
|
|
358847
|
+
const frame = buffer.subarray(start, frameEnd);
|
|
358848
|
+
buffer = buffer.subarray(frameEnd);
|
|
358849
|
+
try {
|
|
358850
|
+
onFrame(frame);
|
|
358851
|
+
} catch {
|
|
358852
|
+
}
|
|
358853
|
+
}
|
|
358854
|
+
};
|
|
358855
|
+
try {
|
|
358856
|
+
req = httpRequest(
|
|
358857
|
+
{ host, port, path: "/", method: "GET", headers: { Connection: "keep-alive" } },
|
|
358858
|
+
(res) => {
|
|
358859
|
+
response = res;
|
|
358860
|
+
if (stopped) {
|
|
358861
|
+
res.destroy();
|
|
358862
|
+
return;
|
|
358863
|
+
}
|
|
358864
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
358865
|
+
fail(new Error(`MJPEG broadcaster returned HTTP ${res.statusCode}`));
|
|
358866
|
+
res.destroy();
|
|
358867
|
+
return;
|
|
358868
|
+
}
|
|
358869
|
+
res.on("data", (chunk) => consume(chunk));
|
|
358870
|
+
res.on("end", () => fail(new Error("MJPEG stream ended")));
|
|
358871
|
+
res.on("error", (err) => fail(err));
|
|
358872
|
+
}
|
|
358873
|
+
);
|
|
358874
|
+
req.on("error", (err) => fail(err));
|
|
358875
|
+
req.setTimeout(1e4, () => fail(new Error("MJPEG connect timeout")));
|
|
358876
|
+
req.end();
|
|
358877
|
+
} catch (err) {
|
|
358878
|
+
fail(err instanceof Error ? err : new Error(String(err)));
|
|
358879
|
+
}
|
|
358880
|
+
return {
|
|
358881
|
+
stop: () => {
|
|
358882
|
+
if (stopped) return;
|
|
358883
|
+
stopped = true;
|
|
358884
|
+
try {
|
|
358885
|
+
req?.destroy();
|
|
358886
|
+
} catch {
|
|
358887
|
+
}
|
|
358888
|
+
try {
|
|
358889
|
+
response?.destroy();
|
|
358890
|
+
} catch {
|
|
358891
|
+
}
|
|
358892
|
+
buffer = Buffer.alloc(0);
|
|
358893
|
+
},
|
|
358894
|
+
isActive: () => !stopped
|
|
358895
|
+
};
|
|
358896
|
+
}
|
|
358897
|
+
|
|
358898
|
+
// src/services/mobile-bridge.ts
|
|
358544
358899
|
init_dist();
|
|
358545
358900
|
var MOBILE_BRIDGE_PORT2 = 9515;
|
|
358546
358901
|
var MAX_REQUEST_BODY_BYTES = 64 * 1024;
|
|
@@ -359094,16 +359449,66 @@ function writeFrameToChannelClient(ch, client, frame) {
|
|
|
359094
359449
|
} catch {
|
|
359095
359450
|
}
|
|
359096
359451
|
}
|
|
359452
|
+
function broadcastChannelFrame(ch, frame) {
|
|
359453
|
+
ch.lastFrame = frame;
|
|
359454
|
+
for (const client of ch.clients) writeFrameToChannelClient(ch, client, frame);
|
|
359455
|
+
}
|
|
359456
|
+
function tryStartChannelMjpeg(ch) {
|
|
359457
|
+
if (ch.mjpeg || ch.mjpegStarting || ch.mjpegUnavailable) return;
|
|
359458
|
+
const port = mobileMjpegPortForDevice(ch.platform, ch.deviceId);
|
|
359459
|
+
if (port === null) {
|
|
359460
|
+
ch.mjpegUnavailable = true;
|
|
359461
|
+
return;
|
|
359462
|
+
}
|
|
359463
|
+
ch.mjpegStarting = true;
|
|
359464
|
+
const handle = startMjpegStream({
|
|
359465
|
+
port,
|
|
359466
|
+
onFrame: (jpeg) => {
|
|
359467
|
+
const isOwned = ch.mjpeg === handle;
|
|
359468
|
+
const isCandidate = ch.mjpeg === null && ch.mjpegStarting;
|
|
359469
|
+
if (!isOwned && !isCandidate) return;
|
|
359470
|
+
ch.mjpegLastFrameAt = Date.now();
|
|
359471
|
+
ch.nullTicks = 0;
|
|
359472
|
+
if (isCandidate) {
|
|
359473
|
+
ch.mjpeg = handle;
|
|
359474
|
+
ch.mjpegStarting = false;
|
|
359475
|
+
if (ch.pollTimer) {
|
|
359476
|
+
clearInterval(ch.pollTimer);
|
|
359477
|
+
ch.pollTimer = null;
|
|
359478
|
+
}
|
|
359479
|
+
}
|
|
359480
|
+
broadcastChannelFrame(ch, {
|
|
359481
|
+
timestamp: ch.mjpegLastFrameAt,
|
|
359482
|
+
screenshot: jpeg.toString("base64"),
|
|
359483
|
+
source: null,
|
|
359484
|
+
mimeType: "image/jpeg"
|
|
359485
|
+
});
|
|
359486
|
+
},
|
|
359487
|
+
onError: () => {
|
|
359488
|
+
const wasActive = ch.mjpeg === handle;
|
|
359489
|
+
ch.mjpeg = null;
|
|
359490
|
+
ch.mjpegStarting = false;
|
|
359491
|
+
ch.mjpegUnavailable = true;
|
|
359492
|
+
if (wasActive && ch.clients.size > 0) ensureChannelPolling(ch);
|
|
359493
|
+
}
|
|
359494
|
+
});
|
|
359495
|
+
const firstFrameDeadline = setTimeout(() => {
|
|
359496
|
+
if (ch.mjpeg) return;
|
|
359497
|
+
ch.mjpegStarting = false;
|
|
359498
|
+
ch.mjpegUnavailable = true;
|
|
359499
|
+
handle.stop();
|
|
359500
|
+
if (ch.clients.size > 0) ensureChannelPolling(ch);
|
|
359501
|
+
}, MOBILE_MJPEG_FIRST_FRAME_TIMEOUT_MS);
|
|
359502
|
+
firstFrameDeadline.unref?.();
|
|
359503
|
+
}
|
|
359097
359504
|
async function pollRunnerChannel(ch) {
|
|
359098
359505
|
if (ch.clients.size === 0 || ch.polling) return;
|
|
359506
|
+
if (ch.mjpeg) return;
|
|
359099
359507
|
ch.polling = true;
|
|
359100
359508
|
try {
|
|
359101
359509
|
validateSessionId(ch.sessionId);
|
|
359102
|
-
const
|
|
359103
|
-
|
|
359104
|
-
getPageSource(`/session/${ch.sessionId}`)
|
|
359105
|
-
]);
|
|
359106
|
-
if (!screenshot && !source) {
|
|
359510
|
+
const screenshot = await getScreenshot(`/session/${ch.sessionId}`);
|
|
359511
|
+
if (!screenshot) {
|
|
359107
359512
|
ch.nullTicks++;
|
|
359108
359513
|
if (ch.nullTicks >= MIRROR_DEAD_SESSION_TICKS) {
|
|
359109
359514
|
const alive = await isSessionAlive(ch.sessionId);
|
|
@@ -359116,32 +359521,45 @@ async function pollRunnerChannel(ch) {
|
|
|
359116
359521
|
} else {
|
|
359117
359522
|
ch.nullTicks = 0;
|
|
359118
359523
|
}
|
|
359119
|
-
|
|
359120
|
-
|
|
359121
|
-
|
|
359524
|
+
broadcastChannelFrame(ch, {
|
|
359525
|
+
timestamp: Date.now(),
|
|
359526
|
+
screenshot: screenshot ?? null,
|
|
359527
|
+
source: null,
|
|
359528
|
+
mimeType: "image/png"
|
|
359529
|
+
});
|
|
359122
359530
|
} catch {
|
|
359123
359531
|
} finally {
|
|
359124
359532
|
ch.polling = false;
|
|
359125
359533
|
}
|
|
359126
359534
|
}
|
|
359127
359535
|
function ensureChannelPolling(ch) {
|
|
359128
|
-
|
|
359536
|
+
tryStartChannelMjpeg(ch);
|
|
359537
|
+
if (ch.mjpeg || ch.pollTimer) return;
|
|
359129
359538
|
ch.pollTimer = setInterval(() => {
|
|
359130
359539
|
void pollRunnerChannel(ch);
|
|
359131
359540
|
}, MOBILE_SCREENSHOT_INTERVAL_MS);
|
|
359132
359541
|
ch.pollTimer.unref?.();
|
|
359133
359542
|
}
|
|
359134
359543
|
function stopChannelPollingIfIdle(ch) {
|
|
359135
|
-
if (ch.clients.size
|
|
359544
|
+
if (ch.clients.size > 0) return;
|
|
359545
|
+
if (ch.pollTimer) {
|
|
359136
359546
|
clearInterval(ch.pollTimer);
|
|
359137
359547
|
ch.pollTimer = null;
|
|
359138
359548
|
}
|
|
359549
|
+
if (ch.mjpeg) {
|
|
359550
|
+
ch.mjpeg.stop();
|
|
359551
|
+
ch.mjpeg = null;
|
|
359552
|
+
}
|
|
359139
359553
|
}
|
|
359140
359554
|
function closeRunnerChannel(ch) {
|
|
359141
359555
|
if (ch.pollTimer) {
|
|
359142
359556
|
clearInterval(ch.pollTimer);
|
|
359143
359557
|
ch.pollTimer = null;
|
|
359144
359558
|
}
|
|
359559
|
+
if (ch.mjpeg) {
|
|
359560
|
+
ch.mjpeg.stop();
|
|
359561
|
+
ch.mjpeg = null;
|
|
359562
|
+
}
|
|
359145
359563
|
for (const client of ch.clients) {
|
|
359146
359564
|
try {
|
|
359147
359565
|
client.end();
|
|
@@ -359726,12 +360144,24 @@ function attachMirrorSession(sessionId, platform3, deviceId) {
|
|
|
359726
360144
|
lastFrame: null,
|
|
359727
360145
|
pollTimer: null,
|
|
359728
360146
|
nullTicks: 0,
|
|
359729
|
-
polling: false
|
|
360147
|
+
polling: false,
|
|
360148
|
+
mjpeg: null,
|
|
360149
|
+
mjpegLastFrameAt: 0,
|
|
360150
|
+
mjpegStarting: false,
|
|
360151
|
+
mjpegUnavailable: false
|
|
359730
360152
|
};
|
|
359731
360153
|
ch.sessionId = sessionId;
|
|
359732
360154
|
ch.platform = platform3;
|
|
359733
360155
|
ch.nullTicks = 0;
|
|
360156
|
+
if (ch.mjpeg) {
|
|
360157
|
+
ch.mjpeg.stop();
|
|
360158
|
+
ch.mjpeg = null;
|
|
360159
|
+
}
|
|
360160
|
+
ch.mjpegUnavailable = false;
|
|
360161
|
+
ch.mjpegStarting = false;
|
|
359734
360162
|
runnerChannels.set(deviceId, ch);
|
|
360163
|
+
void applyMjpegSettings(`/session/${sessionId}`, MOBILE_MJPEG_SETTINGS).catch(() => {
|
|
360164
|
+
});
|
|
359735
360165
|
if (ch.clients.size > 0) {
|
|
359736
360166
|
ensureChannelPolling(ch);
|
|
359737
360167
|
void pollRunnerChannel(ch);
|
|
@@ -362915,7 +363345,9 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
362915
363345
|
liveBrowserHandle.updateSnapshot({
|
|
362916
363346
|
currentUrl: mobileTarget.appId,
|
|
362917
363347
|
thumbnailJpegBase64: base64,
|
|
362918
|
-
thumbnailCapturedAt: Date.now()
|
|
363348
|
+
thumbnailCapturedAt: Date.now(),
|
|
363349
|
+
// Appium's screenshot endpoint returns PNG despite the field name.
|
|
363350
|
+
mimeType: "image/png"
|
|
362919
363351
|
});
|
|
362920
363352
|
requestLiveBrowserHeartbeat();
|
|
362921
363353
|
},
|
|
@@ -364160,6 +364592,22 @@ async function startRunner(opts) {
|
|
|
364160
364592
|
} catch {
|
|
364161
364593
|
}
|
|
364162
364594
|
};
|
|
364595
|
+
const liveFramePushInFlight = /* @__PURE__ */ new Set();
|
|
364596
|
+
const publishLiveFrame = (frame) => {
|
|
364597
|
+
if (!frame.projectId) return;
|
|
364598
|
+
if (liveFramePushInFlight.has(frame.sessionId)) return;
|
|
364599
|
+
liveFramePushInFlight.add(frame.sessionId);
|
|
364600
|
+
void fetch(`${serverUrl}/api/runner/live-frame`, {
|
|
364601
|
+
method: "POST",
|
|
364602
|
+
headers,
|
|
364603
|
+
body: JSON.stringify(frame),
|
|
364604
|
+
signal: AbortSignal.timeout(5e3)
|
|
364605
|
+
}).catch(() => {
|
|
364606
|
+
}).finally(() => {
|
|
364607
|
+
liveFramePushInFlight.delete(frame.sessionId);
|
|
364608
|
+
});
|
|
364609
|
+
};
|
|
364610
|
+
import_runner_core11.liveBrowserRegistry.setFramePublisher(publishLiveFrame);
|
|
364163
364611
|
let liveBrowserHeartbeatInFlight = false;
|
|
364164
364612
|
let liveBrowserHeartbeatPending = false;
|
|
364165
364613
|
let lastLiveBrowserHeartbeatAt = 0;
|