@validate.qa/runner 1.0.78 → 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 +467 -45
- package/dist/cli.mjs +465 -43
- 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();
|
|
@@ -87962,6 +88025,7 @@ var require_playwright_native = __commonJS({
|
|
|
87962
88025
|
var PACKAGE_DIR = (0, path_1.resolve)(__dirname, "..", "..");
|
|
87963
88026
|
var RUNS_DIR = process.env.RUNS_DIR || (0, path_1.join)(PACKAGE_DIR, ".qa-runs");
|
|
87964
88027
|
var PW_CLI = __require.resolve("@playwright/test/cli");
|
|
88028
|
+
var LIVE_BROWSER_STATE_POLL_INTERVAL_MS = 500;
|
|
87965
88029
|
async function executeTestViaNativePlaywright2(options) {
|
|
87966
88030
|
const { playwrightCode, baseUrl, testName, testVariables, authStateCapturePath, storageStatePath, extraHttpHeaders, authStandingMode, validateContext, liveBrowserHandle, onLiveBrowserSnapshot } = options;
|
|
87967
88031
|
const logs2 = [];
|
|
@@ -88051,7 +88115,7 @@ test.beforeEach(async ({ page }, testInfo) => {
|
|
|
88051
88115
|
await __qaLiveBrowserCapture();
|
|
88052
88116
|
__qaLiveBrowserInterval = setInterval(() => {
|
|
88053
88117
|
void __qaLiveBrowserCapture?.();
|
|
88054
|
-
},
|
|
88118
|
+
}, ${LIVE_BROWSER_STATE_POLL_INTERVAL_MS});
|
|
88055
88119
|
});
|
|
88056
88120
|
|
|
88057
88121
|
test.afterEach(async () => {
|
|
@@ -88468,7 +88532,7 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
|
|
|
88468
88532
|
};
|
|
88469
88533
|
const interval = setInterval(() => {
|
|
88470
88534
|
void poll();
|
|
88471
|
-
},
|
|
88535
|
+
}, LIVE_BROWSER_STATE_POLL_INTERVAL_MS);
|
|
88472
88536
|
interval.unref?.();
|
|
88473
88537
|
void poll();
|
|
88474
88538
|
return async () => {
|
|
@@ -352109,16 +352173,16 @@ var require_live_browser_registry = __commonJS({
|
|
|
352109
352173
|
"use strict";
|
|
352110
352174
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
352111
352175
|
exports.liveBrowserRegistry = void 0;
|
|
352176
|
+
var shared_1 = (init_dist(), __toCommonJS(dist_exports));
|
|
352112
352177
|
var THUMBNAIL_INTERVAL_MS = parseInt(process.env.LIVE_BROWSER_THUMBNAIL_INTERVAL_MS ?? "4000", 10) || 4e3;
|
|
352113
352178
|
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
352179
|
var STALE_SESSION_MS = parseInt(process.env.LIVE_BROWSER_STALE_MS ?? `${10 * 60 * 1e3}`, 10) || 10 * 60 * 1e3;
|
|
352117
352180
|
var MAX_SESSIONS = parseInt(process.env.LIVE_BROWSER_MAX_SESSIONS ?? "32", 10) || 32;
|
|
352118
352181
|
var LiveBrowserRegistry = class {
|
|
352119
352182
|
entries = /* @__PURE__ */ new Map();
|
|
352120
352183
|
snapshotTimer = null;
|
|
352121
352184
|
capturing = false;
|
|
352185
|
+
publisher = null;
|
|
352122
352186
|
/** Register a new session. Returns a handle the caller must hold and
|
|
352123
352187
|
* ultimately call .unregister() on. */
|
|
352124
352188
|
register(meta) {
|
|
@@ -352132,7 +352196,10 @@ var require_live_browser_registry = __commonJS({
|
|
|
352132
352196
|
lastUpdatedAt: now,
|
|
352133
352197
|
page: null,
|
|
352134
352198
|
isCapturing: false,
|
|
352135
|
-
pendingCapture: false
|
|
352199
|
+
pendingCapture: false,
|
|
352200
|
+
screencast: null,
|
|
352201
|
+
screencastStarting: false,
|
|
352202
|
+
lastPublishedAt: 0
|
|
352136
352203
|
};
|
|
352137
352204
|
this.entries.set(meta.sessionId, entry);
|
|
352138
352205
|
this.ensureSnapshotLoop();
|
|
@@ -352145,19 +352212,40 @@ var require_live_browser_registry = __commonJS({
|
|
|
352145
352212
|
unregister: () => this.unregister(meta.sessionId)
|
|
352146
352213
|
};
|
|
352147
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
|
+
}
|
|
352148
352222
|
unregister(sessionId) {
|
|
352149
352223
|
const entry = this.entries.get(sessionId);
|
|
352150
352224
|
if (!entry)
|
|
352151
352225
|
return;
|
|
352152
|
-
|
|
352153
|
-
this.entries.delete(sessionId);
|
|
352226
|
+
this.dropEntry(sessionId, entry);
|
|
352154
352227
|
if (this.entries.size === 0)
|
|
352155
352228
|
this.stopSnapshotLoop();
|
|
352156
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
|
+
}
|
|
352157
352243
|
setPage(sessionId, page) {
|
|
352158
352244
|
const entry = this.entries.get(sessionId);
|
|
352159
352245
|
if (!entry)
|
|
352160
352246
|
return;
|
|
352247
|
+
if (entry.screencast && entry.screencast.page !== page)
|
|
352248
|
+
void this.stopScreencast(entry);
|
|
352161
352249
|
entry.page = page;
|
|
352162
352250
|
entry.lastUpdatedAt = Date.now();
|
|
352163
352251
|
if (page) {
|
|
@@ -352167,6 +352255,119 @@ var require_live_browser_registry = __commonJS({
|
|
|
352167
352255
|
entry.currentUrl = url;
|
|
352168
352256
|
} catch {
|
|
352169
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 {
|
|
352170
352371
|
}
|
|
352171
352372
|
}
|
|
352172
352373
|
setCurrentUrl(sessionId, url) {
|
|
@@ -352178,7 +352379,7 @@ var require_live_browser_registry = __commonJS({
|
|
|
352178
352379
|
const previousUrl = entry.currentUrl;
|
|
352179
352380
|
entry.currentUrl = url;
|
|
352180
352381
|
entry.lastUpdatedAt = Date.now();
|
|
352181
|
-
if (url && previousUrl && url !== previousUrl) {
|
|
352382
|
+
if (url && previousUrl && url !== previousUrl && !entry.screencast) {
|
|
352182
352383
|
entry.pendingCapture = true;
|
|
352183
352384
|
const schedule = (delay) => {
|
|
352184
352385
|
setTimeout(() => {
|
|
@@ -352199,6 +352400,11 @@ var require_live_browser_registry = __commonJS({
|
|
|
352199
352400
|
schedule(100);
|
|
352200
352401
|
}
|
|
352201
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
|
+
*/
|
|
352202
352408
|
updateSnapshot(sessionId, snapshot) {
|
|
352203
352409
|
const entry = this.entries.get(sessionId);
|
|
352204
352410
|
if (!entry)
|
|
@@ -352207,12 +352413,10 @@ var require_live_browser_registry = __commonJS({
|
|
|
352207
352413
|
entry.currentUrl = snapshot.currentUrl;
|
|
352208
352414
|
}
|
|
352209
352415
|
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;
|
|
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);
|
|
352216
352420
|
}
|
|
352217
352421
|
entry.lastUpdatedAt = Date.now();
|
|
352218
352422
|
}
|
|
@@ -352255,9 +352459,11 @@ var require_live_browser_registry = __commonJS({
|
|
|
352255
352459
|
async captureAllNow() {
|
|
352256
352460
|
await this.captureAll();
|
|
352257
352461
|
}
|
|
352258
|
-
/** Stop the snapshot loop and clear all entries. Idempotent. */
|
|
352462
|
+
/** Stop the snapshot loop, tear down every screencast, and clear all entries. Idempotent. */
|
|
352259
352463
|
shutdown() {
|
|
352260
352464
|
this.stopSnapshotLoop();
|
|
352465
|
+
for (const entry of this.entries.values())
|
|
352466
|
+
void this.stopScreencast(entry);
|
|
352261
352467
|
this.entries.clear();
|
|
352262
352468
|
}
|
|
352263
352469
|
ensureSnapshotLoop() {
|
|
@@ -352285,15 +352491,13 @@ var require_live_browser_registry = __commonJS({
|
|
|
352285
352491
|
}
|
|
352286
352492
|
}
|
|
352287
352493
|
if (oldestKey) {
|
|
352288
|
-
this.entries.get(oldestKey)
|
|
352289
|
-
this.entries.delete(oldestKey);
|
|
352494
|
+
this.dropEntry(oldestKey, this.entries.get(oldestKey));
|
|
352290
352495
|
}
|
|
352291
352496
|
}
|
|
352292
352497
|
sweepStale(now) {
|
|
352293
352498
|
for (const [key, entry] of this.entries) {
|
|
352294
352499
|
if (now - entry.lastUpdatedAt > STALE_SESSION_MS) {
|
|
352295
|
-
|
|
352296
|
-
this.entries.delete(key);
|
|
352500
|
+
this.dropEntry(key, entry);
|
|
352297
352501
|
}
|
|
352298
352502
|
}
|
|
352299
352503
|
}
|
|
@@ -352334,6 +352538,11 @@ var require_live_browser_registry = __commonJS({
|
|
|
352334
352538
|
entry.currentUrl = url;
|
|
352335
352539
|
} catch {
|
|
352336
352540
|
}
|
|
352541
|
+
if (entry.screencast) {
|
|
352542
|
+
if (entry.thumbnailJpegBase64)
|
|
352543
|
+
entry.thumbnailCapturedAt = Date.now();
|
|
352544
|
+
return;
|
|
352545
|
+
}
|
|
352337
352546
|
const buf = await page.screenshot({
|
|
352338
352547
|
type: "jpeg",
|
|
352339
352548
|
quality: THUMBNAIL_QUALITY,
|
|
@@ -352341,17 +352550,8 @@ var require_live_browser_registry = __commonJS({
|
|
|
352341
352550
|
timeout: 3e3,
|
|
352342
352551
|
scale: "css"
|
|
352343
352552
|
});
|
|
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
352553
|
const viewport = page.viewportSize?.();
|
|
352351
|
-
|
|
352352
|
-
entry.thumbnailWidth = viewport.width;
|
|
352353
|
-
entry.thumbnailHeight = viewport.height;
|
|
352354
|
-
}
|
|
352554
|
+
this.acceptFrame(entry, buf.toString("base64"), "image/jpeg", viewport?.width, viewport?.height);
|
|
352355
352555
|
} catch {
|
|
352356
352556
|
} finally {
|
|
352357
352557
|
entry.isCapturing = false;
|
|
@@ -353065,7 +353265,7 @@ var require_package4 = __commonJS({
|
|
|
353065
353265
|
"package.json"(exports, module) {
|
|
353066
353266
|
module.exports = {
|
|
353067
353267
|
name: "@validate.qa/runner",
|
|
353068
|
-
version: "1.0.
|
|
353268
|
+
version: "1.0.79",
|
|
353069
353269
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
353070
353270
|
bin: {
|
|
353071
353271
|
"validate-runner": "dist/cli.js"
|
|
@@ -353857,6 +354057,24 @@ async function getScreenshot(sessionPath) {
|
|
|
353857
354057
|
return null;
|
|
353858
354058
|
}
|
|
353859
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
|
+
}
|
|
353860
354078
|
async function getPageSource(sessionPath) {
|
|
353861
354079
|
try {
|
|
353862
354080
|
const value = readValue(await appiumRequest(`${sessionPath}/source`));
|
|
@@ -356368,7 +356586,15 @@ function mobileParallelSessionCapabilities(platform3, portOffset = 0, deviceId =
|
|
|
356368
356586
|
return (Number.isFinite(parsed) ? parsed : fallback) + offset;
|
|
356369
356587
|
};
|
|
356370
356588
|
if (platform3 === "ANDROID") {
|
|
356371
|
-
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
|
+
};
|
|
356372
356598
|
}
|
|
356373
356599
|
const safeDevice = deviceId.replace(/[^A-Za-z0-9._-]+/g, "-") || `device-${offset}`;
|
|
356374
356600
|
const derivedDataRoot = env.APPIUM_DERIVED_DATA_PATH?.trim() || join5(tmpdir3(), "validateqa-wda");
|
|
@@ -357623,6 +357849,15 @@ function acquireControlPortSlot(deviceId) {
|
|
|
357623
357849
|
function releaseControlPortSlot(deviceId) {
|
|
357624
357850
|
controlPortSlotsByDevice.delete(deviceId);
|
|
357625
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
|
+
}
|
|
357626
357861
|
var PINNED_DEVICE_SNAPSHOT_TTL_MS = 25 * 6e4;
|
|
357627
357862
|
var pinnedDeviceSnapshot = null;
|
|
357628
357863
|
async function executeMobileTest(options) {
|
|
@@ -358567,6 +358802,100 @@ import { randomBytes, timingSafeEqual } from "crypto";
|
|
|
358567
358802
|
import { homedir as homedir2 } from "os";
|
|
358568
358803
|
import { join as join6, dirname } from "path";
|
|
358569
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
|
|
358570
358899
|
init_dist();
|
|
358571
358900
|
var MOBILE_BRIDGE_PORT2 = 9515;
|
|
358572
358901
|
var MAX_REQUEST_BODY_BYTES = 64 * 1024;
|
|
@@ -359120,16 +359449,66 @@ function writeFrameToChannelClient(ch, client, frame) {
|
|
|
359120
359449
|
} catch {
|
|
359121
359450
|
}
|
|
359122
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
|
+
}
|
|
359123
359504
|
async function pollRunnerChannel(ch) {
|
|
359124
359505
|
if (ch.clients.size === 0 || ch.polling) return;
|
|
359506
|
+
if (ch.mjpeg) return;
|
|
359125
359507
|
ch.polling = true;
|
|
359126
359508
|
try {
|
|
359127
359509
|
validateSessionId(ch.sessionId);
|
|
359128
|
-
const
|
|
359129
|
-
|
|
359130
|
-
getPageSource(`/session/${ch.sessionId}`)
|
|
359131
|
-
]);
|
|
359132
|
-
if (!screenshot && !source) {
|
|
359510
|
+
const screenshot = await getScreenshot(`/session/${ch.sessionId}`);
|
|
359511
|
+
if (!screenshot) {
|
|
359133
359512
|
ch.nullTicks++;
|
|
359134
359513
|
if (ch.nullTicks >= MIRROR_DEAD_SESSION_TICKS) {
|
|
359135
359514
|
const alive = await isSessionAlive(ch.sessionId);
|
|
@@ -359142,32 +359521,45 @@ async function pollRunnerChannel(ch) {
|
|
|
359142
359521
|
} else {
|
|
359143
359522
|
ch.nullTicks = 0;
|
|
359144
359523
|
}
|
|
359145
|
-
|
|
359146
|
-
|
|
359147
|
-
|
|
359524
|
+
broadcastChannelFrame(ch, {
|
|
359525
|
+
timestamp: Date.now(),
|
|
359526
|
+
screenshot: screenshot ?? null,
|
|
359527
|
+
source: null,
|
|
359528
|
+
mimeType: "image/png"
|
|
359529
|
+
});
|
|
359148
359530
|
} catch {
|
|
359149
359531
|
} finally {
|
|
359150
359532
|
ch.polling = false;
|
|
359151
359533
|
}
|
|
359152
359534
|
}
|
|
359153
359535
|
function ensureChannelPolling(ch) {
|
|
359154
|
-
|
|
359536
|
+
tryStartChannelMjpeg(ch);
|
|
359537
|
+
if (ch.mjpeg || ch.pollTimer) return;
|
|
359155
359538
|
ch.pollTimer = setInterval(() => {
|
|
359156
359539
|
void pollRunnerChannel(ch);
|
|
359157
359540
|
}, MOBILE_SCREENSHOT_INTERVAL_MS);
|
|
359158
359541
|
ch.pollTimer.unref?.();
|
|
359159
359542
|
}
|
|
359160
359543
|
function stopChannelPollingIfIdle(ch) {
|
|
359161
|
-
if (ch.clients.size
|
|
359544
|
+
if (ch.clients.size > 0) return;
|
|
359545
|
+
if (ch.pollTimer) {
|
|
359162
359546
|
clearInterval(ch.pollTimer);
|
|
359163
359547
|
ch.pollTimer = null;
|
|
359164
359548
|
}
|
|
359549
|
+
if (ch.mjpeg) {
|
|
359550
|
+
ch.mjpeg.stop();
|
|
359551
|
+
ch.mjpeg = null;
|
|
359552
|
+
}
|
|
359165
359553
|
}
|
|
359166
359554
|
function closeRunnerChannel(ch) {
|
|
359167
359555
|
if (ch.pollTimer) {
|
|
359168
359556
|
clearInterval(ch.pollTimer);
|
|
359169
359557
|
ch.pollTimer = null;
|
|
359170
359558
|
}
|
|
359559
|
+
if (ch.mjpeg) {
|
|
359560
|
+
ch.mjpeg.stop();
|
|
359561
|
+
ch.mjpeg = null;
|
|
359562
|
+
}
|
|
359171
359563
|
for (const client of ch.clients) {
|
|
359172
359564
|
try {
|
|
359173
359565
|
client.end();
|
|
@@ -359752,12 +360144,24 @@ function attachMirrorSession(sessionId, platform3, deviceId) {
|
|
|
359752
360144
|
lastFrame: null,
|
|
359753
360145
|
pollTimer: null,
|
|
359754
360146
|
nullTicks: 0,
|
|
359755
|
-
polling: false
|
|
360147
|
+
polling: false,
|
|
360148
|
+
mjpeg: null,
|
|
360149
|
+
mjpegLastFrameAt: 0,
|
|
360150
|
+
mjpegStarting: false,
|
|
360151
|
+
mjpegUnavailable: false
|
|
359756
360152
|
};
|
|
359757
360153
|
ch.sessionId = sessionId;
|
|
359758
360154
|
ch.platform = platform3;
|
|
359759
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;
|
|
359760
360162
|
runnerChannels.set(deviceId, ch);
|
|
360163
|
+
void applyMjpegSettings(`/session/${sessionId}`, MOBILE_MJPEG_SETTINGS).catch(() => {
|
|
360164
|
+
});
|
|
359761
360165
|
if (ch.clients.size > 0) {
|
|
359762
360166
|
ensureChannelPolling(ch);
|
|
359763
360167
|
void pollRunnerChannel(ch);
|
|
@@ -362941,7 +363345,9 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
362941
363345
|
liveBrowserHandle.updateSnapshot({
|
|
362942
363346
|
currentUrl: mobileTarget.appId,
|
|
362943
363347
|
thumbnailJpegBase64: base64,
|
|
362944
|
-
thumbnailCapturedAt: Date.now()
|
|
363348
|
+
thumbnailCapturedAt: Date.now(),
|
|
363349
|
+
// Appium's screenshot endpoint returns PNG despite the field name.
|
|
363350
|
+
mimeType: "image/png"
|
|
362945
363351
|
});
|
|
362946
363352
|
requestLiveBrowserHeartbeat();
|
|
362947
363353
|
},
|
|
@@ -364186,6 +364592,22 @@ async function startRunner(opts) {
|
|
|
364186
364592
|
} catch {
|
|
364187
364593
|
}
|
|
364188
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);
|
|
364189
364611
|
let liveBrowserHeartbeatInFlight = false;
|
|
364190
364612
|
let liveBrowserHeartbeatPending = false;
|
|
364191
364613
|
let lastLiveBrowserHeartbeatAt = 0;
|