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%
5.8 KB · 87 lines typescript
Raw Blame History
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