fix(game-ui): 공통 Lumen 버튼 모양을 통일한다

상하단 이동·갱신 제어와 장수 목록, 토너먼트, 명령 선택을 공통 semantic button family로 연결한다.

고정 행은 공통 fixed-height 변형이 hover와 active 높이를 보정하게 하고 실제 Chromium geometry 회귀를 추가한다.
This commit is contained in:
2026-08-21 16:31:42 +00:00
parent 31b8b01055
commit aa912a2eed
25 changed files with 628 additions and 377 deletions
+37 -1
View File
@@ -2186,7 +2186,43 @@ test('main cards and command input stay inside their Ref-sized grid slots', asyn
await selectedMenu.evaluate((element) => ((element as HTMLDetailsElement).open = false));
await expect(selectedMenu).not.toHaveAttribute('open', '');
await page.locator('[data-main-target="commands"] .select-command').click();
const selectCommand = page.locator('[data-main-target="commands"] .select-command');
await expect(selectCommand).toHaveClass(/legacy-button--info/u);
await page.mouse.move(1, 1);
const measureSelectCommand = () =>
selectCommand.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,
};
});
const selectDefault = await measureSelectCommand();
expect(selectDefault).toMatchObject({
height: 34,
marginTop: '0px',
borderBottomWidth: '4px',
borderRadius: '5.25px',
backgroundColor: 'rgb(52, 152, 219)',
});
await selectCommand.hover();
const selectHover = await measureSelectCommand();
expect(selectHover).toMatchObject({ height: 33, marginTop: '1px', borderBottomWidth: '3px' });
expect(selectHover.bottom).toBeCloseTo(selectDefault.bottom, 2);
const selectBox = await selectCommand.boundingBox();
if (!selectBox) throw new Error('select command control is not measurable');
await page.mouse.move(selectBox.x + selectBox.width / 2, selectBox.y + selectBox.height / 2);
await page.mouse.down();
const selectActive = await measureSelectCommand();
expect(selectActive).toMatchObject({ height: 32, marginTop: '2px', borderBottomWidth: '2px' });
expect(selectActive.bottom).toBeCloseTo(selectDefault.bottom, 2);
await page.mouse.up();
const picker = page.getByTestId('command-picker');
await expect(picker).toBeVisible();
// The trigger can end up directly above a newly opened category button.
@@ -205,6 +205,86 @@ test('nation generals keeps the 1000px legacy grid and redacted member columns',
await expect(page.locator('#nation-general-list')).toContainText('?');
});
test('nation generals top controls share fixed Lumen state geometry on desktop and mobile', async ({
page,
}, testInfo) => {
await install(page);
const evidence: Record<string, unknown> = {};
for (const viewport of [
{ width: 1200, height: 900 },
{ width: 500, height: 900 },
]) {
await page.setViewportSize(viewport);
await page.goto('nation/generals');
await expect(page.locator('#nation-general-list')).toBeVisible();
const controls = [
page.getByRole('button', { name: '돌아가기' }),
page.getByRole('button', { name: '갱신' }),
page.getByRole('button', { name: '보기 모드⌄' }),
page.getByRole('button', { name: '열 선택⌄' }),
];
const viewportEvidence: Record<string, unknown> = {};
for (const control of controls) {
const label = (await control.textContent())?.trim() ?? 'unknown';
await expect(control).toHaveClass(/legacy-button--fixed-height/u);
const measure = () =>
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,
fontFamily: style.fontFamily,
fontSize: style.fontSize,
};
});
await page.mouse.move(viewport.width - 1, viewport.height - 1);
const base = await measure();
expect(base).toMatchObject({
height: 32,
marginTop: '0px',
borderBottomWidth: '4px',
borderRadius: '5.25px',
fontSize: '14px',
});
expect(base.fontFamily).toContain('Pretendard');
await control.hover();
const hover = await measure();
expect(hover).toMatchObject({ height: 31, marginTop: '1px', borderBottomWidth: '3px' });
expect(hover.bottom).toBeCloseTo(base.bottom, 2);
const box = await control.boundingBox();
if (!box) throw new Error(`${label} control 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();
expect(active).toMatchObject({ height: 30, marginTop: '2px', borderBottomWidth: '2px' });
expect(active.bottom).toBeCloseTo(base.bottom, 2);
await page.mouse.move(viewport.width - 1, viewport.height - 1);
await page.mouse.up();
viewportEvidence[label] = { default: base, hover, active };
}
evidence[`${viewport.width}x${viewport.height}`] = viewportEvidence;
await page.screenshot({
path: testInfo.outputPath(`nation-general-buttons-${viewport.width}.png`),
fullPage: true,
});
}
await testInfo.attach('nation-general-button-geometry', {
body: JSON.stringify(evidence, null, 2),
contentType: 'application/json',
});
});
test('nation generals restores Ref group, saved view, sort, and Korean search behavior', async ({ page }, testInfo) => {
await install(page);
await page.setViewportSize({ width: 1200, height: 900 });
@@ -405,17 +485,20 @@ test('secret office renders five Ref-style command briefs and the forbidden erro
'5 : 휴식',
]);
await expect(commandRows.nth(2)).toHaveAttribute('title', '【다른장수】에게 쌀 200을 증여');
const geometry = await page.locator('#secret-general-list .turns').first().evaluate((element) => {
const rect = element.getBoundingClientRect();
const style = getComputedStyle(element);
return {
width: rect.width,
height: rect.height,
fontSize: style.fontSize,
textAlign: style.textAlign,
horizontalOverflow: element.scrollWidth - element.clientWidth,
};
});
const geometry = await page
.locator('#secret-general-list .turns')
.first()
.evaluate((element) => {
const rect = element.getBoundingClientRect();
const style = getComputedStyle(element);
return {
width: rect.width,
height: rect.height,
fontSize: style.fontSize,
textAlign: style.textAlign,
horizontalOverflow: element.scrollWidth - element.clientWidth,
};
});
expect(geometry.width).toBeGreaterThanOrEqual(190);
expect(geometry.width).toBeLessThanOrEqual(230);
expect(geometry.height).toBeGreaterThanOrEqual(60);
@@ -395,7 +395,10 @@ test('join refresh shows the assigned preliminary group immediately with accessi
await refresh.focus();
await expect(refresh).toBeFocused();
await refresh.hover();
await expect(refresh).toHaveCSS('filter', 'brightness(1.25)');
await expect(refresh).toHaveCSS('filter', 'none');
await expect(refresh).toHaveCSS('height', '43px');
await expect(refresh).toHaveCSS('margin-top', '1px');
await expect(refresh).toHaveCSS('border-bottom-width', '3px');
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(390);
});
@@ -602,7 +605,11 @@ test('mobile betting rankings use tabs and keep dedicated icons beside general n
await expect(dialog.getByText('예상 환수금 280')).toBeVisible();
await dialog.getByLabel('베팅 금액').selectOption('50');
await expect(dialog.getByText('예상 환수금 1,400')).toBeVisible();
await persistScreenshot(page, 'tournament-betting-dialog-mobile', testInfo.outputPath('betting-dialog-mobile.webp'));
await persistScreenshot(
page,
'tournament-betting-dialog-mobile',
testInfo.outputPath('betting-dialog-mobile.webp')
);
await dialog.getByRole('button', { name: '베팅 등록' }).click();
await expect(dialog).not.toBeVisible();
await expect(page.getByRole('status')).toHaveText('베팅이 등록되었습니다.');