refactor(web): extract reusable XtermSession from WorkspaceTerminal
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { XtermSession } from '../../src/components/agent-workspace/xterm-session';
|
||||
|
||||
const { openSpy, wsInstances, fetchSpy } = vi.hoisted(() => ({
|
||||
openSpy: vi.fn(),
|
||||
wsInstances: [] as Array<{ url: string }>,
|
||||
fetchSpy: vi.fn(),
|
||||
}));
|
||||
|
||||
class FakeTerminal {
|
||||
cols = 80;
|
||||
rows = 24;
|
||||
options: Record<string, unknown> = {};
|
||||
open = openSpy;
|
||||
loadAddon = vi.fn();
|
||||
onData = vi.fn();
|
||||
onResize = vi.fn();
|
||||
onSelectionChange = vi.fn();
|
||||
dispose = vi.fn();
|
||||
refresh = vi.fn();
|
||||
write = vi.fn();
|
||||
focus = vi.fn();
|
||||
constructor(opts: Record<string, unknown>) {
|
||||
this.options = opts ?? {};
|
||||
}
|
||||
}
|
||||
|
||||
vi.mock('@xterm/xterm', () => ({ Terminal: FakeTerminal }));
|
||||
vi.mock('@xterm/addon-fit', () => ({
|
||||
FitAddon: class {
|
||||
fit = vi.fn();
|
||||
},
|
||||
}));
|
||||
vi.mock('@xterm/addon-web-links', () => ({
|
||||
WebLinksAddon: class {},
|
||||
}));
|
||||
vi.mock('next-themes', () => ({
|
||||
useTheme: () => ({ resolvedTheme: 'dark' }),
|
||||
}));
|
||||
|
||||
class FakeWebSocket {
|
||||
static OPEN = 1;
|
||||
url: string;
|
||||
readyState = 1;
|
||||
binaryType = '';
|
||||
onopen: (() => void) | null = null;
|
||||
onmessage: ((event: unknown) => void) | null = null;
|
||||
onclose: (() => void) | null = null;
|
||||
onerror: (() => void) | null = null;
|
||||
send = vi.fn();
|
||||
close = vi.fn();
|
||||
constructor(url: string) {
|
||||
this.url = url;
|
||||
wsInstances.push(this);
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
wsInstances.length = 0;
|
||||
openSpy.mockClear();
|
||||
fetchSpy.mockReset();
|
||||
fetchSpy.mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ url: 'ws://x' }),
|
||||
text: async () => '',
|
||||
});
|
||||
vi.stubGlobal('WebSocket', FakeWebSocket);
|
||||
vi.stubGlobal('fetch', fetchSpy);
|
||||
vi.stubGlobal(
|
||||
'ResizeObserver',
|
||||
class {
|
||||
observe() {}
|
||||
disconnect() {}
|
||||
unobserve() {}
|
||||
},
|
||||
);
|
||||
Object.defineProperty(document, 'fonts', {
|
||||
configurable: true,
|
||||
value: {
|
||||
ready: Promise.resolve(),
|
||||
load: () => Promise.resolve([]),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe('XtermSession', () => {
|
||||
it('shows the waiting label and does not open a WebSocket while inactive', async () => {
|
||||
render(
|
||||
<XtermSession
|
||||
active={false}
|
||||
tokenUrl='/api/box/terminal-token'
|
||||
sessionKey='box-1'
|
||||
waitingLabel='Workspace is asleep.'
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Workspace is asleep.')).toBeInTheDocument();
|
||||
// Give any (unexpected) async connect a chance to run.
|
||||
await Promise.resolve();
|
||||
expect(fetchSpy).not.toHaveBeenCalled();
|
||||
expect(wsInstances).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('fetches the token URL and opens a WebSocket to the returned url when active', async () => {
|
||||
render(
|
||||
<XtermSession
|
||||
active
|
||||
tokenUrl='/api/box/terminal-token'
|
||||
sessionKey='box-1'
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetchSpy).toHaveBeenCalledWith(
|
||||
'/api/box/terminal-token',
|
||||
expect.objectContaining({ signal: expect.anything() }),
|
||||
),
|
||||
);
|
||||
await waitFor(() => expect(wsInstances).toHaveLength(1));
|
||||
expect(wsInstances[0]?.url).toBe('ws://x');
|
||||
expect(openSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user