-
Notifications
You must be signed in to change notification settings - Fork 0
test(e2e): Add Playwright E2E tests for Issues CRUD and archive prerequisites (#127) #142
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
65aff3f
9926bc4
cbe1b5a
baa1821
9703e45
fa68719
fe84120
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// E2E tests for Issues CRUD and archive flow. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Verifies that Admin users can navigate to Issues pages, create issues, filter, and access archive prerequisites. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [ExcludeFromCodeCoverage] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [Collection("PlaywrightE2E")] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public class IssuesCrudFlowTests(PlaywrightFixture fixture) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private const string AdminRole = "ADMIN"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Verifies that an Admin user can navigate to the Issues list page. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [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); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+41
to
+44
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| page, | |
| fixture.WebUrl, | |
| credentials.Value.Email, | |
| credentials.Value.Password); | |
| page, | |
| fixture.WebUrl, | |
| credentials.Value.Email, | |
| credentials.Value.Password); |
Copilot
AI
Apr 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the create flow, the test only asserts that navigation moved off "/create" and the URL contains "/issues". The UI currently navigates to "/issues" even if the API create call fails (IssueApiClient.CreateAsync returns null on non-2xx and CreateIssuePage navigates regardless), so this test can pass without actually creating an issue. Add an assertion that the created title appears in the issues table (and consider using a unique title to avoid collisions).
Check warning on line 185 in tests/AppHost.Tests.E2E/Issues/IssuesCrudFlowTests.cs
GitHub Actions / AppHost.Tests.E2E
'LocatorIsVisibleOptions.Timeout' is obsolete
Copilot
AI
Apr 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The filter scenario currently just types into a generic "search" selector and asserts the input contains text, but it never triggers filtering (IssuesPage applies filters when clicking the "Search" button / LoadIssues). This can pass even if filtering is broken. Use the stable selectors (#search, #status-filter, #category-filter), click the "Search" button, and assert that the visible rows match the filter; also cover the "Clear" button restoring results.
| // 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"); | |
| } | |
| var searchInput = page.Locator("#search"); | |
| var statusFilter = page.Locator("#status-filter"); | |
| var categoryFilter = page.Locator("#category-filter"); | |
| var searchButton = page.GetByRole(AriaRole.Button, new() { Name = "Search" }); | |
| var clearButton = page.GetByRole(AriaRole.Button, new() { Name = "Clear" }); | |
| var issueRows = page.Locator("tbody tr"); | |
| (await searchInput.IsVisibleAsync(new LocatorIsVisibleOptions { Timeout = 5000 })) | |
| .Should() | |
| .BeTrue("search input should be visible on the Issues page"); | |
| (await statusFilter.IsVisibleAsync(new LocatorIsVisibleOptions { Timeout = 5000 })) | |
| .Should() | |
| .BeTrue("status filter should be visible on the Issues page"); | |
| (await categoryFilter.IsVisibleAsync(new LocatorIsVisibleOptions { Timeout = 5000 })) | |
| .Should() | |
| .BeTrue("category filter should be visible on the Issues page"); | |
| (await searchButton.IsVisibleAsync(new LocatorIsVisibleOptions { Timeout = 5000 })) | |
| .Should() | |
| .BeTrue("Search button should be visible on the Issues page"); | |
| (await clearButton.IsVisibleAsync(new LocatorIsVisibleOptions { Timeout = 5000 })) | |
| .Should() | |
| .BeTrue("Clear button should be visible on the Issues page"); | |
| var initialRowCount = await issueRows.CountAsync(); | |
| if (initialRowCount > 0) | |
| { | |
| var firstRowText = (await issueRows.First.InnerTextAsync()).Trim(); | |
| firstRowText.Should().NotBeNullOrWhiteSpace("the first visible issue row should contain searchable text"); | |
| var searchTerm = firstRowText | |
| .Split(new[] { ' ', '\t', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) | |
| .FirstOrDefault(token => token.Length >= 3) | |
| ?? firstRowText; | |
| await searchInput.FillAsync(searchTerm); | |
| await searchButton.ClickAsync(); | |
| await page.WaitForLoadStateAsync(LoadState.NetworkIdle); | |
| var filteredRowCount = await issueRows.CountAsync(); | |
| filteredRowCount.Should().BeGreaterThan(0, "searching for text from an existing row should return at least one visible result"); | |
| filteredRowCount.Should().BeLessOrOrEqualTo(initialRowCount, "filtering should not increase the number of visible results"); | |
| var filteredRowsText = await issueRows.AllInnerTextsAsync(); | |
| filteredRowsText.Should().OnlyContain( | |
| row => row.Contains(searchTerm, StringComparison.OrdinalIgnoreCase), | |
| "every visible row should match the applied search term after clicking Search"); | |
| } | |
| else | |
| { | |
| const string missingSearchTerm = "no-matching-issue-term"; | |
| await searchInput.FillAsync(missingSearchTerm); | |
| await searchButton.ClickAsync(); | |
| await page.WaitForLoadStateAsync(LoadState.NetworkIdle); | |
| (await issueRows.CountAsync()).Should().Be(0, "searching with no issues loaded should still leave zero visible rows"); | |
| } | |
| await clearButton.ClickAsync(); | |
| await page.WaitForLoadStateAsync(LoadState.NetworkIdle); | |
| (await searchInput.InputValueAsync()).Should().BeEmpty("Clear should reset the search text"); | |
| (await statusFilter.InputValueAsync()).Should().BeEmpty("Clear should reset the status filter"); | |
| (await categoryFilter.InputValueAsync()).Should().BeEmpty("Clear should reset the category filter"); | |
| (await issueRows.CountAsync()).Should().Be(initialRowCount, "Clear should restore the original visible results"); |
Copilot
AI
Apr 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The archive button selector is unlikely to match the actual UI: CategoriesPage renders the archive control as an icon-only RadzenButton with an id like "archive-{cat.Id}" (no visible text "Archive"). Also, the test currently passes even when no archive UI is present. Prefer locating by the known id prefix (e.g., "[id^='archive-']") and, if the grid has rows (or after seeding a category), assert at least one archive button is visible.
| // 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 | |
| // Archive controls are rendered as icon-only buttons with ids like "archive-{cat.Id}". | |
| var archiveButtonSelector = "[id^='archive-']"; | |
| var archiveButtons = page.Locator(archiveButtonSelector); | |
| // If the categories grid has rows, at least one archive button should be present and visible. | |
| var categoryRows = page.Locator("table tbody tr, .rz-data-grid-data tr, .rz-datatable-data tr"); | |
| var categoryRowCount = await categoryRows.CountAsync(); | |
| var archiveButtonCount = await archiveButtons.CountAsync(); | |
| if (categoryRowCount > 0) | |
| { | |
| archiveButtonCount.Should().BeGreaterThan(0, "archive buttons should be rendered for listed categories"); | |
| (await archiveButtons.First.IsVisibleAsync()).Should().BeTrue("at least one archive button should be visible"); | |
| } | |
| else | |
| { | |
| archiveButtonCount.Should().Be(0, "archive buttons should not be rendered when no categories are listed"); | |
| // No categories listed - verify the page still loaded successfully. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PR title/description calls this a comprehensive Issues "CRUD" E2E suite, but this file currently doesn’t include assertions for reading issue detail, editing/updating, or any delete/archive action (it mainly validates navigation and minimal UI presence). Either expand the scenarios to cover the remaining CRUD/archive steps from Issue #127, or adjust the PR title/description to match what’s actually tested.