Packages

A real-browser external-app verifier for patch-safe Phoenix LiveView interactions, with secondary unstyled reference components.

Current section

Files

Jump to
live_interaction_contracts lib mix tasks live_interaction_contracts.check.ex
Raw

lib/mix/tasks/live_interaction_contracts.check.ex

defmodule Mix.Tasks.LiveInteractionContracts.Check do
@shortdoc "Check a consumer LiveView interaction contract in a real browser"
@moduledoc """
Runs one declarative interaction contract against an already-started LiveView app.
The runner performs real clicks, requires a subject-local patch acknowledgment,
compares DOM object identity and native browser state, and writes JSON evidence.
mix live_interaction_contracts.check \
--url http://127.0.0.1:4000 \
--contract test/interaction_contracts/account_menu.json
Options:
* `--url` — required base URL; loopback only unless `--allow-remote`
* `--contract` — required v1 JSON contract path
* `--browser` — chromium (default), firefox, or webkit
* `--out` — JSON evidence path (default: live-interaction-contracts-result.json)
* `--allow-remote` — permit an explicitly safe non-loopback test target
* `--validate-only` — validate contract/target without launching a browser
Exit status is non-zero for fail, inconclusive, and infrastructure/contract error.
"""
use Mix.Task
@version Mix.Project.config()[:version] || "dev"
@impl true
def run(argv) do
{opts, positional, invalid} =
OptionParser.parse(argv,
strict: [
url: :string,
contract: :string,
browser: :string,
out: :string,
allow_remote: :boolean,
validate_only: :boolean
]
)
out = Path.expand(opts[:out] || "live-interaction-contracts-result.json")
contract = opts[:contract] && Path.expand(opts[:contract])
if contract == out, do: Mix.raise("--out must not overwrite --contract")
File.rm(out)
if positional != [] or invalid != [] do
Mix.raise("unexpected arguments: #{inspect(positional ++ invalid)}")
end
url = opts[:url] || Mix.raise("--url required")
contract = contract || Mix.raise("--contract required")
browser = opts[:browser] || "chromium"
unless browser in ~w(chromium firefox webkit), do: Mix.raise("unsupported --browser #{browser}")
unless File.regular?(contract), do: Mix.raise("--contract is not a regular file: #{contract}")
runner = Path.join([LiveInteractionContracts.priv_dir(), "contract-runner", "check.mjs"])
unless File.regular?(runner), do: Mix.raise("contract runner missing from package: #{runner}")
args = ["--url", url, "--contract", contract, "--browser", browser, "--out", out]
args = if opts[:allow_remote], do: args ++ ["--allow-remote"], else: args
args = if opts[:validate_only], do: args ++ ["--validate-only"], else: args
env = [{"LIC_RUNNER_VERSION", @version}]
case System.cmd("node", [runner | args], into: IO.stream(:stdio, :line), stderr_to_stdout: true, env: env) do
{_, 0} -> :ok
{_, code} -> Mix.raise("interaction contract did not pass (exit #{code})")
end
end
end