})
.then(() => {
$toast.success('Charging stations successfully added')
+ return refreshChargingStations()
})
.catch((error: Error) => {
$toast.error('Error at adding charging stations')
import type { UUIDv4 } from '@/types'
import Button from '@/components/buttons/Button.vue'
-import { convertToBoolean, randomUUID, resetToggleButtonState } from '@/composables'
+import {
+ convertToBoolean,
+ randomUUID,
+ refreshChargingStations,
+ resetToggleButtonState,
+} from '@/composables'
const state = ref<{
autoStart: boolean
template: '',
})
-const templates = getCurrentInstance()?.appContext.config.globalProperties.$templates
+const app = getCurrentInstance()
+
+const templates = app?.appContext.config.globalProperties.$templates
if (templates != null) {
watch(templates, () => {
state.value.renderTemplates = randomUUID()
?.setSupervisionUrl(hashId, state.supervisionUrl)
.then(() => {
$toast.success('Supervision url successfully set')
+ return refreshChargingStations()
})
.catch((error: Error) => {
$toast.error('Error at setting supervision url')
import { ref } from 'vue'
import Button from '@/components/buttons/Button.vue'
-import { resetToggleButtonState } from '@/composables'
+import { refreshChargingStations, resetToggleButtonState } from '@/composables'
const props = defineProps<{
chargingStationId: string
import { useToast } from 'vue-toast-notification'
import Button from '@/components/buttons/Button.vue'
-import { convertToInt, resetToggleButtonState, UIClient, useUIClient } from '@/composables'
+import {
+ convertToInt,
+ refreshChargingStations,
+ resetToggleButtonState,
+ UIClient,
+ useUIClient,
+} from '@/composables'
import { type OCPPVersion } from '@/types'
const props = defineProps<{
ocppVersion: ocppVersion.value,
})
$toast.success('Transaction successfully started')
+ await refreshChargingStations()
} catch (error) {
$toast.error('Error at starting transaction')
console.error('Error at starting transaction:', error)
<template>
<button
- class="button"
+ :class="['button', { 'button--active': active }]"
type="button"
>
<slot />
</button>
</template>
+<script setup lang="ts">
+withDefaults(
+ defineProps<{
+ active?: boolean
+ }>(),
+ {
+ active: false,
+ }
+)
+</script>
+
<style scoped>
.button {
display: block;
outline: 2px solid var(--color-accent);
outline-offset: -2px;
}
+
+.button--active {
+ color: var(--color-text);
+ background-color: var(--color-bg-active);
+ border: 1px solid var(--color-accent);
+ box-shadow: inset 0 2px 4px var(--color-shadow-inset);
+}
</style>
--- /dev/null
+<template>
+ <Button
+ :active="active"
+ @click="active ? off?.() : on?.()"
+ >
+ {{ active ? offLabel : onLabel }}
+ </Button>
+</template>
+
+<script setup lang="ts">
+import Button from '@/components/buttons/Button.vue'
+
+defineProps<{
+ active: boolean
+ off?: () => void
+ offLabel: string
+ on?: () => void
+ onLabel: string
+}>()
+</script>
<template>
<Button
- :class="{ on: state.status }"
+ :active="state.status"
@click="click()"
>
<slot />
$emit('clicked', getFromLocalStorage<boolean>(id, props.status ?? false))
}
</script>
-
-<style scoped>
-button.on {
- color: var(--color-text);
- background-color: var(--color-bg-active);
- border: 1px solid var(--color-accent);
- box-shadow: inset 0 2px 4px var(--color-shadow-inset);
-}
-</style>
{{ atgStatus?.start === true ? 'Yes' : 'No' }}
</td>
<td class="connectors-table__column">
+ <StateButton
+ :active="connector.locked === true"
+ :off="() => unlockConnector()"
+ off-label="Unlock"
+ :on="() => lockConnector()"
+ on-label="Lock"
+ />
<ToggleButton
+ v-if="connector.transactionStarted !== true"
:id="`${hashId}-${evseId ?? 0}-${connectorId}-start-transaction`"
:off="
() => {
>
Start Transaction
</ToggleButton>
- <Button @click="stopTransaction()">
- Stop Transaction
- </Button>
- <Button @click="startAutomaticTransactionGenerator()">
- Start ATG
- </Button>
- <Button @click="stopAutomaticTransactionGenerator()">
- Stop ATG
- </Button>
- <Button
- v-if="connector.locked !== true"
- @click="lockConnector()"
- >
- Lock
- </Button>
<Button
v-else
- @click="unlockConnector()"
+ @click="stopTransaction()"
>
- Unlock
+ Stop Transaction
</Button>
+ <StateButton
+ :active="atgStatus?.start === true"
+ :off="() => stopAutomaticTransactionGenerator()"
+ off-label="Stop ATG"
+ :on="() => startAutomaticTransactionGenerator()"
+ on-label="Start ATG"
+ />
</td>
</tr>
</template>
import type { ConnectorStatus, OCPPVersion, Status } from '@/types'
import Button from '@/components/buttons/Button.vue'
+import StateButton from '@/components/buttons/StateButton.vue'
import ToggleButton from '@/components/buttons/ToggleButton.vue'
import { useUIClient } from '@/composables'
const $toast = useToast()
+const executeAction = (action: Promise<unknown>, successMsg: string, errorMsg: string): void => {
+ action
+ .then(() => {
+ $emit('need-refresh')
+ return $toast.success(successMsg)
+ })
+ .catch((error: Error) => {
+ $toast.error(errorMsg)
+ console.error(`${errorMsg}:`, error)
+ })
+}
+
const stopTransaction = (): void => {
if (props.connector.transactionId == null) {
$toast.error('No transaction to stop')
return
}
- uiClient
- .stopTransaction(props.hashId, {
+ executeAction(
+ uiClient.stopTransaction(props.hashId, {
ocppVersion: props.ocppVersion,
transactionId: props.connector.transactionId,
- })
- .then(() => {
- return $toast.success('Transaction successfully stopped')
- })
- .catch((error: Error) => {
- $toast.error('Error at stopping transaction')
- console.error('Error at stopping transaction:', error)
- })
+ }),
+ 'Transaction successfully stopped',
+ 'Error at stopping transaction'
+ )
}
const lockConnector = (): void => {
- uiClient
- .lockConnector(props.hashId, props.connectorId)
- .then(() => {
- return $toast.success('Connector successfully locked')
- })
- .catch((error: Error) => {
- $toast.error('Error at locking connector')
- console.error('Error at locking connector:', error)
- })
+ executeAction(
+ uiClient.lockConnector(props.hashId, props.connectorId),
+ 'Connector successfully locked',
+ 'Error at locking connector'
+ )
}
const unlockConnector = (): void => {
- uiClient
- .unlockConnector(props.hashId, props.connectorId)
- .then(() => {
- return $toast.success('Connector successfully unlocked')
- })
- .catch((error: Error) => {
- $toast.error('Error at unlocking connector')
- console.error('Error at unlocking connector:', error)
- })
+ executeAction(
+ uiClient.unlockConnector(props.hashId, props.connectorId),
+ 'Connector successfully unlocked',
+ 'Error at unlocking connector'
+ )
}
const startAutomaticTransactionGenerator = (): void => {
- uiClient
- .startAutomaticTransactionGenerator(props.hashId, props.connectorId)
- .then(() => {
- return $toast.success('Automatic transaction generator successfully started')
- })
- .catch((error: Error) => {
- $toast.error('Error at starting automatic transaction generator')
- console.error('Error at starting automatic transaction generator:', error)
- })
+ executeAction(
+ uiClient.startAutomaticTransactionGenerator(props.hashId, props.connectorId),
+ 'Automatic transaction generator successfully started',
+ 'Error at starting automatic transaction generator'
+ )
}
const stopAutomaticTransactionGenerator = (): void => {
- uiClient
- .stopAutomaticTransactionGenerator(props.hashId, props.connectorId)
- .then(() => {
- return $toast.success('Automatic transaction generator successfully stopped')
- })
- .catch((error: Error) => {
- $toast.error('Error at stopping automatic transaction generator')
- console.error('Error at stopping automatic transaction generator:', error)
- })
+ executeAction(
+ uiClient.stopAutomaticTransactionGenerator(props.hashId, props.connectorId),
+ 'Automatic transaction generator successfully stopped',
+ 'Error at stopping automatic transaction generator'
+ )
}
</script>
{{ chargingStation.stationInfo.firmwareVersion ?? 'Ø' }}
</td>
<td class="cs-table__column">
- <Button @click="startChargingStation()">
- Start Charging Station
- </Button>
- <Button @click="stopChargingStation()">
- Stop Charging Station
- </Button>
+ <StateButton
+ :active="chargingStation.started === true"
+ :off="() => stopChargingStation()"
+ off-label="Stop Charging Station"
+ :on="() => startChargingStation()"
+ on-label="Start Charging Station"
+ />
+ <StateButton
+ :active="isWebSocketOpen"
+ :off="() => closeConnection()"
+ off-label="Close Connection"
+ :on="() => openConnection()"
+ on-label="Open Connection"
+ />
<ToggleButton
:id="`${chargingStation.stationInfo.hashId}-set-supervision-url`"
:off="
>
Set Supervision Url
</ToggleButton>
- <Button @click="openConnection()">
- Open Connection
- </Button>
- <Button @click="closeConnection()">
- Close Connection
- </Button>
<Button @click="deleteChargingStation()">
Delete Charging Station
</Button>
</template>
<script setup lang="ts">
+import { computed } from 'vue'
import { useToast } from 'vue-toast-notification'
import type { ChargingStationData, ConnectorStatus, Status } from '@/types'
import Button from '@/components/buttons/Button.vue'
+import StateButton from '@/components/buttons/StateButton.vue'
import ToggleButton from '@/components/buttons/ToggleButton.vue'
import CSConnector from '@/components/charging-stations/CSConnector.vue'
import { deleteFromLocalStorage, getLocalStorage, useUIClient } from '@/composables'
const $emit = defineEmits(['need-refresh'])
+const isWebSocketOpen = computed(() => props.chargingStation.wsState === WebSocket.OPEN)
+
const getConnectorEntries = (): ConnectorTableEntry[] => {
if (Array.isArray(props.chargingStation.evses) && props.chargingStation.evses.length > 0) {
const entries: ConnectorTableEntry[] = []
const $toast = useToast()
-const startChargingStation = (): void => {
- uiClient
- .startChargingStation(props.chargingStation.stationInfo.hashId)
+const executeAction = (action: Promise<unknown>, successMsg: string, errorMsg: string): void => {
+ action
.then(() => {
- return $toast.success('Charging station successfully started')
+ $emit('need-refresh')
+ return $toast.success(successMsg)
})
.catch((error: Error) => {
- $toast.error('Error at starting charging station')
- console.error('Error at starting charging station:', error)
+ $toast.error(errorMsg)
+ console.error(`${errorMsg}:`, error)
})
}
+
+const startChargingStation = (): void => {
+ executeAction(
+ uiClient.startChargingStation(props.chargingStation.stationInfo.hashId),
+ 'Charging station successfully started',
+ 'Error at starting charging station'
+ )
+}
const stopChargingStation = (): void => {
- uiClient
- .stopChargingStation(props.chargingStation.stationInfo.hashId)
- .then(() => {
- return $toast.success('Charging station successfully stopped')
- })
- .catch((error: Error) => {
- $toast.error('Error at stopping charging station')
- console.error('Error at stopping charging station:', error)
- })
+ executeAction(
+ uiClient.stopChargingStation(props.chargingStation.stationInfo.hashId),
+ 'Charging station successfully stopped',
+ 'Error at stopping charging station'
+ )
}
const openConnection = (): void => {
- uiClient
- .openConnection(props.chargingStation.stationInfo.hashId)
- .then(() => {
- return $toast.success('Connection successfully opened')
- })
- .catch((error: Error) => {
- $toast.error('Error at opening connection')
- console.error('Error at opening connection:', error)
- })
+ executeAction(
+ uiClient.openConnection(props.chargingStation.stationInfo.hashId),
+ 'Connection successfully opened',
+ 'Error at opening connection'
+ )
}
const closeConnection = (): void => {
- uiClient
- .closeConnection(props.chargingStation.stationInfo.hashId)
- .then(() => {
- return $toast.success('Connection successfully closed')
- })
- .catch((error: Error) => {
- $toast.error('Error at closing connection')
- console.error('Error at closing connection:', error)
- })
+ executeAction(
+ uiClient.closeConnection(props.chargingStation.stationInfo.hashId),
+ 'Connection successfully closed',
+ 'Error at closing connection'
+ )
}
const deleteChargingStation = (): void => {
uiClient
deleteFromLocalStorage(key)
}
}
+ $emit('need-refresh')
return $toast.success('Charging station successfully deleted')
})
.catch((error: Error) => {
-import type { UUIDv4 } from '@/types'
+import type { Ref } from 'vue'
+
+import { getCurrentInstance } from 'vue'
+
+import type { ChargingStationData, UUIDv4 } from '@/types'
import { UIClient } from './UIClient'
export const useUIClient = (): UIClient => {
return UIClient.getInstance()
}
+
+export const useChargingStations = (): Ref<ChargingStationData[]> | undefined => {
+ return getCurrentInstance()?.appContext.config.globalProperties.$chargingStations
+}
+
+export const refreshChargingStations = async (): Promise<void> => {
+ const ref = useChargingStations()
+ if (ref == null) return
+ const response = await useUIClient().listChargingStations()
+ ref.value = response.chargingStations as ChargingStationData[]
+}
getFromLocalStorage,
getLocalStorage,
randomUUID,
+ refreshChargingStations,
resetToggleButtonState,
setToLocalStorage,
+ useChargingStations,
useUIClient,
} from './Utils'
</option>
</select>
</Container>
- <ToggleButton
- :id="'simulator'"
- :key="state.renderSimulator"
+ <StateButton
+ :active="simulatorStarted === true"
:off="() => stopSimulator()"
+ :off-label="simulatorLabel('Stop')"
:on="() => startSimulator()"
- :status="simulatorStarted"
- >
- {{ simulatorButtonMessage }}
- </ToggleButton>
+ :on-label="simulatorLabel('Start')"
+ />
<ToggleButton
:id="'add-charging-stations'"
:key="state.renderAddChargingStations"
:charging-stations="$chargingStations!.value"
@need-refresh="
() => {
+ getChargingStations()
state.renderAddChargingStations = randomUUID()
state.renderChargingStations = randomUUID()
}
} from '@/types'
import ReloadButton from '@/components/buttons/ReloadButton.vue'
+import StateButton from '@/components/buttons/StateButton.vue'
import ToggleButton from '@/components/buttons/ToggleButton.vue'
import CSTable from '@/components/charging-stations/CSTable.vue'
import Container from '@/components/Container.vue'
const simulatorStarted = computed((): boolean | undefined => simulatorState.value?.started)
-const simulatorButtonMessage = computed(
- (): string =>
- `${simulatorState.value?.started === true ? 'Stop' : 'Start'} Simulator${
- simulatorState.value?.version != null ? ` (${simulatorState.value.version})` : ''
- }`
-)
+const simulatorLabel = (action: string): string =>
+ `${action} Simulator${
+ simulatorState.value?.version != null ? ` (${simulatorState.value.version})` : ''
+ }`
const state = ref<{
gettingChargingStations: boolean
gettingTemplates: boolean
renderAddChargingStations: UUIDv4
renderChargingStations: UUIDv4
- renderSimulator: UUIDv4
uiServerIndex: number
}>({
gettingChargingStations: false,
gettingTemplates: false,
renderAddChargingStations: randomUUID(),
renderChargingStations: randomUUID(),
- renderSimulator: randomUUID(),
uiServerIndex: getFromLocalStorage<number>('uiServerConfigurationIndex', 0),
})
})
}
-watch(simulatorState, () => {
- state.value.renderSimulator = randomUUID()
-})
-
const clearTemplates = (): void => {
if (app != null) {
app.appContext.config.globalProperties.$templates!.value = []
import { toastMock } from '../setup'
import { createConnectorStatus, TEST_HASH_ID, TEST_STATION_ID } from './constants'
-import { ButtonStub, createMockUIClient, type MockUIClient } from './helpers'
+import { ButtonStub, createMockUIClient, type MockUIClient, StateButtonStub } from './helpers'
vi.mock('@/composables', async importOriginal => {
const actual = await importOriginal()
global: {
stubs: {
Button: ButtonStub,
+ StateButton: StateButtonStub,
ToggleButton: true,
},
},
})
it('should show error toast when no transaction to stop', async () => {
- const connector = createConnectorStatus({ transactionId: undefined })
+ const connector = createConnectorStatus({
+ transactionId: undefined,
+ transactionStarted: true,
+ })
const wrapper = mountCSConnector({ connector })
const buttons = wrapper.findAll('button')
const stopBtn = buttons.find(b => b.text() === 'Stop Transaction')
})
it('should call stopAutomaticTransactionGenerator', async () => {
- const wrapper = mountCSConnector()
+ const wrapper = mountCSConnector({ atgStatus: { start: true } })
const buttons = wrapper.findAll('button')
const stopAtgBtn = buttons.find(b => b.text() === 'Stop ATG')
await stopAtgBtn?.trigger('click')
it('should show error toast when ATG stop fails', async () => {
mockClient.stopAutomaticTransactionGenerator.mockRejectedValueOnce(new Error('fail'))
- const wrapper = mountCSConnector()
+ const wrapper = mountCSConnector({ atgStatus: { start: true } })
const buttons = wrapper.findAll('button')
const btn = buttons.find(b => b.text().includes('Stop ATG'))
await btn?.trigger('click')
createEvseEntry,
createStationInfo,
} from './constants'
-import { ButtonStub, createMockUIClient, type MockUIClient } from './helpers'
+import { ButtonStub, createMockUIClient, type MockUIClient, StateButtonStub } from './helpers'
vi.mock('@/composables', async importOriginal => {
const actual = await importOriginal()
stubs: {
Button: ButtonStub,
CSConnector: true,
+ StateButton: StateButtonStub,
ToggleButton: true,
},
},
describe('station actions', () => {
it('should call startChargingStation on button click', async () => {
- const wrapper = mountCSData()
+ const wrapper = mountCSData(createChargingStationData({ started: false }))
const buttons = wrapper.findAll('button')
const startBtn = buttons.find(b => b.text() === 'Start Charging Station')
await startBtn?.trigger('click')
})
it('should call stopChargingStation on button click', async () => {
- const wrapper = mountCSData()
+ const wrapper = mountCSData(createChargingStationData({ started: true }))
const buttons = wrapper.findAll('button')
const stopBtn = buttons.find(b => b.text() === 'Stop Charging Station')
await stopBtn?.trigger('click')
})
it('should call openConnection on button click', async () => {
- const wrapper = mountCSData()
+ const wrapper = mountCSData(createChargingStationData({ wsState: WebSocket.CLOSED }))
const buttons = wrapper.findAll('button')
const openBtn = buttons.find(b => b.text() === 'Open Connection')
await openBtn?.trigger('click')
})
it('should call closeConnection on button click', async () => {
- const wrapper = mountCSData()
+ const wrapper = mountCSData(createChargingStationData({ wsState: WebSocket.OPEN }))
const buttons = wrapper.findAll('button')
const closeBtn = buttons.find(b => b.text() === 'Close Connection')
await closeBtn?.trigger('click')
})
it('should show success toast after starting charging station', async () => {
- const wrapper = mountCSData()
+ const wrapper = mountCSData(createChargingStationData({ started: false }))
const buttons = wrapper.findAll('button')
const startBtn = buttons.find(b => b.text() === 'Start Charging Station')
await startBtn?.trigger('click')
it('should show error toast on start failure', async () => {
mockClient.startChargingStation.mockRejectedValueOnce(new Error('fail'))
- const wrapper = mountCSData()
+ const wrapper = mountCSData(createChargingStationData({ started: false }))
const buttons = wrapper.findAll('button')
const startBtn = buttons.find(b => b.text() === 'Start Charging Station')
await startBtn?.trigger('click')
props: ['loading'],
template: '<button @click="$emit(\'click\')" />',
},
+ StateButton: {
+ name: 'StateButton',
+ props: ['active', 'on', 'off', 'onLabel', 'offLabel'],
+ template:
+ '<button @click="active ? off?.() : on?.()">{{ active ? offLabel : onLabel }}</button>',
+ },
ToggleButton: {
name: 'ToggleButton',
props: ['id', 'on', 'off', 'status', 'shared'],
status: ResponseStatus.SUCCESS,
})
const wrapper = mountView()
- const toggleButtons = wrapper.findAllComponents({ name: 'ToggleButton' })
- const simulatorButton = toggleButtons.find(tb => tb.props('id') === 'simulator')
- const onProp = simulatorButton?.props('on') as (() => void) | undefined
+ const stateButton = wrapper.findComponent({ name: 'StateButton' })
+ const onProp = stateButton.props('on') as (() => void) | undefined
onProp?.()
await flushPromises()
expect(mockClient.startSimulator).toHaveBeenCalled()
it('should show error toast when simulator start fails', async () => {
mockClient.startSimulator = vi.fn().mockRejectedValue(new Error('start failed'))
const wrapper = mountView()
- const toggleButtons = wrapper.findAllComponents({ name: 'ToggleButton' })
- const simulatorButton = toggleButtons.find(tb => tb.props('id') === 'simulator')
- const onProp = simulatorButton?.props('on') as (() => void) | undefined
+ const stateButton = wrapper.findComponent({ name: 'StateButton' })
+ const onProp = stateButton.props('on') as (() => void) | undefined
onProp?.()
await flushPromises()
expect(toastMock.error).toHaveBeenCalledWith('Error at starting simulator')
status: ResponseStatus.SUCCESS,
})
const wrapper = mountView()
- const toggleButtons = wrapper.findAllComponents({ name: 'ToggleButton' })
- const simulatorButton = toggleButtons.find(tb => tb.props('id') === 'simulator')
- const offProp = simulatorButton?.props('off') as (() => void) | undefined
+ const stateButton = wrapper.findComponent({ name: 'StateButton' })
+ const offProp = stateButton.props('off') as (() => void) | undefined
offProp?.()
await flushPromises()
expect(mockClient.stopSimulator).toHaveBeenCalled()
it('should show error toast when simulator stop fails', async () => {
mockClient.stopSimulator = vi.fn().mockRejectedValue(new Error('stop failed'))
const wrapper = mountView()
- const toggleButtons = wrapper.findAllComponents({ name: 'ToggleButton' })
- const simulatorButton = toggleButtons.find(tb => tb.props('id') === 'simulator')
- const offProp = simulatorButton?.props('off') as (() => void) | undefined
+ const stateButton = wrapper.findComponent({ name: 'StateButton' })
+ const offProp = stateButton.props('off') as (() => void) | undefined
offProp?.()
await flushPromises()
expect(toastMock.error).toHaveBeenCalledWith('Error at stopping simulator')
global: {
stubs: {
Button: {
- template: '<button class="button" type="button"><slot /></button>',
+ props: ['active'],
+ template:
+ '<button :class="[\'button\', { \'button--active\': active }]" type="button"><slot /></button>',
},
},
},
it('should not apply on class when status is false', () => {
const wrapper = mountToggleButton({ id: 'off-test', status: false })
const button = wrapper.find('button')
- expect(button.classes()).not.toContain('on')
+ expect(button.classes()).not.toContain('button--active')
})
it('should apply on class when status is true', () => {
const wrapper = mountToggleButton({ id: 'on-test', status: true })
const button = wrapper.find('button')
- expect(button.classes()).toContain('on')
+ expect(button.classes()).toContain('button--active')
})
})
const wrapper = mountToggleButton({ id: 'toggle-inactive-to-active', status: false })
const button = wrapper.find('button')
- expect(button.classes()).not.toContain('on')
+ expect(button.classes()).not.toContain('button--active')
await button.trigger('click')
- expect(button.classes()).toContain('on')
+ expect(button.classes()).toContain('button--active')
})
it('should toggle from active to inactive on click', async () => {
const wrapper = mountToggleButton({ id: 'toggle-active-to-inactive', status: true })
const button = wrapper.find('button')
- expect(button.classes()).toContain('on')
+ expect(button.classes()).toContain('button--active')
await button.trigger('click')
- expect(button.classes()).not.toContain('on')
+ expect(button.classes()).not.toContain('button--active')
})
it('should call on callback when toggled to active', async () => {
const wrapper = mountToggleButton({ id: 'restore-test', status: false })
const button = wrapper.find('button')
- expect(button.classes()).toContain('on')
+ expect(button.classes()).toContain('button--active')
})
it('should use correct localStorage key for non-shared toggle', async () => {
const wrapper = mountToggleButton({ id: 'default-status' })
const button = wrapper.find('button')
- expect(button.classes()).not.toContain('on')
+ expect(button.classes()).not.toContain('button--active')
})
it('should handle multiple consecutive clicks', async () => {
const button = wrapper.find('button')
await button.trigger('click')
- expect(button.classes()).toContain('on')
+ expect(button.classes()).toContain('button--active')
await button.trigger('click')
- expect(button.classes()).not.toContain('on')
+ expect(button.classes()).not.toContain('button--active')
await button.trigger('click')
- expect(button.classes()).toContain('on')
+ expect(button.classes()).toContain('button--active')
})
})
})
template: '<button @click="$emit(\'click\')"><slot /></button>',
}
+// ── StateButtonStub ───────────────────────────────────────────────────────────
+
+/** Functional StateButton stub that renders the active/inactive label and dispatches on/off. */
+export const StateButtonStub = {
+ props: ['active', 'on', 'off', 'onLabel', 'offLabel'],
+ template: '<button @click="active ? off?.() : on?.()">{{ active ? offLabel : onLabel }}</button>',
+}
+
// ── MockWebSocket ─────────────────────────────────────────────────────────────
export class MockWebSocket {