From 97bf503570e9a323c16207412b0edd05db3c7a29 Mon Sep 17 00:00:00 2001 From: Amir Hasanbasic Date: Fri, 14 Mar 2025 13:54:26 +0100 Subject: [PATCH 1/6] chore(e2e): introduce user login case --- e2e/test/e2e/ui/example_user_test.exs | 19 ++++++++++ e2e/test/support/ui_test_case.ex | 54 +++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 e2e/test/e2e/ui/example_user_test.exs create mode 100644 e2e/test/support/ui_test_case.ex diff --git a/e2e/test/e2e/ui/example_user_test.exs b/e2e/test/e2e/ui/example_user_test.exs new file mode 100644 index 000000000..134a1f415 --- /dev/null +++ b/e2e/test/e2e/ui/example_user_test.exs @@ -0,0 +1,19 @@ +defmodule E2E.UI.ExampleUserTest do + use E2E.UI.UserTestCase + + describe "Dashboard" do + test "User can view dashboard elements", %{session: session, organization: organization, base_domain: base_domain} do + # Test starts with already logged-in user because of UserTestCase setup + + # Navigate to dashboard + dashboard_url = "https://#{organization}.#{base_domain}/dashboard" + + session + |> Wallaby.Browser.visit(dashboard_url) + |> Wallaby.Browser.assert_has(Wallaby.Query.css(".dashboard-content")) + + # Take a screenshot of dashboard + Wallaby.Browser.take_screenshot(session) + end + end +end diff --git a/e2e/test/support/ui_test_case.ex b/e2e/test/support/ui_test_case.ex new file mode 100644 index 000000000..0441b02ad --- /dev/null +++ b/e2e/test/support/ui_test_case.ex @@ -0,0 +1,54 @@ +defmodule E2E.UI.UserTestCase do + @moduledoc """ + A custom ExUnit case for UI tests requiring a logged-in user. + + This module automatically: + - Tags tests with :user and :browser + - Sets up Wallaby browser session + - Performs user login before each test + """ + + use ExUnit.CaseTemplate + + using do + quote do + use ExUnit.Case, async: false + use Wallaby.DSL + + @moduletag :user + @moduletag :browser + @moduletag timeout: 300_000 + end + end + + setup_all do + Application.put_env(:wallaby, :js_errors, false) + :ok + end + + setup do + {:ok, session} = Wallaby.start_session(js_errors: false) + + base_domain = Application.get_env(:e2e, :semaphore_base_domain) + root_email = Application.get_env(:e2e, :semaphore_root_email) + root_password = Application.get_env(:e2e, :semaphore_root_password) + organization = Application.get_env(:e2e, :semaphore_organization) + + login_url = "https://id.#{base_domain}/login" + + logged_in_session = + session + |> Wallaby.Browser.visit(login_url) + |> Wallaby.Browser.fill_in(Wallaby.Query.text_field("username"), with: root_email) + |> Wallaby.Browser.fill_in(Wallaby.Query.text_field("password"), with: root_password) + |> Wallaby.Browser.click(Wallaby.Query.css("#kc-login")) + |> Wallaby.Browser.assert_has( + Wallaby.Query.css("h1.f2.f1-m.lh-title.mb1", + text: "Here's what's going on", + timeout: 10_000 + ) + ) + + {:ok, session: logged_in_session, organization: organization, base_domain: base_domain} + end +end From 688f7c3a0ee4375ee1adcc6270acf049dca0f0ec Mon Sep 17 00:00:00 2001 From: Amir Hasanbasic Date: Tue, 15 Apr 2025 08:39:45 +0200 Subject: [PATCH 2/6] fix(e2e): try requiring Wallaby DSL --- e2e/test/support/ui_test_case.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/e2e/test/support/ui_test_case.ex b/e2e/test/support/ui_test_case.ex index 0441b02ad..47216c568 100644 --- a/e2e/test/support/ui_test_case.ex +++ b/e2e/test/support/ui_test_case.ex @@ -14,6 +14,7 @@ defmodule E2E.UI.UserTestCase do quote do use ExUnit.Case, async: false use Wallaby.DSL + require Wallaby.Browser @moduletag :user @moduletag :browser From fe33ab9bfc9adb8b3397a8f855f2f915a8d78987 Mon Sep 17 00:00:00 2001 From: Amir Hasanbasic Date: Wed, 16 Apr 2025 11:43:52 +0200 Subject: [PATCH 3/6] Add git integrations page UI tests and set up env for running e2e tests from local env --- e2e/.gitignore | 3 +- e2e/Dockerfile | 54 +++++++ e2e/Makefile | 44 +++++- e2e/docker-compose.yml | 37 +++++ e2e/lib/e2e/clients/common.ex | 7 +- e2e/lib/e2e/clients/job.ex | 1 - e2e/test/e2e/api/secrets_test.exs | 2 +- e2e/test/e2e/api/task_test.exs | 31 ++-- e2e/test/e2e/api/workflow_test.exs | 1 + e2e/test/e2e/ui/example_user_test.exs | 19 --- e2e/test/e2e/ui/git_integrations_test.exs | 164 ++++++++++++++++++++++ e2e/test/support/ui_test_case.ex | 56 ++++++-- 12 files changed, 368 insertions(+), 51 deletions(-) create mode 100644 e2e/Dockerfile create mode 100644 e2e/docker-compose.yml delete mode 100644 e2e/test/e2e/ui/example_user_test.exs create mode 100644 e2e/test/e2e/ui/git_integrations_test.exs diff --git a/e2e/.gitignore b/e2e/.gitignore index 7cc57ce68..b2a82da61 100644 --- a/e2e/.gitignore +++ b/e2e/.gitignore @@ -24,4 +24,5 @@ e2e-*.tar # Temporary files, for example, from tests. /tmp/ -/out/ \ No newline at end of file +/out/ +.envrc diff --git a/e2e/Dockerfile b/e2e/Dockerfile new file mode 100644 index 000000000..8305cb53b --- /dev/null +++ b/e2e/Dockerfile @@ -0,0 +1,54 @@ +ARG ELIXIR_VERSION=1.14.3 +ARG OTP_VERSION=25.2.3 +ARG ALPINE_VERSION=3.18.0 +ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-alpine-${ALPINE_VERSION}" + +# Base stage with common dependencies +FROM ${BUILDER_IMAGE} AS base + +# Set Wallaby env var +ENV START_WALLABY=true + +# Install system dependencies including ChromeDriver +RUN apk update && \ + apk add --no-cache \ + build-base \ + git \ + python3 \ + curl \ + openssh \ + chromium \ + chromium-chromedriver \ + xvfb \ + bash \ + # Add additional dependencies that may be required + ttf-freefont \ + fontconfig \ + dbus \ + && apk add --no-cache --upgrade busybox busybox-binsh ssl_client + +# Set up Chrome for headless operation +ENV CHROME_BIN=/usr/bin/chromium-browser +ENV CHROME_PATH=/usr/lib/chromium/ +ENV CHROME_DRIVER_PATH=/usr/bin/chromedriver + +# Set up Elixir environment +WORKDIR /app + +# Install hex and rebar +RUN mix local.hex --force && \ + mix local.rebar --force + +# Copy and compile dependencies +COPY mix.exs mix.lock ./ +RUN mix deps.get && mix deps.compile + +# Copy application code +COPY . . + +# Set Wallaby to use Chrome in headless mode +ENV WALLABY_DRIVER=chrome +ENV WALLABY_CHROME_HEADLESS=true + +# Create directory for screenshots +RUN mkdir -p /app/out/screenshots diff --git a/e2e/Makefile b/e2e/Makefile index d76a7f632..05de92b03 100644 --- a/e2e/Makefile +++ b/e2e/Makefile @@ -1,4 +1,4 @@ -.PHONY: format test test.ui +.PHONY: format test test.ui console.bash test.ui.docker SHELL := /bin/bash export MIX_ENV ?= test @@ -17,12 +17,54 @@ ifeq (test.ui,$(MAKECMDGOALS)) export START_WALLABY=true endif +ifeq (test.ui.docker,$(MAKECMDGOALS)) + export START_WALLABY=true +endif + +ifeq (console.bash,$(MAKECMDGOALS)) + export START_WALLABY=true +endif + gcloud.auth: gcloud config set project $(GOOGLE_PROJECT_NAME) --quiet && gcloud auth login --cred-file=$(GOOGLE_APPLICATION_CREDENTIALS) console.ex: env.assert mix.prepare iex -S mix +# Start a bash shell in the Docker container +console.bash: env.assert + docker compose run --rm \ + -e START_WALLABY=true \ + -e MIX_ENV=$(MIX_ENV) \ + -e BASE_DOMAIN=$(BASE_DOMAIN) \ + -e CLOUD_TEST_ENV_PREFIX=$(CLOUD_TEST_ENV_PREFIX) \ + -e GITHUB_ORGANIZATION=$(GITHUB_ORGANIZATION) \ + -e GITHUB_REPOSITORY=$(GITHUB_REPOSITORY) \ + -e GITHUB_BRANCH=$(GITHUB_BRANCH) \ + -e SEMAPHORE_ORGANIZATION=$(SEMAPHORE_ORGANIZATION) \ + -e SEMAPHORE_BASE_DOMAIN=$(SEMAPHORE_BASE_DOMAIN) \ + -e SEMAPHORE_USER_EMAIL=$(SEMAPHORE_USER_EMAIL) \ + -e SEMAPHORE_API_TOKEN=$(SEMAPHORE_API_TOKEN) \ + -e SEMAPHORE_USER_PASSWORD=$(SEMAPHORE_USER_PASSWORD) \ + e2e-tests sh + +# Run UI tests in Docker +test.ui.docker: env.assert + docker compose run --rm \ + -e START_WALLABY=true \ + -e MIX_ENV=$(MIX_ENV) \ + -e BASE_DOMAIN=$(BASE_DOMAIN) \ + -e CLOUD_TEST_ENV_PREFIX=$(CLOUD_TEST_ENV_PREFIX) \ + -e GITHUB_ORGANIZATION=$(GITHUB_ORGANIZATION) \ + -e GITHUB_REPOSITORY=$(GITHUB_REPOSITORY) \ + -e GITHUB_BRANCH=$(GITHUB_BRANCH) \ + -e SEMAPHORE_ORGANIZATION=$(SEMAPHORE_ORGANIZATION) \ + -e SEMAPHORE_BASE_DOMAIN=$(SEMAPHORE_BASE_DOMAIN) \ + -e SEMAPHORE_USER_EMAIL=$(SEMAPHORE_USER_EMAIL) \ + -e SEMAPHORE_API_TOKEN=$(SEMAPHORE_API_TOKEN) \ + -e SEMAPHORE_USER_PASSWORD=$(SEMAPHORE_USER_PASSWORD) \ + e2e-tests sh -c "chromedriver --port=9515 --whitelisted-ips='' --url-base=/wd/hub & mix test $(if $(TEST_FILE),$(TEST_FILE),test/e2e/ui)" + format: SEMAPHORE_API_TOKEN="" \ SEMAPHORE_USER_PASSWORD="" \ diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml new file mode 100644 index 000000000..242b1fefb --- /dev/null +++ b/e2e/docker-compose.yml @@ -0,0 +1,37 @@ +version: "3" + +services: + e2e-tests: + build: + context: . + dockerfile: Dockerfile + platform: linux/arm64 # Use ARM64 platform for Apple Silicon Macs + shm_size: 2gb # Increase shared memory for Chrome + volumes: + - .:/app + - ./out/screenshots:/app/out/screenshots + environment: + - START_WALLABY + - WALLABY_DRIVER=chrome + - WALLABY_CHROME_HEADLESS=true + - MIX_ENV + # Chrome/Chromium configuration + - CHROME_BIN=/usr/bin/chromium-browser + - CHROME_PATH=/usr/lib/chromium/ + # Pass Semaphore environment variables from the host + - SEMAPHORE_API_TOKEN + - SEMAPHORE_USER_PASSWORD + - SEMAPHORE_BASE_DOMAIN + - SEMAPHORE_USER_EMAIL + - SEMAPHORE_ORGANIZATION + - CLOUD_TEST_ENV_PREFIX + - BASE_DOMAIN + - GITHUB_ORGANIZATION + - GITHUB_REPOSITORY + - GITHUB_BRANCH + - GOOGLE_PROJECT_NAME + - GOOGLE_APPLICATION_CREDENTIALS + ports: + - "9515:9515" # ChromeDriver port + command: > + sh -c "chromedriver --port=9515 --verbose --whitelisted-ips='' --url-base=/wd/hub & mix test --include browser" diff --git a/e2e/lib/e2e/clients/common.ex b/e2e/lib/e2e/clients/common.ex index 3caf1f1ae..006374a7a 100644 --- a/e2e/lib/e2e/clients/common.ex +++ b/e2e/lib/e2e/clients/common.ex @@ -45,13 +45,18 @@ defmodule E2E.Clients.Common do url = api_url(endpoint) timeout = Application.get_env(:e2e, :http_timeout, 30_000) - case HTTPoison.get(url, headers, timeout: timeout, recv_timeout: timeout, follow_redirect: true) do + case HTTPoison.get(url, headers, + timeout: timeout, + recv_timeout: timeout, + follow_redirect: true + ) do {:ok, response} -> {:ok, response} {:error, %HTTPoison.Error{reason: :timeout}} -> # Retry once on timeout Process.sleep(1000) + HTTPoison.get(url, headers, timeout: timeout, recv_timeout: timeout, follow_redirect: true) error -> diff --git a/e2e/lib/e2e/clients/job.ex b/e2e/lib/e2e/clients/job.ex index 21ac64197..5b42743b1 100644 --- a/e2e/lib/e2e/clients/job.ex +++ b/e2e/lib/e2e/clients/job.ex @@ -23,5 +23,4 @@ defmodule E2E.Clients.Job do {:error, reason} end end - end diff --git a/e2e/test/e2e/api/secrets_test.exs b/e2e/test/e2e/api/secrets_test.exs index 3182fb75c..e3d170963 100644 --- a/e2e/test/e2e/api/secrets_test.exs +++ b/e2e/test/e2e/api/secrets_test.exs @@ -32,7 +32,7 @@ defmodule E2E.API.SecretsTest do name = "test-project-#{:rand.uniform(1_000_000)}" repository_url = "git@github.com:#{organization}/#{repository}.git" {:ok, project} = Support.prepare_project(name, repository_url) - + on_exit(fn -> :ok = Project.delete(name) end) diff --git a/e2e/test/e2e/api/task_test.exs b/e2e/test/e2e/api/task_test.exs index 4b6dd8d57..fd1862dde 100644 --- a/e2e/test/e2e/api/task_test.exs +++ b/e2e/test/e2e/api/task_test.exs @@ -13,29 +13,32 @@ defmodule E2E.API.TaskTest do name = "test-project-#{:rand.uniform(1_000_000)}" # Create project with a task - task_definitions = [%{ - "name" => "test-task", - "status" => "ACTIVE", - "description" => "Test periodic task", - "at" => "0 * * * *", - "pipeline_file" => ".semaphore/semaphore.yml", - "branch" => "main", - "parameters" => [] - }] + task_definitions = [ + %{ + "name" => "test-task", + "status" => "ACTIVE", + "description" => "Test periodic task", + "at" => "0 * * * *", + "pipeline_file" => ".semaphore/semaphore.yml", + "branch" => "main", + "parameters" => [] + } + ] {:ok, created_project} = Support.prepare_project(name, repository_url, task_definitions) + on_exit(fn -> :ok = Project.delete(name) end) task = created_project["spec"]["tasks"] |> hd - {:ok, - project_id: created_project["metadata"]["id"], - task_id: task["id"] - } + {:ok, project_id: created_project["metadata"]["id"], task_id: task["id"]} end - test "run task with run_now and wait for completion", %{project_id: project_id, task_id: task_id} do + test "run task with run_now and wait for completion", %{ + project_id: project_id, + task_id: task_id + } do # Trigger run_now {:ok, response} = E2E.Clients.Task.run_now(task_id) diff --git a/e2e/test/e2e/api/workflow_test.exs b/e2e/test/e2e/api/workflow_test.exs index 893ead106..183549986 100644 --- a/e2e/test/e2e/api/workflow_test.exs +++ b/e2e/test/e2e/api/workflow_test.exs @@ -55,6 +55,7 @@ defmodule E2E.API.WorkflowTest do Enum.each(pipelines, fn pipeline -> if pipeline["result"] != "PASSED" do {:ok, job_ids} = Pipeline.failed_jobs_id(pipeline["ppl_id"]) + Enum.each(job_ids, fn job_id -> {:ok, events} = Job.events(job_id) Enum.each(events, fn event -> IO.puts(inspect(event)) end) diff --git a/e2e/test/e2e/ui/example_user_test.exs b/e2e/test/e2e/ui/example_user_test.exs deleted file mode 100644 index 134a1f415..000000000 --- a/e2e/test/e2e/ui/example_user_test.exs +++ /dev/null @@ -1,19 +0,0 @@ -defmodule E2E.UI.ExampleUserTest do - use E2E.UI.UserTestCase - - describe "Dashboard" do - test "User can view dashboard elements", %{session: session, organization: organization, base_domain: base_domain} do - # Test starts with already logged-in user because of UserTestCase setup - - # Navigate to dashboard - dashboard_url = "https://#{organization}.#{base_domain}/dashboard" - - session - |> Wallaby.Browser.visit(dashboard_url) - |> Wallaby.Browser.assert_has(Wallaby.Query.css(".dashboard-content")) - - # Take a screenshot of dashboard - Wallaby.Browser.take_screenshot(session) - end - end -end diff --git a/e2e/test/e2e/ui/git_integrations_test.exs b/e2e/test/e2e/ui/git_integrations_test.exs new file mode 100644 index 000000000..3d161928c --- /dev/null +++ b/e2e/test/e2e/ui/git_integrations_test.exs @@ -0,0 +1,164 @@ +defmodule E2E.UI.GitIntegrationsTest do + use E2E.UI.UserTestCase + + def navigate_to_git_integrations(session, organization, base_domain) do + git_integrations_url = "https://#{organization}.#{base_domain}/settings/git_integrations/" + Logger.info("Navigating to Git Integrations page: #{git_integrations_url}") + + session + |> visit(git_integrations_url) + end + + describe "Git Integrations page" do + setup %{session: session, organization: organization, base_domain: base_domain} do + session = navigate_to_git_integrations(session, organization, base_domain) + {:ok, %{session: session}} + end + + test "has correct title and description", %{session: session} do + session + |> assert_has(Wallaby.Query.text("Git Integrations", count: 1)) + end + + test "has two main sections: Integrations and Connect new", %{session: session} do + session + |> assert_has( + Wallaby.Query.css(".bb.b--black-075.w-100-l.mb4.br3.shadow-3.bg-white", + count: 2 + ) + ) + |> assert_has( + Wallaby.Query.css(".bb.bw1.b--black-075.br3.br--top .flex.items-center .b", + text: "Integrations" + ) + ) + |> assert_has( + Wallaby.Query.css(".bb.bw1.b--black-075.br3.br--top .flex.items-center .b", + text: "Connect new" + ) + ) + end + + test "has at least one connected integration", %{session: session} do + session + |> assert_has( + Wallaby.Query.css(".bb.b--black-075.w-100-l .ph3.pv2.mv2", + minimum: 1 + ) + ) + + has_github_or_gitlab = + has?(session, Wallaby.Query.css("img[src*='icn-github.svg']")) || + has?(session, Wallaby.Query.css("img[src*='icn-gitlab.svg']")) + + assert has_github_or_gitlab, "Expected to find either GitHub or GitLab integration card" + + session + |> assert_has(Wallaby.Query.text("connected", minimum: 1)) + end + + test "has available integrations to connect", %{session: session} do + session + |> assert_has( + Wallaby.Query.css(".bb.b--black-075.w-100-l:nth-of-type(2) .ph3.pv2.mv2", + minimum: 1, + timeout: 10_000 + ) + ) + + available_for_connect = + has?(session, Wallaby.Query.css("img[src*='icn-github.svg']:not(.f6.gray ~ *)")) || + has?(session, Wallaby.Query.css("img[src*='icn-gitlab.svg']:not(.f6.gray ~ *)")) || + has?(session, Wallaby.Query.css("img[src*='icn-bitbucket.svg']")) + + assert available_for_connect, + "Expected to find at least one available integration to connect" + + session + |> assert_has(Wallaby.Query.link("Connect", minimum: 1)) + end + end + + describe "Integration details page" do + setup %{session: session, organization: organization, base_domain: base_domain} do + session = navigate_to_git_integrations(session, organization, base_domain) + + # Find the edit button and click it - we use a simpler direct approach + session + |> click( + Wallaby.Query.css(".material-symbols-outlined.f5.b.btn.pointer.pa1.btn-secondary.ml3", + count: 2, + at: 0 + ) + ) + |> assert_has(Wallaby.Query.text("Configuration parameters")) + + {:ok, %{session: session}} + end + + test "has correct page structure and navigation", %{session: session} do + session + |> assert_has(Wallaby.Query.link("← Back to Integration")) + |> assert_has(Wallaby.Query.css("h2.f3.f2-m.mb0")) + end + + test "shows connection status", %{session: session} do + session + |> assert_has(Wallaby.Query.text("GitHub App Connection")) + # Green circle indicator + |> assert_has(Wallaby.Query.css("circle[fill='#00a569']")) + end + + test "has required permissions section", %{session: session} do + session + |> assert_has(Wallaby.Query.text("Required Permissions")) + |> assert_has(Wallaby.Query.css("li", minimum: 5)) + end + + test "has remove connection section", %{session: session} do + session + |> assert_has(Wallaby.Query.text("Remove connection")) + |> assert_has(Wallaby.Query.text("Warning: Removing this integration")) + |> assert_has(Wallaby.Query.button("Delete")) + end + end + + describe "New integration setup page" do + setup %{session: session, organization: organization, base_domain: base_domain} do + session = navigate_to_git_integrations(session, organization, base_domain) + + session + |> click(Wallaby.Query.css("a.btn.btn-primary.btn-small", at: 0)) + + session + |> assert_has(Wallaby.Query.text("Integration Setup")) + + {:ok, %{session: session}} + end + + test "has correct page structure and navigation", %{session: session} do + session + |> assert_has(Wallaby.Query.link("← Back to Integration")) + |> assert_has(Wallaby.Query.text("Integration Setup")) + end + + test "has configuration parameters section", %{session: session} do + session + |> assert_has(Wallaby.Query.text("Configuration parameters")) + |> assert_has(Wallaby.Query.text("Callback URL")) + end + + test "has required permissions list", %{session: session} do + session + |> assert_has(Wallaby.Query.text("Required Permissions")) + |> assert_has(Wallaby.Query.css("li", minimum: 5)) + end + + test "has connect integration form", %{session: session} do + session + |> assert_has(Wallaby.Query.text_field("client_id")) + |> assert_has(Wallaby.Query.css("input[type='password'][name='client_secret']")) + |> assert_has(Wallaby.Query.button("Connect Integration")) + end + end +end diff --git a/e2e/test/support/ui_test_case.ex b/e2e/test/support/ui_test_case.ex index 47216c568..fbfadef05 100644 --- a/e2e/test/support/ui_test_case.ex +++ b/e2e/test/support/ui_test_case.ex @@ -9,12 +9,17 @@ defmodule E2E.UI.UserTestCase do """ use ExUnit.CaseTemplate + require Wallaby.Browser + import Wallaby.Browser + require Logger using do quote do - use ExUnit.Case, async: false + use ExUnit.Case, async: true use Wallaby.DSL require Wallaby.Browser + import Wallaby.Browser + require Logger @moduletag :user @moduletag :browser @@ -37,19 +42,44 @@ defmodule E2E.UI.UserTestCase do login_url = "https://id.#{base_domain}/login" - logged_in_session = - session - |> Wallaby.Browser.visit(login_url) - |> Wallaby.Browser.fill_in(Wallaby.Query.text_field("username"), with: root_email) - |> Wallaby.Browser.fill_in(Wallaby.Query.text_field("password"), with: root_password) - |> Wallaby.Browser.click(Wallaby.Query.css("#kc-login")) - |> Wallaby.Browser.assert_has( - Wallaby.Query.css("h1.f2.f1-m.lh-title.mb1", - text: "Here's what's going on", - timeout: 10_000 + try do + # Fill in login form and authenticate + logged_in_session = + session + |> visit(login_url) + |> (fn s -> + # Verify login form exists + has?(s, Wallaby.Query.css("#kc-form-login")) + s + end).() + |> fill_in(Wallaby.Query.text_field("username"), with: root_email) + |> fill_in(Wallaby.Query.text_field("password"), with: root_password) + |> click(Wallaby.Query.css("#kc-login")) + |> assert_has( + Wallaby.Query.css("h1.f2.f1-m.lh-title.mb1", + text: "Here's what's going on", + timeout: 10_000 + ) ) - ) - {:ok, session: logged_in_session, organization: organization, base_domain: base_domain} + {:ok, session: logged_in_session, organization: organization, base_domain: base_domain} + rescue + e in Wallaby.ExpectationNotMetError -> + # Take screenshot of the error state + take_screenshot(session, name: "login_failure") + # Log the current URL and HTML source for debugging + Logger.error("Login failed! Current URL: #{current_url(session)}") # Attempt to capture some of the page source + html_source = + try do + session + |> execute_script("return document.documentElement.outerHTML") + |> String.slice(0, 500) + rescue + _ -> "Could not retrieve page source" + end + + Logger.error("Page source snippet: #{html_source}...") + reraise e, __STACKTRACE__ + end end end From b70d10145c2912fb72aa81e50fa1c49743764235 Mon Sep 17 00:00:00 2001 From: Amir Hasanbasic Date: Wed, 16 Apr 2025 12:52:35 +0200 Subject: [PATCH 4/6] fix(e2e): stable assertion for logged in session --- e2e/test/support/ui_test_case.ex | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/e2e/test/support/ui_test_case.ex b/e2e/test/support/ui_test_case.ex index fbfadef05..fecc08993 100644 --- a/e2e/test/support/ui_test_case.ex +++ b/e2e/test/support/ui_test_case.ex @@ -55,12 +55,8 @@ defmodule E2E.UI.UserTestCase do |> fill_in(Wallaby.Query.text_field("username"), with: root_email) |> fill_in(Wallaby.Query.text_field("password"), with: root_password) |> click(Wallaby.Query.css("#kc-login")) - |> assert_has( - Wallaby.Query.css("h1.f2.f1-m.lh-title.mb1", - text: "Here's what's going on", - timeout: 10_000 - ) - ) + + assert current_url(logged_in_session) == "https://#{organization}.#{base_domain}/get_started/" {:ok, session: logged_in_session, organization: organization, base_domain: base_domain} rescue From 66647c2371d61920bf12c5a5483318e06253a242 Mon Sep 17 00:00:00 2001 From: Amir Hasanbasic Date: Thu, 17 Apr 2025 11:00:31 +0200 Subject: [PATCH 5/6] fix(e2e): Make sure that ui test case is in elixirc paths only when doign UI tests --- e2e/mix.exs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/e2e/mix.exs b/e2e/mix.exs index 5a9b5e432..ebd21dac4 100644 --- a/e2e/mix.exs +++ b/e2e/mix.exs @@ -19,7 +19,24 @@ defmodule E2E.MixProject do ] end - defp elixirc_paths(:test), do: ["lib", "test/support"] + defp elixirc_paths(:test) do + base = ["lib", "test/support"] + if System.get_env("START_WALLABY") do + base + else + # Exclude ui_test_case.ex if Wallaby is not available + ["lib", "test/support"] + |> Enum.flat_map(fn path -> + if path == "test/support" do + Path.wildcard("test/support/*.ex") + |> Enum.reject(&(&1 =~ "ui_test_case.ex")) + else + [path] + end + end) + end + end + defp elixirc_paths(:dev), do: ["lib", "test/support"] defp elixirc_paths(_), do: ["lib"] From 4e4fb7a9bdf7ed625302559e212bbd7b802b541f Mon Sep 17 00:00:00 2001 From: Amir Hasanbasic Date: Wed, 16 Apr 2025 15:21:46 +0200 Subject: [PATCH 6/6] chore(e2e): add project creation flow happy path test --- e2e/test/e2e/ui/project_creation_test.exs | 113 ++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 e2e/test/e2e/ui/project_creation_test.exs diff --git a/e2e/test/e2e/ui/project_creation_test.exs b/e2e/test/e2e/ui/project_creation_test.exs new file mode 100644 index 000000000..ccae53a71 --- /dev/null +++ b/e2e/test/e2e/ui/project_creation_test.exs @@ -0,0 +1,113 @@ +defmodule E2E.UI.ProjectCreationFlowTest do + use E2E.UI.UserTestCase + require Logger + + describe "Project Creation Flow" do + @tag timeout: 600_000 + test "complete project creation flow", %{session: session} do + Logger.info("Starting Project Creation Flow test") + + # Step 1: Navigate to project creation page + Logger.info("Step 1: Navigating to project creation page") + session = click(session, Wallaby.Query.link("Create new")) + + # Verify we're on the project creation page + Logger.info("Verifying project creation page") + session = assert_has(session, Wallaby.Query.css("h1", text: "Project type")) + + # Step 2: Select GitHub integration using a specific CSS selector + Logger.info("Step 2: Selecting GitHub integration") + session = click(session, Wallaby.Query.css(".f3.b", text: "GitHub")) + + # Take screenshot to see what happened + take_screenshot(session, name: "after_github_click") + + # Verify we're on the repository selection page + Logger.info("Verifying repository selection page") + session = assert_has(session, Wallaby.Query.css("h2", text: "Repository")) + + # Step 3: Search for repositories + Logger.info("Step 3: Searching for repositories") + session = fill_in(session, Wallaby.Query.fillable_field("Search repositories..."), with: "e2e-tests") + + # Give the search some time to complete + :timer.sleep(1000) + + session = assert_has(session, Wallaby.Query.css(".option")) + + # Try to find and click the "Choose" button if available + Logger.info("Clicking 'Choose' button") + session = click(session, Wallaby.Query.css(".green", text: "Choose")) + + # Take screenshot after repository selection + take_screenshot(session, name: "after_repository_selection") + # Give some time to check for duplicates + :timer.sleep(1000) + take_screenshot(session, name: "after_repository_selection_with_duplicates") + + session = maybe_enable_duplicate(session) + + Logger.info("Clicking create project button") + assert_has(session, Wallaby.Query.css("button.btn.btn-primary")) + click(session, Wallaby.Query.button("✓")) + + # Take screenshot after clicking continue + take_screenshot(session, name: "after_continue") + + :timer.sleep(15_000) + # Verify we moved to the analysis page and check for the analysis steps checklist + take_screenshot(session, name: "analysis_page") + + # wait for project creation to complete (until webhook readonly input is visible) + # This can take up to 15 seconds + Logger.info("Waiting for project creation to complete (up to 15 seconds)...") + + # click continue button + session = click(session, Wallaby.Query.link("Continue")) + + # Click on the "I want to configure this project from scratch" link + Logger.info("Clicking 'I want to configure this project from scratch' link") + session = click(session, Wallaby.Query.link("I want to configure this project from scratch")) + + # Take a screenshot after clicking the link + take_screenshot(session, name: "configure_from_scratch") + + # Click continue button again after selecting "configure from scratch" + Logger.info("Clicking Continue button again") + session = assert_has(session, Wallaby.Query.button("Continue")) + session = click(session, Wallaby.Query.button("Continue")) + + # Take a screenshot after clicking continue again + take_screenshot(session, name: "after_second_continue") + + # Click "Looks good, start →" button + Logger.info("Clicking 'Looks good, start →' button") + session = assert_has(session, Wallaby.Query.button("Looks good, start →")) + session = click(session, Wallaby.Query.button("Looks good, start →")) + + # Take a screenshot after clicking the start button + take_screenshot(session, name: "after_start_button") + + # Wait for 15 seconds to allow page transition + Logger.info("Waiting 15 seconds for page transition...") + :timer.sleep(15_000) + + # Verify the URL path starts with "/workflows/" + Logger.info("Verifying we are on the workflows page") + current_url = current_url(session) + assert String.contains?(current_url, "/workflows/"), + "Expected URL to contain '/workflows/', but got: #{current_url}" + + # Take a final screenshot of the workflows page + take_screenshot(session, name: "workflows_page") + + Logger.info("Project Creation completed successfully, flow test is complete") + end + end + + defp maybe_enable_duplicate(session) do + if has?(session, Wallaby.Query.button("Make a duplicate project")) do + click(session, Wallaby.Query.button("Make a duplicate project")) + end + end +end