Skip to content

Commit 8ae09e8

Browse files
[Blazor] Update Blazor Playwright template tests (#54606)
1 parent 3105342 commit 8ae09e8

File tree

13 files changed

+431
-368
lines changed

13 files changed

+431
-368
lines changed

eng/Npm.Workspace.nodeproj

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@
4444
<Message Text="Building NPM packages..." Importance="high" />
4545

4646
<Exec
47-
Condition="$(ContinuousIntegrationBuild) == 'true'"
4847
Command="node $(MSBuildThisFileDirectory)scripts/npm/pack-workspace.mjs --update-versions $(RepoRoot)package.json $(PackageVersion) $(PackageOutputPath) $(IntermediateOutputPath)"
4948
EnvironmentVariables="$(_NpmAdditionalEnvironmentVariables)" />
5049

@@ -65,7 +64,9 @@
6564
</PropertyGroup>
6665
<Message Text="Packing NPM packages..." Importance="high" />
6766
<MakeDir Directories="$(PackageOutputPath)" Condition="!Exists('$(PackageOutputPath)')" />
68-
<Exec Command="node $(MSBuildThisFileDirectory)scripts/npm/pack-workspace.mjs --create-packages $(RepoRoot)package.json $(PackageVersion) $(PackageOutputPath) $(IntermediateOutputPath)" />
67+
<Exec
68+
Command="node $(MSBuildThisFileDirectory)scripts/npm/pack-workspace.mjs --create-packages $(RepoRoot)package.json $(PackageVersion) $(PackageOutputPath) $(IntermediateOutputPath)"
69+
EnvironmentVariables="$(_NpmAdditionalEnvironmentVariables)" />
6970
<ItemGroup>
7071
<_NpmGeneratedPackages Include="$(PackageOutputPath)/*.tgz" />
7172
</ItemGroup>

eng/tools/GenerateFiles/Directory.Build.targets.in

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,9 @@
122122

123123
<!-- When building and running locally, manually resolve the just-built frameworks. On Helix, let the SDK resolve the packs itself (they're laid out on top of the .NET SDK in the work items) -->
124124
<PropertyGroup Condition="$(UpdateAspNetCoreKnownFramework) and '$(HELIX_CORRELATION_PAYLOAD)' == ''">
125-
<EnableTargetingPackDownload>false</EnableTargetingPackDownload>
126-
<EnableRuntimePackDownload>false</EnableRuntimePackDownload>
125+
<!-- Allow additional targeting and runtime packs to be downloaded only if required by a test. -->
126+
<EnableTargetingPackDownload Condition="'$(TestRequiresTargetingPackDownload)' != 'true'">false</EnableTargetingPackDownload>
127+
<EnableRuntimePackDownload Condition="'$(TestRequiresRuntimePackDownload)' != 'true'">false</EnableRuntimePackDownload>
127128
<GenerateErrorForMissingTargetingPacks>false</GenerateErrorForMissingTargetingPacks>
128129
</PropertyGroup>
129130

src/ProjectTemplates/Shared/Project.cs

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,19 @@ internal async Task RunDotNetNewAsync(
6666
bool useLocalDB = false,
6767
bool noHttps = false,
6868
bool errorOnRestoreError = true,
69+
bool isItemTemplate = false,
6970
string[] args = null,
7071
// Used to set special options in MSBuild
7172
IDictionary<string, string> environmentVariables = null)
7273
{
73-
var hiveArg = $" --debug:disable-sdk-templates --debug:custom-hive \"{TemplatePackageInstaller.CustomHivePath}\"";
74+
var hiveArg = $"--debug:disable-sdk-templates --debug:custom-hive \"{TemplatePackageInstaller.CustomHivePath}\"";
7475
var argString = $"new {templateName} {hiveArg}";
7576
environmentVariables ??= new Dictionary<string, string>();
77+
if (!isItemTemplate)
78+
{
79+
argString += " --no-restore";
80+
}
81+
7682
if (!string.IsNullOrEmpty(auth))
7783
{
7884
argString += $" --auth {auth}";
@@ -113,18 +119,30 @@ internal async Task RunDotNetNewAsync(
113119
Directory.Delete(TemplateOutputDir, recursive: true);
114120
}
115121

116-
using var execution = ProcessEx.Run(Output, AppContext.BaseDirectory, DotNetMuxer.MuxerPathOrDefault(), argString, environmentVariables);
117-
await execution.Exited;
122+
using var createExecution = ProcessEx.Run(Output, AppContext.BaseDirectory, DotNetMuxer.MuxerPathOrDefault(), argString, environmentVariables);
123+
await createExecution.Exited;
118124

119-
var result = new ProcessResult(execution);
125+
var createResult = new ProcessResult(createExecution);
126+
Assert.True(0 == createResult.ExitCode, ErrorMessages.GetFailedProcessMessage("create", this, createResult));
120127

121-
// Because dotnet new automatically restores but silently ignores restore errors, need to handle restore errors explicitly
122-
if (errorOnRestoreError && (execution.Output.Contains("Restore failed.") || execution.Error.Contains("Restore failed.")))
128+
if (!isItemTemplate)
123129
{
124-
result.ExitCode = -1;
125-
}
130+
argString = "restore /bl";
131+
using var restoreExecution = ProcessEx.Run(Output, TemplateOutputDir, DotNetMuxer.MuxerPathOrDefault(), argString, environmentVariables);
132+
await restoreExecution.Exited;
126133

127-
Assert.True(0 == result.ExitCode, ErrorMessages.GetFailedProcessMessage("create/restore", this, result));
134+
var restoreResult = new ProcessResult(restoreExecution);
135+
136+
// Because dotnet new automatically restores but silently ignores restore errors, need to handle restore errors explicitly
137+
if (errorOnRestoreError && (restoreExecution.Output.Contains("Restore failed.") || restoreExecution.Error.Contains("Restore failed.")))
138+
{
139+
restoreResult.ExitCode = -1;
140+
}
141+
142+
CaptureBinLogOnFailure(restoreExecution);
143+
144+
Assert.True(0 == restoreResult.ExitCode, ErrorMessages.GetFailedProcessMessage("restore", this, restoreResult));
145+
}
128146
}
129147

130148
internal async Task RunDotNetPublishAsync(IDictionary<string, string> packageOptions = null, string additionalArgs = null, bool noRestore = true)

src/ProjectTemplates/TestInfrastructure/Directory.Build.props.in

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,20 @@
33
<PropertyGroup>
44
<RepoRoot>${RepoRoot}</RepoRoot>
55
<ArtifactsBinDir>${ArtifactsBinDir}</ArtifactsBinDir>
6+
7+
<!--
8+
These properties are required so that we know where to find the locally-built shared framework and targeting pack
9+
when creating a custom "dotnet" root for executing generated templates.
10+
-->
11+
<TargetingPackLayoutRoot>${TargetingPackLayoutRoot}</TargetingPackLayoutRoot>
12+
<SharedFrameworkLayoutRoot>${SharedFrameworkLayoutRoot}</SharedFrameworkLayoutRoot>
13+
14+
<!--
15+
Because some template projects require Microsoft.NETCore.App.Runtime.Mono.browser-wasm, we need to allow
16+
the download of additional targeting and runtime packs.
17+
-->
18+
<TestRequiresTargetingPackDownload>true</TestRequiresTargetingPackDownload>
19+
<TestRequiresRuntimePackDownload>true</TestRequiresRuntimePackDownload>
620
</PropertyGroup>
721

822
<Import Project="${ArtifactsBinDir}GenerateFiles\Directory.Build.props" />

src/ProjectTemplates/TestInfrastructure/PrepareForTest.targets

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
<PropertyGroup>
2727
<TestTemplateCreationFolder>$([MSBuild]::NormalizePath('$(OutputPath)$(TestTemplateCreationFolder)'))</TestTemplateCreationFolder>
2828
<CustomTemplateHivePath>$(TestTemplateCreationFolder)\Hives\$([System.Guid]::NewGuid())\.templateengine</CustomTemplateHivePath>
29+
<TemplateTestDotNetRoot Condition="'$(IsHelixJob)' != 'true'">$(TestTemplateCreationFolder)dotnet\</TemplateTestDotNetRoot>
30+
<_DotNetHostFileName>dotnet</_DotNetHostFileName>
31+
<_DotNetHostFileName Condition="$([MSBuild]::IsOSPlatform(`Windows`))">dotnet.exe</_DotNetHostFileName>
2932
</PropertyGroup>
3033

3134
<ItemGroup>
@@ -48,10 +51,16 @@
4851
<_Parameter1>TestTemplateCreationFolder</_Parameter1>
4952
<_Parameter2>$(TestTemplateCreationFolder)</_Parameter2>
5053
</AssemblyAttribute>
54+
5155
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
5256
<_Parameter1>CustomTemplateHivePath</_Parameter1>
5357
<_Parameter2>$(CustomTemplateHivePath)</_Parameter2>
5458
</AssemblyAttribute>
59+
60+
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute" Condition="'$(TemplateTestDotNetRoot)' != ''">
61+
<_Parameter1>DotNetHostOverride</_Parameter1>
62+
<_Parameter2>$(TemplateTestDotNetRoot)$(_DotNetHostFileName)</_Parameter2>
63+
</AssemblyAttribute>
5564
</ItemGroup>
5665

5766
<Message Importance="high" Text="Preparing environment for tests" />
@@ -72,6 +81,20 @@
7281
<Output TaskParameter="RemovedDirectories" ItemName="_CleanedUpDirectories" />
7382
</RemoveDir>
7483

84+
<ItemGroup Condition="'$(TemplateTestDotNetRoot)' != ''">
85+
<_FilesToCopy Include="$(LocalDotNetRoot)$(_DotNetHostFileName)" />
86+
<_FilesToCopy Include="$(LocalDotNetRoot)host\**\*" DestinationRelativeFolder="host\" />
87+
<_FilesToCopy Include="$(LocalDotNetRoot)shared\**\*" DestinationRelativeFolder="shared\" />
88+
<_FilesToCopy Include="$(LocalDotNetRoot)sdk\**\*" DestinationRelativeFolder="sdk\" />
89+
<_FilesToCopy Include="$(SharedFrameworkLayoutRoot)\**\*" />
90+
91+
<_DestinationFiles Include="@(_FilesToCopy->'$(TemplateTestDotNetRoot)%(DestinationRelativeFolder)%(RecursiveDir)%(Filename)%(Extension)')" />
92+
</ItemGroup>
93+
94+
<Copy SourceFiles="@(_FilesToCopy)"
95+
DestinationFiles="@(_DestinationFiles)"
96+
SkipUnchangedFiles="true" />
97+
7598
<Message Importance="high" Text="Removed directory %(_CleanedUpDirectories.Identity)" />
7699

77100
<Message Importance="high" Text="Created directory %(_CreatedDirectories.Identity)" />
@@ -84,7 +107,7 @@
84107

85108
<GenerateFileFromTemplate
86109
TemplateFile="$(MSBuildThisFileDirectory)..\TestInfrastructure\Directory.Build.props.in"
87-
Properties="RepoRoot=$(RepoRoot);ArtifactsBinDir=$(ArtifactsBinDir)"
110+
Properties="RepoRoot=$(RepoRoot);ArtifactsBinDir=$(ArtifactsBinDir);TargetingPackLayoutRoot=$(TargetingPackLayoutRoot);SharedFrameworkLayoutRoot=$(SharedFrameworkLayoutRoot);TestDependsOnAspNetPackages=$(TestDependsOnAspNetPackages);"
88111
OutputPath="$(TestTemplateCreationFolder)Directory.Build.props" />
89112

90113
<!-- Workaround https://github.com/dotnet/core-setup/issues/6420 - there is no MSBuild setting for rollforward yet -->

src/ProjectTemplates/test/Templates.Blazor.Tests/BlazorTemplateTest.cs

Lines changed: 154 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System;
5-
using System.IO;
64
using System.Runtime.InteropServices;
7-
using System.Threading.Tasks;
85
using Microsoft.AspNetCore.BrowserTesting;
6+
using Microsoft.Playwright;
97
using Templates.Test.Helpers;
10-
using Xunit;
118

129
namespace BlazorTemplates.Tests;
1310

@@ -18,14 +15,19 @@ public abstract class BlazorTemplateTest : BrowserTestBase
1815
public BlazorTemplateTest(ProjectFactoryFixture projectFactory)
1916
{
2017
ProjectFactory = projectFactory;
21-
Microsoft.Playwright.Program.Main(new[] { "install" });
18+
Microsoft.Playwright.Program.Main(["install"]);
2219
}
2320

2421
public ProjectFactoryFixture ProjectFactory { get; set; }
2522

2623
public abstract string ProjectType { get; }
2724

28-
protected async Task<Project> CreateBuildPublishAsync(string auth = null, string[] args = null, string targetFramework = null, bool serverProject = false, bool onlyCreate = false)
25+
protected async Task<Project> CreateBuildPublishAsync(
26+
string auth = null,
27+
string[] args = null,
28+
string targetFramework = null,
29+
Func<Project, Project> getTargetProject = null,
30+
bool onlyCreate = false)
2931
{
3032
// Additional arguments are needed. See: https://github.com/dotnet/aspnetcore/issues/24278
3133
Environment.SetEnvironmentVariable("EnableDefaultScopedCssItems", "true");
@@ -38,21 +40,17 @@ protected async Task<Project> CreateBuildPublishAsync(string auth = null, string
3840

3941
await project.RunDotNetNewAsync(ProjectType, auth: auth, args: args);
4042

43+
project = getTargetProject?.Invoke(project) ?? project;
44+
4145
if (!onlyCreate)
4246
{
43-
var targetProject = project;
44-
if (serverProject)
45-
{
46-
targetProject = GetSubProject(project, "Server", $"{project.ProjectName}.Server");
47-
}
48-
49-
await targetProject.RunDotNetPublishAsync(noRestore: false);
47+
await project.RunDotNetPublishAsync(noRestore: false);
5048

5149
// Run dotnet build after publish. The reason is that one uses Config = Debug and the other uses Config = Release
5250
// The output from publish will go into bin/Release/netcoreappX.Y/publish and won't be affected by calling build
5351
// later, while the opposite is not true.
5452

55-
await targetProject.RunDotNetBuildAsync();
53+
await project.RunDotNetBuildAsync();
5654
}
5755

5856
return project;
@@ -83,6 +81,138 @@ public static bool TryValidateBrowserRequired(BrowserKind browserKind, bool isRe
8381
return isRequired;
8482
}
8583

84+
protected async Task TestBasicInteractionInNewPageAsync(
85+
BrowserKind browserKind,
86+
string listeningUri,
87+
string appName,
88+
BlazorTemplatePages pagesToExclude = BlazorTemplatePages.None,
89+
bool usesAuth = false)
90+
{
91+
if (!BrowserManager.IsAvailable(browserKind))
92+
{
93+
EnsureBrowserAvailable(browserKind);
94+
return;
95+
}
96+
97+
await using var browser = await BrowserManager.GetBrowserInstance(browserKind, BrowserContextInfo);
98+
var page = await browser.NewPageAsync();
99+
100+
Output.WriteLine($"Opening browser at {listeningUri}...");
101+
await page.GotoAsync(listeningUri, new() { WaitUntil = WaitUntilState.NetworkIdle });
102+
103+
await TestBasicInteractionAsync(page, appName, pagesToExclude, usesAuth);
104+
105+
await page.CloseAsync();
106+
}
107+
108+
protected async Task TestBasicInteractionAsync(
109+
IPage page,
110+
string appName,
111+
BlazorTemplatePages pagesToExclude = BlazorTemplatePages.None,
112+
bool usesAuth = false)
113+
{
114+
await page.WaitForSelectorAsync("nav");
115+
116+
if (!pagesToExclude.HasFlag(BlazorTemplatePages.Home))
117+
{
118+
// Initially displays the home page
119+
await page.WaitForSelectorAsync("h1 >> text=Hello, world!");
120+
121+
Assert.Equal("Home", (await page.TitleAsync()).Trim());
122+
}
123+
124+
if (!pagesToExclude.HasFlag(BlazorTemplatePages.Counter))
125+
{
126+
// Can navigate to the counter page
127+
await Task.WhenAll(
128+
page.WaitForNavigationAsync(new() { UrlString = "**/counter" }),
129+
page.WaitForSelectorAsync("h1 >> text=Counter"),
130+
page.WaitForSelectorAsync("p >> text=Current count: 0"),
131+
page.ClickAsync("a[href=counter]"));
132+
133+
// Clicking the counter button works
134+
await IncrementCounterAsync(page);
135+
}
136+
137+
if (usesAuth)
138+
{
139+
await Task.WhenAll(
140+
page.WaitForNavigationAsync(new() { UrlString = "**/Identity/Account/Login**", WaitUntil = WaitUntilState.NetworkIdle }),
141+
page.ClickAsync("text=Log in"));
142+
143+
await Task.WhenAll(
144+
page.WaitForSelectorAsync("[name=\"Input.Email\"]"),
145+
page.WaitForNavigationAsync(new() { UrlString = "**/Identity/Account/Register**", WaitUntil = WaitUntilState.NetworkIdle }),
146+
page.ClickAsync("text=Register as a new user"));
147+
148+
var userName = $"{Guid.NewGuid()}@example.com";
149+
var password = "[PLACEHOLDER]-1a";
150+
151+
await page.TypeAsync("[name=\"Input.Email\"]", userName);
152+
await page.TypeAsync("[name=\"Input.Password\"]", password);
153+
await page.TypeAsync("[name=\"Input.ConfirmPassword\"]", password);
154+
155+
// We will be redirected to the RegisterConfirmation
156+
await Task.WhenAll(
157+
page.WaitForNavigationAsync(new() { UrlString = "**/Identity/Account/RegisterConfirmation**", WaitUntil = WaitUntilState.NetworkIdle }),
158+
page.ClickAsync("#registerSubmit"));
159+
160+
// We will be redirected to the ConfirmEmail
161+
await Task.WhenAll(
162+
page.WaitForNavigationAsync(new() { UrlString = "**/Identity/Account/ConfirmEmail**", WaitUntil = WaitUntilState.NetworkIdle }),
163+
page.ClickAsync("text=Click here to confirm your account"));
164+
165+
// Now we can login
166+
await page.ClickAsync("text=Login");
167+
await page.WaitForSelectorAsync("[name=\"Input.Email\"]");
168+
await page.TypeAsync("[name=\"Input.Email\"]", userName);
169+
await page.TypeAsync("[name=\"Input.Password\"]", password);
170+
await page.ClickAsync("#login-submit");
171+
172+
// Need to navigate to fetch page
173+
await page.GotoAsync(new Uri(page.Url).GetLeftPart(UriPartial.Authority));
174+
Assert.Equal(appName.Trim(), (await page.TitleAsync()).Trim());
175+
}
176+
177+
if (!pagesToExclude.HasFlag(BlazorTemplatePages.Weather))
178+
{
179+
await page.ClickAsync("a[href=weather]");
180+
await page.WaitForSelectorAsync("h1 >> text=Weather");
181+
182+
// Asynchronously loads and displays the table of weather forecasts
183+
await page.WaitForSelectorAsync("table>tbody>tr");
184+
Assert.Equal(5, await page.Locator("p+table>tbody>tr").CountAsync());
185+
}
186+
187+
static async Task IncrementCounterAsync(IPage page)
188+
{
189+
// Allow multiple click attempts because some interactive render modes
190+
// won't be immediately available
191+
const int MaxIncrementAttempts = 5;
192+
const float IncrementTimeoutMilliseconds = 3000f;
193+
for (var i = 0; i < MaxIncrementAttempts; i++)
194+
{
195+
await page.ClickAsync("p+button >> text=Click me");
196+
try
197+
{
198+
await page.WaitForSelectorAsync("p >> text=Current count: 1", new()
199+
{
200+
Timeout = IncrementTimeoutMilliseconds,
201+
});
202+
203+
// The counter successfully incremented, so we're done
204+
return;
205+
}
206+
catch (TimeoutException)
207+
{
208+
// The counter did not increment; try again
209+
}
210+
}
211+
212+
Assert.Fail($"The counter did not increment after {MaxIncrementAttempts} attempts");
213+
}
214+
}
215+
86216
protected void EnsureBrowserAvailable(BrowserKind browserKind)
87217
{
88218
Assert.False(
@@ -92,4 +222,14 @@ protected void EnsureBrowserAvailable(BrowserKind browserKind)
92222
out var errorMessage),
93223
errorMessage);
94224
}
225+
226+
[Flags]
227+
protected enum BlazorTemplatePages
228+
{
229+
None = 0,
230+
Home = 1,
231+
Counter = 2,
232+
Weather = 4,
233+
All = ~0,
234+
}
95235
}

0 commit comments

Comments
 (0)