mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-10 16:21:22 +00:00
Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
573 lines
20 KiB
TypeScript
573 lines
20 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import React from "react";
|
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
import { act, create, type ReactTestRenderer } from "react-test-renderer";
|
|
|
|
import type { GroupConfig, GroupNode, Host } from "../types.ts";
|
|
import {
|
|
buildVisibleHostTreeItems,
|
|
getHostTreeDisplayDetails,
|
|
HostTreeView,
|
|
useHostTreeExpandedPaths,
|
|
} from "./HostTreeView.tsx";
|
|
|
|
const baseHost: Host = {
|
|
id: "host-1",
|
|
label: "Router",
|
|
hostname: "router.example.com",
|
|
username: "ssh-user",
|
|
port: 2222,
|
|
protocol: "telnet",
|
|
tags: [],
|
|
os: "linux",
|
|
createdAt: 1,
|
|
};
|
|
|
|
const installLocalStorageMock = () => {
|
|
const store = new Map<string, string>();
|
|
Object.defineProperty(globalThis, "localStorage", {
|
|
configurable: true,
|
|
value: {
|
|
getItem: (key: string) => store.get(key) ?? null,
|
|
setItem: (key: string, value: string) => {
|
|
store.set(key, value);
|
|
},
|
|
removeItem: (key: string) => {
|
|
store.delete(key);
|
|
},
|
|
},
|
|
});
|
|
};
|
|
|
|
test("HostTreeView display details include inherited telnet defaults", () => {
|
|
const host: Host = {
|
|
...baseHost,
|
|
group: "network",
|
|
username: "ssh-user",
|
|
port: 2222,
|
|
telnetUsername: undefined,
|
|
telnetPort: undefined,
|
|
};
|
|
const groupConfigs: GroupConfig[] = [{
|
|
path: "network",
|
|
telnetUsername: "group-telnet-user",
|
|
telnetPort: 2325,
|
|
}];
|
|
|
|
assert.deepEqual(getHostTreeDisplayDetails(host, groupConfigs), {
|
|
protocol: "telnet",
|
|
username: "group-telnet-user",
|
|
port: 2325,
|
|
});
|
|
});
|
|
|
|
test("HostTreeView display details keep explicit cleared telnet username", () => {
|
|
const host: Host = {
|
|
...baseHost,
|
|
group: "network",
|
|
telnetUsername: "",
|
|
};
|
|
const groupConfigs: GroupConfig[] = [{
|
|
path: "network",
|
|
telnetUsername: "group-telnet-user",
|
|
telnetPort: 2325,
|
|
}];
|
|
|
|
assert.deepEqual(getHostTreeDisplayDetails(host, groupConfigs), {
|
|
protocol: "telnet",
|
|
username: "",
|
|
port: 2325,
|
|
});
|
|
});
|
|
|
|
test("HostTreeView renders the host edit action beside the host label", () => {
|
|
installLocalStorageMock();
|
|
|
|
const markup = renderToStaticMarkup(
|
|
<HostTreeView
|
|
groupTree={[]}
|
|
hosts={[{
|
|
...baseHost,
|
|
notes: "Maintenance notes",
|
|
tags: ["edge"],
|
|
}]}
|
|
onConnect={() => undefined}
|
|
onEditHost={() => undefined}
|
|
onDuplicateHost={() => undefined}
|
|
onDeleteHost={() => undefined}
|
|
onCopyCredentials={() => undefined}
|
|
onNewGroup={() => undefined}
|
|
onRenameGroup={() => undefined}
|
|
onEditGroup={() => undefined}
|
|
onDeleteGroup={() => undefined}
|
|
moveHostToGroup={() => undefined}
|
|
moveGroup={() => undefined}
|
|
/>,
|
|
);
|
|
|
|
const labelIndex = markup.indexOf("Router");
|
|
const editButtonIndex = markup.indexOf('data-host-tree-host-edit-button="host-1"');
|
|
const notesIndex = markup.indexOf('aria-label="Host notes"', labelIndex);
|
|
const protocolIndex = markup.indexOf("TELNET", labelIndex);
|
|
|
|
assert.ok(labelIndex >= 0);
|
|
assert.ok(editButtonIndex > labelIndex);
|
|
assert.ok(notesIndex > editButtonIndex);
|
|
assert.ok(protocolIndex > notesIndex);
|
|
});
|
|
|
|
test("HostTreeView renders the group edit action beside the group label", () => {
|
|
installLocalStorageMock();
|
|
|
|
const groupNode: GroupNode = {
|
|
name: "Production",
|
|
path: "production",
|
|
children: {},
|
|
hosts: [],
|
|
totalHostCount: 2,
|
|
};
|
|
|
|
const markup = renderToStaticMarkup(
|
|
<HostTreeView
|
|
groupTree={[groupNode]}
|
|
hosts={[]}
|
|
expandedPaths={new Set<string>()}
|
|
onTogglePath={() => undefined}
|
|
onConnect={() => undefined}
|
|
onEditHost={() => undefined}
|
|
onDuplicateHost={() => undefined}
|
|
onDeleteHost={() => undefined}
|
|
onCopyCredentials={() => undefined}
|
|
onNewGroup={() => undefined}
|
|
onRenameGroup={() => undefined}
|
|
onEditGroup={() => undefined}
|
|
onDeleteGroup={() => undefined}
|
|
moveHostToGroup={() => undefined}
|
|
moveGroup={() => undefined}
|
|
/>,
|
|
);
|
|
|
|
const labelIndex = markup.indexOf("Production");
|
|
const editButtonIndex = markup.indexOf('data-host-tree-group-edit-button="production"');
|
|
const countIndex = markup.indexOf(">2<", editButtonIndex);
|
|
|
|
assert.ok(labelIndex >= 0);
|
|
assert.ok(editButtonIndex > labelIndex);
|
|
assert.ok(countIndex > editButtonIndex);
|
|
});
|
|
|
|
test("HostTreeView shows selected groups in multi-select mode", () => {
|
|
installLocalStorageMock();
|
|
|
|
const markup = renderToStaticMarkup(
|
|
<HostTreeView
|
|
groupTree={[{
|
|
name: "Production",
|
|
path: "production",
|
|
children: {},
|
|
hosts: [],
|
|
}]}
|
|
groupConfigs={[{ path: "production", notes: "# VPN instructions" }]}
|
|
hosts={[]}
|
|
expandedPaths={new Set<string>()}
|
|
onTogglePath={() => undefined}
|
|
onConnect={() => undefined}
|
|
onEditHost={() => undefined}
|
|
onDuplicateHost={() => undefined}
|
|
onDeleteHost={() => undefined}
|
|
onCopyCredentials={() => undefined}
|
|
onNewGroup={() => undefined}
|
|
onRenameGroup={() => undefined}
|
|
onEditGroup={() => undefined}
|
|
onDeleteGroup={() => undefined}
|
|
moveHostToGroup={() => undefined}
|
|
moveGroup={() => undefined}
|
|
isMultiSelectMode
|
|
selectedGroupPaths={new Set(["production"])}
|
|
toggleGroupSelection={() => undefined}
|
|
/>,
|
|
);
|
|
|
|
assert.match(markup, /aria-label="Group notes"/);
|
|
assert.match(markup, /data-selected="true"/);
|
|
assert.match(markup, /data-group-path="production"/);
|
|
assert.match(markup, /role="tree"/);
|
|
assert.match(markup, /aria-multiselectable="true"/);
|
|
assert.match(markup, /role="treeitem"/);
|
|
assert.match(markup, /aria-selected="true"/);
|
|
assert.match(markup, /data-tree-item-key="group:production"/);
|
|
assert.match(markup, /data-tree-depth="0"/);
|
|
assert.match(markup, /aria-level="1"/);
|
|
assert.match(markup, /tabindex="0"/);
|
|
assert.doesNotMatch(markup, /aria-expanded=/);
|
|
assert.doesNotMatch(markup, /data-host-tree-group-edit-button="production"/);
|
|
});
|
|
|
|
test("HostTreeView exposes selected hosts to keyboard and assistive technology", () => {
|
|
installLocalStorageMock();
|
|
|
|
const markup = renderToStaticMarkup(
|
|
<HostTreeView
|
|
groupTree={[]}
|
|
hosts={[baseHost]}
|
|
onConnect={() => undefined}
|
|
onEditHost={() => undefined}
|
|
onDuplicateHost={() => undefined}
|
|
onDeleteHost={() => undefined}
|
|
onCopyCredentials={() => undefined}
|
|
onNewGroup={() => undefined}
|
|
onRenameGroup={() => undefined}
|
|
onEditGroup={() => undefined}
|
|
onDeleteGroup={() => undefined}
|
|
moveHostToGroup={() => undefined}
|
|
moveGroup={() => undefined}
|
|
isMultiSelectMode
|
|
selectedHostIds={new Set([baseHost.id])}
|
|
toggleHostSelection={() => undefined}
|
|
/>,
|
|
);
|
|
|
|
assert.match(markup, /role="tree"/);
|
|
assert.match(markup, /data-host-id="host-1"[^>]*role="treeitem"/);
|
|
assert.match(markup, /data-host-id="host-1"[^>]*aria-selected="true"/);
|
|
assert.match(markup, /data-host-id="host-1"[^>]*data-tree-item-key="host:host-1"/);
|
|
assert.match(markup, /data-host-id="host-1"[^>]*data-tree-depth="0"/);
|
|
assert.match(markup, /data-host-id="host-1"[^>]*aria-level="1"/);
|
|
assert.match(markup, /data-host-id="host-1"[^>]*tabindex="0"/);
|
|
assert.match(markup, /data-host-id="host-1"[^>]*draggable="true"/);
|
|
});
|
|
|
|
test("HostTreeView exposes only one tree item in the tab order", () => {
|
|
installLocalStorageMock();
|
|
|
|
const markup = renderToStaticMarkup(
|
|
<HostTreeView
|
|
groupTree={[{
|
|
name: "Production",
|
|
path: "production",
|
|
children: {},
|
|
hosts: [],
|
|
}]}
|
|
hosts={[baseHost]}
|
|
onConnect={() => undefined}
|
|
onEditHost={() => undefined}
|
|
onDuplicateHost={() => undefined}
|
|
onDeleteHost={() => undefined}
|
|
onCopyCredentials={() => undefined}
|
|
onNewGroup={() => undefined}
|
|
onRenameGroup={() => undefined}
|
|
onEditGroup={() => undefined}
|
|
onDeleteGroup={() => undefined}
|
|
moveHostToGroup={() => undefined}
|
|
moveGroup={() => undefined}
|
|
/>,
|
|
);
|
|
|
|
assert.equal(markup.match(/tabindex="0"/g)?.length, 1);
|
|
assert.match(
|
|
markup,
|
|
/<button(?=[^>]*data-host-tree-group-edit-button="production")(?=[^>]*tabindex="-1")[^>]*>/,
|
|
);
|
|
});
|
|
|
|
test("HostTreeView flattens only expanded group descendants", () => {
|
|
const childHost = { ...baseHost, id: "child-host", group: "production" };
|
|
const groupTree = [{
|
|
name: "Production",
|
|
path: "production",
|
|
children: {},
|
|
hosts: [childHost],
|
|
}];
|
|
|
|
assert.deepEqual(
|
|
buildVisibleHostTreeItems({
|
|
groupTree,
|
|
ungroupedHosts: [baseHost],
|
|
expandedPaths: new Set(),
|
|
sortMode: "az",
|
|
groupConfigs: [],
|
|
}).map((item) => item.key),
|
|
["group:production", "host:host-1"],
|
|
);
|
|
assert.deepEqual(
|
|
buildVisibleHostTreeItems({
|
|
groupTree,
|
|
ungroupedHosts: [baseHost],
|
|
expandedPaths: new Set(["production"]),
|
|
sortMode: "az",
|
|
groupConfigs: [],
|
|
}).map((item) => item.key),
|
|
["group:production", "host:child-host", "host:host-1"],
|
|
);
|
|
});
|
|
|
|
test("HostTreeView keeps an explicit collapsed state after search auto-expands groups", () => {
|
|
installLocalStorageMock();
|
|
const childHost = { ...baseHost, id: "child-host", group: "production" };
|
|
const markup = renderToStaticMarkup(
|
|
<HostTreeView
|
|
groupTree={[{
|
|
name: "Production",
|
|
path: "production",
|
|
children: {},
|
|
hosts: [childHost],
|
|
}]}
|
|
hosts={[childHost]}
|
|
expandedPaths={new Set()}
|
|
autoExpandGroupsKey="router"
|
|
onTogglePath={() => undefined}
|
|
onExpandAll={() => undefined}
|
|
onCollapseAll={() => undefined}
|
|
onConnect={() => undefined}
|
|
onEditHost={() => undefined}
|
|
onDuplicateHost={() => undefined}
|
|
onDeleteHost={() => undefined}
|
|
onCopyCredentials={() => undefined}
|
|
onNewGroup={() => undefined}
|
|
onRenameGroup={() => undefined}
|
|
onEditGroup={() => undefined}
|
|
onDeleteGroup={() => undefined}
|
|
moveHostToGroup={() => undefined}
|
|
moveGroup={() => undefined}
|
|
/>,
|
|
);
|
|
|
|
assert.match(markup, /data-group-path="production"[^>]*aria-expanded="false"/);
|
|
assert.doesNotMatch(markup, /data-host-id="child-host"/);
|
|
});
|
|
|
|
test("HostTreeView search expansion is temporary and remains collapsible", async () => {
|
|
const actEnvironment = globalThis as typeof globalThis & {
|
|
IS_REACT_ACT_ENVIRONMENT?: boolean;
|
|
};
|
|
const previousActEnvironment = actEnvironment.IS_REACT_ACT_ENVIRONMENT;
|
|
actEnvironment.IS_REACT_ACT_ENVIRONMENT = true;
|
|
let persistentMutationCount = 0;
|
|
let autoExpandGroupsKey: string | undefined = "router";
|
|
let allGroupPaths = ["production"];
|
|
let expandedPaths = new Set<string>();
|
|
let collapseAll = () => undefined;
|
|
let renderer: ReactTestRenderer | null = null;
|
|
const Probe = () => {
|
|
const state = useHostTreeExpandedPaths({
|
|
persistentExpandedPaths: new Set(),
|
|
allGroupPaths,
|
|
autoExpandGroupsKey,
|
|
onTogglePath: () => { persistentMutationCount++; },
|
|
onExpandAll: () => { persistentMutationCount++; },
|
|
onCollapseAll: () => { persistentMutationCount++; },
|
|
});
|
|
expandedPaths = state.expandedPaths;
|
|
collapseAll = state.collapseAll;
|
|
return null;
|
|
};
|
|
|
|
try {
|
|
await act(async () => {
|
|
renderer = create(React.createElement(Probe));
|
|
});
|
|
assert.equal(expandedPaths.has("production"), true);
|
|
|
|
await act(async () => {
|
|
collapseAll();
|
|
});
|
|
assert.equal(expandedPaths.has("production"), false);
|
|
assert.equal(persistentMutationCount, 0);
|
|
|
|
await act(async () => {
|
|
allGroupPaths = ["production", "staging"];
|
|
renderer!.update(React.createElement(Probe));
|
|
});
|
|
assert.equal(expandedPaths.has("production"), false);
|
|
assert.equal(expandedPaths.has("staging"), true);
|
|
assert.equal(persistentMutationCount, 0);
|
|
|
|
await act(async () => {
|
|
autoExpandGroupsKey = undefined;
|
|
renderer!.update(React.createElement(Probe));
|
|
});
|
|
assert.equal(expandedPaths.has("production"), false);
|
|
assert.equal(persistentMutationCount, 0);
|
|
} finally {
|
|
await act(async () => {
|
|
renderer?.unmount();
|
|
});
|
|
actEnvironment.IS_REACT_ACT_ENVIRONMENT = previousActEnvironment;
|
|
}
|
|
});
|
|
|
|
test("HostTreeView virtualizes an 8,000-host tree", () => {
|
|
installLocalStorageMock();
|
|
const hosts = Array.from({ length: 8000 }, (_, index) => ({
|
|
...baseHost,
|
|
id: `host-${index}`,
|
|
label: `Host ${String(index).padStart(4, "0")}`,
|
|
}));
|
|
|
|
const markup = renderToStaticMarkup(
|
|
<HostTreeView
|
|
groupTree={[]}
|
|
hosts={hosts}
|
|
onConnect={() => undefined}
|
|
onEditHost={() => undefined}
|
|
onDuplicateHost={() => undefined}
|
|
onDeleteHost={() => undefined}
|
|
onCopyCredentials={() => undefined}
|
|
onNewGroup={() => undefined}
|
|
onRenameGroup={() => undefined}
|
|
onEditGroup={() => undefined}
|
|
onDeleteGroup={() => undefined}
|
|
moveHostToGroup={() => undefined}
|
|
moveGroup={() => undefined}
|
|
/>,
|
|
);
|
|
|
|
const renderedHosts = markup.match(/data-host-id=/g)?.length ?? 0;
|
|
assert.ok(renderedHosts > 0);
|
|
assert.ok(renderedHosts < 100);
|
|
assert.match(markup, /data-vault-virtual-tree="true"/);
|
|
assert.match(markup, /height:320000px/);
|
|
// Flat ungrouped hosts share one sibling set at the root level.
|
|
assert.match(markup, /aria-setsize="8000"/);
|
|
assert.match(markup, /aria-posinset="1"/);
|
|
assert.match(markup, /role="treeitem"/);
|
|
});
|
|
|
|
test("HostTreeView announces sibling position within each tree level", () => {
|
|
installLocalStorageMock();
|
|
const groupTree: GroupNode[] = [{
|
|
name: "prod",
|
|
path: "prod",
|
|
hosts: [
|
|
{ ...baseHost, id: "host-a", label: "A", group: "prod" },
|
|
{ ...baseHost, id: "host-b", label: "B", group: "prod" },
|
|
],
|
|
children: {
|
|
east: {
|
|
name: "east",
|
|
path: "prod/east",
|
|
hosts: [{ ...baseHost, id: "host-c", label: "C", group: "prod/east" }],
|
|
children: {},
|
|
},
|
|
},
|
|
}];
|
|
const hosts = [
|
|
{ ...baseHost, id: "host-a", label: "A", group: "prod" },
|
|
{ ...baseHost, id: "host-b", label: "B", group: "prod" },
|
|
{ ...baseHost, id: "host-c", label: "C", group: "prod/east" },
|
|
{ ...baseHost, id: "host-root", label: "Root", group: "" },
|
|
];
|
|
const items = buildVisibleHostTreeItems({
|
|
groupTree,
|
|
ungroupedHosts: hosts.filter((host) => !host.group),
|
|
expandedPaths: new Set(["prod", "prod/east"]),
|
|
sortMode: "az",
|
|
groupConfigs: [],
|
|
});
|
|
|
|
const byKey = new Map(items.map((item) => [item.key, item]));
|
|
// Root siblings: prod group + ungrouped host-root
|
|
assert.equal(byKey.get("group:prod")?.posInSet, 1);
|
|
assert.equal(byKey.get("group:prod")?.setSize, 2);
|
|
assert.equal(byKey.get("host:host-root")?.posInSet, 2);
|
|
assert.equal(byKey.get("host:host-root")?.setSize, 2);
|
|
// Children of prod: east group + hosts A/B
|
|
assert.equal(byKey.get("group:prod/east")?.posInSet, 1);
|
|
assert.equal(byKey.get("group:prod/east")?.setSize, 3);
|
|
assert.equal(byKey.get("host:host-a")?.setSize, 3);
|
|
assert.equal(byKey.get("host:host-b")?.setSize, 3);
|
|
// Child of prod/east: only host C
|
|
assert.equal(byKey.get("host:host-c")?.posInSet, 1);
|
|
assert.equal(byKey.get("host:host-c")?.setSize, 1);
|
|
});
|
|
|
|
test("HostTreeView IP sorting stays inside each group and leaves manual order intact", () => {
|
|
const pair = (prefix: string, group = ""): Host[] => [
|
|
{ ...baseHost, id: `${prefix}-ten`, label: "Alpha", hostname: "10.0.0.10", group, order: 1 },
|
|
{ ...baseHost, id: `${prefix}-two`, label: "Zulu", hostname: "10.0.0.2", group, order: 2 },
|
|
];
|
|
const groupTree: GroupNode[] = [{
|
|
name: "prod", path: "prod", hosts: pair("prod", "prod"),
|
|
children: { east: { name: "east", path: "prod/east", hosts: pair("east", "prod/east"), children: {} } },
|
|
}];
|
|
const options = {
|
|
groupTree, ungroupedHosts: pair("root"), expandedPaths: new Set(["prod", "prod/east"]), groupConfigs: [],
|
|
};
|
|
const keys = (sortMode: "ip" | "manual") => buildVisibleHostTreeItems({ ...options, sortMode }).map(item => item.key);
|
|
assert.deepEqual(keys("ip"), ["group:prod", "group:prod/east", "host:east-two", "host:east-ten", "host:prod-two", "host:prod-ten", "host:root-two", "host:root-ten"]);
|
|
assert.deepEqual(keys("manual"), ["group:prod", "group:prod/east", "host:east-ten", "host:east-two", "host:prod-ten", "host:prod-two", "host:root-ten", "host:root-two"]);
|
|
});
|
|
|
|
// Deterministic work-count assertions, not a wall-clock benchmark.
|
|
test("HostTreeView scans manual group order only once across expanded leaf groups", () => {
|
|
const groupTree: GroupNode[] = ["c", "b", "a"].map((path) => ({
|
|
name: path, path, children: {}, hosts: [],
|
|
}));
|
|
const groupConfigs: GroupConfig[] = groupTree.map((node, order) => ({ path: node.path, order }));
|
|
let configVisits = 0;
|
|
Object.defineProperty(groupConfigs, "filter", {
|
|
value: (predicate: (value: GroupConfig, index: number, array: GroupConfig[]) => unknown) =>
|
|
Array.prototype.filter.call(groupConfigs, (value: GroupConfig, index: number, array: GroupConfig[]) => {
|
|
configVisits += 1;
|
|
return predicate(value, index, array);
|
|
}),
|
|
});
|
|
for (const expandedPaths of [new Set<string>(), new Set(["a", "b", "c"])]) {
|
|
configVisits = 0;
|
|
const manual = buildVisibleHostTreeItems({ groupTree, groupConfigs, expandedPaths, ungroupedHosts: [], sortMode: "manual" });
|
|
assert.equal(configVisits, 3);
|
|
assert.deepEqual(manual.map((item) => item.key), ["group:c", "group:b", "group:a"]);
|
|
for (const sortMode of ["az", "za", "newest", "oldest", "group", "ip"] as const) {
|
|
configVisits = 0;
|
|
const items = buildVisibleHostTreeItems({ groupTree, groupConfigs, expandedPaths, ungroupedHosts: [], sortMode });
|
|
assert.equal(configVisits, 0, `${sortMode} must not construct a manual order index`);
|
|
assert.deepEqual(items.map((item) => item.key), sortMode === "za"
|
|
? ["group:c", "group:b", "group:a"]
|
|
: ["group:a", "group:b", "group:c"]);
|
|
}
|
|
}
|
|
});
|
|
|
|
test("HostTreeView shared order index preserves valid duplicates, ties and nested sibling metadata", () => {
|
|
const leaf = (path: string): GroupNode => ({ name: path.split("/").at(-1)!, path, children: {}, hosts: [] });
|
|
const parent: GroupNode = {
|
|
...leaf("parent"),
|
|
children: { z: leaf("parent/z"), a: leaf("parent/a"), b: leaf("parent/b") },
|
|
hosts: [{ ...baseHost, id: "child-host", group: "parent" }],
|
|
};
|
|
const groupTree = [leaf("unordered-z"), leaf("tie-b"), parent, leaf("tie-a"), leaf("unordered-a")];
|
|
const groupConfigs: GroupConfig[] = [
|
|
{ path: "parent", order: 10 }, { path: "parent", order: -1 },
|
|
{ path: "parent", order: Number.NaN }, { path: "parent", order: Infinity },
|
|
{ path: "tie-b", order: 0 }, { path: "tie-a", order: 0 },
|
|
{ path: "unordered-a", order: Number.NaN },
|
|
{ path: "parent/a", order: 0 }, { path: "parent/b", order: 0 },
|
|
{ path: "parent/z", order: -2 },
|
|
];
|
|
const ungroupedHosts = [{ ...baseHost, id: "root-host", group: "" }];
|
|
const expandedPaths = new Set(["parent", "parent/z", "parent/a", "parent/b"]);
|
|
const inputBefore = structuredClone({ groupTree, groupConfigs, ungroupedHosts, expandedPaths });
|
|
const items = buildVisibleHostTreeItems({ groupTree, groupConfigs, ungroupedHosts, expandedPaths, sortMode: "manual" });
|
|
assert.deepEqual(items.map(({ key, depth, posInSet, setSize }) => ({ key, depth, posInSet, setSize })), [
|
|
{ key: "group:parent", depth: 0, posInSet: 1, setSize: 6 },
|
|
{ key: "group:parent/z", depth: 1, posInSet: 1, setSize: 4 },
|
|
{ key: "group:parent/b", depth: 1, posInSet: 2, setSize: 4 },
|
|
{ key: "group:parent/a", depth: 1, posInSet: 3, setSize: 4 },
|
|
{ key: "host:child-host", depth: 1, posInSet: 4, setSize: 4 },
|
|
{ key: "group:tie-a", depth: 0, posInSet: 2, setSize: 6 },
|
|
{ key: "group:tie-b", depth: 0, posInSet: 3, setSize: 6 },
|
|
{ key: "group:unordered-z", depth: 0, posInSet: 4, setSize: 6 },
|
|
{ key: "group:unordered-a", depth: 0, posInSet: 5, setSize: 6 },
|
|
{ key: "host:root-host", depth: 0, posInSet: 6, setSize: 6 },
|
|
]);
|
|
assert.equal(items[0].kind === "group" && items[0].node, parent);
|
|
assert.equal(items[4].kind === "host" && items[4].host, parent.hosts[0]);
|
|
assert.deepEqual({ groupTree, groupConfigs, ungroupedHosts, expandedPaths }, inputBefore);
|
|
const changed = buildVisibleHostTreeItems({
|
|
groupTree, groupConfigs: [...groupConfigs, { path: "tie-a", order: -10 }],
|
|
ungroupedHosts, expandedPaths, sortMode: "manual",
|
|
});
|
|
assert.equal(changed[0].key, "group:tie-a");
|
|
});
|