Skip to content

[Blazor] Fix template tests #54606

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

Merged
merged 35 commits into from
Jul 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
4d2ca4d
Enable packing locally
MackinnonBuck Mar 8, 2024
7613b0e
Fix template project publish
MackinnonBuck Mar 15, 2024
5725234
Unquarantine BlazorWasmStandaloneTemplate_Works
MackinnonBuck Mar 18, 2024
b2a2aa6
Enable skipped tests, remove hosted tests
MackinnonBuck Mar 19, 2024
ad7ced6
Blazor Web tests + fixes + cleanup
MackinnonBuck Mar 20, 2024
dfe0a34
Try using HTTP for PWA test
MackinnonBuck Mar 20, 2024
69d6b37
Collect binlogs on restore failure
MackinnonBuck Mar 20, 2024
5455102
Create and restore in separate steps
MackinnonBuck Mar 21, 2024
27f7330
Fix typo
MackinnonBuck Mar 21, 2024
4015db0
Add explicit reference to M.A.C.WebAssembly.Server
MackinnonBuck Mar 25, 2024
f0bd75f
Trying another fix
MackinnonBuck Mar 25, 2024
27b4da3
Fixing the fix
MackinnonBuck Mar 25, 2024
488e868
Trying something else
MackinnonBuck Jun 13, 2024
91c90c7
More fixes
MackinnonBuck Jun 18, 2024
06dc452
Mostly fixed, just a couple more issues
MackinnonBuck Jun 19, 2024
b88201c
More updates
MackinnonBuck Jun 20, 2024
e5f8506
Debugging CI
MackinnonBuck Jun 20, 2024
f043eb8
Fixed extra '\'
MackinnonBuck Jun 20, 2024
b4228a5
Don't disable targeting pack download in some cases
MackinnonBuck Jun 20, 2024
bb9cfb6
Fix
MackinnonBuck Jun 20, 2024
4f629c2
Enforce build order
MackinnonBuck Jun 20, 2024
7273474
More build order enforcement
MackinnonBuck Jun 20, 2024
dc7cb8a
Disable DotNetHostOverride on Helix
MackinnonBuck Jun 21, 2024
e317489
Fix
MackinnonBuck Jun 21, 2024
cb59171
Ignore some behavior on Helix
MackinnonBuck Jun 21, 2024
1917409
The env variable was lowercase?
MackinnonBuck Jun 21, 2024
acf0b2f
Fixed broken item template test
MackinnonBuck Jun 24, 2024
2db4ac0
Fix
MackinnonBuck Jun 24, 2024
7d2e035
Disable Blazor Web Playwright test on non-helix CI
MackinnonBuck Jun 24, 2024
0ced048
Cleanup
MackinnonBuck Jun 25, 2024
b3e6bb9
Rewrite nuspecs earlier
MackinnonBuck Jul 5, 2024
bb41807
Try re-enabling Helix
MackinnonBuck Jul 5, 2024
02d1354
Revert last two commits
MackinnonBuck Jul 8, 2024
304f577
Try undoing build ordering changes
MackinnonBuck Jul 8, 2024
185862c
Add comments describing changes
MackinnonBuck Jul 8, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions eng/Npm.Workspace.nodeproj
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
<Message Text="Building NPM packages..." Importance="high" />

<Exec
Condition="$(ContinuousIntegrationBuild) == 'true'"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change was required in order to build locally with the -pack option. But unfortunately it seems to increase the build time quite a bit. Might be worth considering somehow moving this step to the "Pack" target.

Command="node $(MSBuildThisFileDirectory)scripts/npm/pack-workspace.mjs --update-versions $(RepoRoot)package.json $(PackageVersion) $(PackageOutputPath) $(IntermediateOutputPath)"
EnvironmentVariables="$(_NpmAdditionalEnvironmentVariables)" />

