Enhance self-serve functionality with dynamic images, gate handling, and machine-scoped vehicle type filtering:
- Added dynamic image previews with query parameter handling and error recovery in `SelfServeTryModal.vue`. - Introduced outside gate controls (`Entrance` and `Exit`) in `SelfServeMachineRelayControls.vue` with corresponding command handling logic. - Enhanced vehicle type scoping logic in `SelfServeTasksPagination.vue` and `SelfServeConditionsPagination.vue` to ensure machine-type filtering consistency. - Updated related unit tests to verify new dynamic image logic, gate controls, and scoping behaviors.
This commit is contained in:
@@ -6,6 +6,7 @@ import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
|||||||
import { useSelfServeLogic } from "@/composables/useSelfServeLogic";
|
import { useSelfServeLogic } from "@/composables/useSelfServeLogic";
|
||||||
import SelfServeQuestionCards from "@/components/displays/selfServe/SelfServeQuestionCards.vue";
|
import SelfServeQuestionCards from "@/components/displays/selfServe/SelfServeQuestionCards.vue";
|
||||||
import SelfServeTaskList from "@/components/displays/selfServe/SelfServeTaskList.vue";
|
import SelfServeTaskList from "@/components/displays/selfServe/SelfServeTaskList.vue";
|
||||||
|
import { API_URL } from "@/config";
|
||||||
|
|
||||||
const { t: $t } = useI18n();
|
const { t: $t } = useI18n();
|
||||||
|
|
||||||
@@ -133,6 +134,79 @@ const canClearAnswers = computed(() => (
|
|||||||
&& normalizedReg.value.length >= 2
|
&& normalizedReg.value.length >= 2
|
||||||
&& !loading.value
|
&& !loading.value
|
||||||
));
|
));
|
||||||
|
const hideDynamicImage = ref(false);
|
||||||
|
|
||||||
|
const parseNonNegativeInt = (value) => {
|
||||||
|
const parsed = parseInt(value);
|
||||||
|
if (Number.isNaN(parsed) || parsed < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
};
|
||||||
|
|
||||||
|
const dynamicImageButtons = computed(() => {
|
||||||
|
const buttonIds = new Set();
|
||||||
|
|
||||||
|
activeTasks.value.forEach((task) => {
|
||||||
|
if (!Array.isArray(task?.buttons)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
task.buttons.forEach((button) => {
|
||||||
|
const buttonId = parseInt(button);
|
||||||
|
if (!Number.isNaN(buttonId) && buttonId >= 0 && buttonId <= 11) {
|
||||||
|
buttonIds.add(buttonId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return [...buttonIds];
|
||||||
|
});
|
||||||
|
|
||||||
|
const dynamicImageThumbPosition = computed(() => {
|
||||||
|
for (const task of activeTasks.value) {
|
||||||
|
const parsedPosition = parseNonNegativeInt(task?.dynamic_images_vehicle_type);
|
||||||
|
if (parsedPosition !== null) {
|
||||||
|
return parsedPosition;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasDynamicImageContext = computed(() => (
|
||||||
|
dynamicImageButtons.value.length > 0 || dynamicImageThumbPosition.value !== null
|
||||||
|
));
|
||||||
|
|
||||||
|
const dynamicImageUrl = computed(() => {
|
||||||
|
const departmentId = parseInt(props.departmentId);
|
||||||
|
const laneId = parseInt(selectedLaneId.value);
|
||||||
|
if (Number.isNaN(departmentId) || departmentId <= 0 || Number.isNaN(laneId) || laneId <= 0 || !hasDynamicImageContext.value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
department: String(departmentId),
|
||||||
|
lane: String(laneId),
|
||||||
|
current_step: "0",
|
||||||
|
buttons: JSON.stringify(dynamicImageButtons.value),
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedVehicleType = parseInt(selectedVehicleTypeId.value);
|
||||||
|
if (!Number.isNaN(selectedVehicleType) && selectedVehicleType > 0) {
|
||||||
|
params.set("vehicle_type", String(selectedVehicleType));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dynamicImageThumbPosition.value !== null) {
|
||||||
|
params.set("thumb_position", String(dynamicImageThumbPosition.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${API_URL}/department/lanes/dynamic-image?${params.toString()}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const displayedDynamicImageUrl = computed(() => (
|
||||||
|
hideDynamicImage.value ? null : dynamicImageUrl.value
|
||||||
|
));
|
||||||
|
|
||||||
const loadLanes = async () => {
|
const loadLanes = async () => {
|
||||||
const lanes = await SessionUser.objects.department_lanes.get.all();
|
const lanes = await SessionUser.objects.department_lanes.get.all();
|
||||||
@@ -215,6 +289,10 @@ const closeModal = () => {
|
|||||||
emit("closeModal");
|
emit("closeModal");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onDynamicImageError = () => {
|
||||||
|
hideDynamicImage.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadLanes();
|
await loadLanes();
|
||||||
await loadVehicleTypes();
|
await loadVehicleTypes();
|
||||||
@@ -232,6 +310,10 @@ watch(() => [selectedLaneId.value, reg.value, normalizeVehicleTypeId(selectedVeh
|
|||||||
|
|
||||||
await refresh();
|
await refresh();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(dynamicImageUrl, () => {
|
||||||
|
hideDynamicImage.value = false;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -338,6 +420,16 @@ watch(() => [selectedLaneId.value, reg.value, normalizeVehicleTypeId(selectedVeh
|
|||||||
<div class="box" style="height: 100%">
|
<div class="box" style="height: 100%">
|
||||||
<h4 class="title is-5">Tasks og session</h4>
|
<h4 class="title is-5">Tasks og session</h4>
|
||||||
|
|
||||||
|
<div v-if="displayedDynamicImageUrl" class="mb-4">
|
||||||
|
<img
|
||||||
|
:src="displayedDynamicImageUrl"
|
||||||
|
alt="Machine status preview"
|
||||||
|
data-testid="self-serve-try-dynamic-image"
|
||||||
|
style="max-width: 100%; height: auto; border-radius: 4px; display: block; margin-left: auto; margin-right: auto;"
|
||||||
|
@error="onDynamicImageError"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<SelfServeTaskList
|
<SelfServeTaskList
|
||||||
:tasks="activeTasks"
|
:tasks="activeTasks"
|
||||||
:completedTasks="emptyCompletedTasks"
|
:completedTasks="emptyCompletedTasks"
|
||||||
|
|||||||
+8
-2
@@ -76,8 +76,14 @@ const sortedList = computed(() => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (vehicleTypeFilter && itemMachineType === 0 && itemProduct !== 0 && itemProduct !== vehicleTypeFilter) {
|
if (vehicleTypeFilter) {
|
||||||
return false;
|
if (itemMachineType !== 0) {
|
||||||
|
if (itemMachineType !== vehicleTypeFilter) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (itemProduct !== 0 && itemProduct !== vehicleTypeFilter) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (conditionFilter && itemCondition !== conditionFilter) {
|
if (conditionFilter && itemCondition !== conditionFilter) {
|
||||||
|
|||||||
+8
-2
@@ -82,8 +82,14 @@ const sortedList = computed(() => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (vehicleTypeFilter && itemMachineType === 0 && itemProduct !== 0 && itemProduct !== vehicleTypeFilter) {
|
if (vehicleTypeFilter) {
|
||||||
return false;
|
if (itemMachineType !== 0) {
|
||||||
|
if (itemMachineType !== vehicleTypeFilter) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (itemProduct !== 0 && itemProduct !== vehicleTypeFilter) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (conditionFilter && itemCondition !== conditionFilter) {
|
if (conditionFilter && itemCondition !== conditionFilter) {
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export function useWashSessionActions(options) {
|
|||||||
|
|
||||||
activeTasks.value.forEach((task) => {
|
activeTasks.value.forEach((task) => {
|
||||||
if (completedTasks.value[task.id]) {
|
if (completedTasks.value[task.id]) {
|
||||||
return;
|
//return;
|
||||||
}
|
}
|
||||||
|
|
||||||
(task.buttons || []).forEach((button) => {
|
(task.buttons || []).forEach((button) => {
|
||||||
|
|||||||
+3
-2
@@ -297,8 +297,9 @@ const matchesScopedProduct = (item) => {
|
|||||||
if (!scopedProductId.value) {
|
if (!scopedProductId.value) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (parseNullableInt(item.machine_type_id)) {
|
const machineTypeId = parseNullableInt(item.machine_type_id);
|
||||||
return true;
|
if (machineTypeId) {
|
||||||
|
return machineTypeId === scopedProductId.value;
|
||||||
}
|
}
|
||||||
const product = parseIntOrZero(item.product);
|
const product = parseIntOrZero(item.product);
|
||||||
return product === 0 || product === scopedProductId.value;
|
return product === 0 || product === scopedProductId.value;
|
||||||
|
|||||||
+1
-1
@@ -15,7 +15,7 @@ export type RelayStatus = {
|
|||||||
online: boolean;
|
online: boolean;
|
||||||
on: boolean;
|
on: boolean;
|
||||||
};
|
};
|
||||||
export type LaneCommand = "START" | "STOP" | "RESET" | "RESERVE" | "RELEASE";
|
export type LaneCommand = "START" | "STOP" | "RESET" | "RESERVE" | "RELEASE" | "OPEN_PROPERTY_ACCESS_GATE" | "OPEN_PROPERTY_EXIT_GATE";
|
||||||
export type LaneGate = "ENTRANCE" | "EXIT";
|
export type LaneGate = "ENTRANCE" | "EXIT";
|
||||||
export type LaneCommandPayload = {
|
export type LaneCommandPayload = {
|
||||||
customer_number?: number | null;
|
customer_number?: number | null;
|
||||||
|
|||||||
+30
@@ -23,6 +23,8 @@ type LaneCommandLoadingState = {
|
|||||||
forceDisable: boolean;
|
forceDisable: boolean;
|
||||||
openEntranceGate: boolean;
|
openEntranceGate: boolean;
|
||||||
openExitGate: boolean;
|
openExitGate: boolean;
|
||||||
|
openOutsideEntranceGate: boolean;
|
||||||
|
openOutsideExitGate: boolean;
|
||||||
};
|
};
|
||||||
type RelayDisplayStatus = "ON" | "OFF" | "OFFLINE" | "MAINTENANCE";
|
type RelayDisplayStatus = "ON" | "OFF" | "OFFLINE" | "MAINTENANCE";
|
||||||
type ForceEnableOptions = {
|
type ForceEnableOptions = {
|
||||||
@@ -35,6 +37,7 @@ type InProgressWashDetails = {
|
|||||||
session: Record<string, unknown> | null;
|
session: Record<string, unknown> | null;
|
||||||
customer: Record<string, unknown> | null;
|
customer: Record<string, unknown> | null;
|
||||||
vehicle: Record<string, unknown> | null;
|
vehicle: Record<string, unknown> | null;
|
||||||
|
wash_in_progress: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_POLL_INTERVAL_MS = 7000;
|
const DEFAULT_POLL_INTERVAL_MS = 7000;
|
||||||
@@ -68,6 +71,8 @@ const createInitialCommandLoadingState = (): LaneCommandLoadingState => ({
|
|||||||
forceDisable: false,
|
forceDisable: false,
|
||||||
openEntranceGate: false,
|
openEntranceGate: false,
|
||||||
openExitGate: false,
|
openExitGate: false,
|
||||||
|
openOutsideEntranceGate: false,
|
||||||
|
openOutsideExitGate: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const toLaneId = (laneId: number): number => {
|
const toLaneId = (laneId: number): number => {
|
||||||
@@ -221,6 +226,7 @@ const normalizeInProgressDetails = (
|
|||||||
session: null,
|
session: null,
|
||||||
customer: null,
|
customer: null,
|
||||||
vehicle: null,
|
vehicle: null,
|
||||||
|
wash_in_progress: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,6 +247,7 @@ const normalizeInProgressDetails = (
|
|||||||
session,
|
session,
|
||||||
customer,
|
customer,
|
||||||
vehicle,
|
vehicle,
|
||||||
|
wash_in_progress: Boolean(source.wash_in_progress),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -456,6 +463,27 @@ export const openGate = async (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const openOutsideGate = async (
|
||||||
|
laneId: number,
|
||||||
|
gate: "ENTRANCE" | "EXIT"
|
||||||
|
): Promise<any> => {
|
||||||
|
const normalizedLaneId = toLaneId(laneId);
|
||||||
|
const loadingKey: CommandLoadingKey = gate === "ENTRANCE"
|
||||||
|
? "openOutsideEntranceGate"
|
||||||
|
: "openOutsideExitGate";
|
||||||
|
|
||||||
|
return runLaneAction(normalizedLaneId, loadingKey, async () => {
|
||||||
|
const command = gate === "ENTRANCE"
|
||||||
|
? "OPEN_PROPERTY_ACCESS_GATE"
|
||||||
|
: "OPEN_PROPERTY_EXIT_GATE";
|
||||||
|
const response = await executeLaneCommand(normalizedLaneId, command);
|
||||||
|
return extractPayload(response);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const openOutsideEntranceGate = (laneId: number): Promise<any> => openOutsideGate(laneId, "ENTRANCE");
|
||||||
|
export const openOutsideExitGate = (laneId: number): Promise<any> => openOutsideGate(laneId, "EXIT");
|
||||||
|
|
||||||
export const openEntranceGate = (laneId: number): Promise<any> => openGate(laneId, "ENTRANCE");
|
export const openEntranceGate = (laneId: number): Promise<any> => openGate(laneId, "ENTRANCE");
|
||||||
export const openExitGate = (laneId: number): Promise<any> => openGate(laneId, "EXIT");
|
export const openExitGate = (laneId: number): Promise<any> => openGate(laneId, "EXIT");
|
||||||
|
|
||||||
@@ -550,6 +578,8 @@ export const useMachineConnectivity = (laneId: number) => {
|
|||||||
openGate: (gate: LaneGate) => openGate(normalizedLaneId, gate),
|
openGate: (gate: LaneGate) => openGate(normalizedLaneId, gate),
|
||||||
openEntranceGate: () => openEntranceGate(normalizedLaneId),
|
openEntranceGate: () => openEntranceGate(normalizedLaneId),
|
||||||
openExitGate: () => openExitGate(normalizedLaneId),
|
openExitGate: () => openExitGate(normalizedLaneId),
|
||||||
|
openOutsideEntranceGate: () => openOutsideEntranceGate(normalizedLaneId),
|
||||||
|
openOutsideExitGate: () => openOutsideExitGate(normalizedLaneId),
|
||||||
forceEnableMachine: (options: ForceEnableOptions = {}) => forceEnableMachine(normalizedLaneId, options),
|
forceEnableMachine: (options: ForceEnableOptions = {}) => forceEnableMachine(normalizedLaneId, options),
|
||||||
forceDisableMachine: (licensePlate: string | null = null) => forceDisableMachine(normalizedLaneId, licensePlate),
|
forceDisableMachine: (licensePlate: string | null = null) => forceDisableMachine(normalizedLaneId, licensePlate),
|
||||||
startPolling: (intervalMs = DEFAULT_POLL_INTERVAL_MS) => startPolling(normalizedLaneId, intervalMs),
|
startPolling: (intervalMs = DEFAULT_POLL_INTERVAL_MS) => startPolling(normalizedLaneId, intervalMs),
|
||||||
|
|||||||
+26
-1
@@ -43,6 +43,10 @@ const hasGateLoading = computed(() => (
|
|||||||
commandLoading.value.openEntranceGate || commandLoading.value.openExitGate
|
commandLoading.value.openEntranceGate || commandLoading.value.openExitGate
|
||||||
));
|
));
|
||||||
|
|
||||||
|
const hasOutsideGateLoading = computed(() => (
|
||||||
|
commandLoading.value.openOutsideEntranceGate || commandLoading.value.openOutsideExitGate
|
||||||
|
))
|
||||||
|
|
||||||
const normalizeDisplayValue = (value: unknown): string | null => {
|
const normalizeDisplayValue = (value: unknown): string | null => {
|
||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
const normalized = value.trim();
|
const normalized = value.trim();
|
||||||
@@ -247,7 +251,7 @@ onUnmounted(() => {
|
|||||||
<p class="divider-text">Wash In Progress</p>
|
<p class="divider-text">Wash In Progress</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="box is-shadowless p-3 mb-3">
|
<div class="box is-shadowless p-3 mb-3">
|
||||||
<template v-if="inProgressLoading">
|
<template v-if="inProgressLoading && !inProgressDetails">
|
||||||
<p>Loading in-progress details...</p>
|
<p>Loading in-progress details...</p>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="inProgressDetails?.in_progress">
|
<template v-else-if="inProgressDetails?.in_progress">
|
||||||
@@ -365,6 +369,27 @@ onUnmounted(() => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- This sends a command to open the outside gate to the property, not to be confused with the entrance gate. -->
|
||||||
|
<div class="columns is-mobile">
|
||||||
|
<div class="column is-half">
|
||||||
|
<button
|
||||||
|
class="button is-warning is-small is-fullwidth"
|
||||||
|
:class="{ 'is-loading': commandLoading.openOutsideEntranceGate }"
|
||||||
|
:disabled="hasOutsideGateLoading"
|
||||||
|
@click="connectivity.openOutsideEntranceGate()">
|
||||||
|
Open outside entrance gate
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="column is-half">
|
||||||
|
<button
|
||||||
|
class="button is-danger is-small is-fullwidth"
|
||||||
|
:class="{ 'is-loading': commandLoading.openOutsideExitGate }"
|
||||||
|
:disabled="hasOutsideGateLoading"
|
||||||
|
@click="connectivity.openOutsideExitGate()">
|
||||||
|
Open outside exit gate
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<p v-if="error" class="help is-danger mt-2">{{ error }}</p>
|
<p v-if="error" class="help is-danger mt-2">{{ error }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -200,4 +200,42 @@ describe("SelfServeMachineRelayControls gate controls", () => {
|
|||||||
expect(wrapper.text()).toContain("Vehicle:");
|
expect(wrapper.text()).toContain("Vehicle:");
|
||||||
expect(wrapper.text()).toContain("AB12345");
|
expect(wrapper.text()).toContain("AB12345");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps existing in-progress content visible while refresh is loading", async () => {
|
||||||
|
mocks.useMachineConnectivity.mockImplementation(() => ({
|
||||||
|
...buildConnectivity(),
|
||||||
|
inProgressDetails: ref({
|
||||||
|
lane_id: 5,
|
||||||
|
in_progress: true,
|
||||||
|
session: { reg: "CD67890", customer_number: 2002 },
|
||||||
|
customer: { display_name: "Jane Doe", customer_number: 2002 },
|
||||||
|
vehicle: { reg: "CD67890" },
|
||||||
|
}),
|
||||||
|
inProgressLoading: ref(true),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const wrapper = mount(SelfServeMachineRelayControls, {
|
||||||
|
props: {
|
||||||
|
machine: {
|
||||||
|
id: 5,
|
||||||
|
name: "Lane 5",
|
||||||
|
status: "ONLINE",
|
||||||
|
relay_in_id: "in",
|
||||||
|
relay_out_id: "out",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
global: {
|
||||||
|
stubs: {
|
||||||
|
SelfServeMachineStatus: { template: "<div />" },
|
||||||
|
BSwitch: { template: "<div />" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await flushMicrotasks();
|
||||||
|
|
||||||
|
expect(wrapper.text()).toContain("Customer:");
|
||||||
|
expect(wrapper.text()).toContain("Jane Doe");
|
||||||
|
expect(wrapper.text()).not.toContain("Loading in-progress details...");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
const readSource = (relativePath) => readFileSync(join(process.cwd(), relativePath), "utf8");
|
||||||
|
|
||||||
|
describe("self-serve pagination machine scope filtering", () => {
|
||||||
|
it("enforces machine-scoped vehicle type matching in tasks pagination", () => {
|
||||||
|
const source = readSource("src/components/displays/pagination/models/DepartmentDashboard/SelfServeTasksPagination.vue");
|
||||||
|
|
||||||
|
expect(source).toContain("if (vehicleTypeFilter) {");
|
||||||
|
expect(source).toContain("if (itemMachineType !== 0) {");
|
||||||
|
expect(source).toContain("if (itemMachineType !== vehicleTypeFilter) {");
|
||||||
|
expect(source).toContain("} else if (itemProduct !== 0 && itemProduct !== vehicleTypeFilter) {");
|
||||||
|
expect(source).not.toContain("if (vehicleTypeFilter && itemMachineType === 0 && itemProduct !== 0 && itemProduct !== vehicleTypeFilter)");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("enforces machine-scoped vehicle type matching in conditions pagination", () => {
|
||||||
|
const source = readSource("src/components/displays/pagination/models/DepartmentDashboard/SelfServeConditionsPagination.vue");
|
||||||
|
|
||||||
|
expect(source).toContain("if (vehicleTypeFilter) {");
|
||||||
|
expect(source).toContain("if (itemMachineType !== 0) {");
|
||||||
|
expect(source).toContain("if (itemMachineType !== vehicleTypeFilter) {");
|
||||||
|
expect(source).toContain("} else if (itemProduct !== 0 && itemProduct !== vehicleTypeFilter) {");
|
||||||
|
expect(source).not.toContain("if (vehicleTypeFilter && itemMachineType === 0 && itemProduct !== 0 && itemProduct !== vehicleTypeFilter)");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -21,6 +21,9 @@ describe("self-serve studio vehicle type scope", () => {
|
|||||||
expect(studioSource).toContain("const scopedQuestions = computed(() => questions.value.filter((entry) => matchesScopedProduct(entry)));");
|
expect(studioSource).toContain("const scopedQuestions = computed(() => questions.value.filter((entry) => matchesScopedProduct(entry)));");
|
||||||
expect(studioSource).toContain("const scopedConditions = computed(() => conditions.value.filter((entry) => matchesScopedProduct(entry)));");
|
expect(studioSource).toContain("const scopedConditions = computed(() => conditions.value.filter((entry) => matchesScopedProduct(entry)));");
|
||||||
expect(studioSource).toContain("const scopedTasks = computed(() => tasks.value.filter((entry) => matchesScopedProduct(entry)));");
|
expect(studioSource).toContain("const scopedTasks = computed(() => tasks.value.filter((entry) => matchesScopedProduct(entry)));");
|
||||||
|
expect(studioSource).toContain("const machineTypeId = parseNullableInt(item.machine_type_id);");
|
||||||
|
expect(studioSource).toContain("return machineTypeId === scopedProductId.value;");
|
||||||
|
expect(studioSource).not.toContain("if (parseNullableInt(item.machine_type_id)) {\n return true;\n }\n const product = parseIntOrZero(item.product);");
|
||||||
expect(studioSource).toContain("product: scopedProductId.value || 0,");
|
expect(studioSource).toContain("product: scopedProductId.value || 0,");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ describe("SelfServeTryModal", () => {
|
|||||||
mocks.questions.value = [];
|
mocks.questions.value = [];
|
||||||
mocks.visibleQuestions.value = [];
|
mocks.visibleQuestions.value = [];
|
||||||
mocks.answers.value = {};
|
mocks.answers.value = {};
|
||||||
|
mocks.activeTasks.value = [];
|
||||||
mocks.currentQuestion.value = null;
|
mocks.currentQuestion.value = null;
|
||||||
mocks.evaluateCondition.mockReturnValue(true);
|
mocks.evaluateCondition.mockReturnValue(true);
|
||||||
});
|
});
|
||||||
@@ -186,6 +187,86 @@ describe("SelfServeTryModal", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders dynamic image preview with expected query parameters when context exists", async () => {
|
||||||
|
mocks.activeTasks.value = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
buttons: [0, 2, 2, -1, 99],
|
||||||
|
dynamic_images_vehicle_type: 5,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const wrapper = mountWithApp(SelfServeTryModal, {
|
||||||
|
props: {
|
||||||
|
departmentId: 9,
|
||||||
|
laneId: 3,
|
||||||
|
vehicleTypeId: 3,
|
||||||
|
},
|
||||||
|
global: {
|
||||||
|
stubs: {
|
||||||
|
SelfServeQuestionCards: true,
|
||||||
|
SelfServeTaskList: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
const image = wrapper.get('[data-testid="self-serve-try-dynamic-image"]');
|
||||||
|
const imageUrl = new URL(image.attributes("src"));
|
||||||
|
|
||||||
|
expect(imageUrl.pathname).toBe("/department/lanes/dynamic-image");
|
||||||
|
expect(imageUrl.searchParams.get("department")).toBe("9");
|
||||||
|
expect(imageUrl.searchParams.get("lane")).toBe("3");
|
||||||
|
expect(imageUrl.searchParams.get("current_step")).toBe("0");
|
||||||
|
expect(imageUrl.searchParams.get("vehicle_type")).toBe("3");
|
||||||
|
expect(imageUrl.searchParams.get("thumb_position")).toBe("5");
|
||||||
|
expect(JSON.parse(imageUrl.searchParams.get("buttons"))).toEqual([0, 2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render dynamic image preview without dynamic-image context", async () => {
|
||||||
|
mocks.activeTasks.value = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
buttons: [-1, 12, 99],
|
||||||
|
dynamic_images_vehicle_type: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const wrapper = mountModal();
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(wrapper.find('[data-testid="self-serve-try-dynamic-image"]').exists()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hides dynamic image on error and restores it when image context changes", async () => {
|
||||||
|
mocks.activeTasks.value = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
buttons: [1],
|
||||||
|
dynamic_images_vehicle_type: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const wrapper = mountModal();
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
await wrapper.get('[data-testid="self-serve-try-dynamic-image"]').trigger("error");
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(wrapper.find('[data-testid="self-serve-try-dynamic-image"]').exists()).toBe(false);
|
||||||
|
|
||||||
|
await wrapper.get('[data-testid="self-serve-try-vehicle-type"]').setValue("3");
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
const image = wrapper.get('[data-testid="self-serve-try-dynamic-image"]');
|
||||||
|
const imageUrl = new URL(image.attributes("src"));
|
||||||
|
expect(imageUrl.searchParams.get("vehicle_type")).toBe("3");
|
||||||
|
expect(JSON.parse(imageUrl.searchParams.get("buttons"))).toEqual([1]);
|
||||||
|
});
|
||||||
|
|
||||||
it("does not duplicate the current question in the question cards list", async () => {
|
it("does not duplicate the current question in the question cards list", async () => {
|
||||||
mocks.currentQuestion.value = { id: 11, question: "Question 11" };
|
mocks.currentQuestion.value = { id: 11, question: "Question 11" };
|
||||||
mocks.visibleQuestions.value = [
|
mocks.visibleQuestions.value = [
|
||||||
|
|||||||
Reference in New Issue
Block a user