Skip to content

Commit 994abd7

Browse files
authored
[INT-324] Allow to pass WebDriver to the VisualCheck method (#244)
1 parent 80fc2ae commit 994abd7

File tree

9 files changed

+261
-30
lines changed

9 files changed

+261
-30
lines changed

visual-dotnet/SauceLabs.Visual.IntegrationTests/IntegrationTestAttribute.cs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,26 @@
55

66
namespace SauceLabs.Visual.IntegrationTests;
77

8-
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
8+
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false)]
99
public class IntegrationTestAttribute : NUnitAttribute, IApplyToTest
1010
{
11+
/// <summary>
12+
/// In some cases, such as passing custom ID when the build does not exist, we only can run a single test class.
13+
/// This is a limitation of the `VisualClient` SDK, which closes a build with or without a custom ID
14+
/// if it was created by the SDK.
15+
/// </summary>
16+
public bool SkipIfSingle { get; }
17+
18+
public IntegrationTestAttribute()
19+
{
20+
SkipIfSingle = false;
21+
}
22+
23+
public IntegrationTestAttribute(bool skipIfSingle)
24+
{
25+
SkipIfSingle = skipIfSingle;
26+
}
27+
1128
public void ApplyToTest(Test test)
1229
{
1330
if (test.RunState == RunState.NotRunnable)
@@ -20,5 +37,11 @@ public void ApplyToTest(Test test)
2037
test.RunState = RunState.Ignored;
2138
test.Properties.Set(PropertyNames.SkipReason, "This test runs only when RUN_IT is \"true\"");
2239
}
40+
41+
if (SkipIfSingle && Environment.GetEnvironmentVariable("RUN_IT_SINGLE") == "true")
42+
{
43+
test.RunState = RunState.Ignored;
44+
test.Properties.Set(PropertyNames.SkipReason, "This test runs only when RUN_IT_SINGLE is NOT \"true\"");
45+
}
2346
}
24-
}
47+
}

visual-dotnet/SauceLabs.Visual.IntegrationTests/LoginPage.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public async Task Setup()
2020
Driver = new RemoteWebDriver(sauceUrl, browserOptions);
2121
Driver.ExecuteScript("sauce:job-name=NUnit C#/.Net Visual Session");
2222

23-
VisualClient = await VisualClient.Create(Driver, Region.UsWest1);
23+
VisualClient = await VisualClient.Create(Driver);
2424
TestContext.Progress.WriteLine($"Build: {VisualClient.Build.Url}");
2525
}
2626

@@ -39,4 +39,4 @@ public async Task Teardown()
3939
Driver?.Quit();
4040
VisualClient?.Dispose();
4141
}
42-
}
42+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using NUnit.Framework;
4+
using OpenQA.Selenium.Remote;
5+
6+
namespace SauceLabs.Visual.IntegrationTests;
7+
8+
[Parallelizable(ParallelScope.Children)]
9+
[IntegrationTest(skipIfSingle: true)]
10+
public class LoginPageConcurrent
11+
{
12+
private VisualClient? VisualClient { get; set; }
13+
14+
[OneTimeSetUp]
15+
public async Task Setup()
16+
{
17+
VisualClient = await VisualClient.Create();
18+
TestContext.Progress.WriteLine($"Build: {VisualClient.Build.Url}");
19+
}
20+
21+
[Test]
22+
public async Task LoginPage_ShouldOpen1()
23+
{
24+
await UsingDriver(async (driver) =>
25+
{
26+
driver.Navigate().GoToUrl("https://www.saucedemo.com");
27+
await VisualClient.VisualCheck(driver, "Login Page");
28+
});
29+
}
30+
31+
[Test]
32+
public async Task LoginPage_ShouldOpen2()
33+
{
34+
await UsingDriver(async (driver) =>
35+
{
36+
driver.Navigate().GoToUrl("https://www.saucedemo.com");
37+
await VisualClient.VisualCheck(driver, "Login Page");
38+
});
39+
}
40+
41+
[Test]
42+
public async Task LoginPage_ShouldOpen3()
43+
{
44+
await UsingDriver(async (driver) =>
45+
{
46+
driver.Navigate().GoToUrl("https://www.saucedemo.com");
47+
await VisualClient.VisualCheck(driver, "Login Page");
48+
});
49+
}
50+
51+
private async Task UsingDriver(Func<RemoteWebDriver, Task> func)
52+
{
53+
var browserOptions = Utils.GetBrowserOptions();
54+
var sauceOptions = Utils.GetSauceOptions();
55+
browserOptions.AddAdditionalOption("sauce:options", sauceOptions);
56+
57+
var sauceUrl = Utils.GetOnDemandURL();
58+
59+
var driver = new RemoteWebDriver(sauceUrl, browserOptions);
60+
try
61+
{
62+
await func(driver);
63+
}
64+
finally
65+
{
66+
driver.Quit();
67+
}
68+
}
69+
70+
[OneTimeTearDown]
71+
public async Task Teardown()
72+
{
73+
await VisualClient.Finish();
74+
VisualClient.Dispose();
75+
}
76+
}

