From: Jérôme Benoit Date: Fri, 17 Apr 2026 01:08:51 +0000 (+0200) Subject: test(ui): add tests for converters, websocket utils, and useFetchData X-Git-Tag: cli@v4.5.0~43 X-Git-Url: https://git.piment-noir.org/?a=commitdiff_plain;h=df99c2e939c3584a1045d9751262fd289896948c;p=e-mobility-charging-stations-simulator.git test(ui): add tests for converters, websocket utils, and useFetchData - Add converters.test.ts in ui-common (26 tests for convertToBoolean/convertToInt) - Add websocket.test.ts in ui-common (6 tests for getWebSocketStateName) - Add useFetchData tests in ui-web (5 tests: success, error, loading guard, reset, no-callback) - Guard onError callback in useFetchData with try/catch for robustness --- diff --git a/ui/common/tests/converters.test.ts b/ui/common/tests/converters.test.ts new file mode 100644 index 00000000..70c85b15 --- /dev/null +++ b/ui/common/tests/converters.test.ts @@ -0,0 +1,118 @@ +import assert from 'node:assert' +import { describe, it } from 'node:test' + +import { convertToBoolean, convertToInt } from '../src/utils/converters.js' + +await describe('converters', async () => { + await describe('convertToBoolean', async () => { + await it('should return true for boolean true', () => { + assert.strictEqual(convertToBoolean(true), true) + }) + + await it('should return false for boolean false', () => { + assert.strictEqual(convertToBoolean(false), false) + }) + + await it('should return true for string "true"', () => { + assert.strictEqual(convertToBoolean('true'), true) + }) + + await it('should return true for string "True"', () => { + assert.strictEqual(convertToBoolean('True'), true) + }) + + await it('should return true for string "TRUE"', () => { + assert.strictEqual(convertToBoolean('TRUE'), true) + }) + + await it('should return true for string "1"', () => { + assert.strictEqual(convertToBoolean('1'), true) + }) + + await it('should return true for numeric 1', () => { + assert.strictEqual(convertToBoolean(1), true) + }) + + await it('should return false for string "false"', () => { + assert.strictEqual(convertToBoolean('false'), false) + }) + + await it('should return false for string "0"', () => { + assert.strictEqual(convertToBoolean('0'), false) + }) + + await it('should return false for numeric 0', () => { + assert.strictEqual(convertToBoolean(0), false) + }) + + await it('should return false for numeric 2', () => { + assert.strictEqual(convertToBoolean(2), false) + }) + + await it('should return false for null', () => { + assert.strictEqual(convertToBoolean(null), false) + }) + + await it('should return false for undefined', () => { + assert.strictEqual(convertToBoolean(undefined), false) + }) + + await it('should return false for empty string', () => { + assert.strictEqual(convertToBoolean(''), false) + }) + + await it('should return false for arbitrary string', () => { + assert.strictEqual(convertToBoolean('hello'), false) + }) + + await it('should return true for whitespace-padded " true "', () => { + assert.strictEqual(convertToBoolean(' true '), true) + }) + + await it('should return true for whitespace-padded " 1 "', () => { + assert.strictEqual(convertToBoolean(' 1 '), true) + }) + }) + + await describe('convertToInt', async () => { + await it('should return integer for integer input', () => { + assert.strictEqual(convertToInt(42), 42) + }) + + await it('should truncate float 42.7 to 42', () => { + assert.strictEqual(convertToInt(42.7), 42) + }) + + await it('should truncate float -42.7 to -42', () => { + assert.strictEqual(convertToInt(-42.7), -42) + }) + + await it('should parse string integer "42"', () => { + assert.strictEqual(convertToInt('42'), 42) + }) + + await it('should parse string integer "-42"', () => { + assert.strictEqual(convertToInt('-42'), -42) + }) + + await it('should return 0 for null', () => { + assert.strictEqual(convertToInt(null), 0) + }) + + await it('should return 0 for undefined', () => { + assert.strictEqual(convertToInt(undefined), 0) + }) + + await it('should throw Error for non-numeric string "abc"', () => { + assert.throws(() => { + convertToInt('abc') + }, Error) + }) + + await it('should throw Error for empty string', () => { + assert.throws(() => { + convertToInt('') + }, Error) + }) + }) +}) diff --git a/ui/common/tests/websocket.test.ts b/ui/common/tests/websocket.test.ts new file mode 100644 index 00000000..0b9fc87e --- /dev/null +++ b/ui/common/tests/websocket.test.ts @@ -0,0 +1,31 @@ +import assert from 'node:assert' +import { describe, it } from 'node:test' + +import { WebSocketReadyState } from '../src/client/types.js' +import { getWebSocketStateName } from '../src/utils/websocket.js' + +await describe('getWebSocketStateName', async () => { + await it('should return "Connecting" for WebSocketReadyState.CONNECTING (0)', () => { + assert.strictEqual(getWebSocketStateName(WebSocketReadyState.CONNECTING), 'Connecting') + }) + + await it('should return "Open" for WebSocketReadyState.OPEN (1)', () => { + assert.strictEqual(getWebSocketStateName(WebSocketReadyState.OPEN), 'Open') + }) + + await it('should return "Closing" for WebSocketReadyState.CLOSING (2)', () => { + assert.strictEqual(getWebSocketStateName(WebSocketReadyState.CLOSING), 'Closing') + }) + + await it('should return "Closed" for WebSocketReadyState.CLOSED (3)', () => { + assert.strictEqual(getWebSocketStateName(WebSocketReadyState.CLOSED), 'Closed') + }) + + await it('should return undefined for unknown state (99)', () => { + assert.strictEqual(getWebSocketStateName(99), undefined) + }) + + await it('should return undefined for undefined input', () => { + assert.strictEqual(getWebSocketStateName(undefined), undefined) + }) +}) diff --git a/ui/web/src/composables/Utils.ts b/ui/web/src/composables/Utils.ts index 405d517f..9e001dbc 100644 --- a/ui/web/src/composables/Utils.ts +++ b/ui/web/src/composables/Utils.ts @@ -130,7 +130,11 @@ export const useFetchData = ( fetching.value = false }) .catch((error: unknown) => { - onError?.() + try { + onError?.() + } catch (callbackError: unknown) { + console.error('Error in onError callback:', callbackError) + } $toast.error(errorMsg) console.error(`${errorMsg}:`, error) }) diff --git a/ui/web/tests/unit/Utils.test.ts b/ui/web/tests/unit/Utils.test.ts index 743fbb88..f9e3d4f4 100644 --- a/ui/web/tests/unit/Utils.test.ts +++ b/ui/web/tests/unit/Utils.test.ts @@ -3,7 +3,7 @@ * @description Unit tests for type conversion, localStorage, UUID, and toggle state utilities. */ import { flushPromises } from '@vue/test-utils' -import { convertToBoolean, convertToInt, randomUUID, validateUUID } from 'ui-common' +import { convertToBoolean, convertToInt, randomUUID, ResponseStatus, validateUUID } from 'ui-common' import { afterEach, describe, expect, it, vi } from 'vitest' import { @@ -13,6 +13,7 @@ import { resetToggleButtonState, setToLocalStorage, useExecuteAction, + useFetchData, } from '@/composables' import { toastMock } from '../setup' @@ -329,4 +330,95 @@ describe('Utils', () => { expect(consoleSpy).toHaveBeenCalledWith('Error at action:', expect.any(Error)) }) }) + + describe('useFetchData', () => { + afterEach(() => { + vi.restoreAllMocks() + }) + + it('should call onSuccess and reset fetching on successful fetch', async () => { + const response = { status: ResponseStatus.SUCCESS } + const onSuccess = vi.fn() + const { fetch, fetching } = useFetchData( + () => Promise.resolve(response), + onSuccess, + 'Error message' + ) + + fetch() + expect(fetching.value).toBe(true) + await flushPromises() + + expect(onSuccess).toHaveBeenCalledWith(response) + expect(fetching.value).toBe(false) + }) + + it('should call onError and toast.error on failed fetch', async () => { + const consoleSpy = vi.spyOn(console, 'error') + const onError = vi.fn() + const { fetch } = useFetchData( + () => Promise.reject(new Error('network')), + vi.fn(), + 'Fetch failed', + onError + ) + + fetch() + await flushPromises() + + expect(onError).toHaveBeenCalled() + expect(toastMock.error).toHaveBeenCalledWith('Fetch failed') + expect(consoleSpy).toHaveBeenCalledWith('Fetch failed:', expect.any(Error)) + }) + + it('should prevent concurrent fetches via loading guard', async () => { + let resolvePromise: ((value: unknown) => void) | undefined + const clientFn = vi.fn().mockImplementation( + () => + new Promise(resolve => { + resolvePromise = resolve + }) + ) + const { fetch } = useFetchData(clientFn, vi.fn(), 'Error') + + fetch() + fetch() + fetch() + + expect(clientFn).toHaveBeenCalledTimes(1) + + if (resolvePromise !== undefined) { + resolvePromise({ status: ResponseStatus.SUCCESS }) + } + await flushPromises() + }) + + it('should reset fetching on error', async () => { + const { fetch, fetching } = useFetchData( + () => Promise.reject(new Error('fail')), + vi.fn(), + 'Error' + ) + + fetch() + await flushPromises() + + expect(fetching.value).toBe(false) + }) + + it('should work without onError callback', async () => { + const consoleSpy = vi.spyOn(console, 'error') + const { fetch } = useFetchData( + () => Promise.reject(new Error('fail')), + vi.fn(), + 'Fetch error' + ) + + fetch() + await flushPromises() + + expect(toastMock.error).toHaveBeenCalledWith('Fetch error') + expect(consoleSpy).toHaveBeenCalled() + }) + }) })