feat(FN-3403): add shell multi-profile support with connection manager onbo
Merges shell multi-profile support for desktop (FN-3403) — spanning new connection-manager flows, shell onboarding interoperability, mobile connection profiles, desktop IPC/shell-settings plumbing, and tokenized titlebar styles — with a secondary migration of the WhatsApp plugin to the Baileys pairi Fusion-Task-Id: FN-3403
This commit is contained in:
@@ -56,6 +56,27 @@ vi.mock("../native.js", () => ({
|
||||
vi.mock("../shell-settings.js", () => ({
|
||||
readShellSettings: mocks.readShellSettings,
|
||||
writeShellSettings: mocks.writeShellSettings,
|
||||
buildSavedProfile: (settings: { profiles: Array<{ id: string }>; }, profile: { id?: string; name: string; serverUrl: string }) => ({
|
||||
id: profile.id ?? "generated-id",
|
||||
name: profile.name.trim() || "Remote Server",
|
||||
serverUrl: profile.serverUrl,
|
||||
createdAt: "",
|
||||
updatedAt: "",
|
||||
authToken: null,
|
||||
lastUsedAt: null,
|
||||
}),
|
||||
applyDeleteProfile: (settings: { activeProfileId: string | null; profiles: Array<{ id: string }> }, profileId: string) => {
|
||||
const profiles = settings.profiles.filter((item) => item.id !== profileId);
|
||||
return {
|
||||
...settings,
|
||||
profiles,
|
||||
activeProfileId: settings.activeProfileId === profileId ? (profiles[0]?.id ?? null) : settings.activeProfileId,
|
||||
};
|
||||
},
|
||||
applySetActiveProfile: (settings: { profiles: Array<{ id: string }> }, profileId: string | null) => ({
|
||||
...settings,
|
||||
activeProfileId: profileId && settings.profiles.some((item) => item.id === profileId) ? profileId : null,
|
||||
}),
|
||||
getDesktopShellModeState: (settings: { hasCompletedModeSelection?: boolean; desktopMode?: "local" | "remote" | null }) => ({
|
||||
isFirstRun: !settings.hasCompletedModeSelection || !settings.desktopMode,
|
||||
desktopMode: settings.desktopMode ?? null,
|
||||
@@ -128,15 +149,27 @@ describe("ipc handlers", () => {
|
||||
expect(window.webContents.send).toHaveBeenCalledWith("shell:state", expect.any(Object));
|
||||
});
|
||||
|
||||
it("shell:saveProfile rejects invalid URLs", async () => {
|
||||
it("shell:saveProfile persists the helper-generated profile", async () => {
|
||||
await registerHandlers();
|
||||
const handler = mocks.ipcHandlers.get("shell:saveProfile");
|
||||
const result = await handler?.({}, { name: " Prod ", serverUrl: "https://fusion.example.com" });
|
||||
|
||||
await expect(handler?.({}, { name: "Prod", serverUrl: "not-a-url" })).rejects.toThrow(
|
||||
"Server URL must be a valid absolute URL",
|
||||
);
|
||||
await expect(handler?.({}, { name: "Prod", serverUrl: "ftp://fusion.example.com" })).rejects.toThrow(
|
||||
"Server URL must use http or https",
|
||||
);
|
||||
expect(result).toMatchObject({ id: "generated-id", name: "Prod" });
|
||||
expect(mocks.writeShellSettings).toHaveBeenCalledWith(expect.objectContaining({ profiles: [expect.objectContaining({ id: "generated-id" })] }));
|
||||
});
|
||||
|
||||
it("shell:deleteProfile falls back to first remaining profile when deleting active", async () => {
|
||||
mocks.readShellSettings.mockResolvedValueOnce({
|
||||
desktopMode: "remote",
|
||||
hasCompletedModeSelection: true,
|
||||
activeProfileId: "p2",
|
||||
profiles: [{ id: "p1" }, { id: "p2" }],
|
||||
});
|
||||
await registerHandlers();
|
||||
|
||||
const handler = mocks.ipcHandlers.get("shell:deleteProfile");
|
||||
await handler?.({}, "p2");
|
||||
|
||||
expect(mocks.writeShellSettings).toHaveBeenCalledWith(expect.objectContaining({ activeProfileId: "p1" }));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -101,4 +101,49 @@ describe("shell-settings", () => {
|
||||
desktopMode: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes invalid profiles, duplicate names, and invalid active id", async () => {
|
||||
mockState.content.set(
|
||||
"/tmp/fusion/shell-connections.json",
|
||||
JSON.stringify({
|
||||
activeProfileId: "missing",
|
||||
profiles: [
|
||||
{ id: "p1", name: "", serverUrl: "https://fusion.example.com" },
|
||||
{ id: "p2", name: "Remote Server", serverUrl: "https://staging.example.com" },
|
||||
{ id: "p3", name: "Bad", serverUrl: "not-a-url" },
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
const { readShellSettings } = await import("../shell-settings.ts");
|
||||
const settings = await readShellSettings();
|
||||
expect(settings.activeProfileId).toBeNull();
|
||||
expect(settings.profiles).toHaveLength(2);
|
||||
expect(settings.profiles[0]?.name).toBe("Remote Server");
|
||||
expect(settings.profiles[1]?.name).toBe("Remote Server (2)");
|
||||
});
|
||||
|
||||
it("deleting active profile picks fallback and deleting last clears state", async () => {
|
||||
const { applyDeleteProfile } = await import("../shell-settings.ts");
|
||||
const first = { id: "p1", name: "Prod", serverUrl: "https://fusion.example.com", authToken: null, createdAt: "", updatedAt: "" };
|
||||
const second = { id: "p2", name: "Staging", serverUrl: "https://staging.example.com", authToken: null, createdAt: "", updatedAt: "" };
|
||||
|
||||
const withFallback = applyDeleteProfile({
|
||||
desktopMode: "remote",
|
||||
hasCompletedModeSelection: true,
|
||||
activeProfileId: "p2",
|
||||
profiles: [first, second],
|
||||
}, "p2");
|
||||
|
||||
expect(withFallback.activeProfileId).toBe("p1");
|
||||
|
||||
const empty = applyDeleteProfile({
|
||||
desktopMode: "remote",
|
||||
hasCompletedModeSelection: true,
|
||||
activeProfileId: "p1",
|
||||
profiles: [first],
|
||||
}, "p1");
|
||||
|
||||
expect(empty).toMatchObject({ activeProfileId: null, profiles: [] });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user