From c8e21b9128e832bf1d857124290cdd071208d86c Mon Sep 17 00:00:00 2001 From: Maurice Date: Fri, 9 Oct 2026 17:44:05 +0200 Subject: [PATCH] fix(admin): the phone addon list wordmark follows an OS theme switch under auto With the appearance set to auto, switching the OS between light and dark left the TREK wordmark in the phone admin addon header in the old colour (dark text on a dark page or the reverse) until something else made the screen render again. The desktop header switches right away. Cause: the phone worked the flag out from the dark_mode setting and window.matchMedia during render, and nothing re-renders the screen when the media query flips. The desktop uses useIsDark, which watches the .dark class that applyAppearance() writes. Fix: MAdminAddonManager uses useIsDark like AddonManager. Red test: FE-MOB-AADD-037 (new, MAdminAddonManager.test.tsx) failed with "Expected the element to have attribute: src="/text-light.svg"" after the .dark class was added under auto. Changed assertions in tests/unit/mobile/admin/MAdminAddonManager.test.tsx: - FE-MOB-AADD-003 (renamed from "dark mode and auto+prefers-dark swap the wordmark" to "the dark palette swaps the wordmark"): it expected window.matchMedia to be called with "(prefers-color-scheme: dark)"; that expectation is gone, since the screen now reads the .dark class. The test now sets that class instead of the dark_mode setting and a matchMedia stub, and still expects src "/text-light.svg". afterEach removes the class again. --- .../screens/admin/MAdminAddonManager.tsx | 5 ++- .../mobile/admin/MAdminAddonManager.test.tsx | 32 +++++++++++-------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/client/src/mobile/screens/admin/MAdminAddonManager.tsx b/client/src/mobile/screens/admin/MAdminAddonManager.tsx index 93517a651..4e161f5b5 100644 --- a/client/src/mobile/screens/admin/MAdminAddonManager.tsx +++ b/client/src/mobile/screens/admin/MAdminAddonManager.tsx @@ -1,6 +1,6 @@ import { type ComponentType } from 'react' import { useTranslation } from '../../../i18n' -import { useSettingsStore } from '../../../store/settingsStore' +import { useIsDark } from '../../../hooks/useIsDark' import { Puzzle, ListChecks, Wallet, FileText, CalendarDays, Globe, Briefcase, Image, Terminal, Link2, Compass, BookOpen, Sparkles, Luggage, Plane, Server, Cloud, Bookmark, Users, Check, Loader2, @@ -56,8 +56,7 @@ function AddonIcon({ name, size = 18, enabled = true }: AddonIconProps) { export default function MAdminAddonManager({ bagTrackingEnabled, onToggleBagTracking, collabFeatures, onToggleCollabFeature }: { bagTrackingEnabled?: boolean; onToggleBagTracking?: () => void; collabFeatures?: CollabFeatures; onToggleCollabFeature?: (key: string) => void }) { const { t } = useTranslation() - const dm = useSettingsStore(s => s.settings.dark_mode) - const dark = dm === true || dm === 'dark' || (dm === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches) + const dark = useIsDark() const { addons, loading, handleToggle, tripAddons, globalAddons, integrationAddons, providerOptions, documentProviderOptions, } = useAddonManager() diff --git a/client/tests/unit/mobile/admin/MAdminAddonManager.test.tsx b/client/tests/unit/mobile/admin/MAdminAddonManager.test.tsx index 8ee9c1c0b..3b77e2c6c 100644 --- a/client/tests/unit/mobile/admin/MAdminAddonManager.test.tsx +++ b/client/tests/unit/mobile/admin/MAdminAddonManager.test.tsx @@ -1,8 +1,8 @@ -// FE-MOB-AADD-001 to FE-MOB-AADD-036 +// FE-MOB-AADD-001 to FE-MOB-AADD-037 import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import userEvent from '@testing-library/user-event'; import { delay, http, HttpResponse } from 'msw'; -import { render, screen, waitFor, within } from '../../../helpers/render'; +import { act, render, screen, waitFor, within } from '../../../helpers/render'; import { server } from '../../../helpers/msw/server'; import { resetAllStores, seedStore } from '../../../helpers/store'; import { buildSettings } from '../../../helpers/factories'; @@ -67,6 +67,7 @@ beforeEach(() => { afterEach(() => { vi.restoreAllMocks(); + document.documentElement.classList.remove('dark'); }); describe('MAdminAddonManager', () => { @@ -92,21 +93,13 @@ describe('MAdminAddonManager', () => { expect(screen.getByAltText('TREK')).toHaveAttribute('src', '/text-dark.svg'); }); - it('FE-MOB-AADD-003: dark mode and auto+prefers-dark swap the wordmark', async () => { - seedStore(useSettingsStore, { settings: buildSettings({ dark_mode: 'dark' }) }); - const { unmount } = render(); - await screen.findByText('No addons available'); - expect(screen.getByAltText('TREK')).toHaveAttribute('src', '/text-light.svg'); - unmount(); - - const matchMedia = vi.fn().mockReturnValue({ matches: true, addEventListener: vi.fn(), removeEventListener: vi.fn() }); - vi.stubGlobal('matchMedia', matchMedia); - seedStore(useSettingsStore, { settings: buildSettings({ dark_mode: 'auto' }) }); + it('FE-MOB-AADD-003: the dark palette swaps the wordmark', async () => { + // The wordmark reads the .dark class, the same source applyAppearance() writes + // for dark and for auto under a dark OS theme. + document.documentElement.classList.add('dark'); render(); await screen.findByText('No addons available'); expect(screen.getByAltText('TREK')).toHaveAttribute('src', '/text-light.svg'); - expect(matchMedia).toHaveBeenCalledWith('(prefers-color-scheme: dark)'); - vi.unstubAllGlobals(); }); it('FE-MOB-AADD-004: a failing load toasts the addon error', async () => { @@ -757,4 +750,15 @@ describe('MAdminAddonManager', () => { expect(document.querySelector('svg.lucide-sparkles')).toBeInTheDocument(); expect(document.querySelector('svg.lucide-puzzle')).not.toBeInTheDocument(); }); + + it('FE-MOB-AADD-037: under auto the wordmark follows an OS theme switch without a re-render', async () => { + seedStore(useSettingsStore, { settings: buildSettings({ dark_mode: 'auto' }) }); + render(); + await screen.findByText('No addons available'); + expect(screen.getByAltText('TREK')).toHaveAttribute('src', '/text-dark.svg'); + + // applyAppearance() flips the .dark class when the OS theme changes under auto. + act(() => document.documentElement.classList.add('dark')); + await waitFor(() => expect(screen.getByAltText('TREK')).toHaveAttribute('src', '/text-light.svg')); + }); });