buninu-browser 0.6.0 → 0.7.0
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/README.md +4 -3
- package/index.js +10 -6
- package/lib/cdp-server.js +85 -61
- package/package.json +1 -1
- package/test/cdp-viewers.test.js +43 -0
- package/test/index.test.js +1 -0
package/README.md
CHANGED
|
@@ -96,9 +96,10 @@ the rest is passed on:
|
|
|
96
96
|
argument goes to casty)
|
|
97
97
|
- `--headless`: the engine alone, as
|
|
98
98
|
a CDP endpoint
|
|
99
|
-
- `--win32
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
- `--win32`: casty with a native Windows
|
|
100
|
+
window (Windows only)
|
|
101
|
+
- `--gtk`, `--appkit`, `--qt`: native
|
|
102
|
+
windowed frontends (reserved, not implemented yet)
|
|
102
103
|
- `--help`/`-h`, `--version`/`-V`,
|
|
103
104
|
`--readme`: this launcher's help,
|
|
104
105
|
version and backend details, and
|
package/index.js
CHANGED
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
* --casty the terminal frontend (default): runs casty with
|
|
10
10
|
* CASTY_BROWSER set to this package's buninu-browser.js
|
|
11
11
|
* --headless the engine alone (buninu-browser.js, a CDP endpoint)
|
|
12
|
-
* --win32
|
|
13
|
-
*
|
|
12
|
+
* --win32 casty's native Windows frontend
|
|
13
|
+
* --gtk, --appkit, --qt
|
|
14
|
+
* reserved for native windowed frontends (not implemented)
|
|
14
15
|
*
|
|
15
16
|
* --help, -h / --version, -V / --readme, --docs
|
|
16
17
|
* this launcher's help, version and README
|
|
@@ -33,7 +34,7 @@ export * from "./buninu-browser.js";
|
|
|
33
34
|
|
|
34
35
|
const CASTY_PACKAGE = "@drxiaozhi/casty";
|
|
35
36
|
const ENGINE = join(import.meta.dir, "buninu-browser.js");
|
|
36
|
-
const RESERVED_FRONTENDS = new Set(["--
|
|
37
|
+
const RESERVED_FRONTENDS = new Set(["--gtk", "--appkit", "--qt"]);
|
|
37
38
|
const INFORMATION = new Map([
|
|
38
39
|
["--help", "help"], ["-h", "help"], ["-help", "help"],
|
|
39
40
|
["--version", "version"], ["-V", "version"], ["-version", "version"],
|
|
@@ -52,6 +53,7 @@ export function chooseFrontend(argv) {
|
|
|
52
53
|
return { frontend: first.slice(2), args: rest };
|
|
53
54
|
|
|
54
55
|
if (first === "--casty") return { frontend: "casty", args: rest };
|
|
56
|
+
if (first === "--win32") return { frontend: "win32", args: rest };
|
|
55
57
|
if (first === "--headless") return { frontend: "headless", args: rest };
|
|
56
58
|
if (RESERVED_FRONTENDS.has(first)) return { frontend: first.slice(2), args: rest, reserved: true };
|
|
57
59
|
if (INFORMATION.has(first)) return { frontend: INFORMATION.get(first), args: rest, information: true };
|
|
@@ -96,7 +98,9 @@ ${typeof waylandHelp === "undefined" ? "" : waylandHelp} --casty
|
|
|
96
98
|
--quickshell-wayland, --quickshell-headless, --quickshell-casty
|
|
97
99
|
Experimental Quickshell CLI (-p FILE_OR_DIRECTORY / -c CONFIG)
|
|
98
100
|
Use -- before backend arguments; each entry supports --help
|
|
99
|
-
--win32
|
|
101
|
+
--win32
|
|
102
|
+
Native Windows window driven by casty (Windows only)
|
|
103
|
+
--gtk, --appkit, --qt
|
|
100
104
|
Native windowed frontends (reserved, not implemented yet)
|
|
101
105
|
|
|
102
106
|
Browser identity:
|
|
@@ -129,7 +133,7 @@ function printVersion() {
|
|
|
129
133
|
console.log("Platform:", `${process.platform}-${process.arch}`);
|
|
130
134
|
console.log("Engine:", ENGINE);
|
|
131
135
|
console.log("casty launcher:", castyCommand([]).join(" "));
|
|
132
|
-
console.log("Frontends: casty (default), headless; reserved:
|
|
136
|
+
console.log("Frontends: casty (default), headless, win32; reserved: gtk, appkit, qt");
|
|
133
137
|
}
|
|
134
138
|
|
|
135
139
|
async function printReadme() {
|
|
@@ -176,6 +180,6 @@ if (import.meta.main) {
|
|
|
176
180
|
|
|
177
181
|
const code = frontend === "headless"
|
|
178
182
|
? await run([process.execPath, ENGINE, ...args])
|
|
179
|
-
: await run(castyCommand(args), { ...process.env, CASTY_BROWSER: ENGINE });
|
|
183
|
+
: await run(castyCommand(frontend === "win32" ? ["--win32", ...args] : args), { ...process.env, CASTY_BROWSER: ENGINE });
|
|
180
184
|
process.exit(code ?? 1);
|
|
181
185
|
}
|
package/lib/cdp-server.js
CHANGED
|
@@ -44,15 +44,10 @@ export class CdpServer {
|
|
|
44
44
|
const offset = frame?.scrollOffset;
|
|
45
45
|
const scrollOffset = Number.isFinite(offset?.x) && Number.isFinite(offset?.y)
|
|
46
46
|
? { x: offset.x, y: offset.y } : null;
|
|
47
|
-
for (const target of state.targets.values()) {
|
|
48
|
-
if (target
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
if (state.implicitTarget?.screencast) {
|
|
54
|
-
state.implicitTarget.screencastScrollOffset = scrollOffset;
|
|
55
|
-
pushScreencastFrame(context, state, state.implicitTarget);
|
|
47
|
+
for (const target of [...state.targets.values(), state.implicitTarget]) {
|
|
48
|
+
if (!target) continue;
|
|
49
|
+
for (const stream of screencasts(target)) stream.scrollOffset = scrollOffset;
|
|
50
|
+
void pushScreencastFrame(context, state, target);
|
|
56
51
|
}
|
|
57
52
|
});
|
|
58
53
|
context.onLifecycle?.((name) => {
|
|
@@ -173,6 +168,8 @@ export class CdpServer {
|
|
|
173
168
|
const msg = JSON.parse(text);
|
|
174
169
|
id = msg.id;
|
|
175
170
|
sessionId = msg.sessionId;
|
|
171
|
+
if (sessionId && state.sessionOwners.get(sessionId) !== ws)
|
|
172
|
+
throw Error("No session owned by this connection");
|
|
176
173
|
let dispatchSessionId = sessionId;
|
|
177
174
|
if (!dispatchSessionId && ws.data?.targetId) {
|
|
178
175
|
dispatchSessionId = `page-${ws.data.targetId}`;
|
|
@@ -219,12 +216,17 @@ export class CdpServer {
|
|
|
219
216
|
},
|
|
220
217
|
close(ws) {
|
|
221
218
|
for (const target of state.targets.values()) {
|
|
222
|
-
|
|
219
|
+
stopScreencast(target, ws);
|
|
223
220
|
if (target.lifecycleEmit?.owner === ws) target.lifecycleEmit = null;
|
|
224
221
|
}
|
|
225
222
|
const implicit = state.implicitTarget;
|
|
226
|
-
if (implicit
|
|
223
|
+
if (implicit) stopScreencast(implicit, ws);
|
|
227
224
|
if (implicit?.lifecycleEmit?.owner === ws) implicit.lifecycleEmit = null;
|
|
225
|
+
for (const [id, owner] of state.sessionOwners) {
|
|
226
|
+
if (owner !== ws) continue;
|
|
227
|
+
state.sessionOwners.delete(id);
|
|
228
|
+
state.sessions.delete(id);
|
|
229
|
+
}
|
|
228
230
|
serverLog("[CdpServer] client disconnected");
|
|
229
231
|
},
|
|
230
232
|
},
|
|
@@ -288,6 +290,7 @@ function createCdpState() {
|
|
|
288
290
|
nextScreencastFrameId: 1,
|
|
289
291
|
targets: new Map(),
|
|
290
292
|
sessions: new Map(),
|
|
293
|
+
sessionOwners: new Map(),
|
|
291
294
|
objects: new Map(),
|
|
292
295
|
initialNavigations: new Map(),
|
|
293
296
|
implicitTarget: null,
|
|
@@ -329,6 +332,7 @@ async function dispatch(ctx, state, method, params, sessionId, emit) {
|
|
|
329
332
|
const target = state.targets.get(targetId);
|
|
330
333
|
target.attached = true;
|
|
331
334
|
state.sessions.set(attachedSessionId, targetId);
|
|
335
|
+
state.sessionOwners.set(attachedSessionId, emit.owner);
|
|
332
336
|
emit("Target.attachedToTarget", {
|
|
333
337
|
sessionId: attachedSessionId,
|
|
334
338
|
targetInfo: createTargetInfo(target),
|
|
@@ -345,6 +349,7 @@ async function dispatch(ctx, state, method, params, sessionId, emit) {
|
|
|
345
349
|
const sessionId = `session-${state.nextSessionId++}`;
|
|
346
350
|
target.attached = true;
|
|
347
351
|
state.sessions.set(sessionId, params.targetId);
|
|
352
|
+
state.sessionOwners.set(sessionId, emit.owner);
|
|
348
353
|
return { sessionId };
|
|
349
354
|
}
|
|
350
355
|
case "Target.getTargets":
|
|
@@ -378,6 +383,7 @@ async function dispatch(ctx, state, method, params, sessionId, emit) {
|
|
|
378
383
|
for (const [attachedSessionId, targetId] of state.sessions) {
|
|
379
384
|
if (targetId !== params.targetId) continue;
|
|
380
385
|
state.sessions.delete(attachedSessionId);
|
|
386
|
+
state.sessionOwners.delete(attachedSessionId);
|
|
381
387
|
emit("Target.detachedFromTarget", {
|
|
382
388
|
sessionId: attachedSessionId,
|
|
383
389
|
targetId,
|
|
@@ -387,9 +393,14 @@ async function dispatch(ctx, state, method, params, sessionId, emit) {
|
|
|
387
393
|
emit("Target.targetDestroyed", { targetId: params.targetId });
|
|
388
394
|
return { success: true };
|
|
389
395
|
}
|
|
390
|
-
case "Target.detachFromTarget":
|
|
396
|
+
case "Target.detachFromTarget": {
|
|
397
|
+
const target = state.targets.get(state.sessions.get(params.sessionId));
|
|
398
|
+
if (state.sessionOwners.get(params.sessionId) !== emit.owner) throw Error("No session owned by this connection");
|
|
399
|
+
if (target) stopScreencast(target, emit.owner, params.sessionId);
|
|
391
400
|
state.sessions.delete(params.sessionId);
|
|
401
|
+
state.sessionOwners.delete(params.sessionId);
|
|
392
402
|
return {};
|
|
403
|
+
}
|
|
393
404
|
case "Page.enable":
|
|
394
405
|
return {};
|
|
395
406
|
case "Page.getResourceContent": {
|
|
@@ -470,7 +481,7 @@ async function dispatch(ctx, state, method, params, sessionId, emit) {
|
|
|
470
481
|
// A javascript: URL runs in the current document: no new loader or history entry.
|
|
471
482
|
if (/^\s*javascript:/i.test(String(params.url))) {
|
|
472
483
|
await ctx.navigate?.(String(params.url).trim());
|
|
473
|
-
if (target.
|
|
484
|
+
if (target.screencasts.size) await pushScreencastFrame(ctx, state, target);
|
|
474
485
|
return { frameId: target.frameId, loaderId: target.loaderId };
|
|
475
486
|
}
|
|
476
487
|
// Events of the next document belong to its new loader.
|
|
@@ -479,7 +490,7 @@ async function dispatch(ctx, state, method, params, sessionId, emit) {
|
|
|
479
490
|
navigateTarget(state, target, params.url, emit, { addHistory: true });
|
|
480
491
|
target.title = await getContextTitle(ctx, target);
|
|
481
492
|
target.history[target.historyIndex].title = target.title;
|
|
482
|
-
if (target.
|
|
493
|
+
if (target.screencasts.size) {
|
|
483
494
|
await pushScreencastFrame(ctx, state, target);
|
|
484
495
|
}
|
|
485
496
|
state.initialNavigations.get(target.targetId)?.navigated.resolve();
|
|
@@ -532,22 +543,26 @@ async function dispatch(ctx, state, method, params, sessionId, emit) {
|
|
|
532
543
|
}
|
|
533
544
|
case "Page.startScreencast": {
|
|
534
545
|
const target = getSessionTarget(state, sessionId);
|
|
535
|
-
stopScreencast(target);
|
|
536
|
-
|
|
537
|
-
target.
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
546
|
+
stopScreencast(target, emit.owner, sessionId ?? null);
|
|
547
|
+
let sessions = target.screencasts.get(emit.owner);
|
|
548
|
+
if (!sessions) target.screencasts.set(emit.owner, sessions = new Map());
|
|
549
|
+
const stream = { params: { ...params }, emit, active: true, busy: false,
|
|
550
|
+
pending: false, scrollOffset: null, frames: new Set() };
|
|
551
|
+
sessions.set(sessionId ?? null, stream);
|
|
552
|
+
// Do not wait for a capture; each viewer owns its own backpressure.
|
|
553
|
+
void pushScreencastFrame(ctx, state, target, stream);
|
|
541
554
|
return {};
|
|
542
555
|
}
|
|
543
556
|
case "Page.screencastFrameAck": {
|
|
544
557
|
const target = getSessionTarget(state, sessionId);
|
|
545
|
-
|
|
558
|
+
const stream = target.screencasts.get(emit.owner)?.get(sessionId ?? null);
|
|
559
|
+
if (stream?.frames.delete(params.sessionId) && stream.pending)
|
|
560
|
+
void pushScreencastFrame(ctx, state, target, stream);
|
|
546
561
|
return {};
|
|
547
562
|
}
|
|
548
563
|
case "Page.stopScreencast": {
|
|
549
564
|
const target = getSessionTarget(state, sessionId);
|
|
550
|
-
stopScreencast(target);
|
|
565
|
+
stopScreencast(target, emit.owner, sessionId ?? null);
|
|
551
566
|
return {};
|
|
552
567
|
}
|
|
553
568
|
case "Page.reload": {
|
|
@@ -727,7 +742,7 @@ async function dispatch(ctx, state, method, params, sessionId, emit) {
|
|
|
727
742
|
navigateTarget(state,target,navigation.url,emit,{addHistory:true});
|
|
728
743
|
target.title=navigation.title ?? await getContextTitle(ctx,target);
|
|
729
744
|
target.history[target.historyIndex].title=target.title;
|
|
730
|
-
if(target.
|
|
745
|
+
if(target.screencasts.size)await pushScreencastFrame(ctx,state,target);
|
|
731
746
|
}
|
|
732
747
|
return {};
|
|
733
748
|
}
|
|
@@ -743,7 +758,7 @@ async function dispatch(ctx, state, method, params, sessionId, emit) {
|
|
|
743
758
|
navigateTarget(state, target, navigation.url, emit, { addHistory: true });
|
|
744
759
|
target.title = navigation.title ?? await getContextTitle(ctx, target);
|
|
745
760
|
target.history[target.historyIndex].title = target.title;
|
|
746
|
-
if (target.
|
|
761
|
+
if (target.screencasts.size) await pushScreencastFrame(ctx, state, target);
|
|
747
762
|
}
|
|
748
763
|
}
|
|
749
764
|
return {};
|
|
@@ -852,9 +867,7 @@ function createTarget(state, url = "about:blank", options = {}) {
|
|
|
852
867
|
height: options.height ?? 600,
|
|
853
868
|
deviceScaleFactor: 1,
|
|
854
869
|
},
|
|
855
|
-
|
|
856
|
-
screencastScrollOffset: null,
|
|
857
|
-
screencastFrames: new Set(),
|
|
870
|
+
screencasts: new Map(),
|
|
858
871
|
};
|
|
859
872
|
state.targets.set(targetId, target);
|
|
860
873
|
state.initialNavigations.set(targetId, {
|
|
@@ -898,54 +911,67 @@ function emitLifecycle(target, name) {
|
|
|
898
911
|
// Bound the amount of image data queued to a slow CDP client. Two outstanding
|
|
899
912
|
// frames keep the pipe moving while acknowledging one; later updates coalesce.
|
|
900
913
|
const MAX_SCREENCAST_FRAMES = 2;
|
|
901
|
-
function
|
|
902
|
-
target.
|
|
903
|
-
target.screencastEmit = null;
|
|
904
|
-
target.screencastPending = false;
|
|
905
|
-
target.screencastScrollOffset = null;
|
|
906
|
-
target.screencastFrames.clear();
|
|
914
|
+
function* screencasts(target) {
|
|
915
|
+
for (const sessions of target.screencasts.values()) yield* sessions.values();
|
|
907
916
|
}
|
|
908
917
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
918
|
+
// No owner means target destruction. An owner without a session means disconnect.
|
|
919
|
+
function stopScreencast(target, owner, sessionId) {
|
|
920
|
+
const owners = owner === undefined ? target.screencasts : new Map([[owner, target.screencasts.get(owner)]]);
|
|
921
|
+
for (const [key, sessions] of owners) {
|
|
922
|
+
if (!sessions) continue;
|
|
923
|
+
for (const [id, stream] of sessions) {
|
|
924
|
+
if (sessionId !== undefined && id !== sessionId) continue;
|
|
925
|
+
stream.active = false;
|
|
926
|
+
stream.pending = false;
|
|
927
|
+
stream.scrollOffset = null;
|
|
928
|
+
stream.frames.clear();
|
|
929
|
+
sessions.delete(id);
|
|
930
|
+
}
|
|
931
|
+
if (!sessions.size) target.screencasts.delete(key);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/** One capture at a time per viewer, with bounded unacknowledged frames. */
|
|
936
|
+
async function pushScreencastFrame(ctx, state, target, stream) {
|
|
937
|
+
if (!stream) {
|
|
938
|
+
await Promise.all([...screencasts(target)].map(s => pushScreencastFrame(ctx, state, target, s)));
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
if (!stream.active) return;
|
|
942
|
+
if (stream.busy || stream.frames.size >= MAX_SCREENCAST_FRAMES) {
|
|
943
|
+
stream.pending = true;
|
|
914
944
|
return;
|
|
915
945
|
}
|
|
916
|
-
|
|
946
|
+
stream.busy = true;
|
|
917
947
|
try {
|
|
918
948
|
do {
|
|
919
|
-
|
|
920
|
-
await emitScreencastFrame(ctx, state, target);
|
|
921
|
-
} while (
|
|
922
|
-
&& target.screencastFrames.size < MAX_SCREENCAST_FRAMES);
|
|
949
|
+
stream.pending = false;
|
|
950
|
+
await emitScreencastFrame(ctx, state, target, stream);
|
|
951
|
+
} while (stream.pending && stream.active && stream.frames.size < MAX_SCREENCAST_FRAMES);
|
|
923
952
|
} catch (error) {
|
|
924
953
|
serverError(`[CdpServer] screencast frame failed: ${error?.message ?? error}`);
|
|
925
954
|
} finally {
|
|
926
|
-
|
|
955
|
+
stream.busy = false;
|
|
927
956
|
}
|
|
928
957
|
}
|
|
929
958
|
|
|
930
|
-
async function emitScreencastFrame(ctx, state, target) {
|
|
931
|
-
const
|
|
959
|
+
async function emitScreencastFrame(ctx, state, target, stream) {
|
|
960
|
+
const options = stream.params, emit = stream.emit;
|
|
932
961
|
const loaderId = target.loaderId;
|
|
933
|
-
//
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
// Private zero-area screencast: signal invalidation without rendering pixels.
|
|
938
|
-
// captureScreenshot keeps its ordinary clip validation.
|
|
939
|
-
const zeroArea = stream.clip?.width === 0 && stream.clip?.height === 0;
|
|
962
|
+
// Pending updates replace this snapshot with the latest position.
|
|
963
|
+
const notifiedOffset = stream.scrollOffset;
|
|
964
|
+
stream.scrollOffset = null;
|
|
965
|
+
const zeroArea = options.clip?.width === 0 && options.clip?.height === 0;
|
|
940
966
|
const data = zeroArea
|
|
941
967
|
? "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Wl6SAAAAABJRU5ErkJggg=="
|
|
942
|
-
: await screenshotData(ctx, target,
|
|
943
|
-
if (!data || stream
|
|
968
|
+
: await screenshotData(ctx, target, options);
|
|
969
|
+
if (!data || !stream.active || loaderId !== target.loaderId) return;
|
|
944
970
|
const scrollOffset = zeroArea && notifiedOffset ? notifiedOffset
|
|
945
971
|
: await ctx.scrollOffset?.() ?? { x: 0, y: 0 };
|
|
946
|
-
if (stream
|
|
972
|
+
if (!stream.active || loaderId !== target.loaderId) return;
|
|
947
973
|
const sessionId = state.nextScreencastFrameId++;
|
|
948
|
-
|
|
974
|
+
stream.frames.add(sessionId);
|
|
949
975
|
emit("Page.screencastFrame", {
|
|
950
976
|
data,
|
|
951
977
|
metadata: {
|
|
@@ -972,7 +998,7 @@ function getPendingInitialNavigationTarget(state) {
|
|
|
972
998
|
|
|
973
999
|
function navigateTarget(state, target, url, emit, options = {}) {
|
|
974
1000
|
const deferLifecycle = typeof state.context?.onLifecycle === "function";
|
|
975
|
-
target.
|
|
1001
|
+
for (const stream of screencasts(target)) stream.scrollOffset = null;
|
|
976
1002
|
target.lifecycleEmit = null;
|
|
977
1003
|
target.url = url;
|
|
978
1004
|
loadDocument(target, url);
|
|
@@ -1067,9 +1093,7 @@ function getImplicitTarget(state) {
|
|
|
1067
1093
|
|
|
1068
1094
|
state.implicitTarget = {
|
|
1069
1095
|
targetId: "implicit-target",
|
|
1070
|
-
|
|
1071
|
-
screencastScrollOffset: null,
|
|
1072
|
-
screencastFrames: new Set(),
|
|
1096
|
+
screencasts: new Map(),
|
|
1073
1097
|
frameId: "implicit-target",
|
|
1074
1098
|
loaderId: "implicit-loader",
|
|
1075
1099
|
type: "page",
|
package/package.json
CHANGED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {test, expect} from 'bun:test';
|
|
2
|
+
import {CdpServer} from '../lib/cdp-server.js';
|
|
3
|
+
const until=async fn=>{for(let i=0;i<200;i++){if(fn())return;await Bun.sleep(5);}throw Error('viewer update timed out');};
|
|
4
|
+
async function connect(url){
|
|
5
|
+
const ws=new WebSocket(url), events=[], pending=new Map();let id=0;
|
|
6
|
+
ws.onmessage=({data})=>{const m=JSON.parse(data);if(m.id){const p=pending.get(m.id);pending.delete(m.id);m.error?p.reject(Error(m.error.message)):p.resolve(m.result);}else events.push(m);};
|
|
7
|
+
await new Promise((resolve,reject)=>{ws.onopen=resolve;ws.onerror=reject;});
|
|
8
|
+
return {ws,events,send(method,params={},sessionId){return new Promise((resolve,reject)=>{pending.set(++id,{resolve,reject});ws.send(JSON.stringify({id,method,params,sessionId}));});}};
|
|
9
|
+
}
|
|
10
|
+
for(const direct of [false,true])test(`independent viewer ACK, stop and disconnect (${direct?'page sockets':'sessions'})`,async()=>{
|
|
11
|
+
let frame;const server=CdpServer.create({screenshot:options=>options.format??'png',onFrame:fn=>frame=fn}).listen(0);
|
|
12
|
+
const clients=[];
|
|
13
|
+
try{
|
|
14
|
+
const root=await connect(`ws://127.0.0.1:${server.port}/devtools/browser/cdp-server`);clients.push(root);
|
|
15
|
+
const {targetId}=await root.send('Target.createTarget',{url:'about:blank'});
|
|
16
|
+
async function viewer(){const c=await connect(`ws://127.0.0.1:${server.port}/devtools/${direct?'page/'+targetId:'browser/cdp-server'}`);clients.push(c);const sid=direct?undefined:(await c.send('Target.attachToTarget',{targetId,flatten:true})).sessionId;return {c,sid,send:(m,p)=>c.send(m,p,sid),frames:()=>c.events.filter(e=>e.method==='Page.screencastFrame')};}
|
|
17
|
+
const a=await viewer(),b=await viewer();
|
|
18
|
+
await a.send('Page.startScreencast',{format:'png'});await b.send('Page.startScreencast',{format:'jpeg'});
|
|
19
|
+
await until(()=>a.frames().length===1&&b.frames().length===1);
|
|
20
|
+
expect(a.frames()[0].params.data).toBe('png');expect(b.frames()[0].params.data).toBe('jpeg');
|
|
21
|
+
frame();await until(()=>a.frames().length===2&&b.frames().length===2);
|
|
22
|
+
for(let i=0;i<5;i++)frame();
|
|
23
|
+
// A cannot ACK B's frames, even when both watch the same target.
|
|
24
|
+
await a.send('Page.screencastFrameAck',{sessionId:b.frames()[0].params.sessionId});
|
|
25
|
+
await Bun.sleep(20);expect(a.frames()).toHaveLength(2);expect(b.frames()).toHaveLength(2);
|
|
26
|
+
await b.send('Page.screencastFrameAck',{sessionId:b.frames()[0].params.sessionId});await until(()=>b.frames().length===3);
|
|
27
|
+
expect(a.frames()).toHaveLength(2);
|
|
28
|
+
await a.send('Page.stopScreencast');
|
|
29
|
+
await b.send('Page.screencastFrameAck',{sessionId:b.frames()[1].params.sessionId});frame();await until(()=>b.frames().length===4);
|
|
30
|
+
a.c.ws.close();await Bun.sleep(20);
|
|
31
|
+
await b.send('Page.screencastFrameAck',{sessionId:b.frames()[2].params.sessionId});frame();await until(()=>b.frames().length===5);
|
|
32
|
+
const replacement=await viewer();await replacement.send('Page.startScreencast');await until(()=>replacement.frames().length===1);
|
|
33
|
+
await b.send('Page.stopScreencast');frame();await until(()=>replacement.frames().length===2);
|
|
34
|
+
if(!direct){
|
|
35
|
+
await expect(root.send('Page.stopScreencast',{},b.sid)).rejects.toThrow('No session owned');
|
|
36
|
+
const {sessionId}=await replacement.c.send('Target.attachToTarget',{targetId,flatten:true});
|
|
37
|
+
await replacement.c.send('Page.startScreencast',{format:'jpeg'},sessionId);
|
|
38
|
+
await until(()=>replacement.c.events.some(e=>e.method==='Page.screencastFrame'&&e.sessionId===sessionId));
|
|
39
|
+
await replacement.send('Page.stopScreencast');frame();
|
|
40
|
+
await until(()=>replacement.c.events.filter(e=>e.method==='Page.screencastFrame'&&e.sessionId===sessionId).length===2);
|
|
41
|
+
}
|
|
42
|
+
}finally{for(const c of clients)c.ws.close();server.stop(true);}
|
|
43
|
+
});
|
package/test/index.test.js
CHANGED
|
@@ -4,6 +4,7 @@ import { castyCommand, chooseFrontend } from "../index.js";
|
|
|
4
4
|
test("the first argument picks the frontend; anything else goes to casty whole", () => {
|
|
5
5
|
expect(chooseFrontend(["--casty", "example.com", "--", "--mobile"])).toEqual({ frontend: "casty", args: ["example.com", "--", "--mobile"] });
|
|
6
6
|
expect(chooseFrontend(["--headless", "--remote-debugging-port=9222"])).toEqual({ frontend: "headless", args: ["--remote-debugging-port=9222"] });
|
|
7
|
+
expect(chooseFrontend(["--win32", "example.com"])).toEqual({ frontend: "win32", args: ["example.com"] });
|
|
7
8
|
expect(chooseFrontend(["--gtk", "x"])).toEqual({ frontend: "gtk", args: ["x"], reserved: true });
|
|
8
9
|
expect(chooseFrontend(["github.com", "--", "--mobile"])).toEqual({ frontend: "casty", args: ["github.com", "--", "--mobile"] });
|
|
9
10
|
expect(chooseFrontend(["--mobile"])).toEqual({ frontend: "casty", args: ["--mobile"] });
|