#60 implement infinite scroll#90
Conversation
| cy.intercept('GET', '/api/users?page=*', req => { | ||
| req.on('response', res => res.setDelay(500)); | ||
| }).as('getUsers'); |
There was a problem hiding this comment.
intercepts the api call to add a 500 ms delay to give cypress enough time to see the loading spinner
devonzara
left a comment
There was a problem hiding this comment.
Apologies for the delay... I've not gotten the chance to pull it down and actually work through/test the PR, but I've added some comments/suggested changes that can be done in the interim. 👍🏼
| env: { | ||
| bash: 'C:\\Program Files\\Git\\bin\\bash.exe', | ||
| comSpec: 'C:\\Windows\\system32\\cmd.exe', | ||
| }, |
There was a problem hiding this comment.
This is no longer needed as part of this PR.
| before(() => { | ||
| // clear Db | ||
| cy.truncateDatabase(); | ||
|
|
||
| // fill DB with 100 users | ||
| cy.createUsers(100); | ||
|
|
||
| // get total pages for tests | ||
| cy.request('http://localhost:3000/api/users').then(response => { | ||
| totalPages = response.body.totalPages; | ||
| }); |
There was a problem hiding this comment.
I'd probably remove these 3 comments as they don't really add any additional information.
| i < totalPages | ||
| ? cy.get('@ulChildren').should('have.length', PAGE_LIMIT * i) | ||
| : // last page not guaranteed to be full | ||
| cy.get('@ulChildren').should('have.length.within', PAGE_LIMIT * (i - 1), PAGE_LIMIT * i); |
There was a problem hiding this comment.
nit: formatting
| i < totalPages | |
| ? cy.get('@ulChildren').should('have.length', PAGE_LIMIT * i) | |
| : // last page not guaranteed to be full | |
| cy.get('@ulChildren').should('have.length.within', PAGE_LIMIT * (i - 1), PAGE_LIMIT * i); | |
| i < totalPages ? | |
| cy.get('@ulChildren').should('have.length', PAGE_LIMIT * i) : | |
| // last page not guaranteed to be full | |
| cy.get('@ulChildren').should('have.length.within', PAGE_LIMIT * (i - 1), PAGE_LIMIT * i); |
| // last page won't have a spinner | ||
| if (i === totalPages) break; | ||
| // check spinner happened & removed | ||
| cy.get('[data-cy="loading"]'); //check if exists | ||
| cy.get('[data-cy="loading"]').should('not.exist'); // removed after data loaded |
There was a problem hiding this comment.
nit: formatting, empty line after breaks/continues & same line comments should typically be avoided
| // last page won't have a spinner | |
| if (i === totalPages) break; | |
| // check spinner happened & removed | |
| cy.get('[data-cy="loading"]'); //check if exists | |
| cy.get('[data-cy="loading"]').should('not.exist'); // removed after data loaded | |
| // last page won't have a spinner | |
| if (i === totalPages) break; | |
| // spinner should exist & be removed after request completes | |
| cy.get('[data-cy="loading"]'); | |
| cy.get('[data-cy="loading"]').should('not.exist'); |
Also, is this missing a cy.wait or something? How does it know when the request is complete?
There was a problem hiding this comment.
@devonzara Cypress will keep retrying their commands for 4 secs each before it declares it failed. That's why a cy.wait isn't required
| cy.get('[data-cy="loading"]'); //check if exists | ||
| cy.wait('@getUsers'); | ||
| cy.get('[data-cy="loading"]').should('not.exist'); // removed after data loaded |
There was a problem hiding this comment.
nit: same as above, inline comments
| users: response.data.users, | ||
| }; | ||
| } catch (err) { | ||
| console.error(err); |
There was a problem hiding this comment.
We should avoid using console on the client side as it will fail without notifying the user... see linked comments on #87 and the approach they took to address it.
| setTotalPageNum(total); | ||
| setUserData([...userData, ...users]); | ||
| } catch (err) { | ||
| console.error(err); |
There was a problem hiding this comment.
Same as above, we should avoid using console in the client-side code.
timmyichen
left a comment
There was a problem hiding this comment.
minor comments, nothing major, i think this is almost ready to go!
| i < totalPages ? | ||
| cy.get('@ulChildren').should('have.length', PAGE_LIMIT * i) : | ||
| // last page not guaranteed to be full | ||
| cy.get('@ulChildren').should('have.length.within', PAGE_LIMIT * (i - 1), PAGE_LIMIT * i); |
There was a problem hiding this comment.
stylistic choice, but we should reserve ternary statements for values, rather than actual execution. an if/else would be preferred here
| return { users, totalPages, error: null }; | ||
| } catch (error) { | ||
| return { users: [], error: error.message }; | ||
| return { users: [], totalPages: 1, error: error.message }; |
There was a problem hiding this comment.
why is totalPages set to 1 here?
There was a problem hiding this comment.
I set it to 1 because if there are no users then there would only be 1 page and we set up the users API to throw an error on calling page=0
| const [currentPage, setCurrentPage] = React.useState(1); | ||
| const [totalPageNum, setTotalPageNum] = React.useState(totalPages); | ||
| const [isLoading, setIsLoading] = React.useState(false); | ||
| const [open, setOpen] = React.useState(false); |
There was a problem hiding this comment.
would prefer to have this be named a little more specifically (if we have more than one alert/modal/etc, what this opens becomes ambiguous). showError, setShowError as a sample alternative
| if (currentPage + 1 > totalPageNum) { | ||
| setIsLoading(false); | ||
| return; | ||
| } |
There was a problem hiding this comment.
can this check happen before we even set isLoading? that way we can just setIsLoading(true) right before we start fetching the next page of users
|
|
||
| return ( | ||
| <Container maxWidth="lg" className="pt-4"> | ||
| <ErrorPopup open={open} setOpen={setOpen} message="Page out of range" /> |
There was a problem hiding this comment.
A note - Page out of range isn't the only possible error we could get - but I think error handling in general can be worked on later so I won't block on this :P
| } | ||
| }, intersectionOptions); | ||
|
|
||
| setIsLoading(false); |
There was a problem hiding this comment.
was this meant to be inside of the observer callback?
There was a problem hiding this comment.
Originally I put it there for a reason I can't remember, but within the observer callback makes more sense 👍
devonzara
left a comment
There was a problem hiding this comment.
Looking good, mostly nits and questions about error handling remain.
There was a problem hiding this comment.
nit: I think this might be better named ErrorToast or even just Toast as Popup is a bit ambiguous and Error limits the scope of its future use.
There was a problem hiding this comment.
I agree on ErrorToast for now, I think configuring the component to handle situations that haven't occurred yet and may never occur goes against YAGNI. In the future, if we need the toast to do more then we can add the functionality then 😎
|
|
||
| cy.intercept('GET', '/api/users?page=*', req => { | ||
| req.on('response', res => res.setDelay(500)); | ||
| }).as('getUsers'); |
There was a problem hiding this comment.
nit: is the .as() alias necessary if we don't have to wait for it?
There was a problem hiding this comment.
While researching about .as() more and if cy.wait() was necessary, I found the correct way to actually pause the response while waiting for the loading spinner
cy.intercept('/api/users?page=*', req => {
/**
* If set, `cy.wait` can be used to await the request/response
* cycle to complete for this request via `cy.wait('@alias')`.
*/
req.alias = 'getUsers';
}).as('getUsers');
cy.get('[data-cy="loading"]');
// here cypress releases the response
cy.wait('@getUsers');
cy.get('[data-cy="loading"]').should('not.exist');docs: https://docs.cypress.io/api/commands/intercept#Request-object-properties
There was a problem hiding this comment.
@MainlyColors Correct me if I'm wrong, but I believe .as() and req.alias = have similar behaviors, and are therefore redundant when used together. Given that dynamic alias assignment is not a requirement in this context, using .as() alone should suffice. No?
|
|
||
| // spinner should exist & be removed after request completes | ||
| cy.get('[data-cy="loading"]'); | ||
| cy.wait('@getUsers'); |
There was a problem hiding this comment.
Is this cy.wait necessary? If so, what's different here vs the previous test? If cy.wait isn't necessary in either case, can we get rid of it and the .as() alias above?
| message: 'Request failed with status code 400', | ||
| }, | ||
| }); | ||
| // req.on('before:response', res => (res.statusCode = 400)); |
There was a problem hiding this comment.
nit: lingering line of commented code
| }, | ||
| }); | ||
| // req.on('before:response', res => (res.statusCode = 400)); | ||
| }).as('getUsers'); |
There was a problem hiding this comment.
nit: unused .as() alias
| users: response.data.users, | ||
| }; | ||
| } catch (err) { | ||
| if (err.code === 'ERR_BAD_REQUEST') { |
There was a problem hiding this comment.
What happens if other errors occur?
There was a problem hiding this comment.
I didn't know all the errors that were possible, Ill look into it more
There was a problem hiding this comment.
Every possible error doesn't need to be explicitly handled; the user doesn't always need to know what the error was.
Some errors may be network related and work themselves out, others might be things we need to address... either way, communicating that an error has occurred to the user instead of leaving them uncertain results in a better user experience.
| setTotalPageNum(total); | ||
| setUserData([...userData, ...users]); | ||
| } catch (err) { | ||
| if (err.message === 'Page out of range') { |
There was a problem hiding this comment.
I'm not sure this conditional is necessary... or, if kept - should we provide an else condition with a generic error?
devonzara
left a comment
There was a problem hiding this comment.
LGTM. Thanks for taking this on!
Closes #60
Description
feature
Intersection Observerto trigger loading of a new batch of 20 profiles on the/directorypage when the user scrolls down to the height of the last card above the bottom of the window/ul that contains all the users/api/usersTests added
directory infinite scrolltest suite to generate 100 usersTesting
manually
npm run dev:servernpm run seed- to generate a 100 users -\directorydiscord_namehas a 20% chance of being empty which is being fixed in another PR but will cause some users not to show up on the pageslow 3Gto catch the loading spinnerAutomatic
npm run dev:servernpm run cypress:openType of change
Screenshots
Tests passing - directory.spec.ts

Known issue - intercepting user api w/ cypress
The use of cypress intercept here in the directory test is intended to wait upon the response and catch the loading spinner. This works 95-99% of the time but if the loading spinner appears for less than 20ms then cypress won't be able to catch it, resulting in a failed test. Just re-run the test to be sure.
Different variations of using
as(),(req) => req.alias, and evenreq.reply(res => res.setDelay(100))and every possible permutation of those 3 were tried by Myself and Devon but we still got random errors.Checklist: