merge: 최신 main을 모바일 터치 드래그 브랜치에 통합

This commit is contained in:
2026-08-21 01:09:37 +00:00
11 changed files with 383 additions and 63 deletions
+60 -7
View File
@@ -953,9 +953,7 @@ test('shows and reserves the Ref spy command for a user on desktop and mobile',
await expect(spy).toBeFocused();
await spy.click();
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(
'인접 도시에서는 더 많은 정보를 얻습니다.'
);
@@ -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') });
});
test('defaults founding to a Ref-selectable nation trait and paints color option labels', async ({
page,
}) => {
test('defaults founding to a Ref-selectable nation trait and paints color option labels', async ({ page }) => {
const foundingColors = [
{ value: 0, label: '색상 1', color: '#FF0000' },
{ 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');
await expect(reloadedChiefPicker.getByRole('button', { name: '전략', exact: true })).toHaveClass(/active/);
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({
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 });
});
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 }) => {
await install(page);
await page.setViewportSize({ width: 1200, height: 900 });
+106
View File
@@ -467,6 +467,112 @@ test('국가 정보의 작위는 Ref 국가 등급 이름으로 표시된다', a
await expect(root).not.toContainText('작 위1');
});
test('map keeps desktop hover navigation and lets touch users choose one-tap or two-tap city navigation', async ({
browser,
page,
}, testInfo) => {
await install(page);
await page.setViewportSize({ width: 1200, height: 900 });
await go(page, 'global-info');
const desktopCity = page.locator('.city-base').first();
await expect(page.locator('.map-toggle-single-tap')).toHaveCount(0);
await desktopCity.hover();
await expect(page.locator('.map-tooltip .tooltip-title')).toHaveText('【하북|특】업');
await desktopCity.click();
await expect(page).toHaveURL(/\/current-city\?cityId=1$/u);
const configuredBaseUrl = testInfo.project.use.baseURL;
if (typeof configuredBaseUrl !== 'string') {
throw new Error('Playwright baseURL is required for the mobile 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 go(mobilePage, 'global-info');
const twoTapButton = mobilePage.getByRole('button', { name: '두번 탭 해 도시 이동 끄기' });
await expect(twoTapButton).toBeVisible();
await expect(twoTapButton).toHaveAttribute('aria-pressed', 'false');
const controlGeometry = await twoTapButton.evaluate((element) => {
const rect = element.getBoundingClientRect();
const mapRect = element.closest('.map-area')?.getBoundingClientRect();
const style = getComputedStyle(element);
return {
right: rect.right,
bottom: rect.bottom,
mapRight: mapRect?.right,
mapBottom: mapRect?.bottom,
fontSize: style.fontSize,
lineHeight: style.lineHeight,
documentWidth: document.documentElement.scrollWidth,
viewportWidth: document.documentElement.clientWidth,
overflowing: Array.from(document.querySelectorAll<HTMLElement>('body *'))
.filter(
(candidate) => candidate.getBoundingClientRect().right > document.documentElement.clientWidth
)
.map((candidate) => ({
tag: candidate.tagName,
className: candidate.className,
right: candidate.getBoundingClientRect().right,
}))
.slice(0, 8),
};
});
expect(controlGeometry).toMatchObject({
fontSize: '11px',
lineHeight: '18px',
viewportWidth: 500,
overflowing: [],
});
expect(controlGeometry.documentWidth).toBeLessThanOrEqual(controlGeometry.viewportWidth + 1);
expect(controlGeometry.mapRight! - controlGeometry.right).toBeCloseTo(4, 1);
expect(controlGeometry.mapBottom! - controlGeometry.bottom).toBeCloseTo(4, 1);
const mobileCities = mobilePage.locator('.city-base');
await mobileCities.nth(0).tap();
await expect(mobilePage).toHaveURL(/\/global-info$/u);
await expect(mobilePage.locator('.map-tooltip .tooltip-title')).toHaveText('【하북|특】업');
await mobileCities.nth(1).tap();
await expect(mobilePage).toHaveURL(/\/global-info$/u);
await expect(mobilePage.locator('.map-tooltip .tooltip-title')).toHaveText('【하북|수】성2');
await mobilePage.screenshot({ path: testInfo.outputPath('mobile-map-first-tap-tooltip.png'), fullPage: true });
await mobileCities.nth(1).tap();
await expect(mobilePage).toHaveURL(/\/current-city\?cityId=2$/u);
await go(mobilePage, 'global-info');
await mobilePage.getByRole('button', { name: '두번 탭 해 도시 이동 끄기' }).click();
const singleTapButton = mobilePage.getByRole('button', { name: '두번 탭 해 도시 이동 켜기' });
await expect(singleTapButton).toHaveAttribute('aria-pressed', 'true');
expect(await mobilePage.evaluate(() => localStorage.getItem('sam.toggleSingleTap'))).toBe('yes');
await mobilePage.reload();
await expect(singleTapButton).toBeVisible();
await mobilePage.locator('.city-base').nth(2).tap();
await expect(mobilePage).toHaveURL(/\/current-city\?cityId=3$/u);
await go(mobilePage, 'global-info');
await mobilePage.getByRole('button', { name: '두번 탭 해 도시 이동 켜기' }).click();
expect(await mobilePage.evaluate(() => localStorage.getItem('sam.toggleSingleTap'))).toBe('no');
await mobilePage.locator('.city-base').first().tap();
await expect(mobilePage).toHaveURL(/\/global-info$/u);
} finally {
await context.close();
}
});
test('global-info renders the ref nation summary columns beside the map', async ({ page }) => {
await install(page);
await page.setViewportSize({ width: 1200, height: 900 });