merge: 최신 메인을 예약 명령 표시 수정에 통합

This commit is contained in:
2026-08-17 10:49:18 +00:00
23 changed files with 639 additions and 153 deletions
@@ -58,7 +58,7 @@ const simulatorOptions = {
{ id: 200, name: '궁병', armType: 2 },
],
},
nationTypes: [{ key: 'che_중립', name: '중립', info: '특별한 효과 없음' }],
nationTypes: [{ key: 'che_도적', name: '도적', info: '금 수입 증가, 쌀 수입 감소' }],
eventDomesticTraits: [{ key: 'che_event_신산', name: '신산', info: '계략 강화' }],
warTraits: [{ key: 'che_필살', name: '필살', info: '필살 확률 증가' }],
personalities: [{ key: 'che_대담', name: '대담', info: '공격적인 성격' }],
@@ -356,6 +356,10 @@ test('operates independent/game presets, imports my general, and renders battle
await page.setViewportSize({ width: 1280, height: 900 });
await gotoSimulator(page);
const nationTypeSelects = page.locator('[data-parity-id="attacker-nation"] select').first();
await expect(nationTypeSelects).toHaveValue('che_도적');
await expect(nationTypeSelects.locator('option[value="che_중립"]')).toHaveCount(0);
const notice = page.getByLabel('시뮬레이터 데이터 안내');
const noticeRect = await notice.boundingBox();
expect(noticeRect?.width).toBeLessThan(100);
+56 -1
View File
@@ -55,7 +55,7 @@ const inputOptions = {
],
crewTypes: [{ value: 1100, label: '보병' }],
armTypes: [{ value: 1, label: '보병' }],
nationTypes: [{ value: 'che_중립', label: '중립' }],
nationTypes: [{ value: 'che_도적', label: '도적', description: '금 수입 증가, 쌀 수입 감소' }],
colors: [{ value: 0, label: '색상 1', color: '#ff0000' }],
items: { horse: [{ value: 'None', label: '판매/해제' }] },
recruitment: {
@@ -590,6 +590,61 @@ test('renders and accepts every Ref strategy command at mobile width', async ({
await picker.screenshot({ path: test.info().outputPath('all-strategy-commands-mobile.png') });
});
test('defaults founding to a Ref-selectable nation trait without exposing the neutral storage trait', async ({
page,
}) => {
const foundingCommandTable = {
general: [
{
category: '국가',
values: [
{
key: 'che_건국',
name: '건국',
reqArg: true,
possible: true,
status: 'needsInput',
inputFields: [
{ key: 'nationName', label: '국가명', kind: 'text', required: true, min: 1, max: 18 },
{
key: 'nationType',
label: '국가 성향',
kind: 'select',
required: true,
optionSource: 'nationTypes',
},
{
key: 'colorType',
label: '국기 색상',
kind: 'select',
required: true,
optionSource: 'colors',
},
],
},
],
},
],
nation: [],
inputOptions,
};
await install(page, false, foundingCommandTable);
await page.setViewportSize({ width: 1200, height: 900 });
await page.goto('/');
await page.getByRole('button', { name: '1턴 명령 입력', exact: true }).click();
const picker = page.getByTestId('command-picker');
await picker.getByRole('button', { name: '국가', exact: true }).click();
await picker.getByRole('button', { name: '건국', exact: true }).click();
const nationType = picker.getByLabel('국가 성향');
await expect(nationType).toHaveValue('che_도적');
await expect(nationType.locator('option[value="che_중립"]')).toHaveCount(0);
await expect(nationType.locator('option')).toHaveText(['도적']);
await nationType.focus();
await expect(nationType).toBeFocused();
await picker.screenshot({ path: test.info().outputPath('founding-selectable-nation-trait-desktop-1200.png') });
});
test('reserves force move, retirement, and resignation from the user command picker', async ({ page }) => {
const specialCommandTable = {
general: [
+72 -11
View File
@@ -327,9 +327,27 @@ const install = async (page: Page, state: FixtureState) => {
const rawRequestBody: unknown = route.request().postData() ? route.request().postDataJSON() : {};
const requestBody =
rawRequestBody && typeof rawRequestBody === 'object' ? (rawRequestBody as Record<string, unknown>) : {};
const queryInputText = new URL(route.request().url()).searchParams.get('input');
let queryInput: Record<string, unknown> = {};
if (queryInputText) {
try {
const parsed: unknown = JSON.parse(queryInputText);
if (parsed && typeof parsed === 'object') {
queryInput = parsed as Record<string, unknown>;
}
} catch {
queryInput = {};
}
}
const results = operations.map((operation, operationIndex) => {
const rawPayload =
requestBody[String(operationIndex)] ?? (operations.length === 1 ? requestBody : undefined);
requestBody[String(operationIndex)] ??
queryInput[String(operationIndex)] ??
(operations.length === 1
? Object.keys(requestBody).length > 0
? requestBody
: queryInput
: undefined);
const payload =
rawPayload && typeof rawPayload === 'object' ? (rawPayload as TrpcRequestPayload) : undefined;
const jsonInput =
@@ -537,10 +555,12 @@ const install = async (page: Page, state: FixtureState) => {
return response(battleCenter(state));
}
if (operation === 'nation.getGeneralLog') {
const type = new URL(route.request().url()).searchParams.get('input')?.includes('generalAction')
? 'generalAction'
: operation;
return response({ type, generalId: 7, logs: [{ id: 1, text: '<Y>감찰 기록</>' }] });
const type =
typeof jsonInput.type === 'string' &&
['generalHistory', 'battleDetail', 'battleResult', 'generalAction'].includes(jsonInput.type)
? jsonInput.type
: 'generalAction';
return response({ type, generalId: 7, logs: [{ id: 1, text: `<Y>${type} 감찰 기록</>` }] });
}
return response({ ok: true });
});
@@ -1310,7 +1330,9 @@ test('감찰부 keeps the selector interaction and shows the permission error pa
await expect(page.locator('.selector-row select').nth(1)).toHaveValue('8');
await page.getByRole('button', { name: '다음 ▶' }).click();
await expect(page.locator('.selector-row select').nth(1)).toHaveValue('7');
await expect(page.locator('.battle-general-name')).toContainText('검증장수 【 간의대부 | 건강 】');
await expect(page.locator('.battle-general-name')).toContainText('검증장수');
await expect(page.locator('.battle-general-name')).toContainText('간의대부');
await expect(page.locator('.battle-general-name')).toContainText('건강');
await expect(page.locator('.battle-general-extra')).toContainText('계급29품관');
await expect(page.locator('.battle-general-card')).toContainText('병종보병');
await expect(page.locator('.battle-general-card')).not.toContainText('che_');
@@ -1321,6 +1343,11 @@ test('감찰부 keeps the selector interaction and shows the permission error pa
expect(battleImages[1]?.backgroundImage).toContain('/game/crewtype1.png');
await expect(page.locator('.battle-general-card [role="progressbar"]')).toHaveCount(14);
await expect(page.locator('.battle-general-card [aria-label*="1,275,975 (EX+)"]')).toHaveCount(5);
await expect(page.locator('.general-meta')).toHaveCount(0);
await expect(page.locator('.battle-general-extra__recent-value')).toHaveText('01-01 00:00');
await expect(page.locator('.battle-general-extra__recent-value')).not.toContainText('2026');
await expect(page.locator('.log-block')).toHaveCount(4);
await expect(page.locator('.log-block[data-log-type="battleResult"]')).toContainText('battleResult 감찰 기록');
expect(
await page
.locator('.battle-general-card [role="progressbar"]')
@@ -1330,7 +1357,11 @@ test('감찰부 keeps the selector interaction and shows the permission error pa
const geometry = await page.locator('.battle-page').evaluate((element) => {
const selector = element.querySelector<HTMLElement>('.selector-row')!;
const controls = [...selector.children].map((child) => (child as HTMLElement).getBoundingClientRect());
const logBlock = element.querySelector<HTMLElement>('.log-block')!.getBoundingClientRect();
const logBlocks = [...element.querySelectorAll<HTMLElement>('.log-block')];
const logBlock = logBlocks[0]!.getBoundingClientRect();
const recentLabel = element.querySelector<HTMLElement>('.battle-general-extra__recent-label')!;
const recentValue = element.querySelector<HTMLElement>('.battle-general-extra__recent-value')!;
const previousStat = recentLabel.previousElementSibling as HTMLElement;
return {
width: element.getBoundingClientRect().width,
fontSize: getComputedStyle(element).fontSize,
@@ -1341,6 +1372,11 @@ test('감찰부 keeps the selector interaction and shows the permission error pa
backgroundImage: getComputedStyle(element).backgroundImage,
generalBackgroundImage: getComputedStyle(element.querySelector<HTMLElement>('.battle-general-card')!)
.backgroundImage,
logBackgroundImages: logBlocks.map((block) => getComputedStyle(block).backgroundImage),
recentLabelTop: recentLabel.getBoundingClientRect().top,
recentValueTop: recentValue.getBoundingClientRect().top,
recentValueWidth: recentValue.getBoundingClientRect().width,
previousStatTop: previousStat.getBoundingClientRect().top,
};
});
expect(geometry.width).toBe(1000);
@@ -1352,16 +1388,41 @@ test('감찰부 keeps the selector interaction and shows the permission error pa
expect(geometry.logBlockWidth).toBeCloseTo(500, 0);
expect(geometry.backgroundImage).toContain('back_walnut.jpg');
expect(geometry.generalBackgroundImage).toContain('back_blue.jpg');
expect(geometry.logBackgroundImages).toHaveLength(4);
expect(geometry.logBackgroundImages.every((background) => background.includes('back_walnut.jpg'))).toBe(true);
expect(geometry.recentLabelTop).toBeCloseTo(geometry.recentValueTop, 0);
expect(geometry.recentLabelTop).toBeGreaterThan(geometry.previousStatTop);
expect(geometry.recentValueWidth).toBeGreaterThan(400);
await persistParityArtifact(page, 'core-battle-center-desktop', geometry);
await page.setViewportSize({ width: 500, height: 900 });
const mobileGeometry = await page.locator('.selector-row').evaluate((element) => ({
columns: getComputedStyle(element).gridTemplateColumns,
controlWidths: [...element.children].map((child) => (child as HTMLElement).getBoundingClientRect().width),
}));
const mobileGeometry = await page.locator('.battle-page').evaluate((element) => {
const selector = element.querySelector<HTMLElement>('.selector-row')!;
const battleResult = element.querySelector<HTMLElement>('.log-block[data-log-type="battleResult"]')!;
const footer = element.querySelector<HTMLElement>('.battle-footer')!;
const pageRect = element.getBoundingClientRect();
const resultRect = battleResult.getBoundingClientRect();
const footerRect = footer.getBoundingClientRect();
return {
columns: getComputedStyle(selector).gridTemplateColumns,
controlWidths: [...selector.children].map((child) => (child as HTMLElement).getBoundingClientRect().width),
pageHeight: pageRect.height,
pageOverflow: getComputedStyle(element).overflow,
resultBottomWithinPage: resultRect.bottom <= pageRect.bottom,
footerAfterResult: footerRect.top >= resultRect.bottom,
resultBackgroundImage: getComputedStyle(battleResult).backgroundImage,
};
});
expect(mobileGeometry.columns.split(' ')).toHaveLength(4);
expect(mobileGeometry.controlWidths[0]).toBeCloseTo(83.33, 0);
expect(mobileGeometry.controlWidths[1]).toBeCloseTo(125, 0);
expect(mobileGeometry.pageHeight).toBeGreaterThan(0);
expect(mobileGeometry.pageOverflow).toBe('visible');
expect(mobileGeometry.resultBottomWithinPage).toBe(true);
expect(mobileGeometry.footerAfterResult).toBe(true);
expect(mobileGeometry.resultBackgroundImage).toContain('back_walnut.jpg');
await page.locator('.log-block[data-log-type="battleResult"]').scrollIntoViewIfNeeded();
await expect(page.locator('.log-block[data-log-type="battleResult"]')).toBeInViewport();
await persistParityArtifact(page, 'core-battle-center-mobile', mobileGeometry);
await page.unrouteAll({ behavior: 'wait' });
@@ -748,6 +748,7 @@ const persistArtifact = async (page: Page, name: string) => {
return {
viewport: { width: innerWidth, height: innerHeight },
global: describe('.main-global-menu'),
bottomGlobalPopup: describe('[data-menu-position="bottom"] .main-menu-popup__list'),
nation: describe('.main-nation-menu'),
bottom: describe('.main-mobile-bottom'),
globalPopup: describe('#mobile-global-menu'),
@@ -809,6 +810,7 @@ test('desktop menus preserve ref columns, prefix-safe routes, and controlled dro
await installFixture(page, state);
await page.setViewportSize({ width: 1200, height: 900 });
await waitForMain(page);
if (artifactRoot) await mkdir(resolve(artifactRoot), { recursive: true });
await expect(page.locator('.main-global-menu')).toHaveCount(3);
expect(await gridColumnCount(page, '.main-global-menu')).toBe(8);
@@ -914,6 +916,20 @@ test('desktop menus preserve ref columns, prefix-safe routes, and controlled dro
await gameInfoButton.press('Enter');
await expect(gameInfoButton).toHaveAttribute('aria-expanded', 'true');
await expect(global.locator('#global-menu-game-info')).toBeVisible();
const topMenuGeometry = await gameInfoButton.evaluate((button) => {
const popup = button.parentElement?.querySelector<HTMLElement>('.main-menu-popup__list');
const caret = button.querySelector<HTMLElement>('.menu-caret');
if (!popup || !caret) throw new Error('top global menu popup geometry is incomplete');
return {
trigger: button.getBoundingClientRect().toJSON(),
popup: popup.getBoundingClientRect().toJSON(),
caretBorderTopWidth: getComputedStyle(caret).borderTopWidth,
caretBorderBottomWidth: getComputedStyle(caret).borderBottomWidth,
};
});
expect(topMenuGeometry.popup.top).toBeGreaterThanOrEqual(topMenuGeometry.trigger.bottom + 1);
expect(topMenuGeometry.caretBorderTopWidth).toBe('4px');
expect(topMenuGeometry.caretBorderBottomWidth).toBe('0px');
await page.keyboard.press('Escape');
await expect(gameInfoButton).toHaveAttribute('aria-expanded', 'false');
await expect(gameInfoButton).toBeFocused();
@@ -921,9 +937,72 @@ test('desktop menus preserve ref columns, prefix-safe routes, and controlled dro
await gameInfoButton.click();
await page.getByRole('heading', { name: '메인 화면 검증 시나리오' }).click();
await expect(gameInfoButton).toHaveAttribute('aria-expanded', 'false');
const bottomGlobal = page.locator('[data-menu-position="bottom"]');
const bottomGameInfoButton = bottomGlobal.locator('[data-menu-id="game-info"]');
await bottomGameInfoButton.click();
await expect(bottomGameInfoButton).toHaveAttribute('aria-expanded', 'true');
await expect(bottomGlobal.locator('#global-menu-game-info')).toBeVisible();
const bottomMenuGeometry = await bottomGameInfoButton.evaluate((button) => {
const popup = button.parentElement?.querySelector<HTMLElement>('.main-menu-popup__list');
const caret = button.querySelector<HTMLElement>('.menu-caret');
if (!popup || !caret) throw new Error('bottom global menu popup geometry is incomplete');
return {
trigger: button.getBoundingClientRect().toJSON(),
popup: popup.getBoundingClientRect().toJSON(),
caretBorderTopWidth: getComputedStyle(caret).borderTopWidth,
caretBorderBottomWidth: getComputedStyle(caret).borderBottomWidth,
boxShadow: getComputedStyle(popup).boxShadow,
};
});
expect(bottomMenuGeometry.popup.bottom).toBeLessThanOrEqual(bottomMenuGeometry.trigger.top - 1);
expect(bottomMenuGeometry.caretBorderTopWidth).toBe('0px');
expect(bottomMenuGeometry.caretBorderBottomWidth).toBe('4px');
expect(bottomMenuGeometry.boxShadow).toContain('0px -8px 18px');
await persistArtifact(page, `${basePath.slice(1)}-desktop-1200`);
});
test('the repeated bottom global menu opens upward on the mobile document', async ({ page }, testInfo) => {
const state: NavigationFixture = {
officerLevel: 5,
permission: 2,
nationLevel: 3,
stage: 1,
npcMode: 1,
scenarioTitle: '하단 메뉴 방향 검증 시나리오',
generalMeCalls: 0,
operations: [],
};
await installFixture(page, state);
await page.setViewportSize({ width: 500, height: 900 });
await waitForMain(page);
const bottomGlobal = page.locator('[data-menu-position="bottom"]');
const gameInfoButton = bottomGlobal.locator('[data-menu-id="game-info"]');
await gameInfoButton.click();
await expect(gameInfoButton).toHaveAttribute('aria-expanded', 'true');
await expect(bottomGlobal.locator('#global-menu-game-info')).toBeVisible();
const geometry = await gameInfoButton.evaluate((button) => {
const popup = button.parentElement?.querySelector<HTMLElement>('.main-menu-popup__list');
const caret = button.querySelector<HTMLElement>('.menu-caret');
if (!popup || !caret) throw new Error('mobile bottom global menu popup geometry is incomplete');
return {
trigger: button.getBoundingClientRect().toJSON(),
popup: popup.getBoundingClientRect().toJSON(),
caretBorderTopWidth: getComputedStyle(caret).borderTopWidth,
caretBorderBottomWidth: getComputedStyle(caret).borderBottomWidth,
viewportHeight: window.innerHeight,
};
});
expect(geometry.popup.bottom).toBeLessThanOrEqual(geometry.trigger.top - 1);
expect(geometry.popup.top).toBeGreaterThanOrEqual(0);
expect(geometry.popup.bottom).toBeLessThanOrEqual(geometry.viewportHeight);
expect(geometry.caretBorderTopWidth).toBe('0px');
expect(geometry.caretBorderBottomWidth).toBe('4px');
await bottomGlobal.screenshot({ path: testInfo.outputPath('mobile-bottom-global-dropup.png') });
await persistArtifact(page, `${basePath.slice(1)}-mobile-bottom-dropup`);
});
test('main general card uses local turn time and command clock tracks corrected server time', async ({ page }) => {
const state: NavigationFixture = {
officerLevel: 0,
@@ -2160,6 +2239,222 @@ test('nation menu presentation follows the server-derived permission matrix', as
);
});
test('all main Lumen button families share the rounded pressed geometry', async ({ page }) => {
const state: NavigationFixture = {
officerLevel: 5,
permission: 2,
nationLevel: 3,
stage: 0,
npcMode: 1,
generalMeCalls: 0,
operations: [],
nationColor: '#663399',
};
await installFixture(page, state);
await page.setViewportSize({ width: 1200, height: 900 });
await waitForMain(page);
const controls: Array<[string, Locator]> = [
[
'천통국 베팅',
page.locator('.main-global-menu[data-menu-position="top"] [data-navigation-id="nation-betting"]'),
],
[
'게임정보',
page.locator('.main-global-menu[data-menu-position="top"]').getByRole('button', {
name: '게임정보',
exact: true,
}),
],
['회 의 실', page.locator('.layout-desktop [data-navigation-id="meeting"]')],
['기 밀 실', page.locator('.layout-desktop [data-navigation-id="secret-board"]')],
[
'당기기',
page.locator('[data-main-target="commands"] .bottom-actions').getByRole('button', { name: '당기기' }),
],
[
'미루기',
page.locator('[data-main-target="commands"] .bottom-actions').getByRole('button', { name: '미루기' }),
],
[
'펼치기',
page.locator('[data-main-target="commands"] .bottom-actions').getByRole('button', { name: '펼치기' }),
],
['실시간 동기화', page.locator('.desktop-action-controls').getByRole('button', { name: / :/u })],
['갱 신', page.locator('.desktop-action-controls').getByRole('button', { name: '갱 신' })],
['로비로', page.locator('.desktop-action-controls').getByRole('button', { name: '로비로' })],
];
const measure = (control: Locator) =>
control.evaluate((element) => {
const rect = element.getBoundingClientRect();
const style = getComputedStyle(element);
return {
top: rect.top,
bottom: rect.bottom,
height: rect.height,
marginTop: style.marginTop,
borderTop: style.borderTopWidth,
borderRight: style.borderRightWidth,
borderBottom: style.borderBottomWidth,
borderLeft: style.borderLeftWidth,
radius: style.borderRadius,
background: style.backgroundColor,
filter: style.filter,
};
});
const evidence: Record<string, Record<string, unknown>> = {};
for (const [index, [label, control]] of controls.entries()) {
await expect(control, `${label} control`).toBeVisible();
await expect(control).toHaveClass(/legacy-button/u);
await control.scrollIntoViewIfNeeded();
await page.mouse.move(1195, 895);
const base = await measure(control);
evidence[label] = { default: base };
if (artifactRoot) {
await control.screenshot({ path: resolve(artifactRoot, `${index + 1}-default.png`) });
}
expect(base, `${label} default geometry`).toMatchObject({
marginTop: '0px',
borderTop: '0px',
borderRight: '1px',
borderBottom: '4px',
borderLeft: '1px',
radius: '5.25px',
filter: 'none',
});
await control.focus();
await expect(control, `${label} keyboard focus`).toBeFocused();
const focused = await measure(control);
evidence[label].focus = focused;
if (artifactRoot) {
await control.screenshot({ path: resolve(artifactRoot, `${index + 1}-focus.png`) });
}
expect(focused.borderBottom, `${label} focus edge`).toBe('4px');
expect(focused.marginTop, `${label} focus position`).toBe('0px');
await control.hover();
const hovered = await measure(control);
evidence[label].hover = hovered;
if (artifactRoot) {
await control.screenshot({ path: resolve(artifactRoot, `${index + 1}-hover.png`) });
}
expect(hovered.borderBottom, `${label} hover edge`).toBe('3px');
expect(hovered.marginTop, `${label} hover position`).toBe('1px');
expect(hovered.top, `${label} hover top`).toBeCloseTo(base.top + 1, 2);
expect(hovered.height, `${label} hover height`).toBeCloseTo(base.height - 1, 2);
expect(hovered.bottom, `${label} hover bottom`).toBeCloseTo(base.bottom, 2);
expect(hovered.background, `${label} hover face`).toBe(base.background);
const box = await control.boundingBox();
if (!box) throw new Error(`${label} control has no bounding box`);
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await page.mouse.down();
const pressed = await measure(control);
evidence[label].pointerDown = pressed;
if (artifactRoot) {
await control.screenshot({ path: resolve(artifactRoot, `${index + 1}-pointer-down.png`) });
}
expect(pressed.borderBottom, `${label} pressed edge`).toBe('2px');
expect(pressed.marginTop, `${label} pressed position`).toBe('2px');
expect(pressed.top, `${label} pressed top`).toBeCloseTo(base.top + 2, 2);
expect(pressed.height, `${label} pressed height`).toBeCloseTo(base.height - 2, 2);
expect(pressed.bottom, `${label} pressed bottom`).toBeCloseTo(base.bottom, 2);
expect(pressed.background, `${label} pressed face`).toBe(base.background);
await page.mouse.move(1195, 895);
await page.mouse.up();
}
state.permission = 0;
await page.locator('.desktop-action-controls').getByRole('button', { name: '갱 신' }).click();
const disabledSecret = page.locator('.layout-desktop [data-navigation-id="secret-board"]');
await expect(disabledSecret).toHaveAttribute('aria-disabled', 'true');
await disabledSecret.scrollIntoViewIfNeeded();
const disabledBase = await measure(disabledSecret);
await disabledSecret.hover({ force: true });
const disabledHover = await measure(disabledSecret);
evidence['기 밀 실 disabled'] = { default: disabledBase, hover: disabledHover };
expect(disabledHover.borderBottom).toBe('4px');
expect(disabledHover.marginTop).toBe('0px');
expect(disabledHover.top).toBeCloseTo(disabledBase.top, 2);
if (artifactRoot) {
await disabledSecret.screenshot({ path: resolve(artifactRoot, 'disabled-secret-hover.png') });
await writeFile(
resolve(artifactRoot, 'main-lumen-button-states.json'),
`${JSON.stringify(evidence, null, 2)}\n`
);
}
await persistArtifact(page, `${basePath.slice(1)}-main-lumen-button-families`);
});
test('mobile main Lumen button families keep the same state geometry without overflow', async ({ page }) => {
const state: NavigationFixture = {
officerLevel: 5,
permission: 2,
nationLevel: 3,
stage: 0,
npcMode: 1,
generalMeCalls: 0,
operations: [],
nationColor: '#663399',
};
await installFixture(page, state);
await page.setViewportSize({ width: 500, height: 900 });
await waitForMain(page);
const controls = [
page.locator('.main-global-menu[data-menu-position="top"] [data-navigation-id="nation-betting"]'),
page.locator('.main-global-menu[data-menu-position="top"]').getByRole('button', {
name: '게임정보',
exact: true,
}),
page.locator('.layout-mobile [data-navigation-id="meeting"]'),
page.locator('.layout-mobile [data-navigation-id="secret-board"]'),
page.locator('[data-main-target="commands"] .bottom-actions').getByRole('button', { name: '당기기' }),
page.locator('[data-main-target="commands"] .bottom-actions').getByRole('button', { name: '미루기' }),
page.locator('[data-main-target="commands"] .bottom-actions').getByRole('button', { name: '펼치기' }),
page.locator('.desktop-action-controls').getByRole('button', { name: / :/u }),
page.locator('.desktop-action-controls').getByRole('button', { name: '갱 신' }),
page.locator('.desktop-action-controls').getByRole('button', { name: '로비로' }),
];
for (const control of controls) {
await expect(control).toBeVisible();
await expect(control).toHaveClass(/legacy-button/u);
await expect(control).toHaveCSS('border-radius', '5.25px');
await expect(control).toHaveCSS('border-bottom-width', '4px');
}
for (const control of [controls[0], controls[2], controls[4], controls[7]]) {
if (!control) throw new Error('mobile Lumen control is missing');
await control.scrollIntoViewIfNeeded();
await control.focus();
await expect(control).toBeFocused();
await expect(control).toHaveCSS('border-bottom-width', '4px');
await control.hover();
await expect(control).toHaveCSS('border-bottom-width', '3px');
await expect(control).toHaveCSS('margin-top', '1px');
const box = await control.boundingBox();
if (!box) throw new Error('mobile Lumen control is not measurable');
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await page.mouse.down();
await expect(control).toHaveCSS('border-bottom-width', '2px');
await expect(control).toHaveCSS('margin-top', '2px');
await page.mouse.move(499, 899);
await page.mouse.up();
}
expect(
await page.evaluate(() => ({
document: document.documentElement.scrollWidth - document.documentElement.clientWidth,
body: document.body.scrollWidth - document.body.clientWidth,
}))
).toEqual({ document: 0, body: 0 });
await persistArtifact(page, `${basePath.slice(1)}-mobile-main-lumen-button-families`);
});
test('mobile single document refreshes once and preserves tokens on lobby return', async ({ page }) => {
const state: NavigationFixture = {
officerLevel: 5,