Expand All @@ -65,7 +64,9 @@
</PropertyGroup>
<Message Text="Packing NPM packages..." Importance="high" />
<MakeDir Directories="$(PackageOutputPath)" Condition="!Exists('$(PackageOutputPath)')" />
<Exec Command="node $(MSBuildThisFileDirectory)scripts/npm/pack-workspace.mjs --create-packages $(RepoRoot)package.json $(PackageVersion) $(PackageOutputPath) $(IntermediateOutputPath)" />
<Exec
Command="node $(MSBuildThisFileDirectory)scripts/npm/pack-workspace.mjs --create-packages $(RepoRoot)package.json $(PackageVersion) $(PackageOutputPath) $(IntermediateOutputPath)"
EnvironmentVariables="$(_NpmAdditionalEnvironmentVariables)" />
<ItemGroup>
<_NpmGeneratedPackages Include="$(PackageOutputPath)/*.tgz" />
</ItemGroup>
Expand Down
5 changes: 3 additions & 2 deletions eng/tools/GenerateFiles/Directory.Build.targets.in
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,9 @@

<!-- 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) -->
<PropertyGroup Condition="$(UpdateAspNetCoreKnownFramework) and '$(HELIX_CORRELATION_PAYLOAD)' == ''">
<EnableTargetingPackDownload>false</EnableTargetingPackDownload>
<EnableRuntimePackDownload>false</EnableRuntimePackDownload>
<!-- Allow additional targeting and runtime packs to be downloaded only if required by a test. -->
<EnableTargetingPackDownload Condition="'$(TestRequiresTargetingPackDownload)' != 'true'">false</EnableTargetingPackDownload>
<EnableRuntimePackDownload Condition="'$(TestRequiresRuntimePackDownload)' != 'true'">false</EnableRuntimePackDownload>
<GenerateErrorForMissingTargetingPacks>false</GenerateErrorForMissingTargetingPacks>
</PropertyGroup>

Expand Down
36 changes: 27 additions & 9 deletions src/ProjectTemplates/Shared/Project.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,19 @@ internal async Task RunDotNetNewAsync(
bool useLocalDB = false,
bool noHttps = false,
bool errorOnRestoreError = true,
bool isItemTemplate = false,
string[] args = null,
// Used to set special options in MSBuild
IDictionary<string, string> environmentVariables = null)
{
var hiveArg = $" --debug:disable-sdk-templates --debug:custom-hive \"{TemplatePackageInstaller.CustomHivePath}\"";
var hiveArg = $"--debug:disable-sdk-templates --debug:custom-hive \"{TemplatePackageInstaller.CustomHivePath}\"";
var argString = $"new {templateName} {hiveArg}";
environmentVariables ??= new Dictionary<string, string>();
if (!isItemTemplate)
{
argString += " --no-restore";
}

if (!string.IsNullOrEmpty(auth))
{
argString += $" --auth {auth}";
Expand Down Expand Up @@ -113,18 +119,30 @@ internal async Task RunDotNetNewAsync(
Directory.Delete(TemplateOutputDir, recursive: true);
}

using var execution = ProcessEx.Run(Output, AppContext.BaseDirectory, DotNetMuxer.MuxerPathOrDefault(), argString, environmentVariables);
await execution.Exited;
using var createExecution = ProcessEx.Run(Output, AppContext.BaseDirectory, DotNetMuxer.MuxerPathOrDefault(), argString, environmentVariables);
await createExecution.Exited;

var result = new ProcessResult(execution);
var createResult = new ProcessResult(createExecution);
Assert.True(0 == createResult.ExitCode, ErrorMessages.GetFailedProcessMessage("create", this, createResult));

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

Assert.True(0 == result.ExitCode, ErrorMessages.GetFailedProcessMessage("create/restore", this, result));
var restoreResult = new ProcessResult(restoreExecution);

// Because dotnet new automatically restores but silently ignores restore errors, need to handle restore errors explicitly
if (errorOnRestoreError && (restoreExecution.Output.Contains("Restore failed.") || restoreExecution.Error.Contains("Restore failed.")))
{
restoreResult.ExitCode = -1;
}

CaptureBinLogOnFailure(restoreExecution);

Assert.True(0 == restoreResult.ExitCode, ErrorMessages.GetFailedProcessMessage("restore", this, restoreResult));
}
}

