import { describe, it, expect, beforeEach, vi } from 'vitest'; import { render, screen, fireEvent, cleanup } from '@testing-library/react'; import { Journal } from './Journal'; import { appStore } from '../../store'; // Phaser does not initialize under happy-dom. The Journal does not import // Phaser directly, but the @testing-library cleanup chain can transitively // pull modules that do — and we want to keep these tests isolated from // the Phaser runtime regardless. vi.mock('../../game/event-bus', () => ({ eventBus: { on: vi.fn(), off: vi.fn(), emit: vi.fn(), removeAllListeners: vi.fn(), }, })); function resetStore(harvested: string[] = []): void { appStore.setState({ harvestedFragmentIds: harvested }); } describe('Journal (D-24 full-screen modal, MEMR-04, MEMR-05)', () => { beforeEach(() => { cleanup(); resetStore(); }); it('returns null when open=false', () => { const { container } = render( {}} />); expect(container.firstChild).toBeNull(); }); it('renders the empty-state copy from uiStrings when no fragments are harvested', () => { render( {}} />); // From content/seasons/01-soil/ui-strings.yaml: "Nothing yet. Plant something." expect(screen.getByText('Nothing yet. Plant something.')).toBeTruthy(); }); it('renders the body of a harvested fragment in selectable DOM (MEMR-05)', () => { resetStore(['season1.soil.first-bloom']); render( {}} />); // The body in fragments.yaml starts with "The first thing that grew was rosemary." expect(screen.getByText(/The first thing that grew was rosemary/)).toBeTruthy(); // And the article wrapper carries userSelect: text (MEMR-05). const article = document.querySelector('article[data-fragment-id="season1.soil.first-bloom"]'); expect(article).not.toBeNull(); expect((article as HTMLElement).style.userSelect).toBe('text'); }); it('groups fragments by Season (D-24)', () => { resetStore(['season1.soil.first-bloom', 'season1.soil.the-cat']); render( {}} />); // Section header text "Season 1" expect(screen.getByText('Season 1')).toBeTruthy(); // Both fragments rendered. expect(screen.getByText(/The first thing that grew was rosemary/)).toBeTruthy(); expect(screen.getByText(/The cat is missing now too/)).toBeTruthy(); }); it('the close button invokes the onClose callback exactly once', () => { const onClose = vi.fn(); render(); fireEvent.click(screen.getByRole('button', { name: /close journal/i })); expect(onClose).toHaveBeenCalledTimes(1); }); it('aria-label on the dialog is "Memory Journal" (accessibility)', () => { render( {}} />); expect(screen.getByLabelText('Memory Journal')).toBeTruthy(); }); it('silently skips harvested ids that do not resolve to a known Fragment (defensive)', () => { resetStore(['season1.soil.first-bloom', 'season9.bogus.never-shipped']); render( {}} />); // Real one renders; bogus one does not crash, just doesn't appear. expect(screen.getByText(/The first thing that grew was rosemary/)).toBeTruthy(); expect(screen.queryByText(/season9.bogus/)).toBeNull(); }); });