merge: 메인 메시지 재야 광고 기능 통합

This commit is contained in:
2026-08-21 01:19:02 +00:00
4 changed files with 181 additions and 6 deletions
+110
View File
@@ -260,6 +260,116 @@ describe('messages router missing-flow compatibility', () => {
expect(queryRaw.mock.calls[0]?.slice(1)).toEqual(expect.arrayContaining([9002, 'diplomacy'])); expect(queryRaw.mock.calls[0]?.slice(1)).toEqual(expect.arrayContaining([9002, 'diplomacy']));
}); });
it('allows a ruler to send a recruitment advertisement to the wanderer mailbox', async () => {
const ruler = { ...general, officerLevel: 12 } as GeneralRow;
const queryRaw = vi.fn(async () => [{ id: 53 }]);
const changeJournal = new ChangeJournal();
const { caller } = buildContext(
{
$queryRaw: queryRaw,
general: {
findUnique: vi.fn(async () => ruler),
findMany: vi.fn(async () => []),
},
nation: {
findMany: vi.fn(async () => []),
findUnique: vi.fn(async () => ({ id: 1, name: '위', color: '#112233', meta: {} })),
},
},
{ changeJournal }
);
const result = await caller.messages.send({
generalId: ruler.id,
mailbox: 9000,
text: '우리 나라로 와주세요',
});
expect(result.msgType).toBe('diplomacy');
expect(queryRaw.mock.calls[0]?.slice(1)).toEqual(expect.arrayContaining([9000, 'diplomacy']));
expect(queryRaw).toHaveBeenCalledTimes(2);
expect(changeJournal.snapshot()).toEqual([
{ domain: 'messages.mailbox', entityId: 9000 },
{ domain: 'messages.mailbox', entityId: 9001 },
]);
});
it('keeps the wanderer mailbox unavailable to a non-diplomat on the server', async () => {
const queryRaw = vi.fn(async () => [{ id: 54 }]);
const { caller } = buildContext({
$queryRaw: queryRaw,
nation: {
findMany: vi.fn(async () => []),
findUnique: vi.fn(async () => ({ id: 1, name: '위', color: '#112233', meta: {} })),
},
});
const result = await caller.messages.send({
generalId: general.id,
mailbox: 9000,
text: '권한 없는 재야 광고',
});
expect(result.msgType).toBe('national');
expect(queryRaw).toHaveBeenCalledTimes(1);
expect(queryRaw.mock.calls[0]?.slice(1)).toEqual(expect.arrayContaining([9001, 'national']));
});
it('shows a wanderer the advertisement stored in mailbox 9000 without diplomacy redaction', async () => {
const wanderer = { ...general, nationId: 0, officerLevel: 0 } as GeneralRow;
const advertisementRow = {
id: 55,
mailbox: 9000,
type: 'diplomacy',
src: 9001,
dest: 9000,
time: new Date(),
valid_until: new Date('9999-12-31T00:00:00Z'),
message: {
src: {
generalId: 1,
generalName: '위왕',
nationId: 1,
nationName: '위',
color: '#112233',
icon: '',
},
dest: {
generalId: 0,
generalName: '',
nationId: 0,
nationName: '재야',
color: '#000000',
icon: '',
},
text: '우리 나라로 와주세요',
option: {},
},
};
const queryRaw = vi.fn(async (...args: unknown[]) => {
const values = args.slice(1);
return values.includes(9000) && values.includes('diplomacy') ? [advertisementRow] : [];
});
const { caller } = buildContext({
$queryRaw: queryRaw,
general: {
findUnique: vi.fn(async () => wanderer),
findMany: vi.fn(async () => []),
},
});
const result = await caller.messages.getRecent({ generalId: wanderer.id });
expect(result.permission).toBe(-1);
expect(result.diplomacy).toEqual([
expect.objectContaining({
text: '우리 나라로 와주세요',
dest: expect.objectContaining({ nationId: 0, nationName: '재야' }),
option: {},
}),
]);
});
it('blocks private messages between foreign ambassadors', async () => { it('blocks private messages between foreign ambassadors', async () => {
const ambassador = { const ambassador = {
...general, ...general,
@@ -220,7 +220,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
label: '외교메시지', label: '외교메시지',
color: '#000000', color: '#000000',
options: contacts options: contacts
.filter((nation) => nation.mailbox !== ownMailbox && nation.nationId > 0) .filter((nation) => nation.mailbox !== ownMailbox)
.map((nation) => ({ .map((nation) => ({
label: nation.name, label: nation.name,
value: nation.mailbox, value: nation.mailbox,
@@ -1,8 +1,11 @@
import { expect, test, type Page, type Route } from '@playwright/test'; import { expect, test, type Page, type Route } from '@playwright/test';
import { mkdir } from 'node:fs/promises';
import { resolve } from 'node:path';
import { canonicalFrontendFixture as fixture } from './fixtures/canonical.js'; import { canonicalFrontendFixture as fixture } from './fixtures/canonical.js';
const gamePort = process.env.FRONTEND_PARITY_GAME_PORT ?? '15102'; const gamePort = process.env.FRONTEND_PARITY_GAME_PORT ?? '15102';
const artifactRoot = process.env.FRONTEND_PARITY_ARTIFACT_DIR;
const response = (data: unknown) => ({ result: { data } }); const response = (data: unknown) => ({ result: { data } });
const errorResponse = (path: string, message: string) => ({ const errorResponse = (path: string, message: string) => ({
error: { error: {
@@ -42,7 +45,35 @@ const general = {
const generalContext = { const generalContext = {
general, general,
city: null, city: null,
nation: null, nation: {
id: 1,
name: '테스트국',
color: '#d32f2f',
level: 1,
levelName: '군벌',
gold: 1000,
rice: 1000,
tech: 1000,
typeCode: 'test',
typeName: '테스트',
typePros: '-',
typeCons: '-',
capitalCityId: 1,
capitalCityName: '낙양',
population: { cityCount: 1, current: 10000, max: 20000 },
crew: { generalCount: 2, current: 1000, max: 16000 },
power: 100,
bill: 10,
taxRate: 10,
strategicCommandLimit: 0,
diplomaticLimit: 0,
prohibitScout: false,
prohibitWar: false,
techLevel: 1,
techLimited: false,
topChiefs: { 12: null, 11: null },
impossibleStrategicCommands: [],
},
settings: {}, settings: {},
penalties: {}, penalties: {},
}; };
@@ -373,14 +404,25 @@ for (const viewport of [
boxShadow: getComputedStyle(element).boxShadow, boxShadow: getComputedStyle(element).boxShadow,
})) }))
).toEqual({ outlineWidth: '0px', boxShadow: 'none' }); ).toEqual({ outlineWidth: '0px', boxShadow: 'none' });
if (artifactRoot) {
await mkdir(artifactRoot, { recursive: true });
await page.locator('.MessagePanel').screenshot({
path: resolve(artifactRoot, `message-panel-${viewport.width}.png`),
animations: 'disabled',
});
}
}); });
} }
test('exposes ambassador targets, reply, read, delete, and successful send interactions', async ({ page }) => { test('exposes nation targets including wanderers, reply, read, delete, and successful send interactions', async ({
page,
}) => {
const mutations = await installFixture(page, { permission: 4 }); const mutations = await installFixture(page, { permission: 4 });
await openMessages(page, { width: 500, height: 900 }); await openMessages(page, { width: 500, height: 900 });
const select = page.getByLabel('메시지 수신 대상'); const select = page.getByLabel('메시지 수신 대상');
await expect(select.locator('optgroup[label="외교메시지"] option[value="9000"]')).toHaveText('재야');
await expect(select.locator('option[value="9002"]')).toHaveCount(1); await expect(select.locator('option[value="9002"]')).toHaveCount(1);
await expect(select.locator('option[value="8"]')).toBeDisabled(); await expect(select.locator('option[value="8"]')).toBeDisabled();
await expect(select.locator('option[value="9"]')).toBeEnabled(); await expect(select.locator('option[value="9"]')).toBeEnabled();
@@ -396,11 +438,21 @@ test('exposes ambassador targets, reply, read, delete, and successful send inter
await deleteButton.click(); await deleteButton.click();
await expect.poll(() => mutations.filter((entry) => entry.operation === 'messages.delete').length).toBe(1); await expect.poll(() => mutations.filter((entry) => entry.operation === 'messages.delete').length).toBe(1);
await select.selectOption('9999'); await select.selectOption('9000');
await page.getByLabel('메시지 입력').fill('전송 성공'); await page.getByLabel('메시지 입력').fill('우리 나라로 와주세요');
if (artifactRoot) {
await mkdir(artifactRoot, { recursive: true });
await page.locator('.MessageInputForm').screenshot({
path: resolve(artifactRoot, 'wanderer-recruitment-target-500.png'),
animations: 'disabled',
});
}
await page.getByRole('button', { name: '서신전달&갱신' }).click(); await page.getByRole('button', { name: '서신전달&갱신' }).click();
await expect(page.getByLabel('메시지 입력')).toHaveValue(''); await expect(page.getByLabel('메시지 입력')).toHaveValue('');
await expect.poll(() => mutations.filter((entry) => entry.operation === 'messages.send').length).toBe(1); await expect.poll(() => mutations.filter((entry) => entry.operation === 'messages.send').length).toBe(1);
expect(JSON.stringify(mutations.find((entry) => entry.operation === 'messages.send')?.body)).toContain(
'"mailbox":9000'
);
}); });
test('accepts recruitment and declines invader prompts through private-message controls', async ({ page }) => { test('accepts recruitment and declines invader prompts through private-message controls', async ({ page }) => {
@@ -442,6 +494,7 @@ test('redacts diplomacy for a low-permission general and preserves the failed-se
await openMessages(page, { width: 500, height: 900 }); await openMessages(page, { width: 500, height: 900 });
const select = page.getByLabel('메시지 수신 대상'); const select = page.getByLabel('메시지 수신 대상');
await expect(select.locator('option[value="9000"]')).toHaveCount(0);
await expect(select.locator('option[value="9002"]')).toHaveCount(0); await expect(select.locator('option[value="9002"]')).toHaveCount(0);
await expect(page.locator('.DiplomacyTalk')).toContainText('삭제된 메시지입니다'); await expect(page.locator('.DiplomacyTalk')).toContainText('삭제된 메시지입니다');
await expect(page.locator('.DiplomacyTalk')).not.toContainText('외교 메시지 본문'); await expect(page.locator('.DiplomacyTalk')).not.toContainText('외교 메시지 본문');
@@ -65,6 +65,14 @@ const measure = async (browser, name, viewport) => {
await ensureGeneral(page); await ensureGeneral(page);
await page.locator('.MessagePanel').waitFor({ state: 'visible', timeout: 30_000 }); await page.locator('.MessagePanel').waitFor({ state: 'visible', timeout: 30_000 });
await page.locator('.BoardHeader').first().waitFor({ state: 'visible' }); await page.locator('.BoardHeader').first().waitFor({ state: 'visible' });
const mailboxOptions = await page.locator('.MessageInputForm select option').evaluateAll((options) =>
options.map((option) => ({
value: option.value,
label: option.textContent?.trim() ?? '',
group: option.parentElement?.tagName === 'OPTGROUP' ? option.parentElement.label : '',
disabled: option.disabled,
}))
);
const marker = `computed-dom-${name}-${Date.now()}`; const marker = `computed-dom-${name}-${Date.now()}`;
await page.locator('.MessageInputForm select').selectOption('9999'); await page.locator('.MessageInputForm select').selectOption('9999');
await page.locator('.MessageInputForm input').fill(marker); await page.locator('.MessageInputForm input').fill(marker);
@@ -160,7 +168,11 @@ const measure = async (browser, name, viewport) => {
page.once('dialog', (dialog) => dialog.accept()); page.once('dialog', (dialog) => dialog.accept());
await deleteButton.click(); await deleteButton.click();
} }
return { ...result, interaction: { hover, focus } }; return {
...result,
mailboxOptions,
interaction: { hover, focus },
};
} finally { } finally {
await context.close(); await context.close();
} }