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.
278 lines
29 KiB
JavaScript
278 lines
29 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { readFile } from 'node:fs/promises';
|
|
import { loadTypescript } from './support/load-typescript.mjs';
|
|
const html = await readFile(new URL('../index.html', import.meta.url),'utf8');
|
|
assert.match(html, /id="settings-dialog"/, 'all options must be in a dismissible dialog, never a persistent sidebar');
|
|
const themes=await loadTypescript(new URL('../src/ui/themes.ts',import.meta.url));
|
|
const {initExperienceUI}=await loadTypescript(new URL('../src/ui/ExperienceUI.ts',import.meta.url));
|
|
class Element {
|
|
dataset={};attributes={};listeners={};value='';hidden=false;open=false;inert=false;isConnected=true;
|
|
addEventListener(type,fn,{signal}={}){(this.listeners[type]??=[]).push(fn);signal?.addEventListener('abort',()=>this.listeners[type]=this.listeners[type].filter(f=>f!==fn));}
|
|
dispatch(type,extra={}){for(const fn of this.listeners[type]??[])fn({target:this,...extra});}
|
|
setAttribute(key,value){this.attributes[key]=value;}
|
|
focus(){this.owner.activeElement=this;}
|
|
showModal(){this.open=true;}
|
|
close(){this.open=false;this.dispatch('close');}
|
|
getBoundingClientRect(){return {left:20,right:300,top:20,bottom:500};}
|
|
}
|
|
function harness(value=null,{readError=false,writeError=false}={}){
|
|
const values=new Map(value===null?[]:[[themes.THEME_STORAGE_KEY,value]]);let writes=0;
|
|
const root=new Element();root.documentElement=new Element();root.activeElement=null;
|
|
const ids=['home-dialog','home-confirm','home-cancel','photo-adjust-open','photo-adjust-panel','photo-adjust-done','scene-menu','scene-menu-open','scene-menu-close','scene-guide-replay','scene-zoom-reset','comparison-exit','first-guide','export-open','export-dialog','credits-open','credits-close','credits-dialog','entry-notices','entry-pet-name','settings-pet-name','entry-name-notice','settings-name-notice','welcome-name','entry-photo-button','image','pet-stage','experience','welcome','flow-overlay','flow-title','flow-detail','flow-photo','journey','journey-motion','flow-actions','flow-retry','flow-flat','settings-dialog','settings-open','settings-close','entry-samples','sample-browser','sample-dialog','sample-close','sample-confirm','flow-samples','theme-select','canvas-wrap','scene-toolbar','scene-photo-change','sample-home','entry-name-step','entry-photo-step','entry-name-next','entry-name-skip','entry-name-edit'];
|
|
const els=Object.fromEntries(ids.map(id=>[id,new Element()]));Object.values(els).forEach(e=>e.owner=root);root.querySelector=s=>els[s.slice(1)]??null;
|
|
const choices=['dog5','dog1','dog2','dog3','dog4'].map(id=>{const e=new Element();e.owner=root;e.dataset.sampleChoice=id;return e;});root.querySelectorAll=()=>choices;
|
|
root.defaultView={get localStorage(){if(readError)throw new Error('blocked');return{getItem:key=>values.get(key)??null,setItem(key,value){if(writeError)throw new Error('full');values.set(key,value);writes++;}};}};
|
|
let homeCalls=0,arrivalEnds=0;const samples=[];const themesSeen=[];const abort=new AbortController();const ui=initExperienceUI(root,abort.signal,theme=>themesSeen.push(theme),()=>{arrivalEnds++;},sample=>samples.push(sample), undefined, ()=>homeCalls++);
|
|
return {root,els,choices,samples,ui,themesSeen,values,abort,get writes(){return writes;},get homeCalls(){return homeCalls;},get arrivalEnds(){return arrivalEnds;}};
|
|
}
|
|
for(const [value,expected] of [[null,'wood'],['wood','wood'],['booth','booth'],['invalid','wood'],['','wood']]){const h=harness(value);assert.equal(h.root.documentElement.dataset.theme,expected);assert.equal(h.els['theme-select'].value,expected);assert.equal(h.themesSeen.at(-1),expected);assert.equal(h.els['settings-dialog'].open,false);assert.equal(h.els.experience.dataset.flow,'entry');assert.equal(h.ui.isBlocked(),true);}
|
|
for(const errors of [{readError:true},{writeError:true},{}]){const h=harness(null,errors);h.els['theme-select'].value='booth';h.els['theme-select'].dispatch('change');assert.equal(h.root.documentElement.dataset.theme,'booth');assert.equal(h.themesSeen.at(-1),'booth');h.els['theme-select'].dispatch('change');assert.equal(h.themesSeen.length,2,'reselect causes no additional appearance work');}
|
|
const h=harness();h.els['theme-select'].value='booth';h.els['theme-select'].dispatch('change');assert.equal(h.values.get(themes.THEME_STORAGE_KEY),'booth');assert.equal(harness(h.values.get(themes.THEME_STORAGE_KEY)).els['theme-select'].value,'booth');
|
|
h.ui.setPhoto('blob:photo');h.ui.setState('loading');assert.equal(h.els['flow-photo'].src,'blob:photo');assert.equal(h.els.experience.dataset.flow,'loading');assert.equal(h.els['flow-retry'].hidden,true);assert.equal(h.ui.isBlocked(),true);
|
|
h.ui.setDetail('model download');assert.equal(h.els['flow-detail'].textContent,'model download');
|
|
h.ui.setState('error',true);assert.equal(h.els.experience.dataset.flow,'error');assert.equal(h.els['flow-retry'].hidden,false);assert.equal(h.els['flow-flat'].hidden,false);
|
|
h.ui.setState('ready');assert.equal(h.els['flow-overlay'].hidden,true);assert.equal(h.els.welcome.hidden,true);assert.equal(h.ui.isBlocked(),false);
|
|
h.ui.setRenderLost(true);assert.equal(h.ui.isBlocked(),true);assert.equal(h.els['canvas-wrap'].inert,true);
|
|
h.ui.setState('ready');assert.equal(h.ui.isBlocked(),true,'a late ready state cannot bypass render recovery');
|
|
h.ui.setRenderLost(false);assert.equal(h.ui.isBlocked(),false);assert.equal(h.els['canvas-wrap'].inert,false);
|
|
h.els['settings-open'].focus();h.els['settings-open'].dispatch('click');assert.equal(h.els['settings-dialog'].open,true);assert.equal(h.ui.isBlocked(),true);assert.equal(h.els['canvas-wrap'].inert,true);
|
|
h.els['settings-dialog'].dispatch('cancel',{preventDefault(){}});assert.equal(h.els['settings-dialog'].open,false);assert.equal(h.root.activeElement,h.els['scene-menu-open']);assert.equal(h.els['canvas-wrap'].inert,false);
|
|
h.root.activeElement=null;h.els['settings-open'].dispatch('click');h.els['settings-dialog'].dispatch('click',{clientX:10,clientY:10});assert.equal(h.els['settings-dialog'].open,false);assert.equal(h.root.activeElement,h.els['scene-menu-open'],'pointer browsers that do not focus buttons still restore to the opener');
|
|
h.els['entry-samples'].focus();h.els['entry-samples'].dispatch('click');assert.equal(h.els['sample-dialog'].open,true);assert.equal(h.els['settings-dialog'].open,false);h.els['sample-close'].dispatch('click');assert.equal(h.root.activeElement,h.els['entry-samples']);
|
|
h.els['settings-open'].dispatch('click');h.ui.setState('loading');assert.equal(h.els['settings-dialog'].open,false,'new media closes the options');assert.equal(h.ui.isBlocked(),true);
|
|
h.els['entry-samples'].dispatch('click');
|
|
assert.equal(h.choices[0].attributes['aria-pressed'],'true');
|
|
h.choices[2].dispatch('click');assert.equal(h.choices[2].attributes['aria-pressed'],'true');assert.equal(h.choices[0].attributes['aria-pressed'],'false');assert.equal(h.samples.length,0,'selection must never start the experience');
|
|
h.els['sample-confirm'].dispatch('click');assert.deepEqual(h.samples,['dog2']);assert.equal(h.els['sample-dialog'].open,false);
|
|
h.els['entry-samples'].dispatch('click');h.els['sample-confirm'].dispatch('click');assert.deepEqual(h.samples,['dog2','dog2'],'same photo can be reselected');
|
|
h.ui.setState('error');assert.equal(h.els['flow-samples'].hidden,false);h.els['flow-samples'].dispatch('click');assert.equal(h.els['sample-dialog'].open,true);h.els['sample-dialog'].dispatch('cancel',{preventDefault(){}});assert.equal(h.root.activeElement,h.els['flow-samples']);
|
|
h.els['flow-samples'].dispatch('click');h.ui.setRenderLost(true);assert.equal(h.els['sample-dialog'].open,false);h.els['flow-samples'].dispatch('click');assert.equal(h.els['sample-dialog'].open,false);h.ui.setRenderLost(false);
|
|
h.els['flow-samples'].dispatch('click');h.abort.abort();assert.equal(h.els['sample-dialog'].open,false);const count=h.themesSeen.length;h.els['theme-select'].value='wood';h.els['theme-select'].dispatch('change');assert.equal(h.themesSeen.length,count,'teardown removes theme handlers');
|
|
console.log('Experience UI: default/persisted/invalid/unavailable storage, no-op reselect, real-state flow, source preview, error recovery, modal dismiss/focus/inert, teardown passed (DOM boundary double, no visual QA)');
|
|
|
|
const named=harness();
|
|
const entryName=named.els['entry-pet-name'], settingsName=named.els['settings-pet-name'];
|
|
entryName.value='호롱';entryName.dispatch('input');
|
|
assert.equal(named.els['entry-photo-button'].textContent,'호롱이 사진 고르기','typing a name updates landing copy');
|
|
assert.equal(settingsName.value,'호롱','settings reflects the same app state');
|
|
assert.equal(named.els['welcome-name'].textContent,'호롱이가 왔어요');
|
|
named.ui.startJourney();assert.equal(named.els['flow-title'].textContent,'호롱이를 할머니 집으로 데려가는 중…');
|
|
settingsName.value='초코';settingsName.dispatch('input');
|
|
assert.equal(named.els['flow-title'].textContent,'초코를 할머니 집으로 데려가는 중…');
|
|
assert.equal(entryName.value,'초코');
|
|
settingsName.dispatch('compositionstart');settingsName.value='보';settingsName.dispatch('input',{isComposing:true});
|
|
assert.equal(named.els['entry-photo-button'].textContent,'초코 사진 고르기','IME intermediate input does not update state');
|
|
settingsName.value='보리';settingsName.dispatch('compositionend');
|
|
assert.equal(named.els['entry-photo-button'].textContent,'보리 사진 고르기');
|
|
settingsName.value=' <img onerror=x> ';settingsName.dispatch('change');
|
|
assert.equal(settingsName.value,'<img onerror=x>');assert.equal(named.els['entry-photo-button'].textContent,'<img onerror=x> 사진 고르기');
|
|
named.ui.setMediaOrigin('sample');named.ui.startJourney();
|
|
assert.equal(named.els['flow-title'].textContent,'누렁이를 할머니 집으로 데려가는 중…');
|
|
assert.equal(settingsName.value,'<img onerror=x>','sample never overwrites the saved name');
|
|
named.ui.setMediaOrigin('personal');named.ui.startJourney();
|
|
assert.equal(named.els['flow-title'].textContent,'<img onerror=x>를 할머니 집으로 데려가는 중…');
|
|
settingsName.value=' ';settingsName.dispatch('change');assert.equal(named.els['entry-photo-button'].textContent,'멍멍이 사진 고르기');
|
|
const blockedName=harness(null,{writeError:true});blockedName.els['entry-pet-name'].value='호롱';blockedName.els['entry-pet-name'].dispatch('input');
|
|
assert.equal(blockedName.els['entry-name-notice'].hidden,false);assert.equal(blockedName.els['settings-name-notice'].hidden,false);
|
|
assert.equal(blockedName.els['entry-photo-button'].textContent,'호롱이 사진 고르기');
|
|
console.log('Pet name UI: live shared copy, Korean IME, literal HTML-like names, sample origin, clearing, storage warning passed.');
|
|
const longName = harness(); longName.root.activeElement = longName.els['entry-pet-name'];
|
|
longName.els['entry-pet-name'].value = '가'.repeat(30); longName.els['entry-pet-name'].dispatch('input');
|
|
assert.equal(longName.els['entry-pet-name'].value, '가'.repeat(20), 'focused field enforces the advertised name length after completed input');
|
|
for (const [root, unavailable] of [[named, false], [blockedName, true]]) {
|
|
for (const prefix of ['entry', 'settings']) {
|
|
const description = root.els[`${prefix}-pet-name`].attributes['aria-describedby'];
|
|
assert.equal(description, `${prefix}-name-help${unavailable ? ` ${prefix}-name-notice` : ''}`, 'screen-reader descriptions include storage failure only when true');
|
|
}
|
|
}
|
|
|
|
const sampleHome = harness();
|
|
sampleHome.ui.setMediaOrigin('sample'); sampleHome.ui.setPhoto('blob:sample'); sampleHome.ui.setState('ready');
|
|
assert.equal(sampleHome.els['sample-home'].hidden, false);
|
|
assert.equal(sampleHome.els['scene-photo-change'].hidden, true);
|
|
assert.equal(sampleHome.els['settings-open'].hidden, true);
|
|
sampleHome.els['settings-open'].dispatch('click');
|
|
assert.equal(sampleHome.els['settings-dialog'].open, false, 'sample settings cannot be opened by stale clicks');
|
|
sampleHome.els['sample-home'].dispatch('click');sampleHome.els['home-confirm'].dispatch('click');
|
|
assert.equal(sampleHome.homeCalls, 1, 'home invokes media cancellation callback once');
|
|
assert.equal(sampleHome.els.experience.dataset.flow, 'entry');
|
|
assert.equal(sampleHome.els.welcome.hidden, false);
|
|
assert.equal(sampleHome.els['sample-home'].hidden, true);
|
|
assert.equal(sampleHome.els['settings-open'].hidden, true);assert.equal(sampleHome.els['credits-open'].hidden,false);
|
|
assert.equal(sampleHome.els['flow-photo'].src, '');
|
|
assert.equal(sampleHome.root.activeElement, sampleHome.els['entry-photo-button']);
|
|
sampleHome.ui.setMediaOrigin('personal'); sampleHome.ui.setState('ready');
|
|
assert.equal(sampleHome.els['scene-photo-change'].hidden, false);
|
|
assert.equal(sampleHome.els['sample-home'].hidden, false);
|
|
assert.match(html, /placeholder="예시 : 장준"/);
|
|
assert.doesNotMatch(html, /placeholder="예: 호롱"|창문 열기/);
|
|
console.log('Sample return: own-photo controls restored, sample settings gated, landing focus and placeholder verified.');
|
|
|
|
for (const state of ['loading', 'error']) {
|
|
const h = harness(); h.ui.setMediaOrigin('sample'); h.ui.setState(state);
|
|
assert.equal(h.els['sample-home'].hidden, false);
|
|
assert.equal(h.els['settings-open'].hidden, true);
|
|
assert.equal(h.els['scene-photo-change'].hidden, true);
|
|
h.els['sample-home'].dispatch('click');h.els['home-confirm'].dispatch('click');
|
|
assert.equal(h.homeCalls, 1);
|
|
assert.equal(h.els.experience.dataset.flow, 'entry');
|
|
}
|
|
const pasteGate=harness();
|
|
assert.equal(pasteGate.ui.canAcceptPhotoPaste(),false,'name step keeps focus on naming');pasteGate.els['entry-name-skip'].dispatch('click');assert.equal(pasteGate.ui.canAcceptPhotoPaste(),true,'photo step accepts paste');
|
|
pasteGate.ui.setMediaOrigin('sample');pasteGate.ui.setState('ready');
|
|
assert.equal(pasteGate.ui.canAcceptPhotoPaste(),false,'sample presentation cannot bypass hidden replace action');
|
|
pasteGate.els['sample-home'].dispatch('click');pasteGate.els['home-confirm'].dispatch('click');assert.equal(pasteGate.ui.canAcceptPhotoPaste(),true);
|
|
pasteGate.els['credits-open'].dispatch('click');assert.equal(pasteGate.ui.canAcceptPhotoPaste(),false);
|
|
pasteGate.els['credits-close'].dispatch('click');assert.equal(pasteGate.ui.canAcceptPhotoPaste(),true);
|
|
pasteGate.ui.setRenderLost(true);assert.equal(pasteGate.ui.canAcceptPhotoPaste(),false);
|
|
pasteGate.ui.setRenderLost(false);pasteGate.abort.abort();assert.equal(pasteGate.ui.canAcceptPhotoPaste(),false);
|
|
|
|
const step=harness();
|
|
assert.equal(step.els['entry-name-step'].hidden,false);
|
|
assert.equal(step.els['entry-photo-step'].hidden,true);assert.equal(step.els['entry-notices'].hidden,true);
|
|
step.els['entry-name-skip'].dispatch('click');
|
|
assert.equal(step.els['entry-name-step'].hidden,true);
|
|
assert.equal(step.els['entry-photo-step'].hidden,false);assert.equal(step.els['entry-notices'].hidden,false);
|
|
step.ui.setMediaOrigin('sample');step.ui.setState('ready');step.els['sample-home'].dispatch('click');step.els['home-confirm'].dispatch('click');
|
|
assert.equal(step.els['entry-photo-step'].hidden,false,'sample return skips already-completed name step');
|
|
step.els['entry-name-edit'].dispatch('click');assert.equal(step.els['entry-name-step'].hidden,false);
|
|
step.els['entry-pet-name'].value='장준';step.els['entry-pet-name'].dispatch('input');step.els['entry-name-next'].dispatch('click');
|
|
assert.equal(step.els['entry-photo-button'].textContent,'장준이 사진 고르기');
|
|
assert.equal(step.els['entry-photo-step'].hidden,false);assert.equal(step.els['entry-notices'].hidden,false);
|
|
|
|
const imeStep=harness();imeStep.els['entry-pet-name'].dispatch('compositionstart');imeStep.els['entry-pet-name'].value='누';
|
|
imeStep.els['entry-pet-name'].dispatch('keydown',{key:'Enter',isComposing:false,keyCode:229,preventDefault(){}});
|
|
assert.equal(imeStep.els['entry-name-step'].hidden,false,'IME commit Enter never advances or steals focus');
|
|
imeStep.els['entry-pet-name'].value='누렁';imeStep.els['entry-pet-name'].dispatch('compositionend');
|
|
imeStep.els['entry-pet-name'].dispatch('keydown',{key:'Enter',isComposing:false,keyCode:13,preventDefault(){}});
|
|
assert.equal(imeStep.els['entry-photo-step'].hidden,false);
|
|
|
|
const personalHome=harness();personalHome.els['entry-name-skip'].dispatch('click');personalHome.ui.setMediaOrigin('personal');personalHome.ui.setPhoto('blob:own');personalHome.ui.setState('ready');
|
|
assert.equal(personalHome.els['sample-home'].hidden,false,'personal scene also offers home');
|
|
personalHome.els['sample-home'].dispatch('click');personalHome.els['home-confirm'].dispatch('click');assert.equal(personalHome.homeCalls,1);assert.equal(personalHome.els.experience.dataset.flow,'entry');assert.equal(personalHome.els['entry-photo-step'].hidden,false);
|
|
const skipVisibility=harness();const nameField=skipVisibility.els['entry-pet-name'];
|
|
assert.equal(skipVisibility.els['entry-name-skip'].hidden,false);
|
|
nameField.value='장준';nameField.dispatch('input');assert.equal(skipVisibility.els['entry-name-skip'].hidden,true);
|
|
nameField.value=' ';nameField.dispatch('input');assert.equal(skipVisibility.els['entry-name-skip'].hidden,false);
|
|
nameField.dispatch('compositionstart');nameField.value='장';nameField.dispatch('input',{isComposing:true});assert.equal(skipVisibility.els['entry-name-skip'].hidden,true,'visible composing text hides skip without mutating stored name');
|
|
nameField.value='';nameField.dispatch('compositionend');assert.equal(skipVisibility.els['entry-name-skip'].hidden,false);
|
|
|
|
// Main entry has maker details instead of settings, without starting a photo flow.
|
|
const credits=harness();
|
|
assert.equal(credits.els['credits-open'].hidden,false);
|
|
assert.equal(credits.els['settings-open'].hidden,true);assert.equal(credits.els['scene-photo-change'].hidden,true);
|
|
credits.els['entry-name-skip'].dispatch('click');
|
|
credits.els['credits-open'].dispatch('click');
|
|
assert.equal(credits.els['credits-dialog'].open,true);
|
|
assert.equal(credits.ui.canAcceptPhotoPaste(),false);
|
|
credits.els['credits-dialog'].dispatch('cancel',{preventDefault(){}});
|
|
assert.equal(credits.els['credits-dialog'].open,false);
|
|
assert.equal(credits.root.activeElement,credits.els['credits-open']);
|
|
credits.els['credits-open'].dispatch('click');credits.ui.setState('loading');
|
|
assert.equal(credits.els['credits-dialog'].open,false);
|
|
assert.equal(credits.els['credits-open'].hidden,true);
|
|
credits.ui.setState('ready');assert.equal(credits.els['settings-open'].hidden,false);
|
|
credits.els['credits-open'].dispatch('click');assert.equal(credits.els['credits-dialog'].open,false);
|
|
credits.els['sample-home'].dispatch('click');credits.els['home-confirm'].dispatch('click');assert.equal(credits.els['credits-open'].hidden,false);
|
|
credits.els['credits-open'].dispatch('click');credits.abort.abort();assert.equal(credits.els['credits-dialog'].open,false);
|
|
assert.match(html, /id="scene-photo-change"[^>]*\bhidden\b/, 'entry markup must not reserve an invisible photo action before JS starts');
|
|
const creditsDismiss=harness();
|
|
creditsDismiss.els['credits-open'].dispatch('click');assert.equal(creditsDismiss.els['credits-dialog'].open,true,'maker works from name entry');
|
|
creditsDismiss.els['credits-open'].dispatch('click');assert.equal(creditsDismiss.els['credits-dialog'].open,true,'repeated click is safe');
|
|
creditsDismiss.els['credits-dialog'].dispatch('click',{clientX:100,clientY:100});assert.equal(creditsDismiss.els['credits-dialog'].open,true,'inside click remains open');
|
|
creditsDismiss.els['credits-dialog'].dispatch('click',{clientX:0,clientY:0});assert.equal(creditsDismiss.els['credits-dialog'].open,false,'backdrop closes');
|
|
creditsDismiss.els['credits-open'].dispatch('click');creditsDismiss.ui.setRenderLost(true);assert.equal(creditsDismiss.els['credits-dialog'].open,false);
|
|
creditsDismiss.els['credits-open'].dispatch('click');assert.equal(creditsDismiss.els['credits-dialog'].open,false,'render-loss guards reopen');
|
|
assert.match(html,/href="mailto:sunha321@gmail.com"/);assert.match(html,/<dd>horoli<\/dd>/);
|
|
|
|
const firstArrival=harness();firstArrival.ui.setPhoto('blob:dog');firstArrival.ui.startJourney();firstArrival.ui.setState('ready');
|
|
assert.equal(firstArrival.arrivalEnds,1,'immediate/reduced arrival notifies onboarding once');assert.equal(firstArrival.ui.canShowFirstGuide(),true);
|
|
firstArrival.els['first-guide'].showModal();firstArrival.ui.refreshBlocking();assert.equal(firstArrival.ui.isBlocked(),true);assert.equal(firstArrival.ui.canAcceptPhotoPaste(),false);assert.equal(firstArrival.ui.canExportPhoto(),false);
|
|
firstArrival.ui.setState('loading');assert.equal(firstArrival.els['first-guide'].open,false);
|
|
firstArrival.ui.setMediaOrigin('sample');firstArrival.ui.setState('ready');assert.equal(firstArrival.ui.canShowFirstGuide(),false);
|
|
firstArrival.ui.setMediaOrigin('personal');firstArrival.els['theme-select'].value='booth';firstArrival.els['theme-select'].dispatch('change');assert.equal(firstArrival.ui.canShowFirstGuide(),false);
|
|
const timedGuide=harness();const guideTimers=new Map();let guideTimerId=0;
|
|
timedGuide.root.defaultView.setTimeout=(fn,ms)=>{guideTimers.set(++guideTimerId,{fn,ms});return guideTimerId;};timedGuide.root.defaultView.clearTimeout=id=>guideTimers.delete(id);
|
|
timedGuide.ui.setPhoto('blob:dog');timedGuide.ui.startJourney();timedGuide.ui.setState('ready');
|
|
assert.equal(timedGuide.arrivalEnds,0);assert.equal(timedGuide.ui.canShowFirstGuide(),false);
|
|
let guideTimer=[...guideTimers.entries()][0];assert.equal(guideTimer[1].ms,1350);guideTimers.delete(guideTimer[0]);guideTimer[1].fn();assert.equal(timedGuide.arrivalEnds,0);
|
|
guideTimer=[...guideTimers.entries()][0];assert.equal(guideTimer[1].ms,1500);guideTimers.delete(guideTimer[0]);guideTimer[1].fn();assert.equal(timedGuide.arrivalEnds,1);assert.equal(timedGuide.ui.canShowFirstGuide(),true);
|
|
timedGuide.ui.setPhoto('blob:next');timedGuide.ui.startJourney();timedGuide.ui.setState('ready');timedGuide.els['settings-open'].dispatch('click');
|
|
assert.equal(timedGuide.arrivalEnds,1,'interrupting arrival never opens a guide beneath another modal');assert.equal(guideTimers.size,0);
|
|
|
|
const menu = harness();
|
|
assert.equal(menu.els['scene-menu-open'].hidden,true,'entry uses credits, not scene menu');
|
|
menu.ui.setState('ready');menu.els['scene-menu-open'].dispatch('click');
|
|
assert.equal(menu.els['scene-menu'].open,true,'scene menu opens');
|
|
assert.equal(menu.ui.isBlocked(),true,'scene input is blocked behind menu');
|
|
assert.equal(menu.ui.canAcceptPhotoPaste(),false,'menu blocks paste');
|
|
assert.equal(menu.ui.canExportPhoto(),true,'menu must not disable export eligibility');
|
|
menu.els['scene-menu'].dispatch('cancel',{preventDefault(){}});
|
|
assert.equal(menu.els['scene-menu'].open,false);assert.equal(menu.root.activeElement,menu.els['scene-menu-open']);
|
|
menu.els['scene-menu-open'].dispatch('click');menu.els['settings-open'].dispatch('click');
|
|
assert.equal(menu.els['scene-menu'].open,false);assert.equal(menu.els['settings-dialog'].open,true);
|
|
menu.els['settings-close'].dispatch('click');assert.equal(menu.root.activeElement,menu.els['scene-menu-open']);
|
|
for (const state of ['loading','error']) {
|
|
menu.ui.setState(state);menu.els['scene-menu-open'].dispatch('click');assert.equal(menu.els['scene-menu'].open,true);
|
|
menu.ui.setState('ready');assert.equal(menu.els['scene-menu'].open,false);
|
|
}
|
|
menu.els['scene-menu-open'].dispatch('click');menu.ui.setRenderLost(true);assert.equal(menu.els['scene-menu'].open,false);
|
|
menu.els['scene-menu-open'].dispatch('click');assert.equal(menu.els['scene-menu'].open,false);
|
|
menu.ui.setRenderLost(false);menu.els['scene-menu-open'].dispatch('click');menu.abort.abort();assert.equal(menu.els['scene-menu'].open,false);
|
|
const toolbar=html.match(/<header id="scene-toolbar"[\s\S]*?<\/header>/)[0];
|
|
for(const id of ['scene-photo-change','export-open','settings-open','scene-zoom-reset'])assert.ok(!toolbar.includes('id="'+id+'"'),'secondary action removed from header: '+id);
|
|
assert.ok(toolbar.includes('id="scene-menu-open"'));
|
|
console.log('Scene menu: lifecycle, blocking, action handoff, focus and reduced header passed.');
|
|
const repeatMenu=harness();repeatMenu.ui.setState('ready');
|
|
repeatMenu.els['scene-menu-open'].dispatch('click');repeatMenu.els['scene-menu-open'].dispatch('click');
|
|
repeatMenu.els['scene-menu-close'].dispatch('click');repeatMenu.els['scene-menu-open'].dispatch('click');
|
|
repeatMenu.els['scene-menu'].dispatch('close');assert.equal(repeatMenu.els['scene-menu'].open,true);assert.equal(repeatMenu.els['scene-menu-open'].attributes['aria-expanded'],'true');
|
|
repeatMenu.els['scene-menu'].dispatch('click',{clientX:0,clientY:0});assert.equal(repeatMenu.els['scene-menu'].open,false);
|
|
for(const action of ['scene-photo-change','export-open','scene-zoom-reset','scene-guide-replay']){
|
|
repeatMenu.els['scene-menu-open'].dispatch('click');repeatMenu.els[action].dispatch('click');assert.equal(repeatMenu.els['scene-menu'].open,false,action+' closes the sheet synchronously');
|
|
}
|
|
repeatMenu.ui.setMediaOrigin('sample');repeatMenu.els['scene-menu-open'].dispatch('click');
|
|
for(const action of ['scene-photo-change','export-open','settings-open','scene-guide-replay'])assert.equal(repeatMenu.els[action].hidden,true);
|
|
repeatMenu.els['sample-home'].dispatch('click');repeatMenu.els['home-confirm'].dispatch('click');assert.equal(repeatMenu.els['scene-menu'].open,false);assert.equal(repeatMenu.els['scene-menu-open'].hidden,true);assert.equal(repeatMenu.root.activeElement,repeatMenu.els['entry-photo-button']);
|
|
const loadingActions=harness();
|
|
for(const origin of ['personal','sample']){
|
|
loadingActions.ui.setMediaOrigin(origin);
|
|
for(const state of ['entry','loading','ready']){
|
|
loadingActions.ui.setState(state);assert.equal(loadingActions.els['flow-actions'].hidden,true,'no replacement action in '+state);
|
|
}
|
|
loadingActions.ui.setState('error',true);assert.equal(loadingActions.els['flow-actions'].hidden,false,'error keeps recovery actions');
|
|
assert.equal(loadingActions.els['flow-retry'].hidden,false);
|
|
}
|
|
const publicSettings=html.match(/<dialog id="settings-dialog"[\s\S]*?<\/dialog>/)[0];
|
|
assert.ok(!publicSettings.includes('id="camera-composition"'),'window composition is not a consumer setting');
|
|
assert.ok(html.indexOf('id="camera-composition"')>html.indexOf('id="internal-controls"'),'retain hidden rollback adapter without changing saved camera');
|
|
assert.ok(!html.includes('익숙한 유리문 너머에서 만나보세요'),'redundant landing introduction removed');
|
|
assert.ok(!html.includes('id="welcome-pet-copy"'),'removed introduction has no live name binding');
|
|
|
|
const edit=harness();edit.ui.setState('ready');
|
|
assert.equal(typeof edit.ui.startPhotoAdjust,'function');edit.ui.startPhotoAdjust();
|
|
assert.equal(edit.ui.isPhotoAdjusting(),true);assert.equal(edit.ui.isBlocked(),false);assert.equal(edit.els['canvas-wrap'].inert,false);
|
|
assert.equal(edit.ui.canShowFirstGuide(),false);assert.equal(edit.ui.canAcceptPhotoPaste(),false);assert.equal(edit.ui.canExportPhoto(),false);
|
|
edit.els['photo-adjust-done'].dispatch('click');assert.equal(edit.ui.isPhotoAdjusting(),false);
|
|
edit.ui.setMediaOrigin('sample');edit.ui.startPhotoAdjust();assert.equal(edit.ui.isPhotoAdjusting(),true,'sample edit supported');
|
|
edit.root.dispatch('keydown',{key:'Escape',preventDefault(){}});assert.equal(edit.ui.isPhotoAdjusting(),false);
|
|
edit.ui.startPhotoAdjust();edit.els['scene-menu-open'].dispatch('click');assert.equal(edit.ui.isPhotoAdjusting(),false);edit.els['scene-menu-close'].dispatch('click');
|
|
edit.ui.startPhotoAdjust();edit.els['sample-home'].dispatch('click');assert.equal(edit.ui.isPhotoAdjusting(),false);assert.equal(edit.homeCalls,0);assert.equal(edit.els['home-dialog'].open,true);
|
|
edit.els['home-cancel'].dispatch('click');assert.equal(edit.els['home-dialog'].open,false);assert.equal(edit.root.activeElement,edit.els['sample-home']);assert.equal(edit.els.experience.dataset.flow,'ready');
|
|
edit.els['sample-home'].dispatch('click');edit.els['home-confirm'].dispatch('click');assert.equal(edit.homeCalls,1);assert.equal(edit.els.experience.dataset.flow,'entry');
|
|
for(const transition of ['state','photo','origin','loss','abort']){
|
|
const e=harness();e.ui.setState('ready');e.ui.startPhotoAdjust();
|
|
if(transition==='state')e.ui.setState('loading');if(transition==='photo')e.ui.setPhoto('blob:new');if(transition==='origin')e.ui.setMediaOrigin('sample');if(transition==='loss')e.ui.setRenderLost(true);if(transition==='abort')e.abort.abort();
|
|
assert.equal(e.ui.isPhotoAdjusting(),false,transition+' exits photo edit');
|
|
}
|
|
const askHome=harness();askHome.ui.setMediaOrigin('sample');askHome.ui.setPhoto('blob:unchanged');askHome.ui.setState('loading');
|
|
askHome.els['sample-home'].dispatch('click');askHome.els['sample-home'].dispatch('click');assert.equal(askHome.homeCalls,0);
|
|
askHome.els['home-dialog'].dispatch('cancel',{preventDefault(){}});assert.equal(askHome.els['flow-photo'].src,'blob:unchanged');assert.equal(askHome.els.experience.dataset.flow,'loading');assert.equal(askHome.homeCalls,0);
|
|
askHome.els['sample-home'].dispatch('click');askHome.els['home-dialog'].dispatch('click',{clientX:0,clientY:0});assert.equal(askHome.els['home-dialog'].open,false);
|
|
askHome.els['sample-home'].dispatch('click');askHome.ui.setState('ready');askHome.els['home-confirm'].dispatch('click');assert.equal(askHome.homeCalls,0,'stale confirmation after new state cannot navigate');
|
|
askHome.els['sample-home'].dispatch('click');askHome.els['home-dialog'].dispatch('close');assert.equal(askHome.els['home-dialog'].open,true,'old close event cannot close a new confirmation');
|
|
askHome.ui.setRenderLost(true);assert.equal(askHome.els['home-dialog'].open,false);
|
|
assert.match(toolbar,/id="sample-home"[^>]*aria-label="첫 화면으로 가기"/);
|
|
assert.ok(!html.match(/<dialog id="scene-menu"[\s\S]*?<\/dialog>/)[0].includes('id="sample-home"'));
|
|
assert.ok(!html.match(/<dialog id="scene-menu"[\s\S]*?<\/dialog>/)[0].includes('id="photo-adjust-open"'));
|