test: wait on observable state instead of blind sleeps
Replace the fixed Process.sleep waits in the import, members-PDF and field-visibility tests with event-based / bounded-poll waits on the observable condition, removing a known flakiness vector.
This commit is contained in:
parent
ccd1f81e3e
commit
655fd80524
3 changed files with 55 additions and 36 deletions
|
|
@ -274,17 +274,38 @@ defmodule Mv.Membership.MembersPDFTest do
|
||||||
|
|
||||||
assert {:ok, _pdf_binary} = result
|
assert {:ok, _pdf_binary} = result
|
||||||
|
|
||||||
# Wait a bit for cleanup (async cleanup might take a moment)
|
count_export_dirs = fn ->
|
||||||
Process.sleep(100)
|
|
||||||
|
|
||||||
# Count temp directories after
|
|
||||||
after_count =
|
|
||||||
temp_base
|
temp_base
|
||||||
|> File.ls!()
|
|> File.ls!()
|
||||||
|> Enum.count(fn name -> String.starts_with?(name, "mv_pdf_export_") end)
|
|> Enum.count(fn name -> String.starts_with?(name, "mv_pdf_export_") end)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Poll the observable cleanup condition (temp-dir count returns to the baseline)
|
||||||
|
# with a bounded deadline instead of a fixed sleep, so the test waits no longer
|
||||||
|
# than the cleanup actually needs and still fails if cleanup never runs.
|
||||||
|
after_count = poll_until_cleaned(count_export_dirs, before_count, 100)
|
||||||
|
|
||||||
# Should have same or fewer temp dirs (cleanup should have run)
|
# Should have same or fewer temp dirs (cleanup should have run)
|
||||||
assert after_count <= before_count + 1
|
assert after_count <= before_count + 1
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Bounded poll: returns the export-dir count once it drops back to the baseline
|
||||||
|
# (cleanup done), or the last observed count when the attempt budget is exhausted
|
||||||
|
# (so the caller's assertion reports the real state on a genuine cleanup stall).
|
||||||
|
defp poll_until_cleaned(count_fun, baseline, attempts_left) do
|
||||||
|
current = count_fun.()
|
||||||
|
|
||||||
|
cond do
|
||||||
|
current <= baseline ->
|
||||||
|
current
|
||||||
|
|
||||||
|
attempts_left <= 0 ->
|
||||||
|
current
|
||||||
|
|
||||||
|
true ->
|
||||||
|
Process.sleep(10)
|
||||||
|
poll_until_cleaned(count_fun, baseline, attempts_left - 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,27 @@ defmodule MvWeb.ImportLiveTest do
|
||||||
confirm_import(view)
|
confirm_import(view)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp wait_for_import_completion, do: Process.sleep(1000)
|
# Waits for the asynchronous chunk-import to finish by polling the rendered
|
||||||
|
# results panel, instead of a fixed sleep. In the test sandbox the chunks run
|
||||||
|
# in-process and signal completion via self-messages; each render/2 forces the
|
||||||
|
# LiveView to drain its mailbox before replying, so the panel appears once all
|
||||||
|
# chunk messages have been processed. Bounded so a genuine stall still fails.
|
||||||
|
defp wait_for_import_completion(view) do
|
||||||
|
wait_for_import_completion(view, 200)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp wait_for_import_completion(_view, 0) do
|
||||||
|
flunk("import did not complete: results panel never rendered")
|
||||||
|
end
|
||||||
|
|
||||||
|
defp wait_for_import_completion(view, attempts_left) do
|
||||||
|
if has_element?(view, "[data-testid='import-results-panel']") do
|
||||||
|
:ok
|
||||||
|
else
|
||||||
|
Process.sleep(10)
|
||||||
|
wait_for_import_completion(view, attempts_left - 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# ---------- Business logic: Authorization ----------
|
# ---------- Business logic: Authorization ----------
|
||||||
describe "Authorization" do
|
describe "Authorization" do
|
||||||
|
|
@ -67,7 +87,7 @@ defmodule MvWeb.ImportLiveTest do
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, ~p"/admin/import")
|
{:ok, view, _html} = live(conn, ~p"/admin/import")
|
||||||
run_full_import(view, csv_content)
|
run_full_import(view, csv_content)
|
||||||
wait_for_import_completion()
|
wait_for_import_completion(view)
|
||||||
|
|
||||||
assert has_element?(view, "[data-testid='import-results-panel']")
|
assert has_element?(view, "[data-testid='import-results-panel']")
|
||||||
assert has_element?(view, "[data-testid='import-summary']")
|
assert has_element?(view, "[data-testid='import-summary']")
|
||||||
|
|
@ -131,7 +151,7 @@ defmodule MvWeb.ImportLiveTest do
|
||||||
} do
|
} do
|
||||||
{:ok, view, _html} = live(conn, ~p"/admin/import")
|
{:ok, view, _html} = live(conn, ~p"/admin/import")
|
||||||
run_full_import(view, csv_content, "invalid_import.csv")
|
run_full_import(view, csv_content, "invalid_import.csv")
|
||||||
wait_for_import_completion()
|
wait_for_import_completion(view)
|
||||||
|
|
||||||
assert has_element?(view, "[data-testid='import-results-panel']")
|
assert has_element?(view, "[data-testid='import-results-panel']")
|
||||||
assert has_element?(view, "[data-testid='import-error-list']")
|
assert has_element?(view, "[data-testid='import-error-list']")
|
||||||
|
|
@ -150,7 +170,7 @@ defmodule MvWeb.ImportLiveTest do
|
||||||
for i <- 1..100, do: "Row#{i};Last#{i};;Country#{i};City#{i};Street#{i};12345\n"
|
for i <- 1..100, do: "Row#{i};Last#{i};;Country#{i};City#{i};Street#{i};12345\n"
|
||||||
|
|
||||||
run_full_import(view, header <> Enum.join(invalid_rows), "large_invalid.csv")
|
run_full_import(view, header <> Enum.join(invalid_rows), "large_invalid.csv")
|
||||||
wait_for_import_completion()
|
wait_for_import_completion(view)
|
||||||
|
|
||||||
assert has_element?(view, "[data-testid='import-results-panel']")
|
assert has_element?(view, "[data-testid='import-results-panel']")
|
||||||
assert has_element?(view, "[data-testid='import-error-list']")
|
assert has_element?(view, "[data-testid='import-error-list']")
|
||||||
|
|
@ -182,7 +202,7 @@ defmodule MvWeb.ImportLiveTest do
|
||||||
|> File.read!()
|
|> File.read!()
|
||||||
|
|
||||||
run_full_import(view, csv_content, "bom_import.csv")
|
run_full_import(view, csv_content, "bom_import.csv")
|
||||||
wait_for_import_completion()
|
wait_for_import_completion(view)
|
||||||
|
|
||||||
assert has_element?(view, "[data-testid='import-results-panel']")
|
assert has_element?(view, "[data-testid='import-results-panel']")
|
||||||
html = render(view)
|
html = render(view)
|
||||||
|
|
@ -200,7 +220,7 @@ defmodule MvWeb.ImportLiveTest do
|
||||||
|> File.read!()
|
|> File.read!()
|
||||||
|
|
||||||
run_full_import(view, csv_content, "empty_lines.csv")
|
run_full_import(view, csv_content, "empty_lines.csv")
|
||||||
wait_for_import_completion()
|
wait_for_import_completion(view)
|
||||||
|
|
||||||
assert has_element?(view, "[data-testid='import-error-list']")
|
assert has_element?(view, "[data-testid='import-error-list']")
|
||||||
html = render(view)
|
html = render(view)
|
||||||
|
|
@ -214,7 +234,7 @@ defmodule MvWeb.ImportLiveTest do
|
||||||
} do
|
} do
|
||||||
{:ok, view, _html} = live(conn, ~p"/admin/import")
|
{:ok, view, _html} = live(conn, ~p"/admin/import")
|
||||||
run_full_import(view, csv_content, "unknown_custom.csv")
|
run_full_import(view, csv_content, "unknown_custom.csv")
|
||||||
wait_for_import_completion()
|
wait_for_import_completion(view)
|
||||||
|
|
||||||
assert has_element?(view, "[data-testid='import-results-panel']")
|
assert has_element?(view, "[data-testid='import-results-panel']")
|
||||||
assert has_element?(view, "[data-testid='import-warnings']")
|
assert has_element?(view, "[data-testid='import-warnings']")
|
||||||
|
|
@ -279,7 +299,7 @@ defmodule MvWeb.ImportLiveTest do
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, ~p"/admin/import")
|
{:ok, view, _html} = live(conn, ~p"/admin/import")
|
||||||
run_full_import(view, csv_content)
|
run_full_import(view, csv_content)
|
||||||
wait_for_import_completion()
|
wait_for_import_completion(view)
|
||||||
assert has_element?(view, "[data-testid='import-progress-container']")
|
assert has_element?(view, "[data-testid='import-progress-container']")
|
||||||
html = render(view)
|
html = render(view)
|
||||||
assert html =~ "aria-live"
|
assert html =~ "aria-live"
|
||||||
|
|
@ -342,7 +362,7 @@ defmodule MvWeb.ImportLiveTest do
|
||||||
} do
|
} do
|
||||||
{:ok, view, _html} = live(conn, ~p"/admin/import")
|
{:ok, view, _html} = live(conn, ~p"/admin/import")
|
||||||
run_full_import(view, csv_content)
|
run_full_import(view, csv_content)
|
||||||
wait_for_import_completion()
|
wait_for_import_completion(view)
|
||||||
|
|
||||||
assert has_element?(view, "[data-testid='import-results-panel']")
|
assert has_element?(view, "[data-testid='import-results-panel']")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -157,9 +157,6 @@ defmodule MvWeb.MemberLive.IndexFieldVisibilityTest do
|
||||||
|> element("button[phx-click='select_item'][phx-value-item='email']")
|
|> element("button[phx-click='select_item'][phx-value-item='email']")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
# Wait for update
|
|
||||||
:timer.sleep(100)
|
|
||||||
|
|
||||||
# Email should no longer be visible
|
# Email should no longer be visible
|
||||||
html = render(view)
|
html = render(view)
|
||||||
refute html =~ "alice@example.com"
|
refute html =~ "alice@example.com"
|
||||||
|
|
@ -186,9 +183,6 @@ defmodule MvWeb.MemberLive.IndexFieldVisibilityTest do
|
||||||
|> element("button[phx-click='select_item'][phx-value-item='#{custom_field_string}']")
|
|> element("button[phx-click='select_item'][phx-value-item='#{custom_field_string}']")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
# Wait for update
|
|
||||||
:timer.sleep(100)
|
|
||||||
|
|
||||||
# Custom field should no longer be visible
|
# Custom field should no longer be visible
|
||||||
html = render(view)
|
html = render(view)
|
||||||
refute html =~ "M001"
|
refute html =~ "M001"
|
||||||
|
|
@ -213,9 +207,6 @@ defmodule MvWeb.MemberLive.IndexFieldVisibilityTest do
|
||||||
|> element("button[phx-click='select_all']")
|
|> element("button[phx-click='select_all']")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
# Wait for update
|
|
||||||
:timer.sleep(100)
|
|
||||||
|
|
||||||
# All fields should be visible
|
# All fields should be visible
|
||||||
html = render(view)
|
html = render(view)
|
||||||
assert html =~ "alice@example.com"
|
assert html =~ "alice@example.com"
|
||||||
|
|
@ -237,9 +228,6 @@ defmodule MvWeb.MemberLive.IndexFieldVisibilityTest do
|
||||||
|> element("button[phx-click='select_none']")
|
|> element("button[phx-click='select_none']")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
# Wait for update
|
|
||||||
:timer.sleep(100)
|
|
||||||
|
|
||||||
# Only first_name should be visible (it's always shown)
|
# Only first_name should be visible (it's always shown)
|
||||||
html = render(view)
|
html = render(view)
|
||||||
# Email and street should be hidden
|
# Email and street should be hidden
|
||||||
|
|
@ -262,9 +250,6 @@ defmodule MvWeb.MemberLive.IndexFieldVisibilityTest do
|
||||||
|> element("button[phx-click='select_item'][phx-value-item='email']")
|
|> element("button[phx-click='select_item'][phx-value-item='email']")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
# Wait for URL update
|
|
||||||
:timer.sleep(100)
|
|
||||||
|
|
||||||
# Check that URL contains fields parameter
|
# Check that URL contains fields parameter
|
||||||
# Note: In LiveView tests, we check the rendered HTML for the updated state
|
# Note: In LiveView tests, we check the rendered HTML for the updated state
|
||||||
# The actual URL update happens via push_patch
|
# The actual URL update happens via push_patch
|
||||||
|
|
@ -329,8 +314,6 @@ defmodule MvWeb.MemberLive.IndexFieldVisibilityTest do
|
||||||
|> element("button[phx-click='select_item'][phx-value-item='email']")
|
|> element("button[phx-click='select_item'][phx-value-item='email']")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
:timer.sleep(100)
|
|
||||||
|
|
||||||
html = render(view)
|
html = render(view)
|
||||||
refute html =~ "alice@example.com"
|
refute html =~ "alice@example.com"
|
||||||
end
|
end
|
||||||
|
|
@ -387,8 +370,6 @@ defmodule MvWeb.MemberLive.IndexFieldVisibilityTest do
|
||||||
view
|
view
|
||||||
|> element("button[phx-click='select_item'][phx-value-item='email']")
|
|> element("button[phx-click='select_item'][phx-value-item='email']")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
:timer.sleep(50)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Should still work correctly
|
# Should still work correctly
|
||||||
|
|
@ -458,9 +439,6 @@ defmodule MvWeb.MemberLive.IndexFieldVisibilityTest do
|
||||||
|> element("button[phx-click='select_item'][phx-value-item='email']")
|
|> element("button[phx-click='select_item'][phx-value-item='email']")
|
||||||
|> render_keydown(%{key: "Enter"})
|
|> render_keydown(%{key: "Enter"})
|
||||||
|
|
||||||
# Wait for update
|
|
||||||
:timer.sleep(100)
|
|
||||||
|
|
||||||
# Email should no longer be visible
|
# Email should no longer be visible
|
||||||
html = render(view)
|
html = render(view)
|
||||||
refute html =~ "alice@example.com"
|
refute html =~ "alice@example.com"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue