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:
Thomas Hallock
2025-09-29 09:44:17 -05:00
parent c95be1df6d
commit 899fc6975f
6 changed files with 350 additions and 2 deletions

View 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()
})
})

View 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()
})
})

View File

@@ -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()
})
})

View 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()
})
})

View File

@@ -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 = {

View File

@@ -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', () => ({