Skip to content

Commit 9669339

Browse files
committed
refactor: convert UnitHistory to TypeScript with modern hooks
- Convert UnitHistory.js to UnitHistory.tsx with full TypeScript support - Replace withRouter HOC with useParams hook from react-router-dom - Replace connect HOC with useSelector and useDispatch hooks from react-redux - Convert class component to functional component with hooks - Add comprehensive unit tests - Add renderWithRoute helper function to testUtils for route-based testing
1 parent 2ff7c9f commit 9669339

4 files changed

Lines changed: 343 additions & 77 deletions

File tree

src/components/UnitHistory.js

Lines changed: 0 additions & 75 deletions
This file was deleted.

src/components/UnitHistory.tsx

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import React, { useEffect, useMemo } from 'react';
2+
import { Link, useParams } from 'react-router-dom';
3+
import { useSelector, useDispatch } from 'react-redux';
4+
5+
import * as actions from '../actions/index';
6+
import ObservationItem from './ObservationItem';
7+
import { Unit, UnitObservation } from '../types';
8+
9+
interface Params {
10+
unitId: string;
11+
}
12+
13+
interface DataState {
14+
unit: { [key: number]: Unit };
15+
observation: { [key: number]: UnitObservation };
16+
}
17+
18+
interface RootState {
19+
data: DataState;
20+
}
21+
22+
const UnitHistory: React.FC = () => {
23+
const params = useParams<Params>();
24+
const dispatch = useDispatch();
25+
26+
const unitId = useMemo(() => Number(params.unitId), [params.unitId]);
27+
28+
const unit = useSelector((state: RootState) => state.data.unit[unitId]);
29+
const observationData = useSelector((state: RootState) => state.data.observation);
30+
31+
const observations = useMemo(() => {
32+
if (!observationData) return [];
33+
return Object.values(observationData).filter(obs => obs.unit === unitId);
34+
}, [observationData, unitId]);
35+
36+
useEffect(() => {
37+
if (params.unitId) {
38+
dispatch(actions.fetchUnitObservations(params.unitId));
39+
}
40+
}, [dispatch, params.unitId]);
41+
42+
const renderObservations = (observations: UnitObservation[]) => {
43+
return observations.map(obs => (
44+
<div key={obs.id} className="list-group-item">
45+
<ObservationItem observation={obs} />
46+
</div>
47+
));
48+
};
49+
50+
if (unit === undefined) {
51+
return <div>Ladataan...</div>;
52+
}
53+
54+
return (
55+
<div className="row">
56+
<div className="col-xs-12">
57+
<div className="list-group facility-return clearfix">
58+
<Link to={`/unit/${unit.id}`} className="list-group-item">
59+
<span className="action-icon glyphicon glyphicon-chevron-left"></span>
60+
{' '}Takaisin
61+
</Link>
62+
</div>
63+
<div className="well">
64+
<h4>{ unit.name.fi }</h4>
65+
</div>
66+
<div className="unit-observations">
67+
<h4>Historia</h4>
68+
{observations && observations.length > 0 ? (
69+
<div className="list-group">
70+
{renderObservations(observations)}
71+
</div>
72+
) : (
73+
<p>Ei historiatietoja</p>
74+
)}
75+
</div>
76+
</div>
77+
</div>);
78+
};
79+
80+
export default UnitHistory;
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import React from 'react';
2+
import { screen } from '@testing-library/react';
3+
4+
import UnitHistory from '../../src/components/UnitHistory';
5+
import { renderWithRoute } from '../testUtils';
6+
import * as actions from '../../src/actions/index';
7+
8+
// Mock the actions
9+
jest.mock('../../src/actions/index', () => ({
10+
fetchUnitObservations: jest.fn()
11+
}));
12+
13+
const mockFetchUnitObservations = actions.fetchUnitObservations as jest.MockedFunction<typeof actions.fetchUnitObservations>;
14+
15+
const mockUnit = {
16+
id: 123,
17+
name: { fi: 'Test Unit' },
18+
services: [1, 2]
19+
};
20+
21+
const mockObservations = [
22+
{
23+
id: 1,
24+
unit: 123,
25+
property: 'test_property_1',
26+
time: '2024-01-01T12:00:00Z',
27+
expiration_time: null,
28+
name: { fi: 'Test Property 1' },
29+
quality: 'good',
30+
value: 'test_value_1',
31+
primary: true
32+
},
33+
{
34+
id: 2,
35+
unit: 123,
36+
property: 'test_property_2',
37+
time: '2024-01-02T12:00:00Z',
38+
expiration_time: null,
39+
name: { fi: 'Test Property 2' },
40+
quality: 'good',
41+
value: 'test_value_2',
42+
primary: false
43+
}
44+
];
45+
46+
const defaultState = {
47+
data: {
48+
unit: { 123: mockUnit },
49+
observation: {
50+
1: mockObservations[0],
51+
2: mockObservations[1]
52+
},
53+
unitsByDistance: [],
54+
observable_property: {},
55+
service: {},
56+
loading: {}
57+
},
58+
auth: {
59+
token: null,
60+
maintenance_organization: 'test-org',
61+
login_id: null
62+
},
63+
authError: null,
64+
updateQueue: {},
65+
updateFlush: false,
66+
serviceGroup: 'skiing',
67+
userLocation: null,
68+
unitsByUpdateTime: [],
69+
unitsByUpdateCount: {},
70+
selectedUnits: [],
71+
observationsByPropertyByUnit: {},
72+
allowedValuesByProperty: {},
73+
massEdit: {
74+
selectedUnits: [],
75+
isEditing: false,
76+
property: null,
77+
value: null
78+
}
79+
};
80+
81+
const renderComponent = (unitId = '123', initialState = defaultState) => {
82+
return renderWithRoute(<UnitHistory />, {
83+
route: `/unit/${unitId}/history`,
84+
path: '/unit/:unitId/history',
85+
initialState
86+
});
87+
};
88+
89+
describe('UnitHistory', () => {
90+
beforeEach(() => {
91+
mockFetchUnitObservations.mockClear();
92+
mockFetchUnitObservations.mockReturnValue({ type: 'FETCH_UNIT_OBSERVATIONS' });
93+
});
94+
95+
it('renders loading state when unit is undefined', () => {
96+
const stateWithoutUnit = {
97+
...defaultState,
98+
data: {
99+
...defaultState.data,
100+
unit: {}
101+
}
102+
};
103+
104+
renderComponent('123', stateWithoutUnit);
105+
expect(screen.getByText('Ladataan...')).toBeInTheDocument();
106+
});
107+
108+
it('renders unit name and back link when unit is loaded', () => {
109+
renderComponent();
110+
111+
expect(screen.getByText('Test Unit')).toBeInTheDocument();
112+
expect(screen.getByText('Takaisin')).toBeInTheDocument();
113+
114+
const backLink = screen.getByRole('link', { name: /takaisin/i });
115+
expect(backLink).toHaveAttribute('href', '/unit/123');
116+
});
117+
118+
it('dispatches fetchUnitObservations on component mount', () => {
119+
renderComponent();
120+
121+
expect(mockFetchUnitObservations).toHaveBeenCalledWith('123');
122+
expect(mockFetchUnitObservations).toHaveBeenCalledTimes(1);
123+
});
124+
125+
it('renders observations when they exist', () => {
126+
renderComponent();
127+
128+
expect(screen.getByText('Historia')).toBeInTheDocument();
129+
// Check that observation components are rendered (they contain the property names)
130+
expect(screen.getByText('Test Property 1')).toBeInTheDocument();
131+
expect(screen.getByText('Test Property 2')).toBeInTheDocument();
132+
});
133+
134+
it('renders "no history" message when observations are empty', () => {
135+
const stateWithoutObservations = {
136+
...defaultState,
137+
data: {
138+
...defaultState.data,
139+
observation: {}
140+
}
141+
};
142+
143+
renderComponent('123', stateWithoutObservations);
144+
145+
expect(screen.getByText('Historia')).toBeInTheDocument();
146+
expect(screen.getByText('Ei historiatietoja')).toBeInTheDocument();
147+
expect(screen.queryByText('Test Property')).not.toBeInTheDocument();
148+
});
149+
150+
it('filters observations by unit id', () => {
151+
const stateWithMultipleObservations = {
152+
...defaultState,
153+
data: {
154+
...defaultState.data,
155+
observation: {
156+
1: { ...mockObservations[0], unit: 123 },
157+
2: { ...mockObservations[1], unit: 123 },
158+
3: {
159+
id: 3,
160+
unit: 456,
161+
property: 'other_property',
162+
time: '2024-01-03T12:00:00Z',
163+
expiration_time: null,
164+
name: { fi: 'Other Property' },
165+
quality: 'good',
166+
value: 'other_value',
167+
primary: false
168+
}
169+
}
170+
}
171+
};
172+
173+
renderComponent('123', stateWithMultipleObservations);
174+
175+
expect(screen.getByText('Test Property 1')).toBeInTheDocument();
176+
expect(screen.getByText('Test Property 2')).toBeInTheDocument();
177+
expect(screen.queryByText('Other Property')).not.toBeInTheDocument();
178+
});
179+
180+
it('handles different unit ids correctly', () => {
181+
const stateWithDifferentUnit = {
182+
...defaultState,
183+
data: {
184+
...defaultState.data,
185+
unit: { 456: { ...mockUnit, id: 456, name: { fi: 'Different Unit' } } },
186+
observation: {
187+
3: { ...mockObservations[0], id: 3, unit: 456 }
188+
}
189+
}
190+
};
191+
192+
renderComponent('456', stateWithDifferentUnit);
193+
194+
expect(screen.getByText('Different Unit')).toBeInTheDocument();
195+
expect(mockFetchUnitObservations).toHaveBeenCalledWith('456');
196+
197+
const backLink = screen.getByRole('link', { name: /takaisin/i });
198+
expect(backLink).toHaveAttribute('href', '/unit/456');
199+
});
200+
201+
it('handles missing observations gracefully', () => {
202+
const stateWithNullObservations = {
203+
...defaultState,
204+
data: {
205+
...defaultState.data,
206+
observation: null as any
207+
}
208+
};
209+
210+
renderComponent('123', stateWithNullObservations);
211+
212+
expect(screen.getByText('Test Unit')).toBeInTheDocument();
213+
expect(screen.getByText('Ei historiatietoja')).toBeInTheDocument();
214+
});
215+
216+
it('renders correct DOM structure', () => {
217+
renderComponent();
218+
219+
// Check main container structure
220+
expect(document.querySelector('.row')).toBeInTheDocument();
221+
expect(document.querySelector('.col-xs-12')).toBeInTheDocument();
222+
expect(document.querySelector('.list-group.facility-return')).toBeInTheDocument();
223+
expect(document.querySelector('.well')).toBeInTheDocument();
224+
expect(document.querySelector('.unit-observations')).toBeInTheDocument();
225+
});
226+
227+
it('renders glyphicon for back link', () => {
228+
renderComponent();
229+
230+
const glyphicon = document.querySelector('.glyphicon-chevron-left');
231+
expect(glyphicon).toBeInTheDocument();
232+
});
233+
});

0 commit comments

Comments
 (0)