fix: bind Amazon results to visible page

This commit is contained in:
syrup 2026-09-02 10:57:01 +08:00
parent 6fbf8b7bcd
commit d42e19f42f
2 changed files with 43 additions and 9 deletions

View File

@ -44,23 +44,29 @@ const main = async () => {
);
await testNextPage('missing next page', '<main>No pagination</main>', null);
await testCurrentPage(
'URL page wins over stale pagination selection',
'visible page wins over hidden stale pagination selection',
'https://www.amazon.com/s?k=home+security&page=11&ref=sr_pg_11',
'<span class="s-pagination-item s-pagination-selected">12</span>',
'<div style="display:none"><span class="s-pagination-item s-pagination-selected">12</span></div><span class="s-pagination-item s-pagination-selected">11</span>',
11,
);
for (const page of [1, 2, 5, 10, 20]) {
await testCurrentPage(
`URL page ${page}`,
`https://www.amazon.com/s?k=home+security&page=${page}&ref=sr_pg_${page}`,
`<span class="s-pagination-item s-pagination-selected">${page + 1}</span>`,
`<span class="s-pagination-item s-pagination-selected">${page}</span>`,
page,
);
}
await testCurrentPage(
'visible page wins while URL has advanced',
'https://www.amazon.com/s?k=home+security&page=3&ref=sr_pg_3',
'<span class="s-pagination-item s-pagination-selected">2</span>',
2,
);
await testCurrentPage(
'invalid URL page uses ref fallback',
'https://www.amazon.com/s?k=home+security&page=invalid&ref=sr_pg_9',
'<span class="s-pagination-item s-pagination-selected">10</span>',
'<main>No selected pagination item</main>',
9,
);
await testCurrentPage(

View File

@ -1,7 +1,7 @@
/**
* Resolves the page being collected. Amazon's selected pagination element can
* point at the next page while the current results are still rendered, so the
* document URL is the authoritative source whenever it carries a page number.
* Resolves the page represented by the currently rendered search results.
* Amazon can update the URL before replacing the visible result DOM, so the
* rendered selected-page control is authoritative and the URL is only a fallback.
*/
export async function collectAmazonCurrentPage(): Promise<number> {
const currentUrl = new URL(window.location.href);
@ -10,6 +10,32 @@ export async function collectAmazonCurrentPage(): Promise<number> {
return Number.isInteger(page) && page > 0 ? page : null;
};
const isRendered = (node: HTMLElement) => {
for (let current: HTMLElement | null = node; current; current = current.parentElement) {
if (current.hidden || current.getAttribute('aria-hidden') === 'true') return false;
const style = current.ownerDocument.defaultView?.getComputedStyle(current);
if (
style &&
(style.display === 'none' ||
style.visibility === 'hidden' ||
style.visibility === 'collapse' ||
style.opacity === '0')
) {
return false;
}
}
if (document.documentElement.getClientRects().length === 0) return true;
return Array.from(node.getClientRects()).some((rect) => rect.width > 0 && rect.height > 0);
};
const renderedSelectedPages = Array.from(
document.querySelectorAll<HTMLElement>('.s-pagination-item.s-pagination-selected'),
)
.filter(isRendered)
.map((node) => parsePage(node.textContent?.trim()))
.filter((page): page is number => page !== null);
if (renderedSelectedPages.length > 0) return renderedSelectedPages[0];
const urlPage = parsePage(currentUrl.searchParams.get('page'));
if (urlPage) return urlPage;
@ -18,8 +44,10 @@ export async function collectAmazonCurrentPage(): Promise<number> {
);
if (refPage) return refPage;
const selected = document.querySelector<HTMLElement>('.s-pagination-item.s-pagination-selected');
return parsePage(selected?.textContent?.trim()) || 1;
const anySelected = document.querySelector<HTMLElement>(
'.s-pagination-item.s-pagination-selected',
);
return parsePage(anySelected?.textContent?.trim()) || 1;
}
/**