Fix frontend smoke test races
This commit is contained in:
@@ -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;
|
||||
|
||||
+35
@@ -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);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: "<div />",
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user