/** * 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' // Setup browser environment for testing const setupMockEnvironment = (storedTheme: string | null, systemPrefersDark = false) => { // Mock localStorage const mockStorage = { getItem: jest.fn((key: string) => { if (key === 'theme') return storedTheme return null }), setItem: jest.fn(), removeItem: jest.fn(), } // Mock system theme preference const mockMatchMedia = jest.fn((query: string) => ({ matches: query.includes('dark') && systemPrefersDark, media: query, addListener: jest.fn(), removeListener: jest.fn(), })) if (typeof window !== 'undefined') { Object.defineProperty(window, 'localStorage', { value: mockStorage, configurable: true, }) Object.defineProperty(window, 'matchMedia', { value: mockMatchMedia, configurable: true, }) } return { mockStorage, mockMatchMedia } } // 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.clearAllMocks() }) 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 = () => { const [mounted, setMounted] = useState(false) const { theme } = useTheme() const isDark = mounted ? theme === 'dark' : false // Record timing and styles for each render phase const currentStyles = { backgroundColor: isDark ? '#1f2937' : '#ffffff', color: isDark ? '#ffffff' : '#000000', } timingData.push({ phase: mounted ? 'CSR' : 'Initial', timestamp: performance.now(), styles: currentStyles, }) useEffect(() => { setMounted(true) }, []) return (
Phase: {mounted ? 'CSR' : 'Initial'} | Theme: {theme} | Visual: {isDark ? 'dark' : 'light'}
) } 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 = () => { const [mounted, setMounted] = useState(false) const { theme } = useTheme() const isDark = mounted ? theme === 'dark' : false // Simulate Tailwind CSS class application const className = `min-h-screen ${isDark ? 'bg-gray-900 text-white' : 'bg-white text-black'}` cssStates.push({ className, timestamp: performance.now(), }) useEffect(() => { setMounted(true) }, []) return (
Classes: {className}
) } 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 () => { // Mock localStorage to throw an error const mockStorage = { getItem: jest.fn(() => { throw new Error('LocalStorage access denied') }), setItem: jest.fn(), removeItem: jest.fn(), } if (typeof window !== 'undefined') { Object.defineProperty(window, 'localStorage', { value: mockStorage, configurable: true, }) } 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') }) 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 }> = [] const PerformanceTestComponent = () => { const [mounted, setMounted] = useState(false) const { theme } = useTheme() performanceMarks.push({ event: 'component-render', timestamp: performance.now() }) useEffect(() => { performanceMarks.push({ event: 'mount-start', timestamp: performance.now() }) setMounted(true) performanceMarks.push({ event: 'mount-complete', timestamp: performance.now() }) }, []) useEffect(() => { if (theme) performanceMarks.push({ event: 'theme-available', timestamp: performance.now() }) }, [theme]) return (
Mounted: {mounted.toString()} | Theme: {theme || 'loading'}
) } setupMockEnvironment('dark') 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 }) }) })