merge: 강행 은퇴 하야 예약턴 입력을 반영

This commit is contained in:
2026-08-15 18:48:37 +00:00
4 changed files with 181 additions and 5 deletions
+61 -2
View File
@@ -119,6 +119,7 @@ describe('buildTurnCommandTable', () => {
'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_해산',
],
});
});
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 GENERAL_REF_EDITOR_ACTIONS = [
'che_은퇴',
'che_임관',
'che_랜덤임관',
'che_강행',
'che_징병',
'che_출병',
'che_농지개간',
'che_화계',
'che_증여',
'che_하야',
'che_장비매매',
] as const;
const NATION_REF_EDITOR_ACTIONS = ['che_포상', 'che_발령', 'che_증축', 'che_필사즉생'] as const;
+114 -3
View File
@@ -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 generalTurns = turns(30);
const nationTurns = turns(12);
@@ -344,7 +344,7 @@ const install = async (page: Page, rejectGeneral = false) => {
? {
kind: 'snapshot',
revision: 'BBBBBBBBBBBBBBBBBBBBBB',
data: commandTable,
data: commandTableResponse,
}
: { kind: 'unchanged', revision: 'BBBBBBBBBBBBBBBBBBBBBB' },
boardAccess: initial
@@ -400,7 +400,7 @@ const install = async (page: Page, rejectGeneral = false) => {
myCity: 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 === 'turns.reserved.getGeneral')
return response({ turns: generalTurns, revision: generalRevision });
@@ -461,6 +461,117 @@ const install = async (page: Page, rejectGeneral = false) => {
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 }) => {
const requests = await install(page);
await page.setViewportSize({ width: 1200, height: 900 });
+3
View File
@@ -11,6 +11,7 @@
"che_사기진작",
"che_요양",
"che_견문",
"che_은퇴",
"che_내정특기초기화",
"che_전투특기초기화",
"che_장비매매",
@@ -34,6 +35,8 @@
"che_증여",
"che_헌납",
"che_이동",
"che_강행",
"che_하야",
"che_선양",
"che_해산",
"휴식"