@factorialco/f0-react-native 0.34.0 → 0.35.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 (41) hide show
  1. package/README.md +7 -5
  2. package/lib/module/components/Button/index.js +1 -1
  3. package/lib/module/components/Button/index.js.map +1 -1
  4. package/lib/module/components/F0Button/F0Button.js +2 -0
  5. package/lib/module/components/F0Button/F0Button.js.map +1 -0
  6. package/lib/module/components/F0Button/F0Button.md +163 -0
  7. package/lib/module/components/F0Button/F0Button.styles.js +2 -0
  8. package/lib/module/components/F0Button/F0Button.styles.js.map +1 -0
  9. package/lib/module/components/F0Button/F0Button.types.js +2 -0
  10. package/lib/module/components/F0Button/F0Button.types.js.map +1 -0
  11. package/lib/module/components/F0Button/index.js +2 -0
  12. package/lib/module/components/F0Button/index.js.map +1 -0
  13. package/lib/module/components/Icon/index.js.map +1 -1
  14. package/lib/module/components/exports.js +1 -1
  15. package/lib/module/components/exports.js.map +1 -1
  16. package/lib/typescript/components/Button/index.d.ts +18 -0
  17. package/lib/typescript/components/Button/index.d.ts.map +1 -1
  18. package/lib/typescript/components/F0Button/F0Button.d.ts +6 -0
  19. package/lib/typescript/components/F0Button/F0Button.d.ts.map +1 -0
  20. package/lib/typescript/components/F0Button/F0Button.styles.d.ts +161 -0
  21. package/lib/typescript/components/F0Button/F0Button.styles.d.ts.map +1 -0
  22. package/lib/typescript/components/F0Button/F0Button.types.d.ts +47 -0
  23. package/lib/typescript/components/F0Button/F0Button.types.d.ts.map +1 -0
  24. package/lib/typescript/components/F0Button/index.d.ts +4 -0
  25. package/lib/typescript/components/F0Button/index.d.ts.map +1 -0
  26. package/lib/typescript/components/Icon/index.d.ts +5 -0
  27. package/lib/typescript/components/Icon/index.d.ts.map +1 -1
  28. package/lib/typescript/components/exports.d.ts +1 -0
  29. package/lib/typescript/components/exports.d.ts.map +1 -1
  30. package/package.json +1 -1
  31. package/src/components/Button/__snapshots__/index.spec.tsx.snap +11 -11
  32. package/src/components/Button/index.tsx +22 -2
  33. package/src/components/F0Button/F0Button.md +163 -0
  34. package/src/components/F0Button/F0Button.styles.ts +141 -0
  35. package/src/components/F0Button/F0Button.tsx +228 -0
  36. package/src/components/F0Button/F0Button.types.ts +82 -0
  37. package/src/components/F0Button/__tests__/F0Button.spec.tsx +285 -0
  38. package/src/components/F0Button/__tests__/__snapshots__/F0Button.spec.tsx.snap +966 -0
  39. package/src/components/F0Button/index.ts +7 -0
  40. package/src/components/Icon/index.tsx +5 -0
  41. package/src/components/exports.ts +1 -0
