Skip to content

Commit 24be299

Browse files
authored
Run Blazor E2E tests on SauceLabs (#18456)
* Run Blazor E2E tests on SauceLabs * Added azure pipeline * update yml * Update meta * More changes
1 parent e24f73e commit 24be299

File tree

18 files changed

+797
-19
lines changed

18 files changed

+797
-19
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Uses Scheduled Triggers, which aren't supported in YAML yet.
2+
# https://docs.microsoft.com/en-us/azure/devops/pipelines/build/triggers?view=vsts&tabs=yaml#scheduled
3+
4+
# Daily Tests for Blazor
5+
# These use Sauce Labs resources, hence they run daily rather than per-commit.
6+
7+
# We just need one Windows machine because all it does is trigger SauceLabs.
8+
variables:
9+
SAUCE_CONNECT_DOWNLOAD_ON_INSTALL: true
10+
E2ETESTS_SauceTest: true
11+
E2ETESTS_Sauce__TunnelIdentifier: 'blazor-e2e-sc-proxy-tunnel'
12+
E2ETESTS_Sauce__HostName: 'sauce.local'
13+
jobs:
14+
- template: jobs/default-build.yml
15+
parameters:
16+
buildDirectory: src/Components
17+
isTestingJob: true
18+
agentOs: Windows
19+
jobName: BlazorDailyTests
20+
jobDisplayName: "Blazor Daily Tests"
21+
afterBuild:
22+
23+
# macOS/Safari
24+
- script: 'dotnet test --filter "StandaloneAppTest"'
25+
workingDirectory: 'src/Components/test/E2ETest'
26+
displayName: 'Run Blazor tests - macOS/Safari'
27+
condition: succeededOrFailed()
28+
env:
29+
# Secrets need to be explicitly mapped to env variables.
30+
E2ETESTS_Sauce__Username: '$(asplab-sauce-labs-username)'
31+
E2ETESTS_Sauce__AccessKey: '$(asplab-sauce-labs-access-key)'
32+
# Set platform/browser configuration.
33+
E2ETESTS_Sauce__TestName: 'Blazor Daily Tests - macOS/Safari'
34+
E2ETESTS_Sauce__PlatformName: 'macOS 10.14'
35+
E2ETESTS_Sauce__BrowserName: 'Safari'
36+
# Need to explicitly set version here because some older versions don't support timeouts in Safari.
37+
E2ETESTS_Sauce__SeleniumVersion: '3.4.0'
38+
39+
# Android/Chrome
40+
- script: 'dotnet test --filter "StandaloneAppTest"'
41+
workingDirectory: 'src/Components/test/E2ETest'
42+
displayName: 'Run Blazor tests - Android/Chrome'
43+
condition: succeededOrFailed()
44+
env:
45+
# Secrets need to be explicitly mapped to env variables.
46+
E2ETESTS_Sauce__Username: '$(asplab-sauce-labs-username)'
47+
E2ETESTS_Sauce__AccessKey: '$(asplab-sauce-labs-access-key)'
48+
# Set platform/browser configuration.
49+
E2ETESTS_Sauce__TestName: 'Blazor Daily Tests - Android/Chrome'
50+
E2ETESTS_Sauce__PlatformName: 'Android'
51+
E2ETESTS_Sauce__PlatformVersion: '10.0'
52+
E2ETESTS_Sauce__BrowserName: 'Chrome'
53+
E2ETESTS_Sauce__DeviceName: 'Android GoogleAPI Emulator'
54+
E2ETESTS_Sauce__DeviceOrientation: 'portrait'
55+
E2ETESTS_Sauce__AppiumVersion: '1.9.1'
56+
artifacts:
57+
- name: Windows_Logs
58+
path: ../../artifacts/log/
59+
publishOnError: true

src/Components/Blazor/testassets/StandaloneApp/wwwroot/index.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
<html>
33
<head>
44
<meta charset="utf-8" />
5-
<meta name="viewport" content="width=device-width">
5+
<!-- Forcing the device width here so that our automated tests work consistently on mobile browsers. -->
6+
<meta name="viewport" content="width=1024">
67
<title>Blazor standalone</title>
78
<base href="/" />
89
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />

src/Components/test/E2ETest/Infrastructure/AssemblyInfo.AssemblyFixtures.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@
66

77
[assembly: TestFramework("Microsoft.AspNetCore.E2ETesting.XunitTestFrameworkWithAssemblyFixture", "Microsoft.AspNetCore.Components.E2ETests")]
88
[assembly: AssemblyFixture(typeof(SeleniumStandaloneServer))]
9+
[assembly: AssemblyFixture(typeof(SauceConnectServer))]

src/Components/test/E2ETest/Infrastructure/ServerFixtures/AspNetSiteServerFixture.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.IO;
77
using System.Linq;
88
using System.Reflection;
9+
using Microsoft.AspNetCore.E2ETesting;
910
using Microsoft.AspNetCore.Hosting;
1011
using Microsoft.Extensions.Hosting;
1112

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

38+
var host = "127.0.0.1";
39+
if (E2ETestOptions.Instance.SauceTest)
40+
{
41+
host = E2ETestOptions.Instance.Sauce.HostName;
42+
}
43+
3744
return BuildWebHostMethod(new[]
3845
{
39-
"--urls", "http://127.0.0.1:0",
46+
"--urls", $"http://{host}:0",
4047
"--contentroot", sampleSitePath,
4148
"--environment", Environment.ToString(),
4249
}.Concat(AdditionalArguments).ToArray());

src/Components/test/E2ETest/Infrastructure/ServerFixtures/DevHostServerFixture.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
using Microsoft.AspNetCore.E2ETesting;
45
using Microsoft.AspNetCore.Hosting;
56
using Microsoft.AspNetCore.Hosting.Server;
67
using Microsoft.AspNetCore.Http.Features;
@@ -24,9 +25,15 @@ protected override IHost CreateWebHost()
2425
ContentRoot = FindSampleOrTestSitePath(
2526
typeof(TProgram).Assembly.FullName);
2627

28+
var host = "127.0.0.1";
29+
if (E2ETestOptions.Instance.SauceTest)
30+
{
31+
host = E2ETestOptions.Instance.Sauce.HostName;
32+
}
33+
2734
var args = new List<string>
2835
{
29-
"--urls", "http://127.0.0.1:0",
36+
"--urls", $"http://{host}:0",
3037
"--contentroot", ContentRoot,
3138
"--pathbase", PathBase,
3239
"--applicationpath", typeof(TProgram).Assembly.Location,

src/Components/test/E2ETest/Infrastructure/ServerFixtures/ServerFixture.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Reflection;
99
using System.Runtime.ExceptionServices;
1010
using System.Threading;
11+
using Microsoft.AspNetCore.E2ETesting;
1112

1213
namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures
1314
{
@@ -22,7 +23,15 @@ public abstract class ServerFixture : IDisposable
2223
public ServerFixture()
2324
{
2425
_rootUriInitializer = new Lazy<Uri>(() =>
25-
new Uri(StartAndGetRootUri()));
26+
{
27+
var uri = new Uri(StartAndGetRootUri());
28+
if (E2ETestOptions.Instance.SauceTest)
29+
{
30+
uri = new UriBuilder(uri.Scheme, E2ETestOptions.Instance.Sauce.HostName, uri.Port).Uri;
31+
}
32+
33+
return uri;
34+
});
2635
}
2736

2837
public abstract void Dispose();

src/Components/test/E2ETest/Infrastructure/ServerFixtures/StaticSiteServerFixture.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.IO;
66
using Microsoft.AspNetCore.Builder;
7+
using Microsoft.AspNetCore.E2ETesting;
78
using Microsoft.AspNetCore.Hosting;
89
using Microsoft.Extensions.Hosting;
910

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

2728
var sampleSitePath = FindSampleOrTestSitePath(SampleSiteName);
2829

30+
var host = "127.0.0.1";
31+
if (E2ETestOptions.Instance.SauceTest)
32+
{
33+
host = E2ETestOptions.Instance.Sauce.HostName;
34+
}
35+
2936
return new HostBuilder()
3037
.ConfigureWebHost(webHostBuilder => webHostBuilder
3138
.UseKestrel()
3239
.UseContentRoot(sampleSitePath)
3340
.UseWebRoot(string.Empty)
3441
.UseStartup<StaticSiteStartup>()
35-
.UseUrls("http://127.0.0.1:0"))
42+
.UseUrls($"http://{host}:0"))
3643
.Build();
3744
}
3845

