FN-7684: move terminal shortcuts/zoom controls into footer on tablet widths
On tablet-width viewports (769-1024px) the terminal header was overcrowded, so shortcut/zoom/preference controls now move into the bottom status-bar footer instead, mirroring the existing FN-7560 mobile layout while true desktop keeps the header controls. - Add an isTabletTerminal detection flag (769-1024px, non-mobile) alongside the existing mobile flag - Render the shared terminalActionControls fragment in the .terminal-status-bar footer for tablet widths, keeping desktop pin/pop-out toggles available there too - True desktop (>1024px) continues to render font size / clear / shortcuts / preferences controls in the header - Update TerminalModal.css with footer layout styles for the tablet action controls - Update TerminalModal tests to cover the new tablet breakpoint rendering - Update docs/dashboard-guide.md to describe the tablet footer control location - Add a changeset (@runfusion/fusion: patch) documenting the fix Files changed: .changeset/fn-7684-tablet-terminal-footer.md | 7 ++ docs/dashboard-guide.md | 6 +- .../dashboard/app/components/TerminalModal.css | 34 ++++++ .../dashboard/app/components/TerminalModal.tsx | 103 ++++++++++++----- .../components/__tests__/TerminalModal.test.tsx | 126 ++++++++++++++++++--- 5 files changed, 231 insertions(+), 45 deletions(-) Fusion-Task-Id: FN-7684 Fusion-Task-Lineage: 627c40af-a152-451a-a045-65ad8babea9e Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7684-tablet-terminal-footer.md
Normal file
7
.changeset/fn-7684-tablet-terminal-footer.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: On tablet widths, move terminal shortcuts/zoom controls into the bottom footer so they no longer overlap header icons.
|
||||
category: fix
|
||||
dev: Adds an isTabletTerminal flag (769–1024px, non-mobile) that renders the shared terminalActionControls fragment in the .terminal-status-bar footer (as FN-7560 did for mobile) instead of the header; true desktop (>1024px) keeps the header layout. Tablet footer keeps the desktop pin/pop-out toggles.
|
||||
@@ -611,12 +611,12 @@ On Windows, the embedded terminal starts a supported shell inside Fusion, such a
|
||||
Use the terminal on desktop/tablet:
|
||||
|
||||
1. Select the **Terminal** button in the footer executor status bar.
|
||||
Expected outcome: the terminal opens as a bottom-docked overlay panel with the active shell session, header controls for font size / clear / shortcuts / preferences, and a draggable top resize handle.
|
||||
2. Select **Pin terminal (push content)** from the terminal header.
|
||||
Expected outcome: the terminal opens as a bottom-docked overlay panel with the active shell session and a draggable top resize handle. On true desktop (wider than the tablet tier) the font size / clear / shortcuts / preferences controls render in the terminal header; on tablet widths (769-1024px) those same controls render in a bottom action-control footer bar instead, so the narrower tablet header does not overflow (the tab strip, workspace picker, and close button stay in the header on both).
|
||||
2. Select **Pin terminal (push content)** — from the terminal header on desktop, or the bottom action-control footer on tablet.
|
||||
Expected outcome: the terminal moves into a persisted below-application panel that reserves space instead of covering the board, chat, or right sidebar. Select **Unpin terminal (overlay content)** to return to the overlay docked panel.
|
||||
3. Drag the top edge of the docked or pinned panel.
|
||||
Expected outcome: the panel height changes within its viewport-safe bounds and persists per project, with pinned mode clamped shorter so the application remains usable.
|
||||
4. Select **Pop out** from the terminal header.
|
||||
4. Select **Pop out** — from the terminal header on desktop, or the bottom action-control footer on tablet.
|
||||
Expected outcome: the terminal switches to a floating window that can be dragged and freely resized; size, position, and display mode are saved per project.
|
||||
5. Select **Dock** in the floating terminal.
|
||||
Expected outcome: the terminal returns to the bottom docked overlay panel using the saved docked height.
|
||||
|
||||
@@ -1790,3 +1790,37 @@ The Android keyboard-open recurrence can start with a touch-primary visualViewpo
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:TerminalFooter 2026-07-08-15:00:
|
||||
FN-7684: extend the FN-7560 mobile footer relocation to the project's canonical
|
||||
tablet tier (769-1024px, `--tablet-breakpoint: 1024px`, matching the JS
|
||||
`isTabletTerminal` flag). A tablet keeps the desktop display modes (docked/
|
||||
floating/pinned-below, unlike mobile's fullscreen shell) but its header still
|
||||
overflows if it renders the full `.terminal-actions` cluster — the help text
|
||||
and zoom controls collide with the pin/pop-out/close icons. TerminalModal.tsx
|
||||
stops rendering `.terminal-actions` in the header at <=1024px and instead
|
||||
renders the SAME shared `terminalActionControls` fragment here, mirroring every
|
||||
mobile footer declaration (not just a display:flex override) so the tablet
|
||||
footer keeps the same min-width: 0 + overflow-x: auto horizontal-scroll safety
|
||||
net as mobile (FN-7550/FN-7560) plus the FN-7621 touch-action carve-out.
|
||||
Unlike mobile, the tablet footer has more room, so it intentionally does NOT
|
||||
reapply the mobile-only `.terminal-shortcuts--header, .terminal-connection-status
|
||||
{ display: none }` hide above — both stay visible on tablet.
|
||||
*/
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.terminal-status-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
gap: var(--space-xs);
|
||||
border-top: 1px solid var(--border);
|
||||
padding: var(--space-xs);
|
||||
padding-bottom: max(var(--space-xs), env(safe-area-inset-bottom, 0));
|
||||
min-height: 36px;
|
||||
min-width: 0;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: thin;
|
||||
touch-action: pan-x pan-y;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,6 +357,24 @@ function isTerminalMobileViewport(): boolean {
|
||||
return window.innerWidth <= 768 || (hasTouchScreen && (getTerminalViewportWidth(true) <= 768 || getTerminalViewportHeight(true) <= 480));
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:TerminalFooter 2026-07-08-15:00:
|
||||
FN-7684: on a tablet-width viewport (769-1024px, the project's canonical tablet
|
||||
tier — `--tablet-breakpoint: 1024px`) the terminal is NOT mobile but its header
|
||||
still overflows if it renders the full desktop `.terminal-actions` cluster (the
|
||||
zoom/font-size controls, Shortcuts/Preferences toggles, connection status, and
|
||||
help text collide with the pin/pop-out/close icons — see attachment 1863.png).
|
||||
This flag is checked ONLY when `isTerminalMobileViewport()` is false, so mobile
|
||||
always wins the mobile fullscreen shell; it gates relocating the shared action-
|
||||
control fragment into the `.terminal-status-bar` footer on tablet too (FN-7560
|
||||
established the footer for mobile).
|
||||
*/
|
||||
function isTerminalTabletViewport(): boolean {
|
||||
if (typeof window === "undefined") return false;
|
||||
if (isTerminalMobileViewport()) return false;
|
||||
return window.innerWidth >= 769 && window.innerWidth <= 1024;
|
||||
}
|
||||
|
||||
function isMacPlatform(): boolean {
|
||||
if (typeof navigator === "undefined") {
|
||||
return false;
|
||||
@@ -548,6 +566,8 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG
|
||||
const [floatingSize, setFloatingSize] = useState<TerminalFloatSize>(() => readTerminalFloatSize(projectId));
|
||||
const [floatingPosition, setFloatingPosition] = useState<TerminalFloatPosition>(() => readTerminalFloatPosition(readTerminalFloatSize(projectId), projectId));
|
||||
const [isMobileTerminal, setIsMobileTerminal] = useState(() => isTerminalMobileViewport());
|
||||
// FNXC:TerminalFooter 2026-07-08-15:00: FN-7684 tablet tier (769-1024px, non-mobile) — keeps the desktop display modes (docked/floating/pinned-below), only the action-control render location changes.
|
||||
const [isTabletTerminal, setIsTabletTerminal] = useState(() => isTerminalTabletViewport());
|
||||
const isDockedMode = !isMobileTerminal && displayMode === "docked";
|
||||
const isFloatingMode = !isMobileTerminal && displayMode === "floating";
|
||||
const isBelowMode = !isMobileTerminal && displayMode === "below";
|
||||
@@ -618,7 +638,11 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG
|
||||
FNXC:Terminal 2026-06-21-22:58:
|
||||
Viewport changes must force the terminal back onto the mobile fullscreen path at <=768px or touch-primary short landscape, then restore the stored desktop/tablet docked/floating mode when the viewport expands.
|
||||
*/
|
||||
const updateViewportMode = () => setIsMobileTerminal(isTerminalMobileViewport());
|
||||
const updateViewportMode = () => {
|
||||
setIsMobileTerminal(isTerminalMobileViewport());
|
||||
// FNXC:TerminalFooter 2026-07-08-15:00: FN-7684 — keep isTabletTerminal in sync from the SAME resize/visualViewport listeners as isMobileTerminal rather than adding a second competing listener.
|
||||
setIsTabletTerminal(isTerminalTabletViewport());
|
||||
};
|
||||
updateViewportMode();
|
||||
window.addEventListener("resize", updateViewportMode);
|
||||
window.visualViewport?.addEventListener("resize", updateViewportMode);
|
||||
@@ -2297,14 +2321,16 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG
|
||||
} as CSSProperties;
|
||||
|
||||
/*
|
||||
FNXC:TerminalFooter 2026-07-04-20:00:
|
||||
FNXC:TerminalFooter 2026-07-08-15:00:
|
||||
Single source of truth for the terminal action-control cluster (reconnect/restart,
|
||||
font-size, Clear, Shortcuts toggle, Preferences toggle, connection status, exit code,
|
||||
help text, and the desktop-only pin/pop-out toggles). Rendered in exactly ONE place
|
||||
per breakpoint: inside the header `.terminal-actions` on desktop/floating/pinned-below
|
||||
(FN-7502), or inside a dedicated bottom `.terminal-status-bar` footer on mobile (FN-7560)
|
||||
so the narrow mobile header does not crowd the tab dropdown and close button. Do not
|
||||
duplicate these handlers elsewhere — always render this fragment.
|
||||
per breakpoint: inside the header `.terminal-actions` ONLY on true desktop (>1024px,
|
||||
!isMobileTerminal && !isTabletTerminal), or inside a dedicated bottom
|
||||
`.terminal-status-bar` footer on BOTH mobile (<=768px, FN-7560) and tablet
|
||||
(769-1024px, FN-7684) so the narrower header does not crowd the tab strip/dropdown,
|
||||
workspace picker, and close button. Same fragment, one location per breakpoint —
|
||||
never duplicate these handlers elsewhere.
|
||||
*/
|
||||
const terminalActionControls = (
|
||||
<>
|
||||
@@ -2621,15 +2647,19 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG
|
||||
)}
|
||||
|
||||
{/*
|
||||
FNXC:TerminalFooter 2026-07-04-20:00:
|
||||
On desktop/floating/pinned-below the header keeps its FN-7502 shape:
|
||||
status title + `.terminal-actions` (rendering the shared
|
||||
`terminalActionControls` fragment) + close. On mobile (isMobileTerminal)
|
||||
the header renders ONLY the close button here — no `.terminal-actions`
|
||||
FNXC:TerminalFooter 2026-07-08-15:00:
|
||||
The header renders `.terminal-actions` (the shared `terminalActionControls`
|
||||
fragment) ONLY on true desktop (!isMobileTerminal && !isTabletTerminal,
|
||||
>1024px) — the FN-7502 shape. On mobile (isMobileTerminal) the header
|
||||
renders ONLY the corner-pinned close button — no `.terminal-actions`
|
||||
shell — because the mobile tab dropdown already occupies the header and
|
||||
the action controls move into the `.terminal-status-bar` footer below
|
||||
(FN-7560) so they don't crowd the dropdown/close. Both sites render the
|
||||
SAME fragment, never a duplicated copy, so handlers cannot drift.
|
||||
the action controls move into the `.terminal-status-bar` footer (FN-7560).
|
||||
On tablet (isTabletTerminal, 769-1024px, FN-7684) the header keeps the
|
||||
desktop tab strip + title + workspace picker but ALSO drops
|
||||
`.terminal-actions` — it still overflows at that width — in favor of a
|
||||
plain close button, with the same action controls relocating into the
|
||||
footer alongside mobile. Both footer/header render sites use the SAME
|
||||
fragment, never a duplicated copy, so handlers cannot drift.
|
||||
|
||||
FNXC:TerminalHeader 2026-07-04-20:45:
|
||||
FN-7565: on mobile, being a direct child of `.terminal-header` (not
|
||||
@@ -2641,7 +2671,9 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG
|
||||
expect for an app-sheet close control. The `terminal-close--corner`
|
||||
class (CSS: highest `order` + `margin-inline-start: auto`) fixes this
|
||||
so the X renders last in flex order and hugs the right edge regardless
|
||||
of how wide the tab dropdown / workspace picker grow.
|
||||
of how wide the tab dropdown / workspace picker grow. Tablet keeps the
|
||||
desktop `.terminal-tabs` (not the mobile dropdown) ahead of title/close
|
||||
in normal DOM order, so its plain close button needs no order override.
|
||||
*/}
|
||||
{isMobileTerminal ? (
|
||||
<button
|
||||
@@ -2660,9 +2692,7 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG
|
||||
{getStatusIndicator()}
|
||||
</div>
|
||||
|
||||
{/* Actions — labels hidden on mobile via .terminal-action-label */}
|
||||
<div className="terminal-actions" data-testid="terminal-actions">
|
||||
{terminalActionControls}
|
||||
{isTabletTerminal ? (
|
||||
<button
|
||||
className="terminal-close"
|
||||
onClick={onClose}
|
||||
@@ -2671,7 +2701,20 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
/* Actions — labels hidden on mobile via .terminal-action-label */
|
||||
<div className="terminal-actions" data-testid="terminal-actions">
|
||||
{terminalActionControls}
|
||||
<button
|
||||
className="terminal-close"
|
||||
onClick={onClose}
|
||||
data-testid="terminal-close-btn"
|
||||
title={t("terminal.closeTerminal", "Close terminal")}
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -2944,16 +2987,20 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG
|
||||
)}
|
||||
|
||||
{/*
|
||||
FNXC:TerminalFooter 2026-07-04-20:00:
|
||||
Mobile-only footer bar (FN-7560) restoring the pre-FN-7502 footer
|
||||
ergonomics: the same `terminalActionControls` fragment used by the
|
||||
desktop header renders here instead so font-size/Clear/Shortcuts/
|
||||
Preferences/connection-status/exit-code stay reachable without
|
||||
crowding the mobile header's tab dropdown and close button. Scrolls
|
||||
horizontally (min-width: 0 + overflow-x: auto) if controls exceed the
|
||||
viewport width, matching the .terminal-shortcut-panel (FN-7550) pattern.
|
||||
FNXC:TerminalFooter 2026-07-08-15:00:
|
||||
Mobile + tablet footer bar (FN-7560 mobile, FN-7684 tablet) restoring
|
||||
the pre-FN-7502 footer ergonomics on both narrower tiers: the same
|
||||
`terminalActionControls` fragment used by the true-desktop header
|
||||
renders here instead so font-size/Clear/Shortcuts/Preferences/
|
||||
connection-status/exit-code stay reachable without crowding the
|
||||
header's tab strip/dropdown, workspace picker, and close button.
|
||||
Scrolls horizontally (min-width: 0 + overflow-x: auto) if controls
|
||||
exceed the viewport width, matching the .terminal-shortcut-panel
|
||||
(FN-7550) pattern. Tablet keeps the desktop display modes (docked/
|
||||
floating/pinned-below), so this footer also carries the !isMobileTerminal-
|
||||
gated pin/pop-out toggles the fragment already renders for tablet.
|
||||
*/}
|
||||
{isMobileTerminal && (
|
||||
{(isMobileTerminal || isTabletTerminal) && (
|
||||
<div className="terminal-status-bar" data-testid="terminal-footer-actions">
|
||||
{terminalActionControls}
|
||||
</div>
|
||||
|
||||
@@ -807,20 +807,26 @@ describe("TerminalModal", () => {
|
||||
expect(belowRule).not.toContain("position: fixed;");
|
||||
expect(belowRule).toContain("height: var(--terminal-below-height);");
|
||||
|
||||
// FN-7560: the `.terminal-status-bar` footer is a MOBILE-ONLY affordance
|
||||
// (isMobileTerminal, which itself excludes below mode) — it must exist only
|
||||
// scoped inside a `@media (max-width: 768px)` block, never as a global/
|
||||
// unscoped rule that could leak a footer shell into desktop/floating/
|
||||
// pinned-below. Strip every mobile media-query block out of the
|
||||
// stylesheet and confirm no `.terminal-status-bar` rule remains outside it.
|
||||
const cssWithoutMobileMediaBlocks = terminalModalCss.replace(
|
||||
/@media \(max-width: 768px\) \{(?:[^{}]*\{[^{}]*\})*[^{}]*\}/g,
|
||||
"",
|
||||
);
|
||||
expect(cssWithoutMobileMediaBlocks).not.toMatch(/\.terminal-status-bar\s*\{/);
|
||||
// FN-7560/FN-7684: the `.terminal-status-bar` footer is a MOBILE + TABLET-ONLY
|
||||
// affordance (isMobileTerminal || isTabletTerminal, both of which exclude
|
||||
// below mode's true-desktop display) — it must exist only scoped inside the
|
||||
// mobile `@media (max-width: 768px)` block or the tablet
|
||||
// `@media (min-width: 769px) and (max-width: 1024px)` block, never as a
|
||||
// global/unscoped rule that could leak a footer shell into true-desktop
|
||||
// (>1024px) docked/floating/pinned-below. Strip every mobile AND tablet
|
||||
// media-query block out of the stylesheet and confirm no `.terminal-status-bar`
|
||||
// rule remains outside them.
|
||||
const cssWithoutMobileAndTabletMediaBlocks = terminalModalCss
|
||||
.replace(/@media \(max-width: 768px\) \{(?:[^{}]*\{[^{}]*\})*[^{}]*\}/g, "")
|
||||
.replace(/@media \(min-width: 769px\) and \(max-width: 1024px\) \{(?:[^{}]*\{[^{}]*\})*[^{}]*\}/g, "");
|
||||
expect(cssWithoutMobileAndTabletMediaBlocks).not.toMatch(/\.terminal-status-bar\s*\{/);
|
||||
const mobileFooterRule =
|
||||
terminalModalCss.match(/@media \(max-width: 768px\) \{[\s\S]*?\.terminal-status-bar\s*\{/);
|
||||
expect(mobileFooterRule).not.toBeNull();
|
||||
const tabletFooterRule = terminalModalCss.match(
|
||||
/@media \(min-width: 769px\) and \(max-width: 1024px\) \{[\s\S]*?\.terminal-status-bar\s*\{/,
|
||||
);
|
||||
expect(tabletFooterRule).not.toBeNull();
|
||||
});
|
||||
|
||||
it("exposes floating drag and resize handles and refits after floating resize", async () => {
|
||||
@@ -1497,6 +1503,30 @@ describe("TerminalModal", () => {
|
||||
expect(footerRule).toContain("min-width: 0;");
|
||||
});
|
||||
|
||||
it("gives the tablet footer bar the same horizontal-scroll pattern (FN-7684)", () => {
|
||||
// FN-7684: the tablet-tier action-control footer mirrors the FN-7560
|
||||
// mobile footer's min-width: 0 + overflow-x: auto flex-scroll pattern.
|
||||
const tabletFooterRule =
|
||||
terminalModalCss.match(
|
||||
/@media \(min-width: 769px\) and \(max-width: 1024px\) \{[\s\S]*?\.terminal-status-bar\s*\{([^}]*)\}/,
|
||||
)?.[1] ?? "";
|
||||
expect(tabletFooterRule).toContain("overflow-x: auto;");
|
||||
expect(tabletFooterRule).toContain("min-width: 0;");
|
||||
expect(tabletFooterRule).toContain("touch-action: pan-x pan-y;");
|
||||
|
||||
// Unlike mobile, the tablet-scoped mobile `display: none` hide for the
|
||||
// help text and connection status must NOT apply at the tablet tier.
|
||||
const mobileHideBlock = terminalModalCss.match(
|
||||
/@media \(max-width: 768px\) \{[\s\S]*?\.terminal-shortcuts--header,\s*\n\s*\.terminal-connection-status \{[\s\S]*?\}\s*\n\}/,
|
||||
);
|
||||
expect(mobileHideBlock).not.toBeNull();
|
||||
const tabletBlock = terminalModalCss.match(
|
||||
/@media \(min-width: 769px\) and \(max-width: 1024px\) \{([\s\S]*?)\n\}/,
|
||||
)?.[1] ?? "";
|
||||
expect(tabletBlock).not.toMatch(/\.terminal-shortcuts--header/);
|
||||
expect(tabletBlock).not.toMatch(/\.terminal-connection-status/);
|
||||
});
|
||||
|
||||
describe("real-CSS mobile cascade (FN-7621 recurrence #3)", () => {
|
||||
// FN-7621: the FN-7550/FN-7560 tests above are leaf-rule string matches —
|
||||
// they proved the declarations exist, but never proved the panel actually
|
||||
@@ -3553,9 +3583,10 @@ describe("TerminalModal — mobile layout contract", () => {
|
||||
});
|
||||
|
||||
it("header actions show connection state without a footer status-bar shell (desktop, FN-7502)", async () => {
|
||||
// FN-7560: explicitly desktop-width — the footer only exists on the mobile
|
||||
// (isMobileTerminal) path; desktop/floating/pinned-below keep the FN-7502
|
||||
// header-actions contract with NO footer shell rendered.
|
||||
// FN-7560/FN-7684: explicitly TRUE-desktop-width (>1024px) — the footer
|
||||
// only exists on the mobile (isMobileTerminal) and tablet (isTabletTerminal)
|
||||
// paths; true desktop/floating/pinned-below keep the FN-7502 header-actions
|
||||
// contract with NO footer shell rendered.
|
||||
const previousInnerWidth = window.innerWidth;
|
||||
Object.defineProperty(window, "innerWidth", { value: 1280, configurable: true });
|
||||
|
||||
@@ -3619,6 +3650,73 @@ describe("TerminalModal — mobile layout contract", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("renders terminal action controls in a tablet footer, not the header, and keeps pin/pop-out toggles (FN-7684)", async () => {
|
||||
// FN-7684: tablet width (769-1024px) must relocate the same shared
|
||||
// terminalActionControls fragment into the footer, exactly as FN-7560
|
||||
// did for mobile — but unlike mobile, tablet keeps the desktop pin/
|
||||
// pop-out toggles and the desktop tab strip / workspace picker.
|
||||
const previousInnerWidth = window.innerWidth;
|
||||
Object.defineProperty(window, "innerWidth", { value: 900, configurable: true });
|
||||
fireEvent(window, new Event("resize"));
|
||||
|
||||
try {
|
||||
render(<TerminalModal isOpen={true} onClose={mockOnClose} />);
|
||||
|
||||
await waitFor(() => {
|
||||
const footer = screen.getByTestId("terminal-footer-actions");
|
||||
expect(footer.className).toContain("terminal-status-bar");
|
||||
|
||||
const clearBtn = screen.getByTestId("terminal-clear-btn");
|
||||
const shortcutToggle = screen.getByTestId("terminal-shortcut-toggle");
|
||||
const preferencesToggle = screen.getByTestId("terminal-preferences-toggle");
|
||||
const fontSizeValue = screen.getByTestId("terminal-font-size-value");
|
||||
const pinToggle = screen.getByTestId("terminal-pin-toggle");
|
||||
const popoutToggle = screen.getByTestId("terminal-popout-toggle");
|
||||
|
||||
// Controls live inside the footer region, including the desktop-only
|
||||
// pin/pop-out toggles (tablet keeps docked/floating/pinned-below modes)...
|
||||
expect(footer.contains(clearBtn)).toBe(true);
|
||||
expect(footer.contains(shortcutToggle)).toBe(true);
|
||||
expect(footer.contains(preferencesToggle)).toBe(true);
|
||||
expect(footer.contains(fontSizeValue)).toBe(true);
|
||||
expect(footer.contains(pinToggle)).toBe(true);
|
||||
expect(footer.contains(popoutToggle)).toBe(true);
|
||||
|
||||
// ...and NOT inside the header.
|
||||
const header = document.querySelector(".terminal-header");
|
||||
expect(header).toBeTruthy();
|
||||
expect(header?.contains(clearBtn)).toBe(false);
|
||||
expect(header?.contains(shortcutToggle)).toBe(false);
|
||||
expect(header?.contains(preferencesToggle)).toBe(false);
|
||||
expect(header?.contains(fontSizeValue)).toBe(false);
|
||||
expect(header?.contains(pinToggle)).toBe(false);
|
||||
expect(header?.contains(popoutToggle)).toBe(false);
|
||||
|
||||
// No empty .terminal-actions shell renders in the tablet header.
|
||||
expect(header?.querySelector(".terminal-actions")).toBeNull();
|
||||
|
||||
// The close button, the desktop tab strip (not the mobile dropdown),
|
||||
// and the workspace picker remain in the header.
|
||||
const closeBtn = screen.getByTestId("terminal-close-btn");
|
||||
expect(header?.contains(closeBtn)).toBe(true);
|
||||
expect(footer.contains(closeBtn)).toBe(false);
|
||||
expect(screen.queryByTestId("terminal-mobile-tabs")).toBeNull();
|
||||
const tabs = screen.queryByTestId("terminal-tabs");
|
||||
expect(tabs).toBeTruthy();
|
||||
expect(header?.contains(tabs)).toBe(true);
|
||||
// The workspace picker only renders when workspaces exist (default mock
|
||||
// has none) — assert it stays in the header when present.
|
||||
const workspacePicker = screen.queryByTestId("terminal-workspace-picker");
|
||||
if (workspacePicker) {
|
||||
expect(header?.contains(workspacePicker)).toBe(true);
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
Object.defineProperty(window, "innerWidth", { value: previousInnerWidth, configurable: true });
|
||||
fireEvent(window, new Event("resize"));
|
||||
}
|
||||
});
|
||||
|
||||
it("pins the mobile close button to the top-right corner of the header, not buried in .terminal-actions (FN-7565)", async () => {
|
||||
const previousInnerWidth = window.innerWidth;
|
||||
Object.defineProperty(window, "innerWidth", { value: 390, configurable: true });
|
||||
|
||||
Reference in New Issue
Block a user