120 lines
5.1 KiB
TypeScript
120 lines
5.1 KiB
TypeScript
import { StrictMode } from "react";
|
|
import { renderToString } from "react-dom/server";
|
|
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import ScanPage from "./page";
|
|
import * as qrDetect from "@/lib/scan/qr-detect";
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe("ScanPage", () => {
|
|
it("keeps the latest scanned note when an earlier lookup finishes late", async () => {
|
|
vi.stubGlobal("isSecureContext", true);
|
|
vi.stubGlobal("navigator", {
|
|
mediaDevices: { getUserMedia: vi.fn().mockResolvedValue({ getTracks: () => [] }) },
|
|
});
|
|
vi.spyOn(HTMLMediaElement.prototype, "play").mockResolvedValue();
|
|
let onScan: ((raw: string) => void) | undefined;
|
|
vi.spyOn(qrDetect, "scanVideoFrames").mockImplementation((_video, _canvas, onFound) => {
|
|
onScan = onFound;
|
|
return new Promise(() => {});
|
|
});
|
|
const pending = new Map<string, (response: Response) => void>();
|
|
vi.stubGlobal("fetch", (path: string) => new Promise<Response>((resolve) => {
|
|
pending.set(new URL(path, "http://localhost").searchParams.get("number")!, resolve);
|
|
}));
|
|
const resolved = (number: string) => new Response(JSON.stringify({
|
|
id: number,
|
|
delivery_note_number: number,
|
|
status: "draft",
|
|
customer_name: "Customer",
|
|
company_name: "Company",
|
|
customer_po: "",
|
|
delivery_date: "",
|
|
signed_copy: "",
|
|
item_count: 1,
|
|
}), { status: 200 });
|
|
render(<ScanPage />);
|
|
fireEvent.click(screen.getByRole("button", { name: "Start camera" }));
|
|
await waitFor(() => expect(onScan).toBeDefined());
|
|
act(() => onScan!("DN26100401"));
|
|
act(() => onScan!("DN26100402"));
|
|
expect(screen.getByRole("status")).toHaveTextContent("Delivery note recognised");
|
|
|
|
await act(async () => pending.get("DN26100402")!(resolved("DN26100402")));
|
|
expect(screen.getByTestId("scan-note")).toHaveTextContent("DN26100402");
|
|
expect(screen.queryByRole("status")).not.toBeInTheDocument();
|
|
await act(async () => pending.get("DN26100401")!(resolved("DN26100401")));
|
|
expect(screen.getByTestId("scan-note")).toHaveTextContent("DN26100402");
|
|
expect(screen.getByTestId("scan-note")).not.toHaveTextContent("DN26100401");
|
|
});
|
|
|
|
it("requests a sharp rear-camera feed instead of relying on low-resolution defaults", async () => {
|
|
vi.stubGlobal("isSecureContext", true);
|
|
const getUserMedia = vi.fn().mockRejectedValue(new Error("Camera unavailable in test"));
|
|
vi.stubGlobal("navigator", { mediaDevices: { getUserMedia } });
|
|
render(<ScanPage />);
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Start camera" }));
|
|
|
|
await waitFor(() => expect(screen.getByTestId("scan-toast")).toHaveTextContent("Camera unavailable in test"));
|
|
const constraints = getUserMedia.mock.calls[0][0];
|
|
expect(constraints.video.facingMode).toEqual({ ideal: "environment" });
|
|
expect(constraints.video.width.ideal).toBeGreaterThanOrEqual(1920);
|
|
expect(constraints.video.height.ideal).toBeGreaterThanOrEqual(1080);
|
|
expect(constraints.audio).toBe(false);
|
|
});
|
|
|
|
it("renders camera controls without a render loop and remains usable after typing", () => {
|
|
vi.stubGlobal("isSecureContext", true);
|
|
vi.stubGlobal("navigator", {
|
|
mediaDevices: { getUserMedia: vi.fn() },
|
|
});
|
|
|
|
render(<StrictMode><ScanPage /></StrictMode>);
|
|
|
|
expect(screen.getByRole("button", { name: "Start camera" })).toBeEnabled();
|
|
expect(screen.getByRole("button", { name: "Go" })).toBeDisabled();
|
|
|
|
fireEvent.change(screen.getByLabelText("Or type the delivery note number"), {
|
|
target: { value: "dn26100401" },
|
|
});
|
|
|
|
expect(screen.getByLabelText("Or type the delivery note number")).toHaveValue("DN26100401");
|
|
expect(screen.getByRole("button", { name: "Go" })).toBeEnabled();
|
|
expect(screen.getByRole("button", { name: "Start camera" })).toBeEnabled();
|
|
});
|
|
|
|
it("keeps manual entry available on an insecure address without a camera", () => {
|
|
vi.stubGlobal("isSecureContext", false);
|
|
vi.stubGlobal("navigator", {});
|
|
|
|
render(<ScanPage />);
|
|
|
|
expect(screen.getByTestId("scan-insecure-warning")).toBeInTheDocument();
|
|
expect(screen.getByRole("button", { name: "Start camera" })).toBeDisabled();
|
|
fireEvent.change(screen.getByLabelText("Or type the delivery note number"), {
|
|
target: { value: "DN26100401" },
|
|
});
|
|
expect(screen.getByRole("button", { name: "Go" })).toBeEnabled();
|
|
});
|
|
|
|
it("hydrates the server page and enables the browser's camera controls", () => {
|
|
vi.stubGlobal("isSecureContext", true);
|
|
vi.stubGlobal("navigator", {
|
|
mediaDevices: { getUserMedia: vi.fn() },
|
|
});
|
|
const container = document.createElement("div");
|
|
container.innerHTML = renderToString(<ScanPage />);
|
|
document.body.appendChild(container);
|
|
expect(container.querySelector("button")).toBeDisabled();
|
|
|
|
render(<ScanPage />, { container, hydrate: true });
|
|
|
|
expect(screen.getByRole("button", { name: "Start camera" })).toBeEnabled();
|
|
expect(screen.getByLabelText("Or type the delivery note number")).toBeInTheDocument();
|
|
});
|
|
});
|