import { describe, expect, it } from 'vitest'; import meta from './meta.json' with { type: 'json' }; import { localMeta } from '../_lib/local-meta.js'; import { listFixtures, loadFixture, runFixtureSuite } from '@rareindex/connectors/testing'; import createConnector, { PRICE_FIELDS, hasAnyPrice, parseProductName } from './index.js'; import { isoDay } from '../_g1-cards-eu-jp-lib/index.js'; const connector = createConnector(localMeta(meta)); describe('cardmarket-priceguide', () => { runFixtureSuite(connector, it, expect); it('parses Cardmarket product names', () => { expect(parseProductName('Kakuna [Bug Bite | Primal Clash]')).toEqual({ name: 'Kakuna', number: null, version: null, disambiguation: 'Bug Bite | Primal Clash' }); expect(parseProductName('Roronoa Zoro (OP01-001)')).toEqual({ name: 'Roronoa Zoro', number: 'OP01-001', version: null, disambiguation: null }); expect(parseProductName('Yokomon (BT1-001)').number).toBe('BT1-001'); expect(parseProductName('Auron (1-001)').number).toBe('1-001'); expect(parseProductName('Forest (V.1)')).toEqual({ name: 'Forest', number: null, version: 'V.1', disambiguation: null }); expect(parseProductName('Whis's Coercion').name).toBe("Whis's Coercion"); // unknown parentheticals stay part of the name (never guessed as numbers) expect(parseProductName('Sol Ring (Extended Art)').name).toBe('Sol Ring (Extended Art)'); }); it('dates observations with the file createdAt (source date, not fetch time)', () => { expect(isoDay('2026-09-07T02:48:04+0200')?.toISOString()).toBe('2026-09-07T00:00:00.000Z'); }); it('detects foil rows and maps every price field to a priceKind', () => { expect(hasAnyPrice({ idProduct: 1, avg: null, low: null, 'trend-foil': 0.36 }, true)).toBe(true); expect(hasAnyPrice({ idProduct: 1, avg: null, low: null, 'trend-foil': 0.36 }, false)).toBe(false); expect(hasAnyPrice(null)).toBe(false); expect(PRICE_FIELDS.map(([, k]) => k)).toEqual(['mid', 'low', 'trend', 'market', 'average_7d', 'average_30d']); }); it('emits cardmarket_id identifiers, EUR observations and a foil variant when foil prices exist', async () => { for (const name of listFixtures('cardmarket-priceguide')) { const fx = loadFixture('cardmarket-priceguide', name); const out = await connector.normalize(fx.raw); const cats = out.filter((r) => r.kind === 'catalog_item'); const obs = out.filter((r) => r.kind === 'price_observation'); expect(cats.length).toBeGreaterThanOrEqual(1); expect(obs.length).toBeGreaterThanOrEqual(1); for (const c of cats) { if (c.kind !== 'catalog_item') continue; expect(c.attributes.identifiers.cardmarket_id).toMatch(/^\d+$/); expect(c.sourceUrl).toContain('downloads.s3.cardmarket.com'); } for (const o of obs) { if (o.kind !== 'price_observation') continue; expect(o.currency).toBe('EUR'); expect(o.price).toBeGreaterThan(0); expect(o.observationDate.getUTCHours()).toBe(0); expect(o.observationDate.getTime()).toBeLessThanOrEqual(Date.now()); expect((o.attributes.metadata as { cardmarket_field: string }).cardmarket_field).toBeTruthy(); } const payload = fx.raw.payload as { prices: Record | null; single: boolean; game: { foilVariant: string } }; const foilPresent = Boolean(payload.single && payload.prices && Object.entries(payload.prices).some(([k, v]) => k.endsWith('-foil') && typeof v === 'number' && v > 0)); const foilCat = cats.find((c) => c.kind === 'catalog_item' && c.attributes.variant === payload.game.foilVariant); expect(Boolean(foilCat)).toBe(foilPresent); if (!payload.single) for (const c of cats) if (c.kind === 'catalog_item') expect(c.condition.completeness).toBe('sealed'); } }); });