diff --git a/src/composables/useWashProgress.js b/src/composables/useWashProgress.js index 16f952bb..b472fe07 100644 --- a/src/composables/useWashProgress.js +++ b/src/composables/useWashProgress.js @@ -165,8 +165,17 @@ export function useWashProgress(options) { } const restoredStep = savedProgress.currentStep; + const isSameRestoredProgress = () => ( + Boolean(washInProgress.value) === Boolean(savedProgress.washInProgress) && + String(washLaneId.value ?? "") === String(savedProgress.washLaneId ?? "") && + String(washStartTime.value ?? "") === String(savedProgress.washStartTime ?? "") && + String(licensePlateInput.value ?? "") === String(savedProgress.licensePlateInput ?? "") && + String(vehicleTypeSelect.value ?? "") === String(savedProgress.vehicleTypeSelect ?? "") && + String(radioLaneOption.value ?? "") === String(savedProgress.radioLaneOption ?? "") + ); + setTimeout(() => { - if (currentStep.value !== restoredStep) { + if (isSameRestoredProgress() && currentStep.value !== restoredStep) { currentStep.value = restoredStep; } isRestoring.value = false; diff --git a/src/views/dashboards/superUserDashboard/collectedOrderInvoices/CollectedOrderInvoicesQueueHistory.vue b/src/views/dashboards/superUserDashboard/collectedOrderInvoices/CollectedOrderInvoicesQueueHistory.vue index c5b39074..9fdc9e22 100644 --- a/src/views/dashboards/superUserDashboard/collectedOrderInvoices/CollectedOrderInvoicesQueueHistory.vue +++ b/src/views/dashboards/superUserDashboard/collectedOrderInvoices/CollectedOrderInvoicesQueueHistory.vue @@ -35,6 +35,35 @@ const selectedJob = ref(null); let pollTimer = null; let loadPromise = null; let isMounted = false; +let pendingLoadAfterCurrent = false; +let pendingLoadSilent = true; +let pendingLoadIsPoll = true; + +const queuePendingLoad = ({ silent = false, isPoll = false } = {}) => { + pendingLoadAfterCurrent = true; + pendingLoadSilent = pendingLoadSilent && silent; + pendingLoadIsPoll = pendingLoadIsPoll && isPoll; +}; + +const takePendingLoad = () => { + if (!pendingLoadAfterCurrent || !isMounted) { + pendingLoadAfterCurrent = false; + pendingLoadSilent = true; + pendingLoadIsPoll = true; + return null; + } + + const pendingLoad = { + silent: pendingLoadSilent, + isPoll: pendingLoadIsPoll, + }; + + pendingLoadAfterCurrent = false; + pendingLoadSilent = true; + pendingLoadIsPoll = true; + + return pendingLoad; +}; const toPositiveInt = (value, fallback = null) => { const parsed = Number.parseInt(String(value), 10); @@ -357,6 +386,7 @@ const startPolling = () => { const loadQueueHistory = async ({ silent = false, isPoll = false } = {}) => { if (loadPromise) { + queuePendingLoad({ silent, isPoll }); return loadPromise; } @@ -409,6 +439,11 @@ const loadQueueHistory = async ({ silent = false, isPoll = false } = {}) => { loading.value = false; refreshing.value = false; loadPromise = null; + + const pendingLoad = takePendingLoad(); + if (pendingLoad) { + void loadQueueHistory(pendingLoad); + } } })(); diff --git a/src/views/dashboards/userDashboard/wash/MyWashStart.vue b/src/views/dashboards/userDashboard/wash/MyWashStart.vue index 4b39e364..0a9cfa34 100644 --- a/src/views/dashboards/userDashboard/wash/MyWashStart.vue +++ b/src/views/dashboards/userDashboard/wash/MyWashStart.vue @@ -886,7 +886,7 @@ watch(() => licensePlateInput.value, (newValue, oldValue) => { }); watch(() => currentStep.value, (newStep, oldStep) => { - if (isRestoring.value && newStep === 0 && oldStep !== 0 && typeof oldStep !== "undefined") { + if (isRestoring.value && washInProgress.value && newStep === steps.VEHICLE && oldStep !== steps.VEHICLE && typeof oldStep !== "undefined") { currentStep.value = oldStep; return; } diff --git a/tests/e2e/auth.smoke.spec.js b/tests/e2e/auth.smoke.spec.js index 72f1f2c9..9a9404aa 100644 --- a/tests/e2e/auth.smoke.spec.js +++ b/tests/e2e/auth.smoke.spec.js @@ -1,21 +1,25 @@ import { expect, test } from "@playwright/test"; import { mockApi } from "./support/network.js"; +const AUTH_ENTRY_TIMEOUT = 60_000; + test.describe("Auth entry smoke", () => { + test.describe.configure({ timeout: 90_000 }); + test("@smoke protected routes redirect to login without token", async ({ page }) => { await mockApi(page); await page.goto("/user"); - await expect(page).toHaveURL(/\/login/); - await expect(page.getByTestId("login-submit")).toBeVisible(); + await expect(page).toHaveURL(/\/login/, { timeout: AUTH_ENTRY_TIMEOUT }); + await expect(page.getByTestId("login-submit")).toBeVisible({ timeout: AUTH_ENTRY_TIMEOUT }); }); test("@smoke login route renders credential controls", async ({ page }) => { await mockApi(page, { authenticated: false }); await page.goto("/login"); - await expect(page.getByTestId("login-customer-number")).toBeVisible(); - await expect(page.getByTestId("login-password")).toBeVisible(); - await expect(page.getByTestId("login-submit")).toBeVisible(); + await expect(page.getByTestId("login-customer-number")).toBeVisible({ timeout: AUTH_ENTRY_TIMEOUT }); + await expect(page.getByTestId("login-password")).toBeVisible({ timeout: AUTH_ENTRY_TIMEOUT }); + await expect(page.getByTestId("login-submit")).toBeVisible({ timeout: AUTH_ENTRY_TIMEOUT }); }); test("@smoke qr login route renders without QR bootstrap import errors", async ({ page }) => { diff --git a/tests/e2e/self-serve-wash.spec.js b/tests/e2e/self-serve-wash.spec.js index 71fe3bf6..f9eb8a54 100644 --- a/tests/e2e/self-serve-wash.spec.js +++ b/tests/e2e/self-serve-wash.spec.js @@ -227,7 +227,8 @@ test.describe("Self-serve wash", () => { await expect(page.getByTestId("self-serve-completed-step")).toBeVisible(); await page.getByTestId("self-serve-nav-close").click(); await expect(page.getByTestId("self-serve-completed-step")).toBeHidden(); - await expect(page.getByTestId("self-serve-nav-complete")).toBeVisible(); + await expect(page.getByTestId("self-serve-vehicle-step")).toBeVisible(); + await expect(page.getByTestId("self-serve-nav-complete")).toBeHidden(); }); test("start route resumes the authenticated customer's active server wash from another device", async ({ page }) => { diff --git a/tests/unit/department-daily-report-page.behavior.spec.js b/tests/unit/department-daily-report-page.behavior.spec.js index a74422f7..0c768d04 100644 --- a/tests/unit/department-daily-report-page.behavior.spec.js +++ b/tests/unit/department-daily-report-page.behavior.spec.js @@ -39,8 +39,10 @@ vi.mock("vue-i18n", async (importOriginal) => { vi.mock("@/components/session/token/SessionUser.vue", () => ({ SessionUser: { canAccessAdmin: () => true, + canAccessAssignedDepartment: (departmentId) => [1, 2].includes(Number(departmentId)), canAccessDepartment: (departmentId) => [1, 2].includes(Number(departmentId)), functions: { + getAccessibleDepartments: () => [1, 2], getDepartmentIdFromUrl: () => 1, date: { isToday: () => false, diff --git a/tests/unit/use-wash-progress.spec.js b/tests/unit/use-wash-progress.spec.js new file mode 100644 index 00000000..4c9d83aa --- /dev/null +++ b/tests/unit/use-wash-progress.spec.js @@ -0,0 +1,117 @@ +// @vitest-environment jsdom +import { mount } from "@vue/test-utils"; +import { ref } from "vue"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { useWashProgress } from "@/composables/useWashProgress.js"; + +const buildSavedProgress = (overrides = {}) => ({ + washInProgress: true, + washLaneId: 7, + washStartTime: Date.now() - 30_000, + currentStep: 4, + licensePlateInput: "AB12345", + vehicleTypeSelect: 2, + radioWashType: "Manual", + radioLaneOption: 7, + customerNumberInput: "12345679", + answers: {}, + completedTasks: {}, + nearestDepartmentId: 6, + forceNearestDepartmentEvaluationId: 0, + savedAt: Date.now(), + isForcingNearestDepartment: false, + ...overrides, +}); + +const createProgress = () => { + const state = { + currentStep: ref(0), + washInProgress: ref(false), + washLaneId: ref(null), + washStartTime: ref(null), + licensePlateInput: ref(null), + vehicleTypeSelect: ref(null), + radioWashType: ref("Manual"), + radioLaneOption: ref("Any"), + customerNumberInput: ref(null), + answers: ref({}), + completedTasks: ref({}), + forceNearestDepartmentEvaluationId: ref(0), + isForcingNearestDepartment: ref(false), + }; + + let progress = null; + const wrapper = mount({ + setup() { + progress = useWashProgress({ + ...state, + nearestDepartmentId: () => 6, + applyRestoredState: (savedProgress) => { + state.washInProgress.value = savedProgress.washInProgress; + state.washLaneId.value = savedProgress.washLaneId; + state.washStartTime.value = savedProgress.washStartTime; + state.currentStep.value = savedProgress.currentStep; + state.licensePlateInput.value = savedProgress.licensePlateInput; + state.vehicleTypeSelect.value = savedProgress.vehicleTypeSelect; + state.radioWashType.value = savedProgress.radioWashType; + state.radioLaneOption.value = savedProgress.radioLaneOption; + state.customerNumberInput.value = savedProgress.customerNumberInput; + state.answers.value = savedProgress.answers; + state.completedTasks.value = savedProgress.completedTasks; + state.forceNearestDepartmentEvaluationId.value = savedProgress.forceNearestDepartmentEvaluationId; + state.isForcingNearestDepartment.value = savedProgress.isForcingNearestDepartment; + }, + }); + + return {}; + }, + template: "
", + }); + + return { progress, state, wrapper }; +}; + +describe("useWashProgress", () => { + const wrappers = []; + + afterEach(() => { + wrappers.splice(0).forEach((wrapper) => wrapper.unmount()); + localStorage.clear(); + vi.useRealTimers(); + }); + + it("does not reapply a restored step after the restored wash state changes", () => { + vi.useFakeTimers(); + localStorage.setItem("mywash_progress_v6", JSON.stringify(buildSavedProgress())); + const { progress, state, wrapper } = createProgress(); + wrappers.push(wrapper); + + progress.restoreProgress(); + + state.washInProgress.value = false; + state.washLaneId.value = null; + state.washStartTime.value = null; + state.licensePlateInput.value = ""; + state.currentStep.value = 0; + + vi.advanceTimersByTime(1500); + + expect(state.currentStep.value).toBe(0); + expect(progress.isRestoring.value).toBe(false); + }); + + it("reapplies the restored step while the restored wash state is still current", () => { + vi.useFakeTimers(); + localStorage.setItem("mywash_progress_v6", JSON.stringify(buildSavedProgress())); + const { progress, state, wrapper } = createProgress(); + wrappers.push(wrapper); + + progress.restoreProgress(); + state.currentStep.value = 0; + + vi.advanceTimersByTime(1500); + + expect(state.currentStep.value).toBe(4); + expect(progress.isRestoring.value).toBe(false); + }); +});