fix: 메인 모바일 겹침과 전용 아이콘 배경을 바로잡는다
This commit is contained in:
@@ -35,6 +35,8 @@ type NavigationFixture = {
|
|||||||
generalMeCalls: number;
|
generalMeCalls: number;
|
||||||
operations: string[];
|
operations: string[];
|
||||||
generalName?: string;
|
generalName?: string;
|
||||||
|
generalPicture?: string | null;
|
||||||
|
generalImageServer?: number;
|
||||||
generalTurnTime?: string;
|
generalTurnTime?: string;
|
||||||
nextTurnMonthOffset?: 0 | 1;
|
nextTurnMonthOffset?: 0 | 1;
|
||||||
serverTime?: string;
|
serverTime?: string;
|
||||||
@@ -446,8 +448,8 @@ const generalContext = (state: NavigationFixture) => ({
|
|||||||
officerCityName: state.officerLevel >= 2 && state.officerLevel <= 4 ? '업' : null,
|
officerCityName: state.officerLevel >= 2 && state.officerLevel <= 4 ? '업' : null,
|
||||||
generalType: '용장',
|
generalType: '용장',
|
||||||
leadershipBonus: state.officerLevel === 12 ? state.nationLevel * 2 : 0,
|
leadershipBonus: state.officerLevel === 12 ? state.nationLevel * 2 : 0,
|
||||||
picture: null,
|
picture: state.generalPicture ?? null,
|
||||||
imageServer: 0,
|
imageServer: state.generalImageServer ?? 0,
|
||||||
stats: { leadership: 70, strength: 60, intelligence: 50 },
|
stats: { leadership: 70, strength: 60, intelligence: 50 },
|
||||||
gold: 1_000,
|
gold: 1_000,
|
||||||
rice: 2_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);
|
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 ({
|
test('desktop menus preserve ref columns, prefix-safe routes, and controlled dropdown behavior', async ({
|
||||||
page,
|
page,
|
||||||
}, testInfo) => {
|
}, 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`);
|
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) => {
|
test('first reserved month crosses December only after the general turn has passed', async ({ page }, testInfo) => {
|
||||||
const state: NavigationFixture = {
|
const state: NavigationFixture = {
|
||||||
officerLevel: 0,
|
officerLevel: 0,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import LegacyProgressBar from '../ui/LegacyProgressBar.vue';
|
|||||||
import RichTooltip from '../ui/RichTooltip.vue';
|
import RichTooltip from '../ui/RichTooltip.vue';
|
||||||
import { formatLocalTimeSeconds } from '../../utils/legacyDateTime';
|
import { formatLocalTimeSeconds } from '../../utils/legacyDateTime';
|
||||||
import { legacyExperiencePercent, ratioPercent } from '../../utils/legacyProgress';
|
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 { configuredGameAssetUrl } from '../../utils/imageAssets';
|
||||||
import { legacyLuminanceTextColor } from '../../utils/legacyNationColor';
|
import { legacyLuminanceTextColor } from '../../utils/legacyNationColor';
|
||||||
|
|
||||||
@@ -164,7 +164,7 @@ const experiencePercent = computed(() =>
|
|||||||
|
|
||||||
const itemNames = computed<ItemDisplayNames>(() => props.general?.itemNames ?? props.general?.equipmentNames ?? {});
|
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 crewTypeIconBackground = computed(() => {
|
||||||
const crewTypeId = props.general?.crewTypeId;
|
const crewTypeId = props.general?.crewTypeId;
|
||||||
@@ -251,11 +251,11 @@ const specialText = computed(() => {
|
|||||||
<div v-else-if="!props.general" class="empty">장수 정보를 불러오지 못했습니다.</div>
|
<div v-else-if="!props.general" class="empty">장수 정보를 불러오지 못했습니다.</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="general-basic-grid general-body">
|
<div class="general-basic-grid general-body">
|
||||||
<span
|
<img
|
||||||
class="general-image general-icon"
|
class="general-image general-icon"
|
||||||
role="img"
|
:src="generalIconUrl"
|
||||||
:aria-label="`${props.general.name} 초상`"
|
:alt="`${props.general.name} 초상`"
|
||||||
:style="{ backgroundImage: generalIconBackground }"
|
@error="useDefaultGeneralIcon"
|
||||||
/>
|
/>
|
||||||
<div class="general-title battle-general-name" :style="titleStyle">
|
<div class="general-title battle-general-name" :style="titleStyle">
|
||||||
{{ props.general.name }} 【
|
{{ props.general.name }} 【
|
||||||
@@ -505,6 +505,7 @@ const specialText = computed(() => {
|
|||||||
background-position: center;
|
background-position: center;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-size: contain;
|
background-size: contain;
|
||||||
|
object-fit: contain;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-webkit-user-drag: 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%);
|
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 {
|
.main-global-menu[data-menu-position='bottom'] .menu-caret {
|
||||||
border-top-width: 0;
|
border-top-width: 0;
|
||||||
border-bottom: 4px solid currentColor;
|
border-bottom: 4px solid currentColor;
|
||||||
|
|||||||
@@ -467,5 +467,9 @@ const forwardResponse = (messageId: number, response: boolean) => {
|
|||||||
z-index: 4;
|
z-index: 4;
|
||||||
top: 62px;
|
top: 62px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Actions {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1065,7 +1065,7 @@ button {
|
|||||||
|
|
||||||
@media (max-width: 939.98px) {
|
@media (max-width: 939.98px) {
|
||||||
.main-mobile-bottom-spacer {
|
.main-mobile-bottom-spacer {
|
||||||
height: 45px;
|
height: calc(61px + env(safe-area-inset-bottom));
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-page {
|
.main-page {
|
||||||
@@ -1100,7 +1100,6 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mobile-message-panel {
|
.mobile-message-panel {
|
||||||
height: 1394.5px;
|
|
||||||
margin-bottom: -10px;
|
margin-bottom: -10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user