Download tests/test_reader_navigation_host.py from VoiceOfML/Search: direct link, hf CLI and curl.
- Browser
- Download file 11.4 kB
-
https://huggingface.co/spaces/VoiceOfML/Search/resolve/main/tests/test_reader_navigation_host.py
- Command line
-
hf download hf://spaces/VoiceOfML/Search/tests/test_reader_navigation_host.py
-
curl -L -o test_reader_navigation_host.py https://huggingface.co/spaces/VoiceOfML/Search/resolve/main/tests/test_reader_navigation_host.py
11.4 kB
| """Search-host navigation: source checks, session handoff and focus ownership.""" | |
| import unittest | |
| import subprocess | |
| from pathlib import Path | |
| from tests import test_paging_recovery as paging | |
| class ReaderNavigationHostTests(unittest.TestCase): | |
| def setUpClass(cls): | |
| paging.PagingRecoveryTests.setUpClass() | |
| def tearDownClass(cls): | |
| paging.PagingRecoveryTests.tearDownClass() | |
| def setUp(self): | |
| self.fixture = paging.PagingRecoveryTests() | |
| self.fixture.setUp() | |
| self.page = self.fixture.page | |
| self.page.route('**/static/reader.html?**', lambda route: route.fulfill( | |
| content_type='text/html', body='<body>reader</body>')) | |
| def tearDown(self): | |
| self.assertEqual(self.fixture.errors, []) | |
| self.fixture.tearDown() | |
| def test_bundled_shell_starts_without_separate_dependency_scripts(self): | |
| root = Path(__file__).resolve().parents[1] | |
| shell = subprocess.check_output(['node', '--input-type=module', '-e', ''' | |
| import {readFileSync} from 'node:fs'; | |
| import {stripBundledScripts} from './scripts/compose_app.mjs'; | |
| process.stdout.write(stripBundledScripts(readFileSync('static/index.html','utf8'))); | |
| '''], cwd=root) | |
| bundle = subprocess.check_output(['node', 'scripts/compose_app.mjs'], cwd=root) | |
| with self.fixture.browser.new_context(service_workers='block') as context: | |
| page = context.new_page() | |
| page.on('pageerror', lambda error: self.fixture.errors.append(str(error))) | |
| page.add_init_script(paging.FETCH_SCRIPT) | |
| page.route('**/api/**', lambda route: route.fulfill(content_type='application/json', body='[]')) | |
| page.route('https://vomebook-hitokoto.hf.space/**', lambda route: route.fulfill(body='{}')) | |
| page.route('**/static/app.js', lambda route: route.fulfill(content_type='text/javascript', body=bundle)) | |
| page.route(self.fixture.origin + '/?q=paging', lambda route: route.fulfill(content_type='text/html', body=shell)) | |
| page.goto(self.fixture.origin + '/?q=paging', wait_until='domcontentloaded') | |
| page.wait_for_function('STATE.results.length === 100 && !STATE.isLoading') | |
| self.assertEqual(page.locator('script[src]').count(), 1) | |
| self.assertTrue(page.evaluate('typeof VoiceOfMLSearchSession.createSearchSession === "function"')) | |
| self.assertTrue(page.evaluate('typeof VoiceOfMLDownloadController.createDownloadController === "function"')) | |
| self.assertTrue(page.evaluate('navigateToReader("/static/reader.html?ext=txt")')) | |
| self.assertTrue(page.evaluate('closeReaderOverlay()')) | |
| def test_replace_preserves_return_focus_session_and_background_attributes(self): | |
| result = self.page.evaluate('''() => { | |
| const trigger=document.createElement('button');document.body.append(trigger);trigger.focus(); | |
| const existing=document.createElement('div');existing.setAttribute('inert',''); | |
| existing.setAttribute('aria-hidden','false');document.body.append(existing); | |
| const readerPath=new URL(document.querySelector('script[src*="reader-navigation.js"]').src).pathname.replace('reader-navigation.js','reader.html'); | |
| const target=readerPath+'?ext=txt&url=https://huggingface.co/datasets/VoiceOfML/Test/resolve/main/a.txt'; | |
| const returnUrl=location.href; | |
| navigateToReader(target);const first=readerOverlay; | |
| const token=new URL(first.src).searchParams.get('nav'); | |
| const hidden=trigger.hasAttribute('inert') && trigger.getAttribute('aria-hidden')==='true'; | |
| handleReaderMessage({origin:location.origin,source:first.contentWindow,data:{type:'voice-reader-open',url:target+'&title=next'}}); | |
| const next=readerOverlay, saved=JSON.parse(sessionStorage.getItem('reader-navigation-current')); | |
| const state=history.state; | |
| const nextUrl=new URL(next.src); | |
| const inherited=nextUrl.searchParams.get('nav')===token && nextUrl.searchParams.get('return')===returnUrl; | |
| const session=saved.readerUrl===next.src && state.readerUrl===next.src && saved.returnScroll.viewKey; | |
| const share=!new URL(location.href).searchParams.has('nav') && !new URL(location.href).searchParams.has('return'); | |
| const removed=!first.isConnected; | |
| history.replaceState(null,'',returnUrl);closeReaderOverlay(); | |
| const restored=document.activeElement===trigger && !trigger.hasAttribute('inert') && !trigger.hasAttribute('aria-hidden') && | |
| existing.hasAttribute('inert') && existing.getAttribute('aria-hidden')==='false'; | |
| const closed=!next.isConnected && closeReaderOverlay()===false; | |
| trigger.remove();existing.remove();return {hidden,inherited,session:!!session,share,removed,restored,closed}; | |
| }''') | |
| self.assertTrue(all(result.values()), result) | |
| def test_click_replenishes_evicted_reader_metadata(self): | |
| result = self.page.evaluate('''() => { | |
| const button=DOM.resultsList.querySelector('[data-action="read"]'); | |
| const id=new URL(button.dataset.readerUrl,location.origin).searchParams.get('id'); | |
| getReaderLink(STATE.results[Number(button.closest('.result-item').dataset.index)]); | |
| const expected=JSON.parse(sessionStorage.getItem('reader-source:'+id)); | |
| for(let i=0;i<200;i++) cacheReaderMetadata('other-'+i,{url:'other'},true); | |
| const evicted=!sessionStorage.getItem('reader-source:'+id); | |
| let captured; | |
| navigateToReader=()=>{captured=JSON.parse(sessionStorage.getItem('reader-source:'+id));}; | |
| button.click(); | |
| return {evicted,expected,captured}; | |
| }''') | |
| self.assertTrue(result['evicted']) | |
| self.assertIsNotNone(result['expected']) | |
| self.assertEqual(result['captured'], result['expected']) | |
| def test_reader_folder_url_uses_current_filters(self): | |
| result = self.page.evaluate('''() => { | |
| STATE.filterMinSize=2048; STATE.filterMaxSize=8192; | |
| STATE.filterExtensions=['pdf','txt']; STATE.searchFolders=false; STATE.exact=false; | |
| DOM.sortSelect.value='size'; STATE.recordHistory=false; | |
| const url=new URL(getReaderFolderUrl({Repo:'VoiceOfML/Test',Folder:['a.b','书']})); | |
| return {path:url.pathname,params:Object.fromEntries(url.searchParams)}; | |
| }''') | |
| self.assertEqual(result['path'], '/Test') | |
| params = result['params'] | |
| self.assertEqual(params['sort'], 'size') | |
| self.assertEqual(params['ext'], 'pdf,txt') | |
| self.assertEqual(params['folder_self'], 'a.b/书') | |
| self.assertEqual(params['min_size'], '2KB') | |
| self.assertEqual(params['max_size'], '8KB') | |
| self.assertEqual(params['search_folders'], 'false') | |
| self.assertEqual(params['exact'], '0') | |
| self.assertEqual(params['history'], '0') | |
| self.assertNotIn('q', params) | |
| def test_reader_return_captures_cleared_query_before_search_finishes(self): | |
| result = self.page.evaluate('''() => { | |
| STATE.query = '旧查询'; DOM.searchInput.value = STATE.query; syncStateToURL(true); | |
| const originalSearch = doSearch; | |
| doSearch = () => new Promise(() => {}); | |
| try { | |
| submitSearchQuery('', {clearResults: true, updateInput: true}); | |
| const current = location.href; | |
| navigateToReader('/static/reader.html?ext=txt'); | |
| const saved = JSON.parse(sessionStorage.getItem('reader-navigation-current')); | |
| history.back(); | |
| return {current, route: saved.returnScroll.route, reader: readerOverlay !== null}; | |
| } finally { doSearch = originalSearch; } | |
| }''') | |
| self.assertNotIn('q=%E6%97%A7%E6%9F%A5%E8%AF%A2', result['current']) | |
| self.assertNotIn('q=%E6%97%A7%E6%9F%A5%E8%AF%A2', result['route']) | |
| self.assertTrue(result['reader']) | |
| def test_reader_folder_return_merges_all_current_search_settings(self): | |
| result = self.page.evaluate('''() => { | |
| STATE.query='手机'; | |
| STATE.filterExtensions=['pdf','txt']; | |
| STATE.filterMinSize=2048; STATE.filterMaxSize=8192; | |
| STATE.searchFolders=false; STATE.exact=false; STATE.recordHistory=false; | |
| STATE.useMirrorLinks=false; DOM.sortSelect.value='size'; | |
| let target=new URL('/Test?folder_self=docs', location.origin); | |
| target=readerNavigation.mergeSearchParams(target, buildSearchURLParams(), { | |
| keys: READER_RETURN_SEARCH_KEYS | |
| }); | |
| return Object.fromEntries(target.searchParams); | |
| }''') | |
| self.assertEqual(result['q'], '手机') | |
| self.assertEqual(result['ext'], 'pdf,txt') | |
| self.assertEqual(result['min_size'], '2KB') | |
| self.assertEqual(result['max_size'], '8KB') | |
| self.assertEqual(result['sort'], 'size') | |
| self.assertEqual(result['search_folders'], 'false') | |
| self.assertEqual(result['exact'], '0') | |
| self.assertEqual(result['history'], '0') | |
| self.assertEqual(result['mirror'], '0') | |
| self.assertEqual(result['folder_self'], 'docs') | |
| def test_untrusted_and_stale_messages_cannot_change_active_reader(self): | |
| result = self.page.evaluate('''() => { | |
| const readerPath=new URL(document.querySelector('script[src*="reader-navigation.js"]').src).pathname.replace('reader-navigation.js','reader.html'); | |
| const target=readerPath+'?ext=txt';navigateToReader(target); | |
| const first=readerOverlay, oldSource=first.contentWindow, theme=STATE.isDark; | |
| const message={type:'voice-reader-open',url:target+'&title=bad'}; | |
| handleReaderMessage({origin:'https://invalid.example',source:oldSource,data:message}); | |
| handleReaderMessage({origin:location.origin,source:window,data:message}); | |
| handleReaderMessage({origin:location.origin,source:oldSource,data:{...message,url:'/wrong-path'}}); | |
| const untouched=readerOverlay===first; | |
| handleReaderMessage({origin:location.origin,source:oldSource,data:{...message,url:target+'&title=valid'}}); | |
| const second=readerOverlay; | |
| handleReaderMessage({origin:location.origin,source:oldSource,data:{type:'voice-reader-theme',theme:theme?'light':'dark'}}); | |
| return {untouched,replaced:second!==first,staleIgnored:STATE.isDark===theme}; | |
| }''') | |
| self.assertTrue(all(result.values()), result) | |
| def test_unavailable_session_storage_does_not_block_replacement(self): | |
| result = self.page.evaluate('''() => { | |
| const readerPath=new URL(document.querySelector('script[src*="reader-navigation.js"]').src).pathname.replace('reader-navigation.js','reader.html'); | |
| const target=readerPath+'?ext=txt';navigateToReader(target);const first=readerOverlay; | |
| const original=Storage.prototype.setItem; | |
| try { | |
| Storage.prototype.setItem=()=>{throw new DOMException('Full','QuotaExceededError');}; | |
| handleReaderMessage({origin:location.origin,source:first.contentWindow,data:{type:'voice-reader-open',url:target+'&title=next'}}); | |
| return readerOverlay!==first && !first.isConnected && history.state.readerUrl===readerOverlay.src; | |
| } finally {Storage.prototype.setItem=original;} | |
| }''') | |
| self.assertTrue(result) | |