mirror of
https://github.com/YuzuZensai/Termix.git
synced 2026-11-12 17:11:48 +00:00
v26.10.1 (#1599)
* feat: AI translations at release, 7 new languages and regional fallback * fix: proper plural forms for count strings and full translations * fix: plural forms for selected, hidden and active counts * fix: synced plugin updates failing on desktop and server-only plugins shown as stopped * fix: restore rounded corners on app icons and favicon * feat: desktop app auto updates with automatic, ask first or never modes * feat: community and custom plugin registries * feat: plugin guide for AI tools in the SDK with a lint check * fix: keep the terminal WebSocket open past nginx's 60s default (#1588) * chore: update changelog * feat: show plugin download counts only and stop CI builds inflating them * feat: hide installed plugins in the browse tab by default * chore: pin bundled plugins to latest releases * chore: update translations * fix: focusing a saved password field and saving no longer erases it * fix: tell desktop users when their 2.8 data needs 2.9 first * fix: point windows update info at the x64 installer * fix: keep desktop client tunnel sockets alive behind proxies * chore: pin bundled plugins to latest releases * fix: cover client tunnel keepalive in the c2s runtime tests * fix: typing after opening a host no longer goes to the host search * fix: accept tab and = separators in SSH config import (#1598) * chore: update changelog * chore: drop plugin versions from core changelog * chore: update translations * chore: lint, format, and bump version to 26.10.1 --------- Co-authored-by: Darnel Hunter <68697450+innotelinc@users.noreply.github.com> Co-authored-by: Iain <Iain1970@protonmail.com>
This commit is contained in:
co-authored by
Darnel Hunter
Iain
parent
6873bc92bb
commit
62ac931468
@@ -3,9 +3,11 @@ import { describe, expect, it, beforeEach } from "vitest";
|
||||
import {
|
||||
changeAppLanguage,
|
||||
consumeLoginLanguage,
|
||||
LOCALE_FILES,
|
||||
normalizeLanguageCode,
|
||||
rememberLoginLanguage,
|
||||
} from "../../i18n/i18n";
|
||||
import { LANGUAGES, TRANSLATED_LANGUAGES } from "../../i18n/languages";
|
||||
|
||||
describe("i18n language handling", () => {
|
||||
beforeEach(() => {
|
||||
@@ -20,6 +22,34 @@ describe("i18n language handling", () => {
|
||||
expect(normalizeLanguageCode("unknown")).toBe("en");
|
||||
});
|
||||
|
||||
it("maps regional variants to the closest shipped language", () => {
|
||||
expect(normalizeLanguageCode("es-MX")).toBe("es-ES");
|
||||
expect(normalizeLanguageCode("es")).toBe("es-ES");
|
||||
expect(normalizeLanguageCode("sv")).toBe("sv-SE");
|
||||
expect(normalizeLanguageCode("pt")).toBe("pt-BR");
|
||||
expect(normalizeLanguageCode("pt-AO")).toBe("pt-BR");
|
||||
expect(normalizeLanguageCode("zh")).toBe("zh-CN");
|
||||
expect(normalizeLanguageCode("zh-HK")).toBe("zh-TW");
|
||||
expect(normalizeLanguageCode("zh-Hant-TW")).toBe("zh-TW");
|
||||
expect(normalizeLanguageCode("nb-NO")).toBe("no");
|
||||
expect(normalizeLanguageCode("de-AT")).toBe("de");
|
||||
expect(normalizeLanguageCode("sk-SK")).toBe("sk");
|
||||
});
|
||||
|
||||
it("ships a translated file for every listed language", () => {
|
||||
const files = new Set(
|
||||
Object.keys(import.meta.glob("../../locales/translated/*.json")).map(
|
||||
(path) => path.split("/").pop()?.replace(".json", ""),
|
||||
),
|
||||
);
|
||||
for (const language of TRANSLATED_LANGUAGES) {
|
||||
expect(LOCALE_FILES[language.code]).toBe(language.file);
|
||||
expect(files.has(language.file), language.file).toBe(true);
|
||||
}
|
||||
expect(LANGUAGES[0]).toEqual({ code: "en", label: "English" });
|
||||
expect(LANGUAGES).toHaveLength(TRANSLATED_LANGUAGES.length + 1);
|
||||
});
|
||||
|
||||
it("stores the normalized language after a successful switch", async () => {
|
||||
await expect(changeAppLanguage("zh_CN")).resolves.toBe("zh-CN");
|
||||
expect(localStorage.getItem("i18nextLng")).toBe("zh-CN");
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { updateToastFor, type DesktopUpdateState } from "@/lib/desktop-updates";
|
||||
|
||||
function state(patch: Partial<DesktopUpdateState>): DesktopUpdateState {
|
||||
return {
|
||||
kind: "auto",
|
||||
mode: "prompt",
|
||||
currentVersion: "26.10.1",
|
||||
status: "available",
|
||||
version: "26.10.2",
|
||||
releaseUrl: null,
|
||||
progress: null,
|
||||
error: null,
|
||||
...patch,
|
||||
};
|
||||
}
|
||||
|
||||
describe("updateToastFor", () => {
|
||||
it("asks before downloading in ask first mode", () => {
|
||||
expect(updateToastFor(state({}))).toBe("prompt");
|
||||
});
|
||||
|
||||
it("stays quiet while automatic mode downloads", () => {
|
||||
expect(updateToastFor(state({ mode: "automatic" }))).toBeNull();
|
||||
expect(
|
||||
updateToastFor(state({ mode: "automatic", status: "downloading" })),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("offers a restart once downloaded", () => {
|
||||
expect(updateToastFor(state({ status: "downloaded" }))).toBe("ready");
|
||||
expect(
|
||||
updateToastFor(state({ mode: "automatic", status: "downloaded" })),
|
||||
).toBe("ready");
|
||||
});
|
||||
|
||||
it("links the release for installs that can not update", () => {
|
||||
expect(updateToastFor(state({ kind: "manual" }))).toBe("manual");
|
||||
});
|
||||
|
||||
it("never toasts when updates are off", () => {
|
||||
expect(updateToastFor(state({ mode: "never" }))).toBeNull();
|
||||
expect(
|
||||
updateToastFor(state({ mode: "never", status: "downloaded" })),
|
||||
).toBeNull();
|
||||
expect(updateToastFor(null)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -38,6 +38,20 @@ describe("getEmbeddedServerFailure", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("surfaces a database too old to open", async () => {
|
||||
setElectronAPI({
|
||||
isElectron: true,
|
||||
getEmbeddedServerStatus: vi.fn().mockResolvedValue({
|
||||
running: false,
|
||||
failure: { reason: "old-database", port: null },
|
||||
}),
|
||||
});
|
||||
await expect(getEmbeddedServerFailure()).resolves.toEqual({
|
||||
reason: "old-database",
|
||||
port: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("surfaces a generic crash", async () => {
|
||||
setElectronAPI({
|
||||
isElectron: true,
|
||||
|
||||
@@ -93,6 +93,22 @@ describe("CORE_STEPS", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("asks how the desktop app updates when it has an updater", () => {
|
||||
const win = window as unknown as { electronAPI?: unknown };
|
||||
win.electronAPI = { isElectron: true, updates: {} };
|
||||
try {
|
||||
expect(ids(ctx({ isDesktop: true }))).toEqual([
|
||||
"desktop-sync",
|
||||
"desktop-updates",
|
||||
"preset",
|
||||
"appearance",
|
||||
]);
|
||||
expect(ids(ctx())).not.toContain("desktop-updates");
|
||||
} finally {
|
||||
delete win.electronAPI;
|
||||
}
|
||||
});
|
||||
|
||||
it("offers account security only when there is something to set up", () => {
|
||||
expect(ids(ctx({ canEnrollSecondFactor: true }))).toEqual([
|
||||
"preset",
|
||||
|
||||
@@ -28,8 +28,7 @@ const [plugin] = mergePlugins(
|
||||
pinnedVersion: null,
|
||||
autoUpdate: false,
|
||||
bundled: false,
|
||||
installCount: null,
|
||||
installCountSource: null,
|
||||
downloads: null,
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
@@ -22,8 +22,7 @@ const plugin = {
|
||||
capabilities: [],
|
||||
contributes: {},
|
||||
versions: [],
|
||||
installCount: null,
|
||||
installCountSource: null,
|
||||
downloads: null,
|
||||
};
|
||||
|
||||
vi.mock("@/plugins/use-plugins-manager", () => ({
|
||||
@@ -40,6 +39,7 @@ vi.mock("@/plugins/use-plugins-manager", () => ({
|
||||
confirmConsent: vi.fn(),
|
||||
developerMode: false,
|
||||
signedOnly: false,
|
||||
registries: [],
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -60,17 +60,32 @@ beforeEach(() => localStorage.clear());
|
||||
afterEach(cleanup);
|
||||
|
||||
describe("PluginsTab", () => {
|
||||
it("tells people community plugins are coming and links the guide", async () => {
|
||||
it("points to the Registries tab and links the submit guide", async () => {
|
||||
await renderTab();
|
||||
fireEvent.click(screen.getByText(en.plugins.manager.tabs.browse));
|
||||
|
||||
expect(
|
||||
screen.getByText(en.plugins.manager.communitySoon, { exact: false }),
|
||||
screen.getByText(en.plugins.manager.registries.browseHint, {
|
||||
exact: false,
|
||||
}),
|
||||
).toBeTruthy();
|
||||
const link = screen.getByText(en.plugins.manager.communitySubmit);
|
||||
expect(link.getAttribute("href")).toBe(docsUrl("communityRegistry"));
|
||||
});
|
||||
|
||||
it("hides installed plugins in Browse until shown", async () => {
|
||||
await renderTab();
|
||||
fireEvent.click(screen.getByText(en.plugins.manager.tabs.browse));
|
||||
|
||||
expect(screen.queryByText("Demo Plugin")).toBeNull();
|
||||
expect(screen.getByText(en.plugins.manager.allInstalled)).toBeTruthy();
|
||||
|
||||
fireEvent.click(screen.getByRole("switch"));
|
||||
|
||||
expect(screen.getByText("Demo Plugin")).toBeTruthy();
|
||||
expect(localStorage.getItem("pluginsTab.browseInstalled")).toBe("show");
|
||||
});
|
||||
|
||||
it("switches to a table and remembers it", async () => {
|
||||
await renderTab();
|
||||
expect(screen.queryByRole("table")).toBeNull();
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
cleanup,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from "@testing-library/react";
|
||||
import i18next from "i18next";
|
||||
import { I18nextProvider, initReactI18next } from "react-i18next";
|
||||
import en from "@/locales/en.json";
|
||||
import { RegistriesSection } from "@/plugins/RegistriesSection";
|
||||
import type { PluginsManager } from "@/plugins/use-plugins-manager";
|
||||
|
||||
const api = vi.hoisted(() => ({
|
||||
getRegistries: vi.fn(),
|
||||
previewRegistry: vi.fn(),
|
||||
addRegistry: vi.fn(),
|
||||
setRegistryEnabled: vi.fn(),
|
||||
removeRegistry: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/api/plugins-api", () => api);
|
||||
vi.mock("sonner", () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
|
||||
|
||||
const manager = {
|
||||
registries: [],
|
||||
managedByServer: false,
|
||||
refresh: vi.fn(async () => {}),
|
||||
} as unknown as PluginsManager;
|
||||
|
||||
function state(communityEnabled = false) {
|
||||
return {
|
||||
managedByServer: false,
|
||||
official: { id: "official", url: "https://example.com/official.json" },
|
||||
community: {
|
||||
id: "community",
|
||||
url: "https://example.com/community.json",
|
||||
enabled: communityEnabled,
|
||||
},
|
||||
custom: [],
|
||||
};
|
||||
}
|
||||
|
||||
async function renderSection() {
|
||||
const i18n = i18next.createInstance();
|
||||
await i18n.use(initReactI18next).init({
|
||||
lng: "en",
|
||||
resources: { en: { translation: en } },
|
||||
});
|
||||
return render(
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<RegistriesSection manager={manager} />
|
||||
</I18nextProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
for (const fn of Object.values(api)) fn.mockReset();
|
||||
api.getRegistries.mockResolvedValue(state());
|
||||
});
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("RegistriesSection", () => {
|
||||
it("asks before turning community plugins on", async () => {
|
||||
const confirm = vi.spyOn(window, "confirm").mockReturnValue(false);
|
||||
await renderSection();
|
||||
const toggle = await screen.findByRole("switch");
|
||||
|
||||
fireEvent.click(toggle);
|
||||
await waitFor(() => expect(confirm).toHaveBeenCalled());
|
||||
expect(confirm.mock.calls[0][0]).toContain(
|
||||
en.plugins.manager.registries.communityConfirmBody,
|
||||
);
|
||||
expect(api.setRegistryEnabled).not.toHaveBeenCalled();
|
||||
|
||||
confirm.mockReturnValue(true);
|
||||
api.getRegistries.mockResolvedValue(state(true));
|
||||
fireEvent.click(toggle);
|
||||
await waitFor(() =>
|
||||
expect(api.setRegistryEnabled).toHaveBeenCalledWith("community", true),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not ask when turning community plugins off", async () => {
|
||||
const confirm = vi.spyOn(window, "confirm");
|
||||
api.getRegistries.mockResolvedValue(state(true));
|
||||
await renderSection();
|
||||
fireEvent.click(await screen.findByRole("switch"));
|
||||
await waitFor(() =>
|
||||
expect(api.setRegistryEnabled).toHaveBeenCalledWith("community", false),
|
||||
);
|
||||
expect(confirm).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("checks a registry before adding it with the key it showed", async () => {
|
||||
api.previewRegistry.mockResolvedValue({
|
||||
id: "acme",
|
||||
name: "Acme Plugins",
|
||||
url: "https://acme.example/index.json",
|
||||
keyId: "abcd1234abcd1234",
|
||||
plugins: 3,
|
||||
});
|
||||
api.addRegistry.mockResolvedValue({
|
||||
id: "acme",
|
||||
name: "Acme Plugins",
|
||||
url: "https://acme.example/index.json",
|
||||
keyId: "abcd1234abcd1234",
|
||||
enabled: true,
|
||||
addedAt: "",
|
||||
});
|
||||
await renderSection();
|
||||
fireEvent.click(await screen.findByText(en.plugins.manager.registries.add));
|
||||
fireEvent.change(
|
||||
screen.getByLabelText(en.plugins.manager.registries.urlLabel),
|
||||
{ target: { value: "https://acme.example/index.json" } },
|
||||
);
|
||||
fireEvent.click(screen.getByText(en.plugins.manager.registries.check));
|
||||
|
||||
expect(await screen.findByText("Acme Plugins")).toBeTruthy();
|
||||
expect(screen.getByText("Key abcd1234abcd1234")).toBeTruthy();
|
||||
expect(api.addRegistry).not.toHaveBeenCalled();
|
||||
|
||||
fireEvent.click(screen.getByText(en.plugins.manager.registries.addConfirm));
|
||||
await waitFor(() =>
|
||||
expect(api.addRegistry).toHaveBeenCalledWith(
|
||||
"https://acme.example/index.json",
|
||||
"abcd1234abcd1234",
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -61,8 +61,7 @@ function entry(
|
||||
pinnedVersion: null,
|
||||
autoUpdate: false,
|
||||
bundled: true,
|
||||
installCount: null,
|
||||
installCountSource: null,
|
||||
downloads: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
@@ -125,6 +124,14 @@ describe("mergePlugins", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("offers no update for a plugin a linked desktop leaves to its server", () => {
|
||||
const [row] = mergePlugins(
|
||||
[summary({ enabled: false, state: "disabled", runsOnServer: true })],
|
||||
[entry()],
|
||||
);
|
||||
expect(row).toMatchObject({ status: "server", updateAvailable: false });
|
||||
});
|
||||
|
||||
it("keeps installed plugins when the registry is unreachable", () => {
|
||||
const rows = mergePlugins([summary()], null);
|
||||
expect(rows).toHaveLength(1);
|
||||
@@ -140,6 +147,9 @@ describe("pluginStatus", () => {
|
||||
"disabled",
|
||||
);
|
||||
expect(pluginStatus(summary())).toBe("running");
|
||||
expect(pluginStatus(summary({ state: "failed", runsOnServer: true }))).toBe(
|
||||
"server",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -224,9 +234,9 @@ describe("popularity", () => {
|
||||
const rows = mergePlugins(
|
||||
[],
|
||||
[
|
||||
entry({ id: "a", name: "A", installed: false, installCount: 5 }),
|
||||
entry({ id: "b", name: "B", installed: false, installCount: null }),
|
||||
entry({ id: "c", name: "C", installed: false, installCount: 90 }),
|
||||
entry({ id: "a", name: "A", installed: false, downloads: 5 }),
|
||||
entry({ id: "b", name: "B", installed: false, downloads: null }),
|
||||
entry({ id: "c", name: "C", installed: false, downloads: 90 }),
|
||||
],
|
||||
);
|
||||
expect(sortPlugins(rows, "popular").map((r) => r.id)).toEqual([
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
act,
|
||||
cleanup,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
} from "@testing-library/react";
|
||||
import { DesktopUpdatesCard } from "@/settings/DesktopUpdatesCard";
|
||||
import type { DesktopUpdateState } from "@/lib/desktop-updates";
|
||||
|
||||
type TestWindow = { electronAPI?: unknown };
|
||||
|
||||
function state(patch: Partial<DesktopUpdateState> = {}): DesktopUpdateState {
|
||||
return {
|
||||
kind: "auto",
|
||||
mode: "prompt",
|
||||
currentVersion: "26.10.1",
|
||||
status: "idle",
|
||||
version: null,
|
||||
releaseUrl: null,
|
||||
progress: null,
|
||||
error: null,
|
||||
...patch,
|
||||
};
|
||||
}
|
||||
|
||||
function bridge(initial: DesktopUpdateState) {
|
||||
return {
|
||||
getState: vi.fn(async () => initial),
|
||||
check: vi.fn(async () => initial),
|
||||
download: vi.fn(async () => initial),
|
||||
install: vi.fn(async () => true),
|
||||
setMode: vi.fn(async () => initial),
|
||||
onState: vi.fn(() => () => {}),
|
||||
};
|
||||
}
|
||||
|
||||
async function renderWith(initial: DesktopUpdateState) {
|
||||
const api = bridge(initial);
|
||||
(window as TestWindow).electronAPI = { isElectron: true, updates: api };
|
||||
await act(async () => {
|
||||
render(<DesktopUpdatesCard />);
|
||||
});
|
||||
return api;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
delete (window as TestWindow).electronAPI;
|
||||
});
|
||||
|
||||
describe("DesktopUpdatesCard", () => {
|
||||
it("renders nothing in the browser", () => {
|
||||
const { container } = render(<DesktopUpdatesCard />);
|
||||
expect(container.innerHTML).toBe("");
|
||||
});
|
||||
|
||||
it("checks on demand", async () => {
|
||||
const api = await renderWith(state());
|
||||
fireEvent.click(screen.getByText("desktopUpdates.checkNow"));
|
||||
expect(api.check).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("offers the download in ask first mode", async () => {
|
||||
const api = await renderWith(
|
||||
state({ status: "available", version: "26.10.2" }),
|
||||
);
|
||||
fireEvent.click(screen.getByText("desktopUpdates.download"));
|
||||
expect(api.download).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("restarts into a downloaded update", async () => {
|
||||
const api = await renderWith(
|
||||
state({ status: "downloaded", version: "26.10.2" }),
|
||||
);
|
||||
fireEvent.click(screen.getByText("desktopUpdates.restart"));
|
||||
expect(api.install).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("links the release for installs that can not update", async () => {
|
||||
await renderWith(
|
||||
state({
|
||||
kind: "manual",
|
||||
status: "available",
|
||||
version: "26.10.2",
|
||||
releaseUrl: "https://example.com/r",
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
screen.getByText("desktopUpdates.download").closest("a"),
|
||||
).toHaveProperty("href", "https://example.com/r");
|
||||
});
|
||||
|
||||
it("hides on dev builds", async () => {
|
||||
await renderWith(state({ kind: "disabled" }));
|
||||
expect(screen.queryByText("desktopUpdates.title")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -33,6 +33,16 @@ describe("settings pages", () => {
|
||||
expect(admin).toContain("security");
|
||||
});
|
||||
|
||||
it("shows Updates to everyone on desktop but only admins in the browser", () => {
|
||||
const ids = (isAdmin: boolean, isElectron: boolean) =>
|
||||
visibleSettingsPages(CORE_SETTINGS_PAGES, { isAdmin, isElectron }).map(
|
||||
(page) => page.id,
|
||||
);
|
||||
expect(ids(false, true)).toContain("updates");
|
||||
expect(ids(false, false)).not.toContain("updates");
|
||||
expect(ids(true, false)).toContain("updates");
|
||||
});
|
||||
|
||||
it("finds a page by a label shown on it, not only its name", () => {
|
||||
const hits = searchSettingsPages(CORE_SETTINGS_PAGES, "theme", translate);
|
||||
expect(hits.some((hit) => hit.page.id === "appearance")).toBe(true);
|
||||
|
||||
@@ -23,7 +23,9 @@ vi.mock("@/sidebar/tree/FolderItem/FolderItem", () => ({
|
||||
folderHostCount: () => 1,
|
||||
}));
|
||||
vi.mock("@/sidebar/tree/HostItem/HostItem", () => ({
|
||||
HostItem: () => <div>Host</div>,
|
||||
HostItem: ({ onOpenTab }: { onOpenTab: (type: string) => void }) => (
|
||||
<button onClick={() => onOpenTab("terminal")}>Host</button>
|
||||
),
|
||||
}));
|
||||
|
||||
const observers: Array<{
|
||||
@@ -161,13 +163,45 @@ it("finds a host inside a closed folder and opens the keyboard-selected match",
|
||||
const search = view.getByLabelText("hosts.speedSearch");
|
||||
expect(document.activeElement).toBe(search);
|
||||
expect(view.container.querySelectorAll("[data-index]")).toHaveLength(3);
|
||||
fireEvent.keyDown(search, { key: "ArrowDown" });
|
||||
fireEvent.keyDown(search, { key: "Enter" });
|
||||
expect(onOpenTab).toHaveBeenCalledWith(beta, "terminal", {
|
||||
forceNewTab: false,
|
||||
});
|
||||
fireEvent.keyDown(search, { key: "Escape" });
|
||||
expect(view.queryByLabelText("hosts.speedSearch")).toBeNull();
|
||||
expect(view.container.querySelectorAll("[data-index]")).toHaveLength(1);
|
||||
expect(document.activeElement).toBe(list);
|
||||
|
||||
fireEvent.keyDown(list, { key: "a" });
|
||||
const again = view.getByLabelText("hosts.speedSearch");
|
||||
fireEvent.keyDown(again, { key: "ArrowDown" });
|
||||
fireEvent.keyDown(again, { key: "Enter" });
|
||||
expect(onOpenTab).toHaveBeenCalledWith(beta, "terminal", {
|
||||
forceNewTab: false,
|
||||
});
|
||||
// The opened session takes over the keyboard.
|
||||
expect(view.queryByLabelText("hosts.speedSearch")).toBeNull();
|
||||
expect(view.container.contains(document.activeElement)).toBe(false);
|
||||
});
|
||||
|
||||
it("lets go of focus when a host opens, so typing reaches the session", () => {
|
||||
const host = {
|
||||
id: "1",
|
||||
name: "alpha",
|
||||
ip: "192.0.2.1",
|
||||
username: "u",
|
||||
} as Host;
|
||||
const onOpenTab = vi.fn();
|
||||
const view = render(
|
||||
<SidebarTree
|
||||
children={[{ name: "group", path: "group", children: [host] }]}
|
||||
onOpenTab={onOpenTab}
|
||||
onEditHost={vi.fn()}
|
||||
selectionMode={false}
|
||||
onToggleSelectionMode={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
const row = view.getByText("Host");
|
||||
row.focus();
|
||||
fireEvent.click(row);
|
||||
expect(onOpenTab).toHaveBeenCalledWith(host, "terminal", undefined);
|
||||
expect(document.activeElement).not.toBe(row);
|
||||
fireEvent.keyDown(document.body, { key: "a" });
|
||||
expect(view.queryByLabelText("hosts.speedSearch")).toBeNull();
|
||||
});
|
||||
|
||||
@@ -38,8 +38,12 @@ describe("onboarding seen map", () => {
|
||||
});
|
||||
|
||||
it("legacy seen covers every core step at launch", () => {
|
||||
// Steps added later stay out so upgraded users still see them.
|
||||
const addedLater = ["desktop-updates"];
|
||||
expect(Object.keys(LEGACY_SEEN).sort()).toEqual(
|
||||
Object.keys(CORE_ONBOARDING_STEPS).sort(),
|
||||
Object.keys(CORE_ONBOARDING_STEPS)
|
||||
.filter((key) => !addedLater.includes(key))
|
||||
.sort(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user