testaro 5.16.2 → 5.17.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/commands.js
CHANGED
|
@@ -196,11 +196,7 @@ exports.commands = {
|
|
|
196
196
|
hover: [
|
|
197
197
|
'Perform a hover test',
|
|
198
198
|
{
|
|
199
|
-
|
|
200
|
-
headSampleSize: [false, 'number', '', 'size of the head sample to be drawn, if any'],
|
|
201
|
-
tailSampleSize: [
|
|
202
|
-
false, 'number', '', 'size of the non-head sample to be drawn, if not all'
|
|
203
|
-
],
|
|
199
|
+
sampleSize: [false, 'number', '', 'limit on sample size of triggers, if any'],
|
|
204
200
|
withItems: [true, 'boolean']
|
|
205
201
|
}
|
|
206
202
|
],
|
package/package.json
CHANGED
package/tests/hover.js
CHANGED
|
@@ -13,12 +13,8 @@
|
|
|
13
13
|
|
|
14
14
|
Despite this delay, the test can make the execution time practical by randomly sampling triggers
|
|
15
15
|
instead of hovering over all of them. When sampling is performed, the results may vary from one
|
|
16
|
-
execution to another. Because hover impacts typically occur near the beginning of a page,
|
|
17
|
-
|
|
18
|
-
headSize (0): the size of an initial subset of triggers (“head”)
|
|
19
|
-
headSampleSize (-1): the size of the sample to be drawn from the head
|
|
20
|
-
tailSampleSize (-1): the size of the sample to be drawn from the remainder of the page
|
|
21
|
-
A sample size of -1 means that there is no sampling, and the entire population is tested.
|
|
16
|
+
execution to another. Because hover impacts typically occur near the beginning of a page, the
|
|
17
|
+
probability of the inclusion of a trigger in a sample decreases with the index of the trigger.
|
|
22
18
|
|
|
23
19
|
An element is reported as unhoverable when it fails the Playwright actionability checks for
|
|
24
20
|
hovering, i.e. fails to be attached to the DOM, visible, stable (not or no longer animating), and
|
|
@@ -34,25 +30,29 @@ let hasTimedOut = false;
|
|
|
34
30
|
|
|
35
31
|
// FUNCTIONS
|
|
36
32
|
|
|
37
|
-
// Samples a population and returns the sample.
|
|
33
|
+
// Samples a population and returns the sample and each member’s sampling probability.
|
|
38
34
|
const getSample = (population, sampleSize) => {
|
|
39
35
|
const popSize = population.length;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const popData = [];
|
|
45
|
-
for (const trigger of population) {
|
|
46
|
-
popData.push({
|
|
47
|
-
trigger,
|
|
48
|
-
sorter: Math.random()
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
popData.sort((a, b) => a.sorter - b.sorter);
|
|
52
|
-
return popData.slice(0, sampleSize).map(obj => obj.trigger);
|
|
36
|
+
// If the sample is at least as large as the population:
|
|
37
|
+
if (sampleSize >= popSize) {
|
|
38
|
+
// Return the population as the sample.
|
|
39
|
+
return population.map(trigger => [trigger, 1]);
|
|
53
40
|
}
|
|
41
|
+
// Otherwise, i.e. if the sample is smaller than the population:
|
|
54
42
|
else {
|
|
55
|
-
|
|
43
|
+
// Force the sample size to be an integer and at least 1.
|
|
44
|
+
sampleSize = Math.floor(Math.max(1, sampleSize));
|
|
45
|
+
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
|
+
// Get the sample.
|
|
51
|
+
const sample = population.map((trigger, index) => {
|
|
52
|
+
const itemProb = samProb(index);
|
|
53
|
+
return [trigger, itemProb];
|
|
54
|
+
}).filter(pair => pair[1] > Math.random());
|
|
55
|
+
return sample;
|
|
56
56
|
}
|
|
57
57
|
};
|
|
58
58
|
// Returns the text of an element.
|
|
@@ -62,28 +62,28 @@ const textOf = async (element, limit) => {
|
|
|
62
62
|
return text.trim().replace(/\s*/sg, '').slice(0, limit);
|
|
63
63
|
};
|
|
64
64
|
// Recursively reports impacts of hovering over triggers.
|
|
65
|
-
const find = async (data, withItems, page,
|
|
66
|
-
// If any
|
|
65
|
+
const find = async (data, withItems, page, sample) => {
|
|
66
|
+
// If any triggers remain and the test has not timed out:
|
|
67
67
|
if (sample.length && ! hasTimedOut) {
|
|
68
68
|
// Get and report the impacts until and unless the test times out.
|
|
69
69
|
try {
|
|
70
|
-
// Identify the first
|
|
70
|
+
// Identify the first trigger and its sampling probability.
|
|
71
71
|
const firstTrigger = sample[0];
|
|
72
|
-
const tagNameJSHandle = await firstTrigger.getProperty('tagName')
|
|
72
|
+
const tagNameJSHandle = await firstTrigger[0].getProperty('tagName')
|
|
73
73
|
.catch(() => '');
|
|
74
74
|
if (tagNameJSHandle) {
|
|
75
75
|
const tagName = await tagNameJSHandle.jsonValue();
|
|
76
76
|
// Identify the root of a subtree likely to contain impacted elements.
|
|
77
|
-
let root = firstTrigger;
|
|
77
|
+
let root = firstTrigger[0];
|
|
78
78
|
if (['A', 'BUTTON', 'LI'].includes(tagName)) {
|
|
79
79
|
const rootJSHandle = await page.evaluateHandle(
|
|
80
|
-
|
|
81
|
-
const parent =
|
|
80
|
+
trigger => {
|
|
81
|
+
const parent = trigger.parentElement || trigger;
|
|
82
82
|
const grandparent = parent.parentElement || parent;
|
|
83
83
|
const greatGrandparent = grandparent.parentElement || parent;
|
|
84
|
-
return
|
|
84
|
+
return trigger.tagName === 'LI' ? grandparent : greatGrandparent;
|
|
85
85
|
},
|
|
86
|
-
firstTrigger
|
|
86
|
+
firstTrigger[0]
|
|
87
87
|
);
|
|
88
88
|
root = rootJSHandle.asElement();
|
|
89
89
|
}
|
|
@@ -95,7 +95,7 @@ const find = async (data, withItems, page, region, sample, popRatio) => {
|
|
|
95
95
|
), preDescendants);
|
|
96
96
|
try {
|
|
97
97
|
// Hover over the trigger.
|
|
98
|
-
await firstTrigger.hover({
|
|
98
|
+
await firstTrigger[0].hover({
|
|
99
99
|
timeout: 500,
|
|
100
100
|
noWaitAfter: true
|
|
101
101
|
});
|
|
@@ -115,7 +115,7 @@ const find = async (data, withItems, page, region, sample, popRatio) => {
|
|
|
115
115
|
.filter(index => index > -1);
|
|
116
116
|
return remainerIndexes;
|
|
117
117
|
}, [preDescendants, postDescendants]);
|
|
118
|
-
// Get the
|
|
118
|
+
// Get the impacts of the hover event.
|
|
119
119
|
const additionCount = postDescendants.length - remainerIndexes.length;
|
|
120
120
|
const removalCount = preDescendants.length - remainerIndexes.length;
|
|
121
121
|
const remainers = [];
|
|
@@ -130,18 +130,24 @@ const find = async (data, withItems, page, region, sample, popRatio) => {
|
|
|
130
130
|
}
|
|
131
131
|
const opacityChangers = remainers
|
|
132
132
|
.filter(remainer => remainer.postOpacity !== remainer.preOpacity);
|
|
133
|
-
const opacityImpact = opacityChangers
|
|
133
|
+
const opacityImpact = opacityChangers
|
|
134
|
+
? await page.evaluate(changers => changers.reduce(
|
|
134
135
|
(total, current) => total + current.element.querySelectorAll('*').length, 0
|
|
135
|
-
), opacityChangers)
|
|
136
|
+
), opacityChangers)
|
|
137
|
+
: 0;
|
|
138
|
+
// If there are any impacts:
|
|
136
139
|
if (additionCount || removalCount || opacityChangers.length) {
|
|
140
|
+
// Return them as estimated population impacts.
|
|
137
141
|
return {
|
|
138
|
-
additionCount,
|
|
139
|
-
removalCount,
|
|
140
|
-
opacityChangers,
|
|
141
|
-
opacityImpact
|
|
142
|
+
additionCount: additionCount / firstTrigger[1],
|
|
143
|
+
removalCount: removalCount / firstTrigger[1],
|
|
144
|
+
opacityChanges: opacityChangers.length / firstTrigger[1],
|
|
145
|
+
opacityImpact: opacityImpact / firstTrigger[1]
|
|
142
146
|
};
|
|
143
147
|
}
|
|
148
|
+
// Otherwise, i.e. if there are no impacts:
|
|
144
149
|
else {
|
|
150
|
+
// Try again.
|
|
145
151
|
return await new Promise(resolve => {
|
|
146
152
|
setTimeout(() => {
|
|
147
153
|
resolve(getImpacts(interval, triesLeft));
|
|
@@ -149,7 +155,9 @@ const find = async (data, withItems, page, region, sample, popRatio) => {
|
|
|
149
155
|
});
|
|
150
156
|
}
|
|
151
157
|
}
|
|
158
|
+
// Otherwise, i.e. if the allowed trial count has been exhausted:
|
|
152
159
|
else {
|
|
160
|
+
// Report non-impact.
|
|
153
161
|
return null;
|
|
154
162
|
}
|
|
155
163
|
};
|
|
@@ -171,27 +179,27 @@ const find = async (data, withItems, page, region, sample, popRatio) => {
|
|
|
171
179
|
// Wait for any delayed and/or slowed hover reaction.
|
|
172
180
|
await page.waitForTimeout(200);
|
|
173
181
|
await root.waitForElementState('stable');
|
|
174
|
-
// Increment the counts of triggers and impacts.
|
|
175
|
-
const {additionCount, removalCount,
|
|
182
|
+
// Increment the estimated counts of triggers and impacts.
|
|
183
|
+
const {additionCount, removalCount, opacityChanges, opacityImpact} = impacts;
|
|
176
184
|
if (hasTimedOut) {
|
|
177
185
|
return Promise.resolve('');
|
|
178
186
|
}
|
|
179
187
|
else {
|
|
180
|
-
data.totals.impactTriggers +=
|
|
181
|
-
data.totals.additions +=
|
|
182
|
-
data.totals.removals +=
|
|
183
|
-
data.totals.opacityChanges +=
|
|
184
|
-
data.totals.opacityImpact +=
|
|
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;
|
|
185
193
|
// If details are to be reported:
|
|
186
194
|
if (withItems) {
|
|
187
|
-
// Report them.
|
|
188
|
-
data.items
|
|
195
|
+
// Report them, with probability weighting removed.
|
|
196
|
+
data.items.impactTriggers.push({
|
|
189
197
|
tagName,
|
|
190
|
-
text: await textOf(firstTrigger, 50),
|
|
191
|
-
additions: additionCount,
|
|
192
|
-
removals: removalCount,
|
|
193
|
-
opacityChanges:
|
|
194
|
-
opacityImpact
|
|
198
|
+
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]
|
|
195
203
|
});
|
|
196
204
|
}
|
|
197
205
|
}
|
|
@@ -203,14 +211,14 @@ const find = async (data, withItems, page, region, sample, popRatio) => {
|
|
|
203
211
|
return Promise.resolve('');
|
|
204
212
|
}
|
|
205
213
|
else {
|
|
206
|
-
data.totals.unhoverables
|
|
214
|
+
data.totals.unhoverables += 1 / firstTrigger[1];
|
|
207
215
|
if (withItems) {
|
|
208
216
|
try {
|
|
209
|
-
const id = await firstTrigger.getAttribute('id');
|
|
210
|
-
data.items
|
|
217
|
+
const id = await firstTrigger[0].getAttribute('id');
|
|
218
|
+
data.items.unhoverables.push({
|
|
211
219
|
tagName,
|
|
212
220
|
id: id || '',
|
|
213
|
-
text: await textOf(firstTrigger, 50)
|
|
221
|
+
text: await textOf(firstTrigger[0], 50)
|
|
214
222
|
});
|
|
215
223
|
}
|
|
216
224
|
catch(error) {
|
|
@@ -221,7 +229,7 @@ const find = async (data, withItems, page, region, sample, popRatio) => {
|
|
|
221
229
|
}
|
|
222
230
|
}
|
|
223
231
|
// Process the remaining potential triggers.
|
|
224
|
-
await find(data, withItems, page,
|
|
232
|
+
await find(data, withItems, page, sample.slice(1));
|
|
225
233
|
}
|
|
226
234
|
catch(error) {
|
|
227
235
|
console.log(`ERROR: Test quit when remaining sample size was ${sample.length}`);
|
|
@@ -232,15 +240,12 @@ const find = async (data, withItems, page, region, sample, popRatio) => {
|
|
|
232
240
|
}
|
|
233
241
|
};
|
|
234
242
|
// Performs the hover test and reports results.
|
|
235
|
-
exports.reporter = async (
|
|
236
|
-
page, headSize = 0, headSampleSize = -1, tailSampleSize = -1, withItems
|
|
237
|
-
) => {
|
|
243
|
+
exports.reporter = async (page, sampleSize = -1, withItems) => {
|
|
238
244
|
// Initialize the result.
|
|
239
245
|
let data = {
|
|
240
246
|
totals: {
|
|
241
247
|
triggers: 0,
|
|
242
|
-
|
|
243
|
-
tailTriggers: 0,
|
|
248
|
+
triggerSample: 0,
|
|
244
249
|
impactTriggers: 0,
|
|
245
250
|
additions: 0,
|
|
246
251
|
removals: 0,
|
|
@@ -253,14 +258,8 @@ exports.reporter = async (
|
|
|
253
258
|
if (withItems) {
|
|
254
259
|
// Add properties for details to the initialized result.
|
|
255
260
|
data.items = {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
unhoverables: []
|
|
259
|
-
},
|
|
260
|
-
tail: {
|
|
261
|
-
impactTriggers: [],
|
|
262
|
-
unhoverables: []
|
|
263
|
-
}
|
|
261
|
+
impactTriggers: [],
|
|
262
|
+
unhoverables: []
|
|
264
263
|
};
|
|
265
264
|
}
|
|
266
265
|
// Identify the triggers.
|
|
@@ -271,19 +270,12 @@ exports.reporter = async (
|
|
|
271
270
|
data.prevented = true;
|
|
272
271
|
return [];
|
|
273
272
|
});
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
const tailTriggerCount = tailTriggers.length;
|
|
279
|
-
data.totals.triggers = headTriggerCount + tailTriggerCount;
|
|
280
|
-
data.totals.headTriggers = headTriggerCount;
|
|
281
|
-
data.totals.tailTriggers = tailTriggerCount;
|
|
282
|
-
// Get the head and tail samples.
|
|
283
|
-
const headSample = getSample(headTriggers, headSampleSize);
|
|
284
|
-
const tailSample = tailSampleSize === -1 ? tailTriggers : getSample(tailTriggers, tailSampleSize);
|
|
273
|
+
data.totals.triggers = triggers.length;
|
|
274
|
+
// Get the sample.
|
|
275
|
+
const sample = getSample(triggers, sampleSize);
|
|
276
|
+
data.sampleSize = sample.length;
|
|
285
277
|
// Set a time limit to cover possible 1.9 seconds per trigger.
|
|
286
|
-
const timeLimit = Math.round(2.2 *
|
|
278
|
+
const timeLimit = Math.round(2.2 * data.sampleSize);
|
|
287
279
|
const timeout = setTimeout(async () => {
|
|
288
280
|
await page.close();
|
|
289
281
|
console.log(
|
|
@@ -297,11 +289,8 @@ exports.reporter = async (
|
|
|
297
289
|
clearTimeout(timeout);
|
|
298
290
|
}, 1000 * timeLimit);
|
|
299
291
|
// Find and document the impacts.
|
|
300
|
-
if (
|
|
301
|
-
await find(data, withItems, page,
|
|
302
|
-
}
|
|
303
|
-
if (tailSample.length && ! hasTimedOut) {
|
|
304
|
-
await find(data, withItems, page, 'tail', tailSample, tailTriggerCount / tailSample.length);
|
|
292
|
+
if (data.sampleSize && ! hasTimedOut) {
|
|
293
|
+
await find(data, withItems, page, sample);
|
|
305
294
|
}
|
|
306
295
|
clearTimeout(timeout);
|
|
307
296
|
// Round the reported totals.
|
|
@@ -16,20 +16,17 @@
|
|
|
16
16
|
"type": "test",
|
|
17
17
|
"which": "hover",
|
|
18
18
|
"what": "hover",
|
|
19
|
-
"
|
|
20
|
-
"headSampleSize": 3,
|
|
21
|
-
"tailSampleSize": 30,
|
|
19
|
+
"sampleSize": 2,
|
|
22
20
|
"withItems": true,
|
|
23
21
|
"expect": [
|
|
24
22
|
["totals.triggers", "=", 2],
|
|
25
|
-
["totals.headTriggers", "=", 2],
|
|
26
|
-
["totals.tailTriggers", "=", 0],
|
|
27
23
|
["totals.impactTriggers", "=", 0],
|
|
28
24
|
["totals.additions", "=", 0],
|
|
29
25
|
["totals.removals", "=", 0],
|
|
30
26
|
["totals.opacityChanges", "=", 0],
|
|
31
27
|
["totals.opacityImpact", "=", 0],
|
|
32
|
-
["totals.unhoverables", "=", 0]
|
|
28
|
+
["totals.unhoverables", "=", 0],
|
|
29
|
+
["items.impactTriggers.0"]
|
|
33
30
|
]
|
|
34
31
|
},
|
|
35
32
|
{
|
|
@@ -41,20 +38,49 @@
|
|
|
41
38
|
"type": "test",
|
|
42
39
|
"which": "hover",
|
|
43
40
|
"what": "hover",
|
|
44
|
-
"
|
|
45
|
-
"headSampleSize": 3,
|
|
46
|
-
"tailSampleSize": 30,
|
|
41
|
+
"sampleSize": 5,
|
|
47
42
|
"withItems": true,
|
|
48
43
|
"expect": [
|
|
49
44
|
["totals.triggers", "=", 4],
|
|
50
|
-
["totals.headTriggers", "=", 3],
|
|
51
|
-
["totals.tailTriggers", "=", 1],
|
|
52
45
|
["totals.impactTriggers", "=", 2],
|
|
53
46
|
["totals.additions", "=", 3],
|
|
54
47
|
["totals.removals", "=", 0],
|
|
55
48
|
["totals.opacityChanges", "=", 1],
|
|
56
49
|
["totals.opacityImpact", "=", 1],
|
|
57
|
-
["totals.unhoverables", "=", 1]
|
|
50
|
+
["totals.unhoverables", "=", 1],
|
|
51
|
+
["items.impactTriggers.0.tagName", "=", "A"],
|
|
52
|
+
["items.impactTriggers.1.tagName", "=", "BUTTON"],
|
|
53
|
+
["items.impactTriggers.0.text", "=", "information"],
|
|
54
|
+
["items.impactTriggers.0.additions", "=", 0],
|
|
55
|
+
["items.impactTriggers.1.additions", "=", 3],
|
|
56
|
+
["items.impactTriggers.0.opacityChanges", "=", 1],
|
|
57
|
+
["items.impactTriggers.1.opacityImpact", "=", 0],
|
|
58
|
+
["items.unhoverables.0.tagName", "=", "BUTTON"],
|
|
59
|
+
["items.unhoverables.0.id", "=", "smallButton"],
|
|
60
|
+
["items.unhoverables.0.text", "=", "button"]
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"type": "url",
|
|
65
|
+
"which": "__targets__/hover/large.html",
|
|
66
|
+
"what": "page with deviant hover behavior"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"type": "test",
|
|
70
|
+
"which": "hover",
|
|
71
|
+
"what": "hover",
|
|
72
|
+
"sampleSize": 10,
|
|
73
|
+
"withItems": true,
|
|
74
|
+
"expect": [
|
|
75
|
+
["totals.triggers", "=", 20],
|
|
76
|
+
["totals.impactTriggers", ">", -1],
|
|
77
|
+
["totals.impactTriggers", "<", 6],
|
|
78
|
+
["totals.additions", ">", -1],
|
|
79
|
+
["totals.additions", "<", 6],
|
|
80
|
+
["totals.removals", "=", 0],
|
|
81
|
+
["totals.opacityChanges", "=", 0],
|
|
82
|
+
["totals.opacityImpact", "=", 0],
|
|
83
|
+
["totals.unhoverables", "=", 0]
|
|
58
84
|
]
|
|
59
85
|
}
|
|
60
86
|
]
|
|
@@ -13,7 +13,7 @@
|
|
|
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
15
|
<p>The small button is mostly covered by a large one here, preventing the small button from receiving a hover event.</p>
|
|
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>
|
|
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
17
|
</main>
|
|
18
18
|
</body>
|
|
19
19
|
</html>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en-US">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>Page with many hover triggers</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 many hover triggers</h1>
|
|
12
|
+
<p>This paragraph contains a link to <a href="https://en.wikipedia.org" onmouseover="document.getElementById('english').removeAttribute('hidden')">information</a> and a <button type="button">button</button>.</p>
|
|
13
|
+
<p>This paragraph contains a link to <a href="https://fr.wikipedia.org" onmouseover="document.getElementById('french').removeAttribute('hidden')">information</a> and a <button type="button">button</button>.</p>
|
|
14
|
+
<p>This paragraph contains a link to <a href="https://ru.wikipedia.org">information</a> and a <button type="button">button</button>.</p>
|
|
15
|
+
<p>This paragraph contains a link to <a href="https://es.wikipedia.org">information</a> and a <button type="button">button</button>.</p>
|
|
16
|
+
<p>This paragraph contains a link to <a href="https://eo.wikipedia.org">information</a> and a <button type="button">button</button>.</p>
|
|
17
|
+
<p>This paragraph contains a link to <a href="https://fi.wikipedia.org">information</a> and a <button type="button">button</button>.</p>
|
|
18
|
+
<p>This paragraph contains a link to <a href="https://de.wikipedia.org" onmouseover="document.getElementById('german').removeAttribute('hidden')">information</a> and a <button type="button">button</button>.</p>
|
|
19
|
+
<p>This paragraph contains a link to <a href="https://hu.wikipedia.org">information</a> and a <button type="button">button</button>.</p>
|
|
20
|
+
<p>This paragraph contains a link to <a href="https://hi.wikipedia.org">information</a> and a <button type="button">button</button>.</p>
|
|
21
|
+
<p>This paragraph contains a link to <a href="https://ja.wikipedia.org">information</a> and a <button type="button">button</button>.</p>
|
|
22
|
+
<p id="english" hidden>English</p>
|
|
23
|
+
<p id="french" hidden>French</p>
|
|
24
|
+
<p id="german" hidden>German</p>
|
|
25
|
+
</main>
|
|
26
|
+
</body>
|
|
27
|
+
</html>
|