diff --git a/scripts/test-amazon-search-page-ready.ts b/scripts/test-amazon-search-page-ready.ts index ec40af2..daec039 100644 --- a/scripts/test-amazon-search-page-ready.ts +++ b/scripts/test-amazon-search-page-ready.ts @@ -12,15 +12,26 @@ const main = async () => { }); Object.assign(globalThis, { document: dom.window.document, window: dom.window }); Object.assign(dom.window.HTMLElement.prototype, { scrollIntoView: () => undefined }); + Object.assign(dom.window, { scrollTo: () => undefined }); + + dom.window.setTimeout(() => { + const sponsoredModule = dom.window.document.createElement('section'); + sponsoredModule.innerHTML = + 'Sponsored

Lazy sponsored product

'; + dom.window.document.querySelector('main')?.append(sponsoredModule); + }, 500); const started = Date.now(); await waitForAmazonSearchPageReady(); const elapsed = Date.now() - started; - dom.window.close(); - if (elapsed >= 2000) { + if (!dom.window.document.querySelector('[data-asin="B000000002"]')) { + throw new Error('Lazy sponsored DOM was not loaded before page readiness completed'); + } + if (elapsed >= 5000) { throw new Error(`Ready search results were delayed by a persistent spinner (${elapsed}ms)`); } + dom.window.close(); const errorDom = new JSDOM( '

SORRY

Something went wrong on our end.

Please go back and try again.

', diff --git a/src/page-worker/web-injectors/amazon-search-page-ready.ts b/src/page-worker/web-injectors/amazon-search-page-ready.ts index 4ecd2a4..58b6a5b 100644 --- a/src/page-worker/web-injectors/amazon-search-page-ready.ts +++ b/src/page-worker/web-injectors/amazon-search-page-ready.ts @@ -7,6 +7,8 @@ export async function waitForAmazonSearchPageReady() { const deadline = Date.now() + 20000; const sleep = (milliseconds: number) => new Promise((resolve) => setTimeout(resolve, milliseconds)); + let nextButton: HTMLElement | null = null; + let pageBecameReady = false; while (Date.now() < deadline) { if (window.location.pathname.startsWith('/ap/signin')) { @@ -26,21 +28,78 @@ export async function waitForAmazonSearchPageReady() { const resultCount = document.querySelectorAll( '[data-component-type="s-search-result"][data-asin], .puis-card-container', ).length; - const nextButton = document.querySelector('.s-pagination-next'); + nextButton = document.querySelector('.s-pagination-next'); const noResults = /no results for/i.test(bodyText); const pageReady = - document.readyState !== 'loading' && - (resultCount > 0 || nextButton !== null || noResults); + document.readyState !== 'loading' && (resultCount > 0 || nextButton !== null || noResults); if (pageReady) { - nextButton?.scrollIntoView({ behavior: 'auto', block: 'center' }); - await sleep(750); - return; + pageBecameReady = true; + break; } window.scrollBy(0, 500 + ~~(Math.random() * 500)); await sleep(300); } - throw new Error('Amazon 搜索结果在 20 秒内未加载完成'); + if (!pageBecameReady) { + throw new Error('Amazon 搜索结果在 20 秒内未加载完成'); + } + + // Visit the full result stream instead of jumping straight to pagination. Amazon loads + // recommendation and sponsored modules when their region approaches the viewport. + for (const fraction of [0.15, 0.35, 0.55, 0.75, 0.92]) { + const documentHeight = Math.max( + document.documentElement.scrollHeight, + document.body?.scrollHeight || 0, + document.documentElement.offsetHeight, + document.body?.offsetHeight || 0, + ); + const top = Math.max(0, (documentHeight - window.innerHeight) * fraction); + window.scrollTo({ top, behavior: 'auto' }); + await sleep(180); + } + nextButton?.scrollIntoView({ behavior: 'auto', block: 'center' }); + + const relevantDomFingerprint = () => { + const root = document.querySelector('#search') || document.body; + if (!root) return ''; + const asins = Array.from(root.querySelectorAll('[data-asin]')) + .map((node) => node.dataset.asin || '') + .filter((asin) => /^[A-Z0-9]{10}$/.test(asin)); + const productLinks = Array.from( + root.querySelectorAll('a[href*="/dp/"], a[href*="%2Fdp%2F"]'), + ).map((anchor) => anchor.href); + const sponsoredLabels = root.querySelectorAll( + '[aria-label*="Sponsored" i], [title*="Sponsored" i]', + ).length; + const cards = root.querySelectorAll( + '[data-component-type="s-search-result"], .puis-card-container, .puisg-row', + ).length; + return [ + Array.from(new Set(asins)).sort().join(','), + Array.from(new Set(productLinks)).sort().join(','), + sponsoredLabels, + cards, + root.querySelectorAll('video').length, + root.querySelectorAll('img[src]').length, + ].join('|'); + }; + + const stabilityStartedAt = Date.now(); + const minimumStabilityWait = 1500; + const stableForRequired = 1000; + const maximumStabilityWait = 3500; + let fingerprint = relevantDomFingerprint(); + let stableSince = Date.now(); + while (Date.now() - stabilityStartedAt < maximumStabilityWait) { + await sleep(250); + const nextFingerprint = relevantDomFingerprint(); + if (nextFingerprint !== fingerprint) { + fingerprint = nextFingerprint; + stableSince = Date.now(); + } + const elapsed = Date.now() - stabilityStartedAt; + if (elapsed >= minimumStabilityWait && Date.now() - stableSince >= stableForRequired) return; + } }