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 = {}; 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) { 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( , ); 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( , ); 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(); }); });