@nocobase/client-v2 2.2.0-beta.9 → 3.0.0-alpha.1
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/es/APIClient.d.ts +1 -3
- package/es/BaseApplication.d.ts +1 -0
- package/es/acl/ACLProvider.d.ts +10 -0
- package/es/authRedirect.d.ts +5 -0
- package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
- package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
- package/es/components/AttachmentUpload.d.ts +51 -0
- package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
- package/es/components/category-tabs/index.d.ts +9 -0
- package/es/components/form/ScanInput/useCodeScanner.d.ts +3 -2
- package/es/components/form/TypedVariableInput.d.ts +31 -5
- package/es/components/form/filter/CollectionFilter.d.ts +2 -0
- package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
- package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
- package/es/components/form/filter/index.d.ts +2 -0
- package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
- package/es/components/index.d.ts +2 -0
- package/es/flow/actions/index.d.ts +1 -1
- package/es/flow/actions/linkageRules.d.ts +3 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/NocoBaseLogo.d.ts +10 -0
- package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
- package/es/flow/components/FieldAssignValueInput.d.ts +9 -0
- package/es/flow/components/FlowRoute.d.ts +2 -2
- package/es/flow/components/RunJSValueEditor.d.ts +1 -0
- package/es/flow/components/filter/FilterGroup.d.ts +1 -0
- package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
- package/es/flow/index.d.ts +1 -0
- package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
- package/es/flow/models/base/ActionModelCore.d.ts +4 -2
- package/es/flow/models/base/BlockGridModel.d.ts +3 -0
- package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
- package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
- package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
- package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
- package/es/flow/models/blocks/table/TableColumnModel.d.ts +1 -0
- package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
- package/es/flow-compat/fieldValidationConstants.d.ts +1 -1
- package/es/flow-compat/routeTypes.d.ts +1 -0
- package/es/index.d.ts +8 -1
- package/es/index.mjs +190 -141
- package/es/nocobase-buildin-plugin/currentUserAuthStatus.d.ts +11 -0
- package/es/nocobase-buildin-plugin/index.d.ts +9 -0
- package/es/settings-app/SettingsApplication.d.ts +18 -0
- package/es/settings-app/SettingsBrand.d.ts +16 -0
- package/es/settings-app/SettingsBuildInPlugin.d.ts +9 -0
- package/es/settings-app/SettingsDocumentRedirect.d.ts +10 -0
- package/es/settings-app/SettingsGroupNav.d.ts +17 -0
- package/es/settings-app/SettingsPluginSettingsManager.d.ts +14 -0
- package/es/settings-app/SettingsRouterManager.d.ts +15 -0
- package/es/settings-app/SettingsSearch.d.ts +17 -0
- package/es/settings-app/SettingsShell.d.ts +10 -0
- package/es/settings-app/isSettingsApp.d.ts +18 -0
- package/es/settings-app/runtimePaths.d.ts +18 -0
- package/es/settings-app/settingsDocumentPath.d.ts +14 -0
- package/es/settings-app/settingsTheme.d.ts +87 -0
- package/es/settings-app/useSettingsThemeConfig.d.ts +22 -0
- package/es/settings-center/AdminSettingsLayout.d.ts +2 -2
- package/es/settings-center/groups.d.ts +66 -0
- package/es/settings-center/useSettingsGroups.d.ts +36 -0
- package/es/settings-center/useSettingsSearch.d.ts +35 -0
- package/es/settings-center/utils.d.ts +24 -0
- package/es/theme/index.d.ts +1 -0
- package/es/theme/itemActive.d.ts +24 -0
- package/es/theme/type.d.ts +9 -0
- package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
- package/es/utils/index.d.ts +1 -0
- package/lib/index.js +195 -146
- package/lib/locale/languageCodes.js +3 -1
- package/package.json +9 -7
- package/src/APIClient.ts +1 -10
- package/src/Application.tsx +4 -0
- package/src/BaseApplication.tsx +11 -6
- package/src/__tests__/app.test.tsx +73 -0
- package/src/__tests__/authRedirect.test.ts +137 -0
- package/src/__tests__/browserChecker.test.ts +185 -6
- package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
- package/src/__tests__/mockSettingsApplication.ts +29 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +79 -10
- package/src/__tests__/plugin-manager.test.tsx +7 -6
- package/src/__tests__/settings-application.test.ts +164 -0
- package/src/__tests__/settings-auth-routes.test.ts +71 -0
- package/src/__tests__/settings-center.test.tsx +38 -37
- package/src/__tests__/settings-layout-root.test.tsx +260 -0
- package/src/__tests__/settings-runtime-paths.test.ts +99 -0
- package/src/__tests__/settings-shell.test.tsx +200 -0
- package/src/acl/ACLProvider.tsx +21 -0
- package/src/authRedirect.ts +72 -3
- package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
- package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
- package/src/collection-manager/field-validation.ts +1 -1
- package/src/components/AttachmentUpload.tsx +275 -0
- package/src/components/README.md +7 -1
- package/src/components/README.zh-CN.md +6 -1
- package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
- package/src/components/category-tabs/index.ts +10 -0
- package/src/components/form/JsonTextArea.tsx +4 -0
- package/src/components/form/ScanInput/CodeScanner.tsx +79 -35
- package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
- package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +284 -11
- package/src/components/form/ScanInput/useCodeScanner.ts +332 -24
- package/src/components/form/TypedVariableInput.tsx +452 -101
- package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
- package/src/components/form/filter/CollectionFilter.tsx +4 -0
- package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
- package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
- package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
- package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
- package/src/components/form/filter/index.ts +2 -0
- package/src/components/form/filter/useFilterActionProps.ts +37 -6
- package/src/components/index.ts +2 -0
- package/src/flow/FlowPage.tsx +9 -1
- package/src/flow/__tests__/FlowPage.test.tsx +50 -3
- package/src/flow/__tests__/FlowRoute.test.tsx +128 -10
- package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
- package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
- package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
- package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
- package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
- package/src/flow/actions/index.ts +2 -0
- package/src/flow/actions/linkageRules.tsx +111 -16
- package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +1 -57
- package/src/flow/admin-shell/admin-layout/NocoBaseLogo.tsx +77 -0
- package/src/flow/admin-shell/admin-layout/TopbarActionsBar.tsx +37 -14
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +253 -6
- package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +77 -13
- package/src/flow/common/Markdown/style.ts +4 -0
- package/src/flow/components/BlockItemCard.tsx +2 -2
- package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
- package/src/flow/components/FieldAssignValueInput.tsx +91 -20
- package/src/flow/components/FlowRoute.tsx +78 -32
- package/src/flow/components/RunJSValueEditor.tsx +9 -1
- package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +216 -0
- package/src/flow/components/filter/FilterGroup.tsx +33 -5
- package/src/flow/components/filter/VariableFilterItem.tsx +158 -10
- package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +70 -1
- package/src/flow/index.ts +3 -0
- package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
- package/src/flow/models/base/ActionModelCore.tsx +11 -2
- package/src/flow/models/base/BlockGridModel.tsx +26 -0
- package/src/flow/models/base/CollectionBlockModel.tsx +6 -2
- package/src/flow/models/base/PageModel/PageModel.tsx +387 -53
- package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
- package/src/flow/models/base/PageModel/PageTabModel.tsx +249 -18
- package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +790 -10
- package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
- package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
- package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +1075 -7
- package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
- package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
- package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +28 -3
- package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +27 -12
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +36 -0
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +66 -4
- package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
- package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
- package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
- package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +2 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +4 -2
- package/src/flow/models/blocks/table/TableColumnModel.tsx +30 -1
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +2 -1
- package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
- package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
- package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
- package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
- package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
- package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
- package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +20 -10
- package/src/flow/models/fields/mobile-components/MobileSelect.tsx +24 -12
- package/src/flow/models/topbar/TopbarActionModel.tsx +52 -180
- package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
- package/src/flow-compat/fieldValidationConstants.ts +1 -1
- package/src/flow-compat/routeTypes.ts +1 -0
- package/src/index.ts +14 -1
- package/src/locale/languageCodes.ts +3 -1
- package/src/nocobase-buildin-plugin/currentUserAuthStatus.ts +58 -0
- package/src/nocobase-buildin-plugin/index.tsx +110 -70
- package/src/settings-app/SettingsApplication.ts +31 -0
- package/src/settings-app/SettingsBrand.tsx +108 -0
- package/src/settings-app/SettingsBuildInPlugin.ts +10 -0
- package/src/settings-app/SettingsDocumentRedirect.tsx +26 -0
- package/src/settings-app/SettingsGroupNav.tsx +124 -0
- package/src/settings-app/SettingsPluginSettingsManager.ts +38 -0
- package/src/settings-app/SettingsRouterManager.ts +92 -0
- package/src/settings-app/SettingsSearch.tsx +238 -0
- package/src/settings-app/SettingsShell.tsx +167 -0
- package/src/settings-app/isSettingsApp.ts +21 -0
- package/src/settings-app/runtimePaths.ts +104 -0
- package/src/settings-app/settingsDocumentPath.ts +66 -0
- package/src/settings-app/settingsTheme.ts +301 -0
- package/src/settings-app/useSettingsThemeConfig.ts +100 -0
- package/src/settings-center/AdminSettingsLayout.tsx +122 -141
- package/src/settings-center/SystemSettingsPage.tsx +4 -170
- package/src/settings-center/groups.ts +108 -0
- package/src/settings-center/useSettingsGroups.ts +105 -0
- package/src/settings-center/useSettingsSearch.ts +173 -0
- package/src/settings-center/utils.tsx +50 -1
- package/src/theme/index.tsx +1 -0
- package/src/theme/itemActive.ts +31 -0
- package/src/theme/type.ts +10 -0
- package/src/utils/getRouteRuntimeVersion.ts +185 -0
- package/src/utils/index.tsx +1 -0
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
11
11
|
import type { DragEndEvent } from '@dnd-kit/core';
|
|
12
|
+
import { render, waitFor } from '@testing-library/react';
|
|
12
13
|
|
|
13
14
|
// Mock FlowModel and other dependencies
|
|
14
15
|
vi.mock('@nocobase/flow-engine', () => {
|
|
@@ -221,6 +222,17 @@ describe('PageModel', () => {
|
|
|
221
222
|
let pageModel: InstanceType<typeof PageModelClass>;
|
|
222
223
|
let mockDragEndEvent: DragEndEvent;
|
|
223
224
|
|
|
225
|
+
const getDndChildren = (result: { props: { children?: unknown } }) => {
|
|
226
|
+
const children = result.props.children;
|
|
227
|
+
return Array.isArray(children) ? children : [children].filter(Boolean);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const getTabsElement = (result: { props: { children?: unknown } }) => getDndChildren(result)[1] as any;
|
|
231
|
+
|
|
232
|
+
const getTabSyncElement = (result: { props: { children?: unknown } }) => getDndChildren(result)[0] as any;
|
|
233
|
+
|
|
234
|
+
const getHiddenActiveTabContent = (result: { props: { children?: unknown } }) => getDndChildren(result)[2];
|
|
235
|
+
|
|
224
236
|
beforeEach(async () => {
|
|
225
237
|
vi.resetModules();
|
|
226
238
|
vi.clearAllMocks();
|
|
@@ -299,9 +311,82 @@ describe('PageModel', () => {
|
|
|
299
311
|
expect(blockOnActive).toHaveBeenCalledWith(true);
|
|
300
312
|
expect(blockOnInactive).toHaveBeenCalledWith(false);
|
|
301
313
|
});
|
|
314
|
+
|
|
315
|
+
it('should use the first actual tab for lifecycle when tabs are disabled even if it is hidden', () => {
|
|
316
|
+
const hiddenFirstTab = {
|
|
317
|
+
uid: 'tab-hidden-first',
|
|
318
|
+
hidden: true,
|
|
319
|
+
context: {},
|
|
320
|
+
subModels: { grid: { mapSubModels: vi.fn() } },
|
|
321
|
+
};
|
|
322
|
+
const visibleSecondTab = {
|
|
323
|
+
uid: 'tab-visible-second',
|
|
324
|
+
context: {},
|
|
325
|
+
subModels: { grid: { mapSubModels: vi.fn() } },
|
|
326
|
+
};
|
|
327
|
+
(pageModel as any).subModels = { tabs: [hiddenFirstTab, visibleSecondTab] };
|
|
328
|
+
pageModel.props = { enableTabs: false, tabActiveKey: 'tab-visible-second' } as any;
|
|
329
|
+
(pageModel as any).context = { view: { navigation: null } };
|
|
330
|
+
const invokeSpy = vi.spyOn(pageModel as any, 'invokeTabModelLifecycleMethod').mockImplementation(() => undefined);
|
|
331
|
+
|
|
332
|
+
pageModel.activateCurrentTab();
|
|
333
|
+
pageModel.deactivateCurrentTab();
|
|
334
|
+
|
|
335
|
+
expect(invokeSpy).toHaveBeenNthCalledWith(1, 'tab-hidden-first', 'onActive', false);
|
|
336
|
+
expect(invokeSpy).toHaveBeenNthCalledWith(2, 'tab-hidden-first', 'onInactive');
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('should keep a hidden active tab responsible for lifecycle when tabs are enabled', () => {
|
|
340
|
+
const hiddenActiveTab = {
|
|
341
|
+
uid: 'tab-hidden-active',
|
|
342
|
+
hidden: true,
|
|
343
|
+
context: {},
|
|
344
|
+
subModels: { grid: { mapSubModels: vi.fn() } },
|
|
345
|
+
};
|
|
346
|
+
const visibleTab = {
|
|
347
|
+
uid: 'tab-visible',
|
|
348
|
+
context: {},
|
|
349
|
+
subModels: { grid: { mapSubModels: vi.fn() } },
|
|
350
|
+
};
|
|
351
|
+
(pageModel as any).subModels = { tabs: [hiddenActiveTab, visibleTab] };
|
|
352
|
+
pageModel.props = { enableTabs: true, tabActiveKey: 'tab-hidden-active' } as any;
|
|
353
|
+
(pageModel as any).context = {
|
|
354
|
+
view: { navigation: { viewParams: { tabUid: 'tab-hidden-active' } } },
|
|
355
|
+
};
|
|
356
|
+
const invokeSpy = vi.spyOn(pageModel as any, 'invokeTabModelLifecycleMethod').mockImplementation(() => undefined);
|
|
357
|
+
|
|
358
|
+
pageModel.activateCurrentTab();
|
|
359
|
+
pageModel.deactivateCurrentTab();
|
|
360
|
+
|
|
361
|
+
expect(invokeSpy).toHaveBeenNthCalledWith(1, 'tab-hidden-active', 'onActive', false);
|
|
362
|
+
expect(invokeSpy).toHaveBeenNthCalledWith(2, 'tab-hidden-active', 'onInactive');
|
|
363
|
+
});
|
|
302
364
|
});
|
|
303
365
|
|
|
304
366
|
describe('renderTabs activeKey logic', () => {
|
|
367
|
+
const createTab = (uid: string, hidden = false) => ({
|
|
368
|
+
uid,
|
|
369
|
+
hidden,
|
|
370
|
+
context: {},
|
|
371
|
+
subModels: { grid: { mapSubModels: vi.fn() } },
|
|
372
|
+
renderChildren: vi.fn(() => uid),
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
const initialSyncState = {
|
|
376
|
+
previousEffectiveActiveKey: undefined,
|
|
377
|
+
previousAllHidden: false,
|
|
378
|
+
lastCorrectionSignature: undefined,
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const runTabSync = (
|
|
382
|
+
result: { props: { children?: unknown } },
|
|
383
|
+
state: typeof initialSyncState | Record<string, unknown> = initialSyncState,
|
|
384
|
+
) => {
|
|
385
|
+
const syncElement = getTabSyncElement(result);
|
|
386
|
+
expect(typeof syncElement?.props?.onSync).toBe('function');
|
|
387
|
+
return syncElement.props.onSync(state);
|
|
388
|
+
};
|
|
389
|
+
|
|
305
390
|
beforeEach(() => {
|
|
306
391
|
// Mock mapTabs to avoid complex rendering logic inside it
|
|
307
392
|
pageModel.mapTabs = vi.fn().mockReturnValue([]);
|
|
@@ -325,7 +410,7 @@ describe('PageModel', () => {
|
|
|
325
410
|
|
|
326
411
|
const result = pageModel.renderTabs() as any;
|
|
327
412
|
// result is <DndProvider><Tabs ... /></DndProvider>
|
|
328
|
-
const tabsElement = result
|
|
413
|
+
const tabsElement = getTabsElement(result);
|
|
329
414
|
|
|
330
415
|
expect(tabsElement.props.activeKey).toBe('tab-from-params');
|
|
331
416
|
});
|
|
@@ -341,10 +426,589 @@ describe('PageModel', () => {
|
|
|
341
426
|
};
|
|
342
427
|
|
|
343
428
|
const result = pageModel.renderTabs() as any;
|
|
344
|
-
const tabsElement = result
|
|
429
|
+
const tabsElement = getTabsElement(result);
|
|
345
430
|
expect(tabsElement.props.activeKey).toBe('first-tab-uid');
|
|
346
431
|
});
|
|
347
432
|
|
|
433
|
+
it('should use the first visible tab when activeKey is unspecified', () => {
|
|
434
|
+
const hiddenTab = createTab('tab-hidden', true);
|
|
435
|
+
const visibleTab = createTab('tab-visible');
|
|
436
|
+
(pageModel as any).subModels = { tabs: [hiddenTab, visibleTab] };
|
|
437
|
+
pageModel.context.view = {
|
|
438
|
+
navigation: {
|
|
439
|
+
viewParams: {},
|
|
440
|
+
changeTo: vi.fn(),
|
|
441
|
+
},
|
|
442
|
+
} as any;
|
|
443
|
+
|
|
444
|
+
const tabsElement = getTabsElement(pageModel.renderTabs() as any);
|
|
445
|
+
|
|
446
|
+
expect(tabsElement.props.activeKey).toBe('tab-visible');
|
|
447
|
+
expect(pageModel.context.view.navigation.changeTo).not.toHaveBeenCalled();
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it('should keep a visible URL activeKey unchanged', () => {
|
|
451
|
+
(pageModel as any).subModels = { tabs: [createTab('tab-1'), createTab('tab-2')] };
|
|
452
|
+
const changeTo = vi.fn();
|
|
453
|
+
pageModel.context.view = {
|
|
454
|
+
navigation: {
|
|
455
|
+
viewParams: { tabUid: 'tab-2' },
|
|
456
|
+
changeTo,
|
|
457
|
+
},
|
|
458
|
+
} as any;
|
|
459
|
+
|
|
460
|
+
const result = pageModel.renderTabs() as any;
|
|
461
|
+
const tabsElement = getTabsElement(result);
|
|
462
|
+
const nextState = runTabSync(result);
|
|
463
|
+
|
|
464
|
+
expect(tabsElement.props.activeKey).toBe('tab-2');
|
|
465
|
+
expect(nextState.effectiveActiveKey).toBe('tab-2');
|
|
466
|
+
expect(changeTo).not.toHaveBeenCalled();
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it('should keep an unknown URL activeKey for the existing deep-link behavior', () => {
|
|
470
|
+
(pageModel as any).subModels = { tabs: [createTab('tab-1'), createTab('tab-2')] };
|
|
471
|
+
const changeTo = vi.fn();
|
|
472
|
+
const invokeSpy = vi.spyOn(pageModel as any, 'invokeTabModelLifecycleMethod');
|
|
473
|
+
pageModel.context.view = {
|
|
474
|
+
navigation: {
|
|
475
|
+
viewParams: { tabUid: 'unknown-tab' },
|
|
476
|
+
changeTo,
|
|
477
|
+
},
|
|
478
|
+
} as any;
|
|
479
|
+
|
|
480
|
+
const result = pageModel.renderTabs() as any;
|
|
481
|
+
const tabsElement = getTabsElement(result);
|
|
482
|
+
const nextState = runTabSync(result);
|
|
483
|
+
|
|
484
|
+
expect(tabsElement.props.activeKey).toBe('unknown-tab');
|
|
485
|
+
expect(nextState.effectiveActiveKey).toBe('unknown-tab');
|
|
486
|
+
expect(changeTo).not.toHaveBeenCalled();
|
|
487
|
+
expect(invokeSpy).not.toHaveBeenCalled();
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('should keep a hidden URL activeKey and render its content without exposing a tab item', () => {
|
|
491
|
+
(pageModel as any).subModels = { tabs: [createTab('tab-hidden', true), createTab('tab-visible')] };
|
|
492
|
+
pageModel.mapTabs = PageModelClass.prototype.mapTabs.bind(pageModel);
|
|
493
|
+
pageModel.props = { tabActiveKey: 'tab-hidden' } as any;
|
|
494
|
+
const changeTo = vi.fn();
|
|
495
|
+
const invokeSpy = vi.spyOn(pageModel as any, 'invokeTabModelLifecycleMethod').mockImplementation(() => undefined);
|
|
496
|
+
pageModel.context.view = {
|
|
497
|
+
navigation: {
|
|
498
|
+
viewParams: { tabUid: 'tab-hidden' },
|
|
499
|
+
changeTo,
|
|
500
|
+
},
|
|
501
|
+
} as any;
|
|
502
|
+
|
|
503
|
+
const result = pageModel.renderTabs() as any;
|
|
504
|
+
const tabsElement = getTabsElement(result);
|
|
505
|
+
const nextState = runTabSync(result, {
|
|
506
|
+
...initialSyncState,
|
|
507
|
+
previousEffectiveActiveKey: 'tab-hidden',
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
expect(tabsElement.props.activeKey).toBe('tab-hidden');
|
|
511
|
+
expect(tabsElement.props.items.map((item: { key: string }) => item.key)).toEqual(['tab-hidden', 'tab-visible']);
|
|
512
|
+
expect(tabsElement.props.items[0].children).toBe('tab-hidden');
|
|
513
|
+
expect(getHiddenActiveTabContent(result)).toBeUndefined();
|
|
514
|
+
expect(typeof tabsElement.props.renderTabBar).toBe('function');
|
|
515
|
+
expect(changeTo).not.toHaveBeenCalled();
|
|
516
|
+
expect(pageModel.props.tabActiveKey).toBe('tab-hidden');
|
|
517
|
+
expect(invokeSpy).not.toHaveBeenCalled();
|
|
518
|
+
expect(nextState.effectiveActiveKey).toBe('tab-hidden');
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it('should keep a hidden explicit props activeKey and render its content without lifecycle switching', () => {
|
|
522
|
+
(pageModel as any).subModels = { tabs: [createTab('tab-hidden', true), createTab('tab-visible')] };
|
|
523
|
+
pageModel.props = { tabActiveKey: 'tab-hidden' } as any;
|
|
524
|
+
const invokeSpy = vi.spyOn(pageModel as any, 'invokeTabModelLifecycleMethod').mockImplementation(() => undefined);
|
|
525
|
+
pageModel.context.view = { navigation: null } as any;
|
|
526
|
+
|
|
527
|
+
const result = pageModel.renderTabs() as any;
|
|
528
|
+
const nextState = runTabSync(result, {
|
|
529
|
+
...initialSyncState,
|
|
530
|
+
previousEffectiveActiveKey: 'tab-hidden',
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
expect(getTabsElement(result).props.activeKey).toBe('tab-hidden');
|
|
534
|
+
expect(pageModel.props.tabActiveKey).toBe('tab-hidden');
|
|
535
|
+
expect(invokeSpy).not.toHaveBeenCalled();
|
|
536
|
+
expect(nextState.effectiveActiveKey).toBe('tab-hidden');
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
it('should not switch when a non-active tab becomes hidden', () => {
|
|
540
|
+
(pageModel as any).subModels = { tabs: [createTab('tab-active'), createTab('tab-hidden', true)] };
|
|
541
|
+
pageModel.props = { tabActiveKey: 'tab-active' } as any;
|
|
542
|
+
const changeTo = vi.fn();
|
|
543
|
+
const invokeSpy = vi.spyOn(pageModel as any, 'invokeTabModelLifecycleMethod');
|
|
544
|
+
pageModel.context.view = {
|
|
545
|
+
navigation: {
|
|
546
|
+
viewParams: { tabUid: 'tab-active' },
|
|
547
|
+
changeTo,
|
|
548
|
+
},
|
|
549
|
+
} as any;
|
|
550
|
+
|
|
551
|
+
const nextState = runTabSync(pageModel.renderTabs() as any, {
|
|
552
|
+
...initialSyncState,
|
|
553
|
+
previousEffectiveActiveKey: 'tab-active',
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
expect(nextState.effectiveActiveKey).toBe('tab-active');
|
|
557
|
+
expect(changeTo).not.toHaveBeenCalled();
|
|
558
|
+
expect(invokeSpy).not.toHaveBeenCalled();
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it('should switch an implicitly active tab when it becomes hidden without adding tabUid to URL', () => {
|
|
562
|
+
const oldTab = createTab('tab-old');
|
|
563
|
+
(pageModel as any).subModels = { tabs: [oldTab, createTab('tab-new')] };
|
|
564
|
+
const changeTo = vi.fn();
|
|
565
|
+
const invokeSpy = vi.spyOn(pageModel as any, 'invokeTabModelLifecycleMethod');
|
|
566
|
+
const updateTitleSpy = vi.spyOn(pageModel as any, 'updateDocumentTitle').mockResolvedValue(undefined);
|
|
567
|
+
pageModel.context.view = {
|
|
568
|
+
navigation: {
|
|
569
|
+
viewParams: {},
|
|
570
|
+
changeTo,
|
|
571
|
+
},
|
|
572
|
+
} as any;
|
|
573
|
+
|
|
574
|
+
let syncState = runTabSync(pageModel.renderTabs() as any);
|
|
575
|
+
expect(pageModel.props.tabActiveKey).toBe('tab-old');
|
|
576
|
+
|
|
577
|
+
invokeSpy.mockClear();
|
|
578
|
+
updateTitleSpy.mockClear();
|
|
579
|
+
oldTab.hidden = true;
|
|
580
|
+
const result = pageModel.renderTabs() as any;
|
|
581
|
+
syncState = runTabSync(result, syncState);
|
|
582
|
+
|
|
583
|
+
expect(changeTo).not.toHaveBeenCalled();
|
|
584
|
+
expect(getTabsElement(result).props.activeKey).toBe('tab-new');
|
|
585
|
+
expect(pageModel.props.tabActiveKey).toBe('tab-new');
|
|
586
|
+
expect(invokeSpy).toHaveBeenNthCalledWith(1, 'tab-new', 'onActive');
|
|
587
|
+
expect(invokeSpy).toHaveBeenNthCalledWith(2, 'tab-old', 'onInactive');
|
|
588
|
+
expect(updateTitleSpy).toHaveBeenCalledWith('tab-new', 0);
|
|
589
|
+
expect(syncState.effectiveActiveKey).toBe('tab-new');
|
|
590
|
+
|
|
591
|
+
invokeSpy.mockClear();
|
|
592
|
+
updateTitleSpy.mockClear();
|
|
593
|
+
syncState = runTabSync(pageModel.renderTabs() as any, syncState);
|
|
594
|
+
|
|
595
|
+
expect(invokeSpy).not.toHaveBeenCalled();
|
|
596
|
+
expect(updateTitleSpy).not.toHaveBeenCalled();
|
|
597
|
+
expect(syncState.effectiveActiveKey).toBe('tab-new');
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
it('should switch the implicit default in UI Editor while keeping hidden tab items', () => {
|
|
601
|
+
const oldTab = createTab('tab-old');
|
|
602
|
+
(pageModel as any).subModels = { tabs: [oldTab, createTab('tab-new')] };
|
|
603
|
+
pageModel.mapTabs = PageModelClass.prototype.mapTabs.bind(pageModel);
|
|
604
|
+
const changeTo = vi.fn();
|
|
605
|
+
const invokeSpy = vi.spyOn(pageModel as any, 'invokeTabModelLifecycleMethod');
|
|
606
|
+
const updateTitleSpy = vi.spyOn(pageModel as any, 'updateDocumentTitle').mockResolvedValue(undefined);
|
|
607
|
+
pageModel.context = {
|
|
608
|
+
t: (value: string) => value,
|
|
609
|
+
flowSettingsEnabled: true,
|
|
610
|
+
view: {
|
|
611
|
+
navigation: {
|
|
612
|
+
viewParams: {},
|
|
613
|
+
changeTo,
|
|
614
|
+
},
|
|
615
|
+
},
|
|
616
|
+
} as any;
|
|
617
|
+
|
|
618
|
+
let syncState = runTabSync(pageModel.renderTabs() as any);
|
|
619
|
+
invokeSpy.mockClear();
|
|
620
|
+
updateTitleSpy.mockClear();
|
|
621
|
+
|
|
622
|
+
oldTab.hidden = true;
|
|
623
|
+
const result = pageModel.renderTabs() as any;
|
|
624
|
+
syncState = runTabSync(result, syncState);
|
|
625
|
+
|
|
626
|
+
const tabsElement = getTabsElement(result);
|
|
627
|
+
expect(tabsElement.props.items.map((item: { key: string }) => item.key)).toEqual(['tab-old', 'tab-new']);
|
|
628
|
+
expect(tabsElement.props.activeKey).toBe('tab-new');
|
|
629
|
+
expect(changeTo).not.toHaveBeenCalled();
|
|
630
|
+
expect(pageModel.props.tabActiveKey).toBe('tab-new');
|
|
631
|
+
expect(invokeSpy).toHaveBeenNthCalledWith(1, 'tab-new', 'onActive');
|
|
632
|
+
expect(invokeSpy).toHaveBeenNthCalledWith(2, 'tab-old', 'onInactive');
|
|
633
|
+
expect(updateTitleSpy).toHaveBeenCalledWith('tab-new', 0);
|
|
634
|
+
expect(syncState.effectiveActiveKey).toBe('tab-new');
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
it('should distinguish an implicit activeKey from explicit props without route navigation', () => {
|
|
638
|
+
const oldTab = createTab('tab-old');
|
|
639
|
+
(pageModel as any).subModels = { tabs: [oldTab, createTab('tab-new')] };
|
|
640
|
+
pageModel.context.view = { navigation: null } as any;
|
|
641
|
+
|
|
642
|
+
let syncState = runTabSync(pageModel.renderTabs() as any);
|
|
643
|
+
expect(pageModel.props.tabActiveKey).toBe('tab-old');
|
|
644
|
+
|
|
645
|
+
oldTab.hidden = true;
|
|
646
|
+
const result = pageModel.renderTabs() as any;
|
|
647
|
+
syncState = runTabSync(result, syncState);
|
|
648
|
+
|
|
649
|
+
expect(getTabsElement(result).props.activeKey).toBe('tab-new');
|
|
650
|
+
expect(pageModel.props.tabActiveKey).toBe('tab-new');
|
|
651
|
+
expect(syncState.effectiveActiveKey).toBe('tab-new');
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
it('should preserve an explicit props update after establishing an implicit activeKey', () => {
|
|
655
|
+
const hiddenTab = createTab('tab-hidden', true);
|
|
656
|
+
(pageModel as any).subModels = { tabs: [createTab('tab-default'), hiddenTab] };
|
|
657
|
+
const invokeSpy = vi.spyOn(pageModel as any, 'invokeTabModelLifecycleMethod').mockImplementation(() => undefined);
|
|
658
|
+
pageModel.context.view = { navigation: null } as any;
|
|
659
|
+
|
|
660
|
+
let syncState = runTabSync(pageModel.renderTabs() as any);
|
|
661
|
+
expect(pageModel.props.tabActiveKey).toBe('tab-default');
|
|
662
|
+
|
|
663
|
+
invokeSpy.mockClear();
|
|
664
|
+
pageModel.setProps('tabActiveKey', 'tab-hidden');
|
|
665
|
+
const result = pageModel.renderTabs() as any;
|
|
666
|
+
syncState = runTabSync(result, syncState);
|
|
667
|
+
|
|
668
|
+
expect(getTabsElement(result).props.activeKey).toBe('tab-hidden');
|
|
669
|
+
expect(pageModel.props.tabActiveKey).toBe('tab-hidden');
|
|
670
|
+
expect(invokeSpy).toHaveBeenNthCalledWith(1, 'tab-hidden', 'onActive');
|
|
671
|
+
expect(invokeSpy).toHaveBeenNthCalledWith(2, 'tab-default', 'onInactive');
|
|
672
|
+
expect(syncState.effectiveActiveKey).toBe('tab-hidden');
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
it('should treat an input tabUid as explicit when a page model mounts again', () => {
|
|
676
|
+
const oldTab = createTab('tab-old');
|
|
677
|
+
(pageModel as any).subModels = { tabs: [oldTab, createTab('tab-new')] };
|
|
678
|
+
vi.spyOn(pageModel as any, 'updateDocumentTitle').mockResolvedValue(undefined);
|
|
679
|
+
pageModel.context.view = { navigation: null, inputArgs: {} } as any;
|
|
680
|
+
|
|
681
|
+
runTabSync(pageModel.renderTabs() as any);
|
|
682
|
+
expect(pageModel.props.tabActiveKey).toBe('tab-old');
|
|
683
|
+
|
|
684
|
+
oldTab.hidden = true;
|
|
685
|
+
pageModel.context.view.inputArgs.tabUid = 'tab-old';
|
|
686
|
+
pageModel.onMount();
|
|
687
|
+
|
|
688
|
+
expect(getTabsElement(pageModel.renderTabs() as any).props.activeKey).toBe('tab-old');
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
it('should replace an implicit activeKey when its tab no longer exists', () => {
|
|
692
|
+
const oldTab = createTab('tab-old');
|
|
693
|
+
const newTab = createTab('tab-new');
|
|
694
|
+
(pageModel as any).subModels = { tabs: [oldTab, newTab] };
|
|
695
|
+
(pageModel as any).flowEngine = { getModel: vi.fn(() => undefined) };
|
|
696
|
+
pageModel.context.view = {
|
|
697
|
+
navigation: {
|
|
698
|
+
viewParams: {},
|
|
699
|
+
changeTo: vi.fn(),
|
|
700
|
+
},
|
|
701
|
+
} as any;
|
|
702
|
+
|
|
703
|
+
let syncState = runTabSync(pageModel.renderTabs() as any);
|
|
704
|
+
(pageModel as any).subModels = { tabs: [newTab] };
|
|
705
|
+
const result = pageModel.renderTabs() as any;
|
|
706
|
+
syncState = runTabSync(result, syncState);
|
|
707
|
+
|
|
708
|
+
expect(getTabsElement(result).props.activeKey).toBe('tab-new');
|
|
709
|
+
expect(pageModel.props.tabActiveKey).toBe('tab-new');
|
|
710
|
+
expect(syncState.effectiveActiveKey).toBe('tab-new');
|
|
711
|
+
expect(pageModel.context.view.navigation.changeTo).not.toHaveBeenCalled();
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
it('should clear an implicit activeKey and restore the page title when its last tab no longer exists', async () => {
|
|
715
|
+
const oldTab = createTab('tab-old');
|
|
716
|
+
(pageModel as any).subModels = { tabs: [oldTab] };
|
|
717
|
+
pageModel.props = { enableTabs: true, title: 'Page title' } as any;
|
|
718
|
+
(pageModel as any).flowEngine = { context: {}, getModel: vi.fn(() => undefined) };
|
|
719
|
+
const changeTo = vi.fn();
|
|
720
|
+
const invokeSpy = vi.spyOn(pageModel as any, 'invokeTabModelLifecycleMethod').mockImplementation(() => undefined);
|
|
721
|
+
const updateTitleSpy = vi.spyOn(pageModel as any, 'updateDocumentTitle');
|
|
722
|
+
pageModel.context.view = {
|
|
723
|
+
navigation: {
|
|
724
|
+
viewParams: {},
|
|
725
|
+
changeTo,
|
|
726
|
+
},
|
|
727
|
+
} as any;
|
|
728
|
+
|
|
729
|
+
let syncState = runTabSync(pageModel.renderTabs() as any);
|
|
730
|
+
invokeSpy.mockClear();
|
|
731
|
+
updateTitleSpy.mockClear();
|
|
732
|
+
|
|
733
|
+
(pageModel as any).subModels = { tabs: [] };
|
|
734
|
+
syncState = runTabSync(pageModel.renderTabs() as any, syncState);
|
|
735
|
+
|
|
736
|
+
expect(pageModel.props.tabActiveKey).toBeUndefined();
|
|
737
|
+
expect(changeTo).not.toHaveBeenCalled();
|
|
738
|
+
expect(invokeSpy).toHaveBeenCalledWith('tab-old', 'onInactive');
|
|
739
|
+
expect(updateTitleSpy).toHaveBeenCalledWith(undefined, 0);
|
|
740
|
+
expect(syncState.effectiveActiveKey).toBeUndefined();
|
|
741
|
+
await waitFor(() => expect(document.title).toBe('Page title'));
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
it('should synchronize an implicit hidden tab through the mounted effect', async () => {
|
|
745
|
+
const oldTab = createTab('tab-old');
|
|
746
|
+
(pageModel as any).subModels = { tabs: [oldTab, createTab('tab-new')] };
|
|
747
|
+
const changeTo = vi.fn();
|
|
748
|
+
const invokeSpy = vi.spyOn(pageModel as any, 'invokeTabModelLifecycleMethod');
|
|
749
|
+
const updateTitleSpy = vi.spyOn(pageModel as any, 'updateDocumentTitle').mockResolvedValue(undefined);
|
|
750
|
+
pageModel.context.view = {
|
|
751
|
+
navigation: {
|
|
752
|
+
viewParams: {},
|
|
753
|
+
changeTo,
|
|
754
|
+
},
|
|
755
|
+
} as any;
|
|
756
|
+
|
|
757
|
+
const view = render(pageModel.renderTabs() as any);
|
|
758
|
+
await waitFor(() => expect(pageModel.props.tabActiveKey).toBe('tab-old'));
|
|
759
|
+
|
|
760
|
+
invokeSpy.mockClear();
|
|
761
|
+
updateTitleSpy.mockClear();
|
|
762
|
+
oldTab.hidden = true;
|
|
763
|
+
view.rerender(pageModel.renderTabs() as any);
|
|
764
|
+
|
|
765
|
+
await waitFor(() => expect(pageModel.props.tabActiveKey).toBe('tab-new'));
|
|
766
|
+
expect(changeTo).not.toHaveBeenCalled();
|
|
767
|
+
expect(invokeSpy).toHaveBeenNthCalledWith(1, 'tab-new', 'onActive');
|
|
768
|
+
expect(invokeSpy).toHaveBeenNthCalledWith(2, 'tab-old', 'onInactive');
|
|
769
|
+
expect(updateTitleSpy).toHaveBeenCalledWith('tab-new', 0);
|
|
770
|
+
|
|
771
|
+
invokeSpy.mockClear();
|
|
772
|
+
updateTitleSpy.mockClear();
|
|
773
|
+
view.rerender(pageModel.renderTabs() as any);
|
|
774
|
+
await waitFor(() => expect(pageModel.props.tabActiveKey).toBe('tab-new'));
|
|
775
|
+
expect(invokeSpy).not.toHaveBeenCalled();
|
|
776
|
+
expect(updateTitleSpy).not.toHaveBeenCalled();
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
it('should clear an implicit activeKey when all tabs become hidden', () => {
|
|
780
|
+
const firstTab = createTab('tab-1');
|
|
781
|
+
const secondTab = createTab('tab-2');
|
|
782
|
+
(pageModel as any).subModels = { tabs: [firstTab, secondTab] };
|
|
783
|
+
const changeTo = vi.fn();
|
|
784
|
+
const invokeSpy = vi.spyOn(pageModel as any, 'invokeTabModelLifecycleMethod');
|
|
785
|
+
const updateTitleSpy = vi.spyOn(pageModel as any, 'updateDocumentTitle').mockResolvedValue(undefined);
|
|
786
|
+
pageModel.context.view = {
|
|
787
|
+
navigation: {
|
|
788
|
+
viewParams: {},
|
|
789
|
+
changeTo,
|
|
790
|
+
},
|
|
791
|
+
} as any;
|
|
792
|
+
|
|
793
|
+
let syncState = runTabSync(pageModel.renderTabs() as any);
|
|
794
|
+
invokeSpy.mockClear();
|
|
795
|
+
updateTitleSpy.mockClear();
|
|
796
|
+
|
|
797
|
+
firstTab.hidden = true;
|
|
798
|
+
secondTab.hidden = true;
|
|
799
|
+
const result = pageModel.renderTabs() as any;
|
|
800
|
+
syncState = runTabSync(result, syncState);
|
|
801
|
+
|
|
802
|
+
expect(getTabsElement(result).props.activeKey).not.toBe('tab-1');
|
|
803
|
+
expect(getTabsElement(result).props.activeKey).not.toBe('tab-2');
|
|
804
|
+
expect(pageModel.props.tabActiveKey).toBeUndefined();
|
|
805
|
+
expect(changeTo).not.toHaveBeenCalled();
|
|
806
|
+
expect(invokeSpy).toHaveBeenCalledWith('tab-1', 'onInactive');
|
|
807
|
+
expect(updateTitleSpy).toHaveBeenCalledWith(undefined, 0);
|
|
808
|
+
expect(syncState).toMatchObject({ effectiveActiveKey: undefined, previousAllHidden: true });
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
it('should restore the implicit default when a tab becomes visible after all tabs were hidden', () => {
|
|
812
|
+
const firstTab = createTab('tab-1');
|
|
813
|
+
const secondTab = createTab('tab-2');
|
|
814
|
+
(pageModel as any).subModels = { tabs: [firstTab, secondTab] };
|
|
815
|
+
const changeTo = vi.fn();
|
|
816
|
+
const invokeSpy = vi.spyOn(pageModel as any, 'invokeTabModelLifecycleMethod');
|
|
817
|
+
pageModel.context.view = {
|
|
818
|
+
navigation: {
|
|
819
|
+
viewParams: {},
|
|
820
|
+
changeTo,
|
|
821
|
+
},
|
|
822
|
+
} as any;
|
|
823
|
+
|
|
824
|
+
let syncState = runTabSync(pageModel.renderTabs() as any);
|
|
825
|
+
firstTab.hidden = true;
|
|
826
|
+
secondTab.hidden = true;
|
|
827
|
+
syncState = runTabSync(pageModel.renderTabs() as any, syncState);
|
|
828
|
+
invokeSpy.mockClear();
|
|
829
|
+
|
|
830
|
+
secondTab.hidden = false;
|
|
831
|
+
const result = pageModel.renderTabs() as any;
|
|
832
|
+
syncState = runTabSync(result, syncState);
|
|
833
|
+
|
|
834
|
+
expect(getTabsElement(result).props.activeKey).toBe('tab-2');
|
|
835
|
+
expect(pageModel.props.tabActiveKey).toBe('tab-2');
|
|
836
|
+
expect(changeTo).not.toHaveBeenCalled();
|
|
837
|
+
expect(invokeSpy).toHaveBeenCalledWith('tab-2', 'onActive');
|
|
838
|
+
expect(syncState).toMatchObject({ effectiveActiveKey: 'tab-2', previousAllHidden: false });
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
it('should keep an explicitly requested activeKey when all tabs are hidden', () => {
|
|
842
|
+
(pageModel as any).subModels = { tabs: [createTab('tab-1', true), createTab('tab-2', true)] };
|
|
843
|
+
pageModel.mapTabs = PageModelClass.prototype.mapTabs.bind(pageModel);
|
|
844
|
+
pageModel.props = { tabActiveKey: 'tab-1' } as any;
|
|
845
|
+
const changeTo = vi.fn();
|
|
846
|
+
const invokeSpy = vi.spyOn(pageModel as any, 'invokeTabModelLifecycleMethod').mockImplementation(() => undefined);
|
|
847
|
+
pageModel.context.view = {
|
|
848
|
+
navigation: {
|
|
849
|
+
viewParams: { tabUid: 'tab-1' },
|
|
850
|
+
changeTo,
|
|
851
|
+
},
|
|
852
|
+
} as any;
|
|
853
|
+
|
|
854
|
+
const result = pageModel.renderTabs() as any;
|
|
855
|
+
const nextState = runTabSync(result, {
|
|
856
|
+
...initialSyncState,
|
|
857
|
+
previousEffectiveActiveKey: 'tab-1',
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
expect(getTabsElement(result).props.activeKey).toBe('tab-1');
|
|
861
|
+
expect(getTabsElement(result).props.items.map((item: { key: string }) => item.key)).toEqual(['tab-1', 'tab-2']);
|
|
862
|
+
expect(getTabsElement(result).props.items[0].children).toBe('tab-1');
|
|
863
|
+
expect(getHiddenActiveTabContent(result)).toBeUndefined();
|
|
864
|
+
expect(changeTo).not.toHaveBeenCalled();
|
|
865
|
+
expect(pageModel.props.tabActiveKey).toBe('tab-1');
|
|
866
|
+
expect(invokeSpy).not.toHaveBeenCalled();
|
|
867
|
+
expect(nextState).toMatchObject({ effectiveActiveKey: 'tab-1', previousAllHidden: true });
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
it('should not implicitly activate a hidden pane when all entries are hidden and activeKey is unspecified', () => {
|
|
871
|
+
(pageModel as any).subModels = { tabs: [createTab('tab-1', true), createTab('tab-2', true)] };
|
|
872
|
+
pageModel.mapTabs = PageModelClass.prototype.mapTabs.bind(pageModel);
|
|
873
|
+
pageModel.context.view = {
|
|
874
|
+
navigation: {
|
|
875
|
+
viewParams: {},
|
|
876
|
+
changeTo: vi.fn(),
|
|
877
|
+
},
|
|
878
|
+
} as any;
|
|
879
|
+
|
|
880
|
+
const tabsElement = getTabsElement(pageModel.renderTabs() as any);
|
|
881
|
+
|
|
882
|
+
expect(tabsElement.props.items.map((item: { key: string }) => item.key)).toEqual(['tab-1', 'tab-2']);
|
|
883
|
+
expect(tabsElement.props.activeKey).not.toBeUndefined();
|
|
884
|
+
expect(tabsElement.props.activeKey).not.toBe('tab-1');
|
|
885
|
+
expect(tabsElement.props.activeKey).not.toBe('tab-2');
|
|
886
|
+
expect(pageModel.context.view.navigation.changeTo).not.toHaveBeenCalled();
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
it('should keep the requested hidden tab active when other tab entries become visible again', () => {
|
|
890
|
+
const firstTab = createTab('tab-1', true);
|
|
891
|
+
const secondTab = createTab('tab-2', true);
|
|
892
|
+
(pageModel as any).subModels = { tabs: [firstTab, secondTab] };
|
|
893
|
+
pageModel.props = { tabActiveKey: 'tab-1' } as any;
|
|
894
|
+
const changeTo = vi.fn();
|
|
895
|
+
const invokeSpy = vi.spyOn(pageModel as any, 'invokeTabModelLifecycleMethod').mockImplementation(() => undefined);
|
|
896
|
+
vi.spyOn(pageModel as any, 'updateDocumentTitle').mockResolvedValue(undefined);
|
|
897
|
+
pageModel.context.view = {
|
|
898
|
+
navigation: {
|
|
899
|
+
viewParams: { tabUid: 'tab-1' },
|
|
900
|
+
changeTo,
|
|
901
|
+
},
|
|
902
|
+
} as any;
|
|
903
|
+
|
|
904
|
+
let syncState = runTabSync(pageModel.renderTabs() as any, {
|
|
905
|
+
...initialSyncState,
|
|
906
|
+
previousEffectiveActiveKey: 'tab-1',
|
|
907
|
+
});
|
|
908
|
+
secondTab.hidden = false;
|
|
909
|
+
syncState = runTabSync(pageModel.renderTabs() as any, syncState);
|
|
910
|
+
|
|
911
|
+
expect(changeTo).not.toHaveBeenCalled();
|
|
912
|
+
expect(pageModel.props.tabActiveKey).toBe('tab-1');
|
|
913
|
+
expect(invokeSpy).not.toHaveBeenCalled();
|
|
914
|
+
expect(syncState.effectiveActiveKey).toBe('tab-1');
|
|
915
|
+
|
|
916
|
+
firstTab.hidden = false;
|
|
917
|
+
syncState = runTabSync(pageModel.renderTabs() as any, syncState);
|
|
918
|
+
|
|
919
|
+
expect(syncState.effectiveActiveKey).toBe('tab-1');
|
|
920
|
+
expect(pageModel.props.tabActiveKey).toBe('tab-1');
|
|
921
|
+
expect(changeTo).not.toHaveBeenCalled();
|
|
922
|
+
expect(invokeSpy).not.toHaveBeenCalled();
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
it('should keep the hidden active tab after UI Editor closes', () => {
|
|
926
|
+
(pageModel as any).subModels = { tabs: [createTab('tab-hidden', true), createTab('tab-visible')] };
|
|
927
|
+
pageModel.props = { tabActiveKey: 'tab-hidden' } as any;
|
|
928
|
+
const changeTo = vi.fn();
|
|
929
|
+
const invokeSpy = vi.spyOn(pageModel as any, 'invokeTabModelLifecycleMethod').mockImplementation(() => undefined);
|
|
930
|
+
pageModel.context = {
|
|
931
|
+
t: (value: string) => value,
|
|
932
|
+
flowSettingsEnabled: true,
|
|
933
|
+
view: {
|
|
934
|
+
navigation: {
|
|
935
|
+
viewParams: { tabUid: 'tab-hidden' },
|
|
936
|
+
changeTo,
|
|
937
|
+
},
|
|
938
|
+
},
|
|
939
|
+
} as any;
|
|
940
|
+
|
|
941
|
+
let syncState = runTabSync(pageModel.renderTabs() as any, {
|
|
942
|
+
...initialSyncState,
|
|
943
|
+
previousEffectiveActiveKey: 'tab-hidden',
|
|
944
|
+
});
|
|
945
|
+
expect(changeTo).not.toHaveBeenCalled();
|
|
946
|
+
|
|
947
|
+
pageModel.context.flowSettingsEnabled = false;
|
|
948
|
+
syncState = runTabSync(pageModel.renderTabs() as any, syncState);
|
|
949
|
+
|
|
950
|
+
expect(syncState.effectiveActiveKey).toBe('tab-hidden');
|
|
951
|
+
expect(changeTo).not.toHaveBeenCalled();
|
|
952
|
+
expect(invokeSpy).not.toHaveBeenCalled();
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
it('should keep hidden active synchronization idempotent across repeated effect passes', () => {
|
|
956
|
+
(pageModel as any).subModels = { tabs: [createTab('tab-hidden', true), createTab('tab-visible')] };
|
|
957
|
+
pageModel.props = { tabActiveKey: 'tab-hidden' } as any;
|
|
958
|
+
const changeTo = vi.fn();
|
|
959
|
+
const invokeSpy = vi.spyOn(pageModel as any, 'invokeTabModelLifecycleMethod').mockImplementation(() => undefined);
|
|
960
|
+
pageModel.context.view = {
|
|
961
|
+
navigation: {
|
|
962
|
+
viewParams: { tabUid: 'tab-hidden' },
|
|
963
|
+
changeTo,
|
|
964
|
+
},
|
|
965
|
+
} as any;
|
|
966
|
+
|
|
967
|
+
let syncState = runTabSync(pageModel.renderTabs() as any, {
|
|
968
|
+
...initialSyncState,
|
|
969
|
+
previousEffectiveActiveKey: 'tab-hidden',
|
|
970
|
+
});
|
|
971
|
+
syncState = runTabSync(pageModel.renderTabs() as any, syncState);
|
|
972
|
+
|
|
973
|
+
expect(syncState.effectiveActiveKey).toBe('tab-hidden');
|
|
974
|
+
expect(changeTo).not.toHaveBeenCalled();
|
|
975
|
+
expect(invokeSpy).not.toHaveBeenCalled();
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
it('should switch lifecycle and title when URL navigation selects a hidden tab', () => {
|
|
979
|
+
(pageModel as any).subModels = { tabs: [createTab('tab-visible'), createTab('tab-hidden', true)] };
|
|
980
|
+
pageModel.props = { tabActiveKey: 'tab-visible' } as any;
|
|
981
|
+
const changeTo = vi.fn();
|
|
982
|
+
const invokeSpy = vi.spyOn(pageModel as any, 'invokeTabModelLifecycleMethod');
|
|
983
|
+
const updateTitleSpy = vi.spyOn(pageModel as any, 'updateDocumentTitle').mockResolvedValue(undefined);
|
|
984
|
+
pageModel.context.view = {
|
|
985
|
+
navigation: {
|
|
986
|
+
viewParams: { tabUid: 'tab-hidden' },
|
|
987
|
+
changeTo,
|
|
988
|
+
},
|
|
989
|
+
} as any;
|
|
990
|
+
|
|
991
|
+
let syncState = runTabSync(pageModel.renderTabs() as any, {
|
|
992
|
+
...initialSyncState,
|
|
993
|
+
previousEffectiveActiveKey: 'tab-visible',
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
expect(changeTo).not.toHaveBeenCalled();
|
|
997
|
+
expect(pageModel.props.tabActiveKey).toBe('tab-hidden');
|
|
998
|
+
expect(invokeSpy).toHaveBeenNthCalledWith(1, 'tab-hidden', 'onActive');
|
|
999
|
+
expect(invokeSpy).toHaveBeenNthCalledWith(2, 'tab-visible', 'onInactive');
|
|
1000
|
+
expect(updateTitleSpy).toHaveBeenCalledWith('tab-hidden', 0);
|
|
1001
|
+
expect(syncState.effectiveActiveKey).toBe('tab-hidden');
|
|
1002
|
+
|
|
1003
|
+
invokeSpy.mockClear();
|
|
1004
|
+
updateTitleSpy.mockClear();
|
|
1005
|
+
syncState = runTabSync(pageModel.renderTabs() as any, syncState);
|
|
1006
|
+
|
|
1007
|
+
expect(invokeSpy).not.toHaveBeenCalled();
|
|
1008
|
+
expect(updateTitleSpy).not.toHaveBeenCalled();
|
|
1009
|
+
expect(syncState.effectiveActiveKey).toBe('tab-hidden');
|
|
1010
|
+
});
|
|
1011
|
+
|
|
348
1012
|
it('should use props.tabActiveKey if viewParams is missing', () => {
|
|
349
1013
|
pageModel.props = { tabActiveKey: 'tab-from-props' } as any;
|
|
350
1014
|
// @ts-ignore
|
|
@@ -353,7 +1017,7 @@ describe('PageModel', () => {
|
|
|
353
1017
|
};
|
|
354
1018
|
|
|
355
1019
|
const result = pageModel.renderTabs() as any;
|
|
356
|
-
const tabsElement = result
|
|
1020
|
+
const tabsElement = getTabsElement(result);
|
|
357
1021
|
expect(tabsElement.props.activeKey).toBe('tab-from-props');
|
|
358
1022
|
});
|
|
359
1023
|
|
|
@@ -366,7 +1030,7 @@ describe('PageModel', () => {
|
|
|
366
1030
|
} as any;
|
|
367
1031
|
|
|
368
1032
|
const result = pageModel.renderTabs() as any;
|
|
369
|
-
const tabsElement = result
|
|
1033
|
+
const tabsElement = getTabsElement(result);
|
|
370
1034
|
|
|
371
1035
|
expect(tabsElement.props.className).toBeUndefined();
|
|
372
1036
|
});
|
|
@@ -380,7 +1044,7 @@ describe('PageModel', () => {
|
|
|
380
1044
|
} as any;
|
|
381
1045
|
|
|
382
1046
|
const result = pageModel.renderTabs() as any;
|
|
383
|
-
const tabsElement = result
|
|
1047
|
+
const tabsElement = getTabsElement(result);
|
|
384
1048
|
|
|
385
1049
|
expect(tabsElement.props.className).toBeUndefined();
|
|
386
1050
|
});
|
|
@@ -394,7 +1058,7 @@ describe('PageModel', () => {
|
|
|
394
1058
|
} as any;
|
|
395
1059
|
|
|
396
1060
|
const result = pageModel.renderTabs() as any;
|
|
397
|
-
const tabsElement = result
|
|
1061
|
+
const tabsElement = getTabsElement(result);
|
|
398
1062
|
|
|
399
1063
|
expect(tabsElement.props.tabBarExtraContent.left).toBeTruthy();
|
|
400
1064
|
});
|
|
@@ -409,7 +1073,7 @@ describe('PageModel', () => {
|
|
|
409
1073
|
pageModel.tabBarExtraContent = { left: 'back' } as any;
|
|
410
1074
|
|
|
411
1075
|
const result = pageModel.renderTabs() as any;
|
|
412
|
-
const tabsElement = result
|
|
1076
|
+
const tabsElement = getTabsElement(result);
|
|
413
1077
|
|
|
414
1078
|
expect(tabsElement.props.tabBarExtraContent.left).toBe('back');
|
|
415
1079
|
});
|
|
@@ -424,7 +1088,7 @@ describe('PageModel', () => {
|
|
|
424
1088
|
pageModel.tabBarExtraContent = { right: 'custom-right' } as any;
|
|
425
1089
|
|
|
426
1090
|
const result = pageModel.renderTabs() as any;
|
|
427
|
-
const tabsElement = result
|
|
1091
|
+
const tabsElement = getTabsElement(result);
|
|
428
1092
|
|
|
429
1093
|
expect(tabsElement.props.tabBarExtraContent.right).toBe('custom-right');
|
|
430
1094
|
});
|
|
@@ -439,7 +1103,7 @@ describe('PageModel', () => {
|
|
|
439
1103
|
} as any;
|
|
440
1104
|
|
|
441
1105
|
const result = pageModel.renderTabs() as any;
|
|
442
|
-
const tabsElement = result
|
|
1106
|
+
const tabsElement = getTabsElement(result);
|
|
443
1107
|
const rightExtraContent = tabsElement.props.tabBarExtraContent.right;
|
|
444
1108
|
|
|
445
1109
|
expect(rightExtraContent).toBeTruthy();
|
|
@@ -466,7 +1130,7 @@ describe('PageModel', () => {
|
|
|
466
1130
|
} as any;
|
|
467
1131
|
|
|
468
1132
|
const result = pageModel.renderTabs() as any;
|
|
469
|
-
const tabsElement = result
|
|
1133
|
+
const tabsElement = getTabsElement(result);
|
|
470
1134
|
|
|
471
1135
|
tabsElement.props.onChange('tab-new');
|
|
472
1136
|
|
|
@@ -476,6 +1140,29 @@ describe('PageModel', () => {
|
|
|
476
1140
|
expect(invokeSpy).not.toHaveBeenCalledWith('tab-new', 'onInactive');
|
|
477
1141
|
expect(pageModel.props.tabActiveKey).toBe('tab-new');
|
|
478
1142
|
});
|
|
1143
|
+
|
|
1144
|
+
it('should not repeat lifecycle when synchronization observes a locally committed tab change', () => {
|
|
1145
|
+
(pageModel as any).subModels = { tabs: [createTab('tab-old'), createTab('tab-new')] };
|
|
1146
|
+
pageModel.props = { tabActiveKey: 'tab-old' } as any;
|
|
1147
|
+
pageModel.context.view = { navigation: null } as any;
|
|
1148
|
+
const invokeSpy = vi.spyOn(pageModel as any, 'invokeTabModelLifecycleMethod').mockImplementation(() => undefined);
|
|
1149
|
+
|
|
1150
|
+
let result = pageModel.renderTabs() as any;
|
|
1151
|
+
let syncState = runTabSync(result, {
|
|
1152
|
+
...initialSyncState,
|
|
1153
|
+
previousEffectiveActiveKey: 'tab-old',
|
|
1154
|
+
});
|
|
1155
|
+
getTabsElement(result).props.onChange('tab-new');
|
|
1156
|
+
|
|
1157
|
+
expect(invokeSpy).toHaveBeenNthCalledWith(1, 'tab-new', 'onActive');
|
|
1158
|
+
expect(invokeSpy).toHaveBeenNthCalledWith(2, 'tab-old', 'onInactive');
|
|
1159
|
+
|
|
1160
|
+
result = pageModel.renderTabs() as any;
|
|
1161
|
+
syncState = runTabSync(result, syncState);
|
|
1162
|
+
|
|
1163
|
+
expect(invokeSpy).toHaveBeenCalledTimes(2);
|
|
1164
|
+
expect(syncState.effectiveActiveKey).toBe('tab-new');
|
|
1165
|
+
});
|
|
479
1166
|
});
|
|
480
1167
|
|
|
481
1168
|
describe('render header spacing with tabs', () => {
|
|
@@ -793,6 +1480,99 @@ describe('PageModel', () => {
|
|
|
793
1480
|
expect(document.title).toBe('Fallback tab title');
|
|
794
1481
|
});
|
|
795
1482
|
|
|
1483
|
+
it('should use the hidden active tab documentTitle when its entry is hidden', async () => {
|
|
1484
|
+
pageModel.props = { enableTabs: true, tabActiveKey: 'tab-hidden' } as any;
|
|
1485
|
+
const hiddenTab = {
|
|
1486
|
+
uid: 'tab-hidden',
|
|
1487
|
+
hidden: true,
|
|
1488
|
+
stepParams: {
|
|
1489
|
+
pageTabSettings: {
|
|
1490
|
+
tab: {
|
|
1491
|
+
documentTitle: 'Hidden tab document title',
|
|
1492
|
+
},
|
|
1493
|
+
},
|
|
1494
|
+
},
|
|
1495
|
+
getTabTitle: vi.fn(() => 'Hidden tab'),
|
|
1496
|
+
};
|
|
1497
|
+
const visibleTab = {
|
|
1498
|
+
uid: 'tab-visible',
|
|
1499
|
+
stepParams: {
|
|
1500
|
+
pageTabSettings: {
|
|
1501
|
+
tab: {
|
|
1502
|
+
documentTitle: 'Visible tab document title',
|
|
1503
|
+
},
|
|
1504
|
+
},
|
|
1505
|
+
},
|
|
1506
|
+
getTabTitle: vi.fn(() => 'Visible tab'),
|
|
1507
|
+
};
|
|
1508
|
+
(pageModel as any).subModels = { tabs: [hiddenTab, visibleTab] };
|
|
1509
|
+
(pageModel as any).context.view.navigation = {
|
|
1510
|
+
viewParams: { tabUid: 'tab-hidden' },
|
|
1511
|
+
};
|
|
1512
|
+
|
|
1513
|
+
await (pageModel as any).updateDocumentTitle();
|
|
1514
|
+
|
|
1515
|
+
expect(document.title).toBe('Hidden tab document title');
|
|
1516
|
+
});
|
|
1517
|
+
|
|
1518
|
+
it('should use page documentTitle when tabs are enabled but all tabs are hidden', async () => {
|
|
1519
|
+
pageModel.props = { enableTabs: true, title: 'Page title' } as any;
|
|
1520
|
+
const hiddenTab = {
|
|
1521
|
+
uid: 'tab-hidden',
|
|
1522
|
+
hidden: true,
|
|
1523
|
+
stepParams: {
|
|
1524
|
+
pageTabSettings: {
|
|
1525
|
+
tab: {
|
|
1526
|
+
documentTitle: 'Hidden tab title',
|
|
1527
|
+
},
|
|
1528
|
+
},
|
|
1529
|
+
},
|
|
1530
|
+
getTabTitle: vi.fn(() => 'Hidden tab'),
|
|
1531
|
+
};
|
|
1532
|
+
(pageModel as any).subModels = { tabs: [hiddenTab] };
|
|
1533
|
+
(pageModel as any).stepParams = {
|
|
1534
|
+
pageSettings: {
|
|
1535
|
+
general: {
|
|
1536
|
+
documentTitle: 'Page document title',
|
|
1537
|
+
},
|
|
1538
|
+
},
|
|
1539
|
+
};
|
|
1540
|
+
(pageModel as any).flowEngine.getModel = vi.fn(() => hiddenTab);
|
|
1541
|
+
|
|
1542
|
+
await (pageModel as any).updateDocumentTitle();
|
|
1543
|
+
|
|
1544
|
+
expect(document.title).toBe('Page document title');
|
|
1545
|
+
});
|
|
1546
|
+
|
|
1547
|
+
it('should fallback to page title when all tabs are hidden and page documentTitle is empty', async () => {
|
|
1548
|
+
pageModel.props = { enableTabs: true, title: 'Fallback page title' } as any;
|
|
1549
|
+
const hiddenTab = {
|
|
1550
|
+
uid: 'tab-hidden',
|
|
1551
|
+
hidden: true,
|
|
1552
|
+
stepParams: {
|
|
1553
|
+
pageTabSettings: {
|
|
1554
|
+
tab: {
|
|
1555
|
+
documentTitle: 'Hidden tab title',
|
|
1556
|
+
},
|
|
1557
|
+
},
|
|
1558
|
+
},
|
|
1559
|
+
getTabTitle: vi.fn(() => 'Hidden tab'),
|
|
1560
|
+
};
|
|
1561
|
+
(pageModel as any).subModels = { tabs: [hiddenTab] };
|
|
1562
|
+
(pageModel as any).stepParams = {
|
|
1563
|
+
pageSettings: {
|
|
1564
|
+
general: {
|
|
1565
|
+
documentTitle: '',
|
|
1566
|
+
},
|
|
1567
|
+
},
|
|
1568
|
+
};
|
|
1569
|
+
(pageModel as any).flowEngine.getModel = vi.fn(() => hiddenTab);
|
|
1570
|
+
|
|
1571
|
+
await (pageModel as any).updateDocumentTitle();
|
|
1572
|
+
|
|
1573
|
+
expect(document.title).toBe('Fallback page title');
|
|
1574
|
+
});
|
|
1575
|
+
|
|
796
1576
|
it('should also update document title for closable page (popup)', async () => {
|
|
797
1577
|
pageModel.props = { enableTabs: false, title: 'Popup page title' } as any;
|
|
798
1578
|
(pageModel as any).context = {
|