merge: 최신 main을 턴 실패 개인 기록 작업에 반영
# Conflicts: # app/game-engine/src/turn/reservedTurnHandler.ts
This commit is contained in:
@@ -416,6 +416,22 @@ const commandTable = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'che_첩보',
|
||||
name: '첩보',
|
||||
reqArg: true,
|
||||
possible: true,
|
||||
status: 'needsInput',
|
||||
inputFields: [
|
||||
{
|
||||
key: 'destCityId',
|
||||
label: '대상 도시',
|
||||
kind: 'select',
|
||||
required: true,
|
||||
optionSource: 'cities',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -921,6 +937,48 @@ test('renders and accepts every Ref strategy command at mobile width', async ({
|
||||
await picker.screenshot({ path: test.info().outputPath('all-strategy-commands-mobile.png') });
|
||||
});
|
||||
|
||||
test('shows and reserves the Ref spy command for a user on desktop and mobile', async ({ page }) => {
|
||||
const requests = await install(page);
|
||||
await page.setViewportSize({ width: 1200, height: 900 });
|
||||
await page.goto('/');
|
||||
const editor = page.locator('[data-command-scope="general"]');
|
||||
await editor.getByRole('button', { name: '1턴 명령 입력', exact: true }).click();
|
||||
|
||||
let picker = page.getByTestId('command-picker');
|
||||
await picker.getByRole('button', { name: '군사', exact: true }).click();
|
||||
const spy = picker.getByRole('button', { name: '첩보', exact: true });
|
||||
await expect(spy).toBeVisible();
|
||||
await spy.hover();
|
||||
await spy.focus();
|
||||
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 form.locator('select').selectOption('2');
|
||||
await picker.screenshot({ path: test.info().outputPath('spy-command-desktop-1200.png') });
|
||||
await picker.getByRole('button', { name: '입력', exact: true }).click();
|
||||
await expect(editor.locator('.action-column > div').first()).toHaveText('【허창】에 첩보 실행');
|
||||
expect(JSON.stringify(requests)).toContain('"action":"che_첩보","args":{"destCityId":2}');
|
||||
|
||||
await page.setViewportSize({ width: 500, height: 900 });
|
||||
await editor.getByRole('button', { name: '2턴 명령 입력', exact: true }).click();
|
||||
picker = page.getByTestId('command-picker');
|
||||
await picker.getByRole('button', { name: '군사', exact: true }).click();
|
||||
await expect(picker.getByRole('button', { name: '첩보', exact: true })).toBeVisible();
|
||||
const geometry = await picker.evaluate((element) => ({
|
||||
width: element.getBoundingClientRect().width,
|
||||
horizontalOverflow: element.scrollWidth - element.clientWidth,
|
||||
}));
|
||||
expect(geometry.width).toBeLessThanOrEqual(500);
|
||||
expect(geometry.horizontalOverflow).toBeLessThanOrEqual(0);
|
||||
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,
|
||||
}) => {
|
||||
|
||||
@@ -1287,7 +1287,9 @@ test('내 정보&설정의 지난 플레이는 기본 탐색과 분리되어 오
|
||||
const actions = element.querySelector<HTMLElement>('.title-actions')!.getBoundingClientRect();
|
||||
const navigation = element.querySelector<HTMLElement>('.navigation-actions')!.getBoundingClientRect();
|
||||
const back = element.querySelector<HTMLAnchorElement>('.navigation-actions a')!.getBoundingClientRect();
|
||||
const refresh = element.querySelector<HTMLButtonElement>('.navigation-actions button')!.getBoundingClientRect();
|
||||
const refresh = element
|
||||
.querySelector<HTMLButtonElement>('.navigation-actions button')!
|
||||
.getBoundingClientRect();
|
||||
const past = element.querySelector<HTMLAnchorElement>('.past-plays-link')!.getBoundingClientRect();
|
||||
const pastStyle = getComputedStyle(element.querySelector<HTMLAnchorElement>('.past-plays-link')!);
|
||||
return {
|
||||
@@ -1325,6 +1327,77 @@ test('내 정보&설정의 지난 플레이는 기본 탐색과 분리되어 오
|
||||
}
|
||||
});
|
||||
|
||||
test('내 정보&설정에서 모바일 메인 패널을 드래그하거나 버튼으로 재정렬하고 기본 순서로 복원한다', async ({ page }) => {
|
||||
const state: FixtureState = { permission: 'head', myset: 3, settingMutations: [], accessPages: [] };
|
||||
await install(page, state);
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.goto('my-page');
|
||||
|
||||
await page.getByRole('button', { name: '순서 바꾸기', exact: true }).click();
|
||||
const dialog = page.getByRole('dialog', { name: '모바일 레이아웃 순서 바꾸기' });
|
||||
await expect(dialog).toBeVisible();
|
||||
const readOrder = () =>
|
||||
dialog
|
||||
.locator('[data-mobile-layout-id]')
|
||||
.evaluateAll((elements) => elements.map((element) => element.getAttribute('data-mobile-layout-id')));
|
||||
const defaultOrder = [
|
||||
'commands',
|
||||
'nation-menu',
|
||||
'nation',
|
||||
'general',
|
||||
'city',
|
||||
'map',
|
||||
'records',
|
||||
'global-menu',
|
||||
'messages',
|
||||
];
|
||||
await expect.poll(readOrder).toEqual(defaultOrder);
|
||||
|
||||
await dialog
|
||||
.locator('[data-mobile-layout-id="messages"]')
|
||||
.dragTo(dialog.locator('[data-mobile-layout-id="commands"]'));
|
||||
await expect
|
||||
.poll(readOrder)
|
||||
.toEqual(['messages', 'commands', 'nation-menu', 'nation', 'general', 'city', 'map', 'records', 'global-menu']);
|
||||
await dialog.getByRole('button', { name: '지도 위로' }).click();
|
||||
await expect
|
||||
.poll(readOrder)
|
||||
.toEqual(['messages', 'commands', 'nation-menu', 'nation', 'general', 'map', 'city', 'records', 'global-menu']);
|
||||
|
||||
const dialogGeometry = await dialog.evaluate((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const firstItem = element.querySelector<HTMLElement>('[data-mobile-layout-id]')?.getBoundingClientRect();
|
||||
const moveButton = element.querySelector<HTMLButtonElement>('[aria-label$="아래로"]')?.getBoundingClientRect();
|
||||
return {
|
||||
rect: rect.toJSON(),
|
||||
firstItem: firstItem?.toJSON() ?? null,
|
||||
moveButton: moveButton?.toJSON() ?? null,
|
||||
overflowX: getComputedStyle(element).overflowX,
|
||||
documentWidth: document.documentElement.scrollWidth,
|
||||
};
|
||||
});
|
||||
expect(dialogGeometry.rect.left).toBeGreaterThanOrEqual(0);
|
||||
expect(dialogGeometry.rect.right).toBeLessThanOrEqual(390);
|
||||
expect(dialogGeometry.firstItem?.height).toBeGreaterThanOrEqual(44);
|
||||
expect(dialogGeometry.moveButton?.width).toBeGreaterThanOrEqual(36);
|
||||
expect(dialogGeometry.documentWidth).toBe(390);
|
||||
await persistParityArtifact(page, 'core-my-page-mobile-layout-order-dialog', dialogGeometry);
|
||||
|
||||
await dialog.getByRole('button', { name: '적용', exact: true }).click();
|
||||
await expect(dialog).toBeHidden();
|
||||
await expect
|
||||
.poll(() => page.evaluate(() => JSON.parse(localStorage.getItem('sam.mobileMainPanelOrder.v1') ?? '[]')))
|
||||
.toEqual(['messages', 'commands', 'nation-menu', 'nation', 'general', 'map', 'city', 'records', 'global-menu']);
|
||||
|
||||
await page.getByRole('button', { name: '순서 바꾸기', exact: true }).click();
|
||||
await dialog.getByRole('button', { name: '기본값', exact: true }).click();
|
||||
await expect.poll(readOrder).toEqual(defaultOrder);
|
||||
await dialog.getByRole('button', { name: '적용', exact: true }).click();
|
||||
await expect
|
||||
.poll(() => page.evaluate(() => JSON.parse(localStorage.getItem('sam.mobileMainPanelOrder.v1') ?? '[]')))
|
||||
.toEqual(defaultOrder);
|
||||
});
|
||||
|
||||
for (const [label, failure] of [
|
||||
['daemon timeout', 'TIMEOUT'],
|
||||
['engine transaction 오류', 'INTERNAL_SERVER_ERROR'],
|
||||
|
||||
@@ -11,6 +11,7 @@ const isLegacyRequest = (route: Route): boolean =>
|
||||
|
||||
const installArchiveViews = async (page: Page) => {
|
||||
const hallRequests: string[] = [];
|
||||
const dynastyRequests: string[] = [];
|
||||
await page.addInitScript((profile) => {
|
||||
localStorage.setItem('sammo-game-token', 'ga_archive_views');
|
||||
localStorage.setItem('sammo-game-profile', profile);
|
||||
@@ -21,6 +22,9 @@ const installArchiveViews = async (page: Page) => {
|
||||
if (operations.some((operation) => operation.startsWith('ranking.getHallOfFame'))) {
|
||||
hallRequests.push(decodeURIComponent(`${route.request().url()} ${route.request().postData() ?? ''}`));
|
||||
}
|
||||
if (operations.some((operation) => operation.startsWith('dynasty.'))) {
|
||||
dynastyRequests.push(decodeURIComponent(`${route.request().url()} ${route.request().postData() ?? ''}`));
|
||||
}
|
||||
const results = operations.map((operation) => {
|
||||
if (operation === 'auth.status') return response({ ok: true });
|
||||
if (operation === 'lobby.info') return response({ myGeneral: { id: 1, name: '기록장수' } });
|
||||
@@ -67,8 +71,8 @@ const installArchiveViews = async (page: Page) => {
|
||||
{
|
||||
id: legacy ? 101 : 1,
|
||||
source: legacy ? 'legacy' : 'current',
|
||||
sourceProfile: legacy ? 'hwe' : 'che',
|
||||
serverId: legacy ? 'hwe-old-1' : 'che-current-1',
|
||||
sourceProfile: 'che',
|
||||
serverId: legacy ? 'che-old-1' : 'che-current-1',
|
||||
phase: legacy ? '이전 1기' : '현재 1기',
|
||||
name: '촉',
|
||||
year: 215,
|
||||
@@ -93,10 +97,10 @@ const installArchiveViews = async (page: Page) => {
|
||||
if (operation === 'dynasty.getDetail') {
|
||||
return response({
|
||||
source: 'legacy',
|
||||
sourceProfile: 'hwe',
|
||||
sourceProfile: 'che',
|
||||
emperor: {
|
||||
id: 101,
|
||||
serverId: 'hwe-old-1',
|
||||
serverId: 'che-old-1',
|
||||
winnerNationId: 1,
|
||||
phase: '이전 1기',
|
||||
nationCount: '1 / 2',
|
||||
@@ -189,7 +193,7 @@ const installArchiveViews = async (page: Page) => {
|
||||
});
|
||||
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(results) });
|
||||
});
|
||||
return { hallRequests };
|
||||
return { dynastyRequests, hallRequests };
|
||||
};
|
||||
|
||||
test('명예의 전당은 현재 profile의 현재·이전 서버 기록만 조회한다', async ({ page }, testInfo) => {
|
||||
@@ -217,19 +221,36 @@ test('명예의 전당은 현재 profile의 현재·이전 서버 기록만 조
|
||||
await page.screenshot({ path: testInfo.outputPath('hall-profile-scope-mobile.png'), fullPage: true });
|
||||
});
|
||||
|
||||
test('왕조 일람과 상세는 이전 서버 source와 profile을 유지한다', async ({ page }) => {
|
||||
await installArchiveViews(page);
|
||||
test('왕조 일람과 상세는 현재 profile의 이전 서버 기록만 조회한다', async ({ page }, testInfo) => {
|
||||
const state = await installArchiveViews(page);
|
||||
await page.setViewportSize({ width: 1200, height: 800 });
|
||||
await page.goto('dynasty');
|
||||
|
||||
await expect(page.getByText('현재 1기')).toBeVisible();
|
||||
await page.getByLabel('기록 구분').focus();
|
||||
await expect(page.getByLabel('기록 구분')).toBeFocused();
|
||||
await page.getByLabel('기록 구분').selectOption('legacy');
|
||||
await expect(page.getByText(/이전 1기.*HWE 이전 서버/)).toBeVisible();
|
||||
await expect(page.getByText(/이전 1기.*이전 서버/)).toBeVisible();
|
||||
await expect(page.getByText(/CHE 이전 서버|HWE 이전 서버/)).toHaveCount(0);
|
||||
await expect(page.locator('.dynasty-page')).toHaveCSS('width', '1000px');
|
||||
await expect(page.locator('.dynasty-table')).toHaveCSS('height', '139px');
|
||||
await expect(page.locator('.dynasty-table .phase-heading')).toHaveCSS('background-color', 'rgb(135, 206, 235)');
|
||||
await page.screenshot({ path: testInfo.outputPath('dynasty-list-profile-scope-desktop.png'), fullPage: true });
|
||||
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await expect(page.locator('.dynasty-page')).toHaveCSS('width', '1000px');
|
||||
await page.screenshot({ path: testInfo.outputPath('dynasty-list-profile-scope-mobile.png'), fullPage: true });
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 800 });
|
||||
const detailLink = page.getByRole('link', { name: '자세히' });
|
||||
await expect(detailLink).toHaveAttribute('href', /dynasty\/101\?source=legacy$/);
|
||||
await detailLink.click();
|
||||
await expect(page.getByText(/이전 1기.*HWE 이전 서버/)).toBeVisible();
|
||||
await expect(page.getByText(/이전 1기.*이전 서버/)).toBeVisible();
|
||||
await expect(page.getByText(/CHE 이전 서버|HWE 이전 서버/)).toHaveCount(0);
|
||||
await expect(page.locator('.dynasty-page')).toHaveCSS('width', '1000px');
|
||||
expect(state.dynastyRequests.some((request) => request.includes('legacy'))).toBe(true);
|
||||
expect(state.dynastyRequests.every((request) => !request.includes('sourceProfile'))).toBe(true);
|
||||
await page.screenshot({ path: testInfo.outputPath('dynasty-detail-profile-scope-desktop.png'), fullPage: true });
|
||||
});
|
||||
|
||||
test('연감 국가 라벨은 밝은 배경에 검정, 어두운 배경에 흰 글자를 사용한다', async ({ page }, testInfo) => {
|
||||
|
||||
@@ -52,6 +52,8 @@ type NavigationFixture = {
|
||||
currentYear?: number;
|
||||
currentMonth?: number;
|
||||
serverId?: string;
|
||||
profile?: string;
|
||||
gameIdx?: number;
|
||||
scenarioTitle?: string;
|
||||
nationColor?: string;
|
||||
lastExecuted?: string | null;
|
||||
@@ -95,11 +97,10 @@ type DashboardBundleInput = {
|
||||
const operationInput = (route: Route, index: number): DashboardBundleInput => {
|
||||
const request = route.request();
|
||||
const queryInput = new URL(request.url()).searchParams.get('input');
|
||||
const parsed = (request.postData()
|
||||
? request.postDataJSON()
|
||||
: queryInput
|
||||
? JSON.parse(queryInput)
|
||||
: {}) as Record<string, unknown>;
|
||||
const parsed = (request.postData() ? request.postDataJSON() : queryInput ? JSON.parse(queryInput) : {}) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
const entry = (parsed[String(index)] ?? parsed) as { json?: DashboardBundleInput };
|
||||
return entry.json ?? (entry as DashboardBundleInput);
|
||||
};
|
||||
@@ -253,32 +254,32 @@ const commandTableFixture = (large: boolean, blockedCount = 0, refCategories = f
|
||||
general: draftCommands
|
||||
? draftCommandGroups
|
||||
: refCategories
|
||||
? refCommandCategoryFixture
|
||||
: large
|
||||
? ['내정', '군사', '계략'].map((category, categoryIndex) => ({
|
||||
category,
|
||||
values: Array.from({ length: 16 }, (_, localIndex) => {
|
||||
const index = categoryIndex * 16 + localIndex;
|
||||
return {
|
||||
key: `command-${index}`,
|
||||
name: index === 0 ? '주민 선정과 장기 도시 개발' : `명령 ${index}`,
|
||||
reqArg: index % 2 === 0,
|
||||
possible: index >= blockedCount,
|
||||
status: index >= blockedCount ? 'available' : 'blocked',
|
||||
inputFields: [
|
||||
{
|
||||
key: 'amount',
|
||||
label: '수량',
|
||||
kind: 'number',
|
||||
required: true,
|
||||
min: 1,
|
||||
max: 10_000,
|
||||
},
|
||||
],
|
||||
};
|
||||
}),
|
||||
}))
|
||||
: [],
|
||||
? refCommandCategoryFixture
|
||||
: large
|
||||
? ['내정', '군사', '계략'].map((category, categoryIndex) => ({
|
||||
category,
|
||||
values: Array.from({ length: 16 }, (_, localIndex) => {
|
||||
const index = categoryIndex * 16 + localIndex;
|
||||
return {
|
||||
key: `command-${index}`,
|
||||
name: index === 0 ? '주민 선정과 장기 도시 개발' : `명령 ${index}`,
|
||||
reqArg: index % 2 === 0,
|
||||
possible: index >= blockedCount,
|
||||
status: index >= blockedCount ? 'available' : 'blocked',
|
||||
inputFields: [
|
||||
{
|
||||
key: 'amount',
|
||||
label: '수량',
|
||||
kind: 'number',
|
||||
required: true,
|
||||
min: 1,
|
||||
max: 10_000,
|
||||
},
|
||||
],
|
||||
};
|
||||
}),
|
||||
}))
|
||||
: [],
|
||||
nation: [],
|
||||
inputOptions: {
|
||||
cities: Array.from({ length: 20 }, (_, index) => ({ value: index + 1, label: `도시 ${index + 1}` })),
|
||||
@@ -500,7 +501,7 @@ const installFixture = async (page: Page, state: NavigationFixture) => {
|
||||
? response({ id: 'user-7', username: 'menu-user', displayName: '메뉴 사용자' })
|
||||
: operation === 'navigation.get'
|
||||
? response(runtimeNavigation)
|
||||
: response({ ok: true })
|
||||
: response({ ok: true })
|
||||
);
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
@@ -530,6 +531,8 @@ const installFixture = async (page: Page, state: NavigationFixture) => {
|
||||
return response({
|
||||
myGeneral: { id: 7, name: '메뉴검증장수' },
|
||||
serverId: state.serverId ?? 'che_fixture_season',
|
||||
profile: state.profile ?? 'che',
|
||||
gameIdx: state.gameIdx ?? 101,
|
||||
year: state.currentYear ?? 185,
|
||||
month: state.currentMonth ?? 1,
|
||||
turnTerm: 10,
|
||||
@@ -793,6 +796,103 @@ const gridColumnCount = async (page: Page, selector: string) =>
|
||||
.first()
|
||||
.evaluate((element) => getComputedStyle(element).gridTemplateColumns.split(' ').length);
|
||||
|
||||
const setMobilePanelOrder = async (page: Page, order: readonly string[]) => {
|
||||
await page.evaluate((nextOrder) => {
|
||||
localStorage.setItem('sam.mobileMainPanelOrder.v1', JSON.stringify(nextOrder));
|
||||
document.dispatchEvent(new CustomEvent('sam-mobile-main-panel-order-changed'));
|
||||
}, order);
|
||||
};
|
||||
|
||||
const inspectMobilePanelLayout = async (page: Page) =>
|
||||
page.locator('.layout-mobile').evaluate((container) => {
|
||||
const containerStyle = getComputedStyle(container);
|
||||
const panels = [...container.querySelectorAll<HTMLElement>(':scope > [data-mobile-panel-id]')].map(
|
||||
(element, domIndex) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const style = getComputedStyle(element);
|
||||
const content = element.firstElementChild as HTMLElement | null;
|
||||
const contentStyle = content ? getComputedStyle(content) : null;
|
||||
return {
|
||||
id: element.dataset.mobilePanelId ?? '',
|
||||
domIndex,
|
||||
top: rect.top,
|
||||
bottom: rect.bottom,
|
||||
left: rect.left,
|
||||
right: rect.right,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
display: style.display,
|
||||
position: style.position,
|
||||
inset: [style.top, style.right, style.bottom, style.left],
|
||||
order: style.order,
|
||||
transform: style.transform,
|
||||
float: style.cssFloat,
|
||||
gridRow: `${style.gridRowStart} / ${style.gridRowEnd}`,
|
||||
gridColumn: `${style.gridColumnStart} / ${style.gridColumnEnd}`,
|
||||
marginTop: style.marginTop,
|
||||
marginBottom: style.marginBottom,
|
||||
content: contentStyle
|
||||
? {
|
||||
position: contentStyle.position,
|
||||
order: contentStyle.order,
|
||||
transform: contentStyle.transform,
|
||||
marginTop: contentStyle.marginTop,
|
||||
marginBottom: contentStyle.marginBottom,
|
||||
height: contentStyle.height,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}
|
||||
);
|
||||
return {
|
||||
container: {
|
||||
display: containerStyle.display,
|
||||
flexDirection: containerStyle.flexDirection,
|
||||
position: containerStyle.position,
|
||||
transform: containerStyle.transform,
|
||||
},
|
||||
panels,
|
||||
visualOrder: [...panels]
|
||||
.sort((left, right) => left.top - right.top || left.left - right.left)
|
||||
.map(({ id }) => id),
|
||||
};
|
||||
});
|
||||
|
||||
const expectMobilePanelVisualOrder = async (page: Page, expectedOrder: readonly string[]) => {
|
||||
await expect
|
||||
.poll(() =>
|
||||
page
|
||||
.locator('.layout-mobile > [data-mobile-panel-id]')
|
||||
.evaluateAll((elements) => elements.map((element) => element.getAttribute('data-mobile-panel-id')))
|
||||
)
|
||||
.toEqual(expectedOrder);
|
||||
const audit = await inspectMobilePanelLayout(page);
|
||||
expect(audit.container).toEqual({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
position: 'static',
|
||||
transform: 'none',
|
||||
});
|
||||
expect(audit.panels.map(({ id }) => id)).toEqual(expectedOrder);
|
||||
expect(audit.visualOrder).toEqual(expectedOrder);
|
||||
expect(audit.panels.every(({ left, right, width }) => left >= 0 && right <= 500 && width === 500)).toBe(true);
|
||||
expect(
|
||||
audit.panels.every((panel, index) => index === 0 || panel.top >= audit.panels[index - 1]!.bottom)
|
||||
).toBe(true);
|
||||
for (const panel of audit.panels) {
|
||||
expect(panel.display, `${panel.id}: display`).not.toBe('none');
|
||||
expect(['static', 'relative'], `${panel.id}: position`).toContain(panel.position);
|
||||
expect(
|
||||
panel.inset.every((value) => value === 'auto' || value === '0px'),
|
||||
`${panel.id}: inset ${panel.inset.join(' ')}`
|
||||
).toBe(true);
|
||||
expect(panel.order, `${panel.id}: order`).toBe('0');
|
||||
expect(panel.transform, `${panel.id}: transform`).toBe('none');
|
||||
expect(panel.float, `${panel.id}: float`).toBe('none');
|
||||
}
|
||||
return audit;
|
||||
};
|
||||
|
||||
const raisedButtonState = async (target: Locator) =>
|
||||
target.evaluate((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
@@ -1016,7 +1116,9 @@ test('desktop menus preserve ref columns, prefix-safe routes, and controlled dro
|
||||
await expect(page.locator('.main-mobile-bottom')).toBeHidden();
|
||||
await expect(page.locator('.layout-desktop')).toBeVisible();
|
||||
await expect(page.locator('.layout-mobile')).toHaveCount(0);
|
||||
await expect(page.getByRole('heading', { name: '메인 화면 검증 시나리오', exact: true })).toHaveCount(1);
|
||||
await expect(page.getByRole('heading', { name: '메인 화면 검증 시나리오 체섭 101기', exact: true })).toHaveCount(
|
||||
1
|
||||
);
|
||||
await expect(page.locator('.game-shell__subtitle')).toHaveCount(0);
|
||||
await expect(page.locator('.legacy-game-info')).toContainText('현재: 185년 1월');
|
||||
await expect(page.locator('.legacy-game-info')).toContainText('턴: 10분');
|
||||
@@ -1168,6 +1270,56 @@ test('desktop menus preserve ref columns, prefix-safe routes, and controlled dro
|
||||
await persistArtifact(page, `${basePath.slice(1)}-desktop-1200`);
|
||||
});
|
||||
|
||||
test('shows the persisted official game index beside the scenario title without viewport overflow', async ({ page }) => {
|
||||
const state: NavigationFixture = {
|
||||
officerLevel: 5,
|
||||
permission: 2,
|
||||
nationLevel: 3,
|
||||
stage: 0,
|
||||
npcMode: 1,
|
||||
profile: 'hwe',
|
||||
gameIdx: 7,
|
||||
scenarioTitle: '메인 화면 검증 시나리오',
|
||||
generalMeCalls: 0,
|
||||
operations: [],
|
||||
};
|
||||
await installFixture(page, state);
|
||||
if (artifactRoot) await mkdir(resolve(artifactRoot), { recursive: true });
|
||||
|
||||
for (const viewport of [
|
||||
{ width: 1200, height: 900 },
|
||||
{ width: 500, height: 900 },
|
||||
]) {
|
||||
await page.setViewportSize(viewport);
|
||||
if (page.url() === 'about:blank') await waitForMain(page);
|
||||
|
||||
const title = page.getByRole('heading', { name: '메인 화면 검증 시나리오 훼섭 7기', exact: true });
|
||||
await expect(title).toBeVisible();
|
||||
const geometry = await title.evaluate((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const mainRect = element.closest<HTMLElement>('.main-page')?.getBoundingClientRect();
|
||||
const style = getComputedStyle(element);
|
||||
return {
|
||||
left: rect.left,
|
||||
right: rect.right,
|
||||
mainLeft: mainRect?.left,
|
||||
mainRight: mainRect?.right,
|
||||
fontFamily: style.fontFamily,
|
||||
fontSize: style.fontSize,
|
||||
lineHeight: style.lineHeight,
|
||||
documentOverflow: document.documentElement.scrollWidth - document.documentElement.clientWidth,
|
||||
};
|
||||
});
|
||||
expect(geometry.left).toBeGreaterThanOrEqual(geometry.mainLeft ?? 0);
|
||||
expect(geometry.right).toBeLessThanOrEqual(geometry.mainRight ?? viewport.width);
|
||||
expect(geometry.documentOverflow).toBeLessThanOrEqual(0);
|
||||
expect(geometry.fontSize).toBe('25.6px');
|
||||
expect(geometry.lineHeight).toBe('38.4px');
|
||||
expect(geometry.fontFamily).toContain('Pretendard');
|
||||
await persistArtifact(page, `official-game-index-${viewport.width}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('nation split buttons keep square inner corners and a single divider in every interaction state', async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
@@ -1558,10 +1710,7 @@ test('message targets keep reply behavior and use nation-color contrast in label
|
||||
|
||||
await page.setViewportSize({ width: 500, height: 900 });
|
||||
const mobilePanel = page.locator('.mobile-message-panel');
|
||||
await expect(mobilePanel.locator('.msg-plate[data-id="101"] .msg-target')).toHaveCSS(
|
||||
'color',
|
||||
'rgb(255, 255, 255)'
|
||||
);
|
||||
await expect(mobilePanel.locator('.msg-plate[data-id="101"] .msg-target')).toHaveCSS('color', 'rgb(255, 255, 255)');
|
||||
await expect(mobilePanel.locator('.msg-plate[data-id="103"] .msg-target')).toHaveCSS('color', 'rgb(0, 0, 0)');
|
||||
await expect(mobilePanel.locator('#mailbox_list optgroup[label="밝은국"]')).toHaveCSS('color', 'rgb(0, 0, 0)');
|
||||
await persistArtifact(page, `${basePath.slice(1)}-message-nation-contrast-mobile-500`);
|
||||
@@ -2146,7 +2295,7 @@ test('the 939/940 boundary switches to the Ref-style 500px single document', asy
|
||||
await expect(page.locator('.main-mobile-bottom')).toBeVisible();
|
||||
|
||||
await page.setViewportSize({ width: 500, height: 900 });
|
||||
await expect(page.getByRole('heading', { name: '모바일 검증 시나리오', exact: true })).toHaveCount(1);
|
||||
await expect(page.getByRole('heading', { name: '모바일 검증 시나리오 체섭 101기', exact: true })).toHaveCount(1);
|
||||
await expect(page.locator('.game-shell__subtitle')).toHaveCount(0);
|
||||
await expect(page.locator('.legacy-game-info')).toContainText('현재: 185년 1월');
|
||||
await expect(page.locator('.legacy-game-info')).toContainText('턴: 10분');
|
||||
@@ -2217,6 +2366,45 @@ test('the 939/940 boundary switches to the Ref-style 500px single document', asy
|
||||
]) {
|
||||
await expect(page.locator(selector)).toBeVisible();
|
||||
}
|
||||
const defaultOrder = [
|
||||
'commands',
|
||||
'nation-menu',
|
||||
'nation',
|
||||
'general',
|
||||
'city',
|
||||
'map',
|
||||
'records',
|
||||
'global-menu',
|
||||
'messages',
|
||||
];
|
||||
const customOrder = [
|
||||
'messages',
|
||||
'map',
|
||||
'commands',
|
||||
'nation-menu',
|
||||
'nation',
|
||||
'general',
|
||||
'city',
|
||||
'records',
|
||||
'global-menu',
|
||||
];
|
||||
const reverseOrder = [...defaultOrder].reverse();
|
||||
const mobilePanelAudits = {
|
||||
default: await expectMobilePanelVisualOrder(page, defaultOrder),
|
||||
custom: null as Awaited<ReturnType<typeof inspectMobilePanelLayout>> | null,
|
||||
reverse: null as Awaited<ReturnType<typeof inspectMobilePanelLayout>> | null,
|
||||
};
|
||||
await setMobilePanelOrder(page, customOrder);
|
||||
mobilePanelAudits.custom = await expectMobilePanelVisualOrder(page, customOrder);
|
||||
await setMobilePanelOrder(page, reverseOrder);
|
||||
mobilePanelAudits.reverse = await expectMobilePanelVisualOrder(page, reverseOrder);
|
||||
if (artifactRoot) {
|
||||
await mkdir(artifactRoot, { recursive: true });
|
||||
await writeFile(
|
||||
resolve(artifactRoot, `${basePath.slice(1)}-mobile-panel-css-order-audit.json`),
|
||||
`${JSON.stringify(mobilePanelAudits, null, 2)}\n`
|
||||
);
|
||||
}
|
||||
await persistArtifact(page, `${basePath.slice(1)}-mobile-500`);
|
||||
});
|
||||
|
||||
@@ -2517,6 +2705,27 @@ test('real mobile devices initially fit the complete 500px game canvas', async (
|
||||
expect(mainGeometry.documentScrollWidth).toBeLessThanOrEqual(mainGeometry.innerWidth);
|
||||
expect(mainGeometry.canvas).toEqual({ left: 0, right: 500, width: 500 });
|
||||
expect(mainGeometry.canvas.right).toBeLessThanOrEqual((mainGeometry.visualViewportWidth ?? 0) + 0.01);
|
||||
let physicalPanelOrderAudit: unknown = null;
|
||||
if (deviceWidth === 390) {
|
||||
const defaultOrder = [
|
||||
'commands',
|
||||
'nation-menu',
|
||||
'nation',
|
||||
'general',
|
||||
'city',
|
||||
'map',
|
||||
'records',
|
||||
'global-menu',
|
||||
'messages',
|
||||
];
|
||||
const reverseOrder = [...defaultOrder].reverse();
|
||||
const defaultAudit = await expectMobilePanelVisualOrder(mobilePage, defaultOrder);
|
||||
await setMobilePanelOrder(mobilePage, reverseOrder);
|
||||
const reverseAudit = await expectMobilePanelVisualOrder(mobilePage, reverseOrder);
|
||||
physicalPanelOrderAudit = { default: defaultAudit, reverse: reverseAudit };
|
||||
await setMobilePanelOrder(mobilePage, defaultOrder);
|
||||
await expectMobilePanelVisualOrder(mobilePage, defaultOrder);
|
||||
}
|
||||
if (artifactRoot) {
|
||||
await mkdir(artifactRoot, { recursive: true });
|
||||
await mobilePage.screenshot({
|
||||
@@ -2560,7 +2769,11 @@ test('real mobile devices initially fit the complete 500px game canvas', async (
|
||||
}
|
||||
}
|
||||
|
||||
measurements[String(deviceWidth)] = { main: mainGeometry, routes: routeGeometry };
|
||||
measurements[String(deviceWidth)] = {
|
||||
main: mainGeometry,
|
||||
mobilePanelOrder: physicalPanelOrderAudit,
|
||||
routes: routeGeometry,
|
||||
};
|
||||
await context.close();
|
||||
}
|
||||
|
||||
@@ -3040,9 +3253,9 @@ for (const viewport of [
|
||||
await refreshActivityAndCommands();
|
||||
await expect(picker.getByLabel('장비 종류', { exact: true })).toHaveValue('weapon');
|
||||
await expect(picker.getByLabel('장비', { exact: true })).toHaveValue('청룡언월도');
|
||||
await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(
|
||||
viewport.width
|
||||
);
|
||||
await expect
|
||||
.poll(() => page.evaluate(() => document.documentElement.scrollWidth))
|
||||
.toBeLessThanOrEqual(viewport.width);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,420 @@
|
||||
import { expect, test, type Page, type Route } from '@playwright/test';
|
||||
|
||||
import { gameProfile, gameTrpcRoute } from './gameTestPaths.js';
|
||||
|
||||
type Role = 'head' | 'member';
|
||||
type AppointmentInput = { destGeneralId: number; destCityId: number; officerLevel: number };
|
||||
type FixtureState = {
|
||||
role: Role;
|
||||
appointed: boolean;
|
||||
secretForbidden?: boolean;
|
||||
appointmentInputs: AppointmentInput[];
|
||||
};
|
||||
|
||||
const response = (data: unknown) => ({ result: { data } });
|
||||
const errorResponse = (path: string, message: string, code = 'BAD_REQUEST') => ({
|
||||
error: { message, code: -32000, data: { code, httpStatus: code === 'FORBIDDEN' ? 403 : 400, path } },
|
||||
});
|
||||
const operations = (route: Route): string[] =>
|
||||
decodeURIComponent(new URL(route.request().url()).pathname.split('/trpc/')[1] ?? '').split(',');
|
||||
const requestInput = (route: Route, index: number): Record<string, unknown> => {
|
||||
const body: unknown = route.request().postData() ? route.request().postDataJSON() : {};
|
||||
const record = body && typeof body === 'object' ? (body as Record<string, unknown>) : {};
|
||||
const raw = record[String(index)] ?? record;
|
||||
const payload = raw && typeof raw === 'object' ? (raw as Record<string, unknown>) : {};
|
||||
const input = payload.input && typeof payload.input === 'object' ? (payload.input as Record<string, unknown>) : {};
|
||||
const json = payload.json ?? input.json ?? payload;
|
||||
return json && typeof json === 'object' ? (json as Record<string, unknown>) : {};
|
||||
};
|
||||
|
||||
const cities = [
|
||||
{
|
||||
id: 1,
|
||||
name: '허창',
|
||||
level: 7,
|
||||
region: 2,
|
||||
population: 99_000,
|
||||
populationMax: 100_000,
|
||||
agriculture: 9_500,
|
||||
agricultureMax: 10_000,
|
||||
commerce: 8_000,
|
||||
commerceMax: 10_000,
|
||||
security: 8_000,
|
||||
securityMax: 10_000,
|
||||
trust: 80,
|
||||
trade: 100,
|
||||
defence: 4_500,
|
||||
defenceMax: 5_000,
|
||||
wall: 4_500,
|
||||
wallMax: 5_000,
|
||||
supplyState: 1,
|
||||
frontState: 0,
|
||||
incomes: { gold: 1000, rice: 900, wall: 800 },
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '낙양',
|
||||
level: 6,
|
||||
region: 2,
|
||||
population: 60_000,
|
||||
populationMax: 100_000,
|
||||
agriculture: 5_000,
|
||||
agricultureMax: 10_000,
|
||||
commerce: 5_000,
|
||||
commerceMax: 10_000,
|
||||
security: 5_000,
|
||||
securityMax: 10_000,
|
||||
trust: 70,
|
||||
trade: 90,
|
||||
defence: 2_500,
|
||||
defenceMax: 5_000,
|
||||
wall: 2_500,
|
||||
wallMax: 5_000,
|
||||
supplyState: 1,
|
||||
frontState: 0,
|
||||
incomes: { gold: 800, rice: 700, wall: 600 },
|
||||
},
|
||||
] as const;
|
||||
|
||||
const overviewFixture = (state: FixtureState) => ({
|
||||
me: { id: state.role === 'head' ? 20 : 21, officerLevel: state.role === 'head' ? 5 : 1 },
|
||||
nation: {
|
||||
id: 1,
|
||||
name: '위',
|
||||
color: '#008000',
|
||||
level: 3,
|
||||
typeCode: 'che_법가',
|
||||
capitalCityId: 1,
|
||||
rate: 20,
|
||||
},
|
||||
chiefStatMin: 65,
|
||||
cities: cities.map((city) => ({
|
||||
...city,
|
||||
officers: {
|
||||
4: state.appointed
|
||||
? { id: 21, name: '장료', npcState: 0, officerLevel: 4, cityId: 1, cityName: '허창' }
|
||||
: null,
|
||||
3: null,
|
||||
2: null,
|
||||
},
|
||||
})),
|
||||
generals: [
|
||||
{
|
||||
id: 1,
|
||||
name: '조조',
|
||||
npcState: 0,
|
||||
officerLevel: 12,
|
||||
cityId: 1,
|
||||
officerCity: 0,
|
||||
stats: { leadership: 90, strength: 80, intelligence: 90 },
|
||||
},
|
||||
{
|
||||
id: 20,
|
||||
name: '순욱',
|
||||
npcState: 0,
|
||||
officerLevel: 5,
|
||||
cityId: 1,
|
||||
officerCity: 0,
|
||||
stats: { leadership: 75, strength: 70, intelligence: 90 },
|
||||
},
|
||||
{
|
||||
id: 21,
|
||||
name: '장료',
|
||||
npcState: 0,
|
||||
officerLevel: state.appointed ? 4 : 1,
|
||||
cityId: 1,
|
||||
officerCity: state.appointed ? 1 : 0,
|
||||
stats: { leadership: 80, strength: 70, intelligence: 50 },
|
||||
},
|
||||
{
|
||||
id: 22,
|
||||
name: '조홍',
|
||||
npcState: 2,
|
||||
officerLevel: 1,
|
||||
cityId: 2,
|
||||
officerCity: 0,
|
||||
stats: { leadership: 60, strength: 65, intelligence: 40 },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const secretGeneral = (id: number, name: string, cityId: number, overrides: Record<string, unknown> = {}) => ({
|
||||
id,
|
||||
name,
|
||||
npcState: 0,
|
||||
injury: 0,
|
||||
stats: { leadership: 70, strength: 70, intelligence: 70 },
|
||||
leadershipBonus: 0,
|
||||
experienceLevel: 9,
|
||||
troopId: 0,
|
||||
troopName: null,
|
||||
gold: 1000,
|
||||
rice: 2000,
|
||||
cityId,
|
||||
cityName: cityId === 1 ? '허창' : '낙양',
|
||||
defenceTrain: 90,
|
||||
defenceTrainText: '☆',
|
||||
crewTypeId: 1,
|
||||
crewTypeName: '보병',
|
||||
crew: 300,
|
||||
train: 90,
|
||||
atmos: 90,
|
||||
killTurn: 7,
|
||||
turnTime: '2026-01-01T01:02:00.000Z',
|
||||
reservedCommands: ['농지 개간', '훈련'],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const secretFixture = () => ({
|
||||
nation: { id: 1, name: '위', color: '#008000', level: 3 },
|
||||
viewer: { generalId: 20, permission: 1 },
|
||||
summary: {
|
||||
gold: 4000,
|
||||
rice: 8000,
|
||||
crew: 1200,
|
||||
generalCount: 4,
|
||||
averageGold: 1000,
|
||||
averageRice: 2000,
|
||||
readiness: {
|
||||
90: { crew: 1200, generals: 4 },
|
||||
80: { crew: 1200, generals: 4 },
|
||||
60: { crew: 1200, generals: 4 },
|
||||
},
|
||||
},
|
||||
generals: [
|
||||
secretGeneral(1, '조조', 1, { leadershipBonus: 6 }),
|
||||
secretGeneral(20, '순욱', 1, { leadershipBonus: 3 }),
|
||||
secretGeneral(21, '장료', 1, {
|
||||
stats: { leadership: 80, strength: 70, intelligence: 50 },
|
||||
}),
|
||||
secretGeneral(22, '조홍', 2, { npcState: 2, reservedCommands: [] }),
|
||||
],
|
||||
});
|
||||
|
||||
const personnelGeneral = (id: number, name: string, officerLevel: number, overrides: Record<string, unknown> = {}) => ({
|
||||
id,
|
||||
name,
|
||||
npcState: 0,
|
||||
officerLevel,
|
||||
cityId: 1,
|
||||
cityName: '허창',
|
||||
troopId: 0,
|
||||
troopName: null,
|
||||
picture: null,
|
||||
imageServer: 0,
|
||||
officerCity: officerLevel >= 2 && officerLevel <= 4 ? 1 : 0,
|
||||
officerCityName: officerLevel >= 2 && officerLevel <= 4 ? '허창' : null,
|
||||
stats: { leadership: 70, strength: 70, intelligence: 70 },
|
||||
experience: 100,
|
||||
dedication: 200,
|
||||
injury: 0,
|
||||
gold: 1000,
|
||||
rice: 1000,
|
||||
crew: 100,
|
||||
personality: null,
|
||||
specialDomestic: null,
|
||||
specialWar: null,
|
||||
belong: 10,
|
||||
permission: 'normal',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const personnelFixture = (state: FixtureState) => {
|
||||
const allGenerals = [
|
||||
personnelGeneral(1, '조조', 12),
|
||||
personnelGeneral(20, '순욱', 5, { stats: { leadership: 75, strength: 70, intelligence: 90 } }),
|
||||
personnelGeneral(21, '장료', state.appointed ? 4 : 1, {
|
||||
stats: { leadership: 80, strength: 70, intelligence: 50 },
|
||||
}),
|
||||
personnelGeneral(22, '조홍', 1, {
|
||||
npcState: 2,
|
||||
cityId: 2,
|
||||
cityName: '낙양',
|
||||
stats: { leadership: 60, strength: 65, intelligence: 40 },
|
||||
}),
|
||||
];
|
||||
const canManage = state.role === 'head';
|
||||
return {
|
||||
me: {
|
||||
id: canManage ? 20 : 21,
|
||||
officerLevel: canManage ? 5 : 1,
|
||||
canManage,
|
||||
canChangePermissions: false,
|
||||
canKick: canManage,
|
||||
},
|
||||
nation: {
|
||||
id: 1,
|
||||
name: '위',
|
||||
color: '#008000',
|
||||
level: 3,
|
||||
typeCode: 'che_법가',
|
||||
capitalCityId: 1,
|
||||
chiefSet: 0,
|
||||
},
|
||||
chiefStatMin: 65,
|
||||
generals: canManage ? allGenerals : [],
|
||||
chiefAssignments: { 12: allGenerals[0], 5: allGenerals[1] },
|
||||
cityAssignments: cities.map((city) => ({
|
||||
id: city.id,
|
||||
name: city.name,
|
||||
level: city.level,
|
||||
region: city.region,
|
||||
officerSet: city.id === 1 && state.appointed ? 1 << 4 : 0,
|
||||
officers: {
|
||||
4: city.id === 1 && state.appointed ? allGenerals[2] : null,
|
||||
3: null,
|
||||
2: null,
|
||||
},
|
||||
})),
|
||||
awards: { tigers: [], eagles: [] },
|
||||
permissionCandidates: { ambassadors: [], auditors: [] },
|
||||
};
|
||||
};
|
||||
|
||||
const install = async (page: Page, state: FixtureState): Promise<void> => {
|
||||
await page.addInitScript((profile) => {
|
||||
localStorage.setItem('sammo-game-token', 'ga_city_office');
|
||||
localStorage.setItem('sammo-game-profile', profile);
|
||||
}, gameProfile);
|
||||
await page.route(gameTrpcRoute, async (route) => {
|
||||
const result = operations(route).map((operation, index) => {
|
||||
if (operation === 'auth.status') return response({ ok: true });
|
||||
if (operation === 'lobby.info') return response({ myGeneral: { id: 20, name: '순욱' } });
|
||||
if (operation === 'join.getConfig') return response({});
|
||||
if (operation === 'nation.getCityOverview') return response(overviewFixture(state));
|
||||
if (operation === 'nation.getSecretGeneralList') {
|
||||
return state.secretForbidden
|
||||
? errorResponse(
|
||||
operation,
|
||||
'권한이 부족합니다. 수뇌부가 아니거나 사관년도가 부족합니다.',
|
||||
'FORBIDDEN'
|
||||
)
|
||||
: response(secretFixture());
|
||||
}
|
||||
if (operation === 'nation.getPersonnelInfo') return response(personnelFixture(state));
|
||||
if (operation === 'nation.appoint') {
|
||||
const input = requestInput(route, index);
|
||||
state.appointmentInputs.push({
|
||||
destGeneralId: Number(input.destGeneralId),
|
||||
destCityId: Number(input.destCityId),
|
||||
officerLevel: Number(input.officerLevel),
|
||||
});
|
||||
state.appointed = true;
|
||||
return response({ ok: true });
|
||||
}
|
||||
return errorResponse(operation, `Unhandled fixture operation: ${operation}`);
|
||||
});
|
||||
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(result) });
|
||||
});
|
||||
};
|
||||
|
||||
test('암행부 행을 도시별로 나누고 수뇌의 인사부 즉시 임명을 반영한다', async ({ page }, testInfo) => {
|
||||
const state: FixtureState = { role: 'head', appointed: false, appointmentInputs: [] };
|
||||
await install(page, state);
|
||||
await page.setViewportSize({ width: 1200, height: 900 });
|
||||
await page.goto('nation/cities');
|
||||
|
||||
await expect(page.locator('.nation-cities-page')).toBeVisible();
|
||||
await expect(page.locator('.city-user-table')).toHaveCount(0);
|
||||
await expect(page.getByRole('button', { name: '인사부 연동' })).toHaveCount(0);
|
||||
|
||||
await page.getByRole('button', { name: '암행부 연동' }).click();
|
||||
await expect(page.locator('.city-user-table')).toHaveCount(2);
|
||||
await expect(page.locator('.city[data-city-id="1"] .city-user-table tr[data-general-id="21"]')).toContainText(
|
||||
'장료'
|
||||
);
|
||||
await expect(page.locator('.city[data-city-id="2"] .city-user-table tr[data-general-id="22"]')).toContainText(
|
||||
'조홍'
|
||||
);
|
||||
await expect(page.locator('.city[data-city-id="2"] .city-user-table tr[data-general-id="21"]')).toHaveCount(0);
|
||||
await expect(page.locator('.city[data-city-id="1"] tr[data-general-id="21"] .command-attention')).toHaveText(
|
||||
'농지 개간'
|
||||
);
|
||||
|
||||
const integratedBox = await page.locator('.city[data-city-id="1"] .city-user-table').evaluate((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const style = getComputedStyle(element);
|
||||
return { width: rect.width, borderCollapse: style.borderCollapse, fontSize: style.fontSize };
|
||||
});
|
||||
expect(integratedBox).toEqual({ width: 941, borderCollapse: 'collapse', fontSize: '14px' });
|
||||
|
||||
await page.getByRole('button', { name: '인사부 연동' }).click();
|
||||
const ordinaryRow = page.locator('.city[data-city-id="1"] tr[data-general-id="21"]');
|
||||
await expect(ordinaryRow.locator('.appointment-button')).toHaveCount(3);
|
||||
await expect(ordinaryRow.locator('.mode-4')).toBeEnabled();
|
||||
await expect(ordinaryRow.locator('.mode-3')).toBeDisabled();
|
||||
await expect(ordinaryRow.locator('.mode-2')).toBeEnabled();
|
||||
await expect(page.locator('tr[data-general-id="1"] .appointment-button')).toHaveCount(0);
|
||||
|
||||
const disabledStyle = await ordinaryRow.locator('.mode-3').evaluate((button) => {
|
||||
const style = getComputedStyle(button);
|
||||
return { borderTopWidth: style.borderTopWidth, backgroundColor: style.backgroundColor };
|
||||
});
|
||||
expect(disabledStyle).toEqual({ borderTopWidth: '0px', backgroundColor: 'rgba(0, 0, 0, 0)' });
|
||||
const appointButton = page.getByRole('button', { name: '장료을(를) 허창 태수로 임명' });
|
||||
await appointButton.hover();
|
||||
expect(await appointButton.evaluate((button) => getComputedStyle(button).cursor)).toBe('pointer');
|
||||
await appointButton.focus();
|
||||
await expect(appointButton).toBeFocused();
|
||||
|
||||
await page.screenshot({ path: testInfo.outputPath('nation-city-integrated-desktop.png'), fullPage: true });
|
||||
await appointButton.click();
|
||||
await expect.poll(() => state.appointmentInputs).toEqual([{ destGeneralId: 21, destCityId: 1, officerLevel: 4 }]);
|
||||
await expect(page.locator('.city[data-city-id="1"] .officer-4-value')).toHaveText('장료');
|
||||
await expect(page.locator('.city[data-city-id="1"] .officer-4-value')).toHaveClass(/effective-officer/u);
|
||||
await expect(page.locator('.city[data-city-id="1"] tr[data-general-id="21"] .mode-4')).toBeDisabled();
|
||||
|
||||
await page.setViewportSize({ width: 500, height: 900 });
|
||||
expect(await page.locator('.nation-cities-page').evaluate((element) => element.getBoundingClientRect().width)).toBe(
|
||||
1000
|
||||
);
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeGreaterThanOrEqual(1000);
|
||||
await page.screenshot({ path: testInfo.outputPath('nation-city-integrated-mobile.png'), fullPage: true });
|
||||
});
|
||||
|
||||
test('수뇌 대상은 재확인하고 일반 장수에게는 임명 버튼을 열지 않는다', async ({ page }) => {
|
||||
const headState: FixtureState = { role: 'head', appointed: false, appointmentInputs: [] };
|
||||
await install(page, headState);
|
||||
await page.goto('nation/cities');
|
||||
await page.getByRole('button', { name: '암행부 연동' }).click();
|
||||
await page.getByRole('button', { name: '인사부 연동' }).click();
|
||||
|
||||
const chiefButton = page.getByRole('button', { name: '순욱을(를) 허창 태수로 임명' });
|
||||
expect(await chiefButton.evaluate((button) => getComputedStyle(button).color)).toBe('rgb(255, 0, 0)');
|
||||
page.once('dialog', async (dialog) => {
|
||||
expect(dialog.message()).toBe('수뇌입니다. 임명할까요?');
|
||||
await dialog.dismiss();
|
||||
});
|
||||
await chiefButton.click();
|
||||
await expect.poll(() => headState.appointmentInputs.length).toBe(0);
|
||||
|
||||
await page.unroute(gameTrpcRoute);
|
||||
const memberState: FixtureState = { role: 'member', appointed: false, appointmentInputs: [] };
|
||||
await install(page, memberState);
|
||||
await page.reload();
|
||||
await page.getByRole('button', { name: '암행부 연동' }).click();
|
||||
page.once('dialog', async (dialog) => {
|
||||
expect(dialog.message()).toBe('수뇌가 아닙니다!');
|
||||
await dialog.accept();
|
||||
});
|
||||
await page.getByRole('button', { name: '인사부 연동' }).click();
|
||||
await expect(page.locator('.appointment-button')).toHaveCount(0);
|
||||
expect(memberState.appointmentInputs).toEqual([]);
|
||||
});
|
||||
|
||||
test('암행부 권한 거부는 도시 기밀 행과 인사부 연동을 열지 않는다', async ({ page }) => {
|
||||
const state: FixtureState = {
|
||||
role: 'member',
|
||||
appointed: false,
|
||||
secretForbidden: true,
|
||||
appointmentInputs: [],
|
||||
};
|
||||
await install(page, state);
|
||||
await page.goto('nation/cities');
|
||||
await page.getByRole('button', { name: '암행부 연동' }).click();
|
||||
|
||||
await expect(page.locator('.integration-error')).toContainText('권한이 부족합니다.');
|
||||
await expect(page.locator('.city-user-table')).toHaveCount(0);
|
||||
await expect(page.getByRole('button', { name: '인사부 연동' })).toHaveCount(0);
|
||||
expect(state.appointmentInputs).toEqual([]);
|
||||
});
|
||||
@@ -21,6 +21,7 @@ export default defineConfig({
|
||||
'troop.spec.ts',
|
||||
'board.spec.ts',
|
||||
'inGameInfo.spec.ts',
|
||||
'nationCityOfficeIntegration.spec.ts',
|
||||
'inGameMenus.spec.ts',
|
||||
'nationOffices.spec.ts',
|
||||
'diplomacy.spec.ts',
|
||||
|
||||
Reference in New Issue
Block a user