TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import { describe, expect, it } from 'vitest';2import { readFileSync } from 'node:fs';3import path from 'node:path';4import { fileURLToPath } from 'node:url';5import { runFixtureSuite } from '@rareindex/connectors/testing';6import { localMeta } from '../_lib/local-meta.js';7import { parseUsDate } from '../_g7-auctions-na-lib/index.js';8import createConnector, { doyleCategory, parseAuctionHeaderDate, parsePastAuctions, parseResultsPage } from './index.js';910const dir = path.dirname(fileURLToPath(import.meta.url));11const meta = localMeta(JSON.parse(readFileSync(path.join(dir, 'meta.json'), 'utf8')));12const connector = createConnector(meta);1314// Real fragments of https://www.doyle.com/past-auctions/ (captured 2026-09-08): a live sale ("Date:") and an online sale ("Ends:").15const CALENDAR = `<div class="auction-calendar-item calendar-third nv"> <a href='/auction/26bp01-rare-books-autographs--maps?au=9180'> <div class="auction-calendar-image" style=""> <img src="https://goauctiondoyle.blob.core.windows.net/26bp01/26BP01-post-sale-placard-1.png" alt="Rare Books, Autographs & Maps" /> </div> </a> <div class="auction-calendar-text "> <a href='/auction/26bp01-rare-books-autographs--maps?au=9180'> <H3>Rare Books, Autographs & Maps</H3> </a> <div> <a href='/auction/26bp01-rare-books-autographs--maps?au=9180'> <strong> Date: Apr 16, 2026 10:00 EST </strong> <br /> </a> </div> <div class="auction-calendar-buttons-left"> <div class="auction-calendar-buttons-left"> <a href="/auction/26bp01-rare-books-autographs--maps?au=9180" class="btn">View Results</a> </div> </div> </div> </div>16<div class="auction-calendar-item calendar-third nv"> <a href='/auction/26kh01-80s-new-york-featuring-the-keith-haring-pop-shop-collection-of-julia-markus?au=9241'> <div class="auction-calendar-image" style=""> <img src="https://goauctiondoyle.goauction.co.uk/placards/26KH01-Placa.jpg" alt="x" /> </div> </a> <div class="auction-calendar-text "> <a href='/auction/26kh01-80s-new-york-featuring-the-keith-haring-pop-shop-collection-of-julia-markus?au=9241'> <H3>'80s New York Featuring the Keith Haring Pop Shop Collection of Julia Markus </H3> </a> <div> <a href='/auction/26kh01-80s-new-york-featuring-the-keith-haring-pop-shop-collection-of-julia-markus?au=9241'> <strong> Ends: Aug 20, 2026 10:00 EST </strong> <br /> </a> </div> </div> </div>17<div class="auction-calendar-item calendar-third nv"> <a href='/section-detail/property-from-the-estate-of-jorie-butler-kent?i=62'> <H3>Section without an auction id</H3> </a> </div>`;1819// Real fragment of https://www.doyle.com/auction/search/?au=9180 (page 1 of 5): a sold lot and an unsold lot, plus the pager.20const RESULTS = `<nav class="pagination"><select class='form-control pull-left active-select'><option value='/auction/search/?au=9180&g=1' selected>1</option><option value='/auction/search/?au=9180&pn=2&g=1' >2</option></select><span class='pull-left'> of 5</span><a href='/auction/search/?au=9180&pn=2&g=1' class='btn btn-sm pull-right'><i class='fal fa-chevron-right fa-fw'></i></a></nav>21<div class='auction-grid agc agh'><div class='auction-grid-lot col-sm-6 col-md-4 col-lg-4'> <div class="auction-lot"> <div class="auction-lot-image" style="position: relative;"> <a href="/auction/lot/lot-1---clutch-of-five-sauropod-eggs/?lot=1461680&so=0&st=&sto=0&au=9180&ef=&et=&ic=False&sd=1&mc=409&pp=96&pn=1&g=1"> <img src="https://goauctiondoyle.goauction.co.uk/stock/2079710-1-small.jpg?v=63907825355487" alt="Lot 1 - Clutch of five sauropod eggs" /> </a> </div> <div class="auction-lot-text " style="position: relative;"> <a class="anchor-offset" name="1461680"></a> <p class="auction-lot-title"> <a href="/auction/lot/lot-1---clutch-of-five-sauropod-eggs/?lot=1461680&so=0&st=&sto=0&au=9180&ef=&et=&ic=False&sd=1&mc=409&pp=96&pn=1&g=1"> <span class='lot-title cat-146'>Lot 1<br />Clutch of five sauropod eggs</span> </a> </p> <p></p> <p> <strong>Sold for $2,048</strong> <br /> <strong>Estimated at $1,500 - $3,000</strong> </p> </div> <div class="clearfix"></div> </div> </div>22<div class='auction-grid-lot col-sm-6 col-md-4 col-lg-4'> <div class="auction-lot"> <div class="auction-lot-image" style="position: relative;"> <a href="/auction/lot/lot-6---a-complete-set-of-the-first-aldine-edition-of-livy/?lot=1468947&so=0&au=9180&pp=96&pn=1&g=1"> <img src="https://goauctiondoyle.goauction.co.uk/stock/2102486-2-small.jpg?v=63909982187970" alt="Lot 6 - A complete set of the first Aldine edition of Livy" /> </a> </div> <div class="auction-lot-text " style="position: relative;"> <a class="anchor-offset" name="1468947"></a> <p class="auction-lot-title"> <a href="/auction/lot/lot-6---a-complete-set-of-the-first-aldine-edition-of-livy/?lot=1468947&so=0&au=9180&pp=96&pn=1&g=1"> <span class='lot-title cat-4'>Lot 6<br />A complete set of the first Aldine edition of Livy</span> </a> </p> <p><p class="stockfields-list">Property sold to benefit Albertus Magnus High School</p></p> <p> <strong>Estimated at $6,000 - $9,000</strong> </p> </div> <div class="clearfix"></div> </div> </div></div>`;2324describe('doyle', () => {25 runFixtureSuite(connector, it, expect);2627 it('parses the past-auctions calendar (Date: and Ends: labels, skips entries without an auction id)', () => {28 const list = parsePastAuctions(CALENDAR);29 expect(list.length).toBe(2);30 expect(list[0]).toEqual({ auId: '9180', code: '26BP01', name: 'Rare Books, Autographs & Maps', dateText: 'Apr 16, 2026 10:00 EST', url: 'https://www.doyle.com/auction/26bp01-rare-books-autographs--maps?au=9180' });31 expect(list[1]).toMatchObject({ auId: '9241', code: '26KH01', dateText: 'Aug 20, 2026 10:00 EST' });32 expect(list[1]!.name.startsWith("'80s New York")).toBe(true);33 // 10:00 US Eastern (EST label) → 15:00 UTC34 expect(parseUsDate(list[0]!.dateText)?.toISOString()).toBe('2026-04-16T15:00:00.000Z');35 expect(parseUsDate('Apr 16, 2026')?.toISOString()).toBe('2026-04-16T00:00:00.000Z');36 });3738 it('parses result cards: lot number/title split, price, estimate, lot id, canonical url, pagination', () => {39 const auction = parsePastAuctions(CALENDAR)[0]!;40 const p = parseResultsPage(RESULTS, auction, 1);41 expect(p.kind).toBe('doyle_results_page');42 expect(p.hasNext).toBe(true);43 expect(p.lots.length).toBe(2);44 expect(p.lots[0]).toMatchObject({ lotId: '1461680', lotNumber: '1', title: 'Clutch of five sauropod eggs', soldPrice: 2048, estimateLow: 1500, estimateHigh: 3000, categoryId: '146', url: 'https://www.doyle.com/auction/lot/lot-1---clutch-of-five-sauropod-eggs/?lot=1461680&au=9180' });45 expect(p.lots[0]!.image).toContain('/stock/2079710-1-small.jpg');46 expect(p.lots[1]).toMatchObject({ lotId: '1468947', lotNumber: '6', soldPrice: null, soldText: null, estimateLow: 6000, estimateHigh: 9000, categoryId: '4' });47 // last page: no pn=2 link48 expect(parseResultsPage(RESULTS.replace(/pn=2/g, 'pn=9'), auction, 1).hasNext).toBe(false);49 expect(parseAuctionHeaderDate('<html><body><div>Request an Auction Estimate Apr 16, 2026 10:00 EST Rare Books</div></body></html>')).toBe('Apr 16, 2026 10:00 EST');50 });5152 it('normalises only sold lots, dated by the calendar, USD with buyer premium included', async () => {53 const auction = parsePastAuctions(CALENDAR)[0]!;54 const payload = parseResultsPage(RESULTS, auction, 1);55 const out = await connector.normalize({ url: 'https://www.doyle.com/auction/search/?au=9180&pp=96&g=1', externalId: 'auction:9180:page:1', kind: 'sale', engine: 'api', fetchedAt: new Date('2026-09-08T00:00:00Z'), payload });56 expect(out.length).toBe(1);57 const s = out[0]!;58 if (s.kind !== 'sale') throw new Error('expected sale');59 expect(s).toMatchObject({ price: 2048, currency: 'USD', buyerPremiumIncluded: true, auctionHouse: 'Doyle', lotNumber: '1', location: 'US', externalId: '1461680' });60 expect(s.saleDate.toISOString()).toBe('2026-04-16T15:00:00.000Z');61 expect(s.attributes.categorySlug).toBe('fossils');62 expect(s.attributes.identifiers).toEqual({ doyle_lot_id: '1461680' });63 expect(s.attributes.metadata).toMatchObject({ auction_id: '9180', auction_code: '26BP01', estimate_low: 1500, estimate_high: 3000, doyle_category_id: '146' });64 // no calendar date → nothing is emitted (never guess a sale date)65 expect(await connector.normalize({ url: 'x', externalId: 'y', kind: 'sale', engine: 'api', fetchedAt: new Date(), payload: { ...payload, auction: { ...auction, dateText: null } } })).toEqual([]);66 });6768 it('maps sale departments and titles to taxonomy slugs', () => {69 expect(doyleCategory('Rare Books, Autographs & Maps', 'A complete set of the first Aldine edition of Livy')).toBe('books');70 expect(doyleCategory('Rare Books, Autographs & Maps', 'Map of the Americas, engraved, 1650')).toBe('maps');71 expect(doyleCategory('Rare Books, Autographs & Maps', 'Thomas Jefferson autograph letter signed to Charles Willson Peale')).toBe('historical_documents');72 expect(doyleCategory('Fine Jewelry', 'Platinum, diamond and sapphire ring')).toBe('jewelry');73 expect(doyleCategory('Important Watches', 'Rolex stainless steel Submariner wristwatch Ref. 5513')).toBe('rolex');74 expect(doyleCategory('Coins, Bank Notes, Stamps & Collectibles', '1921 Morgan silver dollar MS65')).toBe('coins');75 expect(doyleCategory('Coins, Bank Notes, Stamps & Collectibles', 'United States $20 legal tender banknote 1880')).toBe('banknotes');76 expect(doyleCategory('English & Continental Furniture, Old Master Paintings, Silver', 'George III mahogany chest of drawers')).toBe('antiques');77 expect(doyleCategory('English & Continental Furniture, Old Master Paintings, Silver', 'Sterling silver tea service, Tiffany & Co.')).toBe('silver');78 expect(doyleCategory('Post-War & Contemporary Art', 'Keith Haring, Untitled, screenprint, edition of 100')).toBe('contemporary_art');79 expect(doyleCategory('Doyle at Home', 'Meissen porcelain figure group')).toBe('porcelain');80 expect(doyleCategory('The Collection of a Florida Bibliophile', 'Hemingway, first edition, dust jacket')).toBe('books');81 expect(doyleCategory('Entertaining with Style', 'Untitled mixed lot')).toBeNull();82 });83});84