- Implemented `goalProgressResolver` utility for resolving progress keys, slices, and periods. - Refactored `DepartmentGoals` to integrate `goalProgressResolver` for improved clarity and modularity. - Updated progress calculations with cadence-aware key handling and fallback resolution. - Add isLoading handling and loader animations to `DepartmentWeather` and improve `GET` request concurrency.
312 lines
8.9 KiB
JavaScript
312 lines
8.9 KiB
JavaScript
// @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(["yellow"]),
|
|
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"]);
|
|
});
|
|
});
|