defmodule BDS.UI.WorkbenchTest do use ExUnit.Case, async: true alias BDS.UI.Registry alias BDS.UI.MenuBar alias BDS.UI.Workbench test "preview tabs reuse the transient slot per type and route editors through the active tab" do state = Workbench.new() |> Workbench.open_tab(:post, "post-1", :preview) assert state.active_tab == {:post, "post-1"} assert state.editor_route == :post assert [%{type: :post, id: "post-1", is_transient: true}] = state.tabs state = Workbench.open_tab(state, :post, "post-2", :preview) assert state.active_tab == {:post, "post-2"} assert state.editor_route == :post assert [%{type: :post, id: "post-2", is_transient: true}] = state.tabs end test "opening an existing preview tab as pinned upgrades it instead of duplicating it" do state = Workbench.new() |> Workbench.open_tab(:post, "post-1", :preview) |> Workbench.open_tab(:post, "post-1", :pin) assert state.active_tab == {:post, "post-1"} assert [%{type: :post, id: "post-1", is_transient: false}] = state.tabs end test "singleton tabs deduplicate and are never transient" do state = Workbench.new() |> Workbench.open_tab(:settings, "settings", :preview) |> Workbench.open_tab(:settings, "settings", :pin) assert state.active_tab == {:settings, "settings"} assert [%{type: :settings, id: "settings", is_transient: false}] = state.tabs end test "background opening keeps the active editor unchanged while still applying tab policy" do state = Workbench.new() |> Workbench.open_tab(:post, "post-1", :pin) |> Workbench.open_tab_in_background(:media, "media-1", :preview) assert state.active_tab == {:post, "post-1"} assert state.editor_route == :post assert Enum.map(state.tabs, &{&1.type, &1.id, &1.is_transient}) == [ {:post, "post-1", false}, {:media, "media-1", true} ] end test "closing the active tab activates the next tab at the same index and falls back to dashboard" do state = Workbench.new() |> Workbench.open_tab(:post, "post-1", :pin) |> Workbench.open_tab(:media, "media-1", :pin) |> Workbench.open_tab(:chat, "conversation-1", :pin) state = Workbench.close_tab(state, :media, "media-1") assert state.active_tab == {:chat, "conversation-1"} assert state.editor_route == :chat state = state |> Workbench.close_tab(:chat, "conversation-1") |> Workbench.close_tab(:post, "post-1") assert state.tabs == [] assert state.active_tab == nil assert state.editor_route == :dashboard end test "activity clicks switch sidebar views and toggle visibility when re-clicking the active view" do state = Workbench.new() assert state.active_view == :posts assert state.sidebar_visible == true state = Workbench.click_activity(state, :posts) assert state.sidebar_visible == false assert state.active_view == :posts state = Workbench.click_activity(state, :media) assert state.sidebar_visible == true assert state.active_view == :media buttons = Workbench.activity_buttons(state, 108) media_button = Enum.find(buttons, &(&1.id == :media)) git_button = Enum.find(buttons, &(&1.id == :git)) assert media_button.active == true assert git_button.badge.display == "99+" end test "panel tab availability falls back to tasks when the active editor no longer supports the panel" do state = Workbench.new() |> Workbench.set_panel_visible(true) |> Workbench.set_panel_tab(:post_links) |> Workbench.open_tab(:post, "post-1", :pin) assert state.panel.active_tab == :post_links state = Workbench.open_tab(state, :settings, "settings", :pin) assert state.editor_route == :settings assert state.panel.active_tab == :tasks state = Workbench.set_panel_tab(state, :git_log) state = Workbench.open_tab(state, :media, "media-1", :pin) assert state.panel.active_tab == :git_log end test "status bar data follows the active editor kind" do state = Workbench.new() |> Workbench.open_tab(:post, "post-1", :pin) status = Workbench.status_bar(state, post_count: 12, media_count: 7, theme_badge: "zinc", ui_language: "de", offline_mode: true, running_task_message: "Generating site", running_task_overflow: 2, active_post_status: :draft, token_usage: %{input_tokens: 10, output_tokens: 20, cache_read_tokens: 3} ) assert status.left.running_task_message == "Generating site" assert status.right.post_status == "draft" assert status.right.post_count == "12 posts" assert status.right.media_count == "7 media" assert status.right.token_usage == nil state = Workbench.open_tab(state, :chat, "conversation-1", :pin) status = Workbench.status_bar(state, post_count: 12, media_count: 7, theme_badge: "zinc", ui_language: "de", offline_mode: true, token_usage: %{input_tokens: 10, output_tokens: 20, cache_read_tokens: 3} ) assert status.right.post_status == nil assert status.right.token_usage == %{input_tokens: 10, output_tokens: 20, cache_read_tokens: 3} end test "menu commands expose generic shell controls through a shared command model" do state = Workbench.new(sidebar_visible: false, panel_visible: false) groups = MenuBar.default_groups(dev_mode?: false) item_ids = fn items -> items |> Enum.reject(&Map.get(&1, :separator, false)) |> Enum.map(& &1.id) end assert Enum.map(groups, & &1.id) == [:file, :edit, :view, :blog, :help] view_group = Enum.find(groups, &(&1.id == :view)) command_ids = item_ids.(view_group.items) assert :toggle_sidebar in command_ids assert :toggle_panel in command_ids refute :toggle_dev_tools in command_ids blog_group = Enum.find(groups, &(&1.id == :blog)) assert :metadata_diff in item_ids.(blog_group.items) state = MenuBar.execute(state, :toggle_sidebar) state = MenuBar.execute(state, :toggle_panel) state = MenuBar.execute(state, :view_media) assert state.sidebar_visible == true assert state.panel.visible == true assert state.active_view == :media end test "shared menu command routing covers every sidebar view and singleton editor route" do sidebar_views = Registry.sidebar_views() state = Enum.reduce(sidebar_views, Workbench.new(sidebar_visible: false), fn view, acc -> command = String.to_atom("view_#{view.id}") next = MenuBar.execute(acc, command) assert next.active_view == view.id assert next.sidebar_visible == true %{next | sidebar_visible: false} end) singleton_routes = Registry.editor_routes() |> Enum.filter(& &1.singleton) |> Enum.reject(&(&1.id == :dashboard)) final_state = Enum.reduce(singleton_routes, state, fn route, acc -> command = String.to_atom("open_#{route.id}") next = MenuBar.execute(acc, command) assert next.active_tab == {route.id, Atom.to_string(route.id)} assert next.editor_route == route.id next end) assert Enum.any?(final_state.tabs, &(&1.type == :settings and &1.id == "settings")) assert Enum.any?(final_state.tabs, &(&1.type == :menu_editor and &1.id == "menu_editor")) assert Enum.any?(final_state.tabs, &(&1.type == :find_duplicates and &1.id == "find_duplicates")) end end