// Bundled (browser IIFE) by scripts/e2e-mobile-guard.mjs. Builds representative published documents // with the REAL exporter (exportBodyHtml + exportComposedPreviewHtml, the Preview/publish path) so the // real-browser suite measures exactly what customers get. HtmlBlock sanitising needs a DOM, which is // why this runs inside the browser rather than in node. // Uses only exports that exist before and after the mobile-guard change, so the SAME harness can record // a desktop baseline on the base commit (MOBILE_GUARD_RECORD). import { exportBodyHtml, exportComposedPreviewHtml } from '../src/utils/html-export'; import { componentResolver } from '../src/components/resolver'; type N = Record; const defaults = (type: string): Record => { const c = (componentResolver as any)[type]; return c?.craft?.props ? JSON.parse(JSON.stringify(c.craft.props)) : {}; }; const node = (type: string, props: Record, nodes: string[] = [], extra: N = {}) => ({ type: { resolvedName: type }, isCanvas: type === 'Container', props: { ...defaults(type), ...props }, displayName: type, custom: {}, hidden: false, nodes, linkedNodes: {}, ...extra, }); const LONG = 'Honest prices, straight answers and work that is finished when we say it will be, every time.'; function columns(nodes: N, id: string, split: string, n: number) { const linked: Record = {}; for (let i = 0; i < n; i++) { const c = `${id}-c${i}`; const t = `${id}-t${i}`; nodes[c] = node('Container', { tag: 'div', style: { padding: '8px' } }, [t], { parent: id }); nodes[t] = node('TextBlock', { text: `${LONG} (${id} col ${i + 1})`, style: { fontSize: '16px' } }, [], { parent: c }); linked[`col-${i}`] = c; } nodes[id] = node('ColumnLayout', { columns: n, gap: '16px', split }, [], { parent: 'ROOT', linkedNodes: linked }); } const NAV_LINKS = ['Home', 'About us', 'Services', 'Our work', 'Pricing', 'Testimonials', 'Blog', 'Contact us'].map((text, i, a) => ({ text, href: '#' + text.toLowerCase().replace(/\s/g, '-'), ...(i === a.length - 1 ? { isCta: true } : {}), })); /** showMobileMenu false = every Navbar saved before the hamburger existed (links must wrap); * true = the default for a newly dropped Navbar (hamburger). */ function header(showMobileMenu: boolean): string { const nodes: N = { nav: node('Navbar', { logoText: 'Acme Plumbing & Heating Ltd', links: NAV_LINKS, padding: '16px 24px', showMobileMenu }, [], { parent: 'ROOT' }) }; nodes.ROOT = node('Container', { tag: 'div', style: {} }, ['nav']); return JSON.stringify(nodes); } /** First-party components only: every one of them must fit every width. */ export function componentsState(): string { const nodes: N = {}; const kids: string[] = []; const add = (id: string, type: string, props: Record = {}) => { nodes[id] = node(type, props, [], { parent: 'ROOT' }); kids.push(id); }; add('hero', 'HeroSimple', { title: 'Plumbing and heating you can actually rely on', subtitle: LONG }); for (const [id, split, n] of [['c2', '50-50', 2], ['c3', '33-33-33', 3], ['c4', '25-25-25-25', 4], ['side', '30-70', 2]] as const) { columns(nodes, id, split, n); kids.push(id); } add('features', 'FeaturesGrid'); add('testimonials', 'Testimonials'); add('counter', 'NumberCounter'); add('gallery', 'Gallery'); add('pricing', 'PricingTable'); add('reviews', 'ReviewsStrip'); add('img', 'ImageBlock', { src: 'data:image/svg+xml,' + encodeURIComponent('') }); add('longword', 'TextBlock', { text: 'Supercalifragilisticexpialidociouslyunbreakablewordthatgoesonandonandonforeverandever and https://example.com/a/really/long/path/that/never/breaks/because/it/has/no/spaces/at/all?query=string&more=yes' }); nodes.ROOT = node('Container', { tag: 'div', style: {} }, kids); return JSON.stringify(nodes); } /** Customer raw HTML that fits a phone with the net: media shrink,
 scrolls on its own, long words wrap,
 *  a full-width table keeps spanning the screen. */
