TypeScript 61.9%
HTML 37.2%
SQL 0.7%
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, { parseLotPageDate, parseMillonIndex, parseMillonSalePage, parsePagerMax } 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 INDEX = `<input type="number" class="pagerer-page" max="49"><article class="card no-full"> <div class="card-heading"> <div class="location"> <span class="selling-location">Online</span> </div> <div class="time"> <span class="day-of-sale">lun 06 Juil - lun 07 Sep</span> </div> </div> <a class="card-image" href="/catalogue/vente3964-bd-online-3"></a> <div class="card-content"> <h5 class="card-title"><a href="/catalogue/vente3964-bd-online-3" title="BD online #3">BD online #3</a></h5> </div></article>16<article class="card"><h5 class="card-title"><a href="/catalogue/vente4187-horlogerie" title="Horlogerie">Horlogerie</a></h5><span class="selling-location">Paris</span></article>`;1718const SALE = `<h1 class="title-large">BD online #3</h1> <p class="type">Vente en ligne</p> <p class="time"> <span class="day-of-sale">lun 06 Juil - lun 07 Sep</span></p><span class="sale-status">Online</span>19<input type="number" class="pagerer-page" value="1" min="1" max="6">20<article class="card no-full"> <div class="card-heading"> <span class="lot"> Lot 1 </span> </div> <a href="/catalogue/vente3964-bd-online-3/lot1-anonyme-xx" class="card-image"><picture><source srcset="/sites/millon/files/styles/x/public/auction-items/958005-3964-img1.jpg.webp?itok=a 1x"></picture></a> <div class="card-content"> <h5 class="card-title"><a href="/catalogue/vente3964-bd-online-3/lot1-anonyme-xx">ANONYME (XX)</a></h5> <p class="sub-title">Pifou poche n°49, gag de la page 49.</p> <div class="description"> Encre de Chine et crayon bleu sur papier. </div> <div class="footer"> <p class="estimation"> <span class="label">Estimation | </span> 30 € - 50 € </p> <p class="estimation"> <span class="label">Adjugé à | </span> 1 160 € </p> </div></div></article>21<article class="card no-full"> <span class="lot"> Lot 2 </span> <h5 class="card-title"><a href="/catalogue/vente3964-bd-online-3/lot2-uderzo">UDERZO Albert</a></h5> <p class="sub-title">Astérix, planche originale</p> <p class="estimation"> <span class="label">Estimation | </span> 3 000 € - 5 000 € </p></article>`;2223const LOT = `<p class="sale--info"> Vente <a href="/catalogue/vente3964-bd-online-3" target="_blank" hreflang="fr">BD online #3</a> du 6 juillet 2026 </p>`;2425describe('millon', () => {26 runFixtureSuite(connector, it, expect);2728 it('fixtures: EUR results with unknown premium basis', async () => {29 let sales = 0;30 for (const name of listFixtures('millon')) {31 for (const r of await connector.normalize(loadFixture('millon', name).raw)) {32 if (!('attributes' in r)) continue;33 expect(r.attributes.identifiers.millon_lot).toMatch(/^\d+\/\S+$/);34 expect(r.sourceUrl).toMatch(/^https:\/\/www\.millon\.com\/catalogue\/vente\d+/);35 if (r.kind === 'sale') {36 sales++;37 expect(r.currency).toBe('EUR');38 expect(r.buyerPremiumIncluded).toBeNull();39 expect(r.auctionHouse).toBe('Millon');40 }41 }42 }43 expect(sales).toBeGreaterThan(10);44 });4546 it('parses the past-sales index, pager and lot-page date', () => {47 const sales = parseMillonIndex(INDEX);48 expect(sales.map((s) => s.id)).toEqual(['3964', '4187']);49 expect(sales[0]).toMatchObject({ title: 'BD online #3', url: 'https://www.millon.com/catalogue/vente3964-bd-online-3', location: 'Online' });50 expect(parsePagerMax(INDEX)).toBe(49);51 expect(parseLotPageDate(LOT)).toBe('2026-07-06T00:00:00.000Z');52 expect(parseLotPageDate('<p>rien</p>')).toBeNull();53 });5455 it('parses a sale page (French prices, estimates, sold/unsold, 0-based pagination)', async () => {56 const sale = { id: '3964', title: 'BD online #3', url: 'https://www.millon.com/catalogue/vente3964-bd-online-3', date: '2026-07-06T00:00:00.000Z', location: null, extra: {} };57 const p = parseMillonSalePage(SALE, sale, 1)!;58 expect(p.hasMore).toBe(true);59 expect(parseMillonSalePage(SALE, sale, 6)!.hasMore).toBe(false);60 expect(p.lots).toHaveLength(2);61 expect(p.lots[0]).toMatchObject({ lotNo: '1', title: 'ANONYME (XX)', subtitle: 'Pifou poche n°49, gag de la page 49.', price: 1160, currency: 'EUR', premiumIncluded: null, estimateLow: 30, estimateHigh: 50, sold: true, url: 'https://www.millon.com/catalogue/vente3964-bd-online-3/lot1-anonyme-xx', image: 'https://www.millon.com/sites/millon/files/styles/x/public/auction-items/958005-3964-img1.jpg.webp?itok=a' });62 expect(p.lots[1]).toMatchObject({ lotNo: '2', price: null, sold: false, estimateLow: 3000, estimateHigh: 5000 });63 expect(p.sale?.title).toBe('BD online #3');64 const out = await connector.normalize({ url: sale.url, kind: 'sale', engine: 'api', fetchedAt: new Date(), payload: { kind: 'sale_results', url: sale.url, sale, page: 1, totalLots: null, lots: p.lots } });65 expect(out.map((r) => r.kind)).toEqual(['sale', 'auction_lot']);66 expect(attrsOf(out[0]!).categorySlug).toBe('independent_comics');67 expect(connector.salePageUrl(sale, 2)).toBe('https://www.millon.com/catalogue/vente3964-bd-online-3?page=1');68 expect(parseMillonSalePage('<html></html>', sale, 1)).toBeNull();69 });70});71