diff --git a/src/components/dashboard/ActionBarChart.js b/src/components/dashboard/ActionBarChart.js index 3af87908..2785a0aa 100644 --- a/src/components/dashboard/ActionBarChart.js +++ b/src/components/dashboard/ActionBarChart.js @@ -47,15 +47,17 @@ class ActionBarChart extends PureComponent { t: PropTypes.func.isRequired, actions: PropTypes.arrayOf(PropTypes.object), spaces: PropTypes.arrayOf(PropTypes.object), + id: PropTypes.string, }; static defaultProps = { actions: [], spaces: [], + id: null, }; - renderChart = actions => { - const { spaces, theme, t } = this.props; + renderChart = (actions) => { + const { spaces, theme, t, id: elementId } = this.props; const { palette: { primary, type }, } = theme; @@ -75,7 +77,7 @@ class ActionBarChart extends PureComponent { }); return ( - + { + formatDate = (date) => { const d = new Date(date); return `${d.getDate()}/${d.getMonth()}/${d.getFullYear()}`; }; render() { - const { theme, t, actions } = this.props; + const { theme, t, actions, id: elementId } = this.props; const { palette: { primary, type }, } = theme; @@ -71,7 +73,7 @@ class ActionLineChart extends PureComponent { return

{t('No action has been recorded.')}

; } - const dataWithDateFormatted = actions.map(action => ({ + const dataWithDateFormatted = actions.map((action) => ({ date: [this.formatDate(action.createdAt)], data: action.data, })); @@ -88,7 +90,7 @@ class ActionLineChart extends PureComponent { return ( <> {t('Action Count Per Space')} - + ({ +const styles = (theme) => ({ customTooltip: { backgroundColor: 'white', padding: '0.05rem 0.5rem', @@ -86,10 +86,12 @@ class ActionPieChart extends PureComponent { }).isRequired, t: PropTypes.func.isRequired, actions: PropTypes.arrayOf(PropTypes.object), + id: PropTypes.string, }; static defaultProps = { actions: [], + id: null, }; renderCustomizedLabel = ({ @@ -118,7 +120,7 @@ class ActionPieChart extends PureComponent { }; render() { - const { actions, classes, t } = this.props; + const { actions, classes, t, id } = this.props; if (!actions) { return ; @@ -141,7 +143,7 @@ class ActionPieChart extends PureComponent { return ( <> {t('Action Type Chart')} - + ; @@ -38,12 +40,12 @@ class ActionTotalCount extends PureComponent { const count = actions.length; return ( - <> +
{t('Total Action Count')} - +
); } } diff --git a/src/components/dashboard/Dashboard.js b/src/components/dashboard/Dashboard.js index 1ef79509..fbbcaa8c 100644 --- a/src/components/dashboard/Dashboard.js +++ b/src/components/dashboard/Dashboard.js @@ -24,9 +24,19 @@ import { SELECT_ALL_USERS_ID, USER_MODES, } from '../../config/constants'; -import { DASHBOARD_MAIN_ID } from '../../config/selectors'; - -const styles = theme => ({ +import { + DASHBOARD_BAR_CHART_ID, + DASHBOARD_ACTION_EDITOR_ID, + DASHBOARD_MAIN_ID, + DASHBOARD_NO_ACTION_MESSAGE_ID, + DASHBOARD_LINE_CHART_ID, + DASHBOARD_PIE_CHART_ID, + DASHBOARD_TOTAL_COUNT_ID, + DASHBOARD_USER_FILTER_ID, + DASHBOARD_SPACE_FILTER_ID, +} from '../../config/selectors'; + +const styles = (theme) => ({ dashboard: { padding: theme.spacing(3) }, dashboardGridItem: { height: '350px' }, drawerHeader: { @@ -91,24 +101,25 @@ export class Dashboard extends Component { dispatchGetDatabase(); } - handleSpaceChange = event => { + handleSpaceChange = (event) => { this.setState({ spaceId: event.target.value }); }; - handleUserChange = event => { + handleUserChange = (event) => { this.setState({ filteredUserId: event.target.value }); }; renderSpaceFilter = () => { const { database, t } = this.props; const { spaceId } = this.state; + const { spaces = [] } = database; if (!database) { return ; } return ( - + {t('Filter by Space')} @@ -139,7 +150,7 @@ export class Dashboard extends Component { } return ( - + {t('Filter by User')} @@ -157,7 +168,7 @@ export class Dashboard extends Component { ); }; - renderActionWidgets = filteredActions => { + renderActionWidgets = (filteredActions) => { const { database, classes } = this.props; return ( @@ -169,16 +180,29 @@ export class Dashboard extends Component { alignItems="center" > - + - + - + - + ); @@ -188,15 +212,14 @@ export class Dashboard extends Component { const { t, database, userMode, userId } = this.props; const { spaceId, filteredUserId } = this.state; - let filteredActions = database.actions; - const { users } = database; + let filteredActions = database.actions || []; + const { users = [] } = database; const isStudent = userMode === USER_MODES.STUDENT; filteredActions = filteredActions.filter(({ user }) => { const isOwnAction = user === userId; const actionUser = users.find(({ id }) => id === user); - const isAccessible = - actionUser && actionUser.settings.actionAccessibility; + const isAccessible = actionUser?.settings?.actionAccessibility; const filteredUserSelected = filteredUserId !== SELECT_ALL_USERS_ID; // filter action per user if userMode is student @@ -222,10 +245,15 @@ export class Dashboard extends Component { return filteredActions.length ? ( <> {this.renderActionWidgets(filteredActions)} - + ) : ( - {t('No action has been recorded.')} + + {t('No action has been recorded.')} + ); }; diff --git a/src/components/dashboard/Dashboard.test.js b/src/components/dashboard/Dashboard.test.js new file mode 100644 index 00000000..3383c4ce --- /dev/null +++ b/src/components/dashboard/Dashboard.test.js @@ -0,0 +1,179 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import Typography from '@material-ui/core/Typography'; +import FormControl from '@material-ui/core/FormControl'; +import MenuItem from '@material-ui/core/MenuItem'; +import { Dashboard } from './Dashboard'; +import { DEFAULT_USER_MODE, USER_MODES } from '../../config/constants'; +import { + DASHBOARD_ACTION_EDITOR_ID, + DASHBOARD_BAR_CHART_ID, + DASHBOARD_USER_FILTER_ID, + DASHBOARD_LINE_CHART_ID, + DASHBOARD_NO_ACTION_MESSAGE_ID, + DASHBOARD_PIE_CHART_ID, + DASHBOARD_SPACE_FILTER_ID, + DASHBOARD_TOTAL_COUNT_ID, +} from '../../config/selectors'; + +const sampleUserId = '123456'; +const emptyUserId = 'empty'; +const sampleUsers = [ + { id: sampleUserId, username: 'bob' }, + { id: 'randomId', username: 'anna' }, + { id: emptyUserId, username: 'cedric' }, +]; +const sampleSpaces = [{ id: 'spaceId', name: 'spaceName' }]; +const sampleActions = [ + { id: 'actionId', user: sampleUserId, spaceId: 'spaceId' }, +]; +const sampleDatabase = { + actions: sampleActions, + users: sampleUsers, + spaces: sampleSpaces, +}; + +const createDashboardProps = ({ + userId, + userMode = DEFAULT_USER_MODE, + users, + actions, + spaces, +} = {}) => { + return { + t: jest.fn((text) => text), + classes: { + dashboard: '', + dashboardGridItem: '', + root: '', + appBar: '', + appBarShift: '', + menuButton: '', + hide: '', + drawer: '', + drawerPaper: '', + drawerHeader: '', + content: '', + contentShift: '', + developer: '', + screenTitle: '', + }, + theme: { + direction: '', + }, + history: { + replace: jest.fn(), + }, + i18n: { + changeLanguage: jest.fn(), + }, + database: { + spaces, + users, + actions, + }, + dispatchGetDatabase: jest.fn(), + userMode, + userId, + }; +}; + +describe('', () => { + let wrapper; + + describe('default props', () => { + beforeAll(() => { + const props = createDashboardProps(); + // eslint-disable-next-line react/jsx-props-no-spreading + wrapper = shallow(); + }); + + it('renders correctly', () => { + expect(wrapper).toMatchSnapshot(); + }); + + it('no actions renders message', () => { + const typography = wrapper + .find(Typography) + .find(`#${DASHBOARD_NO_ACTION_MESSAGE_ID}`); + expect(typography).toHaveLength(1); + expect(typography.contains('No action has been recorded.')).toBeTruthy(); + }); + }); + + describe('actions is defined', () => { + describe('userMode = student', () => { + it('displays no actions if no action exists for given userId', () => { + const props = createDashboardProps({ + userId: emptyUserId, + ...sampleDatabase, + }); + // eslint-disable-next-line react/jsx-props-no-spreading + wrapper = shallow(); + const typography = wrapper + .find(Typography) + .find(`#${DASHBOARD_NO_ACTION_MESSAGE_ID}`); + expect(typography).toHaveLength(1); + expect( + typography.contains('No action has been recorded.') + ).toBeTruthy(); + }); + + it('displays 4 visualizations widgets, editor, space filter for given userId', () => { + const props = createDashboardProps({ + userId: sampleUserId, + ...sampleDatabase, + }); + // eslint-disable-next-line react/jsx-props-no-spreading + wrapper = shallow(); + const barChart = wrapper.find(`#${DASHBOARD_BAR_CHART_ID}`); + expect(barChart).toHaveLength(1); + expect(barChart).toHaveLength(1); + const lineChart = wrapper.find(`#${DASHBOARD_LINE_CHART_ID}`); + expect(lineChart).toHaveLength(1); + const pieChart = wrapper.find(`#${DASHBOARD_PIE_CHART_ID}`); + expect(pieChart).toHaveLength(1); + const editor = wrapper.find(`#${DASHBOARD_ACTION_EDITOR_ID}`); + expect(editor).toHaveLength(1); + const totalCount = wrapper.find(`#${DASHBOARD_TOTAL_COUNT_ID}`); + expect(totalCount).toHaveLength(1); + + const spaceFilter = wrapper.find(`#${DASHBOARD_SPACE_FILTER_ID}`); + expect(spaceFilter).toHaveLength(1); + + const userFilter = wrapper.find(`#${DASHBOARD_USER_FILTER_ID}`); + expect(userFilter).toHaveLength(0); + }); + }); + + describe(`userMode = teacher`, () => { + beforeAll(() => { + const props = createDashboardProps({ + userId: sampleUserId, + userMode: USER_MODES.TEACHER, + ...sampleDatabase, + }); + // eslint-disable-next-line react/jsx-props-no-spreading + wrapper = shallow(); + }); + + it('displays user and space filters', () => { + const spaceFilter = wrapper.find(`#${DASHBOARD_SPACE_FILTER_ID}`); + expect(spaceFilter).toHaveLength(1); + + const formControl = wrapper + .find(FormControl) + .find(`#${DASHBOARD_USER_FILTER_ID}`); + const menuItems = formControl.find(MenuItem).map((item) => item.text()); + + expect(menuItems).toContain('All Users'); + // eslint-disable-next-line no-restricted-syntax + for (const user of sampleUsers) { + expect(menuItems).toContain(user.username); + } + + expect(menuItems.length).toBe(sampleUsers.length + 1); + }); + }); + }); +}); diff --git a/src/components/dashboard/__snapshots__/Dashboard.test.js.snap b/src/components/dashboard/__snapshots__/Dashboard.test.js.snap new file mode 100644 index 00000000..8836fff7 --- /dev/null +++ b/src/components/dashboard/__snapshots__/Dashboard.test.js.snap @@ -0,0 +1,74 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` default props renders correctly 1`] = ` + +
+ + + + Dashboard + + + + + + + Filter by Space + + + + + All Spaces + + + + + + + + No action has been recorded. + +
+
+`; diff --git a/src/config/selectors.js b/src/config/selectors.js index 04d4592f..72f8f533 100644 --- a/src/config/selectors.js +++ b/src/config/selectors.js @@ -42,7 +42,7 @@ export const SPACE_PREVIEW_ICON_CLASS = 'spacePreviewIcon'; export const PHASE_DESCRIPTION_ID = 'phaseDescription'; export const PHASE_DRAWER_CLASS = 'PhasesDrawer'; export const SPACE_MEDIA_CARD_CLASS = 'spaceMediaCard'; -export const buildPhaseMenuItemId = id => `phaseMenuItemId-${id}`; +export const buildPhaseMenuItemId = (id) => `phaseMenuItemId-${id}`; export const BANNER_WARNING_PREVIEW_ID = 'bannerWarningPreview'; export const SPACE_EXPORT_BUTTON_CLASS = 'spaceExportButton'; export const SPACE_DELETE_BUTTON_CLASS = 'spaceDeleteButton'; @@ -54,10 +54,10 @@ export const TOOLS_CONTENT_PANE_ID = 'toolsContentPane'; export const SPACE_SEARCH_INPUT_ID = 'searchInput'; export const SPACE_CARD_CLASS = 'spaceCard'; -export const buildSpaceCardId = id => `spaceCard-${id}`; +export const buildSpaceCardId = (id) => `spaceCard-${id}`; export const SPACE_DESCRIPTION_EXPAND_BUTTON_CLASS = 'spaceDescriptionExpandButton'; -export const buildSpaceCardDescriptionId = id => `spaceCardDescription-${id}`; +export const buildSpaceCardDescriptionId = (id) => `spaceCardDescription-${id}`; export const SPACE_CARD_LINK_CLASS = 'spaceCardLink'; export const SPACE_NOT_AVAILABLE_TEXT_ID = 'noSpaceAvailableText'; @@ -66,7 +66,7 @@ export const LOAD_INPUT_ID = 'loadInput'; export const LOAD_SUBMIT_BUTTON_ID = 'loadSubmitButton'; export const LOAD_LOAD_BUTTON_ID = 'loadLoadButton'; export const LOAD_BACK_BUTTON_ID = 'loadCancelButton'; -export const buildCheckboxLabel = name => `checkboxLabel-${name}`; +export const buildCheckboxLabel = (name) => `checkboxLabel-${name}`; export const EXPORT_SPACE_BUTTON_ID = 'exportSpaceButton'; export const EXPORT_SPACE_BACK_BUTTON_ID = 'exportSpaceBackButton'; @@ -78,7 +78,7 @@ export const GEOLOCATION_CONTROL_ID = 'settingsGeolocationControl'; export const SYNC_MODE_SWITCH_ID = 'syncModeSwitch'; export const STUDENT_MODE_SWITCH_ID = 'studentModeSwitch'; -export const buildPhaseAppName = id => `phaseApp-${id}`; +export const buildPhaseAppName = (id) => `phaseApp-${id}`; export const LOGIN_USERNAME_INPUT_ID = 'loginUsernameInput'; export const LOGIN_BUTTON_ID = 'loginButton'; @@ -96,7 +96,7 @@ export const CLASSROOM_NAME_INPUT_ID = 'classroomNameInput'; export const ADD_CLASSROOM_VALIDATE_BUTTON_ID = 'addClassroomValidateButton'; export const ADD_CLASSROOM_CANCEL_BUTTON_ID = 'addClassroomCancelButton'; export const CLASSROOM_CARD_CLASS = 'classroomCard'; -export const buildClassroomCardId = id => `classroomCard-${id}`; +export const buildClassroomCardId = (id) => `classroomCard-${id}`; export const EDIT_CLASSROOM_BUTTON_CLASS = 'editClassroomButton'; export const EDIT_CLASSROOM_INPUT_ID = 'editClassroomInput'; export const EDIT_CLASSROOM_VALIDATE_BUTTON_ID = 'editClassroomValidateButton'; @@ -147,9 +147,18 @@ export const EDIT_USER_IN_CLASSROOM_DELETE_DATA_BUTTON_CLASS = 'editUserInClassroomDeleteDataButton'; export const EDIT_CLASSROOM_DELETE_DATA_BUTTON_CLASS = 'editClassroomDeleteDataButton'; -export const buildTableCellSpaceId = id => `tableCellSpace-${id}`; +export const buildTableCellSpaceId = (id) => `tableCellSpace-${id}`; export const DRAWER_HEADER_TEACHER_ICON_ID = 'drawerHeaderTeacherIcon'; export const TOUR_END_SELECTOR = '#react-joyride-step-0 > div > div > div.react-joyride__tooltip > div:nth-child(2) > div > button'; + +export const DASHBOARD_ACTION_EDITOR_ID = 'dashboardActionEditor'; +export const DASHBOARD_BAR_CHART_ID = 'dashboardBarChart'; +export const DASHBOARD_USER_FILTER_ID = 'dashboardUserFilter'; +export const DASHBOARD_LINE_CHART_ID = 'dashboardLineChart'; +export const DASHBOARD_NO_ACTION_MESSAGE_ID = 'dashboardNoActionMessage'; +export const DASHBOARD_PIE_CHART_ID = 'dashboardPieChart'; +export const DASHBOARD_SPACE_FILTER_ID = 'dashboardSpaceFilter'; +export const DASHBOARD_TOTAL_COUNT_ID = 'dashboardTotalCount';