diff --git a/app/game-frontend/e2e/directoryLists.spec.ts b/app/game-frontend/e2e/directoryLists.spec.ts index 7983a16b..43ef9e18 100644 --- a/app/game-frontend/e2e/directoryLists.spec.ts +++ b/app/game-frontend/e2e/directoryLists.spec.ts @@ -710,6 +710,106 @@ test('trait and injury explanations appear on pointer hover and keyboard focus', await page.screenshot({ path: testInfo.outputPath('directory-trait-injury-tooltips.png'), fullPage: true }); }); +for (const width of [1200, 500]) { + test(`catalog description line breaks stay safe on hover and focus at ${width}px`, async ({ page }, testInfo) => { + await page.setViewportSize({ width, height: 900 }); + const warInfo = + '[계략] 화계·탈취·파괴·선동 : 성공률 +10%p
[전투] 계략 시도 확률 +20%p, 계략 성공 확률 +20%p'; + const literalMarkup = '설명'; + const directoryRows = generals.map((general) => + general.id === 10 + ? { + ...general, + specialDomestic: { + ...general.specialDomestic, + info: `상업 특기
수익 증가
매매 효율\r\n${literalMarkup}`, + }, + specialWar: { key: 'che_신산', name: '신산', info: warInfo }, + } + : general + ); + await install(page, 'general', [], [], directoryRows); + await page.goto('general-list'); + await page.reload(); + const prefix = width <= 600 ? 'card-' : ''; + const container = page + .locator(width <= 600 ? '[data-general-card-id="10"]' : 'tr[data-general-id="10"]') + .first(); + await expect(container).toBeVisible(); + await expect(page.getByRole('button', { name: '갱 신' })).not.toHaveAttribute('aria-busy'); + await page.evaluate(async () => { + await document.fonts.ready; + await Promise.all(Array.from(document.images, (image) => image.decode().catch(() => undefined))); + }); + const before = await container.boundingBox(); + const evidence: Record = { width, url: page.url(), before }; + for (const [kind, expected] of [ + ['special-domestic', ['상업 특기', '수익 증가', '매매 효율', literalMarkup]], + ['special-war', warInfo.split('
')], + ] as const) { + const trigger = page.locator(`[data-directory-tooltip="${prefix}${kind}-10"]`); + const tooltip = trigger.getByRole('tooltip'); + await page.mouse.move(0, 0); + await trigger.hover(); + await expect(tooltip).toBeVisible(); + await trigger.focus(); + await expect(tooltip).toBeVisible(); + await expect(trigger).toHaveCSS('text-decoration-line', 'none'); + await expect(trigger).toHaveCSS('outline-style', 'solid'); + const description = tooltip.locator('.directory-tooltip__description'); + await expect(description.locator('br')).toHaveCount(expected.length - 1); + await expect(description.locator('img, b, script')).toHaveCount(0); + expect(await description.innerText()).not.toMatch(//iu); + const metrics = await description.evaluate((element) => { + const lines = Array.from(element.childNodes).filter( + (node) => node.nodeType === Node.TEXT_NODE && node.textContent?.trim() + ); + return { + lines: lines.map((node) => { + const range = document.createRange(); + range.selectNodeContents(node); + return { text: node.textContent?.trim(), top: range.getBoundingClientRect().top }; + }), + html: element.outerHTML, + }; + }); + expect(metrics.lines.map((line) => line.text)).toEqual(expected); + for (let index = 1; index < metrics.lines.length; index += 1) { + expect(metrics.lines[index]!.top).toBeGreaterThan(metrics.lines[index - 1]!.top); + } + const geometry = await tooltip.evaluate((element) => { + const rect = element.getBoundingClientRect(); + const style = getComputedStyle(element); + return { + left: rect.left, + right: rect.right, + top: rect.top, + bottom: rect.bottom, + font: style.font, + background: style.backgroundColor, + position: style.position, + }; + }); + expect(geometry.left).toBeGreaterThanOrEqual(8); + expect(geometry.right).toBeLessThanOrEqual(width - 8); + expect(geometry.top).toBeGreaterThanOrEqual(0); + expect(geometry.bottom).toBeLessThanOrEqual(900); + expect(await container.boundingBox()).toEqual(before); + evidence[kind] = { ...metrics, geometry }; + await page.screenshot({ path: testInfo.outputPath(`catalog-${kind}-${width}.png`), fullPage: true }); + await trigger.blur(); + } + const empty = page.locator(`[data-directory-tooltip="${prefix}special-war-20"]`); + await expect(empty).not.toHaveAttribute('tabindex'); + await expect(empty.getByRole('tooltip')).toHaveCount(0); + await writeFile(testInfo.outputPath('catalog-description.json'), JSON.stringify(evidence, null, 2)); + await writeFile( + testInfo.outputPath('catalog-description.html'), + await page.locator('main').evaluate((element) => element.outerHTML) + ); + }); +} + test('npc directory reuses the dark sort controls and sorts from a table header', async ({ page }, testInfo) => { await install(page); await page.goto('npc-list'); diff --git a/app/game-frontend/src/components/directory/DirectoryTooltip.vue b/app/game-frontend/src/components/directory/DirectoryTooltip.vue index 2c3f1b84..0d5b2859 100644 --- a/app/game-frontend/src/components/directory/DirectoryTooltip.vue +++ b/app/game-frontend/src/components/directory/DirectoryTooltip.vue @@ -1,9 +1,14 @@ diff --git a/app/game-frontend/src/components/ui/RichTooltip.vue b/app/game-frontend/src/components/ui/RichTooltip.vue index 3669be41..b4350f77 100644 --- a/app/game-frontend/src/components/ui/RichTooltip.vue +++ b/app/game-frontend/src/components/ui/RichTooltip.vue @@ -1,5 +1,6 @@