merge: 최신 main을 행동 기반 접속 집계에 통합
This commit is contained in:
@@ -63,6 +63,7 @@ type FixtureState = {
|
||||
joinConfig?: Record<string, unknown>;
|
||||
createGeneralInputs?: Array<Record<string, unknown>>;
|
||||
mainTraits?: { personal: string; specialDomestic: string; specialWar: string };
|
||||
hiddenSeedLogText?: string;
|
||||
recentRecords?: {
|
||||
global: Array<{ id: number; text: string; createdAt?: string }>;
|
||||
general: Array<{ id: number; text: string; createdAt?: string }>;
|
||||
@@ -484,7 +485,10 @@ const install = async (page: Page, state: FixtureState) => {
|
||||
});
|
||||
if (operation === 'general.getMyLog') {
|
||||
state.generalLogQueries = (state.generalLogQueries ?? 0) + 1;
|
||||
return response({ type: 'generalAction', logs: [{ id: 1, text: '<Y>기록</>' }] });
|
||||
return response({
|
||||
type: 'generalAction',
|
||||
logs: [{ id: 1, text: state.hiddenSeedLogText ?? '<Y>기록</>' }],
|
||||
});
|
||||
}
|
||||
if (operation === 'general.instantRetreat') {
|
||||
state.instantRetreatInputs?.push(jsonInput);
|
||||
@@ -560,7 +564,11 @@ const install = async (page: Page, state: FixtureState) => {
|
||||
['generalHistory', 'battleDetail', 'battleResult', 'generalAction'].includes(jsonInput.type)
|
||||
? jsonInput.type
|
||||
: 'generalAction';
|
||||
return response({ type, generalId: 7, logs: [{ id: 1, text: `<Y>${type} 감찰 기록</>` }] });
|
||||
return response({
|
||||
type,
|
||||
generalId: 7,
|
||||
logs: [{ id: 1, text: state.hiddenSeedLogText ?? `<Y>${type} 감찰 기록</>` }],
|
||||
});
|
||||
}
|
||||
return response({ ok: true });
|
||||
});
|
||||
@@ -676,7 +684,7 @@ test('메인 카드의 국가·수도·관직·계급·병종은 Ref 출력명
|
||||
await expect(page.locator('.main-page')).not.toContainText('che_');
|
||||
});
|
||||
|
||||
test('메인 장수 동향과 개인 전투 기록은 모두 21px 행 간격을 유지한다', async ({ page }) => {
|
||||
test('메인 장수 동향과 개인 전투 기록은 Ref 행 간격·색상·글자 크기를 유지한다', async ({ page }) => {
|
||||
const state: FixtureState = {
|
||||
permission: 'head',
|
||||
myset: 3,
|
||||
@@ -704,13 +712,15 @@ test('메인 장수 동향과 개인 전투 기록은 모두 21px 행 간격을
|
||||
id: 18608,
|
||||
text:
|
||||
'<S>◆</>186년 9월:<div class="small_war_log">' +
|
||||
'<span class="me"><span class="crew_type">귀병</span> ' +
|
||||
'<span class="name_plate_cover">【<span class="name">Administrator</span>】</span> ' +
|
||||
'<span class="remain_crew">0</span>(<span class="killed_crew">-2209</span>)</span> ' +
|
||||
'<span class="me"><span class="name_plate"><span class="crew_type">귀병</span> ' +
|
||||
'<span class="name_plate_cover">【<span class="name">Administrator</span>】</span></span> ' +
|
||||
'<span class="crew_plate"><span class="remain_crew">0</span>' +
|
||||
'<span class="killed_plate">(<span class="killed_crew">-2209</span>)</span></span></span> ' +
|
||||
'<span class="war_type war_type_defense">←</span> ' +
|
||||
'<span class="you"><span class="remain_crew">1361</span>' +
|
||||
'(<span class="killed_crew">-5539</span>) <span class="crew_type">기병</span> ' +
|
||||
'<span class="name_plate_cover">【<span class="name">ⓝ뇌동</span>】</span></span></div>',
|
||||
'<span class="you"><span class="crew_plate"><span class="remain_crew">1361</span>' +
|
||||
'<span class="killed_plate">(<span class="killed_crew">-5539</span>)</span></span> ' +
|
||||
'<span class="name_plate"><span class="crew_type">기병</span> ' +
|
||||
'<span class="name_plate_cover">【<span class="name">ⓝ뇌동</span>】</span></span></span></div>',
|
||||
createdAt: '2026-01-01T03:54:00.000Z',
|
||||
},
|
||||
],
|
||||
@@ -754,6 +764,16 @@ test('메인 장수 동향과 개인 전투 기록은 모두 21px 행 간격을
|
||||
const battle = element.querySelector<HTMLElement>('.small_war_log');
|
||||
if (!battle) throw new Error('전투 요약 markup을 찾지 못했습니다.');
|
||||
const battleRect = battle.getBoundingClientRect();
|
||||
const requireElement = (selector: string): HTMLElement => {
|
||||
const target = element.querySelector<HTMLElement>(selector);
|
||||
if (!target) throw new Error(`전투 요약 요소를 찾지 못했습니다: ${selector}`);
|
||||
return target;
|
||||
};
|
||||
const diamond = requireElement('span[style*="skyblue"]');
|
||||
const namePlate = requireElement('.me .name_plate');
|
||||
const nameCover = requireElement('.me .name_plate_cover');
|
||||
const crewPlate = requireElement('.me .crew_plate');
|
||||
const arrow = requireElement('.war_type_defense');
|
||||
return {
|
||||
line: { top: lineRect.top, height: lineRect.height },
|
||||
battle: {
|
||||
@@ -762,6 +782,14 @@ test('메인 장수 동향과 개인 전투 기록은 모두 21px 행 간격을
|
||||
display: getComputedStyle(battle).display,
|
||||
},
|
||||
lineHeight: getComputedStyle(element).lineHeight,
|
||||
styles: {
|
||||
diamondColor: getComputedStyle(diamond).color,
|
||||
namePlateFontSize: getComputedStyle(namePlate).fontSize,
|
||||
nameCoverColor: getComputedStyle(nameCover).color,
|
||||
crewPlateColor: getComputedStyle(crewPlate).color,
|
||||
crewPlateFontSize: getComputedStyle(crewPlate).fontSize,
|
||||
defenseArrowColor: getComputedStyle(arrow).color,
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
@@ -770,6 +798,14 @@ test('메인 장수 동향과 개인 전투 기록은 모두 21px 행 간격을
|
||||
expect(geometry.line.height).toBe(21);
|
||||
expect(geometry.battle.height).toBe(21);
|
||||
expect(geometry.battle.top).toBeCloseTo(geometry.line.top, 0);
|
||||
expect(geometry.styles).toEqual({
|
||||
diamondColor: 'rgb(135, 206, 235)',
|
||||
namePlateFontSize: '10.5px',
|
||||
nameCoverColor: 'rgb(255, 255, 0)',
|
||||
crewPlateColor: 'rgb(255, 69, 0)',
|
||||
crewPlateFontSize: '12.6px',
|
||||
defenseArrowColor: 'rgb(255, 0, 255)',
|
||||
});
|
||||
};
|
||||
|
||||
const desktopGeometry = await inspect(
|
||||
@@ -791,6 +827,80 @@ test('메인 장수 동향과 개인 전투 기록은 모두 21px 행 간격을
|
||||
await persistParityArtifact(page, 'core-main-personal-battle-log-inline-mobile', mobileGeometry);
|
||||
});
|
||||
|
||||
test('전투시드는 메인·내 정보·감찰부에서 숨긴 채 선택할 수 있다', async ({ page }) => {
|
||||
const seedText = '(전투시드: 0123456789abcdef)';
|
||||
const logText =
|
||||
'<D><b>위</b></>의 <Y>검증장수</>가 <G><b>낙양</b></>으로 ' +
|
||||
`진격합니다.<span class="hidden_but_copyable">${seedText}</span>`;
|
||||
const state: FixtureState = {
|
||||
permission: 'head',
|
||||
myset: 3,
|
||||
settingMutations: [],
|
||||
accessPages: [],
|
||||
hiddenSeedLogText: logText,
|
||||
recentRecords: {
|
||||
global: [{ id: 18611, text: logText }],
|
||||
general: [],
|
||||
history: [],
|
||||
},
|
||||
};
|
||||
await install(page, state);
|
||||
|
||||
const inspectHiddenSeed = async (selector: string) => {
|
||||
const seed = page.locator(selector);
|
||||
await expect(seed).toHaveCount(1);
|
||||
return seed.evaluate((element) => {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(element);
|
||||
const selection = window.getSelection();
|
||||
selection?.removeAllRanges();
|
||||
selection?.addRange(range);
|
||||
const style = getComputedStyle(element);
|
||||
const rect = element.getBoundingClientRect();
|
||||
const result = {
|
||||
text: element.textContent,
|
||||
selectedText: selection?.toString(),
|
||||
color: style.color,
|
||||
fontSize: style.fontSize,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
};
|
||||
selection?.removeAllRanges();
|
||||
return result;
|
||||
});
|
||||
};
|
||||
const assertHiddenSeed = (result: Awaited<ReturnType<typeof inspectHiddenSeed>>) => {
|
||||
expect(result.text).toBe(seedText);
|
||||
expect(result.selectedText).toBe(seedText);
|
||||
expect(result.color).toBe('rgba(0, 0, 0, 0)');
|
||||
expect(result.fontSize).toBe('0px');
|
||||
expect(result.width).toBe(0);
|
||||
expect(result.height).toBe(0);
|
||||
};
|
||||
|
||||
for (const viewport of [
|
||||
{ width: 1000, height: 900 },
|
||||
{ width: 500, height: 900 },
|
||||
]) {
|
||||
await page.setViewportSize(viewport);
|
||||
|
||||
await page.goto('');
|
||||
const mainSeed = await inspectHiddenSeed('[data-record-bucket="global"] .hidden_but_copyable');
|
||||
assertHiddenSeed(mainSeed);
|
||||
await persistParityArtifact(page, `core-hidden-battle-seed-main-${viewport.width}`, mainSeed);
|
||||
|
||||
await page.goto('my-page');
|
||||
const myPageSeed = await inspectHiddenSeed('.log-panel:first-child .hidden_but_copyable');
|
||||
assertHiddenSeed(myPageSeed);
|
||||
await persistParityArtifact(page, `core-hidden-battle-seed-my-page-${viewport.width}`, myPageSeed);
|
||||
|
||||
await page.goto('battle-center');
|
||||
const battleCenterSeed = await inspectHiddenSeed('[data-log-type="generalAction"] .hidden_but_copyable');
|
||||
assertHiddenSeed(battleCenterSeed);
|
||||
await persistParityArtifact(page, `core-hidden-battle-seed-battle-center-${viewport.width}`, battleCenterSeed);
|
||||
}
|
||||
});
|
||||
|
||||
test('메인 개인 기록의 공격·수비 시각은 Ref와 같은 90% 글자 크기로 표시한다', async ({ page }) => {
|
||||
const state: FixtureState = {
|
||||
permission: 'head',
|
||||
@@ -1099,6 +1209,66 @@ test('내 정보&설정 keeps desktop density and becomes a 390px horizontal-ide
|
||||
await persistParityArtifact(page, 'core-my-page-mobile', mobile);
|
||||
});
|
||||
|
||||
test('내 정보&설정의 지난 플레이는 기본 탐색과 분리되어 오른쪽에 정렬된다', async ({ page }) => {
|
||||
const state: FixtureState = { permission: 'head', myset: 3, settingMutations: [], accessPages: [] };
|
||||
await install(page, state);
|
||||
|
||||
for (const viewport of [
|
||||
{ name: 'desktop', width: 1000, height: 900 },
|
||||
{ name: 'mobile', width: 390, height: 844 },
|
||||
] as const) {
|
||||
await page.setViewportSize({ width: viewport.width, height: viewport.height });
|
||||
await page.goto('my-page');
|
||||
|
||||
const pastPlaysLink = page.getByRole('link', { name: '지난 플레이' });
|
||||
await expect(pastPlaysLink).toHaveAttribute('href', `${gameBasePath}/past-plays`);
|
||||
await pastPlaysLink.hover();
|
||||
await pastPlaysLink.focus();
|
||||
await expect(pastPlaysLink).toBeFocused();
|
||||
|
||||
const geometry = await page.locator('.title-row').evaluate((element) => {
|
||||
const title = element.getBoundingClientRect();
|
||||
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 past = element.querySelector<HTMLAnchorElement>('.past-plays-link')!.getBoundingClientRect();
|
||||
const pastStyle = getComputedStyle(element.querySelector<HTMLAnchorElement>('.past-plays-link')!);
|
||||
return {
|
||||
title: { left: title.left, right: title.right },
|
||||
actions: { left: actions.left, right: actions.right },
|
||||
navigation: { left: navigation.left, right: navigation.right },
|
||||
back: { top: back.top, right: back.right },
|
||||
refresh: { top: refresh.top, right: refresh.right },
|
||||
past: { top: past.top, left: past.left, right: past.right },
|
||||
pastStyle: {
|
||||
cursor: pastStyle.cursor,
|
||||
minHeight: pastStyle.minHeight,
|
||||
backgroundColor: pastStyle.backgroundColor,
|
||||
},
|
||||
scrollWidth: document.documentElement.scrollWidth,
|
||||
};
|
||||
});
|
||||
|
||||
expect(geometry.actions.left).toBeCloseTo(geometry.title.left + 1, 0);
|
||||
expect(geometry.actions.right).toBeCloseTo(geometry.title.right - 1, 0);
|
||||
expect(geometry.navigation.left).toBeCloseTo(geometry.actions.left, 0);
|
||||
expect(geometry.past.right).toBeCloseTo(geometry.actions.right, 0);
|
||||
expect(geometry.past.left).toBeGreaterThan(geometry.navigation.right);
|
||||
expect(geometry.back.top).toBeCloseTo(geometry.past.top, 0);
|
||||
expect(geometry.refresh.top).toBeCloseTo(geometry.past.top, 0);
|
||||
expect(geometry.pastStyle).toEqual({
|
||||
cursor: 'pointer',
|
||||
minHeight: '34px',
|
||||
backgroundColor: 'rgb(49, 95, 134)',
|
||||
});
|
||||
expect(geometry.scrollWidth).toBe(viewport.width);
|
||||
await persistParityArtifact(page, `core-my-page-past-plays-${viewport.name}`, geometry);
|
||||
await pastPlaysLink.click();
|
||||
await expect(page).toHaveURL(new RegExp(`${gameBasePath}/past-plays$`, 'u'));
|
||||
}
|
||||
});
|
||||
|
||||
for (const [label, failure] of [
|
||||
['daemon timeout', 'TIMEOUT'],
|
||||
['engine transaction 오류', 'INTERNAL_SERVER_ERROR'],
|
||||
|
||||
@@ -87,3 +87,39 @@ textarea {
|
||||
.small_war_log {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/*
|
||||
* Ref's common stylesheet keeps diagnostic seeds in the DOM so they remain
|
||||
* copyable, while removing them from every normal log rendering surface.
|
||||
* This marker can arrive through main, personal, audit, and history log APIs,
|
||||
* so the display contract belongs to the app-wide legacy markup boundary.
|
||||
*/
|
||||
.hidden_but_copyable {
|
||||
color: rgba(0, 0, 0, 0) !important;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
.small_war_log .war_type_attack {
|
||||
color: cyan;
|
||||
}
|
||||
|
||||
.small_war_log .war_type_defense {
|
||||
color: magenta;
|
||||
}
|
||||
|
||||
.small_war_log .war_type_siege {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.small_war_log .name_plate {
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
.small_war_log .name_plate_cover {
|
||||
color: yellow;
|
||||
}
|
||||
|
||||
.small_war_log .crew_plate {
|
||||
color: orangered;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
@@ -84,11 +84,6 @@ const unavailableText: Record<GeneralRecordType, string> = {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.log-line :deep(.hidden_but_copyable) {
|
||||
color: transparent !important;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 2px 8px;
|
||||
color: #999;
|
||||
|
||||
@@ -763,11 +763,6 @@ button {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.record-line :deep(.hidden_but_copyable) {
|
||||
color: transparent !important;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
.record-empty {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
@@ -365,9 +365,13 @@ onMounted(() => {
|
||||
<main id="container" class="legacy-page bg0 responsive-settings-page" :class="`screen-${screenMode}`">
|
||||
<div class="title-row">
|
||||
<span>내 정 보</span>
|
||||
<RouterLink class="legacy-button" to="/past-plays">지난 플레이</RouterLink>
|
||||
<RouterLink class="legacy-button" to="/">돌아가기</RouterLink>
|
||||
<button class="legacy-button" type="button" @click="() => loadPage()">새로고침</button>
|
||||
<div class="title-actions">
|
||||
<div class="navigation-actions">
|
||||
<RouterLink class="legacy-button" to="/">돌아가기</RouterLink>
|
||||
<button class="legacy-button" type="button" @click="() => loadPage()">새로고침</button>
|
||||
</div>
|
||||
<RouterLink class="legacy-button past-plays-link" to="/past-plays">지난 플레이</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="error-row">{{ error }}</div>
|
||||
@@ -675,6 +679,16 @@ onMounted(() => {
|
||||
height: 18px;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
.title-actions {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.navigation-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
.legacy-button,
|
||||
button,
|
||||
select,
|
||||
|
||||
@@ -137,6 +137,27 @@ REF_PERSONAL_WAR_LOG_ARTIFACT_DIR=/path/to/ignored/artifacts \
|
||||
`MENU_PARITY_ARTIFACT_DIR`를 지정하면 1200×900·500×900 screenshot과 computed
|
||||
style JSON을 남깁니다.
|
||||
|
||||
같은 Ref collector는 “진격합니다.” 전투 seed marker도 함께 렌더링하여
|
||||
desktop/mobile의 투명색, 0px 글자 크기, 0×0 rect와 selection text 보존을
|
||||
수집합니다. Core의 대응 test는 `inGameMenus.spec.ts`의 “전투시드는 메인·내
|
||||
정보·감찰부에서 숨긴 채 선택할 수 있다”이며 `PLAYWRIGHT_FRONTEND_MODE=production`
|
||||
과 `MENU_PARITY_ARTIFACT_DIR`를 함께 지정하면 세 화면의 1000×900·500×900
|
||||
screenshot과 computed JSON을 남깁니다.
|
||||
|
||||
개인 전투 결과 요약의 Ref 색상·글자 크기는 실제 `small_war_log` class 구조와
|
||||
빌드 CSS를 사용하는 별도 정적 Chromium fixture로 재현합니다. 방어 화살표,
|
||||
장수명 괄호, 병력 수치와 병종·장수/병력 글자 크기를 desktop/mobile에서
|
||||
수집하며 live session·DB writer 검증과는 구분합니다.
|
||||
|
||||
```sh
|
||||
REF_SAM_ROOT=/path/to/ref/sam \
|
||||
REF_PERSONAL_BATTLE_LOG_ARTIFACT_DIR=/path/to/ignored/artifacts \
|
||||
node tools/frontend-legacy-parity/reference-personal-battle-log-colors.mjs
|
||||
```
|
||||
|
||||
대응하는 Core 검증은 `inGameMenus.spec.ts`의 “개인 전투 기록” test이며 같은
|
||||
viewport에서 한 줄 geometry와 computed 색상·글자 크기를 함께 검사합니다.
|
||||
|
||||
To refresh the PHP ranking evidence after building the ignored reference
|
||||
webpack assets, run:
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { LogCategory, LogFormat } from '@sammo-ts/logic/logging/types.js';
|
||||
import { JosaUtil } from '@sammo-ts/common';
|
||||
import { z } from 'zod';
|
||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
import type { GeneralTurnCommandSpec } from './index.js';
|
||||
import type { MapDefinition } from '@sammo-ts/logic/world/types.js';
|
||||
import { normalizeLegacyIntegerArg, parseArgsWithSchema } from '../parseArgs.js';
|
||||
@@ -68,11 +69,8 @@ export class ActionResolver<
|
||||
|
||||
const destCityId = args.destCityId;
|
||||
const storedDestCityId = args.storedDestCityId ?? destCityId;
|
||||
let destCityName = `도시(${destCityId})`;
|
||||
if (context.map) {
|
||||
const c = context.map.cities.find((ct) => ct.id === destCityId);
|
||||
if (c) destCityName = c.name;
|
||||
}
|
||||
const destCityName =
|
||||
context.map?.cities.find((city) => city.id === destCityId)?.name ?? '알 수 없는 도시';
|
||||
|
||||
const josaRo = JosaUtil.pick(destCityName, '로');
|
||||
|
||||
@@ -132,6 +130,11 @@ export class ActionDefinition<
|
||||
}
|
||||
}
|
||||
|
||||
export const actionContextBuilder: ActionContextBuilder = (base, options) => ({
|
||||
...base,
|
||||
map: options.map,
|
||||
});
|
||||
|
||||
export const commandSpec: GeneralTurnCommandSpec = {
|
||||
key: 'che_NPC능동',
|
||||
category: '특수', // Valid category? Legacy didn't specify category in static prop usually, handled by mapping. Defaulting to '특수'.
|
||||
|
||||
@@ -68,11 +68,8 @@ export class ActionResolver<
|
||||
const goldCost = develCost * 5;
|
||||
|
||||
// Log destination
|
||||
let destCityName = `도시(${destCityId})`;
|
||||
if (context.map) {
|
||||
const c = context.map.cities.find((ct) => ct.id === destCityId);
|
||||
if (c) destCityName = c.name;
|
||||
}
|
||||
const destCityName =
|
||||
context.map?.cities.find((city) => city.id === destCityId)?.name ?? '알 수 없는 도시';
|
||||
|
||||
const josaRo = JosaUtil.pick(destCityName, '로');
|
||||
|
||||
|
||||
@@ -68,11 +68,8 @@ export class ActionResolver<
|
||||
|
||||
const cost = context.develCost ?? 0;
|
||||
|
||||
let destCityName = `도시(${destCityId})`;
|
||||
if (context.map) {
|
||||
const c = context.map.cities.find((ct) => ct.id === destCityId);
|
||||
if (c) destCityName = c.name;
|
||||
}
|
||||
const destCityName =
|
||||
context.map?.cities.find((city) => city.id === destCityId)?.name ?? '알 수 없는 도시';
|
||||
|
||||
const josaRo = JosaUtil.pick(destCityName, '로');
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
||||
import { resolve } from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
|
||||
import { chromium } from '@playwright/test';
|
||||
|
||||
const refRoot = resolve(process.env.REF_SAM_ROOT ?? '/home/letrhee/sam_rebuild/ref/sam');
|
||||
const artifactDir = process.env.REF_PERSONAL_BATTLE_LOG_ARTIFACT_DIR;
|
||||
const formatterUrl = pathToFileURL(resolve(refRoot, 'hwe/ts/utilGame/formatLog.ts')).href;
|
||||
const { formatLog } = await import(formatterUrl);
|
||||
const css = await readFile(resolve(refRoot, 'dist_js/hwe_dynamic/vue/v_main.css'), 'utf8');
|
||||
const record =
|
||||
'<S>◆</>188년 2월:<div class="small_war_log">' +
|
||||
'<span class="me"><span class="name_plate"><span class="crew_type">남귀</span> ' +
|
||||
'<span class="name_plate_cover">【<span class="name">운영자</span>】</span></span> ' +
|
||||
'<span class="crew_plate"><span class="remain_crew">0</span>' +
|
||||
'<span class="killed_plate">(<span class="killed_crew">-4404</span>)</span></span></span> ' +
|
||||
'<span class="war_type war_type_defense">←</span> ' +
|
||||
'<span class="you"><span class="crew_plate"><span class="remain_crew">555</span>' +
|
||||
'<span class="killed_plate">(<span class="killed_crew">-6845</span>)</span></span> ' +
|
||||
'<span class="name_plate"><span class="crew_type">기병</span> ' +
|
||||
'<span class="name_plate_cover">【<span class="name">ⓝ독야청정</span>】</span></span></span></div>';
|
||||
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
try {
|
||||
const context = await browser.newContext({
|
||||
colorScheme: 'dark',
|
||||
deviceScaleFactor: 1,
|
||||
locale: 'ko-KR',
|
||||
timezoneId: 'UTC',
|
||||
});
|
||||
const page = await context.newPage();
|
||||
await page.setContent(
|
||||
`<!doctype html><html><head><style>${css}</style></head><body>` +
|
||||
`<div id="container"><div class="RecordZone row gx-0"><div class="GeneralLog col col-12 col-lg-6">` +
|
||||
`<div class="bg1 center s-border-tb title">개인 기록</div><div class="fixture-line">${formatLog(record)}</div>` +
|
||||
`</div></div></div></body></html>`,
|
||||
{ waitUntil: 'networkidle' }
|
||||
);
|
||||
await page.evaluate(() => document.fonts.ready);
|
||||
|
||||
for (const viewport of [
|
||||
{ name: 'desktop', width: 1200, height: 900 },
|
||||
{ name: 'mobile', width: 500, height: 900 },
|
||||
]) {
|
||||
await page.setViewportSize(viewport);
|
||||
const measurement = await page.locator('.fixture-line').evaluate((element) => {
|
||||
const requireElement = (selector) => {
|
||||
const target = element.querySelector(selector);
|
||||
if (!(target instanceof HTMLElement)) throw new Error(`Ref 전투기록 요소를 찾지 못했습니다: ${selector}`);
|
||||
return target;
|
||||
};
|
||||
const rect = element.getBoundingClientRect();
|
||||
const battle = requireElement('.small_war_log');
|
||||
const battleRect = battle.getBoundingClientRect();
|
||||
return {
|
||||
text: element.textContent,
|
||||
line: { height: rect.height, fontSize: getComputedStyle(element).fontSize },
|
||||
battle: { height: battleRect.height, display: getComputedStyle(battle).display },
|
||||
styles: {
|
||||
diamondColor: getComputedStyle(requireElement('span[style*="skyblue"]')).color,
|
||||
namePlateFontSize: getComputedStyle(requireElement('.me .name_plate')).fontSize,
|
||||
nameCoverColor: getComputedStyle(requireElement('.me .name_plate_cover')).color,
|
||||
crewPlateColor: getComputedStyle(requireElement('.me .crew_plate')).color,
|
||||
crewPlateFontSize: getComputedStyle(requireElement('.me .crew_plate')).fontSize,
|
||||
defenseArrowColor: getComputedStyle(requireElement('.war_type_defense')).color,
|
||||
},
|
||||
};
|
||||
});
|
||||
const output = { viewport, measurement };
|
||||
console.log(JSON.stringify(output));
|
||||
if (artifactDir) {
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
await Promise.all([
|
||||
page.screenshot({ path: resolve(artifactDir, `ref-personal-battle-log-colors-${viewport.name}.png`) }),
|
||||
writeFile(
|
||||
resolve(artifactDir, `ref-personal-battle-log-colors-${viewport.name}.json`),
|
||||
`${JSON.stringify(output, null, 2)}\n`
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
@@ -12,6 +12,8 @@ const css = await readFile(resolve(refRoot, 'dist_js/hwe_dynamic/vue/v_main.css'
|
||||
const records = [
|
||||
'<C>●</>10월:천귀병으로 <Y>ⓝ염행</>의 보병을 <M>수비</>합니다. <1>12:54</>',
|
||||
'<C>●</>10월:천귀병으로 <Y>ⓝ염행</>의 보병을 <M>공격</>합니다. <1>12:55</>',
|
||||
'<D><b>위</b></>의 <Y>검증장수</>가 <G><b>낙양</b></>으로 진격합니다.' +
|
||||
'<span class="hidden_but_copyable">(전투시드: 0123456789abcdef)</span>',
|
||||
];
|
||||
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
@@ -43,6 +45,29 @@ try {
|
||||
const time = spans.find((span) => /^\d{2}:\d{2}$/u.test(span.textContent ?? ''));
|
||||
const name = spans.find((span) => span.textContent === 'ⓝ염행');
|
||||
const action = spans.find((span) => span.textContent === '수비' || span.textContent === '공격');
|
||||
const hiddenSeed = element.querySelector('.hidden_but_copyable');
|
||||
if (hiddenSeed instanceof HTMLElement) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(hiddenSeed);
|
||||
const selection = window.getSelection();
|
||||
selection?.removeAllRanges();
|
||||
selection?.addRange(range);
|
||||
const hiddenSeedRect = hiddenSeed.getBoundingClientRect();
|
||||
const hiddenSeedStyle = getComputedStyle(hiddenSeed);
|
||||
const result = {
|
||||
text: hiddenSeed.textContent,
|
||||
selectedText: selection?.toString(),
|
||||
color: hiddenSeedStyle.color,
|
||||
fontSize: hiddenSeedStyle.fontSize,
|
||||
width: hiddenSeedRect.width,
|
||||
height: hiddenSeedRect.height,
|
||||
};
|
||||
selection?.removeAllRanges();
|
||||
return {
|
||||
text: element.textContent,
|
||||
hiddenSeed: result,
|
||||
};
|
||||
}
|
||||
if (
|
||||
!(time instanceof HTMLElement) ||
|
||||
!(name instanceof HTMLElement) ||
|
||||
@@ -62,6 +87,7 @@ try {
|
||||
timeFontSize: getComputedStyle(time).fontSize,
|
||||
nameFontSize: getComputedStyle(name).fontSize,
|
||||
actionFontSize: getComputedStyle(action).fontSize,
|
||||
hiddenSeed: null,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
@@ -421,6 +421,21 @@ integration('general command success matrix', () => {
|
||||
ignoredPathPatterns: ignoredLifecyclePaths,
|
||||
})
|
||||
).toEqual([]);
|
||||
if (action === 'che_이동' || action === 'che_강행') {
|
||||
const actionLogSuffix = action === 'che_이동' ? '이동했습니다.' : '강행했습니다.';
|
||||
expect(
|
||||
semanticLogSignatures(
|
||||
core.after.logs.filter((entry) => String(entry.text).includes(actionLogSuffix))
|
||||
)
|
||||
).toEqual(
|
||||
semanticLogSignatures(
|
||||
addedReferenceLogs(reference.before, reference.after.logs).filter((entry) =>
|
||||
String(entry.text).includes(actionLogSuffix)
|
||||
)
|
||||
)
|
||||
);
|
||||
expect(core.after.logs.some((entry) => String(entry.text).includes('도시('))).toBe(false);
|
||||
}
|
||||
},
|
||||
120_000
|
||||
);
|
||||
@@ -564,6 +579,12 @@ integration('NPC active command boundary parity', () => {
|
||||
ignoredPathPatterns: ignoredLifecyclePaths,
|
||||
})
|
||||
).toEqual([]);
|
||||
if (completed) {
|
||||
expect(semanticLogSignatures(core.after.logs)).toEqual(
|
||||
semanticLogSignatures(addedReferenceLogs(reference.before, reference.after.logs))
|
||||
);
|
||||
expect(core.after.logs.some((entry) => String(entry.text).includes('도시('))).toBe(false);
|
||||
}
|
||||
},
|
||||
120_000
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user