Files
pleno-vue/tests/unit/use-self-serve-logic.spec.js
T

996 lines
33 KiB
JavaScript

import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
previewAllowed: vi.fn(),
washSummary: vi.fn(),
add: vi.fn(),
getAll: vi.fn(),
deleteCondition: vi.fn(),
attachmentsList: vi.fn(),
attachmentsDownload: vi.fn(),
request: vi.fn(),
}));
vi.mock("@/components/session/token/SessionUser.vue", () => ({
SessionUser: {
request: mocks.request,
objects: {
self_serve_vehicle_conditions: {
get: {
previewAllowed: mocks.previewAllowed,
washSummary: mocks.washSummary,
all: mocks.getAll,
},
add: mocks.add,
delete: mocks.deleteCondition,
},
self_serve_tasks: {
attachments: {
list: mocks.attachmentsList,
download: mocks.attachmentsDownload,
},
},
},
},
}));
import { useSelfServeLogic } from "@/composables/useSelfServeLogic.js";
describe("useSelfServeLogic", () => {
beforeEach(() => {
mocks.previewAllowed.mockReset();
mocks.washSummary.mockReset();
mocks.add.mockReset();
mocks.getAll.mockReset();
mocks.deleteCondition.mockReset();
mocks.attachmentsList.mockReset();
mocks.attachmentsDownload.mockReset();
mocks.request.mockReset();
vi.unstubAllGlobals();
});
it("uses task attachments supplied by preview and summary data", async () => {
const taskAttachments = [
{ id: 201, content: { other: "manual.pdf" }, download_link: "https://cdn.example.test/manual.pdf" },
{ id: 202, content: { other: "photo.jpg" }, download_link: "https://cdn.example.test/photo.jpg" },
];
mocks.previewAllowed.mockResolvedValue({
allowed: true,
machine_available: true,
lane: { id: 7, name: "Lane 7" },
session: { id: 91, status: "IN_PROGRESS" },
questions: [{ id: 1, question: "Machine wash?", answer: null, order_priority: 1 }],
tasks: [{ id: 5, task: "Prepare", order_priority: 1, services: ["MACHINE"], attachments: taskAttachments }],
conditions: [],
rules: [],
allowed_services: ["MACHINE"],
});
mocks.washSummary.mockResolvedValue({
session: { id: 91, status: "IN_PROGRESS" },
lane: { id: 7, name: "Lane 7" },
questions: [{ id: 1, question: "Machine wash?", answer: true, order_priority: 1 }],
tasks: [{ id: 5, task: "Prepare", order_priority: 1, services: ["MACHINE"], attachments: taskAttachments }],
events: [{ id: 1, type: "STARTED", created_at: "2026-01-01T00:00:00.000Z" }],
});
const logic = useSelfServeLogic();
await logic.fetchSelfServeData(1, 9, 7, "ab12345");
expect(mocks.previewAllowed).toHaveBeenCalledWith(7, "AB12345", 9);
expect(mocks.washSummary).toHaveBeenCalledWith({ session_id: 91 });
expect(logic.allowed.value).toBe(true);
expect(logic.machineAvailable.value).toBe(true);
expect(logic.answers.value[1]).toBe(true);
expect(logic.events.value).toHaveLength(1);
expect(mocks.attachmentsList).not.toHaveBeenCalled();
expect(mocks.attachmentsDownload).not.toHaveBeenCalled();
expect(logic.tasks.value[0].attachments[1]).toMatchObject({
id: 202,
download_link: "https://cdn.example.test/photo.jpg",
});
});
it("clears stale wash summary machine availability when a fresh preview has no session", async () => {
mocks.previewAllowed
.mockResolvedValueOnce({
allowed: true,
machine_available: true,
lane: { id: 7, name: "Lane 7" },
session: { id: 91, status: "READY_FOR_MACHINE_START" },
questions: [{ id: 1, question: "Machine wash?", answer: true, order_priority: 1 }],
tasks: [{ id: 5, task: "Prepare", order_priority: 1, services: ["MACHINE"] }],
conditions: [],
rules: [],
allowed_services: ["MACHINE"],
})
.mockResolvedValueOnce({
allowed: true,
machine_available: true,
lane: { id: 7, name: "Lane 7" },
session: null,
questions: [{ id: 1, question: "Machine wash?", answer: true, order_priority: 1 }],
tasks: [{ id: 5, task: "Prepare", order_priority: 1, services: ["MACHINE"] }],
conditions: [],
rules: [],
allowed_services: ["MACHINE"],
});
mocks.washSummary.mockResolvedValueOnce({
machine_available: false,
session: { id: 91, status: "READY_FOR_MACHINE_START" },
lane: { id: 7, name: "Lane 7" },
questions: [{ id: 1, question: "Machine wash?", answer: true, order_priority: 1 }],
tasks: [{ id: 5, task: "Prepare", order_priority: 1, services: ["MACHINE"] }],
events: [],
allowed_services: [],
});
const logic = useSelfServeLogic();
await logic.fetchSelfServeData(1, 9, 7, "ab12345");
expect(logic.machineAvailable.value).toBe(false);
await logic.fetchSelfServeData(1, 9, 7, "cd12345");
expect(logic.summary.value).toBeNull();
expect(logic.machineAvailable.value).toBe(true);
expect(logic.allowedServices.value).toEqual(["MACHINE"]);
});
it("preserves preview task ids when wash summary omits task ids", async () => {
mocks.previewAllowed.mockResolvedValue({
allowed: true,
machine_available: true,
lane: { id: 7, name: "Lane 7" },
session: { id: 91, status: "READY_FOR_MACHINE_START" },
questions: [{ id: 1, question: "Machine wash?", answer: false, order_priority: 1 }],
tasks: [
{
id: 802001,
task: "Vælg program #2 på dial",
condition_id: 702001,
order_priority: 10,
services: ["MACHINE"],
},
{
id: 802006,
task: "Tryk knap #6 (tagbørste program)",
condition_id: 702003,
order_priority: 36,
services: ["MACHINE"],
},
],
conditions: [],
rules: [],
allowed_services: ["MACHINE"],
});
mocks.washSummary.mockResolvedValue({
session: { id: 91, status: "READY_FOR_MACHINE_START" },
lane: { id: 7, name: "Lane 7" },
questions: [{ id: 1, question: "Machine wash?", answer: false, order_priority: 1 }],
tasks: [
{ task: "Vælg program #2 på dial", condition_id: 702001, order_priority: 10, services: ["MACHINE"] },
{ task: "Tryk knap #6 (tagbørste program)", condition_id: 702003, order_priority: 36, services: ["MACHINE"] },
],
events: [],
});
const logic = useSelfServeLogic();
await logic.fetchSelfServeData(1, 9, 7, "ab12345");
expect(logic.tasks.value.map((task) => task.id)).toEqual([802001, 802006]);
expect(logic.activeTasks.value.map((task) => task.id)).toEqual([802001, 802006]);
});
it("does not infer machine permission from summary tasks when allowed services are explicitly empty", async () => {
const machineButtonTask = {
id: 5,
task: "Press reset",
order_priority: 1,
services: ["MACHINE"],
buttons: ["reset"],
dynamic_images_vehicle_type: 2,
};
mocks.previewAllowed.mockResolvedValue({
allowed: false,
machine_available: true,
lane: { id: 7, name: "Lane 7" },
session: { id: 91, status: "MACHINE_NOT_ALLOWED", allowed: false },
questions: [{ id: 1, question: "Machine wash?", answer: false, order_priority: 1 }],
tasks: [machineButtonTask],
conditions: [],
rules: [],
allowed_services: [],
});
mocks.washSummary.mockResolvedValue({
session: { id: 91, status: "MACHINE_NOT_ALLOWED", allowed: false },
lane: { id: 7, name: "Lane 7" },
questions: [{ id: 1, question: "Machine wash?", answer: false, order_priority: 1 }],
tasks: [machineButtonTask],
events: [],
});
const logic = useSelfServeLogic();
await logic.fetchSelfServeData(1, 9, 7, "ab12345");
expect(logic.allowed.value).toBe(false);
expect(logic.allowedServices.value).toEqual([]);
expect(logic.activeTaskServices.value).toEqual([]);
expect(logic.isServiceAllowed("MACHINE")).toBe(false);
expect(logic.tasks.value.map((task) => task.id)).toEqual([5]);
});
it("opens an existing task attachment link without requesting a new download link", async () => {
const openSpy = vi.fn();
vi.stubGlobal("window", { open: openSpy });
const logic = useSelfServeLogic();
await logic.downloadAttachment(5, 202, {
id: 202,
download_link: "https://api-v2.truckwash.io/master/api/attachments/photo.jpg",
content: { other: "photo.jpg" },
});
expect(mocks.attachmentsDownload).not.toHaveBeenCalled();
expect(openSpy).toHaveBeenCalledWith(
"https://api-v2.truckwash.io/master/api/attachments/photo.jpg",
"_blank",
"noopener"
);
});
it("does not open untrusted embedded task attachment links", async () => {
const openSpy = vi.fn();
vi.stubGlobal("window", { open: openSpy });
mocks.attachmentsDownload.mockResolvedValue({ download_link: "javascript:alert(document.domain)" });
const logic = useSelfServeLogic();
await logic.downloadAttachment(5, 202, {
id: 202,
download_link: "data:text/html,<script>alert(document.domain)</script>",
content: { other: "photo.jpg" },
});
expect(mocks.attachmentsDownload).toHaveBeenCalledWith(5, 202);
expect(openSpy).not.toHaveBeenCalled();
});
it("falls back to lane/reg summary when session summary does not include questions", async () => {
mocks.previewAllowed.mockResolvedValue({
allowed: true,
session: { id: 91, status: "IN_PROGRESS" },
questions: [{ id: 1, question: "Machine wash?", answer: null, order_priority: 1 }],
tasks: [],
conditions: [],
rules: [],
});
mocks.washSummary
.mockResolvedValueOnce({
session: { id: 91, status: "IN_PROGRESS" },
events: [],
})
.mockResolvedValueOnce({
session: { id: 91, status: "IN_PROGRESS" },
questions: [{ id: 1, question: "Machine wash?", answer: null, order_priority: 1 }],
tasks: [],
events: [],
});
const logic = useSelfServeLogic();
await logic.fetchSelfServeData(1, 9, 7, "ab12345");
expect(mocks.washSummary).toHaveBeenNthCalledWith(1, { session_id: 91 });
expect(mocks.washSummary).toHaveBeenNthCalledWith(2, { lane_id: 7, reg: "AB12345", vehicle_type: 9 });
expect(logic.visibleQuestions.value.map((question) => question.id)).toEqual([1]);
});
it("keeps preview questions visible when no summary session exists yet", async () => {
mocks.previewAllowed.mockResolvedValue({
allowed: true,
session: null,
questions: [
{ id: 1, question: "Machine wash?", answer: null, order_priority: 1 },
{ id: 2, question: "Doors closed?", answer: null, order_priority: 2 },
],
tasks: [],
conditions: [],
rules: [],
});
const logic = useSelfServeLogic();
await logic.fetchSelfServeData(1, 9, 7, "ab12345");
expect(mocks.washSummary).not.toHaveBeenCalled();
expect(logic.visibleQuestions.value.map((question) => question.id)).toEqual([1, 2]);
});
it("keeps the newest task list when overlapping preview requests resolve out of order", async () => {
const createDeferred = () => {
let resolve;
const promise = new Promise((res) => {
resolve = res;
});
return { promise, resolve };
};
const firstPreview = createDeferred();
const secondPreview = createDeferred();
mocks.previewAllowed
.mockImplementationOnce(() => firstPreview.promise)
.mockImplementationOnce(() => secondPreview.promise);
mocks.attachmentsList.mockResolvedValue({ data: [] });
const logic = useSelfServeLogic();
const firstFetch = logic.fetchSelfServeData(1, null, 7, "ab12345");
const secondFetch = logic.fetchSelfServeData(1, 9, 7, "ab12345");
secondPreview.resolve({
allowed: true,
session: null,
questions: [],
tasks: [
{ id: 1001, task: "Task A", order_priority: 1 },
{ id: 1002, task: "Task B", order_priority: 2 },
],
conditions: [],
rules: [],
allowed_services: [],
});
await secondFetch;
expect(logic.tasks.value.map((task) => task.id)).toEqual([1001, 1002]);
firstPreview.resolve({
allowed: true,
session: null,
questions: [],
tasks: [{ id: 1001, task: "Task A", order_priority: 1 }],
conditions: [],
rules: [],
allowed_services: [],
});
await firstFetch;
expect(logic.tasks.value.map((task) => task.id)).toEqual([1001, 1002]);
});
it("exposes preview errors for retry UI", async () => {
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
mocks.previewAllowed.mockRejectedValue({
response: {
data: {
data: {
message: "Edge gateway command timed out",
},
},
},
});
const logic = useSelfServeLogic();
const result = await logic.fetchSelfServeData(1, 9, 7, "ab12345");
expect(result).toBeNull();
expect(logic.loading.value).toBe(false);
expect(logic.error.value).toBe("Edge gateway command timed out");
consoleErrorSpy.mockRestore();
});
it("surfaces edge gateway failures when updating lane allowed services", async () => {
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
mocks.request
.mockResolvedValueOnce({
data: {
success: false,
data: {
message: "Edge gateway command timed out",
},
},
})
.mockResolvedValueOnce({
data: {
success: true,
data: {
allowed_services: ["GATE"],
},
},
});
const logic = useSelfServeLogic();
logic.tasks.value = [{ id: 5, task_id: 5, task: "Machine step", order_priority: 1, services: ["MACHINE"] }];
await expect(logic.updateLaneAllowedServices(7)).rejects.toThrow("Edge gateway command timed out");
expect(mocks.request).toHaveBeenNthCalledWith(1, "/modules/self-serve/lane/services/allowed", "post", {
lane_id: 7,
task_ids: [5],
});
expect(logic.error.value).toBe("Edge gateway command timed out");
expect(logic.allowedServices.value).toEqual(["MACHINE"]);
await logic.updateLaneAllowedServices(7);
expect(logic.error.value).toBeNull();
expect(logic.allowedServices.value).toEqual(["GATE"]);
consoleErrorSpy.mockRestore();
});
it("evaluates conditional rules for task activation", () => {
const logic = useSelfServeLogic();
logic.questions.value = [
{ id: 1, question: "Question 1", order_priority: 1, condition_id: null },
{ id: 2, question: "Question 2", order_priority: 2, condition_id: 100 },
];
logic.conditions.value = [{ id: 100, name: "Question 1 yes" }];
logic.rules.value = [
{ id: 501, condition_id: 100, object_type: "question", object_id: 1, type: "IS_TRUE", name: "Q1 true" },
];
logic.tasks.value = [
{ id: 8, task: "Machine step", order_priority: 1, condition_id: 100, services: ["MACHINE"] },
{ id: 9, task: "Always visible", order_priority: 2, condition_id: null, services: [] },
];
logic.answers.value = { 1: false };
expect(logic.visibleQuestions.value.map((question) => question.id)).toEqual([]);
expect(logic.evaluateCondition(100)).toBe(false);
expect(logic.activeTasks.value.map((task) => task.id)).toEqual([9]);
logic.answerQuestion(1, true);
expect(logic.visibleQuestions.value.map((question) => question.id)).toEqual([]);
expect(logic.evaluateCondition(100)).toBe(true);
expect(logic.activeTasks.value.map((task) => task.id)).toEqual([8, 9]);
expect(logic.isServiceAllowed("MACHINE")).toBe(true);
});
it("does not show any questions before summary visibility is available", () => {
const logic = useSelfServeLogic();
logic.questions.value = [
{ id: 1, question: "Always visible", order_priority: 1, condition_id: null },
{ id: 2, question: "Included conditional", order_priority: 2, condition_id: 100 },
];
logic.rules.value = [];
logic.answers.value = { 1: true };
expect(logic.visibleQuestions.value.map((question) => question.id)).toEqual([]);
});
it("synchronizes answers and only keeps questions listed in summary response", async () => {
mocks.previewAllowed.mockResolvedValueOnce({
allowed: true,
session: { id: 44, status: "IN_PROGRESS" },
questions: [
{ id: 2, question: "Doors closed?", answer: null, order_priority: 1 },
{ id: 3, question: "Windows closed?", answer: null, order_priority: 2 },
],
tasks: [],
conditions: [],
rules: [],
});
mocks.washSummary.mockResolvedValueOnce({
session: { id: 44, status: "IN_PROGRESS" },
questions: [
{ id: 2, question: "Doors closed?", answer: null, order_priority: 1 },
{ id: 3, question: "Windows closed?", answer: null, order_priority: 2 },
],
tasks: [],
events: [],
});
mocks.add.mockResolvedValue({
data: {
data: {
selfserve: {
session: { id: 44, status: "IN_PROGRESS" },
questions: [{ id: 2, question: "Doors closed?", answer: true, order_priority: 1 }],
tasks: [],
conditions: [],
rules: [],
},
},
},
});
mocks.previewAllowed.mockResolvedValue({
allowed: true,
session: { id: 44, status: "IN_PROGRESS" },
questions: [{ id: 2, question: "Doors closed?", answer: true, order_priority: 1 }],
tasks: [],
conditions: [],
rules: [],
});
mocks.washSummary.mockResolvedValue({
session: { id: 44, status: "IN_PROGRESS" },
questions: [{ id: 2, question: "Doors closed?", answer: true, order_priority: 1 }],
tasks: [],
events: [],
});
const logic = useSelfServeLogic();
await logic.fetchSelfServeData(3, null, 7, "cd67890");
await logic.syncVehicleAnswer({
departmentId: 3,
laneId: 7,
customerNumber: 12345,
reg: "cd67890",
questionId: 2,
value: true,
});
expect(mocks.add).toHaveBeenCalledWith(3, 7, 12345, "CD67890", 2, true, {
activate_machine: false,
sync_relay_state: false,
});
expect(mocks.previewAllowed).toHaveBeenCalledWith(7, "CD67890");
expect(logic.answers.value[2]).toBe(true);
expect(logic.visibleQuestions.value.map((question) => question.id)).toEqual([2]);
});
it("preserves the selected vehicle-type override when refreshing after answer sync", async () => {
mocks.previewAllowed.mockResolvedValue({
allowed: true,
session: { id: 66, status: "IN_PROGRESS" },
questions: [{ id: 1, question: "Primary question", answer: null, order_priority: 1 }],
tasks: [],
conditions: [],
rules: [],
});
mocks.washSummary.mockResolvedValue({
session: { id: 66, status: "IN_PROGRESS" },
questions: [{ id: 1, question: "Primary question", answer: null, order_priority: 1 }],
tasks: [],
events: [],
});
mocks.add.mockResolvedValue({
data: {
data: {
selfserve: {
session: { id: 66, status: "IN_PROGRESS" },
questions: [{ id: 1, question: "Primary question", answer: true, order_priority: 1 }],
tasks: [],
conditions: [],
rules: [],
},
},
},
});
const logic = useSelfServeLogic();
await logic.fetchSelfServeData(1, 9, 7, "ab12345");
await logic.syncVehicleAnswer({
departmentId: 1,
laneId: 7,
customerNumber: 12345,
reg: "ab12345",
questionId: 1,
value: true,
});
expect(mocks.previewAllowed).toHaveBeenNthCalledWith(1, 7, "AB12345", 9);
expect(mocks.previewAllowed).toHaveBeenNthCalledWith(2, 7, "AB12345", 9);
expect(mocks.add).toHaveBeenCalledWith(1, 7, 12345, "AB12345", 1, true, {
activate_machine: false,
sync_relay_state: false,
vehicle_type: 9,
});
});
it("uses the resolved auto vehicle type for lane/reg summary fallback", async () => {
mocks.previewAllowed.mockResolvedValue({
allowed: true,
session: { id: 91, status: "IN_PROGRESS", vehicle_type_id: 5 },
questions: [{ id: 1, question: "Primary question", answer: null, order_priority: 1 }],
tasks: [],
conditions: [],
rules: [],
});
mocks.washSummary.mockResolvedValue({
session: { id: 91, status: "IN_PROGRESS", vehicle_type_id: 5 },
questions: [{ id: 1, question: "Primary question", answer: null, order_priority: 1 }],
tasks: [],
events: [],
});
const logic = useSelfServeLogic();
await logic.fetchSelfServeData(1, null, 7, "ab12345");
expect(logic.resolvedVehicleTypeId.value).toBe(5);
expect(mocks.washSummary).toHaveBeenNthCalledWith(1, { session_id: 91 });
expect(mocks.washSummary).toHaveBeenNthCalledWith(2, { lane_id: 7, reg: "AB12345", vehicle_type: 5 });
});
it("reuses the resolved auto vehicle type when refreshing after answer sync", async () => {
mocks.previewAllowed.mockResolvedValue({
allowed: true,
session: { id: 66, status: "IN_PROGRESS", vehicle_type_id: 5 },
questions: [{ id: 1, question: "Primary question", answer: null, order_priority: 1 }],
tasks: [],
conditions: [],
rules: [],
});
mocks.washSummary.mockResolvedValue({
session: { id: 66, status: "IN_PROGRESS", vehicle_type_id: 5 },
questions: [{ id: 1, question: "Primary question", answer: null, order_priority: 1 }],
tasks: [],
events: [],
});
mocks.add.mockResolvedValue({
data: {
data: {
selfserve: {
session: { id: 66, status: "IN_PROGRESS", vehicle_type_id: 5 },
questions: [{ id: 1, question: "Primary question", answer: true, order_priority: 1 }],
tasks: [],
conditions: [],
rules: [],
},
},
},
});
const logic = useSelfServeLogic();
await logic.fetchSelfServeData(1, null, 7, "ab12345");
await logic.syncVehicleAnswer({
departmentId: 1,
laneId: 7,
customerNumber: 12345,
reg: "ab12345",
questionId: 1,
value: true,
});
expect(mocks.previewAllowed).toHaveBeenNthCalledWith(1, 7, "AB12345");
expect(mocks.previewAllowed).toHaveBeenNthCalledWith(2, 7, "AB12345", 5);
});
it("keeps answered questions visible when summary returns no currently-visible question list", async () => {
mocks.previewAllowed.mockResolvedValue({
allowed: false,
session: { id: 99, status: "MACHINE_NOT_ALLOWED" },
questions: [{ id: 1, question: "Primary question", answer: true, order_priority: 1 }],
tasks: [],
conditions: [],
rules: [],
});
mocks.washSummary.mockResolvedValue({
session: { id: 99, status: "MACHINE_NOT_ALLOWED" },
questions: [],
tasks: [],
events: [],
});
const logic = useSelfServeLogic();
await logic.fetchSelfServeData(1, 9, 7, "ab12345");
expect(logic.answers.value[1]).toBe(true);
expect(logic.visibleQuestions.value.map((question) => question.id)).toEqual([1]);
});
it("hides condition-dependent questions again when the parent answer no longer satisfies the condition", async () => {
const baseConditions = [{ id: 100, name: "Primary answer is yes" }];
const baseRules = [
{ id: 501, condition_id: 100, object_type: "question", object_id: 2, type: "IS_TRUE", name: "Q2 yes" },
];
mocks.previewAllowed.mockResolvedValueOnce({
allowed: true,
session: { id: 55, status: "IN_PROGRESS" },
questions: [
{ id: 2, question: "Primary question", answer: true, order_priority: 1, condition_id: null },
{ id: 3, question: "Conditional follow-up", answer: null, order_priority: 2, condition_id: 100 },
],
tasks: [],
conditions: baseConditions,
rules: baseRules,
});
mocks.washSummary
.mockResolvedValueOnce({
session: { id: 55, status: "IN_PROGRESS" },
questions: [
{ id: 2, question: "Primary question", answer: true, order_priority: 1, condition_id: null },
{ id: 3, question: "Conditional follow-up", answer: null, order_priority: 2, condition_id: 100 },
],
tasks: [],
conditions: baseConditions,
rules: baseRules,
events: [],
})
.mockResolvedValueOnce({
session: { id: 55, status: "IN_PROGRESS" },
questions: [
{ id: 2, question: "Primary question", answer: true, order_priority: 1, condition_id: null },
{ id: 3, question: "Conditional follow-up", answer: null, order_priority: 2, condition_id: 100 },
],
tasks: [],
conditions: baseConditions,
rules: baseRules,
events: [],
});
mocks.add.mockResolvedValue({
data: {
data: {
selfserve: {
session: { id: 55, status: "IN_PROGRESS" },
questions: [{ id: 2, question: "Primary question", answer: false, order_priority: 1 }],
conditions: [],
rules: [],
tasks: [],
},
},
},
});
mocks.previewAllowed.mockResolvedValue({
allowed: true,
session: { id: 55, status: "IN_PROGRESS" },
questions: [{ id: 2, question: "Primary question", answer: false, order_priority: 1 }],
tasks: [],
conditions: [],
rules: [],
});
mocks.washSummary.mockResolvedValue({
session: { id: 55, status: "IN_PROGRESS" },
questions: [{ id: 2, question: "Primary question", answer: false, order_priority: 1 }],
tasks: [],
conditions: [],
rules: [],
events: [],
});
const logic = useSelfServeLogic();
await logic.fetchSelfServeData(1, null, 7, "ab12345");
expect(logic.visibleQuestions.value.map((question) => question.id)).toEqual([2, 3]);
await logic.syncVehicleAnswer({
departmentId: 1,
laneId: 7,
customerNumber: 12345,
reg: "ab12345",
questionId: 2,
value: false,
});
expect(logic.visibleQuestions.value.map((question) => question.id)).toEqual([2]);
});
it("keeps condition-dependent questions visible in stable order when the condition remains true", async () => {
const baseConditions = [{ id: 100, name: "Primary answer is yes" }];
const baseRules = [
{ id: 501, condition_id: 100, object_type: "question", object_id: 1, type: "IS_TRUE", name: "Q1 yes" },
];
mocks.previewAllowed.mockResolvedValueOnce({
allowed: true,
session: { id: 77, status: "IN_PROGRESS" },
questions: [
{ id: 1, question: "Primary question", answer: false, order_priority: 1, condition_id: null },
{ id: 2, question: "Conditional follow-up", answer: null, order_priority: 2, condition_id: 100 },
],
tasks: [],
conditions: baseConditions,
rules: baseRules,
});
mocks.washSummary.mockResolvedValueOnce({
session: { id: 77, status: "IN_PROGRESS" },
questions: [
{ id: 1, question: "Primary question", answer: false, order_priority: 1, condition_id: null },
{ id: 2, question: "Conditional follow-up", answer: null, order_priority: 2, condition_id: 100 },
],
tasks: [],
conditions: baseConditions,
rules: baseRules,
events: [],
});
mocks.add.mockResolvedValue({
data: {
data: {
selfserve: {
session: { id: 77, status: "IN_PROGRESS" },
questions: [
{ id: 1, question: "Primary question", answer: true, order_priority: 1, condition_id: null },
{ id: 2, question: "Conditional follow-up", answer: null, order_priority: 2, condition_id: 100 },
],
conditions: [],
rules: [],
tasks: [],
},
},
},
});
mocks.previewAllowed.mockResolvedValue({
allowed: true,
session: { id: 77, status: "IN_PROGRESS" },
questions: [
{ id: 1, question: "Primary question", answer: true, order_priority: 1, condition_id: null },
{ id: 2, question: "Conditional follow-up", answer: null, order_priority: 2, condition_id: 100 },
],
tasks: [],
conditions: [],
rules: [],
});
mocks.washSummary.mockResolvedValue({
session: { id: 77, status: "IN_PROGRESS" },
questions: [
{ id: 1, question: "Primary question", answer: true, order_priority: 1, condition_id: null },
{ id: 2, question: "Conditional follow-up", answer: null, order_priority: 2, condition_id: 100 },
],
tasks: [],
conditions: [],
rules: [],
events: [],
});
const logic = useSelfServeLogic();
await logic.fetchSelfServeData(1, null, 7, "ab12345");
expect(logic.visibleQuestions.value.map((question) => question.id)).toEqual([1, 2]);
await logic.syncVehicleAnswer({
departmentId: 1,
laneId: 7,
customerNumber: 12345,
reg: "ab12345",
questionId: 1,
value: true,
});
expect(logic.answers.value[1]).toBe(true);
expect(logic.visibleQuestions.value.map((question) => question.id)).toEqual([1, 2]);
});
it("keeps visible question order stable when answer refreshes return the same questions reordered", async () => {
const initialQuestions = [
{ id: 1, question: "Primary question", answer: null, order_priority: 1, condition_id: null },
{ id: 2, question: "Follow-up question", answer: null, order_priority: 2, condition_id: null },
];
const refreshedQuestions = [{ ...initialQuestions[1] }, { ...initialQuestions[0], answer: true }];
mocks.previewAllowed.mockResolvedValueOnce({
allowed: true,
session: { id: 78, status: "IN_PROGRESS" },
questions: initialQuestions,
tasks: [],
conditions: [],
rules: [],
});
mocks.washSummary.mockResolvedValueOnce({
session: { id: 78, status: "IN_PROGRESS" },
questions: initialQuestions,
tasks: [],
conditions: [],
rules: [],
events: [],
});
mocks.add.mockResolvedValue({
data: {
data: {
selfserve: {
session: { id: 78, status: "IN_PROGRESS" },
questions: refreshedQuestions,
conditions: [],
rules: [],
tasks: [],
},
},
},
});
mocks.previewAllowed.mockResolvedValue({
allowed: true,
session: { id: 78, status: "IN_PROGRESS" },
questions: refreshedQuestions,
tasks: [],
conditions: [],
rules: [],
});
mocks.washSummary.mockResolvedValue({
session: { id: 78, status: "IN_PROGRESS" },
questions: refreshedQuestions,
tasks: [],
conditions: [],
rules: [],
events: [],
});
const logic = useSelfServeLogic();
await logic.fetchSelfServeData(1, null, 7, "ab12345");
expect(logic.visibleQuestions.value.map((question) => question.id)).toEqual([1, 2]);
await logic.syncVehicleAnswer({
departmentId: 1,
laneId: 7,
customerNumber: 12345,
reg: "ab12345",
questionId: 1,
value: true,
});
expect(logic.answers.value[1]).toBe(true);
expect(logic.visibleQuestions.value.map((question) => question.id)).toEqual([1, 2]);
});
it("clears persisted answers for a lane/reg and refreshes preview state", async () => {
mocks.previewAllowed
.mockResolvedValueOnce({
allowed: true,
session: { id: 88, status: "IN_PROGRESS" },
questions: [
{ id: 1, question: "Doors closed?", answer: true, order_priority: 1 },
{ id: 2, question: "Windows closed?", answer: false, order_priority: 2 },
],
tasks: [],
conditions: [],
rules: [],
})
.mockResolvedValueOnce({
allowed: true,
session: { id: 88, status: "IN_PROGRESS" },
questions: [
{ id: 1, question: "Doors closed?", answer: null, order_priority: 1 },
{ id: 2, question: "Windows closed?", answer: null, order_priority: 2 },
],
tasks: [],
conditions: [],
rules: [],
});
mocks.washSummary
.mockResolvedValueOnce({
session: { id: 88, status: "IN_PROGRESS" },
questions: [
{ id: 1, question: "Doors closed?", answer: true, order_priority: 1 },
{ id: 2, question: "Windows closed?", answer: false, order_priority: 2 },
],
tasks: [],
events: [],
})
.mockResolvedValueOnce({
session: { id: 88, status: "IN_PROGRESS" },
questions: [
{ id: 1, question: "Doors closed?", answer: null, order_priority: 1 },
{ id: 2, question: "Windows closed?", answer: null, order_priority: 2 },
],
tasks: [],
events: [],
});
mocks.getAll.mockResolvedValue([
{ id: 701, question: 1 },
{ id: 702, question: 2 },
]);
mocks.deleteCondition.mockResolvedValue({ data: { message: "Condition deleted" } });
const logic = useSelfServeLogic();
await logic.fetchSelfServeData(3, null, 7, "ab12345");
expect(logic.answers.value[1]).toBe(true);
expect(logic.answers.value[2]).toBe(false);
const result = await logic.clearVehicleAnswers({
departmentId: 3,
laneId: 7,
reg: "ab12345",
});
expect(result).toEqual({ deletedCount: 2 });
expect(mocks.getAll).toHaveBeenCalledWith({
lane: 7,
reg: "AB12345",
department: 3,
});
expect(mocks.deleteCondition).toHaveBeenCalledTimes(2);
expect(mocks.deleteCondition).toHaveBeenNthCalledWith(1, 701);
expect(mocks.deleteCondition).toHaveBeenNthCalledWith(2, 702);
expect(logic.answers.value[1]).toBe(null);
expect(logic.answers.value[2]).toBe(null);
expect(logic.allVisibleQuestionsAnswered.value).toBe(false);
});
});