testaro 5.2.2 → 5.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -2
- package/create.js +83 -12
- package/data/roles.txt +62 -0
- package/high.js +2 -6
- package/package.json +1 -1
- package/run.js +7 -2
- package/runHost.js +36 -0
- package/tests/focInd.js +1 -1
- package/tests/hover.js +26 -22
- package/tests/htmlcs.js +1 -1
- package/tests/ibm.js +2 -2
- package/tests/role.js +326 -18
- package/tests/tabNav.js +3 -1
- package/validation/executors/debug.js +62 -0
- package/validation/tests/scripts/hover.json +24 -10
- package/validation/tests/scripts/role.json +15 -6
- package/validation/tests/targets/hover/bad.html +2 -2
- package/validation/tests/targets/hover/good.html +1 -1
- package/validation/tests/targets/role/bad.html +25 -3
package/README.md
CHANGED
|
@@ -89,10 +89,13 @@ A script is a JSON file with the properties:
|
|
|
89
89
|
{
|
|
90
90
|
"what": "string: description of the script",
|
|
91
91
|
"strict": "boolean: whether redirections should be treated as failures",
|
|
92
|
+
"timeLimit": "number: limit in seconds on the execution of this script",
|
|
92
93
|
"commands": "array of objects: the commands to be performed"
|
|
93
94
|
}
|
|
94
95
|
```
|
|
95
96
|
|
|
97
|
+
The `timeLimit` property is optional. If it is omitted, a default of 300 seconds (5 minutes) is set.
|
|
98
|
+
|
|
96
99
|
### Example
|
|
97
100
|
|
|
98
101
|
Here is an example of a script:
|
|
@@ -101,6 +104,7 @@ Here is an example of a script:
|
|
|
101
104
|
{
|
|
102
105
|
what: 'Test example.com with alfa',
|
|
103
106
|
strict: true,
|
|
107
|
+
timeLimit: 15,
|
|
104
108
|
commands: [
|
|
105
109
|
{
|
|
106
110
|
type: 'launch',
|
|
@@ -486,13 +490,13 @@ Relative paths must be relative to the Testaro project directory. For example, i
|
|
|
486
490
|
|
|
487
491
|
Also ensure that Testaro can read all those directories and write to `REPORTDIR`.
|
|
488
492
|
|
|
489
|
-
Place a script into `SCRIPTDIR` and, optionally, a batch into `BATCHDIR`. Each should be named `
|
|
493
|
+
Place a script into `SCRIPTDIR` and, optionally, a batch into `BATCHDIR`. Each should be named `idvalue.json`, where `idvalue` is replaced with the value of its `id` property. That value must consist of only lower-case ASCII letters and digits.
|
|
490
494
|
|
|
491
495
|
Then execute the statement `node high scriptID` or `node high scriptID batchID`, replacing `scriptID` and `batchID` with the `id` values of the script and the batch, respectively.
|
|
492
496
|
|
|
493
497
|
The `high` module will call the `runJob` function of the `create` module, which in turn will call the `handleRequest` function of the `run` module. The results will be saved in report files in the `REPORTDIR` directory.
|
|
494
498
|
|
|
495
|
-
If there is no batch, the report file will be named with a unique timestamp, suffixed with a `.json` extension. If there is a batch, then the base of each report file’s name will be the same timestamp, suffixed with `-
|
|
499
|
+
If there is no batch, the report file will be named with a unique timestamp, suffixed with a `.json` extension. If there is a batch, then the base of each report file’s name will be the same timestamp, suffixed with `-hostid`, where `hostid` is the value of the `id` property of the `host` object in the batch file. For example, if you execute `node create script01 wikis`, you might get these report files deposited into `REPORTDIR`:
|
|
496
500
|
- `enp46j-wikipedia.json`
|
|
497
501
|
- `enp45j-wiktionary.json`
|
|
498
502
|
- `enp45j-wikidata.json`
|
|
@@ -602,6 +606,16 @@ The rationales motivating the Testaro-defined tests can be found in comments wit
|
|
|
602
606
|
|
|
603
607
|
## Testing challenges
|
|
604
608
|
|
|
609
|
+
### Abnormal termination
|
|
610
|
+
|
|
611
|
+
On rare occasions a test throws an error that terminates the Node process and cannot be handled with a `try`-`catch` structure. It has been observed, for example, that the `ibm` test does this when run on the host at `https://zenythgroup.com/index` or `https://monsido.com`.
|
|
612
|
+
|
|
613
|
+
If a single process performed all of the commands in a batch-based script, the process could perform tens of thousands of commands, and such an error could stop the process at any point.
|
|
614
|
+
|
|
615
|
+
To handle this risk, Testaro processes batch-based jobs by forking a new process for each host. If such an error occurs, it crashes the child process, preventing a report for that host from being written. The parent process waits for the report to appear in the `REPORTDIR` directory until the time limit. When it fails to appear, the parent process continues to the next host.
|
|
616
|
+
|
|
617
|
+
If you are using high-level invocation, your terminal will show the standard output of the parent process and, if there is a batch, the current child process, too. If you interrupt the process with `CTRL-c`, you will send a `SIGINT` signal to the parent process, which will handle it by sending a message to the child process telling it to terminate itself, and then the parent process will terminate by skipping the remaining hosts.
|
|
618
|
+
|
|
605
619
|
### Activation
|
|
606
620
|
|
|
607
621
|
Testing to determine what happens when a control or link is activated is straightforward, except in the context of a comprehensive set of tests of a single page. There, activating a control or link can change the page or navigate away from it, interfering with the remaining planned tests of the page.
|
package/create.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
require('dotenv').config();
|
|
10
10
|
// Module to read and write files.
|
|
11
11
|
const fs = require('fs/promises');
|
|
12
|
+
const {fork} = require('child_process');
|
|
12
13
|
const {handleRequest} = require('./run');
|
|
13
14
|
// Module to convert a script and a batch to a batch-based array of scripts.
|
|
14
15
|
const {batchify} = require('./batchify');
|
|
@@ -21,10 +22,10 @@ const reportDir = process.env.REPORTDIR;
|
|
|
21
22
|
// ########## FUNCTIONS
|
|
22
23
|
|
|
23
24
|
// Runs one script and writes a report file.
|
|
24
|
-
const runHost = async (id, script
|
|
25
|
+
const runHost = async (id, script) => {
|
|
25
26
|
const report = {
|
|
26
27
|
id,
|
|
27
|
-
host,
|
|
28
|
+
host: {},
|
|
28
29
|
log: [],
|
|
29
30
|
script,
|
|
30
31
|
acts: []
|
|
@@ -35,10 +36,19 @@ const runHost = async (id, script, host = {}) => {
|
|
|
35
36
|
};
|
|
36
37
|
// Runs a file-based job and writes a report file for the script or each host.
|
|
37
38
|
exports.runJob = async (scriptID, batchID) => {
|
|
39
|
+
let healthy = true;
|
|
40
|
+
let childAlive = true;
|
|
41
|
+
process.on('SIGINT', () => {
|
|
42
|
+
console.log('ERROR: Terminal interrupted runJob');
|
|
43
|
+
healthy = false;
|
|
44
|
+
});
|
|
38
45
|
if (scriptID) {
|
|
39
46
|
try {
|
|
40
47
|
const scriptJSON = await fs.readFile(`${scriptDir}/${scriptID}.json`, 'utf8');
|
|
41
48
|
const script = JSON.parse(scriptJSON);
|
|
49
|
+
// Get the time limit of the script or, if none, set it to 5 minutes.
|
|
50
|
+
let {timeLimit} = script;
|
|
51
|
+
timeLimit = timeLimit || 300;
|
|
42
52
|
// Identify the start time and a timestamp.
|
|
43
53
|
const timeStamp = Math.floor((Date.now() - Date.UTC(2022, 1)) / 2000).toString(36);
|
|
44
54
|
// If there is a batch:
|
|
@@ -48,28 +58,89 @@ exports.runJob = async (scriptID, batchID) => {
|
|
|
48
58
|
const batchJSON = await fs.readFile(`${batchDir}/${batchID}.json`, 'utf8');
|
|
49
59
|
batch = JSON.parse(batchJSON);
|
|
50
60
|
const specs = batchify(script, batch, timeStamp);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
const batchSize = specs.length;
|
|
62
|
+
const sizedRep = `${batchSize} report${batchSize > 1 ? 's' : ''}`;
|
|
63
|
+
const timeoutHosts = [];
|
|
64
|
+
const crashHosts = [];
|
|
65
|
+
// FUNCTION DEFINITION START
|
|
66
|
+
// Recursively runs host scripts.
|
|
67
|
+
const runHosts = specs => {
|
|
68
|
+
// If any scripts remain to be run and the process has not been interrupted:
|
|
69
|
+
if (specs.length && healthy) {
|
|
70
|
+
childAlive = true;
|
|
71
|
+
// Run the first one and save the report with a host-suffixed ID.
|
|
72
|
+
const spec = specs.shift();
|
|
73
|
+
const {id, host, script} = spec;
|
|
74
|
+
const subprocess = fork(
|
|
75
|
+
'runHost', [id, JSON.stringify(script), JSON.stringify(host)],
|
|
76
|
+
{
|
|
77
|
+
detached: true,
|
|
78
|
+
stdio: [0, 1, 'ipc']
|
|
79
|
+
}
|
|
80
|
+
);
|
|
81
|
+
subprocess.on('exit', () => {
|
|
82
|
+
childAlive = false;
|
|
83
|
+
});
|
|
84
|
+
const startTime = Date.now();
|
|
85
|
+
// At 5-second intervals:
|
|
86
|
+
const reCheck = setInterval(async () => {
|
|
87
|
+
// If the user has not interrupted the process:
|
|
88
|
+
if (healthy) {
|
|
89
|
+
// If there is no need to keep checking:
|
|
90
|
+
const reportNames = await fs.readdir(reportDir);
|
|
91
|
+
const timedOut = Date.now() - startTime > 1000 * timeLimit;
|
|
92
|
+
if (timedOut || reportNames.includes(`${id}.json`) || ! childAlive) {
|
|
93
|
+
// Stop checking.
|
|
94
|
+
clearInterval(reCheck);
|
|
95
|
+
// If the cause is a timeout:
|
|
96
|
+
if (timedOut) {
|
|
97
|
+
// Add the host to the array of timed-out hosts.
|
|
98
|
+
timeoutHosts.push(id);
|
|
99
|
+
}
|
|
100
|
+
// Otherwise, if the cause is a child crash:
|
|
101
|
+
else if (! childAlive) {
|
|
102
|
+
// Add the host to the array of crashed hosts.
|
|
103
|
+
crashHosts.push(id);
|
|
104
|
+
}
|
|
105
|
+
// Run the script of the next host.
|
|
106
|
+
runHosts(specs);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Otherwise, i.e. if the user has interrupted the process:
|
|
110
|
+
else {
|
|
111
|
+
// Tell the script run to quit.
|
|
112
|
+
subprocess.send('interrupt');
|
|
113
|
+
// Stop checking.
|
|
114
|
+
clearInterval(reCheck);
|
|
115
|
+
}
|
|
116
|
+
}, 5000);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
console.log(`${sizedRep} ${timeStamp}-....json in ${process.env.REPORTDIR}`);
|
|
120
|
+
if (timeoutHosts.length) {
|
|
121
|
+
console.log(`Reports not created:\n${JSON.stringify(timeoutHosts), null, 2}`);
|
|
122
|
+
}
|
|
123
|
+
if (crashHosts.length) {
|
|
124
|
+
console.log(`Hosts crashed:\n${JSON.stringify(crashHosts), null, 2}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
// FUNCTION DEFINITION END
|
|
129
|
+
// Recursively run each host script and save the reports.
|
|
130
|
+
runHosts(specs);
|
|
58
131
|
}
|
|
59
132
|
// Otherwise, i.e. if there is no batch:
|
|
60
133
|
else {
|
|
61
134
|
// Run the script and save the result with a timestamp ID.
|
|
62
135
|
await runHost(timeStamp, script);
|
|
136
|
+
console.log(`Report ${timeStamp}.json in ${process.env.REPORTDIR}`);
|
|
63
137
|
}
|
|
64
|
-
return timeStamp;
|
|
65
138
|
}
|
|
66
139
|
catch(error) {
|
|
67
140
|
console.log(`ERROR: ${error.message}\n${error.stack}`);
|
|
68
|
-
return null;
|
|
69
141
|
}
|
|
70
142
|
}
|
|
71
143
|
else {
|
|
72
144
|
console.log('ERROR: no script specified');
|
|
73
|
-
return null;
|
|
74
145
|
}
|
|
75
146
|
};
|
package/data/roles.txt
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
input type=checkbox role=checkbox
|
|
2
|
+
td role=cell if the ancestor table element is exposed as a role=table
|
|
3
|
+
td role=gridcell if the ancestor table element is exposed as a role=grid or treegrid
|
|
4
|
+
th role=columnheader, rowheader or cell if the ancestor table element is exposed as a role=table
|
|
5
|
+
td role=columnheader, rowheader or gridcell if the ancestor table element is exposed as a role=grid or treegrid
|
|
6
|
+
dd role=definition
|
|
7
|
+
dt role=term
|
|
8
|
+
hr role=separator
|
|
9
|
+
li role=listitem
|
|
10
|
+
ol role=list
|
|
11
|
+
tr role=row
|
|
12
|
+
ul role=list
|
|
13
|
+
dfn role=term
|
|
14
|
+
nav role=navigation
|
|
15
|
+
SVG role=graphics-document
|
|
16
|
+
html role=document
|
|
17
|
+
main role=main
|
|
18
|
+
math role=math
|
|
19
|
+
menu role=list
|
|
20
|
+
aside role=complementary
|
|
21
|
+
table role=table
|
|
22
|
+
tbody role=rowgroup
|
|
23
|
+
tfoot role=rowgroup
|
|
24
|
+
thead role=rowgroup
|
|
25
|
+
button role=button
|
|
26
|
+
dialog role=dialog
|
|
27
|
+
figure role=figure
|
|
28
|
+
output role=status
|
|
29
|
+
article role=article
|
|
30
|
+
details role=group
|
|
31
|
+
section role=region if the section element has an accessible name
|
|
32
|
+
summary role=button
|
|
33
|
+
datalist role=listbox
|
|
34
|
+
fieldset role=group
|
|
35
|
+
h1 to h6 role=heading, aria-level = the number in the element's tag name
|
|
36
|
+
\
|
|
37
|
+
progress role=progressbar
|
|
38
|
+
textarea role=textbox
|
|
39
|
+
a with href role=link
|
|
40
|
+
area with href role=link
|
|
41
|
+
img with alt="" role=presentation
|
|
42
|
+
input type=image role=button
|
|
43
|
+
input type=radio role=radio
|
|
44
|
+
input type=range role=slider
|
|
45
|
+
input type=reset role=button
|
|
46
|
+
input type=button role=button
|
|
47
|
+
input type=number role=spinbutton
|
|
48
|
+
input type=submit role=button
|
|
49
|
+
img with alt="some text" role=img
|
|
50
|
+
img without an alt attribute role=img
|
|
51
|
+
input type=url with no list attribute role=textbox
|
|
52
|
+
input type=tel, with no list attribute role=textbox
|
|
53
|
+
input type=email with no list attribute role=textbox
|
|
54
|
+
input type=search, with no list attribute role=searchbox
|
|
55
|
+
form If the form element has an accessible name: role=form. Otherwise, no corresponding role.
|
|
56
|
+
input type=text or with a missing or invalid type, with no list attribute role=textbox
|
|
57
|
+
select (with a multiple attribute or a size attribute having value greater than 1) role=listbox
|
|
58
|
+
select (with NO multiple attribute and NO size attribute having value greater than 1) role=combobox
|
|
59
|
+
option element that is in a list of options or that represents a suggestion in a datalist role=option
|
|
60
|
+
footer If not a descendant of an article, aside, main, nav or section element, or an element with role=article, complementary, main, navigation or region then role=contentinfo. Otherwise no corresponding role.
|
|
61
|
+
header If not a descendant of an article, aside, main, nav or section element, or an element with role=article, complementary, main, navigation or region then role=banner. Otherwise no corresponding role
|
|
62
|
+
input type=text, search, tel, url, email, or with a missing or invalid type, with a list attribute role=combobox
|
package/high.js
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
/*
|
|
2
2
|
high.js
|
|
3
3
|
Invokes Testaro with the high-level method.
|
|
4
|
-
Usage example: node high
|
|
4
|
+
Usage example: node high tp12 weborgs
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const {runJob} = require('./create');
|
|
8
8
|
const scriptID = process.argv[2];
|
|
9
9
|
const batchID = process.argv[3];
|
|
10
|
-
|
|
11
|
-
const timeStamp = await runJob(scriptID, batchID);
|
|
12
|
-
console.log(`Reports in ${process.env.REPORTDIR}; ID base ${timeStamp}`);
|
|
13
|
-
};
|
|
14
|
-
run(scriptID, batchID);
|
|
10
|
+
runJob(scriptID, batchID);
|
package/package.json
CHANGED
package/run.js
CHANGED
|
@@ -633,6 +633,12 @@ const wait = ms => {
|
|
|
633
633
|
};
|
|
634
634
|
// Recursively performs the acts in a report.
|
|
635
635
|
const doActs = async (report, actIndex, page) => {
|
|
636
|
+
process.on('message', message => {
|
|
637
|
+
if (message === 'interrupt') {
|
|
638
|
+
console.log('ERROR: Terminal interrupted doActs');
|
|
639
|
+
process.exit();
|
|
640
|
+
}
|
|
641
|
+
});
|
|
636
642
|
const {acts} = report;
|
|
637
643
|
// If any more commands are to be performed:
|
|
638
644
|
if (actIndex > -1 && actIndex < acts.length) {
|
|
@@ -1310,8 +1316,7 @@ const doScript = async (report) => {
|
|
|
1310
1316
|
const injectLaunches = acts => {
|
|
1311
1317
|
let injectMore = true;
|
|
1312
1318
|
while (injectMore) {
|
|
1313
|
-
const injectIndex = acts.findIndex(
|
|
1314
|
-
(act, index) =>
|
|
1319
|
+
const injectIndex = acts.findIndex((act, index) =>
|
|
1315
1320
|
index < acts.length - 1
|
|
1316
1321
|
&& act.type === 'test'
|
|
1317
1322
|
&& acts[index + 1].type === 'test'
|
package/runHost.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/*
|
|
2
|
+
runHost.js
|
|
3
|
+
Runs a host job and writes a report file.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ########## IMPORTS
|
|
7
|
+
|
|
8
|
+
// Module to keep secrets.
|
|
9
|
+
require('dotenv').config();
|
|
10
|
+
// Module to read and write files.
|
|
11
|
+
const fs = require('fs/promises');
|
|
12
|
+
const {handleRequest} = require('./run');
|
|
13
|
+
|
|
14
|
+
// ########## CONSTANTS
|
|
15
|
+
const reportDir = process.env.REPORTDIR;
|
|
16
|
+
|
|
17
|
+
// ########## FUNCTIONS
|
|
18
|
+
|
|
19
|
+
// Runs one script and writes a report file.
|
|
20
|
+
const runHost = async (id, scriptJSON, hostJSON) => {
|
|
21
|
+
const report = {
|
|
22
|
+
id,
|
|
23
|
+
host: JSON.parse(hostJSON),
|
|
24
|
+
log: [],
|
|
25
|
+
script: JSON.parse(scriptJSON),
|
|
26
|
+
acts: []
|
|
27
|
+
};
|
|
28
|
+
await handleRequest(report);
|
|
29
|
+
const reportJSON = JSON.stringify(report, null, 2);
|
|
30
|
+
await fs.writeFile(`${reportDir}/${id}.json`, reportJSON);
|
|
31
|
+
process.disconnect();
|
|
32
|
+
process.exit();
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// ########## OPERATION
|
|
36
|
+
runHost(... process.argv.slice(2));
|
package/tests/focInd.js
CHANGED
|
@@ -138,7 +138,7 @@ exports.reporter = async (page, revealAll, allowedDelay, withItems) => {
|
|
|
138
138
|
const hasIndicator
|
|
139
139
|
= hasDiffOutline
|
|
140
140
|
|| hasDiffBorder
|
|
141
|
-
|| diff('
|
|
141
|
+
|| diff('boxShadow')
|
|
142
142
|
|| diff('fontSize')
|
|
143
143
|
|| diff('fontStyle')
|
|
144
144
|
|| diff('textDecorationLine')
|
package/tests/hover.js
CHANGED
|
@@ -28,10 +28,6 @@
|
|
|
28
28
|
value in the same location being the target.
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
-
// CONSTANTS
|
|
32
|
-
|
|
33
|
-
const data = {};
|
|
34
|
-
|
|
35
31
|
// FUNCTIONS
|
|
36
32
|
|
|
37
33
|
// Samples a population and returns the sample.
|
|
@@ -62,14 +58,13 @@ const textOf = async (element, limit) => {
|
|
|
62
58
|
return text.trim().replace(/\s*/sg, '').slice(0, limit);
|
|
63
59
|
};
|
|
64
60
|
// Recursively reports impacts of hovering over triggers.
|
|
65
|
-
const find = async (withItems, page, region, sample, popRatio) => {
|
|
61
|
+
const find = async (data, withItems, page, region, sample, popRatio) => {
|
|
66
62
|
// If any potential triggers remain:
|
|
67
63
|
if (sample.length) {
|
|
68
64
|
// Identify the first of them.
|
|
69
65
|
const firstTrigger = sample[0];
|
|
70
66
|
const tagNameJSHandle = await firstTrigger.getProperty('tagName')
|
|
71
67
|
.catch(error => {
|
|
72
|
-
console.log(`ERROR getting trigger tag name (${error.message})`);
|
|
73
68
|
return '';
|
|
74
69
|
});
|
|
75
70
|
if (tagNameJSHandle) {
|
|
@@ -89,7 +84,7 @@ const find = async (withItems, page, region, sample, popRatio) => {
|
|
|
89
84
|
root = rootJSHandle.asElement();
|
|
90
85
|
}
|
|
91
86
|
// Identify all the descendants of the root.
|
|
92
|
-
const preDescendants = await root.$$('
|
|
87
|
+
const preDescendants = await root.$$(':visible');
|
|
93
88
|
// Identify their opacities.
|
|
94
89
|
const preOpacities = await page.evaluate(elements => elements.map(
|
|
95
90
|
element => window.getComputedStyle(element).opacity
|
|
@@ -102,8 +97,11 @@ const find = async (withItems, page, region, sample, popRatio) => {
|
|
|
102
97
|
});
|
|
103
98
|
// Repeatedly seeks impacts.
|
|
104
99
|
const getImpacts = async (interval, triesLeft) => {
|
|
100
|
+
// If the allowed trial count has not yet been exhausted:
|
|
105
101
|
if (triesLeft--) {
|
|
106
|
-
|
|
102
|
+
// Get the collection of descendants of the root.
|
|
103
|
+
const postDescendants = await root.$$(':visible');
|
|
104
|
+
// Identify the prior descandants of the root still in existence.
|
|
107
105
|
const remainerIndexes = await page.evaluate(args => {
|
|
108
106
|
const preDescendants = args[0];
|
|
109
107
|
const postDescendants = args[1];
|
|
@@ -112,6 +110,7 @@ const find = async (withItems, page, region, sample, popRatio) => {
|
|
|
112
110
|
.filter(index => index > -1);
|
|
113
111
|
return remainerIndexes;
|
|
114
112
|
}, [preDescendants, postDescendants]);
|
|
113
|
+
// Get the count of elements added by the hover event.
|
|
115
114
|
const additionCount = postDescendants.length - remainerIndexes.length;
|
|
116
115
|
const removalCount = preDescendants.length - remainerIndexes.length;
|
|
117
116
|
const remainers = [];
|
|
@@ -158,7 +157,10 @@ const find = async (withItems, page, region, sample, popRatio) => {
|
|
|
158
157
|
position: {
|
|
159
158
|
x: 0,
|
|
160
159
|
y: 0
|
|
161
|
-
}
|
|
160
|
+
},
|
|
161
|
+
timeout: 500,
|
|
162
|
+
force: true,
|
|
163
|
+
noWaitAfter: true
|
|
162
164
|
});
|
|
163
165
|
// Wait for any delayed and/or slowed hover reaction.
|
|
164
166
|
await page.waitForTimeout(200);
|
|
@@ -198,7 +200,7 @@ const find = async (withItems, page, region, sample, popRatio) => {
|
|
|
198
200
|
}
|
|
199
201
|
}
|
|
200
202
|
// Process the remaining potential triggers.
|
|
201
|
-
await find(withItems, page, region, sample.slice(1), popRatio);
|
|
203
|
+
await find(data, withItems, page, region, sample.slice(1), popRatio);
|
|
202
204
|
}
|
|
203
205
|
};
|
|
204
206
|
// Performs the hover test and reports results.
|
|
@@ -206,16 +208,18 @@ exports.reporter = async (
|
|
|
206
208
|
page, headSize = 0, headSampleSize = -1, tailSampleSize = -1, withItems
|
|
207
209
|
) => {
|
|
208
210
|
// Initialize the result.
|
|
209
|
-
data
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
211
|
+
const data = {
|
|
212
|
+
totals: {
|
|
213
|
+
triggers: 0,
|
|
214
|
+
headTriggers: 0,
|
|
215
|
+
tailTriggers: 0,
|
|
216
|
+
impactTriggers: 0,
|
|
217
|
+
additions: 0,
|
|
218
|
+
removals: 0,
|
|
219
|
+
opacityChanges: 0,
|
|
220
|
+
opacityImpact: 0,
|
|
221
|
+
unhoverables: 0
|
|
222
|
+
}
|
|
219
223
|
};
|
|
220
224
|
// If details are to be reported:
|
|
221
225
|
if (withItems) {
|
|
@@ -252,10 +256,10 @@ exports.reporter = async (
|
|
|
252
256
|
const tailSample = tailSampleSize === -1 ? tailTriggers : getSample(tailTriggers, tailSampleSize);
|
|
253
257
|
// Find and document the impacts.
|
|
254
258
|
if (headSample.length) {
|
|
255
|
-
await find(withItems, page, 'head', headSample, headTriggerCount / headSample.length);
|
|
259
|
+
await find(data, withItems, page, 'head', headSample, headTriggerCount / headSample.length);
|
|
256
260
|
}
|
|
257
261
|
if (tailSample.length) {
|
|
258
|
-
await find(withItems, page, 'tail', tailSample, tailTriggerCount / tailSample.length);
|
|
262
|
+
await find(data, withItems, page, 'tail', tailSample, tailTriggerCount / tailSample.length);
|
|
259
263
|
}
|
|
260
264
|
// Round the reported totals.
|
|
261
265
|
Object.keys(data.totals).forEach(key => {
|
package/tests/htmlcs.js
CHANGED
package/tests/ibm.js
CHANGED
|
@@ -104,7 +104,7 @@ exports.reporter = async (page, withItems, withNewContent) => {
|
|
|
104
104
|
result.content = await doTest(typeContent, withItems, timeLimit);
|
|
105
105
|
if (result.content.prevented) {
|
|
106
106
|
result.prevented = true;
|
|
107
|
-
console.log(
|
|
107
|
+
console.log(`ERROR: Getting ibm test report from page timed out at ${timeLimit} seconds`);
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
// If a test with new content is to be performed:
|
|
@@ -114,7 +114,7 @@ exports.reporter = async (page, withItems, withNewContent) => {
|
|
|
114
114
|
result.url = await doTest(typeContent, withItems, timeLimit);
|
|
115
115
|
if (result.url.prevented) {
|
|
116
116
|
result.prevented = true;
|
|
117
|
-
console.log(
|
|
117
|
+
console.log(`ERROR: Getting ibm test report from URL timed out at ${timeLimit} seconds`);
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
await close();
|
package/tests/role.js
CHANGED
|
@@ -120,6 +120,264 @@ exports.reporter = async page => await page.$eval('body', body => {
|
|
|
120
120
|
'treegrid',
|
|
121
121
|
'treeitem',
|
|
122
122
|
]);
|
|
123
|
+
// Implicit roles
|
|
124
|
+
const implicitRoles = {
|
|
125
|
+
article: 'article',
|
|
126
|
+
aside: 'complementary',
|
|
127
|
+
button: 'button',
|
|
128
|
+
datalist: 'listbox',
|
|
129
|
+
dd: 'definition',
|
|
130
|
+
details: 'group',
|
|
131
|
+
dfn: 'term',
|
|
132
|
+
dialog: 'dialog',
|
|
133
|
+
dt: 'term',
|
|
134
|
+
fieldset: 'group',
|
|
135
|
+
figure: 'figure',
|
|
136
|
+
hr: 'separator',
|
|
137
|
+
html: 'document',
|
|
138
|
+
li: 'listitem',
|
|
139
|
+
main: 'main',
|
|
140
|
+
math: 'math',
|
|
141
|
+
menu: 'list',
|
|
142
|
+
nav: 'navigation',
|
|
143
|
+
ol: 'list',
|
|
144
|
+
output: 'status',
|
|
145
|
+
progress: 'progressbar',
|
|
146
|
+
summary: 'button',
|
|
147
|
+
SVG: 'graphics-document',
|
|
148
|
+
table: 'table',
|
|
149
|
+
tbody: 'rowgroup',
|
|
150
|
+
textarea: 'textbox',
|
|
151
|
+
tfoot: 'rowgroup',
|
|
152
|
+
thead: 'rowgroup',
|
|
153
|
+
tr: 'row',
|
|
154
|
+
ul: 'list'
|
|
155
|
+
};
|
|
156
|
+
const implicitAttributes = {
|
|
157
|
+
a: [
|
|
158
|
+
{
|
|
159
|
+
role: 'link',
|
|
160
|
+
attributes: {
|
|
161
|
+
href: /./
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
],
|
|
165
|
+
area: [
|
|
166
|
+
{
|
|
167
|
+
role: 'link',
|
|
168
|
+
attributes: {
|
|
169
|
+
href: /./
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
],
|
|
173
|
+
h1: [
|
|
174
|
+
{
|
|
175
|
+
role: 'heading',
|
|
176
|
+
attributes: {
|
|
177
|
+
'aria-level': /^1$/
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
],
|
|
181
|
+
h2: [
|
|
182
|
+
{
|
|
183
|
+
role: 'heading',
|
|
184
|
+
attributes: {
|
|
185
|
+
'aria-level': /^2$/
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
role: 'heading',
|
|
190
|
+
attributes: {
|
|
191
|
+
'aria-level': false
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
],
|
|
195
|
+
h3: [
|
|
196
|
+
{
|
|
197
|
+
role: 'heading',
|
|
198
|
+
attributes: {
|
|
199
|
+
'aria-level': /^3$/
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
],
|
|
203
|
+
h4: [
|
|
204
|
+
{
|
|
205
|
+
role: 'heading',
|
|
206
|
+
attributes: {
|
|
207
|
+
'aria-level': /^4$/
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
],
|
|
211
|
+
h5: [
|
|
212
|
+
{
|
|
213
|
+
role: 'heading',
|
|
214
|
+
attributes: {
|
|
215
|
+
'aria-level': /^5$/
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
],
|
|
219
|
+
h6: [
|
|
220
|
+
{
|
|
221
|
+
role: 'heading',
|
|
222
|
+
attributes: {
|
|
223
|
+
'aria-level': /^6$/
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
],
|
|
227
|
+
input: [
|
|
228
|
+
{
|
|
229
|
+
role: 'checkbox',
|
|
230
|
+
attributes: {
|
|
231
|
+
type: /^checkbox$/
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
role: 'button',
|
|
236
|
+
attributes: {
|
|
237
|
+
type: /^(?:button|image|reset|submit)$/
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
role: 'combobox',
|
|
242
|
+
attributes: {
|
|
243
|
+
type: /^(?:email|search|tel|text|url)$/,
|
|
244
|
+
list: true
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
role: 'combobox',
|
|
249
|
+
attributes: {
|
|
250
|
+
type: false,
|
|
251
|
+
list: true
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
role: 'radio',
|
|
256
|
+
attributes: {
|
|
257
|
+
type: /^radio$/
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
role: 'searchbox',
|
|
262
|
+
attributes: {
|
|
263
|
+
type: /^search$/,
|
|
264
|
+
list: false
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
role: 'slider',
|
|
269
|
+
attributes: {
|
|
270
|
+
type: /^range$/
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
role: 'spinbutton',
|
|
275
|
+
attributes: {
|
|
276
|
+
type: /^number$/
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
role: 'textbox',
|
|
281
|
+
attributes: {
|
|
282
|
+
type: /^(?:email|tel|text|url)$/,
|
|
283
|
+
list: false
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
role: 'textbox',
|
|
288
|
+
attributes: {
|
|
289
|
+
type: false,
|
|
290
|
+
list: false
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
role: 'checkbox',
|
|
295
|
+
attributes: {
|
|
296
|
+
type: /^checkbox$/
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
role: 'checkbox',
|
|
301
|
+
attributes: {
|
|
302
|
+
type: /^checkbox$/
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
img: [
|
|
307
|
+
{
|
|
308
|
+
role: 'presentation',
|
|
309
|
+
attributes: {
|
|
310
|
+
alt: /^$/
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
role: 'img',
|
|
315
|
+
attributes: {
|
|
316
|
+
alt: /./
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
role: 'img',
|
|
321
|
+
attributes: {
|
|
322
|
+
alt: false
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
],
|
|
326
|
+
select: [
|
|
327
|
+
{
|
|
328
|
+
role: 'listbox',
|
|
329
|
+
attributes: {
|
|
330
|
+
multiple: true
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
role: 'listbox',
|
|
335
|
+
attributes: {
|
|
336
|
+
size: /^(?:[2-9]|[1-9]\d+)$/
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
role: 'combobox',
|
|
341
|
+
attributes: {
|
|
342
|
+
multiple: false,
|
|
343
|
+
size: false
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
role: 'combobox',
|
|
348
|
+
attributes: {
|
|
349
|
+
multiple: false,
|
|
350
|
+
size: /^1$/
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
]
|
|
354
|
+
};
|
|
355
|
+
// Array of th and td elements with redundant roles.
|
|
356
|
+
const redundantCells = [];
|
|
357
|
+
// FUNCTIONS
|
|
358
|
+
const dataInit = (data, tagName, role) => {
|
|
359
|
+
if (! data.tagNames[tagName]) {
|
|
360
|
+
data.tagNames[tagName] = {};
|
|
361
|
+
}
|
|
362
|
+
if (! data.tagNames[tagName][role]) {
|
|
363
|
+
data.tagNames[tagName][role] = {
|
|
364
|
+
bad: 0,
|
|
365
|
+
redundant: 0
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
const tallyTableRedundancy = (elements, okRoles, tagName) => {
|
|
370
|
+
elements.forEach(element => {
|
|
371
|
+
const role = element.getAttribute('role');
|
|
372
|
+
if (okRoles.includes(role)) {
|
|
373
|
+
dataInit(data, tagName, role);
|
|
374
|
+
data.redundantRoleElements++;
|
|
375
|
+
data.tagNames[tagName][role].redundant++;
|
|
376
|
+
redundantCells.push(element);
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
};
|
|
380
|
+
// OPERATION
|
|
123
381
|
// Remove the deprecated roles from the non-abstract roles.
|
|
124
382
|
goodRoles.forEach(role => {
|
|
125
383
|
if (badRoles.has(role)) {
|
|
@@ -128,34 +386,84 @@ exports.reporter = async page => await page.$eval('body', body => {
|
|
|
128
386
|
});
|
|
129
387
|
// Identify all elements with role attributes.
|
|
130
388
|
const roleElements = Array.from(body.querySelectorAll('[role]'));
|
|
131
|
-
// Identify those with roles that are either deprecated or invalid.
|
|
132
|
-
const bads = roleElements.filter(element => {
|
|
133
|
-
const role = element.getAttribute('role');
|
|
134
|
-
return badRoles.has(role) || ! goodRoles.has(role);
|
|
135
|
-
});
|
|
136
389
|
// Initialize the result.
|
|
137
390
|
const data = {
|
|
138
391
|
roleElements: roleElements.length,
|
|
139
|
-
badRoleElements:
|
|
392
|
+
badRoleElements: 0,
|
|
393
|
+
redundantRoleElements: 0,
|
|
140
394
|
tagNames: {}
|
|
141
395
|
};
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
396
|
+
// Identify the th and td elements with redundant roles.
|
|
397
|
+
const gridHeaders = Array.from(
|
|
398
|
+
document.body.querySelectorAll('table[role=grid] th, table[role=treegrid] th')
|
|
399
|
+
);
|
|
400
|
+
const gridCells = Array.from(
|
|
401
|
+
document.body.querySelectorAll('table[role=grid] td, table[role=treegrid] td')
|
|
402
|
+
);
|
|
403
|
+
const tableHeaders = Array.from(
|
|
404
|
+
document.body.querySelectorAll('table[role=table] th, table:not([role]) th')
|
|
405
|
+
);
|
|
406
|
+
const tableCells = Array.from(
|
|
407
|
+
document.body.querySelectorAll('table[role=table] td, table:not([role]) td')
|
|
408
|
+
);
|
|
409
|
+
tallyTableRedundancy(gridHeaders, ['columnheader', 'rowheader', 'gridcell'], 'TH');
|
|
410
|
+
tallyTableRedundancy(gridCells, ['gridcell'], 'TD');
|
|
411
|
+
tallyTableRedundancy(tableHeaders, ['columnheader', 'rowheader', 'cell'], 'TH');
|
|
412
|
+
tallyTableRedundancy(tableCells, ['cell'], 'TD');
|
|
413
|
+
// Identify the additional elements with redundant roles and bad roles.
|
|
414
|
+
roleElements.filter(element => ! redundantCells.includes(element)).forEach(element => {
|
|
146
415
|
const role = element.getAttribute('role');
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
416
|
+
const tagName = element.tagName;
|
|
417
|
+
// If the role is not absolutely valid:
|
|
418
|
+
if (! goodRoles.has(role)) {
|
|
419
|
+
// If it is bad or redundant:
|
|
420
|
+
if (badRoles.has(role)) {
|
|
421
|
+
dataInit(data, tagName, role);
|
|
422
|
+
const lcTagName = tagName.toLowerCase();
|
|
423
|
+
// If it is simply redundant:
|
|
424
|
+
if (role === implicitRoles[lcTagName]) {
|
|
425
|
+
data.redundantRoleElements++;
|
|
426
|
+
data.tagNames[tagName][role].redundant++;
|
|
427
|
+
}
|
|
428
|
+
// Otherwise, if it is attributionally redundant:
|
|
429
|
+
else if (
|
|
430
|
+
implicitAttributes[lcTagName] && implicitAttributes[lcTagName].some(
|
|
431
|
+
criterion => role === criterion.role && Object.keys(criterion.attributes).every(
|
|
432
|
+
attributeName => {
|
|
433
|
+
const rule = criterion.attributes[attributeName];
|
|
434
|
+
const exists = element.hasAttribute(attributeName);
|
|
435
|
+
const value = exists ? element.getAttribute(attributeName) : null;
|
|
436
|
+
if (rule === true) {
|
|
437
|
+
return exists;
|
|
438
|
+
}
|
|
439
|
+
else if (rule === false) {
|
|
440
|
+
return ! exists;
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
return rule.test(value);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
)
|
|
447
|
+
)
|
|
448
|
+
) {
|
|
449
|
+
data.redundantRoleElements++;
|
|
450
|
+
data.tagNames[tagName][role].redundant++;
|
|
451
|
+
}
|
|
452
|
+
// Otherwise, i.e. if it is absolutely invalid:
|
|
453
|
+
else {
|
|
454
|
+
data.badRoleElements++;
|
|
455
|
+
data.tagNames[tagName][role].bad++;
|
|
456
|
+
}
|
|
151
457
|
}
|
|
458
|
+
// Otherwise, i.e. if it is absolutely invalid:
|
|
152
459
|
else {
|
|
153
|
-
|
|
460
|
+
// Add the facts to the result.
|
|
461
|
+
data.badRoleElements++;
|
|
462
|
+
dataInit(data, tagName, role);
|
|
463
|
+
data.tagNames[tagName][role].bad++;
|
|
154
464
|
}
|
|
155
465
|
}
|
|
156
|
-
else {
|
|
157
|
-
data.tagNames[tagName] = {[role]: 1};
|
|
158
|
-
}
|
|
159
466
|
});
|
|
467
|
+
// Return the result.
|
|
160
468
|
return {result: data};
|
|
161
469
|
});
|
package/tests/tabNav.js
CHANGED
|
@@ -103,7 +103,9 @@ exports.reporter = async (page, withItems) => {
|
|
|
103
103
|
});
|
|
104
104
|
})
|
|
105
105
|
.catch(error => {
|
|
106
|
-
console.log(
|
|
106
|
+
console.log(
|
|
107
|
+
`ERROR clicking tab element ${itemData.text} (${error.message.replace(/\n.+/s, '')})`
|
|
108
|
+
);
|
|
107
109
|
pressed = false;
|
|
108
110
|
});
|
|
109
111
|
// Increment the counts of navigations and key navigations.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// app.js
|
|
2
|
+
// Validator for Testaro tests.
|
|
3
|
+
|
|
4
|
+
const fs = require('fs').promises;
|
|
5
|
+
const {handleRequest} = require(`${__dirname}/../../run`);
|
|
6
|
+
const validateTests = async () => {
|
|
7
|
+
const totals = {
|
|
8
|
+
attempts: 0,
|
|
9
|
+
successes: 0
|
|
10
|
+
};
|
|
11
|
+
const scriptFileNames = await fs.readdir(`${__dirname}/../tests/scripts`);
|
|
12
|
+
for (const scriptFileName of ['hover.json']) {
|
|
13
|
+
const rawScriptJSON = await fs
|
|
14
|
+
.readFile(`${__dirname}/../tests/scripts/${scriptFileName}`, 'utf8');
|
|
15
|
+
const scriptJSON = rawScriptJSON
|
|
16
|
+
.replace(/__targets__/g, `file://${__dirname}/../tests/targets`);
|
|
17
|
+
const script = JSON.parse(scriptJSON);
|
|
18
|
+
const report = {script};
|
|
19
|
+
report.log = [];
|
|
20
|
+
report.acts = [];
|
|
21
|
+
await handleRequest(report);
|
|
22
|
+
const {log, acts} = report;
|
|
23
|
+
if (log.length === 2 && log[1].event === 'endTime' && /^\d{4}-.+$/.test(log[1].value)) {
|
|
24
|
+
console.log('Success: Log has been correctly populated');
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
console.log('Failure: Log empty or invalid');
|
|
28
|
+
console.log(JSON.stringify(log, null, 2));
|
|
29
|
+
}
|
|
30
|
+
if (
|
|
31
|
+
acts.length === script.commands.length
|
|
32
|
+
&& acts.every(
|
|
33
|
+
act => act.type && act.type === 'test'
|
|
34
|
+
? act.result && act.result.failureCount !== undefined
|
|
35
|
+
: true
|
|
36
|
+
)
|
|
37
|
+
) {
|
|
38
|
+
totals.attempts++;
|
|
39
|
+
totals.successes++;
|
|
40
|
+
console.log('Success: Reports have been correctly populated');
|
|
41
|
+
if (acts.every(
|
|
42
|
+
act => act.type === 'test' ? act.result.failureCount === 0 : true
|
|
43
|
+
)) {
|
|
44
|
+
totals.attempts++;
|
|
45
|
+
totals.successes++;
|
|
46
|
+
console.log('Success: No failures');
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
totals.attempts++;
|
|
50
|
+
console.log('Failure: At least one test has at least one failure');
|
|
51
|
+
console.log(JSON.stringify(acts, null, 2));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
totals.attempts++;
|
|
56
|
+
console.log('Failure: Reports empty or invalid');
|
|
57
|
+
console.log(JSON.stringify(acts, null, 2));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
console.log(`Grand totals: attempts ${totals.attempts}, successes ${totals.successes}`);
|
|
61
|
+
};
|
|
62
|
+
validateTests();
|
|
@@ -16,12 +16,19 @@
|
|
|
16
16
|
"type": "test",
|
|
17
17
|
"which": "hover",
|
|
18
18
|
"what": "hover",
|
|
19
|
-
"
|
|
19
|
+
"headSize": 3,
|
|
20
|
+
"headSampleSize": 3,
|
|
21
|
+
"tailSampleSize": 30,
|
|
22
|
+
"withItems": true,
|
|
20
23
|
"expect": [
|
|
21
|
-
["totals.triggers", "=",
|
|
22
|
-
["totals.
|
|
23
|
-
["totals.
|
|
24
|
-
["totals.
|
|
24
|
+
["totals.triggers", "=", 2],
|
|
25
|
+
["totals.headTriggers", "=", 2],
|
|
26
|
+
["totals.tailTriggers", "=", 0],
|
|
27
|
+
["totals.impactTriggers", "=", 0],
|
|
28
|
+
["totals.additions", "=", 0],
|
|
29
|
+
["totals.removals", "=", 0],
|
|
30
|
+
["totals.opacityChanges", "=", 0],
|
|
31
|
+
["totals.opacityImpact", "=", 0],
|
|
25
32
|
["totals.unhoverables", "=", 0]
|
|
26
33
|
]
|
|
27
34
|
},
|
|
@@ -34,12 +41,19 @@
|
|
|
34
41
|
"type": "test",
|
|
35
42
|
"which": "hover",
|
|
36
43
|
"what": "hover",
|
|
37
|
-
"
|
|
44
|
+
"headSize": 3,
|
|
45
|
+
"headSampleSize": 3,
|
|
46
|
+
"tailSampleSize": 30,
|
|
47
|
+
"withItems": true,
|
|
38
48
|
"expect": [
|
|
39
|
-
["totals.triggers", "=",
|
|
40
|
-
["totals.
|
|
41
|
-
["totals.
|
|
42
|
-
["totals.
|
|
49
|
+
["totals.triggers", "=", 4],
|
|
50
|
+
["totals.headTriggers", "=", 3],
|
|
51
|
+
["totals.tailTriggers", "=", 1],
|
|
52
|
+
["totals.impactTriggers", "=", 2],
|
|
53
|
+
["totals.additions", "=", 3],
|
|
54
|
+
["totals.removals", "=", 0],
|
|
55
|
+
["totals.opacityChanges", "=", 1],
|
|
56
|
+
["totals.opacityImpact", "=", 1],
|
|
43
57
|
["totals.unhoverables", "=", 1]
|
|
44
58
|
]
|
|
45
59
|
}
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"what": "role",
|
|
19
19
|
"expect": [
|
|
20
20
|
["roleElements", "=", 1],
|
|
21
|
-
["badRoleElements", "=", 0]
|
|
21
|
+
["badRoleElements", "=", 0],
|
|
22
|
+
["redundantRoleElements", "=", 0]
|
|
22
23
|
]
|
|
23
24
|
},
|
|
24
25
|
{
|
|
@@ -31,11 +32,19 @@
|
|
|
31
32
|
"which": "role",
|
|
32
33
|
"what": "role",
|
|
33
34
|
"expect": [
|
|
34
|
-
["roleElements", "=",
|
|
35
|
-
["badRoleElements", "=",
|
|
36
|
-
["
|
|
37
|
-
["tagNames.SECTION.
|
|
38
|
-
["tagNames.
|
|
35
|
+
["roleElements", "=", 9],
|
|
36
|
+
["badRoleElements", "=", 6],
|
|
37
|
+
["redundantRoleElements", "=", 3],
|
|
38
|
+
["tagNames.SECTION.main.bad", "=", 1],
|
|
39
|
+
["tagNames.SECTION.section.bad", "=", 1],
|
|
40
|
+
["tagNames.H2.heading.bad", "=", 0],
|
|
41
|
+
["tagNames.H2.heading.redundant", "=", 1],
|
|
42
|
+
["tagNames.H3.heading.bad", "=", 2],
|
|
43
|
+
["tagNames.H3.heading.redundant", "=", 1],
|
|
44
|
+
["tagNames.INPUT.spinbutton.redundant", "=", 1],
|
|
45
|
+
["tagNames.INPUT.textbox.bad", "=", 1],
|
|
46
|
+
["tagNames.INPUT.textbox.redundant", "=", 0],
|
|
47
|
+
["tagNames.INPUT.combobox.bad", "=", 1]
|
|
39
48
|
]
|
|
40
49
|
}
|
|
41
50
|
]
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
<body>
|
|
10
10
|
<main>
|
|
11
11
|
<h1>Page with deviant hover behavior</h1>
|
|
12
|
-
<p>This page contains a link to <a href="https://en.wikipedia.org" onmouseover="document.getElementById('translucent').style.opacity = 1">information</a
|
|
12
|
+
<p>This page contains a link to <a href="https://en.wikipedia.org" onmouseover="document.getElementById('translucent').style.opacity = 1">information</a> and a <button type="button" onmouseover="document.getElementById('hiddenP').style.display = 'block'">bothersome button</button>. Both elements can be hovered over.</p>
|
|
13
13
|
<p id="hiddenP" style="display: none">The button, when hovered over, makes this paragraph, and therefore this <a href="https://en.wikipedia.org/wiki/Web_accessibility">link on web accessibility</a> and this <button type="button">new button</button>, visible.</p>
|
|
14
14
|
<p id="translucent" style="opacity: 0.4">The first link, when hovered over, changes the opacity of this paragraph from 0.4 to 1. That indirectly changes the opacity of this <span>word</span>, too.</p>
|
|
15
|
-
<p>The small button is mostly covered by a large one
|
|
15
|
+
<p>The small button is mostly covered by a large one here, preventing the small button from receiving a hover event.</p>
|
|
16
16
|
<p style="position: relative"><button style="position: absolute; left: 10rem">button</button><button style="position: absolute; left: 11rem; top: -0.5rem; font-size: x-large">bigger button</button></p>
|
|
17
17
|
</main>
|
|
18
18
|
</body>
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
<body>
|
|
10
10
|
<main>
|
|
11
11
|
<h1>Page with standard hover behavior</h1>
|
|
12
|
-
<p>This paragraph contains a link to <a href="https://en.wikipedia.org">information</a
|
|
12
|
+
<p>This paragraph contains a link to <a href="https://en.wikipedia.org">information</a> and a <button type="button">button</button>. Both of them can be hovered over, and hovering over either of them does not trigger any change in content.</p>
|
|
13
13
|
</main>
|
|
14
14
|
</body>
|
|
15
15
|
</html>
|
|
@@ -11,15 +11,37 @@
|
|
|
11
11
|
<h1>Page with deviant role elements</h1>
|
|
12
12
|
<section role="section">
|
|
13
13
|
<h2>Abstraction</h2>
|
|
14
|
-
<p>This section has an abstract role.</p>
|
|
14
|
+
<p>This section has an abstract role, so it is bad.</p>
|
|
15
15
|
</section>
|
|
16
16
|
<section>
|
|
17
17
|
<h2 role="heading">Redundancy</h2>
|
|
18
|
-
<p>
|
|
18
|
+
<p>The heading of this section has a redundant role, with an inferred level.</p>
|
|
19
|
+
<section>
|
|
20
|
+
<h3 role="heading" aria-level="3">Redundancy with explicit level</h3>
|
|
21
|
+
<p>The heading of this section has a redundant role because the stated level is 3 and the implicit level is 3.</p>
|
|
22
|
+
</section>
|
|
23
|
+
<section>
|
|
24
|
+
<h3 role="heading">Failed redundancy for missing level</h3>
|
|
25
|
+
<p>The heading of this section has a heading that fails redundancy because the inferred level is 2 but the explicit level is 3. So the element role is bad.</p>
|
|
26
|
+
</section>
|
|
27
|
+
<section>
|
|
28
|
+
<h3 role="heading" aria-level="4">Failed redundancy for wrong level</h3>
|
|
29
|
+
<p>The heading of this section has a heading that fails redundancy because the attributional level is 4 but the explicit level is 3. So the element role is bad.</p>
|
|
30
|
+
</section>
|
|
19
31
|
</section>
|
|
20
32
|
<section>
|
|
21
33
|
<h2>Conflict</h2>
|
|
22
|
-
<p>The parent section of these sections has an unnecessary explicit role
|
|
34
|
+
<p>The parent section of these sections has an unnecessary explicit role <code>main</code>, instead of a <code>main</code> element with the same implicit role.</p>
|
|
35
|
+
</section>
|
|
36
|
+
<section>
|
|
37
|
+
<h2>Attributes</h2>
|
|
38
|
+
<h3>Valid redundancy</h3>
|
|
39
|
+
<p>This paragraph contains an input for a number. It has an implicit <code>spinbutton</code> role and the same redundant explicit role. <input type="number" role="spinbutton"></p>
|
|
40
|
+
<h3>Failed redundancy</h3>
|
|
41
|
+
<p>This paragraph contains an input for a number. It has an implicit <code>spinbutton</code> role but an explicit <code>textbox</code> role, so its role is bad. <input type="number" role="textbox"></p>
|
|
42
|
+
<h3>Attribute existence</h3>
|
|
43
|
+
<datalist id="options"><option value="a"></option><option value="b"></option></datalist>
|
|
44
|
+
<p>This input is identical, except that it omits the <code>list</code> attribute. That makes its implicit role <code>textbox</code>, so the explicit role of <code>combobox</code> is bad. <input role="combobox"></p>
|
|
23
45
|
</section>
|
|
24
46
|
</section>
|
|
25
47
|
</body>
|