FN-8806: stabilize task title expansion during resize
Keep expanded task titles stable while their container resizes. - Measure persistent title text separately from the interactive expansion control. - Fence hidden pop-out resize observers and preserve expansion state across detail hosts. - Add rendering and browser coverage for resize-driven title behavior. Files changed: .changeset/fn-8806-task-title-flicker.md | 7 + .../dashboard/app/components/TaskDetailModal.css | 16 +- .../dashboard/app/components/TaskDetailModal.tsx | 37 +-- .../__tests__/TaskDetailModal.rendering.test.tsx | 42 +++- .../app/task-modal-touch-resize-e2e-fixture.tsx | 149 +++++++++++- .../task-modal-touch-resize-browser.test.ts | 267 ++++++++++++++++++++- 6 files changed, 485 insertions(+), 33 deletions(-) Fusion-Task-Id: FN-8806 Fusion-Task-Lineage: 9519a5dc-ae6d-43e1-a0da-7ac963f27d7f Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8806-task-title-flicker.md
Normal file
7
.changeset/fn-8806-task-title-flicker.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Keep expanded task titles stable without flickering during resize.
|
||||
category: fix
|
||||
dev: Measures the persistent title text box so its control cannot alter overflow eligibility.
|
||||
@@ -205,9 +205,19 @@ the previous direct-child flex contract so the transcript and agent log still fi
|
||||
margin-bottom: var(--space-md);
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.detail-title--collapsed {
|
||||
/*
|
||||
FNXC:TaskDetailTitle 2026-08-05-17:54:
|
||||
The measured text stays in flow while the interactive overlay changes state, so eligibility has
|
||||
one geometry in every task-detail host instead of depending on whether its button is present.
|
||||
*/
|
||||
.detail-title-measurement {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.detail-title--collapsed .detail-title-measurement {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
@@ -236,10 +246,12 @@ Summarize-as-title is an in-field affordance, not a separate full-width row: it
|
||||
.detail-title-control {
|
||||
display: block;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
color: transparent;
|
||||
font: inherit;
|
||||
text-align: inherit;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -1207,17 +1207,18 @@ export function TaskDetailContent({
|
||||
const [highlightStallCode, setHighlightStallCode] = useState<string | null>(null);
|
||||
const [descriptionExpanded, setDescriptionExpanded] = useState(false);
|
||||
const [titleOverflows, setTitleOverflows] = useState(false);
|
||||
const titleRef = useRef<HTMLHeadingElement | null>(null);
|
||||
const titleRef = useRef<HTMLSpanElement | null>(null);
|
||||
const displayTitleText = task.title || task.description || task.id;
|
||||
|
||||
/*
|
||||
FNXC:TaskDetailTitle 2026-08-05-16:42:
|
||||
Title overflow eligibility belongs to the stable two-line collapsed layout, while expanded or
|
||||
collapsed display is an explicit operator choice. Do not mutate the live expanded heading to
|
||||
measure it: ResizeObserver callbacks caused by that class change can otherwise replace the
|
||||
title-owned control and flicker the detail view. Reset both values before paint only when this
|
||||
task identity or its displayed title/fallback changes; ordinary rerenders and resize callbacks
|
||||
must never reverse the choice.
|
||||
FNXC:TaskDetailTitle 2026-08-05-18:48:
|
||||
Browser layout proved that swapping bare heading text for the semantic title button can alter the
|
||||
exact box whose overflow decides whether that button exists. Measure an always-present text span
|
||||
with the collapsed two-line rules instead; the button is an out-of-flow accessible overlay and
|
||||
cannot feed back into eligibility. Expansion remains an operator-owned state across modal,
|
||||
full-panel, split-pane, right-dock, and floating-window hosts. Reset only for a new task or its
|
||||
title/description/id fallback, never from a resize delivery. Kept-alive hidden pop-outs are not a
|
||||
live layout authority: disconnect and fence their callbacks until their host is visible again.
|
||||
*/
|
||||
useLayoutEffect(() => {
|
||||
setDescriptionExpanded(false);
|
||||
@@ -1281,6 +1282,10 @@ export function TaskDetailContent({
|
||||
const [prCreateOpen, setPrCreateOpen] = useState(false);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
// A kept-alive floating detail can be hidden while another view owns its layout. Its stale
|
||||
// ResizeObserver delivery must not change eligibility before the host becomes visible again.
|
||||
if (!active) return;
|
||||
|
||||
const titleElement = titleRef.current;
|
||||
if (!titleElement) {
|
||||
setTitleOverflows(false);
|
||||
@@ -1311,7 +1316,7 @@ export function TaskDetailContent({
|
||||
resizeObserver?.disconnect();
|
||||
window.removeEventListener("resize", measureTitleOverflow);
|
||||
};
|
||||
}, [descriptionExpanded, displayTitleText, task.id]);
|
||||
}, [active, descriptionExpanded, displayTitleText, task.id]);
|
||||
|
||||
/*
|
||||
FNXC:WorkflowBadges 2026-06-29-00:00:
|
||||
@@ -4797,10 +4802,10 @@ export function TaskDetailContent({
|
||||
FNXC:TaskDetailTitle 2026-08-04-18:00:
|
||||
An overflowing task-detail title is its own sole expansion control. Keep the semantic button inside the h2 so pointer, touch, and keyboard activation share one accessible target; the separate Show more/Show less row must not return.
|
||||
*/}
|
||||
<h2
|
||||
ref={titleRef}
|
||||
className={`detail-title${descriptionExpanded ? "" : " detail-title--collapsed"}`}
|
||||
>
|
||||
<h2 className={`detail-title${descriptionExpanded ? "" : " detail-title--collapsed"}`}>
|
||||
<span ref={titleRef} className="detail-title-measurement">
|
||||
{displayTitleText}
|
||||
</span>
|
||||
{titleOverflows || descriptionExpanded ? (
|
||||
<button
|
||||
type="button"
|
||||
@@ -4810,10 +4815,8 @@ export function TaskDetailContent({
|
||||
? t("taskDetail.title.collapse", "Collapse task title")
|
||||
: t("taskDetail.title.expand", "Expand task title")}
|
||||
onClick={() => setDescriptionExpanded((expanded) => !expanded)}
|
||||
>
|
||||
{displayTitleText}
|
||||
</button>
|
||||
) : displayTitleText}
|
||||
/>
|
||||
) : null}
|
||||
</h2>
|
||||
{showSummarizeTitleButton && (
|
||||
<button
|
||||
|
||||
@@ -2334,13 +2334,13 @@ describe("TaskDetailModal", () => {
|
||||
Object.defineProperty(HTMLElement.prototype, "scrollHeight", {
|
||||
configurable: true,
|
||||
get() {
|
||||
return this instanceof HTMLElement && this.classList.contains("detail-title") ? titleScrollHeight : 0;
|
||||
return this instanceof HTMLElement && this.classList.contains("detail-title-measurement") ? titleScrollHeight : 0;
|
||||
},
|
||||
});
|
||||
Object.defineProperty(HTMLElement.prototype, "clientHeight", {
|
||||
configurable: true,
|
||||
get() {
|
||||
return this instanceof HTMLElement && this.classList.contains("detail-title") ? titleClientHeight : 0;
|
||||
return this instanceof HTMLElement && this.classList.contains("detail-title-measurement") ? titleClientHeight : 0;
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -2401,13 +2401,16 @@ describe("TaskDetailModal", () => {
|
||||
renderDetail({ title: longTitle });
|
||||
|
||||
const titleControl = await screen.findByRole("button", { name: "Expand task title" });
|
||||
const measuredText = document.querySelector(".detail-title-measurement");
|
||||
const collapsedObserver = titleResizeObservers.at(-1);
|
||||
expect(collapsedObserver).toBeDefined();
|
||||
expect(measuredText?.textContent).toBe(longTitle);
|
||||
|
||||
await userEvent.click(titleControl);
|
||||
expect(document.querySelector("h2.detail-title")).not.toHaveClass("detail-title--collapsed");
|
||||
expect(screen.getByRole("button", { name: "Collapse task title" })).toBe(titleControl);
|
||||
expect(titleControl).toHaveAttribute("aria-expanded", "true");
|
||||
expect(document.querySelector(".detail-title-measurement")).toBe(measuredText);
|
||||
expect(document.querySelector("h2.detail-title")?.textContent).toBe(longTitle);
|
||||
expect(collapsedObserver?.disconnected).toBe(true);
|
||||
|
||||
@@ -2424,6 +2427,7 @@ describe("TaskDetailModal", () => {
|
||||
await userEvent.click(titleControl);
|
||||
const recollapsedObserver = titleResizeObservers.at(-1);
|
||||
expect(document.querySelector("h2.detail-title")).toHaveClass("detail-title--collapsed");
|
||||
expect(document.querySelector(".detail-title-measurement")).toBe(measuredText);
|
||||
expect(screen.getByRole("button", { name: "Expand task title" })).toBe(titleControl);
|
||||
expect(titleControl).toHaveAttribute("aria-expanded", "false");
|
||||
expect(recollapsedObserver).not.toBe(collapsedObserver);
|
||||
@@ -2483,6 +2487,36 @@ describe("TaskDetailModal", () => {
|
||||
Object.defineProperty(window, "innerWidth", { configurable: true, value: originalInnerWidth });
|
||||
});
|
||||
|
||||
it("ignores title observer deliveries while a kept-alive pop-out is hidden", async () => {
|
||||
setTitleLayout({ scrollHeight: 40, clientHeight: 40 });
|
||||
const props = {
|
||||
embedded: true,
|
||||
active: true,
|
||||
initialTab: "definition" as const,
|
||||
onMoveTask: noopMove,
|
||||
onDeleteTask: noopDelete,
|
||||
onMergeTask: noopMerge,
|
||||
onOpenDetail: noopOpenDetail,
|
||||
addToast: noop,
|
||||
};
|
||||
const { rerender } = render(<TaskDetailContent {...props} task={makeTask({ id: "FN-HIDDEN", title: "Visibility fenced title" })} />);
|
||||
const visibleObserver = titleResizeObservers.at(-1);
|
||||
expect(screen.queryByRole("button", { name: /task title/ })).toBeNull();
|
||||
|
||||
rerender(<TaskDetailContent {...props} active={false} task={makeTask({ id: "FN-HIDDEN", title: "Visibility fenced title" })} />);
|
||||
expect(visibleObserver?.disconnected).toBe(true);
|
||||
setTitleLayout({ scrollHeight: 120, clientHeight: 40 });
|
||||
await act(async () => {
|
||||
visibleObserver?.callback([], {} as ResizeObserver);
|
||||
visibleObserver?.callback([], {} as ResizeObserver);
|
||||
});
|
||||
expect(screen.queryByRole("button", { name: /task title/ })).toBeNull();
|
||||
|
||||
rerender(<TaskDetailContent {...props} active task={makeTask({ id: "FN-HIDDEN", title: "Visibility fenced title" })} />);
|
||||
expect(await screen.findByRole("button", { name: "Expand task title" })).toHaveAttribute("aria-expanded", "false");
|
||||
expectNoStandaloneTitleToggle();
|
||||
});
|
||||
|
||||
it("supports keyboard activation through the title control", async () => {
|
||||
renderDetail({ title: "Keyboard title ".repeat(25) });
|
||||
|
||||
@@ -2667,8 +2701,8 @@ describe("TaskDetailModal", () => {
|
||||
it("has desktop and mobile CSS rules that preserve the two-line title clamp", () => {
|
||||
const css = readDashboardStylesSource();
|
||||
expect(css).toContain(".detail-title--collapsed");
|
||||
expectBaseRule(css, ".detail-title--collapsed", "-webkit-line-clamp: 2");
|
||||
expectBaseRule(css, ".detail-title--collapsed", "line-clamp: 2");
|
||||
expectBaseRule(css, ".detail-title--collapsed .detail-title-measurement", "-webkit-line-clamp: 2");
|
||||
expectBaseRule(css, ".detail-title--collapsed .detail-title-measurement", "line-clamp: 2");
|
||||
expectBaseRule(css, ".detail-title-control", "width: 100%");
|
||||
expectBaseRule(css, ".detail-title-control:focus-visible", "box-shadow: var(--focus-ring-strong)");
|
||||
expect(css).toContain("@media (max-width: 768px)");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState } from "react";
|
||||
import type { Task } from "@fusion/core";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import i18n from "i18next";
|
||||
import { I18nextProvider, initReactI18next } from "react-i18next";
|
||||
@@ -6,6 +7,13 @@ import "./styles.css";
|
||||
import "./components/TaskDetailModal.css";
|
||||
import "./components/FloatingWindow.css";
|
||||
import { FloatingWindow } from "./components/FloatingWindow";
|
||||
import { App } from "./App";
|
||||
import { TaskDetailContent } from "./components/TaskDetailModal";
|
||||
import { AppModals } from "./components/AppModals";
|
||||
import { MainContent } from "./components/dashboard/MainContent";
|
||||
import { ListView } from "./components/ListView";
|
||||
import { useRightDockController } from "./components/useRightDockController";
|
||||
import { NavigationHistoryProvider } from "./hooks/useNavigationHistory";
|
||||
import { NewTaskModal } from "./components/NewTaskModal";
|
||||
import { AgentListModal } from "./components/AgentListModal";
|
||||
import { SetupWizardModal } from "./components/SetupWizardModal";
|
||||
@@ -13,6 +21,7 @@ import { ConfirmDialogProvider } from "./hooks/useConfirm";
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const surface = params.get("surface") ?? "new-task";
|
||||
const titleMode = params.get("titleMode") ?? "overflow";
|
||||
if (params.has("reset")) localStorage.clear();
|
||||
|
||||
/*
|
||||
@@ -44,15 +53,141 @@ Their minimal API payloads keep first render deterministic so the CDP assertions
|
||||
geometry before each committed screenshot is captured.
|
||||
*/
|
||||
window.fetch = async (input) => {
|
||||
const url = String(input);
|
||||
const payload = url.includes("/models")
|
||||
? { models: [], favoriteProviders: [], favoriteModels: [] }
|
||||
: url.includes("/settings") ? {}
|
||||
: url.includes("/agents") || url.includes("/nodes") ? []
|
||||
: [];
|
||||
const url = input instanceof Request ? input.url : String(input);
|
||||
const pathname = new URL(url, window.location.href).pathname;
|
||||
const payload = url.includes("/projects/across-nodes")
|
||||
? [{ id: "fixture", name: "Fixture", path: "/fixture", status: "active" }]
|
||||
: url.includes("/tasks/board-workflows")
|
||||
? { flagEnabled: true, defaultWorkflowId: "fixture-workflow", taskWorkflowIds: { [fixtureTask.id]: "fixture-workflow" }, workflows: [{ id: "fixture-workflow", name: "Fixture", columns: [{ id: "todo", name: "Todo", flags: {} }] }] }
|
||||
: url.includes(`/tasks/${fixtureTask.id}/prompt`)
|
||||
? { id: fixtureTask.id, prompt: "" }
|
||||
: pathname === `/api/tasks/${fixtureTask.id}`
|
||||
? fixtureTask
|
||||
: pathname === "/api/tasks"
|
||||
? [fixtureTask]
|
||||
: url.includes("/insights")
|
||||
? { insights: [], count: 0 }
|
||||
: url.includes("/evals")
|
||||
? { results: [] }
|
||||
: url.includes("/missions")
|
||||
? []
|
||||
: url.includes("/goals")
|
||||
? { goals: [] }
|
||||
: url.includes("/models")
|
||||
? { models: [], favoriteProviders: [], favoriteModels: [] }
|
||||
: url.includes("/settings") ? { taskPopupsBoardListOnly: false }
|
||||
: url.includes("/agents") || url.includes("/nodes") ? []
|
||||
: [];
|
||||
return new Response(JSON.stringify(payload), { headers: { "content-type": "application/json" } });
|
||||
};
|
||||
|
||||
const fixtureTitle = titleMode === "fit"
|
||||
? "Fitting browser title"
|
||||
: titleMode === "threshold"
|
||||
? "A threshold title whose real host width crosses the two line clamp"
|
||||
: titleMode === "description" ? undefined : titleMode === "id" ? "" : "A browser measured task title that crosses the two line clamp threshold without changing the operator selected expanded state ".repeat(3);
|
||||
const fixtureDescription = titleMode === "description" ? "A browser measured description fallback that crosses the two line clamp threshold without changing the operator selected expanded state ".repeat(3) : titleMode === "id" ? "" : "Fixture description";
|
||||
const fixtureTask = {
|
||||
id: titleMode === "id" ? "FN-8806" : "FN-TITLE-FLICKER",
|
||||
title: fixtureTitle,
|
||||
description: fixtureDescription,
|
||||
column: "todo",
|
||||
status: "pending",
|
||||
prompt: "",
|
||||
steps: [],
|
||||
attachments: [],
|
||||
dependencies: [],
|
||||
createdAt: "2026-08-05T00:00:00.000Z",
|
||||
updatedAt: "2026-08-05T00:00:00.000Z",
|
||||
} as unknown as Task;
|
||||
|
||||
/*
|
||||
FNXC:TaskDetailTitle 2026-08-05-18:48:
|
||||
The App pop-out browser route hydrates the same project and task caches used after a discarded
|
||||
session, so its board card exists on App's first render. This removes timing retries from the
|
||||
fixture while App still revalidates the stable mocked API data through its production hooks.
|
||||
*/
|
||||
if (surface === "task-detail-title-app-floating") {
|
||||
const savedAt = Date.now();
|
||||
localStorage.setItem("kb-dashboard-projects-cache", JSON.stringify({ savedAt, data: [{ id: "fixture", name: "Fixture", path: "/fixture", status: "active" }] }));
|
||||
localStorage.setItem("kb-dashboard-current-project-cache", JSON.stringify({ savedAt, data: "fixture" }));
|
||||
localStorage.setItem("kb-dashboard-tasks-cache:fixture", JSON.stringify({ savedAt, data: [fixtureTask] }));
|
||||
}
|
||||
|
||||
const detailProps = {
|
||||
task: fixtureTask,
|
||||
initialTab: "definition" as const,
|
||||
onMoveTask: async () => fixtureTask,
|
||||
onDeleteTask: async () => fixtureTask,
|
||||
onMergeTask: async () => ({ success: true } as never),
|
||||
onOpenDetail: () => undefined,
|
||||
addToast: () => undefined,
|
||||
};
|
||||
|
||||
/*
|
||||
FNXC:TaskDetailTitle 2026-08-05-17:54:
|
||||
The Chromium fixture renders the production TaskDetailModal and embedded TaskDetailContent paths,
|
||||
not a title lookalike, so browser geometry can expose control-driven clamp feedback that jsdom does
|
||||
not calculate. The adapters keep API data inert while preserving the real heading, observer, and
|
||||
accessible control contract.
|
||||
*/
|
||||
const noop = () => undefined;
|
||||
const asyncTask = async () => fixtureTask;
|
||||
const asyncMerge = async () => ({ success: true } as never);
|
||||
|
||||
/*
|
||||
FNXC:TaskDetailTitle 2026-08-05-19:01:
|
||||
The browser regression must enter each production owner rather than wrapping TaskDetailContent in
|
||||
fixture-only geometry. These minimal adapters provide inert dependencies but retain AppModals,
|
||||
MainContent, ListView, the right-dock controller, and the task FloatingWindow render paths.
|
||||
*/
|
||||
function TaskDetailTitleModalHarness() {
|
||||
const modalManager = {
|
||||
detailTask: fixtureTask,
|
||||
detailTaskOrigin: "board",
|
||||
closeDetailTask: noop,
|
||||
updateDetailTask: noop,
|
||||
openNewTaskWithDescription: noop,
|
||||
openWorkflowEditor: noop,
|
||||
};
|
||||
return <div data-testid="title-host-modal"><NavigationHistoryProvider value={{ pushNav: noop, replaceCurrent: noop, removeNav: noop }}><AppModals projectId="fixture" tasks={[fixtureTask]} projects={[]} currentProject={null} addToast={noop} toasts={[]} removeToast={noop} modalManager={modalManager as never} projectActions={{} as never} taskHandlers={{} as never} taskOperations={{ moveTask: asyncTask, deleteTask: asyncTask, mergeTask: asyncMerge, archiveTask: asyncTask, retryTask: asyncTask, pauseTask: asyncTask, unpauseTask: asyncTask, resetTask: asyncTask, duplicateTask: asyncTask }} deepLink={{ handleDetailClose: noop }} settings={{ prAuthAvailable: false, autoMerge: true, taskDetailChatFirst: false, themeMode: "system", colorTheme: "default", dashboardFontScalePct: 100, shadcnCustomColors: {}, resolvedThemeMode: "light", setThemeMode: noop, setColorTheme: noop, setDashboardFontScalePct: noop, setShadcnCustomColors: noop, setQuickChatButtonModeImmediate: noop, setMobileNavPrimaryItemsImmediate: noop }} /></NavigationHistoryProvider></div>;
|
||||
}
|
||||
|
||||
function TaskDetailTitleMainPanelHarness() {
|
||||
return <div data-testid="title-host-main-panel" className="fn-8806-constrained-title-host"><MainContent {...{ taskView: "task-detail", mainPanelDetailTask: fixtureTask, tasks: [fixtureTask], currentProject: null, addToast: noop, moveTask: asyncTask, deleteTask: asyncTask, mergeTask: asyncMerge, retryTask: asyncTask, pauseTask: asyncTask, unpauseTask: asyncTask, resetTask: asyncTask, duplicateTask: asyncTask, closeTaskDetailMainPanel: noop, setMainPanelDetailTask: noop, openTaskDetailInMainPanel: noop, popOutTaskDetail: noop, modalManager: { openNewTaskWithDescription: noop }, globalPaused: false, prAuthAvailable: false, autoMerge: true, taskDetailChatFirst: false } as unknown as React.ComponentProps<typeof MainContent>} /></div>;
|
||||
}
|
||||
|
||||
function TaskDetailTitleListHarness() {
|
||||
localStorage.setItem("kb:fixture:kb-dashboard-list-selected-task", fixtureTask.id);
|
||||
return <div data-testid="title-host-list"><ListView {...{ tasks: [fixtureTask], projectId: "fixture", onMoveTask: asyncTask, onDeleteTask: asyncTask, onMergeTask: asyncMerge, addToast: noop, onOpenDetail: noop, onNewTask: noop, onQuickCreate: noop, availableModels: [], autoMerge: true, columnFlagsByTaskId: new Map() } as unknown as React.ComponentProps<typeof ListView>} /></div>;
|
||||
}
|
||||
|
||||
function TaskDetailTitleDockHarness() {
|
||||
localStorage.setItem("fusion:right-dock-open", "true");
|
||||
localStorage.setItem("fusion:right-dock-view", "tasks");
|
||||
const dock = useRightDockController({ active: true, projectId: "fixture", tasks: [fixtureTask], addToast: noop, settingsLoaded: true, researchReadinessVersion: 0, workflowSteps: [], subscribePluginEvents: () => noop, openDetailTask: noop, openTaskPopup: noop, openMobileTasksInPopup: false, openFileInBrowser: noop, onMoveTask: asyncTask, onDeleteTask: asyncTask, onMergeTask: asyncMerge, openSettings: noop, onSendSelectionToTask: noop, onCreateTaskFromInsight: noop, onNavigateToMission: noop, onTaskCreated: noop, prAuthAvailable: false, autoMerge: true, taskDetailChatFirst: false, visibilityOptions: {}, footerVisible: false });
|
||||
React.useEffect(() => { dock.openTaskInDock(fixtureTask); }, []);
|
||||
return <div data-testid="title-host-dock" className="fn-8806-constrained-title-host">{dock.dock}</div>;
|
||||
}
|
||||
|
||||
function TaskDetailTitleFloatingHarness() {
|
||||
return <div data-testid="title-host-floating"><FloatingWindow windowKey="fn-8806-task-floating" title={fixtureTask.id} onClose={noop} hideHeader dragHandleSelector=".task-detail-content--embedded > .modal-header" className="floating-window--task-detail" defaultSize={{ width: 560, height: 480 }} minSize={{ width: 320, height: 240 }} layer="task-detail"><TaskDetailContent {...detailProps} embedded onRequestClose={noop} /></FloatingWindow></div>;
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:TaskDetailTitle 2026-08-05-18:34:
|
||||
The floating-title browser route renders App itself, then opens the task through its board-detail
|
||||
Pop out control. This retains App's live-row selection, origin-view identity, visibility gate, and
|
||||
usePoppedOutTasks persistence instead of treating a hand-composed FloatingWindow as host coverage.
|
||||
*/
|
||||
function TaskDetailTitleAppFloatingHarness() {
|
||||
return <App />;
|
||||
}
|
||||
|
||||
function TaskDetailTitleEmbeddedHarness() {
|
||||
return <div data-testid="title-host-embedded" className="fn-8806-constrained-title-host" style={{ width: "24rem", height: "36rem" }}><TaskDetailContent {...detailProps} embedded /></div>;
|
||||
}
|
||||
|
||||
function TaskDetailResizeHarness() {
|
||||
return <FloatingWindow
|
||||
windowKey="task-detail-fixture"
|
||||
@@ -141,7 +276,7 @@ function GenericFloatingWindowHarness() {
|
||||
function Fixture() {
|
||||
return <I18nextProvider i18n={i18n}>
|
||||
<ConfirmDialogProvider skipConfirmations>
|
||||
{surface === "agent-list-modal" ? <AgentListModal isOpen onClose={() => undefined} addToast={() => undefined} /> : surface === "setup-wizard-modal" ? <SetupWizardModal onProjectRegistered={() => undefined} onClose={() => undefined} /> : surface === "floating-window" ? <FloatingWindowHarness /> : surface === "floating-window-headerless" ? <HeaderlessFloatingWindowHarness /> : surface === "floating-window-generic" ? <GenericFloatingWindowHarness /> : surface === "task-detail" ? <TaskDetailResizeHarness /> : <NewTaskModal
|
||||
{surface === "task-detail-title-app-floating" ? <TaskDetailTitleAppFloatingHarness /> : surface === "agent-list-modal" ? <AgentListModal isOpen onClose={() => undefined} addToast={() => undefined} /> : surface === "setup-wizard-modal" ? <SetupWizardModal onProjectRegistered={() => undefined} onClose={() => undefined} /> : surface === "floating-window" ? <FloatingWindowHarness /> : surface === "floating-window-headerless" ? <HeaderlessFloatingWindowHarness /> : surface === "floating-window-generic" ? <GenericFloatingWindowHarness /> : surface === "task-detail-title-modal" ? <TaskDetailTitleModalHarness /> : surface === "task-detail-title-main-panel" ? <TaskDetailTitleMainPanelHarness /> : surface === "task-detail-title-list" ? <TaskDetailTitleListHarness /> : surface === "task-detail-title-dock" ? <TaskDetailTitleDockHarness /> : surface === "task-detail-title-floating" ? <TaskDetailTitleFloatingHarness /> : surface === "task-detail-title-embedded" ? <TaskDetailTitleEmbeddedHarness /> : surface === "task-detail" ? <TaskDetailResizeHarness /> : <NewTaskModal
|
||||
isOpen
|
||||
tasks={[]}
|
||||
onClose={() => undefined}
|
||||
|
||||
@@ -18,14 +18,21 @@ const browserCandidates = process.platform === "darwin"
|
||||
? ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "/Applications/Chromium.app/Contents/MacOS/Chromium"]
|
||||
: ["/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/usr/bin/chromium", "/usr/bin/chromium-browser"];
|
||||
const executablePath = [process.env.FUSION_BROWSER_SMOKE_BROWSER, process.env.CHROME_BIN, ...browserCandidates].find((candidate): candidate is string => Boolean(candidate) && existsSync(candidate));
|
||||
/*
|
||||
FNXC:TaskDetailTitle 2026-08-05-19:18:
|
||||
FN-8806's acceptance evidence is real Chromium geometry across production hosts. A skipped browser
|
||||
lane can neither observe ResizeObserver delivery nor reject the original flicker, so fail discovery
|
||||
explicitly instead of allowing a green suite without this required rendering-engine regression.
|
||||
*/
|
||||
if (!executablePath) {
|
||||
console.warn(
|
||||
"[task-modal-touch-resize] Skipping Chromium CDP touch geometry: no browser found via FUSION_BROWSER_SMOKE_BROWSER, CHROME_BIN, or platform candidates.",
|
||||
throw new Error(
|
||||
"[task-modal-touch-resize] Chromium is required for task-title stability coverage; set FUSION_BROWSER_SMOKE_BROWSER or CHROME_BIN.",
|
||||
);
|
||||
}
|
||||
const screenshots = path.resolve(process.cwd(), "e2e/__screenshots__/fn-8602");
|
||||
const floatingWindowScreenshots = path.resolve(process.cwd(), "e2e/__screenshots__/fn-8605");
|
||||
const fn8607Screenshots = path.resolve(process.cwd(), "e2e/__screenshots__/fn-8607");
|
||||
const fn8806Screenshots = path.resolve(process.cwd(), "e2e/__screenshots__/fn-8806");
|
||||
|
||||
async function touchDrag(cdp: Cdp, point: Point, delta = { x: 48, y: 36 }) {
|
||||
await cdp.send("Input.dispatchTouchEvent", { type: "touchStart", touchPoints: [{ x: point.x, y: point.y, id: 1 }] });
|
||||
@@ -115,13 +122,49 @@ async function targetCenter(page: Page, selector: string): Promise<Point> {
|
||||
return { x: box.x + box.width / 2, y: box.y + box.height / 2 };
|
||||
}
|
||||
|
||||
async function openProductionTitleHost(page: Page, name: string, hostTestId: string) {
|
||||
await page.evaluate(async ({ name, hostTestId }) => {
|
||||
const nextFrames = async () => {
|
||||
await new Promise<void>((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
|
||||
};
|
||||
await nextFrames();
|
||||
if (name === "list-split") {
|
||||
document.querySelector<HTMLElement>("tr[data-id]")?.click();
|
||||
await nextFrames();
|
||||
}
|
||||
if (name === "floating-window") {
|
||||
for (let frame = 0; frame < 6 && !document.querySelector("[data-id]"); frame++) await nextFrames();
|
||||
const taskCard = document.querySelector<HTMLElement>("[data-id]");
|
||||
if (!taskCard) throw new Error("App fixture did not render its live board task before opening the production pop-out path");
|
||||
taskCard.click();
|
||||
for (let frame = 0; frame < 6 && !document.querySelector("[data-testid='task-detail-pop-out']"); frame++) await nextFrames();
|
||||
const popOut = document.querySelector<HTMLButtonElement>("[data-testid='task-detail-pop-out']");
|
||||
if (!popOut) throw new Error("App board detail did not expose its production Pop out control");
|
||||
popOut.click();
|
||||
await nextFrames();
|
||||
}
|
||||
let host: HTMLElement | null = null;
|
||||
let detailContent: HTMLElement | null = null;
|
||||
for (let frame = 0; frame < 6 && (!host || !detailContent); frame++) {
|
||||
host = name === "floating-window"
|
||||
? document.querySelector<HTMLElement>(`[data-testid^='${hostTestId}']`)
|
||||
: document.querySelector<HTMLElement>(`[data-testid='${hostTestId}']`);
|
||||
detailContent = name === "floating-window"
|
||||
? host?.querySelector<HTMLElement>(".task-detail-content") ?? null
|
||||
: document.querySelector<HTMLElement>(".task-detail-content");
|
||||
if (!host || !detailContent) await nextFrames();
|
||||
}
|
||||
if (!host || !detailContent) throw new Error(`${name} did not render its intended production host and TaskDetailContent (host=${Boolean(host)} content=${Boolean(detailContent)} body=${document.body.textContent?.slice(0, 500) ?? ""})`);
|
||||
}, { name, hostTestId });
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:TaskModalResize 2026-07-26-15:12:
|
||||
Browser CDP gestures are required because jsdom cannot resolve CSS hit targets. This fixture mounts
|
||||
both production resize paths and sends CSS-pixel touch input through Chromium so elementFromPoint,
|
||||
pointer capture, persistence, and header-drag isolation use the same browser input path.
|
||||
*/
|
||||
describe.runIf(executablePath)("Task modal tablet touch resize browser regression", () => {
|
||||
describe("Task modal tablet touch resize browser regression", () => {
|
||||
let server: ViteDevServer; let browser: Browser; let baseUrl = "";
|
||||
beforeAll(async () => {
|
||||
server = await createServer({ root: process.cwd(), server: { host: "127.0.0.1", port: 0, watch: null }, logLevel: "error" });
|
||||
@@ -510,6 +553,224 @@ describe.runIf(executablePath)("Task modal tablet touch resize browser regressio
|
||||
await page.close();
|
||||
}, 30_000);
|
||||
|
||||
/*
|
||||
FNXC:TaskDetailTitle 2026-08-05-17:54:
|
||||
Chromium samples real TaskDetailModal and embedded TaskDetailContent after multiple animation
|
||||
frames. Browser layout—not jsdom's fixed geometry—is the regression authority for a title whose
|
||||
control used to change the same clamp geometry that decided whether the control existed.
|
||||
*/
|
||||
/*
|
||||
FNXC:TaskDetailTitle 2026-08-05-19:01:
|
||||
Each row enters the actual modal orchestration, board main panel, List split pane, right-dock
|
||||
controller, or App task pop-out path. The floating row renders App, opens a live board task, and
|
||||
activates its production Pop out control before sampling. Sample both desktop and constrained widths:
|
||||
a title control must not change its own overflow eligibility after an operator expands or collapses.
|
||||
*/
|
||||
it.each([
|
||||
["modal", "title-host-modal", "task-detail-title-modal", 1024],
|
||||
["main-panel", "title-host-main-panel", "task-detail-title-main-panel", 1024],
|
||||
["list-split", "title-host-list", "task-detail-title-list", 1024],
|
||||
["right-dock", "title-host-dock", "task-detail-title-dock", 1024],
|
||||
["floating-window", "floating-window-overlay-task-detail-", "task-detail-title-app-floating", 1024],
|
||||
["modal", "title-host-modal", "task-detail-title-modal", 720],
|
||||
["main-panel", "title-host-main-panel", "task-detail-title-main-panel", 720],
|
||||
["list-split", "title-host-list", "task-detail-title-list", 820],
|
||||
["right-dock", "title-host-dock", "task-detail-title-dock", 820],
|
||||
["floating-window", "floating-window-overlay-task-detail-", "task-detail-title-app-floating", 720],
|
||||
] as const)("keeps the %s production title host %s via %s stable at %dpx after one activation", async (name, hostTestId, surface, width) => {
|
||||
const page = await browser.newPage({ viewport: { width, height: 844 } });
|
||||
page.on("pageerror", (message) => console.error(`[task-title-${name}] ${message.message ?? ""}\n${(message as { stack?: string }).stack ?? ""}`));
|
||||
await page.goto(`${baseUrl}app/task-modal-touch-resize-e2e-fixture.html?surface=${surface}&reset=1${name === "floating-window" ? "&project=fixture" : ""}`);
|
||||
const samples = await page.evaluate(async ({ hostTestId, name }) => {
|
||||
const nextFrames = async () => {
|
||||
await new Promise<void>((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
|
||||
};
|
||||
await nextFrames();
|
||||
if (name === "list-split") {
|
||||
document.querySelector<HTMLElement>("tr[data-id='FN-TITLE-FLICKER']")?.click();
|
||||
await nextFrames();
|
||||
}
|
||||
if (name === "floating-window") {
|
||||
// The fixture hydrates App's production project/task caches, then awaits its React commit
|
||||
// through animation frames rather than a wall-clock delay.
|
||||
for (let frame = 0; frame < 6 && !document.querySelector("[data-id='FN-TITLE-FLICKER']"); frame++) await nextFrames();
|
||||
const taskCard = document.querySelector<HTMLElement>("[data-id='FN-TITLE-FLICKER']");
|
||||
if (!taskCard) throw new Error("App fixture did not render its live board task before opening the production pop-out path");
|
||||
taskCard.click();
|
||||
for (let frame = 0; frame < 6 && !document.querySelector("[data-testid='task-detail-pop-out']"); frame++) await nextFrames();
|
||||
const popOut = document.querySelector<HTMLButtonElement>("[data-testid='task-detail-pop-out']");
|
||||
if (!popOut) throw new Error(`App board detail did not expose its production Pop out control; title:${document.querySelector("h2.detail-title")?.textContent ?? "missing"}`);
|
||||
popOut.click();
|
||||
await nextFrames();
|
||||
}
|
||||
const host = name === "floating-window"
|
||||
? document.querySelector(`[data-testid^='${hostTestId}']`)
|
||||
: document.querySelector(`[data-testid='${hostTestId}']`);
|
||||
const title = name === "floating-window"
|
||||
? host?.querySelector<HTMLHeadingElement>("h2.detail-title") ?? null
|
||||
: document.querySelector<HTMLHeadingElement>("h2.detail-title");
|
||||
const control = name === "floating-window"
|
||||
? host?.querySelector<HTMLButtonElement>(".detail-title-control") ?? null
|
||||
: document.querySelector<HTMLButtonElement>(".detail-title-control");
|
||||
const detailContent = title?.closest<HTMLElement>(".task-detail-content") ?? null;
|
||||
if (!host || !title || !control || !detailContent) throw new Error(`${name} did not render its production title host, TaskDetailContent, and overflow control (host=${Boolean(host)} title=${Boolean(title)} content=${Boolean(detailContent)} control=${Boolean(control)} aria-hidden=${host?.getAttribute("aria-hidden")}): ${document.querySelector("[data-testid='list-split-detail-content']") ? "split-detail-without-title" : document.querySelector("[data-testid='list-view-body']") ? "list-without-split-detail" : "host-missing"}`);
|
||||
const controlIdentity = control;
|
||||
const sample = () => {
|
||||
const liveTitle = name === "floating-window"
|
||||
? host.querySelector<HTMLHeadingElement>("h2.detail-title")
|
||||
: document.querySelector<HTMLHeadingElement>("h2.detail-title");
|
||||
const liveControl = name === "floating-window"
|
||||
? host.querySelector<HTMLButtonElement>(".detail-title-control")
|
||||
: document.querySelector<HTMLButtonElement>(".detail-title-control");
|
||||
return {
|
||||
collapsed: liveTitle?.classList.contains("detail-title--collapsed") ?? true,
|
||||
expanded: liveControl?.getAttribute("aria-expanded") ?? null,
|
||||
controls: document.querySelectorAll(".detail-title-control").length,
|
||||
sameControl: liveControl === controlIdentity,
|
||||
height: liveTitle?.getBoundingClientRect().height ?? 0,
|
||||
};
|
||||
};
|
||||
control.click();
|
||||
const expanded = [];
|
||||
for (let frame = 0; frame < 6; frame++) { await nextFrames(); expanded.push(sample()); }
|
||||
|
||||
// Resize the live production heading row, then yield native ResizeObserver delivery frames.
|
||||
// This is intentionally not a fixed jsdom geometry stub: Chromium recomputes the real host's
|
||||
// flex and clamp layout while the operator-owned expansion remains selected.
|
||||
const headingRow = title.parentElement;
|
||||
const originalInlineSize = headingRow?.style.inlineSize ?? "";
|
||||
const widthBeforeResize = title.getBoundingClientRect().width;
|
||||
if (headingRow) headingRow.style.inlineSize = "75%";
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
const expandedAfterResize = [];
|
||||
for (let frame = 0; frame < 6; frame++) { await nextFrames(); expandedAfterResize.push(sample()); }
|
||||
const widthAfterResize = title.getBoundingClientRect().width;
|
||||
|
||||
(name === "floating-window"
|
||||
? host.querySelector<HTMLButtonElement>(".detail-title-control")
|
||||
: document.querySelector<HTMLButtonElement>(".detail-title-control"))?.click();
|
||||
const collapsed = [];
|
||||
for (let frame = 0; frame < 6; frame++) { await nextFrames(); collapsed.push(sample()); }
|
||||
if (headingRow) headingRow.style.inlineSize = originalInlineSize;
|
||||
return { expanded, expandedAfterResize, collapsed, widthBeforeResize, widthAfterResize };
|
||||
}, { hostTestId, name });
|
||||
expect(samples.expanded.every((sample) => !sample.collapsed && sample.expanded === "true" && sample.controls === 1 && sample.sameControl)).toBe(true);
|
||||
expect(new Set(samples.expanded.map((sample) => sample.height))).toHaveLength(1);
|
||||
expect(samples.widthAfterResize).toBeLessThan(samples.widthBeforeResize);
|
||||
expect(samples.expandedAfterResize.every((sample) => !sample.collapsed && sample.expanded === "true" && sample.controls === 1 && sample.sameControl)).toBe(true);
|
||||
expect(new Set(samples.expandedAfterResize.map((sample) => sample.height))).toHaveLength(1);
|
||||
expect(samples.collapsed.every((sample) => sample.collapsed && sample.expanded === "false" && sample.controls === 1 && sample.sameControl)).toBe(true);
|
||||
expect(new Set(samples.collapsed.map((sample) => sample.height))).toHaveLength(1);
|
||||
if (name === "modal" && (width === 1024 || width === 720)) {
|
||||
await mkdir(fn8806Screenshots, { recursive: true });
|
||||
await page.screenshot({
|
||||
path: path.join(fn8806Screenshots, width === 1024 ? "task-title-stable-modal-desktop.png" : "task-title-stable-modal-narrow.png"),
|
||||
});
|
||||
}
|
||||
await page.close();
|
||||
}, 30_000);
|
||||
|
||||
/*
|
||||
FNXC:TaskDetailTitle 2026-08-05-19:39:
|
||||
FN-8806 requires Chromium—not fixed jsdom dimensions—to prove the no-control fitting state, both
|
||||
display fallbacks, and a real host-width threshold transition. Every production host and viewport
|
||||
must keep the control absent when fitting, render exactly one accessible control when overflowing,
|
||||
and update only eligibility when native ResizeObserver geometry crosses the clamp boundary.
|
||||
*/
|
||||
const titleHostRows = [
|
||||
["modal", "title-host-modal", "task-detail-title-modal", 1024],
|
||||
["main-panel", "title-host-main-panel", "task-detail-title-main-panel", 1024],
|
||||
["list-split", "title-host-list", "task-detail-title-list", 1024],
|
||||
["right-dock", "title-host-dock", "task-detail-title-dock", 1024],
|
||||
["floating-window", "floating-window-overlay-task-detail-", "task-detail-title-app-floating", 1024],
|
||||
["modal", "title-host-modal", "task-detail-title-modal", 720],
|
||||
["main-panel", "title-host-main-panel", "task-detail-title-main-panel", 720],
|
||||
["list-split", "title-host-list", "task-detail-title-list", 820],
|
||||
["right-dock", "title-host-dock", "task-detail-title-dock", 820],
|
||||
["floating-window", "floating-window-overlay-task-detail-", "task-detail-title-app-floating", 720],
|
||||
] as const;
|
||||
|
||||
for (const [titleMode, expectedText, expectsControl] of [
|
||||
["fit", "Fitting browser title", false],
|
||||
["description", "A browser measured description fallback", true],
|
||||
["id", "FN-8806", false],
|
||||
] as const) {
|
||||
it.each(titleHostRows)("renders %s host %s via %s at %dpx with title fallback state", async (name, hostTestId, surface, width) => {
|
||||
const page = await browser.newPage({ viewport: { width, height: 844 } });
|
||||
page.on("pageerror", (message) => console.error(`[task-title-${name}-${titleMode}] ${message.message ?? ""}`));
|
||||
await page.goto(`${baseUrl}app/task-modal-touch-resize-e2e-fixture.html?surface=${surface}&titleMode=${titleMode}&reset=1${name === "floating-window" ? "&project=fixture" : ""}`);
|
||||
await openProductionTitleHost(page, name, hostTestId);
|
||||
const state = await page.evaluate(({ name, hostTestId }) => {
|
||||
const host = name === "floating-window"
|
||||
? document.querySelector<HTMLElement>(`[data-testid^='${hostTestId}']`)
|
||||
: document.querySelector<HTMLElement>(`[data-testid='${hostTestId}']`);
|
||||
const title = name === "floating-window"
|
||||
? host?.querySelector<HTMLHeadingElement>("h2.detail-title") ?? null
|
||||
: document.querySelector<HTMLHeadingElement>("h2.detail-title");
|
||||
const controls = name === "floating-window"
|
||||
? host?.querySelectorAll<HTMLButtonElement>(".detail-title-control") ?? []
|
||||
: document.querySelectorAll<HTMLButtonElement>(".detail-title-control");
|
||||
return {
|
||||
text: title?.textContent ?? "",
|
||||
collapsed: title?.classList.contains("detail-title--collapsed") ?? false,
|
||||
controls: controls.length,
|
||||
ariaLabels: [...controls].map((control) => control.getAttribute("aria-label")),
|
||||
expanded: [...controls].map((control) => control.getAttribute("aria-expanded")),
|
||||
};
|
||||
}, { name, hostTestId });
|
||||
expect(state.text).toContain(expectedText);
|
||||
expect(state.collapsed).toBe(true);
|
||||
expect(state.controls).toBe(expectsControl ? 1 : 0);
|
||||
expect(state.ariaLabels).toEqual(expectsControl ? ["Expand task title"] : []);
|
||||
expect(state.expanded).toEqual(expectsControl ? ["false"] : []);
|
||||
await page.close();
|
||||
}, 30_000);
|
||||
}
|
||||
|
||||
it.each(titleHostRows)("crosses the clamp threshold in real Chromium for %s host %s via %s at %dpx", async (name, hostTestId, surface, width) => {
|
||||
const page = await browser.newPage({ viewport: { width, height: 844 } });
|
||||
await page.goto(`${baseUrl}app/task-modal-touch-resize-e2e-fixture.html?surface=${surface}&titleMode=threshold&reset=1${name === "floating-window" ? "&project=fixture" : ""}`);
|
||||
await openProductionTitleHost(page, name, hostTestId);
|
||||
const state = await page.evaluate(async ({ name, hostTestId }) => {
|
||||
const nextFrames = async () => {
|
||||
await new Promise<void>((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
|
||||
};
|
||||
const host = name === "floating-window"
|
||||
? document.querySelector<HTMLElement>(`[data-testid^='${hostTestId}']`)
|
||||
: document.querySelector<HTMLElement>(`[data-testid='${hostTestId}']`);
|
||||
const title = name === "floating-window"
|
||||
? host?.querySelector<HTMLHeadingElement>("h2.detail-title")
|
||||
: document.querySelector<HTMLHeadingElement>("h2.detail-title");
|
||||
const headingRow = title?.parentElement as HTMLElement | null;
|
||||
if (!title || !headingRow) throw new Error(`${name} did not retain its production title heading row`);
|
||||
const sample = () => ({
|
||||
width: title.getBoundingClientRect().width,
|
||||
controls: name === "floating-window"
|
||||
? host?.querySelectorAll(".detail-title-control").length ?? 0
|
||||
: document.querySelectorAll(".detail-title-control").length,
|
||||
collapsed: title.classList.contains("detail-title--collapsed"),
|
||||
});
|
||||
headingRow.style.inlineSize = "100rem";
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
for (let frame = 0; frame < 6; frame++) await nextFrames();
|
||||
const fitting = sample();
|
||||
headingRow.style.inlineSize = "12rem";
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
for (let frame = 0; frame < 6; frame++) await nextFrames();
|
||||
const overflowing = sample();
|
||||
headingRow.style.inlineSize = "100rem";
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
for (let frame = 0; frame < 6; frame++) await nextFrames();
|
||||
const fittingAgain = sample();
|
||||
return { fitting, overflowing, fittingAgain };
|
||||
}, { name, hostTestId });
|
||||
expect(state.fitting.width).toBeGreaterThan(state.overflowing.width);
|
||||
expect(state.fitting).toEqual({ width: state.fitting.width, controls: 0, collapsed: true });
|
||||
expect(state.overflowing).toEqual({ width: state.overflowing.width, controls: 1, collapsed: true });
|
||||
expect(state.fittingAgain).toEqual({ width: state.fittingAgain.width, controls: 0, collapsed: true });
|
||||
await page.close();
|
||||
}, 30_000);
|
||||
|
||||
it("keeps the true-phone sheet free of active resize targets", async () => {
|
||||
const page = await browser.newPage({ viewport: { width: 390, height: 844 } });
|
||||
const cdp = await page.context().newCDPSession(page);
|
||||
|
||||
Reference in New Issue
Block a user