Skip to content

Run Blazor E2E tests on SauceLabs #18456

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jan 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions .azure/pipelines/blazor-daily-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Uses Scheduled Triggers, which aren't supported in YAML yet.
# https://docs.microsoft.com/en-us/azure/devops/pipelines/build/triggers?view=vsts&tabs=yaml#scheduled

# Daily Tests for Blazor
# These use Sauce Labs resources, hence they run daily rather than per-commit.

# We just need one Windows machine because all it does is trigger SauceLabs.
variables:
SAUCE_CONNECT_DOWNLOAD_ON_INSTALL: true
E2ETESTS_SauceTest: true
E2ETESTS_Sauce__TunnelIdentifier: 'blazor-e2e-sc-proxy-tunnel'
E2ETESTS_Sauce__HostName: 'sauce.local'
jobs:
- template: jobs/default-build.yml
parameters:
buildDirectory: src/Components
isTestingJob: true
agentOs: Windows
jobName: BlazorDailyTests
jobDisplayName: "Blazor Daily Tests"
afterBuild:

# macOS/Safari
- script: 'dotnet test --filter "StandaloneAppTest"'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy to start with this, but is there a limitation that requires this?

workingDirectory: 'src/Components/test/E2ETest'
displayName: 'Run Blazor tests - macOS/Safari'
condition: succeededOrFailed()
env:
# Secrets need to be explicitly mapped to env variables.
E2ETESTS_Sauce__Username: '$(asplab-sauce-labs-username)'
E2ETESTS_Sauce__AccessKey: '$(asplab-sauce-labs-access-key)'
# Set platform/browser configuration.
E2ETESTS_Sauce__TestName: 'Blazor Daily Tests - macOS/Safari'
E2ETESTS_Sauce__PlatformName: 'macOS 10.14'
E2ETESTS_Sauce__BrowserName: 'Safari'
# Need to explicitly set version here because some older versions don't support timeouts in Safari.
E2ETESTS_Sauce__SeleniumVersion: '3.4.0'

# Android/Chrome
- script: 'dotnet test --filter "StandaloneAppTest"'
workingDirectory: 'src/Components/test/E2ETest'
displayName: 'Run Blazor tests - Android/Chrome'
condition: succeededOrFailed()
env:
# Secrets need to be explicitly mapped to env variables.
E2ETESTS_Sauce__Username: '$(asplab-sauce-labs-username)'
E2ETESTS_Sauce__AccessKey: '$(asplab-sauce-labs-access-key)'
# Set platform/browser configuration.
E2ETESTS_Sauce__TestName: 'Blazor Daily Tests - Android/Chrome'
E2ETESTS_Sauce__PlatformName: 'Android'
E2ETESTS_Sauce__PlatformVersion: '10.0'
E2ETESTS_Sauce__BrowserName: 'Chrome'
E2ETESTS_Sauce__DeviceName: 'Android GoogleAPI Emulator'
E2ETESTS_Sauce__DeviceOrientation: 'portrait'
E2ETESTS_Sauce__AppiumVersion: '1.9.1'
artifacts:
- name: Windows_Logs
path: ../../artifacts/log/
publishOnError: true
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,7 @@ async Task GetDetails(int msg_id, int object_id, CancellationToken token, string
{
result = var_list
});
} catch (Exception e) {
} catch (Exception) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no beuno. We should fix this in Mono and flow it through. But given how small these changes are, I think we can live with this for now.

