Files
ssessesse-ki-battle/tests/preview-server.ts
T

51 lines
5.7 KiB
TypeScript

// Local UI fixture only. This entry point is never compiled into the production server.
import { createGameServer } from '../src/server/create-server.js';
import { readFile } from 'node:fs/promises';
import { build } from 'esbuild';
const clientDirectory = process.env.UI_PREVIEW_CLIENT ?? 'dist/client';
const app = await createGameServer({ mongoUri: process.env.TEST_MONGODB_URI!, databaseName: 'dragon_ball_test_preview_20261012', clientDirectory, adminUser: 'preview', adminPassword: 'preview-test-only', environment: 'test' });
// Test-only credentials stay on the fixture server; normal production auth is tested separately.
const authorization = `Basic ${Buffer.from('preview:preview-test-only').toString('base64')}`;
app.get('/__test/peer', async (_request, reply) => {
const index = await readFile(clientDirectory + '/index.html', 'utf8');
return reply.type('text/html; charset=utf-8').send(index.replace('<head>', '<head><script src="/__test/peer-monitor.js"></script>'));
});
app.get('/__test/peer-monitor.js', async (_request, reply) => reply.type('application/javascript').send(await readFile('tests/peer-browser-monitor.js', 'utf8')));
app.get('/__test/admin', async (_request, reply) => {
const response = await app.inject({ url: '/admin/analytics', headers: { authorization } });
const bootstrap = '<script>const f=window.fetch;window.fetch=(u,o)=>f(typeof u==="string"&&u.startsWith("/api/admin/analytics/")?"/__test/admin-api/"+u.slice(21):u,o)</script>';
return reply.type('text/html; charset=utf-8').send(response.body.replace('<head>', `<head>${bootstrap}`));
});
app.get('/__test/admin-api/*', async (request, reply) => {
const tail = (request.params as { '*': string })['*'];
const query = request.url.includes('?') ? request.url.slice(request.url.indexOf('?')) : '';
const response = await app.inject({ url: `/api/admin/analytics/${tail}${query}`, headers: { authorization } });
return reply.code(response.statusCode).type('application/json').send(response.body);
});
app.get('/__test/mobile', (request, reply) => {
const query = request.query as { width?: string; height?: string };
const width = query.width === '320' ? 320 : 390, height = query.height === '568' ? 568 : 844;
return reply.type('text/html; charset=utf-8').send(`<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>모바일 검증</title><style>body{margin:0;background:#dfe4ec}iframe{width:${width}px;height:${height}px;border:0;display:block}</style><iframe title="모바일 게임" src="/"></iframe>`);
});
app.get('/__test/desktop', (_request, reply) => reply.type('text/html; charset=utf-8').send('<!doctype html><meta charset="utf-8"><title>PC 검증</title><style>body{margin:0;background:#dfe4ec}iframe{width:1280px;height:720px;border:0;display:block}</style><iframe title="PC 게임" src="/"></iframe>'));
app.get('/__test/admin-mobile', (_request, reply) => reply.type('text/html; charset=utf-8').send('<!doctype html><meta charset="utf-8"><title>관리자 모바일 검증</title><style>body{margin:0;background:#dfe4ec}iframe{width:390px;height:844px;border:0;display:block}</style><iframe title="관리자 분석" src="/__test/admin"></iframe>'));
app.get('/__test/scenarios', async (_request, reply) => {
const index = await readFile(clientDirectory + '/index.html', 'utf8');
const styles = index.match(/<link[^>]+rel="stylesheet"[^>]*>/g)?.join('') ?? '';
return reply.type('text/html; charset=utf-8').send(`<!doctype html><html lang="ko"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>UI 상태 검증</title>${styles}<style>body>.app{min-height:calc(100svh - 60px)}#scenarios{display:flex;align-items:center;gap:8px;height:60px;padding:6px 12px;background:white;position:relative;z-index:20}#scenarios select{font:inherit;font-size:14px;border:1px solid #c5ceca;border-radius:8px;padding:8px;min-width:0;max-width:70%;height:44px}#scenarios button{height:44px;min-height:44px;padding:6px 18px}#scenarios+.app header{height:48px}</style></head><body class="game-page"><nav id="scenarios" aria-label="검증 시나리오"></nav><div class="app" data-screen="match"><header><span class="brand">드래곤볼</span><span>UI 상태 검증</span></header><main id="main"><section id="match"></section></main></div><script type="module" src="/__test/scenarios.js"></script></body></html>`);
});
app.get('/__test/scenarios.js', async (_request, reply) => {
const bundle = await build({ entryPoints: ['tests/ui-scenarios.ts'], bundle: true, write: false, format: 'esm', platform: 'browser', loader: { '.css': 'empty' } });
return reply.type('application/javascript').send(bundle.outputFiles[0].text);
});
app.get('/__test/scenarios-mobile', (request, reply) => {
const query = request.query as { width?: string; height?: string };
const width = query.width === '320' ? 320 : 390, height = query.height === '568' ? 568 : 844;
return reply.type('text/html; charset=utf-8').send(`<!doctype html><meta charset="utf-8"><title>모바일 상태 검증</title><style>body{margin:0;background:#dfe4ec}iframe{width:${width}px;height:${height}px;border:0;display:block}</style><iframe title="모바일 UI 상태" src="/__test/scenarios"></iframe>`);
});
app.get('/__test/scenarios-desktop', (_request, reply) => reply.type('text/html; charset=utf-8').send('<!doctype html><meta charset="utf-8"><title>PC 상태 검증</title><style>body{margin:0;background:#dfe4ec}iframe{width:1280px;height:720px;border:0;display:block}</style><iframe title="PC UI 상태" src="/__test/scenarios"></iframe>'));
const port = Number(process.env.UI_PREVIEW_PORT ?? 3001);
await app.listen({ host: '127.0.0.1', port });
console.log(`http://127.0.0.1:${port}`);
for (const signal of ['SIGINT', 'SIGTERM'] as const) process.once(signal, () => { void app.close(); });