fix: 발령 대상에서 아국 도시를 우선 표시
적국과 공백지를 포함한 다른 도시 선택지는 유지하고 기존 지도 선택 흐름을 보존한다. 발령 전용 정렬 단위 테스트와 데스크톱·모바일 Chromium 회귀를 추가한다.
This commit is contained in:
@@ -953,9 +953,7 @@ test('shows and reserves the Ref spy command for a user on desktop and mobile',
|
|||||||
await expect(spy).toBeFocused();
|
await expect(spy).toBeFocused();
|
||||||
await spy.click();
|
await spy.click();
|
||||||
const form = picker.getByTestId('command-argument-form');
|
const form = picker.getByTestId('command-argument-form');
|
||||||
await expect(form.getByTestId('command-argument-guidance')).toContainText(
|
await expect(form.getByTestId('command-argument-guidance')).toContainText('선택한 도시에 첩보를 실행합니다.');
|
||||||
'선택한 도시에 첩보를 실행합니다.'
|
|
||||||
);
|
|
||||||
await expect(form.getByTestId('command-argument-guidance')).toContainText(
|
await expect(form.getByTestId('command-argument-guidance')).toContainText(
|
||||||
'인접 도시에서는 더 많은 정보를 얻습니다.'
|
'인접 도시에서는 더 많은 정보를 얻습니다.'
|
||||||
);
|
);
|
||||||
@@ -979,9 +977,7 @@ test('shows and reserves the Ref spy command for a user on desktop and mobile',
|
|||||||
await picker.screenshot({ path: test.info().outputPath('spy-command-mobile-500.png') });
|
await picker.screenshot({ path: test.info().outputPath('spy-command-mobile-500.png') });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('defaults founding to a Ref-selectable nation trait and paints color option labels', async ({
|
test('defaults founding to a Ref-selectable nation trait and paints color option labels', async ({ page }) => {
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
const foundingColors = [
|
const foundingColors = [
|
||||||
{ value: 0, label: '색상 1', color: '#FF0000' },
|
{ value: 0, label: '색상 1', color: '#FF0000' },
|
||||||
{ value: 15, label: '색상 16', color: '#6495ED' },
|
{ value: 15, label: '색상 16', color: '#6495ED' },
|
||||||
@@ -1284,7 +1280,9 @@ test('keeps general and chief command categories after input and across page rel
|
|||||||
const reloadedChiefPicker = chiefPage.getByTestId('command-picker');
|
const reloadedChiefPicker = chiefPage.getByTestId('command-picker');
|
||||||
await expect(reloadedChiefPicker.getByRole('button', { name: '전략', exact: true })).toHaveClass(/active/);
|
await expect(reloadedChiefPicker.getByRole('button', { name: '전략', exact: true })).toHaveClass(/active/);
|
||||||
await expect(reloadedChiefPicker.getByRole('button', { name: '필사즉생', exact: true })).toBeVisible();
|
await expect(reloadedChiefPicker.getByRole('button', { name: '필사즉생', exact: true })).toBeVisible();
|
||||||
await expect.poll(() => reloadedChiefPicker.evaluate((element) => element.getBoundingClientRect().height)).toBeGreaterThan(200);
|
await expect
|
||||||
|
.poll(() => reloadedChiefPicker.evaluate((element) => element.getBoundingClientRect().height))
|
||||||
|
.toBeGreaterThan(200);
|
||||||
await reloadedChiefPicker.screenshot({
|
await reloadedChiefPicker.screenshot({
|
||||||
path: test.info().outputPath('chief-category-after-reload-mobile-500.png'),
|
path: test.info().outputPath('chief-category-after-reload-mobile-500.png'),
|
||||||
});
|
});
|
||||||
@@ -1825,6 +1823,61 @@ test('shows city or capital maps for every requested chief command', async ({ pa
|
|||||||
await page.screenshot({ path: test.info().outputPath('chief-command-map-guidance.png'), fullPage: true });
|
await page.screenshot({ path: test.info().outputPath('chief-command-map-guidance.png'), fullPage: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('prioritizes own cities for assignment while retaining other map targets', async ({ page }) => {
|
||||||
|
const assignmentTable = structuredClone(commandTable);
|
||||||
|
assignmentTable.inputOptions.cities = [
|
||||||
|
{ value: 2, label: '허창 (적국)', description: '적국 · 예주 · 대도시' },
|
||||||
|
{ value: 3, label: '단양 (무주)' },
|
||||||
|
{ value: 1, label: '업 (아국)' },
|
||||||
|
];
|
||||||
|
|
||||||
|
await install(page, false, assignmentTable);
|
||||||
|
await page.setViewportSize({ width: 1200, height: 900 });
|
||||||
|
await page.goto('/che/chief-center');
|
||||||
|
await page.getByRole('button', { name: '1턴 명령 입력', exact: true }).click();
|
||||||
|
const picker = page.getByTestId('command-picker');
|
||||||
|
await picker.getByRole('button', { name: /^(?:국가:)?인사$/, exact: true }).click();
|
||||||
|
await picker.getByRole('button', { name: /발령/ }).click();
|
||||||
|
|
||||||
|
const form = picker.getByTestId('command-argument-form');
|
||||||
|
const citySelect = form.locator('#command-arg-destCityId');
|
||||||
|
await expect(form.getByTestId('command-argument-map')).toBeVisible();
|
||||||
|
await expect(citySelect.locator('option')).toHaveText(['업 (아국)', '허창 (적국)', '단양 (무주)']);
|
||||||
|
await expect(citySelect).toHaveValue('1');
|
||||||
|
|
||||||
|
await form.getByTestId('command-argument-map').locator('.city-base').nth(1).click();
|
||||||
|
await expect(citySelect).toHaveValue('2');
|
||||||
|
await expect(form.getByTestId('command-map-selection-status')).toContainText('선택 도시허창');
|
||||||
|
await expect(page).toHaveURL(/\/che\/chief-center$/);
|
||||||
|
await form.screenshot({ path: test.info().outputPath('chief-assignment-own-city-priority.png') });
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 500, height: 900 });
|
||||||
|
await page.goto('/che/chief-center');
|
||||||
|
await page.getByRole('button', { name: '1턴 명령 입력', exact: true }).click();
|
||||||
|
const mobilePicker = page.getByTestId('command-picker');
|
||||||
|
await mobilePicker.getByRole('button', { name: /^(?:국가:)?인사$/, exact: true }).click();
|
||||||
|
await mobilePicker.getByRole('button', { name: /발령/ }).click();
|
||||||
|
|
||||||
|
const mobileForm = mobilePicker.getByTestId('command-argument-form');
|
||||||
|
const mobileMap = mobileForm.getByTestId('command-argument-map');
|
||||||
|
const mobileCitySelect = mobileForm.locator('#command-arg-destCityId');
|
||||||
|
await expect(mobileMap).toBeVisible();
|
||||||
|
await expect(mobileCitySelect.locator('option')).toHaveText(['업 (아국)', '허창 (적국)', '단양 (무주)']);
|
||||||
|
const mobileGeometry = await mobilePicker.evaluate((element) => ({
|
||||||
|
width: element.getBoundingClientRect().width,
|
||||||
|
overflow: element.scrollWidth - element.clientWidth,
|
||||||
|
}));
|
||||||
|
expect(mobileGeometry).toEqual({ width: 500, overflow: 0 });
|
||||||
|
const mapGeometry = await mobileMap.locator('.map-area').evaluate((element) => {
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
return { width: rect.width, height: rect.height };
|
||||||
|
});
|
||||||
|
expect(mapGeometry.width / mapGeometry.height).toBeCloseTo(7 / 5, 2);
|
||||||
|
await mobileMap.screenshot({ path: test.info().outputPath('chief-assignment-map-mobile.png') });
|
||||||
|
await mobileCitySelect.scrollIntoViewIfNeeded();
|
||||||
|
await mobilePicker.screenshot({ path: test.info().outputPath('chief-assignment-own-city-priority-mobile.png') });
|
||||||
|
});
|
||||||
|
|
||||||
test('prioritizes current nation targets while preserving every choice', async ({ page }) => {
|
test('prioritizes current nation targets while preserving every choice', async ({ page }) => {
|
||||||
await install(page);
|
await install(page);
|
||||||
await page.setViewportSize({ width: 1200, height: 900 });
|
await page.setViewportSize({ width: 1200, height: 900 });
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import type { CommandMapData, CommandOption } from './types';
|
||||||
|
|
||||||
|
export const commandCityOptions = (
|
||||||
|
commandKey: string,
|
||||||
|
options: readonly CommandOption[],
|
||||||
|
mapData?: CommandMapData | null
|
||||||
|
): CommandOption[] => {
|
||||||
|
if (commandKey !== 'che_발령' || typeof mapData?.myNation !== 'number') return [...options];
|
||||||
|
|
||||||
|
const nationByCityId = new Map(mapData.cityList.map(([cityId, , , nationId]) => [cityId, nationId]));
|
||||||
|
return options
|
||||||
|
.map((option, index) => ({ option, index }))
|
||||||
|
.sort((left, right) => {
|
||||||
|
const leftOwned =
|
||||||
|
typeof left.option.value === 'number' && nationByCityId.get(left.option.value) === mapData.myNation;
|
||||||
|
const rightOwned =
|
||||||
|
typeof right.option.value === 'number' && nationByCityId.get(right.option.value) === mapData.myNation;
|
||||||
|
return Number(rightOwned) - Number(leftOwned) || left.index - right.index;
|
||||||
|
})
|
||||||
|
.map(({ option }) => option);
|
||||||
|
};
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
import { computed, reactive, watch, type CSSProperties } from 'vue';
|
import { computed, reactive, watch, type CSSProperties } from 'vue';
|
||||||
import MapViewer from './MapViewer.vue';
|
import MapViewer from './MapViewer.vue';
|
||||||
import { commandArgumentPresentation } from '../command/commandArgumentPresentation';
|
import { commandArgumentPresentation } from '../command/commandArgumentPresentation';
|
||||||
|
import { commandCityOptions } from '../command/commandArgumentOptions';
|
||||||
import {
|
import {
|
||||||
commandArgumentFieldContract,
|
commandArgumentFieldContract,
|
||||||
shouldPreserveCommandArgumentValue,
|
shouldPreserveCommandArgumentValue,
|
||||||
@@ -64,6 +65,9 @@ const optionsFor = (field: CommandInputField): CommandOption[] => {
|
|||||||
if (field.optionSource === 'nations') {
|
if (field.optionSource === 'nations') {
|
||||||
return props.options.nationTargets?.[props.commandKey] ?? props.options.nations;
|
return props.options.nationTargets?.[props.commandKey] ?? props.options.nations;
|
||||||
}
|
}
|
||||||
|
if (field.optionSource === 'cities') {
|
||||||
|
return commandCityOptions(props.commandKey, props.options.cities, props.mapData);
|
||||||
|
}
|
||||||
if (field.optionSource === 'items') {
|
if (field.optionSource === 'items') {
|
||||||
return props.options.items[String(values.itemType ?? '')] ?? [];
|
return props.options.items[String(values.itemType ?? '')] ?? [];
|
||||||
}
|
}
|
||||||
@@ -104,12 +108,7 @@ const synchronizeValues = () => {
|
|||||||
for (const field of props.fields) {
|
for (const field of props.fields) {
|
||||||
const preserve =
|
const preserve =
|
||||||
!commandChanged &&
|
!commandChanged &&
|
||||||
shouldPreserveCommandArgumentValue(
|
shouldPreserveCommandArgumentValue(field, previousFieldContracts.get(field.key), values, optionsFor(field));
|
||||||
field,
|
|
||||||
previousFieldContracts.get(field.key),
|
|
||||||
values,
|
|
||||||
optionsFor(field)
|
|
||||||
);
|
|
||||||
if (!preserve) values[field.key] = defaultValue(field);
|
if (!preserve) values[field.key] = defaultValue(field);
|
||||||
}
|
}
|
||||||
const itemCodeField = props.fields.find((field) => field.key === 'itemCode');
|
const itemCodeField = props.fields.find((field) => field.key === 'itemCode');
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
|
||||||
|
import { commandCityOptions } from '../src/components/command/commandArgumentOptions.ts';
|
||||||
|
import type { CommandMapData, CommandOption } from '../src/components/command/types.ts';
|
||||||
|
|
||||||
|
const cities: CommandOption[] = [
|
||||||
|
{ value: 20, label: '적국 도시' },
|
||||||
|
{ value: 30, label: '아국 도시 둘' },
|
||||||
|
{ value: 40, label: '공백지' },
|
||||||
|
{ value: 10, label: '아국 도시 하나' },
|
||||||
|
];
|
||||||
|
const mapData: CommandMapData = {
|
||||||
|
year: 200,
|
||||||
|
month: 1,
|
||||||
|
startYear: 180,
|
||||||
|
cityList: [
|
||||||
|
[10, 8, 0, 1, 1, 1],
|
||||||
|
[20, 7, 0, 2, 2, 1],
|
||||||
|
[30, 6, 0, 1, 3, 1],
|
||||||
|
[40, 5, 0, 0, 4, 1],
|
||||||
|
],
|
||||||
|
nationList: [
|
||||||
|
[1, '아국', '#008000', 10],
|
||||||
|
[2, '적국', '#800000', 20],
|
||||||
|
],
|
||||||
|
myCity: 10,
|
||||||
|
myNation: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
void test('발령은 아국 도시를 먼저 두고 적국과 공백지를 원래 순서로 보존한다', () => {
|
||||||
|
const sorted = commandCityOptions('che_발령', cities, mapData);
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
sorted.map((option) => option.value),
|
||||||
|
[30, 10, 20, 40]
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
cities.map((option) => option.value),
|
||||||
|
[20, 30, 40, 10],
|
||||||
|
'공용 입력 option은 변경하지 않는다'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
void test('다른 도시 대상 명령의 순서는 바꾸지 않는다', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
commandCityOptions('che_출병', cities, mapData).map((option) => option.value),
|
||||||
|
[20, 30, 40, 10]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
void test('지도 국가 정보가 아직 없으면 발령의 기존 option 순서를 유지한다', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
commandCityOptions('che_발령', cities, null).map((option) => option.value),
|
||||||
|
[20, 30, 40, 10]
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user