visual-dotnet/SauceLabs.Visual.IntegrationTests/Utils.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,22 +40,22 @@ public static string GetSauceAccessKey()
4040
return accessKey;
4141
}
4242

43-
public static string GetSauceRegion()
43+
public static Region GetSauceRegion()
4444
{
4545
var region = Environment.GetEnvironmentVariable("SAUCE_REGION");
4646
if (string.IsNullOrEmpty(region))
4747
{
48-
return "us-west-1";
48+
return Region.UsWest1;
4949
}
5050

51-
return region;
51+
return Region.FromName(region);
5252
}
5353

5454
public static Uri GetOnDemandURL()
5555
{
5656
var regionName = GetSauceRegion();
57-
var tld = regionName == "staging" ? "net" : "com";
58-
return new Uri("https://ondemand." + regionName + ".saucelabs." + tld + "/wd/hub");
57+
var tld = regionName.Name == "staging" ? "net" : "com";
58+
return new Uri("https://ondemand." + regionName.Name + ".saucelabs." + tld + "/wd/hub");
5959
}
6060

6161
public static DriverOptions GetBrowserOptions()
@@ -74,4 +74,4 @@ public static DriverOptions GetBrowserOptions()
7474
Environment.GetEnvironmentVariable("BROWSER_VERSION") ?? "latest";
7575
return browserOptions;
7676
}
77-
}
77+
}

visual-dotnet/SauceLabs.Visual/ConcurrentVisualClient.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,8 @@ private async Task<string> VisualCheckAsync(
109109
return res.EnsureValidResponse().Result;
110110
});
111111

