mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 08:59:40 +02:00
feat(tiles): no + in the tile header (owner decision 9)
Owner: "remove the + button from these views". The tile header is now ● name ... ⋯ ⤢ ×. Gone with it, because nothing else used them: the + menu (openTileAddMenu, closeTileAddMenu and their hooks in closeTileGrid and the global Escape handler), its "New session in this case" entry and runInCaseForTiles, the .tile-add-empty rules, the four i18n entries only the menu showed, and buildTilePickerSessions' exclude argument (only the menu passed it). Every other way of adding tiles stays and needed nothing from the menu: the Tiles button and its right-click picker, Ctrl/Cmd+click on a tab, dragging a tab onto a tile or an empty slot, "Open group as tiles", and Run joining the open grid (_joinTileGridFromRun). The picker list, the cap helper _tileGridLimit and the user-text skip on names are shared and kept. Tests: the + menu cases (picker, cap, auto-join, the zh-CN harvest) are removed; tile-grid-chrome pins the header as exactly ⋯ ⤢ × with no add menu or runInCaseForTiles left on the app. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1382,7 +1382,6 @@ class CodemanApp {
|
|||||||
this.closeSessionManager();
|
this.closeSessionManager();
|
||||||
this.closeCommandPalette?.();
|
this.closeCommandPalette?.();
|
||||||
this.closeTilePicker?.();
|
this.closeTilePicker?.();
|
||||||
this.closeTileAddMenu?.();
|
|
||||||
this.closeShortcutOverlay?.();
|
this.closeShortcutOverlay?.();
|
||||||
// Overlay layouts only: below 1024px the sidebar is a modal off-canvas
|
// Overlay layouts only: below 1024px the sidebar is a modal off-canvas
|
||||||
// drawer over the terminal, so Escape must close it. The docked desktop
|
// drawer over the terminal, so Escape must close it. The docked desktop
|
||||||
|
|||||||
@@ -1780,21 +1780,20 @@ function dragTrackFractions(fr, index, deltaPx, totalPx, minPx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The sessions the Tiles picker (and a tile's + menu) offers, in tab order:
|
* The sessions the Tiles picker offers (and the Tiles button opens, case c of
|
||||||
* live ones only, never a session popped out to its own window (that window
|
* tileGridOpenSet), in tab order: live ones only, never a session popped out to
|
||||||
* owns its PTY size). A session with no PTY attached IS offered: its tile shows
|
* its own window (that window owns its PTY size). A session with no PTY
|
||||||
* the Attach overlay.
|
* attached IS offered: its tile shows the Attach overlay.
|
||||||
*
|
*
|
||||||
* @param {Map<string, {name?: string}>} sessions
|
* @param {Map<string, {name?: string}>} sessions
|
||||||
* @param {string[]} sessionOrder
|
* @param {string[]} sessionOrder
|
||||||
* @param {{has(id: string): boolean}} [detachedIds]
|
* @param {{has(id: string): boolean}} [detachedIds]
|
||||||
* @param {{has(id: string): boolean}} [exclude] - ids not to offer (already tiled)
|
|
||||||
* @returns {Array<{id: string, label: string}>}
|
* @returns {Array<{id: string, label: string}>}
|
||||||
*/
|
*/
|
||||||
function buildTilePickerSessions(sessions, sessionOrder, detachedIds, exclude) {
|
function buildTilePickerSessions(sessions, sessionOrder, detachedIds) {
|
||||||
const result = [];
|
const result = [];
|
||||||
for (const id of sessionOrder) {
|
for (const id of sessionOrder) {
|
||||||
if (detachedIds?.has?.(id) || exclude?.has?.(id)) continue;
|
if (detachedIds?.has?.(id)) continue;
|
||||||
const session = sessions.get(id);
|
const session = sessions.get(id);
|
||||||
if (!session) continue;
|
if (!session) continue;
|
||||||
result.push({ id, label: session.name || 'Session' });
|
result.push({ id, label: session.name || 'Session' });
|
||||||
|
|||||||
@@ -129,10 +129,6 @@
|
|||||||
'Open tiles': '打开平铺',
|
'Open tiles': '打开平铺',
|
||||||
'No sessions to show as tiles': '没有可平铺显示的会话',
|
'No sessions to show as tiles': '没有可平铺显示的会话',
|
||||||
'This group has no session to show as tiles': '此分组没有可平铺显示的会话',
|
'This group has no session to show as tiles': '此分组没有可平铺显示的会话',
|
||||||
'Add a session to the grid': '向平铺网格添加会话',
|
|
||||||
'Every open session is already tiled': '所有打开的会话都已平铺',
|
|
||||||
'New session in this case': '在此案例中新建会话',
|
|
||||||
'This session is not in a case': '此会话不属于任何案例',
|
|
||||||
'Zoom this tile': '放大此窗格',
|
'Zoom this tile': '放大此窗格',
|
||||||
'Restore the grid': '恢复平铺网格',
|
'Restore the grid': '恢复平铺网格',
|
||||||
'Remove tile (the session keeps running)': '移除窗格(会话继续运行)',
|
'Remove tile (the session keeps running)': '移除窗格(会话继续运行)',
|
||||||
|
|||||||
@@ -19753,14 +19753,12 @@ body.tile-grid-resizing--row * {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tile-picker-hint,
|
.tile-picker-hint,
|
||||||
.tile-picker-empty,
|
.tile-picker-empty {
|
||||||
.tile-add-empty {
|
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile-picker-empty,
|
.tile-picker-empty {
|
||||||
.tile-add-empty {
|
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-114
@@ -296,7 +296,6 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
this._updateConnectionIndicator?.();
|
this._updateConnectionIndicator?.();
|
||||||
this._updateSplitButtonForTiles();
|
this._updateSplitButtonForTiles();
|
||||||
this._updateTileGridButtonState();
|
this._updateTileGridButtonState();
|
||||||
this.closeTileAddMenu();
|
|
||||||
// The tabs drop their .in-tiles marker.
|
// The tabs drop their .in-tiles marker.
|
||||||
this.renderSessionTabs?.();
|
this.renderSessionTabs?.();
|
||||||
if (reselect) this._selectAfterTileGrid(focusedId);
|
if (reselect) this._selectAfterTileGrid(focusedId);
|
||||||
@@ -495,108 +494,6 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
picker.menu.remove();
|
picker.menu.remove();
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* A tile's +: the open sessions not yet tiled, in tab order; picking one
|
|
||||||
* adds it to the grid and focuses it (a human selection). Disabled once the
|
|
||||||
* grid holds what the window can fit.
|
|
||||||
*/
|
|
||||||
openTileAddMenu(event, fromSessionId = null) {
|
|
||||||
event?.preventDefault?.();
|
|
||||||
event?.stopPropagation?.();
|
|
||||||
const grid = this._tileGrid;
|
|
||||||
if (!grid?.open) return;
|
|
||||||
const trigger = event?.currentTarget || null;
|
|
||||||
if (this._tileAddMenu && this._tileAddMenu.trigger === trigger) {
|
|
||||||
this.closeTileAddMenu();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.closeTileAddMenu();
|
|
||||||
const T = window.CodemanTileGrid;
|
|
||||||
const limit = this._tileGridLimit();
|
|
||||||
const full = grid.ids.length >= limit.capacity;
|
|
||||||
const candidates = T.buildTilePickerSessions(this.sessions, this.sessionOrder, this.detachedSessions, grid.tiles);
|
|
||||||
const menu = document.createElement('div');
|
|
||||||
menu.className = 'tab-rail-action-menu tile-add-menu';
|
|
||||||
menu.setAttribute('role', 'menu');
|
|
||||||
menu.setAttribute('aria-label', 'Add a session to the grid');
|
|
||||||
if (candidates.length === 0) {
|
|
||||||
const empty = document.createElement('div');
|
|
||||||
empty.className = 'tile-add-empty';
|
|
||||||
empty.textContent = 'Every open session is already tiled';
|
|
||||||
menu.appendChild(empty);
|
|
||||||
}
|
|
||||||
for (const c of candidates) {
|
|
||||||
const item = document.createElement('button');
|
|
||||||
item.type = 'button';
|
|
||||||
item.setAttribute('role', 'menuitem');
|
|
||||||
// Only the name is user text: skipping the whole button would keep its
|
|
||||||
// title (why it is disabled) out of the translator too.
|
|
||||||
const name = document.createElement('span');
|
|
||||||
name.setAttribute('data-i18n-skip', '');
|
|
||||||
name.textContent = c.label;
|
|
||||||
item.appendChild(name);
|
|
||||||
item.disabled = full;
|
|
||||||
if (full) item.title = limit.full;
|
|
||||||
item.addEventListener('click', () => {
|
|
||||||
this.closeTileAddMenu();
|
|
||||||
if (this.addTile(c.id)) this.selectSession(c.id);
|
|
||||||
});
|
|
||||||
menu.appendChild(item);
|
|
||||||
}
|
|
||||||
// A new session in the case this tile's session belongs to: the normal Run
|
|
||||||
// for that case, which then joins the grid like any Run from this tab.
|
|
||||||
const fromSession = fromSessionId ? this.sessions.get(fromSessionId) : null;
|
|
||||||
const theCase = fromSession ? this._mobileOverviewCaseFor?.(fromSession.workingDir, this.cases || []) : null;
|
|
||||||
const create = document.createElement('button');
|
|
||||||
create.type = 'button';
|
|
||||||
create.className = 'tile-add-new';
|
|
||||||
create.setAttribute('role', 'menuitem');
|
|
||||||
create.textContent = 'New session in this case';
|
|
||||||
create.disabled = full || !theCase;
|
|
||||||
if (!theCase) create.title = 'This session is not in a case';
|
|
||||||
else if (full) create.title = limit.full;
|
|
||||||
create.addEventListener('click', () => {
|
|
||||||
this.closeTileAddMenu();
|
|
||||||
if (theCase) void this.runInCaseForTiles(theCase.name);
|
|
||||||
});
|
|
||||||
menu.appendChild(create);
|
|
||||||
document.body.appendChild(menu);
|
|
||||||
if (trigger?.getBoundingClientRect) {
|
|
||||||
const rect = trigger.getBoundingClientRect();
|
|
||||||
menu.style.position = 'fixed';
|
|
||||||
menu.style.top = `${rect.bottom + 4}px`;
|
|
||||||
menu.style.right = `${Math.max(8, window.innerWidth - rect.right)}px`;
|
|
||||||
}
|
|
||||||
const onOutside = (e) => {
|
|
||||||
if (menu.contains?.(e.target) || (trigger && trigger.contains?.(e.target))) return;
|
|
||||||
this.closeTileAddMenu();
|
|
||||||
};
|
|
||||||
const onKey = (e) => {
|
|
||||||
if (e.key === 'Escape') this.closeTileAddMenu();
|
|
||||||
};
|
|
||||||
this._tileAddMenu = { menu, trigger, onOutside, onKey };
|
|
||||||
document.addEventListener('pointerdown', onOutside, true);
|
|
||||||
document.addEventListener('keydown', onKey);
|
|
||||||
menu.querySelector?.('button:not([disabled])')?.focus?.();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs the normal Run (current run mode) in `caseName`, then puts the
|
|
||||||
* toolbar's case back as it was. The session it creates joins the grid
|
|
||||||
* through _joinTileGridFromRun.
|
|
||||||
*/
|
|
||||||
async runInCaseForTiles(caseName) {
|
|
||||||
const select = document.getElementById('quickStartCase');
|
|
||||||
const previous = select?.value;
|
|
||||||
const swap = !!select && !!caseName && previous !== caseName;
|
|
||||||
if (swap) this.selectQuickStartCase?.(caseName, { save: false });
|
|
||||||
try {
|
|
||||||
await this.run?.();
|
|
||||||
} finally {
|
|
||||||
if (swap && previous) this.selectQuickStartCase?.(previous, { save: false });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A session THIS tab's Run just created (session-ui.js
|
* A session THIS tab's Run just created (session-ui.js
|
||||||
* _ensureCreatedSessionVisible, reached only from the Run paths): with the
|
* _ensureCreatedSessionVisible, reached only from the Run paths): with the
|
||||||
@@ -620,16 +517,6 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
return this.addTile(sessionId);
|
return this.addTile(sessionId);
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Idempotent, like closeTilePicker. */
|
|
||||||
closeTileAddMenu() {
|
|
||||||
const m = this._tileAddMenu;
|
|
||||||
if (!m) return;
|
|
||||||
this._tileAddMenu = null;
|
|
||||||
document.removeEventListener('pointerdown', m.onOutside, true);
|
|
||||||
document.removeEventListener('keydown', m.onKey);
|
|
||||||
m.menu.remove();
|
|
||||||
},
|
|
||||||
|
|
||||||
// The Split button cannot act while the grid is open (openSplitPicker and
|
// The Split button cannot act while the grid is open (openSplitPicker and
|
||||||
// openSplitPane refuse), so it says so: aria-disabled plus a title, the same
|
// openSplitPane refuse), so it says so: aria-disabled plus a title, the same
|
||||||
// refusal the split already gives for web tabs and the welcome screen.
|
// refusal the split already gives for web tabs and the welcome screen.
|
||||||
@@ -1207,7 +1094,8 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
actions.append(
|
actions.append(
|
||||||
button('tile-menu', 'Session actions', '\u22EF', (e) => this.openTabRailActionMenu?.(e, sessionId)),
|
button('tile-menu', 'Session actions', '\u22EF', (e) => this.openTabRailActionMenu?.(e, sessionId)),
|
||||||
zoomBtn,
|
zoomBtn,
|
||||||
button('tile-add', 'Add a session to the grid', '+', (e) => this.openTileAddMenu(e, sessionId)),
|
// (No + here, owner decision 9: tiles are added from the Tiles button's
|
||||||
|
// picker, Ctrl/Cmd+click, a dragged tab, a tab group or Run.)
|
||||||
// Removes the tile ONLY: the session keeps running. Killing it stays
|
// Removes the tile ONLY: the session keeps running. Killing it stays
|
||||||
// behind the menu's Close session and its confirm.
|
// behind the menu's Close session and its confirm.
|
||||||
button('tile-remove', 'Remove tile (the session keeps running)', '\u00D7', () =>
|
button('tile-remove', 'Remove tile (the session keeps running)', '\u00D7', () =>
|
||||||
|
|||||||
@@ -12,25 +12,13 @@
|
|||||||
* A tile that joined before its pane existed resends its size once the pid
|
* 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).
|
* appears (the server spawned the pane at its own default size).
|
||||||
*
|
*
|
||||||
* A tile's + also offers "New session in this case": the normal Run for the
|
|
||||||
* case the tile's session belongs to, the toolbar's case put back afterwards.
|
|
||||||
*
|
|
||||||
* Real code via the shared vm harness (test/mocks/tile-grid-vm.ts); the Run
|
* 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.
|
* hook in session-ui.js is pinned at the source. Port: N/A.
|
||||||
*/
|
*/
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
import { resolve } from 'node:path';
|
import { resolve } from 'node:path';
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import {
|
import { FakeEl, FakeTile, makeGridApp, resetGridHarness, section, type GridApp } from './mocks/tile-grid-vm.js';
|
||||||
FakeEl,
|
|
||||||
FakeTile,
|
|
||||||
body,
|
|
||||||
bySelector,
|
|
||||||
makeGridApp,
|
|
||||||
resetGridHarness,
|
|
||||||
section,
|
|
||||||
type GridApp,
|
|
||||||
} from './mocks/tile-grid-vm.js';
|
|
||||||
|
|
||||||
const IDS = ['s-a', 's-b'];
|
const IDS = ['s-a', 's-b'];
|
||||||
const addSession = (app: GridApp, id: string, workingDir = '/w') =>
|
const addSession = (app: GridApp, id: string, workingDir = '/w') =>
|
||||||
@@ -137,42 +125,3 @@ describe('a pane that starts after its tile connected (#464)', () => {
|
|||||||
for (const id of IDS) expect(tileOf(id).paneStarted).not.toHaveBeenCalled();
|
for (const id of IDS) expect(tileOf(id).paneStarted).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('+ / New session in this case', () => {
|
|
||||||
function openMenu(app: GridApp, fromId: string) {
|
|
||||||
app.openTileAddMenu({ stopPropagation: vi.fn(), preventDefault: vi.fn(), currentTarget: null }, fromId);
|
|
||||||
const menu = body.children.find((c) => c.className.includes('tile-add-menu'))!;
|
|
||||||
return menu.children.find((c) => c.className === 'tile-add-new')!;
|
|
||||||
}
|
|
||||||
|
|
||||||
it('runs the normal Run in the tile session case, then puts the toolbar case back', async () => {
|
|
||||||
const app = makeGridApp(IDS);
|
|
||||||
app.cases = [{ name: 'proj', path: '/w' }];
|
|
||||||
app._mobileOverviewCaseFor = (dir: string, cases: Array<{ name: string; path: string }>) =>
|
|
||||||
cases.find((c) => dir.startsWith(c.path)) ?? null;
|
|
||||||
const select = new FakeEl();
|
|
||||||
select.value = 'other-case';
|
|
||||||
bySelector.set('#quickStartCase', select);
|
|
||||||
const calls: string[] = [];
|
|
||||||
app.selectQuickStartCase = vi.fn((name: string) => {
|
|
||||||
calls.push(`case:${name}`);
|
|
||||||
select.value = name;
|
|
||||||
});
|
|
||||||
app.run = vi.fn(async () => calls.push(`run in ${select.value}`));
|
|
||||||
app.openTileGrid(IDS);
|
|
||||||
const item = openMenu(app, 's-a');
|
|
||||||
expect(item.disabled).toBe(false);
|
|
||||||
item.dispatch('click');
|
|
||||||
await vi.waitFor(() => expect(calls).toHaveLength(3));
|
|
||||||
expect(calls).toEqual(['case:proj', 'run in proj', 'case:other-case']);
|
|
||||||
expect(app.selectQuickStartCase).toHaveBeenCalledWith('proj', { save: false });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('is disabled for a session that is not in a case', () => {
|
|
||||||
const app = makeGridApp(IDS);
|
|
||||||
app.cases = [];
|
|
||||||
app._mobileOverviewCaseFor = () => null;
|
|
||||||
app.openTileGrid(IDS);
|
|
||||||
expect(openMenu(app, 's-a').disabled).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
* the headless frame bar. The layout table still covers 7 to 9 (unreachable).
|
* the headless frame bar. The layout table still covers 7 to 9 (unreachable).
|
||||||
*
|
*
|
||||||
* Every way into the grid stops at the cap even where the window would fit
|
* Every way into the grid stops at the cap even where the window would fit
|
||||||
* nine (the harness window is 2400x1200): opening, adding, a tile's +, a
|
* nine (the harness window is 2400x1200): opening, adding, a session Run
|
||||||
* session Run makes, Ctrl/Cmd+click, the picker and "Open group as tiles".
|
* makes, Ctrl/Cmd+click, the picker and "Open group as tiles".
|
||||||
* The texts say which limit binds: "at most 6" for the cap, "what this window
|
* The texts say which limit binds: "at most 6" for the cap, "what this window
|
||||||
* fits" for a smaller window.
|
* fits" for a smaller window.
|
||||||
*
|
*
|
||||||
@@ -63,19 +63,6 @@ describe('the cap', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('every way in stops at the cap', () => {
|
describe('every way in stops at the cap', () => {
|
||||||
it("a tile's + offers the rest, disabled, and says why", () => {
|
|
||||||
const app = fullGrid();
|
|
||||||
app.openTileAddMenu({ stopPropagation: vi.fn(), preventDefault: vi.fn(), currentTarget: null }, 's-1');
|
|
||||||
const menu = body.children.find((c) => c.className.includes('tile-add-menu'))!;
|
|
||||||
const items = menu.children.filter((c) => ['s-7', 's-8'].includes(c.children[0]?.textContent ?? ''));
|
|
||||||
expect(items).toHaveLength(2);
|
|
||||||
for (const item of items) {
|
|
||||||
expect(item.disabled).toBe(true);
|
|
||||||
expect(item.title).toBe(AT_MOST);
|
|
||||||
}
|
|
||||||
expect(menu.children.find((c) => c.className === 'tile-add-new')!.disabled).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('a session Run makes opens on its own, with a toast', () => {
|
it('a session Run makes opens on its own, with a toast', () => {
|
||||||
const app = fullGrid();
|
const app = fullGrid();
|
||||||
expect(app._joinTileGridFromRun('s-7')).toBe(false);
|
expect(app._joinTileGridFromRun('s-7')).toBe(false);
|
||||||
|
|||||||
@@ -97,6 +97,22 @@ describe('the header', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('header buttons', () => {
|
describe('header buttons', () => {
|
||||||
|
it('are ⋯ ⤢ × and nothing else: no + (owner decision 9)', () => {
|
||||||
|
const app = openGrid();
|
||||||
|
const tile = section.children.find((el) => el.dataset.sessionId === 's-a') as FakeEl;
|
||||||
|
const actions = tile.children[0].children.find((c) => c.className === 'tile-actions') as FakeEl;
|
||||||
|
expect(actions.children.map((b) => b.className)).toEqual([
|
||||||
|
'tile-btn tile-menu',
|
||||||
|
'tile-btn tile-zoom',
|
||||||
|
'tile-btn tile-remove',
|
||||||
|
]);
|
||||||
|
expect(actions.children.map((b) => b.textContent)).toEqual(['\u22EF', '\u2922', '\u00D7']);
|
||||||
|
// The + menu and its "New session in this case" went with it.
|
||||||
|
for (const gone of ['openTileAddMenu', 'closeTileAddMenu', 'runInCaseForTiles']) {
|
||||||
|
expect(gone in app, gone).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('× removes the tile only: the session keeps running, a neighbour takes focus unacknowledged', () => {
|
it('× removes the tile only: the session keeps running, a neighbour takes focus unacknowledged', () => {
|
||||||
const app = openGrid();
|
const app = openGrid();
|
||||||
app._apiDelete = vi.fn();
|
app._apiDelete = vi.fn();
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
*
|
*
|
||||||
* - Runtime strings are HARVESTED from the real tile code (the shared vm
|
* - Runtime strings are HARVESTED from the real tile code (the shared vm
|
||||||
* harness, test/mocks/tile-grid-vm.ts) driven through every state that
|
* harness, test/mocks/tile-grid-vm.ts) driven through every state that
|
||||||
* writes text: the picker (cap and window wording), a tile's +, the Attach
|
* writes text: the picker (cap and window wording), the Attach
|
||||||
* overlay (not attached, attaching, exited, ended), zoom, the header
|
* overlay (not attached, attaching, exited, ended), zoom, the header
|
||||||
* tooltip, the dividers, the empty slot, every toast, the crash-restart
|
* tooltip, the dividers, the empty slot, every toast, the crash-restart
|
||||||
* confirm, the Tiles and Split button titles. Each must translate to text
|
* confirm, the Tiles and Split button titles. Each must translate to text
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
* - Static strings: the shortcut registry's tile entries (overlay and App
|
* - Static strings: the shortcut registry's tile entries (overlay and App
|
||||||
* Settings list), and index.html run through the real translator in JSDOM
|
* Settings list), and index.html run through the real translator in JSDOM
|
||||||
* (the Tiles button, the App Settings chips, the Help modal's Tiles rows).
|
* (the Tiles button, the App Settings chips, the Help modal's Tiles rows).
|
||||||
* - User text stays as typed: session names (tile header, picker, + menu)
|
* - User text stays as typed: session names (tile header, picker)
|
||||||
* and group names carry data-i18n-skip, and a session name inside the
|
* and group names carry data-i18n-skip, and a session name inside the
|
||||||
* confirm passes through the pattern untranslated.
|
* confirm passes through the pattern untranslated.
|
||||||
*
|
*
|
||||||
@@ -130,8 +130,6 @@ async function exercise() {
|
|||||||
let pill = 'idle';
|
let pill = 'idle';
|
||||||
app._sidebarRichRow = () => ({ state: pill, pill, since: { at: 1 } });
|
app._sidebarRichRow = () => ({ state: pill, pill, since: { at: 1 } });
|
||||||
app._mobileOverviewStampText = () => '3m';
|
app._mobileOverviewStampText = () => '3m';
|
||||||
app.cases = [{ name: 'proj', path: '/w' }];
|
|
||||||
app._mobileOverviewCaseFor = (_dir: string, cases: Array<{ name: string }>) => cases[0] ?? null;
|
|
||||||
app.sessions.get('s-2').pid = null;
|
app.sessions.get('s-2').pid = null;
|
||||||
app.sessions.get('s-3').paneExit = { status: 3 };
|
app.sessions.get('s-3').paneExit = { status: 3 };
|
||||||
app.sessions.get('s-4').paneExit = { signal: 9 };
|
app.sessions.get('s-4').paneExit = { signal: 9 };
|
||||||
@@ -183,15 +181,6 @@ async function exercise() {
|
|||||||
harvestAll(app, 'zoomed');
|
harvestAll(app, 'zoomed');
|
||||||
app.zoomTile('s-1');
|
app.zoomTile('s-1');
|
||||||
|
|
||||||
// A tile's + on a full grid, from a session in a case and from one outside.
|
|
||||||
app.openTileAddMenu({ stopPropagation: vi.fn(), preventDefault: vi.fn(), currentTarget: null }, 's-1');
|
|
||||||
harvestAll(app, '+ menu, full');
|
|
||||||
app.closeTileAddMenu();
|
|
||||||
app._mobileOverviewCaseFor = () => null;
|
|
||||||
app.openTileAddMenu({ stopPropagation: vi.fn(), preventDefault: vi.fn(), currentTarget: null }, 's-1');
|
|
||||||
harvestAll(app, '+ menu, no case');
|
|
||||||
app.closeTileAddMenu();
|
|
||||||
|
|
||||||
// The toasts of a full grid, by the cap and by the window.
|
// The toasts of a full grid, by the cap and by the window.
|
||||||
app._joinTileGridFromRun('s-7');
|
app._joinTileGridFromRun('s-7');
|
||||||
app.addSessionToTiles('s-7');
|
app.addSessionToTiles('s-7');
|
||||||
@@ -264,7 +253,6 @@ describe('every tile grid string the code puts on screen translates to zh-CN', (
|
|||||||
'Zoom this tile',
|
'Zoom this tile',
|
||||||
'Restore the grid',
|
'Restore the grid',
|
||||||
'Session actions',
|
'Session actions',
|
||||||
'Add a session to the grid',
|
|
||||||
'Remove tile (the session keeps running)',
|
'Remove tile (the session keeps running)',
|
||||||
'idle 3m',
|
'idle 3m',
|
||||||
'needs you 3m',
|
'needs you 3m',
|
||||||
@@ -276,8 +264,6 @@ describe('every tile grid string the code puts on screen translates to zh-CN', (
|
|||||||
'The agent exited (signal 9)',
|
'The agent exited (signal 9)',
|
||||||
'The session ended',
|
'The session ended',
|
||||||
'It cannot be restarted in place: close it from ⋯ (Close session).',
|
'It cannot be restarted in place: close it from ⋯ (Close session).',
|
||||||
'New session in this case',
|
|
||||||
'This session is not in a case',
|
|
||||||
'The grid holds at most 6 tiles: the new session opens on its own',
|
'The grid holds at most 6 tiles: the new session opens on its own',
|
||||||
'The grid already holds what this window fits (4): the new session opens on its own',
|
'The grid already holds what this window fits (4): the new session opens on its own',
|
||||||
'The grid already holds what this window fits (4)',
|
'The grid already holds what this window fits (4)',
|
||||||
@@ -375,12 +361,12 @@ describe('the static markup through the real translator (JSDOM, zh-CN)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('user text in the tile code', () => {
|
describe('user text in the tile code', () => {
|
||||||
it('session names in a tile header, the picker and the + menu are marked data-i18n-skip', () => {
|
it('session names in a tile header and the picker are marked data-i18n-skip', () => {
|
||||||
// The harvest saw them only inside skipped subtrees.
|
// The harvest saw them only inside skipped subtrees.
|
||||||
expect(userText.has('Open tiles')).toBe(true);
|
expect(userText.has('Open tiles')).toBe(true);
|
||||||
expect([...seen.keys()]).not.toContain('s-1');
|
expect([...seen.keys()]).not.toContain('s-1');
|
||||||
const src = read('tile-grid.js');
|
const src = read('tile-grid.js');
|
||||||
expect(src.match(/setAttribute\('data-i18n-skip', ''\)/g)?.length).toBeGreaterThanOrEqual(3);
|
expect(src.match(/setAttribute\('data-i18n-skip', ''\)/g)?.length).toBeGreaterThanOrEqual(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('the group names of the grouped rail are marked data-i18n-skip', () => {
|
it('the group names of the grouped rail are marked data-i18n-skip', () => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @fileoverview The header Tiles button, its picker, and a tile's + menu.
|
* @fileoverview The header Tiles button and its picker.
|
||||||
*
|
*
|
||||||
* - The button is opt-in (`showTileGridButton`, hidden by its `--hidden` marker
|
* - 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
|
* class) and hard-gated to desktop widths like Split: a JS width check plus a
|
||||||
@@ -12,8 +12,8 @@
|
|||||||
* opens the grid on the checked ones, focusing the active session if checked.
|
* opens the grid on the checked ones, focusing the active session if checked.
|
||||||
* - Escape closes it, and its close method is idempotent (the global Escape
|
* - Escape closes it, and its close method is idempotent (the global Escape
|
||||||
* handler calls every close method).
|
* handler calls every close method).
|
||||||
* - A tile's + lists the open sessions not yet tiled; picking one adds and
|
* - A tile has no + (owner decision 9): tiles are added from this picker,
|
||||||
* focuses it; a grid that holds what the window fits disables them.
|
* Ctrl/Cmd+click, a dragged tab, a tab group or Run.
|
||||||
*
|
*
|
||||||
* Real code via the shared vm harness (test/mocks/tile-grid-vm.ts). Port: N/A.
|
* Real code via the shared vm harness (test/mocks/tile-grid-vm.ts). Port: N/A.
|
||||||
*/
|
*/
|
||||||
@@ -26,7 +26,6 @@ import {
|
|||||||
bySelector,
|
bySelector,
|
||||||
makeGridApp,
|
makeGridApp,
|
||||||
resetGridHarness,
|
resetGridHarness,
|
||||||
section,
|
|
||||||
windowStub,
|
windowStub,
|
||||||
type GridApp,
|
type GridApp,
|
||||||
} from './mocks/tile-grid-vm.js';
|
} from './mocks/tile-grid-vm.js';
|
||||||
@@ -184,11 +183,10 @@ describe('the picker', () => {
|
|||||||
expect(() => app.closeTilePicker()).not.toThrow();
|
expect(() => app.closeTilePicker()).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('the global Escape handler closes the picker and the + menu', () => {
|
it('the global Escape handler closes the picker', () => {
|
||||||
const src = readFileSync(resolve(import.meta.dirname, '../src/web/public/app.js'), 'utf8');
|
const src = readFileSync(resolve(import.meta.dirname, '../src/web/public/app.js'), 'utf8');
|
||||||
const escape = src.slice(src.indexOf("if (e.key === 'Escape') {"), src.indexOf('Option/Alt session navigation'));
|
const escape = src.slice(src.indexOf("if (e.key === 'Escape') {"), src.indexOf('Option/Alt session navigation'));
|
||||||
expect(escape).toContain('this.closeTilePicker?.();');
|
expect(escape).toContain('this.closeTilePicker?.();');
|
||||||
expect(escape).toContain('this.closeTileAddMenu?.();');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('refuses in a narrow window', () => {
|
it('refuses in a narrow window', () => {
|
||||||
@@ -198,44 +196,3 @@ describe('the picker', () => {
|
|||||||
expect(picker()).toBeNull();
|
expect(picker()).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("a tile's + menu", () => {
|
|
||||||
const addMenu = () => body.children.find((c) => c.className.includes('tile-add-menu')) ?? null;
|
|
||||||
const addButtonOf = (id: string) =>
|
|
||||||
(section.children.find((el) => el.dataset.sessionId === id) as FakeEl).children[0].children[2].children.find((b) =>
|
|
||||||
b.className.includes('tile-add')
|
|
||||||
) as FakeEl;
|
|
||||||
|
|
||||||
it('lists the open sessions not yet tiled; picking one adds and focuses it', () => {
|
|
||||||
const app = makeGridApp(IDS);
|
|
||||||
app.openTileGrid(['s-a', 's-b']);
|
|
||||||
app.markIdleAlertSeen.mockClear();
|
|
||||||
addButtonOf('s-a').dispatch('click', { stopPropagation: vi.fn(), currentTarget: null });
|
|
||||||
const items = addMenu()!.children.filter((i) => i.className !== 'tile-add-new');
|
|
||||||
expect(items.map((i) => i.children[0].textContent)).toEqual(['s-other', 's-c']);
|
|
||||||
items[1].dispatch('click');
|
|
||||||
expect(app._tileGrid.ids).toEqual(['s-a', 's-b', 's-c']);
|
|
||||||
expect(app.activeSessionId).toBe('s-c');
|
|
||||||
expect(app.markIdleAlertSeen).toHaveBeenCalledWith('s-c');
|
|
||||||
expect(addMenu()).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('a grid holding what the window fits disables the entries', () => {
|
|
||||||
const app = makeGridApp(IDS);
|
|
||||||
app.openTileGrid(['s-a', 's-b']);
|
|
||||||
const sectionRect = section.getBoundingClientRect;
|
|
||||||
section.getBoundingClientRect = () => ({ width: 1000, height: 400, top: 0, left: 0, right: 1000, bottom: 400 });
|
|
||||||
app.openTileAddMenu({ stopPropagation: vi.fn(), preventDefault: vi.fn(), currentTarget: null });
|
|
||||||
expect(addMenu()!.children.every((i) => i.disabled)).toBe(true);
|
|
||||||
section.getBoundingClientRect = sectionRect;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('closing the grid closes it', () => {
|
|
||||||
const app = makeGridApp(IDS);
|
|
||||||
app.selectSession = vi.fn();
|
|
||||||
app.openTileGrid(['s-a']);
|
|
||||||
app.openTileAddMenu({ stopPropagation: vi.fn(), preventDefault: vi.fn(), currentTarget: null });
|
|
||||||
app.closeTileGrid({ reselect: false });
|
|
||||||
expect(addMenu()).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
Reference in New Issue
Block a user