@equinor/fusion-framework-module-navigation 7.0.9-next.0 → 7.0.10

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.
Files changed (55) hide show
  1. package/dist/esm/version.js +1 -1
  2. package/dist/esm/version.js.map +1 -1
  3. package/dist/tsconfig.tsbuildinfo +1 -1
  4. package/dist/types/version.d.ts +1 -1
  5. package/package.json +10 -7
  6. package/CHANGELOG.md +0 -600
  7. package/src/NavigateEvent.ts +0 -47
  8. package/src/NavigatedEvent.ts +0 -35
  9. package/src/NavigationConfigurator.interface.ts +0 -41
  10. package/src/NavigationConfigurator.ts +0 -226
  11. package/src/NavigationProvider.interface.ts +0 -99
  12. package/src/NavigationProvider.ts +0 -436
  13. package/src/__tests__/BrowserHistory.test.ts +0 -151
  14. package/src/__tests__/HashHistory.test.ts +0 -121
  15. package/src/__tests__/MemoryHistory.test.ts +0 -185
  16. package/src/__tests__/NavigationProvider.test.ts +0 -302
  17. package/src/__tests__/ProxyHistory.test.ts +0 -149
  18. package/src/__tests__/setup.ts +0 -8
  19. package/src/enable-navigation.ts +0 -60
  20. package/src/index.ts +0 -51
  21. package/src/lib/BaseHistory.ts +0 -255
  22. package/src/lib/BrowserHistory.ts +0 -124
  23. package/src/lib/BrowserHistoryHashStack.ts +0 -57
  24. package/src/lib/BrowserHistoryStack.ts +0 -96
  25. package/src/lib/MemoryHistory.ts +0 -76
  26. package/src/lib/MemoryHistoryStack.ts +0 -118
  27. package/src/lib/ProxyHistory.ts +0 -145
  28. package/src/lib/create-history.ts +0 -58
  29. package/src/lib/index.ts +0 -33
  30. package/src/lib/state/actions.ts +0 -108
  31. package/src/lib/state/check-blockers.ts +0 -52
  32. package/src/lib/state/create-flow.ts +0 -36
  33. package/src/lib/state/create-history-reducer.ts +0 -77
  34. package/src/lib/state/create-store.ts +0 -39
  35. package/src/lib/state/flow-creators.ts +0 -7
  36. package/src/lib/state/go.ts +0 -20
  37. package/src/lib/state/history.state.ts +0 -14
  38. package/src/lib/state/index.ts +0 -4
  39. package/src/lib/state/navigate.ts +0 -58
  40. package/src/lib/state/pop.ts +0 -24
  41. package/src/lib/state/validate-current-location.ts +0 -33
  42. package/src/lib/types.ts +0 -190
  43. package/src/lib/utils/encode-trailing-whitespace.ts +0 -18
  44. package/src/lib/utils/has-protocol.ts +0 -21
  45. package/src/lib/utils/index.ts +0 -5
  46. package/src/lib/utils/path-to-string.ts +0 -24
  47. package/src/lib/utils/path-to-url.ts +0 -52
  48. package/src/lib/utils/resolve-browser-location.ts +0 -1
  49. package/src/lib/utils/resolve-hash-location.ts +0 -26
  50. package/src/lib/utils/resolve-path.ts +0 -22
  51. package/src/lib/utils/resolve-window-location.ts +0 -24
  52. package/src/module.ts +0 -80
  53. package/src/version.ts +0 -2
  54. package/tsconfig.json +0 -24
  55. package/vitest.config.ts +0 -14
@@ -1,436 +0,0 @@
1
- // TODO(#5158): Remove @remix-run/router dependency once all apps have migrated to @equinor/fusion-framework-react-router
2
- import { type AgnosticRouteObject, createRouter } from '@remix-run/router';
3
- import type { Observable } from 'rxjs';
4
- import { filter, pairwise, shareReplay } from 'rxjs/operators';
5
-
6
- import {
7
- BaseModuleProvider,
8
- type BaseModuleProviderCtorArgs,
9
- } from '@equinor/fusion-framework-module/provider';
10
-
11
- import type { INavigationProvider } from './NavigationProvider.interface';
12
- import type { INavigationConfigurator } from './NavigationConfigurator.interface';
13
- import type { History, NavigateOptions, NavigationUpdate, Path, To } from './lib/types';
14
- import {
15
- TelemetryLevel,
16
- TelemetryScope,
17
- type ITelemetryProvider,
18
- } from '@equinor/fusion-framework-module-telemetry';
19
- import type { IEventModuleProvider } from '@equinor/fusion-framework-module-event';
20
- import { NavigatedEvent } from './NavigatedEvent';
21
- import { pathToString } from './lib/utils';
22
- import type { BaseHistory } from './lib';
23
-
24
- /**
25
- * Normalizes a pathname by:
26
- * - Collapsing multiple consecutive slashes into a single slash
27
- *
28
- * @example
29
- * normalizePathname("/app//users///profile/") // returns "/app/users/profile/"
30
- * normalizePathname("///multiple///slashes///") // returns "/multiple/slashes/"
31
- *
32
- * @param path - The pathname to normalize
33
- * @returns The normalized pathname without consecutive slashes
34
- */
35
- const normalizePathname = (path: string): string => {
36
- // Use iterative approach instead of regex to avoid potential ReDoS with untrusted input
37
- let result = '';
38
- let lastWasSlash = false;
39
-
40
- // Walk each character once to collapse runs of slashes in a single pass
41
- for (let i = 0; i < path.length; i++) {
42
- const char = path[i];
43
- // Only slashes need de-duplication; every other character passes through
44
- if (char === '/') {
45
- // Keep the first slash of a run, drop the rest
46
- if (!lastWasSlash) {
47
- result += char;
48
- lastWasSlash = true;
49
- }
50
- // Skip consecutive slashes
51
- } else {
52
- result += char;
53
- lastWasSlash = false;
54
- }
55
- }
56
-
57
- return result;
58
- };
59
-
60
- /**
61
- * Removes trailing slashes from a path string.
62
- *
63
- * @param path - The path to trim
64
- * @returns The path without trailing slashes
65
- *
66
- * @example
67
- * stripTrailingSlashes("/apps/my-app/") // returns "/apps/my-app"
68
- * stripTrailingSlashes("/apps/my-app///") // returns "/apps/my-app"
69
- */
70
- const stripTrailingSlashes = (path: string): string => {
71
- // Use iterative approach to avoid ReDoS vulnerability
72
- let endIndex = path.length;
73
- // Shrink endIndex past every trailing slash
74
- while (endIndex > 0 && path[endIndex - 1] === '/') {
75
- endIndex--;
76
- }
77
- return path.substring(0, endIndex);
78
- };
79
-
80
- /**
81
- * Navigation provider implementation.
82
- *
83
- * Manages routing and navigation state with automatic basename localization.
84
- * Wraps a {@link History} instance to expose observable state, path localization,
85
- * and router creation.
86
- *
87
- * @remarks
88
- * - Emits localized paths (basename removed) to consumers via {@link NavigationProvider.state$ | state$}
89
- * - Internally prefixes paths with the basename before forwarding to the history stack
90
- * - Creates routers compatible with Remix / React Router via {@link NavigationProvider.createRouter | createRouter}
91
- * - Dispatches {@link NavigatedEvent} and telemetry on navigation changes
92
- *
93
- * @example
94
- * ```ts
95
- * const provider = new NavigationProvider({ version, config });
96
- * provider.push('/users');
97
- * console.log(provider.path.pathname); // '/users'
98
- * ```
99
- */
100
- export class NavigationProvider
101
- extends BaseModuleProvider<INavigationConfigurator>
102
- implements INavigationProvider
103
- {
104
- #history: History;
105
- #basename?: string;
106
- #state$: Observable<NavigationUpdate>;
107
-
108
- #telemetry?: ITelemetryProvider;
109
- #event?: IEventModuleProvider;
110
-
111
- /**
112
- * Observable stream of navigation state updates.
113
- * @returns Observable of navigation updates.
114
- *
115
- * Emits localized paths (with basename removed) and filters to only
116
- * paths within the basename scope. Late subscribers receive the last
117
- * emitted value immediately.
118
- */
119
- public get state$(): Observable<NavigationUpdate> {
120
- return this.#state$;
121
- }
122
-
123
- /**
124
- * Gets the basename prefix configured for this provider.
125
- *
126
- * @returns The basename string, or an empty string if none is configured
127
- */
128
- public get basename(): string {
129
- return this.#basename ?? '';
130
- }
131
-
132
- /**
133
- * @deprecated Use `history` instead
134
- * @returns The underlying `History` instance.
135
- */
136
- public get navigator(): History {
137
- this.#telemetry?.trackException({
138
- name: 'Navigation::navigator.deprecated',
139
- exception: new Error('navigator is deprecated, use history instead'),
140
- level: TelemetryLevel.Warning,
141
- scope: ['navigation', TelemetryScope.Framework],
142
- });
143
- return this.#history;
144
- }
145
-
146
- /**
147
- * Gets the underlying history instance.
148
- * @returns The {@link History} instance used for navigation
149
- *
150
- * @returns The {@link History} instance used for navigation
151
- */
152
- public get history(): History {
153
- return this.#history;
154
- }
155
-
156
- /**
157
- * Gets the current localized path with the basename prefix removed.
158
- *
159
- * @returns A {@link Path} object representing the current location without basename
160
- */
161
- public get path(): Path {
162
- return this._localizePath(this.#history.location);
163
- }
164
-
165
- /**
166
- * Creates a new {@link NavigationProvider}.
167
- *
168
- * @param args - Configuration arguments containing module config
169
- * @throws {Error} If no history instance is provided in the configuration
170
- */
171
- constructor(args: BaseModuleProviderCtorArgs<INavigationConfigurator>) {
172
- super(args);
173
-
174
- // Extract configuration values
175
- const { basename, history, telemetry, eventProvider } = args.config;
176
-
177
- // Normalize the basename to strip trailing slashes and collapse consecutive
178
- // slashes. React Router requires the current URL to start with the exact
179
- // basename string, so a basename of "/apps/my-app/" would fail to match
180
- // the URL "/apps/my-app" and render nothing (blank page).
181
- // Treat '/' as "no basename" (empty string) since all paths start with '/'.
182
- const normalizedBasename = basename ? stripTrailingSlashes(normalizePathname(basename)) : '';
183
- this.#basename = normalizedBasename || undefined;
184
- this.#event = eventProvider;
185
- this.#telemetry = telemetry;
186
-
187
- // History is required - validate and track error if missing
188
- if (!history) {
189
- this.#telemetry?.trackException({
190
- name: 'Navigation::history.required',
191
- exception: new Error('no history provided!'),
192
- level: TelemetryLevel.Error,
193
- scope: ['navigation', TelemetryScope.Application],
194
- });
195
- throw Error('no history provided!');
196
- }
197
-
198
- this.#history = history;
199
-
200
- // Create state$ observable that filters for paths within basename scope
201
- // shareReplay ensures subscribers get the latest value and share the subscription
202
- this.#state$ = this.#history.state$.pipe(
203
- filter((update: NavigationUpdate) => this._isWithinBasenameScope(update.location.pathname)),
204
- shareReplay({ bufferSize: 1, refCount: true }),
205
- );
206
-
207
- this._initialize();
208
- }
209
-
210
- /**
211
- * Sets up navigation subscriptions for events and telemetry.
212
- *
213
- * Configures subscriptions for:
214
- * - NavigatedEvent dispatching when navigation changes
215
- * - Telemetry tracking for navigation actions and events
216
- * - History disposal cleanup
217
- */
218
- protected _initialize(): void {
219
- // Dispatch NavigatedEvent and track telemetry when navigation changes
220
- // pairwise() gives us [previous, current] pairs to compare navigation state
221
- this._addTeardown(
222
- this.#state$.pipe(pairwise()).subscribe(([previous, current]) => {
223
- // emit NavigatedEvent for other modules to listen to when navigation changes
224
- this.#event?.dispatchEvent(
225
- new NavigatedEvent(
226
- {
227
- action: current.action,
228
- current,
229
- previous,
230
- },
231
- this,
232
- ),
233
- );
234
- // track telemetry for navigation changes
235
- this.#telemetry?.trackEvent({
236
- name: 'Navigation::navigated',
237
- level: TelemetryLevel.Information,
238
- scope: ['navigation', TelemetryScope.Application],
239
- properties: {
240
- action: current.action,
241
- location: pathToString(current.location),
242
- previousLocation: pathToString(previous.location),
243
- },
244
- });
245
- }),
246
- );
247
-
248
- // Track all navigation actions for debugging (if telemetry is enabled)
249
- if (this.#telemetry) {
250
- this._addTeardown(
251
- this.#history.action$.subscribe((action) => {
252
- this.#telemetry?.trackEvent({
253
- name: `Navigation::action:${action.type}`,
254
- level: TelemetryLevel.Debug,
255
- scope: ['navigation', TelemetryScope.Framework],
256
- properties: {
257
- type: action.type,
258
- action: action,
259
- },
260
- });
261
- }),
262
- );
263
- }
264
-
265
- // Clean up history instance when provider is disposed
266
- this._addTeardown(() => this.#history[Symbol.dispose]());
267
- }
268
-
269
- /**
270
- * Creates a router instance from route configuration.
271
- *
272
- * @deprecated Use `@equinor/fusion-framework-react-router` instead.
273
- *
274
- * @param routes - Route configuration objects compatible with Remix/React Router
275
- * @returns A configured and initialized {@link Router} instance with basename applied
276
- */
277
- public createRouter(routes: AgnosticRouteObject[]) {
278
- this.#telemetry?.trackEvent({
279
- name: 'Navigation::createRouter',
280
- level: TelemetryLevel.Warning,
281
- scope: ['navigation', 'deprecated', TelemetryScope.Application],
282
- });
283
- // `this.#history` is typed as the framework's minimal history interface, but Remix Router
284
- // requires its own richer `History` type — the two are runtime-compatible, so cast through
285
- // `unknown`.
286
- const router = createRouter({
287
- basename: this.#basename,
288
- history: this.#history as unknown as import('@remix-run/router').History,
289
- routes,
290
- future: {
291
- v7_prependBasename: true,
292
- },
293
- });
294
- router.initialize();
295
- return router;
296
- }
297
-
298
- /**
299
- * Creates a localized href string including the basename prefix.
300
- *
301
- * @param to - Path or location to resolve (defaults to current path)
302
- * @returns Fully-qualified href string with basename included
303
- *
304
- * @example
305
- * ```ts
306
- * // basename = '/apps/my-app'
307
- * provider.createHref('/users'); // '/apps/my-app/users'
308
- * ```
309
- */
310
- public createHref(to?: To): string {
311
- return this.#history.createHref(this._createToPath(to ?? this.path));
312
- }
313
-
314
- /**
315
- * Creates a full {@link URL} object including the basename prefix.
316
- *
317
- * @param to - Path or location to resolve (defaults to current path)
318
- * @returns A {@link URL} instance representing the resolved navigation target
319
- */
320
- public createURL(to?: To): URL {
321
- return this.#history.createURL(this._createToPath(to ?? this.path));
322
- }
323
-
324
- /**
325
- * Pushes a new navigation entry onto the history stack.
326
- *
327
- * @param to - Path or location to navigate to (relative to basename)
328
- * @param state - Optional state to associate with the navigation
329
- */
330
- public push(to: To, state?: unknown): void {
331
- this.navigate(to, { state });
332
- }
333
-
334
- /**
335
- * Replaces the current history entry with a new one.
336
- *
337
- * @param to - Path or location to navigate to (relative to basename)
338
- * @param state - Optional state to associate with the navigation
339
- */
340
- public replace(to: To, state?: unknown): void {
341
- this.navigate(to, { replace: true, state });
342
- }
343
-
344
- /**
345
- * Navigate to a location with explicit options.
346
- *
347
- * @param to - Optional path or location (defaults to current path)
348
- * @param options - Optional navigation options
349
- */
350
- public navigate(to?: To, options?: Partial<NavigateOptions>): void {
351
- const { replace = false, state } = options ?? {};
352
- this.#history.navigate(this._createToPath(to ?? this.path), { replace, state });
353
- // we need to pop the history to update the notify history listeners
354
- // Frameworks as react router have their internal state management,
355
- // so we need to force a pop to notify the framework that the history has changed
356
- (this.#history as BaseHistory).pop();
357
- }
358
- /**
359
- * Checks whether a pathname falls within the configured basename scope.
360
- *
361
- * Uses a path-boundary check to avoid false positives from apps with
362
- * overlapping name prefixes (e.g. `/apps/my-app` must not match
363
- * `/apps/my-app-other/foo`).
364
- *
365
- * @param pathname - The pathname to check
366
- * @returns `true` if the pathname matches the basename exactly or starts
367
- * with the basename followed by `/` (or no basename is set)
368
- */
369
- protected _isWithinBasenameScope(pathname: string): boolean {
370
- // No basename means everything is in scope
371
- if (!this.#basename) return true;
372
-
373
- // Normalize the pathname for comparison (collapse consecutive slashes)
374
- const normalized = normalizePathname(pathname);
375
-
376
- // Check exact match or path-boundary prefix
377
- return normalized === this.#basename || normalized.startsWith(`${this.#basename}/`);
378
- }
379
-
380
- /**
381
- * Localizes a path by stripping the basename prefix from the pathname.
382
- *
383
- * Only removes the basename when it matches on a path boundary to avoid
384
- * incorrectly stripping partial matches.
385
- *
386
- * @param location - The full path to localize
387
- * @returns A new {@link Path} with the basename removed from the pathname
388
- */
389
- protected _localizePath(location: Path): Path {
390
- const { pathname, search, hash } = location;
391
-
392
- // No basename - return normalized pathname as-is
393
- if (!this.#basename) {
394
- return {
395
- pathname: normalizePathname(pathname) || '/',
396
- search,
397
- hash,
398
- };
399
- }
400
-
401
- const normalized = normalizePathname(pathname);
402
- let localized = normalized;
403
-
404
- // Strip basename only if it matches at path boundary
405
- if (normalized === this.#basename) {
406
- localized = '/';
407
- } else if (normalized.startsWith(`${this.#basename}/`)) {
408
- localized = normalized.slice(this.#basename.length);
409
- }
410
-
411
- return {
412
- pathname: localized || '/',
413
- search,
414
- hash,
415
- };
416
- }
417
-
418
- /**
419
- * Creates a full path object from a target location, prepending the basename prefix.
420
- *
421
- * @param to - The target location (string path or partial {@link Path} object)
422
- * @returns A partial {@link Path} with basename prepended to the pathname
423
- */
424
- protected _createToPath(to: To): Partial<Path> {
425
- // Parse the 'to' parameter into path components
426
- const pathComponents = typeof to === 'string' ? { pathname: to } : to;
427
-
428
- // Extract path parts, defaulting to current path values
429
- const rawPathname = pathComponents.pathname ?? this.path.pathname;
430
- const pathname = normalizePathname(`${this.#basename ?? ''}/${rawPathname}`);
431
- const search = pathComponents.search ?? this.path.search;
432
- const hash = pathComponents.hash ?? this.path.hash;
433
-
434
- return { pathname, search, hash };
435
- }
436
- }
@@ -1,151 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { firstValueFrom, skip } from 'rxjs';
3
- import { filter } from 'rxjs/operators';
4
- import { NavigationProvider } from '../NavigationProvider';
5
- import { createHistory } from '../lib/create-history';
6
- import type { History } from '../lib/types';
7
-
8
- describe('BrowserHistory', () => {
9
- let provider: NavigationProvider;
10
- let history: History;
11
-
12
- beforeEach(() => {
13
- history = createHistory('browser');
14
- provider = new NavigationProvider({
15
- version: '1.0.0',
16
- config: { history },
17
- });
18
- });
19
-
20
- afterEach(() => {
21
- provider?.dispose();
22
- history[Symbol.dispose]();
23
- });
24
-
25
- describe('push', () => {
26
- it('should navigate when push is called', async () => {
27
- const updatePromise = firstValueFrom(history.state$.pipe(skip(1)));
28
- provider.push('/about', { test: 'test' });
29
- await updatePromise;
30
-
31
- expect(provider.path.pathname).toBe(history.location.pathname);
32
-
33
- expect(history.location.pathname).toBe('/about');
34
- expect(history.action).toBe('PUSH');
35
- expect(history.location.state).toEqual({ test: 'test' });
36
- expect(window.location.pathname).toBe('/about');
37
- });
38
-
39
- it('should support browser back/forward navigation', async () => {
40
- history.push('/page1', { page: 1 });
41
- await firstValueFrom(history.state$.pipe(skip(1)));
42
- expect(history.location.pathname).toBe('/page1');
43
-
44
- history.push('/page2', { page: 2 });
45
- await firstValueFrom(history.state$.pipe(skip(1)));
46
- expect(history.location.pathname).toBe('/page2');
47
-
48
- // Go back
49
- const backPromise = firstValueFrom(history.state$.pipe(skip(1)));
50
- window.history.back();
51
- await backPromise;
52
- expect(history.location.pathname).toBe('/page1');
53
- expect(history.action).toBe('POP');
54
- expect(history.location.state).toEqual({ page: 1 });
55
-
56
- // Go forward
57
- const forwardPromise = firstValueFrom(history.state$.pipe(skip(1)));
58
- window.history.forward();
59
- await forwardPromise;
60
- expect(history.location.pathname).toBe('/page2');
61
- expect(history.action).toBe('POP');
62
- expect(history.location.state).toEqual({ page: 2 });
63
- });
64
-
65
- it('should support go() method', async () => {
66
- history.push('/page1');
67
- await firstValueFrom(history.state$.pipe(skip(1)));
68
- expect(history.location.pathname).toBe('/page1');
69
-
70
- history.push('/page2');
71
- await firstValueFrom(history.state$.pipe(skip(1)));
72
- expect(history.location.pathname).toBe('/page2');
73
-
74
- history.push('/page3');
75
- await firstValueFrom(history.state$.pipe(skip(1)));
76
- expect(history.location.pathname).toBe('/page3');
77
-
78
- // Go back - wait for state update to /page2
79
- const goBackPromise = firstValueFrom(
80
- history.state$.pipe(
81
- skip(1),
82
- filter((update) => update.location.pathname === '/page2'),
83
- ),
84
- );
85
- history.go(-1);
86
- await goBackPromise;
87
- expect(history.location.pathname).toBe('/page2');
88
- expect(history.action).toBe('POP');
89
-
90
- // Go forward - wait for state update to /page3
91
- const goForwardPromise = firstValueFrom(
92
- history.state$.pipe(
93
- skip(1),
94
- filter((update) => update.location.pathname === '/page3'),
95
- ),
96
- );
97
- history.go(1);
98
- await goForwardPromise;
99
- expect(history.location.pathname).toBe('/page3');
100
- expect(history.action).toBe('POP');
101
- });
102
- });
103
-
104
- describe('replace', () => {
105
- it('should navigate when replace is called', async () => {
106
- const updatePromise = firstValueFrom(history.state$.pipe(skip(1)));
107
- provider.replace('/about', { test: 'test' });
108
- await updatePromise;
109
-
110
- expect(history.location.pathname).toBe('/about');
111
- expect(history.action).toBe('REPLACE');
112
- expect(history.location.state).toEqual({ test: 'test' });
113
- expect(window.location.pathname).toBe('/about');
114
- });
115
-
116
- it('should replace current entry without adding to history stack', async () => {
117
- // Start at root
118
- expect(history.location.pathname).toBe('/');
119
-
120
- // Push an entry
121
- history.push('/page1', { page: 1 });
122
- await firstValueFrom(history.state$.pipe(skip(1)));
123
- expect(history.location.pathname).toBe('/page1');
124
-
125
- // Replace current entry
126
- history.replace('/page2', { page: 2 });
127
- await firstValueFrom(history.state$.pipe(skip(1)));
128
- expect(history.location.pathname).toBe('/page2');
129
- expect(history.action).toBe('REPLACE');
130
-
131
- // Go back - should go to root, not page1 (because page1 was replaced)
132
- const backPromise = firstValueFrom(history.state$.pipe(skip(1)));
133
- window.history.back();
134
- await backPromise;
135
- expect(history.location.pathname).toBe('/');
136
- expect(history.action).toBe('POP');
137
- });
138
- });
139
-
140
- describe('state$ observable', () => {
141
- it('should emit state updates on navigation', async () => {
142
- const updatePromise = firstValueFrom(history.state$.pipe(skip(1)));
143
- history.push('/test', { data: 'test' });
144
- const update = await updatePromise;
145
-
146
- expect(update.action).toBe('PUSH');
147
- expect(update.location.pathname).toBe('/test');
148
- expect(update.location.state).toEqual({ data: 'test' });
149
- });
150
- });
151
- });