TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import { describe, expect, it } from 'vitest';2import { readFileSync } from 'node:fs';3import path from 'node:path';4import meta from './meta.json' with { type: 'json' };5import { localMeta } from '../_lib/local-meta.js';6import { fixtureDir, loadFixture, runFixtureSuite } from '@rareindex/connectors/testing';7import createConnector, { parseSeriesPage } from './index.js';8import { isJpBundle, jpCondition, parseJpCardTitle } from '../_g1-cards-eu-jp-lib/index.js';910const connector = createConnector(localMeta(meta));1112describe('magi', () => {13 runFixtureSuite(connector, it, expect);1415 it('parses Japanese shop titles (condition, grade, rarity, number, quantity)', () => {16 const a = parseJpCardTitle('〔※状態難/PSA10鑑定済〕レックウザEX【-】{104/171} 1枚');17 expect(a).toMatchObject({ name: 'レックウザEX', grader: 'psa', grade: '10', number: '104', total: '171', quantity: 1, rarity: null });18 const b = parseJpCardTitle('〔状態A-〕ミュウツーV(RR仕様)【P】{273/S-P} 1枚');19 expect(b).toMatchObject({ name: 'ミュウツーV', conditionRaw: '状態A-', rarity: 'P', number: '273', total: 'S-P', notes: ['RR仕様'], grader: null });20 expect(jpCondition(b.conditionRaw)).toBeNull(); // shop-specific scale is never mapped21 const c = parseJpCardTitle('ブラック・マジシャン・ガール ウルトラレア QCCU-JP002 3枚');22 expect(c.quantity).toBe(3);23 expect(isJpBundle('ブラック・マジシャン・ガール ウルトラレア QCCU-JP002 3枚', c.quantity)).toBe(true);24 const d = parseJpCardTitle('【PSA10】ルザミーネ SR 120/114 1枚');25 expect(d.grader).toBe('psa');26 expect(d.grade).toBe('10');27 expect(d.quantity).toBe(1);28 });2930 it('parses the saved series page (items, prices, sold marker, pagination)', () => {31 const doc = readFileSync(path.join(fixtureDir('magi'), 'series-page.html'), 'utf8');32 const { items, hasNext, heading } = parseSeriesPage(doc);33 expect(heading).toContain('出品');34 expect(items.length).toBeGreaterThan(50);35 expect(hasNext).toBe(true);36 for (const it of items.slice(0, 10)) {37 expect(it.url).toMatch(/^https:\/\/magi\.camp\/items\/\d+$/);38 expect(it.priceJpy).toBeGreaterThan(0);39 expect(typeof it.sold).toBe('boolean');40 }41 });4243 it('emits JPY listings, marks SOLD items as sold (never as sales) and skips mystery packs', async () => {44 for (const name of ['pokemon-series-page1', 'yugioh-series-page1']) {45 const fx = loadFixture('magi', name);46 const out = await connector.normalize(fx.raw);47 expect(out.length).toBeGreaterThan(0);48 const payload = fx.raw.payload as { items: Array<{ sold: boolean; title: string }> };49 const soldIn = payload.items.filter((i) => i.sold && !/オリパ|福袋/.test(i.title)).length;50 const soldOut = out.filter((r) => r.kind === 'listing' && r.availability === 'sold').length;51 expect(soldOut).toBe(soldIn);52 for (const r of out) {53 expect(r.kind).toBe('listing');54 if (r.kind !== 'listing') continue;55 expect(r.currency).toBe('JPY');56 expect(r.seller).toBeNull();57 expect(/オリパ|福袋/.test(r.rawTitle)).toBe(false);58 expect(r.attributes.identifiers.magi_item_id).toMatch(/^\d+$/);59 }60 }61 const sold = loadFixture('magi', 'sold-item-page');62 const sout = await connector.normalize(sold.raw);63 for (const r of sout) if (r.kind === 'listing') expect(r.availability).toBe('sold');64 });65});66