[Infrastructure improvements] (#8275)

* Improved selenium start and tear down
  * Selenium is set up and torn down in an assembly fixture.
  * Selenium is initialized lazily and in a non-blocking way.
  * Selenium processes are tracked as part of the build and their pids
    written to a file on disk for cleanup in the event of unexpected
    termination of the test process.
  * Browser fixture retries with linear backoff to create a remote
    driver. Under heavy load (like when we are doing a simultaneous NPM
    restore) the selenium server can become unresponsive so we retry
    three times, with a longer comand timeout allowance each time up to
    a max of 3 minutes.
* Moved test project setup to build time instead of runtime.
  * Added target PrepareForTest to create the required files for testing
    * The template creation folder.
    * The template props file to use our built packages.
    * The folder for the custom hive.
  * Added assembly metadata attributes to find all the data we need to
    run the tests.
    * Path to the artifacts shipping packages folder.
    * Path to the artifacts non-shipping packages folder.
    * Path to the test templates creation folder.
    * Path to use for the custom templating hive used in tests.
  * Proper cleanup as part of the build
    * Remove the test templates creation folder.
    * Remove the test packages restore path.
    * Recreate the test templates creation folder.
    * Recreate the test packages restore path.
  * Generated Directory.Build.Props and Directory.Build.Targets in the
    test templates creation folder.
  * Cleaned up potentially stale templatetestsprops.
* Improved test flows
  * Initialization is done lazily and asynchronously.
    * Selenium
    * Browser fixture
    * Template initialization.
  * Flattened test flows to avoid assertions inside deep callstacks.
    * All assertions happen at the test level with improved error messages.
      * With the exception of the migrations assertions.
    * Assertions contain information about which step failed, for what
      project and what failure details.
  * Broke down tests to perform individual steps instead of mixing build
    and publish.
    * Publish project.
    * Build project. (Debug)
    * Run built project.
    * Run published project.
  * Concentrated build logic into the Project class.
    * Context between the different steps of a test is maintained in
      this class.
    * All operations that require coordination are performed within this
      class.
      * There is a lock for dotnet and a lock for nodejs. When building
        SPAs we acquire the nodejs lock to correctly prevent multiple
        runs of nodejs in parallel.

[ApiAuthorization template cleanups]
  * Fix preview3 issues with breaking changes on Entity framework by
    manually configuring the model in ApiAuthorizationDbContext.
  * Add app.db to the project file when using local db.
  * Fix linting errors on angular template.
  * Fix react tests
  * Add tests to cover new auth options in the SPA templates.
This commit is contained in:
Javier Calvarro Nelson 2019-03-20 08:44:20 +01:00 committed by GitHub
parent 0456c9dcc9
commit 9f1a978230
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
76 changed files with 3459 additions and 1560 deletions

View File

@ -341,6 +341,8 @@ jobs:
beforeBuild:
- bash: "./eng/scripts/install-nginx-linux.sh"
displayName: Installing Nginx
- bash: "echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p"
displayName: Increase inotify limit
afterBuild:
- bash: ./build.sh --no-build --ci --test -p:RunFlakyTests=true
displayName: Run Flaky Tests

View File

@ -125,6 +125,8 @@ jobs:
displayName: Install JDK 11
- powershell: Write-Host "##vso[task.prependpath]$env:JAVA_HOME\bin"
displayName: Prepend JAVA bin folder to the PATH.
- powershell: Write-Host "##vso[task.setvariable variable=SeleniumProcessTrackingFolder]$(BuildDirectory)\obj\selenium\"
displayName: Add Selenium process tracking folder environment variable
- powershell: ./eng/scripts/InstallGoogleChrome.ps1
displayName: Install chrome
- ${{ if and(eq(variables['System.TeamProject'], 'internal'), eq(parameters.agentOs, 'Windows'), eq(parameters.codeSign, 'true')) }}:

View File

@ -22,6 +22,22 @@ function _killJavaInstances() {
}
}
function _killSeleniumTrackedProcesses() {
$files = Get-ChildItem $env:SeleniumProcessTrackingFolder -ErrorAction SilentlyContinue;
# PID files have a format of <<pid>>.<<guid>>.pid
$pids = $files |
Where-Object { $_.Name -match "([0-9]+)\..*?.pid"; } |
Foreach-Object { $Matches[1] };
foreach ($currentPid in $pids) {
try {
& cmd /c "taskkill /T /F /PID $currentPid 2>&1"
} catch {
Write-Host "Failed to kill process: $currentPid"
}
}
}
_kill dotnet.exe
_kill testhost.exe
_kill iisexpress.exe
@ -35,6 +51,7 @@ _kill chrome.exe
_kill h2spec.exe
_kill WerFault.exe
_killJavaInstances
_killSeleniumTrackedProcesses
if (Get-Command iisreset -ErrorAction ignore) {
iisreset /restart

View File

@ -0,0 +1,8 @@
// 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 Microsoft.AspNetCore.E2ETesting;
using Xunit;
[assembly: TestFramework("Microsoft.AspNetCore.E2ETesting.XunitTestFrameworkWithAssemblyFixture", "Microsoft.AspNetCore.Components.E2ETests")]
[assembly: AssemblyFixture(typeof(SeleniumStandaloneServer))]

View File

@ -32,7 +32,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
appElement.FindElement(By.Id("run-without-dispatch")).Click();
WaitAssert.Contains(
Browser.Contains(
$"{typeof(InvalidOperationException).FullName}: The current thread is not associated with the renderer's synchronization context",
() => result.Text);
}

View File

@ -8,6 +8,7 @@ using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI;
using System;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
using Xunit.Abstractions;
@ -23,12 +24,14 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{
_serverFixture.Environment = AspNetEnvironment.Development;
_serverFixture.BuildWebHostMethod = ComponentsApp.Server.Program.BuildWebHost;
}
protected override void InitializeAsyncCore()
{
Navigate("/", noReload: false);
WaitUntilLoaded();
}
[Fact]
public void HasTitle()
{
@ -56,13 +59,13 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
Browser.FindElement(By.LinkText("Counter")).Click();
// Verify we're now on the counter page, with that nav link (only) highlighted
WaitAssert.Equal("Counter", () => Browser.FindElement(mainHeaderSelector).Text);
Browser.Equal("Counter", () => Browser.FindElement(mainHeaderSelector).Text);
Assert.Collection(Browser.FindElements(activeNavLinksSelector),
item => Assert.Equal("Counter", item.Text));
// Verify we can navigate back to home too
Browser.FindElement(By.LinkText("Home")).Click();
WaitAssert.Equal("Hello, world!", () => Browser.FindElement(mainHeaderSelector).Text);
Browser.Equal("Hello, world!", () => Browser.FindElement(mainHeaderSelector).Text);
Assert.Collection(Browser.FindElements(activeNavLinksSelector),
item => Assert.Equal("Home", item.Text));
}
@ -72,7 +75,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{
// Navigate to "Counter"
Browser.FindElement(By.LinkText("Counter")).Click();
WaitAssert.Equal("Counter", () => Browser.FindElement(By.TagName("h1")).Text);
Browser.Equal("Counter", () => Browser.FindElement(By.TagName("h1")).Text);
// Observe the initial value is zero
var countDisplayElement = Browser.FindElement(By.CssSelector("h1 + p"));
@ -81,11 +84,11 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
// Click the button; see it counts
var button = Browser.FindElement(By.CssSelector(".main button"));
button.Click();
WaitAssert.Equal("Current count: 1", () => countDisplayElement.Text);
Browser.Equal("Current count: 1", () => countDisplayElement.Text);
button.Click();
WaitAssert.Equal("Current count: 2", () => countDisplayElement.Text);
Browser.Equal("Current count: 2", () => countDisplayElement.Text);
button.Click();
WaitAssert.Equal("Current count: 3", () => countDisplayElement.Text);
Browser.Equal("Current count: 3", () => countDisplayElement.Text);
}
[Fact]
@ -93,7 +96,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{
// Navigate to "Fetch Data"
Browser.FindElement(By.LinkText("Fetch data")).Click();
WaitAssert.Equal("Weather forecast", () => Browser.FindElement(By.TagName("h1")).Text);
Browser.Equal("Weather forecast", () => Browser.FindElement(By.TagName("h1")).Text);
// Wait until loaded
var tableSelector = By.CssSelector("table.table");

View File

@ -8,6 +8,7 @@ using Microsoft.AspNetCore.E2ETesting;
using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI;
using System;
using System.Threading.Tasks;
using Xunit;
using Xunit.Abstractions;
@ -16,7 +17,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
public class BinaryHttpClientTest : BasicTestAppTestBase, IClassFixture<AspNetSiteServerFixture>
{
readonly ServerFixture _apiServerFixture;
readonly IWebElement _appElement;
IWebElement _appElement;
IWebElement _responseStatus;
IWebElement _responseStatusText;
IWebElement _testOutcome;
@ -30,11 +31,14 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
{
apiServerFixture.BuildWebHostMethod = TestServer.Program.BuildWebHost;
_apiServerFixture = apiServerFixture;
}
protected override void InitializeAsyncCore()
{
Navigate(ServerPathBase, noReload: true);
_appElement = MountTestComponent<BinaryHttpRequestsComponent>();
}
[Fact]
public void CanSendAndReceiveBytes()
{

View File

@ -1,6 +1,7 @@
// 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.Threading.Tasks;
using BasicTestApp;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
@ -19,9 +20,13 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
ToggleExecutionModeServerFixture<Program> serverFixture,
ITestOutputHelper output)
: base(browserFixture, serverFixture, output)
{
}
protected override void InitializeAsyncCore()
{
// On WebAssembly, page reloads are expensive so skip if possible
Navigate(ServerPathBase, noReload: !serverFixture.UsingAspNetHost);
Navigate(ServerPathBase, noReload: !_serverFixture.UsingAspNetHost);
MountTestComponent<BindCasesComponent>();
}
@ -41,12 +46,12 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
Assert.Equal(string.Empty, boundValue.Text); // Doesn't update until change event
Assert.Equal(string.Empty, mirrorValue.GetAttribute("value"));
target.SendKeys("\t");
WaitAssert.Equal("Changed value", () => boundValue.Text);
Browser.Equal("Changed value", () => boundValue.Text);
Assert.Equal("Changed value", mirrorValue.GetAttribute("value"));
// Remove the value altogether
setNullButton.Click();
WaitAssert.Equal(string.Empty, () => target.GetAttribute("value"));
Browser.Equal(string.Empty, () => target.GetAttribute("value"));
Assert.Equal(string.Empty, boundValue.Text);
Assert.Equal(string.Empty, mirrorValue.GetAttribute("value"));
}
@ -65,12 +70,12 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
target.SendKeys("Changed value\t");
WaitAssert.Equal("Changed value", () => boundValue.Text);
Browser.Equal("Changed value", () => boundValue.Text);
Assert.Equal("Changed value", mirrorValue.GetAttribute("value"));
// Remove the value altogether
setNullButton.Click();
WaitAssert.Equal(string.Empty, () => target.GetAttribute("value"));
Browser.Equal(string.Empty, () => target.GetAttribute("value"));
Assert.Equal(string.Empty, boundValue.Text);
Assert.Equal(string.Empty, mirrorValue.GetAttribute("value"));
}
@ -87,7 +92,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
target.SendKeys("Changed value");
Assert.Equal(string.Empty, boundValue.Text); // Don't update as there's no change event fired yet.
target.SendKeys("\t");
WaitAssert.Equal("Changed value", () => boundValue.Text);
Browser.Equal("Changed value", () => boundValue.Text);
}
[Fact]
@ -101,7 +106,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// Modify target; verify value is updated
target.Clear();
target.SendKeys("Changed value\t");
WaitAssert.Equal("Changed value", () => boundValue.Text);
Browser.Equal("Changed value", () => boundValue.Text);
}
[Fact]
@ -115,13 +120,13 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// Modify target; verify value is updated
target.Click();
WaitAssert.True(() => target.Selected);
WaitAssert.Equal("True", () => boundValue.Text);
Browser.True(() => target.Selected);
Browser.Equal("True", () => boundValue.Text);
// Modify data; verify checkbox is updated
invertButton.Click();
WaitAssert.False(() => target.Selected);
WaitAssert.Equal("False", () => boundValue.Text);
Browser.False(() => target.Selected);
Browser.Equal("False", () => boundValue.Text);
}
[Fact]
@ -135,13 +140,13 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// Modify target; verify value is updated
target.Click();
WaitAssert.True(() => target.Selected);
WaitAssert.Equal("True", () => boundValue.Text);
Browser.True(() => target.Selected);
Browser.Equal("True", () => boundValue.Text);
// Modify data; verify checkbox is updated
invertButton.Click();
WaitAssert.False(() => target.Selected);
WaitAssert.Equal("False", () => boundValue.Text);
Browser.False(() => target.Selected);
Browser.Equal("False", () => boundValue.Text);
}
[Fact]
@ -155,13 +160,13 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// Modify target; verify value is updated
target.Click();
WaitAssert.False(() => target.Selected);
WaitAssert.Equal("False", () => boundValue.Text);
Browser.False(() => target.Selected);
Browser.Equal("False", () => boundValue.Text);
// Modify data; verify checkbox is updated
invertButton.Click();
WaitAssert.True(() => target.Selected);
WaitAssert.Equal("True", () => boundValue.Text);
Browser.True(() => target.Selected);
Browser.Equal("True", () => boundValue.Text);
}
[Fact]
@ -174,13 +179,13 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// Modify target; verify value is updated
target.SelectByText("Third choice");
WaitAssert.Equal("Third", () => boundValue.Text);
Browser.Equal("Third", () => boundValue.Text);
// Also verify we can add and select new options atomically
// Don't move this into a separate test, because then the previous assertions
// would be dependent on test execution order (or would require a full page reload)
Browser.FindElement(By.Id("select-box-add-option")).Click();
WaitAssert.Equal("Fourth", () => boundValue.Text);
Browser.Equal("Fourth", () => boundValue.Text);
Assert.Equal("Fourth choice", target.SelectedOption.Text);
}
@ -197,7 +202,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
target.SendKeys("42\t");
WaitAssert.Equal("42", () => boundValue.Text);
Browser.Equal("42", () => boundValue.Text);
Assert.Equal("42", mirrorValue.GetAttribute("value"));
}
@ -214,19 +219,19 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
target.SendKeys("-42\t");
WaitAssert.Equal("-42", () => boundValue.Text);
Browser.Equal("-42", () => boundValue.Text);
Assert.Equal("-42", mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
target.SendKeys("42\t");
WaitAssert.Equal("42", () => boundValue.Text);
Browser.Equal("42", () => boundValue.Text);
Assert.Equal("42", mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
target.SendKeys("\t");
WaitAssert.Equal(string.Empty, () => boundValue.Text);
Browser.Equal(string.Empty, () => boundValue.Text);
Assert.Equal(string.Empty, mirrorValue.GetAttribute("value"));
}
@ -243,7 +248,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
target.SendKeys("-3000000000\t");
WaitAssert.Equal("-3000000000", () => boundValue.Text);
Browser.Equal("-3000000000", () => boundValue.Text);
Assert.Equal("-3000000000", mirrorValue.GetAttribute("value"));
}
@ -260,19 +265,19 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
target.SendKeys("3000000000\t");
WaitAssert.Equal("3000000000", () => boundValue.Text);
Browser.Equal("3000000000", () => boundValue.Text);
Assert.Equal("3000000000", mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
target.SendKeys("-3000000000\t");
WaitAssert.Equal("-3000000000", () => boundValue.Text);
Browser.Equal("-3000000000", () => boundValue.Text);
Assert.Equal("-3000000000", mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
target.SendKeys("\t");
WaitAssert.Equal(string.Empty, () => boundValue.Text);
Browser.Equal(string.Empty, () => boundValue.Text);
Assert.Equal(string.Empty, mirrorValue.GetAttribute("value"));
}
@ -289,7 +294,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
target.SendKeys("-3.141\t");
WaitAssert.Equal("-3.141", () => boundValue.Text);
Browser.Equal("-3.141", () => boundValue.Text);
Assert.Equal("-3.141", mirrorValue.GetAttribute("value"));
}
@ -306,19 +311,19 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
target.SendKeys("3.141\t");
WaitAssert.Equal("3.141", () => boundValue.Text);
Browser.Equal("3.141", () => boundValue.Text);
Assert.Equal("3.141", mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
target.SendKeys("-3.141\t");
WaitAssert.Equal("-3.141", () => boundValue.Text);
Browser.Equal("-3.141", () => boundValue.Text);
Assert.Equal("-3.141", mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
target.SendKeys("\t");
WaitAssert.Equal(string.Empty, () => boundValue.Text);
Browser.Equal(string.Empty, () => boundValue.Text);
Assert.Equal(string.Empty, mirrorValue.GetAttribute("value"));
}
@ -335,14 +340,14 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
target.SendKeys("-3.14159265359\t");
WaitAssert.Equal("-3.14159265359", () => boundValue.Text);
Browser.Equal("-3.14159265359", () => boundValue.Text);
Assert.Equal("-3.14159265359", mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
// Double shouldn't preserve trailing zeros
target.Clear();
target.SendKeys("0.010\t");
WaitAssert.Equal("0.01", () => boundValue.Text);
Browser.Equal("0.01", () => boundValue.Text);
Assert.Equal("0.01", mirrorValue.GetAttribute("value"));
}
@ -359,26 +364,26 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
target.SendKeys("3.14159265359\t");
WaitAssert.Equal("3.14159265359", () => boundValue.Text);
Browser.Equal("3.14159265359", () => boundValue.Text);
Assert.Equal("3.14159265359", mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
target.SendKeys("-3.14159265359\t");
WaitAssert.Equal("-3.14159265359", () => boundValue.Text);
Browser.Equal("-3.14159265359", () => boundValue.Text);
Assert.Equal("-3.14159265359", mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
// Double shouldn't preserve trailing zeros
target.Clear();
target.SendKeys("0.010\t");
WaitAssert.Equal("0.01", () => boundValue.Text);
Browser.Equal("0.01", () => boundValue.Text);
Assert.Equal("0.01", mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
target.SendKeys("\t");
WaitAssert.Equal(string.Empty, () => boundValue.Text);
Browser.Equal(string.Empty, () => boundValue.Text);
Assert.Equal(string.Empty, mirrorValue.GetAttribute("value"));
}
@ -396,7 +401,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// Decimal should preserve trailing zeros
target.Clear();
target.SendKeys("0.010\t");
WaitAssert.Equal("0.010", () => boundValue.Text);
Browser.Equal("0.010", () => boundValue.Text);
Assert.Equal("0.010", mirrorValue.GetAttribute("value"));
}
@ -413,20 +418,20 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
target.SendKeys("0.0000000000000000000000000001\t");
WaitAssert.Equal("0.0000000000000000000000000001", () => boundValue.Text);
Browser.Equal("0.0000000000000000000000000001", () => boundValue.Text);
Assert.Equal("0.0000000000000000000000000001", mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
// Decimal should preserve trailing zeros
target.Clear();
target.SendKeys("0.010\t");
WaitAssert.Equal("0.010", () => boundValue.Text);
Browser.Equal("0.010", () => boundValue.Text);
Assert.Equal("0.010", mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
target.SendKeys("\t");
WaitAssert.Equal(string.Empty, () => boundValue.Text);
Browser.Equal(string.Empty, () => boundValue.Text);
Assert.Equal(string.Empty, mirrorValue.GetAttribute("value"));
}
}

View File

@ -1,6 +1,7 @@
// 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.Threading.Tasks;
using BasicTestApp;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
@ -19,10 +20,14 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
ITestOutputHelper output)
: base(browserFixture, serverFixture, output)
{
Navigate(ServerPathBase, noReload: !serverFixture.UsingAspNetHost);
}
protected override void InitializeAsyncCore()
{
Navigate(ServerPathBase, noReload: !_serverFixture.UsingAspNetHost);
MountTestComponent<BasicTestApp.CascadingValueTest.CascadingValueSupplier>();
}
[Fact]
public void CanUpdateValuesMatchedByType()
{
@ -30,13 +35,13 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var incrementButton = Browser.FindElement(By.Id("increment-count"));
// We have the correct initial value
WaitAssert.Equal("100", () => currentCount.Text);
Browser.Equal("100", () => currentCount.Text);
// Updates are propagated
incrementButton.Click();
WaitAssert.Equal("101", () => currentCount.Text);
Browser.Equal("101", () => currentCount.Text);
incrementButton.Click();
WaitAssert.Equal("102", () => currentCount.Text);
Browser.Equal("102", () => currentCount.Text);
// Didn't re-render unrelated descendants
Assert.Equal("1", Browser.FindElement(By.Id("receive-by-interface-num-renders")).Text);
@ -48,16 +53,16 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var currentFlag1Value = Browser.FindElement(By.Id("flag-1"));
var currentFlag2Value = Browser.FindElement(By.Id("flag-2"));
WaitAssert.Equal("False", () => currentFlag1Value.Text);
WaitAssert.Equal("False", () => currentFlag2Value.Text);
Browser.Equal("False", () => currentFlag1Value.Text);
Browser.Equal("False", () => currentFlag2Value.Text);
// Observe that the correct cascading parameter updates
Browser.FindElement(By.Id("toggle-flag-1")).Click();
WaitAssert.Equal("True", () => currentFlag1Value.Text);
WaitAssert.Equal("False", () => currentFlag2Value.Text);
Browser.Equal("True", () => currentFlag1Value.Text);
Browser.Equal("False", () => currentFlag2Value.Text);
Browser.FindElement(By.Id("toggle-flag-2")).Click();
WaitAssert.Equal("True", () => currentFlag1Value.Text);
WaitAssert.Equal("True", () => currentFlag2Value.Text);
Browser.Equal("True", () => currentFlag1Value.Text);
Browser.Equal("True", () => currentFlag2Value.Text);
// Didn't re-render unrelated descendants
Assert.Equal("1", Browser.FindElement(By.Id("receive-by-interface-num-renders")).Text);
@ -70,13 +75,13 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var decrementButton = Browser.FindElement(By.Id("decrement-count"));
// We have the correct initial value
WaitAssert.Equal("100", () => currentCount.Text);
Browser.Equal("100", () => currentCount.Text);
// Updates are propagated
decrementButton.Click();
WaitAssert.Equal("99", () => currentCount.Text);
Browser.Equal("99", () => currentCount.Text);
decrementButton.Click();
WaitAssert.Equal("98", () => currentCount.Text);
Browser.Equal("98", () => currentCount.Text);
// Didn't re-render descendants
Assert.Equal("1", Browser.FindElement(By.Id("receive-by-interface-num-renders")).Text);

View File

@ -27,7 +27,11 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
ITestOutputHelper output)
: base(browserFixture, serverFixture, output)
{
Navigate(ServerPathBase, noReload: !serverFixture.UsingAspNetHost);
}
protected override void InitializeAsyncCore()
{
Navigate(ServerPathBase, noReload: !_serverFixture.UsingAspNetHost);
}
[Fact]
@ -74,7 +78,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// Clicking button increments count
appElement.FindElement(By.TagName("button")).Click();
WaitAssert.Equal("Current count: 1", () => countDisplayElement.Text);
Browser.Equal("Current count: 1", () => countDisplayElement.Text);
}
[Fact]
@ -87,11 +91,11 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// Clicking 'tick' changes the state, and starts a task
appElement.FindElement(By.Id("tick")).Click();
WaitAssert.Equal("Started", () => stateElement.Text);
Browser.Equal("Started", () => stateElement.Text);
// Clicking 'tock' completes the task, which updates the state
appElement.FindElement(By.Id("tock")).Click();
WaitAssert.Equal("Stopped", () => stateElement.Text);
Browser.Equal("Stopped", () => stateElement.Text);
}
[Fact]
@ -106,12 +110,12 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// Typing adds element
inputElement.SendKeys("a");
WaitAssert.Collection(liElements,
Browser.Collection(liElements,
li => Assert.Equal("a", li.Text));
// Typing again adds another element
inputElement.SendKeys("b");
WaitAssert.Collection(liElements,
Browser.Collection(liElements,
li => Assert.Equal("a", li.Text),
li => Assert.Equal("b", li.Text));
@ -130,19 +134,19 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// Initial count is zero; clicking button increments count
Assert.Equal("Current count: 0", countDisplayElement.Text);
incrementButton.Click();
WaitAssert.Equal("Current count: 1", () => countDisplayElement.Text);
Browser.Equal("Current count: 1", () => countDisplayElement.Text);
// We can remove an event handler
toggleClickHandlerCheckbox.Click();
WaitAssert.Empty(() => appElement.FindElements(By.Id("listening-message")));
Browser.Empty(() => appElement.FindElements(By.Id("listening-message")));
incrementButton.Click();
WaitAssert.Equal("Current count: 1", () => countDisplayElement.Text);
Browser.Equal("Current count: 1", () => countDisplayElement.Text);
// We can add an event handler
toggleClickHandlerCheckbox.Click();
appElement.FindElement(By.Id("listening-message"));
incrementButton.Click();
WaitAssert.Equal("Current count: 2", () => countDisplayElement.Text);
Browser.Equal("Current count: 2", () => countDisplayElement.Text);
}
[Fact]
@ -216,7 +220,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// Clicking increments count in child component
appElement.FindElement(By.TagName("button")).Click();
WaitAssert.Equal("Current count: 1", () => counterDisplay.Text);
Browser.Equal("Current count: 1", () => counterDisplay.Text);
}
[Fact]
@ -231,7 +235,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// Clicking increments count in child element
appElement.FindElement(By.TagName("button")).Click();
WaitAssert.Equal("1", () => messageElementInChild.Text);
Browser.Equal("1", () => messageElementInChild.Text);
}
[Fact]
@ -246,20 +250,20 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// Click to add/remove some child components
addButton.Click();
WaitAssert.Collection(childComponentWrappers,
Browser.Collection(childComponentWrappers,
elem => Assert.Equal("Child 1", elem.FindElement(By.ClassName("message")).Text));
addButton.Click();
WaitAssert.Collection(childComponentWrappers,
Browser.Collection(childComponentWrappers,
elem => Assert.Equal("Child 1", elem.FindElement(By.ClassName("message")).Text),
elem => Assert.Equal("Child 2", elem.FindElement(By.ClassName("message")).Text));
removeButton.Click();
WaitAssert.Collection(childComponentWrappers,
Browser.Collection(childComponentWrappers,
elem => Assert.Equal("Child 1", elem.FindElement(By.ClassName("message")).Text));
addButton.Click();
WaitAssert.Collection(childComponentWrappers,
Browser.Collection(childComponentWrappers,
elem => Assert.Equal("Child 1", elem.FindElement(By.ClassName("message")).Text),
elem => Assert.Equal("Child 3", elem.FindElement(By.ClassName("message")).Text));
}
@ -277,7 +281,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// When property changes, child is renotified before rerender
incrementButton.Click();
WaitAssert.Equal("You supplied: 101", () => suppliedValueElement.Text);
Browser.Equal("You supplied: 101", () => suppliedValueElement.Text);
Assert.Equal("I computed: 202", computedValueElement.Text);
}
@ -296,11 +300,11 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// When we click the button, the region is shown
originalButton.Click();
WaitAssert.Single(fragmentElements);
Browser.Single(fragmentElements);
// The button itself was preserved, so we can click it again and see the effect
originalButton.Click();
WaitAssert.Empty(fragmentElements);
Browser.Empty(fragmentElements);
}
[Fact]
@ -329,7 +333,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
modal.SendKeys("Some value from test");
modal.Accept();
var promptResult = appElement.FindElement(By.TagName("strong"));
WaitAssert.Equal("Some value from test", () => promptResult.Text);
Browser.Equal("Some value from test", () => promptResult.Text);
// NuGet packages can also embed entire components (themselves
// authored as Razor files), including static content. The CSS value
@ -342,7 +346,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var externalComponentButton = specialStyleDiv.FindElement(By.TagName("button"));
Assert.Equal("Click me", externalComponentButton.Text);
externalComponentButton.Click();
WaitAssert.Equal("It works", () => externalComponentButton.Text);
Browser.Equal("It works", () => externalComponentButton.Text);
}
[Fact]
@ -358,7 +362,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
Assert.Equal("10", svgCircleElement.GetAttribute("r"));
appElement.FindElement(By.TagName("button")).Click();
WaitAssert.Equal("20", () => svgCircleElement.GetAttribute("r"));
Browser.Equal("20", () => svgCircleElement.GetAttribute("r"));
}
[Fact]
@ -377,7 +381,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
public void LogicalElementInsertionWorksHierarchically()
{
var appElement = MountTestComponent<LogicalElementInsertionCases>();
WaitAssert.Equal("First Second Third", () => appElement.Text);
Browser.Equal("First Second Third", () => appElement.Text);
}
[Fact]
@ -390,9 +394,9 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
Assert.Equal(string.Empty, inputElement.GetAttribute("value"));
buttonElement.Click();
WaitAssert.Equal("Clicks: 1", () => inputElement.GetAttribute("value"));
Browser.Equal("Clicks: 1", () => inputElement.GetAttribute("value"));
buttonElement.Click();
WaitAssert.Equal("Clicks: 2", () => inputElement.GetAttribute("value"));
Browser.Equal("Clicks: 2", () => inputElement.GetAttribute("value"));
}
[Fact]
@ -408,7 +412,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// Remove the captured element
checkbox.Click();
WaitAssert.Empty(() => appElement.FindElements(By.Id("capturedElement")));
Browser.Empty(() => appElement.FindElements(By.Id("capturedElement")));
// Re-add it; observe it starts empty again
checkbox.Click();
@ -417,7 +421,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// See that the capture variable was automatically updated to reference the new instance
buttonElement.Click();
WaitAssert.Equal("Clicks: 1", () => inputElement.GetAttribute("value"));
Browser.Equal("Clicks: 1", () => inputElement.GetAttribute("value"));
}
[Fact]
@ -432,23 +436,23 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// Verify the reference was captured initially
appElement.FindElement(incrementButtonSelector).Click();
WaitAssert.Equal("Current count: 1", currentCountText);
Browser.Equal("Current count: 1", currentCountText);
resetButton.Click();
WaitAssert.Equal("Current count: 0", currentCountText);
Browser.Equal("Current count: 0", currentCountText);
appElement.FindElement(incrementButtonSelector).Click();
WaitAssert.Equal("Current count: 1", currentCountText);
Browser.Equal("Current count: 1", currentCountText);
// Remove and re-add a new instance of the child, checking the text was reset
toggleChildCheckbox.Click();
WaitAssert.Empty(() => appElement.FindElements(incrementButtonSelector));
Browser.Empty(() => appElement.FindElements(incrementButtonSelector));
toggleChildCheckbox.Click();
WaitAssert.Equal("Current count: 0", currentCountText);
Browser.Equal("Current count: 0", currentCountText);
// Verify we have a new working reference
appElement.FindElement(incrementButtonSelector).Click();
WaitAssert.Equal("Current count: 1", currentCountText);
Browser.Equal("Current count: 1", currentCountText);
resetButton.Click();
WaitAssert.Equal("Current count: 0", currentCountText);
Browser.Equal("Current count: 0", currentCountText);
}
[Fact]
@ -487,7 +491,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// Updating markup blocks
appElement.FindElement(By.TagName("button")).Click();
WaitAssert.Equal(
Browser.Equal(
"[The output was changed completely.]",
() => appElement.FindElement(By.Id("dynamic-markup-block")).Text);
Assert.Equal(
@ -529,7 +533,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var toggle = appElement.FindElement(By.Id("toggle"));
toggle.Click();
WaitAssert.Collection(
Browser.Collection(
() => tfoot.FindElements(By.TagName("td")),
e => Assert.Equal("The", e.Text),
e => Assert.Equal("", e.Text),
@ -550,7 +554,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
await Task.Delay(1000);
var outputElement = appElement.FindElement(By.Id("concurrent-render-output"));
WaitAssert.Equal(expectedOutput, () => outputElement.Text);
Browser.Equal(expectedOutput, () => outputElement.Text);
}
[Fact]
@ -561,7 +565,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
appElement.FindElement(By.Id("run-with-dispatch")).Click();
WaitAssert.Equal("Success (completed synchronously)", () => result.Text);
Browser.Equal("Success (completed synchronously)", () => result.Text);
}
[Fact]
@ -572,7 +576,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
appElement.FindElement(By.Id("run-with-double-dispatch")).Click();
WaitAssert.Equal("Success (completed synchronously)", () => result.Text);
Browser.Equal("Success (completed synchronously)", () => result.Text);
}
[Fact]
@ -583,7 +587,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
appElement.FindElement(By.Id("run-async-with-dispatch")).Click();
WaitAssert.Equal("First Second Third Fourth Fifth", () => result.Text);
Browser.Equal("First Second Third Fourth Fifth", () => result.Text);
}
static IAlert SwitchToAlert(IWebDriver driver)

