1import { describe, expect, it, vi } from "vite-plus/test";
2
3import { start, stop } from "./device-stream.browser";
4
5const envelope = (tag: number, payload: number[]) => {
6 const length = 1 + payload.length;
7 return [
8 length >>> 24,
9 (length >>> 16) & 0xff,
10 (length >>> 8) & 0xff,
11 length & 0xff,
12 tag,
13 ...payload,
14 ];
15};
16
17function viewerFixture(supported: boolean) {
18 vi.useFakeTimers();
19 const configured: string[] = [];
20 vi.stubGlobal(
21 "VideoDecoder",
22 class {
23 static isConfigSupported = async () => ({ supported });
24 state = "unconfigured";
25 decodeQueueSize = 0;
26 configure(config: VideoDecoderConfig) {
27 configured.push(config.codec);
28 this.state = "configured";
29 }
30 decode() {}
31 close() {
32 this.state = "closed";
33 }
34 },
35 );
36 vi.stubGlobal("EncodedVideoChunk", vi.fn());
37 vi.stubGlobal(
38 "WebSocket",
39 class {
40 static OPEN = 1;
41 readyState = 0;
42 send() {}
43 close() {}
44 },
45 );
46 const videoBodies: ReadableStreamDefaultController<Uint8Array>[] = [];
47 const requested: string[] = [];
48 vi.stubGlobal("fetch", (url: string, init: RequestInit) => {
49 requested.push(new URL(url).pathname);
50 if (!url.includes("stream.avcc")) return Promise.resolve(new Response(null));
51 return Promise.resolve(
52 new Response(
53 new ReadableStream<Uint8Array>({
54 start(body) {
55 videoBodies.push(body);
56 init.signal?.addEventListener("abort", () => body.error(new Error("aborted")));
57 },
58 }),
59 ),
60 );
61 });
62 vi.stubGlobal("ReactNativeWebView", { postMessage() {} });
63 start({
64 platform: "ios",
65 deviceId: "sim",
66 access: {
67 httpBase: "https://environment.test/api/device-hub",
68 wsBase: "wss://environment.test/api/device-hub",
69 credentials: false,
70 query: {},
71 },
72 colors: {
73 background: "black",
74 foreground: "white",
75 muted: "gray",
76 buttonBackground: "black",
77 buttonForeground: "white",
78 buttonBorder: "gray",
79 },
80 });
81 return { configured, requested, videoBodies };
82}
83
84describe("native iOS simulator viewer", () => {
85 it.each([
86 { supported: true, image: "" },
87 { supported: false, image: "/api/device-hub/vendor/serve-sim/helper/sim/stream.mjpeg" },
88 ])(
89 "streams H.264 when WebCodecs decodes it, else MJPEG (supported: $supported)",
90 async ({ supported, image }) => {
91 try {
92 const { configured, requested, videoBodies } = viewerFixture(supported);
93 await vi.advanceTimersByTimeAsync(0);
94 expect(requested).toContain("/api/device-hub/vendor/serve-sim/helper/sim/stream.avcc");
95 videoBodies[0]!.enqueue(new Uint8Array(envelope(1, [1, 0x64, 0x00, 0x33])));
96 await vi.advanceTimersByTimeAsync(0);
97 expect(configured).toEqual(supported ? ["avc1.640033"] : []);
98 const img = document.querySelector("img")!;
99 expect(img.src && new URL(img.src).pathname).toBe(image);
100 expect(img.style.display).toBe(supported ? "none" : "block");
101 } finally {
102 stop();
103 vi.useRealTimers();
104 vi.unstubAllGlobals();
105 }
106 },
107 );
108});