TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import { describe, expect, it } from 'vitest';2import type { NormalizedRecord } from '@rareindex/shared';3import { ConnectorMetaSchema } from '@rareindex/connectors';4import { listFixtures, loadFixture, runFixtureSuite } from '@rareindex/connectors/testing';5import metaJson from './meta.json' with { type: 'json' };6import createConnector, { hintFromAsianLabel, locationOf, parseFinishedAuctions, parseItemsPage } from './index.js';78const meta = ConnectorMetaSchema.parse(metaJson);9const connector = createConnector(meta);10const attrsOf = (r: NormalizedRecord) => {11 if (!('attributes' in r)) throw new Error(`record kind ${r.kind} has no attributes`);12 return r.attributes;13};1415const AUCTIONS = [16 { id: 133, type: 'StoreCategory', title: '見素集珍:2026年8月中國書畫網拍', lot_count: 141, start_at: '2026-08-13T04:00:00.000Z' },17 { 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' },18 { id: 611, type: 'Auction', title: '東京中央香港2026年4月春季拍賣', lot_count: 900, start_at: '2026-04-29T05:00:00.000Z', place: '香港' },19];20const ITEMS = [21 { 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 },22 { id: 347548, number: '002', title: 'ROLEX 勞力士 DAYTONA 116500LN 腕錶', final_price: null, status: 'unsold', auction_category: { id: 2, auction_id: 612, currency: 'jpy', title: '名錶珠寶' } },23 { 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: '洋酒' } },24 { id: 347550, number: '004', title: 'Unsupported currency lot', final_price: 100, status: 'hammered', auction_category: { id: 4, auction_id: 612, currency: 'xyz', title: 'x' } },25];2627describe('tokyo-chuo', () => {28 runFixtureSuite(connector, it, expect);2930 it('fixtures: hammer-basis sales in JPY/HKD/TWD with the tcabid item id', async () => {31 let sales = 0;32 for (const name of listFixtures('tokyo-chuo')) {33 for (const r of await connector.normalize(loadFixture('tokyo-chuo', name).raw)) {34 if (!('attributes' in r)) continue;35 expect(r.attributes.identifiers.tokyo_chuo_item_id).toMatch(/^\d+\/\S+$/);36 expect(r.sourceUrl).toMatch(/^https:\/\/tcabid\.com\/auction_items\/\d+$/);37 if (r.kind === 'sale') {38 sales++;39 expect(['JPY', 'HKD', 'TWD']).toContain(r.currency);40 expect(r.buyerPremiumIncluded).toBe(false);41 expect(r.auctionHouse).toBe('Tokyo Chuo Auction');42 }43 }44 }45 expect(sales).toBeGreaterThan(10);46 });4748 it('parses finished auctions (drops StoreCategory online sales) and locations', () => {49 const sales = parseFinishedAuctions(AUCTIONS);50 expect(sales.map((s) => s.id)).toEqual(['612', '611']);51 expect(sales[0]).toMatchObject({ title: '東京中央2026年6月珍藏拍賣', url: 'https://tcabid.com/auctions/612', date: '2026-06-12T01:50:00.000Z', location: 'Tokyo, Japan' });52 expect(sales[1]!.location).toBe('Hong Kong');53 expect(locationOf('台北')).toBe('Taipei, Taiwan');54 expect(parseFinishedAuctions({ nope: 1 })).toEqual([]);55 });5657 it('parses an items page: currency per session, sold/unsold, pagination by page size', async () => {58 const sale = parseFinishedAuctions(AUCTIONS)[0]!;59 const p = parseItemsPage(ITEMS, sale, 4)!;60 expect(p.hasMore).toBe(true);61 expect(parseItemsPage(ITEMS, sale, 100)!.hasMore).toBe(false);62 expect(p.lots).toHaveLength(4);63 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' });64 expect(p.lots[1]).toMatchObject({ lotNo: '002', price: null, sold: false });65 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 } });66 // the unsupported-currency lot is dropped by the schema guard (currency must be supported) → 3 records67 expect(out.map((r) => r.kind)).toEqual(['sale', 'auction_lot', 'sale']);68 expect(attrsOf(out[0]!).categorySlug).toBe('antiques');69 if (out[0]!.kind === 'sale') {70 expect(out[0]!.saleDate.toISOString()).toBe('2026-06-12T03:10:00.000Z');71 expect(out[0]!.location).toBe('Tokyo, Japan');72 }73 expect(attrsOf(out[1]!).categorySlug).toBe('rolex');74 expect(attrsOf(out[1]!).reference).toBe('116500LN');75 expect(attrsOf(out[2]!).categorySlug).toBe('whisky');76 expect(parseItemsPage({ error: 1 }, sale, 100)).toBeNull();77 });7879 it('maps zh/ja session labels to department hints', () => {80 expect(hintFromAsianLabel('名錶珠寶 ROLEX')).toBe('watches');81 expect(hintFromAsianLabel('中國書畫')).toBe('art');82 expect(hintFromAsianLabel('瓷器工藝品')).toBe('asian');83 expect(hintFromAsianLabel('洋酒 山崎')).toBe('wine');84 expect(hintFromAsianLabel('random')).toBe('unknown');85 });86});87