diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
new file mode 100644
index 0000000..710ca02
--- /dev/null
+++ b/.github/workflows/lint.yml
@@ -0,0 +1,24 @@
+name: Lint
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: ["**"]
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.head_ref }}
+ cancel-in-progress: true
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+
+ - name: Setup Node.js with Dependencies
+ uses: ./.github/actions/setup-node
+
+ - name: Lint
+ run: yarn lint:check
diff --git a/.github/workflows/perfomance-stability.yml b/.github/workflows/perfomance-stability.yml
new file mode 100644
index 0000000..632a5e2
--- /dev/null
+++ b/.github/workflows/perfomance-stability.yml
@@ -0,0 +1,23 @@
+name: Test Performance Stability
+
+on: [workflow_dispatch]
+
+permissions: read-all
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: ${{ !contains(github.ref, 'main')}}
+
+jobs:
+ test:
+ name: Performance Stability
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+
+ - name: Setup Node.js with Dependencies
+ uses: ./.github/actions/setup-node
+
+ - name: Run stability checks
+ run: yarn reassure check-stability
diff --git a/.github/workflows/performance-test.yml b/.github/workflows/performance-test.yml
new file mode 100644
index 0000000..eff1644
--- /dev/null
+++ b/.github/workflows/performance-test.yml
@@ -0,0 +1,45 @@
+name: Performance Tests
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: ["**"]
+
+permissions:
+ pull-requests: write # required for Danger to post comments
+ statuses: write # required for Danger to post commit statuses
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.head_ref }}
+ cancel-in-progress: true
+
+jobs:
+ test_performance:
+ name: Performance Tests
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ fetch-depth: 0 # Required for git operations in reassure-tests.sh
+
+ - name: Setup Node.js with Dependencies
+ uses: ./.github/actions/setup-node
+
+ - name: Run Reassure Performance Tests
+ run: ./reassure-tests.sh
+ env:
+ REASSURE_OUTPUT_FILE: .reassure/output.md
+
+ - name: Run Danger
+ run: yarn danger ci
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Upload Reassure report
+ uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: reassure-report
+ path: .reassure/output.md
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..b4937ec
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,24 @@
+name: Tests
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: ["**"]
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.head_ref }}
+ cancel-in-progress: true
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+
+ - name: Setup Node.js with Dependencies
+ uses: ./.github/actions/setup-node
+
+ - name: Test
+ run: yarn test
diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml
new file mode 100644
index 0000000..ac6ea94
--- /dev/null
+++ b/.github/workflows/typecheck.yml
@@ -0,0 +1,24 @@
+name: Typecheck
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: ["**"]
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.head_ref }}
+ cancel-in-progress: true
+
+jobs:
+ typecheck:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+
+ - name: Setup Node.js with Dependencies
+ uses: ./.github/actions/setup-node
+
+ - name: Typecheck
+ run: yarn tsc --noEmit
diff --git a/.gitignore b/.gitignore
index 7c51f9a..b5454a6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -55,3 +55,5 @@ web-build/
# direnv
.envrc
+# Reassure output directory
+.reassure
diff --git a/.npmignore b/.npmignore
index 3d7286e..980a309 100644
--- a/.npmignore
+++ b/.npmignore
@@ -49,4 +49,7 @@ expo
.prettierignore
.vscode
xcode-installation.png
-configs
\ No newline at end of file
+configs
+dangerfile.js
+.reassure
+reassure-tests.sh
diff --git a/configs/eslint/rules/reactNative.ts b/configs/eslint/rules/reactNative.ts
index bf99a89..84e30af 100644
--- a/configs/eslint/rules/reactNative.ts
+++ b/configs/eslint/rules/reactNative.ts
@@ -39,7 +39,7 @@ export const reactNativeConfig = defineConfig([
},
},
{
- files: ['**/*.{test,spec}.{js,jsx,cjs,mjs,ts,tsx,mts,cts}'],
+ files: ['**/*.{test,spec,perf-test}.{js,jsx,cjs,mjs,ts,tsx,mts,cts}'],
rules: {
'react-native/no-inline-styles': 'off',
'react-native/no-raw-text': 'off',
@@ -50,6 +50,7 @@ export const reactNativeConfig = defineConfig([
rules: {
'react-native/no-inline-styles': 'off',
'react-native/no-raw-text': 'off',
+ 'react-native/no-color-literals': 'off',
},
},
])
diff --git a/dangerfile.js b/dangerfile.js
new file mode 100644
index 0000000..029c306
--- /dev/null
+++ b/dangerfile.js
@@ -0,0 +1,6 @@
+// eslint-disable-next-line import-x/no-nodejs-modules
+import path from 'path'
+
+import { dangerReassure } from 'reassure'
+
+dangerReassure({ inputFilePath: path.join(__dirname, './.reassure/output.md') })
diff --git a/jest.config.ts b/jest.config.ts
index b5a706d..37f1408 100644
--- a/jest.config.ts
+++ b/jest.config.ts
@@ -10,7 +10,9 @@ const config: Config.InitialOptions = {
'!**/*.stories.{ts,tsx}',
'!**/index.ts',
'!**/types.ts',
+ '!**/__tests__/*',
],
+ testMatch: ['**/__tests__/**/*.{spec,test}.{ts,tsx}'],
coverageReporters: ['text', 'text-summary'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
testRunner: 'jest-circus',
diff --git a/package.json b/package.json
index f4dbd47..47c2ff3 100644
--- a/package.json
+++ b/package.json
@@ -109,6 +109,7 @@
"react-native-reanimated": "3.19.1",
"react-native-safe-area-context": "5.6.1",
"react-native-svg": "15.12.1",
+ "reassure": "1.4.0",
"release-it": "19.0.6",
"standard-version": "9.5.0",
"storybook": "8.3.5",
diff --git a/reassure-tests.sh b/reassure-tests.sh
new file mode 100755
index 0000000..272adf1
--- /dev/null
+++ b/reassure-tests.sh
@@ -0,0 +1,19 @@
+#!/usr/bin/env bash
+set -e
+
+BASELINE_BRANCH=${GITHUB_BASE_REF:="main"}
+
+# Required for `git switch` on CI
+git fetch origin
+
+# Gather baseline perf measurements
+git switch "$BASELINE_BRANCH"
+
+yarn install --immutable
+yarn reassure --baseline
+
+# Gather current perf measurements & compare results
+git switch --detach -
+
+yarn install --immutable
+yarn reassure --branch
diff --git a/src/components/Accordion/__tests__/Accordion.perf-test.tsx b/src/components/Accordion/__tests__/Accordion.perf-test.tsx
new file mode 100644
index 0000000..a6abcc3
--- /dev/null
+++ b/src/components/Accordion/__tests__/Accordion.perf-test.tsx
@@ -0,0 +1,77 @@
+import { IconUser, IconDiamond } from '@tabler/icons-react-native'
+import { fireEvent, screen } from '@testing-library/react-native'
+import { View } from 'react-native'
+
+import { measureComponentPerformance } from '../../../utils/__tests__/perf-utils'
+import { Accordion, AccordionTestIds } from '../Accordion'
+
+describe('Accordion performance', () => {
+ test('initial render (collapsed)', async () => {
+ await measureComponentPerformance(
+
+
+
+ )
+ })
+
+ test('initial render (expanded)', async () => {
+ await measureComponentPerformance(
+
+
+
+ )
+ })
+
+ test('initial render with Icon', async () => {
+ await measureComponentPerformance(
+
+
+
+ )
+ })
+
+ test('initial render with titleExtra', async () => {
+ await measureComponentPerformance(
+ }>
+
+
+ )
+ })
+
+ test('initial render with withSeparator', async () => {
+ await measureComponentPerformance(
+
+
+
+ )
+ })
+
+ test('initial render when disabled', async () => {
+ await measureComponentPerformance(
+
+
+
+ )
+ })
+
+ test('toggle performance', async () => {
+ const scenario = async () => {
+ fireEvent(
+ screen.getByTestId(AccordionTestIds.contentWrapper, {
+ includeHiddenElements: true,
+ }),
+ 'layout',
+ { nativeEvent: { layout: { height: 100, width: 200, x: 0, y: 0 } } }
+ )
+
+ fireEvent.press(screen.getByText('Accordion'))
+ }
+
+ await measureComponentPerformance(
+
+
+ ,
+ { scenario }
+ )
+ })
+})
diff --git a/src/components/Avatar/__tests__/Avatar.perf-test.tsx b/src/components/Avatar/__tests__/Avatar.perf-test.tsx
new file mode 100644
index 0000000..ad83550
--- /dev/null
+++ b/src/components/Avatar/__tests__/Avatar.perf-test.tsx
@@ -0,0 +1,86 @@
+import { IconUser } from '@tabler/icons-react-native'
+
+import { measureComponentPerformance } from '../../../utils/__tests__/perf-utils'
+import { Badge } from '../../Badge'
+
+import { Avatar } from '../Avatar'
+
+describe('Avatar performance', () => {
+ test('label', async () => {
+ await measureComponentPerformance(
+
+ A
+
+ )
+ })
+
+ test('icon', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test('image', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test('size normal', async () => {
+ await measureComponentPerformance(
+
+ A
+
+ )
+ })
+
+ test('size large', async () => {
+ await measureComponentPerformance(
+
+ A
+
+ )
+ })
+
+ test('size xlarge', async () => {
+ await measureComponentPerformance(
+
+ A
+
+ )
+ })
+
+ test('size custom', async () => {
+ await measureComponentPerformance(
+
+ A
+
+ )
+ })
+
+ test('shape square', async () => {
+ await measureComponentPerformance(
+
+ A
+
+ )
+ })
+
+ test('with badge', async () => {
+ await measureComponentPerformance(
+ 9}
+ shape='circle'
+ size='large'
+ type='label'
+ >
+ A
+
+ )
+ })
+})
diff --git a/src/components/Badge/__tests__/Badge.perf-test.tsx b/src/components/Badge/__tests__/Badge.perf-test.tsx
new file mode 100644
index 0000000..327574c
--- /dev/null
+++ b/src/components/Badge/__tests__/Badge.perf-test.tsx
@@ -0,0 +1,28 @@
+import { measureComponentPerformance } from '../../../utils/__tests__/perf-utils'
+import { Badge, type BadgeSeverity } from '../Badge'
+
+const severities: BadgeSeverity[] = [
+ 'basic',
+ 'info',
+ 'success',
+ 'warning',
+ 'danger',
+]
+
+describe('Badge performance', () => {
+ describe('dot', () => {
+ for (const severity of severities) {
+ test(`severity: ${severity}`, async () => {
+ await measureComponentPerformance()
+ })
+ }
+ })
+
+ describe('text', () => {
+ for (const severity of severities) {
+ test(`severity: ${severity}`, async () => {
+ await measureComponentPerformance(12)
+ })
+ }
+ })
+})
diff --git a/src/components/Button/__tests__/Button.perf-test.tsx b/src/components/Button/__tests__/Button.perf-test.tsx
new file mode 100644
index 0000000..3e68143
--- /dev/null
+++ b/src/components/Button/__tests__/Button.perf-test.tsx
@@ -0,0 +1,71 @@
+import { IconArrowDownRight } from '@tabler/icons-react-native'
+
+import { measureComponentPerformance } from '../../../utils/__tests__/perf-utils'
+import { Button } from '../Button'
+import { ButtonBadge } from '../ButtonBadge'
+import { ButtonSeverity } from '../ButtonSeverity'
+
+describe('Button performance', () => {
+ test('primary button', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test('button with icon', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test('disabled button', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test('loading button', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+})
+
+describe('ButtonBadge performance', () => {
+ test('with text badge', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test('with dot badge', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+})
+
+describe('ButtonSeverity performance', () => {
+ const severities = ['info', 'success', 'warning', 'danger'] as const
+
+ for (const severity of severities) {
+ test(`severity: ${severity}`, async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+ }
+})
diff --git a/src/components/Checkbox/__tests__/Checkbox.perf-test.tsx b/src/components/Checkbox/__tests__/Checkbox.perf-test.tsx
new file mode 100644
index 0000000..dd2d4d0
--- /dev/null
+++ b/src/components/Checkbox/__tests__/Checkbox.perf-test.tsx
@@ -0,0 +1,49 @@
+import { fireEvent, screen } from '@testing-library/react-native'
+
+import { measureComponentPerformance } from '../../../utils/__tests__/perf-utils'
+import { Checkbox } from '../Checkbox'
+
+describe('Checkbox performance', () => {
+ const onPress = jest.fn()
+
+ test('unchecked', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test('checked', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test('indeterminate', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test('disabled', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test('danger state', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test('press interaction', async () => {
+ const scenario = async () => {
+ fireEvent.press(screen.getByTestId('CheckboxButton_Pressable'))
+ }
+
+ await measureComponentPerformance(
+ ,
+ { scenario }
+ )
+ })
+})
diff --git a/src/components/Checkbox/__tests__/Checkbox.test.tsx b/src/components/Checkbox/__tests__/Checkbox.test.tsx
index 7156861..786c861 100644
--- a/src/components/Checkbox/__tests__/Checkbox.test.tsx
+++ b/src/components/Checkbox/__tests__/Checkbox.test.tsx
@@ -3,11 +3,7 @@ import { render } from '@testing-library/react-native'
import { Checkbox, type CheckboxProps } from '../Checkbox'
describe('Checkbox', () => {
- const defaultProps: CheckboxProps = {
- // eslint-disable-next-line @typescript-eslint/no-empty-function
- onPress: () => {},
- state: 'default',
- }
+ const defaultProps: CheckboxProps = { onPress: jest.fn(), state: 'default' }
describe('snapshots', () => {
const snapshotCases: Array<[string, Partial]> = [
diff --git a/src/components/Chip/__tests__/Chip.perf-test.tsx b/src/components/Chip/__tests__/Chip.perf-test.tsx
new file mode 100644
index 0000000..714d489
--- /dev/null
+++ b/src/components/Chip/__tests__/Chip.perf-test.tsx
@@ -0,0 +1,49 @@
+import { IconUser } from '@tabler/icons-react-native'
+import { fireEvent, screen } from '@testing-library/react-native'
+
+import { measureComponentPerformance } from '../../../utils/__tests__/perf-utils'
+import { Chip, TestId } from '../Chip'
+
+describe('Chip performance', () => {
+ test('simple chip', async () => {
+ await measureComponentPerformance()
+ })
+
+ test('chip with icon', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test('chip with close button', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test('disabled chip', async () => {
+ await measureComponentPerformance()
+ })
+
+ test('press interaction', async () => {
+ const scenario = async () => {
+ fireEvent.press(screen.getByTestId(TestId.Container))
+ }
+
+ await measureComponentPerformance(
+ ,
+ { scenario }
+ )
+ })
+
+ test('close press interaction', async () => {
+ const scenario = async () => {
+ fireEvent.press(screen.getByTestId(TestId.RemoveButton))
+ }
+
+ await measureComponentPerformance(
+ ,
+ { scenario }
+ )
+ })
+})
diff --git a/src/components/Dialog/__tests__/Dialog.perf-test.tsx b/src/components/Dialog/__tests__/Dialog.perf-test.tsx
new file mode 100644
index 0000000..f99ec32
--- /dev/null
+++ b/src/components/Dialog/__tests__/Dialog.perf-test.tsx
@@ -0,0 +1,73 @@
+import { PortalProvider } from '@gorhom/portal'
+import { fireEvent, screen } from '@testing-library/react-native'
+import { useState } from 'react'
+
+import { Button, Text, View } from 'react-native'
+
+import { measureComponentPerformance } from '../../../utils/__tests__/perf-utils'
+import { Dialog } from '../Dialog'
+import { DialogHeader } from '../DialogHeader'
+
+const DialogTestComponent = ({
+ withFooter = false,
+ severity,
+}: {
+ readonly withFooter?: boolean
+ readonly severity?: 'info' | 'success' | 'warning' | 'danger' | 'help'
+}) => {
+ const [isVisible, setIsVisible] = useState(false)
+
+ const body = () => Dialog Body
+ const footer = withFooter ? () => Dialog Footer : undefined
+
+ return (
+
+
+
+
+ )
+}
+
+describe('Dialog performance', () => {
+ test('show and hide dialog', async () => {
+ const scenario = async () => {
+ fireEvent.press(screen.getByText('Show Dialog'))
+ await screen.findByTestId('DialogCloseButton')
+ fireEvent.press(screen.getByTestId('DialogCloseButton'))
+ }
+
+ await measureComponentPerformance(, { scenario })
+ })
+
+ test('initial render with header and body', async () => {
+ await measureComponentPerformance()
+ })
+
+ test('initial render with header, body, and footer', async () => {
+ await measureComponentPerformance()
+ })
+
+ const severities = ['info', 'success', 'warning', 'danger', 'help'] as const
+
+ for (const severity of severities) {
+ test(`initial render with severity: ${severity}`, async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+ }
+})
diff --git a/src/components/Divider/__tests__/Divider.perf-test.tsx b/src/components/Divider/__tests__/Divider.perf-test.tsx
new file mode 100644
index 0000000..3eade6b
--- /dev/null
+++ b/src/components/Divider/__tests__/Divider.perf-test.tsx
@@ -0,0 +1,28 @@
+import { IconCircleCheck } from '@tabler/icons-react-native'
+
+import { measureComponentPerformance } from '../../../utils/__tests__/perf-utils'
+import { Divider } from '../Divider'
+
+describe('Divider performance', () => {
+ test('simple horizontal divider', async () => {
+ await measureComponentPerformance()
+ })
+
+ test('horizontal divider with text', async () => {
+ await measureComponentPerformance()
+ })
+
+ test('horizontal divider with text and icon', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test('vertical divider', async () => {
+ await measureComponentPerformance()
+ })
+
+ test('dashed divider', async () => {
+ await measureComponentPerformance()
+ })
+})
diff --git a/src/components/Input/InputSwitch/__tests__/InputSwitch.perf-test.tsx b/src/components/Input/InputSwitch/__tests__/InputSwitch.perf-test.tsx
new file mode 100644
index 0000000..a02f6d2
--- /dev/null
+++ b/src/components/Input/InputSwitch/__tests__/InputSwitch.perf-test.tsx
@@ -0,0 +1,47 @@
+import { fireEvent, screen } from '@testing-library/react-native'
+
+import { measureComponentPerformance } from '../../../../utils/__tests__/perf-utils'
+import { InputSwitch } from '../InputSwitch'
+
+describe('InputSwitch performance', () => {
+ const onCheckedChange = jest.fn()
+
+ test('unchecked', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test('checked', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test('disabled', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test('danger', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test('press interaction', async () => {
+ const scenario = async () => {
+ fireEvent.press(screen.getByTestId('InputSwitch'))
+ }
+
+ await measureComponentPerformance(
+ ,
+ { scenario }
+ )
+ })
+})
diff --git a/src/components/Input/__tests__/Input.perf-test.tsx b/src/components/Input/__tests__/Input.perf-test.tsx
new file mode 100644
index 0000000..bfc88b4
--- /dev/null
+++ b/src/components/Input/__tests__/Input.perf-test.tsx
@@ -0,0 +1,66 @@
+import { IconUser } from '@tabler/icons-react-native'
+import { fireEvent, screen } from '@testing-library/react-native'
+
+import { measureComponentPerformance } from '../../../utils/__tests__/perf-utils'
+import { InputGroup } from '../InputGroup'
+import { InputText } from '../InputText'
+
+describe('Input performance', () => {
+ describe('InputText', () => {
+ test('simple input', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test('disabled input', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test('danger input', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test('floating label input', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test('typing interaction', async () => {
+ const scenario = async () => {
+ fireEvent.changeText(
+ screen.getByPlaceholderText('Typing Test'),
+ 'Hello World'
+ )
+ }
+
+ await measureComponentPerformance(
+ ,
+ { scenario }
+ )
+ })
+ })
+
+ describe('InputGroup', () => {
+ test('input with text addons', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test('input with icon addons', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+ })
+})
diff --git a/src/components/Input/__tests__/InputGroupAddon.test.tsx b/src/components/Input/__tests__/InputGroupAddon.test.tsx
index 46e26a1..f86d5e8 100644
--- a/src/components/Input/__tests__/InputGroupAddon.test.tsx
+++ b/src/components/Input/__tests__/InputGroupAddon.test.tsx
@@ -33,6 +33,6 @@ describe('InputGroup component tests', () => {
fireEvent.press(pressable)
- expect(onPress).toHaveBeenCalled()
+ expect(onPress).toHaveBeenCalledWith()
})
})
diff --git a/src/components/Input/__tests__/InputTextBase.test.tsx b/src/components/Input/__tests__/InputTextBase.test.tsx
index b75c94b..c5e8424 100644
--- a/src/components/Input/__tests__/InputTextBase.test.tsx
+++ b/src/components/Input/__tests__/InputTextBase.test.tsx
@@ -30,7 +30,7 @@ describe('InputTextBase component functionality tests', () => {
fireEvent(input, 'focus')
- expect(onFocusMock).toHaveBeenCalled()
+ expect(onFocusMock).toHaveBeenCalledWith(undefined)
})
test('should handle blur event', () => {
@@ -40,7 +40,7 @@ describe('InputTextBase component functionality tests', () => {
fireEvent(input, 'blur')
- expect(onBlurMock).toHaveBeenCalled()
+ expect(onBlurMock).toHaveBeenCalledWith(undefined)
})
test('should handle text change', () => {
@@ -122,7 +122,7 @@ describe('InputTextBase component functionality tests', () => {
render()
- expect(renderTextInput).toHaveBeenCalled()
+ expect(renderTextInput).toHaveBeenCalledWith(expect.any(Object))
expect(renderTextInput.mock.calls[0]).toMatchSnapshot()
})
diff --git a/src/components/List/Base/__tests__/ListBase.perf-test.tsx b/src/components/List/Base/__tests__/ListBase.perf-test.tsx
new file mode 100644
index 0000000..5ec5b65
--- /dev/null
+++ b/src/components/List/Base/__tests__/ListBase.perf-test.tsx
@@ -0,0 +1,40 @@
+import { IconCheck, IconList, IconUser } from '@tabler/icons-react-native'
+import { fireEvent, screen } from '@testing-library/react-native'
+
+import { measureComponentPerformance } from '../../../../utils/__tests__/perf-utils'
+import { ListBase } from '../ListBase'
+
+describe('ListBase performance', () => {
+ test('minimal list item', async () => {
+ await measureComponentPerformance()
+ })
+
+ test('maximal list item', async () => {
+ await measureComponentPerformance(
+ }
+ text='Maximal'
+ title='Subtitle'
+ />
+ )
+ })
+
+ test('disabled list item', async () => {
+ await measureComponentPerformance()
+ })
+
+ test('press interaction', async () => {
+ const scenario = async () => {
+ fireEvent.press(screen.getByText('Pressable'))
+ }
+
+ await measureComponentPerformance(
+ ,
+ { scenario }
+ )
+ })
+})
diff --git a/src/components/MenuItem/Template/__tests__/MenuItemTemplate.perf-test.tsx b/src/components/MenuItem/Template/__tests__/MenuItemTemplate.perf-test.tsx
new file mode 100644
index 0000000..7fd3a57
--- /dev/null
+++ b/src/components/MenuItem/Template/__tests__/MenuItemTemplate.perf-test.tsx
@@ -0,0 +1,48 @@
+import { IconUser, IconChevronRight } from '@tabler/icons-react-native'
+import { fireEvent, screen } from '@testing-library/react-native'
+
+import { measureComponentPerformance } from '../../../../utils/__tests__/perf-utils'
+import { MenuItemTemplate } from '../MenuItemTemplate'
+
+describe('MenuItemTemplate performance', () => {
+ test('minimal menu item', async () => {
+ await measureComponentPerformance()
+ })
+
+ test('menu item with icon and badge', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test('menu item with accessories', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test('disabled menu item', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test('press interaction', async () => {
+ const scenario = async () => {
+ fireEvent.press(screen.getByText('Pressable'))
+ }
+
+ await measureComponentPerformance(
+ ,
+ { scenario }
+ )
+ })
+})
diff --git a/src/components/Message/__tests__/Message.perf-test.tsx b/src/components/Message/__tests__/Message.perf-test.tsx
new file mode 100644
index 0000000..4cdbd74
--- /dev/null
+++ b/src/components/Message/__tests__/Message.perf-test.tsx
@@ -0,0 +1,39 @@
+import { fireEvent, screen } from '@testing-library/react-native'
+
+import { measureComponentPerformance } from '../../../utils/__tests__/perf-utils'
+import { Message, TestId } from '../Message'
+
+describe('Message performance', () => {
+ const severities = ['info', 'success', 'warning', 'danger'] as const
+
+ for (const severity of severities) {
+ test(`simple message with severity ${severity}`, async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+ }
+
+ test('message with close button', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test('message with timer', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test('close press interaction', async () => {
+ const scenario = async () => {
+ fireEvent.press(screen.getByTestId(TestId.CloseButton))
+ }
+
+ await measureComponentPerformance(
+ ,
+ { scenario }
+ )
+ })
+})
diff --git a/src/components/ProgressBar/__tests__/ProgressBar.perf-test.tsx b/src/components/ProgressBar/__tests__/ProgressBar.perf-test.tsx
new file mode 100644
index 0000000..526197c
--- /dev/null
+++ b/src/components/ProgressBar/__tests__/ProgressBar.perf-test.tsx
@@ -0,0 +1,37 @@
+import { fireEvent, screen } from '@testing-library/react-native'
+import { useState } from 'react'
+import { View, Button } from 'react-native'
+
+import { measureComponentPerformance } from '../../../utils/__tests__/perf-utils'
+import { ProgressBar } from '../ProgressBar'
+
+describe('ProgressBar performance', () => {
+ test('simple progress bar', async () => {
+ await measureComponentPerformance()
+ })
+
+ test('progress bar with value', async () => {
+ await measureComponentPerformance()
+ })
+
+ test('progress change animation', async () => {
+ const ProgressBarTestComponent = () => {
+ const [value, setValue] = useState(20)
+
+ return (
+
+
+
+ )
+ }
+
+ const scenario = async () => {
+ fireEvent.press(screen.getByText('Update'))
+ }
+
+ await measureComponentPerformance(, {
+ scenario,
+ })
+ })
+})
diff --git a/src/components/ProgressSpinner/__tests__/ProgressSpinner.perf-test.tsx b/src/components/ProgressSpinner/__tests__/ProgressSpinner.perf-test.tsx
new file mode 100644
index 0000000..502e9d9
--- /dev/null
+++ b/src/components/ProgressSpinner/__tests__/ProgressSpinner.perf-test.tsx
@@ -0,0 +1,17 @@
+import { measureComponentPerformance } from '../../../utils/__tests__/perf-utils'
+import { ProgressSpinner, type ProgressSpinnerProps } from '../ProgressSpinner'
+
+describe('ProgressSpinner performance', () => {
+ const sizes: Array = ['sm', 'md', 'lg', 'xl']
+ const fills: Array = ['primary', 'white']
+
+ for (const size of sizes) {
+ for (const fill of fills) {
+ test(`size: ${size}, fill: ${fill}`, async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+ }
+ }
+})
diff --git a/src/components/RadioButton/RadioButton.tsx b/src/components/RadioButton/RadioButton.tsx
index c978c61..6351255 100644
--- a/src/components/RadioButton/RadioButton.tsx
+++ b/src/components/RadioButton/RadioButton.tsx
@@ -36,7 +36,7 @@ export const RadioButton = memo(
checked = false,
disabled = false,
state = 'default',
- testID,
+ testID = 'RadioButton_Pressable',
...rest
}) => {
const styles = useStyles()
@@ -112,7 +112,7 @@ export const RadioButton = memo(
diff --git a/src/components/RadioButton/__tests__/RadioButton.perf-test.tsx b/src/components/RadioButton/__tests__/RadioButton.perf-test.tsx
new file mode 100644
index 0000000..d6b03e9
--- /dev/null
+++ b/src/components/RadioButton/__tests__/RadioButton.perf-test.tsx
@@ -0,0 +1,38 @@
+import { fireEvent } from '@testing-library/react-native'
+
+import { measureComponentPerformance } from '../../../utils/__tests__/perf-utils'
+import { RadioButton, type RadioButtonProps } from '../RadioButton'
+
+const states: Array = ['default', 'danger']
+const checkedStates: boolean[] = [true, false]
+const disabledStates: boolean[] = [true, false]
+
+describe('RadioButton performance', () => {
+ for (const state of states) {
+ for (const checked of checkedStates) {
+ for (const disabled of disabledStates) {
+ test(`state: ${state}, checked: ${checked}, disabled: ${disabled}`, async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+ }
+ }
+ }
+
+ test('Interaction', async () => {
+ await measureComponentPerformance(
+ ,
+ {
+ scenario: async ({ getByTestId }) => {
+ fireEvent.press(getByTestId('RadioButton'))
+ },
+ }
+ )
+ })
+})
diff --git a/src/components/RadioButton/__tests__/RadioButton.test.tsx b/src/components/RadioButton/__tests__/RadioButton.test.tsx
index 8c22ddb..4da1a8e 100644
--- a/src/components/RadioButton/__tests__/RadioButton.test.tsx
+++ b/src/components/RadioButton/__tests__/RadioButton.test.tsx
@@ -3,10 +3,7 @@ import { render } from '@testing-library/react-native'
import { RadioButton, type RadioButtonProps } from '../RadioButton'
describe('RadioButton', () => {
- const defaultProps: RadioButtonProps = {
- // eslint-disable-next-line @typescript-eslint/no-empty-function
- onPress: () => {},
- }
+ const defaultProps: RadioButtonProps = { onPress: jest.fn() }
describe('snapshots', () => {
const snapshotCases: Array<[string, Partial]> = [
diff --git a/src/components/Rating/__tests__/Rating.perf-test.tsx b/src/components/Rating/__tests__/Rating.perf-test.tsx
new file mode 100644
index 0000000..e590184
--- /dev/null
+++ b/src/components/Rating/__tests__/Rating.perf-test.tsx
@@ -0,0 +1,42 @@
+import { fireEvent } from '@testing-library/react-native'
+
+import { measureComponentPerformance } from '../../../utils/__tests__/perf-utils'
+import { Rating } from '../Rating'
+
+describe('Rating performance', () => {
+ test('Render with default props', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test('Render with maxRating={10}', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test('Render disabled', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test('Interaction', async () => {
+ await measureComponentPerformance(
+ ,
+ {
+ scenario: async ({ getByTestId }) => {
+ fireEvent.press(getByTestId('RatingItem-5'))
+ fireEvent.press(getByTestId('RatingItem-1'))
+ fireEvent.press(getByTestId('RatingClear'))
+ },
+ }
+ )
+ })
+})
diff --git a/src/components/Rating/__tests__/Rating.test.tsx b/src/components/Rating/__tests__/Rating.test.tsx
index 7a31572..c6a03c2 100644
--- a/src/components/Rating/__tests__/Rating.test.tsx
+++ b/src/components/Rating/__tests__/Rating.test.tsx
@@ -46,7 +46,7 @@ describe('Rating component tests', () => {
const clearButton = getByTestId('RatingClear')
fireEvent.press(clearButton)
- expect(mockOnClear).toHaveBeenCalled()
+ expect(mockOnClear).toHaveBeenCalledWith()
})
test('renders correctly with a different maxRating', () => {
diff --git a/src/components/Rating/__tests__/RatingItem.test.tsx b/src/components/Rating/__tests__/RatingItem.test.tsx
index 4d3e66e..75870a5 100644
--- a/src/components/Rating/__tests__/RatingItem.test.tsx
+++ b/src/components/Rating/__tests__/RatingItem.test.tsx
@@ -32,6 +32,6 @@ describe('RatingItem component tests', () => {
await user.press(pressableContainer)
- expect(mockedOnPress).toHaveBeenCalled()
+ expect(mockedOnPress).toHaveBeenCalledWith(expect.any(Object))
})
})
diff --git a/src/components/SelectButton/SelectButtonItem.tsx b/src/components/SelectButton/SelectButtonItem.tsx
index 9f8b280..134f3b5 100644
--- a/src/components/SelectButton/SelectButtonItem.tsx
+++ b/src/components/SelectButton/SelectButtonItem.tsx
@@ -62,6 +62,7 @@ export const SelectButtonItem = memo(
size = 'base',
showIcon = true,
Icon,
+ testID = 'SelectButtonItem_TouchableOpacity',
}) => {
const styles = useStyles()
@@ -142,7 +143,7 @@ export const SelectButtonItem = memo(
styles[size],
disabled && styles.disabledContainer,
]}
- testID='SelectButtonItem_TouchableOpacity'
+ testID={testID}
onLayout={onLayout}
onPress={onPress}
>
diff --git a/src/components/SelectButton/__tests__/SelectButton.perf-test.tsx b/src/components/SelectButton/__tests__/SelectButton.perf-test.tsx
new file mode 100644
index 0000000..612066e
--- /dev/null
+++ b/src/components/SelectButton/__tests__/SelectButton.perf-test.tsx
@@ -0,0 +1,54 @@
+import { fireEvent } from '@testing-library/react-native'
+import { useSharedValue } from 'react-native-reanimated'
+
+import { measureComponentPerformance } from '../../../utils/__tests__/perf-utils'
+import { SelectButton, type SelectButtonProps } from '../SelectButton'
+
+const buttons: SelectButtonProps['buttons'] = [
+ { key: '1', label: 'Option 1' },
+ { key: '2', label: 'Option 2' },
+ { key: '3', label: 'Option 3' },
+]
+
+const sizes: Array = [
+ 'small',
+ 'base',
+ 'large',
+ 'xlarge',
+]
+
+const SelectButtonControlled = () => {
+ const position = useSharedValue(0)
+
+ return
+}
+
+describe('SelectButton performance', () => {
+ for (const size of sizes) {
+ test(`Render with size: ${size}`, async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+ }
+
+ test('Render disabled', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test('Interaction', async () => {
+ await measureComponentPerformance(, {
+ scenario: async ({ getByTestId }) => {
+ fireEvent.press(getByTestId('SelectButton_SelectButtonItem_1'))
+ fireEvent.press(getByTestId('SelectButton_SelectButtonItem_2'))
+ fireEvent.press(getByTestId('SelectButton_SelectButtonItem_0'))
+ },
+ })
+ })
+
+ test('Controlled', async () => {
+ await measureComponentPerformance()
+ })
+})
diff --git a/src/components/Skeleton/__tests__/Skeleton.perf-test.tsx b/src/components/Skeleton/__tests__/Skeleton.perf-test.tsx
new file mode 100644
index 0000000..91bb27e
--- /dev/null
+++ b/src/components/Skeleton/__tests__/Skeleton.perf-test.tsx
@@ -0,0 +1,41 @@
+import { View } from 'react-native'
+
+import { measureComponentPerformance } from '../../../utils/__tests__/perf-utils'
+import { Skeleton } from '../Skeleton'
+
+describe('Skeleton performance', () => {
+ test('Render with height', async () => {
+ await measureComponentPerformance()
+ })
+
+ test('Render with height and width', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test('Render with borderRadius', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test('Render complex layout', async () => {
+ await measureComponentPerformance(
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+ })
+})
diff --git a/src/components/Slider/__tests__/Slider.perf-test.tsx b/src/components/Slider/__tests__/Slider.perf-test.tsx
new file mode 100644
index 0000000..dc1adcb
--- /dev/null
+++ b/src/components/Slider/__tests__/Slider.perf-test.tsx
@@ -0,0 +1,37 @@
+import { measureComponentPerformance } from '../../../utils/__tests__/perf-utils'
+import { Slider } from '../Slider'
+
+describe('Slider performance', () => {
+ test('Render single slider', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test('Render range slider', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test('Render disabled slider', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+})
diff --git a/src/components/Tabs/__tests__/Tabs.perf-test.tsx b/src/components/Tabs/__tests__/Tabs.perf-test.tsx
new file mode 100644
index 0000000..4cfdeb6
--- /dev/null
+++ b/src/components/Tabs/__tests__/Tabs.perf-test.tsx
@@ -0,0 +1,48 @@
+import { fireEvent } from '@testing-library/react-native'
+import { useState } from 'react'
+
+import { measureComponentPerformance } from '../../../utils/__tests__/perf-utils'
+import { Tabs } from '../Tabs'
+
+const items = [
+ { label: 'First', key: '1' },
+ { label: 'Second Tab', key: '2' },
+ { label: 'Long Third Tab', key: '3' },
+]
+
+const TabsWrapper = () => {
+ const [activeIndex, setActiveIndex] = useState(0)
+
+ return (
+
+ )
+}
+
+describe('Tabs performance', () => {
+ test('Render with 3 tabs', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test('Render with 5 tabs', async () => {
+ const fiveItems = [
+ ...items,
+ { label: 'Fourth Tab', key: '4' },
+ { label: 'Fifth Tab', key: '5' },
+ ]
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test('Interaction', async () => {
+ await measureComponentPerformance(, {
+ scenario: async ({ getByText }) => {
+ fireEvent.press(getByText('Second Tab'))
+ fireEvent.press(getByText('Long Third Tab'))
+ fireEvent.press(getByText('First'))
+ },
+ })
+ })
+})
diff --git a/src/components/Tabs/__tests__/Tabs.test.tsx b/src/components/Tabs/__tests__/Tabs.test.tsx
index 74f82fc..0e9b306 100644
--- a/src/components/Tabs/__tests__/Tabs.test.tsx
+++ b/src/components/Tabs/__tests__/Tabs.test.tsx
@@ -198,6 +198,6 @@ describe('Tabs component tests', () => {
await user.press(container)
- expect(mockedOnTapTabItem).toHaveBeenCalled()
+ expect(mockedOnTapTabItem).toHaveBeenCalledWith(testableIndex)
})
})
diff --git a/src/components/Tag/__tests__/Tag.perf-test.tsx b/src/components/Tag/__tests__/Tag.perf-test.tsx
new file mode 100644
index 0000000..c450233
--- /dev/null
+++ b/src/components/Tag/__tests__/Tag.perf-test.tsx
@@ -0,0 +1,27 @@
+import { measureComponentPerformance } from '../../../utils/__tests__/perf-utils'
+import { Tag, type TagProps } from '../Tag'
+
+const severities: Array = [
+ 'basic',
+ 'info',
+ 'success',
+ 'warning',
+ 'danger',
+ 'secondary',
+]
+
+describe('Tag performance', () => {
+ for (const severity of severities) {
+ test(`Render with severity: ${severity}`, async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test(`Render with severity: ${severity} and rounded`, async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+ }
+})
diff --git a/src/components/Timer/__tests__/Timer.perf-test.tsx b/src/components/Timer/__tests__/Timer.perf-test.tsx
new file mode 100644
index 0000000..4fa1efa
--- /dev/null
+++ b/src/components/Timer/__tests__/Timer.perf-test.tsx
@@ -0,0 +1,16 @@
+import { measureComponentPerformance } from '../../../utils/__tests__/perf-utils'
+import { Timer } from '../Timer'
+
+describe('Timer performance', () => {
+ test('Render with countFrom={10}', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test('Render with countFrom={30}', async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+})
diff --git a/src/components/Timer/__tests__/Timer.test.tsx b/src/components/Timer/__tests__/Timer.test.tsx
index 40eb7af..ffdb62f 100644
--- a/src/components/Timer/__tests__/Timer.test.tsx
+++ b/src/components/Timer/__tests__/Timer.test.tsx
@@ -45,7 +45,7 @@ describe('Timer', () => {
jest.runAllTimers()
})
- expect(mockedOnFinish).toHaveBeenCalled()
+ expect(mockedOnFinish).toHaveBeenCalledWith()
})
test('should NOT call onFinish until the timer expires', () => {
diff --git a/src/components/ToggleButton/__tests__/ToggleButton.perf-test.tsx b/src/components/ToggleButton/__tests__/ToggleButton.perf-test.tsx
new file mode 100644
index 0000000..f0fa64d
--- /dev/null
+++ b/src/components/ToggleButton/__tests__/ToggleButton.perf-test.tsx
@@ -0,0 +1,66 @@
+import { IconUser } from '@tabler/icons-react-native'
+import { fireEvent } from '@testing-library/react-native'
+import { useState } from 'react'
+
+import { measureComponentPerformance } from '../../../utils/__tests__/perf-utils'
+import { ToggleButton, type ToggleButtonProps } from '../ToggleButton'
+
+const sizes: Array = [
+ 'small',
+ 'base',
+ 'large',
+ 'xlarge',
+]
+
+const ToggleButtonWrapper = () => {
+ const [checked, setChecked] = useState(false)
+
+ return (
+ setChecked(!checked)}
+ />
+ )
+}
+
+describe('ToggleButton performance', () => {
+ for (const size of sizes) {
+ test(`Render with size: ${size}`, async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test(`Render with size: ${size} and icon`, async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+
+ test(`Render icon only with size: ${size}`, async () => {
+ await measureComponentPerformance(
+
+ )
+ })
+ }
+
+ test('Interaction', async () => {
+ await measureComponentPerformance(, {
+ scenario: async ({ getByText }) => {
+ fireEvent.press(getByText('Toggle Me'))
+ fireEvent.press(getByText('Toggle Me'))
+ },
+ })
+ })
+})
diff --git a/src/components/ToggleButton/__tests__/ToggleButton.test.tsx b/src/components/ToggleButton/__tests__/ToggleButton.test.tsx
index 4a9ebc4..60f1df2 100644
--- a/src/components/ToggleButton/__tests__/ToggleButton.test.tsx
+++ b/src/components/ToggleButton/__tests__/ToggleButton.test.tsx
@@ -8,10 +8,7 @@ import {
} from '../ToggleButton'
describe('ToggleButton', () => {
- const defaultProps: ToggleButtonProps = {
- // eslint-disable-next-line @typescript-eslint/no-empty-function
- onPress: () => {},
- }
+ const defaultProps: ToggleButtonProps = { onPress: jest.fn() }
describe('snapshots', () => {
const snapshotCases: Array<[string, Partial]> = [
@@ -101,7 +98,7 @@ describe('ToggleButton', () => {
await user.press(pressable)
- expect(mockedOnPress).toHaveBeenCalled()
+ expect(mockedOnPress).toHaveBeenCalledWith(expect.any(Object))
})
test('should NOT handle press', async () => {
diff --git a/src/components/ToggleButton/__tests__/__snapshots__/ToggleButton.test.tsx.snap b/src/components/ToggleButton/__tests__/__snapshots__/ToggleButton.test.tsx.snap
index 66e7e5e..0f327a3 100644
--- a/src/components/ToggleButton/__tests__/__snapshots__/ToggleButton.test.tsx.snap
+++ b/src/components/ToggleButton/__tests__/__snapshots__/ToggleButton.test.tsx.snap
@@ -3,7 +3,7 @@
exports[`ToggleButton snapshots checked = false, disabled = false, iconOnly = false, size = small, with Icon, with label, iconPos = left 1`] = `
{
+ describe('Anchor', () => {
+ test('default', async () => {
+ await measureComponentPerformance(
+ Anchor
+ )
+ })
+ })
+
+ describe('Body', () => {
+ test('default', async () => {
+ await measureComponentPerformance(Body)
+ })
+ })
+
+ describe('Caption', () => {
+ test('default', async () => {
+ await measureComponentPerformance(Caption)
+ })
+ })
+
+ describe('Service', () => {
+ test('default', async () => {
+ await measureComponentPerformance(Service)
+ })
+ })
+
+ describe('Subtitle', () => {
+ test('default', async () => {
+ await measureComponentPerformance(Subtitle)
+ })
+ })
+
+ describe('Title', () => {
+ test('default', async () => {
+ await measureComponentPerformance(Title)
+ })
+ })
+})
diff --git a/src/utils/__tests__/perf-utils.tsx b/src/utils/__tests__/perf-utils.tsx
new file mode 100644
index 0000000..53275fe
--- /dev/null
+++ b/src/utils/__tests__/perf-utils.tsx
@@ -0,0 +1,22 @@
+import type { ReactElement, FunctionComponent, ReactNode } from 'react'
+import { measureRenders, type MeasureRendersOptions } from 'reassure'
+
+import { ThemeContextProvider } from '../../theme'
+
+const Wrapper: FunctionComponent<{ readonly children: ReactNode }> = ({
+ children,
+}) => {children}
+
+export const measureComponentPerformance = (
+ ui: ReactElement,
+ options?: Omit
+) => {
+ return measureRenders(ui, {
+ warmupRuns: 3,
+ runs: 10,
+ writeFile: true,
+ removeOutliers: true,
+ ...options,
+ wrapper: Wrapper,
+ })
+}
diff --git a/yarn.lock b/yarn.lock
index eff4f9a..b874617 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2509,6 +2509,13 @@ __metadata:
languageName: node
linkType: hard
+"@babel/runtime@npm:^7.25.7":
+ version: 7.28.4
+ resolution: "@babel/runtime@npm:7.28.4"
+ checksum: 10/6c9a70452322ea80b3c9b2a412bcf60771819213a67576c8cec41e88a95bb7bf01fc983754cda35dc19603eef52df22203ccbf7777b9d6316932f9fb77c25163
+ languageName: node
+ linkType: hard
+
"@babel/template@npm:^7.25.0, @babel/template@npm:^7.25.9":
version: 7.25.9
resolution: "@babel/template@npm:7.25.9"
@@ -2696,6 +2703,62 @@ __metadata:
languageName: node
linkType: hard
+"@callstack/reassure-cli@npm:1.4.0":
+ version: 1.4.0
+ resolution: "@callstack/reassure-cli@npm:1.4.0"
+ dependencies:
+ "@callstack/reassure-compare": "npm:1.4.0"
+ "@callstack/reassure-logger": "npm:1.4.0"
+ chalk: "npm:4.1.2"
+ simple-git: "npm:^3.27.0"
+ yargs: "npm:^17.7.2"
+ bin:
+ reassure: lib/commonjs/bin.js
+ checksum: 10/63162c8fc6e424240ef8a1bbeed3e8d0c21ee020e9de420c38203ef8a33a16202f38148be1a732ab7818f2fd4ae9f58121a35b707554bb3c257ce55069c0665a
+ languageName: node
+ linkType: hard
+
+"@callstack/reassure-compare@npm:1.4.0":
+ version: 1.4.0
+ resolution: "@callstack/reassure-compare@npm:1.4.0"
+ dependencies:
+ "@callstack/reassure-logger": "npm:1.4.0"
+ ts-markdown-builder: "npm:0.4.1"
+ ts-regex-builder: "npm:^1.8.2"
+ zod: "npm:^3.24.2"
+ checksum: 10/ae71d84a5bd07410acc725d32b31af123adfe2f3327ff827bfea1bce6a55ca04ab2123929e332a334575ff9d7bda967970d12ee7dc27c1ff047d88b9b49d922b
+ languageName: node
+ linkType: hard
+
+"@callstack/reassure-danger@npm:1.4.0":
+ version: 1.4.0
+ resolution: "@callstack/reassure-danger@npm:1.4.0"
+ checksum: 10/b0f72c407bfc7c7be353573261442915a82e80765b8c632a5fb0f6c5cd6a85824f5c5a4823cdd62675667eaaa22718dcf8da2be9f302bfdb68ad262a0dcd3e61
+ languageName: node
+ linkType: hard
+
+"@callstack/reassure-logger@npm:1.4.0":
+ version: 1.4.0
+ resolution: "@callstack/reassure-logger@npm:1.4.0"
+ dependencies:
+ chalk: "npm:4.1.2"
+ checksum: 10/a65c569a615df2ba489ee36c09ff2065f56686c54e825f7664c59e30e5c31e46d36e105bf851dfcbfc6505f88115d185277a57caf46eb6700afecb7040525e6f
+ languageName: node
+ linkType: hard
+
+"@callstack/reassure-measure@npm:1.4.0":
+ version: 1.4.0
+ resolution: "@callstack/reassure-measure@npm:1.4.0"
+ dependencies:
+ "@callstack/reassure-logger": "npm:1.4.0"
+ mathjs: "npm:^13.2.3"
+ pretty-format: "npm:^29.7.0"
+ peerDependencies:
+ react: ">=18.0.0"
+ checksum: 10/6dcaca3a9c3a81c66fbc16e2cc3410cf75b1a347163fe2e0cb4f6bc0e18ba9a93ce1eafd97534d335685651e3d99589d7d1529187c87eb31471f05e5070dd055
+ languageName: node
+ linkType: hard
+
"@cdek-it/react-native-ui-kit@workspace:.":
version: 0.0.0-use.local
resolution: "@cdek-it/react-native-ui-kit@workspace:."
@@ -2768,6 +2831,7 @@ __metadata:
react-native-reanimated: "npm:3.19.1"
react-native-safe-area-context: "npm:5.6.1"
react-native-svg: "npm:15.12.1"
+ reassure: "npm:1.4.0"
release-it: "npm:19.0.6"
standard-version: "npm:9.5.0"
storybook: "npm:8.3.5"
@@ -4690,6 +4754,22 @@ __metadata:
languageName: node
linkType: hard
+"@kwsites/file-exists@npm:^1.1.1":
+ version: 1.1.1
+ resolution: "@kwsites/file-exists@npm:1.1.1"
+ dependencies:
+ debug: "npm:^4.1.1"
+ checksum: 10/4ff945de7293285133aeae759caddc71e73c4a44a12fac710fdd4f574cce2671a3f89d8165fdb03d383cfc97f3f96f677d8de3c95133da3d0e12a123a23109fe
+ languageName: node
+ linkType: hard
+
+"@kwsites/promise-deferred@npm:^1.1.1":
+ version: 1.1.1
+ resolution: "@kwsites/promise-deferred@npm:1.1.1"
+ checksum: 10/07455477a0123d9a38afb503739eeff2c5424afa8d3dbdcc7f9502f13604488a4b1d9742fc7288832a52a6422cf1e1c0a1d51f69a39052f14d27c9a0420b6629
+ languageName: node
+ linkType: hard
+
"@napi-rs/wasm-runtime@npm:^0.2.11":
version: 0.2.12
resolution: "@napi-rs/wasm-runtime@npm:0.2.12"
@@ -8282,6 +8362,13 @@ __metadata:
languageName: node
linkType: hard
+"complex.js@npm:^2.2.5":
+ version: 2.4.3
+ resolution: "complex.js@npm:2.4.3"
+ checksum: 10/904a2b4a09a4cfd94d8636ceb95e15cc077dcdedd07c54e233308210fb38897338dde4e7113811e89c1cfe4c6e3ebcf11735ad5c901e27c6d7e3c132a3078e3c
+ languageName: node
+ linkType: hard
+
"compressible@npm:~2.0.18":
version: 2.0.18
resolution: "compressible@npm:2.0.18"
@@ -9201,6 +9288,13 @@ __metadata:
languageName: node
linkType: hard
+"decimal.js@npm:^10.4.3":
+ version: 10.6.0
+ resolution: "decimal.js@npm:10.6.0"
+ checksum: 10/c0d45842d47c311d11b38ce7ccc911121953d4df3ebb1465d92b31970eb4f6738a065426a06094af59bee4b0d64e42e7c8984abd57b6767c64ea90cf90bb4a69
+ languageName: node
+ linkType: hard
+
"dedent@npm:0.7.0":
version: 0.7.0
resolution: "dedent@npm:0.7.0"
@@ -10109,6 +10203,13 @@ __metadata:
languageName: node
linkType: hard
+"escape-latex@npm:^1.2.0":
+ version: 1.2.0
+ resolution: "escape-latex@npm:1.2.0"
+ checksum: 10/73a787319f0965ecb8244bb38bf3a3cba872f0b9a5d3da8821140e9f39fe977045dc953a62b1a2bed4d12bfccbe75a7d8ec786412bf00739eaa2f627d0a8e0d6
+ languageName: node
+ linkType: hard
+
"escape-string-regexp@npm:^1.0.5":
version: 1.0.5
resolution: "escape-string-regexp@npm:1.0.5"
@@ -11154,6 +11255,13 @@ __metadata:
languageName: node
linkType: hard
+"fraction.js@npm:^4.3.7":
+ version: 4.3.7
+ resolution: "fraction.js@npm:4.3.7"
+ checksum: 10/bb5ebcdeeffcdc37b68ead3bdfc244e68de188e0c64e9702197333c72963b95cc798883ad16adc21588088b942bca5b6a6ff4aeb1362d19f6f3b629035dc15f5
+ languageName: node
+ linkType: hard
+
"freeport-async@npm:^2.0.0":
version: 2.0.0
resolution: "freeport-async@npm:2.0.0"
@@ -12049,7 +12157,7 @@ __metadata:
languageName: node
linkType: hard
-"import-local@npm:^3.0.2":
+"import-local@npm:^3.0.2, import-local@npm:^3.2.0":
version: 3.2.0
resolution: "import-local@npm:3.2.0"
dependencies:
@@ -12958,6 +13066,13 @@ __metadata:
languageName: node
linkType: hard
+"javascript-natural-sort@npm:^0.7.1":
+ version: 0.7.1
+ resolution: "javascript-natural-sort@npm:0.7.1"
+ checksum: 10/7bf6eab67871865d347f09a95aa770f9206c1ab0226bcda6fdd9edec340bf41111a7f82abac30556aa16a21cfa3b2b1ca4a362c8b73dd5ce15220e5d31f49d79
+ languageName: node
+ linkType: hard
+
"jest-changed-files@npm:^29.7.0":
version: 29.7.0
resolution: "jest-changed-files@npm:29.7.0"
@@ -14478,6 +14593,25 @@ __metadata:
languageName: node
linkType: hard
+"mathjs@npm:^13.2.3":
+ version: 13.2.3
+ resolution: "mathjs@npm:13.2.3"
+ dependencies:
+ "@babel/runtime": "npm:^7.25.7"
+ complex.js: "npm:^2.2.5"
+ decimal.js: "npm:^10.4.3"
+ escape-latex: "npm:^1.2.0"
+ fraction.js: "npm:^4.3.7"
+ javascript-natural-sort: "npm:^0.7.1"
+ seedrandom: "npm:^3.0.5"
+ tiny-emitter: "npm:^2.1.0"
+ typed-function: "npm:^4.2.1"
+ bin:
+ mathjs: bin/cli.js
+ checksum: 10/6906693a97c19f820e280cd7236f0fb07264a855a882fe2c69856508e866fe0c0b1d7054f49fa283bbd20ae8e8355b028bc4f3159e5d543e53a45edef41590e0
+ languageName: node
+ linkType: hard
+
"mdn-data@npm:2.0.14":
version: 2.0.14
resolution: "mdn-data@npm:2.0.14"
@@ -16905,6 +17039,21 @@ __metadata:
languageName: node
linkType: hard
+"reassure@npm:1.4.0":
+ version: 1.4.0
+ resolution: "reassure@npm:1.4.0"
+ dependencies:
+ "@callstack/reassure-cli": "npm:1.4.0"
+ "@callstack/reassure-compare": "npm:1.4.0"
+ "@callstack/reassure-danger": "npm:1.4.0"
+ "@callstack/reassure-measure": "npm:1.4.0"
+ import-local: "npm:^3.2.0"
+ bin:
+ reassure: lib/commonjs/bin/reassure.js
+ checksum: 10/267837079c5efa65107e71e9d79eb44d1fad2d05985ae18a36e3a52b54ac0d905266b397ed6685e895ec13a5c8324c249be1d62f0c61cd40b13f15d1d872cc82
+ languageName: node
+ linkType: hard
+
"recast@npm:^0.23.5":
version: 0.23.9
resolution: "recast@npm:0.23.9"
@@ -17542,6 +17691,13 @@ __metadata:
languageName: node
linkType: hard
+"seedrandom@npm:^3.0.5":
+ version: 3.0.5
+ resolution: "seedrandom@npm:3.0.5"
+ checksum: 10/acad5e516c04289f61c2fb9848f449b95f58362b75406b79ec51e101ec885293fc57e3675d2f39f49716336559d7190f7273415d185fead8cd27b171ebf7d8fb
+ languageName: node
+ linkType: hard
+
"semver@npm:2 || 3 || 4 || 5":
version: 5.7.2
resolution: "semver@npm:5.7.2"
@@ -17821,6 +17977,17 @@ __metadata:
languageName: node
linkType: hard
+"simple-git@npm:^3.27.0":
+ version: 3.30.0
+ resolution: "simple-git@npm:3.30.0"
+ dependencies:
+ "@kwsites/file-exists": "npm:^1.1.1"
+ "@kwsites/promise-deferred": "npm:^1.1.1"
+ debug: "npm:^4.4.0"
+ checksum: 10/65f78b2598950d4f7ce163ad736e7ad358199e13dd84096524b9f10b093f92278d6d40c7390330e7603f1a048db7d7ee1f5ea3a157eb382e691b5c85de549a6b
+ languageName: node
+ linkType: hard
+
"simple-plist@npm:^1.1.0":
version: 1.4.0
resolution: "simple-plist@npm:1.4.0"
@@ -18682,6 +18849,13 @@ __metadata:
languageName: node
linkType: hard
+"tiny-emitter@npm:^2.1.0":
+ version: 2.1.0
+ resolution: "tiny-emitter@npm:2.1.0"
+ checksum: 10/75633f4de4f47f43af56aff6162f25b87be7efc6f669fda256658f3c3f4a216f23dc0d13200c6fafaaf1b0c7142f0201352fb06aec0b77f68aea96be898f4516
+ languageName: node
+ linkType: hard
+
"tiny-invariant@npm:^1.3.3":
version: 1.3.3
resolution: "tiny-invariant@npm:1.3.3"
@@ -18833,6 +19007,13 @@ __metadata:
languageName: node
linkType: hard
+"ts-markdown-builder@npm:0.4.1":
+ version: 0.4.1
+ resolution: "ts-markdown-builder@npm:0.4.1"
+ checksum: 10/85aa963b4b962e2acc6eaa6f2912d9d2742e08b6d28ecdea618a75304418007f81684f79bf3806a29b15237d0f37eec7d00d991132bf4f0ab624bbbada9d1b5b
+ languageName: node
+ linkType: hard
+
"ts-node@npm:10.9.2":
version: 10.9.2
resolution: "ts-node@npm:10.9.2"
@@ -18871,6 +19052,13 @@ __metadata:
languageName: node
linkType: hard
+"ts-regex-builder@npm:^1.8.2":
+ version: 1.8.2
+ resolution: "ts-regex-builder@npm:1.8.2"
+ checksum: 10/d154172ba954a0014c573ab8b0308439d4267653a2be7e06c85449bc506a2aafb7fdeec59f289f8a39c89b49b66c101222b6292382b5be7d91122f7c4662c652
+ languageName: node
+ linkType: hard
+
"tslib@npm:^2.0.1, tslib@npm:^2.1.0, tslib@npm:^2.4.0":
version: 2.8.0
resolution: "tslib@npm:2.8.0"
@@ -19058,6 +19246,13 @@ __metadata:
languageName: node
linkType: hard
+"typed-function@npm:^4.2.1":
+ version: 4.2.2
+ resolution: "typed-function@npm:4.2.2"
+ checksum: 10/6d0eb312d2fa2c4b9af7a6cba5f3e9123e1e7c0b2521a7cd30a1140f862ffb94ad44353b2798a8c160fb40a9b341f7870d3c05589a7b54472514ff6f1b12e6ac
+ languageName: node
+ linkType: hard
+
"typedarray@npm:^0.0.6":
version: 0.0.6
resolution: "typedarray@npm:0.0.6"
@@ -19958,7 +20153,7 @@ __metadata:
languageName: node
linkType: hard
-"yargs@npm:^17.0.0, yargs@npm:^17.3.1, yargs@npm:^17.6.2":
+"yargs@npm:^17.0.0, yargs@npm:^17.3.1, yargs@npm:^17.6.2, yargs@npm:^17.7.2":
version: 17.7.2
resolution: "yargs@npm:17.7.2"
dependencies:
@@ -20017,6 +20212,13 @@ __metadata:
languageName: node
linkType: hard
+"zod@npm:^3.24.2":
+ version: 3.25.76
+ resolution: "zod@npm:3.25.76"
+ checksum: 10/f0c963ec40cd96858451d1690404d603d36507c1fc9682f2dae59ab38b578687d542708a7fdbf645f77926f78c9ed558f57c3d3aa226c285f798df0c4da16995
+ languageName: node
+ linkType: hard
+
"zod@npm:^3.25.0 || ^4.0.0":
version: 4.1.12
resolution: "zod@npm:4.1.12"