/** * Real Browser Environment Dark Mode Flicker Test * * This test attempts to simulate real browser refresh scenarios including: * 1. SSR HTML generation phase * 2. Client-side JavaScript loading * 3. Theme system initialization * 4. CSS styles application timing */ import { render, screen, waitFor } from '@testing-library/react' import { ThemeProvider } from 'next-themes' import useTheme from '@/hooks/use-theme' import { useEffect, useState } from 'react' const DARK_MODE_MEDIA_QUERY = /prefers-color-scheme:\s*dark/i // Setup browser environment for testing const setupMockEnvironment = (storedTheme: string | null, systemPrefersDark = false) => { if (typeof window === 'undefined') return try { window.localStorage.clear() } catch { // ignore if localStorage has been replaced by a throwing stub } if (storedTheme === null) window.localStorage.removeItem('theme') else window.localStorage.setItem('theme', storedTheme) document.documentElement.removeAttribute('data-theme') const mockMatchMedia: typeof window.matchMedia = (query: string) => { const listeners = new Set<(event: MediaQueryListEvent) => void>() const isDarkQuery = DARK_MODE_MEDIA_QUERY.test(query) const matches = isDarkQuery ? systemPrefersDark : false const handleAddListener = (listener: (event: MediaQueryListEvent) => void) => { listeners.add(listener) } const handleRemoveListener = (listener: (event: MediaQueryListEvent) => void) => { listeners.delete(listener) } const handleAddEventListener = (_event: string, listener: EventListener) => { if (typeof listener === 'function') listeners.add(listener as (event: MediaQueryListEvent) => void) } const handleRemoveEventListener = (_event: string, listener: EventListener) => { if (typeof listener === 'function') listeners.delete(listener as (event: MediaQueryListEvent) => void) } const handleDispatchEvent = (event: Event) => { listeners.forEach(listener => listener(event as MediaQueryListEvent)) return true } const mediaQueryList: MediaQueryList = { matches, media: query, onchange: null, addListener: handleAddListener, removeListener: handleRemoveListener, addEventListener: handleAddEventListener, removeEventListener: handleRemoveEventListener, dispatchEvent: handleDispatchEvent, } return mediaQueryList } jest.spyOn(window, 'matchMedia').mockImplementation(mockMatchMedia) } // Helper function to create timing page component const createTimingPageComponent = ( timingData: Array<{ phase: string; timestamp: number; styles: { backgroundColor: string; color: string } }>, ) => { const recordTiming = (phase: string, styles: { backgroundColor: string; color: string }) => { timingData.push({ phase, timestamp: performance.now(), styles, }) } const TimingPageComponent = () => { const [mounted, setMounted] = useState(false) const { theme } = useTheme() const isDark = mounted ? theme === 'dark' : false const currentStyles = { backgroundColor: isDark ? '#1f2937' : '#ffffff', color: isDark ? '#ffffff' : '#000000', } recordTiming(mounted ? 'CSR' : 'Initial', currentStyles) useEffect(() => { setMounted(true) }, []) return (
Phase: {mounted ? 'CSR' : 'Initial'} | Theme: {theme} | Visual: {isDark ? 'dark' : 'light'}
) } return TimingPageComponent } // Helper function to create CSS test component const createCSSTestComponent = ( cssStates: Array<{ className: string; timestamp: number }>, ) => { const recordCSSState = (className: string) => { cssStates.push({ className, timestamp: performance.now(), }) } const CSSTestComponent = () => { const [mounted, setMounted] = useState(false) const { theme } = useTheme() const isDark = mounted ? theme === 'dark' : false const className = `min-h-screen ${isDark ? 'bg-gray-900 text-white' : 'bg-white text-black'}` recordCSSState(className) useEffect(() => { setMounted(true) }, []) return (
Classes: {className}
) } return CSSTestComponent } // Helper function to create performance test component const createPerformanceTestComponent = ( performanceMarks: Array<{ event: string; timestamp: number }>, ) => { const recordPerformanceMark = (event: string) => { performanceMarks.push({ event, timestamp: performance.now() }) } const PerformanceTestComponent = () => { const [mounted, setMounted] = useState(false) const { theme } = useTheme() recordPerformanceMark('component-render') useEffect(() => { recordPerformanceMark('mount-start') setMounted(true) recordPerformanceMark('mount-complete') }, []) useEffect(() => { if (theme) recordPerformanceMark('theme-available') }, [theme]) return (
Mounted: {mounted.toString()} | Theme: {theme || 'loading'}
) } return PerformanceTestComponent } // Simulate real page component based on Dify's actual theme usage const PageComponent = () => { const [mounted, setMounted] = useState(false) const { theme } = useTheme() useEffect(() => { setMounted(true) }, []) // Simulate common theme usage pattern in Dify const isDark = mounted ? theme === 'dark' : false return (

