From 3b0a6a022b33423e9b2b5a19b0ec0eb037c4933b Mon Sep 17 00:00:00 2001 From: Huyen Nguyen <25715018+huyenltnguyen@users.noreply.github.com> Date: Tue, 28 Oct 2025 20:08:31 -0700 Subject: [PATCH] test(client): add unit tests for theme initialization logic (#63212) --- client/src/redux/theme-saga.js | 2 +- client/src/redux/theme-saga.test.js | 119 ++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 client/src/redux/theme-saga.test.js diff --git a/client/src/redux/theme-saga.js b/client/src/redux/theme-saga.js index 5281ca91eec..5df2edd8589 100644 --- a/client/src/redux/theme-saga.js +++ b/client/src/redux/theme-saga.js @@ -13,7 +13,7 @@ function* toggleThemeSaga() { yield put(createFlashMessage({ ...data })); } -function* initializeThemeSaga() { +export function* initializeThemeSaga() { // Wait for the fetch userComplete action yield take(actionTypes.fetchUserComplete); diff --git a/client/src/redux/theme-saga.test.js b/client/src/redux/theme-saga.test.js new file mode 100644 index 00000000000..225dbf3c4a0 --- /dev/null +++ b/client/src/redux/theme-saga.test.js @@ -0,0 +1,119 @@ +// @vitest-environment jsdom +import { expectSaga } from 'redux-saga-test-plan'; +import { describe, it, vi, beforeEach, afterEach, expect } from 'vitest'; +import { select, take } from 'redux-saga/effects'; +import { initializeThemeSaga } from './theme-saga.js'; +import { setTheme } from './actions'; +import { actionTypes } from './action-types'; +import { userThemeSelector } from './selectors'; + +describe('initializeThemeSaga', () => { + let localStorageMock; + let matchMediaMock; + + beforeEach(() => { + localStorageMock = { + getItem: vi.fn(), + setItem: vi.fn() + }; + Object.defineProperty(window, 'localStorage', { + value: localStorageMock, + writable: true + }); + + matchMediaMock = vi.fn(); + Object.defineProperty(window, 'matchMedia', { + value: matchMediaMock, + writable: true + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should wait for fetchUserComplete action before initializing', async () => { + localStorageMock.getItem.mockReturnValue('dark'); + matchMediaMock.mockReturnValue({ matches: false }); + + // Without the fetchUserComplete action + await expectSaga(initializeThemeSaga) + .provide([[select(userThemeSelector), null]]) + .not.put(setTheme('dark')) + .run({ silenceTimeout: true }); + + expect(localStorageMock.setItem).not.toHaveBeenCalled(); + + // With the fetchUserComplete action + await expectSaga(initializeThemeSaga) + .provide([ + [take(actionTypes.fetchUserComplete), undefined], + [select(userThemeSelector), null] + ]) + .take(actionTypes.fetchUserComplete) + .put(setTheme('dark')) + .run(); + + expect(localStorageMock.setItem).toHaveBeenCalledWith('theme', 'dark'); + }); + + it('should use localStorage theme regardless of user preference', async () => { + localStorageMock.getItem.mockReturnValue('dark'); + matchMediaMock.mockReturnValue({ matches: false }); + + await expectSaga(initializeThemeSaga) + .provide([ + [take(actionTypes.fetchUserComplete), undefined], + [select(userThemeSelector), 'day'] + ]) + .put(setTheme('dark')) + .run(); + + expect(localStorageMock.setItem).toHaveBeenCalledWith('theme', 'dark'); + }); + + it('should use user preference when localStorage is empty regardless of system preference', async () => { + localStorageMock.getItem.mockReturnValue(null); + matchMediaMock.mockReturnValue({ matches: false }); + + await expectSaga(initializeThemeSaga) + .provide([ + [take(actionTypes.fetchUserComplete), undefined], + [select(userThemeSelector), 'night'] + ]) + .put(setTheme('dark')) + .run(); + + expect(localStorageMock.setItem).toHaveBeenCalledWith('theme', 'dark'); + }); + + it('should use system preference when localStorage and user preference are empty', async () => { + localStorageMock.getItem.mockReturnValue(null); + matchMediaMock.mockReturnValue({ matches: true }); + + await expectSaga(initializeThemeSaga) + .provide([ + [take(actionTypes.fetchUserComplete), undefined], + [select(userThemeSelector), null] + ]) + .put(setTheme('dark')) + .run(); + + expect(localStorageMock.setItem).toHaveBeenCalledWith('theme', 'dark'); + }); + + it('should default to light theme when all preferences are empty', async () => { + localStorageMock.getItem.mockReturnValue(null); + matchMediaMock.mockReturnValue({ matches: false }); + + await expectSaga(initializeThemeSaga) + .provide([ + [take(actionTypes.fetchUserComplete), undefined], + [select(userThemeSelector), null] + ]) + .put(setTheme('light')) + .run(); + + expect(localStorageMock.setItem).toHaveBeenCalledWith('theme', 'light'); + }); +});