fix(frontend): 징병과 모병 직접 수량을 보존

이미 선택된 병종의 행 실행 버튼이 수량을 채우기 값으로 다시 초기화하지 않도록 수정한다. 징병과 모병의 보병·궁병·기병·귀병 직접 입력, 예약 payload, 턴 정보 재조회를 production Chromium 회귀 테스트로 고정한다.
This commit is contained in:
2026-08-17 15:44:11 +00:00
parent 23269abccd
commit 005a61b982
2 changed files with 154 additions and 5 deletions
+153 -4
View File
@@ -265,6 +265,71 @@ const commandTable = {
],
inputOptions,
};
const basicRecruitmentCrewTypes = [
{
id: 1100,
armType: 1,
name: '보병',
attack: 100,
defence: 150,
speed: 7,
avoid: 10,
baseCost: 9,
baseRice: 9,
info: ['표준적인 보병입니다.'],
},
{
id: 1200,
armType: 2,
name: '궁병',
attack: 100,
defence: 100,
speed: 7,
avoid: 20,
baseCost: 10,
baseRice: 10,
info: ['표준적인 궁병입니다.'],
},
{
id: 1300,
armType: 3,
name: '기병',
attack: 150,
defence: 100,
speed: 7,
avoid: 5,
baseCost: 11,
baseRice: 11,
info: ['표준적인 기병입니다.'],
},
{
id: 1400,
armType: 4,
name: '귀병',
attack: 80,
defence: 80,
speed: 7,
avoid: 5,
baseCost: 9,
baseRice: 9,
info: ['계략을 사용하는 병종입니다.'],
},
].map((crewType) => ({ ...crewType, available: true, special: false }));
const fourArmRecruitmentCommandTable = {
...commandTable,
inputOptions: {
...inputOptions,
crewTypes: basicRecruitmentCrewTypes.map((crewType) => ({ value: crewType.id, label: crewType.name })),
recruitment: {
...inputOptions.recruitment,
groups: basicRecruitmentCrewTypes.map((crewType) => ({
armType: crewType.armType,
armName: crewType.name,
values: [crewType],
})),
},
},
};
const buildSimpleCommand = (key: string, name: string) => ({
key,
name,
@@ -379,14 +444,25 @@ const generalContext = {
name: '아국',
color: '#008000',
level: 1,
levelName: '호족',
gold: 5000,
rice: 6000,
tech: 100,
typeCode: 'che_중립',
typeName: '중립',
capitalCityId: 1,
capitalCityName: '',
typePros: '',
typeCons: '',
population: { cityCount: 1, current: 1000, max: 2000 },
crew: { generalCount: 2, current: 500, max: 7000 },
power: 1234,
bill: 100,
taxRate: 20,
strategicCommandLimit: 0,
diplomaticLimit: 0,
prohibitScout: false,
prohibitWar: false,
techLevel: 1,
techLimited: false,
topChiefs: {},
impossibleStrategicCommands: [],
},
settings: {},
penalties: {},
@@ -1210,6 +1286,79 @@ test('uses a Ref-style full recruitment page without horizontal overflow on desk
await expect.poll(() => page.evaluate(() => getComputedStyle(document.body).overflow)).not.toBe('hidden');
});
test('keeps arbitrary direct recruitment and mercenary amounts for all four arms after turn refresh', async ({
page,
}, testInfo) => {
const requests = await install(page, false, fourArmRecruitmentCommandTable);
await page.setViewportSize({ width: 1200, height: 900 });
await page.goto('/');
const entries = [
{ turn: 1, command: '징병', crewTypeId: 1100, name: '보병', inputAmount: 13, savedAmount: 1300 },
{ turn: 2, command: '징병', crewTypeId: 1200, name: '궁병', inputAmount: 27, savedAmount: 2700 },
{ turn: 3, command: '징병', crewTypeId: 1300, name: '기병', inputAmount: 41, savedAmount: 4100 },
{ turn: 4, command: '징병', crewTypeId: 1400, name: '귀병', inputAmount: 59, savedAmount: 5900 },
{ turn: 5, command: '모병', crewTypeId: 1100, name: '보병', inputAmount: 17, savedAmount: 1700 },
{ turn: 6, command: '모병', crewTypeId: 1200, name: '궁병', inputAmount: 31, savedAmount: 3100 },
{ turn: 7, command: '모병', crewTypeId: 1300, name: '기병', inputAmount: 43, savedAmount: 4300 },
{ turn: 8, command: '모병', crewTypeId: 1400, name: '귀병', inputAmount: 61, savedAmount: 6100 },
];
for (const entry of entries) {
await page.getByRole('button', { name: `${entry.turn}턴 명령 입력`, exact: true }).click();
const picker = page.getByTestId('command-picker');
await picker.getByRole('button', { name: '내정', exact: true }).click();
await picker.getByRole('button', { name: entry.command, exact: true }).click();
const row = picker.getByRole('button', { name: `${entry.name} 선택 가능`, exact: true });
const amountInput = row.locator('input[type=number]');
await amountInput.fill(String(entry.inputAmount));
await expect(amountInput).toHaveValue(String(entry.inputAmount));
if (entry.turn === 1) {
const inputGeometry = await amountInput.evaluate((element) => {
if (!(element instanceof HTMLInputElement)) throw new Error('Expected recruitment amount input');
const rect = element.getBoundingClientRect();
return {
width: rect.width,
height: rect.height,
textAlign: getComputedStyle(element).textAlign,
value: element.value,
};
});
expect(inputGeometry).toMatchObject({ height: 28, textAlign: 'right', value: '13' });
expect(inputGeometry.width).toBeGreaterThan(0);
await picker.screenshot({ path: testInfo.outputPath('recruitment-direct-amount-desktop.png') });
}
await row.getByRole('button', { name: entry.command, exact: true }).click();
await expect(picker).toHaveCount(0);
await expect(page.locator('[data-command-scope="general"] .action-column > div').nth(entry.turn - 1)).toHaveText(
`${entry.name}${entry.savedAmount}${entry.command}`
);
}
const refreshResponse = page.waitForResponse((apiResponse) =>
decodeURIComponent(apiResponse.url()).includes('turns.reserved.getGeneral')
);
await page.getByRole('button', { name: '갱 신', exact: true }).click();
await refreshResponse;
for (const entry of entries) {
await expect(page.locator('[data-command-scope="general"] .action-column > div').nth(entry.turn - 1)).toHaveText(
`${entry.name}${entry.savedAmount}${entry.command}`
);
}
await page
.locator('[data-command-scope="general"]')
.screenshot({ path: testInfo.outputPath('recruitment-arbitrary-amounts-after-refresh.png') });
const serializedRequests = JSON.stringify(requests);
for (const entry of entries) {
expect(serializedRequests).toContain(`"crewType":${entry.crewTypeId}`);
expect(serializedRequests).toContain(`"amount":${entry.savedAmount}`);
}
});
test('uses the map to choose a nation target in the chief command window', async ({ page }) => {
await install(page);
await page.setViewportSize({ width: 1200, height: 900 });
@@ -69,7 +69,7 @@ const updateAmount = (event: Event) => {
};
const submit = async (crewType?: RecruitmentCrewType) => {
if (crewType) selectCrewType(crewType);
if (crewType && crewType.id !== selectedCrewTypeId.value) selectCrewType(crewType);
await nextTick();
if (valid.value) emit('submit');
};