290 lines
13 KiB
C#
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));
|
|
}
|
|
}
|
|
}
|