Back to GRAV / RIFT
SOURCE / PINNED RELEASE

Made of little things.

GRAV / RIFT

Release
288aa261ce07…
Author-recorded commit
c549b5a4e17e…
License
LICENSE
Author’s source reference
nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-8a7361981e8

Archive hash verified: e52a01aff70810b3…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

tests/hosted.mjs
// Run against the printed `soyli dev` URL: pnpm test:hosted -- http://127.0.0.1:PORT/
import assert from 'node:assert/strict';
import {fireKeyboard,fireTouch} from './input.mjs';
import {bossPilot} from './boss-pilot.mjs';
import {createRequire} from 'node:module';
import {realpathSync,mkdirSync} from 'node:fs';
const require=createRequire(realpathSync(new URL('../node_modules/@napplet/conformance-cli/package.json',import.meta.url)));
const {chromium}=require('playwright');
const url=process.argv.slice(2).find(arg=>/^https?:\/\//.test(arg));if(!url)throw new Error('Pass the running soyli dev URL.');
mkdirSync('.napplet-space/promotion',{recursive:true});
const browser=await chromium.launch({headless:true});const errors=[];const report=[];
async function open(options={}){
 const context=await browser.newContext({viewport:{width:1200,height:750},...options});
 const page=await context.newPage();page.on('pageerror',e=>errors.push(e.message));
 await page.goto(url);await page.locator('iframe').waitFor();const frame=page.frames().find(f=>f!==page.mainFrame());
 await frame.locator('#deploy').waitFor();
 await frame.locator('#site').selectOption('orbit');
 // Test-only manager override: remove host chrome and the host's 300px iframe minimum.
 // Game code retains its real sandbox and all host services. Assert inner dimensions below.
 await page.addStyleTag({content:'#stage{position:fixed!important;inset:0!important;z-index:100!important}#stage iframe{width:100vw!important;height:100dvh!important;min-height:0!important}body{overflow:hidden!important}'});
 return {context,page,frame};
}
async function run(page,ms){await page.clock.runFor(ms);}
try{
 const {page,frame,context}=await open();await page.clock.install({time:new Date('2026-09-27T12:00:00Z')});await page.clock.pauseAt(new Date('2026-09-27T12:01:00Z'));
 for(const [w,h] of [[200,160],[320,560],[900,600],[1440,900]]){
  await page.setViewportSize({width:w,height:h});await run(page,100);
  const info=await frame.evaluate(()=>({w:innerWidth,h:innerHeight,overflow:document.documentElement.scrollWidth>innerWidth,notice:getComputedStyle(document.querySelector('.minimum-notice')).display,buttons:[...document.querySelectorAll('#deploy,#site,.mode,#upgrade')].map(n=>({id:n.id||n.textContent.trim(),r:n.getBoundingClientRect().toJSON()}))}));
  assert.equal(info.w,w);assert.equal(info.h,h);assert.equal(info.overflow,false);
  if(w===200)assert.equal(info.notice,'flex');else{assert.equal(info.notice,'none');for(const b of info.buttons){assert.ok(b.r.x>=0&&b.r.right<=w+1,`${w}: ${b.id} clipped horizontally`);assert.ok(b.r.bottom<=h+1,`${w}: ${b.id} clipped vertically`);}}
  await page.screenshot({path:`.napplet-space/promotion/layout-${w}x${h}.png`});report.push(`Exact frame ${w}×${h}: passed`);
 }
 await page.setViewportSize({width:1200,height:750});await run(page,100);
 await frame.locator('#practice').click();
 for(const [x,y,direction] of [[1050,350,'right'],[50,350,'left'],[240,140,'up']]){
  await page.mouse.move(x,y);await page.keyboard.down('Space');await run(page,500);
  const d=await frame.locator('#game').evaluate(n=>({...n.dataset}));
  const dx=Number(d.fieldX)-Number(d.shipX),dy=Number(d.fieldY)-Number(d.shipY);
  assert.ok(Math.abs(Math.hypot(dx,dy)-165)<.01,'vortex stays projected ahead of the ship');
  assert.ok(direction==='right'?dx>100:direction==='left'?dx<-100:dy<-90,`mouse steers vortex ${direction}`);
  await page.screenshot({path:`.napplet-space/promotion/vortex-${direction}.png`});
 }
 await page.mouse.move(900,220);await page.keyboard.down('KeyD');await run(page,1600);await page.keyboard.up('KeyD');await run(page,200);
 const aimed=await frame.locator('#game').evaluate(n=>({data:{...n.dataset},rect:n.getBoundingClientRect().toJSON()}));
 const d=aimed.data,r=aimed.rect,scale=Number(d.viewScale);
 const targetX=(900-r.left)/scale+Number(d.camera)-Number(d.shipX),targetY=(220-r.top)/scale+Number(d.cameraY)-Number(d.shipY);
 const fieldX=Number(d.fieldX)-Number(d.shipX),fieldY=Number(d.fieldY)-Number(d.shipY);
 assert.ok(Number(d.camera)>0,'test must actually scroll the camera');
 assert.ok(Math.abs(fieldX*targetY-fieldY*targetX)/(165*Math.hypot(targetX,targetY))<.0001,'stationary mouse aim remains aligned after camera movement');
 await fireKeyboard(page);await page.keyboard.up('Space');await run(page,50);await frame.locator('#pause').click();await frame.locator('#retry').click();
 report.push('Mouse-directed vortex: right, left, upward and stationary cursor during camera scrolling tested');
 await page.keyboard.down('Space');await run(page,1500);
 assert.ok(Number(await frame.locator('#game').getAttribute('data-payload'))>0);
 await page.screenshot({path:'.napplet-space/promotion/orbit.png'});
 await fireKeyboard(page);await page.keyboard.up('Space');await run(page,150);assert.equal(await frame.locator('#game').getAttribute('data-shots'),'1');
 await page.keyboard.down('KeyD');
 for(let i=0;i<110&&Number(await frame.locator('#game').getAttribute('data-ship-x'))<3800;i++)await run(page,100);
 assert.ok(Number(await frame.locator('#game').getAttribute('data-ship-x'))>=3800,'fly through the expanded stage');
 await page.keyboard.up('KeyD');await run(page,500);
 await page.keyboard.down('KeyS');await run(page,450);await page.keyboard.up('KeyS');await run(page,500);
 const trackBoss=bossPilot(page,frame);
 await page.keyboard.press('Digit3');await page.keyboard.down('Space');await trackBoss(8000);
 console.log('BOSS STATE',await frame.locator('#mission').innerText(),await frame.locator('#game').evaluate(n=>({...n.dataset})));
 await page.screenshot({path:'.napplet-space/promotion/boss-tether.png'});
 await frame.locator('#pause').click();assert.equal(await frame.locator('#app').getAttribute('data-phase'),'paused');
 await frame.getByRole('button',{name:'Save record',exact:true}).click();
 await page.clock.runFor(100);await page.waitForTimeout(150);
 console.log('STORAGE',await frame.locator('#storageStatus').textContent());
 assert.match(await frame.locator('#storageStatus').textContent(),/record saved/);
 await frame.locator('#resume').click();
 const before=await frame.locator('#runTime').textContent();
 for(const theme of ['dark','light']){
  await page.evaluate(theme=>{document.documentElement.dataset.theme=theme;},theme);
  await page.emulateMedia({colorScheme:theme});await run(page,250);
  assert.equal(await frame.locator('#app').evaluate(n=>getComputedStyle(n).backgroundColor),'rgb(10, 17, 19)');
 }
 report.push(`Keyboard grab / launch, tether, upgrade, pause / resume, user-triggered host storage: passed; timer ${before}`);
 await fireKeyboard(page);await page.keyboard.up('Space');await page.keyboard.down('Space');await run(page,150);await fireKeyboard(page);await page.keyboard.up('Space');await run(page,1400);
 console.log('AFTER VOLLEY',await frame.locator('#game').evaluate(n=>({...n.dataset})),await frame.locator('#salvage').textContent());
 if(!await frame.locator('#upgrade').isDisabled()){await frame.locator('#upgrade').click();assert.equal(await frame.locator('#fieldLevel').textContent(),'MK. II');}

 await page.keyboard.press('Digit2');
 for(let volley=0;volley<12 && await frame.locator('#app').getAttribute('data-phase')==='playing';volley++){
  await page.keyboard.down('Space');await trackBoss(3000);await fireKeyboard(page);await page.keyboard.up('Space');await trackBoss(1800);
 }
 await page.screenshot({path:'.napplet-space/promotion/combat-end.png'});
 console.log('FINAL COMBAT',await frame.locator('#app').getAttribute('data-phase'),await frame.locator('#game').getAttribute('data-core-hull'));
 assert.equal(await frame.locator('#app').getAttribute('data-phase'),'won','actual hosted run reaches victory through input alone');
 await page.screenshot({path:'.napplet-space/promotion/victory.png'});
 report.push('Hosted end-to-end victory using keyboard flight, tether extraction and thrown debris: passed');
 await context.close();
 for(const [w,h] of [[390,844],[360,640],[844,390]]){
  const {page,frame,context}=await open({viewport:{width:w,height:h},hasTouch:true,isMobile:true});await page.clock.install({time:new Date('2026-09-27T12:00:00Z')});await page.clock.pauseAt(new Date('2026-09-27T12:01:00Z'));await run(page,100);
  assert.deepEqual(await frame.evaluate(()=>[innerWidth,innerHeight]),[w,h]);
  await frame.locator('#practice').tap();
  const stick=await frame.locator('#joystick').boundingBox(),field=await frame.locator('#fieldButton').boundingBox();
  const cdp=await context.newCDPSession(page);
  const point=(id,x,y)=>({id,x,y,radiusX:4,radiusY:4,force:1});
  const a=point(1,stick.x+stick.width/2+24,stick.y+stick.height/2),b=point(2,field.x+field.width/2,field.y+field.height/2);
  await cdp.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[a,b]});await run(page,1800);
  assert.ok(Number(await frame.locator('#game').getAttribute('data-payload'))>0,`${w}: touch gathers`);
  await page.screenshot({path:`.napplet-space/promotion/touch-${w}x${h}.png`});
  await fireTouch(page,frame,cdp,[a,b]);await cdp.send('Input.dispatchTouchEvent',{type:'touchEnd',touchPoints:[]});await run(page,150);
  assert.equal(await frame.locator('#game').getAttribute('data-shots'),'1');
  await frame.locator('[data-mode="1"]').tap();assert.equal(await frame.locator('[data-mode="1"]').getAttribute('aria-pressed'),'true');
  const layout=await frame.evaluate(()=>({overflow:document.documentElement.scrollWidth>innerWidth,buttons:[...document.querySelectorAll('.mode,#upgrade,#joystick,#fieldButton,#pause')].map(n=>({name:n.id||n.textContent.trim(),r:n.getBoundingClientRect().toJSON()}))}));
  assert.equal(layout.overflow,false);for(const b of layout.buttons){assert.ok(b.r.x>=0&&b.r.right<=w+1&&b.r.y>=0&&b.r.bottom<=h+1,`${w}: ${b.name} clipped`);assert.ok(b.r.width>=44&&b.r.height>=44,`${w}: ${b.name} undersized`);}
  report.push(`Touch ${w}×${h}: simultaneous steering/gathering, release, mode switch passed`);
  if(w===844){
   // Fly down using the real touch stick; short frames must follow vertically.
   const down=point(3,stick.x+stick.width/2,stick.y+stick.height/2+32);
   await cdp.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[down]});await run(page,2200);
   await cdp.send('Input.dispatchTouchEvent',{type:'touchEnd',touchPoints:[]});await run(page,600);
   const low=await frame.locator('#game').evaluate(n=>({...n.dataset}));
   assert.ok(Number(low.shipY)>800&&Number(low.cameraY)>290,'landscape camera follows the full vertical flight space');
   await page.mouse.move(620,130);await run(page,100);
   const aimed=await frame.locator('#game').evaluate(n=>({d:{...n.dataset},r:n.getBoundingClientRect().toJSON()}));
   const {d,r}=aimed,scale=Number(d.viewScale);
   const tx=(620-r.left)/scale+Number(d.camera)-Number(d.shipX),ty=(130-r.top)/scale+Number(d.cameraY)-Number(d.shipY);
   const fx=Number(d.fieldX)-Number(d.shipX),fy=Number(d.fieldY)-Number(d.shipY);
   assert.ok(Math.abs(fx*ty-fy*tx)/(165*Math.hypot(tx,ty))<.0001,'pointer conversion includes vertical camera position');
   await page.screenshot({path:'.napplet-space/promotion/touch-landscape-low.png'});
   const shots=await frame.locator('#game').getAttribute('data-shots');
   await page.setViewportSize({width:390,height:844});await run(page,150);
   assert.equal(await frame.locator('#app').getAttribute('data-phase'),'playing');
   assert.equal(await frame.locator('#game').getAttribute('data-shots'),shots);
   assert.ok(Number(await frame.locator('#game').getAttribute('data-ship-y'))>800,'resize preserves position');
   report.push('Landscape vertical touch flight, pointer alignment after vertical scrolling, and live portrait resize: passed');
  }
  await context.close();
 }
 assert.deepEqual(errors,[]);console.log(report.join('\n'));console.log('HOSTED CHECKS PASSED');
}finally{await browser.close();}