diff --git a/src/ui/plugin-host/shell-bridge.ts b/src/ui/plugin-host/shell-bridge.ts index 1654a058c..aecb318e3 100644 --- a/src/ui/plugin-host/shell-bridge.ts +++ b/src/ui/plugin-host/shell-bridge.ts @@ -114,6 +114,11 @@ export const shell: TabShellCallbacks = { openRailView: (...args) => callbacks?.openRailView(...args), closeRailView: (...args) => callbacks?.closeRailView(...args), openHostEditor: (...args) => callbacks?.openHostEditor?.(...args), + get editHost() { + return callbacks?.editHost + ? (hostId: number) => callbacks?.editHost?.(hostId) + : undefined; + }, saveQuickConnect: (...args) => callbacks?.saveQuickConnect?.(...args) ?? Promise.resolve(), confirm: (options) => diff --git a/src/ui/tests/plugin-host/shell-bridge.test.ts b/src/ui/tests/plugin-host/shell-bridge.test.ts index a55ac4b16..a0e415bd8 100644 --- a/src/ui/tests/plugin-host/shell-bridge.test.ts +++ b/src/ui/tests/plugin-host/shell-bridge.test.ts @@ -1,4 +1,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; +import { createElement } from "react"; +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { withPluginScope } from "@/plugin-host/scope"; import { resetShellBridge, setShellCallbacks, @@ -18,7 +21,10 @@ const saved = { socks5Host: "proxy", } as unknown as Host; -afterEach(() => resetShellBridge()); +afterEach(() => { + cleanup(); + resetShellBridge(); +}); describe("withShellHost", () => { it("fills a typed record back in from the shell's own host", () => { @@ -50,3 +56,65 @@ describe("withShellHost", () => { expect(openTab.mock.calls[0][0]).toMatchObject({ socks5Host: "proxy" }); }); }); + +describe("shell.editHost", () => { + it("forwards saved host editing to the current shell", () => { + const editHost = vi.fn(); + const openHostEditor = vi.fn(); + setShellCallbacks({ editHost, openHostEditor } as never); + expect(shell.editHost).toBeTypeOf("function"); + const edit = shell.editHost!; + edit(5); + expect(editHost).toHaveBeenCalledWith(5); + expect(openHostEditor).not.toHaveBeenCalled(); + const next = vi.fn(); + setShellCallbacks({ editHost: next } as never); + edit(7); + expect(next).toHaveBeenCalledWith(7); + resetShellBridge(); + expect(() => edit(5)).not.toThrow(); + expect(next).toHaveBeenCalledTimes(1); + }); + + it("preserves editing through the scoped plugin view without exposing other settings", () => { + const editHost = vi.fn(); + setShellCallbacks({ editHost } as never); + let seen: Host | undefined; + const View = withPluginScope( + "fixture", + ({ host, shell: bridge }: { host: Host; shell: typeof shell }) => { + seen = host; + return bridge.editHost + ? createElement( + "button", + { + onClick: () => bridge.editHost?.(Number(host.id)), + }, + "Edit host", + ) + : null; + }, + ); + render( + createElement(View, { + host: { + ...saved, + pluginSettings: { + fixture: { enabled: true }, + other: { enabled: false }, + }, + }, + shell, + }), + ); + fireEvent.click(screen.getByText("Edit host")); + expect(editHost).toHaveBeenCalledWith(5); + expect(seen?.id).toBe("5"); + expect(seen?.pluginSettings).toEqual({ fixture: { enabled: true } }); + }); + + it("does not advertise editing when the mounted shell cannot edit hosts", () => { + setShellCallbacks({ openHostEditor: vi.fn() } as never); + expect(shell.editHost).toBeUndefined(); + }); +});