tricu

A lazy functional language based on Tree Calculus
Log | Files | Refs | README | LICENSE

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});}