internal async Task RunDotNetPublishAsync(IDictionary<string, string> packageOptions = null, string additionalArgs = null, bool noRestore = true)
Expand Down
14 changes: 14 additions & 0 deletions src/ProjectTemplates/TestInfrastructure/Directory.Build.props.in
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@
<PropertyGroup>
<RepoRoot>${RepoRoot}</RepoRoot>
<ArtifactsBinDir>${ArtifactsBinDir}</ArtifactsBinDir>

<!--
These properties are required so that we know where to find the locally-built shared framework and targeting pack
when creating a custom "dotnet" root for executing generated templates.
-->
<TargetingPackLayoutRoot>${TargetingPackLayoutRoot}</TargetingPackLayoutRoot>
<SharedFrameworkLayoutRoot>${SharedFrameworkLayoutRoot}</SharedFrameworkLayoutRoot>

<!--
Because some template projects require Microsoft.NETCore.App.Runtime.Mono.browser-wasm, we need to allow
the download of additional targeting and runtime packs.
-->
<TestRequiresTargetingPackDownload>true</TestRequiresTargetingPackDownload>
<TestRequiresRuntimePackDownload>true</TestRequiresRuntimePackDownload>
</PropertyGroup>

<Import Project="${ArtifactsBinDir}GenerateFiles\Directory.Build.props" />
Expand Down
25 changes: 24 additions & 1 deletion src/ProjectTemplates/TestInfrastructure/PrepareForTest.targets
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
<PropertyGroup>
<TestTemplateCreationFolder>$([MSBuild]::NormalizePath('$(OutputPath)$(TestTemplateCreationFolder)'))</TestTemplateCreationFolder>
<CustomTemplateHivePath>$(TestTemplateCreationFolder)\Hives\$([System.Guid]::NewGuid())\.templateengine</CustomTemplateHivePath>
<TemplateTestDotNetRoot Condition="'$(IsHelixJob)' != 'true'">$(TestTemplateCreationFolder)dotnet\</TemplateTestDotNetRoot>
<_DotNetHostFileName>dotnet</_DotNetHostFileName>
<_DotNetHostFileName Condition="$([MSBuild]::IsOSPlatform(`Windows`))">dotnet.exe</_DotNetHostFileName>
</PropertyGroup>

<ItemGroup>
Expand All @@ -48,10 +51,16 @@
<_Parameter1>TestTemplateCreationFolder</_Parameter1>
<_Parameter2>$(TestTemplateCreationFolder)</_Parameter2>
</AssemblyAttribute>

<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
<_Parameter1>CustomTemplateHivePath</_Parameter1>
<_Parameter2>$(CustomTemplateHivePath)</_Parameter2>
</AssemblyAttribute>

<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute" Condition="'$(TemplateTestDotNetRoot)' != ''">
<_Parameter1>DotNetHostOverride</_Parameter1>
<_Parameter2>$(TemplateTestDotNetRoot)$(_DotNetHostFileName)</_Parameter2>
</AssemblyAttribute>
</ItemGroup>

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

<ItemGroup Condition="'$(TemplateTestDotNetRoot)' != ''">
<_FilesToCopy Include="$(LocalDotNetRoot)$(_DotNetHostFileName)" />
<_FilesToCopy Include="$(LocalDotNetRoot)host\**\*" DestinationRelativeFolder="host\" />
<_FilesToCopy Include="$(LocalDotNetRoot)shared\**\*" DestinationRelativeFolder="shared\" />
<_FilesToCopy Include="$(LocalDotNetRoot)sdk\**\*" DestinationRelativeFolder="sdk\" />
<_FilesToCopy Include="$(SharedFrameworkLayoutRoot)\**\*" />

