Files
Codeman/test/tile-grid-auto-join.test.ts
T
Codeman maintainer a07c663ca9 test(tiles): the grid harness finds a tile's element, and serves the pure helpers
Five tile-grid tests defined the same `tileEl(id)` lookup and three more
inlined it; the harness (test/mocks/tile-grid-vm.ts) now exports it and
they import it. tile-grid-open-set built a second vm context just to read
constants.js, although the harness it already imports has loaded the same
file: it reads windowStub.CodemanTileGrid instead.

No assertion changed. Mutation-checked: tiles without their
data-session-id fail 34 tests across the seven files, and a broken
tileGridOpenSet fails the open-set cases.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 09:37:04 +02:00

128 lines
5.4 KiB
TypeScript

/**
* @fileoverview New sessions started from THIS tab's Run join the open grid.
*
* Every Run path calls `_ensureCreatedSessionVisible` for each session it
* created, then `selectSession(firstId)` (a human selection). With the grid open,
* `_joinTileGridFromRun` (called from there) adds each new session to the next
* free slot, so that selection focuses its tile instead of leaving the grid.
* Sessions created elsewhere (agents, other devices, cron) arrive only by
* `session:created` and never join. A grid already holding what the window
* fits does not take it: Run's selection then shows it alone, with a hint.
*
* A tile that joined before its pane existed resends its size once the pid
* appears (the server spawned the pane at its own default size).
*
* Real code via the shared vm harness (test/mocks/tile-grid-vm.ts); the Run
* hook in session-ui.js is pinned at the source. Port: N/A.
*/
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { FakeTile, makeGridApp, resetGridHarness, section, type GridApp, tileEl } from './mocks/tile-grid-vm.js';
const IDS = ['s-a', 's-b'];
const addSession = (app: GridApp, id: string, workingDir = '/w') =>
app.sessions.set(id, { id, name: id, mode: 'claude', pid: null, workingDir });
beforeEach(() => {
resetGridHarness();
});
describe('Run from this tab', () => {
it('the Run hook is where every Run path makes its new session visible', () => {
const src = readFileSync(resolve(import.meta.dirname, '../src/web/public/session-ui.js'), 'utf8');
const helper = src.slice(src.indexOf('async _ensureCreatedSessionVisible('), src.indexOf('async run() {'));
expect(helper).toContain('this._joinTileGridFromRun?.(sessionId);');
});
it('with the grid open, the new session joins the next free slot; Run then focuses its tile', () => {
const app = makeGridApp(IDS);
app.openTileGrid(IDS);
addSession(app, 's-new');
expect(app._joinTileGridFromRun('s-new')).toBe(true);
expect(app._tileGrid.ids).toEqual(['s-a', 's-b', 's-new']);
// What Run does next: a human selection of the session it created.
app.selectSession('s-new');
expect(app._tilesOwnTerminal()).toBe(true);
expect(app.activeSessionId).toBe('s-new');
});
it('no Attach overlay flashes on the new tile while Run starts its pane', () => {
const app = makeGridApp(IDS);
app.openTileGrid(IDS);
addSession(app, 's-new');
app._joinTileGridFromRun('s-new');
const tile = tileEl('s-new');
const overlay = tile.children[1].children.find((c) => c.className === 'tile-attach');
expect(!overlay || overlay.hidden).toBe(true);
});
it('with the grid closed, nothing joins', () => {
const app = makeGridApp(IDS);
addSession(app, 's-new');
expect(app._joinTileGridFromRun('s-new')).toBe(false);
expect(app._tileGrid?.open ?? false).toBe(false);
});
it('a grid that already holds what the window fits does not take it, and says so', () => {
const app = makeGridApp(IDS);
app.openTileGrid(IDS);
addSession(app, 's-new');
section.getBoundingClientRect = () => ({ width: 1000, height: 400, top: 0, left: 0, right: 1000, bottom: 400 });
expect(app._joinTileGridFromRun('s-new')).toBe(false);
delete (section as unknown as Record<string, unknown>).getBoundingClientRect;
expect(app._tileGrid.ids).toEqual(IDS);
expect(app.showToast).toHaveBeenCalledWith(expect.stringContaining('opens on its own'), 'info');
});
it('a session created ELSEWHERE (session:created) never joins', () => {
const app = makeGridApp(IDS);
app.openTileGrid(IDS);
app.renderSessionTabs = vi.fn();
app.saveSessionOrder = vi.fn();
app.updateCost = vi.fn();
app._onSessionCreated({ id: 's-agent', name: 's-agent', mode: 'claude', pid: 1, workingDir: '/w' });
expect(app.sessions.has('s-agent')).toBe(true);
expect(app._tileGrid.ids).toEqual(IDS);
});
});
describe('a pane that starts after its tile connected (#464)', () => {
// The tile sent its size before there was a PTY; the server dropped it and
// spawned the pane at its default size, so the size must go out again.
const tileOf = (id: string) => FakeTile.all.filter((t) => t.sessionId === id).at(-1)!;
const setPid = (app: GridApp, id: string, pid: number | null) =>
app.sessions.set(id, { ...app.sessions.get(id), pid });
it('resends the size once when the pid appears, and again only for a new PTY', () => {
const app = makeGridApp(IDS);
app.openTileGrid(IDS);
addSession(app, 's-new');
app._joinTileGridFromRun('s-new');
const tile = tileOf('s-new');
app._renderTileChrome();
expect(tile.paneStarted).not.toHaveBeenCalled();
setPid(app, 's-new', 4242);
app._renderTileChrome();
expect(tile.paneStarted).toHaveBeenCalledTimes(1);
app._renderTileChrome();
expect(tile.paneStarted).toHaveBeenCalledTimes(1);
// The pane went away and a new one started (an Attach, a respawned pane).
setPid(app, 's-new', null);
app._renderTileChrome();
setPid(app, 's-new', 4343);
app._renderTileChrome();
expect(tile.paneStarted).toHaveBeenCalledTimes(2);
});
it('a tile made for a session that already runs never asks', () => {
const app = makeGridApp(IDS);
app.openTileGrid(IDS);
app._renderTileChrome();
app._renderTileChrome();
for (const id of IDS) expect(tileOf(id).paneStarted).not.toHaveBeenCalled();
});
});