1140 lines
39 KiB
JavaScript
1140 lines
39 KiB
JavaScript
// @vitest-environment jsdom
|
|
|
|
import { flushPromises } from "@vue/test-utils";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { ref } from "vue";
|
|
import { mountWithApp } from "./helpers/mountWithApp.js";
|
|
import PosDepartmentStepMobile1 from "@/components/displays/department/pos/steps/mobile/PosDepartmentStepMobile1.vue";
|
|
import {
|
|
LPR_FRAME_CLIENT_BYTES_FIELD,
|
|
LPR_FRAME_CLIENT_CAPTURE_MS_FIELD,
|
|
LPR_FRAME_CLIENT_DRAW_MS_FIELD,
|
|
LPR_FRAME_CLIENT_ENCODE_MS_FIELD,
|
|
LPR_FRAME_CLIENT_HEIGHT_FIELD,
|
|
LPR_FRAME_CLIENT_PREFLIGHT_MS_FIELD,
|
|
LPR_FRAME_CLIENT_VISUAL_FINGERPRINT_MS_FIELD,
|
|
LPR_FRAME_CLIENT_WIDTH_FIELD,
|
|
} from "@/components/viewport/page/templates/scanner/lprFrameCapture";
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
request: vi.fn(),
|
|
setTransparency: vi.fn(),
|
|
setBackgroundColor: vi.fn(),
|
|
setOverflow: vi.fn(),
|
|
cameraSetLatestImage: vi.fn(),
|
|
cameraSetLatestImageBlob: vi.fn(),
|
|
cameraSetLastSuccess: vi.fn(),
|
|
cameraHasDelayAfterSuccessPassed: vi.fn(() => true),
|
|
cameraGetImageCaptureDelay: vi.fn(() => 1005),
|
|
cameraGetImageCaptureDelayAfterSuccess: vi.fn(() => 1005),
|
|
vehicleSelect: vi.fn(),
|
|
attachmentView: null,
|
|
manualInput: null,
|
|
transactionHistoryView: null,
|
|
activeVehicleIndex: null,
|
|
vehicle1: null,
|
|
vehicle2: null,
|
|
vehicle3: null,
|
|
}));
|
|
|
|
vi.mock("@/components/viewport/page/headers/ViewportHeaderSettings.vue", () => ({
|
|
backgroundColors: {
|
|
default: "default",
|
|
},
|
|
setBackgroundColor: mocks.setBackgroundColor,
|
|
setOverflow: mocks.setOverflow,
|
|
setTransparency: mocks.setTransparency,
|
|
}));
|
|
|
|
vi.mock("@/components/session/token/SessionUser.vue", () => ({
|
|
SessionUser: {
|
|
request: mocks.request,
|
|
objects: {
|
|
global: {
|
|
language: {
|
|
scanning: "Scanning",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}));
|
|
|
|
vi.mock("@/components/displays/department/pos/steps/mobile/objects/PosDepartmentStepMobileFlow.vue", () => {
|
|
const manualInput = ref(false);
|
|
const transactionHistoryView = ref(false);
|
|
const attachmentView = ref(false);
|
|
const activeVehicleIndex = ref(1);
|
|
const activeVehicle = ref({ reg: "" });
|
|
const vehicle2 = ref({ reg: "" });
|
|
const vehicle3 = ref({ reg: "" });
|
|
const latestImage = ref(null);
|
|
mocks.attachmentView = attachmentView;
|
|
mocks.manualInput = manualInput;
|
|
mocks.transactionHistoryView = transactionHistoryView;
|
|
mocks.activeVehicleIndex = activeVehicleIndex;
|
|
mocks.vehicle1 = activeVehicle;
|
|
mocks.vehicle2 = vehicle2;
|
|
mocks.vehicle3 = vehicle3;
|
|
|
|
return {
|
|
attachments: {
|
|
base64: ref([]),
|
|
},
|
|
camera: {
|
|
latestImage,
|
|
setLatestImage: mocks.cameraSetLatestImage,
|
|
setLatestImageBlob: mocks.cameraSetLatestImageBlob,
|
|
hasDelayAfterSuccessPassed: mocks.cameraHasDelayAfterSuccessPassed,
|
|
getImageCaptureDelay: mocks.cameraGetImageCaptureDelay,
|
|
getImageCaptureDelayAfterSuccess: mocks.cameraGetImageCaptureDelayAfterSuccess,
|
|
setLastSuccess: mocks.cameraSetLastSuccess,
|
|
},
|
|
manualInput,
|
|
sounds: {
|
|
list: ref({ onAfterSuccessfulScan: "scan" }),
|
|
play: vi.fn(),
|
|
},
|
|
transactionHistoryView,
|
|
vehicles: {
|
|
activeVehicleIndex,
|
|
getActiveVehicle: () => {
|
|
if (activeVehicleIndex.value === 2) {
|
|
return vehicle2.value;
|
|
}
|
|
if (activeVehicleIndex.value === 3) {
|
|
return vehicle3.value;
|
|
}
|
|
return activeVehicle.value;
|
|
},
|
|
get: (index) => {
|
|
if (index === 1) {
|
|
return activeVehicle.value;
|
|
}
|
|
if (index === 2) {
|
|
return vehicle2.value;
|
|
}
|
|
if (index === 3) {
|
|
return vehicle3.value;
|
|
}
|
|
return null;
|
|
},
|
|
select: mocks.vehicleSelect,
|
|
setActiveVehicleIndex: vi.fn((nextIndex) => {
|
|
activeVehicleIndex.value = nextIndex;
|
|
}),
|
|
vehicle_1: activeVehicle,
|
|
vehicle_2: vehicle2,
|
|
vehicle_3: vehicle3,
|
|
},
|
|
views: {
|
|
attachmentView,
|
|
isAnyActive: ref(false),
|
|
},
|
|
};
|
|
});
|
|
|
|
vi.mock("@/components/viewport/page/templates/scanner/graphics/ScannerCamera.vue", () => ({
|
|
default: {
|
|
name: "ScannerCamera",
|
|
emits: ["update:frame"],
|
|
props: {
|
|
captureEnabled: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
captureIntervalMs: {
|
|
type: Number,
|
|
default: null,
|
|
},
|
|
captureMode: {
|
|
type: String,
|
|
default: "lpr",
|
|
},
|
|
getFocusViewportRect: {
|
|
type: Function,
|
|
default: null,
|
|
},
|
|
pausePreview: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
shouldBuildVisualFingerprint: {
|
|
type: Function,
|
|
default: null,
|
|
},
|
|
shouldEncodeFrame: {
|
|
type: Function,
|
|
default: null,
|
|
},
|
|
},
|
|
methods: {
|
|
shouldBuildVisualFingerprintNow() {
|
|
if (this.shouldBuildVisualFingerprint === null) {
|
|
return true;
|
|
}
|
|
|
|
return this.shouldBuildVisualFingerprint();
|
|
},
|
|
shouldEncodeCandidate(visualFingerprint = null) {
|
|
if (this.shouldEncodeFrame === null) {
|
|
return true;
|
|
}
|
|
|
|
return this.shouldEncodeFrame({
|
|
height: 1,
|
|
...(visualFingerprint !== null ? { visualFingerprint } : {}),
|
|
width: 1,
|
|
});
|
|
},
|
|
frame(name, fingerprint = name, contentFingerprint = name, visualFingerprint = null) {
|
|
return {
|
|
blob: new Blob([name], { type: "image/jpeg" }),
|
|
captureDurationMs: 12.345,
|
|
captureTimings: {
|
|
drawMs: 1.25,
|
|
encodeMs: 2.5,
|
|
visualFingerprintMs: 0.75,
|
|
},
|
|
filename: `${name}.jpg`,
|
|
fingerprint,
|
|
getContentFingerprint: vi.fn(() => Promise.resolve(contentFingerprint)),
|
|
height: 1,
|
|
mimeType: "image/jpeg",
|
|
...(visualFingerprint !== null ? { visualFingerprint } : {}),
|
|
width: 1,
|
|
};
|
|
},
|
|
frameWithLazyVisual(name, fingerprint = name, contentFingerprint = name, visualFingerprint = null) {
|
|
return {
|
|
blob: new Blob([name], { type: "image/jpeg" }),
|
|
captureDurationMs: 12.345,
|
|
captureTimings: {
|
|
drawMs: 1.25,
|
|
encodeMs: 2.5,
|
|
visualFingerprintMs: 0.75,
|
|
},
|
|
filename: `${name}.jpg`,
|
|
fingerprint,
|
|
getContentFingerprint: vi.fn(() => Promise.resolve(contentFingerprint)),
|
|
getVisualFingerprint: vi.fn(() => visualFingerprint),
|
|
height: 1,
|
|
mimeType: "image/jpeg",
|
|
width: 1,
|
|
};
|
|
},
|
|
emitFrame(name, fingerprint = name, contentFingerprint = name, visualFingerprint = null) {
|
|
if (this.captureEnabled) {
|
|
this.$emit("update:frame", this.frame(name, fingerprint, contentFingerprint, visualFingerprint));
|
|
}
|
|
},
|
|
},
|
|
template: `
|
|
<div>
|
|
<button data-testid="camera-frame-a" @click="emitFrame('frame-a')" />
|
|
<button data-testid="camera-frame-b" @click="emitFrame('frame-b')" />
|
|
<button data-testid="camera-frame-c" @click="emitFrame('frame-c')" />
|
|
<button data-testid="camera-frame-same-a" @click="emitFrame('same-a', 'same-size', 'same-content-a')" />
|
|
<button data-testid="camera-frame-same-a-duplicate" @click="emitFrame('same-a-copy', 'same-size', 'same-content-a')" />
|
|
<button data-testid="camera-frame-same-b" @click="emitFrame('same-b', 'same-size', 'same-content-b')" />
|
|
</div>
|
|
`,
|
|
},
|
|
}));
|
|
|
|
vi.mock("@/components/viewport/page/templates/scanner/graphics/ScannerInstructions.vue", () => ({
|
|
default: { template: "<div />" },
|
|
}));
|
|
|
|
vi.mock("@/components/viewport/page/templates/scanner/graphics/ScannerOutline.vue", () => ({
|
|
default: { template: "<div />" },
|
|
}));
|
|
|
|
vi.mock("@/components/models/pos/step1/RegistrationNumberSearchResult.vue", () => ({
|
|
default: { template: "<div />" },
|
|
}));
|
|
|
|
vi.mock(
|
|
"@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile1RegistrationNumbers.vue",
|
|
() => ({
|
|
default: { template: "<div />" },
|
|
})
|
|
);
|
|
|
|
vi.mock("@/components/displays/department/pos/steps/mobile/views/PosDepartmentStep1MobileManualInput.vue", () => ({
|
|
default: { template: "<div />" },
|
|
}));
|
|
|
|
vi.mock(
|
|
"@/components/displays/department/pos/steps/mobile/views/PosDepartmentStep1MobileTransactionHistory.vue",
|
|
() => ({
|
|
default: { template: "<div />" },
|
|
})
|
|
);
|
|
|
|
vi.mock("@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobileButtonNextStep.vue", () => ({
|
|
default: { template: "<div />" },
|
|
}));
|
|
|
|
vi.mock(
|
|
"@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobileFixedBottomControl.vue",
|
|
() => ({
|
|
default: {
|
|
name: "PosDepartmentStepMobileFixedBottomControl",
|
|
props: {
|
|
useBackdropBlur: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
},
|
|
template:
|
|
'<div data-testid="fixed-bottom-control" :data-use-backdrop-blur="String(useBackdropBlur)"><slot /></div>',
|
|
},
|
|
})
|
|
);
|
|
|
|
vi.mock("@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile1Location.vue", () => ({
|
|
default: { template: "<div />" },
|
|
}));
|
|
|
|
vi.mock("@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile1Debug.vue", () => ({
|
|
default: { template: "<div />" },
|
|
}));
|
|
|
|
vi.mock("@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobileAttachments.vue", () => ({
|
|
default: { template: "<div />" },
|
|
}));
|
|
|
|
vi.mock("@/components/viewport/elements/icons/UnknownCustomer.vue", () => ({
|
|
default: { template: "<span />" },
|
|
}));
|
|
|
|
vi.mock("@/components/viewport/elements/icons/VerifiedCustomer.vue", () => ({
|
|
default: { template: "<span />" },
|
|
}));
|
|
|
|
vi.mock("@/components/viewport/elements/icons/BookedCustomer.vue", () => ({
|
|
default: { template: "<span />" },
|
|
}));
|
|
|
|
vi.mock("@/components/viewport/elements/icons/KnownCustomer.vue", () => ({
|
|
default: { template: "<span />" },
|
|
}));
|
|
|
|
vi.mock("@/components/viewport/elements/icons/CardPaymentCustomer.vue", () => ({
|
|
default: { template: "<span />" },
|
|
}));
|
|
|
|
const resolveImageImmediately = () => {
|
|
class TestImage {
|
|
onerror = null;
|
|
|
|
set src(_value) {
|
|
this.onerror?.(new Error("skip image decode"));
|
|
}
|
|
}
|
|
|
|
vi.stubGlobal("Image", TestImage);
|
|
};
|
|
|
|
const setDocumentVisibility = (visibilityState) => {
|
|
Object.defineProperty(document, "visibilityState", {
|
|
configurable: true,
|
|
value: visibilityState,
|
|
});
|
|
};
|
|
|
|
const expectRawLPRRequest = async (callIndex, expectedFrameText) => {
|
|
const url = mocks.request.mock.calls[callIndex][0];
|
|
const payload = mocks.request.mock.calls[callIndex][2];
|
|
const options = mocks.request.mock.calls[callIndex][5];
|
|
const parsedUrl = new URL(url, "https://scanner.test");
|
|
|
|
expect(parsedUrl.pathname).toBe("/modules/scanner/lpr");
|
|
expect(payload).toBeInstanceOf(Blob);
|
|
expect(payload.type).toBe("image/jpeg");
|
|
await expect(payload.text()).resolves.toBe(expectedFrameText);
|
|
expect(options.headers).toEqual({
|
|
"Content-Type": "image/jpeg",
|
|
});
|
|
expect(options.transport).toBe("fetch");
|
|
expect(parsedUrl.searchParams.get(LPR_FRAME_CLIENT_WIDTH_FIELD)).toBe("1");
|
|
expect(parsedUrl.searchParams.get(LPR_FRAME_CLIENT_HEIGHT_FIELD)).toBe("1");
|
|
expect(parsedUrl.searchParams.get(LPR_FRAME_CLIENT_BYTES_FIELD)).toBe(String(new Blob([expectedFrameText]).size));
|
|
|
|
return { payload, searchParams: parsedUrl.searchParams };
|
|
};
|
|
|
|
describe("POS mobile camera LPR", () => {
|
|
beforeEach(() => {
|
|
setDocumentVisibility("visible");
|
|
mocks.request.mockReset();
|
|
mocks.cameraSetLatestImage.mockReset();
|
|
mocks.cameraSetLatestImageBlob.mockReset();
|
|
mocks.cameraSetLastSuccess.mockReset();
|
|
mocks.cameraHasDelayAfterSuccessPassed.mockReset();
|
|
mocks.cameraHasDelayAfterSuccessPassed.mockReturnValue(true);
|
|
mocks.cameraGetImageCaptureDelay.mockReset();
|
|
mocks.cameraGetImageCaptureDelay.mockReturnValue(1005);
|
|
mocks.cameraGetImageCaptureDelayAfterSuccess.mockReset();
|
|
mocks.cameraGetImageCaptureDelayAfterSuccess.mockReturnValue(1005);
|
|
mocks.vehicleSelect.mockReset();
|
|
mocks.attachmentView.value = false;
|
|
mocks.manualInput.value = false;
|
|
mocks.transactionHistoryView.value = false;
|
|
mocks.activeVehicleIndex.value = 1;
|
|
mocks.vehicle1.value = { reg: "" };
|
|
mocks.vehicle2.value = { reg: "" };
|
|
mocks.vehicle3.value = { reg: "" };
|
|
resolveImageImmediately();
|
|
});
|
|
|
|
afterEach(() => {
|
|
setDocumentVisibility("visible");
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("does not enqueue overlapping scanner requests while a camera frame is still being parsed", async () => {
|
|
vi.useFakeTimers();
|
|
let resolveFirstRequest;
|
|
mocks.request.mockReturnValueOnce(
|
|
new Promise((resolve) => {
|
|
resolveFirstRequest = resolve;
|
|
})
|
|
);
|
|
mocks.request.mockResolvedValue({ data: { success: false } });
|
|
|
|
const wrapper = mountWithApp(PosDepartmentStepMobile1);
|
|
await wrapper.get('[data-testid="camera-frame-a"]').trigger("click");
|
|
await flushPromises();
|
|
expect(mocks.request).toHaveBeenCalledTimes(1);
|
|
const cameraWrapper = wrapper.findComponent({ name: "ScannerCamera" });
|
|
expect(cameraWrapper.props("pausePreview")).toBe(true);
|
|
let rawRequest = await expectRawLPRRequest(0, "frame-a");
|
|
expect(rawRequest.searchParams.get(LPR_FRAME_CLIENT_CAPTURE_MS_FIELD)).toBe("12.345");
|
|
expect(Number(rawRequest.searchParams.get(LPR_FRAME_CLIENT_PREFLIGHT_MS_FIELD))).toBeGreaterThanOrEqual(0);
|
|
expect(rawRequest.searchParams.get(LPR_FRAME_CLIENT_DRAW_MS_FIELD)).toBe("1.250");
|
|
expect(rawRequest.searchParams.get(LPR_FRAME_CLIENT_ENCODE_MS_FIELD)).toBe("2.500");
|
|
expect(rawRequest.searchParams.get(LPR_FRAME_CLIENT_VISUAL_FINGERPRINT_MS_FIELD)).toBe("0.750");
|
|
expect(mocks.cameraSetLatestImageBlob).not.toHaveBeenCalled();
|
|
|
|
await wrapper.get('[data-testid="camera-frame-b"]').trigger("click");
|
|
await flushPromises();
|
|
expect(mocks.request).toHaveBeenCalledTimes(1);
|
|
expect(mocks.cameraSetLatestImageBlob).not.toHaveBeenCalled();
|
|
|
|
cameraWrapper.vm.$emit("update:frame", cameraWrapper.vm.frame("late-frame"));
|
|
await flushPromises();
|
|
expect(mocks.request).toHaveBeenCalledTimes(1);
|
|
expect(mocks.cameraSetLatestImageBlob).not.toHaveBeenCalled();
|
|
|
|
resolveFirstRequest({ data: { success: false } });
|
|
await flushPromises();
|
|
expect(cameraWrapper.props("pausePreview")).toBe(true);
|
|
vi.advanceTimersByTime(1500);
|
|
await flushPromises();
|
|
expect(cameraWrapper.props("pausePreview")).toBe(false);
|
|
|
|
await wrapper.get('[data-testid="camera-frame-c"]').trigger("click");
|
|
await flushPromises();
|
|
expect(mocks.request).toHaveBeenCalledTimes(2);
|
|
rawRequest = await expectRawLPRRequest(1, "frame-c");
|
|
expect(rawRequest.searchParams.get(LPR_FRAME_CLIENT_CAPTURE_MS_FIELD)).toBe("12.345");
|
|
});
|
|
|
|
it("omits zero-valued client timing query params from scanner raw image requests", async () => {
|
|
mocks.request.mockResolvedValue({ data: { success: false } });
|
|
|
|
const wrapper = mountWithApp(PosDepartmentStepMobile1);
|
|
const cameraWrapper = wrapper.findComponent({ name: "ScannerCamera" });
|
|
const frame = cameraWrapper.vm.frame("zero-timing-frame");
|
|
frame.captureDurationMs = 0;
|
|
frame.captureTimings = {
|
|
drawMs: 0,
|
|
encodeMs: 0,
|
|
visualFingerprintMs: 0,
|
|
};
|
|
|
|
cameraWrapper.vm.$emit("update:frame", frame);
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).toHaveBeenCalledTimes(1);
|
|
const { searchParams } = await expectRawLPRRequest(0, "zero-timing-frame");
|
|
expect(searchParams.has(LPR_FRAME_CLIENT_CAPTURE_MS_FIELD)).toBe(false);
|
|
expect(searchParams.has(LPR_FRAME_CLIENT_DRAW_MS_FIELD)).toBe(false);
|
|
expect(searchParams.has(LPR_FRAME_CLIENT_ENCODE_MS_FIELD)).toBe(false);
|
|
expect(searchParams.has(LPR_FRAME_CLIENT_VISUAL_FINGERPRINT_MS_FIELD)).toBe(false);
|
|
});
|
|
|
|
it("pauses the camera preview while scanner duplicate preflight is still running", async () => {
|
|
vi.useFakeTimers();
|
|
mocks.request.mockResolvedValue({ data: { success: false } });
|
|
|
|
const wrapper = mountWithApp(PosDepartmentStepMobile1);
|
|
const cameraWrapper = wrapper.findComponent({ name: "ScannerCamera" });
|
|
|
|
await wrapper.get('[data-testid="camera-frame-a"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).toHaveBeenCalledTimes(1);
|
|
expect(cameraWrapper.props("pausePreview")).toBe(true);
|
|
|
|
vi.advanceTimersByTime(2500);
|
|
await flushPromises();
|
|
expect(cameraWrapper.props("pausePreview")).toBe(false);
|
|
|
|
let resolveContentFingerprint;
|
|
const slowPreflightFrame = cameraWrapper.vm.frame("frame-a-new-content", "frame-a", "unused-content");
|
|
slowPreflightFrame.getContentFingerprint = vi.fn(
|
|
() =>
|
|
new Promise((resolve) => {
|
|
resolveContentFingerprint = resolve;
|
|
})
|
|
);
|
|
|
|
cameraWrapper.vm.$emit("update:frame", slowPreflightFrame);
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).toHaveBeenCalledTimes(1);
|
|
expect(cameraWrapper.props("pausePreview")).toBe(true);
|
|
|
|
resolveContentFingerprint("frame-a-new-content");
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).toHaveBeenCalledTimes(2);
|
|
expect(cameraWrapper.props("pausePreview")).toBe(true);
|
|
|
|
vi.advanceTimersByTime(2500);
|
|
await flushPromises();
|
|
expect(cameraWrapper.props("pausePreview")).toBe(false);
|
|
});
|
|
|
|
it("keeps the latest camera image without posting LPR requests while attachment view is active", async () => {
|
|
mocks.attachmentView.value = true;
|
|
mocks.request.mockResolvedValue({ data: { success: false } });
|
|
|
|
const wrapper = mountWithApp(PosDepartmentStepMobile1);
|
|
const cameraWrapper = wrapper.findComponent({ name: "ScannerCamera" });
|
|
|
|
expect(cameraWrapper.props("captureMode")).toBe("preview");
|
|
await wrapper.get('[data-testid="camera-frame-a"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).not.toHaveBeenCalled();
|
|
expect(mocks.cameraSetLatestImageBlob).toHaveBeenCalledTimes(1);
|
|
const blob = mocks.cameraSetLatestImageBlob.mock.calls[0][0];
|
|
expect(blob).toBeInstanceOf(Blob);
|
|
await expect(blob.text()).resolves.toBe("frame-a");
|
|
|
|
mocks.attachmentView.value = false;
|
|
await flushPromises();
|
|
expect(cameraWrapper.props("captureMode")).toBe("lpr");
|
|
});
|
|
|
|
it("disables fixed bottom backdrop blur in the live LPR scanner view", async () => {
|
|
const wrapper = mountWithApp(PosDepartmentStepMobile1);
|
|
|
|
expect(wrapper.get('[data-testid="fixed-bottom-control"]').attributes("data-use-backdrop-blur")).toBe("false");
|
|
|
|
mocks.attachmentView.value = true;
|
|
await flushPromises();
|
|
|
|
expect(wrapper.get('[data-testid="fixed-bottom-control"]').attributes("data-use-backdrop-blur")).toBe("true");
|
|
});
|
|
|
|
it("passes the scanner outline viewport rect provider to the camera", async () => {
|
|
const wrapper = mountWithApp(PosDepartmentStepMobile1);
|
|
const cameraWrapper = wrapper.findComponent({ name: "ScannerCamera" });
|
|
const focusTarget = wrapper.get(".scanner-focus-target").element;
|
|
vi.spyOn(focusTarget, "getBoundingClientRect").mockReturnValue({
|
|
bottom: 439,
|
|
height: 234,
|
|
left: 78,
|
|
right: 312,
|
|
toJSON: () => ({}),
|
|
top: 205,
|
|
width: 234,
|
|
x: 78,
|
|
y: 205,
|
|
});
|
|
|
|
expect(cameraWrapper.props("getFocusViewportRect")()).toEqual({
|
|
height: 234,
|
|
width: 234,
|
|
x: 78,
|
|
y: 205,
|
|
});
|
|
|
|
mocks.attachmentView.value = true;
|
|
await flushPromises();
|
|
|
|
expect(cameraWrapper.props("getFocusViewportRect")()).toBeNull();
|
|
});
|
|
|
|
it("does not capture or post LPR frames once all registration slots are filled", async () => {
|
|
mocks.vehicle1.value = { reg: "AB12345" };
|
|
mocks.vehicle2.value = { reg: "CD67890" };
|
|
mocks.vehicle3.value = { reg: "EF54321" };
|
|
mocks.request.mockResolvedValue({ data: { success: false } });
|
|
|
|
const wrapper = mountWithApp(PosDepartmentStepMobile1);
|
|
const cameraWrapper = wrapper.findComponent({ name: "ScannerCamera" });
|
|
|
|
await wrapper.get('[data-testid="camera-frame-a"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).not.toHaveBeenCalled();
|
|
expect(mocks.cameraSetLatestImageBlob).not.toHaveBeenCalled();
|
|
expect(cameraWrapper.props("pausePreview")).toBe(true);
|
|
});
|
|
|
|
it("does not capture or post LPR frames while the active registration slot is already filled", async () => {
|
|
mocks.vehicle1.value = { reg: "AB12345" };
|
|
mocks.request.mockResolvedValue({ data: { success: false } });
|
|
|
|
const wrapper = mountWithApp(PosDepartmentStepMobile1);
|
|
const cameraWrapper = wrapper.findComponent({ name: "ScannerCamera" });
|
|
|
|
await wrapper.get('[data-testid="camera-frame-a"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).not.toHaveBeenCalled();
|
|
expect(mocks.cameraSetLatestImageBlob).not.toHaveBeenCalled();
|
|
expect(cameraWrapper.props("pausePreview")).toBe(true);
|
|
});
|
|
|
|
it("resumes camera LPR capture after the active registration slot moves to an empty slot", async () => {
|
|
mocks.vehicle1.value = { reg: "AB12345" };
|
|
mocks.activeVehicleIndex.value = 2;
|
|
mocks.request.mockResolvedValue({ data: { success: false } });
|
|
|
|
const wrapper = mountWithApp(PosDepartmentStepMobile1);
|
|
|
|
await wrapper.get('[data-testid="camera-frame-a"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).toHaveBeenCalledTimes(1);
|
|
expect(mocks.cameraSetLatestImageBlob).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("pauses camera LPR capture briefly after no-plate responses", async () => {
|
|
vi.useFakeTimers();
|
|
mocks.request.mockResolvedValue({ data: { success: false } });
|
|
|
|
const wrapper = mountWithApp(PosDepartmentStepMobile1);
|
|
const cameraWrapper = wrapper.findComponent({ name: "ScannerCamera" });
|
|
|
|
await wrapper.get('[data-testid="camera-frame-a"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).toHaveBeenCalledTimes(1);
|
|
expect(mocks.cameraSetLatestImageBlob).not.toHaveBeenCalled();
|
|
expect(cameraWrapper.props("pausePreview")).toBe(true);
|
|
|
|
await wrapper.get('[data-testid="camera-frame-b"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).toHaveBeenCalledTimes(1);
|
|
expect(mocks.cameraSetLatestImageBlob).not.toHaveBeenCalled();
|
|
|
|
vi.advanceTimersByTime(1499);
|
|
await flushPromises();
|
|
await wrapper.get('[data-testid="camera-frame-c"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).toHaveBeenCalledTimes(1);
|
|
|
|
vi.advanceTimersByTime(1);
|
|
await flushPromises();
|
|
expect(cameraWrapper.props("pausePreview")).toBe(false);
|
|
await wrapper.get('[data-testid="camera-frame-c"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).toHaveBeenCalledTimes(2);
|
|
expect(mocks.cameraSetLatestImageBlob).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("pauses camera LPR capture briefly after scanner request failures", async () => {
|
|
vi.useFakeTimers();
|
|
mocks.request.mockRejectedValue(new Error("temporary scanner failure"));
|
|
|
|
const wrapper = mountWithApp(PosDepartmentStepMobile1);
|
|
const cameraWrapper = wrapper.findComponent({ name: "ScannerCamera" });
|
|
|
|
await wrapper.get('[data-testid="camera-frame-a"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).toHaveBeenCalledTimes(1);
|
|
expect(mocks.cameraSetLatestImageBlob).not.toHaveBeenCalled();
|
|
expect(cameraWrapper.props("pausePreview")).toBe(true);
|
|
|
|
await wrapper.get('[data-testid="camera-frame-b"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).toHaveBeenCalledTimes(1);
|
|
expect(mocks.cameraSetLatestImageBlob).not.toHaveBeenCalled();
|
|
|
|
vi.advanceTimersByTime(1499);
|
|
await flushPromises();
|
|
await wrapper.get('[data-testid="camera-frame-c"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).toHaveBeenCalledTimes(1);
|
|
|
|
vi.advanceTimersByTime(1);
|
|
await flushPromises();
|
|
expect(cameraWrapper.props("pausePreview")).toBe(false);
|
|
await wrapper.get('[data-testid="camera-frame-c"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).toHaveBeenCalledTimes(2);
|
|
expect(mocks.cameraSetLatestImageBlob).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("uses a failed scanner request frame to suppress unchanged scenes during backoff", async () => {
|
|
vi.useFakeTimers();
|
|
mocks.request.mockRejectedValue(new Error("temporary scanner failure"));
|
|
|
|
const wrapper = mountWithApp(PosDepartmentStepMobile1);
|
|
const cameraWrapper = wrapper.findComponent({ name: "ScannerCamera" });
|
|
const failedFrame = cameraWrapper.vm.frameWithLazyVisual(
|
|
"failed-empty-frame",
|
|
"300x159:101",
|
|
"bytes-a",
|
|
"00000000000000ff"
|
|
);
|
|
|
|
cameraWrapper.vm.$emit("update:frame", failedFrame);
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).toHaveBeenCalledTimes(1);
|
|
expect(failedFrame.getVisualFingerprint).toHaveBeenCalledTimes(1);
|
|
expect(cameraWrapper.props("captureEnabled")).toBe(true);
|
|
expect(cameraWrapper.props("captureIntervalMs")).toBe(700);
|
|
expect(cameraWrapper.props("pausePreview")).toBe(false);
|
|
expect(cameraWrapper.vm.shouldEncodeCandidate("00000000000000f0")).toBe(false);
|
|
|
|
await flushPromises();
|
|
expect(mocks.request).toHaveBeenCalledTimes(1);
|
|
|
|
expect(cameraWrapper.vm.shouldEncodeCandidate("ffffffffffffffff")).toBe(true);
|
|
});
|
|
|
|
it("pauses camera LPR capture after successful scans before encoding another frame", async () => {
|
|
vi.useFakeTimers();
|
|
mocks.vehicle2.value = { reg: "AB12345" };
|
|
mocks.request.mockResolvedValue({
|
|
data: {
|
|
data: {
|
|
license_plate_number: "AB12345",
|
|
success: true,
|
|
},
|
|
success: true,
|
|
},
|
|
});
|
|
|
|
const wrapper = mountWithApp(PosDepartmentStepMobile1);
|
|
const cameraWrapper = wrapper.findComponent({ name: "ScannerCamera" });
|
|
|
|
await wrapper.get('[data-testid="camera-frame-a"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).toHaveBeenCalledTimes(1);
|
|
expect(mocks.cameraSetLastSuccess).toHaveBeenCalledTimes(1);
|
|
expect(mocks.cameraSetLatestImageBlob).toHaveBeenCalledTimes(1);
|
|
expect(mocks.vehicleSelect).not.toHaveBeenCalled();
|
|
expect(cameraWrapper.props("pausePreview")).toBe(true);
|
|
|
|
await wrapper.get('[data-testid="camera-frame-b"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).toHaveBeenCalledTimes(1);
|
|
expect(mocks.cameraSetLatestImageBlob).toHaveBeenCalledTimes(1);
|
|
|
|
vi.advanceTimersByTime(1004);
|
|
await flushPromises();
|
|
await wrapper.get('[data-testid="camera-frame-c"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).toHaveBeenCalledTimes(1);
|
|
|
|
vi.advanceTimersByTime(1);
|
|
await flushPromises();
|
|
expect(cameraWrapper.props("pausePreview")).toBe(false);
|
|
await wrapper.get('[data-testid="camera-frame-c"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).toHaveBeenCalledTimes(2);
|
|
expect(mocks.cameraSetLatestImageBlob).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("clears no-plate capture backoff when the active registration slot changes", async () => {
|
|
vi.useFakeTimers();
|
|
mocks.request.mockResolvedValue({ data: { success: false } });
|
|
|
|
const wrapper = mountWithApp(PosDepartmentStepMobile1);
|
|
|
|
await wrapper.get('[data-testid="camera-frame-a"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).toHaveBeenCalledTimes(1);
|
|
|
|
mocks.activeVehicleIndex.value = 2;
|
|
await flushPromises();
|
|
await wrapper.get('[data-testid="camera-frame-b"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("aborts stale LPR requests when the active registration slot changes", async () => {
|
|
mocks.request.mockImplementation(
|
|
(_url, _method, _payload, _catchCallable, _thenCallable, options) =>
|
|
new Promise((_resolve, reject) => {
|
|
options.signal.addEventListener("abort", () => {
|
|
reject({ code: "ERR_CANCELED" });
|
|
});
|
|
})
|
|
);
|
|
|
|
const wrapper = mountWithApp(PosDepartmentStepMobile1);
|
|
|
|
await wrapper.get('[data-testid="camera-frame-a"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).toHaveBeenCalledTimes(1);
|
|
const requestOptions = mocks.request.mock.calls[0][5];
|
|
expect(requestOptions.signal.aborted).toBe(false);
|
|
|
|
mocks.activeVehicleIndex.value = 2;
|
|
await flushPromises();
|
|
|
|
expect(requestOptions.signal.aborted).toBe(true);
|
|
});
|
|
|
|
it("ignores successful LPR responses that resolve after the active registration slot changes", async () => {
|
|
let resolveRequest;
|
|
mocks.request.mockReturnValueOnce(
|
|
new Promise((resolve) => {
|
|
resolveRequest = resolve;
|
|
})
|
|
);
|
|
|
|
const wrapper = mountWithApp(PosDepartmentStepMobile1);
|
|
|
|
await wrapper.get('[data-testid="camera-frame-a"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).toHaveBeenCalledTimes(1);
|
|
const requestOptions = mocks.request.mock.calls[0][5];
|
|
|
|
mocks.activeVehicleIndex.value = 2;
|
|
await flushPromises();
|
|
expect(requestOptions.signal.aborted).toBe(true);
|
|
|
|
resolveRequest({
|
|
data: {
|
|
data: {
|
|
license_plate_number: "AB12345",
|
|
success: true,
|
|
},
|
|
success: true,
|
|
},
|
|
});
|
|
await flushPromises();
|
|
|
|
expect(mocks.cameraSetLastSuccess).not.toHaveBeenCalled();
|
|
expect(mocks.vehicleSelect).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("aborts active LPR requests when the scanner page becomes hidden", async () => {
|
|
mocks.request.mockImplementation(
|
|
(_url, _method, _payload, _catchCallable, _thenCallable, options) =>
|
|
new Promise((_resolve, reject) => {
|
|
options.signal.addEventListener("abort", () => {
|
|
reject({ code: "ERR_CANCELED" });
|
|
});
|
|
})
|
|
);
|
|
|
|
const wrapper = mountWithApp(PosDepartmentStepMobile1);
|
|
|
|
await wrapper.get('[data-testid="camera-frame-same-a"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).toHaveBeenCalledTimes(1);
|
|
const requestOptions = mocks.request.mock.calls[0][5];
|
|
expect(requestOptions.signal.aborted).toBe(false);
|
|
|
|
setDocumentVisibility("hidden");
|
|
document.dispatchEvent(new Event("visibilitychange"));
|
|
await flushPromises();
|
|
|
|
expect(requestOptions.signal.aborted).toBe(true);
|
|
|
|
setDocumentVisibility("visible");
|
|
document.dispatchEvent(new Event("visibilitychange"));
|
|
await flushPromises();
|
|
await wrapper.get('[data-testid="camera-frame-same-a-duplicate"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).toHaveBeenCalledTimes(2);
|
|
await expectRawLPRRequest(1, "same-a-copy");
|
|
});
|
|
|
|
it("uses lazy content fingerprints only when quick frame fingerprints match", async () => {
|
|
vi.useFakeTimers();
|
|
mocks.request.mockResolvedValue({ data: { success: false } });
|
|
|
|
const wrapper = mountWithApp(PosDepartmentStepMobile1);
|
|
|
|
await wrapper.get('[data-testid="camera-frame-same-a"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).toHaveBeenCalledTimes(1);
|
|
await expectRawLPRRequest(0, "same-a");
|
|
expect(mocks.cameraSetLatestImageBlob).not.toHaveBeenCalled();
|
|
|
|
vi.advanceTimersByTime(1500);
|
|
await flushPromises();
|
|
|
|
await wrapper.get('[data-testid="camera-frame-same-a-duplicate"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).toHaveBeenCalledTimes(1);
|
|
expect(mocks.cameraSetLatestImageBlob).not.toHaveBeenCalled();
|
|
|
|
await wrapper.get('[data-testid="camera-frame-same-b"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).toHaveBeenCalledTimes(1);
|
|
expect(mocks.cameraSetLatestImageBlob).not.toHaveBeenCalled();
|
|
|
|
vi.advanceTimersByTime(699);
|
|
await flushPromises();
|
|
|
|
await wrapper.get('[data-testid="camera-frame-same-b"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).toHaveBeenCalledTimes(1);
|
|
|
|
vi.advanceTimersByTime(1);
|
|
await flushPromises();
|
|
|
|
await wrapper.get('[data-testid="camera-frame-same-b"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).toHaveBeenCalledTimes(2);
|
|
await expectRawLPRRequest(1, "same-b");
|
|
expect(mocks.cameraSetLatestImageBlob).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("skips visually similar no-plate frames even when the encoded blob changes", async () => {
|
|
vi.useFakeTimers();
|
|
mocks.request.mockResolvedValue({ data: { success: false } });
|
|
|
|
const wrapper = mountWithApp(PosDepartmentStepMobile1);
|
|
const cameraWrapper = wrapper.findComponent({ name: "ScannerCamera" });
|
|
|
|
cameraWrapper.vm.$emit(
|
|
"update:frame",
|
|
cameraWrapper.vm.frame("empty-frame-a", "576x576:101", "bytes-a", "00000000000000ff")
|
|
);
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).toHaveBeenCalledTimes(1);
|
|
expect(mocks.cameraSetLatestImageBlob).not.toHaveBeenCalled();
|
|
|
|
vi.advanceTimersByTime(1500);
|
|
await flushPromises();
|
|
|
|
cameraWrapper.vm.$emit(
|
|
"update:frame",
|
|
cameraWrapper.vm.frame("empty-frame-b", "576x576:117", "bytes-b", "00000000000000f0")
|
|
);
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).toHaveBeenCalledTimes(1);
|
|
expect(mocks.cameraSetLatestImageBlob).not.toHaveBeenCalled();
|
|
|
|
vi.advanceTimersByTime(699);
|
|
await flushPromises();
|
|
|
|
cameraWrapper.vm.$emit(
|
|
"update:frame",
|
|
cameraWrapper.vm.frame("changed-frame-too-soon", "576x576:118", "bytes-c", "ffffffffffffffff")
|
|
);
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).toHaveBeenCalledTimes(1);
|
|
|
|
vi.advanceTimersByTime(1);
|
|
await flushPromises();
|
|
|
|
cameraWrapper.vm.$emit(
|
|
"update:frame",
|
|
cameraWrapper.vm.frame("changed-frame", "576x576:118", "bytes-c", "ffffffffffffffff")
|
|
);
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).toHaveBeenCalledTimes(2);
|
|
expect(mocks.cameraSetLatestImageBlob).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("skips visually similar no-plate frames before JPEG encoding", async () => {
|
|
vi.useFakeTimers();
|
|
mocks.request.mockResolvedValue({ data: { success: false } });
|
|
|
|
const wrapper = mountWithApp(PosDepartmentStepMobile1);
|
|
const cameraWrapper = wrapper.findComponent({ name: "ScannerCamera" });
|
|
|
|
cameraWrapper.vm.$emit(
|
|
"update:frame",
|
|
cameraWrapper.vm.frame("empty-frame-a", "576x576:101", "bytes-a", "00000000000000ff")
|
|
);
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).toHaveBeenCalledTimes(1);
|
|
expect(mocks.cameraSetLatestImageBlob).not.toHaveBeenCalled();
|
|
|
|
vi.advanceTimersByTime(1500);
|
|
await flushPromises();
|
|
|
|
expect(cameraWrapper.vm.shouldEncodeCandidate("00000000000000f0")).toBe(false);
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).toHaveBeenCalledTimes(1);
|
|
expect(mocks.cameraSetLatestImageBlob).not.toHaveBeenCalled();
|
|
expect(cameraWrapper.props("captureEnabled")).toBe(false);
|
|
expect(cameraWrapper.props("pausePreview")).toBe(true);
|
|
|
|
vi.advanceTimersByTime(699);
|
|
await flushPromises();
|
|
|
|
expect(cameraWrapper.vm.shouldEncodeCandidate("ffffffffffffffff")).toBe(false);
|
|
|
|
vi.advanceTimersByTime(1);
|
|
await flushPromises();
|
|
expect(cameraWrapper.props("pausePreview")).toBe(false);
|
|
|
|
expect(cameraWrapper.vm.shouldEncodeCandidate("ffffffffffffffff")).toBe(true);
|
|
});
|
|
|
|
it("defers visual fingerprint work until a sent frame misses", async () => {
|
|
vi.useFakeTimers();
|
|
mocks.request.mockResolvedValue({ data: { success: false } });
|
|
|
|
const wrapper = mountWithApp(PosDepartmentStepMobile1);
|
|
const cameraWrapper = wrapper.findComponent({ name: "ScannerCamera" });
|
|
|
|
expect(cameraWrapper.vm.shouldBuildVisualFingerprintNow()).toBe(false);
|
|
|
|
const lazyFrame = cameraWrapper.vm.frameWithLazyVisual(
|
|
"empty-frame-a",
|
|
"300x225:101",
|
|
"bytes-a",
|
|
"00000000000000ff"
|
|
);
|
|
cameraWrapper.vm.$emit("update:frame", lazyFrame);
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).toHaveBeenCalledTimes(1);
|
|
expect(lazyFrame.getVisualFingerprint).toHaveBeenCalledTimes(1);
|
|
expect(cameraWrapper.vm.shouldBuildVisualFingerprintNow()).toBe(true);
|
|
expect(cameraWrapper.props("captureIntervalMs")).toBe(700);
|
|
expect(cameraWrapper.props("pausePreview")).toBe(false);
|
|
|
|
vi.advanceTimersByTime(1500);
|
|
await flushPromises();
|
|
|
|
expect(cameraWrapper.vm.shouldEncodeCandidate("00000000000000f0")).toBe(false);
|
|
});
|
|
|
|
it("uses visual changes to resume LPR during no-plate backoff without polling the same scene", async () => {
|
|
vi.useFakeTimers();
|
|
mocks.request.mockResolvedValue({ data: { success: false } });
|
|
|
|
const wrapper = mountWithApp(PosDepartmentStepMobile1);
|
|
const cameraWrapper = wrapper.findComponent({ name: "ScannerCamera" });
|
|
|
|
const emptyFrame = cameraWrapper.vm.frameWithLazyVisual(
|
|
"empty-frame-a",
|
|
"300x225:101",
|
|
"bytes-a",
|
|
"00000000000000ff"
|
|
);
|
|
cameraWrapper.vm.$emit("update:frame", emptyFrame);
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).toHaveBeenCalledTimes(1);
|
|
expect(emptyFrame.getVisualFingerprint).toHaveBeenCalledTimes(1);
|
|
expect(cameraWrapper.props("captureEnabled")).toBe(true);
|
|
expect(cameraWrapper.props("captureIntervalMs")).toBe(700);
|
|
expect(cameraWrapper.props("pausePreview")).toBe(false);
|
|
expect(cameraWrapper.vm.shouldEncodeCandidate("00000000000000f0")).toBe(false);
|
|
await flushPromises();
|
|
expect(mocks.request).toHaveBeenCalledTimes(1);
|
|
|
|
expect(cameraWrapper.vm.shouldEncodeCandidate("ffffffffffffffff")).toBe(true);
|
|
await flushPromises();
|
|
expect(cameraWrapper.props("captureIntervalMs")).toBeNull();
|
|
cameraWrapper.vm.$emit(
|
|
"update:frame",
|
|
cameraWrapper.vm.frame("plate-frame", "300x225:202", "bytes-b", "ffffffffffffffff")
|
|
);
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).toHaveBeenCalledTimes(2);
|
|
await expectRawLPRRequest(1, "plate-frame");
|
|
});
|
|
|
|
it("clears duplicate frame suppression when the active registration slot changes", async () => {
|
|
vi.useFakeTimers();
|
|
mocks.request.mockResolvedValue({ data: { success: false } });
|
|
|
|
const wrapper = mountWithApp(PosDepartmentStepMobile1);
|
|
|
|
await wrapper.get('[data-testid="camera-frame-same-a"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).toHaveBeenCalledTimes(1);
|
|
|
|
vi.advanceTimersByTime(1500);
|
|
await flushPromises();
|
|
|
|
mocks.activeVehicleIndex.value = 2;
|
|
await flushPromises();
|
|
|
|
await wrapper.get('[data-testid="camera-frame-same-a-duplicate"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).toHaveBeenCalledTimes(2);
|
|
await expectRawLPRRequest(1, "same-a-copy");
|
|
});
|
|
|
|
it("passes an abort signal to LPR requests and cancels stale requests when attachment view opens", async () => {
|
|
mocks.request.mockImplementation(
|
|
(_url, _method, _payload, _catchCallable, _thenCallable, options) =>
|
|
new Promise((_resolve, reject) => {
|
|
options.signal.addEventListener("abort", () => {
|
|
reject({ code: "ERR_CANCELED" });
|
|
});
|
|
})
|
|
);
|
|
|
|
const wrapper = mountWithApp(PosDepartmentStepMobile1);
|
|
|
|
await wrapper.get('[data-testid="camera-frame-a"]').trigger("click");
|
|
await flushPromises();
|
|
|
|
expect(mocks.request).toHaveBeenCalledTimes(1);
|
|
const requestOptions = mocks.request.mock.calls[0][5];
|
|
expect(requestOptions.signal).toBeInstanceOf(AbortSignal);
|
|
expect(requestOptions.signal.aborted).toBe(false);
|
|
|
|
mocks.attachmentView.value = true;
|
|
await flushPromises();
|
|
|
|
expect(requestOptions.signal.aborted).toBe(true);
|
|
});
|
|
});
|