diff --git a/src/react/hooks/paragon/useParagonThemeCore.test.js b/src/react/hooks/paragon/useParagonThemeCore.test.js new file mode 100644 index 000000000..1eecf876c --- /dev/null +++ b/src/react/hooks/paragon/useParagonThemeCore.test.js @@ -0,0 +1,101 @@ +import { renderHook, act } from '@testing-library/react-hooks'; +import { getConfig } from '../../../config'; +import { logError } from '../../../logging'; +import useParagonThemeCore from './useParagonThemeCore'; + +jest.mock('../../../logging'); + +describe('useParagonThemeCore', () => { + const themeOnLoad = jest.fn(); + + afterEach(() => { + document.head.innerHTML = ''; + jest.clearAllMocks(); + }); + + it('should load the core url and change the loading state to true', () => { + const coreConfig = { + themeCore: { + urls: { default: 'https://cdn.jsdelivr.net/npm/@edx/paragon@$21.0.0/dist/core.min.css' }, + }, + onLoad: themeOnLoad, + }; + + renderHook(() => useParagonThemeCore(coreConfig)); + const createdLinkTag = document.head.querySelector('link'); + act(() => createdLinkTag.onload()); + expect(createdLinkTag.href).toBe(coreConfig.themeCore.urls.default); + expect(themeOnLoad).toHaveBeenCalledTimes(1); + }); + + it('should load the core default and brand url and change the loading state to true', () => { + const coreConfig = { + themeCore: { + urls: { + default: 'https://cdn.jsdelivr.net/npm/@edx/paragon@$21.0.0/dist/core.min.css', + brandOverride: 'https://cdn.jsdelivr.net/npm/@edx/brand@$2.0.0Version/dist/core.min.css', + }, + }, + onLoad: themeOnLoad, + }; + + renderHook(() => useParagonThemeCore(coreConfig)); + const createdLinkTag = document.head.querySelector('link[data-paragon-theme-core="true"]'); + const createdBrandLinkTag = document.head.querySelector('link[data-brand-theme-core="true"]'); + + act(() => { createdLinkTag.onload(); createdBrandLinkTag.onload(); }); + expect(createdLinkTag.href).toBe(coreConfig.themeCore.urls.default); + expect(createdBrandLinkTag.href).toBe(coreConfig.themeCore.urls.brandOverride); + expect(themeOnLoad).toHaveBeenCalledTimes(1); + }); + + it('should dispatch a log error and fallback to PARAGON_THEME if can not load the core theme link', () => { + global.PARAGON_THEME = { + paragon: { + version: '1.0.0', + themeUrls: { + core: { + fileName: 'core.min.css', + }, + defaults: { + light: 'light', + }, + variants: { + light: { + fileName: 'light.min.css', + }, + }, + }, + }, + }; + const coreConfig = { + themeCore: { + urls: { + default: 'https://cdn.jsdelivr.net/npm/@edx/paragon@$21.0.0/dist/core.min.css', + }, + }, + onLoad: themeOnLoad, + }; + + renderHook(() => useParagonThemeCore(coreConfig)); + const createdLinkTag = document.head.querySelector('link[data-paragon-theme-core="true"]'); + + act(() => { createdLinkTag.onerror(); }); + expect(logError).toHaveBeenCalledTimes(1); + expect(logError).toHaveBeenCalledWith(`Failed to load core theme CSS from ${coreConfig.themeCore.urls.default}`); + expect(document.querySelector('link').href).toBe(`${getConfig().BASE_URL}/${PARAGON_THEME.paragon.themeUrls.core.fileName}`); + }); + + it('should not create any core link if can not find themeCore urls definition', () => { + const coreConfig = { + themeCore: { + default: 'https://cdn.jsdelivr.net/npm/@edx/paragon@$21.0.0/dist/core.min.css', + }, + onLoad: themeOnLoad, + }; + + renderHook(() => useParagonThemeCore(coreConfig)); + expect(document.head.querySelectorAll('link').length).toBe(0); + expect(themeOnLoad).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/react/hooks/paragon/useParagonThemeVariants.test.js b/src/react/hooks/paragon/useParagonThemeVariants.test.js new file mode 100644 index 000000000..d43dd5588 --- /dev/null +++ b/src/react/hooks/paragon/useParagonThemeVariants.test.js @@ -0,0 +1,140 @@ +import { act, renderHook } from '@testing-library/react-hooks'; +import { getConfig } from '../../../config'; +import { logError } from '../../../logging'; +import useParagonThemeVariants from './useParagonThemeVariants'; + +jest.mock('../../../logging'); + +const mockAddEventListener = jest.fn(); +const mockRemoveEventListener = jest.fn(); +const mockOnChange = jest.fn(); + +Object.defineProperty(window, 'matchMedia', { + value: jest.fn(() => ({ + addEventListener: mockAddEventListener, + removeEventListener: mockRemoveEventListener, + onchange: mockOnChange, + })), +}); + +describe('useParagonThemeVariants', () => { + const themeOnLoad = jest.fn(); + + afterEach(() => { + document.head.innerHTML = ''; + jest.clearAllMocks(); + }); + + it('should create the links tags for each theme variant and change the state to true when all variants are loaded', () => { + const themeVariants = { + light: { + urls: { + default: 'https://cdn.jsdelivr.net/npm/@edx/paragon@$21.0.0/dist/light.min.css', + brandOverride: 'https://cdn.jsdelivr.net/npm/@edx/brand@$2.0.0/dist/light.min.css', + }, + }, + dark: { + urls: { + default: 'https://cdn.jsdelivr.net/npm/@edx/paragon@$21.0.0/dist/dark.min.css', + brandOverride: 'https://cdn.jsdelivr.net/npm/@edx/brand@$2.0.0/dist/dark.min.css', + }, + }, + }; + const currentThemeVariant = 'light'; + + renderHook(() => useParagonThemeVariants({ themeVariants, currentThemeVariant, onLoad: themeOnLoad })); + const themeLinks = document.head.querySelectorAll('link'); + act(() => { themeLinks.forEach((link) => link.onload()); }); + + expect(themeLinks.length).toBe(4); + }); + + it('should dispatch a log error and fallback to PARAGON_THEME if can not load the variant theme link', () => { + global.PARAGON_THEME = { + paragon: { + version: '1.0.0', + themeUrls: { + core: { + fileName: 'core.min.css', + }, + defaults: { + light: 'light', + }, + variants: { + light: { + fileName: 'light.min.css', + }, + }, + }, + }, + }; + const themeVariants = { + light: { + urls: { + default: 'https://cdn.jsdelivr.net/npm/@edx/paragon@$21.0.0/dist/light.min.css', + }, + }, + }; + const currentThemeVariant = 'light'; + + renderHook(() => useParagonThemeVariants({ themeVariants, currentThemeVariant, onLoad: themeOnLoad })); + const createdLinkTag = document.head.querySelector('link'); + act(() => { createdLinkTag.onerror(); }); + expect(logError).toHaveBeenCalledTimes(1); + expect(logError).toHaveBeenCalledWith(`Failed to load theme variant (${currentThemeVariant}) CSS from ${themeVariants.light.urls.default}`); + expect(document.querySelector('link').href).toBe(`${getConfig().BASE_URL}/${PARAGON_THEME.paragon.themeUrls.variants.light.fileName}`); + }); + + it('should configure theme variants according with system preference and add the change event listener', () => { + window.matchMedia['prefers-color-scheme'] = 'dark'; + + const themeVariants = { + light: { + urls: { + default: 'https://cdn.jsdelivr.net/npm/@edx/paragon@$21.0.0/dist/light.min.css', + brandOverride: 'https://cdn.jsdelivr.net/npm/@edx/brand@$2.0.0/dist/light.min.css', + }, + }, + dark: { + urls: { + default: 'https://cdn.jsdelivr.net/npm/@edx/paragon@$21.0.0/dist/dark.min.css', + brandOverride: 'https://cdn.jsdelivr.net/npm/@edx/brand@$2.0.0/dist/dark.min.css', + }, + }, + }; + + const currentThemeVariant = 'light'; + + renderHook(() => useParagonThemeVariants({ themeVariants, currentThemeVariant, onLoad: themeOnLoad })); + + const themeLinks = document.head.querySelectorAll('link'); + act(() => { themeLinks.forEach((link) => link.onload()); }); + + expect(mockAddEventListener).toHaveBeenCalledTimes(1); + }); + + it('should do nothing if themeVariants is not configured', () => { + const themeVariants = null; + const currentTheme = 'light'; + + renderHook(() => useParagonThemeVariants({ themeVariants, currentTheme, onLoad: themeOnLoad })); + expect(document.head.querySelectorAll('link').length).toBe(0); + }); + + it('should not create any core link if can not find themeVariant urls definition', () => { + const themeVariants = { + light: { + default: 'https://cdn.jsdelivr.net/npm/@edx/paragon@$21.0.0/dist/light.min.css', + }, + dark: { + default: 'https://cdn.jsdelivr.net/npm/@edx/paragon@$21.0.0/dist/dark.min.css', + }, + }; + + const currentTheme = 'light'; + + renderHook(() => useParagonThemeVariants({ themeVariants, currentTheme, onLoad: themeOnLoad })); + + expect(document.head.querySelectorAll('link').length).toBe(0); + }); +});