import { describe, expect, it } from 'vitest'; import { getConnectorMeta } from '@rareindex/connectors'; import { loadFixture, runFixtureSuite } from '@rareindex/connectors/testing'; import createConnector, { currencyFromText, hintForSale, lotSearchUrl, parseLotSearch, parseResultsMonth, resultsUrl, type PagePayload } from './index.js'; const connector = createConnector(getConnectorMeta('christies')); describe('christies connector', () => { runFixtureSuite(connector, it, expect); it('emits one premium-inclusive sale per realised lot, with the source currency and date', async () => { const fx = loadFixture('christies', 'results-1'); const payload = fx.raw.payload as PagePayload; const out = await connector.normalize(fx.raw); const realised = payload.lots.filter((l) => !l.withdrawn && (l.priceRealised ?? 0) > 0); expect(out.length).toBeGreaterThan(0); expect(out.length).toBeLessThanOrEqual(realised.length); for (const r of out) { expect(r.kind).toBe('sale'); if (r.kind !== 'sale') continue; const lot = payload.lots.find((l) => l.objectId === r.externalId)!; expect(r.price).toBe(lot.priceRealised); expect(r.currency).toBe(currencyFromText(lot.priceRealisedText)); expect(r.buyerPremiumIncluded).toBe(true); expect(r.saleDate.toISOString()).toBe(new Date(lot.endDate ?? payload.sale.endDate!).toISOString()); expect(r.auctionHouse).toBe("Christie's"); expect(r.lotNumber).toBe(lot.lotNumber); expect(r.attributes.identifiers.christies_sale_number).toBe(payload.sale.saleNumber); expect(r.attributes.metadata.estimate_low).toBe(lot.estimateLow); } }); it('skips withdrawn and unsold lots', async () => { const fx = loadFixture('christies', 'results-1'); const payload = structuredClone(fx.raw.payload) as PagePayload; const keep = payload.lots.find((l) => (l.priceRealised ?? 0) > 0)!; payload.lots = [{ ...keep, objectId: 'w', withdrawn: true }, { ...keep, objectId: 'u', priceRealised: null, priceRealisedText: null }, { ...keep, objectId: 'z', priceRealised: 0 }, keep]; const out = await connector.normalize({ ...fx.raw, payload }); expect(out.map((r) => ('externalId' in r ? r.externalId : null))).toEqual([keep.objectId]); }); it('parses month results and lot searches', () => { const month = { filters: { groups: [{ title_txt: 'Category', type: 'category', filters: [{ id: 'category_9', label_txt: 'Jewellery, Watches & Handbags' }] }] }, events: [{ event_id: '31193', landing_url: 'https://www.christies.com/en/auction/x-31193/', title_txt: 'Important Watches', subtitle_txt: 'Live Auction 24546 | CLOSED', filter_ids: '|category_9|location_36|event_0||event_live|', location_txt: 'London', start_date: '2026-07-08T00:00:00', end_date: '2026-07-08T00:00:00', sale_total_value_txt: 'GBP 12,707,879' }], }; const sales = parseResultsMonth(month); expect(sales).toHaveLength(1); expect(sales[0]).toMatchObject({ saleId: '31193', saleNumber: '24546', eventType: 'Live', categoryLabels: ['Jewellery, Watches & Handbags'], location: 'London' }); expect(hintForSale(sales[0]!, ['Jewellery, Watches & Handbags'])).toBe('watches'); expect(lotSearchUrl(sales[0]!, 2, 84)).toContain('SaleNumber=24546&SaleId=31193&page=2&pageSize=84'); expect(resultsUrl(7, 2026)).toBe('https://www.christies.com/api/discoverywebsite/auctioncalendar/auctionresults?language=en&month=7&year=2026'); const ls = parseLotSearch({ lots: [{ object_id: '1', lot_id_txt: '5', title_primary_txt: 'ROLEX', title_secondary_txt: 'REF. 6239 DAYTONA', price_realised: '190500.0', price_realised_txt: 'GBP 190,500', estimate_low: '40000.0', estimate_high: '60000.0', end_date: '2026-07-07T23:00Z', url: 'https://www.christies.com/en/lot/lot-1' }], total_hits_filtered: 1 }); expect(ls.total).toBe(1); expect(ls.lots[0]).toMatchObject({ objectId: '1', lotNumber: '5', priceRealised: 190500, estimateLow: 40000 }); expect(currencyFromText('HKD 1,250,000')).toBe('HKD'); expect(currencyFromText(null)).toBeNull(); }); it('maps a watch lot from a jewellery/watches sale to the brand slug with reference', async () => { const fx = loadFixture('christies', 'results-1'); const payload = structuredClone(fx.raw.payload) as PagePayload; payload.sale = { ...payload.sale, categoryLabels: ['Jewellery, Watches & Handbags'], title: 'Important Watches' }; const base = payload.lots.find((l) => (l.priceRealised ?? 0) > 0)!; payload.lots = [{ ...base, objectId: 'rolex1', titlePrimary: 'ROLEX', titleSecondary: 'REF. 116500LN DAYTONA, A STAINLESS STEEL AUTOMATIC CHRONOGRAPH WRISTWATCH', titleTertiary: null, description: null, priceRealised: 30000, priceRealisedText: 'USD 30,000' }]; const out = await connector.normalize({ ...fx.raw, payload }); expect(out).toHaveLength(1); const r = out[0]!; if (r.kind !== 'sale') throw new Error('expected sale'); expect(r.attributes.categorySlug).toBe('rolex'); expect(r.attributes.identifiers.reference).toBe('116500LN'); expect(r.attributes.brand).toBe('Rolex'); expect(r.currency).toBe('USD'); }); });