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 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`, `--gtk`, `--appkit`,
100
- `--qt`: native windowed frontends
101
- (reserved, not implemented yet)
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, --gtk, --appkit, --qt
13
- * reserved for the native windowed frontends (not implemented)
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(["--win32", "--gtk", "--appkit", "--qt"]);
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, --gtk, --appkit, --qt
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: win32, gtk, appkit, qt");
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.screencast && target.screencastEmit) {
49
- target.screencastScrollOffset = scrollOffset;
50
- pushScreencastFrame(context, state, target);
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
- if (target.screencastEmit?.owner === ws) stopScreencast(target);
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?.screencastEmit?.owner === ws) stopScreencast(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.screencast) await pushScreencastFrame(ctx, state, 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.screencast) {
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
- target.screencast = { ...params };
537
- target.screencastEmit = emit;
538
- // As in Chrome, the reply does not wait for the first frame (nor the
539
- // renderer, which may still be starting); frames follow as events.
540
- void pushScreencastFrame(ctx, state, target);
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
- if (target.screencastFrames.delete(params.sessionId) && target.screencastPending) void pushScreencastFrame(ctx, state, target);
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.screencast)await pushScreencastFrame(ctx,state,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.screencast) await pushScreencastFrame(ctx, state, 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
- screencast: null,
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 stopScreencast(target) {
902
- target.screencast = null;
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
- /** One capture at a time, with bounded unacknowledged frames. */
910
- async function pushScreencastFrame(ctx, state, target) {
911
- if (!target.screencast || !target.screencastEmit) return;
912
- if (target.screencastBusy || target.screencastFrames.size >= MAX_SCREENCAST_FRAMES) {
913
- target.screencastPending = true;
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
- target.screencastBusy = true;
946
+ stream.busy = true;
917
947
  try {
918
948
  do {
919
- target.screencastPending = false;
920
- await emitScreencastFrame(ctx, state, target);
921
- } while (target.screencastPending && target.screencast && target.screencastEmit
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
- target.screencastBusy = false;
955
+ stream.busy = false;
927
956
  }
928
957
  }
929
958
 
930
- async function emitScreencastFrame(ctx, state, target) {
931
- const stream = target.screencast, emit = target.screencastEmit;
959
+ async function emitScreencastFrame(ctx, state, target, stream) {
960
+ const options = stream.params, emit = stream.emit;
932
961
  const loaderId = target.loaderId;
933
- // This belongs to one invalidation, not a lasting cache. Pending updates
934
- // replace it with the latest position; start/restart without one still query.
935
- const notifiedOffset = target.screencastScrollOffset;
936
- target.screencastScrollOffset = null;
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, stream);
943
- if (!data || stream !== target.screencast || emit !== target.screencastEmit || loaderId !== target.loaderId) return;
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 !== target.screencast || emit !== target.screencastEmit || loaderId !== target.loaderId) return;
972
+ if (!stream.active || loaderId !== target.loaderId) return;
947
973
  const sessionId = state.nextScreencastFrameId++;
948
- target.screencastFrames.add(sessionId);
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.screencastScrollOffset = null;
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
- screencast: null,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buninu-browser",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Buninu Browser is a browser engine written in JavaScript and WebAssembly for Bun.",
5
5
  "keywords": [
6
6
  "buninu",
@@ -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
+ });
@@ -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"] });