import { cleanup, fireEvent, render, screen, waitFor, } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { MachineShell } from '../../src/components/machine/machine-shell'; const { mockUseQuery, fetchSpy } = vi.hoisted(() => ({ mockUseQuery: vi.fn(), fetchSpy: vi.fn(), })); vi.mock('convex/react', () => ({ useQuery: mockUseQuery, })); vi.mock('next/navigation', () => ({ useRouter: () => ({ push: vi.fn(), replace: vi.fn() }), })); vi.mock('sonner', () => ({ toast: { error: vi.fn(), success: vi.fn() }, })); vi.mock('@/components/agent-workspace/xterm-session', () => ({ XtermSession: () =>
terminal
, })); vi.mock('@/components/agent-workspace/code-editor', () => ({ CodeEditor: () =>
editor
, })); const stoppedStatus = { running: false, image: 'spoon/devbox:latest', startedAt: null, memoryLimitBytes: null, containerName: 'spoon-home-gib', }; const tree = { name: '', path: '', type: 'directory', children: [{ name: 'README.md', path: 'README.md', type: 'file' }], }; const jsonResponse = (body: unknown) => ({ ok: true, json: () => Promise.resolve(body), text: () => Promise.resolve(JSON.stringify(body)), }); beforeEach(() => { mockUseQuery.mockReturnValue({ username: 'gib', firstName: 'Gib', enabled: true, }); fetchSpy.mockImplementation((input: RequestInfo | URL) => { const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url; if (url.startsWith('/api/box/status')) { return Promise.resolve(jsonResponse({ status: stoppedStatus })); } if (url.startsWith('/api/box/tree')) { return Promise.resolve(jsonResponse({ tree })); } if (url.startsWith('/api/box/lifecycle')) { return Promise.resolve( jsonResponse({ status: { ...stoppedStatus, running: true, startedAt: new Date().toISOString(), }, }), ); } return Promise.resolve(jsonResponse({})); }); vi.stubGlobal('fetch', fetchSpy); }); afterEach(() => { cleanup(); vi.unstubAllGlobals(); vi.clearAllMocks(); }); describe('MachineShell', () => { it('renders a stopped status card with a Start button', async () => { render(); expect(await screen.findByText('Stopped')).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Start' })).toBeInTheDocument(); }); it('starts the box when Start is clicked', async () => { render(); const startButton = await screen.findByRole('button', { name: 'Start' }); fireEvent.click(startButton); await waitFor(() => expect(fetchSpy).toHaveBeenCalledWith( '/api/box/lifecycle', expect.objectContaining({ method: 'POST', body: JSON.stringify({ action: 'start' }), }), ), ); }); it('renders the home file tree', async () => { render(); expect(await screen.findByText('README.md')).toBeInTheDocument(); }); it('renders the terminal', async () => { render(); expect(await screen.findByText('terminal')).toBeInTheDocument(); }); });