View File

@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Microsoft.AspNetCore.E2ETesting;
using OpenQA.Selenium;
using System;
using System.Threading.Tasks;
using Xunit;
using Xunit.Abstractions;
@ -26,17 +27,21 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
ITestOutputHelper output)
: base(browserFixture, serverFixture, output)
{
Navigate(ServerPathBase, noReload: !serverFixture.UsingAspNetHost);
}
protected override void InitializeAsyncCore()
{
Navigate(ServerPathBase, noReload: !_serverFixture.UsingAspNetHost);
MountTestComponent<EventBubblingComponent>();
}
[Fact]
public void BubblingStandardEvent_FiredOnElementWithHandler()
{
Browser.FindElement(By.Id("button-with-onclick")).Click();
// Triggers event on target and ancestors with handler in upwards direction
WaitAssert.Equal(
Browser.Equal(
new[] { "target onclick", "parent onclick" },
GetLogLines);
}
@ -47,7 +52,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
Browser.FindElement(By.Id("button-without-onclick")).Click();
// Triggers event on ancestors with handler in upwards direction
WaitAssert.Equal(
Browser.Equal(
new[] { "parent onclick" },
GetLogLines);
}
@ -58,7 +63,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
TriggerCustomBubblingEvent("element-with-onsneeze", "sneeze");
// Triggers event on target and ancestors with handler in upwards direction
WaitAssert.Equal(
Browser.Equal(
new[] { "target onsneeze", "parent onsneeze" },
GetLogLines);
}
@ -69,7 +74,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
TriggerCustomBubblingEvent("element-without-onsneeze", "sneeze");
// Triggers event on ancestors with handler in upwards direction
WaitAssert.Equal(
Browser.Equal(
new[] { "parent onsneeze" },
GetLogLines);
}
@ -80,7 +85,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
Browser.FindElement(By.Id("input-with-onfocus")).Click();
// Triggers event only on target, not other ancestors with event handler
WaitAssert.Equal(
Browser.Equal(
new[] { "target onfocus" },
GetLogLines);
}
@ -91,7 +96,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
Browser.FindElement(By.Id("input-without-onfocus")).Click();
// Triggers no event
WaitAssert.Empty(GetLogLines);
Browser.Empty(GetLogLines);
}
private string[] GetLogLines()

View File

@ -1,6 +1,7 @@
// 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.Threading.Tasks;
using BasicTestApp;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
@ -18,9 +19,13 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
ToggleExecutionModeServerFixture<Program> serverFixture,
ITestOutputHelper output)
: base(browserFixture, serverFixture, output)
{
}
protected override void InitializeAsyncCore()
{
// On WebAssembly, page reloads are expensive so skip if possible
Navigate(ServerPathBase, noReload: !serverFixture.UsingAspNetHost);
Navigate(ServerPathBase, noReload: !_serverFixture.UsingAspNetHost);
MountTestComponent<BasicTestApp.EventCallbackTest.EventCallbackCases>();
}

View File

@ -1,6 +1,7 @@
// 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.Threading.Tasks;
using BasicTestApp;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
@ -19,6 +20,10 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
ToggleExecutionModeServerFixture<Program> serverFixture,
ITestOutputHelper output)
: base(browserFixture, serverFixture, output)
{
}
protected override void InitializeAsyncCore()
{
Navigate(ServerPathBase, noReload: true);
MountTestComponent<EventBubblingComponent>();
@ -37,13 +42,13 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// Focus the target, verify onfocusin is fired
input.Click();
WaitAssert.Equal("onfocus,onfocusin,", () => output.Text);
Browser.Equal("onfocus,onfocusin,", () => output.Text);
// Focus something else, verify onfocusout is also fired
var other = Browser.FindElement(By.Id("other"));
other.Click();
WaitAssert.Equal("onfocus,onfocusin,onblur,onfocusout,", () => output.Text);
Browser.Equal("onfocus,onfocusin,onblur,onfocusout,", () => output.Text);
}
[Fact]
@ -64,7 +69,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
.MoveToElement(other);
actions.Perform();
WaitAssert.Equal("onmouseover,onmouseout,", () => output.Text);
Browser.Equal("onmouseover,onmouseout,", () => output.Text);
}
[Fact]
@ -83,7 +88,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
.MoveToElement(input, 10, 10);
actions.Perform();
WaitAssert.Contains("onmousemove,", () => output.Text);
Browser.Contains("onmousemove,", () => output.Text);
}
[Fact]
@ -102,12 +107,12 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var actions = new Actions(Browser).ClickAndHold(input);
actions.Perform();
WaitAssert.Equal("onmousedown,", () => output.Text);
Browser.Equal("onmousedown,", () => output.Text);
actions = new Actions(Browser).Release(input);
actions.Perform();
WaitAssert.Equal("onmousedown,onmouseup,", () => output.Text);
Browser.Equal("onmousedown,onmouseup,", () => output.Text);
}
[Fact]
@ -116,7 +121,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var appElement = MountTestComponent<EventPreventDefaultComponent>();
appElement.FindElement(By.Id("form-1-button")).Click();
WaitAssert.Equal("Event was handled", () => appElement.FindElement(By.Id("event-handled")).Text);
Browser.Equal("Event was handled", () => appElement.FindElement(By.Id("event-handled")).Text);
}
[Fact]
@ -135,13 +140,13 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var input = Browser.FindElement(By.TagName("input"));
var output = Browser.FindElement(By.Id("test-result"));
WaitAssert.Equal(string.Empty, () => output.Text);
Browser.Equal(string.Empty, () => output.Text);
SendKeysSequentially(input, "abcdefghijklmnopqrstuvwxyz");
WaitAssert.Equal("abcdefghijklmnopqrstuvwxyz", () => output.Text);
Browser.Equal("abcdefghijklmnopqrstuvwxyz", () => output.Text);
input.SendKeys(Keys.Backspace);
WaitAssert.Equal("abcdefghijklmnopqrstuvwxy", () => output.Text);
Browser.Equal("abcdefghijklmnopqrstuvwxy", () => output.Text);
}
void SendKeysSequentially(IWebElement target, string text)

View File

@ -23,9 +23,13 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
ToggleExecutionModeServerFixture<Program> serverFixture,
ITestOutputHelper output)
: base(browserFixture, serverFixture, output)
{
}
protected override void InitializeAsyncCore()
{
// On WebAssembly, page reloads are expensive so skip if possible
Navigate(ServerPathBase, noReload: !serverFixture.UsingAspNetHost);
Navigate(ServerPathBase, noReload: !_serverFixture.UsingAspNetHost);
}
[Fact]
@ -42,26 +46,26 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
acceptsTermsInput.Click(); // Accept terms
acceptsTermsInput.Click(); // Un-accept terms
await Task.Delay(500); // There's no expected change to the UI, so just wait a moment before asserting
WaitAssert.Empty(messagesAccessor);
Browser.Empty(messagesAccessor);
Assert.Empty(appElement.FindElements(By.Id("last-callback")));
// Submitting the form does validate
submitButton.Click();
WaitAssert.Equal(new[] { "You must accept the terms" }, messagesAccessor);
WaitAssert.Equal("OnInvalidSubmit", () => appElement.FindElement(By.Id("last-callback")).Text);
Browser.Equal(new[] { "You must accept the terms" }, messagesAccessor);
Browser.Equal("OnInvalidSubmit", () => appElement.FindElement(By.Id("last-callback")).Text);
// Can make another field invalid
userNameInput.Clear();
submitButton.Click();
WaitAssert.Equal(new[] { "Please choose a username", "You must accept the terms" }, messagesAccessor);
WaitAssert.Equal("OnInvalidSubmit", () => appElement.FindElement(By.Id("last-callback")).Text);
Browser.Equal(new[] { "Please choose a username", "You must accept the terms" }, messagesAccessor);
Browser.Equal("OnInvalidSubmit", () => appElement.FindElement(By.Id("last-callback")).Text);
// Can make valid
userNameInput.SendKeys("Bert\t");
acceptsTermsInput.Click();
submitButton.Click();
WaitAssert.Empty(messagesAccessor);
WaitAssert.Equal("OnValidSubmit", () => appElement.FindElement(By.Id("last-callback")).Text);
Browser.Empty(messagesAccessor);
Browser.Equal("OnValidSubmit", () => appElement.FindElement(By.Id("last-callback")).Text);
}
[Fact]
@ -70,22 +74,22 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var appElement = MountTestComponent<TypicalValidationComponent>();
var nameInput = appElement.FindElement(By.ClassName("name")).FindElement(By.TagName("input"));
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
// Validates on edit
WaitAssert.Equal("valid", () => nameInput.GetAttribute("class"));
Browser.Equal("valid", () => nameInput.GetAttribute("class"));
nameInput.SendKeys("Bert\t");
WaitAssert.Equal("modified valid", () => nameInput.GetAttribute("class"));
Browser.Equal("modified valid", () => nameInput.GetAttribute("class"));
// Can become invalid
nameInput.SendKeys("01234567890123456789\t");
WaitAssert.Equal("modified invalid", () => nameInput.GetAttribute("class"));
WaitAssert.Equal(new[] { "That name is too long" }, messagesAccessor);
Browser.Equal("modified invalid", () => nameInput.GetAttribute("class"));
Browser.Equal(new[] { "That name is too long" }, messagesAccessor);
// Can become valid
nameInput.Clear();
nameInput.SendKeys("Bert\t");
WaitAssert.Equal("modified valid", () => nameInput.GetAttribute("class"));
WaitAssert.Empty(messagesAccessor);
Browser.Equal("modified valid", () => nameInput.GetAttribute("class"));
Browser.Empty(messagesAccessor);
}
[Fact]
@ -96,25 +100,25 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
// Validates on edit
WaitAssert.Equal("valid", () => ageInput.GetAttribute("class"));
Browser.Equal("valid", () => ageInput.GetAttribute("class"));
ageInput.SendKeys("123\t");
WaitAssert.Equal("modified valid", () => ageInput.GetAttribute("class"));
Browser.Equal("modified valid", () => ageInput.GetAttribute("class"));
// Can become invalid
ageInput.SendKeys("e100\t");
WaitAssert.Equal("modified invalid", () => ageInput.GetAttribute("class"));
WaitAssert.Equal(new[] { "The AgeInYears field must be a number." }, messagesAccessor);
Browser.Equal("modified invalid", () => ageInput.GetAttribute("class"));
Browser.Equal(new[] { "The AgeInYears field must be a number." }, messagesAccessor);
// Empty is invalid, because it's not a nullable int
ageInput.Clear();
ageInput.SendKeys("\t");
WaitAssert.Equal("modified invalid", () => ageInput.GetAttribute("class"));
WaitAssert.Equal(new[] { "The AgeInYears field must be a number." }, messagesAccessor);
Browser.Equal("modified invalid", () => ageInput.GetAttribute("class"));
Browser.Equal(new[] { "The AgeInYears field must be a number." }, messagesAccessor);
// Zero is within the allowed range
ageInput.SendKeys("0\t");
WaitAssert.Equal("modified valid", () => ageInput.GetAttribute("class"));
WaitAssert.Empty(messagesAccessor);
Browser.Equal("modified valid", () => ageInput.GetAttribute("class"));
Browser.Empty(messagesAccessor);
}
[Fact]
@ -125,20 +129,20 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
// Validates on edit
WaitAssert.Equal("valid", () => heightInput.GetAttribute("class"));
Browser.Equal("valid", () => heightInput.GetAttribute("class"));
heightInput.SendKeys("123.456\t");
WaitAssert.Equal("modified valid", () => heightInput.GetAttribute("class"));
Browser.Equal("modified valid", () => heightInput.GetAttribute("class"));
// Can become invalid
heightInput.SendKeys("e100\t");
WaitAssert.Equal("modified invalid", () => heightInput.GetAttribute("class"));
WaitAssert.Equal(new[] { "The OptionalHeight field must be a number." }, messagesAccessor);
Browser.Equal("modified invalid", () => heightInput.GetAttribute("class"));
Browser.Equal(new[] { "The OptionalHeight field must be a number." }, messagesAccessor);
// Empty is valid, because it's a nullable float
heightInput.Clear();
heightInput.SendKeys("\t");
WaitAssert.Equal("modified valid", () => heightInput.GetAttribute("class"));
WaitAssert.Empty(messagesAccessor);
Browser.Equal("modified valid", () => heightInput.GetAttribute("class"));
Browser.Empty(messagesAccessor);
}
[Fact]
@ -149,20 +153,20 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
// Validates on edit
WaitAssert.Equal("valid", () => descriptionInput.GetAttribute("class"));
Browser.Equal("valid", () => descriptionInput.GetAttribute("class"));
descriptionInput.SendKeys("Hello\t");
WaitAssert.Equal("modified valid", () => descriptionInput.GetAttribute("class"));
Browser.Equal("modified valid", () => descriptionInput.GetAttribute("class"));
// Can become invalid
descriptionInput.SendKeys("too long too long too long too long too long\t");
WaitAssert.Equal("modified invalid", () => descriptionInput.GetAttribute("class"));
WaitAssert.Equal(new[] { "Description is max 20 chars" }, messagesAccessor);
Browser.Equal("modified invalid", () => descriptionInput.GetAttribute("class"));
Browser.Equal(new[] { "Description is max 20 chars" }, messagesAccessor);
// Can become valid
descriptionInput.Clear();
descriptionInput.SendKeys("Hello\t");
WaitAssert.Equal("modified valid", () => descriptionInput.GetAttribute("class"));
WaitAssert.Empty(messagesAccessor);
Browser.Equal("modified valid", () => descriptionInput.GetAttribute("class"));
Browser.Empty(messagesAccessor);
}
[Fact]
@ -173,24 +177,24 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
// Validates on edit
WaitAssert.Equal("valid", () => renewalDateInput.GetAttribute("class"));
Browser.Equal("valid", () => renewalDateInput.GetAttribute("class"));
renewalDateInput.SendKeys("01/01/2000\t");
WaitAssert.Equal("modified valid", () => renewalDateInput.GetAttribute("class"));
Browser.Equal("modified valid", () => renewalDateInput.GetAttribute("class"));
// Can become invalid
renewalDateInput.SendKeys("0/0/0");
WaitAssert.Equal("modified invalid", () => renewalDateInput.GetAttribute("class"));
WaitAssert.Equal(new[] { "The RenewalDate field must be a date." }, messagesAccessor);
Browser.Equal("modified invalid", () => renewalDateInput.GetAttribute("class"));
Browser.Equal(new[] { "The RenewalDate field must be a date." }, messagesAccessor);
// Empty is invalid, because it's not nullable
renewalDateInput.SendKeys($"{Keys.Backspace}\t{Keys.Backspace}\t{Keys.Backspace}\t");
WaitAssert.Equal("modified invalid", () => renewalDateInput.GetAttribute("class"));
WaitAssert.Equal(new[] { "The RenewalDate field must be a date." }, messagesAccessor);
Browser.Equal("modified invalid", () => renewalDateInput.GetAttribute("class"));
Browser.Equal(new[] { "The RenewalDate field must be a date." }, messagesAccessor);
// Can become valid
renewalDateInput.SendKeys("01/01/01\t");
WaitAssert.Equal("modified valid", () => renewalDateInput.GetAttribute("class"));
WaitAssert.Empty(messagesAccessor);
Browser.Equal("modified valid", () => renewalDateInput.GetAttribute("class"));
Browser.Empty(messagesAccessor);
}
[Fact]
@ -201,19 +205,19 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
// Validates on edit
WaitAssert.Equal("valid", () => expiryDateInput.GetAttribute("class"));
Browser.Equal("valid", () => expiryDateInput.GetAttribute("class"));
expiryDateInput.SendKeys("01/01/2000\t");
WaitAssert.Equal("modified valid", () => expiryDateInput.GetAttribute("class"));
Browser.Equal("modified valid", () => expiryDateInput.GetAttribute("class"));
// Can become invalid
expiryDateInput.SendKeys("111111111");
WaitAssert.Equal("modified invalid", () => expiryDateInput.GetAttribute("class"));
WaitAssert.Equal(new[] { "The OptionalExpiryDate field must be a date." }, messagesAccessor);
Browser.Equal("modified invalid", () => expiryDateInput.GetAttribute("class"));
Browser.Equal(new[] { "The OptionalExpiryDate field must be a date." }, messagesAccessor);
// Empty is valid, because it's nullable
expiryDateInput.SendKeys($"{Keys.Backspace}\t{Keys.Backspace}\t{Keys.Backspace}\t");
WaitAssert.Equal("modified valid", () => expiryDateInput.GetAttribute("class"));
WaitAssert.Empty(messagesAccessor);
Browser.Equal("modified valid", () => expiryDateInput.GetAttribute("class"));
Browser.Empty(messagesAccessor);
}
[Fact]
@ -225,14 +229,14 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
// Validates on edit
WaitAssert.Equal("valid", () => select.GetAttribute("class"));
Browser.Equal("valid", () => select.GetAttribute("class"));
ticketClassInput.SelectByText("First class");
WaitAssert.Equal("modified valid", () => select.GetAttribute("class"));
Browser.Equal("modified valid", () => select.GetAttribute("class"));
// Can become invalid
ticketClassInput.SelectByText("(select)");
WaitAssert.Equal("modified invalid", () => select.GetAttribute("class"));
WaitAssert.Equal(new[] { "The TicketClass field is not valid." }, messagesAccessor);
Browser.Equal("modified invalid", () => select.GetAttribute("class"));
Browser.Equal(new[] { "The TicketClass field is not valid." }, messagesAccessor);
}
[Fact]
@ -243,14 +247,14 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
// Validates on edit
WaitAssert.Equal("valid", () => acceptsTermsInput.GetAttribute("class"));
Browser.Equal("valid", () => acceptsTermsInput.GetAttribute("class"));
acceptsTermsInput.Click();
WaitAssert.Equal("modified valid", () => acceptsTermsInput.GetAttribute("class"));
Browser.Equal("modified valid", () => acceptsTermsInput.GetAttribute("class"));
// Can become invalid
acceptsTermsInput.Click();
WaitAssert.Equal("modified invalid", () => acceptsTermsInput.GetAttribute("class"));
WaitAssert.Equal(new[] { "Must accept terms" }, messagesAccessor);
Browser.Equal("modified invalid", () => acceptsTermsInput.GetAttribute("class"));
Browser.Equal(new[] { "Must accept terms" }, messagesAccessor);
}
[Fact]
@ -264,30 +268,30 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var submissionStatus = appElement.FindElement(By.Id("submission-status"));
// Editing a field triggers validation immediately
WaitAssert.Equal("valid", () => userNameInput.GetAttribute("class"));
Browser.Equal("valid", () => userNameInput.GetAttribute("class"));
userNameInput.SendKeys("Too long too long\t");
WaitAssert.Equal("modified invalid", () => userNameInput.GetAttribute("class"));
WaitAssert.Equal(new[] { "That name is too long" }, messagesAccessor);
Browser.Equal("modified invalid", () => userNameInput.GetAttribute("class"));
Browser.Equal(new[] { "That name is too long" }, messagesAccessor);
// Submitting the form validates remaining fields
submitButton.Click();
WaitAssert.Equal(new[] { "That name is too long", "You must accept the terms" }, messagesAccessor);
WaitAssert.Equal("modified invalid", () => userNameInput.GetAttribute("class"));
WaitAssert.Equal("invalid", () => acceptsTermsInput.GetAttribute("class"));
Browser.Equal(new[] { "That name is too long", "You must accept the terms" }, messagesAccessor);
Browser.Equal("modified invalid", () => userNameInput.GetAttribute("class"));
Browser.Equal("invalid", () => acceptsTermsInput.GetAttribute("class"));
// Can make fields valid
userNameInput.Clear();
userNameInput.SendKeys("Bert\t");
WaitAssert.Equal("modified valid", () => userNameInput.GetAttribute("class"));
Browser.Equal("modified valid", () => userNameInput.GetAttribute("class"));
acceptsTermsInput.Click();
WaitAssert.Equal("modified valid", () => acceptsTermsInput.GetAttribute("class"));
WaitAssert.Equal(string.Empty, () => submissionStatus.Text);
Browser.Equal("modified valid", () => acceptsTermsInput.GetAttribute("class"));
Browser.Equal(string.Empty, () => submissionStatus.Text);
submitButton.Click();
WaitAssert.True(() => submissionStatus.Text.StartsWith("Submitted"));
Browser.True(() => submissionStatus.Text.StartsWith("Submitted"));
// Fields can revert to unmodified
WaitAssert.Equal("valid", () => userNameInput.GetAttribute("class"));
WaitAssert.Equal("valid", () => acceptsTermsInput.GetAttribute("class"));
Browser.Equal("valid", () => userNameInput.GetAttribute("class"));
Browser.Equal("valid", () => acceptsTermsInput.GetAttribute("class"));
}
[Fact]
@ -301,20 +305,20 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// Doesn't show messages for other fields
submitButton.Click();
WaitAssert.Empty(emailMessagesAccessor);
Browser.Empty(emailMessagesAccessor);
// Updates on edit
emailInput.SendKeys("abc\t");
WaitAssert.Equal(new[] { "That doesn't look like a real email address" }, emailMessagesAccessor);
Browser.Equal(new[] { "That doesn't look like a real email address" }, emailMessagesAccessor);
// Can show more than one message
emailInput.SendKeys("too long too long too long\t");
WaitAssert.Equal(new[] { "That doesn't look like a real email address", "We only accept very short email addresses (max 10 chars)" }, emailMessagesAccessor);
Browser.Equal(new[] { "That doesn't look like a real email address", "We only accept very short email addresses (max 10 chars)" }, emailMessagesAccessor);
// Can become valid
emailInput.Clear();
emailInput.SendKeys("a@b.com\t");
WaitAssert.Empty(emailMessagesAccessor);
Browser.Empty(emailMessagesAccessor);
}
[Fact]
@ -326,16 +330,16 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
// Shows initial state
WaitAssert.Equal("Economy", () => selectedTicketClassDisplay.Text);
Browser.Equal("Economy", () => selectedTicketClassDisplay.Text);
// Refreshes on edit
ticketClassInput.SelectByValue("Premium");
WaitAssert.Equal("Premium", () => selectedTicketClassDisplay.Text);
Browser.Equal("Premium", () => selectedTicketClassDisplay.Text);
// Leaves previous value unchanged if new entry is unparseable
ticketClassInput.SelectByText("(select)");
WaitAssert.Equal(new[] { "The TicketClass field is not valid." }, messagesAccessor);
WaitAssert.Equal("Premium", () => selectedTicketClassDisplay.Text);
Browser.Equal(new[] { "The TicketClass field is not valid." }, messagesAccessor);
Browser.Equal("Premium", () => selectedTicketClassDisplay.Text);
}
private Func<string[]> CreateValidationMessagesAccessor(IWebElement appElement)

