mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-11 09:49:41 +02:00
The Tiles toggle used to fill a remembered grid to the count picked in the menu (refilling a hole the user made, or trimming tiles), removing the last tile or "Open group as tiles" forgot it, and Ctrl/Cmd+click repacked it. The owner asked for the opposite: "when I turn tiles off and on, always keep what the last setting was". Now the layout the user arranged (which session sits in which cell, holes included, the tile count, the divider sizes, the focused tile and a zoom they chose) is written to localStorage on every change and comes back exactly from the toggle, Ctrl+Shift+G and a reload, however the grid was closed (toggle, a non-tiled tab, leaveTiles or a #session= link, Home, the width gate, the last tile, a group, closing or killing sessions). Nothing of it reaches the server. - The stored value stays v: 1 and gains `count`: how many tiles the user's own last change left. A session that goes away by itself (deleted, popped out, socket refused) does not lower it, so the next activation fills that cell from the ranking; a hole the user made stays a hole. - sanitizeTileGridState reports `freed` cells (sessions gone since) and derives `count` for values written before it; restoreTileGridCells (pure) puts a stored grid back, freed cells first, never trimmed to the window (a too-small window shows the focused tile until it fits). - Only when none of the stored sessions survive does activation rank from scratch. A count picked in the menu still re-forms the grid to it. - Nothing is persisted while a stored grid is being put back, and a #session= link only flips `open`, keeping gone ids for the fill. - The hover card no longer promises "a click opens N" when a stored grid will open instead. This supersedes decision 10's answer that the count wins over a remembered grid's size; the affected tests say so. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
481 lines
20 KiB
TypeScript
481 lines
20 KiB
TypeScript
/**
|
|
* @fileoverview The header Tiles button and its right-click count menu (owner
|
|
* decision 10, which replaced the session picker).
|
|
*
|
|
* - The button is opt-in (`showTileGridButton`, hidden by its `--hidden` marker
|
|
* class) and hard-gated to desktop widths like Split: a JS width check plus a
|
|
* CSS `@media (max-width: 1179px)` backstop, and never in a solo window.
|
|
* With the grid open it closes it (`aria-pressed`).
|
|
* - Right-click (contextmenu, which Shift+F10 and the Menu key fire too) opens
|
|
* a menu of three counts, 2, 4 and 6, each with its shape, the remembered
|
|
* count checked (default 6). No session names in it, so no user text.
|
|
* - A count the window cannot fit is greyed out with the reason, and the
|
|
* keyboard starts on the largest that fits.
|
|
* - Arrows move over the counts that fit (wrapping), Home/End, Enter or Space
|
|
* picks, Escape closes and gives the keyboard back to the Tiles button (the
|
|
* menu owns its Escape in the global handler, like the tab-group menu), Tab
|
|
* and a click elsewhere close it.
|
|
* - A pick is remembered per device (`codeman:tile-count`; `codeman:tile-grid`
|
|
* holds ids and layout, never content) and opens that many tiles: the
|
|
* session to focus included, the rest from the ranking (all quiet and
|
|
* unstamped here: tab order). The click and Ctrl+Shift+G then open with it
|
|
* when no grid is stored (a stored one comes back as it was:
|
|
* tile-grid-layout-memory.test.ts).
|
|
* - With the grid open a pick re-forms it: a shape change under the cell
|
|
* model's rule, the focused tile always kept, tiles dropped from the end,
|
|
* new ones filling the empty cells first, all mounted and laid out before
|
|
* any connects. Fewer open sessions than the count give fewer tiles.
|
|
* - The session picker is gone (no method, no CSS, no markup hook).
|
|
*
|
|
* Real code via the shared vm harness (test/mocks/tile-grid-vm.ts). Port: N/A.
|
|
*/
|
|
import { readFileSync } from 'node:fs';
|
|
import { resolve } from 'node:path';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import {
|
|
FakeEl,
|
|
FakeTile,
|
|
activeElement,
|
|
body,
|
|
bySelector,
|
|
flushFrames,
|
|
localStore,
|
|
makeGridApp,
|
|
resetGridHarness,
|
|
windowStub,
|
|
type GridApp,
|
|
} from './mocks/tile-grid-vm.js';
|
|
|
|
const IDS = ['s-a', 's-b', 's-c', 's-d', 's-e', 's-f', 's-g'];
|
|
const PUBLIC = resolve(import.meta.dirname, '../src/web/public');
|
|
const css = readFileSync(resolve(PUBLIC, 'styles.css'), 'utf8');
|
|
const html = readFileSync(resolve(PUBLIC, 'index.html'), 'utf8');
|
|
const appSrc = readFileSync(resolve(PUBLIC, 'app.js'), 'utf8');
|
|
const gridSrc = readFileSync(resolve(PUBLIC, 'tile-grid.js'), 'utf8');
|
|
|
|
function makeButton() {
|
|
const btn = new FakeEl();
|
|
btn.className = 'btn-icon-header btn-tile-grid btn-tile-grid--hidden';
|
|
bySelector.set('.btn-tile-grid', btn);
|
|
return btn;
|
|
}
|
|
|
|
const menu = () => body.children.find((c) => c.id === 'tileCountMenu') ?? null;
|
|
const items = () => menu()!.children.filter((c) => c.attrs.role === 'menuitemradio');
|
|
const item = (n: number) => items().find((i) => i.dataset.count === String(n))!;
|
|
const hint = () => menu()!.children.find((c) => c.className === 'tile-count-hint') ?? null;
|
|
const focused = () => (activeElement() as FakeEl | null)?.dataset?.count ?? null;
|
|
const key = (app: GridApp, k: string) => {
|
|
const ev = { key: k, preventDefault: vi.fn() };
|
|
app._tileCountMenu.onKey(ev);
|
|
return ev;
|
|
};
|
|
const open = (app: GridApp) => app.openTileCountMenu({ preventDefault: vi.fn() });
|
|
|
|
let wrapRect = { width: 2400, height: 1200 };
|
|
beforeEach(() => {
|
|
resetGridHarness();
|
|
wrapRect = { width: 2400, height: 1200 };
|
|
const wrap = new FakeEl();
|
|
wrap.getBoundingClientRect = () => ({ ...wrapRect, top: 0, left: 0, right: wrapRect.width, bottom: wrapRect.height });
|
|
bySelector.set('.terminal-wrap', wrap);
|
|
makeButton();
|
|
});
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
function gridApp(ids = IDS): GridApp {
|
|
const app = makeGridApp(ids);
|
|
app.selectSession = vi.fn((id: string) => app._selectTiledSession(id, {}));
|
|
return app;
|
|
}
|
|
|
|
describe('the Tiles button', () => {
|
|
it('shows only when its setting is on and the window is desktop-wide', () => {
|
|
const app = makeGridApp(IDS);
|
|
const btn = makeButton();
|
|
app._applyTileGridButtonVisibility(false);
|
|
expect(btn.classList.contains('btn-tile-grid--hidden')).toBe(true);
|
|
app._applyTileGridButtonVisibility(true);
|
|
expect(btn.classList.contains('btn-tile-grid--hidden')).toBe(false);
|
|
windowStub.innerWidth = 1100;
|
|
app._applyTileGridButtonVisibility(true);
|
|
expect(btn.classList.contains('btn-tile-grid--hidden')).toBe(true);
|
|
});
|
|
|
|
it('never in a solo window', () => {
|
|
const app = makeGridApp(IDS);
|
|
app.isSoloWindow = true;
|
|
const btn = makeButton();
|
|
app._applyTileGridButtonVisibility(true);
|
|
expect(btn.classList.contains('btn-tile-grid--hidden')).toBe(true);
|
|
});
|
|
|
|
it('has the CSS backstops: the hidden marker, the 1179px media query, solo mode', () => {
|
|
expect(css).toMatch(/\.btn-tile-grid--hidden\s*\{\s*display: none !important;/);
|
|
expect(css).toMatch(
|
|
/@media \(max-width: 1179px\)\s*\{[^}]*\.btn-icon-header\.btn-tile-grid[^{]*\{\s*display: none !important;/
|
|
);
|
|
expect(css).toMatch(/body\.solo-mode \.btn-tile-grid,/);
|
|
});
|
|
|
|
it('with the grid open, a click closes it, and the button says so meanwhile', () => {
|
|
const app = gridApp(IDS.slice(0, 3));
|
|
const btn = bySelector.get('.btn-tile-grid')!;
|
|
app.openTileGrid(IDS.slice(0, 3));
|
|
expect(btn.getAttribute('aria-pressed')).toBe('true');
|
|
expect(btn.classList.contains('tiles-open')).toBe(true);
|
|
expect(btn.getAttribute('aria-label')).toBe('Tiles: back to a single session (right-click for how many tiles)');
|
|
app.toggleTileGrid();
|
|
expect(app._tilesOwnTerminal()).toBe(false);
|
|
expect(btn.getAttribute('aria-pressed')).toBe('false');
|
|
expect(btn.getAttribute('aria-label')).toBe('Tiles: show several sessions side by side (right-click for how many)');
|
|
// No native title (the hover card says it, tile-grid-hint.test.ts).
|
|
expect(btn.title).toBe('');
|
|
expect(menu()).toBeNull();
|
|
});
|
|
|
|
it('right-click opens the count menu, the click still toggles (one function with Ctrl+Shift+G)', () => {
|
|
expect(html).toContain('onclick="app.toggleTileGrid()" oncontextmenu="app.openTileCountMenu(event)"');
|
|
expect(html).toContain('aria-label="Tiles: show several sessions side by side (right-click for how many)"');
|
|
expect(html).toContain('aria-describedby="tileGridHint"');
|
|
});
|
|
});
|
|
|
|
describe('the count menu', () => {
|
|
it('offers 2, 4 and 6 with their shapes, the default 6 checked and focused; no user text', () => {
|
|
const app = gridApp();
|
|
const ev = { preventDefault: vi.fn() };
|
|
app.openTileCountMenu(ev);
|
|
expect(ev.preventDefault).toHaveBeenCalled();
|
|
expect(menu()!.attrs.role).toBe('menu');
|
|
expect(menu()!.attrs['aria-label']).toBe('How many tiles');
|
|
expect(items().map((i) => i.dataset.count)).toEqual(['2', '4', '6']);
|
|
expect(items().map((i) => i.attrs['aria-checked'])).toEqual(['false', 'false', 'true']);
|
|
expect(items().map((i) => i.children[1].textContent)).toEqual(['2 tiles', '4 tiles', '6 tiles']);
|
|
// Each shape is the grid's own layout for that count: 2x1, 2x2, 3x2 cells.
|
|
expect(items().map((i) => i.children[0].children.length)).toEqual([2, 4, 6]);
|
|
expect(items().every((i) => !i.disabled)).toBe(true);
|
|
expect(hint()).toBeNull();
|
|
expect(focused()).toBe('6');
|
|
expect(app._tilesOwnTerminal()).toBe(false);
|
|
// No session name anywhere in it.
|
|
expect(JSON.stringify(menu()!.children.map((c) => c.textContent))).not.toMatch(/s-[a-g]/);
|
|
});
|
|
|
|
it('checks the remembered count; a bad stored value reads as the default', () => {
|
|
const app = gridApp();
|
|
localStore.set('codeman:tile-count', '4');
|
|
open(app);
|
|
expect(items().map((i) => i.attrs['aria-checked'])).toEqual(['false', 'true', 'false']);
|
|
expect(focused()).toBe('4');
|
|
app.closeTileCountMenu();
|
|
localStore.set('codeman:tile-count', '5');
|
|
open(app);
|
|
expect(item(6).attrs['aria-checked']).toBe('true');
|
|
});
|
|
|
|
it('greys out a count the window cannot fit, says why, and starts on the largest that fits', () => {
|
|
const app = gridApp();
|
|
wrapRect = { width: 1200, height: 900 }; // fits 4 (2x2 of 600x450), not 3x2
|
|
open(app);
|
|
expect(item(6).disabled).toBe(true);
|
|
expect(item(6).attrs['aria-disabled']).toBe('true');
|
|
expect(item(6).title).toBe('This window fits 4 tiles');
|
|
expect(item(4).disabled).toBe(false);
|
|
expect(hint()!.textContent).toBe('This window fits 4 tiles');
|
|
// The remembered 6 stays checked (owner answer), the keyboard starts on 4.
|
|
expect(item(6).attrs['aria-checked']).toBe('true');
|
|
expect(focused()).toBe('4');
|
|
app.closeTileCountMenu();
|
|
wrapRect = { width: 1000, height: 400 }; // fits 2
|
|
open(app);
|
|
expect(items().map((i) => i.disabled)).toEqual([false, true, true]);
|
|
expect(hint()!.textContent).toBe('This window fits 2 tiles');
|
|
});
|
|
|
|
it('a greyed count is never picked, by click or by Enter', () => {
|
|
const app = gridApp();
|
|
wrapRect = { width: 1200, height: 900 };
|
|
open(app);
|
|
item(6).dispatch('click', { stopPropagation: vi.fn() });
|
|
expect(menu()).not.toBeNull();
|
|
item(6).focus();
|
|
key(app, 'Enter');
|
|
expect(app._tilesOwnTerminal()).toBe(false);
|
|
expect(localStore.has('codeman:tile-count')).toBe(false);
|
|
});
|
|
|
|
it('arrows move over the counts that fit and wrap; Home and End', () => {
|
|
const app = gridApp();
|
|
open(app);
|
|
expect(key(app, 'ArrowDown').preventDefault).toHaveBeenCalled();
|
|
expect(focused()).toBe('2');
|
|
key(app, 'ArrowRight');
|
|
expect(focused()).toBe('4');
|
|
key(app, 'ArrowUp');
|
|
key(app, 'ArrowLeft');
|
|
expect(focused()).toBe('6');
|
|
key(app, 'Home');
|
|
expect(focused()).toBe('2');
|
|
key(app, 'End');
|
|
expect(focused()).toBe('6');
|
|
app.closeTileCountMenu();
|
|
wrapRect = { width: 1200, height: 900 };
|
|
open(app);
|
|
key(app, 'ArrowDown');
|
|
expect(focused()).toBe('2');
|
|
key(app, 'ArrowUp');
|
|
expect(focused()).toBe('4');
|
|
// The greyed 6 is skipped both ways.
|
|
key(app, 'ArrowDown');
|
|
expect(focused()).toBe('2');
|
|
});
|
|
|
|
it('Escape closes it and gives the keyboard back to the Tiles button; closing twice is harmless', () => {
|
|
const app = gridApp();
|
|
open(app);
|
|
const btn = bySelector.get('.btn-tile-grid')!;
|
|
const ev = key(app, 'Escape');
|
|
expect(ev.preventDefault).toHaveBeenCalled();
|
|
expect(menu()).toBeNull();
|
|
expect(activeElement()).toBe(btn);
|
|
expect(() => app.closeTileCountMenu()).not.toThrow();
|
|
});
|
|
|
|
it('the menu owns its Escape in the global handler: it closes alone, with the keyboard back on Tiles', () => {
|
|
const escape = appSrc.slice(appSrc.indexOf("if (e.key === 'Escape') {"), appSrc.indexOf('this.closeAllPanels();'));
|
|
expect(escape).toMatch(/if \(this\._tileCountMenu\) \{\s*this\.closeTileCountMenu\(\{ refocus: true \}\);\s*return;/);
|
|
const app = gridApp();
|
|
open(app);
|
|
app.closeTileCountMenu({ refocus: true });
|
|
expect(activeElement()).toBe(bySelector.get('.btn-tile-grid'));
|
|
});
|
|
|
|
it('Tab closes it; a click elsewhere closes it, a click inside does not', () => {
|
|
const app = gridApp();
|
|
open(app);
|
|
key(app, 'Tab');
|
|
expect(menu()).toBeNull();
|
|
open(app);
|
|
app._tileCountMenu.onOutside({ target: item(4) });
|
|
expect(menu()).not.toBeNull();
|
|
app._tileCountMenu.onOutside({ target: new FakeEl() });
|
|
expect(menu()).toBeNull();
|
|
});
|
|
|
|
it('closes when the keyboard leaves it for something else (a late focus of the single view), not for nothing', () => {
|
|
const app = gridApp();
|
|
open(app);
|
|
// Inside: moving between counts keeps it.
|
|
menu()!.dispatch('focusout', { relatedTarget: item(2) });
|
|
expect(menu()).not.toBeNull();
|
|
// Nowhere (a click on a button in Safari focuses nothing): kept.
|
|
menu()!.dispatch('focusout', { relatedTarget: null });
|
|
expect(menu()).not.toBeNull();
|
|
// Another element, the single view's terminal: closed, the keyboard left there.
|
|
const textarea = new FakeEl();
|
|
textarea.className = 'xterm-helper-textarea';
|
|
textarea.focus();
|
|
menu()!.dispatch('focusout', { relatedTarget: textarea });
|
|
expect(menu()).toBeNull();
|
|
expect(activeElement()).toBe(textarea);
|
|
});
|
|
|
|
it('a second right-click closes it; a click on Tiles closes it and toggles', () => {
|
|
const app = gridApp();
|
|
open(app);
|
|
open(app);
|
|
expect(menu()).toBeNull();
|
|
open(app);
|
|
app.toggleTileGrid();
|
|
expect(menu()).toBeNull();
|
|
expect(app._tilesOwnTerminal()).toBe(true);
|
|
});
|
|
|
|
it('refuses in a narrow window', () => {
|
|
const app = gridApp();
|
|
windowStub.innerWidth = 1100;
|
|
open(app);
|
|
expect(menu()).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('picking a count', () => {
|
|
it('Enter on 2 opens two tiles, the active session included and focused, and remembers 2', () => {
|
|
const app = gridApp();
|
|
app.activeSessionId = 's-c';
|
|
open(app);
|
|
key(app, 'ArrowDown'); // 6 -> 2
|
|
key(app, 'Enter');
|
|
expect(menu()).toBeNull();
|
|
expect(app._tileGrid.ids).toEqual(['s-other', 's-c']);
|
|
expect(app._tileGrid.focusedId).toBe('s-c');
|
|
expect(localStore.get('codeman:tile-count')).toBe('2');
|
|
// The grid's own key holds ids and layout only (its tile count included), never content.
|
|
const stored = JSON.parse(localStore.get('codeman:tile-grid')!);
|
|
expect(Object.keys(stored).sort()).toEqual(['colFr', 'count', 'focused', 'ids', 'open', 'rowFr', 'v', 'zoomed']);
|
|
expect(stored.count).toBe(2);
|
|
});
|
|
|
|
it('a click on 4 opens four, Space works too', () => {
|
|
const app = gridApp();
|
|
open(app);
|
|
item(4).dispatch('click', { stopPropagation: vi.fn() });
|
|
expect(app._tileGrid.ids).toHaveLength(4);
|
|
app.closeTileGrid({ reselect: false });
|
|
open(app);
|
|
item(2).focus();
|
|
key(app, ' ');
|
|
expect(app._tileGrid.ids).toHaveLength(2);
|
|
});
|
|
|
|
it('then the click and Ctrl+Shift+G open with the remembered count', () => {
|
|
const app = gridApp();
|
|
localStore.set('codeman:tile-count', '4');
|
|
app.toggleTileGrid();
|
|
expect(app._tileGrid.ids).toEqual(['s-other', 's-a', 's-b', 's-c']);
|
|
app.toggleTileGrid();
|
|
localStore.delete('codeman:tile-grid');
|
|
localStore.set('codeman:tile-count', '2');
|
|
app.runTileShortcut('toggle-tile-grid');
|
|
expect(app._tileGrid.ids).toHaveLength(2);
|
|
});
|
|
|
|
it('the default is 6, and the click never opens more than the window fits', () => {
|
|
const app = gridApp();
|
|
app.toggleTileGrid();
|
|
expect(app._tileGrid.ids).toHaveLength(6);
|
|
app.closeTileGrid({ keepStored: false, reselect: false });
|
|
wrapRect = { width: 1200, height: 900 };
|
|
app.toggleTileGrid();
|
|
expect(app._tileGrid.ids).toHaveLength(4);
|
|
});
|
|
|
|
it('fewer open sessions than the count: fewer tiles', () => {
|
|
const app = gridApp(['s-a', 's-b']);
|
|
open(app);
|
|
item(6).dispatch('click', { stopPropagation: vi.fn() });
|
|
expect(app._tileGrid.ids).toEqual(['s-other', 's-a', 's-b']);
|
|
});
|
|
});
|
|
|
|
describe('Ctrl/Cmd+click on a tab with the grid closed', () => {
|
|
it('opens the remembered count in total, the clicked session among them and focused (owner answer 2)', () => {
|
|
const app = gridApp();
|
|
localStore.set('codeman:tile-count', '4');
|
|
app.addSessionToTiles('s-f');
|
|
expect(app._tileGrid.ids).toEqual(['s-other', 's-a', 's-b', 's-f']);
|
|
expect(app._tileGrid.focusedId).toBe('s-f');
|
|
});
|
|
|
|
it('with the default 6, six in total, never seven', () => {
|
|
const app = gridApp();
|
|
app.addSessionToTiles('s-g');
|
|
expect(app._tileGrid.ids).toHaveLength(6);
|
|
expect(app._tileGrid.ids.at(-1)).toBe('s-g');
|
|
});
|
|
});
|
|
|
|
describe('re-forming an open grid', () => {
|
|
it('6 to 2 keeps the tiles it has first, drops from the end, never the focused one', () => {
|
|
const app = gridApp();
|
|
app.openTileGrid(IDS.slice(0, 6), { focusedId: 's-e' });
|
|
open(app);
|
|
item(2).dispatch('click', { stopPropagation: vi.fn() });
|
|
expect(app._tileGrid.ids).toEqual(['s-a', 's-e']);
|
|
expect(app._tileGrid.focusedId).toBe('s-e');
|
|
expect(app.activeSessionId).toBe('s-e');
|
|
// The dropped ones were destroyed, not reloaded or kept connected.
|
|
const destroyed = FakeTile.all.filter((t) => t.destroy.mock.calls.length).map((t) => t.sessionId);
|
|
expect(destroyed.sort()).toEqual(['s-b', 's-c', 's-d', 's-f']);
|
|
expect(localStore.get('codeman:tile-count')).toBe('2');
|
|
});
|
|
|
|
it('2 to 6 keeps both in their cells and fills from tab order; nothing remounts', () => {
|
|
const app = gridApp();
|
|
app.openTileGrid(['s-c', 's-a']);
|
|
flushFrames();
|
|
const before = new Map(FakeTile.all.map((t) => [t.sessionId, t]));
|
|
open(app);
|
|
item(6).dispatch('click', { stopPropagation: vi.fn() });
|
|
expect(app._tileGrid.cells).toEqual(['s-c', 's-a', 's-other', 's-b', 's-d', 's-e']);
|
|
expect(app._tileFor('s-c')).toBe(before.get('s-c'));
|
|
expect(app._tileFor('s-a')).toBe(before.get('s-a'));
|
|
expect(before.get('s-c')!.connect).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('every joining tile connects only once the grid has its final cells (one fit, one PTY resize)', () => {
|
|
const app = gridApp();
|
|
app.openTileGrid(['s-a', 's-b']);
|
|
const seen: string[] = [];
|
|
const realMount = app._mountTile.bind(app);
|
|
app._mountTile = (id: string, opts?: unknown) => {
|
|
const ok = realMount(id, opts);
|
|
const tile = app._tileGrid.tiles.get(id)?.tile as FakeTile | undefined;
|
|
tile?.connect.mockImplementation(async () => {
|
|
seen.push(`${id}:${app._tileGrid.cells.length}:${app._tileGrid.cells.indexOf(id)}`);
|
|
});
|
|
return ok;
|
|
};
|
|
open(app);
|
|
item(6).dispatch('click', { stopPropagation: vi.fn() });
|
|
// Each one connected into the 3x2 grid, already in its final cell.
|
|
expect(seen).toEqual(['s-other:6:2', 's-c:6:3', 's-d:6:4', 's-e:6:5']);
|
|
});
|
|
|
|
it('holes fill first: a 3x2 of five with an empty middle cell grows to six there', () => {
|
|
const app = gridApp();
|
|
app.openTileGrid(IDS.slice(0, 5));
|
|
app._tileGrid.cells = ['s-a', null, 's-b', 's-c', 's-d', 's-e'];
|
|
app._applyTileLayout();
|
|
open(app);
|
|
item(6).dispatch('click', { stopPropagation: vi.fn() });
|
|
expect(app._tileGrid.cells).toEqual(['s-a', 's-other', 's-b', 's-c', 's-d', 's-e']);
|
|
});
|
|
|
|
it('6 to 4: the cell model rule (a tile left in the third column packs the rest)', () => {
|
|
const app = gridApp();
|
|
app.openTileGrid(IDS.slice(0, 6), { focusedId: 's-a' });
|
|
open(app);
|
|
item(4).dispatch('click', { stopPropagation: vi.fn() });
|
|
expect(app._tileGrid.cols).toBe(2);
|
|
expect(app._tileGrid.cells).toEqual(['s-a', 's-b', 's-c', 's-d']);
|
|
});
|
|
|
|
it('the same count changes nothing; a zoom the user chose ends with a re-form', () => {
|
|
const app = gridApp();
|
|
app.openTileGrid(IDS.slice(0, 4));
|
|
const cells = app._tileGrid.cells.slice();
|
|
open(app);
|
|
item(4).dispatch('click', { stopPropagation: vi.fn() });
|
|
expect(app._tileGrid.cells).toEqual(cells);
|
|
app.zoomTile('s-b');
|
|
open(app);
|
|
item(2).dispatch('click', { stopPropagation: vi.fn() });
|
|
expect(app._tileGrid.zoomedId).toBeNull();
|
|
expect(app._tileGrid.ids).toEqual(['s-a', 's-b']);
|
|
});
|
|
});
|
|
|
|
describe('the session picker is gone', () => {
|
|
it('no picker method, element, style or markup hook is left', () => {
|
|
const app = makeGridApp(IDS);
|
|
expect(app.openTilePicker).toBeUndefined();
|
|
expect(app.closeTilePicker).toBeUndefined();
|
|
expect(gridSrc).not.toMatch(/tilePickerMenu|tile-picker|openTilePicker|closeTilePicker/);
|
|
expect(css).not.toMatch(/tile-picker/);
|
|
expect(html).not.toMatch(/openTilePicker/);
|
|
expect(appSrc).not.toMatch(/closeTilePicker/);
|
|
});
|
|
|
|
it('a tile has no + (owner decision 9): tiles come from the Tiles button, its menu, Ctrl/Cmd+click, a tab, a group or Run', () => {
|
|
const app = gridApp();
|
|
app.openTileGrid(['s-a', 's-b']);
|
|
const header = app._tileGrid.tiles.get('s-a').header as FakeEl;
|
|
const actions = header.children.find((c) => c.className === 'tile-actions')!;
|
|
expect(actions.children.map((b) => b.className)).toEqual(['tile-btn tile-menu', 'tile-btn tile-zoom', 'tile-btn tile-remove']);
|
|
});
|
|
});
|