1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { assert, it } from "@effect/vitest";
3import * as HostProcess from "@t3tools/shared/HostProcess";
4import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";
6import * as Layer from "effect/Layer";
7import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
8import { beforeEach, vi } from "vite-plus/test";
9
10const { fetchVideo, open } = vi.hoisted(() => ({ fetchVideo: vi.fn(), open: vi.fn() }));
11vi.mock("electron", () => ({ net: { fetch: fetchVideo } }));
12
13import * as DesktopVideo from "./DesktopVideo.ts";
14
15const spawner = Layer.mock(ChildProcessSpawner.ChildProcessSpawner, {
16 exitCode: (command) =>
17 Effect.sync(() => {
18 open(command);
19 return ChildProcessSpawner.ExitCode(0);
20 }),
21});
22
23beforeEach(() => {
24 fetchVideo.mockReset().mockImplementation(
25 async () =>
26 new Response("video bytes", {
27 headers: { "content-type": "video/mp4" },
28 }),
29 );
30 open.mockReset();
31});
32
33const layer = DesktopVideo.layer.pipe(Layer.provide(spawner));
34
35it.effect(
36 "streams to a private client temp file, opens only QuickTime, and cleans up on app scope exit",
37 () => {
38 let openedPath = "";
39 return Effect.gen(function* () {
40 const fs = yield* FileSystem.FileSystem;
41 yield* Effect.gen(function* () {
42 const video = yield* DesktopVideo.DesktopVideo;
43 yield* video.open({
44 url: "https://remote.test/api/assets/signed/clip.mp4",
45 name: "../../clip.mp4",
46 });
47 const command = open.mock.lastCall![0];
48 assert.equal(command.command, "/usr/bin/open");
49 assert.deepEqual(command.args.slice(0, 2), [
50 "-a",
51 "/System/Applications/QuickTime Player.app",
52 ]);
53 openedPath = command.args[2];
54 assert.match(openedPath, /\/t3-video-[^/]+\/clip\.mp4$/);
55 assert.equal(yield* fs.readFileString(openedPath), "video bytes");
56 assert.equal((yield* fs.stat(openedPath)).mode & 0o777, 0o600);
57 }).pipe(Effect.provide(layer));
58 assert.isFalse(yield* fs.exists(openedPath));
59 }).pipe(
60 Effect.provideService(HostProcess.Platform, "darwin"),
61 Effect.provide(NodeServices.layer),
62 );
63 },
64);
65
66it.effect("rejects non-macOS before downloading", () =>
67 Effect.gen(function* () {
68 const result = yield* Effect.result(
69 (yield* DesktopVideo.DesktopVideo).open({
70 url: "https://remote.test/clip.mp4",
71 name: "clip.mp4",
72 }),
73 );
74 assert.equal(result._tag, "Failure");
75 assert.equal(fetchVideo.mock.calls.length, 0);
76 assert.equal(open.mock.calls.length, 0);
77 }).pipe(
78 Effect.provide(layer),
79 Effect.provideService(HostProcess.Platform, "linux"),
80 Effect.provide(NodeServices.layer),
81 ),
82);
83
84it.effect("rejects file URLs and nonvideo filenames without reading local files", () =>
85 Effect.gen(function* () {
86 const video = yield* DesktopVideo.DesktopVideo;
87 for (const input of [
88 { url: "file:///Users/private/clip.mp4", name: "clip.mp4" },
89 { url: "https://remote.test/run", name: "run.command" },
90 { url: "https://user:password@remote.test/clip.mp4", name: "clip.mp4" },
91 ])
92 assert.equal((yield* Effect.result(video.open(input)))._tag, "Failure");
93 assert.equal(fetchVideo.mock.calls.length, 0);
94 assert.equal(open.mock.calls.length, 0);
95 }).pipe(
96 Effect.provide(layer),
97 Effect.provideService(HostProcess.Platform, "darwin"),
98 Effect.provide(NodeServices.layer),
99 ),
100);
101
102it.effect("does not open an authorization error or HTML error page as a video", () =>
103 Effect.gen(function* () {
104 const video = yield* DesktopVideo.DesktopVideo;
105 for (const response of [
106 new Response("unauthorized", { status: 401 }),
107 new Response("<html>sign in</html>", { headers: { "content-type": "text/html" } }),
108 ]) {
109 fetchVideo.mockResolvedValueOnce(response);
110 assert.equal(
111 (yield* Effect.result(
112 video.open({ url: "https://remote.test/clip.mp4", name: "clip.mp4" }),
113 ))._tag,
114 "Failure",
115 );
116 }
117 assert.equal(open.mock.calls.length, 0);
118 }).pipe(
119 Effect.provide(layer),
120 Effect.provideService(HostProcess.Platform, "darwin"),
121 Effect.provide(NodeServices.layer),
122 ),
123);
124
125it.effect("does not launch a partial download when its stream fails", () =>
126 Effect.gen(function* () {
127 fetchVideo.mockResolvedValueOnce(
128 new Response(
129 new ReadableStream({
130 start(controller) {
131 controller.enqueue(new TextEncoder().encode("partial"));
132 controller.error(new Error("disconnected"));
133 },
134 }),
135 { headers: { "content-type": "video/mp4" } },
136 ),
137 );
138 const video = yield* DesktopVideo.DesktopVideo;
139 assert.equal(
140 (yield* Effect.result(video.open({ url: "https://remote.test/clip.mp4", name: "clip.mp4" })))
141 ._tag,
142 "Failure",
143 );
144 assert.equal(open.mock.calls.length, 0);
145 }).pipe(
146 Effect.provide(layer),
147 Effect.provideService(HostProcess.Platform, "darwin"),
148 Effect.provide(NodeServices.layer),
149 ),
150);
151
152it.effect("uses the video response type for an extensionless remote download", () =>
153 Effect.gen(function* () {
154 fetchVideo.mockResolvedValueOnce(
155 new Response("movie", { headers: { "content-type": "video/quicktime" } }),
156 );
157 yield* (yield* DesktopVideo.DesktopVideo).open({
158 url: "https://remote.test/download?id=clip",
159 name: "recording",
160 });
161 assert.match(open.mock.lastCall![0].args[2], /\/recording\.mov$/);
162 }).pipe(
163 Effect.provide(layer),
164 Effect.provideService(HostProcess.Platform, "darwin"),
165 Effect.provide(NodeServices.layer),
166 ),
167);