/** * test/webmaker-visual-editor.test.mjs — Ariweb Suite 0.2.0: 도입 모듈 Webmaker 1.8.0 의 직접 편집(시각 편집기) 회귀 검증. * Suite 쪽에서 확인할 것: ① 도입된 순수 모듈(modules/webmaker/lib/visual-editor.js · public/visual-editor-core.js)이 Suite 체크아웃에서 그대로 동작하고, * ② Suite 전용 변경(동시 편집 rev 충돌 409, [Ariweb Suite 변경])이 직접 편집 저장 경로에도 실제로 적용되며(서버를 띄워 확인), ③ 화면에 필수 접근성 조작이 있다. * 브라우저 없이 Node 로만 검사한다. Webmaker 자체 전체 테스트(modules/webmaker: npm test)는 별도로 실행한다. */ import { test, eq, ok, done, sleep } from '../lib/test.mjs'; import { createRequire } from 'node:module'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { readFileSync, existsSync, writeFileSync, mkdirSync, mkdtempSync, chmodSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { randomBytes } from 'node:crypto'; // 비밀값(설정 토큰·비밀번호)은 실행마다 만들고 변수로만 넘긴다 — 소스에 고정 문자열로 두지 않고 출력하지도 않는다 const SETUP_TOKEN = randomBytes(24).toString('base64url'); const PW = { alice: randomBytes(12).toString('base64url'), root: randomBytes(12).toString('base64url') }; import { spawn } from 'node:child_process'; const require = createRequire(import.meta.url); const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); const WM = join(ROOT, 'modules', 'webmaker'); await test('도입 모듈에 직접 편집 순수 모듈·런타임·화면 파일이 있고 Webmaker 1.8.0 · Suite 0.3.10(HAL AI 셸 복사, 도입 모듈 변경 없음) 이다', () => { for (const f of ['lib/visual-editor.js', 'public/visual-editor-core.js', 'public/wm-ve-canvas.js', 'public/visual-editor-ui.js']) ok(existsSync(join(WM, f)), f); eq(JSON.parse(readFileSync(join(WM, 'package.json'), 'utf8')).version, '1.8.0'); eq(JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')).version, '0.3.10'); const imported = JSON.parse(readFileSync(join(ROOT, 'IMPORTED_SOURCES.json'), 'utf8')).sources.find((s) => s.name === 'webmaker'); const pin = (readFileSync(join(ROOT, 'scripts', 'import-sources.mjs'), 'utf8').match(/name: 'webmaker'[^\n]*pin: '([0-9a-f]{40})'/) || [])[1]; ok(pin, '도입 스크립트의 webmaker pin'); if (imported.upstream && imported.upstream.workingTreeDirty) eq(imported.commit, '3b10859884ef2fd0f2af11934ab6c16370fd64b8', '개발 중 기록(--allow-dirty)은 0.1.1 기준 커밋 위의 작업 트리'); else { eq(imported.commit, pin, '깨끗한 기록은 스크립트의 고정 커밋과 같다'); eq(imported.upstream && imported.upstream.headCommit, pin, 'headCommit = pin'); } eq(imported.upstream && imported.upstream.version, '1.8.0'); ok(imported.files.some((f) => f.from === 'lib/visual-editor.js'), '도입 기록에 새 파일'); }); await test('lib/visual-editor: 캔버스 변환은 스크립트를 실행하지 않게 하고, 저장 검증은 새 스크립트·핸들러를 거절한다', () => { const ve = require(join(WM, 'lib', 'visual-editor.js')); const page = 't

안녕

본문

'; const canvas = ve.prepareCanvas(page, { baseHref: 'http://h/x/', session: 's', nonce: 'n', parentOrigin: 'http://h', runtime: '1' }); ok(/')); let err = null; try { ve.validateVisualSave({ original: page, body: body + '', maxBytes: 1e6 }); } catch (e) { err = e; } eq(err && err.status, 400); ok(/스크립트/.test(err.message)); }); await test('visual-editor-core: 실행 취소/다시 실행·저장 상태·메시지 검증·구성 요소가 Suite 체크아웃에서도 같다', () => { const core = require(join(WM, 'public', 'visual-editor-core.js')); const h = core.createHistory(10); h.reset('a'); h.push('b'); eq(h.undo(), 'a'); eq(h.redo(), 'b'); const s = core.createSaveState(); s.markDirty(); ok(s.beginSave()); s.saveOk(); eq(s.state(), 'saved'); const win = {}; ok(core.validateMessage({ source: win, origin: 'null', data: core.makeMessage('s', 'changed', {}) }, { expectedSource: win, expectedOrigin: 'null', session: 's', direction: 'fromCanvas' })); ok(!core.validateMessage({ source: win, origin: 'null', data: core.makeMessage('t', 'changed', {}) }, { expectedSource: win, expectedOrigin: 'null', session: 's', direction: 'fromCanvas' })); for (const id of ['section', 'heading', 'paragraph', 'button', 'image', 'divider', 'spacer']) ok(core.isSafeComponentHtml(core.componentHtml(id)), id); }); await test('화면(index.html)에 직접 편집 전환·저장·실행 취소·다시 실행·버리기·팔레트·속성 조작이 접근성 속성과 함께 있고 Suite 배너는 남아 있다', () => { const html = readFileSync(join(WM, 'public', 'index.html'), 'utf8'); ok(/