-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Posts: Add single site post requesting behavior to Redux state #3108
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,9 @@ | |
*/ | ||
import wpcom from 'lib/wp'; | ||
import { | ||
POST_REQUEST, | ||
POST_REQUEST_SUCCESS, | ||
POST_REQUEST_FAILURE, | ||
POSTS_RECEIVE, | ||
POSTS_REQUEST, | ||
POSTS_REQUEST_SUCCESS, | ||
|
@@ -68,3 +71,36 @@ export function requestSitePosts( siteId, query = {} ) { | |
} ); | ||
}; | ||
} | ||
|
||
/** | ||
* Triggers a network request to fetch a specific post from a site. | ||
* | ||
* @param {Number} siteId Site ID | ||
* @param {Number} postId Post ID | ||
* @return {Function} Action thunk | ||
*/ | ||
export function requestSitePost( siteId, postId ) { | ||
return ( dispatch ) => { | ||
dispatch( { | ||
type: POST_REQUEST, | ||
siteId, | ||
postId | ||
} ); | ||
|
||
return wpcom.site( siteId ).post( postId ).get().then( ( post ) => { | ||
dispatch( receivePost( post ) ); | ||
dispatch( { | ||
type: POST_REQUEST_SUCCESS, | ||
siteId, | ||
postId | ||
} ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this should probably return the post in case someone consuming this wants to chain onto the Promise and use it. See http://jsbin.com/jufayefego/edit?js,console There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Arguably, we shouldn't want to allow this, as the only consumption of data should be directly from the Redux state tree, not from an action thunk resolution. The only reason I opted to return the Promise in the first place is in facilitating test's awareness of the requests completing (as nock mocks aren't as immediate as you might expect). It may also come in handy for server-side rendering in knowing whether the response is ready to serve (based on the resolution of all initial actions), but again, not in specifically consuming any data result. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Promises always fulfill or reject on the next (well, a future) turn of the event loop, unless you mean something else? See http://blog.izs.me/post/59142742143/designing-apis-for-asynchrony for most of the reasoning. Zalgo is bad.
I can get behind that. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yeah, I mean something else. Naive attempts by me in testing nock would look more like: it( 'should finish request on next tick', ( done ) => {
someRequestMockedByNock();
setTimeout( () => {
expect( myAsyncSpy ).to.have.been.calledOnce;
done();
}, 0 );
} ); But for whatever reason, the nock mocks aren't resolved in any predictable next tick. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, interesting! Good to know. |
||
} ).catch( ( error ) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (this is curiosity talking, not a criticism) Why use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Don't feel strongly either way, but I suppose an argument could be made that it lends more clarity to what the code is effectively accomplishing, as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's a real subtle diff between the two cases. By using see http://www.html5rocks.com/en/tutorials/es6/promises/#toc-error-handling There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Subtle indeed, though still unsure which makes the most sense. Would we want to allow the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yup, don't dwell. Usage here is fine. If you want to flip to returning the promise from the test, just drop the catch. |
||
dispatch( { | ||
type: POST_REQUEST_FAILURE, | ||
siteId, | ||
postId, | ||
error | ||
} ); | ||
} ); | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -158,3 +158,20 @@ export function getSitePostsForQueryIgnoringPage( state, siteId, query ) { | |
return memo.concat( getSitePostsForQuery( state, siteId, pageQuery ) || [] ); | ||
}, [] ); | ||
} | ||
|
||
/** | ||
* Returns true if a request is in progress for the specified site post, or | ||
* false otherwise. | ||
* | ||
* @param {Object} state Global state tree | ||
* @param {Number} siteId Site ID | ||
* @param {Number} postId Post ID | ||
* @return {Boolean} Whether request is in progress | ||
*/ | ||
export function isRequestingSitePost( state, siteId, postId ) { | ||
if ( ! state.posts.siteRequests[ siteId ] ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. are you guaranteed that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yep, because the Redux store will be initialized with the default return value of the reducer. Under the hood, the initial state is constructed during the Redux internal There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Are you guaranteed a caller won't do run There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes, or at least, if there are callers, that's a larger issue in itself. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good good |
||
return false; | ||
} | ||
|
||
return !! state.posts.siteRequests[ siteId ][ postId ]; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,9 @@ import Chai, { expect } from 'chai'; | |
* Internal dependencies | ||
*/ | ||
import { | ||
POST_REQUEST, | ||
POST_REQUEST_SUCCESS, | ||
POST_REQUEST_FAILURE, | ||
POSTS_RECEIVE, | ||
POSTS_REQUEST, | ||
POSTS_REQUEST_SUCCESS, | ||
|
@@ -18,10 +21,25 @@ import { | |
import { | ||
receivePost, | ||
receivePosts, | ||
requestSitePosts | ||
requestSitePosts, | ||
requestSitePost | ||
} from '../actions'; | ||
|
||
describe( 'actions', () => { | ||
const spy = sinon.spy(); | ||
|
||
before( () => { | ||
Chai.use( sinonChai ); | ||
} ); | ||
|
||
beforeEach( () => { | ||
spy.reset(); | ||
} ); | ||
|
||
after( () => { | ||
nock.restore(); | ||
} ); | ||
|
||
describe( '#receivePost()', () => { | ||
it( 'should return an action object', () => { | ||
const post = { ID: 841, title: 'Hello World' }; | ||
|
@@ -47,11 +65,7 @@ describe( 'actions', () => { | |
} ); | ||
|
||
describe( '#requestSitePosts()', () => { | ||
const spy = sinon.spy(); | ||
|
||
before( () => { | ||
Chai.use( sinonChai ); | ||
|
||
nock( 'https://public-api.wordpress.com:443' ) | ||
.persist() | ||
.get( '/rest/v1.1/sites/2916284/posts' ) | ||
|
@@ -75,12 +89,8 @@ describe( 'actions', () => { | |
} ); | ||
} ); | ||
|
||
beforeEach( () => { | ||
spy.reset(); | ||
} ); | ||
|
||
after( () => { | ||
nock.restore(); | ||
nock.cleanAll(); | ||
} ); | ||
|
||
it( 'should dispatch fetch action when thunk triggered', () => { | ||
|
@@ -93,22 +103,20 @@ describe( 'actions', () => { | |
} ); | ||
} ); | ||
|
||
it( 'should dispatch posts receive action when request completes', ( done ) => { | ||
requestSitePosts( 2916284 )( spy ).then( () => { | ||
it( 'should dispatch posts receive action when request completes', () => { | ||
return requestSitePosts( 2916284 )( spy ).then( () => { | ||
expect( spy ).to.have.been.calledWith( { | ||
type: POSTS_RECEIVE, | ||
posts: [ | ||
{ ID: 841, title: 'Hello World' }, | ||
{ ID: 413, title: 'Ribs & Chicken' } | ||
] | ||
} ); | ||
|
||
done(); | ||
} ).catch( done ); | ||
} ); | ||
} ); | ||
|
||
it( 'should dispatch posts posts request success action when request completes', ( done ) => { | ||
requestSitePosts( 2916284 )( spy ).then( () => { | ||
it( 'should dispatch posts posts request success action when request completes', () => { | ||
return requestSitePosts( 2916284 )( spy ).then( () => { | ||
expect( spy ).to.have.been.calledWith( { | ||
type: POSTS_REQUEST_SUCCESS, | ||
siteId: 2916284, | ||
|
@@ -119,13 +127,11 @@ describe( 'actions', () => { | |
{ ID: 413, title: 'Ribs & Chicken' } | ||
] | ||
} ); | ||
|
||
done(); | ||
} ).catch( done ); | ||
} ); | ||
} ); | ||
|
||
it( 'should dispatch posts request success action with query results', ( done ) => { | ||
requestSitePosts( 2916284, { search: 'Hello' } )( spy ).then( () => { | ||
it( 'should dispatch posts request success action with query results', () => { | ||
return requestSitePosts( 2916284, { search: 'Hello' } )( spy ).then( () => { | ||
expect( spy ).to.have.been.calledWith( { | ||
type: POSTS_REQUEST_SUCCESS, | ||
siteId: 2916284, | ||
|
@@ -135,22 +141,78 @@ describe( 'actions', () => { | |
{ ID: 841, title: 'Hello World' } | ||
] | ||
} ); | ||
|
||
done(); | ||
} ).catch( done ); | ||
} ); | ||
} ); | ||
|
||
it( 'should dispatch fail action when request fails', ( done ) => { | ||
requestSitePosts( 77203074 )( spy ).then( () => { | ||
it( 'should dispatch fail action when request fails', () => { | ||
return requestSitePosts( 77203074 )( spy ).then( () => { | ||
expect( spy ).to.have.been.calledWith( { | ||
type: POSTS_REQUEST_FAILURE, | ||
siteId: 77203074, | ||
query: {}, | ||
error: sinon.match( { message: 'User cannot access this private blog.' } ) | ||
} ); | ||
} ); | ||
} ); | ||
} ); | ||
|
||
done(); | ||
} ).catch( done ); | ||
describe( '#requestSitePost()', () => { | ||
before( () => { | ||
nock( 'https://public-api.wordpress.com:443' ) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not for this PR, but if it's too painful to mock wpcom.js, maybe we should look at fixing wpcom.js to make it easier to test through. Having to bake all this info (host / port / protocol / path) into tests feels fragile. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yeah, I've used and seen a few different approaches to mocking WPCOM.js requests, though I'm mostly content with this one (see also: mocking WPCOM.js methods gets hairy too). Maybe some abstractions or constants to reduce the chance of changes breaking tests? I dunno, seems a bit premature, and this doesn't feel particularly troubling to me. |
||
.persist() | ||
.get( '/rest/v1.1/sites/2916284/posts/413' ) | ||
.reply( 200, { ID: 413, title: 'Ribs & Chicken' } ) | ||
.get( '/rest/v1.1/sites/2916284/posts/420' ) | ||
.reply( 404, { | ||
error: 'unknown_post', | ||
message: 'Unknown post' | ||
} ); | ||
} ); | ||
|
||
after( () => { | ||
nock.cleanAll(); | ||
} ); | ||
|
||
it( 'should dispatch request action when thunk triggered', () => { | ||
requestSitePost( 2916284, 413 )( spy ); | ||
|
||
expect( spy ).to.have.been.calledWith( { | ||
type: POST_REQUEST, | ||
siteId: 2916284, | ||
postId: 413 | ||
} ); | ||
} ); | ||
|
||
it( 'should dispatch posts receive action when request completes', () => { | ||
return requestSitePost( 2916284, 413 )( spy ).then( () => { | ||
expect( spy ).to.have.been.calledWith( { | ||
type: POSTS_RECEIVE, | ||
posts: [ | ||
sinon.match( { ID: 413, title: 'Ribs & Chicken' } ) | ||
] | ||
} ); | ||
} ); | ||
} ); | ||
|
||
it( 'should dispatch posts posts request success action when request completes', () => { | ||
return requestSitePost( 2916284, 413 )( spy ).then( () => { | ||
expect( spy ).to.have.been.calledWith( { | ||
type: POST_REQUEST_SUCCESS, | ||
siteId: 2916284, | ||
postId: 413 | ||
} ); | ||
} ); | ||
} ); | ||
|
||
it( 'should dispatch fail action when request fails', () => { | ||
return requestSitePost( 2916284, 420 )( spy ).then( () => { | ||
expect( spy ).to.have.been.calledWith( { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i wouldn't expect the success callback to be called at all? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Similar points as above in #3108 (comment) and #3108 (comment). The promise really only exists to mark progress of the request, but is not intended to be used in any effective way (either through consumption of the resolved object, or in throwing errors that occurred during the request). These are better handled by the Redux action handlers. |
||
type: POST_REQUEST_FAILURE, | ||
siteId: 2916284, | ||
postId: 420, | ||
error: sinon.match( { message: 'Unknown post' } ) | ||
} ); | ||
} ); | ||
} ); | ||
} ); | ||
} ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(note: not really for this PR)
I wonder if we should flip all of these to keymirror... It turns out the const doesn't transfer through the export, so an importer can still reassign the imported variable. I feel like the
const
is giving a false sense of security.But maybe it helps with code completion in some editors?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What advantage would there be in using keyMirror?
For me at least, this is less about feeling of security with
const
and rather in opting to use simple language constructs than an external library that, in my opinion, doesn't provide much value.There's some discussion in the
keyMirror
repository about minifiers crushing keys, though I don't think it affects our situation.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just less duplication. it's really easy to fat finger one side of the const construction and not notice.
MY_REALLY_LONG_KEY = "MY_REALY_LONG_KEY"
vs
keyMirror( { MY_REALLY_LONG_KEY: t } )
I'm not worried about the minifier advantages really. Though that could speed up string compares I guess. Likely meh.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe some custom lint rules could help here?
Related: #3015, https://github.com/Automattic/eslint-plugin-wpcalypso