@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.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();
|
|
@@ -87957,6 +88020,7 @@ var require_playwright_native = __commonJS({
|
|
|
87957
88020
|
var PACKAGE_DIR = (0, path_1.resolve)(__dirname, "..", "..");
|
|
87958
88021
|
var RUNS_DIR = process.env.RUNS_DIR || (0, path_1.join)(PACKAGE_DIR, ".qa-runs");
|
|
87959
88022
|
var PW_CLI = require.resolve("@playwright/test/cli");
|
|
88023
|
+
var LIVE_BROWSER_STATE_POLL_INTERVAL_MS = 500;
|
|
87960
88024
|
async function executeTestViaNativePlaywright2(options) {
|
|
87961
88025
|
const { playwrightCode, baseUrl, testName, testVariables, authStateCapturePath, storageStatePath, extraHttpHeaders, authStandingMode, validateContext, liveBrowserHandle, onLiveBrowserSnapshot } = options;
|
|
87962
88026
|
const logs2 = [];
|
|
@@ -88046,7 +88110,7 @@ test.beforeEach(async ({ page }, testInfo) => {
|
|
|
88046
88110
|
await __qaLiveBrowserCapture();
|
|
88047
88111
|
__qaLiveBrowserInterval = setInterval(() => {
|
|
88048
88112
|
void __qaLiveBrowserCapture?.();
|
|
88049
|
-
},
|
|
88113
|
+
}, ${LIVE_BROWSER_STATE_POLL_INTERVAL_MS});
|
|
88050
88114
|
});
|
|
88051
88115
|
|
|
88052
88116
|
test.afterEach(async () => {
|
|
@@ -88463,7 +88527,7 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
|
|
|
88463
88527
|
};
|
|
88464
88528
|
const interval = setInterval(() => {
|
|
88465
88529
|
void poll();
|
|
88466
|
-
},
|
|
88530
|
+
}, LIVE_BROWSER_STATE_POLL_INTERVAL_MS);
|
|
88467
88531
|
interval.unref?.();
|
|
88468
88532
|
void poll();
|
|
88469
88533
|
return async () => {
|
|
@@ -352104,16 +352168,16 @@ var require_live_browser_registry = __commonJS({
|
|
|
352104
352168
|
"use strict";
|
|
352105
352169
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
352106
352170
|
exports2.liveBrowserRegistry = void 0;
|
|
352171
|
+
var shared_1 = (init_dist(), __toCommonJS(dist_exports));
|
|
352107
352172
|
var THUMBNAIL_INTERVAL_MS = parseInt(process.env.LIVE_BROWSER_THUMBNAIL_INTERVAL_MS ?? "4000", 10) || 4e3;
|
|
352108
352173
|
var THUMBNAIL_QUALITY = parseInt(process.env.LIVE_BROWSER_THUMBNAIL_QUALITY ?? "60", 10) || 60;
|
|
352109
|
-
var BROWSER_THUMBNAIL_MAX_BASE64_BYTES = parseInt(process.env.LIVE_BROWSER_THUMBNAIL_MAX_BASE64_BYTES ?? "400000", 10) || 4e5;
|
|
352110
|
-
var MOBILE_THUMBNAIL_MAX_BASE64_BYTES = parseInt(process.env.LIVE_MOBILE_THUMBNAIL_MAX_BASE64_BYTES ?? "4500000", 10) || 45e5;
|
|
352111
352174
|
var STALE_SESSION_MS = parseInt(process.env.LIVE_BROWSER_STALE_MS ?? `${10 * 60 * 1e3}`, 10) || 10 * 60 * 1e3;
|
|
352112
352175
|
var MAX_SESSIONS = parseInt(process.env.LIVE_BROWSER_MAX_SESSIONS ?? "32", 10) || 32;
|
|
352113
352176
|
var LiveBrowserRegistry = class {
|
|
352114
352177
|
entries = /* @__PURE__ */ new Map();
|
|
352115
352178
|
snapshotTimer = null;
|
|
352116
352179
|
capturing = false;
|
|
352180
|
+
publisher = null;
|
|
352117
352181
|
/** Register a new session. Returns a handle the caller must hold and
|
|
352118
352182
|
* ultimately call .unregister() on. */
|
|
352119
352183
|
register(meta) {
|
|
@@ -352127,7 +352191,10 @@ var require_live_browser_registry = __commonJS({
|
|
|
352127
352191
|
lastUpdatedAt: now,
|
|
352128
352192
|
page: null,
|
|
352129
352193
|
isCapturing: false,
|
|
352130
|
-
pendingCapture: false
|
|
352194
|
+
pendingCapture: false,
|
|
352195
|
+
screencast: null,
|
|
352196
|
+
screencastStarting: false,
|
|
352197
|
+
lastPublishedAt: 0
|
|
352131
352198
|
};
|
|
352132
352199
|
this.entries.set(meta.sessionId, entry);
|
|
352133
352200
|
this.ensureSnapshotLoop();
|
|
@@ -352140,19 +352207,40 @@ var require_live_browser_registry = __commonJS({
|
|
|
352140
352207
|
unregister: () => this.unregister(meta.sessionId)
|
|
352141
352208
|
};
|
|
352142
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
|
+
}
|
|
352143
352217
|
unregister(sessionId) {
|
|
352144
352218
|
const entry = this.entries.get(sessionId);
|
|
352145
352219
|
if (!entry)
|
|
352146
352220
|
return;
|
|
352147
|
-
|
|
352148
|
-
this.entries.delete(sessionId);
|
|
352221
|
+
this.dropEntry(sessionId, entry);
|
|
352149
352222
|
if (this.entries.size === 0)
|
|
352150
352223
|
this.stopSnapshotLoop();
|
|
352151
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
|
+
}
|
|
352152
352238
|
setPage(sessionId, page) {
|
|
352153
352239
|
const entry = this.entries.get(sessionId);
|
|
352154
352240
|
if (!entry)
|
|
352155
352241
|
return;
|
|
352242
|
+
if (entry.screencast && entry.screencast.page !== page)
|
|
352243
|
+
void this.stopScreencast(entry);
|
|
352156
352244
|
entry.page = page;
|
|
352157
352245
|
entry.lastUpdatedAt = Date.now();
|
|
352158
352246
|
if (page) {
|
|
@@ -352162,6 +352250,119 @@ var require_live_browser_registry = __commonJS({
|
|
|
352162
352250
|
entry.currentUrl = url;
|
|
352163
352251
|
} catch {
|
|
352164
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 {
|
|
352165
352366
|
}
|
|
352166
352367
|
}
|
|
352167
352368
|
setCurrentUrl(sessionId, url) {
|
|
@@ -352173,7 +352374,7 @@ var require_live_browser_registry = __commonJS({
|
|
|
352173
352374
|
const previousUrl = entry.currentUrl;
|
|
352174
352375
|
entry.currentUrl = url;
|
|
352175
352376
|
entry.lastUpdatedAt = Date.now();
|
|
352176
|
-
if (url && previousUrl && url !== previousUrl) {
|
|
352377
|
+
if (url && previousUrl && url !== previousUrl && !entry.screencast) {
|
|
352177
352378
|
entry.pendingCapture = true;
|
|
352178
352379
|
const schedule = (delay) => {
|
|
352179
352380
|
setTimeout(() => {
|
|
@@ -352194,6 +352395,11 @@ var require_live_browser_registry = __commonJS({
|
|
|
352194
352395
|
schedule(100);
|
|
352195
352396
|
}
|
|
352196
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
|
+
*/
|
|
352197
352403
|
updateSnapshot(sessionId, snapshot) {
|
|
352198
352404
|
const entry = this.entries.get(sessionId);
|
|
352199
352405
|
if (!entry)
|
|
@@ -352202,12 +352408,10 @@ var require_live_browser_registry = __commonJS({
|
|
|
352202
352408
|
entry.currentUrl = snapshot.currentUrl;
|
|
352203
352409
|
}
|
|
352204
352410
|
const thumbnail = snapshot.thumbnailJpegBase64;
|
|
352205
|
-
|
|
352206
|
-
|
|
352207
|
-
|
|
352208
|
-
entry
|
|
352209
|
-
entry.thumbnailWidth = typeof snapshot.thumbnailWidth === "number" && Number.isFinite(snapshot.thumbnailWidth) ? snapshot.thumbnailWidth : void 0;
|
|
352210
|
-
entry.thumbnailHeight = typeof snapshot.thumbnailHeight === "number" && Number.isFinite(snapshot.thumbnailHeight) ? snapshot.thumbnailHeight : void 0;
|
|
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);
|
|
352211
352415
|
}
|
|
352212
352416
|
entry.lastUpdatedAt = Date.now();
|
|
352213
352417
|
}
|
|
@@ -352250,9 +352454,11 @@ var require_live_browser_registry = __commonJS({
|
|
|
352250
352454
|
async captureAllNow() {
|
|
352251
352455
|
await this.captureAll();
|
|
352252
352456
|
}
|
|
352253
|
-
/** Stop the snapshot loop and clear all entries. Idempotent. */
|
|
352457
|
+
/** Stop the snapshot loop, tear down every screencast, and clear all entries. Idempotent. */
|
|
352254
352458
|
shutdown() {
|
|
352255
352459
|
this.stopSnapshotLoop();
|
|
352460
|
+
for (const entry of this.entries.values())
|
|
352461
|
+
void this.stopScreencast(entry);
|
|
352256
352462
|
this.entries.clear();
|
|
352257
352463
|
}
|
|
352258
352464
|
ensureSnapshotLoop() {
|
|
@@ -352280,15 +352486,13 @@ var require_live_browser_registry = __commonJS({
|
|
|
352280
352486
|
}
|
|
352281
352487
|
}
|
|
352282
352488
|
if (oldestKey) {
|
|
352283
|
-
this.entries.get(oldestKey)
|
|
352284
|
-
this.entries.delete(oldestKey);
|
|
352489
|
+
this.dropEntry(oldestKey, this.entries.get(oldestKey));
|
|
352285
352490
|
}
|
|
352286
352491
|
}
|
|
352287
352492
|
sweepStale(now) {
|
|
352288
352493
|
for (const [key, entry] of this.entries) {
|
|
352289
352494
|
if (now - entry.lastUpdatedAt > STALE_SESSION_MS) {
|
|
352290
|
-
|
|
352291
|
-
this.entries.delete(key);
|
|
352495
|
+
this.dropEntry(key, entry);
|
|
352292
352496
|
}
|
|
352293
352497
|
}
|
|
352294
352498
|
}
|
|
@@ -352329,6 +352533,11 @@ var require_live_browser_registry = __commonJS({
|
|
|
352329
352533
|
entry.currentUrl = url;
|
|
352330
352534
|
} catch {
|
|
352331
352535
|
}
|
|
352536
|
+
if (entry.screencast) {
|
|
352537
|
+
if (entry.thumbnailJpegBase64)
|
|
352538
|
+
entry.thumbnailCapturedAt = Date.now();
|
|
352539
|
+
return;
|
|
352540
|
+
}
|
|
352332
352541
|
const buf = await page.screenshot({
|
|
352333
352542
|
type: "jpeg",
|
|
352334
352543
|
quality: THUMBNAIL_QUALITY,
|
|
@@ -352336,17 +352545,8 @@ var require_live_browser_registry = __commonJS({
|
|
|
352336
352545
|
timeout: 3e3,
|
|
352337
352546
|
scale: "css"
|
|
352338
352547
|
});
|
|
352339
|
-
const base64 = buf.toString("base64");
|
|
352340
|
-
if (base64.length > BROWSER_THUMBNAIL_MAX_BASE64_BYTES) {
|
|
352341
|
-
return;
|
|
352342
|
-
}
|
|
352343
|
-
entry.thumbnailJpegBase64 = base64;
|
|
352344
|
-
entry.thumbnailCapturedAt = Date.now();
|
|
352345
352548
|
const viewport = page.viewportSize?.();
|
|
352346
|
-
|
|
352347
|
-
entry.thumbnailWidth = viewport.width;
|
|
352348
|
-
entry.thumbnailHeight = viewport.height;
|
|
352349
|
-
}
|
|
352549
|
+
this.acceptFrame(entry, buf.toString("base64"), "image/jpeg", viewport?.width, viewport?.height);
|
|
352350
352550
|
} catch {
|
|
352351
352551
|
} finally {
|
|
352352
352552
|
entry.isCapturing = false;
|
|
@@ -353060,7 +353260,7 @@ var require_package4 = __commonJS({
|
|
|
353060
353260
|
"package.json"(exports2, module2) {
|
|
353061
353261
|
module2.exports = {
|
|
353062
353262
|
name: "@validate.qa/runner",
|
|
353063
|
-
version: "1.0.
|
|
353263
|
+
version: "1.0.79",
|
|
353064
353264
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
353065
353265
|
bin: {
|
|
353066
353266
|
"validate-runner": "dist/cli.js"
|
|
@@ -353852,6 +354052,24 @@ async function getScreenshot(sessionPath) {
|
|
|
353852
354052
|
return null;
|
|
353853
354053
|
}
|
|
353854
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
|
+
}
|
|
353855
354073
|
async function getPageSource(sessionPath) {
|
|
353856
354074
|
try {
|
|
353857
354075
|
const value = readValue(await appiumRequest(`${sessionPath}/source`));
|
|
@@ -356360,7 +356578,15 @@ function mobileParallelSessionCapabilities(platform3, portOffset = 0, deviceId =
|
|
|
356360
356578
|
return (Number.isFinite(parsed) ? parsed : fallback) + offset;
|
|
356361
356579
|
};
|
|
356362
356580
|
if (platform3 === "ANDROID") {
|
|
356363
|
-
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
|
+
};
|
|
356364
356590
|
}
|
|
356365
356591
|
const safeDevice = deviceId.replace(/[^A-Za-z0-9._-]+/g, "-") || `device-${offset}`;
|
|
356366
356592
|
const derivedDataRoot = env.APPIUM_DERIVED_DATA_PATH?.trim() || (0, import_node_path2.join)((0, import_node_os2.tmpdir)(), "validateqa-wda");
|
|
@@ -357615,6 +357841,15 @@ function acquireControlPortSlot(deviceId) {
|
|
|
357615
357841
|
function releaseControlPortSlot(deviceId) {
|
|
357616
357842
|
controlPortSlotsByDevice.delete(deviceId);
|
|
357617
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
|
+
}
|
|
357618
357853
|
var PINNED_DEVICE_SNAPSHOT_TTL_MS = 25 * 6e4;
|
|
357619
357854
|
var pinnedDeviceSnapshot = null;
|
|
357620
357855
|
async function executeMobileTest(options) {
|
|
@@ -358553,12 +358788,106 @@ function decideMobileDeviceGate(input) {
|
|
|
358553
358788
|
}
|
|
358554
358789
|
|
|
358555
358790
|
// src/services/mobile-bridge.ts
|
|
358556
|
-
var
|
|
358791
|
+
var import_http2 = require("http");
|
|
358557
358792
|
var import_child_process4 = require("child_process");
|
|
358558
358793
|
var import_crypto = require("crypto");
|
|
358559
358794
|
var import_os4 = require("os");
|
|
358560
358795
|
var import_path4 = require("path");
|
|
358561
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
|
|
358562
358891
|
init_dist();
|
|
358563
358892
|
var MOBILE_BRIDGE_PORT2 = 9515;
|
|
358564
358893
|
var MAX_REQUEST_BODY_BYTES = 64 * 1024;
|
|
@@ -359112,16 +359441,66 @@ function writeFrameToChannelClient(ch, client, frame) {
|
|
|
359112
359441
|
} catch {
|
|
359113
359442
|
}
|
|
359114
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
|
+
}
|
|
359115
359496
|
async function pollRunnerChannel(ch) {
|
|
359116
359497
|
if (ch.clients.size === 0 || ch.polling) return;
|
|
359498
|
+
if (ch.mjpeg) return;
|
|
359117
359499
|
ch.polling = true;
|
|
359118
359500
|
try {
|
|
359119
359501
|
validateSessionId(ch.sessionId);
|
|
359120
|
-
const
|
|
359121
|
-
|
|
359122
|
-
getPageSource(`/session/${ch.sessionId}`)
|
|
359123
|
-
]);
|
|
359124
|
-
if (!screenshot && !source) {
|
|
359502
|
+
const screenshot = await getScreenshot(`/session/${ch.sessionId}`);
|
|
359503
|
+
if (!screenshot) {
|
|
359125
359504
|
ch.nullTicks++;
|
|
359126
359505
|
if (ch.nullTicks >= MIRROR_DEAD_SESSION_TICKS) {
|
|
359127
359506
|
const alive = await isSessionAlive(ch.sessionId);
|
|
@@ -359134,32 +359513,45 @@ async function pollRunnerChannel(ch) {
|
|
|
359134
359513
|
} else {
|
|
359135
359514
|
ch.nullTicks = 0;
|
|
359136
359515
|
}
|
|
359137
|
-
|
|
359138
|
-
|
|
359139
|
-
|
|
359516
|
+
broadcastChannelFrame(ch, {
|
|
359517
|
+
timestamp: Date.now(),
|
|
359518
|
+
screenshot: screenshot ?? null,
|
|
359519
|
+
source: null,
|
|
359520
|
+
mimeType: "image/png"
|
|
359521
|
+
});
|
|
359140
359522
|
} catch {
|
|
359141
359523
|
} finally {
|
|
359142
359524
|
ch.polling = false;
|
|
359143
359525
|
}
|
|
359144
359526
|
}
|
|
359145
359527
|
function ensureChannelPolling(ch) {
|
|
359146
|
-
|
|
359528
|
+
tryStartChannelMjpeg(ch);
|
|
359529
|
+
if (ch.mjpeg || ch.pollTimer) return;
|
|
359147
359530
|
ch.pollTimer = setInterval(() => {
|
|
359148
359531
|
void pollRunnerChannel(ch);
|
|
359149
359532
|
}, MOBILE_SCREENSHOT_INTERVAL_MS);
|
|
359150
359533
|
ch.pollTimer.unref?.();
|
|
359151
359534
|
}
|
|
359152
359535
|
function stopChannelPollingIfIdle(ch) {
|
|
359153
|
-
if (ch.clients.size
|
|
359536
|
+
if (ch.clients.size > 0) return;
|
|
359537
|
+
if (ch.pollTimer) {
|
|
359154
359538
|
clearInterval(ch.pollTimer);
|
|
359155
359539
|
ch.pollTimer = null;
|
|
359156
359540
|
}
|
|
359541
|
+
if (ch.mjpeg) {
|
|
359542
|
+
ch.mjpeg.stop();
|
|
359543
|
+
ch.mjpeg = null;
|
|
359544
|
+
}
|
|
359157
359545
|
}
|
|
359158
359546
|
function closeRunnerChannel(ch) {
|
|
359159
359547
|
if (ch.pollTimer) {
|
|
359160
359548
|
clearInterval(ch.pollTimer);
|
|
359161
359549
|
ch.pollTimer = null;
|
|
359162
359550
|
}
|
|
359551
|
+
if (ch.mjpeg) {
|
|
359552
|
+
ch.mjpeg.stop();
|
|
359553
|
+
ch.mjpeg = null;
|
|
359554
|
+
}
|
|
359163
359555
|
for (const client of ch.clients) {
|
|
359164
359556
|
try {
|
|
359165
359557
|
client.end();
|
|
@@ -359556,7 +359948,7 @@ async function startMobileBridge(options) {
|
|
|
359556
359948
|
}
|
|
359557
359949
|
killStaleBridge();
|
|
359558
359950
|
return new Promise((resolve2, reject) => {
|
|
359559
|
-
server2 = (0,
|
|
359951
|
+
server2 = (0, import_http2.createServer)(route);
|
|
359560
359952
|
server2.on("error", (err) => {
|
|
359561
359953
|
if (err.code === "EADDRINUSE") {
|
|
359562
359954
|
reject(new Error(`Port ${port} already in use. Another bridge instance may be running.`));
|
|
@@ -359744,12 +360136,24 @@ function attachMirrorSession(sessionId, platform3, deviceId) {
|
|
|
359744
360136
|
lastFrame: null,
|
|
359745
360137
|
pollTimer: null,
|
|
359746
360138
|
nullTicks: 0,
|
|
359747
|
-
polling: false
|
|
360139
|
+
polling: false,
|
|
360140
|
+
mjpeg: null,
|
|
360141
|
+
mjpegLastFrameAt: 0,
|
|
360142
|
+
mjpegStarting: false,
|
|
360143
|
+
mjpegUnavailable: false
|
|
359748
360144
|
};
|
|
359749
360145
|
ch.sessionId = sessionId;
|
|
359750
360146
|
ch.platform = platform3;
|
|
359751
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;
|
|
359752
360154
|
runnerChannels.set(deviceId, ch);
|
|
360155
|
+
void applyMjpegSettings(`/session/${sessionId}`, MOBILE_MJPEG_SETTINGS).catch(() => {
|
|
360156
|
+
});
|
|
359753
360157
|
if (ch.clients.size > 0) {
|
|
359754
360158
|
ensureChannelPolling(ch);
|
|
359755
360159
|
void pollRunnerChannel(ch);
|
|
@@ -362933,7 +363337,9 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
362933
363337
|
liveBrowserHandle.updateSnapshot({
|
|
362934
363338
|
currentUrl: mobileTarget.appId,
|
|
362935
363339
|
thumbnailJpegBase64: base64,
|
|
362936
|
-
thumbnailCapturedAt: Date.now()
|
|
363340
|
+
thumbnailCapturedAt: Date.now(),
|
|
363341
|
+
// Appium's screenshot endpoint returns PNG despite the field name.
|
|
363342
|
+
mimeType: "image/png"
|
|
362937
363343
|
});
|
|
362938
363344
|
requestLiveBrowserHeartbeat();
|
|
362939
363345
|
},
|
|
@@ -364178,6 +364584,22 @@ async function startRunner(opts) {
|
|
|
364178
364584
|
} catch {
|
|
364179
364585
|
}
|
|
364180
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);
|
|
364181
364603
|
let liveBrowserHeartbeatInFlight = false;
|
|
364182
364604
|
let liveBrowserHeartbeatPending = false;
|
|
364183
364605
|
let lastLiveBrowserHeartbeatAt = 0;
|