aspnetcore/src/ProjectTemplates/test/SpaTemplateTest/SpaTemplateTestBase.cs

290 lines
13 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.Net.Http;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.AspNetCore.E2ETesting;
using Newtonsoft.Json.Linq;
using OpenQA.Selenium;
using Templates.Test.Helpers;
using Xunit;
using Xunit.Abstractions;
// Turn off parallel test run for Edge as the driver does not support multiple Selenium tests at the same time
#if EDGE
[assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly)]
#endif
namespace Templates.Test.SpaTemplateTest
{
public class SpaTemplateTestBase : BrowserTestBase
{
public SpaTemplateTestBase(
ProjectFactoryFixture projectFactory, BrowserFixture browserFixture, ITestOutputHelper output) : base(browserFixture, output)
{
ProjectFactory = projectFactory;
}
public ProjectFactoryFixture ProjectFactory { get; set; }
public Project Project { get; set; }
// Rather than using [Theory] to pass each of the different values for 'template',
// it's important to distribute the SPA template tests over different test classes
// so they can be run in parallel. Xunit doesn't parallelize within a test class.
protected async Task SpaTemplateImplAsync(
string key,
string template,
bool useLocalDb = false,
bool usesAuth = false)
{
Project = await ProjectFactory.GetOrCreateProject(key, Output);
using var createResult = await Project.RunDotNetNewAsync(template, auth: usesAuth ? "Individual" : null, language: null, useLocalDb);
Assert.True(0 == createResult.ExitCode, ErrorMessages.GetFailedProcessMessage("create/restore", Project, createResult));
// We shouldn't have to do the NPM restore in tests because it should happen
// automatically at build time, but by doing it up front we can avoid having
// multiple NPM installs run concurrently which otherwise causes errors when
// tests run in parallel.
var clientAppSubdirPath = Path.Combine(Project.TemplateOutputDir, "ClientApp");
ValidatePackageJson(clientAppSubdirPath);
var projectFileContents = ReadFile(Project.TemplateOutputDir, $"{Project.ProjectName}.csproj");
if (usesAuth && !useLocalDb)
{
Assert.Contains(".db", projectFileContents);
}
using var npmRestoreResult = await Project.RestoreWithRetryAsync(Output, clientAppSubdirPath);
Assert.True(0 == npmRestoreResult.ExitCode, ErrorMessages.GetFailedProcessMessage("npm restore", Project, npmRestoreResult));
using var lintResult = await ProcessEx.RunViaShellAsync(Output, clientAppSubdirPath, "npm run lint");
Assert.True(0 == lintResult.ExitCode, ErrorMessages.GetFailedProcessMessage("npm run lint", Project, lintResult));
if (template == "react" || template == "reactredux")
{
using var testResult = await ProcessEx.RunViaShellAsync(Output, clientAppSubdirPath, "npm run test");
Assert.True(0 == testResult.ExitCode, ErrorMessages.GetFailedProcessMessage("npm run test", Project, testResult));
}
using 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/netcoreapp3.0/publish and won't be affected by calling build
// later, while the opposite is not true.
using var buildResult = await Project.RunDotNetBuildAsync();
Assert.True(0 == buildResult.ExitCode, ErrorMessages.GetFailedProcessMessage("build", Project, buildResult));
// localdb is not installed on the CI machines, so skip it.
var shouldVisitFetchData = !useLocalDb;
if (usesAuth)
{
using var migrationsResult = await Project.RunDotNetEfCreateMigrationAsync(template);
Assert.True(0 == migrationsResult.ExitCode, ErrorMessages.GetFailedProcessMessage("run EF migrations", Project, migrationsResult));
Project.AssertEmptyMigration(template);
if (shouldVisitFetchData)
{
using var dbUpdateResult = await Project.RunDotNetEfUpdateDatabaseAsync();
Assert.True(0 == dbUpdateResult.ExitCode, ErrorMessages.GetFailedProcessMessage("update database", Project, dbUpdateResult));
}
}
using (var aspNetProcess = Project.StartBuiltProjectAsync())
{
Assert.False(
aspNetProcess.Process.HasExited,
ErrorMessages.GetFailedProcessMessageOrEmpty("Run built project", Project, aspNetProcess.Process));
await WarmUpServer(aspNetProcess);
await aspNetProcess.AssertStatusCode("/", HttpStatusCode.OK, "text/html");
if (BrowserFixture.IsHostAutomationSupported())
{
var (browser, logs) = await BrowserFixture.GetOrCreateBrowserAsync(Output, $"{Project.ProjectName}.build");
aspNetProcess.VisitInBrowser(browser);
TestBasicNavigation(visitFetchData: shouldVisitFetchData, usesAuth, browser, logs);
}
else
{
BrowserFixture.EnforceSupportedConfigurations();
}
}
if (usesAuth)
{
UpdatePublishedSettings();
}
using (var aspNetProcess = Project.StartPublishedProjectAsync())
{
Assert.False(
aspNetProcess.Process.HasExited,
ErrorMessages.GetFailedProcessMessageOrEmpty("Run published project", Project, aspNetProcess.Process));
await WarmUpServer(aspNetProcess);
await aspNetProcess.AssertStatusCode("/", HttpStatusCode.OK, "text/html");
if (BrowserFixture.IsHostAutomationSupported())
{
var (browser, logs) = await BrowserFixture.GetOrCreateBrowserAsync(Output, $"{Project.ProjectName}.publish");
aspNetProcess.VisitInBrowser(browser);
TestBasicNavigation(visitFetchData: shouldVisitFetchData, usesAuth, browser, logs);
}
else
{
BrowserFixture.EnforceSupportedConfigurations();
}
}
}
private void ValidatePackageJson(string clientAppSubdirPath)
{
Assert.True(File.Exists(Path.Combine(clientAppSubdirPath, "package.json")), "Missing a package.json");
var packageJson = JObject.Parse(ReadFile(clientAppSubdirPath, "package.json"));
// NPM package names must match ^(?:@[a-z0-9-~][a-z0-9-._~]*/)?[a-z0-9-~][a-z0-9-._~]*$
var packageName = (string)packageJson["name"];
Regex regex = new Regex("^(?:@[a-z0-9-~][a-z0-9-._~]*/)?[a-z0-9-~][a-z0-9-._~]*$");
Assert.True(regex.IsMatch(packageName), "package.json name is invalid format");
}
private static async Task WarmUpServer(AspNetProcess aspNetProcess)
{
var attempt = 0;
var maxAttempts = 3;
do
{
try
{
attempt++;
var response = await aspNetProcess.SendRequest("/");
if (response.StatusCode == HttpStatusCode.OK)
{
break;
}
}
catch (OperationCanceledException)
{
}
catch (HttpRequestException ex) when (ex.Message.StartsWith("The SSL connection could not be established"))
{
}
await Task.Delay(TimeSpan.FromSeconds(5 * attempt));
} while (attempt < maxAttempts);
}
private void UpdatePublishedSettings()
{
// Hijack here the config file to use the development key during publish.
var appSettings = JObject.Parse(File.ReadAllText(Path.Combine(Project.TemplateOutputDir, "appsettings.json")));
var appSettingsDevelopment = JObject.Parse(File.ReadAllText(Path.Combine(Project.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(Project.TemplatePublishDir, "appsettings.json"), testAppSettings);
}
private void TestBasicNavigation(bool visitFetchData, bool usesAuth, IWebDriver browser, ILogs logs)
{
browser.Exists(By.TagName("ul"));
// <title> element gets project ID injected into it during template execution
browser.Contains(Project.ProjectGuid.Replace(".", "._"), () => browser.Title);
// 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("0", () => browser.FindElement(By.CssSelector("p>strong")).Text);
browser.FindElement(By.CssSelector("p+button")).Click();
browser.Equal("1", () => browser.FindElement(By.CssSelector("p>strong")).Text);
if (visitFetchData)
{
browser.FindElement(By.PartialLinkText("Fetch data")).Click();
if (usesAuth)
{
// We will be redirected to the identity UI
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();
}
// Can navigate to the 'fetch data' page
browser.Contains("fetch-data", () => 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"), TimeSpan.FromSeconds(10));
browser.Equal(5, () => browser.FindElements(By.CssSelector("p+table>tbody>tr")).Count);
}
foreach (var logKind in logs.AvailableLogTypes)
{
var entries = logs.GetLog(logKind);
var badEntries = entries.Where(e => new LogLevel[] { LogLevel.Warning, LogLevel.Severe }.Contains(e.Level));
badEntries = badEntries.Where(e =>
!e.Message.Contains("failed: WebSocket is closed before the connection is established.")
&& !e.Message.Contains("[WDS] Disconnected!")
&& !e.Message.Contains("Timed out connecting to Chrome, retrying"));
Assert.True(badEntries.Count() == 0, "There were Warnings or Errors from the browser." + Environment.NewLine + string.Join(Environment.NewLine, badEntries));
}
}
private void AssertFileExists(string basePath, string path, bool shouldExist)
{
var fullPath = Path.Combine(basePath, path);
var doesExist = File.Exists(fullPath);
if (shouldExist)
{
Assert.True(doesExist, "Expected file to exist, but it doesn't: " + path);
}
else
{
Assert.False(doesExist, "Expected file not to exist, but it does: " + path);
}
}
private string ReadFile(string basePath, string path)
{
AssertFileExists(basePath, path, shouldExist: true);
return File.ReadAllText(Path.Combine(basePath, path));
}
}
}