View File

@ -7,6 +7,7 @@ using Microsoft.AspNetCore.E2ETesting;
using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI;
using System;
using System.Threading.Tasks;
using Xunit;
using Xunit.Abstractions;
@ -15,13 +16,17 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
public class HostedInAspNetTest : ServerTestBase<AspNetSiteServerFixture>
{
public HostedInAspNetTest(
BrowserFixture browserFixture,
AspNetSiteServerFixture serverFixture,
BrowserFixture browserFixture,
AspNetSiteServerFixture serverFixture,
ITestOutputHelper output)
: base(browserFixture, serverFixture, output)
{
serverFixture.BuildWebHostMethod = HostedInAspNet.Server.Program.BuildWebHost;
serverFixture.Environment = AspNetEnvironment.Development;
}
protected override void InitializeAsyncCore()
{
Navigate("/", noReload: true);
WaitUntilLoaded();
}

View File

@ -18,7 +18,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
public class HttpClientTest : BasicTestAppTestBase, IClassFixture<AspNetSiteServerFixture>
{
readonly ServerFixture _apiServerFixture;
readonly IWebElement _appElement;
IWebElement _appElement;
IWebElement _responseStatus;
IWebElement _responseBody;
IWebElement _responseHeaders;
@ -32,7 +32,10 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
{
apiServerFixture.BuildWebHostMethod = TestServer.Program.BuildWebHost;
_apiServerFixture = apiServerFixture;
}
protected override void InitializeAsyncCore()
{
Navigate(ServerPathBase, noReload: true);
_appElement = MountTestComponent<HttpRequestsComponent>();
}

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using BasicTestApp;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
@ -18,6 +19,10 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
ToggleExecutionModeServerFixture<Program> serverFixture,
ITestOutputHelper output)
: base(browserFixture, serverFixture, output)
{
}
protected override void InitializeAsyncCore()
{
Navigate(ServerPathBase, noReload: true);
MountTestComponent<InteropComponent>();
@ -106,7 +111,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
expectedValues.Add(kvp.Key, kvp.Value);
}
}
var actualValues = new Dictionary<string, string>();
// Act

View File

@ -7,6 +7,7 @@ using Microsoft.AspNetCore.E2ETesting;
using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI;
using System;
using System.Threading.Tasks;
using Xunit;
using Xunit.Abstractions;
@ -21,6 +22,10 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
: base(browserFixture, serverFixture, output)
{
serverFixture.BuildWebHostMethod = MonoSanity.Program.BuildWebHost;
}
protected override void InitializeAsyncCore()
{
Navigate("/", noReload: true);
WaitUntilMonoRunningInBrowser();
}

View File