export const HTML_FITS: Record = {
  hbIframe: '',
  hbImg: '',
  hbTableFull: '
PlanPrice
Basic$5
', hbPre: '
const reallyLongLine = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
', hbUrl: '

See https://example.com/a/really/long/path/that/never/breaks/because/it/has/no/spaces/at/all/ok

', // Deliberate horizontal scroller: must STILL scroll (the net must not flatten or clip it). hbScrollTable: '
abc
', // Phone probes: none of these may be clipped, unstuck or merged by the net. hbSticky: '', hbDropdown: '
', }; /** Customer raw HTML wider than a phone by design (fixed widths / px grids): clipped as before, never a sideways page. */ export const HTML_WIDE: Record = { hbFixed: '
A fixed 1100px banner
', hbGridPx: '
One
Two
Three
', hbGridFr: '

Multi-Brand Portfolio

Hosting

StreamerTools

Consultancy

', hbStyleFlex: '
A
B
C
', hbMinWidth: '
min-width 900px content
', // Tables are deliberately left alone by the net (scrolling one would shrink full-width tables): clipped as before. hbTableBare: '' + Array.from({ length: 10 }, (_, i) => ``).join('') + '
Column_heading_${i}
', }; function htmlState(blocks: Record, probes: boolean): string { const nodes: N = {}; const kids: string[] = []; nodes.intro = node('TextBlock', { text: 'Before the custom HTML' }, [], { parent: 'ROOT' }); kids.push('intro'); for (const [id, code] of Object.entries(blocks)) { nodes[id] = node('HtmlBlock', { code }, [], { parent: 'ROOT' }); kids.push(id); } if (probes) { // A multi-root HtmlBlock that is a direct flex / grid item: its roots must stay separate items/cells, at every // width (and a boxed wrapper would also move the margin and inline probes at desktop). nodes.flexRow = node('Container', { tag: 'div', style: { display: 'flex', flexDirection: 'row', gap: '12px' } }, ['hbRoots', 'rowText'], { parent: 'ROOT' }); nodes.hbRoots = node('HtmlBlock', { code: '
root one
root two
' }, [], { parent: 'flexRow' }); nodes.rowText = node('TextBlock', { text: 'flex sibling' }, [], { parent: 'flexRow' }); kids.push('flexRow'); nodes.gridBox = node('Container', { tag: 'div', style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px' } }, ['hbCells'], { parent: 'ROOT' }); nodes.hbCells = node('HtmlBlock', { code: '
cell one
cell two
' }, [], { parent: 'gridBox' }); kids.push('gridBox'); nodes.marginText = node('TextBlock', { text: 'margin neighbour', style: { marginBottom: '30px' } }, [], { parent: 'ROOT' }); kids.push('marginText'); nodes.hbMargins = node('HtmlBlock', { code: '

Collapsing margins

' }, [], { parent: 'ROOT' }); kids.push('hbMargins'); nodes.inlineRun = node('Container', { tag: 'div', style: {} }, ['hbInline', 'inlineBtn'], { parent: 'ROOT' }); nodes.hbInline = node('HtmlBlock', { code: 'inline custom' }, [], { parent: 'inlineRun' }); nodes.inlineBtn = node('ButtonLink', { text: 'inline button' }, [], { parent: 'inlineRun' }); kids.push('inlineRun'); } nodes.outro = node('TextBlock', { text: 'After the custom HTML' }, [], { parent: 'ROOT' }); kids.push('outro'); nodes.ROOT = node('Container', { tag: 'div', style: { padding: '20px' } }, kids); return JSON.stringify(nodes); } function compose(state: string, title: string, showMobileMenu: boolean): string { const hdr = exportBodyHtml(header(showMobileMenu), 'header').html; const body = exportBodyHtml(state).html; return exportComposedPreviewHtml(state, hdr + body, { title, includeFonts: false }); } (window as any).wsbMobileGuardDocs = (): Record => ({ components: compose(componentsState(), 'Components', false), htmlblocks: compose(htmlState(HTML_FITS, true), 'Custom HTML', true), htmlwide: compose(htmlState(HTML_WIDE, false), 'Wide custom HTML', true), });