/** * Tests for Resource component - resource.tsx * * TDD RED phase: Write failing tests first. * * This includes tests for the stale registration bug (ui-n710): * Resource component uses useEffect with only [name] dependency, * so changing view components after mount doesn't update the registry. */ import { describe, it, expect } from 'vitest' import { render, screen, act } from '@testing-library/react' import { useState, type FC } from 'react' import { Resource } from '../../components/resource' import { ResourcesProvider, useResources, type ResourcesContextValue } from '../../context/resource-context' import type { Identifier } from '../../types' // Mock view components const MockListComponent: FC = () =>
List
const UpdatedEditComponent: FC<{ id: Identifier }> = ({ id }) =>
Updated Edit {id}
// Helper to access resources context function ResourcesInspector({ onResources }: { onResources: (ctx: ResourcesContextValue) => void }) { const ctx = useResources() onResources(ctx) return null } describe('Resource Component', () => { describe('basic registration', () => { it('should register resource with ResourcesProvider on mount', () => { let ctx: ResourcesContextValue | undefined render( { ctx = c }} /> ) const resource = ctx!.getResource('users') expect(resource).toBeDefined() expect(resource?.name).toBe('users') expect(resource?.label).toBe('Users') expect(resource?.list).toBe(MockListComponent) }) it('should unregister resource on unmount', () => { let ctx: ResourcesContextValue | undefined const { unmount } = render( { ctx = c }} /> ) expect(ctx!.getResource('users')).toBeDefined() unmount() // After unmount, the resource should be unregistered // Note: We need to re-render the inspector to see the change // This is tricky to test because unmount removes everything }) it('should re-register when name changes', () => { let ctx: ResourcesContextValue | undefined function DynamicResource() { const [name, setName] = useState('users') return ( { ctx = c }} /> ) } render() expect(ctx!.getResource('users')).toBeDefined() expect(ctx!.getResource('products')).toBeUndefined() act(() => { screen.getByTestId('change-name').click() }) // Old resource should be unregistered, new one registered expect(ctx!.getResource('users')).toBeUndefined() expect(ctx!.getResource('products')).toBeDefined() }) }) describe('stale registration bug (ui-n710)', () => { /** * This test demonstrates the stale registration bug: * * When view components (list, edit, etc.) change but name stays the same, * the registry doesn't get updated because useEffect only depends on [name]. * * Current behavior: The registry keeps the OLD definition * Expected behavior: The registry should have the UPDATED definition */ it('should update registry when view components change (currently FAILING due to stale registration)', () => { let ctx: ResourcesContextValue | undefined function DynamicResource() { const [editComponent, setEditComponent] = useState | undefined>(undefined) return ( { ctx = c }} /> ) } render() // Initially no edit component expect(ctx!.getResource('users')?.edit).toBeUndefined() expect(ctx!.getResource('users')?.list).toBe(MockListComponent) // Add edit component - the Resource's props change but name stays the same act(() => { screen.getByTestId('add-edit').click() }) // BUG: This assertion will FAIL because useEffect only runs when name changes // The registry still has the OLD definition without the edit component // // Expected: UpdatedEditComponent // Actual: undefined (stale definition) expect(ctx!.getResource('users')?.edit).toBe(UpdatedEditComponent) }) it('should update registry when label changes', () => { let ctx: ResourcesContextValue | undefined function DynamicResource() { const [label, setLabel] = useState('Users') return ( { ctx = c }} /> ) } render() expect(ctx!.getResource('users')?.label).toBe('Users') act(() => { screen.getByTestId('change-label').click() }) // BUG: This will also fail due to stale registration expect(ctx!.getResource('users')?.label).toBe('Updated Users') }) }) describe('children rendering', () => { it('should render children wrapped in ResourceProvider when provided', () => { render(
Child Content
) expect(screen.getByTestId('child-content').textContent).toBe('Child Content') }) it('should not render anything when no children provided', () => { const { container } = render( ) // Resource returns null when no children expect(container.querySelector('[data-testid]')).toBeNull() }) }) })