diff --git a/frontend/__tests__/e2e/pages/Home.spec.ts b/frontend/__tests__/e2e/pages/Home.spec.ts index 4f5aa5ffd9..33caeef0af 100644 --- a/frontend/__tests__/e2e/pages/Home.spec.ts +++ b/frontend/__tests__/e2e/pages/Home.spec.ts @@ -84,4 +84,15 @@ test.describe('Home Page', () => { await expect(page.getByText('Feb 27 — 28, 2025')).toBeVisible() await page.getByRole('button', { name: 'Event 1' }).click() }) + + test('should have truncated text with overflow for all relevant elements', async ({ page }) => { + const truncatedElements = await page.locator('span.truncate').all() + expect(truncatedElements.length).toBeGreaterThan(0) + + for (const element of truncatedElements) { + await expect(element).toHaveCSS('overflow', 'hidden') + await expect(element).toHaveCSS('text-overflow', 'ellipsis') + await expect(element).toHaveCSS('white-space', 'nowrap') + } + }) }) diff --git a/frontend/__tests__/unit/components/TruncatedText.test.tsx b/frontend/__tests__/unit/components/TruncatedText.test.tsx new file mode 100644 index 0000000000..6c9a4559f6 --- /dev/null +++ b/frontend/__tests__/unit/components/TruncatedText.test.tsx @@ -0,0 +1,30 @@ +import { render, screen } from 'wrappers/testUtil' +import { TruncatedText } from 'components/TruncatedText' + +describe('TruncatedText Component', () => { + const longText = 'This is very long text that should be truncated for display.' + + test('renders full text when it fits within the container', () => { + render() + const textElement = screen.getByText('Short text') + expect(textElement).toBeInTheDocument() + expect(textElement).toHaveAttribute('title', 'Short text') + }) + + test('truncates text when it exceeds container width', () => { + render( +
+ +
+ ) + const textElement = screen.getByText(longText) + expect(textElement).toHaveClass('truncate') + expect(textElement).toHaveClass('text-ellipsis') + }) + + test('title attribute is always present', () => { + render() + const textElement = screen.getByText(longText) + expect(textElement).toHaveAttribute('title', longText) + }) +}) diff --git a/frontend/jest.setup.ts b/frontend/jest.setup.ts index 247373e4c6..59e2e8d720 100644 --- a/frontend/jest.setup.ts +++ b/frontend/jest.setup.ts @@ -13,7 +13,6 @@ if (!global.structuredClone) { global.structuredClone = (val) => JSON.parse(JSON.stringify(val)) } -// mock runAnimationFrameCallbacks function for testing beforeAll(() => { if (typeof window !== 'undefined') { jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => { @@ -22,10 +21,16 @@ beforeAll(() => { Object.defineProperty(window, 'runAnimationFrameCallbacks', { value: () => {}, - writable: true, configurable: true, + writable: true, }) } + + global.ResizeObserver = class { + disconnect() {} + observe() {} + unobserve() {} + } }) beforeEach(() => { diff --git a/frontend/src/components/ItemCardList.tsx b/frontend/src/components/ItemCardList.tsx index 7dfc1e18ec..c4fd6ef3c6 100644 --- a/frontend/src/components/ItemCardList.tsx +++ b/frontend/src/components/ItemCardList.tsx @@ -2,6 +2,7 @@ import { JSX } from 'react' import { ProjectIssuesType, ProjectReleaseType } from 'types/project' import { PullRequestsType } from 'types/user' import SecondaryCard from './SecondaryCard' +import { TruncatedText } from './TruncatedText' const ItemCardList = ({ title, @@ -44,7 +45,7 @@ const ItemCardList = ({ href={item?.url} target="_blank" > - {item.title || item.name} + diff --git a/frontend/src/components/RepositoriesCard.tsx b/frontend/src/components/RepositoriesCard.tsx index 16d241d911..83edab903a 100644 --- a/frontend/src/components/RepositoriesCard.tsx +++ b/frontend/src/components/RepositoriesCard.tsx @@ -13,6 +13,7 @@ import type React from 'react' import { useState } from 'react' import { useNavigate } from 'react-router-dom' import { RepositoriesCardProps } from 'types/project' +import { TruncatedText } from './TruncatedText' const RepositoriesCard: React.FC = ({ repositories }) => { const [showAllRepositories, setShowAllRepositories] = useState(false) @@ -59,7 +60,7 @@ const RepositoryItem = ({ details }) => { onClick={handleClick} className="text-start font-semibold text-blue-500 hover:underline dark:text-blue-400" > - {details?.name} +
diff --git a/frontend/src/components/TruncatedText.tsx b/frontend/src/components/TruncatedText.tsx new file mode 100644 index 0000000000..1cf1d60efc --- /dev/null +++ b/frontend/src/components/TruncatedText.tsx @@ -0,0 +1,37 @@ +import { useRef, useEffect, useCallback } from 'react' + +export const TruncatedText = ({ text, className = '' }: { text: string; className?: string }) => { + const textRef = useRef(null) + + const checkTruncation = useCallback(() => { + const element = textRef.current + if (element) { + element.title = text + } + }, [text]) + + useEffect(() => { + checkTruncation() + + const observer = new ResizeObserver(() => checkTruncation()) + if (textRef.current) { + observer.observe(textRef.current) + } + + window.addEventListener('resize', checkTruncation) + + return () => { + observer.disconnect() + window.removeEventListener('resize', checkTruncation) + } + }, [text, checkTruncation]) + + return ( + + {text} + + ) +} diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index ca1b1b93a7..7b82432150 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -29,6 +29,7 @@ import Modal from 'components/Modal' import MultiSearchBar from 'components/MultiSearch' import SecondaryCard from 'components/SecondaryCard' import TopContributors from 'components/ToggleContributors' +import { TruncatedText } from 'components/TruncatedText' import { toaster } from 'components/ui/toaster' export default function Home() { @@ -135,16 +136,16 @@ export default function Home() { />
- +
{data.upcomingEvents.map((event: EventType, index: number) => ( -
+
@@ -173,13 +174,13 @@ export default function Home() {
- +
{data.recentChapters.map((chapter) => (

- {chapter.name} +

@@ -202,15 +203,15 @@ export default function Home() { ))}
- +
{data.recentProjects.map((project) => (
-

- - {project.name} - -

+ +

+ +

+
@@ -280,22 +281,22 @@ export default function Home() { )} />
- +
{data.recentPosts.map((post) => (