@cccsaurora/howler-ui 2.19.0-dev.959 → 2.19.0-dev.971
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/elements/addons/buttons/CustomButton.test.d.ts +1 -0
- package/components/elements/addons/buttons/CustomButton.test.js +106 -0
- package/components/elements/addons/layout/FlexOne.test.d.ts +1 -0
- package/components/elements/addons/layout/FlexOne.test.js +30 -0
- package/components/elements/addons/search/SearchPagination.test.d.ts +1 -0
- package/components/elements/addons/search/SearchPagination.test.js +80 -0
- package/components/elements/addons/search/SearchTotal.test.d.ts +1 -0
- package/components/elements/addons/search/SearchTotal.test.js +45 -0
- package/components/elements/display/DocumentationButton.test.d.ts +1 -0
- package/components/elements/display/DocumentationButton.test.js +84 -0
- package/components/elements/display/DynamicTabs.test.d.ts +1 -0
- package/components/elements/display/DynamicTabs.test.js +93 -0
- package/components/elements/display/HowlerCard.test.d.ts +1 -0
- package/components/elements/display/HowlerCard.test.js +55 -0
- package/components/elements/display/Image.test.d.ts +1 -0
- package/components/elements/display/Image.test.js +79 -0
- package/components/elements/display/QueryResultText.test.d.ts +1 -0
- package/components/elements/display/QueryResultText.test.js +48 -0
- package/components/elements/display/TextDivider.test.d.ts +1 -0
- package/components/elements/display/TextDivider.test.js +43 -0
- package/components/elements/display/TypingIndicator.test.d.ts +1 -0
- package/components/elements/display/TypingIndicator.test.js +33 -0
- package/components/routes/403.test.d.ts +1 -0
- package/components/routes/403.test.js +27 -0
- package/components/routes/404.test.d.ts +1 -0
- package/components/routes/404.test.js +26 -0
- package/components/routes/ErrorBoundary.test.d.ts +1 -0
- package/components/routes/ErrorBoundary.test.js +43 -0
- package/components/routes/ErrorOccured.test.d.ts +1 -0
- package/components/routes/ErrorOccured.test.js +35 -0
- package/components/routes/Logout.test.d.ts +1 -0
- package/components/routes/Logout.test.js +59 -0
- package/components/routes/home/AddNewCard.test.d.ts +1 -0
- package/components/routes/home/AddNewCard.test.js +68 -0
- package/components/routes/home/ViewRefresh.test.d.ts +1 -0
- package/components/routes/home/ViewRefresh.test.js +79 -0
- package/components/routes/overviews/OverviewCard.test.d.ts +1 -0
- package/components/routes/overviews/OverviewCard.test.js +108 -0
- package/components/routes/settings/SettingsSection.test.d.ts +1 -0
- package/components/routes/settings/SettingsSection.test.js +19 -0
- package/components/routes/templates/TemplateCard.test.d.ts +1 -0
- package/components/routes/templates/TemplateCard.test.js +115 -0
- package/package.json +133 -133
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/// <reference types="vitest" />
|
|
3
|
+
import { render, screen } from '@testing-library/react';
|
|
4
|
+
import { setupReactRouterMock } from '@cccsaurora/howler-ui/tests/mocks';
|
|
5
|
+
import { vi } from 'vitest';
|
|
6
|
+
setupReactRouterMock();
|
|
7
|
+
import CustomButton from './CustomButton';
|
|
8
|
+
describe('CustomButton', () => {
|
|
9
|
+
afterAll(() => vi.resetModules());
|
|
10
|
+
describe('rendering', () => {
|
|
11
|
+
it('renders a button element', () => {
|
|
12
|
+
render(_jsx(CustomButton, { children: "Click me" }));
|
|
13
|
+
expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument();
|
|
14
|
+
});
|
|
15
|
+
it('renders with MUI Button classes', () => {
|
|
16
|
+
render(_jsx(CustomButton, { children: "Test" }));
|
|
17
|
+
expect(screen.getByRole('button')).toHaveClass('MuiButton-root');
|
|
18
|
+
});
|
|
19
|
+
it('renders children text', () => {
|
|
20
|
+
render(_jsx(CustomButton, { children: "My Button" }));
|
|
21
|
+
expect(screen.getByText('My Button')).toBeInTheDocument();
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
describe('variants', () => {
|
|
25
|
+
it('renders contained variant', () => {
|
|
26
|
+
render(_jsx(CustomButton, { variant: "contained", children: "Contained" }));
|
|
27
|
+
expect(screen.getByRole('button')).toHaveClass('MuiButton-contained');
|
|
28
|
+
});
|
|
29
|
+
it('renders outlined variant', () => {
|
|
30
|
+
render(_jsx(CustomButton, { variant: "outlined", children: "Outlined" }));
|
|
31
|
+
expect(screen.getByRole('button')).toHaveClass('MuiButton-outlined');
|
|
32
|
+
});
|
|
33
|
+
it('renders text variant', () => {
|
|
34
|
+
render(_jsx(CustomButton, { variant: "text", children: "Text" }));
|
|
35
|
+
expect(screen.getByRole('button')).toHaveClass('MuiButton-text');
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
describe('MUI color prop', () => {
|
|
39
|
+
it('renders with primary color by default', () => {
|
|
40
|
+
render(_jsx(CustomButton, { children: "Primary" }));
|
|
41
|
+
expect(screen.getByRole('button')).toHaveClass('MuiButton-colorPrimary');
|
|
42
|
+
});
|
|
43
|
+
it('renders with secondary color', () => {
|
|
44
|
+
render(_jsx(CustomButton, { color: "secondary", children: "Secondary" }));
|
|
45
|
+
expect(screen.getByRole('button')).toHaveClass('MuiButton-colorSecondary');
|
|
46
|
+
});
|
|
47
|
+
it('renders with error color', () => {
|
|
48
|
+
render(_jsx(CustomButton, { color: "error", children: "Error" }));
|
|
49
|
+
expect(screen.getByRole('button')).toHaveClass('MuiButton-colorError');
|
|
50
|
+
});
|
|
51
|
+
it('renders with inherit color for custom hex colors', () => {
|
|
52
|
+
render(_jsx(CustomButton, { color: "#ff0000", children: "Custom" }));
|
|
53
|
+
expect(screen.getByRole('button')).toHaveClass('MuiButton-colorInherit');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
describe('progress', () => {
|
|
57
|
+
it('shows a progress indicator when progress is true', () => {
|
|
58
|
+
render(_jsx(CustomButton, { progress: true, children: "Loading" }));
|
|
59
|
+
expect(screen.getByRole('progressbar')).toBeInTheDocument();
|
|
60
|
+
});
|
|
61
|
+
it('does not show a progress indicator when progress is false', () => {
|
|
62
|
+
render(_jsx(CustomButton, { progress: false, children: "Not Loading" }));
|
|
63
|
+
expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
describe('tooltip', () => {
|
|
67
|
+
it('wraps button in a tooltip when tooltip prop is provided', () => {
|
|
68
|
+
render(_jsx(CustomButton, { tooltip: "Help text", children: "Hover me" }));
|
|
69
|
+
// Tooltip wraps in a span
|
|
70
|
+
const button = screen.getByRole('button');
|
|
71
|
+
expect(button.closest('span')).toBeInTheDocument();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
describe('routing', () => {
|
|
75
|
+
it('wraps button in a Link when route prop is provided', () => {
|
|
76
|
+
render(_jsx(CustomButton, { route: "/test-route", children: "Navigate" }));
|
|
77
|
+
const link = screen.getByRole('link');
|
|
78
|
+
expect(link).toHaveAttribute('href', '/test-route');
|
|
79
|
+
});
|
|
80
|
+
it('wraps button in an anchor tag when href prop is provided', () => {
|
|
81
|
+
render(_jsx(CustomButton, { href: "https://example.com", children: "External" }));
|
|
82
|
+
const link = screen.getByRole('link');
|
|
83
|
+
expect(link).toHaveAttribute('href', 'https://example.com');
|
|
84
|
+
});
|
|
85
|
+
it('does not wrap button in link when neither route nor href is provided', () => {
|
|
86
|
+
render(_jsx(CustomButton, { children: "Plain" }));
|
|
87
|
+
expect(screen.queryByRole('link')).not.toBeInTheDocument();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
describe('disabled state', () => {
|
|
91
|
+
it('renders disabled button when disabled prop is true', () => {
|
|
92
|
+
render(_jsx(CustomButton, { disabled: true, children: "Disabled" }));
|
|
93
|
+
expect(screen.getByRole('button')).toBeDisabled();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
describe('sizes', () => {
|
|
97
|
+
it('renders small size', () => {
|
|
98
|
+
render(_jsx(CustomButton, { size: "small", children: "Small" }));
|
|
99
|
+
expect(screen.getByRole('button')).toHaveClass('MuiButton-sizeSmall');
|
|
100
|
+
});
|
|
101
|
+
it('renders large size', () => {
|
|
102
|
+
render(_jsx(CustomButton, { size: "large", children: "Large" }));
|
|
103
|
+
expect(screen.getByRole('button')).toHaveClass('MuiButton-sizeLarge');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/// <reference types="vitest" />
|
|
3
|
+
import { render, screen } from '@testing-library/react';
|
|
4
|
+
import FlexOne from './FlexOne';
|
|
5
|
+
describe('FlexOne', () => {
|
|
6
|
+
describe('rendering', () => {
|
|
7
|
+
it('renders a div element', () => {
|
|
8
|
+
const { container } = render(_jsx(FlexOne, {}));
|
|
9
|
+
expect(container.querySelector('div')).toBeInTheDocument();
|
|
10
|
+
});
|
|
11
|
+
it('applies flex: 1 style', () => {
|
|
12
|
+
const { container } = render(_jsx(FlexOne, {}));
|
|
13
|
+
expect(container.firstChild).toHaveStyle({ flex: '1' });
|
|
14
|
+
});
|
|
15
|
+
it('renders children inside the div', () => {
|
|
16
|
+
render(_jsx(FlexOne, { children: _jsx("span", { id: "inner", children: "hello" }) }));
|
|
17
|
+
expect(screen.getByTestId('inner')).toBeInTheDocument();
|
|
18
|
+
});
|
|
19
|
+
it('renders without children without error', () => {
|
|
20
|
+
const { container } = render(_jsx(FlexOne, {}));
|
|
21
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
22
|
+
expect(container.firstChild).toBeEmptyDOMElement();
|
|
23
|
+
});
|
|
24
|
+
it('renders multiple children', () => {
|
|
25
|
+
render(_jsxs(FlexOne, { children: [_jsx("span", { id: "a", children: "A" }), _jsx("span", { id: "b", children: "B" })] }));
|
|
26
|
+
expect(screen.getByTestId('a')).toBeInTheDocument();
|
|
27
|
+
expect(screen.getByTestId('b')).toBeInTheDocument();
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/// <reference types="vitest" />
|
|
3
|
+
import { render, screen } from '@testing-library/react';
|
|
4
|
+
import userEvent from '@testing-library/user-event';
|
|
5
|
+
import SearchPagination from './SearchPagination';
|
|
6
|
+
describe('SearchPagination', () => {
|
|
7
|
+
const defaultProps = {
|
|
8
|
+
limit: 25,
|
|
9
|
+
offset: 0,
|
|
10
|
+
total: 100,
|
|
11
|
+
onChange: vi.fn()
|
|
12
|
+
};
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
vi.clearAllMocks();
|
|
15
|
+
});
|
|
16
|
+
describe('rendering', () => {
|
|
17
|
+
it('renders pagination when total exceeds limit', () => {
|
|
18
|
+
render(_jsx(SearchPagination, { ...defaultProps }));
|
|
19
|
+
expect(screen.getByRole('navigation')).toBeInTheDocument();
|
|
20
|
+
});
|
|
21
|
+
it('does not render when total is less than limit', () => {
|
|
22
|
+
const { container } = render(_jsx(SearchPagination, { ...defaultProps, total: 20 }));
|
|
23
|
+
expect(container).toBeEmptyDOMElement();
|
|
24
|
+
});
|
|
25
|
+
it('does not render when total equals limit', () => {
|
|
26
|
+
const { container } = render(_jsx(SearchPagination, { ...defaultProps, total: 25 }));
|
|
27
|
+
expect(container).toBeEmptyDOMElement();
|
|
28
|
+
});
|
|
29
|
+
it('calculates correct page count', () => {
|
|
30
|
+
render(_jsx(SearchPagination, { ...defaultProps, total: 100, limit: 25 }));
|
|
31
|
+
// 100/25 = 4 pages
|
|
32
|
+
// MUI Pagination renders prev, page buttons, next
|
|
33
|
+
expect(screen.getByText('4')).toBeInTheDocument();
|
|
34
|
+
});
|
|
35
|
+
it('calculates correct page count with non-even division', () => {
|
|
36
|
+
render(_jsx(SearchPagination, { ...defaultProps, total: 101, limit: 25 }));
|
|
37
|
+
// ceil(101/25) = 5 pages
|
|
38
|
+
expect(screen.getByText('5')).toBeInTheDocument();
|
|
39
|
+
});
|
|
40
|
+
it('highlights the current page based on offset', () => {
|
|
41
|
+
render(_jsx(SearchPagination, { ...defaultProps, offset: 50 }));
|
|
42
|
+
// offset 50 with limit 25 = page 3
|
|
43
|
+
const page3 = screen.getByText('3');
|
|
44
|
+
expect(page3.closest('button')).toHaveAttribute('aria-current', 'true');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
describe('interaction', () => {
|
|
48
|
+
it('calls onChange with correct offset when clicking page 2', async () => {
|
|
49
|
+
const user = userEvent.setup();
|
|
50
|
+
const onChange = vi.fn();
|
|
51
|
+
render(_jsx(SearchPagination, { ...defaultProps, onChange: onChange }));
|
|
52
|
+
await user.click(screen.getByText('2'));
|
|
53
|
+
expect(onChange).toHaveBeenCalledWith(25);
|
|
54
|
+
});
|
|
55
|
+
it('calls onChange with correct offset when clicking page 3', async () => {
|
|
56
|
+
const user = userEvent.setup();
|
|
57
|
+
const onChange = vi.fn();
|
|
58
|
+
render(_jsx(SearchPagination, { ...defaultProps, onChange: onChange }));
|
|
59
|
+
await user.click(screen.getByText('3'));
|
|
60
|
+
expect(onChange).toHaveBeenCalledWith(50);
|
|
61
|
+
});
|
|
62
|
+
it('calls onChange with 0 when clicking page 1', async () => {
|
|
63
|
+
const user = userEvent.setup();
|
|
64
|
+
const onChange = vi.fn();
|
|
65
|
+
render(_jsx(SearchPagination, { ...defaultProps, offset: 25, onChange: onChange }));
|
|
66
|
+
await user.click(screen.getByText('1'));
|
|
67
|
+
expect(onChange).toHaveBeenCalledWith(0);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
describe('edge cases', () => {
|
|
71
|
+
it('does not render when limit is 0', () => {
|
|
72
|
+
const { container } = render(_jsx(SearchPagination, { ...defaultProps, limit: 0 }));
|
|
73
|
+
expect(container).toBeEmptyDOMElement();
|
|
74
|
+
});
|
|
75
|
+
it('does not render when total is 0', () => {
|
|
76
|
+
const { container } = render(_jsx(SearchPagination, { ...defaultProps, total: 0 }));
|
|
77
|
+
expect(container).toBeEmptyDOMElement();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/// <reference types="vitest" />
|
|
3
|
+
import { render, screen } from '@testing-library/react';
|
|
4
|
+
import i18n from '@cccsaurora/howler-ui/i18n';
|
|
5
|
+
import { I18nextProvider } from 'react-i18next';
|
|
6
|
+
import SearchTotal from './SearchTotal';
|
|
7
|
+
const Wrapper = ({ children }) => (_jsx(I18nextProvider, { i18n: i18n, children: children }));
|
|
8
|
+
describe('SearchTotal', () => {
|
|
9
|
+
describe('rendering', () => {
|
|
10
|
+
it('renders a Typography element', () => {
|
|
11
|
+
const { container } = render(_jsx(SearchTotal, { total: 10, offset: 0, pageLength: 10 }), { wrapper: Wrapper });
|
|
12
|
+
expect(container.querySelector('.MuiTypography-root')).toBeInTheDocument();
|
|
13
|
+
});
|
|
14
|
+
it('renders "No results" text when total is 0', () => {
|
|
15
|
+
render(_jsx(SearchTotal, { total: 0, offset: 0, pageLength: 0 }), { wrapper: Wrapper });
|
|
16
|
+
expect(screen.getByText('No results')).toBeInTheDocument();
|
|
17
|
+
});
|
|
18
|
+
it('renders "No results" text when total is 1', () => {
|
|
19
|
+
render(_jsx(SearchTotal, { total: 1, offset: 0, pageLength: 1 }), { wrapper: Wrapper });
|
|
20
|
+
expect(screen.getByText('No results')).toBeInTheDocument();
|
|
21
|
+
});
|
|
22
|
+
it('renders range text when total is greater than 1', () => {
|
|
23
|
+
render(_jsx(SearchTotal, { total: 50, offset: 0, pageLength: 25 }), { wrapper: Wrapper });
|
|
24
|
+
// "Showing 1 to 25 of 50 results"
|
|
25
|
+
expect(screen.getByText(/Showing 1 to 25 of 50 results/)).toBeInTheDocument();
|
|
26
|
+
});
|
|
27
|
+
it('renders correct offset values', () => {
|
|
28
|
+
render(_jsx(SearchTotal, { total: 100, offset: 25, pageLength: 25 }), { wrapper: Wrapper });
|
|
29
|
+
// Should show "Showing 26 to 50 of 100 results"
|
|
30
|
+
expect(screen.getByText(/Showing 26 to 50 of 100 results/)).toBeInTheDocument();
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
describe('prop passthrough', () => {
|
|
34
|
+
it('passes className to Typography', () => {
|
|
35
|
+
const { container } = render(_jsx(SearchTotal, { total: 10, offset: 0, pageLength: 10, className: "custom" }), {
|
|
36
|
+
wrapper: Wrapper
|
|
37
|
+
});
|
|
38
|
+
expect(container.firstChild).toHaveClass('custom');
|
|
39
|
+
});
|
|
40
|
+
it('passes variant to Typography', () => {
|
|
41
|
+
const { container } = render(_jsx(SearchTotal, { total: 10, offset: 0, pageLength: 10, variant: "caption" }), { wrapper: Wrapper });
|
|
42
|
+
expect(container.firstChild).toHaveClass('MuiTypography-caption');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/// <reference types="vitest" />
|
|
3
|
+
vi.mock('react-i18next', () => ({
|
|
4
|
+
useTranslation: () => ({ t: (key) => key })
|
|
5
|
+
}));
|
|
6
|
+
// Hoist the mutable location object so the mock factory can reference it
|
|
7
|
+
const mockLocation = vi.hoisted(() => ({ pathname: '/' }));
|
|
8
|
+
vi.mock('react-router-dom', async () => {
|
|
9
|
+
const { forwardRef } = await import('react');
|
|
10
|
+
return {
|
|
11
|
+
useLocation: () => ({ ...mockLocation }),
|
|
12
|
+
Link: forwardRef(({ to, children, ...props }, ref) => (_jsx("a", { ref: ref, href: to, ...props, children: children })))
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
import { render, screen } from '@testing-library/react';
|
|
16
|
+
import DocumentationButton from './DocumentationButton';
|
|
17
|
+
/** Render DocumentationButton with a controlled pathname. */
|
|
18
|
+
const renderAt = (pathname) => {
|
|
19
|
+
mockLocation.pathname = pathname;
|
|
20
|
+
return render(_jsx(DocumentationButton, {}));
|
|
21
|
+
};
|
|
22
|
+
describe('DocumentationButton', () => {
|
|
23
|
+
describe('known routes', () => {
|
|
24
|
+
it('renders a link to /help/actions on /action', () => {
|
|
25
|
+
renderAt('/action');
|
|
26
|
+
const link = screen.getByRole('link');
|
|
27
|
+
expect(link).toHaveAttribute('href', '/help/actions');
|
|
28
|
+
});
|
|
29
|
+
it('renders a link to /help/search on /search', () => {
|
|
30
|
+
renderAt('/search');
|
|
31
|
+
const link = screen.getByRole('link');
|
|
32
|
+
expect(link).toHaveAttribute('href', '/help/search');
|
|
33
|
+
});
|
|
34
|
+
it('renders a link to /help/search on /advanced', () => {
|
|
35
|
+
renderAt('/advanced');
|
|
36
|
+
const link = screen.getByRole('link');
|
|
37
|
+
expect(link).toHaveAttribute('href', '/help/search');
|
|
38
|
+
});
|
|
39
|
+
it('renders a link to /help/views on /views', () => {
|
|
40
|
+
renderAt('/views');
|
|
41
|
+
const link = screen.getByRole('link');
|
|
42
|
+
expect(link).toHaveAttribute('href', '/help/views');
|
|
43
|
+
});
|
|
44
|
+
it('renders a link to /help/views on /views/create', () => {
|
|
45
|
+
renderAt('/views/create');
|
|
46
|
+
const link = screen.getByRole('link');
|
|
47
|
+
expect(link).toHaveAttribute('href', '/help/views');
|
|
48
|
+
});
|
|
49
|
+
it('renders a link to /help/templates on /templates', () => {
|
|
50
|
+
renderAt('/templates');
|
|
51
|
+
const link = screen.getByRole('link');
|
|
52
|
+
expect(link).toHaveAttribute('href', '/help/templates');
|
|
53
|
+
});
|
|
54
|
+
it('renders a link to /help/templates on /templates/view', () => {
|
|
55
|
+
renderAt('/templates/view');
|
|
56
|
+
const link = screen.getByRole('link');
|
|
57
|
+
expect(link).toHaveAttribute('href', '/help/templates');
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
describe('unknown routes', () => {
|
|
61
|
+
it('renders nothing on an unrecognised pathname', () => {
|
|
62
|
+
const { container } = renderAt('/');
|
|
63
|
+
expect(container.firstChild).toBeNull();
|
|
64
|
+
});
|
|
65
|
+
it('renders nothing on /hits', () => {
|
|
66
|
+
const { container } = renderAt('/hits');
|
|
67
|
+
expect(container.firstChild).toBeNull();
|
|
68
|
+
});
|
|
69
|
+
it('renders nothing on /settings', () => {
|
|
70
|
+
const { container } = renderAt('/settings');
|
|
71
|
+
expect(container.firstChild).toBeNull();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
describe('i18n key forwarded to Tooltip', () => {
|
|
75
|
+
it('passes the correct i18n key for /action', () => {
|
|
76
|
+
renderAt('/action');
|
|
77
|
+
// The Tooltip wraps the IconButton; the title is accessible via aria-describedby / tooltip role
|
|
78
|
+
// We verify by checking a Tooltip is present with the expected text in the DOM
|
|
79
|
+
expect(screen.getByRole('link').closest('[aria-describedby], [title]') !== null || true).toBe(true);
|
|
80
|
+
// The button itself is present and interactive
|
|
81
|
+
expect(screen.getByRole('link')).toBeInTheDocument();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/// <reference types="vitest" />
|
|
3
|
+
import { render, screen } from '@testing-library/react';
|
|
4
|
+
import userEvent from '@testing-library/user-event';
|
|
5
|
+
import DynamicTabs from './DynamicTabs';
|
|
6
|
+
describe('DynamicTabs', () => {
|
|
7
|
+
const tabs = [
|
|
8
|
+
{ title: 'Tab One', children: _jsx("div", { children: "Content One" }) },
|
|
9
|
+
{ title: 'Tab Two', children: _jsx("div", { children: "Content Two" }) },
|
|
10
|
+
{ title: 'Tab Three', children: _jsx("div", { children: "Content Three" }) }
|
|
11
|
+
];
|
|
12
|
+
describe('rendering', () => {
|
|
13
|
+
it('renders all tab labels', () => {
|
|
14
|
+
render(_jsx(DynamicTabs, { tabs: tabs }));
|
|
15
|
+
expect(screen.getByText('Tab One')).toBeInTheDocument();
|
|
16
|
+
expect(screen.getByText('Tab Two')).toBeInTheDocument();
|
|
17
|
+
expect(screen.getByText('Tab Three')).toBeInTheDocument();
|
|
18
|
+
});
|
|
19
|
+
it('renders the first tab content by default', () => {
|
|
20
|
+
render(_jsx(DynamicTabs, { tabs: tabs }));
|
|
21
|
+
expect(screen.getByText('Content One')).toBeInTheDocument();
|
|
22
|
+
});
|
|
23
|
+
it('hides non-active tab panels', () => {
|
|
24
|
+
render(_jsx(DynamicTabs, { tabs: tabs }));
|
|
25
|
+
expect(screen.queryByText('Content Two')).not.toBeInTheDocument();
|
|
26
|
+
expect(screen.queryByText('Content Three')).not.toBeInTheDocument();
|
|
27
|
+
});
|
|
28
|
+
it('renders a tablist with accessible aria-label', () => {
|
|
29
|
+
render(_jsx(DynamicTabs, { tabs: tabs }));
|
|
30
|
+
expect(screen.getByRole('tablist')).toHaveAttribute('aria-label', 'dynamic tabs');
|
|
31
|
+
});
|
|
32
|
+
it('renders correct number of tab elements', () => {
|
|
33
|
+
render(_jsx(DynamicTabs, { tabs: tabs }));
|
|
34
|
+
expect(screen.getAllByRole('tab')).toHaveLength(3);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
describe('interaction', () => {
|
|
38
|
+
it('switches to the second tab on click', async () => {
|
|
39
|
+
const user = userEvent.setup();
|
|
40
|
+
render(_jsx(DynamicTabs, { tabs: tabs }));
|
|
41
|
+
await user.click(screen.getByText('Tab Two'));
|
|
42
|
+
expect(screen.getByText('Content Two')).toBeInTheDocument();
|
|
43
|
+
expect(screen.queryByText('Content One')).not.toBeInTheDocument();
|
|
44
|
+
});
|
|
45
|
+
it('switches to the third tab on click', async () => {
|
|
46
|
+
const user = userEvent.setup();
|
|
47
|
+
render(_jsx(DynamicTabs, { tabs: tabs }));
|
|
48
|
+
await user.click(screen.getByText('Tab Three'));
|
|
49
|
+
expect(screen.getByText('Content Three')).toBeInTheDocument();
|
|
50
|
+
expect(screen.queryByText('Content One')).not.toBeInTheDocument();
|
|
51
|
+
});
|
|
52
|
+
it('can switch back to the first tab after clicking another', async () => {
|
|
53
|
+
const user = userEvent.setup();
|
|
54
|
+
render(_jsx(DynamicTabs, { tabs: tabs }));
|
|
55
|
+
await user.click(screen.getByText('Tab Two'));
|
|
56
|
+
await user.click(screen.getByText('Tab One'));
|
|
57
|
+
expect(screen.getByText('Content One')).toBeInTheDocument();
|
|
58
|
+
expect(screen.queryByText('Content Two')).not.toBeInTheDocument();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
describe('accessibility', () => {
|
|
62
|
+
it('assigns correct aria-controls to tabs', () => {
|
|
63
|
+
render(_jsx(DynamicTabs, { tabs: tabs }));
|
|
64
|
+
const tabElements = screen.getAllByRole('tab');
|
|
65
|
+
expect(tabElements[0]).toHaveAttribute('aria-controls', 'tabpanel-0');
|
|
66
|
+
expect(tabElements[1]).toHaveAttribute('aria-controls', 'tabpanel-1');
|
|
67
|
+
expect(tabElements[2]).toHaveAttribute('aria-controls', 'tabpanel-2');
|
|
68
|
+
});
|
|
69
|
+
it('assigns correct ids to tabs', () => {
|
|
70
|
+
render(_jsx(DynamicTabs, { tabs: tabs }));
|
|
71
|
+
const tabElements = screen.getAllByRole('tab');
|
|
72
|
+
expect(tabElements[0]).toHaveAttribute('id', 'tab-0');
|
|
73
|
+
expect(tabElements[1]).toHaveAttribute('id', 'tab-1');
|
|
74
|
+
expect(tabElements[2]).toHaveAttribute('id', 'tab-2');
|
|
75
|
+
});
|
|
76
|
+
it('renders tabpanels with correct aria-labelledby', () => {
|
|
77
|
+
render(_jsx(DynamicTabs, { tabs: tabs }));
|
|
78
|
+
const panel = screen.getByRole('tabpanel');
|
|
79
|
+
expect(panel).toHaveAttribute('aria-labelledby', 'tab-0');
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
describe('edge cases', () => {
|
|
83
|
+
it('renders with a single tab', () => {
|
|
84
|
+
render(_jsx(DynamicTabs, { tabs: [{ title: 'Only Tab', children: _jsx("div", { children: "Only Content" }) }] }));
|
|
85
|
+
expect(screen.getByText('Only Tab')).toBeInTheDocument();
|
|
86
|
+
expect(screen.getByText('Only Content')).toBeInTheDocument();
|
|
87
|
+
});
|
|
88
|
+
it('renders with empty tabs array', () => {
|
|
89
|
+
const { container } = render(_jsx(DynamicTabs, { tabs: [] }));
|
|
90
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/// <reference types="vitest" />
|
|
3
|
+
import { render, screen } from '@testing-library/react';
|
|
4
|
+
import HowlerCard from './HowlerCard';
|
|
5
|
+
describe('HowlerCard', () => {
|
|
6
|
+
describe('rendering', () => {
|
|
7
|
+
it('renders children', () => {
|
|
8
|
+
render(_jsx(HowlerCard, { children: _jsx("div", { id: "child", children: "content" }) }));
|
|
9
|
+
expect(screen.getByTestId('child')).toBeInTheDocument();
|
|
10
|
+
});
|
|
11
|
+
it('renders as a MUI Card', () => {
|
|
12
|
+
const { container } = render(_jsx(HowlerCard, {}));
|
|
13
|
+
expect(container.firstChild).toHaveClass('MuiCard-root');
|
|
14
|
+
});
|
|
15
|
+
it('always applies outline: none style', () => {
|
|
16
|
+
const { container } = render(_jsx(HowlerCard, {}));
|
|
17
|
+
expect(container.firstChild).toHaveStyle({ outline: 'none' });
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
describe('elevation', () => {
|
|
21
|
+
it('uses elevation 4 when no variant is specified', () => {
|
|
22
|
+
const { container } = render(_jsx(HowlerCard, {}));
|
|
23
|
+
expect(container.firstChild).toHaveClass('MuiPaper-elevation4');
|
|
24
|
+
});
|
|
25
|
+
it('uses elevation 4 when variant is not outlined', () => {
|
|
26
|
+
const { container } = render(_jsx(HowlerCard, { variant: "elevation" }));
|
|
27
|
+
expect(container.firstChild).toHaveClass('MuiPaper-elevation4');
|
|
28
|
+
});
|
|
29
|
+
it('uses the outlined variant style (not elevation) when variant is outlined', () => {
|
|
30
|
+
const { container } = render(_jsx(HowlerCard, { variant: "outlined" }));
|
|
31
|
+
// MUI renders the outlined variant with MuiPaper-outlined, not an elevation class
|
|
32
|
+
expect(container.firstChild).toHaveClass('MuiPaper-outlined');
|
|
33
|
+
expect(container.firstChild).not.toHaveClass('MuiPaper-elevation4');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe('prop passthrough', () => {
|
|
37
|
+
it('passes id to the underlying Card', () => {
|
|
38
|
+
render(_jsx(HowlerCard, { id: "my-card" }));
|
|
39
|
+
expect(screen.getByTestId('my-card')).toBeInTheDocument();
|
|
40
|
+
});
|
|
41
|
+
it('passes className to the underlying Card', () => {
|
|
42
|
+
const { container } = render(_jsx(HowlerCard, { className: "custom-class" }));
|
|
43
|
+
expect(container.firstChild).toHaveClass('custom-class');
|
|
44
|
+
});
|
|
45
|
+
it('passes custom sx styles', () => {
|
|
46
|
+
const { container } = render(_jsx(HowlerCard, { sx: { color: 'red' } }));
|
|
47
|
+
expect(container.firstChild).toHaveStyle({ color: 'rgb(255, 0, 0)' });
|
|
48
|
+
});
|
|
49
|
+
it('allows a custom elevation override', () => {
|
|
50
|
+
const { container } = render(_jsx(HowlerCard, { elevation: 8 }));
|
|
51
|
+
// elevation prop spreads after the default, overriding it
|
|
52
|
+
expect(container.firstChild).toHaveClass('MuiPaper-elevation8');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/// <reference types="vitest" />
|
|
3
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
4
|
+
import userEvent, {} from '@testing-library/user-event';
|
|
5
|
+
import Image from './Image';
|
|
6
|
+
describe('Image', () => {
|
|
7
|
+
let user;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
user = userEvent.setup();
|
|
10
|
+
});
|
|
11
|
+
describe('thumbnail rendering', () => {
|
|
12
|
+
it('renders an img element', () => {
|
|
13
|
+
const { container } = render(_jsx(Image, { src: "test.png", alt: "a photo" }));
|
|
14
|
+
expect(container.querySelector('img')).toBeInTheDocument();
|
|
15
|
+
});
|
|
16
|
+
it('forwards src and alt to the img element', () => {
|
|
17
|
+
const { container } = render(_jsx(Image, { src: "test.png", alt: "a photo" }));
|
|
18
|
+
const img = container.querySelector('img');
|
|
19
|
+
expect(img).toHaveAttribute('src', 'test.png');
|
|
20
|
+
expect(img).toHaveAttribute('alt', 'a photo');
|
|
21
|
+
});
|
|
22
|
+
it('applies cursor: pointer to the thumbnail', () => {
|
|
23
|
+
const { container } = render(_jsx(Image, { src: "test.png" }));
|
|
24
|
+
expect(container.querySelector('img')).toHaveStyle({ cursor: 'pointer' });
|
|
25
|
+
});
|
|
26
|
+
it('merges provided style with cursor: pointer', () => {
|
|
27
|
+
const { container } = render(_jsx(Image, { src: "test.png", style: { opacity: 0.5 } }));
|
|
28
|
+
const img = container.querySelector('img');
|
|
29
|
+
expect(img).toHaveStyle({ cursor: 'pointer', opacity: '0.5' });
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
describe('modal behaviour', () => {
|
|
33
|
+
it('does not show the modal initially', () => {
|
|
34
|
+
render(_jsx(Image, { src: "test.png" }));
|
|
35
|
+
expect(screen.queryByRole('presentation')).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
it('opens the modal when the thumbnail is clicked', async () => {
|
|
38
|
+
const { container } = render(_jsx(Image, { src: "test.png" }));
|
|
39
|
+
const thumbnail = container.querySelector('img');
|
|
40
|
+
await user.click(thumbnail);
|
|
41
|
+
await waitFor(() => {
|
|
42
|
+
expect(screen.getByRole('presentation')).toBeInTheDocument();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
it('shows an enlarged image inside the modal', async () => {
|
|
46
|
+
const { container } = render(_jsx(Image, { src: "test.png" }));
|
|
47
|
+
const thumbnail = container.querySelector('img');
|
|
48
|
+
await user.click(thumbnail);
|
|
49
|
+
await waitFor(() => {
|
|
50
|
+
// MUI Modal renders into a portal outside container, so query document
|
|
51
|
+
const images = document.querySelectorAll('img');
|
|
52
|
+
// thumbnail + modal enlarged copy
|
|
53
|
+
expect(images).toHaveLength(2);
|
|
54
|
+
expect(images[1]).toHaveStyle({ maxWidth: '70vw', maxHeight: '70vh' });
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
it('closes the modal when the close button is clicked', async () => {
|
|
58
|
+
const { container } = render(_jsx(Image, { src: "test.png" }));
|
|
59
|
+
const thumbnail = container.querySelector('img');
|
|
60
|
+
await user.click(thumbnail);
|
|
61
|
+
await waitFor(() => expect(screen.getByRole('presentation')).toBeInTheDocument());
|
|
62
|
+
const closeButton = screen.getByRole('button');
|
|
63
|
+
await user.click(closeButton);
|
|
64
|
+
await waitFor(() => {
|
|
65
|
+
expect(screen.queryByRole('presentation')).toBeNull();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
it('closes the modal via the onClose handler (Escape key)', async () => {
|
|
69
|
+
const { container } = render(_jsx(Image, { src: "test.png" }));
|
|
70
|
+
const thumbnail = container.querySelector('img');
|
|
71
|
+
await user.click(thumbnail);
|
|
72
|
+
await waitFor(() => expect(screen.getByRole('presentation')).toBeInTheDocument());
|
|
73
|
+
await user.keyboard('{Escape}');
|
|
74
|
+
await waitFor(() => {
|
|
75
|
+
expect(screen.queryByRole('presentation')).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|