Skip to content

Commit

Permalink
Enhancements
Browse files Browse the repository at this point in the history
  • Loading branch information
FriggaHel committed Jan 23, 2025
1 parent a4407d1 commit ee50b70
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 79 deletions.
49 changes: 49 additions & 0 deletions visual-dotnet/SauceLabs.Visual/AbstractVisualClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using SauceLabs.Visual.GraphQL;
using SauceLabs.Visual.Models;
using SauceLabs.Visual.Utils;

namespace SauceLabs.Visual
{
public abstract class AbstractVisualClient
{
protected readonly List<string> ScreenshotIds = new List<string>();
public bool CaptureDom { get; set; } = false;
public BaselineOverride? BaselineOverride { get; set; }

internal async Task<string> VisualCheckBaseAsync(VisualApi api, VisualBuild build, string name,
VisualCheckOptions options, string jobId, string sessionId, string? sessionMetadataBlob)
{
var ignoredRegions =
IgnoredRegions.SplitIgnoredRegions(options.Regions, options.IgnoreRegions, options.IgnoreElements);

FullPageConfigIn? fullPageConfigIn = null;
if (options.FullPage == true)
{
fullPageConfigIn = (options.FullPageConfig ?? new FullPageConfig()).ToFullPageConfigIn();
}

var result = (await api.CreateSnapshotFromWebDriver(new CreateSnapshotFromWebDriverIn(
buildUuid: build.Id,
name: name,
jobId: jobId,
diffingMethod: options.DiffingMethod ?? DiffingMethod.Balanced,
regions: ignoredRegions.RegionsIn,
ignoredElements: ignoredRegions.ElementsIn,
sessionId: sessionId,
sessionMetadata: sessionMetadataBlob ?? "",
captureDom: options.CaptureDom ?? CaptureDom,
clipElement: options.ClipElement?.GetElementId(),
suiteName: options.SuiteName,
testName: options.TestName,
fullPageConfig: fullPageConfigIn,
diffingOptions: options.DiffingOptions?.ToDiffingOptionsIn(),
baselineOverride: (options.BaselineOverride ?? BaselineOverride)?.ToBaselineOverrideIn()
))).EnsureValidResponse();
result.Result.Diffs.Nodes.ToList().ForEach(d => ScreenshotIds.Add(d.Id));
return result.Result.Id;
}
}
}
58 changes: 19 additions & 39 deletions visual-dotnet/SauceLabs.Visual/ConcurrentVisualClient.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using OpenQA.Selenium;
using SauceLabs.Visual.GraphQL;
using SauceLabs.Visual.Models;
using SauceLabs.Visual.Utils;

