Skip to content

Commit

Permalink
feat(data-loaders): expected errors
Browse files Browse the repository at this point in the history
  • Loading branch information
posva committed Jul 9, 2024
1 parent f0cf9b6 commit a470a51
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 19 deletions.
6 changes: 6 additions & 0 deletions src/data-loaders/createDataLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ export interface DefineDataLoaderOptionsBase<isLazy extends boolean> {
* @defaultValue `'after-load'`
*/
commit?: DefineDataLoaderCommit

/**
* List of _expected_ errors that shouldn't abort the navigation (for non-lazy loaders). Provide a list of
* constructors that can be checked with `instanceof`.
*/
errors?: Array<new (...args: any) => any>
}

/**
Expand Down
37 changes: 19 additions & 18 deletions src/data-loaders/navigation-guard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
setCurrentContext,
DataLoaderPlugin,
NavigationResult,
DataLoaderPluginOptions,
} from 'unplugin-vue-router/runtime'
import { mockPromise } from '../../tests/utils'
import {
Expand Down Expand Up @@ -47,7 +48,7 @@ function mockedLoader<T = string | NavigationResult>(
describe('navigation-guard', () => {
let globalApp: App | undefined

function setupApp(isSSR: boolean) {
function setupApp({ isSSR }: Omit<DataLoaderPluginOptions, 'router'>) {
const app = createApp({ render: () => null })
const selectNavigationResult = vi
.fn()
Expand Down Expand Up @@ -90,7 +91,7 @@ describe('navigation-guard', () => {
const loader3 = defineBasicLoader(async () => {})

it('creates a set of loaders during navigation', async () => {
setupApp(false)
setupApp({ isSSR: false })
const router = getRouter()
router.addRoute({
name: '_test',
Expand All @@ -104,7 +105,7 @@ describe('navigation-guard', () => {
})

it('collects loaders from the matched route', async () => {
setupApp(false)
setupApp({ isSSR: false })
const router = getRouter()
router.addRoute({
name: '_test',
Expand All @@ -131,7 +132,7 @@ describe('navigation-guard', () => {
})

it('collect loaders from nested routes', async () => {
setupApp(false)
setupApp({ isSSR: false })
const router = getRouter()
router.addRoute({
name: '_test',
Expand All @@ -157,7 +158,7 @@ describe('navigation-guard', () => {
})

it('collects all loaders from lazy loaded pages', async () => {
setupApp(false)
setupApp({ isSSR: false })
const router = getRouter()
router.addRoute({
name: '_test',
Expand All @@ -171,7 +172,7 @@ describe('navigation-guard', () => {
})

it('awaits for all loaders to be resolved', async () => {
setupApp(false)
setupApp({ isSSR: false })
const router = getRouter()
const l1 = mockedLoader()
const l2 = mockedLoader()
Expand All @@ -195,7 +196,7 @@ describe('navigation-guard', () => {
})

it('does not await for lazy loaders on client-side navigation', async () => {
setupApp(false)
setupApp({ isSSR: false })
const router = getRouter()
const l1 = mockedLoader({ lazy: true })
const l2 = mockedLoader({ lazy: false })
Expand All @@ -220,7 +221,7 @@ describe('navigation-guard', () => {
})

it('awaits for lazy loaders on server-side navigation', async () => {
setupApp(true)
setupApp({ isSSR: true })
const router = getRouter()
const l1 = mockedLoader({ lazy: true })
const l2 = mockedLoader({ lazy: false })
Expand All @@ -246,7 +247,7 @@ describe('navigation-guard', () => {
})

it('does not run loaders on server side if server: false', async () => {
setupApp(true)
setupApp({ isSSR: true })
const router = getRouter()
const l1 = mockedLoader({ lazy: true, server: false })
const l2 = mockedLoader({ lazy: false, server: false })
Expand All @@ -268,7 +269,7 @@ describe('navigation-guard', () => {
it.each([true, false] as const)(
'throws if a non lazy loader rejects, isSSR: %s',
async (isSSR) => {
setupApp(isSSR)
setupApp({ isSSR })
const router = getRouter()
const l1 = mockedLoader({ lazy: false })
router.addRoute({
Expand All @@ -289,7 +290,7 @@ describe('navigation-guard', () => {
)

it('does not throw if a lazy loader rejects', async () => {
setupApp(false)
setupApp({ isSSR: false })
const router = getRouter()
const l1 = mockedLoader({ lazy: true })
router.addRoute({
Expand All @@ -309,7 +310,7 @@ describe('navigation-guard', () => {
})

it('throws if a lazy loader rejects on server-side', async () => {
setupApp(true)
setupApp({ isSSR: true })
const router = getRouter()
const l1 = mockedLoader({ lazy: true })
router.addRoute({
Expand All @@ -334,7 +335,7 @@ describe('navigation-guard', () => {

describe('signal', () => {
it('aborts the signal if the navigation throws', async () => {
setupApp(false)
setupApp({ isSSR: false })
const router = getRouter()

router.setNextGuardReturn(new Error('canceled'))
Expand All @@ -352,7 +353,7 @@ describe('navigation-guard', () => {
})

it('aborts the signal if the navigation is canceled', async () => {
setupApp(false)
setupApp({ isSSR: false })
const router = getRouter()

router.setNextGuardReturn(false)
Expand All @@ -376,7 +377,7 @@ describe('navigation-guard', () => {

describe('selectNavigationResult', () => {
it('can change the navigation result within a loader', async () => {
const { selectNavigationResult } = setupApp(false)
const { selectNavigationResult } = setupApp({ isSSR: false })
const router = getRouter()
const l1 = mockedLoader()
router.addRoute({
Expand All @@ -397,7 +398,7 @@ describe('navigation-guard', () => {
})

it('selectNavigationResult is called with an array of all the results returned by the loaders', async () => {
const { selectNavigationResult } = setupApp(false)
const { selectNavigationResult } = setupApp({ isSSR: false })
const router = getRouter()
const l1 = mockedLoader()
const l2 = mockedLoader()
Expand All @@ -424,7 +425,7 @@ describe('navigation-guard', () => {
})

it('can change the navigation result returned by multiple loaders', async () => {
const { selectNavigationResult } = setupApp(false)
const { selectNavigationResult } = setupApp({ isSSR: false })
const router = getRouter()
const l1 = mockedLoader()
const l2 = mockedLoader()
Expand All @@ -449,7 +450,7 @@ describe('navigation-guard', () => {
})

it('immediately stops if a NavigationResult is thrown instead of returned inside the loader', async () => {
const { selectNavigationResult } = setupApp(false)
const { selectNavigationResult } = setupApp({ isSSR: false })
const router = getRouter()
const l1 = mockedLoader()
router.addRoute({
Expand Down
17 changes: 16 additions & 1 deletion src/data-loaders/navigation-guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export function setupLoaderGuard({
app,
effect,
isSSR,
errors = [],
selectNavigationResult = (results) => results[0]!.value,
}: SetupLoaderGuardOptions) {
// avoid creating the guards multiple times
Expand Down Expand Up @@ -162,7 +163,15 @@ export function setupLoaderGuard({
return !isSSR && lazy
? undefined
: // return the non-lazy loader to commit changes after all loaders are done
ret
ret.catch((reason) =>
// Check if the error is an expected error to discard it
loader._.options.errors?.some((Err) => reason instanceof Err) ||
(Array.isArray(errors)
? errors.some((Err) => reason instanceof Err)
: errors(reason))
? undefined
: Promise.reject(reason)
)
})
) // let the navigation go through by returning true or void
.then(() => {
Expand Down Expand Up @@ -361,4 +370,10 @@ export interface DataLoaderPluginOptions {
selectNavigationResult?: (
results: NavigationResult[]
) => _Awaitable<Exclude<NavigationGuardReturn, Function | Promise<unknown>>>

/**
* List of _expected_ errors that shouldn't abort the navigation (for non-lazy loaders). Provide a list of
* constructors that can be checked with `instanceof` or a custom function that returns `true` for expected errors.
*/
errors?: Array<new (...args: any) => any> | ((reason?: unknown) => boolean)
}
88 changes: 88 additions & 0 deletions tests/data-loaders/tester.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,94 @@ export function testDefineLoader<Context = void>(
expect(router.currentRoute.value.path).toBe('/fetch')
})

