merge: 최신 main을 능력치 버튼 눌림 수정에 통합

This commit is contained in:
2026-08-15 18:19:57 +00:00
12 changed files with 528 additions and 46 deletions
+91 -5
View File
@@ -160,7 +160,8 @@ const install = async (
mode: 'member' | 'wanderer' | 'admin' = 'member',
trade: number | null = 100,
globalNationCount = 2,
mapFixture = map
mapFixture = map,
denseCurrentCity = false
) => {
await page.addInitScript((profile) => {
localStorage.setItem('sammo-game-token', 'ga_info');
@@ -372,8 +373,32 @@ const install = async (
crew: 500,
train: 90,
atmos: 90,
turns: ['징병'],
turns: denseCurrentCity ? ['징병', '훈련'] : ['징병'],
},
...(denseCurrentCity
? Array.from({ length: 12 }, (_, index) => ({
id: index + 2,
name: `NPC장수이름이긴${index + 1}`,
npcState: 2,
picture: null,
imageServer: 0,
nationId: 1,
nationName: '아국',
leadership: 60,
strength: 60,
intelligence: 60,
injury: 0,
officerLevel: 1,
leadershipBonus: 0,
defenceTrain: 80,
crewTypeId: 1,
crewTypeName: '보병',
crew: 500,
train: 90,
atmos: 90,
turns: [],
}))
: []),
],
forceSummary: {
enemyCrew: 0,
@@ -407,7 +432,7 @@ test('four legacy menu pages keep the 1000px desktop table contract', async ({ p
['nation/info', '.legacy-info-page', '14px', 'Pretendard', 'collapse'],
['nation/cities', '.nation-cities-page', '14px', 'Pretendard', 'collapse'],
['global-info', '.global-page', '14px', 'Pretendard', 'collapse'],
['current-city', '.city-page', '16px', 'Times New Roman', 'separate'],
['current-city', '.city-page', '14px', 'Pretendard', 'separate'],
] as const) {
await go(page, path);
await expect(page.locator(selector)).toBeVisible();
@@ -774,8 +799,8 @@ test('current-city exposes own general details to a member and admin fixture', a
};
});
expect(legacyGeometry.selector).toMatchObject({ width: 400, height: 19 });
expect(legacyGeometry.stats).toEqual({ x: 100, y: 178, width: 1000, height: 136 });
expect(legacyGeometry.generals).toMatchObject({ x: 88, y: 332, width: 1024 });
expect(legacyGeometry.stats).toEqual({ x: 100, y: 165.375, width: 1000, height: 106.9375 });
expect(legacyGeometry.generals).toMatchObject({ x: 88, y: 290.3125, width: 1024 });
expect(legacyGeometry.titleAlign).toBe('start');
expect(legacyGeometry.icon).toMatchObject({ width: 64, height: 64, naturalWidth: 64, naturalHeight: 64 });
if (artifactRoot) {
@@ -834,6 +859,67 @@ test('current-city exposes own general details to a member and admin fixture', a
}
});
test('current-city wraps dense general names and only shrinks reserved turns', async ({ page }) => {
await install(page, 'member', 100, 2, map, true);
for (const viewport of [
{ name: 'desktop', width: 1200, height: 900 },
{ name: 'mobile', width: 500, height: 900 },
]) {
await page.setViewportSize({ width: viewport.width, height: viewport.height });
await go(page, 'current-city');
const pageStyle = await page.locator('.city-page').evaluate((element) => {
const style = getComputedStyle(element);
return { fontFamily: style.fontFamily, fontSize: style.fontSize };
});
expect(pageStyle.fontFamily).toContain('Pretendard');
expect(pageStyle.fontSize).toBe('14px');
const names = page.locator('.general-names');
await expect(names).toHaveCSS('white-space', 'normal');
const nameLineCount = await names.locator('span').evaluateAll((elements) => {
const tops = elements.map((element) => Math.round(element.getBoundingClientRect().top));
return new Set(tops).size;
});
expect(nameLineCount).toBeGreaterThan(1);
const rows = page.locator('.generals tbody tr');
const reservedTurns = rows.nth(0).locator('.turns');
const npcTurns = rows.nth(1).locator('.turns');
await expect(reservedTurns).toContainText('1 : 징병');
await expect(reservedTurns).toContainText('2 : 훈련');
await expect(reservedTurns).toHaveClass(/turns--reserved/);
await expect(npcTurns).toHaveText('NPC 장수');
await expect(npcTurns).not.toHaveClass(/turns--reserved/);
const turnFontSizes = await Promise.all([
reservedTurns.evaluate((element) => getComputedStyle(element).fontSize),
npcTurns.evaluate((element) => getComputedStyle(element).fontSize),
]);
expect(Number.parseFloat(turnFontSizes[0])).toBeLessThan(Number.parseFloat(pageStyle.fontSize));
expect(turnFontSizes[1]).toBe(pageStyle.fontSize);
const reservedLineTops = await reservedTurns
.locator('.turn-line')
.evaluateAll((elements) => elements.map((element) => Math.round(element.getBoundingClientRect().top)));
expect(new Set(reservedLineTops).size).toBe(2);
if (artifactRoot) {
await mkdir(artifactRoot, { recursive: true });
await writeFile(
resolve(artifactRoot, `core-current-city-dense-${viewport.name}.json`),
`${JSON.stringify({ pageStyle, nameLineCount, turnFontSizes, reservedLineTops }, null, 2)}\n`,
'utf8'
);
await page.screenshot({
path: resolve(artifactRoot, `core-current-city-dense-${viewport.name}.png`),
fullPage: true,
animations: 'disabled',
});
}
}
});
test('current-city renders a missing merchant rate with the legacy dash and percent text', async ({ page }) => {
await install(page, 'member', null);
await page.setViewportSize({ width: 1200, height: 900 });
+48 -3
View File
@@ -656,17 +656,32 @@ test('메인 카드의 국가·수도·관직·계급·병종은 Ref 출력명
await expect(page.locator('.main-page')).not.toContainText('che_');
});
test('메인 개인 기록의 전투 결과는 월 표제와 시각을 한 줄에 표시한다', async ({ page }) => {
test('메인 장수 동향과 개인 전투 기록은 모두 21px 행 간격을 유지한다', async ({ page }) => {
const state: FixtureState = {
permission: 'head',
myset: 3,
settingMutations: [],
accessPages: [],
recentRecords: {
global: [],
general: [
global: [
{
id: 18611,
text:
'<C>●</>9월:<D><b>위</b></>의 <Y>Administrator</>가 <G><b>낙양</b></>으로 ' +
'진격합니다.<span class="hidden_but_copyable">(전투시드: 0123456789abcdef)</span>',
},
{
id: 18610,
text: '<C>●</>9월:<Y>Administrator</>가 <D><b>위</b></>에 <S>임관</>했습니다.',
},
{
id: 18609,
text: '<C>●</>9월:<Y>뇌동</>의 기병이 퇴각했습니다.',
},
],
general: [
{
id: 18608,
text:
'<S>◆</>186년 9월:<div class="small_war_log">' +
'<span class="me"><span class="crew_type">귀병</span> ' +
@@ -686,6 +701,31 @@ test('메인 개인 기록의 전투 결과는 월 표제와 시각을 한 줄
await page.setViewportSize({ width: 1200, height: 900 });
await page.goto('');
const inspectGlobalRhythm = async (selector: string) => {
const lines = page.locator(selector);
await expect(lines).toHaveCount(3);
return lines.evaluateAll((elements) =>
elements.map((element) => {
const rect = element.getBoundingClientRect();
return {
top: rect.top,
height: rect.height,
lineHeight: getComputedStyle(element).lineHeight,
};
})
);
};
const assertUniformGlobalRhythm = (geometry: Awaited<ReturnType<typeof inspectGlobalRhythm>>) => {
expect(geometry.map((line) => line.height)).toEqual([21, 21, 21]);
expect(geometry.map((line) => line.lineHeight)).toEqual(['21px', '21px', '21px']);
expect(geometry[1]!.top - geometry[0]!.top).toBe(21);
expect(geometry[2]!.top - geometry[1]!.top).toBe(21);
};
const desktopGlobalGeometry = await inspectGlobalRhythm('.record-zone [data-record-bucket="global"] .record-line');
assertUniformGlobalRhythm(desktopGlobalGeometry);
await persistParityArtifact(page, 'core-main-trend-log-rhythm-desktop', desktopGlobalGeometry);
const expectedText = '◆186년 9월:귀병 【Administrator】 0(-2209) ← 1361(-5539) 기병 【ⓝ뇌동】 12:54';
const inspect = async (line: Locator) => {
await expect(line).toContainText(expectedText);
@@ -723,6 +763,11 @@ test('메인 개인 기록의 전투 결과는 월 표제와 시각을 한 줄
page.locator('.record-zone-mobile [data-record-bucket="general"] .record-line').first()
);
assertSingleLine(mobileGeometry);
const mobileGlobalGeometry = await inspectGlobalRhythm(
'.record-zone-mobile [data-record-bucket="global"] .record-line'
);
assertUniformGlobalRhythm(mobileGlobalGeometry);
await persistParityArtifact(page, 'core-main-trend-log-rhythm-mobile', mobileGlobalGeometry);
await persistParityArtifact(page, 'core-main-personal-battle-log-inline-mobile', mobileGeometry);
});
+99 -27
View File
@@ -32,6 +32,7 @@ type NavigationFixture = {
forceSnapshotCalls?: number;
refreshDelayMs?: number;
largeCommandTable?: boolean;
refCommandCategories?: boolean;
currentYear?: number;
currentMonth?: number;
scenarioTitle?: string;
@@ -108,32 +109,48 @@ const emitReadModelInvalidation = (page: Page, invalidation: ReturnType<typeof r
);
}, invalidation);
const commandTableFixture = (large: boolean, blockedCount = 0) => ({
general: 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,
},
],
};
}),
}))
: [],
const refCommandCategoryFixture = ['개인', '내정', '군사', '인사', '계략', '국가'].map((category, index) => ({
category,
values: [
{
key: `ref-command-${index}`,
name: category === '계략' ? '화계' : `${category} 명령`,
reqArg: false,
possible: true,
status: 'available' as const,
inputFields: [],
},
],
}));
const commandTableFixture = (large: boolean, blockedCount = 0, refCategories = false) => ({
general: 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,
},
],
};
}),
}))
: [],
nation: [],
inputOptions: {
cities: Array.from({ length: 20 }, (_, index) => ({ value: index + 1, label: `도시 ${index + 1}` })),
@@ -371,7 +388,11 @@ const installFixture = async (page: Page, state: NavigationFixture) => {
? {
kind: 'snapshot' as const,
revision: currentCommandTableRevision,
data: commandTableFixture(state.largeCommandTable === true, state.commandBlockedCount),
data: commandTableFixture(
state.largeCommandTable === true,
state.commandBlockedCount,
state.refCommandCategories === true
),
}
: input.known.commandTable === currentCommandTableRevision
? { kind: 'unchanged' as const, revision: currentCommandTableRevision }
@@ -953,6 +974,57 @@ test('pure NPC message senders are not rendered as reply targets', async ({ page
await persistArtifact(page, `${basePath.slice(1)}-npc-reply-targets-desktop-1200`);
});
test('main reserved-turn picker renders the Ref general category order', async ({ page }) => {
const state: NavigationFixture = {
officerLevel: 1,
permission: 0,
nationLevel: 1,
stage: 0,
npcMode: 1,
generalMeCalls: 0,
operations: [],
refCommandCategories: true,
reservedTurns: Array.from({ length: 30 }, (_, index) => ({ index, action: '휴식', args: {} })),
};
await installFixture(page, state);
await page.setViewportSize({ width: 1200, height: 900 });
await waitForMain(page);
await page.getByRole('button', { name: '1턴 명령 입력', exact: true }).click();
const picker = page.getByTestId('command-picker');
const categoryButtons = picker.locator('.category-btn');
await expect(categoryButtons).toHaveText(['개인', '내정', '군사', '인사', '계략', '국가']);
const desktopGeometry = await picker.evaluate((element) => {
const categories = element.querySelector<HTMLElement>('.category-list');
if (!categories) throw new Error('command category list is missing');
const buttons = [...categories.querySelectorAll<HTMLElement>('.category-btn')];
return {
columns: getComputedStyle(categories).gridTemplateColumns,
rows: new Set(buttons.map((button) => button.getBoundingClientRect().y)).size,
horizontalOverflow: element.scrollWidth - element.clientWidth,
};
});
expect(desktopGeometry.columns.split(' ')).toHaveLength(3);
expect(desktopGeometry.rows).toBe(2);
expect(desktopGeometry.horizontalOverflow).toBeLessThanOrEqual(0);
const strategyCategory = picker.getByRole('button', { name: '계략', exact: true });
await strategyCategory.hover();
await strategyCategory.focus();
await expect(strategyCategory).toBeFocused();
await strategyCategory.click();
await expect(strategyCategory).toHaveClass(/active/);
await expect(picker.locator('.command-item')).toHaveText(['화계']);
await page.setViewportSize({ width: 500, height: 900 });
await page.getByRole('button', { name: '1턴 명령 입력', exact: true }).click();
const mobilePicker = page.getByTestId('command-picker');
await expect(mobilePicker.locator('.category-btn')).toHaveText(['개인', '내정', '군사', '인사', '계략', '국가']);
expect(await mobilePicker.evaluate((element) => element.scrollWidth - element.clientWidth)).toBeLessThanOrEqual(0);
await persistArtifact(page, `${basePath.slice(1)}-main-reserved-ref-categories-mobile-500`);
});
test('main cards and command input stay inside their Ref-sized grid slots', async ({ page }) => {
const state: NavigationFixture = {
officerLevel: 1,