@nocobase/client-v2 2.1.15 → 2.1.18

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.
@@ -7,11 +7,12 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { FlowContext, FlowEngine } from '@nocobase/flow-engine';
10
+ import { FlowContext, FlowEngine, type FlowModel } from '@nocobase/flow-engine';
11
+ import type React from 'react';
11
12
  import { describe, expect, it, vi, beforeEach } from 'vitest';
12
13
  import { getViewDiffAndUpdateHidden } from '../../getViewDiffAndUpdateHidden';
13
14
  import { getOpenViewStepParams } from '../../flows/openViewFlow';
14
- import { resolveViewParamsToViewList, updateViewListHidden } from '../../resolveViewParamsToViewList';
15
+ import { resolveViewParamsToViewList, updateViewListHidden, type ViewItem } from '../../resolveViewParamsToViewList';
15
16
  import { AdminLayoutRouteCoordinator } from '../AdminLayoutRouteCoordinator';
16
17
  import { BaseLayoutRouteCoordinator, toViewStack } from '../BaseLayoutRouteCoordinator';
17
18
  import { RouteModel } from '../../models/base/RouteModel';
@@ -27,9 +28,9 @@ vi.mock('../../getViewDiffAndUpdateHidden', () => ({
27
28
  }));
28
29
 
29
30
  vi.mock('../../flows/openViewFlow', async (importOriginal) => {
30
- const actual = await importOriginal();
31
+ const actual = await importOriginal<typeof import('../../flows/openViewFlow')>();
31
32
  return {
32
- ...(actual as any),
33
+ ...actual,
33
34
  getOpenViewStepParams: vi.fn(),
34
35
  };
35
36
  });
@@ -39,7 +40,48 @@ const mockGetViewDiffAndUpdateHidden = vi.mocked(getViewDiffAndUpdateHidden);
39
40
  const mockGetOpenViewStepParams = vi.mocked(getOpenViewStepParams);
40
41
  const mockUpdateViewListHidden = vi.mocked(updateViewListHidden);
41
42
 
