Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle Invalid scale and arcDegreeScale values in segments to avoid crash #114

Open
wants to merge 10 commits into
base: development
Choose a base branch
from
28 changes: 6 additions & 22 deletions src/SegmentedArc.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ import Svg from 'react-native-svg';
import Segment from './components/Segment';
import Cap from './components/Cap';
import RangesDisplay from './components/RangesDisplay';
import { ensureDefaultSegmentScaleValues } from './utils/scale';
import { useShowSegmentedArcWarnings } from './hooks/useSegmentedArcWarning';

const SegmentedArcContext = createContext();
const DEFAULT_SEGMENTS = [];

export const SegmentedArc = ({
fillValue = 0,
segments = [],
segments: segmentsProps = DEFAULT_SEGMENTS,
filledArcWidth = 8,
emptyArcWidth = 8,
spaceBetweenSegments = 2,
Expand All @@ -30,8 +33,10 @@ export const SegmentedArc = ({
alignRangesWithSegments = true,
children
}) => {
useShowSegmentedArcWarnings({ segments: segmentsProps });
const [arcAnimatedValue] = useState(new Animated.Value(0));
const animationRunning = useRef(false);
const segments = ensureDefaultSegmentScaleValues(segmentsProps);

if (segments.length === 0) {
return null;
Expand Down Expand Up @@ -67,29 +72,8 @@ export const SegmentedArc = ({
...middleContentContainerStyle
};

const _ensureDefaultSegmentScale = () => {
const segmentsWithoutScale = segments.filter(segment => !segment.scale);
const allocatedScale = segments.reduce((total, current) => total + (current.scale || 0), 0);
const defaultArcScale = (1 - allocatedScale) / segmentsWithoutScale.length;
segmentsWithoutScale.forEach(segment => (segment.scale = defaultArcScale));
};

const _ensureDefaultArcScale = () => {
const segmentsWithoutArcDegreeScaleLength = segments.filter(
segment => typeof segment.arcDegreeScale !== 'number'
).length;
const totalProvidedArcDegreeScale = segments.reduce((acc, segment) => acc + (segment.arcDegreeScale ?? 0), 0);
segments.forEach(segment => {
if (typeof segment.arcDegreeScale !== 'number')
segment.arcDegreeScale = (1 - totalProvidedArcDegreeScale) / segmentsWithoutArcDegreeScaleLength;
});
};

let remainingValue = fillValue;

_ensureDefaultSegmentScale();
_ensureDefaultArcScale();

let arcs = [];
segments.forEach((segment, index) => {
const arcDegreeScale = segment.arcDegreeScale;
Expand Down
26 changes: 26 additions & 0 deletions src/__tests__/SegmentedArc.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import { Text, Animated, Easing } from 'react-native';
import { SegmentedArc } from '../SegmentedArc';
import { render } from '@testing-library/react-native';
import { createInvalidScaleValueError } from '../utils/segmentedArcWarnings';

describe('SegmentedArc', () => {
let segments = [
Expand Down Expand Up @@ -44,6 +45,7 @@ describe('SegmentedArc', () => {
Easing.out = jest.fn();
Easing.ease = jest.fn();
Animated.timing.mockReturnValue({ start: jest.fn() });
jest.spyOn(console, 'warn').mockImplementation();
props = { segments, fillValue: 50 };
});

Expand All @@ -65,6 +67,30 @@ describe('SegmentedArc', () => {
expect(wrapper.queryByTestId(testId)).toBeNull();
});

it('does not warn about invalid segment scale in production', () => {
const currentGlobalDev = global.__DEV__;
global.__DEV__ = false;

wrapper = getWrapper({ ...props, segments: [{ ...props.segments[0], scale: NaN }] });

expect(console.warn).not.toHaveBeenCalled();
global.__DEV__ = currentGlobalDev;
});

it('show warnings and renders the component when segments have invalid scale or arcDegreeScale data', () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adding s

Suggested change
it('show warnings and renders the component when segments have invalid scale or arcDegreeScale data', () => {
it('shows warnings and renders the component when segments have invalid scale or arcDegreeScale data', () => {

wrapper = getWrapper({
...props,
segments: [
{ arcDegreeScale: NaN, emptyColor: '#F3F3F4', filledColor: '#502D91' },
{ scale: NaN, emptyColor: '#F3F3F4', filledColor: '#177CBA' },
{ emptyColor: '#F3F3F4', filledColor: '#CF5625' }
]
});
expect(console.warn).toHaveBeenCalledWith(createInvalidScaleValueError('scale', NaN));
expect(console.warn).toHaveBeenCalledWith(createInvalidScaleValueError('arcDegreeScale', NaN));
expect(wrapper.getByTestId(testId).props).toMatchSnapshot();
});

it("automatically increases the component's height when arcDegree is greater than 180 degrees", () => {
wrapper = getWrapper({ ...props, arcDegree: 270 });
expect(wrapper.getByTestId(testId).props).toMatchSnapshot();
Expand Down
137 changes: 137 additions & 0 deletions src/__tests__/__snapshots__/SegmentedArc.spec.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1226,3 +1226,140 @@ exports[`SegmentedArc sets the last segment for lastFilledSegment prop when fill
"testID": "container",
}
`;

exports[`SegmentedArc show warnings and renders the component when segments have invalid scale or arcDegreeScale data 1`] = `
{
"children": [
<Svg
height={126}
preserveAspectRatio="xMidYMid meet"
width={240}
>
<Context.Provider
value={
{
"alignRangesWithSegments": true,
"animationDuration": 1000,
"arcAnimatedValue": 0,
"arcSegmentDegree": 58.666666666666664,
"arcs": [
{
"centerX": 120,
"centerY": 120,
"data": undefined,
"emptyColor": "#F3F3F4",
"end": 58.666666666666664,
"filled": 58.666666666666664,
"filledColor": "#502D91",
"isComplete": true,
"start": 0,
},
{
"centerX": 120,
"centerY": 120,
"data": undefined,
"emptyColor": "#F3F3F4",
"end": 119.33333333333333,
"filled": 90.00000000000001,
"filledColor": "#177CBA",
"isComplete": false,
"start": 60.666666666666664,
},
{
"centerX": 120,
"centerY": 120,
"data": undefined,
"emptyColor": "#F3F3F4",
"end": 180,
"filled": 121.33333333333333,
"filledColor": "#CF5625",
"isComplete": false,
"start": 121.33333333333333,
},
],
"arcsStart": 0,
"capInnerColor": "#28E037",
"capOuterColor": "#FFFFFF",
"center": 120,
"emptyArcWidth": 8,
"filledArcWidth": 8,
"isAnimated": true,
"lastFilledSegment": {
"centerX": 120,
"centerY": 120,
"data": undefined,
"emptyColor": "#F3F3F4",
"end": 119.33333333333333,
"filled": 90.00000000000001,
"filledColor": "#177CBA",
"isComplete": false,
"start": 60.666666666666664,
},
"margin": 12,
"radius": 100,
"ranges": [],
"rangesTextColor": "#000000",
"rangesTextStyle": {
"fontSize": 12,
},
"spaceBetweenSegments": 2,
"totalArcs": 3,
}
}
>
<Segment
arc={
{
"centerX": 120,
"centerY": 120,
"data": undefined,
"emptyColor": "#F3F3F4",
"end": 58.666666666666664,
"filled": 58.666666666666664,
"filledColor": "#502D91",
"isComplete": true,
"start": 0,
}
}
/>
<Segment
arc={
{
"centerX": 120,
"centerY": 120,
"data": undefined,
"emptyColor": "#F3F3F4",
"end": 119.33333333333333,
"filled": 90.00000000000001,
"filledColor": "#177CBA",
"isComplete": false,
"start": 60.666666666666664,
}
}
/>
<Segment
arc={
{
"centerX": 120,
"centerY": 120,
"data": undefined,
"emptyColor": "#F3F3F4",
"end": 180,
"filled": 121.33333333333333,
"filledColor": "#CF5625",
"isComplete": false,
"start": 121.33333333333333,
}
}
/>
<Cap />
</Context.Provider>
</Svg>,
undefined,
],
"style": {
"alignItems": "center",
},
"testID": "container",
}
`;
14 changes: 14 additions & 0 deletions src/hooks/useSegmentedArcWarning.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useEffect, useId, useRef } from 'react';
import { warnAboutInvalidSegmentsData } from '../utils/segmentedArcWarnings';

export const useShowSegmentedArcWarnings = ({ segments }) => {
// Conditionally render hook, because this is not useful in production
if (!__DEV__) return;

const { current: currentSegments } = useRef(segments);
const id = useId();

useEffect(() => {
warnAboutInvalidSegmentsData(currentSegments, id);
}, [currentSegments]);
};
Loading
Loading