import { describe, expect, it } from 'vitest'; import { getConnectorMeta } from '@rareindex/connectors'; import { listFixtures, loadFixture, runFixtureSuite } from '@rareindex/connectors/testing'; import createConnector, { auctionLinks, classifyTitle, hintsForAuction, lotUrl, parseAuctionPage, parseGraphqlLots, type PagePayload } from './index.js'; const connector = createConnector(getConnectorMeta('sothebys')); describe('sothebys connector', () => { runFixtureSuite(connector, it, expect); it('closed sales: price includes premium, hammer kept in metadata, date from the lot closing time', async () => { const closed = listFixtures('sothebys').filter((n) => n.startsWith('closed')); expect(closed.length).toBeGreaterThan(0); const fx = loadFixture('sothebys', closed[0]!); const payload = fx.raw.payload as PagePayload; expect(payload.auction.state).toBe('Closed'); const out = await connector.normalize(fx.raw); expect(out.length).toBeGreaterThan(0); for (const r of out) { expect(r.kind).toBe('sale'); if (r.kind !== 'sale') continue; const lot = payload.lots.find((l) => l.lotId === r.externalId)!; expect(lot.isSold).toBe(true); expect(r.price).toBe(lot.finalPrice); expect(r.price).toBeGreaterThanOrEqual(lot.currentBid ?? 0); expect(r.buyerPremiumIncluded).toBe(true); expect(r.attributes.metadata.hammer_price).toBe(lot.currentBid); expect(r.saleDate.toISOString()).toBe(new Date(lot.closingTime ?? payload.auction.closedAt!).toISOString()); expect(r.auctionHouse).toBe("Sotheby's"); expect(r.sourceUrl).toBe(lotUrl(payload.auction.url, lot.slug, lot.lotId)); } }); it('open sales: lots become auction_lot records with estimates', async () => { const live = listFixtures('sothebys').filter((n) => n.startsWith('live')); expect(live.length).toBeGreaterThan(0); const fx = loadFixture('sothebys', live[0]!); const payload = fx.raw.payload as PagePayload; expect(payload.auction.state).not.toBe('Closed'); const out = await connector.normalize(fx.raw); expect(out.length).toBeGreaterThan(0); for (const r of out) { expect(r.kind).toBe('auction_lot'); if (r.kind !== 'auction_lot') continue; expect(['upcoming', 'live']).toContain(r.status); expect(r.currency).toBe(payload.auction.currency); expect(r.auctionName).toBe(payload.auction.title); } }); it('mixed multi-department sales only trust title keywords', () => { const mixed = { auctionId: 'x', url: 'u', title: 'Arcade | New York', saleNumber: null, state: 'Closed', type: 'Timed', departments: ['American Furniture', 'Decorative Art', 'American Art', 'Books & Manuscripts', 'Contemporary Art'], currency: 'USD', location: 'New York', startsAt: null, endsAt: null, closedAt: null, totalLots: null }; const hints = hintsForAuction(mixed); expect(hints[0]).toBe('unknown'); expect(classifyTitle('Tom Baril — Lilies', hints)).toBeNull(); expect(classifyTitle('Andy Warhol — Marilyn, screenprint', hints)).toBe('contemporary_art'); const watches = { ...mixed, title: 'Important Watches', departments: ['Watches'] }; expect(classifyTitle('Patek Philippe — Nautilus Ref. 5711/1A', hintsForAuction(watches))).toBe('patek_philippe'); }); it('parses GraphQL pages and discovery links defensively', () => { expect(parseAuctionPage('', 'u')).toBeNull(); expect(parseGraphqlLots({ errors: [{ message: 'x' }] })).toBeNull(); const gl = parseGraphqlLots({ data: { auction: { lotCards: { totalCount: 1, hasNextPage: false, lots: [{ lotId: 'l1', title: 'T', lotNumber: { lotDisplayNumber: '7' }, slug: { lotSlug: 't' }, estimateV2: { lowEstimate: { amount: '10' }, highEstimate: { amount: '20' } }, bidState: { isClosed: true, closingTime: '2026-08-26T16:01Z', numberOfBids: 3, currentBidV2: { amount: '900', currency: 'USD' }, sold: { __typename: 'ResultVisible', isSold: true, premiums: { finalPriceV2: { amount: '1152', currency: 'USD' } } } } }] } } } }); expect(gl?.lots[0]).toMatchObject({ lotId: 'l1', lotNumber: '7', estimateLow: 10, currentBid: 900, finalPrice: 1152, finalCurrency: 'USD', isSold: true }); expect(auctionLinks('x https://www.sothebys.com/en/buy/auction/2026/arcade-new-york/lilies')).toEqual(['https://www.sothebys.com/en/buy/auction/2026/arcade-new-york']); }); });