Run Blazor E2E tests on SauceLabs (#18456)
* Run Blazor E2E tests on SauceLabs * Added azure pipeline * update yml * Update meta * More changes
This commit is contained in:
parent
e24f73e14b
commit
24be2992de
|
|
@ -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"'
|
||||
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
|
||||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -6,3 +6,4 @@ using Xunit;
|
|||
|
||||
[assembly: TestFramework("Microsoft.AspNetCore.E2ETesting.XunitTestFrameworkWithAssemblyFixture", "Microsoft.AspNetCore.Components.E2ETests")]
|
||||
[assembly: AssemblyFixture(typeof(SeleniumStandaloneServer))]
|
||||
[assembly: AssemblyFixture(typeof(SauceConnectServer))]
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using System.Collections.Generic;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.E2ETesting;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
|
|
@ -34,9 +35,15 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures
|
|||
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());
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -24,9 +25,15 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures
|
|||
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,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ using System.Linq;
|
|||
using System.Reflection;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Threading;
|
||||
using Microsoft.AspNetCore.E2ETesting;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures
|
||||
{
|
||||
|
|
@ -22,7 +23,15 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures
|
|||
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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
@ -26,13 +27,19 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures
|
|||
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ using OpenQA.Selenium;
|
|||
using OpenQA.Selenium.Support.UI;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
|
|
@ -52,7 +51,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
// 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();
|
||||
|
|
@ -60,13 +59,13 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
// 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]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"DefaultWaitTimeoutInSeconds": 20,
|
||||
"ScreenShotsPath": "../../screenshots"
|
||||
"ScreenShotsPath": "../../screenshots",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
import { EOL } from "os";
|
||||
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");
|
||||
});
|
||||
})();
|
||||
|
|
@ -2,6 +2,23 @@
|
|||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@types/node@^13.1.7":
|
||||
version "13.1.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-13.1.8.tgz#1d590429fe8187a02707720ecf38a6fe46ce294b"
|
||||
integrity sha512-6XzyyNM9EKQW4HKuzbo/CkOIjn/evtCmsU+MUM1xDfJ+3/rNjBttM1NgN7AOQvN6tP1Sl1D1PIKMreTArnxM9A==
|
||||
|
||||
adm-zip@~0.4.3:
|
||||
version "0.4.13"
|
||||
resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.13.tgz#597e2f8cc3672151e1307d3e95cddbc75672314a"
|
||||
integrity sha512-fERNJX8sOXfel6qCBCMPvZLzENBEhZTzKqg6vrOW5pvoEaQuJhRU4ndTAh6lHOxn1I6jnz2NHra56ZODM751uw==
|
||||
|
||||
agent-base@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee"
|
||||
integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==
|
||||
dependencies:
|
||||
es6-promisify "^5.0.0"
|
||||
|
||||
ajv@^6.5.5:
|
||||
version "6.10.2"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52"
|
||||
|
|
@ -12,6 +29,11 @@ ajv@^6.5.5:
|
|||
json-schema-traverse "^0.4.1"
|
||||
uri-js "^4.2.2"
|
||||
|
||||
arg@^4.1.0:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.2.tgz#e70c90579e02c63d80e3ad4e31d8bfdb8bd50064"
|
||||
integrity sha512-+ytCkGcBtHZ3V2r2Z06AncYO8jz46UEamcspGoU8lHcEbpn6J77QK0vdWvChsclg/tM5XIJC5tnjmPp7Eq6Obg==
|
||||
|
||||
asn1@~0.2.3:
|
||||
version "0.2.4"
|
||||
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
|
||||
|
|
@ -24,7 +46,7 @@ assert-plus@1.0.0, assert-plus@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
|
||||
integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=
|
||||
|
||||
async@^2.6.2:
|
||||
async@^2.1.2, async@^2.6.2:
|
||||
version "2.6.3"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
|
||||
integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==
|
||||
|
|
@ -46,6 +68,11 @@ aws4@^1.8.0:
|
|||
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
|
||||
integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==
|
||||
|
||||
balanced-match@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
|
||||
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
|
||||
|
||||
bcrypt-pbkdf@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
|
||||
|
|
@ -61,11 +88,24 @@ bl@^2.2.0:
|
|||
readable-stream "^2.3.5"
|
||||
safe-buffer "^5.1.1"
|
||||
|
||||
brace-expansion@^1.1.7:
|
||||
version "1.1.11"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
|
||||
integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
|
||||
dependencies:
|
||||
balanced-match "^1.0.0"
|
||||
concat-map "0.0.1"
|
||||
|
||||
buffer-crc32@~0.2.3:
|
||||
version "0.2.13"
|
||||
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
|
||||
integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=
|
||||
|
||||
buffer-from@^1.0.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
|
||||
integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
|
||||
|
||||
caseless@~0.12.0:
|
||||
version "0.12.0"
|
||||
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
|
||||
|
|
@ -83,6 +123,11 @@ commander@^2.19.0:
|
|||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422"
|
||||
integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==
|
||||
|
||||
concat-map@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
|
||||
|
||||
core-util-is@1.0.2, core-util-is@~1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
|
||||
|
|
@ -106,6 +151,13 @@ dashdash@^1.12.0:
|
|||
dependencies:
|
||||
assert-plus "^1.0.0"
|
||||
|
||||
debug@^3.1.0:
|
||||
version "3.2.6"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
|
||||
integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
|
||||
dependencies:
|
||||
ms "^2.1.1"
|
||||
|
||||
debug@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
|
||||
|
|
@ -118,6 +170,11 @@ delayed-stream@~1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||
integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
|
||||
|
||||
diff@^4.0.1:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
|
||||
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
|
||||
|
||||
ecc-jsbn@~0.1.1:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
|
||||
|
|
@ -133,6 +190,18 @@ end-of-stream@^1.4.1:
|
|||
dependencies:
|
||||
once "^1.4.0"
|
||||
|
||||
es6-promise@^4.0.3:
|
||||
version "4.2.8"
|
||||
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
|
||||
integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
|
||||
|
||||
es6-promisify@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203"
|
||||
integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=
|
||||
dependencies:
|
||||
es6-promise "^4.0.3"
|
||||
|
||||
extend@~3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
|
||||
|
|
@ -184,6 +253,11 @@ fs-constants@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
|
||||
integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
|
||||
|
||||
fs.realpath@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
||||
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
|
||||
|
||||
getpass@^0.1.1:
|
||||
version "0.1.7"
|
||||
resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
|
||||
|
|
@ -191,6 +265,18 @@ getpass@^0.1.1:
|
|||
dependencies:
|
||||
assert-plus "^1.0.0"
|
||||
|
||||
glob@^7.1.3:
|
||||
version "7.1.6"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
|
||||
integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
|
||||
dependencies:
|
||||
fs.realpath "^1.0.0"
|
||||
inflight "^1.0.4"
|
||||
inherits "2"
|
||||
minimatch "^3.0.4"
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
har-schema@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
|
||||
|
|
@ -213,7 +299,23 @@ http-signature@~1.2.0:
|
|||
jsprim "^1.2.2"
|
||||
sshpk "^1.7.0"
|
||||
|
||||
inherits@^2.0.3, inherits@~2.0.3:
|
||||
https-proxy-agent@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz#b8c286433e87602311b01c8ea34413d856a4af81"
|
||||
integrity sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==
|
||||
dependencies:
|
||||
agent-base "^4.3.0"
|
||||
debug "^3.1.0"
|
||||
|
||||
inflight@^1.0.4:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
|
||||
integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
|
||||
dependencies:
|
||||
once "^1.3.0"
|
||||
wrappy "1"
|
||||
|
||||
inherits@2, inherits@^2.0.3, inherits@~2.0.3:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||
|
|
@ -268,11 +370,21 @@ jsprim@^1.2.2:
|
|||
json-schema "0.2.3"
|
||||
verror "1.10.0"
|
||||
|
||||
lodash@^4.16.6:
|
||||
version "4.17.15"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
|
||||
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
|
||||
|
||||
lodash@^4.17.11, lodash@^4.17.14:
|
||||
version "4.17.14"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba"
|
||||
integrity sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==
|
||||
|
||||
make-error@^1.1.1:
|
||||
version "1.3.5"
|
||||
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8"
|
||||
integrity sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==
|
||||
|
||||
mime-db@1.40.0:
|
||||
version "1.40.0"
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32"
|
||||
|
|
@ -285,6 +397,13 @@ mime-types@^2.1.12, mime-types@~2.1.19:
|
|||
dependencies:
|
||||
mime-db "1.40.0"
|
||||
|
||||
minimatch@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
|
||||
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimist@0.0.8:
|
||||
version "0.0.8"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
|
||||
|
|
@ -317,13 +436,18 @@ oauth-sign@~0.9.0:
|
|||
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
|
||||
integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
|
||||
|
||||
once@^1.4.0:
|
||||
once@^1.3.0, once@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
||||
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
|
||||
dependencies:
|
||||
wrappy "1"
|
||||
|
||||
path-is-absolute@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
|
||||
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
|
||||
|
||||
path-key@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
|
||||
|
|
@ -417,6 +541,13 @@ request@2.88.0:
|
|||
tunnel-agent "^0.6.0"
|
||||
uuid "^3.3.2"
|
||||
|
||||
rimraf@^2.5.4:
|
||||
version "2.7.1"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
|
||||
integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
|
||||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519"
|
||||
|
|
@ -432,6 +563,17 @@ safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
|
|||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||
|
||||
sauce-connect-launcher@^1.3.1:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/sauce-connect-launcher/-/sauce-connect-launcher-1.3.1.tgz#31137f57b0f7176e1c0525b7fb09c6da746647cf"
|
||||
integrity sha512-vIf9qDol3q2FlYzrKt0dr3kvec6LSjX2WS+/mVnAJIhqh1evSkPKCR2AzcJrnSmx9Xt9PtV0tLY7jYh0wsQi8A==
|
||||
dependencies:
|
||||
adm-zip "~0.4.3"
|
||||
async "^2.1.2"
|
||||
https-proxy-agent "^3.0.0"
|
||||
lodash "^4.16.6"
|
||||
rimraf "^2.5.4"
|
||||
|
||||
selenium-standalone@^6.15.4:
|
||||
version "6.16.0"
|
||||
resolved "https://registry.yarnpkg.com/selenium-standalone/-/selenium-standalone-6.16.0.tgz#ffcf02665c58ff7a7472427ae819ba79c15967ac"
|
||||
|
|
@ -468,6 +610,19 @@ shebang-regex@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
|
||||
integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
|
||||
|
||||
source-map-support@^0.5.6:
|
||||
version "0.5.16"
|
||||
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042"
|
||||
integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==
|
||||
dependencies:
|
||||
buffer-from "^1.0.0"
|
||||
source-map "^0.6.0"
|
||||
|
||||
source-map@^0.6.0:
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
|
||||
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
|
||||
|
||||
sshpk@^1.7.0:
|
||||
version "1.16.1"
|
||||
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
|
||||
|
|
@ -516,6 +671,17 @@ tough-cookie@~2.4.3:
|
|||
psl "^1.1.24"
|
||||
punycode "^1.4.1"
|
||||
|
||||
ts-node@^8.6.2:
|
||||
version "8.6.2"
|
||||
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.6.2.tgz#7419a01391a818fbafa6f826a33c1a13e9464e35"
|
||||
integrity sha512-4mZEbofxGqLL2RImpe3zMJukvEvcO1XP8bj8ozBPySdCUXEcU5cIRwR0aM3R+VoZq7iXc8N86NC0FspGRqP4gg==
|
||||
dependencies:
|
||||
arg "^4.1.0"
|
||||
diff "^4.0.1"
|
||||
make-error "^1.1.1"
|
||||
source-map-support "^0.5.6"
|
||||
yn "3.1.1"
|
||||
|
||||
tunnel-agent@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
|
||||
|
|
@ -528,6 +694,11 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0:
|
|||
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
|
||||
integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
|
||||
|
||||
typescript@^3.7.5:
|
||||
version "3.7.5"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.5.tgz#0692e21f65fd4108b9330238aac11dd2e177a1ae"
|
||||
integrity sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==
|
||||
|
||||
uri-js@^4.2.2:
|
||||
version "4.2.2"
|
||||
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"
|
||||
|
|
@ -578,3 +749,8 @@ yauzl@^2.10.0:
|
|||
dependencies:
|
||||
buffer-crc32 "~0.2.3"
|
||||
fd-slicer "~1.1.0"
|
||||
|
||||
yn@3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
|
||||
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
|
||||
|
|
|
|||
|
|
@ -72,13 +72,24 @@ namespace Microsoft.AspNetCore.E2ETesting
|
|||
|
||||
public Task<(IWebDriver, ILogs)> GetOrCreateBrowserAsync(ITestOutputHelper output, string isolationContext = "")
|
||||
{
|
||||
if (!IsHostAutomationSupported())
|
||||
Func<string, ITestOutputHelper, Task<(IWebDriver, ILogs)>> createBrowserFunc;
|
||||
if (E2ETestOptions.Instance.SauceTest)
|
||||
{
|
||||
output.WriteLine($"{nameof(BrowserFixture)}: Host does not support browser automation.");
|
||||
return Task.FromResult<(IWebDriver, ILogs)>(default);
|
||||
createBrowserFunc = CreateSauceBrowserAsync;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!IsHostAutomationSupported())
|
||||
{
|
||||
output.WriteLine($"{nameof(BrowserFixture)}: Host does not support browser automation.");
|
||||
return Task.FromResult<(IWebDriver, ILogs)>(default);
|
||||
}
|
||||
|
||||
createBrowserFunc = CreateBrowserAsync;
|
||||
}
|
||||
|
||||
return _browsers.GetOrAdd(isolationContext, CreateBrowserAsync, output);
|
||||
|
||||
return _browsers.GetOrAdd(isolationContext, createBrowserFunc, output);
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => Task.CompletedTask;
|
||||
|
|
@ -143,5 +154,106 @@ namespace Microsoft.AspNetCore.E2ETesting
|
|||
|
||||
throw new InvalidOperationException("Couldn't create a Selenium remote driver client. The server is irresponsive");
|
||||
}
|
||||
|
||||
private async Task<(IWebDriver browser, ILogs log)> CreateSauceBrowserAsync(string context, ITestOutputHelper output)
|
||||
{
|
||||
var sauce = E2ETestOptions.Instance.Sauce;
|
||||
|
||||
if (sauce == null ||
|
||||
string.IsNullOrEmpty(sauce.TestName) ||
|
||||
string.IsNullOrEmpty(sauce.Username) ||
|
||||
string.IsNullOrEmpty(sauce.AccessKey) ||
|
||||
string.IsNullOrEmpty(sauce.TunnelIdentifier) ||
|
||||
string.IsNullOrEmpty(sauce.PlatformName) ||
|
||||
string.IsNullOrEmpty(sauce.BrowserName))
|
||||
{
|
||||
throw new InvalidOperationException("Required SauceLabs environment variables not set.");
|
||||
}
|
||||
|
||||
var name = sauce.TestName;
|
||||
if (!string.IsNullOrEmpty(context))
|
||||
{
|
||||
name = $"{name} - {context}";
|
||||
}
|
||||
|
||||
var capabilities = new DesiredCapabilities();
|
||||
|
||||
// Required config
|
||||
capabilities.SetCapability("username", sauce.Username);
|
||||
capabilities.SetCapability("accessKey", sauce.AccessKey);
|
||||
capabilities.SetCapability("tunnelIdentifier", sauce.TunnelIdentifier);
|
||||
capabilities.SetCapability("name", name);
|
||||
|
||||
if (!string.IsNullOrEmpty(sauce.BrowserName))
|
||||
{
|
||||
capabilities.SetCapability("browserName", sauce.BrowserName);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(sauce.PlatformVersion))
|
||||
{
|
||||
capabilities.SetCapability("platformName", sauce.PlatformName);
|
||||
capabilities.SetCapability("platformVersion", sauce.PlatformVersion);
|
||||
}
|
||||
else
|
||||
{
|
||||
// In some cases (like macOS), SauceLabs expects us to set "platform" instead of "platformName".
|
||||
capabilities.SetCapability("platform", sauce.PlatformName);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(sauce.BrowserVersion))
|
||||
{
|
||||
capabilities.SetCapability("browserVersion", sauce.BrowserVersion);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(sauce.DeviceName))
|
||||
{
|
||||
capabilities.SetCapability("deviceName", sauce.DeviceName);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(sauce.DeviceOrientation))
|
||||
{
|
||||
capabilities.SetCapability("deviceOrientation", sauce.DeviceOrientation);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(sauce.AppiumVersion))
|
||||
{
|
||||
capabilities.SetCapability("appiumVersion", sauce.AppiumVersion);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(sauce.SeleniumVersion))
|
||||
{
|
||||
capabilities.SetCapability("seleniumVersion", sauce.SeleniumVersion);
|
||||
}
|
||||
|
||||
await SauceConnectServer.StartAsync(output);
|
||||
|
||||
var attempt = 0;
|
||||
const int maxAttempts = 3;
|
||||
do
|
||||
{
|
||||
try
|
||||
{
|
||||
// Attempt to create a new browser in SauceLabs.
|
||||
var driver = new RemoteWebDriver(
|
||||
new Uri("http://localhost:4445/wd/hub"),
|
||||
capabilities,
|
||||
TimeSpan.FromSeconds(60).Add(TimeSpan.FromSeconds(attempt * 60)));
|
||||
|
||||
driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(1);
|
||||
var logs = new RemoteLogs(driver);
|
||||
|
||||
return (driver, logs);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
output.WriteLine($"Error initializing RemoteWebDriver: {ex.Message}");
|
||||
}
|
||||
|
||||
attempt++;
|
||||
|
||||
} while (attempt < maxAttempts);
|
||||
|
||||
throw new InvalidOperationException("Couldn't create a SauceLabs remote driver client.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ namespace Microsoft.AspNetCore.E2ETesting
|
|||
}
|
||||
|
||||
Configuration = builder
|
||||
.AddEnvironmentVariables("E2ETESTS")
|
||||
.AddEnvironmentVariables("E2ETESTS_")
|
||||
.Build();
|
||||
|
||||
var instance = new E2ETestOptions();
|
||||
|
|
@ -56,5 +56,9 @@ namespace Microsoft.AspNetCore.E2ETesting
|
|||
public string ScreenShotsPath { get; set; }
|
||||
|
||||
public double DefaultAfterFailureWaitTimeoutInSeconds { get; set; } = 3;
|
||||
|
||||
public bool SauceTest { get; set; }
|
||||
|
||||
public SauceOptions Sauce { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
<SeleniumScreenShotsFolderPath>$([MSBuild]::NormalizeDirectory('$(ArtifactsTestResultsDir)','$(MSBuildProjectName)'))</SeleniumScreenShotsFolderPath>
|
||||
<SeleniumProcessTrackingFolder Condition="'$(SeleniumProcessTrackingFolder)' == ''">$([MSBuild]::EnsureTrailingSlash('$(RepoRoot)'))artifacts\tmp\selenium\</SeleniumProcessTrackingFolder>
|
||||
<SeleniumE2ETestsSupported Condition="'$(SeleniumE2ETestsSupported)' == '' and '$(TargetArchitecture)' != 'arm' and '$(OS)' == 'Windows_NT'">true</SeleniumE2ETestsSupported>
|
||||
<SauceConnectProcessTrackingFolder Condition="'$(SauceConnectProcessTrackingFolder)' == ''">$([MSBuild]::EnsureTrailingSlash('$(RepoRoot)'))artifacts\tmp\sauceconnect\</SauceConnectProcessTrackingFolder>
|
||||
|
||||
<!-- We want to enforce prerequisites when we build from the CI or within Visual Studio -->
|
||||
<EnforcedE2EBuildEnvironment Condition="'$(ContinuousIntegrationBuild)' == 'true' or '$(BuildingInsideVisualStudio)' == 'true'">true</EnforcedE2EBuildEnvironment>
|
||||
|
|
|
|||
|
|
@ -121,6 +121,14 @@
|
|||
<_Parameter2>$(SeleniumProcessTrackingFolder)</_Parameter2>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
<MakeDir Directories="$(SauceConnectProcessTrackingFolder)" />
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute
|
||||
Include="System.Reflection.AssemblyMetadataAttribute">
|
||||
<_Parameter1>Microsoft.AspNetCore.Testing.SauceConnect.ProcessTracking</_Parameter1>
|
||||
<_Parameter2>$(SauceConnectProcessTrackingFolder)</_Parameter2>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
<Target Name="_EnsureSeleniumScreenShotsFolder" BeforeTargets="Build">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,262 @@
|
|||
// 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.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.E2ETesting;
|
||||
using Microsoft.Extensions.Internal;
|
||||
using Xunit.Abstractions;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace Microsoft.AspNetCore.E2ETesting
|
||||
{
|
||||
public class SauceConnectServer : IDisposable
|
||||
{
|
||||
private static SemaphoreSlim _semaphore = new SemaphoreSlim(1);
|
||||
|
||||
private Process _process;
|
||||
private string _sentinelPath;
|
||||
private Process _sentinelProcess;
|
||||
private static IMessageSink _diagnosticsMessageSink;
|
||||
|
||||
// 2h
|
||||
private static int SauceConnectProcessTimeout = 7200;
|
||||
|
||||
public SauceConnectServer(IMessageSink diagnosticsMessageSink)
|
||||
{
|
||||
if (Instance != null || _diagnosticsMessageSink != null)
|
||||
{
|
||||
throw new InvalidOperationException("Sauce connect 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(
|
||||
Process process,
|
||||
string sentinelPath,
|
||||
Process sentinelProcess)
|
||||
{
|
||||
_process = process;
|
||||
_sentinelPath = sentinelPath;
|
||||
_sentinelProcess = sentinelProcess;
|
||||
}
|
||||
|
||||
internal static SauceConnectServer Instance { get; private set; }
|
||||
|
||||
public static async Task StartAsync(ITestOutputHelper output)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _semaphore.WaitAsync();
|
||||
if (Instance._process == null)
|
||||
{
|
||||
// No process was started, meaning the instance wasn't initialized.
|
||||
await InitializeInstance(output);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task InitializeInstance(ITestOutputHelper output)
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "npm",
|
||||
Arguments = "run sauce --" +
|
||||
$" --sauce-user {E2ETestOptions.Instance.Sauce.Username}" +
|
||||
$" --sauce-key {E2ETestOptions.Instance.Sauce.AccessKey}" +
|
||||
$" --sauce-tunnel {E2ETestOptions.Instance.Sauce.TunnelIdentifier}" +
|
||||
$" --use-hostname {E2ETestOptions.Instance.Sauce.HostName}",
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
};
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
psi.FileName = "cmd";
|
||||
psi.Arguments = $"/c npm {psi.Arguments}";
|
||||
}
|
||||
|
||||
// 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 'SauceConnectProcessTrackingFolder' 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, SauceConnectProcessTimeout);
|
||||
}
|
||||
catch
|
||||
{
|
||||
ProcessCleanup(process, pidFilePath);
|
||||
ProcessCleanup(sentinel, pidFilePath: null);
|
||||
throw;
|
||||
}
|
||||
|
||||
// Log output for sauce connect process.
|
||||
// This is for the case where the server fails to launch.
|
||||
var logOutput = new BlockingCollection<string>();
|
||||
|
||||
process.OutputDataReceived += LogOutput;
|
||||
process.ErrorDataReceived += LogOutput;
|
||||
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
// The Sauce connect server 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, args) => ProcessCleanup(process, pidFilePath);
|
||||
|
||||
// Log
|
||||
void LogOutput(object sender, DataReceivedEventArgs e)
|
||||
{
|
||||
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)
|
||||
{
|
||||
_diagnosticsMessageSink.OnMessage(new DiagnosticMessage(e.Data));
|
||||
}
|
||||
}
|
||||
|
||||
var uri = new UriBuilder("http", E2ETestOptions.Instance.Sauce.HostName, 4445).Uri;
|
||||
var httpClient = new HttpClient
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(1),
|
||||
};
|
||||
|
||||
var retries = 0;
|
||||
do
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
try
|
||||
{
|
||||
var response = await httpClient.GetAsync(uri);
|
||||
if (response.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
output = null;
|
||||
Instance.Initialize(process, pidFilePath, sentinel);
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
}
|
||||
|
||||
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 Sauce connect 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 rouge sauce connect server that wasn't 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)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (process?.HasExited == false)
|
||||
{
|
||||
try
|
||||
{
|
||||
process?.KillTree(TimeSpan.FromSeconds(10));
|
||||
process?.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore errors here since we can't do anything
|
||||
}
|
||||
}
|
||||
if (pidFilePath != null && File.Exists(pidFilePath))
|
||||
{
|
||||
File.Delete(pidFilePath);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore errors here since we can't do anything
|
||||
}
|
||||
}
|
||||
|
||||
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}");
|
||||
}
|
||||
|
||||
private static string GetProcessTrackingFolder() =>
|
||||
typeof(SauceConnectServer).Assembly
|
||||
.GetCustomAttributes<AssemblyMetadataAttribute>()
|
||||
.Single(a => a.Key == "Microsoft.AspNetCore.Testing.SauceConnect.ProcessTracking").Value;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
ProcessCleanup(_process, _sentinelPath);
|
||||
ProcessCleanup(_sentinelProcess, pidFilePath: null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
// 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 Microsoft.AspNetCore.E2ETesting
|
||||
{
|
||||
public class SauceOptions
|
||||
{
|
||||
public string Username { get; set; }
|
||||
|
||||
public string AccessKey { get; set; }
|
||||
|
||||
public string TunnelIdentifier { get; set; }
|
||||
|
||||
public string HostName { get; set; }
|
||||
|
||||
public string TestName { get; set; }
|
||||
|
||||
public bool IsRealDevice { get; set; }
|
||||
|
||||
public string PlatformName { get; set; }
|
||||
|
||||
public string PlatformVersion { get; set; }
|
||||
|
||||
public string BrowserName { get; set; }
|
||||
|
||||
public string BrowserVersion { get; set; }
|
||||
|
||||
public string DeviceName { get; set; }
|
||||
|
||||
public string DeviceOrientation { get; set; }
|
||||
|
||||
public string AppiumVersion { get; set; }
|
||||
|
||||
public string SeleniumVersion { get; set; }
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue