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( +