fix: update tutorial tests to use consolidated AbacusDisplayProvider
- Update test imports to use centralized abacus-react provider - Ensure test compatibility with consolidated context management - Maintain test coverage for tutorial celebration functionality 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
62
apps/web/src/app/__tests__/layout.nav.test.tsx
Normal file
62
apps/web/src/app/__tests__/layout.nav.test.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import RootLayout from '../layout'
|
||||
|
||||
// Mock AppNavBar to verify it receives the nav prop
|
||||
const MockAppNavBar = ({ navSlot }: { navSlot?: React.ReactNode }) => (
|
||||
<div data-testid="app-nav-bar">
|
||||
{navSlot && <div data-testid="nav-slot-content">{navSlot}</div>}
|
||||
</div>
|
||||
)
|
||||
|
||||
jest.mock('../../components/AppNavBar', () => ({
|
||||
AppNavBar: MockAppNavBar,
|
||||
}))
|
||||
|
||||
// Mock all context providers
|
||||
jest.mock('../../contexts/AbacusDisplayContext', () => ({
|
||||
AbacusDisplayProvider: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}))
|
||||
|
||||
jest.mock('../../contexts/UserProfileContext', () => ({
|
||||
UserProfileProvider: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}))
|
||||
|
||||
jest.mock('../../contexts/GameModeContext', () => ({
|
||||
GameModeProvider: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}))
|
||||
|
||||
jest.mock('../../contexts/FullscreenContext', () => ({
|
||||
FullscreenProvider: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}))
|
||||
|
||||
describe('RootLayout with nav slot', () => {
|
||||
it('passes nav slot to AppNavBar', () => {
|
||||
const navContent = <div>Memory Lightning</div>
|
||||
const pageContent = <div>Page content</div>
|
||||
|
||||
render(
|
||||
<RootLayout nav={navContent}>
|
||||
{pageContent}
|
||||
</RootLayout>
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('app-nav-bar')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('nav-slot-content')).toBeInTheDocument()
|
||||
expect(screen.getByText('Memory Lightning')).toBeInTheDocument()
|
||||
expect(screen.getByText('Page content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('works without nav slot', () => {
|
||||
const pageContent = <div>Page content</div>
|
||||
|
||||
render(
|
||||
<RootLayout nav={null}>
|
||||
{pageContent}
|
||||
</RootLayout>
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('app-nav-bar')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('nav-slot-content')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('Page content')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
120
apps/web/src/components/__tests__/AppNavBar.integration.test.tsx
Normal file
120
apps/web/src/components/__tests__/AppNavBar.integration.test.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import React, { Suspense } from 'react'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import { vi } from 'vitest'
|
||||
import { AppNavBar } from '../AppNavBar'
|
||||
|
||||
// Mock Next.js hooks
|
||||
vi.mock('next/navigation', () => ({
|
||||
usePathname: () => '/games/matching',
|
||||
useRouter: () => ({
|
||||
push: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock contexts
|
||||
vi.mock('../../contexts/FullscreenContext', () => ({
|
||||
useFullscreen: () => ({
|
||||
isFullscreen: false,
|
||||
toggleFullscreen: vi.fn(),
|
||||
exitFullscreen: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock AbacusDisplayDropdown
|
||||
vi.mock('../AbacusDisplayDropdown', () => ({
|
||||
AbacusDisplayDropdown: () => <div data-testid="abacus-dropdown">Dropdown</div>,
|
||||
}))
|
||||
|
||||
describe('AppNavBar Nav Slot Integration', () => {
|
||||
it('renders actual nav slot content from lazy component', async () => {
|
||||
// Create a lazy component that simulates the @nav slot behavior
|
||||
const MatchingNavContent = () => (
|
||||
<h1 style={{
|
||||
fontSize: '18px',
|
||||
fontWeight: 'bold',
|
||||
background: 'linear-gradient(135deg, #60a5fa, #a78bfa, #f472b6)',
|
||||
backgroundClip: 'text',
|
||||
color: 'transparent',
|
||||
margin: 0
|
||||
}}>
|
||||
🧩 Memory Pairs
|
||||
</h1>
|
||||
)
|
||||
|
||||
const LazyMatchingNav = React.lazy(() => Promise.resolve({ default: MatchingNavContent }))
|
||||
|
||||
const navSlot = (
|
||||
<Suspense fallback={<div data-testid="nav-loading">Loading...</div>}>
|
||||
<LazyMatchingNav />
|
||||
</Suspense>
|
||||
)
|
||||
|
||||
render(<AppNavBar navSlot={navSlot} />)
|
||||
|
||||
// Initially should show loading fallback
|
||||
expect(screen.getByTestId('nav-loading')).toBeInTheDocument()
|
||||
|
||||
// Wait for lazy component to load and render
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('🧩 Memory Pairs')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Verify loading state is gone
|
||||
expect(screen.queryByTestId('nav-loading')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('reproduces the issue: lazy component without Suspense boundary fails to render', async () => {
|
||||
// This test reproduces the actual issue - lazy components need Suspense
|
||||
const MatchingNavContent = () => (
|
||||
<h1>🧩 Memory Pairs</h1>
|
||||
)
|
||||
|
||||
const LazyMatchingNav = React.lazy(() => Promise.resolve({ default: MatchingNavContent }))
|
||||
|
||||
// This is what's happening in the actual app - lazy component without Suspense
|
||||
const navSlot = <LazyMatchingNav />
|
||||
|
||||
// This should throw an error or not render properly
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
try {
|
||||
render(<AppNavBar navSlot={navSlot} />)
|
||||
|
||||
// The lazy component should not render without Suspense
|
||||
expect(screen.queryByText('🧩 Memory Pairs')).not.toBeInTheDocument()
|
||||
} catch (error) {
|
||||
// Expected to fail - lazy components need Suspense boundary
|
||||
expect(error.message).toContain('Suspense')
|
||||
}
|
||||
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('simulates Next.js App Router parallel route slot structure', async () => {
|
||||
// This mimics the actual navSlot structure from Next.js App Router
|
||||
const mockParallelRouteSlot = {
|
||||
$$typeof: Symbol.for('react.element'),
|
||||
type: {
|
||||
$$typeof: Symbol.for('react.lazy'),
|
||||
_payload: Promise.resolve({
|
||||
default: () => <h1>🧩 Memory Pairs</h1>
|
||||
}),
|
||||
_init: (payload: any) => payload.then((module: any) => module.default),
|
||||
},
|
||||
key: null,
|
||||
ref: null,
|
||||
props: {
|
||||
parallelRouterKey: 'nav',
|
||||
segmentPath: ['nav'],
|
||||
template: {},
|
||||
notFoundStyles: []
|
||||
},
|
||||
}
|
||||
|
||||
// This is the structure we're actually receiving from Next.js
|
||||
render(<AppNavBar navSlot={mockParallelRouteSlot as any} />)
|
||||
|
||||
// This should fail to render the content without proper handling
|
||||
expect(screen.queryByText('🧩 Memory Pairs')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,99 @@
|
||||
import React, { Suspense } from 'react'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import { vi } from 'vitest'
|
||||
import { AppNavBar } from '../AppNavBar'
|
||||
|
||||
// Mock Next.js hooks
|
||||
vi.mock('next/navigation', () => ({
|
||||
usePathname: () => '/games/matching',
|
||||
useRouter: () => ({
|
||||
push: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock contexts
|
||||
vi.mock('../../contexts/FullscreenContext', () => ({
|
||||
useFullscreen: () => ({
|
||||
isFullscreen: false,
|
||||
toggleFullscreen: vi.fn(),
|
||||
exitFullscreen: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock AbacusDisplayDropdown
|
||||
vi.mock('../AbacusDisplayDropdown', () => ({
|
||||
AbacusDisplayDropdown: () => <div data-testid="abacus-dropdown">Dropdown</div>,
|
||||
}))
|
||||
|
||||
describe('AppNavBar Suspense Fix', () => {
|
||||
it('renders nav slot content with Suspense boundary (FIXED)', async () => {
|
||||
// Simulate the exact structure that Next.js App Router provides
|
||||
const MatchingNavContent = () => (
|
||||
<h1 style={{
|
||||
fontSize: '18px',
|
||||
fontWeight: 'bold',
|
||||
background: 'linear-gradient(135deg, #60a5fa, #a78bfa, #f472b6)',
|
||||
backgroundClip: 'text',
|
||||
color: 'transparent',
|
||||
margin: 0
|
||||
}}>
|
||||
🧩 Memory Pairs
|
||||
</h1>
|
||||
)
|
||||
|
||||
// Create a lazy component like Next.js does
|
||||
const LazyMatchingNav = React.lazy(() => Promise.resolve({ default: MatchingNavContent }))
|
||||
|
||||
// This is what Next.js App Router passes to our component
|
||||
const navSlot = <LazyMatchingNav />
|
||||
|
||||
render(<AppNavBar navSlot={navSlot} />)
|
||||
|
||||
// Should show loading state briefly
|
||||
expect(screen.getByText('Loading...')).toBeInTheDocument()
|
||||
|
||||
// Wait for the lazy component to load and render
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('🧩 Memory Pairs')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Loading state should be gone
|
||||
expect(screen.queryByText('Loading...')).not.toBeInTheDocument()
|
||||
|
||||
// Game name should be visible in the nav
|
||||
expect(screen.getByText('🧩 Memory Pairs')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('demonstrates the original issue was fixed', async () => {
|
||||
// This test shows that without our Suspense fix, this would have failed
|
||||
const MemoryQuizContent = () => <h1>🧠 Memory Lightning</h1>
|
||||
const LazyMemoryQuizNav = React.lazy(() => Promise.resolve({ default: MemoryQuizContent }))
|
||||
|
||||
const navSlot = <LazyMemoryQuizNav />
|
||||
|
||||
render(<AppNavBar navSlot={navSlot} />)
|
||||
|
||||
// Without Suspense boundary in AppNavBar, this would fail to render
|
||||
// But now it works because we wrap navSlot in Suspense
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('🧠 Memory Lightning')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('shows that lazy components need Suspense to render', () => {
|
||||
// This test shows what happens without Suspense - it should fail
|
||||
const TestContent = () => <h1>Test Content</h1>
|
||||
const LazyTest = React.lazy(() => Promise.resolve({ default: TestContent }))
|
||||
|
||||
// Trying to render lazy component without Suspense should fail
|
||||
expect(() => render(<LazyTest />)).toThrow()
|
||||
})
|
||||
|
||||
it('handles nav slot gracefully when null or undefined', () => {
|
||||
render(<AppNavBar navSlot={null} />)
|
||||
expect(screen.queryByText('Loading...')).not.toBeInTheDocument()
|
||||
|
||||
render(<AppNavBar navSlot={undefined} />)
|
||||
expect(screen.queryByText('Loading...')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
67
apps/web/src/components/__tests__/AppNavBar.test.tsx
Normal file
67
apps/web/src/components/__tests__/AppNavBar.test.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import React from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { vi } from 'vitest'
|
||||
import { AppNavBar } from '../AppNavBar'
|
||||
|
||||
// Mock Next.js hooks
|
||||
vi.mock('next/navigation', () => ({
|
||||
usePathname: () => '/games/matching',
|
||||
useRouter: () => ({
|
||||
push: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock contexts
|
||||
vi.mock('../../contexts/FullscreenContext', () => ({
|
||||
useFullscreen: () => ({
|
||||
isFullscreen: false,
|
||||
toggleFullscreen: vi.fn(),
|
||||
exitFullscreen: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock AbacusDisplayDropdown
|
||||
vi.mock('../AbacusDisplayDropdown', () => ({
|
||||
AbacusDisplayDropdown: () => <div data-testid="abacus-dropdown">Dropdown</div>,
|
||||
}))
|
||||
|
||||
describe('AppNavBar', () => {
|
||||
it('renders navSlot when provided', () => {
|
||||
const navSlot = <div data-testid="nav-slot">🧩 Memory Pairs</div>
|
||||
|
||||
render(<AppNavBar navSlot={navSlot} />)
|
||||
|
||||
expect(screen.getByTestId('nav-slot')).toBeInTheDocument()
|
||||
expect(screen.getByText('🧩 Memory Pairs')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not render nav branding when navSlot is null', () => {
|
||||
render(<AppNavBar navSlot={null} />)
|
||||
|
||||
expect(screen.queryByTestId('nav-slot')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not render nav branding when navSlot is undefined', () => {
|
||||
render(<AppNavBar />)
|
||||
|
||||
expect(screen.queryByTestId('nav-slot')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders minimal variant for game pages', () => {
|
||||
const navSlot = <div data-testid="nav-slot">Game Name</div>
|
||||
|
||||
render(<AppNavBar variant="full" navSlot={navSlot} />)
|
||||
|
||||
// Should auto-detect minimal variant for /games/matching path
|
||||
expect(screen.getByTestId('nav-slot')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders fullscreen toggle button', () => {
|
||||
const navSlot = <div data-testid="nav-slot">Game Name</div>
|
||||
|
||||
render(<AppNavBar navSlot={navSlot} />)
|
||||
|
||||
// Check that fullscreen toggle button is present
|
||||
expect(screen.getByTitle('Enter Fullscreen')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -4,7 +4,7 @@ import { vi } from 'vitest'
|
||||
import { TutorialProvider, useTutorialContext } from '../TutorialContext'
|
||||
import { TutorialPlayer } from '../TutorialPlayer'
|
||||
import { Tutorial, TutorialStep } from '../../../types/tutorial'
|
||||
import { AbacusDisplayProvider } from '@/contexts/AbacusDisplayContext'
|
||||
import { AbacusDisplayProvider } from '@soroban/abacus-react'
|
||||
|
||||
// Mock tutorial data
|
||||
const mockTutorial: Tutorial = {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { vi } from 'vitest'
|
||||
import { TutorialProvider } from '../TutorialContext'
|
||||
import { TutorialPlayer } from '../TutorialPlayer'
|
||||
import { Tutorial } from '../../../types/tutorial'
|
||||
import { AbacusDisplayProvider } from '@/contexts/AbacusDisplayContext'
|
||||
import { AbacusDisplayProvider } from '@soroban/abacus-react'
|
||||
|
||||
// Mock the AbacusReact component to make testing easier
|
||||
vi.mock('@soroban/abacus-react', () => ({
|
||||
|
||||
Reference in New Issue
Block a user