// @vitest-environment jsdom import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import { cleanup, fireEvent, render, waitFor, within } from '@testing-library/react' import React from 'react' // The terminals use xterm + ResizeObserver, which don't belong in a jsdom unit // test. Stub them — the rest of the workbench renders for real against the mock. vi.mock('../src/renderer/src/terminals', () => { let n = 0 return { Terminal: () => null, lid: () => ++n } }) import { App } from '../src/renderer/src/App' import { ProjectProvider } from '../src/renderer/src/project' beforeAll(() => { // jsdom gaps used by the tree/tab code. globalThis.ResizeObserver = class { observe() {} unobserve() {} disconnect() {} } as never if (!Element.prototype.scrollIntoView) Element.prototype.scrollIntoView = () => {} }) afterEach(() => { cleanup(); localStorage.clear() }) function renderApp(): HTMLElement { const { container } = render( , ) return container } function rowWithText(container: HTMLElement, selector: string, text: string): HTMLElement | undefined { return Array.from(container.querySelectorAll(selector)).find((el) => el.textContent?.includes(text)) } // The tree opens fully collapsed, so expand each ancestor folder before reaching // a nested file. Each folder is clicked exactly once (a second click collapses). async function expandTo(c: HTMLElement, ...folders: string[]): Promise { for (const name of folders) { const row = await waitFor(() => { const r = rowWithText(c, '.tree-row', name) if (!r) throw new Error(`folder ${name} not ready`) return r }) fireEvent.click(row) } } describe('App (mock data, jsdom)', () => { it('renders the four-column workbench with the git change list', async () => { const c = renderApp() await waitFor(() => expect(rowWithText(c, '.git-row', 'UserController.php')).toBeTruthy()) expect(c.querySelector('.workbench')).toBeTruthy() expect(c.querySelector('.commit-box')).toBeTruthy() // git panel expect(c.querySelector('.tree-body')).toBeTruthy() // explorer // no file open yet expect(c.textContent).toContain('No file open') }) it('opens a changed file from the git panel into the diff view', async () => { const c = renderApp() const row = await waitFor(() => { const r = rowWithText(c, '.git-row', 'UserController.php') if (!r) throw new Error('row not ready') return r }) fireEvent.click(row) // no tabs anymore — the file opens straight into the editor's diff toolbar, // and its path shows in the title-bar breadcrumb. await waitFor(() => expect(c.querySelector('.diff-bar')).toBeTruthy()) expect(c.querySelector('.diff-bar')?.textContent).toContain('Modified') expect(rowWithText(c, '.tb-crumb', 'UserController.php')).toBeTruthy() }) it('makes an edited buffer dirty (breadcrumb dot)', async () => { const c = renderApp() await expandTo(c, 'public', 'assets') const treeRow = await waitFor(() => { const r = rowWithText(c, '.tree-row', 'store.js') if (!r) throw new Error('tree not ready') return r }) fireEvent.click(treeRow) // unchanged file → editable "code" mode const ta = await waitFor(() => { const t = c.querySelector('.ce-ta') if (!t) throw new Error('editor not ready') return t }) expect(c.querySelector('.tb-dirty')).toBeNull() fireEvent.change(ta, { target: { value: '// edited\n' } }) await waitFor(() => expect(c.querySelector('.tb-dirty')).toBeTruthy()) }) it('searches file contents from the search modal', async () => { const c = renderApp() await waitFor(() => expect(rowWithText(c, '.git-row', 'UserController.php')).toBeTruthy()) const searchBtn = rowWithText(c, '.tb-btn', 'Search')! fireEvent.click(searchBtn) const modal = await waitFor(() => { const m = c.querySelector('.search-modal') if (!m) throw new Error('modal not open') return m as HTMLElement }) const input = within(modal).getByPlaceholderText(/Search content/i) fireEvent.change(input, { target: { value: 'balance' } }) await waitFor(() => expect(modal.querySelectorAll('.sr-file').length).toBeGreaterThan(0)) }) })