<_DestinationFiles Include="@(_FilesToCopy->'$(TemplateTestDotNetRoot)%(DestinationRelativeFolder)%(RecursiveDir)%(Filename)%(Extension)')" />
</ItemGroup>

<Copy SourceFiles="@(_FilesToCopy)"
DestinationFiles="@(_DestinationFiles)"
SkipUnchangedFiles="true" />

<Message Importance="high" Text="Removed directory %(_CleanedUpDirectories.Identity)" />

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

<GenerateFileFromTemplate
TemplateFile="$(MSBuildThisFileDirectory)..\TestInfrastructure\Directory.Build.props.in"
Properties="RepoRoot=$(RepoRoot);ArtifactsBinDir=$(ArtifactsBinDir)"
Properties="RepoRoot=$(RepoRoot);ArtifactsBinDir=$(ArtifactsBinDir);TargetingPackLayoutRoot=$(TargetingPackLayoutRoot);SharedFrameworkLayoutRoot=$(SharedFrameworkLayoutRoot);TestDependsOnAspNetPackages=$(TestDependsOnAspNetPackages);"
OutputPath="$(TestTemplateCreationFolder)Directory.Build.props" />

<!-- Workaround https://github.com/dotnet/core-setup/issues/6420 - there is no MSBuild setting for rollforward yet -->
Expand Down
168 changes: 154 additions & 14 deletions src/ProjectTemplates/test/Templates.Blazor.Tests/BlazorTemplateTest.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Microsoft.AspNetCore.BrowserTesting;
using Microsoft.Playwright;
using Templates.Test.Helpers;
using Xunit;

namespace BlazorTemplates.Tests;

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

public ProjectFactoryFixture ProjectFactory { get; set; }

public abstract string ProjectType { get; }

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

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

project = getTargetProject?.Invoke(project) ?? project;

if (!onlyCreate)
{
var targetProject = project;
if (serverProject)
{
targetProject = GetSubProject(project, "Server", $"{project.ProjectName}.Server");
}

await targetProject.RunDotNetPublishAsync(noRestore: false);
await project.RunDotNetPublishAsync(noRestore: false);

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

await targetProject.RunDotNetBuildAsync();
await project.RunDotNetBuildAsync();
}

return project;
Expand Down Expand Up @@ -83,6 +81,138 @@ public static bool TryValidateBrowserRequired(BrowserKind browserKind, bool isRe
return isRequired;
}

protected async Task TestBasicInteractionInNewPageAsync(
BrowserKind browserKind,
string listeningUri,
string appName,
BlazorTemplatePages pagesToExclude = BlazorTemplatePages.None,
bool usesAuth = false)
{
if (!BrowserManager.IsAvailable(browserKind))
{
EnsureBrowserAvailable(browserKind);
return;
}

await using var browser = await BrowserManager.GetBrowserInstance(browserKind, BrowserContextInfo);
var page = await browser.NewPageAsync();

Output.WriteLine($"Opening browser at {listeningUri}...");
await page.GotoAsync(listeningUri, new() { WaitUntil = WaitUntilState.NetworkIdle });

await TestBasicInteractionAsync(page, appName, pagesToExclude, usesAuth);

await page.CloseAsync();
}

