From: Jérôme Benoit Date: Wed, 25 Mar 2026 22:41:36 +0000 (+0100) Subject: test(webui): harmonize test stubs, remove AAA comments, add composable and StateButto... X-Git-Tag: ocpp-server@v3.4.0~6 X-Git-Url: https://git.piment-noir.org/?a=commitdiff_plain;h=56d264fec3211a43337144ff6c18c013eadd849b;p=e-mobility-charging-stations-simulator.git test(webui): harmonize test stubs, remove AAA comments, add composable and StateButton tests --- diff --git a/ui/web/tests/unit/ChargingStationsView.test.ts b/ui/web/tests/unit/ChargingStationsView.test.ts index d56f241f..3990ccd8 100644 --- a/ui/web/tests/unit/ChargingStationsView.test.ts +++ b/ui/web/tests/unit/ChargingStationsView.test.ts @@ -15,7 +15,7 @@ import ChargingStationsView from '@/views/ChargingStationsView.vue' import { toastMock } from '../setup' import { createChargingStationData, createUIServerConfig } from './constants' -import { createMockUIClient, type MockUIClient } from './helpers' +import { createMockUIClient, type MockUIClient, StateButtonStub, ToggleButtonStub } from './helpers' vi.mock('@/composables', async importOriginal => { const actual = await importOriginal() @@ -90,17 +90,8 @@ function mountView ( props: ['loading'], template: '', - }, - ToggleButton: { - name: 'ToggleButton', - props: ['id', 'on', 'off', 'status', 'shared'], - template: '', - }, + StateButton: StateButtonStub, + ToggleButton: ToggleButtonStub, }, }, }) diff --git a/ui/web/tests/unit/StateButton.test.ts b/ui/web/tests/unit/StateButton.test.ts new file mode 100644 index 00000000..94736003 --- /dev/null +++ b/ui/web/tests/unit/StateButton.test.ts @@ -0,0 +1,99 @@ +/** + * @file Tests for StateButton component + * @description Unit tests for label switching, active state CSS class, and on/off callback dispatch. + */ +import { mount } from '@vue/test-utils' +import { describe, expect, it, vi } from 'vitest' + +import StateButton from '@/components/buttons/StateButton.vue' + +import { ButtonActiveStub } from './helpers' + +/** + * Mount factory — stubs Button with active class support + * @param props - Component props + * @param props.active - Whether the button is in active state + * @param props.off - Callback when toggled off + * @param props.offLabel - Label displayed when active + * @param props.on - Callback when toggled on + * @param props.onLabel - Label displayed when inactive + * @returns Mounted wrapper + */ +function mountStateButton (props: { + active: boolean + off?: () => void + offLabel: string + on?: () => void + onLabel: string +}) { + return mount(StateButton, { + global: { + stubs: { + Button: ButtonActiveStub, + }, + }, + props, + }) +} + +describe('StateButton', () => { + it('should display onLabel when inactive', () => { + const wrapper = mountStateButton({ + active: false, + offLabel: 'Stop', + onLabel: 'Start', + }) + expect(wrapper.text()).toBe('Start') + }) + + it('should display offLabel when active', () => { + const wrapper = mountStateButton({ + active: true, + offLabel: 'Stop', + onLabel: 'Start', + }) + expect(wrapper.text()).toBe('Stop') + }) + + it('should call on callback when clicked while inactive', async () => { + const onFn = vi.fn() + const wrapper = mountStateButton({ + active: false, + offLabel: 'Stop', + on: onFn, + onLabel: 'Start', + }) + await wrapper.find('button').trigger('click') + expect(onFn).toHaveBeenCalledOnce() + }) + + it('should call off callback when clicked while active', async () => { + const offFn = vi.fn() + const wrapper = mountStateButton({ + active: true, + off: offFn, + offLabel: 'Stop', + onLabel: 'Start', + }) + await wrapper.find('button').trigger('click') + expect(offFn).toHaveBeenCalledOnce() + }) + + it('should apply button--active class when active', () => { + const wrapper = mountStateButton({ + active: true, + offLabel: 'Stop', + onLabel: 'Start', + }) + expect(wrapper.find('button').classes()).toContain('button--active') + }) + + it('should not apply button--active class when inactive', () => { + const wrapper = mountStateButton({ + active: false, + offLabel: 'Stop', + onLabel: 'Start', + }) + expect(wrapper.find('button').classes()).not.toContain('button--active') + }) +}) diff --git a/ui/web/tests/unit/ToggleButton.test.ts b/ui/web/tests/unit/ToggleButton.test.ts index 65536aca..d2af0ce9 100644 --- a/ui/web/tests/unit/ToggleButton.test.ts +++ b/ui/web/tests/unit/ToggleButton.test.ts @@ -7,6 +7,8 @@ import { describe, expect, it, vi } from 'vitest' import ToggleButton from '@/components/buttons/ToggleButton.vue' +import { ButtonActiveStub } from './helpers' + /** * Mount factory — stubs Button child component with slot passthrough * @param props - Component props @@ -29,11 +31,7 @@ function mountToggleButton ( return mount(ToggleButton, { global: { stubs: { - Button: { - props: ['active'], - template: - '', - }, + Button: ButtonActiveStub, }, }, props, diff --git a/ui/web/tests/unit/Utils.test.ts b/ui/web/tests/unit/Utils.test.ts index d606d1c1..4714ed17 100644 --- a/ui/web/tests/unit/Utils.test.ts +++ b/ui/web/tests/unit/Utils.test.ts @@ -2,7 +2,8 @@ * @file Tests for Utils composable * @description Unit tests for type conversion, localStorage, UUID, and toggle state utilities. */ -import { afterEach, describe, expect, it } from 'vitest' +import { flushPromises } from '@vue/test-utils' +import { afterEach, describe, expect, it, vi } from 'vitest' import { convertToBoolean, @@ -13,9 +14,12 @@ import { randomUUID, resetToggleButtonState, setToLocalStorage, + useExecuteAction, validateUUID, } from '@/composables/Utils' +import { toastMock } from '../setup' + describe('Utils', () => { describe('convertToBoolean', () => { it('should return true for boolean true', () => { @@ -125,139 +129,105 @@ describe('Utils', () => { }) it('should get value from localStorage when key exists', () => { - // Arrange const key = 'test-key' const value = { count: 42, name: 'test' } localStorage.setItem(key, JSON.stringify(value)) - // Act const result = getFromLocalStorage(key, null) - // Assert expect(result).toEqual(value) }) it('should return default value when key does not exist', () => { - // Arrange const key = 'non-existent-key' const defaultValue = { name: 'default' } - // Act const result = getFromLocalStorage(key, defaultValue) - // Assert expect(result).toEqual(defaultValue) }) it('should set value to localStorage', () => { - // Arrange const key = 'test-key' const value = { count: 42, name: 'test' } - // Act setToLocalStorage(key, value) - // Assert expect(localStorage.getItem(key)).toBe(JSON.stringify(value)) }) it('should set string value to localStorage', () => { - // Arrange const key = 'string-key' const value = 'test-string' - // Act setToLocalStorage(key, value) - // Assert expect(localStorage.getItem(key)).toBe(JSON.stringify(value)) }) it('should delete value from localStorage', () => { - // Arrange const key = 'test-key' localStorage.setItem(key, 'test-value') - // Act deleteFromLocalStorage(key) - // Assert expect(localStorage.getItem(key)).toBeNull() }) it('should return localStorage instance', () => { - // Act const result = getLocalStorage() - // Assert expect(result).toBe(localStorage) }) }) describe('UUID', () => { it('should generate valid UUID v4', () => { - // Act const uuid = randomUUID() - // Assert expect(validateUUID(uuid)).toBe(true) }) it('should generate different UUIDs on each call', () => { - // Act const uuid1 = randomUUID() const uuid2 = randomUUID() - // Assert expect(uuid1).not.toBe(uuid2) }) it('should validate correct UUID v4 format', () => { - // Arrange const validUUID = '550e8400-e29b-41d4-a716-446655440000' - // Act const result = validateUUID(validUUID) - // Assert expect(result).toBe(true) }) it('should reject non-string UUID', () => { - // Act const result = validateUUID(123) - // Assert expect(result).toBe(false) }) it('should reject invalid UUID format', () => { - // Act const result = validateUUID('not-a-uuid') - // Assert expect(result).toBe(false) }) it('should reject UUID with wrong version (v3 instead of v4)', () => { - // Arrange const v3UUID = '550e8400-e29b-31d4-a716-446655440000' - // Act const result = validateUUID(v3UUID) - // Assert expect(result).toBe(false) }) it('should reject UUID with invalid variant', () => { - // Arrange const invalidVariantUUID = '550e8400-e29b-41d4-c716-446655440000' - // Act const result = validateUUID(invalidVariantUUID) - // Assert expect(result).toBe(false) }) }) @@ -268,55 +238,74 @@ describe('Utils', () => { }) it('should delete non-shared toggle button state with correct key', () => { - // Arrange const id = 'button-1' const key = `toggle-button-${id}` localStorage.setItem(key, 'true') - // Act resetToggleButtonState(id, false) - // Assert expect(localStorage.getItem(key)).toBeNull() }) it('should delete shared toggle button state with correct key', () => { - // Arrange const id = 'button-1' const key = `shared-toggle-button-${id}` localStorage.setItem(key, 'true') - // Act resetToggleButtonState(id, true) - // Assert expect(localStorage.getItem(key)).toBeNull() }) it('should use non-shared key by default', () => { - // Arrange const id = 'button-1' const nonSharedKey = `toggle-button-${id}` const sharedKey = `shared-toggle-button-${id}` localStorage.setItem(nonSharedKey, 'true') localStorage.setItem(sharedKey, 'true') - // Act resetToggleButtonState(id) - // Assert expect(localStorage.getItem(nonSharedKey)).toBeNull() expect(localStorage.getItem(sharedKey)).toBe('true') }) it('should handle deletion of non-existent key gracefully', () => { - // Arrange const id = 'non-existent' - // Act & Assert expect(() => { resetToggleButtonState(id) }).not.toThrow() }) }) + + describe('useExecuteAction', () => { + afterEach(() => { + vi.restoreAllMocks() + }) + + it('should call emit and toast.success on action success', async () => { + const emit = vi.fn() + const executeAction = useExecuteAction(emit) + + executeAction(Promise.resolve(), 'Success message', 'Error message') + await flushPromises() + + expect(emit).toHaveBeenCalledWith('need-refresh') + expect(toastMock.success).toHaveBeenCalledWith('Success message') + }) + + it('should call toast.error and console.error on action failure', async () => { + const consoleSpy = vi.spyOn(console, 'error') + const emit = vi.fn() + const executeAction = useExecuteAction(emit) + + executeAction(Promise.reject(new Error('fail')), 'Success', 'Error at action') + await flushPromises() + + expect(emit).not.toHaveBeenCalled() + expect(toastMock.error).toHaveBeenCalledWith('Error at action') + expect(consoleSpy).toHaveBeenCalledWith('Error at action:', expect.any(Error)) + }) + }) }) diff --git a/ui/web/tests/unit/helpers.ts b/ui/web/tests/unit/helpers.ts index 2e9e4b46..ed58cfee 100644 --- a/ui/web/tests/unit/helpers.ts +++ b/ui/web/tests/unit/helpers.ts @@ -46,14 +46,31 @@ export const ButtonStub = { template: '', } +/** Functional Button stub that supports the active prop and CSS class. */ +export const ButtonActiveStub = { + props: ['active'], + template: + '', +} + // ── StateButtonStub ─────────────────────────────────────────────────────────── /** Functional StateButton stub that renders the active/inactive label and dispatches on/off. */ export const StateButtonStub = { + name: 'StateButton', props: ['active', 'on', 'off', 'onLabel', 'offLabel'], template: '', } +// ── ToggleButtonStub ────────────────────────────────────────────────────────── + +/** Functional ToggleButton stub that renders slot content and dispatches on/off via props. */ +export const ToggleButtonStub = { + name: 'ToggleButton', + props: ['id', 'on', 'off', 'status', 'shared'], + template: '', +} + // ── MockWebSocket ───────────────────────────────────────────────────────────── export class MockWebSocket {