Skip to content
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

feat: 🎸 Timetables #12

Merged
merged 3 commits into from
Apr 9, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions package.json
Original file line number Diff line number Diff line change
@@ -19,32 +19,32 @@
"publish-package": "npm publish --access public"
},
"dependencies": {
"react-redux": "^7.2.2",
"react-redux": "^7.2.3",
"redux": "^4.0.5"
},
"peerDependencies": {
"@skolplattformen/embedded-api": "^2.0.0",
"@skolplattformen/embedded-api": "^4.0.0",
"react": "^16.11.0"
},
"devDependencies": {
"@babel/preset-env": "^7.13.10",
"@babel/preset-react": "^7.12.13",
"@babel/preset-env": "^7.13.15",
"@babel/preset-react": "^7.13.13",
"@babel/preset-typescript": "^7.13.0",
"@skolplattformen/embedded-api": "^2.0.0",
"@testing-library/jest-dom": "^5.11.9",
"@testing-library/react": "^11.2.5",
"@testing-library/react-hooks": "^5.1.0",
"@types/jest": "^26.0.20",
"@skolplattformen/embedded-api": "^4.0.0",
"@testing-library/jest-dom": "^5.11.10",
"@testing-library/react": "^11.2.6",
"@testing-library/react-hooks": "^5.1.1",
"@types/jest": "^26.0.22",
"@types/react": "^16.14.3",
"@types/react-redux": "^7.1.16",
"@typescript-eslint/eslint-plugin": "^4.17.0",
"@typescript-eslint/eslint-plugin": "^4.21.0",
"babel-jest": "^26.6.3",
"eslint": "^7.21.0",
"eslint": "^7.23.0",
"eslint-config-airbnb-typescript": "^12.3.1",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jest": "^24.2.1",
"eslint-plugin-jest": "^24.3.4",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-react": "^7.22.0",
"eslint-plugin-react": "^7.23.2",
"eslint-plugin-react-hooks": "^4.2.0",
"events": "^3.3.0",
"jest": "^26.6.3",
2 changes: 2 additions & 0 deletions src/__mocks__/@skolplattformen/embedded-api.js
Original file line number Diff line number Diff line change
@@ -15,12 +15,14 @@ const createApi = () => ({

getCalendar: jest.fn(),
getChildren: jest.fn(),
getSkola24Children: jest.fn(),
getClassmates: jest.fn(),
getMenu: jest.fn(),
getNews: jest.fn(),
getNewsDetails: jest.fn(),
getNotifications: jest.fn(),
getSchedule: jest.fn(),
getTimetable: jest.fn(),
getUser: jest.fn(),
})
const init = jest.fn().mockImplementation(() => createApi())
66 changes: 66 additions & 0 deletions src/childlists.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { merge } from './childlists'

describe('childlists', () => {
describe('merge', () => {
it('works with empty skola24children list', () => {
const etjanstChildren = [
{ name: 'Uwe Übrink (elev)' },
{ name: 'Cassius Übrink (elev)' },
]
const skola24Children = []

const children = [
{ name: 'Uwe Übrink (elev)' },
{ name: 'Cassius Übrink (elev)' },
]
expect(merge(etjanstChildren, skola24Children)).toEqual(children)
})
it('works with same length skola24children list', () => {
const etjanstChildren = [
{ name: 'Uwe Übrink (elev)' },
{ name: 'Cassius Übrink (elev)' },
]
const skola24Children = [
{ firstName: 'Uwe', lastName: 'Vredstein Übrink' },
{ firstName: 'Cassius', lastName: 'Vredstein Übrink' },
]

const children = [
{ name: 'Uwe Übrink (elev)', firstName: 'Uwe', lastName: 'Vredstein Übrink' },
{ name: 'Cassius Übrink (elev)', firstName: 'Cassius', lastName: 'Vredstein Übrink' },
]
expect(merge(etjanstChildren, skola24Children)).toEqual(children)
})
it('works with different length skola24children list', () => {
const etjanstChildren = [
{ name: 'Uwe Übrink (elev)' },
{ name: 'Cassius Übrink (elev)' },
]
const skola24Children = [
{ firstName: 'Uwe', lastName: 'Vredstein Übrink' },
]

const children = [
{ name: 'Uwe Übrink (elev)', firstName: 'Uwe', lastName: 'Vredstein Übrink' },
{ name: 'Cassius Übrink (elev)' },
]
expect(merge(etjanstChildren, skola24Children)).toEqual(children)
})
it('works with non matching skola24children list', () => {
const etjanstChildren = [
{ name: 'Uwe Übrink (elev)' },
{ name: 'Cassius Übrink (elev)' },
]
const skola24Children = [
{ firstName: 'Uwe', lastName: 'Vredstein Übrink' },
{ firstName: 'Rolph', lastName: 'Gögendorff Bröök' },
]

const children = [
{ name: 'Uwe Übrink (elev)', firstName: 'Uwe', lastName: 'Vredstein Übrink' },
{ name: 'Cassius Übrink (elev)' },
]
expect(merge(etjanstChildren, skola24Children)).toEqual(children)
})
})
})
15 changes: 15 additions & 0 deletions src/childlists.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Child, EtjanstChild, Skola24Child } from '@skolplattformen/embedded-api'

