mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 08:59:40 +02:00
refactor(split): move the pane class into terminal-tile.js as TerminalTile
Pure move and rename, no behavior change. The split pane's second terminal (SplitTerminalPane) moves out of terminal-split.js into its own terminal-tile.js (load order 7.4) as TerminalTile, so the tile grid can reuse it. terminal-split.js keeps the split orchestration (picker, divider, auto-collapse) and constructs a TerminalTile for Pane B. Tests follow the class: split-pane-terminal-unit becomes terminal-tile-unit, and the Shift+Enter guard and the two browser suites read terminal-tile.js / window.TerminalTile. The browser suites match master (one pre-existing environmental failure in both). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,7 @@ import { describe, expect, it } from 'vitest';
|
||||
|
||||
const PUBLIC = join(new URL('.', import.meta.url).pathname, '../src/web/public');
|
||||
|
||||
describe.each(['terminal-ui.js', 'terminal-split.js'])('%s Shift/Ctrl+Enter handler', (file) => {
|
||||
describe.each(['terminal-ui.js', 'terminal-tile.js'])('%s Shift/Ctrl+Enter handler', (file) => {
|
||||
const src = readFileSync(join(PUBLIC, file), 'utf8');
|
||||
|
||||
it('swallows every event type for Shift/Ctrl+Enter', () => {
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
* worked all along because Chromium fires no keypress for it.
|
||||
*
|
||||
* The page is the real app served by a real WebServer, so the handlers under test are the ones
|
||||
* terminal-ui.js (the main pane, `app.terminal`) and terminal-split.js (Pane B, a real
|
||||
* `SplitTerminalPane`) attach. Nothing restates their predicate. What stands in for the server is
|
||||
* terminal-ui.js (the main pane, `app.terminal`) and terminal-tile.js (Pane B, a real
|
||||
* `TerminalTile`) attach. Nothing restates their predicate. What stands in for the server is
|
||||
* only the edge: a fetch wrapper records the send-key POSTs instead of letting them reach tmux, and
|
||||
* no session exists behind the ids, so nothing is ever typed into a real pane.
|
||||
*
|
||||
@@ -74,7 +74,7 @@ describe('Shift/Ctrl+Enter through the shipped key handlers (keypress must be sw
|
||||
const mount = document.createElement('div');
|
||||
mount.style.cssText = 'position:fixed;left:0;top:0;width:400px;height:300px;';
|
||||
document.body.appendChild(mount);
|
||||
const pane = new w.SplitTerminalPane(paneBId, mount, { mode: 'claude' });
|
||||
const pane = new w.TerminalTile(paneBId, mount, { mode: 'claude' });
|
||||
void pane.connect().catch(() => {});
|
||||
// What xterm emits here is exactly what Pane B's own onData forwards to its WebSocket.
|
||||
w.__paneBData = [] as string[];
|
||||
@@ -200,7 +200,7 @@ describe('Shift/Ctrl+Enter through the shipped key handlers (keypress must be sw
|
||||
expect(shift.sendKeys).toEqual([]);
|
||||
});
|
||||
|
||||
it("Pane B (terminal-split.js): Shift+Enter and Ctrl+Enter write nothing and POST send-key once for Pane B's own session", async () => {
|
||||
it("Pane B (terminal-tile.js): Shift+Enter and Ctrl+Enter write nothing and POST send-key once for Pane B's own session", async () => {
|
||||
const shift = await pressIn('paneB', 'Shift+Enter');
|
||||
expect(shift.focused).toBe(true);
|
||||
expect(shift.data).toEqual([]);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/** @fileoverview Real Chromium + real WebSocket coverage for SplitTerminalPane (Task 4 of the split-pane-sessions plan). */
|
||||
/** @fileoverview Real Chromium + real WebSocket coverage for TerminalTile (Task 4 of the split-pane-sessions plan). */
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import { chromium, type Browser, type Page } from 'playwright';
|
||||
import { WebServer } from '../src/web/server.js';
|
||||
@@ -6,7 +6,7 @@ import { WebServer } from '../src/web/server.js';
|
||||
const PORT = 3175;
|
||||
const BASE_URL = `http://localhost:${PORT}`;
|
||||
|
||||
describe('SplitTerminalPane in a real browser', () => {
|
||||
describe('TerminalTile in a real browser', () => {
|
||||
let server: WebServer;
|
||||
let browser: Browser;
|
||||
let page: Page;
|
||||
@@ -37,7 +37,7 @@ describe('SplitTerminalPane in a real browser', () => {
|
||||
// Testing section) — the shell PTY only spawns once this is called, and
|
||||
// without it the WS opens but no bytes ever flow, and the echo assertion
|
||||
// below would hang until its own timeout for reasons unrelated to
|
||||
// SplitTerminalPane.
|
||||
// TerminalTile.
|
||||
await fetch(`/api/sessions/${id}/shell`, { method: 'POST' });
|
||||
return id;
|
||||
});
|
||||
@@ -48,7 +48,7 @@ describe('SplitTerminalPane in a real browser', () => {
|
||||
mount.style.height = '300px';
|
||||
document.body.appendChild(mount);
|
||||
|
||||
const pane = new (window as any).SplitTerminalPane(id, mount);
|
||||
const pane = new (window as any).TerminalTile(id, mount);
|
||||
pane.connect();
|
||||
|
||||
// Wait for the WS to open, then send a real input frame — testMode's
|
||||
@@ -107,7 +107,7 @@ describe('SplitTerminalPane in a real browser', () => {
|
||||
});
|
||||
const id = (await res.json()).data.session.id;
|
||||
await fetch(`/api/sessions/${id}/shell`, { method: 'POST' });
|
||||
// Write directly to the session (not through SplitTerminalPane, which
|
||||
// Write directly to the session (not through TerminalTile, which
|
||||
// does not exist yet). Poll the real ?full=1 capture (same endpoint
|
||||
// connect() below will use) rather than a fixed delay — the shell's
|
||||
// own startup can race an early write and, on this box, a startup
|
||||
@@ -135,7 +135,7 @@ describe('SplitTerminalPane in a real browser', () => {
|
||||
mount.style.height = '300px';
|
||||
document.body.appendChild(mount);
|
||||
|
||||
const pane = new (window as any).SplitTerminalPane(id, mount);
|
||||
const pane = new (window as any).TerminalTile(id, mount);
|
||||
await pane.connect();
|
||||
|
||||
// xterm's write() parses asynchronously (it queues data and processes it
|
||||
@@ -191,7 +191,7 @@ describe('SplitTerminalPane in a real browser', () => {
|
||||
mount.style.height = '300px';
|
||||
document.body.appendChild(mount);
|
||||
|
||||
const pane = new (window as any).SplitTerminalPane(id, mount);
|
||||
const pane = new (window as any).TerminalTile(id, mount);
|
||||
await pane.connect();
|
||||
await new Promise((resolve) => {
|
||||
const check = () => (pane._wsReady ? resolve(undefined) : setTimeout(check, 100));
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// test/split-pane-terminal-unit.test.ts
|
||||
// Port: N/A (no server/browser; SplitTerminalPane is loaded via `vm`, like
|
||||
// test/terminal-tile-unit.test.ts
|
||||
// Port: N/A (no server/browser; TerminalTile is loaded via `vm`, like
|
||||
// split-pane-auto-collapse-unit.test.ts loads the CodemanApp patches).
|
||||
//
|
||||
// Unit coverage for the two SplitTerminalPane (terminal-split.js) fixes from
|
||||
// Unit coverage for the two TerminalTile (terminal-tile.js, the split pane's
|
||||
// Pane B until it moved out of terminal-split.js) fixes from
|
||||
// the final review of #453 that need no browser: destroy() nulling EVERY socket
|
||||
// handler (onclose used to survive it and fire its "disconnected" write into a
|
||||
// pane already torn down), and the `{t:'r'}` server-refresh path being
|
||||
@@ -15,7 +16,7 @@
|
||||
// The last block covers the scroll-to-top history pull: a burst of output leaves
|
||||
// a shell pane's xterm with about one screen of scrollback while tmux holds every
|
||||
// line, and Pane B (a separate xterm from the primary pane) never went back to
|
||||
// ask. See _maybeLoadMoreHistory / _pullHistory in terminal-split.js.
|
||||
// ask. See _maybeLoadMoreHistory / _pullHistory in terminal-tile.js.
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import vm from 'node:vm';
|
||||
@@ -73,9 +74,9 @@ const fetchMock = vi.fn();
|
||||
const rafQueue: Array<() => void> = [];
|
||||
/** Recorded deadline timers (see the context's setTimeout); `fn` aborts the request. */
|
||||
const deadlines: Array<{ fn: () => void; ms: number; cleared: boolean }> = [];
|
||||
const SOURCE = readFileSync(resolve(import.meta.dirname, '../src/web/public/terminal-split.js'), 'utf8');
|
||||
const SOURCE = readFileSync(resolve(import.meta.dirname, '../src/web/public/terminal-tile.js'), 'utf8');
|
||||
|
||||
function loadSplitTerminalPane() {
|
||||
function loadTerminalTile() {
|
||||
const context = vm.createContext({
|
||||
console: { ...console, log: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
||||
// The primary pane's row estimator, reduced to a line count: the pull only
|
||||
@@ -111,18 +112,18 @@ function loadSplitTerminalPane() {
|
||||
});
|
||||
// The module's tail patches CodemanApp.prototype; nothing on it runs here.
|
||||
vm.runInContext(`class CodemanApp { _onSessionDeleted() {} selectSession() {} }\n${SOURCE}`, context);
|
||||
return (context.window as { SplitTerminalPane: new (id: string, mount: unknown, opts?: object) => PaneUnderTest })
|
||||
.SplitTerminalPane;
|
||||
return (context.window as { TerminalTile: new (id: string, mount: unknown, opts?: object) => PaneUnderTest })
|
||||
.TerminalTile;
|
||||
}
|
||||
|
||||
const SplitTerminalPane = loadSplitTerminalPane();
|
||||
const TerminalTile = loadTerminalTile();
|
||||
|
||||
function makePane(
|
||||
mode = 'claude',
|
||||
mount: unknown = {},
|
||||
opts: { detachedSessions?: Set<string> } = {}
|
||||
): PaneUnderTest & { terminal: FakeTerminal } {
|
||||
const pane = new SplitTerminalPane('s1', mount, { mode, ...opts });
|
||||
const pane = new TerminalTile('s1', mount, { mode, ...opts });
|
||||
pane.terminal = {
|
||||
// xterm invokes a write's callback once everything before it is parsed.
|
||||
write: vi.fn((_data: string, done?: () => void) => done?.()),
|
||||
@@ -187,7 +188,7 @@ beforeEach(() => {
|
||||
clock = 0;
|
||||
});
|
||||
|
||||
describe('SplitTerminalPane.destroy()', () => {
|
||||
describe('TerminalTile.destroy()', () => {
|
||||
it('nulls every WebSocket handler, onclose included, before closing the socket', () => {
|
||||
const pane = makePane();
|
||||
const terminal = pane.terminal;
|
||||
@@ -210,7 +211,7 @@ describe('SplitTerminalPane.destroy()', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('SplitTerminalPane server-refresh single-flight', () => {
|
||||
describe('TerminalTile server-refresh single-flight', () => {
|
||||
it('a refresh with nothing in flight clears and fetches straight away', async () => {
|
||||
const pane = makePane();
|
||||
fetchMock.mockResolvedValueOnce(jsonResponse('one'));
|
||||
@@ -329,7 +330,7 @@ describe('SplitTerminalPane server-refresh single-flight', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('SplitTerminalPane scroll-to-top history pull', () => {
|
||||
describe('TerminalTile scroll-to-top history pull', () => {
|
||||
it('a shell pane at the top pulls a bounded window of full history and replays it', async () => {
|
||||
const pane = makePane('shell');
|
||||
const term = pane.terminal;
|
||||
Reference in New Issue
Block a user