feat: add scalable device hierarchy model
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { buildDeviceTree, flattenVisibleRows } = require('../device-tree');
|
||||
const { createInitialState, reduceSidebar, createSidebarController } = require('../sidebar-controller');
|
||||
|
||||
function fixture() {
|
||||
const model = buildDeviceTree({
|
||||
sites: [{ id: 's', name: 'Site' }],
|
||||
groups: [{ id: 'g', name: 'Group', parentId: 's' }],
|
||||
devices: [
|
||||
{ id: 'a', name: 'Alpha', siteId: 's', deviceGroupId: 'g' },
|
||||
{ id: 'b', name: 'Beta', siteId: 's', deviceGroupId: 'g' },
|
||||
],
|
||||
});
|
||||
return model;
|
||||
}
|
||||
|
||||
function visible(model, state) {
|
||||
return flattenVisibleRows(model, state);
|
||||
}
|
||||
|
||||
test('reducer expands, navigates, selects, collapses, and preserves hidden selection', () => {
|
||||
const model = fixture();
|
||||
let state = createInitialState();
|
||||
let rows = visible(model, state);
|
||||
state = reduceSidebar(state, { type: 'KEY', key: 'Home' }, rows);
|
||||
assert.equal(state.activeKey, 'site:s');
|
||||
state = reduceSidebar(state, { type: 'KEY', key: 'ArrowRight' }, rows);
|
||||
assert.ok(state.expandedKeys.has('site:s'));
|
||||
|
||||
rows = visible(model, state);
|
||||
state = reduceSidebar(state, { type: 'KEY', key: 'ArrowDown' }, rows);
|
||||
assert.equal(state.activeKey, 'group:s:g');
|
||||
state = reduceSidebar(state, { type: 'KEY', key: 'ArrowRight' }, rows);
|
||||
rows = visible(model, state);
|
||||
state = reduceSidebar(state, { type: 'KEY', key: 'ArrowDown' }, rows);
|
||||
state = reduceSidebar(state, { type: 'KEY', key: 'Enter' }, rows);
|
||||
assert.equal(state.selectedKey, 'camera:a');
|
||||
|
||||
state = reduceSidebar(state, { type: 'ACTIVATE', rowKey: 'site:s' }, rows);
|
||||
assert.equal(state.selectedKey, 'camera:a');
|
||||
assert.equal(visible(model, state).hiddenSelected, true);
|
||||
});
|
||||
|
||||
test('keyboard Left/Right parent behavior, bounds, Space and Escape', () => {
|
||||
const model = fixture();
|
||||
let state = createInitialState({
|
||||
expandedKeys: new Set(['site:s', 'group:s:g']),
|
||||
activeKey: 'camera:a',
|
||||
searchQuery: 'alpha',
|
||||
});
|
||||
let rows = visible(model, state);
|
||||
state = reduceSidebar(state, { type: 'KEY', key: 'ArrowLeft' }, rows);
|
||||
assert.equal(state.activeKey, 'group:s:g');
|
||||
state = reduceSidebar(state, { type: 'KEY', key: 'ArrowLeft' }, rows);
|
||||
assert.equal(state.expandedKeys.has('group:s:g'), false);
|
||||
state = reduceSidebar(state, { type: 'KEY', key: 'End' }, visible(model, state));
|
||||
assert.equal(state.activeKey, 'group:s:g');
|
||||
state = reduceSidebar(state, { type: 'KEY', key: ' ' }, visible(model, state));
|
||||
assert.equal(state.expandedKeys.has('group:s:g'), true);
|
||||
state = reduceSidebar(state, { type: 'KEY', key: 'Escape' }, visible(model, state));
|
||||
assert.equal(state.searchQuery, '');
|
||||
});
|
||||
|
||||
test('malformed and unknown delegated row keys are no-ops', () => {
|
||||
const state = createInitialState({ activeKey: 'site:s' });
|
||||
const rows = visible(fixture(), state);
|
||||
assert.equal(reduceSidebar(state, { type: 'ACTIVATE', rowKey: '__proto__' }, rows), state);
|
||||
assert.equal(reduceSidebar(state, { type: 'CLICK', rowKey: 'camera:not-there' }, rows), state);
|
||||
});
|
||||
|
||||
test('controller commits exactly once per dispatch through view adapter', () => {
|
||||
const model = fixture();
|
||||
const commits = [];
|
||||
const view = { commit: (snapshot) => commits.push(snapshot) };
|
||||
const controller = createSidebarController({ model, view, rowHeight: 28, overscan: 3 });
|
||||
assert.equal(commits.length, 1);
|
||||
controller.dispatch({ type: 'ACTIVATE', rowKey: 'site:s' });
|
||||
assert.equal(commits.length, 2);
|
||||
assert.equal(commits[1].state.expandedKeys.has('site:s'), true);
|
||||
assert.ok(Array.isArray(commits[1].window.rows));
|
||||
});
|
||||
|
||||
test('active and selected state survive virtualization and selection is indicated when hidden', () => {
|
||||
const model = fixture();
|
||||
const commits = [];
|
||||
const controller = createSidebarController({ model, view: { commit: (value) => commits.push(value) } });
|
||||
controller.dispatch({ type: 'SET_SELECTION', rowKey: 'camera:b' });
|
||||
controller.dispatch({ type: 'SET_VIEWPORT', scrollTop: 9999, viewportHeight: 20 });
|
||||
const snapshot = commits.at(-1);
|
||||
assert.equal(snapshot.state.selectedKey, 'camera:b');
|
||||
assert.equal(snapshot.rows.hiddenSelected, true);
|
||||
});
|
||||
|
||||
test('controller search temporarily expands matches with one completion commit', async () => {
|
||||
const model = fixture();
|
||||
const commits = [];
|
||||
const scheduled = [];
|
||||
const controller = createSidebarController({
|
||||
model,
|
||||
view: { commit: (value) => commits.push(value) },
|
||||
scheduler: (work) => scheduled.push(work),
|
||||
chunkSize: 1,
|
||||
});
|
||||
const pending = controller.search('beta group');
|
||||
while (scheduled.length) scheduled.shift()();
|
||||
const result = await pending;
|
||||
assert.equal(result.count, 1);
|
||||
assert.equal(commits.length, 2);
|
||||
assert.deepEqual(commits.at(-1).rows.map((row) => row.key), ['site:s', 'group:s:g', 'camera:b']);
|
||||
assert.equal(controller.getState().expandedKeys.size, 0);
|
||||
});
|
||||
Reference in New Issue
Block a user