diff --git a/web_ui/src/app/chrome/AppBar.test.tsx b/web_ui/src/app/chrome/AppBar.test.tsx new file mode 100644 index 0000000..aaa3626 --- /dev/null +++ b/web_ui/src/app/chrome/AppBar.test.tsx @@ -0,0 +1,229 @@ +import { ReactFlowProvider } from "@xyflow/react"; +import { render, screen, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it, vi } from "vitest"; +import { AppBar } from "./AppBar"; +import { OverlayProvider } from "../overlays/overlays"; +import { SceneStore } from "../canvas/sceneStore"; +import type { WsTransport } from "../../lib/ws/transport"; + +// R8a (UI/UX issue list findings #5 and #8): this is the first dedicated +// test file for AppBar.tsx. jsdom does not implement CSS @container queries +// (or any layout at all), so it cannot verify the RESPONSIVE half of finding +// #5's fix - which of the two copies of a button is actually visible at a +// given width is a live-browser-only concern, verified separately. What +// these tests cover instead: the dead provider-mode that used to + * sit here was permanently `disabled`, held exactly one hardcoded option + * ("Ollama (Local)"), and its onChange was a literal no-op - there has never + * been a setProviderMode intent anywhere in backend/ for it to call. Removed + * outright rather than left as a dead control; Settings' own provider pages + * are the real, complete switcher. + * + * R8a (finding #5): below ~1120px window width this toolbar's 12 buttons + * (13 with the now-removed select) had no shrink/wrap/overflow behavior at + * all, so the low end of it - Settings, About, Help, the connection status + * next to this component - ran off the right edge of the window, dragging a + * horizontal scrollbar across the WHOLE document with it (the canvas and + * composer went with it, off-screen). + * + * Fixed with a real overflow menu, not the audit's own "minimum stopgap" + * (bare overflow-x: auto). `.appbar` gets `min-width: 0` (a flex item + * otherwise floors at its content's min-content width - that is what forced + * the overflow in the first place) and `container-type: inline-size` + * (styles.css), so its own descendants can query how much room THIS + * toolbar - not the window - actually has. Every collapsible button is + * rendered TWICE: once inline, once as a duplicate inside the overflow + * menu, tagged with the same data-tier either way. Pure CSS @container + * rules (styles.css) decide which copy is visible at the current width, in + * three tiers (least-used collapses first) - no ResizeObserver/width- + * measurement JS anywhere; container queries are declarative, and this app + * targets one Chromium engine (WebView2), so there is no compatibility + * reason to reach for JS instead. Library, Save and Settings never + * collapse - those three are exactly what the finding flagged as becoming + * unreachable. + * + * The overflow menu is the shared `Popover` (overlays.tsx), NOT `NodeMenu` + * (canvas/NodeMenu.tsx) - tried first, reverted after live testing caught a + * real bug: NodeMenu portals to document.body, and a portaled element is no + * longer a DESCENDANT of `.appbar` in the DOM, so it falls OUTSIDE the + * `@container appbar` scope entirely - every item in it would have silently + * stayed hidden forever, regardless of width. `.appbar` therefore does NOT + * get `overflow: hidden` either (the version that used NodeMenu needed it, + * to stop tier-hidden buttons spilling past the toolbar mid-resize, and + * could afford it because the portaled menu didn't live inside that box to + * begin with); the horizontal-spill backstop instead lives one level up, on + * `.app-topbar` (`overflow-x: hidden`, with `overflow-y: visible` so it + * does not clip this dropdown, which extends below the header row by + * design). `Popover`'s own light-dismiss (outside pointerdown) and the + * OverlayProvider's single-open policy (opening Settings while this is open + * correctly closes it, same as every other surface) both apply for free. + * + * Both copies of a collapsible button call the exact same handler - the + * handler is the single source of truth for BEHAVIOR, only the two bits of + * JSX markup (label text) are duplicated, which is what stays in sync via + * ordinary code review rather than an abstraction neither codebase + * precedent nor this component's small, fixed button set actually needs. */ export function AppBar({ store }: { store: SceneStore }) { @@ -36,6 +86,14 @@ export function AppBar({ store }: { store: SceneStore }) { const viewport = getViewport(); setViewport({ ...viewport, zoom: 1 }, { duration: 200 }); }; + const exportPng = () => void exportCanvasAsPng({ getNodes, getViewport, setViewport }, "--gl-surface-window"); + + // Overlay-opening actions (Pins/View/Plugins/About/Help) close this popover + // for free via OverlayProvider's own single-open policy - opening any + // surface replaces whatever else was open, "toolbar-overflow" included. + // Plain actions (Organize/Zoom/Fit/Export) never touch the overlay + // registry at all, so their overflow copies close it explicitly. + const closeOverflow = () => overlays.close(); return (
@@ -51,9 +109,12 @@ export function AppBar({ store }: { store: SceneStore }) { + + - - - - - - - + + - - + + + + + + + + + + + + + +