Skip to content

Commit 10c5d56

Browse files
csharpfritzCopilot
andcommitted
feat(cli): complete semantic pattern infrastructure and WingtipToys acceptance tests
- Implement pattern infrastructure for query, action, and account pages - Wire production and test registration for all semantic patterns - Add focused concrete and integration tests for pattern workflows - Update WingtipToys sample pages with latest Blazor patterns - Add WingtipToysPlaywrightFixture for acceptance test automation - Resolve merge conflicts and rebase onto upstream/dev Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 0f39474 commit 10c5d56

18 files changed

Lines changed: 1016 additions & 243 deletions

File tree

samples/AfterBlazorServerSide.Tests/AfterBlazorServerSide.Tests.csproj

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,17 @@
1919

2020
<ItemGroup>
2121
<ProjectReference Include="../AfterBlazorServerSide/AfterBlazorServerSide.csproj" />
22+
<ProjectReference Include="../AfterWingtipToys/WingtipToys.csproj" />
2223
</ItemGroup>
2324

25+
<Target Name="BuildPlaywrightHostsInRelease" BeforeTargets="VSTest">
26+
<MSBuild Projects="../AfterBlazorServerSide/AfterBlazorServerSide.csproj;../AfterWingtipToys/WingtipToys.csproj"
27+
Targets="Build"
28+
Properties="Configuration=Release" />
29+
</Target>
30+
2431
<ItemGroup>
2532
<Using Include="Xunit" />
2633
</ItemGroup>
2734

28-
</Project>
35+
</Project>
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
using Microsoft.Playwright;
2+
using System.Diagnostics;
3+
4+
namespace AfterBlazorServerSide.Tests;
5+
6+
[CollectionDefinition(nameof(WingtipToysPlaywrightCollection))]
7+
public class WingtipToysPlaywrightCollection : ICollectionFixture<WingtipToysPlaywrightFixture>
8+
{
9+
}
10+
11+
public class WingtipToysPlaywrightFixture : IAsyncLifetime
12+
{
13+
private IPlaywright? _playwright;
14+
private IBrowser? _browser;
15+
private Process? _serverProcess;
16+
private const int ServerPort = 5556;
17+
18+
public string BaseUrl => $"http://localhost:{ServerPort}";
19+
20+
public async Task InitializeAsync()
21+
{
22+
var dllPath = ResolveDllPath();
23+
24+
_serverProcess = new Process
25+
{
26+
StartInfo = new ProcessStartInfo
27+
{
28+
FileName = "dotnet",
29+
Arguments = $"\"{dllPath}\" --urls {BaseUrl}",
30+
UseShellExecute = false,
31+
RedirectStandardOutput = true,
32+
RedirectStandardError = true,
33+
CreateNoWindow = true,
34+
WorkingDirectory = Path.GetDirectoryName(dllPath)
35+
}
36+
};
37+
38+
_serverProcess.Start();
39+
40+
_serverProcess.OutputDataReceived += (_, e) =>
41+
{
42+
if (!string.IsNullOrEmpty(e.Data))
43+
{
44+
Console.WriteLine($"Wingtip Server Output: {e.Data}");
45+
}
46+
};
47+
_serverProcess.ErrorDataReceived += (_, e) =>
48+
{
49+
if (!string.IsNullOrEmpty(e.Data))
50+
{
51+
Console.WriteLine($"Wingtip Server Error: {e.Data}");
52+
}
53+
};
54+
_serverProcess.BeginOutputReadLine();
55+
_serverProcess.BeginErrorReadLine();
56+
57+
using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(5) };
58+
var isReady = false;
59+
60+
for (var attempt = 0; attempt < 60 && !isReady; attempt++)
61+
{
62+
try
63+
{
64+
var response = await httpClient.GetAsync(BaseUrl);
65+
if (response.IsSuccessStatusCode)
66+
{
67+
var content = await response.Content.ReadAsStringAsync();
68+
isReady = content.Contains("</html>", StringComparison.OrdinalIgnoreCase)
69+
|| content.Contains("Wingtip Toys", StringComparison.Ordinal);
70+
}
71+
}
72+
catch (Exception ex) when (attempt < 59)
73+
{
74+
Console.WriteLine($"Wingtip startup attempt {attempt + 1}: {ex.Message}");
75+
}
76+
77+
if (!isReady)
78+
{
79+
await Task.Delay(1000);
80+
}
81+
}
82+
83+
if (!isReady)
84+
{
85+
throw new Exception("WingtipToys server failed to start within 60 seconds.");
86+
}
87+
88+
_playwright = await Playwright.CreateAsync();
89+
_browser = await _playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions
90+
{
91+
Headless = true
92+
});
93+
}
94+
95+
public async Task DisposeAsync()
96+
{
97+
if (_browser is not null)
98+
{
99+
await _browser.CloseAsync();
100+
}
101+
102+
_playwright?.Dispose();
103+
104+
if (_serverProcess is not null && !_serverProcess.HasExited)
105+
{
106+
_serverProcess.Kill(true);
107+
_serverProcess.WaitForExit(5000);
108+
_serverProcess.Dispose();
109+
}
110+
}
111+
112+
public async Task<IPage> NewPageAsync()
113+
{
114+
if (_browser is null)
115+
{
116+
throw new InvalidOperationException("Browser not initialized");
117+
}
118+
119+
return await _browser.NewPageAsync();
120+
}
121+
122+
private static string ResolveDllPath()
123+
{
124+
foreach (var configuration in new[] { "Release", "Debug" })
125+
{
126+
var candidate = Path.GetFullPath(Path.Combine(
127+
AppContext.BaseDirectory,
128+
"..", "..", "..", "..",
129+
"AfterWingtipToys",
130+
"bin",
131+
configuration,
132+
"net10.0",
133+
"WingtipToys.dll"));
134+
135+
if (File.Exists(candidate))
136+
{
137+
return candidate;
138+
}
139+
}
140+
141+
throw new FileNotFoundException("Could not find WingtipToys server DLL in Release or Debug output.");
142+
}
143+
}
Lines changed: 40 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,52 @@
11
@page "/Account/Login"
2+
@inject NavigationManager Navigation
23

