Skip to content

Commit 6b11f5d

Browse files
committed
refactor: convert UnitMassEdit to TypeScript and modernize test infrastructure
- Convert UnitMassEdit component to TypeScript with enhanced type safety - Add proper interfaces for FormValues and AllowedValueWithProperty - Improve function typing and maintain redux-actions compatibility - Preserve all existing functionality with better type checking - Modernize testing library patterns across all test files - Replace fireEvent with userEvent for more realistic user interactions - Add @testing-library/user-event v14.6.1 dependency - Update 6 test files: DeleteConfirmation, LoginScreen, UnitDescriptiveStatusForm, UnitMassEdit, UpdateConfirmation, UpdateQueue - Convert to async/await patterns with userEvent.setup() - Clean up legacy code - Remove unused withRouter HOC from hooks/index.js - Minor formatting improvements in testUtils.tsx
1 parent 22ef847 commit 6b11f5d

11 files changed

Lines changed: 855 additions & 143 deletions

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
"@testing-library/dom": "^10.4.1",
5858
"@testing-library/jest-dom": "^6.9.1",
5959
"@testing-library/react": "^16.3.1",
60+
"@testing-library/user-event": "^14.6.1",
6061
"@types/jest": "^30.0.0",
6162
"@types/lodash": "^4.17.21",
6263
"@types/node": "^25.0.3",
Lines changed: 97 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,92 @@
11
import React, { useState } from 'react';
2-
import { connect } from 'react-redux';
3-
import { Link } from 'react-router-dom';
2+
import { useSelector, useDispatch } from 'react-redux';
3+
import { Link, useParams, useNavigate } from 'react-router-dom';
44
import _ from 'lodash';
55

66
import { ACTION_TYPE, canPropertyBeMaintained, HELP_TEXTS } from './UpdateConfirmation';
77
import { allowedValuesByQuality } from './UnitDetails';
88
import ObservationItem from './ObservationItem';
99
import { unitObservableProperties } from '../lib/municipalServicesClient';
10-
import { withRouter } from '../hooks';
1110
import * as constants from '../constants/index';
1211
import * as actions from '../actions/index';
12+
import { RootState } from '../reducers/types';
13+
import { Unit, ObservableProperty, AllowedValue, UnitObservation } from '../types';
1314

1415
import { QUALITIES } from './utils';
1516

