merge: align main menu and NPC reply affordance
This commit is contained in:
@@ -32,6 +32,8 @@ type NavigationFixture = {
|
||||
refreshDelayMs?: number;
|
||||
largeCommandTable?: boolean;
|
||||
reservedTurns?: Array<{ index: number; action: string; args: Record<string, unknown> }>;
|
||||
messages?: unknown;
|
||||
messageContacts?: unknown;
|
||||
dashboardResponses?: Array<{
|
||||
bytes: number;
|
||||
contextKind: string | null;
|
||||
@@ -405,8 +407,10 @@ const installFixture = async (page: Page, state: NavigationFixture) => {
|
||||
if (operation === 'turns.reserved.getGeneral' || operation === 'turns.reserved.getNation') {
|
||||
return response({ turns: state.reservedTurns ?? [], revision: 0 });
|
||||
}
|
||||
if (operation === 'messages.getRecent') return response(emptyMessages(state.permission));
|
||||
if (operation === 'messages.getContacts') return response({ nation: [] });
|
||||
if (operation === 'messages.getRecent') {
|
||||
return response(state.messages ?? emptyMessages(state.permission));
|
||||
}
|
||||
if (operation === 'messages.getContacts') return response(state.messageContacts ?? { nation: [] });
|
||||
if (operation === 'general.getRecentRecords') {
|
||||
return response({
|
||||
global: [{ id: 3, text: '장수 동향 기록' }],
|
||||
@@ -617,7 +621,23 @@ test('desktop menus preserve ref columns, prefix-safe routes, and controlled dro
|
||||
await expect(page.locator('.main-nation-menu [data-navigation-id="tournament"]')).toHaveClass(/highlight/);
|
||||
|
||||
const gameInfoButton = global.locator('[data-menu-id="game-info"]');
|
||||
const bettingButton = global.locator('[data-navigation-id="nation-betting"]');
|
||||
await expect
|
||||
.poll(() =>
|
||||
bettingButton.evaluate((element) => ({
|
||||
backgroundColor: getComputedStyle(element).backgroundColor,
|
||||
backgroundImage: getComputedStyle(element).backgroundImage,
|
||||
}))
|
||||
)
|
||||
.toEqual({ backgroundColor: 'rgb(0, 88, 44)', backgroundImage: 'none' });
|
||||
await bettingButton.hover();
|
||||
await expect
|
||||
.poll(() => bettingButton.evaluate((element) => getComputedStyle(element).backgroundColor))
|
||||
.toBe('rgb(0, 88, 44)');
|
||||
await gameInfoButton.focus();
|
||||
await expect
|
||||
.poll(() => gameInfoButton.evaluate((element) => getComputedStyle(element).backgroundColor))
|
||||
.toBe('rgb(0, 88, 44)');
|
||||
await gameInfoButton.press('Enter');
|
||||
await expect(gameInfoButton).toHaveAttribute('aria-expanded', 'true');
|
||||
await expect(global.locator('#global-menu-game-info')).toBeVisible();
|
||||
@@ -631,6 +651,74 @@ test('desktop menus preserve ref columns, prefix-safe routes, and controlled dro
|
||||
await persistArtifact(page, `${basePath.slice(1)}-desktop-1200`);
|
||||
});
|
||||
|
||||
test('pure NPC message senders are not rendered as reply targets', async ({ page }) => {
|
||||
const target = (generalId: number, generalName: string) => ({
|
||||
generalId,
|
||||
generalName,
|
||||
nationId: 1,
|
||||
nationName: '위',
|
||||
color: '#008000',
|
||||
icon: '',
|
||||
});
|
||||
const messages = {
|
||||
...emptyMessages(0),
|
||||
public: [
|
||||
{
|
||||
id: 102,
|
||||
text: 'NPC 메시지',
|
||||
time: '2026-08-12 12:00:00',
|
||||
msgType: 'public',
|
||||
src: target(22, '순수NPC'),
|
||||
dest: null,
|
||||
option: {},
|
||||
},
|
||||
{
|
||||
id: 101,
|
||||
text: '유저 메시지',
|
||||
time: '2026-08-12 11:59:00',
|
||||
msgType: 'public',
|
||||
src: target(21, '유저장수'),
|
||||
dest: null,
|
||||
option: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
const state: NavigationFixture = {
|
||||
officerLevel: 1,
|
||||
permission: 0,
|
||||
nationLevel: 1,
|
||||
stage: 0,
|
||||
npcMode: 1,
|
||||
generalMeCalls: 0,
|
||||
operations: [],
|
||||
messages,
|
||||
messageContacts: {
|
||||
nation: [
|
||||
{
|
||||
nationId: 1,
|
||||
mailbox: 9001,
|
||||
name: '위',
|
||||
color: '#008000',
|
||||
general: [[21, '유저장수', 0]],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
await installFixture(page, state);
|
||||
await page.setViewportSize({ width: 1200, height: 900 });
|
||||
await waitForMain(page);
|
||||
|
||||
const npcMessage = page.locator('.desktop-message-panel .msg-plate[data-id="102"]');
|
||||
const userMessage = page.locator('.desktop-message-panel .msg-plate[data-id="101"]');
|
||||
await expect(npcMessage.locator('.msg-header')).toContainText('순수NPC:위');
|
||||
await expect(npcMessage.locator('.msg-header')).not.toContainText('↩');
|
||||
await expect(npcMessage.getByRole('button', { name: /순수NPC/ })).toHaveCount(0);
|
||||
await expect(userMessage.getByRole('button', { name: /유저장수:위.*↩/ })).toBeVisible();
|
||||
await userMessage.getByRole('button', { name: /유저장수:위.*↩/ }).click();
|
||||
await expect(page.locator('.desktop-message-panel #mailbox_list')).toHaveValue('21');
|
||||
await persistArtifact(page, `${basePath.slice(1)}-npc-reply-targets-desktop-1200`);
|
||||
});
|
||||
|
||||
test('main cards and command input stay inside their Ref-sized grid slots', async ({ page }) => {
|
||||
const state: NavigationFixture = {
|
||||
officerLevel: 1,
|
||||
@@ -925,6 +1013,15 @@ test('the 939/940 boundary switches to the Ref-style 500px single document', asy
|
||||
await expect(page.locator('.main-mobile-bottom')).toBeVisible();
|
||||
|
||||
await page.setViewportSize({ width: 500, height: 900 });
|
||||
await expect
|
||||
.poll(() =>
|
||||
page
|
||||
.locator('.main-global-menu')
|
||||
.first()
|
||||
.locator('[data-navigation-id="nation-betting"]')
|
||||
.evaluate((element) => getComputedStyle(element).backgroundColor)
|
||||
)
|
||||
.toBe('rgb(0, 88, 44)');
|
||||
const documentGeometry = await page.locator('.main-page').evaluate((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return {
|
||||
|
||||
@@ -115,6 +115,33 @@ const isActive = (link: MainNavigationLinkItem) => link.id === 'survey' && props
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.main-global-menu > :deep(.main-menu-link),
|
||||
.main-menu-popup > .main-menu-button,
|
||||
.main-menu-split > :deep(.main-menu-link),
|
||||
.main-menu-split > .main-menu-split__toggle {
|
||||
border-color: var(--sammo-button-navigation-border);
|
||||
background-color: var(--sammo-button-navigation-bg);
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
.main-global-menu > :deep(.main-menu-link:hover),
|
||||
.main-global-menu > :deep(.main-menu-link:focus-visible),
|
||||
.main-global-menu > :deep(.main-menu-link:active),
|
||||
.main-menu-popup > .main-menu-button:hover,
|
||||
.main-menu-popup > .main-menu-button:focus-visible,
|
||||
.main-menu-popup > .main-menu-button:active,
|
||||
.main-menu-popup > .main-menu-button[aria-expanded='true'],
|
||||
.main-menu-split > :deep(.main-menu-link:hover),
|
||||
.main-menu-split > :deep(.main-menu-link:focus-visible),
|
||||
.main-menu-split > :deep(.main-menu-link:active),
|
||||
.main-menu-split > .main-menu-split__toggle:hover,
|
||||
.main-menu-split > .main-menu-split__toggle:focus-visible,
|
||||
.main-menu-split > .main-menu-split__toggle:active,
|
||||
.main-menu-split > .main-menu-split__toggle[aria-expanded='true'] {
|
||||
border-color: var(--sammo-button-navigation-border);
|
||||
background-color: var(--sammo-button-navigation-bg);
|
||||
}
|
||||
|
||||
.main-menu-popup > .main-menu-button,
|
||||
.main-menu-split > :deep(.main-menu-link) {
|
||||
width: 100%;
|
||||
|
||||
@@ -87,6 +87,13 @@ const bucket = (type: MessageType): MessageEntry[] => props.messages?.[type] ??
|
||||
const visibleMessages = (type: MessageType): MessageEntry[] => bucket(type).slice(0, visibleLimits[type]);
|
||||
|
||||
const permission = computed(() => props.messages?.permission ?? -1);
|
||||
const replyableGeneralIds = computed(() =>
|
||||
props.mailboxGroups.flatMap((group) =>
|
||||
group.options
|
||||
.filter((option) => !option.disabled && option.value > 0 && option.value < 9000)
|
||||
.map((option) => option.value)
|
||||
)
|
||||
);
|
||||
|
||||
const setMailbox = (value: string) => {
|
||||
const parsed = Number(value);
|
||||
@@ -239,6 +246,7 @@ const forwardResponse = (messageId: number, response: boolean) => {
|
||||
:nation-id="nationId"
|
||||
:permission="permission"
|
||||
:can-respond-diplomacy="canRespondDiplomacy"
|
||||
:replyable-general-ids="replyableGeneralIds"
|
||||
@set-target="setReplyTarget"
|
||||
@delete="emit('delete', $event)"
|
||||
@respond="forwardResponse"
|
||||
|
||||
@@ -29,6 +29,7 @@ const props = defineProps<{
|
||||
nationId: number;
|
||||
permission: number;
|
||||
canRespondDiplomacy: boolean;
|
||||
replyableGeneralIds: number[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -107,6 +108,7 @@ const isBright = (color: string): boolean => {
|
||||
};
|
||||
|
||||
const iconUrl = computed(() => resolveMessageGeneralIconUrl(props.message.src.icon));
|
||||
const canReplyToGeneral = (target: MessageTarget): boolean => props.replyableGeneralIds.includes(target.generalId);
|
||||
|
||||
const targetClass = (target: MessageTarget) => ({
|
||||
'msg-target': true,
|
||||
@@ -167,6 +169,7 @@ onBeforeUnmount(() => {
|
||||
>
|
||||
<span class="msg-from-to">▶</span>
|
||||
<button
|
||||
v-if="canReplyToGeneral(destination)"
|
||||
:class="targetClass(destination)"
|
||||
:style="{ backgroundColor: destination.color }"
|
||||
type="button"
|
||||
@@ -174,9 +177,13 @@ onBeforeUnmount(() => {
|
||||
>
|
||||
{{ destination.generalName }}:{{ destination.nationName }} | ↩
|
||||
</button>
|
||||
<span v-else :class="targetClass(destination)" :style="{ backgroundColor: destination.color }">
|
||||
{{ destination.generalName }}:{{ destination.nationName }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button
|
||||
v-if="canReplyToGeneral(message.src)"
|
||||
:class="targetClass(message.src)"
|
||||
:style="{ backgroundColor: message.src.color }"
|
||||
type="button"
|
||||
@@ -184,6 +191,9 @@ onBeforeUnmount(() => {
|
||||
>
|
||||
{{ message.src.generalName }}:{{ message.src.nationName }} | ↩
|
||||
</button>
|
||||
<span v-else :class="targetClass(message.src)" :style="{ backgroundColor: message.src.color }">
|
||||
{{ message.src.generalName }}:{{ message.src.nationName }}
|
||||
</span>
|
||||
<span class="msg-from-to">▶</span>
|
||||
<span :class="targetClass(destination)" :style="{ backgroundColor: destination.color }"
|
||||
>나</span
|
||||
@@ -241,7 +251,7 @@ onBeforeUnmount(() => {
|
||||
</template>
|
||||
|
||||
<button
|
||||
v-else-if="message.src.generalId !== generalId"
|
||||
v-else-if="message.src.generalId !== generalId && canReplyToGeneral(message.src)"
|
||||
:class="targetClass(message.src)"
|
||||
:style="{ backgroundColor: message.src.color }"
|
||||
type="button"
|
||||
@@ -249,6 +259,13 @@ onBeforeUnmount(() => {
|
||||
>
|
||||
{{ message.src.generalName }}:{{ message.src.nationName }} | ↩
|
||||
</button>
|
||||
<span
|
||||
v-else-if="message.src.generalId !== generalId"
|
||||
:class="targetClass(message.src)"
|
||||
:style="{ backgroundColor: message.src.color }"
|
||||
>
|
||||
{{ message.src.generalName }}:{{ message.src.nationName }}
|
||||
</span>
|
||||
<span v-else :class="targetClass(message.src)" :style="{ backgroundColor: message.src.color }">
|
||||
{{ message.src.generalName }}
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user