merge: align main menu and NPC reply affordance
This commit is contained in:
@@ -32,6 +32,8 @@ type NavigationFixture = {
|
|||||||
refreshDelayMs?: number;
|
refreshDelayMs?: number;
|
||||||
largeCommandTable?: boolean;
|
largeCommandTable?: boolean;
|
||||||
reservedTurns?: Array<{ index: number; action: string; args: Record<string, unknown> }>;
|
reservedTurns?: Array<{ index: number; action: string; args: Record<string, unknown> }>;
|
||||||
|
messages?: unknown;
|
||||||
|
messageContacts?: unknown;
|
||||||
dashboardResponses?: Array<{
|
dashboardResponses?: Array<{
|
||||||
bytes: number;
|
bytes: number;
|
||||||
contextKind: string | null;
|
contextKind: string | null;
|
||||||
@@ -405,8 +407,10 @@ const installFixture = async (page: Page, state: NavigationFixture) => {
|
|||||||
if (operation === 'turns.reserved.getGeneral' || operation === 'turns.reserved.getNation') {
|
if (operation === 'turns.reserved.getGeneral' || operation === 'turns.reserved.getNation') {
|
||||||
return response({ turns: state.reservedTurns ?? [], revision: 0 });
|
return response({ turns: state.reservedTurns ?? [], revision: 0 });
|
||||||
}
|
}
|
||||||
if (operation === 'messages.getRecent') return response(emptyMessages(state.permission));
|
if (operation === 'messages.getRecent') {
|
||||||
if (operation === 'messages.getContacts') return response({ nation: [] });
|
return response(state.messages ?? emptyMessages(state.permission));
|
||||||
|
}
|
||||||
|
if (operation === 'messages.getContacts') return response(state.messageContacts ?? { nation: [] });
|
||||||
if (operation === 'general.getRecentRecords') {
|
if (operation === 'general.getRecentRecords') {
|
||||||
return response({
|
return response({
|
||||||
global: [{ id: 3, text: '장수 동향 기록' }],
|
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/);
|
await expect(page.locator('.main-nation-menu [data-navigation-id="tournament"]')).toHaveClass(/highlight/);
|
||||||
|
|
||||||
const gameInfoButton = global.locator('[data-menu-id="game-info"]');
|
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 gameInfoButton.focus();
|
||||||
|
await expect
|
||||||
|
.poll(() => gameInfoButton.evaluate((element) => getComputedStyle(element).backgroundColor))
|
||||||
|
.toBe('rgb(0, 88, 44)');
|
||||||
await gameInfoButton.press('Enter');
|
await gameInfoButton.press('Enter');
|
||||||
await expect(gameInfoButton).toHaveAttribute('aria-expanded', 'true');
|
await expect(gameInfoButton).toHaveAttribute('aria-expanded', 'true');
|
||||||
await expect(global.locator('#global-menu-game-info')).toBeVisible();
|
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`);
|
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 }) => {
|
test('main cards and command input stay inside their Ref-sized grid slots', async ({ page }) => {
|
||||||
const state: NavigationFixture = {
|
const state: NavigationFixture = {
|
||||||
officerLevel: 1,
|
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 expect(page.locator('.main-mobile-bottom')).toBeVisible();
|
||||||
|
|
||||||
await page.setViewportSize({ width: 500, height: 900 });
|
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 documentGeometry = await page.locator('.main-page').evaluate((element) => {
|
||||||
const rect = element.getBoundingClientRect();
|
const rect = element.getBoundingClientRect();
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -115,6 +115,33 @@ const isActive = (link: MainNavigationLinkItem) => link.id === 'survey' && props
|
|||||||
min-width: 0;
|
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-popup > .main-menu-button,
|
||||||
.main-menu-split > :deep(.main-menu-link) {
|
.main-menu-split > :deep(.main-menu-link) {
|
||||||
width: 100%;
|
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 visibleMessages = (type: MessageType): MessageEntry[] => bucket(type).slice(0, visibleLimits[type]);
|
||||||
|
|
||||||
const permission = computed(() => props.messages?.permission ?? -1);
|
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 setMailbox = (value: string) => {
|
||||||
const parsed = Number(value);
|
const parsed = Number(value);
|
||||||
@@ -239,6 +246,7 @@ const forwardResponse = (messageId: number, response: boolean) => {
|
|||||||
:nation-id="nationId"
|
:nation-id="nationId"
|
||||||
:permission="permission"
|
:permission="permission"
|
||||||
:can-respond-diplomacy="canRespondDiplomacy"
|
:can-respond-diplomacy="canRespondDiplomacy"
|
||||||
|
:replyable-general-ids="replyableGeneralIds"
|
||||||
@set-target="setReplyTarget"
|
@set-target="setReplyTarget"
|
||||||
@delete="emit('delete', $event)"
|
@delete="emit('delete', $event)"
|
||||||
@respond="forwardResponse"
|
@respond="forwardResponse"
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const props = defineProps<{
|
|||||||
nationId: number;
|
nationId: number;
|
||||||
permission: number;
|
permission: number;
|
||||||
canRespondDiplomacy: boolean;
|
canRespondDiplomacy: boolean;
|
||||||
|
replyableGeneralIds: number[];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -107,6 +108,7 @@ const isBright = (color: string): boolean => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const iconUrl = computed(() => resolveMessageGeneralIconUrl(props.message.src.icon));
|
const iconUrl = computed(() => resolveMessageGeneralIconUrl(props.message.src.icon));
|
||||||
|
const canReplyToGeneral = (target: MessageTarget): boolean => props.replyableGeneralIds.includes(target.generalId);
|
||||||
|
|
||||||
const targetClass = (target: MessageTarget) => ({
|
const targetClass = (target: MessageTarget) => ({
|
||||||
'msg-target': true,
|
'msg-target': true,
|
||||||
@@ -167,6 +169,7 @@ onBeforeUnmount(() => {
|
|||||||
>
|
>
|
||||||
<span class="msg-from-to">▶</span>
|
<span class="msg-from-to">▶</span>
|
||||||
<button
|
<button
|
||||||
|
v-if="canReplyToGeneral(destination)"
|
||||||
:class="targetClass(destination)"
|
:class="targetClass(destination)"
|
||||||
:style="{ backgroundColor: destination.color }"
|
:style="{ backgroundColor: destination.color }"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -174,9 +177,13 @@ onBeforeUnmount(() => {
|
|||||||
>
|
>
|
||||||
{{ destination.generalName }}:{{ destination.nationName }} | ↩
|
{{ destination.generalName }}:{{ destination.nationName }} | ↩
|
||||||
</button>
|
</button>
|
||||||
|
<span v-else :class="targetClass(destination)" :style="{ backgroundColor: destination.color }">
|
||||||
|
{{ destination.generalName }}:{{ destination.nationName }}
|
||||||
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<button
|
<button
|
||||||
|
v-if="canReplyToGeneral(message.src)"
|
||||||
:class="targetClass(message.src)"
|
:class="targetClass(message.src)"
|
||||||
:style="{ backgroundColor: message.src.color }"
|
:style="{ backgroundColor: message.src.color }"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -184,6 +191,9 @@ onBeforeUnmount(() => {
|
|||||||
>
|
>
|
||||||
{{ message.src.generalName }}:{{ message.src.nationName }} | ↩
|
{{ message.src.generalName }}:{{ message.src.nationName }} | ↩
|
||||||
</button>
|
</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="msg-from-to">▶</span>
|
||||||
<span :class="targetClass(destination)" :style="{ backgroundColor: destination.color }"
|
<span :class="targetClass(destination)" :style="{ backgroundColor: destination.color }"
|
||||||
>나</span
|
>나</span
|
||||||
@@ -241,7 +251,7 @@ onBeforeUnmount(() => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
v-else-if="message.src.generalId !== generalId"
|
v-else-if="message.src.generalId !== generalId && canReplyToGeneral(message.src)"
|
||||||
:class="targetClass(message.src)"
|
:class="targetClass(message.src)"
|
||||||
:style="{ backgroundColor: message.src.color }"
|
:style="{ backgroundColor: message.src.color }"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -249,6 +259,13 @@ onBeforeUnmount(() => {
|
|||||||
>
|
>
|
||||||
{{ message.src.generalName }}:{{ message.src.nationName }} | ↩
|
{{ message.src.generalName }}:{{ message.src.nationName }} | ↩
|
||||||
</button>
|
</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 }">
|
<span v-else :class="targetClass(message.src)" :style="{ backgroundColor: message.src.color }">
|
||||||
{{ message.src.generalName }}
|
{{ message.src.generalName }}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user