fix: 장수 동향 행 간격과 임관 월 표기를 맞춤
출병형 기록을 포함한 메인 동향 행을 21px 리듬으로 고정한다. 세 임관 명령의 전체 기록은 레거시와 같은 월 표제를 사용하고 단위·Chromium 회귀 검증을 추가한다.
This commit is contained in:
@@ -656,17 +656,32 @@ test('메인 카드의 국가·수도·관직·계급·병종은 Ref 출력명
|
|||||||
await expect(page.locator('.main-page')).not.toContainText('che_');
|
await expect(page.locator('.main-page')).not.toContainText('che_');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('메인 개인 기록의 전투 결과는 월 표제와 시각을 한 줄에 표시한다', async ({ page }) => {
|
test('메인 장수 동향과 개인 전투 기록은 모두 21px 행 간격을 유지한다', async ({ page }) => {
|
||||||
const state: FixtureState = {
|
const state: FixtureState = {
|
||||||
permission: 'head',
|
permission: 'head',
|
||||||
myset: 3,
|
myset: 3,
|
||||||
settingMutations: [],
|
settingMutations: [],
|
||||||
accessPages: [],
|
accessPages: [],
|
||||||
recentRecords: {
|
recentRecords: {
|
||||||
global: [],
|
global: [
|
||||||
general: [
|
{
|
||||||
|
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,
|
id: 18609,
|
||||||
|
text: '<C>●</>9월:<Y>뇌동</>의 기병이 퇴각했습니다.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
general: [
|
||||||
|
{
|
||||||
|
id: 18608,
|
||||||
text:
|
text:
|
||||||
'<S>◆</>186년 9월:<div class="small_war_log">' +
|
'<S>◆</>186년 9월:<div class="small_war_log">' +
|
||||||
'<span class="me"><span class="crew_type">귀병</span> ' +
|
'<span class="me"><span class="crew_type">귀병</span> ' +
|
||||||
@@ -686,6 +701,31 @@ test('메인 개인 기록의 전투 결과는 월 표제와 시각을 한 줄
|
|||||||
await page.setViewportSize({ width: 1200, height: 900 });
|
await page.setViewportSize({ width: 1200, height: 900 });
|
||||||
await page.goto('');
|
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 expectedText = '◆186년 9월:귀병 【Administrator】 0(-2209) ← 1361(-5539) 기병 【ⓝ뇌동】 12:54';
|
||||||
const inspect = async (line: Locator) => {
|
const inspect = async (line: Locator) => {
|
||||||
await expect(line).toContainText(expectedText);
|
await expect(line).toContainText(expectedText);
|
||||||
@@ -723,6 +763,11 @@ test('메인 개인 기록의 전투 결과는 월 표제와 시각을 한 줄
|
|||||||
page.locator('.record-zone-mobile [data-record-bucket="general"] .record-line').first()
|
page.locator('.record-zone-mobile [data-record-bucket="general"] .record-line').first()
|
||||||
);
|
);
|
||||||
assertSingleLine(mobileGeometry);
|
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);
|
await persistParityArtifact(page, 'core-main-personal-battle-log-inline-mobile', mobileGeometry);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -666,11 +666,21 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.record-line {
|
.record-line {
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: 21px;
|
||||||
|
margin: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
overflow-wrap: normal;
|
overflow-wrap: normal;
|
||||||
|
line-height: 21px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.record-line :deep(.small_war_log) {
|
||||||
|
height: 21px;
|
||||||
|
line-height: 21px;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
.record-line :deep(.hidden_but_copyable) {
|
.record-line :deep(.hidden_but_copyable) {
|
||||||
color: transparent !important;
|
color: transparent !important;
|
||||||
font-size: 0;
|
font-size: 0;
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ export class ActionDefinition<
|
|||||||
{
|
{
|
||||||
scope: LogScope.SYSTEM,
|
scope: LogScope.SYSTEM,
|
||||||
category: LogCategory.SUMMARY,
|
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>임관</>했습니다.`, {
|
context.addLog(`<Y>${context.general.name}</>${josaYi} <D><b>${destNationName}</b></>에 <S>임관</>했습니다.`, {
|
||||||
scope: LogScope.SYSTEM,
|
scope: LogScope.SYSTEM,
|
||||||
category: LogCategory.SUMMARY,
|
category: LogCategory.SUMMARY,
|
||||||
format: LogFormat.RAWTEXT,
|
format: LogFormat.MONTH,
|
||||||
});
|
});
|
||||||
|
|
||||||
tryApplyUniqueLottery(context, {
|
tryApplyUniqueLottery(context, {
|
||||||
|
|||||||
@@ -183,6 +183,7 @@ export class ActionDefinition<
|
|||||||
context.addLog(`<Y>${general.name}</>${josaYi} <D><b>${destNation.name}</b></>에 <S>임관</>했습니다.`, {
|
context.addLog(`<Y>${general.name}</>${josaYi} <D><b>${destNation.name}</b></>에 <S>임관</>했습니다.`, {
|
||||||
scope: LogScope.SYSTEM,
|
scope: LogScope.SYSTEM,
|
||||||
category: LogCategory.SUMMARY,
|
category: LogCategory.SUMMARY,
|
||||||
|
format: LogFormat.MONTH,
|
||||||
});
|
});
|
||||||
|
|
||||||
const initialNationGenLimit = context.initialNationGenLimit ?? 10;
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user