Files
pleno-vue/tests/unit/department-modules-display-self-serve.spec.js
T
Jeppe B 5204536f92 Fix self-serve settings state and contract (#229)
Ensure department-scoped self-serve settings load and save safely across route transitions, document the API contract, and cover stale in-flight state.
2026-07-28 18:34:35 +02:00

324 lines
8.9 KiB
JavaScript

// @vitest-environment jsdom
import { defineComponent, h, nextTick } from "vue";
import { mount } from "@vue/test-utils";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
authenticatedRequest: vi.fn(),
toastSuccess: vi.fn(),
toastError: vi.fn(),
}));
vi.mock("vue-router", async () => {
const { ref } = await import("vue");
const currentRoute = ref({
params: {
departmentId: "12",
},
});
const push = vi.fn();
return {
useRouter: () => ({
currentRoute,
push,
}),
__routeRef: currentRoute,
__pushMock: push,
};
});
vi.mock("@/components/session/authenticatedRequest.vue", () => ({
authenticatedRequest: mocks.authenticatedRequest,
}));
vi.mock("vue-toast-notification", () => ({
useToast: () => ({
success: mocks.toastSuccess,
error: mocks.toastError,
}),
}));
vi.mock("@/composables/useAppToast.js", () => ({
useAppToast: () => ({
success: mocks.toastSuccess,
error: mocks.toastError,
}),
}));
vi.mock("vue-i18n", () => ({
useI18n: () => ({
t: (key) => key,
}),
}));
vi.mock("buefy", () => ({
BSwitch: defineComponent({
name: "MockSwitch",
props: {
modelValue: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
},
emits: ["update:model-value"],
setup(props, { emit }) {
return () =>
h("button", {
"data-testid": "self-serve-switch",
"data-state": props.modelValue ? "on" : "off",
disabled: props.disabled,
onClick: () => emit("update:model-value", !props.modelValue),
});
},
}),
}));
import DepartmentModulesDisplay from "@/components/displays/department/moduleNavigation/DepartmentModulesDisplay.vue";
import { __pushMock, __routeRef } from "vue-router";
const flushMicrotasks = async () => {
await Promise.resolve();
await Promise.resolve();
await nextTick();
};
const createDeferred = () => {
let resolve;
let reject;
const promise = new Promise((promiseResolve, promiseReject) => {
resolve = promiseResolve;
reject = promiseReject;
});
return { promise, resolve, reject };
};
describe("DepartmentModulesDisplay self-serve management", () => {
let setIntervalSpy;
let consoleErrorSpy;
beforeEach(() => {
mocks.authenticatedRequest.mockReset();
mocks.toastSuccess.mockReset();
mocks.toastError.mockReset();
__routeRef.value = { params: { departmentId: "12" } };
setIntervalSpy = vi.spyOn(globalThis, "setInterval").mockImplementation(() => 1);
consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
mocks.authenticatedRequest.mockImplementation(async (url, method) => {
if (String(url).startsWith("/admin/bookings/department/count?department_id=12")) {
return {
data: {
data: {
message: 0,
},
},
};
}
if (url === "/departments/self-serve/enabled?id=12" && method === "GET") {
return {
data: {
data: {
enabled: false,
},
},
};
}
if (url === "/departments/self-serve/enabled?id=12&enabled=true" && method === "PUT") {
return {
data: {
data: {
enabled: true,
},
},
};
}
if (url === "/departments/self-serve/enabled?id=12&enabled=false" && method === "PUT") {
return {
data: {
data: {
enabled: false,
},
},
};
}
throw new Error(`Unexpected request: ${method} ${url}`);
});
});
afterEach(() => {
setIntervalSpy.mockRestore();
consoleErrorSpy.mockRestore();
});
it("loads the current self-serve status on mount", async () => {
const wrapper = mount(DepartmentModulesDisplay);
await flushMicrotasks();
expect(mocks.authenticatedRequest).toHaveBeenCalledWith("/departments/self-serve/enabled?id=12", "GET");
expect(wrapper.get("[data-testid='self-serve-switch']").attributes("data-state")).toBe("off");
});
it("shows the Bookinger-style loading badge while the self-serve status is loading", async () => {
const selfServeStatus = createDeferred();
mocks.authenticatedRequest.mockImplementation((url, method) => {
if (String(url).startsWith("/admin/bookings/department/count?department_id=12")) {
return Promise.resolve({
data: {
data: {
message: 0,
},
},
});
}
if (url === "/departments/self-serve/enabled?id=12" && method === "GET") {
return selfServeStatus.promise;
}
throw new Error(`Unexpected request: ${method} ${url}`);
});
const wrapper = mount(DepartmentModulesDisplay);
await nextTick();
const loadingBadge = wrapper.get("[data-testid='department-module-self-serve-state-loading']");
expect(loadingBadge.classes()).toContain("department-module-card__state-loading");
expect(wrapper.find("[data-testid='self-serve-switch']").exists()).toBe(false);
selfServeStatus.resolve({
data: {
data: {
enabled: true,
},
},
});
await flushMicrotasks();
await nextTick();
expect(wrapper.find("[data-testid='department-module-self-serve-state-loading']").exists()).toBe(false);
expect(wrapper.get("[data-testid='self-serve-switch']").attributes("data-state")).toBe("on");
});
it("renders responsive overview columns for each module card", async () => {
const wrapper = mount(DepartmentModulesDisplay);
await flushMicrotasks();
const firstColumn = wrapper.find(".column");
expect(firstColumn.classes()).toEqual(
expect.arrayContaining(["is-12-mobile", "is-6-tablet", "is-4-desktop", "is-3-widescreen"])
);
});
it("updates self-serve status and keeps UI in sync on success", async () => {
const wrapper = mount(DepartmentModulesDisplay);
await flushMicrotasks();
await wrapper.get("[data-testid='self-serve-switch']").trigger("click");
await flushMicrotasks();
expect(mocks.authenticatedRequest).toHaveBeenCalledWith(
"/departments/self-serve/enabled?id=12&enabled=true",
"PUT"
);
expect(wrapper.get("[data-testid='self-serve-switch']").attributes("data-state")).toBe("on");
expect(mocks.toastSuccess).toHaveBeenCalledWith("admin.department_modules.self_wash.enabled");
});
it("keeps the self-serve switch isolated from card navigation", async () => {
const wrapper = mount(DepartmentModulesDisplay);
await flushMicrotasks();
__pushMock.mockClear();
await wrapper.get("[data-testid='self-serve-switch']").trigger("click");
await flushMicrotasks();
expect(__pushMock).not.toHaveBeenCalled();
});
it("keeps the bookings count badge isolated from the card click handler", async () => {
mocks.authenticatedRequest.mockImplementation(async (url, method) => {
if (String(url).startsWith("/admin/bookings/department/count?department_id=12")) {
return {
data: {
data: {
message: 5,
},
},
};
}
if (url === "/departments/self-serve/enabled?id=12" && method === "GET") {
return {
data: {
data: {
enabled: false,
},
},
};
}
throw new Error(`Unexpected request: ${method} ${url}`);
});
const wrapper = mount(DepartmentModulesDisplay);
await flushMicrotasks();
__pushMock.mockClear();
await wrapper.get(".department-module-card__count").trigger("click");
expect(__pushMock).toHaveBeenCalledTimes(1);
expect(__pushMock).toHaveBeenCalledWith("/admin/12/modules/bookings?search=pending");
});
it("reverts self-serve status and shows an error when update fails", async () => {
mocks.authenticatedRequest.mockImplementation(async (url, method) => {
if (String(url).startsWith("/admin/bookings/department/count?department_id=12")) {
return {
data: {
data: {
message: 0,
},
},
};
}
if (url === "/departments/self-serve/enabled?id=12" && method === "GET") {
return {
data: {
data: {
enabled: false,
},
},
};
}
if (url === "/departments/self-serve/enabled?id=12&enabled=true" && method === "PUT") {
throw new Error("save failed");
}
throw new Error(`Unexpected request: ${method} ${url}`);
});
const wrapper = mount(DepartmentModulesDisplay);
await flushMicrotasks();
await wrapper.get("[data-testid='self-serve-switch']").trigger("click");
await flushMicrotasks();
expect(wrapper.get("[data-testid='self-serve-switch']").attributes("data-state")).toBe("off");
expect(mocks.toastError).toHaveBeenCalledWith("admin.department_modules.self_wash.update_error");
});
});