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:
5
.changeset/fn-3723-plugin-toggle-rendering.md
Normal file
5
.changeset/fn-3723-plugin-toggle-rendering.md
Normal 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.
|
||||
@@ -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>
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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
3
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user