16-
const UnitMassEdit = (props) => {
17-
const { units, groupId, observableProperty, allowedValues, enqueueObservation, navigate } = props;
18-
const [formValues, setFormValues] = useState({
17+
interface FormValues {
18+
units: string[];
19+
quality: string;
20+
observationType: string;
21+
notice: string;
22+
}
23+
24+
// Extended AllowedValue type with property for internal use
25+
type AllowedValueWithProperty = AllowedValue & {
26+
property: string;
27+
};
28+
29+
const UnitMassEdit: React.FC = () => {
30+
const dispatch = useDispatch();
31+
const navigate = useNavigate();
32+
const params = useParams<{ groupId: string; propertyId: string }>();
33+
34+
const unit = useSelector((state: RootState) => state.data.unit);
35+
const service = useSelector((state: RootState) => state.data.service);
36+
const serviceGroup = useSelector((state: RootState) => state.serviceGroup);
37+
38+
const [formValues, setFormValues] = useState<FormValues>({
1939
units: [],
2040
quality: '',
2141
observationType: '',
2242
notice: ''
2343
});
44+
45+
if (!params.groupId || !params.propertyId) {
46+
return <div>Virheellinen parametrit</div>;
47+
}
48+
49+
const { groupId, propertyId } = params;
50+
const onlyQualityProperties = serviceGroup !== constants.SERVICE_GROUPS.swimming.id;
51+
const units = unit ? _.filter(unit, (u: Unit) => u.extensions?.maintenance_group === groupId) : undefined;
52+
53+
let allowedValues: Record<string, Record<string, AllowedValue[]>> = {};
54+
let unitsIncludingSelectedProperty: Unit[] = [];
55+
let observableProperty: ObservableProperty | undefined;
56+
57+
if (units) {
58+
units.forEach((u: Unit) => {
59+
const allObservableProperties = unitObservableProperties(u, service, onlyQualityProperties);
60+
const selectedObservableProperty = _.filter(allObservableProperties, (op: ObservableProperty) => op.id === propertyId);
61+
const selectedObservablePropertyId = selectedObservableProperty[0]?.id;
62+
if (selectedObservablePropertyId === propertyId) {
63+
unitsIncludingSelectedProperty.push(u);
64+
if (selectedObservableProperty[0]) {
65+
observableProperty = selectedObservableProperty[0];
66+
}
67+
}
68+
});
69+
}
70+
71+
if (observableProperty) {
72+
allowedValues = { [observableProperty.id]: allowedValuesByQuality(observableProperty) };
73+
}
74+
2475
const unitsHeader = 'Valitse päivitettävät paikat';
25-
const qualityHeader = observableProperty.name ? observableProperty.name.fi : 'Kuntotilanne';
76+
const qualityHeader = observableProperty?.name ? observableProperty.name.fi : 'Kuntotilanne';
2677
const observationHeader = 'Vahvista valinta';
2778
const noticeHeader = 'Päivitä kuntokuvaus';
2879
const hasUnitsSelected = !!formValues.units.length;
2980
const hasQualitySelected = !!formValues.quality;
3081
const hasObservationTypeSelected = !!formValues.observationType;
3182
const submitEnabled = hasUnitsSelected && hasQualitySelected && hasObservationTypeSelected;
3283

33-
function handleChange(event) {
34-
const { value, name, checked, type } = event.target;
84+
function handleChange(event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) {
85+
const target = event.target as HTMLInputElement;
86+
const { value, name, checked, type } = target;
3587

3688
if (type === 'checkbox') {
37-
let newChoices = [...formValues[name]];
89+
let newChoices = [...formValues[name as keyof FormValues] as string[]];
3890
newChoices = newChoices.filter(e => e !== value);
3991
if (checked) {
4092
newChoices.push(value);
@@ -60,26 +112,29 @@ const UnitMassEdit = (props) => {
60112
}
61113
}
62114

63-
function allowedValue() {
115+
function allowedValue(): AllowedValueWithProperty | undefined {
64116
if (!observableProperty || !formValues?.quality) return undefined;
65117

66-
return _.find(observableProperty.allowed_values, (value) => {
67-
value.property = observableProperty.id;
68-
return (value.identifier == formValues.quality);
69-
});
118+
return _.find(observableProperty.allowed_values, (value: AllowedValue) => {
119+
const valueWithProperty = value as AllowedValueWithProperty;
120+
valueWithProperty.property = observableProperty.id;
121+
return value.identifier === formValues.quality;
122+
}) as AllowedValueWithProperty | undefined;
70123
}
71124

72125
function onSubmit() {
73-
const propertyId = observableProperty?.id;
126+
if (!observableProperty) return;
127+
128+
const propertyId = observableProperty.id;
74129
const unitIds = formValues.units;
75130
const value = allowedValue();
76131
const isServiced = formValues.observationType === 'serviced';
77132
const notice = formValues.notice;
78133

79-
unitIds.forEach((unitId) => {
80-
enqueueObservation(propertyId, value, Number.parseInt(unitId), isServiced);
134+
unitIds.forEach((unitId: string) => {
135+
dispatch((actions.enqueueObservation as any)(propertyId, value?.identifier || '', Number.parseInt(unitId, 10), isServiced));
81136
if (notice) {
82-
enqueueObservation('notice', notice, Number.parseInt(unitId));
137+
dispatch((actions.enqueueObservation as any)('notice', notice, Number.parseInt(unitId, 10)));
83138
}
84139
});
85140

@@ -90,10 +145,10 @@ const UnitMassEdit = (props) => {
90145
return <div>Ladataan...</div>;
91146
}
92147

93-
const renderUnitInputs = _.map(_.sortBy(units, [(u) => u.name.fi]), (unit) => {
148+
const renderUnitInputs = _.map(_.sortBy(unitsIncludingSelectedProperty, [(u: Unit) => u.name.fi]), (unit: Unit) => {
94149
const observations = _.map(
95-
unit.observations,
96-
(obs) => { return <ObservationItem key={obs.id} observation={obs} />; }
150+
unit.observations || [],
151+
(obs: UnitObservation) => { return <ObservationItem key={obs.id} observation={obs} />; }
97152
);
98153
return (
99154
<div key={unit.id} className="mass-edit-checkbox">
@@ -113,40 +168,43 @@ const UnitMassEdit = (props) => {
113168
);
114169
});
115170

116-
const renderQualityInputs = () => {
117-
let values = [];
171+
const renderQualityInputs = (): React.ReactElement[] => {
172+
if (!observableProperty) return [];
173+
174+
let values: React.ReactElement[] = [];
118175

119-
_.each(QUALITIES, (quality) => {
120-
values = values.concat(_.map(allowedValues[observableProperty.id][quality], (v) => {
176+
_.each(QUALITIES, (quality: string) => {
177+
const qualityValues = allowedValues[observableProperty.id]?.[quality] || [];
178+
values = values.concat(_.map(qualityValues, (v: AllowedValue) => {
121179
return (
122180
<div key={v.identifier} className="mass-edit-radio">
123181
<input
124182
type="radio"
125183
value={v.identifier}
126184
name="quality"
127-
id={`id-${v.property}-${v.identifier}`}
185+
id={`id-${observableProperty.id}-${v.identifier}`}
128186
checked={formValues.quality === v.identifier}
129187
onChange={handleChange}
130188
disabled={!hasUnitsSelected}
131189
/>
132-
<label htmlFor={`id-${v.property}-${v.identifier}`}>{v.name.fi}</label>
190+
<label htmlFor={`id-${observableProperty.id}-${v.identifier}`}>{v.name.fi}</label>
133191
</div>
134-
)
192+
);
135193
}));
136194
});
137195

138196
return values;
139-
}
197+
};
140198

141-
const renderConfirmation = () => {
199+
const renderConfirmation = (): React.ReactElement | undefined => {
142200
const value = allowedValue();
143201

144202
if (!value) return;
145203

146-
const render = () => {
204+
const render = (): React.ReactElement => {
147205
const quality = value.quality;
148206

149-
const observedOption = (showHelpText=true) => (
207+
const observedOption = (showHelpText: boolean = true): React.ReactElement => (
150208
<div className="mass-edit-radio">
151209
<input
152210
type="radio"
@@ -179,7 +237,7 @@ const UnitMassEdit = (props) => {
179237
);
180238

181239
if (canPropertyBeMaintained(value.property) && (
182-
quality === 'good' || quality == 'satisfactory' || value.identifier == 'event')
240+
quality === 'good' || quality === 'satisfactory' || value.identifier === 'event')
183241
) {
184242
return (
185243
<>
@@ -202,7 +260,7 @@ const UnitMassEdit = (props) => {
202260
);
203261
};
204262

205-
const renderNoticeField = () => (
263+
const renderNoticeField = (): React.ReactElement => (
206264
<div className="panel panel-default">
207265
<div className="panel-heading">{noticeHeader}</div>
208266
<div className="panel-body">
@@ -212,7 +270,7 @@ const UnitMassEdit = (props) => {
212270
id="notice"
213271
name="notice"
214272
className="form-control"
215-
rows="3"
273+
rows={3}
216274
value={formValues.notice}
217275
onChange={handleChange}
218276
disabled={!hasUnitsSelected}
@@ -244,8 +302,7 @@ const UnitMassEdit = (props) => {
244302
<div className="panel panel-default">
245303
<div className="panel-heading">{qualityHeader}</div>
246304
<div className="panel-body">
247-
{!hasUnitsSelected && <p className="text-muted"><em>Valitse ensin päivitettävät paikat</em></p>}
248-
{hasUnitsSelected && renderQualityInputs()}
305+
{renderQualityInputs()}
249306
</div>
250307
</div>
251308
{(hasUnitsSelected && hasQualitySelected) && renderConfirmation()}
@@ -266,46 +323,6 @@ const UnitMassEdit = (props) => {
266323
</div>
267324
</div>
268325
);
269-
}
270-
271-
function mapStateToProps(state, ownProps) {
272-
const { groupId, propertyId } = ownProps.params;
273-
const { unit, service } = state.data;
274-
const onlyQualityProperties = state.serviceGroup !== constants.SERVICE_GROUPS.swimming.id;
275-
const units = _.filter(unit, (u) => u.extensions.maintenance_group);
276-
277-
let allowedValues;
278-
let unitsIncludingSelectedProperty = [];
279-
let observableProperty;
280-
281-
units.forEach((u) => {
282-
const allObservableProperties = unitObservableProperties(u, service, onlyQualityProperties);
283-
const selectedObservableProperty = _.filter(allObservableProperties, (op) => op.id === propertyId);
284-
const selectedObservablePropertyId = selectedObservableProperty[0]?.id;
285-
if (selectedObservablePropertyId === propertyId) {
286-
unitsIncludingSelectedProperty.push(u);
287-
observableProperty = selectedObservableProperty;
288-
}
289-
});
290-
291-
if (observableProperty) {
292-
allowedValues = _.fromPairs(_.map(observableProperty, (p) => [p.id, allowedValuesByQuality(p)]))
293-
}
294-
295-
return {
296-
allowedValues,
297-
groupId,
298-
observableProperty: Object.assign({}, ...observableProperty),
299-
units: unitsIncludingSelectedProperty
300-
};
301-
}
302-
303-
function mapDispatchToProps(dispatch) {
304-
return {
305-
enqueueObservation: (property, allowedValue, unitId, addServicedObservation) => {
306-
dispatch(actions.enqueueObservation(property, allowedValue, unitId, addServicedObservation));
307-
}
308-
};
309-
}
326+
};
310327

311-
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(UnitMassEdit));
328+
export default UnitMassEdit;

src/components/__tests__/DeleteConfirmation.test.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from 'react';
2-
import { screen, fireEvent } from '@testing-library/react';
2+
import { screen } from '@testing-library/react';
3+
import userEvent from '@testing-library/user-event';
34
import DeleteConfirmation from '../DeleteConfirmation';
45
import { renderWithRoute } from '../../../test/testUtils';
56
import * as actions from '../../actions';
@@ -202,20 +203,22 @@ describe('DeleteConfirmation', () => {
202203
});
203204

204205
describe('button clicks and action dispatching', () => {
205-
it('dispatches clearObservation when delete button is clicked', () => {
206+
it('dispatches clearObservation when delete button is clicked', async () => {
207+
const user = userEvent.setup();
206208
renderComponent();
207209

208210
const deleteButton = screen.getByRole('link', { name: /poista/i });
209-
fireEvent.click(deleteButton);
211+
await user.click(deleteButton);
210212

211213
expect(mockEnqueueObservation).toHaveBeenCalledWith('notice', null, 123);
212214
});
213215

214-
it('does not dispatch action when cancel button is clicked', () => {
216+
it('does not dispatch action when cancel button is clicked', async () => {
217+
const user = userEvent.setup();
215218
renderComponent();
216219

217220
const cancelButton = screen.getByRole('link', { name: /peruuta/i });
218-
fireEvent.click(cancelButton);
221+
await user.click(cancelButton);
219222

220223
expect(mockEnqueueObservation).not.toHaveBeenCalled();
221224
});

0 commit comments

Comments
 (0)