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=' ';settingsName.dispatch('change'); assert.equal(settingsName.value,'');assert.equal(named.els['entry-photo-button'].textContent,' 사진 고르기'); named.ui.setMediaOrigin('sample');named.ui.startJourney(); assert.equal(named.els['flow-title'].textContent,'누렁이를 할머니 집으로 데려가는 중…'); assert.equal(settingsName.value,'','sample never overwrites the saved name'); named.ui.setMediaOrigin('personal');named.ui.startJourney(); assert.equal(named.els['flow-title'].textContent,'를 할머니 집으로 데려가는 중…'); 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,/
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(/
/)[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(//)[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(//)[0].includes('id="sample-home"')); assert.ok(!html.match(//)[0].includes('id="photo-adjust-open"'));