@ -7,6 +7,7 @@ using Microsoft.AspNetCore.E2ETesting;
using OpenQA.Selenium;
using System;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
using Xunit.Abstractions;
@ -20,6 +21,10 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
DevHostServerFixture<Blazor.E2EPerformance.Program> serverFixture,
ITestOutputHelper output)
: base(browserFixture, serverFixture, output)
{
}
protected override void InitializeAsyncCore()
{
Navigate("/", noReload: true);
}
@ -43,8 +48,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
runAllButton.Click();
// The "run" button goes away while the benchmarks execute, then it comes back
WaitAssert.False(() => runAllButton.Displayed);
WaitAssert.True(
Browser.False(() => runAllButton.Displayed);
Browser.True(
() => runAllButton.Displayed || Browser.FindElements(By.CssSelector(".benchmark-error")).Any(),
TimeSpan.FromSeconds(60));

View File

@ -4,6 +4,7 @@
using System;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using BasicTestApp;
using BasicTestApp.RouterTest;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
@ -23,6 +24,10 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
ToggleExecutionModeServerFixture<Program> serverFixture,
ITestOutputHelper output)
: base(browserFixture, serverFixture, output)
{
}
protected override void InitializeAsyncCore()
{
Navigate(ServerPathBase, noReload: false);
WaitUntilTestSelectorReady();
@ -92,7 +97,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
[Fact]
public void CanArriveAtFallbackPageFromBadURI()
{
SetUrlViaPushState("/Oopsie_Daisies%20%This_Aint_A_Real_Page");
SetUrlViaPushState("/Oopsie_Daisies%20%This_Aint_A_Real_Page");
var app = MountTestComponent<TestRouter>();
Assert.Equal("Oops, that component wasn't found!", app.FindElement(By.Id("test-info")).Text);
@ -105,7 +110,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var app = MountTestComponent<TestRouter>();
app.FindElement(By.LinkText("Other")).Click();
WaitAssert.Equal("This is another page.", () => app.FindElement(By.Id("test-info")).Text);
Browser.Equal("This is another page.", () => app.FindElement(By.Id("test-info")).Text);
AssertHighlightedLinks("Other", "Other with base-relative URL (matches all)");
}
@ -121,14 +126,14 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var app = MountTestComponent<TestRouter>();
var button = app.FindElement(By.LinkText("Other"));
new Actions(Browser).KeyDown(key).Click(button).Build().Perform();
WaitAssert.Equal(2, () => Browser.WindowHandles.Count);
Browser.Equal(2, () => Browser.WindowHandles.Count);
}
finally
{
// Leaving the ctrl key up
// Leaving the ctrl key up
new Actions(Browser).KeyUp(key).Build().Perform();
// Closing newly opened windows if a new one was opened
@ -151,11 +156,11 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
{
SetUrlViaPushState("/");
var app = MountTestComponent<TestRouter>();
var app = MountTestComponent<TestRouter>();
app.FindElement(By.LinkText("Target (_blank)")).Click();
WaitAssert.Equal(2, () => Browser.WindowHandles.Count);
Browser.Equal(2, () => Browser.WindowHandles.Count);
}
finally
{
@ -178,20 +183,20 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
SetUrlViaPushState("/");
var app = MountTestComponent<TestRouter>();
app.FindElement(By.LinkText("Other")).Click();
Assert.Single(Browser.WindowHandles);
}
[Fact]
public void CanFollowLinkToOtherPageWithBaseRelativeUrl()
{
SetUrlViaPushState("/");
SetUrlViaPushState("/");
var app = MountTestComponent<TestRouter>();
app.FindElement(By.LinkText("Other with base-relative URL (matches all)")).Click();
WaitAssert.Equal("This is another page.", () => app.FindElement(By.Id("test-info")).Text);
Browser.Equal("This is another page.", () => app.FindElement(By.Id("test-info")).Text);
AssertHighlightedLinks("Other", "Other with base-relative URL (matches all)");
}
@ -202,7 +207,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var app = MountTestComponent<TestRouter>();
app.FindElement(By.LinkText("Default with base-relative URL (matches all)")).Click();
WaitAssert.Equal("This is the default page.", () => app.FindElement(By.Id("test-info")).Text);
Browser.Equal("This is the default page.", () => app.FindElement(By.Id("test-info")).Text);
AssertHighlightedLinks("Default (matches all)", "Default with base-relative URL (matches all)");
}
@ -213,12 +218,12 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var app = MountTestComponent<TestRouter>();
app.FindElement(By.LinkText("With parameters")).Click();
WaitAssert.Equal("Your full name is Abc .", () => app.FindElement(By.Id("test-info")).Text);
Browser.Equal("Your full name is Abc .", () => app.FindElement(By.Id("test-info")).Text);
AssertHighlightedLinks("With parameters");
// Can add more parameters while remaining on same page
app.FindElement(By.LinkText("With more parameters")).Click();
WaitAssert.Equal("Your full name is Abc McDef.", () => app.FindElement(By.Id("test-info")).Text);
Browser.Equal("Your full name is Abc McDef.", () => app.FindElement(By.Id("test-info")).Text);
AssertHighlightedLinks("With parameters", "With more parameters");
// Can remove parameters while remaining on same page
@ -227,7 +232,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// Without that, the page would retain the old value.
// See https://github.com/aspnet/AspNetCore/issues/6864 where we reverted the logic to auto-reset.
app.FindElement(By.LinkText("With parameters")).Click();
WaitAssert.Equal("Your full name is Abc .", () => app.FindElement(By.Id("test-info")).Text);
Browser.Equal("Your full name is Abc .", () => app.FindElement(By.Id("test-info")).Text);
AssertHighlightedLinks("With parameters");
}
@ -238,7 +243,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var app = MountTestComponent<TestRouter>();
app.FindElement(By.LinkText("Default (matches all)")).Click();
WaitAssert.Equal("This is the default page.", () => app.FindElement(By.Id("test-info")).Text);
Browser.Equal("This is the default page.", () => app.FindElement(By.Id("test-info")).Text);
AssertHighlightedLinks("Default (matches all)", "Default with base-relative URL (matches all)");
}
@ -249,7 +254,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var app = MountTestComponent<TestRouter>();
app.FindElement(By.LinkText("Other with query")).Click();
WaitAssert.Equal("This is another page.", () => app.FindElement(By.Id("test-info")).Text);
Browser.Equal("This is another page.", () => app.FindElement(By.Id("test-info")).Text);
AssertHighlightedLinks("Other", "Other with query");
}
@ -260,7 +265,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var app = MountTestComponent<TestRouter>();
app.FindElement(By.LinkText("Default with query")).Click();
WaitAssert.Equal("This is the default page.", () => app.FindElement(By.Id("test-info")).Text);
Browser.Equal("This is the default page.", () => app.FindElement(By.Id("test-info")).Text);
AssertHighlightedLinks("Default with query");
}
@ -271,7 +276,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var app = MountTestComponent<TestRouter>();
app.FindElement(By.LinkText("Other with hash")).Click();
WaitAssert.Equal("This is another page.", () => app.FindElement(By.Id("test-info")).Text);
Browser.Equal("This is another page.", () => app.FindElement(By.Id("test-info")).Text);
AssertHighlightedLinks("Other", "Other with hash");
}
@ -282,7 +287,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var app = MountTestComponent<TestRouter>();
app.FindElement(By.LinkText("Default with hash")).Click();
WaitAssert.Equal("This is the default page.", () => app.FindElement(By.Id("test-info")).Text);
Browser.Equal("This is the default page.", () => app.FindElement(By.Id("test-info")).Text);
AssertHighlightedLinks("Default with hash");
}
@ -295,8 +300,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var testSelector = WaitUntilTestSelectorReady();
app.FindElement(By.Id("do-navigation")).Click();
WaitAssert.True(() => Browser.Url.EndsWith("/Other"));
WaitAssert.Equal("This is another page.", () => app.FindElement(By.Id("test-info")).Text);
Browser.True(() => Browser.Url.EndsWith("/Other"));
Browser.Equal("This is another page.", () => app.FindElement(By.Id("test-info")).Text);
AssertHighlightedLinks("Other", "Other with base-relative URL (matches all)");
// Because this was client-side navigation, we didn't lose the state in the test selector
@ -312,7 +317,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var testSelector = WaitUntilTestSelectorReady();
app.FindElement(By.Id("do-navigation-forced")).Click();
WaitAssert.True(() => Browser.Url.EndsWith("/Other"));
Browser.True(() => Browser.Url.EndsWith("/Other"));
// Because this was a full-page load, our element references should no longer be valid
Assert.Throws<StaleElementReferenceException>(() =>
@ -344,7 +349,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
private void AssertHighlightedLinks(params string[] linkTexts)
{
WaitAssert.Equal(linkTexts, () => Browser
Browser.Equal(linkTexts, () => Browser
.FindElements(By.CssSelector("a.active"))
.Select(x => x.Text));
}

View File

@ -8,6 +8,7 @@ using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI;
using System;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
using Xunit.Abstractions;
@ -17,10 +18,14 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
: ServerTestBase<DevHostServerFixture<StandaloneApp.Program>>, IDisposable
{
public StandaloneAppTest(
BrowserFixture browserFixture,
BrowserFixture browserFixture,
DevHostServerFixture<StandaloneApp.Program> serverFixture,
ITestOutputHelper output)
: base(browserFixture, serverFixture, output)
{
}
protected override void InitializeAsyncCore()
{
Navigate("/", noReload: true);
WaitUntilLoaded();

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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.Threading.Tasks;
using IdentityServer4.EntityFramework.Entities;
using IdentityServer4.EntityFramework.Extensions;
@ -49,9 +50,47 @@ namespace Microsoft.AspNetCore.ApiAuthorization.IdentityServer
/// <inheritdoc />
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.ConfigurePersistedGrantContext(_operationalStoreOptions.Value);
ConfigureGrantContext(builder, _operationalStoreOptions.Value);
}
private void ConfigureGrantContext(ModelBuilder modelBuilder, OperationalStoreOptions storeOptions)
{
if (!string.IsNullOrWhiteSpace(storeOptions.DefaultSchema)) modelBuilder.HasDefaultSchema(storeOptions.DefaultSchema);
modelBuilder.Entity<PersistedGrant>(grant =>
{
grant.ToTable("PersistedGrants");
grant.Property(x => x.Key).HasMaxLength(200).ValueGeneratedNever();
grant.Property(x => x.Type).HasMaxLength(50).IsRequired();
grant.Property(x => x.SubjectId).HasMaxLength(200);
grant.Property(x => x.ClientId).HasMaxLength(200).IsRequired();
grant.Property(x => x.CreationTime).IsRequired();
grant.Property(x => x.Data).HasMaxLength(50000).IsRequired();
grant.HasKey(x => x.Key);
grant.HasIndex(x => new { x.SubjectId, x.ClientId, x.Type });
});
modelBuilder.Entity<DeviceFlowCodes>(codes =>
{
codes.ToTable("DeviceCodes");
codes.Property(x => x.DeviceCode).HasMaxLength(200).IsRequired();
codes.Property(x => x.UserCode).HasMaxLength(200).IsRequired();
codes.Property(x => x.SubjectId).HasMaxLength(200);
codes.Property(x => x.ClientId).HasMaxLength(200).IsRequired();
codes.Property(x => x.CreationTime).IsRequired();
codes.Property(x => x.Expiration).IsRequired();
codes.Property(x => x.Data).HasMaxLength(50000).IsRequired();
codes.HasKey(x => new { x.UserCode });
codes.HasIndex(x => x.DeviceCode).IsUnique();
codes.HasIndex(x => x.UserCode).IsUnique();
});
}
}
}

View File

@ -55,7 +55,7 @@ namespace Microsoft.AspNetCore.SpaServices
/// <summary>
/// Gets or sets the <see cref="StaticFileOptions"/> that supplies content
/// for serving the SPA's default page.
///
///
/// If not set, a default file provider will read files from the
/// <see cref="IHostingEnvironment.WebRootPath"/>, which by default is
/// the <c>wwwroot</c> directory.
@ -73,6 +73,6 @@ namespace Microsoft.AspNetCore.SpaServices
/// Gets or sets the maximum duration that a request will wait for the SPA
/// to become ready to serve to the client.
/// </summary>
public TimeSpan StartupTimeout { get; set; } = TimeSpan.FromSeconds(50);
public TimeSpan StartupTimeout { get; set; } = TimeSpan.FromSeconds(120);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -26,6 +26,9 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="${MicrosoftEntityFrameworkCoreToolsPackageVersion}" Condition=" '$(IndividualLocalAuth)' == 'True' " />
</ItemGroup>
<ItemGroup Condition=" '$(IndividualLocalAuth)' == 'True' AND '$(UseLocalDB)' != 'True' ">
<None Update="app.db" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<!-- Don't publish the SPA source files, but do show them in the project files list -->
<Content Remove="$(SpaRoot)**" />

View File

@ -23,6 +23,9 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="${MicrosoftEntityFrameworkCoreToolsPackageVersion}" Condition=" '$(IndividualLocalAuth)' == 'True' " />
</ItemGroup>
<ItemGroup Condition=" '$(IndividualLocalAuth)' == 'True' AND '$(UseLocalDB)' != 'True' ">
<None Update="app.db" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<!-- Don't publish the SPA source files, but do show them in the project files list -->
<Content Remove="$(SpaRoot)**" />

View File

@ -5,12 +5,12 @@
"requires": true,
"dependencies": {
"@angular-devkit/architect": {
"version": "0.13.4",
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.13.4.tgz",
"integrity": "sha512-wJF8oz8MurtpFi0ik42bkI2F5gEnuOe79KHPO1i3SYfdhEp5NY8igVKZ6chB/eq4Ml50aHxas8Hh9ke12K+Pxw==",
"version": "0.13.5",
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.13.5.tgz",
"integrity": "sha512-ouqDu5stZA2gsWnbKMThDfOG/D6lJQaLL+oGEoM5zfnKir3ctyV5rOm73m2pDYUblByTCb+rkj5KmooUWpnV1g==",
"dev": true,
"requires": {
"@angular-devkit/core": "7.3.4",
"@angular-devkit/core": "7.3.5",
"rxjs": "6.3.3"
},
"dependencies": {
@ -26,16 +26,16 @@
}
},
"@angular-devkit/build-angular": {
"version": "0.13.4",
"resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-0.13.4.tgz",
"integrity": "sha512-7yJzgNk3ToiAHd8vnYonqiswvVNYzOUKg2xZfpx+SD5m7mVE+CSUp+P4YzUrI0Vm9WitZOYaCv1I6G1NguJHqA==",
"version": "0.13.5",
"resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-0.13.5.tgz",
"integrity": "sha512-xJq46Jz7MMyprcJ4PflJjPtJ+2OVqbnz6HwtUyJLwYXmC0ldWnhGiNwn+0o7Em40Ol8gf2TYqcDGcSi5OyOZMg==",
"dev": true,
"requires": {
"@angular-devkit/architect": "0.13.4",
"@angular-devkit/build-optimizer": "0.13.4",
"@angular-devkit/build-webpack": "0.13.4",
"@angular-devkit/core": "7.3.4",
"@ngtools/webpack": "7.3.4",
"@angular-devkit/architect": "0.13.5",
"@angular-devkit/build-optimizer": "0.13.5",
"@angular-devkit/build-webpack": "0.13.5",
"@angular-devkit/core": "7.3.5",
"@ngtools/webpack": "7.3.5",
"ajv": "6.9.1",
"autoprefixer": "9.4.6",
"circular-dependency-plugin": "5.0.2",
@ -118,9 +118,9 @@
}
},
"@angular-devkit/build-optimizer": {
"version": "0.13.4",
"resolved": "https://registry.npmjs.org/@angular-devkit/build-optimizer/-/build-optimizer-0.13.4.tgz",
"integrity": "sha512-YTpiE4F2GnFc4jbXZkmFUMHOvo3kWcMPAInVbjXNSIWMqW8Ibs7d6MAcualQX4NCvcn45+mVXLfY/8hWZ/b7lw==",
"version": "0.13.5",
"resolved": "https://registry.npmjs.org/@angular-devkit/build-optimizer/-/build-optimizer-0.13.5.tgz",
"integrity": "sha512-bkyKYplkUnWCbXfDuS0gFuPDoi9OEUNRBtvYtY3rgE3XKSAJBjV+KLgoXSSpLL6ucLDx6gOyDXitUFLiRCDMqg==",
"dev": true,
"requires": {
"loader-utils": "1.2.3",
@ -134,17 +134,23 @@
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz",
"integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=",
"dev": true
},
"typescript": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.2.4.tgz",
"integrity": "sha512-0RNDbSdEokBeEAkgNbxJ+BLwSManFy9TeXz8uW+48j/xhEXv1ePME60olyzw2XzUqUBNAYFeJadIqAgNqIACwg==",
"dev": true
}
}
},
"@angular-devkit/build-webpack": {
"version": "0.13.4",
"resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.13.4.tgz",
"integrity": "sha512-W5baPrsNUUyeD5K9ZjiTfiDsytBoqDvGDMKRUO9XWV8xF8LYF2ttsBQxlJK7SKkMyJXcjmiHhdkMq5wgRE7n0A==",
"version": "0.13.5",
"resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.13.5.tgz",
"integrity": "sha512-/abR1cxCLiRJciaW0Dc0RYNbYQIhHFut1r1Dv8xx7He2/wYgCzGsYl9EeFm48Nrw62/9rIPJxhZoZtcf1Mrocg==",
"dev": true,
"requires": {
"@angular-devkit/architect": "0.13.4",
"@angular-devkit/core": "7.3.4",
"@angular-devkit/architect": "0.13.5",
"@angular-devkit/core": "7.3.5",
"rxjs": "6.3.3"
},
"dependencies": {
@ -160,9 +166,9 @@
}
},
"@angular-devkit/core": {
"version": "7.3.4",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-7.3.4.tgz",
"integrity": "sha512-MBfen51iOBKfK4tlg5KwmPxePsF1QoFNUMGLuvUUwPkteonrGcupX1Q7NWTpf+HA+i08mOnZGuepeuQkD12IQw==",
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-7.3.5.tgz",
"integrity": "sha512-J/Tztq2BZ3tpwUsbiz8N61rf9lwqn85UvJsDui2SPIdzDR9KmPr5ESI2Irc/PEb9i+CBXtVuhr8AIqo7rR6ZTg==",
"dev": true,
"requires": {
"ajv": "6.9.1",
@ -202,12 +208,12 @@
}
},
"@angular-devkit/schematics": {
"version": "7.3.4",
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-7.3.4.tgz",
"integrity": "sha512-BLI4MDHmpzw+snu/2Dw1nMmfJ0VAARTbU6DrmzXyl2Se45+iE/tdRy4yNx3IfHhyoCrVZ15R0y9CXeEsLftlIg==",
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-7.3.5.tgz",
"integrity": "sha512-BFCCkwRMBC4aFlngaloi1avCTgGrl1MFc/0Av2sCpBh/fdm1FqSVzmOiTfu93dehRVVL/bTrA2qj+xpNsXCxzA==",
"dev": true,
"requires": {
"@angular-devkit/core": "7.3.4",
"@angular-devkit/core": "7.3.5",
"rxjs": "6.3.3"
},
"dependencies": {
@ -223,24 +229,24 @@
}
},
"@angular/animations": {
"version": "7.2.5",
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-7.2.5.tgz",
"integrity": "sha512-BJPm9pls6MuIhn6TF1f2ZwkGFTamuyJbhXz8n9u669tTI4deUAEEHCzYaEgVu4q007niVg2ZnO4MDcxXtc5nFQ==",
"version": "7.2.8",
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-7.2.8.tgz",
"integrity": "sha512-dJn9koYukyz15TouBc+z5z9fdThDk+bKgdlij25eYSu5Mpmtk04gB4eIMQA97K0UDh1d4YukgSJ5w3ZIk0m8DQ==",
"requires": {
"tslib": "^1.9.0"
}
},
"@angular/cli": {
"version": "7.3.4",
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-7.3.4.tgz",
"integrity": "sha512-uGL8xiQf+GvuJvqvMUu/XHcijbq9ocbX487LO2PgJ29etHfI7dC0toJbQ8ob+HnF9e1qwMe+uu45OU4C2p+a1A==",
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-7.3.5.tgz",
"integrity": "sha512-WL339qoWMIsQympJAabtcvX6hMydGD/H0fm8K9ihD7xd6Af1QCSDN/aWTIvYyEzj9Hb8/sJ3mgRtvLlr1xTHzg==",
"dev": true,
"requires": {
"@angular-devkit/architect": "0.13.4",
"@angular-devkit/core": "7.3.4",
"@angular-devkit/schematics": "7.3.4",
"@schematics/angular": "7.3.4",
"@schematics/update": "0.13.4",
"@angular-devkit/architect": "0.13.5",
"@angular-devkit/core": "7.3.5",
"@angular-devkit/schematics": "7.3.5",
"@schematics/angular": "7.3.5",
"@schematics/update": "0.13.5",
"@yarnpkg/lockfile": "1.1.0",
"ini": "1.3.5",
"inquirer": "6.2.1",
@ -252,29 +258,29 @@
}
},
"@angular/common": {
"version": "7.2.5",
"resolved": "https://registry.npmjs.org/@angular/common/-/common-7.2.5.tgz",
"integrity": "sha512-IW3vk0DDblbZMD8gkKVpPa/krXky4i5baFhKgqN2xYo48epXYvAezm5q71a982eadjUussbaYPlsXzYNAhdVKQ==",
"version": "7.2.8",
"resolved": "https://registry.npmjs.org/@angular/common/-/common-7.2.8.tgz",
"integrity": "sha512-LgOhf68+LPndGZhtnUlGFd2goReXYmHzaFZW8gCEi9aC+H+Io8bjYh0gkH3xDreevEOe3f0z6coXNFLIxSmTuA==",
"requires": {
"tslib": "^1.9.0"
}
},
"@angular/compiler": {
"version": "7.2.5",
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-7.2.5.tgz",
"integrity": "sha512-/41ehOSupAA+uc32XHmN5jOvqmb4A4D+V+MXDmnlYVaYAYZrGf3AS+1RJuBy5cIUGQ1Nv+Nbj4Y7X/ydb6ncOQ==",
"version": "7.2.8",
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-7.2.8.tgz",
"integrity": "sha512-PrU97cTsOdofpaDkxK0rWUA/CGd0u6ESOI6XvFVm5xH9zJInsdY8ShSHklnr1JJnss70e1dGKZbZq32OChxWMw==",
"requires": {
"tslib": "^1.9.0"
}
},
"@angular/compiler-cli": {
"version": "7.2.5",
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-7.2.5.tgz",
"integrity": "sha512-3PzRaz3cKKnhhWKixKhXUvD2klKoAiFO/81ETMC+lp4GGWL35NAts0KnudSNxQIktYOlardQHEggtfgxq+spRg==",
"version": "7.2.8",
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-7.2.8.tgz",
"integrity": "sha512-DM35X5GHDCWGKfA+Q/nfBdw+hgahCT+zn7ywOvzyL4p+rkyOUHIHLnLfJekRpUXJYJrq5011MrUMw86HrR0vUg==",
"dev": true,
"requires": {
"canonical-path": "1.0.0",
"chokidar": "^1.4.2",
"chokidar": "^2.1.1",
"convert-source-map": "^1.5.1",
"dependency-graph": "^0.7.2",
"magic-string": "^0.25.0",
@ -292,42 +298,6 @@
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
"dev": true
},
"anymatch": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz",
"integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==",
"dev": true,
"requires": {
"micromatch": "^2.1.5",
"normalize-path": "^2.0.0"
}
},
"arr-diff": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz",
"integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=",
"dev": true,
"requires": {
"arr-flatten": "^1.0.1"
}
},
"array-unique": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz",
"integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=",
"dev": true
},
"braces": {
"version": "1.8.5",
"resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz",
"integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=",
"dev": true,
"requires": {
"expand-range": "^1.8.1",
"preserve": "^0.2.0",
"repeat-element": "^1.1.2"
}
},
"camelcase": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz",
@ -335,20 +305,23 @@
"dev": true
},
"chokidar": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz",
"integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=",
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.2.tgz",
"integrity": "sha512-IwXUx0FXc5ibYmPC2XeEj5mpXoV66sR+t3jqu2NS2GYwCktt3KF1/Qqjws/NkegajBA4RbZ5+DDwlOiJsxDHEg==",
"dev": true,
"requires": {
"anymatch": "^1.3.0",
"async-each": "^1.0.0",
"fsevents": "^1.0.0",
"glob-parent": "^2.0.0",
"inherits": "^2.0.1",
"anymatch": "^2.0.0",
"async-each": "^1.0.1",
"braces": "^2.3.2",
"fsevents": "^1.2.7",
"glob-parent": "^3.1.0",
"inherits": "^2.0.3",
"is-binary-path": "^1.0.0",
"is-glob": "^2.0.0",
"is-glob": "^4.0.0",
"normalize-path": "^3.0.0",
"path-is-absolute": "^1.0.0",
"readdirp": "^2.0.0"
"readdirp": "^2.2.1",
"upath": "^1.1.0"
}
},
"cross-spawn": {
@ -377,24 +350,6 @@
"strip-eof": "^1.0.0"
}
},
"expand-brackets": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz",
"integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=",
"dev": true,
"requires": {
"is-posix-bracket": "^0.1.0"
}
},
"extglob": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz",
"integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=",
"dev": true,
"requires": {
"is-extglob": "^1.0.0"
}
},
"find-up": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
@ -404,45 +359,12 @@
"locate-path": "^2.0.0"
}
},
"glob-parent": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz",
"integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=",
"dev": true,
"requires": {
"is-glob": "^2.0.0"
}
},
"is-extglob": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
"integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
"dev": true
},
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
"dev": true
},
"is-glob": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
"integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
"dev": true,
"requires": {
"is-extglob": "^1.0.0"
}
},
"kind-of": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
"integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
"dev": true,
"requires": {
"is-buffer": "^1.1.5"
}
},
"load-json-file": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz",
@ -464,26 +386,11 @@
"mimic-fn": "^1.0.0"
}
},
"micromatch": {
"version": "2.3.11",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz",
"integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=",
"dev": true,
"requires": {
"arr-diff": "^2.0.0",
"array-unique": "^0.2.1",
"braces": "^1.8.2",
"expand-brackets": "^0.1.4",
"extglob": "^0.3.1",
"filename-regex": "^2.0.0",
"is-extglob": "^1.0.0",
"is-glob": "^2.0.1",
"kind-of": "^3.0.2",
"normalize-path": "^2.0.1",
"object.omit": "^2.0.0",
"parse-glob": "^3.0.4",
"regex-cache": "^0.4.2"
}
"normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true
},
"os-locale": {
"version": "2.1.0",
@ -596,25 +503,25 @@
}
},
"@angular/core": {
"version": "7.2.5",
"resolved": "https://registry.npmjs.org/@angular/core/-/core-7.2.5.tgz",
"integrity": "sha512-SKBDqoKNj9vjuLeNToFySafTWb+fyIhCj6C/yzlPcsRPLZj0Kzbvn1IKE+TWBLa/85dUiaE1xdBNQ66jTtpFSA==",
"version": "7.2.8",
"resolved": "https://registry.npmjs.org/@angular/core/-/core-7.2.8.tgz",
"integrity": "sha512-QKwug2kWJC00zm2rvmD9mCJzsOkMVhSu8vqPWf83poWTh8+F9aIVWcy29W0VoGpBkSchOnK8hf9DnKVv28j9nw==",
"requires": {
"tslib": "^1.9.0"
}
},
"@angular/forms": {
"version": "7.2.5",
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-7.2.5.tgz",
"integrity": "sha512-VBWbQ26ck1V014DSkFjlrlCksAZ3Q8rmHLZFy+o2k1CVyy49ojV/OxLDfJutp0QvflO+sWnzfDPaND/Ed9tS4w==",
"version": "7.2.8",
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-7.2.8.tgz",
"integrity": "sha512-lbSX4IHFHz/c4e2RHiPpL8MJlzDkCuQEHnqsujDaV2X9o9fApS6+C1X4x7Z2XDKqonmeX+aHQwv9+SLejX6OyQ==",
"requires": {
"tslib": "^1.9.0"
}
},
"@angular/http": {
"version": "7.2.5",
"resolved": "https://registry.npmjs.org/@angular/http/-/http-7.2.5.tgz",
"integrity": "sha512-F5AE3QcNibShnhxokFaFhid2Abb+qtMbjfTZu3dSBOWbuz7+H0g7WbCFB4UZvWkTiOaQkTuk0J9IBrwrvt3fkQ==",
"version": "7.2.8",
"resolved": "https://registry.npmjs.org/@angular/http/-/http-7.2.8.tgz",
"integrity": "sha512-bCLgXKbYeSiZPXQ58YbxVKg50PwecDm9SqiXh1QOQOSJsAG7oXXWesN/IOnfP38XeRg9C2NBbJ6mKOyfD/4jdw==",
"requires": {
"tslib": "^1.9.0"
}
@ -626,25 +533,25 @@
"dev": true
},
"@angular/platform-browser": {
"version": "7.2.5",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-7.2.5.tgz",
"integrity": "sha512-trSFOsRC+PrjqE709RQ7ezVCouehD7e82FhQNZQx9O1IZQyO0hxE2ncVB4Lvd7KpunAiFX7M1A2wfksHQl+0qw==",
"version": "7.2.8",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-7.2.8.tgz",
"integrity": "sha512-SizCRMc7Or27g2CugcqWnaAikRPfgLgRvb9GFFGpcgoq8CRfOVwkyR5dFZuqN39H+uwtwuTMP5OUYhZcrFNKug==",
"requires": {
"tslib": "^1.9.0"
}
},
"@angular/platform-browser-dynamic": {
"version": "7.2.5",
"resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-7.2.5.tgz",
"integrity": "sha512-GlipaKFqWlcaGWowccFxAgscpgMnWJucRnDrHRgvp3iUbqt2mC4sLko8BOi0S5FkE1D4+EqyEyp8DLM4o7VDvg==",
"version": "7.2.8",
"resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-7.2.8.tgz",
"integrity": "sha512-nOJt28A5pRn4mdL8y98V7bA6OOdMRjsQAcWCr/isGYF0l1yDC0ijUGWkHuRtj3z1/9tmERN0BLXx+xs1h4JhCQ==",
"requires": {
"tslib": "^1.9.0"
}
},
"@angular/platform-server": {
"version": "7.2.5",
"resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-7.2.5.tgz",
"integrity": "sha512-JgNSkFq9U+117UrIFM9f2lDJRdXqw4ZxST1kr5cLZ3BAnzlp1JQS9TT9RgIOLVD6rie8Aqgoli7fhaZQ7txvLQ==",
"version": "7.2.8",
"resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-7.2.8.tgz",
"integrity": "sha512-UAg+6rlEiFVw+Fvfyt5VnE5lS5xlLkPmxaKAGotj9y9sUjD7TTI4rJ7ciGAWOuVb5MprDWKlXxPYeBwBHiO0Mw==",
"requires": {
"domino": "^2.1.0",
"tslib": "^1.9.0",
@ -652,9 +559,9 @@
}
},
"@angular/router": {
"version": "7.2.5",
"resolved": "https://registry.npmjs.org/@angular/router/-/router-7.2.5.tgz",
"integrity": "sha512-WjEdnTyLQRntB8ixQ4qH8PFURFhgTtUjAsu3S3lf2wWbDDADIJO/xTMtXDhGubTmzRbBVROw6ZQzgDZtJyYKrw==",
"version": "7.2.8",
"resolved": "https://registry.npmjs.org/@angular/router/-/router-7.2.8.tgz",
"integrity": "sha512-G8cA/JbaKFNeosCUGE/0Z7+5FBhZTVV/hacgUBRAEj8NNnECFqkAY9F16Twe+X8qx9WkpMw51WSYDNHPI1/dXQ==",
"requires": {
"tslib": "^1.9.0"
}
@ -843,12 +750,12 @@
}
},
"@ngtools/webpack": {
"version": "7.3.4",
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-7.3.4.tgz",
"integrity": "sha512-qTfw/LGZ3kDZAgqb6gMVr36E0W3M+bnS/xAxNTxshxmJOCQr9AcKtX4sP65QweKS60KoBBR1a7nR6SOi1NJkxA==",
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-7.3.5.tgz",
"integrity": "sha512-KqJ4ZR8XicN+ElrSNiNPidTM134Z23F7ib0Rl8Ny3PDHkAYIBIxEnQDgZ2mazKRUVMlStUnjmzQIQj7/qZGLaw==",
"dev": true,
"requires": {
"@angular-devkit/core": "7.3.4",
"@angular-devkit/core": "7.3.5",
"enhanced-resolve": "4.1.0",
"rxjs": "6.3.3",
"tree-kill": "1.2.1",
@ -867,29 +774,37 @@
}
},
"@nguniversal/module-map-ngfactory-loader": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@nguniversal/module-map-ngfactory-loader/-/module-map-ngfactory-loader-7.1.0.tgz",
"integrity": "sha512-GYfb24OLJKBY58CgUsIsGgci5ceZAt4+GrVKh7RZRIHtZ/bjdGsvpIbfE9udqsnSowxIxHA5KzYHbC1x6AAB0A=="
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/@nguniversal/module-map-ngfactory-loader/-/module-map-ngfactory-loader-7.1.1.tgz",
"integrity": "sha512-cZaxdY64C5xPwbE3qqEQGmnKIEgIA57JTozAsT1gvlxNYwssxTlhCYT0HQcqNfNBBjf3xdqXTfRPC7lfpE4qWA=="
},
"@schematics/angular": {
"version": "7.3.4",
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-7.3.4.tgz",
"integrity": "sha512-Bb5DZQ8MeP8yhxPe6nVqyQ7sGVNwUx6nXPlrQV45ZycD3nJlqsuxr2DE75HFpn5oU+vlkq9J/Sys4WLJ4E/OMw==",
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-7.3.5.tgz",
"integrity": "sha512-fKNZccf1l2OcDwtDupYj54N/YuiMLCWeaXNxcJNUYvGnBtzxQJ4P2LtSCjB4HDvYCtseQM7iyc7D1Xrr5gI8nw==",
"dev": true,
"requires": {
"@angular-devkit/core": "7.3.4",
"@angular-devkit/schematics": "7.3.4",
"@angular-devkit/core": "7.3.5",
"@angular-devkit/schematics": "7.3.5",
"typescript": "3.2.4"
},
"dependencies": {
"typescript": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.2.4.tgz",
"integrity": "sha512-0RNDbSdEokBeEAkgNbxJ+BLwSManFy9TeXz8uW+48j/xhEXv1ePME60olyzw2XzUqUBNAYFeJadIqAgNqIACwg==",
"dev": true
}
}
},
"@schematics/update": {
"version": "0.13.4",
"resolved": "https://registry.npmjs.org/@schematics/update/-/update-0.13.4.tgz",
"integrity": "sha512-YarSCCBSVPVG/MyN5H/FliRwaIDoeercy5Nip+NWZJsDyvtsAekO9s6QwizSvAr3541MmSQFeQICsjyM2dl3Bg==",
"version": "0.13.5",
"resolved": "https://registry.npmjs.org/@schematics/update/-/update-0.13.5.tgz",
"integrity": "sha512-bmwVKeyOmC948gJrIxPg0TY0999nusqSVqXJ8hqAgD0fyD6rnzF74nUhovQGvwFV0pZK8fCkRfdJIqgAPFcHcw==",
"dev": true,
"requires": {
"@angular-devkit/core": "7.3.4",
"@angular-devkit/schematics": "7.3.4",
"@angular-devkit/core": "7.3.5",
"@angular-devkit/schematics": "7.3.5",
"@yarnpkg/lockfile": "1.1.0",
"ini": "1.3.5",
"pacote": "9.4.0",
@ -925,9 +840,9 @@
}
},
"@types/node": {
"version": "11.9.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-11.9.6.tgz",
"integrity": "sha512-4hS2K4gwo9/aXIcoYxCtHpdgd8XUeDmo1siRCAH3RziXB65JlPqUFMtfy9VPj+og7dp3w1TFjGwYga4e0m9GwA==",
"version": "11.10.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-11.10.5.tgz",
"integrity": "sha512-DuIRlQbX4K+d5I+GMnv+UfnGh+ist0RdlvOp+JZ7ePJ6KQONCFQv/gKYSU1ZzbVdFSUCKZOltjmpFAGGv5MdYA==",
"dev": true
},
"@types/q": {
@ -2151,9 +2066,9 @@
}
},
"caniuse-lite": {
"version": "1.0.30000941",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000941.tgz",
"integrity": "sha512-4vzGb2MfZcO20VMPj1j6nRAixhmtlhkypM4fL4zhgzEucQIYiRzSqPcWIu1OF8i0FETD93FMIPWfUJCAcFvrqA==",
"version": "1.0.30000942",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000942.tgz",
"integrity": "sha512-wLf+IhZUy2rfz48tc40OH7jHjXjnvDFEYqBHluINs/6MgzoNLPf25zhE4NOVzqxLKndf+hau81sAW0RcGHIaBQ==",
"dev": true
},
"canonical-path": {
@ -3482,57 +3397,6 @@
}
}
},
"expand-range": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz",
"integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=",
"dev": true,
"requires": {
"fill-range": "^2.1.0"
},
"dependencies": {
"fill-range": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz",
"integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==",
"dev": true,
"requires": {
"is-number": "^2.1.0",
"isobject": "^2.0.0",
"randomatic": "^3.0.0",
"repeat-element": "^1.1.2",
"repeat-string": "^1.5.2"
}
},
"is-number": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz",
"integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=",
"dev": true,
"requires": {
"kind-of": "^3.0.2"
}
},
"isobject": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz",
"integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=",
"dev": true,
"requires": {
"isarray": "1.0.0"
}
},
"kind-of": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
"integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
"dev": true,
"requires": {
"is-buffer": "^1.1.5"
}
}
}
},
"express": {
"version": "4.16.4",
"resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz",
@ -3762,12 +3626,6 @@
"schema-utils": "^1.0.0"
}
},
"filename-regex": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz",
"integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=",
"dev": true
},
"fileset": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/fileset/-/fileset-2.0.3.tgz",
@ -4598,42 +4456,6 @@
"path-is-absolute": "^1.0.0"
}
},
"glob-base": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz",
"integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=",
"dev": true,
"requires": {
"glob-parent": "^2.0.0",
"is-glob": "^2.0.0"
},
"dependencies": {
"glob-parent": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz",
"integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=",
"dev": true,
"requires": {
"is-glob": "^2.0.0"
}
},
"is-extglob": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
"integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
"dev": true
},
"is-glob": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
"integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
"dev": true,
"requires": {
"is-extglob": "^1.0.0"
}
}
}
},
"glob-parent": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
@ -5393,21 +5215,6 @@
"integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=",
"dev": true
},
"is-dotfile": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz",
"integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=",
"dev": true
},
"is-equal-shallow": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz",
"integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=",
"dev": true,
"requires": {
"is-primitive": "^2.0.0"
}
},
"is-extendable": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
@ -5495,18 +5302,6 @@
"isobject": "^3.0.1"
}
},
"is-posix-bracket": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz",
"integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=",
"dev": true
},
"is-primitive": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz",
"integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=",
"dev": true
},
"is-promise": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz",
@ -6422,12 +6217,6 @@
"object-visit": "^1.0.0"
}
},
"math-random": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz",
"integrity": "sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==",
"dev": true
},
"md5.js": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
@ -7082,27 +6871,6 @@
"isobject": "^3.0.0"
}
},
"object.omit": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz",
"integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=",
"dev": true,
"requires": {
"for-own": "^0.1.4",
"is-extendable": "^0.1.1"
},
"dependencies": {
"for-own": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz",
"integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=",
"dev": true,
"requires": {
"for-in": "^1.0.1"
}
}
}
},
"object.pick": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz",
@ -7446,35 +7214,6 @@
"safe-buffer": "^5.1.1"
}
},
"parse-glob": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz",
"integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=",
"dev": true,
"requires": {
"glob-base": "^0.3.0",
"is-dotfile": "^1.0.0",
"is-extglob": "^1.0.0",
"is-glob": "^2.0.0"
},
"dependencies": {
"is-extglob": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
"integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
"dev": true
},
"is-glob": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
"integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
"dev": true,
"requires": {
"is-extglob": "^1.0.0"
}
}
}
},
"parse-json": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
@ -7776,12 +7515,6 @@
"integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
"dev": true
},
"preserve": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz",
"integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=",
"dev": true
},
"process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
@ -7972,25 +7705,6 @@
"integrity": "sha512-sluvZZ1YiTLD5jsqZcDmFyV2EwToyXZBfpoVOmktMmW+VEnhgakFHnasVph65fOjGPTWN0Nw3+XQaSeMayr0kg==",
"dev": true
},
"randomatic": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz",
"integrity": "sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==",
"dev": true,
"requires": {
"is-number": "^4.0.0",
"kind-of": "^6.0.0",
"math-random": "^1.0.1"
},
"dependencies": {
"is-number": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz",
"integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==",
"dev": true
}
}
},
"randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@ -8139,15 +7853,6 @@
"integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=",
"optional": true
},
"regex-cache": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz",
"integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==",
"dev": true,
"requires": {
"is-equal-shallow": "^0.1.3"
}
},
"regex-not": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz",

View File

@ -33,13 +33,13 @@
"popper.js": "^1.14.3"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.13.2",
"@angular/cli": "~7.3.2",
"@angular/compiler-cli": "7.2.5",
"@angular-devkit/build-angular": "~0.13.5",
"@angular/cli": "~7.3.5",
"@angular/compiler-cli": "^7.2.8",
"@angular/language-service": "^7.2.5",
"@types/jasmine": "~3.3.9",
"@types/jasminewd2": "~2.0.6",
"@types/node": "~11.9.4",
"@types/node": "~11.10.5",
"codelyzer": "~4.5.0",
"jasmine-core": "~3.3.0",
"jasmine-spec-reporter": "~4.2.1",

View File

@ -14,7 +14,7 @@ import { LoginActions, QueryParameterNames, ApplicationPaths, ReturnUrlType } fr
styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {
private message = new BehaviorSubject<string>(null);
public message = new BehaviorSubject<string>(null);
constructor(
private authorizeService: AuthorizeService,

View File

@ -14,7 +14,7 @@ import { LogoutActions, ApplicationPaths, ReturnUrlType } from '../api-authoriza
styleUrls: ['./logout.component.css']
})
export class LogoutComponent implements OnInit {
private message = new BehaviorSubject<string>(null);
public message = new BehaviorSubject<string>(null);
constructor(
private authorizeService: AuthorizeService,

View File

@ -34,6 +34,7 @@
"Data/**",
"Models/**",
"ClientApp/src/components/api-authorization/**",
"ClientApp/src/setupTests.js",
"Controllers/OidcConfigurationController.cs"
]
},

View File

@ -6,7 +6,7 @@ import { FetchData } from './components/FetchData';
import { Counter } from './components/Counter';
////#if (IndividualLocalAuth)
import AuthorizeRoute from './components/api-authorization/AuthorizeRoute';
import ApiAuthorizationRoutes from './components/api-authorization/ApiAuthorizationRoutes'
import ApiAuthorizationRoutes from './components/api-authorization/ApiAuthorizationRoutes';
import { ApplicationPaths } from './components/api-authorization/ApiAuthorizationConstants';
////#endif

View File

@ -3,10 +3,11 @@ import ReactDOM from 'react-dom';
import { MemoryRouter } from 'react-router-dom';
import App from './App';
it('renders without crashing', () => {
it('renders without crashing', async () => {
const div = document.createElement('div');
ReactDOM.render(
<MemoryRouter>
<App />
</MemoryRouter>, div);
await new Promise(resolve => setTimeout(resolve, 1000));
});

View File

@ -1,4 +1,4 @@
import React from 'react'
import React from 'react'
import { Component } from 'react'
import { Route, Redirect } from 'react-router-dom'
import { ApplicationPaths, QueryParameterNames } from './ApiAuthorizationConstants'

View File

@ -1,4 +1,4 @@
import { UserManager, WebStorageStateStore } from 'oidc-client';
import { UserManager, WebStorageStateStore } from 'oidc-client';
import { ApplicationPaths, ApplicationName } from './ApiAuthorizationConstants';
export class AuthorizeService {

View File

@ -1,4 +1,4 @@
import React from 'react'
import React from 'react'
import { Component } from 'react';
import authService from './AuthorizeService';
import { AuthenticationResultStatus } from './AuthorizeService';

View File

@ -1,4 +1,4 @@
import React, { Component, Fragment } from 'react';
import React, { Component, Fragment } from 'react';
import { NavItem, NavLink } from 'reactstrap';
import { Link } from 'react-router-dom';
import authService from './AuthorizeService';

View File

@ -1,4 +1,4 @@
import React from 'react'
import React from 'react'
import { Component } from 'react';
import authService from './AuthorizeService';
import { AuthenticationResultStatus } from './AuthorizeService';

View File

@ -0,0 +1,23 @@
const localStorageMock = {
getItem: jest.fn(),
setItem: jest.fn(),
removeItem: jest.fn(),
clear: jest.fn(),
};
global.localStorage = localStorageMock;
// Mock the request issued by the react app to get the client configuration parameters.
window.fetch = () => {
return Promise.resolve(
{
ok: true,
json: () => Promise.resolve({
"authority": "https://localhost:5001",
"client_id": "Company.WebApplication1",
"redirect_uri": "https://localhost:5001/authentication/login-callback",
"post_logout_redirect_uri": "https://localhost:5001/authentication/logout-callback",
"response_type": "id_token token",
"scope": "Company.WebApplication1API openid profile"
})
});
};

View File

@ -1,4 +1,11 @@
// 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 Microsoft.AspNetCore.E2ETesting;
using ProjectTemplates.Tests.Helpers;
using Templates.Test.Helpers;
using Xunit;
[assembly: TestFramework("Microsoft.AspNetCore.E2ETesting.XunitTestFrameworkWithAssemblyFixture", "ProjectTemplates.Tests")]
[assembly: AssemblyFixture(typeof(ProjectFactoryFixture))]
[assembly: AssemblyFixture(typeof(SeleniumStandaloneServer))]

View File

@ -4,9 +4,11 @@
using System;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using ProjectTemplates.Tests.Helpers;
using Templates.Test.Helpers;
using Xunit;
using Xunit.Abstractions;
@ -14,9 +16,25 @@ namespace Templates.Test
{
public class BaselineTest
{
private static readonly Regex TemplateNameRegex = new Regex(
"new (?<template>[a-zA-Z]+)",
RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.Singleline,
TimeSpan.FromSeconds(1));
private static readonly Regex AuthenticationOptionRegex = new Regex(
"-au (?<auth>[a-zA-Z]+)",
RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.Singleline,
TimeSpan.FromSeconds(1));
private static readonly Regex LanguageRegex = new Regex(
"--language (?<language>\\w+)",
RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.Singleline,
TimeSpan.FromSeconds(1));
public BaselineTest(ProjectFactoryFixture projectFactory, ITestOutputHelper output)
{
Project = projectFactory.CreateProject(output);
ProjectFactory = projectFactory;
Output = output;
}
public Project Project { get; set; }
@ -47,14 +65,20 @@ namespace Templates.Test
}
}
public ProjectFactoryFixture ProjectFactory { get; }
public ITestOutputHelper Output { get; }
[Theory]
[MemberData(nameof(TemplateBaselines))]
public void Template_Produces_The_Right_Set_Of_Files(string arguments, string[] expectedFiles)
public async Task Template_Produces_The_Right_Set_Of_FilesAsync(string arguments, string[] expectedFiles)
{
Project.RunDotNet(arguments);
Project = await ProjectFactory.GetOrCreateProject("baseline" + SanitizeArgs(arguments), Output);
var createResult = await Project.RunDotNetNewRawAsync(arguments);
Assert.True(createResult.ExitCode == 0, createResult.GetFormattedOutput());
foreach (var file in expectedFiles)
{
Project.AssertFileExists(file, shouldExist: true);
AssertFileExists(Project.TemplateOutputDir, file, shouldExist: true);
}
var filesInFolder = Directory.EnumerateFiles(Project.TemplateOutputDir, "*", SearchOption.AllDirectories);
@ -73,5 +97,36 @@ namespace Templates.Test
Assert.Contains(relativePath, expectedFiles);
}
}
private string SanitizeArgs(string arguments)
{
var text = TemplateNameRegex.Match(arguments)
.Groups.TryGetValue("template", out var template) ? template.Value : "";
text += AuthenticationOptionRegex.Match(arguments)
.Groups.TryGetValue("auth", out var auth) ? auth.Value : "";
text += arguments.Contains("--uld") ? "uld" : "";
text += LanguageRegex.Match(arguments)
.Groups.TryGetValue("language", out var language) ? language.Value.Replace("#", "Sharp") : "";
return text;
}
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);
}
}
}
}