src/Components/test/E2ETest/Tests/StandaloneAppTest.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
using OpenQA.Selenium.Support.UI;
99
using System;
1010
using System.Linq;
11-
using System.Threading.Tasks;
1211
using Xunit;
1312
using Xunit.Abstractions;
1413

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

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

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

6564
// Verify we can navigate back to home too
6665
Browser.FindElement(By.LinkText("Home")).Click();
6766
Assert.Equal("Hello, world!", Browser.FindElement(mainHeaderSelector).Text);
6867
Assert.Collection(Browser.FindElements(activeNavLinksSelector),
69-
item => Assert.Equal("Home", item.Text));
68+
item => Assert.Equal("Home", item.Text.Trim()));
7069
}
7170

7271
[Fact]
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
22
"DefaultWaitTimeoutInSeconds": 20,
3-
"ScreenShotsPath": "../../screenshots"
3+
"ScreenShotsPath": "../../screenshots",
44
}

src/Components/test/E2ETest/package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,18 @@
66
"private": true,
77
"scripts": {
88
"selenium-standalone": "selenium-standalone",
9-
"prepare": "selenium-standalone install"
9+
"prepare": "selenium-standalone install",
10+
"sauce": "ts-node ./scripts/sauce.ts"
1011
},
1112
"author": "",
1213
"license": "Apache-2.0",
1314
"dependencies": {
15+
"sauce-connect-launcher": "^1.3.1",
1416
"selenium-standalone": "^6.15.4"
17+
},
18+
"devDependencies": {
19+
"@types/node": "^13.1.7",
20+
"ts-node": "^8.6.2",
21+
"typescript": "^3.7.5"
1522
}
1623
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { EOL } from "os";
2+
import * as _fs from "fs";
3+
import { promisify } from "util";
4+
5+
// Promisify things from fs we want to use.
6+
const fs = {
7+
createWriteStream: _fs.createWriteStream,
8+
exists: promisify(_fs.exists),
9+
mkdir: promisify(_fs.mkdir),
10+
appendFile: promisify(_fs.appendFile),
11+
readFile: promisify(_fs.readFile),
12+
};
13+
14+
process.on("unhandledRejection", (reason) => {
15+
console.error(`Unhandled promise rejection: ${reason}`);
16+
process.exit(1);
17+
});
18+
19+
let sauceUser = null;
20+
let sauceKey = null;
21+
let tunnelIdentifier = null;
22+
let hostName = null;
23+
24+
for (let i = 0; i < process.argv.length; i += 1) {
25+
switch (process.argv[i]) {
26+
case "--sauce-user":
27+
i += 1;
28+
sauceUser = process.argv[i];
29+
break;
30+
case "--sauce-key":
31+
i += 1;
32+
sauceKey = process.argv[i];
33+
break;
34+
case "--sauce-tunnel":
35+
i += 1;
36+
tunnelIdentifier = process.argv[i];
37+
break;
38+
case "--use-hostname":
39+
i += 1;
40+
hostName = process.argv[i];
41+
break;
42+
}
43+
}
44+
45+
const HOSTSFILE_PATH = process.platform === "win32" ? `${process.env.SystemRoot}\\System32\\drivers\\etc\\hosts` : null;
46+
47+
(async () => {
48+
49+
if (hostName) {
50+
// Register a custom hostname in the hosts file (requires Admin, but AzDO agents run as Admin)
51+
// Used to work around issues in Sauce Labs.
52+
if (process.platform !== "win32") {
53+
throw new Error("Can't use '--use-hostname' on non-Windows platform.");
54+
}
55+
56+
try {
57+
58+
console.log(`Updating Hosts file (${HOSTSFILE_PATH}) to register host name '${hostName}'`);
59+
await fs.appendFile(HOSTSFILE_PATH, `${EOL}127.0.0.1 ${hostName}${EOL}`);
60+
61+
} catch (error) {
62+
console.log(`Unable to update hosts file at ${HOSTSFILE_PATH}. Error: ${error}`);
63+
}
64+
}
65+
66+
67+
// Creates a persistent proxy tunnel using Sauce Connect.
68+
var sauceConnectLauncher = require('sauce-connect-launcher');
69+
70+
sauceConnectLauncher({
71+
username: sauceUser,
72+
accessKey: sauceKey,
73+
tunnelIdentifier: tunnelIdentifier,
74+
}, function (err, sauceConnectProcess) {
75+
if (err) {
76+
console.error(err.message);
77+
return;
78+
}
79+
80+
console.log("Sauce Connect ready");
81+
});
82+
})();

0 commit comments

Comments
 (0)