diff --git a/src/utilities/createRouter.ts b/src/utilities/createRouter.ts index 98746826..f26d7e4a 100644 --- a/src/utilities/createRouter.ts +++ b/src/utilities/createRouter.ts @@ -1,14 +1,10 @@ import { readonly } from 'vue' import { Resolved, Route, RouteMethods, Routes } from '@/types' -import { createRouteMethods, createRouterNavigation, resolveRoutes } from '@/utilities' +import { createRouteMethods, createRouterNavigation, resolveRoutes, routeMatch } from '@/utilities' import { resolveRoutesRegex } from '@/utilities/resolveRoutesRegex' -import { routeMatch } from '@/utilities/routeMatch' -import { updateBrowserUrl } from '@/utilities/updateBrowserUrl' type RouterPush = (url: string, options?: { replace: boolean }) => Promise type RouterReplace = (url: string) => Promise -type RouterBackForward = () => Promise -type RouterGo = (delta: number) => Promise export type Router< TRoutes extends Routes @@ -17,43 +13,37 @@ export type Router< route: Readonly>, push: RouterPush, replace: RouterReplace, - back: RouterBackForward, - forward: RouterBackForward, - go: RouterGo, + back: () => void, + forward: () => void, + go: (delta: number) => void, } export function createRouter(routes: T): Router { const resolved = resolveRoutes(routes) const resolvedWithRegex = resolveRoutesRegex(resolved) - const routerNavigation = createRouterNavigation() + const navigation = createRouterNavigation({ + onLocationUpdate, + }) // todo: implement this const route: Router['route'] = readonly({} as any) - const push: RouterPush = async (url, options) => { + async function onLocationUpdate(url: string): Promise { const match = routeMatch(resolvedWithRegex, url) if (!match) { - return updateBrowserUrl(url, options) + throw 'not implemented' } - await routerNavigation.update(url, options) - } - - const replace: RouterReplace = (url) => { - return push(url, { replace: true }) + throw 'not implemented' } - const forward: RouterBackForward = async () => { - await routerNavigation.forward() - } - - const back: RouterBackForward = async () => { - await routerNavigation.back() + const push: RouterPush = async (url, options) => { + await navigation.update(url, options) } - const go: RouterGo = async (delta) => { - await routerNavigation.go(delta) + const replace: RouterReplace = async (url) => { + await navigation.update(url, { replace: true }) } const router = { @@ -61,9 +51,9 @@ export function createRouter(routes: T): Router { route, push, replace, - forward, - back, - go, + forward: navigation.forward, + back: navigation.back, + go: navigation.go, } return router diff --git a/src/utilities/routerNavigation.browser.spec.ts b/src/utilities/routerNavigation.browser.spec.ts index e3b06802..86da4b15 100644 --- a/src/utilities/routerNavigation.browser.spec.ts +++ b/src/utilities/routerNavigation.browser.spec.ts @@ -7,8 +7,9 @@ describe('createRouterNavigation', () => { test('when go is called, forwards call to window history', () => { vi.spyOn(window.history, 'go') + const onLocationUpdate = vi.fn() const delta = random.number({ min: 0, max: 100 }) - const history = createRouterNavigation() + const history = createRouterNavigation({ onLocationUpdate }) history.go(delta) @@ -18,7 +19,8 @@ describe('createRouterNavigation', () => { test('when back is called, forwards call to window history', () => { vi.spyOn(window.history, 'back') - const history = createRouterNavigation() + const onLocationUpdate = vi.fn() + const history = createRouterNavigation({ onLocationUpdate }) history.back() @@ -28,7 +30,8 @@ describe('createRouterNavigation', () => { test('when forward is called, forwards call to window history', () => { vi.spyOn(window.history, 'forward') - const history = createRouterNavigation() + const onLocationUpdate = vi.fn() + const history = createRouterNavigation({ onLocationUpdate }) history.forward() @@ -38,11 +41,36 @@ describe('createRouterNavigation', () => { test('when update is called, calls updateBrowserUrl', () => { vi.spyOn(utilities, 'updateBrowserUrl') + const onLocationUpdate = vi.fn() const url = random.number().toString() - const history = createRouterNavigation() + const history = createRouterNavigation({ onLocationUpdate }) history.update(url) - expect(utilities.updateBrowserUrl).toHaveBeenCalledWith(url) + expect(utilities.updateBrowserUrl).toHaveBeenCalledWith(url, undefined) + }) + + test('when update is called and same origin calls onLocationUpdate', async () => { + vi.spyOn(utilities, 'isSameOrigin').mockReturnValue(true) + + const onLocationUpdate = vi.fn() + const url = random.number().toString() + const history = createRouterNavigation({ onLocationUpdate }) + + await history.update(url) + + expect(onLocationUpdate).toHaveBeenCalledWith(url) + }) + + test('when update is called and not same origin does not call onLocationUpdate ', () => { + const onLocationUpdate = vi.fn() + vi.spyOn(utilities, 'isSameOrigin').mockReturnValue(true) + + const url = random.number().toString() + const history = createRouterNavigation({ onLocationUpdate }) + + history.update(url) + + expect(onLocationUpdate).not.toHaveBeenCalled() }) }) \ No newline at end of file diff --git a/src/utilities/routerNavigation.spec.ts b/src/utilities/routerNavigation.spec.ts index ea207d50..98de68fc 100644 --- a/src/utilities/routerNavigation.spec.ts +++ b/src/utilities/routerNavigation.spec.ts @@ -1,8 +1,24 @@ -import { describe, expect, test } from 'vitest' +import { describe, expect, test, vi } from 'vitest' import { createRouterNavigation } from '@/utilities/routerNavigation' +import { random } from '@/utilities/testHelpers' describe('createRouterNavigation', () => { - test('is not implemented, and throws exception', () => { - expect(() => createRouterNavigation()).toThrowError('not implemented') + test('Browser like navigation is not supported', () => { + const onLocationUpdate = vi.fn() + const navigation = createRouterNavigation({ onLocationUpdate }) + + expect(() => navigation.back()).toThrowError() + expect(() => navigation.forward()).toThrowError() + expect(() => navigation.go(1)).toThrowError() + }) + + test('when update is called and same origin calls onLocationUpdate', () => { + const onLocationUpdate = vi.fn() + const url = random.number().toString() + const history = createRouterNavigation({ onLocationUpdate }) + + history.update(url) + + expect(onLocationUpdate).toHaveBeenCalledWith(url) }) }) \ No newline at end of file diff --git a/src/utilities/routerNavigation.ts b/src/utilities/routerNavigation.ts index f0fee5a4..b0a2ebb2 100644 --- a/src/utilities/routerNavigation.ts +++ b/src/utilities/routerNavigation.ts @@ -1,30 +1,79 @@ import { isBrowser } from '@/utilities/isBrowser' import { updateBrowserUrl } from '@/utilities/updateBrowserUrl' +type RouterNavigationOptions = { + onLocationUpdate: (url: string) => Promise, +} + type RouterNavigationUpdateOptions = { replace?: boolean, } +type NavigationForward = () => void +type NavigationBack = () => void +type NavigationGo = (delta: number) => void +type NavigationUpdate = (url: string, options?: RouterNavigationUpdateOptions) => Promise +type NavigationCleanup = () => void + type RouterNavigation = { - forward: () => void, - back: () => void, - go: (delta: number) => void, - update: (url: string, options?: RouterNavigationUpdateOptions) => void, + forward: NavigationForward, + back: NavigationBack, + go: NavigationGo, + update: NavigationUpdate, + cleanup?: () => void, } -export function createRouterNavigation(): RouterNavigation { - return isBrowser() ? createWebNavigation() : createMemoryNavigation() +export function createRouterNavigation(options: RouterNavigationOptions): RouterNavigation { + if (isBrowser()) { + return createBrowserNavigation(options) + } + + return createNodeNavigation(options) } -function createWebNavigation(): RouterNavigation { +function createBrowserNavigation({ onLocationUpdate }: RouterNavigationOptions): RouterNavigation { + + const update: NavigationUpdate = async (url, options) => { + await updateBrowserUrl(url, options) + + return await onLocationUpdate(url) + } + + const cleanup: NavigationCleanup = () => { + removeEventListener('popstate', onPopstate) + } + + const onPopstate = (): void => { + onLocationUpdate(window.location.toString()) + } + + addEventListener('popstate', onPopstate) + return { - go: window.history.go, - back: window.history.back, - forward: window.history.forward, - update: updateBrowserUrl, + forward: history.forward, + back: history.back, + go: history.go, + update, + cleanup, } } -function createMemoryNavigation(): RouterNavigation { - throw 'not implemented' -} \ No newline at end of file +function createNodeNavigation({ onLocationUpdate }: RouterNavigationOptions): RouterNavigation { + const notSupported = (): void => { + throw new Error('Browser like navigation is not supported outside of a browser context') + } + + const update: NavigationUpdate = async (url) => { + return await onLocationUpdate(url) + } + + const cleanup: NavigationCleanup = () => {} + + return { + forward: notSupported, + back: notSupported, + go: notSupported, + cleanup, + update, + } +} diff --git a/src/utilities/updateBrowserUrl.spec.ts b/src/utilities/updateBrowserUrl.spec.ts deleted file mode 100644 index a82d7533..00000000 --- a/src/utilities/updateBrowserUrl.spec.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { expect, test } from 'vitest' -import { updateBrowserUrl } from '@/utilities/updateBrowserUrl' - -test('does nothing when the window is not available', () => { - expect(() => updateBrowserUrl('http://example.com2/foo')).not.toThrowError() -}) \ No newline at end of file diff --git a/src/utilities/updateBrowserUrl.ts b/src/utilities/updateBrowserUrl.ts index 9a4d6151..b6a442bb 100644 --- a/src/utilities/updateBrowserUrl.ts +++ b/src/utilities/updateBrowserUrl.ts @@ -1,19 +1,19 @@ -import { isBrowser } from '@/utilities/isBrowser' - type UpdateBrowserUrlOptions = { replace?: boolean, } -export function updateBrowserUrl(url: string, options: UpdateBrowserUrlOptions = {}): void { - if (!isBrowser()) { - return - } - +export function updateBrowserUrl(url: string, options: UpdateBrowserUrlOptions = {}): Promise { if (isSameOrigin(url)) { - return updateHistory(url, options) + return new Promise(resolve => { + updateHistory(url, options) + resolve() + }) } - return updateWindow(url, options) + // intentionally never resolves because we want the router to just stall until window.location takes over + return new Promise(() => { + updateWindow(url, options) + }) } function updateHistory(url: string, options: UpdateBrowserUrlOptions): void { @@ -32,7 +32,7 @@ function updateWindow(url: string, options: UpdateBrowserUrlOptions): void { return window.location.assign(url) } -function isSameOrigin(url: string): boolean { +export function isSameOrigin(url: string): boolean { const { origin } = new URL(url, window.location.origin) return origin === window.location.origin