Skip to content

Commit 6991171

Browse files
authored
Validate empty div creation and save (#254)
* Fix nested dispatch calls, display feedback empty div creation * Fix broken tests and add new tests for custom hooks
1 parent 08ec221 commit 6991171

29 files changed

+912
-333
lines changed

__mocks__/peaks.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export const Peaks = jest.fn((opts) => {
6262
return peaks;
6363
});
6464

65-
// segements are built in match with timespans from 'testSmData'
65+
// segments are built in match with timespans from 'testSmData'
6666
// in ./testing-helpers.js file
6767
const peaksSegments = (opts, peaks) => {
6868
let segments = [
@@ -104,6 +104,12 @@ const peaksSegments = (opts, peaks) => {
104104

105105
export const Segment = jest.fn((opts) => {
106106
let segment = { ...opts };
107+
// Ensure both id and _id are set
108+
if (opts._id && !opts.id) {
109+
segment.id = opts._id;
110+
} else if (opts.id && !opts._id) {
111+
segment._id = opts.id;
112+
}
107113
let checkProp = (newOpts, prop) => {
108114
if (newOpts.hasOwnProperty(prop)) {
109115
return true;

src/App.test.js

Lines changed: 71 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
manifestWoStructure,
1111
manifestWithInvalidStruct,
1212
manifestWEmptyCanvas,
13+
manifestWEmptyRanges,
1314
} from './services/testing-helpers';
1415
import mockAxios from 'axios';
1516
import Peaks from 'peaks.js';
@@ -289,8 +290,9 @@ describe('App component', () => {
289290
await act(() => Promise.resolve());
290291

291292
expect(app.queryByTestId('waveform-container')).toBeInTheDocument();
292-
expect(app.queryByTestId('alert-container')).toBeInTheDocument();
293-
expect(app.getByTestId('alert-message').innerHTML).toBe(
293+
// Display 2 alerts for empty media and invalid structure
294+
expect(app.queryAllByTestId('alert-container').length).toEqual(2);
295+
expect(app.getAllByTestId('alert-message')[1].innerHTML).toBe(
294296
'No available media. Editing structure is disabled.'
295297
);
296298
});
@@ -432,39 +434,79 @@ describe('App component', () => {
432434
});
433435
});
434436

435-
test('when structure has invalid timespans', async () => {
436-
mockAxios.get.mockImplementationOnce(() => {
437-
return Promise.resolve({
438-
status: 200,
439-
data: manifestWithInvalidStruct
437+
describe('when structure has', () => {
438+
test('invalid timespans', async () => {
439+
mockAxios.get.mockImplementationOnce(() => {
440+
return Promise.resolve({
441+
status: 200,
442+
data: manifestWithInvalidStruct
443+
});
440444
});
441-
});
442-
mockAxios.head.mockImplementationOnce(() => {
443-
return Promise.resolve({
444-
status: 200,
445-
request: {
446-
responseURL: 'https://example.com/lunchroom_manners/waveform.json',
445+
mockAxios.head.mockImplementationOnce(() => {
446+
return Promise.resolve({
447+
status: 200,
448+
request: {
449+
responseURL: 'https://example.com/lunchroom_manners/waveform.json',
450+
},
451+
});
452+
});
453+
const initialState = {
454+
manifest: {
455+
manifestFetched: true,
456+
manifest: manifestWithInvalidStruct,
457+
mediaInfo: {
458+
src: 'http://example.com/volleyball-for-boys/high/volleyball-for-boys.mp4',
459+
duration: 662.037,
460+
},
447461
},
462+
};
463+
const app = renderWithRedux(<App {...props} />, { initialState });
464+
465+
await waitFor(() => {
466+
expect(app.queryAllByTestId('list-item').length).toBeGreaterThan(0);
467+
expect(app.getAllByTestId('heading-label')[0].innerHTML).toEqual('Lunchroom Manners');
468+
expect(app.getByTestId('alert-container')).toBeInTheDocument();
469+
expect(app.getByTestId('alert-message').innerHTML)
470+
.toEqual('Please check the marked invalid timespan(s)/heading(s).');
471+
expect(app.getByTestId('structure-save-button')).not.toBeEnabled();
448472
});
449473
});
450-
const initialState = {
451-
manifest: {
452-
manifestFetched: true,
453-
manifest: manifestWithInvalidStruct,
454-
mediaInfo: {
455-
src: 'http://example.com/volleyball-for-boys/high/volleyball-for-boys.mp4',
456-
duration: 662.037,
474+
475+
test('invalid headings (empty)', async () => {
476+
mockAxios.get.mockImplementationOnce(() => {
477+
return Promise.resolve({
478+
status: 200,
479+
data: manifestWEmptyRanges
480+
});
481+
});
482+
mockAxios.head.mockImplementationOnce(() => {
483+
return Promise.resolve({
484+
status: 200,
485+
request: {
486+
responseURL: 'https://example.com/lunchroom_manners/waveform.json',
487+
},
488+
});
489+
});
490+
const initialState = {
491+
manifest: {
492+
manifestFetched: true,
493+
manifest: manifestWEmptyRanges,
494+
mediaInfo: {
495+
src: 'http://example.com/volleyball-for-boys/high/volleyball-for-boys.mp4',
496+
duration: 662.037,
497+
},
457498
},
458-
},
459-
};
460-
const app = renderWithRedux(<App {...props} />, { initialState });
499+
};
500+
const app = renderWithRedux(<App {...props} />, { initialState });
461501

462-
await waitFor(() => {
463-
expect(app.queryAllByTestId('list-item').length).toBeGreaterThan(0);
464-
expect(app.getAllByTestId('heading-label')[0].innerHTML).toEqual('Lunchroom Manners');
465-
expect(app.getByTestId('alert-container')).toBeInTheDocument();
466-
expect(app.getByTestId('alert-message').innerHTML)
467-
.toEqual('Please check start/end times of the marked invalid timespan(s).');
502+
await waitFor(() => {
503+
expect(app.queryAllByTestId('list-item').length).toBeGreaterThan(0);
504+
expect(app.getAllByTestId('heading-label')[0].innerHTML).toEqual('Root');
505+
expect(app.getByTestId('alert-container')).toBeInTheDocument();
506+
expect(app.getByTestId('alert-message').innerHTML)
507+
.toEqual('Please check the marked invalid timespan(s)/heading(s).');
508+
expect(app.getByTestId('structure-save-button')).not.toBeEnabled();
509+
});
468510
});
469511
});
470512
});

src/actions/forms.js

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,10 @@ import { v4 as uuidv4 } from 'uuid';
66
* Enable/disable other editing actions when editing a list item
77
* @param {Integer} code - choose from; 1(true) | 0(false)
88
*/
9-
export const handleEditingTimespans =
10-
(
11-
code,
12-
valid = true // assumes structure data is valid by default
13-
) =>
14-
(dispatch) => {
15-
dispatch({ type: types.IS_EDITING_TIMESPAN, code });
16-
// Remove dismissible alerts when a CRUD action has been initiated
17-
// given editing is starting (code = 1) and structure is validated.
18-
if (code == 1 && valid) {
19-
dispatch(clearExistingAlerts());
20-
}
21-
};
9+
export const handleEditingTimespans = (code) => ({
10+
type: types.IS_EDITING_TIMESPAN,
11+
code
12+
});
2213

2314
export const setAlert = (alert) => (dispatch) => {
2415
const id = uuidv4();

src/actions/sm-data.js

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,5 @@
11
import * as types from './types';
2-
import { updateStructureStatus, clearExistingAlerts } from './forms';
3-
4-
export function reBuildSMUI(items, duration) {
5-
return (dispatch, getState) => {
6-
dispatch(buildSMUI(items, duration));
7-
const { structuralMetadata } = getState();
8-
// Remove invalid structure alert when data is corrected
9-
if (structuralMetadata.smDataIsValid) {
10-
dispatch(clearExistingAlerts());
11-
}
12-
dispatch(updateStructureStatus(0));
13-
};
14-
}
2+
import { updateStructureStatus } from './forms';
153

164
export function buildSMUI(json, duration) {
175
return {
@@ -21,10 +9,11 @@ export function buildSMUI(json, duration) {
219
};
2210
}
2311

24-
export function deleteItem(id) {
12+
export function updateSMUI(json, isValid) {
2513
return {
26-
type: types.DELETE_ITEM,
27-
id,
14+
type: types.UPDATE_SM_UI,
15+
json,
16+
isValid,
2817
};
2918
}
3019

src/actions/types.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
export const BUILD_SM_UI = 'BUILD_SM_UI';
2-
export const REBUILD_SM_UI = 'REBUILD_SM_UI';
3-
export const DELETE_ITEM = 'DELETE_ITEM';
2+
export const UPDATE_SM_UI = 'UPDATE_SM_UI';
43

54
export const ADD_DROP_TARGETS = 'ADD_DROP_TARGETS';
65
export const REMOVE_DROP_TARGETS = 'REMOVE_DROP_TARGETS';

src/components/ButtonSection.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import HeadingFormContainer from '../containers/HeadingFormContainer';
77
import TimespanFormContainer from '../containers/TimespanFormContainer';
88
import * as peaksActions from '../actions/peaks-instance';
99
import { configureAlert } from '../services/alert-status';
10-
import { handleEditingTimespans, setAlert } from '../actions/forms';
10+
import { setAlert } from '../actions/forms';
11+
import { useStructureUpdate } from '../services/sme-hooks';
1112

1213
const styles = {
1314
well: {
@@ -27,8 +28,11 @@ const ButtonSection = () => {
2728
const dispatch = useDispatch();
2829
const createTempSegment = () => dispatch(peaksActions.insertTempSegment());
2930
const removeTempSegment = (id) => dispatch(peaksActions.deleteTempSegment(id));
30-
const updateEditingTimespans = (value) => dispatch(handleEditingTimespans(value));
3131
const settingAlert = (alert) => dispatch(setAlert(alert));
32+
const dragSegment = (id, startTimeChanged, flag) =>
33+
dispatch(peaksActions.dragSegment(id, startTimeChanged, flag));
34+
35+
const { updateEditingTimespans } = useStructureUpdate();
3236

3337
// Get state variables from Redux store
3438
const { editingDisabled, structureInfo, streamInfo } = useSelector((state) => state.forms);
@@ -90,7 +94,7 @@ const ButtonSection = () => {
9094
settingAlert(noSpaceAlert);
9195
} else {
9296
// Initialize Redux store with temporary segment
93-
dispatch(peaksActions.dragSegment(tempSegment.id, null, 0));
97+
dragSegment(tempSegment.id, null, 0);
9498
setInitSegment(tempSegment);
9599
setTimespanOpen(true);
96100
setIsInitializing(true);

src/components/ListItem.js

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,18 @@ import ListItemEditForm from './ListItemEditForm';
1010
import ListItemControls from './ListItemControls';
1111
import { ItemTypes } from '../services/Constants';
1212
import * as actions from '../actions/sm-data';
13-
import { deleteSegment } from '../actions/peaks-instance';
14-
import { handleEditingTimespans } from '../actions/forms';
13+
import { useStructureUpdate } from '../services/sme-hooks';
1514

1615
const ListItem = ({ item, children }) => {
1716
// Dispatch actions to Redux store
1817
const dispatch = useDispatch();
19-
const deleteItem = (id) => dispatch(actions.deleteItem(id));
2018
const addDropTargets = (item) => dispatch(actions.addDropTargets(item));
2119
const removeDropTargets = () => dispatch(actions.removeDropTargets());
2220
const removeActiveDragSources = () => dispatch(actions.removeActiveDragSources());
2321
const setActiveDragSource = (id) => dispatch(actions.setActiveDragSource(id));
2422
const handleListItemDrop = (item, dropItem) => dispatch(actions.handleListItemDrop(item, dropItem));
25-
const removeSegment = (item) => dispatch(deleteSegment(item));
26-
const updateEditingTimespans = (value, smDataIsValid) => dispatch(handleEditingTimespans(value, smDataIsValid));
2723

28-
// Get state variables from Redux store
29-
const { smDataIsValid } = useSelector((state) => state.structuralMetadata);
24+
const { deleteStructItem, updateEditingTimespans } = useStructureUpdate();
3025

3126
const [editing, setEditing] = useState(false);
3227

@@ -54,21 +49,20 @@ const ListItem = ({ item, children }) => {
5449

5550
const handleDelete = () => {
5651
try {
57-
deleteItem(item.id);
58-
removeSegment(item);
52+
deleteStructItem(item);
5953
} catch (error) {
6054
showBoundary(error);
6155
}
6256
};
6357

6458
const handleEditClick = () => {
65-
updateEditingTimespans(1, smDataIsValid);
59+
updateEditingTimespans(1);
6660
setEditing(true);
6761
};
6862

6963
const handleEditFormCancel = () => {
7064
setEditing(false);
71-
updateEditingTimespans(0, smDataIsValid);
65+
updateEditingTimespans(0);
7266
};
7367

7468
const handleShowDropTargetsClick = () => {
@@ -143,6 +137,15 @@ const ListItem = ({ item, children }) => {
143137
className='structure-title heading'
144138
data-testid='heading-label'
145139
>
140+
{(!valid && type !== 'root') && (
141+
<>
142+
<FontAwesomeIcon
143+
icon={faExclamationTriangle}
144+
className='icon-invalid'
145+
title='Please add at least one timespan or remove this heading.' />
146+
{' '}
147+
</>
148+
)}
146149
{label}
147150
</div>
148151
)}

src/components/ListItemControls.js

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,9 @@ import PopoverBody from 'react-bootstrap/PopoverBody';
77
import PopoverHeader from 'react-bootstrap/PopoverHeader';
88
import PropTypes from 'prop-types';
99
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
10-
import { useDispatch, useSelector } from 'react-redux';
11-
import {
12-
handleEditingTimespans,
13-
updateStructureStatus,
14-
} from '../actions/forms';
10+
import { useSelector } from 'react-redux';
1511
import { faPen, faTrash, faDotCircle } from '@fortawesome/free-solid-svg-icons';
12+
import { useStructureUpdate } from '../services/sme-hooks';
1613

1714
const styles = {
1815
buttonToolbar: {
@@ -26,10 +23,7 @@ const styles = {
2623
};
2724

2825
const ListItemControls = ({ handleDelete, handleEditClick, handleShowDropTargetsClick, item }) => {
29-
// Dispatch actions to Redux store
30-
const dispatch = useDispatch();
31-
const updateEditingTimespans = (value) => dispatch(handleEditingTimespans(value));
32-
const updateStructStatus = (value) => dispatch(updateStructureStatus(value));
26+
const { updateEditingTimespans } = useStructureUpdate();
3327

3428
// Get state variables from Redux store
3529
const { editingDisabled } = useSelector((state) => state.forms);
@@ -47,8 +41,6 @@ const ListItemControls = ({ handleDelete, handleEditClick, handleShowDropTargets
4741
enableEditing();
4842
setDeleteMessage('');
4943
setShowDeleteConfirm(false);
50-
// Change structureIsSaved to false
51-
updateStructStatus(0);
5244
};
5345

5446
const handleDeleteClick = (e) => {

src/components/ListItemEditForm.js

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,20 @@
11
import React, { useEffect, useState } from 'react';
22
import { useErrorBoundary } from 'react-error-boundary';
33
import PropTypes from 'prop-types';
4-
import { useDispatch, useSelector } from 'react-redux';
4+
import { useSelector } from 'react-redux';
55
import TimespanInlineForm from './TimespanInlineForm';
66
import HeadingInlineForm from './HeadingInlineForm';
7-
import { reBuildSMUI } from '../actions/sm-data';
87
import { cloneDeep } from 'lodash';
98
import StructuralMetadataUtils from '../services/StructuralMetadataUtils';
9+
import { useStructureUpdate } from '../services/sme-hooks';
1010

1111
const structuralMetadataUtils = new StructuralMetadataUtils();
1212

1313
const ListItemEditForm = ({ item, handleEditFormCancel }) => {
14-
// Dispatch actions to Redux store
15-
const dispatch = useDispatch();
16-
const updateSMUI = (cloned, duration) => dispatch(reBuildSMUI(cloned, duration));
14+
const { updateStructure } = useStructureUpdate();
1715

1816
// Get state variables from Redux store
1917
const { smData } = useSelector((state) => state.structuralMetadata);
20-
const { duration } = useSelector((state) => state.peaksInstance);
2118

2219
const [isTyping, _setIsTyping] = useState(false);
2320
const [isInitializing, _setIsInitializing] = useState(true);
@@ -78,8 +75,8 @@ const ListItemEditForm = ({ item, handleEditFormCancel }) => {
7875
// Update item values
7976
item = addUpdatedValues(item, payload);
8077

81-
// Send updated smData back to redux
82-
updateSMUI(clonedItems, duration);
78+
// Send updated smData back to redux via custom hook
79+
updateStructure(clonedItems);
8380

8481
// Turn off editing state
8582
handleEditFormCancel();

0 commit comments

Comments
 (0)