Search / tests /test_reader_accessibility_browser.py
vomebook's picture
feat(a11y): improve search keyboard navigation and high-contrast controls
ecb31fb verified
Raw History Blame Contribute Delete
11.8 kB
"""Focused browser keyboard/accessible-name regressions for the floating Reader UI.
Controlled Chromium verifies DOM/AX contracts, not actual screen-reader speech or
universal WCAG conformance. Uses the existing pinned two-site Reader fixtures.
"""
import unittest
from urllib.parse import urlencode
from tests import test_reader_pdf_followup as support
class ReaderAccessibilityTests(unittest.TestCase):
setUpClass = classmethod(support.PdfFollowupTests.setUpClass.__func__)
setUp = support.PdfFollowupTests.setUp
def open_book(self, extension='md', body=None):
self.page.emulate_media(reduced_motion='reduce')
self.page.route('**/api/reader-content**', lambda route: route.fulfill(
content_type='text/plain', body=body or '# Chapter one\n\n' + 'needle body\n\n' * 600 + '# Chapter two\n\nEnd'))
self.page.goto(self.origin + support.PREFIX + '/static/reader.html?' + urlencode(dict(
url='https://huggingface.co/datasets/VoiceOfML/Test/resolve/main/accessibility.' + extension,
ext=extension)))
self.page.wait_for_function("()=>['ready','failed'].includes(document.documentElement.dataset.readerPhase)")
if extension == 'md':
self.page.wait_for_function("()=>document.querySelector('#history').getAttribute('aria-expanded')==='true'")
def open_with_keyboard(self):
if self.page.locator('#history').get_attribute('aria-expanded') == 'true':
self.page.locator('#history').press('Enter')
self.page.locator('#history').press('Enter')
def test_forced_colors_keeps_bookmark_and_selected_controls_visible(self):
self.open_book()
self.page.emulate_media(forced_colors='active', reduced_motion='reduce')
ribbon = self.page.locator('#bookmark-ribbon')
self.page.wait_for_function('()=>getComputedStyle(document.querySelector("#bookmark-ribbon")).clipPath==="none"')
self.assertGreater(float(ribbon.evaluate('node=>parseFloat(getComputedStyle(node).borderTopWidth)')), 0)
self.assertIn('书签', ribbon.evaluate('node=>getComputedStyle(node,"::after").content'))
for theme in ('dark', 'light'):
self.page.evaluate('theme=>{document.documentElement.dataset.theme=theme}', theme)
self.open_with_keyboard()
self.assertNotEqual(self.page.locator('#toc-tab').evaluate('node=>getComputedStyle(node).outlineStyle'), 'none')
self.page.keyboard.press('Escape')
ribbon.focus()
self.assertNotEqual(ribbon.evaluate('node=>getComputedStyle(node).outlineStyle'), 'none')
self.assertEqual(self.page.locator('.reader-markdown').evaluate('node=>getComputedStyle(node).filter'), 'none')
def test_names_relations_auto_open_and_keyboard_focus(self):
self.page.set_viewport_size({'width': 1200, 'height': 800})
self.open_book()
self.assertFalse(self.page.evaluate("document.querySelector('#history-panel').contains(document.activeElement)"))
self.page.keyboard.press('Tab')
self.assertTrue(self.page.locator('.reader-skip-link').evaluate('node=>node===document.activeElement'))
self.page.keyboard.press('Tab')
self.assertEqual(self.page.evaluate('document.activeElement.id'), 'back')
self.assertEqual(self.page.get_by_role('complementary', name='阅读选项').count(), 1)
self.assertEqual(self.page.get_by_role('button', name='关闭阅读选项', exact=True).count(), 1)
self.assertEqual(self.page.locator('#history').get_attribute('aria-controls'), 'history-panel')
self.open_with_keyboard()
self.assertEqual(self.page.evaluate('document.activeElement.id'), 'toc-tab')
self.page.keyboard.press('ArrowRight')
self.assertEqual(self.page.evaluate('document.activeElement.id'), 'bookmarks-tab')
self.page.keyboard.press('Home')
self.assertEqual(self.page.evaluate('document.activeElement.id'), 'toc-tab')
self.page.keyboard.press('Escape')
self.assertEqual(self.page.evaluate('document.activeElement.id'), 'history')
self.assertTrue(self.page.locator('#history-panel').evaluate('node=>node.inert'))
self.page.locator('#full-search-toggle').evaluate('node=>node.focus()')
self.assertEqual(self.page.evaluate('document.activeElement.id'), 'history')
def test_skip_link_reaches_reading_surface_without_changing_position(self):
self.open_book()
self.page.locator('#history-close').click()
self.page.locator('#viewport').evaluate('node=>{node.scrollTop=450}')
before = self.page.locator('#viewport').evaluate('node=>node.scrollTop')
link = self.page.get_by_role('link', name='跳转到正文')
link.focus()
self.assertGreaterEqual(link.bounding_box()['y'], 0)
self.assertNotEqual(link.evaluate('node=>getComputedStyle(node).outlineStyle'), 'none')
link.press('Enter')
self.assertEqual(self.page.evaluate('document.activeElement.id'), 'viewport')
self.assertEqual(self.page.locator('#viewport').evaluate('node=>node.scrollTop'), before)
self.page.keyboard.press('PageDown')
self.page.wait_for_function('top=>document.querySelector("#viewport").scrollTop>top', arg=before)
before = self.page.locator('#viewport').evaluate('node=>node.scrollTop')
self.page.evaluate('''()=>{const host=document.createElement('span');
document.querySelector('#content').append(host);
const root=host.attachShadow({mode:'open'}),button=document.createElement('button');
button.textContent='Chapter action';button.onclick=()=>window.__chapterAction=true;
root.append(button);button.focus({preventScroll:true});}''')
self.page.keyboard.press('Space')
self.assertTrue(self.page.evaluate('!!window.__chapterAction'))
self.assertEqual(self.page.locator('#viewport').evaluate('node=>node.scrollTop'), before)
def test_search_escape_restores_focus_and_expansion_state(self):
self.open_book()
self.open_with_keyboard()
self.page.locator('#full-search-toggle').press('Enter')
self.assertEqual(self.page.evaluate('document.activeElement.id'), 'full-search-input')
self.assertEqual(self.page.locator('#full-search-toggle').get_attribute('aria-controls'), 'full-search-view')
self.assertEqual(self.page.locator('#full-search-toggle').get_attribute('aria-expanded'), 'true')
self.page.locator('#full-search-input').fill('needle')
self.page.wait_for_function("()=>document.querySelector('#full-search-status').textContent==='600 个结果'")
self.page.keyboard.press('Escape')
self.assertEqual(self.page.evaluate('document.activeElement.id'), 'full-search-toggle')
self.assertEqual(self.page.locator('#full-search-toggle').get_attribute('aria-expanded'), 'false')
self.assertEqual(self.page.locator('#history').get_attribute('aria-expanded'), 'true')
self.page.locator('#full-search-toggle').press('Enter')
self.page.locator('#bookmarks-tab').click()
self.assertEqual(self.page.locator('#full-search-toggle').get_attribute('aria-expanded'), 'false')
self.page.keyboard.press('Escape')
self.assertEqual(self.page.evaluate('document.activeElement.id'), 'history')
def test_directory_current_location_and_keys_do_not_scroll_the_book(self):
self.open_book()
self.assertEqual(self.page.locator('#toc-list [aria-current="location"]').count(), 1)
self.assertEqual(self.page.locator('.toc-current-mark[aria-hidden="true"]').count(), 2)
link = self.page.locator('#toc-list [role="link"]').first
link.focus()
before = self.page.locator('#viewport').evaluate('node=>node.scrollTop')
self.page.keyboard.press('ArrowDown')
self.assertEqual(self.page.locator('#viewport').evaluate('node=>node.scrollTop'), before)
self.page.locator('#toc-list [role="link"]').last.press('Enter')
self.page.wait_for_function('()=>document.querySelector("#history").getAttribute("aria-expanded")==="false"')
self.assertEqual(self.page.evaluate('document.activeElement.id'), 'history')
self.page.locator('#history').press('Enter')
self.page.wait_for_function('()=>document.querySelector("#toc-list .toc-item:last-child [aria-current=location]")')
self.page.get_by_role('button', name='搜索目录', exact=True).press('Enter')
field = self.page.locator('#toc-panel .panel-search')
self.assertTrue(field.evaluate('node=>node===document.activeElement'))
self.assertEqual(self.page.locator('#toc-panel .panel-search-toggle').get_attribute('aria-controls'), field.get_attribute('id'))
def test_keyboard_progress_can_undo_without_pointer_events(self):
self.open_book('txt', 'Long plain text\n' * 2000)
self.page.locator('#history').press('Enter')
slider = self.page.get_by_role('slider', name='阅读进度')
before = self.page.locator('#viewport').evaluate('node=>node.scrollTop')
slider.press('End')
self.page.wait_for_function('top=>document.querySelector("#viewport").scrollTop>top+100', arg=before)
self.assertEqual(slider.get_attribute('aria-valuetext'), '100.0%')
self.page.get_by_role('button', name='撤销进度调整').press('Enter')
self.page.wait_for_function('top=>Math.abs(document.querySelector("#viewport").scrollTop-top)<2', arg=before)
self.assertEqual(self.page.locator('.reader-progress-undo').count(), 1)
self.assertTrue(self.page.locator('.reader-progress-undo').evaluate('node=>node.hidden'))
self.assertTrue(slider.evaluate('node=>node===document.activeElement'))
def test_bookmark_names_delete_focus_and_modal_priority(self):
self.open_book('txt', 'A document\n' * 200)
self.page.evaluate('''async()=>{const url=new URL(location.href).searchParams.get('url');
for(const [id,label] of [['one','First bookmark'],['two','Second bookmark']])
await VoiceOfMLReaderStore.putBookmark({id,url,label,createdAt:id==='one'?2:1});}''')
self.page.locator('#history').press('Enter')
self.page.locator('#bookmarks-tab').press('Enter')
self.page.get_by_role('button', name='编辑书签:First bookmark', exact=True).press('Enter')
self.assertEqual(self.page.get_by_role('dialog').count(), 1)
self.page.keyboard.press('Shift+Tab')
self.assertEqual(self.page.evaluate('document.activeElement.id'), 'bookmark-cancel')
self.page.keyboard.press('Escape')
self.assertEqual(self.page.locator('#history').get_attribute('aria-expanded'), 'true')
self.page.get_by_role('button', name='删除书签:First bookmark', exact=True).press('Enter')
self.page.wait_for_function("()=>document.querySelectorAll('#bookmarks-list .panel-item').length===1")
self.assertTrue(self.page.get_by_role('button', name='Second bookmark', exact=True).evaluate('node=>node===document.activeElement'))
self.page.get_by_role('button', name='删除书签:Second bookmark', exact=True).press('Enter')
self.page.wait_for_function("()=>!document.querySelector('#bookmarks-list .panel-item')")
self.assertEqual(self.page.evaluate('document.activeElement.id'), 'bookmarks-all')
def test_failure_is_announced_without_stealing_keyboard_focus(self):
self.open_book('unsupported', 'not supported')
self.assertEqual(self.page.get_by_role('alert').count(), 1)
self.assertIn('此文件暂不支持在线阅读', self.page.get_by_role('alert').inner_text())
self.assertFalse(self.page.evaluate("document.querySelector('.reader-error').contains(document.activeElement)"))