FN-7036: make Automations popup movable and resizable
Render the Automations modal in the shared floating-window shell while preserving embedded and mobile behavior. - Wrap the modal presentation in FloatingWindow with persisted geometry, drag handle, and resize sizing. - Update Automations modal CSS so desktop fills the floating shell and mobile remains full-screen with resize handles hidden. - Cover floating-window rendering, dragging, resizing, mobile CSS contract, and embedded mode in ScheduledTasksModal tests. - Add a minor changeset for the published Fusion package. Files changed: .changeset/fn-7036-automation-modal-floating.md | 7 ++ .../app/components/ScheduledTasksModal.tsx | 43 +++++---- packages/dashboard/app/components/ScriptsModal.css | 78 +++++++++------- .../__tests__/ScheduledTasksModal.test.tsx | 103 +++++++++++++++++++-- 4 files changed, 172 insertions(+), 59 deletions(-) Fusion-Task-Id: FN-7036 Fusion-Task-Lineage: 3d6926b7-ec6d-4179-ad17-573bcfef297f
This commit is contained in:
7
.changeset/fn-7036-automation-modal-floating.md
Normal file
7
.changeset/fn-7036-automation-modal-floating.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Automations popup is now movable and resizable like other Fusion pop-outs.
|
||||||
|
category: feature
|
||||||
|
dev: ScheduledTasksModal modal presentation now renders inside the shared FloatingWindow (windowKey "automation", persistGeometryKey "floating-window:automation"); embedded presentation unchanged. Mobile stays full-screen by CSS.
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// ScheduledTasksModal renders schedule/routine cards using .scheduling-*, .routine-*,
|
// ScheduledTasksModal renders schedule/routine cards using .scheduling-*, .routine-*,
|
||||||
// .schedule-form classes that live in ScriptsModal.css. Both modals share that file.
|
// .schedule-form classes that live in ScriptsModal.css. Both modals share that file.
|
||||||
import "./ScriptsModal.css";
|
import "./ScriptsModal.css";
|
||||||
import { useState, useEffect, useCallback, useMemo, useRef } from "react";
|
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Plus, Zap, Globe, Folder, X } from "lucide-react";
|
import { Plus, Zap, Globe, Folder, X } from "lucide-react";
|
||||||
import type { Routine, RoutineCreateInput } from "@fusion/core";
|
import type { Routine, RoutineCreateInput } from "@fusion/core";
|
||||||
@@ -16,9 +16,8 @@ import {
|
|||||||
import { RoutineCard } from "./RoutineCard";
|
import { RoutineCard } from "./RoutineCard";
|
||||||
import { RoutineEditor } from "./RoutineEditor";
|
import { RoutineEditor } from "./RoutineEditor";
|
||||||
import type { ToastType } from "../hooks/useToast";
|
import type { ToastType } from "../hooks/useToast";
|
||||||
import { useModalResizePersist } from "../hooks/useModalResizePersist";
|
|
||||||
import { useOverlayDismiss } from "../hooks/useOverlayDismiss";
|
|
||||||
import { useEmbeddedPresentation, type ModalPresentation } from "../hooks/useEmbeddedPresentation";
|
import { useEmbeddedPresentation, type ModalPresentation } from "../hooks/useEmbeddedPresentation";
|
||||||
|
import { FloatingWindow } from "./FloatingWindow";
|
||||||
|
|
||||||
/** Polling interval for auto-refreshing the schedule/routine list (30 seconds). */
|
/** Polling interval for auto-refreshing the schedule/routine list (30 seconds). */
|
||||||
const POLL_INTERVAL_MS = 30_000;
|
const POLL_INTERVAL_MS = 30_000;
|
||||||
@@ -28,11 +27,11 @@ export type SchedulingScope = "global" | "project";
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* FNXC:AutomationsEmbedded 2026-06-22-00:00:
|
* FNXC:AutomationsEmbedded 2026-06-22-00:00:
|
||||||
* Automations can render either as a fixed modal overlay ("modal", the default and historical path) or inline
|
* Automations can render either as a draggable/resizable floating modal ("modal", the default path) or inline
|
||||||
* as a main-content-area view ("embedded"). The embedded presentation fills the main panel like Command Center:
|
* as a main-content-area view ("embedded"). The embedded presentation fills the main panel like Command Center:
|
||||||
* no overlay, no card/shadow/border chrome, a plain `.cc-header`-style title row, and a responsive two-pane
|
* no overlay, no card/shadow/border chrome, a plain `.cc-header`-style title row, and a responsive two-pane
|
||||||
* body (list + detail) that collapses to a single column below ~900px. The modal path is kept byte-identical;
|
* body (list + detail) that collapses to a single column below ~900px. Floating chrome and Escape-to-close are
|
||||||
* modal-only behaviors (scroll lock via resize-persist, escape-to-close, overlay dismiss) are disabled when embedded.
|
* modal-only behaviors; the embedded presentation bypasses FloatingWindow entirely.
|
||||||
*/
|
*/
|
||||||
interface ScheduledTasksModalProps {
|
interface ScheduledTasksModalProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -45,7 +44,7 @@ interface ScheduledTasksModalProps {
|
|||||||
|
|
||||||
export function ScheduledTasksModal({ onClose, addToast, projectId, presentation = "modal" }: ScheduledTasksModalProps) {
|
export function ScheduledTasksModal({ onClose, addToast, projectId, presentation = "modal" }: ScheduledTasksModalProps) {
|
||||||
const { t } = useTranslation("app");
|
const { t } = useTranslation("app");
|
||||||
const { isEmbedded, resizePersistEnabled, escapeEnabled } = useEmbeddedPresentation(presentation);
|
const { isEmbedded, escapeEnabled } = useEmbeddedPresentation(presentation);
|
||||||
// Scope state: defaults to "project" when projectId exists, else "global"
|
// Scope state: defaults to "project" when projectId exists, else "global"
|
||||||
const [activeScope, setActiveScope] = useState<SchedulingScope>(() => projectId ? "project" : "global");
|
const [activeScope, setActiveScope] = useState<SchedulingScope>(() => projectId ? "project" : "global");
|
||||||
|
|
||||||
@@ -58,10 +57,6 @@ export function ScheduledTasksModal({ onClose, addToast, projectId, presentation
|
|||||||
// FNXC:AutomationsEmbedded 2026-06-22-00:00: Two-pane embedded layout tracks the routine selected in the left list to render its detail on the right.
|
// FNXC:AutomationsEmbedded 2026-06-22-00:00: Two-pane embedded layout tracks the routine selected in the left list to render its detail on the right.
|
||||||
const [selectedRoutineId, setSelectedRoutineId] = useState<string | null>(null);
|
const [selectedRoutineId, setSelectedRoutineId] = useState<string | null>(null);
|
||||||
|
|
||||||
const modalRef = useRef<HTMLDivElement>(null);
|
|
||||||
// Resize-persist is a modal-only affordance; the embedded view fills its host and never resizes.
|
|
||||||
useModalResizePersist(modalRef, resizePersistEnabled, "fusion:automation-modal-size");
|
|
||||||
|
|
||||||
// Build scope options for API calls
|
// Build scope options for API calls
|
||||||
const scopeOptions = useMemo(() => ({
|
const scopeOptions = useMemo(() => ({
|
||||||
scope: activeScope,
|
scope: activeScope,
|
||||||
@@ -124,8 +119,6 @@ export function ScheduledTasksModal({ onClose, addToast, projectId, presentation
|
|||||||
return () => document.removeEventListener("keydown", handleKey);
|
return () => document.removeEventListener("keydown", handleKey);
|
||||||
}, [onClose, routineView, escapeEnabled]);
|
}, [onClose, routineView, escapeEnabled]);
|
||||||
|
|
||||||
const overlayDismissProps = useOverlayDismiss(onClose);
|
|
||||||
|
|
||||||
// ── Routine CRUD handlers ───────────────────────────────────────────────
|
// ── Routine CRUD handlers ───────────────────────────────────────────────
|
||||||
|
|
||||||
const handleCreateRoutine = useCallback(
|
const handleCreateRoutine = useCallback(
|
||||||
@@ -440,11 +433,25 @@ export function ScheduledTasksModal({ onClose, addToast, projectId, presentation
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Modal (fixed overlay) presentation ──────────────────────────────────
|
// ── Modal (floating window) presentation ────────────────────────────────
|
||||||
return (
|
return (
|
||||||
<div className="modal-overlay open" {...overlayDismissProps}>
|
<FloatingWindow
|
||||||
<div ref={modalRef} className="modal modal-lg automation-modal" role="dialog" aria-modal="true" aria-labelledby="schedules-modal-title">
|
windowKey="automation"
|
||||||
<div className="modal-header">
|
title={t("schedule.title", "Automations")}
|
||||||
|
onClose={onClose}
|
||||||
|
hideHeader
|
||||||
|
dragHandleSelector=".automation-modal__drag-handle"
|
||||||
|
className="floating-window--automation"
|
||||||
|
defaultSize={{ width: 720, height: 640 }}
|
||||||
|
minSize={{ width: 420, height: 360 }}
|
||||||
|
persistGeometryKey="floating-window:automation"
|
||||||
|
>
|
||||||
|
{/**
|
||||||
|
* FNXC:Automations 2026-06-26-00:00:
|
||||||
|
* FN-7036 moves the desktop Automations popup into the shared FloatingWindow shell so it matches Plan Mission and Workflow editor drag, resize, stack, clamp, and geometry-persistence behavior. Mobile remains full-screen through the ScriptsModal.css floating-window contract, while the embedded main-content presentation above bypasses all FloatingWindow chrome.
|
||||||
|
*/}
|
||||||
|
<div className="modal modal-lg automation-modal" role="dialog" aria-modal="true" aria-labelledby="schedules-modal-title">
|
||||||
|
<div className="modal-header automation-modal__drag-handle">
|
||||||
<div className="detail-title-row">
|
<div className="detail-title-row">
|
||||||
<Zap size={20} className="icon-triage" />
|
<Zap size={20} className="icon-triage" />
|
||||||
<h3 id="schedules-modal-title">{t("schedule.title", "Automations")}</h3>
|
<h3 id="schedules-modal-title">{t("schedule.title", "Automations")}</h3>
|
||||||
@@ -460,6 +467,6 @@ export function ScheduledTasksModal({ onClose, addToast, projectId, presentation
|
|||||||
{renderContent()}
|
{renderContent()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</FloatingWindow>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,14 +103,35 @@ The Automations toolbar should match Artifacts' controls row: a plain body row w
|
|||||||
|
|
||||||
/* === Automation (ScheduledTasksModal) === */
|
/* === Automation (ScheduledTasksModal) === */
|
||||||
.modal.automation-modal {
|
.modal.automation-modal {
|
||||||
width: min(95vw, 720px);
|
|
||||||
max-width: 95vw;
|
|
||||||
min-width: 0;
|
|
||||||
height: 80vh;
|
|
||||||
min-height: calc(var(--space-2xl) * 15);
|
|
||||||
max-height: calc(100dvh - var(--overlay-padding-top, 10vh) - var(--space-lg));
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
resize: both;
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:Automations 2026-06-26-00:00:
|
||||||
|
FN-7036 hosts the Automations popup in FloatingWindow on desktop, so the inner modal fills the movable/resizable panel instead of owning fixed overlay dimensions or CSS resize. Mobile uses the same floating shell but forces a full-screen panel and hides resize handles; the embedded Automations view keeps its separate .automations-embedded path.
|
||||||
|
*/
|
||||||
|
.floating-window--automation .floating-window__body {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-window--automation .modal.automation-modal {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-width: none;
|
||||||
|
max-height: none;
|
||||||
|
border: 0;
|
||||||
|
border-radius: inherit;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-window--automation .automation-modal__drag-handle {
|
||||||
|
cursor: grab;
|
||||||
|
user-select: none;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-window--automation .automation-modal__drag-handle:active {
|
||||||
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
.schedule-modal-content {
|
.schedule-modal-content {
|
||||||
@@ -1210,8 +1231,8 @@ The activity-log embedded (.activity-log-embedded / .activity-log-modal--embedde
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
FNXC:AutomationsEmbedded 2026-06-22-00:00:
|
FNXC:AutomationsEmbedded 2026-06-22-00:00:
|
||||||
Automations can render inline in the main content area (presentation="embedded") instead of as a fixed modal overlay.
|
Automations can render inline in the main content area (presentation="embedded") instead of as a floating modal popup.
|
||||||
The embedded root fills its host and sheds all modal chrome — no overlay, no card/shadow/border/radius — so the view
|
The embedded root fills its host and sheds all modal chrome — no FloatingWindow, overlay, card/shadow/border/radius — so the view
|
||||||
blends into the main panel like Command Center. The view container carries --space-lg padding and a plain .cc-header
|
blends into the main panel like Command Center. The view container carries --space-lg padding and a plain .cc-header
|
||||||
title row (reused from Command Center). The body is a responsive two-pane layout (list + detail) via container query
|
title row (reused from Command Center). The body is a responsive two-pane layout (list + detail) via container query
|
||||||
when supported, falling back to a min-width media breakpoint, that collapses to a single column below ~900px.
|
when supported, falling back to a min-width media breakpoint, that collapses to a single column below ~900px.
|
||||||
@@ -4302,32 +4323,25 @@ Refresh button pinned at the end of the section nav strip (replaces the removed
|
|||||||
will-change: transform;
|
will-change: transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Same treatment for the Automations modal — same min-width: 480px would
|
.floating-window--automation {
|
||||||
otherwise force it wider than narrow viewports.
|
inset: 0 !important;
|
||||||
FNXC:Automations 2026-06-22-16:00: scope the viewport-takeover to the
|
width: 100vw !important;
|
||||||
dialog presentation only. The embedded Automations view uses the distinct
|
height: 100dvh !important;
|
||||||
.automations-embedded / .automations-embedded-view classes (it does NOT
|
min-width: 100vw !important;
|
||||||
carry .automation-modal), so it is unaffected here — but the guard keeps
|
min-height: 100dvh !important;
|
||||||
this rule from ever leaking onto an embedded variant should the markup
|
max-width: 100vw !important;
|
||||||
share the base class later. The embedded view fills its pane and scrolls
|
max-height: 100dvh !important;
|
||||||
via .automations-embedded-view (inline-size:100% + overflow-y:auto). */
|
border: 0;
|
||||||
.modal-overlay:has(.automation-modal:not(.automation-modal--embedded)) {
|
border-radius: 0;
|
||||||
padding-top: 0;
|
box-shadow: none;
|
||||||
align-items: stretch;
|
|
||||||
justify-content: stretch;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal.automation-modal:not(.automation-modal--embedded) {
|
.floating-window--automation .floating-window__resize-handle {
|
||||||
width: 100vw;
|
display: none;
|
||||||
min-width: 0;
|
}
|
||||||
max-width: 100vw;
|
|
||||||
height: 100dvh;
|
.floating-window--automation .modal.automation-modal {
|
||||||
min-height: 0;
|
|
||||||
max-height: 100dvh;
|
|
||||||
margin: 0;
|
|
||||||
border: none;
|
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
resize: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.gm-layout {
|
.gm-layout {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
import { act, render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||||
import { readFileSync } from "node:fs";
|
import { readFileSync } from "node:fs";
|
||||||
import { resolve } from "node:path";
|
import { resolve } from "node:path";
|
||||||
import { ScheduledTasksModal } from "../ScheduledTasksModal";
|
import { ScheduledTasksModal } from "../ScheduledTasksModal";
|
||||||
@@ -86,6 +86,16 @@ vi.mock("../CustomModelDropdown", () => ({
|
|||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
function setViewport(width: number, height: number) {
|
||||||
|
Object.defineProperty(window, "innerWidth", { configurable: true, value: width });
|
||||||
|
Object.defineProperty(window, "innerHeight", { configurable: true, value: height });
|
||||||
|
}
|
||||||
|
|
||||||
|
function stubPointerCapture(element: HTMLElement) {
|
||||||
|
Object.defineProperty(element, "setPointerCapture", { configurable: true, value: vi.fn() });
|
||||||
|
Object.defineProperty(element, "releasePointerCapture", { configurable: true, value: vi.fn() });
|
||||||
|
}
|
||||||
|
|
||||||
function makeRoutine(overrides: Partial<Routine> = {}): Routine {
|
function makeRoutine(overrides: Partial<Routine> = {}): Routine {
|
||||||
return {
|
return {
|
||||||
id: "routine-001",
|
id: "routine-001",
|
||||||
@@ -114,13 +124,17 @@ describe("ScheduledTasksModal", () => {
|
|||||||
mockConfirm.mockResolvedValue(true);
|
mockConfirm.mockResolvedValue(true);
|
||||||
mockFetchAutomations.mockResolvedValue([]);
|
mockFetchAutomations.mockResolvedValue([]);
|
||||||
mockFetchRoutines.mockResolvedValue([]);
|
mockFetchRoutines.mockResolvedValue([]);
|
||||||
|
localStorage.removeItem("floating-window:automation");
|
||||||
|
localStorage.removeItem("fusion:automation-modal-size");
|
||||||
|
setViewport(1200, 900);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders the unified automations modal", async () => {
|
it("renders the unified automations modal", async () => {
|
||||||
render(<ScheduledTasksModal onClose={onClose} addToast={addToast} />);
|
render(<ScheduledTasksModal onClose={onClose} addToast={addToast} />);
|
||||||
|
|
||||||
expect(screen.getByText("Automations")).toBeDefined();
|
expect(screen.getByText("Automations")).toBeDefined();
|
||||||
expect(screen.getByRole("dialog").getAttribute("aria-labelledby")).toBe("schedules-modal-title");
|
const dialogs = screen.getAllByRole("dialog");
|
||||||
|
expect(dialogs.some((dialog) => dialog.getAttribute("aria-labelledby") === "schedules-modal-title")).toBe(true);
|
||||||
expect(screen.getByRole("button", { name: "Close" })).toBeDefined();
|
expect(screen.getByRole("button", { name: "Close" })).toBeDefined();
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText("No automations yet")).toBeDefined();
|
expect(screen.getByText("No automations yet")).toBeDefined();
|
||||||
@@ -130,6 +144,76 @@ describe("ScheduledTasksModal", () => {
|
|||||||
expect(mockFetchAutomations).not.toHaveBeenCalled();
|
expect(mockFetchAutomations).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders Automations inside a headerless floating window with default geometry", async () => {
|
||||||
|
render(<ScheduledTasksModal onClose={onClose} addToast={addToast} />);
|
||||||
|
|
||||||
|
const panel = screen.getByTestId("floating-window-automation");
|
||||||
|
expect(panel).toHaveClass("floating-window--automation");
|
||||||
|
expect(panel).toHaveClass("floating-window--headerless");
|
||||||
|
expect(panel.style.width).toBe("720px");
|
||||||
|
expect(panel.style.height).toBe("640px");
|
||||||
|
expect(screen.queryByTestId("floating-window-drag-handle-automation")).toBeNull();
|
||||||
|
expect(screen.getAllByRole("button", { name: "Close" })).toHaveLength(1);
|
||||||
|
|
||||||
|
const title = screen.getByText("Automations");
|
||||||
|
expect(title.closest(".automation-modal__drag-handle")).toBeTruthy();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("No automations yet")).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("drags and resizes the desktop Automations floating window", async () => {
|
||||||
|
render(<ScheduledTasksModal onClose={onClose} addToast={addToast} />);
|
||||||
|
|
||||||
|
const panel = screen.getByTestId("floating-window-automation");
|
||||||
|
const header = screen.getByText("Automations").closest(".automation-modal__drag-handle") as HTMLElement;
|
||||||
|
stubPointerCapture(panel);
|
||||||
|
|
||||||
|
const initialLeft = Number.parseFloat(panel.style.left);
|
||||||
|
const initialTop = Number.parseFloat(panel.style.top);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
fireEvent.pointerDown(header, { pointerId: 11, clientX: 120, clientY: 80 });
|
||||||
|
fireEvent.pointerMove(panel, { pointerId: 11, clientX: 220, clientY: 140 });
|
||||||
|
fireEvent.pointerUp(panel, { pointerId: 11, clientX: 220, clientY: 140 });
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(Number.parseFloat(panel.style.left)).toBeGreaterThan(initialLeft);
|
||||||
|
expect(Number.parseFloat(panel.style.top)).toBeGreaterThan(initialTop);
|
||||||
|
});
|
||||||
|
|
||||||
|
const resizeHandle = screen.getByTestId("floating-window-resize-se") as HTMLElement;
|
||||||
|
stubPointerCapture(resizeHandle);
|
||||||
|
const widthAfterDrag = Number.parseFloat(panel.style.width);
|
||||||
|
const heightAfterDrag = Number.parseFloat(panel.style.height);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
fireEvent.pointerDown(resizeHandle, { pointerId: 12, clientX: 700, clientY: 600 });
|
||||||
|
fireEvent.pointerMove(resizeHandle, { pointerId: 12, clientX: 760, clientY: 650 });
|
||||||
|
fireEvent.pointerUp(resizeHandle, { pointerId: 12, clientX: 760, clientY: 650 });
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(Number.parseFloat(panel.style.width)).toBeGreaterThan(widthAfterDrag);
|
||||||
|
expect(Number.parseFloat(panel.style.height)).toBeGreaterThan(heightAfterDrag);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps mobile Automations full-screen and hides resize handles by CSS contract", () => {
|
||||||
|
const source = readFileSync(resolve(__dirname, "../ScriptsModal.css"), "utf8");
|
||||||
|
const mobileBlock = source
|
||||||
|
.match(/@media \(max-width: 768px\)\s*\{[\s\S]*?\n\}/g)
|
||||||
|
?.find((block) => block.includes(".floating-window--automation")) ?? "";
|
||||||
|
|
||||||
|
expect(mobileBlock).toContain(".floating-window--automation");
|
||||||
|
expect(mobileBlock).toContain("width: 100vw !important;");
|
||||||
|
expect(mobileBlock).toContain("height: 100dvh !important;");
|
||||||
|
expect(mobileBlock).toContain(".floating-window--automation .floating-window__resize-handle");
|
||||||
|
expect(mobileBlock).toContain("display: none;");
|
||||||
|
});
|
||||||
|
|
||||||
it("shows routine cards and the new automation button when routines exist", async () => {
|
it("shows routine cards and the new automation button when routines exist", async () => {
|
||||||
mockFetchRoutines.mockResolvedValue([
|
mockFetchRoutines.mockResolvedValue([
|
||||||
makeRoutine({ name: "Database Backup", command: "npx runfusion.ai backup --create" }),
|
makeRoutine({ name: "Database Backup", command: "npx runfusion.ai backup --create" }),
|
||||||
@@ -146,17 +230,17 @@ describe("ScheduledTasksModal", () => {
|
|||||||
|
|
||||||
it("renders scope controls in the toolbar below the modal header", async () => {
|
it("renders scope controls in the toolbar below the modal header", async () => {
|
||||||
mockFetchRoutines.mockResolvedValue([makeRoutine({ name: "Scoped Routine" })]);
|
mockFetchRoutines.mockResolvedValue([makeRoutine({ name: "Scoped Routine" })]);
|
||||||
const { container } = render(<ScheduledTasksModal onClose={onClose} addToast={addToast} />);
|
render(<ScheduledTasksModal onClose={onClose} addToast={addToast} />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText("Scoped Routine")).toBeDefined();
|
expect(screen.getByText("Scoped Routine")).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
const header = container.querySelector(".modal-header");
|
const header = document.querySelector(".modal-header");
|
||||||
const toolbar = container.querySelector(".scheduling-toolbar");
|
const toolbar = document.querySelector(".scheduling-toolbar");
|
||||||
const toolbarLeft = container.querySelector(".scheduling-toolbar-left");
|
const toolbarLeft = document.querySelector(".scheduling-toolbar-left");
|
||||||
const toolbarRight = container.querySelector(".scheduling-toolbar-right");
|
const toolbarRight = document.querySelector(".scheduling-toolbar-right");
|
||||||
const scopeSelector = container.querySelector(".scheduling-scope-selector");
|
const scopeSelector = document.querySelector(".scheduling-scope-selector");
|
||||||
const newAutomationButton = screen.getByRole("button", { name: /new automation/i });
|
const newAutomationButton = screen.getByRole("button", { name: /new automation/i });
|
||||||
|
|
||||||
expect(header).toBeTruthy();
|
expect(header).toBeTruthy();
|
||||||
@@ -386,7 +470,8 @@ describe("ScheduledTasksModal", () => {
|
|||||||
});
|
});
|
||||||
expect(screen.getByText("Automations")).toBeDefined();
|
expect(screen.getByText("Automations")).toBeDefined();
|
||||||
expect(container.querySelector(".automations-embedded")).not.toBeNull();
|
expect(container.querySelector(".automations-embedded")).not.toBeNull();
|
||||||
// No fixed overlay backdrop, no dialog role, no modal close button in embedded mode.
|
// No floating window, fixed overlay backdrop, dialog role, or modal close button in embedded mode.
|
||||||
|
expect(screen.queryByTestId("floating-window-automation")).toBeNull();
|
||||||
expect(container.querySelector(".modal-overlay")).toBeNull();
|
expect(container.querySelector(".modal-overlay")).toBeNull();
|
||||||
expect(screen.queryByRole("dialog")).toBeNull();
|
expect(screen.queryByRole("dialog")).toBeNull();
|
||||||
expect(screen.queryByRole("button", { name: "Close" })).toBeNull();
|
expect(screen.queryByRole("button", { name: "Close" })).toBeNull();
|
||||||
|
|||||||
Reference in New Issue
Block a user