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('', `
`);
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' });
});
});