From d865b181e94f2616fd0d371eec091ce11c40f7d8 Mon Sep 17 00:00:00 2001 From: Amit Haridas Date: Fri, 5 Jun 2026 15:39:36 +0530 Subject: [PATCH] test(renderer): phase 5 integration smoke test --- tests/integration/phase5-smoke.test.tsx | 162 ++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 tests/integration/phase5-smoke.test.tsx diff --git a/tests/integration/phase5-smoke.test.tsx b/tests/integration/phase5-smoke.test.tsx new file mode 100644 index 0000000..7b9494b --- /dev/null +++ b/tests/integration/phase5-smoke.test.tsx @@ -0,0 +1,162 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { render, screen, fireEvent, act } from '@testing-library/react'; +import { AppShell } from '@/components/layout/AppShell'; +import { useFileStore } from '@/stores/file-store'; +import { useEditorStore } from '@/stores/editor-store'; +import { useAppStore } from '@/stores/app-store'; + +// Mock react-resizable-panels for jsdom (same pattern as AppShell.test.tsx) +vi.mock('@/components/ui/resizable', () => ({ + ResizablePanelGroup: ({ children, direction }: any) => ( +
+ {children} +
+ ), + ResizablePanel: ({ children, defaultSize }: any) => ( +
+ {children} +
+ ), + ResizableHandle: () =>
, +})); + +vi.mock('@/lib/ipc', () => ({ + ipc: { + file: { + open: vi.fn().mockResolvedValue({ ok: false, error: { code: 'NO_BRIDGE', message: 'mock' } }), + read: vi.fn().mockResolvedValue({ ok: true, data: '# hello' }), + write: vi.fn().mockResolvedValue({ ok: true, data: undefined }), + list: vi.fn().mockResolvedValue({ + ok: true, + data: [ + { name: 'README.md', path: '/root/README.md', isDirectory: false }, + { name: 'src', path: '/root/src', isDirectory: true }, + ], + }), + pickFolder: vi.fn().mockResolvedValue({ ok: true, data: '/root' }), + pickFile: vi.fn().mockResolvedValue({ ok: true, data: '/root/README.md' }), + onChange: vi.fn(() => () => {}), + }, + }, +})); + +describe('Phase 5 integration', () => { + beforeEach(() => { + localStorage.clear(); + useFileStore.setState({ + tree: null, + rootPath: null, + expanded: new Set(), + openTabs: [], + activeTabId: null, + }); + useEditorStore.setState({ buffers: new Map(), activeId: null }); + useAppStore.setState({ + sidebarVisible: true, + previewVisible: true, + zenMode: false, + paneSizes: { sidebar: 20, editor: 50, preview: 30 }, + }); + }); + + it('AppShell renders Sidebar (Files + Outline sections) and TabBar', () => { + render(); + expect(screen.getByText('Files')).toBeInTheDocument(); + expect(screen.getByText('Outline')).toBeInTheDocument(); + expect(screen.getByText(/no files open/i)).toBeInTheDocument(); + }); + + it('opening a folder via the Open Folder button populates the tree', async () => { + render(); + const openBtn = screen.getByRole('button', { name: /open folder/i }); + await act(async () => { + fireEvent.click(openBtn); + }); + // After openFolder, the tree should have children + const state = useFileStore.getState(); + expect(state.tree).not.toBeNull(); + expect(state.tree?.children).toHaveLength(2); + expect(state.rootPath).toBe('/root'); + }); + + it('clicking a file in the tree opens it as a tab and populates the editor buffer', async () => { + // Seed the tree directly + useFileStore.setState({ + tree: { + name: 'root', + path: '/root', + isDirectory: true, + loaded: true, + children: [ + { name: 'README.md', path: '/root/README.md', isDirectory: false, children: null }, + ], + }, + rootPath: '/root', + openTabs: [], + activeTabId: null, + }); + + render(); + const fileButton = screen.getByText('README.md').closest('button')!; + await act(async () => { + fireEvent.click(fileButton); + }); + + const state = useFileStore.getState(); + expect(state.openTabs).toHaveLength(1); + expect(state.openTabs[0].id).toBe('/root/README.md'); + expect(state.activeTabId).toBe('/root/README.md'); + + // Editor buffer should have the file content + const buf = useEditorStore.getState().buffers.get('/root/README.md'); + expect(buf?.content).toBe('# hello'); + }); + + it('TabBar shows the open tab and switches active on click', async () => { + useFileStore.setState({ + tree: null, + rootPath: null, + expanded: new Set(), + openTabs: [ + { id: '/a.md', path: '/a.md', title: 'a.md', dirty: false }, + { id: '/b.md', path: '/b.md', title: 'b.md', dirty: false }, + ], + activeTabId: '/a.md', + }); + render(); + const aTab = screen.getByText('a.md').closest('[role="tab"]')!; + const bTab = screen.getByText('b.md').closest('[role="tab"]')!; + expect(aTab).toHaveAttribute('aria-current', 'page'); + await act(async () => { + fireEvent.click(bTab); + }); + expect(useFileStore.getState().activeTabId).toBe('/b.md'); + }); + + it('keyboard shortcut Ctrl+S triggers saveActiveBuffer (no-op when no active tab)', () => { + render(); + fireEvent.keyDown(window, { key: 's', ctrlKey: true }); + // No assertion on side effect, just confirms it doesn't throw + expect(useFileStore.getState().openTabs).toHaveLength(0); + }); + + it('drag-reorder updates the openTabs order', () => { + useFileStore.setState({ + tree: null, + rootPath: null, + expanded: new Set(), + openTabs: [ + { id: '/a.md', path: '/a.md', title: 'a.md', dirty: false }, + { id: '/b.md', path: '/b.md', title: 'b.md', dirty: false }, + { id: '/c.md', path: '/c.md', title: 'c.md', dirty: false }, + ], + activeTabId: '/a.md', + }); + render(); + act(() => { + useFileStore.getState().reorderTabs(0, 2); + }); + const order = useFileStore.getState().openTabs.map((t) => t.id); + expect(order).toEqual(['/b.md', '/c.md', '/a.md']); + }); +});