]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
test(ui-web): mock useTheme/useSkin in SimulatorBar.test.ts to fix vitest 4.x teardow...
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Thu, 4 Jun 2026 17:17:35 +0000 (19:17 +0200)
committerGitHub <noreply@github.com>
Thu, 4 Jun 2026 17:17:35 +0000 (19:17 +0200)
* 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<boolean>, 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 <select> exists.

* test(ui-web): use THEME_IDS/SKIN_IDS indices instead of hardcoded values

Address PR review: hardcoding 'dracula' and 'classic' as the target select
values would silently break the assertions if THEME_IDS or SKIN_IDS are
reordered or renamed in ui-common (the DOM $lt;select$gt; would keep its
current option, switchTheme/switchSkin would be called with that stale
value, and the toHaveBeenCalledWith assertion would compare against a
constant that no longer matches the dispatched event).

Pick targets that are guaranteed to differ from the mocked active values:
THEME_IDS[1] (≠ activeThemeId = THEME_IDS[0]) and the first SKIN_IDS entry
that is not 'modern' (≠ activeSkinId). The assertions then reference the
same constants, keeping the test coupled to the actual contract surface.

Note: the third reviewer comment about adding beforeEach(mockClear) is not
needed — vitest.config.ts already sets clearMocks: true and
restoreMocks: true, which run before each test.

* Revert "test(ui-web): use THEME_IDS/SKIN_IDS indices instead of hardcoded values"

This reverts commit 0941e5324d4501ca89094925654261ea1eda3fac.

* test(ui-web): add explicit afterEach mock cleanup in SimulatorBar test

Aligns SimulatorBar.test.ts with the dominant convention in ui/web's test
suite (Dialogs, StationCard, ConnectorRow, ClassicLayout, Actions, …),
which all add an explicit `afterEach(() => { vi.clearAllMocks() })`
alongside vitest.config.ts's global `clearMocks: true` / `restoreMocks: true`.
Belt-and-braces: the explicit reset keeps mock lifecycle visible next to
the mock declarations, even though it is technically redundant with the
global flags.

ui/web/tests/unit/skins/modern/SimulatorBar.test.ts

index 01e6f3e6142597bdc9a5b1ca3c82f6548c8383b6..f627e617f850485d394f432525116b7ec2e4beda 100644 (file)
@@ -3,12 +3,49 @@
  * @description Server switcher, simulator state display, action buttons.
  */
 import { mount } from '@vue/test-utils'
-import { describe, expect, it } from 'vitest'
+import { SKIN_IDS, THEME_IDS } from 'ui-common'
+import { afterEach, describe, expect, it, vi } from 'vitest'
 
 import SimulatorBar from '@/skins/modern/components/SimulatorBar.vue'
 
 import { createUIServerConfig } from '../../constants.js'
 
+// Mock useTheme/useSkin to avoid post-teardown console.warn from
+// validateTokenContract() (scheduled via requestAnimationFrame) and from
+// the floating switchSkin() Promise. Vitest 4.x rejects pending RPC at
+// teardown ("Closing rpc while \"onUserConsoleLog\" was pending"), and
+// jsdom does not resolve --color-* CSS variables so the contract check
+// would log ~24 warnings per <select> 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<Record<string, unknown>>()),
+    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<Record<string, unknown>>()),
+    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<string, unknown> = {}) {
 }
 
 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 () => {