fix: 장수 동향 행 간격과 임관 월 표기를 맞춤

출병형 기록을 포함한 메인 동향 행을 21px 리듬으로 고정한다. 세 임관 명령의 전체 기록은 레거시와 같은 월 표제를 사용하고 단위·Chromium 회귀 검증을 추가한다.
This commit is contained in:
2026-08-15 18:13:03 +00:00
parent d3ebbb2a47
commit 4c221a1531
6 changed files with 181 additions and 5 deletions
+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);
});
+10
View File
@@ -666,11 +666,21 @@ button {
}
.record-line {
box-sizing: border-box;
height: 21px;
margin: 0;
overflow: hidden;
overflow-wrap: normal;
line-height: 21px;
white-space: nowrap;
}
.record-line :deep(.small_war_log) {
height: 21px;
line-height: 21px;
vertical-align: top;
}
.record-line :deep(.hidden_but_copyable) {
color: transparent !important;
font-size: 0;
@@ -272,7 +272,7 @@ export class ActionDefinition<
{
scope: LogScope.SYSTEM,
category: LogCategory.SUMMARY,
format: LogFormat.PLAIN,
format: LogFormat.MONTH,
}
)
);
@@ -104,7 +104,7 @@ export class ActionDefinition<
context.addLog(`<Y>${context.general.name}</>${josaYi} <D><b>${destNationName}</b></>에 <S>임관</>했습니다.`, {
scope: LogScope.SYSTEM,
category: LogCategory.SUMMARY,
format: LogFormat.RAWTEXT,
format: LogFormat.MONTH,
});
tryApplyUniqueLottery(context, {
@@ -183,6 +183,7 @@ export class ActionDefinition<
context.addLog(`<Y>${general.name}</>${josaYi} <D><b>${destNation.name}</b></>에 <S>임관</>했습니다.`, {
scope: LogScope.SYSTEM,
category: LogCategory.SUMMARY,
format: LogFormat.MONTH,
});
const initialNationGenLimit = context.initialNationGenLimit ?? 10;
@@ -0,0 +1,120 @@
import { describe, expect, it } from 'vitest';
import type { General, Nation } from '../../../src/domain/entities.js';
import type { GeneralActionResolveContext } from '../../../src/actions/engine.js';
import { finalizeLogEntry } from '../../../src/logging/entries.js';
import { LogCategory, type LogEntryDraft, LogFormat, LogScope } from '../../../src/logging/types.js';
import type { TurnCommandEnv } from '../../../src/actions/turn/commandEnv.js';
import { ActionDefinition as AppointmentAction } from '../../../src/actions/turn/general/che_임관.js';
import { ActionDefinition as RandomAppointmentAction } from '../../../src/actions/turn/general/che_랜덤임관.js';
import { ActionDefinition as FollowAppointmentAction } from '../../../src/actions/turn/general/che_장수대상임관.js';
const actor = {
id: 1,
name: '검증장수',
nationId: 0,
cityId: 1,
npcState: 0,
officerLevel: 1,
experience: 100,
stats: { leadership: 50, strength: 50, intelligence: 50 },
role: {
personality: null,
specialDomestic: null,
specialWar: null,
items: { horse: null, weapon: null, book: null, item: null },
},
meta: {},
} as General;
const nation = {
id: 2,
name: '검증국',
capitalCityId: 2,
meta: { gennum: 1 },
} as unknown as Nation;
const createLogSink =
(logs: LogEntryDraft[]): GeneralActionResolveContext['addLog'] =>
(text, options = {}) => {
logs.push({
scope: options.scope ?? LogScope.GENERAL,
category: options.category ?? LogCategory.ACTION,
text,
format: options.format ?? LogFormat.MONTH,
});
};
const expectMonthlySummary = (logs: LogEntryDraft[]): void => {
const summary = logs.find((entry) => entry.scope === LogScope.SYSTEM && entry.category === LogCategory.SUMMARY);
expect(summary).toBeDefined();
expect(summary?.format).toBe(LogFormat.MONTH);
expect(finalizeLogEntry(summary!, { year: 186, month: 9 })?.text).toMatch(/^<C><\/>9:/u);
};
describe('appointment global summary log format', () => {
it('adds the legacy month prefix to direct appointment', () => {
const logs: LogEntryDraft[] = [];
const action = new AppointmentAction({} as TurnCommandEnv);
action.resolve(
{
general: actor,
destNation: nation,
destNationGeneralCount: 1,
destCityId: 2,
addLog: createLogSink(logs),
} as unknown as Parameters<typeof action.resolve>[0],
{ destNationId: nation.id }
);
expectMonthlySummary(logs);
});
it('adds the legacy month prefix to random appointment', () => {
const action = new RandomAppointmentAction({} as TurnCommandEnv);
const result = action.resolve(
{
general: actor,
rng: {
nextFloat1: () => 0,
nextInt: () => 0,
},
candidateNations: [
{
nation,
generals: [],
generalCount: 1,
monarchCityId: 2,
monarchAffinity: 0,
},
],
relYear: 1,
initialNationGenLimit: 10,
historicalNpcAffinityMode: false,
} as unknown as Parameters<typeof action.resolve>[0],
{}
);
const logs = result.effects.flatMap((effect) => (effect.type === 'log' ? [effect.entry] : []));
expectMonthlySummary(logs);
});
it('keeps the same month prefix when following another general', () => {
const logs: LogEntryDraft[] = [];
const action = new FollowAppointmentAction();
action.resolve(
{
general: actor,
destNation: nation,
destNationGeneralCount: 1,
initialNationGenLimit: 10,
addLog: createLogSink(logs),
} as unknown as Parameters<typeof action.resolve>[0],
{ destGeneralID: 9 }
);
expectMonthlySummary(logs);
});
});