2026-11-01 14:58:39 -08:00
// 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 < string , any >;
const defaults = ( type : string ) : Record < string , unknown > => {
const c = ( componentResolver as any )[ type ];
return c ? . craft ? . props ? JSON . parse ( JSON . stringify ( c . craft . props )) : {};
};
const node = ( type : string , props : Record < string , unknown >, 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 < string , string > = {};
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 < string , unknown > = {}) => { 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 ( '<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="600"><rect width="1600" height="600" fill="#888"/></svg>' ) });
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 );
}
2026-11-01 15:16:53 -08:00
/** Customer raw HTML that fits a phone with the net: media shrink, <pre> scrolls on its own, long words wrap,
* a full-width table keeps spanning the screen. */
2026-11-01 15:09:22 -08:00
export const HTML_FITS : Record < string , string > = {
2026-11-01 14:58:39 -08:00
hbIframe : '<iframe width="800" height="450" src="about:blank" title="video"></iframe>' ,
hbImg : '<img src="data:image/gif;base64,R0lGODlhAQABAAAAACw=" width="1200" height="400" alt="">' ,
2026-11-01 15:16:53 -08:00
hbTableFull : '<table id="full-table" style="width:100%;border-collapse:collapse" border="1"><tr><th>Plan</th><th>Price</th></tr><tr><td>Basic</td><td>$5</td></tr></table>' ,
2026-11-01 14:58:39 -08:00
hbPre : '<pre>const reallyLongLine = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";</pre>' ,
hbUrl : '<p>See https://example.com/a/really/long/path/that/never/breaks/because/it/has/no/spaces/at/all/ok</p>' ,
// Deliberate horizontal scroller: must STILL scroll (the net must not flatten or clip it).
hbScrollTable : '<div id="intentional-scroll" style="overflow-x:auto"><table style="width:1000px"><tr><td>a</td><td>b</td><td>c</td></tr></table></div>' ,
2026-11-01 15:09:22 -08:00
// Phone probes: none of these may be clipped, unstuck or merged by the net.
hbSticky : '<div id="sticky-box" style="height:1200px;background:#f5f5f5"><div id="sticky-probe" style="position:sticky;top:0;background:#ffeb3b;padding:8px">Sticky bar</div></div>' ,
hbDropdown : '<div style="position:relative"><button type="button">Menu</button><ul id="dropdown-probe" style="position:absolute;top:100%;left:0;z-index:10;list-style:none;margin:0;padding:8px;background:#fff;box-shadow:0 4px 12px #0004"><li>One</li><li>Two</li><li>Three</li><li>Four</li><li id="dropdown-last">Last item</li></ul></div>' ,
2026-11-01 14:58:39 -08:00
};
2026-11-01 15:09:22 -08:00
/** 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 < string , string > = {
hbFixed : '<div style="width:1100px;background:#eee;padding:20px">A fixed 1100px banner</div>' ,
hbGridPx : '<div style="display:grid;grid-template-columns:repeat(3,300px);gap:20px"><div style="padding:16px;border:1px solid #ccc">One</div><div style="padding:16px;border:1px solid #ccc">Two</div><div style="padding:16px;border:1px solid #ccc">Three</div></div>' ,
hbGridFr : '<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:32px"><div style="padding:24px"><h3>Multi-Brand Portfolio</h3></div><div style="padding:24px"><h3>Hosting</h3></div><div style="padding:24px"><h3>StreamerTools</h3></div><div style="padding:24px"><h3>Consultancy</h3></div></div>' ,
hbStyleFlex : '<style>.cards{display:flex;gap:16px}.card{flex:0 0 360px;padding:16px;border:1px solid #ccc}</style><div class="cards"><div class="card">A</div><div class="card">B</div><div class="card">C</div></div>' ,
hbMinWidth : '<div style="min-width:900px">min-width 900px content</div>' ,
2026-11-01 15:16:53 -08:00
// Tables are deliberately left alone by the net (scrolling one would shrink full-width tables): clipped as before.
hbTableBare : '<table id="bare-table"><tr>' + Array . from ({ length : 10 }, ( _ , i ) => `<td>Column_heading_ ${ i } </td>` ). join ( '' ) + '</tr></table>' ,
2026-11-01 15:09:22 -08:00
};
function htmlState ( blocks : Record < string , string >, probes : boolean ) : string {
2026-11-01 14:58:39 -08:00
const nodes : N = {};
const kids : string [] = [];
nodes . intro = node ( 'TextBlock' , { text : 'Before the custom HTML' }, [], { parent : 'ROOT' }); kids . push ( 'intro' );
2026-11-01 15:09:22 -08:00
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 : '<div class="root-probe" style="flex:1;background:#eef">root one</div><div class="root-probe" style="flex:2;background:#fee">root two</div>' }, [], { 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 : '<div class="cell-probe" style="background:#efe">cell one</div><div class="cell-probe" style="background:#fef">cell two</div>' }, [], { 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 : '<h2 style="margin:40px 0">Collapsing margins</h2>' }, [], { parent : 'ROOT' }); kids . push ( 'hbMargins' );
nodes . inlineRun = node ( 'Container' , { tag : 'div' , style : {} }, [ 'hbInline' , 'inlineBtn' ], { parent : 'ROOT' });
nodes . hbInline = node ( 'HtmlBlock' , { code : '<span>inline custom</span>' }, [], { parent : 'inlineRun' });
nodes . inlineBtn = node ( 'ButtonLink' , { text : 'inline button' }, [], { parent : 'inlineRun' });
kids . push ( 'inlineRun' );
}
2026-11-01 14:58:39 -08:00
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 < string , string > => ({
components : compose ( componentsState (), 'Components' , false ),
2026-11-01 15:09:22 -08:00
htmlblocks : compose ( htmlState ( HTML_FITS , true ), 'Custom HTML' , true ),
htmlwide : compose ( htmlState ( HTML_WIDE , false ), 'Wide custom HTML' , true ),
2026-11-01 14:58:39 -08:00
});