Debug ($"failed to parse {res.Value}");
}
SendResponse(msg_id, Result.Ok(o), token);
Expand Down Expand Up @@ -534,7 +534,7 @@ async Task GetScopeProperties (int msg_id, int scope_id, CancellationToken token
});
SendResponse (msg_id, Result.Ok (o), token);
}
catch (Exception exc) {
catch (Exception) {
SendResponse (msg_id, res, token);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width">
<!-- Forcing the device width here so that our automated tests work consistently on mobile browsers. -->
<meta name="viewport" content="width=1024">
<title>Blazor standalone</title>
<base href="/" />
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@

[assembly: TestFramework("Microsoft.AspNetCore.E2ETesting.XunitTestFrameworkWithAssemblyFixture", "Microsoft.AspNetCore.Components.E2ETests")]
[assembly: AssemblyFixture(typeof(SeleniumStandaloneServer))]
[assembly: AssemblyFixture(typeof(SauceConnectServer))]
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.IO;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.E2ETesting;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;

Expand Down Expand Up @@ -34,9 +35,15 @@ protected override IHost CreateWebHost()
var assembly = ApplicationAssembly ?? BuildWebHostMethod.Method.DeclaringType.Assembly;
var sampleSitePath = FindSampleOrTestSitePath(assembly.FullName);

var host = "127.0.0.1";
if (E2ETestOptions.Instance.SauceTest)
{
host = E2ETestOptions.Instance.Sauce.HostName;
}

return BuildWebHostMethod(new[]
{
"--urls", "http://127.0.0.1:0",
"--urls", $"http://{host}:0",
"--contentroot", sampleSitePath,
"--environment", Environment.ToString(),
}.Concat(AdditionalArguments).ToArray());
Expand Down
Original file line number Diff line number Diff line change
@@ -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 Microsoft.AspNetCore.E2ETesting;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Http.Features;
Expand All @@ -24,9 +25,15 @@ protected override IHost CreateWebHost()
ContentRoot = FindSampleOrTestSitePath(
typeof(TProgram).Assembly.FullName);

var host = "127.0.0.1";
if (E2ETestOptions.Instance.SauceTest)
{
host = E2ETestOptions.Instance.Sauce.HostName;
}

var args = new List<string>
{
"--urls", "http://127.0.0.1:0",
"--urls", $"http://{host}:0",
"--contentroot", ContentRoot,
"--pathbase", PathBase,
"--applicationpath", typeof(TProgram).Assembly.Location,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Threading;
using Microsoft.AspNetCore.E2ETesting;

namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures
{
Expand All @@ -22,7 +23,15 @@ public abstract class ServerFixture : IDisposable
public ServerFixture()
{
_rootUriInitializer = new Lazy<Uri>(() =>
new Uri(StartAndGetRootUri()));
{
var uri = new Uri(StartAndGetRootUri());
if (E2ETestOptions.Instance.SauceTest)
{
uri = new UriBuilder(uri.Scheme, E2ETestOptions.Instance.Sauce.HostName, uri.Port).Uri;
}

return uri;
});
}

public abstract void Dispose();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.IO;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.E2ETesting;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;

Expand All @@ -26,13 +27,19 @@ protected override IHost CreateWebHost()

var sampleSitePath = FindSampleOrTestSitePath(SampleSiteName);

var host = "127.0.0.1";
if (E2ETestOptions.Instance.SauceTest)
{
host = E2ETestOptions.Instance.Sauce.HostName;
}

return new HostBuilder()
.ConfigureWebHost(webHostBuilder => webHostBuilder
.UseKestrel()
.UseContentRoot(sampleSitePath)
.UseWebRoot(string.Empty)
.UseStartup<StaticSiteStartup>()
.UseUrls("http://127.0.0.1:0"))
.UseUrls($"http://{host}:0"))
.Build();
}

Expand Down
7 changes: 3 additions & 4 deletions src/Components/test/E2ETest/Tests/StandaloneAppTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
using OpenQA.Selenium.Support.UI;
using System;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
using Xunit.Abstractions;

Expand Down Expand Up @@ -52,21 +51,21 @@ public void NavMenuHighlightsCurrentLocation()
// Verify we start at home, with the home link highlighted
Assert.Equal("Hello, world!", Browser.FindElement(mainHeaderSelector).Text);
Assert.Collection(Browser.FindElements(activeNavLinksSelector),
item => Assert.Equal("Home", item.Text));
item => Assert.Equal("Home", item.Text.Trim()));

// Click on the "counter" link
Browser.FindElement(By.LinkText("Counter")).Click();

// Verify we're now on the counter page, with that nav link (only) highlighted
Assert.Equal("Counter", Browser.FindElement(mainHeaderSelector).Text);
Assert.Collection(Browser.FindElements(activeNavLinksSelector),
item => Assert.Equal("Counter", item.Text));
item => Assert.Equal("Counter", item.Text.Trim()));

