feat: 응답 가능한 서신과 턴 시간 호환 이관
등용장과 이민족 선택 응답을 turn daemon transaction으로 연결하고 Ref의 통일 이후 상태 전이와 수신자별 메시지 저장 규칙을 보존한다.\n\n유산 턴 시간 변경을 nextTurnTimeBase 기반 결정적 계산으로 바로잡고 API, 엔진, Chromium 회귀를 추가한다.
This commit is contained in:
@@ -41,31 +41,8 @@ const general = {
|
||||
|
||||
const generalContext = {
|
||||
general,
|
||||
city: {
|
||||
id: 1,
|
||||
name: '낙양',
|
||||
level: 7,
|
||||
nationId: 1,
|
||||
population: 50000,
|
||||
agriculture: 5000,
|
||||
commerce: 5000,
|
||||
security: 5000,
|
||||
defence: 5000,
|
||||
wall: 5000,
|
||||
supplyState: 1,
|
||||
frontState: 2,
|
||||
},
|
||||
nation: {
|
||||
id: 1,
|
||||
name: '테스트국',
|
||||
color: '#d32f2f',
|
||||
level: 5,
|
||||
gold: 10000,
|
||||
rice: 10000,
|
||||
tech: 1200,
|
||||
typeCode: 'che_군벌',
|
||||
capitalCityId: 1,
|
||||
},
|
||||
city: null,
|
||||
nation: null,
|
||||
settings: {},
|
||||
penalties: {},
|
||||
};
|
||||
@@ -111,12 +88,30 @@ const buildMessages = (permission: number) => ({
|
||||
{
|
||||
id: 103,
|
||||
msgType: 'private',
|
||||
src: foreignTarget,
|
||||
src: target(9, '상대일반', 2, '상대국', '#2457a6'),
|
||||
dest: ownTarget,
|
||||
text: '개인 메시지 본문',
|
||||
option: {},
|
||||
time: messageTime,
|
||||
},
|
||||
{
|
||||
id: 105,
|
||||
msgType: 'private',
|
||||
src: foreignTarget,
|
||||
dest: ownTarget,
|
||||
text: '상대국으로 망명 권유 서신',
|
||||
option: { action: 'scout', used: false },
|
||||
time: messageTime,
|
||||
},
|
||||
{
|
||||
id: 106,
|
||||
msgType: 'private',
|
||||
src: target(0, '', 0, 'System', '#000000'),
|
||||
dest: ownTarget,
|
||||
text: '이벤트 게임으로 이민족[보통]을 소환',
|
||||
option: { action: 'raiseInvader', args: [-2, -1.2, -1, -0.5], used: false },
|
||||
time: messageTime,
|
||||
},
|
||||
],
|
||||
diplomacy: [
|
||||
{
|
||||
@@ -192,12 +187,40 @@ const installFixture = async (
|
||||
await page.route('**/che/api/trpc/**', async (route) => {
|
||||
const body = route.request().postDataJSON();
|
||||
const results = operationNames(route).map((operation) => {
|
||||
if (operation === 'dashboard.getContextBundleDelta') {
|
||||
return response({
|
||||
context: {
|
||||
kind: 'snapshot',
|
||||
revision: 'AAAAAAAAAAAAAAAAAAAAAA',
|
||||
data: generalContext,
|
||||
},
|
||||
commandTable: {
|
||||
kind: 'snapshot',
|
||||
revision: 'BBBBBBBBBBBBBBBBBBBBBB',
|
||||
data: { general: [], nation: [] },
|
||||
},
|
||||
boardAccess: {
|
||||
kind: 'snapshot',
|
||||
revision: 'CCCCCCCCCCCCCCCCCCCCCC',
|
||||
data: { canMeeting: true, canSecret: true, permission: options.permission },
|
||||
},
|
||||
});
|
||||
}
|
||||
if (operation === 'auth.status') return response({ userId: 'frontend-parity-user' });
|
||||
if (operation === 'lobby.info') {
|
||||
return response({ ...fixture.game.lobby, myGeneral: general });
|
||||
}
|
||||
if (operation === 'general.me') return response(generalContext);
|
||||
if (operation === 'world.getMapLayout') return response(fixture.game.mapLayout);
|
||||
if (operation === 'world.getState') {
|
||||
return response({
|
||||
currentYear: 197,
|
||||
currentMonth: 7,
|
||||
tickSeconds: 3600,
|
||||
config: { npcMode: 0, const: {}, environment: {} },
|
||||
meta: {},
|
||||
});
|
||||
}
|
||||
if (operation === 'world.getMap') {
|
||||
return response({ ...fixture.game.map, myCity: 1, myNation: 1 });
|
||||
}
|
||||
@@ -251,7 +274,10 @@ const openMessages = async (page: Page, viewport: { width: number; height: numbe
|
||||
await page.goto(`http://127.0.0.1:${gamePort}/che/`);
|
||||
await expect(page.getByRole('heading', { name: '전장 현황' })).toBeVisible();
|
||||
if (viewport.width <= 1024) {
|
||||
await page.getByRole('button', { name: '메시지', exact: true }).click();
|
||||
const mobileMessageButton = page.getByRole('button', { name: '메시지', exact: true });
|
||||
if ((await mobileMessageButton.count()) > 0) {
|
||||
await mobileMessageButton.click();
|
||||
}
|
||||
}
|
||||
await expect(page.locator('.MessagePanel')).toBeVisible();
|
||||
};
|
||||
@@ -359,8 +385,8 @@ test('exposes ambassador targets, reply, read, delete, and successful send inter
|
||||
await expect(select.locator('option[value="8"]')).toBeDisabled();
|
||||
await expect(select.locator('option[value="9"]')).toBeEnabled();
|
||||
|
||||
await page.locator('.PrivateTalk .msg-target').filter({ hasText: '상대장수' }).click();
|
||||
await expect(select).toHaveValue('8');
|
||||
await page.locator('.PrivateTalk .msg-target').filter({ hasText: '상대일반' }).click();
|
||||
await expect(select).toHaveValue('9');
|
||||
|
||||
await page.locator('.PrivateTalk').getByRole('button', { name: '모두 읽음' }).click();
|
||||
await expect.poll(() => mutations.filter((entry) => entry.operation === 'messages.readLatest').length).toBe(1);
|
||||
@@ -377,6 +403,37 @@ test('exposes ambassador targets, reply, read, delete, and successful send inter
|
||||
await expect.poll(() => mutations.filter((entry) => entry.operation === 'messages.send').length).toBe(1);
|
||||
});
|
||||
|
||||
test('accepts recruitment and declines invader prompts through private-message controls', async ({ page }) => {
|
||||
const mutations = await installFixture(page, { permission: 4 });
|
||||
await openMessages(page, { width: 500, height: 900 });
|
||||
|
||||
const recruitment = page.locator('.PrivateTalk .msg-plate').filter({ hasText: '망명 권유 서신' });
|
||||
const invader = page.locator('.PrivateTalk .msg-plate').filter({ hasText: '이민족[보통]을 소환' });
|
||||
await expect(recruitment.getByRole('button', { name: '수락' })).toBeVisible();
|
||||
await expect(invader.getByRole('button', { name: '거절' })).toBeVisible();
|
||||
|
||||
page.once('dialog', async (dialog) => {
|
||||
expect(dialog.message()).toBe('수락하시겠습니까?');
|
||||
await dialog.accept();
|
||||
});
|
||||
await recruitment.getByRole('button', { name: '수락' }).click();
|
||||
await expect.poll(() => mutations.filter((entry) => entry.operation === 'messages.respond').length).toBe(1);
|
||||
|
||||
page.once('dialog', async (dialog) => {
|
||||
expect(dialog.message()).toBe('거절하시겠습니까?');
|
||||
await dialog.accept();
|
||||
});
|
||||
await invader.getByRole('button', { name: '거절' }).click();
|
||||
await expect.poll(() => mutations.filter((entry) => entry.operation === 'messages.respond').length).toBe(2);
|
||||
|
||||
const responses = mutations.filter((entry) => entry.operation === 'messages.respond');
|
||||
expect(responses).toHaveLength(2);
|
||||
expect(JSON.stringify(responses[0]!.body)).toContain('"messageId":105');
|
||||
expect(JSON.stringify(responses[0]!.body)).toContain('"response":true');
|
||||
expect(JSON.stringify(responses[1]!.body)).toContain('"messageId":106');
|
||||
expect(JSON.stringify(responses[1]!.body)).toContain('"response":false');
|
||||
});
|
||||
|
||||
test('redacts diplomacy for a low-permission general and preserves the failed-send error flow', async ({ page }) => {
|
||||
const mutations = await installFixture(page, {
|
||||
permission: 2,
|
||||
|
||||
@@ -97,6 +97,7 @@ const statusFixture = {
|
||||
|
||||
const installFixture = async (page: Page, options: { failBuff?: boolean } = {}) => {
|
||||
let buffMutationCount = 0;
|
||||
let resetTurnMutationCount = 0;
|
||||
await installImages(page);
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem('sammo-game-token', 'ga_inherit-visual-token');
|
||||
@@ -140,6 +141,10 @@ const installFixture = async (page: Page, options: { failBuff?: boolean } = {})
|
||||
buffMutationCount += 1;
|
||||
return response({ ok: true, remainPoint: 11_800 });
|
||||
}
|
||||
if (name === 'inherit.resetTurnTime') {
|
||||
resetTurnMutationCount += 1;
|
||||
return response({ ok: true, nextTurnTimeBase: 302.5143852464758, nextTurnTimeLabel: '00:05' });
|
||||
}
|
||||
throw new Error(`Unhandled inheritance fixture operation: ${name}`);
|
||||
});
|
||||
await route.fulfill({
|
||||
@@ -148,10 +153,32 @@ const installFixture = async (page: Page, options: { failBuff?: boolean } = {})
|
||||
body: JSON.stringify(result),
|
||||
});
|
||||
});
|
||||
return { buffMutationCount: () => buffMutationCount };
|
||||
return {
|
||||
buffMutationCount: () => buffMutationCount,
|
||||
resetTurnMutationCount: () => resetTurnMutationCount,
|
||||
};
|
||||
};
|
||||
|
||||
test.describe('inheritance management legacy parity', () => {
|
||||
test('confirms and displays the Ref-compatible pending turn-time base', async ({ page }) => {
|
||||
const fixture = await installFixture(page);
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.goto(gameUrl);
|
||||
await expect(page.locator('#container')).toBeVisible();
|
||||
|
||||
const item = page.locator('.simple-item').filter({ hasText: '랜덤 턴 초기화' });
|
||||
const button = item.getByRole('button', { name: '구입' });
|
||||
await expect(button).toBeEnabled();
|
||||
page.once('dialog', async (dialog) => {
|
||||
expect(dialog.message()).toBe('턴 시간을 1000 포인트로 초기화하시겠습니까?');
|
||||
await dialog.accept();
|
||||
});
|
||||
await button.click();
|
||||
|
||||
await expect(item).toContainText('적용 시간: 00:05');
|
||||
expect(fixture.resetTurnMutationCount()).toBe(1);
|
||||
});
|
||||
|
||||
test('matches the ref 1000px grid and computed styles on desktop and mobile', async ({ page }) => {
|
||||
await installFixture(page);
|
||||
await page.setViewportSize({ width: 1280, height: 900 });
|
||||
@@ -185,7 +212,7 @@ test.describe('inheritance management legacy parity', () => {
|
||||
|
||||
expect(desktop.container.width).toBe(1000);
|
||||
expect(desktop.container.x).toBe(140);
|
||||
expect(desktop.firstPoint.width).toBeCloseTo(327.3, 0);
|
||||
expect(Math.abs(desktop.firstPoint.width - 327.3)).toBeLessThanOrEqual(1);
|
||||
expect(desktop.fontFamily).toContain('Pretendard');
|
||||
expect(desktop.fontSize).toBe('14px');
|
||||
expect(desktop.backgroundImage).toContain('back_walnut.jpg');
|
||||
@@ -220,7 +247,7 @@ test.describe('inheritance management legacy parity', () => {
|
||||
await page.keyboard.press('Tab');
|
||||
await page.keyboard.press('Shift+Tab');
|
||||
await expect(buyButton).toBeFocused();
|
||||
await expect.poll(() => buyButton.evaluate((element) => getComputedStyle(element).outlineStyle)).toBe('solid');
|
||||
await expect.poll(() => buyButton.evaluate((element) => getComputedStyle(element).boxShadow)).not.toBe('none');
|
||||
|
||||
await buyButton.evaluate((element) => element.setAttribute('disabled', ''));
|
||||
await expect.poll(() => buyButton.evaluate((element) => getComputedStyle(element).opacity)).toBe('0.65');
|
||||
@@ -245,7 +272,7 @@ test.describe('inheritance management legacy parity', () => {
|
||||
};
|
||||
});
|
||||
expect(mobile.containerWidth).toBe(500);
|
||||
expect(mobile.firstWidth).toBeCloseTo(482, 0);
|
||||
expect(mobile.firstWidth).toBeCloseTo(484, 0);
|
||||
expect(mobile.stacked).toBe(true);
|
||||
|
||||
if (artifactRoot) {
|
||||
|
||||
@@ -37,31 +37,8 @@ const general = {
|
||||
|
||||
const generalContext = {
|
||||
general,
|
||||
city: {
|
||||
id: 1,
|
||||
name: '낙양',
|
||||
level: 7,
|
||||
nationId: 1,
|
||||
population: 50000,
|
||||
agriculture: 5000,
|
||||
commerce: 5000,
|
||||
security: 5000,
|
||||
defence: 5000,
|
||||
wall: 5000,
|
||||
supplyState: 1,
|
||||
frontState: 2,
|
||||
},
|
||||
nation: {
|
||||
id: 1,
|
||||
name: '수락국',
|
||||
color: '#d32f2f',
|
||||
level: 5,
|
||||
gold: 10000,
|
||||
rice: 10000,
|
||||
tech: 1200,
|
||||
typeCode: 'che_군벌',
|
||||
capitalCityId: 1,
|
||||
},
|
||||
city: null,
|
||||
nation: null,
|
||||
settings: {},
|
||||
penalties: {},
|
||||
};
|
||||
@@ -69,8 +46,22 @@ const generalContext = {
|
||||
const diplomacyMessage = {
|
||||
id: 701,
|
||||
msgType: 'diplomacy',
|
||||
src: { generalId: 2, generalName: '제안장수', nationId: 2, nationName: '제안국' },
|
||||
dest: { generalId: 1, generalName: '수락장수', nationId: 1, nationName: '수락국' },
|
||||
src: {
|
||||
generalId: 2,
|
||||
generalName: '제안장수',
|
||||
nationId: 2,
|
||||
nationName: '제안국',
|
||||
color: '#2457a6',
|
||||
icon: '',
|
||||
},
|
||||
dest: {
|
||||
generalId: 1,
|
||||
generalName: '수락장수',
|
||||
nationId: 1,
|
||||
nationName: '수락국',
|
||||
color: '#d32f2f',
|
||||
icon: '',
|
||||
},
|
||||
text: '제안국에서 191년 2월까지 불가침을 제안했습니다.',
|
||||
option: {
|
||||
action: 'noAggression',
|
||||
@@ -118,12 +109,44 @@ const installFixture = async (
|
||||
const operations = operationNames(route);
|
||||
const requestBody = route.request().postDataJSON();
|
||||
const results = operations.map((operation) => {
|
||||
if (operation === 'dashboard.getContextBundleDelta') {
|
||||
return response({
|
||||
context: {
|
||||
kind: 'snapshot',
|
||||
revision: 'AAAAAAAAAAAAAAAAAAAAAA',
|
||||
data: generalContext,
|
||||
},
|
||||
commandTable: {
|
||||
kind: 'snapshot',
|
||||
revision: 'BBBBBBBBBBBBBBBBBBBBBB',
|
||||
data: { general: [], nation: [] },
|
||||
},
|
||||
boardAccess: {
|
||||
kind: 'snapshot',
|
||||
revision: 'CCCCCCCCCCCCCCCCCCCCCC',
|
||||
data: {
|
||||
canMeeting: true,
|
||||
canSecret: true,
|
||||
permission: options.canRespondDiplomacy === false ? 2 : 4,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
if (operation === 'auth.status') return response({ userId: 'frontend-parity-user' });
|
||||
if (operation === 'lobby.info') {
|
||||
return response({ ...fixture.game.lobby, myGeneral: general });
|
||||
}
|
||||
if (operation === 'general.me') return response(generalContext);
|
||||
if (operation === 'world.getMapLayout') return response(fixture.game.mapLayout);
|
||||
if (operation === 'world.getState') {
|
||||
return response({
|
||||
currentYear: 190,
|
||||
currentMonth: 3,
|
||||
tickSeconds: 3600,
|
||||
config: { npcMode: 0, const: {}, environment: {} },
|
||||
meta: {},
|
||||
});
|
||||
}
|
||||
if (operation === 'world.getMap') {
|
||||
return response({ ...fixture.game.map, myCity: 1, myNation: 1 });
|
||||
}
|
||||
@@ -256,15 +279,15 @@ test.describe('instant diplomacy response UI', () => {
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.goto(`http://127.0.0.1:${gamePort}/che/`);
|
||||
await expect(page.getByRole('heading', { name: '전장 현황' })).toBeVisible();
|
||||
await page.getByRole('button', { name: '메시지', exact: true }).click();
|
||||
const mobileMessageButton = page.getByRole('button', { name: '메시지', exact: true });
|
||||
if ((await mobileMessageButton.count()) > 0) await mobileMessageButton.click();
|
||||
|
||||
const responseRow = page.locator('.message-response');
|
||||
await expect(responseRow).toBeVisible();
|
||||
const itemWidth = await page
|
||||
.locator('.DiplomacyTalk .msg-plate')
|
||||
.evaluate((element) => element.getBoundingClientRect().width);
|
||||
expect(itemWidth).toBeGreaterThanOrEqual(389);
|
||||
expect(itemWidth).toBeLessThanOrEqual(390);
|
||||
expect(itemWidth).toBeCloseTo(500, 0);
|
||||
|
||||
page.once('dialog', async (dialog) => {
|
||||
expect(dialog.message()).toBe('거절하시겠습니까?');
|
||||
@@ -295,7 +318,8 @@ test.describe('instant diplomacy response UI', () => {
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.goto(`http://127.0.0.1:${gamePort}/che/`);
|
||||
await expect(page.getByRole('heading', { name: '전장 현황' })).toBeVisible();
|
||||
await page.getByRole('button', { name: '메시지', exact: true }).click();
|
||||
const mobileMessageButton = page.getByRole('button', { name: '메시지', exact: true });
|
||||
if ((await mobileMessageButton.count()) > 0) await mobileMessageButton.click();
|
||||
|
||||
const accept = page.locator('.message-response').getByRole('button', { name: '수락' });
|
||||
await expect(accept).toBeDisabled();
|
||||
|
||||
Reference in New Issue
Block a user