diff options
Diffstat (limited to 'scripts/theme-studio/browser-gates.js')
| -rw-r--r-- | scripts/theme-studio/browser-gates.js | 175 |
1 files changed, 162 insertions, 13 deletions
diff --git a/scripts/theme-studio/browser-gates.js b/scripts/theme-studio/browser-gates.js index 3b909c424..fcdfaff00 100644 --- a/scripts/theme-studio/browser-gates.js +++ b/scripts/theme-studio/browser-gates.js @@ -221,6 +221,21 @@ if(location.hash==='#mocktest')gate('mocktest',A=>withSavedState(['UIMAP','PKGMA pickEnum(pkgWeight(),'heavy'); A(PKGMAP[app][face].weight==='heavy'&&PKGMAP[app][face].source==='user','pkg weight dropdown writes the model and marks the face edited'); })); +// Cursor-row gate (open with #cursorrowtest): the cursor face honors only fg +// (the glyph on it) and bg (the cursor color); weight/slant/underline/strike and +// box are no-ops, so the row mutes them to a dash while non-cursor rows keep them. +if(location.hash==='#cursorrowtest')gate('cursorrowtest',A=>{ + buildUITable(); + const rows=[...document.querySelectorAll('#uibody tr')]; + const cur=rows.find(r=>r.dataset.face==='cursor'); + A(!!cur,'cursor row present'); + A(!!cur.cells[2].querySelector('.cdd'),'cursor keeps the fg swatch'); + A(!!cur.cells[3].querySelector('.cdd'),'cursor keeps the bg swatch'); + A(!cur.cells[4].querySelector('.enumdd')&&cur.cells[4].textContent.includes('—'),'cursor mutes the style controls'); + A(cur.cells[5].textContent.includes('—'),'cursor mutes the box control'); + const ml=rows.find(r=>r.dataset.face==='mode-line'); + A(!!ml.cells[4].querySelector('.enumdd'),'non-cursor rows keep the style controls'); +}); // Palette-generator gate (open with #generatortest): previewing is non-mutating, // clicking a generated tile loads the existing selector, adding creates a normal // singleton base column, and appending a preview column commits all span members @@ -810,6 +825,115 @@ if(location.hash==='#gnustest')gate('gnustest',A=>{ assertPreviewFaces(A, renderGnusPreview(), APPS['gnus']&&APPS['gnus'].faces, 20, 'gnus', ['gnus-header-name','gnus-header-from','gnus-header-subject','gnus-cite-1','gnus-cite-attribution','gnus-signature','gnus-button','gnus-emphasis-highlight-words']); }); +// nerd-icons legend gate (open with #nerdiconstest): nerd-icons is a bespoke +// filetype-legend app; every glyph span is a real nerd-icons face, the dir row +// models nerd-icons-yellow, and recoloring a face repaints every row mapped to it. +if(location.hash==='#nerdiconstest')gate('nerdiconstest',A=>{ + A(!!APPS['nerd-icons'],'nerd-icons is a registered app'); + A(APPS['nerd-icons']&&APPS['nerd-icons'].preview==='nerdicons','nerd-icons uses the nerdicons preview renderer'); + A(!!PACKAGE_PREVIEWS['nerdicons'],'nerdicons renderer registered'); + const legend=(APPS['nerd-icons']&&APPS['nerd-icons'].legend)||[]; + A(Array.isArray(legend)&&legend.length>=10,'legend has the curated rows ('+legend.length+')'); + const dir=legend.find(r=>r.key==='dir'); + A(dir&&dir.face==='nerd-icons-yellow','dir row models nerd-icons-yellow'); + // Gallery: the full colored catalog as a grid — one row per color face, rows + // ordered by hue so families cluster, each color's distinct icons deduped. + const gallery=(APPS['nerd-icons']&&APPS['nerd-icons'].gallery)||[]; + A(Array.isArray(gallery)&&gallery.length>=30,'gallery has the color groups ('+gallery.length+')'); + const hues=gallery.map(g=>g.hue); + A(hues.every((hu,i)=>i===0||hues[i-1]<=hu),'gallery rows ordered by hue (families cluster)'); + A(gallery.every(g=>typeof g.face==='string'&&g.face.indexOf('nerd-icons-')===0&&typeof g.hue==='number'&&Array.isArray(g.glyphs)&&g.glyphs.length>0),'every gallery group is a real nerd-icons face with a hue and glyphs'); + A(gallery.every(g=>g.glyphs.every(e=>e.glyph&&e.name)),'every gallery glyph carries glyph and icon name'); + A(gallery.every(g=>new Set(g.glyphs.map(e=>e.name)).size===g.glyphs.length),'icons are deduplicated within each color row'); + if(PACKAGE_PREVIEWS['nerdicons']&&APPS['nerd-icons']){ + // assertPreviewFaces over the grid — every data-face, across the ~314 deduped + // glyph cells and the per-row swatches, is a real nerd-icons face with a valid owner. + assertPreviewFaces(A, renderNerdIconsPreview(), APPS['nerd-icons'].faces, 10, 'nerd-icons', + ['nerd-icons-purple','nerd-icons-yellow','nerd-icons-blue','nerd-icons-dblue']); + // Recoloring a face repaints every element in its row (the swatch + each glyph + // cell), since os reads the live registry. + withSavedState(['PKGMAP'],()=>{ + const target='nerd-icons-purple',gGroup=gallery.find(g=>g.face===target); + const expected=gGroup?1+gGroup.glyphs.length:0; + A(!!gGroup,'gallery has a '+target+' row'); + PKGMAP['nerd-icons']=PKGMAP['nerd-icons']||{}; + PKGMAP['nerd-icons'][target]={fg:'#abcdef',bg:null,weight:null,slant:null,inherit:null,height:1,source:'user'}; + const box=document.createElement('div');box.innerHTML=renderNerdIconsPreview(); + const els=[...box.querySelectorAll('[data-face="'+target+'"]')]; + A(els.length===expected,'every '+target+' element rendered, swatch+glyphs ('+els.length+'/'+expected+')'); + A(els.length>0&&els.every(e=>/#abcdef/i.test(e.getAttribute('style')||'')),'recolor repaints every element in the row'); + }); + // Export/import round-trip over an assigned nerd-icons color; the separate + // nerd-icons-completion app (dir-face) is untouched by the nerd-icons pane. + const m=seedPkgmap(); + m['nerd-icons']['nerd-icons-blue']={fg:'#123456',bg:null,weight:null,slant:null,inherit:null,height:1,source:'user'}; + const exp=packagesForExport(m); + A(exp['nerd-icons']&&exp['nerd-icons']['nerd-icons-blue']&&exp['nerd-icons']['nerd-icons-blue'].fg==='#123456','assigned nerd-icons color exports'); + const round=seedPkgmap();mergePackagesInto(round,exp); + A(round['nerd-icons']&&round['nerd-icons']['nerd-icons-blue'].fg==='#123456','nerd-icons color re-imports to the same state'); + A(!(exp['nerd-icons']&&('nerd-icons-completion-dir-face' in exp['nerd-icons'])),'dir-face stays out of the nerd-icons app'); + } + }); +// Preview-pane dropdown gate (open with #previewpanetest): the preview label is a +// "preview:" dropdown. A single-pane app shows its name disabled; nerd-icons is +// multi-pane (one pane per font size in pt), enabled, and selecting a size renders +// the grid at it. Locate is unaffected — the flash targets whatever pane is rendered. +if(location.hash==='#previewpanetest')gate('previewpanetest',A=>{ + const np=previewPanes('nerd-icons'); + A(np.length===NERD_ICON_SIZES_PT.length&&np.length>1,'nerd-icons is multi-pane, one per size ('+np.length+')'); + A(np.every(p=>typeof p.size==='number'&&/ pt$/.test(p.label)),'each nerd-icons pane carries a pt size and a label'); + A(NERD_ICON_SIZES_PT[defaultPaneIdx('nerd-icons')]===NERD_ICON_DEFAULT_PT,'nerd-icons defaults to '+NERD_ICON_DEFAULT_PT+' pt'); + const single=Object.keys(APPS).find(k=>k!=='nerd-icons'); + A(previewPanes(single).length===1,'a non-nerd-icons app has a single pane ('+single+')'); + // size drives the rendered glyph font-size; no arg defaults to 14 pt + const small=renderNerdIconsPreview(10),big=renderNerdIconsPreview(24); + A(/font-size:10pt/.test(small)&&!/font-size:24pt/.test(small),'10 pt pane renders glyphs at 10pt'); + A(/font-size:24pt/.test(big)&&!/font-size:10pt/.test(big),'24 pt pane renders glyphs at 24pt'); + A(/font-size:14pt/.test(renderNerdIconsPreview()),'default (no-arg) pane renders glyphs at 14 pt'); + // gallery-absent fallback: the dropdown must not promise sizes it can't render — + // with no gallery, one pane only and the grid falls back to the generic preview. + const savedG=APPS['nerd-icons'].gallery;delete APPS['nerd-icons'].gallery; + A(previewPanes('nerd-icons').length===1,'no gallery -> single pane (dropdown disabled)'); + A(!/ni-gallery/.test(renderNerdIconsPreview()),'no gallery -> grid falls back to the generic preview'); + APPS['nerd-icons'].gallery=savedG; + // DOM wiring: dropdown enabled+populated on nerd-icons, disabled on a single-pane app + const vs=document.getElementById('viewsel'),saved=vs&&vs.value; + if(vs){ + vs.value='nerd-icons'; + if(curApp()==='nerd-icons'){ + PREV_PANE['nerd-icons']=99; // a stale, out-of-range selection + buildPkgPreview(); + const sel=document.getElementById('pkgprevsel'); + A(+sel.value===defaultPaneIdx('nerd-icons'),'a stale pane index resets to the default'); + A(!sel.disabled&&sel.options.length===NERD_ICON_SIZES_PT.length,'nerd-icons: dropdown enabled with one option per size'); + // Left/Right arrows step the size, clamped at the ends. + PREV_PANE['nerd-icons']=0;buildPkgPreview(); + document.getElementById('pkgprevsel').dispatchEvent(new KeyboardEvent('keydown',{key:'ArrowRight',bubbles:true})); + A(PREV_PANE['nerd-icons']===1,'ArrowRight steps to the next size'); + document.getElementById('pkgprevsel').dispatchEvent(new KeyboardEvent('keydown',{key:'ArrowLeft',bubbles:true})); + document.getElementById('pkgprevsel').dispatchEvent(new KeyboardEvent('keydown',{key:'ArrowLeft',bubbles:true})); + A(PREV_PANE['nerd-icons']===0,'ArrowLeft steps back and clamps at the first size'); + // The visible ‹ › buttons step the size too, and clamp. + PREV_PANE['nerd-icons']=0;buildPkgPreview(); + document.getElementById('pkgprevnext').click(); + A(PREV_PANE['nerd-icons']===1,'the > button steps to the next size'); + document.getElementById('pkgprevprev').click(); + document.getElementById('pkgprevprev').click(); + A(PREV_PANE['nerd-icons']===0,'the < button steps back and clamps at the first size'); + A(!document.getElementById('pkgprevprev').disabled&&!document.getElementById('pkgprevnext').disabled,'the nav buttons are enabled when multi-pane'); + // The glyph actually computes to the selected point size (pt -> px): 24 pt = 32 px. + PREV_PANE['nerd-icons']=NERD_ICON_SIZES_PT.indexOf(24);buildPkgPreview(); + const gw=document.querySelector('#pkgpreview .ni-cell > span'); + const gpx=gw?parseFloat(getComputedStyle(gw).fontSize):0; + A(Math.abs(gpx-32)<2,'24 pt glyph computes to ~32 px, so the point size renders to size ('+gpx+' px)'); + PREV_PANE['nerd-icons']=defaultPaneIdx('nerd-icons'); + vs.value=single;buildPkgPreview(); + A(sel.disabled&&sel.options.length===1,'single-pane app: dropdown disabled with one option'); + A(document.getElementById('pkgprevprev').disabled&&document.getElementById('pkgprevnext').disabled,'single-pane app: the nav buttons are disabled too'); + } + vs.value=saved;buildPkgPreview(); + } +}); // picker-distinct gate (open with #pickertest): the color picker panel must stand // out from the page background. It carries a highlighted gold accent border, and its // background is meaningfully lighter than the body so the two are easy to tell apart. @@ -828,7 +952,7 @@ if(location.hash==='#pickertest')gate('pickertest',A=>{ // four radio buttons (none / line / pressed / raised); the color swatch shows // only while a box style is active. if(location.hash==='#boxtest')gate('boxtest',A=>{ - LOCKED.clear();const f=UI_FACES[0][0];const saveBox=UIMAP[f].box; + LOCKED.clear();const f=UI_FACES.map(x=>x[0]).find(x=>x!=='cursor');const saveBox=UIMAP[f].box; // cursor has no box control by design UIMAP[f].box=null;buildUITable(); const cell=document.querySelector('#uibody tr[data-face="'+f+'"]').cells[5]; A(!!cell.querySelector('.boxcluster'),'box-cluster-present'); @@ -847,7 +971,7 @@ if(location.hash==='#boxtest')gate('boxtest',A=>{ // Style-cluster gate (open with #styletest): the style cell holds a weight // selector, a slant selector, and box-like underline and strike controls. if(location.hash==='#styletest')gate('styletest',A=>{ - buildUITable();const f=UI_FACES[0][0]; + buildUITable();const f=UI_FACES.map(x=>x[0]).find(x=>x!=='cursor'); // cursor row has no style cluster by design const cell=document.querySelector('#uibody tr[data-face="'+f+'"]').cells[4]; const cluster=cell.querySelector('.stylecluster'); A(!!cluster,'style-cluster-present'); @@ -1208,17 +1332,7 @@ if(location.hash==='#locatehovertest')gate('locatehovertest',A=>withSavedState([ rebuildLocateRegistry(); const cb=document.createElement('div');cb.innerHTML=os(app,face,'x'); A(/cleared, rendering as default/.test(cb.querySelector('[data-face]').getAttribute('title')),'cleared face title carries the cleared-rendering note'); - // info line on hover - PKGMAP[app][face]={fg:'#abcdef',bg:null,inherit:null,source:'user'}; - buildPkgPreview(); - const p=document.getElementById('pkgpreview'),lbl=document.getElementById('pkgprevlabel'),base=lbl.textContent; - rebuildLocateRegistry(); - p.innerHTML=os(app,face,'hover me'); - p.querySelector('[data-owner-app]').dispatchEvent(new MouseEvent('mouseover',{bubbles:true})); - A(lbl.textContent===locateInfoLine(locateFaceMeta(app,face,LOCATE_REG)),'hover updates the info line to section > face — value: '+lbl.textContent); - A(/ > .* — /.test(lbl.textContent),'info line uses the section > face — value shape'); - p.dispatchEvent(new MouseEvent('mouseleave')); - A(lbl.textContent===base,'leaving the preview restores the base label: '+lbl.textContent); + // Wayfinding is the per-span hover title (above); there is no separate info line. })); // Click + cursor gate (open with #locateclicktest): an on-pane element carries the // locate-onpane class (pointer cursor) and clicking flashes its assignment row via @@ -1255,3 +1369,38 @@ if(location.hash==='#locateclicktest')gate('locateclicktest',A=>withSavedState([ mspan.dispatchEvent(new MouseEvent('click',{bubbles:true})); A(urow()&&urow().classList.contains('flash'),'a UI mock span still flashes its row through the unified dispatcher');} })); +// Embedded-font gate (open with #fonttest): the nerd-icons legend, dashboard +// navigator, and package previews render their glyphs in a real nerd font +// instead of tofu. Verifies (1) the ThemeStudioNerd @font-face is registered, +// (2) previewLines actually APPLIES that family — the div is parsed into the DOM +// and getComputedStyle must resolve to ThemeStudioNerd (a double-quoted family in +// the inline style attribute silently drops it, so a plain string match would +// false-pass), and (3) the embedded woff2 loads AND covers the glyph codepoints +// the previews use — both a BMP glyph (U+F121) and a supplementary-plane Material +// Design glyph (U+F0474), the range most likely missing from a partial font. +// Async: it awaits the font load, then appends the verdict (the runner's +// virtual-time budget covers it). +if(location.hash==='#fonttest'){ + const fam='ThemeStudioNerd',notes=[]; + const bmp='',supp='\u{f0474}'; + const finish=()=>{const v='FONTTEST '+(notes.length?'FAIL':'PASS');document.title=v; + const d=document.createElement('div');d.id='fonttest'; + d.textContent=v+(notes.length?' fails='+notes.join(','):'');document.body.appendChild(d);}; + const registered=[...document.fonts].some(f=>f.family.replace(/["']/g,'')===fam); + if(!registered)notes.push('no-fontface'); + // Parse the actual previewLines output into the DOM and read the resolved + // font-family off the rendered element — not a substring of the HTML string. A + // double-quoted family name inside the inline style="..." attribute terminates + // the attribute early and silently drops the font-family, so a string match + // passes while the rendered font is empty; computed style catches that. + const probe=document.createElement('div');probe.innerHTML=previewLines(['x']); + document.body.appendChild(probe); + const inner=probe.firstElementChild; + const ff=inner?getComputedStyle(inner).fontFamily:''; + if(ff.indexOf(fam)<0)notes.push('previews-font-not-applied('+(ff||'empty')+')'); + Promise.all([document.fonts.load('16px "'+fam+'"',bmp),document.fonts.load('16px "'+fam+'"',supp)]).then(()=>{ + if(!document.fonts.check('16px "'+fam+'"',bmp))notes.push('bmp-glyph-missing'); + if(!document.fonts.check('16px "'+fam+'"',supp))notes.push('supp-glyph-missing'); + probe.remove();finish(); + }).catch(e=>{probe.remove();notes.push('load-error:'+(e&&e.message||e));finish();}); +} |
