Skip to content

feat: Add Playwright E2E tests for role-based navigation#112

Merged
mpaulosky merged 3 commits intomainfrom
squad/109-e2e-role-navigation-tests
Mar 12, 2026
Merged

feat: Add Playwright E2E tests for role-based navigation#112
mpaulosky merged 3 commits intomainfrom
squad/109-e2e-role-navigation-tests

Conversation

@mpaulosky
Copy link
Owner

Summary

Implements Playwright E2E tests that verify login and navigation behavior for each user role.

Closes #109

Changes

New Files

  • PlaywrightFixture.cs - Shared fixture that initializes the Aspire AppHost and Playwright browser
  • PlaywrightCollection.cs - Collection definition for shared fixture
  • Auth0LoginHelper.cs - Helper for Auth0 browser-based OIDC authentication

Navigation Test Files (30 tests total)

  • AdminNavigationTests.cs (6 tests): Admin login, menu visibility, navigation
  • AuthorNavigationTests.cs (5 tests): Author login, menu visibility, permissions
  • UserNavigationTests.cs (5 tests): User login, menu visibility, access
  • UnauthenticatedNavigationTests.cs (7 tests): Login button, protected routes, public access
  • LogoutTests.cs (7 tests): Logout functionality, menu hidden after logout

Configuration

Test credentials are read from environment variables for security:

  • E2E_TEST_ADMIN_EMAIL / E2E_TEST_ADMIN_PASSWORD
  • E2E_TEST_AUTHOR_EMAIL / E2E_TEST_AUTHOR_PASSWORD
  • E2E_TEST_USER_EMAIL / E2E_TEST_USER_PASSWORD

Tests skip gracefully when credentials are not configured.

Tech Stack

  • Playwright for browser automation
  • xUnit v3 with shared fixtures
  • Aspire Hosting Testing for distributed app startup

- Add PlaywrightFixture that initializes Aspire AppHost and Playwright browser
- Add PlaywrightCollection for shared test fixture across E2E tests
- Add Auth0LoginHelper for browser-based OIDC authentication flow
- Add AdminNavigationTests (6 tests): login, menu visibility, navigation to all admin pages
- Add AuthorNavigationTests (5 tests): login, menu visibility, navigation permissions
- Add UserNavigationTests (5 tests): login, menu visibility, protected content access
- Add UnauthenticatedNavigationTests (7 tests): login button, protected route redirect
- Add LogoutTests (7 tests): logout functionality, menu hidden after logout

Test credentials are read from environment variables for security:
- E2E_TEST_ADMIN_EMAIL/PASSWORD
- E2E_TEST_AUTHOR_EMAIL/PASSWORD
- E2E_TEST_USER_EMAIL/PASSWORD

Closes #109

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 12, 2026 07:10
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds Playwright-based end-to-end coverage to verify Auth0 login and role-based navigation behavior in the IssueManager web UI, using an Aspire-hosted AppHost started once per test collection.

Changes:

  • Introduces a shared PlaywrightFixture + xUnit collection for running E2E browser tests against the Aspire-hosted app.
  • Adds an Auth0LoginHelper for browser-driven Auth0 OIDC login/logout using env-var credentials.
  • Adds role-focused navigation and logout E2E test suites (admin/author/user/unauthenticated).

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
tests/AppHost.Tests.E2E/Fixtures/PlaywrightFixture.cs Starts Aspire AppHost, waits for web resource readiness, and provides Playwright browser/page creation.
tests/AppHost.Tests.E2E/Fixtures/PlaywrightCollection.cs Defines the shared Playwright E2E test collection.
tests/AppHost.Tests.E2E/Helpers/Auth0LoginHelper.cs Implements Auth0 login/logout helpers for Playwright tests, reading credentials from environment variables.
tests/AppHost.Tests.E2E/Navigation/AdminNavigationTests.cs Admin role E2E navigation assertions.
tests/AppHost.Tests.E2E/Navigation/AuthorNavigationTests.cs Author role E2E navigation assertions (including admin-menu exclusion).
tests/AppHost.Tests.E2E/Navigation/UserNavigationTests.cs User role E2E navigation assertions (including admin-menu exclusion).
tests/AppHost.Tests.E2E/Navigation/UnauthenticatedNavigationTests.cs Anonymous-user E2E coverage for login button, protected-route redirects, and menu visibility.
tests/AppHost.Tests.E2E/Navigation/LogoutTests.cs E2E coverage for logout behavior and post-logout access/menu state.
tests/AppHost.Tests.E2E/GlobalUsings.cs Adds global usings for Playwright and the new helper namespace.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +117 to +121
await Auth0LoginHelper.LoginAsync(
page,
fixture.WebUrl,
credentials.Value.Email,
credentials.Value.Password);
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test ignores the LoginAsync result and immediately asserts that admin-only links are not visible. If the login failed or the authenticated nav hasn't rendered yet, these negative assertions can pass incorrectly. Wait for a logged-in indicator (e.g., logout link visible) before asserting admin links are hidden.

