homeflowjs 0.7.15 → 0.7.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/Jenkinsfile +41 -0
  2. package/__tests__/instant-valuation.test.jsx +1 -1
  3. package/actions/properties.actions.js +5 -0
  4. package/actions/properties.actions.test.js +13 -0
  5. package/actions/properties.types.js +1 -0
  6. package/actions/user.actions.js +9 -15
  7. package/actions/user.actions.test.js +1 -0
  8. package/app/notify.js +6 -7
  9. package/app/with-homeflow-state.jsx +4 -0
  10. package/bin/peer-dependencies +12 -0
  11. package/bin/test +17 -0
  12. package/branches/branches-search-form/branches-search-input.component.jsx +40 -3
  13. package/package.json +7 -2
  14. package/reducers/index.js +9 -1
  15. package/reducers/properties.reducer.js +8 -2
  16. package/reducers/properties.reducer.test.js +16 -0
  17. package/reducers/user.reducer.js +1 -1
  18. package/search/location-input/location-input.component.jsx +8 -1
  19. package/search/location-input/location-input.test.js +1 -1
  20. package/search/radius-select/radius-select.test.js +1 -0
  21. package/search/search-form/search-form.component.jsx +1 -0
  22. package/store.js +3 -1
  23. package/user/default-profile/account/account-edit.component.jsx +9 -9
  24. package/user/default-profile/account/register-form.component.jsx +7 -6
  25. package/user/default-profile/account/sign-in-form.component.jsx +2 -2
  26. package/user/default-profile/account/sign-in-form.component.test.jsx +149 -0
  27. package/user/index.js +2 -0
  28. package/user/sign-out-button/sign-out-button.component.jsx +1 -1
  29. package/user/user-input/user-input.component.jsx +1 -0
  30. package/user/user-sign-in-form/sign-in-input.component.jsx +1 -0
  31. package/user/user-sign-in-form/user-sign-in-form.component.jsx +6 -2
  32. package/utils/index.js +4 -0
  33. package/utils/index.test.js +21 -0
package/Jenkinsfile ADDED
@@ -0,0 +1,41 @@
1
+ pipeline {
2
+ triggers {
3
+ bitbucketPush()
4
+ }
5
+
6
+ environment {
7
+ TERM = 'xterm'
8
+ }
9
+
10
+ agent {
11
+ docker {
12
+ alwaysPull true
13
+ image 'homeflowdev/rubydocker'
14
+ args '-v /var/run/docker.sock:/var/run/docker.sock \
15
+ -v /var/secrets:/var/secrets \
16
+ -v /var/secrets/npm/.npmrc:/root/.npmrc'
17
+ }
18
+ }
19
+
20
+ stages {
21
+ stage('Setup') {
22
+ steps {
23
+ sh 'yarn install'
24
+ sh 'npm install --global $(./bin/peer-dependencies -v)'
25
+ sh 'for f in $(./bin/peer-dependencies); do (cd "/usr/lib/node_modules/${f}/" && yarn link); done'
26
+ }
27
+ }
28
+
29
+ stage('Test') {
30
+ steps {
31
+ sh 'yarn test'
32
+ }
33
+ }
34
+ }
35
+
36
+ post {
37
+ always {
38
+ cleanWs()
39
+ }
40
+ }
41
+ }
@@ -61,7 +61,7 @@ describe('Instant Valuation', () => {
61
61
  }),
62
62
  );
63
63
 
64
- const message = await screen.findByText('Please fill out the required fields');
64
+ const message = await screen.getByText('Please fill out the required fields');
65
65
  expect(message).toBeInTheDocument();
66
66
  });
67
67
  });
@@ -91,6 +91,11 @@ export const setPropertyLinksAsync = () => (dispatch) => {
91
91
  });
92
92
  };
93
93
 
94
+ export const setProperty = (payload) => ({
95
+ type: PropertiesActionTypes.SET_PROPERTY,
96
+ payload,
97
+ });
98
+
94
99
  export const setProperties = (payload) => ({
95
100
  type: PropertiesActionTypes.SET_PROPERTIES,
96
101
  payload,
@@ -0,0 +1,13 @@
1
+ import store from '../store';
2
+ import * as actions from './properties.actions';
3
+
4
+ describe('Properties action creators', () => {
5
+ it('creates correct action for setProperty', () => {
6
+ const expectedAction = {
7
+ type: 'SET_PROPERTY',
8
+ payload: { property_ref: 'ABC123', postcode: 'AB1 1AA' },
9
+ };
10
+
11
+ expect(actions.setProperty({ property_ref: 'ABC123', postcode: 'AB1 1AA' })).toEqual(expectedAction);
12
+ });
13
+ });
@@ -1,4 +1,5 @@
1
1
  const PropertiesActionTypes = {
2
+ SET_PROPERTY: 'SET_PROPERTY',
2
3
  SET_PROPERTIES: 'SET_PROPERTIES',
3
4
  TOGGLE_SAVED_PROPERTY: 'TOGGLE_SAVED_PROPERTY',
4
5
  SET_SAVED_PROPERTIES: 'SET_SAVED_PROPERTIES',
@@ -3,7 +3,7 @@ import { fetchSavedProperties, setSavedProperties } from './properties.actions';
3
3
  import { fetchSavedSearches, setSavedSearches } from './search.actions';
4
4
  import { setLoading } from './app.actions';
5
5
  import { INITIAL_USER_DATA } from '../reducers/user.reducer';
6
- import { objectDiff } from '../utils';
6
+ import { objectDiff, compact } from '../utils';
7
7
 
8
8
  export const setCurrentUser = (payload) => ({
9
9
  type: UserActionTypes.SET_CURRENT_USER,
@@ -33,10 +33,10 @@ export const fetchUser = () => (dispatch) => {
33
33
  const serializedSavedProperties = localStorage.getItem('savedProperties');
34
34
 
35
35
  if (serializedSavedSearches) {
36
- dispatch(setSavedSearches(JSON.parse(serializedSavedSearches)));
36
+ dispatch(setSavedSearches(compact(JSON.parse(serializedSavedSearches))));
37
37
  }
38
38
  if (serializedSavedProperties) {
39
- dispatch(setSavedProperties(JSON.parse(serializedSavedProperties)));
39
+ dispatch(setSavedProperties(compact(JSON.parse(serializedSavedProperties))));
40
40
  }
41
41
  }
42
42
 
@@ -60,17 +60,7 @@ export const createUser = (payload) => (dispatch, getState) => {
60
60
  authenticity_token: authenticityToken
61
61
  }),
62
62
  })
63
- .then((response) => response.json())
64
- .then((json) => {
65
- dispatch(setCurrentUser({ ...localUser, ...json }));
66
- dispatch(
67
- setLocalUser({
68
- ...INITIAL_USER_DATA,
69
- password: '',
70
- password_confirmation: '',
71
- })
72
- );
73
- });
63
+ .then(() => dispatch(fetchUser()));
74
64
  };
75
65
 
76
66
  export const signInUser = (payload) => (dispatch, getState) => {
@@ -89,8 +79,10 @@ export const signInUser = (payload) => (dispatch, getState) => {
89
79
  .then(({ status }) => {
90
80
  if (status === 200) {
91
81
  dispatch(fetchUser());
82
+ return { success: true };
92
83
  } else {
93
- console.error('Could not sign in.');
84
+ console.error('Could not sign in');
85
+ return { success: false };
94
86
  }
95
87
  });
96
88
  };
@@ -116,6 +108,8 @@ export const signOutUser = () => (dispatch) => (
116
108
  })
117
109
  .then(() => {
118
110
  dispatch(setCurrentUser(INITIAL_USER_DATA));
111
+ dispatch(setLocalUser(INITIAL_USER_DATA));
112
+ dispatch(setUserCredentials({ email: '', password: '' }));
119
113
  dispatch(setSavedProperties([]));
120
114
  dispatch(setSavedSearches([]));
121
115
  })
@@ -1,3 +1,4 @@
1
+ import store from '../store';
1
2
  import * as actions from './user.actions';
2
3
 
3
4
  describe('user action creators', () => {
package/app/notify.js CHANGED
@@ -1,14 +1,12 @@
1
1
  import Toastify from 'toastify-js';
2
2
  import 'toastify-js/src/toastify.css';
3
3
 
4
+ import { sanitizeText } from '../utils/index';
5
+
4
6
  const notify = (message, type, config = {}) => {
5
7
  if (!message) return;
6
8
 
7
- let color = '#28a745'; // default is success
8
-
9
- if (type === 'error') {
10
- color = '#dc3545';
11
- }
9
+ const color = { error: '#dc3545' }[type] || '#28a745'; // default is success
12
10
 
13
11
  Toastify({
14
12
  duration: 3000,
@@ -16,8 +14,9 @@ const notify = (message, type, config = {}) => {
16
14
  gravity: 'top',
17
15
  position: 'center',
18
16
  stopOnFocus: true,
19
- text: message,
20
- backgroundColor: color,
17
+ text: sanitizeText(message),
18
+ escapeMarkup: false,
19
+ style: { background: color },
21
20
  ...config,
22
21
  }).showToast();
23
22
  };
@@ -4,6 +4,10 @@ import { HashRouter } from 'react-router-dom';
4
4
 
5
5
  import store from '../store';
6
6
 
7
+ export const resetStore = () => {
8
+ store.dispatch({ type: 'RESET_STORE' });
9
+ };
10
+
7
11
  const withHomeflowState = (WrappedComponent, props) => (
8
12
  <Provider store={store} {...props}>
9
13
  <HashRouter>
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const toVersionString = ([package, version]) => `${package}@${version.replace(/[^0-9\.]/, '')}`;
5
+ const peerDependencies = JSON.parse(fs.readFileSync('./package.json', 'utf8'))['peerDependencies'];
6
+ const dependenciesString = Object.entries(peerDependencies).map(toVersionString).join(' ');
7
+
8
+ if (process.argv[2] === '-v') {
9
+ process.stdout.write(dependenciesString);
10
+ } else {
11
+ process.stdout.write(Object.keys(peerDependencies).join(' '));
12
+ }
package/bin/test ADDED
@@ -0,0 +1,17 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ dependencies="$(bin/peer-dependencies)"
5
+
6
+ yarn link ${dependencies} >/dev/null
7
+ echo 'Linked peer dependencies.'
8
+
9
+ set +e
10
+ jest $@
11
+ code=$?
12
+
13
+ set -e
14
+ yarn unlink ${dependencies} >/dev/null
15
+ echo 'Unlinked peer dependencies.'
16
+
17
+ exit ${code}
@@ -1,13 +1,30 @@
1
- import React, { useState } from 'react';
1
+ import React, { useState, useRef, useEffect } from 'react';
2
2
  import { connect } from 'react-redux';
3
3
  import PropTypes from 'prop-types';
4
4
  import Autosuggest from 'react-autosuggest';
5
5
  import { setBranchesSearch } from '../../actions/branches.actions';
6
6
 
7
- const BranchesSearchInput = ({ branchesSearch, setBranchesSearch, placeholder }) => {
7
+ const BranchesSearchInput = ({
8
+ branchesSearch, setBranchesSearch, placeholder, isSelected, setIsSelected,
9
+ }) => {
8
10
  const [placeID, setPlaceID] = useState('');
9
11
  const [suggestions, setSuggestions] = useState([]);
10
12
 
13
+ /**
14
+ * On country wide (mobile screens) the form takes up the whole screen when the
15
+ * form input is selected. This code handles that by using setFullScreen
16
+ * for the parent component to know if the input is currently selected.
17
+ * Note we have to use useRef as we can't use a onclick to the Autosuggest component.
18
+ */
19
+ const inputEl = useRef(null);
20
+ useEffect(() => {
21
+ inputEl.current.input.onclick = () => {
22
+ if (!isSelected && isSelected !== '') {
23
+ setIsSelected(true);
24
+ }
25
+ };
26
+ }, [inputEl]);
27
+
11
28
  const getSuggestions = (q) => fetch(`/places?term=${q}`)
12
29
  .then((response) => response.json())
13
30
  .then((data) => {
@@ -30,13 +47,23 @@ const BranchesSearchInput = ({ branchesSearch, setBranchesSearch, placeholder })
30
47
  <input type="hidden" name="place_id" value={placeID} />
31
48
  <input type="hidden" name="location" value={branchesSearch} />
32
49
  <Autosuggest
50
+ ref={inputEl}
33
51
  suggestions={suggestions}
34
52
  onSuggestionsClearRequested={() => setSuggestions([])}
35
53
  onSuggestionsFetchRequested={({ value }) => {
36
54
  setBranchesSearch(value);
37
55
  getSuggestions(value);
38
56
  }}
39
- onSuggestionSelected={(_, { suggestion }) => setPlaceID(suggestion.place)}
57
+ onSuggestionSelected={(_, { suggestion }) => {
58
+ /**
59
+ * Check is setIsSelect exists as its is only passed as a prop on
60
+ * countrywide themes, other themes will not use this.
61
+ */
62
+ if (setIsSelected) setIsSelected(false);
63
+ return (
64
+ setPlaceID(suggestion.place)
65
+ );
66
+ }}
40
67
  getSuggestionValue={(suggestion) => suggestion.label}
41
68
  renderSuggestion={(suggestion) => renderSuggestion(suggestion.label)}
42
69
  inputProps={{
@@ -56,10 +83,20 @@ BranchesSearchInput.propTypes = {
56
83
  branchesSearch: PropTypes.string.isRequired,
57
84
  setBranchesSearch: PropTypes.func.isRequired,
58
85
  placeholder: PropTypes.string,
86
+ isSelected: PropTypes.oneOfType([
87
+ PropTypes.bool,
88
+ PropTypes.string,
89
+ ]),
90
+ setIsSelected: PropTypes.oneOfType([
91
+ PropTypes.func,
92
+ PropTypes.string,
93
+ ]),
59
94
  };
60
95
 
61
96
  BranchesSearchInput.defaultProps = {
62
97
  placeholder: '',
98
+ isSelected: '',
99
+ setIsSelected: '',
63
100
  };
64
101
 
65
102
  const mapStateToProps = (state) => ({
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "homeflowjs",
3
- "version": "0.7.15",
3
+ "version": "0.7.19",
4
4
  "description": "JavaScript toolkit for Homeflow themes",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
- "test": "jest"
7
+ "test": "bin/test"
8
8
  },
9
9
  "author": "",
10
10
  "license": "ISC",
@@ -39,16 +39,21 @@
39
39
  "react": "^16.14.0",
40
40
  "react-dom": "^16.14.0",
41
41
  "react-redux": "^7.2.1",
42
+ "react-router": "^5.2.0",
43
+ "react-router-dom": "^5.3.0",
42
44
  "redux": "^4.0.5"
43
45
  },
44
46
  "devDependencies": {
45
47
  "@babel/preset-env": "^7.12.1",
48
+ "@testing-library/dom": "^8.11.1",
46
49
  "@testing-library/jest-dom": "^5.11.9",
47
50
  "@testing-library/react": "^11.2.3",
51
+ "@testing-library/user-event": "^13.5.0",
48
52
  "babel-eslint": "^10.1.0",
49
53
  "babel-jest": "^26.6.0",
50
54
  "enzyme": "^3.11.0",
51
55
  "enzyme-adapter-react-16": "^1.15.5",
56
+ "es-abstract": "^1.19.1",
52
57
  "eslint": "^7.18.0",
53
58
  "eslint-config-airbnb": "^18.2.1",
54
59
  "eslint-config-airbnb-base": "^14.2.1",
package/reducers/index.js CHANGED
@@ -7,7 +7,7 @@ import appReducer from './app.reducer';
7
7
  import propertiesReducer from './properties.reducer';
8
8
  import branchesReducer from './branches.reducer';
9
9
 
10
- export default combineReducers({
10
+ const combined = combineReducers({
11
11
  app: appReducer,
12
12
  search: searchReducer,
13
13
  user: userReducer,
@@ -15,3 +15,11 @@ export default combineReducers({
15
15
  properties: propertiesReducer,
16
16
  branches: branchesReducer,
17
17
  });
18
+
19
+ export default (state, action) => {
20
+ if (action.type == 'RESET_STORE') {
21
+ return combined(undefined, action);
22
+ } else {
23
+ return combined(state, action);
24
+ }
25
+ };
@@ -12,10 +12,15 @@ const INITIAL_STATE = {
12
12
 
13
13
  const propertiesReducer = (state = INITIAL_STATE, action) => {
14
14
  switch (action.type) {
15
+ case PropertiesActionTypes.SET_PROPERTY:
16
+ return {
17
+ ...state,
18
+ property: action.payload,
19
+ };
15
20
  case PropertiesActionTypes.SET_PROPERTIES:
16
21
  return {
17
22
  ...state,
18
- properties: action.payload
23
+ properties: action.payload,
19
24
  };
20
25
  case PropertiesActionTypes.SET_PAGINATION:
21
26
  return {
@@ -52,7 +57,8 @@ const propertiesReducer = (state = INITIAL_STATE, action) => {
52
57
  } else {
53
58
  // need to find the property from the propertiesReducer and add it here
54
59
  const property = state.properties.find(({ property_id }) => property_id === propertyId);
55
- newSavedProperties.push(property);
60
+
61
+ newSavedProperties.push(property || state.property);
56
62
 
57
63
  if (!action.payload.userLoggedIn) {
58
64
  localStorage.setItem('savedProperties', JSON.stringify(newSavedProperties));
@@ -35,4 +35,20 @@ describe('propertiesReducer', () => {
35
35
 
36
36
  expect(reducedState.savedProperties.length).toEqual(0);
37
37
  });
38
+
39
+ it('Sets the current property when it receives the SET_PROPERTY action', () => {
40
+ const property = { id: '123456789', property_ref: 'ABC12345', postcode: 'AB1 1AA' };
41
+ const state = { properties: [], savedProperties: [], pagination: {}, propertyLinks: { next: '', previous: '' } };
42
+ const reducedState = propertiesReducer(state, { type: 'SET_PROPERTY', payload: property });
43
+
44
+ expect(reducedState.property).toEqual(property);
45
+ });
46
+
47
+ it('Retains initial state when it receives the SET_PROPERTY action', () => {
48
+ const property = { id: '123456789', property_ref: 'ABC12345', postcode: 'AB1 1AA' };
49
+ const state = { properties: [], savedProperties: [], pagination: {}, propertyLinks: { next: '', previous: '' } };
50
+ const reducedState = propertiesReducer(state, { type: 'SET_PROPERTY', payload: property });
51
+
52
+ expect(reducedState).toMatchObject(state);
53
+ });
38
54
  });
@@ -28,7 +28,7 @@ export const INITIAL_USER_DATA = {
28
28
  town: '',
29
29
  user_id: null,
30
30
  password: '',
31
- confirm_password: '',
31
+ password_confirmation: '',
32
32
  };
33
33
 
34
34
  // set both currentUser and localUser to same initial data
@@ -71,7 +71,14 @@ class LocationInput extends Component {
71
71
 
72
72
  setSearchField({ placeId: suggestion.place, isQuerySearch: false });
73
73
 
74
- if (searchOnSelection) propertySearch(search);
74
+ if (searchOnSelection) {
75
+ const newSearch = {
76
+ ...search,
77
+ page: null,
78
+ };
79
+
80
+ propertySearch(newSearch);
81
+ }
75
82
 
76
83
  return suggestion.label;
77
84
  }
@@ -8,7 +8,7 @@ describe('LocationInput', () => {
8
8
  it('renders the component', () => {
9
9
  const testProps = {
10
10
  suggestions: [],
11
- q: '',
11
+ search: { q: '' },
12
12
  setSuggestions: jest.fn(),
13
13
  setSearchField: jest.fn(),
14
14
  };
@@ -8,6 +8,7 @@ describe('RadiusSelect', () => {
8
8
  const testProps = {
9
9
  within: '5-miles',
10
10
  setSearchField: jest.fn(),
11
+ search: {},
11
12
  };
12
13
  render(<RadiusSelect { ...testProps } />);
13
14
  expect(screen.getByTestId('select')).toBeInTheDocument();
@@ -80,6 +80,7 @@ class SearchForm extends Component {
80
80
  setPlace,
81
81
  children,
82
82
  setInitialSearch,
83
+ submitCallback,
83
84
  ...otherProps
84
85
  } = this.props;
85
86
 
package/store.js CHANGED
@@ -5,7 +5,8 @@ import { composeWithDevTools } from 'redux-devtools-extension/developmentOnly';
5
5
  import rootReducer from './reducers';
6
6
  import { setThemePreferences, setThemeSettings, setAuthenticityToken } from './actions/app.actions';
7
7
  import { fetchUser } from './actions/user.actions';
8
- import { setProperties, setPagination, setPropertyLinksAsync } from './actions/properties.actions';
8
+ import { setProperties, setPagination, setProperty, setPropertyLinksAsync } from './actions/properties.actions';
9
+ // import { fetchCompanyPreferences } from './actions/company-preferences.actions';
9
10
  // import { fetchCompanyPreferences } from './actions/company-preferences.actions';
10
11
 
11
12
  const middleware = [thunk];
@@ -29,6 +30,7 @@ window.addEventListener('DOMContentLoaded', () => {
29
30
  // TODO: This should use data set inline for use setCompanyPreferences.
30
31
  // store.dispatch(fetchCompanyPreferences());
31
32
 
33
+ store.dispatch(setProperty(Homeflow.get('property')));
32
34
  store.dispatch(setProperties(Homeflow.get('properties')));
33
35
  store.dispatch(setPagination(Homeflow.get('pagination')));
34
36
  store.dispatch(setThemePreferences(Homeflow.get('theme_preferences')));
@@ -8,48 +8,48 @@ import Loader from '../../../shared/loader.component';
8
8
 
9
9
  const AccountEdit = ({ localUser, toggleEdit, updating }) => (
10
10
  <div className="account-edit">
11
- <h3>Edit Your Profile</h3>
11
+ <h3 role="heading">Edit Your Profile</h3>
12
12
 
13
13
  <UserEditForm className="profile-edit-form" callback={toggleEdit}>
14
14
  <fieldset>
15
15
  <div className="profile-edit-form__group">
16
16
  <UserInput name="first_name" required />
17
- <label className={localUser.first_name ? 'shrink' : ''}>First name</label>
17
+ <label htmlFor="user-input-first_name" className={localUser.first_name ? 'shrink' : ''}>First name</label>
18
18
  </div>
19
19
 
20
20
  <div className="profile-edit-form__group">
21
21
  <UserInput name="last_name" required />
22
- <label className={localUser.last_name ? 'shrink' : ''}>Last name</label>
22
+ <label htmlFor="user-input-last_name" className={localUser.last_name ? 'shrink' : ''}>Last name</label>
23
23
  </div>
24
24
 
25
25
  <div className="profile-edit-form__group">
26
26
  <UserInput name="email" required />
27
- <label className={localUser.email ? 'shrink' : ''}>Email</label>
27
+ <label htmlFor="user-input-email" className={localUser.email ? 'shrink' : ''}>Email</label>
28
28
  </div>
29
29
 
30
30
  <div className="profile-edit-form__group">
31
31
  <UserInput name="tel_home" required />
32
- <label className={localUser.tel_home ? 'shrink' : ''}>Phone</label>
32
+ <label htmlFor="user-input-tel_home" className={localUser.tel_home ? 'shrink' : ''}>Phone</label>
33
33
  </div>
34
34
 
35
35
  <div className="profile-edit-form__group">
36
36
  <UserInput name="street_address" />
37
- <label className={localUser.street_address ? 'shrink' : ''}>Street Address</label>
37
+ <label htmlFor="user-input-street_address" className={localUser.street_address ? 'shrink' : ''}>Street Address</label>
38
38
  </div>
39
39
 
40
40
  <div className="profile-edit-form__group">
41
41
  <UserInput name="town" />
42
- <label className={localUser.town ? 'shrink' : ''}>Town</label>
42
+ <label htmlFor="user-input-town" className={localUser.town ? 'shrink' : ''}>Town</label>
43
43
  </div>
44
44
 
45
45
  <div className="profile-edit-form__group">
46
46
  <UserInput name="county" />
47
- <label className={localUser.county ? 'shrink' : ''}>County</label>
47
+ <label htmlFor="user-input-county" className={localUser.county ? 'shrink' : ''}>County</label>
48
48
  </div>
49
49
 
50
50
  <div className="profile-edit-form__group">
51
51
  <UserInput name="postcode" />
52
- <label className={localUser.postcode ? 'shrink' : ''}>Postcode</label>
52
+ <label htmlFor="user-input-postcode" className={localUser.postcode ? 'shrink' : ''}>Postcode</label>
53
53
  </div>
54
54
 
55
55
  <button
@@ -13,36 +13,37 @@ const RegisterForm = ({ localUser, registering }) => (
13
13
  <fieldset>
14
14
  <div className="profile-register-form__group">
15
15
  <UserInput name="first_name" required />
16
- <label className={localUser.first_name ? 'shrink' : ''}>First name</label>
16
+ <label htmlFor="user-input-first_name" className={localUser.first_name ? 'shrink' : ''}>First name</label>
17
17
  </div>
18
18
 
19
19
  <div className="profile-register-form__group">
20
20
  <UserInput name="last_name" required />
21
- <label className={localUser.last_name ? 'shrink' : ''}>Last name</label>
21
+ <label htmlFor="user-input-last_name" className={localUser.last_name ? 'shrink' : ''}>Last name</label>
22
22
  </div>
23
23
 
24
24
  <div className="profile-register-form__group">
25
25
  <UserInput name="email" required />
26
- <label className={localUser.email ? 'shrink' : ''}>Email</label>
26
+ <label htmlFor="user-input-email" className={localUser.email ? 'shrink' : ''}>Email</label>
27
27
  </div>
28
28
 
29
29
  <div className="profile-register-form__group">
30
30
  <UserInput name="tel_home" required />
31
- <label className={localUser.tel_home ? 'shrink' : ''}>Phone</label>
31
+ <label htmlFor="user-input-tel_home" className={localUser.tel_home ? 'shrink' : ''}>Phone</label>
32
32
  </div>
33
33
 
34
34
  <div className="profile-register-form__group">
35
35
  <UserInput name="password" required />
36
- <label className={localUser.password ? 'shrink' : ''}>Password</label>
36
+ <label htmlFor="user-input-password" className={localUser.password ? 'shrink' : ''}>Password</label>
37
37
  </div>
38
38
 
39
39
  <div className="profile-register-form__group">
40
40
  <UserInput name="password_confirmation" required />
41
- <label className={localUser.password_confirmation ? 'shrink' : ''}>Confirm password</label>
41
+ <label htmlFor="user-input-password_confirmation" className={localUser.password_confirmation ? 'shrink' : ''}>Confirm password</label>
42
42
  </div>
43
43
 
44
44
  <button
45
45
  className="profile-register-form__submit"
46
+ role="button"
46
47
  type="submit"
47
48
  disabled={registering}
48
49
  >
@@ -11,12 +11,12 @@ const SignInForm = ({ userCredentials }) => (
11
11
  <fieldset>
12
12
  <div className="profile-sign-in__group">
13
13
  <SignInInput name="email" required />
14
- <label className={userCredentials.email ? 'shrink' : ''}>Email</label>
14
+ <label htmlFor="sign-in-input-email" className={userCredentials.email ? 'shrink' : ''}>Email</label>
15
15
  </div>
16
16
 
17
17
  <div className="profile-sign-in__group">
18
18
  <SignInInput name="password" required />
19
- <label className={userCredentials.password ? 'shrink' : ''}>Password</label>
19
+ <label htmlFor="sign-in-input-password" className={userCredentials.password ? 'shrink' : ''}>Password</label>
20
20
  </div>
21
21
 
22
22
  <button
@@ -0,0 +1,149 @@
1
+ import React from 'react';
2
+ import { render, screen, waitFor, cleanup } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event'
4
+ import { rest } from 'msw';
5
+ import { setupServer } from 'msw/node';
6
+
7
+ import withHomeflowState, { resetStore } from '../../../app/with-homeflow-state';
8
+ import store from '../../../store';
9
+ import SignInForm from './sign-in-form.component';
10
+ import UserProfileModal from '../user-profile/user-profile-modal.component';
11
+
12
+ const user = {
13
+ user_id: '1000',
14
+ first_name: 'Test',
15
+ last_name: 'User',
16
+ email: 'user@example.com',
17
+ tel_home: '0123456789',
18
+ };
19
+
20
+ const server = setupServer(
21
+ rest.post('/session', (req, res, ctx) => (
22
+ res(ctx.json({}))
23
+ )),
24
+ rest.delete('/session', (req, res, ctx) => (
25
+ res(ctx.json({}))
26
+ )),
27
+ rest.get('/user.ljson', (req, res, ctx) => (
28
+ res(ctx.json({ user }))
29
+ )),
30
+ rest.post('/user.ljson', (req, res, ctx) => (
31
+ res(ctx.json({ user }))
32
+ )),
33
+ rest.get('/saved_properties.ljson', (req, res, ctx) => (
34
+ res(ctx.json({ properties: [] }))
35
+ )),
36
+ rest.get('/saved_searches.ljson', (req, res, ctx) => (
37
+ res(ctx.json([]))
38
+ )),
39
+ );
40
+
41
+ beforeAll(() => server.listen());
42
+ afterEach(() => server.resetHandlers());
43
+ afterEach(() => resetStore());
44
+ afterAll(() => server.close());
45
+
46
+ describe('SignInForm Component', () => {
47
+ it('has an Email input field', () => {
48
+ const { getByLabelText } = render(withHomeflowState(SignInForm));
49
+
50
+ expect(getByLabelText('Email')).toBeInTheDocument();
51
+ });
52
+
53
+ it('has a Password input field', () => {
54
+ const { getByLabelText } = render(withHomeflowState(SignInForm));
55
+
56
+ expect(getByLabelText('Password')).toBeInTheDocument();
57
+ });
58
+
59
+ it('reports successful sign in', async () => {
60
+ const { findByText, findAllByText, getByText, getAllByText } = render(withHomeflowState(UserProfileModal));
61
+
62
+ await findByText('Register or Sign in');
63
+ userEvent.click(getByText('Register or Sign in'));
64
+ await findAllByText('Sign In');
65
+ userEvent.click(getAllByText('Sign In')[1]);
66
+
67
+ expect(await findByText('Welcome, Test.')).toBeInTheDocument();
68
+ });
69
+
70
+ it('reports failed sign in', async () => {
71
+ const { findByText, getByText } = render(withHomeflowState(SignInForm));
72
+
73
+ server.use(
74
+ rest.post('/session', (req, res, ctx) => res(ctx.status(401))),
75
+ );
76
+ jest.spyOn(console, 'error').mockImplementation(() => {});
77
+
78
+ userEvent.click(getByText('Sign In'));
79
+
80
+ expect(await findByText('Sign in failed, please try again')).toBeInTheDocument();
81
+ });
82
+
83
+ it('reports succesful sign out', async () => {
84
+ const { findByText, findAllByText, getByText, getAllByText } = render(withHomeflowState(UserProfileModal));
85
+
86
+ await findByText('Register or Sign in');
87
+ userEvent.click(getByText('Register or Sign in'));
88
+ await findAllByText('Sign In');
89
+ userEvent.click(getAllByText('Sign In')[1]);
90
+
91
+ await findByText('Welcome, Test.');
92
+ await findByText('Sign Out');
93
+
94
+ userEvent.click(getByText('Sign Out'));
95
+
96
+ expect(await findByText('You have been signed out')).toBeInTheDocument();
97
+ });
98
+
99
+ it('clears sign-in credentials on sign-out', async () => {
100
+ const {
101
+ findByText, findAllByText, getAllByLabelText, getByText, getAllByText
102
+ } = render(withHomeflowState(UserProfileModal));
103
+
104
+ await findByText('Register or Sign in');
105
+ userEvent.click(getByText('Register or Sign in'));
106
+ await findAllByText('Sign In');
107
+ userEvent.type(getAllByLabelText('Email')[0], 'user@example.com');
108
+ userEvent.type(getAllByLabelText('Password')[0], 'password');
109
+ userEvent.click(getAllByText('Sign In')[1]);
110
+
111
+ await findByText('Welcome, Test.');
112
+ await findByText('Sign Out');
113
+
114
+ userEvent.click(getByText('Sign Out'));
115
+
116
+ await findByText('You have been signed out');
117
+ await findAllByText('Password');
118
+
119
+ expect(getAllByLabelText('Email')[0]).toHaveValue('');
120
+ expect(getAllByLabelText('Password')[0]).toHaveValue('');
121
+ });
122
+
123
+ it('populates new user data in profile edit page afte user registration', async () => {
124
+ const {
125
+ findByText, findAllByText, findByRole, getByLabelText, getAllByLabelText, getByRole, getByText, getAllByText
126
+ } = render(withHomeflowState(UserProfileModal));
127
+
128
+ await findByText('Register or Sign in');
129
+ userEvent.click(getByText('Register or Sign in'));
130
+ await findByText('Welcome, Guest.');
131
+ userEvent.type(getByLabelText('First name'), user.first_name);
132
+ userEvent.type(getByLabelText('Last name'), user.last_name);
133
+ userEvent.type(getAllByLabelText('Email')[0], user.email);
134
+ userEvent.type(getByLabelText('Phone'), user.pohne);
135
+ userEvent.type(getAllByLabelText('Password')[0], 'password');
136
+ userEvent.type(getByLabelText('Confirm password'), 'password');
137
+ userEvent.click(getByRole('button', { name: 'Register' }));
138
+
139
+ await findByText('Welcome, Test.');
140
+ userEvent.click(getByText('Edit your profile'));
141
+
142
+ await findByRole('heading', { name: 'Edit Your Profile' });
143
+
144
+ expect(getByLabelText('First name')).toHaveValue(user.first_name);
145
+ expect(getByLabelText('Last name')).toHaveValue(user.last_name);
146
+ expect(getByLabelText('Email')).toHaveValue(user.email);
147
+ expect(getByLabelText('Phone')).toHaveValue(user.tel_home);
148
+ });
149
+ });
package/user/index.js CHANGED
@@ -6,12 +6,14 @@ import SignInInput from './user-sign-in-form/sign-in-input.component';
6
6
  import SignOutButton from './sign-out-button/sign-out-button.component';
7
7
  import MarketingPreferencesForm from './marketing-preferences-form/marketing-preferences-form.component';
8
8
  import ForgottenPasswordForm from './forgotten-password-form/forgotten-password-form.component';
9
+ import UserEditForm from './user-edit-form/user-edit-form.component';
9
10
 
10
11
  export {
11
12
  withUser,
12
13
  UserRegisterForm,
13
14
  UserInput,
14
15
  UserSignInForm,
16
+ UserEditForm,
15
17
  SignInInput,
16
18
  SignOutButton,
17
19
  MarketingPreferencesForm,
@@ -11,7 +11,7 @@ const SignOutButton = ({ signOutUser, children, ...otherProps }) => (
11
11
  onClick={(e) => {
12
12
  e.preventDefault();
13
13
  signOutUser()
14
- .then(() => notify('You have been signed out.', 'success'));
14
+ .then(() => notify('You have been signed out', 'success'));
15
15
  }}
16
16
  {...otherProps}
17
17
  >
@@ -14,6 +14,7 @@ const UserInput = ({ name, value, setLocalUser, ...otherProps }) => {
14
14
 
15
15
  return (
16
16
  <input
17
+ id={`user-input-${name}`}
17
18
  type={type}
18
19
  name={name}
19
20
  value={value}
@@ -7,6 +7,7 @@ const SignInInput = ({ name, value, setUserCredentials, ...otherProps }) => (
7
7
  <input
8
8
  name={name}
9
9
  value={value}
10
+ id={`sign-in-input-${name}`}
10
11
  type={name === 'password' ? 'password' : 'text'}
11
12
  onChange={e => setUserCredentials({ [name]: e.target.value })}
12
13
  {...otherProps}
@@ -11,9 +11,13 @@ const UserSignInForm = ({ children, signInUser, userCredentials, setLoading, ...
11
11
  setLoading({ userSignIn: true });
12
12
 
13
13
  signInUser(userCredentials)
14
- .then(() => {
14
+ .then(({ success }) => {
15
15
  setLoading({ userSignIn: false });
16
- notify('You have successfully signed in', 'success');
16
+ if (success) {
17
+ notify('You have successfully signed in', 'success');
18
+ } else {
19
+ notify('Sign in failed, please try again', 'error');
20
+ }
17
21
  });
18
22
  };
19
23
 
package/utils/index.js CHANGED
@@ -79,3 +79,7 @@ export const objectDiff = (oldObject, newObject) => {
79
79
 
80
80
  return changed;
81
81
  }
82
+
83
+ export const compact = (array) => array.filter(item => typeof item !== 'undefined' && item !== null);
84
+
85
+ export const sanitizeText = (string) => new Option(string).innerHTML;
@@ -0,0 +1,21 @@
1
+ import * as utils from './index';
2
+
3
+ describe('compact', () => {
4
+ it('removes null elements from array', () => {
5
+ expect(utils.compact([1, null, 2, 3, null, 4, null, 5])).toEqual([1, 2, 3, 4, 5]);
6
+ });
7
+
8
+ it('removes undefined elements from array', () => {
9
+ expect(utils.compact([1, undefined, 2, 3, undefined, 4, undefined, 5])).toEqual([1, 2, 3, 4, 5]);
10
+ });
11
+ });
12
+
13
+ describe('sanitizeText', () => {
14
+ it('converts < and > to &lt; and &gt;', () => {
15
+ expect(utils.sanitizeText('<hello>')).toEqual('&lt;hello&gt;');
16
+ });
17
+
18
+ it('converts & to &amp;', () => {
19
+ expect(utils.sanitizeText('<hello>')).toEqual('&lt;hello&gt;');
20
+ });
21
+ });