Merge branch 'main' into feature/general-icon-names
This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
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]);
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
import { chromium } from '@playwright/test';
|
||||
|
||||
const baseUrl = process.env.DYNASTY_REF_URL ?? 'http://127.0.0.1:3410/hwe';
|
||||
const detailId = process.env.DYNASTY_REF_DETAIL_ID ?? '1';
|
||||
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const page = await browser.newPage({
|
||||
viewport: { width: 1280, height: 900 },
|
||||
deviceScaleFactor: 1,
|
||||
colorScheme: 'dark',
|
||||
locale: 'ko-KR',
|
||||
});
|
||||
|
||||
const measure = async (url) => {
|
||||
await page.goto(url, { waitUntil: 'networkidle' });
|
||||
return page.evaluate(() => {
|
||||
const tables = Array.from(document.querySelectorAll('table')).map((table) => {
|
||||
const rect = table.getBoundingClientRect();
|
||||
const style = getComputedStyle(table);
|
||||
return {
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
backgroundImage: style.backgroundImage,
|
||||
borderSpacing: style.borderSpacing,
|
||||
fontFamily: style.fontFamily,
|
||||
fontSize: style.fontSize,
|
||||
lineHeight: style.lineHeight,
|
||||
};
|
||||
});
|
||||
const button = document.querySelector('button');
|
||||
const buttonStyle = button ? getComputedStyle(button) : null;
|
||||
const firstCell = document.querySelector('td');
|
||||
const firstCellStyle = firstCell ? getComputedStyle(firstCell) : null;
|
||||
return {
|
||||
chromium: navigator.userAgent,
|
||||
body: {
|
||||
width: document.body.getBoundingClientRect().width,
|
||||
minWidth: getComputedStyle(document.body).minWidth,
|
||||
margin: getComputedStyle(document.body).margin,
|
||||
fontFamily: getComputedStyle(document.body).fontFamily,
|
||||
fontSize: getComputedStyle(document.body).fontSize,
|
||||
lineHeight: getComputedStyle(document.body).lineHeight,
|
||||
},
|
||||
tables,
|
||||
button: buttonStyle
|
||||
? {
|
||||
height: button.getBoundingClientRect().height,
|
||||
borderWidth: buttonStyle.borderWidth,
|
||||
borderRadius: buttonStyle.borderRadius,
|
||||
backgroundColor: buttonStyle.backgroundColor,
|
||||
color: buttonStyle.color,
|
||||
padding: buttonStyle.padding,
|
||||
fontFamily: buttonStyle.fontFamily,
|
||||
fontSize: buttonStyle.fontSize,
|
||||
lineHeight: buttonStyle.lineHeight,
|
||||
cursor: buttonStyle.cursor,
|
||||
}
|
||||
: null,
|
||||
firstCell: firstCellStyle
|
||||
? {
|
||||
padding: firstCellStyle.padding,
|
||||
borderWidth: firstCellStyle.borderWidth,
|
||||
textAlign: firstCellStyle.textAlign,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const output = {
|
||||
list: await measure(`${baseUrl}/a_emperior.php`),
|
||||
detail: await measure(`${baseUrl}/a_emperior_detail.php?select=${detailId}`),
|
||||
};
|
||||
|
||||
if (process.env.FRONTEND_PARITY_ARTIFACT_DIR) {
|
||||
await page.goto(`${baseUrl}/a_emperior.php`, { waitUntil: 'networkidle' });
|
||||
await page.screenshot({
|
||||
path: `${process.env.FRONTEND_PARITY_ARTIFACT_DIR}/dynasty-ref-list.png`,
|
||||
fullPage: true,
|
||||
});
|
||||
if (process.env.DYNASTY_REF_CAPTURE_DETAIL === '1') {
|
||||
await page.goto(`${baseUrl}/a_emperior_detail.php?select=${detailId}`, { waitUntil: 'networkidle' });
|
||||
await page.screenshot({
|
||||
path: `${process.env.FRONTEND_PARITY_ARTIFACT_DIR}/dynasty-ref-detail.png`,
|
||||
fullPage: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
process.stdout.write(`${JSON.stringify(output, null, 2)}\n`);
|
||||
await browser.close();
|
||||
@@ -5,6 +5,7 @@ import { defineConfig, devices } from '@playwright/test';
|
||||
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
|
||||
const gatewayPort = process.env.FRONTEND_PARITY_GATEWAY_PORT ?? '15100';
|
||||
const gamePort = process.env.FRONTEND_PARITY_GAME_PORT ?? '15102';
|
||||
const reuseExistingServer = process.env.FRONTEND_PARITY_REUSE_SERVER === '1';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: '.',
|
||||
@@ -14,6 +15,7 @@ export default defineConfig({
|
||||
'instant-diplomacy-message.spec.ts',
|
||||
'ingame-message-parity.spec.ts',
|
||||
'tournament-betting.spec.ts',
|
||||
'dynasty-parity.spec.ts',
|
||||
'inheritance-management.spec.ts',
|
||||
],
|
||||
fullyParallel: false,
|
||||
@@ -41,14 +43,14 @@ export default defineConfig({
|
||||
command: `VITE_APP_BASE_PATH=/gateway VITE_GATEWAY_API_URL=/gateway/api/trpc VITE_GAME_API_URL_TEMPLATE=/{profile}/api/trpc VITE_GAME_ASSET_URL=/image pnpm --filter @sammo-ts/gateway-frontend dev --host 127.0.0.1 --port ${gatewayPort}`,
|
||||
cwd: repositoryRoot,
|
||||
url: `http://127.0.0.1:${gatewayPort}/gateway/`,
|
||||
reuseExistingServer: false,
|
||||
reuseExistingServer,
|
||||
timeout: 120_000,
|
||||
},
|
||||
{
|
||||
command: `VITE_APP_BASE_PATH=/che VITE_GAME_API_URL=/che/api/trpc VITE_GAME_ASSET_URL=/image VITE_GAME_PROFILE=che VITE_GATEWAY_WEB_URL=/gateway/ pnpm --filter @sammo-ts/game-frontend dev --host 127.0.0.1 --port ${gamePort}`,
|
||||
cwd: repositoryRoot,
|
||||
url: `http://127.0.0.1:${gamePort}/che/`,
|
||||
reuseExistingServer: false,
|
||||
reuseExistingServer,
|
||||
timeout: 120_000,
|
||||
},
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user