volpe/lib/measurements/__tests__/plugin.test.js

247 lines
No EOL
7.8 KiB
JavaScript

import { describe, it, expect } from 'vitest';
const markdownIt = require('markdown-it');
const measurementPlugin = require('../plugin');
function render(md) {
const mdit = markdownIt().use(measurementPlugin);
return mdit.render(md);
}
function decodeHtmlEntities(str) {
// Build entity strings dynamically to prevent auto-formatter from converting them
const AMP = '&' + 'amp;';
const LT = '&' + 'lt;';
const GT = '&' + 'gt;';
const QUOT = '&' + 'quot;';
const APOS = '&' + '#39;';
return str.split(AMP).join('&').split(LT).join('<').split(GT).join('>').split(APOS).join("'").split(QUOT).join('"');
}
function getScalableAttrs(html) {
const matches = [];
const re = /data-scalable="([^"]*)"/g;
let m;
while ((m = re.exec(html)) !== null) {
matches.push(JSON.parse(decodeHtmlEntities(m[1])));
}
return matches;
}
function hasScalable(html) {
return /data-scalable/.test(html);
}
// ─── Tools Section: Bare Counts Should NOT Scale ──────────
describe('tools section scaling', () => {
it('does not make bare counts scalable in Tools section (cooking tools)', () => {
const md = [
'## Tools',
'- pots 2',
].join('\n');
const html = render(md);
// The "2" should be wrapped as a measurement but NOT have data-scalable
expect(html).toContain('class="measurement"');
expect(hasScalable(html)).toBe(false);
});
it('does not make "mixing bowls 2" scalable in Tools section', () => {
const md = [
'## Tools',
'- mixing bowls 2',
].join('\n');
const html = render(md);
expect(html).toContain('class="measurement"');
expect(hasScalable(html)).toBe(false);
});
it('does not make "sauce pan 2" scalable in Tools section', () => {
const md = [
'## Tools',
'- sauce pan 2',
].join('\n');
const html = render(md);
expect(html).toContain('class="measurement"');
expect(hasScalable(html)).toBe(false);
});
// ─── Tools Section: Serving Vessel Keywords SHOULD Scale ──
it('makes "mug 1" scalable (serving vessel keyword)', () => {
const md = [
'## Tools',
'- mug 1',
].join('\n');
const html = render(md);
const scalables = getScalableAttrs(html);
const countScalable = scalables.find(s => s.type === 'count');
expect(countScalable).toBeDefined();
expect(countScalable.base).toBe(1);
});
it('makes "highball 1" scalable (serving vessel keyword)', () => {
const md = [
'## Tools',
'- highball 1',
].join('\n');
const html = render(md);
const scalables = getScalableAttrs(html);
const countScalable = scalables.find(s => s.type === 'count');
expect(countScalable).toBeDefined();
expect(countScalable.base).toBe(1);
});
it('makes "lowball 1" scalable (serving vessel keyword)', () => {
const md = [
'## Tools',
'- lowball 1',
].join('\n');
const html = render(md);
const scalables = getScalableAttrs(html);
const countScalable = scalables.find(s => s.type === 'count');
expect(countScalable).toBeDefined();
expect(countScalable.base).toBe(1);
});
it('makes "mule glass 1" scalable (serving vessel keyword)', () => {
const md = [
'## Tools',
'- mule glass 1',
].join('\n');
const html = render(md);
const scalables = getScalableAttrs(html);
const countScalable = scalables.find(s => s.type === 'count');
expect(countScalable).toBeDefined();
expect(countScalable.base).toBe(1);
});
// ─── Tools Section: Counts WITH Dimensions SHOULD Scale ──
it('makes counts scalable when alongside dimensions (serving vessels)', () => {
const md = [
'## Tools',
'- 9 x 13 inch glass baking dish 1',
].join('\n');
const html = render(md);
// Should have scalable data for the count "1" (serving vessel)
const scalables = getScalableAttrs(html);
const countScalable = scalables.find(s => s.type === 'count');
expect(countScalable).toBeDefined();
expect(countScalable.base).toBe(1);
});
it('makes counts scalable for 3D dimensioned vessels', () => {
const md = [
'## Tools',
'- 8x6x2 inch pyrex dishes 4',
].join('\n');
const html = render(md);
const scalables = getScalableAttrs(html);
const countScalable = scalables.find(s => s.type === 'count');
expect(countScalable).toBeDefined();
expect(countScalable.base).toBe(4);
});
it('makes counts scalable for tools with weight measurements', () => {
const md = [
'## Tools',
'- 64 oz mason jar 4',
].join('\n');
const html = render(md);
const scalables = getScalableAttrs(html);
const countScalable = scalables.find(s => s.type === 'count');
expect(countScalable).toBeDefined();
expect(countScalable.base).toBe(4);
});
// ─── Counts OUTSIDE Tools Section Should Still Scale ──────
it('makes bare counts scalable outside Tools section (ingredients)', () => {
const md = [
'## Ingredients',
'- eggs 3',
].join('\n');
const html = render(md);
const scalables = getScalableAttrs(html);
const countScalable = scalables.find(s => s.type === 'count');
expect(countScalable).toBeDefined();
expect(countScalable.base).toBe(3);
});
it('makes bare counts scalable in Steps section', () => {
const md = [
'## Steps',
'- use 3 tortillas',
].join('\n');
const html = render(md);
const scalables = getScalableAttrs(html);
const countScalable = scalables.find(s => s.type === 'count');
expect(countScalable).toBeDefined();
expect(countScalable.base).toBe(3);
});
// ─── Section Boundary Detection ───────────────────────────
it('exits tools section when next heading at same level appears', () => {
const md = [
'## Tools',
'- pots 2',
'## Steps',
'- use 3 eggs',
].join('\n');
const html = render(md);
// "pots 2" count should NOT be scalable (tools section)
// "3" in steps should be scalable
const scalables = getScalableAttrs(html);
const countScalable = scalables.find(s => s.type === 'count');
expect(countScalable).toBeDefined();
expect(countScalable.base).toBe(3);
});
it('handles Tools as h3 heading', () => {
const md = [
'### Tools',
'- pots 2',
].join('\n');
const html = render(md);
expect(html).toContain('class="measurement"');
expect(hasScalable(html)).toBe(false);
});
// ─── Full Recipe Simulation ───────────────────────────────
it('handles a full recipe with ingredients, tools, and steps', () => {
const md = [
'## Ingredients',
'- onion 1',
'- ground beef 2lbs',
'- cheddar shredded 2 cups',
'## Tools',
'- pan',
'- mixing bowl',
'- 9 x 13 inch glass baking dish 1',
'## Steps',
'- preheat oven to 350 °F',
'- bake for 30 minutes',
].join('\n');
const html = render(md);
const scalables = getScalableAttrs(html);
// Ingredients should have scalable counts and weights/volumes
const weightScalables = scalables.filter(s => s.type === 'weight');
const volumeScalables = scalables.filter(s => s.type === 'volume');
expect(weightScalables.length).toBeGreaterThan(0);
expect(volumeScalables.length).toBeGreaterThan(0);
// The "1" count after "baking dish" in tools should be scalable
// (it's alongside a dimension measurement)
const countScalables = scalables.filter(s => s.type === 'count');
const dishCount = countScalables.find(s => s.base === 1);
expect(dishCount).toBeDefined();
// The ingredient "onion 1" count should also be scalable
// Both should exist in countScalables
expect(countScalables.length).toBeGreaterThanOrEqual(1);
});
});