namespace SauceLabs.Visual
Expand All @@ -14,31 +12,41 @@ namespace SauceLabs.Visual
/// <c>VisualClient</c> provides an access to Sauce Labs Visual services.
/// </summary>
[Obsolete("This is an unstable API. It may change in the future.")]
public class ConcurrentVisualClient : IDisposable
public class ConcurrentVisualClient : AbstractVisualClient, IDisposable
{
// Stores VisualClientV2, indexed by region
private static readonly ConcurrentDictionary<Region, ConcurrentVisualClient> Instances = new ConcurrentDictionary<Region, ConcurrentVisualClient>();

// Stores session metadata, indexed by sessionId
private readonly Dictionary<string, WebDriverSessionInfo> _sessionsMetadata = new Dictionary<string, WebDriverSessionInfo>();
private readonly ConcurrentDictionary<string, WebDriverSessionInfo> _sessionsMetadata = new ConcurrentDictionary<string, WebDriverSessionInfo>();

private readonly VisualApi _api;
private VisualBuild _build;
private VisualBuild Build { get; set; }

private string? _previousSuiteName;

/// <summary>
/// Get the instance of <c>VisualClient</c> for <c>region</c>
/// </summary>
/// <param name="region">target region</param>
/// <param name="buildOptions">the options of the build creation</param>
public static async Task<ConcurrentVisualClient> Get(CreateBuildOptions buildOptions, Region region)
{
return await Get(buildOptions, region, VisualCredential.CreateFromEnvironment());
}

/// <summary>
/// Get the instance of <c>VisualClient</c> for <c>region</c>
/// </summary>
/// <param name="region">target region</param>
/// <param name="creds">Credentials to be used</param>
/// <param name="buildOptions">the options of the build creation</param>
public static async Task<ConcurrentVisualClient> Get(Region region, VisualCredential creds, CreateBuildOptions buildOptions)
public static async Task<ConcurrentVisualClient> Get(CreateBuildOptions buildOptions, Region region, VisualCredential creds)
{
var client = await Instances.GetOrAdd(region, async (key) =>
var client = await Instances.GetOrAddAsync(region, async (key) =>
{
var client = new ConcurrentVisualClient(key, creds);
client._build = await BuildFactory.Get(client._api, buildOptions);
client.Build = await BuildFactory.Get(client._api, buildOptions);
return client;
});
return client;
Expand All @@ -55,7 +63,7 @@ public static async Task Finish()
ConcurrentVisualClient client;
Instances.TryRemove(key, out client);

await client._api.FinishBuild(client._build.Id);
await client._api.FinishBuild(client.Build.Id);
client.Dispose();
}
}
Expand Down Expand Up @@ -109,41 +117,13 @@ private async Task<string> VisualCheckAsync(
var sessionId = wd.SessionId.ToString();
var jobId = wd.Capabilities.HasCapability("jobUuid") ? wd.Capabilities.GetCapability("jobUuid").ToString() : sessionId;

var sessionMetadata = await _sessionsMetadata.GetOrAdd(sessionId, async (_) =>
var sessionMetadata = await _sessionsMetadata.GetOrAddAsync(sessionId, async (_) =>
{
var res = await _api.WebDriverSessionInfo(sessionId, jobId);
return res.EnsureValidResponse().Result;
});

var ignoredRegions = IgnoredRegions.SplitIgnoredRegions(options.Regions, options.IgnoreRegions, options.IgnoreElements);

FullPageConfigIn? fullPageConfigIn = null;
if (options.FullPage == true)
{
fullPageConfigIn = (options.FullPageConfig ?? new FullPageConfig()).ToFullPageConfigIn();
}

var result = (await _api.CreateSnapshotFromWebDriver(new CreateSnapshotFromWebDriverIn(
buildUuid: _build.Id,
name: name,
jobId: jobId,
diffingMethod: options.DiffingMethod ?? DiffingMethod.Balanced,
regions: ignoredRegions.RegionsIn,
ignoredElements: ignoredRegions.ElementsIn,
sessionId: sessionId,
sessionMetadata: sessionMetadata.Blob ?? "",
// FIXME: Restore Class wide setting
captureDom: options.CaptureDom ?? false,
clipElement: options.ClipElement?.GetElementId(),
suiteName: options.SuiteName,
testName: options.TestName,
fullPageConfig: fullPageConfigIn,
diffingOptions: options.DiffingOptions?.ToDiffingOptionsIn(),
// FIXME: Restore Class wide setting
baselineOverride: options.BaselineOverride?.ToBaselineOverrideIn()
))).EnsureValidResponse();
// result.Result.Diffs.Nodes.ToList().ForEach(d => _screenshotIds.Add(d.Id));
return result.Result.Id;
return await VisualCheckBaseAsync(_api, Build, name, options, jobId, sessionId, sessionMetadata.Blob);
}
}
}
27 changes: 19 additions & 8 deletions visual-dotnet/SauceLabs.Visual/Utils/DictionaryExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,22 +1,33 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace SauceLabs.Visual.Utils
{
internal static class DictionaryExtensions
{
public static async Task<TValue> GetOrAdd<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key, Func<TKey, Task<TValue>> valueProvider)
private static readonly SemaphoreSlim Semaphore = new SemaphoreSlim(1, 1);

public static async Task<TValue> GetOrAddAsync<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key, Func<TKey, Task<TValue>> valueProvider)
{
if (dict == null) throw new ArgumentNullException(nameof(dict));
if (key == null) throw new ArgumentNullException(nameof(key));
if (valueProvider == null) throw new ArgumentNullException(nameof(valueProvider));
await Semaphore.WaitAsync();
try
{
if (dict == null) throw new ArgumentNullException(nameof(dict));
if (key == null) throw new ArgumentNullException(nameof(key));
if (valueProvider == null) throw new ArgumentNullException(nameof(valueProvider));

if (dict.TryGetValue(key, out var foundValue))
return foundValue;
if (dict.TryGetValue(key, out var foundValue))
return foundValue;

dict[key] = await valueProvider(key);
return dict[key];
dict[key] = await valueProvider(key);
return dict[key];
}
finally
{
Semaphore.Release();
}
}
}
}
7 changes: 5 additions & 2 deletions visual-dotnet/SauceLabs.Visual/VisualApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,11 @@ public VisualApi(Region region, string username, string accessKey, HttpClient? h

httpClient ??= new HttpClient();

var serializerOptions = new JsonSerializerSettings();
serializerOptions.Converters.Add(new ConstantCaseEnumConverter());
var serializerOptions = new JsonSerializerSettings()
{
NullValueHandling = NullValueHandling.Ignore,
Converters = { new ConstantCaseEnumConverter() }
};

var currentAssembly = Assembly.GetExecutingAssembly().GetName();
_graphQlClient = new GraphQLHttpClient(
Expand Down
32 changes: 2 additions & 30 deletions visual-dotnet/SauceLabs.Visual/VisualClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,14 @@ namespace SauceLabs.Visual
/// <summary>
/// <c>VisualClient</c> provides an access to Sauce Labs Visual services.
/// </summary>
public class VisualClient : IDisposable
public class VisualClient : AbstractVisualClient, IDisposable
{
internal readonly VisualApi Api;
private readonly string _sessionId;
private readonly string _jobId;
private string? _sessionMetadataBlob;
private readonly List<string> _screenshotIds = new List<string>();
public VisualBuild Build { get; private set; }
public bool CaptureDom { get; set; } = false;
public BaselineOverride? BaselineOverride { get; set; }
private readonly ResiliencePipeline _retryPipeline;

private string? _previousSuiteName = null;
Expand Down Expand Up @@ -161,33 +159,7 @@ public Task<string> VisualCheck(string name, VisualCheckOptions? options = null,

private async Task<string> VisualCheckAsync(string name, VisualCheckOptions options)
{
var ignoredRegions = IgnoredRegions.SplitIgnoredRegions(options.Regions, options.IgnoreRegions, options.IgnoreElements);

FullPageConfigIn? fullPageConfigIn = null;
if (options.FullPage == true)
{
fullPageConfigIn = (options.FullPageConfig ?? new FullPageConfig()).ToFullPageConfigIn();
}

var result = (await Api.CreateSnapshotFromWebDriver(new CreateSnapshotFromWebDriverIn(
buildUuid: Build.Id,
name: name,
jobId: _jobId,
diffingMethod: options.DiffingMethod ?? DiffingMethod.Balanced,
regions: ignoredRegions.RegionsIn,
ignoredElements: ignoredRegions.ElementsIn,
sessionId: _sessionId,
sessionMetadata: _sessionMetadataBlob ?? "",
captureDom: options.CaptureDom ?? CaptureDom,
clipElement: options.ClipElement?.GetElementId(),
suiteName: options.SuiteName,
testName: options.TestName,
fullPageConfig: fullPageConfigIn,
diffingOptions: options.DiffingOptions?.ToDiffingOptionsIn(),
baselineOverride: (options.BaselineOverride ?? BaselineOverride)?.ToBaselineOverrideIn()
))).EnsureValidResponse();
result.Result.Diffs.Nodes.ToList().ForEach(d => _screenshotIds.Add(d.Id));
return result.Result.Id;
return await VisualCheckBaseAsync(Api, Build, name, options, _jobId, _sessionId, _sessionMetadataBlob);
}

/// <summary>
Expand Down

0 comments on commit ee50b70

Please sign in to comment.