import-folds.mjs (6973B)
1 // Cursor-driven import folding and inline module browsing; no runtime assets required. 2 // nix shell nixpkgs#nodejs nixpkgs#chromium -c node ext/demo-site/test/import-folds.mjs 3 import assert from 'node:assert/strict'; 4 import {createServer} from 'node:http'; 5 import {spawn} from 'node:child_process'; 6 import {mkdtemp,readFile,rm} from 'node:fs/promises'; 7 import {tmpdir} from 'node:os'; 8 import {join} from 'node:path'; 9 const dir=await mkdtemp(join(tmpdir(),'arboricx-import-folds-')); 10 let browser,socket,server; 11 try{ 12 const root=new URL('../',import.meta.url); 13 browser=spawn(process.argv[2]||'chromium',['--headless','--no-sandbox','--disable-dev-shm-usage','--remote-debugging-port=0',`--user-data-dir=${join(dir,'profile')}`,'about:blank'],{stdio:['ignore','ignore','pipe']}); 14 const endpoint=await new Promise((resolve,reject)=>{ 15 let log='';const timer=setTimeout(()=>reject(Error(log)),15000); 16 browser.on('error',reject); 17 browser.stderr.on('data',chunk=>{log+=chunk;const m=log.match(/DevTools listening on (ws:\/\/\S+)/);if(m){clearTimeout(timer);resolve(new URL(m[1]));}}); 18 }); 19 const tab=await(await fetch(`http://${endpoint.host}/json/new?about:blank`,{method:'PUT'})).json(); 20 socket=new WebSocket(tab.webSocketDebuggerUrl); 21 await new Promise((resolve,reject)=>{socket.onopen=resolve;socket.onerror=reject;}); 22 let id=0;const pending=new Map(); 23 socket.onmessage=({data})=>{const event=JSON.parse(data);if(event.id){const p=pending.get(event.id);pending.delete(event.id);event.error?p.reject(Error(JSON.stringify(event.error))):p.resolve(event.result);}}; 24 const send=(method,params={})=>new Promise((resolve,reject)=>{pending.set(++id,{resolve,reject});socket.send(JSON.stringify({id,method,params}));}); 25 const evaluate=async expression=>{const r=await send('Runtime.evaluate',{expression,returnByValue:true,awaitPromise:true});if(r.exceptionDetails)throw Error(JSON.stringify(r.exceptionDetails));return r.result.value;}; 26 27 server=createServer(async(req,res)=>{ 28 try { 29 if(req.url==='/'){res.setHeader('Content-Type','text/html');res.end('<link rel="stylesheet" href="/style.css"><div id="editor" style="height:500px;position:relative"></div>');return;} 30 res.setHeader('Content-Type',req.url.endsWith('.js')?'text/javascript':'text/css'); 31 res.end(await readFile(new URL('.'+req.url,root))); 32 }catch{res.statusCode=404;res.end();} 33 }); 34 await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve)); 35 await send('Page.navigate',{url:`http://127.0.0.1:${server.address().port}/`}); 36 for(let i=0;!await evaluate("!!document.getElementById('editor')");i++){assert(i<200);await new Promise(r=>setTimeout(r,25));} 37 const result=await evaluate(`(async()=>{ 38 const {SourceEditor}=await import('/dist/source-editor.js'); 39 const {moduleInspector}=await import('/dist/module-inspector.js'); 40 const editor=new SourceEditor(document.getElementById('editor'));globalThis.editor=editor; 41 const hash='a'.repeat(64),other='b'.repeat(64); 42 const source='!import '+hash+' as A\\n!import '+other+' as B\\nanswer = 42'; 43 editor.replaceSource(source); 44 moduleInspector(editor,()=>[{manifest:hash,label:'Local test',names:['one','two']}]); 45 const check=(ok,message)=>{if(!ok)throw Error(message);}; 46 const full=()=>editor.dom.querySelectorAll('.import-hash-full'); 47 const folded=()=>editor.dom.querySelectorAll('.import-hash'); 48 check(full().length===1&&folded().length===1,'Initial cursor line expands'); 49 editor.select(source.length);check(full().length===0,'Leaving folds all imports'); 50 folded()[0].click();check(full().length===1,'Folded click selects line'); 51 check(editor.view.state.selection.main.head===8+64,'Folded click selects end'); 52 check(editor.dom.querySelector('.module-card').hidden,'Folded click does not open card'); 53 full()[0].click();await Promise.resolve(); 54 check(!editor.dom.querySelector('.module-card').hidden,'Expanded click opens card'); 55 check(editor.dom.querySelectorAll('.module-exports button').length===2,'Exact module exports'); 56 const search=editor.dom.querySelector('.module-card input');search.value='two';search.dispatchEvent(new Event('input')); 57 check(editor.dom.querySelectorAll('.module-exports button').length===1,'Shared export filtering'); 58 check(full().length===1,'Card focus preserves expansion'); 59 editor.select(0,source.indexOf('!import '+other));await Promise.resolve(); 60 check(full().length===1&&full()[0].textContent===other,'Selection head alone expands'); 61 check(editor.dom.querySelector('.module-card').hidden,'Moving cursor dismisses card'); 62 check(editor.getSource()===source&&!editor.undo(),'Presentation preserves document/history'); 63 return 'cursor-line folding and module card passed'; 64 })()`); 65 // Exercise native hit testing as well as programmatic/keyboard cursor changes. 66 await evaluate('editor.select(editor.getSource().length)'); 67 const click=async selector=>{ 68 const point=await evaluate(`(()=>{const r=document.querySelector(${JSON.stringify(selector)}).getBoundingClientRect();return {x:r.left+r.width/2,y:r.top+r.height/2};})()`); 69 await send('Input.dispatchMouseEvent',{type:'mousePressed',...point,button:'left',clickCount:1}); 70 await send('Input.dispatchMouseEvent',{type:'mouseReleased',...point,button:'left',clickCount:1}); 71 }; 72 await click('.import-hash'); 73 assert.equal(await evaluate("document.querySelectorAll('.import-hash-full').length"),1); 74 assert.equal(await evaluate("document.querySelector('.module-card').hidden"),true); 75 await click('.import-hash-full'); 76 assert.equal(await evaluate("document.querySelector('.module-card').hidden"),false); 77 assert.equal(await evaluate("document.querySelectorAll('.module-exports button').length"),2); 78 // Export labels must fit their buttons at both desktop and narrow widths. 79 for(const width of [1440,390]){ 80 await send('Emulation.setDeviceMetricsOverride',{width,height:900,deviceScaleFactor:1,mobile:false}); 81 const layout=await evaluate(`(async()=>{ 82 const {renderLibraryExports}=await import('/dist/library-browser.js'); 83 const list=document.querySelector('.module-exports'); 84 renderLibraryExports(list,['false_', 'resultMatch', 'mapMaybe', 'iterateUntil', 'extraordinarilyLongExportName'.repeat(5)],'',()=>{}); 85 await new Promise(resolve=>requestAnimationFrame(resolve)); 86 const buttons=[...list.querySelectorAll('button')]; 87 return buttons.every(button=>{ 88 const box=button.getBoundingClientRect(),range=document.createRange();range.selectNodeContents(button); 89 const text=range.getBoundingClientRect(); 90 return text.left>=box.left&&text.right<=box.right&&text.top>=box.top&&text.bottom<=box.bottom&&button.scrollWidth<=button.clientWidth; 91 })&&list.scrollWidth<=list.clientWidth; 92 })()`); 93 assert.equal(layout,true,`Export labels fit without overlap at ${width}px`); 94 } 95 console.log(result+'; trusted mouse clicks and export layout passed'); 96 }finally{socket?.close();browser?.kill();server?.close();await rm(dir,{recursive:true,force:true,maxRetries:10,retryDelay:100});}