Files
Codeman/test/tile-grid-sse-filter.test.ts
T

87 lines
3.8 KiB
TypeScript
Raw Normal View History

/**
* @fileoverview While the tile grid owns the terminal, the page's SSE filter
* names TILE_GRID_SSE_FILTER (constants.js), which no session matches.
*
* The SSE filter gates only session:terminal batches (server side, pinned by
* test/sse-tile-grid-filter.test.ts). With the grid open those frames were for
* the focused tile alone and were only parsed to be dropped: the main terminal
* is parked and the tiles carry their own output over their own sockets. Both
* places that set the filter ask `_sseFilterSessionId()`: the live re-subscribe
* (`_updateSseSubscription`, run by every tile focus) and the connect URL
* (`connectSSE`, rebuilt by every SSE reconnect with the grid still open).
* Leaving the grid gives the filter back to the session shown.
*
* Real code: the shared vm harness (test/mocks/tile-grid-vm.ts). Port: N/A.
*/
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { beforeEach, describe, expect, it } from 'vitest';
import { fetchSpy, makeGridApp, resetGridHarness, windowStub, type GridApp } from './mocks/tile-grid-vm.js';
const IDS = ['s-a', 's-b', 's-c'];
const FILTER = (windowStub.CodemanTileGrid as { TILE_GRID_SSE_FILTER: string }).TILE_GRID_SSE_FILTER;
/** The app with the REAL _updateSseSubscription, and the filters it posted, in order. */
function makeApp(): GridApp {
const app = makeGridApp(IDS);
delete app._updateSseSubscription;
app._clientId = 'client-1';
return app;
}
const posted = () =>
fetchSpy.mock.calls
.filter(([url]) => url === '/api/events/subscribe')
.map(([, init]) => JSON.parse((init as { body: string }).body).sessions);
beforeEach(() => {
resetGridHarness();
fetchSpy.mockClear();
});
describe('the SSE filter while tiles own the terminal', () => {
it('is a fixed id no session can take', () => {
expect(FILTER).toBe('tile-grid');
// Session ids are UUIDs.
expect(FILTER).not.toMatch(/^[0-9a-f-]{36}$/);
});
it('opening the grid and every tile focus subscribe with it, never a session id', async () => {
const app = makeApp();
app.openTileGrid(IDS);
delete app.selectSession;
await app.selectSession('s-b');
await app.selectSession('s-c');
expect(posted().length).toBeGreaterThanOrEqual(3);
expect(new Set(posted().map((s: string[]) => s.join()))).toEqual(new Set([FILTER]));
});
it('leaving the grid gives the filter back to the session shown', () => {
const app = makeApp();
app.openTileGrid(IDS);
expect(app._sseFilterSessionId('s-a')).toBe(FILTER);
app.closeTileGrid({ keepStored: true, reselect: false });
// What the single view's selectSession (app.js) then posts.
app._updateSseSubscription('s-a');
expect(posted().at(-1)).toEqual(['s-a']);
expect(app._sseFilterSessionId('s-b')).toBe('s-b');
});
it('a page with no grid open (a reload into the single view) filters on the session as before', () => {
const app = makeApp();
expect(app._sseFilterSessionId('s-a')).toBe('s-a');
expect(app._sseFilterSessionId(null)).toBe(null);
app._updateSseSubscription('s-a');
expect(posted()).toEqual([['s-a']]);
});
it('the connect URL asks the same question, so an SSE reconnect with the grid open keeps the filter', () => {
// connectSSE builds an EventSource, which this harness has none of, so its
// URL building is read from source: one place, through the helper.
const app = readFileSync(resolve(import.meta.dirname, '../src/web/public/app.js'), 'utf8');
const start = app.indexOf('const _sseParams = new URLSearchParams(');
expect(start).toBeGreaterThan(-1);
const block = app.slice(start, app.indexOf('this.eventSource = new EventSource(', start));
expect(block).toContain('this._sseFilterSessionId(this.activeSessionId)');
expect(block).not.toMatch(/set\('sessions', this\.activeSessionId\)/);
});
});