diff --git a/app/components/animated_number/index.test.tsx b/app/components/animated_number/index.test.tsx new file mode 100644 index 000000000..472d978a8 --- /dev/null +++ b/app/components/animated_number/index.test.tsx @@ -0,0 +1,133 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {fireEvent, render, waitFor, screen} from '@testing-library/react-native'; +import React from 'react'; +import {Animated} from 'react-native'; + +import AnimatedNumber from '.'; + +const NUMBER_HEIGHT = 10; + +describe('AnimatedNumber', () => { + // running on jest, since Animated is a native module, Animated.timing.start needs to be mocked in order to update to the final Animated.Value. + // Ex: 1 => 2, the Animated.Value should be -20 (from -10) after the animation is done + jest.spyOn(Animated, 'timing').mockImplementation((a, b) => ({ + + // @ts-expect-error mock implementation for testing + start: jest.fn().mockImplementation(() => a.setValue(b.toValue)), + }) as unknown as Animated.CompositeAnimation); + + it('should render the non-animated number', () => { + render(); + + const text = screen.getByTestId('no-animation-number'); + expect(text.children).toContainEqual('123'); + }); + + it('should removed the non-animation number after getting the correct height', () => { + render(); + + const text = screen.getByTestId('no-animation-number'); + + fireEvent(text, 'onLayout', {nativeEvent: {layout: {height: NUMBER_HEIGHT}}}); + + const removedText = screen.queryByTestId('no-animation-number'); + + expect(removedText).toBeNull(); + }); + + it('should switch to the animated number view', async () => { + render(); + + const text = screen.getByTestId('no-animation-number'); + + fireEvent(text, 'onLayout', {nativeEvent: {layout: {height: NUMBER_HEIGHT}}}); + + const animatedView = screen.getByTestId('animation-number-main'); + expect(animatedView).toBeTruthy(); + }); + + describe.each([1, 23, 579, -123, 6789, 23456])('should show the correct number of animated views based on the digits', (animateToNumber: number) => { + const numberOfDigits = animateToNumber.toString().length; + it(`should display ${numberOfDigits} view(s) for ${animateToNumber}`, async () => { + render(); + + const text = screen.getByTestId('no-animation-number'); + + fireEvent(text, 'onLayout', {nativeEvent: {layout: {height: NUMBER_HEIGHT}}}); + + await waitFor(() => { + const animatedView = screen.getByTestId('animation-number-main'); + expect(animatedView.children).toHaveLength(numberOfDigits); + }); + }); + }); + + describe.each([123, 9982, 12345, 901876, -157])('should show the correct number', (animateToNumber: number) => { + const absAnimatedNumberString = String(Math.abs(animateToNumber)); + const numberOfDigits = absAnimatedNumberString.length; + it(`should display the number ${animateToNumber}`, async () => { + render(); + + const text = screen.getByTestId('no-animation-number'); + + fireEvent(text, 'onLayout', {nativeEvent: {layout: {height: NUMBER_HEIGHT}}}); + + const checkEachDigit = absAnimatedNumberString.split('').map(async (number, index) => { + const useIndex = numberOfDigits - 1 - index; + + // every digit will have a row of 10 numbers, so the translateY should be the height of the number * the number * -1 (since the animation is going up) + const transformedView = screen.getByTestId(`animated-number-view-${useIndex}`); + const {translateY} = transformedView.props.style.transform[0]; + + expect(Math.abs(translateY / NUMBER_HEIGHT)).toEqual(Number(number)); + }); + + await Promise.all(checkEachDigit); + }); + }); + + describe.each([146, 144, 1, 1000000, -145])('should rerender the correct number that it animates to', (animateToNumber: number) => { + it(`should display the number ${animateToNumber}`, async () => { + const startingNumber = 145; + render(); + + const text = screen.getByTestId('no-animation-number'); + + fireEvent(text, 'onLayout', {nativeEvent: {layout: {height: NUMBER_HEIGHT}}}); + + screen.rerender(); + + const animateToNumberString = String(Math.abs(animateToNumber)); + const checkEachDigit = animateToNumberString.split('').map(async (number, index) => { + const useIndex = animateToNumberString.length - 1 - index; + + const transformedView = screen.getByTestId(`animated-number-view-${useIndex}`); + const {translateY} = transformedView.props.style.transform[0]; + + expect(Math.abs((translateY) / NUMBER_HEIGHT)).toEqual(Number(number)); + }); + + await Promise.all(checkEachDigit); + }); + }); + + it('KNOWN UI BUG: should show that there will be an issue if the text height changes, due to the non-animated number view has been removed', async () => { + // the number text will get cut-off if the user changes the text size on their mobile devices + render(); + + const text = screen.getByTestId('no-animation-number'); + + fireEvent(text, 'onLayout', {nativeEvent: {layout: {height: NUMBER_HEIGHT}}}); + + try { + fireEvent(text, 'onLayout', {nativeEvent: {layout: {height: NUMBER_HEIGHT + NUMBER_HEIGHT}}}); + } catch (e) { + expect(e).toEqual(new Error('Unable to find node on an unmounted component.')); + } + + const animatedView = screen.getByTestId('animation-number-main'); + expect(animatedView).toBeTruthy(); + }); +}); diff --git a/app/components/animated_number/index.tsx b/app/components/animated_number/index.tsx index 3b8ee973f..f8a61328f 100644 --- a/app/components/animated_number/index.tsx +++ b/app/components/animated_number/index.tsx @@ -94,43 +94,54 @@ const AnimatedNumber = ({ return ( <> {numberHeight !== 0 && ( - + {animateToNumber < 0 && ( {'-'} )} - {Array.from(animateToNumberString, Number).map((_, index) => ( - - { + const useIndex = animateToNumberString.length - 1 - index; + return ( + - {NUMBERS.map((number, i) => ( - - - {number} - - - ))} - - - ))} + + {NUMBERS.map((number, i) => ( + + + {number} + + + ))} + + + ); + })} )} {numberHeight === 0 && {animateToNumberString}