Copilot uses AI. Check for mistakes.
{
// Act
await page.GotoAsync(fixture.WebUrl, new PageGotoOptions { WaitUntil = WaitUntilState.NetworkIdle });

Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test asserts admin-only links are not visible immediately after navigation, but it doesn't first wait for a stable unauthenticated UI indicator (e.g., login link visible). If the nav/auth state is still loading, these negative checks can pass incorrectly. Wait for the login button (or another known element) before asserting admin links are hidden.

Suggested change
// Wait for a stable unauthenticated UI indicator (login button visible)
var loginButton = page.GetByRole(AriaRole.Button, new PageGetByRoleOptions { Name = "Log in" });
await loginButton.WaitForAsync(new LocatorWaitForOptions
{
State = WaitForSelectorState.Visible,
Timeout = 10000
});

Copilot uses AI. Check for mistakes.
// Act - Log out
await Auth0LoginHelper.LogoutAsync(page, fixture.WebUrl);
await page.GotoAsync(fixture.WebUrl, new PageGotoOptions { WaitUntil = WaitUntilState.NetworkIdle });

Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test checks that the "New Issue" link is hidden right after logout + navigation, but it doesn't first wait for the logged-out UI state to be confirmed (e.g., login button visible / logout link hidden). Without that, the negative assertion can be flaky or even pass before the UI updates. Add an explicit wait/assert for logged-out state before checking link visibility.

Suggested change
// Wait for logged-out UI state (e.g., login link visible) before asserting protected content is hidden
var loginLink = page.Locator("a[href='/login']");
await loginLink.WaitForAsync(new LocatorWaitForOptions
{
State = WaitForSelectorState.Visible,
Timeout = 5000
});

Copilot uses AI. Check for mistakes.
namespace AppHost.Tests.E2E.Fixtures;

/// <summary>
/// Playwright fixture that extends DistributedApplicationFixture.
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The XML doc comment says this fixture "extends DistributedApplicationFixture", but it doesn't inherit from that type (it implements IAsyncLifetime and has its own Aspire startup). Update the summary to avoid misleading future readers.

Suggested change
/// Playwright fixture that extends DistributedApplicationFixture.
/// Playwright fixture that hosts the Aspire AppHost for end-to-end tests.

Copilot uses AI. Check for mistakes.
Comment on lines +103 to +109
"web",
KnownResourceStates.Running,
CancellationToken.None).WaitAsync(TimeSpan.FromMinutes(3));

// Get the web app URL
_webUrl = _app.GetEndpoint("web", "https")?.ToString()
?? _app.GetEndpoint("web", "http")?.ToString();
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PlaywrightFixture waits for resource "web", but the Web project is registered in the AppHost with the resource name Website ("WebApp") (see src/AppHost/Program.cs). With the current value, this wait will never complete and the fixture will be marked unavailable. Use the actual resource name (preferably the shared Website constant).

Suggested change
"web",
KnownResourceStates.Running,
CancellationToken.None).WaitAsync(TimeSpan.FromMinutes(3));
// Get the web app URL
_webUrl = _app.GetEndpoint("web", "https")?.ToString()
?? _app.GetEndpoint("web", "http")?.ToString();
"Website",
KnownResourceStates.Running,
CancellationToken.None).WaitAsync(TimeSpan.FromMinutes(3));
// Get the web app URL
_webUrl = _app.GetEndpoint("Website", "https")?.ToString()
?? _app.GetEndpoint("Website", "http")?.ToString();

Copilot uses AI. Check for mistakes.
Comment on lines +107 to +110
// Get the web app URL
_webUrl = _app.GetEndpoint("web", "https")?.ToString()
?? _app.GetEndpoint("web", "http")?.ToString();

Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to the resource-name mismatch: _app.GetEndpoint("web", ...) uses the same incorrect resource name. This will yield a null endpoint and cause WebUrl to be unavailable even if the app started. Use the same resource name the AppHost registers for the Web project (e.g., Website / "WebApp").

Copilot uses AI. Check for mistakes.
Comment on lines +119 to +123
await Auth0LoginHelper.LoginAsync(
page,
fixture.WebUrl,
credentials.Value.Email,
credentials.Value.Password);
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test logs in and then proceeds to role-based nav assertions without first verifying the authenticated UI has fully rendered (and it also ignores the LoginAsync return value). If the nav/auth state is still loading, the negative assertions can become false positives. Add an explicit wait/assert for logged-in state (e.g., logout link visible) before checking admin links are hidden.

