feat(FN-2579): merge fusion/fn-2579 (auto-resolved)

- fix(FN-2579): complete Step 3 — refine split-button accessibility
- fix(FN-2579): resolve workspace lint regression in provider
- test(FN-2579): complete Step 2 — update header engine control tests
- feat(FN-2579): complete Step 1 — style engine split-button dropdown
- feat(FN-2579): complete Step 0 — add engine control split-button behavior
- chore(release): v1.0.0
This commit is contained in:
Fusion
2026-04-26 00:29:19 -07:00
committed by gsxdsm
parent cddf7bbd64
commit 82cd58fe13
31 changed files with 307 additions and 99 deletions

View File

@@ -223,36 +223,40 @@ describe("Header", () => {
});
describe("pause controls", () => {
it("renders pause button for engine pause", () => {
it("renders engine control split-button", () => {
renderHeader();
expect(screen.getByTitle("Pause scheduling")).toBeDefined();
expect(screen.getByTestId("engine-control-main-btn")).toBeDefined();
expect(screen.getByTestId("engine-control-chevron-btn")).toBeDefined();
});
it("renders stop button for global pause", () => {
it("renders pause triage option in dropdown", () => {
renderHeader();
expect(screen.getByTitle("Stop AI engine")).toBeDefined();
fireEvent.click(screen.getByTestId("engine-control-chevron-btn"));
expect(screen.getByTestId("engine-control-pause-triage-btn")).toBeDefined();
});
it("calls onToggleEnginePause when pause button is clicked", () => {
it("calls onToggleEnginePause when pause triage item is clicked", () => {
const onToggleEnginePause = vi.fn();
renderHeader({ onToggleEnginePause });
fireEvent.click(screen.getByTitle("Pause scheduling"));
fireEvent.click(screen.getByTestId("engine-control-chevron-btn"));
fireEvent.click(screen.getByTestId("engine-control-pause-triage-btn"));
expect(onToggleEnginePause).toHaveBeenCalled();
});
it("calls onToggleGlobalPause when stop button is clicked", () => {
it("calls onToggleGlobalPause when main button is clicked", () => {
const onToggleGlobalPause = vi.fn();
renderHeader({ onToggleGlobalPause });
fireEvent.click(screen.getByTitle("Stop AI engine"));
fireEvent.click(screen.getByTestId("engine-control-main-btn"));
expect(onToggleGlobalPause).toHaveBeenCalled();
});
it("shows resume text when engine is paused", () => {
it("shows resume text in dropdown when engine is paused", () => {
renderHeader({ enginePaused: true });
fireEvent.click(screen.getByTestId("engine-control-chevron-btn"));
expect(screen.getByTitle("Resume scheduling")).toBeDefined();
});
it("shows start text when global is paused", () => {
it("shows start AI engine title on main button when global is paused", () => {
renderHeader({ globalPaused: true });
expect(screen.getByTitle("Start AI engine")).toBeDefined();
});
@@ -1171,25 +1175,27 @@ describe("Header", () => {
onRunScript: noop,
}, "desktop");
// Get all inline btn-icon buttons inside header-actions
// Get direct children of header-actions: top-level btn-icon buttons AND the split-button container
const headerActions = container.querySelector(".header-actions")!;
const inlineButtons = Array.from(headerActions.querySelectorAll<HTMLButtonElement>(":scope > button.btn-icon"));
const inlineItems = Array.from(
headerActions.querySelectorAll<HTMLElement>(
":scope > button.btn-icon, :scope > .engine-control-split-btn"
)
);
// Find the Settings button index and the Pause/Stop button indices
const settingsIdx = inlineButtons.findIndex((btn) => btn.title === "Settings");
const pauseIdx = inlineButtons.findIndex((btn) => btn.title === "Pause scheduling" || btn.title === "Resume scheduling");
const stopIdx = inlineButtons.findIndex((btn) => btn.title === "Stop AI engine" || btn.title === "Start AI engine");
const settingsIdx = inlineItems.findIndex(
(el) => el instanceof HTMLButtonElement && el.title === "Settings"
);
const splitBtnIdx = inlineItems.findIndex((el) =>
el.classList.contains("engine-control-split-btn")
);
// Settings must exist
expect(settingsIdx).toBeGreaterThanOrEqual(0);
expect(splitBtnIdx).toBeGreaterThanOrEqual(0);
expect(settingsIdx).toBeGreaterThan(splitBtnIdx);
// Settings must come after pause and stop (engine controls come before Settings)
expect(settingsIdx).toBeGreaterThan(pauseIdx);
expect(settingsIdx).toBeGreaterThan(stopIdx);
// Settings must be the very last button — no buttons after it
const buttonsAfterSettings = inlineButtons.slice(settingsIdx + 1);
expect(buttonsAfterSettings).toHaveLength(0);
const itemsAfterSettings = inlineItems.slice(settingsIdx + 1);
expect(itemsAfterSettings).toHaveLength(0);
});
it("Settings is the last item in the mobile overflow menu", () => {