"""Direct deep-position recovery and demand-loaded gaps in real Chromium.""" import unittest from tests import test_search_positions as positions class PositionWindowTests(unittest.TestCase): @classmethod def setUpClass(cls): positions.SearchPositionTests.setUpClass() @classmethod def tearDownClass(cls): positions.SearchPositionTests.tearDownClass() def setUp(self): self.fixture = positions.SearchPositionTests() self.fixture.setUp() self.page = self.fixture.page self.page.evaluate("STATE.query='paging-other';doSearch()") self.page.wait_for_function('!STATE.isLoading') self.page.evaluate('''() => { searchViewSnapshots.clear(); window.windowCalls=[];window.windowFailures=new Set();window.windowGeneration='one'; window.windowAnchor=49850;window.windowTotal=50001;window.windowStall=0; window.originalWindowKey=[...searchPositions.keys()].find(key=>JSON.parse(key).query==='paging-original'); const saved=searchPositions.get(originalWindowKey); Object.assign(saved,{index:49850,loadedPage:500,anchorId:'VoiceOfML/Test\\0paging-49850.txt',offset:11}); // Isolate recovery/demand requests from the normal next-page prefetch. prefetchNextPage=()=>{}; window.realMaybeLoadNextPage=maybeLoadNextPage;maybeLoadNextPage=()=>{}; fetchPositionPage=async(query,page,signal,anchorId)=>{ windowCalls.push(page); if(windowStall===page) await new Promise(resolve=>window.releaseWindow=resolve); if(windowFailures.has(page)) throw new Error('offline'); await new Promise(resolve=>setTimeout(resolve,10)); const total=windowTotal; return {page,page_size:query.pageSize,total,generation:windowGeneration,anchor_index:windowAnchor, results:Array.from({length:Math.min(query.pageSize,Math.max(0,total-(page-1)*query.pageSize))},(_,i)=>{ const index=(page-1)*query.pageSize+i; const name=index===windowAnchor?'paging-49850':index===49850?'moved-away':'paging-'+index; return {Repo:'VoiceOfML/Test',File:name,Extension:'txt',Folder:[],Size:1,ID:String(index)}; })}; }; STATE.query='paging-original';doSearch(false,false,true); }''') self.wait_anchor(49850) def tearDown(self): errors = self.fixture.fixture.errors[:] self.fixture.tearDown() self.assertEqual(errors, []) def wait_anchor(self, index, offset=11): self.page.wait_for_function('''({index,offset})=>!positionRestore && !!resultWindow && findVirtualIndex(getResultScrollTop())===index && !!STATE.results[index] && Math.abs(getResultScrollTop()-getVirtualOffset(index)-offset)<2''', arg={'index':index,'offset':offset}) def jump(self, index): self.page.evaluate('index=>{setResultScrollTop(getVirtualOffset(index)+11);renderVisible()}', index) def test_depth_500_uses_only_anchor_neighborhood_and_real_loaded_count(self): self.assertEqual(self.page.evaluate('windowCalls.slice(0,4)'), [1,498,499,500]) self.assertTrue(self.page.evaluate('windowCalls.every(page=>[1,498,499,500,501].includes(page))')) self.assertTrue(self.page.evaluate('''() => STATE.total === 50001 && STATE.results.length >= 50000 && STATE.results.length <= 50001 && resultWindow.count === Object.keys(STATE.results).length''')) self.assertEqual(self.page.locator('#current-result-position').input_value(), '49851') def test_jump_back_fills_only_visible_gap_and_keeps_anchor(self): self.jump(25050) self.wait_anchor(25050) self.assertEqual(self.page.evaluate('STATE.results[25050].File'), 'paging-25050') self.assertTrue(self.page.evaluate('windowCalls.every(page=>[1,498,499,500,501,248,249,250,251,252,253,254,255,256].includes(page))')) self.assertEqual(self.page.evaluate('getResultStableId(STATE.results[49850])'), 'VoiceOfML/Test\0paging-49850.txt') def test_gap_loading_invalidates_snapshot_bytes_and_keeps_prior_snapshot_independent(self): self.page.evaluate('window.beforeGap=saveSearchViewSnapshot()') self.jump(25050) self.wait_anchor(25050) result = self.page.evaluate('''() => { const after=saveSearchViewSnapshot(); return {beforeMissing:!beforeGap.results[25050],afterPresent:!!after.results[25050], exact:getSearchSnapshotResultsBytes(STATE.results,true)===new TextEncoder().encode(JSON.stringify(STATE.results)).byteLength}; }''') self.assertEqual(result, dict(beforeMissing=True, afterPresent=True, exact=True)) def test_failed_gap_keeps_saved_position_and_can_retry(self): self.page.set_viewport_size({'width':390,'height':844}) self.page.evaluate('windowFailures.add(251);saveSearchPosition()') self.jump(25050) self.page.locator('#search-window-status').get_by_text('重试加载', exact=True).wait_for() self.assertEqual(self.page.evaluate('windowCalls.filter(page=>page===251).length'), 3) self.page.wait_for_timeout(800) self.assertEqual(self.page.evaluate('windowCalls.filter(page=>page===251).length'), 3) self.assertEqual(self.page.evaluate('searchPositions.get(originalWindowKey).index'), 49850) self.page.evaluate('windowFailures.clear()') self.page.locator('#search-window-status button').click() self.wait_anchor(25050) self.assertEqual(self.page.evaluate('windowCalls.filter(page=>page===251).length'), 4) def test_transient_gap_failure_recovers_without_click_or_scroll(self): self.page.evaluate('windowFailures.add(251)') self.jump(25050) self.page.wait_for_function('windowCalls.includes(251)') self.assertEqual(self.page.locator('.result-window-placeholder button').count(), 0) self.page.evaluate('windowFailures.clear();for(let i=0;i<20;i++)renderVisible()') self.wait_anchor(25050) self.assertEqual(self.page.evaluate('windowCalls.filter(page=>page===251).length'), 2) def test_failed_prefetch_is_bounded_and_does_not_poison_demand(self): self.page.evaluate('windowFailures.add(251);loadResultWindowPage(251,true)') self.page.wait_for_function('resultWindow.prefetchFailures?.has(251)') self.page.evaluate('for(let i=0;i<20;i++)loadResultWindowPage(251,true)') self.assertFalse(self.page.evaluate('resultWindow.failures.has(251)')) self.assertEqual(self.page.evaluate('windowCalls.filter(page=>page===251).length'), 1) self.page.evaluate('windowFailures.clear()') self.jump(25050) self.wait_anchor(25050) self.assertEqual(self.page.evaluate('windowCalls.filter(page=>page===251).length'), 2) def test_demand_joins_failed_inflight_prefetch_and_owns_retries(self): self.page.evaluate('windowStall=251;windowFailures.add(251);loadResultWindowPage(251,true);void 0') self.page.wait_for_function('!!window.releaseWindow') self.jump(25050) self.page.evaluate('releaseWindow();windowStall=0') self.page.wait_for_timeout(100) self.page.evaluate('windowFailures.clear()') self.wait_anchor(25050) self.assertEqual(self.page.evaluate('windowCalls.filter(page=>page===251).length'), 2) def test_query_change_cancels_retry_delay(self): self.page.evaluate('windowFailures.add(251)') self.jump(25050) self.page.wait_for_function('windowCalls.includes(251)') self.page.evaluate("STATE.query='paging-latest';doSearch()") self.page.wait_for_timeout(1000) self.assertEqual(self.page.evaluate('windowCalls.filter(page=>page===251).length'), 1) self.assertTrue(self.page.evaluate('resultWindow===null')) def test_online_recovers_exhausted_visible_page_only(self): self.page.evaluate('windowFailures.add(251)') self.jump(25050) self.page.wait_for_function('resultWindow.failures.has(251)') self.page.evaluate("resultWindow.failures.set(101,new Error('offline'));windowFailures.clear();window.dispatchEvent(new Event('online'))") self.wait_anchor(25050) self.assertTrue(self.page.evaluate('resultWindow.failures.has(101)')) self.assertEqual(self.page.evaluate('windowCalls.filter(page=>page===251).length'), 4) self.assertFalse(self.page.evaluate('windowCalls.includes(101)')) def test_hidden_retry_waits_for_return_and_respects_concurrency(self): self.page.evaluate('windowFailures.add(251)') self.jump(25050) self.page.wait_for_function('windowCalls.includes(251)') self.page.evaluate("Object.defineProperty(document,'hidden',{configurable:true,value:true})") self.page.wait_for_timeout(900) self.assertEqual(self.page.evaluate('windowCalls.filter(page=>page===251).length'), 1) self.assertLessEqual(self.page.evaluate('resultWindow.pending.size'), 3) self.page.evaluate("windowFailures.clear();Object.defineProperty(document,'hidden',{configurable:true,value:false});document.dispatchEvent(new Event('visibilitychange'))") self.wait_anchor(25050) self.assertEqual(self.page.evaluate('windowCalls.filter(page=>page===251).length'), 2) def test_restore_retries_lookup_and_missing_neighbor_without_repeating_ready_pages(self): self.page.evaluate("STATE.query='paging-other';doSearch()") self.page.wait_for_function('!STATE.isLoading') self.page.evaluate("searchViewSnapshots.clear();windowCalls=[];windowFailures.add(1);STATE.query='paging-original';doSearch(false,false,true)") self.page.wait_for_function('windowCalls.includes(1)') self.page.evaluate('windowFailures.delete(1);windowFailures.add(499)') self.page.wait_for_function('windowCalls.includes(499)') self.page.evaluate('windowFailures.clear()') self.wait_anchor(49850) self.assertEqual(self.page.evaluate('windowCalls.filter(page=>page===1).length'), 2) self.assertEqual(self.page.evaluate('windowCalls.filter(page=>page===499).length'), 2) self.assertEqual(self.page.evaluate('windowCalls.filter(page=>page===498).length'), 1) def test_retry_policy_excludes_cancel_invalid_data_and_permanent_http_errors(self): self.assertEqual(self.page.evaluate('''() => { return [408,429,500,502,503,504,400,403,404].map(status=>isRecoverableSearchError({status})) .concat([new DOMException('Cancelled','AbortError'),new Error('RESTORE_PAGE_CHANGED'), new Error('INVALID_API_RESPONSE'),new Error('API_TIMEOUT'),new TypeError('Failed to fetch')].map(isRecoverableSearchError)); }'''), [True]*6 + [False]*6 + [True,True]) self.assertEqual(self.page.evaluate('''() => [408,429,403].map(status=>{ resetPagingRecovery();finishPagingAttempt(false,{status});return pagingFailures; })'''), [1,1,2]) def test_generation_change_relocates_without_mixing_pages(self): self.page.evaluate("saveSearchPosition();windowGeneration='two';windowAnchor=35050") self.jump(25050) self.page.locator('#search-window-status').get_by_text('重新定位', exact=True).wait_for() self.assertFalse(self.page.evaluate('!!STATE.results[25050]')) self.page.locator('#search-window-status button').click() self.wait_anchor(35050) self.assertEqual(self.page.evaluate('STATE.results[35050].File'), 'paging-49850') self.assertEqual(self.page.evaluate('resultWindow.generation'), 'two') def test_late_gap_response_cannot_replace_a_new_query(self): self.page.evaluate('windowStall=251') self.jump(25050) self.page.wait_for_function('!!window.releaseWindow') self.page.evaluate("STATE.query='paging-latest';doSearch();releaseWindow()") self.page.wait_for_function("STATE.query==='paging-latest' && !STATE.isLoading") self.page.wait_for_timeout(50) self.assertTrue(self.page.evaluate('resultWindow===null && STATE.results.length<1000')) def test_sparse_snapshot_and_selection_keep_only_actual_records(self): self.page.evaluate('DOM.multiSelectToggle.checked=true;updateSelectionUI();DOM.multiSelectAll.click()') self.assertTrue(self.page.evaluate('''() => Object.keys(selectedIndices).length >= 400 && Object.keys(selectedIndices).length <= Object.keys(STATE.results).length''')) self.assertTrue(self.page.evaluate('Object.keys(selectedIndices).every(index=>!!STATE.results[index])')) self.page.evaluate("STATE.query='paging-other';doSearch()") self.page.wait_for_function('!STATE.isLoading') self.page.evaluate("STATE.query='paging-original';doSearch(false,false,true)") self.wait_anchor(49850) self.assertEqual(self.page.evaluate('resultWindow.count'), self.page.evaluate('Object.keys(STATE.results).length')) self.assertFalse(self.page.evaluate('!!STATE.results[25050]')) def test_forward_append_reaches_exact_end_and_earlier_pages_remain_accessible(self): self.page.evaluate('realMaybeLoadNextPage(false,true)') self.page.wait_for_function('STATE.results.length===50001 && !STATE.hasMore') self.assertEqual(self.page.evaluate('STATE.results[50000].ID'), '50000') self.jump(150) self.wait_anchor(150) self.assertEqual(self.page.evaluate('STATE.results[150].ID'), '150') self.assertEqual(self.page.evaluate('STATE.total'), 50001) def test_drag_defers_gap_application_and_reuses_response_after_release(self): self.page.evaluate('windowStall=251') self.jump(25050) self.page.wait_for_function('!!window.releaseWindow') self.page.evaluate('VSCROLL.isDraggingThumb=true;releaseWindow()') self.page.wait_for_function('resultWindow.prefetched?.has(251)') self.assertFalse(self.page.evaluate('!!STATE.results[25050]')) self.page.evaluate('VSCROLL.isDraggingThumb=false') self.jump(35050) self.wait_anchor(35050) self.jump(25050) self.wait_anchor(25050) self.assertEqual(self.page.evaluate('windowCalls.filter(page=>page===251).length'), 1) def persist_viewport(self): self.page.evaluate('''async () => { searchPositions.get(originalWindowKey).savedAt=0;saveSearchPosition(); await new Promise(resolve=>{const tx=searchPositionDB.transaction(['positions','viewports']);tx.oncomplete=resolve}); }''') def assert_cached_viewport(self, offset=11): self.page.wait_for_function('positionRestore?.preview && !!window.releaseWindow') self.assertTrue(self.page.evaluate('''offset => { const box=DOM.resultsContainer.getBoundingClientRect(); return findVirtualIndex(getResultScrollTop())===49850 && Math.abs(getResultScrollTop()-getVirtualOffset(49850)-offset)<2 && getComputedStyle(DOM.resultsContainer).visibility==='visible' && [...DOM.resultsList.querySelectorAll('.result-window-placeholder')].every(row=>{ const rect=row.getBoundingClientRect();return rect.bottom<=box.top || rect.top>=box.bottom; }); }''', offset)) self.assertEqual('49851', self.page.locator('#current-result-position').input_value()) self.assertTrue(self.page.evaluate('Object.keys(VSCROLL.heights).length<300 && Object.keys(VSCROLL.heightTree).length<2000')) def test_persisted_viewport_shows_before_lookup_and_keeps_nodes_after_validation(self): self.persist_viewport() self.page.evaluate("STATE.query='paging-other';doSearch()") self.page.wait_for_function('!STATE.isLoading') self.page.evaluate("searchViewSnapshots.clear();searchViewportSnapshots.clear();windowStall=1;STATE.query='paging-original';doSearch(false,false,true)") self.assert_cached_viewport() self.page.evaluate("window.cachedRow=DOM.resultsList.querySelector('[data-index=\"49850\"]');releaseWindow()") self.wait_anchor(49850) self.assertTrue(self.page.evaluate("cachedRow===DOM.resultsList.querySelector('[data-index=\"49850\"]')")) def test_reload_displays_saved_content_while_network_is_blocked(self): self.page.set_viewport_size({'width':390,'height':844}) self.page.wait_for_timeout(100) self.persist_viewport() # Resizing can change fractional row geometry; restore the saved mobile offset. saved_offset = self.page.evaluate('searchPositions.get(originalWindowKey).offset') fetcher = self.page.evaluate('fetchPositionPage.toString()') self.page.add_init_script('''document.addEventListener('DOMContentLoaded',()=>{ window.windowCalls=[];window.windowFailures=new Set();window.windowGeneration='one'; window.windowAnchor=49850;window.windowTotal=50001;window.windowStall=1; prefetchNextPage=()=>{};maybeLoadNextPage=()=>{};fetchPositionPage=(''' + fetcher + '''); });''') self.page.reload(wait_until='domcontentloaded') self.assert_cached_viewport(saved_offset) self.page.evaluate('releaseWindow()') self.wait_anchor(49850, saved_offset) def test_loading_an_adjacent_page_does_not_replace_visible_rows(self): self.page.evaluate('window.keptRow=DOM.resultsList.querySelector(\'[data-index="49850"]\');loadResultWindowPage(497)') self.page.wait_for_function('resultWindow.pages.has(497)') self.assertTrue(self.page.evaluate('keptRow===DOM.resultsList.querySelector(\'[data-index="49850"]\')')) self.assertTrue(self.page.evaluate('''() => { const button=keptRow.querySelector('[data-action="read"]'), before=button.dataset.readerUrl; readerAssets={[getResultStableId(STATE.results[49850])]:{s:2,m:'p',p:'objects/00/'+'0'.repeat(64)+'/linearized.pdf'}}; refreshResultReaderActions();renderVisible(); return keptRow===DOM.resultsList.querySelector('[data-index="49850"]') && button===keptRow.querySelector('[data-action="read"]') && button.dataset.readerUrl!==before; }''')) def test_failed_validation_leaves_cached_content_visible_and_cancels_cleanly(self): self.persist_viewport() self.page.evaluate("STATE.query='paging-other';doSearch()") self.page.wait_for_function('!STATE.isLoading') self.page.evaluate("searchViewSnapshots.clear();windowFailures.add(1);STATE.query='paging-original';doSearch(false,false,true)") self.page.get_by_text('重试恢复', exact=True).wait_for() self.assertEqual(self.page.evaluate('findVirtualIndex(getResultScrollTop())'), 49850) self.assertEqual(self.page.evaluate('getComputedStyle(DOM.resultsContainer).visibility'), 'visible') for width in [1280, 390, 320]: self.page.set_viewport_size({'width': width, 'height': 844}) self.page.evaluate('STATE.isMobile=innerWidth<600;applyMobileMode()') self.assertTrue(self.page.locator('#load-info #retry-position-btn').is_visible()) self.assertTrue(self.page.locator('#retry-position-btn').evaluate('''button=>{ const retry=button.getBoundingClientRect(),top=document.getElementById('restart-position-btn').getBoundingClientRect(); const count=document.querySelector('.result-position-count').getBoundingClientRect(); const bar=document.getElementById('load-info').getBoundingClientRect(); return [retry,top].every(box=>box.left>=bar.left && box.right<=bar.right && box.top>=bar.top && box.bottom<=bar.bottom && !(box.leftcount.left && box.topcount.top)); }''')) self.page.locator('#restart-position-btn').click() self.page.wait_for_function('!positionRestore && !STATE.isLoading && !resultWindow') def test_saved_viewport_is_bounded_and_not_used_for_another_anchor(self): self.persist_viewport() self.assertLessEqual(self.page.evaluate('searchViewportSnapshots.get(originalWindowKey).records.length'), 80) self.page.evaluate("STATE.query='paging-other';doSearch()") self.page.wait_for_function('!STATE.isLoading') self.page.evaluate("searchViewSnapshots.clear();searchPositions.get(originalWindowKey).anchorId='different';windowStall=1;STATE.query='paging-original';doSearch(false,false,true)") self.page.wait_for_function('!!window.releaseWindow') self.assertFalse(self.page.evaluate('!!positionRestore.preview')) self.page.evaluate('releaseWindow()') self.wait_anchor(49850) def test_large_api_records_prioritize_visible_content_within_storage_budget(self): self.page.evaluate('''() => { STATE.results.forEach(record=>record.Metadata='中'.repeat(900)); const original=fetchPositionPage; fetchPositionPage=async (...args)=>{ const data=await original(...args);data.results.forEach(record=>record.Metadata='中'.repeat(900));return data; }; }''') self.persist_viewport() self.assertTrue(self.page.evaluate('''() => { const saved=searchViewportSnapshots.get(originalWindowKey); return saved.records.length<80 && new TextEncoder().encode(JSON.stringify(saved)).byteLength<=SEARCH_VIEWPORT_BYTES_MAX; }''')) self.page.evaluate("STATE.query='paging-other';doSearch()") self.page.wait_for_function('!STATE.isLoading') self.page.evaluate("searchViewSnapshots.clear();searchViewportSnapshots.clear();windowStall=1;STATE.query='paging-original';doSearch(false,false,true)") self.assert_cached_viewport() self.page.evaluate('releaseWindow()') self.wait_anchor(49850)