File size: 11,845 Bytes
9ebca63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ecb31fb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9ebca63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
"""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)"))