diff --git a/README.md b/README.md index 1eb2916f..1940759e 100644 --- a/README.md +++ b/README.md @@ -6,3 +6,4 @@ This repository contains the SDKs for Sauce Labs Visual. - [C#](./visual-dotnet) - [Java](./visual-java) +- [Python](./visual-python) diff --git a/visual-dotnet/SauceLabs.Visual/BuildFactory.cs b/visual-dotnet/SauceLabs.Visual/BuildFactory.cs index e634e0da..93c7f635 100644 --- a/visual-dotnet/SauceLabs.Visual/BuildFactory.cs +++ b/visual-dotnet/SauceLabs.Visual/BuildFactory.cs @@ -154,10 +154,10 @@ private static async Task Create(VisualApi api, CreateBuildOptions options.CustomId ??= EnvVars.CustomId; var result = (await api.CreateBuild(new CreateBuildIn { - Name = StringUtils.ValueOrDefault(EnvVars.BuildName, options.Name), - Project = StringUtils.ValueOrDefault(EnvVars.Project, options.Project), - Branch = StringUtils.ValueOrDefault(EnvVars.Branch, options.Branch), - DefaultBranch = StringUtils.ValueOrDefault(EnvVars.DefaultBranch, options.DefaultBranch), + Name = StringUtils.ValueOrDefault(options.Name, EnvVars.BuildName), + Project = StringUtils.ValueOrDefault(options.Project, EnvVars.Project), + Branch = StringUtils.ValueOrDefault(options.Branch, EnvVars.Branch), + DefaultBranch = StringUtils.ValueOrDefault(options.DefaultBranch, EnvVars.DefaultBranch), CustomId = options.CustomId, })).EnsureValidResponse(); diff --git a/visual-dotnet/SauceLabs.Visual/GraphQL/CreateSnapshotFromWebDriverIn.cs b/visual-dotnet/SauceLabs.Visual/GraphQL/CreateSnapshotFromWebDriverIn.cs index 03c1a6d3..00724ad2 100644 --- a/visual-dotnet/SauceLabs.Visual/GraphQL/CreateSnapshotFromWebDriverIn.cs +++ b/visual-dotnet/SauceLabs.Visual/GraphQL/CreateSnapshotFromWebDriverIn.cs @@ -9,6 +9,8 @@ internal class CreateSnapshotFromWebDriverIn public string BuildUuid { get; } [JsonProperty("diffingMethod")] public DiffingMethod DiffingMethod { get; } + [JsonProperty("diffingOptions")] + public DiffingOptionsIn? DiffingOptions { get; set; } [JsonProperty("ignoreRegions")] public RegionIn[] IgnoreRegions { get; } [JsonProperty("jobId")] @@ -43,7 +45,8 @@ public CreateSnapshotFromWebDriverIn( string? clipSelector, string? suiteName, string? testName, - FullPageConfigIn? fullPageConfig + FullPageConfigIn? fullPageConfig, + DiffingOptionsIn? diffingOptions ) { BuildUuid = buildUuid; @@ -58,6 +61,7 @@ public CreateSnapshotFromWebDriverIn( SuiteName = suiteName; TestName = testName; FullPageConfig = fullPageConfig; + DiffingOptions = diffingOptions; } } } \ No newline at end of file diff --git a/visual-dotnet/SauceLabs.Visual/GraphQL/DiffingOptionsIn.cs b/visual-dotnet/SauceLabs.Visual/GraphQL/DiffingOptionsIn.cs new file mode 100644 index 00000000..b45c21a8 --- /dev/null +++ b/visual-dotnet/SauceLabs.Visual/GraphQL/DiffingOptionsIn.cs @@ -0,0 +1,30 @@ +using Newtonsoft.Json; + +namespace SauceLabs.Visual.GraphQL +{ + public class DiffingOptionsIn + { + [JsonProperty("content")] + public bool Content { get; set; } + [JsonProperty("dimensions")] + public bool Dimensions { get; set; } + [JsonProperty("position")] + public bool Position { get; set; } + [JsonProperty("structure")] + public bool Structure { get; set; } + [JsonProperty("style")] + public bool Style { get; set; } + [JsonProperty("visual")] + public bool Visual { get; set; } + + public DiffingOptionsIn(bool defaultValue) + { + Content = defaultValue; + Dimensions = defaultValue; + Position = defaultValue; + Structure = defaultValue; + Style = defaultValue; + Visual = defaultValue; + } + } +} \ No newline at end of file diff --git a/visual-dotnet/SauceLabs.Visual/GraphQL/RegionIn.cs b/visual-dotnet/SauceLabs.Visual/GraphQL/RegionIn.cs index d0931290..d0104397 100644 --- a/visual-dotnet/SauceLabs.Visual/GraphQL/RegionIn.cs +++ b/visual-dotnet/SauceLabs.Visual/GraphQL/RegionIn.cs @@ -1,6 +1,7 @@ using Newtonsoft.Json; using OpenQA.Selenium; using SauceLabs.Visual.Models; +using SauceLabs.Visual.Utils; namespace SauceLabs.Visual.GraphQL { @@ -16,6 +17,8 @@ internal class RegionIn public int Width { get; } [JsonProperty("height")] public int Height { get; } + [JsonProperty("diffingOptions")] + public DiffingOptionsIn? DiffingOptions { get; } public RegionIn(int x, int y, int width, int height) { @@ -29,10 +32,26 @@ public RegionIn(string name, int x, int y, int width, int height) : this(x, y, w Name = name; } + public RegionIn(int x, int y, int width, int height, DiffingOptionsIn diffingOptions) : this(x, y, width, height) + { + DiffingOptions = diffingOptions; + } public RegionIn(IWebElement input) : this(input.Location.X, input.Location.Y, input.Size.Width, input.Size.Height) { } + public RegionIn(IWebElement input, DiffingOptionsIn? options) : this(input.Location.X, input.Location.Y, + input.Size.Width, input.Size.Height) + { + DiffingOptions = options; + } + public RegionIn(IgnoreRegion input) : this(input.X, input.Y, input.Width, input.Height) { } + + public RegionIn(SauceLabs.Visual.Models.Region input, DiffingOptionsIn? options) : this(input.X, input.Y, input.Width, + input.Height) + { + DiffingOptions = options; + } } } \ No newline at end of file diff --git a/visual-dotnet/SauceLabs.Visual/Models/DiffingMethod.cs b/visual-dotnet/SauceLabs.Visual/Models/DiffingMethod.cs index 014c0cd6..3320ad51 100644 --- a/visual-dotnet/SauceLabs.Visual/Models/DiffingMethod.cs +++ b/visual-dotnet/SauceLabs.Visual/Models/DiffingMethod.cs @@ -3,6 +3,7 @@ namespace SauceLabs.Visual.Models public enum DiffingMethod { Simple, - Experimental + Experimental, + Balanced, } } \ No newline at end of file diff --git a/visual-dotnet/SauceLabs.Visual/Models/DiffingOption.cs b/visual-dotnet/SauceLabs.Visual/Models/DiffingOption.cs new file mode 100644 index 00000000..27cb833f --- /dev/null +++ b/visual-dotnet/SauceLabs.Visual/Models/DiffingOption.cs @@ -0,0 +1,16 @@ +using System; + +namespace SauceLabs.Visual.Models +{ + [Flags] + public enum DiffingOption + { + None = 0, + Content = 1 << 0, + Dimensions = 1 << 1, + Position = 1 << 2, + Structure = 1 << 3, + Style = 1 << 4, + Visual = 1 << 5, + } +} \ No newline at end of file diff --git a/visual-dotnet/SauceLabs.Visual/Models/Region.cs b/visual-dotnet/SauceLabs.Visual/Models/Region.cs new file mode 100644 index 00000000..11280f1e --- /dev/null +++ b/visual-dotnet/SauceLabs.Visual/Models/Region.cs @@ -0,0 +1,18 @@ +namespace SauceLabs.Visual.Models +{ + public class Region + { + public int X { get; } + public int Y { get; } + public int Width { get; } + public int Height { get; } + + public Region(int x, int y, int width, int height) + { + X = x; + Y = y; + Width = width; + Height = height; + } + } +} \ No newline at end of file diff --git a/visual-dotnet/SauceLabs.Visual/Models/SelectiveRegion.cs b/visual-dotnet/SauceLabs.Visual/Models/SelectiveRegion.cs new file mode 100644 index 00000000..968ab663 --- /dev/null +++ b/visual-dotnet/SauceLabs.Visual/Models/SelectiveRegion.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using OpenQA.Selenium; +using SauceLabs.Visual.GraphQL; +using SauceLabs.Visual.Utils; + +namespace SauceLabs.Visual.Models +{ + /// + /// SelectiveRegion describe a region where change kind can be filtered. + /// + [Obsolete("WARNING: This API is currently unstable. It may be changed at anytime")] + public class SelectiveRegion + { + internal Region? Region { get; } + internal IWebElement? Element { get; } + + internal DiffingOption? EnableOnly { get; } + internal DiffingOption? DisableOnly { get; } + + private SelectiveRegion(IWebElement element, DiffingOption? enableOnly, DiffingOption? disableOnly) + { + Element = element; + EnableOnly = enableOnly; + DisableOnly = disableOnly; + } + + private SelectiveRegion(Region region, DiffingOption? enableOnly, DiffingOption? disableOnly) + { + Region = region; + EnableOnly = enableOnly; + DisableOnly = disableOnly; + } + + [Obsolete("WARNING: This API is currently unstable. It may be changed at anytime")] + public static SelectiveRegion EnabledFor(IWebElement element) + { + return new SelectiveRegion(element, null, null); + } + + [Obsolete("WARNING: This API is currently unstable. It may be changed at anytime")] + public static SelectiveRegion EnabledFor(IWebElement element, DiffingOption flags) + { + return new SelectiveRegion(element, flags, null); + } + + [Obsolete("WARNING: This API is currently unstable. It may be changed at anytime")] + public static SelectiveRegion EnabledFor(Region region) + { + return new SelectiveRegion(region, DiffingOption.None, null); + } + + [Obsolete("WARNING: This API is currently unstable. It may be changed at anytime")] + public static SelectiveRegion EnabledFor(Region region, DiffingOption flags) + { + return new SelectiveRegion(region, flags, null); + } + + [Obsolete("WARNING: This API is currently unstable. It may be changed at anytime")] + public static SelectiveRegion DisabledFor(IWebElement element) + { + return new SelectiveRegion(element, null, DiffingOption.None); + } + + [Obsolete("WARNING: This API is currently unstable. It may be changed at anytime")] + public static SelectiveRegion DisabledFor(IWebElement element, DiffingOption flags) + { + return new SelectiveRegion(element, null, flags); + } + + [Obsolete("WARNING: This API is currently unstable. It may be changed at anytime")] + public static SelectiveRegion DisabledFor(Region region) + { + return new SelectiveRegion(region, null, DiffingOption.None); + } + + [Obsolete("WARNING: This API is currently unstable. It may be changed at anytime")] + public static SelectiveRegion DisabledFor(Region region, DiffingOption flags) + { + return new SelectiveRegion(region, null, flags); + } + + internal RegionIn ToRegionIn() + { + var diffingOptions = DiffingOptionsInHelper.CreateFromEnableOnlyDisable(EnableOnly, DisableOnly); + if (Region != null) + { + return new RegionIn(Region, diffingOptions); + } + + if (Element != null) + { + return new RegionIn(Element, diffingOptions); + } + + throw new VisualClientException("No Element nor Region has been passed"); + } + } +} \ No newline at end of file diff --git a/visual-dotnet/SauceLabs.Visual/SauceLabs.Visual.csproj b/visual-dotnet/SauceLabs.Visual/SauceLabs.Visual.csproj index f94fc901..36ba3cc2 100644 --- a/visual-dotnet/SauceLabs.Visual/SauceLabs.Visual.csproj +++ b/visual-dotnet/SauceLabs.Visual/SauceLabs.Visual.csproj @@ -7,7 +7,7 @@ en-US en SauceLabs.Visual - 0.3.1 + 0.6.0 Sauce Labs Visual Binding saucelabs sauce labs visual testing screenshot capture dom https://github.com/saucelabs/visual-sdks diff --git a/visual-dotnet/SauceLabs.Visual/Utils/DiffingOptionsInHelper.cs b/visual-dotnet/SauceLabs.Visual/Utils/DiffingOptionsInHelper.cs new file mode 100644 index 00000000..396b77a2 --- /dev/null +++ b/visual-dotnet/SauceLabs.Visual/Utils/DiffingOptionsInHelper.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using SauceLabs.Visual.GraphQL; +using SauceLabs.Visual.Models; + +namespace SauceLabs.Visual.Utils +{ + public static class DiffingOptionsInHelper + { + internal static DiffingOptionsIn SetOptions(DiffingOptionsIn opts, DiffingOption flags, bool value) + { + if (flags.HasFlag(DiffingOption.Content)) + { + opts.Content = value; + } + + if (flags.HasFlag(DiffingOption.Dimensions)) + { + opts.Dimensions = value; + } + + if (flags.HasFlag(DiffingOption.Position)) + { + opts.Position = value; + } + + if (flags.HasFlag(DiffingOption.Structure)) + { + opts.Structure = value; + } + + if (flags.HasFlag(DiffingOption.Style)) + { + opts.Style = value; + } + + if (flags.HasFlag(DiffingOption.Visual)) + { + opts.Visual = value; + } + + return opts; + } + + internal static DiffingOptionsIn? CreateFromEnableOnlyDisable(DiffingOption? enableOnly, DiffingOption? disableOnly) + { + if (enableOnly.HasValue) + { + var options = new DiffingOptionsIn(false); + options = SetOptions(options, enableOnly.Value, true); + return options; + } + + if (disableOnly.HasValue) + { + var options = new DiffingOptionsIn(true); + options = SetOptions(options, disableOnly.Value, false); + return options; + } + + return null; + } + } +} \ No newline at end of file diff --git a/visual-dotnet/SauceLabs.Visual/VisualCheckDiffingOptions.cs b/visual-dotnet/SauceLabs.Visual/VisualCheckDiffingOptions.cs new file mode 100644 index 00000000..65f20a59 --- /dev/null +++ b/visual-dotnet/SauceLabs.Visual/VisualCheckDiffingOptions.cs @@ -0,0 +1,59 @@ +using System.Diagnostics; +using System.Linq; +using OpenQA.Selenium; +using SauceLabs.Visual.GraphQL; +using SauceLabs.Visual.Models; +using SauceLabs.Visual.Utils; + +namespace SauceLabs.Visual +{ + public class VisualCheckDiffingOptions + { + private enum DiffingOptionMode + { + EnableOnly, + DisableOnly + } + + private readonly DiffingOptionMode _mode; + private readonly DiffingOption _flags; + + private VisualCheckDiffingOptions(DiffingOptionMode mode, DiffingOption flags) + { + _mode = mode; + _flags = flags; + } + + /// + /// EnableOnly sets which change types to check for. + /// Compatible only with DiffingMethod.Balanced + /// + public static VisualCheckDiffingOptions EnableOnly(DiffingOption flags) + { + return new VisualCheckDiffingOptions(DiffingOptionMode.EnableOnly, flags); + } + + /// + /// DisableOnly sets which change types to ignore. + /// Compatible only with DiffingMethod.Balanced + /// + public static VisualCheckDiffingOptions DisableOnly(DiffingOption flags) + { + return new VisualCheckDiffingOptions(DiffingOptionMode.DisableOnly, flags); + } + + internal DiffingOptionsIn ToDiffingOptionsIn() + { + DiffingOptionsIn options; + if (_mode == DiffingOptionMode.EnableOnly) + { + options = DiffingOptionsInHelper.SetOptions(new DiffingOptionsIn(false), _flags, true); + } + else + { + options = DiffingOptionsInHelper.SetOptions(new DiffingOptionsIn(true), _flags, false); + } + return options; + } + } +} \ No newline at end of file diff --git a/visual-dotnet/SauceLabs.Visual/VisualCheckOptions.cs b/visual-dotnet/SauceLabs.Visual/VisualCheckOptions.cs index 9e38c2bc..db3223e4 100644 --- a/visual-dotnet/SauceLabs.Visual/VisualCheckOptions.cs +++ b/visual-dotnet/SauceLabs.Visual/VisualCheckOptions.cs @@ -19,6 +19,16 @@ public class VisualCheckOptions public FullPageConfig? FullPageConfig { get; set; } public string? ClipSelector { get; set; } + /// + /// DiffingOptions set which kind of changes should be considered. + /// + public VisualCheckDiffingOptions? DiffingOptions { get; set; } + + /// + /// Regions allows to specify what kind of checks needs to be done in a specific region. + /// + public SelectiveRegion[]? Regions { get; set; } + /// /// SuiteName manually set the SuiteName of the Test. /// diff --git a/visual-dotnet/SauceLabs.Visual/VisualClient.cs b/visual-dotnet/SauceLabs.Visual/VisualClient.cs index d828b177..0a260688 100644 --- a/visual-dotnet/SauceLabs.Visual/VisualClient.cs +++ b/visual-dotnet/SauceLabs.Visual/VisualClient.cs @@ -144,6 +144,7 @@ private async Task VisualCheckAsync(string name, VisualCheckOptions opti var ignored = new List(); ignored.AddRange(options.IgnoreRegions?.Select(r => new RegionIn(r)) ?? new List()); ignored.AddRange(options.IgnoreElements?.Select(r => new RegionIn(r)) ?? new List()); + ignored.AddRange(options.Regions?.Select(r => r.ToRegionIn()) ?? new List()); FullPageConfigIn? fullPageConfigIn = null; if (options.FullPage == true) @@ -163,7 +164,8 @@ private async Task VisualCheckAsync(string name, VisualCheckOptions opti clipSelector: options.ClipSelector, suiteName: options.SuiteName, testName: options.TestName, - fullPageConfig: fullPageConfigIn + fullPageConfig: fullPageConfigIn, + diffingOptions: options.DiffingOptions?.ToDiffingOptionsIn() ))).EnsureValidResponse(); result.Result.Diffs.Nodes.ToList().ForEach(d => _screenshotIds.Add(d.Id)); return result.Result.Id; diff --git a/visual-java/pom.xml b/visual-java/pom.xml index 6819213c..8adf01fd 100644 --- a/visual-java/pom.xml +++ b/visual-java/pom.xml @@ -6,7 +6,7 @@ com.saucelabs.visual java-client - 0.3.387 + 0.5.1 visual-java-client Java library to interact with Sauce Visual diff --git a/visual-java/src/main/graphql/visual/schema.graphqls b/visual-java/src/main/graphql/visual/schema.graphqls index 7507dce4..8a3fa3d2 100644 --- a/visual-java/src/main/graphql/visual/schema.graphqls +++ b/visual-java/src/main/graphql/visual/schema.graphqls @@ -1,3 +1,12 @@ +schema { + mutation: Mutation + query: Query +} + +type ApplicationSummary { + name: String! +} + input ApproveBuildIn { """@deprecated Use `uuid`. This field will be removed in a future update.""" id: ID @@ -53,6 +62,7 @@ type Baseline implements Node { """The method to use when ordering `Diff`.""" orderBy: [DiffsOrderBy!] = [PRIMARY_KEY_ASC] ): DiffsConnection! + hasDom: Boolean! id: UUID! imageUrl: String! isLatest: Boolean! @@ -175,6 +185,23 @@ type Build implements Node { createdByOrgId: UUID! createdByUser: User! createdByUserId: UUID! + + """ + User provided id for a build. + + Use `buildByCustomId` to look up a build by its `customId`. + + Properties: + - up to 64 bytes (try to stick to ASCII characters) + - in case of colissions, the latest build is returned + - collissions may hurt query performance + + Recommendations: + - generate the id from the CI pipeline link by applying a hashing function + - prefix/postfix the id with a team id to avoid collisions with other teams, e.g. `sha512(teamId + ':' + url)` + """ + customId: String + defaultBranch: String diffCount(status: DiffStatus!): Int! @deprecated(reason: "Use diffCountExtended. This will be removed by 2024-02-11.") """ @@ -233,6 +260,7 @@ type Build implements Node { """Full-text search ranking when filtered by `fullText`.""" fullTextRank: Float id: UUID! + keepAliveTimeout: Int mode: BuildMode! name: String! @@ -346,6 +374,14 @@ input BuildFilter { input BuildIn { branch: String + customId: String + defaultBranch: String + + """ + A positive integer that is the time in seconds that the Build is allowed to be in the RUNNING state after the last snapshot was created or updated. + The number clipped to the interval [1;86400]. + """ + keepAliveTimeout: Int name: String project: String } @@ -435,7 +471,9 @@ enum BuildsOrderBy { CREATED_AT_ASC CREATED_AT_DESC CREATED_BY_ORG_ID_ASC + CREATED_BY_ORG_ID_ASC__CUSTOM_ID_ASC CREATED_BY_ORG_ID_DESC + CREATED_BY_ORG_ID_DESC__CUSTOM_ID_DESC CREATED_BY_USER_ID_ASC CREATED_BY_USER_ID_DESC ID_ASC @@ -457,8 +495,23 @@ input CreateSnapshotFromWebDriverIn { """ buildId: ID buildUuid: UUID + captureDom: Boolean + + """ + A querySelector compatible selector of an element that we should crop the screenshot to. + """ + clipSelector: String diffingMethod: DiffingMethod + diffingOptions: DiffingOptionsIn + + """ + Enable full page screenshot using scroll-and-stitch strategy. + Limitation: Currently, this feature is supported only on desktop browsers. + """ + fullPageConfig: FullPageConfigIn ignoreRegions: [RegionIn!] + + """This will be mandatory in the future.""" jobId: String name: String! sessionId: ID! @@ -524,12 +577,17 @@ type Diff implements Node { diffBounds: Rect diffClusters: [Rect]! diffingMethod: DiffingMethod! + + """snapshot { uploadId } should be requested at the same moment""" + domDiffUrl: String + hasDom: Boolean! id: UUID! """ A globally unique identifier. Can be used in various places throughout the system to identify this single value. """ nodeId: ID! + options: DiffingOption """Reads a single `Snapshot` that is related to this `Diff`.""" snapshot: Snapshot @@ -651,10 +709,29 @@ Method to use for diffing. SIMPLE is the default. """ enum DiffingMethod { + BALANCED EXPERIMENTAL SIMPLE } +type DiffingOption { + content: Boolean + dimensions: Boolean + position: Boolean + structure: Boolean + style: Boolean + visual: Boolean +} + +input DiffingOptionsIn { + content: Boolean + dimensions: Boolean + position: Boolean + structure: Boolean + style: Boolean + visual: Boolean +} + """A connection to a list of `Diff` values.""" type DiffsConnection { """ @@ -708,6 +785,35 @@ input FinishBuildIn { uuid: UUID } +input FullPageConfigIn { + """Adjust address bar padding on iOS and Android for viewport cutout.""" + addressBarShadowPadding: Float + + """ + Delay in ms after scrolling and before taking screenshots. + A slight delay can be helpful if the page is using lazy loading when scrolling + """ + delayAfterScrollMs: Int + + """Disable CSS animations and the input caret in the app.""" + disableCSSAnimation: Boolean + + """Hide elements on the page after first scroll by css selectors.""" + hideAfterFirstScroll: [String] + + """Hide all scrollbars in the app.""" + hideScrollBars: Boolean + + """ + Limit the number of screenshots taken for scrolling and stitching. + Default and max value is 10 + """ + scrollLimit: Int + + """Adjust toolbar padding on iOS and Android for viewport cutout.""" + toolBarShadowPadding: Int +} + scalar FullText """ @@ -793,6 +899,161 @@ enum OperatingSystem { WINDOWS } +type Org { + id: UUID + + """Reads and enables pagination through a set of `OrgStat`.""" + orgStats( + """Read all values in the set after (below) this cursor.""" + after: Cursor + + """Read all values in the set before (above) this cursor.""" + before: Cursor + + """ + A condition to be used in determining which values should be returned by the collection. + """ + condition: OrgStatCondition + + """ + A filter to be used in determining which values should be returned by the collection. + """ + filter: OrgStatFilter + + """Only read the first `n` values of the set.""" + first: Int + + """Only read the last `n` values of the set.""" + last: Int + + """ + Skip the first `n` values from our `after` cursor, an alternative to cursor + based pagination. May not be used with `last`. + """ + offset: Int + + """The method to use when ordering `OrgStat`.""" + orderBy: [OrgStatsOrderBy!] = [PRIMARY_KEY_ASC] + ): OrgStatsConnection! + statsGroupedByDay( + """Read all values in the set after (below) this cursor.""" + after: Cursor + + """Read all values in the set before (above) this cursor.""" + before: Cursor + + """Only read the first `n` values of the set.""" + first: Int + + """Only read the last `n` values of the set.""" + last: Int + + """ + Skip the first `n` values from our `after` cursor, an alternative to cursor + based pagination. May not be used with `last`. + """ + offset: Int + ): OrgStatsGroupedByDayConnection! +} + +type OrgStat implements Node { + hour: Datetime! + + """ + A globally unique identifier. Can be used in various places throughout the system to identify this single value. + """ + nodeId: ID! + + """Reads a single `Org` that is related to this `OrgStat`.""" + org: Org + orgId: UUID! + snapshotsUsed: Int! +} + +""" +A condition to be used against `OrgStat` object types. All fields are tested for equality and combined with a logical ‘and.’ +""" +input OrgStatCondition { + """Checks for equality with the object’s `orgId` field.""" + orgId: UUID +} + +""" +A filter to be used against `OrgStat` object types. All fields are combined with a logical ‘and.’ +""" +input OrgStatFilter { + """Filter by the object’s `orgId` field.""" + orgId: UUIDFilter +} + +"""A connection to a list of `OrgStat` values.""" +type OrgStatsConnection { + """ + A list of edges which contains the `OrgStat` and cursor to aid in pagination. + """ + edges: [OrgStatsEdge!]! + + """A list of `OrgStat` objects.""" + nodes: [OrgStat!]! + + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """The count of *all* `OrgStat` you could get from the connection.""" + totalCount: Int! +} + +"""A `OrgStat` edge in the connection.""" +type OrgStatsEdge { + """A cursor for use in pagination.""" + cursor: Cursor + + """The `OrgStat` at the end of the edge.""" + node: OrgStat! +} + +"""A connection to a list of `OrgStatsGroupedByDayRecord` values.""" +type OrgStatsGroupedByDayConnection { + """ + A list of edges which contains the `OrgStatsGroupedByDayRecord` and cursor to aid in pagination. + """ + edges: [OrgStatsGroupedByDayEdge!]! + + """A list of `OrgStatsGroupedByDayRecord` objects.""" + nodes: [OrgStatsGroupedByDayRecord!]! + + """ + The count of *all* `OrgStatsGroupedByDayRecord` you could get from the connection. + """ + totalCount: Int! +} + +"""A `OrgStatsGroupedByDayRecord` edge in the connection.""" +type OrgStatsGroupedByDayEdge { + """A cursor for use in pagination.""" + cursor: Cursor + + """The `OrgStatsGroupedByDayRecord` at the end of the edge.""" + node: OrgStatsGroupedByDayRecord! +} + +"""The return type of our `statsGroupedByDay` query.""" +type OrgStatsGroupedByDayRecord { + day: Datetime + snapshotsUsed: Int +} + +"""Methods to use when ordering `OrgStat`.""" +enum OrgStatsOrderBy { + NATURAL + ORG_ID_ASC + ORG_ID_ASC__HOUR_ASC + ORG_ID_DESC + ORG_ID_DESC__HOUR_DESC + PRIMARY_KEY_ASC + PRIMARY_KEY_DESC +} + """Information about pagination in a connection.""" type PageInfo { """When paginating forwards, the cursor to continue.""" @@ -859,6 +1120,7 @@ type Query implements Node { """ branches(filtername: String): [String!]! build(id: UUID!): Build + buildByCustomId(customId: String!): Build """Reads a single `Build` using its globally unique `ID`.""" buildByNodeId( @@ -954,6 +1216,14 @@ type Query implements Node { The root query type must be a `Node` to work well with Relay 1 mutations. This just resolves to `query`. """ nodeId: ID! + org: Org + orgStat(hour: Datetime!, orgId: UUID!): OrgStat + + """Reads a single `OrgStat` using its globally unique `ID`.""" + orgStatByNodeId( + """The globally unique `ID` to be used in selecting a single `OrgStat`.""" + nodeId: ID! + ): OrgStat """ List all the build projects that are visible to the current user and that include `filtername`. @@ -1014,6 +1284,7 @@ type Query implements Node { } type Rect { + flags: DiffingOption height: Int! width: Int! x: Int! @@ -1021,6 +1292,7 @@ type Rect { } type Region { + diffingOptions: DiffingOption height: Int! name: String width: Int! @@ -1029,6 +1301,7 @@ type Region { } input RegionIn { + diffingOptions: DiffingOptionsIn height: Int! name: String width: Int! @@ -1078,7 +1351,9 @@ type Snapshot implements Node { build: Build buildId: UUID! createdAt: Datetime! + defaultBranch: String device: String + devicePixelRatio: Float! """Reads and enables pagination through a set of `Diff`.""" diffs( @@ -1113,6 +1388,7 @@ type Snapshot implements Node { """The method to use when ordering `Diff`.""" orderBy: [DiffsOrderBy!] = [PRIMARY_KEY_ASC] ): DiffsConnection! + domDiffUrl: String """ If not null, it indicates that the snapshot is invalid. @@ -1124,6 +1400,12 @@ type Snapshot implements Node { together with the JSON contents of `error`. """ error: JSON + hasDom: Boolean! + + """ + `height` is determined asynchronously and may be null right after snapshot creation. + """ + height: Int id: UUID! ignoreRegions: [Region]! imageUrl: String! @@ -1147,6 +1429,11 @@ type Snapshot implements Node { url: String! viewportHeight: Int viewportWidth: Int + + """ + `width` is determined asynchronously and may be null right after snapshot creation. + """ + width: Int } """ @@ -1188,7 +1475,9 @@ input SnapshotIn { buildId: ID buildUuid: UUID device: String + devicePixelRatio: Float diffingMethod: DiffingMethod + diffingOptions: DiffingOptionsIn ignoreRegions: [RegionIn!] jobUrl: String name: String! @@ -1212,7 +1501,13 @@ input SnapshotIn { type SnapshotUpload { buildId: UUID! + domUploadUrl: String id: UUID! + imageUploadUrl: String + + """ + @deprecated "Use imageUploadUrl." + """ uploadUrl: String! } @@ -1328,6 +1623,8 @@ type User { } type WebdriverSession { + applicationSummary: ApplicationSummary + """ Encodes all metadata in an opaque scalar that can be passed to `CreateSnapshotFromWebDriver`. """ @@ -1347,8 +1644,3 @@ input WebdriverSessionInfoIn { jobId: ID! sessionId: ID! } - -schema { - mutation: Mutation - query: Query -} \ No newline at end of file diff --git a/visual-java/src/main/java/com/saucelabs/visual/CheckOptions.java b/visual-java/src/main/java/com/saucelabs/visual/CheckOptions.java index bf4f34a6..8fac9c0d 100644 --- a/visual-java/src/main/java/com/saucelabs/visual/CheckOptions.java +++ b/visual-java/src/main/java/com/saucelabs/visual/CheckOptions.java @@ -1,8 +1,12 @@ package com.saucelabs.visual; +import com.saucelabs.visual.graphql.type.DiffingOptionsIn; +import com.saucelabs.visual.model.DiffingFlag; import com.saucelabs.visual.model.FullPageScreenshotConfig; import com.saucelabs.visual.model.IgnoreRegion; +import com.saucelabs.visual.model.VisualRegion; import java.util.ArrayList; +import java.util.EnumSet; import java.util.List; import org.openqa.selenium.WebElement; @@ -10,7 +14,8 @@ public class CheckOptions { public enum DiffingMethod { SIMPLE, - EXPERIMENTAL + EXPERIMENTAL, + BALANCED, } public CheckOptions() {} @@ -18,28 +23,34 @@ public CheckOptions() {} public CheckOptions( List ignoreElements, List ignoreRegions, + List regions, String testName, String suiteName, DiffingMethod diffingMethod, + DiffingOptionsIn diffingOptions, Boolean captureDom, String clipSelector, FullPageScreenshotConfig fullPageScreenshotConfig) { this.ignoreElements = ignoreElements; this.ignoreRegions = ignoreRegions; + this.regions = regions; this.testName = testName; this.suiteName = suiteName; this.diffingMethod = diffingMethod; this.captureDom = captureDom; this.clipSelector = clipSelector; this.fullPageScreenshotConfig = fullPageScreenshotConfig; + this.diffingOptions = diffingOptions; } private List ignoreElements = new ArrayList<>(); private List ignoreRegions = new ArrayList<>(); + private List regions = new ArrayList<>(); private String testName; private String suiteName; private DiffingMethod diffingMethod; + private DiffingOptionsIn diffingOptions; private Boolean captureDom; private String clipSelector; @@ -48,9 +59,11 @@ public CheckOptions( public static class Builder { private List ignoreElements = new ArrayList<>(); private List ignoreRegions = new ArrayList<>(); + private List regions = new ArrayList<>(); private String testName; private String suiteName; private DiffingMethod diffingMethod; + private DiffingOptionsIn diffingOptions; private Boolean captureDom; private String clipSelector; private FullPageScreenshotConfig fullPageScreenshotConfig; @@ -95,13 +108,41 @@ public Builder withFullPageConfig(FullPageScreenshotConfig fullPageScreenshotCon return this; } + public Builder disableOnly(EnumSet flags) { + this.diffingOptions = new DiffingOptionsIn(); + DiffingFlag.setAll(this.diffingOptions, true); + for (DiffingFlag f : flags) f.apply(this.diffingOptions, false); + return this; + } + + public Builder enableOnly(EnumSet flags) { + this.diffingOptions = new DiffingOptionsIn(); + DiffingFlag.setAll(this.diffingOptions, false); + for (DiffingFlag f : flags) f.apply(this.diffingOptions, true); + return this; + } + + public Builder enableOnly(EnumSet flags, WebElement element) { + + this.regions.add(VisualRegion.ignoreChangesFor(element).except(flags)); + return this; + } + + public Builder disableOnly(EnumSet flags, WebElement element) { + + this.regions.add(VisualRegion.detectChangesFor(element).except(flags)); + return this; + } + public CheckOptions build() { return new CheckOptions( ignoreElements, ignoreRegions, + regions, testName, suiteName, diffingMethod, + diffingOptions, captureDom, clipSelector, fullPageScreenshotConfig); @@ -132,6 +173,14 @@ public void setTestName(String testName) { this.testName = testName; } + public List getRegions() { + return regions; + } + + public void setRegions(List regions) { + this.regions = regions; + } + public String getSuiteName() { return suiteName; } @@ -148,6 +197,10 @@ public DiffingMethod getDiffingMethod() { return diffingMethod; } + public DiffingOptionsIn getDiffingOptions() { + return diffingOptions; + } + public void setCaptureDom(Boolean captureDom) { this.captureDom = captureDom; } diff --git a/visual-java/src/main/java/com/saucelabs/visual/VisualApi.java b/visual-java/src/main/java/com/saucelabs/visual/VisualApi.java index ada3e318..cd625ebb 100644 --- a/visual-java/src/main/java/com/saucelabs/visual/VisualApi.java +++ b/visual-java/src/main/java/com/saucelabs/visual/VisualApi.java @@ -1,14 +1,13 @@ package com.saucelabs.visual; import static com.saucelabs.visual.utils.EnvironmentVariables.isNotBlank; +import static com.saucelabs.visual.utils.EnvironmentVariables.valueOrDefault; import com.saucelabs.visual.exception.VisualApiException; import com.saucelabs.visual.graphql.*; -import com.saucelabs.visual.graphql.type.Diff; -import com.saucelabs.visual.graphql.type.DiffStatus; -import com.saucelabs.visual.graphql.type.DiffingMethod; -import com.saucelabs.visual.graphql.type.RegionIn; +import com.saucelabs.visual.graphql.type.*; import com.saucelabs.visual.model.IgnoreRegion; +import com.saucelabs.visual.model.VisualRegion; import com.saucelabs.visual.utils.ConsoleColors; import com.saucelabs.visual.utils.EnvironmentVariables; import dev.failsafe.Failsafe; @@ -16,7 +15,6 @@ import java.time.Duration; import java.util.*; import java.util.stream.Collectors; -import org.openqa.selenium.Rectangle; import org.openqa.selenium.WebElement; import org.openqa.selenium.remote.RemoteWebDriver; import org.slf4j.Logger; @@ -230,33 +228,22 @@ public String getName() { if (isNotBlank(EnvironmentVariables.BUILD_NAME_DEPRECATED)) { log.warn( "Sauce Labs Visual: Environment variable \"BUILD_NAME\" is deprecated and will be removed in a future version. Please use \"SAUCE_VISUAL_BUILD_NAME\" instead."); - return EnvironmentVariables.BUILD_NAME_DEPRECATED; } - if (isNotBlank(EnvironmentVariables.BUILD_NAME)) { - return EnvironmentVariables.BUILD_NAME; - } - return name; + return valueOrDefault( + valueOrDefault(name, EnvironmentVariables.BUILD_NAME), + EnvironmentVariables.BUILD_NAME_DEPRECATED); } public String getProject() { - if (isNotBlank(EnvironmentVariables.PROJECT_NAME)) { - return EnvironmentVariables.PROJECT_NAME; - } - return project; + return valueOrDefault(project, EnvironmentVariables.PROJECT_NAME); } public String getBranch() { - if (isNotBlank(EnvironmentVariables.BRANCH_NAME)) { - return EnvironmentVariables.BRANCH_NAME; - } - return branch; + return valueOrDefault(branch, EnvironmentVariables.BRANCH_NAME); } public String getDefaultBranch() { - if (isNotBlank(EnvironmentVariables.DEFAULT_BRANCH_NAME)) { - return EnvironmentVariables.DEFAULT_BRANCH_NAME; - } - return defaultBranch; + return valueOrDefault(defaultBranch, EnvironmentVariables.DEFAULT_BRANCH_NAME); } } @@ -342,6 +329,7 @@ public void sauceVisualCheck(String snapshotName, CheckOptions options) { new CreateSnapshotFromWebDriverMutation.CreateSnapshotFromWebDriverIn( this.build.getId(), diffingMethod, + Optional.ofNullable(options.getDiffingOptions()), extractIgnoreList(options), this.jobId, snapshotName, @@ -387,6 +375,8 @@ private static DiffingMethod toDiffingMethod(CheckOptions options) { } switch (options.getDiffingMethod()) { + case BALANCED: + return DiffingMethod.BALANCED; case EXPERIMENTAL: return DiffingMethod.EXPERIMENTAL; default: @@ -458,43 +448,48 @@ private List extractIgnoreList(CheckOptions options) { if (options == null) { return Collections.emptyList(); } + + List ignoredElements = + options.getIgnoreElements() == null ? Arrays.asList() : options.getIgnoreElements(); + + List ignoredRegions = + options.getIgnoreRegions() == null ? Arrays.asList() : options.getIgnoreRegions(); + + List visualRegions = + options.getIgnoreRegions() == null ? Arrays.asList() : options.getRegions(); + List result = new ArrayList<>(); - for (int i = 0; i < options.getIgnoreElements().size(); i++) { - WebElement element = options.getIgnoreElements().get(i); + for (int i = 0; i < ignoredElements.size(); i++) { + WebElement element = ignoredElements.get(i); if (validate(element) == null) { throw new VisualApiException("options.ignoreElement[" + i + "] does not exist (yet)"); } - result.add(toIgnoreIn(element)); + result.add(VisualRegion.ignoreChangesFor(element).toRegionIn()); } - for (int i = 0; i < options.getIgnoreRegions().size(); i++) { - IgnoreRegion ignoreRegion = options.getIgnoreRegions().get(i); + for (int i = 0; i < ignoredRegions.size(); i++) { + IgnoreRegion ignoreRegion = ignoredRegions.get(i); if (validate(ignoreRegion) == null) { throw new VisualApiException("options.ignoreRegion[" + i + "] is an invalid ignore region"); } - result.add(toIgnoreIn(ignoreRegion)); + result.add( + VisualRegion.ignoreChangesFor( + ignoreRegion.getName(), + ignoreRegion.getX(), + ignoreRegion.getHeight(), + ignoreRegion.getWidth(), + ignoreRegion.getHeight()) + .toRegionIn()); + } + for (int i = 0; i < visualRegions.size(); i++) { + VisualRegion region = visualRegions.get(i); + if (validate(region) == null) { + throw new VisualApiException("options.region[" + i + "] is an invalid visual region"); + } + result.add(region.toRegionIn()); } return result; } - private RegionIn toIgnoreIn(WebElement element) { - Rectangle r = element.getRect(); - return RegionIn.builder() - .withX(r.getX()) - .withY(r.getY()) - .withWidth(r.getWidth()) - .withHeight(r.getHeight()) - .build(); - } - - private RegionIn toIgnoreIn(IgnoreRegion r) { - return RegionIn.builder() - .withX(r.getX()) - .withY(r.getY()) - .withWidth(r.getWidth()) - .withHeight(r.getHeight()) - .build(); - } - private WebElement validate(WebElement element) { if (element == null || !element.isDisplayed() || element.getRect() == null) { return null; @@ -512,6 +507,20 @@ private IgnoreRegion validate(IgnoreRegion region) { return region; } + private VisualRegion validate(VisualRegion region) { + if (region == null) { + return null; + } + if (0 < region.getHeight() * region.getWidth()) { + return region; + } + WebElement ele = region.getElement(); + if (ele != null && ele.isDisplayed() && ele.getRect() != null) { + return region; + } + return null; + } + public VisualBuild getBuild() { return build; } diff --git a/visual-java/src/main/java/com/saucelabs/visual/graphql/CreateSnapshotFromWebDriverMutation.java b/visual-java/src/main/java/com/saucelabs/visual/graphql/CreateSnapshotFromWebDriverMutation.java index 3daaf1ae..f5f33f59 100644 --- a/visual-java/src/main/java/com/saucelabs/visual/graphql/CreateSnapshotFromWebDriverMutation.java +++ b/visual-java/src/main/java/com/saucelabs/visual/graphql/CreateSnapshotFromWebDriverMutation.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.saucelabs.visual.graphql.type.DiffingMethod; +import com.saucelabs.visual.graphql.type.DiffingOptionsIn; import com.saucelabs.visual.graphql.type.DiffsConnection; import com.saucelabs.visual.graphql.type.RegionIn; import com.saucelabs.visual.model.FullPageScreenshotConfig; @@ -41,9 +42,12 @@ public static class CreateSnapshotFromWebDriverIn { public Optional fullPageConfig = Optional.empty(); + public Optional diffingOptions = Optional.empty(); + public CreateSnapshotFromWebDriverIn( String buildUuid, DiffingMethod diffingMethod, + Optional diffingOptions, List ignoreRegions, String jobId, String name, @@ -51,6 +55,7 @@ public CreateSnapshotFromWebDriverIn( String sessionMetadata) { this.buildUuid = buildUuid; this.diffingMethod = diffingMethod; + this.diffingOptions = diffingOptions; this.ignoreRegions = ignoreRegions; this.jobId = jobId; this.name = name; @@ -77,6 +82,10 @@ public void setClipSelector(String clipSelector) { public void setFullPageConfig(FullPageScreenshotConfig fullPageConfig) { this.fullPageConfig = Optional.ofNullable(fullPageConfig); } + + public void setDiffingOptions(DiffingOptionsIn diffingOptions) { + this.diffingOptions = Optional.of(diffingOptions); + } } public static class Data { diff --git a/visual-java/src/main/java/com/saucelabs/visual/model/DiffingFlag.java b/visual-java/src/main/java/com/saucelabs/visual/model/DiffingFlag.java new file mode 100644 index 00000000..60b587f2 --- /dev/null +++ b/visual-java/src/main/java/com/saucelabs/visual/model/DiffingFlag.java @@ -0,0 +1,27 @@ +package com.saucelabs.visual.model; + +import com.saucelabs.visual.graphql.type.DiffingOptionsIn; + +public enum DiffingFlag { + Content, + Dimensions, + Position, + Structure, + Style, + Visual; + + public void apply(DiffingOptionsIn options, boolean value) { + if (this == Content) options.setContent(value); + if (this == Dimensions) options.setDimensions(value); + if (this == Position) options.setPosition(value); + if (this == Structure) options.setStructure(value); + if (this == Style) options.setStyle(value); + if (this == Visual) options.setVisual(value); + } + + public static void setAll(DiffingOptionsIn options, boolean value) { + for (DiffingFlag o : DiffingFlag.values()) { + o.apply(options, value); + } + } +} diff --git a/visual-java/src/main/java/com/saucelabs/visual/model/VisualRegion.java b/visual-java/src/main/java/com/saucelabs/visual/model/VisualRegion.java new file mode 100644 index 00000000..94fc75fc --- /dev/null +++ b/visual-java/src/main/java/com/saucelabs/visual/model/VisualRegion.java @@ -0,0 +1,168 @@ +package com.saucelabs.visual.model; + +import com.saucelabs.visual.graphql.type.DiffingOptionsIn; +import com.saucelabs.visual.graphql.type.RegionIn; +import java.util.EnumSet; +import org.openqa.selenium.Rectangle; +import org.openqa.selenium.WebElement; + +public class VisualRegion { + private DiffingOptionsIn options; + private boolean isIgnoreRegion; + private WebElement element; + private int x; + private int y; + private int width; + private int height; + private String name; + + private VisualRegion() {} + + public VisualRegion(IgnoreRegion ir) { + this.options = setAllFlags(new DiffingOptionsIn(), false); + this.isIgnoreRegion = true; + this.name = ir.getName(); + this.height = ir.getHeight(); + this.width = ir.getWidth(); + this.x = ir.getX(); + this.y = ir.getY(); + } + + public VisualRegion(WebElement element, DiffingOptionsIn diffingOptions) { + this.options = diffingOptions; + this.element = element; + } + + public static VisualRegion ignoreChangesFor(WebElement element) { + VisualRegion r = new VisualRegion(); + r.options = setAllFlags(new DiffingOptionsIn(), false); + r.isIgnoreRegion = true; + r.element = element; + return r; + } + + public static VisualRegion detectChangesFor(WebElement element) { + VisualRegion r = new VisualRegion(); + r.options = setAllFlags(new DiffingOptionsIn(), true); + r.isIgnoreRegion = false; + r.element = element; + return r; + } + + public static VisualRegion ignoreChangesFor(String name, int x, int y, int width, int height) { + VisualRegion r = new VisualRegion(); + r.options = setAllFlags(new DiffingOptionsIn(), false); + r.isIgnoreRegion = true; + r.name = name; + r.height = height; + r.width = width; + r.x = x; + r.y = y; + return r; + } + + public static VisualRegion ignoreChangesFor(int x, int y, int width, int height) { + return VisualRegion.ignoreChangesFor("", x, y, width, height); + } + + public static VisualRegion detectChangesFor(String name, int x, int y, int width, int height) { + VisualRegion r = new VisualRegion(); + r.options = setAllFlags(new DiffingOptionsIn(), true); + r.isIgnoreRegion = false; + r.name = name; + r.height = height; + r.width = width; + r.x = x; + r.y = y; + return r; + } + + public static VisualRegion detectChangesFor(int x, int y, int width, int height) { + return VisualRegion.detectChangesFor("", x, y, width, height); + } + + private static DiffingOptionsIn setAllFlags(DiffingOptionsIn opt, boolean value) { + opt.setContent(value); + opt.setDimensions(value); + opt.setPosition(value); + opt.setStructure(value); + opt.setStyle(value); + opt.setVisual(value); + return opt; + } + + public WebElement getElement() { + return element; + } + + public void setElement(WebElement value) { + this.element = value; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getHeight() { + return height; + } + + public void setHeight(int height) { + this.height = height; + } + + public int getWidth() { + return width; + } + + public void setWidth(int width) { + this.width = width; + } + + public int getX() { + return x; + } + + public void setX(int x) { + this.x = x; + } + + public int getY() { + return y; + } + + public void setY(int y) { + this.y = y; + } + + public VisualRegion except(EnumSet flags) { + for (DiffingFlag f : flags) { + f.apply(this.options, this.isIgnoreRegion); + } + return this; + } + + public RegionIn toRegionIn() { + RegionIn r = new RegionIn(); + r.setName(this.name); + r.setHeight(this.height); + r.setWidth(this.width); + r.setX(this.x); + r.setY(this.y); + r.setDiffingOptions(this.options); + + if (this.element != null) { + Rectangle rect = element.getRect(); + r.setX(rect.getX()); + r.setY(rect.getY()); + r.setWidth(rect.getWidth()); + r.setHeight(rect.getHeight()); + } + + return r; + } +} diff --git a/visual-java/src/main/java/com/saucelabs/visual/utils/EnvironmentVariables.java b/visual-java/src/main/java/com/saucelabs/visual/utils/EnvironmentVariables.java index 328faed4..f3e53c1d 100644 --- a/visual-java/src/main/java/com/saucelabs/visual/utils/EnvironmentVariables.java +++ b/visual-java/src/main/java/com/saucelabs/visual/utils/EnvironmentVariables.java @@ -16,4 +16,8 @@ private EnvironmentVariables() {} public static boolean isNotBlank(String str) { return str != null && !str.trim().isEmpty(); } + + public static String valueOrDefault(String str, String defaultValue) { + return isNotBlank(str) ? str : defaultValue; + } } diff --git a/visual-python/.bumpversion.toml b/visual-python/.bumpversion.toml index a2f98454..7e6e7e43 100644 --- a/visual-python/.bumpversion.toml +++ b/visual-python/.bumpversion.toml @@ -1,5 +1,5 @@ [tool.bumpversion] -current_version = "0.0.7" +current_version = "0.0.10" parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)" serialize = ["{major}.{minor}.{patch}"] search = "{current_version}" @@ -20,3 +20,8 @@ commit_args = "" filename = "pyproject.toml" search = "{current_version}" replace = "{new_version}" + +[[tool.bumpversion.files]] +filename = "src/saucelabs_visual/client.py" +search = "PKG_VERSION = '{current_version}'" +replace = "PKG_VERSION = '{new_version}'" diff --git a/visual-python/README.md b/visual-python/README.md index 9d3c14d7..960e00a3 100644 --- a/visual-python/README.md +++ b/visual-python/README.md @@ -1,3 +1,7 @@ # Sauce Labs Visual for Python Sauce Labs Visual for Python exposes Sauce Labs Visual Testing for your Python project with Selenium. + +## Installation & Usage + +View installation and usage instructions on the [Sauce Docs website](https://docs.saucelabs.com/visual-testing/integrations/python/). diff --git a/visual-python/pyproject.toml b/visual-python/pyproject.toml index 05107aed..f80e9c9f 100644 --- a/visual-python/pyproject.toml +++ b/visual-python/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "saucelabs_visual" -version = "0.0.7" +version = "0.0.10" description = "Python bindings for Sauce Labs Visual" dependencies=[ "requests", diff --git a/visual-python/src/saucelabs_visual/client.py b/visual-python/src/saucelabs_visual/client.py index 4d9dd322..9188e52b 100644 --- a/visual-python/src/saucelabs_visual/client.py +++ b/visual-python/src/saucelabs_visual/client.py @@ -10,34 +10,38 @@ from saucelabs_visual.regions import Region from saucelabs_visual.typing import IgnoreRegion, FullPageConfig, DiffingMethod, BuildStatus +PKG_VERSION = '0.0.10' class SauceLabsVisual: - client: Client = None + _client: Client = None build_id: Union[str, None] = None build_url: Union[str, None] = None meta_cache: dict = {} region: Region = None - def __init__(self): - self._create_client() + @property + def client(self): + if self._client is None: + self._client = self._create_client() + return self._client def _create_client(self): username = environ.get("SAUCE_USERNAME") access_key = environ.get("SAUCE_ACCESS_KEY") if username is None or access_key is None: - raise Exception( + raise RuntimeError( 'Sauce Labs credentials not set. Please check that you set correctly your ' '`SAUCE_USERNAME` and `SAUCE_ACCESS_KEY` environment variables.' ) self.region = Region.from_name(environ.get("SAUCE_REGION") or 'us-west-1') region_url = self.region.graphql_endpoint - transport = RequestsHTTPTransport(url=region_url, auth=HTTPBasicAuth(username, access_key)) - self.client = Client(transport=transport, execute_timeout=90) - - def get_client(self) -> Client: - return self.client + user_agent = 'visual-python/{version}'.format(version=PKG_VERSION) + transport = RequestsHTTPTransport(url=region_url, auth=HTTPBasicAuth(username, access_key), headers={ + 'user-agent': user_agent, + }) + return Client(transport=transport, execute_timeout=90) def create_build( self, diff --git a/visual-python/src/saucelabs_visual/frameworks/robot.py b/visual-python/src/saucelabs_visual/frameworks/robot.py index 9d730271..1485e5d6 100644 --- a/visual-python/src/saucelabs_visual/frameworks/robot.py +++ b/visual-python/src/saucelabs_visual/frameworks/robot.py @@ -16,13 +16,36 @@ @library(scope='GLOBAL') class SauceLabsVisual: - client: Client = None + _client: Client = None + selenium_library_key: Union[str, None] = None - def __init__(self): - self.client = Client() + @property + def client(self): + if self._client is None: + self._client = Client() + return self._client def _get_selenium_library(self) -> SeleniumLibrary: - return BuiltIn().get_library_instance('SeleniumLibrary') + all_libraries: dict = BuiltIn().get_library_instance(all=True) + + # SeleniumLibrary may be imported under another name if an alias is provided -- ex: + # + # Library SeleniumLibrary AS slib + # + # Instead of importing by static name, we'll get all imported libraries and iterate over + # them to find the instance of SeleniumLibrary and cache that key. + if self.selenium_library_key is None: + for key, value in all_libraries.items(): + if type(value) is SeleniumLibrary: + self.selenium_library_key = key + break + + if self.selenium_library_key is None: + raise RuntimeError( + 'SeleniumLibrary instance not found in Robot. Is it imported in your project?' + ) + + return BuiltIn().get_library_instance(self.selenium_library_key) def _get_selenium_id(self) -> str: return self._get_selenium_library().get_session_id() @@ -65,9 +88,7 @@ def _parse_ignore_regions( for ignore_region in ignore_regions: literal_type = type(ignore_region) - if literal_type is IgnoreRegion: - parsed_ignore_regions.append(ignore_region) - elif literal_type is dict: + if literal_type is dict: parsed_ignore_regions.append( ignore_region_from_dict(ignore_region) ) @@ -79,14 +100,7 @@ def _parse_ignore_regions( elements_to_query.append(ignore_region) for element in elements_to_query: - rect = element.rect - region = IgnoreRegion( - width=rect.get('width'), - height=rect.get('height'), - x=rect.get('x'), - y=rect.get('y'), - ) - parsed_ignore_regions.append(region) + parsed_ignore_regions.append(ignore_region_from_dict(element.rect)) return [ region for region in parsed_ignore_regions if self._is_valid_ignore_region(region)