-
-
<%= if Plausible.Teams.locked?(@site.team) do %>
+ <%= if @verify_installation? do %>
+ {live_render(@conn, PlausibleWeb.Live.Verification,
+ id: "live-verification",
+ session: @verification_session
+ )}
+ <% end %>
<%= if ee?() && !@conn.assigns[:current_user] && @conn.assigns[:demo] do %>
diff --git a/priv/repo/migrations/20260727120000_add_onboarding_status_to_sites.exs b/priv/repo/migrations/20260727120000_add_onboarding_status_to_sites.exs
new file mode 100644
index 000000000000..c2421fdbc363
--- /dev/null
+++ b/priv/repo/migrations/20260727120000_add_onboarding_status_to_sites.exs
@@ -0,0 +1,9 @@
+defmodule Plausible.Repo.Migrations.AddOnboardingStatusToSites do
+ use Ecto.Migration
+
+ def change do
+ alter table(:sites) do
+ add :onboarding_status, :string, null: false, default: "completed"
+ end
+ end
+end
diff --git a/test/plausible/installation_support/check_runner_test.exs b/test/plausible/installation_support/check_runner_test.exs
new file mode 100644
index 000000000000..752391bdc23f
--- /dev/null
+++ b/test/plausible/installation_support/check_runner_test.exs
@@ -0,0 +1,58 @@
+defmodule Plausible.InstallationSupport.CheckRunnerTest do
+ use Plausible.DataCase, async: true
+
+ on_ee do
+ alias Plausible.InstallationSupport.{CheckRunner, State}
+
+ defmodule NoopCheck do
+ @moduledoc false
+ use Plausible.InstallationSupport.Check
+
+ @impl true
+ def report_progress_as, do: "noop"
+
+ @impl true
+ def perform(state, _opts), do: state
+ end
+
+ defp init_state do
+ %State{url: "https://example.com", data_domain: "example.com", report_to: self()}
+ end
+
+ describe "run/3" do
+ test "defaults launch_delay to 0" do
+ started_at = System.monotonic_time(:millisecond)
+
+ CheckRunner.run(init_state(), [{NoopCheck, []}], async?: false, slowdown: 0)
+
+ assert System.monotonic_time(:millisecond) - started_at < 100
+ end
+
+ test "awaits :launch_delay before the first check starts" do
+ started_at = System.monotonic_time(:millisecond)
+
+ {:ok, _pid} =
+ CheckRunner.run(init_state(), [{NoopCheck, []}],
+ slowdown: 0,
+ launch_delay: 100,
+ report_to: self()
+ )
+
+ assert_receive {:check_start, {NoopCheck, _state}}, 1000
+
+ assert System.monotonic_time(:millisecond) - started_at >= 100
+ end
+
+ test "runs the first check immediately when :launch_delay is 0" do
+ {:ok, _pid} =
+ CheckRunner.run(init_state(), [{NoopCheck, []}],
+ slowdown: 0,
+ launch_delay: 0,
+ report_to: self()
+ )
+
+ assert_receive {:check_start, {NoopCheck, _state}}, 100
+ end
+ end
+ end
+end
diff --git a/test/plausible/installation_support/verification/checks_mock_test.exs b/test/plausible/installation_support/verification/checks_mock_test.exs
new file mode 100644
index 000000000000..c4d2bf0c7c62
--- /dev/null
+++ b/test/plausible/installation_support/verification/checks_mock_test.exs
@@ -0,0 +1,152 @@
+defmodule Plausible.InstallationSupport.Verification.ChecksMockTest do
+ use Plausible.DataCase, async: true
+
+ on_ee do
+ alias Plausible.InstallationSupport.{Checks, Result}
+ alias Plausible.InstallationSupport.Verification.{ChecksMock, Diagnostics, MockScenarios}
+
+ @url "https://example.com"
+
+ describe "run/4" do
+ test "raises when no scenario is registered for the domain" do
+ domain = insert(:site).domain
+
+ assert_raise RuntimeError, ~r/no scenario is registered/, fn ->
+ ChecksMock.run(@url, domain, "manual",
+ async?: false,
+ slowdown: 0,
+ launch_delay: 0,
+ report_to: nil
+ )
+ end
+ end
+
+ test "runs synchronously, keeping the given installation_type in the resulting state" do
+ domain = insert(:site).domain
+ :ok = MockScenarios.put(domain, :success)
+
+ state =
+ ChecksMock.run(@url, domain, "wordpress",
+ async?: false,
+ slowdown: 0,
+ launch_delay: 0,
+ report_to: nil
+ )
+
+ assert state.url == @url
+ assert state.data_domain == domain
+ assert state.diagnostics.selected_installation_type == "wordpress"
+ end
+
+ test "notifies check_start for all 3 checks with the same messages as real verification, then all_checks_done" do
+ domain = insert(:site).domain
+ :ok = MockScenarios.put(domain, :success)
+
+ ChecksMock.run(@url, domain, "manual",
+ async?: false,
+ slowdown: 0,
+ launch_delay: 0,
+ report_to: self()
+ )
+
+ assert_received {:check_start, {ChecksMock.FakeUrlCheck, _state}}
+ assert_received {:check_start, {ChecksMock.FakeVerifyInstallationCheck, _state}}
+ assert_received {:check_start, {ChecksMock.FakeVerifyInstallationCacheBustCheck, _state}}
+ assert_received {:all_checks_done, %{data_domain: ^domain}}
+
+ assert ChecksMock.FakeUrlCheck.report_progress_as() ==
+ Checks.Url.report_progress_as()
+
+ assert ChecksMock.FakeVerifyInstallationCheck.report_progress_as() ==
+ Checks.VerifyInstallation.report_progress_as()
+
+ assert ChecksMock.FakeVerifyInstallationCacheBustCheck.report_progress_as() ==
+ Checks.VerifyInstallationCacheBust.report_progress_as()
+ end
+
+ test "defaults state.url from data_domain when called with url: nil, mirroring the real Url check" do
+ domain = insert(:site).domain
+ :ok = MockScenarios.put(domain, :success)
+
+ state =
+ ChecksMock.run(nil, domain, "manual",
+ async?: false,
+ slowdown: 0,
+ launch_delay: 0,
+ report_to: nil
+ )
+
+ assert state.url == "https://#{domain}"
+ end
+ end
+
+ describe "interpret_diagnostics/1" do
+ test "returns the named result for the registered scenario" do
+ domain = insert(:site).domain
+ :ok = MockScenarios.put(domain, :success)
+
+ state =
+ ChecksMock.run(@url, domain, "manual",
+ async?: false,
+ slowdown: 0,
+ launch_delay: 0,
+ report_to: nil
+ )
+
+ assert %Result{ok?: true} = ChecksMock.interpret_diagnostics(state)
+ end
+
+ test "returns interpretation based on installation type" do
+ domain = insert(:site).domain
+ :ok = MockScenarios.put(domain, :plausible_not_found)
+
+ state =
+ ChecksMock.run(@url, domain, "wordpress",
+ async?: false,
+ slowdown: 0,
+ launch_delay: 0,
+ report_to: nil
+ )
+
+ assert %Result{
+ ok?: false,
+ recommendations: [%{text: recommendation}]
+ } = ChecksMock.interpret_diagnostics(state)
+
+ assert recommendation =~ "WordPress plugin"
+ end
+
+ test "uses state.url (e.g. a custom retry URL) as attempted_url, not just the bare domain" do
+ domain = insert(:site).domain
+ :ok = MockScenarios.put(domain, :domain_not_found)
+
+ custom_url = "https://abc.de"
+
+ state =
+ ChecksMock.run(custom_url, domain, "manual",
+ async?: false,
+ slowdown: 0,
+ launch_delay: 0,
+ report_to: nil
+ )
+
+ assert %Result{errors: [error]} = ChecksMock.interpret_diagnostics(state)
+ assert error =~ custom_url
+ end
+
+ test "raises when no scenario is registered for the domain" do
+ domain = insert(:site).domain
+
+ state = %Plausible.InstallationSupport.State{
+ url: @url,
+ data_domain: domain,
+ diagnostics: %Diagnostics{selected_installation_type: "manual"}
+ }
+
+ assert_raise RuntimeError, ~r/no scenario is registered/, fn ->
+ ChecksMock.interpret_diagnostics(state)
+ end
+ end
+ end
+ end
+end
diff --git a/test/plausible/installation_support/verification/checks_observability_test.exs b/test/plausible/installation_support/verification/checks_observability_test.exs
index 5464c08c019a..eda236fcc05e 100644
--- a/test/plausible/installation_support/verification/checks_observability_test.exs
+++ b/test/plausible/installation_support/verification/checks_observability_test.exs
@@ -136,7 +136,8 @@ defmodule Plausible.InstallationSupport.Verification.ChecksObservabilityTest do
verify_installation_check_timeout: 100,
report_to: nil,
async?: false,
- slowdown: 0
+ slowdown: 0,
+ launch_delay: 0
)
log = capture_log(fn -> Checks.interpret_diagnostics(state) end)
@@ -167,7 +168,8 @@ defmodule Plausible.InstallationSupport.Verification.ChecksObservabilityTest do
Checks.run(@url_to_verify, @expected_domain, "manual",
report_to: nil,
async?: false,
- slowdown: 0
+ slowdown: 0,
+ launch_delay: 0
)
end
diff --git a/test/plausible/installation_support/verification/checks_test.exs b/test/plausible/installation_support/verification/checks_test.exs
index 01e01e4d7910..f893a490e604 100644
--- a/test/plausible/installation_support/verification/checks_test.exs
+++ b/test/plausible/installation_support/verification/checks_test.exs
@@ -13,6 +13,11 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do
@expected_domain "example.com"
@url_to_verify "https://#{@expected_domain}"
+ @verify_manually_url "https://plausible.io/docs/troubleshoot-integration#how-to-manually-check-your-integration"
+ @verify_manually_inline_link %{
+ text: "verify your installation manually",
+ href: @verify_manually_url
+ }
describe "URL check" do
test "returns error when DNS check fails with domain not found error, offers custom URL input" do
@@ -22,21 +27,21 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do
ok?: false,
data: %{offer_custom_url_input: true},
errors: [
- ^any(:string, ~r/We couldn't find your website at #{@url_to_verify}$/)
+ ^any(:string, ~r/We couldn't reach #{@url_to_verify}$/)
],
recommendations: [
%{
text:
- "Please check that the domain you entered is correct and reachable publicly. If it's intentionally private, you'll need to verify that Plausible works manually",
- url:
- "https://plausible.io/docs/troubleshoot-integration#how-to-manually-check-your-integration"
+ "Check that the URL is correct and publicly accessible. If your site is intentionally private, you'll need to verify your installation manually",
+ inline_links: [^@verify_manually_inline_link]
}
]
} =
Checks.run(@url_to_verify, @expected_domain, "manual",
report_to: nil,
async?: false,
- slowdown: 0
+ slowdown: 0,
+ launch_delay: 0
)
|> Checks.interpret_diagnostics()
end
@@ -49,21 +54,21 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do
ok?: false,
data: %{offer_custom_url_input: true},
errors: [
- ^any(:string, ~r/We couldn't find your website at #{url_to_verify}$/)
+ ^any(:string, ~r/We couldn't reach #{url_to_verify}$/)
],
recommendations: [
%{
text:
- "Please check that the domain you entered is correct and reachable publicly. If it's intentionally private, you'll need to verify that Plausible works manually",
- url:
- "https://plausible.io/docs/troubleshoot-integration#how-to-manually-check-your-integration"
+ "Check that the URL is correct and publicly accessible. If your site is intentionally private, you'll need to verify your installation manually",
+ inline_links: [^@verify_manually_inline_link]
}
]
} =
Checks.run(url_to_verify, @expected_domain, "manual",
report_to: nil,
async?: false,
- slowdown: 0
+ slowdown: 0,
+ launch_delay: 0
)
|> Checks.interpret_diagnostics()
end
@@ -94,13 +99,13 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do
for {installation_type, expected_recommendation} <- [
{"wordpress",
- "Please check that you've installed the WordPress plugin correctly, or verify your installation manually"},
+ "Check you've installed the WordPress plugin correctly, or verify your installation manually"},
{"gtm",
- "Please check that you've entered the ID in the GTM template correctly, or verify your installation manually"},
+ "Check you've entered the ID in the GTM template correctly, or verify your installation manually"},
{"npm",
- "Please check that you've initialized Plausible with the correct domain, or verify your installation manually"},
+ "Check you've initialized Plausible with the correct domain, or verify your installation manually"},
{"manual",
- "Please check that the snippet on your site matches the installation instructions exactly, or verify your installation manually"}
+ "Check that the snippet on your site matches the one shown in the installation instructions, or verify your installation manually"}
] do
test "returns error when test event domain doesn't match the expected domain, with recommendation for installation type: #{installation_type}" do
verification_stub =
@@ -119,12 +124,11 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do
assert_matches %Result{
ok?: false,
- errors: ["Plausible test event is not for this site"],
+ errors: ["Your Plausible snippet is configured for a different domain"],
recommendations: [
%{
text: unquote(expected_recommendation),
- url:
- "https://plausible.io/docs/troubleshoot-integration#how-to-manually-check-your-integration"
+ inline_links: [^@verify_manually_inline_link]
}
]
} =
@@ -154,11 +158,16 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do
assert_matches %Result{
ok?: false,
- errors: [^any(:string, ~r/.*proxy.*/)],
+ errors: [^any(:string, ~r/.*proxied.*/)],
recommendations: [
%{
text: ^any(:string, ~r/.*proxied.*/),
- url: "https://plausible.io/docs/proxy/introduction"
+ inline_links: [
+ %{
+ text: "Learn more",
+ href: "https://plausible.io/docs/proxy/introduction"
+ }
+ ]
}
]
} =
@@ -187,9 +196,9 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do
errors: [^any(:string, ~r/.*couldn't verify.*/)],
recommendations: [
%{
- text: ^any(:string, ~r/.*try verifying again.*/),
- url:
- "https://plausible.io/docs/troubleshoot-integration#how-to-manually-check-your-integration"
+ text:
+ "Please try verifying again in a few minutes, or verify your installation manually",
+ inline_links: [^@verify_manually_inline_link]
}
]
} =
@@ -213,9 +222,8 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do
recommendations: [
%{
text:
- "Please make sure you've copied the snippet to the head of your site, or verify your installation manually",
- url:
- "https://plausible.io/docs/troubleshoot-integration#how-to-manually-check-your-integration"
+ "Make sure you've copied the snippet to the head of your site, or verify your installation manually",
+ inline_links: [^@verify_manually_inline_link]
}
]
} =
@@ -236,14 +244,19 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do
assert_matches %Result{
ok?: false,
errors: [
- "We encountered an issue with your site's Content Security Policy (CSP)"
+ "Your site's Content Security Policy (CSP) is blocking Plausible"
],
recommendations: [
%{
text:
- "Please add plausible.io domain specifically to the allowed list of domains in your site's CSP",
- url:
- "https://plausible.io/docs/troubleshoot-integration#does-your-site-use-a-content-security-policy-csp"
+ "Add plausible.io to the list of allowed domains in your site's Content Security Policy to allow Plausible to collect analytics. Learn more",
+ inline_links: [
+ %{
+ text: "Learn more",
+ href:
+ "https://plausible.io/docs/troubleshoot-integration#does-your-site-use-a-content-security-policy-csp"
+ }
+ ]
}
]
} =
@@ -261,15 +274,12 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do
assert_matches %Result{
ok?: false,
data: %{offer_custom_url_input: true},
- errors: [
- "We couldn't verify your website at https://example.com"
- ],
+ errors: ["We couldn't verify https://example.com"],
recommendations: [
%{
text:
- "Accessing the website resulted in a network error. Please verify your installation manually",
- url:
- "https://plausible.io/docs/troubleshoot-integration#how-to-manually-check-your-integration"
+ "We encountered a network error while trying to access your website. You can verify your installation manually",
+ inline_links: [^@verify_manually_inline_link]
}
]
} =
@@ -291,15 +301,12 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do
assert_matches %Result{
ok?: false,
data: %{offer_custom_url_input: true},
- errors: [
- "We couldn't verify your website at https://example.com"
- ],
+ errors: ["We couldn't verify https://example.com"],
recommendations: [
%{
text:
- "Accessing the website resulted in an unexpected status code 403. Please check for anything that might be blocking us from reaching your site, like a firewall, authentication requirements, or CDN rules. If you'd prefer, you can skip this and verify your installation manually",
- url:
- "https://plausible.io/docs/troubleshoot-integration#how-to-manually-check-your-integration"
+ "Accessing your website returned an unexpected status code (403). Check for anything that might be blocking our access to your site, such as a firewall, authentication requirements, or CDN rules. You can also verify your installation manually",
+ inline_links: [^@verify_manually_inline_link]
}
]
} =
@@ -309,13 +316,13 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do
for {installation_type, expected_recommendation} <- [
{"wordpress",
- "Please make sure you've enabled the plugin, or verify your installation manually"},
+ "Make sure you've enabled the WordPress plugin, or verify your installation manually"},
{"gtm",
- "Please make sure you've configured the GTM template correctly, or verify your installation manually"},
+ "Make sure you've configured the GTM template correctly, or verify your installation manually"},
{"npm",
- "Please make sure you've initialized Plausible on your site, or verify your installation manually"},
+ "Make sure you've initialized Plausible on your site, or verify your installation manually"},
{"manual",
- "Please make sure you've copied the snippet to the head of your site, or verify your installation manually"}
+ "Make sure you've copied the snippet to the head of your site, or verify your installation manually"}
] do
test "returns error \"We couldn't detect Plausible on your site\" when plausible_is_on_window is false (with best guess recommendation for installation type: #{installation_type})" do
verification_stub =
@@ -334,8 +341,7 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do
recommendations: [
%{
text: unquote(expected_recommendation),
- url:
- "https://plausible.io/docs/troubleshoot-integration#how-to-manually-check-your-integration"
+ inline_links: [^@verify_manually_inline_link]
}
]
} =
@@ -363,8 +369,7 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do
recommendations: [
%{
text: unquote(expected_recommendation),
- url:
- "https://plausible.io/docs/troubleshoot-integration#how-to-manually-check-your-integration"
+ inline_links: [^@verify_manually_inline_link]
}
]
} =
@@ -426,8 +431,13 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do
recommendations: [
%{
text: ^any(:string, ~r/.*cache.*/),
- url:
- "https://plausible.io/docs/troubleshoot-integration#have-you-cleared-the-cache-of-your-site"
+ inline_links: [
+ %{
+ text: "Learn more",
+ href:
+ "https://plausible.io/docs/troubleshoot-integration#have-you-cleared-the-cache-of-your-site"
+ }
+ ]
}
]
} = run_checks(verification_stub) |> Checks.interpret_diagnostics()
@@ -484,12 +494,12 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do
assert_matches %Result{
ok?: false,
- errors: [^any(:string, ~r/.*temporary service error.*/)],
+ errors: [^any(:string, ~r/.*temporarily unavailable.*/)],
recommendations: [
%{
- text: ^any(:string, ~r/.*in a few minutes.*/),
- url:
- "https://plausible.io/docs/troubleshoot-integration#how-to-manually-check-your-integration"
+ text:
+ "Please try again in a few minutes or verify your installation manually",
+ inline_links: [^@verify_manually_inline_link]
}
]
} = Checks.interpret_diagnostics(state)
@@ -514,12 +524,12 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do
assert_matches %Result{
ok?: false,
- errors: [^any(:string, ~r/.*temporary service error.*/)],
+ errors: [^any(:string, ~r/.*temporarily unavailable.*/)],
recommendations: [
%{
- text: ^any(:string, ~r/.*in a few minutes.*/),
- url:
- "https://plausible.io/docs/troubleshoot-integration#how-to-manually-check-your-integration"
+ text:
+ "Please try again in a few minutes or verify your installation manually",
+ inline_links: [^@verify_manually_inline_link]
}
]
} = Checks.interpret_diagnostics(state)
@@ -553,7 +563,8 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do
Checks.run(@url_to_verify, @expected_domain, installation_type,
report_to: nil,
async?: false,
- slowdown: 0
+ slowdown: 0,
+ launch_delay: 0
)
assert expected_req_count == :atomics.get(counter, 1)
@@ -565,7 +576,8 @@ defmodule Plausible.InstallationSupport.Verification.ChecksTest do
Checks.run(@url_to_verify, @expected_domain, installation_type,
report_to: nil,
async?: false,
- slowdown: 0
+ slowdown: 0,
+ launch_delay: 0
)
end
end
diff --git a/test/plausible/installation_support/verification/diagnostics_test.exs b/test/plausible/installation_support/verification/diagnostics_test.exs
new file mode 100644
index 000000000000..21a065654faa
--- /dev/null
+++ b/test/plausible/installation_support/verification/diagnostics_test.exs
@@ -0,0 +1,49 @@
+defmodule Plausible.InstallationSupport.Verification.DiagnosticsTest do
+ use ExUnit.Case, async: true
+ use Plausible
+
+ on_ee do
+ alias Plausible.InstallationSupport.Verification.Diagnostics.Error
+
+ describe "Error.new!/1" do
+ test "accepts a recommendation whose inline_links text appears exactly once" do
+ assert %Error{} =
+ Error.new!(%{
+ message: "Something went wrong",
+ recommendation: "Check the docs for more info",
+ inline_links: [%{text: "docs", href: "https://plausible.io/docs"}]
+ })
+ end
+
+ test "raises when inline_links text isn't found in the recommendation" do
+ assert_raise ArgumentError, ~r/must appear exactly once/, fn ->
+ Error.new!(%{
+ message: "Something went wrong",
+ recommendation: "Check the manual for more info",
+ inline_links: [%{text: "docs", href: "https://plausible.io/docs"}]
+ })
+ end
+ end
+
+ test "raises when inline_links text appears more than once in the recommendation" do
+ assert_raise ArgumentError, ~r/must appear exactly once/, fn ->
+ Error.new!(%{
+ message: "Something went wrong",
+ recommendation: "Check the docs, or check the docs again",
+ inline_links: [%{text: "the docs", href: "https://plausible.io/docs"}]
+ })
+ end
+ end
+
+ test "raises when inline_links href doesn't point at plausible.io" do
+ assert_raise ArgumentError, ~r/must start with/, fn ->
+ Error.new!(%{
+ message: "Something went wrong",
+ recommendation: "Check the docs for more info",
+ inline_links: [%{text: "docs", href: "https://example.com/docs"}]
+ })
+ end
+ end
+ end
+ end
+end
diff --git a/test/plausible/installation_support/verification/mock_scenarios_test.exs b/test/plausible/installation_support/verification/mock_scenarios_test.exs
new file mode 100644
index 000000000000..ed191e6cc410
--- /dev/null
+++ b/test/plausible/installation_support/verification/mock_scenarios_test.exs
@@ -0,0 +1,90 @@
+defmodule Plausible.InstallationSupport.Verification.MockScenariosTest do
+ use Plausible.DataCase, async: true
+
+ on_ee do
+ alias Plausible.InstallationSupport.Verification.MockScenarios
+
+ test "get/1 returns nil for a domain with no registered scenario" do
+ site = insert(:site)
+
+ assert MockScenarios.get(site.domain) == nil
+ end
+
+ test "put/3 registers a scenario, get/1 returns it" do
+ site = insert(:site)
+
+ :ok = MockScenarios.put(site.domain, :success, [])
+
+ assert MockScenarios.get(site.domain) == %{
+ interpretation_result: :success,
+ slowdown: nil,
+ launch_delay: nil
+ }
+ end
+
+ test "put/3 stores a slowdown opt alongside the interpretation result" do
+ site = insert(:site)
+
+ :ok = MockScenarios.put(site.domain, :domain_not_found, slowdown: 2000)
+
+ assert MockScenarios.get(site.domain) == %{
+ interpretation_result: :domain_not_found,
+ slowdown: 2000,
+ launch_delay: nil
+ }
+ end
+
+ test "put/3 stores a launch_delay opt alongside the interpretation result" do
+ site = insert(:site)
+
+ :ok = MockScenarios.put(site.domain, :domain_not_found, launch_delay: 2000)
+
+ assert MockScenarios.get(site.domain) == %{
+ interpretation_result: :domain_not_found,
+ slowdown: nil,
+ launch_delay: 2000
+ }
+ end
+
+ test "put/3 overwrites a previously registered scenario for the same domain" do
+ site = insert(:site)
+
+ :ok = MockScenarios.put(site.domain, :success, [])
+ :ok = MockScenarios.put(site.domain, :csp_disallowed, [])
+
+ assert MockScenarios.get(site.domain) == %{
+ interpretation_result: :csp_disallowed,
+ slowdown: nil,
+ launch_delay: nil
+ }
+ end
+
+ test "scenarios registered for one domain never leak into another domain on the same registry" do
+ site_a = insert(:site)
+ site_b = insert(:site)
+
+ :ok = MockScenarios.put(site_a.domain, :success, [])
+ :ok = MockScenarios.put(site_b.domain, :domain_not_found, slowdown: 500, launch_delay: 100)
+
+ assert MockScenarios.get(site_a.domain) == %{
+ interpretation_result: :success,
+ slowdown: nil,
+ launch_delay: nil
+ }
+
+ assert MockScenarios.get(site_b.domain) == %{
+ interpretation_result: :domain_not_found,
+ slowdown: 500,
+ launch_delay: 100
+ }
+
+ :ok = MockScenarios.put(site_a.domain, :csp_disallowed, [])
+
+ assert MockScenarios.get(site_b.domain) == %{
+ interpretation_result: :domain_not_found,
+ slowdown: 500,
+ launch_delay: 100
+ }
+ end
+ end
+end
diff --git a/test/plausible/site/schema_test.exs b/test/plausible/site/schema_test.exs
index 91a19761f268..aa1cf24212a2 100644
--- a/test/plausible/site/schema_test.exs
+++ b/test/plausible/site/schema_test.exs
@@ -4,6 +4,25 @@ defmodule Plausible.SiteTest do
doctest Plausible.Site
+ describe "new/1" do
+ test "sets onboarding_status to :new_site by default" do
+ changeset = Site.new(%{"domain" => "example.com", "timezone" => "Europe/London"})
+
+ assert Ecto.Changeset.get_change(changeset, :onboarding_status) == :new_site
+ end
+
+ test "sets onboarding_status to :completed for a consolidated site" do
+ changeset =
+ Site.new(%{
+ "domain" => "example.com",
+ "timezone" => "Europe/London",
+ "consolidated" => true
+ })
+
+ assert Ecto.Changeset.apply_changes(changeset).onboarding_status == :completed
+ end
+ end
+
describe "tz_offset/2" do
test "returns offset from utc in seconds" do
site = build(:site, timezone: "US/Eastern")
@@ -31,4 +50,62 @@ defmodule Plausible.SiteTest do
assert Site.tz_offset(site, ~U[2023-11-05 06:00:00Z]) == -18_000
end
end
+
+ describe "put_onboarding_status_advance/2" do
+ test "advances onboarding_status forward" do
+ site = insert(:site, onboarding_status: :new_site)
+
+ changeset = Site.put_onboarding_status_advance(site, :verification_succeeded)
+
+ assert Ecto.Changeset.get_change(changeset, :onboarding_status) == :verification_succeeded
+ end
+
+ test "can skip an intermediate status" do
+ site = insert(:site, onboarding_status: :new_site)
+
+ changeset = Site.put_onboarding_status_advance(site, :first_pageview)
+
+ assert Ecto.Changeset.get_change(changeset, :onboarding_status) == :first_pageview
+ end
+
+ test "is a no-op when the site is already at the given status" do
+ site = insert(:site, onboarding_status: :verification_succeeded)
+
+ changeset = Site.put_onboarding_status_advance(site, :verification_succeeded)
+
+ refute Ecto.Changeset.get_change(changeset, :onboarding_status)
+ end
+
+ test "is a no-op when the site is already past the given status" do
+ site = insert(:site, onboarding_status: :completed)
+
+ changeset = Site.put_onboarding_status_advance(site, :verification_succeeded)
+
+ refute Ecto.Changeset.get_change(changeset, :onboarding_status)
+ end
+
+ test "composes with other changes on the same changeset" do
+ site = insert(:site, onboarding_status: :new_site)
+
+ changeset =
+ site
+ |> Site.set_stats_start_date(~D[2024-01-01])
+ |> Site.put_onboarding_status_advance(:first_pageview)
+
+ assert Ecto.Changeset.get_change(changeset, :stats_start_date) == ~D[2024-01-01]
+ assert Ecto.Changeset.get_change(changeset, :onboarding_status) == :first_pageview
+ end
+
+ test "persists via Repo.update!" do
+ site = insert(:site, onboarding_status: :new_site)
+
+ updated_site =
+ site
+ |> Site.put_onboarding_status_advance(:verification_succeeded)
+ |> Repo.update!()
+
+ assert updated_site.onboarding_status == :verification_succeeded
+ assert Repo.reload!(site).onboarding_status == :verification_succeeded
+ end
+ end
end
diff --git a/test/plausible/sites_test.exs b/test/plausible/sites_test.exs
index 5e9ea8db3cb6..8007f9f63867 100644
--- a/test/plausible/sites_test.exs
+++ b/test/plausible/sites_test.exs
@@ -143,11 +143,11 @@ defmodule Plausible.SitesTest do
end
end
- describe "stats_start_date" do
+ describe "ensure_stats_start_date" do
test "is nil if site has no stats" do
site = insert(:site)
- assert Sites.stats_start_date(site) == nil
+ assert Sites.ensure_stats_start_date(site).stats_start_date == nil
end
test "is date if site does have stats" do
@@ -157,7 +157,8 @@ defmodule Plausible.SitesTest do
build(:pageview)
])
- assert Sites.stats_start_date(site) == Plausible.Times.today(site.timezone)
+ assert Sites.ensure_stats_start_date(site).stats_start_date ==
+ Plausible.Times.today(site.timezone)
end
test "memoizes value of start date" do
@@ -169,10 +170,33 @@ defmodule Plausible.SitesTest do
build(:pageview)
])
- assert Sites.stats_start_date(site) == Plausible.Times.today(site.timezone)
+ assert Sites.ensure_stats_start_date(site).stats_start_date ==
+ Plausible.Times.today(site.timezone)
+
assert Repo.reload!(site).stats_start_date == Plausible.Times.today(site.timezone)
end
+ test "advances onboarding_status to :first_pageview when stats are first discovered, in the returned site" do
+ site = insert(:site, onboarding_status: :new_site)
+
+ populate_stats(site, [build(:pageview)])
+
+ updated_site = Sites.ensure_stats_start_date(site)
+
+ assert updated_site.onboarding_status == :first_pageview
+ assert Repo.reload!(site).onboarding_status == :first_pageview
+ end
+
+ test "does not regress :completed onboarding_status" do
+ site = insert(:site, onboarding_status: :completed, stats_start_date: nil)
+
+ populate_stats(site, [build(:pageview)])
+
+ Sites.ensure_stats_start_date(site)
+
+ assert Repo.reload!(site).onboarding_status == :completed
+ end
+
on_ee do
test "resets consolidated view stats dates every time" do
owner = new_user()
@@ -183,11 +207,12 @@ defmodule Plausible.SitesTest do
consolidated_view = new_consolidated_view(team)
assert consolidated_view.stats_start_date == ~D[2000-01-01]
- assert Sites.stats_start_date(consolidated_view) == ~D[2000-01-01]
+ assert Sites.ensure_stats_start_date(consolidated_view).stats_start_date == ~D[2000-01-01]
new_site(team: team, native_stats_start_at: ~N[1999-01-01 12:00:00])
- assert Sites.stats_start_date(consolidated_view) == ~D[1999-01-01]
+ assert Sites.ensure_stats_start_date(consolidated_view).stats_start_date ==
+ ~D[1999-01-01]
end
end
end
diff --git a/test/plausible_web/controllers/api/internal_controller_test.exs b/test/plausible_web/controllers/api/internal_controller_test.exs
index 1039882381e3..b8e640a5aeb4 100644
--- a/test/plausible_web/controllers/api/internal_controller_test.exs
+++ b/test/plausible_web/controllers/api/internal_controller_test.exs
@@ -11,9 +11,22 @@ defmodule PlausibleWeb.Api.InternalControllerTest do
conn = get(conn, "/api/sites")
%{"data" => sites} = json_response(conn, 200)
+ domains = Enum.map(sites, & &1["domain"])
- assert %{"domain" => site.domain} in sites
- assert %{"domain" => site2.domain} in sites
+ assert site.domain in domains
+ assert site2.domain in domains
+ end
+
+ @tag :ee_only
+ test "needs_verification reflects the site's onboarding status", %{conn: conn, user: user} do
+ new_site_ = new_site(owner: user, onboarding_status: :new_site)
+ onboarded_site = new_site(owner: user, onboarding_status: :completed)
+
+ conn = get(conn, "/api/sites")
+ %{"data" => sites} = json_response(conn, 200)
+
+ assert %{"domain" => new_site_.domain, "needs_verification" => true} in sites
+ assert %{"domain" => onboarded_site.domain, "needs_verification" => false} in sites
end
test "returns a list of max 9 site domains for the current user, putting pinned first", %{
@@ -44,16 +57,14 @@ defmodule PlausibleWeb.Api.InternalControllerTest do
%{"data" => sites} =
json_response(conn, 200)
+ domains = Enum.map(sites, & &1["domain"])
+
assert Enum.count(sites) == 9
- assert [
- %{"domain" => "site05.example.com"},
- %{"domain" => "site07.example.com"},
- %{"domain" => "site01.example.com"} | _
- ] = sites
+ assert ["site05.example.com", "site07.example.com", "site01.example.com" | _] = domains
- assert %{"domain" => "site09.example.com"} in sites
- refute %{"domain" => "sites10.example.com"} in sites
+ assert "site09.example.com" in domains
+ refute "sites10.example.com" in domains
end
end
@@ -142,4 +153,78 @@ defmodule PlausibleWeb.Api.InternalControllerTest do
assert %{conversions_enabled: true} = Plausible.Sites.get_by_domain(site.domain)
end
end
+
+ describe "PUT /api/:domain/complete-onboarding" do
+ setup [:create_user, :log_in]
+
+ test "when the logged-in user is the owner of the site", %{conn: conn, user: user} do
+ site = new_site(owner: user, onboarding_status: :first_pageview)
+
+ conn = put(conn, "/api/#{site.domain}/complete-onboarding")
+
+ assert json_response(conn, 200) == "ok"
+ assert %{onboarding_status: :completed} = Plausible.Sites.get_by_domain(site.domain)
+ end
+
+ test "when the logged-in user is an editor guest of the site", %{conn: conn, user: user} do
+ site = new_site(onboarding_status: :first_pageview)
+ add_guest(site, user: user, role: :editor)
+
+ conn = put(conn, "/api/#{site.domain}/complete-onboarding")
+
+ assert json_response(conn, 200) == "ok"
+ assert %{onboarding_status: :completed} = Plausible.Sites.get_by_domain(site.domain)
+ end
+
+ test "returns 401 when the logged-in user is a viewer of the site", %{conn: conn, user: user} do
+ site = new_site(onboarding_status: :first_pageview)
+ add_guest(site, user: user, role: :viewer)
+
+ conn = put(conn, "/api/#{site.domain}/complete-onboarding")
+
+ assert json_response(conn, 401) == %{
+ "error" => "You need to be logged in as the owner, admin, or editor of this site"
+ }
+
+ assert %{onboarding_status: :first_pageview} = Plausible.Sites.get_by_domain(site.domain)
+ end
+
+ test "returns 401 when the logged-in user doesn't have site access at all", %{conn: conn} do
+ site = new_site(onboarding_status: :first_pageview)
+
+ conn = put(conn, "/api/#{site.domain}/complete-onboarding")
+
+ assert json_response(conn, 401) == %{
+ "error" => "You need to be logged in as the owner, admin, or editor of this site"
+ }
+
+ assert %{onboarding_status: :first_pageview} = Plausible.Sites.get_by_domain(site.domain)
+ end
+
+ test "is idempotent - calling it again on an already-completed site is still a 200", %{
+ conn: conn,
+ user: user
+ } do
+ site = new_site(owner: user, onboarding_status: :completed)
+
+ conn = put(conn, "/api/#{site.domain}/complete-onboarding")
+
+ assert json_response(conn, 200) == "ok"
+ assert %{onboarding_status: :completed} = Plausible.Sites.get_by_domain(site.domain)
+ end
+ end
+
+ describe "PUT /api/:domain/complete-onboarding - user not logged in" do
+ test "returns 401 unauthorized", %{conn: conn} do
+ site = insert(:site, onboarding_status: :first_pageview)
+
+ conn = put(conn, "/api/#{site.domain}/complete-onboarding")
+
+ assert json_response(conn, 401) == %{
+ "error" => "You need to be logged in as the owner, admin, or editor of this site"
+ }
+
+ assert %{onboarding_status: :first_pageview} = Plausible.Sites.get_by_domain(site.domain)
+ end
+ end
end
diff --git a/test/plausible_web/controllers/site_controller_test.exs b/test/plausible_web/controllers/site_controller_test.exs
index bb76d271a853..8c66b55e5e8a 100644
--- a/test/plausible_web/controllers/site_controller_test.exs
+++ b/test/plausible_web/controllers/site_controller_test.exs
@@ -362,7 +362,7 @@ defmodule PlausibleWeb.SiteControllerTest do
})
assert redirected_to(conn) ==
- "/#{URI.encode_www_form("éxample.com")}/installation?site_created=true&flow="
+ "/#{URI.encode_www_form("éxample.com")}/installation?flow="
assert site = Repo.get_by(Plausible.Site, domain: "éxample.com")
assert site.timezone == "Europe/London"
@@ -480,7 +480,7 @@ defmodule PlausibleWeb.SiteControllerTest do
}
})
- assert redirected_to(conn) == "/example.com/installation?site_created=true&flow="
+ assert redirected_to(conn) == "/example.com/installation?flow="
assert Repo.get_by(Plausible.Site, domain: "example.com")
end
@@ -501,7 +501,7 @@ defmodule PlausibleWeb.SiteControllerTest do
}
})
- assert redirected_to(conn) == "/example.com/installation?site_created=true&flow="
+ assert redirected_to(conn) == "/example.com/installation?flow="
assert Plausible.Teams.Billing.site_usage(team) == 3
end
@@ -515,7 +515,7 @@ defmodule PlausibleWeb.SiteControllerTest do
}
})
- assert redirected_to(conn) == "/example.com/installation?site_created=true&flow="
+ assert redirected_to(conn) == "/example.com/installation?flow="
assert Repo.get_by(Plausible.Site, domain: "example.com")
end
end
@@ -555,7 +555,7 @@ defmodule PlausibleWeb.SiteControllerTest do
})
assert redirected_to(conn) ==
- "/example.com%2Fsome_blog_site/installation?site_created=true&flow="
+ "/example.com%2Fsome_blog_site/installation?flow="
end
test "renders form again when it is a duplicate domain", %{conn: conn} do
@@ -618,7 +618,7 @@ defmodule PlausibleWeb.SiteControllerTest do
})
assert redirected_to(conn) ==
- "/example.com/installation?site_created=true&flow="
+ "/example.com/installation?flow="
end
for role <- [:owner, :admin, :editor] do
@@ -917,6 +917,46 @@ defmodule PlausibleWeb.SiteControllerTest do
end
end
+ describe "GET /:domain/settings/email-reports" do
+ setup [:create_user, :log_in, :create_site]
+
+ test "renders the page without advancing onboarding_status by default", %{
+ conn: conn,
+ site: site
+ } do
+ site = site |> Ecto.Changeset.change(onboarding_status: :first_pageview) |> Repo.update!()
+
+ conn = get(conn, "/#{site.domain}/settings/email-reports")
+
+ assert html_response(conn, 200) =~ "Weekly email reports"
+ assert Repo.reload!(site).onboarding_status == :first_pageview
+ end
+
+ test "advances onboarding_status to :completed when cta_clicked=true", %{
+ conn: conn,
+ site: site
+ } do
+ site = site |> Ecto.Changeset.change(onboarding_status: :first_pageview) |> Repo.update!()
+
+ conn = get(conn, "/#{site.domain}/settings/email-reports?cta_clicked=true")
+
+ assert html_response(conn, 200)
+ assert Repo.reload!(site).onboarding_status == :completed
+ end
+
+ test "cta_clicked=true does not regress :completed onboarding_status", %{
+ conn: conn,
+ site: site
+ } do
+ site = site |> Ecto.Changeset.change(onboarding_status: :completed) |> Repo.update!()
+
+ conn = get(conn, "/#{site.domain}/settings/email-reports?cta_clicked=true")
+
+ assert html_response(conn, 200)
+ assert Repo.reload!(site).onboarding_status == :completed
+ end
+ end
+
describe "GET /:domain/settings/visibility" do
setup [:create_user, :log_in, :create_site]
diff --git a/test/plausible_web/controllers/stats_controller_test.exs b/test/plausible_web/controllers/stats_controller_test.exs
index 38155954a91d..d6c249a5165f 100644
--- a/test/plausible_web/controllers/stats_controller_test.exs
+++ b/test/plausible_web/controllers/stats_controller_test.exs
@@ -3,6 +3,7 @@ defmodule PlausibleWeb.StatsControllerTest do
use Plausible.Repo
@react_container "div#stats-react-container"
+ @verification_banner "#verification-ui"
describe "GET /:domain - anonymous user" do
test "public site - shows site stats", %{conn: conn} do
@@ -104,28 +105,42 @@ defmodule PlausibleWeb.StatsControllerTest do
assert resp =~ "Getting started"
end
- test "public site - redirect to /login when no stats because verification requires it", %{
- conn: conn
- } do
+ test "public site - shows an empty dashboard without stats (no verification banner)",
+ %{
+ conn: conn
+ } do
new_site(domain: "some-other-public-site.io", public: true)
- conn = get(conn, conn |> get("/some-other-public-site.io") |> redirected_to())
+ resp = get(conn, "/some-other-public-site.io") |> html_response(200)
- assert redirected_to(conn) ==
- Routes.auth_path(conn, :login_form,
- return_to: "/some-other-public-site.io/verification"
- )
+ refute element_exists?(resp, @verification_banner)
end
- test "public site - no stats with skip_to_dashboard", %{
- conn: conn
- } do
+ test "public site - anonymous visitors never see the verification banner, even with the param",
+ %{
+ conn: conn
+ } do
new_site(domain: "some-other-public-site.io", public: true)
- conn = get(conn, "/some-other-public-site.io?skip_to_dashboard=true")
- resp = html_response(conn, 200)
+ resp =
+ get(conn, "/some-other-public-site.io?verify_installation=true") |> html_response(200)
+
+ assert text_of_attr(resp, @react_container, "data-logged-in") == "false"
+ refute element_exists?(resp, @verification_banner)
+ end
+
+ test "public site - anonymous visitors never see the email reports CTA", %{conn: conn} do
+ public_site =
+ new_site(
+ domain: "some-other-public-site.io",
+ public: true,
+ onboarding_status: :first_pageview
+ )
+
+ resp = get(conn, "/#{public_site.domain}") |> html_response(200)
assert text_of_attr(resp, @react_container, "data-logged-in") == "false"
+ assert text_of_attr(resp, @react_container, "data-show-email-reports-cta") == "false"
end
test "can not view stats of a private website", %{conn: conn} do
@@ -147,15 +162,130 @@ defmodule PlausibleWeb.StatsControllerTest do
assert text_of_attr(resp, @react_container, "data-current-user-id") == "#{user.id}"
end
- test "can view stats of a website I've created, enforcing pageviews check skip", %{
+ on_ee do
+ test "verification banner showing in the provisioning flow",
+ %{
+ conn: conn,
+ user: user,
+ site: site
+ } do
+ get_dashboard_resp = fn conn, site, q ->
+ get(conn, "/#{site.domain}#{q}") |> html_response(200)
+ end
+
+ q = "?verify_installation=true&flow=#{PlausibleWeb.Flows.provisioning()}"
+
+ # No `?verify_installation=true` query parameter -> doesn't show
+ resp = get_dashboard_resp.(conn, site, "")
+ refute element_exists?(resp, @verification_banner)
+
+ # site.onboarding_status != :new_site -> doesn't show
+ for status <- [:verification_succeeded, :first_pageview, :completed] do
+ site = new_site(owner: user, onboarding_status: status)
+ resp = get_dashboard_resp.(conn, site, q)
+ refute element_exists?(resp, @verification_banner)
+ end
+
+ # site.onboarding_status != :new_site & flow param not provided -> doesn't show
+ for status <- [:verification_succeeded, :first_pageview, :completed] do
+ site = new_site(owner: user, onboarding_status: status)
+ resp = get_dashboard_resp.(conn, site, "?verify_installation=true")
+ refute element_exists?(resp, @verification_banner)
+ end
+
+ # both conditions met -> shows
+ resp = get_dashboard_resp.(conn, site, q)
+ assert element_exists?(resp, @verification_banner)
+ end
+
+ for flow <- [PlausibleWeb.Flows.review(), PlausibleWeb.Flows.domain_change()] do
+ test "verification banner in #{flow} flow shows when verify_installation query param is present",
+ %{
+ conn: conn,
+ site: site
+ } do
+ site
+ |> Plausible.Site.put_onboarding_status_advance(:completed)
+ |> Plausible.Repo.update!()
+
+ resp =
+ get(conn, "/#{site.domain}?verify_installation=true&flow=#{unquote(flow)}")
+ |> html_response(200)
+
+ assert element_exists?(resp, @verification_banner)
+ end
+ end
+ end
+
+ test "shows email reports CTA when onboarding_status is :first_pageview", %{
conn: conn,
- site: site
+ user: user
} do
- resp = conn |> get(conn |> get("/" <> site.domain) |> redirected_to()) |> html_response(200)
- refute text_of_attr(resp, @react_container, "data-logged-in") == "true"
+ site = new_site(owner: user, onboarding_status: :first_pageview)
- resp = conn |> get("/" <> site.domain <> "?skip_to_dashboard=true") |> html_response(200)
- assert text_of_attr(resp, @react_container, "data-logged-in") == "true"
+ resp = get(conn, "/#{site.domain}") |> html_response(200)
+
+ assert text_of_attr(resp, @react_container, "data-show-email-reports-cta") == "true"
+ end
+
+ test "shows email reports CTA on the very first load that discovers a pageview, without needing a second refresh",
+ %{conn: conn, user: user} do
+ site = new_site(owner: user, onboarding_status: :verification_succeeded)
+ populate_stats(site, [build(:pageview)])
+
+ assert Repo.reload!(site).onboarding_status == :verification_succeeded
+
+ resp = get(conn, "/#{site.domain}") |> html_response(200)
+
+ assert text_of_attr(resp, @react_container, "data-show-email-reports-cta") == "true"
+ assert Repo.reload!(site).onboarding_status == :first_pageview
+ end
+
+ for status <- [:new_site, :verification_succeeded, :completed] do
+ test "does not show email reports CTA when onboarding_status is #{status}", %{
+ conn: conn,
+ user: user
+ } do
+ site = new_site(owner: user, onboarding_status: unquote(status))
+
+ resp = get(conn, "/#{site.domain}") |> html_response(200)
+
+ assert text_of_attr(resp, @react_container, "data-show-email-reports-cta") == "false"
+ end
+ end
+
+ test "does not show email reports CTA for a viewer, since they can't reach the settings page it links to",
+ %{conn: conn, user: user} do
+ site = new_site(onboarding_status: :first_pageview)
+ add_guest(site, user: user, role: :viewer)
+
+ resp = get(conn, "/#{site.domain}") |> html_response(200)
+
+ assert text_of_attr(resp, @react_container, "data-current-user-role") == "viewer"
+ assert text_of_attr(resp, @react_container, "data-show-email-reports-cta") == "false"
+ end
+
+ on_ee do
+ test "does not show email reports CTA for consolidated views", %{
+ conn: conn,
+ user: user
+ } do
+ new_site(owner: user)
+ new_site(owner: user)
+ cv = user |> team_of() |> new_consolidated_view()
+
+ # `onboarding_status` should always be :completed for
+ # consolidated views anyway but this test makes sure that
+ # stats_controller explicitly excludes email reports CTA
+ # for consolidated views too.
+ cv
+ |> Ecto.Changeset.change(%{onboarding_status: :first_pageview})
+ |> Plausible.Repo.update!()
+
+ resp = get(conn, "/#{cv.domain}") |> html_response(200)
+
+ assert text_of_attr(resp, @react_container, "data-show-email-reports-cta") == "false"
+ end
end
on_ee do
@@ -229,21 +359,22 @@ defmodule PlausibleWeb.StatsControllerTest do
assert cv.native_stats_start_at == twenty_days_ago
end
- test "does not redirect consolidated views to verification", %{
- conn: conn,
- user: user
- } do
+ test "does not show verification banner for consolidated views even with the explicit param",
+ %{
+ conn: conn,
+ user: user
+ } do
new_site(owner: user)
new_site(owner: user)
cv = user |> team_of() |> new_consolidated_view()
- conn = get(conn, "/" <> cv.domain)
- resp = html_response(conn, 200)
+ resp = get(conn, "/#{cv.domain}?verify_installation=true") |> html_response(200)
assert text_of_attr(resp, @react_container, "data-domain") == cv.domain
assert text_of_attr(resp, @react_container, "data-logged-in") == "true"
assert text_of_attr(resp, @react_container, "data-current-user-role") == "owner"
assert text_of_attr(resp, @react_container, "data-current-user-id") == "#{user.id}"
+ refute element_exists?(resp, @verification_banner)
end
test "redirects to /sites if for some reason ineligible anymore", %{
@@ -334,9 +465,9 @@ defmodule PlausibleWeb.StatsControllerTest do
end
test "does not show CRM link to the site", %{conn: conn, site: site} do
- conn = get(conn, conn |> get("/" <> site.domain) |> redirected_to())
+ resp = get(conn, "/" <> site.domain) |> html_response(200)
- refute html_response(conn, 200) =~ "/cs/sites"
+ refute resp =~ "/cs/sites"
end
test "all segments (personal or site) are stuffed into dataset, with their associated owner_id and owner_name",
@@ -387,11 +518,21 @@ defmodule PlausibleWeb.StatsControllerTest do
assert text_of_attr(resp, @react_container, "data-current-user-id") == "#{user.id}"
end
- test "can enter verification when site is without stats", %{conn: conn} do
- site = new_site()
+ test "can enter verification regardless of whether the site has stats or not", %{conn: conn} do
+ site_without_stats = new_site()
+ site_with_stats = new_site()
+ populate_stats(site_with_stats, [build(:pageview)])
+
+ for site <- [site_without_stats, site_with_stats] do
+ resp =
+ get(
+ conn,
+ "/#{site.domain}?verify_installation=true&flow=#{PlausibleWeb.Flows.review()}"
+ )
+ |> html_response(200)
- conn = get(conn, conn |> get("/" <> site.domain) |> redirected_to())
- assert html_response(conn, 200) =~ "Verifying your installation"
+ assert element_exists?(resp, @verification_banner)
+ end
end
test "can view a private locked dashboard with stats", %{conn: conn} do
@@ -405,13 +546,25 @@ defmodule PlausibleWeb.StatsControllerTest do
assert resp =~ "This dashboard is actually locked"
end
- test "can view private locked verification without stats", %{conn: conn} do
- user = new_user()
- site = new_site(owner: user)
- site.team |> Ecto.Changeset.change(locked: true) |> Repo.update!()
+ test "can trigger verification on a locked private dashboard regardless of whether the site has stats or not",
+ %{conn: conn} do
+ site_without_stats = new_site(owner: new_user())
+ site_without_stats.team |> Ecto.Changeset.change(locked: true) |> Repo.update!()
+
+ site_with_stats = new_site(owner: new_user())
+ populate_stats(site_with_stats, [build(:pageview)])
+ site_with_stats.team |> Ecto.Changeset.change(locked: true) |> Repo.update!()
+
+ for site <- [site_without_stats, site_with_stats] do
+ resp =
+ get(
+ conn,
+ "/#{site.domain}?verify_installation=true&flow=#{PlausibleWeb.Flows.review()}"
+ )
+ |> html_response(200)
- conn = get(conn, conn |> get("/#{site.domain}") |> redirected_to())
- assert html_response(conn, 200) =~ "Verifying your installation"
+ assert element_exists?(resp, @verification_banner)
+ end
end
test "can view a locked public dashboard", %{conn: conn} do
@@ -424,12 +577,34 @@ defmodule PlausibleWeb.StatsControllerTest do
assert resp =~ "This dashboard is actually locked"
end
+ test "does not show email reports CTA when viewing as a super admin without site membership",
+ %{conn: conn} do
+ site = new_site(onboarding_status: :first_pageview)
+
+ conn = get(conn, "/" <> site.domain)
+ resp = html_response(conn, 200)
+
+ assert text_of_attr(resp, @react_container, "data-current-user-role") == "super_admin"
+ assert text_of_attr(resp, @react_container, "data-show-email-reports-cta") == "false"
+ end
+
+ test "still shows email reports CTA for a super admin who is also a real site member",
+ %{conn: conn, user: user} do
+ site = new_site(owner: user, onboarding_status: :first_pageview)
+
+ conn = get(conn, "/" <> site.domain)
+ resp = html_response(conn, 200)
+
+ assert text_of_attr(resp, @react_container, "data-current-user-role") == "owner"
+ assert text_of_attr(resp, @react_container, "data-show-email-reports-cta") == "true"
+ end
+
on_ee do
test "shows CRM link to the site", %{conn: conn} do
site = new_site()
- conn = get(conn, conn |> get("/" <> site.domain) |> redirected_to())
+ resp = get(conn, "/" <> site.domain) |> html_response(200)
- assert html_response(conn, 200) =~
+ assert resp =~
Routes.customer_support_site_path(PlausibleWeb.Endpoint, :show, site.id)
end
end
@@ -465,6 +640,18 @@ defmodule PlausibleWeb.StatsControllerTest do
assert text_of_attr(resp, @react_container, "data-current-user-role") == "public"
end
+ test "never shows the email reports CTA, regardless of the site's onboarding_status", %{
+ conn: conn
+ } do
+ site = new_site(onboarding_status: :first_pageview)
+ link = insert(:shared_link, site: site)
+
+ conn = get(conn, "/share/#{site.domain}/?auth=#{link.slug}")
+ resp = html_response(conn, 200)
+
+ assert text_of_attr(resp, @react_container, "data-show-email-reports-cta") == "false"
+ end
+
test "if the shared link is limited to a segment, only that segment is stuffed into data-segments",
%{
conn: conn
diff --git a/test/plausible_web/live/components/verification_banner_test.exs b/test/plausible_web/live/components/verification_banner_test.exs
new file mode 100644
index 000000000000..22d638c9d794
--- /dev/null
+++ b/test/plausible_web/live/components/verification_banner_test.exs
@@ -0,0 +1,244 @@
+defmodule PlausibleWeb.Live.Components.VerificationBannerTest do
+ use PlausibleWeb.ConnCase, async: true
+
+ on_ee do
+ import Phoenix.LiveViewTest, only: [render_component: 2]
+
+ alias Plausible.InstallationSupport.{State, Verification}
+
+ @moduletag :capture_log
+
+ @component PlausibleWeb.Live.Components.VerificationBanner
+ @banner "#verification-ui"
+ @progress ~s|#verification-ui p#progress|
+
+ @loading_spinner ~s|#{@banner} svg.animate-spin|
+ @check_circle ~s|#{@banner} #check-circle|
+ @recommendations ~s|#recommendation|
+ @super_admin_report ~s|#super-admin-report|
+
+ test "renders initial state" do
+ html = render_component(@component, domain: "example.com")
+ assert element_exists?(html, @progress)
+
+ assert text_of_element(html, @progress) ==
+ "We're visiting your site to ensure that everything is working..."
+
+ assert element_exists?(html, @loading_spinner)
+ refute element_exists?(html, @recommendations)
+ refute element_exists?(html, @check_circle)
+ refute element_exists?(html, @super_admin_report)
+ end
+
+ test "renders failed state without progress spinner" do
+ html = render_component(@component, domain: "example.com", success?: false, finished?: true)
+ refute element_exists?(html, @loading_spinner)
+ refute element_exists?(html, @check_circle)
+ refute element_exists?(html, @recommendations)
+ assert text_of_element(html, @banner) =~ "We couldn't verify your installation"
+ end
+
+ test "renders diagnostic interpretation with inline verify link and standalone review-installation sentence" do
+ interpretation =
+ Verification.Checks.interpret_diagnostics(%State{
+ url: "https://example.com",
+ data_domain: "example.com",
+ diagnostics: %Verification.Diagnostics{service_error: %{code: :domain_not_found}}
+ })
+
+ html =
+ render_component(@component,
+ domain: "example.com",
+ success?: false,
+ finished?: true,
+ interpretation: interpretation
+ )
+
+ assert [recommendation] = html |> find(@recommendations) |> Enum.map(&text/1)
+ assert recommendation =~ "Check that the URL is correct and publicly accessible"
+ assert recommendation =~ "verify your installation manually"
+ refute recommendation =~ "review your installation"
+ assert recommendation =~ "See your installation instructions again here"
+
+ assert element_exists?(
+ html,
+ ~s|#recommendation a[href="https://plausible.io/docs/troubleshoot-integration#how-to-manually-check-your-integration"]|
+ )
+
+ assert element_exists?(
+ html,
+ ~s|#recommendation a[href="/example.com/installation?flow="]|
+ )
+
+ refute element_exists?(html, @super_admin_report)
+ end
+
+ test "renders inline verify-manually link when the recommendation mentions it (no custom URL retry)" do
+ interpretation =
+ Verification.Checks.interpret_diagnostics(%State{
+ url: "https://example.com",
+ data_domain: "example.com",
+ diagnostics: %Verification.Diagnostics{
+ plausible_is_on_window: false,
+ selected_installation_type: "manual"
+ }
+ })
+
+ refute Map.get(interpretation.data || %{}, :offer_custom_url_input) == true
+
+ html =
+ render_component(@component,
+ domain: "example.com",
+ success?: false,
+ finished?: true,
+ interpretation: interpretation
+ )
+
+ assert [recommendation] = html |> find(@recommendations) |> Enum.map(&text/1)
+ assert recommendation =~ "Make sure you've copied the snippet"
+ assert recommendation =~ "verify your installation manually"
+ refute recommendation =~ "review your installation"
+ refute recommendation =~ "Learn more"
+ refute recommendation =~ "See your installation instructions again here"
+
+ assert element_exists?(
+ html,
+ ~s|#recommendation a[href="https://plausible.io/docs/troubleshoot-integration#how-to-manually-check-your-integration"]|
+ )
+
+ refute element_exists?(html, ~s|#recommendation a[href^="/example.com/installation"]|)
+ end
+
+ test "renders super-admin report" do
+ state = %State{
+ url: "https://example.com",
+ data_domain: "example.com",
+ diagnostics: %Verification.Diagnostics{}
+ }
+
+ interpretation = Verification.Checks.interpret_diagnostics(state)
+
+ html =
+ render_component(@component,
+ domain: "example.com",
+ success?: false,
+ finished?: true,
+ interpretation: interpretation,
+ verification_state: state,
+ super_admin?: true
+ )
+
+ assert element_exists?(html, @super_admin_report)
+ assert text_of_element(html, @super_admin_report) =~ "Plausible is on window: nil"
+ end
+
+ test "hides pulsating circle when finished, shows check circle" do
+ html =
+ render_component(@component,
+ domain: "example.com",
+ success?: true,
+ finished?: true
+ )
+
+ refute element_exists?(html, @loading_spinner)
+ assert element_exists?(html, @check_circle)
+ end
+
+ test "renders a progress message" do
+ html = render_component(@component, domain: "example.com", message: "Arbitrary message")
+
+ assert text_of_element(html, @progress) == "Arbitrary message..."
+ end
+
+ test "renders contact link on >=3 attempts" do
+ html = render_component(@component, domain: "example.com", attempts: 2, finished?: true)
+ refute html =~ "Need help?"
+ refute element_exists?(html, ~s|a[href="https://plausible.io/contact"]|)
+
+ html = render_component(@component, domain: "example.com", attempts: 3, finished?: true)
+ assert html =~ "Need help?"
+ assert element_exists?(html, ~s|a[href="https://plausible.io/contact"]|)
+ end
+
+ test "renders a Try another URL ghost button when a custom URL retry is offered" do
+ interpretation =
+ Verification.Checks.interpret_diagnostics(%State{
+ url: "example.com",
+ diagnostics: %Verification.Diagnostics{
+ plausible_is_on_window: false,
+ plausible_is_initialized: false,
+ service_error: %{code: :domain_not_found}
+ }
+ })
+
+ assert interpretation.data.offer_custom_url_input == true
+
+ html =
+ render_component(@component,
+ domain: "example.com",
+ finished?: true,
+ success?: false,
+ interpretation: interpretation
+ )
+
+ assert text_of_element(html, "#verify-custom-url-link") =~ "Try another URL"
+ assert element_exists?(html, ~s|a#verify-custom-url-link[phx-click="show-custom-url-form"]|)
+ refute html =~ "Review installation"
+ end
+
+ test "renders the custom URL input inline, replacing Check again with the Verify URL submit button, and hides the secondary action" do
+ interpretation =
+ Verification.Checks.interpret_diagnostics(%State{
+ url: "example.com",
+ diagnostics: %Verification.Diagnostics{
+ plausible_is_on_window: false,
+ plausible_is_initialized: false,
+ service_error: %{code: :domain_not_found}
+ }
+ })
+
+ html =
+ render_component(@component,
+ domain: "example.com",
+ finished?: true,
+ success?: false,
+ interpretation: interpretation,
+ custom_url_input?: true
+ )
+
+ refute element_exists?(html, "#verify-custom-url-link")
+ refute element_exists?(html, ~s|a[phx-click="retry"]|)
+ refute html =~ "Review installation"
+
+ assert text_of_element(html, ~s|form[phx-submit="verify-custom-url"] button[type="submit"]|) =~
+ "Verify URL"
+
+ assert element_exists?(
+ html,
+ ~s|form[phx-submit="verify-custom-url"] input[name="custom_url"]|
+ )
+
+ assert text_of_attr(html, ~s|form[phx-submit="verify-custom-url"] input|, "value") =~
+ "https://example.com"
+ end
+
+ test "offers a Review installation ghost button on failure by default" do
+ html =
+ render_component(@component,
+ domain: "example.com",
+ success?: false,
+ finished?: true,
+ flow: PlausibleWeb.Flows.review()
+ )
+
+ refute element_exists?(html, ~s|a[href="/example.com/settings/general"]|)
+
+ assert element_exists?(
+ html,
+ ~s|a[href="/example.com/installation?flow=review"]|
+ )
+
+ assert html =~ "Review installation"
+ end
+ end
+end
diff --git a/test/plausible_web/live/components/verification_test.exs b/test/plausible_web/live/components/verification_test.exs
deleted file mode 100644
index 2645de32cc4e..000000000000
--- a/test/plausible_web/live/components/verification_test.exs
+++ /dev/null
@@ -1,162 +0,0 @@
-defmodule PlausibleWeb.Live.Components.VerificationTest do
- use PlausibleWeb.ConnCase, async: true
-
- on_ee do
- import Phoenix.LiveViewTest, only: [render_component: 2]
-
- alias Plausible.InstallationSupport.{State, Verification}
-
- @moduletag :capture_log
-
- @component PlausibleWeb.Live.Components.Verification
- @progress ~s|#verification-ui p#progress|
-
- @pulsating_circle ~s|div#verification-ui div.pulsating-circle|
- @check_circle ~s|div#verification-ui #check-circle|
- @error_circle ~s|div#verification-ui #error-circle|
- @recommendations ~s|#recommendation|
- @super_admin_report ~s|#super-admin-report|
-
- test "renders initial state" do
- html = render_component(@component, domain: "example.com")
- assert element_exists?(html, @progress)
-
- assert text_of_element(html, @progress) ==
- "We're visiting your site to ensure that everything is working"
-
- assert element_exists?(html, @pulsating_circle)
- refute class_of_element(html, @pulsating_circle) =~ "hidden"
- refute element_exists?(html, @recommendations)
- refute element_exists?(html, @check_circle)
- refute element_exists?(html, @super_admin_report)
- end
-
- test "renders error badge on error" do
- html = render_component(@component, domain: "example.com", success?: false, finished?: true)
- refute element_exists?(html, @pulsating_circle)
- refute element_exists?(html, @check_circle)
- refute element_exists?(html, @recommendations)
- assert element_exists?(html, @error_circle)
- end
-
- test "renders diagnostic interpretation" do
- interpretation =
- Verification.Checks.interpret_diagnostics(%State{
- url: "https://example.com",
- data_domain: "example.com",
- diagnostics: %Verification.Diagnostics{service_error: %{code: :domain_not_found}}
- })
-
- html =
- render_component(@component,
- domain: "example.com",
- success?: false,
- finished?: true,
- interpretation: interpretation
- )
-
- assert [recommendation] = html |> find(@recommendations) |> Enum.map(&text/1)
- assert recommendation =~ "check that the domain you entered is correct"
-
- refute element_exists?(html, @super_admin_report)
- end
-
- test "renders super-admin report" do
- state = %State{
- url: "https://example.com",
- data_domain: "example.com",
- diagnostics: %Verification.Diagnostics{}
- }
-
- interpretation = Verification.Checks.interpret_diagnostics(state)
-
- html =
- render_component(@component,
- domain: "example.com",
- success?: false,
- finished?: true,
- interpretation: interpretation,
- verification_state: state,
- super_admin?: true
- )
-
- assert element_exists?(html, @super_admin_report)
- assert text_of_element(html, @super_admin_report) =~ "Plausible is on window: nil"
- end
-
- test "hides pulsating circle when finished, shows check circle" do
- html =
- render_component(@component,
- domain: "example.com",
- success?: true,
- finished?: true
- )
-
- refute element_exists?(html, @pulsating_circle)
- assert element_exists?(html, @check_circle)
- end
-
- test "renders a progress message" do
- html = render_component(@component, domain: "example.com", message: "Arbitrary message")
-
- assert text_of_element(html, @progress) == "Arbitrary message"
- end
-
- test "renders contact link on >3 attempts" do
- html = render_component(@component, domain: "example.com", attempts: 2, finished?: true)
- refute html =~ "Need further help with your installation?"
- refute element_exists?(html, ~s|a[href="https://plausible.io/contact"]|)
-
- html = render_component(@component, domain: "example.com", attempts: 3, finished?: true)
- assert html =~ "Need further help with your installation?"
- assert element_exists?(html, ~s|a[href="https://plausible.io/contact"]|)
- end
-
- test "renders link to verify installation at a different URL" do
- interpretation =
- Verification.Checks.interpret_diagnostics(%State{
- url: "example.com",
- diagnostics: %Verification.Diagnostics{
- plausible_is_on_window: false,
- plausible_is_initialized: false,
- service_error: %{code: :domain_not_found}
- }
- })
-
- assert interpretation.data.offer_custom_url_input == true
-
- expected_link_href =
- PlausibleWeb.Router.Helpers.site_path(PlausibleWeb.Endpoint, :verification, "example.com")
-
- html =
- render_component(@component,
- domain: "example.com",
- finished?: true,
- success?: false,
- interpretation: interpretation
- )
-
- assert text_of_element(html, "#verify-custom-url-link") =~ "different URL?"
- assert text_of_attr(html, "#verify-custom-url-link a", "href") =~ expected_link_href
- assert text_of_attr(html, "#verify-custom-url-link a", "href") =~ "custom_url=true"
- end
-
- test "offers escape paths: settings and installation instructions on failure" do
- html =
- render_component(@component,
- domain: "example.com",
- success?: false,
- finished?: true,
- installation_type: "wordpress",
- flow: PlausibleWeb.Flows.review()
- )
-
- assert element_exists?(html, ~s|a[href="/example.com/settings/general"]|)
-
- assert element_exists?(
- html,
- ~s|a[href="/example.com/installation?flow=review&installation_type=wordpress"]|
- )
- end
- end
-end
diff --git a/test/plausible_web/live/installation_test.exs b/test/plausible_web/live/installation_test.exs
index 84e379b321fa..794b6ec09f4d 100644
--- a/test/plausible_web/live/installation_test.exs
+++ b/test/plausible_web/live/installation_test.exs
@@ -8,6 +8,20 @@ defmodule PlausibleWeb.Live.InstallationTest do
@migration_guide_link "https://plausible.io/docs/script-update-guide"
+ on_ee do
+ @manual_button_text "Verify Script installation"
+ @wordpress_button_text "Verify WordPress installation"
+ @gtm_button_text "Verify Tag Manager installation"
+ @npm_button_text "Verify NPM installation"
+ else
+ @shared_button_text "Proceed to dashboard"
+
+ @manual_button_text @shared_button_text
+ @wordpress_button_text @shared_button_text
+ @gtm_button_text @shared_button_text
+ @npm_button_text @shared_button_text
+ end
+
setup [:create_user, :log_in, :create_site]
describe "GET /:domain/installation" do
@@ -43,7 +57,7 @@ defmodule PlausibleWeb.Live.InstallationTest do
{lv, _} = get_lv(conn, site)
html = render_async(lv, 500)
- assert text(html) =~ "Verify WordPress installation"
+ assert text(html) =~ @wordpress_button_text
end
@tag :ee_only
@@ -57,7 +71,7 @@ defmodule PlausibleWeb.Live.InstallationTest do
{lv, _} = get_lv(conn, site, "?type=wordpress")
html = render_async(lv, 500)
- assert text(html) =~ "Verify WordPress installation"
+ assert text(html) =~ @wordpress_button_text
end
@tag :ee_only
@@ -71,7 +85,7 @@ defmodule PlausibleWeb.Live.InstallationTest do
{lv, _} = get_lv(conn, site, "?type=gtm")
html = render_async(lv, 500)
- assert text(html) =~ "Verify Tag Manager installation"
+ assert text(html) =~ @gtm_button_text
end
@tag :ee_only
@@ -85,7 +99,7 @@ defmodule PlausibleWeb.Live.InstallationTest do
{lv, _} = get_lv(conn, site, "?type=npm")
html = render_async(lv, 500)
- assert text(html) =~ "Verify NPM installation"
+ assert text(html) =~ @npm_button_text
end
@tag :ee_only
@@ -99,54 +113,64 @@ defmodule PlausibleWeb.Live.InstallationTest do
{lv, _} = get_lv(conn, site, "?type=manual")
html = render_async(lv, 500)
- assert text(html) =~ "Verify Script installation"
+ assert text(html) =~ @manual_button_text
end
- @tag :ee_only
- test "allows switching between installation tabs (EE)", %{conn: conn, site: site} do
- stub_dns()
- stub_detection_manual()
+ on_ee do
+ test "allows switching between installation tabs (EE)", %{conn: conn, site: site} do
+ stub_dns()
+ stub_detection_manual()
- {lv, _html} = get_lv(conn, site, "?type=manual")
+ {lv, _html} = get_lv(conn, site, "?type=manual")
- html = render_async(lv, 500)
- assert html =~ "Verify Script installation"
+ html = render_async(lv, 500)
+ assert html =~ @manual_button_text
- lv
- |> element("a[href*=\"type=wordpress\"]")
- |> render_click()
+ lv
+ |> element("a[href*=\"type=wordpress\"]")
+ |> render_click()
- html = render(lv)
- assert html =~ "Verify WordPress installation"
+ html = render(lv)
+ assert html =~ @wordpress_button_text
- lv
- |> element("a[href*=\"type=gtm\"]")
- |> render_click()
+ lv
+ |> element("a[href*=\"type=gtm\"]")
+ |> render_click()
- html = render(lv)
- assert html =~ "Verify Tag Manager installation"
+ html = render(lv)
+ assert html =~ @gtm_button_text
- lv
- |> element("a[href*=\"type=npm\"]")
- |> render_click()
+ lv
+ |> element("a[href*=\"type=npm\"]")
+ |> render_click()
- html = render(lv)
- assert html =~ "Verify NPM installation"
- end
+ html = render(lv)
+ assert html =~ @npm_button_text
+ end
+ else
+ test "allows switching between installation tabs (CE)", %{conn: conn, site: site} do
+ {lv, _html} = get_lv(conn, site)
- @tag :ce_build_only
- test "allows switching between installation tabs (CE)", %{conn: conn, site: site} do
- {lv, _html} = get_lv(conn, site)
+ html = render_async(lv, 500)
+ assert html =~ "window.plausible"
+ assert html =~ @shared_button_text
- html = render_async(lv, 500)
- assert html =~ "Verify Script installation"
+ lv
+ |> element("a[href*=\"type=wordpress\"]")
+ |> render_click()
- lv
- |> element("a[href*=\"type=wordpress\"]")
- |> render_click()
+ html = render(lv)
+ assert html =~ "https://plausible.io/wordpress-analytics-plugin"
+ assert html =~ @shared_button_text
- html = render(lv)
- assert html =~ "Verify WordPress installation"
+ lv
+ |> element("a[href*=\"type=npm\"]")
+ |> render_click()
+
+ html = render(lv)
+ assert html =~ "@plausible-analytics/tracker"
+ assert html =~ @shared_button_text
+ end
end
test "manual installations has script snippet with expected ID", %{conn: conn, site: site} do
@@ -159,7 +183,7 @@ defmodule PlausibleWeb.Live.InstallationTest do
assert eventually(fn ->
html = render(lv)
- {html =~ "Verify Script installation", html}
+ {html =~ @manual_button_text, html}
end)
html = render(lv)
@@ -178,7 +202,7 @@ defmodule PlausibleWeb.Live.InstallationTest do
{lv, _html} = get_lv(conn, site, "?type=manual&flow=review")
html = render_async(lv, 500)
- assert html =~ "Verify Script installation"
+ assert html =~ @manual_button_text
assert html =~ "Optional measurements"
assert html =~ "Outbound links"
assert html =~ "File downloads"
@@ -194,7 +218,7 @@ defmodule PlausibleWeb.Live.InstallationTest do
{lv, _html} = get_lv(conn, site, "?type=manual&flow=review")
html = render_async(lv, 500)
- assert html =~ "Verify Script installation"
+ assert html =~ @manual_button_text
assert html =~ "Advanced options"
assert html =~ "Manual tagging"
assert html =~ "404 error pages"
@@ -215,7 +239,7 @@ defmodule PlausibleWeb.Live.InstallationTest do
{lv, _html} = get_lv(conn, site, "?type=manual&flow=review")
html = render_async(lv, 500)
- assert html =~ "Verify Script installation"
+ assert html =~ @manual_button_text
config = TrackerScriptConfiguration |> Plausible.Repo.get_by!(site_id: site.id)
assert config.outbound_links == true
@@ -241,16 +265,18 @@ defmodule PlausibleWeb.Live.InstallationTest do
on_ee do
for {type, expected_text} <- [
- {"manual", "Verify Script installation"},
- {"wordpress", "Verify WordPress installation"},
- {"gtm", "Verify Tag Manager installation"},
- {"npm", "Verify NPM installation"}
+ {"manual", @manual_button_text},
+ {"wordpress", @wordpress_button_text},
+ {"gtm", @gtm_button_text},
+ {"npm", @npm_button_text}
] do
- test "submitting form with #{type} redirects to verification (EE)", %{
- conn: conn,
- site: site
- } do
+ test "submitting form with #{type} redirects to the dashboard with the verification banner (EE)",
+ %{
+ conn: conn,
+ site: site
+ } do
stub_dns()
+
stub_detection_manual()
{lv, _html} = get_lv(conn, site, "?type=#{unquote(type)}")
@@ -270,9 +296,9 @@ defmodule PlausibleWeb.Live.InstallationTest do
assert_redirect(
lv,
- Routes.site_path(conn, :verification, site.domain,
- flow: "provisioning",
- installation_type: unquote(type)
+ Routes.stats_path(conn, :stats, site.domain,
+ verify_installation: true,
+ flow: "provisioning"
)
)
end
@@ -280,7 +306,10 @@ defmodule PlausibleWeb.Live.InstallationTest do
end
@tag :ce_build_only
- test "submitting the form redirects to verification (CE)", %{conn: conn, site: site} do
+ test "submitting the form redirects straight to the dashboard, no banner (CE)", %{
+ conn: conn,
+ site: site
+ } do
{lv, _html} = get_lv(conn, site)
lv
@@ -294,13 +323,7 @@ defmodule PlausibleWeb.Live.InstallationTest do
}
})
- assert_redirect(
- lv,
- Routes.site_path(conn, :verification, site.domain,
- flow: "provisioning",
- installation_type: "manual"
- )
- )
+ assert_redirect(lv, Routes.stats_path(conn, :stats, site.domain))
end
test "404 goal gets created regardless of user options", %{conn: conn, site: site} do
@@ -312,7 +335,7 @@ defmodule PlausibleWeb.Live.InstallationTest do
{lv, _html} = get_lv(conn, site, "?type=manual")
html = render_async(lv, 500)
- assert html =~ "Verify Script installation"
+ assert html =~ @manual_button_text
# Test with all options disabled
lv
@@ -331,10 +354,11 @@ defmodule PlausibleWeb.Live.InstallationTest do
assert Enum.any?(goals, &(&1.event_name == "404"))
end
- test "submitting form with review flow redirects to verification with flow param", %{
- conn: conn,
- site: site
- } do
+ test "submitting form with review flow redirects to the dashboard with the flow param preserved",
+ %{
+ conn: conn,
+ site: site
+ } do
on_ee do
stub_dns()
stub_detection_manual()
@@ -343,7 +367,7 @@ defmodule PlausibleWeb.Live.InstallationTest do
{lv, _html} = get_lv(conn, site, "?type=manual&flow=review")
html = render_async(lv, 500)
- assert html =~ "Verify Script installation"
+ assert html =~ @manual_button_text
lv
|> element("form[phx-submit='submit']")
@@ -356,13 +380,19 @@ defmodule PlausibleWeb.Live.InstallationTest do
}
})
- assert_redirect(
- lv,
- Routes.site_path(conn, :verification, site.domain,
- flow: "review",
- installation_type: "manual"
+ on_ee do
+ assert_redirect(
+ lv,
+ Routes.stats_path(conn, :stats, site.domain,
+ verify_installation: true,
+ flow: "review"
+ )
)
- )
+ end
+
+ on_ce do
+ assert_redirect(lv, Routes.stats_path(conn, :stats, site.domain))
+ end
end
@tag :ee_only
@@ -398,7 +428,7 @@ defmodule PlausibleWeb.Live.InstallationTest do
html = render_async(lv, 500)
refute text(html) =~ "We've detected your website is using WordPress"
- assert text(html) =~ "Verify Script installation"
+ assert text(html) =~ @manual_button_text
end
@tag :ee_only
@@ -409,7 +439,7 @@ defmodule PlausibleWeb.Live.InstallationTest do
{lv, _} = get_lv(conn, site)
html = render_async(lv, 500)
- assert html =~ "Verify Tag Manager installation"
+ assert html =~ @gtm_button_text
assert text(html) =~ "We've detected your website is using Google Tag Manager"
end
@@ -429,7 +459,7 @@ defmodule PlausibleWeb.Live.InstallationTest do
{lv, _} = get_lv(conn, site)
html = render_async(lv, 500)
- assert html =~ "Verify NPM installation"
+ assert html =~ @npm_button_text
end
@tag :ee_only
@@ -468,7 +498,7 @@ defmodule PlausibleWeb.Live.InstallationTest do
{lv, _} = get_lv(conn, site, "?type=wordpress")
html = render_async(lv, 500)
- assert html =~ "Verify WordPress installation"
+ assert html =~ @wordpress_button_text
refute element_exists?(html, "a[href='#{@migration_guide_link}']")
end
@@ -485,7 +515,7 @@ defmodule PlausibleWeb.Live.InstallationTest do
assert eventually(fn ->
html = render(lv)
# Should default to manual installation when detection returns {:error, _}
- {html =~ "Verify Script installation", html}
+ {html =~ @manual_button_text, html}
end)
end)
end
@@ -503,7 +533,7 @@ defmodule PlausibleWeb.Live.InstallationTest do
html = render_async(lv, 500)
# Should default to manual installation when detection returns {:error, _}
- assert html =~ "Verify Script installation"
+ assert html =~ @manual_button_text
end)
end
end
@@ -530,7 +560,7 @@ defmodule PlausibleWeb.Live.InstallationTest do
{lv, _} = get_lv(conn, site)
html = render_async(lv, 500)
- assert html =~ "Verify Script installation"
+ assert html =~ @manual_button_text
end
test "allows editor access to installation page", %{conn: conn, user: user} do
@@ -545,7 +575,7 @@ defmodule PlausibleWeb.Live.InstallationTest do
{lv, _} = get_lv(conn, site)
html = render_async(lv, 500)
- assert html =~ "Verify Script installation"
+ assert html =~ @manual_button_text
end
end
@@ -563,7 +593,7 @@ defmodule PlausibleWeb.Live.InstallationTest do
{lv, _} = get_lv(conn, site, "?type=invalid")
html = render_async(lv, 500)
- assert html =~ "Verify Script installation"
+ assert html =~ @manual_button_text
end
test "falls back to provisioning flow when invalid flow parameter supplied", %{
@@ -578,7 +608,7 @@ defmodule PlausibleWeb.Live.InstallationTest do
{lv, _} = get_lv(conn, site, "?flow=invalid")
html = render_async(lv, 500)
- assert html =~ "Verify Script installation"
+ assert html =~ @manual_button_text
end
end
@@ -598,7 +628,7 @@ defmodule PlausibleWeb.Live.InstallationTest do
{lv, _} = get_lv(conn, site)
html = render_async(lv, 500)
- assert html =~ "Verify Tag Manager installation"
+ assert html =~ @gtm_button_text
end
end
@@ -622,7 +652,7 @@ defmodule PlausibleWeb.Live.InstallationTest do
{lv, _} = get_lv(conn, site, "?flow=review")
html = render_async(lv, 500)
- assert html =~ "Verify WordPress installation"
+ assert html =~ @wordpress_button_text
end
end
diff --git a/test/plausible_web/live/sites_test.exs b/test/plausible_web/live/sites_test.exs
index add5590f8443..e78e9fbd5834 100644
--- a/test/plausible_web/live/sites_test.exs
+++ b/test/plausible_web/live/sites_test.exs
@@ -152,6 +152,71 @@ defmodule PlausibleWeb.Live.SitesTest do
end
on_ee do
+ describe "pending setup badge and verification query parameter" do
+ @tag :ee_only
+ test "shows for a site with onboarding_status :new_site", %{conn: conn, user: user} do
+ site = new_site(owner: user, onboarding_status: :new_site)
+
+ {:ok, _lv, html} = live(conn, "/sites")
+
+ site_card = text_of_element(html, "li[data-domain=\"#{site.domain}\"]")
+ assert site_card =~ "Setup pending"
+
+ dashboard_link_href = text_of_attr(html, "li[data-domain=\"#{site.domain}\"] > a", "href")
+ assert dashboard_link_href =~ "verify_installation=true"
+
+ assert Repo.reload!(site).onboarding_status == :new_site
+ end
+
+ @tag :ee_only
+ test "advances onboarding_status (and hides the badge) when the site already has visitors, before its dashboard has ever been loaded",
+ %{conn: conn, user: user} do
+ site = new_site(owner: user, onboarding_status: :new_site)
+ populate_stats(site, [build(:pageview)])
+
+ {:ok, _lv, html} = live(conn, "/sites")
+
+ site_card = text_of_element(html, "li[data-domain=\"#{site.domain}\"]")
+ refute site_card =~ "Setup pending"
+
+ dashboard_link_href = text_of_attr(html, "li[data-domain=\"#{site.domain}\"] > a", "href")
+ refute dashboard_link_href =~ "verify_installation=true"
+
+ assert Repo.reload!(site).onboarding_status == :first_pageview
+ end
+
+ for status <- [:verification_succeeded, :first_pageview, :completed] do
+ @tag :ee_only
+ test "does not show once onboarding_status has moved to #{status} (even with stats_start_date reset)",
+ %{conn: conn, user: user} do
+ site = new_site(owner: user, onboarding_status: unquote(status), stats_start_date: nil)
+
+ {:ok, _lv, html} = live(conn, "/sites")
+
+ site_card = text_of_element(html, "li[data-domain=\"#{site.domain}\"]")
+ refute site_card =~ "Setup pending"
+
+ dashboard_link_href =
+ text_of_attr(html, "li[data-domain=\"#{site.domain}\"] > a", "href")
+
+ refute dashboard_link_href =~ "verify_installation=true"
+ end
+ end
+
+ @tag :ce_build_only
+ test "never shows on CE", %{conn: conn, user: user} do
+ site = new_site(owner: user, onboarding_status: :new_site)
+
+ {:ok, _lv, html} = live(conn, "/sites")
+
+ site_card = text_of_element(html, "li[data-domain=\"#{site.domain}\"]")
+ refute site_card =~ "Setup pending"
+
+ dashboard_link_href = text_of_attr(html, "li[data-domain=\"#{site.domain}\"] > a", "href")
+ refute dashboard_link_href =~ "verify_installation=true"
+ end
+ end
+
describe "consolidated views appearance" do
test "consolidated view shows up", %{conn: conn, user: user} do
new_site(owner: user)
diff --git a/test/plausible_web/live/verification_test.exs b/test/plausible_web/live/verification_test.exs
index 1e2928306459..61c15140b354 100644
--- a/test/plausible_web/live/verification_test.exs
+++ b/test/plausible_web/live/verification_test.exs
@@ -5,37 +5,43 @@ defmodule PlausibleWeb.Live.VerificationTest do
import Phoenix.LiveViewTest
+ alias Plausible.Repo
+ alias Plausible.Site
+
@moduletag :capture_log
setup [:create_user, :log_in, :create_site]
- # @verify_button ~s|button#launch-verification-button[phx-click="launch-verification"]|
@retry_button ~s|a[phx-click="retry"]|
- # @go_to_dashboard_button ~s|a[href$="?skip_to_dashboard=true"]|
@progress ~s|#verification-ui p#progress|
- @awaiting ~s|#verification-ui span#awaiting|
- @heading ~s|#verification-ui h2|
+ @heading ~s|#verification-ui h3|
+ @banner ~s|#verification-ui|
+
+ @in_progress_text "Verifying your installation"
describe "GET /:domain" do
@tag :ee_only
- test "static verification screen renders", %{conn: conn, site: site} do
+ test "static verification banner renders on a freshly provisioned site", %{
+ conn: conn,
+ site: site
+ } do
resp =
- get(conn, conn |> no_slowdown() |> get("/#{site.domain}") |> redirected_to)
+ conn
+ |> no_slowdown()
+ |> get("/#{site.domain}?verify_installation=true")
|> html_response(200)
assert text_of_element(resp, @progress) =~
"We're visiting your site to ensure that everything is working"
- assert resp =~ "Verifying your installation"
+ assert resp =~ @in_progress_text
end
@tag :ce_build_only
- test "static verification screen renders (ce)", %{conn: conn, site: site} do
- resp =
- get(conn, conn |> no_slowdown() |> get("/#{site.domain}") |> redirected_to)
- |> html_response(200)
+ test "no verification banner renders on CE", %{conn: conn, site: site} do
+ resp = get(conn, "/#{site.domain}") |> html_response(200)
- assert resp =~ "Awaiting your first pageview …"
+ refute resp =~ "verification-ui"
end
end
@@ -51,45 +57,61 @@ defmodule PlausibleWeb.Live.VerificationTest do
{_, html} = get_lv(conn, site)
- assert html =~ "Verifying your installation"
+ assert html =~ @in_progress_text
assert text_of_element(html, @progress) =~
"We're visiting your site to ensure that everything is working"
end
- @tag :ce_build_only
- test "LiveView mounts (ce)", %{conn: conn, site: site} do
- {_, html} = get_lv(conn, site)
- assert html =~ "Awaiting your first pageview …"
- end
-
@tag :ee_only
- test "from custom URL input form to verification", %{conn: conn, site: site} do
+ test "clicking the custom URL link reveals an inline form next to the retry button, submitting kicks off a new run",
+ %{
+ conn: conn,
+ site: site
+ } do
stub_dns()
stub_verification_result(%{
- "completed" => false,
- "error" => %{"message" => "Error"}
+ "completed" => true,
+ "trackerIsInHtml" => false,
+ "plausibleIsOnWindow" => false,
+ "plausibleIsInitialized" => false
})
- # Get liveview with ?custom_url=true query param
- {:ok, lv, html} =
- conn |> no_slowdown() |> live("/#{site.domain}/verification?custom_url=true")
+ {:ok, lv} = kick_off_live_verification(conn, site)
+
+ assert eventually(fn ->
+ html = render(lv)
+
+ {
+ text_of_element(html, @heading) =~ "We couldn't detect Plausible on your site",
+ html
+ }
+ end)
+
+ html = lv |> render_click("show-custom-url-form")
+
+ refute html =~ @in_progress_text
+
+ refute element_exists?(html, @retry_button)
+ refute element_exists?(html, "#verify-custom-url-link")
- verifying_installation_text = "Verifying your installation"
+ assert element_exists?(
+ html,
+ ~s|form[phx-submit="verify-custom-url"] input[name="custom_url"]|
+ )
- # Assert form is rendered instead of kicking off verification automatically
- assert html =~ "Enter Your Custom URL"
assert html =~ ~s[value="https://#{site.domain}"]
assert html =~ ~s[placeholder="https://#{site.domain}"]
- refute html =~ verifying_installation_text
- # Submit custom URL form
- html = lv |> element("form") |> render_submit(%{"custom_url" => "https://abc.de"})
+ lv
+ |> element("form[phx-submit='verify-custom-url']")
+ |> render_submit(%{"custom_url" => "https://abc.de"})
- # Should now show verification progress and hide custom URL form
- assert html =~ verifying_installation_text
- refute html =~ "Enter Your Custom URL"
+ assert eventually(fn ->
+ html = render(lv)
+ {html =~ @in_progress_text, html}
+ end)
end
@tag :ee_only
@@ -113,25 +135,46 @@ defmodule PlausibleWeb.Live.VerificationTest do
assert eventually(fn ->
html = render(lv)
-
- {
- text_of_element(html, @awaiting) =~
- "Awaiting your first pageview",
- html
- }
+ {html =~ "Tracking is active on your site", html}
end)
+ end
- html = render(lv)
- assert html =~ "Success!"
- assert html =~ "Awaiting your first pageview"
+ for {flow, message} <- %{
+ PlausibleWeb.Flows.review() => "Visitors are being counted correctly.",
+ PlausibleWeb.Flows.domain_change() =>
+ "Visitors are being counted correctly on your new domain."
+ } do
+ @tag :ee_only
+ test "shows a flow-specific success message for flow=#{flow}", %{conn: conn, site: site} do
+ stub_dns()
+
+ stub_verification_result(%{
+ "completed" => true,
+ "trackerIsInHtml" => true,
+ "plausibleIsOnWindow" => true,
+ "plausibleIsInitialized" => true,
+ "testEvent" => %{
+ "normalizedBody" => %{
+ "domain" => site.domain
+ },
+ "responseStatus" => 200
+ }
+ })
+
+ {:ok, lv} = kick_off_live_verification(conn, site, unquote(flow))
+
+ assert eventually(fn ->
+ html = render(lv)
+ {html =~ unquote(message), html}
+ end)
+ end
end
@tag :ee_only
- test "won't await first pageview if site has pageviews", %{conn: conn, site: site} do
- populate_stats(site, [
- build(:pageview)
- ])
-
+ test "advances onboarding_status to :verification_succeeded on success", %{
+ conn: conn,
+ site: site
+ } do
stub_dns()
stub_verification_result(%{
@@ -151,20 +194,53 @@ defmodule PlausibleWeb.Live.VerificationTest do
assert eventually(fn ->
html = render(lv)
-
- {
- text(html) =~ "Success",
- html
- }
+ {html =~ "Tracking is active on your site", html}
end)
- html = render(lv)
+ assert Repo.reload!(site).onboarding_status == :verification_succeeded
+ end
+
+ for flow <- [PlausibleWeb.Flows.review(), PlausibleWeb.Flows.domain_change()] do
+ @tag :ee_only
+ test "advances onboarding_status to :verification_succeeded on first success via flow=#{flow}",
+ %{conn: conn, site: site} do
+ assert site.onboarding_status == :new_site
+
+ stub_dns()
- refute text_of_element(html, @awaiting) =~ "Awaiting your first pageview"
- refute_redirected(lv, "/#{URI.encode_www_form(site.domain)}/")
+ stub_verification_result(%{
+ "completed" => true,
+ "trackerIsInHtml" => true,
+ "plausibleIsOnWindow" => true,
+ "plausibleIsInitialized" => true,
+ "testEvent" => %{
+ "normalizedBody" => %{
+ "domain" => site.domain
+ },
+ "responseStatus" => 200
+ }
+ })
+
+ {:ok, lv} = kick_off_live_verification(conn, site, unquote(flow))
+
+ assert eventually(fn ->
+ html = render(lv)
+ {html =~ "Tracking is active on your site", html}
+ end)
+
+ assert Repo.reload!(site).onboarding_status == :verification_succeeded
+ end
end
- test "will redirect when first pageview arrives", %{conn: conn, site: site} do
+ @tag :ee_only
+ test "does not regress onboarding_status if already past :verification_succeeded", %{
+ conn: conn,
+ site: site
+ } do
+ site
+ |> Site.put_onboarding_status_advance(:completed)
+ |> Repo.update!()
+
stub_dns()
stub_verification_result(%{
@@ -180,64 +256,121 @@ defmodule PlausibleWeb.Live.VerificationTest do
}
})
+ {:ok, lv} = kick_off_live_verification(conn, site, PlausibleWeb.Flows.review())
+
+ assert eventually(fn ->
+ html = render(lv)
+ {html =~ "Tracking is active on your site", html}
+ end)
+
+ assert Repo.reload!(site).onboarding_status == :completed
+ end
+
+ @tag :ee_only
+ test "does not advance onboarding_status when verification fails", %{
+ conn: conn,
+ site: site
+ } do
+ stub_dns()
+
+ stub_verification_result(%{
+ "completed" => true,
+ "trackerIsInHtml" => false,
+ "plausibleIsOnWindow" => false,
+ "plausibleIsInitialized" => false
+ })
+
{:ok, lv} = kick_off_live_verification(conn, site)
assert eventually(fn ->
html = render(lv)
{
- text(html) =~ "Awaiting",
+ text_of_element(html, @heading) =~ "We couldn't detect Plausible on your site",
html
}
end)
- populate_stats(site, [
- build(:pageview)
- ])
-
- assert_redirect(lv, Routes.stats_path(PlausibleWeb.Endpoint, :stats, site.domain))
+ assert Repo.reload!(site).onboarding_status == :new_site
end
- @tag :ce_build_only
- test "will redirect when first pageview arrives (ce)", %{conn: conn, site: site} do
+ @tag :ee_only
+ test "the dismissed flag keeps the banner hidden even if a late update arrives while still connected",
+ %{conn: conn, site: site} do
+ stub_dns()
+
+ stub_verification_result(%{
+ "completed" => true,
+ "trackerIsInHtml" => true,
+ "plausibleIsOnWindow" => true,
+ "plausibleIsInitialized" => true,
+ "testEvent" => %{
+ "normalizedBody" => %{
+ "domain" => site.domain
+ },
+ "responseStatus" => 200
+ }
+ })
+
{:ok, lv} = kick_off_live_verification(conn, site)
html = render(lv)
- assert text(html) =~ "Awaiting your first pageview …"
+ assert html =~ @in_progress_text
+ refute class_of_element(html, @banner) =~ "hidden"
+
+ html = render_click(lv, "dismiss")
+ assert class_of_element(html, @banner) =~ "hidden"
- populate_stats(site, [build(:pageview)])
+ # This might look a bit counter-intuitive -- dismissing the banner
+ # closes the websocket connection and the LV process would normally
+ # die before the component gets notified of success.
+
+ # However, `Phoenix.LiveViewTest` can't simulate a real socket closing,
+ # so the process here just stays alive regardless. What this guards is
+ # the defensive `dismissed?` gate itself: if this process is ever still
+ # around when a late update arrives, for whatever reason, the banner
+ # must stay hidden.
+ assert eventually(fn ->
+ html = render(lv)
+ {html =~ "Tracking is active on your site", html}
+ end)
- assert_redirect(lv, Routes.stats_path(PlausibleWeb.Endpoint, :stats, site.domain))
+ html = render(lv)
+ assert class_of_element(html, @banner) =~ "hidden"
end
- for {installation_type_param, expected_text, saved_installation_type} <- [
- {"manual",
- "Please make sure you've copied the snippet to the head of your site, or verify your installation manually.",
- nil},
- {"npm",
- "Please make sure you've initialized Plausible on your site, or verify your installation manually.",
- nil},
- {"gtm",
- "Please make sure you've configured the GTM template correctly, or verify your installation manually.",
- nil},
- {"wordpress",
- "Please make sure you've enabled the plugin, or verify your installation manually.",
- nil},
- # trusts param over saved installation type
- {"wordpress",
- "Please make sure you've enabled the plugin, or verify your installation manually.",
- "npm"},
- # falls back to saved installation type if no param
- {"",
- "Please make sure you've initialized Plausible on your site, or verify your installation manually.",
+ @tag :ee_only
+ test "dismissing tells the client to close the websocket connection",
+ %{conn: conn, site: site} do
+ stub_dns()
+
+ stub_verification_result(%{
+ "completed" => false,
+ "error" => %{"message" => "Error"}
+ })
+
+ {:ok, lv} = kick_off_live_verification(conn, site)
+
+ render_click(lv, "dismiss")
+
+ assert_push_event(lv, "disconnect-liveview", %{})
+ end
+
+ for {expected_text, saved_installation_type} <- [
+ {"Make sure you've copied the snippet to the head of your site, or verify your installation manually.",
+ "manual"},
+ {"Make sure you've initialized Plausible on your site, or verify your installation manually.",
"npm"},
- # falls back to manual if no param and no saved installation type
- {"",
- "Please make sure you've copied the snippet to the head of your site, or verify your installation manually.",
+ {"Make sure you've configured the GTM template correctly, or verify your installation manually.",
+ "gtm"},
+ {"Make sure you've enabled the WordPress plugin, or verify your installation manually.",
+ "wordpress"},
+ # falls back to manual when there's no saved installation type
+ {"Make sure you've copied the snippet to the head of your site, or verify your installation manually.",
nil}
] do
@tag :ee_only
- test "eventually fails to verify installation (?installation_type=#{installation_type_param}) if saved installation type is #{inspect(saved_installation_type)}",
+ test "eventually fails to verify installation if saved installation type is #{inspect(saved_installation_type)}",
%{
conn: conn,
site: site
@@ -257,51 +390,58 @@ defmodule PlausibleWeb.Live.VerificationTest do
})
end
- {:ok, lv} =
- kick_off_live_verification(
- conn,
- site,
- "?installation_type=#{unquote(installation_type_param)}"
- )
-
- assert html =
- eventually(fn ->
- html = render(lv)
- {html =~ "", html}
-
- {
- text_of_element(html, @heading) =~
- "We couldn't detect Plausible on your site",
- html
- }
- end)
+ {:ok, lv} = kick_off_live_verification(conn, site)
+
+ html =
+ eventually(fn ->
+ html = render(lv)
+
+ {
+ text_of_element(html, @heading) =~ "We couldn't detect Plausible on your site",
+ html
+ }
+ end)
assert element_exists?(html, @retry_button)
- assert html =~ htmlize_quotes(unquote(expected_text))
+ assert text_of_element(html, "#recommendation") =~ unquote(expected_text)
refute element_exists?(html, "#super-admin-report")
end
end
end
- defp get_lv(conn, site, qs \\ nil) do
- {:ok, lv, html} = conn |> no_slowdown() |> live("/#{site.domain}/verification#{qs}")
+ defp get_lv(conn, site) do
+ {:ok, lv, html} =
+ conn |> no_slowdown() |> as_live() |> live(verification_path(site))
+
{lv, html}
end
- defp kick_off_live_verification(conn, site, qs \\ nil) do
+ defp kick_off_live_verification(conn, site, flow \\ nil) do
{:ok, lv, _html} =
- conn |> no_slowdown() |> no_delay() |> live("/#{site.domain}/verification#{qs}")
+ conn |> no_slowdown() |> no_delay() |> as_live() |> live(verification_path(site, flow))
{:ok, lv}
end
+ # `PlausibleWeb.Live.Verification` is rendered via `live_render/3` from a
+ # plain controller-rendered page rather than a live router route, so it
+ # never carries the `:live_module` assign `Phoenix.LiveViewTest.live/2`
+ # looks for. Mirrors the same workaround used in other live_render-embedded
+ # LiveView tests (e.g. props_settings_test.exs).
+ defp as_live(conn), do: assign(conn, :live_module, PlausibleWeb.Live.Verification)
+
+ defp verification_path(site, flow \\ nil) do
+ base = "/#{site.domain}?verify_installation=true"
+ if flow, do: base <> "&flow=#{flow}", else: base
+ end
+
defp no_slowdown(conn) do
Plug.Conn.put_private(conn, :slowdown, 0)
end
defp no_delay(conn) do
- Plug.Conn.put_private(conn, :delay, 0)
+ Plug.Conn.put_private(conn, :launch_delay, 0)
end
defp stub_verification_result(js_data) do
diff --git a/test/support/dev/controllers/e2e_controller.ex b/test/support/dev/controllers/e2e_controller.ex
index bd61fdfbf2e1..165992b82ee7 100644
--- a/test/support/dev/controllers/e2e_controller.ex
+++ b/test/support/dev/controllers/e2e_controller.ex
@@ -51,6 +51,7 @@ defmodule PlausibleWeb.E2EController do
site
|> Plausible.Site.set_native_stats_start_at(stats_start_time)
|> Plausible.Site.set_stats_start_date(stats_start_date)
+ |> Plausible.Site.put_onboarding_status_advance(:first_pageview)
|> Plausible.Repo.update!()
populate(events, site)
@@ -96,6 +97,28 @@ defmodule PlausibleWeb.E2EController do
send_resp(conn, 200, Jason.encode!(%{"ok" => true}))
end
+ def put_verification_scenario(conn, %{"domain" => domain, "scenario" => scenario} = params) do
+ opts = [
+ slowdown: params["options"]["slowdown"] || 0,
+ launch_delay: params["options"]["launch_delay"] || 0
+ ]
+
+ # Registering a new scenario is treated as the start of a fresh
+ # verification "phase" in e2e specs - reset the rate limit so a spec
+ # exercising multiple scenarios/flows against the same domain doesn't
+ # hit the real per-domain verification rate limit.
+ rate_limit_key = "site_verification:#{domain}"
+ :ets.select_delete(Plausible.RateLimit, [{{{rate_limit_key, :_}, :_, :_}, [], [true]}])
+
+ case Plausible.InstallationSupport.Verification.MockScenarios.put(domain, scenario, opts) do
+ :ok ->
+ send_resp(conn, 200, Jason.encode!(%{"ok" => true}))
+
+ {:error, :unknown_scenario} ->
+ send_resp(conn, 422, Jason.encode!(%{"error" => "Unknown scenario: #{scenario}"}))
+ end
+ end
+
defp get_goal(site, name) do
Plausible.Repo.get_by!(Plausible.Goal, site_id: site.id, display_name: name)
end
diff --git a/test/support/factory.ex b/test/support/factory.ex
index 6e2fa3a49032..e534bb72514b 100644
--- a/test/support/factory.ex
+++ b/test/support/factory.ex
@@ -85,7 +85,8 @@ defmodule Plausible.Factory do
site = %Plausible.Site{
native_stats_start_at: ~N[2000-01-01 00:00:00],
domain: domain,
- timezone: "Etc/UTC"
+ timezone: "Etc/UTC",
+ onboarding_status: :new_site
}
merge_attributes(site, attrs)
diff --git a/test/support/installation_support/verification/checks_mock.ex b/test/support/installation_support/verification/checks_mock.ex
new file mode 100644
index 000000000000..b25a3ed2951f
--- /dev/null
+++ b/test/support/installation_support/verification/checks_mock.ex
@@ -0,0 +1,212 @@
+# This file lives under `test/support` (rather than `extra/lib`, alongside
+# the rest of the EE-only installation-support code it depends on -
+# `Diagnostics`, `State`, `CheckRunner`, `Checks`, `Check`) so it's available
+# in the `:dev` env too - see `Plausible.InstallationSupport.MockScenarios`
+# and `ChecksMock`'s moduledocs. `test/support` also compiles under
+# `:ce_test`/`:ce_dev`, where none of those EE-only dependencies exist, so
+# both modules are wrapped in a single `on_ee` block - under CE builds,
+# neither is defined at all, matching the fact that this whole feature
+# (installation verification) doesn't exist there.
+use Plausible
+
+on_ee do
+ defmodule Plausible.InstallationSupport.Verification.MockScenarios do
+ @moduledoc """
+ Per-domain registry of forced verification outcomes. It's a public ETS
+ table owned by a simple GenServer process. Used to bypass the real DNS
+ lookup and browserless checks when iterating on verification banner UI
+ locally, or when driving it from Playwright e2e specs.
+ """
+
+ use GenServer
+
+ alias Plausible.InstallationSupport.Verification.Diagnostics
+
+ @table __MODULE__
+
+ @type scenario :: %{
+ interpretation_result: atom(),
+ slowdown: non_neg_integer() | nil,
+ launch_delay: non_neg_integer() | nil
+ }
+
+ def start_link(_opts) do
+ GenServer.start_link(__MODULE__, nil, name: __MODULE__)
+ end
+
+ @impl true
+ def init(nil) do
+ :ets.new(@table, [:set, :public, :named_table, read_concurrency: true])
+ {:ok, nil}
+ end
+
+ @doc """
+ Registers a mock verification for `domain`.
+
+ `key` (an atom or a string) must name a scenario recognized by
+ `Diagnostics.named_result!/2` - see `Diagnostics.named_scenario_keys/0`.
+ Returns `{:error, :unknown_scenario}` otherwise.
+
+ ### Opts
+
+ * `:slowdown` - overrides the check pipeline's default per-check delay
+ * `:launch_delay` - overrides the delay before the first check starts
+ """
+ @spec put(String.t(), atom() | String.t(), Keyword.t()) :: :ok | {:error, :unknown_scenario}
+ def put(domain, key, opts \\ []) when is_binary(domain) do
+ with {:ok, key} <- resolve_key(key) do
+ scenario = %{
+ interpretation_result: key,
+ slowdown: Keyword.get(opts, :slowdown),
+ launch_delay: Keyword.get(opts, :launch_delay)
+ }
+
+ :ets.insert(@table, {domain, scenario})
+ :ok
+ end
+ end
+
+ defp resolve_key(key) when is_atom(key) do
+ if key in Diagnostics.named_scenario_keys(),
+ do: {:ok, key},
+ else: {:error, :unknown_scenario}
+ end
+
+ defp resolve_key(key) when is_binary(key) do
+ case Diagnostics.named_scenario_from_string(key) do
+ {:ok, key} -> {:ok, key}
+ :error -> {:error, :unknown_scenario}
+ end
+ end
+
+ @doc "Returns the scenario registered for `domain`, or `nil` if none was set."
+ @spec get(String.t()) :: scenario() | nil
+ def get(domain) when is_binary(domain) do
+ case :ets.lookup(@table, domain) do
+ [{^domain, scenario}] -> scenario
+ [] -> nil
+ end
+ end
+ end
+
+ defmodule Plausible.InstallationSupport.Verification.ChecksMock do
+ @moduledoc """
+ Drop-in replacement for `Plausible.InstallationSupport.Verification.Checks`
+ that never performs a real DNS lookup or browserless check for a domain
+ with a registered mock scenario. Used locally (`:dev`) and in Playwright
+ e2e specs (`:e2e_test`) to deterministically drive Verification banner UI.
+
+ When no scenario is registered for a domain:
+
+ * in `:dev`, falls back to the real `Checks` module - casually loading a
+ site with `?verify_installation=true` still verifies for real unless
+ you've deliberately mocked that domain.
+
+ * everywhere else (`:e2e_test`, and `:test` for this module's own
+ tests), raises - every e2e spec that drives verification is expected
+ to register a scenario before triggering it, and it shouldn't
+ silently fall back to a real, slow, non-deterministic check.
+ """
+
+ alias Plausible.InstallationSupport.{State, CheckRunner, Checks}
+ alias Plausible.InstallationSupport.Verification.{Diagnostics, MockScenarios}
+ alias Plausible.InstallationSupport.Verification.Checks, as: RealChecks
+
+ defmodule FakeUrlCheck do
+ @moduledoc false
+ use Plausible.InstallationSupport.Check
+
+ @impl true
+ def report_progress_as, do: Checks.Url.report_progress_as()
+
+ @impl true
+ def perform(state, _opts), do: state
+ end
+
+ defmodule FakeVerifyInstallationCheck do
+ @moduledoc false
+ use Plausible.InstallationSupport.Check
+
+ @impl true
+ def report_progress_as, do: Checks.VerifyInstallation.report_progress_as()
+
+ @impl true
+ def perform(state, _opts), do: state
+ end
+
+ defmodule FakeVerifyInstallationCacheBustCheck do
+ @moduledoc false
+ use Plausible.InstallationSupport.Check
+
+ @impl true
+ def report_progress_as, do: Checks.VerifyInstallationCacheBust.report_progress_as()
+
+ @impl true
+ def perform(state, _opts), do: state
+ end
+
+ @spec run(String.t(), String.t(), String.t(), Keyword.t()) :: {:ok, pid()} | State.t()
+ def run(url, data_domain, installation_type, opts \\ []) do
+ case MockScenarios.get(data_domain) do
+ nil ->
+ raise_unless_dev_env!(data_domain)
+ RealChecks.run(url, data_domain, installation_type, opts)
+
+ scenario ->
+ run_mocked(url, data_domain, installation_type, opts, scenario)
+ end
+ end
+
+ defp run_mocked(url, data_domain, installation_type, opts, scenario) do
+ report_to = Keyword.get(opts, :report_to, self())
+ async? = Keyword.get(opts, :async?, true)
+ slowdown = scenario.slowdown || Keyword.get(opts, :slowdown, 500)
+ launch_delay = scenario.launch_delay || Keyword.get(opts, :launch_delay, 500)
+
+ init_state = %State{
+ url: url || "https://#{data_domain}",
+ data_domain: data_domain,
+ report_to: report_to,
+ diagnostics: %Diagnostics{selected_installation_type: installation_type}
+ }
+
+ checks = [
+ {FakeUrlCheck, []},
+ {FakeVerifyInstallationCheck, []},
+ {FakeVerifyInstallationCacheBustCheck, []}
+ ]
+
+ CheckRunner.run(init_state, checks,
+ async?: async?,
+ report_to: report_to,
+ slowdown: slowdown,
+ launch_delay: launch_delay
+ )
+ end
+
+ @spec interpret_diagnostics(State.t()) :: Plausible.InstallationSupport.Result.t()
+ def interpret_diagnostics(%State{data_domain: data_domain} = state) do
+ case MockScenarios.get(data_domain) do
+ nil ->
+ raise_unless_dev_env!(data_domain)
+ RealChecks.interpret_diagnostics(state)
+
+ scenario ->
+ Diagnostics.named_result!(scenario.interpretation_result,
+ installation_type: state.diagnostics.selected_installation_type,
+ attempted_url: state.url,
+ page_response_status: 500
+ )
+ end
+ end
+
+ defp raise_unless_dev_env!(data_domain) do
+ if Mix.env() != :dev do
+ raise """
+ ChecksMock was used to verify #{inspect(data_domain)}, but no scenario \
+ is registered for it. Call MockScenarios.put/3 first.
+ """
+ end
+ end
+ end
+end
diff --git a/test/workers/import_analytics_test.exs b/test/workers/import_analytics_test.exs
index 42613cbaa4a6..4af13ff2b850 100644
--- a/test/workers/import_analytics_test.exs
+++ b/test/workers/import_analytics_test.exs
@@ -59,7 +59,10 @@ defmodule Plausible.Workers.ImportAnalyticsTest do
site = Repo.reload!(site)
assert site.stats_start_date == nil
- assert Plausible.Sites.stats_start_date(site) == import_opts[:start_date]
+
+ assert Plausible.Sites.ensure_stats_start_date(site).stats_start_date ==
+ import_opts[:start_date]
+
assert Repo.reload!(site).stats_start_date == import_opts[:start_date]
end