Skip to content

Commit dca3fa6

Browse files
committed
Add test for almost all the components
1 parent 0818f82 commit dca3fa6

13 files changed

+1403
-0
lines changed

src/App.test.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { render, screen } from '@testing-library/react';
2+
import { HelmetProvider } from '@dr.pogodin/react-helmet';
3+
import { describe, expect, it, vi } from 'vitest';
4+
import App from './App';
5+
6+
// Mock child components to simplify testing
7+
vi.mock('./components/Root', () => ({
8+
default: () => <div data-testid="root">Root Layout</div>,
9+
}));
10+
11+
vi.mock('./components/Home', () => ({
12+
default: () => <div data-testid="home">Home Page</div>,
13+
}));
14+
15+
vi.mock('./components/Search', () => ({
16+
default: () => <div data-testid="search">Search Page</div>,
17+
}));
18+
19+
vi.mock('./components/Buckets', () => ({
20+
default: () => <div data-testid="buckets">Buckets Page</div>,
21+
}));
22+
23+
describe('App', () => {
24+
it('renders the application with Helmet', () => {
25+
render(
26+
<HelmetProvider>
27+
<App />
28+
</HelmetProvider>
29+
);
30+
31+
// The Root component should be rendered
32+
expect(screen.getByTestId('root')).toBeInTheDocument();
33+
});
34+
});
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { render } from '@testing-library/react';
2+
import { describe, expect, it } from 'vitest';
3+
import BucketTypeIcon from './BucketTypeIcon';
4+
5+
describe('BucketTypeIcon', () => {
6+
describe('icon type', () => {
7+
it('renders official bucket icon (verified) for official bucket', () => {
8+
const { container } = render(<BucketTypeIcon official={true} />);
9+
10+
const svg = container.querySelector('svg');
11+
expect(svg).toBeInTheDocument();
12+
expect(svg).toHaveAttribute('color', '#2E86C1');
13+
});
14+
15+
it('renders popular community bucket icon for community bucket with 50+ stars', () => {
16+
const { container } = render(<BucketTypeIcon official={false} stars={50} />);
17+
18+
const svg = container.querySelector('svg');
19+
expect(svg).toBeInTheDocument();
20+
expect(svg).toHaveAttribute('color', '#6B9DBF');
21+
});
22+
23+
it('renders regular community bucket icon for community bucket with less than 50 stars', () => {
24+
const { container } = render(<BucketTypeIcon official={false} stars={25} />);
25+
26+
const svg = container.querySelector('svg');
27+
expect(svg).toBeInTheDocument();
28+
expect(svg).toHaveAttribute('color', '#CCCCCC');
29+
});
30+
31+
it('renders regular community bucket icon when stars is 0', () => {
32+
const { container } = render(<BucketTypeIcon official={false} stars={0} />);
33+
34+
const svg = container.querySelector('svg');
35+
expect(svg).toBeInTheDocument();
36+
expect(svg).toHaveAttribute('color', '#CCCCCC');
37+
});
38+
39+
it('renders regular community bucket icon when stars is not provided', () => {
40+
const { container } = render(<BucketTypeIcon official={false} />);
41+
42+
const svg = container.querySelector('svg');
43+
expect(svg).toBeInTheDocument();
44+
expect(svg).toHaveAttribute('color', '#CCCCCC');
45+
});
46+
});
47+
48+
describe('tooltip configuration', () => {
49+
it('renders with tooltip wrapper by default', () => {
50+
const { container } = render(<BucketTypeIcon official={true} />);
51+
52+
// The component wraps the icon in an OverlayTrigger span
53+
const spanWrapper = container.querySelector('span');
54+
expect(spanWrapper).toBeInTheDocument();
55+
});
56+
57+
it('renders without tooltip content when showTooltip is false', () => {
58+
const { container } = render(<BucketTypeIcon official={true} showTooltip={false} />);
59+
60+
// Icon should still render
61+
const svg = container.querySelector('svg');
62+
expect(svg).toBeInTheDocument();
63+
});
64+
65+
it('renders icon when stars is provided', () => {
66+
const { container } = render(<BucketTypeIcon official={false} stars={100} />);
67+
68+
const svg = container.querySelector('svg');
69+
expect(svg).toBeInTheDocument();
70+
});
71+
72+
it('renders icon when stars is not provided', () => {
73+
const { container } = render(<BucketTypeIcon official={true} />);
74+
75+
const svg = container.querySelector('svg');
76+
expect(svg).toBeInTheDocument();
77+
});
78+
});
79+
80+
describe('props spreading', () => {
81+
it('passes additional props to the icon', () => {
82+
const { container } = render(
83+
<BucketTypeIcon official={true} data-testid="bucket-icon" className="custom-class" />
84+
);
85+
86+
const svg = container.querySelector('svg');
87+
expect(svg).toHaveAttribute('data-testid', 'bucket-icon');
88+
expect(svg).toHaveClass('custom-class');
89+
});
90+
});
91+
});

