ferns-ui 1.10.0 → 1.12.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.
Files changed (76) hide show
  1. package/dist/Accordion.js +1 -1
  2. package/dist/Accordion.js.map +1 -1
  3. package/dist/Accordion.test.d.ts +1 -0
  4. package/dist/Accordion.test.js +71 -0
  5. package/dist/Accordion.test.js.map +1 -0
  6. package/dist/AddressField.test.d.ts +1 -0
  7. package/dist/AddressField.test.js +65 -0
  8. package/dist/AddressField.test.js.map +1 -0
  9. package/dist/Avatar.js +2 -2
  10. package/dist/Avatar.js.map +1 -1
  11. package/dist/Avatar.test.d.ts +1 -0
  12. package/dist/Avatar.test.js +131 -0
  13. package/dist/Avatar.test.js.map +1 -0
  14. package/dist/Badge.d.ts +1 -1
  15. package/dist/Badge.js +3 -3
  16. package/dist/Badge.js.map +1 -1
  17. package/dist/Badge.test.d.ts +1 -0
  18. package/dist/Badge.test.js +76 -0
  19. package/dist/Badge.test.js.map +1 -0
  20. package/dist/Box.test.d.ts +1 -0
  21. package/dist/Box.test.js +528 -0
  22. package/dist/Box.test.js.map +1 -0
  23. package/dist/Common.d.ts +101 -2
  24. package/dist/Common.js.map +1 -1
  25. package/dist/DateTimeField.js +15 -2
  26. package/dist/DateTimeField.js.map +1 -1
  27. package/dist/Heading.js +2 -0
  28. package/dist/Heading.js.map +1 -1
  29. package/dist/InfoModalIcon.js +1 -1
  30. package/dist/InfoModalIcon.js.map +1 -1
  31. package/dist/MarkdownView.d.ts +5 -0
  32. package/dist/MarkdownView.js +44 -0
  33. package/dist/MarkdownView.js.map +1 -0
  34. package/dist/Slider.d.ts +3 -0
  35. package/dist/Slider.js +94 -0
  36. package/dist/Slider.js.map +1 -0
  37. package/dist/Text.js +2 -0
  38. package/dist/Text.js.map +1 -1
  39. package/dist/TextField.test.js +9 -9
  40. package/dist/TextField.test.js.map +1 -1
  41. package/dist/Tooltip.js +2 -0
  42. package/dist/Tooltip.js.map +1 -1
  43. package/dist/index.d.ts +2 -0
  44. package/dist/index.js +2 -0
  45. package/dist/index.js.map +1 -1
  46. package/dist/setupTests.js +40 -2
  47. package/dist/setupTests.js.map +1 -1
  48. package/dist/test-utils.js.map +1 -1
  49. package/package.json +2 -47
  50. package/src/Accordion.test.tsx +104 -0
  51. package/src/Accordion.tsx +1 -0
  52. package/src/AddressField.test.tsx +89 -0
  53. package/src/Avatar.test.tsx +163 -0
  54. package/src/Avatar.tsx +2 -0
  55. package/src/Badge.test.tsx +116 -0
  56. package/src/Badge.tsx +3 -1
  57. package/src/Box.test.tsx +665 -0
  58. package/src/Common.ts +115 -2
  59. package/src/DateTimeField.tsx +15 -2
  60. package/src/Heading.tsx +2 -0
  61. package/src/InfoModalIcon.tsx +1 -0
  62. package/src/MarkdownView.tsx +67 -0
  63. package/src/Slider.tsx +205 -0
  64. package/src/Text.tsx +2 -0
  65. package/src/TextField.test.tsx +59 -71
  66. package/src/Tooltip.tsx +2 -0
  67. package/src/__snapshots__/Accordion.test.tsx.snap +120 -0
  68. package/src/__snapshots__/AddressField.test.tsx.snap +464 -0
  69. package/src/__snapshots__/Avatar.test.tsx.snap +78 -0
  70. package/src/__snapshots__/Badge.test.tsx.snap +44 -0
  71. package/src/__snapshots__/Box.test.tsx.snap +159 -0
  72. package/src/__snapshots__/TextArea.test.tsx.snap +12 -0
  73. package/src/__snapshots__/TextField.test.tsx.snap +38 -1
  74. package/src/index.tsx +2 -0
  75. package/src/setupTests.ts +45 -2
  76. package/src/test-utils.tsx +1 -0
