mirror of
https://github.com/primedigitaltech/azon_seeker.git
synced 2026-10-05 15:22:59 +08:00
fix: classify Amazon sponsored brand and video search ads
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
72b0c348a9
commit
7d446d8f54
@ -216,6 +216,721 @@ async function main() {
|
|||||||
assert.equal(groupedAdsByAsin.get('B000000014')?.adType, 'sponsoredDisplay');
|
assert.equal(groupedAdsByAsin.get('B000000014')?.adType, 'sponsoredDisplay');
|
||||||
assert.equal(groupedAdsByAsin.get('B000000014')?.placement, 'sidebar');
|
assert.equal(groupedAdsByAsin.get('B000000014')?.placement, 'sidebar');
|
||||||
|
|
||||||
|
const liveSponsoredBrandsDom = new JSDOM(
|
||||||
|
`
|
||||||
|
<main id="search">
|
||||||
|
<div class="s-main-slot">
|
||||||
|
<div class="s-widget AdHolder" data-cel-widget="CardInstanceArlo">
|
||||||
|
<div class="sb-headline">Arlo Security for any Home</div>
|
||||||
|
<span>Sponsored</span>
|
||||||
|
<div data-asin="B08AR2S6MC">
|
||||||
|
<span>Video Doorbell</span>
|
||||||
|
<a href="/sspa/click?ie=UTF8&url=%2Fdp%2FB08AR2S6MC">
|
||||||
|
<img data-src="https://example.com/arlo-1.jpg" src="https://example.com/arlo-1.jpg" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div data-asin="B0ARLO0002">
|
||||||
|
<span>Outdoor Camera</span>
|
||||||
|
<a href="/sspa/click?url=%2Fdp%2FB0ARLO0002">
|
||||||
|
<img src="https://example.com/arlo-2.jpg" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div data-asin="B0ARLO0003">
|
||||||
|
<span>Bundle</span>
|
||||||
|
<a href="/sspa/click?url=%2Fdp%2FB0ARLO0003">
|
||||||
|
<img src="https://example.com/arlo-3.jpg" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<article data-component-type="s-search-result" data-asin="B09RING001">
|
||||||
|
<span>Sponsored</span>
|
||||||
|
<h2>Ring Spotlight Cam Plus, Battery</h2>
|
||||||
|
<a href="/sspa/click?url=%2Fdp%2FB09RING001"><img class="s-image" src="https://example.com/ring.jpg" /></a>
|
||||||
|
</article>
|
||||||
|
<article data-component-type="s-search-result" data-asin="B09ORG0001">
|
||||||
|
<h2>Organic camera</h2>
|
||||||
|
<a href="/dp/B09ORG0001"><img class="s-image" src="https://example.com/org.jpg" /></a>
|
||||||
|
</article>
|
||||||
|
<div class="sbv-container AdHolder">
|
||||||
|
<span>Sponsored</span>
|
||||||
|
<video></video>
|
||||||
|
<div class="sbv-copy">PROTECT YOUR EQUIPMENT & FACILITIES</div>
|
||||||
|
<div data-asin="B0VOSKER01" class="sbv-product">
|
||||||
|
<h2>Vosker VKX Solar Powered 4G-LTE Outdoor Cellular Security Camera</h2>
|
||||||
|
<a href="/sspa/click?url=%2Fdp%2FB0VOSKER01"><img src="https://example.com/vosker.jpg" /></a>
|
||||||
|
<span class="a-price"><span class="a-offscreen">$379.99</span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<section class="brands-related-to-your-search AdHolder">
|
||||||
|
<h2>Brands related to your search</h2>
|
||||||
|
<span>Sponsored</span>
|
||||||
|
<div class="brand-card" data-asin="B0BRAND001">
|
||||||
|
<img src="https://example.com/vosker-brand.jpg" alt="VOSKER" />
|
||||||
|
<a href="/sspa/click?url=%2Fdp%2FB0BRAND001">VOSKER</a>
|
||||||
|
</div>
|
||||||
|
<div class="brand-card">
|
||||||
|
<img src="https://example.com/reolink-brand.jpg" alt="Reolink" />
|
||||||
|
<a href="/sspa/click?url=%2Fgp%2Fproduct%2FB0BRAND002">Reolink</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
`,
|
||||||
|
{ url: 'https://www.amazon.com/s?k=home+security' },
|
||||||
|
);
|
||||||
|
Object.assign(globalThis, {
|
||||||
|
document: liveSponsoredBrandsDom.window.document,
|
||||||
|
Node: liveSponsoredBrandsDom.window.Node,
|
||||||
|
Element: liveSponsoredBrandsDom.window.Element,
|
||||||
|
HTMLElement: liveSponsoredBrandsDom.window.HTMLElement,
|
||||||
|
HTMLAnchorElement: liveSponsoredBrandsDom.window.HTMLAnchorElement,
|
||||||
|
HTMLImageElement: liveSponsoredBrandsDom.window.HTMLImageElement,
|
||||||
|
});
|
||||||
|
const liveSponsoredBrands = await collectAmazonSearchPageData({ pattern: 'pattern-2' });
|
||||||
|
const liveByAsin = new Map(liveSponsoredBrands.map((item) => [item.asin, item]));
|
||||||
|
assert.equal(liveByAsin.get('B08AR2S6MC')?.adType, 'sponsoredBrands');
|
||||||
|
assert.equal(liveByAsin.get('B0ARLO0002')?.adType, 'sponsoredBrands');
|
||||||
|
assert.equal(liveByAsin.get('B0ARLO0003')?.adType, 'sponsoredBrands');
|
||||||
|
assert.equal(liveByAsin.get('B08AR2S6MC')?.placement, 'topOfSearch');
|
||||||
|
assert.equal(liveByAsin.get('B09RING001')?.adType, 'sponsoredProducts');
|
||||||
|
assert.equal(liveByAsin.get('B09ORG0001')?.adType, 'organic');
|
||||||
|
assert.equal(liveByAsin.get('B0VOSKER01')?.adType, 'sponsoredBrands');
|
||||||
|
assert.equal(liveByAsin.get('B0VOSKER01')?.adFormat, 'video');
|
||||||
|
assert.equal(liveByAsin.get('B0VOSKER01')?.placement, 'bottomOfSearch');
|
||||||
|
assert.equal(liveByAsin.get('B0BRAND001')?.adType, 'sponsoredBrands');
|
||||||
|
assert.equal(liveByAsin.get('B0BRAND002')?.adType, 'sponsoredBrands');
|
||||||
|
|
||||||
|
const lazyImageDom = new JSDOM(
|
||||||
|
`
|
||||||
|
<main id="search">
|
||||||
|
<div class="s-main-slot">
|
||||||
|
<div class="s-widget AdHolder">
|
||||||
|
<div class="sb-headline">Arlo Security for any Home</div>
|
||||||
|
<span>Sponsored</span>
|
||||||
|
<div data-asin="B08AR2LAZY">
|
||||||
|
<h3>Video Doorbell</h3>
|
||||||
|
<a href="/sspa/click?url=%2Fdp%2FB08AR2LAZY">
|
||||||
|
<img data-src="https://example.com/arlo-lazy.jpg" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
`,
|
||||||
|
{ url: 'https://www.amazon.com/s?k=home+security' },
|
||||||
|
);
|
||||||
|
Object.assign(globalThis, {
|
||||||
|
document: lazyImageDom.window.document,
|
||||||
|
Node: lazyImageDom.window.Node,
|
||||||
|
Element: lazyImageDom.window.Element,
|
||||||
|
HTMLElement: lazyImageDom.window.HTMLElement,
|
||||||
|
HTMLAnchorElement: lazyImageDom.window.HTMLAnchorElement,
|
||||||
|
HTMLImageElement: lazyImageDom.window.HTMLImageElement,
|
||||||
|
});
|
||||||
|
const lazyImageItems = await collectAmazonSearchPageData({ pattern: 'pattern-2' });
|
||||||
|
assert.equal(lazyImageItems.length, 1);
|
||||||
|
assert.equal(lazyImageItems[0]?.asin, 'B08AR2LAZY');
|
||||||
|
assert.equal(lazyImageItems[0]?.adType, 'sponsoredBrands');
|
||||||
|
assert.equal(lazyImageItems[0]?.imageSrc, 'https://example.com/arlo-lazy.jpg');
|
||||||
|
|
||||||
|
const classificationBoundaryDom = new JSDOM(
|
||||||
|
`
|
||||||
|
<main id="search">
|
||||||
|
<div class="s-main-slot">
|
||||||
|
<div class="s-widget AdHolder" data-cel-widget="CardInstanceArlo">
|
||||||
|
<div class="sb-headline">Arlo Security for any Home</div>
|
||||||
|
<span>Sponsored</span>
|
||||||
|
<div data-asin="B08ARLOSB1">
|
||||||
|
<span>Video Doorbell</span>
|
||||||
|
<a href="/sspa/click?url=%2Fdp%2FB08ARLOSB1">
|
||||||
|
<img src="https://example.com/arlo-banner-1.jpg" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div data-asin="B08ARLOSB2">
|
||||||
|
<span>Outdoor Camera</span>
|
||||||
|
<a href="/sspa/click?url=%2Fdp%2FB08ARLOSB2">
|
||||||
|
<img src="https://example.com/arlo-banner-2.jpg" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="s-widget AdHolder" data-cel-widget="search-grid-wrap">
|
||||||
|
<article data-component-type="s-search-result" data-asin="B0ARLOGRID" class="AdHolder">
|
||||||
|
<span>Sponsored</span>
|
||||||
|
<h2>Arlo Video Doorbell Wired</h2>
|
||||||
|
<a href="/sspa/click?url=%2Fdp%2FB0ARLOGRID">
|
||||||
|
<img class="s-image" src="https://example.com/arlo-grid.jpg" />
|
||||||
|
</a>
|
||||||
|
</article>
|
||||||
|
<article data-component-type="s-search-result" data-asin="B09RINGSP1" class="AdHolder">
|
||||||
|
<span>Sponsored</span>
|
||||||
|
<h2>Ring Spotlight Cam Plus, Battery</h2>
|
||||||
|
<a href="/sspa/click?url=%2Fdp%2FB09RINGSP1">
|
||||||
|
<img class="s-image" src="https://example.com/ring-grid.jpg" />
|
||||||
|
</a>
|
||||||
|
</article>
|
||||||
|
<article data-component-type="s-search-result" data-asin="B09ORGKEEP">
|
||||||
|
<h2>Organic camera</h2>
|
||||||
|
<a href="/dp/B09ORGKEEP"><img class="s-image" src="https://example.com/org-keep.jpg" /></a>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<article data-component-type="s-search-result" data-asin="B0TRACKSP1" class="AdHolder">
|
||||||
|
<span>Sponsored</span>
|
||||||
|
<h2>Arlo Go 2 LTE Security Camera</h2>
|
||||||
|
<a href="/sspa/click?url=%2Fdp%2FB0TRACKSP1">
|
||||||
|
<img class="s-image" src="https://example.com/arlo-track.jpg" />
|
||||||
|
</a>
|
||||||
|
<a href="/gp/product/B0TRACKXX2">related tracking</a>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article data-component-type="s-search-result" data-asin="B0DEMOSP01" class="AdHolder">
|
||||||
|
<span>Sponsored</span>
|
||||||
|
<h2>Ring Indoor Cam with demo</h2>
|
||||||
|
<video></video>
|
||||||
|
<a href="/dp/B0DEMOSP01">
|
||||||
|
<img class="s-image" src="https://example.com/ring-demo.jpg" />
|
||||||
|
</a>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<div class="sbv-container AdHolder">
|
||||||
|
<span>Sponsored</span>
|
||||||
|
<video></video>
|
||||||
|
<div class="sbv-copy">PROTECT YOUR EQUIPMENT & FACILITIES</div>
|
||||||
|
<div data-asin="B0VOSKER02" class="sbv-product">
|
||||||
|
<h2>Vosker VKX Solar Powered 4G-LTE Outdoor Cellular Security Camera</h2>
|
||||||
|
<a href="/sspa/click?url=%2Fdp%2FB0VOSKER02">
|
||||||
|
<img src="https://example.com/vosker-2.jpg" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<article data-component-type="s-search-result" data-asin="B0NOADVID1">
|
||||||
|
<h2>Organic with product video</h2>
|
||||||
|
<video></video>
|
||||||
|
<a href="/dp/B0NOADVID1">
|
||||||
|
<img class="s-image" src="https://example.com/organic-video.jpg" />
|
||||||
|
</a>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<section class="brands-related-to-your-search AdHolder">
|
||||||
|
<h2>Brands related to your search</h2>
|
||||||
|
<span>Sponsored</span>
|
||||||
|
<div class="brand-card" data-asin="B0BRANDWL1">
|
||||||
|
<img src="https://example.com/vosker-wall.jpg" alt="VOSKER" />
|
||||||
|
<a href="/sspa/click?url=%2Fdp%2FB0BRANDWL1">VOSKER</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<aside id="rightResultsATF" data-component-type="sponsored-display">
|
||||||
|
<span>Sponsored</span>
|
||||||
|
<article data-asin="B0SIDEBAR1">
|
||||||
|
<h3>Sidebar display product</h3>
|
||||||
|
<a href="/dp/B0SIDEBAR1"><img src="https://example.com/sidebar.jpg" /></a>
|
||||||
|
</article>
|
||||||
|
</aside>
|
||||||
|
</main>
|
||||||
|
`,
|
||||||
|
{ url: 'https://www.amazon.com/s?k=home+security' },
|
||||||
|
);
|
||||||
|
Object.assign(globalThis, {
|
||||||
|
document: classificationBoundaryDom.window.document,
|
||||||
|
Node: classificationBoundaryDom.window.Node,
|
||||||
|
Element: classificationBoundaryDom.window.Element,
|
||||||
|
HTMLElement: classificationBoundaryDom.window.HTMLElement,
|
||||||
|
HTMLAnchorElement: classificationBoundaryDom.window.HTMLAnchorElement,
|
||||||
|
HTMLImageElement: classificationBoundaryDom.window.HTMLImageElement,
|
||||||
|
});
|
||||||
|
const classificationBoundary = await collectAmazonSearchPageData({ pattern: 'pattern-2' });
|
||||||
|
const boundaryByAsin = new Map(classificationBoundary.map((item) => [item.asin, item]));
|
||||||
|
assert.equal(boundaryByAsin.get('B08ARLOSB1')?.adType, 'sponsoredBrands');
|
||||||
|
assert.equal(boundaryByAsin.get('B08ARLOSB2')?.adType, 'sponsoredBrands');
|
||||||
|
assert.equal(boundaryByAsin.get('B0ARLOGRID')?.adType, 'sponsoredProducts');
|
||||||
|
assert.equal(boundaryByAsin.get('B09RINGSP1')?.adType, 'sponsoredProducts');
|
||||||
|
assert.equal(boundaryByAsin.get('B09ORGKEEP')?.adType, 'organic');
|
||||||
|
assert.equal(boundaryByAsin.get('B0TRACKSP1')?.adType, 'sponsoredProducts');
|
||||||
|
assert.equal(boundaryByAsin.has('B0TRACKXX2'), false);
|
||||||
|
assert.equal(boundaryByAsin.get('B0DEMOSP01')?.adType, 'sponsoredProducts');
|
||||||
|
assert.equal(boundaryByAsin.get('B0VOSKER02')?.adType, 'sponsoredBrands');
|
||||||
|
assert.equal(boundaryByAsin.get('B0VOSKER02')?.adFormat, 'video');
|
||||||
|
assert.equal(boundaryByAsin.get('B0NOADVID1')?.adType, 'organic');
|
||||||
|
assert.equal(boundaryByAsin.get('B0BRANDWL1')?.adType, 'sponsoredBrands');
|
||||||
|
assert.equal(boundaryByAsin.get('B0SIDEBAR1')?.adType, 'sponsoredDisplay');
|
||||||
|
assert.equal(boundaryByAsin.get('B0SIDEBAR1')?.placement, 'sidebar');
|
||||||
|
|
||||||
|
const thematicLoomDom = new JSDOM(
|
||||||
|
`
|
||||||
|
<main id="search">
|
||||||
|
<div class="s-main-slot">
|
||||||
|
<article data-component-type="s-search-result" data-asin="B0ORGLOOM1">
|
||||||
|
<h2>Organic camera</h2>
|
||||||
|
<a href="/dp/B0ORGLOOM1"><img class="s-image" src="https://example.com/org-loom.jpg" /></a>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="s-widget-container celwidget slot=MAIN template=FEATURED_ASINS_LIST widget=loom-desktop-bottom-slot_sptw-trending-hardlines-bottom-na-page2plus-t1"
|
||||||
|
cel_widget_id="MAIN-FEATURED_ASINS_LIST-22"
|
||||||
|
>
|
||||||
|
<div class="a-section s-include-content-margin">
|
||||||
|
<h2 id="loom-desktop-bottom-slot_featuredasins-heading">Trending now</h2>
|
||||||
|
<span
|
||||||
|
data-action="multi-ad-feedback-form-trigger"
|
||||||
|
data-multi-ad-feedback-form-trigger='{"adPlacementMetaData":{"slotName":"sp_search_thematic_btf-trending-hardlines","feedbackType":"sponsoredProductsLoom"}}'
|
||||||
|
>
|
||||||
|
<a aria-label="View Sponsored information or leave ad feedback" class="s-widget-sponsored-label-text">Sponsored</a>
|
||||||
|
</span>
|
||||||
|
<span data-component-type="s-searchgrid-carousel" class="s-searchgrid-carousel">
|
||||||
|
<ol class="a-carousel">
|
||||||
|
<li class="a-carousel-card" aria-hidden="false">
|
||||||
|
<div data-asin="B0B2MPYRKP" class="s-result-item">
|
||||||
|
<div class="puis-card-container">
|
||||||
|
<h2>iulock Keypad Smart Door Lock</h2>
|
||||||
|
<a href="/sspa/click?url=%2Fdp%2FB0B2MPYRKP&widgetName=sp_search_thematic_btf">
|
||||||
|
<img class="s-image" src="https://example.com/iulock.jpg" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="a-carousel-card" aria-hidden="false">
|
||||||
|
<div data-asin="B0F8HS3RRT" class="s-result-item">
|
||||||
|
<div class="puis-card-container">
|
||||||
|
<h2>Richelock fingerprint lock</h2>
|
||||||
|
<a href="/sspa/click?url=%2Fdp%2FB0F8HS3RRT&widgetName=sp_search_thematic_btf">
|
||||||
|
<img class="s-image" src="https://example.com/richelock.jpg" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="a-carousel-card" aria-hidden="false">
|
||||||
|
<div data-asin="B0GQX6GHK2" class="s-result-item">
|
||||||
|
<div class="puis-card-container">
|
||||||
|
<h2>Swann MaxRanger4K</h2>
|
||||||
|
<a href="/sspa/click?url=%2Fdp%2FB0GQX6GHK2&widgetName=sp_search_thematic_btf">
|
||||||
|
<img class="s-image" src="https://example.com/swann.jpg" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="a-carousel-card" aria-hidden="false" style="visibility: hidden;">
|
||||||
|
<div data-asin="B0CN95HR9V" class="s-result-item">
|
||||||
|
<div class="puis-card-container">
|
||||||
|
<h2>Solar Fence Lights</h2>
|
||||||
|
<a href="/sspa/click?url=%2Fdp%2FB0CN95HR9V&widgetName=sp_search_thematic_btf">
|
||||||
|
<img class="s-image" src="https://example.com/solar.jpg" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="a-carousel-card" aria-hidden="true" style="visibility: hidden;">
|
||||||
|
<div data-asin="B07M68S376" class="s-result-item">
|
||||||
|
<div class="puis-card-container">
|
||||||
|
<h2>SECRUI Wireless Doorbell</h2>
|
||||||
|
<a href="/sspa/click?url=%2Fdp%2FB07M68S376&widgetName=sp_search_thematic_btf">
|
||||||
|
<img class="s-image" src="https://example.com/secrui.jpg" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sbv-container AdHolder">
|
||||||
|
<span>Sponsored</span>
|
||||||
|
<video></video>
|
||||||
|
<div class="sbv-copy">PROTECT YOUR EQUIPMENT & FACILITIES</div>
|
||||||
|
<div data-asin="B0VOSKERLM" class="sbv-product">
|
||||||
|
<h2>Vosker VKX Solar Powered camera</h2>
|
||||||
|
<a href="/sspa/click?url=%2Fdp%2FB0VOSKERLM"><img src="https://example.com/vosker-loom.jpg" /></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="s-widget AdHolder" data-cel-widget="CardInstanceArlo">
|
||||||
|
<div class="sb-headline">Arlo Security for any Home</div>
|
||||||
|
<span>Sponsored</span>
|
||||||
|
<div data-asin="B08ARLOLM1">
|
||||||
|
<span>Video Doorbell</span>
|
||||||
|
<a href="/sspa/click?url=%2Fdp%2FB08ARLOLM1">
|
||||||
|
<img src="https://example.com/arlo-loom.jpg" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
`,
|
||||||
|
{ url: 'https://www.amazon.com/s?k=home+security' },
|
||||||
|
);
|
||||||
|
Object.assign(globalThis, {
|
||||||
|
document: thematicLoomDom.window.document,
|
||||||
|
Node: thematicLoomDom.window.Node,
|
||||||
|
Element: thematicLoomDom.window.Element,
|
||||||
|
HTMLElement: thematicLoomDom.window.HTMLElement,
|
||||||
|
HTMLAnchorElement: thematicLoomDom.window.HTMLAnchorElement,
|
||||||
|
HTMLImageElement: thematicLoomDom.window.HTMLImageElement,
|
||||||
|
});
|
||||||
|
const thematicLoom = await collectAmazonSearchPageData({ pattern: 'pattern-2' });
|
||||||
|
const thematicByAsin = new Map(thematicLoom.map((item) => [item.asin, item]));
|
||||||
|
assert.equal(thematicByAsin.get('B0ORGLOOM1')?.adType, 'organic');
|
||||||
|
assert.equal(thematicByAsin.get('B0B2MPYRKP')?.adType, 'sponsoredProducts');
|
||||||
|
assert.equal(thematicByAsin.get('B0F8HS3RRT')?.adType, 'sponsoredProducts');
|
||||||
|
assert.equal(thematicByAsin.get('B0GQX6GHK2')?.adType, 'sponsoredProducts');
|
||||||
|
assert.equal(
|
||||||
|
thematicByAsin.get('B0B2MPYRKP')?.placementId,
|
||||||
|
thematicByAsin.get('B0F8HS3RRT')?.placementId,
|
||||||
|
);
|
||||||
|
assert.equal(thematicByAsin.get('B0B2MPYRKP')?.adProductIndex, 1);
|
||||||
|
assert.equal(thematicByAsin.get('B0F8HS3RRT')?.adProductIndex, 2);
|
||||||
|
assert.equal(thematicByAsin.get('B0GQX6GHK2')?.adProductIndex, 3);
|
||||||
|
assert.equal(thematicByAsin.has('B0CN95HR9V'), false);
|
||||||
|
assert.equal(thematicByAsin.has('B07M68S376'), false);
|
||||||
|
assert.equal(thematicByAsin.get('B0VOSKERLM')?.adType, 'sponsoredBrands');
|
||||||
|
assert.equal(thematicByAsin.get('B0VOSKERLM')?.adFormat, 'video');
|
||||||
|
assert.equal(thematicByAsin.get('B08ARLOLM1')?.adType, 'sponsoredBrands');
|
||||||
|
|
||||||
|
const organicLoomDom = new JSDOM(
|
||||||
|
`
|
||||||
|
<main id="search">
|
||||||
|
<div class="s-main-slot">
|
||||||
|
<article data-component-type="s-search-result" data-asin="B0ORGCFV01">
|
||||||
|
<h2>Organic camera</h2>
|
||||||
|
<a href="/dp/B0ORGCFV01"><img class="s-image" src="https://example.com/org-cfv.jpg" /></a>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="s-widget-container celwidget slot=MAIN template=FEATURED_ASINS_LIST widget=loom-desktop-bottom-slot_sptw-frequently-viewed-bottom-na"
|
||||||
|
cel_widget_id="MAIN-FEATURED_ASINS_LIST-23"
|
||||||
|
>
|
||||||
|
<div class="a-section s-include-content-margin">
|
||||||
|
<h2 id="loom-desktop-bottom-slot_featuredasins-heading">Customers frequently viewed</h2>
|
||||||
|
<span
|
||||||
|
data-action="multi-ad-feedback-form-trigger"
|
||||||
|
data-multi-ad-feedback-form-trigger='{"adPlacementMetaData":{"slotName":"sp_search_thematic_btf-frequently-viewed","feedbackType":"sponsoredProductsLoom"}}'
|
||||||
|
></span>
|
||||||
|
<span
|
||||||
|
aria-label="Sponsored"
|
||||||
|
style="position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0, 0, 0, 0)"
|
||||||
|
>Sponsored</span>
|
||||||
|
<span data-component-type="s-searchgrid-carousel" class="s-searchgrid-carousel">
|
||||||
|
<ol class="a-carousel">
|
||||||
|
<li class="a-carousel-card" aria-hidden="false">
|
||||||
|
<div data-asin="B0CFV00001" class="s-result-item">
|
||||||
|
<div class="puis-card-container">
|
||||||
|
<h2>ANNKE security camera</h2>
|
||||||
|
<a href="/sspa/click?url=%2Fdp%2FB0CFV00001&widgetName=sp_search_thematic_btf">
|
||||||
|
<img class="s-image" src="https://example.com/annke.jpg" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="a-carousel-card" aria-hidden="false">
|
||||||
|
<div data-asin="B0CFV00002" class="s-result-item">
|
||||||
|
<div class="puis-card-container">
|
||||||
|
<h2>Night Owl camera</h2>
|
||||||
|
<a href="/sspa/click?url=%2Fdp%2FB0CFV00002&widgetName=sp_search_thematic_btf">
|
||||||
|
<img class="s-image" src="https://example.com/nightowl.jpg" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="s-widget-container celwidget slot=MAIN template=FEATURED_ASINS_LIST widget=loom-desktop-bottom-slot_sptw-recently-bought-bottom-na"
|
||||||
|
cel_widget_id="MAIN-FEATURED_ASINS_LIST-24"
|
||||||
|
>
|
||||||
|
<h2>Recently bought and rated</h2>
|
||||||
|
<span data-component-type="s-searchgrid-carousel" class="s-searchgrid-carousel">
|
||||||
|
<ol class="a-carousel">
|
||||||
|
<li class="a-carousel-card" aria-hidden="false">
|
||||||
|
<div data-asin="B0RBR00001" class="s-result-item">
|
||||||
|
<div class="puis-card-container">
|
||||||
|
<h2>Rated doorbell</h2>
|
||||||
|
<a href="/dp/B0RBR00001">
|
||||||
|
<img class="s-image" src="https://example.com/rated.jpg" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
`,
|
||||||
|
{ url: 'https://www.amazon.com/s?k=home+security' },
|
||||||
|
);
|
||||||
|
Object.assign(globalThis, {
|
||||||
|
document: organicLoomDom.window.document,
|
||||||
|
Node: organicLoomDom.window.Node,
|
||||||
|
Element: organicLoomDom.window.Element,
|
||||||
|
HTMLElement: organicLoomDom.window.HTMLElement,
|
||||||
|
HTMLAnchorElement: organicLoomDom.window.HTMLAnchorElement,
|
||||||
|
HTMLImageElement: organicLoomDom.window.HTMLImageElement,
|
||||||
|
});
|
||||||
|
const organicLoom = await collectAmazonSearchPageData({ pattern: 'pattern-2' });
|
||||||
|
const organicLoomByAsin = new Map(organicLoom.map((item) => [item.asin, item]));
|
||||||
|
assert.equal(organicLoomByAsin.get('B0ORGCFV01')?.adType, 'organic');
|
||||||
|
assert.equal(organicLoomByAsin.get('B0CFV00001')?.adType, 'organic');
|
||||||
|
assert.equal(organicLoomByAsin.get('B0CFV00002')?.adType, 'organic');
|
||||||
|
assert.equal(organicLoomByAsin.get('B0RBR00001')?.adType, 'organic');
|
||||||
|
assert.equal(
|
||||||
|
organicLoom.some((item) => item.adType === 'sponsoredProducts'),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
const organicVideoStripDom = new JSDOM(
|
||||||
|
`
|
||||||
|
<main id="search">
|
||||||
|
<div class="s-main-slot">
|
||||||
|
<article data-component-type="s-search-result" data-asin="B0ORGVID01">
|
||||||
|
<h2>Organic camera</h2>
|
||||||
|
<a href="/dp/B0ORGVID01"><img class="s-image" src="https://example.com/org-vid.jpg" /></a>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<div class="sbv-container AdHolder">
|
||||||
|
<video></video>
|
||||||
|
<a href="/stores/chamberlain"><span class="brand-logo">Chamberlain</span></a>
|
||||||
|
<div class="sb-headline">Chamberlain myQ</div>
|
||||||
|
<div class="sbv-copy">Control your garage from anywhere</div>
|
||||||
|
<div data-asin="B0NOSBV001" class="sbv-product">
|
||||||
|
<h2>Chamberlain garage opener</h2>
|
||||||
|
<a href="/dp/B0NOSBV001"><img src="https://example.com/chamberlain-nosponsored.jpg" /></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sbv-container AdHolder">
|
||||||
|
<span
|
||||||
|
aria-label="Sponsored"
|
||||||
|
style="position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0, 0, 0, 0)"
|
||||||
|
>Sponsored</span>
|
||||||
|
<video></video>
|
||||||
|
<a href="/stores/vosker"><span class="brand-logo">Vosker</span></a>
|
||||||
|
<div class="sbv-copy">PROTECT YOUR EQUIPMENT</div>
|
||||||
|
<div data-asin="B0HIDDENSB" class="sbv-product">
|
||||||
|
<h2>Vosker solar camera</h2>
|
||||||
|
<a href="/sspa/click?url=%2Fdp%2FB0HIDDENSB"><img src="https://example.com/vosker-hidden.jpg" /></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sbv-container AdHolder">
|
||||||
|
<span>Sponsored</span>
|
||||||
|
<video></video>
|
||||||
|
<a href="/stores/vosker"><span class="brand-logo">Vosker</span></a>
|
||||||
|
<div class="sbv-copy">PROTECT YOUR EQUIPMENT</div>
|
||||||
|
<div data-asin="B0YESBV001" class="sbv-product">
|
||||||
|
<h2>Vosker with visible sponsored</h2>
|
||||||
|
<a href="/sspa/click?url=%2Fdp%2FB0YESBV001"><img src="https://example.com/vosker-yes.jpg" /></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
`,
|
||||||
|
{ url: 'https://www.amazon.com/s?k=home+security' },
|
||||||
|
);
|
||||||
|
Object.assign(globalThis, {
|
||||||
|
document: organicVideoStripDom.window.document,
|
||||||
|
Node: organicVideoStripDom.window.Node,
|
||||||
|
Element: organicVideoStripDom.window.Element,
|
||||||
|
HTMLElement: organicVideoStripDom.window.HTMLElement,
|
||||||
|
HTMLAnchorElement: organicVideoStripDom.window.HTMLAnchorElement,
|
||||||
|
HTMLImageElement: organicVideoStripDom.window.HTMLImageElement,
|
||||||
|
});
|
||||||
|
const organicVideoStrip = await collectAmazonSearchPageData({ pattern: 'pattern-2' });
|
||||||
|
const videoStripByAsin = new Map(organicVideoStrip.map((item) => [item.asin, item]));
|
||||||
|
assert.equal(videoStripByAsin.get('B0ORGVID01')?.adType, 'organic');
|
||||||
|
assert.equal(videoStripByAsin.get('B0NOSBV001')?.adType, 'organic');
|
||||||
|
assert.equal(videoStripByAsin.get('B0HIDDENSB')?.adType, 'organic');
|
||||||
|
assert.equal(videoStripByAsin.get('B0YESBV001')?.adType, 'sponsoredBrands');
|
||||||
|
assert.equal(videoStripByAsin.get('B0YESBV001')?.adFormat, 'video');
|
||||||
|
assert.deepEqual(
|
||||||
|
organicVideoStrip.filter((item) => item.adType === 'sponsoredBrands').map((item) => item.asin),
|
||||||
|
['B0YESBV001'],
|
||||||
|
);
|
||||||
|
|
||||||
|
const liveBrandWallDom = new JSDOM(
|
||||||
|
`
|
||||||
|
<main id="search">
|
||||||
|
<div class="s-main-slot">
|
||||||
|
<article data-component-type="s-search-result" data-asin="B0ORGGRID1">
|
||||||
|
<h2>Organic camera</h2>
|
||||||
|
<a href="/dp/B0ORGGRID1"><img class="s-image" src="https://example.com/org-grid.jpg" /></a>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="s-result-item s-widget AdHolder s-flex-full-width"
|
||||||
|
data-asin=""
|
||||||
|
cel_widget_id="multi-brand-creative-desktop_loom-desktop-brand-footer-slot_46"
|
||||||
|
>
|
||||||
|
<span aria-label="Brands related to your search" role="heading">Brands related to your search</span>
|
||||||
|
<span aria-label="Leave feedback on Sponsored ad" tabindex="0" role="button">Sponsored</span>
|
||||||
|
<div class="sbx-desktop" data-aid="301209426575402" data-slot="desktop-hsa-3psl">
|
||||||
|
<a href="/stores/page/VOSKER?lp_asins=B0D5Z3V464%2CB0FY2QCXRJ%2CB0FY2MRK56&lp_query=home%20security&lp_slot=desktop-hsa-3psl">
|
||||||
|
<img
|
||||||
|
alt="Vosker"
|
||||||
|
src="https://images-na.ssl-images-amazon.com/images/G/01/x-locale/common/grey-pixel.gif"
|
||||||
|
data-src="https://example.com/vosker-wall.jpg"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="sbx-desktop" data-aid="301252396855702" data-slot="desktop-hsa-3psl">
|
||||||
|
<a href="/stores/page/REOLINK?lp_asins=B0C2PY2KTJ%2CB0FM45MSS8&lp_slot=desktop-hsa-3psl">
|
||||||
|
<img
|
||||||
|
alt="REOLINK"
|
||||||
|
src="https://images-na.ssl-images-amazon.com/images/G/01/x-locale/common/grey-pixel.gif"
|
||||||
|
data-src="https://example.com/reolink-wall.jpg"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
`,
|
||||||
|
{ url: 'https://www.amazon.com/s?k=home+security' },
|
||||||
|
);
|
||||||
|
Object.assign(globalThis, {
|
||||||
|
document: liveBrandWallDom.window.document,
|
||||||
|
Node: liveBrandWallDom.window.Node,
|
||||||
|
Element: liveBrandWallDom.window.Element,
|
||||||
|
HTMLElement: liveBrandWallDom.window.HTMLElement,
|
||||||
|
HTMLAnchorElement: liveBrandWallDom.window.HTMLAnchorElement,
|
||||||
|
HTMLImageElement: liveBrandWallDom.window.HTMLImageElement,
|
||||||
|
});
|
||||||
|
const liveBrandWall = await collectAmazonSearchPageData({ pattern: 'pattern-2' });
|
||||||
|
const liveBrandWallByAsin = new Map(liveBrandWall.map((item) => [item.asin, item]));
|
||||||
|
assert.equal(liveBrandWallByAsin.get('B0ORGGRID1')?.adType, 'organic');
|
||||||
|
assert.equal(liveBrandWallByAsin.get('B0D5Z3V464')?.adType, 'sponsoredBrands');
|
||||||
|
assert.equal(liveBrandWallByAsin.get('B0C2PY2KTJ')?.adType, 'sponsoredBrands');
|
||||||
|
assert.equal(liveBrandWallByAsin.get('B0D5Z3V464')?.title, 'Vosker');
|
||||||
|
assert.equal(
|
||||||
|
liveBrandWallByAsin.get('B0D5Z3V464')?.imageSrc,
|
||||||
|
'https://example.com/vosker-wall.jpg',
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
liveBrandWallByAsin.get('B0C2PY2KTJ')?.imageSrc,
|
||||||
|
'https://example.com/reolink-wall.jpg',
|
||||||
|
);
|
||||||
|
assert.equal(liveBrandWallByAsin.has('B0FY2QCXRJ'), false);
|
||||||
|
|
||||||
|
const influencerPicksDom = new JSDOM(
|
||||||
|
`
|
||||||
|
<main id="search">
|
||||||
|
<div class="s-main-slot">
|
||||||
|
<article data-component-type="s-search-result" data-asin="B0ORGINF01">
|
||||||
|
<h2>Organic camera</h2>
|
||||||
|
<a href="/dp/B0ORGINF01"><img class="s-image" src="https://example.com/org-inf.jpg" /></a>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<section class="s-widget-container celwidget">
|
||||||
|
<h2>Picks from Amazon Influencers</h2>
|
||||||
|
<span>Sponsored</span>
|
||||||
|
<span>From Amazon Influencer storefronts</span>
|
||||||
|
<div data-asin="B0ANNKE001" class="puis-card-container">
|
||||||
|
<h2>ANNKE 4K PoE Security Camera System</h2>
|
||||||
|
<a href="/sspa/click?url=%2Fdp%2FB0ANNKE001"><img src="https://example.com/annke.jpg" /></a>
|
||||||
|
</div>
|
||||||
|
<div data-asin="B0NIGHTO01" class="puis-card-container">
|
||||||
|
<h2>Night Owl DVR</h2>
|
||||||
|
<a href="/shop/nightowlstorefront"><span>storefront</span></a>
|
||||||
|
<a href="/sspa/click?url=%2Fdp%2FB0NIGHTO01"><img src="https://example.com/nightowl.jpg" /></a>
|
||||||
|
</div>
|
||||||
|
<div data-asin="B0SOLAR001" class="puis-card-container">
|
||||||
|
<h2>Solar Street Lights</h2>
|
||||||
|
<a href="/sspa/click?url=%2Fdp%2FB0SOLAR001"><img src="https://example.com/solar.jpg" /></a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="s-widget AdHolder">
|
||||||
|
<div class="creative-collage">
|
||||||
|
<img src="https://example.com/jobsite.jpg" alt="jobsite" />
|
||||||
|
<img src="https://example.com/camera-closeup.jpg" alt="camera" />
|
||||||
|
<button aria-label="Pause"></button>
|
||||||
|
<img src="https://example.com/packaging.jpg" alt="packaging" />
|
||||||
|
</div>
|
||||||
|
<div data-asin="B0VOSKERVK">
|
||||||
|
<span>Sponsored</span>
|
||||||
|
<h2>Vosker VKX Solar Powered 4G-LTE Outdoor Cellular Security Camera</h2>
|
||||||
|
<a href="/sspa/click?url=%2Fdp%2FB0VOSKERVK">
|
||||||
|
<img src="https://example.com/vosker-vkx.jpg" />
|
||||||
|
</a>
|
||||||
|
<span class="a-price"><span class="a-offscreen">$379.99</span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sbv-container AdHolder">
|
||||||
|
<div class="sbv-creative">
|
||||||
|
<img src="https://example.com/jobsite2.jpg" alt="PROTECT YOUR INVESTMENT" />
|
||||||
|
<img src="https://example.com/closeup2.jpg" alt="CELLULAR SECURITY CAMERA" />
|
||||||
|
<button class="sbv-pause" aria-label="Pause"></button>
|
||||||
|
</div>
|
||||||
|
<div data-asin="B0VOSKERSB" class="sbv-product">
|
||||||
|
<span>Sponsored</span>
|
||||||
|
<h2>Vosker VKX Solar Powered camera</h2>
|
||||||
|
<a href="/sspa/click?url=%2Fdp%2FB0VOSKERSB">
|
||||||
|
<img src="https://example.com/vosker-sb.jpg" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<article data-component-type="s-search-result" data-asin="B0VIEWVOSK">
|
||||||
|
<span>Sponsored</span>
|
||||||
|
<span class="previously-viewed-badge">Previously viewed</span>
|
||||||
|
<h2>Vosker VKX Solar Powered 4G-LTE Outdoor Cellular Security Camera</h2>
|
||||||
|
<a href="/sspa/click?url=%2Fdp%2FB0VIEWVOSK">
|
||||||
|
<img class="s-image" src="https://example.com/vosker-grid.jpg" />
|
||||||
|
</a>
|
||||||
|
<span class="a-price"><span class="a-offscreen">$149.99</span></span>
|
||||||
|
</article>
|
||||||
|
<article data-component-type="s-search-result" data-asin="B0OWLLABS1">
|
||||||
|
<span>Sponsored</span>
|
||||||
|
<h2>Owl Labs Meeting Owl 3</h2>
|
||||||
|
<a href="/sspa/click?url=%2Fdp%2FB0OWLLABS1">
|
||||||
|
<img class="s-image" src="https://example.com/owl.jpg" />
|
||||||
|
</a>
|
||||||
|
<span class="a-price"><span class="a-offscreen">$1,099.00</span></span>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<div class="s-related-searches AdHolder">
|
||||||
|
<h2>Related searches</h2>
|
||||||
|
<a href="/s?k=home+security+camera">home security camera</a>
|
||||||
|
<a href="/s?k=outdoor+camera">outdoor camera</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
`,
|
||||||
|
{ url: 'https://www.amazon.com/s?k=home+security' },
|
||||||
|
);
|
||||||
|
Object.assign(globalThis, {
|
||||||
|
document: influencerPicksDom.window.document,
|
||||||
|
Node: influencerPicksDom.window.Node,
|
||||||
|
Element: influencerPicksDom.window.Element,
|
||||||
|
HTMLElement: influencerPicksDom.window.HTMLElement,
|
||||||
|
HTMLAnchorElement: influencerPicksDom.window.HTMLAnchorElement,
|
||||||
|
HTMLImageElement: influencerPicksDom.window.HTMLImageElement,
|
||||||
|
});
|
||||||
|
const influencerPicks = await collectAmazonSearchPageData({ pattern: 'pattern-2' });
|
||||||
|
const influencerByAsin = new Map(influencerPicks.map((item) => [item.asin, item]));
|
||||||
|
assert.equal(influencerByAsin.get('B0ORGINF01')?.adType, 'organic');
|
||||||
|
assert.equal(influencerByAsin.get('B0ANNKE001')?.adType, 'sponsoredProducts');
|
||||||
|
assert.equal(influencerByAsin.get('B0NIGHTO01')?.adType, 'sponsoredProducts');
|
||||||
|
assert.equal(influencerByAsin.get('B0SOLAR001')?.adType, 'sponsoredProducts');
|
||||||
|
assert.equal(
|
||||||
|
influencerByAsin.get('B0ANNKE001')?.placementId,
|
||||||
|
influencerByAsin.get('B0NIGHTO01')?.placementId,
|
||||||
|
);
|
||||||
|
assert.equal(influencerByAsin.get('B0VOSKERVK')?.adType, 'sponsoredBrands');
|
||||||
|
assert.equal(influencerByAsin.get('B0VOSKERVK')?.adFormat, 'standard');
|
||||||
|
assert.equal(influencerByAsin.get('B0VOSKERSB')?.adType, 'sponsoredBrands');
|
||||||
|
assert.equal(influencerByAsin.get('B0VIEWVOSK')?.adType, 'sponsoredProducts');
|
||||||
|
assert.equal(influencerByAsin.get('B0OWLLABS1')?.adType, 'sponsoredProducts');
|
||||||
|
assert.equal(
|
||||||
|
influencerPicks.some((item) => /related searches/i.test(item.title)),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
console.log('Amazon search parser fixtures passed.');
|
console.log('Amazon search parser fixtures passed.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -48,7 +48,7 @@ export async function waitForAmazonSearchPageReady() {
|
|||||||
|
|
||||||
// Visit the full result stream instead of jumping straight to pagination. Amazon loads
|
// Visit the full result stream instead of jumping straight to pagination. Amazon loads
|
||||||
// recommendation and sponsored modules when their region approaches the viewport.
|
// recommendation and sponsored modules when their region approaches the viewport.
|
||||||
for (const fraction of [0.2, 0.55, 0.92]) {
|
for (const fraction of [0.15, 0.35, 0.55, 0.75, 0.92]) {
|
||||||
const documentHeight = Math.max(
|
const documentHeight = Math.max(
|
||||||
document.documentElement.scrollHeight,
|
document.documentElement.scrollHeight,
|
||||||
document.body?.scrollHeight || 0,
|
document.body?.scrollHeight || 0,
|
||||||
|
|||||||
@ -92,14 +92,29 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
|
|||||||
return decoded;
|
return decoded;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const parseAsinFromText = (value: string) => {
|
||||||
|
const decoded = decodeRepeatedly(value);
|
||||||
|
const patterns = [
|
||||||
|
/\/(?:dp|gp\/product|gp\/aw\/d)\/([A-Z0-9]{10})(?:[/?]|$)/i,
|
||||||
|
/[?&#]asin=([A-Z0-9]{10})(?:[&]|$)/i,
|
||||||
|
/[?&#]lp_asins=([A-Z0-9]{10})/i,
|
||||||
|
/\/dp\/([A-Z0-9]{10})/i,
|
||||||
|
];
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
const match = pattern.exec(decoded);
|
||||||
|
if (match) return match[1].toUpperCase();
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
const parseAsin = (node: HTMLElement, href?: string) => {
|
const parseAsin = (node: HTMLElement, href?: string) => {
|
||||||
const dataAsin = node.closest<HTMLElement>('[data-asin]')?.dataset.asin || node.dataset.asin;
|
const dataAsin = node.closest<HTMLElement>('[data-asin]')?.dataset.asin || node.dataset.asin;
|
||||||
if (dataAsin && /^[A-Z0-9]{10}$/.test(dataAsin)) return dataAsin;
|
if (dataAsin && /^[A-Z0-9]{10}$/.test(dataAsin)) return dataAsin;
|
||||||
|
|
||||||
const candidates = [href || '', decodeRepeatedly(href || '')];
|
const candidates = [href || '', decodeRepeatedly(href || '')];
|
||||||
for (const candidate of candidates) {
|
for (const candidate of candidates) {
|
||||||
const match = /\/(?:dp|gp\/product)\/([A-Z0-9]{10})(?:[/?]|$)/i.exec(candidate);
|
const asin = parseAsinFromText(candidate);
|
||||||
if (match) return match[1].toUpperCase();
|
if (asin) return asin;
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
@ -125,9 +140,14 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
|
|||||||
const hasSponsoredDisclosure = (node: Element) =>
|
const hasSponsoredDisclosure = (node: Element) =>
|
||||||
sponsoredDisclosureCandidates(node).some(isSponsoredDisclosure);
|
sponsoredDisclosureCandidates(node).some(isSponsoredDisclosure);
|
||||||
|
|
||||||
|
const closestAsinHost = (node: Element) => {
|
||||||
|
const host = node.closest<HTMLElement>('[data-asin]');
|
||||||
|
return host && /^[A-Z0-9]{10}$/.test(host.dataset.asin || '') ? host : null;
|
||||||
|
};
|
||||||
|
|
||||||
const hasModuleSponsoredDisclosure = (node: Element) =>
|
const hasModuleSponsoredDisclosure = (node: Element) =>
|
||||||
sponsoredDisclosureCandidates(node).some(
|
sponsoredDisclosureCandidates(node).some(
|
||||||
(candidate) => !candidate.closest('[data-asin]') && isSponsoredDisclosure(candidate),
|
(candidate) => !closestAsinHost(candidate) && isSponsoredDisclosure(candidate),
|
||||||
);
|
);
|
||||||
|
|
||||||
const metadata = (node: Element) =>
|
const metadata = (node: Element) =>
|
||||||
@ -152,6 +172,10 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
|
|||||||
const asin = candidate.dataset.asin;
|
const asin = candidate.dataset.asin;
|
||||||
if (asin && /^[A-Z0-9]{10}$/.test(asin)) asins.add(asin);
|
if (asin && /^[A-Z0-9]{10}$/.test(asin)) asins.add(asin);
|
||||||
});
|
});
|
||||||
|
node.querySelectorAll<HTMLAnchorElement>('a[href]').forEach((anchor) => {
|
||||||
|
const asin = parseAsinFromText(anchor.href);
|
||||||
|
if (asin) asins.add(asin);
|
||||||
|
});
|
||||||
return asins.size;
|
return asins.size;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -161,17 +185,117 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
|
|||||||
const hasVisibleElement = (node: Element, selector: string) =>
|
const hasVisibleElement = (node: Element, selector: string) =>
|
||||||
Array.from(node.querySelectorAll<HTMLElement>(selector)).some(isVisible);
|
Array.from(node.querySelectorAll<HTMLElement>(selector)).some(isVisible);
|
||||||
|
|
||||||
|
const isSearchResultCard = (node: Element) =>
|
||||||
|
node.matches('[data-component-type="s-search-result"]');
|
||||||
|
|
||||||
|
const hasModuleHeadline = (node: Element) =>
|
||||||
|
Array.from(
|
||||||
|
node.querySelectorAll<HTMLElement>(
|
||||||
|
'h1, h2, h3, [class*="headline"], [class*="sb-headline"], [class*="sbv-"]',
|
||||||
|
),
|
||||||
|
).some(
|
||||||
|
(heading) =>
|
||||||
|
!heading.closest('[data-asin]') && isVisible(heading) && text(heading).length > 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
const isOutsideProductCard = (node: Element, root: Element) => {
|
||||||
|
if (node.closest('[data-component-type="s-search-result"]')) return false;
|
||||||
|
const asinHost = node.closest<HTMLElement>('[data-asin]');
|
||||||
|
return !asinHost || asinHost === root;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasModuleLevelVideo = (node: Element) =>
|
||||||
|
Array.from(node.querySelectorAll('video')).some((video) => isOutsideProductCard(video, node));
|
||||||
|
|
||||||
|
const hasModuleLevelBrandVisual = (node: Element) => {
|
||||||
|
if (hasModuleLevelVideo(node)) return true;
|
||||||
|
const playPause = Array.from(
|
||||||
|
node.querySelectorAll<HTMLElement>(
|
||||||
|
'[aria-label*="Pause" i], [aria-label*="Play" i], [title*="Pause" i], [title*="Play" i]',
|
||||||
|
),
|
||||||
|
).some((candidate) => isOutsideProductCard(candidate, node) && isVisible(candidate));
|
||||||
|
if (playPause) return true;
|
||||||
|
const outsideImages = Array.from(node.querySelectorAll<HTMLElement>('img')).filter(
|
||||||
|
(image) => isOutsideProductCard(image, node) && isVisible(image),
|
||||||
|
);
|
||||||
|
return outsideImages.length >= 2;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasInfluencerThematicCopy = (node: Element) =>
|
||||||
|
Array.from(node.querySelectorAll<HTMLElement>('h1, h2, h3, span, a, [class*="headline"]')).some(
|
||||||
|
(candidate) => {
|
||||||
|
if (closestAsinHost(candidate) || isVisuallyHiddenText(candidate)) return false;
|
||||||
|
return /picks from amazon influencers|influencer storefronts|amazon influencer/i.test(
|
||||||
|
text(candidate),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasThematicSpSignal = (node: Element) => {
|
||||||
|
const blob = [
|
||||||
|
metadata(node),
|
||||||
|
node.getAttribute('cel_widget_id') || '',
|
||||||
|
node.getAttribute('data-multi-ad-feedback-form-trigger') || '',
|
||||||
|
node.getAttribute('data-component-props') || '',
|
||||||
|
]
|
||||||
|
.join(' ')
|
||||||
|
.toLowerCase();
|
||||||
|
if (/brand-footer|multi-brand-creative|brands-related|desktop-hsa-3psl/.test(blob)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
/featured.?asins.?list|sponsoredproductsloom|sp_search_thematic|loom-desktop|influencer/.test(
|
||||||
|
blob,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return hasInfluencerThematicCopy(node);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSponsoredProductsThematic = (node: Element) => {
|
||||||
|
if (hasThematicSpSignal(node)) return true;
|
||||||
|
if (
|
||||||
|
Array.from(
|
||||||
|
node.querySelectorAll(
|
||||||
|
'[cel_widget_id], [data-multi-ad-feedback-form-trigger], [id*="loom"], [class*="loom"]',
|
||||||
|
),
|
||||||
|
).some(hasThematicSpSignal)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (
|
||||||
|
let current = node.parentElement;
|
||||||
|
current && !isMainResultsBoundary(current);
|
||||||
|
current = current.parentElement
|
||||||
|
) {
|
||||||
|
if (hasThematicSpSignal(current)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
const hasVisibleBrandCreative = (node: Element) => {
|
const hasVisibleBrandCreative = (node: Element) => {
|
||||||
|
if (isSearchResultCard(node)) return false;
|
||||||
|
if (isSponsoredProductsThematic(node)) return false;
|
||||||
|
|
||||||
const visibleBrandDestination = hasVisibleElement(
|
const visibleBrandDestination = hasVisibleElement(
|
||||||
node,
|
node,
|
||||||
'a[href*="/stores/"], a[href*="/brands/"], [class*="brand-logo"], [class*="brandLogo"]',
|
'a[href*="/stores/"], a[href*="/brands/"], [class*="brand-logo"], [class*="brandLogo"]',
|
||||||
);
|
);
|
||||||
const visibleModuleHeadline = Array.from(node.querySelectorAll<HTMLElement>('h1, h2, h3')).some(
|
const explicitBrandModule = /sponsored.?brand|brand.?ad|brands-related|brand-card|\bsbv-/.test(
|
||||||
(heading) =>
|
metadata(node),
|
||||||
!heading.closest('[data-asin]') && isVisible(heading) && text(heading).length > 0,
|
);
|
||||||
|
const brandsRelatedCopy = /brands related to your search/i.test(text(node));
|
||||||
|
const moduleSponsored = hasModuleSponsoredDisclosure(node) || hasSponsoredDisclosure(node);
|
||||||
|
const videoStrip = hasModuleLevelBrandVisual(node) && moduleSponsored;
|
||||||
|
const headlineUnit = hasModuleHeadline(node) && moduleSponsored;
|
||||||
|
return (
|
||||||
|
visibleBrandDestination ||
|
||||||
|
explicitBrandModule ||
|
||||||
|
brandsRelatedCopy ||
|
||||||
|
videoStrip ||
|
||||||
|
headlineUnit
|
||||||
);
|
);
|
||||||
const explicitBrandModule = /sponsored.?brand|brand.?ad/.test(metadata(node));
|
|
||||||
return visibleBrandDestination || (explicitBrandModule && visibleModuleHeadline);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const findAdModule = (card: HTMLElement) => {
|
const findAdModule = (card: HTMLElement) => {
|
||||||
@ -216,11 +340,18 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
|
|||||||
adFormat: 'standard' as const,
|
adFormat: 'standard' as const,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (isSearchResultCard(card)) {
|
||||||
|
return {
|
||||||
|
adType: 'sponsoredProducts' as const,
|
||||||
|
placement: 'middleOfSearch' as const,
|
||||||
|
adFormat: hasVisibleElement(card, 'video') ? ('video' as const) : ('standard' as const),
|
||||||
|
};
|
||||||
|
}
|
||||||
if (hasVisibleBrandCreative(module)) {
|
if (hasVisibleBrandCreative(module)) {
|
||||||
return {
|
return {
|
||||||
adType: 'sponsoredBrands' as const,
|
adType: 'sponsoredBrands' as const,
|
||||||
placement: 'middleOfSearch' as const,
|
placement: 'middleOfSearch' as const,
|
||||||
adFormat: module.querySelector('video') ? ('video' as const) : ('standard' as const),
|
adFormat: hasModuleLevelVideo(module) ? ('video' as const) : ('standard' as const),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@ -230,6 +361,24 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const hasCollectableProductLink = (node: Element) =>
|
||||||
|
Boolean(
|
||||||
|
node.querySelector(
|
||||||
|
'a[href*="/dp/"], a[href*="%2Fdp%2F"], a[href*="/gp/product"], a[href*="%2Fgp%2Fproduct%2F"], a[href*="sspa/click"], a[href*="lp_asins="]',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const addCandidate = (node: HTMLElement) => {
|
||||||
|
if (
|
||||||
|
isVisible(node) &&
|
||||||
|
(node.dataset.asin ||
|
||||||
|
hasCollectableProductLink(node) ||
|
||||||
|
parseAsin(node, node.querySelector<HTMLAnchorElement>('a[href]')?.href))
|
||||||
|
) {
|
||||||
|
candidateSet.add(node);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const candidateSet = new Set<HTMLElement>();
|
const candidateSet = new Set<HTMLElement>();
|
||||||
document
|
document
|
||||||
.querySelectorAll<HTMLElement>(
|
.querySelectorAll<HTMLElement>(
|
||||||
@ -238,13 +387,7 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
|
|||||||
.forEach((node) => {
|
.forEach((node) => {
|
||||||
const stableCard =
|
const stableCard =
|
||||||
node.closest<HTMLElement>('[data-component-type="s-search-result"]') || node;
|
node.closest<HTMLElement>('[data-component-type="s-search-result"]') || node;
|
||||||
if (
|
addCandidate(stableCard);
|
||||||
isVisible(stableCard) &&
|
|
||||||
(stableCard.dataset.asin ||
|
|
||||||
stableCard.querySelector('a[href*="/dp/"], a[href*="%2Fdp%2F"]'))
|
|
||||||
) {
|
|
||||||
candidateSet.add(stableCard);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
document.querySelectorAll<HTMLElement>('[data-asin]').forEach((node) => {
|
document.querySelectorAll<HTMLElement>('[data-asin]').forEach((node) => {
|
||||||
@ -268,13 +411,29 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
|
|||||||
isVisible(sidebarAd) &&
|
isVisible(sidebarAd) &&
|
||||||
hasSponsoredDisclosure(sidebarAd) &&
|
hasSponsoredDisclosure(sidebarAd) &&
|
||||||
sidebarAd.querySelector('h2, h3') &&
|
sidebarAd.querySelector('h2, h3') &&
|
||||||
sidebarAd.querySelector('img[src]') &&
|
sidebarAd.querySelector('img[src], img[data-src]') &&
|
||||||
sidebarAd.querySelector('a[href*="/dp/"], a[href*="%2Fdp%2F"]')
|
hasCollectableProductLink(sidebarAd)
|
||||||
) {
|
) {
|
||||||
candidateSet.add(sidebarAd);
|
candidateSet.add(sidebarAd);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document
|
||||||
|
.querySelectorAll<HTMLElement>(
|
||||||
|
'.AdHolder, [class*="brands-related"], [class*="brand-card"], [data-component-type="sponsored-brand"]',
|
||||||
|
)
|
||||||
|
.forEach((module) => {
|
||||||
|
if (!isVisible(module)) return;
|
||||||
|
const nestedCards = module.querySelectorAll<HTMLElement>(
|
||||||
|
'[data-asin], [class*="brand-card"], .sbv-product, [data-slot*="3psl"], [data-slot*="hsa"]',
|
||||||
|
);
|
||||||
|
if (nestedCards.length > 0) {
|
||||||
|
nestedCards.forEach(addCandidate);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addCandidate(module);
|
||||||
|
});
|
||||||
|
|
||||||
const cards = Array.from(candidateSet)
|
const cards = Array.from(candidateSet)
|
||||||
.filter(
|
.filter(
|
||||||
(card, index, all) =>
|
(card, index, all) =>
|
||||||
@ -310,13 +469,22 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
|
|||||||
if (!asin) return items;
|
if (!asin) return items;
|
||||||
|
|
||||||
const titleNode = card.querySelector<HTMLElement>('h2[aria-label], h2, h3[aria-label], h3');
|
const titleNode = card.querySelector<HTMLElement>('h2[aria-label], h2, h3[aria-label], h3');
|
||||||
const imageNode = card.querySelector<HTMLImageElement>('img.s-image, img[src]');
|
const imageNode = card.querySelector<HTMLImageElement>(
|
||||||
|
'img.s-image, img[src], img[data-src], img[srcset]',
|
||||||
|
);
|
||||||
const title =
|
const title =
|
||||||
titleNode?.getAttribute('aria-label')?.trim() ||
|
titleNode?.getAttribute('aria-label')?.trim() ||
|
||||||
text(titleNode) ||
|
text(titleNode) ||
|
||||||
imageNode?.alt?.trim() ||
|
imageNode?.alt?.trim() ||
|
||||||
asin;
|
asin;
|
||||||
const imageSrc = imageNode?.src;
|
const dataSrc = imageNode?.getAttribute('data-src') || imageNode?.dataset.src;
|
||||||
|
const srcsetUrl = (imageNode?.getAttribute('srcset') || '')
|
||||||
|
.split(',')[0]
|
||||||
|
?.trim()
|
||||||
|
.split(/\s+/)[0];
|
||||||
|
const rawSrc = imageNode?.src && !imageNode.src.startsWith('data:') ? imageNode.src : undefined;
|
||||||
|
const placeholderSrc = /grey-pixel|transparent-pixel|\/1x1(?:\.|$)/i.test(rawSrc || '');
|
||||||
|
const imageSrc = (placeholderSrc ? undefined : rawSrc) || dataSrc || srcsetUrl;
|
||||||
if (!imageSrc) return items;
|
if (!imageSrc) return items;
|
||||||
|
|
||||||
const currentPriceNode = card.querySelector<HTMLElement>(
|
const currentPriceNode = card.querySelector<HTMLElement>(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user