From: Jérôme Benoit Date: Thu, 4 Jun 2026 17:17:35 +0000 (+0200) Subject: test(ui-web): mock useTheme/useSkin in SimulatorBar.test.ts to fix vitest 4.x teardow... X-Git-Url: https://git.piment-noir.org/?a=commitdiff_plain;h=5790cab94b1b64c3f4e453860ce86436e69fff2f;p=e-mobility-charging-stations-simulator.git test(ui-web): mock useTheme/useSkin in SimulatorBar.test.ts to fix vitest 4.x teardown RPC leak (#1884) * test(ui-web): mock useTheme/useSkin in SimulatorBar.test.ts to fix vitest 4.x teardown leak The two final tests ('should call switchTheme/switchSkin when … select changes') mounted SimulatorBar with the real useTheme/useSkin composables. Both code paths schedule console.warn output AFTER the synchronous test body resolves: 1. validateTokenContract() (src/shared/tokens/contract.ts) wraps work in requestAnimationFrame and warns when CSS variables are missing. jsdom does not resolve --color-* CSS variables, so the contract check logs ~24 warnings via rAF on every theme/skin switch. 2. switchSkin() returns Promise, but the @change handler discards the promise and trigger('change') only awaits Vue's nextTick. The unawaited loadStyles() dynamic import resolves later and may also call console.warn. Vitest 4.x tightened teardown to fail rather than swallow pending RPC calls, surfacing this latent bug as: EnvironmentTeardownError: [vitest-worker]: Closing rpc while "onUserConsoleLog" was pending Reproducibly fails on Linux/Node 22 in CI; passes on macOS/Node 24 because rAF + dynamic-import timing differs. Fix mirrors the existing precedent in tests/unit/router.test.ts: vi.mock both composables at the top of the test file, returning shapes that match the real contract (THEME_IDS / SKIN_IDS imported from ui-common rather than hand-rolled subsets). The mocked switchSkin resolves immediately so no teardown leak occurs. Also tightens the two affected tests to actually assert what their names claim (switchTheme/switchSkin called with the selected value), instead of merely checking the change. +const switchThemeMock = vi.fn() +const switchSkinMock = vi.fn().mockResolvedValue(true) + +vi.mock('@/shared/composables/useTheme.js', async importOriginal => { + const { readonly, ref } = await import('vue') + return { + ...(await importOriginal>()), + useTheme: () => ({ + activeThemeId: readonly(ref(THEME_IDS[0])), + availableThemes: THEME_IDS, + lastError: readonly(ref(null)), + switchTheme: switchThemeMock, + }), + } +}) + +vi.mock('@/shared/composables/useSkin.js', async importOriginal => { + const { readonly, ref } = await import('vue') + return { + ...(await importOriginal>()), + useSkin: () => ({ + activeSkinId: readonly(ref<(typeof SKIN_IDS)[number]>('modern')), + availableSkins: SKIN_IDS.map(id => ({ id, label: id })), + isSwitching: readonly(ref(false)), + lastError: readonly(ref(null)), + switchSkin: switchSkinMock, + }), + } +}) + const baseServer = createUIServerConfig({ name: 'Alpha' }) const altServer = createUIServerConfig({ host: 'beta', name: 'Beta' }) @@ -28,6 +65,10 @@ function mountBar (props: Record = {}) { } describe('SimulatorBar', () => { + afterEach(() => { + vi.clearAllMocks() + }) + it('should show Disconnected pill when simulatorState is undefined', () => { const wrapper = mountBar() expect(wrapper.text()).toContain('Disconnected') @@ -122,14 +163,18 @@ describe('SimulatorBar', () => { const wrapper = mountBar() const themeSelect = wrapper.find('.modern-bar__select[aria-label="Theme"]') expect(themeSelect.exists()).toBe(true) + await themeSelect.setValue('dracula') await themeSelect.trigger('change') + expect(switchThemeMock).toHaveBeenCalledWith('dracula') }) it('should call switchSkin when skin select changes', async () => { const wrapper = mountBar() const skinSelect = wrapper.find('.modern-bar__select[aria-label="Skin"]') expect(skinSelect.exists()).toBe(true) + await skinSelect.setValue('classic') await skinSelect.trigger('change') + expect(switchSkinMock).toHaveBeenCalledWith('classic') }) it('should emit switch-server with selectedIndex when server select changes via trigger', async () => {