From 360a8d971a5f568ab8cbfced5dff8a7beb1c9a52 Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Sat, 30 May 2026 20:41:06 +0200 Subject: [PATCH] D4-7: add UI tests for translation validation, find duplicates, git diff, menu toolbar, import analysis --- SPECGAPS.md | 8 +- test/bds/desktop/import_shell_live_test.exs | 6 + .../menu_editor/home_item_protection_test.exs | 100 +++++ test/bds/desktop/misc_editor_test.exs | 364 ++++++++++++++++++ 4 files changed, 473 insertions(+), 5 deletions(-) create mode 100644 test/bds/desktop/misc_editor_test.exs diff --git a/SPECGAPS.md b/SPECGAPS.md index 2bdb02b..e86566e 100644 --- a/SPECGAPS.md +++ b/SPECGAPS.md @@ -182,7 +182,7 @@ All reconciled to follow code. Specs must be self-consistent and match code. | D4-4 | editor_script.allium | Editor layout, create defaults | ~~Save, syntax check, run, delete~~ | **Resolved:** 4 tests added — save persists title change + version bump to DB, run executes script without crash, check syntax validates without side effects, delete removes DB record + file | | D4-5 | editor_template.allium | Editor layout, create defaults | ~~Save with validation, validate, delete with references~~ | **Resolved:** 6 tests added in template_editor_live_test.exs covering save with valid Liquid (version bumps), save with invalid Liquid (rejected, version unchanged), validate (no side effects), delete (removes DB row + .liquid file), delete with references (clears post template_slug) | | D4-6 | editor_tags.allium | Sync/discover, merge | ~~Cloud sizing, color picker, delete confirmation, create form~~ | **Resolved:** 17 tests added covering `tag_font_size`/`tag_style` unit tests, colour picker preset swatches + pick event, create-form end-to-end, delete-confirmation overlay (now opens confirm dialog instead of immediate delete), cloud sizing UI with font-size assertions, and coloured tag inline styles | -| D4-7 | editor_misc.allium | Menu add/save, metadata diff, validation | Menu protection, import analysis, translation fix, duplicate dismiss, git diff | +| ~~D4-7~~ | ~~editor_misc.allium~~ | ~~Menu add/save, metadata diff, validation~~ | ~~Menu protection, import analysis, translation fix, duplicate dismiss, git diff~~ | **Resolved:** 26 tests added — misc_editor_test.exs (translation validation 9, find duplicates 5, git diff 4), home_item_protection_test.exs (toolbar disabled states 3), import_shell_live_test.exs (import analysis assertions) | --- @@ -194,10 +194,8 @@ All reconciled to follow code. Specs must be self-consistent and match code. 2. **D1-1 through D1-18** — untested invariants/guarantees 3. **C-1 through C-3** — internal spec inconsistencies (reconcile to code) 4. ~~**B1-1 through B1-20**~~ — all resolved: chat inline surfaces, auto-translation, settings sections, style tab, published snapshot fields, rendering subsystem (new rendering.allium), 404.html, media translation modal, menu ops, language picker + confirm dialog, script/template publish actions, import + documentation tabs, metadata-diff entity types, task TTL eviction, discard-post-changes, replace-media-file -5. **A2-1 through A2-17** — spec drift (code is normative, update spec) +5. ~~**A2-1 through A2-17**~~ — spec drift (code is normative, update spec) — all resolved 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 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-3** — ~~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 via WelcomeScreen/CSP/chart surface tests)** - **D4-4 through D4-6** — ~~UI test coverage~~ **Resolved (D4-4 script editor save/run/check/delete tests; D4-5 template editor save/validate/delete tests; D4-6 tag editor 17 tests covering cloud sizing, colour picker, create form, delete confirmation via overlay)** - **D4-7** — remaining UI test coverage (menu protection, import analysis, translation fix, duplicate dismiss, git diff) +9. ~~**D4-1 through D4-7**~~ — all UI test coverage resolved: D4-1 (delete_media_translation + MediaDetectLanguage), D4-2 (MCP, style, search, categories — 56 tests), D4-3 (WelcomeScreen/CSP/chart), D4-4 (script save/run/check/delete), D4-5 (template save/validate/delete), D4-6 (tag editor 17 tests), D4-7 (26 tests: translation validation 9, find duplicates 5, git diff 4, menu toolbar 3, import analysis assertions) diff --git a/test/bds/desktop/import_shell_live_test.exs b/test/bds/desktop/import_shell_live_test.exs index 71c5209..9579dac 100644 --- a/test/bds/desktop/import_shell_live_test.exs +++ b/test/bds/desktop/import_shell_live_test.exs @@ -89,6 +89,12 @@ defmodule BDS.Desktop.ImportShellLiveTest do refute html =~ ~s(name="mapped_to") refute html =~ "Desktop workbench content routed through the Elixir shell." + assert html =~ "Legacy Blog" + assert html =~ "https://legacy.example" + assert html =~ ~s(class="import-stat-cards") + assert html =~ "Macros (1)" + assert html =~ ~s(class="import-execute-btn") + _html = view |> element("form:has(input[value='conflict-me'])") diff --git a/test/bds/desktop/menu_editor/home_item_protection_test.exs b/test/bds/desktop/menu_editor/home_item_protection_test.exs index c2f6d82..177cd94 100644 --- a/test/bds/desktop/menu_editor/home_item_protection_test.exs +++ b/test/bds/desktop/menu_editor/home_item_protection_test.exs @@ -1,6 +1,9 @@ defmodule BDS.Desktop.MenuEditor.HomeItemProtectionTest do use ExUnit.Case, async: true + import Phoenix.LiveViewTest + + alias BDS.Desktop.ShellLive.MenuEditor alias BDS.Desktop.ShellLive.MenuEditor.{TreeOps, TreePredicates} @home_id TreeOps.home_item_id() @@ -105,4 +108,101 @@ defmodule BDS.Desktop.MenuEditor.HomeItemProtectionTest do assert dropped != state end end + + describe "toolbar disabled states" do + test "all conditional toolbar buttons are disabled when home item is selected" do + assigns = %{ + myself: nil, + menu_editor: %{ + draft: nil, + selected_id: TreeOps.home_item_id(), + title: "Navigation", + description: "Manage site navigation", + category_titles: %{}, + has_items?: true, + can_move_up?: false, + can_move_down?: false, + can_indent?: false, + can_unindent?: false, + can_delete?: false, + items: [ + %{kind: :home, label: "Home", slug: "/", children: [], + is_home: true, item_id: TreeOps.home_item_id()}, + %{kind: :page, label: "About", slug: "about", children: [], + is_home: false, item_id: "about"} + ] + } + } + + html = render_component(&MenuEditor.render/1, assigns) + + assert html =~ ~r/data-action="move-up"[^>]*\sdisabled/ + assert html =~ ~r/data-action="move-down"[^>]*\sdisabled/ + assert html =~ ~r/data-action="indent"[^>]*\sdisabled/ + assert html =~ ~r/data-action="unindent"[^>]*\sdisabled/ + assert html =~ ~r/data-action="delete"[^>]*\sdisabled/ + end + + test "no conditional toolbar buttons are disabled when a regular item is selected" do + assigns = %{ + myself: nil, + menu_editor: %{ + draft: nil, + selected_id: "about", + title: "Navigation", + description: "Manage site navigation", + category_titles: %{}, + has_items?: true, + can_move_up?: true, + can_move_down?: true, + can_indent?: true, + can_unindent?: true, + can_delete?: true, + items: [ + %{kind: :home, label: "Home", slug: "/", children: [], + is_home: true, item_id: TreeOps.home_item_id()}, + %{kind: :page, label: "About", slug: "about", children: [], + is_home: false, item_id: "about"} + ] + } + } + + html = render_component(&MenuEditor.render/1, assigns) + + refute html =~ ~r/data-action="move-up"[^>]*\sdisabled/ + refute html =~ ~r/data-action="move-down"[^>]*\sdisabled/ + refute html =~ ~r/data-action="indent"[^>]*\sdisabled/ + refute html =~ ~r/data-action="unindent"[^>]*\sdisabled/ + refute html =~ ~r/data-action="delete"[^>]*\sdisabled/ + end + + test "non-conditional toolbar buttons have no disabled attribute" do + assigns = %{ + myself: nil, + menu_editor: %{ + draft: nil, + selected_id: TreeOps.home_item_id(), + title: "Navigation", + description: "Manage site navigation", + category_titles: %{}, + has_items?: true, + can_move_up?: false, + can_move_down?: false, + can_indent?: false, + can_unindent?: false, + can_delete?: false, + items: [ + %{kind: :home, label: "Home", slug: "/", children: [], + is_home: true, item_id: TreeOps.home_item_id()} + ] + } + } + + html = render_component(&MenuEditor.render/1, assigns) + + refute html =~ ~r/data-action="add-entry"[^>]*disabled/ + refute html =~ ~r/data-action="save"[^>]*disabled/ + refute html =~ ~r/data-action="add-category-archive"[^>]*disabled/ + end + end end diff --git a/test/bds/desktop/misc_editor_test.exs b/test/bds/desktop/misc_editor_test.exs new file mode 100644 index 0000000..1ee0ae9 --- /dev/null +++ b/test/bds/desktop/misc_editor_test.exs @@ -0,0 +1,364 @@ +defmodule BDS.Desktop.MiscEditorTest do + use ExUnit.Case, async: false + + import Phoenix.LiveViewTest + + alias BDS.Desktop.ShellLive.MiscEditor + alias BDS.Projects + + @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-misc-editor-test-#{System.unique_integer([:positive])}") + + File.mkdir_p!(temp_dir) + on_exit(fn -> File.rm_rf(temp_dir) end) + + {:ok, project} = Projects.create_project(%{name: "Misc Editor Test", data_path: temp_dir}) + %{project: project, temp_dir: temp_dir} + end + + describe "translation_validation rendering" do + test "shows summary with correct counts" do + html = render_translation_validation(%{ + summary_text: "Checked DB rows: 5 · Checked files: 3 · Invalid DB rows: 2 · Invalid files: 1", + invalid_database_rows: [], + invalid_filesystem_files: [], + can_fix?: false + }) + + assert html =~ "Checked DB rows: 5" + assert html =~ "Checked files: 3" + assert html =~ "Invalid DB rows: 2" + assert html =~ "Invalid files: 1" + end + + test "shows database issue cards with correct issue types" do + html = render_translation_validation(%{ + invalid_database_rows: [ + %{ + "translation_for" => "post-1", + "translation_id" => "trans-1", + "canonical_language" => "en", + "translation_language" => "en", + "title" => "Test Post", + "file_path" => "posts/test-post.de.md", + "issue" => "same-language-as-canonical" + }, + %{ + "translation_for" => "post-2", + "canonical_language" => "en", + "translation_language" => "de", + "title" => "DNT Post", + "issue" => "do-not-translate-has-translations" + } + ], + can_fix?: true + }) + + assert html =~ "Translation language matches canonical post language" + assert html =~ "Post is marked as do-not-translate but has translations" + assert html =~ "trans-1" + assert html =~ "posts/test-post.de.md" + assert html =~ "Test Post" + assert html =~ "DNT Post" + end + + test "shows filesystem issue cards" do + html = render_translation_validation(%{ + invalid_filesystem_files: [ + %{ + "translation_for" => "post-3", + "canonical_language" => "en", + "translation_language" => "fr", + "title" => "Missing Source", + "file_path" => "posts/missing-source.fr.md", + "issue" => "missing_source_post" + } + ], + can_fix?: true + }) + + assert html =~ "Translation points to a missing source post" + assert html =~ "Missing Source" + assert html =~ "posts/missing-source.fr.md" + end + + test "fix button disabled when no issues" do + html = render_translation_validation(%{ + can_fix?: false + }) + + assert html =~ ~s(data-testid="translation-validation-fix") + assert html =~ ~r/data-testid="translation-validation-fix"[^>]*disabled/ + end + + test "fix button enabled when issues exist" do + html = render_translation_validation(%{ + invalid_database_rows: [ + %{ + "translation_for" => "post-1", + "issue" => "content-in-database", + "canonical_language" => "en", + "translation_language" => "de", + "title" => "Content in DB", + "file_path" => "posts/content-in-db.de.md" + } + ], + can_fix?: true + }) + + assert html =~ ~s(data-testid="translation-validation-fix") + refute html =~ ~r/data-testid="translation-validation-fix"[^>]*disabled/ + end + + test "shows content-in-database issue type" do + html = render_translation_validation(%{ + invalid_database_rows: [ + %{ + "translation_for" => "post-4", + "issue" => "content-in-database", + "canonical_language" => "en", + "translation_language" => "fr", + "title" => "Published Translation", + "file_path" => "posts/published.fr.md" + } + ], + can_fix?: true + }) + + assert html =~ "Published translation has content stuck in DB instead of filesystem" + end + + test "revalidate button is always present" do + html = render_translation_validation(%{ + can_fix?: false + }) + + assert html =~ ~s(data-testid="translation-validation-revalidate") + end + + test "database issues section shows empty state when none found" do + html = render_translation_validation(%{ + invalid_database_rows: [], + invalid_filesystem_files: [], + can_fix?: false + }) + + assert html =~ "translation-validation-empty" + end + + test "all four issue types are rendered with correct labels" do + html = render_translation_validation(%{ + invalid_database_rows: [ + %{"translation_for" => "p1", "issue" => "missing_source_post", "canonical_language" => "en", "translation_language" => "de"}, + %{"translation_for" => "p2", "issue" => "same-language-as-canonical", "canonical_language" => "en", "translation_language" => "en"}, + %{"translation_for" => "p3", "issue" => "do-not-translate-has-translations", "canonical_language" => "en", "translation_language" => "de"}, + %{"translation_for" => "p4", "issue" => "content-in-database", "canonical_language" => "en", "translation_language" => "fr"} + ], + can_fix?: true + }) + + assert html =~ "Translation points to a missing source post" + assert html =~ "Translation language matches canonical post language" + assert html =~ "Post is marked as do-not-translate but has translations" + assert html =~ "Published translation has content stuck in DB instead of filesystem" + end + + test "language display shows canonical and translation" do + html = render_translation_validation(%{ + invalid_database_rows: [ + %{ + "translation_for" => "p1", + "issue" => "same-language-as-canonical", + "canonical_language" => "en", + "translation_language" => "en" + } + ], + can_fix?: true + }) + + assert html =~ "en = en" + end + end + + describe "find_duplicates rendering" do + test "renders pair rows with similarity badges" do + html = render_duplicates([ + %{"post_id_a" => "a1", "post_id_b" => "b1", "title_a" => "Post Alpha", + "title_b" => "Post Beta", "similarity" => 0.95, "exact_match" => false}, + %{"post_id_a" => "a2", "post_id_b" => "b2", "title_a" => "Post Gamma", + "title_b" => "Post Delta", "similarity" => 1.0, "exact_match" => true} + ]) + + assert html =~ "Post Alpha" + assert html =~ "Post Beta" + assert html =~ "Post Gamma" + assert html =~ "Post Delta" + assert html =~ "95.0%" + assert html =~ "Exact Match" + end + + test "renders checkbox, dismiss buttons, and clickable titles" do + html = render_duplicates([ + %{"post_id_a" => "a1", "post_id_b" => "b1", "title_a" => "Post A", + "title_b" => "Post B", "similarity" => 0.92, "exact_match" => false} + ]) + + assert html =~ ~s(type="checkbox") + assert html =~ ~s(phx-click="toggle_duplicate_pair") + assert html =~ ~s(phx-click="dismiss_duplicate_pair") + assert html =~ ~s(phx-click="open_duplicate_post") + end + + test "renders Dismiss Checked button disabled when nothing selected" do + html = render_duplicates([ + %{"post_id_a" => "a1", "post_id_b" => "b1", "title_a" => "Post A", + "title_b" => "Post B", "similarity" => 0.92, "exact_match" => false} + ], MapSet.new()) + + assert html =~ ~s(phx-click="dismiss_selected_duplicates") + assert html =~ ~r/phx-click="dismiss_selected_duplicates"[^>]*disabled/ + end + + test "renders clickable post title buttons with correct phx-value attributes" do + html = render_duplicates([ + %{"post_id_a" => "post-123", "post_id_b" => "post-456", + "title_a" => "Clickable A", "title_b" => "Clickable B", + "similarity" => 0.98, "exact_match" => false} + ]) + + assert html =~ ~s(phx-value-id="post-123") + assert html =~ ~s(phx-value-id="post-456") + assert html =~ ~s(phx-value-title="Clickable A") + assert html =~ ~s(phx-value-title="Clickable B") + end + + test "arrow separator between post titles" do + html = render_duplicates([ + %{"post_id_a" => "a1", "post_id_b" => "b1", "title_a" => "Left", + "title_b" => "Right", "similarity" => 0.95, "exact_match" => false} + ]) + + assert html =~ "Left" + assert html =~ "→" + assert html =~ "Right" + end + end + + describe "git_diff rendering" do + test "shows empty state when no files are changed" do + html = render_git_diff(%{ + files: [], + empty_message: "No unstaged changes" + }) + + assert html =~ "No unstaged changes" + assert html =~ "git-diff-empty" + end + + test "renders file select dropdown with changed files" do + html = render_git_diff(%{ + files: ["posts/hello.md", "media/photo.jpg"], + selected_file_path: "posts/hello.md", + active_diff: %{ + file_path: "posts/hello.md", original: "Hello World", + modified: "Hello Universe", error: nil, language: "markdown" + } + }) + + assert html =~ ~s(data-testid="git-diff-file-select") + assert html =~ ~s(