diff --git a/bin/smoke_test.exs b/bin/smoke_test.exs
index d97534c..01ebafc 100755
--- a/bin/smoke_test.exs
+++ b/bin/smoke_test.exs
@@ -13,17 +13,12 @@ check_output = fn (output, pattern) ->
end
end
-demo_project_path = "./demo"
-demo_test_files =
- [
- "test/demo_web/integration/pages_wallaby_test.exs",
- "test/demo_web/integration/pages_hound_test.exs"
- ]
+demo_project_paths = ["./demo/demo_hound", "./demo/demo_wallaby"]
-Enum.each(demo_test_files, fn demo_test_file ->
- IO.puts("checking #{demo_test_file}")
+Enum.each(demo_project_paths, fn demo_project_path ->
+ IO.puts("checking #{demo_project_path}")
- {output, 2} = System.cmd("mix", ["test", demo_test_file], cd: demo_project_path)
+ {output, 2} = System.cmd("mix", ["test", "test/demo_web/integration/pages_test.exs"], cd: demo_project_path)
check_output.(output, ~r/3 (features|tests), 2 failures/)
check_output.(output, "invalid page with accessibility errors")
@@ -37,7 +32,7 @@ Enum.each(demo_test_files, fn demo_test_file ->
check_output.(output, "1.
Angelika's Star Trek series ranking ")
check_output.(output, "... and 10 more nodes")
- IO.puts("ok #{demo_test_file}")
+ IO.puts("ok #{demo_project_path}")
end)
IO.puts("all ok")
diff --git a/demo/.formatter.exs b/demo/demo_hound/.formatter.exs
similarity index 100%
rename from demo/.formatter.exs
rename to demo/demo_hound/.formatter.exs
diff --git a/demo/.gitignore b/demo/demo_hound/.gitignore
similarity index 100%
rename from demo/.gitignore
rename to demo/demo_hound/.gitignore
diff --git a/demo/README.md b/demo/demo_hound/README.md
similarity index 100%
rename from demo/README.md
rename to demo/demo_hound/README.md
diff --git a/demo/assets/js/app.js b/demo/demo_hound/assets/js/app.js
similarity index 100%
rename from demo/assets/js/app.js
rename to demo/demo_hound/assets/js/app.js
diff --git a/demo/assets/vendor/topbar.js b/demo/demo_hound/assets/vendor/topbar.js
similarity index 100%
rename from demo/assets/vendor/topbar.js
rename to demo/demo_hound/assets/vendor/topbar.js
diff --git a/demo/config/config.exs b/demo/demo_hound/config/config.exs
similarity index 100%
rename from demo/config/config.exs
rename to demo/demo_hound/config/config.exs
diff --git a/demo/config/dev.exs b/demo/demo_hound/config/dev.exs
similarity index 100%
rename from demo/config/dev.exs
rename to demo/demo_hound/config/dev.exs
diff --git a/demo/config/prod.exs b/demo/demo_hound/config/prod.exs
similarity index 100%
rename from demo/config/prod.exs
rename to demo/demo_hound/config/prod.exs
diff --git a/demo/config/runtime.exs b/demo/demo_hound/config/runtime.exs
similarity index 100%
rename from demo/config/runtime.exs
rename to demo/demo_hound/config/runtime.exs
diff --git a/demo/config/test.exs b/demo/demo_hound/config/test.exs
similarity index 100%
rename from demo/config/test.exs
rename to demo/demo_hound/config/test.exs
diff --git a/demo/lib/demo.ex b/demo/demo_hound/lib/demo.ex
similarity index 100%
rename from demo/lib/demo.ex
rename to demo/demo_hound/lib/demo.ex
diff --git a/demo/lib/demo/application.ex b/demo/demo_hound/lib/demo/application.ex
similarity index 100%
rename from demo/lib/demo/application.ex
rename to demo/demo_hound/lib/demo/application.ex
diff --git a/demo/lib/demo_web.ex b/demo/demo_hound/lib/demo_web.ex
similarity index 100%
rename from demo/lib/demo_web.ex
rename to demo/demo_hound/lib/demo_web.ex
diff --git a/demo/lib/demo_web/components/core_components.ex b/demo/demo_hound/lib/demo_web/components/core_components.ex
similarity index 100%
rename from demo/lib/demo_web/components/core_components.ex
rename to demo/demo_hound/lib/demo_web/components/core_components.ex
diff --git a/demo/lib/demo_web/components/layouts.ex b/demo/demo_hound/lib/demo_web/components/layouts.ex
similarity index 100%
rename from demo/lib/demo_web/components/layouts.ex
rename to demo/demo_hound/lib/demo_web/components/layouts.ex
diff --git a/demo/lib/demo_web/components/layouts/app.html.heex b/demo/demo_hound/lib/demo_web/components/layouts/app.html.heex
similarity index 100%
rename from demo/lib/demo_web/components/layouts/app.html.heex
rename to demo/demo_hound/lib/demo_web/components/layouts/app.html.heex
diff --git a/demo/lib/demo_web/components/layouts/root.html.heex b/demo/demo_hound/lib/demo_web/components/layouts/root.html.heex
similarity index 100%
rename from demo/lib/demo_web/components/layouts/root.html.heex
rename to demo/demo_hound/lib/demo_web/components/layouts/root.html.heex
diff --git a/demo/lib/demo_web/controllers/error_html.ex b/demo/demo_hound/lib/demo_web/controllers/error_html.ex
similarity index 100%
rename from demo/lib/demo_web/controllers/error_html.ex
rename to demo/demo_hound/lib/demo_web/controllers/error_html.ex
diff --git a/demo/lib/demo_web/controllers/error_json.ex b/demo/demo_hound/lib/demo_web/controllers/error_json.ex
similarity index 100%
rename from demo/lib/demo_web/controllers/error_json.ex
rename to demo/demo_hound/lib/demo_web/controllers/error_json.ex
diff --git a/demo/lib/demo_web/controllers/page_controller.ex b/demo/demo_hound/lib/demo_web/controllers/page_controller.ex
similarity index 100%
rename from demo/lib/demo_web/controllers/page_controller.ex
rename to demo/demo_hound/lib/demo_web/controllers/page_controller.ex
diff --git a/demo/lib/demo_web/controllers/page_html.ex b/demo/demo_hound/lib/demo_web/controllers/page_html.ex
similarity index 100%
rename from demo/lib/demo_web/controllers/page_html.ex
rename to demo/demo_hound/lib/demo_web/controllers/page_html.ex
diff --git a/demo/lib/demo_web/controllers/page_html/dynamic_invalid.html.heex b/demo/demo_hound/lib/demo_web/controllers/page_html/dynamic_invalid.html.heex
similarity index 100%
rename from demo/lib/demo_web/controllers/page_html/dynamic_invalid.html.heex
rename to demo/demo_hound/lib/demo_web/controllers/page_html/dynamic_invalid.html.heex
diff --git a/demo/lib/demo_web/controllers/page_html/invalid.html.heex b/demo/demo_hound/lib/demo_web/controllers/page_html/invalid.html.heex
similarity index 100%
rename from demo/lib/demo_web/controllers/page_html/invalid.html.heex
rename to demo/demo_hound/lib/demo_web/controllers/page_html/invalid.html.heex
diff --git a/demo/lib/demo_web/controllers/page_html/valid.html.heex b/demo/demo_hound/lib/demo_web/controllers/page_html/valid.html.heex
similarity index 100%
rename from demo/lib/demo_web/controllers/page_html/valid.html.heex
rename to demo/demo_hound/lib/demo_web/controllers/page_html/valid.html.heex
diff --git a/demo/lib/demo_web/endpoint.ex b/demo/demo_hound/lib/demo_web/endpoint.ex
similarity index 100%
rename from demo/lib/demo_web/endpoint.ex
rename to demo/demo_hound/lib/demo_web/endpoint.ex
diff --git a/demo/lib/demo_web/router.ex b/demo/demo_hound/lib/demo_web/router.ex
similarity index 100%
rename from demo/lib/demo_web/router.ex
rename to demo/demo_hound/lib/demo_web/router.ex
diff --git a/demo/lib/demo_web/telemetry.ex b/demo/demo_hound/lib/demo_web/telemetry.ex
similarity index 100%
rename from demo/lib/demo_web/telemetry.ex
rename to demo/demo_hound/lib/demo_web/telemetry.ex
diff --git a/demo/demo_hound/mix.exs b/demo/demo_hound/mix.exs
new file mode 100644
index 0000000..9c36b25
--- /dev/null
+++ b/demo/demo_hound/mix.exs
@@ -0,0 +1,73 @@
+defmodule Demo.MixProject do
+ use Mix.Project
+
+ def project do
+ [
+ app: :demo,
+ version: "0.1.0",
+ elixir: "~> 1.13",
+ elixirc_paths: elixirc_paths(Mix.env()),
+ start_permanent: Mix.env() == :prod,
+ aliases: aliases(),
+ deps: deps()
+ ]
+ end
+
+ # Configuration for the OTP application.
+ #
+ # Type `mix help compile.app` for more information.
+ def application do
+ [
+ mod: {Demo.Application, []},
+ extra_applications: [:logger, :runtime_tools]
+ ]
+ end
+
+ # Specifies which paths to compile per environment.
+ defp elixirc_paths(:test), do: ["lib", "test/support"]
+ defp elixirc_paths(_), do: ["lib"]
+
+ # Specifies your project dependencies.
+ #
+ # Type `mix help deps` for examples and options.
+ defp deps do
+ [
+ {:phoenix, "~> 1.7.11"},
+ {:phoenix_html, "~> 4.0"},
+ {:phoenix_live_reload, "~> 1.2", only: :dev},
+ {:phoenix_live_view, "~> 0.20.2"},
+ {:floki, ">= 0.30.0", only: :test},
+ {:phoenix_live_dashboard, "~> 0.8.3"},
+ {:esbuild, "~> 0.8", runtime: Mix.env() == :dev},
+ {:telemetry_metrics, "~> 0.6"},
+ {:telemetry_poller, "~> 1.0"},
+ {:jason, "~> 1.2"},
+ {:dns_cluster, "~> 0.1.1"},
+ {:bandit, "~> 1.2"},
+ {:hound, "~> 1.1", runtime: false, only: :test},
+ {:a11y_audit, path: "../../", only: [:test]}
+ ]
+ end
+
+ # Aliases are shortcuts or tasks specific to the current project.
+ # For example, to install project dependencies and perform other setup tasks, run:
+ #
+ # $ mix setup
+ #
+ # See the documentation for `Mix` for more info on aliases.
+ defp aliases do
+ [
+ setup: ["deps.get", "assets.setup", "assets.build"],
+ "assets.setup": ["esbuild.install --if-missing"],
+ "assets.build": ["esbuild demo"],
+ "assets.deploy": [
+ "esbuild demo --minify",
+ "phx.digest"
+ ],
+ test: [
+ "assets.build",
+ "test"
+ ]
+ ]
+ end
+end
diff --git a/demo/demo_hound/mix.lock b/demo/demo_hound/mix.lock
new file mode 100644
index 0000000..578441e
--- /dev/null
+++ b/demo/demo_hound/mix.lock
@@ -0,0 +1,35 @@
+%{
+ "bandit": {:hex, :bandit, "1.5.2", "ed0a41c43a9e529c670d0fd48371db4027e7b80d43b1942893e17deb8bed0540", [:mix], [{:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "35ddbdce7e8a2a3c6b5093f7299d70832a43ed2f4a1852885a61d334cab1b4ad"},
+ "castore": {:hex, :castore, "1.0.7", "b651241514e5f6956028147fe6637f7ac13802537e895a724f90bf3e36ddd1dd", [:mix], [], "hexpm", "da7785a4b0d2a021cd1292a60875a784b6caef71e76bf4917bdee1f390455cf5"},
+ "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
+ "dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"},
+ "esbuild": {:hex, :esbuild, "0.8.1", "0cbf919f0eccb136d2eeef0df49c4acf55336de864e63594adcea3814f3edf41", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "25fc876a67c13cb0a776e7b5d7974851556baeda2085296c14ab48555ea7560f"},
+ "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
+ "floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"},
+ "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"},
+ "hound": {:hex, :hound, "1.1.1", "d3afce4cf0f446331d9d00427e9eb74fa135c296b1d3745d4bbe2096ce259087", [:mix], [{:hackney, "~> 1.5", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8c6342b49f53bb0e5c51d5ecca18a8ce872c44da05a8ce6f828385ebd744fe2a"},
+ "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
+ "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
+ "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
+ "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
+ "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
+ "mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"},
+ "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
+ "phoenix": {:hex, :phoenix, "1.7.12", "1cc589e0eab99f593a8aa38ec45f15d25297dd6187ee801c8de8947090b5a9d3", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "d646192fbade9f485b01bc9920c139bfdd19d0f8df3d73fd8eaf2dfbe0d2837c"},
+ "phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"},
+ "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.3", "7ff51c9b6609470f681fbea20578dede0e548302b0c8bdf338b5a753a4f045bf", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "f9470a0a8bae4f56430a23d42f977b5a6205fdba6559d76f932b876bfaec652d"},
+ "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.3", "f2161c207fda0e4fb55165f650f7f8db23f02b29e3bff00ff7ef161d6ac1f09d", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b4ec9cd73cb01ff1bd1cac92e045d13e7030330b74164297d1aee3907b54803c"},
+ "phoenix_live_view": {:hex, :phoenix_live_view, "0.20.14", "70fa101aa0539e81bed4238777498f6215e9dda3461bdaa067cad6908110c364", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "82f6d006c5264f979ed5eb75593d808bbe39020f20df2e78426f4f2d570e2402"},
+ "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
+ "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
+ "plug": {:hex, :plug, "1.16.0", "1d07d50cb9bb05097fdf187b31cf087c7297aafc3fed8299aac79c128a707e47", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cbf53aa1f5c4d758a7559c0bd6d59e286c2be0c6a1fac8cc3eee2f638243b93e"},
+ "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
+ "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
+ "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
+ "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"},
+ "telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"},
+ "thousand_island": {:hex, :thousand_island, "1.3.5", "6022b6338f1635b3d32406ff98d68b843ba73b3aa95cfc27154223244f3a6ca5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2be6954916fdfe4756af3239fb6b6d75d0b8063b5df03ba76fd8a4c87849e180"},
+ "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
+ "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
+ "websock_adapter": {:hex, :websock_adapter, "0.5.6", "0437fe56e093fd4ac422de33bf8fc89f7bc1416a3f2d732d8b2c8fd54792fe60", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "e04378d26b0af627817ae84c92083b7e97aca3121196679b73c73b99d0d133ea"},
+}
diff --git a/demo/priv/static/favicon.ico b/demo/demo_hound/priv/static/favicon.ico
similarity index 100%
rename from demo/priv/static/favicon.ico
rename to demo/demo_hound/priv/static/favicon.ico
diff --git a/demo/priv/static/images/logo.svg b/demo/demo_hound/priv/static/images/logo.svg
similarity index 100%
rename from demo/priv/static/images/logo.svg
rename to demo/demo_hound/priv/static/images/logo.svg
diff --git a/demo/priv/static/robots.txt b/demo/demo_hound/priv/static/robots.txt
similarity index 100%
rename from demo/priv/static/robots.txt
rename to demo/demo_hound/priv/static/robots.txt
diff --git a/demo/test/demo_web/controllers/error_html_test.exs b/demo/demo_hound/test/demo_web/controllers/error_html_test.exs
similarity index 100%
rename from demo/test/demo_web/controllers/error_html_test.exs
rename to demo/demo_hound/test/demo_web/controllers/error_html_test.exs
diff --git a/demo/test/demo_web/controllers/error_json_test.exs b/demo/demo_hound/test/demo_web/controllers/error_json_test.exs
similarity index 100%
rename from demo/test/demo_web/controllers/error_json_test.exs
rename to demo/demo_hound/test/demo_web/controllers/error_json_test.exs
diff --git a/demo/test/demo_web/integration/pages_hound_test.exs b/demo/demo_hound/test/demo_web/integration/pages_test.exs
similarity index 97%
rename from demo/test/demo_web/integration/pages_hound_test.exs
rename to demo/demo_hound/test/demo_web/integration/pages_test.exs
index 3c9a958..6863ea8 100644
--- a/demo/test/demo_web/integration/pages_hound_test.exs
+++ b/demo/demo_hound/test/demo_web/integration/pages_test.exs
@@ -1,4 +1,4 @@
-defmodule DemoWeb.PagesHoundTest do
+defmodule DemoWeb.PagesTest do
use ExUnit.Case
use Hound.Helpers
diff --git a/demo/test/support/conn_case.ex b/demo/demo_hound/test/support/conn_case.ex
similarity index 100%
rename from demo/test/support/conn_case.ex
rename to demo/demo_hound/test/support/conn_case.ex
diff --git a/demo/demo_hound/test/test_helper.exs b/demo/demo_hound/test/test_helper.exs
new file mode 100644
index 0000000..a036ff6
--- /dev/null
+++ b/demo/demo_hound/test/test_helper.exs
@@ -0,0 +1,3 @@
+ExUnit.start()
+Application.put_env(:wallaby, :base_url, DemoWeb.Endpoint.url())
+{:ok, _} = Application.ensure_all_started(:hound)
diff --git a/demo/demo_wallaby/.formatter.exs b/demo/demo_wallaby/.formatter.exs
new file mode 100644
index 0000000..e945e12
--- /dev/null
+++ b/demo/demo_wallaby/.formatter.exs
@@ -0,0 +1,5 @@
+[
+ import_deps: [:phoenix],
+ plugins: [Phoenix.LiveView.HTMLFormatter],
+ inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}"]
+]
diff --git a/demo/demo_wallaby/.gitignore b/demo/demo_wallaby/.gitignore
new file mode 100644
index 0000000..16ccdc4
--- /dev/null
+++ b/demo/demo_wallaby/.gitignore
@@ -0,0 +1,37 @@
+# The directory Mix will write compiled artifacts to.
+/_build/
+
+# If you run "mix test --cover", coverage assets end up here.
+/cover/
+
+# The directory Mix downloads your dependencies sources to.
+/deps/
+
+# Where 3rd-party dependencies like ExDoc output generated docs.
+/doc/
+
+# Ignore .fetch files in case you like to edit your project deps locally.
+/.fetch
+
+# If the VM crashes, it generates a dump, let's ignore it too.
+erl_crash.dump
+
+# Also ignore archive artifacts (built via "mix archive.build").
+*.ez
+
+# Temporary files, for example, from tests.
+/tmp/
+
+# Ignore package tarball (built via "mix hex.build").
+demo-*.tar
+
+# Ignore assets that are produced by build tools.
+/priv/static/assets/
+
+# Ignore digested assets cache.
+/priv/static/cache_manifest.json
+
+# In case you use Node.js/npm, you want to ignore these.
+npm-debug.log
+/assets/node_modules/
+
diff --git a/demo/demo_wallaby/README.md b/demo/demo_wallaby/README.md
new file mode 100644
index 0000000..2591d47
--- /dev/null
+++ b/demo/demo_wallaby/README.md
@@ -0,0 +1,18 @@
+# Demo
+
+To start your Phoenix server:
+
+ * Run `mix setup` to install and setup dependencies
+ * Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server`
+
+Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
+
+Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html).
+
+## Learn more
+
+ * Official website: https://www.phoenixframework.org/
+ * Guides: https://hexdocs.pm/phoenix/overview.html
+ * Docs: https://hexdocs.pm/phoenix
+ * Forum: https://elixirforum.com/c/phoenix-forum
+ * Source: https://github.com/phoenixframework/phoenix
diff --git a/demo/demo_wallaby/assets/js/app.js b/demo/demo_wallaby/assets/js/app.js
new file mode 100644
index 0000000..ced0434
--- /dev/null
+++ b/demo/demo_wallaby/assets/js/app.js
@@ -0,0 +1,44 @@
+// If you want to use Phoenix channels, run `mix help phx.gen.channel`
+// to get started and then uncomment the line below.
+// import "./user_socket.js"
+
+// You can include dependencies in two ways.
+//
+// The simplest option is to put them in assets/vendor and
+// import them using relative paths:
+//
+// import "../vendor/some-package.js"
+//
+// Alternatively, you can `npm install some-package --prefix assets` and import
+// them using a path starting with the package name:
+//
+// import "some-package"
+//
+
+// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
+import "phoenix_html"
+// Establish Phoenix Socket and LiveView configuration.
+// import {Socket} from "phoenix"
+// import {LiveSocket} from "phoenix_live_view"
+// import topbar from "../vendor/topbar"
+
+// let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
+// let liveSocket = new LiveSocket("/live", Socket, {
+// longPollFallbackMs: 2500,
+// params: {_csrf_token: csrfToken}
+// })
+
+// Show progress bar on live navigation and form submits
+// topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
+// window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
+// window.addEventListener("phx:page-loading-stop", _info => topbar.hide())
+
+// connect if there are any LiveViews on the page
+// liveSocket.connect()
+
+// expose liveSocket on window for web console debug logs and latency simulation:
+// >> liveSocket.enableDebug()
+// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
+// >> liveSocket.disableLatencySim()
+// window.liveSocket = liveSocket
+
diff --git a/demo/demo_wallaby/assets/vendor/topbar.js b/demo/demo_wallaby/assets/vendor/topbar.js
new file mode 100644
index 0000000..4195727
--- /dev/null
+++ b/demo/demo_wallaby/assets/vendor/topbar.js
@@ -0,0 +1,165 @@
+/**
+ * @license MIT
+ * topbar 2.0.0, 2023-02-04
+ * https://buunguyen.github.io/topbar
+ * Copyright (c) 2021 Buu Nguyen
+ */
+(function (window, document) {
+ "use strict";
+
+ // https://gist.github.com/paulirish/1579671
+ (function () {
+ var lastTime = 0;
+ var vendors = ["ms", "moz", "webkit", "o"];
+ for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
+ window.requestAnimationFrame =
+ window[vendors[x] + "RequestAnimationFrame"];
+ window.cancelAnimationFrame =
+ window[vendors[x] + "CancelAnimationFrame"] ||
+ window[vendors[x] + "CancelRequestAnimationFrame"];
+ }
+ if (!window.requestAnimationFrame)
+ window.requestAnimationFrame = function (callback, element) {
+ var currTime = new Date().getTime();
+ var timeToCall = Math.max(0, 16 - (currTime - lastTime));
+ var id = window.setTimeout(function () {
+ callback(currTime + timeToCall);
+ }, timeToCall);
+ lastTime = currTime + timeToCall;
+ return id;
+ };
+ if (!window.cancelAnimationFrame)
+ window.cancelAnimationFrame = function (id) {
+ clearTimeout(id);
+ };
+ })();
+
+ var canvas,
+ currentProgress,
+ showing,
+ progressTimerId = null,
+ fadeTimerId = null,
+ delayTimerId = null,
+ addEvent = function (elem, type, handler) {
+ if (elem.addEventListener) elem.addEventListener(type, handler, false);
+ else if (elem.attachEvent) elem.attachEvent("on" + type, handler);
+ else elem["on" + type] = handler;
+ },
+ options = {
+ autoRun: true,
+ barThickness: 3,
+ barColors: {
+ 0: "rgba(26, 188, 156, .9)",
+ ".25": "rgba(52, 152, 219, .9)",
+ ".50": "rgba(241, 196, 15, .9)",
+ ".75": "rgba(230, 126, 34, .9)",
+ "1.0": "rgba(211, 84, 0, .9)",
+ },
+ shadowBlur: 10,
+ shadowColor: "rgba(0, 0, 0, .6)",
+ className: null,
+ },
+ repaint = function () {
+ canvas.width = window.innerWidth;
+ canvas.height = options.barThickness * 5; // need space for shadow
+
+ var ctx = canvas.getContext("2d");
+ ctx.shadowBlur = options.shadowBlur;
+ ctx.shadowColor = options.shadowColor;
+
+ var lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
+ for (var stop in options.barColors)
+ lineGradient.addColorStop(stop, options.barColors[stop]);
+ ctx.lineWidth = options.barThickness;
+ ctx.beginPath();
+ ctx.moveTo(0, options.barThickness / 2);
+ ctx.lineTo(
+ Math.ceil(currentProgress * canvas.width),
+ options.barThickness / 2
+ );
+ ctx.strokeStyle = lineGradient;
+ ctx.stroke();
+ },
+ createCanvas = function () {
+ canvas = document.createElement("canvas");
+ var style = canvas.style;
+ style.position = "fixed";
+ style.top = style.left = style.right = style.margin = style.padding = 0;
+ style.zIndex = 100001;
+ style.display = "none";
+ if (options.className) canvas.classList.add(options.className);
+ document.body.appendChild(canvas);
+ addEvent(window, "resize", repaint);
+ },
+ topbar = {
+ config: function (opts) {
+ for (var key in opts)
+ if (options.hasOwnProperty(key)) options[key] = opts[key];
+ },
+ show: function (delay) {
+ if (showing) return;
+ if (delay) {
+ if (delayTimerId) return;
+ delayTimerId = setTimeout(() => topbar.show(), delay);
+ } else {
+ showing = true;
+ if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId);
+ if (!canvas) createCanvas();
+ canvas.style.opacity = 1;
+ canvas.style.display = "block";
+ topbar.progress(0);
+ if (options.autoRun) {
+ (function loop() {
+ progressTimerId = window.requestAnimationFrame(loop);
+ topbar.progress(
+ "+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2)
+ );
+ })();
+ }
+ }
+ },
+ progress: function (to) {
+ if (typeof to === "undefined") return currentProgress;
+ if (typeof to === "string") {
+ to =
+ (to.indexOf("+") >= 0 || to.indexOf("-") >= 0
+ ? currentProgress
+ : 0) + parseFloat(to);
+ }
+ currentProgress = to > 1 ? 1 : to;
+ repaint();
+ return currentProgress;
+ },
+ hide: function () {
+ clearTimeout(delayTimerId);
+ delayTimerId = null;
+ if (!showing) return;
+ showing = false;
+ if (progressTimerId != null) {
+ window.cancelAnimationFrame(progressTimerId);
+ progressTimerId = null;
+ }
+ (function loop() {
+ if (topbar.progress("+.1") >= 1) {
+ canvas.style.opacity -= 0.05;
+ if (canvas.style.opacity <= 0.05) {
+ canvas.style.display = "none";
+ fadeTimerId = null;
+ return;
+ }
+ }
+ fadeTimerId = window.requestAnimationFrame(loop);
+ })();
+ },
+ };
+
+ if (typeof module === "object" && typeof module.exports === "object") {
+ module.exports = topbar;
+ } else if (typeof define === "function" && define.amd) {
+ define(function () {
+ return topbar;
+ });
+ } else {
+ this.topbar = topbar;
+ }
+}.call(this, window, document));
diff --git a/demo/demo_wallaby/config/config.exs b/demo/demo_wallaby/config/config.exs
new file mode 100644
index 0000000..11b682a
--- /dev/null
+++ b/demo/demo_wallaby/config/config.exs
@@ -0,0 +1,44 @@
+# This file is responsible for configuring your application
+# and its dependencies with the aid of the Config module.
+#
+# This configuration file is loaded before any dependency and
+# is restricted to this project.
+
+# General application configuration
+import Config
+
+config :demo,
+ generators: [timestamp_type: :utc_datetime]
+
+# Configures the endpoint
+config :demo, DemoWeb.Endpoint,
+ url: [host: "localhost"],
+ adapter: Bandit.PhoenixAdapter,
+ render_errors: [
+ formats: [html: DemoWeb.ErrorHTML, json: DemoWeb.ErrorJSON],
+ layout: false
+ ],
+ pubsub_server: Demo.PubSub,
+ live_view: [signing_salt: "w+ygebMg"]
+
+# Configure esbuild (the version is required)
+config :esbuild,
+ version: "0.17.11",
+ demo: [
+ args:
+ ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
+ cd: Path.expand("../assets", __DIR__),
+ env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
+ ]
+
+# Configures Elixir's Logger
+config :logger, :console,
+ format: "$time $metadata[$level] $message\n",
+ metadata: [:request_id]
+
+# Use Jason for JSON parsing in Phoenix
+config :phoenix, :json_library, Jason
+
+# Import environment specific config. This must remain at the bottom
+# of this file so it overrides the configuration defined above.
+import_config "#{config_env()}.exs"
diff --git a/demo/demo_wallaby/config/dev.exs b/demo/demo_wallaby/config/dev.exs
new file mode 100644
index 0000000..f110fb0
--- /dev/null
+++ b/demo/demo_wallaby/config/dev.exs
@@ -0,0 +1,67 @@
+import Config
+
+# For development, we disable any cache and enable
+# debugging and code reloading.
+#
+# The watchers configuration can be used to run external
+# watchers to your application. For example, we can use it
+# to bundle .js and .css sources.
+config :demo, DemoWeb.Endpoint,
+ # Binding to loopback ipv4 address prevents access from other machines.
+ # Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
+ http: [ip: {127, 0, 0, 1}, port: 4000],
+ check_origin: false,
+ code_reloader: true,
+ debug_errors: true,
+ secret_key_base: "NHqcXYFD7uQAtDOjKUwFkkyhTmt4wcd5EYrxYf0aUNK5gbCEoQS/eHaRtEn9uYlQ",
+ watchers: [
+ esbuild: {Esbuild, :install_and_run, [:demo, ~w(--sourcemap=inline --watch)]}
+ ]
+
+# ## SSL Support
+#
+# In order to use HTTPS in development, a self-signed
+# certificate can be generated by running the following
+# Mix task:
+#
+# mix phx.gen.cert
+#
+# Run `mix help phx.gen.cert` for more information.
+#
+# The `http:` config above can be replaced with:
+#
+# https: [
+# port: 4001,
+# cipher_suite: :strong,
+# keyfile: "priv/cert/selfsigned_key.pem",
+# certfile: "priv/cert/selfsigned.pem"
+# ],
+#
+# If desired, both `http:` and `https:` keys can be
+# configured to run both http and https servers on
+# different ports.
+
+# Watch static and templates for browser reloading.
+config :demo, DemoWeb.Endpoint,
+ live_reload: [
+ patterns: [
+ ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$",
+ ~r"lib/demo_web/(controllers|live|components)/.*(ex|heex)$"
+ ]
+ ]
+
+# Enable dev routes for dashboard and mailbox
+config :demo, dev_routes: true
+
+# Do not include metadata nor timestamps in development logs
+config :logger, :console, format: "[$level] $message\n"
+
+# Set a higher stacktrace during development. Avoid configuring such
+# in production as building large stacktraces may be expensive.
+config :phoenix, :stacktrace_depth, 20
+
+# Initialize plugs at runtime for faster development compilation
+config :phoenix, :plug_init_mode, :runtime
+
+# Include HEEx debug annotations as HTML comments in rendered markup
+config :phoenix_live_view, :debug_heex_annotations, true
diff --git a/demo/demo_wallaby/config/prod.exs b/demo/demo_wallaby/config/prod.exs
new file mode 100644
index 0000000..ff10aa9
--- /dev/null
+++ b/demo/demo_wallaby/config/prod.exs
@@ -0,0 +1,14 @@
+import Config
+
+# Note we also include the path to a cache manifest
+# containing the digested version of static files. This
+# manifest is generated by the `mix assets.deploy` task,
+# which you should run after static files are built and
+# before starting your production server.
+config :demo, DemoWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json"
+
+# Do not print debug messages in production
+config :logger, level: :info
+
+# Runtime production configuration, including reading
+# of environment variables, is done on config/runtime.exs.
diff --git a/demo/demo_wallaby/config/runtime.exs b/demo/demo_wallaby/config/runtime.exs
new file mode 100644
index 0000000..8100355
--- /dev/null
+++ b/demo/demo_wallaby/config/runtime.exs
@@ -0,0 +1,84 @@
+import Config
+
+# config/runtime.exs is executed for all environments, including
+# during releases. It is executed after compilation and before the
+# system starts, so it is typically used to load production configuration
+# and secrets from environment variables or elsewhere. Do not define
+# any compile-time configuration in here, as it won't be applied.
+# The block below contains prod specific runtime configuration.
+
+# ## Using releases
+#
+# If you use `mix release`, you need to explicitly enable the server
+# by passing the PHX_SERVER=true when you start it:
+#
+# PHX_SERVER=true bin/demo start
+#
+# Alternatively, you can use `mix phx.gen.release` to generate a `bin/server`
+# script that automatically sets the env var above.
+if System.get_env("PHX_SERVER") do
+ config :demo, DemoWeb.Endpoint, server: true
+end
+
+if config_env() == :prod do
+ # The secret key base is used to sign/encrypt cookies and other secrets.
+ # A default value is used in config/dev.exs and config/test.exs but you
+ # want to use a different value for prod and you most likely don't want
+ # to check this value into version control, so we use an environment
+ # variable instead.
+ secret_key_base =
+ System.get_env("SECRET_KEY_BASE") ||
+ raise """
+ environment variable SECRET_KEY_BASE is missing.
+ You can generate one by calling: mix phx.gen.secret
+ """
+
+ host = System.get_env("PHX_HOST") || "example.com"
+ port = String.to_integer(System.get_env("PORT") || "4000")
+
+ config :demo, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
+
+ config :demo, DemoWeb.Endpoint,
+ url: [host: host, port: 443, scheme: "https"],
+ http: [
+ # Enable IPv6 and bind on all interfaces.
+ # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
+ # See the documentation on https://hexdocs.pm/bandit/Bandit.html#t:options/0
+ # for details about using IPv6 vs IPv4 and loopback vs public addresses.
+ ip: {0, 0, 0, 0, 0, 0, 0, 0},
+ port: port
+ ],
+ secret_key_base: secret_key_base
+
+ # ## SSL Support
+ #
+ # To get SSL working, you will need to add the `https` key
+ # to your endpoint configuration:
+ #
+ # config :demo, DemoWeb.Endpoint,
+ # https: [
+ # ...,
+ # port: 443,
+ # cipher_suite: :strong,
+ # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
+ # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")
+ # ]
+ #
+ # The `cipher_suite` is set to `:strong` to support only the
+ # latest and more secure SSL ciphers. This means old browsers
+ # and clients may not be supported. You can set it to
+ # `:compatible` for wider support.
+ #
+ # `:keyfile` and `:certfile` expect an absolute path to the key
+ # and cert in disk or a relative path inside priv, for example
+ # "priv/ssl/server.key". For all supported SSL configuration
+ # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1
+ #
+ # We also recommend setting `force_ssl` in your config/prod.exs,
+ # ensuring no data is ever sent via http, always redirecting to https:
+ #
+ # config :demo, DemoWeb.Endpoint,
+ # force_ssl: [hsts: true]
+ #
+ # Check `Plug.SSL` for all available options in `force_ssl`.
+end
diff --git a/demo/demo_wallaby/config/test.exs b/demo/demo_wallaby/config/test.exs
new file mode 100644
index 0000000..14773f3
--- /dev/null
+++ b/demo/demo_wallaby/config/test.exs
@@ -0,0 +1,14 @@
+import Config
+
+# We don't run a server during test. If one is required,
+# you can enable the server option below.
+config :demo, DemoWeb.Endpoint,
+ http: [ip: {127, 0, 0, 1}, port: 4002],
+ secret_key_base: "D2jg+Cv2EmgPyq+gr/n6eT6RejYNCy/11TwlsdIBUb2Ipiux0Lx2JcpPxASdBKMJ",
+ server: true
+
+# Print only warnings and errors during test
+config :logger, level: :warning
+
+# Initialize plugs at runtime for faster test compilation
+config :phoenix, :plug_init_mode, :runtime
diff --git a/demo/demo_wallaby/lib/demo.ex b/demo/demo_wallaby/lib/demo.ex
new file mode 100644
index 0000000..3f1ac75
--- /dev/null
+++ b/demo/demo_wallaby/lib/demo.ex
@@ -0,0 +1,9 @@
+defmodule Demo do
+ @moduledoc """
+ Demo keeps the contexts that define your domain
+ and business logic.
+
+ Contexts are also responsible for managing your data, regardless
+ if it comes from the database, an external API or others.
+ """
+end
diff --git a/demo/demo_wallaby/lib/demo/application.ex b/demo/demo_wallaby/lib/demo/application.ex
new file mode 100644
index 0000000..6412e9a
--- /dev/null
+++ b/demo/demo_wallaby/lib/demo/application.ex
@@ -0,0 +1,33 @@
+defmodule Demo.Application do
+ # See https://hexdocs.pm/elixir/Application.html
+ # for more information on OTP Applications
+ @moduledoc false
+
+ use Application
+
+ @impl true
+ def start(_type, _args) do
+ children = [
+ DemoWeb.Telemetry,
+ {DNSCluster, query: Application.get_env(:demo, :dns_cluster_query) || :ignore},
+ {Phoenix.PubSub, name: Demo.PubSub},
+ # Start a worker by calling: Demo.Worker.start_link(arg)
+ # {Demo.Worker, arg},
+ # Start to serve requests, typically the last entry
+ DemoWeb.Endpoint
+ ]
+
+ # See https://hexdocs.pm/elixir/Supervisor.html
+ # for other strategies and supported options
+ opts = [strategy: :one_for_one, name: Demo.Supervisor]
+ Supervisor.start_link(children, opts)
+ end
+
+ # Tell Phoenix to update the endpoint configuration
+ # whenever the application is updated.
+ @impl true
+ def config_change(changed, _new, removed) do
+ DemoWeb.Endpoint.config_change(changed, removed)
+ :ok
+ end
+end
diff --git a/demo/demo_wallaby/lib/demo_web.ex b/demo/demo_wallaby/lib/demo_web.ex
new file mode 100644
index 0000000..9b8ca32
--- /dev/null
+++ b/demo/demo_wallaby/lib/demo_web.ex
@@ -0,0 +1,111 @@
+defmodule DemoWeb do
+ @moduledoc """
+ The entrypoint for defining your web interface, such
+ as controllers, components, channels, and so on.
+
+ This can be used in your application as:
+
+ use DemoWeb, :controller
+ use DemoWeb, :html
+
+ The definitions below will be executed for every controller,
+ component, etc, so keep them short and clean, focused
+ on imports, uses and aliases.
+
+ Do NOT define functions inside the quoted expressions
+ below. Instead, define additional modules and import
+ those modules here.
+ """
+
+ def static_paths, do: ~w(assets fonts images favicon.ico robots.txt)
+
+ def router do
+ quote do
+ use Phoenix.Router, helpers: false
+
+ # Import common connection and controller functions to use in pipelines
+ import Plug.Conn
+ import Phoenix.Controller
+ import Phoenix.LiveView.Router
+ end
+ end
+
+ def channel do
+ quote do
+ use Phoenix.Channel
+ end
+ end
+
+ def controller do
+ quote do
+ use Phoenix.Controller,
+ formats: [:html, :json],
+ layouts: [html: DemoWeb.Layouts]
+
+ import Plug.Conn
+
+ unquote(verified_routes())
+ end
+ end
+
+ def live_view do
+ quote do
+ use Phoenix.LiveView,
+ layout: {DemoWeb.Layouts, :app}
+
+ unquote(html_helpers())
+ end
+ end
+
+ def live_component do
+ quote do
+ use Phoenix.LiveComponent
+
+ unquote(html_helpers())
+ end
+ end
+
+ def html do
+ quote do
+ use Phoenix.Component
+
+ # Import convenience functions from controllers
+ import Phoenix.Controller,
+ only: [get_csrf_token: 0, view_module: 1, view_template: 1]
+
+ # Include general helpers for rendering HTML
+ unquote(html_helpers())
+ end
+ end
+
+ defp html_helpers do
+ quote do
+ # HTML escaping functionality
+ import Phoenix.HTML
+ # Core UI components and translation
+ import DemoWeb.CoreComponents
+
+ # Shortcut for generating JS commands
+ alias Phoenix.LiveView.JS
+
+ # Routes generation with the ~p sigil
+ unquote(verified_routes())
+ end
+ end
+
+ def verified_routes do
+ quote do
+ use Phoenix.VerifiedRoutes,
+ endpoint: DemoWeb.Endpoint,
+ router: DemoWeb.Router,
+ statics: DemoWeb.static_paths()
+ end
+ end
+
+ @doc """
+ When used, dispatch to the appropriate controller/view/etc.
+ """
+ defmacro __using__(which) when is_atom(which) do
+ apply(__MODULE__, which, [])
+ end
+end
diff --git a/demo/demo_wallaby/lib/demo_web/components/core_components.ex b/demo/demo_wallaby/lib/demo_web/components/core_components.ex
new file mode 100644
index 0000000..8abc862
--- /dev/null
+++ b/demo/demo_wallaby/lib/demo_web/components/core_components.ex
@@ -0,0 +1,670 @@
+defmodule DemoWeb.CoreComponents do
+ @moduledoc """
+ Provides core UI components.
+
+ At first glance, this module may seem daunting, but its goal is to provide
+ core building blocks for your application, such as modals, tables, and
+ forms. The components consist mostly of markup and are well-documented
+ with doc strings and declarative assigns. You may customize and style
+ them in any way you want, based on your application growth and needs.
+
+ The default components use Tailwind CSS, a utility-first CSS framework.
+ See the [Tailwind CSS documentation](https://tailwindcss.com) to learn
+ how to customize them or feel free to swap in another framework altogether.
+
+ Icons are provided by [heroicons](https://heroicons.com). See `icon/1` for usage.
+ """
+ use Phoenix.Component
+
+ alias Phoenix.LiveView.JS
+
+ @doc """
+ Renders a modal.
+
+ ## Examples
+
+ <.modal id="confirm-modal">
+ This is a modal.
+
+
+ JS commands may be passed to the `:on_cancel` to configure
+ the closing/cancel event, for example:
+
+ <.modal id="confirm" on_cancel={JS.navigate(~p"/posts")}>
+ This is another modal.
+
+
+ """
+ attr :id, :string, required: true
+ attr :show, :boolean, default: false
+ attr :on_cancel, JS, default: %JS{}
+ slot :inner_block, required: true
+
+ def modal(assigns) do
+ ~H"""
+
+
+
+
+
+ <.focus_wrap
+ id={"#{@id}-container"}
+ phx-window-keydown={JS.exec("data-cancel", to: "##{@id}")}
+ phx-key="escape"
+ phx-click-away={JS.exec("data-cancel", to: "##{@id}")}
+ class="shadow-zinc-700/10 ring-zinc-700/10 relative hidden rounded-2xl bg-white p-14 shadow-lg ring-1 transition"
+ >
+
+
+ <.icon name="hero-x-mark-solid" class="h-5 w-5" />
+
+
+
+ <%= render_slot(@inner_block) %>
+
+
+
+
+
+
+ """
+ end
+
+ @doc """
+ Renders flash notices.
+
+ ## Examples
+
+ <.flash kind={:info} flash={@flash} />
+ <.flash kind={:info} phx-mounted={show("#flash")}>Welcome Back!
+ """
+ attr :id, :string, doc: "the optional id of flash container"
+ attr :flash, :map, default: %{}, doc: "the map of flash messages to display"
+ attr :title, :string, default: nil
+ attr :kind, :atom, values: [:info, :error], doc: "used for styling and flash lookup"
+ attr :rest, :global, doc: "the arbitrary HTML attributes to add to the flash container"
+
+ slot :inner_block, doc: "the optional inner block that renders the flash message"
+
+ def flash(assigns) do
+ assigns = assign_new(assigns, :id, fn -> "flash-#{assigns.kind}" end)
+
+ ~H"""
+ hide("##{@id}")}
+ role="alert"
+ class={[
+ "fixed top-2 right-2 mr-2 w-80 sm:w-96 z-50 rounded-lg p-3 ring-1",
+ @kind == :info && "bg-emerald-50 text-emerald-800 ring-emerald-500 fill-cyan-900",
+ @kind == :error && "bg-rose-50 text-rose-900 shadow-md ring-rose-500 fill-rose-900"
+ ]}
+ {@rest}
+ >
+
+ <.icon :if={@kind == :info} name="hero-information-circle-mini" class="h-4 w-4" />
+ <.icon :if={@kind == :error} name="hero-exclamation-circle-mini" class="h-4 w-4" />
+ <%= @title %>
+
+
<%= msg %>
+
+ <.icon name="hero-x-mark-solid" class="h-5 w-5 opacity-40 group-hover:opacity-70" />
+
+
+ """
+ end
+
+ @doc """
+ Shows the flash group with standard titles and content.
+
+ ## Examples
+
+ <.flash_group flash={@flash} />
+ """
+ attr :flash, :map, required: true, doc: "the map of flash messages"
+ attr :id, :string, default: "flash-group", doc: "the optional id of flash container"
+
+ def flash_group(assigns) do
+ ~H"""
+
+ <.flash kind={:info} title="Success!" flash={@flash} />
+ <.flash kind={:error} title="Error!" flash={@flash} />
+ <.flash
+ id="client-error"
+ kind={:error}
+ title="We can't find the internet"
+ phx-disconnected={show(".phx-client-error #client-error")}
+ phx-connected={hide("#client-error")}
+ hidden
+ >
+ Attempting to reconnect <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
+
+
+ <.flash
+ id="server-error"
+ kind={:error}
+ title="Something went wrong!"
+ phx-disconnected={show(".phx-server-error #server-error")}
+ phx-connected={hide("#server-error")}
+ hidden
+ >
+ Hang in there while we get back on track
+ <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
+
+
+ """
+ end
+
+ @doc """
+ Renders a simple form.
+
+ ## Examples
+
+ <.simple_form for={@form} phx-change="validate" phx-submit="save">
+ <.input field={@form[:email]} label="Email"/>
+ <.input field={@form[:username]} label="Username" />
+ <:actions>
+ <.button>Save
+
+
+ """
+ attr :for, :any, required: true, doc: "the datastructure for the form"
+ attr :as, :any, default: nil, doc: "the server side parameter to collect all input under"
+
+ attr :rest, :global,
+ include: ~w(autocomplete name rel action enctype method novalidate target multipart),
+ doc: "the arbitrary HTML attributes to apply to the form tag"
+
+ slot :inner_block, required: true
+ slot :actions, doc: "the slot for form actions, such as a submit button"
+
+ def simple_form(assigns) do
+ ~H"""
+ <.form :let={f} for={@for} as={@as} {@rest}>
+
+ <%= render_slot(@inner_block, f) %>
+
+ <%= render_slot(action, f) %>
+
+
+
+ """
+ end
+
+ @doc """
+ Renders a button.
+
+ ## Examples
+
+ <.button>Send!
+ <.button phx-click="go" class="ml-2">Send!
+ """
+ attr :type, :string, default: nil
+ attr :class, :string, default: nil
+ attr :rest, :global, include: ~w(disabled form name value)
+
+ slot :inner_block, required: true
+
+ def button(assigns) do
+ ~H"""
+
+ <%= render_slot(@inner_block) %>
+
+ """
+ end
+
+ @doc """
+ Renders an input with label and error messages.
+
+ A `Phoenix.HTML.FormField` may be passed as argument,
+ which is used to retrieve the input name, id, and values.
+ Otherwise all attributes may be passed explicitly.
+
+ ## Types
+
+ This function accepts all HTML input types, considering that:
+
+ * You may also set `type="select"` to render a `` tag
+
+ * `type="checkbox"` is used exclusively to render boolean values
+
+ * For live file uploads, see `Phoenix.Component.live_file_input/1`
+
+ See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input
+ for more information.
+
+ ## Examples
+
+ <.input field={@form[:email]} type="email" />
+ <.input name="my-input" errors={["oh no!"]} />
+ """
+ attr :id, :any, default: nil
+ attr :name, :any
+ attr :label, :string, default: nil
+ attr :value, :any
+
+ attr :type, :string,
+ default: "text",
+ values: ~w(checkbox color date datetime-local email file hidden month number password
+ range radio search select tel text textarea time url week)
+
+ attr :field, Phoenix.HTML.FormField,
+ doc: "a form field struct retrieved from the form, for example: @form[:email]"
+
+ attr :errors, :list, default: []
+ attr :checked, :boolean, doc: "the checked flag for checkbox inputs"
+ attr :prompt, :string, default: nil, doc: "the prompt for select inputs"
+ attr :options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2"
+ attr :multiple, :boolean, default: false, doc: "the multiple flag for select inputs"
+
+ attr :rest, :global,
+ include: ~w(accept autocomplete capture cols disabled form list max maxlength min minlength
+ multiple pattern placeholder readonly required rows size step)
+
+ slot :inner_block
+
+ def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
+ assigns
+ |> assign(field: nil, id: assigns.id || field.id)
+ |> assign(:errors, Enum.map(field.errors, &translate_error(&1)))
+ |> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end)
+ |> assign_new(:value, fn -> field.value end)
+ |> input()
+ end
+
+ def input(%{type: "checkbox"} = assigns) do
+ assigns =
+ assign_new(assigns, :checked, fn ->
+ Phoenix.HTML.Form.normalize_value("checkbox", assigns[:value])
+ end)
+
+ ~H"""
+
+
+
+
+ <%= @label %>
+
+ <.error :for={msg <- @errors}><%= msg %>
+
+ """
+ end
+
+ def input(%{type: "select"} = assigns) do
+ ~H"""
+
+ <.label for={@id}><%= @label %>
+
+ <%= @prompt %>
+ <%= Phoenix.HTML.Form.options_for_select(@options, @value) %>
+
+ <.error :for={msg <- @errors}><%= msg %>
+
+ """
+ end
+
+ def input(%{type: "textarea"} = assigns) do
+ ~H"""
+
+ <.label for={@id}><%= @label %>
+
+ <.error :for={msg <- @errors}><%= msg %>
+
+ """
+ end
+
+ # All other inputs text, datetime-local, url, password, etc. are handled here...
+ def input(assigns) do
+ ~H"""
+
+ <.label for={@id}><%= @label %>
+
+ <.error :for={msg <- @errors}><%= msg %>
+
+ """
+ end
+
+ @doc """
+ Renders a label.
+ """
+ attr :for, :string, default: nil
+ slot :inner_block, required: true
+
+ def label(assigns) do
+ ~H"""
+
+ <%= render_slot(@inner_block) %>
+
+ """
+ end
+
+ @doc """
+ Generates a generic error message.
+ """
+ slot :inner_block, required: true
+
+ def error(assigns) do
+ ~H"""
+
+ <.icon name="hero-exclamation-circle-mini" class="mt-0.5 h-5 w-5 flex-none" />
+ <%= render_slot(@inner_block) %>
+
+ """
+ end
+
+ @doc """
+ Renders a header with title.
+ """
+ attr :class, :string, default: nil
+
+ slot :inner_block, required: true
+ slot :subtitle
+ slot :actions
+
+ def header(assigns) do
+ ~H"""
+
+ """
+ end
+
+ @doc ~S"""
+ Renders a table with generic styling.
+
+ ## Examples
+
+ <.table id="users" rows={@users}>
+ <:col :let={user} label="id"><%= user.id %>
+ <:col :let={user} label="username"><%= user.username %>
+
+ """
+ attr :id, :string, required: true
+ attr :rows, :list, required: true
+ attr :row_id, :any, default: nil, doc: "the function for generating the row id"
+ attr :row_click, :any, default: nil, doc: "the function for handling phx-click on each row"
+
+ attr :row_item, :any,
+ default: &Function.identity/1,
+ doc: "the function for mapping each row before calling the :col and :action slots"
+
+ slot :col, required: true do
+ attr :label, :string
+ end
+
+ slot :action, doc: "the slot for showing user actions in the last table column"
+
+ def table(assigns) do
+ assigns =
+ with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do
+ assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end)
+ end
+
+ ~H"""
+
+
+
+
+ <%= col[:label] %>
+
+ Actions
+
+
+
+
+
+
+
+
+
+ <%= render_slot(col, @row_item.(row)) %>
+
+
+
+
+
+
+
+ <%= render_slot(action, @row_item.(row)) %>
+
+
+
+
+
+
+
+ """
+ end
+
+ @doc """
+ Renders a data list.
+
+ ## Examples
+
+ <.list>
+ <:item title="Title"><%= @post.title %>
+ <:item title="Views"><%= @post.views %>
+
+ """
+ slot :item, required: true do
+ attr :title, :string, required: true
+ end
+
+ def list(assigns) do
+ ~H"""
+
+
+
+
<%= item.title %>
+ <%= render_slot(item) %>
+
+
+
+ """
+ end
+
+ @doc """
+ Renders a back navigation link.
+
+ ## Examples
+
+ <.back navigate={~p"/posts"}>Back to posts
+ """
+ attr :navigate, :any, required: true
+ slot :inner_block, required: true
+
+ def back(assigns) do
+ ~H"""
+
+ <.link
+ navigate={@navigate}
+ class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
+ >
+ <.icon name="hero-arrow-left-solid" class="h-3 w-3" />
+ <%= render_slot(@inner_block) %>
+
+
+ """
+ end
+
+ @doc """
+ Renders a [Heroicon](https://heroicons.com).
+
+ Heroicons come in three styles – outline, solid, and mini.
+ By default, the outline style is used, but solid and mini may
+ be applied by using the `-solid` and `-mini` suffix.
+
+ You can customize the size and colors of the icons by setting
+ width, height, and background color classes.
+
+ Icons are extracted from the `deps/heroicons` directory and bundled within
+ your compiled app.css by the plugin in your `assets/tailwind.config.js`.
+
+ ## Examples
+
+ <.icon name="hero-x-mark-solid" />
+ <.icon name="hero-arrow-path" class="ml-1 w-3 h-3 animate-spin" />
+ """
+ attr :name, :string, required: true
+ attr :class, :string, default: nil
+
+ def icon(%{name: "hero-" <> _} = assigns) do
+ ~H"""
+
+ """
+ end
+
+ ## JS Commands
+
+ def show(js \\ %JS{}, selector) do
+ JS.show(js,
+ to: selector,
+ transition:
+ {"transition-all transform ease-out duration-300",
+ "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
+ "opacity-100 translate-y-0 sm:scale-100"}
+ )
+ end
+
+ def hide(js \\ %JS{}, selector) do
+ JS.hide(js,
+ to: selector,
+ time: 200,
+ transition:
+ {"transition-all transform ease-in duration-200",
+ "opacity-100 translate-y-0 sm:scale-100",
+ "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"}
+ )
+ end
+
+ def show_modal(js \\ %JS{}, id) when is_binary(id) do
+ js
+ |> JS.show(to: "##{id}")
+ |> JS.show(
+ to: "##{id}-bg",
+ transition: {"transition-all transform ease-out duration-300", "opacity-0", "opacity-100"}
+ )
+ |> show("##{id}-container")
+ |> JS.add_class("overflow-hidden", to: "body")
+ |> JS.focus_first(to: "##{id}-content")
+ end
+
+ def hide_modal(js \\ %JS{}, id) do
+ js
+ |> JS.hide(
+ to: "##{id}-bg",
+ transition: {"transition-all transform ease-in duration-200", "opacity-100", "opacity-0"}
+ )
+ |> hide("##{id}-container")
+ |> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"})
+ |> JS.remove_class("overflow-hidden", to: "body")
+ |> JS.pop_focus()
+ end
+
+ @doc """
+ Translates an error message using gettext.
+ """
+ def translate_error({msg, opts}) do
+ # You can make use of gettext to translate error messages by
+ # uncommenting and adjusting the following code:
+
+ # if count = opts[:count] do
+ # Gettext.dngettext(DemoWeb.Gettext, "errors", msg, msg, count, opts)
+ # else
+ # Gettext.dgettext(DemoWeb.Gettext, "errors", msg, opts)
+ # end
+
+ Enum.reduce(opts, msg, fn {key, value}, acc ->
+ String.replace(acc, "%{#{key}}", fn _ -> to_string(value) end)
+ end)
+ end
+
+ @doc """
+ Translates the errors for a field from a keyword list of errors.
+ """
+ def translate_errors(errors, field) when is_list(errors) do
+ for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts})
+ end
+end
diff --git a/demo/demo_wallaby/lib/demo_web/components/layouts.ex b/demo/demo_wallaby/lib/demo_web/components/layouts.ex
new file mode 100644
index 0000000..58a4271
--- /dev/null
+++ b/demo/demo_wallaby/lib/demo_web/components/layouts.ex
@@ -0,0 +1,5 @@
+defmodule DemoWeb.Layouts do
+ use DemoWeb, :html
+
+ embed_templates "layouts/*"
+end
diff --git a/demo/demo_wallaby/lib/demo_web/components/layouts/app.html.heex b/demo/demo_wallaby/lib/demo_web/components/layouts/app.html.heex
new file mode 100644
index 0000000..c753bc6
--- /dev/null
+++ b/demo/demo_wallaby/lib/demo_web/components/layouts/app.html.heex
@@ -0,0 +1,3 @@
+
+ <%= @inner_content %>
+
diff --git a/demo/demo_wallaby/lib/demo_web/components/layouts/root.html.heex b/demo/demo_wallaby/lib/demo_web/components/layouts/root.html.heex
new file mode 100644
index 0000000..2fb9c2e
--- /dev/null
+++ b/demo/demo_wallaby/lib/demo_web/components/layouts/root.html.heex
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+ <.live_title suffix=" · Phoenix Framework">
+ <%= assigns[:page_title] || "Demo" %>
+
+
+
+
+
+ <%= @inner_content %>
+
+
diff --git a/demo/demo_wallaby/lib/demo_web/controllers/error_html.ex b/demo/demo_wallaby/lib/demo_web/controllers/error_html.ex
new file mode 100644
index 0000000..d59f047
--- /dev/null
+++ b/demo/demo_wallaby/lib/demo_web/controllers/error_html.ex
@@ -0,0 +1,19 @@
+defmodule DemoWeb.ErrorHTML do
+ use DemoWeb, :html
+
+ # If you want to customize your error pages,
+ # uncomment the embed_templates/1 call below
+ # and add pages to the error directory:
+ #
+ # * lib/demo_web/controllers/error_html/404.html.heex
+ # * lib/demo_web/controllers/error_html/500.html.heex
+ #
+ # embed_templates "error_html/*"
+
+ # The default is to render a plain text page based on
+ # the template name. For example, "404.html" becomes
+ # "Not Found".
+ def render(template, _assigns) do
+ Phoenix.Controller.status_message_from_template(template)
+ end
+end
diff --git a/demo/demo_wallaby/lib/demo_web/controllers/error_json.ex b/demo/demo_wallaby/lib/demo_web/controllers/error_json.ex
new file mode 100644
index 0000000..53154fa
--- /dev/null
+++ b/demo/demo_wallaby/lib/demo_web/controllers/error_json.ex
@@ -0,0 +1,15 @@
+defmodule DemoWeb.ErrorJSON do
+ # If you want to customize a particular status code,
+ # you may add your own clauses, such as:
+ #
+ # def render("500.json", _assigns) do
+ # %{errors: %{detail: "Internal Server Error"}}
+ # end
+
+ # By default, Phoenix returns the status message from
+ # the template name. For example, "404.json" becomes
+ # "Not Found".
+ def render(template, _assigns) do
+ %{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}}
+ end
+end
diff --git a/demo/demo_wallaby/lib/demo_web/controllers/page_controller.ex b/demo/demo_wallaby/lib/demo_web/controllers/page_controller.ex
new file mode 100644
index 0000000..8955c67
--- /dev/null
+++ b/demo/demo_wallaby/lib/demo_web/controllers/page_controller.ex
@@ -0,0 +1,15 @@
+defmodule DemoWeb.PageController do
+ use DemoWeb, :controller
+
+ def valid(conn, _params) do
+ render(conn, :valid)
+ end
+
+ def invalid(conn, _params) do
+ render(conn, :invalid)
+ end
+
+ def dynamic_invalid(conn, _params) do
+ render(conn, :dynamic_invalid)
+ end
+end
diff --git a/demo/demo_wallaby/lib/demo_web/controllers/page_html.ex b/demo/demo_wallaby/lib/demo_web/controllers/page_html.ex
new file mode 100644
index 0000000..74bc494
--- /dev/null
+++ b/demo/demo_wallaby/lib/demo_web/controllers/page_html.ex
@@ -0,0 +1,5 @@
+defmodule DemoWeb.PageHTML do
+ use DemoWeb, :html
+
+ embed_templates "page_html/*"
+end
diff --git a/demo/demo_wallaby/lib/demo_web/controllers/page_html/dynamic_invalid.html.heex b/demo/demo_wallaby/lib/demo_web/controllers/page_html/dynamic_invalid.html.heex
new file mode 100644
index 0000000..4b0e02a
--- /dev/null
+++ b/demo/demo_wallaby/lib/demo_web/controllers/page_html/dynamic_invalid.html.heex
@@ -0,0 +1,49 @@
+
+ Angelika's Star Trek series ranking
+
+ Star Trek: Deep Space Nine
+ Star Trek: The Next Generation
+ Star Trek: Strange New Worlds
+ Star Trek: Voyager
+ Star Trek: Enterprise
+ Star Trek: The Original Series
+ The Orville [1]
+ Star Trek: Discovery
+ Star Trek: Picard
+
+
+
+ [1] While I am aware that The Orville is not technically part of the Star Trek universe,
+ it is better at capturing what makes Star Trek Star Trek
+ and telling interesting stories
+ than the most recent attempts at rebooting the series, Discovery and Picard.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Paint it black
+
+
+
+
+
diff --git a/demo/demo_wallaby/lib/demo_web/controllers/page_html/invalid.html.heex b/demo/demo_wallaby/lib/demo_web/controllers/page_html/invalid.html.heex
new file mode 100644
index 0000000..dd9595d
--- /dev/null
+++ b/demo/demo_wallaby/lib/demo_web/controllers/page_html/invalid.html.heex
@@ -0,0 +1,32 @@
+
+ Angelika's Star Trek series ranking
+
+ Star Trek: Deep Space Nine
+ Star Trek: The Next Generation
+ Star Trek: Strange New Worlds
+ Star Trek: Voyager
+ Star Trek: Enterprise
+ Star Trek: The Original Series
+ The Orville [1]
+ Star Trek: Discovery
+ Star Trek: Picard
+
+
+
+ [1] While I am aware that The Orville is not technically part of the Star Trek universe,
+ it is better at capturing what makes Star Trek Star Trek
+ and telling interesting stories
+ than the most recent attempts at rebooting the series, Discovery and Picard.
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demo/demo_wallaby/lib/demo_web/controllers/page_html/valid.html.heex b/demo/demo_wallaby/lib/demo_web/controllers/page_html/valid.html.heex
new file mode 100644
index 0000000..4a4fd4d
--- /dev/null
+++ b/demo/demo_wallaby/lib/demo_web/controllers/page_html/valid.html.heex
@@ -0,0 +1,35 @@
+
+ Angelika's Star Trek series ranking
+
+ Star Trek: Deep Space Nine
+ Star Trek: The Next Generation
+ Star Trek: Strange New Worlds
+ Star Trek: Voyager
+ Star Trek: Enterprise
+ Star Trek: The Original Series
+ The Orville [1]
+ Star Trek: Discovery
+ Star Trek: Picard
+
+
+
+ [1] While I am aware that The Orville is not technically part of the Star Trek universe,
+ it is better at capturing what makes Star Trek Star Trek
+ and telling interesting stories
+ than the most recent attempts at rebooting the series, Discovery and Picard.
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demo/demo_wallaby/lib/demo_web/endpoint.ex b/demo/demo_wallaby/lib/demo_web/endpoint.ex
new file mode 100644
index 0000000..9a0ca04
--- /dev/null
+++ b/demo/demo_wallaby/lib/demo_web/endpoint.ex
@@ -0,0 +1,52 @@
+defmodule DemoWeb.Endpoint do
+ use Phoenix.Endpoint, otp_app: :demo
+
+ # The session will be stored in the cookie and signed,
+ # this means its contents can be read but not tampered with.
+ # Set :encryption_salt if you would also like to encrypt it.
+ @session_options [
+ store: :cookie,
+ key: "_demo_key",
+ signing_salt: "OzmAnOt4",
+ same_site: "Lax"
+ ]
+
+ socket "/live", Phoenix.LiveView.Socket,
+ websocket: [connect_info: [session: @session_options]],
+ longpoll: [connect_info: [session: @session_options]]
+
+ # Serve at "/" the static files from "priv/static" directory.
+ #
+ # You should set gzip to true if you are running phx.digest
+ # when deploying your static files in production.
+ plug Plug.Static,
+ at: "/",
+ from: :demo,
+ gzip: false,
+ only: DemoWeb.static_paths()
+
+ # Code reloading can be explicitly enabled under the
+ # :code_reloader configuration of your endpoint.
+ if code_reloading? do
+ socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
+ plug Phoenix.LiveReloader
+ plug Phoenix.CodeReloader
+ end
+
+ plug Phoenix.LiveDashboard.RequestLogger,
+ param_key: "request_logger",
+ cookie_key: "request_logger"
+
+ plug Plug.RequestId
+ plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
+
+ plug Plug.Parsers,
+ parsers: [:urlencoded, :multipart, :json],
+ pass: ["*/*"],
+ json_decoder: Phoenix.json_library()
+
+ plug Plug.MethodOverride
+ plug Plug.Head
+ plug Plug.Session, @session_options
+ plug DemoWeb.Router
+end
diff --git a/demo/demo_wallaby/lib/demo_web/router.ex b/demo/demo_wallaby/lib/demo_web/router.ex
new file mode 100644
index 0000000..9b77b14
--- /dev/null
+++ b/demo/demo_wallaby/lib/demo_web/router.ex
@@ -0,0 +1,45 @@
+defmodule DemoWeb.Router do
+ use DemoWeb, :router
+
+ pipeline :browser do
+ plug :accepts, ["html"]
+ plug :fetch_session
+ plug :fetch_live_flash
+ plug :put_root_layout, html: {DemoWeb.Layouts, :root}
+ plug :protect_from_forgery
+ plug :put_secure_browser_headers
+ end
+
+ pipeline :api do
+ plug :accepts, ["json"]
+ end
+
+ scope "/", DemoWeb do
+ pipe_through :browser
+
+ get "/valid", PageController, :valid
+ get "/invalid", PageController, :invalid
+ get "/dynamic_invalid", PageController, :dynamic_invalid
+ end
+
+ # Other scopes may use custom stacks.
+ # scope "/api", DemoWeb do
+ # pipe_through :api
+ # end
+
+ # Enable LiveDashboard in development
+ if Application.compile_env(:demo, :dev_routes) do
+ # If you want to use the LiveDashboard in production, you should put
+ # it behind authentication and allow only admins to access it.
+ # If your application does not have an admins-only section yet,
+ # you can use Plug.BasicAuth to set up some basic authentication
+ # as long as you are also using SSL (which you should anyway).
+ import Phoenix.LiveDashboard.Router
+
+ scope "/dev" do
+ pipe_through :browser
+
+ live_dashboard "/dashboard", metrics: DemoWeb.Telemetry
+ end
+ end
+end
diff --git a/demo/demo_wallaby/lib/demo_web/telemetry.ex b/demo/demo_wallaby/lib/demo_web/telemetry.ex
new file mode 100644
index 0000000..7705011
--- /dev/null
+++ b/demo/demo_wallaby/lib/demo_web/telemetry.ex
@@ -0,0 +1,69 @@
+defmodule DemoWeb.Telemetry do
+ use Supervisor
+ import Telemetry.Metrics
+
+ def start_link(arg) do
+ Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
+ end
+
+ @impl true
+ def init(_arg) do
+ children = [
+ # Telemetry poller will execute the given period measurements
+ # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics
+ {:telemetry_poller, measurements: periodic_measurements(), period: 10_000}
+ # Add reporters as children of your supervision tree.
+ # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()}
+ ]
+
+ Supervisor.init(children, strategy: :one_for_one)
+ end
+
+ def metrics do
+ [
+ # Phoenix Metrics
+ summary("phoenix.endpoint.start.system_time",
+ unit: {:native, :millisecond}
+ ),
+ summary("phoenix.endpoint.stop.duration",
+ unit: {:native, :millisecond}
+ ),
+ summary("phoenix.router_dispatch.start.system_time",
+ tags: [:route],
+ unit: {:native, :millisecond}
+ ),
+ summary("phoenix.router_dispatch.exception.duration",
+ tags: [:route],
+ unit: {:native, :millisecond}
+ ),
+ summary("phoenix.router_dispatch.stop.duration",
+ tags: [:route],
+ unit: {:native, :millisecond}
+ ),
+ summary("phoenix.socket_connected.duration",
+ unit: {:native, :millisecond}
+ ),
+ summary("phoenix.channel_joined.duration",
+ unit: {:native, :millisecond}
+ ),
+ summary("phoenix.channel_handled_in.duration",
+ tags: [:event],
+ unit: {:native, :millisecond}
+ ),
+
+ # VM Metrics
+ summary("vm.memory.total", unit: {:byte, :kilobyte}),
+ summary("vm.total_run_queue_lengths.total"),
+ summary("vm.total_run_queue_lengths.cpu"),
+ summary("vm.total_run_queue_lengths.io")
+ ]
+ end
+
+ defp periodic_measurements do
+ [
+ # A module, function and arguments to be invoked periodically.
+ # This function must call :telemetry.execute/3 and a metric must be added above.
+ # {DemoWeb, :count_users, []}
+ ]
+ end
+end
diff --git a/demo/mix.exs b/demo/demo_wallaby/mix.exs
similarity index 94%
rename from demo/mix.exs
rename to demo/demo_wallaby/mix.exs
index cbc3cf9..475354b 100644
--- a/demo/mix.exs
+++ b/demo/demo_wallaby/mix.exs
@@ -45,8 +45,7 @@ defmodule Demo.MixProject do
{:dns_cluster, "~> 0.1.1"},
{:bandit, "~> 1.2"},
{:wallaby, "~> 0.30", runtime: false, only: :test},
- {:hound, "~> 1.1", runtime: false, only: :test},
- {:a11y_audit, path: "../", only: [:test]}
+ {:a11y_audit, path: "../../", only: [:test]}
]
end
diff --git a/demo/mix.lock b/demo/demo_wallaby/mix.lock
similarity index 97%
rename from demo/mix.lock
rename to demo/demo_wallaby/mix.lock
index 69eda04..9aac913 100644
--- a/demo/mix.lock
+++ b/demo/demo_wallaby/mix.lock
@@ -7,7 +7,6 @@
"file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
"floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"},
"hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"},
- "hound": {:hex, :hound, "1.1.1", "d3afce4cf0f446331d9d00427e9eb74fa135c296b1d3745d4bbe2096ce259087", [:mix], [{:hackney, "~> 1.5", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8c6342b49f53bb0e5c51d5ecca18a8ce872c44da05a8ce6f828385ebd744fe2a"},
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
"httpoison": {:hex, :httpoison, "2.2.1", "87b7ed6d95db0389f7df02779644171d7319d319178f6680438167d7b69b1f3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "51364e6d2f429d80e14fe4b5f8e39719cacd03eb3f9a9286e61e216feac2d2df"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
diff --git a/demo/demo_wallaby/priv/static/favicon.ico b/demo/demo_wallaby/priv/static/favicon.ico
new file mode 100644
index 0000000..7f372bf
Binary files /dev/null and b/demo/demo_wallaby/priv/static/favicon.ico differ
diff --git a/demo/demo_wallaby/priv/static/images/logo.svg b/demo/demo_wallaby/priv/static/images/logo.svg
new file mode 100644
index 0000000..9f26bab
--- /dev/null
+++ b/demo/demo_wallaby/priv/static/images/logo.svg
@@ -0,0 +1,6 @@
+
+
+
diff --git a/demo/demo_wallaby/priv/static/robots.txt b/demo/demo_wallaby/priv/static/robots.txt
new file mode 100644
index 0000000..26e06b5
--- /dev/null
+++ b/demo/demo_wallaby/priv/static/robots.txt
@@ -0,0 +1,5 @@
+# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
+#
+# To ban all spiders from the entire site uncomment the next two lines:
+# User-agent: *
+# Disallow: /
diff --git a/demo/demo_wallaby/test/demo_web/controllers/error_html_test.exs b/demo/demo_wallaby/test/demo_web/controllers/error_html_test.exs
new file mode 100644
index 0000000..aaf8e9f
--- /dev/null
+++ b/demo/demo_wallaby/test/demo_web/controllers/error_html_test.exs
@@ -0,0 +1,14 @@
+defmodule DemoWeb.ErrorHTMLTest do
+ use DemoWeb.ConnCase, async: true
+
+ # Bring render_to_string/4 for testing custom views
+ import Phoenix.Template
+
+ test "renders 404.html" do
+ assert render_to_string(DemoWeb.ErrorHTML, "404", "html", []) == "Not Found"
+ end
+
+ test "renders 500.html" do
+ assert render_to_string(DemoWeb.ErrorHTML, "500", "html", []) == "Internal Server Error"
+ end
+end
diff --git a/demo/demo_wallaby/test/demo_web/controllers/error_json_test.exs b/demo/demo_wallaby/test/demo_web/controllers/error_json_test.exs
new file mode 100644
index 0000000..aa12730
--- /dev/null
+++ b/demo/demo_wallaby/test/demo_web/controllers/error_json_test.exs
@@ -0,0 +1,12 @@
+defmodule DemoWeb.ErrorJSONTest do
+ use DemoWeb.ConnCase, async: true
+
+ test "renders 404" do
+ assert DemoWeb.ErrorJSON.render("404.json", %{}) == %{errors: %{detail: "Not Found"}}
+ end
+
+ test "renders 500" do
+ assert DemoWeb.ErrorJSON.render("500.json", %{}) ==
+ %{errors: %{detail: "Internal Server Error"}}
+ end
+end
diff --git a/demo/test/demo_web/integration/pages_wallaby_test.exs b/demo/demo_wallaby/test/demo_web/integration/pages_test.exs
similarity index 96%
rename from demo/test/demo_web/integration/pages_wallaby_test.exs
rename to demo/demo_wallaby/test/demo_web/integration/pages_test.exs
index ed55ae1..96484d5 100644
--- a/demo/test/demo_web/integration/pages_wallaby_test.exs
+++ b/demo/demo_wallaby/test/demo_web/integration/pages_test.exs
@@ -1,4 +1,4 @@
-defmodule DemoWeb.PagesWallabyTest do
+defmodule DemoWeb.PagesTest do
use ExUnit.Case, async: true
use Wallaby.Feature
diff --git a/demo/demo_wallaby/test/support/conn_case.ex b/demo/demo_wallaby/test/support/conn_case.ex
new file mode 100644
index 0000000..e51004a
--- /dev/null
+++ b/demo/demo_wallaby/test/support/conn_case.ex
@@ -0,0 +1,37 @@
+defmodule DemoWeb.ConnCase do
+ @moduledoc """
+ This module defines the test case to be used by
+ tests that require setting up a connection.
+
+ Such tests rely on `Phoenix.ConnTest` and also
+ import other functionality to make it easier
+ to build common data structures and query the data layer.
+
+ Finally, if the test case interacts with the database,
+ we enable the SQL sandbox, so changes done to the database
+ are reverted at the end of every test. If you are using
+ PostgreSQL, you can even run database tests asynchronously
+ by setting `use DemoWeb.ConnCase, async: true`, although
+ this option is not recommended for other databases.
+ """
+
+ use ExUnit.CaseTemplate
+
+ using do
+ quote do
+ # The default endpoint for testing
+ @endpoint DemoWeb.Endpoint
+
+ use DemoWeb, :verified_routes
+
+ # Import conveniences for testing with connections
+ import Plug.Conn
+ import Phoenix.ConnTest
+ import DemoWeb.ConnCase
+ end
+ end
+
+ setup _tags do
+ {:ok, conn: Phoenix.ConnTest.build_conn()}
+ end
+end
diff --git a/demo/test/test_helper.exs b/demo/demo_wallaby/test/test_helper.exs
similarity index 72%
rename from demo/test/test_helper.exs
rename to demo/demo_wallaby/test/test_helper.exs
index fc756b6..d480fed 100644
--- a/demo/test/test_helper.exs
+++ b/demo/demo_wallaby/test/test_helper.exs
@@ -1,4 +1,3 @@
ExUnit.start()
Application.put_env(:wallaby, :base_url, DemoWeb.Endpoint.url())
{:ok, _} = Application.ensure_all_started(:wallaby)
-{:ok, _} = Application.ensure_all_started(:hound)
diff --git a/mix.exs b/mix.exs
index 8b19ecc..dfa038a 100644
--- a/mix.exs
+++ b/mix.exs
@@ -42,7 +42,7 @@ defmodule A11yAudit.MixProject do
end
defp description() do
- "Automated accessibility checks for Elixir web apps, based on axe-core."
+ " Accessibility auditing for Elixir web applications using axe-core"
end
defp package() do