defmodule BDS.Desktop.ShellLiveTest do
use ExUnit.Case, async: false
import Phoenix.ConnTest
import Phoenix.LiveViewTest
alias BDS.Persistence
alias BDS.AI
alias BDS.Media
alias BDS.Metadata
alias BDS.Posts
alias BDS.Posts.Post
alias BDS.Projects
alias BDS.Repo
alias BDS.Scripts
alias BDS.Templates
alias BDS.Tags
alias BDS.ImportDefinitions
alias BDS.UI.{Session, Workbench}
defmodule FakeEndpointModelHttpClient do
def get("https://api.example.test/v1/models", _headers) do
{:ok,
%{status: 200, headers: %{}, body: Jason.encode!(%{"data" => [%{"id" => "gpt-4.1"}, %{"id" => "gpt-4.1-mini"}]})}}
end
def get("http://localhost:11434/v1/models", _headers) do
{:ok,
%{status: 200, headers: %{}, body: Jason.encode!(%{"data" => [%{"id" => "llama3.3"}, %{"id" => "llava:latest"}]})}}
end
def get(_url, _headers), do: {:error, :not_found}
end
@endpoint BDS.Desktop.Endpoint
setup do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, {:shared, self()})
temp_dir = Path.join(System.tmp_dir!(), "bds-shell-live-#{System.unique_integer([:positive])}")
File.mkdir_p!(temp_dir)
on_exit(fn -> File.rm_rf(temp_dir) end)
{:ok, project} = Projects.create_project(%{name: "Shell Project", data_path: temp_dir})
{:ok, _project} = Projects.set_active_project(project.id)
original_shell_platform = Application.get_env(:bds, :shell_platform)
original_git_remote_state_provider = Application.get_env(:bds, :git_remote_state_provider)
original_ai_http_client = Application.get_env(:bds, :ai_http_client)
on_exit(fn ->
if is_nil(original_shell_platform) do
Application.delete_env(:bds, :shell_platform)
else
Application.put_env(:bds, :shell_platform, original_shell_platform)
end
if is_nil(original_git_remote_state_provider) do
Application.delete_env(:bds, :git_remote_state_provider)
else
Application.put_env(:bds, :git_remote_state_provider, original_git_remote_state_provider)
end
if is_nil(original_ai_http_client) do
Application.delete_env(:bds, :ai_http_client)
else
Application.put_env(:bds, :ai_http_client, original_ai_http_client)
end
end)
%{project: project, temp_dir: temp_dir}
end
test "sidebar headers expose old-app create actions for posts, media, scripts, templates, and imports" do
{:ok, view, html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
assert html =~ ~s(data-testid="sidebar-create-action")
assert html =~ ~s(data-sidebar-action="post")
assert html =~ ~s(data-testid="sidebar-filter-toggle")
html = render_click(view, "select_view", %{"view" => "media"})
assert html =~ ~s(data-sidebar-action="media")
assert html =~ ~s(data-testid="sidebar-filter-toggle")
html =
view
|> element("[data-testid='activity-button'][data-view='scripts']")
|> render_click()
assert html =~ ~s(data-sidebar-action="script")
html =
view
|> element("[data-testid='activity-button'][data-view='templates']")
|> render_click()
assert html =~ ~s(data-sidebar-action="template")
html =
view
|> element("[data-testid='activity-button'][data-view='import']")
|> render_click()
assert html =~ ~s(data-sidebar-action="import")
end
test "sidebar create actions follow the old-app post, script, template, and import flows", %{project: project} do
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
post_count_before = Repo.aggregate(Post, :count, :id)
script_count_before = Repo.aggregate(BDS.Scripts.Script, :count, :id)
template_count_before = Repo.aggregate(BDS.Templates.Template, :count, :id)
import_count_before = Repo.aggregate(ImportDefinitions.ImportDefinition, :count, :id)
html =
view
|> element("[data-testid='sidebar-create-action'][data-sidebar-action='post']")
|> render_click()
assert Repo.aggregate(Post, :count, :id) == post_count_before + 1
created_post = Repo.one!(Post)
assert created_post.project_id == project.id
assert created_post.title == ""
assert created_post.content == ""
refute html =~ ~s(data-tab-type="post")
_html = render_click(view, "select_view", %{"view" => "scripts"})
html =
view
|> element("[data-testid='sidebar-create-action'][data-sidebar-action='script']")
|> render_click()
assert Repo.aggregate(BDS.Scripts.Script, :count, :id) == script_count_before + 1
created_script = Repo.one!(BDS.Scripts.Script)
assert created_script.project_id == project.id
assert created_script.title == "New Script"
assert created_script.entrypoint == "main"
assert created_script.content == "print(\"new script\")"
assert html =~ ~s(data-tab-type="scripts")
assert html =~ ~s(data-tab-id="#{created_script.id}")
_html = render_click(view, "select_view", %{"view" => "templates"})
html =
view
|> element("[data-testid='sidebar-create-action'][data-sidebar-action='template']")
|> render_click()
assert Repo.aggregate(BDS.Templates.Template, :count, :id) == template_count_before + 1
created_template = Repo.get_by!(BDS.Templates.Template, title: "New Template")
assert created_template.project_id == project.id
assert created_template.title == "New Template"
assert created_template.content == ""
assert html =~ ~s(data-tab-type="templates")
assert html =~ ~s(data-tab-id="#{created_template.id}")
_html = render_click(view, "select_view", %{"view" => "import"})
html =
view
|> element("[data-testid='sidebar-create-action'][data-sidebar-action='import']")
|> render_click()
assert Repo.aggregate(ImportDefinitions.ImportDefinition, :count, :id) == import_count_before + 1
created_definition = Repo.one!(ImportDefinitions.ImportDefinition)
assert created_definition.project_id == project.id
assert created_definition.name == "New Import Definition"
assert html =~ ~s(data-tab-type="import")
assert html =~ ~s(data-tab-id="#{created_definition.id}")
end
test "shell live owns pane visibility and activity selection on the server" do
{:ok, view, html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
assert html =~ ~s(data-testid="sidebar-shell")
assert html =~ ~s(data-testid="status-bar")
assert html =~ ~s(data-testid="status-task-button")
assert html =~ ~s(class="panel-shell is-hidden")
assert html =~ ~s(data-testid="activity-button")
assert html =~ ~s(data-view="posts")
assert html =~ ~s(data-view="media")
assert html =~ ~s(aria-label="Posts")
html = render_click(view, "select_view", %{"view" => "templates"})
assert html =~ ~s(data-view="templates")
assert html =~ ~s(data-active="true")
assert html =~ ~s(aria-label="Templates")
html =
view
|> element("[data-testid='toggle-sidebar']")
|> render_click()
assert html =~ ~s(class="sidebar-shell is-hidden")
html =
view
|> element("[data-testid='toggle-sidebar']")
|> render_click()
refute html =~ ~s(class="sidebar-shell is-hidden")
html =
view
|> element("[data-testid='toggle-panel']")
|> render_click()
assert html =~ ~s(data-region="panel")
refute html =~ ~s(class="panel-shell is-hidden")
assert html =~ ~s(data-testid="panel-close")
html =
view
|> element("[data-testid='panel-close']")
|> render_click()
assert html =~ ~s(class="panel-shell is-hidden")
html =
view
|> element("[data-testid='activity-button'][data-view='media']")
|> render_click()
assert html =~ ~s(aria-label="Media")
assert html =~ ~s(data-view="media")
settings_html =
view
|> element("[data-testid='activity-button'][data-view='settings']")
|> render_click()
assert settings_html =~ ~s(data-testid="sidebar-open-item")
html =
view
|> element("[data-testid='sidebar-open-item'][data-item-id='settings-project']")
|> render_click()
assert html =~ ~s(data-tab-type="settings")
assert html =~ ">Settings<"
html =
view
|> element("[data-testid='tab-close'][data-tab-type='settings'][data-tab-id='settings']")
|> render_click()
refute html =~ ~s(data-tab-type="settings")
assert html =~ ~s(class="tab-bar-empty")
end
test "macos hides the custom titlebar and moves shell toggles into the status bar" do
{:ok, view, html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
refute html =~ ~s(data-testid="window-titlebar")
refute html =~ ~s(data-testid="window-titlebar-menu-bar")
refute html =~ ~s(data-testid="window-titlebar-menu-button")
refute html =~ ~s(data-testid="window-titlebar-menu-dropdown")
assert html =~ ~s(data-testid="status-shell-controls")
assert html =~ ~s(data-testid="toggle-sidebar")
assert html =~ ~s(data-testid="toggle-panel")
assert html =~ ~s(data-testid="toggle-assistant")
html =
view
|> element("[data-testid='toggle-sidebar']")
|> render_click()
assert html =~ ~s(class="sidebar-shell is-hidden")
html =
render_hook(view, "native_menu_action", %{"action" => "edit_preferences"})
assert html =~ ~s(data-tab-type="settings")
assert html =~ ">Settings<"
end
test "titlebar menu matches the old shell contract on windows and linux" do
Application.put_env(:bds, :shell_platform, {:unix, :linux})
{:ok, view, html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
refute html =~ ~s(class="window-titlebar is-mac")
assert html =~ ~s(data-testid="window-titlebar-menu-bar")
assert html =~ ~s(data-testid="window-titlebar-menu-button")
assert html =~ ~s(data-menu-group="file")
assert html =~ ~s(>File<)
html =
view
|> element("[data-testid='window-titlebar-menu-button'][data-menu-group='file']")
|> render_click()
assert html =~ ~s(data-testid="window-titlebar-menu-dropdown")
assert html =~ ~s(data-testid="window-titlebar-menu-item")
assert html =~ ~s(data-menu-action="new_post")
assert html =~ ~s(>New Post<)
html =
view
|> element("[data-testid='window-titlebar-menu-button'][data-menu-group='edit']")
|> render_click()
assert html =~ ~s(data-menu-action="edit_preferences")
html =
view
|> element("[data-testid='window-titlebar-menu-item'][data-menu-action='edit_preferences']")
|> render_click()
assert html =~ ~s(data-tab-type="settings")
assert html =~ ">Settings<"
refute html =~ ~s(data-testid="window-titlebar-menu-dropdown")
end
test "titlebar menu keyboard navigation is owned by liveview on windows and linux" do
Application.put_env(:bds, :shell_platform, {:unix, :linux})
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
html =
view
|> element("[data-testid='window-titlebar-menu-button'][data-menu-group='file']")
|> render_click()
assert html =~ ~s(data-open-menu-group="file")
html = render_keydown(view, "titlebar_menu_keydown", %{key: "ArrowRight"})
assert html =~ ~s(data-open-menu-group="edit")
assert html =~ ~s(data-menu-action="edit_preferences")
html = render_keydown(view, "titlebar_menu_keydown", %{key: "End"})
assert html =~ ~s(class="window-titlebar-menu-item is-keyboard-active")
assert html =~ ~s(data-menu-action="edit_preferences")
html = render_keydown(view, "titlebar_menu_keydown", %{key: "Enter"})
assert html =~ ~s(data-tab-type="settings")
assert html =~ ">Settings<"
refute html =~ ~s(data-testid="window-titlebar-menu-dropdown")
end
test "workbench session restore reopens permanent and transient tabs and selected activity" do
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
session_payload =
Workbench.new()
|> Workbench.click_activity(:media)
|> Workbench.open_tab(:post, "post-1", :pin)
|> Workbench.open_tab(:media, "media-1", :preview)
|> Session.serialize()
html = render_hook(view, "restore_workbench_session", %{"session" => session_payload})
assert html =~ ~s(data-view="media")
assert html =~ ~s(data-active="true")
assert html =~ ~s(data-tab-type="post")
assert html =~ ~s(data-tab-id="post-1")
assert html =~ ~s(data-tab-type="media")
assert html =~ ~s(data-tab-id="media-1")
assert html =~ ~s(class="tab active transient")
end
test "metadata diff refresh reruns after workbench session restore", %{project: project} do
:ok = BDS.Tasks.clear_finished()
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
session_payload =
Workbench.new()
|> Workbench.open_tab(:metadata_diff, "metadata_diff", :pin)
|> Session.serialize()
html = render_hook(view, "restore_workbench_session", %{"session" => session_payload})
assert html =~ ~s(data-tab-type="metadata_diff")
existing_ids = MapSet.new(Enum.map(BDS.Tasks.list_tasks(), & &1.id))
_html =
view
|> element("button[phx-click='rerun_misc_editor']")
|> render_click()
refresh_task = new_task!(existing_ids, "Metadata Diff")
assert refresh_task.group_name == "Maintenance"
completed_task!(refresh_task.id)
send(view.pid, :refresh_task_status)
assert render(view) =~ project.name
end
test "shell live renders the legacy git activity badge from remote behind count" do
Application.put_env(:bds, :git_remote_state_provider, fn _project_id, _opts ->
{:ok, %{local_branch: "main", upstream_branch: "origin/main", has_upstream: true, ahead: 0, behind: 7}}
end)
{:ok, _view, html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
assert html =~ ~s(data-view="git")
assert html =~ ~s(class="activity-bar-badge")
assert html =~ ">7<"
end
test "assistant sidebar exposes context, prompt, and offline-gated transcript" do
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
html =
view
|> element("[data-testid='toggle-assistant']")
|> render_click()
assert html =~ ~s(data-testid="assistant-shell")
assert html =~ ~s(data-testid="assistant-context")
assert html =~ ~s(data-testid="assistant-prompt-form")
assert html =~ ~s(data-testid="assistant-prompt-input")
assert html =~ ~s(data-testid="assistant-start-button")
assert html =~ ~s(>Dashboard<)
html =
render_submit(view, "submit_assistant_prompt", %{
"assistant" => %{"prompt" => "Summarize the current project"}
})
assert html =~ ~s(data-testid="assistant-message-user")
assert html =~ ~s(data-testid="assistant-message-assistant")
assert html =~ "Summarize the current project"
assert html =~ "Automatic AI actions stay gated by airplane mode."
end
test "ai settings expose two openai-compatible endpoints and clear legacy mistral config" do
assert {:ok, _endpoint} =
AI.put_endpoint(:mistral, %{
url: "https://legacy.example.test/v1",
api_key: "legacy-secret",
model: "legacy-model"
})
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
_html =
view
|> element("[data-testid='activity-button'][data-view='settings']")
|> render_click()
html =
view
|> element("[data-testid='sidebar-open-item'][data-item-id='settings-project']")
|> render_click()
assert html =~ "AI"
assert html =~ "Online Endpoint URL"
assert html =~ "Offline Endpoint URL"
assert html =~ "Online API Key"
assert html =~ "Offline API Key"
refute html =~ "Mistral API Key"
refute html =~ "Anthropic / Online API Key"
_html =
render_change(view, "change_settings_ai", %{
"settings_ai" => %{
"online_url" => "https://api.example.test/v1",
"online_api_key" => "online-secret",
"online_chat_model" => "gpt-4.1",
"online_title_model" => "gpt-4.1-mini",
"online_image_analysis_model" => "gpt-4.1-vision",
"offline_url" => "http://localhost:11434/v1",
"offline_api_key" => "",
"offline_chat_model" => "llama3.3",
"offline_title_model" => "llama3.2",
"offline_image_analysis_model" => "llava:latest",
"offline_mode" => "true",
"system_prompt" => "You are the local test prompt."
}
})
_html = render_click(view, "save_settings_ai")
assert {:ok, online_endpoint} = AI.get_endpoint(:online)
assert online_endpoint.url == "https://api.example.test/v1"
assert online_endpoint.api_key == "online-secret"
assert online_endpoint.model == "gpt-4.1"
assert {:ok, offline_endpoint} = AI.get_endpoint(:airplane)
assert offline_endpoint.url == "http://localhost:11434/v1"
assert offline_endpoint.api_key in [nil, ""]
assert offline_endpoint.model == "llama3.3"
assert {:ok, nil} = AI.get_endpoint(:mistral)
assert AI.airplane_mode?()
assert {:ok, "gpt-4.1"} = AI.get_model_preference(:chat)
assert {:ok, "gpt-4.1-mini"} = AI.get_model_preference(:title)
assert {:ok, "gpt-4.1-vision"} = AI.get_model_preference(:image_analysis)
assert {:ok, "llama3.3"} = AI.get_model_preference(:airplane_chat)
assert {:ok, "llama3.2"} = AI.get_model_preference(:airplane_title)
assert {:ok, "llava:latest"} = AI.get_model_preference(:airplane_image_analysis)
end
test "ai settings refresh models from the configured endpoints" do
Application.put_env(:bds, :ai_http_client, FakeEndpointModelHttpClient)
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
_html =
view
|> element("[data-testid='activity-button'][data-view='settings']")
|> render_click()
html =
view
|> element("[data-testid='sidebar-open-item'][data-item-id='settings-project']")
|> render_click()
assert html =~ "Refresh Online Models"
assert html =~ "Refresh Offline Models"
_html =
render_change(view, "change_settings_ai", %{
"settings_ai" => %{
"online_url" => "https://api.example.test/v1",
"offline_url" => "http://localhost:11434/v1"
}
})
html =
view
|> element("button[phx-click='refresh_settings_ai_models'][phx-value-endpoint='online']")
|> render_click()
assert html =~ ~s()
assert html =~ ~s()
html =
view
|> element("button[phx-click='refresh_settings_ai_models'][phx-value-endpoint='airplane']")
|> render_click()
assert html =~ ~s()
assert html =~ ~s()
end
test "status bar airplane toggle persists the active ai mode" do
assert :ok = AI.set_airplane_mode(false)
{:ok, view, html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
refute html =~ ~s(status-bar-item offline-badge active)
refute AI.airplane_mode?()
html =
view
|> element("[data-testid='status-offline-button']")
|> render_click()
assert html =~ ~s(status-bar-item offline-badge active)
assert AI.airplane_mode?()
html =
view
|> element("[data-testid='status-offline-button']")
|> render_click()
refute html =~ ~s(status-bar-item offline-badge active)
refute AI.airplane_mode?()
end
test "sidebar open supports preview and pin intents for entity tabs" do
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
html =
render_click(view, "open_sidebar_item", %{
"route" => "post",
"id" => "post-1",
"title" => "First Post",
"subtitle" => "draft"
})
assert html =~ ~s(data-tab-type="post")
assert html =~ ~s(data-tab-id="post-1")
assert html =~ ~s(class="tab active transient")
html =
render_click(view, "pin_sidebar_item", %{
"route" => "post",
"id" => "post-1",
"title" => "First Post",
"subtitle" => "draft"
})
assert html =~ ~s(data-tab-id="post-1")
refute html =~ ~s(class="tab active transient")
html =
render_click(view, "open_sidebar_item", %{
"route" => "post",
"id" => "page-1",
"title" => "About Page",
"subtitle" => "page"
})
assert html =~ ~s(data-tab-id="post-1")
assert html =~ ~s(data-tab-id="page-1")
assert String.contains?(html, ">First Post<")
assert String.contains?(html, ">About Page<")
_html =
render_click(view, "pin_sidebar_item", %{
"route" => "media",
"id" => "media-1",
"title" => "hero.png",
"subtitle" => "12 KB"
})
html =
render_click(view, "open_sidebar_item", %{
"route" => "media",
"id" => "media-2",
"title" => "cover.png",
"subtitle" => "8 KB"
})
assert html =~ ~s(data-tab-id="media-1")
assert html =~ ~s(data-tab-id="media-2")
assert String.contains?(html, ">hero.png<")
assert String.contains?(html, ">cover.png<")
end
test "global shortcuts route through the shared command model" do
{:ok, view, html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
assert html =~ ~s(data-testid="sidebar-shell")
assert html =~ ~s(class="panel-shell is-hidden")
html = render_keydown(view, "shortcut", %{key: "b", meta: true})
assert html =~ ~s(class="sidebar-shell is-hidden")
html = render_keydown(view, "shortcut", %{key: "j", meta: true})
refute html =~ ~s(class="panel-shell is-hidden")
html = render_keydown(view, "shortcut", %{key: "2", meta: true})
assert html =~ ~s(data-view="media")
html =
render_click(view, "pin_sidebar_item", %{
"route" => "media",
"id" => "media-1",
"title" => "hero.png",
"subtitle" => "12 KB"
})
assert html =~ ~s(data-tab-id="media-1")
html = render_keydown(view, "shortcut", %{key: "w", meta: true})
refute html =~ ~s(data-tab-id="media-1")
end
test "hiding the sidebar collapses its width to zero" do
{:ok, view, html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
assert html =~ ~s(data-testid="sidebar-shell")
assert html =~ ~s(style="width: 280px;")
html =
view
|> element("[data-testid='toggle-sidebar']")
|> render_click()
assert html =~ ~s(class="sidebar-shell is-hidden")
assert html =~ ~s(style="width: 0px;")
end
test "layout hooks sync persisted widths and apply drag resizing" do
{:ok, view, html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
assert html =~ ~s(style="width: 280px;")
html = render_hook(view, "sync_layout", %{"sidebar_width" => 420, "assistant_sidebar_width" => 480})
assert html =~ ~s(data-testid="sidebar-shell")
assert html =~ ~s(style="width: 420px;")
html =
view
|> element("[data-testid='toggle-assistant']")
|> render_click()
assert html =~ ~s(data-testid="assistant-shell")
assert html =~ ~s(style="width: 480px;")
html = render_hook(view, "resize_panel", %{"target" => "sidebar", "width" => 460})
assert html =~ ~s(data-testid="sidebar-shell")
assert html =~ ~s(style="width: 460px;")
end
test "sidebar filters and load more are server-driven", %{project: project} do
seed_sidebar_posts(project.id)
assert {:ok, _tag} = Tags.create_tag(%{project_id: project.id, name: "tech", color: "#112233"})
{:ok, view, html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
assert html =~ ~s(data-testid="sidebar-search-form")
assert html =~ ~s(data-testid="sidebar-filter-toggle")
assert html =~ ~s(class="sidebar-section-header")
assert html =~ ~s(class="sidebar-actions")
assert html =~ ~s(data-testid="sidebar-load-more")
assert html_position(html, ~s(data-testid="sidebar-load-more")) > html_position(html, ">Archived<")
refute html =~ ~s(data-testid="sidebar-filter-tag")
assert html =~ "Alpha Post"
refute html =~ "Overflow Post"
html =
view
|> element("[data-testid='sidebar-filter-toggle']")
|> render_click()
assert html =~ ~s(class="calendar-header collapsible-header collapsed")
assert html =~ ~s(class="filter-header collapsible-header collapsed")
refute html =~ ~s(class="calendar-year-header")
refute html =~ ~s(data-testid="sidebar-filter-tag")
html =
view
|> element("[data-testid='sidebar-filter-tags-header']")
|> render_click()
assert html =~ ~s(class="filter-chip has-color")
assert html =~ ~s(data-testid="sidebar-filter-tag")
html =
view
|> form("[data-testid='sidebar-search-form']", %{sidebar_filters: %{search: "Alpha"}})
|> render_change()
assert html =~ "Alpha Post"
refute html =~ ~s(data-open-title="Beta Post")
html =
view
|> element("[data-testid='sidebar-clear-search']")
|> render_click()
assert html =~ "Beta Post"
html =
view
|> element("[data-testid='sidebar-filter-tag'][data-filter-tag='tech']")
|> render_click()
assert html =~ "Alpha Post"
refute html =~ ~s(data-open-title="Beta Post")
html =
view
|> element("[data-testid='sidebar-clear-filters']")
|> render_click()
assert html =~ "Beta Post"
html =
view
|> element("[data-testid='sidebar-load-more']")
|> render_click()
assert html =~ "Overflow Post"
end
test "project switcher, ui language, dashboard recents, and output log are wired", %{temp_dir: temp_dir} do
{:ok, other_project} = Projects.create_project(%{name: "Second Blog", data_path: Path.join(temp_dir, "second")})
{:ok, recent_post} = Posts.create_post(%{project_id: other_project.id, title: "Recent Shell Post", content: "body"})
{:ok, view, html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
assert html =~ "Shell Project"
refute html =~ "Second Blog"
html =
view
|> element("[data-testid='project-selector-trigger']")
|> render_click()
assert html =~ ~s(data-testid="project-dropdown")
assert html =~ "Second Blog"
html =
view
|> element("[data-testid='project-item'][data-project-id='#{other_project.id}']")
|> render_click()
assert html =~ "Second Blog"
html =
view
|> form("[data-testid='status-language-form']", %{ui_language: "de"})
|> render_change()
assert html =~ "Beiträge durchsuchen..."
html =
view
|> element("[data-testid='recent-post-item'][data-post-id='#{recent_post.id}']")
|> render_click()
assert html =~ ~s(data-tab-type="post")
assert html =~ ~s(data-tab-id="#{recent_post.id}")
assert html =~ "Recent Shell Post"
html =
render_click(view, "select_panel_tab", %{"tab" => "output"})
assert html =~ "Activated Second Blog"
end
test "task button opens tasks and post panels render real link and git data", %{project: project, temp_dir: temp_dir} do
{:ok, target} = Posts.create_post(%{project_id: project.id, title: "Target Post", content: "target body"})
{:ok, target} = Posts.publish_post(target.id)
target_href = canonical_post_href(target)
{:ok, source} =
Posts.create_post(%{project_id: project.id, title: "Linking Source", content: "See [Target](#{target_href})"})
{:ok, source} = Posts.publish_post(source.id)
:ok = Posts.rebuild_post_links(project.id)
init_git_repo!(temp_dir, "Add published posts")
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
html =
render_click(view, "pin_sidebar_item", %{
"route" => "post",
"id" => target.id,
"title" => "Target Post",
"subtitle" => "published"
})
assert html =~ "Target Post"
html = render_click(view, "select_panel_tab", %{"tab" => "post_links"})
assert html =~ "Backlinks"
assert html =~ source.title
html = render_click(view, "select_panel_tab", %{"tab" => "git_log"})
assert html =~ "Add published posts"
html = render_click(view, "select_panel_tab", %{"tab" => "output"})
refute html =~ ~s(class="panel-shell is-hidden")
html =
view
|> element("[data-testid='status-task-button']")
|> render_click()
refute html =~ ~s(class="panel-shell is-hidden")
assert html =~ ~s(