Add DepartmentSelfServeStudio.vue for advanced self-serve configuration management:
- Introduced a new Vue component supporting versioned configuration authoring for questions, conditions, tasks, and rules. - Implemented scoped editing with lane and machine type-specific configurations. - Added version tracking features, including validation, publication, and rollback. - Enhanced UI with tabbed navigation, modals for adding/editing records, and notifications for actions.
This commit is contained in:
@@ -0,0 +1,382 @@
|
||||
// @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(),
|
||||
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,
|
||||
}),
|
||||
}));
|
||||
|
||||
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.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.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("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: "<button data-testid='emit-card-answer' @click=\"$emit('answer-question', visibleQuestions[0]?.id, true)\">answer</button>",
|
||||
},
|
||||
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("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: "<div data-testid='question-cards-probe'>{{ visibleQuestions.map(q => q.question).join('|') }}</div>",
|
||||
},
|
||||
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: "<div data-testid='question-cards-probe'>{{ visibleQuestions.map(q => q.question).join('|') }}</div>",
|
||||
},
|
||||
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: "<div data-testid='question-cards-probe'>{{ visibleQuestions.map(q => q.question).join('|') }}</div>",
|
||||
},
|
||||
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: "<div data-testid='question-cards-probe'>{{ visibleQuestions.map(q => q.question).join('|') }}</div>",
|
||||
},
|
||||
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",
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user