0.2.3 #99
15 changed files with 161 additions and 161 deletions
|
|
@ -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())));
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -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}`);
|
||||||
|
|
|
||||||
|
|
@ -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));
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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 = {};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 = [];
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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 fly’s “connectome”',
|
title: 'The most complete brain map ever is here: A fly’s “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 fly’s “connectome”',
|
title: 'The most complete brain map ever is here: A fly’s “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' },
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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 fly’s “connectome”',
|
title: 'The most complete brain map ever is here: A fly’s “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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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 fly’s “connectome”',
|
title: 'The most complete brain map ever is here: A fly’s “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,
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue