feat: 사용자 전용 특수 명령을 예약턴에 연결
강행, 은퇴, 하야를 기본 명령 프로필에 추가한다. Ref 분류와 조건 경계, 예약 payload를 API 및 실제 Chromium 회귀 테스트로 고정한다.
This commit is contained in:
@@ -119,6 +119,7 @@ describe('buildTurnCommandTable', () => {
|
|||||||
'che_단련',
|
'che_단련',
|
||||||
'che_숙련전환',
|
'che_숙련전환',
|
||||||
'che_견문',
|
'che_견문',
|
||||||
|
'che_은퇴',
|
||||||
'che_장비매매',
|
'che_장비매매',
|
||||||
'che_군량매매',
|
'che_군량매매',
|
||||||
'che_내정특기초기화',
|
'che_내정특기초기화',
|
||||||
@@ -135,9 +136,67 @@ describe('buildTurnCommandTable', () => {
|
|||||||
'che_주민선정',
|
'che_주민선정',
|
||||||
],
|
],
|
||||||
군사: ['che_징병', 'che_모병', 'che_훈련', 'che_사기진작', 'che_출병', 'che_집합', 'che_소집해제'],
|
군사: ['che_징병', 'che_모병', 'che_훈련', 'che_사기진작', 'che_출병', 'che_집합', 'che_소집해제'],
|
||||||
인사: ['che_이동', 'che_인재탐색', 'che_귀환', 'che_임관', 'che_랜덤임관'],
|
인사: ['che_이동', 'che_강행', 'che_인재탐색', 'che_귀환', 'che_임관', 'che_랜덤임관'],
|
||||||
계략: ['che_화계'],
|
계략: ['che_화계'],
|
||||||
국가: ['che_증여', 'che_헌납', 'che_물자조달', 'che_거병', 'che_건국', 'che_선양', 'che_해산'],
|
국가: [
|
||||||
|
'che_증여',
|
||||||
|
'che_헌납',
|
||||||
|
'che_물자조달',
|
||||||
|
'che_하야',
|
||||||
|
'che_거병',
|
||||||
|
'che_건국',
|
||||||
|
'che_선양',
|
||||||
|
'che_해산',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('projects the Ref availability boundaries for force move, retirement, and resignation', async () => {
|
||||||
|
const buildTable = (general: GeneralRow, nation: NationRow | null = buildNation()) =>
|
||||||
|
buildTurnCommandTable({
|
||||||
|
worldState: buildWorldState(),
|
||||||
|
general,
|
||||||
|
city: buildCity(),
|
||||||
|
nation,
|
||||||
|
nationGenerals: null,
|
||||||
|
});
|
||||||
|
const findCommand = (table: Awaited<ReturnType<typeof buildTable>>, key: string) =>
|
||||||
|
table.general.flatMap((group) => group.values).find((command) => command.key === key);
|
||||||
|
|
||||||
|
const ordinary = await buildTable(buildGeneral());
|
||||||
|
expect(findCommand(ordinary, 'che_강행')).toMatchObject({
|
||||||
|
name: '강행',
|
||||||
|
reqArg: true,
|
||||||
|
possible: true,
|
||||||
|
inputFields: [{ key: 'destCityId', optionSource: 'cities' }],
|
||||||
|
});
|
||||||
|
expect(findCommand(ordinary, 'che_은퇴')).toMatchObject({
|
||||||
|
name: '은퇴',
|
||||||
|
possible: false,
|
||||||
|
status: 'blocked',
|
||||||
|
reason: '나이가 60세 이상이어야 합니다.',
|
||||||
|
});
|
||||||
|
expect(findCommand(ordinary, 'che_하야')).toMatchObject({
|
||||||
|
name: '하야',
|
||||||
|
possible: true,
|
||||||
|
status: 'available',
|
||||||
|
});
|
||||||
|
|
||||||
|
const oldEnough = await buildTable({ ...buildGeneral(), age: 60 } as GeneralRow);
|
||||||
|
expect(findCommand(oldEnough, 'che_은퇴')).toMatchObject({ possible: true, status: 'available' });
|
||||||
|
|
||||||
|
const ruler = await buildTable({ ...buildGeneral(), officerLevel: 12 } as GeneralRow);
|
||||||
|
expect(findCommand(ruler, 'che_하야')).toMatchObject({
|
||||||
|
possible: false,
|
||||||
|
status: 'blocked',
|
||||||
|
reason: expect.stringContaining('군주'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const neutral = await buildTable({ ...buildGeneral(), nationId: 0, officerLevel: 0 } as GeneralRow, null);
|
||||||
|
expect(findCommand(neutral, 'che_하야')).toMatchObject({
|
||||||
|
possible: false,
|
||||||
|
status: 'blocked',
|
||||||
|
reason: '재야입니다.',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -18,13 +18,16 @@ const GENERAL_AI_ACTIONS = [
|
|||||||
|
|
||||||
const NATION_AI_ACTIONS = ['che_몰수', 'che_발령', 'che_선전포고', 'che_천도', 'che_포상'] as const;
|
const NATION_AI_ACTIONS = ['che_몰수', 'che_발령', 'che_선전포고', 'che_천도', 'che_포상'] as const;
|
||||||
const GENERAL_REF_EDITOR_ACTIONS = [
|
const GENERAL_REF_EDITOR_ACTIONS = [
|
||||||
|
'che_은퇴',
|
||||||
'che_임관',
|
'che_임관',
|
||||||
'che_랜덤임관',
|
'che_랜덤임관',
|
||||||
|
'che_강행',
|
||||||
'che_징병',
|
'che_징병',
|
||||||
'che_출병',
|
'che_출병',
|
||||||
'che_농지개간',
|
'che_농지개간',
|
||||||
'che_화계',
|
'che_화계',
|
||||||
'che_증여',
|
'che_증여',
|
||||||
|
'che_하야',
|
||||||
'che_장비매매',
|
'che_장비매매',
|
||||||
] as const;
|
] as const;
|
||||||
const NATION_REF_EDITOR_ACTIONS = ['che_포상', 'che_발령', 'che_증축', 'che_필사즉생'] as const;
|
const NATION_REF_EDITOR_ACTIONS = ['che_포상', 'che_발령', 'che_증축', 'che_필사즉생'] as const;
|
||||||
|
|||||||
@@ -294,7 +294,7 @@ const chiefCenter = {
|
|||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
const install = async (page: Page, rejectGeneral = false) => {
|
const install = async (page: Page, rejectGeneral = false, commandTableResponse: unknown = commandTable) => {
|
||||||
const requests: unknown[] = [];
|
const requests: unknown[] = [];
|
||||||
const generalTurns = turns(30);
|
const generalTurns = turns(30);
|
||||||
const nationTurns = turns(12);
|
const nationTurns = turns(12);
|
||||||
@@ -344,7 +344,7 @@ const install = async (page: Page, rejectGeneral = false) => {
|
|||||||
? {
|
? {
|
||||||
kind: 'snapshot',
|
kind: 'snapshot',
|
||||||
revision: 'BBBBBBBBBBBBBBBBBBBBBB',
|
revision: 'BBBBBBBBBBBBBBBBBBBBBB',
|
||||||
data: commandTable,
|
data: commandTableResponse,
|
||||||
}
|
}
|
||||||
: { kind: 'unchanged', revision: 'BBBBBBBBBBBBBBBBBBBBBB' },
|
: { kind: 'unchanged', revision: 'BBBBBBBBBBBBBBBBBBBBBB' },
|
||||||
boardAccess: initial
|
boardAccess: initial
|
||||||
@@ -400,7 +400,7 @@ const install = async (page: Page, rejectGeneral = false) => {
|
|||||||
myCity: 1,
|
myCity: 1,
|
||||||
myNation: 1,
|
myNation: 1,
|
||||||
});
|
});
|
||||||
if (name === 'turns.getCommandTable') return response(commandTable);
|
if (name === 'turns.getCommandTable') return response(commandTableResponse);
|
||||||
if (name === 'nation.getChiefCenter') return response(chiefCenter);
|
if (name === 'nation.getChiefCenter') return response(chiefCenter);
|
||||||
if (name === 'turns.reserved.getGeneral')
|
if (name === 'turns.reserved.getGeneral')
|
||||||
return response({ turns: generalTurns, revision: generalRevision });
|
return response({ turns: generalTurns, revision: generalRevision });
|
||||||
@@ -461,6 +461,117 @@ const install = async (page: Page, rejectGeneral = false) => {
|
|||||||
return requests;
|
return requests;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
test('reserves force move, retirement, and resignation from the user command picker', async ({ page }) => {
|
||||||
|
const specialCommandTable = {
|
||||||
|
general: [
|
||||||
|
{
|
||||||
|
category: '개인',
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
key: 'che_은퇴',
|
||||||
|
name: '은퇴',
|
||||||
|
reqArg: false,
|
||||||
|
possible: false,
|
||||||
|
status: 'blocked',
|
||||||
|
reason: '나이가 60세 이상이어야 합니다.',
|
||||||
|
inputFields: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: '인사',
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
key: 'che_강행',
|
||||||
|
name: '강행',
|
||||||
|
reqArg: true,
|
||||||
|
possible: true,
|
||||||
|
status: 'available',
|
||||||
|
inputFields: [
|
||||||
|
{
|
||||||
|
key: 'destCityId',
|
||||||
|
label: '대상 도시',
|
||||||
|
kind: 'select',
|
||||||
|
required: true,
|
||||||
|
optionSource: 'cities',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: '국가',
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
key: 'che_하야',
|
||||||
|
name: '하야',
|
||||||
|
reqArg: false,
|
||||||
|
possible: true,
|
||||||
|
status: 'available',
|
||||||
|
inputFields: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
nation: [],
|
||||||
|
inputOptions,
|
||||||
|
};
|
||||||
|
const requests = await install(page, false, specialCommandTable);
|
||||||
|
await page.setViewportSize({ width: 1200, height: 900 });
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
const editor = page.locator('[data-command-scope="general"]');
|
||||||
|
|
||||||
|
await editor.getByRole('button', { name: '1턴 명령 입력', exact: true }).click();
|
||||||
|
let picker = page.getByTestId('command-picker');
|
||||||
|
const retirement = picker.getByRole('button', { name: '은퇴', exact: true });
|
||||||
|
await expect(retirement).toHaveClass(/blocked/);
|
||||||
|
await expect(retirement).toHaveAttribute('title', '나이가 60세 이상이어야 합니다.');
|
||||||
|
await retirement.hover();
|
||||||
|
await retirement.focus();
|
||||||
|
await expect(retirement).toBeFocused();
|
||||||
|
await picker.screenshot({ path: test.info().outputPath('special-user-commands-desktop-1200.png') });
|
||||||
|
await retirement.click();
|
||||||
|
await expect(editor.locator('.action-column > div').nth(0)).toHaveText('은퇴');
|
||||||
|
|
||||||
|
await editor.getByRole('button', { name: '2턴 명령 입력', exact: true }).click();
|
||||||
|
picker = page.getByTestId('command-picker');
|
||||||
|
await picker.getByRole('button', { name: '국가', exact: true }).click();
|
||||||
|
await picker.getByRole('button', { name: '하야', exact: true }).click();
|
||||||
|
await expect(editor.locator('.action-column > div').nth(1)).toHaveText('하야');
|
||||||
|
|
||||||
|
await editor.getByRole('button', { name: '3턴 명령 입력', exact: true }).click();
|
||||||
|
picker = page.getByTestId('command-picker');
|
||||||
|
await picker.getByRole('button', { name: '인사', exact: true }).click();
|
||||||
|
await picker.getByRole('button', { name: '강행', exact: true }).click();
|
||||||
|
const forceMoveForm = picker.getByTestId('command-argument-form');
|
||||||
|
await expect(forceMoveForm.getByTestId('command-argument-guidance')).toContainText('선택한 도시로 강행합니다.');
|
||||||
|
await forceMoveForm.locator('select').selectOption('2');
|
||||||
|
await picker.getByRole('button', { name: '입력', exact: true }).click();
|
||||||
|
await expect(editor.locator('.action-column > div').nth(2)).toHaveText('강행');
|
||||||
|
|
||||||
|
const serialized = JSON.stringify(requests);
|
||||||
|
expect(serialized).toContain('"action":"che_은퇴","args":{}');
|
||||||
|
expect(serialized).toContain('"action":"che_하야","args":{}');
|
||||||
|
expect(serialized).toContain('"action":"che_강행","args":{"destCityId":2}');
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 500, height: 900 });
|
||||||
|
await editor.getByRole('button', { name: '4턴 명령 입력', exact: true }).click();
|
||||||
|
picker = page.getByTestId('command-picker');
|
||||||
|
await expect(picker.locator('.category-btn')).toHaveText(['개인', '인사', '국가']);
|
||||||
|
const mobileGeometry = await picker.evaluate((element) => ({
|
||||||
|
width: element.getBoundingClientRect().width,
|
||||||
|
horizontalOverflow: element.scrollWidth - element.clientWidth,
|
||||||
|
categoryColumns: getComputedStyle(element.querySelector<HTMLElement>('.category-list')!).gridTemplateColumns,
|
||||||
|
}));
|
||||||
|
expect(mobileGeometry.width).toBeLessThanOrEqual(500);
|
||||||
|
expect(mobileGeometry.horizontalOverflow).toBeLessThanOrEqual(0);
|
||||||
|
expect(mobileGeometry.categoryColumns.split(' ')).toHaveLength(3);
|
||||||
|
await picker.getByRole('button', { name: '개인', exact: true }).click();
|
||||||
|
await expect(picker.getByRole('button', { name: '은퇴', exact: true })).toBeVisible();
|
||||||
|
await picker.screenshot({ path: test.info().outputPath('special-user-commands-mobile-500.png') });
|
||||||
|
});
|
||||||
|
|
||||||
test('enters general and nation command arguments and sends exact values', async ({ page }) => {
|
test('enters general and nation command arguments and sends exact values', async ({ page }) => {
|
||||||
const requests = await install(page);
|
const requests = await install(page);
|
||||||
await page.setViewportSize({ width: 1200, height: 900 });
|
await page.setViewportSize({ width: 1200, height: 900 });
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"che_사기진작",
|
"che_사기진작",
|
||||||
"che_요양",
|
"che_요양",
|
||||||
"che_견문",
|
"che_견문",
|
||||||
|
"che_은퇴",
|
||||||
"che_내정특기초기화",
|
"che_내정특기초기화",
|
||||||
"che_전투특기초기화",
|
"che_전투특기초기화",
|
||||||
"che_장비매매",
|
"che_장비매매",
|
||||||
@@ -34,6 +35,8 @@
|
|||||||
"che_증여",
|
"che_증여",
|
||||||
"che_헌납",
|
"che_헌납",
|
||||||
"che_이동",
|
"che_이동",
|
||||||
|
"che_강행",
|
||||||
|
"che_하야",
|
||||||
"che_선양",
|
"che_선양",
|
||||||
"che_해산",
|
"che_해산",
|
||||||
"휴식"
|
"휴식"
|
||||||
|
|||||||
Reference in New Issue
Block a user