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.
// 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();}
