Skip to content
Open
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
@@ -1,4 +1,4 @@
import React from 'react';

Check warning on line 1 in src/components/HrTools/GoalCalculator/CalculatorSettings/Categories/InformationCategory/InformationCategory.test.tsx

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ New issue: Lines of Code in a Single File

This module has 304 lines of code, improve code health by reducing it to 300 The number of Lines of Code in a single file. More Lines of Code lowers the code health.
import { render, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { GqlMockedProvider } from '__tests__/util/graphqlMocking';
Expand All @@ -23,12 +23,14 @@
single?: boolean;
readOnly?: boolean;
benefitsPlan?: MpdGoalBenefitsConstantPlanEnum;
geographicLocation?: string | null;
}

const TestComponent: React.FC<TestComponentProps> = ({
single = false,
readOnly = false,
benefitsPlan = MpdGoalBenefitsConstantPlanEnum.Base,
geographicLocation = null,
}) => (
<GqlMockedProvider<{
GoalCalculation: GoalCalculationQuery;
Expand All @@ -44,6 +46,7 @@
? MpdGoalBenefitsConstantSizeEnum.Single
: MpdGoalBenefitsConstantSizeEnum.MarriedNoChildren,
benefitsPlan,
geographicLocation,
},
},
GoalCalculatorConstants: {
Expand Down Expand Up @@ -324,6 +327,59 @@
);
});

it('does not save while the Geographic Location is cleared by typing', async () => {
Comment thread
wjames111 marked this conversation as resolved.
mutationSpy.mockClear();
const { getByRole } = render(
<TestComponent geographicLocation="Orlando, FL" />,
);

const input = getByRole('combobox', { name: 'Geographic Location' });
await waitFor(() => expect(input).toHaveValue('Orlando, FL'));

userEvent.clear(input);

// Yield to the microtask queue so any pending mutation would have fired.
await new Promise((resolve) => setTimeout(resolve, 0));
expect(mutationSpy).not.toHaveGraphqlOperation('UpdateGoalCalculation');
// The field stays empty instead of resetting mid-edit
expect(input).toHaveValue('');
});

it('reverts to the saved Geographic Location when the cleared field loses focus', async () => {
mutationSpy.mockClear();
const { getByRole } = render(
<TestComponent geographicLocation="Orlando, FL" />,
);

const input = getByRole('combobox', { name: 'Geographic Location' });
await waitFor(() => expect(input).toHaveValue('Orlando, FL'));

userEvent.clear(input);
userEvent.tab();

// The field is not clearable, so blurring restores the saved value
// without firing a mutation
await waitFor(() => expect(input).toHaveValue('Orlando, FL'));
expect(mutationSpy).not.toHaveGraphqlOperation('UpdateGoalCalculation');
});

it('defaults to None and does not save when cleared and blurred without a saved location', async () => {
mutationSpy.mockClear();
const { getByRole } = render(<TestComponent />);

const input = getByRole('combobox', { name: 'Geographic Location' });
await waitFor(() => expect(input).not.toBeDisabled());
// An unset location displays as None
await waitFor(() => expect(input).toHaveValue('None'));

userEvent.clear(input);
userEvent.tab();

// Yield to the microtask queue so any pending mutation would have fired.
await new Promise((resolve) => setTimeout(resolve, 0));
expect(mutationSpy).not.toHaveGraphqlOperation('UpdateGoalCalculation');
});

