feat(game-ui): 토너먼트와 베팅 메뉴를 합치고 화면 설정을 분리한다

토너먼트 기본 동작과 베팅 선택을 split 메뉴로 묶고 빈 국가 메뉴 칸에 화면 설정을 배치한다. 내 정보 화면에는 게임 상태 설정만 남기며 화면 폭, 모바일 패널 순서, 개인 CSS는 기기별 전용 화면에서 관리한다.
This commit is contained in:
2026-08-21 23:09:07 +00:00
parent 6537bcc5c1
commit bd894398b5
11 changed files with 567 additions and 768 deletions
+72 -10
View File
@@ -22,6 +22,13 @@ const persistParityArtifact = async (page: Page, name: string, geometry: unknown
]);
};
const waitForVisualAssets = async (page: Page): Promise<void> => {
await page.waitForLoadState('networkidle');
await page.evaluate(async () => {
await document.fonts.ready;
});
};
const readGeneralPanelImages = async (panel: Locator) =>
panel.evaluate((element) =>
[...element.querySelectorAll<HTMLElement>('.general-image')].map((image) => {
@@ -1270,6 +1277,9 @@ test('내 정보&설정 keeps desktop density and becomes a 390px horizontal-ide
await expect(page.locator('#container')).not.toContainText('che_');
await expect(page.locator('.title-row')).toContainText('내 정 보');
await expect(page.locator('#set_my_setting')).toBeVisible();
await expect(page.getByRole('radiogroup', { name: '화면 폭 모드' })).toHaveCount(0);
await expect(page.getByRole('button', { name: '순서 바꾸기', exact: true })).toHaveCount(0);
await expect(page.locator('#custom_css')).toHaveCount(0);
await expect(page.locator('.general-column [role="progressbar"]')).toHaveCount(14);
await expect(page.locator('.general-column [aria-label*="1,275,975 (EX+)"]')).toHaveCount(5);
await expect.poll(() => state.generalMeQueries).toBeGreaterThan(0);
@@ -1290,7 +1300,6 @@ test('내 정보&설정 keeps desktop density and becomes a 390px horizontal-ide
const settings = element.querySelector<HTMLElement>('.settings-column')!.getBoundingClientRect();
const saveButton = element.querySelector<HTMLElement>('#set_my_setting')!;
const save = saveButton.getBoundingClientRect();
const customCss = element.querySelector<HTMLElement>('#custom_css')!.getBoundingClientRect();
const columns = getComputedStyle(element.querySelector('.top-grid')!).gridTemplateColumns;
return {
width: rect.width,
@@ -1302,8 +1311,6 @@ test('내 정보&설정 keeps desktop density and becomes a 390px horizontal-ide
saveWidth: save.width,
saveHeight: save.height,
saveBackground: getComputedStyle(saveButton).backgroundColor,
customCssWidth: customCss.width,
customCssHeight: customCss.height,
backgroundImage: getComputedStyle(element).backgroundImage,
sectionBackgroundImage: getComputedStyle(element.querySelector('.section-title')!).backgroundImage,
};
@@ -1317,8 +1324,6 @@ test('내 정보&설정 keeps desktop density and becomes a 390px horizontal-ide
expect(desktop.saveWidth).toBe(160);
expect(desktop.saveHeight).toBe(30);
expect(desktop.saveBackground).toBe('rgb(34, 85, 0)');
expect(desktop.customCssWidth).toBe(420);
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)');
@@ -1583,11 +1588,66 @@ test('내 정보&설정의 지난 플레이는 기본 탐색과 분리되어 오
}
});
test('내 정보&설정에서 모바일 메인 패널을 드래그하거나 버튼으로 재정렬하고 기본 순서로 복원한다', async ({ page }) => {
test('화면 설정에서 화면 폭과 개인 CSS를 저장하고 게임 설정 API는 호출하지 않는다', async ({ page }, testInfo) => {
const state: FixtureState = { permission: 'head', myset: 3, settingMutations: [], accessPages: [] };
await install(page, state);
await page.setViewportSize({ width: 1000, height: 900 });
await page.goto('my-settings');
await waitForVisualAssets(page);
await expect(page.locator('.title-row')).toContainText('화 면 설 정');
await expect(page.locator('.scope-note')).toContainText('게임 상태에는 영향을 주지 않습니다.');
await expect(page.locator('#set_my_setting')).toHaveCount(0);
await expect(page.locator('#custom_css')).toBeVisible();
expect(state.settingMutations).toHaveLength(0);
const desktop = await page.locator('#interface-settings').evaluate((element) => {
const rect = element.getBoundingClientRect();
const css = element.querySelector<HTMLTextAreaElement>('#custom_css')!.getBoundingClientRect();
return {
width: rect.width,
columns: getComputedStyle(element.querySelector('.settings-grid')!).gridTemplateColumns,
cssWidth: css.width,
cssHeight: css.height,
backgroundImage: getComputedStyle(element).backgroundImage,
sectionBackgroundImage: getComputedStyle(element.querySelector('.section-title')!).backgroundImage,
};
});
expect(desktop.width).toBe(1000);
expect(desktop.columns.split(' ')).toHaveLength(2);
expect(desktop.cssWidth).toBe(420);
expect(desktop.cssHeight).toBe(150);
expect(desktop.backgroundImage).toContain('back_walnut.jpg');
expect(desktop.sectionBackgroundImage).toContain('back_green.jpg');
await page.screenshot({ path: testInfo.outputPath('interface-settings-desktop.png'), fullPage: true });
await page.getByRole('radio', { name: '500px' }).check();
await expect.poll(() => page.evaluate(() => localStorage.getItem('sam.screenMode'))).toBe('500px');
await expect(page.locator('meta[name="viewport"]')).toHaveAttribute('content', 'width=500');
const cssText = '#interface-settings { --ui-settings-e2e: 23px; }';
await page.getByLabel('개인용 CSS').fill(cssText);
await expect(page.locator('.custom-css span')).toHaveText('(저장 중)');
await expect.poll(() => page.evaluate(() => localStorage.getItem('sam_customCSS'))).toBe(cssText);
await expect.poll(() => page.locator('#sammo-custom-css').textContent()).toBe(cssText);
await page.reload();
await expect.poll(() => page.locator('#sammo-custom-css').textContent()).toBe(cssText);
expect(state.settingMutations).toHaveLength(0);
await page.getByLabel('개인용 CSS').fill('');
await expect.poll(() => page.evaluate(() => localStorage.getItem('sam_customCSS'))).toBe('');
await page.getByRole('radio', { name: '자동' }).check();
await persistParityArtifact(page, 'core-interface-settings-desktop', desktop);
});
test('화면 설정에서 모바일 메인 패널을 드래그하거나 버튼으로 재정렬하고 기본 순서로 복원한다', async ({
page,
}, testInfo) => {
const state: FixtureState = { permission: 'head', myset: 3, settingMutations: [], accessPages: [] };
await install(page, state);
await page.setViewportSize({ width: 390, height: 844 });
await page.goto('my-page');
await page.goto('my-settings');
await waitForVisualAssets(page);
await page.getByRole('button', { name: '순서 바꾸기', exact: true }).click();
const dialog = page.getByRole('dialog', { name: '모바일 레이아웃 순서 바꾸기' });
@@ -1637,7 +1697,8 @@ test('내 정보&설정에서 모바일 메인 패널을 드래그하거나 버
expect(dialogGeometry.firstItem?.height).toBeGreaterThanOrEqual(44);
expect(dialogGeometry.moveButton?.width).toBeGreaterThanOrEqual(36);
expect(dialogGeometry.documentWidth).toBe(390);
await persistParityArtifact(page, 'core-my-page-mobile-layout-order-dialog', dialogGeometry);
await dialog.screenshot({ path: testInfo.outputPath('interface-settings-mobile-layout-dialog.png') });
await persistParityArtifact(page, 'core-interface-settings-mobile-layout-order-dialog', dialogGeometry);
await dialog.getByRole('button', { name: '적용', exact: true }).click();
await expect(dialog).toBeHidden();
@@ -1654,7 +1715,7 @@ test('내 정보&설정에서 모바일 메인 패널을 드래그하거나 버
.toEqual(defaultOrder);
});
test('실제 모바일 터치로 메인 패널 순서를 재정렬한다', async ({ browser }, testInfo) => {
test('화면 설정에서 실제 모바일 터치로 메인 패널 순서를 재정렬한다', async ({ browser }, testInfo) => {
const configuredBaseUrl = testInfo.project.use.baseURL;
if (typeof configuredBaseUrl !== 'string') {
throw new Error('Playwright baseURL is required for the mobile touch contract');
@@ -1672,7 +1733,8 @@ test('실제 모바일 터치로 메인 패널 순서를 재정렬한다', async
try {
const state: FixtureState = { permission: 'head', myset: 3, settingMutations: [], accessPages: [] };
await install(mobilePage, state);
await mobilePage.goto('my-page');
await mobilePage.goto('my-settings');
await waitForVisualAssets(mobilePage);
await mobilePage.getByRole('button', { name: '순서 바꾸기', exact: true }).click();
const dialog = mobilePage.getByRole('dialog', { name: '모바일 레이아웃 순서 바꾸기' });
+33 -6
View File
@@ -1260,7 +1260,27 @@ test('desktop menus preserve ref columns, prefix-safe routes, and controlled dro
await expect(global.locator('[data-navigation-id="board-community"]')).toHaveAttribute('href', '/xe/community');
await expect(global.locator('[data-navigation-id="official-chat"]')).toHaveAttribute('target', '_blank');
await expect(global.locator('[data-navigation-id="survey"]')).toHaveClass(/highlight/);
await expect(page.locator('.main-nation-menu [data-navigation-id="tournament"]')).toHaveClass(/highlight/);
const nationMenu = page.locator('.main-nation-menu:visible');
await expect(nationMenu.locator(':scope > *')).toHaveCount(20);
const tournamentMain = nationMenu.locator('[data-navigation-id="tournament"]');
const tournamentToggle = nationMenu.locator('[data-menu-id="tournament-betting"]');
await expect(tournamentMain).toHaveClass(/highlight/);
await expect(tournamentMain).toHaveAttribute('href', `${basePath}/tournament`);
await expect(tournamentToggle).toHaveAttribute('aria-expanded', 'false');
await expect(nationMenu.locator('[data-navigation-id="my-settings"]')).toHaveAttribute(
'href',
`${basePath}/my-settings`
);
await tournamentToggle.click();
await expect(tournamentToggle).toHaveAttribute('aria-expanded', 'true');
await expect(nationMenu.locator('#nation-menu-tournament-betting [data-navigation-id="tournament-menu"]')).toHaveText(
'토너먼트'
);
await expect(nationMenu.locator('#nation-menu-tournament-betting [data-navigation-id="betting"]')).toHaveAttribute(
'href',
`${basePath}/betting`
);
await page.keyboard.press('Escape');
const gameInfoButton = global.locator('[data-menu-id="game-info"]');
const bettingButton = global.locator('[data-navigation-id="nation-betting"]');
@@ -1455,7 +1475,7 @@ test('nation split buttons keep square inner corners and a single divider in eve
officerLevel: 5,
permission: 2,
nationLevel: 3,
stage: 1,
stage: 6,
npcMode: 1,
scenarioTitle: '분할 버튼 이음새 검증 시나리오',
generalMeCalls: 0,
@@ -1515,14 +1535,21 @@ test('nation split buttons keep square inner corners and a single divider in eve
for (const width of [1200, 500]) {
await page.setViewportSize({ width, height: 900 });
await waitForMain(page);
const nationSplit = page.locator('.main-nation-menu:visible .nation-menu-split').first();
const pairs: Array<[string, Locator, Locator]> = [
[
'nation',
nationSplit.locator('[data-navigation-id="auction-resource"]'),
nationSplit.locator('[data-menu-id="auction"]'),
'tournament',
page.locator('.main-nation-menu:visible [data-navigation-id="tournament"]'),
page.locator('.main-nation-menu:visible [data-menu-id="tournament-betting"]'),
],
[
'auction',
page.locator('.main-nation-menu:visible [data-navigation-id="auction-resource"]'),
page.locator('.main-nation-menu:visible [data-menu-id="auction"]'),
],
];
await expect(page.locator('.main-nation-menu:visible [data-navigation-id="tournament"]')).toHaveClass(
/highlight/
);
for (const [label, main, toggle] of pairs) {
await expect(main).toBeVisible();