fix: 메인 모바일 겹침과 전용 아이콘 배경을 바로잡는다

This commit is contained in:
2026-08-28 06:56:44 +00:00
parent f43a1db136
commit c741ef4be5
5 changed files with 177 additions and 10 deletions
+161 -2
View File
@@ -35,6 +35,8 @@ type NavigationFixture = {
generalMeCalls: number;
operations: string[];
generalName?: string;
generalPicture?: string | null;
generalImageServer?: number;
generalTurnTime?: string;
nextTurnMonthOffset?: 0 | 1;
serverTime?: string;
@@ -446,8 +448,8 @@ const generalContext = (state: NavigationFixture) => ({
officerCityName: state.officerLevel >= 2 && state.officerLevel <= 4 ? '업' : null,
generalType: '용장',
leadershipBonus: state.officerLevel === 12 ? state.nationLevel * 2 : 0,
picture: null,
imageServer: 0,
picture: state.generalPicture ?? null,
imageServer: state.generalImageServer ?? 0,
stats: { leadership: 70, strength: 60, intelligence: 50 },
gold: 1_000,
rice: 2_000,
@@ -1428,6 +1430,85 @@ test('main info fills the eighth slot with Ref-compatible autorun status and an
await expect(page.getByRole('tooltip')).toHaveCount(0);
});
test('main user icon keeps transparent pixels clear and only falls back after an image error', async ({
page,
}, testInfo) => {
const state: NavigationFixture = {
officerLevel: 5,
permission: 2,
nationLevel: 3,
stage: 0,
npcMode: 1,
generalPicture: 'transparent-main.png',
generalImageServer: 1,
generalMeCalls: 0,
operations: [],
};
await page.route('**/icons/transparent-main.png', async (route) => {
await route.fulfill({
status: 200,
contentType: 'image/png',
body: Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M/wHwAF/gL+X1N6WQAAAABJRU5ErkJggg==',
'base64'
),
});
});
await installFixture(page, state);
await page.setViewportSize({ width: 500, height: 900 });
await waitForMain(page);
const icon = page.locator('[data-main-target="general"] .general-icon');
await expect(icon).toHaveJSProperty('tagName', 'IMG');
await expect(icon).toHaveAttribute('src', /\/icons\/transparent-main\.png$/u);
await expect(icon).toHaveCSS('background-image', 'none');
await expect(icon).toHaveCSS('object-fit', 'contain');
await icon.screenshot({ path: testInfo.outputPath('transparent-main-user-icon.png') });
});
test('mobile top menu popup stays above the eight-cell game information strip', async ({ page }, testInfo) => {
const state: NavigationFixture = {
officerLevel: 5,
permission: 2,
nationLevel: 3,
stage: 0,
npcMode: 1,
generalMeCalls: 0,
operations: [],
};
await installFixture(page, state);
await page.setViewportSize({ width: 500, height: 900 });
await waitForMain(page);
const topMenu = page.locator('[data-menu-position="top"]');
const gameInfoButton = topMenu.locator('[data-menu-id="game-info"]');
await gameInfoButton.click();
const popup = topMenu.locator('#global-menu-game-info');
await expect(popup).toBeVisible();
const stacking = await popup.evaluate((element) => {
const menu = element.closest<HTMLElement>('.main-global-menu');
const gameInfo = document.querySelector<HTMLElement>('.legacy-game-info');
if (!menu || !gameInfo) throw new Error('mobile top menu stacking targets are missing');
const popupRect = element.getBoundingClientRect();
const gameInfoRect = gameInfo.getBoundingClientRect();
const left = Math.max(popupRect.left, gameInfoRect.left);
const right = Math.min(popupRect.right, gameInfoRect.right);
const top = Math.max(popupRect.top, gameInfoRect.top);
const bottom = Math.min(popupRect.bottom, gameInfoRect.bottom);
if (right <= left || bottom <= top) throw new Error('mobile popup does not overlap the game information strip');
const hit = document.elementFromPoint((left + right) / 2, (top + bottom) / 2);
return {
menuZIndex: Number(getComputedStyle(menu).zIndex),
gameInfoZIndex: Number(getComputedStyle(gameInfo).zIndex),
hitInsidePopup: hit === element || element.contains(hit),
};
});
expect(stacking.menuZIndex).toBeGreaterThan(stacking.gameInfoZIndex);
expect(stacking.hitInsidePopup).toBe(true);
await page.screenshot({ path: testInfo.outputPath('mobile-top-menu-game-info-stacking.png') });
});
test('desktop menus preserve ref columns, prefix-safe routes, and controlled dropdown behavior', async ({
page,
}, testInfo) => {
@@ -2026,6 +2107,84 @@ test('the repeated bottom global menu opens upward on the mobile document', asyn
await persistArtifact(page, `${basePath.slice(1)}-mobile-bottom-dropup`);
});
test('mobile message content owns its full height and leaves the diplomacy action clear of the bottom bar', async ({
page,
}, testInfo) => {
const target = (generalId: number, generalName: string) => ({
generalId,
generalName,
nationId: 1,
nationName: '위',
color: '#008000',
icon: '',
});
const entries = (type: 'public' | 'national' | 'private' | 'diplomacy', startId: number) =>
Array.from({ length: 12 }, (_, index) => ({
id: startId + index,
text: `${type} 모바일 높이 검증 메시지 ${index + 1}`,
time: '2026-08-12 12:00:00',
msgType: type,
src: target(21, '보낸장수'),
dest: type === 'public' ? null : target(7, '메뉴검증장수'),
option: {},
}));
const state: NavigationFixture = {
officerLevel: 5,
permission: 2,
nationLevel: 3,
stage: 0,
npcMode: 1,
generalMeCalls: 0,
operations: [],
messages: {
...emptyMessages(2),
public: entries('public', 100),
national: entries('national', 200),
private: entries('private', 300),
diplomacy: entries('diplomacy', 400),
},
};
await installFixture(page, state);
await page.setViewportSize({ width: 500, height: 900 });
await waitForMain(page);
const panel = page.locator('.mobile-message-panel');
const diplomacyLoadOlder = panel.locator('.DiplomacyTalk .load-older');
await expect(diplomacyLoadOlder).toBeVisible();
const flowGeometry = await panel.evaluate((element) => {
const repeated = document.querySelector<HTMLElement>('[data-menu-position="bottom"]');
const privateTalk = element.querySelector<HTMLElement>('.PrivateTalk');
const actions = element.querySelector<HTMLElement>('.DiplomacyTalk .Actions');
if (!repeated || !privateTalk || !actions) throw new Error('mobile message flow targets are missing');
return {
panel: element.getBoundingClientRect().toJSON(),
panelClientHeight: element.clientHeight,
panelScrollHeight: element.scrollHeight,
privateTalk: privateTalk.getBoundingClientRect().toJSON(),
actionsColumns: getComputedStyle(actions).gridTemplateColumns,
repeatedMenu: repeated.getBoundingClientRect().toJSON(),
};
});
expect(flowGeometry.panelClientHeight).toBeGreaterThanOrEqual(flowGeometry.panelScrollHeight);
expect(flowGeometry.repeatedMenu.top).toBeGreaterThanOrEqual(flowGeometry.panel.bottom - 0.5);
expect(flowGeometry.repeatedMenu.top).toBeGreaterThanOrEqual(flowGeometry.privateTalk.bottom - 0.5);
expect(flowGeometry.actionsColumns.split(' ')).toHaveLength(2);
await page.evaluate(() => window.scrollTo(0, document.documentElement.scrollHeight));
const bottomClearance = await diplomacyLoadOlder.evaluate((button) => {
const bottomBar = document.querySelector<HTMLElement>('.main-mobile-bottom');
const spacer = document.querySelector<HTMLElement>('.main-mobile-bottom-spacer');
if (!bottomBar || !spacer) throw new Error('mobile bottom clearance targets are missing');
return {
clearance: bottomBar.getBoundingClientRect().top - button.getBoundingClientRect().bottom,
spacerHeight: spacer.getBoundingClientRect().height,
};
});
expect(bottomClearance.clearance).toBeGreaterThanOrEqual(12);
expect(bottomClearance.spacerHeight).toBeGreaterThanOrEqual(61);
await page.screenshot({ path: testInfo.outputPath('mobile-diplomacy-bottom-clearance.png') });
});
test('first reserved month crosses December only after the general turn has passed', async ({ page }, testInfo) => {
const state: NavigationFixture = {
officerLevel: 0,
@@ -6,7 +6,7 @@ import LegacyProgressBar from '../ui/LegacyProgressBar.vue';
import RichTooltip from '../ui/RichTooltip.vue';
import { formatLocalTimeSeconds } from '../../utils/legacyDateTime';
import { legacyExperiencePercent, ratioPercent } from '../../utils/legacyProgress';
import { DEFAULT_GENERAL_ICON_URL, resolveGeneralIconBackgroundImage } from '../../utils/generalIcon';
import { DEFAULT_GENERAL_ICON_URL, resolveGeneralIconUrl, useDefaultGeneralIcon } from '../../utils/generalIcon';
import { configuredGameAssetUrl } from '../../utils/imageAssets';
import { legacyLuminanceTextColor } from '../../utils/legacyNationColor';
@@ -164,7 +164,7 @@ const experiencePercent = computed(() =>
const itemNames = computed<ItemDisplayNames>(() => props.general?.itemNames ?? props.general?.equipmentNames ?? {});
const generalIconBackground = computed(() => resolveGeneralIconBackgroundImage(props.general ?? {}));
const generalIconUrl = computed(() => resolveGeneralIconUrl(props.general ?? {}));
const crewTypeIconBackground = computed(() => {
const crewTypeId = props.general?.crewTypeId;
@@ -251,11 +251,11 @@ const specialText = computed(() => {
<div v-else-if="!props.general" class="empty">장수 정보를 불러오지 못했습니다.</div>
<template v-else>
<div class="general-basic-grid general-body">
<span
<img
class="general-image general-icon"
role="img"
:aria-label="`${props.general.name} 초상`"
:style="{ backgroundImage: generalIconBackground }"
:src="generalIconUrl"
:alt="`${props.general.name} 초상`"
@error="useDefaultGeneralIcon"
/>
<div class="general-title battle-general-name" :style="titleStyle">
{{ props.general.name }}
@@ -505,6 +505,7 @@ const specialText = computed(() => {
background-position: center;
background-repeat: no-repeat;
background-size: contain;
object-fit: contain;
pointer-events: none;
user-select: none;
-webkit-user-drag: none;
@@ -173,6 +173,10 @@ const isActive = (link: MainNavigationLinkItem) => link.highlightWhen === 'vote'
box-shadow: 0 -8px 18px rgb(0 0 0 / 45%);
}
.main-global-menu[data-menu-position='top'] {
z-index: 101;
}
.main-global-menu[data-menu-position='bottom'] .menu-caret {
border-top-width: 0;
border-bottom: 4px solid currentColor;
@@ -467,5 +467,9 @@ const forwardResponse = (messageId: number, response: boolean) => {
z-index: 4;
top: 62px;
}
.Actions {
grid-template-columns: 1fr 1fr;
}
}
</style>
+1 -2
View File
@@ -1065,7 +1065,7 @@ button {
@media (max-width: 939.98px) {
.main-mobile-bottom-spacer {
height: 45px;
height: calc(61px + env(safe-area-inset-bottom));
}
.main-page {
@@ -1100,7 +1100,6 @@ button {
}
.mobile-message-panel {
height: 1394.5px;
margin-bottom: -10px;
}
}