aspnetcore/src/ProjectTemplates/test/BlazorWasmTemplateTest.cs

405 lines
19 KiB
C#

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.E2ETesting;
using Microsoft.Extensions.CommandLineUtils;
using Newtonsoft.Json.Linq;
using OpenQA.Selenium;
using OpenQA.Selenium.Support.Extensions;
using Templates.Test.Helpers;
using Xunit;
using Xunit.Abstractions;
namespace Templates.Test
{
public class BlazorWasmTemplateTest : BrowserTestBase
{
public BlazorWasmTemplateTest(ProjectFactoryFixture projectFactory, BrowserFixture browserFixture, ITestOutputHelper output)
: base(browserFixture, output)
{
ProjectFactory = projectFactory;
}
public ProjectFactoryFixture ProjectFactory { get; set; }
public override Task InitializeAsync()
{
return InitializeAsync(isolationContext: Guid.NewGuid().ToString());
}
[Fact]
public async Task BlazorWasmStandaloneTemplate_Works()
{
var project = await ProjectFactory.GetOrCreateProject("blazorstandalone", Output);
project.TargetFramework = "netstandard2.1";
var createResult = await project.RunDotNetNewAsync("blazorwasm");
Assert.True(0 == createResult.ExitCode, ErrorMessages.GetFailedProcessMessage("create/restore", project, createResult));
var publishResult = await project.RunDotNetPublishAsync();
Assert.True(0 == publishResult.ExitCode, ErrorMessages.GetFailedProcessMessage("publish", project, publishResult));
var buildResult = await project.RunDotNetBuildAsync();
Assert.True(0 == buildResult.ExitCode, ErrorMessages.GetFailedProcessMessage("build", project, buildResult));
await BuildAndRunTest(project.ProjectName, project);
using var serveProcess = RunPublishedStandaloneBlazorProject(project);
}
private ProcessEx RunPublishedStandaloneBlazorProject(Project project)
{
var publishDir = Path.Combine(project.TemplatePublishDir, "wwwroot");
AspNetProcess.EnsureDevelopmentCertificates();
Output.WriteLine("Running dotnet serve on published output...");
var serveProcess = ProcessEx.Run(Output, publishDir, DotNetMuxer.MuxerPathOrDefault(), "serve -S");
// Todo: Use dynamic port assignment: https://github.com/natemcmaster/dotnet-serve/pull/40/files
var listeningUri = "https://localhost:8080";
Output.WriteLine($"Opening browser at {listeningUri}...");
Browser.Navigate().GoToUrl(listeningUri);
TestBasicNavigation(project.ProjectName);
return serveProcess;
}
[Fact]
public async Task BlazorWasmHostedTemplate_Works()
{
var project = await ProjectFactory.GetOrCreateProject("blazorhosted", Output);
var createResult = await project.RunDotNetNewAsync("blazorwasm", args: new[] { "--hosted" });
Assert.True(0 == createResult.ExitCode, ErrorMessages.GetFailedProcessMessage("create/restore", project, createResult));
var serverProject = GetSubProject(project, "Server", $"{project.ProjectName}.Server");
var publishResult = await serverProject.RunDotNetPublishAsync();
Assert.True(0 == publishResult.ExitCode, ErrorMessages.GetFailedProcessMessage("publish", serverProject, publishResult));
var buildResult = await serverProject.RunDotNetBuildAsync();
Assert.True(0 == buildResult.ExitCode, ErrorMessages.GetFailedProcessMessage("build", serverProject, buildResult));
await BuildAndRunTest(project.ProjectName, serverProject);
using var aspNetProcess = serverProject.StartPublishedProjectAsync();
Assert.False(
aspNetProcess.Process.HasExited,
ErrorMessages.GetFailedProcessMessageOrEmpty("Run published project", serverProject, aspNetProcess.Process));
await aspNetProcess.AssertStatusCode("/", HttpStatusCode.OK, "text/html");
if (BrowserFixture.IsHostAutomationSupported())
{
aspNetProcess.VisitInBrowser(Browser);
TestBasicNavigation(project.ProjectName);
}
else
{
BrowserFixture.EnforceSupportedConfigurations();
}
}
[Fact]
public async Task BlazorWasmPwaTemplate_Works()
{
var project = await ProjectFactory.GetOrCreateProject("blazorpwa", Output);
project.TargetFramework = "netstandard2.1";
var createResult = await project.RunDotNetNewAsync("blazorwasm", args: new[] { "--pwa" });
Assert.True(0 == createResult.ExitCode, ErrorMessages.GetFailedProcessMessage("create/restore", project, createResult));
var publishResult = await project.RunDotNetPublishAsync();
Assert.True(0 == publishResult.ExitCode, ErrorMessages.GetFailedProcessMessage("publish", project, publishResult));
var buildResult = await project.RunDotNetBuildAsync();
Assert.True(0 == buildResult.ExitCode, ErrorMessages.GetFailedProcessMessage("build", project, buildResult));
await BuildAndRunTest(project.ProjectName, project);
var publishDir = Path.Combine(project.TemplatePublishDir, "wwwroot");
// When publishing the PWA template, we generate an assets manifest
// and move service-worker.published.js to overwrite service-worker.js
Assert.False(File.Exists(Path.Combine(publishDir, "service-worker.published.js")), "service-worker.published.js should not be published");
Assert.True(File.Exists(Path.Combine(publishDir, "service-worker.js")), "service-worker.js should be published");
Assert.True(File.Exists(Path.Combine(publishDir, "service-worker-assets.js")), "service-worker-assets.js should be published");
using (var serverProcess = RunPublishedStandaloneBlazorProject(project))
{
// We want to use this form to ensure that it gets disposed right after the test.
}
// Todo: Use dynamic port assignment: https://github.com/natemcmaster/dotnet-serve/pull/40/files
var listeningUri = "https://localhost:8080";
// The PWA template supports offline use. By now, the browser should have cached everything it needs,
// so we can continue working even without the server.
ValidateAppWorksOffline(project, listeningUri);
}
private void ValidateAppWorksOffline(Project project, string listeningUri)
{
Browser.Navigate().GoToUrl("about:blank"); // Be sure we're really reloading
Output.WriteLine($"Opening browser without corresponding server at {listeningUri}...");
Browser.Navigate().GoToUrl(listeningUri);
TestBasicNavigation(project.ProjectName);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task BlazorWasmHostedTemplate_IndividualAuth_Works(bool useLocalDb)
{
var project = await ProjectFactory.GetOrCreateProject("blazorhostedindividual" + (useLocalDb ? "uld" : ""), Output);
var createResult = await project.RunDotNetNewAsync("blazorwasm", args: new[] { "--hosted", "-au", "Individual", useLocalDb ? "-uld" : "" });
Assert.True(0 == createResult.ExitCode, ErrorMessages.GetFailedProcessMessage("create/restore", project, createResult));
var serverProject = GetSubProject(project, "Server", $"{project.ProjectName}.Server");
var serverProjectFileContents = ReadFile(serverProject.TemplateOutputDir, $"{serverProject.ProjectName}.csproj");
if (!useLocalDb)
{
Assert.Contains(".db", serverProjectFileContents);
}
var appSettings = ReadFile(serverProject.TemplateOutputDir, "appSettings.json");
var element = JsonSerializer.Deserialize<JsonElement>(appSettings);
var clientsProperty = element.GetProperty("IdentityServer").EnumerateObject().Single().Value.EnumerateObject().Single();
var replacedSection = element.GetRawText().Replace(clientsProperty.Name, serverProject.ProjectName.Replace(".Server", ".Client"));
var appSettingsPath = Path.Combine(serverProject.TemplateOutputDir, "appSettings.json");
File.WriteAllText(appSettingsPath, replacedSection);
var publishResult = await serverProject.RunDotNetPublishAsync();
Assert.True(0 == publishResult.ExitCode, ErrorMessages.GetFailedProcessMessage("publish", serverProject, publishResult));
// 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.
var buildResult = await serverProject.RunDotNetBuildAsync();
Assert.True(0 == buildResult.ExitCode, ErrorMessages.GetFailedProcessMessage("build", serverProject, buildResult));
var migrationsResult = await serverProject.RunDotNetEfCreateMigrationAsync("blazorwasm");
Assert.True(0 == migrationsResult.ExitCode, ErrorMessages.GetFailedProcessMessage("run EF migrations", serverProject, migrationsResult));
serverProject.AssertEmptyMigration("blazorwasm");
if (useLocalDb)
{
using var dbUpdateResult = await serverProject.RunDotNetEfUpdateDatabaseAsync();
Assert.True(0 == dbUpdateResult.ExitCode, ErrorMessages.GetFailedProcessMessage("update database", serverProject, dbUpdateResult));
}
await BuildAndRunTest(project.ProjectName, serverProject, usesAuth: true);
UpdatePublishedSettings(serverProject);
using var aspNetProcess = serverProject.StartPublishedProjectAsync();
Assert.False(
aspNetProcess.Process.HasExited,
ErrorMessages.GetFailedProcessMessageOrEmpty("Run published project", serverProject, aspNetProcess.Process));
await aspNetProcess.AssertStatusCode("/", HttpStatusCode.OK, "text/html");
if (BrowserFixture.IsHostAutomationSupported())
{
aspNetProcess.VisitInBrowser(Browser);
TestBasicNavigation(project.ProjectName, usesAuth: true);
}
else
{
BrowserFixture.EnforceSupportedConfigurations();
}
}
[Fact]
public async Task BlazorWasmStandaloneTemplate_IndividualAuth_Works()
{
var project = await ProjectFactory.GetOrCreateProject("blazorstandaloneindividual", Output);
project.TargetFramework = "netstandard2.1";
var createResult = await project.RunDotNetNewAsync("blazorwasm", args: new[] {
"-au",
"Individual",
"--authority",
"https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration",
"--client-id",
"sample-client-id"
});
Assert.True(0 == createResult.ExitCode, ErrorMessages.GetFailedProcessMessage("create/restore", project, createResult));
var publishResult = await project.RunDotNetPublishAsync();
Assert.True(0 == publishResult.ExitCode, ErrorMessages.GetFailedProcessMessage("publish", project, publishResult));
// 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.
var buildResult = await project.RunDotNetBuildAsync();
Assert.True(0 == buildResult.ExitCode, ErrorMessages.GetFailedProcessMessage("build", project, buildResult));
// We don't want to test the auth flow as we don't have the required settings to talk to a third-party IdP
// but we want to make sure that we are able to run the app without errors.
// That will at least test that we are able to initialize and retrieve the configuration from the IdP
// for that, we use the common microsoft tenant.
await BuildAndRunTest(project.ProjectName, project, usesAuth: false);
using var serveProcess = RunPublishedStandaloneBlazorProject(project);
}
protected async Task BuildAndRunTest(string appName, Project project, bool usesAuth = false)
{
using var aspNetProcess = project.StartBuiltProjectAsync();
Assert.False(
aspNetProcess.Process.HasExited,
ErrorMessages.GetFailedProcessMessageOrEmpty("Run built project", project, aspNetProcess.Process));
await aspNetProcess.AssertStatusCode("/", HttpStatusCode.OK, "text/html");
if (BrowserFixture.IsHostAutomationSupported())
{
aspNetProcess.VisitInBrowser(Browser);
TestBasicNavigation(appName, usesAuth);
}
else
{
BrowserFixture.EnforceSupportedConfigurations();
}
}
private void TestBasicNavigation(string appName, bool usesAuth = false)
{
// Start fresh always
if (usesAuth)
{
Browser.ExecuteJavaScript("sessionStorage.clear()");
Browser.ExecuteJavaScript("localStorage.clear()");
Browser.Manage().Cookies.DeleteAllCookies();
Browser.Navigate().Refresh();
}
// Give components.server enough time to load so that it can replace
// the prerendered content before we start making assertions.
Thread.Sleep(5000);
Browser.Exists(By.TagName("ul"));
// <title> element gets project ID injected into it during template execution
Browser.Equal(appName.Trim(), () => Browser.Title.Trim());
// Initially displays the home page
Browser.Equal("Hello, world!", () => Browser.FindElement(By.TagName("h1")).Text);
// Can navigate to the counter page
Browser.FindElement(By.PartialLinkText("Counter")).Click();
Browser.Contains("counter", () => Browser.Url);
Browser.Equal("Counter", () => Browser.FindElement(By.TagName("h1")).Text);
// Clicking the counter button works
Browser.Equal("Current count: 0", () => Browser.FindElement(By.CssSelector("h1 + p")).Text);
Browser.FindElement(By.CssSelector("p+button")).Click();
Browser.Equal("Current count: 1", () => Browser.FindElement(By.CssSelector("h1 + p")).Text);
if (usesAuth)
{
Browser.FindElement(By.PartialLinkText("Log in")).Click();
Browser.Contains("/Identity/Account/Login", () => Browser.Url);
Browser.FindElement(By.PartialLinkText("Register as a new user")).Click();
var userName = $"{Guid.NewGuid()}@example.com";
var password = $"!Test.Password1$";
Browser.Exists(By.Name("Input.Email"));
Browser.FindElement(By.Name("Input.Email")).SendKeys(userName);
Browser.FindElement(By.Name("Input.Password")).SendKeys(password);
Browser.FindElement(By.Name("Input.ConfirmPassword")).SendKeys(password);
Browser.FindElement(By.Id("registerSubmit")).Click();
// We will be redirected to the RegisterConfirmation
Browser.Contains("/Identity/Account/RegisterConfirmation", () => Browser.Url);
Browser.FindElement(By.PartialLinkText("Click here to confirm your account")).Click();
// We will be redirected to the ConfirmEmail
Browser.Contains("/Identity/Account/ConfirmEmail", () => Browser.Url);
// Now we can login
Browser.FindElement(By.PartialLinkText("Login")).Click();
Browser.Exists(By.Name("Input.Email"));
Browser.FindElement(By.Name("Input.Email")).SendKeys(userName);
Browser.FindElement(By.Name("Input.Password")).SendKeys(password);
Browser.FindElement(By.Id("login-submit")).Click();
// Need to navigate to fetch page
Browser.Navigate().GoToUrl(new Uri(Browser.Url).GetLeftPart(UriPartial.Authority));
Browser.Equal(appName.Trim(), () => Browser.Title.Trim());
}
// Can navigate to the 'fetch data' page
Browser.FindElement(By.PartialLinkText("Fetch data")).Click();
Browser.Contains("fetchdata", () => Browser.Url);
Browser.Equal("Weather forecast", () => Browser.FindElement(By.TagName("h1")).Text);
// Asynchronously loads and displays the table of weather forecasts
Browser.Exists(By.CssSelector("table>tbody>tr"));
Browser.Equal(5, () => Browser.FindElements(By.CssSelector("p+table>tbody>tr")).Count);
}
private string ReadFile(string basePath, string path)
{
var fullPath = Path.Combine(basePath, path);
var doesExist = File.Exists(fullPath);
Assert.True(doesExist, $"Expected file to exist, but it doesn't: {path}");
return File.ReadAllText(Path.Combine(basePath, path));
}
private Project GetSubProject(Project project, string projectDirectory, string projectName)
{
var subProjectDirectory = Path.Combine(project.TemplateOutputDir, projectDirectory);
if (!Directory.Exists(subProjectDirectory))
{
throw new DirectoryNotFoundException($"Directory {subProjectDirectory} was not found.");
}
var subProject = new Project
{
DotNetNewLock = project.DotNetNewLock,
NodeLock = project.NodeLock,
Output = project.Output,
DiagnosticsMessageSink = project.DiagnosticsMessageSink,
ProjectName = projectName,
TemplateOutputDir = subProjectDirectory,
};
return subProject;
}
private void UpdatePublishedSettings(Project serverProject)
{
// Hijack here the config file to use the development key during publish.
var appSettings = JObject.Parse(File.ReadAllText(Path.Combine(serverProject.TemplateOutputDir, "appsettings.json")));
var appSettingsDevelopment = JObject.Parse(File.ReadAllText(Path.Combine(serverProject.TemplateOutputDir, "appsettings.Development.json")));
((JObject)appSettings["IdentityServer"]).Merge(appSettingsDevelopment["IdentityServer"]);
((JObject)appSettings["IdentityServer"]).Merge(new
{
IdentityServer = new
{
Key = new
{
FilePath = "./tempkey.json"
}
}
});
var testAppSettings = appSettings.ToString();
File.WriteAllText(Path.Combine(serverProject.TemplatePublishDir, "appsettings.json"), testAppSettings);
}
}
}