defmodule BDS.UI.ShellTest do use ExUnit.Case, async: true alias BDS.UI.Commands alias BDS.UI.Registry alias BDS.UI.Session alias BDS.UI.Workbench @css_sources [ "tokens.css", "shell.css", "sidebar.css", "tabs.css", "editor.css", "forms.css", "panel.css", "assistant.css", "overlays.css", "menu_editor.css", "media_editor.css", "import_editor.css", "utilities.css" ] defp css_source do @css_sources |> Enum.map(&File.read!("/Users/gb/Projects/bDS2/assets/css/#{&1}")) |> Enum.join("\n") end test "registry exposes the shared sidebar and editor contracts for the base shell" do sidebar_views = Registry.sidebar_views() editor_routes = Registry.editor_routes() assert Registry.default_sidebar_view() == :posts assert Enum.map(sidebar_views, & &1.id) == [ :posts, :pages, :media, :scripts, :templates, :tags, :chat, :import, :git, :settings ] assert Enum.find(sidebar_views, &(&1.id == :media)).activity_group == :top assert Enum.find(sidebar_views, &(&1.id == :git)).activity_group == :bottom assert Enum.any?(editor_routes, &(&1.id == :dashboard)) assert Enum.any?(editor_routes, &(&1.id == :post and &1.entity_tab == true)) assert Enum.any?(editor_routes, &(&1.id == :settings and &1.singleton == true)) end test "workbench session roundtrips tabs, dirty state, shell visibility, and widths" do state = Workbench.new(sidebar_visible: false, panel_visible: true) |> Workbench.set_sidebar_width(412) |> Workbench.set_assistant_sidebar_width(511) |> Workbench.open_tab(:post, "post-1", :pin) |> Workbench.open_tab(:media, "media-1", :preview) |> Workbench.mark_dirty(:post, "post-1") |> Workbench.click_activity(:media) payload = Session.serialize(state) restored = Session.restore(payload) assert restored.sidebar_visible == true assert restored.panel.visible == true assert restored.sidebar_width == 412 assert restored.assistant_sidebar_width == 511 assert restored.active_view == :media assert restored.active_tab == {:media, "media-1"} assert Workbench.dirty?(restored, :post, "post-1") == true assert Enum.map(restored.tabs, &{&1.type, &1.id, &1.is_transient}) == [ {:post, "post-1", false}, {:media, "media-1", true} ] end test "keyboard commands drive the same shared workbench policy" do state = Workbench.new(sidebar_visible: true) |> Workbench.open_tab(:post, "post-1", :pin) state = Commands.handle_shortcut(state, %{meta: true, key: "b"}) assert state.sidebar_visible == false state = Commands.handle_shortcut(state, %{meta: true, key: "j"}) assert state.panel.visible == true state = Commands.handle_shortcut(state, %{meta: true, key: "1"}) assert state.active_view == :posts state = Commands.handle_shortcut(state, %{meta: true, key: "2"}) assert state.active_view == :media state = Commands.handle_shortcut(state, %{meta: true, key: "w"}) assert state.tabs == [] assert state.editor_route == :dashboard state = Commands.handle_shortcut(state, %{meta: true, key: ","}) assert state.editor_route == :settings end test "resizing is clamped to the shell limits and dirty flags only apply to post tabs" do state = Workbench.new() |> Workbench.set_sidebar_width(999) |> Workbench.set_assistant_sidebar_width(120) |> Workbench.open_tab(:media, "media-1", :pin) |> Workbench.mark_dirty(:media, "media-1") |> Workbench.open_tab(:post, "post-1", :pin) |> Workbench.mark_dirty(:post, "post-1") assert state.sidebar_width == 500 assert state.assistant_sidebar_width == 280 assert Workbench.dirty?(state, :media, "media-1") == false assert Workbench.dirty?(state, :post, "post-1") == true end test "desktop shell keeps the compact frame metrics and live bootstrap assets" do css = css_source() live_js = File.read!("/Users/gb/Projects/bDS2/assets/js/app.js") template = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/index.html.heex") assert File.exists?("/Users/gb/Projects/bDS2/assets/css/shell.css") assert File.exists?("/Users/gb/Projects/bDS2/assets/js/app.js") assert css =~ ".window-titlebar" assert css =~ "height: 34px" assert css =~ "width: 48px" assert css =~ "height: 35px" assert css =~ "height: 22px" assert live_js =~ "new LiveSocket" assert live_js =~ "Socket" assert template =~ "data-project-id={@projects.active_project_id || \"\"}" assert template =~ "data-workbench-session={encoded_workbench_session(@workbench)}" end test "phase 4 css defines normalized shared primitives" do css = css_source() assert css =~ ".ui-button {" assert css =~ ".ui-button-secondary {" assert css =~ ".ui-button-danger {" assert css =~ ".ui-input," assert css =~ ".ui-textarea {" assert css =~ ".ui-tab {" assert css =~ ".ui-badge {" assert css =~ ".ui-panel-entry {" assert css =~ ".ui-empty-state {" end test "tailwind source keeps theme tokens and shared component primitives" do css = css_source() app_css = File.read!("/Users/gb/Projects/bDS2/assets/css/app.css") assert app_css =~ ~s|@import "tailwindcss" source(none);| assert css =~ "@theme" assert css =~ "--color-shell-bg:" assert css =~ "--font-sans:" assert css =~ "@layer components" assert css =~ ".btn-base" assert css =~ ".btn-theme-primary" assert css =~ ".btn-theme-danger" assert css =~ ".panel-entry" assert css =~ ".monaco-host" end test "live javascript is split into focused Phoenix asset modules" do app_js = File.read!("/Users/gb/Projects/bDS2/assets/js/app.js") assert app_js =~ ~s(import { createHooks } from "./hooks/index.js";) assert app_js =~ ~s(import { syncTitlebarOverlayInsets } from "./bridges/titlebar_overlay.js";) assert app_js =~ ~s(import { createMenuRuntimeCommandRunner } from "./bridges/menu_runtime.js";) assert app_js =~ ~s(import { createMonacoServices } from "./monaco/services.js";) assert File.exists?("/Users/gb/Projects/bDS2/assets/js/hooks/index.js") assert File.exists?("/Users/gb/Projects/bDS2/assets/js/bridges/titlebar_overlay.js") assert File.exists?("/Users/gb/Projects/bDS2/assets/js/bridges/menu_runtime.js") assert File.exists?("/Users/gb/Projects/bDS2/assets/js/monaco/services.js") end test "top level shell render uses utility classes for common layout" do template = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/index.html.heex") assert template =~ ~s(class="app flex h-full w-full flex-col") assert template =~ ~s(class="app-main flex min-h-0 flex-1 overflow-hidden") assert template =~ ~s(class="app-content flex min-w-0 flex-1 flex-col overflow-hidden") assert template =~ ~s(class="tab-bar flex h-[35px] shrink-0 items-center overflow-hidden") end test "desktop shell css keeps editor and help docs on the VS Code dark surface" do css = css_source() assert css =~ ".post-editor .post-editor-markdown-surface" assert css =~ ".scripts-monaco.monaco-editor-shell" assert css =~ ".templates-monaco.monaco-editor-shell" assert css =~ ".help-doc-markdown" assert css =~ "background: var(--vscode-editor-background);" assert css =~ "color: var(--vscode-editor-foreground);" refute Regex.match?(~r/\.sidebar-item\s*\{[^}]*background:\s*var\(--panel-2\)/s, css) refute Regex.match?(~r/\.sidebar-item\s*\{[^}]*color:\s*var\(--ink\)/s, css) end test "desktop help documentation keeps the old markdown viewer styling contract" do css = css_source() assert css =~ ".help-doc-view" assert css =~ ".help-doc-view .misc-editor-content" assert css =~ ".documentation-article" assert css =~ ".documentation-content.markdown-body h1" assert css =~ ".documentation-content.markdown-body table" assert css =~ ".documentation-content.markdown-body ul" assert css =~ "background: var(--doc-surface);" assert css =~ "box-shadow: 0 10px 24px rgba(0, 0, 0, 0.18);" end test "desktop settings editor keeps the old preferences styling contract" do css = css_source() assert css =~ ".settings-view" assert css =~ ".settings-header" assert css =~ ".settings-content" assert css =~ ".setting-section" assert css =~ ".setting-section-header" assert css =~ ".setting-section-content" assert css =~ ".setting-row" assert css =~ ".setting-control" assert css =~ "grid-template-columns: minmax(180px, 240px) minmax(0, 1fr);" assert css =~ "background: var(--panel-2, #252526);" assert css =~ "border: 1px solid var(--line, #3c3c3c);" end test "monaco editor styling forces the internal editor surface to the dark theme" do css = css_source() live_js = File.read!("/Users/gb/Projects/bDS2/assets/js/app.js") assert css =~ ".monaco-editor .margin" assert css =~ ".monaco-editor-background" assert css =~ "background-color: var(--vscode-editor-background) !important;" assert css =~ ".monaco-editor .view-line" assert live_js =~ "base: \"vs-dark\"" assert live_js =~ "monaco.editor.setTheme(\"bds-theme\");" end test "monaco editor hook forces first visible layout and textarea content sync" do live_js = File.read!("/Users/gb/Projects/bDS2/assets/js/app.js") assert live_js =~ "this.syncEditorFromTextarea" assert live_js =~ "this.layoutEditorSoon" assert live_js =~ "this.waitForMonacoVisibleSize" assert live_js =~ "ResizeObserver" assert live_js =~ "requestAnimationFrame" assert live_js =~ "this.editor.layout()" assert live_js =~ "this.syncEditorFromTextarea()" end test "monaco theme uses normalized app colors before defining the dark theme" do live_js = File.read!("/Users/gb/Projects/bDS2/assets/js/app.js") assert live_js =~ "normalizeMonacoColor" assert live_js =~ "base: \"vs-dark\"" assert live_js =~ "\"editor.background\": background" assert live_js =~ "monaco.editor.defineTheme(\"bds-theme\"" end test "desktop shell assets persist workbench layout per project" do live_js = File.read!("/Users/gb/Projects/bDS2/assets/js/app.js") live_ex = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live.ex") session_util_ex = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/session_util.ex") assert live_js =~ "bds-workbench-" assert live_js =~ "restore_workbench_session" assert live_js =~ "dataset.workbenchSession" assert live_ex =~ ~s(def handle_event("restore_workbench_session") assert session_util_ex =~ "Session.restore" assert live_ex =~ "encoded_workbench_session" end test "desktop shell assets reveal loaded media sidebar thumbnails" do css = css_source() live_js = File.read!("/Users/gb/Projects/bDS2/assets/js/app.js") assert css =~ ".media-thumbnail.is-loaded .media-thumbnail-image" assert css =~ ".media-thumbnail.is-loaded .media-thumbnail-fallback" assert live_js =~ "media-thumbnail-image" assert live_js =~ "classList.add(\"is-loaded\")" assert live_js =~ "classList.remove(\"is-loaded\")" end test "desktop shell css keeps the status bar and hidden menu alignment rules" do css = css_source() assert css =~ ".window-titlebar-menu-bar.is-hidden" assert css =~ "--vscode-statusBar-background: #007acc" assert css =~ ".status-bar-left," assert css =~ ".status-shell-controls" assert css =~ ".status-shell-toggle-button" assert css =~ "gap: 4px" assert css =~ "padding: 0 8px" assert css =~ "height: 100%" refute css =~ "background: var(--status)" assert css =~ ".status-bar-language-select" assert css =~ ".status-bar-item.language-badge" assert css =~ ".status-bar-item.offline-badge" end test "desktop shell assets keep old activity, tab, focus, and titlebar overlay parity rules" do css = css_source() live_js = File.read!("/Users/gb/Projects/bDS2/assets/js/app.js") titlebar_js = File.read!("/Users/gb/Projects/bDS2/assets/js/bridges/titlebar_overlay.js") template = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/index.html.heex") assert css =~ "color: var(--vscode-activityBar-foreground)" assert css =~ ".activity-bar-badge" assert css =~ ".tab-actions" assert css =~ ".tab-dirty-indicator" assert css =~ ".tab.dirty .tab-close" assert css =~ ".tab:focus-visible" assert css =~ ".window-titlebar-action-button:focus" assert css =~ ".panel-tab.active" assert css =~ "border-bottom-color: var(--vscode-focusBorder);" assert css =~ ".sidebar-section-header" assert css =~ "justify-content: space-between" assert css =~ "align-items: center" assert css =~ "padding-right: calc(10px + var(--bds-titlebar-overlay-right, 0px));" assert titlebar_js =~ "windowControlsOverlay" assert titlebar_js =~ "geometrychange" assert titlebar_js =~ "--bds-titlebar-overlay-left" assert live_js =~ "dataset.shortcuts" assert live_js =~ "addEventListener(\"keydown\", this.handleShortcutKeyDown, true)" assert live_js =~ "event.preventDefault()" assert live_js =~ "this.pushEvent(\"shortcut\"" assert template =~ "data-shortcuts={encoded_shortcuts(@client_shortcuts)}" assert template =~ "data-testid=\"status-shell-controls\"" assert template =~ "activity-bar-badge" assert template =~ "tab-actions" assert template =~ "tab-dirty-indicator" end test "desktop shell keeps sidebar delete buttons visible in the default state" do css = css_source() assert Regex.match?(~r/\.sidebar-delete-button\s*\{[^}]*opacity:\s*1;/s, css) refute Regex.match?(~r/\.sidebar-delete-button\s*\{[^}]*opacity:\s*0;/s, css) end test "desktop shell css keeps the old activity bar active marker contrast" do css = css_source() assert css =~ "--vscode-activityBar-foreground: #ffffff" assert css =~ ".activity-bar-item:hover {" assert css =~ ".activity-bar-item.active::before {" assert css =~ "width: 2px;" assert css =~ "background-color: var(--vscode-activityBar-foreground);" end test "desktop shell assets keep legacy titlebar menu keyboard and anchoring behavior" do css = css_source() live_js = File.read!("/Users/gb/Projects/bDS2/assets/js/app.js") live_ex = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live.ex") template = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/index.html.heex") assert css =~ ".window-titlebar-menu-group {" assert css =~ "left: 0;" refute live_js =~ "--bds-titlebar-menu-left" refute live_js =~ "syncTitlebarMenuAnchor" refute live_js =~ "handleTitlebarMenuKeyDown" refute live_js =~ "keyboardMenuIndex" assert template =~ "phx-window-keydown={if(@titlebar_menu_group, do: \"titlebar_menu_keydown\")}" assert template =~ "window-titlebar-menu-group" assert live_ex =~ ~s(def handle_event("titlebar_menu_keydown") assert live_ex =~ "titlebar_menu_item_index" end test "desktop shell css keeps the old media editor layout contract" do css = css_source() template = File.read!( "/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex" ) assert css =~ ".media-preview {" assert css =~ "min-height: 300px;" assert css =~ ".media-preview-image {" assert css =~ "width: 100%;" assert css =~ "height: 100%;" assert css =~ "box-sizing: border-box;" assert css =~ ".media-preview-image img {" assert css =~ "object-fit: contain;" assert css =~ ".media-details {" assert css =~ "width: 320px;" assert css =~ ".media-details textarea {" assert css =~ "resize: vertical;" assert css =~ ".linked-posts-section label {" assert css =~ "justify-content: space-between;" assert css =~ ".add-link-btn {" assert css =~ "font-size: 11px;" assert css =~ ".post-picker {" assert css =~ "max-height: 250px;" assert css =~ ".post-picker-search input {" assert css =~ "padding: 6px 10px;" assert css =~ ".linked-post-item:hover .unlink-btn {" assert css =~ "opacity: 1;" assert Regex.match?( ~r/class="secondary quick-actions-btn ui-button ui-button-secondary inline-flex items-center gap-2".*?⚡<\/span>\s*<%= dgettext\("ui", "Quick Actions"\) %><\/span>/s, template ) assert template =~ ~s(class="quick-action-text flex min-w-0 flex-1 flex-col") assert template =~ ~s(class="quick-action-icon">🤖) assert Regex.match?( ~r/class="quick-action-text[^"]*">\s*<%= dgettext\("ui", "AI Suggestions"\) %><\/strong>.*?<\/span>\s*🤖<\/span>/s, template ) refute template =~ ~s|🤖 | end test "desktop shell status task area keeps the compact running-task markup" do template = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/index.html.heex") assert template =~ "task-message-text" assert template =~ "task-spinner" assert template =~ "status-bar-count" end test "desktop shell css keeps old panel and output density" do css = css_source() assert css =~ ".panel-content {" assert css =~ "padding: 8px;" assert css =~ ".task-spinner {" assert css =~ ".task-message-text" assert css =~ ".output-entry {" assert css =~ "background-color: var(--vscode-sideBar-background);" assert css =~ "border-radius: 4px;" assert css =~ ".task-entry {" assert css =~ "background-color: var(--vscode-sideBar-background);" end test "desktop shell css keeps legacy sidebar header and post list layout" do css = css_source() assert css =~ ".sidebar-section {" assert css =~ "margin-bottom: 4px;" assert css =~ "border-left: 2px solid transparent;" assert css =~ "border-left-color: var(--vscode-focusBorder);" assert css =~ ".sidebar-section-header {" assert css =~ "justify-content: space-between;" assert css =~ "font-weight: 600;" assert css =~ ".sidebar-item {" assert css =~ "align-items: flex-start;" assert css =~ "gap: 8px;" end test "desktop shell assets keep the assistant sidebar chat surface contract" do css = css_source() template = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/index.html.heex") assert css =~ ".assistant-sidebar-context" assert css =~ ".assistant-sidebar-prompt" assert css =~ ".assistant-sidebar-start-button" assert css =~ ".assistant-sidebar-message" assert template =~ "data-testid=\"assistant-context\"" assert template =~ "data-testid=\"assistant-prompt-form\"" assert template =~ "data-testid=\"assistant-prompt-input\"" assert template =~ "data-testid=\"assistant-start-button\"" assert template =~ "assistant-sidebar-transcript" end test "desktop shell assets expose the shared overlay render contract" do css = css_source() live_ex = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live.ex") template = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/index.html.heex") overlay_ex = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/overlay_components.ex") overlay_template = File.read!( "/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex" ) assert template =~ "render_editor_toolbar(assigns)" assert template =~ "⚡<\/span>\s*<%= dgettext\("ui", "Quick Actions"\) %><\/span>/s, post_template ) refute live_ex =~ "defp update_post_editor(" refute live_ex =~ "defp persist_post_editor(" refute live_ex =~ "defp discard_post_editor(" refute live_ex =~ "defp delete_post_editor(" refute live_ex =~ "defp update_post_editor_expanded(" end test "desktop shell keeps media editor logic in the feature slice" do live_ex = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live.ex") template = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/index.html.heex") media_editor_ex = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/media_editor.ex") assert template =~ "<.live_component module={MediaEditor}" assert media_editor_ex =~ "defp build_data(socket)" refute live_ex =~ "defp update_media_editor(" refute live_ex =~ "defp persist_media_editor(" refute live_ex =~ "defp delete_media_editor(" end test "desktop shell keeps sidebar logic in its own slice" do live_ex = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live.ex") template = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/index.html.heex") sidebar_ex = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/sidebar_components.ex") sidebar_state_ex = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/sidebar_state.ex") assert template =~ "