Replace project with current server release and Windows setup
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.
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import {test} from 'node:test';
|
||||
import {readFile} from 'node:fs/promises';
|
||||
import {loadTypescript} from './support/load-typescript.mjs';
|
||||
// Removing the carousel markup must fail before implementing it.
|
||||
test('entry replaces three permanent explanations with one manually controlled notice region',async()=>{
|
||||
const html=await readFile(new URL('../index.html',import.meta.url),'utf8');
|
||||
assert.match(html,/id="entry-notices"/);
|
||||
assert.match(html,/id="notice-prev"[^>]*aria-label="이전 안내"/);
|
||||
assert.match(html,/id="notice-next"[^>]*aria-label="다음 안내"/);
|
||||
assert.doesNotMatch(html,/<p class="entry-privacy">/);
|
||||
});
|
||||
class El extends EventTarget {
|
||||
constructor(id){super();this.id=id;this.hidden=false;this.dataset={};this.attrs={};}
|
||||
setAttribute(k,v){this.attrs[k]=v;}
|
||||
fire(type,props={}){const e=new Event(type,{cancelable:true});Object.assign(e,props);this.dispatchEvent(e);return e;}
|
||||
closest(){return null;}
|
||||
}
|
||||
async function harness(desktop=false){
|
||||
const {initEntryNotices}=await loadTypescript(new URL('../src/ui/EntryNotices.ts',import.meta.url));
|
||||
const ids=['entry-notices','notice-prev','notice-next','notice-position','notice-status','photo-tip','privacy-tip','paste-tip'];
|
||||
const els=Object.fromEntries(ids.map(id=>[id,new El(id)]));
|
||||
els['photo-tip'].textContent='정면 사진이면 더 귀여워요';els['privacy-tip'].textContent='사진은 이 브라우저 안에서만 처리해요';els['paste-tip'].textContent='복사한 사진도 붙여넣을 수 있어요 Ctrl+V · ⌘V';
|
||||
const media=new El('media');media.matches=desktop;
|
||||
const root={querySelector:s=>els[s.slice(1)],defaultView:{matchMedia:()=>media}};
|
||||
const controller=new AbortController();initEntryNotices(root,controller.signal);
|
||||
return {els,media,controller,active:()=>ids.filter(id=>els[id].dataset.active==='true')};
|
||||
}
|
||||
test('mobile has two notices, arrows loop, and initial render is silent',async()=>{
|
||||
const h=await harness();assert.deepEqual(h.active(),['photo-tip']);assert.equal(h.els['paste-tip'].hidden,true);assert.equal(h.els['notice-position'].textContent,'1 / 2');assert.equal(h.els['notice-status'].textContent,undefined);
|
||||
h.els['notice-next'].fire('click');assert.deepEqual(h.active(),['privacy-tip']);assert.match(h.els['notice-status'].textContent,/2 \/ 2/);
|
||||
h.els['notice-next'].fire('click');assert.deepEqual(h.active(),['photo-tip']);
|
||||
h.els['notice-prev'].fire('click');assert.deepEqual(h.active(),['privacy-tip']);
|
||||
});
|
||||
test('desktop adds paste notice and changing to touch-only removes it immediately',async()=>{
|
||||
const h=await harness(true);h.els['notice-prev'].fire('click');assert.deepEqual(h.active(),['paste-tip']);
|
||||
h.media.matches=false;h.media.fire('change');assert.deepEqual(h.active(),['photo-tip']);assert.equal(h.els['paste-tip'].hidden,true);assert.equal(h.els['notice-status'].textContent,'');
|
||||
});
|
||||
test('horizontal single-touch swipe advances; vertical, cancelled and multiple pointers do not',async()=>{
|
||||
const h=await harness();const el=h.els['entry-notices'];
|
||||
const down=(id=1)=>el.fire('pointerdown',{pointerId:id,pointerType:'touch',isPrimary:true,clientX:160,clientY:20,targetElement:null});
|
||||
const up=(x,y=20,id=1)=>el.fire('pointerup',{pointerId:id,clientX:x,clientY:y});
|
||||
down();up(70);assert.deepEqual(h.active(),['privacy-tip']);
|
||||
down();up(150,100);assert.deepEqual(h.active(),['privacy-tip']);
|
||||
down();el.fire('pointercancel',{pointerId:1});up(70);assert.deepEqual(h.active(),['privacy-tip']);
|
||||
down();el.fire('pointerdown',{pointerId:2,pointerType:'touch',isPrimary:false,clientX:120,clientY:20});up(70);assert.deepEqual(h.active(),['privacy-tip']);
|
||||
});
|
||||
test('abort removes interaction and media listeners',async()=>{
|
||||
const h=await harness(true);h.controller.abort();h.els['notice-next'].fire('click');h.media.matches=false;h.media.fire('change');assert.deepEqual(h.active(),['photo-tip']);assert.equal(h.els['paste-tip'].hidden,false);
|
||||
});
|
||||
test('notices follow the sample action and use app-font counters with a bottom mobile layout',async()=>{
|
||||
const html=await readFile(new URL('../index.html',import.meta.url),'utf8');
|
||||
const css=await readFile(new URL('../src/style.css',import.meta.url),'utf8');
|
||||
assert.ok(html.indexOf('id="entry-samples"') < html.indexOf('id="entry-notices"'));
|
||||
assert.match(css,/#notice-position\s*\{[^}]*font-family:inherit/);
|
||||
assert.match(css,/@media\s*\(max-width:640px\)[\s\S]*#entry-notices\s*\{[^}]*margin-top:auto/);
|
||||
});
|
||||
Reference in New Issue
Block a user