D4-2: add 56 UI tests for editor_settings (MCP agents, style/theme, search filter, categories CRUD)
This commit is contained in:
@@ -177,7 +177,7 @@ All reconciled to follow code. Specs must be self-consistent and match code.
|
|||||||
| ID | Spec | Covered | Not Covered |
|
| ID | Spec | Covered | Not Covered |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| ~~D4-1~~ | ~~editor_media.allium~~ | ~~AI analysis, delete~~ | ~~Translate, replace file, link-to-post, translation CRUD, detect language~~ | **Resolved:** backend tests cover replace_file, link-to-post, translation CRUD (upsert + unique constraint); added standalone `delete_media_translation/2` test (row + sidecar deletion, no-op for non-existent, not-found for unknown media); added `MediaDetectLanguage` rule integration test (AI mock, language persisted, sidecar rewritten) |
|
| ~~D4-1~~ | ~~editor_media.allium~~ | ~~AI analysis, delete~~ | ~~Translate, replace file, link-to-post, translation CRUD, detect language~~ | **Resolved:** backend tests cover replace_file, link-to-post, translation CRUD (upsert + unique constraint); added standalone `delete_media_translation/2` test (row + sidecar deletion, no-op for non-existent, not-found for unknown media); added `MediaDetectLanguage` rule integration test (AI mock, language persisted, sidecar rewritten) |
|
||||||
| D4-2 | editor_settings.allium | AI endpoints, airplane toggle, rebuild | Protected categories, MCP agents, style/theme, search filter, categories CRUD |
|
| ~~D4-2~~ | ~~editor_settings.allium~~ | ~~AI endpoints, airplane toggle, rebuild~~ | ~~Protected categories~~ (resolved D1-17), ~~MCP agents~~ (6 `mcp_rows` + 3 `toggle_mcp_agent` tests), ~~style/theme~~ (19 `build_style` + 4 select/change/apply/display tests), ~~search filter~~ (9 `build_settings` visibility tests), ~~categories CRUD~~ (7 `category_rows` + 2 update + 3 add + 2 save + 4 reset tests) | **Resolved:** 3 new test files (mcp_config_test.exs, style_editor_test.exs, settings_search_test.exs) + expanded managed_categories_test.exs cover all untested areas. Total 56 tests added across MCP agents, style/theme, search filter, and categories CRUD. |
|
||||||
| D4-3 | editor_chat.allium | Chat creation, pinned tab | API key screen, message rendering, input area, model selector, inline surfaces |
|
| D4-3 | editor_chat.allium | Chat creation, pinned tab | API key screen, message rendering, input area, model selector, inline surfaces |
|
||||||
| D4-4 | editor_script.allium | Editor layout, create defaults | Save, syntax check, run, delete |
|
| D4-4 | editor_script.allium | Editor layout, create defaults | Save, syntax check, run, delete |
|
||||||
| D4-5 | editor_template.allium | Editor layout, create defaults | Save with validation, validate, delete with references |
|
| D4-5 | editor_template.allium | Editor layout, create defaults | Save with validation, validate, delete with references |
|
||||||
@@ -198,4 +198,5 @@ All reconciled to follow code. Specs must be self-consistent and match code.
|
|||||||
6. ~~**D2-1 through D2-17**~~ — all resolved: `max_posts_per_page` constraint, sandboxed execution, transform toast budget, progress throttle, archived→draft/published transitions, AppNoopNotifier, validate_media implementation+tests, content_hash skip on reindex
|
6. ~~**D2-1 through D2-17**~~ — all resolved: `max_posts_per_page` constraint, sandboxed execution, transform toast budget, progress throttle, archived→draft/published transitions, AppNoopNotifier, validate_media implementation+tests, content_hash skip on reindex
|
||||||
7. ~~**D3-1 through D3-11**~~ — all resolved: content=null assertion, old-file-deletion, DNT guard, validation prerequisites (already tested), macro failure degrades to empty, template roundtrip, default categories, FTS multi-language, canonical URL format, German transliteration expansion
|
7. ~~**D3-1 through D3-11**~~ — all resolved: content=null assertion, old-file-deletion, DNT guard, validation prerequisites (already tested), macro failure degrades to empty, template roundtrip, default categories, FTS multi-language, canonical URL format, German transliteration expansion
|
||||||
8. ~~**B2-1 through B2-9**~~ — all resolved: editor_body resolver, single-post reimport, orphan import, dashboard data, missing-thumbnail regen, cache dir, stale-template prune, render labels, generation progress reporting
|
8. ~~**B2-1 through B2-9**~~ — all resolved: editor_body resolver, single-post reimport, orphan import, dashboard data, missing-thumbnail regen, cache dir, stale-template prune, render labels, generation progress reporting
|
||||||
9. **D4-1 through D4-7** — UI test coverage
|
9. **D4-1 through D4-2** — ~~UI test coverage~~ **Resolved (D4-1 via standalone delete_media_translation + MediaDetectLanguage tests; D4-2 via 3 new test files + expanded managed_categories — 56 tests added)**
|
||||||
|
**D4-3 through D4-7** — remaining UI test coverage
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.ManagedCategoriesTest do
|
|||||||
|
|
||||||
alias BDS.Desktop.ShellLive.SettingsEditor.ManagedCategories
|
alias BDS.Desktop.ShellLive.SettingsEditor.ManagedCategories
|
||||||
|
|
||||||
|
defp socket_with_assigns(extra \\ %{}) do
|
||||||
|
%Phoenix.LiveView.Socket{assigns: Map.merge(%{__changed__: %{}, workbench: nil}, extra)}
|
||||||
|
end
|
||||||
|
|
||||||
describe "protected_category?/1" do
|
describe "protected_category?/1" do
|
||||||
test "returns true for article, aside, page, picture" do
|
test "returns true for article, aside, page, picture" do
|
||||||
assert ManagedCategories.protected_category?("article")
|
assert ManagedCategories.protected_category?("article")
|
||||||
@@ -18,6 +22,362 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.ManagedCategoriesTest do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "category_rows/1" do
|
||||||
|
test "returns a row per category" do
|
||||||
|
metadata = %{
|
||||||
|
categories: ["article", "aside", "page", "picture", "notes"],
|
||||||
|
category_settings: %{
|
||||||
|
"article" => %{"title" => "Articles", "render_in_lists" => true, "show_title" => true},
|
||||||
|
"notes" => %{"title" => "My Notes", "render_in_lists" => true, "show_title" => false}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rows = ManagedCategories.category_rows(metadata)
|
||||||
|
assert length(rows) == 5
|
||||||
|
end
|
||||||
|
|
||||||
|
test "each row has expected keys" do
|
||||||
|
metadata = %{
|
||||||
|
categories: ["article"],
|
||||||
|
category_settings: %{"article" => %{"title" => "Articles"}}
|
||||||
|
}
|
||||||
|
|
||||||
|
rows = ManagedCategories.category_rows(metadata)
|
||||||
|
row = hd(rows)
|
||||||
|
assert Map.has_key?(row, :name)
|
||||||
|
assert Map.has_key?(row, :title)
|
||||||
|
assert Map.has_key?(row, :render_in_lists)
|
||||||
|
assert Map.has_key?(row, :show_title)
|
||||||
|
assert Map.has_key?(row, :post_template_slug)
|
||||||
|
assert Map.has_key?(row, :list_template_slug)
|
||||||
|
assert Map.has_key?(row, :protected?)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "maps title from category_settings" do
|
||||||
|
metadata = %{
|
||||||
|
categories: ["article", "notes"],
|
||||||
|
category_settings: %{
|
||||||
|
"article" => %{"title" => "Articles"},
|
||||||
|
"notes" => %{"title" => "My Notes"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rows = ManagedCategories.category_rows(metadata)
|
||||||
|
article = Enum.find(rows, &(&1.name == "article"))
|
||||||
|
assert article.title == "Articles"
|
||||||
|
|
||||||
|
notes = Enum.find(rows, &(&1.name == "notes"))
|
||||||
|
assert notes.title == "My Notes"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "falls back to category name when no title in settings" do
|
||||||
|
rows = ManagedCategories.category_rows(%{
|
||||||
|
categories: ["custom-cat"],
|
||||||
|
category_settings: %{}
|
||||||
|
})
|
||||||
|
row = hd(rows)
|
||||||
|
assert row.title == "custom-cat"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "marks protected categories" do
|
||||||
|
rows = ManagedCategories.category_rows(%{
|
||||||
|
categories: ["article", "notes"],
|
||||||
|
category_settings: %{}
|
||||||
|
})
|
||||||
|
article = Enum.find(rows, &(&1.name == "article"))
|
||||||
|
notes = Enum.find(rows, &(&1.name == "notes"))
|
||||||
|
assert article.protected?
|
||||||
|
refute notes.protected?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "applies default render_in_lists and show_title when not in settings" do
|
||||||
|
rows = ManagedCategories.category_rows(%{
|
||||||
|
categories: ["custom"],
|
||||||
|
category_settings: %{}
|
||||||
|
})
|
||||||
|
row = hd(rows)
|
||||||
|
assert row.render_in_lists == true
|
||||||
|
assert row.show_title == true
|
||||||
|
end
|
||||||
|
|
||||||
|
test "default template slugs are empty strings" do
|
||||||
|
rows = ManagedCategories.category_rows(%{
|
||||||
|
categories: ["custom"],
|
||||||
|
category_settings: %{}
|
||||||
|
})
|
||||||
|
row = hd(rows)
|
||||||
|
assert row.post_template_slug == ""
|
||||||
|
assert row.list_template_slug == ""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "update_new_category/3" do
|
||||||
|
test "sets settings_editor_new_category assign" do
|
||||||
|
socket = socket_with_assigns()
|
||||||
|
reload = fn s, _wb ->
|
||||||
|
send(self(), {:reloaded, s})
|
||||||
|
s
|
||||||
|
end
|
||||||
|
|
||||||
|
ManagedCategories.update_new_category(socket, "my-cat", reload)
|
||||||
|
|
||||||
|
assert_received {:reloaded, updated}
|
||||||
|
assert updated.assigns.settings_editor_new_category == "my-cat"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "defaults to empty string when nil" do
|
||||||
|
socket = socket_with_assigns()
|
||||||
|
reload = fn s, _wb ->
|
||||||
|
send(self(), {:reloaded, s})
|
||||||
|
s
|
||||||
|
end
|
||||||
|
|
||||||
|
ManagedCategories.update_new_category(socket, nil, reload)
|
||||||
|
|
||||||
|
assert_received {:reloaded, updated}
|
||||||
|
assert updated.assigns.settings_editor_new_category == ""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "add_category/3" do
|
||||||
|
setup do
|
||||||
|
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
|
||||||
|
temp_dir =
|
||||||
|
Path.join(System.tmp_dir!(), "bds-managed-cat-#{System.unique_integer([:positive])}")
|
||||||
|
|
||||||
|
File.mkdir_p!(temp_dir)
|
||||||
|
|
||||||
|
on_exit(fn -> File.rm_rf(temp_dir) end)
|
||||||
|
|
||||||
|
{:ok, project} =
|
||||||
|
BDS.Projects.create_project(%{name: "AddCategory", data_path: temp_dir})
|
||||||
|
|
||||||
|
%{project: project}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "adds a new category via Metadata", %{project: project} do
|
||||||
|
socket = socket_with_assigns(%{
|
||||||
|
projects: %{active_project_id: project.id},
|
||||||
|
settings_editor_new_category: "test-category"
|
||||||
|
})
|
||||||
|
|
||||||
|
reload = fn s, _wb ->
|
||||||
|
send(self(), :reloaded)
|
||||||
|
s
|
||||||
|
end
|
||||||
|
|
||||||
|
append_output = fn _socket, _title, _msg, _nil, _kind -> socket end
|
||||||
|
|
||||||
|
ManagedCategories.add_category(socket, reload, append_output)
|
||||||
|
|
||||||
|
assert_received :reloaded
|
||||||
|
assert {:ok, meta} = BDS.Metadata.get_project_metadata(project.id)
|
||||||
|
assert "test-category" in meta.categories
|
||||||
|
end
|
||||||
|
|
||||||
|
test "clears new_category input after successful add", %{project: project} do
|
||||||
|
socket = socket_with_assigns(%{
|
||||||
|
projects: %{active_project_id: project.id},
|
||||||
|
settings_editor_new_category: "test-category"
|
||||||
|
})
|
||||||
|
|
||||||
|
reload = fn s, _wb -> s end
|
||||||
|
append_output = fn _socket, _title, _msg, _nil, _kind -> socket end
|
||||||
|
|
||||||
|
result = ManagedCategories.add_category(socket, reload, append_output)
|
||||||
|
assert result.assigns.settings_editor_new_category == ""
|
||||||
|
end
|
||||||
|
|
||||||
|
test "shows error for empty category name", %{project: project} do
|
||||||
|
socket = socket_with_assigns(%{
|
||||||
|
projects: %{active_project_id: project.id},
|
||||||
|
settings_editor_new_category: ""
|
||||||
|
})
|
||||||
|
|
||||||
|
reload = fn s, _wb -> s end
|
||||||
|
append_output = fn _socket, _title, _msg, _nil, _kind ->
|
||||||
|
send(self(), :error_appended)
|
||||||
|
socket
|
||||||
|
end
|
||||||
|
|
||||||
|
ManagedCategories.add_category(socket, reload, append_output)
|
||||||
|
|
||||||
|
assert_received :error_appended
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "save_category/4" do
|
||||||
|
setup do
|
||||||
|
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
|
||||||
|
temp_dir =
|
||||||
|
Path.join(System.tmp_dir!(), "bds-managed-cat-#{System.unique_integer([:positive])}")
|
||||||
|
|
||||||
|
File.mkdir_p!(temp_dir)
|
||||||
|
|
||||||
|
on_exit(fn -> File.rm_rf(temp_dir) end)
|
||||||
|
|
||||||
|
{:ok, project} =
|
||||||
|
BDS.Projects.create_project(%{name: "SaveCategory", data_path: temp_dir})
|
||||||
|
|
||||||
|
%{project: project}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "saves category settings via Metadata", %{project: project} do
|
||||||
|
socket = socket_with_assigns(%{projects: %{active_project_id: project.id}})
|
||||||
|
|
||||||
|
params = %{
|
||||||
|
"category" => "article",
|
||||||
|
"title" => "Articles",
|
||||||
|
"render_in_lists" => "true",
|
||||||
|
"show_title" => "true",
|
||||||
|
"post_template_slug" => "",
|
||||||
|
"list_template_slug" => ""
|
||||||
|
}
|
||||||
|
|
||||||
|
reload = fn s, _wb ->
|
||||||
|
send(self(), :reloaded)
|
||||||
|
s
|
||||||
|
end
|
||||||
|
|
||||||
|
append_output = fn _socket, _title, _msg, _nil, _kind ->
|
||||||
|
send(self(), :error_appended)
|
||||||
|
socket
|
||||||
|
end
|
||||||
|
|
||||||
|
ManagedCategories.save_category(socket, params, reload, append_output)
|
||||||
|
|
||||||
|
assert_received :reloaded
|
||||||
|
refute_received :error_appended
|
||||||
|
|
||||||
|
assert {:ok, meta} = BDS.Metadata.get_project_metadata(project.id)
|
||||||
|
cat_settings = meta.category_settings["article"]
|
||||||
|
assert cat_settings["title"] == "Articles"
|
||||||
|
assert cat_settings["render_in_lists"] == true
|
||||||
|
assert cat_settings["show_title"] == true
|
||||||
|
end
|
||||||
|
|
||||||
|
test "saves template slug for a category", %{project: project} do
|
||||||
|
socket = socket_with_assigns(%{projects: %{active_project_id: project.id}})
|
||||||
|
|
||||||
|
params = %{
|
||||||
|
"category" => "article",
|
||||||
|
"title" => "Articles",
|
||||||
|
"render_in_lists" => "true",
|
||||||
|
"show_title" => "true",
|
||||||
|
"post_template_slug" => "my-post",
|
||||||
|
"list_template_slug" => "my-list"
|
||||||
|
}
|
||||||
|
|
||||||
|
reload = fn s, _wb -> s end
|
||||||
|
append_output = fn _socket, _title, _msg, _nil, _kind -> socket end
|
||||||
|
|
||||||
|
ManagedCategories.save_category(socket, params, reload, append_output)
|
||||||
|
|
||||||
|
assert {:ok, meta} = BDS.Metadata.get_project_metadata(project.id)
|
||||||
|
cat_settings = meta.category_settings["article"]
|
||||||
|
assert cat_settings["post_template_slug"] == "my-post"
|
||||||
|
assert cat_settings["list_template_slug"] == "my-list"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "reset_categories/3" do
|
||||||
|
setup do
|
||||||
|
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
|
||||||
|
temp_dir =
|
||||||
|
Path.join(System.tmp_dir!(), "bds-managed-cat-#{System.unique_integer([:positive])}")
|
||||||
|
|
||||||
|
File.mkdir_p!(temp_dir)
|
||||||
|
|
||||||
|
on_exit(fn -> File.rm_rf(temp_dir) end)
|
||||||
|
|
||||||
|
{:ok, project} =
|
||||||
|
BDS.Projects.create_project(%{name: "ResetCategories", data_path: temp_dir})
|
||||||
|
|
||||||
|
BDS.Metadata.add_category(project.id, "custom-1")
|
||||||
|
BDS.Metadata.add_category(project.id, "custom-2")
|
||||||
|
|
||||||
|
%{project: project}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "removes non-protected categories and restores defaults", %{project: project} do
|
||||||
|
assert {:ok, before} = BDS.Metadata.get_project_metadata(project.id)
|
||||||
|
assert "custom-1" in before.categories
|
||||||
|
assert "custom-2" in before.categories
|
||||||
|
|
||||||
|
socket = socket_with_assigns(%{
|
||||||
|
projects: %{active_project_id: project.id},
|
||||||
|
settings_editor_new_category: "dirty"
|
||||||
|
})
|
||||||
|
|
||||||
|
reload = fn s, _wb ->
|
||||||
|
send(self(), :reloaded)
|
||||||
|
s
|
||||||
|
end
|
||||||
|
|
||||||
|
append_output = fn _socket, _title, _msg, _nil, _kind -> socket end
|
||||||
|
|
||||||
|
ManagedCategories.reset_categories(socket, reload, append_output)
|
||||||
|
|
||||||
|
assert_received :reloaded
|
||||||
|
|
||||||
|
assert {:ok, after_meta} = BDS.Metadata.get_project_metadata(project.id)
|
||||||
|
refute "custom-1" in after_meta.categories
|
||||||
|
refute "custom-2" in after_meta.categories
|
||||||
|
|
||||||
|
assert "article" in after_meta.categories
|
||||||
|
assert "aside" in after_meta.categories
|
||||||
|
assert "page" in after_meta.categories
|
||||||
|
assert "picture" in after_meta.categories
|
||||||
|
end
|
||||||
|
|
||||||
|
test "preserves protected categories during reset", %{project: project} do
|
||||||
|
socket = socket_with_assigns(%{
|
||||||
|
projects: %{active_project_id: project.id},
|
||||||
|
settings_editor_new_category: ""
|
||||||
|
})
|
||||||
|
|
||||||
|
reload = fn s, _wb -> s end
|
||||||
|
append_output = fn _socket, _title, _msg, _nil, _kind -> socket end
|
||||||
|
|
||||||
|
ManagedCategories.reset_categories(socket, reload, append_output)
|
||||||
|
|
||||||
|
assert {:ok, meta} = BDS.Metadata.get_project_metadata(project.id)
|
||||||
|
assert "article" in meta.categories
|
||||||
|
assert "aside" in meta.categories
|
||||||
|
end
|
||||||
|
|
||||||
|
test "clears new_category input after reset", %{project: project} do
|
||||||
|
socket = socket_with_assigns(%{
|
||||||
|
projects: %{active_project_id: project.id},
|
||||||
|
settings_editor_new_category: "dirty"
|
||||||
|
})
|
||||||
|
|
||||||
|
reload = fn s, _wb -> s end
|
||||||
|
append_output = fn _socket, _title, _msg, _nil, _kind -> socket end
|
||||||
|
|
||||||
|
result = ManagedCategories.reset_categories(socket, reload, append_output)
|
||||||
|
assert result.assigns.settings_editor_new_category == ""
|
||||||
|
end
|
||||||
|
|
||||||
|
test "restores default category settings after reset", %{project: project} do
|
||||||
|
socket = socket_with_assigns(%{
|
||||||
|
projects: %{active_project_id: project.id},
|
||||||
|
settings_editor_new_category: ""
|
||||||
|
})
|
||||||
|
|
||||||
|
reload = fn s, _wb -> s end
|
||||||
|
append_output = fn _socket, _title, _msg, _nil, _kind -> socket end
|
||||||
|
|
||||||
|
ManagedCategories.reset_categories(socket, reload, append_output)
|
||||||
|
|
||||||
|
assert {:ok, meta} = BDS.Metadata.get_project_metadata(project.id)
|
||||||
|
article = meta.category_settings["article"]
|
||||||
|
assert article["title"] == "article"
|
||||||
|
assert article["render_in_lists"] == true
|
||||||
|
assert article["show_title"] == true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "remove_category/4" do
|
describe "remove_category/4" do
|
||||||
setup do
|
setup do
|
||||||
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
|
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
|
||||||
@@ -35,7 +395,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.ManagedCategoriesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "rejects deletion of protected category with error output", %{project: project} do
|
test "rejects deletion of protected category with error output", %{project: project} do
|
||||||
socket = %{assigns: %{projects: %{active_project_id: project.id}, workbench: nil}}
|
socket = socket_with_assigns(%{projects: %{active_project_id: project.id}})
|
||||||
|
|
||||||
append_output = fn _socket, _title, _msg, _nil, _kind ->
|
append_output = fn _socket, _title, _msg, _nil, _kind ->
|
||||||
send(self(), :error_appended)
|
send(self(), :error_appended)
|
||||||
@@ -53,7 +413,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.ManagedCategoriesTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "rejects deletion of all protected categories", %{project: project} do
|
test "rejects deletion of all protected categories", %{project: project} do
|
||||||
socket = %{assigns: %{projects: %{active_project_id: project.id}, workbench: nil}}
|
socket = socket_with_assigns(%{projects: %{active_project_id: project.id}})
|
||||||
|
|
||||||
for cat <- ["article", "aside", "page", "picture"] do
|
for cat <- ["article", "aside", "page", "picture"] do
|
||||||
append_output = fn _socket, _title, _msg, _nil, _kind ->
|
append_output = fn _socket, _title, _msg, _nil, _kind ->
|
||||||
@@ -75,12 +435,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.ManagedCategoriesTest do
|
|||||||
test "allows deletion of non-protected category via Metadata.remove_category", %{
|
test "allows deletion of non-protected category via Metadata.remove_category", %{
|
||||||
project: project
|
project: project
|
||||||
} do
|
} do
|
||||||
socket = %{
|
socket = socket_with_assigns(%{projects: %{active_project_id: project.id}})
|
||||||
assigns: %{
|
|
||||||
projects: %{active_project_id: project.id},
|
|
||||||
workbench: nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BDS.Metadata.add_category(project.id, "test-cat")
|
BDS.Metadata.add_category(project.id, "test-cat")
|
||||||
assert {:ok, meta} = BDS.Metadata.get_project_metadata(project.id)
|
assert {:ok, meta} = BDS.Metadata.get_project_metadata(project.id)
|
||||||
|
|||||||
82
test/bds/desktop/mcp_config_test.exs
Normal file
82
test/bds/desktop/mcp_config_test.exs
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
defmodule BDS.Desktop.ShellLive.SettingsEditor.MCPConfigTest do
|
||||||
|
use ExUnit.Case, async: false
|
||||||
|
|
||||||
|
alias BDS.Desktop.ShellLive.SettingsEditor.MCPConfig
|
||||||
|
|
||||||
|
describe "mcp_rows/0" do
|
||||||
|
test "returns 7 agent rows" do
|
||||||
|
rows = MCPConfig.mcp_rows()
|
||||||
|
assert length(rows) == 7
|
||||||
|
end
|
||||||
|
|
||||||
|
test "has correct agent order" do
|
||||||
|
rows = MCPConfig.mcp_rows()
|
||||||
|
ids = Enum.map(rows, & &1.id)
|
||||||
|
assert ids == [:claude_code, :claude_desktop, :github_copilot, :gemini_cli, :opencode, :mistral_vibe, :openai_codex]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "Claude Code and GitHub Copilot are supported" do
|
||||||
|
rows = MCPConfig.mcp_rows()
|
||||||
|
claude = Enum.find(rows, &(&1.id == :claude_code))
|
||||||
|
copilot = Enum.find(rows, &(&1.id == :github_copilot))
|
||||||
|
assert claude.supported?
|
||||||
|
assert copilot.supported?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "other agents are not supported" do
|
||||||
|
rows = MCPConfig.mcp_rows()
|
||||||
|
unsupported = Enum.reject(rows, & &1.supported?)
|
||||||
|
assert length(unsupported) == 5
|
||||||
|
assert Enum.all?(unsupported, &(!&1.supported?))
|
||||||
|
end
|
||||||
|
|
||||||
|
test "unsupported agents have nil config_path" do
|
||||||
|
rows = MCPConfig.mcp_rows()
|
||||||
|
unsupported = Enum.filter(rows, &(!&1.supported?))
|
||||||
|
assert Enum.all?(unsupported, &is_nil(&1.config_path))
|
||||||
|
end
|
||||||
|
|
||||||
|
test "unsupported agents have configured? false" do
|
||||||
|
rows = MCPConfig.mcp_rows()
|
||||||
|
unsupported = Enum.filter(rows, &(!&1.supported?))
|
||||||
|
assert Enum.all?(unsupported, &(&1.configured? == false))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "toggle_mcp_agent/4" do
|
||||||
|
test "unsupported agent appends not-supported error" do
|
||||||
|
socket = %{assigns: %{workbench: nil}}
|
||||||
|
reload = fn s, _wb -> send(self(), :reloaded); s end
|
||||||
|
append_output = fn _socket, _title, _msg, _nil, _kind ->
|
||||||
|
send(self(), :error_appended)
|
||||||
|
socket
|
||||||
|
end
|
||||||
|
|
||||||
|
MCPConfig.toggle_mcp_agent(socket, "gemini_cli", reload, append_output)
|
||||||
|
|
||||||
|
assert_received :error_appended
|
||||||
|
assert_received :reloaded
|
||||||
|
end
|
||||||
|
|
||||||
|
test "unsupported agent does not touch AgentConfig" do
|
||||||
|
socket = %{assigns: %{workbench: nil}}
|
||||||
|
reload = fn s, _wb -> s end
|
||||||
|
append_output = fn _socket, _title, _msg, _nil, _kind -> socket end
|
||||||
|
|
||||||
|
assert MCPConfig.toggle_mcp_agent(socket, "opencode", reload, append_output) == socket
|
||||||
|
end
|
||||||
|
|
||||||
|
test "unknown agent is treated as unsupported" do
|
||||||
|
socket = %{assigns: %{workbench: nil}}
|
||||||
|
reload = fn s, _wb -> s end
|
||||||
|
append_output = fn _socket, _title, _msg, _nil, _kind ->
|
||||||
|
send(self(), :error_appended)
|
||||||
|
socket
|
||||||
|
end
|
||||||
|
|
||||||
|
MCPConfig.toggle_mcp_agent(socket, "nonexistent_agent", reload, append_output)
|
||||||
|
|
||||||
|
assert_received :error_appended
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
114
test/bds/desktop/settings_search_test.exs
Normal file
114
test/bds/desktop/settings_search_test.exs
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
defmodule BDS.Desktop.ShellLive.SettingsEditor.SettingsSearchTest do
|
||||||
|
use ExUnit.Case, async: false
|
||||||
|
|
||||||
|
alias BDS.Desktop.ShellLive.SettingsEditor
|
||||||
|
|
||||||
|
describe "build_settings/1 — search filter" do
|
||||||
|
setup do
|
||||||
|
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
|
||||||
|
temp_dir =
|
||||||
|
Path.join(System.tmp_dir!(), "bds-settings-search-#{System.unique_integer([:positive])}")
|
||||||
|
|
||||||
|
File.mkdir_p!(temp_dir)
|
||||||
|
|
||||||
|
on_exit(fn -> File.rm_rf(temp_dir) end)
|
||||||
|
|
||||||
|
{:ok, project} =
|
||||||
|
BDS.Projects.create_project(%{name: "SearchFilter", data_path: temp_dir})
|
||||||
|
|
||||||
|
%{project: project, temp_dir: temp_dir}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp base_assigns(project_id, temp_dir, query) do
|
||||||
|
%{
|
||||||
|
projects: %{active_project_id: project_id},
|
||||||
|
current_project: %{data_path: temp_dir},
|
||||||
|
settings_editor_search: query,
|
||||||
|
settings_editor_project_draft: %{},
|
||||||
|
settings_editor_editor_draft: %{},
|
||||||
|
settings_editor_ai_draft: %{},
|
||||||
|
settings_editor_publishing_draft: %{},
|
||||||
|
current_tab: %{type: :settings, id: "settings"},
|
||||||
|
tab_meta: %{}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "empty query shows all sections visible", %{project: project, temp_dir: temp_dir} do
|
||||||
|
result = SettingsEditor.build_settings(base_assigns(project.id, temp_dir, ""))
|
||||||
|
|
||||||
|
assert result.project_visible?
|
||||||
|
assert result.editor_visible?
|
||||||
|
assert result.content_visible?
|
||||||
|
assert result.ai_visible?
|
||||||
|
assert result.technology_visible?
|
||||||
|
assert result.publishing_visible?
|
||||||
|
assert result.mcp_visible?
|
||||||
|
assert result.data_visible?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "matching query shows only relevant sections", %{project: project, temp_dir: temp_dir} do
|
||||||
|
result = SettingsEditor.build_settings(base_assigns(project.id, temp_dir, "ai"))
|
||||||
|
|
||||||
|
assert result.ai_visible?
|
||||||
|
refute result.editor_visible?
|
||||||
|
refute result.content_visible?
|
||||||
|
refute result.publishing_visible?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "query 'publishing' shows publishing section only", %{project: project, temp_dir: temp_dir} do
|
||||||
|
result = SettingsEditor.build_settings(base_assigns(project.id, temp_dir, "publishing"))
|
||||||
|
|
||||||
|
assert result.publishing_visible?
|
||||||
|
refute result.editor_visible?
|
||||||
|
refute result.ai_visible?
|
||||||
|
refute result.technology_visible?
|
||||||
|
refute result.mcp_visible?
|
||||||
|
refute result.data_visible?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "query 'mcp' shows mcp section", %{project: project, temp_dir: temp_dir} do
|
||||||
|
result = SettingsEditor.build_settings(base_assigns(project.id, temp_dir, "mcp"))
|
||||||
|
|
||||||
|
assert result.mcp_visible?
|
||||||
|
refute result.editor_visible?
|
||||||
|
refute result.ai_visible?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "query 'claude' matches mcp section", %{project: project, temp_dir: temp_dir} do
|
||||||
|
result = SettingsEditor.build_settings(base_assigns(project.id, temp_dir, "claude"))
|
||||||
|
|
||||||
|
assert result.mcp_visible?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "query 'data' shows data section", %{project: project, temp_dir: temp_dir} do
|
||||||
|
result = SettingsEditor.build_settings(base_assigns(project.id, temp_dir, "data"))
|
||||||
|
|
||||||
|
assert result.data_visible?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "query 'editor' shows editor section", %{project: project, temp_dir: temp_dir} do
|
||||||
|
result = SettingsEditor.build_settings(base_assigns(project.id, temp_dir, "editor"))
|
||||||
|
|
||||||
|
assert result.editor_visible?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "no match query shows no sections", %{project: project, temp_dir: temp_dir} do
|
||||||
|
result = SettingsEditor.build_settings(base_assigns(project.id, temp_dir, "zzzzz"))
|
||||||
|
|
||||||
|
refute result.project_visible?
|
||||||
|
refute result.editor_visible?
|
||||||
|
refute result.content_visible?
|
||||||
|
refute result.ai_visible?
|
||||||
|
refute result.technology_visible?
|
||||||
|
refute result.publishing_visible?
|
||||||
|
refute result.mcp_visible?
|
||||||
|
refute result.data_visible?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "build_settings/1 — returns nil without active project" do
|
||||||
|
test "returns nil when active_project_id is nil" do
|
||||||
|
assert SettingsEditor.build_settings(%{projects: %{active_project_id: nil}}) == nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
173
test/bds/desktop/style_editor_test.exs
Normal file
173
test/bds/desktop/style_editor_test.exs
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
defmodule BDS.Desktop.ShellLive.SettingsEditor.StyleEditorTest do
|
||||||
|
use ExUnit.Case, async: false
|
||||||
|
|
||||||
|
alias BDS.Desktop.ShellLive.SettingsEditor.StyleEditor
|
||||||
|
|
||||||
|
defp socket_with_assigns(extra \\ %{}) do
|
||||||
|
%Phoenix.LiveView.Socket{assigns: Map.merge(%{__changed__: %{}, workbench: nil}, extra)}
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "build_style/1" do
|
||||||
|
test "returns nil when no active project" do
|
||||||
|
assigns = %{projects: %{active_project_id: nil}}
|
||||||
|
assert StyleEditor.build_style(assigns) == nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "build_style/1 with real project" do
|
||||||
|
setup do
|
||||||
|
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
|
||||||
|
temp_dir =
|
||||||
|
Path.join(System.tmp_dir!(), "bds-style-editor-#{System.unique_integer([:positive])}")
|
||||||
|
|
||||||
|
File.mkdir_p!(temp_dir)
|
||||||
|
|
||||||
|
on_exit(fn -> File.rm_rf(temp_dir) end)
|
||||||
|
|
||||||
|
{:ok, project} =
|
||||||
|
BDS.Projects.create_project(%{name: "StyleEditor", data_path: temp_dir})
|
||||||
|
|
||||||
|
%{project: project}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns map with themes, selected_theme, applied_theme, preview_mode, preview_url", %{project: project} do
|
||||||
|
assigns = %{projects: %{active_project_id: project.id}}
|
||||||
|
|
||||||
|
result = StyleEditor.build_style(assigns)
|
||||||
|
assert is_map(result)
|
||||||
|
assert Map.has_key?(result, :themes)
|
||||||
|
assert Map.has_key?(result, :selected_theme)
|
||||||
|
assert Map.has_key?(result, :applied_theme)
|
||||||
|
assert Map.has_key?(result, :preview_mode)
|
||||||
|
assert Map.has_key?(result, :preview_url)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "includes 20 themes", %{project: project} do
|
||||||
|
assigns = %{projects: %{active_project_id: project.id}}
|
||||||
|
result = StyleEditor.build_style(assigns)
|
||||||
|
assert length(result.themes) == 20
|
||||||
|
end
|
||||||
|
|
||||||
|
test "each theme has required keys", %{project: project} do
|
||||||
|
assigns = %{projects: %{active_project_id: project.id}}
|
||||||
|
result = StyleEditor.build_style(assigns)
|
||||||
|
theme = hd(result.themes)
|
||||||
|
assert Map.has_key?(theme, :name)
|
||||||
|
assert Map.has_key?(theme, :accent_color)
|
||||||
|
assert Map.has_key?(theme, :light_bg_color)
|
||||||
|
assert Map.has_key?(theme, :dark_bg_color)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "includes default, amber, and zinc themes", %{project: project} do
|
||||||
|
assigns = %{projects: %{active_project_id: project.id}}
|
||||||
|
result = StyleEditor.build_style(assigns)
|
||||||
|
names = Enum.map(result.themes, & &1.name)
|
||||||
|
assert "default" in names
|
||||||
|
assert "amber" in names
|
||||||
|
assert "zinc" in names
|
||||||
|
end
|
||||||
|
|
||||||
|
test "preview_url includes theme and mode params", %{project: project} do
|
||||||
|
assigns = %{projects: %{active_project_id: project.id}}
|
||||||
|
result = StyleEditor.build_style(assigns)
|
||||||
|
assert result.preview_url =~ "theme=default"
|
||||||
|
assert result.preview_url =~ "mode=auto"
|
||||||
|
assert result.preview_url =~ "127.0.0.1:4123/__style-preview"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "preview_url reflects selected_theme", %{project: project} do
|
||||||
|
assigns = %{
|
||||||
|
projects: %{active_project_id: project.id},
|
||||||
|
style_editor_theme: "amber"
|
||||||
|
}
|
||||||
|
|
||||||
|
result = StyleEditor.build_style(assigns)
|
||||||
|
assert result.selected_theme == "amber"
|
||||||
|
assert result.preview_url =~ "theme=amber"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "preview_url reflects preview_mode", %{project: project} do
|
||||||
|
assigns = %{
|
||||||
|
projects: %{active_project_id: project.id},
|
||||||
|
style_editor_preview_mode: "dark"
|
||||||
|
}
|
||||||
|
|
||||||
|
result = StyleEditor.build_style(assigns)
|
||||||
|
assert result.preview_mode == "dark"
|
||||||
|
assert result.preview_url =~ "mode=dark"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "all 20 theme names are strings", %{project: project} do
|
||||||
|
assigns = %{projects: %{active_project_id: project.id}}
|
||||||
|
result = StyleEditor.build_style(assigns)
|
||||||
|
assert Enum.all?(result.themes, &is_binary(&1.name))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "theme_display_name/1" do
|
||||||
|
test "replaces hyphens with spaces and capitalizes" do
|
||||||
|
assert StyleEditor.theme_display_name("default") == "Default"
|
||||||
|
assert StyleEditor.theme_display_name("amber") == "Amber"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "handles empty string" do
|
||||||
|
assert StyleEditor.theme_display_name("") == ""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "select_style_theme/3" do
|
||||||
|
test "updates style_editor_theme assign" do
|
||||||
|
socket = socket_with_assigns()
|
||||||
|
reload = fn s, _wb ->
|
||||||
|
send(self(), {:reloaded, s})
|
||||||
|
s
|
||||||
|
end
|
||||||
|
|
||||||
|
StyleEditor.select_style_theme(socket, "amber", reload)
|
||||||
|
|
||||||
|
assert_received {:reloaded, updated_socket}
|
||||||
|
assert updated_socket.assigns.style_editor_theme == "amber"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "defaults to default when nil" do
|
||||||
|
socket = socket_with_assigns()
|
||||||
|
reload = fn s, _wb ->
|
||||||
|
send(self(), {:reloaded, s})
|
||||||
|
s
|
||||||
|
end
|
||||||
|
|
||||||
|
StyleEditor.select_style_theme(socket, nil, reload)
|
||||||
|
|
||||||
|
assert_received {:reloaded, updated_socket}
|
||||||
|
assert updated_socket.assigns.style_editor_theme == "default"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "change_style_preview_mode/3" do
|
||||||
|
test "updates style_editor_preview_mode assign" do
|
||||||
|
socket = socket_with_assigns()
|
||||||
|
reload = fn s, _wb ->
|
||||||
|
send(self(), {:reloaded, s})
|
||||||
|
s
|
||||||
|
end
|
||||||
|
|
||||||
|
StyleEditor.change_style_preview_mode(socket, "dark", reload)
|
||||||
|
|
||||||
|
assert_received {:reloaded, updated_socket}
|
||||||
|
assert updated_socket.assigns.style_editor_preview_mode == "dark"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "defaults to auto when nil" do
|
||||||
|
socket = socket_with_assigns()
|
||||||
|
reload = fn s, _wb ->
|
||||||
|
send(self(), {:reloaded, s})
|
||||||
|
s
|
||||||
|
end
|
||||||
|
|
||||||
|
StyleEditor.change_style_preview_mode(socket, nil, reload)
|
||||||
|
|
||||||
|
assert_received {:reloaded, updated_socket}
|
||||||
|
assert updated_socket.assigns.style_editor_preview_mode == "auto"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user