Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import React from 'react';
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Formik } from 'formik';
import { DeepPartial } from 'ts-essentials';
import {
NsGoalCalculatorTestWrapper,
defaultGoalCalculation,
} from '../NsGoalCalculatorTestWrapper';
import { GoalSettingsPreviewProvider } from './GoalSettingsPreviewContext';
import { PreviewNewStaffGoalCalculationMutation } from './NewStaffGoalCalculation.generated';
import { FinancialInformationSection } from './Sections/FinancialInformationSection';
import { NsoInformationSection } from './Sections/NsoInformationSection';
import { calculationToFormValues } from './goalSettingsApiMapping';
import { GoalSettingsSectionProps } from './goalSettingsSectionProps';

const accountListId = 'account-list-1';
const mutationSpy = jest.fn();

const savedCalculation = {
...defaultGoalCalculation,
calculations: {
...defaultGoalCalculation.calculations,
contributing403bAmount: 150,
spouseContributing403bAmount: 200,
specialNeedsLeft: 900,
},
};

const sectionProps: GoalSettingsSectionProps = {
hasSpouse: true,
seniorStaff: false,
calculations: {
...savedCalculation.calculations,
contributing403bAmount: 1,
spouseContributing403bAmount: 2,
specialNeedsLeft: 3,
},
primaryName: 'John',
spouseName: 'Jane',
visibleHeaders: ['John (Joining)', 'Jane (Joining)'],
sharedHeader: 'John (Joining) & Jane (Joining)',
};

const previewOf = (calculations: {
contributing403bAmount?: number;
spouseContributing403bAmount?: number;
specialNeedsLeft?: number;
}): DeepPartial<PreviewNewStaffGoalCalculationMutation> => ({
previewNewStaffGoalCalculation: {
newStaffGoalCalculation: {
id: savedCalculation.id,
calculations,
},
},
});

const preview403b = previewOf({
contributing403bAmount: 175,
spouseContributing403bAmount: 210,
});

const TestComponent: React.FC<{
previewMock?: DeepPartial<PreviewNewStaffGoalCalculationMutation>;
}> = ({ previewMock }) => (
<NsGoalCalculatorTestWrapper previewMock={previewMock} onCall={mutationSpy}>
<Formik
initialValues={calculationToFormValues(savedCalculation)}
onSubmit={jest.fn()}
>
<GoalSettingsPreviewProvider
accountListId={accountListId}
calculation={savedCalculation}
>
<FinancialInformationSection {...sectionProps} />
<NsoInformationSection {...sectionProps} />
</GoalSettingsPreviewProvider>
</Formik>
</NsGoalCalculatorTestWrapper>
);

