diff --git a/frontend/e2e/tool-style-consistency.spec.js b/frontend/e2e/tool-style-consistency.spec.js new file mode 100644 index 0000000..0f01a41 --- /dev/null +++ b/frontend/e2e/tool-style-consistency.spec.js @@ -0,0 +1,108 @@ +import { test, expect } from '@playwright/test'; + +const TOOLS = [ + 'code-encoder', + 'code-encrypter', + 'hash-generator', + 'code-converter', + 'text-utilities', + 'number-converter', + 'datetime-converter', + 'jwt', + 'barcode', + 'data-generator', + 'code-formatter', + 'color-converter', + 'url-inspector', + 'cron', + 'regexp', + 'diff', +]; + +const THEMES = [ + { mode: 'light', name: 'github-light' }, + { mode: 'dark', name: 'github-dark' }, +]; + +async function setTheme(page, theme) { + await page.evaluate(({ mode, name }) => { + localStorage.setItem('dt-mode', mode); + localStorage.setItem('dt-name', name); + }, theme); +} + +test.describe('Tool visual style consistency', () => { + for (const theme of THEMES) { + for (const slug of TOOLS) { + test(`${slug} keeps tool text and borders consistent in ${theme.mode}`, async ({ page }) => { + await page.goto(`/tool/${slug}`); + await setTheme(page, theme); + await page.goto(`/tool/${slug}`); + await page.waitForLoadState('networkidle'); + + const issues = await page.evaluate(() => { + const isVisible = (element, style) => { + const rect = element.getBoundingClientRect(); + return ( + rect.width > 0 && + rect.height > 0 && + style.visibility !== 'hidden' && + style.display !== 'none' && + style.opacity !== '0' + ); + }; + + const selector = 'main, [data-testid="tool-page"], [role="main"]'; + const root = document.querySelector(selector) ?? document.body; + + return Array.from(root.querySelectorAll('*')).flatMap((element) => { + const style = getComputedStyle(element); + if (!isVisible(element, style)) return []; + + const rect = element.getBoundingClientRect(); + const text = element.textContent?.replace(/\s+/g, ' ').trim() ?? ''; + const tag = element.tagName.toLowerCase(); + const classes = element.className?.toString?.() ?? ''; + const elementIssues = []; + + const fontSize = parseFloat(style.fontSize); + const hasOwnText = + text.length > 0 && + Array.from(element.children).every((child) => child.textContent?.trim() !== text); + + if (hasOwnText && fontSize > 0 && fontSize < 11) { + elementIssues.push({ + kind: 'font-size', + tag, + classes, + text: text.slice(0, 80), + value: style.fontSize, + }); + } + + const borderWidths = [ + style.borderTopWidth, + style.borderRightWidth, + style.borderBottomWidth, + style.borderLeftWidth, + ].map(parseFloat); + + if (rect.width > 8 && rect.height > 8 && borderWidths.some((width) => width > 2)) { + elementIssues.push({ + kind: 'border-width', + tag, + classes, + text: text.slice(0, 80), + value: borderWidths.join('/'), + }); + } + + return elementIssues; + }); + }); + + expect(issues).toEqual([]); + }); + } + } +}); diff --git a/frontend/src/components/inputs/ToolCopyButton.jsx b/frontend/src/components/inputs/ToolCopyButton.jsx index 805db80..6ca4f90 100644 --- a/frontend/src/components/inputs/ToolCopyButton.jsx +++ b/frontend/src/components/inputs/ToolCopyButton.jsx @@ -42,7 +42,7 @@ export function ToolCopyButton({ size={size} onClick={handleCopy} disabled={isDisabled} - className={cn('h-7 gap-1.5 text-[10px] font-bold uppercase tracking-wider', className)} + className={cn('h-7 gap-1.5 text-[11px] font-bold uppercase tracking-wider', className)} > {copied ? : } {copied ? 'Copied' : 'Copy'} diff --git a/frontend/src/components/inputs/ToolInput.jsx b/frontend/src/components/inputs/ToolInput.jsx index e08db0b..eb0a415 100644 --- a/frontend/src/components/inputs/ToolInput.jsx +++ b/frontend/src/components/inputs/ToolInput.jsx @@ -16,7 +16,7 @@ export function ToolInput({ return (
{label && ( -