definition-names-probe.mjs (5930B)
1 // Compiled-definition inspection through the shipped editor controls. 2 import {el,input,change,status,wait,inspect,card,result,pause,sourceEditor} from './source-probe-helpers.mjs'; 3 const assert=(ok,message)=>{if(!ok)throw Error(message);}; 4 export async function run(control,backend='haskell-wasm'){ 5 await status('Ready'); 6 document.querySelector('[data-compiler-example=identity]').click(); 7 const long='aVeryLongDefinitionName'+ 'Extended'.repeat(9); 8 const source=`-- λ🙂 positions stay native UTF-16 offsets 9 !export answer 10 answer = 42 11 identity x = x 12 ${long} = 9 13 text = "fake = 1" 14 outer x = let 15 local = x 16 in local 17 select [] = 0 18 select xs = 1 19 -- comment = 1`; 20 const editor=sourceEditor,wrapper=editor.dom; 21 input('source-editor',source);await status('Compiled');editor.view.scrollDOM.scrollTop=0; 22 await pause(0); 23 const target=name=>wrapper.querySelector(`[data-definition="${name}"]`); 24 assert(!wrapper.querySelector('.definition-gutter'),'No gutter'); 25 assert(parseFloat(getComputedStyle(wrapper).paddingLeft)<=20,'No reserved horizontal inspection column'); 26 const names=[...new Set([...wrapper.querySelectorAll('[data-definition]')].map(button=>button.dataset.definition))]; 27 assert(JSON.stringify(names)===JSON.stringify(['answer','identity',long,'text','outer','select']),'Only unique top-level declaration heads are interactive'); 28 assert(!target('local')&&!target('x')&&!target('fake')&&!target('comment'),'No local variables, references, comments or string lookalikes'); 29 assert(!target('answer').hidden&&target('answer').getAttribute('aria-disabled')==='false'&&target('answer').tabIndex===0,'Compiled names are keyboard accessible'); 30 assert(await inspect('answer')==='42','Name opens the actual retained definition'); 31 assert(target('answer').getAttribute('aria-expanded')==='true','Open name highlighted'); 32 const close=card().querySelector('[aria-label="Close definition inspection"]'); 33 const bounds=card().getBoundingClientRect(),buttonBounds=close.getBoundingClientRect(); 34 assert(Math.abs(bounds.right-buttonBounds.right-13)<=2&&Math.abs(buttonBounds.top-bounds.top-9)<=2,'Close is in the top-right corner'); 35 assert(buttonBounds.width>=44&&buttonBounds.height>=44,'Close has a comfortable touch target'); 36 card().querySelector('.edit-definition-name').click(); 37 const offset=source.indexOf('answer ='); 38 assert(card().hidden&&document.activeElement===editor.view.contentDOM&&editor.getSelection().from===offset&&editor.getSelection().to===offset+6,'Edit name selects exactly the declaration, not its export or references'); 39 assert(editor.getSource()===source,'Opening/closing and Edit name do not mutate source'); 40 // Selection, hold and drag checks complement trusted mouse/touch/keyboard input. 41 const button=target('answer'); 42 const down=()=>button.dispatchEvent(new PointerEvent('pointerdown',{clientX:10,clientY:10,pointerId:1,bubbles:true})); 43 const click=()=>button.dispatchEvent(new MouseEvent('click',{detail:1,bubbles:true})); 44 down();await pause(550);click();assert(card().hidden,'Holding is not a tap'); 45 down();button.dispatchEvent(new PointerEvent('pointermove',{clientX:30,clientY:10,pointerId:1,bubbles:true}));click();assert(card().hidden,'Dragging is not a tap'); 46 down();const menu=new MouseEvent('contextmenu',{bubbles:true,cancelable:true});button.dispatchEvent(menu);click();assert(!menu.defaultPrevented&&card().hidden,'Native context menu is not intercepted'); 47 down();document.getSelection().selectAllChildren(button);click();assert(card().hidden,'Selecting the identifier does not inspect');document.getSelection().removeAllRanges(); 48 wrapper.dispatchEvent(new CompositionEvent('compositionstart')); 49 assert(getComputedStyle(target('answer')).pointerEvents==='none','IME disables name pointer activation'); 50 wrapper.dispatchEvent(new CompositionEvent('compositionend')); 51 input('source-editor',source.replaceAll('answer','renamed'));await pause(0); 52 assert([...wrapper.querySelectorAll('[data-definition]')].every(button=>button.getAttribute('aria-disabled')==='true'||button.hidden),'Dirty names lose clickability immediately'); 53 await status('Compiled');assert(await inspect('renamed')==='42','Renamed definition resolves from the new compiled revision'); 54 input('source-editor','!export renamed\nrenamed = missing');await status('Compilation failed'); 55 assert([...wrapper.querySelectorAll('[data-definition]')].every(button=>button.getAttribute('aria-disabled')==='true'&&button.hidden),'Failed compilation leaves no clickable underline'); 56 input('source-editor',source);await status('Compiled');editor.view.scrollDOM.scrollTop=0;await pause(0); 57 // Wrapped declaration fragments retain their inline text hit areas. 58 const span=target(long);span.scrollIntoView({block:'center',behavior:'instant'});await new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(r)));const boxes=[...span.getClientRects()]; 59 assert(boxes.length>0&&span.tabIndex===0,'Wrapped inline name has one keyboard stop'); 60 for(const box of boxes){ 61 const hit=document.elementFromPoint(box.left+Math.min(3,box.width/2),box.top+box.height/2); 62 assert(hit&&span.contains(hit),'Every visible name fragment has its own native inline hit area: '+JSON.stringify({box,hit:hit?.outerHTML.slice(0,180),window:innerHeight})); 63 } 64 assert(!wrapper.querySelector('.definition-targets'),'No manual overlay rectangles'); 65 assert(await inspect(long)==='9','Wrapped private definition is inspectable'); 66 card().querySelector('[aria-label="Close definition inspection"]').click(); 67 assert(document.activeElement===target(long),'Closing returns focus to the name'); 68 editor.view.scrollDOM.scrollTop=0; 69 return {userAgent:navigator.userAgent,checks:['underlined definition names without a gutter; exact wrapped hit areas; unique heads only; retained/private inspection; Edit name and focus; dirty/error invalidation; native context menus; hold/drag/selection suppression; IME; one keyboard stop per name']}; 70 }