View File

@ -13,6 +13,7 @@ using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Security.Cryptography;
using System.Threading.Tasks;
using Templates.Test.Helpers;
@ -27,14 +28,22 @@ namespace Templates.Test
private readonly HttpClient _httpClient;
private static List<ScriptTag> _scriptTags;
private static List<LinkTag> _linkTags;
private static readonly string[] _packages;
static CdnScriptTagTests()
{
var packages = MondoHelpers.GetNupkgFiles();
var searchPattern = "*.nupkg";
_packages = Directory.EnumerateFiles(
ResolveFolder("ArtifactsShippingPackagesDir"),
searchPattern)
.Concat(Directory.EnumerateFiles(
ResolveFolder("ArtifactsNonShippingPackagesDir"),
searchPattern))
.ToArray();
_scriptTags = new List<ScriptTag>();
_linkTags = new List<LinkTag>();
foreach (var packagePath in packages)
foreach (var packagePath in _packages)
{
var tags = GetTags(packagePath);
_scriptTags.AddRange(tags.scripts);
@ -42,6 +51,11 @@ namespace Templates.Test
}
}
private static string ResolveFolder(string folder) =>
typeof(CdnScriptTagTests).Assembly
.GetCustomAttributes<AssemblyMetadataAttribute>()
.Single(a => a.Key == folder).Value;
public CdnScriptTagTests(ITestOutputHelper output)
{
_output = output;
@ -162,7 +176,8 @@ namespace Templates.Test
private async Task<HttpResponseMessage> GetFromCDN(string src)
{
var logger = NullLogger.Instance;
return await RetryHelper.RetryRequest(async () => {
return await RetryHelper.RetryRequest(async () =>
{
var request = new HttpRequestMessage(HttpMethod.Get, new Uri(src));
return await _httpClient.SendAsync(request);
}, logger);
@ -191,7 +206,7 @@ namespace Templates.Test
private static string GetFileContentFromArchive(ScriptTag scriptTag, string relativeFilePath)
{
var file = MondoHelpers.GetNupkgFiles().Single(f => f.EndsWith(scriptTag.FileName));
var file = _packages.Single(f => f.EndsWith(scriptTag.FileName));
using (var zip = new ZipArchive(File.OpenRead(file), ZipArchiveMode.Read, leaveOpen: false))
{
var entry = zip.Entries

View File

@ -1,4 +1,4 @@
<Project>
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.targets))\Directory.Build.targets" />
<Import Project="GenerateTestProps.targets" />
<Import Project="$(MSBuildThisFileDirectory)Infrastructure\GenerateTestProps.targets" />
</Project>

View File

@ -1,8 +1,8 @@
// 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 Microsoft.AspNetCore.E2ETesting;
using ProjectTemplates.Tests.Helpers;
using System.Threading.Tasks;
using Templates.Test.Helpers;
using Xunit;
using Xunit.Abstractions;
@ -12,22 +12,50 @@ namespace Templates.Test
{
public EmptyWebTemplateTest(ProjectFactoryFixture projectFactory, ITestOutputHelper output)
{
Project = projectFactory.CreateProject(output);
ProjectFactory = projectFactory;
Output = output;
}
public Project Project { get; }
public Project Project { get; set; }
public ProjectFactoryFixture ProjectFactory { get; }
public ITestOutputHelper Output { get; }
[Fact]
public void EmptyWebTemplate()
public async Task EmptyWebTemplateAsync()
{
Project.RunDotNetNew("web");
Project = await ProjectFactory.GetOrCreateProject("empty", Output);
foreach (var publish in new[] { false, true })
var createResult = await Project.RunDotNetNewAsync("web");
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/netcoreapp3.0/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));
using (var aspNetProcess = Project.StartBuiltProjectAsync())
{
using (var aspNetProcess = Project.StartAspNetProcess(publish))
{
aspNetProcess.AssertOk("/");
}
Assert.False(
aspNetProcess.Process.HasExited,
ErrorMessages.GetFailedProcessMessageOrEmpty("Run built project", Project, aspNetProcess.Process));
await aspNetProcess.AssertOk("/");
}
using (var aspNetProcess = Project.StartPublishedProjectAsync())
{
Assert.False(
aspNetProcess.Process.HasExited,
ErrorMessages.GetFailedProcessMessageOrEmpty("Run published project", Project, aspNetProcess.Process));
await aspNetProcess.AssertOk("/");
}
}
}

View File

@ -1,65 +0,0 @@
using System;
using System.Diagnostics;
using System.IO;
namespace Templates.Test.Helpers
{
internal class AddFirewallExclusion : IDisposable
{
private bool _disposedValue = false;
private readonly string _exclusionPath;
public AddFirewallExclusion(string exclusionPath)
{
if (!File.Exists(exclusionPath))
{
throw new FileNotFoundException($"File {exclusionPath} was not found.");
}
_exclusionPath = exclusionPath;
var startInfo = new ProcessStartInfo
{
RedirectStandardError = true,
RedirectStandardOutput = true,
CreateNoWindow = true,
FileName = "cmd.exe",
Arguments = $"/c netsh advfirewall firewall add rule name=\"Allow {exclusionPath}\" dir=in action=allow program=\"{exclusionPath}\"",
UseShellExecute = false,
Verb = "runas",
WindowStyle = ProcessWindowStyle.Hidden,
};
Process.Start(startInfo);
}
public void Dispose()
{
Dispose(true);
}
private void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
var startInfo = new ProcessStartInfo
{
RedirectStandardError = true,
RedirectStandardOutput = true,
CreateNoWindow = true,
FileName = "cmd.exe",
Arguments = $"/c netsh advfirewall firewall delete rule name=\"Allow {_exclusionPath}\"",
UseShellExecute = false,
Verb = "runas",
WindowStyle = ProcessWindowStyle.Hidden,
};
Process.Start(startInfo);
}
_disposedValue = true;
}
}
}
}

View File

@ -3,10 +3,10 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Certificates.Generation;
using Microsoft.Extensions.CommandLineUtils;
using OpenQA.Selenium;
@ -18,16 +18,18 @@ namespace Templates.Test.Helpers
{
public class AspNetProcess : IDisposable
{
private const string DefaultFramework = "netcoreapp3.0";
private const string ListeningMessagePrefix = "Now listening on: ";
private static int Port = 5000 + new Random().Next(3000);
private readonly ProcessEx _process;
private readonly Uri _listeningUri;
private readonly HttpClient _httpClient;
private readonly ITestOutputHelper _output;
public AspNetProcess(ITestOutputHelper output, string workingDirectory, string projectName, bool publish)
internal ProcessEx Process { get; }
public AspNetProcess(
ITestOutputHelper output,
string workingDirectory,
string dllPath,
IDictionary<string,string> environmentVariables)
{
_output = output;
_httpClient = new HttpClient(new HttpClientHandler()
@ -35,54 +37,23 @@ namespace Templates.Test.Helpers
AllowAutoRedirect = true,
UseCookies = true,
CookieContainer = new CookieContainer(),
ServerCertificateCustomValidationCallback = (m, c, ch, p) => true
});
ServerCertificateCustomValidationCallback = (m, c, ch, p) => true,
})
{
Timeout = TimeSpan.FromMinutes(2)
};
var now = DateTimeOffset.Now;
new CertificateManager().EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1));
if (publish)
{
output.WriteLine("Publishing ASP.NET application...");
// Workaround for issue with runtime store not yet being published
// https://github.com/aspnet/Home/issues/2254#issuecomment-339709628
var extraArgs = "-p:PublishWithAspNetCoreTargetManifest=false";
ProcessEx
.Run(output, workingDirectory, DotNetMuxer.MuxerPathOrDefault(), $"publish -c Release {extraArgs}")
.WaitForExit(assertSuccess: true);
workingDirectory = Path.Combine(workingDirectory, "bin", "Release", DefaultFramework, "publish");
if (File.Exists(Path.Combine(workingDirectory, "ClientApp", "package.json")))
{
Npm.RestoreWithRetry(output, Path.Combine(workingDirectory, "ClientApp"));
}
}
else
{
output.WriteLine("Building ASP.NET application...");
ProcessEx
.Run(output, workingDirectory, DotNetMuxer.MuxerPathOrDefault(), "build -c Debug")
.WaitForExit(assertSuccess: true);
}
var envVars = new Dictionary<string, string>
{
{ "ASPNETCORE_URLS", $"http://127.0.0.1:0;https://127.0.0.1:0" }
};
if (!publish)
{
envVars["ASPNETCORE_ENVIRONMENT"] = "Development";
}
output.WriteLine("Running ASP.NET application...");
var dllPath = publish ? $"{projectName}.dll" : $"bin/Debug/{DefaultFramework}/{projectName}.dll";
_process = ProcessEx.Run(output, workingDirectory, DotNetMuxer.MuxerPathOrDefault(), $"exec {dllPath}", envVars: envVars);
Process = ProcessEx.Run(output, workingDirectory, DotNetMuxer.MuxerPathOrDefault(), $"exec {dllPath}", envVars: environmentVariables);
_listeningUri = GetListeningUri(output);
}
public void VisitInBrowser(IWebDriver driver)
{
_output.WriteLine($"Opening browser at {_listeningUri}...");
@ -116,31 +87,42 @@ namespace Templates.Test.Helpers
{
// Wait until the app is accepting HTTP requests
output.WriteLine("Waiting until ASP.NET application is accepting connections...");
var listeningMessage = _process
var listeningMessage = Process
.OutputLinesAsEnumerable
.Where(line => line != null)
.FirstOrDefault(line => line.Trim().StartsWith(ListeningMessagePrefix, StringComparison.Ordinal));
Assert.True(!string.IsNullOrEmpty(listeningMessage), $"ASP.NET process exited without listening for requests.\nOutput: { _process.Output }\nError: { _process.Error }");
listeningMessage = listeningMessage.Trim();
// Verify we have a valid URL to make requests to
var listeningUrlString = listeningMessage.Substring(ListeningMessagePrefix.Length);
output.WriteLine($"Detected that ASP.NET application is accepting connections on: {listeningUrlString}");
listeningUrlString = listeningUrlString.Substring(0, listeningUrlString.IndexOf(':')) +
"://localhost" +
listeningUrlString.Substring(listeningUrlString.LastIndexOf(':'));
if (!string.IsNullOrEmpty(listeningMessage))
{
listeningMessage = listeningMessage.Trim();
// Verify we have a valid URL to make requests to
var listeningUrlString = listeningMessage.Substring(ListeningMessagePrefix.Length);
output.WriteLine($"Detected that ASP.NET application is accepting connections on: {listeningUrlString}");
listeningUrlString = listeningUrlString.Substring(0, listeningUrlString.IndexOf(':')) +
"://localhost" +
listeningUrlString.Substring(listeningUrlString.LastIndexOf(':'));
output.WriteLine("Sending requests to " + listeningUrlString);
return new Uri(listeningUrlString, UriKind.Absolute);
output.WriteLine("Sending requests to " + listeningUrlString);
return new Uri(listeningUrlString, UriKind.Absolute);
}
else
{
return null;
}
}
public void AssertOk(string requestUrl)
public Task AssertOk(string requestUrl)
=> AssertStatusCode(requestUrl, HttpStatusCode.OK);
public void AssertNotFound(string requestUrl)
public Task AssertNotFound(string requestUrl)
=> AssertStatusCode(requestUrl, HttpStatusCode.NotFound);
public void AssertStatusCode(string requestUrl, HttpStatusCode statusCode, string acceptContentType = null)
internal Task<HttpResponseMessage> SendRequest(string path)
{
return _httpClient.GetAsync(new Uri(_listeningUri, path));
}
public async Task AssertStatusCode(string requestUrl, HttpStatusCode statusCode, string acceptContentType = null)
{
var request = new HttpRequestMessage(
HttpMethod.Get,
@ -151,14 +133,14 @@ namespace Templates.Test.Helpers
request.Headers.Add("Accept", acceptContentType);
}
var response = _httpClient.SendAsync(request).Result;
var response = await _httpClient.SendAsync(request);
Assert.Equal(statusCode, response.StatusCode);
}
public void Dispose()
{
_httpClient.Dispose();
_process.Dispose();
Process.Dispose();
}
}
}

View File

@ -0,0 +1,20 @@
// 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.
namespace Templates.Test.Helpers
{
internal static class ErrorMessages
{
public static string GetFailedProcessMessage(string step, Project project, ProcessEx processResult)
{
return $@"Project {project.ProjectArguments} failed to {step}.
{processResult.GetFormattedOutput()}";
}
public static string GetFailedProcessMessageOrEmpty(string step, Project project, ProcessEx processResult)
{
return processResult.HasExited ? $@"Project {project.ProjectArguments} failed to {step}.
{processResult.GetFormattedOutput()}" : "";
}
}
}

View File

@ -1,44 +0,0 @@
// 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;
namespace Templates.Test.Helpers
{
public static class MondoHelpers
{
public static string[] GetNupkgFiles()
{
var mondoRoot = GetMondoRepoRoot();
#if DEBUG
var configuration = "Debug";
#else
var configuration = "Release";
#endif
return Directory.GetFiles(Path.Combine(mondoRoot, "artifacts", "packages", configuration), "*.nupkg", SearchOption.AllDirectories);
}
private static string GetMondoRepoRoot()
{
return FindAncestorDirectoryContaining(".gitmodules");
}
private static string FindAncestorDirectoryContaining(string filename)
{
var dir = AppContext.BaseDirectory;
while (dir != null)
{
if (File.Exists(Path.Combine(dir, filename)))
{
return dir;
}
dir = Directory.GetParent(dir)?.FullName;
}
throw new InvalidOperationException($"Could not find any ancestor directory containing {filename} at or above {AppContext.BaseDirectory}");
}
}
}

View File

@ -1,73 +0,0 @@
using System;
using System.IO;
using Xunit.Abstractions;
namespace Templates.Test.Helpers
{
public static class Npm
{
private static object NpmInstallLock = new object();
public static void RestoreWithRetry(ITestOutputHelper output, string workingDirectory)
{
// "npm restore" sometimes fails randomly in AppVeyor with errors like:
// EPERM: operation not permitted, scandir <path>...
// This appears to be a general NPM reliability issue on Windows which has
// been reported many times (e.g., https://github.com/npm/npm/issues/18380)
// So, allow multiple attempts at the restore.
const int maxAttempts = 3;
var attemptNumber = 0;
while (true)
{
try
{
attemptNumber++;
Restore(output, workingDirectory);
break; // Success
}
catch (Exception ex)
{
if (attemptNumber < maxAttempts)
{
output.WriteLine(
$"NPM restore in {workingDirectory} failed on attempt {attemptNumber} of {maxAttempts}. " +
$"Error was: {ex}");
// Clean up the possibly-incomplete node_modules dir before retrying
var nodeModulesDir = Path.Combine(workingDirectory, "node_modules");
if (Directory.Exists(nodeModulesDir))
{
Directory.Delete(nodeModulesDir, recursive: true);
}
}
else
{
output.WriteLine(
$"Giving up attempting NPM restore in {workingDirectory} after {attemptNumber} attempts.");
throw;
}
}
}
}
private static void Restore(ITestOutputHelper output, string workingDirectory)
{
// It's not safe to run multiple NPM installs in parallel
// https://github.com/npm/npm/issues/2500
lock (NpmInstallLock)
{
output.WriteLine($"Restoring NPM packages in '{workingDirectory}' using npm...");
ProcessEx.RunViaShell(output, workingDirectory, "npm install");
}
}
public static void Test(ITestOutputHelper outputHelper, string workingDirectory)
{
ProcessEx.RunViaShell(outputHelper, workingDirectory, "npm run lint");
if (!File.Exists(Path.Join(workingDirectory, "angular.json")))
{
ProcessEx.RunViaShell(outputHelper, workingDirectory, "npm run test");
}
}
}
}

View File

@ -1,8 +1,10 @@
using System;
// 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.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
@ -15,10 +17,7 @@ namespace Templates.Test.Helpers
{
internal class ProcessEx : IDisposable
{
private static readonly string NUGET_PACKAGES = typeof(ProcessEx).Assembly
.GetCustomAttributes<AssemblyMetadataAttribute>()
.First(attribute => attribute.Key == "TestPackageRestorePath")
.Value;
private static readonly string NUGET_PACKAGES = GetNugetPackagesRestorePath();
private readonly ITestOutputHelper _output;
private readonly Process _process;
@ -28,6 +27,54 @@ namespace Templates.Test.Helpers
private BlockingCollection<string> _stdoutLines;
private TaskCompletionSource<int> _exited;
public ProcessEx(ITestOutputHelper output, Process proc)
{
_output = output;
_stdoutCapture = new StringBuilder();
_stderrCapture = new StringBuilder();
_stdoutLines = new BlockingCollection<string>();
_process = proc;
proc.EnableRaisingEvents = true;
proc.OutputDataReceived += OnOutputData;
proc.ErrorDataReceived += OnErrorData;
proc.Exited += OnProcessExited;
proc.BeginOutputReadLine();
proc.BeginErrorReadLine();
_exited = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
}
public Task Exited => _exited.Task;
public bool HasExited => _process.HasExited;
public string Error
{
get
{
lock (_pipeCaptureLock)
{
return _stderrCapture.ToString();
}
}
}
public string Output
{
get
{
lock (_pipeCaptureLock)
{
return _stdoutCapture.ToString();
}
}
}
public IEnumerable<string> OutputLinesAsEnumerable => _stdoutLines.GetConsumingEnumerable();
public int ExitCode => _process.ExitCode;
public static ProcessEx Run(ITestOutputHelper output, string workingDirectory, string command, string args = null, IDictionary<string, string> envVars = null)
{
var startInfo = new ProcessStartInfo(command, args)
@ -55,59 +102,17 @@ namespace Templates.Test.Helpers
return new ProcessEx(output, proc);
}
public static void RunViaShell(ITestOutputHelper output, string workingDirectory, string commandAndArgs)
public static async Task<ProcessEx> RunViaShellAsync(ITestOutputHelper output, string workingDirectory, string commandAndArgs)
{
var (shellExe, argsPrefix) = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? ("cmd", "/c")
: ("bash", "-c");
Run(output, workingDirectory, shellExe, $"{argsPrefix} \"{commandAndArgs}\"")
.WaitForExit(assertSuccess: true);
var result = Run(output, workingDirectory, shellExe, $"{argsPrefix} \"{commandAndArgs}\"");
await result.Exited;
return result;
}
public ProcessEx(ITestOutputHelper output, Process proc)
{
_output = output;
_stdoutCapture = new StringBuilder();
_stderrCapture = new StringBuilder();
_stdoutLines = new BlockingCollection<string>();
_process = proc;
proc.EnableRaisingEvents = true;
proc.OutputDataReceived += OnOutputData;
proc.ErrorDataReceived += OnErrorData;
proc.Exited += OnProcessExited;
proc.BeginOutputReadLine();
proc.BeginErrorReadLine();
_exited = new TaskCompletionSource<int>();
}
public Task Exited => _exited.Task;
public string Error
{
get
{
lock (_pipeCaptureLock)
{
return _stderrCapture.ToString();
}
}
}
public string Output
{
get
{
lock (_pipeCaptureLock)
{
return _stdoutCapture.ToString();
}
}
}
public int ExitCode => _process.ExitCode;
private void OnErrorData(object sender, DataReceivedEventArgs e)
{
if (e.Data == null)
@ -151,6 +156,16 @@ namespace Templates.Test.Helpers
_exited.TrySetResult(_process.ExitCode);
}
internal string GetFormattedOutput()
{
if (!_process.HasExited)
{
throw new InvalidOperationException("Process has not finished running.");
}
return $"Process exited with code {_process.ExitCode}\nStdErr: {Error}\nStdOut: {Output}";
}
public void WaitForExit(bool assertSuccess)
{
Exited.Wait();
@ -161,6 +176,12 @@ namespace Templates.Test.Helpers
}
}
private static string GetNugetPackagesRestorePath() =>
typeof(ProcessEx).Assembly
.GetCustomAttributes<AssemblyMetadataAttribute>()
.First(attribute => attribute.Key == "TestPackageRestorePath")
.Value;
public void Dispose()
{
if (_process != null && !_process.HasExited)
@ -176,7 +197,5 @@ namespace Templates.Test.Helpers
_process.Exited -= OnProcessExited;
_process.Dispose();
}
public IEnumerable<string> OutputLinesAsEnumerable => _stdoutLines.GetConsumingEnumerable();
}
}

View File

