const {chromium}=require('playwright'); const staircaseFits=require('./presentation-staircase.cjs'); const assert=require('node:assert/strict');const fs=require('node:fs'); (async()=>{ const base=process.argv[2]||'http://127.0.0.1:43853';const root=base+'/eldorado-v1/documentation/'; const out='.cache/homepage-embed';fs.mkdirSync(out,{recursive:true}); const browser=await chromium.launch({headless:true,executablePath:process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE,args:['--no-sandbox']});const results=[]; try{ for(const viewport of [{width:1440,height:900},{width:390,height:844},{width:320,height:720},{width:844,height:390}]){ const context=await browser.newContext({viewport,hasTouch:true,reducedMotion:'reduce'});const p=await context.newPage();const errors=[]; p.on('pageerror',e=>errors.push(e.message));p.on('response',r=>{if(r.status()>=400)errors.push(`${r.status()} ${r.url()}`)}); await p.goto(root,{waitUntil:'networkidle'});const section=p.locator('section[aria-labelledby="presentation-title"]');await section.scrollIntoViewIfNeeded(); const iframe=p.locator('iframe[title="Less Prompting, More Shipping — interactive presentation"]');const f=await iframe.elementHandle().then(e=>e.contentFrame());await f.waitForFunction(()=>window.Reveal?.isReady());await f.evaluate(()=>document.fonts.ready); assert.deepEqual(await p.locator('#start-title,#presentation-title').evaluateAll(es=>es.map(e=>{const s=getComputedStyle(e);return [s.fontFamily,s.fontSize,s.fontWeight,s.letterSpacing,s.lineHeight]})).then(a=>a[0]),await p.locator('#presentation-title').evaluate(e=>{const s=getComputedStyle(e);return [s.fontFamily,s.fontSize,s.fontWeight,s.letterSpacing,s.lineHeight]})); assert.equal(await f.locator('.toolbar').isVisible(),false);assert.equal(await f.evaluate(()=>Reveal.getConfig().hash),false);assert.equal(await f.evaluate(()=>Reveal.getConfig().transition),'none');assert.equal(await f.evaluate(()=>Reveal.getTotalSlides()),23); assert(await p.evaluate(()=>document.documentElement.scrollWidth<=innerWidth)); if(viewport.width<=390)assert(await iframe.evaluate(e=>e.clientHeight>e.clientWidth),'phone stage must recompose rather than shrink landscape text'); await p.getByRole('button',{name:'Fullscreen',exact:true}).focus();await p.keyboard.press('ArrowRight');assert.equal(await f.evaluate(()=>Reveal.getIndices().h),0); await f.locator('.controls .navigate-right').click();assert.equal(await f.evaluate(()=>Reveal.getIndices().h),1); const scroll=await p.evaluate(()=>scrollY);await p.keyboard.press('ArrowRight');assert.equal(await f.evaluate(()=>Reveal.getIndices().h),2);assert.equal(await p.evaluate(()=>scrollY),scroll);assert.equal(p.url(),root); await f.evaluate(()=>Reveal.slide(0)); const visited=[]; for(let i=0;i<23;i++){ assert.equal(await f.evaluate(()=>Reveal.getIndices().h),i);visited.push(await f.evaluate(()=>Reveal.getCurrentSlide().id)); const bad=await f.evaluate(()=>{const r=Reveal.getCurrentSlide().getBoundingClientRect();return [...Reveal.getCurrentSlide().querySelectorAll('svg,text,.journey,.learning-loop,.contribution-grid,h1,h2,p,li,table,.flow,.roles,.timeline')].filter(e=>!e.closest('.notes')&&e.getBoundingClientRect().width>0).filter(e=>{const b=e.getBoundingClientRect();return b.bottom>r.bottom+1||b.right>r.right+1||b.lefte.textContent)});assert.deepEqual(bad,[]); if(i===6)assert(await f.evaluate(staircaseFits),'staircase aligns each arrow with its intended steps'); if(i===4)assert(await f.evaluate(()=>[...document.querySelectorAll('#contributors .contribution-grid>div')].every(e=>{const a=e.querySelector('span').getBoundingClientRect(), arrow=e.querySelector('i').getBoundingClientRect(), b=e.querySelector('b').getBoundingClientRect();return a.bottom<=arrow.top+2&&arrow.bottom<=b.top+2&&Math.abs((a.left+a.right-b.left-b.right)/2)<2&&getComputedStyle(e.querySelector('i')).transform==='matrix(0, 1, -1, 0, 0, 0)'})),'contribution arrows must point down to their own phase'); await iframe.screenshot({path:`${out}/${viewport.width}-${i+1}.png`}); if(i<22)await p.keyboard.press('ArrowRight'); } assert.equal(new Set(visited).size,23);assert.equal(p.url(),root);assert.equal(new URL(f.url()).hash,''); await f.evaluate(()=>Reveal.slide(0));await section.screenshot({path:`${out}/${viewport.width}-light.png`}); await p.evaluate(()=>document.documentElement.setAttribute('data-theme','dark'));await section.screenshot({path:`${out}/${viewport.width}-dark.png`}); await p.getByRole('button',{name:'Fullscreen',exact:true}).click();await p.waitForFunction(()=>!!document.fullscreenElement);await f.waitForTimeout(100); await p.screenshot({path:`${out}/${viewport.width}-fullscreen.png`}); await p.getByRole('button',{name:'Exit fullscreen',exact:true}).click();await p.waitForFunction(()=>!document.fullscreenElement);await p.waitForFunction(()=>document.activeElement?.textContent==='Fullscreen'); // Native touch swipe inside the stage, not a programmatic Reveal.next(). await iframe.scrollIntoViewIfNeeded();const b=await iframe.boundingBox();const cdp=await context.newCDPSession(p); await cdp.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[{x:b.x+b.width*.8,y:b.y+b.height*.4}]}); await cdp.send('Input.dispatchTouchEvent',{type:'touchMove',touchPoints:[{x:b.x+b.width*.2,y:b.y+b.height*.4}]});await cdp.send('Input.dispatchTouchEvent',{type:'touchEnd',touchPoints:[]});assert.equal(await f.evaluate(()=>Reveal.getIndices().h),1); await p.emulateMedia({reducedMotion:'no-preference'});await f.waitForFunction(()=>Reveal.getConfig().transition==='fade');await p.emulateMedia({reducedMotion:'reduce'});await f.waitForFunction(()=>Reveal.getConfig().transition==='none'); await f.locator('.navigate-right').focus();await p.keyboard.press('f');await p.keyboard.press('s');assert.equal(context.pages().length,1);assert.equal(await p.evaluate(()=>!!document.fullscreenElement),false); if(viewport.width<=390){ await p.setViewportSize({width:844,height:390});await f.waitForFunction(()=>Reveal.getConfig().width===1200);assert.equal(await f.evaluate(()=>Reveal.getIndices().h),1); await p.setViewportSize(viewport);await f.waitForFunction(()=>Reveal.getConfig().width===600); } // Unsupported fullscreen is non-destructive and keeps a working standalone escape. await p.evaluate(()=>{window.savedFullscreen=Element.prototype.requestFullscreen;Element.prototype.requestFullscreen=undefined}); await p.getByRole('button',{name:'Fullscreen',exact:true}).click();await p.getByRole('status').filter({hasText:'Fullscreen is unavailable'}).waitFor();assert.equal(p.url(),root); await p.evaluate(()=>{Element.prototype.requestFullscreen=window.savedFullscreen}); // No keyboard trap: browser Tab can leave the frame for the standalone affordance. await f.locator('.navigate-right').focus();for(let i=0;i<12&&!(await p.getByRole('link',{name:/Open presentation/}).evaluate(e=>e===document.activeElement));i++)await p.keyboard.press('Tab'); assert(await p.getByRole('link',{name:/Open presentation/}).evaluate(e=>e===document.activeElement)); for(const href of await p.locator('.start-card').evaluateAll(es=>es.map(e=>e.href)))assert.equal((await p.request.get(href)).status(),200); await p.getByRole('link',{name:/Open presentation/}).click();await p.waitForFunction(()=>window.Reveal?.isReady());assert(!p.url().includes('embed'));assert(await p.locator('.toolbar').isVisible()); assert.deepEqual(errors,[]);results.push({viewport,visited,headingParity:true,keyboardIsolation:true,scrollIsolation:true,touch:true,fullscreen:true,tabExit:true,errors});await context.close(); } fs.writeFileSync(`${out}/results.json`,JSON.stringify(results,null,2));console.log('PASS: 92 inline slides, heading parity, URLs, focus/scroll isolation, touch, fullscreen, Tab exit, themes, reduced motion and standalone handoff'); }finally{await browser.close()} })().catch(e=>{console.error(e);process.exitCode=1});