diff --git a/src/lib/scrapers/index.ts b/src/lib/scrapers/index.ts new file mode 100644 index 0000000..98edccb --- /dev/null +++ b/src/lib/scrapers/index.ts @@ -0,0 +1,19 @@ +import { detectShop, type Shop } from '@/lib/shops' +import type { PriceScraper, ScrapeResult } from './types' + +const registry = new Map() + +export function registerScraper(scraper: PriceScraper) { + registry.set(scraper.shop, scraper) +} + +export async function scrapeUrl(url: string): Promise { + const shop = detectShop(url) + if (!shop) throw new Error(`Unsupported URL: ${url}`) + const scraper = registry.get(shop) + if (!scraper) throw new Error(`No scraper registered for shop: ${shop}`) + return scraper.scrape(url) +} + +export function registerScraperForTest(s: PriceScraper) { registry.set(s.shop, s) } +export function resetScrapersForTest() { registry.clear() } diff --git a/src/lib/scrapers/types.ts b/src/lib/scrapers/types.ts new file mode 100644 index 0000000..c2fd34a --- /dev/null +++ b/src/lib/scrapers/types.ts @@ -0,0 +1,15 @@ +import type { Shop } from '@/lib/shops' + +export interface ScrapeResult { + price: number | null + currency: string + availability: 'in_stock' | 'out_of_stock' | 'unknown' + name?: string + imageUrl?: string + error?: string +} + +export interface PriceScraper { + shop: Shop + scrape(url: string): Promise +} diff --git a/tests/scrapers/registry.test.ts b/tests/scrapers/registry.test.ts new file mode 100644 index 0000000..b02e2d6 --- /dev/null +++ b/tests/scrapers/registry.test.ts @@ -0,0 +1,24 @@ +import { describe, it, expect, vi } from 'vitest' +import { scrapeUrl, registerScraperForTest, resetScrapersForTest } from '@/lib/scrapers' +import type { PriceScraper } from '@/lib/scrapers/types' + +describe('scrapeUrl', () => { + it('dispatches to matching shop scraper', async () => { + const fake: PriceScraper = { + shop: 'geizhals', + scrape: vi.fn().mockResolvedValue({ + price: 42, currency: 'EUR', availability: 'in_stock', name: 'X', + }), + } + resetScrapersForTest() + registerScraperForTest(fake) + const result = await scrapeUrl('https://geizhals.de/foo') + expect(result.price).toBe(42) + expect(fake.scrape).toHaveBeenCalledWith('https://geizhals.de/foo') + }) + + it('throws on unknown shop', async () => { + resetScrapersForTest() + await expect(scrapeUrl('https://example.com')).rejects.toThrow(/unsupported/i) + }) +})