TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import { describe, expect, it } from 'vitest';2import meta from './meta.json' with { type: 'json' };3import { localMeta } from '../_lib/local-meta.js';4import { listFixtures, loadFixture, runFixtureSuite } from '@rareindex/connectors/testing';5import createConnector, { PRICE_FIELDS, hasAnyPrice, parseProductName } from './index.js';6import { isoDay } from '../_g1-cards-eu-jp-lib/index.js';78const connector = createConnector(localMeta(meta));910describe('cardmarket-priceguide', () => {11 runFixtureSuite(connector, it, expect);1213 it('parses Cardmarket product names', () => {14 expect(parseProductName('Kakuna [Bug Bite | Primal Clash]')).toEqual({ name: 'Kakuna', number: null, version: null, disambiguation: 'Bug Bite | Primal Clash' });15 expect(parseProductName('Roronoa Zoro (OP01-001)')).toEqual({ name: 'Roronoa Zoro', number: 'OP01-001', version: null, disambiguation: null });16 expect(parseProductName('Yokomon (BT1-001)').number).toBe('BT1-001');17 expect(parseProductName('Auron (1-001)').number).toBe('1-001');18 expect(parseProductName('Forest (V.1)')).toEqual({ name: 'Forest', number: null, version: 'V.1', disambiguation: null });19 expect(parseProductName('Whis's Coercion').name).toBe("Whis's Coercion");20 // unknown parentheticals stay part of the name (never guessed as numbers)21 expect(parseProductName('Sol Ring (Extended Art)').name).toBe('Sol Ring (Extended Art)');22 });2324 it('dates observations with the file createdAt (source date, not fetch time)', () => {25 expect(isoDay('2026-09-07T02:48:04+0200')?.toISOString()).toBe('2026-09-07T00:00:00.000Z');26 });2728 it('detects foil rows and maps every price field to a priceKind', () => {29 expect(hasAnyPrice({ idProduct: 1, avg: null, low: null, 'trend-foil': 0.36 }, true)).toBe(true);30 expect(hasAnyPrice({ idProduct: 1, avg: null, low: null, 'trend-foil': 0.36 }, false)).toBe(false);31 expect(hasAnyPrice(null)).toBe(false);32 expect(PRICE_FIELDS.map(([, k]) => k)).toEqual(['mid', 'low', 'trend', 'market', 'average_7d', 'average_30d']);33 });3435 it('emits cardmarket_id identifiers, EUR observations and a foil variant when foil prices exist', async () => {36 for (const name of listFixtures('cardmarket-priceguide')) {37 const fx = loadFixture('cardmarket-priceguide', name);38 const out = await connector.normalize(fx.raw);39 const cats = out.filter((r) => r.kind === 'catalog_item');40 const obs = out.filter((r) => r.kind === 'price_observation');41 expect(cats.length).toBeGreaterThanOrEqual(1);42 expect(obs.length).toBeGreaterThanOrEqual(1);43 for (const c of cats) {44 if (c.kind !== 'catalog_item') continue;45 expect(c.attributes.identifiers.cardmarket_id).toMatch(/^\d+$/);46 expect(c.sourceUrl).toContain('downloads.s3.cardmarket.com');47 }48 for (const o of obs) {49 if (o.kind !== 'price_observation') continue;50 expect(o.currency).toBe('EUR');51 expect(o.price).toBeGreaterThan(0);52 expect(o.observationDate.getUTCHours()).toBe(0);53 expect(o.observationDate.getTime()).toBeLessThanOrEqual(Date.now());54 expect((o.attributes.metadata as { cardmarket_field: string }).cardmarket_field).toBeTruthy();55 }56 const payload = fx.raw.payload as { prices: Record<string, number | null> | null; single: boolean; game: { foilVariant: string } };57 const foilPresent = Boolean(payload.single && payload.prices && Object.entries(payload.prices).some(([k, v]) => k.endsWith('-foil') && typeof v === 'number' && v > 0));58 const foilCat = cats.find((c) => c.kind === 'catalog_item' && c.attributes.variant === payload.game.foilVariant);59 expect(Boolean(foilCat)).toBe(foilPresent);60 if (!payload.single) for (const c of cats) if (c.kind === 'catalog_item') expect(c.condition.completeness).toBe('sealed');61 }62 });63});64