Import the 2026-10-09 source release, Fastify/SQLite backend, restored source assets, and analytics-enabled server build. Replace files from the previous release while retaining the existing repository history. Make builds portable across Windows and Linux, configure local startup, normalize Windows static asset paths, and fix test fixture cleanup and file URL handling. Exclude local environment settings, databases, dependencies, validation output, and TypeScript build cache from version control. Validation: npm ci, static and server builds, 106 arrival tests, and actual local HTTP checks passed. The full suite passed 467 of 470 tests; three tests still require QA fixtures omitted from the supplied release, documented in README-서버실행.md.
88 lines
5.9 KiB
JavaScript
88 lines
5.9 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { readFile } from 'node:fs/promises';
|
|
import { test } from 'node:test';
|
|
import ts from 'typescript';
|
|
const path = new URL('../src/ui/SceneHints.ts', import.meta.url);
|
|
const source = await readFile(path, 'utf8').catch(() => 'export class SceneHints { update() { return null; } interact() {} }');
|
|
const { SceneHints } = await import(`data:text/javascript;base64,${Buffer.from(ts.transpileModule(source,{compilerOptions:{target:ts.ScriptTarget.ES2020,module:ts.ModuleKind.ES2020}}).outputText).toString('base64')}`);
|
|
const context = { enabled: true, tiltActive: false, sliding: true, doorOpen: 0, reducedMotion: false };
|
|
test('idle starts at five seconds, fades .6s, holds 3s, fades .8s; next unseen hint waits 18s', () => {
|
|
const h = new SceneHints();
|
|
assert.equal(h.update(0,context),null);
|
|
assert.equal(h.update(4999,context),null);
|
|
assert.deepEqual(h.update(5000,context),{kind:'drag',phase:'in',text:'드래그해서 유리 너머를 둘러보세요'});
|
|
assert.equal(h.update(5600,context).phase,'hold');
|
|
assert.equal(h.update(8599,context).phase,'hold');
|
|
assert.equal(h.update(8600,context).phase,'out');
|
|
assert.equal(h.update(9400,context),null);
|
|
assert.equal(h.update(27399,context),null);
|
|
assert.equal(h.update(27400,context).kind,'door-open');
|
|
});
|
|
test('actual gesture hides immediately, learned gestures survive media/context resets', () => {
|
|
const h=new SceneHints();h.update(0,context);h.update(5000,context);
|
|
h.interact(5100,'drag');assert.equal(h.update(5100,context),null);
|
|
assert.equal(h.update(10099,context),null);assert.equal(h.update(10100,context).kind,'door-open');
|
|
h.interact(10200,'door-open');h.update(10300,{...context,enabled:false});
|
|
h.update(20000,context);assert.equal(h.update(25000,context),null);
|
|
const closed={...context,doorOpen:1};assert.equal(h.update(25001,closed),null);
|
|
assert.equal(h.update(30001,closed).kind,'door-close');
|
|
});
|
|
test('tilt is taught only after active samples, only one hint and current door direction', () => {
|
|
const h=new SceneHints();h.update(0,context);h.interact(1,'drag');
|
|
assert.equal(h.update(5001,{...context,sliding:false}),null);
|
|
assert.equal(h.update(6000,{...context,sliding:false,tiltActive:true}),null);
|
|
assert.equal(h.update(11000,{...context,sliding:false,tiltActive:true}).kind,'tilt');
|
|
assert.equal(h.update(11001,{...context,sliding:false,tiltActive:false}),null);
|
|
const d=new SceneHints();d.update(0,context);d.interact(1,'drag');
|
|
assert.equal(d.update(5001,context).kind,'door-open');
|
|
assert.equal(d.update(5100,{...context,doorOpen:1}),null);
|
|
assert.equal(d.update(10100,{...context,doorOpen:1}).kind,'door-close');
|
|
});
|
|
test('reduced motion has a static 3-second hint, blocks and generic gestures do not teach',()=>{
|
|
const h=new SceneHints(),c={...context,reducedMotion:true};h.update(0,c);
|
|
assert.equal(h.update(5000,c).phase,'hold');assert.equal(h.update(8000,c),null);
|
|
h.interact(8100);h.update(9000,{...c,enabled:false});h.update(10000,c);
|
|
assert.equal(h.update(14999,c),null);assert.ok(h.update(15000,c));
|
|
});
|
|
test('movement inventory stays unique with view controls in settings',async()=>{
|
|
const html=await readFile(new URL('../index.html',import.meta.url),'utf8');
|
|
const scene=html.slice(0,html.indexOf('<dialog id="settings-dialog"'));
|
|
for(const id of ['gyro','gyro-recenter','reset','camera-offset','scene-zoom-reset']) {
|
|
assert.match(html,new RegExp(`id="${id}"`));
|
|
assert.equal([...html.matchAll(new RegExp(`id="${id}"`,'g'))].length,1);
|
|
}
|
|
assert.doesNotMatch(scene,/id="motion-tools"/);assert.match(scene,/id="gyro-label"/);
|
|
assert.match(scene,/id="hint"[^>]*aria-hidden="true"/);
|
|
assert.doesNotMatch(scene,/id="door-help"/);
|
|
});
|
|
test('door keyboard limit no-ops do not teach an unperformed opening or closing',async()=>{
|
|
const {default:vm}=await import('node:vm');
|
|
const main=await readFile(new URL('../src/main.ts',import.meta.url),'utf8');
|
|
const listeners={};
|
|
for(const [initial,key] of [[0,'Home'],[0,'ArrowRight'],[1,'End'],[1,'ArrowLeft']]){
|
|
const h=new SceneHints();h.update(0,context);h.interact(0,'drag');
|
|
const doorMotion={target:initial};
|
|
vm.runInNewContext(ts.transpileModule(main.slice(main.indexOf("doorToggle.addEventListener('click'"),main.indexOf('// Intersect the held camera')),{compilerOptions:{target:ts.ScriptTarget.ES2020}}).outputText,{
|
|
doorToggle:{addEventListener:(type,fn)=>listeners[type]=fn},listenerAbort:{signal:null},slideEnabled:()=>true,pointer:{cancelGesture(){}},doorMotion,
|
|
hideHintAfterDrag:kind=>h.interact(0,kind),animateDoorOpen:value=>doorMotion.target=Math.max(0,Math.min(1,value)),
|
|
});
|
|
listeners.keydown({key,preventDefault(){}});
|
|
const c={...context,doorOpen:initial};
|
|
assert.equal(h.update(5000,c)?.kind,initial?'door-close':'door-open',`${key} at ${initial} does not learn a direction`);
|
|
}
|
|
});
|
|
test('tilt accessible name contains the displayed state and retains the action',async()=>{
|
|
const {default:vm}=await import('node:vm');
|
|
const main=await readFile(new URL('../src/main.ts',import.meta.url),'utf8');
|
|
let status;const attrs={},label={textContent:''};
|
|
const a=main.indexOf('const gyro = new GyroController'),b=main.indexOf('// One normalized',a);
|
|
vm.runInNewContext(ts.transpileModule(main.slice(a,b),{compilerOptions:{target:ts.ScriptTarget.ES2020}}).outputText,{
|
|
GyroController:class{constructor(_view,fn){status=fn;}},gyroLabel:label,gyroButton:{setAttribute:(key,value)=>attrs[key]=value},
|
|
gyroRecenter:{},gyroStatus:{},gyroNotice:{},mobileTilt:true,experience:{isBlocked:()=>false,isGuideOpen:()=>false,isPhotoAdjusting:()=>false,stopPhotoAdjust(){}},hideHintAfterDrag(){},
|
|
});
|
|
for(const state of ['off','permission','waiting','active','paused','unavailable']){
|
|
status(state,'test status');assert.ok(attrs['aria-label'].includes(label.textContent),state);
|
|
if(['waiting','active','paused'].includes(state))assert.match(attrs['aria-label'],/끄기/);
|
|
}
|
|
});
|