feat(web): persistent workspace terminal across tab switches
This commit is contained in:
@@ -3,10 +3,15 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { XtermSession } from '../../src/components/agent-workspace/xterm-session';
|
||||
|
||||
const { openSpy, wsInstances, fetchSpy } = vi.hoisted(() => ({
|
||||
const { openSpy, wsInstances, fetchSpy, fitSpy } = vi.hoisted(() => ({
|
||||
openSpy: vi.fn(),
|
||||
wsInstances: [] as Array<{ url: string }>,
|
||||
wsInstances: [] as Array<{
|
||||
url: string;
|
||||
send: ReturnType<typeof vi.fn>;
|
||||
close: ReturnType<typeof vi.fn>;
|
||||
}>,
|
||||
fetchSpy: vi.fn(),
|
||||
fitSpy: vi.fn(),
|
||||
}));
|
||||
|
||||
class FakeTerminal {
|
||||
@@ -30,7 +35,7 @@ class FakeTerminal {
|
||||
vi.mock('@xterm/xterm', () => ({ Terminal: FakeTerminal }));
|
||||
vi.mock('@xterm/addon-fit', () => ({
|
||||
FitAddon: class {
|
||||
fit = vi.fn();
|
||||
fit = fitSpy;
|
||||
},
|
||||
}));
|
||||
vi.mock('@xterm/addon-web-links', () => ({
|
||||
@@ -60,6 +65,7 @@ class FakeWebSocket {
|
||||
beforeEach(() => {
|
||||
wsInstances.length = 0;
|
||||
openSpy.mockClear();
|
||||
fitSpy.mockClear();
|
||||
fetchSpy.mockReset();
|
||||
fetchSpy.mockResolvedValue({
|
||||
ok: true,
|
||||
@@ -127,4 +133,64 @@ describe('XtermSession', () => {
|
||||
expect(wsInstances[0]?.url).toBe('ws://x');
|
||||
expect(openSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('refits and sends a resize frame when it becomes visible', async () => {
|
||||
const { rerender } = render(
|
||||
<XtermSession
|
||||
active
|
||||
visible={false}
|
||||
tokenUrl='/api/box/terminal-token'
|
||||
sessionKey='box-1'
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(wsInstances).toHaveLength(1));
|
||||
const ws = wsInstances[0]!;
|
||||
// Ignore fits/sends from the initial connect; only the visible toggle matters.
|
||||
fitSpy.mockClear();
|
||||
ws.send.mockClear();
|
||||
|
||||
rerender(
|
||||
<XtermSession
|
||||
active
|
||||
visible
|
||||
tokenUrl='/api/box/terminal-token'
|
||||
sessionKey='box-1'
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(fitSpy).toHaveBeenCalled());
|
||||
await waitFor(() =>
|
||||
expect(ws.send).toHaveBeenCalledWith(
|
||||
expect.stringContaining('"type":"resize"'),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps the WebSocket open when hidden while still active', async () => {
|
||||
const { rerender } = render(
|
||||
<XtermSession
|
||||
active
|
||||
visible
|
||||
tokenUrl='/api/box/terminal-token'
|
||||
sessionKey='box-1'
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(wsInstances).toHaveLength(1));
|
||||
const ws = wsInstances[0]!;
|
||||
|
||||
rerender(
|
||||
<XtermSession
|
||||
active
|
||||
visible={false}
|
||||
tokenUrl='/api/box/terminal-token'
|
||||
sessionKey='box-1'
|
||||
/>,
|
||||
);
|
||||
await Promise.resolve();
|
||||
|
||||
expect(ws.close).not.toHaveBeenCalled();
|
||||
expect(wsInstances).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user