merge: 최신 main을 tRPC JSON 본문 전송에 통합

This commit is contained in:
2026-08-17 11:12:09 +00:00
49 changed files with 1182 additions and 162 deletions
@@ -221,19 +221,19 @@ test('dynasty list matches the ref Chromium table geometry and interactions', as
{ y: 65, height: 37 },
{ y: 112, height: 139 },
]);
expect(geometry.tables[0]!.fontFamily).toContain('Times New Roman');
expect(geometry.tables[0]!.fontSize).toBe('16px');
expect(geometry.tables[0]!.lineHeight).toBe('normal');
expect(geometry.button).toEqual({
expect(geometry.tables[0]!.fontFamily).toContain('Pretendard');
expect(geometry.tables[0]!.fontSize).toBe('14px');
expect(geometry.tables[0]!.lineHeight).toBe('18.2px');
expect(geometry.button.fontFamily).toContain('Pretendard');
expect(geometry.button).toMatchObject({
height: 22,
borderWidth: '2px',
borderRadius: '0px',
padding: '1px 6px',
fontFamily: 'Arial',
fontSize: '13.3333px',
cursor: 'default',
});
expect(geometry.firstCell).toEqual({ padding: '1px', borderWidth: '0px', textAlign: 'start' });
expect(geometry.firstCell).toEqual({ padding: '0px', borderWidth: '1px', textAlign: 'start' });
const historyLink = page.getByRole('link', { name: '역사 보기' }).last();
await expect(historyLink).toHaveAttribute('href', '/che/yearbook?serverID=hwe_260725_u3uE');
@@ -276,17 +276,23 @@ test('dynasty detail preserves the legacy fields, old-nation table and error flo
const first = container.querySelector<HTMLTableElement>('table')!.getBoundingClientRect();
const emperor = container.querySelector<HTMLTableElement>('.emperor-table')!.getBoundingClientRect();
const oldNation = container.querySelector<HTMLTableElement>('.old-nation-table')!.getBoundingClientRect();
const containerStyle = getComputedStyle(container);
const buttonStyle = getComputedStyle(container.querySelector<HTMLButtonElement>('button')!);
return {
container: { x: rect.x, y: rect.y, width: rect.width },
first: { x: first.x, y: first.y, width: first.width, height: first.height },
emperor: { x: emperor.x, y: emperor.y, width: emperor.width },
oldNation: { x: oldNation.x, width: oldNation.width },
fontFamily: containerStyle.fontFamily,
buttonFontFamily: buttonStyle.fontFamily,
};
});
expect(geometry.container).toEqual({ x: 140, y: 8, width: 1000 });
expect(geometry.first).toEqual({ x: 140, y: 8, width: 1000, height: 47 });
expect(geometry.emperor).toEqual({ x: 140, y: 55, width: 1000 });
expect(geometry.oldNation).toEqual({ x: 140, width: 1000 });
expect(geometry.fontFamily).toContain('Pretendard');
expect(geometry.buttonFontFamily).toContain('Pretendard');
await page.goto(`${gameOrigin}/che/dynasty/999`);
await expect(page.getByRole('alert')).toHaveText('왕조 정보를 찾을 수 없습니다.');
@@ -0,0 +1,40 @@
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { defineConfig, devices } from '@playwright/test';
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
const gamePort = process.env.FRONTEND_PARITY_GAME_PORT ?? '15126';
const gameOrigin = `http://127.0.0.1:${gamePort}`;
const frontendEnv =
'VITE_APP_BASE_PATH=/che VITE_GAME_API_URL=/che/api/trpc ' +
'VITE_IMAGE_PUBLIC_URL=/image VITE_GAME_ASSET_URL=/image ' +
'VITE_GAME_PROFILE=che VITE_GATEWAY_WEB_URL=/gateway/';
export default defineConfig({
testDir: '.',
testMatch: ['dynasty-parity.spec.ts', 'map-trend.spec.ts'],
fullyParallel: false,
workers: 1,
timeout: 30_000,
expect: { timeout: 5_000 },
reporter: [['list']],
outputDir: process.env.SAMMO_TEST_OUTPUT_DIR ?? resolve(repositoryRoot, 'test-results/game-font'),
use: {
...devices['Desktop Chrome'],
baseURL: `${gameOrigin}/che/`,
colorScheme: 'dark',
deviceScaleFactor: 1,
locale: 'ko-KR',
timezoneId: 'UTC',
trace: 'retain-on-failure',
screenshot: 'only-on-failure',
},
webServer: {
command: `${frontendEnv} pnpm --filter @sammo-ts/game-frontend build && ${frontendEnv} pnpm --filter @sammo-ts/game-frontend preview --host 127.0.0.1 --port ${gamePort}`,
cwd: repositoryRoot,
url: `${gameOrigin}/che/`,
reuseExistingServer: false,
timeout: 120_000,
},
});
@@ -179,6 +179,15 @@ const installMainFixture = async (page: Page, failRecords = false) => {
myNation: 1,
});
}
if (operation === 'public.getMapLayout') return response(fixture.game.mapLayout);
if (operation === 'public.getCachedMap') {
return response({ ...fixture.game.map, history: cachedHistory });
}
if (operation === 'public.getWorldTrend') {
return response({ year: 200, month: 1, turnTerm: 10 });
}
if (operation === 'public.getNationList') return response([]);
if (operation === 'public.getGeneralList') return response([]);
if (operation === 'turns.getCommandTable') return response({ general: [], nation: [] });
if (operation === 'turns.reserved.getGeneral') return response([]);
if (operation === 'messages.getRecent') return response(emptyMessages);
@@ -265,6 +274,40 @@ test('shows the current in-game map and all three recent record streams', async
}
});
test('uses the shared content font for public game history on desktop and mobile', async ({ page }) => {
await installMainFixture(page);
for (const viewport of [
{ width: 1200, height: 900 },
{ width: 500, height: 900 },
]) {
await page.setViewportSize(viewport);
await page.goto(gameUrl('/public'));
await expect(page.locator('.recent-log-list')).toContainText('유비가 촉을 건국하였습니다.');
const font = await page.locator('.recent-log-list').evaluate((element) => {
const style = getComputedStyle(element);
return { family: style.fontFamily, size: style.fontSize };
});
expect(font.family).toContain('Pretendard');
expect(font.size).toBe('14px');
const pretendardFont = await page.evaluate(async () => {
await document.fonts.load('400 14px Pretendard', '유비가 촉을 건국하였습니다.');
await document.fonts.ready;
const statuses = [...document.fonts]
.filter((face) => face.family.replaceAll('"', '') === 'Pretendard')
.map((face) => face.status);
return {
statuses,
koreanGlyphsLoaded: document.fonts.check('400 14px Pretendard', '유비가 촉을 건국하였습니다.'),
};
});
expect(pretendardFont.statuses).toContain('loaded');
expect(pretendardFont.koreanGlyphsLoaded).toBe(true);
}
});
test('keeps the current map visible when the recent record request fails', async ({ page }) => {
await installMainFixture(page, true);
await page.setViewportSize({ width: 1440, height: 1000 });
@@ -0,0 +1,83 @@
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
import { chromium } from '@playwright/test';
const refRoot = resolve(process.env.REF_SAM_ROOT ?? '/home/letrhee/sam_rebuild/ref/sam');
const artifactDir = process.env.REF_PERSONAL_WAR_LOG_ARTIFACT_DIR;
const formatterUrl = pathToFileURL(resolve(refRoot, 'hwe/ts/utilGame/formatLog.ts')).href;
const { formatLog } = await import(formatterUrl);
const css = await readFile(resolve(refRoot, 'dist_js/hwe_dynamic/vue/v_main.css'), 'utf8');
const records = [
'<C>●</>10월:천귀병으로 <Y>ⓝ염행</>의 보병을 <M>수비</>합니다. <1>12:54</>',
'<C>●</>10월:천귀병으로 <Y>ⓝ염행</>의 보병을 <M>공격</>합니다. <1>12:55</>',
];
const browser = await chromium.launch({ headless: true });
try {
const context = await browser.newContext({
colorScheme: 'dark',
deviceScaleFactor: 1,
locale: 'ko-KR',
timezoneId: 'UTC',
});
const page = await context.newPage();
const lines = records.map((record) => `<div class="fixture-line">${formatLog(record)}</div>`).join('');
await page.setContent(
`<!doctype html><html><head><style>${css}</style></head><body>` +
`<div id="container"><div class="RecordZone row gx-0"><div class="GeneralLog col col-12 col-lg-6">` +
`<div class="bg1 center s-border-tb title">개인 기록</div>${lines}</div></div></div></body></html>`,
{ waitUntil: 'networkidle' }
);
await page.evaluate(() => document.fonts.ready);
for (const viewport of [
{ name: 'desktop', width: 1200, height: 900 },
{ name: 'mobile', width: 500, height: 900 },
]) {
await page.setViewportSize(viewport);
const measurement = await page.locator('.fixture-line').evaluateAll((elements) =>
elements.map((element) => {
const spans = [...element.querySelectorAll('span')];
const time = spans.find((span) => /^\d{2}:\d{2}$/u.test(span.textContent ?? ''));
const name = spans.find((span) => span.textContent === 'ⓝ염행');
const action = spans.find((span) => span.textContent === '수비' || span.textContent === '공격');
if (
!(time instanceof HTMLElement) ||
!(name instanceof HTMLElement) ||
!(action instanceof HTMLElement)
) {
throw new Error('Ref 개인 공격·수비 기록의 비교 span을 찾지 못했습니다.');
}
const rect = element.getBoundingClientRect();
return {
text: element.textContent,
row: {
width: rect.width,
height: rect.height,
fontSize: getComputedStyle(element).fontSize,
lineHeight: getComputedStyle(element).lineHeight,
},
timeFontSize: getComputedStyle(time).fontSize,
nameFontSize: getComputedStyle(name).fontSize,
actionFontSize: getComputedStyle(action).fontSize,
};
})
);
const output = { viewport, measurement };
console.log(JSON.stringify(output));
if (artifactDir) {
await mkdir(artifactDir, { recursive: true });
await Promise.all([
page.screenshot({ path: resolve(artifactDir, `ref-personal-war-log-font-${viewport.name}.png`) }),
writeFile(
resolve(artifactDir, `ref-personal-war-log-font-${viewport.name}.json`),
`${JSON.stringify(output, null, 2)}\n`
),
]);
}
}
} finally {
await browser.close();
}
@@ -955,6 +955,63 @@ test.describe('yearbook legacy parity', () => {
});
}
for (const viewport of [
{ name: 'desktop', width: 1365, height: 768, minimumWorldHeight: 128 },
{ name: 'mobile', width: 390, height: 844, minimumWorldHeight: 149 },
]) {
test(`grows record blocks with long content on ${viewport.name}`, async ({ page }) => {
const longWorldHistory = Array.from(
{ length: 12 },
(_, index) => `<C>●</> 중원 정세 긴 기록 ${index + 1}번째 줄입니다.`
);
const longGlobalAction = Array.from(
{ length: 8 },
(_, index) => `<L>●</> 장수 동향 긴 기록 ${index + 1}번째 줄입니다.`
);
await page.route('**/che/api/trpc/**', async (route) => {
if (!operationNames(route).includes('yearbook.getHistory')) {
await route.fallback();
return;
}
await fulfillOperations(route, () => ({
...fixture.game.yearbook,
data: {
...fixture.game.yearbook.data,
globalHistory: longWorldHistory,
globalAction: longGlobalAction,
},
}));
});
await page.setViewportSize(viewport);
await page.goto(gameUrl('/yearbook'));
await expect(page.getByText('중원 정세 긴 기록 12번째 줄입니다.')).toBeVisible();
await expect(page.getByText('장수 동향 긴 기록 8번째 줄입니다.')).toBeVisible();
if (artifactRoot) {
await page.screenshot({
path: resolve(artifactRoot, `yearbook-long-content-${viewport.name}.png`),
fullPage: true,
animations: 'disabled',
});
}
const geometry = await page.locator('.history-log').evaluateAll((blocks) =>
blocks.map((block) => {
const element = block as HTMLElement;
return {
height: element.getBoundingClientRect().height,
clientHeight: element.clientHeight,
scrollHeight: element.scrollHeight,
};
})
);
expect(geometry[0]?.height).toBeGreaterThan(viewport.minimumWorldHeight);
expect(geometry[1]?.height).toBeGreaterThan(65);
expect(geometry[0]?.clientHeight).toBe(geometry[0]?.scrollHeight);
expect(geometry[1]?.clientHeight).toBe(geometry[1]?.scrollHeight);
});
}
test('shows the history API error while keeping month navigation available', async ({ page }) => {
await page.route('**/che/api/trpc/**', async (route) => {
if (operationNames(route).includes('yearbook.getHistory')) {