@ -0,0 +1,388 @@
// 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.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.CommandLineUtils;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Templates.Test.Helpers
{
public class Project
{
public const string DefaultFramework = "netcoreapp3.0";
public SemaphoreSlim DotNetNewLock { get; set; }
public SemaphoreSlim NodeLock { get; set; }
public string ProjectName { get; set; }
public string ProjectArguments { get; set; }
public string ProjectGuid { get; set; }
public string TemplateOutputDir { get; set; }
public string TemplateBuildDir => Path.Combine(TemplateOutputDir, "bin", "Debug", DefaultFramework);
public string TemplatePublishDir => Path.Combine(TemplateOutputDir, "bin", "Release", DefaultFramework, "publish");
public ITestOutputHelper Output { get; set; }
public IMessageSink DiagnosticsMessageSink { get; set; }
internal async Task<ProcessEx> RunDotNetNewAsync(string templateName, string auth = null, string language = null, bool useLocalDB = false, bool noHttps = false)
{
var hiveArg = $"--debug:custom-hive \"{TemplatePackageInstaller.CustomHivePath}\"";
var args = $"new {templateName} {hiveArg}";
if (!string.IsNullOrEmpty(auth))
{
args += $" --auth {auth}";
}
if (!string.IsNullOrEmpty(language))
{
args += $" -lang {language}";
}
if (useLocalDB)
{
args += $" --use-local-db";
}
if (noHttps)
{
args += $" --no-https";
}
// Save a copy of the arguments used for better diagnostic error messages later.
// We omit the hive argument and the template output dir as they are not relevant and add noise.
ProjectArguments = args.Replace(hiveArg, "");
args += $" -o {TemplateOutputDir}";
// Only run one instance of 'dotnet new' at once, as a workaround for
// https://github.com/aspnet/templating/issues/63
await DotNetNewLock.WaitAsync();
try
{
var execution = ProcessEx.Run(Output, AppContext.BaseDirectory, DotNetMuxer.MuxerPathOrDefault(), args);
await execution.Exited;
return execution;
}
finally
{
DotNetNewLock.Release();
}
}
internal async Task<ProcessEx> RunDotNetPublishAsync(bool takeNodeLock = false)
{
Output.WriteLine("Publishing ASP.NET application...");
// Workaround for issue with runtime store not yet being published
// https://github.com/aspnet/Home/issues/2254#issuecomment-339709628
var extraArgs = "-p:PublishWithAspNetCoreTargetManifest=false";
// This is going to trigger a build, so we need to acquire the lock like in the other cases.
// We want to take the node lock as some builds run NPM as part of the build and we want to make sure
// it's run without interruptions.
var effectiveLock = takeNodeLock ? new OrderedLock(NodeLock, DotNetNewLock) : new OrderedLock(nodeLock: null, DotNetNewLock);
await effectiveLock.WaitAsync();
try
{
var result = ProcessEx.Run(Output, TemplateOutputDir, DotNetMuxer.MuxerPathOrDefault(), $"publish -c Release {extraArgs}");
await result.Exited;
return result;
}
finally
{
effectiveLock.Release();
}
}
internal async Task<ProcessEx> RunDotNetBuildAsync(bool takeNodeLock = false)
{
Output.WriteLine("Building ASP.NET application...");
// This is going to trigger a build, so we need to acquire the lock like in the other cases.
// We want to take the node lock as some builds run NPM as part of the build and we want to make sure
// it's run without interruptions.
var effectiveLock = takeNodeLock ? new OrderedLock(NodeLock, DotNetNewLock) : new OrderedLock(nodeLock: null, DotNetNewLock);
await effectiveLock.WaitAsync();
try
{
var result = ProcessEx.Run(Output, TemplateOutputDir, DotNetMuxer.MuxerPathOrDefault(), "build -c Debug");
await result.Exited;
return result;
}
finally
{
effectiveLock.Release();
}
}
internal AspNetProcess StartBuiltProjectAsync()
{
var environment = new Dictionary<string, string>
{
["ASPNETCORE_URLS"] = $"http://127.0.0.1:0;https://127.0.0.1:0",
["ASPNETCORE_ENVIRONMENT"] = "Development"
};
var projectDll = Path.Combine(TemplateBuildDir, $"{ProjectName}.dll");
return new AspNetProcess(Output, TemplateOutputDir, projectDll, environment);
}
internal AspNetProcess StartPublishedProjectAsync()
{
var environment = new Dictionary<string, string>
{
["ASPNETCORE_URLS"] = $"http://127.0.0.1:0;https://127.0.0.1:0",
};
var projectDll = $"{ProjectName}.dll";
return new AspNetProcess(Output, TemplatePublishDir, projectDll, environment);
}
internal async Task<ProcessEx> RestoreWithRetryAsync(ITestOutputHelper output, string workingDirectory)
{
// "npm restore" sometimes fails randomly in AppVeyor with errors like:
// EPERM: operation not permitted, scandir <path>...
// This appears to be a general NPM reliability issue on Windows which has
// been reported many times (e.g., https://github.com/npm/npm/issues/18380)
// So, allow multiple attempts at the restore.
const int maxAttempts = 3;
var attemptNumber = 0;
ProcessEx restoreResult;
do
{
restoreResult = await RestoreAsync(output, workingDirectory);
if (restoreResult.ExitCode == 0)
{
return restoreResult;
}
else
{
// TODO: We should filter for EPEM here to avoid masking other errors silently.
output.WriteLine(
$"NPM restore in {workingDirectory} failed on attempt {attemptNumber} of {maxAttempts}. " +
$"Error was: {restoreResult.GetFormattedOutput()}");
// Clean up the possibly-incomplete node_modules dir before retrying
CleanNodeModulesFolder(workingDirectory, output);
}
attemptNumber++;
} while (attemptNumber < maxAttempts);
output.WriteLine($"Giving up attempting NPM restore in {workingDirectory} after {attemptNumber} attempts.");
return restoreResult;
void CleanNodeModulesFolder(string workingDirectory, ITestOutputHelper output)
{
var nodeModulesDir = Path.Combine(workingDirectory, "node_modules");
try
{
if (Directory.Exists(nodeModulesDir))
{
Directory.Delete(nodeModulesDir, recursive: true);
}
}
catch
{
output.WriteLine($"Failed to clean up node_modules folder at {nodeModulesDir}.");
}
}
}
private async Task<ProcessEx> RestoreAsync(ITestOutputHelper output, string workingDirectory)
{
// It's not safe to run multiple NPM installs in parallel
// https://github.com/npm/npm/issues/2500
await NodeLock.WaitAsync();
try
{
output.WriteLine($"Restoring NPM packages in '{workingDirectory}' using npm...");
var result = await ProcessEx.RunViaShellAsync(output, workingDirectory, "npm install");
return result;
}
finally
{
NodeLock.Release();
}
}
internal async Task<ProcessEx> RunDotNetEfCreateMigrationAsync(string migrationName)
{
var assembly = typeof(ProjectFactoryFixture).Assembly;
var dotNetEfFullPath = assembly.GetCustomAttributes<AssemblyMetadataAttribute>()
.First(attribute => attribute.Key == "DotNetEfFullPath")
.Value;
var args = $"\"{dotNetEfFullPath}\" --verbose --no-build migrations add {migrationName}";
// Only run one instance of 'dotnet new' at once, as a workaround for
// https://github.com/aspnet/templating/issues/63
await DotNetNewLock.WaitAsync();
try
{
var result = ProcessEx.Run(Output, TemplateOutputDir, DotNetMuxer.MuxerPathOrDefault(), args);
await result.Exited;
return result;
}
finally
{
DotNetNewLock.Release();
}
}
// If this fails, you should generate new migrations via migrations/updateMigrations.cmd
public void AssertEmptyMigration(string migration)
{
var fullPath = Path.Combine(TemplateOutputDir, "Data/Migrations");
var file = Directory.EnumerateFiles(fullPath).Where(f => f.EndsWith($"{migration}.cs")).FirstOrDefault();
Assert.NotNull(file);
var contents = File.ReadAllText(file);
var emptyMigration = @"protected override void Up(MigrationBuilder migrationBuilder)
{
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}";
// This comparison can break depending on how GIT checked out newlines on different files.
Assert.Contains(RemoveNewLines(emptyMigration), RemoveNewLines(contents));
static string RemoveNewLines(string str)
{
return str.Replace("\n", string.Empty).Replace("\r", string.Empty);
}
}
internal async Task<ProcessEx> RunDotNetNewRawAsync(string arguments)
{
await DotNetNewLock.WaitAsync();
try
{
var result = ProcessEx.Run(
Output,
AppContext.BaseDirectory,
DotNetMuxer.MuxerPathOrDefault(),
arguments +
$" --debug:custom-hive \"{TemplatePackageInstaller.CustomHivePath}\"" +
$" -o {TemplateOutputDir}");
await result.Exited;
return result;
}
finally
{
DotNetNewLock.Release();
}
}
public void Dispose()
{
DeleteOutputDirectory();
}
public void DeleteOutputDirectory()
{
const int NumAttempts = 10;
for (var numAttemptsRemaining = NumAttempts; numAttemptsRemaining > 0; numAttemptsRemaining--)
{
try
{
Directory.Delete(TemplateOutputDir, true);
return;
}
catch (Exception ex)
{
if (numAttemptsRemaining > 1)
{
DiagnosticsMessageSink.OnMessage(new DiagnosticMessage($"Failed to delete directory {TemplateOutputDir} because of error {ex.Message}. Will try again {numAttemptsRemaining - 1} more time(s)."));
Thread.Sleep(3000);
}
else
{
DiagnosticsMessageSink.OnMessage(new DiagnosticMessage($"Giving up trying to delete directory {TemplateOutputDir} after {NumAttempts} attempts. Most recent error was: {ex.StackTrace}"));
}
}
}
}
private class OrderedLock
{
private bool _nodeLockTaken;
private bool _dotNetLockTaken;
public OrderedLock(SemaphoreSlim nodeLock, SemaphoreSlim dotnetLock)
{
NodeLock = nodeLock;
DotnetLock = dotnetLock;
}
public SemaphoreSlim NodeLock { get; }
public SemaphoreSlim DotnetLock { get; }
public async Task WaitAsync()
{
if (NodeLock == null)
{
await DotnetLock.WaitAsync();
_dotNetLockTaken = true;
return;
}
try
{
// We want to take the NPM lock first as is going to be the busiest one, and we want other threads to be
// able to run dotnet new while we are waiting for another thread to finish running NPM.
await NodeLock.WaitAsync();
_nodeLockTaken = true;
await DotnetLock.WaitAsync();
_dotNetLockTaken = true;
}
catch
{
if (_nodeLockTaken)
{
NodeLock.Release();
_nodeLockTaken = false;
}
throw;
}
}
public void Release()
{
try
{
if (_dotNetLockTaken)
{
DotnetLock.Release();
_dotNetLockTaken = false;
}
}
finally
{
if (_nodeLockTaken)
{
NodeLock.Release();
_nodeLockTaken = false;
}
}
}
}
}
}

View File

@ -8,61 +8,55 @@ using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using Microsoft.Extensions.CommandLineUtils;
using Templates.Test.Helpers;
using Xunit;
using System.Threading.Tasks;
using Xunit.Abstractions;
namespace ProjectTemplates.Tests.Helpers
namespace Templates.Test.Helpers
{
public class ProjectFactoryFixture : IDisposable
{
private static object DotNetNewLock = new object();
private static SemaphoreSlim DotNetNewLock = new SemaphoreSlim(1);
private static SemaphoreSlim NodeLock = new SemaphoreSlim(1);
private ConcurrentBag<Project> _projects = new ConcurrentBag<Project>();
private ConcurrentDictionary<string, Project> _projects = new ConcurrentDictionary<string, Project>();
public Project CreateProject(ITestOutputHelper output)
public IMessageSink DiagnosticsMessageSink { get; }
public ProjectFactoryFixture(IMessageSink diagnosticsMessageSink)
{
TemplatePackageInstaller.EnsureTemplatingEngineInitialized(output);
var project = new Project
{
DotNetNewLock = DotNetNewLock,
Output = output,
ProjectGuid = Guid.NewGuid().ToString("N").Substring(0, 6)
};
project.ProjectName = $"AspNet.Template.{project.ProjectGuid}";
_projects.Add(project);
var assemblyPath = GetType().GetTypeInfo().Assembly.CodeBase;
var assemblyUri = new Uri(assemblyPath, UriKind.Absolute);
var basePath = Path.GetDirectoryName(assemblyUri.LocalPath);
project.TemplateOutputDir = Path.Combine(basePath, "TestTemplates", project.ProjectName);
Directory.CreateDirectory(project.TemplateOutputDir);
// We don't want any of the host repo's build config interfering with
// how the test project is built, so disconnect it from the
// Directory.Build.props/targets context
var templatesTestsPropsFilePath = Path.Combine(basePath, "TemplateTests.props");
var directoryBuildPropsContent =
$@"<Project>
<Import Project=""Directory.Build.After.props"" Condition=""Exists('Directory.Build.After.props')"" />
</Project>";
File.WriteAllText(Path.Combine(project.TemplateOutputDir, "Directory.Build.props"), directoryBuildPropsContent);
// TODO: remove this once we get a newer version of the SDK which supports an implicit FrameworkReference
// cref https://github.com/aspnet/websdk/issues/424
var directoryBuildTargetsContent =
$@"<Project>
<Import Project=""{templatesTestsPropsFilePath}"" />
</Project>";
File.WriteAllText(Path.Combine(project.TemplateOutputDir, "Directory.Build.targets"), directoryBuildTargetsContent);
return project;
DiagnosticsMessageSink = diagnosticsMessageSink;
}
public async Task<Project> GetOrCreateProject(string projectKey, ITestOutputHelper output)
{
await TemplatePackageInstaller.EnsureTemplatingEngineInitializedAsync(output);
return _projects.GetOrAdd(
projectKey,
(key, outputHelper) =>
{
var project = new Project
{
DotNetNewLock = DotNetNewLock,
NodeLock = NodeLock,
Output = outputHelper,
DiagnosticsMessageSink = DiagnosticsMessageSink,
ProjectGuid = Guid.NewGuid().ToString("N").Substring(0, 6)
};
project.ProjectName = $"AspNet.{key}.{project.ProjectGuid}";
var assemblyPath = GetType().Assembly;
string basePath = GetTemplateFolderBasePath(assemblyPath);
project.TemplateOutputDir = Path.Combine(basePath, project.ProjectName);
return project;
},
output);
}
private static string GetTemplateFolderBasePath(Assembly assembly) =>
assembly.GetCustomAttributes<AssemblyMetadataAttribute>()
.Single(a => a.Key == "TestTemplateCreationFolder")
.Value;
public void Dispose()
{
var list = new List<Exception>();
@ -70,9 +64,9 @@ $@"<Project>
{
try
{
project.Dispose();
project.Value.Dispose();
}
catch(Exception e)
catch (Exception e)
{
list.Add(e);
}
@ -84,171 +78,4 @@ $@"<Project>
}
}
}
public class Project
{
public string ProjectName { get; set; }
public string ProjectGuid { get; set; }
public string TemplateOutputDir { get; set; }
public ITestOutputHelper Output { get; set; }
public object DotNetNewLock { get; set; }
public void RunDotNetNew(string templateName, string auth = null, string language = null, bool useLocalDB = false, bool noHttps = false)
{
var args = $"new {templateName} --debug:custom-hive \"{TemplatePackageInstaller.CustomHivePath}\"";
if (!string.IsNullOrEmpty(auth))
{
args += $" --auth {auth}";
}
if (!string.IsNullOrEmpty(language))
{
args += $" -lang {language}";
}
if (useLocalDB)
{
args += $" --use-local-db";
}
if (noHttps)
{
args += $" --no-https";
}
// Only run one instance of 'dotnet new' at once, as a workaround for
// https://github.com/aspnet/templating/issues/63
lock (DotNetNewLock)
{
ProcessEx.Run(Output, TemplateOutputDir, DotNetMuxer.MuxerPathOrDefault(), args).WaitForExit(assertSuccess: true);
}
}
public void RunDotNet(string arguments)
{
lock (DotNetNewLock)
{
ProcessEx.Run(Output, TemplateOutputDir, DotNetMuxer.MuxerPathOrDefault(), arguments + $" --debug:custom-hive \"{TemplatePackageInstaller.CustomHivePath}\"").WaitForExit(assertSuccess: true);
}
}
public void RunDotNetEfCreateMigration(string migrationName)
{
var assembly = typeof(ProjectFactoryFixture).Assembly;
var dotNetEfFullPath = assembly.GetCustomAttributes<AssemblyMetadataAttribute>()
.First(attribute => attribute.Key == "DotNetEfFullPath")
.Value;
var args = $"\"{dotNetEfFullPath}\" --verbose migrations add {migrationName}";
// Only run one instance of 'dotnet new' at once, as a workaround for
// https://github.com/aspnet/templating/issues/63
lock (DotNetNewLock)
{
ProcessEx.Run(Output, TemplateOutputDir, DotNetMuxer.MuxerPathOrDefault(), args).WaitForExit(assertSuccess: true);
}
}
public void AssertDirectoryExists(string path, bool shouldExist)
{
var fullPath = Path.Combine(TemplateOutputDir, path);
var doesExist = Directory.Exists(fullPath);
if (shouldExist)
{
Assert.True(doesExist, "Expected directory to exist, but it doesn't: " + path);
}
else
{
Assert.False(doesExist, "Expected directory not to exist, but it does: " + path);
}
}
// If this fails, you should generate new migrations via migrations/updateMigrations.cmd
public void AssertEmptyMigration(string migration)
{
var fullPath = Path.Combine(TemplateOutputDir, "Data/Migrations");
var file = Directory.EnumerateFiles(fullPath).Where(f => f.EndsWith($"{migration}.cs")).FirstOrDefault();
Assert.NotNull(file);
var contents = File.ReadAllText(file);
var emptyMigration = @"protected override void Up(MigrationBuilder migrationBuilder)
{
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}";
// This comparison can break depending on how GIT checked out newlines on different files.
Assert.Contains(RemoveNewLines(emptyMigration), RemoveNewLines(contents));
}
private static string RemoveNewLines(string str)
{
return str.Replace("\n", string.Empty).Replace("\r", string.Empty);
}
public void AssertFileExists(string path, bool shouldExist)
{
var fullPath = Path.Combine(TemplateOutputDir, 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);
}
}
public string ReadFile(string path)
{
AssertFileExists(path, shouldExist: true);
return File.ReadAllText(Path.Combine(TemplateOutputDir, path));
}
public AspNetProcess StartAspNetProcess(bool publish = false)
{
return new AspNetProcess(Output, TemplateOutputDir, ProjectName, publish);
}
public void Dispose()
{
DeleteOutputDirectory();
}
public void DeleteOutputDirectory()
{
const int NumAttempts = 10;
for (var numAttemptsRemaining = NumAttempts; numAttemptsRemaining > 0; numAttemptsRemaining--)
{
try
{
Directory.Delete(TemplateOutputDir, true);
return;
}
catch (Exception ex)
{
if (numAttemptsRemaining > 1)
{
Output.WriteLine($"Failed to delete directory {TemplateOutputDir} because of error {ex.Message}. Will try again {numAttemptsRemaining - 1} more time(s).");
Thread.Sleep(3000);
}
else
{
Output.WriteLine($"Giving up trying to delete directory {TemplateOutputDir} after {NumAttempts} attempts. Most recent error was: {ex.StackTrace}");
}
}
}
}
}
}

View File

