diff --git a/.squad/agents/legolas/history.md b/.squad/agents/legolas/history.md index 0abdccd..2fcf56e 100644 --- a/.squad/agents/legolas/history.md +++ b/.squad/agents/legolas/history.md @@ -72,3 +72,15 @@ Frontend Developer on IssueManager (.NET 10, Blazor Interactive Server Rendering - Tailwind optional wasm helper entries updated as expected lockfile churn - Merged to main; created reusable `.squad/skills/dependabot-lockfile-review/` skill for future Web dependency reviews - Decision recorded in `.squad/decisions.md` + +### 2026-04-12: E2E Playwright Tests for Issues CRUD (PR #142) +- Implemented 6 E2E test scenarios in `tests/AppHost.Tests.E2E/Issues/IssuesCrudFlowTests.cs` +- Test patterns: Use `[Collection("PlaywrightE2E")]` attribute, `PlaywrightFixture fixture` constructor parameter, and `try/finally` with page context cleanup +- All tests check fixture availability and test credentials before running +- Defensive testing: Assertions check for UI element existence, not specific data (works with empty database) +- Route pattern: `/issues`, `/issues/create`, `/issues/{Id}/edit`, `/categories` +- Auth pattern: Use `Auth0LoginHelper.GetTestCredentials("ADMIN")` and `Auth0LoginHelper.LoginAsync(page, testBaseUrl, username, password, 30000)` +- Timeout strategy: Most WaitFor operations use 15000ms, Auth0LoginHelper uses 30000ms default +- FluentAssertions style: `.Should().BeTrue()`, `.Should().Contain()`, `.Should().NotBeNull()` +- GlobalUsings: E2E project has global usings for Xunit, FluentAssertions, Playwright, fixtures, and helpers — no additional using statements needed +- Build verification: Tests build successfully with warnings consistent with existing E2E tests diff --git a/tests/AppHost.Tests.E2E/Issues/IssuesCrudFlowTests.cs b/tests/AppHost.Tests.E2E/Issues/IssuesCrudFlowTests.cs new file mode 100644 index 0000000..65c22db --- /dev/null +++ b/tests/AppHost.Tests.E2E/Issues/IssuesCrudFlowTests.cs @@ -0,0 +1,291 @@ +// ============================================ +// Copyright (c) 2026. All rights reserved. +// File Name : IssuesCrudFlowTests.cs +// Company : mpaulosky +// Author : Matthew Paulosky +// Solution Name : IssueManager +// Project Name : AppHost.Tests.E2E +// ============================================= + +namespace AppHost.Tests.E2E.Issues; + +/// +/// E2E tests for Issues CRUD and archive flow. +/// Verifies that Admin users can navigate to Issues pages, create issues, filter, and access archive prerequisites. +/// +[ExcludeFromCodeCoverage] +[Collection("PlaywrightE2E")] +public class IssuesCrudFlowTests(PlaywrightFixture fixture) +{ + private const string AdminRole = "ADMIN"; + + /// + /// Verifies that an Admin user can navigate to the Issues list page. + /// + [Fact] + public async Task Admin_CanNavigateToIssuesPage() + { + // Arrange + if (!fixture.IsAvailable) + throw SkipException.ForSkip(fixture.UnavailableReason ?? "Playwright fixture unavailable"); + + var credentials = Auth0LoginHelper.GetTestCredentials(AdminRole); + if (credentials is null) + throw SkipException.ForSkip("Admin test credentials not configured (E2E_TEST_ADMIN_EMAIL/PASSWORD)"); + + var page = await fixture.NewPageAsync(); + + try + { + await Auth0LoginHelper.LoginAsync( + page, + fixture.WebUrl, + credentials.Value.Email, + credentials.Value.Password); + + // Act + await page.GotoAsync($"{fixture.WebUrl}/issues", new PageGotoOptions { WaitUntil = WaitUntilState.NetworkIdle }); + + // Assert + page.Url.Should().Contain("/issues", "should navigate to Issues page"); + } + finally + { + await page.Context.CloseAsync(); + } + } + + /// + /// Verifies that an Admin user can navigate to the Create Issue page. + /// + [Fact] + public async Task Admin_CanNavigateToCreateIssuePage() + { + // Arrange + if (!fixture.IsAvailable) + throw SkipException.ForSkip(fixture.UnavailableReason ?? "Playwright fixture unavailable"); + + var credentials = Auth0LoginHelper.GetTestCredentials(AdminRole); + if (credentials is null) + throw SkipException.ForSkip("Admin test credentials not configured (E2E_TEST_ADMIN_EMAIL/PASSWORD)"); + + var page = await fixture.NewPageAsync(); + + try + { + await Auth0LoginHelper.LoginAsync( + page, + fixture.WebUrl, + credentials.Value.Email, + credentials.Value.Password); + + // Act + await page.GotoAsync($"{fixture.WebUrl}/issues/create", new PageGotoOptions { WaitUntil = WaitUntilState.NetworkIdle }); + + // Assert + page.Url.Should().Contain("/issues/create", "should navigate to Create Issue page"); + + // Verify form is present + var formLocator = page.Locator("form, input, button[type='submit']"); + (await formLocator.First.IsVisibleAsync()).Should().BeTrue("form element should be visible on create page"); + } + finally + { + await page.Context.CloseAsync(); + } + } + + /// + /// Verifies that an Admin user can create and view an issue via the form. + /// + [Fact] + public async Task Admin_CanCreateAndViewIssue() + { + // Arrange + if (!fixture.IsAvailable) + throw SkipException.ForSkip(fixture.UnavailableReason ?? "Playwright fixture unavailable"); + + var credentials = Auth0LoginHelper.GetTestCredentials(AdminRole); + if (credentials is null) + throw SkipException.ForSkip("Admin test credentials not configured (E2E_TEST_ADMIN_EMAIL/PASSWORD)"); + + var page = await fixture.NewPageAsync(); + + try + { + await Auth0LoginHelper.LoginAsync( + page, + fixture.WebUrl, + credentials.Value.Email, + credentials.Value.Password); + + // Act - Navigate to create page + await page.GotoAsync($"{fixture.WebUrl}/issues/create", new PageGotoOptions { WaitUntil = WaitUntilState.NetworkIdle }); + + // Wait for form to load + await page.WaitForSelectorAsync("form", new PageWaitForSelectorOptions { Timeout = 15000 }); + + // Fill in the Title field + var titleSelector = "input[id='title'], input[name='title'], input[placeholder*='title' i], input[aria-label*='title' i]"; + await page.FillAsync(titleSelector, "E2E Test Issue"); + + // Fill in the Description field + var descriptionSelector = "textarea[id='description'], textarea[name='description'], textarea[placeholder*='description' i]"; + await page.FillAsync(descriptionSelector, "This is an E2E test issue created by Playwright"); + + // Click submit button + await page.ClickAsync("button[type='submit']"); + + // Wait for navigation away from create page + await page.WaitForURLAsync(url => !url.Contains("/create"), new PageWaitForURLOptions { Timeout = 15000 }); + + // Assert - Should redirect to either list or detail page + page.Url.Should().Contain("/issues", "should redirect to issues page after creation"); + } + finally + { + await page.Context.CloseAsync(); + } + } + + /// + /// Verifies that the Issues page has filter/search UI when issues exist. + /// + [Fact] + public async Task Admin_CanFilterIssuesPage() + { + // Arrange + if (!fixture.IsAvailable) + throw SkipException.ForSkip(fixture.UnavailableReason ?? "Playwright fixture unavailable"); + + var credentials = Auth0LoginHelper.GetTestCredentials(AdminRole); + if (credentials is null) + throw SkipException.ForSkip("Admin test credentials not configured (E2E_TEST_ADMIN_EMAIL/PASSWORD)"); + + var page = await fixture.NewPageAsync(); + + try + { + await Auth0LoginHelper.LoginAsync( + page, + fixture.WebUrl, + credentials.Value.Email, + credentials.Value.Password); + + // Act + await page.GotoAsync($"{fixture.WebUrl}/issues", new PageGotoOptions { WaitUntil = WaitUntilState.NetworkIdle }); + + // Assert - Page should load + page.Url.Should().Contain("/issues", "should navigate to Issues page"); + + // Look for search/filter input (defensive - may not exist if no issues) + var searchSelector = "input[type='search'], input[placeholder*='search' i], input[aria-label*='search' i]"; + var searchInput = page.Locator(searchSelector); + + if (await searchInput.IsVisibleAsync(new LocatorIsVisibleOptions { Timeout = 5000 })) + { + // If filter UI exists, test it + await searchInput.FillAsync("test"); + (await searchInput.InputValueAsync()).Should().Contain("test", "search input should accept text"); + } + else + { + // Filter UI not present (likely no issues) - just verify page loaded + var pageContent = page.Locator("h1, h2, h3"); + (await pageContent.First.IsVisibleAsync()).Should().BeTrue("page should have loaded with heading visible"); + } + } + finally + { + await page.Context.CloseAsync(); + } + } + + /// + /// Verifies that an Admin user can navigate to the Categories page (archive prerequisite). + /// + [Fact] + public async Task Admin_CanNavigateToCategoriesPage() + { + // Arrange + if (!fixture.IsAvailable) + throw SkipException.ForSkip(fixture.UnavailableReason ?? "Playwright fixture unavailable"); + + var credentials = Auth0LoginHelper.GetTestCredentials(AdminRole); + if (credentials is null) + throw SkipException.ForSkip("Admin test credentials not configured (E2E_TEST_ADMIN_EMAIL/PASSWORD)"); + + var page = await fixture.NewPageAsync(); + + try + { + await Auth0LoginHelper.LoginAsync( + page, + fixture.WebUrl, + credentials.Value.Email, + credentials.Value.Password); + + // Act + await page.GotoAsync($"{fixture.WebUrl}/categories", new PageGotoOptions { WaitUntil = WaitUntilState.NetworkIdle }); + + // Assert + page.Url.Should().Contain("/categories", "should navigate to Categories page"); + } + finally + { + await page.Context.CloseAsync(); + } + } + + /// + /// Verifies that the Categories page shows archive UI for Admin users. + /// + [Fact] + public async Task Admin_CanViewCategoriesForArchive() + { + // Arrange + if (!fixture.IsAvailable) + throw SkipException.ForSkip(fixture.UnavailableReason ?? "Playwright fixture unavailable"); + + var credentials = Auth0LoginHelper.GetTestCredentials(AdminRole); + if (credentials is null) + throw SkipException.ForSkip("Admin test credentials not configured (E2E_TEST_ADMIN_EMAIL/PASSWORD)"); + + var page = await fixture.NewPageAsync(); + + try + { + await Auth0LoginHelper.LoginAsync( + page, + fixture.WebUrl, + credentials.Value.Email, + credentials.Value.Password); + + // Act + await page.GotoAsync($"{fixture.WebUrl}/categories", new PageGotoOptions { WaitUntil = WaitUntilState.NetworkIdle }); + + // Assert - Page should load + page.Url.Should().Contain("/categories", "should navigate to Categories page"); + + // Look for archive button (defensive - may not exist if no categories) + var archiveButtonSelector = "button:has-text('Archive'), button[aria-label*='archive' i], button[title*='archive' i]"; + var archiveButton = page.Locator(archiveButtonSelector); + + if (await archiveButton.IsVisibleAsync(new LocatorIsVisibleOptions { Timeout = 5000 })) + { + // Archive button found - verify it's visible + (await archiveButton.First.IsVisibleAsync()).Should().BeTrue("archive button should be visible"); + } + else + { + // No archive button (likely no categories) - just verify page loaded + var pageHeading = page.Locator("h1, h2, h3"); + (await pageHeading.First.IsVisibleAsync()).Should().BeTrue("page should have loaded with heading visible"); + } + } + finally + { + await page.Context.CloseAsync(); + } + } +}