Merge pull request #115 from copenhagentruckwash/update-userdashboard-to-inspect-lane-readiness
Guard wash entry self-serve CTA
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { computed, onMounted } from "vue";
|
||||
import { BLoading } from "buefy";
|
||||
import { BLoading, BMessage } from "buefy";
|
||||
import UserDashboardPageWrapper from "@/views/dashboards/userDashboard/UserDashboardPageWrapper.vue";
|
||||
import PosDepartmentStepMobile1Location from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile1Location.vue";
|
||||
import WhiteBoxCard from "@/components/displays/boxes/WhiteBoxCard.vue";
|
||||
@@ -14,12 +14,58 @@ const {
|
||||
fetchDepartments,
|
||||
evaluateLocationDepartments,
|
||||
orderDepartmentsByDistance,
|
||||
} = useWashDepartments();
|
||||
selectDepartment,
|
||||
} = useWashDepartments({ includeLanes: true });
|
||||
|
||||
const orderedDepartments = computed(() => orderDepartmentsByDistance(guestDepartments.value));
|
||||
const hasLocationCoordinates = computed(() => !!locations.location.value?.coords);
|
||||
const shouldShowChooseDepartmentMessage = computed(() => !hasLocationCoordinates.value && !nearestDepartment.value);
|
||||
|
||||
const isDepartmentSelfServeEnabled = (department) => department?.self_serve_enabled === true;
|
||||
|
||||
const isLaneSelfServeEnabled = (lane) =>
|
||||
!(
|
||||
lane?.selfserve_enabled === false ||
|
||||
lane?.selfserve_enabled === 0 ||
|
||||
lane?.selfserve_enabled === "0" ||
|
||||
["false", "off", "no"].includes(
|
||||
String(lane?.selfserve_enabled ?? "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
)
|
||||
);
|
||||
|
||||
const isLaneReadyForSelfServe = (lane) => lane?.status === "AVAILABLE" && isLaneSelfServeEnabled(lane);
|
||||
|
||||
const canDepartmentStartSelfServe = (department) =>
|
||||
isDepartmentSelfServeEnabled(department) && (department?.lanes || []).some(isLaneReadyForSelfServe);
|
||||
|
||||
const nearestDepartmentCanStartSelfServe = computed(() => canDepartmentStartSelfServe(nearestDepartment.value));
|
||||
|
||||
const eligibleDepartments = computed(() => guestDepartments.value.filter(canDepartmentStartSelfServe));
|
||||
|
||||
const eligibleAlternativeDepartments = computed(() =>
|
||||
eligibleDepartments.value.filter((department) => department.id !== nearestDepartment.value?.id)
|
||||
);
|
||||
|
||||
const hasEligibleAlternativeDepartment = computed(() => eligibleAlternativeDepartments.value.length > 0);
|
||||
|
||||
const selfServeUnavailableMessage = computed(() => {
|
||||
if (!nearestDepartment.value) {
|
||||
return "Vi kan ikke finde din nærmeste afdeling uden en placering. Vælg en selvvask-klar afdeling nedenfor.";
|
||||
}
|
||||
|
||||
if (!isDepartmentSelfServeEnabled(nearestDepartment.value)) {
|
||||
return `${nearestDepartment.value.name} er ikke aktiveret til selvvask lige nu.`;
|
||||
}
|
||||
|
||||
if (!(nearestDepartment.value.lanes || []).some(isLaneReadyForSelfServe)) {
|
||||
return `${nearestDepartment.value.name} har ingen ledige selvvask-baner lige nu.`;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
const getDepartmentDistance = (department) => {
|
||||
if (!locations.location.value?.coords) {
|
||||
return null;
|
||||
@@ -83,13 +129,22 @@ onMounted(async () => {
|
||||
<template #footer>
|
||||
<div class="card-footer-item">
|
||||
<router-link
|
||||
v-if="nearestDepartment"
|
||||
v-if="nearestDepartment && nearestDepartmentCanStartSelfServe"
|
||||
to="/user/wash/start"
|
||||
class="button is-link is-fullwidth"
|
||||
data-testid="self-serve-home-start"
|
||||
>
|
||||
{{ $t("user_dashboard.wash.start_wash") }}
|
||||
</router-link>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="button is-link is-light is-fullwidth"
|
||||
data-testid="self-serve-home-start-disabled"
|
||||
disabled
|
||||
>
|
||||
Selvvask er ikke tilgængelig her
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</WhiteBoxCard>
|
||||
@@ -117,7 +172,17 @@ onMounted(async () => {
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="card-footer-item">
|
||||
<button
|
||||
v-if="canDepartmentStartSelfServe(department) && !nearestDepartmentCanStartSelfServe"
|
||||
type="button"
|
||||
class="button is-link is-fullwidth"
|
||||
:data-testid="`self-serve-home-select-department-${department.id}`"
|
||||
@click="selectDepartment(department.id)"
|
||||
>
|
||||
Vælg denne afdeling
|
||||
</button>
|
||||
<router-link
|
||||
v-else
|
||||
:to="{ name: 'pos', params: { departmentId: department.id } }"
|
||||
class="button is-light is-fullwidth"
|
||||
>
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
// @vitest-environment jsdom
|
||||
import { flushPromises } from "@vue/test-utils";
|
||||
import { nextTick } from "vue";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { mountWithApp } from "./helpers/mountWithApp.js";
|
||||
|
||||
const mocks = vi.hoisted(() => {
|
||||
const asMockRef = (value) => ({ value, __v_isRef: true });
|
||||
|
||||
return {
|
||||
guestDepartments: asMockRef([]),
|
||||
nearestDepartment: asMockRef(null),
|
||||
fetchDepartments: vi.fn(async () => []),
|
||||
evaluateLocationDepartments: vi.fn(),
|
||||
orderDepartmentsByDistance: vi.fn((departments) => departments),
|
||||
selectDepartment: vi.fn(),
|
||||
useWashDepartments: vi.fn(),
|
||||
locationRef: asMockRef({
|
||||
coords: {
|
||||
latitude: 55.5,
|
||||
longitude: 12.4,
|
||||
},
|
||||
}),
|
||||
getDistance: vi.fn(() => 3),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@/composables/useWashDepartments", () => ({
|
||||
useWashDepartments: mocks.useWashDepartments,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/displays/department/pos/steps/mobile/objects/PosDepartmentStepMobileFlow.vue", () => ({
|
||||
locations: {
|
||||
location: mocks.locationRef,
|
||||
getDistance: mocks.getDistance,
|
||||
},
|
||||
}));
|
||||
|
||||
import MyWash from "@/views/dashboards/userDashboard/wash/MyWash.vue";
|
||||
|
||||
const eligibleLane = { id: 10, name: "10", status: "AVAILABLE", selfserve_enabled: true };
|
||||
|
||||
const makeDepartment = (overrides = {}) => ({
|
||||
id: 1,
|
||||
name: "Glostrup",
|
||||
address: "Fabriksparken 18",
|
||||
latitude: 55.5,
|
||||
longitude: 12.4,
|
||||
distance: 1,
|
||||
self_serve_enabled: true,
|
||||
lanes: [eligibleLane],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const mountMyWash = async () => {
|
||||
const wrapper = mountWithApp(MyWash, {
|
||||
global: {
|
||||
stubs: {
|
||||
PosDepartmentStepMobile1Location: {
|
||||
emits: ["location-updated"],
|
||||
template: "<button data-testid='location-stub' @click=\"$emit('location-updated', {})\">location</button>",
|
||||
},
|
||||
BLoading: {
|
||||
props: ["modelValue"],
|
||||
template: "<div v-if='modelValue' data-testid='loading-stub' />",
|
||||
},
|
||||
BMessage: {
|
||||
template: "<div class='b-message-stub' v-bind='$attrs'><slot /></div>",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
await nextTick();
|
||||
|
||||
return wrapper;
|
||||
};
|
||||
|
||||
describe("MyWash entry", () => {
|
||||
beforeEach(() => {
|
||||
mocks.guestDepartments.value = [];
|
||||
mocks.nearestDepartment.value = null;
|
||||
mocks.locationRef.value = {
|
||||
coords: {
|
||||
latitude: 55.5,
|
||||
longitude: 12.4,
|
||||
},
|
||||
};
|
||||
mocks.fetchDepartments.mockReset();
|
||||
mocks.fetchDepartments.mockImplementation(async () => mocks.guestDepartments.value);
|
||||
mocks.evaluateLocationDepartments.mockReset();
|
||||
mocks.orderDepartmentsByDistance.mockReset();
|
||||
mocks.orderDepartmentsByDistance.mockImplementation((departments) => departments);
|
||||
mocks.selectDepartment.mockReset();
|
||||
mocks.selectDepartment.mockImplementation((departmentId) => {
|
||||
mocks.nearestDepartment.value =
|
||||
mocks.guestDepartments.value.find((department) => department.id === departmentId) || null;
|
||||
});
|
||||
mocks.useWashDepartments.mockReset();
|
||||
mocks.useWashDepartments.mockImplementation(() => ({
|
||||
guestDepartments: mocks.guestDepartments,
|
||||
nearestDepartment: mocks.nearestDepartment,
|
||||
fetchDepartments: mocks.fetchDepartments,
|
||||
evaluateLocationDepartments: mocks.evaluateLocationDepartments,
|
||||
orderDepartmentsByDistance: mocks.orderDepartmentsByDistance,
|
||||
selectDepartment: mocks.selectDepartment,
|
||||
}));
|
||||
mocks.getDistance.mockReset();
|
||||
mocks.getDistance.mockReturnValue(3);
|
||||
});
|
||||
|
||||
it("enables the start CTA when the nearest department is self-serve enabled with an available lane", async () => {
|
||||
const nearest = makeDepartment();
|
||||
mocks.guestDepartments.value = [nearest];
|
||||
mocks.nearestDepartment.value = nearest;
|
||||
|
||||
const wrapper = await mountMyWash();
|
||||
|
||||
expect(mocks.useWashDepartments).toHaveBeenCalledWith({ includeLanes: true });
|
||||
expect(wrapper.find('[data-testid="self-serve-home-start"]').exists()).toBe(true);
|
||||
expect(wrapper.find('[data-testid="self-serve-home-start"]').attributes("href")).toBe("/user/wash/start");
|
||||
expect(wrapper.find('[data-testid="self-serve-home-start-disabled"]').exists()).toBe(false);
|
||||
expect(wrapper.find('[data-testid="self-serve-home-unavailable-message"]').exists()).toBe(false);
|
||||
});
|
||||
|
||||
it("replaces the start CTA when the nearest department is disabled and lets users select another eligible department", async () => {
|
||||
const nearest = makeDepartment({ self_serve_enabled: false });
|
||||
const alternative = makeDepartment({ id: 2, name: "Roskilde", distance: 5 });
|
||||
mocks.guestDepartments.value = [nearest, alternative];
|
||||
mocks.nearestDepartment.value = nearest;
|
||||
|
||||
const wrapper = await mountMyWash();
|
||||
|
||||
expect(wrapper.find('[data-testid="self-serve-home-start"]').exists()).toBe(false);
|
||||
expect(wrapper.find('[data-testid="self-serve-home-start-disabled"]').exists()).toBe(true);
|
||||
expect(wrapper.find('[data-testid="self-serve-home-unavailable-message"]').text()).toContain(
|
||||
"Glostrup er ikke aktiveret til selvvask lige nu."
|
||||
);
|
||||
|
||||
await wrapper.find('[data-testid="self-serve-home-select-department-2"]').trigger("click");
|
||||
await nextTick();
|
||||
|
||||
expect(mocks.selectDepartment).toHaveBeenCalledWith(2);
|
||||
});
|
||||
|
||||
it("replaces the start CTA when the nearest department has no enabled and available lanes", async () => {
|
||||
const nearest = makeDepartment({
|
||||
lanes: [
|
||||
{ id: 10, name: "10", status: "MAINTENANCE", selfserve_enabled: true },
|
||||
{ id: 11, name: "11", status: "AVAILABLE", selfserve_enabled: false },
|
||||
],
|
||||
});
|
||||
const alternative = makeDepartment({ id: 3, name: "Køge", distance: 7 });
|
||||
mocks.guestDepartments.value = [nearest, alternative];
|
||||
mocks.nearestDepartment.value = nearest;
|
||||
|
||||
const wrapper = await mountMyWash();
|
||||
|
||||
expect(wrapper.find('[data-testid="self-serve-home-start"]').exists()).toBe(false);
|
||||
expect(wrapper.find('[data-testid="self-serve-home-start-disabled"]').exists()).toBe(true);
|
||||
expect(wrapper.find('[data-testid="self-serve-home-unavailable-message"]').text()).toContain(
|
||||
"Glostrup har ingen ledige selvvask-baner lige nu."
|
||||
);
|
||||
expect(wrapper.find('[data-testid="self-serve-home-select-department-3"]').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("handles missing geolocation by disabling start and offering eligible departments", async () => {
|
||||
const eligible = makeDepartment({ id: 4, name: "Odense" });
|
||||
mocks.locationRef.value = null;
|
||||
mocks.guestDepartments.value = [eligible];
|
||||
mocks.nearestDepartment.value = null;
|
||||
|
||||
const wrapper = await mountMyWash();
|
||||
|
||||
expect(wrapper.find('[data-testid="self-serve-home-start"]').exists()).toBe(false);
|
||||
expect(wrapper.find('[data-testid="self-serve-home-start-disabled"]').exists()).toBe(true);
|
||||
expect(wrapper.find('[data-testid="self-serve-home-unavailable-message"]').text()).toContain(
|
||||
"Vi kan ikke finde din nærmeste afdeling uden en placering."
|
||||
);
|
||||
expect(wrapper.find('[data-testid="self-serve-home-select-department-4"]').exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user