-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Write tests for lazyResource and clearResourceErrors
- Loading branch information
Showing
1 changed file
with
340 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,25 +1,349 @@ | ||
import { lazyResource } from '.'; | ||
import { clearResourceErrors, lazyResource } from '.'; | ||
|
||
it('allows creating a lazy resource', async () => { | ||
const resource = lazyResource(() => Promise.resolve('hello')); | ||
beforeEach(() => jest.useFakeTimers()); | ||
|
||
expect(resource.get()).toBe(null); | ||
const aSuccessLoader = () => { | ||
const result = { success: true }; | ||
const promise = Promise.resolve(result); | ||
const loader = jest.fn(() => promise); | ||
|
||
const promise = resource.load(); | ||
return { | ||
result, | ||
loader, | ||
}; | ||
}; | ||
|
||
// toThrow does not work with promises | ||
let thrownValue: unknown = null; | ||
const anErrorLoader = () => { | ||
const error = new Error('Failed to load'); | ||
const promise = Promise.reject(error); | ||
const loader = jest.fn(() => promise); | ||
|
||
try { | ||
resource.read(); | ||
} catch (value) { | ||
thrownValue = value; | ||
} | ||
return { | ||
error, | ||
loader, | ||
}; | ||
}; | ||
|
||
expect(thrownValue).toBe(promise); | ||
const anErrorThenSuccessLoader = () => { | ||
const result = { success: true }; | ||
const error = new Error('Failed to load'); | ||
const resolvedPromise = Promise.resolve(result); | ||
const rejectedPromise = Promise.reject(error); | ||
const loader = jest.fn(() => resolvedPromise); | ||
|
||
await promise; | ||
loader.mockReturnValueOnce(rejectedPromise); | ||
|
||
expect(resource.read()).toBe('hello'); | ||
expect(resource.get()).toBe('hello'); | ||
return { | ||
result, | ||
error, | ||
loader, | ||
}; | ||
}; | ||
|
||
describe('lazyResource', () => { | ||
describe('get', () => { | ||
it('returns null while the resource has not loaded', async () => { | ||
const { loader } = aSuccessLoader(); | ||
|
||
const resource = lazyResource(loader); | ||
|
||
expect(resource.get()).toBe(null); | ||
|
||
resource.load(); | ||
|
||
expect(resource.get()).toBe(null); | ||
}); | ||
|
||
it('returns the result when the resource loaded', async () => { | ||
const { loader, result } = aSuccessLoader(); | ||
|
||
const resource = lazyResource(loader); | ||
|
||
await resource.load(); | ||
|
||
expect(resource.get()).toBe(result); | ||
}); | ||
|
||
it('returns null when the resource fails to loaded', async () => { | ||
const { loader } = anErrorLoader(); | ||
|
||
const resource = lazyResource(loader); | ||
|
||
try { | ||
await resource.load(); | ||
} catch {} | ||
|
||
expect(resource.get()).toBe(null); | ||
}); | ||
}); | ||
|
||
describe('read', () => { | ||
it('throws an invariant violation error if called before the resource starts loading', async () => { | ||
const { loader } = aSuccessLoader(); | ||
|
||
const resource = lazyResource(loader); | ||
|
||
expect(() => resource.read()).toThrowErrorMatchingInlineSnapshot( | ||
`"Invariant violation. You must start loading the resource with resource.load() before trying to read it with resource.read()"`, | ||
); | ||
}); | ||
|
||
it('throws the resource promise while the resource is loading', async () => { | ||
const { loader } = aSuccessLoader(); | ||
|
||
const resource = lazyResource(loader); | ||
|
||
const promise = resource.load(); | ||
|
||
// toThrow does not work with promises | ||
let thrownValue: unknown = null; | ||
|
||
try { | ||
resource.read(); | ||
} catch (value) { | ||
thrownValue = value; | ||
} | ||
|
||
expect(thrownValue).toBe(promise); | ||
}); | ||
|
||
it('returns the result when the resource loaded', async () => { | ||
const { loader, result } = aSuccessLoader(); | ||
|
||
const resource = lazyResource(loader); | ||
|
||
await resource.load(); | ||
|
||
expect(resource.read()).toBe(result); | ||
}); | ||
|
||
it('throws the promise rejection error when the resource fails to loaded', async () => { | ||
const { loader, error } = anErrorLoader(); | ||
|
||
const resource = lazyResource(loader); | ||
|
||
try { | ||
await resource.load(); | ||
} catch {} | ||
|
||
expect(() => resource.read()).toThrow(error); | ||
}); | ||
}); | ||
|
||
describe('load', () => { | ||
it('calls the loader', async () => { | ||
const { loader } = aSuccessLoader(); | ||
|
||
const resource = lazyResource(loader); | ||
|
||
resource.load(); | ||
|
||
expect(loader).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('returns a promise that resolves with the result if the loader promise resolves', async () => { | ||
const { loader, result } = aSuccessLoader(); | ||
|
||
const resource = lazyResource(loader); | ||
|
||
const promise = resource.load(); | ||
|
||
const value = await promise; | ||
|
||
expect(value).toBe(result); | ||
}); | ||
|
||
it('returns a promise that rejects with the rejection error if the loader promise rejects', async () => { | ||
expect.assertions(1); | ||
|
||
const { loader, error } = anErrorLoader(); | ||
|
||
const resource = lazyResource(loader); | ||
|
||
try { | ||
await resource.load(); | ||
} catch (err) { | ||
expect(err).toBe(error); | ||
} | ||
}); | ||
|
||
it('calls the loader one time even if called multiple times', async () => { | ||
const { loader } = aSuccessLoader(); | ||
|
||
const resource = lazyResource(loader); | ||
|
||
const promise1 = resource.load(); | ||
const promise2 = resource.load(); | ||
|
||
expect(loader).toHaveBeenCalledTimes(1); | ||
expect(promise1).toBe(promise2); | ||
}); | ||
|
||
it('returns the same promise after the resource loaded', async () => { | ||
const { loader } = aSuccessLoader(); | ||
|
||
const resource = lazyResource(loader); | ||
|
||
const promise1 = resource.load(); | ||
|
||
await promise1; | ||
|
||
const promise2 = resource.load(); | ||
|
||
expect(loader).toHaveBeenCalledTimes(1); | ||
expect(promise1).toBe(promise2); | ||
}); | ||
|
||
it('returns the same promise after the resource fails to load', async () => { | ||
const { loader } = anErrorLoader(); | ||
|
||
const resource = lazyResource(loader); | ||
|
||
const promise1 = resource.load(); | ||
|
||
try { | ||
await promise1; | ||
} catch {} | ||
|
||
const promise2 = resource.load(); | ||
|
||
expect(loader).toHaveBeenCalledTimes(1); | ||
expect(promise1).toBe(promise2); | ||
}); | ||
|
||
it('auto-retries calling the loader if autoRetry is true and the promise rejects', async () => { | ||
const { loader, result } = anErrorThenSuccessLoader(); | ||
|
||
const resource = lazyResource(loader, { autoRetry: true }); | ||
|
||
const promise = resource.load(); | ||
|
||
// Allow the loader promise to reject | ||
await Promise.resolve(); | ||
|
||
jest.advanceTimersByTime(2000); | ||
|
||
await Promise.resolve(); | ||
|
||
expect(loader).toHaveBeenCalledTimes(1); | ||
|
||
jest.advanceTimersByTime(1000); | ||
|
||
await Promise.resolve(); | ||
|
||
expect(loader).toHaveBeenCalledTimes(2); | ||
|
||
const value = await promise; | ||
|
||
expect(value).toBe(result); | ||
}); | ||
|
||
it('auto-retries calling the loader if autoRetry is true, respecting the autoRetryTimeoutMs', async () => { | ||
const { loader, result } = anErrorThenSuccessLoader(); | ||
|
||
const resource = lazyResource(loader, { | ||
autoRetry: true, | ||
autoRetryTimeoutMs: 10000, | ||
}); | ||
|
||
const promise = resource.load(); | ||
|
||
// Allow the loader promise to reject and set the timer | ||
await Promise.resolve(); | ||
|
||
jest.advanceTimersByTime(5000); | ||
|
||
await Promise.resolve(); | ||
|
||
expect(loader).toHaveBeenCalledTimes(1); | ||
|
||
jest.advanceTimersByTime(5000); | ||
|
||
await Promise.resolve(); | ||
|
||
expect(loader).toHaveBeenCalledTimes(2); | ||
|
||
const value = await promise; | ||
|
||
expect(value).toBe(result); | ||
}); | ||
}); | ||
|
||
describe('clearError', () => { | ||
it('has no effect before the resource even starts loading', async () => { | ||
expect.assertions(1); | ||
|
||
const { loader, error } = anErrorLoader(); | ||
|
||
const resource = lazyResource(loader); | ||
|
||
resource.clearError(); | ||
|
||
try { | ||
await resource.load(); | ||
} catch (err) { | ||
expect(err).toBe(error); | ||
} | ||
}); | ||
|
||
it('has no effect after the resource loaded successfully', async () => { | ||
const { loader, result } = aSuccessLoader(); | ||
|
||
const resource = lazyResource(loader); | ||
|
||
await resource.load(); | ||
|
||
resource.clearError(); | ||
|
||
expect(resource.get()).toBe(result); | ||
|
||
await resource.load(); | ||
|
||
expect(loader).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('clears the error if called after the resource failed to load', async () => { | ||
expect.assertions(3); | ||
|
||
const { loader, error, result } = anErrorThenSuccessLoader(); | ||
|
||
const resource = lazyResource(loader); | ||
|
||
try { | ||
await resource.load(); | ||
} catch (err) { | ||
expect(err).toBe(error); | ||
} | ||
|
||
resource.clearError(); | ||
|
||
const value = await resource.load(); | ||
|
||
expect(value).toBe(result); | ||
expect(loader).toHaveBeenCalledTimes(2); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('clearResourceErrors', () => { | ||
it('clears resource errors', async () => { | ||
expect.assertions(3); | ||
|
||
const { loader, error, result } = anErrorThenSuccessLoader(); | ||
|
||
const resource = lazyResource(loader); | ||
|
||
try { | ||
await resource.load(); | ||
} catch (err) { | ||
expect(err).toBe(error); | ||
} | ||
|
||
clearResourceErrors(); | ||
|
||
const value = await resource.load(); | ||
|
||
expect(value).toBe(result); | ||
expect(loader).toHaveBeenCalledTimes(2); | ||
}); | ||
}); |