bundle-offers-app/engine/tests/storefront.test.js
metatroncubeswdev d4d42b917b Proxy pages carry their own CSS/JS; align festival calendar; App Store screenshots
Bundle pages stay styled and functional when the app embed is off (e.g. on a draft
theme). Listing screenshots captured from the dev store and rendered at 1600x900.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:26:04 -04:00

49 lines
3.4 KiB
JavaScript

// Proxy page Liquid: merchant text can't inject Liquid/HTML; every bundle type renders; writes the output to
// $BO_LIQUID_OUT (if set) so it can be run through theme-check.
import { test, expect } from 'vitest';
import { mkdirSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { bundlePageLiquid, bundlesListLiquid, esc } from '../../app/lib/storefront.server.ts';
const base = { id: 'x', code: 'o1', handle: 'diwali-kit', oldHandles: [], name: 'Diwali {{ shop.name }} <b>Kit</b>', badge: 'Diwali {% raw %}', description: 'Line 1\nLine 2', i18n: { fr: { name: 'Coffret Diwali' } }, sequence: 10, active: true, published: true, countries: [],
discountType: 'percent', discountValue: 20, mixQty: 3, buyQty: 2, getQty: 1, getPercent: 100, startsAt: null, endsAt: new Date('2026-11-09T04:59:59Z'), maxPerOrder: 0,
showHome: true, showProduct: true, showCartHints: true, imageUrl: null, limitedByPlan: false, createdAt: new Date(), updatedAt: new Date(), shopId: 's',
lines: [{ productId: '1', variantId: '11', quantity: 1, handle: 'diya', title: 'Diya', imageUrl: 'https://cdn.shopify.com/a.jpg' }, { productId: '2', variantId: null, quantity: 2, handle: "camphor'}}", title: 'Camphor', imageUrl: null }],
scopes: [{ role: 'mix', kind: 'collection', refId: '50', handle: 'sweets' }, { role: 'mix', kind: 'variant', refId: '71', parentId: '7', handle: 'ladoo' },
{ role: 'buy', kind: 'collection', refId: '60', handle: 'garlands' }, { role: 'get', kind: 'product', refId: '8', handle: 'marigold' }] };
const shop = { currency: 'CAD', timezone: 'America/Toronto', branding: true };
test('escaping neutralises Liquid and HTML', () => {
expect(esc('{{ x }} {% y %} <a>')).not.toMatch(/[{}%<>]/);
});
test('bundle pages render for every type without merchant-controlled Liquid', () => {
const out = process.env.BO_LIQUID_OUT;
if (out) mkdirSync(join(out, 'snippets'), { recursive: true });
for (const type of ['fixed', 'mix', 'bxgy']) {
const liquid = bundlePageLiquid({ ...base, type }, shop, null);
expect(liquid).not.toContain('{{ shop.name');
expect(liquid).not.toContain('{% raw');
expect(liquid).not.toContain("camphor'}}");
if (type === 'fixed') expect(liquid).toContain("all_products['camphor']");
expect(liquid).toContain(type === 'fixed' ? 'Add bundle to cart' : 'Add to cart');
if (out) writeFileSync(join(out, 'snippets', `page-${type}.liquid`), liquid);
}
expect(bundlePageLiquid({ ...base, type: 'mix' }, shop, 'fr')).toContain('Coffret Diwali');
const list = bundlesListLiquid([{ ...base, type: 'fixed' }, { ...base, type: 'mix', discountType: 'fixed_price' }], shop, null);
expect(list).toContain('data-bo-money="20"');
if (out) writeFileSync(join(out, 'snippets', 'list.liquid'), list);
});
test('inlined storefront CSS/JS contain no Liquid delimiters (proxy pages render them as Liquid)', async () => {
// vitest stubs CSS imports, so check the source files; the production build inlines them via ?raw
const { readFileSync } = await import('node:fs');
for (const f of ['bundle-offers.css', 'bundle-offers.js']) {
const src = readFileSync(new URL(`../../extensions/bundle-offers-theme/assets/${f}`, import.meta.url), 'utf8');
expect(src.length).toBeGreaterThan(1000);
expect(src).not.toMatch(/\{\{|\{%/);
}
const { selfContained } = await import('../../app/lib/storefront.server.ts');
expect(selfContained('<p>x</p>')).toContain('window.BundleOffers');
});