Current section

Files

Jump to
accrue lib accrue test pdf_assertions.ex
Raw

lib/accrue/test/pdf_assertions.ex

defmodule Accrue.Test.PdfAssertions do
@moduledoc """
ExUnit-style assertions for `Accrue.PDF.Test` render captures (D-34).
Symmetric with `Accrue.Test.MailerAssertions`.
## Usage
use Accrue.Test.PdfAssertions
# or
import Accrue.Test.PdfAssertions
## Matching
- `:invoice_id` -> `opts[:invoice_id] || opts["invoice_id"]`
- `:contains` (string) -> `String.contains?(html, value)`
- `:matches` (1-arity fn) -> predicate escape hatch on normalized render data
- `:opts_include` (keyword) -> subset match on the opts keyword list
All helpers consume `{:pdf_rendered, html, opts}` tuples sent by
`Accrue.PDF.Test.render/2`. Messages are process-local; `async: true`
is safe. Cross-process owner or global capture modes are intentionally
not the default; if a host test needs them, name that mode explicitly
and treat it with the same caution as Mox or Swoosh global mode.
"""
defmacro __using__(_opts) do
quote do
import Accrue.Test.PdfAssertions
end
end
@doc """
Asserts that a PDF was rendered (via `Accrue.PDF.Test`) matching the
given `matchers`. Flunks if no matching message is received within
`timeout` (default 100ms).
"""
defmacro assert_pdf_rendered(matchers \\ [], timeout \\ 100) do
quote do
matchers = unquote(matchers)
t = unquote(timeout)
observed = Accrue.Test.PdfAssertions.__collect_pdfs__(t)
case Enum.find(observed, fn {html, opts} ->
Accrue.Test.PdfAssertions.__match__(html, opts, matchers)
end) do
{html, opts} ->
{html, opts}
nil ->
ExUnit.Assertions.flunk(
"no PDF rendered within #{t}ms and did not match #{inspect(matchers)}; " <>
"Observed PDFs: #{inspect(Accrue.Test.PdfAssertions.__summaries__(observed))}"
)
end
end
end
@doc """
Refutes that a PDF render matching `matchers` occurred within `timeout`
(default 100ms). Non-matching render messages are ignored.
"""
defmacro refute_pdf_rendered(matchers \\ [], timeout \\ 100) do
quote do
matchers = unquote(matchers)
t = unquote(timeout)
observed = Accrue.Test.PdfAssertions.__collect_pdfs__(t)
case Enum.find(observed, fn {html, opts} ->
Accrue.Test.PdfAssertions.__match__(html, opts, matchers)
end) do
{_html, _opts} ->
ExUnit.Assertions.flunk(
"unexpected PDF rendered matching #{inspect(matchers)}; " <>
"Observed PDFs: #{inspect(Accrue.Test.PdfAssertions.__summaries__(observed))}"
)
nil ->
:ok
end
end
end
@doc false
def __match__(html, opts, matchers) do
render = %{html: html, opts: opts, invoice_id: opts_value(opts, :invoice_id)}
Enum.all?(normalize_matcher(matchers), fn
{:invoice_id, value} ->
render.invoice_id == value
{:contains, value} when is_binary(value) ->
String.contains?(html, value)
{:matches, fun} when is_function(fun, 1) ->
predicate_match?(fun, render, html)
{:opts_include, kw} when is_list(kw) ->
Enum.all?(kw, fn {key, value} -> opts_value(opts, key) == value end)
{key, value} ->
opts_value(opts, key) == value
end)
end
@doc false
def __collect_pdfs__(timeout), do: collect_pdfs([], timeout)
@doc false
def __summaries__(observed) do
Enum.map(observed, fn {html, opts} ->
%{
invoice_id: opts_value(opts, :invoice_id),
html_preview: String.slice(html, 0, 80),
opts_keys: opts_keys(opts)
}
end)
end
defp collect_pdfs(acc, timeout) do
receive do
{:pdf_rendered, html, opts} -> collect_pdfs([{html, opts} | acc], 0)
after
timeout -> Enum.reverse(acc)
end
end
defp normalize_matcher(matchers) when is_list(matchers), do: matchers
defp normalize_matcher(matchers) when is_map(matchers), do: Map.to_list(matchers)
defp normalize_matcher(matchers) when is_function(matchers, 1), do: [matches: matchers]
defp normalize_matcher(nil), do: []
defp opts_value(opts, key) when is_list(opts) do
keyword_value = if is_atom(key), do: Keyword.get(opts, key)
keyword_value || list_string_value(opts, key)
end
defp opts_value(opts, key) when is_map(opts),
do: Map.get(opts, key, Map.get(opts, to_string(key)))
defp opts_value(_opts, _key), do: nil
defp opts_keys(opts) when is_list(opts), do: Keyword.keys(opts)
defp opts_keys(opts) when is_map(opts), do: Map.keys(opts)
defp opts_keys(_opts), do: []
defp list_string_value(opts, key) do
string_key = to_string(key)
Enum.find_value(opts, fn
{^string_key, value} -> value
_other -> nil
end)
end
defp predicate_match?(fun, preferred, fallback) do
fun.(preferred)
rescue
_ -> fun.(fallback)
end
end