@@ -0,0 +1,228 @@
1
+ import React, {
2
+ forwardRef,
3
+ useCallback,
4
+ useEffect,
5
+ useMemo,
6
+ useState,
7
+ } from "react"
8
+ import { View } from "react-native"
9
+ import Animated, {
10
+ Easing,
11
+ cancelAnimation,
12
+ useAnimatedStyle,
13
+ useSharedValue,
14
+ withRepeat,
15
+ withTiming,
16
+ } from "react-native-reanimated"
17
+
18
+ import { cn, omitProps } from "../../lib/utils"
19
+ import { F0Icon } from "../primitives/F0Icon"
20
+ import { F0Text } from "../primitives/F0Text"
21
+ import { PressableFeedback } from "../primitives/PressableFeedback"
22
+
23
+ import {
24
+ buttonVariants,
25
+ loadingContentVariants,
26
+ loadingIndicatorVariants,
27
+ pressedVariants,
28
+ getIconColor,
29
+ getIconOnlyColor,
30
+ getTextColor,
31
+ } from "./F0Button.styles"
32
+ import {
33
+ F0_BUTTON_BLOCKED_FORWARD_PROPS,
34
+ type F0ButtonProps,
35
+ } from "./F0Button.types"
36
+
37
+ const F0Button = React.memo(
38
+ forwardRef<View, F0ButtonProps>(function F0Button(
39
+ {
40
+ label,
41
+ onPress,
42
+ disabled = false,
43
+ loading = false,
44
+ icon,
45
+ emoji,
46
+ hideLabel = false,
47
+ variant = "default",
48
+ size = "md",
49
+ round = false,
50
+ accessibilityHint,
51
+ showBadge = false,
52
+ fullWidth = false,
53
+ testID,
54
+ feedback = "both",
55
+ ...rest
56
+ },
57
+ ref
58
+ ) {
59
+ const [isLoading, setIsLoading] = useState(false)
60
+ const [isPressed, setIsPressed] = useState(false)
61
+ const spinnerRotation = useSharedValue(0)
62
+
63
+ const isBusy = loading || isLoading
64
+ const isDisabled = disabled || isBusy
65
+ const isRound = hideLabel && round
66
+
67
+ const handlePress = useCallback(async () => {
68
+ if (!onPress || isDisabled) return
69
+
70
+ try {
71
+ const result = onPress()
72
+
73
+ if (result && typeof result.then === "function") {
74
+ setIsLoading(true)
75
+ try {
76
+ await result
77
+ } catch {
78
+ // Avoid bubbling async handler rejections from a design-system component.
79
+ } finally {
80
+ setIsLoading(false)
81
+ }
82
+ }
83
+ } catch {
84
+ // Avoid bubbling sync exceptions from a design-system component.
85
+ }
86
+ }, [onPress, isDisabled])
87
+
88
+ useEffect(() => {
89
+ if (!isBusy) {
90
+ cancelAnimation(spinnerRotation)
91
+ spinnerRotation.value = 0
92
+ return
93
+ }
94
+
95
+ spinnerRotation.value = 0
96
+ spinnerRotation.value = withRepeat(
97
+ withTiming(1, {
98
+ duration: 1000,
99
+ easing: Easing.linear,
100
+ }),
101
+ -1,
102
+ false
103
+ )
104
+ }, [isBusy, spinnerRotation])
105
+
106
+ const handlePressIn = useCallback(() => {
107
+ if (!isDisabled) {
108
+ setIsPressed(true)
109
+ }
110
+ }, [isDisabled])
111
+
112
+ const handlePressOut = useCallback(() => {
113
+ setIsPressed(false)
114
+ }, [])
115
+
116
+ const baseClassName = useMemo(
117
+ () =>
118
+ buttonVariants({
119
+ variant,
120
+ size,
121
+ disabled: isDisabled,
122
+ round: isRound,
123
+ }),
124
+ [variant, size, isDisabled, isRound]
125
+ )
126
+
127
+ const accessibilityLabel = useMemo(() => {
128
+ const parts = [label]
129
+ if (isDisabled) parts.push("disabled")
130
+ if (isBusy) parts.push("loading")
131
+ return parts.join(", ")
132
+ }, [label, isBusy, isDisabled])
133
+
134
+ const shouldShowPressed = isPressed && !isDisabled
135
+
136
+ const className = shouldShowPressed
137
+ ? cn(baseClassName, pressedVariants({ variant }))
138
+ : baseClassName
139
+
140
+ const iconIsOnly = isRound || (hideLabel && !emoji)
141
+ const iconColor = icon
142
+ ? iconIsOnly
143
+ ? getIconOnlyColor(variant, shouldShowPressed)
144
+ : getIconColor(variant, shouldShowPressed)
145
+ : undefined
146
+ const textColor = getTextColor(variant, shouldShowPressed)
147
+ const forwardedProps = omitProps(rest, F0_BUTTON_BLOCKED_FORWARD_PROPS)
148
+ const loadingIndicatorStyle = useAnimatedStyle(() => {
149
+ return {
150
+ borderTopColor: "transparent",
151
+ transform: [{ rotateZ: `${spinnerRotation.value * 360}deg` }],
152
+ }
153
+ })
154
+
155
+ return (
156
+ <View className={`flex ${fullWidth ? "flex-1" : "items-start"}`}>
157
+ <PressableFeedback
158
+ ref={ref}
159
+ {...forwardedProps}
160
+ disabled={isDisabled}
161
+ variant={feedback}
162
+ onPress={handlePress}
163
+ onPressIn={handlePressIn}
164
+ onPressOut={handlePressOut}
165
+ accessibilityLabel={accessibilityLabel}
166
+ accessibilityRole="button"
167
+ accessibilityState={{
168
+ disabled: isDisabled,
169
+ busy: isBusy,
170
+ }}
171
+ accessibilityHint={accessibilityHint}
172
+ testID={testID}
173
+ >
174
+ <View className={cn("relative", className)}>
175
+ <View
176
+ testID="f0-button-content"
177
+ className={loadingContentVariants({ loading: isBusy })}
178
+ >
179
+ <View className="flex-row items-center justify-center gap-1">
180
+ {icon && (
181
+ <F0Icon
182
+ icon={icon}
183
+ size="lg"
184
+ className={isRound ? undefined : "-ml-0.5"}
185
+ color={iconColor}
186
+ />
187
+ )}
188
+ {emoji && (
189
+ <F0Text variant="body-md-medium" color={textColor}>
190
+ {emoji}
191
+ </F0Text>
192
+ )}
193
+ {!hideLabel && (
194
+ <F0Text variant="body-md-medium" color={textColor}>
195
+ {label}
196
+ </F0Text>
197
+ )}
198
+ </View>
199
+ </View>
200
+ {isBusy && (
201
+ <View
202
+ pointerEvents="none"
203
+ className="absolute inset-0 items-center justify-center"
204
+ >
205
+ <Animated.View
206
+ testID="f0-button-loading-indicator"
207
+ accessibilityLabel="Loading indicator"
208
+ className={loadingIndicatorVariants({ variant, size })}
209
+ style={loadingIndicatorStyle}
210
+ />
211
+ </View>
212
+ )}
213
+ </View>
214
+ </PressableFeedback>
215
+ {showBadge && variant === "outline" && (
216
+ <View
217
+ accessibilityLabel="Notification Badge"
218
+ className="absolute top-1.5 right-1.5 h-1.5 w-1.5 rounded-full bg-f0-icon-accent"
219
+ />
220
+ )}
221
+ </View>
222
+ )
223
+ })
224
+ )
225
+
226
+ F0Button.displayName = "F0Button"
227
+
228
+ export { F0Button }
@@ -0,0 +1,82 @@
1
+ import type { IconType } from "../primitives/F0Icon"
2
+ import type {
3
+ PressableFeedbackProps,
4
+ PressableFeedbackVariant,
5
+ } from "../primitives/PressableFeedback"
6
+
7
+ /**
8
+ * Button variant types
9
+ */
10
+ export const BUTTON_VARIANTS = [
11
+ "default",
12
+ "outline",
13
+ "critical",
14
+ "neutral",
15
+ "ghost",
16
+ "promote",
17
+ ] as const
18
+
19
+ export type ButtonVariant = (typeof BUTTON_VARIANTS)[number]
20
+ export type F0ButtonVariant = ButtonVariant
21
+
22
+ /**
23
+ * Button size types
24
+ */
25
+ export const BUTTON_SIZES = ["sm", "md", "lg"] as const
26
+
27
+ export type ButtonSize = (typeof BUTTON_SIZES)[number]
28
+ export type F0ButtonSize = ButtonSize
29
+
30
+ /**
31
+ * Props that are controlled by F0Button and must not be passed through.
32
+ * This preserves F0Button press-state behavior and accessibility contract.
33
+ */
34
+ const F0_BUTTON_CONTROLLED_PASSTHROUGH_PROPS = [
35
+ "onPressIn",
36
+ "onPressOut",
37
+ "accessibilityLabel",
38
+ "accessibilityRole",
39
+ "accessibilityState",
40
+ ] as const
41
+
42
+ export const F0_BUTTON_BANNED_PROPS = ["style", "className"] as const
43
+
44
+ export const F0_BUTTON_BLOCKED_FORWARD_PROPS = [
45
+ ...F0_BUTTON_CONTROLLED_PASSTHROUGH_PROPS,
46
+ ...F0_BUTTON_BANNED_PROPS,
47
+ ] as const
48
+
49
+ interface F0ButtonPropsInternal extends Omit<
50
+ PressableFeedbackProps,
51
+ | "children"
52
+ | "variant"
53
+ | "disabled"
54
+ | (typeof F0_BUTTON_CONTROLLED_PASSTHROUGH_PROPS)[number]
55
+ > {
56
+ label: string
57
+ onPress?: () => void | Promise<unknown>
58
+ variant?: ButtonVariant
59
+ size?: ButtonSize
60
+ disabled?: boolean
61
+ loading?: boolean
62
+ icon?: IconType
63
+ emoji?: string
64
+ hideLabel?: boolean
65
+ round?: boolean
66
+ showBadge?: boolean
67
+ fullWidth?: boolean
68
+ accessibilityHint?: string
69
+ testID?: string
70
+ feedback?: PressableFeedbackVariant
71
+ }
72
+
73
+ /**
74
+ * Public props for the F0Button component.
75
+ *
76
+ * Note: `className` and `style` props are NOT available.
77
+ * Use semantic props (variant, size, etc.) for styling.
78
+ */
79
+ export type F0ButtonProps = Omit<
80
+ F0ButtonPropsInternal,
81
+ (typeof F0_BUTTON_BANNED_PROPS)[number]
82
+ >
@@ -0,0 +1,285 @@
1
+ import { render, fireEvent, screen, act } from "@testing-library/react-native"
2
+ import React from "react"
3
+
4
+ import { F0Button } from "../"
5
+ import type { IconType } from "../../primitives/F0Icon"
6
+
7
+ jest.mock("../../primitives/F0Icon", () => ({
8
+ F0Icon: () => null,
9
+ }))
10
+
11
+ const mockIcon: IconType = "check" as unknown as IconType
12
+ const mockOnPress = jest.fn()
13
+
14
+ describe("F0Button", () => {
15
+ const defaultProps = {
16
+ label: "Test Button",
17
+ onPress: mockOnPress,
18
+ }
19
+
20
+ beforeEach(() => {
21
+ jest.clearAllMocks()
22
+ })
23
+
24
+ it("Snapshot - default button", () => {
25
+ const { toJSON } = render(<F0Button {...defaultProps} />)
26
+ expect(toJSON()).toMatchSnapshot()
27
+ })
28
+
29
+ it("Snapshot - outline variant", () => {
30
+ const { toJSON } = render(<F0Button {...defaultProps} variant="outline" />)
31
+ expect(toJSON()).toMatchSnapshot()
32
+ })
33
+
34
+ it("Snapshot - critical variant", () => {
35
+ const { toJSON } = render(<F0Button {...defaultProps} variant="critical" />)
36
+ expect(toJSON()).toMatchSnapshot()
37
+ })
38
+
39
+ it("Snapshot - with icon", () => {
40
+ const { toJSON } = render(<F0Button {...defaultProps} icon={mockIcon} />)
41
+ expect(toJSON()).toMatchSnapshot()
42
+ })
43
+
44
+ it("Snapshot - with emoji", () => {
45
+ const { toJSON } = render(<F0Button {...defaultProps} emoji="👋" />)
46
+ expect(toJSON()).toMatchSnapshot()
47
+ })
48
+
49
+ it("Snapshot - disabled state", () => {
50
+ const { toJSON } = render(<F0Button {...defaultProps} disabled />)
51
+ expect(toJSON()).toMatchSnapshot()
52
+ })
53
+
54
+ it("Snapshot - loading state", () => {
55
+ const { toJSON } = render(<F0Button {...defaultProps} loading />)
56
+ expect(toJSON()).toMatchSnapshot()
57
+ })
58
+
59
+ it("renders loading indicator and hides content when loading", () => {
60
+ render(<F0Button {...defaultProps} loading />)
61
+
62
+ expect(screen.getByTestId("f0-button-loading-indicator")).toBeDefined()
63
+ expect(screen.getByTestId("f0-button-content").props.className).toContain(
64
+ "opacity-0"
65
+ )
66
+ })
67
+
68
+ it("Snapshot - different sizes", () => {
69
+ const sizes = ["sm", "md", "lg"] as const
70
+ sizes.forEach((size) => {
71
+ const { toJSON } = render(<F0Button {...defaultProps} size={size} />)
72
+ expect(toJSON()).toMatchSnapshot()
73
+ })
74
+ })
75
+
76
+ it("Snapshot - round button with hidden label", () => {
77
+ const { toJSON } = render(<F0Button {...defaultProps} round hideLabel />)
78
+ expect(toJSON()).toMatchSnapshot()
79
+ })
80
+
81
+ it("renders correctly with required props", () => {
82
+ render(<F0Button {...defaultProps} />)
83
+ const button = screen.getByText("Test Button")
84
+ expect(button).toBeDefined()
85
+ })
86
+
87
+ it("handles press events", () => {
88
+ render(<F0Button {...defaultProps} />)
89
+ fireEvent.press(screen.getByRole("button"))
90
+ expect(mockOnPress).toHaveBeenCalled()
91
+ })
92
+
93
+ it("does not call onPress when disabled", () => {
94
+ render(<F0Button {...defaultProps} disabled />)
95
+ fireEvent.press(screen.getByRole("button"))
96
+ expect(mockOnPress).not.toHaveBeenCalled()
97
+ })
98
+
99
+ it("shows correct accessibility label when disabled", () => {
100
+ render(<F0Button {...defaultProps} disabled />)
101
+ const button = screen.getByRole("button")
102
+ expect(button.props.accessibilityLabel).toBe("Test Button, disabled")
103
+ })
104
+
105
+ it("shows correct accessibility label when loading", () => {
106
+ render(<F0Button {...defaultProps} loading />)
107
+ const button = screen.getByRole("button")
108
+ expect(button.props.accessibilityLabel).toBe(
109
+ "Test Button, disabled, loading"
110
+ )
111
+ })
112
+
113
+ it("sets correct accessibilityState when disabled", () => {
114
+ render(<F0Button {...defaultProps} disabled />)
115
+ const button = screen.getByRole("button")
116
+ expect(button.props.accessibilityState).toMatchObject({
117
+ disabled: true,
118
+ busy: false,
119
+ })
120
+ })
121
+
122
+ it("sets correct accessibilityState when loading", () => {
123
+ render(<F0Button {...defaultProps} loading />)
124
+ const button = screen.getByRole("button")
125
+ expect(button.props.accessibilityState).toMatchObject({
126
+ disabled: true,
127
+ busy: true,
128
+ })
129
+ })
130
+
131
+ it("hides label text when hideLabel is true", () => {
132
+ render(<F0Button {...defaultProps} hideLabel />)
133
+ expect(screen.queryByText("Test Button")).toBeNull()
134
+ })
135
+
136
+ it("uses label as accessibilityLabel even when hideLabel is true", () => {
137
+ render(<F0Button {...defaultProps} hideLabel />)
138
+ const button = screen.getByRole("button")
139
+ expect(button.props.accessibilityLabel).toBe("Test Button")
140
+ })
141
+
142
+ it("renders with testID", () => {
143
+ render(<F0Button {...defaultProps} testID="my-button" />)
144
+ expect(screen.getByTestId("my-button")).toBeDefined()
145
+ })
146
+
147
+ it("renders badge for outline variant with showBadge", () => {
148
+ render(<F0Button {...defaultProps} variant="outline" showBadge />)
149
+ expect(screen.getByLabelText("Notification Badge")).toBeDefined()
150
+ })
151
+
152
+ it("does not render badge for non-outline variant even with showBadge", () => {
153
+ render(<F0Button {...defaultProps} variant="default" showBadge />)
154
+ expect(screen.queryByLabelText("Notification Badge")).toBeNull()
155
+ })
156
+
157
+ it("does not render badge when showBadge is false", () => {
158
+ render(<F0Button {...defaultProps} variant="outline" />)
159
+ expect(screen.queryByLabelText("Notification Badge")).toBeNull()
160
+ })
161
+
162
+ it("auto-enters loading state for async onPress", async () => {
163
+ let resolvePromise: () => void
164
+ const asyncOnPress = jest.fn(
165
+ () =>
166
+ new Promise<void>((resolve) => {
167
+ resolvePromise = resolve
168
+ })
169
+ )
170
+
171
+ render(<F0Button label="Async" onPress={asyncOnPress} />)
172
+
173
+ await act(async () => {
174
+ fireEvent.press(screen.getByRole("button"))
175
+ })
176
+
177
+ expect(asyncOnPress).toHaveBeenCalled()
178
+ expect(screen.getByTestId("f0-button-loading-indicator")).toBeDefined()
179
+ expect(screen.getByTestId("f0-button-content").props.className).toContain(
180
+ "opacity-0"
181
+ )
182
+
183
+ await act(async () => {
184
+ resolvePromise!()
185
+ await asyncOnPress.mock.results[0].value
186
+ })
187
+
188
+ expect(screen.queryByTestId("f0-button-loading-indicator")).toBeNull()
189
+ expect(screen.getByTestId("f0-button-content").props.className).toContain(
190
+ "opacity-100"
191
+ )
192
+ })
193
+
194
+ it("handles rejected async onPress without leaking rejections", async () => {
195
+ const onPressError = new Error("Async failure")
196
+ const asyncOnPress = jest.fn().mockRejectedValue(onPressError)
197
+ render(<F0Button label="Async reject" onPress={asyncOnPress} />)
198
+
199
+ await act(async () => {
200
+ fireEvent.press(screen.getByRole("button"))
201
+ await Promise.resolve()
202
+ })
203
+
204
+ expect(asyncOnPress).toHaveBeenCalled()
205
+ expect(screen.queryByTestId("f0-button-loading-indicator")).toBeNull()
206
+ })
207
+
208
+ it("handles sync throw in onPress without throwing from press handler", () => {
209
+ const onPressError = new Error("Sync failure")
210
+ const throwingOnPress = jest.fn(() => {
211
+ throw onPressError
212
+ })
213
+
214
+ render(<F0Button label="Sync throw" onPress={throwingOnPress} />)
215
+ expect(() => {
216
+ fireEvent.press(screen.getByRole("button"))
217
+ }).not.toThrow()
218
+
219
+ expect(throwingOnPress).toHaveBeenCalled()
220
+ expect(screen.queryByTestId("f0-button-loading-indicator")).toBeNull()
221
+ })
222
+
223
+ it("renders all six variants without error", () => {
224
+ const allVariants = [
225
+ "default",
226
+ "outline",
227
+ "critical",
228
+ "neutral",
229
+ "ghost",
230
+ "promote",
231
+ ] as const
232
+
233
+ allVariants.forEach((variant) => {
234
+ const { unmount } = render(<F0Button label={variant} variant={variant} />)
235
+ expect(screen.getByText(variant)).toBeDefined()
236
+ unmount()
237
+ })
238
+ })
239
+
240
+ it("ignores className passed via unsafe cast", () => {
241
+ const unsafeProps = {
242
+ ...defaultProps,
243
+ className: "bg-red-500 p-9",
244
+ } as unknown as React.ComponentProps<typeof F0Button>
245
+ render(<F0Button {...unsafeProps} />)
246
+
247
+ const button = screen.getByRole("button")
248
+ expect(button.props.className).toBe("overflow-hidden")
249
+ })
250
+
251
+ it("ignores style passed via unsafe cast", () => {
252
+ const unsafeProps = {
253
+ ...defaultProps,
254
+ style: { padding: 999 },
255
+ } as unknown as React.ComponentProps<typeof F0Button>
256
+ render(<F0Button {...unsafeProps} />)
257
+
258
+ const button = screen.getByRole("button")
259
+ expect(button.props.style?.[1]).toBeUndefined()
260
+ })
261
+
262
+ it("ignores onPressIn passed via unsafe cast", () => {
263
+ const unsafeOnPressIn = jest.fn()
264
+ const unsafeProps = {
265
+ ...defaultProps,
266
+ onPressIn: unsafeOnPressIn,
267
+ } as unknown as React.ComponentProps<typeof F0Button>
268
+ render(<F0Button {...unsafeProps} />)
269
+
270
+ const button = screen.getByRole("button")
271
+ fireEvent(button, "pressIn")
272
+ expect(unsafeOnPressIn).not.toHaveBeenCalled()
273
+ })
274
+
275
+ it("ignores accessibilityLabel passed via unsafe cast", () => {
276
+ const unsafeProps = {
277
+ ...defaultProps,
278
+ accessibilityLabel: "Unsafe label override",
279
+ } as unknown as React.ComponentProps<typeof F0Button>
280
+ render(<F0Button {...unsafeProps} />)
281
+
282
+ const button = screen.getByRole("button")
283
+ expect(button.props.accessibilityLabel).toBe("Test Button")
284
+ })
285
+ })