import { describe, expect, it } from 'vitest'; import { readFileSync } from 'node:fs'; import path from 'node:path'; import meta from './meta.json' with { type: 'json' }; import { localMeta } from '../_lib/local-meta.js'; import { fixtureDir, listFixtures, loadFixture, runFixtureSuite } from '@rareindex/connectors/testing'; import createConnector, { parseListPage } from './index.js'; import { jpCondition, parseJpCardTitle } from '../_g1-cards-eu-jp-lib/index.js'; const connector = createConnector(localMeta(meta)); describe('cardrush', () => { runFixtureSuite(connector, it, expect); it('parses MTG and Pokémon store titles', () => { const m = parseJpCardTitle('[PLD](黒枠)ラノワールのエルフ/Llanowar Elves《日本語》【4ED】', { bracket: 'set' }); expect(m).toMatchObject({ name: 'Llanowar Elves', conditionRaw: 'PLD', setCode: '4ED', language: 'Japanese', notes: ['黒枠'], rarity: null }); expect(jpCondition('PLD')).toBeNull(); const e = parseJpCardTitle('[EX]暗黒の儀式/Dark Ritual《英語》【4ED】', { bracket: 'set' }); expect(e).toMatchObject({ name: 'Dark Ritual', language: 'English', setCode: '4ED' }); expect(jpCondition(e.conditionRaw)).toBe('excellent'); const p = parseJpCardTitle('〔PSA10鑑定済〕ミロカロスδ-デルタ種【★】{013/068}'); expect(p).toMatchObject({ name: 'ミロカロスδ-デルタ種', grader: 'psa', grade: '10', rarity: '★', number: '013', total: '068' }); const box = parseJpCardTitle('ブースターパック 受け継がれる意志【未開封BOX】{-}'); expect(box.sealed).toBe(true); expect(box.number).toBeNull(); const mixed = parseJpCardTitle('〔※状態難/PSA8鑑定済〕リザードン LV.76(かえん/マークあり)【★】{旧裏}'); expect(mixed.grade).toBe('8'); expect(mixed.number).toBe('旧裏'); }); it('parses the saved list page: items, yen prices, stock, sold-out flag, pager', () => { const doc = readFileSync(path.join(fixtureDir('cardrush'), 'list-page.html'), 'utf8'); const { title, items, hasNext } = parseListPage(doc, 'www.cardrush-mtg.jp'); expect(title).toBeTruthy(); expect(items.length).toBe(100); expect(hasNext).toBe(true); const first = items[0]!; expect(first.url).toMatch(/^https:\/\/www\.cardrush-mtg\.jp\/product\/\d+$/); expect(first.priceJpy).toBeGreaterThan(0); expect(first.image).toMatch(/^https:\/\//); expect(items.some((i) => i.soldOut && i.stock === 0)).toBe(true); expect(items.some((i) => !i.soldOut && (i.stock ?? 0) > 0)).toBe(true); }); it('emits JPY listings per store with the right category, language and availability', async () => { for (const name of listFixtures('cardrush')) { const fx = loadFixture('cardrush', name); const out = await connector.normalize(fx.raw); expect(out.length).toBeGreaterThan(0); const payload = fx.raw.payload as { store: { categorySlug: string }; items: Array<{ soldOut: boolean; title: string }> }; for (const r of out) { if (r.kind !== 'listing') continue; expect(r.currency).toBe('JPY'); expect(r.price).toBeGreaterThan(0); expect(r.seller).toBe('Cardrush'); expect(r.attributes.categorySlug).toBe(payload.store.categorySlug); expect(['Japanese', 'English', 'Korean', 'Chinese']).toContain(r.attributes.language); expect(r.attributes.identifiers.cardrush_product_id).toMatch(/^www\.cardrush[a-z-]*\.jp\/\d+$/); } const ended = out.filter((r) => r.kind === 'listing' && r.availability === 'ended').length; const soldOutIn = payload.items.filter((i) => i.soldOut && !/オリパ|サプライ/.test(i.title)).length; expect(ended).toBe(soldOutIn); if (name.includes('sealed')) for (const r of out) if (r.kind === 'listing') expect(r.condition.completeness).toBe('sealed'); if (name.includes('pokemon')) expect(out.some((r) => r.kind === 'listing' && r.grade.grader === 'psa' && r.grade.grade)).toBe(true); } }); });