merge: 메인 메시지 재야 광고 기능 통합
This commit is contained in:
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user