feat(FN-3723): fix plugin toggle rendering

The merge completes the plugin toggle rendering fix (FN-3723) by verifying the quality gates and adding the required changeset for the patch release.

Fusion-Task-Id: FN-3723
This commit is contained in:
Fusion
2026-05-07 23:32:15 -07:00
committed by gsxdsm
parent 924584a96d
commit 966368c250
5 changed files with 150 additions and 8 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Fix plugin list enable/disable toggle rendering so the native checkbox is visually hidden and the custom slider reflects checked and focus-visible states.

View File

@@ -984,6 +984,7 @@ export function PluginManager({ addToast, projectId }: PluginManagerProps) {
type="checkbox"
checked={plugin.enabled}
onChange={() => plugin.enabled ? handleDisable(plugin) : handleEnable(plugin)}
aria-label={`${plugin.enabled ? "Disable" : "Enable"} ${plugin.name}`}
/>
<span className="toggle-slider"></span>
</label>

View File

@@ -0,0 +1,116 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, waitFor, cleanup } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { PluginManager } from "../PluginManager";
import { loadAllAppCss } from "../../test/cssFixture";
vi.mock("../../api", () => ({
fetchPlugins: vi.fn(() => Promise.resolve([])),
installPlugin: vi.fn(() => Promise.resolve({})),
enablePlugin: vi.fn(() => Promise.resolve({})),
disablePlugin: vi.fn(() => Promise.resolve({})),
uninstallPlugin: vi.fn(() => Promise.resolve()),
fetchPluginSettings: vi.fn(() => Promise.resolve({})),
updatePluginSettings: vi.fn(() => Promise.resolve({})),
reloadPlugin: vi.fn(() => Promise.resolve({})),
fetchPluginSetupStatus: vi.fn(() => Promise.resolve({ hasSetup: false })),
installPluginSetup: vi.fn(() => Promise.resolve({ success: true })),
updatePlugin: vi.fn(() => Promise.resolve({})),
rescanPlugin: vi.fn(() => Promise.resolve({})),
browseDirectory: vi.fn(() => Promise.resolve({ currentPath: "/", parentPath: null, entries: [] })),
}));
import { fetchPlugins, disablePlugin } from "../../api";
const addToast = vi.fn();
function plugin(enabled: boolean) {
return {
id: "plugin-a",
name: "Test Plugin A",
version: "1.0.0",
state: "started" as const,
enabled,
path: "/plugins/plugin-a",
settings: {},
settingsSchema: {},
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
};
}
beforeEach(() => {
vi.clearAllMocks();
const styleEl = document.createElement("style");
styleEl.setAttribute("data-test-id", "all-app-css");
styleEl.textContent = loadAllAppCss();
document.head.appendChild(styleEl);
const esInstance = {
readyState: 1,
close: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
onerror: null,
onopen: null,
onmessage: null,
};
const MockES = vi.fn(() => esInstance) as unknown as typeof EventSource;
(MockES as unknown as { CONNECTING: number; OPEN: number; CLOSED: number }).CONNECTING = 0;
(MockES as unknown as { CONNECTING: number; OPEN: number; CLOSED: number }).OPEN = 1;
(MockES as unknown as { CONNECTING: number; OPEN: number; CLOSED: number }).CLOSED = 2;
vi.stubGlobal("EventSource", MockES);
});
afterEach(() => {
cleanup();
document.querySelector('[data-test-id="all-app-css"]')?.remove();
vi.restoreAllMocks();
});
describe("PluginManager toggle switch", () => {
it("keeps checkbox focusable but visually hidden", async () => {
vi.mocked(fetchPlugins).mockResolvedValue([plugin(true)]);
render(<PluginManager addToast={addToast} />);
const checkbox = await screen.findByRole("checkbox", { name: "Disable Test Plugin A" });
const styles = getComputedStyle(checkbox);
expect(styles.position).toBe("absolute");
expect(styles.opacity).toBe("0");
expect(styles.pointerEvents).toBe("none");
});
it("toggles by clicking the label/slider control", async () => {
vi.mocked(fetchPlugins).mockResolvedValue([plugin(true)]);
render(<PluginManager addToast={addToast} />);
const checkbox = await screen.findByRole("checkbox", { name: "Disable Test Plugin A" });
const label = checkbox.closest("label.toggle-switch") as HTMLLabelElement;
await userEvent.click(label);
await waitFor(() => {
expect(disablePlugin).toHaveBeenCalledWith("plugin-a", undefined);
});
});
it("renders slider next to input and reflects enabled state", async () => {
vi.mocked(fetchPlugins).mockResolvedValue([plugin(true)]);
const first = render(<PluginManager addToast={addToast} />);
const enabled = await screen.findByRole("checkbox", { name: "Disable Test Plugin A" });
expect(enabled).toBeChecked();
expect(enabled.nextElementSibling).toHaveClass("toggle-slider");
first.unmount();
vi.mocked(fetchPlugins).mockResolvedValue([plugin(false)]);
render(<PluginManager addToast={addToast} />);
const disabled = await screen.findByRole("checkbox", { name: "Enable Test Plugin A" });
expect(disabled).not.toBeChecked();
expect(disabled.nextElementSibling).toHaveClass("toggle-slider");
});
});

View File

@@ -2900,18 +2900,28 @@ input[type="range"]:focus-visible {
margin: 4px;
}
/* Auto-merge toggle */
.auto-merge-toggle {
display: flex;
/* Toggle controls */
.auto-merge-toggle,
.toggle-switch {
display: inline-flex;
align-items: center;
gap: 6px;
gap: var(--space-sm);
cursor: pointer;
user-select: none;
flex-shrink: 0;
}
.auto-merge-toggle input {
display: none;
.toggle-switch {
position: relative;
}
.auto-merge-toggle input,
.toggle-switch input[type="checkbox"] {
position: absolute;
inline-size: 0;
block-size: 0;
opacity: 0;
pointer-events: none;
}
.toggle-slider {
@@ -2936,14 +2946,21 @@ input[type="range"]:focus-visible {
transition: transform var(--transition-normal);
}
.auto-merge-toggle input:checked + .toggle-slider {
.auto-merge-toggle input:checked + .toggle-slider,
.toggle-switch input:checked + .toggle-slider {
background: var(--in-review);
}
.auto-merge-toggle input:checked + .toggle-slider::after {
.auto-merge-toggle input:checked + .toggle-slider::after,
.toggle-switch input:checked + .toggle-slider::after {
transform: translateX(12px);
}
.toggle-switch input:focus-visible + .toggle-slider,
.auto-merge-toggle input:focus-visible + .toggle-slider {
box-shadow: var(--focus-ring-strong);
}
.toggle-label {
font-size: 0.6875rem;
color: var(--text-muted);

3
pnpm-lock.yaml generated
View File

@@ -758,6 +758,9 @@ importers:
'@fusion/core':
specifier: workspace:*
version: link:../../packages/core
'@fusion/dashboard':
specifier: workspace:*
version: link:../../packages/dashboard
'@fusion/plugin-sdk':
specifier: workspace:*
version: link:../../packages/plugin-sdk