52c9947267
Finding 1: parsePriceValue mishandled thousands separators — "1.299,00"
and "1,299.00" both parsed to 1.3. Both the meta-tag path and the
text-regex path now funnel through a single normalizeAmount() helper
that picks the last separator as the decimal one, and the text regex
also matches thousands-grouped amounts.
Finding 2: the text fallback read <script>/<style> contents via
$('body').text(); those elements are now stripped from a clone first.
Finding 6: the default fetch now uses a 15s AbortSignal.timeout and a
browser-like User-Agent, so one unresponsive host cannot stall the
whole sequential run. The fetchImpl injection point is unchanged.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gwc3aCiyQxmCWQke8RvEz
89 lines
3.7 KiB
JavaScript
89 lines
3.7 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const { extractPrice, fetchAndExtractPrice, parsePriceValue } = require('../src/scraper');
|
|
|
|
function loadFixture(name) {
|
|
return fs.readFileSync(path.join(__dirname, 'fixtures', name), 'utf8');
|
|
}
|
|
|
|
test('extractPrice reads price from JSON-LD Product offers', () => {
|
|
const result = extractPrice(loadFixture('json-ld.html'));
|
|
assert.deepEqual(result, { price: 19.99, method: 'json-ld' });
|
|
});
|
|
|
|
test('extractPrice reads price from og:price:amount meta tag', () => {
|
|
const result = extractPrice(loadFixture('meta-tag.html'));
|
|
assert.deepEqual(result, { price: 24.50, method: 'meta' });
|
|
});
|
|
|
|
test('extractPrice falls back to euro-amount regex in page text', () => {
|
|
const result = extractPrice(loadFixture('regex-fallback.html'));
|
|
assert.deepEqual(result, { price: 14.95, method: 'regex' });
|
|
});
|
|
|
|
test('extractPrice returns null price when nothing matches', () => {
|
|
const result = extractPrice(loadFixture('no-price.html'));
|
|
assert.deepEqual(result, { price: null, method: null });
|
|
});
|
|
|
|
test('parsePriceValue handles both thousands-separator conventions', () => {
|
|
assert.equal(parsePriceValue('1.299,00'), 1299.00);
|
|
assert.equal(parsePriceValue('1,299.00'), 1299.00);
|
|
assert.equal(parsePriceValue('19,99'), 19.99);
|
|
assert.equal(parsePriceValue('19.99'), 19.99);
|
|
assert.equal(parsePriceValue('1.234.567,89'), 1234567.89);
|
|
assert.equal(parsePriceValue(null), null);
|
|
assert.equal(parsePriceValue('geen prijs'), null);
|
|
});
|
|
|
|
test('extractPrice reads a thousands-grouped price from a meta tag', () => {
|
|
const result = extractPrice(loadFixture('thousands-meta.html'));
|
|
assert.deepEqual(result, { price: 1299.00, method: 'meta' });
|
|
});
|
|
|
|
test('extractPrice reads a thousands-grouped price from page text', () => {
|
|
const result = extractPrice(loadFixture('thousands-regex.html'));
|
|
assert.deepEqual(result, { price: 1299.00, method: 'regex' });
|
|
});
|
|
|
|
test('extractPrice ignores euro amounts inside script and style tags', () => {
|
|
const result = extractPrice(loadFixture('script-decoy.html'));
|
|
assert.deepEqual(result, { price: 49.95, method: 'regex' });
|
|
});
|
|
|
|
test('fetchAndExtractPrice returns error when fetch rejects', async () => {
|
|
const fetchImpl = async () => { throw new Error('network down'); };
|
|
const result = await fetchAndExtractPrice('https://example.invalid', { fetchImpl });
|
|
assert.equal(result.price, null);
|
|
assert.ok(result.error instanceof Error);
|
|
});
|
|
|
|
test('fetchAndExtractPrice returns error on non-OK response', async () => {
|
|
const fetchImpl = async () => ({ ok: false, status: 404, text: async () => '' });
|
|
const result = await fetchAndExtractPrice('https://example.invalid', { fetchImpl });
|
|
assert.equal(result.price, null);
|
|
assert.ok(result.error instanceof Error);
|
|
});
|
|
|
|
test('fetchAndExtractPrice returns error when the request times out', async () => {
|
|
const fetchImpl = async () => {
|
|
const err = new Error('The operation was aborted due to timeout');
|
|
err.name = 'TimeoutError';
|
|
throw err;
|
|
};
|
|
const result = await fetchAndExtractPrice('https://example.invalid', { fetchImpl });
|
|
assert.deepEqual(result.price, null);
|
|
assert.equal(result.method, null);
|
|
assert.ok(result.error instanceof Error);
|
|
assert.equal(result.error.name, 'TimeoutError');
|
|
});
|
|
|
|
test('fetchAndExtractPrice extracts price from fetched HTML', async () => {
|
|
const html = loadFixture('meta-tag.html');
|
|
const fetchImpl = async () => ({ ok: true, status: 200, text: async () => html });
|
|
const result = await fetchAndExtractPrice('https://example.invalid', { fetchImpl });
|
|
assert.deepEqual(result, { price: 24.50, method: 'meta', error: null });
|
|
});
|