merge: 최신 main을 토너먼트 새 창 닫기에 통합한다

This commit is contained in:
2026-08-22 05:00:50 +00:00
15 changed files with 789 additions and 782 deletions
+74 -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,68 @@ 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.getByText('이 설정은 이 기기의 화면 표시만 바꾸며 게임 상태에는 영향을 주지 않습니다.')
).toHaveCount(0);
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 +1699,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 +1717,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 +1735,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: '모바일 레이아웃 순서 바꾸기' });
+137 -12
View File
@@ -29,6 +29,8 @@ type NavigationFixture = {
permission: number;
nationLevel: number;
stage: number;
tournamentType?: 0 | 1 | 2 | 3;
tournamentWinnerId?: number;
npcMode: number;
generalMeCalls: number;
operations: string[];
@@ -758,7 +760,16 @@ const installFixture = async (page: Page, state: NavigationFixture) => {
canSecret: state.permission >= 2,
});
}
if (operation === 'tournament.getState') return response({ stage: state.stage });
if (operation === 'tournament.getState') {
if (state.stage === 0 && state.tournamentType === undefined && state.tournamentWinnerId === undefined) {
return response(null);
}
return response({
stage: state.stage,
type: state.tournamentType ?? 0,
winnerId: state.tournamentWinnerId,
});
}
return response({ ok: true });
});
operations.forEach((operation, index) => {
@@ -1260,7 +1271,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"]');
@@ -1417,6 +1448,95 @@ test('shows the persisted official game index beside the scenario title without
}
});
test('tournament split main action follows recruitment, betting, finals, and tournament type on desktop and mobile', async ({
page,
}, testInfo) => {
const state: NavigationFixture = {
officerLevel: 5,
permission: 2,
nationLevel: 3,
stage: 1,
tournamentType: 3,
npcMode: 1,
scenarioTitle: '토너먼트 동적 메뉴 검증 시나리오',
generalMeCalls: 0,
operations: [],
};
await installFixture(page, state);
const cases = [
{ stage: 1, type: 3 as const, label: '설 전', route: '/tournament' },
{ stage: 5, type: 2 as const, label: '일 기 토', route: '/tournament' },
{ stage: 6, type: 2 as const, label: '베 팅 장', route: '/betting' },
{ stage: 7, type: 2 as const, label: '일 기 토', route: '/tournament' },
{ stage: 10, type: 3 as const, label: '설 전', route: '/tournament' },
{ stage: 0, type: 3 as const, label: '설 전', route: '/tournament', winnerId: 17 },
];
for (const viewport of [
{ width: 1200, height: 900 },
{ width: 500, height: 900 },
]) {
await page.setViewportSize(viewport);
for (const lifecycle of cases) {
state.stage = lifecycle.stage;
state.tournamentType = lifecycle.type;
state.tournamentWinnerId = lifecycle.winnerId;
if (page.url() === 'about:blank') {
await waitForMain(page);
} else {
await page.reload();
await waitForMain(page);
}
const nationMenu = page.locator('.main-nation-menu:visible');
const main = nationMenu.locator('[data-navigation-id="tournament"]');
await expect(main).toHaveText(lifecycle.label);
await expect(main).toHaveAttribute('href', `${basePath}${lifecycle.route}`);
await expect(main).toHaveClass(/highlight/);
const toggle = nationMenu.locator('[data-menu-id="tournament-betting"]');
await toggle.click();
const tournamentItem = nationMenu.locator(
'#nation-menu-tournament-betting [data-navigation-id="tournament-menu"]'
);
const bettingItem = nationMenu.locator('#nation-menu-tournament-betting [data-navigation-id="betting"]');
if (lifecycle.stage === 6) {
await expect(tournamentItem).not.toHaveClass(/highlight/);
await expect(bettingItem).toHaveClass(/highlight/);
} else {
await expect(tournamentItem).toHaveClass(/highlight/);
await expect(bettingItem).not.toHaveClass(/highlight/);
}
await page.keyboard.press('Escape');
if (viewport.width === 500) {
const nationTrigger = page.locator('[data-bottom-menu="nation"]');
await nationTrigger.click();
const mobileTournament = page.locator('#mobile-nation-menu [data-navigation-id="tournament-menu"]');
const mobileBetting = page.locator('#mobile-nation-menu [data-navigation-id="betting"]');
if (lifecycle.stage === 6) {
await expect(mobileTournament).not.toHaveClass(/highlight/);
await expect(mobileBetting).toHaveClass(/highlight/);
} else {
await expect(mobileTournament).toHaveClass(/highlight/);
await expect(mobileBetting).not.toHaveClass(/highlight/);
}
await page.keyboard.press('Escape');
}
}
await page
.locator('.main-nation-menu:visible [data-menu-id="tournament-betting"]')
.locator('..')
.screenshot({
path: artifactRoot
? resolve(artifactRoot, `tournament-dynamic-main-${viewport.width}.png`)
: testInfo.outputPath(`tournament-dynamic-main-${viewport.width}.png`),
});
}
});
test('shows game index zero for a profile whose first game starts at zero', async ({ page }) => {
const state: NavigationFixture = {
officerLevel: 5,
@@ -1455,7 +1575,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 +1635,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();
@@ -3959,9 +4086,7 @@ for (const viewport of [
await picker.getByLabel('장비 종류', { exact: true }).selectOption('weapon');
const equipment = picker.getByLabel('장비', { exact: true });
await equipment.selectOption('청룡언월도');
const optionLabelBeforeRefresh = await equipment
.locator('option[value="청룡언월도"]')
.textContent();
const optionLabelBeforeRefresh = await equipment.locator('option[value="청룡언월도"]').textContent();
await equipment.evaluate((element) => {
const select = element as HTMLSelectElement;
const valueDescriptor = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, 'value');
@@ -4091,8 +4216,8 @@ test('keeps an Android Chromium native command select untouched while a turn sig
await waitForMain(mobilePage);
await expect
.poll(() =>
mobilePage.evaluate(
() => (window as unknown as { __hasMainRealtime: () => boolean }).__hasMainRealtime()
mobilePage.evaluate(() =>
(window as unknown as { __hasMainRealtime: () => boolean }).__hasMainRealtime()
)
)
.toBe(true);