testaro 12.3.2 → 12.4.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.
Files changed (40) hide show
  1. package/README.md +7 -6
  2. package/actSpecs.js +6 -0
  3. package/dupAtt/temp.html +19 -0
  4. package/package.json +2 -1
  5. package/run.js +6 -4
  6. package/tests/attVal.js +2 -2
  7. package/tests/dupAtt.js +54 -0
  8. package/validation/tests/jobs/allHidden.json +1 -1
  9. package/validation/tests/jobs/attVal.json +60 -0
  10. package/validation/tests/jobs/autocomplete.json +4 -4
  11. package/validation/tests/jobs/bulk.json +1 -1
  12. package/validation/tests/jobs/docType.json +1 -1
  13. package/validation/tests/jobs/dupAtt.json +51 -0
  14. package/validation/tests/jobs/elements.json +1 -1
  15. package/validation/tests/jobs/embAc.json +1 -1
  16. package/validation/tests/jobs/filter.json +1 -1
  17. package/validation/tests/jobs/focAll.json +1 -1
  18. package/validation/tests/jobs/focInd.json +1 -1
  19. package/validation/tests/jobs/focOp.json +1 -1
  20. package/validation/tests/jobs/focVis.json +1 -1
  21. package/validation/tests/jobs/hover.json +1 -1
  22. package/validation/tests/jobs/labClash.json +1 -1
  23. package/validation/tests/jobs/linkTo.json +1 -1
  24. package/validation/tests/jobs/linkUl.json +1 -1
  25. package/validation/tests/jobs/menuNav.json +1 -1
  26. package/validation/tests/jobs/miniText.json +1 -1
  27. package/validation/tests/jobs/motion.json +1 -1
  28. package/validation/tests/jobs/nonTable.json +1 -1
  29. package/validation/tests/jobs/radioSet.json +1 -1
  30. package/validation/tests/jobs/role.json +1 -1
  31. package/validation/tests/jobs/styleDiff.json +1 -1
  32. package/validation/tests/jobs/tabNav.json +1 -1
  33. package/validation/tests/jobs/textNodes.json +1 -1
  34. package/validation/tests/jobs/title.json +1 -1
  35. package/validation/tests/jobs/titledEl.json +1 -1
  36. package/validation/tests/jobs/zIndex.json +1 -1
  37. package/validation/tests/targets/attVal/bad.html +15 -0
  38. package/validation/tests/targets/attVal/good.html +15 -0
  39. package/validation/tests/targets/dupAtt/bad.html +19 -0
  40. package/validation/tests/targets/dupAtt/good.html +16 -0
