From 46f7642f18d3ee68fb2af9015433443c4ffc7676 Mon Sep 17 00:00:00 2001 From: Amir Hasanbasic <43892661+hamir-suspect@users.noreply.github.com> Date: Fri, 10 Oct 2025 14:56:19 +0200 Subject: [PATCH] toil(e2e): user management and project creation (#267) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 📝 Description ## ✅ Checklist - [ ] I have tested this change - [ ] This change requires documentation update --- e2e/.gitignore | 1 + e2e/config/config.exs | 3 +- e2e/mix.exs | 16 +- e2e/test/e2e/ui/login_test.exs | 4 + e2e/test/e2e/ui/project_creation_test.exs | 136 ++++++++++++ e2e/test/e2e/ui/user_management_test.exs | 239 ++++++++++++++++++++++ e2e/test/support/ui_test_case.ex | 27 ++- e2e/test/support/user_action.ex | 65 ++++++ 8 files changed, 469 insertions(+), 22 deletions(-) create mode 100644 e2e/test/e2e/ui/project_creation_test.exs create mode 100644 e2e/test/e2e/ui/user_management_test.exs create mode 100644 e2e/test/support/user_action.ex diff --git a/e2e/.gitignore b/e2e/.gitignore index ea0c39761..485a757dc 100644 --- a/e2e/.gitignore +++ b/e2e/.gitignore @@ -25,3 +25,4 @@ e2e-*.tar # Temporary files, for example, from tests. /tmp/ .envrc +/out/ diff --git a/e2e/config/config.exs b/e2e/config/config.exs index a996d5b7f..7b0f9d8f4 100644 --- a/e2e/config/config.exs +++ b/e2e/config/config.exs @@ -47,8 +47,7 @@ if System.get_env("START_WALLABY") do "--no-sandbox", "--disable-gpu", "--disable-dev-shm-usage", - "--disable-software-rasterizer", - "--window-size=1280,800" + "--window-size=1920,1200" ] ] end diff --git a/e2e/mix.exs b/e2e/mix.exs index ebd21dac4..832aa77c9 100644 --- a/e2e/mix.exs +++ b/e2e/mix.exs @@ -24,15 +24,19 @@ defmodule E2E.MixProject do if System.get_env("START_WALLABY") do base else - # Exclude ui_test_case.ex if Wallaby is not available + ui_test_files = [ + "test/support/ui_test_case.ex", + "test/support/user_action.ex" + ] + ["lib", "test/support"] - |> Enum.flat_map(fn path -> - if path == "test/support" do + |> Enum.flat_map(fn + "test/support" -> Path.wildcard("test/support/*.ex") - |> Enum.reject(&(&1 =~ "ui_test_case.ex")) - else + |> Enum.reject(&(&1 in ui_test_files)) + + path -> [path] - end end) end end diff --git a/e2e/test/e2e/ui/login_test.exs b/e2e/test/e2e/ui/login_test.exs index 9930c4b2c..2ba752eb2 100644 --- a/e2e/test/e2e/ui/login_test.exs +++ b/e2e/test/e2e/ui/login_test.exs @@ -41,6 +41,10 @@ defmodule E2E.UI.LoginTest do |> assert_has(Query.css("#kc-login[type='submit'][value='Sign In']")) |> fill_in(Query.text_field("username"), with: root_email) |> fill_in(Query.text_field("password"), with: root_password) + |> then(fn s -> + s |> find(Query.css("#kc-form-buttons")) + s + end) |> click(Query.css("#kc-login")) |> assert_has( Query.css("h1.f2.f1-m.lh-title.mb1", 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..4b50c0885 --- /dev/null +++ b/e2e/test/e2e/ui/project_creation_test.exs @@ -0,0 +1,136 @@ +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") + session = wait_for(session, Wallaby.Query.css("button.btn.btn-primary"), 30_000) + 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") + + 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 + duplicate_button = Wallaby.Query.button("Make a duplicate project") + + if has?(session, duplicate_button) do + click(session, duplicate_button) + else + session + end + end + + defp wait_for(session, query, timeout_ms, interval_ms \\ 200) + defp wait_for(session, query, timeout_ms, _interval_ms) when timeout_ms <= 0 do + assert_has(session, query) + end + + defp wait_for(session, query, timeout_ms, interval_ms) do + if has?(session, query) do + session + else + Process.sleep(interval_ms) + wait_for(session, query, timeout_ms - interval_ms, interval_ms) + end + end + +end diff --git a/e2e/test/e2e/ui/user_management_test.exs b/e2e/test/e2e/ui/user_management_test.exs new file mode 100644 index 000000000..ed318f712 --- /dev/null +++ b/e2e/test/e2e/ui/user_management_test.exs @@ -0,0 +1,239 @@ +defmodule E2E.UI.UserManagementTest do + use E2E.UI.UserTestCase, async: false + require Logger + + describe "User Management Page" do + setup %{base_url: base_url} do + on_exit(fn -> + {:ok, cleanup_session} = Wallaby.start_session() + cleanup_session = cleanup_session |> E2E.Support.UserAction.login() |> navigate_to_people_page(base_url) + remove_all_users(cleanup_session) + end) + :ok + end + + test "accessing and interacting with the People page", %{session: session, base_url: base_url} do + emails = random_emails(10) + + navigate_to_people_page(session, base_url) + |> then(fn session -> + assert_has(session, Wallaby.Query.css("div.b", text: "People")) + session + end) + |> click(Wallaby.Query.button("Add people")) + |> create_users(emails) + |> click(Wallaby.Query.button("Create Accounts")) + |> then(fn session -> + :timer.sleep(2_000) + session + end) + end + + test "can create a user and log in with it", %{session: session, base_url: base_url, login_url: login_url} do + emails = random_emails(1) + + session = + session + |> navigate_to_people_page(base_url) + |> assert_has(Wallaby.Query.css("div.b", text: "People")) + |> click(Wallaby.Query.button("Add people")) + |> then(fn s -> :timer.sleep(1000); s end) + |> create_users(emails) + |> click(Wallaby.Query.button("Create Accounts")) + |> then(fn s -> :timer.sleep(2000); s end) + + [user_cred] = extract_user_credentials(session) + + {:ok, login_session} = Wallaby.start_session() + + login_session + |> E2E.Support.UserAction.login(login_url, user_cred.email, user_cred.password) + |> E2E.Support.UserAction.change_password(hd(random_emails(1))) + end + + test "can create a user and find them in the people list", %{session: session, base_url: base_url} do + known_email = "knownuser@example.com" + session = + session + |> navigate_to_people_page(base_url) + |> assert_has(Wallaby.Query.css("div.b", text: "People")) + |> click(Wallaby.Query.button("Add people")) + |> then(fn s -> :timer.sleep(1000); s end) + |> create_users([known_email]) + |> click(Wallaby.Query.button("Create Accounts")) + |> then(fn s -> :timer.sleep(2000); s end) + + # Go back to People page if needed + session = navigate_to_people_page(session, base_url) + + # Find the member div containing the known email + find_member_scope_by_email(session, known_email) + |> click(Wallaby.Query.css("button.btn.btn-secondary span", text: "Edit")) + + session + |> then(fn scope -> + assert_has(scope, Wallaby.Query.text("Edit user")) + scope + end) + |> click(Wallaby.Query.button("Reset password")) + |> then(fn scope -> + assert_has(scope, Wallaby.Query.text("Are you sure you want to reset the password?")) + scope + end) + |> click(Wallaby.Query.button("Reset password")) + |> then(fn scope -> + assert_has(scope, Wallaby.Query.text("New temporary password")) + scope + end) + |> then(fn scope -> + # Find the Admin label and click it + admin_label = find(scope, Wallaby.Query.css("label.pointer", text: "Admin")) + Wallaby.Element.click(admin_label) + scope + end) + |> then(fn scope -> + # Zoom out to make sure the button is in viewport + _ = execute_script(scope, "document.body.style.zoom = '0.7';") + scope + end) + |> wait_and_click(Wallaby.Query.button("Save changes"), 1_000) + |> then(fn scope -> + _ = execute_script(scope, "document.body.style.zoom = '1';") + scope + end) + |> wait_for(Wallaby.Query.text("Role successfully assigned"), 2_000) + |> then(fn scope -> + _ = execute_script(scope, "document.body.style.zoom = '0.6';") + take_screenshot(scope, name: "role_assigned") + _ = execute_script(scope, "document.body.style.zoom = '1';") + scope + end) + |> then(fn scope -> + _ = execute_script(scope, "document.body.style.zoom = '0.7';") + scope + end) + |> wait_and_click(Wallaby.Query.button("Cancel"), 1_000) + + # confirm that member is now admin + session + |> wait_for(Wallaby.Query.css("div#members"), 3_000) + |> find_member_scope_by_email(known_email) + |> wait_for(Wallaby.Query.css("span", text: "Admin"), 3_000) + end + end + + @doc """ + Helper function to navigate to the People page + """ + def navigate_to_people_page(session, base_url) do + visit(session, "#{base_url}/people") + end + + defp remove_all_users(session) do + remove_query = Wallaby.Query.css("button.btn.btn-secondary[name=remove-btn]") + do_remove_all_users(session, remove_query) + end + + defp do_remove_all_users(session, query) do + case all(session, query) do + [btn | _] -> + Wallaby.Element.click(btn) + :timer.sleep(500) + do_remove_all_users(session, query) + [] -> + session + end + end + + # Helper: create users by filling in emails and submitting + defp create_users(session, emails) do + Enum.reduce(Enum.with_index(emails, 1), session, fn {email, _i}, session_acc -> + email_fields = all(session_acc, Wallaby.Query.fillable_field("Enter email address")) + email_field_index = length(email_fields) - 1 + updated_session = + if email_field_index >= 0 do + fill_in( + session_acc, + Wallaby.Query.fillable_field("Enter email address", count: :any, at: email_field_index), + with: email + ) + else + fill_in(session_acc, Wallaby.Query.fillable_field("Enter email address"), with: email) + end + :timer.sleep(300) + updated_session + end) + end + + # Helper: extract credentials from confirmation blocks + defp extract_user_credentials(session) do + account_blocks = all(session, Wallaby.Query.css(".email-input-group.mb3")) + {credentials, failures} = + Enum.reduce(account_blocks, {[], 0}, fn block, {acc, fails} -> + try do + email_element = find(block, Wallaby.Query.css(".f4")) + email = Wallaby.Element.text(email_element) + password_element = find(block, Wallaby.Query.css("code.f6")) + password = Wallaby.Element.text(password_element) + {[ %{email: email, password: password} | acc ], fails} + rescue + Wallaby.QueryError -> + {acc, fails + 1} + end + end) + if failures > length(account_blocks)/2 do + flunk("Failed to extract credentials from more than 50% of blocks (#{failures} failures)") + end + Enum.reverse(credentials) + end + + defp find_member_scope_by_email(session, email) do + username = String.split(email, "@") |> hd() + + all(session, Wallaby.Query.css("div#member")) + |> Enum.find(fn div -> + has?(div, Wallaby.Query.link(username)) + end) + |> case do + nil -> + flunk("Could not find member card for #{email}") + + member_div -> + member_div + end + end + + defp random_emails(n) do + random_str = Enum.map(1..5, fn _ -> Enum.random(~c(abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789)) end) |> to_string() + Enum.map(1..n, fn i -> + random_email(random_str, i) + end) + end + # Helper: generate a random email + defp random_email(random_str, n) do + "#{random_str}#{n}@example.com" + end + + defp wait_and_click(scope, query, timeout_ms) do + wait_for(scope, query, timeout_ms) + click(scope, query) + end + + defp wait_for(scope, query, timeout_ms, interval_ms \\ 200) + defp wait_for(scope, query, timeout_ms, _interval_ms) when timeout_ms <= 0 do + if has?(scope, query) do + scope + else + flunk("Timed out waiting for #{inspect(query)}") + end + end + + defp wait_for(scope, query, timeout_ms, interval_ms) do + if has?(scope, query) do + scope + else + Process.sleep(interval_ms) + wait_for(scope, query, timeout_ms - interval_ms, interval_ms) + end + end +end diff --git a/e2e/test/support/ui_test_case.ex b/e2e/test/support/ui_test_case.ex index 0cacc86ae..9c6f91381 100644 --- a/e2e/test/support/ui_test_case.ex +++ b/e2e/test/support/ui_test_case.ex @@ -11,11 +11,11 @@ defmodule E2E.UI.UserTestCase do use ExUnit.CaseTemplate require Wallaby.Browser import Wallaby.Browser + import E2E.Support.UserAction require Logger using do quote do - use ExUnit.Case, async: true use Wallaby.DSL require Wallaby.Browser import Wallaby.Browser @@ -40,29 +40,28 @@ defmodule E2E.UI.UserTestCase do root_password = Application.get_env(:e2e, :semaphore_root_password) organization = Application.get_env(:e2e, :semaphore_organization) + base_url = "https://#{organization}.#{base_domain}" login_url = "https://id.#{base_domain}/login" 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")) + logged_in_session = login(session, login_url, root_email, root_password) - {:ok, session: logged_in_session, organization: organization, base_domain: base_domain} + :timer.sleep(1000) + take_screenshot(logged_in_session, name: "loggedin") + + assert current_url(logged_in_session) == + "https://#{organization}.#{base_domain}/get_started/" + + {:ok, session: logged_in_session, organization: organization, base_domain: base_domain, base_url: base_url, login_url: login_url} 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 + # Attempt to capture some of the page source + Logger.error("Login failed! Current URL: #{current_url(session)}") + html_source = try do session diff --git a/e2e/test/support/user_action.ex b/e2e/test/support/user_action.ex new file mode 100644 index 000000000..94bc23919 --- /dev/null +++ b/e2e/test/support/user_action.ex @@ -0,0 +1,65 @@ +defmodule E2E.Support.UserAction do + require Wallaby.Browser + import Wallaby.Browser + import Wallaby.Query + + def login(session) do + 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) + + login_url = "https://id.#{base_domain}/login" + + login(session, login_url, root_email, root_password) + end + + def login(session, login_url, email, password) do + session + |> visit(login_url) + |> wait_for(css("#kc-form-login"), 3_000) + |> fill_in(text_field("username"), with: email) + |> fill_in(text_field("password"), with: password) + |> wait_for(css("#kc-form-buttons"), 3_000) + |> wait_for(css("#kc-login"), 3_000) + |> with_retry(fn s -> click(s, css("#kc-login")) end) + end + + def change_password(session, password) do + session + |> wait_for(css("#kc-passwd-update-form"), 3_000) + |> fill_in(text_field("password-new"), with: password) + |> fill_in(text_field("password-confirm"), with: password) + |> wait_for(button("Submit"), 3_000) + |> click(button("Submit")) + end + + def wait_for(session, query, timeout_ms, interval_ms \\ 200) + def wait_for(session, query, timeout_ms, _interval_ms) when timeout_ms <= 0 do + assert_has(session, query) + end + + def wait_for(session, query, timeout_ms, interval_ms) do + if has?(session, query) do + session + else + Process.sleep(interval_ms) + wait_for(session, query, timeout_ms - interval_ms, interval_ms) + end + end + + defp with_retry(session, fun, attempts \\ 3) + defp with_retry(session, fun, 0), do: fun.(session) + + defp with_retry(session, fun, attempts) do + fun.(session) + rescue + e in Wallaby.StaleReferenceError -> + Process.sleep(200) + + if attempts > 1 do + with_retry(session, fun, attempts - 1) + else + reraise e, __STACKTRACE__ + end + end +end