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.2 KB · 85 lines typescript
Raw Blame History
1import { describe, expect, it } from 'vitest';2import { getConnectorMeta } from '@rareindex/connectors';3import { loadFixture, runFixtureSuite } from '@rareindex/connectors/testing';4import createConnector, { auctionPageUrl, lotUrl, parseAuctionList, parseAuctionPage, type PagePayload } from './index.js';5import { hintFromLabel, slugFromTitle, watchReference } from '../_auction-lib/categories.js';67const connector = createConnector(getConnectorMeta('bonhams'));89describe('bonhams connector', () => {10  runFixtureSuite(connector, it, expect);1112  it('stores the premium-inclusive price and keeps the hammer', async () => {13    const fx = loadFixture('bonhams', 'results-1');14    const payload = fx.raw.payload as PagePayload;15    const out = await connector.normalize(fx.raw);16    const sold = payload.lots.filter((l) => l.status === 'SOLD' && (l.hammerPremium ?? 0) > 0);17    expect(out.length).toBe(sold.length);18    const lot = sold[0]!;19    const rec = out.find((r) => r.kind === 'sale' && r.externalId === `${payload.auction.id}-${lot.lotNo}`);20    expect(rec && rec.kind === 'sale').toBe(true);21    if (!rec || rec.kind !== 'sale') return;22    expect(rec.price).toBe(lot.hammerPremium);23    expect(rec.buyerPremiumIncluded).toBe(true);24    expect(rec.attributes.metadata.hammer_price).toBe(lot.hammerPrice);25    expect(rec.currency).toBe(lot.currency);26    expect(rec.saleDate.toISOString()).toBe(new Date(lot.hammerTime!).toISOString());27    expect(rec.auctionHouse).toBe('Bonhams');28    expect(rec.lotNumber).toBe(lot.lotNo);29    expect(rec.sourceUrl).toBe(lotUrl(payload.auction.id, lot.lotNo, lot.slug));30    expect(['rolex', 'patek_philippe', 'audemars_piguet', 'omega', 'other_watches']).toContain(rec.attributes.categorySlug);31  });3233  it('emits auction_lot records for lots that have not ended', async () => {34    const fx = loadFixture('bonhams', 'results-1');35    const payload = structuredClone(fx.raw.payload) as PagePayload;36    const future = new Date(Date.now() + 5 * 86_400_000).toISOString();37    payload.auction = { ...payload.auction, isEnded: false, end: future, start: new Date(Date.now() - 86_400_000).toISOString() };38    payload.lots = payload.lots.slice(0, 3).map((l) => ({ ...l, status: 'READY', hammerPrice: null, hammerPremium: null, isEnded: false, hammerTime: future, endDate: future }));39    const out = await connector.normalize({ ...fx.raw, payload });40    expect(out.length).toBe(3);41    for (const r of out) {42      expect(r.kind).toBe('auction_lot');43      if (r.kind !== 'auction_lot') continue;44      expect(r.status).toBe('live');45      expect(r.estimateLow).not.toBeNull();46      expect(r.endsAt?.toISOString()).toBe(new Date(future).toISOString());47    }48  });4950  it('skips unsold, withdrawn and unmapped-department lots', async () => {51    const fx = loadFixture('bonhams', 'results-1');52    const payload = structuredClone(fx.raw.payload) as PagePayload;53    payload.lots = [54      { ...payload.lots[0]!, status: 'UNSOLD', hammerPrice: null, hammerPremium: null },55      { ...payload.lots[1]!, status: 'WITHDRAWN' },56      { ...payload.lots[2]!, department: 'Carpets, Rugs & Tapestries', title: 'A Persian rug' },57      payload.lots[3]!,58    ];59    payload.auction = { ...payload.auction, departments: ['Watches'] };60    const out = await connector.normalize({ ...fx.raw, payload });61    expect(out.map((r) => ('externalId' in r ? r.externalId : null))).toEqual([`${payload.auction.id}-${payload.lots[3]!.lotNo}`]);62  });6364  it('parses listing and auction pages defensively', () => {65    expect(parseAuctionList('<html></html>')).toEqual({ auctions: [], nbHits: null });66    expect(parseAuctionPage('<html></html>')).toBeNull();67    expect(auctionPageUrl({ id: '31992', slug: 'weekly-watches' }, 2)).toBe('https://www.bonhams.com/auction/31992/weekly-watches/?page=2');68  });6970  it('maps departments and titles to taxonomy slugs', () => {71    expect(slugFromTitle('ROLEX. A STAINLESS STEEL AUTOMATIC CHRONOGRAPH WRISTWATCH REF 116500LN DAYTONA', hintFromLabel('Watches'))).toBe('rolex');72    expect(watchReference('REF 116500LN DAYTONA')).toBe('116500LN');73    expect(slugFromTitle('Château Lafite Rothschild 1982 (12 bottles)', hintFromLabel('Wine'))).toBe('wine');74    expect(slugFromTitle('The Macallan 25 Year Old Sherry Oak', hintFromLabel('Whisky'))).toBe('whisky');75    expect(slugFromTitle('1965 Ferrari 275 GTB Berlinetta Chassis no. 07589', hintFromLabel('Cars'))).toBe('automobiles');76    expect(slugFromTitle('Hermès Birkin 30 Togo leather 2019', hintFromLabel('Designer Handbags & Fashion'))).toBe('luxury_handbags');77    expect(slugFromTitle('A Gibson Les Paul Standard owned by ...', hintFromLabel('Popular Culture'))).toBe('musical_instruments');78    expect(slugFromTitle('Star Wars 1977 original one-sheet poster', hintFromLabel('Popular Culture'))).toBe('movie_posters');79    expect(slugFromTitle('Elizabeth II gold sovereign 1958', hintFromLabel('Coins, Medals and Banknotes'))).toBe('coins');80    expect(slugFromTitle('Bank of England £5 banknote 1935', hintFromLabel('Coins, Medals and Banknotes'))).toBe('banknotes');81    expect(slugFromTitle('A large ammonite fossil, Madagascar', hintFromLabel('Natural History'))).toBe('fossils');82    expect(slugFromTitle('Untitled, acrylic on canvas', hintFromLabel('Post-War and Contemporary Art'))).toBe('contemporary_art');83  });84});85