merge: 지도 호출부별 선택 동작을 main에 통합
This commit is contained in:
@@ -2,7 +2,7 @@ import { expect, test, type Page, type Route } from '@playwright/test';
|
|||||||
import { readFile } from 'node:fs/promises';
|
import { readFile } from 'node:fs/promises';
|
||||||
import { dirname, resolve } from 'node:path';
|
import { dirname, resolve } from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { gameProfile, gameTrpcRoute } from './gameTestPaths.js';
|
import { gamePath, gameProfile, gameTrpcRoute } from './gameTestPaths.js';
|
||||||
import { touchDrag } from './touchDrag.js';
|
import { touchDrag } from './touchDrag.js';
|
||||||
|
|
||||||
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../..');
|
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../..');
|
||||||
@@ -1403,6 +1403,32 @@ test('enters general and nation command arguments and sends exact values', async
|
|||||||
await page.setViewportSize({ width: 1200, height: 900 });
|
await page.setViewportSize({ width: 1200, height: 900 });
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
await expect(page.getByTestId('current-city-marker')).toHaveCount(0);
|
await expect(page.getByTestId('current-city-marker')).toHaveCount(0);
|
||||||
|
const mainMap = page.locator('[data-main-target="map"]');
|
||||||
|
const currentMainCity = mainMap.locator('.city-base.mine');
|
||||||
|
await expect(currentMainCity).toHaveAttribute('aria-label', '업, 현재 도시');
|
||||||
|
const currentCityHighlight = await currentMainCity.locator('.city-filler.my-city').evaluate((element) => {
|
||||||
|
const style = getComputedStyle(element);
|
||||||
|
return {
|
||||||
|
outlineColor: style.outlineColor,
|
||||||
|
outlineStyle: style.outlineStyle,
|
||||||
|
outlineWidth: style.outlineWidth,
|
||||||
|
animationName: style.animationName,
|
||||||
|
boxShadow: style.boxShadow,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
expect(currentCityHighlight).toMatchObject({
|
||||||
|
outlineColor: 'rgb(211, 47, 47)',
|
||||||
|
outlineStyle: 'solid',
|
||||||
|
outlineWidth: '2px',
|
||||||
|
animationName: 'none',
|
||||||
|
});
|
||||||
|
expect(currentCityHighlight.boxShadow).toContain('211, 47, 47');
|
||||||
|
await mainMap.screenshot({ path: test.info().outputPath('main-map-current-city-static-highlight-desktop.png') });
|
||||||
|
await mainMap.locator('.city-base').nth(1).click();
|
||||||
|
await expect(page).toHaveURL(/\/current-city\?cityId=2$/u);
|
||||||
|
await page.goBack();
|
||||||
|
await expect(page).toHaveURL(/\/$/u);
|
||||||
|
await expect(page.locator('[data-main-target="map"] .city-base.selected')).toHaveCount(0);
|
||||||
|
|
||||||
await page.getByRole('button', { name: '1턴 명령 입력', exact: true }).click();
|
await page.getByRole('button', { name: '1턴 명령 입력', exact: true }).click();
|
||||||
await page.getByTestId('command-picker').getByRole('button', { name: /화계/ }).click();
|
await page.getByTestId('command-picker').getByRole('button', { name: /화계/ }).click();
|
||||||
@@ -1464,6 +1490,12 @@ test('enters general and nation command arguments and sends exact values', async
|
|||||||
await expect(currentCityMarker).toHaveAttribute('aria-label', '현재 도시 업');
|
await expect(currentCityMarker).toHaveAttribute('aria-label', '현재 도시 업');
|
||||||
await expect(mapCities.nth(0)).toHaveClass(/mine/);
|
await expect(mapCities.nth(0)).toHaveClass(/mine/);
|
||||||
await expect(mapCities.nth(1)).toHaveClass(/selected/);
|
await expect(mapCities.nth(1)).toHaveClass(/selected/);
|
||||||
|
expect(
|
||||||
|
await mapCities
|
||||||
|
.nth(1)
|
||||||
|
.locator('.city-icon')
|
||||||
|
.evaluate((element) => getComputedStyle(element).boxShadow)
|
||||||
|
).toContain('255, 235, 150');
|
||||||
await mapCities.nth(1).hover();
|
await mapCities.nth(1).hover();
|
||||||
expect(await mapCities.nth(1).evaluate((element) => getComputedStyle(element).cursor)).toBe('pointer');
|
expect(await mapCities.nth(1).evaluate((element) => getComputedStyle(element).cursor)).toBe('pointer');
|
||||||
await mapCities.nth(1).focus();
|
await mapCities.nth(1).focus();
|
||||||
@@ -1480,7 +1512,7 @@ test('enters general and nation command arguments and sends exact values', async
|
|||||||
'【허창】에 화계실행'
|
'【허창】에 화계실행'
|
||||||
);
|
);
|
||||||
|
|
||||||
await page.goto('/che/chief-center');
|
await page.goto(gamePath('/chief-center'));
|
||||||
await page.getByRole('button', { name: '1턴 명령 입력', exact: true }).click();
|
await page.getByRole('button', { name: '1턴 명령 입력', exact: true }).click();
|
||||||
const chiefPicker = page.getByTestId('command-picker');
|
const chiefPicker = page.getByTestId('command-picker');
|
||||||
await chiefPicker.getByRole('button', { name: /^(?:국가:)?인사$/, exact: true }).click();
|
await chiefPicker.getByRole('button', { name: /^(?:국가:)?인사$/, exact: true }).click();
|
||||||
@@ -1836,6 +1868,66 @@ test('uses the map to choose a nation target in the chief command window', async
|
|||||||
await page.screenshot({ path: test.info().outputPath('chief-nation-map-option.png'), fullPage: true });
|
await page.screenshot({ path: test.info().outputPath('chief-nation-map-option.png'), fullPage: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('touch command maps select city and nation on the first tap without changing navigation mode', async ({
|
||||||
|
browser,
|
||||||
|
}, testInfo) => {
|
||||||
|
const configuredBaseUrl = testInfo.project.use.baseURL;
|
||||||
|
if (typeof configuredBaseUrl !== 'string') {
|
||||||
|
throw new Error('Playwright baseURL is required for the mobile command map contract');
|
||||||
|
}
|
||||||
|
const context = await browser.newContext({
|
||||||
|
baseURL: configuredBaseUrl,
|
||||||
|
viewport: { width: 390, height: 844 },
|
||||||
|
screen: { width: 390, height: 844 },
|
||||||
|
deviceScaleFactor: 1,
|
||||||
|
isMobile: true,
|
||||||
|
hasTouch: true,
|
||||||
|
colorScheme: 'dark',
|
||||||
|
});
|
||||||
|
const mobilePage = await context.newPage();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await install(mobilePage);
|
||||||
|
await mobilePage.addInitScript(() => localStorage.setItem('sam.toggleSingleTap', 'no'));
|
||||||
|
await mobilePage.goto('/');
|
||||||
|
|
||||||
|
const mainCurrentCity = mobilePage.locator('[data-main-target="map"] .city-base.mine');
|
||||||
|
await expect(mainCurrentCity).toHaveAttribute('aria-label', '업, 현재 도시');
|
||||||
|
await expect(mobilePage.locator('[data-main-target="map"] .city-base.selected')).toHaveCount(0);
|
||||||
|
await mobilePage.locator('[data-main-target="map"]').screenshot({
|
||||||
|
path: testInfo.outputPath('main-map-current-city-static-highlight-mobile.png'),
|
||||||
|
});
|
||||||
|
|
||||||
|
await mobilePage.getByRole('button', { name: '1턴 명령 입력', exact: true }).click();
|
||||||
|
await mobilePage.getByTestId('command-picker').getByRole('button', { name: /화계/ }).click();
|
||||||
|
let form = mobilePage.getByTestId('command-argument-form');
|
||||||
|
let commandMap = form.getByTestId('command-argument-map');
|
||||||
|
await expect(commandMap.locator('.map-toggle-single-tap')).toHaveCount(0);
|
||||||
|
await commandMap.locator('.city-base').nth(1).tap();
|
||||||
|
await expect(form.locator('#command-arg-destCityId')).toHaveValue('2');
|
||||||
|
await expect(commandMap.locator('.city-base').nth(1)).toHaveClass(/selected/);
|
||||||
|
await expect(mobilePage).toHaveURL(/\/$/u);
|
||||||
|
|
||||||
|
await mobilePage.goto(gamePath('/chief-center'));
|
||||||
|
await mobilePage.getByRole('button', { name: '1턴 명령 입력', exact: true }).click();
|
||||||
|
const picker = mobilePage.getByTestId('command-picker');
|
||||||
|
await picker.getByRole('button', { name: /^(?:국가:)?외교$/, exact: true }).click();
|
||||||
|
await picker.getByRole('button', { name: /선전포고/ }).click();
|
||||||
|
form = picker.getByTestId('command-argument-form');
|
||||||
|
commandMap = form.getByTestId('command-argument-map');
|
||||||
|
await expect(commandMap.locator('.map-toggle-single-tap')).toHaveCount(0);
|
||||||
|
await commandMap.locator('.city-base').nth(1).tap();
|
||||||
|
await expect(form.locator('#command-arg-destNationId')).toHaveValue('2');
|
||||||
|
await expect(commandMap.locator('.city-base').nth(1)).toHaveClass(/selected/);
|
||||||
|
await expect(mobilePage).toHaveURL(new RegExp(`${gamePath('/chief-center')}$`, 'u'));
|
||||||
|
expect(await mobilePage.evaluate(() => localStorage.getItem('sam.toggleSingleTap'))).toBe('no');
|
||||||
|
|
||||||
|
await mobilePage.screenshot({ path: testInfo.outputPath('command-map-first-tap-selection-mobile.png') });
|
||||||
|
} finally {
|
||||||
|
await context.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('shows a map and target details for every city or nation argument chief command except assignment', async ({
|
test('shows a map and target details for every city or nation argument chief command except assignment', async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
@@ -481,6 +481,13 @@ test('map keeps desktop hover navigation and lets touch users choose one-tap or
|
|||||||
await expect(page.locator('.map-tooltip .tooltip-title')).toHaveText('【하북|특】업');
|
await expect(page.locator('.map-tooltip .tooltip-title')).toHaveText('【하북|특】업');
|
||||||
await desktopCity.click();
|
await desktopCity.click();
|
||||||
await expect(page).toHaveURL(/\/current-city\?cityId=1$/u);
|
await expect(page).toHaveURL(/\/current-city\?cityId=1$/u);
|
||||||
|
await page.goBack();
|
||||||
|
await expect(page).toHaveURL(/\/global-info$/u);
|
||||||
|
const retainedSelection = page.locator('.city-base').first();
|
||||||
|
await expect(retainedSelection).toHaveClass(/selected/);
|
||||||
|
expect(
|
||||||
|
await retainedSelection.locator('.city-icon').evaluate((element) => getComputedStyle(element).boxShadow)
|
||||||
|
).toContain('255, 235, 150');
|
||||||
|
|
||||||
const configuredBaseUrl = testInfo.project.use.baseURL;
|
const configuredBaseUrl = testInfo.project.use.baseURL;
|
||||||
if (typeof configuredBaseUrl !== 'string') {
|
if (typeof configuredBaseUrl !== 'string') {
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ const touchend = (event: TouchEvent) => {
|
|||||||
:to="
|
:to="
|
||||||
props.selectOnly || props.readonly ? undefined : { name: 'current-city', query: { cityId: props.city.id } }
|
props.selectOnly || props.readonly ? undefined : { name: 'current-city', query: { cityId: props.city.id } }
|
||||||
"
|
"
|
||||||
|
:aria-label="props.city.isMyCity ? `${props.city.name}, 현재 도시` : props.city.name"
|
||||||
:class="[
|
:class="[
|
||||||
`state-${props.city.stateClass}`,
|
`state-${props.city.stateClass}`,
|
||||||
{
|
{
|
||||||
@@ -125,6 +126,7 @@ const touchend = (event: TouchEvent) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.city-dot {
|
.city-dot {
|
||||||
|
position: relative;
|
||||||
border: 1px solid rgba(232, 221, 196, 0.6);
|
border: 1px solid rgba(232, 221, 196, 0.6);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -138,8 +140,16 @@ const touchend = (event: TouchEvent) => {
|
|||||||
background: rgba(232, 221, 196, 0.9);
|
background: rgba(232, 221, 196, 0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-city.mine .city-dot {
|
.map-city.mine .city-dot::after {
|
||||||
box-shadow: 0 0 0 2px rgba(201, 164, 90, 0.6);
|
position: absolute;
|
||||||
|
inset: -4px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 2px;
|
||||||
|
outline: 2px solid rgb(211, 47, 47);
|
||||||
|
box-shadow: 0 0 6px 2px rgba(211, 47, 47, 0.72);
|
||||||
|
content: '';
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-city.selected .city-dot {
|
.map-city.selected .city-dot {
|
||||||
|
|||||||
@@ -184,6 +184,7 @@ const cityStateStyle = computed(() => ({
|
|||||||
:to="
|
:to="
|
||||||
props.selectOnly || props.readonly ? undefined : { name: 'current-city', query: { cityId: props.city.id } }
|
props.selectOnly || props.readonly ? undefined : { name: 'current-city', query: { cityId: props.city.id } }
|
||||||
"
|
"
|
||||||
|
:aria-label="props.city.isMyCity ? `${props.city.name}, 현재 도시` : props.city.name"
|
||||||
:class="[
|
:class="[
|
||||||
{
|
{
|
||||||
mine: props.city.isMyCity,
|
mine: props.city.isMyCity,
|
||||||
@@ -256,11 +257,16 @@ const cityStateStyle = computed(() => ({
|
|||||||
.city-filler {
|
.city-filler {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: -2px;
|
inset: -2px;
|
||||||
|
box-sizing: border-box;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.city-base.mine .city-icon {
|
.city-filler.my-city {
|
||||||
box-shadow: 0 0 0 1px rgba(201, 164, 90, 0.7);
|
inset: -4px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 2px;
|
||||||
|
outline: 2px solid rgb(211, 47, 47);
|
||||||
|
box-shadow: 0 0 6px 2px rgba(211, 47, 47, 0.72);
|
||||||
}
|
}
|
||||||
|
|
||||||
.city-base.selected .city-icon {
|
.city-base.selected .city-icon {
|
||||||
|
|||||||
@@ -72,12 +72,14 @@ const props = withDefaults(
|
|||||||
detailMode?: boolean;
|
detailMode?: boolean;
|
||||||
fitContainer?: boolean;
|
fitContainer?: boolean;
|
||||||
showCurrentCityMarker?: boolean;
|
showCurrentCityMarker?: boolean;
|
||||||
|
showSelectionBorder?: boolean;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
// Vue casts an absent Boolean prop to false unless undefined is an explicit default.
|
// Vue casts an absent Boolean prop to false unless undefined is an explicit default.
|
||||||
detailMode: undefined,
|
detailMode: undefined,
|
||||||
selectedCityId: undefined,
|
selectedCityId: undefined,
|
||||||
|
showSelectionBorder: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -176,6 +178,7 @@ const effectiveDetailMode = computed(() => props.detailMode ?? storeDetailMode.v
|
|||||||
const effectiveSelectedCityId = computed(() =>
|
const effectiveSelectedCityId = computed(() =>
|
||||||
props.selectedCityId === undefined ? storeSelectedCityId.value : props.selectedCityId
|
props.selectedCityId === undefined ? storeSelectedCityId.value : props.selectedCityId
|
||||||
);
|
);
|
||||||
|
const isSelectionMap = computed(() => props.selectedCityId !== undefined);
|
||||||
|
|
||||||
const mapWidth = computed(() => `${BASE_MAP_WIDTH * mapScale.value}px`);
|
const mapWidth = computed(() => `${BASE_MAP_WIDTH * mapScale.value}px`);
|
||||||
|
|
||||||
@@ -213,7 +216,7 @@ const cityViews = computed<CityView[]>(() => {
|
|||||||
y,
|
y,
|
||||||
isCapital: nation?.capitalCityId === layoutCity.id,
|
isCapital: nation?.capitalCityId === layoutCity.id,
|
||||||
isMyCity: props.mapData?.myCity === layoutCity.id,
|
isMyCity: props.mapData?.myCity === layoutCity.id,
|
||||||
selected: effectiveSelectedCityId.value === layoutCity.id,
|
selected: props.showSelectionBorder && effectiveSelectedCityId.value === layoutCity.id,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -417,7 +420,7 @@ const touchCity = (cityId: number, event: TouchEvent) => {
|
|||||||
if (touchPreviewCityId.value !== cityId) {
|
if (touchPreviewCityId.value !== cityId) {
|
||||||
touchPreviewCityId.value = cityId;
|
touchPreviewCityId.value = cityId;
|
||||||
setHoveredCity(cityId);
|
setHoveredCity(cityId);
|
||||||
if (!singleTapNavigation.value) {
|
if (!isSelectionMap.value && !singleTapNavigation.value) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -469,7 +472,7 @@ const selectCity = (cityId: number) => {
|
|||||||
:city="city"
|
:city="city"
|
||||||
:map-scale="mapScale"
|
:map-scale="mapScale"
|
||||||
:show-name="showCityName"
|
:show-name="showCityName"
|
||||||
:select-only="props.selectedCityId !== undefined"
|
:select-only="isSelectionMap"
|
||||||
:readonly="props.readonly"
|
:readonly="props.readonly"
|
||||||
v-bind="detailProps"
|
v-bind="detailProps"
|
||||||
@hover="setHoveredCity"
|
@hover="setHoveredCity"
|
||||||
@@ -497,7 +500,7 @@ const selectCity = (cityId: number) => {
|
|||||||
도시명 표기 {{ showCityName ? '끄기' : '켜기' }}
|
도시명 표기 {{ showCityName ? '끄기' : '켜기' }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="hasTouchInput"
|
v-if="hasTouchInput && !isSelectionMap && !props.readonly"
|
||||||
class="map-toggle map-toggle-single-tap"
|
class="map-toggle map-toggle-single-tap"
|
||||||
:class="{ active: singleTapNavigation }"
|
:class="{ active: singleTapNavigation }"
|
||||||
:aria-pressed="singleTapNavigation"
|
:aria-pressed="singleTapNavigation"
|
||||||
|
|||||||
@@ -324,7 +324,12 @@ watch(
|
|||||||
|
|
||||||
<div v-else-if="panelId === 'map'" class="mobile-panel" data-mobile-panel-id="map">
|
<div v-else-if="panelId === 'map'" class="mobile-panel" data-mobile-panel-id="map">
|
||||||
<PanelCard title="지도" data-main-target="map">
|
<PanelCard title="지도" data-main-target="map">
|
||||||
<MapViewer :map-data="worldMap" :map-layout="mapLayout" :loading="loading" />
|
<MapViewer
|
||||||
|
:map-data="worldMap"
|
||||||
|
:map-layout="mapLayout"
|
||||||
|
:loading="loading"
|
||||||
|
:show-selection-border="false"
|
||||||
|
/>
|
||||||
</PanelCard>
|
</PanelCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -415,7 +420,12 @@ watch(
|
|||||||
|
|
||||||
<section v-else class="layout-desktop">
|
<section v-else class="layout-desktop">
|
||||||
<PanelCard title="지도" subtitle="실시간 지도 + 도시 상황" data-main-target="map">
|
<PanelCard title="지도" subtitle="실시간 지도 + 도시 상황" data-main-target="map">
|
||||||
<MapViewer :map-data="worldMap" :map-layout="mapLayout" :loading="loading" />
|
<MapViewer
|
||||||
|
:map-data="worldMap"
|
||||||
|
:map-layout="mapLayout"
|
||||||
|
:loading="loading"
|
||||||
|
:show-selection-border="false"
|
||||||
|
/>
|
||||||
</PanelCard>
|
</PanelCard>
|
||||||
<PanelCard title="명령 목록" aria-label="명령 목록" data-main-target="commands">
|
<PanelCard title="명령 목록" aria-label="명령 목록" data-main-target="commands">
|
||||||
<CommandListPanel
|
<CommandListPanel
|
||||||
|
|||||||
Reference in New Issue
Block a user