654 lines
31 KiB
C#
654 lines
31 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.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Net.Http;
|
|
using System.Net.Http.Headers;
|
|
using System.Text.Json;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.AspNetCore.E2ETesting;
|
|
using Microsoft.AspNetCore.Internal;
|
|
using Microsoft.AspNetCore.Testing;
|
|
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()
|
|
{
|
|
// Additional arguments are needed. See: https://github.com/dotnet/aspnetcore/issues/24278
|
|
Environment.SetEnvironmentVariable("EnableDefaultScopedCssItems", "true");
|
|
|
|
var project = await ProjectFactory.GetOrCreateProject("blazorstandalone", Output);
|
|
project.RuntimeIdentifier = "browser-wasm";
|
|
|
|
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));
|
|
|
|
// The service worker assets manifest isn't generated for non-PWA projects
|
|
var publishDir = Path.Combine(project.TemplatePublishDir, "wwwroot");
|
|
Assert.False(File.Exists(Path.Combine(publishDir, "service-worker-assets.js")), "Non-PWA templates should not produce service-worker-assets.js");
|
|
|
|
var buildResult = await project.RunDotNetBuildAsync();
|
|
Assert.True(0 == buildResult.ExitCode, ErrorMessages.GetFailedProcessMessage("build", project, buildResult));
|
|
|
|
await BuildAndRunTest(project.ProjectName, project);
|
|
|
|
if (BrowserFixture.IsHostAutomationSupported())
|
|
{
|
|
var (serveProcess, listeningUri) = RunPublishedStandaloneBlazorProject(project);
|
|
using (serveProcess)
|
|
{
|
|
Output.WriteLine($"Opening browser at {listeningUri}...");
|
|
Browser.Navigate().GoToUrl(listeningUri);
|
|
TestBasicNavigation(project.ProjectName);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
BrowserFixture.EnforceSupportedConfigurations();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
[QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/20172")]
|
|
public async Task BlazorWasmHostedTemplate_Works()
|
|
{
|
|
// Additional arguments are needed. See: https://github.com/dotnet/aspnetcore/issues/24278
|
|
Environment.SetEnvironmentVariable("EnableDefaultScopedCssItems", "true");
|
|
|
|
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");
|
|
await AssertCompressionFormat(aspNetProcess, "br");
|
|
if (BrowserFixture.IsHostAutomationSupported())
|
|
{
|
|
aspNetProcess.VisitInBrowser(Browser);
|
|
TestBasicNavigation(project.ProjectName);
|
|
}
|
|
else
|
|
{
|
|
BrowserFixture.EnforceSupportedConfigurations();
|
|
}
|
|
}
|
|
|
|
private static async Task AssertCompressionFormat(AspNetProcess aspNetProcess, string expectedEncoding)
|
|
{
|
|
var response = await aspNetProcess.SendRequest(() =>
|
|
{
|
|
var request = new HttpRequestMessage(HttpMethod.Get, new Uri(aspNetProcess.ListeningUri, "/_framework/blazor.boot.json"));
|
|
// These are the same as chrome
|
|
request.Headers.AcceptEncoding.Clear();
|
|
request.Headers.AcceptEncoding.Add(StringWithQualityHeaderValue.Parse("gzip"));
|
|
request.Headers.AcceptEncoding.Add(StringWithQualityHeaderValue.Parse("deflate"));
|
|
request.Headers.AcceptEncoding.Add(StringWithQualityHeaderValue.Parse("br"));
|
|
|
|
return request;
|
|
});
|
|
Assert.Equal(expectedEncoding, response.Content.Headers.ContentEncoding.Single());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task BlazorWasmStandalonePwaTemplate_Works()
|
|
{
|
|
// Additional arguments are needed. See: https://github.com/dotnet/aspnetcore/issues/24278
|
|
Environment.SetEnvironmentVariable("EnableDefaultScopedCssItems", "true");
|
|
|
|
var project = await ProjectFactory.GetOrCreateProject("blazorstandalonepwa", Output);
|
|
project.RuntimeIdentifier = "browser-wasm";
|
|
|
|
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);
|
|
|
|
ValidatePublishedServiceWorker(project);
|
|
|
|
if (BrowserFixture.IsHostAutomationSupported())
|
|
{
|
|
var (serveProcess, listeningUri) = RunPublishedStandaloneBlazorProject(project);
|
|
using (serveProcess)
|
|
{
|
|
Output.WriteLine($"Opening browser at {listeningUri}...");
|
|
Browser.Navigate().GoToUrl(listeningUri);
|
|
TestBasicNavigation(project.ProjectName);
|
|
}
|
|
|
|
// 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, skipFetchData: false);
|
|
}
|
|
else
|
|
{
|
|
BrowserFixture.EnforceSupportedConfigurations();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task BlazorWasmHostedPwaTemplate_Works()
|
|
{
|
|
// Additional arguments are needed. See: https://github.com/dotnet/aspnetcore/issues/24278
|
|
Environment.SetEnvironmentVariable("EnableDefaultScopedCssItems", "true");
|
|
|
|
var project = await ProjectFactory.GetOrCreateProject("blazorhostedpwa", Output);
|
|
|
|
var createResult = await project.RunDotNetNewAsync("blazorwasm", args: new[] { "--hosted", "--pwa" });
|
|
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);
|
|
|
|
ValidatePublishedServiceWorker(serverProject);
|
|
|
|
string listeningUri = null;
|
|
if (BrowserFixture.IsHostAutomationSupported())
|
|
{
|
|
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");
|
|
aspNetProcess.VisitInBrowser(Browser);
|
|
TestBasicNavigation(project.ProjectName);
|
|
|
|
// Note: we don't want to use aspNetProcess.ListeningUri because that isn't necessarily the HTTPS URI
|
|
var browserUri = new Uri(Browser.Url);
|
|
listeningUri = $"{browserUri.Scheme}://{browserUri.Authority}";
|
|
}
|
|
|
|
// 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.
|
|
// Since this is the hosted project, backend APIs won't work offline, so we need to skip "fetchdata"
|
|
ValidateAppWorksOffline(project, listeningUri, skipFetchData: true);
|
|
}
|
|
else
|
|
{
|
|
BrowserFixture.EnforceSupportedConfigurations();
|
|
}
|
|
}
|
|
|
|
private void ValidatePublishedServiceWorker(Project 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");
|
|
|
|
// We automatically append the SWAM version as a comment in the published service worker file
|
|
var serviceWorkerAssetsManifestContents = ReadFile(publishDir, "service-worker-assets.js");
|
|
var serviceWorkerContents = ReadFile(publishDir, "service-worker.js");
|
|
|
|
// Parse the "version": "..." value from the SWAM, and check it's in the service worker
|
|
var serviceWorkerAssetsManifestVersionMatch = new Regex(@"^\s*\""version\"":\s*(\""[^\""]+\"")", RegexOptions.Multiline)
|
|
.Match(serviceWorkerAssetsManifestContents);
|
|
Assert.True(serviceWorkerAssetsManifestVersionMatch.Success);
|
|
var serviceWorkerAssetsManifestVersionJson = serviceWorkerAssetsManifestVersionMatch.Groups[1].Captures[0].Value;
|
|
var serviceWorkerAssetsManifestVersion = JsonSerializer.Deserialize<string>(serviceWorkerAssetsManifestVersionJson);
|
|
Assert.True(serviceWorkerContents.Contains($"/* Manifest version: {serviceWorkerAssetsManifestVersion} */", StringComparison.Ordinal));
|
|
}
|
|
|
|
private void ValidateAppWorksOffline(Project project, string listeningUri, bool skipFetchData)
|
|
{
|
|
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, skipFetchData: skipFetchData);
|
|
}
|
|
|
|
[ConditionalFact]
|
|
// LocalDB doesn't work on non Windows platforms
|
|
[OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)]
|
|
public Task BlazorWasmHostedTemplate_IndividualAuth_Works_WithLocalDB()
|
|
{
|
|
return BlazorWasmHostedTemplate_IndividualAuth_Works(true);
|
|
}
|
|
|
|
[Fact]
|
|
public Task BlazorWasmHostedTemplate_IndividualAuth_Works_WithOutLocalDB()
|
|
{
|
|
return BlazorWasmHostedTemplate_IndividualAuth_Works(false);
|
|
}
|
|
|
|
private async Task BlazorWasmHostedTemplate_IndividualAuth_Works(bool useLocalDb)
|
|
{
|
|
// Additional arguments are needed. See: https://github.com/dotnet/aspnetcore/issues/24278
|
|
Environment.SetEnvironmentVariable("EnableDefaultScopedCssItems", "true");
|
|
|
|
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)
|
|
{
|
|
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);
|
|
|
|
if (BrowserFixture.IsHostAutomationSupported())
|
|
{
|
|
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");
|
|
|
|
aspNetProcess.VisitInBrowser(Browser);
|
|
TestBasicNavigation(project.ProjectName, usesAuth: true);
|
|
}
|
|
else
|
|
{
|
|
BrowserFixture.EnforceSupportedConfigurations();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
[QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/23639")]
|
|
public async Task BlazorWasmStandaloneTemplate_IndividualAuth_Works()
|
|
{
|
|
// Additional arguments are needed. See: https://github.com/dotnet/aspnetcore/issues/24278
|
|
Environment.SetEnvironmentVariable("EnableDefaultScopedCssItems", "true");
|
|
|
|
var project = await ProjectFactory.GetOrCreateProject("blazorstandaloneindividual", Output);
|
|
project.RuntimeIdentifier = "browser-wasm";
|
|
|
|
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);
|
|
|
|
if (BrowserFixture.IsHostAutomationSupported())
|
|
{
|
|
var (serveProcess, listeningUri) = RunPublishedStandaloneBlazorProject(project);
|
|
using (serveProcess)
|
|
{
|
|
Output.WriteLine($"Opening browser at {listeningUri}...");
|
|
Browser.Navigate().GoToUrl(listeningUri);
|
|
TestBasicNavigation(project.ProjectName);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
BrowserFixture.EnforceSupportedConfigurations();
|
|
}
|
|
}
|
|
|
|
public static TheoryData<TemplateInstance> TemplateData => new TheoryData<TemplateInstance>
|
|
{
|
|
new TemplateInstance(
|
|
"blazorwasmhostedaadb2c", "-ho",
|
|
"-au", "IndividualB2C",
|
|
"--aad-b2c-instance", "example.b2clogin.com",
|
|
"-ssp", "b2c_1_siupin",
|
|
"--client-id", "clientId",
|
|
"--domain", "my-domain",
|
|
"--default-scope", "full",
|
|
"--app-id-uri", "ApiUri",
|
|
"--api-client-id", "1234123413241324"),
|
|
new TemplateInstance(
|
|
"blazorwasmhostedaad", "-ho",
|
|
"-au", "SingleOrg",
|
|
"--domain", "my-domain",
|
|
"--tenant-id", "tenantId",
|
|
"--client-id", "clientId",
|
|
"--default-scope", "full",
|
|
"--app-id-uri", "ApiUri",
|
|
"--api-client-id", "1234123413241324"),
|
|
new TemplateInstance(
|
|
"blazorwasmstandaloneaadb2c",
|
|
"-au", "IndividualB2C",
|
|
"--aad-b2c-instance", "example.b2clogin.com",
|
|
"-ssp", "b2c_1_siupin",
|
|
"--client-id", "clientId",
|
|
"--domain", "my-domain"),
|
|
new TemplateInstance(
|
|
"blazorwasmstandaloneaad",
|
|
"-au", "SingleOrg",
|
|
"--domain", "my-domain",
|
|
"--tenant-id", "tenantId",
|
|
"--client-id", "clientId"),
|
|
};
|
|
|
|
public class TemplateInstance
|
|
{
|
|
public TemplateInstance(string name, params string[] arguments)
|
|
{
|
|
Name = name;
|
|
Arguments = arguments;
|
|
}
|
|
|
|
public string Name { get; }
|
|
public string[] Arguments { get; }
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(TemplateData))]
|
|
public async Task BlazorWasmHostedTemplate_AzureActiveDirectoryTemplate_Works(TemplateInstance instance)
|
|
{
|
|
var project = await ProjectFactory.GetOrCreateProject(instance.Name, Output);
|
|
project.TargetFramework = "netstandard2.1";
|
|
|
|
var createResult = await project.RunDotNetNewAsync("blazorwasm", args: instance.Arguments);
|
|
|
|
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));
|
|
}
|
|
|
|
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, bool skipFetchData = 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());
|
|
}
|
|
|
|
if (!skipFetchData)
|
|
{
|
|
// 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
|
|
{
|
|
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);
|
|
}
|
|
|
|
private (ProcessEx, string url) RunPublishedStandaloneBlazorProject(Project project)
|
|
{
|
|
var publishDir = Path.Combine(project.TemplatePublishDir, "wwwroot");
|
|
|
|
Output.WriteLine("Running dotnet serve on published output...");
|
|
var developmentCertificate = DevelopmentCertificate.Create(project.TemplateOutputDir);
|
|
var serveProcess = ProcessEx.Run(Output, publishDir, DotNetMuxer.MuxerPathOrDefault(), $"serve -S --pfx \"{developmentCertificate.CertificatePath}\" --pfx-pwd \"{developmentCertificate.CertificatePassword}\" --port 0");
|
|
var listeningUri = ResolveListeningUrl(serveProcess);
|
|
return (serveProcess, listeningUri);
|
|
}
|
|
|
|
private static string ResolveListeningUrl(ProcessEx process)
|
|
{
|
|
var buffer = new List<string>();
|
|
try
|
|
{
|
|
foreach (var line in process.OutputLinesAsEnumerable)
|
|
{
|
|
if (line != null)
|
|
{
|
|
buffer.Add(line);
|
|
if (line.Trim().Contains("https://", StringComparison.Ordinal) || line.Trim().Contains("http://", StringComparison.Ordinal))
|
|
{
|
|
return line.Trim();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
}
|
|
|
|
throw new InvalidOperationException(@$"Couldn't find listening url:
|
|
{string.Join(Environment.NewLine, buffer.Append(process.Error))}");
|
|
}
|
|
}
|
|
}
|