]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
test(webui): harmonize test stubs, remove AAA comments, add composable and StateButto...
authorJérôme Benoit <jerome.benoit@sap.com>
Wed, 25 Mar 2026 22:41:36 +0000 (23:41 +0100)
committerJérôme Benoit <jerome.benoit@sap.com>
Wed, 25 Mar 2026 22:41:36 +0000 (23:41 +0100)
ui/web/tests/unit/ChargingStationsView.test.ts
ui/web/tests/unit/StateButton.test.ts [new file with mode: 0644]
ui/web/tests/unit/ToggleButton.test.ts
ui/web/tests/unit/Utils.test.ts
ui/web/tests/unit/helpers.ts

index d56f241f497fb198710eac2864b54229bca02f69..3990ccd88721fc9030292ca9749ea766ad472c18 100644 (file)
@@ -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: '<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'],
-          template: '<button><slot /></button>',
-        },
+        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 (file)
index 0000000..9473600
--- /dev/null
@@ -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')
+  })
+})
index 65536aca985316415f144442474a9c2beca9dbfe..d2af0ce9c1ec821eb65c926fc47704f75bc1f017 100644 (file)
@@ -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 :class="[\'button\', { \'button--active\': active }]" type="button"><slot /></button>',
-        },
+        Button: ButtonActiveStub,
       },
     },
     props,
index d606d1c1466a9b4fb5fa3854c4dc58dd70ff2e62..4714ed179d75447c7fdf5500fff08a7095d69476 100644 (file)
@@ -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))
+    })
+  })
 })
index 2e9e4b46bdf82522b4b712c0575e1cb55ed46db7..ed58cfee34d26b34c3a68dfe05044b9387fc763a 100644 (file)
@@ -46,14 +46,31 @@ export const ButtonStub = {
   template: '<button @click="$emit(\'click\')"><slot /></button>',
 }
 
+/** Functional Button stub that supports the active prop and CSS class. */
+export const ButtonActiveStub = {
+  props: ['active'],
+  template:
+    '<button :class="[\'button\', { \'button--active\': active }]" type="button"><slot /></button>',
+}
+
 // ── 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: '<button @click="active ? off?.() : on?.()">{{ active ? offLabel : onLabel }}</button>',
 }
 
+// ── 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: '<button><slot /></button>',
+}
+
 // ── MockWebSocket ─────────────────────────────────────────────────────────────
 
 export class MockWebSocket {