@typed/navigation 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/DOM.d.ts +12 -0
- package/dist/DOM.d.ts.map +1 -0
- package/dist/DOM.js +89 -0
- package/dist/DOM.js.map +1 -0
- package/dist/Memory.d.ts +10 -0
- package/dist/Memory.d.ts.map +1 -0
- package/dist/Memory.js +55 -0
- package/dist/Memory.js.map +1 -0
- package/dist/Navigation.d.ts +117 -0
- package/dist/Navigation.d.ts.map +1 -0
- package/dist/Navigation.js +36 -0
- package/dist/Navigation.js.map +1 -0
- package/dist/_makeServerWindow.d.ts +16 -0
- package/dist/_makeServerWindow.d.ts.map +1 -0
- package/dist/_makeServerWindow.js +9 -0
- package/dist/_makeServerWindow.js.map +1 -0
- package/dist/cjs/DOM.d.ts +12 -0
- package/dist/cjs/DOM.d.ts.map +1 -0
- package/dist/cjs/DOM.js +118 -0
- package/dist/cjs/DOM.js.map +1 -0
- package/dist/cjs/Memory.d.ts +10 -0
- package/dist/cjs/Memory.d.ts.map +1 -0
- package/dist/cjs/Memory.js +82 -0
- package/dist/cjs/Memory.js.map +1 -0
- package/dist/cjs/Navigation.d.ts +117 -0
- package/dist/cjs/Navigation.d.ts.map +1 -0
- package/dist/cjs/Navigation.js +69 -0
- package/dist/cjs/Navigation.js.map +1 -0
- package/dist/cjs/_makeServerWindow.d.ts +16 -0
- package/dist/cjs/_makeServerWindow.d.ts.map +1 -0
- package/dist/cjs/_makeServerWindow.js +36 -0
- package/dist/cjs/_makeServerWindow.js.map +1 -0
- package/dist/cjs/constant.d.ts +2 -0
- package/dist/cjs/constant.d.ts.map +1 -0
- package/dist/cjs/constant.js +30 -0
- package/dist/cjs/constant.js.map +1 -0
- package/dist/cjs/dom-intent.d.ts +29 -0
- package/dist/cjs/dom-intent.d.ts.map +1 -0
- package/dist/cjs/dom-intent.js +173 -0
- package/dist/cjs/dom-intent.js.map +1 -0
- package/dist/cjs/history.d.ts +31 -0
- package/dist/cjs/history.d.ts.map +1 -0
- package/dist/cjs/history.js +115 -0
- package/dist/cjs/history.js.map +1 -0
- package/dist/cjs/index.d.ts +4 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +20 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/json.d.ts +13 -0
- package/dist/cjs/json.d.ts.map +1 -0
- package/dist/cjs/json.js +24 -0
- package/dist/cjs/json.js.map +1 -0
- package/dist/cjs/memory-intent.d.ts +28 -0
- package/dist/cjs/memory-intent.d.ts.map +1 -0
- package/dist/cjs/memory-intent.js +149 -0
- package/dist/cjs/memory-intent.js.map +1 -0
- package/dist/cjs/model.d.ts +22 -0
- package/dist/cjs/model.d.ts.map +1 -0
- package/dist/cjs/model.js +48 -0
- package/dist/cjs/model.js.map +1 -0
- package/dist/cjs/shared-intent.d.ts +15 -0
- package/dist/cjs/shared-intent.d.ts.map +1 -0
- package/dist/cjs/shared-intent.js +82 -0
- package/dist/cjs/shared-intent.js.map +1 -0
- package/dist/cjs/storage.d.ts +19 -0
- package/dist/cjs/storage.d.ts.map +1 -0
- package/dist/cjs/storage.js +101 -0
- package/dist/cjs/storage.js.map +1 -0
- package/dist/cjs/util.d.ts +5 -0
- package/dist/cjs/util.d.ts.map +1 -0
- package/dist/cjs/util.js +39 -0
- package/dist/cjs/util.js.map +1 -0
- package/dist/constant.d.ts +2 -0
- package/dist/constant.d.ts.map +1 -0
- package/dist/constant.js +4 -0
- package/dist/constant.js.map +1 -0
- package/dist/dom-intent.d.ts +29 -0
- package/dist/dom-intent.d.ts.map +1 -0
- package/dist/dom-intent.js +141 -0
- package/dist/dom-intent.js.map +1 -0
- package/dist/history.d.ts +31 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +88 -0
- package/dist/history.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/intent.d.ts +31 -0
- package/dist/intent.d.ts.map +1 -0
- package/dist/intent.js +157 -0
- package/dist/intent.js.map +1 -0
- package/dist/json.d.ts +13 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +17 -0
- package/dist/json.js.map +1 -0
- package/dist/memory-intent.d.ts +28 -0
- package/dist/memory-intent.d.ts.map +1 -0
- package/dist/memory-intent.js +117 -0
- package/dist/memory-intent.js.map +1 -0
- package/dist/model.d.ts +22 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +21 -0
- package/dist/model.js.map +1 -0
- package/dist/shared-intent.d.ts +15 -0
- package/dist/shared-intent.d.ts.map +1 -0
- package/dist/shared-intent.js +51 -0
- package/dist/shared-intent.js.map +1 -0
- package/dist/storage.d.ts +19 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +73 -0
- package/dist/storage.js.map +1 -0
- package/dist/tsconfig.cjs.build.tsbuildinfo +1 -0
- package/dist/util.d.ts +5 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +12 -0
- package/dist/util.js.map +1 -0
- package/eslintrc.json +3 -0
- package/package.json +38 -0
- package/project.json +43 -0
- package/src/DOM.test.ts +704 -0
- package/src/DOM.ts +165 -0
- package/src/Memory.test.ts +464 -0
- package/src/Memory.ts +102 -0
- package/src/Navigation.ts +192 -0
- package/src/_makeServerWindow.ts +28 -0
- package/src/constant.ts +5 -0
- package/src/dom-intent.ts +276 -0
- package/src/history.ts +141 -0
- package/src/index.ts +3 -0
- package/src/json.ts +31 -0
- package/src/memory-intent.ts +221 -0
- package/src/model.ts +54 -0
- package/src/shared-intent.ts +120 -0
- package/src/storage.ts +101 -0
- package/src/util.ts +20 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.cjs.build.json +22 -0
- package/tsconfig.json +27 -0
- package/vite.config.js +3 -0
package/src/DOM.ts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { pipe } from '@effect/data/Function'
|
|
2
|
+
import * as Option from '@effect/data/Option'
|
|
3
|
+
import * as Cause from '@effect/io/Cause'
|
|
4
|
+
import * as Effect from '@effect/io/Effect'
|
|
5
|
+
import * as Layer from '@effect/io/Layer'
|
|
6
|
+
import * as Context from '@typed/context'
|
|
7
|
+
import { Location, History, Window, Storage, addWindowListener, Document } from '@typed/dom'
|
|
8
|
+
import * as Fx from '@typed/fx'
|
|
9
|
+
|
|
10
|
+
import { Destination, DestinationKey, Navigation, NavigationError } from './Navigation.js'
|
|
11
|
+
import { makeIntent } from './dom-intent.js'
|
|
12
|
+
import { onHistoryEvent, patchHistory } from './history.js'
|
|
13
|
+
import { NavigationEventJson } from './json.js'
|
|
14
|
+
import { makeModel } from './model.js'
|
|
15
|
+
import { getInitialValues } from './storage.js'
|
|
16
|
+
|
|
17
|
+
export type NavigationServices = Window | Document | Location | History | Storage
|
|
18
|
+
|
|
19
|
+
export interface DomNavigationOptions {
|
|
20
|
+
// Defaults to a random value, but you can provide your own
|
|
21
|
+
// Navigation keys can be provided to Navigation.navigate along the way as needed.
|
|
22
|
+
readonly initialKey?: DestinationKey
|
|
23
|
+
|
|
24
|
+
// Defaults to 50
|
|
25
|
+
// The maximum number of entries to keep in memory and storage.
|
|
26
|
+
readonly maxEntries?: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const dom = (
|
|
30
|
+
options: DomNavigationOptions = {},
|
|
31
|
+
): Layer.Layer<NavigationServices, never, Navigation> => {
|
|
32
|
+
return Navigation.layerScoped(
|
|
33
|
+
Effect.gen(function* ($) {
|
|
34
|
+
// Get resources
|
|
35
|
+
const context = yield* $(Effect.context<NavigationServices>())
|
|
36
|
+
const history = Context.get(context, History)
|
|
37
|
+
const document = Context.get(context, Document)
|
|
38
|
+
const base = document.querySelector('base')
|
|
39
|
+
const baseHref = base ? getBasePathFromHref(base.href) : '/'
|
|
40
|
+
|
|
41
|
+
// Create model and intent
|
|
42
|
+
const [initialEntries, initialIndex] = yield* $(getInitialValues(baseHref, options))
|
|
43
|
+
const model = yield* $(makeModel(initialEntries, initialIndex))
|
|
44
|
+
const intent = makeIntent(model, baseHref, options)
|
|
45
|
+
|
|
46
|
+
// Used to ensure ordering of navigation events
|
|
47
|
+
const lock = Effect.unsafeMakeSemaphore(1).withPermits(1)
|
|
48
|
+
|
|
49
|
+
const handleNavigationError =
|
|
50
|
+
(depth: number) =>
|
|
51
|
+
(
|
|
52
|
+
error: NavigationError | Cause.NoSuchElementException,
|
|
53
|
+
): Effect.Effect<never, never, Destination> =>
|
|
54
|
+
Effect.provideContext(
|
|
55
|
+
Effect.gen(function* ($) {
|
|
56
|
+
if (depth >= 50) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
'Too many redirects. You may have an infinite loop of onNavigation handlers that are redirecting.',
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
switch (error._tag) {
|
|
63
|
+
case 'NoSuchElementException':
|
|
64
|
+
case 'CancelNavigation':
|
|
65
|
+
return yield* $(model.currentEntry.get)
|
|
66
|
+
case 'RedirectNavigation':
|
|
67
|
+
return yield* $(
|
|
68
|
+
Effect.catchAll(
|
|
69
|
+
intent.navigate(error.url, error),
|
|
70
|
+
handleNavigationError(depth + 1),
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
}),
|
|
75
|
+
context,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
const catchNavigationError = <R, A>(
|
|
79
|
+
effect: Effect.Effect<R, NavigationError | Cause.NoSuchElementException, A>,
|
|
80
|
+
) => Effect.catchAll(effect, handleNavigationError(0))
|
|
81
|
+
|
|
82
|
+
// Used to provide a locked effect with the current context
|
|
83
|
+
const provideLocked = <E, A>(effect: Effect.Effect<NavigationServices, E, A>) =>
|
|
84
|
+
Effect.provideContext(lock(effect), context)
|
|
85
|
+
|
|
86
|
+
// Constructor our service
|
|
87
|
+
const navigation: Navigation = {
|
|
88
|
+
back: provideLocked(catchNavigationError(intent.back(false))),
|
|
89
|
+
base: baseHref,
|
|
90
|
+
canGoBack: model.canGoBack,
|
|
91
|
+
canGoForward: model.canGoForward,
|
|
92
|
+
currentEntry: model.currentEntry,
|
|
93
|
+
entries: model.entries,
|
|
94
|
+
forward: provideLocked(catchNavigationError(intent.forward(false))),
|
|
95
|
+
goTo: (a) =>
|
|
96
|
+
pipe(
|
|
97
|
+
a,
|
|
98
|
+
intent.goTo,
|
|
99
|
+
Effect.catchAll((a) => pipe(a, handleNavigationError(0), Effect.map(Option.some))),
|
|
100
|
+
provideLocked,
|
|
101
|
+
),
|
|
102
|
+
navigate: (url, options) => pipe(intent.navigate(url, options), catchNavigationError, provideLocked),
|
|
103
|
+
onNavigation: (handler, options) =>
|
|
104
|
+
pipe(intent.onNavigation(handler, options), catchNavigationError, Effect.asUnit),
|
|
105
|
+
onNavigationEnd: (handler, options) =>
|
|
106
|
+
Effect.asUnit(intent.onNavigationEnd(handler, options)),
|
|
107
|
+
reload: provideLocked(catchNavigationError(intent.reload)),
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Patch History API to enable sending events
|
|
111
|
+
const historyEvents = yield* $(patchHistory)
|
|
112
|
+
|
|
113
|
+
// Listen to various events and update our model
|
|
114
|
+
yield* $(
|
|
115
|
+
Fx.mergeAll(
|
|
116
|
+
// Listen to history events and keep track of entries
|
|
117
|
+
pipe(
|
|
118
|
+
historyEvents,
|
|
119
|
+
Fx.flatMapEffect((event) => lock(onHistoryEvent(event, intent))),
|
|
120
|
+
),
|
|
121
|
+
// Listen to hash changes and push them to the history
|
|
122
|
+
pipe(
|
|
123
|
+
addWindowListener('hashchange', { capture: true }),
|
|
124
|
+
Fx.flatMapEffect((ev) => lock(intent.push(ev.newURL, { state: history.state }, true))),
|
|
125
|
+
),
|
|
126
|
+
// Listen to popstate events and go to the correct entry
|
|
127
|
+
pipe(
|
|
128
|
+
addWindowListener('popstate'),
|
|
129
|
+
Fx.map((ev) => {
|
|
130
|
+
// TODO: Should we throw some kind of error here?
|
|
131
|
+
// This should never happen if you are solely using the Navigation Service
|
|
132
|
+
if (!ev.state || !ev.state.event) {
|
|
133
|
+
return Option.none()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const navigation = ev.state.event as NavigationEventJson
|
|
137
|
+
|
|
138
|
+
return Option.some(lock(intent.goTo(navigation.destination.key)))
|
|
139
|
+
}),
|
|
140
|
+
Fx.compact,
|
|
141
|
+
Fx.flattenEffect,
|
|
142
|
+
),
|
|
143
|
+
),
|
|
144
|
+
Fx.drain,
|
|
145
|
+
Effect.forkScoped,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return navigation
|
|
149
|
+
}),
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function getBasePathFromHref(href: string) {
|
|
154
|
+
try {
|
|
155
|
+
const url = new URL(href)
|
|
156
|
+
|
|
157
|
+
return getCurrentPathFromLocation(url)
|
|
158
|
+
} catch {
|
|
159
|
+
return href
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function getCurrentPathFromLocation(location: Location | HTMLAnchorElement | URL) {
|
|
164
|
+
return location.pathname + location.search + location.hash
|
|
165
|
+
}
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import { deepStrictEqual, ok } from 'assert'
|
|
2
|
+
|
|
3
|
+
import * as Duration from '@effect/data/Duration'
|
|
4
|
+
import * as Option from '@effect/data/Option'
|
|
5
|
+
import * as Effect from '@effect/io/Effect'
|
|
6
|
+
import * as Fiber from '@effect/io/Fiber'
|
|
7
|
+
import * as Fx from '@typed/fx'
|
|
8
|
+
import { describe, it } from 'vitest'
|
|
9
|
+
|
|
10
|
+
import { MemoryNavigationOptions, memory } from './Memory.js'
|
|
11
|
+
import {
|
|
12
|
+
Destination,
|
|
13
|
+
DestinationKey,
|
|
14
|
+
Navigation,
|
|
15
|
+
NavigationType,
|
|
16
|
+
cancelNavigation,
|
|
17
|
+
redirect,
|
|
18
|
+
} from './Navigation.js'
|
|
19
|
+
|
|
20
|
+
const provide = <R, E, A>(effect: Effect.Effect<R, E, A>, options: MemoryNavigationOptions) =>
|
|
21
|
+
Effect.provideSomeLayer(memory(options))(effect)
|
|
22
|
+
|
|
23
|
+
const testKey = DestinationKey('keys-are-random-and-not-tested-by-default-assertions')
|
|
24
|
+
const testUrl = 'https://example.com'
|
|
25
|
+
const testDestination = Destination(DestinationKey('default'), new URL(testUrl))
|
|
26
|
+
const testPathname1 = `${testUrl}/1`
|
|
27
|
+
const testPathname1Destination = Destination(testKey, new URL(testPathname1))
|
|
28
|
+
const testPathname2 = `${testUrl}/2`
|
|
29
|
+
const testPathname2Destination = Destination(testKey, new URL(testPathname2))
|
|
30
|
+
|
|
31
|
+
const testNavigation = <Y extends Effect.EffectGen<any, any, any>, A>(
|
|
32
|
+
f: (adapter: Effect.Adapter, navigation: Navigation) => Generator<Y, A, any>,
|
|
33
|
+
options: MemoryNavigationOptions = { initialUrl: new URL(testUrl) },
|
|
34
|
+
) =>
|
|
35
|
+
Effect.scoped(
|
|
36
|
+
provide(
|
|
37
|
+
Effect.tapErrorCause(
|
|
38
|
+
Effect.gen(function* ($) {
|
|
39
|
+
const navigation = yield* $(Navigation)
|
|
40
|
+
const result = yield* f($, navigation)
|
|
41
|
+
|
|
42
|
+
return result
|
|
43
|
+
}),
|
|
44
|
+
Effect.logError,
|
|
45
|
+
),
|
|
46
|
+
options,
|
|
47
|
+
),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
const fxToFiber = <R, E, A>(fx: Fx.Fx<R, E, A>, take: number) =>
|
|
51
|
+
Effect.gen(function* ($) {
|
|
52
|
+
const fiber = yield* $(fx, Fx.take(take), Fx.toReadonlyArray, Effect.forkScoped)
|
|
53
|
+
|
|
54
|
+
yield* $(Effect.sleep(Duration.millis(0)))
|
|
55
|
+
yield* $(Effect.sleep(Duration.millis(0)))
|
|
56
|
+
|
|
57
|
+
return fiber
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const assertEqualDestination: (a: Destination, b: Destination) => void = (a, b) => {
|
|
61
|
+
deepStrictEqual(
|
|
62
|
+
a.url.href,
|
|
63
|
+
b.url.href,
|
|
64
|
+
'Urls should match. Actual: ' + a.url.href + ' Expected: ' + b.url.href,
|
|
65
|
+
)
|
|
66
|
+
deepStrictEqual(
|
|
67
|
+
a.state,
|
|
68
|
+
b.state,
|
|
69
|
+
'State should match. Actual: ' +
|
|
70
|
+
JSON.stringify(a.state) +
|
|
71
|
+
' Expeced: ' +
|
|
72
|
+
JSON.stringify(b.state),
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const assertSomeDestination = (a: Option.Option<Destination>, b: Destination) => {
|
|
77
|
+
ok(Option.isSome(a))
|
|
78
|
+
assertEqualDestination(a.value, b)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const assertEqualDestinations: (a: readonly Destination[], b: readonly Destination[]) => void = (
|
|
82
|
+
a,
|
|
83
|
+
b,
|
|
84
|
+
) => {
|
|
85
|
+
deepStrictEqual(a.length, b.length, 'Destinations should have the same length')
|
|
86
|
+
|
|
87
|
+
for (let i = 0; i < a.length; ++i) {
|
|
88
|
+
assertEqualDestination(a[i], b[i])
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
describe(import.meta.url, () => {
|
|
93
|
+
describe('entries', () => {
|
|
94
|
+
it('returns the initial entry immediately', async () => {
|
|
95
|
+
const test = testNavigation(function* ($, { entries }) {
|
|
96
|
+
const initial = yield* $(entries)
|
|
97
|
+
|
|
98
|
+
assertEqualDestinations(initial, [testDestination])
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
await Effect.runPromise(test)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('is observerable', async () => {
|
|
105
|
+
const test = testNavigation(function* ($, { entries, navigate }) {
|
|
106
|
+
const fiber = yield* $(fxToFiber(entries, 2))
|
|
107
|
+
|
|
108
|
+
yield* $(navigate(testPathname1))
|
|
109
|
+
|
|
110
|
+
const results = yield* $(Fiber.join(fiber))
|
|
111
|
+
|
|
112
|
+
deepStrictEqual(results.length, 2)
|
|
113
|
+
|
|
114
|
+
assertEqualDestinations(results[0], [testDestination])
|
|
115
|
+
assertEqualDestinations(results[1], [testDestination, testPathname1Destination])
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
await Effect.runPromise(test)
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
describe('currentEntry', () => {
|
|
123
|
+
it('returns the initial entry immediately', async () => {
|
|
124
|
+
const test = testNavigation(function* ($, { currentEntry }) {
|
|
125
|
+
const initial = yield* $(currentEntry)
|
|
126
|
+
|
|
127
|
+
assertEqualDestination(initial, testDestination)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
await Effect.runPromise(test)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('is observerable', async () => {
|
|
134
|
+
const test = testNavigation(function* ($, { currentEntry, navigate }) {
|
|
135
|
+
const fiber = yield* $(fxToFiber(currentEntry, 3))
|
|
136
|
+
|
|
137
|
+
yield* $(navigate(testPathname1))
|
|
138
|
+
yield* $(navigate(testPathname2))
|
|
139
|
+
|
|
140
|
+
const results = yield* $(Fiber.join(fiber))
|
|
141
|
+
|
|
142
|
+
assertEqualDestinations(results, [
|
|
143
|
+
testDestination,
|
|
144
|
+
testPathname1Destination,
|
|
145
|
+
testPathname2Destination,
|
|
146
|
+
])
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
await Effect.runPromise(test)
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
describe('navigate', () => {
|
|
154
|
+
it('navigates to a new url', async () => {
|
|
155
|
+
const test = testNavigation(function* ($, { currentEntry, navigate }) {
|
|
156
|
+
const initial = yield* $(currentEntry)
|
|
157
|
+
|
|
158
|
+
assertEqualDestination(initial, testDestination)
|
|
159
|
+
|
|
160
|
+
const destination = yield* $(navigate(testPathname1))
|
|
161
|
+
|
|
162
|
+
assertEqualDestination(destination, testPathname1Destination)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
await Effect.runPromise(test)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('sets state when provided', async () => {
|
|
169
|
+
const test = testNavigation(function* ($, { navigate }) {
|
|
170
|
+
const destination = yield* $(navigate(testPathname1, { state: testKey }))
|
|
171
|
+
|
|
172
|
+
assertEqualDestination(destination, Destination(testKey, new URL(testPathname1), testKey))
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
await Effect.runPromise(test)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('allows replacing the current entry', async () => {
|
|
179
|
+
const test = testNavigation(function* ($, { entries, navigate }) {
|
|
180
|
+
assertEqualDestinations(yield* $(entries), [testDestination])
|
|
181
|
+
|
|
182
|
+
const destination = yield* $(navigate(testPathname1, { history: 'replace' }))
|
|
183
|
+
|
|
184
|
+
assertEqualDestinations(yield* $(entries), [destination])
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
await Effect.runPromise(test)
|
|
188
|
+
})
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
describe('onNavigation', () => {
|
|
192
|
+
it('allows subscribing to navigation events', async () => {
|
|
193
|
+
const test = testNavigation(function* ($, { navigate, onNavigation }) {
|
|
194
|
+
let i = 0
|
|
195
|
+
yield* $(
|
|
196
|
+
onNavigation((event) => {
|
|
197
|
+
if (i === 1) {
|
|
198
|
+
deepStrictEqual(event.navigationType, NavigationType.Push)
|
|
199
|
+
assertEqualDestination(event.destination, testPathname1Destination)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (i === 2) {
|
|
203
|
+
deepStrictEqual(event.navigationType, NavigationType.Push)
|
|
204
|
+
assertEqualDestination(event.destination, testPathname2Destination)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
i++
|
|
208
|
+
|
|
209
|
+
return Effect.unit
|
|
210
|
+
}),
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
yield* $(navigate(testPathname1))
|
|
214
|
+
yield* $(navigate(testPathname2))
|
|
215
|
+
|
|
216
|
+
deepStrictEqual(i, 3)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
await Effect.runPromise(test)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('allows canceling the requested navigation', async () => {
|
|
223
|
+
const test = testNavigation(function* ($, { navigate, onNavigation }) {
|
|
224
|
+
yield* $(onNavigation(() => cancelNavigation))
|
|
225
|
+
|
|
226
|
+
const destination = yield* $(navigate(testPathname1))
|
|
227
|
+
|
|
228
|
+
assertEqualDestination(destination, testDestination)
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
await Effect.runPromise(test)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('allow redirection to a different url', async () => {
|
|
235
|
+
const test = testNavigation(function* ($, { navigate, onNavigation }) {
|
|
236
|
+
yield* $(
|
|
237
|
+
onNavigation(({ destination }) =>
|
|
238
|
+
destination.url.href === testPathname1 ? redirect(testPathname2) : Effect.unit,
|
|
239
|
+
),
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
const destination = yield* $(navigate(testPathname1))
|
|
243
|
+
|
|
244
|
+
assertEqualDestination(destination, testPathname2Destination)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
await Effect.runPromise(test)
|
|
248
|
+
})
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
describe('canGoBack', () => {
|
|
252
|
+
it('returns false when on the first entry', async () => {
|
|
253
|
+
const test = testNavigation(function* ($, { canGoBack }) {
|
|
254
|
+
deepStrictEqual(yield* $(canGoBack), false)
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
await Effect.runPromise(test)
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it('returns true when there are entries to go back to', async () => {
|
|
261
|
+
const test = testNavigation(function* ($, { canGoBack, navigate }) {
|
|
262
|
+
yield* $(navigate(testPathname1))
|
|
263
|
+
|
|
264
|
+
deepStrictEqual(yield* $(canGoBack), true)
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
await Effect.runPromise(test)
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it('is observable', async () => {
|
|
271
|
+
const test = testNavigation(function* ($, { canGoBack, navigate }) {
|
|
272
|
+
const fiber = yield* $(fxToFiber(canGoBack, 2))
|
|
273
|
+
|
|
274
|
+
yield* $(navigate(testPathname1))
|
|
275
|
+
yield* $(navigate(testPathname2))
|
|
276
|
+
|
|
277
|
+
const results = yield* $(Fiber.join(fiber))
|
|
278
|
+
|
|
279
|
+
deepStrictEqual(results, [false, true])
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
await Effect.runPromise(test)
|
|
283
|
+
})
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
describe('back', () => {
|
|
287
|
+
it('does nothing when there are no entries to go back to', async () => {
|
|
288
|
+
const test = testNavigation(function* ($, { back }) {
|
|
289
|
+
assertEqualDestination(yield* $(back), testDestination)
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
await Effect.runPromise(test)
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
it('goes back to the previous entry', async () => {
|
|
296
|
+
const test = testNavigation(function* ($, { back, navigate }) {
|
|
297
|
+
yield* $(navigate(testPathname1))
|
|
298
|
+
|
|
299
|
+
assertEqualDestination(yield* $(back), testDestination)
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
await Effect.runPromise(test)
|
|
303
|
+
})
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
describe('canGoForward', () => {
|
|
307
|
+
it('returns false when on the last entry', async () => {
|
|
308
|
+
const test = testNavigation(function* ($, { canGoForward }) {
|
|
309
|
+
deepStrictEqual(yield* $(canGoForward), false)
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
await Effect.runPromise(test)
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
it('returns true when there are entries to go forward to', async () => {
|
|
316
|
+
const test = testNavigation(function* ($, { canGoForward, back, navigate }) {
|
|
317
|
+
yield* $(navigate(testPathname1))
|
|
318
|
+
yield* $(navigate(testPathname2))
|
|
319
|
+
|
|
320
|
+
deepStrictEqual(yield* $(canGoForward), false)
|
|
321
|
+
|
|
322
|
+
yield* $(back)
|
|
323
|
+
|
|
324
|
+
deepStrictEqual(yield* $(canGoForward), true)
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
await Effect.runPromise(test)
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
it('is observable', async () => {
|
|
331
|
+
const test = testNavigation(function* ($, { canGoForward, back, navigate }) {
|
|
332
|
+
const fiber = yield* $(fxToFiber(canGoForward, 2))
|
|
333
|
+
|
|
334
|
+
yield* $(navigate(testPathname1))
|
|
335
|
+
yield* $(back)
|
|
336
|
+
|
|
337
|
+
const results = yield* $(Fiber.join(fiber))
|
|
338
|
+
|
|
339
|
+
deepStrictEqual(results, [false, true]) // Duplication between first and second entry is skipped
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
await Effect.runPromise(test)
|
|
343
|
+
})
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
describe('forward', () => {
|
|
347
|
+
it('does nothing when there are no entries to go forward to', async () => {
|
|
348
|
+
const test = testNavigation(function* ($, { forward }) {
|
|
349
|
+
assertEqualDestination(yield* $(forward), testDestination)
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
await Effect.runPromise(test)
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
it('goes forward to the next entry', async () => {
|
|
356
|
+
const test = testNavigation(function* ($, { forward, back, navigate }) {
|
|
357
|
+
yield* $(navigate(testPathname1))
|
|
358
|
+
yield* $(navigate(testPathname2))
|
|
359
|
+
|
|
360
|
+
assertEqualDestination(yield* $(back), testPathname1Destination)
|
|
361
|
+
assertEqualDestination(yield* $(forward), testPathname2Destination)
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
await Effect.runPromise(test)
|
|
365
|
+
})
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
describe('reload', () => {
|
|
369
|
+
it('reloads the current entry', async () => {
|
|
370
|
+
const test = testNavigation(function* ($, { reload, navigate }) {
|
|
371
|
+
yield* $(navigate(testPathname1))
|
|
372
|
+
|
|
373
|
+
assertEqualDestination(yield* $(reload), testPathname1Destination)
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
await Effect.runPromise(test)
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
it('sends a reload event to subscribers', async () => {
|
|
380
|
+
const test = testNavigation(function* ($, { reload, navigate, onNavigation }) {
|
|
381
|
+
let i = 0
|
|
382
|
+
yield* $(
|
|
383
|
+
onNavigation((event) => {
|
|
384
|
+
if (i === 1) {
|
|
385
|
+
deepStrictEqual(event.navigationType, NavigationType.Push)
|
|
386
|
+
assertEqualDestination(event.destination, testPathname1Destination)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (i === 2) {
|
|
390
|
+
deepStrictEqual(event.navigationType, NavigationType.Reload)
|
|
391
|
+
assertEqualDestination(event.destination, testPathname1Destination)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
i++
|
|
395
|
+
|
|
396
|
+
return Effect.unit
|
|
397
|
+
}),
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
yield* $(navigate(testPathname1))
|
|
401
|
+
yield* $(reload)
|
|
402
|
+
|
|
403
|
+
deepStrictEqual(i, 3)
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
await Effect.runPromise(test)
|
|
407
|
+
})
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
describe('goTo', () => {
|
|
411
|
+
it('goes to a specific entry', async () => {
|
|
412
|
+
const test = testNavigation(function* ($, { currentEntry, goTo, navigate }) {
|
|
413
|
+
const d0 = yield* $(currentEntry)
|
|
414
|
+
const d1 = yield* $(navigate(testPathname1))
|
|
415
|
+
const d2 = yield* $(navigate(testPathname2))
|
|
416
|
+
|
|
417
|
+
assertSomeDestination(yield* $(goTo(d0.key)), testDestination)
|
|
418
|
+
assertSomeDestination(yield* $(goTo(d1.key)), testPathname1Destination)
|
|
419
|
+
assertSomeDestination(yield* $(goTo(d2.key)), testPathname2Destination)
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
await Effect.runPromise(test)
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
it('returns None when the entry does not exist', async () => {
|
|
426
|
+
const test = testNavigation(function* ($, { goTo }) {
|
|
427
|
+
deepStrictEqual(yield* $(goTo(DestinationKey('does-not-exist'))), Option.none())
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
await Effect.runPromise(test)
|
|
431
|
+
})
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
describe('maxEntries', () => {
|
|
435
|
+
it('allows configuring how many entries are stored', async () => {
|
|
436
|
+
const test = testNavigation(
|
|
437
|
+
function* ($, { currentEntry, entries, navigate }) {
|
|
438
|
+
const fiber = yield* $(fxToFiber(currentEntry, 3))
|
|
439
|
+
|
|
440
|
+
yield* $(navigate(testPathname1))
|
|
441
|
+
yield* $(navigate(testPathname2))
|
|
442
|
+
|
|
443
|
+
const results = yield* $(Fiber.join(fiber))
|
|
444
|
+
|
|
445
|
+
assertEqualDestinations(results, [
|
|
446
|
+
testDestination,
|
|
447
|
+
testPathname1Destination,
|
|
448
|
+
testPathname2Destination,
|
|
449
|
+
])
|
|
450
|
+
|
|
451
|
+
const entriesAfterNavigation = yield* $(entries)
|
|
452
|
+
|
|
453
|
+
assertEqualDestinations(entriesAfterNavigation, [
|
|
454
|
+
testPathname1Destination,
|
|
455
|
+
testPathname2Destination,
|
|
456
|
+
])
|
|
457
|
+
},
|
|
458
|
+
{ initialUrl: new URL(testUrl), maxEntries: 2 },
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
await Effect.runPromise(test)
|
|
462
|
+
})
|
|
463
|
+
})
|
|
464
|
+
})
|