fix: wait for lazy Amazon ad modules

This commit is contained in:
syrup 2026-09-02 11:28:18 +08:00
parent 100b4501e6
commit 832ae83443
2 changed files with 79 additions and 9 deletions

View File

@ -12,15 +12,26 @@ const main = async () => {
}); });
Object.assign(globalThis, { document: dom.window.document, window: dom.window }); Object.assign(globalThis, { document: dom.window.document, window: dom.window });
Object.assign(dom.window.HTMLElement.prototype, { scrollIntoView: () => undefined }); 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 =
'<span aria-label="Sponsored">Sponsored</span><article data-asin="B000000002"><h3>Lazy sponsored product</h3><a href="/dp/B000000002"><img src="https://example.com/lazy.jpg"></a></article>';
dom.window.document.querySelector('main')?.append(sponsoredModule);
}, 500);
const started = Date.now(); const started = Date.now();
await waitForAmazonSearchPageReady(); await waitForAmazonSearchPageReady();
const elapsed = Date.now() - started; 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)`); throw new Error(`Ready search results were delayed by a persistent spinner (${elapsed}ms)`);
} }
dom.window.close();
const errorDom = new JSDOM( const errorDom = new JSDOM(
'<main><h1>SORRY</h1><p>Something went wrong on our end.</p><p>Please go back and try again.</p></main>', '<main><h1>SORRY</h1><p>Something went wrong on our end.</p><p>Please go back and try again.</p></main>',

View File

@ -7,6 +7,8 @@ export async function waitForAmazonSearchPageReady() {
const deadline = Date.now() + 20000; const deadline = Date.now() + 20000;
const sleep = (milliseconds: number) => const sleep = (milliseconds: number) =>
new Promise<void>((resolve) => setTimeout(resolve, milliseconds)); new Promise<void>((resolve) => setTimeout(resolve, milliseconds));
let nextButton: HTMLElement | null = null;
let pageBecameReady = false;
while (Date.now() < deadline) { while (Date.now() < deadline) {
if (window.location.pathname.startsWith('/ap/signin')) { if (window.location.pathname.startsWith('/ap/signin')) {
@ -26,21 +28,78 @@ export async function waitForAmazonSearchPageReady() {
const resultCount = document.querySelectorAll( const resultCount = document.querySelectorAll(
'[data-component-type="s-search-result"][data-asin], .puis-card-container', '[data-component-type="s-search-result"][data-asin], .puis-card-container',
).length; ).length;
const nextButton = document.querySelector<HTMLElement>('.s-pagination-next'); nextButton = document.querySelector<HTMLElement>('.s-pagination-next');
const noResults = /no results for/i.test(bodyText); const noResults = /no results for/i.test(bodyText);
const pageReady = const pageReady =
document.readyState !== 'loading' && document.readyState !== 'loading' && (resultCount > 0 || nextButton !== null || noResults);
(resultCount > 0 || nextButton !== null || noResults);
if (pageReady) { if (pageReady) {
nextButton?.scrollIntoView({ behavior: 'auto', block: 'center' }); pageBecameReady = true;
await sleep(750); break;
return;
} }
window.scrollBy(0, 500 + ~~(Math.random() * 500)); window.scrollBy(0, 500 + ~~(Math.random() * 500));
await sleep(300); 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<HTMLElement>('[data-asin]'))
.map((node) => node.dataset.asin || '')
.filter((asin) => /^[A-Z0-9]{10}$/.test(asin));
const productLinks = Array.from(
root.querySelectorAll<HTMLAnchorElement>('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;
}
} }