Download tests/interaction.test.mjs from fffiloni/SimReady-assets-explorer: direct link, hf CLI and curl.
- Browser
- Download file 8.17 kB
-
https://huggingface.co/spaces/fffiloni/SimReady-assets-explorer/resolve/main/tests/interaction.test.mjs
- Command line
-
hf download hf://spaces/fffiloni/SimReady-assets-explorer/tests/interaction.test.mjs
-
curl -L -o interaction.test.mjs https://huggingface.co/spaces/fffiloni/SimReady-assets-explorer/resolve/main/tests/interaction.test.mjs
8.17 kB
| // React interaction tests with a deliberately simulated 3D engine, not WebGL QA. | |
| import test,{after} from 'node:test'; | |
| import assert from 'node:assert/strict'; | |
| import {readFile,mkdir,rm} from 'node:fs/promises'; | |
| import {build} from 'esbuild'; | |
| import {JSDOM} from 'jsdom'; | |
| const dom=new JSDOM('<!doctype html><html><head></head><body><div id="root"></div></body></html>',{url:'https://example.test/',pretendToBeVisual:true}); | |
| for(const name of ['window','document','navigator','HTMLElement','SVGElement','localStorage','Event','MouseEvent','KeyboardEvent'])Object.defineProperty(globalThis,name,{value:dom.window[name],configurable:true}); | |
| Object.assign(globalThis,{innerWidth:1200,innerHeight:800,IS_REACT_ACT_ENVIRONMENT:true,matchMedia:()=>({matches:false}),requestAnimationFrame:fn=>setTimeout(()=>fn(performance.now()),8),cancelAnimationFrame:clearTimeout,ResizeObserver:class{constructor(fn){this.fn=fn;}observe(){this.fn();}disconnect(){}}}); | |
| Object.defineProperties(dom.window.HTMLElement.prototype,{clientWidth:{get(){return 1200}},clientHeight:{get(){return 800}}}); | |
| dom.window.HTMLElement.prototype.getBoundingClientRect=function(){return {x:0,y:0,left:0,top:0,right:1200,bottom:800,width:1200,height:800}}; | |
| dom.window.HTMLMediaElement.prototype.pause=function(){}; | |
| const {createElement,act}=await import('react'),{createRoot}=await import('react-dom/client'); | |
| const records=[];globalThis.__qaEngines=records; | |
| const cache=new URL('../.cache/interactions/',import.meta.url);await mkdir(cache,{recursive:true}); | |
| for(const component of ['ObjectViewer','App'])await build({entryPoints:[new URL(`../src/${component}.tsx`,import.meta.url).pathname],outfile:new URL(component+'.mjs',cache).pathname,bundle:true,platform:'node',format:'esm',jsx:'automatic',packages:'external',resolveExtensions:['.ts','.tsx','.js','.jsx','.json'],define:{'import.meta.env.BASE_URL':'"/"'},loader:{'.css':'empty'},logLevel:'silent',plugins:[{name:'simulated-engine',setup(b){b.onResolve({filter:/^\.\/(basket|Basket)$/},args=>({path:new URL('../src/'+(args.path==='./basket'?'basket.ts':'Basket.tsx'),import.meta.url).pathname}));b.onResolve({filter:/viewer-engine$/},()=>({path:'engine',namespace:'qa'}));b.onLoad({filter:/.*/,namespace:'qa'},()=>({contents:`export function createEngine(host,asset,status,ready,failure,motion){const r={asset,status,ready,failure,motion,calls:[],disposed:false};globalThis.__qaEngines.push(r);status('Loading 3D…');return {dispose(){r.disposed=true},setVisible(v){r.calls.push(['visible',v])},play(...args){r.calls.push(['play',...args])},setJoint(...args){r.calls.push(['joint',...args])},selectJoint(...args){r.calls.push(['select',...args])},setHighlight(...args){r.calls.push(['highlight',...args])}}}` }));}}]}); | |
| const {default:Viewer}=await import(new URL('ObjectViewer.mjs',cache)),{default:App}=await import(new URL('App.mjs',cache)); | |
| const catalogue=JSON.parse(await readFile(new URL('../public/catalogue.json',import.meta.url))); | |
| const baseAsset={...catalogue.assets[0],bytes:1200,video:null,moving:1}; | |
| const defaults={asset:baseAsset,x:600,y:400,width:1200,height:800,objectSize:328,scale:1,inBasket:false,onClose:()=>{},onToggleBasket:()=>{}}; | |
| let root=createRoot(document.getElementById('root')); | |
| const render=async(props={})=>act(async()=>root.render(createElement(Viewer,{...defaults,...props}))); | |
| const click=async label=>act(async()=>{const button=[...document.querySelectorAll('button')].find(b=>b.getAttribute('aria-label')===label||b.textContent.trim()===label);assert(button,`Missing ${label}`);button.dispatchEvent(new MouseEvent('click',{bubbles:true}));}); | |
| const until=async check=>{for(let i=0;i<80;i++){if(check())return;await act(async()=>{await new Promise(r=>setTimeout(r,10))});}assert(check(),'Timed out');}; | |
| const reset=async()=>{await act(async()=>root.unmount());root=createRoot(document.getElementById('root'));records.length=0;}; | |
| after(async()=>{await act(async()=>root.unmount());dom.window.close();await rm(cache,{recursive:true,force:true});}); | |
| test('selection shows feedback immediately and cancels abandoned loads before creating a renderer',async()=>{ | |
| await render({moving:true});assert(document.querySelector('.selection-indicator[data-state=loading]'));assert.equal(records.length,0); | |
| await render({moving:true,asset:{...baseAsset,id:'second'}});await new Promise(r=>setTimeout(r,25));assert.equal(records.length,0); | |
| await render({moving:false,asset:{...baseAsset,id:'second'}});await until(()=>records.length===1); | |
| await reset();assert.equal(records.length,0); | |
| }); | |
| test('failure, retry and movement controls follow the current engine; old callbacks cannot overwrite selection',async()=>{ | |
| await render();await until(()=>records.length===1);const first=records[0]; | |
| await act(async()=>first.failure(new Error('Network test failure')));assert.match(document.body.textContent,/3D unavailable/);assert(document.querySelector('.asset-image')); | |
| await click('Retry 3D preview');await until(()=>records.length===2);assert(first.disposed); | |
| const current=records[1];await act(async()=>{first.ready([]);current.ready([{id:'door',label:'door',type:'revolute',home:0,far:90},{id:'drawer',type:'prismatic',home:0,far:.5}]);}); | |
| assert.match(document.body.textContent,/3D ready/);assert.equal(document.querySelector('input[type=range]').getAttribute('aria-label'),'Door position'); | |
| await click('Play this movement');assert.deepEqual(current.calls.at(-1),['play',true,'door']); | |
| await click('Next movement');assert.deepEqual(current.calls.at(-1),['select','drawer']); | |
| await click('Reset this movement');assert.deepEqual(current.calls.at(-1),['joint','drawer',0]); | |
| await render({x:-400});assert(!document.querySelector('[aria-label="Object controls"]'));assert.equal(records.length,2);assert(current.calls.some(c=>c[0]==='visible'&&!c[1])); | |
| await render();assert(document.querySelector('[aria-label="Object controls"]'));assert.equal(records.length,2); | |
| await reset();assert(current.disposed); | |
| }); | |
| test('null model and explicit large-preview gate never allocate a renderer until requested',async()=>{ | |
| await render({asset:{...baseAsset,model:null}});await new Promise(r=>setTimeout(r,35));assert.equal(records.length,0);assert(!document.querySelector('.tiny-spinner')); | |
| await reset();await render({asset:{...baseAsset,bytes:80*1024*1024}});assert.equal(records.length,0); | |
| await act(async()=>document.querySelector('.selection-status button').click());await until(()=>records.length===1);await reset(); | |
| }); | |
| test('information, downloads and clipboard fallback work while the preview is unavailable',async()=>{ | |
| await render({asset:{...baseAsset,model:null}});await click('Object information');assert(document.getElementById('selection-details'));assert.match(document.body.textContent,/Source formatOpenUSD/); | |
| Object.defineProperty(navigator,'clipboard',{value:{writeText:async()=>{throw Error('Unavailable')}},configurable:true}); | |
| await click('Download object');await click('Copy download command');assert(document.querySelector('[role=alert]'));assert(document.querySelector('details[open] code'));await reset(); | |
| }); | |
| test('canvas selection keeps the current zoom, recenters, and cancels cleanly on close',async()=>{ | |
| globalThis.fetch=async url=>({ok:true,json:async()=>String(url).includes('preview-index')?{schema:1,assets:{}}:{meta:{...catalogue.meta,assetCount:3},assets:catalogue.assets.slice(0,3).map(a=>({...a,bytes:1000}))}}); | |
| await act(async()=>root.render(createElement(App)));await until(()=>document.querySelectorAll('.object').length===3); | |
| await click('Zoom in');await until(()=>document.querySelector('.zoom-value').textContent==='120%'); | |
| const contentSize=(parseFloat(document.querySelectorAll('.object')[1].style.width)-28)*1.2; | |
| await act(async()=>document.querySelectorAll('.object')[1].click());assert(document.querySelector('.selection-indicator')); | |
| await until(()=>records.length===1);assert.equal(document.querySelector('.zoom-value').textContent,'120%'); | |
| assert(Math.abs(parseFloat(document.querySelector('.object-stage').style.width)-contentSize)<1e-6); | |
| await click('Close object');assert(records[0].disposed);assert(!document.querySelector('.object-stage'));await reset(); | |
| }); | |