diff --git a/src/features/dashboard/components/ProjectCard.test.tsx b/src/features/dashboard/components/ProjectCard.test.tsx
index c4caa159..c77fd12d 100644
--- a/src/features/dashboard/components/ProjectCard.test.tsx
+++ b/src/features/dashboard/components/ProjectCard.test.tsx
@@ -3,6 +3,7 @@ import userEvent from '@testing-library/user-event'
import { describe, expect, it, vi } from 'vitest'
import { renderWithTheme } from '../../../test/renderWithTheme'
import { ProjectCard, type Project } from './ProjectCard'
+import { ProjectCardSkeleton } from './ProjectCardSkeleton'
const project: Project = {
id: 42,
@@ -66,3 +67,67 @@ describe('ProjectCard', () => {
expect(onClick).not.toHaveBeenCalled()
})
})
+
+describe('ProjectCard Layout Parity', () => {
+ it('skeleton and real card share the same outer container structure', () => {
+ const { container: skeletonContainer } = renderWithTheme()
+ const skeletonElement = skeletonContainer.firstElementChild as HTMLElement
+
+ const { container: cardContainer } = renderWithTheme()
+ const cardElement = cardContainer.firstElementChild as HTMLElement
+
+ // Note: Visual regression testing is ideal here, but asserting shared layout classes
+ // is a good unit-test proxy to prevent layout shifts.
+ const sharedClasses = [
+ 'w-full',
+ 'text-left',
+ 'backdrop-blur-[30px]',
+ 'rounded-[18px]',
+ 'border',
+ 'p-5',
+ ]
+ sharedClasses.forEach((cls) => {
+ expect(skeletonElement).toHaveClass(cls)
+ expect(cardElement).toHaveClass(cls)
+ })
+ })
+
+ it('maintains layout bounds even with missing optional fields or long wrapping text', () => {
+ // Edge case: card with a long title/description wrapping to two lines
+ // and missing an optional field (e.g. no tags) vs skeleton's fixed placeholders.
+ const edgeCaseProject = {
+ ...project,
+ name: 'A very very long project name that will likely wrap into multiple lines if the container is narrow enough to force it',
+ description:
+ 'An extremely long description that takes up a lot of vertical space and pushes content down, which might cause layout shift if the skeleton does not account for typical wrapping bounds.',
+ tags: [], // missing optional field
+ }
+
+ const { container: edgeCardContainer } = renderWithTheme(
+
+ )
+ const cardElement = edgeCardContainer.firstElementChild as HTMLElement
+
+ const { container: skeletonContainer } = renderWithTheme()
+ const skeletonElement = skeletonContainer.firstElementChild as HTMLElement
+
+ // Asserting class structure is maintained despite edge case content
+ const sharedClasses = [
+ 'w-full',
+ 'text-left',
+ 'backdrop-blur-[30px]',
+ 'rounded-[18px]',
+ 'border',
+ 'p-5',
+ ]
+ sharedClasses.forEach((cls) => {
+ expect(cardElement).toHaveClass(cls)
+ expect(skeletonElement).toHaveClass(cls)
+ })
+
+ // Check that tags container is present but empty, verifying structural integrity
+ const tagContainer = edgeCardContainer.querySelector('.flex.flex-wrap.gap-1\\.5')
+ expect(tagContainer).toBeInTheDocument()
+ expect(tagContainer?.children).toHaveLength(0)
+ })
+})
diff --git a/src/features/dashboard/components/ProjectCardSkeleton.tsx b/src/features/dashboard/components/ProjectCardSkeleton.tsx
index 9d57dfea..62b7d0e6 100644
--- a/src/features/dashboard/components/ProjectCardSkeleton.tsx
+++ b/src/features/dashboard/components/ProjectCardSkeleton.tsx
@@ -1,16 +1,18 @@
-import { useTheme } from '../../../shared/contexts/ThemeContext';
-import { SkeletonLoader } from '../../../shared/components/SkeletonLoader';
+import { useTheme } from '../../../shared/contexts/ThemeContext'
+import { SkeletonLoader } from '../../../shared/components/SkeletonLoader'
export function ProjectCardSkeleton() {
- const { theme } = useTheme();
- const isDark = theme === 'dark';
+ const { theme } = useTheme()
+ const isDark = theme === 'dark'
return (
+ // CONVENTION: This skeleton must closely mirror the outer structural shape and key
+ // layout classes of ProjectCard.tsx to prevent layout shifts when data loads.
+ // If you change the padding, border radius, or grid structure of ProjectCard,
+ // update this skeleton accordingly.
{/* Icon */}
@@ -20,7 +22,7 @@ export function ProjectCardSkeleton() {
{/* Title */}
-
+
{/* Description */}
@@ -54,19 +56,5 @@ export function ProjectCardSkeleton() {
- );
+ )
}
-
-
-
-
-
-
-
-
-
-
-
-
-
-