@ -4,36 +4,20 @@
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.CommandLineUtils;
using Xunit;
using Xunit.Abstractions;
namespace Templates.Test.Helpers
{
internal class NullTestOutputHelper : ITestOutputHelper
{
public bool Throw { get; set; }
public string Output => null;
public void WriteLine(string message)
{
return;
}
public void WriteLine(string format, params object[] args)
{
return;
}
}
internal static class TemplatePackageInstaller
{
private static object _templatePackagesReinstallationLock = new object();
private static SemaphoreSlim InstallerLock = new SemaphoreSlim(1);
private static bool _haveReinstalledTemplatePackages;
private static object DotNetNewLock = new object();
private static readonly string[] _templatePackages = new[]
{
"Microsoft.DotNet.Common.ItemTemplates",
@ -51,11 +35,14 @@ namespace Templates.Test.Helpers
"Microsoft.DotNet.Web.Spa.ProjectTemplates.3.0"
};
public static string CustomHivePath { get; } = Path.Combine(AppContext.BaseDirectory, ".templateengine");
public static string CustomHivePath { get; } = typeof(TemplatePackageInstaller)
.Assembly.GetCustomAttributes<AssemblyMetadataAttribute>()
.Single(s => s.Key == "CustomTemplateHivePath").Value;
public static void EnsureTemplatingEngineInitialized(ITestOutputHelper output)
public static async Task EnsureTemplatingEngineInitializedAsync(ITestOutputHelper output)
{
lock (_templatePackagesReinstallationLock)
await InstallerLock.WaitAsync();
try
{
if (!_haveReinstalledTemplatePackages)
{
@ -63,68 +50,78 @@ namespace Templates.Test.Helpers
{
Directory.Delete(CustomHivePath, recursive: true);
}
InstallTemplatePackages(output);
await InstallTemplatePackages(output);
_haveReinstalledTemplatePackages = true;
}
}
}
public static ProcessEx RunDotNetNew(ITestOutputHelper output, string arguments, bool assertSuccess)
{
lock (DotNetNewLock)
finally
{
var proc = ProcessEx.Run(
output,
AppContext.BaseDirectory,
DotNetMuxer.MuxerPathOrDefault(),
$"new {arguments} --debug:custom-hive \"{CustomHivePath}\"");
proc.WaitForExit(assertSuccess);
return proc;
InstallerLock.Release();
}
}
private static void InstallTemplatePackages(ITestOutputHelper output)
public static async Task<ProcessEx> RunDotNetNew(ITestOutputHelper output, string arguments)
{
var proc = ProcessEx.Run(
output,
AppContext.BaseDirectory,
DotNetMuxer.MuxerPathOrDefault(),
$"new {arguments} --debug:custom-hive \"{CustomHivePath}\"");
await proc.Exited;
return proc;
}
private static async Task InstallTemplatePackages(ITestOutputHelper output)
{
var builtPackages = Directory.EnumerateFiles(
typeof(TemplatePackageInstaller).Assembly
.GetCustomAttributes<AssemblyMetadataAttribute>()
.Single(a => a.Key == "ArtifactsShippingPackagesDir").Value,
"*.nupkg")
.Where(p => _templatePackages.Any(t => Path.GetFileName(p).StartsWith(t, StringComparison.OrdinalIgnoreCase)))
.ToArray();
Assert.Equal(4, builtPackages.Length);
// Remove any previous or prebundled version of the template packages
foreach (var packageName in _templatePackages)
{
// We don't need this command to succeed, because we'll verify next that
// uninstallation had the desired effect. This command is expected to fail
// in the case where the package wasn't previously installed.
RunDotNetNew(new NullTestOutputHelper(), $"--uninstall {packageName}", assertSuccess: false);
await RunDotNetNew(output, $"--uninstall {packageName}");
}
VerifyCannotFindTemplate(output, "web");
VerifyCannotFindTemplate(output, "webapp");
VerifyCannotFindTemplate(output, "mvc");
VerifyCannotFindTemplate(output, "react");
VerifyCannotFindTemplate(output, "reactredux");
VerifyCannotFindTemplate(output, "angular");
await VerifyCannotFindTemplateAsync(output, "web");
await VerifyCannotFindTemplateAsync(output, "webapp");
await VerifyCannotFindTemplateAsync(output, "mvc");
await VerifyCannotFindTemplateAsync(output, "react");
await VerifyCannotFindTemplateAsync(output, "reactredux");
await VerifyCannotFindTemplateAsync(output, "angular");
var builtPackages = MondoHelpers.GetNupkgFiles();
var templatePackages = builtPackages.Where(b => _templatePackages.Any(t => Path.GetFileName(b).StartsWith(t, StringComparison.OrdinalIgnoreCase)));
Assert.Equal(4, templatePackages.Count());
foreach (var packagePath in templatePackages)
foreach (var packagePath in builtPackages)
{
output.WriteLine($"Installing templates package {packagePath}...");
RunDotNetNew(output, $"--install \"{packagePath}\"", assertSuccess: true);
var result = await RunDotNetNew(output, $"--install \"{packagePath}\"");
Assert.True(result.ExitCode == 0, result.GetFormattedOutput());
}
VerifyCanFindTemplate(output, "webapp");
VerifyCanFindTemplate(output, "web");
VerifyCanFindTemplate(output, "react");
await VerifyCanFindTemplate(output, "webapp");
await VerifyCanFindTemplate(output, "web");
await VerifyCanFindTemplate(output, "react");
}
private static void VerifyCanFindTemplate(ITestOutputHelper output, string templateName)
private static async Task VerifyCanFindTemplate(ITestOutputHelper output, string templateName)
{
var proc = RunDotNetNew(output, $"", assertSuccess: false);
var proc = await RunDotNetNew(output, $"");
if (!proc.Output.Contains($" {templateName} "))
{
throw new InvalidOperationException($"Couldn't find {templateName} as an option in {proc.Output}.");
}
}
private static void VerifyCannotFindTemplate(ITestOutputHelper output, string templateName)
private static async Task VerifyCannotFindTemplateAsync(ITestOutputHelper output, string templateName)
{
// Verify we really did remove the previous templates
var tempDir = Path.Combine(AppContext.BaseDirectory, Path.GetRandomFileName(), Guid.NewGuid().ToString("D"));
@ -132,7 +129,7 @@ namespace Templates.Test.Helpers
try
{
var proc = RunDotNetNew(output, $"\"{templateName}\"", assertSuccess: false);
var proc = await RunDotNetNew(output, $"\"{templateName}\"");
if (!proc.Error.Contains($"No templates matched the input template name: {templateName}."))
{

View File

@ -1,3 +1,6 @@
// 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 OpenQA.Selenium;
using OpenQA.Selenium.Interactions;
using OpenQA.Selenium.Support.UI;
@ -8,6 +11,12 @@ namespace Templates.Test.Helpers
{
public static class WebDriverExtensions
{
// Maximum time any action performed by WebDriver will wait before failing.
// Any action will have to be completed in at most 10 seconds.
// Providing a smaller value won't improve the speed of the tests in any
// significant way and will make them more prone to fail on slower drivers.
internal const int DefaultMaxWaitTimeInSeconds = 10;
public static string GetText(this ISearchContext driver, string cssSelector)
{
return driver.FindElement(By.CssSelector(cssSelector)).Text;
@ -27,10 +36,6 @@ namespace Templates.Test.Helpers
.Perform();
}
public static void Click(this IWebDriver driver, string cssSelector)
{
Click(driver, null, cssSelector);
}
public static void Click(this IWebDriver driver, ISearchContext searchContext, string cssSelector)
{
@ -47,21 +52,11 @@ namespace Templates.Test.Helpers
return webElement.FindElement(By.XPath(".."));
}
public static IWebElement FindElement(this IWebDriver driver, string cssSelector, int timeoutSeconds)
{
return FindElement(driver, null, cssSelector, timeoutSeconds);
}
public static IWebElement FindElement(this IWebDriver driver, ISearchContext searchContext, string cssSelector, int timeoutSeconds)
{
return FindElement(driver, searchContext, By.CssSelector(cssSelector), timeoutSeconds);
}
public static IWebElement FindElement(this IWebDriver driver, By by, int timeoutSeconds)
{
return FindElement(driver, null, by, timeoutSeconds);
}
public static IWebElement FindElement(this IWebDriver driver, ISearchContext searchContext, By by, int timeoutSeconds)
{
return new WebDriverWait(driver, TimeSpan.FromSeconds(timeoutSeconds))
@ -70,19 +65,19 @@ namespace Templates.Test.Helpers
public static void WaitForUrl(this IWebDriver browser, string expectedUrl)
{
new WebDriverWait(browser, TimeSpan.FromSeconds(WebDriverFactory.DefaultMaxWaitTimeInSeconds))
new WebDriverWait(browser, TimeSpan.FromSeconds(DefaultMaxWaitTimeInSeconds))
.Until(driver => driver.Url.Contains(expectedUrl, StringComparison.OrdinalIgnoreCase));
}
public static void WaitForElement(this IWebDriver browser, string expectedElementCss)
{
new WebDriverWait(browser, TimeSpan.FromSeconds(WebDriverFactory.DefaultMaxWaitTimeInSeconds))
new WebDriverWait(browser, TimeSpan.FromSeconds(DefaultMaxWaitTimeInSeconds))
.Until(driver => driver.FindElements(By.CssSelector(expectedElementCss)).Count > 0);
}
public static void WaitForText(this IWebDriver browser, string cssSelector, string expectedText)
{
new WebDriverWait(browser, TimeSpan.FromSeconds(WebDriverFactory.DefaultMaxWaitTimeInSeconds))
new WebDriverWait(browser, TimeSpan.FromSeconds(DefaultMaxWaitTimeInSeconds))
.Until(driver => {
try
{

View File

@ -1,15 +0,0 @@
using System;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
namespace Templates.Test.Helpers
{
public static class WebDriverFactory
{
// Maximum time any action performed by WebDriver will wait before failing.
// Any action will have to be completed in at most 10 seconds.
// Providing a smaller value won't improve the speed of the tests in any
// significant way and will make them more prone to fail on slower drivers.
internal const int DefaultMaxWaitTimeInSeconds = 10;
}
}

View File

@ -0,0 +1,3 @@
<Project>
<!-- This file gets copied above the template test projects so that we disconnect the templates from the rest of the repository -->
</Project>

View File

@ -0,0 +1,3 @@
<Project>
<Import Project="${TemplateTestsPropsPath}" />
</Project>

View File

@ -1,5 +1,9 @@
<Project>
<Target Name="GenerateTestProps" BeforeTargets="CoreCompile">
<Target
Name="GenerateTestProps"
BeforeTargets="CoreCompile"
DependsOnTargets="PrepareForTest"
Condition="$(DesignTimeBuild) != true">
<PropertyGroup>
<PropsProperties>
RestoreSources=$([MSBuild]::Escape("$(RestoreSources);$(ArtifactsShippingPackagesDir);$(ArtifactsNonShippingPackagesDir)"));
@ -12,8 +16,8 @@
</PropertyGroup>
<Sdk_GenerateFileFromTemplate
TemplateFile="$(MSBuildThisFileDirectory)TemplateTests.props.in"
TemplateFile="$(MSBuildThisFileDirectory)\TemplateTests.props.in"
Properties="$(PropsProperties)"
OutputPath="$(OutputPath)TemplateTests.props" />
OutputPath="$(TestTemplateTestsProps)" />
</Target>
</Project>

View File

@ -1,7 +1,9 @@
// 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 ProjectTemplates.Tests.Helpers;
using System.IO;
using System.Threading.Tasks;
using Templates.Test.Helpers;
using Xunit;
using Xunit.Abstractions;
@ -11,71 +13,152 @@ namespace Templates.Test
{
public MvcTemplateTest(ProjectFactoryFixture projectFactory, ITestOutputHelper output)
{
Project = projectFactory.CreateProject(output);
ProjectFactory = projectFactory;
Output = output;
}
public Project Project { get; }
public Project Project { get; set; }
public ProjectFactoryFixture ProjectFactory { get; }
public ITestOutputHelper Output { get; }
[Theory]
[InlineData(null)]
[InlineData("F#")]
private void MvcTemplate_NoAuthImpl(string languageOverride)
public async Task MvcTemplate_NoAuthImplAsync(string languageOverride)
{
Project.RunDotNetNew("mvc", language: languageOverride);
Project = await ProjectFactory.GetOrCreateProject("mvcnoauth" + (languageOverride == "F#" ? "fsharp" : "csharp"), Output);
Project.AssertDirectoryExists("Areas", false);
Project.AssertDirectoryExists("Extensions", false);
Project.AssertFileExists("urlRewrite.config", false);
Project.AssertFileExists("Controllers/AccountController.cs", false);
var createResult = await Project.RunDotNetNewAsync("mvc", language: languageOverride);
Assert.True(0 == createResult.ExitCode, ErrorMessages.GetFailedProcessMessage("create/restore", Project, createResult));
AssertDirectoryExists(Project.TemplateOutputDir, "Areas", false);
AssertDirectoryExists(Project.TemplateOutputDir, "Extensions", false);
AssertFileExists(Project.TemplateOutputDir, "urlRewrite.config", false);
AssertFileExists(Project.TemplateOutputDir, "Controllers/AccountController.cs", false);
var projectExtension = languageOverride == "F#" ? "fsproj" : "csproj";
var projectFileContents = Project.ReadFile($"{Project.ProjectName}.{projectExtension}");
var projectFileContents = ReadFile(Project.TemplateOutputDir, $"{Project.ProjectName}.{projectExtension}");
Assert.DoesNotContain(".db", projectFileContents);
Assert.DoesNotContain("Microsoft.EntityFrameworkCore.Tools", projectFileContents);
Assert.DoesNotContain("Microsoft.VisualStudio.Web.CodeGeneration.Design", projectFileContents);
Assert.DoesNotContain("Microsoft.EntityFrameworkCore.Tools.DotNet", projectFileContents);
Assert.DoesNotContain("Microsoft.Extensions.SecretManager.Tools", projectFileContents);
foreach (var publish in new[] { false, true })
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.
var buildResult = await Project.RunDotNetBuildAsync();
Assert.True(0 == buildResult.ExitCode, ErrorMessages.GetFailedProcessMessage("build", Project, buildResult));
using (var aspNetProcess = Project.StartBuiltProjectAsync())
{
using (var aspNetProcess = Project.StartAspNetProcess(publish))
{
aspNetProcess.AssertOk("/");
aspNetProcess.AssertOk("/Home/Privacy");
}
Assert.False(
aspNetProcess.Process.HasExited,
ErrorMessages.GetFailedProcessMessageOrEmpty("Run built project", Project, aspNetProcess.Process));
await aspNetProcess.AssertOk("/");
await aspNetProcess.AssertOk("/Home/Privacy");
}
using (var aspNetProcess = Project.StartPublishedProjectAsync())
{
Assert.False(
aspNetProcess.Process.HasExited,
ErrorMessages.GetFailedProcessMessageOrEmpty("Run published project", Project, aspNetProcess.Process));
await aspNetProcess.AssertOk("/");
await aspNetProcess.AssertOk("/Home/Privacy");
}
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void MvcTemplate_IndividualAuthImpl(bool useLocalDB)
public async Task MvcTemplate_IndividualAuthImplAsync(bool useLocalDB)
{
Project.RunDotNetNew("mvc", auth: "Individual", useLocalDB: useLocalDB);
Project = await ProjectFactory.GetOrCreateProject("mvcindividual" + (useLocalDB ? "uld" : ""), Output);
Project.AssertDirectoryExists("Extensions", false);
Project.AssertFileExists("urlRewrite.config", false);
Project.AssertFileExists("Controllers/AccountController.cs", false);
var createResult = await Project.RunDotNetNewAsync("mvc", auth: "Individual", useLocalDB: useLocalDB);
Assert.True(0 == createResult.ExitCode, ErrorMessages.GetFailedProcessMessage("create/restore", Project, createResult));
var projectFileContents = Project.ReadFile($"{Project.ProjectName}.csproj");
AssertDirectoryExists(Project.TemplateOutputDir, "Extensions", false);
AssertFileExists(Project.TemplateOutputDir, "urlRewrite.config", false);
AssertFileExists(Project.TemplateOutputDir, "Controllers/AccountController.cs", false);
var projectFileContents = ReadFile(Project.TemplateOutputDir, $"{Project.ProjectName}.csproj");
if (!useLocalDB)
{
Assert.Contains(".db", projectFileContents);
}
Project.RunDotNetEfCreateMigration("mvc");
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.
var buildResult = await Project.RunDotNetBuildAsync();
Assert.True(0 == buildResult.ExitCode, ErrorMessages.GetFailedProcessMessage("build", Project, buildResult));
var migrationsResult = await Project.RunDotNetEfCreateMigrationAsync("mvc");
Assert.True(0 == migrationsResult.ExitCode, ErrorMessages.GetFailedProcessMessage("run EF migrations", Project, migrationsResult));
Project.AssertEmptyMigration("mvc");
foreach (var publish in new[] { false, true })
using (var aspNetProcess = Project.StartBuiltProjectAsync())
{
using (var aspNetProcess = Project.StartAspNetProcess(publish))
{
aspNetProcess.AssertOk("/");
aspNetProcess.AssertOk("/Identity/Account/Login");
aspNetProcess.AssertOk("/Home/Privacy");
}
await aspNetProcess.AssertOk("/");
await aspNetProcess.AssertOk("/Identity/Account/Login");
await aspNetProcess.AssertOk("/Home/Privacy");
}
using (var aspNetProcess = Project.StartPublishedProjectAsync())
{
await aspNetProcess.AssertOk("/");
await aspNetProcess.AssertOk("/Identity/Account/Login");
await aspNetProcess.AssertOk("/Home/Privacy");
}
}
private void AssertDirectoryExists(string basePath, string path, bool shouldExist)
{
var fullPath = Path.Combine(basePath, path);
var doesExist = Directory.Exists(fullPath);
if (shouldExist)
{
Assert.True(doesExist, "Expected directory to exist, but it doesn't: " + path);
}
else
{
Assert.False(doesExist, "Expected directory not to exist, but it does: " + path);
}
}
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));
}
}
}

View File

@ -12,6 +12,12 @@
<SkipTests Condition="'$(RunTemplateTests)' != 'true'">true</SkipTests>
<!-- https://github.com/aspnet/AspNetCore/issues/6857 -->
<BuildHelixPayload>false</BuildHelixPayload>
<!-- Properties that affect test runs -->
<!-- TestTemplateCreationFolder is the folder where the templates will be created. Will point out to $(OutputDir)$(TestTemplateCreationFolder) -->
<TestTemplateCreationFolder>TestTemplates</TestTemplateCreationFolder>
<TestPackageRestorePath>$([MSBuild]::EnsureTrailingSlash('$(RepositoryRoot)'))obj\template-restore\</TestPackageRestorePath>
<TestTemplateTestsProps>TemplateTests.props</TestTemplateTestsProps>
</PropertyGroup>
<ItemGroup>
@ -45,10 +51,81 @@
</AssemblyAttribute>
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
<_Parameter1>TestPackageRestorePath</_Parameter1>
<_Parameter2>$([MSBuild]::EnsureTrailingSlash('$(RepositoryRoot)'))obj\template-restore\</_Parameter2>
<_Parameter2>$(TestPackageRestorePath)</_Parameter2>
</AssemblyAttribute>
</ItemGroup>
<Target Name="PrepareForTest" BeforeTargets="CoreCompile" Condition="$(DesignTimeBuild) != true">
<PropertyGroup>
<TestTemplateCreationFolder>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)$([MSBuild]::EnsureTrailingSlash('$(OutputPath)$(TestTemplateCreationFolder)'))'))</TestTemplateCreationFolder>
<TestTemplateTestsProps>$(TestTemplateCreationFolder)$(TestTemplateTestsProps)</TestTemplateTestsProps>
<CustomTemplateHivePath>$(TestTemplateCreationFolder)\Hives\$([System.Guid]::NewGuid())\.templateengine</CustomTemplateHivePath>
</PropertyGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
<_Parameter1>ArtifactsShippingPackagesDir</_Parameter1>
<_Parameter2>$(ArtifactsShippingPackagesDir)</_Parameter2>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
<_Parameter1>ArtifactsNonShippingPackagesDir</_Parameter1>
<_Parameter2>$(ArtifactsNonShippingPackagesDir)</_Parameter2>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
<_Parameter1>TestTemplateCreationFolder</_Parameter1>
<_Parameter2>$(TestTemplateCreationFolder)</_Parameter2>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
<_Parameter1>CustomTemplateHivePath</_Parameter1>
<_Parameter2>$(CustomTemplateHivePath)</_Parameter2>
</AssemblyAttribute>
</ItemGroup>
<Message Importance="high" Text="Preparing environment for tests" />
<!-- Remove the template creation folders and the package-restore folders to ensure that when we run the tests we don't
get cached results and changes show up.
-->
<ItemGroup>
<_ExistingFilesFromLastRun Include="$(TestTemplateCreationFolder)**\*" />
</ItemGroup>
<Delete Files="@(_ExistingFilesFromLastRun)" ContinueOnError="true" />
<Removedir Directories="$(TestTemplateCreationFolder)" Condition="Exists('$(TestTemplateCreationFolder)')" ContinueOnError="true">
<Output TaskParameter="RemovedDirectories" ItemName="_CleanedUpDirectories" />
</Removedir>
<Removedir Directories="$(TestPackageRestorePath)" Condition="Exists('$(TestPackageRestorePath)')" ContinueOnError="true">
<Output TaskParameter="RemovedDirectories" ItemName="_CleanedUpDirectories" />
</Removedir>
<Message Importance="high" Text="Removed directory %(_CleanedUpDirectories.Identity)" />
<MakeDir Directories="$(TestTemplateCreationFolder)">
<Output TaskParameter="DirectoriesCreated" ItemName="_CreatedDirectories" />
</MakeDir>
<MakeDir Directories="$(TestPackageRestorePath)">
<Output TaskParameter="DirectoriesCreated" ItemName="_CreatedDirectories" />
</MakeDir>
<Message Importance="high" Text="Created directory %(_CreatedDirectories.Identity)" />
<Sdk_GenerateFileFromTemplate
TemplateFile="$(MSBuildThisFileDirectory)Infrastructure\Directory.Build.targets.in"
Properties="TemplateTestsPropsPath=$(TestTemplateTestsProps)"
OutputPath="$(TestTemplateCreationFolder)Directory.Build.targets" />
<Sdk_GenerateFileFromTemplate
TemplateFile="$(MSBuildThisFileDirectory)Infrastructure\Directory.Build.props.in"
Properties=""
OutputPath="$(TestTemplateCreationFolder)Directory.Build.props" />
<Delete Files="$(TestTemplateTestsProps)" />
</Target>
<!-- Shared testing infrastructure for running E2E tests using selenium -->
<Import Project="$(SharedSourceRoot)E2ETesting\E2ETesting.targets" />

View File

@ -1,13 +1,11 @@
// 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 Microsoft.AspNetCore.E2ETesting;
using Microsoft.Extensions.CommandLineUtils;
using OpenQA.Selenium;
using ProjectTemplates.Tests.Helpers;
using System.IO;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.E2ETesting;
using OpenQA.Selenium;
using Templates.Test.Helpers;
using Xunit;
using Xunit.Abstractions;
@ -18,25 +16,52 @@ namespace Templates.Test
{
public RazorComponentsTemplateTest(ProjectFactoryFixture projectFactory, BrowserFixture browserFixture, ITestOutputHelper output) : base(browserFixture, output)
{
Project = projectFactory.CreateProject(output);
ProjectFactory = projectFactory;
}
public Project Project { get; }
public ProjectFactoryFixture ProjectFactory { get; set; }
public Project Project { get; private set; }
[Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/8244")]
public void RazorComponentsTemplateWorks()
public async Task RazorComponentsTemplateWorksAsync()
{
Project.RunDotNetNew("razorcomponents");
TestApplication(publish: false);
TestApplication(publish: true);
}
Project = await ProjectFactory.GetOrCreateProject("razorcomponents", Output);
private void TestApplication(bool publish)
{
using (var aspNetProcess = Project.StartAspNetProcess(publish))
var createResult = await Project.RunDotNetNewAsync("razorcomponents");
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/netcoreapp3.0/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));
using (var aspNetProcess = Project.StartBuiltProjectAsync())
{
aspNetProcess.AssertStatusCode("/", HttpStatusCode.OK, "text/html");
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();
}
}
using (var aspNetProcess = Project.StartPublishedProjectAsync())
{
Assert.False(
aspNetProcess.Process.HasExited,
ErrorMessages.GetFailedProcessMessageOrEmpty("Run published project", Project, aspNetProcess.Process));
await aspNetProcess.AssertStatusCode("/", HttpStatusCode.OK, "text/html");
if (BrowserFixture.IsHostAutomationSupported())
{
aspNetProcess.VisitInBrowser(Browser);
@ -67,7 +92,7 @@ namespace Templates.Test
var counterDisplay = Browser.FindElement("h1 + p");
Assert.Equal("Current count: 0", counterDisplay.Text);
Browser.Click(counterComponent, "button");
WaitAssert.Equal("Current count: 1", () => Browser.FindElement("h1+p").Text);
Browser.Equal("Current count: 1", () => Browser.FindElement("h1+p").Text);
// Can navigate to the 'fetch data' page
Browser.Click(By.PartialLinkText("Fetch data"));

View File

@ -1,8 +1,9 @@
// 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 Microsoft.AspNetCore.Testing.xunit;
using ProjectTemplates.Tests.Helpers;
using System.IO;
using System.Threading.Tasks;
using Templates.Test.Helpers;
using Xunit;
using Xunit.Abstractions;
@ -12,63 +13,130 @@ namespace Templates.Test
{
public RazorPagesTemplateTest(ProjectFactoryFixture projectFactory, ITestOutputHelper output)
{
Project = projectFactory.CreateProject(output);
ProjectFactory = projectFactory;
Output = output;
}
public Project Project { get; }
public Project Project { get; set; }
public ProjectFactoryFixture ProjectFactory { get; set; }
public ITestOutputHelper Output { get; }
[Fact]
private void RazorPagesTemplate_NoAuthImpl()
public async Task RazorPagesTemplate_NoAuthImplAsync()
{
Project.RunDotNetNew("razor");
Project = await ProjectFactory.GetOrCreateProject("razorpagesnoauth", Output);
Project.AssertFileExists("Pages/Shared/_LoginPartial.cshtml", false);
var createResult = await Project.RunDotNetNewAsync("razor");
Assert.True(0 == createResult.ExitCode, ErrorMessages.GetFailedProcessMessage("razor", Project, createResult));
var projectFileContents = Project.ReadFile($"{Project.ProjectName}.csproj");
AssertFileExists(Project.TemplateOutputDir, "Pages/Shared/_LoginPartial.cshtml", false);
var projectFileContents = ReadFile(Project.TemplateOutputDir, $"{Project.ProjectName}.csproj");
Assert.DoesNotContain(".db", projectFileContents);
Assert.DoesNotContain("Microsoft.EntityFrameworkCore.Tools", projectFileContents);
Assert.DoesNotContain("Microsoft.VisualStudio.Web.CodeGeneration.Design", projectFileContents);
Assert.DoesNotContain("Microsoft.EntityFrameworkCore.Tools.DotNet", projectFileContents);
Assert.DoesNotContain("Microsoft.Extensions.SecretManager.Tools", projectFileContents);
foreach (var publish in new[] { false, true })
var publishResult = await Project.RunDotNetPublishAsync();
Assert.True(0 == publishResult.ExitCode, ErrorMessages.GetFailedProcessMessage("publish", Project, createResult));
// 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.
var buildResult = await Project.RunDotNetBuildAsync();
Assert.True(0 == buildResult.ExitCode, ErrorMessages.GetFailedProcessMessage("build", Project, createResult));
using (var aspNetProcess = Project.StartBuiltProjectAsync())
{
using (var aspNetProcess = Project.StartAspNetProcess(publish))
{
aspNetProcess.AssertOk("/");
aspNetProcess.AssertOk("/Privacy");
}
Assert.False(
aspNetProcess.Process.HasExited,
ErrorMessages.GetFailedProcessMessageOrEmpty("Run built project", Project, aspNetProcess.Process));
await aspNetProcess.AssertOk("/");
await aspNetProcess.AssertOk("/Privacy");
}
using (var aspNetProcess = Project.StartPublishedProjectAsync())
{
Assert.False(
aspNetProcess.Process.HasExited,
ErrorMessages.GetFailedProcessMessageOrEmpty("Run published project", Project, aspNetProcess.Process));
await aspNetProcess.AssertOk("/");
await aspNetProcess.AssertOk("/Privacy");
}
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void RazorPagesTemplate_IndividualAuthImpl( bool useLocalDB)
public async Task RazorPagesTemplate_IndividualAuthImplAsync(bool useLocalDB)
{
Project.RunDotNetNew("razor", auth: "Individual", useLocalDB: useLocalDB);
Project = await ProjectFactory.GetOrCreateProject("razorpagesindividual" + (useLocalDB ? "uld" : ""), Output);
Project.AssertFileExists("Pages/Shared/_LoginPartial.cshtml", true);
var createResult = await Project.RunDotNetNewAsync("razor", auth: "Individual", useLocalDB: useLocalDB);
Assert.True(0 == createResult.ExitCode, ErrorMessages.GetFailedProcessMessage("create/restore", Project, createResult));
var projectFileContents = Project.ReadFile($"{Project.ProjectName}.csproj");
AssertFileExists(Project.TemplateOutputDir, "Pages/Shared/_LoginPartial.cshtml", true);
var projectFileContents = ReadFile(Project.TemplateOutputDir, $"{Project.ProjectName}.csproj");
if (!useLocalDB)
{
Assert.Contains(".db", projectFileContents);
}
Project.RunDotNetEfCreateMigration("razorpages");
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.
var buildResult = await Project.RunDotNetBuildAsync();
Assert.True(0 == buildResult.ExitCode, ErrorMessages.GetFailedProcessMessage("build", Project, buildResult));
var migrationsResult = await Project.RunDotNetEfCreateMigrationAsync("razorpages");
Assert.True(0 == migrationsResult.ExitCode, ErrorMessages.GetFailedProcessMessage("run EF migrations", Project, migrationsResult));
Project.AssertEmptyMigration("razorpages");
foreach (var publish in new[] { false, true })
using (var aspNetProcess = Project.StartBuiltProjectAsync())
{
using (var aspNetProcess = Project.StartAspNetProcess(publish))
{
aspNetProcess.AssertOk("/");
aspNetProcess.AssertOk("/Identity/Account/Login");
aspNetProcess.AssertOk("/Privacy");
}
await aspNetProcess.AssertOk("/");
await aspNetProcess.AssertOk("/Identity/Account/Login");
await aspNetProcess.AssertOk("/Privacy");
}
using (var aspNetProcess = Project.StartPublishedProjectAsync())
{
await aspNetProcess.AssertOk("/");
await aspNetProcess.AssertOk("/Identity/Account/Login");
await aspNetProcess.AssertOk("/Privacy");
}
}
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));
}
}
}

View File

@ -1,8 +1,9 @@
// 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.Threading.Tasks;
using Microsoft.AspNetCore.E2ETesting;
using ProjectTemplates.Tests.Helpers;
using Templates.Test.Helpers;
using Xunit;
using Xunit.Abstractions;
@ -13,8 +14,16 @@ namespace Templates.Test.SpaTemplateTest
public AngularTemplateTest(ProjectFactoryFixture projectFactory, BrowserFixture browserFixture, ITestOutputHelper output)
: base(projectFactory, browserFixture, output) { }
[Fact(Skip = "https://github.com/aspnet/AspNetCore-Internal/issues/1854")]
public void AngularTemplate_Works()
=> SpaTemplateImpl("angular");
[Fact]
public Task AngularTemplate_Works()
=> SpaTemplateImplAsync("angularnoauth", "angular", useLocalDb: false, usesAuth: false);
[Fact]
public Task AngularTemplate_IndividualAuth_Works()
=> SpaTemplateImplAsync("angularindividual", "angular", useLocalDb: false, usesAuth: true);
[Fact]
public Task AngularTemplate_IndividualAuth_Works_LocalDb()
=> SpaTemplateImplAsync("angularindividualuld", "angular", useLocalDb: true, usesAuth: true);
}
}

View File

@ -1,8 +1,9 @@
// 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.Threading.Tasks;
using Microsoft.AspNetCore.E2ETesting;
using ProjectTemplates.Tests.Helpers;
using Templates.Test.Helpers;
using Xunit;
using Xunit.Abstractions;
@ -15,8 +16,8 @@ namespace Templates.Test.SpaTemplateTest
{
}
[Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/7377")]
public void ReactReduxTemplate_Works_NetCore()
=> SpaTemplateImpl("reactredux");
[Fact]
public Task ReactReduxTemplate_Works_NetCore()
=> SpaTemplateImplAsync("reactredux", "reactredux",useLocalDb: false, usesAuth: false);
}
}

View File

@ -1,8 +1,9 @@
// 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.Threading.Tasks;
using Microsoft.AspNetCore.E2ETesting;
using ProjectTemplates.Tests.Helpers;
using Templates.Test.Helpers;
using Xunit;
using Xunit.Abstractions;
@ -15,8 +16,16 @@ namespace Templates.Test.SpaTemplateTest
{
}
[Fact(Skip="This test is flaky. Using https://github.com/aspnet/AspNetCore-Internal/issues/1745 to track re-enabling this.")]
public void ReactTemplate_Works_NetCore()
=> SpaTemplateImpl("react");
[Fact]
public Task ReactTemplate_Works_NetCore()
=> SpaTemplateImplAsync("reactnoauth", "react", useLocalDb: false, usesAuth: false);
[Fact]
public Task ReactTemplate_IndividualAuth_NetCore()
=> SpaTemplateImplAsync("reactindividual", "react", useLocalDb: false, usesAuth: true);
[Fact]
public Task ReactTemplate_IndividualAuth_NetCore_LocalDb()
=> SpaTemplateImplAsync("reactindividualuld", "react", useLocalDb: true, usesAuth: true);
}
}

View File

@ -1,11 +1,13 @@
// 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 Microsoft.AspNetCore.E2ETesting;
using OpenQA.Selenium;
using ProjectTemplates.Tests.Helpers;
using System;
using System.IO;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.E2ETesting;
using Newtonsoft.Json.Linq;
using OpenQA.Selenium;
using Templates.Test.Helpers;
using Xunit;
using Xunit.Abstractions;
@ -14,7 +16,6 @@ using Xunit.Abstractions;
#if EDGE
[assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly)]
#endif
[assembly: TestFramework("Microsoft.AspNetCore.E2ETesting.XunitTestFrameworkWithAssemblyFixture", "ProjectTemplates.Tests")]
namespace Templates.Test.SpaTemplateTest
{
public class SpaTemplateTestBase : BrowserTestBase
@ -22,49 +23,150 @@ namespace Templates.Test.SpaTemplateTest
public SpaTemplateTestBase(
ProjectFactoryFixture projectFactory, BrowserFixture browserFixture, ITestOutputHelper output) : base(browserFixture, output)
{
Project = projectFactory.CreateProject(output);
ProjectFactory = projectFactory;
}
public Project Project { get; }
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 void SpaTemplateImpl(string template, bool noHttps = false)
protected async Task SpaTemplateImplAsync(
string key,
string template,
bool useLocalDb = false,
bool usesAuth = false)
{
Project.RunDotNetNew(template, noHttps: noHttps);
Project = await ProjectFactory.GetOrCreateProject(key, Output);
// For some SPA templates, the NPM root directory is './ClientApp'. In other
// templates it's at the project root. Strictly speaking 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 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");
Assert.True(File.Exists(Path.Combine(clientAppSubdirPath, "package.json")), "Missing a package.json");
Npm.RestoreWithRetry(Output, clientAppSubdirPath);
Npm.Test(Output, clientAppSubdirPath);
TestApplication(publish: false);
TestApplication(publish: true);
}
private void TestApplication(bool publish)
{
using (var aspNetProcess = Project.StartAspNetProcess(publish))
var projectFileContents = ReadFile(Project.TemplateOutputDir, $"{Project.ProjectName}.csproj");
if (usesAuth && !useLocalDb)
{
aspNetProcess.AssertStatusCode("/", HttpStatusCode.OK, "text/html");
Assert.Contains(".db", projectFileContents);
}
var npmRestoreResult = await Project.RestoreWithRetryAsync(Output, clientAppSubdirPath);
Assert.True(0 == npmRestoreResult.ExitCode, ErrorMessages.GetFailedProcessMessage("npm restore", Project, npmRestoreResult));
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")
{
var testResult = await ProcessEx.RunViaShellAsync(Output, clientAppSubdirPath, "npm run test");
Assert.True(0 == testResult.ExitCode, ErrorMessages.GetFailedProcessMessage("npm run test", Project, testResult));
}
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.
var buildResult = await Project.RunDotNetBuildAsync();
Assert.True(0 == buildResult.ExitCode, ErrorMessages.GetFailedProcessMessage("build", Project, buildResult));
if (usesAuth)
{
var migrationsResult = await Project.RunDotNetEfCreateMigrationAsync(template);
Assert.True(0 == migrationsResult.ExitCode, ErrorMessages.GetFailedProcessMessage("run EF migrations", Project, migrationsResult));
Project.AssertEmptyMigration(template);
}
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())
{
aspNetProcess.VisitInBrowser(Browser);
TestBasicNavigation();
TestBasicNavigation(visitFetchData: !usesAuth);
}
}
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())
{
aspNetProcess.VisitInBrowser(Browser);
TestBasicNavigation(visitFetchData: !usesAuth);
}
}
}
private void TestBasicNavigation()
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)
{
}
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)
{
Browser.WaitForElement("ul");
// <title> element gets project ID injected into it during template execution
@ -85,16 +187,40 @@ namespace Templates.Test.SpaTemplateTest
Browser.Click(counterComponent, "button");
Assert.Equal("1", counterComponent.GetText("strong"));
// Can navigate to the 'fetch data' page
Browser.Click(By.PartialLinkText("Fetch data"));
Browser.WaitForUrl("fetch-data");
Assert.Equal("Weather forecast", Browser.GetText("h1"));
if (visitFetchData)
{
// Can navigate to the 'fetch data' page
Browser.Click(By.PartialLinkText("Fetch data"));
Browser.WaitForUrl("fetch-data");
Assert.Equal("Weather forecast", Browser.GetText("h1"));
// Asynchronously loads and displays the table of weather forecasts
var fetchDataComponent = Browser.FindElement("h1").Parent();
Browser.WaitForElement("table>tbody>tr");
var table = Browser.FindElement(fetchDataComponent, "table", timeoutSeconds: 5);
Assert.Equal(5, table.FindElements(By.CssSelector("tbody tr")).Count);
// Asynchronously loads and displays the table of weather forecasts
var fetchDataComponent = Browser.FindElement("h1").Parent();
Browser.WaitForElement("table>tbody>tr");
var table = Browser.FindElement(fetchDataComponent, "table", timeoutSeconds: 5);
Assert.Equal(5, table.FindElements(By.CssSelector("tbody tr")).Count);
}
}
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));
}
}
}

