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, loadFixture, runFixtureSuite } from '@rareindex/connectors/testing'; import createConnector, { parseSeriesPage } from './index.js'; import { isJpBundle, jpCondition, parseJpCardTitle } from '../_g1-cards-eu-jp-lib/index.js'; const connector = createConnector(localMeta(meta)); describe('magi', () => { runFixtureSuite(connector, it, expect); it('parses Japanese shop titles (condition, grade, rarity, number, quantity)', () => { const a = parseJpCardTitle('〔※状態難/PSA10鑑定済〕レックウザEX【-】{104/171} 1枚'); expect(a).toMatchObject({ name: 'レックウザEX', grader: 'psa', grade: '10', number: '104', total: '171', quantity: 1, rarity: null }); const b = parseJpCardTitle('〔状態A-〕ミュウツーV(RR仕様)【P】{273/S-P} 1枚'); expect(b).toMatchObject({ name: 'ミュウツーV', conditionRaw: '状態A-', rarity: 'P', number: '273', total: 'S-P', notes: ['RR仕様'], grader: null }); expect(jpCondition(b.conditionRaw)).toBeNull(); // shop-specific scale is never mapped const c = parseJpCardTitle('ブラック・マジシャン・ガール ウルトラレア QCCU-JP002 3枚'); expect(c.quantity).toBe(3); expect(isJpBundle('ブラック・マジシャン・ガール ウルトラレア QCCU-JP002 3枚', c.quantity)).toBe(true); const d = parseJpCardTitle('【PSA10】ルザミーネ SR 120/114 1枚'); expect(d.grader).toBe('psa'); expect(d.grade).toBe('10'); expect(d.quantity).toBe(1); }); it('parses the saved series page (items, prices, sold marker, pagination)', () => { const doc = readFileSync(path.join(fixtureDir('magi'), 'series-page.html'), 'utf8'); const { items, hasNext, heading } = parseSeriesPage(doc); expect(heading).toContain('出品'); expect(items.length).toBeGreaterThan(50); expect(hasNext).toBe(true); for (const it of items.slice(0, 10)) { expect(it.url).toMatch(/^https:\/\/magi\.camp\/items\/\d+$/); expect(it.priceJpy).toBeGreaterThan(0); expect(typeof it.sold).toBe('boolean'); } }); it('emits JPY listings, marks SOLD items as sold (never as sales) and skips mystery packs', async () => { for (const name of ['pokemon-series-page1', 'yugioh-series-page1']) { const fx = loadFixture('magi', name); const out = await connector.normalize(fx.raw); expect(out.length).toBeGreaterThan(0); const payload = fx.raw.payload as { items: Array<{ sold: boolean; title: string }> }; const soldIn = payload.items.filter((i) => i.sold && !/オリパ|福袋/.test(i.title)).length; const soldOut = out.filter((r) => r.kind === 'listing' && r.availability === 'sold').length; expect(soldOut).toBe(soldIn); for (const r of out) { expect(r.kind).toBe('listing'); if (r.kind !== 'listing') continue; expect(r.currency).toBe('JPY'); expect(r.seller).toBeNull(); expect(/オリパ|福袋/.test(r.rawTitle)).toBe(false); expect(r.attributes.identifiers.magi_item_id).toMatch(/^\d+$/); } } const sold = loadFixture('magi', 'sold-item-page'); const sout = await connector.normalize(sold.raw); for (const r of sout) if (r.kind === 'listing') expect(r.availability).toBe('sold'); }); });