feat(web): /machine dev-box page with status, terminal, and home file browser
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
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: () => <div>terminal</div>,
|
||||
}));
|
||||
|
||||
vi.mock('@/components/agent-workspace/code-editor', () => ({
|
||||
CodeEditor: () => <div>editor</div>,
|
||||
}));
|
||||
|
||||
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(<MachineShell />);
|
||||
expect(await screen.findByText('Stopped')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Start' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('starts the box when Start is clicked', async () => {
|
||||
render(<MachineShell />);
|
||||
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(<MachineShell />);
|
||||
expect(await screen.findByText('README.md')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the terminal', async () => {
|
||||
render(<MachineShell />);
|
||||
expect(await screen.findByText('terminal')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user