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.
- package/Jenkinsfile +41 -0
- package/__tests__/instant-valuation.test.jsx +1 -1
- package/actions/properties.actions.js +5 -0
- package/actions/properties.actions.test.js +13 -0
- package/actions/properties.types.js +1 -0
- package/actions/user.actions.js +9 -15
- package/actions/user.actions.test.js +1 -0
- package/app/notify.js +6 -7
- package/app/with-homeflow-state.jsx +4 -0
- package/bin/peer-dependencies +12 -0
- package/bin/test +17 -0
- package/branches/branches-search-form/branches-search-input.component.jsx +40 -3
- package/package.json +7 -2
- package/reducers/index.js +9 -1
- package/reducers/properties.reducer.js +8 -2
- package/reducers/properties.reducer.test.js +16 -0
- package/reducers/user.reducer.js +1 -1
- package/search/location-input/location-input.component.jsx +8 -1
- package/search/location-input/location-input.test.js +1 -1
- package/search/radius-select/radius-select.test.js +1 -0
- package/search/search-form/search-form.component.jsx +1 -0
- package/store.js +3 -1
- package/user/default-profile/account/account-edit.component.jsx +9 -9
- package/user/default-profile/account/register-form.component.jsx +7 -6
- package/user/default-profile/account/sign-in-form.component.jsx +2 -2
- package/user/default-profile/account/sign-in-form.component.test.jsx +149 -0
- package/user/index.js +2 -0
- package/user/sign-out-button/sign-out-button.component.jsx +1 -1
- package/user/user-input/user-input.component.jsx +1 -0
- package/user/user-sign-in-form/sign-in-input.component.jsx +1 -0
- package/user/user-sign-in-form/user-sign-in-form.component.jsx +6 -2
- package/utils/index.js +4 -0
- 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.
|
|
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
|
+
});
|
package/actions/user.actions.js
CHANGED
|
@@ -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((
|
|
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
|
})
|
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
|
-
|
|
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
|
-
|
|
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 = ({
|
|
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 }) =>
|
|
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.
|
|
3
|
+
"version": "0.7.19",
|
|
4
4
|
"description": "JavaScript toolkit for Homeflow themes",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
});
|
package/reducers/user.reducer.js
CHANGED
|
@@ -71,7 +71,14 @@ class LocationInput extends Component {
|
|
|
71
71
|
|
|
72
72
|
setSearchField({ placeId: suggestion.place, isQuerySearch: false });
|
|
73
73
|
|
|
74
|
-
if (searchOnSelection)
|
|
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
|
}
|
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
|
|
14
|
+
.then(() => notify('You have been signed out', 'success'));
|
|
15
15
|
}}
|
|
16
16
|
{...otherProps}
|
|
17
17
|
>
|
|
@@ -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
|
-
|
|
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
|
@@ -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 < and >', () => {
|
|
15
|
+
expect(utils.sanitizeText('<hello>')).toEqual('<hello>');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('converts & to &', () => {
|
|
19
|
+
expect(utils.sanitizeText('<hello>')).toEqual('<hello>');
|
|
20
|
+
});
|
|
21
|
+
});
|