266 lines
7.5 KiB
TypeScript
266 lines
7.5 KiB
TypeScript
// This file is part of Moonfire NVR, a security camera network video recorder.
|
|
// Copyright (C) 2021 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt.
|
|
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception
|
|
|
|
import { screen } from "@testing-library/react";
|
|
import { utcToZonedTime } from "date-fns-tz";
|
|
import format from "date-fns/format";
|
|
import { DefaultBodyType, delay, http, HttpResponse, PathParams } from "msw";
|
|
import { setupServer } from "msw/node";
|
|
import { Recording, VideoSampleEntry } from "../api";
|
|
import { renderWithCtx } from "../testutil";
|
|
import { Camera, Stream } from "../types";
|
|
import VideoList, { combine } from "./VideoList";
|
|
import { beforeAll, afterAll, afterEach, expect, test } from "vitest";
|
|
|
|
const TEST_CAMERA: Camera = {
|
|
uuid: "c7278ba0-a001-420c-911e-fff4e33f6916",
|
|
shortName: "test-camera",
|
|
description: "",
|
|
streams: {},
|
|
};
|
|
|
|
const TEST_STREAM: Stream = {
|
|
camera: TEST_CAMERA,
|
|
id: 1,
|
|
streamType: "main",
|
|
retainBytes: 0,
|
|
minStartTime90k: 0,
|
|
maxEndTime90k: 0,
|
|
totalDuration90k: 0,
|
|
totalSampleFileBytes: 0,
|
|
fsBytes: 0,
|
|
days: {},
|
|
record: true,
|
|
};
|
|
|
|
const TEST_RANGE1: [number, number] = [
|
|
145747836000000, // 2021-04-26T00:00:00:00000-07:00
|
|
145755612000000, // 2021-04-27T00:00:00:00000-07:00
|
|
];
|
|
|
|
const TEST_RANGE2: [number, number] = [
|
|
145755612000000, // 2021-04-27T00:00:00:00000-07:00
|
|
145763388000000, // 2021-04-28T00:00:00:00000-07:00
|
|
];
|
|
|
|
const TEST_RECORDINGS1: Recording[] = [
|
|
{
|
|
startId: 44,
|
|
openId: 1,
|
|
runStartId: 44,
|
|
startTime90k: 145750553550000, // 2021-04-26T08:23:15:00000-07:00
|
|
endTime90k: 145750558950000, // 2021-04-26T08:24:15:00000-07:00
|
|
videoSampleEntryId: 4,
|
|
videoSamples: 1860,
|
|
sampleFileBytes: 248000,
|
|
},
|
|
{
|
|
startId: 43,
|
|
openId: 1,
|
|
runStartId: 40,
|
|
startTime90k: 145750548150000, // 2021-04-26T08:22:15:00000-07:00
|
|
endTime90k: 145750553550000, // 2021-04-26T08:23:15:00000-07:00
|
|
videoSampleEntryId: 4,
|
|
videoSamples: 1860,
|
|
sampleFileBytes: 248000,
|
|
},
|
|
{
|
|
startId: 42,
|
|
openId: 1,
|
|
runStartId: 40,
|
|
startTime90k: 145750542570000, // 2021-04-26T08:21:13:00000-07:00
|
|
endTime90k: 145750548150000, // 2021-04-26T08:22:15:00000-07:00
|
|
videoSampleEntryId: 4,
|
|
videoSamples: 1860,
|
|
sampleFileBytes: 248000,
|
|
},
|
|
];
|
|
|
|
const TEST_RECORDINGS2: Recording[] = [
|
|
{
|
|
startId: 42,
|
|
openId: 1,
|
|
runStartId: 40,
|
|
startTime90k: 145757651670000, // 2021-04-27T06:17:43:00000-07:00
|
|
endTime90k: 145757656980000, // 2021-04-27T06:18:42:00000-07:00
|
|
videoSampleEntryId: 4,
|
|
videoSamples: 1860,
|
|
sampleFileBytes: 248000,
|
|
},
|
|
];
|
|
|
|
const TEST_VIDEO_SAMPLE_ENTRIES: { [id: number]: VideoSampleEntry } = {
|
|
4: {
|
|
width: 1920,
|
|
height: 1080,
|
|
aspectWidth: 16,
|
|
aspectHeight: 9,
|
|
},
|
|
};
|
|
|
|
function TestFormat(time90k: number) {
|
|
return format(
|
|
utcToZonedTime(new Date(time90k / 90), "America/Los_Angeles"),
|
|
"d MMM yyyy HH:mm:ss"
|
|
);
|
|
}
|
|
|
|
const server = setupServer(
|
|
http.get<PathParams, DefaultBodyType, any>(
|
|
"/api/cameras/:camera/:streamType/recordings",
|
|
async ({ request }) => {
|
|
const url = new URL(request.url);
|
|
const p = url.searchParams;
|
|
const range90k = [
|
|
parseInt(p.get("startTime90k")!, 10),
|
|
parseInt(p.get("endTime90k")!, 10),
|
|
];
|
|
if (range90k[0] === 42) {
|
|
return HttpResponse.text("server error", { status: 503 });
|
|
}
|
|
if (range90k[0] === TEST_RANGE1[0]) {
|
|
return HttpResponse.json({
|
|
recordings: TEST_RECORDINGS1,
|
|
videoSampleEntries: TEST_VIDEO_SAMPLE_ENTRIES,
|
|
});
|
|
} else {
|
|
await delay(2000); // 2 second delay
|
|
return HttpResponse.json({
|
|
recordings: TEST_RECORDINGS2,
|
|
videoSampleEntries: TEST_VIDEO_SAMPLE_ENTRIES,
|
|
});
|
|
}
|
|
}
|
|
)
|
|
);
|
|
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
|
|
afterEach(() => server.resetHandlers());
|
|
afterAll(() => server.close());
|
|
|
|
test("combine", () => {
|
|
const actual = combine(undefined, {
|
|
videoSampleEntries: TEST_VIDEO_SAMPLE_ENTRIES,
|
|
recordings: TEST_RECORDINGS1,
|
|
});
|
|
const expected = [
|
|
// 44 shouldn't be combined; it's not from the same run as the others.
|
|
{
|
|
startId: 44,
|
|
endId: 44,
|
|
openId: 1,
|
|
runStartId: 44,
|
|
startTime90k: 145750553550000, // 2021-04-26T08:23:15:00000-07:00
|
|
endTime90k: 145750558950000, // 2021-04-26T08:24:15:00000-07:00
|
|
videoSamples: 1860,
|
|
sampleFileBytes: 248000,
|
|
aspectWidth: TEST_VIDEO_SAMPLE_ENTRIES[4].aspectWidth,
|
|
aspectHeight: TEST_VIDEO_SAMPLE_ENTRIES[4].aspectHeight,
|
|
width: TEST_VIDEO_SAMPLE_ENTRIES[4].width,
|
|
height: TEST_VIDEO_SAMPLE_ENTRIES[4].height,
|
|
firstUncommitted: undefined,
|
|
growing: undefined,
|
|
},
|
|
// 42 and 43 are combinable.
|
|
{
|
|
startId: 42,
|
|
endId: 43,
|
|
openId: 1,
|
|
runStartId: 40,
|
|
startTime90k: 145750542570000, // 2021-04-26T08:21:13:00000-07:00
|
|
endTime90k: 145750553550000, // 2021-04-26T08:23:15:00000-07:00
|
|
videoSamples: 3720,
|
|
sampleFileBytes: 496000,
|
|
aspectWidth: TEST_VIDEO_SAMPLE_ENTRIES[4].aspectWidth,
|
|
aspectHeight: TEST_VIDEO_SAMPLE_ENTRIES[4].aspectHeight,
|
|
width: TEST_VIDEO_SAMPLE_ENTRIES[4].width,
|
|
height: TEST_VIDEO_SAMPLE_ENTRIES[4].height,
|
|
firstUncommitted: undefined,
|
|
growing: undefined,
|
|
},
|
|
];
|
|
|
|
// XXX: unsure why this doesn't work:
|
|
//
|
|
// expect(actual).toContainEqual(expected)
|
|
//
|
|
// ...but this does:
|
|
expect(actual).toHaveLength(expected.length);
|
|
for (let i = 0; i < expected.length; i++) {
|
|
expect(actual[i]).toEqual(expected[i]);
|
|
}
|
|
});
|
|
|
|
test("load", async () => {
|
|
renderWithCtx(
|
|
<table>
|
|
<VideoList
|
|
stream={TEST_STREAM}
|
|
range90k={TEST_RANGE1}
|
|
setActiveRecording={() => {}}
|
|
formatTime={TestFormat}
|
|
trimStartAndEnd={false}
|
|
/>
|
|
</table>
|
|
);
|
|
expect(await screen.findByText(/26 Apr 2021 08:21:13/)).toBeInTheDocument();
|
|
});
|
|
|
|
// This test may be slightly flaky because it uses real timers. It looks like
|
|
// msw specifically avoids using test timers:
|
|
// https://github.com/mswjs/msw/pull/243
|
|
test("slow replace", async () => {
|
|
const { rerender } = renderWithCtx(
|
|
<table>
|
|
<VideoList
|
|
stream={TEST_STREAM}
|
|
range90k={TEST_RANGE1}
|
|
setActiveRecording={() => {}}
|
|
formatTime={TestFormat}
|
|
trimStartAndEnd={false}
|
|
/>
|
|
</table>
|
|
);
|
|
expect(await screen.findByText(/26 Apr 2021 08:21:13/)).toBeInTheDocument();
|
|
rerender(
|
|
<table>
|
|
<VideoList
|
|
stream={TEST_STREAM}
|
|
range90k={TEST_RANGE2}
|
|
setActiveRecording={() => {}}
|
|
formatTime={TestFormat}
|
|
trimStartAndEnd={false}
|
|
/>
|
|
</table>
|
|
);
|
|
|
|
// The first results don't go away immediately.
|
|
expect(screen.getByText(/26 Apr 2021 08:21:13/)).toBeInTheDocument();
|
|
|
|
// A loading indicator should show up after a second.
|
|
// The default timeout is 1 second; extend it to pass reliably.
|
|
expect(
|
|
await screen.findByRole("progressbar", {}, { timeout: 2000 })
|
|
).toBeInTheDocument();
|
|
|
|
// Then the second query result should show up.
|
|
expect(
|
|
await screen.findByText(/27 Apr 2021 06:17:43/, {}, { timeout: 2000 })
|
|
).toBeInTheDocument();
|
|
});
|
|
|
|
test("error", async () => {
|
|
renderWithCtx(
|
|
<table>
|
|
<VideoList
|
|
stream={TEST_STREAM}
|
|
range90k={[42, 64]}
|
|
setActiveRecording={() => {}}
|
|
formatTime={TestFormat}
|
|
trimStartAndEnd={false}
|
|
/>
|
|
</table>
|
|
);
|
|
expect(await screen.findByRole("alert")).toHaveTextContent(/server error/);
|
|
});
|