-
Notifications
You must be signed in to change notification settings - Fork 355
Regression Tests for APG Example Pages
Table of Contents
- What do the APG Regression Tests Test?
- Running Regression Tests Locally
- Understanding regression test results
- Writing Tests
- Test coverage report
The APG Regression Tests test the example implementations of the WAI-ARIA Authoring Practice Guides design patterns and widgets found in this repository. They are not generalizable tests of the APG design patterns or widgets because they each test one out of many possible implementations of the design pattern or widget.
The example implementations can be found under the "Example" heading for each design pattern or widget. For example, in the Checkbox design pattern description under the "example" heading, there are links to two example implementations of the Checkbox pattern:
The regression tests test the attributes and keyboard interactions as they are described on the example pages linked above. On each example page you will see tables with names similar to:
- "Keyboard Support"
- "Role, Property, State, and Tabindex Attributes"
Each row of these tables corresponds to one or more regression test. The table rows contain the attribute data-test-id
, the value of which is referenced in the name of the corresponding regression test. If a test fails, it indicates a bug in the implementation of the example widget according to the description in these tables. Be sure to check, however, that the documentation does not contain any mistakes or bugs as well!
Additionally behavior that is not included in these tables (for example, how the widget interacts with clicks) can and should be tested as well. See the section called "Writing Additional Tests".
- Download Firefox (for compatibility with selenium webdriver, use version 55 or later)
- Clone the APG git repository
- In the root directory, run:
npm install
To run all tests, run:
$ npm run regression
To run a sub set of tests, you can match on keywords in the test files:
$ npm run regression -- --match "*treeview-1a*"
NOTE: In this example any test file with the string "treeview-1a" will be run. For example, using role names like "button" or "grid" will match multiple test files. If you want to run a single file you can specify the file specificially like this:
$ npm run regression -- test/tests/treeview_treeview-2a.js
By default, Ava runs Firefox in headless mode to test (that is, no browser window is visible while running tests).
If you prefer the test to run non-headless (this means the tests will open a new browser window for every example page), you can specify the DEBUG
environment variable. In this mode, you can watch the tests happen, and add waits to slow down the test to inspect what is happening while debugging your tests (delays can be added by editing the asynchonous test function bodies with await new Promise((resolve) => setTimeout(resolve, 1000))
at appropriate points in code execution).
To run in Firefox non-headless mode:
$ DEBUG=1 npm run regression
To run using TAP format for test results instead of the default Ava testing format, specify the --tap
option:
$ npm run regression -- --tap
All test names have the format:
<test file name> › <example file location> [data-test-id="<string>"]: <test description>
For example, the test name for the test of the space
key behavior for the checkbox/checkbox-1/checkbox-1.html example looks like this:
checkbox-1 › checkbox/checkbox-1/checkbox-1.html [data-test-id="key-space"]: key SPACE selects or unselects checkbox
The test results are reported using the Ava default test format (see "Running tests" below to switch to TAP output).
When a test passes, the test name is prepended with a ✔
character.
When a test fails, it is prepended with a ✖
character. The error message is appended to the end of the test name, and a detailed report of all failures is listed at the end of the file after all of the tests run.
When a test is expected to fail, the test name is still prepended with a ✔
character but the color of the name is red (when color output is possible). After all tests are run, a summary of "expected to fail" tests are listed before the details about unexpected failures (in this example, only feed/feed.html [data-test-id="key-control-home"]: key home moves focus out of feed
expected to fail and failed). If a test passes when it is expected to fail, the output is as if it is a failed test with the error message: "Test was expected to fail, but succeeded, you should stop marking the test as failing."
Example output:
✔ feed/feed.html [data-test-id="feed-role"]: role="feed" exists (5.5s)
✔ feed/feed.html [data-test-id="feed-aria-busy"]: aria-busy attribute on feed element (2.1s)
✔ feed/feed.html [data-test-id="article-role"]: role="article" exists (2.2s)
✔ feed/feed.html [data-test-id="article-tabindex"]: tabindex="-1" on article elements (2.2s)
✔ feed/feed.html [data-test-id="article-labelledby"]: aria-labelledby set on article elements (2.2s)
✔ feed/feed.html [data-test-id="article-describedby"]: aria-describedby set on article elements (2.5s)
✔ feed/feed.html [data-test-id="article-aria-posinset"]: aria-posinset on article element (4.3s)
✖ feed/feed.html [data-test-id="article-aria-setsize"]: aria-setsize on article element Article number 1 does not have aria-setsize set correctly, after first load.
✔ feed/feed.html [data-test-id="key-page-down"]: PAGE DOWN moves focus between articles (2.1s)
✔ feed/feed.html [data-test-id="key-page-up"]: PAGE UP moves focus between articles (2.1s)
✔ feed/feed.html [data-test-id="key-control-end"]: CONTROL+END moves focus out of feed (2.1s)
✖ feed/feed.html [data-test-id="feed-aria-labelledby"]: aria-labelledby attribute on feed element Test was expected to fail, but succeeded, you should stop marking the test as failing
✔ feed/feed.html [data-test-id="key-control-home"]: key home moves focus out of feed
2 tests failed
1 known failure
feed/feed.html [data-test-id="key-control-home"]: key home moves focus out of feed
feed/feed.html [data-test-id="article-aria-setsize"]: aria-setsize on article element
/home/travis/build/w3c/aria-practices/test/tests/feed.js:157
156: for (let index = 1; index <= numArticles; index++) {
157: t.is(
158: await articles[index - 1].getAttribute('aria-setsize'),
Article number 1 does not have aria-setsize set correctly, after first load.
Difference:
- '10'
+ '20'
feed/feed.html [data-test-id="feed-aria-labelledby"]: aria-labelledby attribute on feed element
Test was expected to fail, but succeeded, you should stop marking the test as failing
If you prefer TAP output, you can pass the CLI argument -t
:
- -TAP version 13 -# slider/slider-1.html [data-test-id="slider-role"]: role="slider" on div element -ok 1 - slider/slider-1.html [data-test-id="slider-role"]: role="slider" on div element -# slider/slider-1.html [data-test-id="tabindex"]: "tabindex" set to "0" on sliders -ok 2 - slider/slider-1.html [data-test-id="tabindex"]: "tabindex" set to "0" on sliders -# slider/slider-1.html [data-test-id="aria-valuemax"]: "aria-valuemax" set to "255" on sliders -... -# slider/slider-1.html [data-test-id="key-right-arrow"]: Right arrow increases slider value by 1 -ok 7 - slider/slider-1.html [data-test-id="key-right-arrow"]: Right arrow increases slider value by 1 -... -1..14 -# tests 14 -# pass 14 -# fail 0 -
- All tests are located under:
- All helper functions are located under:
The tests use the Ava API for assertions. The tests use Selenium WebDriverJS API to run tests in the browsers. Geckodriver is the technology that receives and acts upon instructions from Selenium.
ariaTest(
'Right arrow increases slider value by 1', // Short description
'slider/slider-1.html', // Example file
'key-right-arrow', // data-test-id
async (t) => { // Declarative test
t.plan(1); // https://github.com/avajs/ava#assertion-planning
const sliders = await t.context.session.queryElements(By.css(ex.sliderSelector));
// Send 1 key to red slider
const redSlider = sliders[0];
await redSlider.sendKeys(Key.ARROW_RIGHT);
t.is(
await redSlider.getAttribute('aria-valuenow'),
'1',
'After sending 1 arrow right key to the red slider, the value of the red slider should be 1'
);
}
);
- We use Ava for test framework and assertions.
- We wrap the Ava
test
call with our ownariaTest
.- Before running the test body,
ariaTest
will:- Navigate to the the example page
- Verify the referenced data-test-id exists in the example page
- Before running the test body,
- Start each test body with a
t.plan()
- Access the assertion API through
t
: the Ava execution object.
t.is(
await redSlider.getAttribute('aria-valuenow'),
'1',
'After sending 1 arrow right key to the red slider, the value of the red slider should be 1'
);
The ariaTest API:
const ariaTest = function (desc, page, testId, body)
Declare a test for a behavior documented on and demonstrated by an
aria-practices example page.
@param {String} desc - short description of the test
@param {String} page - path to the example file
@param {String} testId - unique identifier for the documented behavior
within the demonstration page. See
attribute `data-test-id`.
@param {Function} body - script which implements the test
page
The path to the example page after example/
. ariaTest
will build the location of this file, assign it to t.context.url
, and load it into the browser session.
testId
In the example page, each tr
element in the "Keyboard Support" and "Role, Property, State, and Tabindex Attributes" tables should have a data-test-id
corresponding to this testId
value. The test will error if the data-test-id
cannot be found. The rows with that data-test-id
contains the documentation for the test contained in the ariaTest
function.
There can be one or more ariaTest
function calls that refer to a single data-test-id
in a single file. The same data-test-id
can also be found on multiple rows in the "Keyboard Support" or "Role, Property, State, and Tabindex Attributes" tables.
If you are testing some behavior that is not described in the tables of the example page (such as mouse support), use [data-test-id="test-additional-behavior"]
. If you are writing tests for an example page and a row in the table cannot be tested, then set the data-test-id
attribute to test-not-required
.
body
The body function MUST accept one argument: t
. t
is the Ava execution object that contains the Ava Test API. It also contains the selenium session for this tests file under t.context.session
, which is an object of class WebDriver (see WebDriver Class documentation).
- We use SeleniumJS to interact with and query the state of the web browser.
- Used to:
- Load URL, refresh page
- Send key presses and clicks
- Query the state of the webpage
- Send code to execute in the browser
- Selenium talks with Firefox.
- We use firefox because it has a headless option we can specify when running in the browser.
Our test framework starts a single session for each example widget and a series of tests are then run. The session is a WebDriver object and can be reached in the individual tests via the Ava execution object: t.context.session
. You use the WebDriver object to interact with the browser in various ways.
t.context.session.get(t.context.url);
t.context.session.getUrl();
t.context.session.findElement(...);
In this test suite, we also provide two functions that wrap the Selenium API for querying elements. They provide the same functionality except they throw when the query returns no element(s). The intention is to keep false positives from occurring when testing a set of elements in a loop. If you want to query to confirm nothing exists, use the SeleniumJS findElement
.
t.context.session.queryElements(...); // Wrapper for SeleniumJS findElements
t.context.session.queryElement(...); // Wrapper for SeleniumJS findElement
Selenium can also be used to send scripts to execute within the browser.
let attributeExists = await t.context.session.executeScript(
async function () {
const [selector, attribute] = arguments;
let el = document.querySelector(selector);
return el.hasAttribute('attribute');
},
elementSelector,
attribute
);
Or, to avoid race conditions using .wait
:
await t.context.session.wait(
async function () {
let newfocus = await t.context.session
.findElement(By.css(textboxSelector))
.getAttribute('aria-activedescendant');
return newfocus != originalFocus;
},
t.context.waitTime,
'Timeout waiting for "aria-activedescendant" value to change from: ' + originalFocus
);
The WebElement is used by Selenium to represent dom elements. You can send clicks or keys to WebElements and query for information about them.
const textboxElement = await t.context.session.findElement(By.css(ex.textboxSelector))
const listboxElement = await t.context.session.findElement(By.css(ex.listboxSelector))
await textboxElement.sendKeys(Key.ARROW_DOWN);
t.true(
await listboxElement.isDisplayed(),
'In listbox should display after ARROW_DOWN keypress'
);
const { Key } = require('selenium-webdriver');
await element.sendKeys(Key.ARROW_DOWN);
await element.sendKeys(Key.chord(Key.CONTROL, Key.HOME));
const { By } = require('selenium-webdriver');
const button = await t.context.session.findElement(By.css('[role="button"]'))
const example = await t.context.session.findElement(By.id('ex1'))
Each test file under test/tests/
corresponds to a single example page. Each test file contains multiple tests wrapped in an ariaTest
call. The body
argument of the ariaTest
is a declarative test of a behavior described in the example.
- All
ariaTest
test bodies should begin witht.plan(n)
wheren
is the number of assertions in the tests. Although tedious to count when writing the test, this "plan" will help prevent false positives in your tests and help you to be confident that every assertion you wrote was executed and passes. Read about Ava assertion planning here. - Test should be well commented. The order of interactions with the browser should be clear while reading the test in case a test fails and a contributor needs to reproduce the error.
- All Ava assertions should include clear error messages that can double as documentation of the test.
- All tests are asynchronous. You might need to use the selenium wait (
t.context.session.wait
) wrapper to wait on a dom change before making an assertion, as an assertion may be made faster than the dom will update. Read more about asynchronous browser testing here.
Let's say you are writing tests for the aria 1.0 combobox with auto-complete example. After writing a test for "Up Arrow" in the Listbox popup table, you notice that the aria-activedescendant
attribute fails to update properly. Visually, the focus moves from the first option to the last option after sending the "Up Arrow" key, but your test fails because it tests for the appropriate change in the aria-activedescendant
value.
What do you do? You don't want to check in a failing test -- if you do, you'd cause the CI to fail for every subsequent pull request on the repo. So instead..
-
Report the bug. For an example, see this reporting of the bug described above.
-
Use Ava's expected failing functionality. Call
ariaTest.failing
instead ofariaTest
, and make sure to leave a comment with the issue describing the bug. The test will be run on each subsequent run of Ava, but if it fails as expected the result will be ignored. If the test suddenly passes, this will be reported as a failure with the message: Test was expected to fail, but succeeded, you should stop marking the test as failing.
// This test fails due to bug: https://github.com/w3c/aria-practices/issues/821
ariaTest.failing('Test up key press with focus on listbox', exampleFile, 'listbox-key-up-arrow', async (t) => {
t.plan(3);
...
}
exampleFile
This constant refers to the example page that will be tested by this test file.
ex
All html selectors and example page specific structure should be contained in the ex
global object declared at the top of the page. This should ease the work of future maintainers. If the html structure in an example page changes, it should be easy to update the tests by primarily updating the ex
global object.
other global helper functions
Functions that will ease the tests of this particular example page. These functions assume the HTML structure of the specific page and may need to be updated if the example HTML is changed.
Tests all rely on the following imports:
const { ariaTest } = require('..');
const { By, Key } = require('selenium-webdriver');
As well as any number of helper functions from the test/util directory.
All utility functions count as one assert for t.plan()
calculations. See the file under test/util
for further documentation
- assertAriaActivedescendant (t, ariaDescendantSelector, optionsSelector, index)
- assertAriaControls (t elementSelector)
- assertAriaDescribedby (t elementSelector)
- assertAriaLabelExists (t, elementSelector)
- assertAriaLabelledby (t elementSelector)
- assertAriaRoles (t, exampleId, role, roleCount, elementTag)
- assertAriaSelectedAndActivedescendant (t, ariaDescendantSelector, optionsSelector, index)
- assertTabOrder (t, tabOrderSelectors)
- assertRovingTabindex (t, elementsSelector, key)
- assertAttributeDNE (t, selector, attribute)
- assertAttributeValues (t, elementSelector, attribute, value)
- Assertion util functions should include only one call to
t.pass()
in order for the call to count as one assertion in the calculation oft.plan()
. - Use the native node
assert
library for multiple assertions. All requirements under the section "Declarative test norms (ariaTest)" apply here as well.
Frequently, APG examples will have links, and we want to test that the appropriate keyboard press (such as 'space' or 'enter') successfully triggers the link.
We should NOT write tests that follow external links. We would like to be able to run the tests without an internet connection, or with a very unreliable one. Originally, tests that included following links resulted in flaky failures in the CI. They have all been rewritten to follow the following pattern:
const replaceExternalLink = require('../util/replaceExternalLink');
// Update url to remove external reference for dependable testing
const newUrl = t.context.url + '#test-url-change';
// The last positional argument, "index", is necessary if your css selector returns a list, for example, a list of navigational menuitems
await replaceExternalLink(t, newUrl, ex.menuitemSelector, index);
await openMenu(t);
await item.sendKeys(Key.ENTER);
t.is(
await t.context.session.getCurrentUrl(),
newUrl,
'Key ENTER on menuitem at index ' + index + 'should active the link'
);
To send a space key press, use sendKeys(Key.SPACE)
if the element is an input
or button
element, otherwise use sendKeys(' ')
. (Details)
A common subtle mistake is to accidentally omit an await
when using, e.g., queryElements()
and sendKeys()
. These methods return a promise, so each need await
, or the next thing in the script will run before the sendKeys()
promise has settled.
This is wrong:
(await t.context.queryElements(t, ex.textboxSelector))[0].sendKeys(Key.ARROW_DOWN);
This is correct:
await (await t.context.queryElements(t, ex.textboxSelector))[0].sendKeys(Key.ARROW_DOWN);
or
const textboxElement = (await t.context.queryElements(t, ex.textboxSelector))[0];
await textboxElement.sendKeys(Key.ARROW_DOWN);
Sometimes mousing over part of a widget can change the state of the widget in a way that is hard to predict or understand in an automated test. Therefore, it's recommended to not use "click" to open menus or perform other state changes during a test (unless you are explicitly testing mouse behavior). Instead, send keys to the widget according the the keyboard support table to get the widget into the correct state to test (such as opening a submenu in order to test interactions with the menuitems in the submenu).
If you want to write a test or regression test for features of the widget that are not described in the example page, you can use the "fake" data-test-id: test-additional-behavior
. For example, if you want to test the interactions a mouse user will perform with the widget, such as a click, you might write the following test.
ariaTest('click opens and closes listbox', exampleFile, 'test-additional-behavior', async (t) => {
const combobox = await t.context.session.findElement(By.css(ex.comboSelector));
const listbox = await t.context.session.findElement(By.css(ex.listboxSelector));
await combobox.click();
t.true(await listbox.isDisplayed(), 'listbox should be present after click');
await combobox.click();
t.false(await listbox.isDisplayed(), 'second click should close listbox');
t.is(await combobox.getAttribute('aria-expanded'), 'false', 'aria-expanded should be set to false after second click');
});
To run the test coverage report:
$ npm run regression-report
The test coverage report will report three things:
-
Examples without regression tests: Any example pages that have no associated test file in the
test/tests/
directory. -
Examples missing regression tests: Any example pages that have an associated test file but some tests are missing (there is not a test for every
data-test-id
found on the example page). The missingdata-test-ids
will be listed. Only missing tests in this category produce a non-zero exit code and a failure in the CI. -
Examples documentation table rows without data-test-ids: Any example pages that have rows in the "Keyboard Support" or "Role, Property, State, and Tabindex Attributes" table WITHOUT a
data-test-id
attribute.
The output of the report:
Examples without regression tests:
dialog-modal/alertdialog.html
radio/radio-1/radio-1.html
Examples missing regression tests:
feed/feed.html:
key-control-end
key-control-home
Examples documentation table rows without data-test-ids:
dialog-modal/alertdialog.html
"Keyboard Support" table(s):
Tab
Shift + Tab
Escape
Command + S
Control + S
"Attributes" table(s):
alertdialog
aria-labelledby=IDREF
aria-describedby=IDREF
aria-modal=true
alert
SUMMARY:
41 example pages found.
2 example pages have no regression tests.
1 example pages are missing approximately 2 out of approximately 572 tests.
ERROR:
Please write missing tests for this report to pass.
If there is a table row in a "Keyboard Support" or "Role, Property, State, and Tabindex Attributes" table that should NOT have a corresponding test, then use the test-not-required
data test id. For example, this attribute is used in the alert example because the attribute aria-live="assertive"
is implicit on an element with role="alert"
. Because it is implicit, it is not testable.
The report script looks for .html
files in the example directory in order to find the example pages. If a .html
file in the example directory is not an example page, then you can ignore that file by adding the file path after example
directory to the following file:
test/util/report_files/ignore_html_files
If you would prefer to ignore a whole directory (such as the landmarks
directory), you can add the path to the directory after the example
directory to the following file:
test/util/report_files/ignore_test_directories
The "Keyboard Support" and "Role, Property, State, and Tabindex Attributes" tables are discovered by looking for the def
class or attributes
class on a table
element. These classes are currently only used for these documentation tables, if this changes, the report will produce erroneous results.
As the test names are compiled during runtime, the test name are gathered by running npm run regresison
with the environment variable REGRESSION_COVERAGE_REPORT
set. In this scenario, all tests files will run without starting geckodriver or firefox and all tests will fail immediately instead of running the body
argument of avaTest
.
- Home
- About the APG TF Work
- Contributing
- Meetings
- Management and Operations Documentation
- Publication Change Logs