diff --git a/jest.config.js b/jest.config.js index 16174fe5..19c62ddf 100644 --- a/jest.config.js +++ b/jest.config.js @@ -18,6 +18,11 @@ module.exports = { '^@/(.*)$': '/src/$1', '^@react-native-community/netinfo$': '/src/__mocks__/@react-native-community/netinfo.js', }, - setupFilesAfterEnv: [], + setupFilesAfterEnv: ['/jest.setup.js'], + // Snapshot serialization is handled by `jest-expo` preset. + + testEnvironment: 'node', }; + + diff --git a/jest.setup.js b/jest.setup.js new file mode 100644 index 00000000..e0a68a8b --- /dev/null +++ b/jest.setup.js @@ -0,0 +1,20 @@ +// Global Jest setup for React Native snapshot/component tests. +// Fixes: +// - @react-native-async-storage/async-storage native module null in Jest +// - Hermes parser crashes when RN internals are loaded during tests + +jest.mock('@react-native-async-storage/async-storage', () => { + return require('@react-native-async-storage/async-storage/jest/async-storage-mock'); +}); + +// Mock RN Text to avoid importing RN native Text internals that can trigger +// Hermes parser issues in Node/Jest. +jest.mock('react-native/Libraries/Text/Text', () => { + const React = require('react'); + return function MockText(props) { + return React.createElement('Text', props, props.children); + }; +}); + + + diff --git a/package.json b/package.json index 86ac0394..51cebeab 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "isr:validate:dry": "node scripts/isr-validate.js --dry-run" }, "dependencies": { + "@graphql-tools/schema": "^10.0.31", "@react-native-async-storage/async-storage": "2.1.2", "@react-native-community/datetimepicker": "^9.1.0", "@react-native-community/netinfo": "11.4.1", @@ -90,10 +91,6 @@ "bullmq": "^5.79.1", "ethers": "^5.8.0", "expo": "~53.0.20", - "@graphql-tools/schema": "^10.0.31", - "graphql": "^16.13.2", - "graphql-http": "^1.22.4", - "pg": "^8.16.0", "expo-application": "~6.1.5", "expo-clipboard": "~7.1.5", "expo-dev-client": "~5.2.4", @@ -101,8 +98,12 @@ "expo-linking": "~7.1.7", "expo-notifications": "^0.31.5", "expo-status-bar": "~2.2.3", + "express": "^4.21.2", + "graphql": "^16.13.2", + "graphql-http": "^1.22.4", "i18next": "^26.0.8", "ioredis": "^5.11.1", + "pg": "^8.16.0", "react": "19.2.5", "react-i18next": "^17.0.6", "react-native": "0.85.2", @@ -114,7 +115,6 @@ "react-native-safe-area-context": "5.7.0", "react-native-screens": "~4.24.0", "react-native-svg": "15.15.4", - "express": "^4.21.2", "zod": "^3.23.8", "zustand": "^5.0.0" }, diff --git a/src/components/common/Button.snapshot.test.tsx b/src/components/common/Button.snapshot.test.tsx new file mode 100644 index 00000000..feac74a7 --- /dev/null +++ b/src/components/common/Button.snapshot.test.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { Platform } from 'react-native'; +import { render } from '@testing-library/react-native'; + +import { ThemeProvider } from '../../context/ThemeContext'; + +import { Button as ButtonComponent } from './Button'; + +jest.mock('../../hooks/useThemeColors', () => ({ + useThemeColors: () => ({ + brand: { primary: '#000000', secondary: '#111111' }, + accent: '#222222', + status: { error: '#ff0000' }, + onPrimary: '#ffffff', + onSecondary: '#ffffff', + background: { card: '#ffffff' }, + border: { default: '#cccccc' }, + }), +})); + + + +describe('Button (snapshot)', () => { + it('renders default primary button', () => { + + + + const { toJSON } = render( + + + + ); + + expect(toJSON()).toMatchSnapshot({ platform: Platform.OS }); + }); + + it('renders disabled state', () => { + const { toJSON } = render( + + + + ); + + expect(toJSON()).toMatchSnapshot({ platform: Platform.OS }); + }); +}); + + diff --git a/src/components/common/Card.snapshot.test.tsx b/src/components/common/Card.snapshot.test.tsx new file mode 100644 index 00000000..0b6d2bf7 --- /dev/null +++ b/src/components/common/Card.snapshot.test.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { Platform, Text } from 'react-native'; +import { render } from '@testing-library/react-native'; + +import { Card } from './Card'; +import { ThemeProvider } from '../../context/ThemeContext'; + +describe('Card (snapshot)', () => { + it('renders default card with children', () => { + const { toJSON } = render( + + + Card content + + + ); + + expect(toJSON()).toMatchSnapshot({ platform: Platform.OS }); + }); +}); + + diff --git a/src/components/subscription/SubscriptionCard.snapshot.test.tsx b/src/components/subscription/SubscriptionCard.snapshot.test.tsx new file mode 100644 index 00000000..dc7ac40b --- /dev/null +++ b/src/components/subscription/SubscriptionCard.snapshot.test.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { Platform } from 'react-native'; +import { render } from '@testing-library/react-native'; +import { SubscriptionCard } from './SubscriptionCard'; +import { mockSubscription } from '../../__fixtures__/subscriptions'; + +// Match existing behavioural tests: SubscriptionCard depends on zustand-persist + AsyncStorage. +jest.mock('@react-native-async-storage/async-storage', () => + require('@react-native-async-storage/async-storage/jest/async-storage-mock') +); + +describe('SubscriptionCard (snapshot)', () => { + it('renders active subscription card', () => { + const { toJSON } = render( + + ); + + expect(toJSON()).toMatchSnapshot({ platform: Platform.OS }); + }); +}); +