feat: added a gallery quick action and fleshed out builtin macros
This commit is contained in:
@@ -3998,7 +3998,7 @@ defmodule BDS.Desktop.ShellLiveTest do
|
||||
|> element("[data-testid='chat-send-button']")
|
||||
|> render_click()
|
||||
|
||||
send(view.pid, {
|
||||
send_and_await(view, {
|
||||
:chat_tool_call,
|
||||
conversation.id,
|
||||
%{
|
||||
@@ -4245,14 +4245,14 @@ defmodule BDS.Desktop.ShellLiveTest do
|
||||
assert html =~ ~s(class="chat-input chat-surface-input ui-textarea")
|
||||
|
||||
css = desktop_css_source()
|
||||
assert css =~ "--chat-input-line-height: 20px;"
|
||||
assert css =~ "--chat-input-min-height: 20px;"
|
||||
assert css =~ "--chat-input-line-height: 22px;"
|
||||
assert css =~ "--chat-input-min-height: 24px;"
|
||||
assert css =~ ".chat-panel .chat-input-container"
|
||||
assert css =~ "padding: 8px 16px;"
|
||||
assert css =~ "padding: 12px 16px;"
|
||||
assert css =~ "padding: 6px 8px;"
|
||||
assert css =~ ".chat-panel .chat-input-wrapper"
|
||||
assert css =~ "min-height: 30px;"
|
||||
assert css =~ "padding: 4px 6px;"
|
||||
assert css =~ "min-height: 40px;"
|
||||
assert css =~ "padding: 6px 8px;"
|
||||
assert css =~ ".chat-panel .chat-input"
|
||||
assert css =~ "box-sizing: border-box;"
|
||||
assert css =~ "margin: 0;"
|
||||
@@ -4565,7 +4565,7 @@ defmodule BDS.Desktop.ShellLiveTest do
|
||||
assert assistant_index < user_index
|
||||
assert html =~ ~r/<textarea[^>]*class="chat-input chat-surface-input ui-textarea"[^>]*disabled/
|
||||
|
||||
send(view.pid, {
|
||||
send_and_await(view, {
|
||||
:chat_tool_call,
|
||||
conversation.id,
|
||||
%{
|
||||
@@ -4629,11 +4629,13 @@ defmodule BDS.Desktop.ShellLiveTest do
|
||||
|> element(".chat-input-wrapper")
|
||||
|> render_change(%{"message" => "Newest question"})
|
||||
|
||||
_html =
|
||||
html =
|
||||
view
|
||||
|> element("[data-testid='chat-send-button']")
|
||||
|> render_click()
|
||||
|
||||
assert html =~ ~s(data-testid="chat-streaming-thinking")
|
||||
|
||||
assert Enum.count(AI.list_chat_messages(conversation.id), fn message ->
|
||||
message.role == :user and message.content == "Newest question"
|
||||
end) == 1
|
||||
@@ -4660,7 +4662,9 @@ defmodule BDS.Desktop.ShellLiveTest do
|
||||
})
|
||||
)
|
||||
|
||||
send(view.pid, {
|
||||
_ensure_sync = render(view)
|
||||
|
||||
send_and_await(view, {
|
||||
:chat_tool_call,
|
||||
conversation.id,
|
||||
%{
|
||||
@@ -4931,6 +4935,20 @@ defmodule BDS.Desktop.ShellLiveTest do
|
||||
run_git!(project_dir, ["commit", "-m", message])
|
||||
end
|
||||
|
||||
# Sends a message to the LiveView and blocks until it has been processed.
|
||||
# Uses a test ping/pong to guarantee the message was handled before returning.
|
||||
defp send_and_await(view, message) do
|
||||
ref = make_ref()
|
||||
send(view.pid, message)
|
||||
send(view.pid, {:test_ping, self(), ref})
|
||||
|
||||
receive do
|
||||
{:test_pong, ^ref} -> :ok
|
||||
after
|
||||
5000 -> raise "LiveView did not process messages within 5s"
|
||||
end
|
||||
end
|
||||
|
||||
defp run_git!(dir, args) do
|
||||
{output, status} = System.cmd("git", args, cd: dir, stderr_to_stdout: true)
|
||||
|
||||
|
||||
252
test/bds/gallery_pipeline_test.exs
Normal file
252
test/bds/gallery_pipeline_test.exs
Normal file
@@ -0,0 +1,252 @@
|
||||
defmodule BDS.GalleryPipelineTest do
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
alias BDS.{Repo, Media}
|
||||
|
||||
setup do
|
||||
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Repo)
|
||||
Ecto.Adapters.SQL.Sandbox.mode(Repo, {:shared, self()})
|
||||
|
||||
temp_dir =
|
||||
Path.join(System.tmp_dir!(), "bds-gallery-test-#{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: "Gallery Test", data_path: temp_dir})
|
||||
{:ok, _project} = BDS.Projects.set_active_project(project.id)
|
||||
|
||||
{:ok, _metadata} =
|
||||
BDS.Metadata.update_project_metadata(project.id, %{
|
||||
blog_languages: ["de", "fr"],
|
||||
main_language: "en"
|
||||
})
|
||||
|
||||
{:ok, post} =
|
||||
BDS.Posts.create_post(%{
|
||||
project_id: project.id,
|
||||
title: "Gallery Post",
|
||||
content: "Content"
|
||||
})
|
||||
|
||||
%{project: project, post: post, temp_dir: temp_dir}
|
||||
end
|
||||
|
||||
describe "GalleryImport.start/6 concurrency" do
|
||||
test "processes all paths with a sliding window", %{project: project, post: post} do
|
||||
parent = self()
|
||||
concurrency = 2
|
||||
|
||||
paths =
|
||||
Enum.map(1..5, fn i ->
|
||||
path = Path.join(project.data_path, "image_#{i}.txt")
|
||||
File.write!(path, "content #{i}")
|
||||
path
|
||||
end)
|
||||
|
||||
ref = make_ref()
|
||||
send(parent, {:test_gallery_start, ref, paths, project.id, post.id, concurrency})
|
||||
end
|
||||
end
|
||||
|
||||
# ── gallery_count with [[gallery]] ─────────────────────────────────────────
|
||||
|
||||
describe "gallery_count" do
|
||||
test "counts [[gallery]] macro as gallery content" do
|
||||
form = %{"content" => "Some text\n\n[[gallery]]\n\nMore text"}
|
||||
|
||||
assert BDS.Desktop.ShellLive.PostEditor.PostMetadata.gallery_count(form) == 1
|
||||
end
|
||||
|
||||
test "counts inline images as before" do
|
||||
form = %{"content" => ""}
|
||||
|
||||
assert BDS.Desktop.ShellLive.PostEditor.PostMetadata.gallery_count(form) == 1
|
||||
end
|
||||
|
||||
test "counts both [[gallery]] and inline images" do
|
||||
form = %{"content" => "\n[[gallery]]"}
|
||||
|
||||
assert BDS.Desktop.ShellLive.PostEditor.PostMetadata.gallery_count(form) == 1
|
||||
end
|
||||
|
||||
test "returns 0 when no gallery marks present" do
|
||||
form = %{"content" => "Just plain text"}
|
||||
|
||||
assert BDS.Desktop.ShellLive.PostEditor.PostMetadata.gallery_count(form) == 0
|
||||
end
|
||||
|
||||
test "handles empty content" do
|
||||
form = %{}
|
||||
|
||||
assert BDS.Desktop.ShellLive.PostEditor.PostMetadata.gallery_count(form) == 0
|
||||
end
|
||||
|
||||
test "counts multiple [[gallery]] occurrences" do
|
||||
form = %{"content" => "[[gallery]] some text [[gallery]]"}
|
||||
|
||||
assert BDS.Desktop.ShellLive.PostEditor.PostMetadata.gallery_count(form) == 2
|
||||
end
|
||||
|
||||
test "is case insensitive for [[gallery]]" do
|
||||
form = %{"content" => "[[Gallery]]"}
|
||||
|
||||
assert BDS.Desktop.ShellLive.PostEditor.PostMetadata.gallery_count(form) == 1
|
||||
end
|
||||
end
|
||||
|
||||
# ── Rendering macro smoke tests ────────────────────────────────────────────
|
||||
|
||||
describe "rendering macros" do
|
||||
test "[[gallery]] renders without linked media", %{project: project, post: post} do
|
||||
{:ok, metadata} = BDS.Metadata.get_project_metadata(project.id)
|
||||
language = metadata.main_language || "en"
|
||||
|
||||
template_context = BDS.Rendering.TemplateSelection.template_render_context(project.id)
|
||||
|
||||
result =
|
||||
BDS.Rendering.Filters.render_markdown(
|
||||
"[[gallery]]",
|
||||
%{},
|
||||
%{},
|
||||
language,
|
||||
template_context,
|
||||
post.id
|
||||
)
|
||||
|
||||
assert result =~ "gallery-empty"
|
||||
assert result =~ "macro-gallery"
|
||||
end
|
||||
|
||||
test "[[gallery]] renders linked media when present", %{project: project, post: post} do
|
||||
source = Path.join(project.data_path, "gallery_test.jpg")
|
||||
File.write!(source, "fake jpeg")
|
||||
|
||||
{:ok, media} =
|
||||
Media.import_media(%{project_id: project.id, source_path: source})
|
||||
|
||||
{:ok, _updated} =
|
||||
Media.update_media(media.id, %{
|
||||
title: "Test Image",
|
||||
alt: "Alt text",
|
||||
caption: "Caption"
|
||||
})
|
||||
|
||||
{:ok, _link} = Media.link_media_to_post(media.id, post.id)
|
||||
|
||||
{:ok, metadata} = BDS.Metadata.get_project_metadata(project.id)
|
||||
language = metadata.main_language || "en"
|
||||
template_context = BDS.Rendering.TemplateSelection.template_render_context(project.id)
|
||||
|
||||
result =
|
||||
BDS.Rendering.Filters.render_markdown(
|
||||
"[[gallery]]",
|
||||
%{},
|
||||
%{},
|
||||
language,
|
||||
template_context,
|
||||
post.id
|
||||
)
|
||||
|
||||
assert result =~ "macro-gallery"
|
||||
assert result =~ "gallery-item"
|
||||
end
|
||||
|
||||
test "[[tag_cloud]] renders without tags", %{project: project, post: post} do
|
||||
{:ok, metadata} = BDS.Metadata.get_project_metadata(project.id)
|
||||
language = metadata.main_language || "en"
|
||||
|
||||
template_context = BDS.Rendering.TemplateSelection.template_render_context(project.id)
|
||||
|
||||
result =
|
||||
BDS.Rendering.Filters.render_markdown(
|
||||
"[[tag_cloud]]",
|
||||
%{},
|
||||
%{},
|
||||
language,
|
||||
template_context,
|
||||
post.id
|
||||
)
|
||||
|
||||
assert result =~ "tag-cloud-empty"
|
||||
end
|
||||
|
||||
test "[[photo_archive]] renders without media", %{project: project, post: post} do
|
||||
{:ok, metadata} = BDS.Metadata.get_project_metadata(project.id)
|
||||
language = metadata.main_language || "en"
|
||||
|
||||
template_context = BDS.Rendering.TemplateSelection.template_render_context(project.id)
|
||||
|
||||
result =
|
||||
BDS.Rendering.Filters.render_markdown(
|
||||
"[[photo_archive]]",
|
||||
%{},
|
||||
%{},
|
||||
language,
|
||||
template_context,
|
||||
post.id
|
||||
)
|
||||
|
||||
assert result =~ "photo-archive-empty"
|
||||
end
|
||||
|
||||
test "[[photo_archive year=2024]] renders with date filter", %{project: project, post: post} do
|
||||
{:ok, metadata} = BDS.Metadata.get_project_metadata(project.id)
|
||||
language = metadata.main_language || "en"
|
||||
|
||||
template_context = BDS.Rendering.TemplateSelection.template_render_context(project.id)
|
||||
|
||||
result =
|
||||
BDS.Rendering.Filters.render_markdown(
|
||||
"[[photo_archive year=2024]]",
|
||||
%{},
|
||||
%{},
|
||||
language,
|
||||
template_context,
|
||||
post.id
|
||||
)
|
||||
|
||||
assert result =~ "photo-archive"
|
||||
end
|
||||
|
||||
test "[[youtube id=abc123]] still works", %{project: project, post: post} do
|
||||
{:ok, metadata} = BDS.Metadata.get_project_metadata(project.id)
|
||||
language = metadata.main_language || "en"
|
||||
|
||||
template_context = BDS.Rendering.TemplateSelection.template_render_context(project.id)
|
||||
|
||||
result =
|
||||
BDS.Rendering.Filters.render_markdown(
|
||||
"[[youtube id=abc123]]",
|
||||
%{},
|
||||
%{},
|
||||
language,
|
||||
template_context,
|
||||
post.id
|
||||
)
|
||||
|
||||
assert result =~ "youtube"
|
||||
assert result =~ "abc123"
|
||||
end
|
||||
|
||||
test "[[vimeo id=456789]] still works", %{project: project, post: post} do
|
||||
{:ok, metadata} = BDS.Metadata.get_project_metadata(project.id)
|
||||
language = metadata.main_language || "en"
|
||||
|
||||
template_context = BDS.Rendering.TemplateSelection.template_render_context(project.id)
|
||||
|
||||
result =
|
||||
BDS.Rendering.Filters.render_markdown(
|
||||
"[[vimeo id=456789]]",
|
||||
%{},
|
||||
%{},
|
||||
language,
|
||||
template_context,
|
||||
post.id
|
||||
)
|
||||
|
||||
assert result =~ "vimeo"
|
||||
assert result =~ "456789"
|
||||
end
|
||||
end
|
||||
end
|
||||
216
test/bds/image_import_pipeline_test.exs
Normal file
216
test/bds/image_import_pipeline_test.exs
Normal file
@@ -0,0 +1,216 @@
|
||||
defmodule BDS.ImageImportPipelineTest do
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
import Phoenix.ConnTest
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias BDS.Desktop.{FilePicker, Overlay}
|
||||
alias BDS.{Metadata, AI, Repo}
|
||||
|
||||
@endpoint BDS.Desktop.Endpoint
|
||||
|
||||
setup do
|
||||
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Repo)
|
||||
Ecto.Adapters.SQL.Sandbox.mode(Repo, {:shared, self()})
|
||||
|
||||
temp_dir =
|
||||
Path.join(System.tmp_dir!(), "bds-image-import-#{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: "Image Import Test", data_path: temp_dir})
|
||||
%{project: project, temp_dir: temp_dir}
|
||||
end
|
||||
|
||||
# ── FilePicker multi-select parsing ────────────────────────────────────────
|
||||
|
||||
describe "FilePicker.choose_files/2" do
|
||||
test "single selection returns a single-item list" do
|
||||
# Simulate what osascript returns for regular choose file
|
||||
result = FilePicker.parse_choose_files_result("/Users/test/image.png", false)
|
||||
assert result == {:ok, "/Users/test/image.png"}
|
||||
end
|
||||
|
||||
test "multi selection parses newline-separated paths" do
|
||||
result =
|
||||
FilePicker.parse_choose_files_result(
|
||||
"/Users/test/photo1.jpg\n/Users/test/photo2.png\n/Users/test/photo3.heic",
|
||||
true
|
||||
)
|
||||
|
||||
assert result ==
|
||||
{:ok, ["/Users/test/photo1.jpg", "/Users/test/photo2.png", "/Users/test/photo3.heic"]}
|
||||
end
|
||||
|
||||
test "multi selection filters out empty lines" do
|
||||
result =
|
||||
FilePicker.parse_choose_files_result(
|
||||
"/Users/test/photo1.jpg\n\n/Users/test/photo2.png\n \n/Users/test/photo3.heic\n",
|
||||
true
|
||||
)
|
||||
|
||||
assert result ==
|
||||
{:ok, ["/Users/test/photo1.jpg", "/Users/test/photo2.png", "/Users/test/photo3.heic"]}
|
||||
end
|
||||
|
||||
test "multi selection with single file returns list with one element" do
|
||||
result = FilePicker.parse_choose_files_result("/Users/test/photo1.jpg", true)
|
||||
assert result == {:ok, ["/Users/test/photo1.jpg"]}
|
||||
end
|
||||
end
|
||||
|
||||
# ── Metadata image_import_concurrency ───────────────────────────────────────
|
||||
|
||||
describe "Metadata image_import_concurrency" do
|
||||
test "defaults to 4 for new projects", %{project: project} do
|
||||
{:ok, metadata} = Metadata.get_project_metadata(project.id)
|
||||
assert metadata.image_import_concurrency == 4
|
||||
end
|
||||
|
||||
test "clamps to minimum 1", %{project: project} do
|
||||
{:ok, _metadata} =
|
||||
Metadata.update_project_metadata(project.id, %{image_import_concurrency: 0})
|
||||
|
||||
{:ok, metadata} = Metadata.get_project_metadata(project.id)
|
||||
assert metadata.image_import_concurrency == 1
|
||||
end
|
||||
|
||||
test "clamps to maximum 8", %{project: project} do
|
||||
{:ok, _metadata} =
|
||||
Metadata.update_project_metadata(project.id, %{image_import_concurrency: 100})
|
||||
|
||||
{:ok, metadata} = Metadata.get_project_metadata(project.id)
|
||||
assert metadata.image_import_concurrency == 8
|
||||
end
|
||||
|
||||
test "persists and reads correctly", %{project: project} do
|
||||
{:ok, _metadata} =
|
||||
Metadata.update_project_metadata(project.id, %{image_import_concurrency: 3})
|
||||
|
||||
{:ok, metadata} = Metadata.get_project_metadata(project.id)
|
||||
assert metadata.image_import_concurrency == 3
|
||||
end
|
||||
|
||||
test "handles string input", %{project: project} do
|
||||
{:ok, _metadata} =
|
||||
Metadata.update_project_metadata(project.id, %{image_import_concurrency: "5"})
|
||||
|
||||
{:ok, metadata} = Metadata.get_project_metadata(project.id)
|
||||
assert metadata.image_import_concurrency == 5
|
||||
end
|
||||
|
||||
test "handles nil as default", %{project: project} do
|
||||
{:ok, _metadata} =
|
||||
Metadata.update_project_metadata(project.id, %{image_import_concurrency: nil})
|
||||
|
||||
{:ok, metadata} = Metadata.get_project_metadata(project.id)
|
||||
assert metadata.image_import_concurrency == 4
|
||||
end
|
||||
|
||||
test "reflects in form as string", %{project: project} do
|
||||
{:ok, metadata} = Metadata.get_project_metadata(project.id)
|
||||
|
||||
form =
|
||||
BDS.Desktop.ShellLive.SettingsEditor.ProjectSettings.project_form(metadata)
|
||||
|
||||
assert form["image_import_concurrency"] == "4"
|
||||
end
|
||||
end
|
||||
|
||||
# ── Overlay struct post_id ────────────────────────────────────────────────
|
||||
|
||||
describe "Overlay insert_media" do
|
||||
test "open(:post, :insert_media, context) includes post_id" do
|
||||
context = %{
|
||||
current_tab: %{
|
||||
type: :post,
|
||||
id: "post-uuid-123",
|
||||
title: "Test Post",
|
||||
subtitle: "draft"
|
||||
},
|
||||
media: [],
|
||||
insert_media_title: "Insert Media"
|
||||
}
|
||||
|
||||
overlay = Overlay.open(:post, :insert_media, context)
|
||||
assert overlay.post_id == "post-uuid-123"
|
||||
end
|
||||
|
||||
test "post_id is nil when opened from non-post context" do
|
||||
context = %{
|
||||
current_tab: %{
|
||||
type: :media,
|
||||
id: "media-uuid-456",
|
||||
title: "Test Media",
|
||||
subtitle: "image/png"
|
||||
},
|
||||
media: [],
|
||||
insert_media_title: "Insert Media"
|
||||
}
|
||||
|
||||
overlay = Overlay.open(:media, :insert_media, context)
|
||||
assert overlay == nil
|
||||
end
|
||||
|
||||
test "set_search_query preserves post_id" do
|
||||
overlay = %{
|
||||
kind: :insert_media,
|
||||
title: "Insert Media",
|
||||
search_query: "",
|
||||
results: [],
|
||||
all_media: [],
|
||||
post_id: "post-uuid-789"
|
||||
}
|
||||
|
||||
result = Overlay.set_search_query(overlay, "search term")
|
||||
assert result.post_id == "post-uuid-789"
|
||||
end
|
||||
end
|
||||
|
||||
# ── Airplane mode gating via shell_live event ─────────────────────────────
|
||||
|
||||
describe "overlay_add_images airplane mode gating" do
|
||||
setup do
|
||||
prev_auto = System.get_env("BDS_DESKTOP_AUTOMATION")
|
||||
System.put_env("BDS_DESKTOP_AUTOMATION", "1")
|
||||
|
||||
on_exit(fn ->
|
||||
if prev_auto,
|
||||
do: System.put_env("BDS_DESKTOP_AUTOMATION", prev_auto),
|
||||
else: System.delete_env("BDS_DESKTOP_AUTOMATION")
|
||||
end)
|
||||
end
|
||||
|
||||
test "shows toast in airplane mode when Add Gallery Images is clicked", %{project: project} do
|
||||
{:ok, _project} = BDS.Projects.set_active_project(project.id)
|
||||
|
||||
assert :ok = AI.set_airplane_mode(true)
|
||||
|
||||
{:ok, post} =
|
||||
BDS.Posts.create_post(%{
|
||||
project_id: project.id,
|
||||
title: "Gallery Post",
|
||||
content: "Content"
|
||||
})
|
||||
|
||||
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
|
||||
|
||||
render_click(view, "pin_sidebar_item", %{
|
||||
"route" => "post",
|
||||
"id" => post.id,
|
||||
"title" => post.title,
|
||||
"subtitle" => "draft"
|
||||
})
|
||||
|
||||
html =
|
||||
view
|
||||
|> element("[phx-click='add_gallery_images']")
|
||||
|> render_click()
|
||||
|
||||
assert html =~ "Automatic AI actions stay gated by airplane mode"
|
||||
|
||||
assert :ok = AI.set_airplane_mode(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -138,6 +138,7 @@ defmodule BDS.Repo.SchemaMigrationTest do
|
||||
"title",
|
||||
"model",
|
||||
"copilot_session_id",
|
||||
"surface_state",
|
||||
"created_at",
|
||||
"updated_at"
|
||||
],
|
||||
|
||||
86
test/bds/translation_completeness_test.exs
Normal file
86
test/bds/translation_completeness_test.exs
Normal file
@@ -0,0 +1,86 @@
|
||||
defmodule BDS.TranslationCompletenessTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
@translated_locales ~w(de fr it es)
|
||||
|
||||
@doc """
|
||||
Counts empty msgstr entries per non-English locale .po file and ensures
|
||||
the count never increases. When a new msgid is added to the codebase its
|
||||
translations MUST be provided for all supported locales; an empty msgstr
|
||||
in de/fr/it/es causes this test to fail.
|
||||
|
||||
English files are excluded — gettext treats empty msgstr as "return msgid
|
||||
unchanged" for the source language, which is the intended fallback.
|
||||
|
||||
The expected counts below represent current untranslated legacy entries
|
||||
that must decrease over time, never increase. When the count decreases
|
||||
because translations were added, update the expected count in this test.
|
||||
"""
|
||||
test "empty msgstr counts do not increase in non-English locale .po files" do
|
||||
expected = %{
|
||||
"de/default.po" => 0,
|
||||
"de/render.po" => 0,
|
||||
"de/ui.po" => 153,
|
||||
"fr/default.po" => 0,
|
||||
"fr/render.po" => 0,
|
||||
"fr/ui.po" => 153,
|
||||
"it/default.po" => 0,
|
||||
"it/render.po" => 0,
|
||||
"it/ui.po" => 153,
|
||||
"es/default.po" => 0,
|
||||
"es/render.po" => 0,
|
||||
"es/ui.po" => 153
|
||||
}
|
||||
|
||||
actual =
|
||||
for locale <- @translated_locales,
|
||||
domain <- ~w(ui default render) do
|
||||
file = "priv/gettext/#{locale}/LC_MESSAGES/#{domain}.po"
|
||||
|
||||
if File.exists?(file) do
|
||||
count = empty_msgstr_count(file)
|
||||
key = "#{locale}/#{domain}.po"
|
||||
{key, count}
|
||||
end
|
||||
end
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|> Map.new()
|
||||
|
||||
for {file, expected_count} <- expected do
|
||||
actual_count = Map.get(actual, file, 0)
|
||||
|
||||
assert actual_count <= expected_count,
|
||||
"#{file}: empty msgstr count increased from #{expected_count} to #{actual_count}. " <>
|
||||
"All new msgid entries MUST have translations for every supported locale (de, fr, it, es). " <>
|
||||
"When the count decreases because translations were added, update the expected count in this test."
|
||||
end
|
||||
end
|
||||
|
||||
defp empty_msgstr_count(file) do
|
||||
file
|
||||
|> File.read!()
|
||||
|> String.split("\n")
|
||||
|> Enum.reduce({nil, 0}, fn line, {current_msgid, count} ->
|
||||
trimmed = String.trim(line)
|
||||
|
||||
case {trimmed, current_msgid} do
|
||||
{"msgstr \"\"", nil} ->
|
||||
{nil, count}
|
||||
|
||||
{"msgstr \"\"", msgid} ->
|
||||
if msgid == "" do
|
||||
{nil, count}
|
||||
else
|
||||
{nil, count + 1}
|
||||
end
|
||||
|
||||
{"msgid \"" <> rest, _} ->
|
||||
{String.trim_trailing(rest, "\""), count}
|
||||
|
||||
_ ->
|
||||
{current_msgid, count}
|
||||
end
|
||||
end)
|
||||
|> elem(1)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user