Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OSOE-770: Add support for structured html-validate output #354

Closed
wants to merge 12 commits into from
57 changes: 14 additions & 43 deletions Lombiq.Tests.UI/Docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,51 +59,22 @@ Recommendations and notes for such configuration:

### HTML validation configuration

If you want to change some HTML validation rules from only a few specific tests, you can create a custom _.htmlvalidate.json_ file (e.g. _TestName.htmlvalidate.json_). For example:

```json
{
"extends": [
"html-validate:recommended"
],

"rules": {
"attribute-boolean-style": "off",
"element-required-attributes": "off",
"no-trailing-whitespace": "off",
"no-inline-style": "off",
"no-implicit-button-type": "off",
"wcag/h30": "off",
"wcag/h32": "off",
"wcag/h36": "off",
"wcag/h37": "off",
"wcag/h67": "off",
"wcag/h71": "off"
},

"root": true
}
If you want to change some HTML validation rules for only a few specific tests you can exclude them from the results.
For example if you want to exclude the `prefer-native-element` rule from the results you can do it by doing the following:

```c#
configuration => configuration.HtmlValidationConfiguration.AssertHtmlValidationResultAsync =
validationResult =>
{
var errors = validationResult.GetParsedErrors()
.Where(error => error.RuleId is not "prefer-native-element");
errors.ShouldBeEmpty(string.Join('\n', errors.Select(error => error.Message)));
return Task.CompletedTask;
});
```

Then you can change the configuration to use that:

```cs
changeConfiguration: configuration => configuration.HtmlValidationConfiguration.HtmlValidationOptions =
configuration.HtmlValidationConfiguration.HtmlValidationOptions
.CloneWith(validationOptions => validationOptions.ConfigPath =
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "TestName.htmlvalidate.json")));
```

Make sure to also include the `root` attribute and set it to `true` inside the custom _.htmlvalidate.json_ file and include it in the test project like this:

```xml
<ItemGroup>
<Content Include="TestName.htmlvalidate.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<PackageCopyToOutput>true</PackageCopyToOutput>
</Content>
</ItemGroup>
```
Note that the `RuleId` is the identifier of the rule that you want to exclude from the results.
The custom string formatter in the call to `errors.ShouldBeEmpty` is used to display the errors in a more readable way and is not strictly necessary.

## Multi-process test execution

Expand Down
34 changes: 34 additions & 0 deletions Lombiq.Tests.UI/Extensions/HtmlValidationResultExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using Atata.HtmlValidation;
using Lombiq.Tests.UI.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;

namespace Lombiq.Tests.UI.Extensions;
Expand All @@ -22,4 +24,36 @@ public static async Task<IEnumerable<string>> GetErrorsAsync(this HtmlValidation
.Select(error =>
(error.StartsWith("error:", StringComparison.OrdinalIgnoreCase) ? string.Empty : "error:") + error);
}

/// <summary>
/// Gets the parsed errors from the HTML validation result.
/// Can only be used if the output formatter is set to JSON.
/// </summary>
public static IEnumerable<JsonHtmlValidationError> GetParsedErrors(this HtmlValidationResult result) => ParseOutput(result.Output);

private static IEnumerable<JsonHtmlValidationError> ParseOutput(string output)
{
output = output.Trim();
if ((!output.StartsWith('{') || !output.EndsWith('}')) &&
(!output.StartsWith('[') || !output.EndsWith(']')))
{
throw new JsonException($"Invalid JSON, make sure to set the OutputFormatter to JSON. Output: {output}");
}

try
{
var document = JsonDocument.Parse(output);
return document.RootElement.EnumerateArray()
.SelectMany(element => element.GetProperty("messages").EnumerateArray())
.Select(message =>
{
var rawMessageText = message.GetRawText();
return JsonSerializer.Deserialize<JsonHtmlValidationError>(rawMessageText);
});
}
catch (JsonException)
{
throw new JsonException("Unable to parse output, did you set the OutputFormatter to JSON?");
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Atata.Cli;
using Atata.Cli.HtmlValidate;
using Atata.HtmlValidation;
using Lombiq.Tests.UI.Exceptions;
using Lombiq.Tests.UI.Services;
Expand Down Expand Up @@ -53,6 +54,12 @@ public static HtmlValidationResult ValidateHtml(
{
var options = context.Configuration.HtmlValidationConfiguration.HtmlValidationOptions.Clone();
htmlValidationOptionsAdjuster?.Invoke(options);

// Because of the default settings of Atata.HtmlValidation.HtmlValidationOptions overriding the formatter
// we need to set this back to JSON
if (options.OutputFormatter == HtmlValidateFormatter.Names.Stylish)
options.ResultFileFormatter = HtmlValidateFormatter.Names.Json;

try
{
return new HtmlValidator(options).Validate(context.Driver.PageSource);
Expand Down
28 changes: 28 additions & 0 deletions Lombiq.Tests.UI/Models/JsonHtmlValidationError.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Lombiq.Tests.UI.Models;

public class JsonHtmlValidationError
{
[JsonPropertyName("ruleId")]
public string RuleId { get; set; }
[JsonPropertyName("severity")]
public int Severity { get; set; }
[JsonPropertyName("message")]
public string Message { get; set; }
[JsonPropertyName("offset")]
public int Offset { get; set; }
[JsonPropertyName("line")]
public int Line { get; set; }
[JsonPropertyName("column")]
public int Column { get; set; }
[JsonPropertyName("size")]
public int Size { get; set; }
[JsonPropertyName("selector")]
public string Selector { get; set; }
[JsonPropertyName("ruleUrl")]
public string RuleUrl { get; set; }
[JsonPropertyName("context")]
public JsonElement Context { get; set; }
}
18 changes: 17 additions & 1 deletion Lombiq.Tests.UI/Services/HtmlValidationConfiguration.cs
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's good that now we can filter in a simpler way, but the output message should be adjusted too, because now it's not human readable:

    Lombiq.Tests.UI.Exceptions.PageChangeAssertionException : An assertion during the page change event has failed on page https://localhost:9024/ (Lombiq's OSOCE - UI Testing - Blog).
    ---- Lombiq.Tests.UI.Exceptions.HtmlValidationAssertionException : await validationResult.GetParsedErrorsAsync()
        should be empty but had
    1
        item and was
    [Lombiq.Tests.UI.Models.HtmlValidationError (2681320)] Check the HTML validation report in the failure dump for details.
    -------- Shouldly.ShouldAssertException : await validationResult.GetParsedErrorsAsync()
        should be empty but had
    1
        item and was
    [Lombiq.Tests.UI.Models.HtmlValidationError (2681320)]

image

Previously the errors names were listed here.
Like this:

  Lombiq.Tests.UI.Exceptions.PageChangeAssertionException : An assertion during the page change event has failed on page https://localhost:9022/ (Lombiq's OSOCE - UI Testing - Blog).
  ---- Lombiq.Tests.UI.Exceptions.HtmlValidationAssertionException : validationResult.Output
      should be empty but was
  "G:\Work\Open-Source-Orchard-Core-Extensions\test\Lombiq.OSOCE.Tests.UI\bin\Debug\net8.0\HtmlValidationTemp\5bc0f7c6-8eb3-4ec7-acf5-6744e3373ea6.html
    44:8  error  Prefer to use the native <button> element  prefer-native-element
  
  ✖ 1 problem (1 error, 0 warnings)
  
  More information:
    https://html-validate.org/rules/prefer-native-element.html
  " Check the HTML validation report in the failure dump for details.
  -------- Shouldly.ShouldAssertException : validationResult.Output
      should be empty but was
  "G:\Work\Open-Source-Orchard-Core-Extensions\test\Lombiq.OSOCE.Tests.UI\bin\Debug\net8.0\HtmlValidationTemp\5bc0f7c6-8eb3-4ec7-acf5-6744e3373ea6.html
    44:8  error  Prefer to use the native <button> element  prefer-native-element
  
  ✖ 1 problem (1 error, 0 warnings)
  
  More information:
    https://html-validate.org/rules/prefer-native-element.html
  "

(To repro this, just remove the custom configuration.HtmlValidationConfiguration.AssertHtmlValidationResultAsync from a test that uses it. Also to get what should the output look like, you can check out the dev branch and do the same thing.)

And I think the same thing applies to the HtmlValidationReport.txt (that is generated in the UI test failure dump (locally \Open-Source-Orchard-Core-Extensions\test\Lombiq.OSOCE.Tests.UI\bin\Debug\net8.0\FailureDumps). I think the format of that changed too:

Previously:
HTMLVA~1.TXT

Now:
HtmlValidationReport.txt

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DemeSzabolcs I have set the output back to the text for the output file so that should be fixed. For the message on the actual test run I can not fully put that back the way it was because it's just the way that Shouldly reports on the errors based on the new format.

What I did try was pass a custom message to shouldly and then the test output will look like: here
image

But this would mean that we'd have to pass in the custom string formatter into everywhere we use shouldly. If you have another idea on how to solve this I'd be happy to hear.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well if it's this way because of how Shouldly summarizes the error and it's not our code, then I think using that custom message is a fine solution.

Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
using Atata.Cli.HtmlValidate;
using Atata.HtmlValidation;
using Lombiq.Tests.UI.Extensions;
using Lombiq.Tests.UI.Helpers;
using Shouldly;
using System;
using System.Linq;
using System.Threading.Tasks;

namespace Lombiq.Tests.UI.Services;
Expand All @@ -28,6 +31,8 @@ public class HtmlValidationConfiguration
/// </summary>
public HtmlValidationOptions HtmlValidationOptions { get; set; } = new()
{
ResultFileFormatter = HtmlValidateFormatter.Names.Text,
OutputFormatter = HtmlValidateFormatter.Names.Json,
SaveHtmlToFile = HtmlSaveCondition.Never,
SaveResultToFile = true,
// This is necessary so no long folder names will be generated, see:
Expand Down Expand Up @@ -64,7 +69,18 @@ public class HtmlValidationConfiguration
public static readonly Func<HtmlValidationResult, Task> AssertHtmlValidationOutputIsEmptyAsync =
validationResult =>
{
validationResult.Output.ShouldBeEmpty();
// Keep supporting cases where output format is not set to JSON.
if (validationResult.Output.Trim().StartsWith('[') ||
validationResult.Output.Trim().StartsWith('{'))
{
var errors = validationResult.GetParsedErrors();
errors.ShouldBeEmpty(string.Join('\n', errors.Select(error => error.Message)));
}
else
{
validationResult.Output.ShouldBeEmpty();
}

return Task.CompletedTask;
};

Expand Down