42
- function setupRouteReplay(viewParams: Record<string, any>) {
43
+ type RouteOpenPayload = {
44
+ target?: HTMLElement | null;
45
+ destroyRef: React.RefObject<(result?: unknown, force?: boolean) => void>;
46
+ updateRef: React.RefObject<(value: unknown) => void>;
47
+ activateRef: React.RefObject<(forceRefresh?: boolean) => void>;
48
+ deactivateRef: React.RefObject<() => void>;
49
+ onOpen: () => void;
50
+ pageActive: boolean;
51
+ activationControlledByLayout: boolean;
52
+ hidden: { value: boolean };
53
+ triggerByRouter: boolean;
54
+ [key: string]: unknown;
55
+ };
56
+
57
+ type DispatchEventMock = ReturnType<typeof createDispatchEventMock>;
58
+
59
+ const createDispatchEventMock = (
60
+ implementation?: (eventName: string, payload: RouteOpenPayload) => Promise<unknown[] | void>,
61
+ ) => vi.fn((eventName: string, payload: RouteOpenPayload) => implementation?.(eventName, payload) ?? Promise.resolve());
62
+
63
+ const createViewModel = (uid: string, dispatchEvent: DispatchEventMock) =>
64
+ ({
65
+ uid,
66
+ dispatchEvent,
67
+ }) as unknown as FlowModel;
68
+
69
+ const createViewItem = (
70
+ uid: string,
71
+ dispatchEvent: DispatchEventMock,
72
+ index = 0,
73
+ params: ViewItem['params'] = { viewUid: uid },
74
+ ): ViewItem => ({
75
+ params,
76
+ modelUid: uid,
77
+ model: createViewModel(uid, dispatchEvent),
78
+ hidden: { value: false },
79
+ index,
80
+ });
81
+
82
+ const nextTick = () => new Promise((resolve) => setTimeout(resolve, 0));
83
+
84
+ function setupRouteReplay(viewParams: Record<string, unknown>) {
43
85
  const engine = new FlowEngine();
44
86
  engine.registerModels({ RouteModel });
45
87
  engine.context.defineProperty('route', {
@@ -54,87 +96,713 @@ function setupRouteReplay(viewParams: Record<string, any>) {
54
96
  },
55
97
  });
56
98
 
57
- const dispatchEvent = vi.fn((_eventName: string, _payload: any) => Promise.resolve());
58
- const viewItem = {
59
- params: {
60
- viewUid: 'popup',
61
- filterByTk: 'member',
62
- ...viewParams,
63
- },
64
- modelUid: 'popup',
65
- model: { uid: 'popup', dispatchEvent } as any,
66
- hidden: { value: false },
67
- index: 0,
68
- };
99
+ const dispatchEvent = createDispatchEventMock();
100
+ const viewItem = createViewItem('popup', dispatchEvent, 0, {
101
+ viewUid: 'popup',
102
+ filterByTk: 'member',
103
+ ...viewParams,
104
+ });
105
+
106
+ mockResolveViewParamsToViewList.mockReturnValue([viewItem]);
107
+ mockGetViewDiffAndUpdateHidden.mockReturnValue({
108
+ viewsToClose: [],
109
+ viewsToOpen: [viewItem],
110
+ });
111
+ mockGetOpenViewStepParams.mockReturnValue({
112
+ collectionName: 'roles',
113
+ associationName: 'users.roles',
114
+ dataSourceKey: 'main',
115
+ });
116
+
117
+ const coordinator = new AdminLayoutRouteCoordinator(engine);
118
+ coordinator.registerPage('test-route', {
119
+ active: true,
120
+ layoutContentElement: document.createElement('div'),
121
+ });
122
+ coordinator.syncRoute({
123
+ params: { name: 'test-route' },
124
+ pathname: '/admin/popup/filterbytk/member',
125
+ });
126
+
127
+ return { dispatchEvent };
128
+ }
129
+
130
+ describe('AdminLayoutRouteCoordinator', () => {
131
+ beforeEach(() => {
132
+ vi.clearAllMocks();
133
+ });
134
+
135
+ it('drops configured associationName during route replay when sourceId is absent', () => {
136
+ const { dispatchEvent } = setupRouteReplay({});
137
+
138
+ expect(dispatchEvent.mock.calls[0][1]).toMatchObject({
139
+ collectionName: 'roles',
140
+ associationName: null,
141
+ dataSourceKey: 'main',
142
+ filterByTk: 'member',
143
+ triggerByRouter: true,
144
+ });
145
+ });
146
+
147
+ it('keeps configured associationName during route replay when sourceId is present', () => {
148
+ const { dispatchEvent } = setupRouteReplay({ sourceId: '1' });
149
+
150
+ expect(dispatchEvent.mock.calls[0][1]).toMatchObject({
151
+ collectionName: 'roles',
152
+ associationName: 'users.roles',
153
+ dataSourceKey: 'main',
154
+ filterByTk: 'member',
155
+ sourceId: '1',
156
+ triggerByRouter: true,
157
+ });
158
+ });
159
+
160
+ it('passes RunJS openView route state as runtime mode and size during route replay', () => {
161
+ const { dispatchEvent } = setupRouteReplay({
162
+ openViewRouteState: { mode: 'dialog', size: 'large' },
163
+ });
164
+
165
+ expect(dispatchEvent.mock.calls[0][1]).toMatchObject({
166
+ mode: 'dialog',
167
+ size: 'large',
168
+ openViewRouteState: { mode: 'dialog', size: 'large' },
169
+ triggerByRouter: true,
170
+ });
171
+ });
172
+
173
+ it('uses layout content element before route page placeholder as view target', () => {
174
+ const engine = new FlowEngine();
175
+ engine.registerModels({ RouteModel });
176
+ engine.context.defineProperty('route', {
177
+ value: {},
178
+ });
179
+ engine.context.defineProperty('routeRepository', {
180
+ value: {
181
+ getRouteBySchemaUid: vi.fn(() => ({})),
182
+ },
183
+ });
184
+
185
+ const layoutContentElement = document.createElement('div');
186
+ const routePageElement = document.createElement('div');
187
+ const dispatchEvent = createDispatchEventMock();
188
+ const viewItem = createViewItem('test-route', dispatchEvent);
189
+ mockResolveViewParamsToViewList.mockReturnValue([viewItem]);
190
+ mockGetViewDiffAndUpdateHidden.mockReturnValue({
191
+ viewsToClose: [],
192
+ viewsToOpen: [viewItem],
193
+ });
194
+ mockGetOpenViewStepParams.mockReturnValue({
195
+ collectionName: '',
196
+ dataSourceKey: '',
197
+ });
198
+
199
+ const coordinator = new BaseLayoutRouteCoordinator(engine, { basePathname: '/admin' });
200
+ coordinator.setLayoutContentElement(layoutContentElement);
201
+ coordinator.registerPage('test-route', {
202
+ active: true,
203
+ layoutContentElement: routePageElement,
204
+ });
205
+ coordinator.syncRoute({
206
+ pageUid: 'test-route',
207
+ pathname: '/admin/test-route',
208
+ });
209
+
210
+ expect(dispatchEvent.mock.calls[0][1].target).toBe(layoutContentElement);
211
+ });
212
+
213
+ it('replays the active route view when the layout content element changes', () => {
214
+ const engine = new FlowEngine();
215
+ engine.registerModels({ RouteModel });
216
+ engine.context.defineProperty('route', {
217
+ value: {},
218
+ });
219
+ engine.context.defineProperty('routeRepository', {
220
+ value: {
221
+ getRouteBySchemaUid: vi.fn(() => ({})),
222
+ },
223
+ });
224
+
225
+ const firstDestroy = vi.fn();
226
+ const dispatchEvent = createDispatchEventMock((_eventName, payload) => {
227
+ if (dispatchEvent.mock.calls.length === 1) {
228
+ payload.destroyRef.current = firstDestroy;
229
+ }
230
+ return Promise.resolve();
231
+ });
232
+ const viewItem = createViewItem('test-route', dispatchEvent);
233
+ mockResolveViewParamsToViewList.mockReturnValue([viewItem]);
234
+ mockGetViewDiffAndUpdateHidden.mockReturnValue({
235
+ viewsToClose: [],
236
+ viewsToOpen: [viewItem],
237
+ });
238
+ mockGetOpenViewStepParams.mockReturnValue({
239
+ collectionName: '',
240
+ dataSourceKey: '',
241
+ });
242
+
243
+ const firstLayoutContentElement = document.createElement('div');
244
+ const secondLayoutContentElement = document.createElement('div');
245
+ const routePageElement = document.createElement('div');
246
+
247
+ const coordinator = new BaseLayoutRouteCoordinator(engine, { basePathname: '/admin' });
248
+ coordinator.setLayoutContentElement(firstLayoutContentElement);
249
+ const routeModel = coordinator.registerPage('test-route', {
250
+ active: true,
251
+ layoutContentElement: routePageElement,
252
+ });
253
+ coordinator.syncRoute({
254
+ pageUid: 'test-route',
255
+ pathname: '/admin/test-route',
256
+ });
257
+
258
+ expect(dispatchEvent).toHaveBeenCalledTimes(1);
259
+ expect(dispatchEvent.mock.calls[0][1].target).toBe(firstLayoutContentElement);
260
+
261
+ coordinator.setLayoutContentElement(null);
262
+ expect(dispatchEvent).toHaveBeenCalledTimes(1);
263
+
264
+ coordinator.setLayoutContentElement(secondLayoutContentElement);
265
+
266
+ expect(firstDestroy).toHaveBeenCalledTimes(1);
267
+ expect(dispatchEvent).toHaveBeenCalledTimes(2);
268
+ expect(dispatchEvent.mock.calls[1][1].target).toBe(secondLayoutContentElement);
269
+ expect(engine.getModel('test-route')).toBe(routeModel);
270
+ });
271
+
272
+ it('reopens an inactive cached route view on the new layout content element when it becomes active again', () => {
273
+ const engine = new FlowEngine();
274
+ engine.registerModels({ RouteModel });
275
+ engine.context.defineProperty('route', {
276
+ value: {},
277
+ });
278
+ engine.context.defineProperty('routeRepository', {
279
+ value: {
280
+ getRouteBySchemaUid: vi.fn(() => ({})),
281
+ },
282
+ });
283
+
284
+ const dispatchEvent = createDispatchEventMock((_eventName, payload) => {
285
+ payload.destroyRef.current = vi.fn();
286
+ payload.activateRef.current = vi.fn();
287
+ payload.deactivateRef.current = vi.fn();
288
+ return Promise.resolve();
289
+ });
290
+ const viewItem = createViewItem('test-route', dispatchEvent);
291
+ mockResolveViewParamsToViewList.mockReturnValue([viewItem]);
292
+ mockGetViewDiffAndUpdateHidden.mockImplementation((prevViewList, currentViewList) => ({
293
+ viewsToClose: prevViewList.filter((prevViewItem) => !currentViewList.includes(prevViewItem)),
294
+ viewsToOpen: currentViewList.filter((currentViewItem) => !prevViewList.includes(currentViewItem)),
295
+ }));
296
+ mockGetOpenViewStepParams.mockReturnValue({
297
+ collectionName: '',
298
+ dataSourceKey: '',
299
+ });
300
+
301
+ const firstLayoutContentElement = document.createElement('div');
302
+ const secondLayoutContentElement = document.createElement('div');
303
+ const coordinator = new BaseLayoutRouteCoordinator(engine, { basePathname: '/admin' });
304
+ coordinator.setLayoutContentElement(firstLayoutContentElement);
305
+ coordinator.registerPage('test-route', {
306
+ active: true,
307
+ layoutContentElement: document.createElement('div'),
308
+ });
309
+ coordinator.syncRoute({
310
+ pageUid: 'test-route',
311
+ pathname: '/admin/test-route',
312
+ });
313
+
314
+ coordinator.syncRoute({
315
+ pageUid: 'other-route',
316
+ pathname: '/admin/other-route',
317
+ });
318
+ coordinator.setLayoutContentElement(secondLayoutContentElement);
319
+ coordinator.syncRoute({
320
+ pageUid: 'test-route',
321
+ pathname: '/admin/test-route',
322
+ });
323
+
324
+ expect(dispatchEvent).toHaveBeenCalledTimes(2);
325
+ expect(dispatchEvent.mock.calls[1][1].target).toBe(secondLayoutContentElement);
326
+ });
327
+
328
+ it('ignores stale view onOpen callbacks after the layout content element changes', () => {
329
+ const engine = new FlowEngine();
330
+ engine.registerModels({ RouteModel });
331
+ engine.context.defineProperty('route', {
332
+ value: {},
333
+ });
334
+ engine.context.defineProperty('routeRepository', {
335
+ value: {
336
+ getRouteBySchemaUid: vi.fn(() => ({})),
337
+ },
338
+ });
339
+
340
+ const rootDispatchEvent = createDispatchEventMock((_eventName, payload) => {
341
+ payload.destroyRef.current = vi.fn();
342
+ return Promise.resolve();
343
+ });
344
+ let stalePopupOnOpen: (() => void) | undefined;
345
+ const popupDispatchEvent = createDispatchEventMock((_eventName, payload) => {
346
+ stalePopupOnOpen = payload.onOpen;
347
+ payload.destroyRef.current = vi.fn();
348
+ return Promise.resolve();
349
+ });
350
+ const detailDispatchEvent = createDispatchEventMock((_eventName, payload) => {
351
+ payload.destroyRef.current = vi.fn();
352
+ return Promise.resolve();
353
+ });
354
+ const rootViewItem = createViewItem('test-route', rootDispatchEvent);
355
+ const popupViewItem = createViewItem('popup', popupDispatchEvent, 1);
356
+ const detailViewItem = createViewItem('detail', detailDispatchEvent, 2);
357
+ let resolvedViewList: ViewItem[] = [rootViewItem];
358
+ mockResolveViewParamsToViewList.mockImplementation(() => resolvedViewList);
359
+ mockGetViewDiffAndUpdateHidden.mockImplementation((prevViewList, currentViewList) => ({
360
+ viewsToClose: prevViewList.filter((prevViewItem) => !currentViewList.includes(prevViewItem)),
361
+ viewsToOpen: currentViewList.filter((currentViewItem) => !prevViewList.includes(currentViewItem)),
362
+ }));
363
+ mockGetOpenViewStepParams.mockReturnValue({
364
+ collectionName: '',
365
+ dataSourceKey: '',
366
+ });
367
+
368
+ const coordinator = new BaseLayoutRouteCoordinator(engine, { basePathname: '/admin' });
369
+ coordinator.setLayoutContentElement(document.createElement('div'));
370
+ coordinator.registerPage('test-route', {
371
+ active: true,
372
+ layoutContentElement: document.createElement('div'),
373
+ });
374
+ coordinator.syncRoute({
375
+ pageUid: 'test-route',
376
+ pathname: '/admin/test-route',
377
+ });
378
+
379
+ resolvedViewList = [rootViewItem, popupViewItem, detailViewItem];
380
+ coordinator.syncRoute({
381
+ pageUid: 'test-route',
382
+ pathname: '/admin/test-route',
383
+ });
384
+
385
+ expect(popupDispatchEvent).toHaveBeenCalledTimes(1);
386
+ expect(detailDispatchEvent).not.toHaveBeenCalled();
387
+ expect(stalePopupOnOpen).toBeDefined();
388
+
389
+ resolvedViewList = [rootViewItem];
390
+ coordinator.setLayoutContentElement(document.createElement('div'));
391
+ stalePopupOnOpen?.();
392
+
393
+ expect(detailDispatchEvent).not.toHaveBeenCalled();
394
+ });
395
+
396
+ it('ignores stale async model loads after the layout content element changes', async () => {
397
+ const engine = new FlowEngine();
398
+ engine.registerModels({ RouteModel });
399
+ engine.context.defineProperty('route', {
400
+ value: {},
401
+ });
402
+ engine.context.defineProperty('routeRepository', {
403
+ value: {
404
+ getRouteBySchemaUid: vi.fn(() => ({})),
405
+ },
406
+ });
407
+
408
+ const rootDispatchEvent = createDispatchEventMock((_eventName, payload) => {
409
+ payload.destroyRef.current = vi.fn();
410
+ return Promise.resolve();
411
+ });
412
+ const popupDispatchEvent = createDispatchEventMock((_eventName, payload) => {
413
+ payload.destroyRef.current = vi.fn();
414
+ return Promise.resolve();
415
+ });
416
+ const rootViewItem = createViewItem('test-route', rootDispatchEvent);
417
+ const popupViewItem: ViewItem = {
418
+ params: { viewUid: 'popup' },
419
+ modelUid: 'popup',
420
+ model: undefined,
421
+ hidden: { value: false },
422
+ index: 1,
423
+ };
424
+ let resolvePopupModel: (model: FlowModel) => void = () => {};
425
+ vi.spyOn(engine, 'loadModel').mockImplementation(
426
+ () =>
427
+ new Promise((resolve) => {
428
+ resolvePopupModel = resolve;
429
+ }),
430
+ );
431
+ let resolvedViewList: ViewItem[] = [rootViewItem];
432
+ mockResolveViewParamsToViewList.mockImplementation(() => resolvedViewList);
433
+ mockGetViewDiffAndUpdateHidden.mockImplementation((prevViewList, currentViewList) => ({
434
+ viewsToClose: prevViewList.filter((prevViewItem) => !currentViewList.includes(prevViewItem)),
435
+ viewsToOpen: currentViewList.filter((currentViewItem) => !prevViewList.includes(currentViewItem)),
436
+ }));
437
+ mockGetOpenViewStepParams.mockReturnValue({
438
+ collectionName: '',
439
+ dataSourceKey: '',
440
+ });
441
+
442
+ const coordinator = new BaseLayoutRouteCoordinator(engine, { basePathname: '/admin' });
443
+ coordinator.setLayoutContentElement(document.createElement('div'));
444
+ coordinator.registerPage('test-route', {
445
+ active: true,
446
+ layoutContentElement: document.createElement('div'),
447
+ });
448
+ coordinator.syncRoute({
449
+ pageUid: 'test-route',
450
+ pathname: '/admin/test-route',
451
+ });
452
+
453
+ resolvedViewList = [rootViewItem, popupViewItem];
454
+ coordinator.syncRoute({
455
+ pageUid: 'test-route',
456
+ pathname: '/admin/test-route',
457
+ });
458
+
459
+ resolvedViewList = [rootViewItem];
460
+ coordinator.setLayoutContentElement(document.createElement('div'));
461
+ resolvePopupModel(createViewModel('popup', popupDispatchEvent));
462
+ await nextTick();
463
+
464
+ expect(popupDispatchEvent.mock.calls.length).toBe(0);
465
+ });
466
+
467
+ it('ignores stale async view opens after the pathname changes', async () => {
468
+ const engine = new FlowEngine();
469
+ engine.registerModels({ RouteModel });
470
+ engine.context.defineProperty('route', {
471
+ value: {},
472
+ });
473
+ engine.context.defineProperty('routeRepository', {
474
+ value: {
475
+ getRouteBySchemaUid: vi.fn(() => ({})),
476
+ },
477
+ });
478
+
479
+ const rootDispatchEvent = createDispatchEventMock((_eventName, payload) => {
480
+ payload.destroyRef.current = vi.fn();
481
+ return Promise.resolve();
482
+ });
483
+ const popupDispatchEvent = createDispatchEventMock((_eventName, payload) => {
484
+ payload.destroyRef.current = vi.fn();
485
+ return Promise.resolve();
486
+ });
487
+ const rootViewItem = createViewItem('test-route', rootDispatchEvent);
488
+ const popupViewItem: ViewItem = {
489
+ params: { viewUid: 'popup' },
490
+ modelUid: 'popup',
491
+ model: undefined,
492
+ hidden: { value: false },
493
+ index: 1,
494
+ };
495
+ const resolvePopupModels: Array<(model: FlowModel) => void> = [];
496
+ vi.spyOn(engine, 'loadModel').mockImplementation(
497
+ () =>
498
+ new Promise((resolve) => {
499
+ resolvePopupModels.push(resolve);
500
+ }),
501
+ );
502
+ let resolvedViewList: ViewItem[] = [rootViewItem];
503
+ mockResolveViewParamsToViewList.mockImplementation(() => resolvedViewList);
504
+ mockGetViewDiffAndUpdateHidden.mockImplementation((prevViewList, currentViewList) => ({
505
+ viewsToClose: prevViewList.filter((prevViewItem) => !currentViewList.includes(prevViewItem)),
506
+ viewsToOpen: currentViewList.filter((currentViewItem) => !prevViewList.includes(currentViewItem)),
507
+ }));
508
+ mockGetOpenViewStepParams.mockReturnValue({
509
+ collectionName: '',
510
+ dataSourceKey: '',
511
+ });
512
+
513
+ const coordinator = new BaseLayoutRouteCoordinator(engine, { basePathname: '/admin' });
514
+ coordinator.setLayoutContentElement(document.createElement('div'));
515
+ coordinator.registerPage('test-route', {
516
+ active: true,
517
+ layoutContentElement: document.createElement('div'),
518
+ });
519
+ coordinator.syncRoute({
520
+ pageUid: 'test-route',
521
+ pathname: '/admin/test-route',
522
+ });
523
+
524
+ resolvedViewList = [rootViewItem, popupViewItem];
525
+ coordinator.syncRoute({
526
+ pageUid: 'test-route',
527
+ pathname: '/admin/test-route/view/popup',
528
+ });
529
+
530
+ resolvedViewList = [rootViewItem];
531
+ coordinator.syncRoute({
532
+ pageUid: 'test-route',
533
+ pathname: '/admin/test-route',
534
+ });
535
+ resolvePopupModels.forEach((resolvePopupModel) => {
536
+ resolvePopupModel(createViewModel('popup', popupDispatchEvent));
537
+ });
538
+ await nextTick();
539
+
540
+ expect(popupDispatchEvent.mock.calls.length).toBe(0);
541
+ });
542
+
543
+ it('does not duplicate pending async view opens when the same pathname syncs again', async () => {
544
+ const engine = new FlowEngine();
545
+ engine.registerModels({ RouteModel });
546
+ engine.context.defineProperty('route', {
547
+ value: {},
548
+ });
549
+ engine.context.defineProperty('routeRepository', {
550
+ value: {
551
+ getRouteBySchemaUid: vi.fn(() => ({})),
552
+ },
553
+ });
554
+
555
+ const rootDispatchEvent = createDispatchEventMock((_eventName, payload) => {
556
+ payload.destroyRef.current = vi.fn();
557
+ return Promise.resolve();
558
+ });
559
+ const popupDispatchEvent = createDispatchEventMock((_eventName, payload) => {
560
+ payload.destroyRef.current = vi.fn();
561
+ return Promise.resolve();
562
+ });
563
+ const rootViewItem = createViewItem('test-route', rootDispatchEvent);
564
+ const popupViewItem: ViewItem = {
565
+ params: { viewUid: 'popup' },
566
+ modelUid: 'popup',
567
+ model: undefined,
568
+ hidden: { value: false },
569
+ index: 1,
570
+ };
571
+ const resolvePopupModels: Array<(model: FlowModel) => void> = [];
572
+ vi.spyOn(engine, 'loadModel').mockImplementation(
573
+ () =>
574
+ new Promise((resolve) => {
575
+ resolvePopupModels.push(resolve);
576
+ }),
577
+ );
578
+ let resolvedViewList: ViewItem[] = [rootViewItem];
579
+ mockResolveViewParamsToViewList.mockImplementation(() => resolvedViewList);
580
+ mockGetViewDiffAndUpdateHidden.mockImplementation((prevViewList, currentViewList) => ({
581
+ viewsToClose: prevViewList.filter((prevViewItem) => !currentViewList.includes(prevViewItem)),
582
+ viewsToOpen: currentViewList.filter((currentViewItem) => !prevViewList.includes(currentViewItem)),
583
+ }));
584
+ mockGetOpenViewStepParams.mockReturnValue({
585
+ collectionName: '',
586
+ dataSourceKey: '',
587
+ });
588
+
589
+ const coordinator = new BaseLayoutRouteCoordinator(engine, { basePathname: '/admin' });
590
+ coordinator.setLayoutContentElement(document.createElement('div'));
591
+ coordinator.registerPage('test-route', {
592
+ active: true,
593
+ layoutContentElement: document.createElement('div'),
594
+ });
595
+ coordinator.syncRoute({
596
+ pageUid: 'test-route',
597
+ pathname: '/admin/test-route',
598
+ });
599
+
600
+ resolvedViewList = [rootViewItem, popupViewItem];
601
+ coordinator.syncRoute({
602
+ pageUid: 'test-route',
603
+ pathname: '/admin/test-route/view/popup',
604
+ });
605
+ coordinator.syncRoute({
606
+ pageUid: 'test-route',
607
+ pathname: '/admin/test-route/view/popup',
608
+ });
609
+
610
+ resolvePopupModels.forEach((resolvePopupModel) => {
611
+ resolvePopupModel(createViewModel('popup', popupDispatchEvent));
612
+ });
613
+ await nextTick();
614
+
615
+ expect(popupDispatchEvent).toHaveBeenCalledTimes(1);
616
+ });
617
+
618
+ it('clears pending route view opens when dispatchEvent throws synchronously', async () => {
619
+ const engine = new FlowEngine();
620
+ engine.registerModels({ RouteModel });
621
+ engine.context.defineProperty('route', {
622
+ value: {},
623
+ });
624
+ engine.context.defineProperty('routeRepository', {
625
+ value: {
626
+ getRouteBySchemaUid: vi.fn(() => ({})),
627
+ },
628
+ });
629
+
630
+ const dispatchError = new Error('open failed');
631
+ const dispatchEvent = createDispatchEventMock((_eventName, payload) => {
632
+ if (dispatchEvent.mock.calls.length === 1) {
633
+ throw dispatchError;
634
+ }
69
635
 
70
- mockResolveViewParamsToViewList.mockReturnValue([viewItem]);
71
- mockGetViewDiffAndUpdateHidden.mockReturnValue({
72
- viewsToClose: [],
73
- viewsToOpen: [viewItem],
74
- });
75
- mockGetOpenViewStepParams.mockReturnValue({
76
- collectionName: 'roles',
77
- associationName: 'users.roles',
78
- dataSourceKey: 'main',
79
- } as any);
636
+ payload.destroyRef.current = vi.fn();
637
+ return Promise.resolve();
638
+ });
639
+ const viewItem = createViewItem('test-route', dispatchEvent);
640
+ mockResolveViewParamsToViewList.mockReturnValue([viewItem]);
641
+ mockGetViewDiffAndUpdateHidden.mockImplementation((prevViewList, currentViewList) => ({
642
+ viewsToClose: prevViewList.filter((prevViewItem) => !currentViewList.includes(prevViewItem)),
643
+ viewsToOpen: currentViewList.filter((currentViewItem) => !prevViewList.includes(currentViewItem)),
644
+ }));
645
+ mockGetOpenViewStepParams.mockReturnValue({
646
+ collectionName: '',
647
+ dataSourceKey: '',
648
+ });
649
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
80
650
 
81
- const coordinator = new AdminLayoutRouteCoordinator(engine);
82
- coordinator.registerPage('test-route', {
83
- active: true,
84
- layoutContentElement: document.createElement('div'),
85
- });
86
- coordinator.syncRoute({
87
- params: { name: 'test-route' },
88
- pathname: '/admin/popup/filterbytk/member',
89
- });
651
+ const coordinator = new BaseLayoutRouteCoordinator(engine, { basePathname: '/admin' });
652
+ coordinator.setLayoutContentElement(document.createElement('div'));
653
+ coordinator.registerPage('test-route', {
654
+ active: true,
655
+ layoutContentElement: document.createElement('div'),
656
+ });
657
+ coordinator.syncRoute({
658
+ pageUid: 'test-route',
659
+ pathname: '/admin/test-route',
660
+ });
661
+ await nextTick();
90
662
 
91
- return { dispatchEvent };
92
- }
663
+ coordinator.syncRoute({
664
+ pageUid: 'test-route',
665
+ pathname: '/admin/test-route',
666
+ });
93
667
 
94
- describe('AdminLayoutRouteCoordinator', () => {
95
- beforeEach(() => {
96
- vi.clearAllMocks();
668
+ expect(dispatchEvent).toHaveBeenCalledTimes(2);
669
+ consoleErrorSpy.mockRestore();
97
670
  });
98
671
 
99
- it('drops configured associationName during route replay when sourceId is absent', () => {
100
- const { dispatchEvent } = setupRouteReplay({});
672
+ it('ignores stale initial deep-link replay after the pathname changes before a view opens', async () => {
673
+ const engine = new FlowEngine();
674
+ engine.registerModels({ RouteModel });
675
+ engine.context.defineProperty('router', {
676
+ value: {
677
+ navigate: vi.fn(),
678
+ },
679
+ });
680
+ engine.context.defineProperty('route', {
681
+ value: {},
682
+ });
683
+ engine.context.defineProperty('routeRepository', {
684
+ value: {
685
+ getRouteBySchemaUid: vi.fn(() => ({})),
686
+ },
687
+ });
101
688
 
102
- expect(dispatchEvent.mock.calls[0][1]).toMatchObject({
103
- collectionName: 'roles',
104
- associationName: null,
105
- dataSourceKey: 'main',
106
- filterByTk: 'member',
107
- triggerByRouter: true,
689
+ const rootDispatchEvent = createDispatchEventMock((_eventName, payload) => {
690
+ payload.destroyRef.current = vi.fn();
691
+ payload.onOpen();
692
+ return Promise.resolve();
693
+ });
694
+ const popupDispatchEvent = createDispatchEventMock((_eventName, payload) => {
695
+ payload.destroyRef.current = vi.fn();
696
+ return Promise.resolve();
697
+ });
698
+ const rootViewItem = createViewItem('test-route', rootDispatchEvent);
699
+ const popupViewItem = createViewItem('popup', popupDispatchEvent, 1);
700
+ mockResolveViewParamsToViewList.mockImplementation((_engine, viewParams) => {
701
+ if (viewParams[1]?.viewUid === 'popup') {
702
+ return [rootViewItem, popupViewItem];
703
+ }
704
+ return [];
705
+ });
706
+ mockGetViewDiffAndUpdateHidden.mockImplementation((prevViewList, currentViewList) => ({
707
+ viewsToClose: prevViewList.filter((prevViewItem) => !currentViewList.includes(prevViewItem)),
708
+ viewsToOpen: currentViewList.filter((currentViewItem) => !prevViewList.includes(currentViewItem)),
709
+ }));
710
+ mockGetOpenViewStepParams.mockReturnValue({
711
+ collectionName: '',
712
+ dataSourceKey: '',
108
713
  });
109
- });
110
714
 
111
- it('keeps configured associationName during route replay when sourceId is present', () => {
112
- const { dispatchEvent } = setupRouteReplay({ sourceId: '1' });
715
+ const coordinator = new BaseLayoutRouteCoordinator(engine, { basePathname: '/admin' });
716
+ coordinator.setLayoutContentElement(document.createElement('div'));
717
+ coordinator.registerPage('test-route', {
718
+ active: true,
719
+ layoutContentElement: document.createElement('div'),
720
+ });
721
+ coordinator.syncRoute({
722
+ pageUid: 'test-route',
723
+ pathname: '/admin/test-route/view/popup',
724
+ });
113
725
 
114
- expect(dispatchEvent.mock.calls[0][1]).toMatchObject({
115
- collectionName: 'roles',
116
- associationName: 'users.roles',
117
- dataSourceKey: 'main',
118
- filterByTk: 'member',
119
- sourceId: '1',
120
- triggerByRouter: true,
726
+ coordinator.syncRoute({
727
+ pageUid: 'test-route',
728
+ pathname: '/admin/test-route',
121
729
  });
730
+ await nextTick();
731
+
732
+ expect(popupDispatchEvent.mock.calls.length).toBe(0);
122
733
  });
123
734
 
124
- it('passes RunJS openView route state as runtime mode and size during route replay', () => {
125
- const { dispatchEvent } = setupRouteReplay({
126
- openViewRouteState: { mode: 'dialog', size: 'large' },
735
+ it('ignores stale async view opens after the route is cleared', async () => {
736
+ const engine = new FlowEngine();
737
+ engine.registerModels({ RouteModel });
738
+ engine.context.defineProperty('route', {
739
+ value: {},
740
+ });
741
+ engine.context.defineProperty('routeRepository', {
742
+ value: {
743
+ getRouteBySchemaUid: vi.fn(() => ({})),
744
+ },
127
745
  });
128
746
 
129
- expect(dispatchEvent.mock.calls[0][1]).toMatchObject({
130
- mode: 'dialog',
131
- size: 'large',
132
- openViewRouteState: { mode: 'dialog', size: 'large' },
133
- triggerByRouter: true,
747
+ const rootDispatchEvent = createDispatchEventMock((_eventName, payload) => {
748
+ payload.destroyRef.current = vi.fn();
749
+ return Promise.resolve();
750
+ });
751
+ const popupDispatchEvent = createDispatchEventMock((_eventName, payload) => {
752
+ payload.destroyRef.current = vi.fn();
753
+ return Promise.resolve();
754
+ });
755
+ const rootViewItem = createViewItem('test-route', rootDispatchEvent);
756
+ const popupViewItem: ViewItem = {
757
+ params: { viewUid: 'popup' },
758
+ modelUid: 'popup',
759
+ model: undefined,
760
+ hidden: { value: false },
761
+ index: 1,
762
+ };
763
+ let resolvePopupModel: (model: FlowModel) => void = () => {};
764
+ vi.spyOn(engine, 'loadModel').mockImplementation(
765
+ () =>
766
+ new Promise((resolve) => {
767
+ resolvePopupModel = resolve;
768
+ }),
769
+ );
770
+ let resolvedViewList: ViewItem[] = [rootViewItem];
771
+ mockResolveViewParamsToViewList.mockImplementation(() => resolvedViewList);
772
+ mockGetViewDiffAndUpdateHidden.mockImplementation((prevViewList, currentViewList) => ({
773
+ viewsToClose: prevViewList.filter((prevViewItem) => !currentViewList.includes(prevViewItem)),
774
+ viewsToOpen: currentViewList.filter((currentViewItem) => !prevViewList.includes(currentViewItem)),
775
+ }));
776
+ mockGetOpenViewStepParams.mockReturnValue({
777
+ collectionName: '',
778
+ dataSourceKey: '',
779
+ });
780
+
781
+ const coordinator = new BaseLayoutRouteCoordinator(engine, { basePathname: '/admin' });
782
+ coordinator.setLayoutContentElement(document.createElement('div'));
783
+ coordinator.registerPage('test-route', {
784
+ active: true,
785
+ layoutContentElement: document.createElement('div'),
786
+ });
787
+ coordinator.syncRoute({
788
+ pageUid: 'test-route',
789
+ pathname: '/admin/test-route',
790
+ });
791
+
792
+ resolvedViewList = [rootViewItem, popupViewItem];
793
+ coordinator.syncRoute({
794
+ pageUid: 'test-route',
795
+ pathname: '/admin/test-route/view/popup',
134
796
  });
797
+
798
+ coordinator.syncRoute({});
799
+ resolvePopupModel(createViewModel('popup', popupDispatchEvent));
800
+ await nextTick();
801
+
802
+ expect(popupDispatchEvent).not.toHaveBeenCalled();
135
803
  });
136
804
 
137
- it('uses layout content element before route page placeholder as view target', () => {
805
+ it('ignores stale async view opens after switching away and back to the same pathname', async () => {
138
806
  const engine = new FlowEngine();
139
807
  engine.registerModels({ RouteModel });
140
808
  engine.context.defineProperty('route', {
@@ -146,35 +814,161 @@ describe('AdminLayoutRouteCoordinator', () => {
146
814
  },
147
815
  });
148
816
 
149
- const layoutContentElement = document.createElement('div');
150
- const routePageElement = document.createElement('div');
151
- const dispatchEvent = vi.fn((_eventName: string, _payload: any) => Promise.resolve());
152
- const viewItem = {
153
- params: { viewUid: 'test-route' },
154
- modelUid: 'test-route',
155
- model: { uid: 'test-route', dispatchEvent } as any,
817
+ const pageRootDispatchEvent = createDispatchEventMock((_eventName, payload) => {
818
+ payload.destroyRef.current = vi.fn();
819
+ return Promise.resolve();
820
+ });
821
+ const otherRootDispatchEvent = createDispatchEventMock((_eventName, payload) => {
822
+ payload.destroyRef.current = vi.fn();
823
+ return Promise.resolve();
824
+ });
825
+ const stalePopupDispatchEvent = createDispatchEventMock((_eventName, payload) => {
826
+ payload.destroyRef.current = vi.fn();
827
+ return Promise.resolve();
828
+ });
829
+ const currentPopupDispatchEvent = createDispatchEventMock((_eventName, payload) => {
830
+ payload.destroyRef.current = vi.fn();
831
+ return Promise.resolve();
832
+ });
833
+ const pageRootViewItem = createViewItem('test-route', pageRootDispatchEvent);
834
+ const otherRootViewItem = createViewItem('other-route', otherRootDispatchEvent);
835
+ const firstPopupViewItem: ViewItem = {
836
+ params: { viewUid: 'popup' },
837
+ modelUid: 'popup',
838
+ model: undefined,
156
839
  hidden: { value: false },
157
- index: 0,
840
+ index: 1,
158
841
  };
159
- mockResolveViewParamsToViewList.mockReturnValue([viewItem]);
160
- mockGetViewDiffAndUpdateHidden.mockReturnValue({
161
- viewsToClose: [],
162
- viewsToOpen: [viewItem],
842
+ const secondPopupViewItem: ViewItem = {
843
+ params: { viewUid: 'popup' },
844
+ modelUid: 'popup',
845
+ model: undefined,
846
+ hidden: { value: false },
847
+ index: 1,
848
+ };
849
+ const resolvePopupModels: Array<(model: FlowModel) => void> = [];
850
+ vi.spyOn(engine, 'loadModel').mockImplementation(
851
+ () =>
852
+ new Promise((resolve) => {
853
+ resolvePopupModels.push(resolve);
854
+ }),
855
+ );
856
+ let resolvedViewList: ViewItem[] = [pageRootViewItem];
857
+ mockResolveViewParamsToViewList.mockImplementation(() => resolvedViewList);
858
+ mockGetViewDiffAndUpdateHidden.mockImplementation((prevViewList, currentViewList) => ({
859
+ viewsToClose: prevViewList.filter((prevViewItem) => !currentViewList.includes(prevViewItem)),
860
+ viewsToOpen: currentViewList.filter((currentViewItem) => !prevViewList.includes(currentViewItem)),
861
+ }));
862
+ mockGetOpenViewStepParams.mockReturnValue({
863
+ collectionName: '',
864
+ dataSourceKey: '',
163
865
  });
164
- mockGetOpenViewStepParams.mockReturnValue({} as any);
165
866
 
166
867
  const coordinator = new BaseLayoutRouteCoordinator(engine, { basePathname: '/admin' });
167
- coordinator.setLayoutContentElement(layoutContentElement);
868
+ coordinator.setLayoutContentElement(document.createElement('div'));
168
869
  coordinator.registerPage('test-route', {
169
870
  active: true,
170
- layoutContentElement: routePageElement,
871
+ layoutContentElement: document.createElement('div'),
872
+ });
873
+ coordinator.registerPage('other-route', {
874
+ active: false,
875
+ layoutContentElement: document.createElement('div'),
171
876
  });
172
877
  coordinator.syncRoute({
173
878
  pageUid: 'test-route',
174
879
  pathname: '/admin/test-route',
175
880
  });
176
881
 
177
- expect(dispatchEvent.mock.calls[0][1].target).toBe(layoutContentElement);
882
+ resolvedViewList = [pageRootViewItem, firstPopupViewItem];
883
+ coordinator.syncRoute({
884
+ pageUid: 'test-route',
885
+ pathname: '/admin/test-route/view/popup',
886
+ });
887
+
888
+ resolvedViewList = [otherRootViewItem];
889
+ coordinator.syncRoute({
890
+ pageUid: 'other-route',
891
+ pathname: '/admin/other-route',
892
+ });
893
+
894
+ resolvedViewList = [pageRootViewItem, secondPopupViewItem];
895
+ coordinator.syncRoute({
896
+ pageUid: 'test-route',
897
+ pathname: '/admin/test-route/view/popup',
898
+ });
899
+
900
+ resolvePopupModels[0]?.(createViewModel('popup', stalePopupDispatchEvent));
901
+ resolvePopupModels[1]?.(createViewModel('popup', currentPopupDispatchEvent));
902
+ await nextTick();
903
+
904
+ expect(stalePopupDispatchEvent).not.toHaveBeenCalled();
905
+ expect(currentPopupDispatchEvent).toHaveBeenCalledTimes(1);
906
+ });
907
+
908
+ it('does not run initial deep-link step navigation while replaying after layout content element changes', () => {
909
+ const engine = new FlowEngine();
910
+ engine.registerModels({ RouteModel });
911
+ const navigate = vi.fn();
912
+ engine.context.defineProperty('router', {
913
+ value: {
914
+ navigate,
915
+ },
916
+ });
917
+ engine.context.defineProperty('route', {
918
+ value: {},
919
+ });
920
+ engine.context.defineProperty('routeRepository', {
921
+ value: {
922
+ getRouteBySchemaUid: vi.fn(() => ({})),
923
+ },
924
+ });
925
+
926
+ const rootDispatchEvent = createDispatchEventMock((_eventName, payload) => {
927
+ payload.destroyRef.current = vi.fn();
928
+ payload.onOpen();
929
+ return Promise.resolve();
930
+ });
931
+ const popupDispatchEvent = createDispatchEventMock((_eventName, payload) => {
932
+ payload.destroyRef.current = vi.fn();
933
+ return Promise.resolve();
934
+ });
935
+ const rootViewItem = createViewItem('test-route', rootDispatchEvent);
936
+ const popupViewItem = createViewItem('popup', popupDispatchEvent, 1);
937
+ let resolvedViewList: ViewItem[] = [rootViewItem];
938
+ mockResolveViewParamsToViewList.mockImplementation(() => resolvedViewList);
939
+ mockGetViewDiffAndUpdateHidden.mockImplementation((prevViewList, currentViewList) => ({
940
+ viewsToClose: prevViewList.filter((prevViewItem) => !currentViewList.includes(prevViewItem)),
941
+ viewsToOpen: currentViewList.filter((currentViewItem) => !prevViewList.includes(currentViewItem)),
942
+ }));
943
+ mockGetOpenViewStepParams.mockReturnValue({
944
+ collectionName: '',
945
+ dataSourceKey: '',
946
+ });
947
+
948
+ const coordinator = new BaseLayoutRouteCoordinator(engine, { basePathname: '/admin' });
949
+ coordinator.setLayoutContentElement(document.createElement('div'));
950
+ coordinator.registerPage('test-route', {
951
+ active: true,
952
+ layoutContentElement: document.createElement('div'),
953
+ });
954
+ coordinator.syncRoute({
955
+ pageUid: 'test-route',
956
+ pathname: '/admin/test-route',
957
+ });
958
+
959
+ resolvedViewList = [rootViewItem, popupViewItem];
960
+ coordinator.syncRoute({
961
+ pageUid: 'test-route',
962
+ pathname: '/admin/test-route/view/popup',
963
+ });
964
+ navigate.mockClear();
965
+
966
+ const nextLayoutContentElement = document.createElement('div');
967
+ coordinator.setLayoutContentElement(nextLayoutContentElement);
968
+
969
+ expect(navigate).not.toHaveBeenCalled();
970
+ expect(popupDispatchEvent).toHaveBeenCalledTimes(2);
971
+ expect(popupDispatchEvent.mock.calls[1][1].target).toBe(nextLayoutContentElement);
178
972
  });
179
973
 
180
974
  it('replaces stale non-route model before registering route page', () => {
@@ -281,18 +1075,12 @@ describe('AdminLayoutRouteCoordinator', () => {
281
1075
  },
282
1076
  });
283
1077
 
284
- const dispatchEvent = vi.fn((_eventName: string, payload: any) => {
1078
+ const dispatchEvent = createDispatchEventMock((_eventName, payload) => {
285
1079
  payload.activateRef.current = vi.fn();
286
1080
  payload.deactivateRef.current = vi.fn();
287
1081
  return Promise.resolve();
288
1082
  });
289
- const viewItem = {
290
- params: { viewUid: 'test-route' },
291
- modelUid: 'test-route',
292
- model: { uid: 'test-route', dispatchEvent } as any,
293
- hidden: { value: false },
294
- index: 0,
295
- };
1083
+ const viewItem = createViewItem('test-route', dispatchEvent);
296
1084
  mockResolveViewParamsToViewList.mockReturnValue([viewItem]);
297
1085
  mockGetViewDiffAndUpdateHidden.mockReturnValueOnce({
298
1086
  viewsToClose: [],
@@ -342,28 +1130,26 @@ describe('AdminLayoutRouteCoordinator', () => {
342
1130
  },
343
1131
  });
344
1132
 
345
- const viewItemsByPageUid = new Map<string, any>();
346
- const createViewItem = (pageUid: string) => {
347
- const dispatchEvent = vi.fn((_eventName: string, payload: any) => {
1133
+ const viewItemsByPageUid = new Map<string, ViewItem>();
1134
+ const createCachedViewItem = (pageUid: string) => {
1135
+ const dispatchEvent = createDispatchEventMock((_eventName, payload) => {
348
1136
  payload.activateRef.current = vi.fn();
349
1137
  payload.deactivateRef.current = vi.fn();
350
1138
  return Promise.resolve();
351
1139
  });
352
- return {
353
- params: { viewUid: pageUid },
354
- modelUid: pageUid,
355
- model: { uid: pageUid, dispatchEvent } as any,
356
- hidden: { value: false },
357
- index: 0,
358
- };
1140
+ return createViewItem(pageUid, dispatchEvent);
359
1141
  };
360
1142
 
361
1143
  mockResolveViewParamsToViewList.mockImplementation((_engine, viewParams) => {
362
1144
  const pageUid = viewParams[0].viewUid;
363
1145
  if (!viewItemsByPageUid.has(pageUid)) {
364
- viewItemsByPageUid.set(pageUid, createViewItem(pageUid));
1146
+ viewItemsByPageUid.set(pageUid, createCachedViewItem(pageUid));
1147
+ }
1148
+ const viewItem = viewItemsByPageUid.get(pageUid);
1149
+ if (!viewItem) {
1150
+ throw new Error(`Expected cached view item for ${pageUid}.`);
365
1151
  }
366
- return [viewItemsByPageUid.get(pageUid)];
1152
+ return [viewItem];
367
1153
  });
368
1154
  mockGetViewDiffAndUpdateHidden.mockImplementation((prevViewList, currentViewList) => ({
369
1155
  viewsToClose: prevViewList.filter((prevViewItem) => !currentViewList.includes(prevViewItem)),
@@ -420,18 +1206,12 @@ describe('AdminLayoutRouteCoordinator', () => {
420
1206
  },
421
1207
  });
422
1208
 
423
- const dispatchEvent = vi.fn((_eventName: string, payload: any) => {
1209
+ const dispatchEvent = createDispatchEventMock((_eventName, payload) => {
424
1210
  payload.activateRef.current = vi.fn();
425
1211
  payload.deactivateRef.current = vi.fn();
426
1212
  return Promise.resolve();
427
1213
  });
428
- const viewItem = {
429
- params: { viewUid: 'test-route' },
430
- modelUid: 'test-route',
431
- model: { uid: 'test-route', dispatchEvent } as any,
432
- hidden: { value: false },
433
- index: 0,
434
- };
1214
+ const viewItem = createViewItem('test-route', dispatchEvent);
435
1215
  mockResolveViewParamsToViewList.mockReturnValue([viewItem]);
436
1216
  mockGetViewDiffAndUpdateHidden.mockReturnValueOnce({
437
1217
  viewsToClose: [],
@@ -479,13 +1259,10 @@ describe('AdminLayoutRouteCoordinator', () => {
479
1259
  },
480
1260
  });
481
1261
 
482
- const popupViewItem = {
483
- params: { viewUid: 'popup', filterByTk: '1' },
484
- modelUid: 'popup',
485
- model: { uid: 'popup', dispatchEvent: vi.fn() } as any,
486
- hidden: { value: false },
487
- index: 1,
488
- };
1262
+ const popupViewItem = createViewItem('popup', createDispatchEventMock(), 1, {
1263
+ viewUid: 'popup',
1264
+ filterByTk: '1',
1265
+ });
489
1266
  mockResolveViewParamsToViewList.mockImplementation((_engine, viewParams, routeModel) => [
490
1267
  {
491
1268
  params: viewParams[0],