Files
Recordly/electron/native/ScreenCaptureKitRecorder.lifecycle.test.ts

104 lines
3.5 KiB
TypeScript

import { spawnSync } from "node:child_process";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
// Exercise the production service and stdin reader in a real Swift process.
// Only the device backend is simulated: CI must not request screen/mic access.
describe.skipIf(process.platform !== "darwin")("recorder process lifecycle", () => {
let directory: string;
let executable: string;
beforeAll(() => {
directory = mkdtempSync(join(tmpdir(), "recordly-lifecycle-"));
executable = join(directory, "helper");
const source = readFileSync(
new URL("./ScreenCaptureKitRecorder.swift", import.meta.url),
"utf8",
);
const service = source.slice(
source.indexOf("final class RecorderService"),
source.indexOf("guard CommandLine.arguments.count"),
);
const commands = source
.slice(
source.indexOf("let service = RecorderService()"),
source.indexOf("if !service.waitUntilFinished()"),
)
.replace(
"service.stop()",
"service.stop()\n\tservice.drainCommandsForTest()\n\tcommandsFinished.signal()",
);
writeFileSync(
join(directory, "main.swift"),
`
import Foundation
final class ScreenCaptureRecorder {
var output = ""
func startCapture(configJSON: String) async throws {
try await Task.sleep(nanoseconds: 50_000_000)
if configJSON == "fail" { throw NSError(domain: "startup", code: 1) }
output = configJSON
try "recording".write(toFile: output, atomically: true, encoding: .utf8)
}
func stopCapture() async throws -> String {
print("BACKEND_STOP")
fflush(stdout)
if output.isEmpty { throw NSError(domain: "inactive", code: 2) }
try "finalized".write(toFile: output, atomically: true, encoding: .utf8)
return output
}
func pauseCapture() async -> Bool { true }
func resumeCapture() async -> Bool { true }
}
${service}
extension RecorderService {
func drainCommandsForTest() { queue.sync {} }
}
let commandsFinished = DispatchSemaphore(value: 0)
${commands}
let success = service.waitUntilFinished()
// Wait for the reader to enqueue stop AND for the serialized operations to finish.
commandsFinished.wait()
exit(success ? 0 : 1)
`,
);
const build = spawnSync(
"swiftc",
[
"-module-cache-path",
join(directory, "cache"),
join(directory, "main.swift"),
"-o",
executable,
],
{ encoding: "utf8", timeout: 60_000 },
);
expect(build.status, build.stderr).toBe(0);
}, 60_000);
afterAll(() => {
if (directory) rmSync(directory, { recursive: true, force: true });
});
it.each([
"",
"stop\n",
"pause\nresume\n",
"stop\nstop\n",
])("finalizes once with stdin %j", (input) => {
const output = join(directory, "recording");
const result = spawnSync(executable, [output], { input, encoding: "utf8", timeout: 5_000 });
expect(result.status, result.stderr).toBe(0);
expect(result.stdout.match(/BACKEND_STOP/g)).toHaveLength(1);
expect(readFileSync(output, "utf8")).toBe("finalized");
});
it.each(["", "stop\n"])("exits cleanly after failed startup with stdin %j", (input) => {
const result = spawnSync(executable, ["fail"], { input, encoding: "utf8", timeout: 5_000 });
expect(result.signal, result.stderr).toBeNull();
expect(result.status, result.stderr).toBe(1);
expect(result.stderr).toContain("Error starting capture");
expect(result.stdout).not.toContain("BACKEND_STOP");
});
});