34
<PageTitle>Login</PageTitle>
45
<Site>
5-
<Content ContentPlaceHolderID="MainContent">
6-
<h1>Login</h1>
7-
@if (Registered.GetValueOrDefault() != 0)
8-
{
9-
<p class="text-success">Registration succeeded. Please log in.</p>
10-
}
11-
@if (!string.IsNullOrWhiteSpace(Error))
12-
{
13-
<p class="text-danger">@Error</p>
14-
}
15-
<form method="get" action="/Account/PerformLogin" class="form-horizontal">
16-
<div class="form-group">
17-
<label class="col-md-2 control-label" for="login-email">Email</label>
18-
<div class="col-md-10">
19-
<input id="login-email" name="email" type="email" class="form-control" required />
6+
<ChildComponents>
7+
<Content ContentPlaceHolderID="MainContent">
8+
<h1>Login</h1>
9+
@if (Registered.GetValueOrDefault() != 0)
10+
{
11+
<p class="text-success">Registration succeeded. Please log in.</p>
12+
}
13+
@if (!string.IsNullOrWhiteSpace(Error))
14+
{
15+
<p class="text-danger">@Error</p>
16+
}
17+
<form method="get" action="/Account/PerformLogin" class="form-horizontal">
18+
<div class="form-group">
19+
<label class="col-md-2 control-label" for="login-email">Email</label>
20+
<div class="col-md-10">
21+
<input id="login-email" name="email" type="email" class="form-control" required />
22+
</div>
2023
</div>
21-
</div>
22-
<div class="form-group">
23-
<label class="col-md-2 control-label" for="login-password">Password</label>
24-
<div class="col-md-10">
25-
<input id="login-password" name="password" type="password" class="form-control" required />
24+
<div class="form-group">
25+
<label class="col-md-2 control-label" for="login-password">Password</label>
26+
<div class="col-md-10">
27+
<input id="login-password" name="password" type="password" class="form-control" required />
28+
</div>
2629
</div>
27-
</div>
28-
<div class="form-group">
29-
<div class="col-md-offset-2 col-md-10">
30-
<button type="submit" class="btn btn-default">Log in</button>
30+
<div class="form-group">
31+
<div class="col-md-offset-2 col-md-10">
32+
<button type="submit" class="btn btn-default">Log in</button>
33+
</div>
3134
</div>
32-
</div>
33-
</form>
34-
<p><a href="/Account/Register">Register as a new user</a></p>
35-
</Content>
35+
</form>
36+
<p><a href="/Account/Register">Register as a new user</a></p>
37+
</Content>
38+
</ChildComponents>
3639
</Site>
3740

3841
@code {
39-
[SupplyParameterFromQuery]
40-
public string? Error { get; set; }
42+
private string? Error => GetQueryValue("error");
4143

42-
[SupplyParameterFromQuery]
43-
public int? Registered { get; set; }
44+
private int? Registered => int.TryParse(GetQueryValue("registered"), out var registered) ? registered : null;
45+
46+
private string? GetQueryValue(string key)
47+
{
48+
var uri = Navigation.ToAbsoluteUri(Navigation.Uri);
49+
var query = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(uri.Query);
50+
return query.TryGetValue(key, out var value) ? value.ToString() : null;
51+
}
4452
}
Lines changed: 39 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,51 @@
11
@page "/Account/Register"
2+
@inject NavigationManager Navigation
23

