diff --git a/frontend/src/components/Dashboard/Post.vue b/frontend/src/components/Dashboard/Post.vue
index 852719b..5ad6a52 100644
--- a/frontend/src/components/Dashboard/Post.vue
+++ b/frontend/src/components/Dashboard/Post.vue
@@ -9,14 +9,7 @@
>
-
@@ -198,12 +186,14 @@ import {
} from 'firebase/auth';
import Email from './misc/icons/Email.vue';
import Password from './misc/icons/Password.vue';
+import Loading from './misc/Loading.vue';
import { mapGetters } from 'vuex';
export default {
components: {
Email,
Password,
+ Loading,
},
data() {
return {
diff --git a/frontend/src/components/ResetPassword.spec.js b/frontend/src/components/ResetPassword.spec.js
new file mode 100644
index 0000000..e273594
--- /dev/null
+++ b/frontend/src/components/ResetPassword.spec.js
@@ -0,0 +1,46 @@
+import { mount } from '@vue/test-utils';
+import ResetPassword from './ResetPassword.vue';
+
+const mountComponent = () =>
+ mount(ResetPassword, {
+ global: {
+ stubs: ['router-link'],
+ },
+ });
+
+describe('ResetPassword', () => {
+ describe('Email field', () => {
+ describe('when entering "test@test.com"', () => {
+ it('sets the input value to "test@test.com', async () => {
+ const wrapper = mountComponent();
+
+ const input = wrapper.find('input[type="email"]');
+
+ await input.setValue('test@test.com');
+
+ expect(input.element.value).toBe('test@test.com');
+ });
+ });
+ });
+ describe('Submit button', () => {
+ it('is disabled by default', () => {
+ const wrapper = mountComponent();
+
+ const button = wrapper.find('button[type="submit"]');
+
+ expect(button.element.disabled).toBe(true);
+ });
+ describe('when entering a valid email and a password', () => {
+ it('is enabled', async () => {
+ const wrapper = mountComponent();
+
+ const input = wrapper.find('input[type="email"]');
+
+ await input.setValue('test@test.com');
+
+ const button = wrapper.find('button');
+ expect(button.element.disabled).toBe(false);
+ });
+ });
+ });
+});
diff --git a/frontend/src/components/ResetPassword.vue b/frontend/src/components/ResetPassword.vue
new file mode 100644
index 0000000..73470cf
--- /dev/null
+++ b/frontend/src/components/ResetPassword.vue
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+ {{ appName }}
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/misc/Loading.vue b/frontend/src/components/misc/Loading.vue
new file mode 100644
index 0000000..2f05d80
--- /dev/null
+++ b/frontend/src/components/misc/Loading.vue
@@ -0,0 +1,86 @@
+
+
+
+
+
diff --git a/frontend/src/index.css b/frontend/src/index.css
index 97ec612..bd6213e 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -1,75 +1,3 @@
@tailwind base;
@tailwind components;
-@tailwind utilities;
-
-.lds-ring {
- display: inline-block;
- position: relative;
- width: 80px;
- height: 80px;
-}
-.lds-ring div {
- box-sizing: border-box;
- display: block;
- position: absolute;
- width: 64px;
- height: 64px;
- margin: 8px;
- border: 8px solid #fff;
- border-radius: 50%;
- animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
- border-color: #fff transparent transparent transparent;
-}
-.lds-ring div:nth-child(1) {
- animation-delay: -0.45s;
-}
-.lds-ring div:nth-child(2) {
- animation-delay: -0.3s;
-}
-.lds-ring div:nth-child(3) {
- animation-delay: -0.15s;
-}
-@keyframes lds-ring {
- 0% {
- transform: rotate(0deg);
- }
- 100% {
- transform: rotate(360deg);
- }
-}
-
-.lds-ring-small {
- display: inline-block;
- position: relative;
- width: 10px;
- height: 10px;
-}
-.lds-ring-small div {
- box-sizing: border-box;
- display: block;
- position: absolute;
- width: 8px;
- height: 8px;
- margin: 1px;
- border: 1px solid #fff;
- border-radius: 50%;
- animation: lds-ring-small 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
- border-color: #fff transparent transparent transparent;
-}
-.lds-ring-small div:nth-child(1) {
- animation-delay: -0.45s;
-}
-.lds-ring-small div:nth-child(2) {
- animation-delay: -0.3s;
-}
-.lds-ring-small div:nth-child(3) {
- animation-delay: -0.15s;
-}
-@keyframes lds-ring-small {
- 0% {
- transform: rotate(0deg);
- }
- 100% {
- transform: rotate(360deg);
- }
-}
\ No newline at end of file
+@tailwind utilities;
\ No newline at end of file
diff --git a/frontend/src/routes/index.js b/frontend/src/routes/index.js
index 35b44fb..8952ce2 100644
--- a/frontend/src/routes/index.js
+++ b/frontend/src/routes/index.js
@@ -5,6 +5,7 @@ import Dashboard from '../components/Dashboard';
import Terms from '../components/misc/Terms';
import PrivacyPolicy from '../components/misc/PrivacyPolicy';
import About from '../components/misc/About';
+import ResetPassword from '../components/ResetPassword';
const router = createRouter({
mode: 'history',
@@ -16,6 +17,11 @@ const router = createRouter({
name: 'Login',
component: Login,
},
+ {
+ path: '/reset-password',
+ name: 'ResetPassword',
+ component: ResetPassword,
+ },
{
path: '/register',
name: 'Register',
diff --git a/frontend/src/store/actions/index.js b/frontend/src/store/actions/index.js
index 288dd66..1b256c3 100644
--- a/frontend/src/store/actions/index.js
+++ b/frontend/src/store/actions/index.js
@@ -89,4 +89,7 @@ export default {
commit('SET_IS_CHECKING_NAME', false);
}
},
+ setTheme({ commit }, theme) {
+ commit('SET_THEME', theme);
+ },
};
diff --git a/frontend/src/store/actions/index.spec.js b/frontend/src/store/actions/index.spec.js
index 488d854..ce919fc 100644
--- a/frontend/src/store/actions/index.spec.js
+++ b/frontend/src/store/actions/index.spec.js
@@ -1,5 +1,5 @@
import actions from '.';
-import { apiRequest } from './api';
+import { apiRequest, unAuthApiRequest } from './api';
jest.mock('./api');
// helper for testing action with expected mutations
@@ -122,8 +122,14 @@ describe('actions', () => {
});
});
describe('postMessage', () => {
+ const date = new Date(2020, 3, 1);
afterEach(() => {
jest.resetAllMocks();
+ jest.useRealTimers();
+ });
+ beforeEach(() => {
+ jest.useFakeTimers('modern');
+ jest.setSystemTime(date);
});
it('posts the message to the API', (done) => {
apiRequest.mockResolvedValueOnce({ data: POSTS_RESPONSE_FIXTURE[0] });
@@ -157,5 +163,136 @@ describe('actions', () => {
},
);
});
+ it('can post a comment to a message', (done) => {
+ apiRequest.mockResolvedValueOnce({
+ data: { ...POSTS_RESPONSE_FIXTURE[0], parentId: '1' },
+ });
+
+ testAction(
+ actions.postMessage,
+ 'Hello World 2',
+ {
+ posts: [{ id: '1', message: 'hello world' }],
+ creatingPost: false,
+ parentId: '1',
+ },
+ [
+ {
+ type: 'IS_CREATING_POST',
+ payload: true,
+ },
+ {
+ type: 'PUSH_COMMENT',
+ payload: {
+ message: 'Hello World 2',
+ date: date.getTime(),
+ parentId: '1',
+ },
+ },
+ { type: 'POP_COMMENT' },
+ {
+ type: 'PUSH_COMMENT',
+ payload: {
+ message: 'Hello World 2',
+ date: 1555555555555,
+ parentId: '1',
+ },
+ },
+ { type: 'INCREMENT_COMMENTS_COUNT' },
+ {
+ type: 'IS_CREATING_POST',
+ payload: false,
+ },
+ ],
+ () => {
+ expect(apiRequest).toHaveBeenCalledWith('POST', '/posts', null, {
+ message: 'Hello World 2',
+ parentId: '1',
+ });
+ done();
+ },
+ );
+ });
+ });
+ describe('toggleLike', () => {
+ beforeEach(() => {
+ apiRequest.mockResolvedValueOnce({
+ data: { ...POSTS_RESPONSE_FIXTURE[0] },
+ });
+ });
+ it('toggles the like on the message', (done) => {
+ testAction(
+ actions.toggleLike,
+ {
+ post: { id: '1', likes: 0, message: 'Hello World' },
+ like: true,
+ },
+ { posts: [{ id: '1', message: 'Hello World', likes: 0 }] },
+ [
+ {
+ type: 'SET_LIKING_POST',
+ payload: '1',
+ },
+ {
+ type: 'SET_POST_BY_ID',
+ payload: {
+ id: '1',
+ message: 'Hello World',
+ likes: 1,
+ likedByMe: true,
+ },
+ },
+ {
+ type: 'SET_POST_BY_ID',
+ payload: POSTS_RESPONSE_FIXTURE[0],
+ },
+ {
+ type: 'SET_LIKING_POST',
+ payload: null,
+ },
+ ],
+ () => {
+ expect(apiRequest).toHaveBeenCalledWith('PUT', '/posts/1', null, {
+ like: true,
+ });
+ done();
+ },
+ );
+ });
+ });
+ describe('checkExists', () => {
+ beforeEach(() => {
+ unAuthApiRequest.mockResolvedValueOnce({ data: { exists: true } });
+ });
+ it('checks if the username exists', (done) => {
+ const payload = { name: 'test' };
+ testAction(
+ actions.checkExists,
+ payload,
+ {},
+ [
+ {
+ type: 'SET_IS_CHECKING_NAME',
+ payload: true,
+ },
+ {
+ type: 'SET_USER_NAME_EXISTS',
+ payload: true,
+ },
+ {
+ type: 'SET_IS_CHECKING_NAME',
+ payload: false,
+ },
+ ],
+ () => {
+ expect(unAuthApiRequest).toHaveBeenCalledWith(
+ 'POST',
+ '/users/exists',
+ payload,
+ );
+ done();
+ },
+ );
+ });
});
});
diff --git a/frontend/src/store/getters.spec.js b/frontend/src/store/getters.spec.js
index d31ff0f..9879fbd 100644
--- a/frontend/src/store/getters.spec.js
+++ b/frontend/src/store/getters.spec.js
@@ -52,4 +52,23 @@ describe('getters', () => {
expect(result).toEqual(state.posts);
});
});
+ describe('getPostById', () => {
+ it('should return the post by id', () => {
+ const state = {
+ posts: [
+ {
+ id: '1',
+ message: 'Hello World',
+ },
+ {
+ id: '2',
+ message: 'Hello World 2',
+ },
+ ],
+ };
+ const result = getters.getPostById(state, '2');
+
+ expect(result).toEqual(state.posts[1]);
+ });
+ });
});
diff --git a/frontend/src/store/index.js b/frontend/src/store/index.js
index 0cc3679..602e9ad 100644
--- a/frontend/src/store/index.js
+++ b/frontend/src/store/index.js
@@ -21,6 +21,7 @@ const store = createStore({
isCheckingEmail: false,
isCheckingName: false,
parentId: null,
+ theme: null,
},
getters,
mutations,
diff --git a/frontend/src/store/mutations.js b/frontend/src/store/mutations.js
index aa8e713..46be6a5 100644
--- a/frontend/src/store/mutations.js
+++ b/frontend/src/store/mutations.js
@@ -75,4 +75,7 @@ export default {
SET_IS_CHECKING_NAME(state, data) {
state.isCheckingName = data;
},
+ SET_THEME(state, data) {
+ state.theme = data;
+ },
};
diff --git a/frontend/src/store/mutations.spec.js b/frontend/src/store/mutations.spec.js
index 7f7e8f6..87bcb19 100644
--- a/frontend/src/store/mutations.spec.js
+++ b/frontend/src/store/mutations.spec.js
@@ -1,8 +1,17 @@
import mutations from './mutations';
// destructure assign `mutations`
-const { SET_USER, SET_POSTS, PUSH_MESSAGE, PUSH_COMMENT, SET_MESSAGE } =
- mutations;
+const {
+ INCREMENT_COMMENTS_COUNT,
+ SET_USER,
+ SET_POST_BY_ID,
+ SET_POSTS,
+ POP_COMMENT,
+ POP_MESSAGE,
+ PUSH_MESSAGE,
+ PUSH_COMMENT,
+ SET_MESSAGE,
+} = mutations;
describe('mutations', () => {
describe('SET_USER', () => {
@@ -16,6 +25,17 @@ describe('mutations', () => {
expect(state.user).toEqual(userFixture);
});
});
+ describe('SET_POST_BY_ID', () => {
+ it('can set the post by id', () => {
+ const postFixture = { id: '123', message: 'hello2' };
+ // mock state
+ const state = { posts: [{ id: '123', message: 'hello1' }] };
+ // apply mutation
+ SET_POST_BY_ID(state, postFixture);
+ // assert result
+ expect(state.posts).toEqual([postFixture]);
+ });
+ });
describe('SET_POSTS', () => {
it('can set the posts', () => {
const postsFixture = [
@@ -34,16 +54,25 @@ describe('mutations', () => {
});
describe('PUSH_MESSAGE', () => {
it('can push a message to the posts', () => {
+ const posts = [
+ {
+ id: '2',
+ message: 'Hello World',
+ },
+ ];
const messageFixture = {
id: '1',
- message: 'Hello World',
+ message: 'Hello World 2',
};
// mock state
- const state = { posts: [], user: { displayName: 'Test' } };
+ const state = { posts, user: { displayName: 'Test' } };
// apply mutation
PUSH_MESSAGE(state, messageFixture);
// assert result
- expect(state.posts).toEqual([{ ...messageFixture, userName: 'Test' }]);
+ expect(state.posts).toEqual([
+ { ...messageFixture, userName: 'Test' },
+ ...posts,
+ ]);
});
});
describe('PUSH_COMMENT', () => {
@@ -67,6 +96,70 @@ describe('mutations', () => {
]);
});
});
+ describe('POP_MESSAGE', () => {
+ it('can pop a message from the posts', () => {
+ const posts = [
+ {
+ id: '2',
+ message: 'Hello World',
+ },
+ {
+ id: '1',
+ message: 'Hello World 2',
+ },
+ ];
+ // mock state
+ const state = { posts };
+ // apply mutation
+ POP_MESSAGE(state);
+ // assert result
+ expect(state.posts).toEqual([posts[1]]);
+ });
+ });
+ describe('POP_COMMENT', () => {
+ it('can pop a comment from the posts', () => {
+ const posts = [
+ {
+ id: '2',
+ message: 'Hello World',
+ },
+ {
+ id: '1',
+ message: 'Hello World 2',
+ parentId: '2',
+ },
+ ];
+ // mock state
+ const state = { posts };
+ // apply mutation
+ POP_COMMENT(state);
+ // assert result
+ expect(state.posts).toEqual([posts[0]]);
+ });
+ });
+ describe('INCREMENT_COMMENTS_COUNT', () => {
+ it('can increment the comments count', () => {
+ const posts = [
+ {
+ id: '2',
+ message: 'Hello World',
+ comments: 0,
+ },
+ {
+ id: '1',
+ message: 'Hello World 2',
+ parentId: '2',
+ },
+ ];
+ // mock state
+ const state = { posts, parentId: '2' };
+ // apply mutation
+ INCREMENT_COMMENTS_COUNT(state);
+ // assert result
+ expect(state.posts).toEqual(posts);
+ expect(state.posts[0].comments).toEqual(1);
+ });
+ });
describe('SET_MESSAGE', () => {
it('can set the message', () => {
const messageFixture = {