merge: 최신 main을 tRPC JSON 본문 전송에 통합
This commit is contained in:
@@ -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')) {
|
||||
|
||||
Reference in New Issue
Block a user