testaro 5.17.2 → 5.18.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "5.17.2",
3
+ "version": "5.18.0",
4
4
  "description": "Automation of accessibility testing",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/tests/hover.js CHANGED
@@ -1,17 +1,22 @@
1
1
  /*
2
2
  hover
3
3
  This test reports unexpected impacts of hovering. The effects include additions and removals
4
- of visible elements, opacity changes, and unhoverable elements. The elements that are
5
- subjected to hovering (called “triggers”) are the Playwright-visible elements that have 'A',
6
- 'BUTTON', or 'LI' tag names or have 'onmouseenter' or 'onmouseover' attributes. When such an
7
- element is hovered over, the test examines the impacts on descendants of the great grandparents
8
- of the elements with tag names 'A' and 'BUTTON', grandparents of elements with tag name 'LI',
9
- and otherwise the descendants of the elements themselves. Four impacts are counted: (1) an
10
- element is added or becomes visible, (2) an element is removed or becomes invisible, (3) the
11
- opacity of an element changes, and (4) the element is a descendant of an element whose opacity
4
+ of visible elements, opacity changes, unhoverable elements, and suppression of hover indication.
5
+ The elements that are subjected to hovering (called “triggers”) are the Playwright-visible
6
+ elements that have 'A', 'BUTTON', or 'LI' tag names or have 'onmouseenter' or 'onmouseover'
7
+ attributes.
8
+
9
+ When such an element is hovered over, the test examines the impacts on descendants of the great
10
+ grandparents of the elements with tag names 'A' and 'BUTTON', grandparents of elements with tag
11
+ name 'LI', and otherwise the descendants of the elements themselves. Four impacts are counted:
12
+ (1) an element is added or becomes visible, (2) an element is removed or becomes invisible, (3)
13
+ the opacity of an element changes, and (4) the element is a descendant of an element whose opacity
12
14
  changes. The test checks up to 4 times for hovering impacts at intervals of 0.3 second.
13
15
 
14
- Despite this delay, the test can make the execution time practical by randomly sampling triggers
16
+ The test also examines how the hover event is indicated to the user with the mouse cursor and with
17
+ style changes.
18
+
19
+ Despite the delay, the test can make the execution time practical by randomly sampling triggers
15
20
  instead of hovering over all of them. When sampling is performed, the results may vary from one
16
21
  execution to another. Because hover impacts typically occur near the beginning of a page, the
17
22
  probability of the inclusion of a trigger in a sample decreases with the index of the trigger.
@@ -30,7 +35,10 @@ let hasTimedOut = false;
30
35
 
31
36
  // FUNCTIONS
32
37
 
33
- // Samples a population and returns the sample and each member’s sampling probability.
38
+ // Gets the probability of a trigger being sampled.
39
+ const samProb = (index, popSize, sampleRatio) =>
40
+ sampleRatio * (1 + Math.sin(Math.PI * index / popSize + Math.PI / 2));
41
+ // Samples the trigger population and returns the sample and each member’s sampling probability.
34
42
  const getSample = (population, sampleSize) => {
35
43
  const popSize = population.length;
36
44
  // If the sample is at least as large as the population:
@@ -43,13 +51,9 @@ const getSample = (population, sampleSize) => {
43
51
  // Force the sample size to be an integer and at least 1.
44
52
  sampleSize = Math.floor(Math.max(1, sampleSize));
45
53
  const sampleRatio = sampleSize / popSize;
46
- // FUNCTION DEFINITION START
47
- // Gets the probability of a trigger being sampled.
48
- const samProb = index => (1 + Math.sin(Math.PI * index / popSize + Math.PI / 2)) * sampleRatio;
49
- // FUNCTION DEFINITION END
50
54
  // Get the sample.
51
55
  const sample = population.map((trigger, index) => {
52
- const itemProb = samProb(index);
56
+ const itemProb = samProb(index, popSize, sampleRatio);
53
57
  return [trigger, itemProb];
54
58
  }).filter(pair => pair[1] > Math.random());
55
59
  return sample;
@@ -59,9 +63,84 @@ const getSample = (population, sampleSize) => {
59
63
  const textOf = async (element, limit) => {
60
64
  let text = await element.textContent();
61
65
  text = text.trim() || await element.innerHTML();
62
- return text.trim().replace(/\s*/sg, '').slice(0, limit);
66
+ return text.trim().replace(/\s+/sg, ' ').slice(0, limit);
67
+ };
68
+ // Returns the impacts of hovering over a sampled trigger.
69
+ const getImpacts = async (
70
+ interval, triesLeft, root, page, preDescendants, preOpacities
71
+ ) => {
72
+ // If the allowed trial count has not yet been exhausted:
73
+ if (triesLeft-- && ! hasTimedOut) {
74
+ // Get the collection of descendants of the root.
75
+ const postDescendants = await root.$$(':visible');
76
+ // Identify the prior descendants of the root still in existence.
77
+ const remainerIndexes = await page.evaluate(args => {
78
+ const preDescendants = args[0];
79
+ const postDescendants = args[1];
80
+ const remainerIndexes = preDescendants
81
+ .map((element, index) => postDescendants.includes(element) ? index : -1)
82
+ .filter(index => index > -1);
83
+ return remainerIndexes;
84
+ }, [preDescendants, postDescendants]);
85
+ // Get the impacts of the hover event.
86
+ const additions = postDescendants.length - remainerIndexes.length;
87
+ const removals = preDescendants.length - remainerIndexes.length;
88
+ const remainers = [];
89
+ for (const index of remainerIndexes) {
90
+ remainers.push({
91
+ element: preDescendants[index],
92
+ preOpacity: preOpacities[index],
93
+ postOpacity: await page.evaluate(
94
+ element => window.getComputedStyle(element).opacity, preDescendants[index]
95
+ )
96
+ });
97
+ }
98
+ const opacityChangers = remainers
99
+ .filter(remainer => remainer.postOpacity !== remainer.preOpacity);
100
+ const opacityImpact = opacityChangers
101
+ ? await page.evaluate(changers => changers.reduce(
102
+ (total, current) => total + current.element.querySelectorAll('*').length, 0
103
+ ), opacityChangers)
104
+ : 0;
105
+ // If there are any impacts:
106
+ if (additions || removals || opacityChangers.length) {
107
+ // Return them.
108
+ return {
109
+ additions,
110
+ removals,
111
+ opacityChanges: opacityChangers.length,
112
+ opacityImpact
113
+ };
114
+ }
115
+ // Otherwise, i.e. if there are no impacts:
116
+ else {
117
+ // Try again.
118
+ return await new Promise(resolve => {
119
+ setTimeout(() => {
120
+ resolve(getImpacts(interval, triesLeft, root, page, preDescendants, preOpacities));
121
+ }, interval);
122
+ });
123
+ }
124
+ }
125
+ // Otherwise, i.e. if the allowed trial count has been exhausted:
126
+ else {
127
+ // Report non-impact.
128
+ return null;
129
+ }
63
130
  };
