fix: 정보 화면 버튼을 공통 Lumen 계열로 통일

세력도시·세력정보·현재도시·내 정보·인사부·외교부의 raised control을 공통 semantic button family에 연결한다. 화면별 크기와 색상은 유지하고 실제 Chromium 상태 geometry 회귀를 추가한다.
This commit is contained in:
2026-08-21 17:34:45 +00:00
parent 24afa7f466
commit 71ce057318
13 changed files with 393 additions and 160 deletions
+5 -4
View File
@@ -2,6 +2,7 @@ import { mkdir, writeFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { expect, test, type Page, type Route } from '@playwright/test';
import { expectLumenButtonStates } from './lumenButton.js';
const basePath = `/${(process.env.PLAYWRIGHT_GAME_BASE_PATH ?? 'che').replace(/^\/+|\/+$/g, '')}`;
const artifactRoot = process.env.DIPLOMACY_ARTIFACT_DIR ? resolve(process.env.DIPLOMACY_ARTIFACT_DIR) : null;
@@ -103,7 +104,9 @@ for (const viewport of [
await expect(card.getByRole('link', { name: '자료' })).toHaveAttribute('href', 'https://example.com');
await expect(card.getByRole('link', { name: '자료' })).toHaveAttribute('rel', 'noopener noreferrer nofollow');
await expect(card.locator('.letter-text script, .letter-text svg, .letter-text math')).toHaveCount(0);
await expect(card.locator('.letter-text [onerror], .letter-text [onclick], .letter-text [style]')).toHaveCount(0);
await expect(card.locator('.letter-text [onerror], .letter-text [onclick], .letter-text [style]')).toHaveCount(
0
);
expect(await page.evaluate(() => (globalThis as Record<string, unknown>).__diplomacyXss)).toBeUndefined();
const geometry = await card.evaluate((element) => {
@@ -142,9 +145,7 @@ for (const viewport of [
}
const send = page.getByRole('button', { name: '전송' });
await send.focus();
await expect(send).toBeFocused();
await send.hover();
await expectLumenButtonStates(page, send, 'rgb(55, 90, 127)');
await screenshot(page, `diplomacy-html-${basePath.slice(1)}-${viewport.name}.png`);
});
}
+8
View File
@@ -2,6 +2,7 @@ import { expect, test, type Page, type Route } from '@playwright/test';
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { expectLumenButtonStates } from './lumenButton.js';
const response = (data: unknown) => ({ result: { data } });
const artifactRoot = process.env.CITY_PARITY_ARTIFACT_DIR;
@@ -493,6 +494,13 @@ test('four legacy menu pages keep the 1000px desktop table contract', async ({ p
.first()
.evaluate((el) => getComputedStyle(el).borderCollapse)
).toBe(borderCollapse);
if (path === 'nation/info' || path === 'current-city') {
await expectLumenButtonStates(
page,
page.getByRole('button', { name: '돌아가기' }).first(),
'rgb(0, 88, 44)'
);
}
if (path === 'nation/info') {
await expect(page.locator(selector)).toContainText('작 위호족');
await expect(page.locator(selector)).not.toContainText('작 위1');
@@ -2,6 +2,7 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises';
import { basename, resolve } from 'node:path';
import { expect, test, type Locator, type Page, type Route } from '@playwright/test';
import { gameBasePath, gameProfile, gameTrpcRoute } from './gameTestPaths.js';
import { expectLumenButtonStates } from './lumenButton.js';
import { touchDrag } from './touchDrag.js';
const response = (data: unknown) => ({ result: { data } });
@@ -1320,6 +1321,7 @@ test('내 정보&설정 keeps desktop density and becomes a 390px horizontal-ide
expect(desktop.customCssHeight).toBe(150);
expect(desktop.backgroundImage).toContain('back_walnut.jpg');
expect(desktop.sectionBackgroundImage).toContain('back_green.jpg');
await expectLumenButtonStates(page, page.locator('#set_my_setting'), 'rgb(34, 85, 0)');
await persistParityArtifact(page, 'core-my-page-desktop', desktop);
const defenceSelect = page.locator('select').filter({ has: page.locator('option[value="999"]') });
+66
View File
@@ -0,0 +1,66 @@
import { expect, type Locator, type Page } from '@playwright/test';
type ButtonGeometry = {
top: number;
bottom: number;
height: number;
marginTop: string;
borderBottomWidth: string;
borderRadius: string;
backgroundColor: string;
};
const measure = (control: Locator): Promise<ButtonGeometry> =>
control.evaluate((element) => {
const rect = element.getBoundingClientRect();
const style = getComputedStyle(element);
return {
top: rect.top,
bottom: rect.bottom,
height: rect.height,
marginTop: style.marginTop,
borderBottomWidth: style.borderBottomWidth,
borderRadius: style.borderRadius,
backgroundColor: style.backgroundColor,
};
});
export const expectLumenButtonStates = async (
page: Page,
control: Locator,
expectedBackground: string
): Promise<{ base: ButtonGeometry; hover: ButtonGeometry; active: ButtonGeometry }> => {
await expect(control).toBeVisible();
await page.mouse.move(0, 0);
const base = await measure(control);
expect(base).toMatchObject({
marginTop: '0px',
borderBottomWidth: '4px',
borderRadius: '5.25px',
backgroundColor: expectedBackground,
});
await control.hover();
const hover = await measure(control);
expect(hover).toMatchObject({ marginTop: '1px', borderBottomWidth: '3px' });
expect(hover.top).toBeCloseTo(base.top + 1, 1);
expect(hover.bottom).toBeCloseTo(base.bottom, 1);
const box = await control.boundingBox();
if (!box) throw new Error('Lumen button is not measurable');
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await page.mouse.down();
const active = await measure(control);
expect(active).toMatchObject({ marginTop: '2px', borderBottomWidth: '2px' });
expect(active.top).toBeCloseTo(base.top + 2, 1);
expect(active.bottom).toBeCloseTo(base.bottom, 1);
await page.mouse.move(0, 0);
await page.mouse.up();
await page.keyboard.press('Tab');
await control.focus();
await expect(control).toBeFocused();
await expect.poll(() => control.evaluate((element) => getComputedStyle(element).boxShadow)).not.toBe('none');
return { base, hover, active };
};
@@ -1,6 +1,7 @@
import { expect, test, type Page, type Route } from '@playwright/test';
import { gameProfile, gameTrpcRoute } from './gameTestPaths.js';
import { expectLumenButtonStates } from './lumenButton.js';
type Role = 'head' | 'member';
type AppointmentInput = { destGeneralId: number; destCityId: number; officerLevel: number };
@@ -352,6 +353,7 @@ test('암행부 행을 도시별로 나누고 수뇌의 인사부 즉시 임명
await expect(page.locator('.nation-cities-page')).toBeVisible();
await expect(page.locator('.city-user-table')).toHaveCount(0);
await expect(page.getByRole('button', { name: '인사부 연동' })).toHaveCount(0);
await expectLumenButtonStates(page, page.getByRole('button', { name: '암행부 연동' }), 'rgb(55, 90, 127)');
const citySort = page.locator('#nation-city-sort');
await expect(citySort).toHaveCSS('background-color', 'rgb(24, 35, 29)');
+3 -1
View File
@@ -3,6 +3,7 @@ import { mkdir, readFile } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { gameProfile, gameTrpcRoute } from './gameTestPaths.js';
import { expectLumenButtonStates } from './lumenButton.js';
type Role = 'leader' | 'head' | 'member';
type FixtureState = {
@@ -290,6 +291,7 @@ test('personnel keeps the desktop frame while exposing row-level appointment con
await page.setViewportSize({ width: 1000, height: 900 });
await gotoOffice(page, 'nation/personnel');
await expect(page.getByText('작위검증국')).toBeVisible();
await expectLumenButtonStates(page, page.locator('.personnel-change-button').first(), 'rgb(49, 91, 61)');
const computed = await page.locator('#personnel-container').evaluate((container) => {
const box = (selector?: string) => {
@@ -339,7 +341,7 @@ test('personnel keeps the desktop frame while exposing row-level appointment con
await changeButton.hover();
expect(await changeButton.evaluate((button) => getComputedStyle(button).cursor)).toBe('pointer');
await changeButton.focus();
expect(await changeButton.evaluate((button) => getComputedStyle(button).outlineStyle)).not.toBe('none');
expect(await changeButton.evaluate((button) => getComputedStyle(button).boxShadow)).not.toBe('none');
await expect(page.getByRole('button', { name: '허창 태수 변경하기', exact: true })).toBeVisible();
await expect(page.getByRole('button', { name: '허창 군사 변경하기', exact: true })).toHaveCount(0);
await screenshot(page, 'core-personnel-desktop-leader.png');