protected async Task TestBasicInteractionAsync(
IPage page,
string appName,
BlazorTemplatePages pagesToExclude = BlazorTemplatePages.None,
bool usesAuth = false)
{
await page.WaitForSelectorAsync("nav");

if (!pagesToExclude.HasFlag(BlazorTemplatePages.Home))
{
// Initially displays the home page
await page.WaitForSelectorAsync("h1 >> text=Hello, world!");

Assert.Equal("Home", (await page.TitleAsync()).Trim());
}

if (!pagesToExclude.HasFlag(BlazorTemplatePages.Counter))
{
// Can navigate to the counter page
await Task.WhenAll(
page.WaitForNavigationAsync(new() { UrlString = "**/counter" }),
page.WaitForSelectorAsync("h1 >> text=Counter"),
page.WaitForSelectorAsync("p >> text=Current count: 0"),
page.ClickAsync("a[href=counter]"));

// Clicking the counter button works
await IncrementCounterAsync(page);
}

if (usesAuth)
{
await Task.WhenAll(
page.WaitForNavigationAsync(new() { UrlString = "**/Identity/Account/Login**", WaitUntil = WaitUntilState.NetworkIdle }),
page.ClickAsync("text=Log in"));

await Task.WhenAll(
page.WaitForSelectorAsync("[name=\"Input.Email\"]"),
page.WaitForNavigationAsync(new() { UrlString = "**/Identity/Account/Register**", WaitUntil = WaitUntilState.NetworkIdle }),
page.ClickAsync("text=Register as a new user"));

var userName = $"{Guid.NewGuid()}@example.com";
var password = "[PLACEHOLDER]-1a";

await page.TypeAsync("[name=\"Input.Email\"]", userName);
await page.TypeAsync("[name=\"Input.Password\"]", password);
await page.TypeAsync("[name=\"Input.ConfirmPassword\"]", password);

// We will be redirected to the RegisterConfirmation
await Task.WhenAll(
page.WaitForNavigationAsync(new() { UrlString = "**/Identity/Account/RegisterConfirmation**", WaitUntil = WaitUntilState.NetworkIdle }),
page.ClickAsync("#registerSubmit"));

// We will be redirected to the ConfirmEmail
await Task.WhenAll(
page.WaitForNavigationAsync(new() { UrlString = "**/Identity/Account/ConfirmEmail**", WaitUntil = WaitUntilState.NetworkIdle }),
page.ClickAsync("text=Click here to confirm your account"));

// Now we can login
await page.ClickAsync("text=Login");
await page.WaitForSelectorAsync("[name=\"Input.Email\"]");
await page.TypeAsync("[name=\"Input.Email\"]", userName);
await page.TypeAsync("[name=\"Input.Password\"]", password);
await page.ClickAsync("#login-submit");

// Need to navigate to fetch page
await page.GotoAsync(new Uri(page.Url).GetLeftPart(UriPartial.Authority));
Assert.Equal(appName.Trim(), (await page.TitleAsync()).Trim());
}

if (!pagesToExclude.HasFlag(BlazorTemplatePages.Weather))
{
await page.ClickAsync("a[href=weather]");
await page.WaitForSelectorAsync("h1 >> text=Weather");

// Asynchronously loads and displays the table of weather forecasts
await page.WaitForSelectorAsync("table>tbody>tr");
Assert.Equal(5, await page.Locator("p+table>tbody>tr").CountAsync());
}

static async Task IncrementCounterAsync(IPage page)
{
// Allow multiple click attempts because some interactive render modes
// won't be immediately available
const int MaxIncrementAttempts = 5;
const float IncrementTimeoutMilliseconds = 3000f;
for (var i = 0; i < MaxIncrementAttempts; i++)
{
await page.ClickAsync("p+button >> text=Click me");
try
{
await page.WaitForSelectorAsync("p >> text=Current count: 1", new()
{
Timeout = IncrementTimeoutMilliseconds,
});

// The counter successfully incremented, so we're done
return;
}
catch (TimeoutException)
{
// The counter did not increment; try again
}
}

Assert.Fail($"The counter did not increment after {MaxIncrementAttempts} attempts");
}
}

protected void EnsureBrowserAvailable(BrowserKind browserKind)
{
Assert.False(
Expand All @@ -92,4 +222,14 @@ protected void EnsureBrowserAvailable(BrowserKind browserKind)
out var errorMessage),
errorMessage);
}

[Flags]
protected enum BlazorTemplatePages
{
None = 0,
Home = 1,
Counter = 2,
Weather = 4,
All = ~0,
}
}
Loading
Loading