Automated web accessibility testing with .NET, C#, and Selenium. Wraps the axe-core accessibility scanning engine and the Selenium.WebDriver browser automation framework.
Compatible with .NET Standard 2.0+, .NET Framework 4.7.1+, and .NET Core 2.0+.
Install via NuGet:
PM> Install-Package Deque.AxeCore.Selenium
# or, use the Visual Studio "Manage NuGet Packages" UI
Ensure you have Playwright browsers installed. For reference, see https://playwright.dev/dotnet/docs/browsers
Import this namespace:
using Deque.AxeCore.Commons;
using Deque.AxeCore.Selenium;
To run an axe accessibility scan of a web page with the default configuration, create a new AxeBuilder
using your Selenium IWebDriver
object and call Analyze
:
IWebDriver webDriver = new ChromeDriver();
AxeResult axeResult = new AxeBuilder(webDriver).Analyze();
To cause a test to pass or fail based on the scan, use the Violations
property of the AxeResult
:
// We recommend FluentAssertions to get great error messages out of the box
using FluentAssertions;
axeResult.Violations.Should().BeEmpty();
To configure different scan options, use the chainable methods of the AxeBuilder
(reference docs):
AxeResult axeResult = new AxeBuilder(webDriver)
.Exclude(".css-class-of-element-with-known-failures")
.WithTags("wcag2a")
.Analyze();
For a complete working sample project that uses this library, see the C# sample in microsoft/axe-pipelines-samples.
AxeResult axeResult = new AxeBuilder(webDriver).Analyze();
Runs an axe accessibility scan of the entire page using all previously chained options and returns an AxeResult
representing the scan results.
IWebElement elementToTest = webDriver.FindElement(By.Id("nav-bar"));
AxeResult axeResult = new AxeBuilder(webDriver)
.Analyze(elementToTest);
Runs an axe accessibility scan scoped using all previously chained options to the given Selenium IWebElement
. Returns an AxeResult
representing the scan results.
Not compatible with AxeBuilder.Include
or AxeBuilder.Exclude
; the element passed to Analyze
will take precedence and the Include
/Exclude
calls will be ignored.
AxeResult axeResult = new AxeBuilder(webDriver)
.Include(".class-of-element-under-test")
.Analyze();
Scopes future Analyze()
calls to include only the element(s) matching the given CSS selector.
Include
may be chained multiple times to include multiple selectors in a scan.
Include
may be combined with Exclude
to scan a tree of elements but omit some children of that tree. For example:
AxeResult axeResult = new AxeBuilder(webDriver)
.Include("#element-under-test")
.Exclude("#element-under-test div.child-class-with-known-issues")
.Analyze();
Include
is not compatible with Analyze(IWebElement)
- the Analyze
argument will take precedence and Include
will be ignored.
This overload of Include
only supports CSS selectors which refer to elements which are in the topmost frame of the page and not contained within a shadow DOM. To specify a selector in an iframe or a shadow DOM, see the overload that accepts an AxeSelector
.
AxeResult axeResult = new AxeBuilder(webDriver)
.Include(new AxeSelector("#element-inside-iframe", new List<string> { "#containing-iframe-element" }))
.Analyze();
Scopes future Analyze()
calls to include only the element(s) matching the given AxeSelector
.
Include
may be chained multiple times to include multiple selectors in a scan.
Include
may be combined with Exclude
to scan a tree of elements but omit some children of that tree. For example:
AxeResult axeResult = new AxeBuilder(webDriver)
.Include(new AxeSelector("#element-inside-iframe", new List<string> { "#containing-iframe-element" }))
.Exclude(new AxeSelector("#element-inside-iframe div.child-class-with-known-issues", new List<string> { "#containing-iframe-element" }))
.Analyze();
This overload of Include
supports complex AxeSelectors which specify elements inside iframes and/or shadow DOMs:
AxeSelector elementInNestedIframes = new AxeSelector("#element-in-nested-iframe", new List<string> { "#topmost-iframe", "#nested-iframe" });
AxeSelector elementInShadowDom = AxeSelector.FromFrameShadowSelectors(new List<IList<string>> { new List<string> { "#shadow-root-in-topmost-frame", "#element-in-shadow-dom" }});
AxeSelector elementInComplexFrameShadowLayout = AxeSelector.FromFrameShadowSelectors(new List<IList<string>> {
new List<string> { "#shadow-root-in-topmost-frame", "#nested-shadow-root", "#iframe-in-nested-shadow-dom" },
new List<string> { "#shadow-root-in-iframe", "#deeply-nested-target-element" }
});
AxeResult axeResult = new AxeBuilder(webDriver)
.Exclude(".class-of-element-with-known-issues")
.Analyze();
Scopes future Analyze()
calls to exclude the element(s) matching the given CSS selector.
Exclude
may be chained multiple times to exclude multiple selectors in a scan.
Exclude
may be combined with Include
to scan a tree of elements but omit some children of that tree. For example:
AxeResult axeResult = new AxeBuilder(webDriver)
.Include("#element-under-test")
.Exclude("#element-under-test div.child-class-with-known-issues")
.Analyze();
Exclude
is not compatible with Analyze(IWebElement)
- the Analyze
argument will take precedence and Exclude
will be ignored.
This overload of Include
only supports CSS selectors which refer to elements which are in the topmost frame of the page and not contained within a shadow DOM. To specify a selector in an iframe or a shadow DOM, see the overload that accepts an AxeSelector
.
AxeResult axeResult = new AxeBuilder(webDriver)
.Exclude(new AxeSelector("#element-inside-iframe-with-known-issues", new List<string> { "#containing-iframe-element" }))
.Analyze();
Scopes future Analyze()
calls to exclude the element(s) matching the given CSS selector.
Exclude
may be chained multiple times to exclude multiple selectors in a scan.
Exclude
may be combined with Include
to scan a tree of elements but omit some children of that tree. For example:
AxeResult axeResult = new AxeBuilder(webDriver)
.Include(new AxeSelector("#element-inside-iframe", new List<string> { "#containing-iframe-element" }))
.Exclude(new AxeSelector("#element-inside-iframe div.child-class-with-known-issues", new List<string> { "#containing-iframe-element" }))
.Analyze();
Exclude
is not compatible with Analyze(IWebElement)
- the Analyze
argument will take precedence and Exclude
will be ignored.
This overload of Exclude
supports complex AxeSelectors which specify elements inside iframes and/or shadow DOMs:
AxeSelector elementInNestedIframes = new AxeSelector("#element-in-nested-iframe", new List<string> { "#topmost-iframe", "#nested-iframe" });
AxeSelector elementInShadowDom = AxeSelector.FromFrameShadowSelectors(new List<IList<string>> { new List<string> { "#shadow-root-in-topmost-frame", "#element-in-shadow-dom" }});
AxeSelector elementInComplexFrameShadowLayout = AxeSelector.FromFrameShadowSelectors(new List<IList<string>> {
new List<string> { "#shadow-root-in-topmost-frame", "#nested-shadow-root", "#iframe-in-nested-shadow-dom" },
new List<string> { "#shadow-root-in-iframe", "#deeply-nested-target-element" }
});
AxeResult axeResult = new AxeBuilder(webDriver)
.WithRules("color-contrast", "duplicate-id")
.Analyze();
Causes future calls to Analyze
to only run the specified axe rules.
For a list of the available axe rules, see https://dequeuniversity.com/rules/axe/3.3. The "Rule ID" at the top of each individual rule's page is the ID you would want to pass to this method.
WithRules
is not compatible with WithTags
or DisableRules
; whichever one you specify last will take precedence.
WithRules
is not compatible with the deprecated raw Options
property.
AxeResult axeResult = new AxeBuilder(webDriver)
.DisableRules("color-contrast", "duplicate-id")
.Analyze();
Causes future calls to Analyze
to omit the specified axe rules.
For a list of the available axe rules, see https://dequeuniversity.com/rules/axe/3.3. The "Rule ID" at the top of each individual rule's page is the ID you would want to pass to this method.
DisableRules
is compatible with WithTags
; you can use this to run all-but-some of the rules for a given set of tags. For example, to run all WCAG 2.0 A rules except for color-contrast
:
AxeResult axeResult = new AxeBuilder(webDriver)
.WithTags("wcag2a")
.DisableRules("color-contrast")
.Analyze();
DisableRules
is not compatible with WithRules
; whichever one you specify second will take precedence.
DisableRules
is not compatible with the deprecated raw Options
property.
Causes future calls to Analyze
to only run axe rules that match at least one of the specified tags.
A "tag" is a string that may be associated with a given axe rule. See the axe-core API documentation for a complete list of available tags.
WithTags
is compatible with DisableRules
; you can use this to run all-but-some of the rules for a given set of tags. For example, to run all WCAG 2.0 A rules except for color-contrast
:
AxeResult axeResult = new AxeBuilder(webDriver)
.WithTags("wcag2a")
.DisableRules("color-contrast")
.Analyze();
WithTags
is not compatible with WithRules
; whichever one you specify second will take precedence.
WithTags
is not compatible with the deprecated raw Options
property.
Note: in most cases, the simpler WithRules
, WithTags
, and DisableRules
can be used instead.
AxeResult axeResult = new AxeBuilder(webDriver)
.WithOptions(new AxeRunOptions()
{
RunOnly = new RunOnlyOptions
{
Type = "rule",
Values = new List<string> { "duplicate-id", "color-contrast" }
},
RestoreScroll = true
})
.Analyze();
Causes future calls to Analyze
to use the specified options when calling axe.run
in axe-core. See the axe-core API documentation for descriptions of the different properties of AxeRunOptions
.
WithOptions
will override any values previously set by WithRules
, WithTags
, and DisableRules
.
WithOptions
is not compatible with the deprecated raw Options
property.
If you don't want to run Axe on iFrames you can tell Axe skip with AxeRunOptions.
AxeResult axeResult = new AxeBuilder(webDriver)
.WithOptions(new AxeRunOptions()
{
Iframes = false
})
.Analyze();
Prevents Axe core from getting injected into page iFrames.
Causes future calls to Analyze
to use the specified options when calling axe.run
in axe-core. See the axe-core API documentation for descriptions of the different properties of AxeRunOptions
.
AxeResult axeResult = new AxeBuilder(webDriver)
.WithOutputFile(@"./path/to/axe-results.json")
.Analyze();
Causes future calls to Analyze
to export their results to a JSON file, in addition to being returned as an AxeResult
object as usual.
The output format is exactly the same as axe-core would have produced natively, and is compatible with other tools that read axe result JSON, like axe-sarif-converter.
This constructor overload enables certain advanced options not required by most Deque.AxeCore.Selenium
users. Currently, its only use is to allow you to use a custom axe-core
implementation, rather than the one that is packaged with this library.
AxeBuilderOptions axeBuilderOptions = new AxeBuilderOptions
{
ScriptProvider = new FileAxeScriptProvider(".\\axe.min.js")
};
AxeResult axeResult = new AxeBuilder(webDriver, axeBuilderOptions).Analyze();
Set the frame testing method to "legacy mode". In this mode, axe will not open a blank page in which to aggregate its results. This can be used in an environment where opening a blank page causes issues.
With legacy mode turned on, axe will fall back to its test solution prior to the 4.3 release, but with cross-origin frame testing disabled. The frame-tested rule will report which frames were untested.
Important: Use of .UseLegacyMode()
is a last resort. If you find there is no other solution, please report this as an issue. It will be removed in a future release.
AxeResult axeResult = new AxeBuilder(webDriver)
.UseLegacyMode()
.Analyze();
In most cases, you would run an axe scan from within a test method in a suite of end to end tests, and you would want to use a test assertion to verify that there are no unexpected accessibility violations in a page or component.
We strongly recommend FluentAssertions, a NuGet package that does a good job of producing actionable error messages based on the AxeResult
output from a scan. That said, if you prefer a different assertion library, you can still use this library; it does not require any particular test or assertion framework.
If you start with no accessibility issues in your page, you can stay clean by validating that the Violations list is empty:
IWebDriver webDriver = new ChromeDriver();
AxeResult results = new AxeBuilder(webDriver).Analyze();
results.Violations.Should().BeEmpty();
If you already have some accessibility issues & you want to make sure that you do not introduce any more new issues till you get to a clean state, you can use Exclude
to remove problematic elements from a broad scan, and a combination of Include
and DisableRules
to perform a more scoped scan of the element with known issues:
// Suppose #element-with-contrast-issue has known violations of the color-contrast rule.
// You could scan that element with the color-contrast rule disabled...
new AxeBuilder(webDriver)
.Include("#element-with-contrast-issue")
.DisableRules("color-contrast")
.Analyze()
.Violations.Should().BeEmpty();
// ...and then also scan the rest of the page with all rules enabled.
new AxeBuilder(webDriver)
.Exclude("#element-with-contrast-issue")
.Analyze()
.Violations.Should().BeEmpty();
Migrating from Selenium.Axe
(SeleniumAxeDotnet)
This project acts as a drop-in replacement for most of the functionality from the Selenium.Axe
NuGet package (SeleniumAxeDotnet). To migrate:
- Update your test
.csproj
file's<PackageReference>
forSelenium.Axe
toDeque.AxeCore.Selenium
- Add a new
<PackageReference>
toDeque.AxeCore.Commons
at the same version number asDeque.AxeCore.Selenium
- Update all
using Selenium.Axe;
statements in your tests tousing Deque.AxeCore.Selenium;
and/orusing Deque.AxeCore.Commons;
This project does not include a replacement for Selenium.Axe
's built-in HTML report functionality. We expect it to be split out into a separate standalone library (usable from either this package or Deque.AxeCore.Playwright
) at a later date, but there is currently no direct replacement.
Finally, there are a few minor breaking changes which won't impact most Selenium.Axe
users, but which may require updates if you use certain advanced features:
- The
.Target
and.XPath
properties ofAxeResultNode
orAxeResultRelatedNode
are now strongly-typedAxeSelector
objects. MostSelenium.Axe
users do not refer to these properties explicitly, but if your tests do, you will probably want to do so via theirToString()
representations. AxeRunOptions.FrameWaitTimeInMilliseconds
was renamed toAxeRunOptions.FrameWaitTime
to match the equivalentaxe-core
API. The usage is unchanged; it still represents a value in milliseconds.AxeResult.TestEngineName
andAxeResult.TestEngineVersion
were replaced by a separateAxeTestEngine
object containingName
andVersion
properties. You will have to replace usages ofAxeResult.TestEngineName
andAxeResult.TestEngineVersion
withAxeResult.TestEngine.Name
andAxeResult.TestEngine.Version
, respectively.- The already-deprecated
AxeBuilder.Options
property was removed; replace it withWithOptions
,WithRules
,WithTags
, and/orDisableRules
. - The
AxeResult.Error
property was removed; any errors that would have appeared here are instead reported as exceptions. - The
AxeBuilder.Include
andAxeBuilder.Exclude
overloads which accept more than one parameter have changed:- If you were using it to refer to an element inside a nested frame, replace it with the
Include
/Exclude
overloads which accept anAxeSelector
://old new AxeBuilder(webDriver).Include("#some-iframe", "#element-in-nested-frame").Analyze(); // new new AxeBuilder(webDriver).Include(new AxeBuilder("#element-in-nested-frame", new List<string> { "#some-iframe" })).Analyze();
- If you were using it to refer to multiple elements in the main frame of the page under test, replace it with multiple
Include
/Exclude
calls (one per element):// old // This wasn't doing what you meant it to and may have been hiding issues; this // would have looked for #bar elements inside an iframe with selector #foo, *not* // for sibling #foo and #bar elements in the main frame of a page. new AxeBuilder(webDriver).Include("#foo", "#bar").Analyze(); // new new AxeBuilder(webDriver).Include("#foo").Include("#bar").Analyze();
- If you were using it to refer to an element inside a nested frame, replace it with the
Refer to the general axe-core-nuget CONTRIBUTING.md.
This package is distributed under the terms of the MIT License.
However, note that it has a dependency on the (MPL licensed) Deque.AxeCore.Commons
NuGet package.
This package builds on past work from the SeleniumAxeDotnet project (see NOTICE.txt). We thank all of its contributors for their work.