Files
pleno-vue/tests/unit/scanner-camera-capture-enabled.spec.js
T

882 lines
24 KiB
JavaScript

// @vitest-environment jsdom
import { flushPromises } from "@vue/test-utils";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { mountWithApp } from "./helpers/mountWithApp.js";
import ScannerCamera from "@/components/viewport/page/templates/scanner/graphics/ScannerCamera.vue";
import { captureVideoFrameBlobForLPR } from "@/components/viewport/page/templates/scanner/lprFrameCapture";
const mocks = vi.hoisted(() => ({
captureVideoFrameBlobForLPR: vi.fn(),
getLPRFrameSourceRect: vi.fn(),
getLPRFrameTargetSize: vi.fn(),
getUserMedia: vi.fn(),
isVideoFrameReadyForLPR: vi.fn(),
trackStop: vi.fn(),
videoTrack: null,
}));
vi.mock("@/components/viewport/page/templates/scanner/lprFrameCapture", () => ({
captureVideoFrameBlobForLPR: mocks.captureVideoFrameBlobForLPR,
getLPRFrameSourceRect: mocks.getLPRFrameSourceRect,
getLPRFrameTargetSize: mocks.getLPRFrameTargetSize,
isVideoFrameReadyForLPR: mocks.isVideoFrameReadyForLPR,
LPR_CAMERA_VIDEO_HEIGHT: 576,
LPR_CAMERA_VIDEO_WIDTH: 1024,
}));
vi.mock("@/components/displays/department/pos/steps/mobile/objects/PosDepartmentStepMobileFlow.vue", () => ({
camera: {
getImageCaptureDelay: vi.fn((isFirstCapture) => (isFirstCapture ? 25 : 1000)),
getZoom: vi.fn(() => 1),
},
isCameraMounted: { value: false },
}));
const setDocumentVisibility = (visibilityState) => {
Object.defineProperty(document, "visibilityState", {
configurable: true,
value: visibilityState,
});
};
const overconstrainedError = (constraint) => ({
constraint,
name: "OverconstrainedError",
});
describe("ScannerCamera capture gating", () => {
beforeEach(() => {
vi.useFakeTimers();
setDocumentVisibility("visible");
mocks.captureVideoFrameBlobForLPR.mockReset();
mocks.getLPRFrameSourceRect.mockReset();
mocks.getLPRFrameSourceRect.mockReturnValue({ height: 1, width: 1, x: 0, y: 0 });
mocks.getLPRFrameTargetSize.mockReset();
mocks.getLPRFrameTargetSize.mockReturnValue({ width: 1, height: 1 });
mocks.captureVideoFrameBlobForLPR.mockResolvedValue({
blob: new Blob(["frame"], { type: "image/jpeg" }),
filename: "frame.jpg",
fingerprint: "frame",
height: 1,
mimeType: "image/jpeg",
width: 1,
});
mocks.isVideoFrameReadyForLPR.mockReset();
mocks.isVideoFrameReadyForLPR.mockReturnValue(true);
mocks.getUserMedia.mockReset();
mocks.trackStop.mockReset();
mocks.videoTrack = {
enabled: true,
kind: "video",
stop: mocks.trackStop,
};
mocks.getUserMedia.mockResolvedValue({
getTracks: () => [mocks.videoTrack],
getVideoTracks: () => [mocks.videoTrack],
});
Object.defineProperty(navigator, "mediaDevices", {
configurable: true,
value: {
getUserMedia: mocks.getUserMedia,
},
});
Object.defineProperty(HTMLMediaElement.prototype, "play", {
configurable: true,
value: vi.fn(() => Promise.resolve()),
});
Object.defineProperty(HTMLMediaElement.prototype, "pause", {
configurable: true,
value: vi.fn(),
});
});
afterEach(() => {
setDocumentVisibility("visible");
vi.useRealTimers();
vi.restoreAllMocks();
});
it("does not encode frames while capture is disabled", async () => {
const wrapper = mountWithApp(ScannerCamera, {
props: {
captureEnabled: false,
},
});
await flushPromises();
vi.advanceTimersByTime(100);
await flushPromises();
expect(mocks.getUserMedia).toHaveBeenCalledTimes(1);
expect(captureVideoFrameBlobForLPR).not.toHaveBeenCalled();
expect(vi.getTimerCount()).toBe(0);
wrapper.unmount();
});
it("pauses frame capture while the page is hidden and resumes when visible", async () => {
setDocumentVisibility("hidden");
const wrapper = mountWithApp(ScannerCamera, {
props: {
captureEnabled: true,
},
});
await flushPromises();
vi.advanceTimersByTime(2000);
await flushPromises();
expect(mocks.getUserMedia).toHaveBeenCalledTimes(1);
expect(captureVideoFrameBlobForLPR).not.toHaveBeenCalled();
expect(vi.getTimerCount()).toBe(0);
setDocumentVisibility("visible");
document.dispatchEvent(new Event("visibilitychange"));
await flushPromises();
vi.advanceTimersByTime(0);
await flushPromises();
expect(captureVideoFrameBlobForLPR).toHaveBeenCalledTimes(1);
vi.advanceTimersByTime(999);
await flushPromises();
expect(captureVideoFrameBlobForLPR).toHaveBeenCalledTimes(1);
vi.advanceTimersByTime(1);
await flushPromises();
expect(captureVideoFrameBlobForLPR).toHaveBeenCalledTimes(2);
wrapper.unmount();
});
it("does not emit an in-flight captured frame after the page becomes hidden", async () => {
let resolveFrame;
mocks.captureVideoFrameBlobForLPR.mockReturnValueOnce(
new Promise((resolve) => {
resolveFrame = resolve;
})
);
const wrapper = mountWithApp(ScannerCamera, {
props: {
captureEnabled: true,
},
});
await flushPromises();
vi.advanceTimersByTime(25);
await flushPromises();
expect(captureVideoFrameBlobForLPR).toHaveBeenCalledTimes(1);
setDocumentVisibility("hidden");
document.dispatchEvent(new Event("visibilitychange"));
resolveFrame({
blob: new Blob(["hidden-frame"], { type: "image/jpeg" }),
filename: "hidden-frame.jpg",
fingerprint: "hidden-frame",
height: 1,
mimeType: "image/jpeg",
width: 1,
});
await flushPromises();
expect(wrapper.emitted("update:frame")).toBeUndefined();
setDocumentVisibility("visible");
document.dispatchEvent(new Event("visibilitychange"));
await flushPromises();
vi.advanceTimersByTime(0);
await flushPromises();
expect(captureVideoFrameBlobForLPR).toHaveBeenCalledTimes(2);
expect(wrapper.emitted("update:frame")).toHaveLength(1);
wrapper.unmount();
});
it("encodes frames when capture is enabled", async () => {
const wrapper = mountWithApp(ScannerCamera, {
props: {
captureEnabled: true,
},
});
await flushPromises();
vi.advanceTimersByTime(25);
await flushPromises();
expect(captureVideoFrameBlobForLPR).toHaveBeenCalledTimes(1);
expect(captureVideoFrameBlobForLPR).toHaveBeenCalledWith(
expect.any(HTMLVideoElement),
expect.any(HTMLCanvasElement),
{
focusCrop: true,
focusViewportRect: null,
shouldBuildVisualFingerprint: undefined,
shouldEncode: undefined,
visualFingerprintCanvas: expect.any(HTMLCanvasElement),
}
);
expect(wrapper.emitted("update:frame")?.[0]?.[0]).toMatchObject({
filename: "frame.jpg",
fingerprint: "frame",
});
wrapper.unmount();
});
it("keeps the live preview running while the current frame is encoded", async () => {
const pause = HTMLMediaElement.prototype.pause;
let resolveFrame;
mocks.captureVideoFrameBlobForLPR.mockImplementationOnce(() => {
expect(mocks.videoTrack.enabled).toBe(true);
return new Promise((resolve) => {
resolveFrame = resolve;
});
});
const wrapper = mountWithApp(ScannerCamera, {
props: {
captureEnabled: true,
},
});
await flushPromises();
vi.advanceTimersByTime(25);
await flushPromises();
expect(captureVideoFrameBlobForLPR).toHaveBeenCalledTimes(1);
expect(mocks.videoTrack.enabled).toBe(true);
expect(pause).not.toHaveBeenCalled();
resolveFrame({
blob: new Blob(["encoded-frame"], { type: "image/jpeg" }),
filename: "encoded-frame.jpg",
fingerprint: "encoded-frame",
height: 1,
mimeType: "image/jpeg",
width: 1,
});
await flushPromises();
expect(mocks.videoTrack.enabled).toBe(true);
expect(pause).not.toHaveBeenCalled();
expect(wrapper.emitted("update:frame")?.[0]?.[0]).toMatchObject({
filename: "encoded-frame.jpg",
fingerprint: "encoded-frame",
});
wrapper.unmount();
});
it("pauses the live preview while requested and resumes it afterward", async () => {
const play = HTMLMediaElement.prototype.play;
const pause = HTMLMediaElement.prototype.pause;
const wrapper = mountWithApp(ScannerCamera, {
props: {
captureEnabled: true,
},
});
await flushPromises();
expect(play).toHaveBeenCalledTimes(1);
expect(pause).not.toHaveBeenCalled();
await wrapper.setProps({ pausePreview: true });
await flushPromises();
expect(pause).toHaveBeenCalledTimes(1);
expect(mocks.videoTrack.enabled).toBe(false);
await wrapper.setProps({ pausePreview: false });
await flushPromises();
expect(play).toHaveBeenCalledTimes(2);
expect(mocks.videoTrack.enabled).toBe(true);
wrapper.unmount();
});
it("does not replay an already running preview after each preview-mode capture", async () => {
const play = HTMLMediaElement.prototype.play;
const wrapper = mountWithApp(ScannerCamera, {
props: {
captureEnabled: true,
captureMode: "preview",
},
});
await flushPromises();
expect(play).toHaveBeenCalledTimes(1);
vi.advanceTimersByTime(25);
await flushPromises();
vi.advanceTimersByTime(1000);
await flushPromises();
expect(captureVideoFrameBlobForLPR).toHaveBeenCalledTimes(2);
expect(play).toHaveBeenCalledTimes(1);
wrapper.unmount();
});
it("does not capture frozen preview frames while the live preview is paused", async () => {
const wrapper = mountWithApp(ScannerCamera, {
props: {
captureEnabled: true,
},
});
await flushPromises();
await wrapper.setProps({ pausePreview: true });
await flushPromises();
vi.advanceTimersByTime(2000);
await flushPromises();
expect(captureVideoFrameBlobForLPR).not.toHaveBeenCalled();
await wrapper.setProps({ pausePreview: false });
await flushPromises();
vi.advanceTimersByTime(99);
await flushPromises();
expect(captureVideoFrameBlobForLPR).not.toHaveBeenCalled();
vi.advanceTimersByTime(1);
await flushPromises();
expect(captureVideoFrameBlobForLPR).toHaveBeenCalledTimes(1);
wrapper.unmount();
});
it("does not precompute canvas dimensions outside the capture helper", async () => {
const wrapper = mountWithApp(ScannerCamera, {
props: {
captureEnabled: true,
},
});
await flushPromises();
vi.advanceTimersByTime(25);
await flushPromises();
expect(captureVideoFrameBlobForLPR).toHaveBeenCalledTimes(1);
expect(mocks.getLPRFrameSourceRect).not.toHaveBeenCalled();
expect(mocks.getLPRFrameTargetSize).not.toHaveBeenCalled();
wrapper.unmount();
});
it("caps the requested live camera stream resolution and frame rate", async () => {
const wrapper = mountWithApp(ScannerCamera, {
props: {
captureEnabled: true,
},
});
await flushPromises();
expect(mocks.getUserMedia).toHaveBeenCalledWith(
expect.objectContaining({
video: expect.objectContaining({
frameRate: { ideal: 30, max: 30 },
height: { ideal: 576, max: 576 },
width: { ideal: 1024, max: 1024 },
}),
})
);
wrapper.unmount();
});
it("preserves the resolution cap when the camera rejects the frame-rate cap", async () => {
mocks.getUserMedia.mockRejectedValueOnce(overconstrainedError("frameRate")).mockResolvedValueOnce({
getTracks: () => [mocks.videoTrack],
getVideoTracks: () => [mocks.videoTrack],
});
const wrapper = mountWithApp(ScannerCamera, {
props: {
captureEnabled: true,
},
});
await flushPromises();
expect(mocks.getUserMedia).toHaveBeenCalledTimes(2);
expect(mocks.getUserMedia.mock.calls[0][0]).toEqual(
expect.objectContaining({
video: expect.objectContaining({
frameRate: { ideal: 30, max: 30 },
height: { ideal: 576, max: 576 },
width: { ideal: 1024, max: 1024 },
}),
})
);
expect(mocks.getUserMedia.mock.calls[1][0]).toEqual(
expect.objectContaining({
video: expect.objectContaining({
frameRate: { ideal: 30 },
height: { ideal: 576, max: 576 },
width: { ideal: 1024, max: 1024 },
}),
})
);
expect(mocks.getUserMedia.mock.calls[1][0].video.frameRate).not.toHaveProperty("max");
expect(mocks.getUserMedia.mock.calls[1][0].video.height).toHaveProperty("max", 576);
expect(mocks.getUserMedia.mock.calls[1][0].video.width).toHaveProperty("max", 1024);
wrapper.unmount();
});
it("preserves the frame-rate cap when the camera rejects the resolution cap", async () => {
mocks.getUserMedia.mockRejectedValueOnce(overconstrainedError("width")).mockResolvedValueOnce({
getTracks: () => [mocks.videoTrack],
getVideoTracks: () => [mocks.videoTrack],
});
const wrapper = mountWithApp(ScannerCamera, {
props: {
captureEnabled: true,
},
});
await flushPromises();
expect(mocks.getUserMedia).toHaveBeenCalledTimes(2);
expect(mocks.getUserMedia.mock.calls[0][0]).toEqual(
expect.objectContaining({
video: expect.objectContaining({
frameRate: { ideal: 30, max: 30 },
height: { ideal: 576, max: 576 },
width: { ideal: 1024, max: 1024 },
}),
})
);
expect(mocks.getUserMedia.mock.calls[1][0]).toEqual(
expect.objectContaining({
video: expect.objectContaining({
frameRate: { ideal: 30, max: 30 },
height: { ideal: 576 },
width: { ideal: 1024 },
}),
})
);
expect(mocks.getUserMedia.mock.calls[1][0].video.frameRate).toHaveProperty("max", 30);
expect(mocks.getUserMedia.mock.calls[1][0].video.height).not.toHaveProperty("max");
expect(mocks.getUserMedia.mock.calls[1][0].video.width).not.toHaveProperty("max");
wrapper.unmount();
});
it("uses the visible preview crop when capture mode is preview", async () => {
const wrapper = mountWithApp(ScannerCamera, {
props: {
captureEnabled: true,
captureMode: "preview",
},
});
await flushPromises();
vi.advanceTimersByTime(25);
await flushPromises();
expect(captureVideoFrameBlobForLPR).toHaveBeenCalledTimes(1);
expect(captureVideoFrameBlobForLPR).toHaveBeenCalledWith(
expect.any(HTMLVideoElement),
expect.any(HTMLCanvasElement),
{
focusCrop: false,
focusViewportRect: null,
shouldBuildVisualFingerprint: undefined,
shouldEncode: undefined,
visualFingerprintCanvas: null,
}
);
wrapper.unmount();
});
it("passes a video-relative focus rect when a scanner target is provided", async () => {
const getFocusViewportRect = vi.fn(() => ({
height: 90,
width: 100,
x: 10,
y: 20,
}));
const wrapper = mountWithApp(ScannerCamera, {
props: {
captureEnabled: true,
getFocusViewportRect,
},
});
await flushPromises();
vi.spyOn(wrapper.get("video").element, "getBoundingClientRect").mockReturnValue({
bottom: 854,
height: 844,
left: 5,
right: 395,
toJSON: () => ({}),
top: 10,
width: 390,
x: 5,
y: 10,
});
vi.advanceTimersByTime(25);
await flushPromises();
expect(getFocusViewportRect).toHaveBeenCalled();
expect(captureVideoFrameBlobForLPR).toHaveBeenCalledWith(
expect.any(HTMLVideoElement),
expect.any(HTMLCanvasElement),
{
focusCrop: true,
focusViewportRect: {
height: 90,
width: 100,
x: 5,
y: 10,
},
shouldBuildVisualFingerprint: undefined,
shouldEncode: undefined,
viewportHeight: 844,
viewportWidth: 390,
visualFingerprintCanvas: expect.any(HTMLCanvasElement),
}
);
wrapper.unmount();
});
it("reuses cached video-relative focus geometry until viewport geometry changes", async () => {
const getFocusViewportRect = vi.fn(() => ({
height: 90,
width: 100,
x: 10,
y: 20,
}));
const wrapper = mountWithApp(ScannerCamera, {
props: {
captureEnabled: true,
getFocusViewportRect,
},
});
await flushPromises();
const videoRectSpy = vi.spyOn(wrapper.get("video").element, "getBoundingClientRect").mockReturnValue({
bottom: 854,
height: 844,
left: 5,
right: 395,
toJSON: () => ({}),
top: 10,
width: 390,
x: 5,
y: 10,
});
vi.advanceTimersByTime(25);
await flushPromises();
vi.advanceTimersByTime(1000);
await flushPromises();
expect(captureVideoFrameBlobForLPR).toHaveBeenCalledTimes(2);
expect(getFocusViewportRect).toHaveBeenCalledTimes(1);
expect(videoRectSpy).toHaveBeenCalledTimes(1);
expect(captureVideoFrameBlobForLPR.mock.calls[1][2].focusViewportRect).toEqual({
height: 90,
width: 100,
x: 5,
y: 10,
});
expect(captureVideoFrameBlobForLPR.mock.calls[1][2]).toEqual(
expect.objectContaining({
viewportHeight: 844,
viewportWidth: 390,
})
);
getFocusViewportRect.mockReturnValue({
height: 90,
width: 100,
x: 30,
y: 45,
});
videoRectSpy.mockReturnValue({
bottom: 859,
height: 844,
left: 8,
right: 398,
toJSON: () => ({}),
top: 15,
width: 390,
x: 8,
y: 15,
});
window.dispatchEvent(new Event("resize"));
vi.advanceTimersByTime(1000);
await flushPromises();
expect(captureVideoFrameBlobForLPR).toHaveBeenCalledTimes(3);
expect(getFocusViewportRect).toHaveBeenCalledTimes(2);
expect(videoRectSpy).toHaveBeenCalledTimes(2);
expect(captureVideoFrameBlobForLPR.mock.calls[2][2].focusViewportRect).toEqual({
height: 90,
width: 100,
x: 22,
y: 30,
});
expect(captureVideoFrameBlobForLPR.mock.calls[2][2]).toEqual(
expect.objectContaining({
viewportHeight: 844,
viewportWidth: 390,
})
);
wrapper.unmount();
});
it("uses the shorter first-capture delay before the recurring capture interval", async () => {
const wrapper = mountWithApp(ScannerCamera, {
props: {
captureEnabled: true,
},
});
await flushPromises();
vi.advanceTimersByTime(24);
await flushPromises();
expect(captureVideoFrameBlobForLPR).not.toHaveBeenCalled();
vi.advanceTimersByTime(1);
await flushPromises();
expect(captureVideoFrameBlobForLPR).toHaveBeenCalledTimes(1);
vi.advanceTimersByTime(999);
await flushPromises();
expect(captureVideoFrameBlobForLPR).toHaveBeenCalledTimes(1);
vi.advanceTimersByTime(1);
await flushPromises();
expect(captureVideoFrameBlobForLPR).toHaveBeenCalledTimes(2);
wrapper.unmount();
});
it("uses a custom recurring capture interval when provided", async () => {
const wrapper = mountWithApp(ScannerCamera, {
props: {
captureEnabled: true,
captureIntervalMs: 350,
},
});
await flushPromises();
vi.advanceTimersByTime(25);
await flushPromises();
expect(captureVideoFrameBlobForLPR).toHaveBeenCalledTimes(1);
vi.advanceTimersByTime(349);
await flushPromises();
expect(captureVideoFrameBlobForLPR).toHaveBeenCalledTimes(1);
vi.advanceTimersByTime(1);
await flushPromises();
expect(captureVideoFrameBlobForLPR).toHaveBeenCalledTimes(2);
await wrapper.setProps({ captureIntervalMs: 200 });
await flushPromises();
vi.advanceTimersByTime(199);
await flushPromises();
expect(captureVideoFrameBlobForLPR).toHaveBeenCalledTimes(2);
vi.advanceTimersByTime(1);
await flushPromises();
expect(captureVideoFrameBlobForLPR).toHaveBeenCalledTimes(3);
wrapper.unmount();
});
it("does not schedule another recurring capture while frame encoding is still settling", async () => {
let resolveSlowFrame;
mocks.captureVideoFrameBlobForLPR
.mockResolvedValueOnce({
blob: new Blob(["first"], { type: "image/jpeg" }),
filename: "first.jpg",
fingerprint: "first",
height: 1,
mimeType: "image/jpeg",
width: 1,
})
.mockReturnValueOnce(
new Promise((resolve) => {
resolveSlowFrame = resolve;
})
)
.mockResolvedValue({
blob: new Blob(["next"], { type: "image/jpeg" }),
filename: "next.jpg",
fingerprint: "next",
height: 1,
mimeType: "image/jpeg",
width: 1,
});
const wrapper = mountWithApp(ScannerCamera, {
props: {
captureEnabled: true,
},
});
await flushPromises();
vi.advanceTimersByTime(25);
await flushPromises();
expect(captureVideoFrameBlobForLPR).toHaveBeenCalledTimes(1);
vi.advanceTimersByTime(1000);
await flushPromises();
expect(captureVideoFrameBlobForLPR).toHaveBeenCalledTimes(2);
vi.advanceTimersByTime(5000);
await flushPromises();
expect(captureVideoFrameBlobForLPR).toHaveBeenCalledTimes(2);
resolveSlowFrame({
blob: new Blob(["slow"], { type: "image/jpeg" }),
filename: "slow.jpg",
fingerprint: "slow",
height: 1,
mimeType: "image/jpeg",
width: 1,
});
await flushPromises();
vi.advanceTimersByTime(999);
await flushPromises();
expect(captureVideoFrameBlobForLPR).toHaveBeenCalledTimes(2);
vi.advanceTimersByTime(1);
await flushPromises();
expect(captureVideoFrameBlobForLPR).toHaveBeenCalledTimes(3);
wrapper.unmount();
});
it("captures immediately and restarts the interval when capture is re-enabled", async () => {
const wrapper = mountWithApp(ScannerCamera, {
props: {
captureEnabled: false,
},
});
await flushPromises();
vi.advanceTimersByTime(25);
await flushPromises();
expect(captureVideoFrameBlobForLPR).not.toHaveBeenCalled();
await wrapper.setProps({ captureEnabled: true });
await flushPromises();
vi.advanceTimersByTime(0);
await flushPromises();
expect(captureVideoFrameBlobForLPR).toHaveBeenCalledTimes(1);
vi.advanceTimersByTime(999);
await flushPromises();
expect(captureVideoFrameBlobForLPR).toHaveBeenCalledTimes(1);
vi.advanceTimersByTime(1);
await flushPromises();
expect(captureVideoFrameBlobForLPR).toHaveBeenCalledTimes(2);
wrapper.unmount();
});
it("retries shortly when the first capture fires before the video frame is ready", async () => {
mocks.isVideoFrameReadyForLPR.mockReturnValue(false);
const wrapper = mountWithApp(ScannerCamera, {
props: {
captureEnabled: true,
},
});
await flushPromises();
vi.advanceTimersByTime(25);
await flushPromises();
expect(captureVideoFrameBlobForLPR).not.toHaveBeenCalled();
mocks.isVideoFrameReadyForLPR.mockReturnValue(true);
vi.advanceTimersByTime(99);
await flushPromises();
expect(captureVideoFrameBlobForLPR).not.toHaveBeenCalled();
vi.advanceTimersByTime(1);
await flushPromises();
expect(captureVideoFrameBlobForLPR).toHaveBeenCalledTimes(1);
vi.advanceTimersByTime(999);
await flushPromises();
expect(captureVideoFrameBlobForLPR).toHaveBeenCalledTimes(1);
vi.advanceTimersByTime(1);
await flushPromises();
expect(captureVideoFrameBlobForLPR).toHaveBeenCalledTimes(2);
wrapper.unmount();
});
it("captures from the video loadeddata event before the first-capture timer fires", async () => {
mocks.isVideoFrameReadyForLPR.mockReturnValue(false);
const wrapper = mountWithApp(ScannerCamera, {
props: {
captureEnabled: true,
},
});
await flushPromises();
mocks.isVideoFrameReadyForLPR.mockReturnValue(true);
await wrapper.get("video").trigger("loadeddata");
vi.advanceTimersByTime(0);
await flushPromises();
expect(captureVideoFrameBlobForLPR).toHaveBeenCalledTimes(1);
vi.advanceTimersByTime(25);
await flushPromises();
expect(captureVideoFrameBlobForLPR).toHaveBeenCalledTimes(1);
wrapper.unmount();
});
});