From 62a415a248746ce89d557b6f640617983b071fd7 Mon Sep 17 00:00:00 2001 From: Sam Aryasa Date: Fri, 19 Jul 2024 14:55:16 +0800 Subject: [PATCH] feat: add unit tests --- jest.setup.ts | 1 + package.json | 5 + src/CreditCardInput.tsx | 10 +- src/LiteCreditCardInput.tsx | 10 +- src/__tests__/CreditCardInput.test.tsx | 135 +++++++++++++++++++++ src/__tests__/LiteCreditCardInput.test.tsx | 135 +++++++++++++++++++++ src/__tests__/index.test.tsx | 1 - src/useCreditCardForm.tsx | 1 - yarn.lock | 38 +++++- 9 files changed, 330 insertions(+), 6 deletions(-) create mode 100644 jest.setup.ts create mode 100644 src/__tests__/CreditCardInput.test.tsx create mode 100644 src/__tests__/LiteCreditCardInput.test.tsx delete mode 100644 src/__tests__/index.test.tsx diff --git a/jest.setup.ts b/jest.setup.ts new file mode 100644 index 00000000..1d3ff307 --- /dev/null +++ b/jest.setup.ts @@ -0,0 +1 @@ +import '@testing-library/react-native/extend-expect'; diff --git a/package.json b/package.json index 198d39c1..73d52d2f 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "@evilmartians/lefthook": "^1.5.0", "@react-native/eslint-config": "^0.73.1", "@release-it/conventional-changelog": "^5.0.0", + "@testing-library/react-native": "^12.5.1", "@types/jest": "^29.5.5", "@types/react": "^18.2.44", "@types/react-native-flip-card": "^3.5.7", @@ -76,6 +77,7 @@ "react": "18.2.0", "react-native": "0.74.3", "react-native-builder-bob": "^0.25.0", + "react-test-renderer": "^18.3.1", "release-it": "^15.0.0", "typescript": "^5.2.2" }, @@ -95,6 +97,9 @@ "modulePathIgnorePatterns": [ "/example/node_modules", "/lib/" + ], + "setupFilesAfterEnv": [ + "/jest.setup.ts" ] }, "commitlint": { diff --git a/src/CreditCardInput.tsx b/src/CreditCardInput.tsx index bd34f334..cbbc6b48 100644 --- a/src/CreditCardInput.tsx +++ b/src/CreditCardInput.tsx @@ -31,6 +31,7 @@ interface Props { }; onChange: (formData: CreditCardFormData) => void; onFocusField?: (field: CreditCardFormField) => void; + testID?: string; } const s = StyleSheet.create({ @@ -89,6 +90,7 @@ const CreditCardInput = (props: Props) => { }, onChange = () => {}, onFocusField = () => {}, + testID, } = props; const { values, onChangeValue } = useCreditCardForm(onChange); @@ -100,7 +102,10 @@ const CreditCardInput = (props: Props) => { }, [autoFocus]); return ( - + {labels.number} { onFocus={() => onFocusField('number')} autoCorrect={false} underlineColorAndroid={'transparent'} + testID="CC_NUMBER" /> @@ -130,6 +136,7 @@ const CreditCardInput = (props: Props) => { onFocus={() => onFocusField('expiry')} autoCorrect={false} underlineColorAndroid={'transparent'} + testID="CC_EXPIRY" /> @@ -145,6 +152,7 @@ const CreditCardInput = (props: Props) => { onFocus={() => onFocusField('cvc')} autoCorrect={false} underlineColorAndroid={'transparent'} + testID="CC_CVC" /> diff --git a/src/LiteCreditCardInput.tsx b/src/LiteCreditCardInput.tsx index 08e316bb..cec6e942 100644 --- a/src/LiteCreditCardInput.tsx +++ b/src/LiteCreditCardInput.tsx @@ -28,6 +28,7 @@ interface Props { }; onChange?: (formData: CreditCardFormData) => void; onFocusField?: (field: CreditCardFormField) => void; + testID?: string; } const s = StyleSheet.create({ @@ -94,6 +95,7 @@ const LiteCreditCardInput = (props: Props) => { }, onChange = () => {}, onFocusField = () => {}, + testID, } = props; const _onChange = (formData: CreditCardFormData): void => { @@ -133,7 +135,10 @@ const LiteCreditCardInput = (props: Props) => { }, [values.type]); return ( - + { onFocus={() => onFocusField('number')} autoCorrect={false} underlineColorAndroid={'transparent'} + testID="CC_NUMBER" /> @@ -193,6 +199,7 @@ const LiteCreditCardInput = (props: Props) => { onFocus={() => onFocusField('expiry')} autoCorrect={false} underlineColorAndroid={'transparent'} + testID="CC_EXPIRY" /> @@ -208,6 +215,7 @@ const LiteCreditCardInput = (props: Props) => { onFocus={() => onFocusField('cvc')} autoCorrect={false} underlineColorAndroid={'transparent'} + testID="CC_CVC" /> diff --git a/src/__tests__/CreditCardInput.test.tsx b/src/__tests__/CreditCardInput.test.tsx new file mode 100644 index 00000000..f259de12 --- /dev/null +++ b/src/__tests__/CreditCardInput.test.tsx @@ -0,0 +1,135 @@ +import { + render, + screen, + userEvent, + within, +} from '@testing-library/react-native'; +import CreditCardInput from '../CreditCardInput'; + +describe('CreditCardInput', () => { + let onChange: ReturnType; + let user: ReturnType; + let cardInput: ReturnType; + + beforeEach(() => { + onChange = jest.fn(); + + user = userEvent.setup(); + render( + + ); + + cardInput = within(screen.getByTestId('CARD_INPUT')); + }); + + it('should validate and format valid credit-card information', async () => { + await user.type(cardInput.getByTestId('CC_NUMBER'), '4242424242424242'); + await user.type(cardInput.getByTestId('CC_EXPIRY'), '233'); + await user.type(cardInput.getByTestId('CC_CVC'), '333'); + + expect(onChange).toHaveBeenLastCalledWith({ + valid: true, + status: { + number: 'valid', + expiry: 'valid', + cvc: 'valid', + }, + values: { + number: '4242 4242 4242 4242', + expiry: '02/33', + cvc: '333', + type: 'visa', + }, + }); + }); + + it('should ignores non number characters ', async () => { + await user.type( + cardInput.getByTestId('CC_NUMBER'), + '--drop db "users" 4242-4242-4242-4242' + ); + await user.type(cardInput.getByTestId('CC_EXPIRY'), '#$!@#!@# 12/33'); + await user.type(cardInput.getByTestId('CC_CVC'), 'lorem ipsum 333'); + + expect(onChange).toHaveBeenLastCalledWith( + expect.objectContaining({ + values: expect.objectContaining({ + number: '4242 4242 4242 4242', + expiry: '12/33', + cvc: '333', + }), + }) + ); + }); + + it('should return validation error for invalid card information', async () => { + await user.type(cardInput.getByTestId('CC_NUMBER'), '5555 5555 5555 4443'); + await user.type(cardInput.getByTestId('CC_EXPIRY'), '02 / 99'); + await user.type(cardInput.getByTestId('CC_CVC'), '33'); + + expect(onChange).toHaveBeenLastCalledWith( + expect.objectContaining({ + status: { + number: 'invalid', // failed crc + expiry: 'invalid', // too far in the future + cvc: 'incomplete', // cvv is too short + }, + }) + ); + }); + + it('should return credit card issuer based on card number', async () => { + const numberField = cardInput.getByTestId('CC_NUMBER'); + + await user.clear(numberField); + await user.type(numberField, '4242424242424242'); + expect(onChange).toHaveBeenLastCalledWith( + expect.objectContaining({ + values: expect.objectContaining({ type: 'visa' }), + }) + ); + + await user.clear(numberField); + await user.type(numberField, '5555555555554444'); + expect(onChange).toHaveBeenLastCalledWith( + expect.objectContaining({ + values: expect.objectContaining({ type: 'mastercard' }), + }) + ); + + await user.clear(numberField); + await user.type(numberField, '371449635398431'); + expect(onChange).toHaveBeenLastCalledWith( + expect.objectContaining({ + values: expect.objectContaining({ type: 'american-express' }), + }) + ); + + await user.clear(numberField); + await user.type(numberField, '6011111111111117'); + expect(onChange).toHaveBeenLastCalledWith( + expect.objectContaining({ + values: expect.objectContaining({ type: 'discover' }), + }) + ); + + await user.clear(numberField); + await user.type(numberField, '3056930009020004'); + expect(onChange).toHaveBeenLastCalledWith( + expect.objectContaining({ + values: expect.objectContaining({ type: 'diners-club' }), + }) + ); + + await user.clear(numberField); + await user.type(numberField, '3566002020360505'); + expect(onChange).toHaveBeenLastCalledWith( + expect.objectContaining({ + values: expect.objectContaining({ type: 'jcb' }), + }) + ); + }); +}); diff --git a/src/__tests__/LiteCreditCardInput.test.tsx b/src/__tests__/LiteCreditCardInput.test.tsx new file mode 100644 index 00000000..bf7f39b3 --- /dev/null +++ b/src/__tests__/LiteCreditCardInput.test.tsx @@ -0,0 +1,135 @@ +import { + render, + screen, + userEvent, + within, +} from '@testing-library/react-native'; +import LiteCreditCardInput from '../LiteCreditCardInput'; + +describe('LiteCreditCardInput', () => { + let onChange: ReturnType; + let user: ReturnType; + let cardInput: ReturnType; + + beforeEach(() => { + onChange = jest.fn(); + + user = userEvent.setup(); + render( + + ); + + cardInput = within(screen.getByTestId('CARD_INPUT')); + }); + + it('should validate and format valid credit-card information', async () => { + await user.type(cardInput.getByTestId('CC_NUMBER'), '4242424242424242'); + await user.type(cardInput.getByTestId('CC_EXPIRY'), '233'); + await user.type(cardInput.getByTestId('CC_CVC'), '333'); + + expect(onChange).toHaveBeenLastCalledWith({ + valid: true, + status: { + number: 'valid', + expiry: 'valid', + cvc: 'valid', + }, + values: { + number: '4242 4242 4242 4242', + expiry: '02/33', + cvc: '333', + type: 'visa', + }, + }); + }); + + it('should ignores non number characters ', async () => { + await user.type( + cardInput.getByTestId('CC_NUMBER'), + '--drop db "users" 4242-4242-4242-4242' + ); + await user.type(cardInput.getByTestId('CC_EXPIRY'), '#$!@#!@# 12/33'); + await user.type(cardInput.getByTestId('CC_CVC'), 'lorem ipsum 333'); + + expect(onChange).toHaveBeenLastCalledWith( + expect.objectContaining({ + values: expect.objectContaining({ + number: '4242 4242 4242 4242', + expiry: '12/33', + cvc: '333', + }), + }) + ); + }); + + it('should return validation error for invalid card information', async () => { + await user.type(cardInput.getByTestId('CC_NUMBER'), '5555 5555 5555 4443'); + await user.type(cardInput.getByTestId('CC_EXPIRY'), '02 / 99'); + await user.type(cardInput.getByTestId('CC_CVC'), '33'); + + expect(onChange).toHaveBeenLastCalledWith( + expect.objectContaining({ + status: { + number: 'invalid', // failed crc + expiry: 'invalid', // too far in the future + cvc: 'incomplete', // cvv is too short + }, + }) + ); + }); + + it('should return credit card issuer based on card number', async () => { + const numberField = cardInput.getByTestId('CC_NUMBER'); + + await user.clear(numberField); + await user.type(numberField, '4242424242424242'); + expect(onChange).toHaveBeenLastCalledWith( + expect.objectContaining({ + values: expect.objectContaining({ type: 'visa' }), + }) + ); + + await user.clear(numberField); + await user.type(numberField, '5555555555554444'); + expect(onChange).toHaveBeenLastCalledWith( + expect.objectContaining({ + values: expect.objectContaining({ type: 'mastercard' }), + }) + ); + + await user.clear(numberField); + await user.type(numberField, '371449635398431'); + expect(onChange).toHaveBeenLastCalledWith( + expect.objectContaining({ + values: expect.objectContaining({ type: 'american-express' }), + }) + ); + + await user.clear(numberField); + await user.type(numberField, '6011111111111117'); + expect(onChange).toHaveBeenLastCalledWith( + expect.objectContaining({ + values: expect.objectContaining({ type: 'discover' }), + }) + ); + + await user.clear(numberField); + await user.type(numberField, '3056930009020004'); + expect(onChange).toHaveBeenLastCalledWith( + expect.objectContaining({ + values: expect.objectContaining({ type: 'diners-club' }), + }) + ); + + await user.clear(numberField); + await user.type(numberField, '3566002020360505'); + expect(onChange).toHaveBeenLastCalledWith( + expect.objectContaining({ + values: expect.objectContaining({ type: 'jcb' }), + }) + ); + }); +}); diff --git a/src/__tests__/index.test.tsx b/src/__tests__/index.test.tsx deleted file mode 100644 index bf84291a..00000000 --- a/src/__tests__/index.test.tsx +++ /dev/null @@ -1 +0,0 @@ -it.todo('write a test'); diff --git a/src/useCreditCardForm.tsx b/src/useCreditCardForm.tsx index 8e46d629..bb6090d5 100644 --- a/src/useCreditCardForm.tsx +++ b/src/useCreditCardForm.tsx @@ -138,7 +138,6 @@ export const useCreditCardForm = ( cardValidator.expirationDate(newFormattedValues.expiry) ), cvc: toStatus(cardValidator.cvv(newFormattedValues.cvc, cvcMaxLength)), - type: numberValidation.card?.type, }; setValues(newFormattedValues); diff --git a/yarn.lock b/yarn.lock index 15ae6b6f..97379d80 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3550,6 +3550,25 @@ __metadata: languageName: node linkType: hard +"@testing-library/react-native@npm:^12.5.1": + version: 12.5.1 + resolution: "@testing-library/react-native@npm:12.5.1" + dependencies: + jest-matcher-utils: ^29.7.0 + pretty-format: ^29.7.0 + redent: ^3.0.0 + peerDependencies: + jest: ">=28.0.0" + react: ">=16.8.0" + react-native: ">=0.59" + react-test-renderer: ">=16.8.0" + peerDependenciesMeta: + jest: + optional: true + checksum: 5fa582184ae99244d0175f72d7804659393d6952510b7ff2a85c43ea0dd043bb1c8ab79fe52d73ceca3c2dddbc0df7c3b1fb2915a2aff1ac98f4423b202c6146 + languageName: node + linkType: hard + "@tsconfig/node10@npm:^1.0.7": version: 1.0.11 resolution: "@tsconfig/node10@npm:1.0.11" @@ -12514,7 +12533,7 @@ __metadata: languageName: node linkType: hard -"react-is@npm:^16.12.0 || ^17.0.0 || ^18.0.0, react-is@npm:^18.0.0": +"react-is@npm:^16.12.0 || ^17.0.0 || ^18.0.0, react-is@npm:^18.0.0, react-is@npm:^18.3.1": version: 18.3.1 resolution: "react-is@npm:18.3.1" checksum: e20fe84c86ff172fc8d898251b7cc2c43645d108bf96d0b8edf39b98f9a2cae97b40520ee7ed8ee0085ccc94736c4886294456033304151c3f94978cec03df21 @@ -12588,6 +12607,7 @@ __metadata: "@evilmartians/lefthook": ^1.5.0 "@react-native/eslint-config": ^0.73.1 "@release-it/conventional-changelog": ^5.0.0 + "@testing-library/react-native": ^12.5.1 "@types/jest": ^29.5.5 "@types/react": ^18.2.44 "@types/react-native-flip-card": ^3.5.7 @@ -12603,6 +12623,7 @@ __metadata: react-native: 0.74.3 react-native-builder-bob: ^0.25.0 react-native-flip-card: ^3.5.7 + react-test-renderer: ^18.3.1 release-it: ^15.0.0 typescript: ^5.2.2 peerDependencies: @@ -12712,6 +12733,19 @@ __metadata: languageName: node linkType: hard +"react-test-renderer@npm:^18.3.1": + version: 18.3.1 + resolution: "react-test-renderer@npm:18.3.1" + dependencies: + react-is: ^18.3.1 + react-shallow-renderer: ^16.15.0 + scheduler: ^0.23.2 + peerDependencies: + react: ^18.3.1 + checksum: e8e58e738835fab3801afb63f6bfe0fcf6e68ea39619fae5bdf47feefc36b1e4acb48c9dd139c7533611466eff1dfce6ffdda4b317e06aee663dda7d91438f26 + languageName: node + linkType: hard + "react@npm:18.2.0": version: 18.2.0 resolution: "react@npm:18.2.0" @@ -13382,7 +13416,7 @@ __metadata: languageName: node linkType: hard -"scheduler@npm:^0.23.0": +"scheduler@npm:^0.23.0, scheduler@npm:^0.23.2": version: 0.23.2 resolution: "scheduler@npm:0.23.2" dependencies: