mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 16:13:15 +00:00
103 lines
5.2 KiB
TypeScript
103 lines
5.2 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { extractStructuredData, mayContainStructuredData } from '../extract';
|
|
|
|
const page = (head: string, body = '') => `<!DOCTYPE html><html><head>${head}</head><body>${body}</body></html>`;
|
|
const ld = (value: unknown, type = 'application/ld+json') => `<script type="${type}">${JSON.stringify(value)}</script>`;
|
|
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('', `<div itemscope itemtype="http://schema.org/EmailMessage">${ld(order)}</div>`);
|
|
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 = `<blockquote>${ld(order)}</blockquote><div class="gmail_quote">${ld(order)}</div>`;
|
|
const outlook = `<p>FYI</p><div id="divRplyFwdMsg">From: someone</div><div>${ld(order)}</div>`;
|
|
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('<script>window.__structuredMailRan = true</script>', `<img src="https://tracker.invalid/x.gif">${ld(order)}`);
|
|
extractStructuredData(html);
|
|
expect((window as unknown as Record<string, unknown>).__structuredMailRan).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('extractStructuredData: microdata', () => {
|
|
it('reads values from content, href, src, datetime and text', () => {
|
|
const html = page('', `
|
|
<div itemscope itemtype="http://schema.org/ParcelDelivery">
|
|
<meta itemprop="trackingNumber" content=" 123 ">
|
|
<a itemprop="trackingUrl" href="https://track.example.com/123">track</a>
|
|
<img itemprop="image" src="https://img.example.com/a.png">
|
|
<time itemprop="expectedArrivalUntil" datetime="2026-11-02">2 Nov</time>
|
|
<span itemprop="carrier"> DHL
|
|
Express </span>
|
|
</div>`);
|
|
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('', `
|
|
<div itemscope itemtype="https://schema.org/Order">
|
|
<meta itemprop="orderNumber" content="7">
|
|
<div itemprop="merchant seller" itemscope itemtype="http://schema.org/Organization"><meta itemprop="name" content="Shop"></div>
|
|
<meta itemprop="orderStatus" content="">
|
|
<link itemprop="orderStatus" href="http://schema.org/OrderProcessing">
|
|
<div itemprop="acceptedOffer" itemscope itemtype="http://schema.org/Offer">
|
|
<div itemprop="itemOffered" itemscope itemtype="http://schema.org/Product"><meta itemprop="name" content="One"></div>
|
|
</div>
|
|
<div itemprop="acceptedOffer" itemscope itemtype="http://schema.org/Offer">
|
|
<div itemprop="itemOffered" itemscope itemtype="http://schema.org/Product"><meta itemprop="name" content="Two"></div>
|
|
</div>
|
|
</div>`);
|
|
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('', `
|
|
<div itemscope itemtype="http://example.com/Order"><meta itemprop="orderNumber" content="1"></div>
|
|
<div itemscope><meta itemprop="orderNumber" content="2"></div>`);
|
|
expect(extractStructuredData(html)).toEqual([]);
|
|
});
|
|
|
|
it('accepts several itemtypes and keeps the schema.org ones', () => {
|
|
const html = page('', '<div itemscope itemtype="http://example.com/X http://schema.org/Order"><meta itemprop="orderNumber" content="1"></div>');
|
|
expect(extractStructuredData(html)[0].item).toMatchObject({ '@type': 'Order', orderNumber: '1' });
|
|
});
|
|
});
|