View File

@ -1,7 +1,8 @@
// 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 ProjectTemplates.Tests.Helpers;
using System.Threading.Tasks;
using Templates.Test.Helpers;
using Xunit;
using Xunit.Abstractions;
@ -11,23 +12,53 @@ namespace Templates.Test
{
public WebApiTemplateTest(ProjectFactoryFixture factoryFixture, ITestOutputHelper output)
{
Project = factoryFixture.CreateProject(output);
FactoryFixture = factoryFixture;
Output = output;
}
public Project Project { get; }
public ProjectFactoryFixture FactoryFixture { get; }
public ITestOutputHelper Output { get; }
public Project Project { get; set; }
[Fact]
public void WebApiTemplate()
public async Task WebApiTemplateAsync()
{
Project.RunDotNetNew("webapi");
Project = await FactoryFixture.GetOrCreateProject("webapi", Output);
foreach (var publish in new[] { false, true })
var createResult = await Project.RunDotNetNewAsync("webapi");
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/netcoreapp3.0/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));
using (var aspNetProcess = Project.StartBuiltProjectAsync())
{
using (var aspNetProcess = Project.StartAspNetProcess(publish))
{
aspNetProcess.AssertOk("/api/values");
aspNetProcess.AssertNotFound("/");
}
Assert.False(
aspNetProcess.Process.HasExited,
ErrorMessages.GetFailedProcessMessageOrEmpty("Run built project", Project, aspNetProcess.Process));
await aspNetProcess.AssertOk("/api/values");
await aspNetProcess.AssertNotFound("/");
}
using (var aspNetProcess = Project.StartPublishedProjectAsync())
{
Assert.False(
aspNetProcess.Process.HasExited,
ErrorMessages.GetFailedProcessMessageOrEmpty("Run published project", Project, aspNetProcess.Process));
await aspNetProcess.AssertOk("/api/values");
await aspNetProcess.AssertNotFound("/");
}
}
}

View File

@ -4,52 +4,25 @@
using System;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Remote;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Microsoft.AspNetCore.E2ETesting
{
public class BrowserFixture : IDisposable
{
private RemoteWebDriver _browser;
private RemoteLogs _logs;
public BrowserFixture(IMessageSink diagnosticsMessageSink)
{
DiagnosticsMessageSink = diagnosticsMessageSink;
if (!IsHostAutomationSupported())
{
DiagnosticsMessageSink.OnMessage(new DiagnosticMessage("Host does not support browser automation."));
return;
}
var opts = new ChromeOptions();
// Comment this out if you want to watch or interact with the browser (e.g., for debugging)
opts.AddArgument("--headless");
// Log errors
opts.SetLoggingPreference(LogType.Browser, LogLevel.All);
// On Windows/Linux, we don't need to set opts.BinaryLocation
// But for Travis Mac builds we do
var binaryLocation = Environment.GetEnvironmentVariable("TEST_CHROME_BINARY");
if (!string.IsNullOrEmpty(binaryLocation))
{
opts.BinaryLocation = binaryLocation;
DiagnosticsMessageSink.OnMessage(new DiagnosticMessage($"Set {nameof(ChromeOptions)}.{nameof(opts.BinaryLocation)} to {binaryLocation}"));
}
var driver = new RemoteWebDriver(SeleniumStandaloneServer.Instance.Uri, opts);
driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(1);
Browser = driver;
Logs = new RemoteLogs(driver);
}
public IWebDriver Browser { get; }
public ILogs Logs { get; }
public ILogs Logs { get; private set; }
public IMessageSink DiagnosticsMessageSink { get; }
@ -79,10 +52,81 @@ namespace Microsoft.AspNetCore.E2ETesting
public void Dispose()
{
if (Browser != null)
_browser?.Dispose();
}
public async Task<(IWebDriver, ILogs)> GetOrCreateBrowserAsync(ITestOutputHelper output)
{
if (!IsHostAutomationSupported())
{
Browser.Dispose();
output.WriteLine($"{nameof(BrowserFixture)}: Host does not support browser automation.");
return default;
}
if ((_browser, _logs) != (null, null))
{
return (_browser, _logs);
}
var opts = new ChromeOptions();
// Comment this out if you want to watch or interact with the browser (e.g., for debugging)
opts.AddArgument("--headless");
// Log errors
opts.SetLoggingPreference(LogType.Browser, LogLevel.All);
// On Windows/Linux, we don't need to set opts.BinaryLocation
// But for Travis Mac builds we do
var binaryLocation = Environment.GetEnvironmentVariable("TEST_CHROME_BINARY");
if (!string.IsNullOrEmpty(binaryLocation))
{
opts.BinaryLocation = binaryLocation;
output.WriteLine($"Set {nameof(ChromeOptions)}.{nameof(opts.BinaryLocation)} to {binaryLocation}");
}
var instance = await SeleniumStandaloneServer.GetInstanceAsync(output);
var attempt = 0;
var maxAttempts = 3;
do
{
try
{
// The driver opens the browser window and tries to connect to it on the constructor.
// Under heavy load, this can cause issues
// To prevent this we let the client attempt several times to connect to the server, increasing
// the max allowed timeout for a command on each attempt linearly.
// This can also be caused if many tests are running concurrently, we might want to manage
// chrome and chromedriver instances more aggresively if we have to.
// Additionally, if we think the selenium server has become irresponsive, we could spin up
// replace the current selenium server instance and let a new instance take over for the
// remaining tests.
var driver = new RemoteWebDriver(
instance.Uri,
opts.ToCapabilities(),
TimeSpan.FromSeconds(60).Add(TimeSpan.FromSeconds(attempt * 60)));
driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(1);
var logs = new RemoteLogs(driver);
_browser = driver;
_logs = logs;
return (_browser, _logs);
}
catch
{
if (attempt >= maxAttempts)
{
throw new InvalidOperationException("Couldn't create a Selenium remote driver client. The server is irresponsive");
}
}
attempt++;
} while (attempt < maxAttempts);
// We will never get here. Keeping the compiler happy.
throw new InvalidOperationException("Couldn't create a Selenium remote driver client. The server is unresponsive");
}
}
}

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading;
using System.Threading.Tasks;
using OpenQA.Selenium;
using Xunit;
using Xunit.Abstractions;
@ -9,23 +10,46 @@ using Xunit.Abstractions;
namespace Microsoft.AspNetCore.E2ETesting
{
[CaptureSeleniumLogs]
public class BrowserTestBase : IClassFixture<BrowserFixture>
public class BrowserTestBase : IClassFixture<BrowserFixture>, IAsyncLifetime
{
private static readonly AsyncLocal<IWebDriver> _browser = new AsyncLocal<IWebDriver>();
private static readonly AsyncLocal<IWebDriver> _asyncBrowser = new AsyncLocal<IWebDriver>();
private static readonly AsyncLocal<ILogs> _logs = new AsyncLocal<ILogs>();
private static readonly AsyncLocal<ITestOutputHelper> _output = new AsyncLocal<ITestOutputHelper>();
public static IWebDriver Browser => _browser.Value;
public BrowserTestBase(BrowserFixture browserFixture, ITestOutputHelper output)
{
BrowserFixture = browserFixture;
_output.Value = output;
}
public IWebDriver Browser { get; set; }
public static IWebDriver BrowserAccessor => _asyncBrowser.Value;
public static ILogs Logs => _logs.Value;
public static ITestOutputHelper Output => _output.Value;
public BrowserTestBase(BrowserFixture browserFixture, ITestOutputHelper output)
public BrowserFixture BrowserFixture { get; }
public Task DisposeAsync()
{
return Task.CompletedTask;
}
public virtual async Task InitializeAsync()
{
var (browser, logs) = await BrowserFixture.GetOrCreateBrowserAsync(Output);
_asyncBrowser.Value = browser;
_logs.Value = logs;
Browser = browser;
InitializeAsyncCore();
}
protected virtual void InitializeAsyncCore()
{
_browser.Value = browserFixture.Browser;
_logs.Value = browserFixture.Logs;
_output.Value = output;
}
}
}

View File

@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.E2ETesting
public override void After(MethodInfo methodUnderTest)
{
var browser = BrowserTestBase.Browser;
var browser = BrowserTestBase.BrowserAccessor;
var logs = BrowserTestBase.Logs;
var output = BrowserTestBase.Output;

View File

@ -2,6 +2,7 @@
<PropertyGroup>
<_DefaultProjectFilter>$(MSBuildProjectDirectory)\..\..</_DefaultProjectFilter>
<DefaultItemExcludes>$(DefaultItemExcludes);node_modules\**</DefaultItemExcludes>
<SeleniumProcessTrackingFolder Condition="'$(SeleniumProcessTrackingFolder)' == ''">$([MSBuild]::EnsureTrailingSlash('$(RepositoryRoot)'))obj\selenium\</SeleniumProcessTrackingFolder>
<SeleniumE2ETestsSupported Condition="'$(SeleniumE2ETestsSupported)' == '' and '$(TargetArchitecture)' != 'arm' and '$(OS)' == 'Windows_NT'">true</SeleniumE2ETestsSupported>
<EnforcePrerequisites Condition="'$(SeleniumE2ETestsSupported)' == 'true' and '$(EnforcePrerequisites)' == ''">true</EnforcePrerequisites>

View File

@ -102,4 +102,15 @@
</ItemGroup>
</Target>
<Target Name="_AddProcessTrackingMetadataAttribute" BeforeTargets="BeforeCompile">
<MakeDir Directories="$(SeleniumProcessTrackingFolder)" />
<ItemGroup>
<AssemblyAttribute
Include="System.Reflection.AssemblyMetadataAttribute">
<_Parameter1>Microsoft.AspNetCore.Testing.Selenium.ProcessTracking</_Parameter1>
<_Parameter2>$(SeleniumProcessTrackingFolder)</_Parameter2>
</AssemblyAttribute>
</ItemGroup>
</Target>
</Project>

View File

@ -2,31 +2,93 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Sockets;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Testing;
using Microsoft.AspNetCore.E2ETesting;
using Microsoft.Extensions.Internal;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Microsoft.AspNetCore.E2ETesting
{
class SeleniumStandaloneServer
public class SeleniumStandaloneServer : IDisposable
{
private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(30);
private static Lazy<SeleniumStandaloneServer> _instance = new Lazy<SeleniumStandaloneServer>(() => new SeleniumStandaloneServer());
private static SemaphoreSlim _semaphore = new SemaphoreSlim(1);
public Uri Uri { get; }
private Process _process;
private string _sentinelPath;
private Process _sentinelProcess;
private static IMessageSink _diagnosticsMessageSink;
public static SeleniumStandaloneServer Instance => _instance.Value;
// 1h 30 min
private static int SeleniumProcessTimeout = 5400;
private SeleniumStandaloneServer()
public SeleniumStandaloneServer(IMessageSink diagnosticsMessageSink)
{
if (Instance != null || _diagnosticsMessageSink != null)
{
throw new InvalidOperationException("Selenium standalone singleton already created.");
}
// The assembly level attribute AssemblyFixture takes care of this being being instantiated before tests run
// and disposed after tests are run, gracefully shutting down the server when possible by calling Dispose on
// the singleton.
Instance = this;
_diagnosticsMessageSink = diagnosticsMessageSink;
}
private void Initialize(
Uri uri,
Process process,
string sentinelPath,
Process sentinelProcess)
{
Uri = uri;
_process = process;
_sentinelPath = sentinelPath;
_sentinelProcess = sentinelProcess;
}
public Uri Uri { get; private set; }
internal static SeleniumStandaloneServer Instance { get; private set; }
public static async Task<SeleniumStandaloneServer> GetInstanceAsync(ITestOutputHelper output)
{
await _semaphore.WaitAsync();
try
{
if (Instance == null)
{
}
if (Instance._process == null)
{
// No process was started, meaning the instance wasn't initialized.
await InitializeInstance(output);
}
}
finally
{
_semaphore.Release();
}
return Instance;
}
private static async Task InitializeInstance(ITestOutputHelper output)
{
var port = FindAvailablePort();
Uri = new UriBuilder("http", "localhost", port, "/wd/hub").Uri;
var uri = new UriBuilder("http", "localhost", port, "/wd/hub").Uri;
var psi = new ProcessStartInfo
{
@ -42,9 +104,34 @@ namespace Microsoft.AspNetCore.E2ETesting
psi.Arguments = $"/c npm {psi.Arguments}";
}
var process = Process.Start(psi);
// It's important that we get the folder value before we start the process to prevent
// untracked processes when the tracking folder is not correctly configure.
var trackingFolder = GetProcessTrackingFolder();
if (!Directory.Exists(trackingFolder))
{
throw new InvalidOperationException($"Invalid tracking folder. Set the 'SeleniumProcessTrackingFolder' MSBuild property to a valid folder.");
}
Process process = null;
Process sentinel = null;
string pidFilePath = null;
try
{
process = Process.Start(psi);
pidFilePath = await WriteTrackingFileAsync(output, trackingFolder, process);
sentinel = StartSentinelProcess(process, pidFilePath, SeleniumProcessTimeout);
}
catch
{
ProcessCleanup(process, pidFilePath);
ProcessCleanup(sentinel, pidFilePath: null);
throw;
}
// Log output for selenium standalone process.
// This is for the case where the server fails to launch.
var logOutput = new BlockingCollection<string>();
var builder = new StringBuilder();
process.OutputDataReceived += LogOutput;
process.ErrorDataReceived += LogOutput;
@ -52,72 +139,120 @@ namespace Microsoft.AspNetCore.E2ETesting
process.BeginErrorReadLine();
// The Selenium sever has to be up for the entirety of the tests and is only shutdown when the application (i.e. the test) exits.
AppDomain.CurrentDomain.ProcessExit += (sender, e) =>
{
if (!process.HasExited)
{
process.KillTree(TimeSpan.FromSeconds(10));
process.Dispose();
}
};
AppDomain.CurrentDomain.ProcessExit += (sender, args) => ProcessCleanup(process, pidFilePath);
// Log
void LogOutput(object sender, DataReceivedEventArgs e)
{
lock (builder)
logOutput.TryAdd(e.Data);
// We avoid logging on the output here because it is unreliable. We can only log in the diagnostics sink.
lock (_diagnosticsMessageSink)
{
builder.AppendLine(e.Data);
_diagnosticsMessageSink.OnMessage(new DiagnosticMessage(e.Data));
}
}
var waitForStart = Task.Run(async () =>
var httpClient = new HttpClient
{
var httpClient = new HttpClient
Timeout = TimeSpan.FromSeconds(1),
};
var retries = 0;
do
{
await Task.Delay(1000);
try
{
Timeout = TimeSpan.FromSeconds(1),
};
var retries = 0;
while (retries++ < 30)
var response = await httpClient.GetAsync(uri);
if (response.StatusCode == HttpStatusCode.OK)
{
output = null;
Instance.Initialize(uri, process, pidFilePath, sentinel);
return;
}
}
catch (OperationCanceledException)
{
try
{
var responseTask = httpClient.GetAsync(Uri);
var response = await responseTask;
if (response.StatusCode == HttpStatusCode.OK)
{
return;
}
}
catch (OperationCanceledException)
{
}
await Task.Delay(1000);
}
throw new Exception("Failed to launch the server");
});
retries++;
} while (retries < 30);
// Make output null so that we stop logging to it.
output = null;
logOutput.CompleteAdding();
var exitCodeString = process.HasExited ? process.ExitCode.ToString() : "Process has not yet exited.";
var message = @$"Failed to launch the server.
ExitCode: {exitCodeString}
Captured output lines:
{string.Join(Environment.NewLine, logOutput.GetConsumingEnumerable())}.";
// If we got here, we couldn't launch Selenium or get it to respond. So shut it down.
ProcessCleanup(process, pidFilePath);
throw new InvalidOperationException(message);
}
private static Process StartSentinelProcess(Process process, string sentinelFile, int timeout)
{
// This sentinel process will start and will kill any roge selenium server that want' torn down
// via normal means.
var psi = new ProcessStartInfo
{
FileName = "powershell",
Arguments = $"-NoProfile -NonInteractive -Command \"Start-Sleep {timeout}; " +
$"if(Test-Path {sentinelFile}){{ " +
$"Write-Output 'Stopping process {process.Id}'; Stop-Process {process.Id}; }}" +
$"else{{ Write-Output 'Sentinel file {sentinelFile} not found.'}}",
};
return Process.Start(psi);
}
private static void ProcessCleanup(Process process, string pidFilePath)
{
if (process?.HasExited == false)
{
try
{
process?.KillTree(TimeSpan.FromSeconds(10));
process?.Dispose();
}
catch
{
}
}
try
{
// Wait in intervals instead of indefinitely to prevent thread starvation.
while (!waitForStart.TimeoutAfter(Timeout).Wait(1000))
if (pidFilePath != null && File.Exists(pidFilePath))
{
File.Delete(pidFilePath);
}
}
catch (Exception ex)
catch
{
string output;
lock (builder)
{
output = builder.ToString();
}
throw new InvalidOperationException($"Failed to start selenium sever. {System.Environment.NewLine}{output}", ex.GetBaseException());
}
}
private static async Task<string> WriteTrackingFileAsync(ITestOutputHelper output, string trackingFolder, Process process)
{
var pidFile = Path.Combine(trackingFolder, $"{process.Id}.{Guid.NewGuid()}.pid");
for (var i = 0; i < 3; i++)
{
try
{
await File.WriteAllTextAsync(pidFile, process.Id.ToString());
return pidFile;
}
catch
{
output.WriteLine($"Can't write file to process tracking folder: {trackingFolder}");
}
}
throw new InvalidOperationException($"Failed to write file for process {process.Id}");
}
static int FindAvailablePort()
{
var listener = new TcpListener(IPAddress.Loopback, 0);
@ -132,5 +267,16 @@ namespace Microsoft.AspNetCore.E2ETesting
listener.Stop();
}
}
private static string GetProcessTrackingFolder() =>
typeof(SeleniumStandaloneServer).Assembly
.GetCustomAttributes<AssemblyMetadataAttribute>()
.Single(a => a.Key == "Microsoft.AspNetCore.Testing.Selenium.ProcessTracking").Value;
public void Dispose()
{
ProcessCleanup(_process, _sentinelPath);
ProcessCleanup(_sentinelProcess, pidFilePath: null);
}
}
}

View File

@ -13,35 +13,35 @@ namespace Microsoft.AspNetCore.E2ETesting
{
// XUnit assertions, but hooked into Selenium's polling mechanism
public class WaitAssert
public static class WaitAssert
{
private readonly static TimeSpan DefaultTimeout = TimeSpan.FromSeconds(3);
public static void Equal<T>(T expected, Func<T> actual)
=> WaitAssertCore(() => Assert.Equal(expected, actual()));
public static void Equal<T>(this IWebDriver driver, T expected, Func<T> actual)
=> WaitAssertCore(driver, () => Assert.Equal(expected, actual()));
public static void True(Func<bool> actual)
=> WaitAssertCore(() => Assert.True(actual()));
public static void True(this IWebDriver driver, Func<bool> actual)
=> WaitAssertCore(driver, () => Assert.True(actual()));
public static void True(Func<bool> actual, TimeSpan timeout)
=> WaitAssertCore(() => Assert.True(actual()), timeout);
public static void True(this IWebDriver driver, Func<bool> actual, TimeSpan timeout)
=> WaitAssertCore(driver, () => Assert.True(actual()), timeout);
public static void False(Func<bool> actual)
=> WaitAssertCore(() => Assert.False(actual()));
public static void False(this IWebDriver driver, Func<bool> actual)
=> WaitAssertCore(driver, () => Assert.False(actual()));
public static void Contains(string expectedSubstring, Func<string> actualString)
=> WaitAssertCore(() => Assert.Contains(expectedSubstring, actualString()));
public static void Contains(this IWebDriver driver, string expectedSubstring, Func<string> actualString)
=> WaitAssertCore(driver, () => Assert.Contains(expectedSubstring, actualString()));
public static void Collection<T>(Func<IEnumerable<T>> actualValues, params Action<T>[] elementInspectors)
=> WaitAssertCore(() => Assert.Collection(actualValues(), elementInspectors));
public static void Collection<T>(this IWebDriver driver, Func<IEnumerable<T>> actualValues, params Action<T>[] elementInspectors)
=> WaitAssertCore(driver, () => Assert.Collection(actualValues(), elementInspectors));
public static void Empty(Func<IEnumerable> actualValues)
=> WaitAssertCore(() => Assert.Empty(actualValues()));
public static void Empty(this IWebDriver driver, Func<IEnumerable> actualValues)
=> WaitAssertCore(driver, () => Assert.Empty(actualValues()));
public static void Single(Func<IEnumerable> actualValues)
=> WaitAssertCore(() => Assert.Single(actualValues()));
public static void Single(this IWebDriver driver, Func<IEnumerable> actualValues)
=> WaitAssertCore(driver, () => Assert.Single(actualValues()));
private static void WaitAssertCore(Action assertion, TimeSpan timeout = default)
private static void WaitAssertCore(IWebDriver driver, Action assertion, TimeSpan timeout = default)
{
if (timeout == default)
{
@ -50,7 +50,7 @@ namespace Microsoft.AspNetCore.E2ETesting
try
{
new WebDriverWait(BrowserTestBase.Browser, timeout).Until(_ =>
new WebDriverWait(driver, timeout).Until(_ =>
{
try
{

View File

@ -31,15 +31,24 @@ namespace Microsoft.AspNetCore.E2ETesting
// Find all the AssemblyFixtureAttributes on the test assembly
Aggregator.Run(() =>
{
var fixturesAttributes = ((IReflectionAssemblyInfo)TestAssembly.Assembly).Assembly
.GetCustomAttributes(typeof(AssemblyFixtureAttribute), false)
.Cast<AssemblyFixtureAttribute>()
.ToList();
var fixturesAttributes = ((IReflectionAssemblyInfo)TestAssembly.Assembly)
.Assembly
.GetCustomAttributes(typeof(AssemblyFixtureAttribute), false)
.Cast<AssemblyFixtureAttribute>()
.ToList();
// Instantiate all the fixtures
foreach (var fixtureAttribute in fixturesAttributes)
{
_assemblyFixtureMappings[fixtureAttribute.FixtureType] = Activator.CreateInstance(fixtureAttribute.FixtureType);
var ctorWithDiagnostics = fixtureAttribute.FixtureType.GetConstructor(new[] { typeof(IMessageSink) });
if (ctorWithDiagnostics != null)
{
_assemblyFixtureMappings[fixtureAttribute.FixtureType] = Activator.CreateInstance(fixtureAttribute.FixtureType, DiagnosticMessageSink);
}
else
{
_assemblyFixtureMappings[fixtureAttribute.FixtureType] = Activator.CreateInstance(fixtureAttribute.FixtureType);
}
}
});
}
@ -55,10 +64,11 @@ namespace Microsoft.AspNetCore.E2ETesting
return base.BeforeTestAssemblyFinishedAsync();
}
protected override Task<RunSummary> RunTestCollectionAsync(IMessageBus messageBus,
ITestCollection testCollection,
IEnumerable<IXunitTestCase> testCases,
CancellationTokenSource cancellationTokenSource)
protected override Task<RunSummary> RunTestCollectionAsync(
IMessageBus messageBus,
ITestCollection testCollection,
IEnumerable<IXunitTestCase> testCases,
CancellationTokenSource cancellationTokenSource)
=> new XunitTestCollectionRunnerWithAssemblyFixture(
_assemblyFixtureMappings,
testCollection,