// src/layout/__tests__/Header.test.tsx import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { render, screen, cleanup, fireEvent } from '@testing-library/react' import { Header } from '../Header' import { LayoutProvider } from '../LayoutContext' const updates = vi.hoisted(() => ({ available: false, apply: vi.fn(), retry: vi.fn(), error: '', flush: vi.fn() })) vi.mock('../../app/pwa-updates', () => ({ updateAvailable: () => updates.available, updateError: () => updates.error, subscribeToUpdates: () => () => {}, applyPwaUpdate: updates.apply, retryPwaUpdate: updates.retry, registerPwaUpdateFlush: updates.flush, })) describe('Header', () => { beforeEach(() => { vi.useFakeTimers() updates.available = false updates.error = '' updates.apply.mockReset() updates.retry.mockReset() }) afterEach(() => { Object.defineProperty(window, 'visualViewport', { value: undefined, writable: true, configurable: true }) vi.useRealTimers() cleanup() }) function renderHeader() { return render(
) } it('displays Reams branding text', () => { renderHeader() expect(screen.getByText('Reams')).toBeInTheDocument() }) it('does not display document title', () => { renderHeader() const header = screen.getByRole('banner') expect(header).not.toHaveTextContent('No document open') }) it('uses the shared action button when an update is ready', () => { updates.available = true renderHeader() expect(screen.getByRole('button', { name: 'Update' })).toHaveClass('reams-action-button', 'header-update-btn') }) it('does not render an editor mode button, including with legacy props', () => { render(
) expect(screen.queryByRole('button', { name: /current mode/i })).not.toBeInTheDocument() }) it('does not register a no-op flush when no before-update callback is provided', () => { renderHeader() expect(updates.flush).not.toHaveBeenCalled() }) it('registers the provided before-update callback as the update flush', () => { const onBeforeUpdate = vi.fn().mockResolvedValue(undefined) render(
) expect(updates.flush).toHaveBeenCalledWith(onBeforeUpdate) }) it('shows a retry button when an update error is reported', () => { updates.error = 'write failed' renderHeader() expect(screen.getByRole('button', { name: 'Retry' })).toBeInTheDocument() expect(screen.getByRole('alert')).toHaveTextContent('write failed') }) it('retries the update when the retry button is clicked', async () => { updates.error = 'write failed' updates.retry.mockResolvedValue(undefined) renderHeader() fireEvent.click(screen.getByRole('button', { name: 'Retry' })) expect(updates.retry).toHaveBeenCalledTimes(1) }) describe('branding', () => { let removeItemSpy: ReturnType let reloadSpy: ReturnType let deleteDatabaseSpy: ReturnType beforeEach(() => { removeItemSpy = vi.spyOn(Storage.prototype, 'removeItem') deleteDatabaseSpy = vi.fn() Object.defineProperty(window, 'indexedDB', { value: { deleteDatabase: deleteDatabaseSpy }, writable: true, configurable: true, }) // Mock location.reload — jsdom doesn't support navigation // Use Object.defineProperty since location.reload is non-configurable reloadSpy = vi.fn() as unknown as ReturnType Object.defineProperty(window, 'location', { value: { ...window.location, reload: reloadSpy }, writable: true, configurable: true, }) }) afterEach(() => { removeItemSpy.mockRestore() Object.defineProperty(window, 'indexedDB', { value: undefined, writable: true, configurable: true }) }) it('preserves local state after repeated branding clicks', () => { localStorage.setItem('reams:ribbon-config', '{}') localStorage.setItem('other-app:data', 'keep') renderHeader() const branding = screen.getByText('Reams') for (let i = 0; i < 8; i++) { fireEvent.click(branding) } expect(removeItemSpy).not.toHaveBeenCalled() expect(deleteDatabaseSpy).not.toHaveBeenCalled() expect(localStorage.getItem('reams:ribbon-config')).toBe('{}') expect(localStorage.getItem('other-app:data')).toBe('keep') expect(reloadSpy).not.toHaveBeenCalled() }) }) })