Files

294 lines
12 KiB
TypeScript

import { expect, test, type Page, type Route } from '@playwright/test';
import { resolve } from 'node:path';
const artifactRoot = process.env.FRONTEND_PARITY_ARTIFACT_DIR;
const gameOrigin = `http://127.0.0.1:${process.env.FRONTEND_PARITY_GAME_PORT ?? '15102'}`;
const response = (data: unknown) => ({ result: { data } });
const errorResponse = (path: string, code: 'BAD_REQUEST' | 'NOT_FOUND', message: string) => ({
error: {
message,
code: -32000,
data: { code, httpStatus: code === 'NOT_FOUND' ? 404 : 400, path },
},
});
const operationNames = (route: Route): string[] => {
const pathname = new URL(route.request().url()).pathname;
return decodeURIComponent(pathname.slice(pathname.lastIndexOf('/trpc/') + 6)).split(',');
};
const listPayload = {
current: { year: 200, month: 1 },
entries: [
{
id: 1,
serverId: 'hwe_260725_u3uE',
phase: '훼2기',
name: '백년01',
year: 215,
month: 4,
color: '#FF0000',
type: 'che_병가',
power: 34434,
gennum: 71,
citynum: 78,
l12name: 'ⓜ⑮오리온자리',
l11name: 'ⓜ②물조명성너프필수',
l10name: 'ⓜ㉗좌우쌍욍검유비',
l9name: 'ⓜ②다니엘레메로시',
l8name: 'ⓜ⑤쿠로',
l7name: 'ⓜ⑮료우기시키',
l6name: 'ⓜ㉓랜덤박스',
l5name: 'ⓜ㉒임사영',
},
],
};
const detailPayload = {
emperor: {
id: 1,
serverId: 'hwe_260725_u3uE',
winnerNationId: 1,
phase: '훼2기',
nationCount: '8 / 17',
nationName: '백년01, 청해, 백제',
nationHist: '병가(3), 유가(2)',
genCount: '71 / 92',
personalHist: '의리(14), 대담(11)',
specialHist: '상재(8), 견고(7)',
name: '백년01',
type: 'che_병가',
color: '#FF0000',
year: 215,
month: 4,
power: 34434,
gennum: 71,
citynum: 78,
pop: '2,345,000 / 2,500,000',
poprate: '93.8 %',
gold: 120000,
rice: 150000,
l12name: 'ⓜ⑮오리온자리',
l11name: 'ⓜ②물조명성너프필수',
l10name: 'ⓜ㉗좌우쌍욍검유비',
l9name: 'ⓜ②다니엘레메로시',
l8name: 'ⓜ⑤쿠로',
l7name: 'ⓜ⑮료우기시키',
l6name: 'ⓜ㉓랜덤박스',
l5name: 'ⓜ㉒임사영',
tiger: '관우【10】, 조운【9】',
eagle: '주유【7】, 육손【6】',
gen: '오리온자리, 물조명성너프필수, 좌우쌍욍검유비',
history: ['<C>●</><Y><b>【통일】</b></><D><b>백년01</b></>이 전토를 통일하였습니다.'],
},
nations: [
{
nation: 1,
isWinner: true,
name: '백년01',
color: '#FF0000',
type: 'che_병가',
typeName: '병가',
level: 7,
levelName: '황제',
tech: 4000,
maxPower: 34434,
maxCrew: 120000,
maxCities: ['낙양', '장안', '성도'],
generals: [11, 12],
history: ['<Y>오리온자리</>가 황제로 즉위'],
date: '2026-07-25T12:00:00.000Z',
generalsFull: [
{ generalNo: 11, name: '오리온자리', lastYearMonth: 21504 },
{ generalNo: 12, name: '료우기시키', lastYearMonth: 21504 },
],
},
],
};
const installFixture = async (page: Page): Promise<void> => {
await page.route('**/image/game/**', async (route) => {
await route.fulfill({
status: 200,
contentType: 'image/png',
body: Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
'base64'
),
});
});
await page.route('**/che/api/trpc/**', async (route) => {
const results = operationNames(route).map((operation) => {
if (operation === 'lobby.info') {
return response({ myGeneral: null });
}
if (operation === 'dynasty.getList') {
return response(listPayload);
}
if (operation === 'dynasty.getDetail') {
const input = new URL(route.request().url()).searchParams.get('input') ?? '';
return input.includes('999')
? errorResponse(operation, 'NOT_FOUND', '왕조 정보를 찾을 수 없습니다.')
: response(detailPayload);
}
return errorResponse(operation, 'BAD_REQUEST', `Unhandled fixture operation: ${operation}`);
});
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(results),
});
});
};
test.beforeEach(async ({ page }) => {
await installFixture(page);
await page.setViewportSize({ width: 1280, height: 900 });
});
test('dynasty list matches the ref Chromium table geometry and interactions', async ({ page }) => {
await page.goto(`${gameOrigin}/che/dynasty`);
await expect(page.getByText('백년01 (215年 4月)')).toBeVisible();
const geometry = await page.locator('#dynasty-list-container').evaluate((container) => {
const rect = container.getBoundingClientRect();
const tables = Array.from(container.querySelectorAll<HTMLTableElement>('table')).map((table) => {
const tableRect = table.getBoundingClientRect();
const style = getComputedStyle(table);
return {
x: tableRect.x,
y: tableRect.y,
width: tableRect.width,
height: tableRect.height,
borderSpacing: style.borderSpacing,
fontFamily: style.fontFamily,
fontSize: style.fontSize,
lineHeight: style.lineHeight,
};
});
const button = container.querySelector<HTMLButtonElement>('button')!;
const buttonStyle = getComputedStyle(button);
const cellStyle = getComputedStyle(container.querySelector<HTMLTableCellElement>('td')!);
return {
container: { x: rect.x, y: rect.y, width: rect.width },
tables,
button: {
height: button.getBoundingClientRect().height,
borderWidth: buttonStyle.borderWidth,
borderRadius: buttonStyle.borderRadius,
padding: buttonStyle.padding,
fontFamily: buttonStyle.fontFamily,
fontSize: buttonStyle.fontSize,
cursor: buttonStyle.cursor,
},
firstCell: {
padding: cellStyle.padding,
borderWidth: cellStyle.borderWidth,
textAlign: cellStyle.textAlign,
},
};
});
expect(geometry.container).toEqual({ x: 140, y: 8, width: 1000 });
expect(geometry.tables.slice(0, 3).map(({ x, width, borderSpacing }) => ({ x, width, borderSpacing }))).toEqual([
{ x: 140, width: 1000, borderSpacing: '2px' },
{ x: 140, width: 1000, borderSpacing: '2px' },
{ x: 140, width: 1000, borderSpacing: '2px' },
]);
expect(geometry.tables.slice(0, 3).map(({ y, height }) => ({ y, height }))).toEqual([
{ y: 8, height: 47 },
{ 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({
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' });
const historyLink = page.getByRole('link', { name: '역사 보기' }).last();
await expect(historyLink).toHaveAttribute('href', '/che/yearbook?serverID=hwe_260725_u3uE');
if (artifactRoot) {
await page.screenshot({ path: resolve(artifactRoot, 'dynasty-core-list.png'), fullPage: true });
}
const detailLink = page.getByRole('link', { name: '자세히' });
await detailLink.focus();
await expect(detailLink).toBeFocused();
await detailLink.click();
await expect(page).toHaveURL(/\/che\/dynasty\/1$/);
await expect(page.getByText('국 가 수')).toBeVisible();
if (artifactRoot) {
await page.screenshot({ path: resolve(artifactRoot, 'dynasty-core-detail.png'), fullPage: true });
}
});
test('dynasty detail preserves the legacy fields, old-nation table and error flow', async ({ page }) => {
await page.goto(`${gameOrigin}/che/dynasty/1`);
await expect(page.getByText('장 수 성 격')).toBeVisible();
await expect(page.getByText('의리(14), 대담(11)')).toBeVisible();
await expect(page.getByText('오 호 장 군')).toBeVisible();
await expect(page.getByText('건 안 칠 자')).toBeVisible();
await expect(page.getByText('【 백년01 】')).toBeVisible();
await expect(page.getByText('황제', { exact: true })).toBeVisible();
await expect(page.locator('.old-nation-table')).toHaveCount(1);
const geometry = await page.locator('#dynasty-detail-container').evaluate((container) => {
const rect = container.getBoundingClientRect();
const first = container.querySelector<HTMLTableElement>('table')!.getBoundingClientRect();
const emperor = container.querySelector<HTMLTableElement>('.emperor-table')!.getBoundingClientRect();
const oldNation = container.querySelector<HTMLTableElement>('.old-nation-table')!.getBoundingClientRect();
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 },
};
});
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 });
await page.goto(`${gameOrigin}/che/dynasty/999`);
await expect(page.getByRole('alert')).toHaveText('왕조 정보를 찾을 수 없습니다.');
await expect(page.locator('.emperor-table')).toHaveCount(0);
await page.getByRole('button', { name: '창 닫기' }).first().click();
await expect(page).toHaveURL(/\/che\/public$/);
});
test('dynasty public view is identical for anonymous and distinct user tokens', async ({ browser }) => {
const snapshots: string[] = [];
for (const token of [null, 'ga_dynasty_owner_a', 'ga_dynasty_owner_b']) {
const context = await browser.newContext({
viewport: { width: 1280, height: 900 },
deviceScaleFactor: 1,
colorScheme: 'dark',
});
const page = await context.newPage();
await installFixture(page);
if (token) {
await page.addInitScript((value) => {
window.localStorage.setItem('sammo-game-token', value);
}, token);
}
await page.goto(`${gameOrigin}/che/dynasty`);
await expect(page.getByText('백년01 (215年 4月)')).toBeVisible();
snapshots.push(await page.locator('#dynasty-list-container').innerText());
await context.close();
}
expect(snapshots[1]).toBe(snapshots[0]);
expect(snapshots[2]).toBe(snapshots[0]);
});