it('shows errors and does not save when input is invalid', async () => {
const { getByRole } = render(<TestComponent />);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,10 @@
<Grid size={12}>
<Autocomplete
options={locations}
value={geographicLocation ?? null}
// The None option takes the place of clearing the field, so
// emptying the input never fires a mid-typing null save
disableClearable
value={geographicLocation ?? 'None'}

Check warning on line 156 in src/components/HrTools/GoalCalculator/CalculatorSettings/Categories/InformationCategory/InformationCategoryForm/InformationCategoryPersonalForm.tsx

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ Getting worse: Complex Method

InformationCategoryPersonalForm:React.FC<InformationCategoryPersonalFormProps> already has high cyclomatic complexity, and now it increases in Lines of Code from 246 to 247 This function has many conditional statements (e.g. if, for, while), leading to lower code health. Avoid adding more conditionals and code to it without refactoring.
onChange={(_, newValue) =>
saveField({ geographicLocation: newValue })
}
Expand Down
97 changes: 97 additions & 0 deletions src/components/HrTools/PdsGoalCalculator/Setup/SetupStep.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';

Check warning on line 1 in src/components/HrTools/PdsGoalCalculator/Setup/SetupStep.test.tsx

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ Getting worse: Lines of Code in a Single File

The lines of code increases from 442 to 518, improve code health by reducing it to 500 The number of Lines of Code in a single file. More Lines of Code lowers the code health.
import { render, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import {
Expand Down Expand Up @@ -344,6 +344,103 @@
);
});

it('does not save while the Geographic Multiplier is cleared by typing', async () => {
mutationSpy.mockClear();
const { findByRole } = renderSetup({
calculationMock: {
...fullTimeSalariedMock,
geographicLocation: 'Orlando, FL',
},
onCall: mutationSpy,
});

const input = await findByRole('combobox', {
name: 'Geographic Multiplier',
});
await waitFor(() => expect(input).toHaveValue('Orlando, FL (6%)'));

userEvent.clear(input);

// Yield to the microtask queue so any pending mutation would have fired.
await new Promise((r) => setTimeout(r, 0));
expect(mutationSpy).not.toHaveGraphqlOperation('UpdatePdsGoalCalculation');
// The field stays empty instead of resetting to None mid-edit
expect(input).toHaveValue('');
});

it('reverts to the saved Geographic Multiplier when the cleared field loses focus', async () => {
mutationSpy.mockClear();
const { findByRole } = renderSetup({
calculationMock: {
...fullTimeSalariedMock,
geographicLocation: 'Orlando, FL',
},
onCall: mutationSpy,
});

const input = await findByRole('combobox', {
name: 'Geographic Multiplier',
});
await waitFor(() => expect(input).toHaveValue('Orlando, FL (6%)'));

userEvent.clear(input);
userEvent.tab();

// The field is not clearable, so blurring restores the saved value
// without firing a mutation
await waitFor(() => expect(input).toHaveValue('Orlando, FL (6%)'));
expect(mutationSpy).not.toHaveGraphqlOperation('UpdatePdsGoalCalculation');
});

it('saves None when it is explicitly selected', async () => {
mutationSpy.mockClear();
const { findByRole } = renderSetup({
calculationMock: {
...fullTimeSalariedMock,
geographicLocation: 'Orlando, FL',
},
onCall: mutationSpy,
});

const input = await findByRole('combobox', {
name: 'Geographic Multiplier',
});
await waitFor(() => expect(input).toHaveValue('Orlando, FL (6%)'));

userEvent.click(input);
userEvent.click(await findByRole('option', { name: 'None' }));

await waitFor(() =>
expect(mutationSpy).toHaveGraphqlOperation('UpdatePdsGoalCalculation', {
attributes: {
id: 'goal-1',
geographicLocation: 'None',
},
}),
);
});

it('does not save when a Geographic Multiplier of None is cleared and loses focus', async () => {
mutationSpy.mockClear();
const { findByRole } = renderSetup({
calculationMock: fullTimeSalariedMock,
onCall: mutationSpy,
});

const input = await findByRole('combobox', {
name: 'Geographic Multiplier',
});
await waitFor(() => expect(input).not.toBeDisabled());
await waitFor(() => expect(input).toHaveValue('None'));

userEvent.clear(input);
userEvent.tab();

// Yield to the microtask queue so any pending mutation would have fired.
await new Promise((r) => setTimeout(r, 0));
expect(mutationSpy).not.toHaveGraphqlOperation('UpdatePdsGoalCalculation');
});

it('renders the Calculate my average hours button next to Hours Worked', async () => {
const { findByRole } = renderSetup({
calculationMock: fullTimeHourlyMock,
Expand Down
5 changes: 4 additions & 1 deletion src/components/HrTools/PdsGoalCalculator/Setup/SetupStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -281,9 +281,12 @@
<Grid size={12}>
<Autocomplete
options={locations}
// The None option takes the place of clearing the field, so
// emptying the input never fires a mid-typing null save
disableClearable
Comment thread
wjames111 marked this conversation as resolved.
getOptionLabel={getLocationLabel}
value={calculation?.geographicLocation ?? 'None'}
onChange={(_, newValue: string | null) =>
onChange={(_, newValue) =>

Check warning on line 289 in src/components/HrTools/PdsGoalCalculator/Setup/SetupStep.tsx

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ Getting worse: Large Method

SetupStep:React.FC increases from 253 to 254 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.
saveField({ geographicLocation: newValue })
}
disabled={!calculation}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,40 @@ import { useSaveField } from './useSaveField';

export interface AutosaveAutocompleteProps
extends Omit<
AutocompleteProps<string, false, false, false>,
AutocompleteProps<string, false, boolean, false>,
'renderInput' | 'onChange' | 'value'
> {
fieldName: string;
label: string;
textFieldProps?: Partial<TextFieldProps>;
/**
* Option displayed when the field has no saved value, e.g. 'None'. Must be
* one of the options. When set, the field is not clearable — selecting the
* empty-value option takes the place of clearing.
*/
emptyValue?: string;
}

export const AutosaveAutocomplete: React.FC<AutosaveAutocompleteProps> = ({
fieldName,
label,
options,
textFieldProps,
emptyValue,
...props
}) => {
const saveField = useSaveField();
const { calculation } = useSalaryCalculator();

const value = calculation?.[fieldName] ?? null;
const value = calculation?.[fieldName] ?? emptyValue ?? null;

return (
<Autocomplete
options={options}
// With an emptyValue option there is nothing to clear to, and disabling
// clearing also stops MUI from firing a mid-typing null save when the
// input is emptied
disableClearable={emptyValue !== undefined}
Comment thread
wjames111 marked this conversation as resolved.
value={value}
onChange={(_, newValue) => saveField({ [fieldName]: newValue })}
disabled={!calculation}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ const TestComponent: React.FC<{
hasSpouse?: boolean;
requestMock?: SalaryRequestMock;
payrollDates?: SalaryCalculatorTestWrapperProps['payrollDates'];
}> = ({ hasSpouse, requestMock, payrollDates }) => (
onCall?: SalaryCalculatorTestWrapperProps['onCall'];
}> = ({ hasSpouse, requestMock, payrollDates, onCall }) => (
<SalaryCalculatorTestWrapper
hasSpouse={hasSpouse}
salaryRequestMock={requestMock}
payrollDates={payrollDates}
onCall={onCall}
>
<PersonalInformationSection />
</SalaryCalculatorTestWrapper>
Expand Down Expand Up @@ -68,10 +70,10 @@ describe('PersonalInformationSection', () => {
});

it('should display married personal information values correctly', async () => {
// Explicitly clear the saved location so the combobox is empty. (MUI v7's
// Autocomplete renders a controlled `value` even when it isn't one of the
// `options`, so relying on the auto-generated mock string would populate
// the field.)
// Explicitly clear the saved location so the combobox falls back to None.
// (MUI v7's Autocomplete renders a controlled `value` even when it isn't
// one of the `options`, so relying on the auto-generated mock string would
// populate the field.)
const { findByRole } = render(
<TestComponent requestMock={{ location: null }} />,
);
Expand All @@ -80,7 +82,7 @@ describe('PersonalInformationSection', () => {
name: 'Nearest Geographic Multiplier Location',
});
await waitFor(() => {
expect(locationCombobox).toHaveValue('');
expect(locationCombobox).toHaveValue('None');
});

expect(await findByRole('cell', { name: '4 years' })).toBeInTheDocument();
Expand Down Expand Up @@ -112,7 +114,39 @@ describe('PersonalInformationSection', () => {

userEvent.click(await findByRole('button', { name: 'Open' }));

expect(await findAllByRole('option')).toHaveLength(2);
// The two mocked locations plus the guaranteed None option
expect(await findAllByRole('option')).toHaveLength(3);
});

it('does not save while the location is cleared by typing and reverts on blur', async () => {
Comment thread
wjames111 marked this conversation as resolved.
const mutationSpy = jest.fn();
const { findByRole } = render(
<TestComponent
requestMock={{ location: 'Miami, FL' }}
onCall={mutationSpy}
/>,
);

const locationCombobox = await findByRole('combobox', {
name: 'Nearest Geographic Multiplier Location',
});
await waitFor(() => {
expect(locationCombobox).toHaveValue('Miami, FL');
});

userEvent.clear(locationCombobox);

// Yield to the microtask queue so any pending mutation would have fired.
await new Promise((resolve) => setTimeout(resolve, 0));
expect(mutationSpy).not.toHaveGraphqlOperation('UpdateSalaryCalculation');
expect(locationCombobox).toHaveValue('');

userEvent.tab();

// The field is not clearable, so blurring restores the saved value
// without firing a mutation
await waitFor(() => expect(locationCombobox).toHaveValue('Miami, FL'));
expect(mutationSpy).not.toHaveGraphqlOperation('UpdateSalaryCalculation');
});

it('should render the effective paycheck note when payroll dates match', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export const PersonalInformationSection: React.FC = () => {
label={t('Nearest Geographic Multiplier Location')}
fieldName="location"
options={locations}
emptyValue="None"
textFieldProps={{
InputLabelProps: {
sx: { fontSize: theme.typography.body2.fontSize },
Expand Down
Loading
Loading