62 lines
6.2 KiB
JavaScript
62 lines
6.2 KiB
JavaScript
|
|
'use strict';
|
||
|
|
const {spawn}=require('child_process');
|
||
|
|
const fs=require('fs');
|
||
|
|
const os=require('os');
|
||
|
|
const path=require('path');
|
||
|
|
const assert=require('assert/strict');
|
||
|
|
const {root,starterPuzzle,read,functionSource}=require('./helpers/app-source');
|
||
|
|
|
||
|
|
const app=read('app.js'),serverSource=read('server.js'),html=read('index.html'),css=read('style.css');
|
||
|
|
assert(html.includes('class="shared-indicator"')&&html.includes('<span>共有</span>'),'Compact shared indicator is missing');
|
||
|
|
assert(!html.includes('共有 · 接続中')&&!html.includes('共有成功')&&!html.includes('共有失敗'),'Variable shared-status labels remain in the visible UI');
|
||
|
|
assert(css.includes('.shared-indicator')&&css.includes('bottom:calc(16px + var(--safe-bottom))'),'Shared indicator is not fixed above FPS');
|
||
|
|
assert(!functionSource('setCloudStatus').includes('textContent'),'Shared indicator still changes visible text');
|
||
|
|
assert(functionSource('currentCloudPending').includes('stateIds:[...cloudJournalStateIds]'),'Unfinished states are omitted from cloud pending data');
|
||
|
|
assert(!functionSource('noteCloudRow').includes("solved!==true"),'Unfinished states are still removed from the shared outbox');
|
||
|
|
assert(functionSource('requestBoardClaim').indexOf("fetchJson('/api/realtime/claim'")<functionSource('requestBoardClaim').indexOf('requestBoardClaimThroughRealtime')&&functionSource('requestBoardClaimThroughRealtime').includes("type:'claim'"),'Board claim does not use the direct shared API with realtime fallback');
|
||
|
|
assert(functionSource('markPendingClaimPointerReleased').includes('pending.released=true')&&functionSource('bindBoard').includes('finishReleased'),'A drag released while claim approval is pending is discarded');
|
||
|
|
assert(!app.includes('BroadcastChannel')&&!app.includes('syncStorageKey')&&!app.includes('queueWorldSignal'),'Retired cross-tab board synchronization remains');
|
||
|
|
assert(serverSource.includes('const state=JSON.parse(JSON.stringify(incoming));state.solved=false'),'Server still discards unfinished board paths');
|
||
|
|
assert(serverSource.includes('unfinishedBoard')&&serverSource.includes('Board claim is required'),'Unfinished state writes are not protected by the active claim');
|
||
|
|
|
||
|
|
const port=25000+Math.floor(Math.random()*5000);
|
||
|
|
const dataDir=fs.mkdtempSync(path.join(os.tmpdir(),'linkfield-v4797-'));
|
||
|
|
const child=spawn(process.execPath,[path.join(root,'server.js')],{env:{...process.env,PORT:String(port),HOST:'127.0.0.1',LINK_FIELD_TEST_DATA_ROOT:dataDir},stdio:['ignore','pipe','pipe']});
|
||
|
|
let stderr='';child.stderr.on('data',chunk=>stderr+=chunk);
|
||
|
|
const base=`http://127.0.0.1:${port}`;
|
||
|
|
const sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms));
|
||
|
|
async function request(url,options={}){const response=await fetch(base+url,options),body=await response.json();return{response,body}}
|
||
|
|
function auth(session){return{authorization:`Bearer ${session.playerId}.${session.token}`,'content-type':'application/json'}}
|
||
|
|
function boardMeta(puzzle){return{id:'B0',x:0,y:0,chunks:[[0,0]],level:1,targetLevel:1,seed:1717,axis:'MIX',sealedSides:[],puzzle,rev:1,revAuthor:'client'}}
|
||
|
|
|
||
|
|
(async()=>{
|
||
|
|
for(let index=0;index<100;index++){try{if((await request('/api/cloud/status')).response.ok)break}catch(_){}await sleep(30)}
|
||
|
|
const alice=(await request('/api/cloud/session',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({name:'Alice'})})).body;
|
||
|
|
const bob=(await request('/api/cloud/session',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({name:'Bob'})})).body;
|
||
|
|
const puzzle=starterPuzzle(),meta=boardMeta(puzzle);
|
||
|
|
let result=await request('/api/cloud/push',{method:'POST',headers:auth(alice),body:JSON.stringify({baseRevision:0,global:{schema:31,worldGeneration:'linkfield-single-world-20260801',nextId:1},metas:[meta],states:[{id:'B0',value:{paths:[],specialProgress:{crossings:[]},solved:false}}],deleted:[]})});
|
||
|
|
assert.equal(result.response.status,200);assert.equal(result.body.revision,1);
|
||
|
|
|
||
|
|
const route=puzzle.solution[0],partial={paths:[{startGate:route.startGate,endGate:null,openGate:null,cells:route.cells.slice(0,3).map(cell=>[...cell])}],specialProgress:{crossings:[]},solved:false};
|
||
|
|
result=await request('/api/cloud/push',{method:'POST',headers:auth(bob),body:JSON.stringify({baseRevision:1,global:{nextId:1},metas:[],states:[{id:'B0',value:partial}],deleted:[]})});
|
||
|
|
assert.equal(result.response.status,423,'A player without the board claim changed unfinished progress');
|
||
|
|
|
||
|
|
const connected=await request('/api/realtime/connect',{method:'POST',headers:auth(alice),body:'{}'});
|
||
|
|
assert.equal(connected.response.status,200);
|
||
|
|
const ready=connected.body.messages.find(message=>message.type==='ready');assert(ready?.presenceId,'Polling realtime connection did not return a presence id');
|
||
|
|
const directClaim=await request('/api/realtime/claim',{method:'POST',headers:auth(alice),body:JSON.stringify({presenceId:ready.presenceId,boardId:'B0'})});
|
||
|
|
assert.equal(directClaim.response.status,200);assert.equal(directClaim.body.ok,true,'Direct claim API failed');
|
||
|
|
const requestId='v4797-claim';
|
||
|
|
const claimEnvelope=await request('/api/realtime/send',{method:'POST',headers:auth(alice),body:JSON.stringify({presenceId:ready.presenceId,message:{type:'claim',requestId,boardId:'B0'},afterSequence:connected.body.sequence||0})});
|
||
|
|
const claimResult=claimEnvelope.body.messages.find(message=>message.type==='claim-result'&&message.requestId===requestId);
|
||
|
|
assert.equal(claimResult?.ok,true,'Claim through the cursor/reaction polling transport failed');
|
||
|
|
|
||
|
|
result=await request('/api/cloud/push',{method:'POST',headers:auth(alice),body:JSON.stringify({baseRevision:1,global:{nextId:1},metas:[],states:[{id:'B0',value:partial}],deleted:[]})});
|
||
|
|
assert.equal(result.response.status,200);assert.equal(result.body.revision,2);
|
||
|
|
const pulled=await request('/api/cloud/pull?since=0',{headers:auth(bob)});
|
||
|
|
assert.equal(pulled.response.status,200);
|
||
|
|
assert.deepEqual(pulled.body.page.states.B0.paths[0].cells,partial.paths[0].cells,'Another player did not receive unfinished board progress');
|
||
|
|
assert.equal(pulled.body.page.states.B0.solved,false);
|
||
|
|
|
||
|
|
console.log('LinkField v48.0 shared board input and progress smoke test passed');
|
||
|
|
})().catch(error=>{console.error(error);if(stderr)console.error(stderr);process.exitCode=1}).finally(()=>{child.kill('SIGTERM');fs.rmSync(dataDir,{recursive:true,force:true})});
|