diff --git a/app/game-frontend/e2e/inGameMenus.spec.ts b/app/game-frontend/e2e/inGameMenus.spec.ts index c2a6b351..ec554971 100644 --- a/app/game-frontend/e2e/inGameMenus.spec.ts +++ b/app/game-frontend/e2e/inGameMenus.spec.ts @@ -63,6 +63,7 @@ type FixtureState = { joinConfig?: Record; createGeneralInputs?: Array>; 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: '기록' }] }); + return response({ + type: 'generalAction', + logs: [{ id: 1, text: state.hiddenSeedLogText ?? '기록' }], + }); } 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: `${type} 감찰 기록` }] }); + return response({ + type, + generalId: 7, + logs: [{ id: 1, text: state.hiddenSeedLogText ?? `${type} 감찰 기록` }], + }); } return response({ ok: true }); }); @@ -791,6 +799,80 @@ test('메인 장수 동향과 개인 전투 기록은 모두 21px 행 간격을 await persistParityArtifact(page, 'core-main-personal-battle-log-inline-mobile', mobileGeometry); }); +test('전투시드는 메인·내 정보·감찰부에서 숨긴 채 선택할 수 있다', async ({ page }) => { + const seedText = '(전투시드: 0123456789abcdef)'; + const logText = + '검증장수가 낙양으로 ' + + `진격합니다.${seedText}`; + 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>) => { + 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', diff --git a/app/game-frontend/src/assets/main.css b/app/game-frontend/src/assets/main.css index 52aaefe7..e5113943 100644 --- a/app/game-frontend/src/assets/main.css +++ b/app/game-frontend/src/assets/main.css @@ -87,3 +87,14 @@ 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; +} diff --git a/app/game-frontend/src/components/main/GeneralRecordPanels.vue b/app/game-frontend/src/components/main/GeneralRecordPanels.vue index 7e756914..8d2bb70f 100644 --- a/app/game-frontend/src/components/main/GeneralRecordPanels.vue +++ b/app/game-frontend/src/components/main/GeneralRecordPanels.vue @@ -84,11 +84,6 @@ const unavailableText: Record = { border-bottom: 0; } -.log-line :deep(.hidden_but_copyable) { - color: transparent !important; - font-size: 0; -} - .empty { padding: 2px 8px; color: #999; diff --git a/app/game-frontend/src/views/MainView.vue b/app/game-frontend/src/views/MainView.vue index 0e5fa475..45c72071 100644 --- a/app/game-frontend/src/views/MainView.vue +++ b/app/game-frontend/src/views/MainView.vue @@ -763,11 +763,6 @@ button { vertical-align: top; } -.record-line :deep(.hidden_but_copyable) { - color: transparent !important; - font-size: 0; -} - .record-empty { color: #aaa; } diff --git a/docs/frontend-legacy-parity.md b/docs/frontend-legacy-parity.md index a4e217cf..762eb486 100644 --- a/docs/frontend-legacy-parity.md +++ b/docs/frontend-legacy-parity.md @@ -137,6 +137,13 @@ 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을 남깁니다. + To refresh the PHP ranking evidence after building the ignored reference webpack assets, run: diff --git a/tools/frontend-legacy-parity/reference-personal-war-log-font.mjs b/tools/frontend-legacy-parity/reference-personal-war-log-font.mjs index 3d81d429..19f614c1 100644 --- a/tools/frontend-legacy-parity/reference-personal-war-log-font.mjs +++ b/tools/frontend-legacy-parity/reference-personal-war-log-font.mjs @@ -12,6 +12,8 @@ const css = await readFile(resolve(refRoot, 'dist_js/hwe_dynamic/vue/v_main.css' const records = [ '●10월:천귀병으로 ⓝ염행의 보병을 수비합니다. <1>12:54', '●10월:천귀병으로 ⓝ염행의 보병을 공격합니다. <1>12:55', + '검증장수가 낙양으로 진격합니다.' + + '(전투시드: 0123456789abcdef)', ]; 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, }; }) );