fix(game-ui): 분할 버튼 이음새를 하나로 연결
공용 split primitive가 내부 모서리와 구분선을 소유하게 해 Lumen 규칙의 cascade 덮어쓰기를 막습니다. 공통·국가 메뉴의 desktop/mobile 및 상호작용 상태를 Chromium 회귀로 고정합니다.
This commit is contained in:
@@ -995,6 +995,123 @@ test('desktop menus preserve ref columns, prefix-safe routes, and controlled dro
|
|||||||
await persistArtifact(page, `${basePath.slice(1)}-desktop-1200`);
|
await persistArtifact(page, `${basePath.slice(1)}-desktop-1200`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('split buttons keep square inner corners and a single divider in every interaction state', async ({
|
||||||
|
page,
|
||||||
|
}, testInfo) => {
|
||||||
|
const state: NavigationFixture = {
|
||||||
|
officerLevel: 5,
|
||||||
|
permission: 2,
|
||||||
|
nationLevel: 3,
|
||||||
|
stage: 1,
|
||||||
|
npcMode: 1,
|
||||||
|
scenarioTitle: '분할 버튼 이음새 검증 시나리오',
|
||||||
|
generalMeCalls: 0,
|
||||||
|
operations: [],
|
||||||
|
nationColor: '#663399',
|
||||||
|
};
|
||||||
|
await installFixture(page, state);
|
||||||
|
if (artifactRoot) await mkdir(resolve(artifactRoot), { recursive: true });
|
||||||
|
|
||||||
|
const measure = (main: Locator, toggle: Locator) =>
|
||||||
|
Promise.all([
|
||||||
|
main.evaluate((element) => {
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
const style = getComputedStyle(element);
|
||||||
|
return {
|
||||||
|
rect: rect.toJSON(),
|
||||||
|
borderRightWidth: style.borderRightWidth,
|
||||||
|
borderTopLeftRadius: style.borderTopLeftRadius,
|
||||||
|
borderTopRightRadius: style.borderTopRightRadius,
|
||||||
|
borderBottomRightRadius: style.borderBottomRightRadius,
|
||||||
|
borderBottomLeftRadius: style.borderBottomLeftRadius,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
toggle.evaluate((element) => {
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
const style = getComputedStyle(element);
|
||||||
|
return {
|
||||||
|
rect: rect.toJSON(),
|
||||||
|
borderLeftWidth: style.borderLeftWidth,
|
||||||
|
borderTopLeftRadius: style.borderTopLeftRadius,
|
||||||
|
borderTopRightRadius: style.borderTopRightRadius,
|
||||||
|
borderBottomRightRadius: style.borderBottomRightRadius,
|
||||||
|
borderBottomLeftRadius: style.borderBottomLeftRadius,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const expectAttached = async (main: Locator, toggle: Locator) => {
|
||||||
|
const [mainStyle, toggleStyle] = await measure(main, toggle);
|
||||||
|
expect(mainStyle).toMatchObject({
|
||||||
|
borderRightWidth: '1px',
|
||||||
|
borderTopLeftRadius: '5.25px',
|
||||||
|
borderTopRightRadius: '0px',
|
||||||
|
borderBottomRightRadius: '0px',
|
||||||
|
borderBottomLeftRadius: '5.25px',
|
||||||
|
});
|
||||||
|
expect(toggleStyle).toMatchObject({
|
||||||
|
borderLeftWidth: '0px',
|
||||||
|
borderTopLeftRadius: '0px',
|
||||||
|
borderTopRightRadius: '5.25px',
|
||||||
|
borderBottomRightRadius: '5.25px',
|
||||||
|
borderBottomLeftRadius: '0px',
|
||||||
|
});
|
||||||
|
expect(toggleStyle.rect.left).toBeCloseTo(mainStyle.rect.right, 2);
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const width of [1200, 500]) {
|
||||||
|
await page.setViewportSize({ width, height: 900 });
|
||||||
|
await waitForMain(page);
|
||||||
|
const globalSplit = page.locator('.main-global-menu:visible .main-menu-split').first();
|
||||||
|
const nationSplit = page.locator('.main-nation-menu:visible .nation-menu-split').first();
|
||||||
|
const pairs: Array<[string, Locator, Locator]> = [
|
||||||
|
[
|
||||||
|
'global',
|
||||||
|
globalSplit.locator('[data-navigation-id="board-community"]'),
|
||||||
|
globalSplit.locator('[data-menu-id="boards"]'),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'nation',
|
||||||
|
nationSplit.locator('[data-navigation-id="auction-resource"]'),
|
||||||
|
nationSplit.locator('[data-menu-id="auction"]'),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [label, main, toggle] of pairs) {
|
||||||
|
await expect(main).toBeVisible();
|
||||||
|
await expect(toggle).toBeVisible();
|
||||||
|
await page.mouse.move(width - 1, 899);
|
||||||
|
await expectAttached(main, toggle);
|
||||||
|
|
||||||
|
await toggle.focus();
|
||||||
|
await expect(toggle).toBeFocused();
|
||||||
|
await expectAttached(main, toggle);
|
||||||
|
|
||||||
|
await toggle.hover();
|
||||||
|
await expectAttached(main, toggle);
|
||||||
|
|
||||||
|
const box = await toggle.boundingBox();
|
||||||
|
if (!box) throw new Error(`${label} split toggle is not measurable`);
|
||||||
|
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
||||||
|
await page.mouse.down();
|
||||||
|
await expectAttached(main, toggle);
|
||||||
|
await page.mouse.move(width - 1, 899);
|
||||||
|
await page.mouse.up();
|
||||||
|
|
||||||
|
await toggle.click();
|
||||||
|
await expect(toggle).toHaveAttribute('aria-expanded', 'true');
|
||||||
|
await expectAttached(main, toggle);
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
|
||||||
|
await toggle.locator('..').screenshot({
|
||||||
|
path: artifactRoot
|
||||||
|
? resolve(artifactRoot, `${basePath.slice(1)}-${width}-${label}-split-button.png`)
|
||||||
|
: testInfo.outputPath(`${width}-${label}-split-button.png`),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('the repeated bottom global menu opens upward on the mobile document', async ({ page }, testInfo) => {
|
test('the repeated bottom global menu opens upward on the mobile document', async ({ page }, testInfo) => {
|
||||||
const state: NavigationFixture = {
|
const state: NavigationFixture = {
|
||||||
officerLevel: 5,
|
officerLevel: 5,
|
||||||
|
|||||||
@@ -153,3 +153,18 @@
|
|||||||
border-color: var(--legacy-button-border);
|
border-color: var(--legacy-button-border);
|
||||||
background: var(--legacy-button-bg);
|
background: var(--legacy-button-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Split controls share one outer silhouette. Keep the main control's right
|
||||||
|
* border as the subtle divider while removing every inner corner and the
|
||||||
|
* toggle's overlapping left border. These rules intentionally follow the
|
||||||
|
* Lumen family so its border shorthand cannot restore the inner rounding.
|
||||||
|
*/
|
||||||
|
.legacy-split-button > .main-menu-link {
|
||||||
|
border-radius: 5.25px 0 0 5.25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legacy-split-button > .legacy-split-button__toggle {
|
||||||
|
border-left-width: 0;
|
||||||
|
border-radius: 0 5.25px 5.25px 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ const isActive = (link: MainNavigationLinkItem) => link.id === 'survey' && props
|
|||||||
</template>
|
</template>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="main-menu-split">
|
<div v-else class="main-menu-split legacy-split-button">
|
||||||
<MainNavigationLink
|
<MainNavigationLink
|
||||||
:link="entry.main"
|
:link="entry.main"
|
||||||
:enabled="isNavigationConfigured(entry.main)"
|
:enabled="isNavigationConfigured(entry.main)"
|
||||||
@@ -67,7 +67,7 @@ const isActive = (link: MainNavigationLinkItem) => link.id === 'survey' && props
|
|||||||
lumen-variant="navigation"
|
lumen-variant="navigation"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
class="main-menu-button main-menu-split__toggle legacy-button legacy-button--navigation"
|
class="main-menu-button main-menu-split__toggle legacy-split-button__toggle legacy-button legacy-button--navigation"
|
||||||
type="button"
|
type="button"
|
||||||
:data-menu-id="entry.id"
|
:data-menu-id="entry.id"
|
||||||
:aria-label="`${entry.main.label} 하위 메뉴`"
|
:aria-label="`${entry.main.label} 하위 메뉴`"
|
||||||
@@ -131,12 +131,6 @@ const isActive = (link: MainNavigationLinkItem) => link.id === 'survey' && props
|
|||||||
min-width: 28px;
|
min-width: 28px;
|
||||||
width: 28px;
|
width: 28px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border-left-width: 0;
|
|
||||||
border-radius: 0 5.25px 5.25px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-menu-split > :deep(.main-menu-link) {
|
|
||||||
border-radius: 5.25px 0 0 5.25px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-caret {
|
.menu-caret {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const isActive = (link: MainNavigationLinkItem) => link.highlightStage === props
|
|||||||
:active="isActive(entry)"
|
:active="isActive(entry)"
|
||||||
lumen-variant="lumen"
|
lumen-variant="lumen"
|
||||||
/>
|
/>
|
||||||
<div v-else-if="entry.kind === 'split'" class="nation-menu-split">
|
<div v-else-if="entry.kind === 'split'" class="nation-menu-split legacy-split-button">
|
||||||
<MainNavigationLink
|
<MainNavigationLink
|
||||||
:link="entry.main"
|
:link="entry.main"
|
||||||
:enabled="isNationNavigationEnabled(entry.main, access)"
|
:enabled="isNationNavigationEnabled(entry.main, access)"
|
||||||
@@ -43,7 +43,7 @@ const isActive = (link: MainNavigationLinkItem) => link.highlightStage === props
|
|||||||
lumen-variant="lumen"
|
lumen-variant="lumen"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
class="main-menu-button nation-menu-split__toggle legacy-button legacy-button--lumen"
|
class="main-menu-button nation-menu-split__toggle legacy-split-button__toggle legacy-button legacy-button--lumen"
|
||||||
type="button"
|
type="button"
|
||||||
:data-menu-id="entry.id"
|
:data-menu-id="entry.id"
|
||||||
:aria-label="`${entry.main.label} 하위 메뉴`"
|
:aria-label="`${entry.main.label} 하위 메뉴`"
|
||||||
@@ -108,12 +108,6 @@ const isActive = (link: MainNavigationLinkItem) => link.highlightStage === props
|
|||||||
min-width: 28px;
|
min-width: 28px;
|
||||||
width: 28px;
|
width: 28px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border-left-width: 0;
|
|
||||||
border-radius: 0 5.25px 5.25px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nation-menu-split > :deep(.main-menu-link) {
|
|
||||||
border-radius: 5.25px 0 0 5.25px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-caret {
|
.menu-caret {
|
||||||
|
|||||||
Reference in New Issue
Block a user