64
- // Recursively reports impacts of hovering over triggers.
131
+ // Returns the hover-related style properties of a trigger.
132
+ const getHoverStyles = async (page, element) => await page.evaluate(
133
+ element => {
134
+ const {cursor, outline, color, backgroundColor} = window.getComputedStyle(element);
135
+ return {
136
+ cursor: cursor.replace(/^.+, */, ''),
137
+ outline,
138
+ color,
139
+ backgroundColor
140
+ };
141
+ }, element
142
+ );
143
+ // Recursively adds estimated and itemized impacts of hovering over triggers to data.
65
144
  const find = async (data, withItems, page, sample) => {
66
145
  // If any triggers remain and the test has not timed out:
67
146
  if (sample.length && ! hasTimedOut) {
@@ -69,6 +148,8 @@ const find = async (data, withItems, page, sample) => {
69
148
  try {
70
149
  // Identify the first trigger and its sampling probability.
71
150
  const firstTrigger = sample[0];
151
+ const onmouseenter = await firstTrigger[0].getAttribute('onmouseenter');
152
+ const onmouseover = await firstTrigger[0].getAttribute('onmouseover');
72
153
  const tagNameJSHandle = await firstTrigger[0].getProperty('tagName')
73
154
  .catch(() => '');
74
155
  if (tagNameJSHandle) {
@@ -93,78 +174,70 @@ const find = async (data, withItems, page, sample) => {
93
174
  const preOpacities = await page.evaluate(elements => elements.map(
94
175
  element => window.getComputedStyle(element).opacity
95
176
  ), preDescendants);
177
+ // Get the style properties of the trigger.
178
+ const triggerPreStyles = await getHoverStyles(page, firstTrigger[0]);
179
+ const totalEstimate = 1 / firstTrigger[1];
180
+ const itemData = {
181
+ tagName,
182
+ id: (await firstTrigger[0].getAttribute('id')) || '',
183
+ text: await textOf(firstTrigger[0], 50)
184
+ };
96
185
  try {
97
186
  // Hover over the trigger.
98
187
  await firstTrigger[0].hover({
99
188
  timeout: 500,
100
189
  noWaitAfter: true
101
190
  });
102
- // FUNCTION DEFINITION START
103
- // Repeatedly seeks impacts.
104
- const getImpacts = async (interval, triesLeft) => {
105
- // If the allowed trial count has not yet been exhausted:
106
- if (triesLeft-- && ! hasTimedOut) {
107
- // Get the collection of descendants of the root.
108
- const postDescendants = await root.$$(':visible');
109
- // Identify the prior descandants of the root still in existence.
110
- const remainerIndexes = await page.evaluate(args => {
111
- const preDescendants = args[0];
112
- const postDescendants = args[1];
113
- const remainerIndexes = preDescendants
114
- .map((element, index) => postDescendants.includes(element) ? index : -1)
115
- .filter(index => index > -1);
116
- return remainerIndexes;
117
- }, [preDescendants, postDescendants]);
118
- // Get the impacts of the hover event.
119
- const additionCount = postDescendants.length - remainerIndexes.length;
120
- const removalCount = preDescendants.length - remainerIndexes.length;
121
- const remainers = [];
122
- for (const index of remainerIndexes) {
123
- remainers.push({
124
- element: preDescendants[index],
125
- preOpacity: preOpacities[index],
126
- postOpacity: await page.evaluate(
127
- element => window.getComputedStyle(element).opacity, preDescendants[index]
128
- )
129
- });
130
- }
131
- const opacityChangers = remainers
132
- .filter(remainer => remainer.postOpacity !== remainer.preOpacity);
133
- const opacityImpact = opacityChangers
134
- ? await page.evaluate(changers => changers.reduce(
135
- (total, current) => total + current.element.querySelectorAll('*').length, 0
136
- ), opacityChangers)
137
- : 0;
138
- // If there are any impacts:
139
- if (additionCount || removalCount || opacityChangers.length) {
140
- // Return them as estimated population impacts.
141
- return {
142
- additionCount: additionCount / firstTrigger[1],
143
- removalCount: removalCount / firstTrigger[1],
144
- opacityChanges: opacityChangers.length / firstTrigger[1],
145
- opacityImpact: opacityImpact / firstTrigger[1]
146
- };
147
- }
148
- // Otherwise, i.e. if there are no impacts:
149
- else {
150
- // Try again.
151
- return await new Promise(resolve => {
152
- setTimeout(() => {
153
- resolve(getImpacts(interval, triesLeft));
154
- }, interval);
155
- });
156
- }
191
+ // Repeatedly seek impacts of the hover at intervals.
192
+ const impacts = await getImpacts(
193
+ 300, 4, root, page, preDescendants, preOpacities, firstTrigger
194
+ );
195
+ // Get the style properties of the trigger.
196
+ const triggerPostStyles = await getHoverStyles(page, firstTrigger[0]);
197
+ // Add cursor and other style defects to the data.
198
+ const cursor = triggerPreStyles.cursor;
199
+ // If the trigger has no cursor:
200
+ if (cursor === 'none') {
201
+ // Add this fact to the data.
202
+ data.totals.noCursors += totalEstimate;
203
+ if (withItems) {
204
+ data.items.noCursors.push(itemData);
157
205
  }
158
- // Otherwise, i.e. if the allowed trial count has been exhausted:
159
- else {
160
- // Report non-impact.
161
- return null;
206
+ }
207
+ // If the trigger has an improper cursor:
208
+ if (
209
+ tagName === 'A' && cursor !== 'pointer'
210
+ || tagName === 'BUTTON' && cursor !== 'default'
211
+ ){
212
+ // Add this fact to the data.
213
+ data.totals.badCursors += totalEstimate;
214
+ if (withItems) {
215
+ data.items.badCursors.push(itemData);
162
216
  }
163
- };
164
- // FUNCTION DEFINITION END
165
- // Repeatedly seek impacts of the hover at intervals.
166
- const impacts = await getImpacts(300, 4);
167
- // If there were any:
217
+ }
218
+ // If hover indication is required but is absent:
219
+ if (
220
+ (tagName === 'BUTTON' || onmouseenter || onmouseover)
221
+ && JSON.stringify(triggerPostStyles) === JSON.stringify(triggerPreStyles)
222
+ ) {
223
+ // Add this fact to the data.
224
+ data.totals.noIndicators += totalEstimate;
225
+ if (withItems) {
226
+ data.items.noIndicators.push(itemData);
227
+ }
228
+ }
229
+ // If hover indication is illicit but is present:
230
+ if (
231
+ tagName === 'LI'
232
+ && JSON.stringify(triggerPostStyles) !== JSON.stringify(triggerPreStyles)
233
+ ) {
234
+ // Add this fact to the data.
235
+ data.totals.badIndicators += totalEstimate;
236
+ if (withItems) {
237
+ data.items.badIndicators.push(itemData);
238
+ }
239
+ }
240
+ // If there were any impacts:
168
241
  if (impacts) {
169
242
  // Hover over the upper-left corner of the page, to undo any impacts.
170
243
  await page.hover('body', {
@@ -176,30 +249,30 @@ const find = async (data, withItems, page, sample) => {
176
249
  force: true,
177
250
  noWaitAfter: true
178
251
  });
179
- // Wait for any delayed and/or slowed hover reaction.
252
+ // Wait for any delayed and/or slowed reaction.
180
253
  await page.waitForTimeout(200);
181
254
  await root.waitForElementState('stable');
182
255
  // Increment the estimated counts of triggers and impacts.
183
- const {additionCount, removalCount, opacityChanges, opacityImpact} = impacts;
256
+ const {additions, removals, opacityChanges, opacityImpact} = impacts;
184
257
  if (hasTimedOut) {
185
258
  return Promise.resolve('');
186
259
  }
187
260
  else {
188
- data.totals.impactTriggers += 1 / firstTrigger[1];
189
- data.totals.additions += additionCount;
190
- data.totals.removals += removalCount;
191
- data.totals.opacityChanges += opacityChanges;
192
- data.totals.opacityImpact += opacityImpact;
261
+ data.totals.impactTriggers += totalEstimate;
262
+ data.totals.additions += additions / firstTrigger[1];
263
+ data.totals.removals += removals / firstTrigger[1];
264
+ data.totals.opacityChanges += opacityChanges / firstTrigger[1];
265
+ data.totals.opacityImpact += opacityImpact / firstTrigger[1];
193
266
  // If details are to be reported:
194
267
  if (withItems) {
195
- // Report them, with probability weighting removed.
268
+ // Add them to the data.
196
269
  data.items.impactTriggers.push({
197
270
  tagName,
198
271
  text: await textOf(firstTrigger[0], 50),
199
- additions: additionCount * firstTrigger[1],
200
- removals: removalCount * firstTrigger[1],
201
- opacityChanges: opacityChanges * firstTrigger[1],
202
- opacityImpact: opacityImpact * firstTrigger[1]
272
+ additions,
273
+ removals,
274
+ opacityChanges,
275
+ opacityImpact
203
276
  });
204
277
  }
205
278
  }
@@ -211,15 +284,10 @@ const find = async (data, withItems, page, sample) => {
211
284
  return Promise.resolve('');
212
285
  }
213
286
  else {
214
- data.totals.unhoverables += 1 / firstTrigger[1];
287
+ data.totals.unhoverables += totalEstimate;
215
288
  if (withItems) {
216
289
  try {
217
- const id = await firstTrigger[0].getAttribute('id');
218
- data.items.unhoverables.push({
219
- tagName,
220
- id: id || '',
221
- text: await textOf(firstTrigger[0], 50)
222
- });
290
+ data.items.unhoverables.push(itemData);
223
291
  }
224
292
  catch(error) {
225
293
  console.log('ERROR itemizing unhoverable element');
@@ -251,7 +319,11 @@ exports.reporter = async (page, sampleSize = -1, withItems) => {
251
319
  removals: 0,
252
320
  opacityChanges: 0,
253
321
  opacityImpact: 0,
254
- unhoverables: 0
322
+ unhoverables: 0,
323
+ noCursors: 0,
324
+ badCursors: 0,
325
+ noIndicators: 0,
326
+ badIndicators: 0
255
327
  }
256
328
  };
257
329
  // If details are to be reported:
@@ -259,7 +331,11 @@ exports.reporter = async (page, sampleSize = -1, withItems) => {
259
331
  // Add properties for details to the initialized result.
260
332
  data.items = {
261
333
  impactTriggers: [],
262
- unhoverables: []
334
+ unhoverables: [],
335
+ noCursors: [],
336
+ badCursors: [],
337
+ noIndicators: [],
338
+ badIndicators: []
263
339
  };
264
340
  }
265
341
  // Identify the triggers.
@@ -288,7 +364,7 @@ exports.reporter = async (page, sampleSize = -1, withItems) => {
288
364
  };
289
365
  clearTimeout(timeout);
290
366
  }, 1000 * timeLimit);
291
- // Find and document the impacts.
367
+ // Find and document the style defects and impacts of the sampled triggers.
292
368
  if (sample.length && ! hasTimedOut) {
293
369
  await find(data, withItems, page, sample);
294
370
  }
package/tests/titledEl.js CHANGED
@@ -10,7 +10,9 @@ exports.reporter = async (page, withItems) => {
10
10
  badTitleElements => {
11
11
  // FUNCTION DEFINITION START
12
12
  // Returns a space-minimized copy of a string.
13
- const compact = string => string.replace(/[\t\n]/g, '').replace(/\s{2,}/g, ' ').trim();
13
+ const compact = string => string
14
+ ? string.replace(/[\t\n]/g, '').replace(/\s{2,}/g, ' ').trim()
15
+ : '';
14
16
  // FUNCTION DEFINITION END
15
17
  return badTitleElements.map(element => ({
16
18
  tagName: element.tagName,
@@ -16,10 +16,10 @@
16
16
  "type": "test",
17
17
  "which": "hover",
18
18
  "what": "hover",
19
- "sampleSize": 2,
19
+ "sampleSize": 5,
20
20
  "withItems": true,
21
21
  "expect": [
22
- ["totals.triggers", "=", 2],
22
+ ["totals.triggers", "=", 4],
23
23
  ["totals.impactTriggers", "=", 0],
24
24
  ["totals.additions", "=", 0],
25
25
  ["totals.removals", "=", 0],
@@ -38,21 +38,23 @@
38
38
  "type": "test",
39
39
  "which": "hover",
40
40
  "what": "hover",
41
- "sampleSize": 5,
41
+ "sampleSize": 8,
42
42
  "withItems": true,
43
43
  "expect": [
44
- ["totals.triggers", "=", 4],
45
- ["totals.impactTriggers", "=", 2],
44
+ ["totals.triggers", "=", 6],
45
+ ["totals.impactTriggers", "=", 3],
46
46
  ["totals.additions", "=", 3],
47
- ["totals.removals", "=", 0],
47
+ ["totals.removals", "=", 1],
48
48
  ["totals.opacityChanges", "=", 1],
49
49
  ["totals.opacityImpact", "=", 1],
50
50
  ["totals.unhoverables", "=", 1],
51
51
  ["items.impactTriggers.0.tagName", "=", "A"],
52
52
  ["items.impactTriggers.1.tagName", "=", "BUTTON"],
53
+ ["items.impactTriggers.2.tagName", "=", "LI"],
53
54
  ["items.impactTriggers.0.text", "=", "information"],
54
55
  ["items.impactTriggers.0.additions", "=", 0],
55
56
  ["items.impactTriggers.1.additions", "=", 3],
57
+ ["items.impactTriggers.2.removals", "=", 1],
56
58
  ["items.impactTriggers.0.opacityChanges", "=", 1],
57
59
  ["items.impactTriggers.1.opacityImpact", "=", 0],
58
60
  ["items.unhoverables.0.tagName", "=", "BUTTON"],
@@ -70,7 +72,7 @@
70
72
  "which": "hover",
71
73
  "what": "hover",
72
74
  "sampleSize": 10,
73
- "withItems": true,
75
+ "withItems": false,
74
76
  "expect": [
75
77
  ["totals.triggers", "=", 20],
76
78
  ["totals.impactTriggers", ">", -1],
@@ -82,6 +84,26 @@
82
84
  ["totals.opacityImpact", "=", 0],
83
85
  ["totals.unhoverables", "=", 0]
84
86
  ]
87
+ },
88
+ {
89
+ "type": "url",
90
+ "which": "__targets__/hover/styleBad.html",
91
+ "what": "page with deviant trigger styles"
92
+ },
93
+ {
94
+ "type": "test",
95
+ "which": "hover",
96
+ "what": "hover",
97
+ "sampleSize": 5,
98
+ "withItems": true,
99
+ "expect": [
100
+ ["totals.noIndicators", "=", 1],
101
+ ["totals.noCursors", "=", 1],
102
+ ["totals.badIndicators", "=", 1],
103
+ ["items.noIndicators.0.text", "=", "button"],
104
+ ["items.noCursors.0.tagName", "=", "LI"],
105
+ ["items.badIndicators.0.id", "=", "li2"]
106
+ ]
85
107
  }
86
108
  ]
87
109
  }
@@ -14,6 +14,12 @@
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
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 id="smallButton" 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
+ <p>Hovering over the first of the following list items changes content.</p>
18
+ <ul>
19
+ <li onmouseover="document.getElementById('listDependent').setAttribute('hidden', true)">This is a list item.</li>
20
+ <li>This is another list item.</li>
21
+ </ul>
22
+ <p id="listDependent">This paragraph becomes invisible when you hover over the first list item.</p>
17
23
  </main>
18
24
  </body>
19
25
  </html>
@@ -10,6 +10,11 @@
10
10
  <main>
11
11
  <h1>Page with standard hover behavior</h1>
12
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
+ <p>Hovering over either of the following list items changes no content.</p>
14
+ <ul>
15
+ <li>This is a list item.</li>
16
+ <li>This is another list item.</li>
17
+ </ul>
13
18
  </main>
14
19
  </body>
15
20
  </html>
@@ -0,0 +1,35 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en-US">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Page with deviant hover indicators</title>
6
+ <meta name="description" content="tester">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1">
8
+ <style>
9
+ button, button:hover {
10
+ color: red;
11
+ background-color: white;
12
+ border: 2px solid black;
13
+ }
14
+ li.cursorless {
15
+ cursor: none;
16
+ }
17
+ li.hoverChanger:hover {
18
+ color: blue;
19
+ background-color: yellow;
20
+ }
21
+ </style>
22
+ </head>
23
+ <body>
24
+ <main>
25
+ <h1>Page with deviant hover indicators</h1>
26
+ <p>This page contains a link to <a href="https://en.wikipedia.org">information</a> and a <button type="button">button</button>. The default style change of the button is prevented.</p>
27
+ <p>This is a list.</p>
28
+ <ul>
29
+ <li>This is a normal list item.</li>
30
+ <li class="cursorless">This is a list item that loses its cursor when hovered over.</li>
31
+ <li id="li2" class="hoverChanger">This is a list item that changes when hovered over although it should not.</li>
32
+ </ul>
33
+ </main>
34
+ </body>
35
+ </html>