// Verify we can navigate back to home too
Browser.FindElement(By.LinkText("Home")).Click();
Assert.Equal("Hello, world!", Browser.FindElement(mainHeaderSelector).Text);
Assert.Collection(Browser.FindElements(activeNavLinksSelector),
item => Assert.Equal("Home", item.Text));
item => Assert.Equal("Home", item.Text.Trim()));
}

[Fact]
Expand Down
2 changes: 1 addition & 1 deletion src/Components/test/E2ETest/e2eTestSettings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"DefaultWaitTimeoutInSeconds": 20,
"ScreenShotsPath": "../../screenshots"
"ScreenShotsPath": "../../screenshots",
}
9 changes: 8 additions & 1 deletion src/Components/test/E2ETest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,18 @@
"private": true,
"scripts": {
"selenium-standalone": "selenium-standalone",
"prepare": "selenium-standalone install"
"prepare": "selenium-standalone install",
"sauce": "ts-node ./scripts/sauce.ts"
},
"author": "",
"license": "Apache-2.0",
"dependencies": {
"sauce-connect-launcher": "^1.3.1",
"selenium-standalone": "^6.15.4"
},
"devDependencies": {
"@types/node": "^13.1.7",
"ts-node": "^8.6.2",
"typescript": "^3.7.5"
}
}
82 changes: 82 additions & 0 deletions src/Components/test/E2ETest/scripts/sauce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { EOL } from "os";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this a typescript file? There's nothing typescript specific here and we shouldn't need typescript for something as simple as this.

Can we change this to be a plain JavaScript file? Any recent version of node will handle modern javascript.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took the easy path as we do similar things in SignalR which is in typescript. IMO there is no downside to using typescript for this. It is nicer :)

import * as _fs from "fs";
import { promisify } from "util";

// Promisify things from fs we want to use.
const fs = {
createWriteStream: _fs.createWriteStream,
exists: promisify(_fs.exists),
mkdir: promisify(_fs.mkdir),
appendFile: promisify(_fs.appendFile),
readFile: promisify(_fs.readFile),
};

process.on("unhandledRejection", (reason) => {
console.error(`Unhandled promise rejection: ${reason}`);
process.exit(1);
});

let sauceUser = null;
let sauceKey = null;
let tunnelIdentifier = null;
let hostName = null;

for (let i = 0; i < process.argv.length; i += 1) {
switch (process.argv[i]) {
case "--sauce-user":
i += 1;
sauceUser = process.argv[i];
break;
case "--sauce-key":
i += 1;
sauceKey = process.argv[i];
break;
case "--sauce-tunnel":
i += 1;
tunnelIdentifier = process.argv[i];
break;
case "--use-hostname":
i += 1;
hostName = process.argv[i];
break;
}
}

const HOSTSFILE_PATH = process.platform === "win32" ? `${process.env.SystemRoot}\\System32\\drivers\\etc\\hosts` : null;

(async () => {

if (hostName) {
// Register a custom hostname in the hosts file (requires Admin, but AzDO agents run as Admin)
// Used to work around issues in Sauce Labs.
if (process.platform !== "win32") {
throw new Error("Can't use '--use-hostname' on non-Windows platform.");
}

try {

console.log(`Updating Hosts file (${HOSTSFILE_PATH}) to register host name '${hostName}'`);
await fs.appendFile(HOSTSFILE_PATH, `${EOL}127.0.0.1 ${hostName}${EOL}`);

} catch (error) {
console.log(`Unable to update hosts file at ${HOSTSFILE_PATH}. Error: ${error}`);
}
}


// Creates a persistent proxy tunnel using Sauce Connect.
var sauceConnectLauncher = require('sauce-connect-launcher');

sauceConnectLauncher({
username: sauceUser,
accessKey: sauceKey,
tunnelIdentifier: tunnelIdentifier,
}, function (err, sauceConnectProcess) {
if (err) {
console.error(err.message);
return;
}

console.log("Sauce Connect ready");
});
})();
Loading