@@ -0,0 +1,163 @@
1
+ import {act, fireEvent} from "@testing-library/react-native";
2
+ // import {ImageManipulator} from "expo-image-manipulator";
3
+ // Import the actual implementation for SaveFormat
4
+ import * as ImageManipulator from "expo-image-manipulator";
5
+ import React from "react";
6
+
7
+ import {Avatar} from "./Avatar";
8
+ import {renderWithTheme} from "./test-utils";
9
+
10
+ // Mock expo-image-picker
11
+ jest.mock("expo-image-picker", () => ({
12
+ launchImageLibraryAsync: jest.fn(),
13
+ }));
14
+
15
+ // Mock expo-image-manipulator
16
+ const mockResize = jest.fn();
17
+ const mockRenderAsync = jest.fn();
18
+ const mockSaveAsync = jest.fn();
19
+
20
+ jest.mock("expo-image-manipulator", () => {
21
+ const actual = jest.requireActual("expo-image-manipulator");
22
+ return {
23
+ ...actual,
24
+ ImageManipulator: {
25
+ manipulate: jest.fn().mockImplementation(() => ({
26
+ resize: mockResize.mockImplementation(() => ({
27
+ renderAsync: mockRenderAsync.mockResolvedValue({
28
+ saveAsync: mockSaveAsync.mockResolvedValue({
29
+ uri: "test-uri",
30
+ base64: "test-base64",
31
+ }),
32
+ }),
33
+ })),
34
+ })),
35
+ },
36
+ SaveFormat: {
37
+ PNG: "png",
38
+ JPEG: "jpeg",
39
+ },
40
+ };
41
+ });
42
+
43
+ jest.mock("expo-image-picker", () => ({
44
+ launchImageLibraryAsync: jest.fn().mockResolvedValue({
45
+ canceled: false,
46
+ assets: [
47
+ {
48
+ uri: "test-uri",
49
+ width: 100,
50
+ height: 100,
51
+ },
52
+ ],
53
+ }),
54
+ }));
55
+
56
+ // Mock the LinearGradient component
57
+ jest.mock("expo-linear-gradient", () => ({
58
+ LinearGradient: "LinearGradient",
59
+ }));
60
+
61
+ describe("Avatar", () => {
62
+ const defaultProps = {
63
+ name: "John Doe",
64
+ src: "https://example.com/avatar.jpg",
65
+ testID: "avatar",
66
+ };
67
+
68
+ beforeEach(() => {
69
+ jest.clearAllMocks();
70
+ });
71
+
72
+ it("renders correctly with default props", () => {
73
+ const {toJSON} = renderWithTheme(<Avatar {...defaultProps} />);
74
+ expect(toJSON()).toMatchSnapshot();
75
+ });
76
+
77
+ it("renders initials when no image is provided", () => {
78
+ const {getByText} = renderWithTheme(<Avatar name="John Doe" testID="avatar" />);
79
+ expect(getByText("JD")).toBeTruthy();
80
+ });
81
+
82
+ it("renders image when src is provided", () => {
83
+ const {getByTestId} = renderWithTheme(<Avatar {...defaultProps} />);
84
+ const image = getByTestId("avatar-image");
85
+ expect(image).toBeTruthy();
86
+ });
87
+
88
+ it("shows initials when image fails to load", () => {
89
+ const {getByText, getByTestId} = renderWithTheme(<Avatar {...defaultProps} name="John Doe" />);
90
+
91
+ // Simulate image load error
92
+ fireEvent(getByTestId("avatar-image"), "onError");
93
+
94
+ expect(getByText("JD")).toBeTruthy();
95
+ });
96
+
97
+ it("applies correct size class", () => {
98
+ const size = "lg";
99
+ const {getByTestId} = renderWithTheme(<Avatar {...defaultProps} size={size} />);
100
+ const avatar = getByTestId("avatar-image");
101
+ // Check if the style contains the expected size
102
+ expect(avatar.props.style).toMatchObject({
103
+ height: 72, // lg size from the sizes object
104
+ });
105
+ });
106
+
107
+ it("shows status indicator when status is provided", () => {
108
+ const {getByTestId} = renderWithTheme(<Avatar {...defaultProps} status="online" />);
109
+ expect(getByTestId("status-indicator")).toBeTruthy();
110
+ });
111
+
112
+ it("shows edit icon when status is imagePicker and size is xl", () => {
113
+ const {getByText} = renderWithTheme(
114
+ <Avatar {...defaultProps} size="xl" status="imagePicker" />
115
+ );
116
+ expect(getByText("Upload Image")).toBeTruthy();
117
+ });
118
+
119
+ it("calls onChange when edit icon is pressed", async () => {
120
+ const mockOnChange = jest.fn();
121
+ const {getByText} = renderWithTheme(
122
+ <Avatar {...defaultProps} size="xl" status="imagePicker" onChange={mockOnChange} />
123
+ );
124
+
125
+ await act(async () => {
126
+ fireEvent.press(getByText("Upload Image"));
127
+ });
128
+
129
+ // The onChange should be called with the processed image
130
+ expect(mockOnChange).toHaveBeenCalledWith({
131
+ avatarImageFormat: "png",
132
+ uri: "data:image/png;base64,test-base64",
133
+ base64: "test-base64",
134
+ });
135
+ expect(ImageManipulator.ImageManipulator.manipulate).toHaveBeenCalled();
136
+ expect(mockResize).toHaveBeenCalled();
137
+ expect(mockRenderAsync).toHaveBeenCalled();
138
+ expect(mockSaveAsync).toHaveBeenCalledWith({
139
+ format: "png",
140
+ base64: true,
141
+ });
142
+ });
143
+
144
+ it("applies border when hasBorder is true", () => {
145
+ const {getByTestId} = renderWithTheme(<Avatar {...defaultProps} hasBorder />);
146
+ const avatar = getByTestId("avatar-image");
147
+ // Check if the style contains border properties
148
+ expect(avatar.props.style).toMatchObject({
149
+ borderWidth: expect.any(Number),
150
+ borderColor: expect.any(String),
151
+ });
152
+ });
153
+
154
+ it("shows warning when imagePicker status is used with non-xl size", () => {
155
+ const consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
156
+ renderWithTheme(<Avatar {...defaultProps} size="lg" status="imagePicker" />);
157
+
158
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
159
+ "Avatars with the status of 'imagePicker' should also have an onChange property."
160
+ );
161
+ consoleWarnSpy.mockRestore();
162
+ });
163
+ });
package/src/Avatar.tsx CHANGED
@@ -175,6 +175,7 @@ export const Avatar: FC<AvatarProps> = ({
175
175
  right: 0,
176
176
  zIndex: 5,
177
177
  }}
