mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-10 01:09:43 +02:00
fix(tiles): file drops, focus handoffs, pop-out fallback, split merge, tablet default
- A file dragged onto the tile grid navigated the browser away: the single view's drop handler sits on #terminalContainer, hidden while tiles are open. The #tileGrid section now cancels every file dragover and drop (bubble phase, so tab and tile drags stay with _acceptTabDrops), and a drop on a tile uploads its images to THAT tile's session through _uploadAndInsertImages, with the same "Only image files are supported" toast as image-input.js (now in the zh-CN table). - App-driven refocus no longer moves DOM focus into another session's xterm: a remote delete of the focused tile, _reconcileTileGrid and a socket closed with 4003/4004/4010 (_onTileExit) pass focus: false. removeTile gains a focus option; user-initiated removes keep focusing. - Popping out the last tile left the parked terminal's stale content under the popped-out tab (and snapshotted it on the next switch). _selectAfterTileGrid treats a detached session as unusable for both the focused id and the fallback. - "Open group as tiles" and Ctrl/Cmd+click with the grid closed pass mergeSplit: false, so an open split no longer adds its two sessions on top of a set already sized to the group, the count and the window. - Touch-primary devices (primary pointer coarse: iPad, Android tablets) default the Tiles button OFF in getDefaultSettings(); touchscreen laptops (fine primary pointer) keep the desktop default ON. The button, the App Settings chip and the Ctrl+Shift+G gate all resolve an absent key through these defaults, so they agree. CLAUDE.md and the invariants doc say so. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -375,6 +375,10 @@ export function resetGridHarness() {
|
||||
// The CLI catalog the server injects (labels for the harness logos); a test sets its own.
|
||||
delete windowStub.__codemanCliCatalog;
|
||||
section.children = [];
|
||||
// The grid binds its file-drop guard on the section once per app; an earlier
|
||||
// test's app must not still be listening there.
|
||||
section.listeners = {};
|
||||
section.captureFlags = {};
|
||||
main.className = 'main';
|
||||
// A split a test left open moved .terminal-wrap into its container, with
|
||||
// Pane A's header strip in it.
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
* Real code via the shared vm harness (test/mocks/tile-grid-vm.ts). Port: N/A.
|
||||
*/
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { FakeTile, makeGridApp, resetGridHarness, type GridApp } from './mocks/tile-grid-vm.js';
|
||||
import { FakeTile, flushFrames, makeGridApp, resetGridHarness, type GridApp } from './mocks/tile-grid-vm.js';
|
||||
|
||||
const IDS = ['s-a', 's-b', 's-c'];
|
||||
|
||||
@@ -148,3 +148,57 @@ describe('a tiled session deleted elsewhere', () => {
|
||||
expect(app.showWelcome).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// The handoffs above are the APP's choice: focus moves (activeSessionId, the
|
||||
// focus paint) but the keyboard does not. Moving DOM focus into the
|
||||
// neighbour's xterm sent whatever the user was still typing, Enter included,
|
||||
// into another session's agent; the single view sends it nowhere. Only a
|
||||
// removal the user made (the x button, Remove Focused Tile) carries the
|
||||
// keyboard along.
|
||||
describe('an app-driven refocus never moves the keyboard into another session', () => {
|
||||
const live = (id: string) => FakeTile.all.find((t) => t.sessionId === id && !t._destroyed)!;
|
||||
const focusCalls = () => FakeTile.all.reduce((n, t) => n + t.terminal.focus.mock.calls.length, 0);
|
||||
/** A grid on IDS focused on s-b, every terminal built, no focus recorded yet. */
|
||||
function built() {
|
||||
const app = makeGridApp(IDS);
|
||||
app.openTileGrid(IDS, { focusedId: 's-b' });
|
||||
flushFrames();
|
||||
for (const t of FakeTile.all) t.terminal.focus.mockClear();
|
||||
return app;
|
||||
}
|
||||
|
||||
it('a remote delete of the focused tile hands focus to the neighbour, DOM focus untouched', () => {
|
||||
const app = built();
|
||||
app._onSessionDeleted({ id: 's-b' });
|
||||
expect(app._tileGrid.ids).toEqual(['s-a', 's-c']);
|
||||
expect(app.activeSessionId).toBe('s-c');
|
||||
expect(app._tileGrid.focusedId).toBe('s-c');
|
||||
expect(focusCalls()).toBe(0);
|
||||
// No deferred focus left to land later either.
|
||||
expect(app._tileGrid.focusOnConnect ?? null).toBeNull();
|
||||
});
|
||||
|
||||
it.each([4003, 4004, 4010])('a socket the server closed (%i) removes the tile, DOM focus untouched', (code) => {
|
||||
const app = built();
|
||||
app._onTileExit('s-b', live('s-b'), code);
|
||||
expect(app._tileGrid.ids).toEqual(['s-a', 's-c']);
|
||||
expect(app.activeSessionId).toBe('s-c');
|
||||
expect(focusCalls()).toBe(0);
|
||||
});
|
||||
|
||||
it('a reconcile that finds the focused session gone moves focus, DOM focus untouched', () => {
|
||||
const app = built();
|
||||
app.sessions.delete('s-b');
|
||||
app._reconcileTileGrid();
|
||||
expect(app._tileGrid.ids).toEqual(['s-a', 's-c']);
|
||||
expect(app.activeSessionId).toBe('s-a');
|
||||
expect(focusCalls()).toBe(0);
|
||||
});
|
||||
|
||||
it('a removal the user made still carries the keyboard to the neighbour', () => {
|
||||
const app = built();
|
||||
app.removeFocusedTile();
|
||||
expect(app.activeSessionId).toBe('s-c');
|
||||
expect(live('s-c').terminal.focus).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* @fileoverview A file dragged onto the tile grid.
|
||||
*
|
||||
* The single view's file drop (image-input.js) listens on #terminalContainer,
|
||||
* which is hidden while tiles are open. Nothing else cancelled a file drag,
|
||||
* so dropping a screenshot on a tile made the browser open the file in place
|
||||
* of Codeman. Now the grid section itself takes every file drag (bubble
|
||||
* phase): anywhere over it (a tile, an empty cell, a divider, its padding)
|
||||
* dragover and drop are cancelled, so the page never navigates, and a drop on
|
||||
* a tile uploads its images to THAT tile's session, the same upload and the
|
||||
* same "Only image files" toast as the single view. Tab and tile drags are
|
||||
* the targets' own (_acceptTabDrops, capture phase) and stay untouched.
|
||||
*
|
||||
* Real code via the shared vm harness (test/mocks/tile-grid-vm.ts). Port: N/A.
|
||||
*/
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { FakeEl, makeGridApp, resetGridHarness, section, tileEl, type GridApp } from './mocks/tile-grid-vm.js';
|
||||
|
||||
const IDS = ['s-a', 's-b', 's-c'];
|
||||
|
||||
function fileEvent(target: FakeEl, files: Array<{ type: string; name?: string }>, types = ['Files']) {
|
||||
return {
|
||||
target,
|
||||
dataTransfer: { types, files, dropEffect: 'none' },
|
||||
preventDefault: vi.fn(),
|
||||
stopPropagation: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
/** A grid on `ids`, s-a focused, uploads stubbed. Five tiles make a 3x2 with one empty cell. */
|
||||
function gridApp(ids = IDS): GridApp {
|
||||
const app = makeGridApp(ids);
|
||||
app._uploadAndInsertImages = vi.fn();
|
||||
app.openTileGrid(ids, { focusedId: 's-a' });
|
||||
return app;
|
||||
}
|
||||
|
||||
const slot = () => section.children.find((el) => el.classList.contains('tile-slot'))!;
|
||||
const PNG = { type: 'image/png', name: 'shot.png' };
|
||||
|
||||
beforeEach(() => {
|
||||
resetGridHarness();
|
||||
});
|
||||
|
||||
describe('a file dropped on a tile', () => {
|
||||
it("uploads its images to that tile's session (not the focused one), the page staying put", () => {
|
||||
const app = gridApp();
|
||||
// Deep inside the tile, as on xterm's rows or its helper textarea.
|
||||
const inner = tileEl('s-b').querySelector('.tile-body')!;
|
||||
const over = fileEvent(inner, [PNG]);
|
||||
section.dispatch('dragover', over);
|
||||
expect(over.preventDefault).toHaveBeenCalled();
|
||||
expect(over.dataTransfer.dropEffect).toBe('copy');
|
||||
|
||||
const drop = fileEvent(inner, [PNG, { type: 'image/jpeg' }]);
|
||||
section.dispatch('drop', drop);
|
||||
expect(drop.preventDefault).toHaveBeenCalled();
|
||||
expect(app._uploadAndInsertImages).toHaveBeenCalledTimes(1);
|
||||
expect(app._uploadAndInsertImages).toHaveBeenCalledWith([PNG, { type: 'image/jpeg' }], { sessionId: 's-b' });
|
||||
// A drop is not a selection: focus stays where it was.
|
||||
expect(app.activeSessionId).toBe('s-a');
|
||||
});
|
||||
|
||||
it('only the images of a mixed drop are uploaded', () => {
|
||||
const app = gridApp();
|
||||
section.dispatch('drop', fileEvent(tileEl('s-c'), [{ type: 'text/plain' }, PNG]));
|
||||
expect(app._uploadAndInsertImages).toHaveBeenCalledWith([PNG], { sessionId: 's-c' });
|
||||
});
|
||||
|
||||
it('a drop with no image says so, as the single view does, and uploads nothing', () => {
|
||||
const app = gridApp();
|
||||
const drop = fileEvent(tileEl('s-b'), [{ type: 'application/pdf' }]);
|
||||
section.dispatch('drop', drop);
|
||||
expect(drop.preventDefault).toHaveBeenCalled();
|
||||
expect(app._uploadAndInsertImages).not.toHaveBeenCalled();
|
||||
expect(app.showToast).toHaveBeenCalledWith('Only image files are supported', 'error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('a file dragged anywhere else over the grid', () => {
|
||||
it('an empty cell, a divider or the padding: cancelled (no navigation), nothing uploaded', () => {
|
||||
const app = gridApp([...IDS, 's-d', 's-e']);
|
||||
for (const target of [slot(), section]) {
|
||||
expect(target).toBeTruthy();
|
||||
const over = fileEvent(target, [PNG]);
|
||||
section.dispatch('dragover', over);
|
||||
expect(over.preventDefault).toHaveBeenCalled();
|
||||
const drop = fileEvent(target, [PNG]);
|
||||
section.dispatch('drop', drop);
|
||||
expect(drop.preventDefault).toHaveBeenCalled();
|
||||
}
|
||||
expect(app._uploadAndInsertImages).not.toHaveBeenCalled();
|
||||
expect(app.showToast).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('tab and tile drags are left to their targets', () => {
|
||||
it('a drag that carries no files is not touched by the grid section', () => {
|
||||
const app = gridApp();
|
||||
app.draggedTabId = 's-other';
|
||||
const over = fileEvent(section, [], ['text/plain']);
|
||||
section.dispatch('dragover', over);
|
||||
section.dispatch('drop', over);
|
||||
expect(over.preventDefault).not.toHaveBeenCalled();
|
||||
expect(app._uploadAndInsertImages).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("a tab dropped on a tile still replaces it through the tile's own handler", () => {
|
||||
const app = gridApp();
|
||||
app.draggedTabId = 's-other';
|
||||
const drop = fileEvent(tileEl('s-b'), [], ['text/plain']);
|
||||
tileEl('s-b').dispatch('drop', drop);
|
||||
expect(app._tileGrid.ids).toContain('s-other');
|
||||
expect(app._tileGrid.ids).not.toContain('s-b');
|
||||
expect(app._uploadAndInsertImages).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('the guard is installed once', () => {
|
||||
it('opening and closing the grid again never stacks listeners', () => {
|
||||
const app = gridApp();
|
||||
app.closeTileGrid({ reselect: false });
|
||||
app.openTileGrid(IDS);
|
||||
app.closeTileGrid({ reselect: false });
|
||||
app.openTileGrid(IDS);
|
||||
expect(section.listeners.dragover).toHaveLength(1);
|
||||
expect(section.listeners.drop).toHaveLength(1);
|
||||
// Bubble phase: the tab and tile drop targets stop their own drags in capture.
|
||||
expect(section.captureFlags.drop).toEqual([false]);
|
||||
});
|
||||
});
|
||||
@@ -215,6 +215,14 @@ async function exercise() {
|
||||
harvestAll(app, 'zoomed');
|
||||
app.zoomTile('s-1');
|
||||
|
||||
// A file that is not an image dropped on a tile.
|
||||
section.dispatch('drop', {
|
||||
target: section.children.find((el) => el.dataset.sessionId === 's-1'),
|
||||
dataTransfer: { types: ['Files'], files: [{ type: 'application/pdf' }] },
|
||||
preventDefault: vi.fn(),
|
||||
});
|
||||
harvestAll(app, 'file drop');
|
||||
|
||||
// The toasts of a full grid, by the cap and by the window.
|
||||
app._joinTileGridFromRun('s-7');
|
||||
app.addSessionToTiles('s-7');
|
||||
@@ -314,6 +322,7 @@ describe('every tile grid string the code puts on screen translates to zh-CN', (
|
||||
'Could not attach the session',
|
||||
'This group has no session to show as tiles',
|
||||
'No sessions to show as tiles',
|
||||
'Only image files are supported',
|
||||
'Split: unavailable while tiles are open',
|
||||
'Tiles: show several sessions side by side (right-click for how many)',
|
||||
'Tiles: back to a single session (right-click for how many tiles)',
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
// Port: none (pure static analysis, runs in CI, no browser/server).
|
||||
//
|
||||
// `showTileGridButton` (the header Tiles button, and the Ctrl+Shift+G chord) is
|
||||
// a PER-DEVICE setting (default ON on desktop, OFF on handhelds) with the same three-way rule as
|
||||
// a PER-DEVICE setting (default ON on desktop, OFF on handhelds and touch-primary
|
||||
// tablets) with the same three-way rule as
|
||||
// showSplitButton (test/split-pane-per-device-setting.test.ts, read it for the
|
||||
// history): in settings-ui.js's displayKeys merge policy, stripped out of the
|
||||
// object saveAppSettings() PUTs (SettingsUpdateSchema is .strict(), so sending
|
||||
@@ -14,7 +15,9 @@ import { describe, it, expect } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { join } from 'node:path';
|
||||
import vm from 'node:vm';
|
||||
import { JSDOM } from 'jsdom';
|
||||
import { makeGridApp } from './mocks/tile-grid-vm.js';
|
||||
|
||||
const HERE = fileURLToPath(new URL('.', import.meta.url));
|
||||
const PUBLIC = join(HERE, '../src/web/public');
|
||||
@@ -59,6 +62,100 @@ describe('showTileGridButton stays per-device: display key, stripped from the PU
|
||||
});
|
||||
});
|
||||
|
||||
// The real getDefaultSettings() (settings-ui.js) on each kind of device. A
|
||||
// touch-primary tablet (iPad, an Android tablet: primary pointer coarse) is
|
||||
// not a handheld, so it used to take the desktop defaults and get the Tiles
|
||||
// button ON, although a tile has none of the main terminal's touch, IME and
|
||||
// soft-keyboard handling. It defaults OFF there (opt-in, like Split); a
|
||||
// touchscreen laptop (primary pointer fine) keeps the desktop default.
|
||||
describe('the device default: ON on desktop, OFF on handhelds and touch-primary tablets', () => {
|
||||
const SOURCE = readFileSync(join(PUBLIC, 'settings-ui.js'), 'utf8');
|
||||
type Device = { handheld: boolean; touch: boolean; coarse: boolean };
|
||||
function defaultsOn({ handheld, touch, coarse }: Device) {
|
||||
const CodemanApp = function CodemanApp(this: unknown) {};
|
||||
const context = vm.createContext({
|
||||
CodemanApp,
|
||||
VoiceInput: {},
|
||||
localStorage: { getItem: () => null, setItem: () => {} },
|
||||
document: { getElementById: () => null },
|
||||
console,
|
||||
MobileDetection: {
|
||||
isHandheldDevice: () => handheld,
|
||||
isTouchDevice: () => touch,
|
||||
getDeviceType: () => (handheld ? 'mobile' : 'desktop'),
|
||||
},
|
||||
window: {
|
||||
matchMedia: (q: string) => ({
|
||||
matches: q === '(pointer: coarse)' ? coarse : q === '(pointer: fine)' && !coarse,
|
||||
}),
|
||||
},
|
||||
});
|
||||
vm.runInContext(SOURCE, context, { filename: 'settings-ui.js' });
|
||||
const app = Object.create(CodemanApp.prototype) as { getDefaultSettings(): Record<string, unknown> };
|
||||
return app.getDefaultSettings();
|
||||
}
|
||||
/** What every reader resolves an absent key to: the button, the App Settings chip and the chord. */
|
||||
const resolved = (d: Device) => (defaultsOn(d).showTileGridButton ?? true) === true;
|
||||
|
||||
const DESKTOP = { handheld: false, touch: false, coarse: false };
|
||||
const TOUCH_LAPTOP = { handheld: false, touch: true, coarse: false };
|
||||
const TABLET = { handheld: false, touch: true, coarse: true };
|
||||
const PHONE = { handheld: true, touch: true, coarse: true };
|
||||
|
||||
it('a desktop and a touchscreen laptop (fine primary pointer): ON', () => {
|
||||
expect(defaultsOn(DESKTOP).showTileGridButton).toBeUndefined();
|
||||
expect(resolved(DESKTOP)).toBe(true);
|
||||
expect(defaultsOn(TOUCH_LAPTOP).showTileGridButton).toBeUndefined();
|
||||
expect(resolved(TOUCH_LAPTOP)).toBe(true);
|
||||
});
|
||||
|
||||
it('a touch-primary tablet (coarse primary pointer, not a handheld): OFF, and only that key changes', () => {
|
||||
expect(defaultsOn(TABLET)).toEqual({ showTileGridButton: false });
|
||||
expect(resolved(TABLET)).toBe(false);
|
||||
});
|
||||
|
||||
it('a handheld: OFF, as before', () => {
|
||||
expect(defaultsOn(PHONE).showTileGridButton).toBe(false);
|
||||
});
|
||||
|
||||
it('a context with no window at all still answers (the desktop default)', () => {
|
||||
const CodemanApp = function CodemanApp(this: unknown) {};
|
||||
const context = vm.createContext({
|
||||
CodemanApp,
|
||||
VoiceInput: {},
|
||||
localStorage: { getItem: () => null, setItem: () => {} },
|
||||
document: { getElementById: () => null },
|
||||
console,
|
||||
MobileDetection: { isHandheldDevice: () => false, getDeviceType: () => 'desktop' },
|
||||
});
|
||||
vm.runInContext(SOURCE, context, { filename: 'settings-ui.js' });
|
||||
const app = Object.create(CodemanApp.prototype) as { getDefaultSettings(): Record<string, unknown> };
|
||||
expect(app.getDefaultSettings()).toEqual({});
|
||||
});
|
||||
|
||||
it('the Ctrl+Shift+G chord follows the same default: inert on a tablet with nothing stored, live on a touchscreen laptop', () => {
|
||||
const TOGGLE = {
|
||||
type: 'keydown',
|
||||
key: 'G',
|
||||
code: 'KeyG',
|
||||
ctrlKey: true,
|
||||
shiftKey: true,
|
||||
metaKey: false,
|
||||
altKey: false,
|
||||
target: { closest: () => null },
|
||||
};
|
||||
for (const [device, applies] of [
|
||||
[TABLET, false],
|
||||
[TOUCH_LAPTOP, true],
|
||||
] as const) {
|
||||
const app = makeGridApp(['s-a', 's-b']);
|
||||
app.loadAppSettingsFromStorage = () => ({});
|
||||
app.getDefaultSettings = () => defaultsOn(device);
|
||||
expect(app.tileShortcutFor(TOGGLE)).toBe(applies ? 'toggle-tile-grid' : null);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('App Settings search finds Split and Tiles', () => {
|
||||
// The real filter (settings-ui.js) over the real markup: it matches each chip
|
||||
// by its own data-search and its text, so the chips carry their own keywords.
|
||||
|
||||
@@ -201,6 +201,35 @@ describe('moving focus between tiles', () => {
|
||||
expect(app.markIdleAlertSeen).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// The last tile popped out closes the grid. The single view must never try
|
||||
// to show that session: selectSession only raises its window, and the parked
|
||||
// terminal's stale pre-grid content would stay up under its tab (and be
|
||||
// saved as its snapshot on the next switch).
|
||||
it('popping out the LAST tile falls through to the next session, never the popped-out one', () => {
|
||||
const app = makeGridApp(['s-a']);
|
||||
app.openTileGrid(['s-a']);
|
||||
app.$ = () => null;
|
||||
app.selectSession = vi.fn();
|
||||
app._markDetached('s-a', true);
|
||||
expect(app._tilesOwnTerminal()).toBe(false);
|
||||
expect(app.selectSession.mock.calls).toEqual([['s-other', { auto: true }]]);
|
||||
// Nulled before the selection, so the parked buffer is not snapshotted as s-a's.
|
||||
expect(app.activeSessionId).toBeNull();
|
||||
});
|
||||
|
||||
it('popping out the last tile with every other session popped out too: the welcome screen, the parked terminal cleared', () => {
|
||||
const app = makeGridApp(['s-a']);
|
||||
app.detachedSessions.add('s-other');
|
||||
app.openTileGrid(['s-a']);
|
||||
app.$ = () => null;
|
||||
app.selectSession = vi.fn();
|
||||
app._markDetached('s-a', true);
|
||||
expect(app.selectSession).not.toHaveBeenCalled();
|
||||
expect(app.activeSessionId).toBeNull();
|
||||
expect(app.terminal.clear).toHaveBeenCalled();
|
||||
expect(app.showWelcome).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('going Home leaves the grid, remembered', () => {
|
||||
const app = openGrid();
|
||||
app.goHome();
|
||||
|
||||
@@ -100,6 +100,62 @@ describe('opening the grid over an open split', () => {
|
||||
expect(app._tileGrid.ids).toEqual(['s-a', 's-b', 's-c']);
|
||||
expect(app.activeSessionId).toBe('s-a');
|
||||
});
|
||||
|
||||
// The entry points that size their own set never get the split's two
|
||||
// prepended on top of it (that went past the group, the count and the
|
||||
// window's capacity, and auto-zoomed with a "too small" toast).
|
||||
it("'Open group as tiles' over a split opens exactly the group, the split closed", () => {
|
||||
const app = makeGridApp([...IDS, 's-d']);
|
||||
app.tabLayout = {
|
||||
groups: [
|
||||
{
|
||||
id: 'g',
|
||||
name: 'G',
|
||||
refs: [
|
||||
{ kind: 'session', id: 's-c' },
|
||||
{ kind: 'session', id: 's-d' },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const paneB = openSplit(app);
|
||||
|
||||
app.openGroupAsTiles('g');
|
||||
|
||||
expect(paneB.destroy).toHaveBeenCalledTimes(1);
|
||||
expect(app._splitPane).toBeNull();
|
||||
expect(app._tileGrid.ids).toEqual(['s-c', 's-d']);
|
||||
expect(app.activeSessionId).toBe('s-c');
|
||||
});
|
||||
|
||||
it('Ctrl/Cmd+click over a split opens the remembered count in total, never one more', () => {
|
||||
const app = makeGridApp(IDS);
|
||||
localStore.set('codeman:tile-count', '2');
|
||||
openSplit(app);
|
||||
|
||||
app.addSessionToTiles('s-c');
|
||||
|
||||
expect(app._splitPane).toBeNull();
|
||||
// The split's Pane A seeds the set (tileGridOpenSet), the clicked one joins: 2, not 3.
|
||||
expect(app._tileGrid.ids).toEqual(['s-a', 's-c']);
|
||||
expect(app.activeSessionId).toBe('s-c');
|
||||
});
|
||||
|
||||
it('Ctrl/Cmd+click over a split with a remembered grid: the remembered grid wins, not the split', () => {
|
||||
const app = makeGridApp(IDS);
|
||||
app.selectSession = vi.fn();
|
||||
app.openTileGrid(['s-other', 's-c']);
|
||||
app.closeTileGrid({ reselect: false });
|
||||
app.activeSessionId = 's-a';
|
||||
localStore.set('codeman:tile-count', '2');
|
||||
openSplit(app);
|
||||
|
||||
app.addSessionToTiles('s-b');
|
||||
|
||||
expect(app._splitPane).toBeNull();
|
||||
expect(app._tileGrid.ids).toEqual(['s-other', 's-b']);
|
||||
expect(app.activeSessionId).toBe('s-b');
|
||||
});
|
||||
});
|
||||
|
||||
describe('while the grid is open', () => {
|
||||
|
||||
Reference in New Issue
Block a user