Copilot uses AI. Check for mistakes.
Comment on lines +98 to +102
await Auth0LoginHelper.LogoutAsync(page, fixture.WebUrl);
await page.GotoAsync(fixture.WebUrl, new PageGotoOptions { WaitUntil = WaitUntilState.NetworkIdle });

// Assert - Admin menu items should be hidden
var categoriesLink = page.Locator("a[href='/categories']");
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After LogoutAsync, this test immediately asserts that admin links are hidden. If the UI/auth state hasn't finished updating yet, these negative assertions can pass/fail nondeterministically. Add an explicit wait/assert for logged-out state (e.g., login button visible / logout link hidden) before checking that admin menu items are not visible.

Copilot uses AI. Check for mistakes.
Comment on lines +77 to +82
await Auth0LoginHelper.LoginAsync(
page,
fixture.WebUrl,
credentials.Value.Email,
credentials.Value.Password);

Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test proceeds right after LoginAsync without checking the returned success value or waiting for a logged-in indicator (e.g., logout link visible). If login fails or the authenticated nav hasn't rendered yet, the menu visibility assertions can be flaky or misleading. Consider asserting login success and/or waiting for logged-in state before asserting menu items.

Suggested change
await Auth0LoginHelper.LoginAsync(
page,
fixture.WebUrl,
credentials.Value.Email,
credentials.Value.Password);
var loginSuccess = await Auth0LoginHelper.LoginAsync(
page,
fixture.WebUrl,
credentials.Value.Email,
credentials.Value.Password);
loginSuccess.Should().BeTrue("Admin should be able to log in successfully before checking navigation menu");
(await Auth0LoginHelper.IsLoggedInAsync(page)).Should().BeTrue("Page should show logged-in state before checking navigation menu");

Copilot uses AI. Check for mistakes.
Comment on lines +53 to +89
try
{
// Navigate to the login endpoint which triggers Auth0 redirect
await page.GotoAsync($"{baseUrl.TrimEnd('/')}/auth/login", new PageGotoOptions
{
WaitUntil = WaitUntilState.NetworkIdle,
Timeout = timeout
});

// Wait for Auth0 login page
await page.WaitForURLAsync("**/u/login**", new PageWaitForURLOptions { Timeout = timeout });

// Fill in the Auth0 Universal Login form
await page.FillAsync("input[name='username'], input[name='email'], input#username, input[type='email']", email);
await page.FillAsync("input[name='password'], input#password, input[type='password']", password);

// Click the submit button
await page.ClickAsync("button[type='submit'], button[name='action'], button[data-action-button-primary='true']");

// Wait for redirect back to our app (should redirect to home or return URL)
await page.WaitForURLAsync(url =>
url.Contains(new Uri(baseUrl).Host) && !url.Contains("auth0.com"),
new PageWaitForURLOptions { Timeout = timeout });

// Wait for the page to stabilize
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);