178
+ testID="status-indicator"
178
179
  >
179
180
  {icon({
180
181
  doNotDisturb,
@@ -213,6 +214,7 @@ export const Avatar: FC<AvatarProps> = ({
213
214
  height: avatarImageDiameter,
214
215
  overflow: "hidden",
215
216
  }}
217
+ testID="avatar-image"
216
218
  onError={handleImageError}
217
219
  />
218
220
  ) : (
@@ -0,0 +1,116 @@
1
+ import React from "react";
2
+
3
+ import {Badge} from "./Badge";
4
+ import {renderWithTheme} from "./test-utils";
5
+
6
+ describe("Badge", () => {
7
+ const defaultProps = {
8
+ value: "Test",
9
+ };
10
+
11
+ it("renders correctly with default props", () => {
12
+ const {toJSON} = renderWithTheme(<Badge {...defaultProps} />);
13
+ expect(toJSON()).toMatchSnapshot();
14
+ });
15
+
16
+ it("renders text value correctly", () => {
17
+ const {getByText} = renderWithTheme(<Badge {...defaultProps} />);
18
+ expect(getByText("Test")).toBeTruthy();
19
+ });
20
+
21
+ it("renders number value correctly", () => {
22
+ const {getByText} = renderWithTheme(<Badge value={42} />);
23
+ expect(getByText("42")).toBeTruthy();
24
+ });
25
+
26
+ it("truncates large numbers with maxValue", () => {
27
+ const {getByText} = renderWithTheme(<Badge maxValue={100} value={150} variant="numberOnly" />);
28
+ expect(getByText("100+")).toBeTruthy();
29
+ });
30
+
31
+ it("does not truncate numbers below maxValue", () => {
32
+ const {getByText} = renderWithTheme(<Badge maxValue={100} value={50} variant="numberOnly" />);
33
+ expect(getByText("50")).toBeTruthy();
34
+ });
35
+
36
+ it("applies correct status colors", () => {
37
+ const statuses = ["error", "warning", "info", "success", "neutral"] as const;
38
+
39
+ statuses.forEach((status) => {
40
+ let {getByTestId} = renderWithTheme(
41
+ <Badge {...defaultProps} status={status} testID="badge" />
42
+ );
43
+
44
+ // Test primary variant
45
+ const badge = getByTestId("badge");
46
+ expect(badge).toHaveStyle({backgroundColor: expect.any(String)});
47
+
48
+ // Test secondary variant
49
+ ({getByTestId} = renderWithTheme(
50
+ <Badge {...defaultProps} secondary status={status} testID="badge-secondary" />
51
+ ));
52
+ const secondaryBadge = getByTestId("badge-secondary");
53
+ expect(secondaryBadge).toHaveStyle({backgroundColor: expect.any(String)});
54
+ });
55
+ });
56
+
57
+ it("renders icon when iconName is provided", () => {
58
+ const {getByTestId} = renderWithTheme(
59
+ <Badge {...defaultProps} iconName="check" testID="badge-with-icon" />
60
+ );
61
+ expect(getByTestId("icon")).toBeTruthy();
62
+ });
63
+
64
+ it("renders icon only when variant is iconOnly", () => {
65
+ const {getByTestId, queryByText} = renderWithTheme(
66
+ <Badge {...defaultProps} iconName="check" testID="icon-only-badge" variant="iconOnly" />
67
+ );
68
+
69
+ expect(getByTestId("icon")).toBeTruthy();
70
+ expect(queryByText("Test")).toBeNull();
71
+ });
72
+
73
+ it("applies custom colors when status is custom", () => {
74
+ const customColors = {
75
+ customBackgroundColor: "#123456",
76
+ customTextColor: "#ffffff",
77
+ customBorderColor: "#654321",
78
+ customIconColor: "#ffcc00",
79
+ };
80
+
81
+ const {getByTestId} = renderWithTheme(
82
+ <Badge
83
+ {...defaultProps}
84
+ iconName="star"
85
+ secondary
86
+ status="custom"
87
+ testID="custom-badge"
88
+ {...(customColors as any)}
89
+ />
90
+ );
91
+
92
+ const badge = getByTestId("custom-badge");
93
+ expect(badge).toHaveStyle({
94
+ backgroundColor: customColors.customBackgroundColor,
95
+ borderColor: customColors.customBorderColor,
96
+ });
97
+ });
98
+
99
+ it("applies correct border radius based on variant", () => {
100
+ // Default variant
101
+ let {getByTestId} = renderWithTheme(<Badge {...defaultProps} testID="default-badge" />);
102
+ expect(getByTestId("default-badge")).toHaveStyle({borderRadius: expect.any(Number)});
103
+
104
+ // Icon only variant
105
+ ({getByTestId} = renderWithTheme(
106
+ <Badge {...defaultProps} iconName="check" testID="icon-only-badge" variant="iconOnly" />
107
+ ));
108
+ expect(getByTestId("icon-only-badge")).toHaveStyle({borderRadius: expect.any(Number)});
109
+
110
+ // Number only variant
111
+ ({getByTestId} = renderWithTheme(
112
+ <Badge {...defaultProps} testID="number-only-badge" variant="numberOnly" />
113
+ ));
114
+ expect(getByTestId("number-only-badge")).toHaveStyle({borderRadius: expect.any(Number)});
115
+ });
116
+ });
package/src/Badge.tsx CHANGED
@@ -17,6 +17,7 @@ export const Badge = ({
17
17
  customBorderColor,
18
18
  customIconColor,
19
19
  customIconName,
20
+ testID,
20
21
  }: BadgeProps): React.ReactElement => {
21
22
  const {theme} = useTheme();
22
23
  const isIconOnly = variant === "iconOnly";
@@ -110,9 +111,10 @@ export const Badge = ({
110
111
  isIconOnly && {height: 16, width: 16},
111
112
  secondary && {borderWidth: 1, borderColor},
112
113
  ]}
114
+ testID={testID}
113
115
  >
114
116
  {Boolean(variant !== "numberOnly" && iconName) && (
115
- <View style={{marginRight: variant === "iconOnly" ? 0 : theme.spacing.sm}}>
117
+ <View style={{marginRight: variant === "iconOnly" ? 0 : theme.spacing.sm}} testID="icon">
116
118
  <Icon color={iconColor} iconName={customIconName ?? iconName!} size="xs" />
117
119
  </View>
118
120
  )}