Add outside gate controls to SelfServeMachineRelayControls with comprehensive tests:
- Introduced `openOutsideEntranceGate` and `openOutsideExitGate` actions. - Added dynamic button rendering based on department gate capabilities. - Implemented unit tests for gate visibility and action triggers.
This commit is contained in:
+36
-5
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted } from "vue";
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
|
||||
import SelfServeMachineStatus
|
||||
from "@/views/dashboards/superUserDashboard/selfserve/displays/machine/SelfServeMachineStatus.vue";
|
||||
import {
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import type { Machine } from "@/views/dashboards/superUserDashboard/selfserve/types/MachineType.vue";
|
||||
import {BSwitch, BTooltip} from "buefy";
|
||||
import MachineStatusType from "@/views/dashboards/superUserDashboard/selfserve/types/MachineStatusType.vue";
|
||||
import { getDepartmentOutsideGateCapabilities } from "@/composables/departmentOutsideGateCapabilities.js";
|
||||
|
||||
const props = defineProps<{ machine: Machine }>();
|
||||
|
||||
@@ -45,7 +46,15 @@ const hasGateLoading = computed(() => (
|
||||
|
||||
const hasOutsideGateLoading = computed(() => (
|
||||
commandLoading.value.openOutsideEntranceGate || commandLoading.value.openOutsideExitGate
|
||||
))
|
||||
));
|
||||
|
||||
const hasOutsideEntranceGate = ref(false);
|
||||
const hasOutsideExitGate = ref(false);
|
||||
const hasAnyOutsideGates = computed(() => (
|
||||
hasOutsideEntranceGate.value || hasOutsideExitGate.value
|
||||
));
|
||||
|
||||
let outsideGateCapabilityRequestId = 0;
|
||||
|
||||
const normalizeDisplayValue = (value: unknown): string | null => {
|
||||
if (typeof value === "string") {
|
||||
@@ -141,6 +150,25 @@ const openEntranceGate = () => openGate("ENTRANCE");
|
||||
const openExitGate = () => openGate("EXIT");
|
||||
const openSharedGate = () => openGate("ENTRANCE");
|
||||
|
||||
const loadOutsideGateCapabilities = async () => {
|
||||
const requestId = ++outsideGateCapabilityRequestId;
|
||||
const capabilities = await getDepartmentOutsideGateCapabilities(props.machine.department);
|
||||
if (requestId !== outsideGateCapabilityRequestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
hasOutsideEntranceGate.value = capabilities.hasOutsideEntranceGate;
|
||||
hasOutsideExitGate.value = capabilities.hasOutsideExitGate;
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.machine.department,
|
||||
() => {
|
||||
loadOutsideGateCapabilities();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await connectivity.fetchAllRelayStatuses();
|
||||
@@ -152,6 +180,7 @@ onMounted(async () => {
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
outsideGateCapabilityRequestId += 1;
|
||||
connectivity.stopPolling();
|
||||
});
|
||||
</script>
|
||||
@@ -370,19 +399,21 @@ onUnmounted(() => {
|
||||
</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">
|
||||
<div v-if="hasAnyOutsideGates" class="columns is-mobile">
|
||||
<div v-if="hasOutsideEntranceGate" class="column" :class="hasOutsideExitGate ? 'is-half' : 'is-full'">
|
||||
<button
|
||||
class="button is-warning is-small is-fullwidth"
|
||||
data-testid="outside-entrance-gate-button"
|
||||
:class="{ 'is-loading': commandLoading.openOutsideEntranceGate }"
|
||||
:disabled="hasOutsideGateLoading"
|
||||
@click="connectivity.openOutsideEntranceGate()">
|
||||
Open outside entrance gate
|
||||
</button>
|
||||
</div>
|
||||
<div class="column is-half">
|
||||
<div v-if="hasOutsideExitGate" class="column" :class="hasOutsideEntranceGate ? 'is-half' : 'is-full'">
|
||||
<button
|
||||
class="button is-danger is-small is-fullwidth"
|
||||
data-testid="outside-exit-gate-button"
|
||||
:class="{ 'is-loading': commandLoading.openOutsideExitGate }"
|
||||
:disabled="hasOutsideGateLoading"
|
||||
@click="connectivity.openOutsideExitGate()">
|
||||
|
||||
@@ -5,11 +5,14 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
openGate: vi.fn(() => Promise.resolve()),
|
||||
openOutsideEntranceGate: vi.fn(() => Promise.resolve()),
|
||||
openOutsideExitGate: vi.fn(() => Promise.resolve()),
|
||||
fetchAllRelayStatuses: vi.fn(() => Promise.resolve()),
|
||||
startPolling: vi.fn(() => Promise.resolve()),
|
||||
stopPolling: vi.fn(),
|
||||
toggleRelay: vi.fn(() => Promise.resolve()),
|
||||
useMachineConnectivity: vi.fn(),
|
||||
getDepartmentOutsideGateCapabilities: vi.fn(),
|
||||
}));
|
||||
|
||||
const buildConnectivity = () => ({
|
||||
@@ -29,12 +32,16 @@ const buildConnectivity = () => ({
|
||||
forceDisable: false,
|
||||
openEntranceGate: false,
|
||||
openExitGate: false,
|
||||
openOutsideEntranceGate: false,
|
||||
openOutsideExitGate: false,
|
||||
}),
|
||||
error: ref(null),
|
||||
inProgressDetails: ref(null),
|
||||
inProgressLoading: ref(false),
|
||||
toggleRelay: mocks.toggleRelay,
|
||||
openGate: mocks.openGate,
|
||||
openOutsideEntranceGate: mocks.openOutsideEntranceGate,
|
||||
openOutsideExitGate: mocks.openOutsideExitGate,
|
||||
fetchAllRelayStatuses: mocks.fetchAllRelayStatuses,
|
||||
startPolling: mocks.startPolling,
|
||||
stopPolling: mocks.stopPolling,
|
||||
@@ -53,6 +60,10 @@ vi.mock(
|
||||
})
|
||||
);
|
||||
|
||||
vi.mock("@/composables/departmentOutsideGateCapabilities.js", () => ({
|
||||
getDepartmentOutsideGateCapabilities: mocks.getDepartmentOutsideGateCapabilities,
|
||||
}));
|
||||
|
||||
import SelfServeMachineRelayControls
|
||||
from "@/views/dashboards/superUserDashboard/selfserve/displays/machine/SelfServeMachineRelayControls.vue";
|
||||
|
||||
@@ -64,15 +75,24 @@ const flushMicrotasks = async () => {
|
||||
describe("SelfServeMachineRelayControls gate controls", () => {
|
||||
beforeEach(() => {
|
||||
mocks.openGate.mockReset();
|
||||
mocks.openOutsideEntranceGate.mockReset();
|
||||
mocks.openOutsideExitGate.mockReset();
|
||||
mocks.fetchAllRelayStatuses.mockReset();
|
||||
mocks.startPolling.mockReset();
|
||||
mocks.stopPolling.mockReset();
|
||||
mocks.toggleRelay.mockReset();
|
||||
mocks.useMachineConnectivity.mockReset();
|
||||
mocks.getDepartmentOutsideGateCapabilities.mockReset();
|
||||
mocks.openGate.mockResolvedValue(undefined);
|
||||
mocks.openOutsideEntranceGate.mockResolvedValue(undefined);
|
||||
mocks.openOutsideExitGate.mockResolvedValue(undefined);
|
||||
mocks.fetchAllRelayStatuses.mockResolvedValue(undefined);
|
||||
mocks.startPolling.mockResolvedValue(undefined);
|
||||
mocks.useMachineConnectivity.mockImplementation(() => buildConnectivity());
|
||||
mocks.getDepartmentOutsideGateCapabilities.mockResolvedValue({
|
||||
hasOutsideEntranceGate: true,
|
||||
hasOutsideExitGate: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("shows a single full-width Open Gate button when entrance/exit share the same relay id", async () => {
|
||||
@@ -195,9 +215,9 @@ describe("SelfServeMachineRelayControls gate controls", () => {
|
||||
|
||||
await flushMicrotasks();
|
||||
|
||||
expect(wrapper.text()).toContain("Customer:");
|
||||
expect(wrapper.text()).toContain("Customer");
|
||||
expect(wrapper.text()).toContain("John Doe");
|
||||
expect(wrapper.text()).toContain("Vehicle:");
|
||||
expect(wrapper.text()).toContain("Vehicle");
|
||||
expect(wrapper.text()).toContain("AB12345");
|
||||
});
|
||||
|
||||
@@ -234,8 +254,138 @@ describe("SelfServeMachineRelayControls gate controls", () => {
|
||||
|
||||
await flushMicrotasks();
|
||||
|
||||
expect(wrapper.text()).toContain("Customer:");
|
||||
expect(wrapper.text()).toContain("Customer");
|
||||
expect(wrapper.text()).toContain("Jane Doe");
|
||||
expect(wrapper.text()).not.toContain("Loading in-progress details...");
|
||||
});
|
||||
|
||||
it("shows and wires both outside gate buttons when department has outside entrance and exit gates", async () => {
|
||||
const wrapper = mount(SelfServeMachineRelayControls, {
|
||||
props: {
|
||||
machine: {
|
||||
id: 6,
|
||||
department: 12,
|
||||
name: "Lane 6",
|
||||
status: "ONLINE",
|
||||
relay_in_id: "in",
|
||||
relay_out_id: "out",
|
||||
},
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
SelfServeMachineStatus: { template: "<div />" },
|
||||
BSwitch: { template: "<div />" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await flushMicrotasks();
|
||||
|
||||
const outsideEntranceButton = wrapper.get("[data-testid='outside-entrance-gate-button']");
|
||||
const outsideExitButton = wrapper.get("[data-testid='outside-exit-gate-button']");
|
||||
|
||||
expect(outsideEntranceButton.exists()).toBe(true);
|
||||
expect(outsideExitButton.exists()).toBe(true);
|
||||
|
||||
await outsideEntranceButton.trigger("click");
|
||||
await outsideExitButton.trigger("click");
|
||||
|
||||
expect(mocks.openOutsideEntranceGate).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.openOutsideExitGate).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("shows only the outside entrance button when department only has an outside entrance gate", async () => {
|
||||
mocks.getDepartmentOutsideGateCapabilities.mockResolvedValueOnce({
|
||||
hasOutsideEntranceGate: true,
|
||||
hasOutsideExitGate: false,
|
||||
});
|
||||
|
||||
const wrapper = mount(SelfServeMachineRelayControls, {
|
||||
props: {
|
||||
machine: {
|
||||
id: 7,
|
||||
department: 13,
|
||||
name: "Lane 7",
|
||||
status: "ONLINE",
|
||||
relay_in_id: "in",
|
||||
relay_out_id: "out",
|
||||
},
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
SelfServeMachineStatus: { template: "<div />" },
|
||||
BSwitch: { template: "<div />" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await flushMicrotasks();
|
||||
|
||||
expect(wrapper.find("[data-testid='outside-entrance-gate-button']").exists()).toBe(true);
|
||||
expect(wrapper.find("[data-testid='outside-exit-gate-button']").exists()).toBe(false);
|
||||
});
|
||||
|
||||
it("shows only the outside exit button when department only has an outside exit gate", async () => {
|
||||
mocks.getDepartmentOutsideGateCapabilities.mockResolvedValueOnce({
|
||||
hasOutsideEntranceGate: false,
|
||||
hasOutsideExitGate: true,
|
||||
});
|
||||
|
||||
const wrapper = mount(SelfServeMachineRelayControls, {
|
||||
props: {
|
||||
machine: {
|
||||
id: 8,
|
||||
department: 14,
|
||||
name: "Lane 8",
|
||||
status: "ONLINE",
|
||||
relay_in_id: "in",
|
||||
relay_out_id: "out",
|
||||
},
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
SelfServeMachineStatus: { template: "<div />" },
|
||||
BSwitch: { template: "<div />" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await flushMicrotasks();
|
||||
|
||||
expect(wrapper.find("[data-testid='outside-entrance-gate-button']").exists()).toBe(false);
|
||||
expect(wrapper.find("[data-testid='outside-exit-gate-button']").exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("hides outside gate controls when department has no outside gates", async () => {
|
||||
mocks.getDepartmentOutsideGateCapabilities.mockResolvedValueOnce({
|
||||
hasOutsideEntranceGate: false,
|
||||
hasOutsideExitGate: false,
|
||||
});
|
||||
|
||||
const wrapper = mount(SelfServeMachineRelayControls, {
|
||||
props: {
|
||||
machine: {
|
||||
id: 9,
|
||||
department: 15,
|
||||
name: "Lane 9",
|
||||
status: "ONLINE",
|
||||
relay_in_id: "in",
|
||||
relay_out_id: "out",
|
||||
},
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
SelfServeMachineStatus: { template: "<div />" },
|
||||
BSwitch: { template: "<div />" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await flushMicrotasks();
|
||||
|
||||
expect(wrapper.find("[data-testid='outside-entrance-gate-button']").exists()).toBe(false);
|
||||
expect(wrapper.find("[data-testid='outside-exit-gate-button']").exists()).toBe(false);
|
||||
expect(wrapper.text()).not.toContain("Open outside entrance gate");
|
||||
expect(wrapper.text()).not.toContain("Open outside exit gate");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user