TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import { readFileSync } from 'node:fs';2import path from 'node:path';3import { describe, expect, it } from 'vitest';4import { getConnectorMeta } from '@rareindex/connectors';5import { fixtureDir, loadFixture, runFixtureSuite } from '@rareindex/connectors/testing';6import createConnector, { closedUrl, parseClosedPage, parseItemPage, parseSearchPage, searchUrl } from './index.js';7import { isCjkBundle, normaliseCjkGradeText, parseJstDateTime, refineCategory } from '../_g9-asia-watch-sneaker-lib/index.js';89const connector = createConnector(getConnectorMeta('yahoo-auctions-jp'));10const sample = (name: string) => readFileSync(path.join(fixtureDir('yahoo-auctions-jp'), name), 'utf8');11const seed = { q: 'ポケモンカード PSA10', category: 'pokemon', language: 'Japanese', auccat: null };1213describe('yahoo-auctions-jp', () => {14 runFixtureSuite(connector, it, expect);1516 it('parses live search cards (id, title, current + buy-now price, bids, end time, image, category path)', () => {17 const p = parseSearchPage(sample('search-cards.sample.html'), 'https://auctions.yahoo.co.jp/search/search?p=x', seed, 1);18 expect(p.kind).toBe('search_page');19 expect(p.rows.length).toBe(3);20 expect(p.total).toBe(6711);21 const r = p.rows[0]!;22 expect(r.id).toBe('v1243685419');23 expect(r.title).toContain('レックウザ');24 expect(r.price).toBe(1100000);25 expect(r.buyNow).toBe(1485000); // displayed 即決 (tax included), not the data attribute26 expect(r.bids).toBe(0);27 expect(r.categoryId).toBe('2084317608');28 expect(r.categoryPath).toContain('25826');29 expect(r.image).toMatch(/^https:\/\/auc-pctr\.c\.yimg\.jp\//);30 expect(r.isUnused).toBe(true);31 expect(parseJstDateTime(r.endTime)?.getTime()).toBe(1788965106 * 1000);32 });3334 it('parses closed search Next.js state into rows with ISO+09:00 end times', async () => {35 const p = parseClosedPage(sample('closed-nextdata.sample.html'), 'https://auctions.yahoo.co.jp/closedsearch/closedsearch?p=x', seed, 1);36 expect(p.kind).toBe('closed_page');37 expect(p.rows.length).toBe(3);38 expect(p.total).toBe(83670);39 const r = p.rows[0]!;40 expect(r.id).toBe('z676811124');41 expect(r.price).toBe(15000);42 expect(r.bids).toBe(1);43 expect(r.endTime).toBe('2026-09-08T17:07:22+09:00');44 expect(r.categoryPath).toContain('トレーディングカードゲーム');45 const out = await connector.normalize({ url: p.url, externalId: null, kind: 'sale', engine: 'api', fetchedAt: new Date('2026-09-08T10:00:00Z'), payload: p });46 const s = out[0];47 if (s?.kind !== 'sale') throw new Error('expected sale');48 expect(s.saleDate.toISOString()).toBe('2026-09-08T08:07:22.000Z');49 expect(s.currency).toBe('JPY');50 expect(s.buyerPremiumIncluded).toBe(false);51 expect(s.saleType).toBe('fixed_price');52 expect(s.isBundle).toBe(true); // "4枚セット まとめ売り"53 expect(s.grade.grader).toBe('psa');54 expect(s.grade.grade).toBe('10');55 expect(s.attributes.identifiers.yahoo_auction_id).toBe('z676811124');56 expect(s.auctionHouse).toBe('Yahoo! Auctions Japan');57 });5859 it('drops closed lots without a winning bid', async () => {60 const p = parseClosedPage(sample('closed-nextdata.sample.html'), 'https://auctions.yahoo.co.jp/closedsearch/closedsearch?p=x', seed, 1);61 p.rows.forEach((r) => (r.bids = 0));62 const out = await connector.normalize({ url: p.url, externalId: null, kind: 'sale', engine: 'api', fetchedAt: new Date(), payload: p });63 expect(out).toHaveLength(0);64 });6566 it('parses a single item page (lookup) with JST wall-clock end time', async () => {67 const p = parseItemPage(sample('item-page.sample.html'), 'https://auctions.yahoo.co.jp/jp/auction/v1243685419', seed);68 expect(p?.rows[0]?.id).toBe('v1243685419');69 expect(p?.rows[0]?.price).toBe(1000000);70 expect(p?.rows[0]?.buyNow).toBe(1350000);71 expect(p?.rows[0]?.endTime).toBe('2026-09-09 23:45:06');72 const out = await connector.normalize({ url: p!.url, externalId: null, kind: 'listing', engine: 'api', fetchedAt: new Date('2026-09-08T00:00:00Z'), payload: p! });73 const l = out[0];74 if (l?.kind !== 'listing') throw new Error('expected listing');75 expect(l.endsAt?.toISOString()).toBe('2026-09-09T14:45:06.000Z');76 expect(l.listedAt?.toISOString()).toBe('2026-09-07T11:56:01.000Z');77 expect(l.listingType).toBe('auction');78 expect(l.availability).toBe('available');79 });8081 it('live fixture normalises to JPY listings with grades and refined categories', async () => {82 const fx = loadFixture('yahoo-auctions-jp', 'pokemon-psa10-live-p1');83 const out = await connector.normalize(fx.raw);84 expect(out.length).toBeGreaterThan(5);85 for (const r of out) {86 if (r.kind !== 'listing') throw new Error('expected listing');87 expect(r.currency).toBe('JPY');88 expect(r.price).toBeGreaterThan(0);89 expect(r.sourceUrl).toMatch(/^https:\/\/auctions\.yahoo\.co\.jp\/jp\/auction\/[a-z]?\d+$/);90 }91 expect(out.some((r) => r.kind === 'listing' && r.attributes.categorySlug === 'pokemon')).toBe(true);92 });9394 it('never sends robots-disallowed query parameters', () => {95 for (const u of [searchUrl(seed, 1), searchUrl({ ...seed, auccat: '2084317608' }, 51), closedUrl(seed, 101)]) {96 expect(u).not.toMatch(/[?&](n|mode|s1|o1|min|max|istatus|abatch)=/);97 expect(decodeURIComponent(u)).not.toMatch(/[?&]n=/);98 }99 expect(searchUrl(seed, 51)).toContain('&b=51');100 expect(closedUrl(seed, 1)).toBe('https://auctions.yahoo.co.jp/closedsearch/closedsearch?p=%E3%83%9D%E3%82%B1%E3%83%A2%E3%83%B3%E3%82%AB%E3%83%BC%E3%83%89%20PSA10');101 });102103 it('CJK helpers: JST dates, grade text, bundles, category refinement', () => {104 expect(parseJstDateTime('2026-09-09 18:32:00')?.toISOString()).toBe('2026-09-09T09:32:00.000Z');105 expect(parseJstDateTime('2026-09-08T17:07:22+09:00')?.toISOString()).toBe('2026-09-08T08:07:22.000Z');106 expect(parseJstDateTime('n/a')).toBeNull();107 expect(normaliseCjkGradeText('【PSA10】リザードン')).toContain('PSA 10');108 expect(isCjkBundle('ポケモンカード まとめ売り 50枚')).toBe(true);109 expect(isCjkBundle('ポケモンカード151 強化拡張パック BOX')).toBe(false);110 expect(refineCategory('trading_cards', '遊戯王 ブラック・マジシャン PSA10')).toBe('yugioh');111 expect(refineCategory('pokemon', '遊戯王 something')).toBe('pokemon'); // explicit seed category wins112 expect(refineCategory('watches', 'ロレックス サブマリーナ 126610LN')).toBe('rolex');113 });114});115