src/components/Buckets.test.tsx

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import { render, screen, waitFor } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
import { http, HttpResponse } from 'msw';
4+
import { MemoryRouter } from 'react-router-dom';
5+
import { describe, expect, it, vi, beforeEach } from 'vitest';
6+
import { HelmetProvider } from '@dr.pogodin/react-helmet';
7+
import Buckets from './Buckets';
8+
import { server, SEARCH_API_URL } from '../__tests__/server';
9+
10+
// Mock bucket data response
11+
const mockOfficialBucketsResponse = {
12+
'@odata.count': 2,
13+
'@search.facets': {
14+
'Metadata/Repository': [
15+
{ value: 'ScoopInstaller/Main', count: 1234 },
16+
{ value: 'ScoopInstaller/Extras', count: 890 },
17+
],
18+
},
19+
value: [],
20+
};
21+
22+
const mockCommunityBucketsResponse = {
23+
'@odata.count': 1,
24+
'@search.facets': {
25+
'Metadata/Repository': [
26+
{ value: 'user/community-bucket', count: 456 },
27+
],
28+
},
29+
value: [],
30+
};
31+
32+
describe('Buckets', () => {
33+
const renderBuckets = () => {
34+
return render(
35+
<HelmetProvider>
36+
<MemoryRouter>
37+
<Buckets />
38+
</MemoryRouter>
39+
</HelmetProvider>
40+
);
41+
};
42+
43+
beforeEach(() => {
44+
// Setup MSW handlers for bucket fetching
45+
server.use(
46+
http.post(`${SEARCH_API_URL}/search`, async ({ request }) => {
47+
const body = (await request.json()) as { filter?: string };
48+
49+
// Return official buckets for official filter
50+
if (body.filter?.includes('1')) {
51+
return HttpResponse.json(mockOfficialBucketsResponse);
52+
}
53+
54+
// Return community buckets for non-official filter
55+
return HttpResponse.json(mockCommunityBucketsResponse);
56+
})
57+
);
58+
});
59+
60+
describe('rendering', () => {
61+
it('shows loading state initially', () => {
62+
renderBuckets();
63+
64+
expect(screen.getByText(/Searching for buckets/)).toBeInTheDocument();
65+
});
66+
67+
it('displays bucket results after loading', async () => {
68+
renderBuckets();
69+
70+
await waitFor(() => {
71+
expect(screen.getByText(/Found 3 buckets/)).toBeInTheDocument();
72+
});
73+
74+
// Check that bucket names are displayed
75+
expect(screen.getByText('ScoopInstaller/Main')).toBeInTheDocument();
76+
expect(screen.getByText('ScoopInstaller/Extras')).toBeInTheDocument();
77+
expect(screen.getByText('user/community-bucket')).toBeInTheDocument();
78+
});
79+
80+
it('displays manifest counts', async () => {
81+
renderBuckets();
82+
83+
await waitFor(() => {
84+
expect(screen.getByText('1234')).toBeInTheDocument();
85+
});
86+
87+
expect(screen.getByText('890')).toBeInTheDocument();
88+
expect(screen.getByText('456')).toBeInTheDocument();
89+
});
90+
91+
it('renders table headers', async () => {
92+
renderBuckets();
93+
94+
await waitFor(() => {
95+
expect(screen.getByRole('columnheader', { name: 'Bucket' })).toBeInTheDocument();
96+
});
97+
98+
expect(screen.getByRole('columnheader', { name: 'Manifests' })).toBeInTheDocument();
99+
});
100+
});
101+
102+
describe('sorting', () => {
103+
it('renders sort dropdown with options', async () => {
104+
renderBuckets();
105+
106+
await waitFor(() => {
107+
expect(screen.getByText(/Found 3 buckets/)).toBeInTheDocument();
108+
});
109+
110+
const select = screen.getByRole('combobox');
111+
expect(select).toBeInTheDocument();
112+
// Check that the options exist
113+
expect(screen.getByRole('option', { name: 'Default' })).toBeInTheDocument();
114+
expect(screen.getByRole('option', { name: 'Name' })).toBeInTheDocument();
115+
expect(screen.getByRole('option', { name: 'Manifests' })).toBeInTheDocument();
116+
});
117+
118+
it('sorts by name when Name option is selected', async () => {
119+
const user = userEvent.setup();
120+
renderBuckets();
121+
122+
await waitFor(() => {
123+
expect(screen.getByText(/Found 3 buckets/)).toBeInTheDocument();
124+
});
125+
126+
const select = screen.getByRole('combobox');
127+
await user.selectOptions(select, 'Name');
128+
129+
// After sorting by name, check order
130+
const rows = screen.getAllByRole('row');
131+
expect(rows.length).toBeGreaterThan(1);
132+
});
133+
134+
it('sorts by manifests when Manifests option is selected', async () => {
135+
const user = userEvent.setup();
136+
renderBuckets();
137+
138+
await waitFor(() => {
139+
expect(screen.getByText(/Found 3 buckets/)).toBeInTheDocument();
140+
});
141+
142+
const select = screen.getByRole('combobox');
143+
await user.selectOptions(select, 'Manifests');
144+
145+
// After sorting by manifests, check that items are rendered
146+
const rows = screen.getAllByRole('row');
147+
expect(rows.length).toBeGreaterThan(1);
148+
});
149+
});
150+
151+
describe('error handling', () => {
152+
it('handles API errors gracefully', async () => {
153+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
154+
155+
server.use(
156+
http.post(`${SEARCH_API_URL}/search`, () => {
157+
return HttpResponse.error();
158+
})
159+
);
160+
161+
renderBuckets();
162+
163+
await waitFor(() => {
164+
expect(consoleSpy).toHaveBeenCalled();
165+
});
166+
167+
consoleSpy.mockRestore();
168+
});
169+
});
170+
171+
describe('links', () => {
172+
it('creates correct link for official buckets', async () => {
173+
renderBuckets();
174+
175+
await waitFor(() => {
176+
expect(screen.getByText('ScoopInstaller/Main')).toBeInTheDocument();
177+
});
178+
179+
const link = screen.getByRole('link', { name: /ScoopInstaller\/Main/i });
180+
expect(link).toHaveAttribute('href', expect.stringContaining('/apps'));
181+
});
182+
183+
it('creates correct link for community buckets with o=false param', async () => {
184+
renderBuckets();
185+
186+
await waitFor(() => {
187+
expect(screen.getByText('user/community-bucket')).toBeInTheDocument();
188+
});
189+
190+
const link = screen.getByRole('link', { name: /user\/community-bucket/i });
191+
expect(link).toHaveAttribute('href', expect.stringContaining('o=false'));
192+
});
193+
});
194+
});
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { act, fireEvent, render, screen } from '@testing-library/react';
2+
import type { ComponentProps } from 'react';
3+
import { describe, expect, it, vi } from 'vitest';
4+
import CopyToClipboardButton from './CopyToClipboardButton';
5+
6+
describe('CopyToClipboardButton', () => {
7+
const renderButton = (overrides: Partial<ComponentProps<typeof CopyToClipboardButton>> = {}) => {
8+
const props = {
9+
title: 'Copy command',
10+
variant: 'outline-primary',
11+
className: 'copy-button',
12+
onClick: vi.fn(),
13+
...overrides,
14+
};
15+
16+
return {
17+
...render(<CopyToClipboardButton {...props} />),
18+
props,
19+
};
20+
};
21+
22+
it('invokes the provided onClick and disables itself temporarily', () => {
23+
vi.useFakeTimers();
24+
const { props } = renderButton();
25+
const button = screen.getByRole('button', { name: /copy command/i });
26+
27+
fireEvent.click(button);
28+
29+
expect(props.onClick).toHaveBeenCalledTimes(1);
30+
expect(button).toBeDisabled();
31+
32+
act(() => {
33+
vi.advanceTimersByTime(1500);
34+
});
35+
36+
expect(button).not.toBeDisabled();
37+
vi.useRealTimers();
38+
});
39+
40+
it('allows subsequent copies after the notification delay', () => {
41+
vi.useFakeTimers();
42+
const { props } = renderButton();
43+
const button = screen.getByRole('button', { name: /copy command/i });
44+
45+
fireEvent.click(button);
46+
47+
act(() => {
48+
vi.advanceTimersByTime(1500);
49+
});
50+
51+
fireEvent.click(button);
52+
expect(props.onClick).toHaveBeenCalledTimes(2);
53+
vi.useRealTimers();
54+
});
55+
});

0 commit comments

Comments
 (0)