fix(game): 외교권자와 조언자 복수 임명 UI를 복원한다

Ref처럼 역할별 후보를 분리하고 클릭과 터치만으로 최대 두 명을 선택할 수 있게 한다. API 후보·엔진 payload·모바일 Chromium 회귀를 함께 고정한다.
This commit is contained in:
2026-08-24 08:38:38 +00:00
parent 69e34e3c95
commit 87e565ae72
5 changed files with 368 additions and 37 deletions
+75 -2
View File
@@ -14,6 +14,7 @@ type FixtureState = {
appointedGeneralId?: number;
appointedCityId?: number;
appointedOfficerLevel?: number;
permissionMutationInput?: { isAmbassador: boolean; targetGeneralIds: number[] };
noticeMutationInput?: string;
scoutMutationInput?: string;
uploadDataUrl?: string;
@@ -101,6 +102,8 @@ const personnelFixture = (state: FixtureState) => {
general(5, '정욱', 2),
general(6, '장료', 1),
general(7, '허저', 1, { permission: 'ambassador' }),
general(8, '가후', 1, { permission: 'auditor' }),
general(9, '전위', 1),
];
const visibleGenerals =
state.role === 'member' ? fullGenerals.filter((entry) => entry.officerLevel >= 2) : fullGenerals;
@@ -145,8 +148,13 @@ const personnelFixture = (state: FixtureState) => {
ambassadors: [
{ id: 6, name: '장료', npcState: 0, permission: 'normal', maxPermission: 4 },
{ id: 7, name: '허저', npcState: 0, permission: 'ambassador', maxPermission: 4 },
{ id: 9, name: '전위', npcState: 0, permission: 'normal', maxPermission: 4 },
],
auditors: [
{ id: 6, name: '장료', npcState: 0, permission: 'normal', maxPermission: 4 },
{ id: 8, name: '가후', npcState: 0, permission: 'auditor', maxPermission: 4 },
{ id: 9, name: '전위', npcState: 0, permission: 'normal', maxPermission: 4 },
],
auditors: [{ id: 6, name: '장료', npcState: 0, permission: 'normal', maxPermission: 4 }],
}
: { ambassadors: [], auditors: [] },
};
@@ -238,7 +246,16 @@ const installFixture = async (page: Page, state: FixtureState) => {
state.appointedOfficerLevel = Number(jsonInput.officerLevel ?? 0);
return response({ ok: true });
}
if (operation === 'nation.kick' || operation === 'nation.changePermission') return response({ ok: true });
if (operation === 'nation.changePermission') {
state.permissionMutationInput = {
isAmbassador: jsonInput.isAmbassador === true,
targetGeneralIds: Array.isArray(jsonInput.targetGeneralIds)
? jsonInput.targetGeneralIds.map((id) => Number(id))
: [],
};
return response({ ok: true });
}
if (operation === 'nation.kick') return response({ ok: true });
if (operation === 'nation.setRate') {
if (state.failNextRate) {
state.failNextRate = false;
@@ -347,6 +364,62 @@ test('personnel keeps the desktop frame while exposing row-level appointment con
await screenshot(page, 'core-personnel-desktop-leader.png');
});
test('leader can grant two ambassador and auditor permissions by click or touch without modifier keys', async ({
page,
}) => {
const state: FixtureState = { role: 'leader', rate: 20 };
await installFixture(page, state);
await page.setViewportSize({ width: 390, height: 844 });
await gotoOffice(page, 'nation/personnel');
const ambassadorTrigger = page.locator('.permission-multiselect-trigger').first();
await expect(ambassadorTrigger).toHaveAccessibleName('외교권자 선택, 현재 1명');
await ambassadorTrigger.click();
const ambassadorOptions = page.getByRole('listbox', { name: '외교권자 후보' });
await expect(ambassadorOptions).toBeVisible();
await expect(ambassadorOptions.getByRole('option', { name: '허저' })).toHaveAttribute('aria-selected', 'true');
await ambassadorOptions.getByRole('option', { name: '장료' }).click();
await expect(ambassadorOptions.getByRole('option', { name: '장료' })).toHaveAttribute('aria-selected', 'true');
await expect(ambassadorTrigger).toHaveAccessibleName('외교권자 선택, 현재 2명');
await ambassadorOptions.getByRole('option', { name: '전위' }).click();
await expect(page.getByTestId('game-toast')).toContainText('최대 2명까지 설정 가능합니다.');
await expect(ambassadorOptions.getByRole('option', { name: '전위' })).toHaveAttribute('aria-selected', 'false');
const ambassadorGeometry = await ambassadorOptions.evaluate((element) => {
const rect = element.getBoundingClientRect();
return { left: rect.left, right: rect.right, width: rect.width };
});
expect(ambassadorGeometry.left).toBeGreaterThanOrEqual(0);
expect(ambassadorGeometry.right).toBeLessThanOrEqual(390);
expect(ambassadorGeometry.width).toBeGreaterThanOrEqual(100);
await screenshot(page, 'core-personnel-mobile-permission-picker-open.png');
page.once('dialog', async (dialog) => {
expect(dialog.message()).toBe('외교권자를 변경할까요?');
await dialog.accept();
});
await page.getByRole('button', { name: '외교권자 임명 반영' }).click();
await expect(page.getByTestId('game-toast').filter({ hasText: '권한을 변경했습니다.' })).toBeVisible();
await expect.poll(() => state.permissionMutationInput).toEqual({ isAmbassador: true, targetGeneralIds: [7, 6] });
const auditorTrigger = page.locator('.permission-multiselect-trigger').nth(1);
await expect(auditorTrigger).toHaveAccessibleName('조언자 선택, 현재 1명');
await auditorTrigger.click();
const auditorOptions = page.getByRole('listbox', { name: '조언자 후보' });
await expect(auditorOptions.getByRole('option', { name: '허저' })).toHaveCount(0);
await auditorOptions.getByRole('option', { name: '장료' }).click();
await expect(auditorTrigger).toHaveAccessibleName('조언자 선택, 현재 2명');
page.once('dialog', async (dialog) => {
expect(dialog.message()).toBe('조언자를 변경할까요?');
await dialog.accept();
});
await page.getByRole('button', { name: '조언자 임명 반영' }).click();
await expect(page.getByTestId('game-toast').filter({ hasText: '권한을 변경했습니다.' })).toBeVisible();
await expect.poll(() => state.permissionMutationInput).toEqual({ isAmbassador: false, targetGeneralIds: [8, 6] });
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBe(390);
await screenshot(page, 'core-personnel-mobile-permission-picker.png');
});
test('personnel selects an informed general and reports the JosaUtil-composed result in a toast', async ({ page }) => {
const state: FixtureState = { role: 'head', rate: 20 };
await installFixture(page, state);