SPB Git forge

spb/rareindex

Public
54commits 1branches 0releases
7.1 MBsize
maindefault branch
10 days agolast push
TypeScript 61.9% HTML 37.2% SQL 0.7%
6.4 KB · 115 lines typescript
Raw Blame History
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