112-
return await VisualCheckBaseAsync(name, options, jobId, sessionId, sessionMetadata.Blob);
112+
var metadata = new WebDriverMetadata(sessionId, jobId, sessionMetadata.Blob);
113+
return await VisualCheckBaseAsync(name, options, metadata);
113114
}
114115
}
115116
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
namespace SauceLabs.Visual.Utils
2+
{
3+
internal class WebDriverMetadata
4+
{
5+
public string SessionId { get; }
6+
public string JobId { get; }
7+
public string SessionMetadataBlob { get; }
8+
9+
public WebDriverMetadata(string sessionId, string jobId, string sessionMetadataBlob)
10+
{
11+
SessionId = sessionId;
12+
JobId = jobId;
13+
SessionMetadataBlob = sessionMetadataBlob;
14+
}
15+
}
16+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System.Collections.Concurrent;
2+
using System.Threading.Tasks;
3+
4+
namespace SauceLabs.Visual.Utils
5+
{
6+
internal class WebDriverMetadataCache
7+
{
8+
private readonly ConcurrentDictionary<string, WebDriverMetadata> _cache = new ConcurrentDictionary<string, WebDriverMetadata>();
9+
10+
public async Task<WebDriverMetadata> GetMetadata(VisualApi api, string sessionId, string jobId)
11+
{
12+
if (_cache.TryGetValue(sessionId, out var cached))
13+
{
14+
return cached;
15+
}
16+
17+
var response = await api.WebDriverSessionInfo(jobId, sessionId);
18+
var metadataBlob = response.EnsureValidResponse().Result.Blob;
19+
var metadata = new WebDriverMetadata(sessionId, jobId, metadataBlob);
20+
21+
// We don't care if this fails
22+
_cache.TryAdd(sessionId, metadata);
23+
24+
return metadata;
25+
}
26+
}
27+
}

visual-dotnet/SauceLabs.Visual/VisualClient.cs

Lines changed: 103 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.Runtime.CompilerServices;
23
using System.Threading.Tasks;
34
using OpenQA.Selenium;
@@ -10,9 +11,9 @@ namespace SauceLabs.Visual
1011
/// </summary>
1112
public class VisualClient : VisualClientBase
1213
{
13-
private readonly string _sessionId;
14-
private readonly string _jobId;
15-
private string? _sessionMetadataBlob;
14+
private static readonly WebDriverMetadataCache _wdCache = new WebDriverMetadataCache();
15+
16+
private WebDriverMetadata? _metadata;
1617

1718
/// <summary>
1819
/// Creates a new instance of <c>VisualClient</c>
@@ -76,17 +77,75 @@ public static async Task<VisualClient> Create(WebDriver wd, Region region, strin
7677
/// <param name="buildOptions">the options of the build creation</param>
7778
public static async Task<VisualClient> Create(WebDriver wd, Region region, string username, string accessKey, CreateBuildOptions buildOptions)
7879
{
79-
var client = new VisualClient(wd, region, username, accessKey);
80+
var client = new VisualClient(region, username, accessKey);
81+
client._metadata = await client.GetMetadata(wd);
8082
await client.SetupBuild(buildOptions);
8183
return client;
8284
}
8385

84-
private async Task SetupBuild(CreateBuildOptions buildOptions)
86+
/// <summary>
87+
/// Creates a new instance of <c>VisualClient</c>
88+
/// </summary>
89+
public static async Task<VisualClient> Create()
90+
{
91+
return await Create(Region.FromEnvironment(), EnvVars.Username, EnvVars.AccessKey, new CreateBuildOptions());
92+
}
93+
94+
/// <summary>
95+
/// Creates a new instance of <c>VisualClient</c>
96+
/// </summary>
97+
/// <param name="buildOptions">the options of the build creation</param>
98+
public static async Task<VisualClient> Create(CreateBuildOptions buildOptions)
99+
{
100+
return await Create(Region.FromEnvironment(), EnvVars.Username, EnvVars.AccessKey, buildOptions);
101+
}
102+
103+
/// <summary>
104+
/// Creates a new instance of <c>VisualClient</c>
105+
/// </summary>
106+
/// <param name="region">the Sauce Labs region to connect to</param>
107+
public static async Task<VisualClient> Create(Region region)
85108
{
86-
var response = await Api.WebDriverSessionInfo(_jobId, _sessionId);
87-
var metadata = response.EnsureValidResponse();
88-
_sessionMetadataBlob = metadata.Result.Blob;
109+
return await Create(region, EnvVars.Username, EnvVars.AccessKey, new CreateBuildOptions());
110+
}
111+
112+
/// <summary>
113+
/// Creates a new instance of <c>VisualClient</c>
114+
/// </summary>
115+
/// <param name="region">the Sauce Labs region to connect to</param>
116+
/// <param name="buildOptions">the options of the build creation</param>
117+
public static async Task<VisualClient> Create(Region region, CreateBuildOptions buildOptions)
118+
{
119+
return await Create(region, EnvVars.Username, EnvVars.AccessKey, buildOptions);
120+
}
121+
122+
/// <summary>
123+
/// Creates a new instance of <c>VisualClient</c>
124+
/// </summary>
125+
/// <param name="region">the Sauce Labs region to connect to</param>
126+
/// <param name="username">the Sauce Labs username</param>
127+
/// <param name="accessKey">the Sauce Labs access key</param>
128+
public static async Task<VisualClient> Create(Region region, string username, string accessKey)
129+
{
130+
return await Create(region, username, accessKey, new CreateBuildOptions());
131+
}
89132

133+
/// <summary>
134+
/// Creates a new instance of <c>VisualClient</c>
135+
/// </summary>
136+
/// <param name="region">the Sauce Labs region to connect to</param>
137+
/// <param name="username">the Sauce Labs username</param>
138+
/// <param name="accessKey">the Sauce Labs access key</param>
139+
/// <param name="buildOptions">the options of the build creation</param>
140+
public static async Task<VisualClient> Create(Region region, string username, string accessKey, CreateBuildOptions buildOptions)
141+
{
142+
var client = new VisualClient(region, username, accessKey);
143+
await client.SetupBuild(buildOptions);
144+
return client;
145+
}
146+
147+
private async Task SetupBuild(CreateBuildOptions buildOptions)
148+
{
90149
Build = await BuildFactory.Get(Api, buildOptions);
91150
}
92151

@@ -97,11 +156,8 @@ private async Task SetupBuild(CreateBuildOptions buildOptions)
97156
/// <param name="region">the Sauce Labs region to connect to</param>
98157
/// <param name="username">the Sauce Labs username</param>
99158
/// <param name="accessKey">the Sauce Labs access key</param>
100-
private VisualClient(WebDriver wd, Region region, string username, string accessKey) : base(region, username, accessKey)
159+
private VisualClient(Region region, string username, string accessKey) : base(region, username, accessKey)
101160
{
102-
_sessionId = wd.SessionId.ToString();
103-
_jobId = wd.Capabilities.HasCapability("jobUuid") ? wd.Capabilities.GetCapability("jobUuid").ToString() : _sessionId;
104-
105161
}
106162

107163
/// <summary>
@@ -122,16 +178,48 @@ private async Task FinishBuild(VisualBuild build)
122178
/// <returns></returns>
123179
public Task<string> VisualCheck(string name, VisualCheckOptions? options = null,
124180
[CallerMemberName] string callerMemberName = "")
181+
{
182+
if (_metadata == null)
183+
{
184+
throw new InvalidOperationException("VisualClient has not been initialized with a WebDriver instance. Please use the `VisualCheck` method accepting a WebDriver instance.");
185+
}
186+
187+
options ??= new VisualCheckOptions();
188+
options.EnsureTestContextIsPopulated(callerMemberName, PreviousSuiteName);
189+
PreviousSuiteName = options.SuiteName;
190+
191+
return VisualCheckAsync(name, options, _metadata);
192+
}
193+
194+
/// <summary>
195+
/// <c>VisualCheck</c> captures a screenshot and queue it for processing.
196+
/// </summary>
197+
/// <param name="wd">the instance of the WebDriver session</param>
198+
/// <param name="name">the name of the screenshot</param>
199+
/// <param name="options">the configuration for the screenshot capture and comparison</param>
200+
/// <param name="callerMemberName">the member name of the caller (automated) </param>
201+
/// <returns></returns>
202+
public async Task<string> VisualCheck(WebDriver wd, string name, VisualCheckOptions? options = null,
203+
[CallerMemberName] string callerMemberName = "")
125204
{
126205
options ??= new VisualCheckOptions();
127206
options.EnsureTestContextIsPopulated(callerMemberName, PreviousSuiteName);
128207
PreviousSuiteName = options.SuiteName;
129-
return VisualCheckAsync(name, options);
208+
209+
var metadata = await GetMetadata(wd);
210+
return await VisualCheckAsync(name, options, metadata);
211+
}
212+
213+
private async Task<string> VisualCheckAsync(string name, VisualCheckOptions options, WebDriverMetadata webDriverMetadata)
214+
{
215+
return await VisualCheckBaseAsync(name, options, webDriverMetadata);
130216
}
131217

132-
private async Task<string> VisualCheckAsync(string name, VisualCheckOptions options)
218+
private async Task<WebDriverMetadata> GetMetadata(WebDriver wd)
133219
{
134-
return await VisualCheckBaseAsync(name, options, _jobId, _sessionId, _sessionMetadataBlob);
220+
var sessionId = wd.SessionId.ToString();
221+
var jobId = wd.Capabilities.HasCapability("jobUuid") ? wd.Capabilities.GetCapability("jobUuid").ToString() : sessionId;
222+
return await _wdCache.GetMetadata(Api, sessionId, jobId);
135223
}
136224

137225
/// <summary>

0 commit comments

Comments
 (0)