@tpzdsp/next-toolkit 1.2.9 → 1.4.0
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/package.json +14 -1
- package/src/components/Card/Card.stories.tsx +21 -0
- package/src/components/Card/Card.test.tsx +46 -7
- package/src/components/Card/Card.tsx +1 -1
- package/src/components/ErrorBoundary/ErrorBoundary.stories.tsx +89 -0
- package/src/components/ErrorBoundary/ErrorBoundary.test.tsx +75 -0
- package/src/components/ErrorBoundary/ErrorBoundary.tsx +28 -0
- package/src/components/ErrorBoundary/ErrorFallback.stories.tsx +70 -0
- package/src/components/ErrorBoundary/ErrorFallback.test.tsx +54 -0
- package/src/components/ErrorBoundary/ErrorFallback.tsx +30 -0
- package/src/components/Paragraph/Paragraph.tsx +9 -5
- package/src/components/backToTop/BackToTop.stories.tsx +409 -0
- package/src/components/backToTop/BackToTop.test.tsx +57 -0
- package/src/components/backToTop/BackToTop.tsx +131 -0
- package/src/components/chip/Chip.stories.tsx +40 -0
- package/src/components/chip/Chip.test.tsx +31 -0
- package/src/components/chip/Chip.tsx +20 -0
- package/src/components/cookieBanner/CookieBanner.stories.tsx +258 -0
- package/src/components/cookieBanner/CookieBanner.test.tsx +68 -0
- package/src/components/cookieBanner/CookieBanner.tsx +73 -0
- package/src/components/form/Input.stories.tsx +435 -0
- package/src/components/form/Input.test.tsx +214 -0
- package/src/components/form/Input.tsx +24 -0
- package/src/components/form/TextArea.stories.tsx +465 -0
- package/src/components/form/TextArea.test.tsx +236 -0
- package/src/components/form/TextArea.tsx +24 -0
- package/src/components/googleAnalytics/GlobalVars.tsx +31 -0
- package/src/components/googleAnalytics/GoogleAnalytics.tsx +15 -0
- package/src/components/index.ts +15 -0
- package/src/components/skipLink/SkipLink.stories.tsx +346 -0
- package/src/components/skipLink/SkipLink.test.tsx +22 -0
- package/src/components/skipLink/SkipLink.tsx +19 -0
- package/src/errors/ApiError.ts +71 -0
- package/src/errors/index.ts +1 -0
- package/src/utils/utils.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tpzdsp/next-toolkit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "A reusable React component library for Next.js applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -53,6 +53,11 @@
|
|
|
53
53
|
"import": "./src/map/index.ts",
|
|
54
54
|
"require": "./src/map/index.ts"
|
|
55
55
|
},
|
|
56
|
+
"./errors": {
|
|
57
|
+
"types": "./src/errors/index.ts",
|
|
58
|
+
"import": "./src/errors/index.ts",
|
|
59
|
+
"require": "./src/errors/index.ts"
|
|
60
|
+
},
|
|
56
61
|
"./types": {
|
|
57
62
|
"types": "./src/types/index.ts",
|
|
58
63
|
"import": "./src/types/index.ts",
|
|
@@ -111,6 +116,7 @@
|
|
|
111
116
|
"@storybook/react": "8.6.14",
|
|
112
117
|
"@storybook/react-vite": "8.6.14",
|
|
113
118
|
"@storybook/test": "^8.6.14",
|
|
119
|
+
"@storybook/types": "8.6.14",
|
|
114
120
|
"@tailwindcss/typography": "^0.5.16",
|
|
115
121
|
"@testing-library/dom": "^10.4.0",
|
|
116
122
|
"@testing-library/jest-dom": "^6.6.3",
|
|
@@ -129,6 +135,8 @@
|
|
|
129
135
|
"@vitest/coverage-v8": "^3.2.4",
|
|
130
136
|
"@vitest/eslint-plugin": "^1.3.4",
|
|
131
137
|
"autoprefixer": "^10.4.21",
|
|
138
|
+
"buffer": "^6.0.3",
|
|
139
|
+
"crypto-browserify": "^3.12.1",
|
|
132
140
|
"eslint": "^9.30.1",
|
|
133
141
|
"eslint-config-prettier": "^10.1.8",
|
|
134
142
|
"eslint-import-resolver-alias": "^1.1.2",
|
|
@@ -155,19 +163,23 @@
|
|
|
155
163
|
"prettier": "^3.6.2",
|
|
156
164
|
"prettier-plugin-classnames": "^0.8.1",
|
|
157
165
|
"prettier-plugin-tailwindcss": "^0.6.14",
|
|
166
|
+
"process": "^0.11.10",
|
|
158
167
|
"proj4": "^2.19.10",
|
|
159
168
|
"react": "^19.1.0",
|
|
160
169
|
"react-dom": "^19.1.0",
|
|
170
|
+
"react-error-boundary": "^6.0.0",
|
|
161
171
|
"react-icons": "^5.5.0",
|
|
162
172
|
"react-select": "^5.10.2",
|
|
163
173
|
"react-select-event": "^5.5.1",
|
|
164
174
|
"rollup-plugin-peer-deps-external": "^2.2.4",
|
|
165
175
|
"semantic-release": "^24.2.7",
|
|
166
176
|
"storybook": "8.6.14",
|
|
177
|
+
"stream-browserify": "^3.0.0",
|
|
167
178
|
"tailwind-merge": "^3.3.1",
|
|
168
179
|
"tailwindcss": "^3.4.16",
|
|
169
180
|
"typescript": "~5.8.3",
|
|
170
181
|
"typescript-eslint": "^8.35.1",
|
|
182
|
+
"util": "^0.12.5",
|
|
171
183
|
"vite": "^7.0.4",
|
|
172
184
|
"vite-plugin-dts": "^4.5.4",
|
|
173
185
|
"vitest": "^3.2.4"
|
|
@@ -187,6 +199,7 @@
|
|
|
187
199
|
"proj4": "^2.19.10",
|
|
188
200
|
"react": "^19.1.0",
|
|
189
201
|
"react-dom": "^19.1.0",
|
|
202
|
+
"react-error-boundary": "^6.0.0",
|
|
190
203
|
"react-icons": "^5.5.0"
|
|
191
204
|
},
|
|
192
205
|
"peerDependenciesMeta": {
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { FaChevronRight } from 'react-icons/fa6';
|
|
2
|
+
|
|
1
3
|
/* eslint-disable storybook/no-renderer-packages */
|
|
2
4
|
import type { Meta, StoryFn } from '@storybook/react';
|
|
3
5
|
|
|
4
6
|
import { Card, type CardProps } from './Card';
|
|
7
|
+
import { Paragraph } from '../Paragraph/Paragraph';
|
|
5
8
|
|
|
6
9
|
export default {
|
|
7
10
|
children: 'Card',
|
|
@@ -33,3 +36,21 @@ ImageAndText.args = {
|
|
|
33
36
|
</div>
|
|
34
37
|
),
|
|
35
38
|
};
|
|
39
|
+
|
|
40
|
+
export const WithLink = Template.bind({});
|
|
41
|
+
WithLink.args = {
|
|
42
|
+
children: (
|
|
43
|
+
<>
|
|
44
|
+
<a
|
|
45
|
+
href="https://google.co.uk"
|
|
46
|
+
className="mb-4 flex flex-nowrap items-center gap-2 justify-between font-bold"
|
|
47
|
+
>
|
|
48
|
+
<strong>title</strong>
|
|
49
|
+
|
|
50
|
+
<FaChevronRight className="text-base" />
|
|
51
|
+
</a>
|
|
52
|
+
|
|
53
|
+
<Paragraph>Some descriptive text</Paragraph>
|
|
54
|
+
</>
|
|
55
|
+
),
|
|
56
|
+
};
|
|
@@ -1,12 +1,51 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
|
|
3
1
|
import { Card } from './Card';
|
|
4
|
-
import { render, screen } from '../../test/renderers';
|
|
2
|
+
import { render, screen, userEvent, within } from '../../test/renderers';
|
|
3
|
+
|
|
4
|
+
describe('Card Component', () => {
|
|
5
|
+
it('renders children correctly', () => {
|
|
6
|
+
render(
|
|
7
|
+
<Card>
|
|
8
|
+
<p>Hello, World!</p>
|
|
9
|
+
</Card>,
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
expect(within(screen.getByRole('article')).getByText('Hello, World!')).toBeInTheDocument();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('renders a link inside the card and handles click', async () => {
|
|
16
|
+
const user = userEvent.setup();
|
|
17
|
+
const handleClick = vi.fn();
|
|
18
|
+
|
|
19
|
+
render(
|
|
20
|
+
<Card>
|
|
21
|
+
<a href="/some-path" onClick={handleClick}>
|
|
22
|
+
Go to page
|
|
23
|
+
</a>
|
|
24
|
+
</Card>,
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const link = screen.getByRole('link', { name: /go to page/i });
|
|
28
|
+
|
|
29
|
+
expect(link).toBeInTheDocument();
|
|
30
|
+
|
|
31
|
+
await user.click(link);
|
|
32
|
+
expect(handleClick).toHaveBeenCalled();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('merges custom className correctly', () => {
|
|
36
|
+
render(<Card className="bg-red-500 rounded-lg">Custom</Card>);
|
|
37
|
+
const article = screen.getByRole('article');
|
|
38
|
+
|
|
39
|
+
expect(article).toHaveClass('bg-red-500');
|
|
40
|
+
expect(article).toHaveClass('rounded-lg');
|
|
41
|
+
});
|
|
5
42
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
43
|
+
it('overrides conflicting className using twMerge', () => {
|
|
44
|
+
render(<Card className="pt-4">Override Padding</Card>);
|
|
45
|
+
const article = screen.getByRole('article');
|
|
9
46
|
|
|
10
|
-
|
|
47
|
+
// Should NOT have original 'pt-[12px]' due to twMerge override
|
|
48
|
+
expect(article?.className).not.toMatch(/pt-\[12px\]/);
|
|
49
|
+
expect(article).toHaveClass('pt-4');
|
|
11
50
|
});
|
|
12
51
|
});
|
|
@@ -9,7 +9,7 @@ export const Card = ({ className, children }: CardProps) => {
|
|
|
9
9
|
return (
|
|
10
10
|
<article
|
|
11
11
|
className={twMerge(
|
|
12
|
-
'
|
|
12
|
+
'h-full flex flex-col pt-[12px] px-[4px] mx-3 border-t border-slate-500',
|
|
13
13
|
className,
|
|
14
14
|
)}
|
|
15
15
|
>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/* eslint-disable storybook/no-renderer-packages */
|
|
2
|
+
import type { FallbackProps } from 'react-error-boundary';
|
|
3
|
+
|
|
4
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
5
|
+
|
|
6
|
+
import { ErrorBoundary } from './ErrorBoundary';
|
|
7
|
+
|
|
8
|
+
// Component that intentionally throws an error
|
|
9
|
+
const Bomb = () => {
|
|
10
|
+
throw new Error('Boom!');
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Simple custom fallback for demonstration
|
|
14
|
+
const CustomFallback = ({ error, resetErrorBoundary }: FallbackProps) => (
|
|
15
|
+
<div role="alert" className="p-4 border border-red-300 rounded-lg bg-red-50">
|
|
16
|
+
<p className="font-medium text-red-700">Custom Fallback:</p>
|
|
17
|
+
|
|
18
|
+
<p className="text-red-600">{error.message}</p>
|
|
19
|
+
|
|
20
|
+
<button
|
|
21
|
+
className="px-3 py-1 mt-2 text-white bg-red-600 rounded hover:bg-red-700"
|
|
22
|
+
onClick={resetErrorBoundary}
|
|
23
|
+
>
|
|
24
|
+
Try Again
|
|
25
|
+
</button>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const meta = {
|
|
30
|
+
title: 'Components/ErrorBoundary',
|
|
31
|
+
component: ErrorBoundary,
|
|
32
|
+
parameters: {
|
|
33
|
+
layout: 'centered',
|
|
34
|
+
docs: {
|
|
35
|
+
description: {
|
|
36
|
+
component:
|
|
37
|
+
'A wrapper around `react-error-boundary` that catches errors in the component tree and renders a fallback UI.',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
tags: ['autodocs'],
|
|
42
|
+
} satisfies Meta<typeof ErrorBoundary>;
|
|
43
|
+
|
|
44
|
+
export default meta;
|
|
45
|
+
type Story = StoryObj<typeof meta>;
|
|
46
|
+
|
|
47
|
+
export const Default: Story = {
|
|
48
|
+
args: {
|
|
49
|
+
children: (
|
|
50
|
+
<div className="p-4 text-green-800 border rounded-md bg-green-50">
|
|
51
|
+
<p>No errors here — everything renders normally.</p>
|
|
52
|
+
</div>
|
|
53
|
+
),
|
|
54
|
+
},
|
|
55
|
+
parameters: {
|
|
56
|
+
docs: {
|
|
57
|
+
description: {
|
|
58
|
+
story: 'Default behavior — children render normally when no error is thrown.',
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const WithError: Story = {
|
|
65
|
+
args: {
|
|
66
|
+
children: <Bomb />,
|
|
67
|
+
},
|
|
68
|
+
parameters: {
|
|
69
|
+
docs: {
|
|
70
|
+
description: {
|
|
71
|
+
story: 'When a child throws an error, the default fallback UI is displayed.',
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const WithCustomFallback: Story = {
|
|
78
|
+
args: {
|
|
79
|
+
children: <Bomb />,
|
|
80
|
+
fallback: CustomFallback,
|
|
81
|
+
},
|
|
82
|
+
parameters: {
|
|
83
|
+
docs: {
|
|
84
|
+
description: {
|
|
85
|
+
story: 'ErrorBoundary can render a custom fallback component when errors occur.',
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { FallbackProps } from 'react-error-boundary';
|
|
2
|
+
|
|
3
|
+
import { ErrorBoundary } from './ErrorBoundary';
|
|
4
|
+
import { render, screen, userEvent } from '../../test/renderers';
|
|
5
|
+
import { identityFn } from '../../utils/utils';
|
|
6
|
+
|
|
7
|
+
// Component that always throws
|
|
8
|
+
const Bomb = () => {
|
|
9
|
+
throw new Error('Boom!');
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// Custom fallback with button for reset
|
|
13
|
+
const CustomFallback = ({ error, resetErrorBoundary }: FallbackProps) => (
|
|
14
|
+
<div role="alert">
|
|
15
|
+
<p>Custom fallback: {error.message}</p>
|
|
16
|
+
|
|
17
|
+
<button onClick={resetErrorBoundary}>Try again</button>
|
|
18
|
+
</div>
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
describe('ErrorBoundary', () => {
|
|
22
|
+
it('renders children when no error is thrown', () => {
|
|
23
|
+
render(
|
|
24
|
+
<ErrorBoundary>
|
|
25
|
+
<p>Safe content</p>
|
|
26
|
+
</ErrorBoundary>,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
expect(screen.getByRole('paragraph')).toHaveTextContent('Safe content');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('renders default ErrorFallback when an error is thrown', () => {
|
|
33
|
+
render(
|
|
34
|
+
<ErrorBoundary>
|
|
35
|
+
<Bomb />
|
|
36
|
+
</ErrorBoundary>,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
expect(screen.getByRole('alert')).toBeInTheDocument();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('calls console.error when an error is thrown', () => {
|
|
43
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(identityFn);
|
|
44
|
+
|
|
45
|
+
render(
|
|
46
|
+
<ErrorBoundary>
|
|
47
|
+
<Bomb />
|
|
48
|
+
</ErrorBoundary>,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
expect(screen.getByRole('alert')).toBeInTheDocument();
|
|
52
|
+
expect(consoleSpy).toHaveBeenCalled();
|
|
53
|
+
|
|
54
|
+
consoleSpy.mockRestore();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('supports a custom fallback component with reset', async () => {
|
|
58
|
+
const user = userEvent.setup();
|
|
59
|
+
|
|
60
|
+
render(
|
|
61
|
+
<ErrorBoundary fallback={CustomFallback}>
|
|
62
|
+
<Bomb />
|
|
63
|
+
</ErrorBoundary>,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const alert = screen.getByRole('alert');
|
|
67
|
+
|
|
68
|
+
expect(alert).toHaveTextContent('Custom fallback: Boom!');
|
|
69
|
+
|
|
70
|
+
await user.click(screen.getByRole('button', { name: /try again/i }));
|
|
71
|
+
|
|
72
|
+
// Child will throw again immediately, so fallback remains
|
|
73
|
+
expect(screen.getByRole('alert')).toBeInTheDocument();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ComponentType, ErrorInfo, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import { ErrorBoundary as ReactErrorBoundary, type FallbackProps } from 'react-error-boundary';
|
|
4
|
+
|
|
5
|
+
import { ErrorFallback } from './ErrorFallback';
|
|
6
|
+
|
|
7
|
+
type ErrorBoundaryProps = {
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
fallback?: ComponentType<FallbackProps>;
|
|
10
|
+
onReset?: () => void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const ErrorBoundary = ({ children, fallback, onReset }: ErrorBoundaryProps) => {
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
15
|
+
const logError = (error: Error, info: ErrorInfo) => {
|
|
16
|
+
// send error to third party api, etc
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<ReactErrorBoundary
|
|
21
|
+
FallbackComponent={fallback ?? ErrorFallback}
|
|
22
|
+
onError={logError}
|
|
23
|
+
onReset={onReset}
|
|
24
|
+
>
|
|
25
|
+
{children}
|
|
26
|
+
</ReactErrorBoundary>
|
|
27
|
+
);
|
|
28
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/* eslint-disable storybook/no-renderer-packages */
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
|
|
4
|
+
import { ErrorFallback } from './ErrorFallback';
|
|
5
|
+
import { ApiError } from '../../errors/ApiError';
|
|
6
|
+
import { Http } from '../../utils/http';
|
|
7
|
+
|
|
8
|
+
const meta = {
|
|
9
|
+
title: 'Components/ErrorFallback',
|
|
10
|
+
component: ErrorFallback,
|
|
11
|
+
parameters: {
|
|
12
|
+
layout: 'centered',
|
|
13
|
+
docs: {
|
|
14
|
+
description: {
|
|
15
|
+
component:
|
|
16
|
+
'A UI component displayed when an error is caught by `ErrorBoundary`. It shows the error message, optional cause, and a retry button.',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
tags: ['autodocs'],
|
|
21
|
+
} satisfies Meta<typeof ErrorFallback>;
|
|
22
|
+
|
|
23
|
+
export default meta;
|
|
24
|
+
type Story = StoryObj<typeof meta>;
|
|
25
|
+
|
|
26
|
+
// Shared mock reset fn
|
|
27
|
+
const mockReset = () => alert('Reset error boundary called');
|
|
28
|
+
|
|
29
|
+
export const Default: Story = {
|
|
30
|
+
args: {
|
|
31
|
+
error: new Error('Something went wrong'),
|
|
32
|
+
resetErrorBoundary: mockReset,
|
|
33
|
+
},
|
|
34
|
+
parameters: {
|
|
35
|
+
docs: {
|
|
36
|
+
description: {
|
|
37
|
+
story: 'Default rendering when given a standard JavaScript `Error`.',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const WithApiError: Story = {
|
|
44
|
+
args: {
|
|
45
|
+
error: new ApiError('Request failed', Http.InternalServerError, undefined, 'Extra details'),
|
|
46
|
+
resetErrorBoundary: mockReset,
|
|
47
|
+
},
|
|
48
|
+
parameters: {
|
|
49
|
+
docs: {
|
|
50
|
+
description: {
|
|
51
|
+
story: 'Rendering when the error is an `ApiError` that includes extra details.',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const WithUnknownError: Story = {
|
|
58
|
+
args: {
|
|
59
|
+
error: {} as unknown as Error,
|
|
60
|
+
resetErrorBoundary: mockReset,
|
|
61
|
+
},
|
|
62
|
+
parameters: {
|
|
63
|
+
docs: {
|
|
64
|
+
description: {
|
|
65
|
+
story:
|
|
66
|
+
'If the error is not an `Error` or `ApiError`, a generic "Unknown" message is shown.',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { ErrorFallback } from './ErrorFallback';
|
|
2
|
+
import { ApiError } from '../../errors';
|
|
3
|
+
import { render, screen, userEvent } from '../../test/renderers';
|
|
4
|
+
import { Http } from '../../utils/http';
|
|
5
|
+
import { identityFn } from '../../utils/utils';
|
|
6
|
+
|
|
7
|
+
describe('ErrorFallback', () => {
|
|
8
|
+
it('renders a generic error message for an unknown error type', () => {
|
|
9
|
+
render(<ErrorFallback error={{} as unknown as Error} resetErrorBoundary={identityFn} />);
|
|
10
|
+
|
|
11
|
+
const alert = screen.getByRole('alert');
|
|
12
|
+
|
|
13
|
+
expect(alert).toHaveTextContent(/there was an error/i);
|
|
14
|
+
expect(alert).toHaveTextContent(/reason: unknown/i);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('renders message for standard Error instance', () => {
|
|
18
|
+
const error = new Error('Something went wrong');
|
|
19
|
+
|
|
20
|
+
render(<ErrorFallback error={error} resetErrorBoundary={identityFn} />);
|
|
21
|
+
|
|
22
|
+
expect(screen.getByText(/reason: something went wrong/i)).toBeInTheDocument();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('renders message and cause for ApiError instance', () => {
|
|
26
|
+
const apiError = new ApiError('Fake Reason', Http.InternalServerError);
|
|
27
|
+
|
|
28
|
+
render(<ErrorFallback error={apiError} resetErrorBoundary={identityFn} />);
|
|
29
|
+
|
|
30
|
+
expect(screen.getByText(/reason: fake reason/i)).toBeInTheDocument();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('renders cause only when it is a string', () => {
|
|
34
|
+
const error = new Error('Has a cause', { cause: ["I won't display"] });
|
|
35
|
+
|
|
36
|
+
render(<ErrorFallback error={error} resetErrorBoundary={identityFn} />);
|
|
37
|
+
|
|
38
|
+
expect(screen.getByText(/reason: has a cause/i)).toBeInTheDocument();
|
|
39
|
+
expect(screen.queryByText(/cause:/i)).not.toBeInTheDocument();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('calls resetErrorBoundary when Try Again is clicked', async () => {
|
|
43
|
+
const user = userEvent.setup();
|
|
44
|
+
const resetSpy = vi.fn();
|
|
45
|
+
|
|
46
|
+
render(<ErrorFallback error={new Error('Test error')} resetErrorBoundary={resetSpy} />);
|
|
47
|
+
|
|
48
|
+
const button = screen.getByRole('button', { name: /try again/i });
|
|
49
|
+
|
|
50
|
+
await user.click(button);
|
|
51
|
+
|
|
52
|
+
expect(resetSpy).toHaveBeenCalledTimes(1);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type FallbackProps } from 'react-error-boundary';
|
|
2
|
+
|
|
3
|
+
import { Button, ErrorText } from '@tpzdsp/next-toolkit/components';
|
|
4
|
+
|
|
5
|
+
import { ApiError } from '../../errors/ApiError';
|
|
6
|
+
|
|
7
|
+
export const ErrorFallback = ({ resetErrorBoundary, error }: FallbackProps) => {
|
|
8
|
+
let message;
|
|
9
|
+
let cause = undefined;
|
|
10
|
+
|
|
11
|
+
if (error instanceof ApiError) {
|
|
12
|
+
message = error.message;
|
|
13
|
+
cause = error.details;
|
|
14
|
+
} else if (error instanceof Error) {
|
|
15
|
+
({ message, cause } = error);
|
|
16
|
+
} else {
|
|
17
|
+
message = 'Unknown';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div>
|
|
22
|
+
<ErrorText>
|
|
23
|
+
<p>There was an error. (Reason: {message})</p>
|
|
24
|
+
{typeof cause === 'string' ? <p>Cause: {cause}</p> : null}
|
|
25
|
+
</ErrorText>
|
|
26
|
+
|
|
27
|
+
<Button onClick={resetErrorBoundary}>Try Again</Button>
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import type { ExtendProps } from '../../types/utils';
|
|
2
|
+
|
|
3
|
+
export type ParagraphProps = ExtendProps<'p'>;
|
|
4
4
|
|
|
5
|
-
export const Paragraph = ({ children }: ParagraphProps) => {
|
|
6
|
-
return
|
|
5
|
+
export const Paragraph = ({ className, children, ...props }: ParagraphProps) => {
|
|
6
|
+
return (
|
|
7
|
+
<p className={`pb-4 text-sm text-text-primary ${className}`} {...props}>
|
|
8
|
+
{children}
|
|
9
|
+
</p>
|
|
10
|
+
);
|
|
7
11
|
};
|