fix: 턴 실패 사유를 실행 장수의 개인 기록에 남김
개인턴과 수뇌턴 공통 로그 생성 경로에서 generalId를 필수로 전달해 PostgreSQL 영속화 단계의 누락을 막는다. 비교 도구의 소유자 추론을 제거하고 단위, PostgreSQL, Chromium 회귀 검증을 추가한다.
This commit is contained in:
@@ -156,15 +156,15 @@ export const applyLegacyGeneralProgression = (
|
||||
meta.explevel = expLevel;
|
||||
if (expLevel !== previousExpLevel && actionResolvedExpLevel !== expLevel) {
|
||||
const josaRo = JosaUtil.pick(String(expLevel), '로');
|
||||
logs.push({
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.PLAIN,
|
||||
text:
|
||||
logs.push(
|
||||
createGeneralActionLog(
|
||||
general.id,
|
||||
expLevel > previousExpLevel
|
||||
? `<C>Lv ${expLevel}</>${josaRo} <C>레벨업</>!`
|
||||
: `<C>Lv ${expLevel}</>${josaRo} <R>레벨다운</>!`,
|
||||
});
|
||||
{ format: LogFormat.PLAIN }
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!preserveLevel && (forceRefreshLevel || general.dedication !== previousGeneral.dedication)) {
|
||||
@@ -176,15 +176,15 @@ export const applyLegacyGeneralProgression = (
|
||||
const billText = getBillByLevel(dedicationLevel).toLocaleString('en-US');
|
||||
const josaRoDedication = JosaUtil.pick(dedicationLevelText, '로');
|
||||
const josaRoBill = JosaUtil.pick(billText, '로');
|
||||
logs.push({
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.PLAIN,
|
||||
text:
|
||||
logs.push(
|
||||
createGeneralActionLog(
|
||||
general.id,
|
||||
dedicationLevel > previousDedicationLevel
|
||||
? `<Y>${dedicationLevelText}</>${josaRoDedication} <C>승급</>하여 봉록이 <C>${billText}</>${josaRoBill} <C>상승</>했습니다!`
|
||||
: `<Y>${dedicationLevelText}</>${josaRoDedication} <R>강등</>되어 봉록이 <C>${billText}</>${josaRoBill} <R>하락</>했습니다!`,
|
||||
});
|
||||
{ format: LogFormat.PLAIN }
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -715,12 +715,27 @@ const buildConstraintContext = (
|
||||
mode: 'full',
|
||||
});
|
||||
|
||||
const createActionLog = (message: string, meta?: Record<string, unknown>): LogEntryDraft => ({
|
||||
/**
|
||||
* Ref ActionLogger is constructed with a general ID, so every personal action
|
||||
* log carries its owner before it reaches persistence. Keep that ownership
|
||||
* explicit here: finalizeLogEntry intentionally rejects ownerless GENERAL logs.
|
||||
*/
|
||||
interface GeneralActionLogOptions {
|
||||
format?: LogFormat;
|
||||
meta?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const createGeneralActionLog = (
|
||||
generalId: number,
|
||||
message: string,
|
||||
options: GeneralActionLogOptions = {}
|
||||
): LogEntryDraft => ({
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.MONTH,
|
||||
generalId,
|
||||
format: options.format ?? LogFormat.MONTH,
|
||||
text: message,
|
||||
meta,
|
||||
...(options.meta ? { meta: options.meta } : {}),
|
||||
});
|
||||
|
||||
const resolveDefinition = (
|
||||
@@ -936,7 +951,7 @@ export const createReservedTurnHandler = async (options: {
|
||||
actionKey = definition.key;
|
||||
usedFallback = true;
|
||||
blockedReason = failureText;
|
||||
logs.push(createActionLog(failureText));
|
||||
logs.push(createGeneralActionLog(currentGeneral.id, failureText));
|
||||
}
|
||||
|
||||
const actionConstraintEnv = {
|
||||
@@ -972,7 +987,7 @@ export const createReservedTurnHandler = async (options: {
|
||||
const failureText =
|
||||
failedDefinition.formatConstraintFailure?.(reason, constraintCtx, failedActionArgs, view) ??
|
||||
`${reason} ${failedDefinition.name} 실패.`;
|
||||
logs.push(createActionLog(failureText, meta));
|
||||
logs.push(createGeneralActionLog(currentGeneral.id, failureText, meta ? { meta } : {}));
|
||||
}
|
||||
if (!usedFallback && (kind === 'general' || currentNation)) {
|
||||
const currentYearMonth = joinYearMonth(context.world.currentYear, context.world.currentMonth);
|
||||
@@ -987,7 +1002,7 @@ export const createReservedTurnHandler = async (options: {
|
||||
actionKey = definition.key;
|
||||
usedFallback = true;
|
||||
blockedReason = `${remainTurn}턴 더 기다려야 합니다`;
|
||||
logs.push(createActionLog(blockedReason));
|
||||
logs.push(createGeneralActionLog(currentGeneral.id, blockedReason));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1068,7 +1083,7 @@ export const createReservedTurnHandler = async (options: {
|
||||
actionKey = definition.key;
|
||||
usedFallback = true;
|
||||
blockedReason = '예약된 명령을 실행하지 못했습니다.';
|
||||
logs.push(createActionLog('예약된 명령을 실행하지 못했습니다.'));
|
||||
logs.push(createGeneralActionLog(currentGeneral.id, '예약된 명령을 실행하지 못했습니다.'));
|
||||
actionRng = sharedActionRng ?? buildRng(actionKey);
|
||||
baseContext = {
|
||||
general: currentGeneral,
|
||||
@@ -1151,7 +1166,7 @@ export const createReservedTurnHandler = async (options: {
|
||||
const progressText =
|
||||
executionDefinition.getProgressText?.(actionContext, actionArgs, nextTerm, termMax) ??
|
||||
`${definition.name} 수행중... (${nextTerm}/${termMax})`;
|
||||
logs.push(createActionLog(progressText));
|
||||
logs.push(createGeneralActionLog(currentGeneral.id, progressText));
|
||||
return { actionKey, usedFallback, completed: false, blockedReason };
|
||||
}
|
||||
}
|
||||
@@ -1576,7 +1591,7 @@ export const createReservedTurnHandler = async (options: {
|
||||
},
|
||||
rng: preprocessRng,
|
||||
log: {
|
||||
push: (message) => logs.push(createActionLog(message)),
|
||||
push: (message) => logs.push(createGeneralActionLog(currentGeneral.id, message)),
|
||||
},
|
||||
});
|
||||
preTurnPipeline.getPreTurnExecuteTriggerList(preTurnContext).fire(preTurnContext, baseConstraintEnv);
|
||||
@@ -1602,7 +1617,12 @@ export const createReservedTurnHandler = async (options: {
|
||||
}
|
||||
currentGeneral.crew = 0;
|
||||
currentGeneral.rice = 0;
|
||||
logs.push(createActionLog('군량이 모자라 병사들이 <R>소집해제</>되었습니다!'));
|
||||
logs.push(
|
||||
createGeneralActionLog(
|
||||
currentGeneral.id,
|
||||
'군량이 모자라 병사들이 <R>소집해제</>되었습니다!'
|
||||
)
|
||||
);
|
||||
preTurnContext.skill.activate('pre.소집해제');
|
||||
}
|
||||
preTurnContext.skill.activate('pre.병력군량소모');
|
||||
@@ -1625,7 +1645,8 @@ export const createReservedTurnHandler = async (options: {
|
||||
if (isBlocked) {
|
||||
currentGeneral.meta.killturn = Math.max(0, currentGeneral.meta.killturn - 1);
|
||||
logs.push(
|
||||
createActionLog(
|
||||
createGeneralActionLog(
|
||||
currentGeneral.id,
|
||||
blockCode === 2
|
||||
? '현재 멀티, 또는 비매너로 인한<R>블럭</> 대상자입니다.'
|
||||
: '현재 악성유저로 분류되어 <R>블럭</> 대상자입니다.'
|
||||
@@ -1981,7 +2002,8 @@ export const createReservedTurnHandler = async (options: {
|
||||
? currentGeneral.meta.owner_name
|
||||
: currentGeneral.userId;
|
||||
logs.push(
|
||||
createActionLog(
|
||||
createGeneralActionLog(
|
||||
currentGeneral.id,
|
||||
`${ownerName ?? '사용자'}이 <Y>${currentGeneral.name}</>의 육체에서 <S>유체이탈</>합니다!`
|
||||
)
|
||||
);
|
||||
@@ -2060,7 +2082,8 @@ export const createReservedTurnHandler = async (options: {
|
||||
chiefGeneralId: successor.id,
|
||||
};
|
||||
logs.push(
|
||||
createActionLog(
|
||||
createGeneralActionLog(
|
||||
currentGeneral.id,
|
||||
`<Y>${successor.name}</>이 <D><b>${currentNation.name}</b></>의 유지를 이어 받았습니다`
|
||||
)
|
||||
);
|
||||
@@ -2093,7 +2116,12 @@ export const createReservedTurnHandler = async (options: {
|
||||
if (!deleteGeneral && currentGeneral.age >= retirementYear && currentGeneral.npcState === 0) {
|
||||
currentGeneral = resetRetiredGeneral(currentGeneral);
|
||||
lifecycleOutcome = 'retired';
|
||||
logs.push(createActionLog('나이가 들어 <R>은퇴</>하고 자손에게 자리를 물려줍니다.'));
|
||||
logs.push(
|
||||
createGeneralActionLog(
|
||||
currentGeneral.id,
|
||||
'나이가 들어 <R>은퇴</>하고 자손에게 자리를 물려줍니다.'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
currentGeneral = {
|
||||
@@ -2246,8 +2274,7 @@ export const createImmediateGeneralActionExecutor = async (options: {
|
||||
`${reason} ${definition.name} 실패.`;
|
||||
if (input.actionKey === 'che_접경귀환' || input.actionKey === 'che_등용수락') {
|
||||
options.world.pushLog({
|
||||
...createActionLog(failureText),
|
||||
generalId: general.id,
|
||||
...createGeneralActionLog(general.id, failureText),
|
||||
});
|
||||
}
|
||||
return { ok: false, reason: failureText };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import type { TurnSchedule } from '@sammo-ts/logic';
|
||||
import { finalizeLogEntry, type TurnSchedule } from '@sammo-ts/logic';
|
||||
import type { TurnGeneral, TurnWorldSnapshot, TurnWorldState } from '../src/turn/types.js';
|
||||
import { InMemoryTurnWorld } from '../src/turn/inMemoryWorld.js';
|
||||
import { InMemoryReservedTurnStore } from '../src/turn/reservedTurnStore.js';
|
||||
@@ -41,8 +41,9 @@ const mockDate = new Date('0189-01-01T00:00:00Z');
|
||||
|
||||
// We need a mock Prisma client that satisfies the shape required by InMemoryReservedTurnStore
|
||||
// It expects { generalTurn: { findMany, deleteMany, createMany }, nationTurn: { ... } }
|
||||
const createMockPrisma = (initialGeneralRows: any[] = []) => {
|
||||
const createMockPrisma = (initialGeneralRows: any[] = [], initialNationRows: any[] = []) => {
|
||||
let generalRows = [...initialGeneralRows];
|
||||
let nationRows = [...initialNationRows];
|
||||
return {
|
||||
generalTurn: {
|
||||
findMany: vi.fn(async ({ where } = {}) => {
|
||||
@@ -67,9 +68,28 @@ const createMockPrisma = (initialGeneralRows: any[] = []) => {
|
||||
}),
|
||||
},
|
||||
nationTurn: {
|
||||
findMany: vi.fn(async () => []),
|
||||
deleteMany: vi.fn(async () => ({ count: 0 })),
|
||||
createMany: vi.fn(async () => ({ count: 0 })),
|
||||
findMany: vi.fn(async ({ where } = {}) => {
|
||||
if (where?.nationId && where?.officerLevel) {
|
||||
return nationRows
|
||||
.filter((row) => row.nationId === where.nationId && row.officerLevel === where.officerLevel)
|
||||
.sort((left, right) => left.turnIdx - right.turnIdx);
|
||||
}
|
||||
return nationRows;
|
||||
}),
|
||||
deleteMany: vi.fn(async ({ where } = {}) => {
|
||||
if (where?.nationId && where?.officerLevel) {
|
||||
nationRows = nationRows.filter(
|
||||
(row) => row.nationId !== where.nationId || row.officerLevel !== where.officerLevel
|
||||
);
|
||||
}
|
||||
return { count: 0 };
|
||||
}),
|
||||
createMany: vi.fn(async ({ data }) => {
|
||||
if (Array.isArray(data)) {
|
||||
nationRows.push(...data);
|
||||
}
|
||||
return { count: data.length };
|
||||
}),
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -423,8 +443,17 @@ describe('Reserved Turn Execution Integration', () => {
|
||||
};
|
||||
|
||||
const invalidRows = [{ generalId: 1, turnIdx: 0, actionCode: 'che_이동', arg: { destCityId: 'bad' } }];
|
||||
const invalidNationRows = [
|
||||
{
|
||||
nationId: 1,
|
||||
officerLevel: 5,
|
||||
turnIdx: 0,
|
||||
actionCode: 'che_천도',
|
||||
arg: { destCityId: 'bad' },
|
||||
},
|
||||
];
|
||||
|
||||
const mockPrisma = createMockPrisma(invalidRows);
|
||||
const mockPrisma = createMockPrisma(invalidRows, invalidNationRows);
|
||||
const reservedTurnStore = new InMemoryReservedTurnStore(mockPrisma as any, {
|
||||
maxGeneralTurns: 10,
|
||||
maxNationTurns: 10,
|
||||
@@ -456,7 +485,26 @@ describe('Reserved Turn Execution Integration', () => {
|
||||
|
||||
const dirty = world.consumeDirtyState();
|
||||
expect(world.getGeneralById(1)!.cityId).toBe(1);
|
||||
expect(dirty.logs.some((log) => log.text.includes('인자가 올바르지 않습니다. 이동 실패.'))).toBe(true);
|
||||
expect(dirty.logs.find((log) => log.text.includes('인자가 올바르지 않습니다. 천도 실패.'))).toMatchObject({
|
||||
scope: 'GENERAL',
|
||||
category: 'ACTION',
|
||||
generalId: 1,
|
||||
});
|
||||
expect(dirty.logs.find((log) => log.text.includes('인자가 올바르지 않습니다. 이동 실패.'))).toMatchObject({
|
||||
scope: 'GENERAL',
|
||||
category: 'ACTION',
|
||||
generalId: 1,
|
||||
});
|
||||
const personalActionLogs = dirty.logs.filter(
|
||||
(log) => log.scope === 'GENERAL' && log.category === 'ACTION'
|
||||
);
|
||||
expect(personalActionLogs.length).toBeGreaterThan(0);
|
||||
expect(personalActionLogs.every((log) => log.generalId === 1)).toBe(true);
|
||||
expect(
|
||||
personalActionLogs.map((log) =>
|
||||
finalizeLogEntry(log, { year: invalidState.currentYear, month: invalidState.currentMonth })
|
||||
)
|
||||
).not.toContain(null);
|
||||
expect(dirty.logs.some((log) => log.text.includes('아무것도 실행하지 않았습니다.'))).toBe(true);
|
||||
});
|
||||
|
||||
@@ -620,6 +668,7 @@ describe('Reserved Turn Execution Integration', () => {
|
||||
const denyLog = dirty.logs.find((log) => log.text.includes('같은 도시입니다.'));
|
||||
expect(denyLog?.text).toContain('이동 실패.');
|
||||
expect(denyLog?.meta?.constraintName).toBe('notSameDestCity');
|
||||
expect(denyLog?.generalId).toBe(1);
|
||||
expect(dirty.logs.some((log) => log.text.includes('아무것도 실행하지 않았습니다.'))).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
import { createGamePostgresConnector, type GamePrismaClient, type InputJsonValue } from '@sammo-ts/infra';
|
||||
import { LogCategory, LogFormat, LogScope, type TurnSchedule } from '@sammo-ts/logic';
|
||||
|
||||
import { createDatabaseTurnHooks, type DatabaseTurnHooks } from '../src/turn/databaseHooks.js';
|
||||
import { InMemoryTurnWorld } from '../src/turn/inMemoryWorld.js';
|
||||
import type { TurnWorldSnapshot, TurnWorldState } from '../src/turn/types.js';
|
||||
|
||||
const databaseUrl = process.env.INPUT_EVENT_DATABASE_URL;
|
||||
const integration = describe.skipIf(!databaseUrl);
|
||||
const worldId = 2_146_200_820;
|
||||
const generalId = 2_146_200_821;
|
||||
const turnTime = new Date('0190-01-01T00:00:00.000Z');
|
||||
const turnRunResult = {
|
||||
lastTurnTime: turnTime.toISOString(),
|
||||
processedGenerals: 1,
|
||||
processedTurns: 1,
|
||||
durationMs: 0,
|
||||
partial: false,
|
||||
} as const;
|
||||
|
||||
const schedule: TurnSchedule = {
|
||||
entries: [{ startMinute: 0, tickMinutes: 10 }],
|
||||
};
|
||||
|
||||
const state: TurnWorldState = {
|
||||
id: worldId,
|
||||
currentYear: 190,
|
||||
currentMonth: 1,
|
||||
tickSeconds: 600,
|
||||
lastTurnTime: turnTime,
|
||||
meta: {},
|
||||
};
|
||||
|
||||
const snapshot: TurnWorldSnapshot = {
|
||||
generals: [],
|
||||
cities: [],
|
||||
nations: [],
|
||||
troops: [],
|
||||
diplomacy: [],
|
||||
events: [],
|
||||
initialEvents: [],
|
||||
map: {
|
||||
id: 'turn-failure-log-persistence',
|
||||
name: '턴 실패 로그 영속화',
|
||||
cities: [],
|
||||
defaults: { trust: 50, trade: 100, supplyState: 1, frontState: 0 },
|
||||
},
|
||||
scenarioConfig: {
|
||||
stat: { total: 300, min: 10, max: 100, npcTotal: 150, npcMax: 50, npcMin: 10, chiefMin: 70 },
|
||||
iconPath: '',
|
||||
map: {},
|
||||
const: {},
|
||||
environment: { mapName: 'che', unitSet: 'che' },
|
||||
},
|
||||
scenarioMeta: {
|
||||
title: '턴 실패 로그 영속화',
|
||||
startYear: 190,
|
||||
life: null,
|
||||
fiction: null,
|
||||
history: [],
|
||||
ignoreDefaultEvents: false,
|
||||
},
|
||||
};
|
||||
|
||||
integration('turn failure personal-record persistence', () => {
|
||||
let db: GamePrismaClient;
|
||||
let disconnect: (() => Promise<void>) | undefined;
|
||||
let databaseHooks: DatabaseTurnHooks | undefined;
|
||||
|
||||
const cleanup = async () => {
|
||||
await db.logEntry.deleteMany({ where: { generalId } });
|
||||
await db.worldState.deleteMany({ where: { id: worldId } });
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const connector = createGamePostgresConnector({ url: databaseUrl! });
|
||||
await connector.connect();
|
||||
db = connector.prisma;
|
||||
disconnect = () => connector.disconnect();
|
||||
await cleanup();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await databaseHooks?.close();
|
||||
await cleanup();
|
||||
await disconnect?.();
|
||||
});
|
||||
|
||||
it('stores personal and nation-turn failure reasons under the acting general', async () => {
|
||||
await db.worldState.create({
|
||||
data: {
|
||||
id: worldId,
|
||||
scenarioCode: 'turn-failure-log-persistence',
|
||||
currentYear: state.currentYear,
|
||||
currentMonth: state.currentMonth,
|
||||
tickSeconds: state.tickSeconds,
|
||||
config: snapshot.scenarioConfig as unknown as InputJsonValue,
|
||||
meta: {},
|
||||
},
|
||||
});
|
||||
|
||||
const world = new InMemoryTurnWorld(state, snapshot, { schedule });
|
||||
world.pushLog({
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
generalId,
|
||||
format: LogFormat.MONTH,
|
||||
text: '대상 도시가 아국이 아닙니다. 발령 실패.',
|
||||
});
|
||||
world.pushLog({
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
generalId,
|
||||
format: LogFormat.MONTH,
|
||||
text: '같은 도시입니다. 이동 실패.',
|
||||
});
|
||||
|
||||
databaseHooks = await createDatabaseTurnHooks(databaseUrl!, world);
|
||||
await databaseHooks.hooks.flushChanges?.(turnRunResult);
|
||||
|
||||
const records = await db.logEntry.findMany({
|
||||
where: {
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
generalId,
|
||||
},
|
||||
orderBy: { id: 'asc' },
|
||||
select: { generalId: true, text: true },
|
||||
});
|
||||
|
||||
expect(records).toEqual([
|
||||
{ generalId, text: '<C>●</>1월:대상 도시가 아국이 아닙니다. 발령 실패.' },
|
||||
{ generalId, text: '<C>●</>1월:같은 도시입니다. 이동 실패.' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -827,6 +827,62 @@ test('메인 장수 동향과 개인 전투 기록은 Ref 행 간격·색상·
|
||||
await persistParityArtifact(page, 'core-main-personal-battle-log-inline-mobile', mobileGeometry);
|
||||
});
|
||||
|
||||
test('개인턴·수뇌턴 실패 사유를 메인 개인 기록에 표시한다', async ({ page }) => {
|
||||
const state: FixtureState = {
|
||||
permission: 'head',
|
||||
myset: 3,
|
||||
settingMutations: [],
|
||||
accessPages: [],
|
||||
recentRecords: {
|
||||
global: [],
|
||||
general: [
|
||||
{
|
||||
id: 19002,
|
||||
text: '<C>●</>1월:대상 도시가 아국이 아닙니다. <Y>여포</> 발령 실패.',
|
||||
createdAt: '2026-01-01T03:55:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 19001,
|
||||
text: '<C>●</>1월:같은 도시입니다. <G><b>업</b></>으로 이동 실패.',
|
||||
createdAt: '2026-01-01T03:54:00.000Z',
|
||||
},
|
||||
],
|
||||
history: [],
|
||||
},
|
||||
};
|
||||
await install(page, state);
|
||||
await page.setViewportSize({ width: 1200, height: 900 });
|
||||
await page.goto('');
|
||||
|
||||
const inspectFailureLogs = async (selector: string) => {
|
||||
const lines = page.locator(selector);
|
||||
await expect(lines).toHaveCount(2);
|
||||
await expect(lines.nth(0)).toContainText('대상 도시가 아국이 아닙니다. 여포 발령 실패. 12:55');
|
||||
await expect(lines.nth(1)).toContainText('같은 도시입니다. 업으로 이동 실패. 12:54');
|
||||
return lines.evaluateAll((elements) =>
|
||||
elements.map((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const style = getComputedStyle(element);
|
||||
return {
|
||||
text: element.textContent?.trim(),
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
lineHeight: style.lineHeight,
|
||||
};
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const desktop = await inspectFailureLogs('.record-zone [data-record-bucket="general"] .record-line');
|
||||
expect(desktop.every((line) => line.width > 0 && line.height === 21 && line.lineHeight === '21px')).toBe(true);
|
||||
await persistParityArtifact(page, 'core-main-turn-failure-personal-records-desktop', desktop);
|
||||
|
||||
await page.setViewportSize({ width: 500, height: 900 });
|
||||
const mobile = await inspectFailureLogs('.record-zone-mobile [data-record-bucket="general"] .record-line');
|
||||
expect(mobile.every((line) => line.width > 0 && line.height === 21 && line.lineHeight === '21px')).toBe(true);
|
||||
await persistParityArtifact(page, 'core-main-turn-failure-personal-records-mobile', mobile);
|
||||
});
|
||||
|
||||
test('전투시드는 메인·내 정보·감찰부에서 숨긴 채 선택할 수 있다', async ({ page }) => {
|
||||
const seedText = '(전투시드: 0123456789abcdef)';
|
||||
const logText =
|
||||
|
||||
Reference in New Issue
Block a user