// @vitest-environment jsdom import { mount } from "@vue/test-utils"; import { describe, expect, it, vi, beforeEach } from "vitest"; import { nextTick } from "vue"; const { getObjectsMock } = vi.hoisted(() => ({ getObjectsMock: vi.fn(), })); vi.mock("@/components/session/token/SessionUser/Objects/ObjectsGlobal.vue", () => ({ ObjectsGlobal: { get: { objects: getObjectsMock, }, }, })); import DepartmentWeather from "@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentWeather.vue"; const flushAll = async () => { await nextTick(); await Promise.resolve(); await nextTick(); await Promise.resolve(); }; const createDeferred = () => { let resolve; let reject; const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); return { promise, resolve, reject }; }; const makeEntry = (overrides = {}) => ({ date: "2026-03-24", time: "00:00", current: false, weather: "clear", washes: 1, hours: 2, status: "healthy", ...overrides, }); const getFirstWashesCellText = (wrapper) => { const rows = wrapper.findAll("tbody tr"); const washesRow = rows[1]; if (!washesRow) { return null; } const cells = washesRow.findAll("td"); return cells[1]?.text() ?? null; }; const getRenderedHours = (wrapper) => { const headers = wrapper.findAll("thead tr th").slice(1); return headers.map((th) => th.text()); }; const getRowValues = (wrapper, rowIndex) => { const rows = wrapper.findAll("tbody tr"); const row = rows[rowIndex]; if (!row) { return []; } return row .findAll("td") .slice(1) .map((td) => td.text()); }; const getProductivityCellClasses = (wrapper) => { const rows = wrapper.findAll("tbody tr"); const productivityRow = rows[3]; if (!productivityRow) { return []; } return productivityRow .findAll("td") .slice(1) .map((td) => td.classes()); }; describe("DepartmentWeather", () => { beforeEach(() => { getObjectsMock.mockReset(); }); it("calls weather endpoint with ids/date_from/date_to", async () => { getObjectsMock.mockResolvedValueOnce([makeEntry()]); mount(DepartmentWeather, { props: { department_ids: [3, 1, 3, 2], date_from: "2026-03-23", date_to: "2026-03-24", }, }); await flushAll(); expect(getObjectsMock).toHaveBeenCalledTimes(1); expect(getObjectsMock).toHaveBeenCalledWith("/departments/weather", { ids: "1,2,3", date_from: "2026-03-23", date_to: "2026-03-24", }); }); it("refetches when ids or dates change", async () => { getObjectsMock.mockResolvedValueOnce([makeEntry({ washes: 3 })]).mockResolvedValueOnce([makeEntry({ washes: 7 })]); const wrapper = mount(DepartmentWeather, { props: { department_ids: [2], date_from: "2026-03-23", date_to: "2026-03-24", }, }); await flushAll(); await wrapper.setProps({ department_ids: [2, 5], date_from: "2026-03-24", date_to: "2026-03-25", }); await flushAll(); expect(getObjectsMock).toHaveBeenCalledTimes(2); expect(getObjectsMock).toHaveBeenNthCalledWith(2, "/departments/weather", { ids: "2,5", date_from: "2026-03-24", date_to: "2026-03-25", }); }); it("clears and skips fetch when no departments are selected", async () => { getObjectsMock.mockResolvedValueOnce([makeEntry()]); const wrapper = mount(DepartmentWeather, { props: { department_ids: [9], date_from: "2026-03-23", date_to: "2026-03-24", }, }); await flushAll(); expect(wrapper.find("section.daily-metrics").exists()).toBe(true); await wrapper.setProps({ department_ids: [], }); await flushAll(); expect(getObjectsMock).toHaveBeenCalledTimes(1); expect(wrapper.find("section.daily-metrics").exists()).toBe(false); }); it("ignores stale responses when a newer request finishes first", async () => { const first = createDeferred(); const second = createDeferred(); getObjectsMock.mockImplementationOnce(() => first.promise).mockImplementationOnce(() => second.promise); const wrapper = mount(DepartmentWeather, { props: { department_ids: [1], date_from: "2026-03-23", date_to: "2026-03-24", }, }); await nextTick(); await wrapper.setProps({ date_to: "2026-03-25", }); await nextTick(); second.resolve([makeEntry({ washes: 9002, time: "01:00" })]); await flushAll(); expect(getFirstWashesCellText(wrapper)).toBe("9002"); first.resolve([makeEntry({ washes: 9001, time: "00:00" })]); await flushAll(); expect(getFirstWashesCellText(wrapper)).toBe("9002"); }); it("shows loading state while weather request is in flight", async () => { const pending = createDeferred(); getObjectsMock.mockImplementationOnce(() => pending.promise); const wrapper = mount(DepartmentWeather, { props: { department_ids: [1], date_from: "2026-03-24", date_to: "2026-03-24", }, }); await nextTick(); expect(wrapper.find(".daily-metrics-loading").exists()).toBe(true); expect(wrapper.find("table.table").exists()).toBe(false); pending.resolve([makeEntry({ time: "06:00", washes: 4, hours: 2 })]); await flushAll(); expect(wrapper.find(".daily-metrics-loading").exists()).toBe(false); expect(wrapper.find("table.table").exists()).toBe(true); }); it("hides hour columns when both washes and hours are zero", async () => { getObjectsMock.mockResolvedValueOnce([ makeEntry({ time: "00:00", washes: 0, hours: 0 }), makeEntry({ time: "01:00", washes: "0", hours: "0" }), makeEntry({ time: "06:00", washes: 1, hours: 0 }), makeEntry({ time: "07:00", washes: 0, hours: 2 }), ]); const wrapper = mount(DepartmentWeather, { props: { department_ids: [1], date_from: "2026-03-24", date_to: "2026-03-24", }, }); await flushAll(); expect(getRenderedHours(wrapper)).toEqual(["06", "07"]); }); it("shows summary toggle only for multi-day selections", async () => { getObjectsMock.mockResolvedValueOnce([makeEntry({ time: "06:00", washes: 1, hours: 1 })]); const wrapper = mount(DepartmentWeather, { props: { department_ids: [1], date_from: "2026-03-23", date_to: "2026-03-24", }, }); await flushAll(); expect(wrapper.find("button.summary-toggle").exists()).toBe(true); await wrapper.setProps({ date_to: "2026-03-23", }); await flushAll(); expect(wrapper.find("button.summary-toggle").exists()).toBe(false); }); it("summarizes entries by day when toggle is enabled", async () => { getObjectsMock.mockResolvedValueOnce([ makeEntry({ date: "2026-03-23", time: "06:00", weather: "clear", washes: 1, hours: 1, status: "healthy" }), makeEntry({ date: "2026-03-23", time: "07:00", weather: "rain", washes: 2, hours: 0, status: "degraded" }), makeEntry({ date: "2026-03-23", time: "08:00", weather: "rain", washes: 0, hours: 1, status: "degraded" }), makeEntry({ date: "2026-03-24", time: "09:00", weather: "showers", washes: 0, hours: 2, status: "healthy" }), makeEntry({ date: "2026-03-24", time: "10:00", weather: "showers", washes: 0, hours: 1, status: "degraded" }), makeEntry({ date: "2026-03-24", time: "11:00", weather: "rain", washes: 1, hours: 0, status: "unhealthy" }), ]); const wrapper = mount(DepartmentWeather, { props: { department_ids: [1], date_from: "2026-03-23", date_to: "2026-03-24", }, }); await flushAll(); expect(getRenderedHours(wrapper)).toEqual(["06", "07", "08", "09", "10", "11"]); await wrapper.get("button.summary-toggle").trigger("click"); await flushAll(); expect(getRenderedHours(wrapper)).toEqual(["23/03", "24/03"]); expect(getRowValues(wrapper, 1)).toEqual(["3", "1"]); expect(getRowValues(wrapper, 2)).toEqual(["2", "3"]); expect(getProductivityCellClasses(wrapper)).toEqual([ expect.arrayContaining(["green"]), expect.arrayContaining(["red"]), ]); }); it("starts in hourly mode and lets users toggle to daily summary in multi-day mode", async () => { getObjectsMock.mockResolvedValueOnce([ makeEntry({ date: "2026-03-23", time: "06:00", washes: 1, hours: 1 }), makeEntry({ date: "2026-03-23", time: "07:00", washes: 2, hours: 2 }), makeEntry({ date: "2026-03-24", time: "08:00", washes: 3, hours: 3 }), ]); const wrapper = mount(DepartmentWeather, { props: { department_ids: [1], date_from: "2026-03-23", date_to: "2026-03-24", }, }); await flushAll(); expect(getRenderedHours(wrapper)).toEqual(["06", "07", "08"]); await wrapper.get("button.summary-toggle").trigger("click"); await flushAll(); expect(getRenderedHours(wrapper)).toEqual(["23/03", "24/03"]); }); });