882 lines
24 KiB
JavaScript
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();
|
|
});
|
|
});
|