@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.test.ts
ADDED
|
@@ -0,0 +1,704 @@
|
|
|
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 Layer from '@effect/io/Layer'
|
|
8
|
+
import { GlobalThis, History, Location, Window, localStorage, makeDomServices } from '@typed/dom'
|
|
9
|
+
import * as Fx from '@typed/fx'
|
|
10
|
+
import { describe, it } from 'vitest'
|
|
11
|
+
|
|
12
|
+
import { DomNavigationOptions, dom } from './DOM.js'
|
|
13
|
+
import {
|
|
14
|
+
Destination,
|
|
15
|
+
DestinationKey,
|
|
16
|
+
Navigation,
|
|
17
|
+
NavigationType,
|
|
18
|
+
cancelNavigation,
|
|
19
|
+
redirect,
|
|
20
|
+
} from './Navigation.js'
|
|
21
|
+
import { makeServerWindow } from './_makeServerWindow.js'
|
|
22
|
+
import { encodeDestination } from './json.js'
|
|
23
|
+
import { getStoredEvents } from './storage.js'
|
|
24
|
+
|
|
25
|
+
const serviceNavigation = (url: string, options: DomNavigationOptions = {}) => {
|
|
26
|
+
const window = makeServerWindow({ url })
|
|
27
|
+
const services = makeDomServices(window, window, window.document.body)
|
|
28
|
+
|
|
29
|
+
return Layer.provideMerge(
|
|
30
|
+
Layer.succeedContext(services),
|
|
31
|
+
Layer.provideMerge(localStorage, dom(options)),
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const provide = <R, E, A>(
|
|
36
|
+
effect: Effect.Effect<R, E, A>,
|
|
37
|
+
url: string,
|
|
38
|
+
options: DomNavigationOptions = {},
|
|
39
|
+
) => Effect.provideSomeLayer(serviceNavigation(url, options))(effect)
|
|
40
|
+
|
|
41
|
+
const testKey = DestinationKey('keys-are-random-and-not-tested-by-default-assertions')
|
|
42
|
+
const testUrl = 'https://example.com'
|
|
43
|
+
const testDestination = Destination(DestinationKey('default'), new URL(testUrl))
|
|
44
|
+
const testPathname1 = `${testUrl}/1`
|
|
45
|
+
const testPathname1Destination = Destination(testKey, new URL(testPathname1))
|
|
46
|
+
const testPathname2 = `${testUrl}/2`
|
|
47
|
+
const testPathname2Destination = Destination(testKey, new URL(testPathname2))
|
|
48
|
+
|
|
49
|
+
const testNavigation = <Y extends Effect.EffectGen<any, any, any>, A>(
|
|
50
|
+
f: (adapter: Effect.Adapter, navigation: Navigation) => Generator<Y, A, any>,
|
|
51
|
+
initialUrl = testUrl,
|
|
52
|
+
options: DomNavigationOptions = {},
|
|
53
|
+
) =>
|
|
54
|
+
Effect.scoped(
|
|
55
|
+
provide(
|
|
56
|
+
Effect.tapErrorCause(
|
|
57
|
+
Effect.gen(function* ($) {
|
|
58
|
+
const navigation = yield* $(Navigation)
|
|
59
|
+
const result = yield* f($, navigation)
|
|
60
|
+
|
|
61
|
+
return result
|
|
62
|
+
}),
|
|
63
|
+
Effect.logError,
|
|
64
|
+
),
|
|
65
|
+
initialUrl,
|
|
66
|
+
options,
|
|
67
|
+
),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
const fxToFiber = <R, E, A>(fx: Fx.Fx<R, E, A>, take: number) =>
|
|
71
|
+
Effect.gen(function* ($) {
|
|
72
|
+
const fiber = yield* $(fx, Fx.take(take), Fx.toReadonlyArray, Effect.forkScoped)
|
|
73
|
+
|
|
74
|
+
yield* $(Effect.sleep(Duration.millis(1)))
|
|
75
|
+
yield* $(Effect.sleep(Duration.millis(1)))
|
|
76
|
+
|
|
77
|
+
return fiber
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const assertEqualDestination: (a: Destination, b: Destination) => void = (a, b) => {
|
|
81
|
+
console.log(a, b)
|
|
82
|
+
|
|
83
|
+
deepStrictEqual(
|
|
84
|
+
a.url.href,
|
|
85
|
+
b.url.href,
|
|
86
|
+
'Urls should match. Actual: ' + a.url.href + ' Expected: ' + b.url.href,
|
|
87
|
+
)
|
|
88
|
+
deepStrictEqual(
|
|
89
|
+
a.state,
|
|
90
|
+
b.state,
|
|
91
|
+
'State should match. Actual: ' +
|
|
92
|
+
JSON.stringify(a.state) +
|
|
93
|
+
' Expeced: ' +
|
|
94
|
+
JSON.stringify(b.state),
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const assertSomeDestination = (a: Option.Option<Destination>, b: Destination) => {
|
|
99
|
+
ok(Option.isSome(a))
|
|
100
|
+
assertEqualDestination(a.value, b)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const assertEqualDestinations: (a: readonly Destination[], b: readonly Destination[]) => void = (
|
|
104
|
+
a,
|
|
105
|
+
b,
|
|
106
|
+
) => {
|
|
107
|
+
deepStrictEqual(a.length, b.length, 'Destinations should have the same length')
|
|
108
|
+
|
|
109
|
+
for (let i = 0; i < a.length; ++i) {
|
|
110
|
+
assertEqualDestination(a[i], b[i])
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
describe(import.meta.url, () => {
|
|
115
|
+
describe('entries', () => {
|
|
116
|
+
it('returns the initial entry immediately', async () => {
|
|
117
|
+
const test = testNavigation(function* ($, { entries }) {
|
|
118
|
+
const initial = yield* $(entries)
|
|
119
|
+
|
|
120
|
+
assertEqualDestinations(initial, [testDestination])
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
await Effect.runPromise(test)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('is observerable', async () => {
|
|
127
|
+
const test = testNavigation(function* ($, { entries, navigate }) {
|
|
128
|
+
const fiber = yield* $(fxToFiber(entries, 2))
|
|
129
|
+
|
|
130
|
+
yield* $(navigate(testPathname1))
|
|
131
|
+
|
|
132
|
+
const results = yield* $(Fiber.join(fiber))
|
|
133
|
+
|
|
134
|
+
deepStrictEqual(results.length, 2)
|
|
135
|
+
|
|
136
|
+
assertEqualDestinations(results[0], [testDestination])
|
|
137
|
+
assertEqualDestinations(results[1], [testDestination, testPathname1Destination])
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
await Effect.runPromise(test)
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
describe('currentEntry', () => {
|
|
145
|
+
it('returns the initial entry immediately', async () => {
|
|
146
|
+
const test = testNavigation(function* ($, { currentEntry }) {
|
|
147
|
+
const initial = yield* $(currentEntry)
|
|
148
|
+
|
|
149
|
+
assertEqualDestination(initial, testDestination)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
await Effect.runPromise(test)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('is observerable', async () => {
|
|
156
|
+
const test = testNavigation(function* ($, { currentEntry, navigate }) {
|
|
157
|
+
const fiber = yield* $(fxToFiber(currentEntry, 3))
|
|
158
|
+
|
|
159
|
+
yield* $(navigate(testPathname1))
|
|
160
|
+
yield* $(navigate(testPathname2))
|
|
161
|
+
|
|
162
|
+
const results = yield* $(Fiber.join(fiber))
|
|
163
|
+
|
|
164
|
+
assertEqualDestinations(results, [
|
|
165
|
+
testDestination,
|
|
166
|
+
testPathname1Destination,
|
|
167
|
+
testPathname2Destination,
|
|
168
|
+
])
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
await Effect.runPromise(test)
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
describe('navigate', () => {
|
|
176
|
+
it('navigates to a new url', async () => {
|
|
177
|
+
const test = testNavigation(function* ($, { currentEntry, navigate }) {
|
|
178
|
+
const initial = yield* $(currentEntry)
|
|
179
|
+
|
|
180
|
+
assertEqualDestination(initial, testDestination)
|
|
181
|
+
|
|
182
|
+
const destination = yield* $(navigate(testPathname1))
|
|
183
|
+
|
|
184
|
+
assertEqualDestination(destination, testPathname1Destination)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
await Effect.runPromise(test)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('sets state when provided', async () => {
|
|
191
|
+
const test = testNavigation(function* ($, { navigate }) {
|
|
192
|
+
const destination = yield* $(navigate(testPathname1, { state: testKey }))
|
|
193
|
+
|
|
194
|
+
assertEqualDestination(destination, Destination(testKey, new URL(testPathname1), testKey))
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
await Effect.runPromise(test)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('allows replacing the current entry', async () => {
|
|
201
|
+
const test = testNavigation(function* ($, { entries, navigate }) {
|
|
202
|
+
assertEqualDestinations(yield* $(entries), [testDestination])
|
|
203
|
+
|
|
204
|
+
const destination = yield* $(navigate(testPathname1, { history: 'replace' }))
|
|
205
|
+
|
|
206
|
+
assertEqualDestinations(yield* $(entries), [destination])
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
await Effect.runPromise(test)
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
describe('onNavigation', () => {
|
|
214
|
+
it('allows subscribing to navigation events', async () => {
|
|
215
|
+
const test = testNavigation(function* ($, { navigate, onNavigation }) {
|
|
216
|
+
let i = 0
|
|
217
|
+
yield* $(
|
|
218
|
+
onNavigation((event) => {
|
|
219
|
+
if (i === 0) {
|
|
220
|
+
deepStrictEqual(event.navigationType, NavigationType.Push)
|
|
221
|
+
assertEqualDestination(event.destination, testDestination)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (i === 1) {
|
|
225
|
+
deepStrictEqual(event.navigationType, NavigationType.Push)
|
|
226
|
+
assertEqualDestination(event.destination, testPathname1Destination)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (i === 2) {
|
|
230
|
+
deepStrictEqual(event.navigationType, NavigationType.Push)
|
|
231
|
+
assertEqualDestination(event.destination, testPathname2Destination)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
i++
|
|
235
|
+
|
|
236
|
+
return Effect.unit
|
|
237
|
+
}),
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
yield* $(navigate(testPathname1))
|
|
241
|
+
yield* $(navigate(testPathname2))
|
|
242
|
+
|
|
243
|
+
deepStrictEqual(i, 3)
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
await Effect.runPromise(test)
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it('allows canceling the requested navigation', async () => {
|
|
250
|
+
const test = testNavigation(function* ($, { navigate, onNavigation }) {
|
|
251
|
+
yield* $(onNavigation(() => cancelNavigation))
|
|
252
|
+
|
|
253
|
+
const destination = yield* $(navigate(testPathname1))
|
|
254
|
+
|
|
255
|
+
assertEqualDestination(destination, testDestination)
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
await Effect.runPromise(test)
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it('allow redirection to a different url', async () => {
|
|
262
|
+
const test = testNavigation(function* ($, { navigate, onNavigation }) {
|
|
263
|
+
yield* $(
|
|
264
|
+
onNavigation(({ destination }) =>
|
|
265
|
+
destination.url.href === testPathname1 ? redirect(testPathname2) : Effect.unit,
|
|
266
|
+
),
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
const destination = yield* $(navigate(testPathname1))
|
|
270
|
+
|
|
271
|
+
assertEqualDestination(destination, testPathname2Destination)
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
await Effect.runPromise(test)
|
|
275
|
+
})
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
describe('canGoBack', () => {
|
|
279
|
+
it('returns false when on the first entry', async () => {
|
|
280
|
+
const test = testNavigation(function* ($, { canGoBack }) {
|
|
281
|
+
deepStrictEqual(yield* $(canGoBack), false)
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
await Effect.runPromise(test)
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
it('returns true when there are entries to go back to', async () => {
|
|
288
|
+
const test = testNavigation(function* ($, { canGoBack, navigate }) {
|
|
289
|
+
yield* $(navigate(testPathname1))
|
|
290
|
+
|
|
291
|
+
deepStrictEqual(yield* $(canGoBack), true)
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
await Effect.runPromise(test)
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
it('is observable', async () => {
|
|
298
|
+
const test = testNavigation(function* ($, { canGoBack, navigate }) {
|
|
299
|
+
const fiber = yield* $(fxToFiber(canGoBack, 2))
|
|
300
|
+
|
|
301
|
+
yield* $(navigate(testPathname1))
|
|
302
|
+
yield* $(navigate(testPathname2))
|
|
303
|
+
|
|
304
|
+
const results = yield* $(Fiber.join(fiber))
|
|
305
|
+
|
|
306
|
+
deepStrictEqual(results, [false, true])
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
await Effect.runPromise(test)
|
|
310
|
+
})
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
describe('back', () => {
|
|
314
|
+
it('does nothing when there are no entries to go back to', async () => {
|
|
315
|
+
const test = testNavigation(function* ($, { back }) {
|
|
316
|
+
assertEqualDestination(yield* $(back), testDestination)
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
await Effect.runPromise(test)
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
it('goes back to the previous entry', async () => {
|
|
323
|
+
const test = testNavigation(function* ($, { back, navigate }) {
|
|
324
|
+
yield* $(navigate(testPathname1))
|
|
325
|
+
|
|
326
|
+
assertEqualDestination(yield* $(back), testDestination)
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
await Effect.runPromise(test)
|
|
330
|
+
})
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
describe('canGoForward', () => {
|
|
334
|
+
it('returns false when on the last entry', async () => {
|
|
335
|
+
const test = testNavigation(function* ($, { canGoForward }) {
|
|
336
|
+
deepStrictEqual(yield* $(canGoForward), false)
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
await Effect.runPromise(test)
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
it('returns true when there are entries to go forward to', async () => {
|
|
343
|
+
const test = testNavigation(function* ($, { canGoForward, back, navigate }) {
|
|
344
|
+
yield* $(navigate(testPathname1))
|
|
345
|
+
yield* $(navigate(testPathname2))
|
|
346
|
+
|
|
347
|
+
deepStrictEqual(yield* $(canGoForward), false)
|
|
348
|
+
|
|
349
|
+
yield* $(back)
|
|
350
|
+
|
|
351
|
+
deepStrictEqual(yield* $(canGoForward), true)
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
await Effect.runPromise(test)
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
it('is observable', async () => {
|
|
358
|
+
const test = testNavigation(function* ($, { canGoForward, back, navigate }) {
|
|
359
|
+
const fiber = yield* $(fxToFiber(canGoForward, 2))
|
|
360
|
+
|
|
361
|
+
yield* $(navigate(testPathname1))
|
|
362
|
+
yield* $(back)
|
|
363
|
+
|
|
364
|
+
const results = yield* $(Fiber.join(fiber))
|
|
365
|
+
|
|
366
|
+
deepStrictEqual(results, [false, true]) // Duplication between first and second entry is skipped
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
await Effect.runPromise(test)
|
|
370
|
+
})
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
describe('forward', () => {
|
|
374
|
+
it('does nothing when there are no entries to go forward to', async () => {
|
|
375
|
+
const test = testNavigation(function* ($, { forward }) {
|
|
376
|
+
assertEqualDestination(yield* $(forward), testDestination)
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
await Effect.runPromise(test)
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
it('goes forward to the next entry', async () => {
|
|
383
|
+
const test = testNavigation(function* ($, { forward, back, navigate }) {
|
|
384
|
+
yield* $(navigate(testPathname1))
|
|
385
|
+
yield* $(navigate(testPathname2))
|
|
386
|
+
|
|
387
|
+
assertEqualDestination(yield* $(back), testPathname1Destination)
|
|
388
|
+
assertEqualDestination(yield* $(forward), testPathname2Destination)
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
await Effect.runPromise(test)
|
|
392
|
+
})
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
describe('reload', () => {
|
|
396
|
+
it('reloads the current entry', async () => {
|
|
397
|
+
const test = testNavigation(function* ($, { reload, navigate }) {
|
|
398
|
+
yield* $(navigate(testPathname1))
|
|
399
|
+
|
|
400
|
+
assertEqualDestination(yield* $(reload), testPathname1Destination)
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
await Effect.runPromise(test)
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
it('sends a reload event to subscribers', async () => {
|
|
407
|
+
const test = testNavigation(function* ($, { reload, navigate, onNavigation }) {
|
|
408
|
+
let i = 0
|
|
409
|
+
yield* $(
|
|
410
|
+
onNavigation((event) => {
|
|
411
|
+
if (i === 1) {
|
|
412
|
+
deepStrictEqual(event.navigationType, NavigationType.Push)
|
|
413
|
+
assertEqualDestination(event.destination, testPathname1Destination)
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (i === 2) {
|
|
417
|
+
deepStrictEqual(event.navigationType, NavigationType.Reload)
|
|
418
|
+
assertEqualDestination(event.destination, testPathname1Destination)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
i++
|
|
422
|
+
|
|
423
|
+
return Effect.unit
|
|
424
|
+
}),
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
yield* $(navigate(testPathname1))
|
|
428
|
+
yield* $(reload)
|
|
429
|
+
|
|
430
|
+
deepStrictEqual(i, 3)
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
await Effect.runPromise(test)
|
|
434
|
+
})
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
describe('goTo', () => {
|
|
438
|
+
it('goes to a specific entry', async () => {
|
|
439
|
+
const test = testNavigation(function* ($, { currentEntry, goTo, navigate }) {
|
|
440
|
+
const d0 = yield* $(currentEntry)
|
|
441
|
+
const d1 = yield* $(navigate(testPathname1))
|
|
442
|
+
const d2 = yield* $(navigate(testPathname2))
|
|
443
|
+
|
|
444
|
+
assertSomeDestination(yield* $(goTo(d0.key)), testDestination)
|
|
445
|
+
assertSomeDestination(yield* $(goTo(d1.key)), testPathname1Destination)
|
|
446
|
+
assertSomeDestination(yield* $(goTo(d2.key)), testPathname2Destination)
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
await Effect.runPromise(test)
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
it('returns None when the entry does not exist', async () => {
|
|
453
|
+
const test = testNavigation(function* ($, { goTo }) {
|
|
454
|
+
deepStrictEqual(yield* $(goTo(DestinationKey('does-not-exist'))), Option.none())
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
await Effect.runPromise(test)
|
|
458
|
+
})
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
describe('localStorage', () => {
|
|
462
|
+
it('saves and retrieves entries from localStorage', async () => {
|
|
463
|
+
const test = testNavigation(function* ($, { navigate }) {
|
|
464
|
+
yield* $(navigate(testPathname1))
|
|
465
|
+
yield* $(navigate(testPathname2))
|
|
466
|
+
|
|
467
|
+
const events = yield* $(getStoredEvents)
|
|
468
|
+
|
|
469
|
+
assertEqualDestinations(
|
|
470
|
+
events.map((x) => x.destination),
|
|
471
|
+
[testDestination, testPathname1Destination, testPathname2Destination],
|
|
472
|
+
)
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
await Effect.runPromise(test)
|
|
476
|
+
})
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
describe('history', () => {
|
|
480
|
+
it('patches history.pushState', async () => {
|
|
481
|
+
const test = testNavigation(function* ($, { currentEntry }) {
|
|
482
|
+
const history = yield* $(History)
|
|
483
|
+
const fiber = yield* $(fxToFiber(currentEntry, 3))
|
|
484
|
+
|
|
485
|
+
history.pushState(undefined, '', '/1')
|
|
486
|
+
history.pushState(undefined, '', '/2')
|
|
487
|
+
|
|
488
|
+
const results = yield* $(Fiber.join(fiber))
|
|
489
|
+
|
|
490
|
+
assertEqualDestinations(results, [
|
|
491
|
+
testDestination,
|
|
492
|
+
testPathname1Destination,
|
|
493
|
+
testPathname2Destination,
|
|
494
|
+
])
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
await Effect.runPromise(test)
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
it('patches history.replaceState', async () => {
|
|
501
|
+
const test = testNavigation(function* ($, { currentEntry }) {
|
|
502
|
+
const history = yield* $(History)
|
|
503
|
+
const fiber = yield* $(fxToFiber(currentEntry, 3))
|
|
504
|
+
|
|
505
|
+
const state = { x: Math.random(), y: Math.random() }
|
|
506
|
+
|
|
507
|
+
history.replaceState(state, '', '/1')
|
|
508
|
+
history.replaceState(state, '', '/2')
|
|
509
|
+
|
|
510
|
+
const results = yield* $(Fiber.join(fiber))
|
|
511
|
+
|
|
512
|
+
assertEqualDestinations(results, [
|
|
513
|
+
testDestination,
|
|
514
|
+
{ ...testPathname1Destination, state },
|
|
515
|
+
{ ...testPathname2Destination, state },
|
|
516
|
+
])
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
await Effect.runPromise(test)
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
it('patches history.back', async () => {
|
|
523
|
+
const test = testNavigation(function* ($, { currentEntry }) {
|
|
524
|
+
const history = yield* $(History)
|
|
525
|
+
const fiber = yield* $(fxToFiber(currentEntry, 3))
|
|
526
|
+
|
|
527
|
+
history.pushState(undefined, '', '/1')
|
|
528
|
+
history.back()
|
|
529
|
+
|
|
530
|
+
const results = yield* $(Fiber.join(fiber))
|
|
531
|
+
|
|
532
|
+
assertEqualDestinations(results, [
|
|
533
|
+
testDestination,
|
|
534
|
+
testPathname1Destination,
|
|
535
|
+
testDestination,
|
|
536
|
+
])
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
await Effect.runPromise(test)
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
it('patches history.forward', async () => {
|
|
543
|
+
const test = testNavigation(function* ($, { currentEntry }) {
|
|
544
|
+
const history = yield* $(History)
|
|
545
|
+
const fiber = yield* $(fxToFiber(currentEntry, 4))
|
|
546
|
+
|
|
547
|
+
history.pushState(undefined, '', '/1')
|
|
548
|
+
history.back()
|
|
549
|
+
history.forward()
|
|
550
|
+
|
|
551
|
+
const results = yield* $(Fiber.join(fiber))
|
|
552
|
+
|
|
553
|
+
assertEqualDestinations(results, [
|
|
554
|
+
testDestination,
|
|
555
|
+
testPathname1Destination,
|
|
556
|
+
testDestination,
|
|
557
|
+
testPathname1Destination,
|
|
558
|
+
])
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
await Effect.runPromise(test)
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
it('patches history.go', async () => {
|
|
565
|
+
const test = testNavigation(function* ($, { currentEntry }) {
|
|
566
|
+
const history = yield* $(History)
|
|
567
|
+
const fiber = yield* $(fxToFiber(currentEntry, 4))
|
|
568
|
+
|
|
569
|
+
history.pushState(undefined, '', '/1')
|
|
570
|
+
history.go(-1)
|
|
571
|
+
history.go(1)
|
|
572
|
+
|
|
573
|
+
const results = yield* $(Fiber.join(fiber))
|
|
574
|
+
|
|
575
|
+
assertEqualDestinations(results, [
|
|
576
|
+
testDestination,
|
|
577
|
+
testPathname1Destination,
|
|
578
|
+
testDestination,
|
|
579
|
+
testPathname1Destination,
|
|
580
|
+
])
|
|
581
|
+
})
|
|
582
|
+
|
|
583
|
+
await Effect.runPromise(test)
|
|
584
|
+
})
|
|
585
|
+
})
|
|
586
|
+
|
|
587
|
+
describe('hashchange', () => {
|
|
588
|
+
it('updates currentEntry when the hash changes', async () => {
|
|
589
|
+
const test = testNavigation(function* ($, { currentEntry }) {
|
|
590
|
+
const fiber = yield* $(fxToFiber(currentEntry, 3))
|
|
591
|
+
|
|
592
|
+
yield* $(changeHash('1'))
|
|
593
|
+
yield* $(changeHash('2'))
|
|
594
|
+
|
|
595
|
+
const results = yield* $(Fiber.join(fiber))
|
|
596
|
+
|
|
597
|
+
assertEqualDestinations(results, [
|
|
598
|
+
testDestination,
|
|
599
|
+
{ ...testDestination, url: new URL(testUrl + '#1') },
|
|
600
|
+
{ ...testDestination, url: new URL(testUrl + '#2') },
|
|
601
|
+
])
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
await Effect.runPromise(test)
|
|
605
|
+
})
|
|
606
|
+
})
|
|
607
|
+
|
|
608
|
+
describe('popstate', () => {
|
|
609
|
+
it('updates currentEntry when the state changes', async () => {
|
|
610
|
+
const test = testNavigation(
|
|
611
|
+
function* ($, { currentEntry, navigate }) {
|
|
612
|
+
const fiber = yield* $(fxToFiber(currentEntry, 3))
|
|
613
|
+
|
|
614
|
+
yield* $(navigate(testPathname1))
|
|
615
|
+
yield* $(popstate(testDestination))
|
|
616
|
+
|
|
617
|
+
const results = yield* $(Fiber.join(fiber))
|
|
618
|
+
|
|
619
|
+
assertEqualDestinations(results, [
|
|
620
|
+
testDestination,
|
|
621
|
+
testPathname1Destination,
|
|
622
|
+
testDestination,
|
|
623
|
+
])
|
|
624
|
+
},
|
|
625
|
+
testUrl,
|
|
626
|
+
{ initialKey: testDestination.key },
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
await Effect.runPromise(test)
|
|
630
|
+
})
|
|
631
|
+
})
|
|
632
|
+
|
|
633
|
+
describe('maxEntries', () => {
|
|
634
|
+
it('allows configuring how many entries are stored', async () => {
|
|
635
|
+
const test = testNavigation(
|
|
636
|
+
function* ($, { currentEntry, entries, navigate }) {
|
|
637
|
+
const fiber = yield* $(fxToFiber(currentEntry, 3))
|
|
638
|
+
|
|
639
|
+
yield* $(navigate(testPathname1))
|
|
640
|
+
yield* $(navigate(testPathname2))
|
|
641
|
+
|
|
642
|
+
const results = yield* $(Fiber.join(fiber))
|
|
643
|
+
|
|
644
|
+
assertEqualDestinations(results, [
|
|
645
|
+
testDestination,
|
|
646
|
+
testPathname1Destination,
|
|
647
|
+
testPathname2Destination,
|
|
648
|
+
])
|
|
649
|
+
|
|
650
|
+
const entriesAfterNavigation = yield* $(entries)
|
|
651
|
+
|
|
652
|
+
assertEqualDestinations(entriesAfterNavigation, [
|
|
653
|
+
testPathname1Destination,
|
|
654
|
+
testPathname2Destination,
|
|
655
|
+
])
|
|
656
|
+
},
|
|
657
|
+
testUrl,
|
|
658
|
+
{ maxEntries: 2 },
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
await Effect.runPromise(test)
|
|
662
|
+
})
|
|
663
|
+
})
|
|
664
|
+
})
|
|
665
|
+
|
|
666
|
+
// Happy-DOM does not send hashchange events when the hash changes programmatically.
|
|
667
|
+
function changeHash(hash: string) {
|
|
668
|
+
return Effect.gen(function* ($) {
|
|
669
|
+
const globalThis = yield* $(GlobalThis)
|
|
670
|
+
const window = yield* $(Window)
|
|
671
|
+
const location = yield* $(Location)
|
|
672
|
+
const oldUrl = location.href
|
|
673
|
+
|
|
674
|
+
location.hash = `#${hash}`
|
|
675
|
+
|
|
676
|
+
const event = new globalThis.HashChangeEvent('hashchange')
|
|
677
|
+
|
|
678
|
+
// Setting these values in the constructor does not work with Happy-DOM.
|
|
679
|
+
;(event as any).oldURL = oldUrl
|
|
680
|
+
;(event as any).newURL = location.href
|
|
681
|
+
|
|
682
|
+
window.dispatchEvent(event)
|
|
683
|
+
})
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function popstate(destination: Destination) {
|
|
687
|
+
return Effect.gen(function* ($) {
|
|
688
|
+
const window = yield* $(Window)
|
|
689
|
+
const globalThis = yield* $(GlobalThis)
|
|
690
|
+
const state = {
|
|
691
|
+
// Not a full event, but enough to get the test to pass
|
|
692
|
+
event: {
|
|
693
|
+
destination: encodeDestination(destination),
|
|
694
|
+
},
|
|
695
|
+
state: destination.state,
|
|
696
|
+
}
|
|
697
|
+
const event = new globalThis.PopStateEvent('popstate')
|
|
698
|
+
|
|
699
|
+
// Setting these values in the constructor does not work with Happy-DOM.
|
|
700
|
+
;(event as any).state = state
|
|
701
|
+
|
|
702
|
+
window.dispatchEvent(event)
|
|
703
|
+
})
|
|
704
|
+
}
|