Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
tests/pages/**/*.html -text -diff
32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,35 @@ jobs:
name: store-zip
path: dist-zips/*.zip
if-no-files-found: error

e2e:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
with:
# The upgrade scenarios check out the v2.0 store commit.
fetch-depth: 0

- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm

- run: npm ci

# Full Chromium, not the headless shell: extensions need it. The browser
# version is pinned by the @playwright/test version in package.json.
- name: Install Chromium
run: npx playwright install --with-deps --no-shell chromium

- name: Chromium harness
run: npx playwright test

- uses: actions/upload-artifact@v4
if: failure()
with:
name: e2e-report
path: |
tests/e2e/.report
tests/e2e/.results
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,8 @@ dist/
build/
*.zip
dist-zips/

# e2e harness output
tests/e2e/.build/
tests/e2e/.results/
tests/e2e/.report/
4 changes: 3 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ AmazonSellerScraper/
│ │ ├── scraper.js # DOM scraping on Amazon pages
│ │ ├── chatbot.js # Floating AI chatbot widget (Shadow DOM)
│ │ └── offer-fetcher.js # Seller price fetching for spread analysis
│ ├── lib/
│ │ └── parsers.js # Pure search/offer parsing (global Parsers)
│ ├── background/
│ │ └── service-worker.js # Message routing + Gemini API calls
│ └── modules/
Expand Down Expand Up @@ -93,7 +95,7 @@ AmazonSellerScraper/

## Testing

### JavaScript (Jest + JSDOM) — 214 tests
### JavaScript (Jest + JSDOM)

```bash
npm test # Run all JS tests
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ For local Firebase work, `npm run build:dev` points the build at the emulators u

`npm test` runs the Jest suite and the tool tests. `npm run check` builds, then runs the permission lock (nothing may be added over `tools/live-manifest.json`, the published v2.0 manifest), the version gate and the secret scan. `npm run zip` writes the store package to `dist-zips/` and refuses a dev build, a stray file, uncommitted changes (`node tools/zip.mjs --allow-dirty` overrides that for local tries), or any gate failure. CI runs all of these.

The Jest suite includes a golden corpus of saved Amazon pages (`tests/pages/`, see its README). `npm run test:e2e` loads the built extension into Chromium and runs scrape scenarios against those pages, with every request answered locally. Run `npx playwright install --no-shell chromium` once first. Known bugs run as expected failures tagged with their audit finding id; `PROSCAN_SHOW_KNOWN=1 npm run test:e2e` shows what they fail on.

## Usage

1. Navigate to any Amazon search results or seller page
Expand Down Expand Up @@ -179,9 +181,11 @@ AmazonSellerScraper/
│ └── popup.js # UI state management and export handling
├── scripts/
│ ├── content/
│ │ ├── scraper.js # DOM scraping with cascading selectors
│ │ ├── scraper.js # Scrape loop: storage, messages, pagination
│ │ ├── chatbot.js # Floating AI chatbot (Shadow DOM)
│ │ └── offer-fetcher.js # Seller offer page fetching for spread analysis
│ ├── lib/
│ │ └── parsers.js # Pure search and offer page parsing
│ ├── background/
│ │ └── service-worker.js # Message routing + Gemini API
│ └── modules/
Expand Down
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module.exports = {
coverageDirectory: 'coverage',
collectCoverageFrom: [
'scripts/modules/*.js',
'scripts/lib/*.js',
'scripts/content/scraper.js',
'scripts/content/offer-fetcher.js',
'!**/node_modules/**'
Expand Down
1 change: 1 addition & 0 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"matches": ["*://*.amazon.com/*"],
"js": [
"scripts/modules/price.js",
"scripts/lib/parsers.js",
"scripts/modules/delta.js",
"scripts/content/scraper.js",
"scripts/content/chatbot.js",
Expand Down
46 changes: 46 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@
"test:unit": "jest tests/unit --verbose",
"test:integration": "jest tests/integration --verbose",
"test:tools": "node --test --test-concurrency=1 \"tools/tests/*.test.mjs\"",
"test:e2e": "playwright test",
"test:coverage": "jest --coverage",
"lock": "node tools/permission-lock.mjs && node tools/version-gate.mjs",
"scan:secrets": "node tools/secret-scan.mjs",
"check": "npm run build && npm run lock && npm run scan:secrets"
},
"devDependencies": {
"@playwright/test": "1.63.0",
"adm-zip": "^0.5.17",
"esbuild": "^0.28.0",
"jest": "^29.7.0",
Expand Down
14 changes: 14 additions & 0 deletions playwright.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { defineConfig } from '@playwright/test';

// One Chromium at a time: every test loads the extension into its own profile.
export default defineConfig({
testDir: 'tests/e2e',
testMatch: '*.spec.mjs',
globalSetup: './tests/e2e/global-setup.mjs',
workers: 1,
fullyParallel: false,
retries: 0,
timeout: 90_000,
reporter: process.env.CI ? [['list'], ['html', { open: 'never', outputFolder: 'tests/e2e/.report' }]] : 'list',
outputDir: 'tests/e2e/.results',
});
1 change: 1 addition & 0 deletions popup/popup.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<link rel="stylesheet" href="popup.css">
<script src="../libs/xlsx.full.min.js"></script>
<script src="../scripts/modules/price.js"></script>
<script src="../scripts/lib/parsers.js"></script>
<script src="../scripts/modules/storage.js"></script>
<script src="../scripts/modules/analyzer.js"></script>
<script src="../scripts/modules/spread-analyzer.js"></script>
Expand Down
99 changes: 3 additions & 96 deletions scripts/content/offer-fetcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,102 +22,9 @@
/** @type {boolean} Whether spread analysis is currently running */
let isAnalyzing = false;

/**
* CSS selectors for extracting prices from Amazon offer listing pages.
* Multiple selectors for resilience against layout changes.
* @const {Object}
*/
const OFFER_SELECTORS = {
// AOD (All Offers Display) format — modern Amazon
aodPriceBlock: '.aod-information-block .a-price .a-offscreen',

// Classic offer listing page format
offerListPrice: '#olpOfferList .a-price .a-offscreen',

// Legacy offer listing format
legacyOfferPrice: '.olpOfferPrice',

// Scoped fallback — prices inside an offer/seller container only.
// Must NOT be the global '.a-price .a-offscreen' selector, which would
// scoop up buy-box, sponsored, and accessory prices as fake seller offers.
generalPrice: '#aod-offer-list .a-price .a-offscreen, [id^="aod-offer"] .a-price .a-offscreen, .olpOffer .a-price .a-offscreen'
};

/**
* Build the offer listing URL for a given ASIN.
* Uses the classic offer listing page format filtered to new condition.
*
* @param {string} asin - Amazon Standard Identification Number
* @returns {string} Full URL for the offer listing page
*/
function buildOfferUrl(asin) {
return `https://www.amazon.com/gp/offer-listing/${asin}/ref=dp_olp_all_mbc?ie=UTF8&condition=new`;
}

/**
* Build the AOD (All Offers Display) AJAX URL for a given ASIN.
* This endpoint returns HTML fragments with all seller offers.
*
* @param {string} asin - Amazon Standard Identification Number
* @returns {string} AJAX endpoint URL
*/
function buildAodUrl(asin) {
return `https://www.amazon.com/gp/aod/ajax?asin=${asin}&condition=new&m=&qid=&smid=&sourcecustomerorglistid=&sourcecustomerorglistitemid=&sr=&pc=dp`;
}

/**
* Parse a price string from Amazon's DOM into a numeric value.
* Handles currency symbols, commas, and whitespace.
*
* @param {string} text - Raw price text (e.g., "$19.99", "$1,299.00")
* @returns {number} Parsed price, or 0 if unparseable
*/
function parseOfferPrice(text) {
if (!text) return 0;
const cleaned = text.replace(/[^0-9.]/g, '');
const price = parseFloat(cleaned);
return isNaN(price) ? 0 : price;
}

/**
* Extract all seller prices from an HTML document using cascading selectors.
* Tries multiple selector strategies in order of specificity.
*
* @param {Document} doc - Parsed HTML document (from DOMParser)
* @returns {number[]} Array of valid seller prices (> 0)
*/
function extractPricesFromDocument(doc) {
const prices = [];

// Try each selector strategy in order
const selectorStrategies = [
OFFER_SELECTORS.aodPriceBlock,
OFFER_SELECTORS.offerListPrice,
OFFER_SELECTORS.legacyOfferPrice,
OFFER_SELECTORS.generalPrice
];

for (const selector of selectorStrategies) {
const elements = doc.querySelectorAll(selector);
if (elements.length > 0) {
elements.forEach(el => {
const price = parseOfferPrice(el.textContent);
if (price > 0) {
prices.push(price);
}
});
// If we found prices with this selector, don't try fallbacks
if (prices.length > 0) break;
}
}

// Return all prices as-is. Do NOT dedup: multiple distinct sellers
// legitimately list the same price, and each is a real data point for
// the spread statistics (sellerCount, mean, stdDev, CV, confidence factor).
// The cascade `break`s after the first selector that yields prices, so a
// single physical price element is never matched twice.
return prices;
}
// Pure parsing lives in scripts/lib/parsers.js. These names stay for the fetch
// loop below and for the unit tests that load this file.
var { buildOfferUrl, buildAodUrl, parseOfferPrice, extractPricesFromDocument } = Parsers;

/**
* Fetch the offer listing page for a single ASIN and extract seller prices.
Expand Down
Loading
Loading