describe('custom errors', () => {
class CustomError extends Error {
override name = 'CustomError'
}
it('should complete the navigation if a non-lazy loader throws an expected error', async () => {
const { wrapper, useData, router } = singleLoaderOneRoute(
loaderFactory({
fn: async () => {
throw new CustomError()
},
lazy: false,
commit,
errors: [CustomError],
})
)
await router.push('/fetch')
const { data, error, isLoading } = useData()
expect(wrapper.get('#error').text()).toBe('CustomError')
expect(isLoading.value).toBe(false)
expect(data.value).toBe(undefined)
expect(error.value).toBeInstanceOf(CustomError)
expect(router.currentRoute.value.path).toBe('/fetch')
})

it('should complete the navigation if a non-lazy loader throws an expected global error', async () => {
const { wrapper, useData, router } = singleLoaderOneRoute(
loaderFactory({
fn: async () => {
throw new CustomError()
},
lazy: false,
commit,
// errors: [],
}),
{ errors: [CustomError] }
)
await router.push('/fetch')
const { data, error, isLoading } = useData()
expect(wrapper.get('#error').text()).toBe('CustomError')
expect(isLoading.value).toBe(false)
expect(data.value).toBe(undefined)
expect(error.value).toBeInstanceOf(CustomError)
expect(router.currentRoute.value.path).toBe('/fetch')
})

it('accepts a function as a global setting instead of a constructor', async () => {
const { wrapper, useData, router } = singleLoaderOneRoute(
loaderFactory({
fn: async () => {
throw new CustomError()
},
lazy: false,
commit,
// errors: [],
}),
{ errors: (reason) => reason instanceof CustomError }
)
await router.push('/fetch')
const { data, error, isLoading } = useData()
expect(wrapper.get('#error').text()).toBe('CustomError')
expect(isLoading.value).toBe(false)
expect(data.value).toBe(undefined)
expect(error.value).toBeInstanceOf(CustomError)
expect(router.currentRoute.value.path).toBe('/fetch')
})

it('local errors take priority over a global function that returns false', async () => {
const { wrapper, useData, router } = singleLoaderOneRoute(
loaderFactory({
fn: async () => {
throw new CustomError()
},
lazy: false,
commit,
errors: [CustomError],
}),
{ errors: () => false }
)
await router.push('/fetch')
const { data, error, isLoading } = useData()
expect(wrapper.get('#error').text()).toBe('CustomError')
expect(isLoading.value).toBe(false)
expect(data.value).toBe(undefined)
expect(error.value).toBeInstanceOf(CustomError)
expect(router.currentRoute.value.path).toBe('/fetch')
})
})

it('propagates errors from nested loaders', async () => {
const l1 = mockedLoader({
key: 'nested',
Expand Down

0 comments on commit a470a51

Please sign in to comment.