Files
behind_glass/scripts/test-tilt-integration.mjs
horoli e358e24436 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.
2026-10-09 16:43:35 +09:00

80 lines
6.3 KiB
JavaScript

import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { test } from 'node:test';
import ts from 'typescript';
import * as THREE from 'three';
import {loadTypescript} from './support/load-typescript.mjs';
const {CameraController}=await loadTypescript(new URL('../src/input/CameraController.ts',import.meta.url));
test('camera absolute input rejects invalid readings, clamps, and smooths at default .75', () => {
const camera = new THREE.PerspectiveCamera(), controller = new CameraController(camera);
controller.setView(3, -3); assert.deepEqual(controller.target.toArray(), [1, -1]);
controller.setView(NaN, 0); assert.deepEqual(controller.target.toArray(), [1, -1]);
const first = controller.update(1 / 60, 1); assert.ok(first.x > 0 && first.x < 1);
for (let i = 0; i < 120; i++) controller.update(1 / 60, 2);
assert.equal(camera.position.x, .75); assert.equal(camera.position.y, -.75); assert.equal(camera.position.z, 3);
const before = controller.current.toArray(); controller.hold(); controller.update(.1, 2); assert.deepEqual(controller.current.toArray(), before);
controller.setMaxOffset(NaN); controller.update(.1, 2); assert.equal(camera.position.x, .75);
});
test('the existing camera smoothing is frame-rate independent', () => {
const results = [30, 60, 120].map(fps => { const c = new CameraController(new THREE.PerspectiveCamera()); c.setView(.75, -.25); for (let i = 0; i < fps; i++) c.update(1 / fps, 1); return c.current.x; });
assert.ok(Math.max(...results) - Math.min(...results) < 1e-12);
});
test('integration keeps scene zoom separate and freezes camera at pinch start', async () => {
const main = await readFile(new URL('../src/main.ts', import.meta.url), 'utf8');
const html = await readFile(new URL('../index.html', import.meta.url), 'utf8');
assert.match(main, /if \(pinching\)\s*\{?\s*cameraController\.hold\(\)/);
assert.match(main, /gyro\.setPaused\(experience\.isBlocked\(\) \|\| experience\.isPhotoAdjusting\(\) \|\| pointer\.isInteracting\)/);
assert.match(main, /gyroButton\.onclick = \(\) => \{ if \(mobileTilt && !experience\.isBlocked\(\) && !experience\.isPhotoAdjusting\(\)\) void gyro\.toggle\(\); \}/);
assert.match(main, /gyroRecenter\.addEventListener\('click', \(\) => \{ if\(experience\.isBlocked\(\)\|\|experience\.isPhotoAdjusting\(\)\)return;cameraController\.reset\(\); gyro\.reset\(\); \}/);
assert.match(main, /gyro\.syncManualView\(\)/);
assert.match(html, /id="gyro"[^>]*aria-pressed="false"[^>]*>[\s\S]*?id="gyro-label">기울기 연결/);
assert.match(html, /id="gyro-status"[^>]*role="status"/);
assert.match(html, /센서값은 이 기기 안에서만/);
assert.match(main, /gyro\.dispose\(\)/);
});
test('real pointer, gyro and camera controllers freeze pending smoothing and resume manual view', async () => {
async function controllerModule(name) {
const text = await readFile(new URL(`../src/input/${name}.ts`, import.meta.url), 'utf8');
const output = ts.transpileModule(text, { compilerOptions: { target: ts.ScriptTarget.ES2020, module: ts.ModuleKind.ES2020 } }).outputText;
return import(`data:text/javascript;base64,${Buffer.from(output).toString('base64')}`);
}
const { PointerController } = await controllerModule('PointerController');
const { GyroController } = await controllerModule('GyroController');
class Events {
handlers = new Map(); captures = new Set(); classList = { add() {}, remove() {} };
addEventListener(type, fn) { if (!this.handlers.has(type)) this.handlers.set(type, new Set()); this.handlers.get(type).add(fn); }
removeEventListener(type, fn) { this.handlers.get(type)?.delete(fn); }
emit(type, init = {}) { for (const fn of [...(this.handlers.get(type) ?? [])]) fn({ pointerType: 'touch', pointerId: 1, clientX: 0, clientY: 0, ...init }); }
setPointerCapture(id) { this.captures.add(id); }
hasPointerCapture(id) { return this.captures.has(id); }
releasePointerCapture(id) { this.captures.delete(id); }
getBoundingClientRect() { return { left: 0, top: 0, width: 200, height: 100 }; }
}
const win = new Events(), doc = new Events(), element = new Events(), orientation = new Events();
const timers = new Map(); let timer = 0;
Object.assign(win, { screen: { orientation }, DeviceOrientationEvent: {}, isSecureContext: true, setTimeout: fn => { timers.set(++timer, fn); return timer; }, clearTimeout: id => timers.delete(id) });
orientation.angle = 0; doc.hidden = false; globalThis.window = win; globalThis.document = doc;
const camera = new THREE.PerspectiveCamera(), controller = new CameraController(camera);
const gyro = new GyroController((x, y) => controller.setView(x, y), () => {}, () => ({ x: controller.target.x, y: controller.target.y }));
let zoom = 1;
const pointer = new PointerController(element, (x, y) => { controller.add(x, y); gyro.syncManualView(); }, undefined, {
getZoom: () => zoom, setZoom: value => { zoom = value; },
onGestureChange: (active, pinching) => { if (pinching) controller.hold(); gyro.setPaused(active); },
});
const reading = (beta, gamma) => win.emit('deviceorientation', { alpha: 0, beta, gamma });
await gyro.toggle(); reading(0, 0); reading(0, 30); controller.update(1 / 60, 1);
assert.ok(controller.target.x > controller.current.x, 'smoothing was still in flight');
element.emit('pointerdown'); element.emit('pointerdown', { pointerId: 2, clientX: 100 });
const held = camera.position.clone(); reading(45, -30);
element.emit('pointermove', { pointerId: 2, clientX: 150 }); assert.equal(zoom, 1.5);
for (let i = 0; i < 20; i++) controller.update(1 / 60, 1);
assert.deepEqual(camera.position.toArray(), held.toArray(), 'pinch freezes real camera, including remaining smoothing');
element.emit('pointerup', { pointerId: 2 }); element.emit('pointermove', { clientX: 20 });
const manual = controller.target.clone(); assert.ok(manual.x > held.x / .75);
element.emit('pointerup'); reading(45, -30);
assert.deepEqual(controller.target.toArray(), manual.toArray(), 'first sensor sample preserves surviving-finger drag');
controller.add(-.06, 0); gyro.syncManualView(); const keyboard = controller.target.clone(); reading(50, -25);
assert.deepEqual(controller.target.toArray(), keyboard.toArray(), 'first sensor sample preserves keyboard view');
pointer.dispose(); gyro.dispose(); assert.equal(timers.size, 0);
});