package/README.md CHANGED
@@ -31,7 +31,9 @@ Testaro uses:
31
31
  - [Playwright](https://playwright.dev/) to launch browsers, perform user actions in them, and perform tests
32
32
  - [pixelmatch](https://www.npmjs.com/package/pixelmatch) to measure motion
33
33
 
34
- Testaro includes some of its own accessibility tests. In addition, it performs the tests of these tools:
34
+ Testaro includes some of its own accessibility tests. Some of them are derived from tests performed by the [BBC Accessibility Standards Checker](https://github.com/bbc/bbc-a11y).
35
+
36
+ In addition, Testaro performs tests of these tools:
35
37
  - [accessibility-checker](https://www.npmjs.com/package/accessibility-checker) (the IBM Equal Access Accessibility Checker)
36
38
  - [alfa](https://alfa.siteimprove.com/) (Siteimprove alfa)
37
39
  - [axe-playwright](https://www.npmjs.com/package/axe-playwright) (Deque Axe-core)
@@ -42,8 +44,6 @@ Testaro includes some of its own accessibility tests. In addition, it performs t
42
44
  - [Tenon](https://tenon.io/documentation/what-tenon-tests.php) (Level Access)
43
45
  - [WAVE API](https://wave.webaim.org/api/) (WebAIM WAVE)
44
46
 
45
- Some of the Testaro tests are derived from tests performed by the [BBC Accessibility Standards Checker](https://github.com/bbc/bbc-a11y).
46
-
47
47
  As of this version, the counts of tests of the tools referenced above were:
48
48
  - Alfa: 103
49
49
  - Axe-core: 138
@@ -54,9 +54,8 @@ As of this version, the counts of tests of the tools referenced above were:
54
54
  - QualWeb core: 121
55
55
  - Tenon: 180
56
56
  - WAVE: 110
57
- - subtotal: 1327
58
- - Testaro tests: 24
59
- - grand total: 1351
57
+ - Testaro: 29
58
+ - total: 1356
60
59
 
61
60
  ## Quasi-tests
62
61
 
@@ -537,6 +536,8 @@ The third item in each array, if there are 3 items in the array, is the criterio
537
536
 
538
537
  A typical use for an `expect` property is checking the correctness of a Testaro test. Thus, the validation jobs in the `validation/tests/jobs` directory all contain `test` acts with `expect` properties. See the “Validation” section below.
539
538
 
539
+ When a `test` act has an `expect` property, the result for that act has an `expectations` property reporting whether the expectations were satisfied. The value of `expectations` is an array of objects, one object per expectation. Each object includes a `property` property identifying the expectation, and a `passed` property with `true` or `false` value reporting whether the expectation was satisfied. If applicable, it also has other properties identifying what was expected and what was actually reported.
540
+
540
541
  ## Execution
541
542
 
542
543
  ### Introduction
package/actSpecs.js CHANGED
@@ -187,6 +187,12 @@ exports.actSpecs = {
187
187
  rules: [false, 'array', 'areNumbers', 'rule numbers (e.g., 25), if not all']
188
188
  }
189
189
  ],
190
+ dupAtt: [
191
+ 'Perform a dupAtt test',
192
+ {
193
+ withItems: [true, 'boolean', '', 'itemize']
194
+ }
195
+ ],
190
196
  elements: [
191
197
  'Perform an elements test',
192
198
  {
@@ -0,0 +1,19 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en-US">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Test page</title>
6
+ <meta name="description" content="tester">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1">
8
+ </head>
9
+ <body>
10
+ <main>
11
+ <h1>Test page</h1>
12
+ <p><button id="violator" width="4rem" width="10rem">Submit</button></p>
13
+ <p
14
+ aria-label="large"
15
+ aria-label="small"
16
+ >A paragraph</p>
17
+ </main>
18
+ </body>
19
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "12.3.2",
3
+ "version": "12.4.0",
4
4
  "description": "Automation of accessibility testing",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -35,6 +35,7 @@
35
35
  "accessibility-checker": "*",
36
36
  "axe-playwright": "*",
37
37
  "dotenv": "*",
38
+ "node-fetch": "*",
38
39
  "pixelmatch": "*",
39
40
  "playwright": "*"
40
41
  },
package/run.js CHANGED
@@ -39,6 +39,7 @@ const tests = {
39
39
  bulk: 'count of visible elements',
40
40
  continuum: 'Level Access Continuum, community edition',
41
41
  docType: 'document without a doctype property',
42
+ dupAtt: 'elements with duplicate attributes',
42
43
  elements: 'data on specified elements',
43
44
  embAc: 'active elements embedded in links or buttons',
44
45
  filter: 'filter styles on elements',
@@ -498,7 +499,7 @@ const isTrue = (object, specs) => {
498
499
  }
499
500
  // Otherwise, i.e. if the expectation is of a property value:
500
501
  else if (specs.length === 3) {
501
- // Determine whether the expectation was fulfilled.
502
+ // Return whether the expectation was fulfilled.
502
503
  const relation = specs[1];
503
504
  const criterion = specs[2];
504
505
  let satisfied;
@@ -518,16 +519,17 @@ const isTrue = (object, specs) => {
518
519
  satisfied = actual !== criterion;
519
520
  }
520
521
  else if (relation === 'i') {
521
- satisfied = actual.includes(criterion);
522
+ satisfied = typeof actual === 'string' && actual.includes(criterion);
522
523
  }
523
524
  else if (relation === '!i') {
524
- satisfied = ! actual.includes(criterion);
525
+ satisfied = typeof actual === 'string' && ! actual.includes(criterion);
525
526
  }
526
527
  return [actual, satisfied];
527
528
  }
528
529
  // Otherwise, i.e. if the specifications are invalid:
529
530
  else {
530
- //
531
+ // Return this.
532
+ return [null, false];
531
533
  }
532
534
  };
533
535
  // Adds a wait error result to an act.
package/tests/attVal.js CHANGED
@@ -5,13 +5,13 @@
5
5
  exports.reporter = async (page, attributeName, areLicit, values, withItems) => {
6
6
  // Identify the elements that have the specified attribute with illicit values.
7
7
  const badAttributeData = await page.evaluate(
8
- args => {
8
+ async args => {
9
9
  const attributeName = args[0];
10
10
  // Whether the values are the licit or the illicit ones.
11
11
  const areLicit = args[1];
12
12
  const values = args[2];
13
13
  // Returns the text of an element.
14
- const textOf = async (element, limit) => {
14
+ const textOf = (element, limit) => {
15
15
  let text = element.textContent;
16
16
  text = text.trim() || element.innerHTML;
17
17
  return text.replace(/\s+/sg, ' ').replace(/<>&/g, '').slice(0, limit);
@@ -0,0 +1,54 @@
1
+ /*
2
+ dupAtt.js
3
+ This test reports duplicate attributes in the source of a document.
4
+ */
5
+
6
+ // ########## IMPORTS
7
+
8
+ // Module to make HTTP requests.
9
+ const fetch = require('node-fetch');
10
+ // Module to process files.
11
+ const fs = require('fs/promises');
12
+
13
+ // ########## FUNCTIONS
14
+
15
+ // Reports failures.
16
+ exports.reporter = async (page, withItems) => {
17
+ // Initialize the data.
18
+ const data = {total: 0};
19
+ if (withItems) {
20
+ data.items = [];
21
+ }
22
+ // Get the page.
23
+ const url = page.url();
24
+ const scheme = url.replace(/:.+/, '');
25
+ let rawPage;
26
+ if (scheme === 'file') {
27
+ const filePath = url.slice(7);
28
+ rawPage = await fs.readFile(filePath, 'utf8');
29
+ }
30
+ else {
31
+ rawPage = await fetch(url);
32
+ }
33
+ // Extract its elements, in a uniform format.
34
+ const elements = rawPage
35
+ .match(/<[^/<>]+>/g)
36
+ .map(element => element.slice(1, -1).trim().replace(/\s*=\s*/g, '='))
37
+ .map(element => element.replace(/\s+/g, ' '));
38
+ // For each element:
39
+ elements.forEach(element => {
40
+ // Identify its attributes.
41
+ const attributes = element.split(' ').slice(1).map(attVal => attVal.replace(/=.+/, ''));
42
+ // If any is duplicated:
43
+ const attSet = new Set(attributes);
44
+ if (attSet.size < attributes.length) {
45
+ // Add this to the data.
46
+ data.total++;
47
+ if (withItems) {
48
+ data.items.push(element);
49
+ }
50
+ }
51
+ });
52
+ // Return the data.
53
+ return {result: data};
54
+ };
@@ -1,5 +1,5 @@
1
1
  {
2
- "id": "allHiddenVal",
2
+ "id": "allHidden",
3
3
  "what": "validation of allHidden test",
4
4
  "strict": true,
5
5
  "timeLimit": 20,
@@ -0,0 +1,60 @@
1
+ {
2
+ "id": "attVal",
3
+ "what": "validation of attVal test",
4
+ "strict": true,
5
+ "timeLimit": 20,
6
+ "acts": [
7
+ {
8
+ "type": "launch",
9
+ "which": "chromium",
10
+ "what": "usual browser"
11
+ },
12
+ {
13
+ "type": "url",
14
+ "which": "__targets__/attVal/good.html",
15
+ "what": "page with permitted attribute values"
16
+ },
17
+ {
18
+ "type": "test",
19
+ "which": "attVal",
20
+ "attributeName": "lang",
21
+ "areLicit": true,
22
+ "values": ["en-US", "de-CH"],
23
+ "what": "attribute values",
24
+ "withItems": true,
25
+ "expect": [
26
+ ["total", "=", 0],
27
+ ["items.1"]
28
+ ]
29
+ },
30
+ {
31
+ "type": "url",
32
+ "which": "__targets__/attVal/bad.html",
33
+ "what": "page with permitted attribute values"
34
+ },
35
+ {
36
+ "type": "test",
37
+ "which": "attVal",
38
+ "attributeName": "lang",
39
+ "areLicit": false,
40
+ "values": ["en", "de"],
41
+ "what": "attribute values",
42
+ "withItems": true,
43
+ "expect": [
44
+ ["total", "=", 2],
45
+ ["items.0.tagName", "=", "HTML"],
46
+ ["items.0.attributeValue", "=", "en"],
47
+ ["items.1.tagName", "=", "SPAN"],
48
+ ["items.1.attributeValue", "=", "de"],
49
+ ["items.1.textStart", "i", "Veloparkieren"]
50
+ ]
51
+ }
52
+ ],
53
+ "sources": {
54
+ "script": "",
55
+ "host": {},
56
+ "requester": ""
57
+ },
58
+ "creationTime": "2023-04-19T12:34:00",
59
+ "timeStamp": "00000"
60
+ }
@@ -1,5 +1,5 @@
1
1
  {
2
- "id": "autocompleteVal",
2
+ "id": "autocomplete",
3
3
  "what": "validation of autocomplete test",
4
4
  "strict": true,
5
5
  "timeLimit": 20,
@@ -20,7 +20,7 @@
20
20
  "what": "autocomplete attributes for name and email",
21
21
  "withItems": true,
22
22
  "expect": [
23
- ["totals", "=", 0],
23
+ ["total", "=", 0],
24
24
  ["items.1"]
25
25
  ]
26
26
  },
@@ -35,7 +35,7 @@
35
35
  "what": "autocomplete attributes for name and email",
36
36
  "withItems": true,
37
37
  "expect": [
38
- ["totals", "=", 4],
38
+ ["total", "=", 4],
39
39
  ["items.1.0", "=", "family-name"],
40
40
  ["items.2.1", "=", "Your email address"]
41
41
  ]
@@ -46,6 +46,6 @@
46
46
  "host": {},
47
47
  "requester": ""
48
48
  },
49
- "creationTime": "2003-04-16T21:06:00",
49
+ "creationTime": "2023-04-16T21:06:00",
50
50
  "timeStamp": "00000"
51
51
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "id": "bulkVal",
2
+ "id": "bulk",
3
3
  "what": "validation of bulk test",
4
4
  "strict": true,
5
5
  "timeLimit": 20,
@@ -1,5 +1,5 @@
1
1
  {
2
- "id": "docTypeVal",
2
+ "id": "docType",
3
3
  "what": "validation of docType test",
4
4
  "strict": true,
5
5
  "timeLimit": 20,
@@ -0,0 +1,51 @@
1
+ {
2
+ "id": "dupAtt",
3
+ "what": "validation of dupAtt test",
4
+ "strict": true,
5
+ "timeLimit": 20,
6
+ "acts": [
7
+ {
8
+ "type": "launch",
9
+ "which": "chromium",
10
+ "what": "usual browser"
11
+ },
12
+ {
13
+ "type": "url",
14
+ "which": "__targets__/dupAtt/good.html",
15
+ "what": "page without duplicate attributes"
16
+ },
17
+ {
18
+ "type": "test",
19
+ "which": "dupAtt",
20
+ "what": "elements with duplicate attributes",
21
+ "withItems": true,
22
+ "expect": [
23
+ ["total", "=", 0],
24
+ ["items.1"]
25
+ ]
26
+ },
27
+ {
28
+ "type": "url",
29
+ "which": "__targets__/dupAtt/bad.html",
30
+ "what": "page with duplicate attributes"
31
+ },
32
+ {
33
+ "type": "test",
34
+ "which": "dupAtt",
35
+ "what": "elements with duplicate attributes",
36
+ "withItems": true,
37
+ "expect": [
38
+ ["total", "=", 2],
39
+ ["items.0", "=", "p class=\"narrow\" id=\"daParagraph\" class=\"wide\""],
40
+ ["items.1", "i", "large"]
41
+ ]
42
+ }
43
+ ],
44
+ "sources": {
45
+ "script": "",
46
+ "host": {},
47
+ "requester": ""
48
+ },
49
+ "creationTime": "2023-04-18T11:02:00",
50
+ "timeStamp": "00000"
51
+ }
@@ -1,5 +1,5 @@
1
1
  {
2
- "id": "elementsVal",
2
+ "id": "elements",
3
3
  "what": "validation of elements test",
4
4
  "strict": true,
5
5
  "timeLimit": 20,
@@ -1,5 +1,5 @@
1
1
  {
2
- "id": "embAcVal",
2
+ "id": "embAc",
3
3
  "what": "validation of embAc test",
4
4
  "strict": true,
5
5
  "timeLimit": 20,
@@ -1,5 +1,5 @@
1
1
  {
2
- "id": "filterVal",
2
+ "id": "filter",
3
3
  "what": "validation of filter test",
4
4
  "strict": true,
5
5
  "timeLimit": 20,
@@ -1,5 +1,5 @@
1
1
  {
2
- "id": "focAllVal",
2
+ "id": "focAll",
3
3
  "what": "validation of focAll test",
4
4
  "strict": true,
5
5
  "timeLimit": 20,
@@ -1,5 +1,5 @@
1
1
  {
2
- "id": "focIndVal",
2
+ "id": "focInd",
3
3
  "what": "validation of focInd test",
4
4
  "strict": true,
5
5
  "timeLimit": 20,
@@ -1,5 +1,5 @@
1
1
  {
2
- "id": "focOpVal",
2
+ "id": "focOp",
3
3
  "what": "validation of focOp test",
4
4
  "strict": true,
5
5
  "timeLimit": 20,
@@ -1,5 +1,5 @@
1
1
  {
2
- "id": "focVisVal",
2
+ "id": "focVis",
3
3
  "what": "validation of focVis test",
4
4
  "strict": true,
5
5
  "timeLimit": 20,
@@ -1,5 +1,5 @@
1
1
  {
2
- "id": "hoverVal",
2
+ "id": "hover",
3
3
  "what": "validation of hover test",
4
4
  "strict": true,
5
5
  "timeLimit": 20,
@@ -1,5 +1,5 @@
1
1
  {
2
- "id": "labClashVal",
2
+ "id": "labClash",
3
3
  "what": "validation of labClash test",
4
4
  "strict": true,
5
5
  "timeLimit": 20,
@@ -1,5 +1,5 @@
1
1
  {
2
- "id": "linkToVal",
2
+ "id": "linkTo",
3
3
  "what": "validation of linkTo test",
4
4
  "strict": true,
5
5
  "timeLimit": 20,
@@ -1,5 +1,5 @@
1
1
  {
2
- "id": "linkUlVal",
2
+ "id": "linkUl",
3
3
  "what": "validation of linkUl test",
4
4
  "strict": true,
5
5
  "timeLimit": 20,
@@ -1,5 +1,5 @@
1
1
  {
2
- "id": "menuNavVal",
2
+ "id": "menuNav",
3
3
  "what": "validation of menuNav test",
4
4
  "strict": true,
5
5
  "timeLimit": 20,
@@ -1,5 +1,5 @@
1
1
  {
2
- "id": "miniTextVal",
2
+ "id": "miniText",
3
3
  "what": "validation of miniText test",
4
4
  "strict": true,
5
5
  "timeLimit": 20,
@@ -1,5 +1,5 @@
1
1
  {
2
- "id": "motionVal",
2
+ "id": "motion",
3
3
  "what": "validation of motion test",
4
4
  "strict": true,
5
5
  "timeLimit": 20,
@@ -1,5 +1,5 @@
1
1
  {
2
- "id": "nonTableVal",
2
+ "id": "nonTable",
3
3
  "what": "validation of nonTable test",
4
4
  "strict": true,
5
5
  "timeLimit": 20,
@@ -1,5 +1,5 @@
1
1
  {
2
- "id": "radioSetVal",
2
+ "id": "radioSet",
3
3
  "what": "validation of radioSet test",
4
4
  "strict": true,
5
5
  "timeLimit": 20,
@@ -1,5 +1,5 @@
1
1
  {
2
- "id": "roleVal",
2
+ "id": "role",
3
3
  "what": "validation of role test",
4
4
  "strict": true,
5
5
  "timeLimit": 20,
@@ -1,5 +1,5 @@
1
1
  {
2
- "id": "styleDiffVal",
2
+ "id": "styleDiff",
3
3
  "what": "validation of styleDiff test",
4
4
  "strict": true,
5
5
  "timeLimit": 20,
@@ -1,5 +1,5 @@
1
1
  {
2
- "id": "tabNavVal",
2
+ "id": "tabNav",
3
3
  "what": "validation of tabNav test",
4
4
  "strict": true,
5
5
  "timeLimit": 20,
@@ -1,5 +1,5 @@
1
1
  {
2
- "id": "elementsVal",
2
+ "id": "elements",
3
3
  "what": "validation of elements test",
4
4
  "strict": true,
5
5
  "timeLimit": 20,
@@ -1,5 +1,5 @@
1
1
  {
2
- "id": "titleVal",
2
+ "id": "title",
3
3
  "what": "validation of title test",
4
4
  "strict": true,
5
5
  "timeLimit": 20,
@@ -1,5 +1,5 @@
1
1
  {
2
- "id": "titledElVal",
2
+ "id": "titledEl",
3
3
  "what": "validation of titledEl test",
4
4
  "strict": true,
5
5
  "timeLimit": 20,
@@ -1,5 +1,5 @@
1
1
  {
2
- "id": "zIndexVal",
2
+ "id": "zIndex",
3
3
  "what": "validation of zIndex test",
4
4
  "strict": true,
5
5
  "timeLimit": 20,
@@ -0,0 +1,15 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Page with illicit attribute values</title>
6
+ <meta name="description" content="tester">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1">
8
+ </head>
9
+ <body>
10
+ <main>
11
+ <h1>Page with illicit attribute values</h1>
12
+ <p><q><span lang="de">Veloparkieren nicht gestattet</span></q> is Swiss German, so it should be marked up with the <code>de-ch</code> language tag.</p>
13
+ </main>
14
+ </body>
15
+ </html>
@@ -0,0 +1,15 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en-US">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Page with licit attribute values</title>
6
+ <meta name="description" content="tester">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1">
8
+ </head>
9
+ <body>
10
+ <main>
11
+ <h1>Page with illicit attribute values</h1>
12
+ <p><q><span>lang="de-CH">Veloparkieren nicht gestattet</span></q> is Swiss German, so it is marked up with the <code>de-CH</code> language tag.</p>
13
+ </main>
14
+ </body>
15
+ </html>
@@ -0,0 +1,19 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en-US">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Page with duplicate attributes</title>
6
+ <meta name="description" content="tester">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1">
8
+ </head>
9
+ <body>
10
+ <main>
11
+ <h1>Page with duplicate attributes</h1>
12
+ <p class="narrow" id="daParagraph" class="wide">Submit</p>
13
+ <p><button
14
+ aria-label="large"
15
+ aria-label="small"
16
+ >A paragraph</button></p>
17
+ </main>
18
+ </body>
19
+ </html>
@@ -0,0 +1,16 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en-US">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Page without duplicate attributes</title>
6
+ <meta name="description" content="tester">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1">
8
+ </head>
9
+ <body>
10
+ <main>
11
+ <h1>Page without duplicate attributes</h1>
12
+ <p class="narrow" id="okParagraph" lang="es-US">Buscar</p>
13
+ <p><button id="p" aria-label="A body of text">A paragraph</button></p>
14
+ </main>
15
+ </body>
16
+ </html>