describe('GoalSettingsPreviewContext', () => {
it('shows the saved worksheet figures while the form is untouched', async () => {
Comment thread
zweatshirt marked this conversation as resolved.
const { findByText, getByText } = render(<TestComponent />);

const johnAmount = (await findByText('403(b) Amount — John')).parentElement;
expect(johnAmount).toHaveTextContent('$150.00');
expect(johnAmount).toHaveAttribute('aria-busy', 'false');
expect(getByText('403(b) Amount — Jane').parentElement).toHaveTextContent(
'$200.00',
);
expect(getByText('$900.00')).toBeInTheDocument();

expect(mutationSpy).not.toHaveGraphqlOperation(
'PreviewNewStaffGoalCalculation',
);
});

it('substitutes the previewed 403(b) amounts after an unsaved edit', async () => {
const { findByText, getByText, getByRole } = render(
<TestComponent previewMock={preview403b} />,
);

const percentage = getByRole('spinbutton', {
name: '403(b) Contribution — John',
});
userEvent.clear(percentage);
userEvent.type(percentage, '10');
expect(await findByText('$175.00')).toBeInTheDocument();
expect(getByText('403(b) Amount — John').parentElement).toHaveTextContent(
'$175.00',
);
expect(getByText('403(b) Amount — Jane').parentElement).toHaveTextContent(
'$210.00',
);
expect(mutationSpy).toHaveGraphqlOperation(
'PreviewNewStaffGoalCalculation',
{
input: {
accountListId,
id: savedCalculation.id,
attributes: { contribution403bPercentage: 10 },
},
},
);
});

it('substitutes the previewed special-needs remainder after an unsaved edit', async () => {
const { findByText, getByRole } = render(
<TestComponent previewMock={previewOf({ specialNeedsLeft: 450 })} />,
);

const supportRaised = getByRole('spinbutton', {
name: 'Support Raised for NSO',
});
userEvent.clear(supportRaised);
userEvent.type(supportRaised, '50');

expect(await findByText('$450.00')).toBeInTheDocument();
expect(mutationSpy).toHaveGraphqlOperation(
'PreviewNewStaffGoalCalculation',
{
input: {
accountListId,
id: savedCalculation.id,
attributes: { nsoSpecialNeedsSupportReceived: 50 },
},
},
);
});

it('holds the previewed amount while a further edit is still in flight', async () => {
const { findByText, getByText, queryByText, getByRole } = render(
<TestComponent previewMock={preview403b} />,
);

const percentage = getByRole('spinbutton', {
name: '403(b) Contribution — John',
});
userEvent.clear(percentage);
userEvent.type(percentage, '10');
expect(await findByText('$175.00')).toBeInTheDocument();

userEvent.clear(percentage);
userEvent.type(percentage, '12');

const johnAmount = getByText('403(b) Amount — John').parentElement;
expect(johnAmount).toHaveTextContent('$175.00');
expect(johnAmount).toHaveAttribute('aria-busy', 'true');
expect(queryByText('$150.00')).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,16 @@ import {
import { useMpdGoalPreview } from './useMpdGoalPreview';
import { NewStaffGoalCalculation } from './useNewStaffGoalCalculation';

type PreviewCalculations = Pick<
NewStaffGoalCalculation['calculations'],
'contributing403bAmount' | 'spouseContributing403bAmount' | 'specialNeedsLeft'
>;

interface GoalSettingsPreviewValue {
calculating: boolean;
failed: boolean;
previewGoal: number | null;
previewCalculations: PreviewCalculations;
warnings: GoalSettingsWarningItem[];
fieldSeverity: (name: string) => WarningSeverity | undefined;
}
Expand Down Expand Up @@ -42,24 +48,33 @@ export const GoalSettingsPreviewProvider: React.FC<
const { t } = useTranslation();
const { values } = useFormikContext<GoalSettingsFormValues>();

const {
id: calculationId,
calculations: {
salaryOverCap: savedSalaryOverCap,
debtOverCap: savedDebtOverCap,
},
} = calculation;
const { id: calculationId, calculations: savedCalculations } = calculation;

const {
calculating,
failed,
previewGoal,
previewLineItems,
previewSalaryOverCap,
previewDebtOverCap,
} = useMpdGoalPreview({ accountListId, calculationId });

const salaryOverCap = previewSalaryOverCap ?? savedSalaryOverCap;
const debtOverCap = previewDebtOverCap ?? savedDebtOverCap;
const previewCalculations = useMemo<PreviewCalculations>(
() => ({
contributing403bAmount:
previewLineItems?.contributing403bAmount ??
savedCalculations.contributing403bAmount,
spouseContributing403bAmount:
previewLineItems?.spouseContributing403bAmount ??
savedCalculations.spouseContributing403bAmount,
specialNeedsLeft:
previewLineItems?.specialNeedsLeft ??
savedCalculations.specialNeedsLeft,
}),
[savedCalculations, previewLineItems],
);
const salaryOverCap = previewSalaryOverCap ?? savedCalculations.salaryOverCap;
const debtOverCap = previewDebtOverCap ?? savedCalculations.debtOverCap;

const value = useMemo<GoalSettingsPreviewValue>(() => {
const warnings = buildGoalSettingsWarnings({
Expand All @@ -73,10 +88,20 @@ export const GoalSettingsPreviewProvider: React.FC<
calculating,
failed,
previewGoal,
previewCalculations,
warnings,
fieldSeverity: (name) => getFieldSeverity(warnings, name),
};
}, [values, salaryOverCap, debtOverCap, t, calculating, failed, previewGoal]);
}, [
values,
salaryOverCap,
debtOverCap,
t,
calculating,
failed,
previewGoal,
previewCalculations,
]);

return (
<GoalSettingsPreviewContext.Provider value={value}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,17 @@ const previewGoalMock = (
previewNewStaffGoalCalculation: {
newStaffGoalCalculation: {
id: calculationId,
calculations: { monthlyGoal, salaryOverCap: false, debtOverCap: false },
calculations: {
monthlyGoal,
contributing403bAmount:
defaultGoalCalculation.calculations.contributing403bAmount,
spouseContributing403bAmount:
defaultGoalCalculation.calculations.spouseContributing403bAmount,
specialNeedsLeft:
defaultGoalCalculation.calculations.specialNeedsLeft,
salaryOverCap: false,
debtOverCap: false,
},
},
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@ mutation PreviewNewStaffGoalCalculation(
newStaffGoalCalculation {
id
calculations {
contributing403bAmount
spouseContributing403bAmount
specialNeedsLeft
monthlyGoal
salaryOverCap
debtOverCap
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { GoalSettingsNumberField } from '../Fields/GoalSettingsNumberField';
import { GoalSettingsSelect, SelectOption } from '../Fields/GoalSettingsSelect';
import { ColumnHeaderRow, FieldRow, Section } from '../GoalSettingsLayout';
import { useGoalSettingsPreview } from '../GoalSettingsPreviewContext';
import { GoalSettingsFormValues } from '../goalSettingsFormValues';
import { GoalSettingsSectionProps } from '../goalSettingsSectionProps';

Expand All @@ -25,77 +26,92 @@
const { t } = useTranslation();
const { formatCurrency } = useFormatters();
const seniorStaffOnly = t('Senior Staff Only');

const preview = useGoalSettingsPreview();
const { contributing403bAmount, spouseContributing403bAmount } =
preview?.previewCalculations ?? calculations;

const calculating = preview?.calculating ?? false;

const {
values: { calculationsYear },
} = useFormikContext<GoalSettingsFormValues>();
// Keep the geographic constants on the same year the goal is calculated with
const { goalGeographicConstantMap } = useGoalCalculatorConstants(
calculationsYear ? Number(calculationsYear) : null,
);

const geographicLocationOptions = useMemo<SelectOption[]>(
() =>
Array.from(goalGeographicConstantMap.keys(), (location) => ({
value: location,
label: location,
})),
[goalGeographicConstantMap],
);

return (
<Section title={t('Financial Information')}>
<ColumnHeaderRow columns={visibleHeaders} />

<FieldRow label={t('Annual Requested Salary')}>
<GoalSettingsNumberField
name="annualRequestedSalary"
label={t('Annual Requested Salary')}
personName={primaryName}
adornment="currency"
/>
{hasSpouse && (
<GoalSettingsNumberField
name="spouseRequestedAnnualSalary"
label={t('Annual Requested Salary')}
personName={spouseName}
adornment="currency"
/>
)}
</FieldRow>

<FieldRow label={t('403(b) Contribution')}>
<GoalSettingsNumberField
name="contribution403bPercentage"
label={t('403(b) Contribution')}
personName={primaryName}
adornment="percentage"
/>
{hasSpouse && (
<GoalSettingsNumberField
name="spouseContribution403bPercentage"
label={t('403(b) Contribution')}
personName={spouseName}
adornment="percentage"
/>
)}
</FieldRow>

<FieldRow
label={t('403(b) Amount')}
helperText={t('Calculated monthly amount')}
>
<Typography variant="body1">
<Typography
variant="body1"
aria-busy={calculating}
sx={{ opacity: calculating ? 0.56 : 1 }}
>
<Box component="span" sx={visuallyHidden as SxProps<Theme>}>
{t('403(b) Amount — {{name}}', { name: primaryName })}
</Box>
{formatCurrency(calculations.contributing403bAmount)}
{formatCurrency(contributing403bAmount)}
Comment thread
zweatshirt marked this conversation as resolved.
</Typography>
{hasSpouse && (
<Typography variant="body1">
<Typography
variant="body1"
aria-busy={calculating}
sx={{ opacity: calculating ? 0.56 : 1 }}
>
<Box component="span" sx={visuallyHidden as SxProps<Theme>}>
{t('403(b) Amount — {{name}}', { name: spouseName })}
</Box>
{formatCurrency(calculations.spouseContributing403bAmount)}
{formatCurrency(spouseContributing403bAmount)}

Check warning on line 114 in src/components/HrTools/NsGoalCalculator/GoalSettings/Sections/FinancialInformationSection.tsx

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ Getting worse: Large Method

FinancialInformationSection:React.FC<GoalSettingsSectionProps> increases from 165 to 177 lines of code, threshold = 120 Large functions with many lines of code are generally harder to understand and lower the code health. Avoid adding more lines to this function.
</Typography>
)}
</FieldRow>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { GoalSettingsNumberField } from '../Fields/GoalSettingsNumberField';
import { GoalSettingsPlaceholder } from '../Fields/GoalSettingsPlaceholder';
import { GoalSettingsSelect, SelectOption } from '../Fields/GoalSettingsSelect';
import { ColumnHeaderRow, FieldRow, Section } from '../GoalSettingsLayout';
import { useGoalSettingsPreview } from '../GoalSettingsPreviewContext';
import { GoalSettingsSectionProps } from '../goalSettingsSectionProps';

export const NsoInformationSection: React.FC<GoalSettingsSectionProps> = ({
Expand All @@ -21,6 +22,11 @@ export const NsoInformationSection: React.FC<GoalSettingsSectionProps> = ({
const { t } = useTranslation();
const { formatCurrency } = useFormatters();

const preview = useGoalSettingsPreview();
const { specialNeedsLeft } = preview?.previewCalculations ?? calculations;

const calculating = preview?.calculating ?? false;

const nsoHousingOptions = useMemo<SelectOption[]>(
() =>
[
Expand Down Expand Up @@ -86,8 +92,12 @@ export const NsoInformationSection: React.FC<GoalSettingsSectionProps> = ({
</FieldRow>

<FieldRow label={t('Left to Raise')}>
<Typography variant="body1">
{formatCurrency(calculations.specialNeedsLeft)}
<Typography
variant="body1"
aria-busy={calculating}
sx={{ opacity: calculating ? 0.56 : 1 }}
>
{formatCurrency(specialNeedsLeft)}
Comment thread
zweatshirt marked this conversation as resolved.
</Typography>
</FieldRow>
</Section>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ const optionalInteger = (label: string, t: TFunction) =>
const optionalPercentage = (label: string, t: TFunction) =>
percentage(label, t).nullable().transform(emptyToNull);

const optional403bPercentage = (label: string, t: TFunction) =>
optionalPercentage(label, t).lessThan(
100,
t('{{fieldName}} must be less than 100%', { fieldName: label }),
);

export const getGoalSettingsSchema = (t: TFunction) =>
yup.object({
// Personal
Expand All @@ -37,8 +43,11 @@ export const getGoalSettingsSchema = (t: TFunction) =>
t('Annual Requested Salary'),
t,
),
contribution403bPercentage: optionalPercentage(t('403(b) Contribution'), t),
spouseContribution403bPercentage: optionalPercentage(
contribution403bPercentage: optional403bPercentage(
t('403(b) Contribution'),
t,
),
spouseContribution403bPercentage: optional403bPercentage(
t('403(b) Contribution'),
t,
),
Expand Down
Loading
Loading