aboutsummaryrefslogtreecommitdiffstats
path: root/tests/browser.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'tests/browser.mjs')
-rw-r--r--tests/browser.mjs45
1 files changed, 44 insertions, 1 deletions
diff --git a/tests/browser.mjs b/tests/browser.mjs
index 32cf65f..0f77585 100644
--- a/tests/browser.mjs
+++ b/tests/browser.mjs
@@ -1,6 +1,9 @@
// The web surface in a real browser: headless Chromium, driven over the
// DevTools protocol with what a user does (keys, a click on a row, typing
-// Korean text and Enter), each step waiting for the page to show its effect.
+// Korean text and Enter), each step waiting for the page to show its effect;
+// and what a screen reader gets from it, read from Chromium's accessibility
+// tree: roles, names, the list's focus and selection, each option's place in
+// the whole list, and a focused list that survives redraws.
//
// node tests/browser.mjs URL (the app: examples/todo.aup with tasks a, b, c)
// Prints one JSON line: the steps that held.
@@ -71,9 +74,42 @@ try {
};
await until('three tasks, the last focused', `document.querySelectorAll('li').length === 3 && ${selected} === 2`);
+ await send('Accessibility.enable');
+ const role = n => n.role?.value, name = n => n.name?.value;
+ const prop = (n, key) => n.properties?.find(p => p.name === key)?.value;
+ const tree = async () => {
+ const nodes = (await send('Accessibility.getFullAXTree')).nodes.filter(n => !n.ignored);
+ const listbox = nodes.find(n => role(n) === 'listbox');
+ const options = nodes.filter(n => role(n) === 'option');
+ const active = prop(listbox, 'activedescendant')?.relatedNodes?.[0]?.backendDOMNodeId;
+ return { nodes, listbox, options, active };
+ };
+ const check = (what, holds, seen) => {
+ if (!holds) throw new Error(`${what}: ${JSON.stringify(seen)}`);
+ steps.push(what);
+ };
+ let a = await tree();
+ const heading = a.nodes.find(n => role(n) === 'heading');
+ check('the title is a heading of level 1', heading && name(heading) === 'To do' && prop(heading, 'level')?.value === 1,
+ heading);
+ check('the list is a focused listbox named for its view', name(a.listbox) === 'tasks_list' &&
+ prop(a.listbox, 'focused')?.value === true, a.listbox);
+ // Set size and position are not in the DevTools tree: read what Chromium maps to the platform's.
+ const places = `[...document.querySelectorAll('[role=option]')].map(o => o.getAttribute('aria-posinset') + '/' + o.getAttribute('aria-setsize')).join()`;
+ check('its options are the tasks, each knowing its place in the whole list', a.options.map(name).join() === 'a,b,c' &&
+ await evaluate(places) === '1/3,2/3,3/3', [a.options.map(name), await evaluate(places)]);
+ check('the focused task is selected and the active descendant', a.options.map(o => prop(o, 'selected')?.value).join() ===
+ 'false,false,true' && a.active === a.options[2].backendDOMNodeId, a.options);
+ const keys = a.nodes.filter(n => role(n) === 'button' && name(n));
+ check('keys are named buttons', keys.length >= 2 && keys.some(k => /up/i.test(name(k))), keys.map(name));
+ const list = a.listbox.backendDOMNodeId;
await until('title and field', `document.querySelector('h1').textContent === 'To do' && document.querySelector('dd').textContent === '3 of 3'`);
await key('ArrowUp', 'ArrowUp', 38);
await until('ArrowUp moves the focus', `${selected} === 1`);
+ a = await tree();
+ check('a screen reader follows it: the same list, still focused, its active descendant moved',
+ a.listbox.backendDOMNodeId === list && prop(a.listbox, 'focused')?.value === true &&
+ a.active === a.options[1].backendDOMNodeId && prop(a.options[1], 'selected')?.value === true, a.options);
const box = await evaluate(`(() => { const r = document.querySelectorAll('li')[0].getBoundingClientRect(); return [r.x + r.width / 2, r.y + r.height / 2]; })()`);
await send('Input.dispatchMouseEvent', { type: 'mousePressed', x: box[0], y: box[1], button: 'left', clickCount: 1 });
await send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: box[0], y: box[1], button: 'left', clickCount: 1 });
@@ -86,6 +122,13 @@ try {
await key('Enter', 'Enter', 13, '\r');
await until('typed text becomes a task', `document.querySelectorAll('li').length === 4 && document.querySelectorAll('li')[3].textContent === '브라우저' && ${selected} === 3`);
await until('the input is emptied', `document.getElementById('entry').value === ''`);
+ a = await tree();
+ const done = a.options[0];
+ check('the added task is option 4 of 4, and the done one still says so', a.options.length === 4 &&
+ await evaluate(places) === '1/4,2/4,3/4,4/4' && name(a.options[3]) === '브라우저' && /done/.test(name(done)),
+ [a.options.map(name), await evaluate(places)]);
+ const entry = a.nodes.find(n => role(n) === 'textbox');
+ check('the text input is named for its input', entry && name(entry) === 'add', entry);
await finish(true);
} catch (error) {
await finish(false, String(error.message || error));