return true;
}
catch (TimeoutException)
{
return false;
}
catch (PlaywrightException)
{
return false;
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LoginAsync catches exceptions and returns false, which hides the underlying reason for the login failure and makes debugging test failures harder (especially where callers don't check the returned bool). Consider failing fast by rethrowing/wrapping the exception, or returning a richer result that includes the error details for assertions/logging.

Suggested change
try
{
// Navigate to the login endpoint which triggers Auth0 redirect
await page.GotoAsync($"{baseUrl.TrimEnd('/')}/auth/login", new PageGotoOptions
{
WaitUntil = WaitUntilState.NetworkIdle,
Timeout = timeout
});
// Wait for Auth0 login page
await page.WaitForURLAsync("**/u/login**", new PageWaitForURLOptions { Timeout = timeout });
// Fill in the Auth0 Universal Login form
await page.FillAsync("input[name='username'], input[name='email'], input#username, input[type='email']", email);
await page.FillAsync("input[name='password'], input#password, input[type='password']", password);
// Click the submit button
await page.ClickAsync("button[type='submit'], button[name='action'], button[data-action-button-primary='true']");
// Wait for redirect back to our app (should redirect to home or return URL)
await page.WaitForURLAsync(url =>
url.Contains(new Uri(baseUrl).Host) && !url.Contains("auth0.com"),
new PageWaitForURLOptions { Timeout = timeout });
// Wait for the page to stabilize
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
return true;
}
catch (TimeoutException)
{
return false;
}
catch (PlaywrightException)
{
return false;
}
// Navigate to the login endpoint which triggers Auth0 redirect
await page.GotoAsync($"{baseUrl.TrimEnd('/')}/auth/login", new PageGotoOptions
{
WaitUntil = WaitUntilState.NetworkIdle,
Timeout = timeout
});
// Wait for Auth0 login page
await page.WaitForURLAsync("**/u/login**", new PageWaitForURLOptions { Timeout = timeout });
// Fill in the Auth0 Universal Login form
await page.FillAsync("input[name='username'], input[name='email'], input#username, input[type='email']", email);
await page.FillAsync("input[name='password'], input#password, input[type='password']", password);
// Click the submit button
await page.ClickAsync("button[type='submit'], button[name='action'], button[data-action-button-primary='true']");
// Wait for redirect back to our app (should redirect to home or return URL)
await page.WaitForURLAsync(url =>
url.Contains(new Uri(baseUrl).Host) && !url.Contains("auth0.com"),
new PageWaitForURLOptions { Timeout = timeout });
// Wait for the page to stabilize
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
return true;

Copilot uses AI. Check for mistakes.
@github-actions
Copy link

github-actions bot commented Mar 12, 2026

Test Results Summary

  6 files   -  1   33 suites   - 5   13s ⏱️ -1s
835 tests  - 18  835 ✅  - 18  0 💤 ±0  0 ❌ ±0 
846 runs   - 18  846 ✅  - 18  0 💤 ±0  0 ❌ ±0 

Results for commit ceb1bf4. ± Comparison against base commit fde78b0.

This pull request removes 18 tests.
AddConnectionString CanBeInvoked
AddConnectionString ResourceIsRegistered
AddConnectionString UsesConfiguredDatabaseName
AddRedisServices CanBeInvoked
AddRedisServices RegistersResourceWithCorrectName
AddServiceDefaults ShouldRegisterHealthChecks
AddServiceDefaults ShouldRegisterOpenTelemetry
AddServiceDefaults ShouldRegisterSelfHealthCheck
AddServiceDefaults ShouldRegisterServiceDiscovery
AddServiceDefaults WithEmptyOtlpEndpoint ShouldNotConfigureOtlpExporter
…

♻️ This comment has been updated with latest results.

@codecov
Copy link

codecov bot commented Mar 12, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 55.52%. Comparing base (fde78b0) to head (ceb1bf4).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #112   +/-   ##
=======================================
  Coverage   55.52%   55.52%           
=======================================
  Files         124      124           
  Lines        2826     2826           
  Branches      313      313           
=======================================
  Hits         1569     1569           
  Misses       1027     1027           
  Partials      230      230           
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

- Fix PlaywrightFixture to use Website constant instead of hardcoded 'web'
- Update PlaywrightFixture doc comment to not reference non-existent class
- Add login verification before negative assertions in test methods
- Add wait for unauthenticated state (login button visible) before asserting
  menu items are hidden in UnauthenticatedNavigationTests and LogoutTests

Addresses Copilot reviewer feedback on PR #112.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mpaulosky
Copy link
Owner Author

PR Review Feedback Addressed ✅

I've applied the following fixes based on the Copilot code reviewer feedback:

Critical Bug Fixed

  • PlaywrightFixture resource name: Changed from hardcoded \web\ to \Website\ constant (\WebApp) - this was a critical bug that would have caused tests to timeout waiting for a non-existent resource

Test Robustness Improvements

  • Added login verification (\IsLoggedInAsync()) before negative assertions in:
    • \AdminNavigationTests.cs\ - Admin_SeesAllAdminMenuItems test
    • \AuthorNavigationTests.cs\ - Author_DoesNotSeeAdminOnlyMenuItems test
    • \UserNavigationTests.cs\ - User_DoesNotSeeAdminOnlyMenuItems test
  • Added wait for login button visible before negative assertions in:
    • \UnauthenticatedNavigationTests.cs\ - Unauthenticated_DoesNotSeeAdminOnlyMenuItems test
    • \LogoutTests.cs\ - Admin_AfterLogoutMenuItemsAreHidden test
    • \LogoutTests.cs\ - Author_AfterLogoutMenuItemsAreHidden test

Documentation Fixed

  • Updated PlaywrightFixture XML doc comment to remove incorrect claim about extending \DistributedApplicationFixture\

CI Status

  • ✅ 23 checks passed
  • ⏭️ 2 E2E checks cancelled (expected - Docker/Playwright not available in CI)

Ready for merge when you're ready! 🚀

@mpaulosky mpaulosky merged commit b49f1b3 into main Mar 12, 2026
23 of 25 checks passed
@mpaulosky mpaulosky deleted the squad/109-e2e-role-navigation-tests branch March 12, 2026 08:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Sprint 3] Implement Role-Based Navigation E2E Tests

2 participants