// eslint-disable-next-line import/prefer-default-export
export const merge = (etjanstChildren: EtjanstChild[], skola24Children: Skola24Child[]): Child[] => (
etjanstChildren.map((etjanstChild) => {
const skola24Child: Skola24Child = (
skola24Children.find((s24c) => s24c.firstName && etjanstChild.name.startsWith(s24c.firstName)) || {}
)
const child: Child = {
...etjanstChild,
...skola24Child,
}
return child
})
)
4 changes: 2 additions & 2 deletions src/fake.test.js
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ import { ApiProvider } from '.'
import createStorage from './__mocks__/AsyncStorage'
import {
useCalendar,
useChildList,
useEtjanstChildren,
useClassmates,
useMenu,
useNews,
@@ -69,7 +69,7 @@ describe('hooks with fake data', () => {
const {
result,
waitForNextUpdate,
} = renderHook(() => useChildList(), { wrapper })
} = renderHook(() => useEtjanstChildren(), { wrapper })

await waitForNextUpdate()
await waitForNextUpdate()
58 changes: 54 additions & 4 deletions src/hooks.ts
Original file line number Diff line number Diff line change
@@ -5,10 +5,13 @@ import {
CalendarItem,
Child,
Classmate,
EtjanstChild,
MenuItem,
NewsItem,
Notification,
ScheduleItem,
Skola24Child,
TimetableEntry,
User,
} from '@skolplattformen/embedded-api'
import {
@@ -22,6 +25,7 @@ import {
import { useApi } from './context'
import { loadAction } from './actions'
import store from './store'
import { merge } from './childlists'

interface StoreSelector<T> {
(state: EntityStoreRootState): EntityMap<T>
@@ -73,7 +77,15 @@ const hook = <T>(
}
useEffect(() => { load() }, [isLoggedIn])

let mounted: boolean
useEffect(() => {
mounted = true
return () => { mounted = false }
}, [])

const listener = () => {
if (!mounted) return

const newState = select(getState())
if (newState.status !== state.status
|| newState.data !== state.data
@@ -94,14 +106,22 @@ const hook = <T>(
}
}

export const useChildList = () => hook<Child[]>(
'CHILDREN',
'children',
export const useEtjanstChildren = () => hook<EtjanstChild[]>(
'ETJANST_CHILDREN',
'etjanst_children',
[],
(s) => s.children,
(s) => s.etjanstChildren,
(api) => () => api.getChildren(),
)

export const useSkola24Children = () => hook<Skola24Child[]>(
'SKOLA24_CHILDREN',
'skola24_children',
[],
(s) => s.skola24Children,
(api) => () => api.getSkola24Children(),
)

export const useCalendar = (child: Child) => hook<CalendarItem[]>(
'CALENDAR',
`calendar_${child.id}`,
@@ -158,10 +178,40 @@ export const useSchedule = (child: Child, from: string, to: string) => hook<Sche
(api) => () => api.getSchedule(child, from, to),
)

export const useTimetable = (child: Skola24Child, week: number, year: number) => hook<TimetableEntry[]>(
'TIMETABLE',
`timetable_${child.personGuid}_${week}_${year}`,
[],
(s) => s.timetable,
(api) => () => api.getTimetable(child, week, year),
)

export const useUser = () => hook<User>(
'USER',
'user',
{},
(s) => s.user,
(api) => () => api.getUser(),
)

export const useChildList = (): EntityHookResult<Child[]> => {
const {
data: etjanstData, status, error, reload: etjanstReload,
} = useEtjanstChildren()
const { data: skola24Data, reload: skola24Reload } = useSkola24Children()

const [data, setData] = useState<Child[]>([])
const reload = () => {
etjanstReload()
skola24Reload()
}

useEffect(() => {
if (!etjanstData.length) return
setData(merge(etjanstData, skola24Data))
}, [etjanstData, skola24Data])

return {
data, status, error, reload,
}
}
10 changes: 5 additions & 5 deletions src/logout.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react'
import { renderHook, act } from '@testing-library/react-hooks'
import { ApiProvider } from './provider'
import { useChildList } from './hooks'
import { useEtjanstChildren } from './hooks'
import store from './store'
import init from './__mocks__/@skolplattformen/embedded-api'
import createStorage from './__mocks__/AsyncStorage'
@@ -32,7 +32,7 @@ describe('logout - cleanup', () => {
})
))
storage = createStorage({
'123_children': [{ id: 2 }],
'123_etjanst_children': [{ id: 2 }],
}, 2)
})
afterEach(async () => {
@@ -46,7 +46,7 @@ describe('logout - cleanup', () => {
api.isLoggedIn = true
api.isFake = false

const { waitForNextUpdate: wait1 } = renderHook(() => useChildList(), { wrapper })
const { waitForNextUpdate: wait1 } = renderHook(() => useEtjanstChildren(), { wrapper })

await wait1()
await wait1()
@@ -56,14 +56,14 @@ describe('logout - cleanup', () => {
api.isLoggedIn = false
api.emitter.emit('logout')

const { result } = renderHook(() => useChildList(), { wrapper })
const { result } = renderHook(() => useEtjanstChildren(), { wrapper })

expect(result.current.data).toHaveLength(0)

api.isLoggedIn = true
api.emitter.emit('login')

const { result: result2, waitForNextUpdate: wait2 } = renderHook(() => useChildList(), { wrapper })
const { result: result2, waitForNextUpdate: wait2 } = renderHook(() => useEtjanstChildren(), { wrapper })

await wait2()

8 changes: 6 additions & 2 deletions src/reducers.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import {
CalendarItem,
Child,
Classmate,
EtjanstChild,
Skola24Child,
MenuItem,
NewsItem,
Notification,
ScheduleItem,
User,
TimetableEntry,
} from '@skolplattformen/embedded-api'
import { EntityName, EntityReducer, EntityState } from './types'

@@ -65,11 +67,13 @@ const createReducer = <T>(entity: EntityName): EntityReducer<T> => {
}

export const user = createReducer<User>('USER')
export const children = createReducer<Child[]>('CHILDREN')
export const etjanstChildren = createReducer<EtjanstChild[]>('ETJANST_CHILDREN')
export const skola24Children = createReducer<Skola24Child[]>('SKOLA24_CHILDREN')
export const calendar = createReducer<CalendarItem[]>('CALENDAR')
export const classmates = createReducer<Classmate[]>('CLASSMATES')
export const menu = createReducer<MenuItem[]>('MENU')
export const news = createReducer<NewsItem[]>('NEWS')
export const newsDetails = createReducer<NewsItem[]>('NEWS_DETAILS')
export const notifications = createReducer<Notification[]>('NOTIFICATIONS')
export const schedule = createReducer<ScheduleItem[]>('SCHEDULE')
export const timetable = createReducer<TimetableEntry[]>('TIMETABLE')
8 changes: 6 additions & 2 deletions src/store.ts
Original file line number Diff line number Diff line change
@@ -2,25 +2,29 @@ import { createStore, combineReducers, applyMiddleware } from 'redux'
import { apiMiddleware, cacheMiddleware } from './middleware'
import {
calendar,
children,
classmates,
etjanstChildren,
menu,
news,
newsDetails,
notifications,
schedule,
skola24Children,
timetable,
user,
} from './reducers'

const appReducer = combineReducers({
calendar,
children,
classmates,
etjanstChildren,
menu,
news,
newsDetails,
notifications,
schedule,
skola24Children,
timetable,
user,
})
const rootReducer: typeof appReducer = (state, action) => {
11 changes: 9 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import {
Api,
Child,
EtjanstChild,
Skola24Child,
User,
CalendarItem,
Classmate,
MenuItem,
NewsItem,
Notification,
ScheduleItem,
TimetableEntry,
} from '@skolplattformen/embedded-api'
import { Action, Reducer } from 'redux'

@@ -50,6 +52,8 @@ export type EntityActionType = 'GET_FROM_API'
| 'STORE_IN_CACHE'
| 'CLEAR'
export type EntityName = 'USER'
| 'ETJANST_CHILDREN'
| 'SKOLA24_CHILDREN'
| 'CHILDREN'
| 'CALENDAR'
| 'CLASSMATES'
@@ -58,6 +62,7 @@ export type EntityName = 'USER'
| 'NEWS_DETAILS'
| 'NOTIFICATIONS'
| 'SCHEDULE'
| 'TIMETABLE'
| 'ALL'
export interface EntityAction<T> extends Action<EntityActionType> {
entity: EntityName
@@ -71,7 +76,8 @@ export interface EntityMap<T> {
export type EntityReducer<T> = Reducer<EntityMap<T>, EntityAction<T>>

export interface EntityStoreRootState {
children: EntityMap<Child[]>
etjanstChildren: EntityMap<EtjanstChild[]>
skola24Children: EntityMap<Skola24Child[]>
user: EntityMap<User>
calendar: EntityMap<CalendarItem[]>,
classmates: EntityMap<Classmate[]>,
@@ -80,6 +86,7 @@ export interface EntityStoreRootState {
newsDetails: EntityMap<NewsItem>,
notifications: EntityMap<Notification[]>,
schedule: EntityMap<ScheduleItem[]>,
timetable: EntityMap<TimetableEntry[]>,
}

export interface EntityHookResult<T> extends EntityState<T> {
269 changes: 269 additions & 0 deletions src/useChildList.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
import React from 'react'
import { renderHook, act } from '@testing-library/react-hooks'
import { ApiProvider } from './provider'
import { useChildList } from './hooks'
import store from './store'
import init from './__mocks__/@skolplattformen/embedded-api'
import createStorage from './__mocks__/AsyncStorage'
import reporter from './__mocks__/reporter'
import { etjanstChildren } from './reducers'

const pause = (ms = 0) => new Promise((r) => setTimeout(r, ms))

describe('useChildList()', () => {
let api
let storage
let echildrenCache
let skola24Cache
let echildrenResponse
let skola24Response
const wrapper = ({ children }) => (
<ApiProvider
api={api}
storage={storage}
reporter={reporter}
>
{children}
</ApiProvider>
)
beforeEach(() => {
echildrenCache = [{ id: 2, name: 'Uwe Übrink (elev)' }]
skola24Cache = [{ personGuid: '2', firstName: 'Uwe', lastName: 'Vredstein Übrink' }]

echildrenResponse = [{ id: 1, name: 'Uwe Übrink (elev)' }]
skola24Response = [{ personGuid: '1', firstName: 'Uwe', lastName: 'Vredstein Übrink' }]

api = init()
api.getPersonalNumber.mockReturnValue('123')

api.getChildren.mockImplementation(() => (
new Promise((res) => {
setTimeout(() => res(echildrenResponse), 50)
})
))
api.getSkola24Children.mockImplementation(() => (
new Promise((res) => {
setTimeout(() => res(skola24Response), 50)
})
))
storage = createStorage({
'123_etjanst_children': echildrenCache,
'123_skola24_children': skola24Cache,
}, 2)
})
afterEach(async () => {
await act(async () => {
await pause(70)
store.dispatch({ entity: 'ALL', type: 'CLEAR' })
})
})
it('returns correct initial value', () => {
const { result } = renderHook(() => useChildList(), { wrapper })

expect(result.current.status).toEqual('pending')
})
it('calls api', async () => {
await act(async () => {
api.isLoggedIn = true
const { waitForNextUpdate } = renderHook(() => useChildList(), { wrapper })

await waitForNextUpdate()
await waitForNextUpdate()

expect(api.getChildren).toHaveBeenCalled()
expect(api.getSkola24Children).toHaveBeenCalled()
})
})
it('only calls api once', async () => {
await act(async () => {
api.isLoggedIn = true
renderHook(() => useChildList(), { wrapper })
const { waitForNextUpdate } = renderHook(() => useChildList(), { wrapper })

await waitForNextUpdate()
renderHook(() => useChildList(), { wrapper })
await waitForNextUpdate()
renderHook(() => useChildList(), { wrapper })
await waitForNextUpdate()
await waitForNextUpdate()

const { result } = renderHook(() => useChildList(), { wrapper })

expect(api.getChildren).toHaveBeenCalledTimes(1)
expect(api.getSkola24Children).toHaveBeenCalledTimes(1)
expect(result.current.status).toEqual('loaded')
})
})
it('calls cache', async () => {
await act(async () => {
api.isLoggedIn = true
const { result, waitForNextUpdate } = renderHook(() => useChildList(), { wrapper })

await waitForNextUpdate()
await waitForNextUpdate()
await waitForNextUpdate()
await waitForNextUpdate()
await waitForNextUpdate()

expect(result.current.data).toEqual([{
id: 2,
name: 'Uwe Übrink (elev)',
personGuid: '2',
firstName: 'Uwe',
lastName: 'Vredstein Übrink',
}])
})
})
it('updates status to loading', async () => {
await act(async () => {
api.isLoggedIn = true
const { result, waitForNextUpdate } = renderHook(() => useChildList(), { wrapper })

await waitForNextUpdate()
await waitForNextUpdate()

expect(result.current.status).toEqual('loading')
})
})
it('updates status to loaded', async () => {
await act(async () => {
api.isLoggedIn = true
const { result, waitForNextUpdate } = renderHook(() => useChildList(), { wrapper })

await waitForNextUpdate()
await waitForNextUpdate()
await waitForNextUpdate()
await waitForNextUpdate()
await waitForNextUpdate()
await waitForNextUpdate()

expect(result.current.status).toEqual('loaded')
})
})
it('stores in cache if not fake', async () => {
await act(async () => {
api.isLoggedIn = true
api.isFake = false

const { waitForNextUpdate } = renderHook(() => useChildList(), { wrapper })

await waitForNextUpdate()
await waitForNextUpdate()
await waitForNextUpdate()
await waitForNextUpdate()
await waitForNextUpdate()
await waitForNextUpdate()
await pause(20)

expect(storage.cache['123_etjanst_children']).toEqual(JSON.stringify(echildrenResponse))
expect(storage.cache['123_skola24_children']).toEqual(JSON.stringify(skola24Response))
})
})
it('does not store in cache if fake', async () => {
await act(async () => {
api.isLoggedIn = true
api.isFake = true

const { result, waitForNextUpdate } = renderHook(() => useChildList(), { wrapper })

await waitForNextUpdate()
await waitForNextUpdate()
await waitForNextUpdate()
await pause(20)

expect(result.current.status).toEqual('loaded')
expect(storage.cache['123_etjanst_children']).toEqual(JSON.stringify(echildrenCache))
expect(storage.cache['123_skola24_children']).toEqual(JSON.stringify(skola24Cache))
})
})
it('retries if etjanst-api fails', async () => {
await act(async () => {
api.isLoggedIn = true
const error = new Error('fail')
api.getChildren.mockRejectedValueOnce(error)

const { result, waitForNextUpdate } = renderHook(() => useChildList(), { wrapper })

await waitForNextUpdate()
await waitForNextUpdate()
await waitForNextUpdate()

expect(result.current.error).toEqual(error)
expect(result.current.status).toEqual('loading')
expect(result.current.data).toEqual(echildrenCache)

jest.advanceTimersToNextTimer()

await waitForNextUpdate()
await waitForNextUpdate()
await waitForNextUpdate()
await waitForNextUpdate()
await waitForNextUpdate()
await waitForNextUpdate()

expect(result.current.status).toEqual('loaded')
expect(result.current.data).toEqual([{
id: 1,
name: 'Uwe Übrink (elev)',
personGuid: '1',
firstName: 'Uwe',
lastName: 'Vredstein Übrink',
}])
})
})
it('gives up after 3 retries', async () => {
await act(async () => {
api.isLoggedIn = true
const error = new Error('fail')
api.getChildren.mockRejectedValueOnce(error)
api.getChildren.mockRejectedValueOnce(error)
api.getChildren.mockRejectedValueOnce(error)

const { result, waitForNextUpdate } = renderHook(() => useChildList(), { wrapper })

await waitForNextUpdate()
await waitForNextUpdate()
await waitForNextUpdate()

expect(result.current.error).toEqual(error)
expect(result.current.status).toEqual('loading')
expect(result.current.data).toEqual(echildrenCache)

jest.advanceTimersToNextTimer()

await waitForNextUpdate()
await waitForNextUpdate()
await waitForNextUpdate()
await waitForNextUpdate()
await waitForNextUpdate()
await waitForNextUpdate()

expect(result.current.error).toEqual(error)
expect(result.current.status).toEqual('error')
expect(result.current.data).toEqual([{
id: 2,
name: 'Uwe Übrink (elev)',
personGuid: '1',
firstName: 'Uwe',
lastName: 'Vredstein Übrink',
}])
})
})
it('reports if api fails', async () => {
await act(async () => {
api.isLoggedIn = true
const error = new Error('fail')
api.getChildren.mockRejectedValueOnce(error)

const { result, waitForNextUpdate } = renderHook(() => useChildList(), { wrapper })

await waitForNextUpdate()
await waitForNextUpdate()
await waitForNextUpdate()

expect(result.current.error).toEqual(error)

expect(reporter.error).toHaveBeenCalledWith(error, 'Error getting ETJANST_CHILDREN from API')
})
})
})
42 changes: 21 additions & 21 deletions src/useChildlist.test.js → src/useEtjanstChildren.test.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import React from 'react'
import { renderHook, act } from '@testing-library/react-hooks'
import { ApiProvider } from './provider'
import { useChildList } from './hooks'
import { useEtjanstChildren } from './hooks'
import store from './store'
import init from './__mocks__/@skolplattformen/embedded-api'
import createStorage from './__mocks__/AsyncStorage'
import reporter from './__mocks__/reporter'

const pause = (ms = 0) => new Promise((r) => setTimeout(r, ms))

describe('useChildList()', () => {
describe('useEtjanstChildren()', () => {
let api
let storage
let response
@@ -32,7 +32,7 @@ describe('useChildList()', () => {
})
))
storage = createStorage({
'123_children': [{ id: 2 }],
'123_etjanst_children': [{ id: 2 }],
}, 2)
})
afterEach(async () => {
@@ -42,14 +42,14 @@ describe('useChildList()', () => {
})
})
it('returns correct initial value', () => {
const { result } = renderHook(() => useChildList(), { wrapper })
const { result } = renderHook(() => useEtjanstChildren(), { wrapper })

expect(result.current.status).toEqual('pending')
})
it('calls api', async () => {
await act(async () => {
api.isLoggedIn = true
const { waitForNextUpdate } = renderHook(() => useChildList(), { wrapper })
const { waitForNextUpdate } = renderHook(() => useEtjanstChildren(), { wrapper })

await waitForNextUpdate()
await waitForNextUpdate()
@@ -60,16 +60,16 @@ describe('useChildList()', () => {
it('only calls api once', async () => {
await act(async () => {
api.isLoggedIn = true
renderHook(() => useChildList(), { wrapper })
const { waitForNextUpdate } = renderHook(() => useChildList(), { wrapper })
renderHook(() => useEtjanstChildren(), { wrapper })
const { waitForNextUpdate } = renderHook(() => useEtjanstChildren(), { wrapper })

await waitForNextUpdate()
renderHook(() => useChildList(), { wrapper })
renderHook(() => useEtjanstChildren(), { wrapper })
await waitForNextUpdate()
renderHook(() => useChildList(), { wrapper })
renderHook(() => useEtjanstChildren(), { wrapper })
await waitForNextUpdate()

const { result } = renderHook(() => useChildList(), { wrapper })
const { result } = renderHook(() => useEtjanstChildren(), { wrapper })

expect(api.getChildren).toHaveBeenCalledTimes(1)
expect(result.current.status).toEqual('loaded')
@@ -78,7 +78,7 @@ describe('useChildList()', () => {
it('calls cache', async () => {
await act(async () => {
api.isLoggedIn = true
const { result, waitForNextUpdate } = renderHook(() => useChildList(), { wrapper })
const { result, waitForNextUpdate } = renderHook(() => useEtjanstChildren(), { wrapper })

await waitForNextUpdate()
await waitForNextUpdate()
@@ -89,7 +89,7 @@ describe('useChildList()', () => {
it('updates status to loading', async () => {
await act(async () => {
api.isLoggedIn = true
const { result, waitForNextUpdate } = renderHook(() => useChildList(), { wrapper })
const { result, waitForNextUpdate } = renderHook(() => useEtjanstChildren(), { wrapper })

await waitForNextUpdate()
await waitForNextUpdate()
@@ -100,7 +100,7 @@ describe('useChildList()', () => {
it('updates status to loaded', async () => {
await act(async () => {
api.isLoggedIn = true
const { result, waitForNextUpdate } = renderHook(() => useChildList(), { wrapper })
const { result, waitForNextUpdate } = renderHook(() => useEtjanstChildren(), { wrapper })

await waitForNextUpdate()
await waitForNextUpdate()
@@ -114,28 +114,28 @@ describe('useChildList()', () => {
api.isLoggedIn = true
api.isFake = false

const { waitForNextUpdate } = renderHook(() => useChildList(), { wrapper })
const { waitForNextUpdate } = renderHook(() => useEtjanstChildren(), { wrapper })

await waitForNextUpdate()
await waitForNextUpdate()
await waitForNextUpdate()
await pause(20)

expect(storage.cache['123_children']).toEqual('[{"id":1}]')
expect(storage.cache['123_etjanst_children']).toEqual('[{"id":1}]')
})
})
it('does not store in cache if fake', async () => {
await act(async () => {
api.isLoggedIn = true
api.isFake = true

const { waitForNextUpdate } = renderHook(() => useChildList(), { wrapper })
const { waitForNextUpdate } = renderHook(() => useEtjanstChildren(), { wrapper })

await waitForNextUpdate()
await waitForNextUpdate()
await pause(20)

expect(storage.cache['123_children']).toEqual('[{"id":2}]')
expect(storage.cache['123_etjanst_children']).toEqual('[{"id":2}]')
})
})
it('retries if api fails', async () => {
@@ -144,7 +144,7 @@ describe('useChildList()', () => {
const error = new Error('fail')
api.getChildren.mockRejectedValueOnce(error)

const { result, waitForNextUpdate } = renderHook(() => useChildList(), { wrapper })
const { result, waitForNextUpdate } = renderHook(() => useEtjanstChildren(), { wrapper })

await waitForNextUpdate()
await waitForNextUpdate()
@@ -172,7 +172,7 @@ describe('useChildList()', () => {
api.getChildren.mockRejectedValueOnce(error)
api.getChildren.mockRejectedValueOnce(error)

const { result, waitForNextUpdate } = renderHook(() => useChildList(), { wrapper })
const { result, waitForNextUpdate } = renderHook(() => useEtjanstChildren(), { wrapper })

await waitForNextUpdate()
await waitForNextUpdate()
@@ -199,15 +199,15 @@ describe('useChildList()', () => {
const error = new Error('fail')
api.getChildren.mockRejectedValueOnce(error)

const { result, waitForNextUpdate } = renderHook(() => useChildList(), { wrapper })
const { result, waitForNextUpdate } = renderHook(() => useEtjanstChildren(), { wrapper })

await waitForNextUpdate()
await waitForNextUpdate()
await waitForNextUpdate()

expect(result.current.error).toEqual(error)

expect(reporter.error).toHaveBeenCalledWith(error, 'Error getting CHILDREN from API')
expect(reporter.error).toHaveBeenCalledWith(error, 'Error getting ETJANST_CHILDREN from API')
})
})
})
213 changes: 213 additions & 0 deletions src/useSkola24Children.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import React from 'react'
import { renderHook, act } from '@testing-library/react-hooks'
import { ApiProvider } from './provider'
import { useSkola24Children } from './hooks'
import store from './store'
import init from './__mocks__/@skolplattformen/embedded-api'
import createStorage from './__mocks__/AsyncStorage'
import reporter from './__mocks__/reporter'

const pause = (ms = 0) => new Promise((r) => setTimeout(r, ms))

describe('useSkola24Children()', () => {
let api
let storage
let response
const wrapper = ({ children }) => (
<ApiProvider
api={api}
storage={storage}
reporter={reporter}
>
{children}
</ApiProvider>
)
beforeEach(() => {
response = [{ personGuid: '1' }]
api = init()
api.getPersonalNumber.mockReturnValue('123')
api.getSkola24Children.mockImplementation(() => (
new Promise((res) => {
setTimeout(() => res(response), 50)
})
))
storage = createStorage({
'123_skola24_children': [{ personGuid: '2' }],
}, 2)
})
afterEach(async () => {
await act(async () => {
await pause(70)
store.dispatch({ entity: 'ALL', type: 'CLEAR' })
})
})
it('returns correct initial value', () => {
const { result } = renderHook(() => useSkola24Children(), { wrapper })

expect(result.current.status).toEqual('pending')
})
it('calls api', async () => {
await act(async () => {
api.isLoggedIn = true
const { waitForNextUpdate } = renderHook(() => useSkola24Children(), { wrapper })

await waitForNextUpdate()
await waitForNextUpdate()

expect(api.getSkola24Children).toHaveBeenCalled()
})
})
it('only calls api once', async () => {
await act(async () => {
api.isLoggedIn = true
renderHook(() => useSkola24Children(), { wrapper })
const { waitForNextUpdate } = renderHook(() => useSkola24Children(), { wrapper })

await waitForNextUpdate()
renderHook(() => useSkola24Children(), { wrapper })
await waitForNextUpdate()
renderHook(() => useSkola24Children(), { wrapper })
await waitForNextUpdate()

const { result } = renderHook(() => useSkola24Children(), { wrapper })

expect(api.getSkola24Children).toHaveBeenCalledTimes(1)
expect(result.current.status).toEqual('loaded')
})
})
it('calls cache', async () => {
await act(async () => {
api.isLoggedIn = true
const { result, waitForNextUpdate } = renderHook(() => useSkola24Children(), { wrapper })

await waitForNextUpdate()
await waitForNextUpdate()

expect(result.current.data).toEqual([{ personGuid: '2' }])
})
})
it('updates status to loading', async () => {
await act(async () => {
api.isLoggedIn = true
const { result, waitForNextUpdate } = renderHook(() => useSkola24Children(), { wrapper })

await waitForNextUpdate()
await waitForNextUpdate()

expect(result.current.status).toEqual('loading')
})
})
it('updates status to loaded', async () => {
await act(async () => {
api.isLoggedIn = true
const { result, waitForNextUpdate } = renderHook(() => useSkola24Children(), { wrapper })

await waitForNextUpdate()
await waitForNextUpdate()
await waitForNextUpdate()

expect(result.current.status).toEqual('loaded')
})
})
it('stores in cache if not fake', async () => {
await act(async () => {
api.isLoggedIn = true
api.isFake = false

const { waitForNextUpdate } = renderHook(() => useSkola24Children(), { wrapper })

await waitForNextUpdate()
await waitForNextUpdate()
await waitForNextUpdate()
await pause(20)

expect(storage.cache['123_skola24_children']).toEqual('[{"personGuid":"1"}]')
})
})
it('does not store in cache if fake', async () => {
await act(async () => {
api.isLoggedIn = true
api.isFake = true

const { waitForNextUpdate } = renderHook(() => useSkola24Children(), { wrapper })

await waitForNextUpdate()
await waitForNextUpdate()
await pause(20)

expect(storage.cache['123_skola24_children']).toEqual('[{"personGuid":"2"}]')
})
})
it('retries if api fails', async () => {
await act(async () => {
api.isLoggedIn = true
const error = new Error('fail')
api.getSkola24Children.mockRejectedValueOnce(error)

const { result, waitForNextUpdate } = renderHook(() => useSkola24Children(), { wrapper })

await waitForNextUpdate()
await waitForNextUpdate()
await waitForNextUpdate()

expect(result.current.error).toEqual(error)
expect(result.current.status).toEqual('loading')
expect(result.current.data).toEqual([{ personGuid: '2' }])

jest.advanceTimersToNextTimer()

await waitForNextUpdate()
await waitForNextUpdate()
await waitForNextUpdate()

expect(result.current.status).toEqual('loaded')
expect(result.current.data).toEqual([{ personGuid: '1' }])
})
})
it('gives up after 3 retries', async () => {
await act(async () => {
api.isLoggedIn = true
const error = new Error('fail')
api.getSkola24Children.mockRejectedValueOnce(error)
api.getSkola24Children.mockRejectedValueOnce(error)
api.getSkola24Children.mockRejectedValueOnce(error)

const { result, waitForNextUpdate } = renderHook(() => useSkola24Children(), { wrapper })

await waitForNextUpdate()
await waitForNextUpdate()
await waitForNextUpdate()

expect(result.current.error).toEqual(error)
expect(result.current.status).toEqual('loading')
expect(result.current.data).toEqual([{ personGuid: '2' }])

jest.advanceTimersToNextTimer()

await waitForNextUpdate()
await waitForNextUpdate()
await waitForNextUpdate()

expect(result.current.error).toEqual(error)
expect(result.current.status).toEqual('error')
expect(result.current.data).toEqual([{ personGuid: '2' }])
})
})
it('reports if api fails', async () => {
await act(async () => {
api.isLoggedIn = true
const error = new Error('fail')
api.getSkola24Children.mockRejectedValueOnce(error)

const { result, waitForNextUpdate } = renderHook(() => useSkola24Children(), { wrapper })

await waitForNextUpdate()
await waitForNextUpdate()
await waitForNextUpdate()

expect(result.current.error).toEqual(error)

expect(reporter.error).toHaveBeenCalledWith(error, 'Error getting SKOLA24_CHILDREN from API')
})
})
})
219 changes: 219 additions & 0 deletions src/useTimetable.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
import React from 'react'
import { renderHook, act } from '@testing-library/react-hooks'
import { ApiProvider } from './provider'
import { useTimetable } from './hooks'
import store from './store'
import init from './__mocks__/@skolplattformen/embedded-api'
import createStorage from './__mocks__/AsyncStorage'
import reporter from './__mocks__/reporter'

const pause = (ms = 0) => new Promise((r) => setTimeout(r, ms))

describe('useTimetable(child, week, year)', () => {
let api
let storage
let response
let child
let week
let year
const wrapper = ({ children }) => (
<ApiProvider
api={api}
storage={storage}
reporter={reporter}
>
{children}
</ApiProvider>
)
beforeEach(() => {
response = [{ id: 1 }]
api = init()
api.getPersonalNumber.mockReturnValue('123')
api.getTimetable.mockImplementation(() => (
new Promise((res) => {
setTimeout(() => res(response), 50)
})
))
storage = createStorage({
'123_timetable_10_15_2021': [{ id: 2 }],
}, 2)
child = { personGuid: '10' }
week = 15
year = 2021
})
afterEach(async () => {
await act(async () => {
await pause(70)
store.dispatch({ entity: 'ALL', type: 'CLEAR' })
})
})
it('returns correct initial value', () => {
const { result } = renderHook(() => useTimetable(child, week, year), { wrapper })

expect(result.current.status).toEqual('pending')
})
it('calls api', async () => {
await act(async () => {
api.isLoggedIn = true
const { waitForNextUpdate } = renderHook(() => useTimetable(child, week, year), { wrapper })

await waitForNextUpdate()
await waitForNextUpdate()

expect(api.getTimetable).toHaveBeenCalled()
})
})
it('only calls api once', async () => {
await act(async () => {
api.isLoggedIn = true
renderHook(() => useTimetable(child, week, year), { wrapper })
const { waitForNextUpdate } = renderHook(() => useTimetable(child, week, year), { wrapper })

await waitForNextUpdate()
renderHook(() => useTimetable(child, week, year), { wrapper })
await waitForNextUpdate()
renderHook(() => useTimetable(child, week, year), { wrapper })
await waitForNextUpdate()

const { result } = renderHook(() => useTimetable(child, week, year), { wrapper })

expect(api.getTimetable).toHaveBeenCalledTimes(1)
expect(result.current.status).toEqual('loaded')
})
})
it('calls cache', async () => {
await act(async () => {
api.isLoggedIn = true
const { result, waitForNextUpdate } = renderHook(() => useTimetable(child, week, year), { wrapper })

await waitForNextUpdate()
await waitForNextUpdate()

expect(result.current.data).toEqual([{ id: 2 }])
})
})
it('updates status to loading', async () => {
await act(async () => {
api.isLoggedIn = true
const { result, waitForNextUpdate } = renderHook(() => useTimetable(child, week, year), { wrapper })

await waitForNextUpdate()
await waitForNextUpdate()

expect(result.current.status).toEqual('loading')
})
})
it('updates status to loaded', async () => {
await act(async () => {
api.isLoggedIn = true
const { result, waitForNextUpdate } = renderHook(() => useTimetable(child, week, year), { wrapper })

await waitForNextUpdate()
await waitForNextUpdate()
await waitForNextUpdate()

expect(result.current.status).toEqual('loaded')
})
})
it('stores in cache if not fake', async () => {
await act(async () => {
api.isLoggedIn = true
api.isFake = false

const { waitForNextUpdate } = renderHook(() => useTimetable(child, week, year), { wrapper })

await waitForNextUpdate()
await waitForNextUpdate()
await waitForNextUpdate()
await pause(20)

expect(storage.cache['123_timetable_10_15_2021']).toEqual('[{"id":1}]')
})
})
it('does not store in cache if fake', async () => {
await act(async () => {
api.isLoggedIn = true
api.isFake = true

const { waitForNextUpdate } = renderHook(() => useTimetable(child, week, year), { wrapper })

await waitForNextUpdate()
await waitForNextUpdate()
await pause(20)

expect(storage.cache['123_timetable_10_15_2021']).toEqual('[{"id":2}]')
})
})
it('retries if api fails', async () => {
await act(async () => {
api.isLoggedIn = true
const error = new Error('fail')
api.getTimetable.mockRejectedValueOnce(error)

const { result, waitForNextUpdate } = renderHook(() => useTimetable(child, week, year), { wrapper })

await waitForNextUpdate()
await waitForNextUpdate()
await waitForNextUpdate()

expect(result.current.error).toEqual(error)
expect(result.current.status).toEqual('loading')
expect(result.current.data).toEqual([{ id: 2 }])

jest.advanceTimersToNextTimer()

await waitForNextUpdate()
await waitForNextUpdate()
await waitForNextUpdate()

expect(result.current.status).toEqual('loaded')
expect(result.current.data).toEqual([{ id: 1 }])
})
})
it('gives up after 3 retries', async () => {
await act(async () => {
api.isLoggedIn = true
const error = new Error('fail')
api.getTimetable.mockRejectedValueOnce(error)
api.getTimetable.mockRejectedValueOnce(error)
api.getTimetable.mockRejectedValueOnce(error)

const { result, waitForNextUpdate } = renderHook(() => useTimetable(child, week, year), { wrapper })

await waitForNextUpdate()
await waitForNextUpdate()
await waitForNextUpdate()

expect(result.current.error).toEqual(error)
expect(result.current.status).toEqual('loading')
expect(result.current.data).toEqual([{ id: 2 }])

jest.advanceTimersToNextTimer()

await waitForNextUpdate()
await waitForNextUpdate()
await waitForNextUpdate()

expect(result.current.error).toEqual(error)
expect(result.current.status).toEqual('error')
expect(result.current.data).toEqual([{ id: 2 }])
})
})
it('reports if api fails', async () => {
await act(async () => {
api.isLoggedIn = true
const error = new Error('fail')
api.getTimetable.mockRejectedValueOnce(error)

const { result, waitForNextUpdate } = renderHook(() => useTimetable(child, week, year), { wrapper })

await waitForNextUpdate()
await waitForNextUpdate()
await waitForNextUpdate()

expect(result.current.error).toEqual(error)

expect(reporter.error).toHaveBeenCalledWith(error, 'Error getting TIMETABLE from API')
})
})
})
526 changes: 376 additions & 150 deletions yarn.lock

Large diffs are not rendered by default.