[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
206
components/hostPickerVirtualization.test.ts
Normal file
206
components/hostPickerVirtualization.test.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import test from 'node:test';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import path from 'node:path';
|
||||
|
||||
import {
|
||||
buildItemIndexToVisualIndexMap,
|
||||
clampListIndex,
|
||||
getFixedSizeVirtualWindow,
|
||||
stepListIndex,
|
||||
} from './ui/virtualListMath.ts';
|
||||
import { getQuickSwitcherVisualRowHeight } from './QuickSwitcher.tsx';
|
||||
|
||||
const root = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const read = (relativePath: string) =>
|
||||
readFileSync(path.join(root, relativePath), 'utf8');
|
||||
|
||||
test('SelectHostPanelContent virtualizes with listbox keyboard model', () => {
|
||||
const source = read('SelectHostPanelContent.tsx');
|
||||
assert.match(source, /VariableSizeVirtualList/);
|
||||
assert.match(source, /data-host-picker-virtual="select-host"/);
|
||||
assert.match(source, /role="listbox"/);
|
||||
assert.match(source, /handleListKeyDown/);
|
||||
assert.match(source, /stepListIndex/);
|
||||
// Screen readers need active option linkage while focus stays on the listbox.
|
||||
assert.match(source, /aria-activedescendant=\{activeDescendantId\}/);
|
||||
assert.match(source, /optionDomId/);
|
||||
// Navigable-index IDs avoid collisions when group paths sanitize identically.
|
||||
assert.match(source, /id=\{navIndex >= 0 \? optionDomId\(navIndex\) : undefined\}/);
|
||||
assert.match(source, /\$\{listboxId\}-opt-\$\{navIndex\}/);
|
||||
// Virtual rows must O(1)-lookup nav indices, not findIndex the full navigable list.
|
||||
assert.match(source, /navigableIndexByKey/);
|
||||
assert.match(source, /navigableIndexByKey\.get\(row\.key\)/);
|
||||
assert.doesNotMatch(source, /navigable\.findIndex/);
|
||||
// Selected hosts must still show the keyboard active ring.
|
||||
assert.match(source, /isSelected \? 'bg-muted' : isActive \? 'bg-primary\/10' : 'hover:bg-muted\/70'/);
|
||||
assert.match(source, /isActive && 'ring-1 ring-primary\/40'/);
|
||||
assert.doesNotMatch(
|
||||
source,
|
||||
/isSelected \? 'bg-muted' : isActive \? 'bg-primary\/10 ring-1 ring-primary\/40'/,
|
||||
);
|
||||
assert.doesNotMatch(source, /filteredHosts\.map\(\(host\) =>/);
|
||||
// One listbox tab stop — not per-host tabIndex under virtualization.
|
||||
assert.match(source, /role="listbox"[\s\S]*tabIndex=\{0\}/);
|
||||
assert.doesNotMatch(source, /data-host-id=\{host\.id\}[\s\S]{0,120}tabIndex=\{0\}/);
|
||||
// Path/filter changes must reset the cursor; length-only shrinks still clamp.
|
||||
assert.match(
|
||||
source,
|
||||
/setActiveNavIndex\(0\);\s*\}, \[currentPath, searchQuery, selectedTags, sortMode\]/,
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/setActiveNavIndex\(\(prev\) => clampListIndex\(prev, navigable\.length\)\);\s*\}, \[navigable\.length\]/,
|
||||
);
|
||||
// Multi-select listbox must advertise multi-selection to AT.
|
||||
assert.match(source, /aria-multiselectable=\{multiSelect \|\| undefined\}/);
|
||||
});
|
||||
|
||||
test('QuickSwitcher virtualizes categorized rows and clamps empty-list keyboard', () => {
|
||||
const source = read('QuickSwitcher.tsx');
|
||||
assert.match(source, /VariableSizeVirtualList/);
|
||||
assert.match(source, /data-host-picker-virtual="quick-switcher"/);
|
||||
assert.match(source, /stepListIndex/);
|
||||
assert.match(source, /clampListIndex/);
|
||||
// Two-line plugin rows need a taller virtual slot than QS_ROW_HEIGHT.
|
||||
assert.match(source, /QS_PLUGIN_ROW_HEIGHT/);
|
||||
// Fixed-height slots must clip/truncate so long titles never overlap the next row.
|
||||
assert.match(source, /overflow-hidden/);
|
||||
assert.match(source, /max-w-\[12rem\] shrink-0 truncate/);
|
||||
// List scroller needs a definite max height under max-h-only popup chrome.
|
||||
assert.match(source, /max-h-\[min\(360px,calc\(100vh-14rem\)\)\]/);
|
||||
assert.doesNotMatch(source, /results\.map\(\(host\) =>/);
|
||||
// Must not use bare length-1 which yields -1 on empty lists.
|
||||
assert.doesNotMatch(
|
||||
source,
|
||||
/setSelectedIndex\(\(prev\) => Math\.min\(prev \+ 1, flatItems\.length - 1\)\)/,
|
||||
);
|
||||
});
|
||||
|
||||
test('AddToWorkspaceDialog virtualizes and clamps selection on shrink', () => {
|
||||
const source = read('workspace/AddToWorkspaceDialog.tsx');
|
||||
assert.match(source, /VariableSizeVirtualList/);
|
||||
assert.match(source, /data-host-picker-virtual="add-workspace"/);
|
||||
assert.match(source, /clampListIndex\(prev, items\.length\)/);
|
||||
assert.match(source, /onClick=\{\(\) => handleTargetClick\(idx, LOCAL_ITEM_ID\)\}/);
|
||||
assert.match(source, /onClick=\{\(\) => handleTargetClick\(idx, host\.id\)\}/);
|
||||
// Long group paths must not wrap out of the fixed virtual row.
|
||||
assert.match(source, /max-w-\[12rem\] truncate text-\[11px\] text-muted-foreground/);
|
||||
assert.match(source, /max-h-\[min\(360px,calc\(100vh-14rem\)\)\]/);
|
||||
assert.doesNotMatch(source, /filteredHosts\.map\(\(host, i\) =>/);
|
||||
});
|
||||
|
||||
test('CreateWorkspaceDialog virtualizes selectable hosts', () => {
|
||||
const source = read('CreateWorkspaceDialog.tsx');
|
||||
assert.match(source, /FixedSizeVirtualList/);
|
||||
assert.match(source, /data-host-picker-virtual="create-workspace"/);
|
||||
assert.doesNotMatch(source, /filteredHosts\.map\(host =>/);
|
||||
});
|
||||
|
||||
test('FixedSizeVirtualList uses shared window math (render path clamp)', () => {
|
||||
const source = read('ui/FixedSizeVirtualList.tsx');
|
||||
assert.match(source, /getFixedSizeVirtualWindow/);
|
||||
assert.match(source, /effectiveScrollTop/);
|
||||
});
|
||||
|
||||
test('shipped virtual window math proves large lists only render a viewport slice', () => {
|
||||
const window = getFixedSizeVirtualWindow({
|
||||
itemCount: 8_000,
|
||||
itemHeight: 52,
|
||||
scrollTop: 12_000,
|
||||
viewportHeight: 300,
|
||||
overscan: 8,
|
||||
});
|
||||
const rendered = window.endIndex - window.startIndex;
|
||||
assert.ok(rendered > 0);
|
||||
assert.ok(rendered < 80);
|
||||
assert.ok(window.startIndex > 0);
|
||||
assert.ok(window.endIndex < 8_000);
|
||||
});
|
||||
|
||||
test('shipped keyboard helpers block empty-list ArrowDown crash path', () => {
|
||||
// Reproduce the review finding: empty list + ArrowDown must not yield -1.
|
||||
let index = 0;
|
||||
index = stepListIndex(index, 0, 1);
|
||||
assert.equal(index, 0);
|
||||
index = clampListIndex(index, 0);
|
||||
assert.equal(index, 0);
|
||||
// List repopulates with 5 items — index stays valid for Enter.
|
||||
index = clampListIndex(index, 5);
|
||||
assert.equal(index, 0);
|
||||
});
|
||||
|
||||
test('shipped visual index map matches picker header/item interleaving', () => {
|
||||
// Mirrors QuickSwitcher / SFTP / AddToWorkspace visual row shape.
|
||||
const visual = [
|
||||
{ kind: 'header' },
|
||||
{ kind: 'item' },
|
||||
{ kind: 'item' },
|
||||
{ kind: 'header' },
|
||||
{ kind: 'item' },
|
||||
];
|
||||
const map = buildItemIndexToVisualIndexMap(visual);
|
||||
assert.deepEqual([...map.entries()], [[0, 1], [1, 2], [2, 4]]);
|
||||
});
|
||||
|
||||
test('QuickSwitcher plugin rows use a taller virtual slot than single-line hosts', () => {
|
||||
assert.equal(
|
||||
getQuickSwitcherVisualRowHeight({ kind: 'header', key: 'h', label: 'Hosts' }),
|
||||
32,
|
||||
);
|
||||
assert.equal(
|
||||
getQuickSwitcherVisualRowHeight({
|
||||
kind: 'item',
|
||||
key: 'host:1',
|
||||
itemIndex: 0,
|
||||
item: { type: 'host', id: '1' },
|
||||
}),
|
||||
44,
|
||||
);
|
||||
// Two-line plugin chrome must not share the 44px host slot (overflow/overlap).
|
||||
assert.equal(
|
||||
getQuickSwitcherVisualRowHeight({
|
||||
kind: 'item',
|
||||
key: 'plugin:1',
|
||||
itemIndex: 1,
|
||||
item: {
|
||||
type: 'plugin-command',
|
||||
id: 'p1',
|
||||
commandId: 'cmd',
|
||||
title: 'Run',
|
||||
pluginTitle: 'Plugin',
|
||||
},
|
||||
}),
|
||||
56,
|
||||
);
|
||||
assert.equal(
|
||||
getQuickSwitcherVisualRowHeight({
|
||||
kind: 'item',
|
||||
key: 'view:1',
|
||||
itemIndex: 2,
|
||||
item: { type: 'plugin-view', id: 'v1', title: 'View', pluginTitle: 'Plugin' },
|
||||
}),
|
||||
56,
|
||||
);
|
||||
assert.ok(
|
||||
getQuickSwitcherVisualRowHeight({
|
||||
kind: 'item',
|
||||
key: 'plugin:1',
|
||||
itemIndex: 1,
|
||||
item: {
|
||||
type: 'plugin-command',
|
||||
id: 'p1',
|
||||
commandId: 'cmd',
|
||||
title: 'Run',
|
||||
pluginTitle: 'Plugin',
|
||||
},
|
||||
}) > getQuickSwitcherVisualRowHeight({
|
||||
kind: 'item',
|
||||
key: 'host:1',
|
||||
itemIndex: 0,
|
||||
item: { type: 'host', id: '1' },
|
||||
}),
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user