Files
NetMesh/components/KeychainCardLayout.test.tsx
zhaolei 3c72efcb7f
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
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

339 lines
9.8 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import React from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { I18nProvider } from "../application/i18n/I18nProvider.tsx";
import { STORAGE_KEY_VAULT_KEYS_VIEW_MODE } from "../infrastructure/config/storageKeys.ts";
import type { Identity, SSHKey } from "../types.ts";
import KeychainManager from "./KeychainManager.tsx";
import { IdentityCard } from "./keychain/IdentityCard.tsx";
import { KeyCard } from "./keychain/KeyCard.tsx";
import {
shouldShowIdentitySection,
shouldShowKeySection,
shouldShowSearchNoResults,
} from "./keychain/utils.ts";
const longLabel =
"sdakdjkasjakjskajskaijssdakdjkasjakjskajskaijssdakdjkasjakjskajskaijssdakdjkasjakjskajskaijs";
const renderWithI18n = (node: React.ReactElement) =>
renderToStaticMarkup(
React.createElement(I18nProvider, { locale: "en" }, node),
);
const installStorageStub = (viewMode: string | null = null) => {
const values = new Map<string, string>();
if (viewMode) {
values.set(STORAGE_KEY_VAULT_KEYS_VIEW_MODE, viewMode);
}
Object.defineProperty(globalThis, "localStorage", {
configurable: true,
value: {
getItem: (key: string) => values.get(key) ?? null,
setItem: (key: string, value: string) => {
values.set(key, value);
},
removeItem: (key: string) => {
values.delete(key);
},
},
});
};
const installNavigatorStub = () => {
const currentNavigator = globalThis.navigator;
if (
typeof currentNavigator?.platform === "string" &&
typeof currentNavigator?.userAgent === "string"
) {
return;
}
Object.defineProperty(globalThis, "navigator", {
configurable: true,
value: {
platform: "MacIntel",
userAgent: "Mac OS",
},
});
};
const identity: Identity = {
id: "identity-1",
label: longLabel,
username: "root",
authMethod: "password",
password: "pw",
created: 1,
};
const keyItem: SSHKey = {
id: "key-1",
label: longLabel,
type: "ED25519",
privateKey: "",
publicKey: "",
source: "imported",
category: "key",
created: 1,
};
test("IdentityCard list layout constrains long labels", () => {
const markup = renderWithI18n(
React.createElement(IdentityCard, {
identity,
viewMode: "list",
isSelected: false,
onClick: () => {},
}),
);
assert.match(markup, /group cursor-pointer min-w-0 w-full max-w-full/);
assert.doesNotMatch(markup, /group cursor-pointer min-w-0 w-full max-w-full overflow-hidden/);
assert.match(markup, /flex items-center gap-3 h-full min-w-0/);
assert.doesNotMatch(markup, /flex items-center gap-3 h-full min-w-0 overflow-hidden/);
assert.match(markup, /min-w-0 flex-1 basis-0 overflow-hidden/);
assert.match(markup, /block max-w-full truncate text-sm font-semibold/);
});
test("KeyCard list layout constrains long labels", () => {
const markup = renderWithI18n(
React.createElement(KeyCard, {
keyItem,
viewMode: "list",
isSelected: false,
isMac: false,
onClick: () => {},
onEdit: () => {},
onExport: () => {},
onCopyPublicKey: () => {},
onDelete: () => {},
}),
);
assert.match(markup, /group cursor-pointer min-w-0 w-full max-w-full/);
assert.doesNotMatch(markup, /group cursor-pointer min-w-0 w-full max-w-full overflow-hidden/);
assert.match(markup, /flex items-center gap-3 h-full min-w-0/);
assert.doesNotMatch(markup, /flex items-center gap-3 h-full min-w-0 overflow-hidden/);
assert.match(markup, /min-w-0 flex-1 basis-0 overflow-hidden/);
assert.match(markup, /block max-w-full truncate text-sm font-semibold/);
});
test("KeychainManager shows keys and identities on the same page", () => {
installNavigatorStub();
installStorageStub("list");
const markup = renderWithI18n(
React.createElement(KeychainManager, {
keys: [keyItem],
identities: [identity],
hosts: [],
proxyProfiles: [],
customGroups: [],
groupConfigs: [],
managedSources: [],
onSave: () => {},
onUpdate: () => {},
onReorderKeys: () => {},
onDelete: () => {},
onSaveIdentity: () => {},
onReorderIdentities: () => {},
onDeleteIdentity: () => {},
}),
);
assert.match(markup, /h-full min-w-0 w-full overflow-hidden flex relative/);
assert.match(markup, /flex-1 min-w-0 w-full overflow-y-auto/);
assert.doesNotMatch(markup, /flex-1 min-w-0 w-full overflow-y-auto overflow-x-hidden/);
assert.match(markup, /flex min-w-0 w-full max-w-full flex-col gap-0/);
assert.match(markup, /block min-w-0 w-full max-w-full/);
assert.match(markup, /data-section="keychain-identities"/);
assert.match(markup, /data-section="keychain-keys"/);
assert.doesNotMatch(markup, /data-section="keychain-empty"/);
assert.doesNotMatch(markup, />Identities<\/button>/);
});
test("KeychainManager shows keys without the identities section when no identities exist", () => {
installNavigatorStub();
installStorageStub();
const markup = renderWithI18n(
React.createElement(KeychainManager, {
keys: [keyItem],
identities: [],
onSave: () => {},
onUpdate: () => {},
onDelete: () => {},
onSaveIdentity: () => {},
}),
);
assert.match(markup, /data-section="keychain-keys"/);
assert.doesNotMatch(markup, /data-section="keychain-identities"/);
assert.doesNotMatch(markup, /data-section="keychain-empty"/);
});
test("KeychainManager shows the empty prompt only when keys and identities are absent", () => {
installNavigatorStub();
installStorageStub();
const markup = renderWithI18n(
React.createElement(KeychainManager, {
keys: [],
identities: [],
onSave: () => {},
onUpdate: () => {},
onDelete: () => {},
onSaveIdentity: () => {},
}),
);
assert.match(markup, /data-section="keychain-keys"/);
assert.match(markup, /data-section="keychain-empty"/);
assert.doesNotMatch(markup, /data-section="keychain-identities"/);
});
test("KeychainManager hides the empty key CTA when identities exist without keys", () => {
installNavigatorStub();
installStorageStub();
const markup = renderWithI18n(
React.createElement(KeychainManager, {
keys: [],
identities: [identity],
onSave: () => {},
onUpdate: () => {},
onDelete: () => {},
onSaveIdentity: () => {},
}),
);
assert.match(markup, /data-section="keychain-identities"/);
assert.doesNotMatch(markup, /data-section="keychain-keys"/);
assert.doesNotMatch(markup, /data-section="keychain-empty"/);
});
test("KeychainManager exposes new-key, import-certificate, and new-identity header actions", () => {
installNavigatorStub();
installStorageStub();
const markup = renderWithI18n(
React.createElement(KeychainManager, {
keys: [],
identities: [],
onSave: () => {},
onUpdate: () => {},
onDelete: () => {},
onSaveIdentity: () => {},
}),
);
// Header split "New Key" uses the same secondary chrome as sibling actions
assert.match(
markup,
/rounded-md shrink-0 bg-foreground\/5 text-foreground[\s\S]*?>New Key<\/button>/,
);
assert.match(markup, />Import Certificate<\/button>/);
assert.match(markup, />New Identity<\/button>/);
assert.doesNotMatch(markup, />KEY<\/button>/);
assert.doesNotMatch(markup, />CERTIFICATE<\/button>/);
assert.doesNotMatch(markup, />Identities<\/button>/);
});
test("keychain search reveals matching keys when identities do not match", () => {
assert.equal(shouldShowIdentitySection({
identityCount: 1,
filteredIdentityCount: 0,
filteredKeyCount: 1,
search: "matching-key",
}), false);
});
test("keychain search keeps identities for identity matches or no results", () => {
assert.equal(shouldShowIdentitySection({
identityCount: 1,
filteredIdentityCount: 1,
filteredKeyCount: 0,
search: "matching-identity",
}), true);
assert.equal(shouldShowIdentitySection({
identityCount: 1,
filteredIdentityCount: 0,
filteredKeyCount: 0,
search: "no-results",
}), true);
});
test("keychain search shows both sections when both kinds match", () => {
const state = {
identityCount: 1,
filteredIdentityCount: 1,
filteredKeyCount: 1,
search: "shared-label",
};
assert.equal(shouldShowIdentitySection(state), true);
assert.equal(shouldShowKeySection(state), true);
});
test("keychain browsing shows keys and identities together", () => {
assert.equal(shouldShowKeySection({
identityCount: 1,
filteredKeyCount: 1,
search: "",
}), true);
assert.equal(shouldShowIdentitySection({
identityCount: 1,
filteredIdentityCount: 1,
filteredKeyCount: 1,
search: "",
}), true);
assert.equal(shouldShowIdentitySection({
identityCount: 0,
filteredIdentityCount: 0,
filteredKeyCount: 1,
search: "",
}), false);
});
test("keychain browsing hides keys when only identities exist", () => {
assert.equal(shouldShowKeySection({
identityCount: 1,
filteredKeyCount: 0,
search: "",
}), false);
});
test("keychain search hides the keys section when only identities match", () => {
assert.equal(shouldShowKeySection({
identityCount: 1,
filteredKeyCount: 0,
search: "identity-only",
}), false);
});
test("keychain search keeps keys visible for empty key searches without identities", () => {
assert.equal(shouldShowKeySection({
identityCount: 0,
filteredKeyCount: 0,
search: "no-results",
}), true);
});
test("keychain search hides keys when identities cover the empty-search state", () => {
assert.equal(shouldShowKeySection({
identityCount: 1,
filteredKeyCount: 0,
search: "no-results",
}), false);
});
test("keychain distinguishes search misses from an empty vault", () => {
assert.equal(shouldShowSearchNoResults("missing", 0, 1), true);
assert.equal(shouldShowSearchNoResults("", 0, 1), false);
assert.equal(shouldShowSearchNoResults("missing", 0, 0), false);
});