Dify Application

Current Theme: {mounted ? theme : 'unknown'}
Appearance: {isDark ? 'dark' : 'light'}
) } const TestThemeProvider = ({ children }: { children: React.ReactNode }) => ( {children} ) describe('Real Browser Environment Dark Mode Flicker Test', () => { beforeEach(() => { jest.restoreAllMocks() jest.clearAllMocks() if (typeof window !== 'undefined') { try { window.localStorage.clear() } catch { // ignore when localStorage is replaced with an error-throwing stub } document.documentElement.removeAttribute('data-theme') } }) describe('Page Refresh Scenario Simulation', () => { test('simulates complete page loading process with dark theme', async () => { // Setup: User previously selected dark mode setupMockEnvironment('dark') render( , ) // Check initial client-side rendering state const initialState = { theme: screen.getByTestId('theme-indicator').textContent, appearance: screen.getByTestId('visual-appearance').textContent, } console.log('Initial client state:', initialState) // Wait for theme system to fully initialize await waitFor(() => { expect(screen.getByTestId('theme-indicator')).toHaveTextContent('Current Theme: dark') }) const finalState = { theme: screen.getByTestId('theme-indicator').textContent, appearance: screen.getByTestId('visual-appearance').textContent, } console.log('Final state:', finalState) // Document the state change - this is the source of flicker console.log('State change detection: Initial -> Final') }) test('handles light theme correctly', async () => { setupMockEnvironment('light') render( , ) await waitFor(() => { expect(screen.getByTestId('theme-indicator')).toHaveTextContent('Current Theme: light') }) expect(screen.getByTestId('visual-appearance')).toHaveTextContent('Appearance: light') }) test('handles system theme with dark preference', async () => { setupMockEnvironment('system', true) // system theme, dark preference render( , ) await waitFor(() => { expect(screen.getByTestId('theme-indicator')).toHaveTextContent('Current Theme: dark') }) expect(screen.getByTestId('visual-appearance')).toHaveTextContent('Appearance: dark') }) test('handles system theme with light preference', async () => { setupMockEnvironment('system', false) // system theme, light preference render( , ) await waitFor(() => { expect(screen.getByTestId('theme-indicator')).toHaveTextContent('Current Theme: light') }) expect(screen.getByTestId('visual-appearance')).toHaveTextContent('Appearance: light') }) test('handles no stored theme (defaults to system)', async () => { setupMockEnvironment(null, false) // no stored theme, system prefers light render( , ) await waitFor(() => { expect(screen.getByTestId('theme-indicator')).toHaveTextContent('Current Theme: light') }) }) test('measures timing window of style changes', async () => { setupMockEnvironment('dark') const timingData: Array<{ phase: string; timestamp: number; styles: any }> = [] const TimingPageComponent = createTimingPageComponent(timingData) render( , ) await waitFor(() => { expect(screen.getByTestId('timing-status')).toHaveTextContent('Phase: CSR') }) // Analyze timing and style changes console.log('\n=== Style Change Timeline ===') timingData.forEach((data, index) => { console.log(`${index + 1}. ${data.phase}: bg=${data.styles.backgroundColor}, color=${data.styles.color}`) }) // Check if there are style changes (this is visible flicker) const hasStyleChange = timingData.length > 1 && timingData[0].styles.backgroundColor !== timingData[timingData.length - 1].styles.backgroundColor if (hasStyleChange) console.log('⚠️ Style changes detected - this causes visible flicker') else console.log('✅ No style changes detected') expect(timingData.length).toBeGreaterThan(1) }) }) describe('CSS Application Timing Tests', () => { test('checks CSS class changes causing flicker', async () => { setupMockEnvironment('dark') const cssStates: Array<{ className: string; timestamp: number }> = [] const CSSTestComponent = createCSSTestComponent(cssStates) render( , ) await waitFor(() => { expect(screen.getByTestId('css-classes')).toHaveTextContent('bg-gray-900 text-white') }) console.log('\n=== CSS Class Change Detection ===') cssStates.forEach((state, index) => { console.log(`${index + 1}. ${state.className}`) }) // Check if CSS classes have changed const hasCSSChange = cssStates.length > 1 && cssStates[0].className !== cssStates[cssStates.length - 1].className if (hasCSSChange) { console.log('⚠️ CSS class changes detected - may cause style flicker') console.log(`From: "${cssStates[0].className}"`) console.log(`To: "${cssStates[cssStates.length - 1].className}"`) } expect(hasCSSChange).toBe(true) // We expect to see this change }) }) describe('Edge Cases and Error Handling', () => { test('handles localStorage access errors gracefully', async () => { setupMockEnvironment(null) const mockStorage = { getItem: jest.fn(() => { throw new Error('LocalStorage access denied') }), setItem: jest.fn(), removeItem: jest.fn(), clear: jest.fn(), } Object.defineProperty(window, 'localStorage', { value: mockStorage, configurable: true, }) try { render( , ) // Should fallback gracefully without crashing await waitFor(() => { expect(screen.getByTestId('theme-indicator')).toBeInTheDocument() }) // Should default to light theme when localStorage fails expect(screen.getByTestId('visual-appearance')).toHaveTextContent('Appearance: light') } finally { Reflect.deleteProperty(window, 'localStorage') } }) test('handles invalid theme values in localStorage', async () => { setupMockEnvironment('invalid-theme-value') render( , ) await waitFor(() => { expect(screen.getByTestId('theme-indicator')).toBeInTheDocument() }) // Should handle invalid values gracefully const themeIndicator = screen.getByTestId('theme-indicator') expect(themeIndicator).toBeInTheDocument() }) }) describe('Performance and Regression Tests', () => { test('verifies ThemeProvider position fix reduces initialization delay', async () => { const performanceMarks: Array<{ event: string; timestamp: number }> = [] setupMockEnvironment('dark') expect(window.localStorage.getItem('theme')).toBe('dark') const PerformanceTestComponent = createPerformanceTestComponent(performanceMarks) render( , ) await waitFor(() => { expect(screen.getByTestId('performance-test')).toHaveTextContent('Theme: dark') }) // Analyze performance timeline console.log('\n=== Performance Timeline ===') performanceMarks.forEach((mark) => { console.log(`${mark.event}: ${mark.timestamp.toFixed(2)}ms`) }) expect(performanceMarks.length).toBeGreaterThan(3) }) }) describe('Solution Requirements Definition', () => { test('defines technical requirements to eliminate flicker', () => { const technicalRequirements = { ssrConsistency: 'SSR and CSR must render identical initial styles', synchronousDetection: 'Theme detection must complete synchronously before first render', noStyleChanges: 'No visible style changes should occur after hydration', performanceImpact: 'Solution should not significantly impact page load performance', browserCompatibility: 'Must work consistently across all major browsers', } console.log('\n=== Technical Requirements ===') Object.entries(technicalRequirements).forEach(([key, requirement]) => { console.log(`${key}: ${requirement}`) expect(requirement).toBeDefined() }) // A successful solution should pass all these requirements }) }) })