Files
pleno-vue/tests/unit/department-overview-period-sync.behavior.spec.js

613 lines
18 KiB
JavaScript

// @vitest-environment jsdom
import { nextTick } from "vue";
import { mount } from "@vue/test-utils";
import { beforeEach, describe, expect, it, vi } from "vitest";
const getTransactionCountMock = vi.hoisted(() => vi.fn());
const getDepartmentMock = vi.hoisted(() => vi.fn());
const getLaneStatusTogglesMock = vi.hoisted(() => vi.fn());
const setMachineStatusEnabledMock = vi.hoisted(() => vi.fn());
const setLaneSelfServeEnabledMock = vi.hoisted(() => vi.fn());
const hasPermissionMock = vi.hoisted(() => vi.fn());
const laneHelpers = vi.hoisted(() => {
const requiredFields = [
"relay_in_id",
"relay_out_id",
"relay_machine_id",
"relay_machine_program_picker_id",
"relay_machine_cleaner_id",
"dynamic_image_id",
"machine_type_id",
];
const hasConfiguredValue = (value) =>
value !== null && value !== undefined && String(value).trim() !== "" && String(value).trim() !== "0";
const getWarnings = (lane) =>
lane?.dognvask_configuration_warnings ??
requiredFields
.filter((field) => !hasConfiguredValue(lane?.[field]))
.map((field) => ({ field, message: `${field} mangler` }));
return {
getWarnings,
isMachineStatusEnabled: (lane) =>
typeof lane?.machine_status_enabled === "boolean"
? lane.machine_status_enabled
: ["AVAILABLE", "OCCUPIED", "RESERVED"].includes(String(lane?.status ?? "").toUpperCase()),
isSelfServeConfigured: (lane) =>
typeof lane?.dognvask_configured === "boolean" ? lane.dognvask_configured : getWarnings(lane).length === 0,
};
});
const sharedDateState = vi.hoisted(() => ({
selected_date: null,
selected_date_to: null,
}));
vi.mock("@/components/session/token/SessionUser.vue", () => ({
SessionUser: {
objects: {
department_daily_reports: {
functions: {
getTransactionCount: getTransactionCountMock,
},
},
departments: {
get: {
single: getDepartmentMock,
},
},
department_lanes: {
functions: {
getStatusToggles: getLaneStatusTogglesMock,
setMachineStatusEnabled: setMachineStatusEnabledMock,
isMachineStatusEnabled: laneHelpers.isMachineStatusEnabled,
isSelfServeConfigured: laneHelpers.isSelfServeConfigured,
getSelfServeConfigurationWarnings: laneHelpers.getWarnings,
},
set: {
selfserve_enabled: setLaneSelfServeEnabledMock,
},
},
},
hasPermission: hasPermissionMock,
functions: {
date: {
isToday: () => false,
},
currency: {
toLocal: (value) => String(value),
},
},
},
}));
vi.mock("@/components/pagination/departmentTabs.vue", async () => {
const { ref } = await import("vue");
return {
departments: ref([
{ id: 10, name: "Dept 10" },
{ id: 20, name: "Dept 20" },
]),
};
});
vi.mock("@/views/dashboards/departmentDashboard/other/displays/DepartmentsOverviewObject.vue", () => {
let fetchId = 0;
return {
next_fetch_id: () => {
fetchId += 1;
return fetchId;
},
is_latest_fetch: () => true,
is_loading: () => false,
finished_loading: vi.fn(),
};
});
vi.mock("@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentDailyReportObject.vue", async () => {
const { ref } = await import("vue");
const selected_date = ref("2026-03-23");
const selected_date_to = ref("2026-03-23");
sharedDateState.selected_date = selected_date;
sharedDateState.selected_date_to = selected_date_to;
return {
selected_date,
selected_date_to,
selectDate: (startIso, endIso) => {
selected_date.value = startIso;
selected_date_to.value = endIso;
},
};
});
vi.mock("@/views/dashboards/departmentDashboard/other/displays/DepartmentDashboardOverviewNavigation.vue", () => ({
default: {
name: "DepartmentDashboardOverviewNavigation",
template: "<div class='overview-navigation-stub' />",
},
}));
import DepartmentDailyReportThisWeek from "@/views/dashboards/departmentDashboard/other/displays/DepartmentDailyReportThisWeek.vue";
import DepartmentDailyReportSmall from "@/views/dashboards/departmentDashboard/other/displays/DepartmentDailyReportSmall.vue";
const flushAll = async () => {
await nextTick();
await Promise.resolve();
await nextTick();
await Promise.resolve();
};
describe("Department overview period sync behavior", () => {
beforeEach(() => {
getTransactionCountMock.mockReset();
getDepartmentMock.mockReset();
getLaneStatusTogglesMock.mockReset();
setMachineStatusEnabledMock.mockReset();
setLaneSelfServeEnabledMock.mockReset();
hasPermissionMock.mockReset();
hasPermissionMock.mockImplementation((permission) =>
["modules_selfserve_lane_status_set", "edit_department_lane"].includes(permission)
);
getDepartmentMock.mockResolvedValue({ id: 20, name: "Dept 20" });
getLaneStatusTogglesMock.mockResolvedValue({ data: { data: [] } });
setMachineStatusEnabledMock.mockResolvedValue({
data: { data: { status: "AVAILABLE", machine_status_enabled: true } },
});
setLaneSelfServeEnabledMock.mockResolvedValue({ data: { data: { lane: { selfserve_enabled: true } } } });
getTransactionCountMock.mockResolvedValue({
data: {
data: {
earnings: 10,
products: 2,
quantity: 3,
washes: 4,
outside_hours: {
total: 1,
by_source: {
orders: 0,
xlvask: 1,
selfserve: 0,
},
has_missing_opening_hours: false,
missing_department_ids: [],
},
},
},
});
if (sharedDateState.selected_date) {
sharedDateState.selected_date.value = "2026-03-23";
}
if (sharedDateState.selected_date_to) {
sharedDateState.selected_date_to.value = "2026-03-23";
}
});
it("list summary and compact cards fetch against the same shared period", async () => {
mount(DepartmentDailyReportThisWeek, {
props: {
departments: [10],
},
global: {
mocks: {
$t: (value) => value,
},
stubs: {
DepartmentDashboardOverviewNavigation: true,
BSkeleton: true,
},
},
});
mount(DepartmentDailyReportSmall, {
props: {
department_id: 20,
},
global: {
mocks: {
$t: (value) => value,
},
stubs: {
BLoading: true,
},
},
});
await flushAll();
await flushAll();
const initialCalls = getTransactionCountMock.mock.calls.map(
([departmentId, date, dateTo]) => `${departmentId}:${date}:${dateTo}`
);
expect(initialCalls).toEqual(expect.arrayContaining(["10:2026-03-23:2026-03-23", "20:2026-03-23:2026-03-23"]));
});
it("changing shared range refetches both list and cards with the new period", async () => {
mount(DepartmentDailyReportThisWeek, {
props: {
departments: [10],
},
global: {
mocks: {
$t: (value) => value,
},
stubs: {
DepartmentDashboardOverviewNavigation: true,
BSkeleton: true,
},
},
});
mount(DepartmentDailyReportSmall, {
props: {
department_id: 20,
},
global: {
mocks: {
$t: (value) => value,
},
stubs: {
BLoading: true,
},
},
});
await flushAll();
await flushAll();
getTransactionCountMock.mockClear();
sharedDateState.selected_date_to.value = "2026-03-24";
await flushAll();
await flushAll();
const rangeCalls = getTransactionCountMock.mock.calls.map(
([departmentId, date, dateTo]) => `${departmentId}:${date}:${dateTo}`
);
expect(rangeCalls).toEqual(expect.arrayContaining(["10:2026-03-23:2026-03-24", "20:2026-03-23:2026-03-24"]));
});
it("shows missing-opening-hours labels in the list summary", async () => {
getTransactionCountMock.mockResolvedValueOnce({
data: {
data: {
earnings: 10,
products: 2,
quantity: 3,
washes: 4,
outside_hours: {
total: 1,
by_source: {
orders: 0,
xlvask: 1,
selfserve: 0,
},
has_missing_opening_hours: true,
missing_department_ids: [20],
},
},
},
});
const wrapper = mount(DepartmentDailyReportThisWeek, {
props: {
departments: [10],
},
global: {
mocks: {
$t: (key, values = {}) => {
if (key === "admin.daily_report.reports.night_washes.warning_missing_opening_hours") {
return `Opening hours missing for: ${values.departments}`;
}
return key;
},
},
stubs: {
DepartmentDashboardOverviewNavigation: true,
BSkeleton: true,
},
},
});
await flushAll();
await flushAll();
expect(wrapper.find('[data-testid="overview-summary-night-washes-warning"]').text()).toContain(
"Opening hours missing for: Dept 20"
);
});
it("does not show missing-opening-hours labels in compact department rows", async () => {
getTransactionCountMock.mockResolvedValueOnce({
data: {
data: {
earnings: 10,
products: 2,
quantity: 3,
washes: 4,
outside_hours: {
total: 1,
by_source: {
orders: 0,
xlvask: 1,
selfserve: 0,
},
has_missing_opening_hours: true,
missing_department_ids: [20],
},
},
},
});
const wrapper = mount(DepartmentDailyReportSmall, {
props: {
department_id: 20,
},
global: {
mocks: {
$t: (key, values = {}) => {
if (key === "admin.daily_report.reports.night_washes.warning_missing_opening_hours") {
return `Opening hours missing for: ${values.departments}`;
}
return key;
},
},
stubs: {
BLoading: true,
},
},
});
await flushAll();
await flushAll();
expect(wrapper.text()).toContain("Dept 20");
expect(wrapper.text()).toContain("1");
expect(wrapper.text()).not.toContain("Opening hours missing for:");
});
it("renders lane machine and Døgnvask toggles and warns when Døgnvask setup is incomplete", async () => {
const laneWithCompleteSetup = {
id: 21,
department: 20,
name: "T1",
status: "AVAILABLE",
machine_status_enabled: true,
selfserve_enabled: true,
relay_in_id: "in-1",
relay_out_id: "out-1",
relay_machine_id: "machine-1",
relay_machine_program_picker_id: "picker-1",
relay_machine_cleaner_id: "cleaner-1",
dynamic_image_id: 1,
machine_type_id: 1,
dognvask_configured: true,
dognvask_configuration_warnings: [],
machine_status_audit: {
modified_at: "2026-05-26T11:45:00+02:00",
modified_by_name: "Jeppe",
},
};
const laneWithMissingSetup = {
id: 22,
department: 20,
name: "T2",
status: "MAINTENANCE",
machine_status_enabled: false,
selfserve_enabled: true,
relay_in_id: "in-2",
relay_out_id: "",
relay_machine_id: "machine-2",
relay_machine_program_picker_id: "",
relay_machine_cleaner_id: "cleaner-2",
dynamic_image_id: null,
machine_type_id: null,
dognvask_configured: false,
dognvask_configuration_warnings: [
{ field: "relay_out_id", message: "Udgangsrelæ mangler" },
{ field: "dynamic_image_id", message: "Maskinstatusbillede mangler" },
],
};
getLaneStatusTogglesMock.mockResolvedValueOnce({
data: {
data: [laneWithCompleteSetup, laneWithMissingSetup],
},
});
setMachineStatusEnabledMock.mockResolvedValueOnce({
data: {
data: {
id: 22,
status: "AVAILABLE",
machine_status_enabled: true,
},
},
});
setLaneSelfServeEnabledMock
.mockResolvedValueOnce({
data: {
data: {
lane: {
...laneWithCompleteSetup,
selfserve_enabled: false,
},
},
},
})
.mockResolvedValueOnce({
data: {
data: {
lane: {
...laneWithMissingSetup,
selfserve_enabled: false,
},
},
},
});
const wrapper = mount(DepartmentDailyReportSmall, {
props: {
department_id: 20,
},
global: {
mocks: {
$t: (value) => value,
},
stubs: {
BLoading: true,
},
},
});
await flushAll();
await flushAll();
expect(getLaneStatusTogglesMock).toHaveBeenCalledWith(20);
expect(wrapper.text()).toContain("Maskinstatus");
expect(wrapper.text()).toContain("Sidst ændret");
expect(wrapper.text()).toContain("Ændret af");
expect(wrapper.text()).toContain("Jeppe");
expect(wrapper.text()).toContain("2026");
expect(wrapper.text()).toContain("Døgnvask");
expect(wrapper.find('[data-testid="overview-department-20-lane-21-dognvask-info"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="overview-department-20-lane-22-dognvask-warning"]').exists()).toBe(true);
expect(wrapper.find("#dognvask-20-21").element.checked).toBe(true);
expect(wrapper.find("#dognvask-20-22").element.checked).toBe(true);
await wrapper.find("#machine-status-20-22").setValue(true);
await flushAll();
expect(setMachineStatusEnabledMock).toHaveBeenCalledWith(22, true);
await wrapper.find("#dognvask-20-21").setValue(false);
await flushAll();
expect(setLaneSelfServeEnabledMock).toHaveBeenCalledWith(21, false);
expect(wrapper.find("#dognvask-20-21").element.checked).toBe(false);
await wrapper.find("#dognvask-20-22").setValue(false);
await flushAll();
expect(setLaneSelfServeEnabledMock).toHaveBeenCalledWith(22, false);
expect(setLaneSelfServeEnabledMock).toHaveBeenCalledTimes(2);
expect(wrapper.find("#dognvask-20-22").element.checked).toBe(false);
await wrapper.find("#dognvask-20-22").setValue(true);
await flushAll();
expect(setLaneSelfServeEnabledMock).toHaveBeenCalledTimes(2);
expect(wrapper.find("#dognvask-20-22").element.checked).toBe(false);
});
it("disables lane mutation toggles for users without lane mutation permissions", async () => {
hasPermissionMock.mockImplementation((permission) => permission === "list_department_daily_reports");
getLaneStatusTogglesMock.mockResolvedValueOnce({
data: {
data: [
{
id: 21,
department: 20,
name: "T1",
status: "AVAILABLE",
machine_status_enabled: true,
selfserve_enabled: true,
relay_in_id: "in-1",
relay_out_id: "out-1",
relay_machine_id: "machine-1",
relay_machine_program_picker_id: "picker-1",
relay_machine_cleaner_id: "cleaner-1",
dynamic_image_id: 1,
machine_type_id: 1,
dognvask_configured: true,
dognvask_configuration_warnings: [],
},
],
},
});
const wrapper = mount(DepartmentDailyReportSmall, {
props: {
department_id: 20,
},
global: {
mocks: {
$t: (value) => value,
},
stubs: {
BLoading: true,
},
},
});
await flushAll();
await flushAll();
const machineToggle = wrapper.find("#machine-status-20-21");
const dognvaskToggle = wrapper.find("#dognvask-20-21");
expect(machineToggle.element.disabled).toBe(true);
expect(dognvaskToggle.element.disabled).toBe(true);
await machineToggle.trigger("change");
await dognvaskToggle.trigger("change");
await flushAll();
expect(setMachineStatusEnabledMock).not.toHaveBeenCalled();
expect(setLaneSelfServeEnabledMock).not.toHaveBeenCalled();
});
it("does not allow wash-lane list permission to mutate lane toggles", async () => {
hasPermissionMock.mockImplementation((permission) => permission === "list_department_wash_lanes");
getLaneStatusTogglesMock.mockResolvedValueOnce({
data: {
data: [
{
id: 21,
department: 20,
name: "T1",
status: "AVAILABLE",
machine_status_enabled: true,
selfserve_enabled: true,
relay_in_id: "in-1",
relay_out_id: "out-1",
relay_machine_id: "machine-1",
relay_machine_program_picker_id: "picker-1",
relay_machine_cleaner_id: "cleaner-1",
dynamic_image_id: 1,
machine_type_id: 1,
dognvask_configured: true,
dognvask_configuration_warnings: [],
},
],
},
});
const wrapper = mount(DepartmentDailyReportSmall, {
props: {
department_id: 20,
},
global: {
mocks: {
$t: (value) => value,
},
stubs: {
BLoading: true,
},
},
});
await flushAll();
await flushAll();
const machineToggle = wrapper.find("#machine-status-20-21");
const dognvaskToggle = wrapper.find("#dognvask-20-21");
expect(machineToggle.element.disabled).toBe(true);
expect(dognvaskToggle.element.disabled).toBe(true);
await machineToggle.trigger("change");
await dognvaskToggle.trigger("change");
await flushAll();
expect(setMachineStatusEnabledMock).not.toHaveBeenCalled();
expect(setLaneSelfServeEnabledMock).not.toHaveBeenCalled();
});
});