Skip to content

Commit 9f07c98

Browse files
authored
feat(frontend): add loaders badges timeline and event links (#1189)
Co-authored-by: Sule Sabira <YOUR_VERIFIED_SABIRA2003_EMAIL>
1 parent d13b742 commit 9f07c98

18 files changed

Lines changed: 3274 additions & 806 deletions

File tree

Lines changed: 255 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,255 @@
1-
import React from "react"
2-
import { render, screen } from "@testing-library/react"
3-
import { Skeleton } from "@/components/ui/skeleton"
4-
5-
describe("Skeleton Component", () => {
6-
it("renders a div with aria-hidden and data-slot attributes", () => {
7-
render(<Skeleton data-testid="skeleton" />)
8-
const el = screen.getByTestId("skeleton")
9-
expect(el).toBeInTheDocument()
10-
expect(el).toHaveAttribute("aria-hidden", "true")
11-
expect(el).toHaveAttribute("data-slot", "skeleton")
12-
})
13-
14-
it("renders the rectangle variant with rounded-md by default", () => {
15-
render(<Skeleton data-testid="rect" />)
16-
expect(screen.getByTestId("rect")).toHaveClass("rounded-md")
17-
})
18-
19-
it("renders the circle variant with rounded-full", () => {
20-
render(<Skeleton variant="circle" data-testid="circle" />)
21-
expect(screen.getByTestId("circle")).toHaveClass("rounded-full")
22-
})
23-
24-
it("renders the text variant with h-4 and w-full", () => {
25-
render(<Skeleton variant="text" data-testid="text" />)
26-
const el = screen.getByTestId("text")
27-
expect(el).toHaveClass("h-4", "w-full")
28-
})
29-
30-
it("applies numeric width and height as pixel inline styles", () => {
31-
render(<Skeleton width={120} height={80} data-testid="sized" />)
32-
expect(screen.getByTestId("sized")).toHaveStyle({ width: "120px", height: "80px" })
33-
})
34-
35-
it("applies string width and height values as-is", () => {
36-
render(<Skeleton width="50%" height="2rem" data-testid="str-sized" />)
37-
expect(screen.getByTestId("str-sized")).toHaveStyle({ width: "50%", height: "2rem" })
38-
})
39-
40-
it("applies the shimmer animation class", () => {
41-
render(<Skeleton data-testid="shimmer" />)
42-
expect(screen.getByTestId("shimmer")).toHaveClass("animate-shimmer")
43-
})
44-
45-
it("forwards ref to the root div", () => {
46-
const ref = React.createRef<HTMLDivElement>()
47-
render(<Skeleton ref={ref} />)
48-
expect(ref.current).not.toBeNull()
49-
expect(ref.current?.tagName).toBe("DIV")
50-
expect(ref.current?.getAttribute("aria-hidden")).toBe("true")
51-
})
52-
53-
it("merges extra className without losing base classes", () => {
54-
render(<Skeleton className="mt-4" data-testid="merged" />)
55-
const el = screen.getByTestId("merged")
56-
expect(el).toHaveClass("mt-4")
57-
expect(el).toHaveClass("rounded-md")
58-
})
59-
60-
it("does not expose a semantic role in the accessibility tree", () => {
61-
render(<Skeleton data-testid="no-role" />)
62-
expect(screen.queryByRole("img")).toBeNull()
63-
expect(screen.queryByRole("status")).toBeNull()
64-
})
65-
})
1+
import React from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
4+
import {
5+
Skeleton,
6+
SkeletonCard,
7+
SkeletonChart,
8+
SkeletonTable,
9+
SkeletonText,
10+
} from '@/components/ui/skeleton';
11+
12+
describe('Skeleton', () => {
13+
it('renders with the expected accessibility attributes', () => {
14+
render(<Skeleton data-testid="skeleton" />);
15+
16+
const skeleton = screen.getByTestId('skeleton');
17+
18+
expect(skeleton).toBeInTheDocument();
19+
expect(skeleton).toHaveAttribute('aria-hidden', 'true');
20+
expect(skeleton).toHaveAttribute('data-slot', 'skeleton');
21+
});
22+
23+
it('uses the rectangle and shimmer variants by default', () => {
24+
render(<Skeleton data-testid="skeleton" />);
25+
26+
expect(screen.getByTestId('skeleton')).toHaveClass('rounded-md', 'animate-shimmer');
27+
});
28+
29+
it('supports pulse animation', () => {
30+
render(<Skeleton data-testid="skeleton" animation="pulse" />);
31+
32+
expect(screen.getByTestId('skeleton')).toHaveClass('animate-pulse');
33+
});
34+
35+
it('supports disabling animation', () => {
36+
render(<Skeleton data-testid="skeleton" animation="none" />);
37+
38+
const skeleton = screen.getByTestId('skeleton');
39+
40+
expect(skeleton).not.toHaveClass('animate-pulse');
41+
expect(skeleton).not.toHaveClass('animate-shimmer');
42+
});
43+
44+
it('supports circle and text variants', () => {
45+
const { rerender } = render(<Skeleton data-testid="skeleton" variant="circle" />);
46+
47+
expect(screen.getByTestId('skeleton')).toHaveClass('rounded-full');
48+
49+
rerender(<Skeleton data-testid="skeleton" variant="text" />);
50+
51+
expect(screen.getByTestId('skeleton')).toHaveClass('h-4', 'w-full', 'rounded');
52+
});
53+
54+
it('converts numeric dimensions to pixels', () => {
55+
render(<Skeleton data-testid="skeleton" width={120} height={80} />);
56+
57+
expect(screen.getByTestId('skeleton')).toHaveStyle({
58+
width: '120px',
59+
height: '80px',
60+
});
61+
});
62+
63+
it('uses string dimensions without changing them', () => {
64+
render(<Skeleton data-testid="skeleton" width="50%" height="2rem" />);
65+
66+
expect(screen.getByTestId('skeleton')).toHaveStyle({
67+
width: '50%',
68+
height: '2rem',
69+
});
70+
});
71+
72+
it('supports named animation speeds', () => {
73+
const { rerender } = render(
74+
<Skeleton data-testid="skeleton" animation="pulse" animationSpeed="fast" />,
75+
);
76+
77+
expect(screen.getByTestId('skeleton')).toHaveStyle({
78+
animationDuration: '700ms',
79+
});
80+
81+
rerender(<Skeleton data-testid="skeleton" animation="pulse" animationSpeed="slow" />);
82+
83+
expect(screen.getByTestId('skeleton')).toHaveStyle({
84+
animationDuration: '2.5s',
85+
});
86+
});
87+
88+
it('supports numeric and custom animation speeds', () => {
89+
const { rerender } = render(<Skeleton data-testid="skeleton" animationSpeed={900} />);
90+
91+
expect(screen.getByTestId('skeleton')).toHaveStyle({
92+
animationDuration: '900ms',
93+
});
94+
95+
rerender(<Skeleton data-testid="skeleton" animationSpeed="1.2s" />);
96+
97+
expect(screen.getByTestId('skeleton')).toHaveStyle({
98+
animationDuration: '1.2s',
99+
});
100+
});
101+
102+
it('forwards its ref', () => {
103+
const ref = React.createRef<HTMLDivElement>();
104+
105+
render(<Skeleton ref={ref} />);
106+
107+
expect(ref.current).toBeInstanceOf(HTMLDivElement);
108+
});
109+
});
110+
111+
describe('SkeletonText', () => {
112+
it('renders the configured number of pulsing lines', () => {
113+
render(<SkeletonText data-testid="skeleton-text" lines={3} />);
114+
115+
const container = screen.getByTestId('skeleton-text');
116+
const lines = container.querySelectorAll('[data-slot="skeleton-text-line"]');
117+
118+
expect(lines).toHaveLength(3);
119+
120+
lines.forEach((line) => {
121+
expect(line).toHaveClass('animate-pulse');
122+
});
123+
});
124+
125+
it('supports custom container and line dimensions', () => {
126+
render(
127+
<SkeletonText
128+
data-testid="skeleton-text"
129+
lines={2}
130+
width={320}
131+
lineHeight={18}
132+
gap={12}
133+
lineWidths={['100%', '45%']}
134+
/>,
135+
);
136+
137+
const container = screen.getByTestId('skeleton-text');
138+
const lines = container.querySelectorAll('[data-slot="skeleton-text-line"]');
139+
140+
expect(container).toHaveStyle({
141+
width: '320px',
142+
gap: '12px',
143+
});
144+
145+
expect(lines[0]).toHaveStyle({
146+
width: '100%',
147+
height: '18px',
148+
});
149+
150+
expect(lines[1]).toHaveStyle({
151+
width: '45%',
152+
height: '18px',
153+
});
154+
});
155+
});
156+
157+
describe('SkeletonCard', () => {
158+
it('renders stacked card placeholders', () => {
159+
render(<SkeletonCard data-testid="skeleton-card" lines={2} showAvatar />);
160+
161+
const card = screen.getByTestId('skeleton-card');
162+
163+
expect(card).toBeInTheDocument();
164+
expect(card.querySelector('[data-slot="skeleton-card-media"]')).toBeInTheDocument();
165+
expect(card.querySelector('[data-slot="skeleton-card-avatar"]')).toBeInTheDocument();
166+
expect(card.querySelector('[data-slot="skeleton-card-title"]')).toBeInTheDocument();
167+
expect(card.querySelectorAll('[data-slot="skeleton-text-line"]')).toHaveLength(2);
168+
});
169+
170+
it('supports configurable card dimensions', () => {
171+
render(
172+
<SkeletonCard data-testid="skeleton-card" width={420} height="24rem" showMedia={false} />,
173+
);
174+
175+
expect(screen.getByTestId('skeleton-card')).toHaveStyle({
176+
width: '420px',
177+
height: '24rem',
178+
});
179+
});
180+
});
181+
182+
describe('SkeletonTable', () => {
183+
it('renders the configured rows and columns', () => {
184+
render(<SkeletonTable data-testid="skeleton-table" rows={4} columns={3} />);
185+
186+
const table = screen.getByTestId('skeleton-table');
187+
188+
expect(table.querySelectorAll('[data-slot="skeleton-table-header-cell"]')).toHaveLength(3);
189+
190+
expect(table.querySelectorAll('[data-slot="skeleton-table-row"]')).toHaveLength(4);
191+
192+
expect(table.querySelectorAll('[data-slot="skeleton-table-cell"]')).toHaveLength(12);
193+
});
194+
195+
it('can render without a header', () => {
196+
render(<SkeletonTable data-testid="skeleton-table" showHeader={false} />);
197+
198+
expect(
199+
screen.getByTestId('skeleton-table').querySelector('[data-slot="skeleton-table-header"]'),
200+
).not.toBeInTheDocument();
201+
});
202+
203+
it('supports configurable table dimensions and speed', () => {
204+
render(
205+
<SkeletonTable data-testid="skeleton-table" width="80%" height={360} animationSpeed="fast" />,
206+
);
207+
208+
const table = screen.getByTestId('skeleton-table');
209+
const firstCell = table.querySelector('[data-slot="skeleton-table-cell"]');
210+
211+
expect(table).toHaveStyle({
212+
width: '80%',
213+
height: '360px',
214+
});
215+
216+
expect(firstCell).toHaveStyle({
217+
animationDuration: '700ms',
218+
});
219+
});
220+
});
221+
222+
describe('SkeletonChart', () => {
223+
it('renders a chart placeholder with configurable bars', () => {
224+
render(<SkeletonChart data-testid="skeleton-chart" bars={6} />);
225+
226+
const chart = screen.getByTestId('skeleton-chart');
227+
228+
expect(chart.querySelectorAll('[data-slot="skeleton-chart-bar"]')).toHaveLength(6);
229+
});
230+
231+
it('supports configurable chart dimensions and animation', () => {
232+
render(
233+
<SkeletonChart
234+
data-testid="skeleton-chart"
235+
width={640}
236+
height="20rem"
237+
animation="shimmer"
238+
animationSpeed="slow"
239+
/>,
240+
);
241+
242+
const chart = screen.getByTestId('skeleton-chart');
243+
const firstBar = chart.querySelector('[data-slot="skeleton-chart-bar"]');
244+
245+
expect(chart).toHaveStyle({
246+
width: '640px',
247+
height: '20rem',
248+
});
249+
250+
expect(firstBar).toHaveClass('animate-shimmer');
251+
expect(firstBar).toHaveStyle({
252+
animationDuration: '2.5s',
253+
});
254+
});
255+
});
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { Skeleton, SkeletonCard, SkeletonTable, SkeletonText } from '@/components/ui/skeleton';
2+
3+
export default function ContractsLoading() {
4+
return (
5+
<div
6+
role="status"
7+
aria-busy="true"
8+
aria-label="Loading contracts"
9+
className="min-h-screen bg-terminal-black p-4 font-terminal-mono sm:p-8"
10+
>
11+
<span className="sr-only">Loading contracts</span>
12+
13+
<div className="mx-auto max-w-7xl space-y-6">
14+
<header className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
15+
<SkeletonText
16+
lines={2}
17+
width={420}
18+
lineHeight={18}
19+
gap={12}
20+
lineWidths={['70%', '100%']}
21+
/>
22+
23+
<div className="flex flex-col gap-2 sm:flex-row">
24+
<Skeleton width={145} height={40} animation="pulse" />
25+
26+
<Skeleton width={130} height={40} animation="pulse" />
27+
28+
<Skeleton width={155} height={40} animation="pulse" />
29+
</div>
30+
</header>
31+
32+
<section className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
33+
{Array.from({ length: 4 }, (_, index) => (
34+
<SkeletonCard
35+
key={index}
36+
showMedia={false}
37+
lines={1}
38+
height={128}
39+
animation="pulse"
40+
animationSpeed="normal"
41+
/>
42+
))}
43+
</section>
44+
45+
<section className="space-y-4 rounded-lg border border-terminal-green/20 p-4">
46+
<SkeletonText lines={1} width={220} lineHeight={20} />
47+
48+
<SkeletonTable
49+
rows={7}
50+
columns={5}
51+
rowHeight={18}
52+
animation="pulse"
53+
animationSpeed="normal"
54+
/>
55+
</section>
56+
</div>
57+
</div>
58+
);
59+
}

0 commit comments

Comments
 (0)