TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import { adapters, type ConnectorMeta, type RareIndexConnector } from '@rareindex/connectors';2import { listFixtures, loadFixture, runFixtureSuite } from '@rareindex/connectors/testing';3import { SUPPORTED_CURRENCIES, type NormalizedListing } from '@rareindex/shared';45type It = (name: string, fn: () => Promise<void> | void) => void;6// vitest's `expect` is typed loosely here so the helper does not depend on vitest at type level.7type Expect = (v: unknown) => any;89/**10 * Shared vitest suite for the g4-shops-intl storefront connectors (metadata-only Shopify/WooCommerce11 * stores). Runs the framework fixture suite plus the invariants every store connector must honour:12 * native currency = config.currency, every category is one the connector declares, listings only13 * (asking prices are never sales, SPEC §111), seller label present, stable external ids, no zero prices.14 */15export function storeSuite(meta: ConnectorMeta, connector: RareIndexConnector, it: It, expect: Expect): void {16 const cfg = adapters.StorefrontConfigSchema.parse(meta.config);17 runFixtureSuite(connector, it, expect);1819 it('declares a supported native currency and complete compliance metadata', () => {20 expect(SUPPORTED_CURRENCIES as readonly string[]).toContain(cfg.currency);21 expect(meta.currency).toContain(cfg.currency);22 expect(Boolean(meta.accessNotes && meta.accessNotes.length > 120 && !/TODO/.test(meta.accessNotes))).toBe(true);23 expect(meta.supportsListings).toBe(true);24 expect(meta.supportsSold).toBe(false);25 expect(meta.refreshFrequencyMinutes).toBeGreaterThanOrEqual(360);26 expect(cfg.collections.length + (meta.config.wholeShop ? 1 : 0)).toBeGreaterThan(0);27 for (const c of cfg.collections) expect(/^[a-z0-9-]+$/.test(c.handle)).toBe(true);28 expect(new Set(cfg.collections.map((c) => c.handle)).size).toBe(cfg.collections.length);29 });3031 it('every collection and rule maps onto a category the connector declares', () => {32 const declared = new Set(meta.categories);33 for (const c of cfg.collections) if (c.categorySlug) expect(declared.has(c.categorySlug)).toBe(true);34 for (const r of cfg.rules) {35 expect(declared.has(r.categorySlug)).toBe(true);36 expect(() => new RegExp(r.match, 'i')).not.toThrow();37 }38 expect(() => new RegExp(cfg.exclude, 'i')).not.toThrow();39 if (cfg.titlePattern) expect(() => new RegExp(cfg.titlePattern!, 'i')).not.toThrow();40 });4142 it('normalises every fixture into listings in the shop currency with stable ids', async () => {43 for (const name of listFixtures(meta.id)) {44 const fx = loadFixture(meta.id, name);45 const out = await connector.normalize(fx.raw);46 expect(out.length).toBeGreaterThan(0);47 const ids = new Set<string>();48 for (const r of out) {49 expect(r.kind).toBe('listing');50 if (r.kind !== 'listing') continue;51 expect(r.currency).toBe(cfg.currency);52 expect(r.price).toBeGreaterThan(0);53 expect(r.listingType).toBe('fixed_price');54 expect(r.seller).toBe(cfg.seller ?? null);55 expect(meta.categories).toContain(r.attributes.categorySlug);56 expect(r.sourceUrl.startsWith(meta.sourceUrl)).toBe(true);57 expect(Boolean(r.externalId && r.externalId.length > 0)).toBe(true);58 expect(ids.has(r.externalId!)).toBe(false);59 ids.add(r.externalId!);60 expect(r.confidence).toBeLessThan(1);61 expect(['available', 'ended', 'unknown']).toContain(r.availability);62 }63 }64 });65}6667export interface MappingCase {68 title: string;69 /** collection handle (Shopify) / category slug (WooCommerce) the product was listed under */70 collection: string | null;71 type?: string | null;72 tags?: string[];73 vendor?: string | null;74 /** optional single variant title (e.g. "Near Mint Foil", "UK 9") */75 variant?: string | null;76 /** expected taxonomy slug, or null when the product must be skipped (accessory, unmapped…) */77 expect: string | null;78 set?: string | null;79 number?: string | null;80 name?: string;81 grader?: string | null;82 grade?: string | null;83 brand?: string | null;84 series?: string | null;85 franchise?: string | null;86 conditionRaw?: string | null;87 year?: number | null;88}8990/**91 * Parser unit test on synthetic storefront payloads: checks the collection → rule → exclude chain92 * and titlePattern extraction for representative titles seen live. Complements the real fixtures.93 */94export async function expectMapping(meta: ConnectorMeta, connector: RareIndexConnector, expect: Expect, cases: MappingCase[]): Promise<void> {95 const woo = /woocommerce/i.test(meta.acquisitionMethod ?? '');96 const cfg = adapters.StorefrontConfigSchema.parse(meta.config);97 const site = meta.sourceUrl.replace(/\/+$/, '');98 let n = 1;99 for (const c of cases) {100 const id = 900000 + n++;101 const payload = woo102 ? { category: c.collection, product: { id, name: c.title, slug: `p-${id}`, permalink: `${site}/product/p-${id}/`, sku: null, prices: { price: '1999', regular_price: '1999', currency_code: cfg.currency, currency_minor_unit: 2 }, images: [], categories: c.type ? c.type.split(' / ').map((name) => ({ name, slug: name.toLowerCase().replace(/\s+/g, '-') })) : [], tags: (c.tags ?? []).map((name) => ({ name })), is_in_stock: true, type: 'simple', brands: c.vendor ? [{ name: c.vendor }] : undefined } }103 : { collection: c.collection, product: { id, title: c.title, handle: `p-${id}`, vendor: c.vendor ?? null, product_type: c.type ?? null, tags: c.tags ?? [], variants: [{ id: id * 10, title: c.variant ?? 'Default Title', sku: null, price: '19.99', available: true }], images: [] } };104 const out = (await connector.normalize({ url: `${site}/products/p-${id}`, externalId: String(id), kind: 'listing', engine: 'api', fetchedAt: new Date('2026-09-08T00:00:00Z'), payload })) as NormalizedListing[];105 const label = `"${c.title}" [${c.collection ?? '-'}]`;106 if (c.expect === null) {107 expect({ label, count: out.length }).toEqual({ label, count: 0 });108 continue;109 }110 expect({ label, count: out.length }).toEqual({ label, count: 1 });111 const r = out[0]!;112 expect({ label, slug: r.attributes.categorySlug }).toEqual({ label, slug: c.expect });113 if (c.set !== undefined) expect({ label, set: r.attributes.set }).toEqual({ label, set: c.set });114 if (c.number !== undefined) expect({ label, number: r.attributes.number }).toEqual({ label, number: c.number });115 if (c.name !== undefined) expect({ label, name: r.attributes.name }).toEqual({ label, name: c.name });116 if (c.grader !== undefined) expect({ label, grader: r.grade.grader }).toEqual({ label, grader: c.grader });117 if (c.grade !== undefined) expect({ label, grade: r.grade.grade }).toEqual({ label, grade: c.grade });118 if (c.brand !== undefined) expect({ label, brand: r.attributes.brand }).toEqual({ label, brand: c.brand });119 if (c.series !== undefined) expect({ label, series: r.attributes.series }).toEqual({ label, series: c.series });120 if (c.franchise !== undefined) expect({ label, franchise: r.attributes.franchise }).toEqual({ label, franchise: c.franchise });121 if (c.conditionRaw !== undefined) expect({ label, conditionRaw: r.condition.conditionRaw }).toEqual({ label, conditionRaw: c.conditionRaw });122 if (c.year !== undefined) expect({ label, year: r.attributes.year }).toEqual({ label, year: c.year });123 }124}125