Skip to content

Commit 989a2bb

Browse files
csharpfritzCopilot
andcommitted
test: Add WingtipToys Playwright acceptance tests
Adds end-to-end acceptance test project for migrated WingtipToys apps: Navigation Tests: - Home page loads successfully - About, Contact, Products navbar links work - Shopping Cart, Register, Login links navigate correctly Shopping Cart Tests: - Product list displays products - Add item to cart via product details - Update cart quantity - Remove item from cart Authentication Tests: - Register page has expected form fields (email, password, confirm) - Login page has expected form fields (email, password) - Register -> Login end-to-end flow Configure via WINGTIPTOYS_BASE_URL env var (defaults to https://localhost:5001). Uses Playwright + xUnit, targets net10.0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 7b88cd3 commit 989a2bb

9 files changed

Lines changed: 527 additions & 0 deletions

BlazorMeetsWebForms.sln

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72
4242
EndProject
4343
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazorWebFormsComponents.Analyzers", "src\BlazorWebFormsComponents.Analyzers\BlazorWebFormsComponents.Analyzers.csproj", "{E12935A8-C95C-4D67-A24B-A984A99AFE5F}"
4444
EndProject
45+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WingtipToys.AcceptanceTests", "src\WingtipToys.AcceptanceTests\WingtipToys.AcceptanceTests.csproj", "{70142BE9-A3B0-4E6E-9A8B-0F20EADB875D}"
46+
EndProject
4547
Global
4648
GlobalSection(SolutionConfigurationPlatforms) = preSolution
4749
Debug|Any CPU = Debug|Any CPU
@@ -177,6 +179,24 @@ Global
177179
{E12935A8-C95C-4D67-A24B-A984A99AFE5F}.WebForms|x64.Build.0 = Debug|Any CPU
178180
{E12935A8-C95C-4D67-A24B-A984A99AFE5F}.WebForms|x86.ActiveCfg = Debug|Any CPU
179181
{E12935A8-C95C-4D67-A24B-A984A99AFE5F}.WebForms|x86.Build.0 = Debug|Any CPU
182+
{70142BE9-A3B0-4E6E-9A8B-0F20EADB875D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
183+
{70142BE9-A3B0-4E6E-9A8B-0F20EADB875D}.Debug|Any CPU.Build.0 = Debug|Any CPU
184+
{70142BE9-A3B0-4E6E-9A8B-0F20EADB875D}.Debug|x64.ActiveCfg = Debug|Any CPU
185+
{70142BE9-A3B0-4E6E-9A8B-0F20EADB875D}.Debug|x64.Build.0 = Debug|Any CPU
186+
{70142BE9-A3B0-4E6E-9A8B-0F20EADB875D}.Debug|x86.ActiveCfg = Debug|Any CPU
187+
{70142BE9-A3B0-4E6E-9A8B-0F20EADB875D}.Debug|x86.Build.0 = Debug|Any CPU
188+
{70142BE9-A3B0-4E6E-9A8B-0F20EADB875D}.Release|Any CPU.ActiveCfg = Release|Any CPU
189+
{70142BE9-A3B0-4E6E-9A8B-0F20EADB875D}.Release|Any CPU.Build.0 = Release|Any CPU
190+
{70142BE9-A3B0-4E6E-9A8B-0F20EADB875D}.Release|x64.ActiveCfg = Release|Any CPU
191+
{70142BE9-A3B0-4E6E-9A8B-0F20EADB875D}.Release|x64.Build.0 = Release|Any CPU
192+
{70142BE9-A3B0-4E6E-9A8B-0F20EADB875D}.Release|x86.ActiveCfg = Release|Any CPU
193+
{70142BE9-A3B0-4E6E-9A8B-0F20EADB875D}.Release|x86.Build.0 = Release|Any CPU
194+
{70142BE9-A3B0-4E6E-9A8B-0F20EADB875D}.WebForms|Any CPU.ActiveCfg = Debug|Any CPU
195+
{70142BE9-A3B0-4E6E-9A8B-0F20EADB875D}.WebForms|Any CPU.Build.0 = Debug|Any CPU
196+
{70142BE9-A3B0-4E6E-9A8B-0F20EADB875D}.WebForms|x64.ActiveCfg = Debug|Any CPU
197+
{70142BE9-A3B0-4E6E-9A8B-0F20EADB875D}.WebForms|x64.Build.0 = Debug|Any CPU
198+
{70142BE9-A3B0-4E6E-9A8B-0F20EADB875D}.WebForms|x86.ActiveCfg = Debug|Any CPU
199+
{70142BE9-A3B0-4E6E-9A8B-0F20EADB875D}.WebForms|x86.Build.0 = Debug|Any CPU
180200
EndGlobalSection
181201
GlobalSection(SolutionProperties) = preSolution
182202
HideSolutionNode = FALSE
@@ -188,6 +208,7 @@ Global
188208
{1669CD22-5CCE-4D96-A02B-31D81B5EFB2B} = {240E45D9-B9FF-42E8-B0C1-332861E02DBF}
189209
{CA277C6F-A3DD-4FAF-9B7C-56E7B844CEF7} = {240E45D9-B9FF-42E8-B0C1-332861E02DBF}
190210
{E12935A8-C95C-4D67-A24B-A984A99AFE5F} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
211+
{70142BE9-A3B0-4E6E-9A8B-0F20EADB875D} = {FD758025-7F6D-4EE8-9837-98C0E0BFCB6B}
191212
EndGlobalSection
192213
GlobalSection(ExtensibilityGlobals) = postSolution
193214
SolutionGuid = {E288F9FB-039F-4718-8AEB-85F89B29EB4E}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
using Microsoft.Playwright;
2+
3+
namespace WingtipToys.AcceptanceTests;
4+
5+
/// <summary>
6+
/// Verifies that the Register and Login pages render correctly
7+
/// with the expected BWFC form controls and are functional.
8+
/// </summary>
9+
[Collection("Playwright")]
10+
public class AuthenticationTests
11+
{
12+
private readonly PlaywrightFixture _fixture;
13+
14+
public AuthenticationTests(PlaywrightFixture fixture) => _fixture = fixture;
15+
16+
[Fact]
17+
public async Task RegisterPage_HasExpectedFormFields()
18+
{
19+
var page = await _fixture.NewPageAsync();
20+
await page.GotoAsync($"{TestConfiguration.BaseUrl}/Account/Register");
21+
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
22+
23+
// Register page should have email, password, and confirm password fields
24+
var emailInput = page.Locator("input[type='text'], input[type='email']").First;
25+
var passwordInputs = page.Locator("input[type='password']");
26+
27+
Assert.True(await emailInput.CountAsync() > 0, "Register page should have an email/text input");
28+
Assert.True(await passwordInputs.CountAsync() >= 2, "Register page should have password and confirm password fields");
29+
30+
// Should have a submit button
31+
var submitButton = page.GetByRole(AriaRole.Button).First;
32+
Assert.True(await submitButton.CountAsync() > 0, "Register page should have a submit button");
33+
}
34+
35+
[Fact]
36+
public async Task LoginPage_HasExpectedFormFields()
37+
{
38+
var page = await _fixture.NewPageAsync();
39+
await page.GotoAsync($"{TestConfiguration.BaseUrl}/Account/Login");
40+
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
41+
42+
// Login page should have email and password fields
43+
var emailInput = page.Locator("input[type='text'], input[type='email']").First;
44+
var passwordInput = page.Locator("input[type='password']").First;
45+
46+
Assert.True(await emailInput.CountAsync() > 0, "Login page should have an email/text input");
47+
Assert.True(await passwordInput.CountAsync() > 0, "Login page should have a password input");
48+
49+
// Should have a login button
50+
var loginButton = page.GetByRole(AriaRole.Button).First;
51+
Assert.True(await loginButton.CountAsync() > 0, "Login page should have a login button");
52+
}
53+
54+
[Fact]
55+
public async Task RegisterAndLogin_EndToEnd()
56+
{
57+
var page = await _fixture.NewPageAsync();
58+
59+
// Step 1: Register a new user
60+
await page.GotoAsync($"{TestConfiguration.BaseUrl}/Account/Register");
61+
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
62+
63+
var testEmail = $"test-{Guid.NewGuid():N}@example.com";
64+
const string testPassword = "Test@12345";
65+
66+
var emailInput = page.Locator("input[type='text'], input[type='email']").First;
67+
await emailInput.FillAsync(testEmail);
68+
69+
var passwordInputs = page.Locator("input[type='password']");
70+
if (await passwordInputs.CountAsync() >= 2)
71+
{
72+
await passwordInputs.Nth(0).FillAsync(testPassword);
73+
await passwordInputs.Nth(1).FillAsync(testPassword);
74+
}
75+
76+
var registerButton = page.GetByRole(AriaRole.Button).First;
77+
await registerButton.ClickAsync();
78+
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
79+
80+
// Step 2: Log in with the new user
81+
await page.GotoAsync($"{TestConfiguration.BaseUrl}/Account/Login");
82+
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
83+
84+
emailInput = page.Locator("input[type='text'], input[type='email']").First;
85+
await emailInput.FillAsync(testEmail);
86+
87+
var passwordInput = page.Locator("input[type='password']").First;
88+
await passwordInput.FillAsync(testPassword);
89+
90+
var loginButton = page.GetByRole(AriaRole.Button).First;
91+
await loginButton.ClickAsync();
92+
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
93+
94+
// Step 3: Verify authenticated state — should see user greeting or manage link
95+
var pageContent = await page.ContentAsync();
96+
var isAuthenticated =
97+
pageContent.Contains("Hello", StringComparison.OrdinalIgnoreCase) ||
98+
pageContent.Contains("Manage", StringComparison.OrdinalIgnoreCase) ||
99+
pageContent.Contains("Log out", StringComparison.OrdinalIgnoreCase) ||
100+
pageContent.Contains(testEmail, StringComparison.OrdinalIgnoreCase);
101+
102+
Assert.True(isAuthenticated,
103+
"After login, the page should show an authenticated state (greeting, manage link, or logout)");
104+
}
105+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
global using Xunit;
2+
global using Microsoft.Playwright;
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
using Microsoft.Playwright;
2+
3+
namespace WingtipToys.AcceptanceTests;
4+
5+
/// <summary>
6+
/// Verifies that every top-level navigation link in the WingtipToys navbar
7+
/// resolves to a page that loads without errors.
8+
/// </summary>
9+
[Collection("Playwright")]
10+
public class NavigationTests
11+
{
12+
private readonly PlaywrightFixture _fixture;
13+
14+
public NavigationTests(PlaywrightFixture fixture) => _fixture = fixture;
15+
16+
[Fact]
17+
public async Task HomePage_Loads()
18+
{
19+
var page = await _fixture.NewPageAsync();
20+
var response = await page.GotoAsync(TestConfiguration.BaseUrl);
21+
22+
Assert.NotNull(response);
23+
Assert.True(response.Ok, $"Home page returned {response.Status}");
24+
// Verify some content loaded
25+
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
26+
var title = await page.TitleAsync();
27+
Assert.False(string.IsNullOrEmpty(title), "Home page should have a title");
28+
}
29+
30+
[Theory]
31+
[InlineData("About")]
32+
[InlineData("Contact")]
33+
[InlineData("ProductList")]
34+
public async Task NavbarLink_LoadsPage(string pageName)
35+
{
36+
var page = await _fixture.NewPageAsync();
37+
await page.GotoAsync(TestConfiguration.BaseUrl);
38+
39+
// Click the navbar link whose href ends with the page name
40+
var link = page.Locator($"a[href='/{pageName}']").First;
41+
await link.ClickAsync();
42+
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
43+
44+
// Page should load without a server error
45+
Assert.DoesNotContain("Error", await page.TitleAsync(), StringComparison.OrdinalIgnoreCase);
46+
Assert.True(page.Url.Contains(pageName, StringComparison.OrdinalIgnoreCase),
47+
$"Expected URL to contain '{pageName}' but was '{page.Url}'");
48+
}
49+
50+
[Fact]
51+
public async Task ShoppingCartLink_LoadsPage()
52+
{
53+
var page = await _fixture.NewPageAsync();
54+
await page.GotoAsync(TestConfiguration.BaseUrl);
55+
56+
var cartLink = page.Locator("a[href='/ShoppingCart']").First;
57+
await cartLink.ClickAsync();
58+
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
59+
60+
Assert.Contains("ShoppingCart", page.Url, StringComparison.OrdinalIgnoreCase);
61+
}
62+
63+
[Fact]
64+
public async Task RegisterLink_LoadsPage()
65+
{
66+
var page = await _fixture.NewPageAsync();
67+
await page.GotoAsync(TestConfiguration.BaseUrl);
68+
69+
var registerLink = page.Locator("a[href='/Account/Register']").First;
70+
await registerLink.ClickAsync();
71+
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
72+
73+
Assert.Contains("Register", page.Url, StringComparison.OrdinalIgnoreCase);
74+
}
75+
76+
[Fact]
77+
public async Task LoginLink_LoadsPage()
78+
{
79+
var page = await _fixture.NewPageAsync();
80+
await page.GotoAsync(TestConfiguration.BaseUrl);
81+
82+
var loginLink = page.Locator("a[href='/Account/Login']").First;
83+
await loginLink.ClickAsync();
84+
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
85+
86+
Assert.Contains("Login", page.Url, StringComparison.OrdinalIgnoreCase);
87+
}
88+
89+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using Microsoft.Playwright;
2+
3+
namespace WingtipToys.AcceptanceTests;
4+
5+
/// <summary>
6+
/// Shared Playwright browser instance across all tests in the collection.
7+
/// Installs browsers on first use if needed.
8+
/// </summary>
9+
public class PlaywrightFixture : IAsyncLifetime
10+
{
11+
public IPlaywright Playwright { get; private set; } = null!;
12+
public IBrowser Browser { get; private set; } = null!;
13+
14+
public async Task InitializeAsync()
15+
{
16+
Playwright = await Microsoft.Playwright.Playwright.CreateAsync();
17+
Browser = await Playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions
18+
{
19+
Headless = true
20+
});
21+
}
22+
23+
public async Task DisposeAsync()
24+
{
25+
await Browser.DisposeAsync();
26+
Playwright.Dispose();
27+
}
28+
29+
public async Task<IPage> NewPageAsync()
30+
{
31+
var context = await Browser.NewContextAsync(new BrowserNewContextOptions
32+
{
33+
IgnoreHTTPSErrors = true
34+
});
35+
return await context.NewPageAsync();
36+
}
37+
}
38+
39+
[CollectionDefinition("Playwright")]
40+
public class PlaywrightCollection : ICollectionFixture<PlaywrightFixture> { }
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# WingtipToys Acceptance Tests
2+
3+
Playwright-based end-to-end acceptance tests for migrated WingtipToys Blazor applications.
4+
These tests verify that each migration run produces a working application.
5+
6+
## Prerequisites
7+
8+
- .NET 10.0 SDK
9+
- Playwright browsers (installed automatically on first run)
10+
11+
## Configuration
12+
13+
Set the base URL of the WingtipToys site via environment variable:
14+
15+
```bash
16+
# PowerShell
17+
$env:WINGTIPTOYS_BASE_URL = "https://localhost:5001"
18+
19+
# Bash
20+
export WINGTIPTOYS_BASE_URL="https://localhost:5001"
21+
```
22+
23+
Defaults to `https://localhost:5001` if not set.
24+
25+
## Running Tests
26+
27+
```bash
28+
# Restore and install Playwright browsers
29+
dotnet build src/WingtipToys.AcceptanceTests
30+
pwsh src/WingtipToys.AcceptanceTests/bin/Debug/net10.0/playwright.ps1 install chromium
31+
32+
# Start the WingtipToys app (e.g., Run12)
33+
dotnet run --project samples/Run12WingtipToys/WingtipToys.csproj &
34+
35+
# Run the tests
36+
dotnet test src/WingtipToys.AcceptanceTests
37+
```
38+
39+
## Test Coverage
40+
41+
| Test Class | Scenarios |
42+
|------------|-----------|
43+
| **NavigationTests** | Home page loads, About/Contact/Products navbar links work, Shopping Cart link works, Register link works, Login link works |
44+
| **ShoppingCartTests** | Product list displays products, add item to cart, update cart quantity, remove item from cart |
45+
| **AuthenticationTests** | Register page has expected form fields, Login page has expected form fields, Register → Login end-to-end flow |
46+
47+
## Usage in Migration Iterations
48+
49+
After each migration run (e.g., Run 13, Run 14), point the tests at the newly migrated app:
50+
51+
```bash
52+
$env:WINGTIPTOYS_BASE_URL = "https://localhost:5001"
53+
dotnet run --project samples/Run13WingtipToys/WingtipToys.csproj &
54+
dotnet test src/WingtipToys.AcceptanceTests
55+
```
56+
57+
This provides a consistent quality gate across all migration iterations.

0 commit comments

Comments
 (0)