import { describe, expect, it } from 'vitest'; import { extractStructuredData, mayContainStructuredData } from '../extract'; const page = (head: string, body = '') => `${head}${body}`; const ld = (value: unknown, type = 'application/ld+json') => ``; const order = { '@context': 'http://schema.org', '@type': 'Order', orderNumber: '42' }; describe('extractStructuredData: JSON-LD', () => { it('reads blocks in head and body, type compared loosely', () => { const html = page(ld(order), ld({ ...order, orderNumber: '43' }, ' Application/LD+JSON ')); expect(extractStructuredData(html).map((f) => f.item)).toEqual([order, { ...order, orderNumber: '43' }]); }); it('reads a block nested inside a microdata tree', () => { const html = page('', `
${ld(order)}
`); expect(extractStructuredData(html).filter((f) => f.source === 'json-ld').map((f) => f.item)).toEqual([order]); }); it('skips mail without any type it reads, without parsing', () => { const html = page(ld({ '@context': 'http://schema.org', '@type': 'PromotionCard' })); expect(mayContainStructuredData(html)).toBe(false); expect(extractStructuredData(html)).toEqual([]); }); it('ignores markup inside quoted or forwarded parts', () => { const quoted = `
${ld(order)}
${ld(order)}
`; const outlook = `

FYI

From: someone
${ld(order)}
`; expect(extractStructuredData(page('', quoted))).toEqual([]); expect(extractStructuredData(page('', outlook))).toEqual([]); }); it('caps the number of blocks it reads', () => { const html = page(Array.from({ length: 60 }, () => ld(order)).join('')); expect(extractStructuredData(html)).toHaveLength(50); }); it('runs no script and loads nothing while parsing', () => { const html = page('', `${ld(order)}`); extractStructuredData(html); expect((window as unknown as Record).__structuredMailRan).toBeUndefined(); }); }); describe('extractStructuredData: microdata', () => { it('reads values from content, href, src, datetime and text', () => { const html = page('', `
track DHL Express
`); expect(extractStructuredData(html)).toEqual([{ source: 'microdata', item: { '@context': 'https://schema.org', '@type': 'ParcelDelivery', trackingNumber: '123', trackingUrl: 'https://track.example.com/123', image: 'https://img.example.com/a.png', expectedArrivalUntil: '2026-11-02', carrier: 'DHL Express', }, }]); }); it('nests items, splits itemprop lists and turns repeats into arrays', () => { const html = page('', `
`); const [{ item }] = extractStructuredData(html); const shop = { '@type': 'Organization', name: 'Shop' }; expect(item).toEqual({ '@context': 'https://schema.org', '@type': 'Order', orderNumber: '7', merchant: shop, seller: shop, orderStatus: 'http://schema.org/OrderProcessing', acceptedOffer: [ { '@type': 'Offer', itemOffered: { '@type': 'Product', name: 'One' } }, { '@type': 'Offer', itemOffered: { '@type': 'Product', name: 'Two' } }, ], }); }); it('ignores items not typed on schema.org', () => { const html = page('', `
`); expect(extractStructuredData(html)).toEqual([]); }); it('accepts several itemtypes and keeps the schema.org ones', () => { const html = page('', '
'); expect(extractStructuredData(html)[0].item).toMatchObject({ '@type': 'Order', orderNumber: '1' }); }); });