import { describe, expect, it } from 'vitest'; import type { NormalizedRecord } from '@rareindex/shared'; import { ConnectorMetaSchema } from '@rareindex/connectors'; import { listFixtures, loadFixture, runFixtureSuite } from '@rareindex/connectors/testing'; import metaJson from './meta.json' with { type: 'json' }; import createConnector, { hintFromAsianLabel, locationOf, parseFinishedAuctions, parseItemsPage } from './index.js'; const meta = ConnectorMetaSchema.parse(metaJson); const connector = createConnector(meta); const attrsOf = (r: NormalizedRecord) => { if (!('attributes' in r)) throw new Error(`record kind ${r.kind} has no attributes`); return r.attributes; }; const AUCTIONS = [ { id: 133, type: 'StoreCategory', title: '見素集珍:2026年8月中國書畫網拍', lot_count: 141, start_at: '2026-08-13T04:00:00.000Z' }, { id: 612, type: 'Auction', title: '東京中央2026年6月珍藏拍賣', lot_count: 452, start_at: '2026-06-12T01:50:00.000Z', place: '株式會社 東京中央拍賣(東京都中央區京橋3-7-5)', auction_items_number_range: 'LOT 001 ~ 462' }, { id: 611, type: 'Auction', title: '東京中央香港2026年4月春季拍賣', lot_count: 900, start_at: '2026-04-29T05:00:00.000Z', place: '香港' }, ]; const ITEMS = [ { id: 347547, number: '001', title: '清 乾隆 青花纏枝蓮紋賞瓶', desc: '高 38cm', final_price: 450000, starting_price: 300000, estimate_price_from: 400000, estimate_price_to: 600000, status: 'hammered', auction_start_at: '2026-06-12T01:50:00.000Z', transaction_time: '2026-06-12T03:10:00.000Z', cover: { url: 'https://bucket-biddingart-production.oss-accelerate.aliyuncs.com/a' }, images: [{ url: 'https://bucket-biddingart-production.oss-accelerate.aliyuncs.com/b' }], auction_category: { id: 1, auction_id: 612, currency: 'jpy', title: '瓷器工藝品', place: '東京' }, show_items_final_price: true, is_finished: true }, { id: 347548, number: '002', title: 'ROLEX 勞力士 DAYTONA 116500LN 腕錶', final_price: null, status: 'unsold', auction_category: { id: 2, auction_id: 612, currency: 'jpy', title: '名錶珠寶' } }, { id: 347549, number: '003', title: '山崎 18年 威士忌 700ml', final_price: 90000, status: 'hammered', transaction_time: '2026-06-12T05:00:00.000Z', auction_category: { id: 3, auction_id: 612, currency: 'jpy', title: '洋酒' } }, { id: 347550, number: '004', title: 'Unsupported currency lot', final_price: 100, status: 'hammered', auction_category: { id: 4, auction_id: 612, currency: 'xyz', title: 'x' } }, ]; describe('tokyo-chuo', () => { runFixtureSuite(connector, it, expect); it('fixtures: hammer-basis sales in JPY/HKD/TWD with the tcabid item id', async () => { let sales = 0; for (const name of listFixtures('tokyo-chuo')) { for (const r of await connector.normalize(loadFixture('tokyo-chuo', name).raw)) { if (!('attributes' in r)) continue; expect(r.attributes.identifiers.tokyo_chuo_item_id).toMatch(/^\d+\/\S+$/); expect(r.sourceUrl).toMatch(/^https:\/\/tcabid\.com\/auction_items\/\d+$/); if (r.kind === 'sale') { sales++; expect(['JPY', 'HKD', 'TWD']).toContain(r.currency); expect(r.buyerPremiumIncluded).toBe(false); expect(r.auctionHouse).toBe('Tokyo Chuo Auction'); } } } expect(sales).toBeGreaterThan(10); }); it('parses finished auctions (drops StoreCategory online sales) and locations', () => { const sales = parseFinishedAuctions(AUCTIONS); expect(sales.map((s) => s.id)).toEqual(['612', '611']); expect(sales[0]).toMatchObject({ title: '東京中央2026年6月珍藏拍賣', url: 'https://tcabid.com/auctions/612', date: '2026-06-12T01:50:00.000Z', location: 'Tokyo, Japan' }); expect(sales[1]!.location).toBe('Hong Kong'); expect(locationOf('台北')).toBe('Taipei, Taiwan'); expect(parseFinishedAuctions({ nope: 1 })).toEqual([]); }); it('parses an items page: currency per session, sold/unsold, pagination by page size', async () => { const sale = parseFinishedAuctions(AUCTIONS)[0]!; const p = parseItemsPage(ITEMS, sale, 4)!; expect(p.hasMore).toBe(true); expect(parseItemsPage(ITEMS, sale, 100)!.hasMore).toBe(false); expect(p.lots).toHaveLength(4); expect(p.lots[0]).toMatchObject({ lotNo: '001', price: 450000, currency: 'JPY', premiumIncluded: false, estimateLow: 400000, estimateHigh: 600000, sold: true, date: '2026-06-12T03:10:00.000Z', image: 'https://bucket-biddingart-production.oss-accelerate.aliyuncs.com/a' }); expect(p.lots[1]).toMatchObject({ lotNo: '002', price: null, sold: false }); const out = await connector.normalize({ url: sale.url, kind: 'sale', engine: 'api', fetchedAt: new Date('2026-09-08T00:00:00Z'), payload: { kind: 'sale_results', url: sale.url, sale, page: 1, totalLots: null, lots: p.lots } }); // the unsupported-currency lot is dropped by the schema guard (currency must be supported) → 3 records expect(out.map((r) => r.kind)).toEqual(['sale', 'auction_lot', 'sale']); expect(attrsOf(out[0]!).categorySlug).toBe('antiques'); if (out[0]!.kind === 'sale') { expect(out[0]!.saleDate.toISOString()).toBe('2026-06-12T03:10:00.000Z'); expect(out[0]!.location).toBe('Tokyo, Japan'); } expect(attrsOf(out[1]!).categorySlug).toBe('rolex'); expect(attrsOf(out[1]!).reference).toBe('116500LN'); expect(attrsOf(out[2]!).categorySlug).toBe('whisky'); expect(parseItemsPage({ error: 1 }, sale, 100)).toBeNull(); }); it('maps zh/ja session labels to department hints', () => { expect(hintFromAsianLabel('名錶珠寶 ROLEX')).toBe('watches'); expect(hintFromAsianLabel('中國書畫')).toBe('art'); expect(hintFromAsianLabel('瓷器工藝品')).toBe('asian'); expect(hintFromAsianLabel('洋酒 山崎')).toBe('wine'); expect(hintFromAsianLabel('random')).toBe('unknown'); }); });