0.2.3 #99

Merged
sonny merged 112 commits from development into master 2020-05-23 16:58:42 +02:00
15 changed files with 161 additions and 161 deletions
Showing only changes of commit f9bce3507c - Show all commits

View file

@ -28,49 +28,6 @@ export const receiveCategories = categories => ({
export const requestCategory = () => ({ type: REQUEST_CATEGORY }); export const requestCategory = () => ({ type: REQUEST_CATEGORY });
export const requestCategories = () => ({ type: REQUEST_CATEGORIES }); export const requestCategories = () => ({ type: REQUEST_CATEGORIES });
export const fetchCategories = () => {
return dispatch => {
dispatch(requestCategories());
return fetch('/api/categories/')
.then(response => response.json())
.then(json => {
const categories = {};
json.forEach(category => {
categories[category.id] = { ...category };
});
dispatch(receiveCategories(categories));
return json;
})
.then(json => {
const promises = json.map(category => {
return fetch(`/api/categories/${category.id}/rules/`);
});
dispatch(requestRules());
return Promise.all(promises);
})
.then(responses => {
return Promise.all(responses.map(response => response.json()));
})
.then(responseData => {
let rules = {};
responseData.forEach(json => {
const data = Object.values(json);
data.forEach(item => {
rules = { ...rules, [item.id]: item };
});
});
dispatch(receiveRules(rules));
});
};
};
export const fetchCategory = category => { export const fetchCategory = category => {
return (dispatch, getState) => { return (dispatch, getState) => {
const { selected } = getState(); const { selected } = getState();
@ -93,3 +50,28 @@ export const fetchCategory = category => {
}); });
}; };
}; };
export const fetchCategories = () => {
return dispatch => {
dispatch(requestCategories());
return fetch('/api/categories/')
.then(response => response.json())
.then(categories => {
dispatch(receiveCategories(categories));
return categories;
})
.then(categories => {
dispatch(requestRules());
const promises = categories.map(category => {
return fetch(`/api/categories/${category.id}/rules/`);
});
return Promise.all(promises);
})
.then(responses => Promise.all(responses.map(response => response.json())))
.then(nestedRules => dispatch(receiveRules(nestedRules.flat())));
};
};

View file

