Files
Recordly/electron/ipc/cursor/telemetry.test.ts

123 lines
3.5 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import { CURSOR_TELEMETRY_VERSION } from "../constants";
const { writeFile, rm } = vi.hoisted(() => ({
writeFile: vi.fn(),
rm: vi.fn(),
}));
vi.mock("node:fs/promises", () => ({
default: {
writeFile,
rm,
},
}));
vi.mock("electron", () => ({
app: {
getPath: vi.fn(() => "/tmp"),
},
}));
vi.mock("../utils", () => ({
getTelemetryPathForVideo: vi.fn(() => "/tmp/recording.cursor.json"),
getScreen: vi.fn(() => ({
getCursorScreenPoint: () => ({ x: 0, y: 0 }),
getPrimaryDisplay: () => ({ scaleFactor: 1 }),
getDisplayNearestPoint: () => ({ bounds: { x: 0, y: 0, width: 1, height: 1 } }),
getAllDisplays: () => [],
})),
}));
import { activeCursorSamples, setActiveCursorSamples, setCursorCaptureStartTimeMs } from "../state";
import {
getCursorCaptureElapsedMs,
normalizeCursorTelemetrySamples,
pauseCursorCapture,
pauseCursorCaptureAtBoundary,
pushCursorSample,
resetCursorCaptureClock,
resumeCursorCapture,
writeCursorTelemetry,
} from "./telemetry";
describe("cursor telemetry pause clock", () => {
beforeEach(() => {
writeFile.mockReset();
rm.mockReset();
setCursorCaptureStartTimeMs(1_000);
setActiveCursorSamples([]);
resetCursorCaptureClock();
});
it("subtracts paused time from elapsed cursor timestamps", () => {
expect(getCursorCaptureElapsedMs(1_120)).toBe(120);
pauseCursorCapture(1_200);
expect(getCursorCaptureElapsedMs(1_450)).toBe(200);
resumeCursorCapture(1_700);
expect(getCursorCaptureElapsedMs(1_900)).toBe(400);
});
it("ignores duplicate pause or resume transitions", () => {
pauseCursorCapture(1_150);
pauseCursorCapture(1_250);
resumeCursorCapture(1_500);
resumeCursorCapture(1_650);
expect(getCursorCaptureElapsedMs(1_900)).toBe(550);
});
it("drops cursor samples captured after the renderer pause boundary", () => {
pushCursorSample(0.1, 0.1, 120, "move");
pushCursorSample(0.2, 0.2, 205, "move");
pushCursorSample(0.3, 0.3, 260, "move");
pauseCursorCaptureAtBoundary(1_200);
expect(getCursorCaptureElapsedMs(1_500)).toBe(200);
expect(activeCursorSamples.map((sample) => sample.timeMs)).toEqual([120]);
resumeCursorCapture(1_700);
expect(getCursorCaptureElapsedMs(1_900)).toBe(400);
});
it("normalizes cursor telemetry samples before persisting them", async () => {
const samples = normalizeCursorTelemetrySamples([
{ timeMs: 30, cx: 2, cy: -1, interactionType: "click", cursorType: "pointer" },
{ timeMs: -10, cx: Number.NaN, cy: 0.2, interactionType: "drag", cursorType: "ibeam" },
{ timeMs: 10, cx: 0.25, cy: 0.75, interactionType: "move", cursorType: "text" },
]);
expect(samples).toEqual([
{ timeMs: 0, cx: 0.5, cy: 0.2, interactionType: undefined, cursorType: undefined },
{ timeMs: 10, cx: 0.25, cy: 0.75, interactionType: "move", cursorType: "text" },
{ timeMs: 30, cx: 1, cy: 0, interactionType: "click", cursorType: "pointer" },
]);
await writeCursorTelemetry("/tmp/recording.mp4", samples);
expect(writeFile).toHaveBeenCalledWith(
"/tmp/recording.cursor.json",
JSON.stringify(
{
version: CURSOR_TELEMETRY_VERSION,
samples,
},
null,
2,
),
"utf-8",
);
expect(rm).not.toHaveBeenCalled();
});
it("removes the sidecar when saving an empty cursor telemetry payload", async () => {
await writeCursorTelemetry("/tmp/recording.mp4", []);
expect(rm).toHaveBeenCalledWith("/tmp/recording.cursor.json", { force: true });
expect(writeFile).not.toHaveBeenCalled();
});
});