From 3f34ed8341c5d490bef5a0924012e99a956fa2af Mon Sep 17 00:00:00 2001 From: syrup <1032771964@qq.com> Date: Wed, 2 Sep 2026 10:36:04 +0800 Subject: [PATCH] fix: derive Amazon result page from URL --- scripts/test-amazon-search-pagination.ts | 70 ++++++++++++++++--- .../web-injectors/amazon-search-pagination.ts | 24 +++++++ src/page-worker/web-injectors/amazon.ts | 12 +--- 3 files changed, 89 insertions(+), 17 deletions(-) diff --git a/scripts/test-amazon-search-pagination.ts b/scripts/test-amazon-search-pagination.ts index 71475c0..6bc4d56 100644 --- a/scripts/test-amazon-search-pagination.ts +++ b/scripts/test-amazon-search-pagination.ts @@ -1,32 +1,86 @@ import { JSDOM } from 'jsdom'; -import { collectAmazonNextPageUrl } from '../src/page-worker/web-injectors/amazon-search-pagination'; +import { + collectAmazonCurrentPage, + collectAmazonNextPageUrl, +} from '../src/page-worker/web-injectors/amazon-search-pagination'; -const test = async (name: string, html: string, expected: string | null) => { - const dom = new JSDOM(html, { url: 'https://www.amazon.com/s?k=security+camera&page=1' }); +const withDom = async (url: string, html: string, callback: () => T | Promise) => { + const dom = new JSDOM(html, { url }); const previousDocument = globalThis.document; const previousWindow = globalThis.window; Object.assign(globalThis, { document: dom.window.document, window: dom.window }); try { - const actual = await collectAmazonNextPageUrl(); - if (actual !== expected) throw new Error(`${name}: expected ${expected}, received ${actual}`); + return await callback(); } finally { Object.assign(globalThis, { document: previousDocument, window: previousWindow }); dom.window.close(); } }; +const testNextPage = async (name: string, html: string, expected: string | null) => { + const actual = await withDom( + 'https://www.amazon.com/s?k=security+camera&page=1', + html, + collectAmazonNextPageUrl, + ); + if (actual !== expected) throw new Error(`${name}: expected ${expected}, received ${actual}`); +}; + +const testCurrentPage = async (name: string, url: string, html: string, expected: number) => { + const actual = await withDom(url, html, collectAmazonCurrentPage); + if (actual !== expected) throw new Error(`${name}: expected ${expected}, received ${actual}`); +}; + const main = async () => { - await test( + await testNextPage( 'active next page', 'Next', 'https://www.amazon.com/s?k=security+camera&page=2', ); - await test( + await testNextPage( 'disabled next page', 'Next', null, ); - await test('missing next page', '
No pagination
', null); + await testNextPage('missing next page', '
No pagination
', null); + await testCurrentPage( + 'URL page wins over stale pagination selection', + 'https://www.amazon.com/s?k=home+security&page=11&ref=sr_pg_11', + '12', + 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}`, + `${page + 1}`, + page, + ); + } + await testCurrentPage( + 'invalid URL page uses ref fallback', + 'https://www.amazon.com/s?k=home+security&page=invalid&ref=sr_pg_9', + '10', + 9, + ); + await testCurrentPage( + 'ref page fallback', + 'https://www.amazon.com/s?k=home+security&ref=sr_pg_7', + '
No selected pagination item
', + 7, + ); + await testCurrentPage( + 'selected pagination fallback', + 'https://www.amazon.com/s?k=home+security', + '5', + 5, + ); + await testCurrentPage( + 'first page fallback', + 'https://www.amazon.com/s?k=home+security', + '
No pagination
', + 1, + ); console.log('Amazon search pagination tests passed.'); }; diff --git a/src/page-worker/web-injectors/amazon-search-pagination.ts b/src/page-worker/web-injectors/amazon-search-pagination.ts index 20d140b..1c5870b 100644 --- a/src/page-worker/web-injectors/amazon-search-pagination.ts +++ b/src/page-worker/web-injectors/amazon-search-pagination.ts @@ -1,3 +1,27 @@ +/** + * 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. + */ +export async function collectAmazonCurrentPage(): Promise { + const currentUrl = new URL(window.location.href); + const parsePage = (value: string | null | undefined) => { + const page = Number.parseInt(value || '', 10); + return Number.isInteger(page) && page > 0 ? page : null; + }; + + const urlPage = parsePage(currentUrl.searchParams.get('page')); + if (urlPage) return urlPage; + + const refPage = parsePage( + /(?:^|_)pg_(\d+)(?:_|$)/.exec(currentUrl.searchParams.get('ref') || '')?.[1], + ); + if (refPage) return refPage; + + const selected = document.querySelector('.s-pagination-item.s-pagination-selected'); + return parsePage(selected?.textContent?.trim()) || 1; +} + /** * Reads the next search-page URL without navigating from the injected script. * Navigation must happen only after browser.scripting.executeScript has returned, diff --git a/src/page-worker/web-injectors/amazon.ts b/src/page-worker/web-injectors/amazon.ts index e398553..a50322c 100644 --- a/src/page-worker/web-injectors/amazon.ts +++ b/src/page-worker/web-injectors/amazon.ts @@ -2,12 +2,9 @@ import { BaseInjector } from './base'; import { collectAmazonSearchPageData, type AmazonSearchPagePattern } from './amazon-search-parser'; import { collectAmazonDetailImageUrls } from './amazon-detail-images'; import { waitForAmazonDetailPageReady } from './amazon-detail-page-ready'; -import { collectAmazonNextPageUrl } from './amazon-search-pagination'; +import { collectAmazonCurrentPage, collectAmazonNextPageUrl } from './amazon-search-pagination'; import { waitForAmazonSearchPageReady } from './amazon-search-page-ready'; -import { - collectAmazonNextReviewPageUrl, - waitForAmazonReviewPageReady, -} from './amazon-review-page'; +import { collectAmazonNextReviewPageUrl, waitForAmazonReviewPageReady } from './amazon-review-page'; export class AmazonSearchPageInjector extends BaseInjector { public waitForPageLoaded() { @@ -38,10 +35,7 @@ export class AmazonSearchPageInjector extends BaseInjector { } public async getCurrentPage() { - return this.run(async () => { - const node = document.querySelector('.s-pagination-item.s-pagination-selected'); - return node ? Number(node.innerText) : 1; - }); + return this.run(collectAmazonCurrentPage); } public async getNextPageUrl() {