@ -75,15 +75,7 @@ export const fetchPostsBySection = (section, page = false) => {
return fetch(url) return fetch(url)
.then(response => response.json()) .then(response => response.json())
.then(json => { .then(posts => dispatch(receivePosts(posts.results, posts.next)))
const posts = {};
json.results.forEach(post => {
posts[post.id] = post;
});
dispatch(receivePosts(posts, json.next));
})
.catch(error => { .catch(error => {
if (error instanceof TypeError) { if (error instanceof TypeError) {
console.log(`Unable to parse posts from request: ${error}`); console.log(`Unable to parse posts from request: ${error}`);

View file

@ -61,14 +61,6 @@ export const fetchRulesByCategory = category => {
return fetch(`/api/categories/${category.id}/rules/`) return fetch(`/api/categories/${category.id}/rules/`)
.then(response => response.json()) .then(response => response.json())
.then(responseData => { .then(rules => dispatch(receiveRules(rules)));
const rules = {};
responseData.forEach(rule => {
rules[rule.id] = { ...rule };
});
dispatch(receiveRules(rules));
});
}; };
}; };

View file

@ -2,6 +2,8 @@ import { isEqual } from 'lodash';
import { CATEGORY_TYPE, RULE_TYPE } from '../constants.js'; import { CATEGORY_TYPE, RULE_TYPE } from '../constants.js';
import { objectsFromArray } from '../../../utils.js';
import { import {
RECEIVE_CATEGORY, RECEIVE_CATEGORY,
RECEIVE_CATEGORIES, RECEIVE_CATEGORIES,
@ -26,13 +28,7 @@ export const categories = (state = { ...defaultState }, action) => {
isFetching: false, isFetching: false,
}; };
case RECEIVE_CATEGORIES: case RECEIVE_CATEGORIES:
const receivedCategories = {}; const receivedCategories = objectsFromArray(action.categories, 'id');
Object.values({ ...action.categories }).forEach(category => {
receivedCategories[category.id] = {
...category,
};
});
return { return {
...state, ...state,
@ -41,10 +37,7 @@ export const categories = (state = { ...defaultState }, action) => {
}; };
case REQUEST_CATEGORIES: case REQUEST_CATEGORIES:
case REQUEST_CATEGORY: case REQUEST_CATEGORY:
return { return { ...state, isFetching: true };
...state,
isFetching: true,
};
case MARK_POST_READ: case MARK_POST_READ:
let category = {}; let category = {};

View file

@ -1,4 +1,6 @@
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { objectsFromArray } from '../../../utils.js';
import { CATEGORY_TYPE, RULE_TYPE } from '../constants.js'; import { CATEGORY_TYPE, RULE_TYPE } from '../constants.js';
import { import {
@ -15,12 +17,6 @@ const defaultState = { items: {}, isFetching: false };
export const posts = (state = { ...defaultState }, action) => { export const posts = (state = { ...defaultState }, action) => {
switch (action.type) { switch (action.type) {
case RECEIVE_POSTS:
return {
...state,
isFetching: false,
items: { ...state.items, ...action.posts },
};
case REQUEST_POSTS: case REQUEST_POSTS:
return { ...state, isFetching: true }; return { ...state, isFetching: true };
case RECEIVE_POST: case RECEIVE_POST:
@ -28,6 +24,14 @@ export const posts = (state = { ...defaultState }, action) => {
...state, ...state,
items: { ...state.items, [action.post.id]: { ...action.post } }, items: { ...state.items, [action.post.id]: { ...action.post } },
}; };
case RECEIVE_POSTS:
const receivedItems = objectsFromArray(action.posts, 'id');
return {
...state,
isFetching: false,
items: { ...state.items, ...receivedItems },
};
case MARK_SECTION_READ: case MARK_SECTION_READ:
const updatedPosts = {}; const updatedPosts = {};
let relatedPosts = []; let relatedPosts = [];

View file

@ -1,5 +1,7 @@
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { objectsFromArray } from '../../../utils.js';
import { CATEGORY_TYPE, RULE_TYPE } from '../constants.js'; import { CATEGORY_TYPE, RULE_TYPE } from '../constants.js';
import { import {
@ -17,14 +19,13 @@ export const rules = (state = { ...defaultState }, action) => {
switch (action.type) { switch (action.type) {
case REQUEST_RULE: case REQUEST_RULE:
case REQUEST_RULES: case REQUEST_RULES:
return { return { ...state, isFetching: true };
...state,
isFetching: true,
};
case RECEIVE_RULES: case RECEIVE_RULES:
const receivedItems = objectsFromArray(action.rules, 'id');
return { return {
...state, ...state,
items: { ...state.items, ...action.rules }, items: { ...state.items, ...receivedItems },
isFetching: false, isFetching: false,
}; };
case RECEIVE_RULE: case RECEIVE_RULE:

View file

@ -67,15 +67,9 @@ export const selected = (state = { ...defaultState }, action) => {
...state, ...state,
}; };
case SELECT_POST: case SELECT_POST:
return { return { ...state, post: action.post };
...state,
post: action.post,
};
case UNSELECT_POST: case UNSELECT_POST:
return { return { ...state, post: {} };
...state,
post: {},
};
case MARK_POST_READ: case MARK_POST_READ:
return { return {
...state, ...state,

View file

@ -96,22 +96,13 @@ describe('category actions', () => {
}); });
it('should create multiple actions when fetching categories', () => { it('should create multiple actions when fetching categories', () => {
const categories = { const categories = [
1: { id: 1, name: 'Tech', unread: 29 }, { id: 1, name: 'Tech', unread: 29 },
2: { id: 2, name: 'World news', unread: 956 }, { id: 2, name: 'World news', unread: 956 },
}; ];
const rules = { const rules = [
4: { {
id: 4,
name: 'BBC',
url: 'http://feeds.bbci.co.uk/news/world/rss.xml',
favicon:
'https://m.files.bbci.co.uk/modules/bbc-morph-news-waf-page-meta/2.5.2/apple-touch-icon-57x57-precomposed.png',
category: 2,
unread: 345,
},
5: {
id: 5, id: 5,
name: 'Ars Technica', name: 'Ars Technica',
url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml',
@ -119,19 +110,28 @@ describe('category actions', () => {
category: 1, category: 1,
unread: 7, unread: 7,
}, },
}; {
id: 6,
name: 'BBC',
url: 'http://feeds.bbci.co.uk/news/world/rss.xml',
favicon:
'https://m.files.bbci.co.uk/modules/bbc-morph-news-waf-page-meta/2.5.2/apple-touch-icon-57x57-precomposed.png',
category: 2,
unread: 345,
},
];
fetchMock fetchMock
.get('/api/categories/', { .get('/api/categories/', {
body: Object.values({ ...categories }), body: categories,
headers: { 'content-type': 'application/json' }, headers: { 'content-type': 'application/json' },
}) })
.get('/api/categories/1/rules/', { .get('/api/categories/1/rules/', {
body: [{ ...rules[5] }], body: [{ ...rules[0] }],
headers: { 'content-type': 'application/json' }, headers: { 'content-type': 'application/json' },
}) })
.get('/api/categories/2/rules/', { .get('/api/categories/2/rules/', {
body: [{ ...rules[4] }], body: [{ ...rules[1] }],
headers: { 'content-type': 'application/json' }, headers: { 'content-type': 'application/json' },
}); });
@ -161,8 +161,8 @@ describe('category actions', () => {
unread: 0, unread: 0,
}; };
const rules = { const rules = [
1: { {
id: 1, id: 1,
name: 'Ars Technica', name: 'Ars Technica',
url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml',
@ -170,7 +170,7 @@ describe('category actions', () => {
category: 1, category: 1,
unread: 200, unread: 200,
}, },
2: { {
id: 2, id: 2,
name: 'Hacker News', name: 'Hacker News',
url: 'https://news.ycombinator.com/rss', url: 'https://news.ycombinator.com/rss',
@ -178,7 +178,7 @@ describe('category actions', () => {
category: 1, category: 1,
unread: 350, unread: 350,
}, },
}; ];
fetchMock fetchMock
.get('/api/categories/1', { .get('/api/categories/1', {
@ -186,7 +186,7 @@ describe('category actions', () => {
headers: { 'content-type': 'application/json' }, headers: { 'content-type': 'application/json' },
}) })
.get('/api/categories/1/rules/', { .get('/api/categories/1/rules/', {
body: Object.values({ ...rules }), body: rules,
headers: { 'content-type': 'application/json' }, headers: { 'content-type': 'application/json' },
}); });
@ -197,7 +197,7 @@ describe('category actions', () => {
category: { ...category, unread: 500 }, category: { ...category, unread: 500 },
}, },
{ type: ruleActions.REQUEST_RULES }, { type: ruleActions.REQUEST_RULES },
{ type: ruleActions.RECEIVE_RULES, rules: { ...rules } }, { type: ruleActions.RECEIVE_RULES, rules },
]; ];
const store = mockStore({ const store = mockStore({

View file

@ -163,8 +163,8 @@ describe('rule actions', () => {
}); });
it('should create multiple actions to fetch posts by rule', () => { it('should create multiple actions to fetch posts by rule', () => {
const posts = { const posts = [
2067: { {
id: 2067, id: 2067,
remoteIdentifier: 'https://arstechnica.com/?p=1648607', remoteIdentifier: 'https://arstechnica.com/?p=1648607',
title: title:
@ -177,7 +177,7 @@ describe('rule actions', () => {
rule: 4, rule: 4,
read: false, read: false,
}, },
2141: { {
id: 2141, id: 2141,
remoteIdentifier: 'https://arstechnica.com/?p=1648757', remoteIdentifier: 'https://arstechnica.com/?p=1648757',
title: 'The most complete brain map ever is here: A flys “connectome”', title: 'The most complete brain map ever is here: A flys “connectome”',
@ -189,7 +189,7 @@ describe('rule actions', () => {
rule: 4, rule: 4,
read: false, read: false,
}, },
}; ];
const rule = { const rule = {
id: 4, id: 4,
@ -206,7 +206,7 @@ describe('rule actions', () => {
count: 2, count: 2,
next: 'https://durp.com/api/rules/4/posts/?page=2&read=false', next: 'https://durp.com/api/rules/4/posts/?page=2&read=false',
previous: null, previous: null,
results: Object.values({ ...posts }), results: posts,
}, },
headers: { 'content-type': 'application/json' }, headers: { 'content-type': 'application/json' },
}); });
@ -233,8 +233,8 @@ describe('rule actions', () => {
}); });
it('should create multiple actions to fetch posts by category', () => { it('should create multiple actions to fetch posts by category', () => {
const posts = { const posts = [
2067: { {
id: 2067, id: 2067,
remoteIdentifier: 'https://arstechnica.com/?p=1648607', remoteIdentifier: 'https://arstechnica.com/?p=1648607',
title: title:
@ -247,7 +247,7 @@ describe('rule actions', () => {
rule: 4, rule: 4,
read: false, read: false,
}, },
2141: { {
id: 2141, id: 2141,
remoteIdentifier: 'https://arstechnica.com/?p=1648757', remoteIdentifier: 'https://arstechnica.com/?p=1648757',
title: 'The most complete brain map ever is here: A flys “connectome”', title: 'The most complete brain map ever is here: A flys “connectome”',
@ -259,7 +259,7 @@ describe('rule actions', () => {
rule: 4, rule: 4,
read: false, read: false,
}, },
}; ];
const category = { const category = {
id: 1, id: 1,
@ -273,7 +273,7 @@ describe('rule actions', () => {
count: 2, count: 2,
next: 'https://durp.com/api/categories/4/posts/?page=2&read=false', next: 'https://durp.com/api/categories/4/posts/?page=2&read=false',
previous: null, previous: null,
results: Object.values({ ...posts }), results: posts,
}, },
headers: { 'content-type': 'application/json' }, headers: { 'content-type': 'application/json' },
}); });

View file

@ -2,6 +2,8 @@ import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk'; import thunk from 'redux-thunk';
import fetchMock from 'fetch-mock'; import fetchMock from 'fetch-mock';
import { objectsFromArray } from '../../../utils.js';
import * as actions from '../../../pages/homepage/actions/rules.js'; import * as actions from '../../../pages/homepage/actions/rules.js';
import * as constants from '../../../pages/homepage/constants.js'; import * as constants from '../../../pages/homepage/constants.js';
import * as categoryActions from '../../../pages/homepage/actions/categories.js'; import * as categoryActions from '../../../pages/homepage/actions/categories.js';
@ -63,8 +65,8 @@ describe('rule actions', () => {
}); });
it('should create an action to receive multiple rules', () => { it('should create an action to receive multiple rules', () => {
const rules = { const rules = [
1: { {
id: 1, id: 1,
name: 'Test rule', name: 'Test rule',
unread: 100, unread: 100,
@ -72,7 +74,7 @@ describe('rule actions', () => {
url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml',
favicon: 'https://cdn.arstechnica.net/favicon.ico', favicon: 'https://cdn.arstechnica.net/favicon.ico',
}, },
2: { {
id: 2, id: 2,
name: 'Test rule 2', name: 'Test rule 2',
unread: 50, unread: 50,
@ -80,7 +82,7 @@ describe('rule actions', () => {
url: 'https://xkcd.com/atom.xml', url: 'https://xkcd.com/atom.xml',
favicon: null, favicon: null,
}, },
}; ];
const expectedAction = { const expectedAction = {
type: actions.RECEIVE_RULES, type: actions.RECEIVE_RULES,
@ -211,8 +213,8 @@ describe('rule actions', () => {
unread: 0, unread: 0,
}; };
const rules = { const rules = [
1: { {
id: 1, id: 1,
name: 'Ars Technica', name: 'Ars Technica',
url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml',
@ -220,7 +222,7 @@ describe('rule actions', () => {
category: 1, category: 1,
unread: 200, unread: 200,
}, },
2: { {
id: 2, id: 2,
name: 'Hacker News', name: 'Hacker News',
url: 'https://news.ycombinator.com/rss', url: 'https://news.ycombinator.com/rss',
@ -228,10 +230,10 @@ describe('rule actions', () => {
category: 1, category: 1,
unread: 350, unread: 350,
}, },
}; ];
fetchMock.getOnce('/api/categories/1/rules/', { fetchMock.getOnce('/api/categories/1/rules/', {
body: Object.values({ ...rules }), body: rules,
headers: { 'content-type': 'application/json' }, headers: { 'content-type': 'application/json' },
}); });

View file

@ -1,5 +1,7 @@
import { categories as reducer } from '../../../pages/homepage/reducers/categories.js'; import { categories as reducer } from '../../../pages/homepage/reducers/categories.js';
import { objectsFromArray } from '../../../utils.js';
import * as actions from '../../../pages/homepage/actions/categories.js'; import * as actions from '../../../pages/homepage/actions/categories.js';
import * as postActions from '../../../pages/homepage/actions/posts.js'; import * as postActions from '../../../pages/homepage/actions/posts.js';
import * as selectedActions from '../../../pages/homepage/actions/selected.js'; import * as selectedActions from '../../../pages/homepage/actions/selected.js';
@ -25,19 +27,15 @@ describe('category reducer', () => {
}); });
it('should return state after receiving multiple categories', () => { it('should return state after receiving multiple categories', () => {
const receivedCategories = { const receivedCategories = [
0: { id: 9, name: 'Tech', unread: 291 }, { id: 9, name: 'Tech', unread: 291 },
1: { id: 2, name: 'World news', unread: 444 }, { id: 2, name: 'World news', unread: 444 },
}; ];
const action = { type: actions.RECEIVE_CATEGORIES, categories: receivedCategories }; const action = { type: actions.RECEIVE_CATEGORIES, categories: receivedCategories };
const items = {}; const expectedCategories = objectsFromArray(receivedCategories, 'id');
const expectedState = { ...defaultState, items: expectedCategories };
Object.values({ ...receivedCategories }).forEach(category => {
items[category.id] = category;
});
const expectedState = { ...defaultState, items };
expect(reducer(undefined, action)).toEqual(expectedState); expect(reducer(undefined, action)).toEqual(expectedState);
}); });

View file

@ -1,5 +1,7 @@
import { posts as reducer } from '../../../pages/homepage/reducers/posts.js'; import { posts as reducer } from '../../../pages/homepage/reducers/posts.js';
import { objectsFromArray } from '../../../utils.js';
import * as actions from '../../../pages/homepage/actions/posts.js'; import * as actions from '../../../pages/homepage/actions/posts.js';
import * as selectedActions from '../../../pages/homepage/actions/selected.js'; import * as selectedActions from '../../../pages/homepage/actions/selected.js';
import * as constants from '../../../pages/homepage/constants.js'; import * as constants from '../../../pages/homepage/constants.js';
@ -45,8 +47,8 @@ describe('post actions', () => {
}); });
it('should return state after receiving posts', () => { it('should return state after receiving posts', () => {
const posts = { const posts = [
2067: { {
id: 2067, id: 2067,
remoteIdentifier: 'https://arstechnica.com/?p=1648607', remoteIdentifier: 'https://arstechnica.com/?p=1648607',
title: title:
@ -59,7 +61,7 @@ describe('post actions', () => {
rule: 4, rule: 4,
read: false, read: false,
}, },
2141: { {
id: 2141, id: 2141,
remoteIdentifier: 'https://arstechnica.com/?p=1648757', remoteIdentifier: 'https://arstechnica.com/?p=1648757',
title: 'The most complete brain map ever is here: A flys “connectome”', title: 'The most complete brain map ever is here: A flys “connectome”',
@ -71,7 +73,7 @@ describe('post actions', () => {
rule: 4, rule: 4,
read: false, read: false,
}, },
}; ];
const action = { const action = {
type: actions.RECEIVE_POSTS, type: actions.RECEIVE_POSTS,
@ -79,10 +81,11 @@ describe('post actions', () => {
posts, posts,
}; };
const expectedPosts = objectsFromArray(posts, 'id');
const expectedState = { const expectedState = {
...defaultState, ...defaultState,
isFetching: false, isFetching: false,
items: posts, items: expectedPosts,
}; };
expect(reducer(undefined, action)).toEqual(expectedState); expect(reducer(undefined, action)).toEqual(expectedState);

View file

@ -1,5 +1,7 @@
import { rules as reducer } from '../../../pages/homepage/reducers/rules.js'; import { rules as reducer } from '../../../pages/homepage/reducers/rules.js';
import { objectsFromArray } from '../../../utils.js';
import * as actions from '../../../pages/homepage/actions/rules.js'; import * as actions from '../../../pages/homepage/actions/rules.js';
import * as postActions from '../../../pages/homepage/actions/posts.js'; import * as postActions from '../../../pages/homepage/actions/posts.js';
import * as selectedActions from '../../../pages/homepage/actions/selected.js'; import * as selectedActions from '../../../pages/homepage/actions/selected.js';
@ -49,8 +51,8 @@ describe('category reducer', () => {
}); });
it('should return state after receiving multiple rules', () => { it('should return state after receiving multiple rules', () => {
const rules = { const rules = [
1: { {
id: 1, id: 1,
name: 'Test rule', name: 'Test rule',
unread: 100, unread: 100,
@ -58,7 +60,7 @@ describe('category reducer', () => {
url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml',
favicon: 'https://cdn.arstechnica.net/favicon.ico', favicon: 'https://cdn.arstechnica.net/favicon.ico',
}, },
2: { {
id: 2, id: 2,
name: 'Another Test rule', name: 'Another Test rule',
unread: 444, unread: 444,
@ -66,11 +68,12 @@ describe('category reducer', () => {
url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml', url: 'http://feeds.arstechnica.com/arstechnica/index?fmt=xml',
favicon: 'https://cdn.arstechnica.net/favicon.ico', favicon: 'https://cdn.arstechnica.net/favicon.ico',
}, },
}; ];
const action = { type: actions.RECEIVE_RULES, rules }; const action = { type: actions.RECEIVE_RULES, rules };
const expectedState = { ...defaultState, items: { ...rules } }; const mappedRules = objectsFromArray(rules, 'id');
const expectedState = { ...defaultState, items: { ...mappedRules } };
expect(reducer(undefined, action)).toEqual(expectedState); expect(reducer(undefined, action)).toEqual(expectedState);
}); });

View file

@ -211,8 +211,8 @@ describe('selected reducer', () => {
}); });
it('should return state after receiving posts', () => { it('should return state after receiving posts', () => {
const posts = { const posts = [
2067: { {
id: 2067, id: 2067,
remoteIdentifier: 'https://arstechnica.com/?p=1648607', remoteIdentifier: 'https://arstechnica.com/?p=1648607',
title: title:
@ -225,7 +225,7 @@ describe('selected reducer', () => {
rule: 4, rule: 4,
read: false, read: false,
}, },
2141: { {
id: 2141, id: 2141,
remoteIdentifier: 'https://arstechnica.com/?p=1648757', remoteIdentifier: 'https://arstechnica.com/?p=1648757',
title: 'The most complete brain map ever is here: A flys “connectome”', title: 'The most complete brain map ever is here: A flys “connectome”',
@ -237,7 +237,7 @@ describe('selected reducer', () => {
rule: 4, rule: 4,
read: false, read: false,
}, },
}; ];
const action = { const action = {
type: postActions.RECEIVE_POSTS, type: postActions.RECEIVE_POSTS,
@ -254,7 +254,7 @@ describe('selected reducer', () => {
expect(reducer(undefined, action)).toEqual(expectedState); expect(reducer(undefined, action)).toEqual(expectedState);
}); });
it('should return state after receiving a post', () => { it('should return state after receiving a post which is selected', () => {
const post = { const post = {
id: 2067, id: 2067,
remoteIdentifier: 'https://arstechnica.com/?p=1648607', remoteIdentifier: 'https://arstechnica.com/?p=1648607',
@ -280,6 +280,32 @@ describe('selected reducer', () => {
expect(reducer(state, action)).toEqual(expectedState); expect(reducer(state, action)).toEqual(expectedState);
}); });
it('should return state after receiving a post with none selected', () => {
const post = {
id: 2067,
remoteIdentifier: 'https://arstechnica.com/?p=1648607',
title:
'This amazing glitch puts Star Fox 64 ships in an unmodified Zelda cartridge',
body:
'"Stale-reference manipulation," 300-character file names, and a clash between worlds.',
author: 'Kyle Orland',
publicationDate: '2020-01-24T19:50:12Z',
url: 'https://arstechnica.com/?p=1648607',
rule: 4,
read: false,
};
const action = {
type: postActions.RECEIVE_POST,
post: { ...post, rule: 6 },
};
const state = { ...defaultState, post: {} };
const expectedState = { ...defaultState, post: {} };
expect(reducer(state, action)).toEqual(expectedState);
});
it('should return state after selecting a post', () => { it('should return state after selecting a post', () => {
const post = { const post = {
id: 2067, id: 2067,

View file

@ -12,3 +12,13 @@ export const formatDatetime = dateString => {
return date.toLocaleDateString(locale, dateOptions); return date.toLocaleDateString(locale, dateOptions);
}; };
export const objectsFromArray = (array, key) => {
const arrayEntries = array
.filter(object => key in object)
.map(object => {
return [object[key], { ...object }];
});
return Object.fromEntries(arrayEntries);
};