34
<PageTitle>Register</PageTitle>
45
<Site>
5-
<Content ContentPlaceHolderID="MainContent">
6-
<h1>Register</h1>
7-
@if (!string.IsNullOrWhiteSpace(Error))
8-
{
9-
<p class="text-danger">@Error</p>
10-
}
11-
<form method="get" action="/Account/PerformRegister" class="form-horizontal">
12-
<div class="form-group">
13-
<label class="col-md-2 control-label" for="register-email">Email</label>
14-
<div class="col-md-10">
15-
<input id="register-email" name="email" type="email" class="form-control" required />
6+
<ChildComponents>
7+
<Content ContentPlaceHolderID="MainContent">
8+
<h1>Register</h1>
9+
@if (!string.IsNullOrWhiteSpace(Error))
10+
{
11+
<p class="text-danger">@Error</p>
12+
}
13+
<form method="get" action="/Account/PerformRegister" class="form-horizontal">
14+
<div class="form-group">
15+
<label class="col-md-2 control-label" for="register-email">Email</label>
16+
<div class="col-md-10">
17+
<input id="register-email" name="email" type="email" class="form-control" required />
18+
</div>
1619
</div>
17-
</div>
18-
<div class="form-group">
19-
<label class="col-md-2 control-label" for="register-password">Password</label>
20-
<div class="col-md-10">
21-
<input id="register-password" name="password" type="password" class="form-control" required />
20+
<div class="form-group">
21+
<label class="col-md-2 control-label" for="register-password">Password</label>
22+
<div class="col-md-10">
23+
<input id="register-password" name="password" type="password" class="form-control" required />
24+
</div>
2225
</div>
23-
</div>
24-
<div class="form-group">
25-
<label class="col-md-2 control-label" for="register-confirm">Confirm password</label>
26-
<div class="col-md-10">
27-
<input id="register-confirm" name="confirmPassword" type="password" class="form-control" required />
26+
<div class="form-group">
27+
<label class="col-md-2 control-label" for="register-confirm">Confirm password</label>
28+
<div class="col-md-10">
29+
<input id="register-confirm" name="confirmPassword" type="password" class="form-control" required />
30+
</div>
2831
</div>
29-
</div>
30-
<div class="form-group">
31-
<div class="col-md-offset-2 col-md-10">
32-
<button type="submit" class="btn btn-default">Register</button>
32+
<div class="form-group">
33+
<div class="col-md-offset-2 col-md-10">
34+
<button type="submit" class="btn btn-default">Register</button>
35+
</div>
3336
</div>
34-
</div>
35-
</form>
36-
</Content>
37+
</form>
38+
</Content>
39+
</ChildComponents>
3740
</Site>
3841

3942
@code {
40-
[SupplyParameterFromQuery]
41-
public string? Error { get; set; }
43+
private string? Error => GetQueryValue("error");
44+
45+
private string? GetQueryValue(string key)
46+
{
47+
var uri = Navigation.ToAbsoluteUri(Navigation.Uri);
48+
var query = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(uri.Query);
49+
return query.TryGetValue(key, out var value) ? value.ToString() : null;
50+
}
4251
}

samples/AfterWingtipToys/Default.razor

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,19 @@
22

33
<PageTitle>Welcome</PageTitle>
44
<Site>
5-
<Content ContentPlaceHolderID="MainContent">
6-
<div class="jumbotron">
7-
<h1>Wingtip Toys</h1>
8-
<h2>Find the perfect transportation toy.</h2>
9-
<p class="lead">
10-
Browse boats, cars, planes, and trucks from the classic Wingtip catalog.
11-
Each product page includes images, descriptions, and a quick path into the shopping cart.
12-
</p>
13-
<p>
14-
<a class="btn btn-primary btn-lg" href="/ProductList">Browse Products</a>
15-
</p>
16-
</div>
17-
</Content>
5+
<ChildComponents>
6+
<Content ContentPlaceHolderID="MainContent">
7+
<div class="jumbotron">
8+
<h1>Wingtip Toys</h1>
9+
<h2>Find the perfect transportation toy.</h2>
10+
<p class="lead">
11+
Browse boats, cars, planes, and trucks from the classic Wingtip catalog.
12+
Each product page includes images, descriptions, and a quick path into the shopping cart.
13+
</p>
14+
<p>
15+
<a class="btn btn-primary btn-lg" href="/ProductList">Browse Products</a>
16+
</p>
17+
</div>
18+
</Content>
19+
</ChildComponents>
1820
</Site>

0 commit comments

Comments
 (0)