Skip to content

Commit

Permalink
added infinite scroll async collection bundle
Browse files Browse the repository at this point in the history
  • Loading branch information
abuinitski committed Jul 11, 2019
1 parent 19e5899 commit 822af68
Show file tree
Hide file tree
Showing 19 changed files with 912 additions and 360 deletions.
6 changes: 6 additions & 0 deletions src/__mocks__/MockApiClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ const Items = {
2: 'Two',
3: 'Three',
nilItem: null,
collection1: ['one', 'two', 'three'],
collection2: ['four', 'five', 'six'],
}

export default function MockApiClient() {
Expand All @@ -18,6 +20,10 @@ export default function MockApiClient() {
})
})

this.resolveAllFetchRequests = async () => {
requestQueue.map(r => r.itemId).forEach(itemId => this.resolveFetchRequest(itemId))
}

this.resolveFetchRequest = async (itemId, error = null) => {
const index = requestQueue.findIndex(r => r.itemId === itemId && !r.resolved)
if (index < 0) {
Expand Down
15 changes: 15 additions & 0 deletions src/__mocks__/pagingBundle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export default {
name: 'paging',
reducer: (state = { currentPage: null, pageSize: 10 }, action) => {
if (action.type === 'paging.change') {
return {
...state,
...action.payload,
}
}
return state
},
doChangePaging: payload => ({ type: 'paging.change', payload: payload }),
selectCurrentPage: state => state.paging.currentPage,
selectPageSize: state => state.paging.pageSize,
}
23 changes: 23 additions & 0 deletions src/__mocks__/time.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import timekeeper from 'timekeeper'
import { tapReactors } from './utils'

export const START_TIME = 1000

export function setUpTimeTravel() {
beforeEach(() => timekeeper.freeze(START_TIME))

afterEach(() => timekeeper.reset())
}

export function timeTravelTo(time, store) {
return new Promise(async (resolve, reject) => {
try {
timekeeper.travel(START_TIME + time)
store.dispatch({ type: 'dummy action', payload: null }) // just tap the app time
await tapReactors()
resolve()
} catch (e) {
reject(e)
}
})
}
6 changes: 6 additions & 0 deletions src/__mocks__/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export async function tapReactors() {
const tapOneReactor = () => new Promise(resolve => setTimeout(resolve, 0))
await tapOneReactor()
await tapOneReactor()
await tapOneReactor()
}
105 changes: 105 additions & 0 deletions src/__test_behaviors__/behavesAsResourceWithDependencies.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { tapReactors } from '../__mocks__/utils'

export default function behavesAsStalingResource(
{ selectors, actionCreators },
{ fetchActionCreator, fetchPendingSelector, createStore }
) {
const isPendingForFetch = store => store[fetchPendingSelector]()
const isStale = store => store[selectors.isStale]()
const isPresent = store => store[selectors.isPresent]()
const triggerFetch = store => store[fetchActionCreator]()

describe('dependency keys', () => {
describe('with one dependency', () => {
test('does not try to fetch while dependencies are not satisfied', () => {
const { store } = createStore({ dependencyKey: 'currentPage' })
expect(isPendingForFetch(store)).toBe(false)
})

test('triggers pending state as soon as dependency is resolved', async () => {
const { store } = createStore({ dependencyKey: 'currentPage' })
store.doChangePaging({ currentPage: 1 })
await tapReactors()
expect(isPendingForFetch(store)).toBe(true)
})

test('clears the store when dependency changes', async () => {
const { store, apiMock } = createStore({ dependencyKey: 'currentPage' })
store.doChangePaging({ currentPage: 1 })
await tapReactors()
triggerFetch(store)
await apiMock.resolveAllFetchRequests()

store.doChangePaging({ currentPage: 2 })
await tapReactors()
expect(isPresent(store)).toBe(false)
})

test('stales a resource instead of clearing it with "stale" option', async () => {
const { store, apiMock } = createStore({ dependencyKey: { key: 'currentPage', staleOnChange: true } })
store.doChangePaging({ currentPage: 1 })
await tapReactors()
triggerFetch(store)
await apiMock.resolveAllFetchRequests()

store.doChangePaging({ currentPage: 2 })
await tapReactors()

expect(isPresent(store)).toBe(true)
expect(isStale(store)).toBe(true)
})

test('allows to enable passing down blank values', async () => {
const { store } = createStore({ dependencyKey: { key: 'currentPage', allowBlank: true } })
await tapReactors()
expect(isPendingForFetch(store)).toBe(true)
})

test.todo('mixes in dependency values into getPromise call')
})

describe('with multiple dependencies', () => {
test('does not try to fetch while dependencies are not satisfied', () => {
const { store } = createStore({ dependencyKey: ['currentPage', 'pageSize'] })
expect(isPendingForFetch(store)).toBe(false)
})

test('triggers pending state as soon as dependency is resolved', async () => {
const { store } = createStore({ dependencyKey: ['currentPage', 'pageSize'] })
store.doChangePaging({ currentPage: 1 })
await tapReactors()
await tapReactors()
expect(isPendingForFetch(store)).toBe(true)
})

test('clears the store when dependency changes', async () => {
const { store, apiMock } = createStore({ dependencyKey: ['currentPage', 'pageSize'] })
store.doChangePaging({ currentPage: 1 })
await tapReactors()
await tapReactors()

triggerFetch(store)
await apiMock.resolveAllFetchRequests()

store.doChangePaging({ currentPage: 2 })
await tapReactors()
expect(isPresent(store)).toBe(false)
})

test('stales a resource instead of clearing it with "stale" option and allows blank items', async () => {
const { store } = createStore({
dependencyKey: [{ key: 'currentPage', allowBlank: true }, { key: 'pageSize', staleOnChange: true }],
})

await tapReactors()
expect(isPendingForFetch(store)).toBe(true)

store.doChangePaging({ pageSize: 120 })
await tapReactors()
expect(isStale(store)).toBe(true)
})

test.todo('mixes in dependency values into getPromise call')
})
})
}
37 changes: 37 additions & 0 deletions src/__test_behaviors__/behavesAsStalingResource.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { timeTravelTo } from '../__mocks__/time'

export default function behavesAsStalingResource({ selectors, actionCreators }, { fetchActionCreator, createStore }) {
describe('staling feature', () => {
test('marks item as stale manually', async () => {
const { store, apiMock } = createStore({ staleAfter: 15 })

store[fetchActionCreator]()

await apiMock.resolveAllFetchRequests()

store[actionCreators.doMarkAsStale]()

expect(store[selectors.isPresent]()).toBe(true)
expect(store[selectors.isStale]()).toBe(true)

await timeTravelTo(16, store)

expect(store[selectors.isPresent]()).toBe(true)
expect(store[selectors.isStale]()).toBe(true)
})

test('marks item as stale with a timer', async () => {
const { store, apiMock } = createStore({ staleAfter: 15 })

store[fetchActionCreator]()

await apiMock.resolveAllFetchRequests()

expect(store[selectors.isStale]()).toBe(false)

await timeTravelTo(16, store)

expect(store[selectors.isStale]()).toBe(true)
})
})
}
Loading

0 comments on commit 822af68

Please sign in to comment.