// @vitest-environment jsdom import { flushPromises } from "@vue/test-utils"; import { beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ fetchSelfServeData: vi.fn(), syncVehicleAnswer: vi.fn(), clearVehicleAnswers: vi.fn(), downloadAttachment: vi.fn(), swalFire: vi.fn(), getLanes: vi.fn(), getVehicleTypeOptions: vi.fn(), loading: { value: false, __v_isRef: true }, lane: { value: { id: 3, name: "Lane 3" }, __v_isRef: true }, machineType: { value: null, __v_isRef: true }, session: { value: null, __v_isRef: true }, events: { value: [], __v_isRef: true }, questions: { value: [], __v_isRef: true }, answers: { value: {}, __v_isRef: true }, allowedServices: { value: [], __v_isRef: true }, configVersionId: { value: null, __v_isRef: true }, evaluationTrace: { value: null, __v_isRef: true }, visibleQuestions: { value: [], __v_isRef: true }, activeTasks: { value: [], __v_isRef: true }, currentQuestion: { value: null, __v_isRef: true }, allVisibleQuestionsAnswered: { value: false, __v_isRef: true }, machineAvailable: { value: true, __v_isRef: true }, allowed: { value: true, __v_isRef: true }, evaluateCondition: vi.fn(() => true), })); vi.mock("@/components/session/token/SessionUser.vue", () => ({ SessionUser: { user: { customer_number: { value: 12345 }, }, objects: { department_lanes: { get: { all: mocks.getLanes, }, }, vehicles: { columns: { type: { options: mocks.getVehicleTypeOptions, }, }, }, }, }, })); vi.mock("sweetalert2", () => ({ default: { fire: mocks.swalFire, }, })); vi.mock("@/composables/useSelfServeLogic", () => ({ useSelfServeLogic: () => ({ loading: mocks.loading, lane: mocks.lane, machineType: mocks.machineType, session: mocks.session, events: mocks.events, questions: mocks.questions, answers: mocks.answers, allowedServices: mocks.allowedServices, configVersionId: mocks.configVersionId, evaluationTrace: mocks.evaluationTrace, visibleQuestions: mocks.visibleQuestions, activeTasks: mocks.activeTasks, currentQuestion: mocks.currentQuestion, allVisibleQuestionsAnswered: mocks.allVisibleQuestionsAnswered, machineAvailable: mocks.machineAvailable, allowed: mocks.allowed, fetchSelfServeData: mocks.fetchSelfServeData, syncVehicleAnswer: mocks.syncVehicleAnswer, clearVehicleAnswers: mocks.clearVehicleAnswers, evaluateCondition: mocks.evaluateCondition, downloadAttachment: mocks.downloadAttachment, }), })); import { mountWithApp } from "./helpers/mountWithApp.js"; import SelfServeTryModal from "@/components/displays/department/tables/SelfServeTryModal.vue"; describe("SelfServeTryModal", () => { beforeEach(() => { mocks.fetchSelfServeData.mockReset(); mocks.syncVehicleAnswer.mockReset(); mocks.clearVehicleAnswers.mockReset(); mocks.downloadAttachment.mockReset(); mocks.swalFire.mockReset(); mocks.getLanes.mockReset(); mocks.getVehicleTypeOptions.mockReset(); mocks.evaluateCondition.mockReset(); mocks.getLanes.mockResolvedValue([{ id: 3, department: 9, name: "Lane 3" }]); mocks.getVehicleTypeOptions.mockResolvedValue([ { id: 0, name: "Ukendt" }, { id: 2, name: "Truck" }, { id: 3, name: "Van" }, ]); mocks.clearVehicleAnswers.mockResolvedValue({ deletedCount: 1 }); mocks.swalFire.mockResolvedValue({ isConfirmed: true }); mocks.loading.value = false; mocks.questions.value = []; mocks.visibleQuestions.value = []; mocks.answers.value = {}; mocks.activeTasks.value = []; mocks.currentQuestion.value = null; mocks.evaluateCondition.mockReturnValue(true); }); const mountModal = () => mountWithApp(SelfServeTryModal, { props: { departmentId: 9, laneId: 3, }, global: { stubs: { SelfServeQuestionCards: true, SelfServeTaskList: true, }, }, }); it("keeps clear answers enabled for valid lane/reg even when no visible answers exist", async () => { const wrapper = mountModal(); await flushPromises(); const clearButton = wrapper.get('[data-testid="self-serve-try-clear-answers"]'); expect(clearButton.attributes("disabled")).toBeUndefined(); }); it("allows selecting vehicle type and refreshes preview with the selected type", async () => { const wrapper = mountModal(); await flushPromises(); mocks.fetchSelfServeData.mockClear(); await wrapper.get('[data-testid="self-serve-try-vehicle-type"]').setValue("3"); await flushPromises(); expect(mocks.fetchSelfServeData).toHaveBeenCalledWith(9, 3, 3, "AB12345"); }); it("passes simulator task attachment downloads to self-serve logic", async () => { const attachment = { id: 201, content: { other: "manual.pdf" }, download_link: "https://cdn.example.test/manual.pdf", }; mocks.activeTasks.value = [ { id: 5, task: "Prepare", attachments: [attachment], }, ]; const wrapper = mountWithApp(SelfServeTryModal, { props: { departmentId: 9, laneId: 3, }, global: { stubs: { SelfServeQuestionCards: true, SelfServeTaskList: { props: ["tasks"], emits: ["download-attachment"], template: "", }, }, }, }); await flushPromises(); await wrapper.get("[data-testid='emit-task-download']").trigger("click"); expect(mocks.downloadAttachment).toHaveBeenCalledWith(5, 201, attachment); }); it("syncs answers with the selected vehicle type context", async () => { mocks.visibleQuestions.value = [{ id: 77, question: "Question 77" }]; mocks.currentQuestion.value = null; mocks.syncVehicleAnswer.mockResolvedValue({}); const wrapper = mountWithApp(SelfServeTryModal, { props: { departmentId: 9, laneId: 3, }, global: { stubs: { SelfServeQuestionCards: { props: ["visibleQuestions"], emits: ["answer-question"], template: "", }, SelfServeTaskList: true, }, }, }); await flushPromises(); await wrapper.get('[data-testid="self-serve-try-vehicle-type"]').setValue("3"); await flushPromises(); await wrapper.get("[data-testid='emit-card-answer']").trigger("click"); await flushPromises(); expect(mocks.syncVehicleAnswer).toHaveBeenCalledWith({ departmentId: 9, laneId: 3, customerNumber: 12345, reg: "AB12345", questionId: 77, value: true, vehicleTypeId: 3, }); }); it("renders dynamic image preview with expected query parameters when context exists", async () => { mocks.activeTasks.value = [ { id: 1, buttons: ["reset", 0, 2, 2, "start", -1, 99], dynamic_images_vehicle_type: 5, }, ]; const wrapper = mountWithApp(SelfServeTryModal, { props: { departmentId: 9, laneId: 3, vehicleTypeId: 3, }, global: { stubs: { SelfServeQuestionCards: true, SelfServeTaskList: true, }, }, }); await flushPromises(); const image = wrapper.get('[data-testid="self-serve-try-dynamic-image"]'); const imageUrl = new URL(image.attributes("src"), "http://localhost"); expect(imageUrl.pathname).toMatch(/\/department\/lanes\/dynamic-image$/); expect(imageUrl.searchParams.get("department")).toBe("9"); expect(imageUrl.searchParams.get("lane")).toBe("3"); expect(imageUrl.searchParams.get("current_step")).toBe("0"); expect(imageUrl.searchParams.get("vehicle_type")).toBe("3"); expect(imageUrl.searchParams.get("thumb_position")).toBe("5"); expect(JSON.parse(imageUrl.searchParams.get("buttons"))).toEqual(["reset", 0, 2, "start"]); }); it("does not render dynamic image preview without dynamic-image context", async () => { mocks.activeTasks.value = [ { id: 1, buttons: [-1, 12, 99], dynamic_images_vehicle_type: null, }, ]; const wrapper = mountModal(); await flushPromises(); expect(wrapper.find('[data-testid="self-serve-try-dynamic-image"]').exists()).toBe(false); }); it("hides dynamic image on error and restores it when image context changes", async () => { mocks.activeTasks.value = [ { id: 1, buttons: [1], dynamic_images_vehicle_type: null, }, ]; const wrapper = mountModal(); await flushPromises(); await wrapper.get('[data-testid="self-serve-try-dynamic-image"]').trigger("error"); await flushPromises(); expect(wrapper.find('[data-testid="self-serve-try-dynamic-image"]').exists()).toBe(false); await wrapper.get('[data-testid="self-serve-try-vehicle-type"]').setValue("3"); await flushPromises(); const image = wrapper.get('[data-testid="self-serve-try-dynamic-image"]'); const imageUrl = new URL(image.attributes("src"), "http://localhost"); expect(imageUrl.searchParams.get("vehicle_type")).toBe("3"); expect(JSON.parse(imageUrl.searchParams.get("buttons"))).toEqual([1]); }); it("does not duplicate the current question in the question cards list", async () => { mocks.currentQuestion.value = { id: 11, question: "Question 11" }; mocks.visibleQuestions.value = [ { id: 11, question: "Question 11" }, { id: 22, question: "Question 22" }, ]; const wrapper = mountWithApp(SelfServeTryModal, { props: { departmentId: 9, laneId: 3, }, global: { stubs: { SelfServeQuestionCards: { props: ["visibleQuestions"], template: "
{{ visibleQuestions.map(q => q.question).join('|') }}
", }, SelfServeTaskList: true, }, }, }); await flushPromises(); expect(wrapper.find(".notification.is-info").exists()).toBe(false); expect(wrapper.get("[data-testid='question-cards-probe']").text()).toBe("Question 11|Question 22"); }); it("keeps rendering known questions when visible list is temporarily empty but unanswered questions still exist", async () => { mocks.visibleQuestions.value = []; mocks.questions.value = [ { id: 11, question: "Question 11", order_priority: 1 }, { id: 22, question: "Question 22", order_priority: 2 }, ]; mocks.answers.value = { 11: true, 22: null }; const wrapper = mountWithApp(SelfServeTryModal, { props: { departmentId: 9, laneId: 3, }, global: { stubs: { SelfServeQuestionCards: { props: ["visibleQuestions"], template: "
{{ visibleQuestions.map(q => q.question).join('|') }}
", }, SelfServeTaskList: true, }, }, }); await flushPromises(); expect(wrapper.get("[data-testid='question-cards-probe']").text()).toBe("Question 11|Question 22"); expect(wrapper.text()).toContain("Alle synlige sporgsmal besvaret: Nej"); }); it("keeps condition-true unanswered follow-up visible when server visible list is stale", async () => { mocks.visibleQuestions.value = [{ id: 11, question: "Question 11", order_priority: 1, condition_id: null }]; mocks.questions.value = [ { id: 11, question: "Question 11", order_priority: 1, condition_id: null }, { id: 12, question: "Follow-up question", order_priority: 2, condition_id: 100 }, ]; mocks.answers.value = { 11: true, 12: null }; mocks.evaluateCondition.mockImplementation((conditionId) => parseInt(conditionId) === 100); const wrapper = mountWithApp(SelfServeTryModal, { props: { departmentId: 9, laneId: 3, }, global: { stubs: { SelfServeQuestionCards: { props: ["visibleQuestions"], template: "
{{ visibleQuestions.map(q => q.question).join('|') }}
", }, SelfServeTaskList: true, }, }, }); await flushPromises(); expect(wrapper.get("[data-testid='question-cards-probe']").text()).toBe("Question 11|Follow-up question"); }); it("does not add follow-up questions when their condition is false", async () => { mocks.visibleQuestions.value = [{ id: 11, question: "Question 11", order_priority: 1, condition_id: null }]; mocks.questions.value = [ { id: 11, question: "Question 11", order_priority: 1, condition_id: null }, { id: 12, question: "Follow-up question", order_priority: 2, condition_id: 100 }, ]; mocks.answers.value = { 11: false, 12: null }; mocks.evaluateCondition.mockReturnValue(false); const wrapper = mountWithApp(SelfServeTryModal, { props: { departmentId: 9, laneId: 3, }, global: { stubs: { SelfServeQuestionCards: { props: ["visibleQuestions"], template: "
{{ visibleQuestions.map(q => q.question).join('|') }}
", }, SelfServeTaskList: true, }, }, }); await flushPromises(); expect(wrapper.get("[data-testid='question-cards-probe']").text()).toBe("Question 11"); }); it("disables clear answers when registration is invalid", async () => { const wrapper = mountModal(); await flushPromises(); await wrapper.get('[data-testid="self-serve-try-reg"]').setValue("A"); await flushPromises(); const clearButton = wrapper.get('[data-testid="self-serve-try-clear-answers"]'); expect(clearButton.attributes("disabled")).toBeDefined(); }); it("disables clear answers while loading", async () => { mocks.loading.value = true; const wrapper = mountModal(); await flushPromises(); const clearButton = wrapper.get('[data-testid="self-serve-try-clear-answers"]'); expect(clearButton.attributes("disabled")).toBeDefined(); }); it("does not clear answers when confirmation is cancelled", async () => { mocks.swalFire.mockResolvedValue({ isConfirmed: false }); const wrapper = mountModal(); await flushPromises(); await wrapper.get('[data-testid="self-serve-try-clear-answers"]').trigger("click"); expect(mocks.swalFire).toHaveBeenCalledTimes(1); expect(mocks.clearVehicleAnswers).not.toHaveBeenCalled(); }); it("clears answers for the selected lane and registration when confirmed", async () => { const wrapper = mountWithApp(SelfServeTryModal, { props: { departmentId: 9, laneId: 3, }, global: { stubs: { SelfServeQuestionCards: true, SelfServeTaskList: true, }, }, }); await flushPromises(); const clearButton = wrapper.get('[data-testid="self-serve-try-clear-answers"]'); expect(clearButton.attributes("disabled")).toBeUndefined(); await clearButton.trigger("click"); expect(mocks.swalFire).toHaveBeenCalledTimes(1); expect(mocks.swalFire.mock.calls[0][0]).toMatchObject({ title: "Ryd besvarelser?", icon: "warning", }); expect(mocks.swalFire.mock.calls[0][0].text).toContain("AB12345"); expect(mocks.swalFire.mock.calls[0][0].text).toContain("3"); expect(mocks.clearVehicleAnswers).toHaveBeenCalledWith({ departmentId: 9, laneId: 3, vehicleTypeId: null, reg: "AB12345", }); }); });