@rancher/shell 3.0.12-rc.5 → 3.0.12-rc.6
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/apis/intf/resources-api/resource-base.ts +42 -2
- package/apis/intf/resources-api/resource-instance.ts +101 -0
- package/apis/intf/resources-api/resources-api.ts +177 -23
- package/apis/intf/resources.ts +8 -6
- package/apis/resources/__tests__/resources-api-class.test.ts +309 -28
- package/apis/resources/resources-api-class.ts +232 -43
- package/assets/images/providers/traefik.png +0 -0
- package/assets/translations/en-us.yaml +10 -4
- package/chart/__tests__/rancher-monitoring-dashboards-index.test.ts +269 -0
- package/chart/rancher-monitoring-dashboards/index.vue +155 -0
- package/cloud-credential/__tests__/generic.test.ts +132 -0
- package/cloud-credential/generic.vue +33 -3
- package/components/EtcdInfoBanner.vue +2 -12
- package/components/GrafanaDashboard.vue +2 -8
- package/components/Resource/Detail/Masthead/__tests__/index.test.ts +68 -14
- package/components/Resource/Detail/Masthead/index.vue +8 -3
- package/components/form/Security.vue +1 -1
- package/components/formatter/Translate.vue +22 -1
- package/components/nav/Group.vue +5 -10
- package/components/nav/TopLevelMenu.vue +1 -30
- package/components/nav/__tests__/Group.test.ts +61 -0
- package/config/labels-annotations.js +1 -0
- package/config/table-headers.js +5 -2
- package/core/__tests__/plugin-products-apply.test.ts +787 -0
- package/core/__tests__/plugin-products-extend.test.ts +189 -0
- package/core/__tests__/plugin-products-helpers.test.ts +81 -78
- package/core/__tests__/plugin-products-new.test.ts +389 -0
- package/core/__tests__/plugin-products-scenarios.test.ts +980 -0
- package/core/__tests__/plugin-products-side-menu.test.ts +1935 -0
- package/core/plugin-products-base.ts +201 -93
- package/core/plugin-products-extending.ts +1 -1
- package/core/plugin-products-external.ts +380 -0
- package/core/plugin-products-helpers.ts +28 -24
- package/core/plugin-products-internal.ts +207 -0
- package/core/plugin-products-top-level.ts +10 -5
- package/core/plugin-products-type-guards.ts +17 -6
- package/core/plugin-products.ts +5 -7
- package/core/plugin-types.ts +1 -389
- package/core/plugin.ts +10 -11
- package/core/types.ts +6 -202
- package/detail/__tests__/provisioning.cattle.io.cluster.test.ts +52 -0
- package/detail/pod.vue +1 -1
- package/detail/provisioning.cattle.io.cluster.vue +0 -2
- package/edit/auth/AuthProviderWarningBanners.vue +12 -0
- package/edit/auth/__tests__/AuthProviderWarningBanners.test.ts +10 -1
- package/edit/auth/__tests__/azuread.test.ts +1 -0
- package/edit/auth/__tests__/github.test.ts +1 -0
- package/edit/auth/__tests__/oidc.test.ts +1 -0
- package/edit/auth/__tests__/saml.test.ts +1 -0
- package/edit/auth/ldap/__tests__/index.test.ts +1 -0
- package/edit/provisioning.cattle.io.cluster/index.vue +1 -1
- package/edit/provisioning.cattle.io.cluster/rke2.vue +1 -1
- package/edit/provisioning.cattle.io.cluster/tabs/networking/index.vue +2 -2
- package/list/management.cattle.io.user.vue +24 -5
- package/list/utils/management.cattle.io.cluster.utils.ts +1 -1
- package/machine-config/__tests__/generic.test.ts +163 -0
- package/machine-config/components/EC2Networking.vue +31 -7
- package/machine-config/components/__tests__/EC2Networking.test.ts +38 -15
- package/machine-config/generic.vue +32 -5
- package/mixins/resource-fetch.js +1 -1
- package/models/__tests__/rke-machine.cattle.io.vmwarevspheremachinetemplate.test.ts +22 -0
- package/models/pod.js +12 -10
- package/models/rke-machine.cattle.io.vmwarevspheremachinetemplate.js +9 -0
- package/package.json +1 -1
- package/pages/auth/login.vue +1 -1
- package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +110 -0
- package/pages/c/_cluster/apps/charts/install.vue +29 -0
- package/pages/c/_cluster/auth/config/index.vue +11 -4
- package/pages/c/_cluster/monitoring/index.vue +6 -1
- package/plugins/dashboard-store/resource-class.js +10 -3
- package/plugins/steve/__tests__/steve-class-resource-api.test.ts +147 -0
- package/plugins/steve/steve-class.js +43 -0
- package/plugins/steve/subscribe.js +11 -0
- package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +115 -0
- package/rancher-components/Form/LabeledInput/LabeledInput.vue +83 -3
- package/store/__tests__/auth.test.ts +18 -0
- package/store/__tests__/notifications.test.ts +466 -1
- package/store/__tests__/ui-context.test.ts +255 -0
- package/store/auth.js +11 -1
- package/store/features.js +1 -0
- package/store/plugins.js +6 -0
- package/types/rancher/steve.api.ts +2 -2
- package/types/shell/index.d.ts +58 -0
- package/types/store/dashboard-store.types.ts +2 -2
- package/types/store/type-map.ts +262 -1
- package/utils/async.ts +2 -0
- package/utils/grafana.js +2 -17
- package/utils/monitoring.js +38 -1
- package/core/__tests__/plugin-products.test.ts +0 -4694
|
@@ -0,0 +1,1935 @@
|
|
|
1
|
+
import { PluginProduct } from '@shell/core/plugin-products';
|
|
2
|
+
import { IExtension } from '@shell/core/types';
|
|
3
|
+
import { ProductChildGroup, ProductChildPage, ProductMetadata, StandardProductNames } from '@shell/core/plugin-products-external';
|
|
4
|
+
|
|
5
|
+
// Mock the helper functions
|
|
6
|
+
jest.mock('@shell/core/plugin-products-helpers', () => ({
|
|
7
|
+
gatherChildrenOrdering: jest.fn((config) => config),
|
|
8
|
+
generateTopLevelExtensionSimpleBaseRoute: jest.fn((name, opts) => ({
|
|
9
|
+
name: `${ name }-simple`,
|
|
10
|
+
path: opts?.omitPath ? '' : `/${ name }`,
|
|
11
|
+
component: opts?.component,
|
|
12
|
+
})),
|
|
13
|
+
generateVirtualTypeRoute: jest.fn((parentName, childName, opts) => ({
|
|
14
|
+
name: childName ? `${ parentName }-${ childName }` : `${ parentName }-group`,
|
|
15
|
+
path: opts?.omitPath ? '' : `/${ parentName }/${ childName || 'group' }`,
|
|
16
|
+
component: opts?.component,
|
|
17
|
+
})),
|
|
18
|
+
generateConfigureTypeRoute: jest.fn((parentName, page, opts) => {
|
|
19
|
+
const routeName = opts?.extendProduct ? `c-cluster-${ parentName }-resource` : `${ parentName }-c-cluster-resource`;
|
|
20
|
+
const routePath = opts?.extendProduct ? `c/:cluster/${ parentName }/:resource` : `${ parentName }/c/:cluster/:resource`;
|
|
21
|
+
const cluster = opts?.extendProduct ? undefined : '__BLANK_CLUSTER__';
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
name: routeName,
|
|
25
|
+
path: opts?.omitPath ? '' : routePath,
|
|
26
|
+
params: cluster ? {
|
|
27
|
+
product: parentName.replace(/-/g, ''),
|
|
28
|
+
cluster,
|
|
29
|
+
resource: page?.type,
|
|
30
|
+
} : {
|
|
31
|
+
product: parentName,
|
|
32
|
+
resource: page?.type,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}),
|
|
36
|
+
generateResourceRoutes: jest.fn((parentName, child) => [
|
|
37
|
+
{
|
|
38
|
+
name: `${ parentName }-${ child.type }-list`,
|
|
39
|
+
path: `/${ parentName }/${ child.type }`,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: `${ parentName }-${ child.type }-detail`,
|
|
43
|
+
path: `/${ parentName }/${ child.type }/:id`,
|
|
44
|
+
},
|
|
45
|
+
]),
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
jest.mock('@shell/core/productDebugger', () => ({
|
|
49
|
+
DSLRegistrationsPerProduct: jest.fn(),
|
|
50
|
+
registeredRoutes: jest.fn(),
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
// Create mock factories
|
|
54
|
+
function createMockPlugin(): IExtension {
|
|
55
|
+
return {
|
|
56
|
+
_registerTopLevelProduct: jest.fn(),
|
|
57
|
+
addRoute: jest.fn(),
|
|
58
|
+
enableServerSidePagination: jest.fn(),
|
|
59
|
+
DSL: jest.fn(),
|
|
60
|
+
} as any as IExtension;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function createMockStore(extendableProducts: string[] = Object.values(StandardProductNames), managementSchemas: string[] = []): any {
|
|
64
|
+
return {
|
|
65
|
+
getters: {
|
|
66
|
+
'type-map/productByName': (productName: string) => (extendableProducts.includes(productName) ? { name: productName, extendable: true } : undefined),
|
|
67
|
+
'management/schemaFor': (type: string) => (managementSchemas.includes(type) ? { id: type } : undefined),
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
describe('pluginProduct', () => {
|
|
73
|
+
// describe('side menu structure and ordering', () => {
|
|
74
|
+
// describe('new product - virtualType ordering', () => {
|
|
75
|
+
// it('should register virtualTypes in config array order when no weights specified', () => {
|
|
76
|
+
// const mockPlugin = createMockPlugin();
|
|
77
|
+
// const mockStore = createMockStore();
|
|
78
|
+
// const virtualTypeCalls: any[] = [];
|
|
79
|
+
// const mockDSL = {
|
|
80
|
+
// product: jest.fn(),
|
|
81
|
+
// basicType: jest.fn(),
|
|
82
|
+
// labelGroup: jest.fn(),
|
|
83
|
+
// setGroupDefaultType: jest.fn(),
|
|
84
|
+
// weightGroup: jest.fn(),
|
|
85
|
+
// virtualType: jest.fn((...args) => virtualTypeCalls.push(args)),
|
|
86
|
+
// configureType: jest.fn(),
|
|
87
|
+
// weightType: jest.fn(),
|
|
88
|
+
// };
|
|
89
|
+
|
|
90
|
+
// (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
91
|
+
|
|
92
|
+
// const productMetadata: ProductMetadata = {
|
|
93
|
+
// name: 'test-ordering',
|
|
94
|
+
// label: 'Test Ordering',
|
|
95
|
+
// };
|
|
96
|
+
// const config: ProductChildPage[] = [
|
|
97
|
+
// {
|
|
98
|
+
// name: 'first-page',
|
|
99
|
+
// label: 'First Page',
|
|
100
|
+
// component: { name: 'FirstComponent' },
|
|
101
|
+
// },
|
|
102
|
+
// {
|
|
103
|
+
// name: 'second-page',
|
|
104
|
+
// label: 'Second Page',
|
|
105
|
+
// component: { name: 'SecondComponent' },
|
|
106
|
+
// },
|
|
107
|
+
// {
|
|
108
|
+
// name: 'third-page',
|
|
109
|
+
// label: 'Third Page',
|
|
110
|
+
// component: { name: 'ThirdComponent' },
|
|
111
|
+
// },
|
|
112
|
+
// ];
|
|
113
|
+
|
|
114
|
+
// const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
115
|
+
|
|
116
|
+
// pluginProduct.apply(mockPlugin, mockStore);
|
|
117
|
+
|
|
118
|
+
// // Verify order - virtualType calls should match config order
|
|
119
|
+
// expect(virtualTypeCalls).toHaveLength(3);
|
|
120
|
+
// expect(virtualTypeCalls[0][0]).toMatchObject({ name: 'testordering-first-page' });
|
|
121
|
+
// expect(virtualTypeCalls[1][0]).toMatchObject({ name: 'testordering-second-page' });
|
|
122
|
+
// expect(virtualTypeCalls[2][0]).toMatchObject({ name: 'testordering-third-page' });
|
|
123
|
+
// });
|
|
124
|
+
|
|
125
|
+
// it('should register virtualTypes with weight parameter for menu ordering', () => {
|
|
126
|
+
// const mockPlugin = createMockPlugin();
|
|
127
|
+
// const mockStore = createMockStore();
|
|
128
|
+
// const virtualTypeCalls: any[] = [];
|
|
129
|
+
// const mockDSL = {
|
|
130
|
+
// product: jest.fn(),
|
|
131
|
+
// basicType: jest.fn(),
|
|
132
|
+
// labelGroup: jest.fn(),
|
|
133
|
+
// setGroupDefaultType: jest.fn(),
|
|
134
|
+
// weightGroup: jest.fn(),
|
|
135
|
+
// virtualType: jest.fn((...args) => virtualTypeCalls.push(args)),
|
|
136
|
+
// configureType: jest.fn(),
|
|
137
|
+
// weightType: jest.fn(),
|
|
138
|
+
// };
|
|
139
|
+
|
|
140
|
+
// (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
141
|
+
|
|
142
|
+
// const productMetadata: ProductMetadata = {
|
|
143
|
+
// name: 'test-weights',
|
|
144
|
+
// label: 'Test Weights',
|
|
145
|
+
// };
|
|
146
|
+
// const config: ProductChildPage[] = [
|
|
147
|
+
// {
|
|
148
|
+
// name: 'low-priority',
|
|
149
|
+
// label: 'Low Priority',
|
|
150
|
+
// component: { name: 'Component1' },
|
|
151
|
+
// sideMenu: { weight: 100 },
|
|
152
|
+
// },
|
|
153
|
+
// {
|
|
154
|
+
// name: 'high-priority',
|
|
155
|
+
// label: 'High Priority',
|
|
156
|
+
// component: { name: 'Component2' },
|
|
157
|
+
// sideMenu: { weight: 1 },
|
|
158
|
+
// },
|
|
159
|
+
// {
|
|
160
|
+
// name: 'medium-priority',
|
|
161
|
+
// label: 'Medium Priority',
|
|
162
|
+
// component: { name: 'Component3' },
|
|
163
|
+
// sideMenu: { weight: 50 },
|
|
164
|
+
// },
|
|
165
|
+
// ];
|
|
166
|
+
|
|
167
|
+
// const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
168
|
+
|
|
169
|
+
// pluginProduct.apply(mockPlugin, mockStore);
|
|
170
|
+
|
|
171
|
+
// // Verify weight is passed to virtualType
|
|
172
|
+
// expect(virtualTypeCalls[0][0].weight).toBe(100);
|
|
173
|
+
// expect(virtualTypeCalls[1][0].weight).toBe(1);
|
|
174
|
+
// expect(virtualTypeCalls[2][0].weight).toBe(50);
|
|
175
|
+
// });
|
|
176
|
+
// });
|
|
177
|
+
|
|
178
|
+
// describe('new product - mixed types ordering', () => {
|
|
179
|
+
// it('should maintain order of virtualTypes and configureTypes as specified in config', () => {
|
|
180
|
+
// const mockPlugin = createMockPlugin();
|
|
181
|
+
// const mockStore = createMockStore();
|
|
182
|
+
// const dslCallOrder: string[] = [];
|
|
183
|
+
// const mockDSL = {
|
|
184
|
+
// product: jest.fn(),
|
|
185
|
+
// basicType: jest.fn((...args) => dslCallOrder.push(`basicType:${ args[0] }`)),
|
|
186
|
+
// labelGroup: jest.fn(),
|
|
187
|
+
// setGroupDefaultType: jest.fn(),
|
|
188
|
+
// weightGroup: jest.fn(),
|
|
189
|
+
// virtualType: jest.fn((...args) => dslCallOrder.push(`virtualType:${ args[0].name }`)),
|
|
190
|
+
// configureType: jest.fn((...args) => dslCallOrder.push(`configureType:${ args[0] }`)),
|
|
191
|
+
// weightType: jest.fn(),
|
|
192
|
+
// };
|
|
193
|
+
|
|
194
|
+
// (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
195
|
+
|
|
196
|
+
// const productMetadata: ProductMetadata = {
|
|
197
|
+
// name: 'mixed-types',
|
|
198
|
+
// label: 'Mixed Types Product',
|
|
199
|
+
// };
|
|
200
|
+
// const config: ProductChildPage[] = [
|
|
201
|
+
// { type: 'fleet.cattle.io.clustergroup', sideMenu: { weight: 1 } },
|
|
202
|
+
// {
|
|
203
|
+
// name: 'custom-page',
|
|
204
|
+
// label: 'Custom Page',
|
|
205
|
+
// component: { name: 'CustomComponent' },
|
|
206
|
+
// sideMenu: { weight: 2 },
|
|
207
|
+
// },
|
|
208
|
+
// { type: 'upgrade.cattle.io.plan', sideMenu: { weight: 3 } },
|
|
209
|
+
// ];
|
|
210
|
+
|
|
211
|
+
// const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
212
|
+
|
|
213
|
+
// pluginProduct.apply(mockPlugin, mockStore);
|
|
214
|
+
|
|
215
|
+
// // Verify DSL methods called in correct order
|
|
216
|
+
// const relevantCalls = dslCallOrder.filter((call) => call.startsWith('configureType:') || call.startsWith('virtualType:')
|
|
217
|
+
// );
|
|
218
|
+
|
|
219
|
+
// expect(relevantCalls).toStrictEqual([
|
|
220
|
+
// 'configureType:fleet.cattle.io.clustergroup',
|
|
221
|
+
// 'virtualType:mixedtypes-custom-page',
|
|
222
|
+
// 'configureType:upgrade.cattle.io.plan',
|
|
223
|
+
// ]);
|
|
224
|
+
// });
|
|
225
|
+
// });
|
|
226
|
+
|
|
227
|
+
// describe('new product - group menu structure', () => {
|
|
228
|
+
// it('should create nested side menu structure with groups', () => {
|
|
229
|
+
// const mockPlugin = createMockPlugin();
|
|
230
|
+
// const mockStore = createMockStore();
|
|
231
|
+
// const basicTypeCalls: any[] = [];
|
|
232
|
+
// const labelGroupCalls: any[] = [];
|
|
233
|
+
// const weightGroupCalls: any[] = [];
|
|
234
|
+
// const virtualTypeCalls: any[] = [];
|
|
235
|
+
|
|
236
|
+
// const mockDSL = {
|
|
237
|
+
// product: jest.fn(),
|
|
238
|
+
// basicType: jest.fn((...args) => basicTypeCalls.push(args)),
|
|
239
|
+
// labelGroup: jest.fn((...args) => labelGroupCalls.push(args)),
|
|
240
|
+
// setGroupDefaultType: jest.fn(),
|
|
241
|
+
// weightGroup: jest.fn((...args) => weightGroupCalls.push(args)),
|
|
242
|
+
// virtualType: jest.fn((...args) => virtualTypeCalls.push(args)),
|
|
243
|
+
// configureType: jest.fn(),
|
|
244
|
+
// weightType: jest.fn(),
|
|
245
|
+
// };
|
|
246
|
+
|
|
247
|
+
// (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
248
|
+
|
|
249
|
+
// const productMetadata: ProductMetadata = {
|
|
250
|
+
// name: 'grouped-product',
|
|
251
|
+
// label: 'Grouped Product',
|
|
252
|
+
// };
|
|
253
|
+
// const config: ProductChildGroup[] = [
|
|
254
|
+
// {
|
|
255
|
+
// name: 'settings-group',
|
|
256
|
+
// label: 'Settings',
|
|
257
|
+
// sideMenu: {
|
|
258
|
+
// weight: 10,
|
|
259
|
+
// children: [
|
|
260
|
+
// {
|
|
261
|
+
// name: 'general',
|
|
262
|
+
// label: 'General',
|
|
263
|
+
// component: { name: 'GeneralComponent' },
|
|
264
|
+
// },
|
|
265
|
+
// {
|
|
266
|
+
// name: 'advanced',
|
|
267
|
+
// label: 'Advanced',
|
|
268
|
+
// component: { name: 'AdvancedComponent' },
|
|
269
|
+
// },
|
|
270
|
+
// ],
|
|
271
|
+
// },
|
|
272
|
+
// },
|
|
273
|
+
// ];
|
|
274
|
+
|
|
275
|
+
// const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
276
|
+
|
|
277
|
+
// pluginProduct.apply(mockPlugin, mockStore);
|
|
278
|
+
|
|
279
|
+
// // Verify group label was set
|
|
280
|
+
// expect(labelGroupCalls.length).toBeGreaterThan(0);
|
|
281
|
+
// expect(labelGroupCalls[0]).toStrictEqual(['groupedproduct-settings-group', 'Settings', undefined]);
|
|
282
|
+
|
|
283
|
+
// // Verify group weight was set
|
|
284
|
+
// expect(weightGroupCalls.length).toBeGreaterThan(0);
|
|
285
|
+
// expect(weightGroupCalls[0]).toStrictEqual(['groupedproduct-settings-group', 10, true]);
|
|
286
|
+
|
|
287
|
+
// // Verify basicType was called for navigation with group children
|
|
288
|
+
// // basicType is called twice: once for top-level items (excluding groups), once for group children
|
|
289
|
+
// expect(basicTypeCalls.length).toBeGreaterThan(1);
|
|
290
|
+
// // Check the second call which includes group children
|
|
291
|
+
// expect(basicTypeCalls[1][0]).toStrictEqual(expect.arrayContaining([
|
|
292
|
+
// expect.stringContaining('general'),
|
|
293
|
+
// expect.stringContaining('advanced')
|
|
294
|
+
// ]));
|
|
295
|
+
|
|
296
|
+
// // Verify nested virtualTypes were created
|
|
297
|
+
// expect(virtualTypeCalls).toHaveLength(2);
|
|
298
|
+
// });
|
|
299
|
+
|
|
300
|
+
// it('should set label for groups without explicit label via labelGroup', () => {
|
|
301
|
+
// const mockPlugin = createMockPlugin();
|
|
302
|
+
// const mockStore = createMockStore();
|
|
303
|
+
// const labelGroupCalls: any[] = [];
|
|
304
|
+
|
|
305
|
+
// const mockDSL = {
|
|
306
|
+
// product: jest.fn(),
|
|
307
|
+
// basicType: jest.fn(),
|
|
308
|
+
// labelGroup: jest.fn((...args) => labelGroupCalls.push(args)),
|
|
309
|
+
// setGroupDefaultType: jest.fn(),
|
|
310
|
+
// weightGroup: jest.fn(),
|
|
311
|
+
// virtualType: jest.fn(),
|
|
312
|
+
// configureType: jest.fn(),
|
|
313
|
+
// weightType: jest.fn(),
|
|
314
|
+
// };
|
|
315
|
+
|
|
316
|
+
// (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
317
|
+
|
|
318
|
+
// const productMetadata: ProductMetadata = {
|
|
319
|
+
// name: 'product-with-groups',
|
|
320
|
+
// label: 'Product',
|
|
321
|
+
// };
|
|
322
|
+
// const config: ProductChildGroup[] = [
|
|
323
|
+
// {
|
|
324
|
+
// name: 'my-group',
|
|
325
|
+
// label: 'My Group Label',
|
|
326
|
+
// sideMenu: {
|
|
327
|
+
// children: [
|
|
328
|
+
// {
|
|
329
|
+
// name: 'child1',
|
|
330
|
+
// label: 'Child 1',
|
|
331
|
+
// component: { name: 'Child1Component' },
|
|
332
|
+
// },
|
|
333
|
+
// ],
|
|
334
|
+
// }
|
|
335
|
+
|
|
336
|
+
// },
|
|
337
|
+
// ];
|
|
338
|
+
|
|
339
|
+
// const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
340
|
+
|
|
341
|
+
// pluginProduct.apply(mockPlugin, mockStore);
|
|
342
|
+
|
|
343
|
+
// // Verify labelGroup was called with the group's label
|
|
344
|
+
// expect(labelGroupCalls[0]).toStrictEqual(['productwithgroups-my-group', 'My Group Label', undefined]);
|
|
345
|
+
// });
|
|
346
|
+
// });
|
|
347
|
+
|
|
348
|
+
// describe('extending product - side menu additions', () => {
|
|
349
|
+
// it('should add virtualType to existing product navigation in order specified', () => {
|
|
350
|
+
// const mockPlugin = createMockPlugin();
|
|
351
|
+
// const mockStore = createMockStore();
|
|
352
|
+
// const virtualTypeCalls: any[] = [];
|
|
353
|
+
// const mockDSL = {
|
|
354
|
+
// product: jest.fn(),
|
|
355
|
+
// basicType: jest.fn(),
|
|
356
|
+
// labelGroup: jest.fn(),
|
|
357
|
+
// setGroupDefaultType: jest.fn(),
|
|
358
|
+
// weightGroup: jest.fn(),
|
|
359
|
+
// virtualType: jest.fn((...args) => virtualTypeCalls.push(args)),
|
|
360
|
+
// configureType: jest.fn(),
|
|
361
|
+
// weightType: jest.fn(),
|
|
362
|
+
// };
|
|
363
|
+
|
|
364
|
+
// (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
365
|
+
|
|
366
|
+
// const validStandardProduct = StandardProductNames.EXPLORER;
|
|
367
|
+
// const config: ProductChildPage[] = [
|
|
368
|
+
// {
|
|
369
|
+
// name: 'custom-section-1',
|
|
370
|
+
// label: 'Custom Section 1',
|
|
371
|
+
// component: { name: 'CustomComponent1' },
|
|
372
|
+
// sideMenu: { weight: 99 },
|
|
373
|
+
// },
|
|
374
|
+
// {
|
|
375
|
+
// name: 'custom-section-2',
|
|
376
|
+
// label: 'Custom Section 2',
|
|
377
|
+
// component: { name: 'CustomComponent2' },
|
|
378
|
+
// sideMenu: { weight: 98 },
|
|
379
|
+
// },
|
|
380
|
+
// ];
|
|
381
|
+
|
|
382
|
+
// const pluginProduct = new PluginProduct(mockPlugin, validStandardProduct, config);
|
|
383
|
+
|
|
384
|
+
// pluginProduct.apply(mockPlugin, mockStore);
|
|
385
|
+
|
|
386
|
+
// // Verify virtualTypes added in order with weights
|
|
387
|
+
// expect(virtualTypeCalls).toHaveLength(2);
|
|
388
|
+
// expect(virtualTypeCalls[0][0]).toMatchObject({ name: 'explorer-custom-section-1' });
|
|
389
|
+
// expect(virtualTypeCalls[0][0].weight).toBe(99);
|
|
390
|
+
// expect(virtualTypeCalls[1][0]).toMatchObject({ name: 'explorer-custom-section-2' });
|
|
391
|
+
// expect(virtualTypeCalls[1][0].weight).toBe(98);
|
|
392
|
+
// });
|
|
393
|
+
|
|
394
|
+
// it('should add groups to existing product with proper navigation structure', () => {
|
|
395
|
+
// const mockPlugin = createMockPlugin();
|
|
396
|
+
// const mockStore = createMockStore();
|
|
397
|
+
// const basicTypeCalls: any[] = [];
|
|
398
|
+
// const labelGroupCalls: any[] = [];
|
|
399
|
+
|
|
400
|
+
// const mockDSL = {
|
|
401
|
+
// product: jest.fn(),
|
|
402
|
+
// basicType: jest.fn((...args) => basicTypeCalls.push(args)),
|
|
403
|
+
// labelGroup: jest.fn((...args) => labelGroupCalls.push(args)),
|
|
404
|
+
// setGroupDefaultType: jest.fn(),
|
|
405
|
+
// weightGroup: jest.fn(),
|
|
406
|
+
// virtualType: jest.fn(),
|
|
407
|
+
// configureType: jest.fn(),
|
|
408
|
+
// weightType: jest.fn(),
|
|
409
|
+
// };
|
|
410
|
+
|
|
411
|
+
// (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
412
|
+
|
|
413
|
+
// const validStandardProduct = StandardProductNames.SETTINGS;
|
|
414
|
+
// const config: ProductChildGroup[] = [
|
|
415
|
+
// {
|
|
416
|
+
// name: 'extension-settings',
|
|
417
|
+
// label: 'Extension Settings',
|
|
418
|
+
// sideMenu: {
|
|
419
|
+
// weight: 5,
|
|
420
|
+
// children: [
|
|
421
|
+
// {
|
|
422
|
+
// name: 'config-page',
|
|
423
|
+
// label: 'Configuration',
|
|
424
|
+
// component: { name: 'ConfigComponent' },
|
|
425
|
+
// },
|
|
426
|
+
// {
|
|
427
|
+
// name: 'advanced-page',
|
|
428
|
+
// label: 'Advanced',
|
|
429
|
+
// component: { name: 'AdvancedComponent' },
|
|
430
|
+
// },
|
|
431
|
+
// ],
|
|
432
|
+
// },
|
|
433
|
+
// },
|
|
434
|
+
// ];
|
|
435
|
+
|
|
436
|
+
// const pluginProduct = new PluginProduct(mockPlugin, validStandardProduct, config);
|
|
437
|
+
|
|
438
|
+
// pluginProduct.apply(mockPlugin, mockStore);
|
|
439
|
+
|
|
440
|
+
// // Verify group label was set for the extended product
|
|
441
|
+
// expect(labelGroupCalls.length).toBeGreaterThan(0);
|
|
442
|
+
// expect(labelGroupCalls[0]).toStrictEqual(['settings-extension-settings', 'Extension Settings', undefined]);
|
|
443
|
+
|
|
444
|
+
// // Verify basicType includes children for navigation
|
|
445
|
+
// // basicType is called multiple times: first for top-level items, then for each group's children
|
|
446
|
+
// expect(basicTypeCalls.length).toBeGreaterThan(0);
|
|
447
|
+
// // Check the call that includes group children (should be in basicTypeCalls)
|
|
448
|
+
// const groupChildrenCall = basicTypeCalls.find((call) => call[0] && Array.isArray(call[0]) && call[0].some((name: string) => name.includes('config-page'))
|
|
449
|
+
// );
|
|
450
|
+
|
|
451
|
+
// expect(groupChildrenCall).toBeDefined();
|
|
452
|
+
// expect(groupChildrenCall![0]).toStrictEqual(expect.arrayContaining([
|
|
453
|
+
// expect.stringContaining('config-page'),
|
|
454
|
+
// expect.stringContaining('advanced-page')
|
|
455
|
+
// ]));
|
|
456
|
+
// });
|
|
457
|
+
// });
|
|
458
|
+
|
|
459
|
+
// describe('navigation and default route determination', () => {
|
|
460
|
+
// it('should pass correct default route to product registration', () => {
|
|
461
|
+
// const mockPlugin = createMockPlugin();
|
|
462
|
+
// const mockStore = createMockStore();
|
|
463
|
+
// let productConfig: any;
|
|
464
|
+
// const mockDSL = {
|
|
465
|
+
// product: jest.fn((config) => {
|
|
466
|
+
// productConfig = config;
|
|
467
|
+
// }),
|
|
468
|
+
// basicType: jest.fn(),
|
|
469
|
+
// labelGroup: jest.fn(),
|
|
470
|
+
// setGroupDefaultType: jest.fn(),
|
|
471
|
+
// weightGroup: jest.fn(),
|
|
472
|
+
// virtualType: jest.fn(),
|
|
473
|
+
// configureType: jest.fn(),
|
|
474
|
+
// weightType: jest.fn(),
|
|
475
|
+
// };
|
|
476
|
+
|
|
477
|
+
// (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
478
|
+
|
|
479
|
+
// const productMetadata: ProductMetadata = {
|
|
480
|
+
// name: 'test-default-route',
|
|
481
|
+
// label: 'Test Default Route',
|
|
482
|
+
// };
|
|
483
|
+
// const config: ProductChildPage[] = [
|
|
484
|
+
// {
|
|
485
|
+
// name: 'overview',
|
|
486
|
+
// label: 'Overview',
|
|
487
|
+
// component: { name: 'OverviewComponent' },
|
|
488
|
+
// },
|
|
489
|
+
// {
|
|
490
|
+
// name: 'details',
|
|
491
|
+
// label: 'Details',
|
|
492
|
+
// component: { name: 'DetailsComponent' },
|
|
493
|
+
// },
|
|
494
|
+
// ];
|
|
495
|
+
|
|
496
|
+
// const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
497
|
+
|
|
498
|
+
// pluginProduct.apply(mockPlugin, mockStore);
|
|
499
|
+
|
|
500
|
+
// // Verify product registration includes default route (first config item)
|
|
501
|
+
// expect(productConfig).toBeDefined();
|
|
502
|
+
// expect(productConfig.to).toBeDefined();
|
|
503
|
+
// });
|
|
504
|
+
|
|
505
|
+
// it('should use first group child as default route when first config item is a group', () => {
|
|
506
|
+
// const mockPlugin = createMockPlugin();
|
|
507
|
+
// const mockStore = createMockStore();
|
|
508
|
+
// let productConfig: any;
|
|
509
|
+
// const mockDSL = {
|
|
510
|
+
// product: jest.fn((config) => {
|
|
511
|
+
// productConfig = config;
|
|
512
|
+
// }),
|
|
513
|
+
// basicType: jest.fn(),
|
|
514
|
+
// labelGroup: jest.fn(),
|
|
515
|
+
// setGroupDefaultType: jest.fn(),
|
|
516
|
+
// weightGroup: jest.fn(),
|
|
517
|
+
// virtualType: jest.fn(),
|
|
518
|
+
// configureType: jest.fn(),
|
|
519
|
+
// weightType: jest.fn(),
|
|
520
|
+
// };
|
|
521
|
+
|
|
522
|
+
// (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
523
|
+
|
|
524
|
+
// const productMetadata: ProductMetadata = {
|
|
525
|
+
// name: 'test-group-default',
|
|
526
|
+
// label: 'Test Group Default',
|
|
527
|
+
// };
|
|
528
|
+
// const config: ProductChildGroup[] = [
|
|
529
|
+
// {
|
|
530
|
+
// name: 'main-group',
|
|
531
|
+
// label: 'Main Group',
|
|
532
|
+
// sideMenu: {
|
|
533
|
+
// children: [
|
|
534
|
+
// {
|
|
535
|
+
// name: 'first-child',
|
|
536
|
+
// label: 'First Child',
|
|
537
|
+
// component: { name: 'FirstChildComponent' },
|
|
538
|
+
// },
|
|
539
|
+
// {
|
|
540
|
+
// name: 'second-child',
|
|
541
|
+
// label: 'Second Child',
|
|
542
|
+
// component: { name: 'SecondChildComponent' },
|
|
543
|
+
// },
|
|
544
|
+
// ],
|
|
545
|
+
// },
|
|
546
|
+
// },
|
|
547
|
+
// ];
|
|
548
|
+
|
|
549
|
+
// const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
550
|
+
|
|
551
|
+
// pluginProduct.apply(mockPlugin, mockStore);
|
|
552
|
+
|
|
553
|
+
// // Verify default route points to first child of the group
|
|
554
|
+
// expect(productConfig).toBeDefined();
|
|
555
|
+
// expect(productConfig.to).toBeDefined();
|
|
556
|
+
// });
|
|
557
|
+
// });
|
|
558
|
+
|
|
559
|
+
// describe('comprehensive ordering scenario', () => {
|
|
560
|
+
// it('should maintain complex menu structure with proper ordering of mixed items', () => {
|
|
561
|
+
// const mockPlugin = createMockPlugin();
|
|
562
|
+
// const mockStore = createMockStore();
|
|
563
|
+
// const dslCallOrder: string[] = [];
|
|
564
|
+
// const weightTypeCalls: any[] = [];
|
|
565
|
+
|
|
566
|
+
// const mockDSL = {
|
|
567
|
+
// product: jest.fn(),
|
|
568
|
+
// basicType: jest.fn((...args) => dslCallOrder.push(`basicType`)),
|
|
569
|
+
// labelGroup: jest.fn((...args) => dslCallOrder.push(`labelGroup:${ args[0] }`)),
|
|
570
|
+
// setGroupDefaultType: jest.fn(),
|
|
571
|
+
// weightGroup: jest.fn((...args) => dslCallOrder.push(`weightGroup:${ args[0] }`)),
|
|
572
|
+
// virtualType: jest.fn((...args) => dslCallOrder.push(`virtualType:${ args[0].name }`)),
|
|
573
|
+
// configureType: jest.fn((...args) => dslCallOrder.push(`configureType:${ args[0] }`)),
|
|
574
|
+
// weightType: jest.fn((...args) => {
|
|
575
|
+
// weightTypeCalls.push(args);
|
|
576
|
+
// dslCallOrder.push('weightType');
|
|
577
|
+
// }),
|
|
578
|
+
// };
|
|
579
|
+
|
|
580
|
+
// (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
581
|
+
|
|
582
|
+
// const productMetadata: ProductMetadata = {
|
|
583
|
+
// name: 'complex-product',
|
|
584
|
+
// label: 'Complex Product',
|
|
585
|
+
// };
|
|
586
|
+
// const config: (ProductChildGroup | ProductChildPage)[] = [
|
|
587
|
+
// { type: 'fleet.cattle.io.clustergroup', sideMenu: { weight: 1 } }, // Resource type first
|
|
588
|
+
// {
|
|
589
|
+
// name: 'overview',
|
|
590
|
+
// label: 'Overview',
|
|
591
|
+
// component: { name: 'OverviewComponent' },
|
|
592
|
+
// sideMenu: { weight: 2 },
|
|
593
|
+
// },
|
|
594
|
+
// {
|
|
595
|
+
// name: 'settings',
|
|
596
|
+
// label: 'Settings',
|
|
597
|
+
// sideMenu: {
|
|
598
|
+
// weight: 50,
|
|
599
|
+
// children: [
|
|
600
|
+
// {
|
|
601
|
+
// name: 'general',
|
|
602
|
+
// label: 'General',
|
|
603
|
+
// component: { name: 'GeneralComponent' },
|
|
604
|
+
// },
|
|
605
|
+
// {
|
|
606
|
+
// name: 'advanced',
|
|
607
|
+
// label: 'Advanced',
|
|
608
|
+
// component: { name: 'AdvancedComponent' },
|
|
609
|
+
// },
|
|
610
|
+
// ],
|
|
611
|
+
// },
|
|
612
|
+
// },
|
|
613
|
+
// { type: 'upgrade.cattle.io.plan', sideMenu: { weight: 3 } }, // Another resource type
|
|
614
|
+
// {
|
|
615
|
+
// name: 'monitoring',
|
|
616
|
+
// label: 'Monitoring',
|
|
617
|
+
// component: { name: 'MonitoringComponent' },
|
|
618
|
+
// sideMenu: { weight: 100 },
|
|
619
|
+
// },
|
|
620
|
+
// ];
|
|
621
|
+
|
|
622
|
+
// const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
623
|
+
|
|
624
|
+
// pluginProduct.apply(mockPlugin, mockStore);
|
|
625
|
+
|
|
626
|
+
// // Verify configureTypes were registered
|
|
627
|
+
// const configureTypeCalls = dslCallOrder.filter((call) => call.startsWith('configureType:'));
|
|
628
|
+
|
|
629
|
+
// expect(configureTypeCalls).toContain('configureType:fleet.cattle.io.clustergroup');
|
|
630
|
+
// expect(configureTypeCalls).toContain('configureType:upgrade.cattle.io.plan');
|
|
631
|
+
|
|
632
|
+
// // Verify virtualTypes were registered
|
|
633
|
+
// const virtualTypeCalls = dslCallOrder.filter((call) => call.startsWith('virtualType:'));
|
|
634
|
+
|
|
635
|
+
// expect(virtualTypeCalls).toContain('virtualType:complexproduct-overview');
|
|
636
|
+
// expect(virtualTypeCalls).toContain('virtualType:complexproduct-monitoring');
|
|
637
|
+
// expect(virtualTypeCalls).toContain('virtualType:complexproduct-settings-general');
|
|
638
|
+
// expect(virtualTypeCalls).toContain('virtualType:complexproduct-settings-advanced');
|
|
639
|
+
|
|
640
|
+
// // Verify group was configured
|
|
641
|
+
// const labelGroupCalls = dslCallOrder.filter((call) => call.startsWith('labelGroup:'));
|
|
642
|
+
|
|
643
|
+
// expect(labelGroupCalls).toContain('labelGroup:complexproduct-settings');
|
|
644
|
+
// });
|
|
645
|
+
// });
|
|
646
|
+
// });
|
|
647
|
+
|
|
648
|
+
describe('sideNav menu structure rendering', () => {
|
|
649
|
+
describe('new product - menu structure validation', () => {
|
|
650
|
+
it('should create correct menu structure with proper ordering and names for simple virtualTypes', () => {
|
|
651
|
+
const mockPlugin = createMockPlugin();
|
|
652
|
+
const mockStore = createMockStore();
|
|
653
|
+
|
|
654
|
+
// Capture the menu structure that would be created
|
|
655
|
+
const menuStructure: any = {
|
|
656
|
+
basicTypes: [],
|
|
657
|
+
virtualTypes: [],
|
|
658
|
+
groupLabels: {},
|
|
659
|
+
groupWeights: {},
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
const mockDSL = {
|
|
663
|
+
product: jest.fn(),
|
|
664
|
+
basicType: jest.fn((types, group) => {
|
|
665
|
+
menuStructure.basicTypes.push({ types, group });
|
|
666
|
+
}),
|
|
667
|
+
labelGroup: jest.fn((group, label, labelKey) => {
|
|
668
|
+
menuStructure.groupLabels[group] = { label, labelKey };
|
|
669
|
+
}),
|
|
670
|
+
setGroupDefaultType: jest.fn(),
|
|
671
|
+
weightGroup: jest.fn((group, weight) => {
|
|
672
|
+
menuStructure.groupWeights[group] = weight;
|
|
673
|
+
}),
|
|
674
|
+
virtualType: jest.fn((config) => {
|
|
675
|
+
menuStructure.virtualTypes.push({
|
|
676
|
+
name: config.name,
|
|
677
|
+
label: config.label,
|
|
678
|
+
weight: config.weight,
|
|
679
|
+
});
|
|
680
|
+
}),
|
|
681
|
+
configureType: jest.fn(),
|
|
682
|
+
weightType: jest.fn(),
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
686
|
+
|
|
687
|
+
const productMetadata: ProductMetadata = {
|
|
688
|
+
name: 'my-product',
|
|
689
|
+
label: 'My Product',
|
|
690
|
+
};
|
|
691
|
+
const config: ProductChildPage[] = [
|
|
692
|
+
{
|
|
693
|
+
name: 'overview',
|
|
694
|
+
label: 'Overview',
|
|
695
|
+
component: { name: 'OverviewComponent' },
|
|
696
|
+
sideMenu: { weight: 100 },
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
name: 'settings',
|
|
700
|
+
label: 'Settings',
|
|
701
|
+
component: { name: 'SettingsComponent' },
|
|
702
|
+
sideMenu: { weight: 50 },
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
name: 'monitoring',
|
|
706
|
+
label: 'Monitoring',
|
|
707
|
+
component: { name: 'MonitoringComponent' },
|
|
708
|
+
sideMenu: { weight: 25 },
|
|
709
|
+
},
|
|
710
|
+
];
|
|
711
|
+
|
|
712
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
713
|
+
|
|
714
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
715
|
+
|
|
716
|
+
// Verify menu structure ordering (virtualTypes are registered in config order but with weights)
|
|
717
|
+
expect(menuStructure.virtualTypes).toHaveLength(3);
|
|
718
|
+
|
|
719
|
+
// Menu items should have concatenated product-page names
|
|
720
|
+
expect(menuStructure.virtualTypes[0].name).toBe('myproduct-overview');
|
|
721
|
+
expect(menuStructure.virtualTypes[0].label).toBe('Overview');
|
|
722
|
+
expect(menuStructure.virtualTypes[0].weight).toBe(100);
|
|
723
|
+
|
|
724
|
+
expect(menuStructure.virtualTypes[1].name).toBe('myproduct-settings');
|
|
725
|
+
expect(menuStructure.virtualTypes[1].label).toBe('Settings');
|
|
726
|
+
expect(menuStructure.virtualTypes[1].weight).toBe(50);
|
|
727
|
+
|
|
728
|
+
expect(menuStructure.virtualTypes[2].name).toBe('myproduct-monitoring');
|
|
729
|
+
expect(menuStructure.virtualTypes[2].label).toBe('Monitoring');
|
|
730
|
+
expect(menuStructure.virtualTypes[2].weight).toBe(25);
|
|
731
|
+
|
|
732
|
+
// Verify basicType creates flat navigation structure (no groups for simple pages)
|
|
733
|
+
expect(menuStructure.basicTypes.length).toBeGreaterThan(0);
|
|
734
|
+
expect(menuStructure.basicTypes[0].types).toStrictEqual(expect.arrayContaining([
|
|
735
|
+
'myproduct-overview',
|
|
736
|
+
'myproduct-settings',
|
|
737
|
+
'myproduct-monitoring',
|
|
738
|
+
]));
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
it('should create correct menu structure with groups and nested items', () => {
|
|
742
|
+
const mockPlugin = createMockPlugin();
|
|
743
|
+
const mockStore = createMockStore();
|
|
744
|
+
|
|
745
|
+
const menuStructure: any = {
|
|
746
|
+
basicTypes: [],
|
|
747
|
+
virtualTypes: [],
|
|
748
|
+
groupLabels: {},
|
|
749
|
+
groupWeights: {},
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
const mockDSL = {
|
|
753
|
+
product: jest.fn(),
|
|
754
|
+
basicType: jest.fn((types, group) => {
|
|
755
|
+
menuStructure.basicTypes.push({ types, group });
|
|
756
|
+
}),
|
|
757
|
+
labelGroup: jest.fn((group, label, labelKey) => {
|
|
758
|
+
menuStructure.groupLabels[group] = { label, labelKey };
|
|
759
|
+
}),
|
|
760
|
+
setGroupDefaultType: jest.fn(),
|
|
761
|
+
weightGroup: jest.fn((group, weight) => {
|
|
762
|
+
menuStructure.groupWeights[group] = weight;
|
|
763
|
+
}),
|
|
764
|
+
virtualType: jest.fn((config) => {
|
|
765
|
+
menuStructure.virtualTypes.push(config);
|
|
766
|
+
}),
|
|
767
|
+
configureType: jest.fn(),
|
|
768
|
+
weightType: jest.fn(),
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
772
|
+
|
|
773
|
+
const productMetadata: ProductMetadata = {
|
|
774
|
+
name: 'grouped-product',
|
|
775
|
+
label: 'Grouped Product',
|
|
776
|
+
};
|
|
777
|
+
const config: (ProductChildPage | ProductChildGroup)[] = [
|
|
778
|
+
{
|
|
779
|
+
name: 'overview',
|
|
780
|
+
label: 'Overview',
|
|
781
|
+
component: { name: 'OverviewComponent' },
|
|
782
|
+
sideMenu: { weight: 100 },
|
|
783
|
+
},
|
|
784
|
+
{
|
|
785
|
+
name: 'admin',
|
|
786
|
+
label: 'Administration',
|
|
787
|
+
sideMenu: {
|
|
788
|
+
weight: 50,
|
|
789
|
+
children: [
|
|
790
|
+
{
|
|
791
|
+
name: 'users',
|
|
792
|
+
label: 'Users',
|
|
793
|
+
component: { name: 'UsersComponent' },
|
|
794
|
+
},
|
|
795
|
+
{
|
|
796
|
+
name: 'roles',
|
|
797
|
+
label: 'Roles',
|
|
798
|
+
component: { name: 'RolesComponent' },
|
|
799
|
+
},
|
|
800
|
+
],
|
|
801
|
+
},
|
|
802
|
+
},
|
|
803
|
+
{
|
|
804
|
+
name: 'reports',
|
|
805
|
+
label: 'Reports',
|
|
806
|
+
component: { name: 'ReportsComponent' },
|
|
807
|
+
sideMenu: { weight: 25 },
|
|
808
|
+
},
|
|
809
|
+
];
|
|
810
|
+
|
|
811
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
812
|
+
|
|
813
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
814
|
+
|
|
815
|
+
// Verify group structure
|
|
816
|
+
expect(menuStructure.groupLabels['groupedproduct-admin']).toStrictEqual({
|
|
817
|
+
label: 'Administration',
|
|
818
|
+
labelKey: undefined,
|
|
819
|
+
});
|
|
820
|
+
expect(menuStructure.groupWeights['groupedproduct-admin']).toBe(50);
|
|
821
|
+
|
|
822
|
+
// Verify nested items in group have correct names
|
|
823
|
+
const adminGroupItems = menuStructure.virtualTypes.filter((vt: any) => vt.name.includes('admin')
|
|
824
|
+
);
|
|
825
|
+
|
|
826
|
+
expect(adminGroupItems).toHaveLength(2);
|
|
827
|
+
expect(adminGroupItems.some((item: any) => item.name === 'groupedproduct-admin-users')).toBe(true);
|
|
828
|
+
expect(adminGroupItems.some((item: any) => item.name === 'groupedproduct-admin-roles')).toBe(true);
|
|
829
|
+
|
|
830
|
+
// Verify top-level items ordering via basicType
|
|
831
|
+
const topLevelCall = menuStructure.basicTypes.find((bt: any) => !bt.group);
|
|
832
|
+
|
|
833
|
+
expect(topLevelCall).toBeDefined();
|
|
834
|
+
expect(topLevelCall.types).toContain('groupedproduct-overview');
|
|
835
|
+
expect(topLevelCall.types).toContain('groupedproduct-reports');
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
it('should render mixed virtualTypes and configureTypes in correct order', () => {
|
|
839
|
+
const mockPlugin = createMockPlugin();
|
|
840
|
+
const mockStore = createMockStore();
|
|
841
|
+
|
|
842
|
+
const menuStructure: any = {
|
|
843
|
+
basicTypes: [],
|
|
844
|
+
virtualTypes: [],
|
|
845
|
+
configureTypes: [],
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
const mockDSL = {
|
|
849
|
+
product: jest.fn(),
|
|
850
|
+
basicType: jest.fn((types, group) => {
|
|
851
|
+
menuStructure.basicTypes.push({ types, group });
|
|
852
|
+
}),
|
|
853
|
+
labelGroup: jest.fn(),
|
|
854
|
+
setGroupDefaultType: jest.fn(),
|
|
855
|
+
weightGroup: jest.fn(),
|
|
856
|
+
virtualType: jest.fn((config) => {
|
|
857
|
+
menuStructure.virtualTypes.push({ name: config.name, weight: config.weight });
|
|
858
|
+
}),
|
|
859
|
+
configureType: jest.fn((type, config) => {
|
|
860
|
+
menuStructure.configureTypes.push({
|
|
861
|
+
type, config, weight: config.sideMenu?.weight
|
|
862
|
+
});
|
|
863
|
+
}),
|
|
864
|
+
weightType: jest.fn((type, weight) => {
|
|
865
|
+
// Find existing configureType and add weight
|
|
866
|
+
const existing = menuStructure.configureTypes.find((ct: any) => ct.type === type);
|
|
867
|
+
|
|
868
|
+
if (existing) {
|
|
869
|
+
existing.weight = weight;
|
|
870
|
+
}
|
|
871
|
+
}),
|
|
872
|
+
};
|
|
873
|
+
|
|
874
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
875
|
+
|
|
876
|
+
const productMetadata: ProductMetadata = {
|
|
877
|
+
name: 'mixed-product',
|
|
878
|
+
label: 'Mixed Product',
|
|
879
|
+
};
|
|
880
|
+
const config: ProductChildPage[] = [
|
|
881
|
+
{ type: 'fleet.cattle.io.clustergroup', sideMenu: { weight: 100 } },
|
|
882
|
+
{
|
|
883
|
+
name: 'overview',
|
|
884
|
+
label: 'Overview',
|
|
885
|
+
component: { name: 'OverviewComponent' },
|
|
886
|
+
sideMenu: { weight: 75 },
|
|
887
|
+
},
|
|
888
|
+
{ type: 'workload.io.deployment', sideMenu: { weight: 50 } },
|
|
889
|
+
{
|
|
890
|
+
name: 'settings',
|
|
891
|
+
label: 'Settings',
|
|
892
|
+
component: { name: 'SettingsComponent' },
|
|
893
|
+
sideMenu: { weight: 25 },
|
|
894
|
+
},
|
|
895
|
+
];
|
|
896
|
+
|
|
897
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
898
|
+
|
|
899
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
900
|
+
|
|
901
|
+
// In SideNav, items are sorted by weight descending
|
|
902
|
+
// Verify that the menu structure reflects the intended ordering
|
|
903
|
+
|
|
904
|
+
// ConfigureTypes (resources)
|
|
905
|
+
expect(menuStructure.configureTypes[0]).toMatchObject({
|
|
906
|
+
type: 'fleet.cattle.io.clustergroup',
|
|
907
|
+
weight: 100,
|
|
908
|
+
});
|
|
909
|
+
expect(menuStructure.configureTypes[1]).toMatchObject({
|
|
910
|
+
type: 'workload.io.deployment',
|
|
911
|
+
weight: 50,
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
// VirtualTypes (custom pages)
|
|
915
|
+
expect(menuStructure.virtualTypes[0]).toMatchObject({
|
|
916
|
+
name: 'mixedproduct-overview',
|
|
917
|
+
weight: 75,
|
|
918
|
+
});
|
|
919
|
+
expect(menuStructure.virtualTypes[1]).toMatchObject({
|
|
920
|
+
name: 'mixedproduct-settings',
|
|
921
|
+
weight: 25,
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
// Verify basicType call includes all items for navigation
|
|
925
|
+
const allItems = menuStructure.basicTypes[0].types;
|
|
926
|
+
|
|
927
|
+
expect(allItems).toContain('fleet.cattle.io.clustergroup');
|
|
928
|
+
expect(allItems).toContain('mixedproduct-overview');
|
|
929
|
+
expect(allItems).toContain('workload.io.deployment');
|
|
930
|
+
expect(allItems).toContain('mixedproduct-settings');
|
|
931
|
+
});
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
describe('extending product - menu structure validation', () => {
|
|
935
|
+
it('should add items to existing product menu with correct naming', () => {
|
|
936
|
+
const mockPlugin = createMockPlugin();
|
|
937
|
+
const mockStore = createMockStore();
|
|
938
|
+
|
|
939
|
+
const menuStructure: any = {
|
|
940
|
+
basicTypes: [],
|
|
941
|
+
virtualTypes: [],
|
|
942
|
+
};
|
|
943
|
+
|
|
944
|
+
const mockDSL = {
|
|
945
|
+
product: jest.fn(),
|
|
946
|
+
basicType: jest.fn((types, group) => {
|
|
947
|
+
menuStructure.basicTypes.push({ types, group });
|
|
948
|
+
}),
|
|
949
|
+
labelGroup: jest.fn(),
|
|
950
|
+
setGroupDefaultType: jest.fn(),
|
|
951
|
+
weightGroup: jest.fn(),
|
|
952
|
+
virtualType: jest.fn((config) => {
|
|
953
|
+
menuStructure.virtualTypes.push({
|
|
954
|
+
name: config.name,
|
|
955
|
+
label: config.label,
|
|
956
|
+
weight: config.weight,
|
|
957
|
+
});
|
|
958
|
+
}),
|
|
959
|
+
configureType: jest.fn(),
|
|
960
|
+
weightType: jest.fn(),
|
|
961
|
+
};
|
|
962
|
+
|
|
963
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
964
|
+
|
|
965
|
+
const validStandardProduct = StandardProductNames.EXPLORER;
|
|
966
|
+
const config: ProductChildPage[] = [
|
|
967
|
+
{
|
|
968
|
+
name: 'my-custom-page',
|
|
969
|
+
label: 'My Custom Page',
|
|
970
|
+
component: { name: 'CustomComponent' },
|
|
971
|
+
sideMenu: { weight: 99 },
|
|
972
|
+
},
|
|
973
|
+
{
|
|
974
|
+
name: 'another-page',
|
|
975
|
+
label: 'Another Page',
|
|
976
|
+
component: { name: 'AnotherComponent' },
|
|
977
|
+
sideMenu: { weight: 98 },
|
|
978
|
+
},
|
|
979
|
+
];
|
|
980
|
+
|
|
981
|
+
const pluginProduct = new PluginProduct(mockPlugin, validStandardProduct, config);
|
|
982
|
+
|
|
983
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
984
|
+
|
|
985
|
+
// When extending EXPLORER, names are prefixed with product name
|
|
986
|
+
expect(menuStructure.virtualTypes[0]).toMatchObject({
|
|
987
|
+
name: 'explorer-my-custom-page',
|
|
988
|
+
label: 'My Custom Page',
|
|
989
|
+
weight: 99,
|
|
990
|
+
});
|
|
991
|
+
expect(menuStructure.virtualTypes[1]).toMatchObject({
|
|
992
|
+
name: 'explorer-another-page',
|
|
993
|
+
label: 'Another Page',
|
|
994
|
+
weight: 98,
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
// These should be added to EXPLORER's existing navigation
|
|
998
|
+
const explorerItems = menuStructure.basicTypes[0].types;
|
|
999
|
+
|
|
1000
|
+
expect(explorerItems).toContain('explorer-my-custom-page');
|
|
1001
|
+
expect(explorerItems).toContain('explorer-another-page');
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
it('should add groups to existing product with correct hierarchy', () => {
|
|
1005
|
+
const mockPlugin = createMockPlugin();
|
|
1006
|
+
const mockStore = createMockStore();
|
|
1007
|
+
|
|
1008
|
+
const menuStructure: any = {
|
|
1009
|
+
basicTypes: [],
|
|
1010
|
+
virtualTypes: [],
|
|
1011
|
+
groupLabels: {},
|
|
1012
|
+
groupWeights: {},
|
|
1013
|
+
};
|
|
1014
|
+
|
|
1015
|
+
const mockDSL = {
|
|
1016
|
+
product: jest.fn(),
|
|
1017
|
+
basicType: jest.fn((types, group) => {
|
|
1018
|
+
menuStructure.basicTypes.push({ types, group });
|
|
1019
|
+
}),
|
|
1020
|
+
labelGroup: jest.fn((group, label, labelKey) => {
|
|
1021
|
+
menuStructure.groupLabels[group] = { label, labelKey };
|
|
1022
|
+
}),
|
|
1023
|
+
setGroupDefaultType: jest.fn(),
|
|
1024
|
+
weightGroup: jest.fn((group, weight) => {
|
|
1025
|
+
menuStructure.groupWeights[group] = weight;
|
|
1026
|
+
}),
|
|
1027
|
+
virtualType: jest.fn((config) => {
|
|
1028
|
+
menuStructure.virtualTypes.push({
|
|
1029
|
+
name: config.name,
|
|
1030
|
+
label: config.label,
|
|
1031
|
+
weight: config.sideMenu?.weight,
|
|
1032
|
+
});
|
|
1033
|
+
}),
|
|
1034
|
+
configureType: jest.fn(),
|
|
1035
|
+
weightType: jest.fn(),
|
|
1036
|
+
};
|
|
1037
|
+
|
|
1038
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
1039
|
+
|
|
1040
|
+
const validStandardProduct = StandardProductNames.SETTINGS;
|
|
1041
|
+
const config: ProductChildGroup[] = [
|
|
1042
|
+
{
|
|
1043
|
+
name: 'extensions',
|
|
1044
|
+
label: 'Extensions',
|
|
1045
|
+
sideMenu: {
|
|
1046
|
+
weight: 80,
|
|
1047
|
+
children: [
|
|
1048
|
+
{
|
|
1049
|
+
name: 'marketplace',
|
|
1050
|
+
label: 'Marketplace',
|
|
1051
|
+
component: { name: 'MarketplaceComponent' },
|
|
1052
|
+
},
|
|
1053
|
+
{
|
|
1054
|
+
name: 'installed',
|
|
1055
|
+
label: 'Installed',
|
|
1056
|
+
component: { name: 'InstalledComponent' },
|
|
1057
|
+
},
|
|
1058
|
+
],
|
|
1059
|
+
},
|
|
1060
|
+
},
|
|
1061
|
+
];
|
|
1062
|
+
|
|
1063
|
+
const pluginProduct = new PluginProduct(mockPlugin, validStandardProduct, config);
|
|
1064
|
+
|
|
1065
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
1066
|
+
|
|
1067
|
+
// Verify group is added with correct naming (settings-extensions)
|
|
1068
|
+
expect(menuStructure.groupLabels['settings-extensions']).toStrictEqual({
|
|
1069
|
+
label: 'Extensions',
|
|
1070
|
+
labelKey: undefined,
|
|
1071
|
+
});
|
|
1072
|
+
expect(menuStructure.groupWeights['settings-extensions']).toBe(80);
|
|
1073
|
+
|
|
1074
|
+
// Verify child items have correct hierarchy in their names
|
|
1075
|
+
const extensionItems = menuStructure.virtualTypes.filter((vt: any) => vt.name.includes('extensions')
|
|
1076
|
+
);
|
|
1077
|
+
|
|
1078
|
+
expect(extensionItems).toHaveLength(2);
|
|
1079
|
+
expect(extensionItems.some((item: any) => item.name === 'settings-extensions-marketplace')).toBe(true);
|
|
1080
|
+
expect(extensionItems.some((item: any) => item.name === 'settings-extensions-installed')).toBe(true);
|
|
1081
|
+
|
|
1082
|
+
// Verify group navigation structure
|
|
1083
|
+
const groupNavCall = menuStructure.basicTypes.find((bt: any) => bt.group === 'settings-extensions'
|
|
1084
|
+
);
|
|
1085
|
+
|
|
1086
|
+
expect(groupNavCall).toBeDefined();
|
|
1087
|
+
expect(groupNavCall.types).toStrictEqual(expect.arrayContaining([
|
|
1088
|
+
'settings-extensions-marketplace',
|
|
1089
|
+
'settings-extensions-installed',
|
|
1090
|
+
'settings-extensions', // Group itself is also in the nav
|
|
1091
|
+
]));
|
|
1092
|
+
});
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
describe('comprehensive menu rendering scenario', () => {
|
|
1096
|
+
it('should create complete menu structure matching SideNav expectations', () => {
|
|
1097
|
+
const mockPlugin = createMockPlugin();
|
|
1098
|
+
const mockStore = createMockStore();
|
|
1099
|
+
|
|
1100
|
+
// Simulate the complete menu structure as SideNav would build it
|
|
1101
|
+
const menuStructure: any = {
|
|
1102
|
+
groups: [],
|
|
1103
|
+
items: [],
|
|
1104
|
+
};
|
|
1105
|
+
|
|
1106
|
+
const mockDSL = {
|
|
1107
|
+
product: jest.fn(),
|
|
1108
|
+
basicType: jest.fn((types, group) => {
|
|
1109
|
+
if (group) {
|
|
1110
|
+
// This is a group with children
|
|
1111
|
+
const existingGroup = menuStructure.groups.find((g: any) => g.name === group);
|
|
1112
|
+
|
|
1113
|
+
if (existingGroup) {
|
|
1114
|
+
existingGroup.children = types.filter((t: string) => t !== group);
|
|
1115
|
+
} else {
|
|
1116
|
+
menuStructure.groups.push({
|
|
1117
|
+
name: group,
|
|
1118
|
+
children: types.filter((t: string) => t !== group),
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
}),
|
|
1123
|
+
labelGroup: jest.fn((group, label, labelKey) => {
|
|
1124
|
+
const existingGroup = menuStructure.groups.find((g: any) => g.name === group);
|
|
1125
|
+
|
|
1126
|
+
if (existingGroup) {
|
|
1127
|
+
existingGroup.label = label;
|
|
1128
|
+
existingGroup.labelKey = labelKey;
|
|
1129
|
+
} else {
|
|
1130
|
+
menuStructure.groups.push({
|
|
1131
|
+
name: group,
|
|
1132
|
+
label,
|
|
1133
|
+
labelKey,
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
}),
|
|
1137
|
+
setGroupDefaultType: jest.fn(),
|
|
1138
|
+
weightGroup: jest.fn((group, weight) => {
|
|
1139
|
+
const existingGroup = menuStructure.groups.find((g: any) => g.name === group);
|
|
1140
|
+
|
|
1141
|
+
if (existingGroup) {
|
|
1142
|
+
existingGroup.weight = weight;
|
|
1143
|
+
}
|
|
1144
|
+
}),
|
|
1145
|
+
virtualType: jest.fn((config) => {
|
|
1146
|
+
menuStructure.items.push({
|
|
1147
|
+
name: config.name,
|
|
1148
|
+
label: config.label,
|
|
1149
|
+
weight: config.weight,
|
|
1150
|
+
type: 'virtual',
|
|
1151
|
+
});
|
|
1152
|
+
}),
|
|
1153
|
+
configureType: jest.fn((type) => {
|
|
1154
|
+
menuStructure.items.push({
|
|
1155
|
+
name: type,
|
|
1156
|
+
type: 'configure',
|
|
1157
|
+
weight: 0, // Will be set by weightType if provided
|
|
1158
|
+
});
|
|
1159
|
+
}),
|
|
1160
|
+
weightType: jest.fn((type, weight) => {
|
|
1161
|
+
const item = menuStructure.items.find((i: any) => i.name === type);
|
|
1162
|
+
|
|
1163
|
+
if (item) {
|
|
1164
|
+
item.weight = weight;
|
|
1165
|
+
}
|
|
1166
|
+
}),
|
|
1167
|
+
};
|
|
1168
|
+
|
|
1169
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
1170
|
+
|
|
1171
|
+
const productMetadata: ProductMetadata = {
|
|
1172
|
+
name: 'complete-app',
|
|
1173
|
+
label: 'Complete App',
|
|
1174
|
+
};
|
|
1175
|
+
const config: (ProductChildGroup | ProductChildPage)[] = [
|
|
1176
|
+
{
|
|
1177
|
+
name: 'dashboard',
|
|
1178
|
+
label: 'Dashboard',
|
|
1179
|
+
component: { name: 'DashboardComponent' },
|
|
1180
|
+
sideMenu: { weight: 100 },
|
|
1181
|
+
},
|
|
1182
|
+
{
|
|
1183
|
+
name: 'workloads',
|
|
1184
|
+
label: 'Workloads',
|
|
1185
|
+
sideMenu: {
|
|
1186
|
+
weight: 90,
|
|
1187
|
+
children: [
|
|
1188
|
+
{ type: 'workload.io.deployment', sideMenu: { weight: 50 } },
|
|
1189
|
+
{ type: 'workload.io.pod', sideMenu: { weight: 45 } },
|
|
1190
|
+
{
|
|
1191
|
+
name: 'jobs',
|
|
1192
|
+
label: 'Jobs',
|
|
1193
|
+
component: { name: 'JobsComponent' },
|
|
1194
|
+
},
|
|
1195
|
+
],
|
|
1196
|
+
},
|
|
1197
|
+
},
|
|
1198
|
+
{ type: 'config.io.configmap', sideMenu: { weight: 80 } },
|
|
1199
|
+
{
|
|
1200
|
+
name: 'settings',
|
|
1201
|
+
label: 'Settings',
|
|
1202
|
+
sideMenu: {
|
|
1203
|
+
weight: 70,
|
|
1204
|
+
children: [
|
|
1205
|
+
{
|
|
1206
|
+
name: 'general',
|
|
1207
|
+
label: 'General',
|
|
1208
|
+
component: { name: 'GeneralComponent' },
|
|
1209
|
+
},
|
|
1210
|
+
{
|
|
1211
|
+
name: 'advanced',
|
|
1212
|
+
label: 'Advanced',
|
|
1213
|
+
component: { name: 'AdvancedComponent' },
|
|
1214
|
+
},
|
|
1215
|
+
],
|
|
1216
|
+
},
|
|
1217
|
+
},
|
|
1218
|
+
];
|
|
1219
|
+
|
|
1220
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
1221
|
+
|
|
1222
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
1223
|
+
|
|
1224
|
+
// Verify groups were created with correct structure
|
|
1225
|
+
const workloadsGroup = menuStructure.groups.find((g: any) => g.name === 'completeapp-workloads');
|
|
1226
|
+
|
|
1227
|
+
expect(workloadsGroup).toBeDefined();
|
|
1228
|
+
expect(workloadsGroup.label).toBe('Workloads');
|
|
1229
|
+
expect(workloadsGroup.weight).toBe(90);
|
|
1230
|
+
expect(workloadsGroup.children).toStrictEqual(expect.arrayContaining([
|
|
1231
|
+
'workload.io.deployment',
|
|
1232
|
+
'workload.io.pod',
|
|
1233
|
+
'completeapp-workloads-jobs',
|
|
1234
|
+
]));
|
|
1235
|
+
|
|
1236
|
+
const settingsGroup = menuStructure.groups.find((g: any) => g.name === 'completeapp-settings');
|
|
1237
|
+
|
|
1238
|
+
expect(settingsGroup).toBeDefined();
|
|
1239
|
+
expect(settingsGroup.label).toBe('Settings');
|
|
1240
|
+
expect(settingsGroup.weight).toBe(70);
|
|
1241
|
+
expect(settingsGroup.children).toStrictEqual(expect.arrayContaining([
|
|
1242
|
+
'completeapp-settings-general',
|
|
1243
|
+
'completeapp-settings-advanced',
|
|
1244
|
+
]));
|
|
1245
|
+
|
|
1246
|
+
// Verify all menu items were created with correct properties
|
|
1247
|
+
const virtualItems = menuStructure.items.filter((i: any) => i.type === 'virtual');
|
|
1248
|
+
const configureItems = menuStructure.items.filter((i: any) => i.type === 'configure');
|
|
1249
|
+
|
|
1250
|
+
// Check dashboard (top-level virtual type)
|
|
1251
|
+
const dashboardItem = virtualItems.find((i: any) => i.name === 'completeapp-dashboard');
|
|
1252
|
+
|
|
1253
|
+
expect(dashboardItem).toBeDefined();
|
|
1254
|
+
expect(dashboardItem.label).toBe('Dashboard');
|
|
1255
|
+
expect(dashboardItem.weight).toBe(100);
|
|
1256
|
+
|
|
1257
|
+
// Check configmap (top-level configure type)
|
|
1258
|
+
const configMapItem = configureItems.find((i: any) => i.name === 'config.io.configmap');
|
|
1259
|
+
|
|
1260
|
+
expect(configMapItem).toBeDefined();
|
|
1261
|
+
expect(configMapItem.weight).toBe(80);
|
|
1262
|
+
|
|
1263
|
+
// Verify group items
|
|
1264
|
+
const jobsItem = virtualItems.find((i: any) => i.name === 'completeapp-workloads-jobs');
|
|
1265
|
+
|
|
1266
|
+
expect(jobsItem).toBeDefined();
|
|
1267
|
+
expect(jobsItem.label).toBe('Jobs');
|
|
1268
|
+
|
|
1269
|
+
const generalItem = virtualItems.find((i: any) => i.name === 'completeapp-settings-general');
|
|
1270
|
+
|
|
1271
|
+
expect(generalItem).toBeDefined();
|
|
1272
|
+
expect(generalItem.label).toBe('General');
|
|
1273
|
+
|
|
1274
|
+
const advancedItem = virtualItems.find((i: any) => i.name === 'completeapp-settings-advanced');
|
|
1275
|
+
|
|
1276
|
+
expect(advancedItem).toBeDefined();
|
|
1277
|
+
expect(advancedItem.label).toBe('Advanced');
|
|
1278
|
+
|
|
1279
|
+
// Verify resource types in workloads group
|
|
1280
|
+
const deploymentsItem = configureItems.find((i: any) => i.name === 'workload.io.deployment');
|
|
1281
|
+
|
|
1282
|
+
expect(deploymentsItem).toBeDefined();
|
|
1283
|
+
expect(deploymentsItem.weight).toBe(50);
|
|
1284
|
+
|
|
1285
|
+
const podsItem = configureItems.find((i: any) => i.name === 'workload.io.pod');
|
|
1286
|
+
|
|
1287
|
+
expect(podsItem).toBeDefined();
|
|
1288
|
+
expect(podsItem.weight).toBe(45);
|
|
1289
|
+
|
|
1290
|
+
// Verify total counts
|
|
1291
|
+
expect(virtualItems.length).toBeGreaterThanOrEqual(4); // dashboard, jobs, general, advanced
|
|
1292
|
+
expect(configureItems.length).toBeGreaterThanOrEqual(3); // configmap, deployment, pod
|
|
1293
|
+
expect(menuStructure.groups).toHaveLength(2); // workloads, settings
|
|
1294
|
+
});
|
|
1295
|
+
});
|
|
1296
|
+
|
|
1297
|
+
describe('deeply nested groups (groups within groups)', () => {
|
|
1298
|
+
it('should handle 2-level nested groups with correct hierarchical paths', () => {
|
|
1299
|
+
const mockPlugin = createMockPlugin();
|
|
1300
|
+
const mockStore = createMockStore();
|
|
1301
|
+
const basicTypeCalls: any[] = [];
|
|
1302
|
+
|
|
1303
|
+
const mockDSL = {
|
|
1304
|
+
product: jest.fn(),
|
|
1305
|
+
basicType: jest.fn((...args) => basicTypeCalls.push(args)),
|
|
1306
|
+
labelGroup: jest.fn(),
|
|
1307
|
+
setGroupDefaultType: jest.fn(),
|
|
1308
|
+
weightGroup: jest.fn(),
|
|
1309
|
+
virtualType: jest.fn(),
|
|
1310
|
+
configureType: jest.fn(),
|
|
1311
|
+
weightType: jest.fn(),
|
|
1312
|
+
};
|
|
1313
|
+
|
|
1314
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
1315
|
+
|
|
1316
|
+
const productMetadata: ProductMetadata = {
|
|
1317
|
+
name: 'nested-product',
|
|
1318
|
+
label: 'Nested Product',
|
|
1319
|
+
};
|
|
1320
|
+
const config: ProductChildGroup[] = [
|
|
1321
|
+
{
|
|
1322
|
+
name: 'root-group',
|
|
1323
|
+
label: 'Root Group',
|
|
1324
|
+
sideMenu: {
|
|
1325
|
+
weight: 100,
|
|
1326
|
+
children: [
|
|
1327
|
+
{
|
|
1328
|
+
name: 'page1',
|
|
1329
|
+
label: 'Page 1',
|
|
1330
|
+
component: { name: 'Page1Component' },
|
|
1331
|
+
},
|
|
1332
|
+
{
|
|
1333
|
+
name: 'nested-group',
|
|
1334
|
+
label: 'Nested Group',
|
|
1335
|
+
sideMenu: {
|
|
1336
|
+
weight: 50,
|
|
1337
|
+
children: [
|
|
1338
|
+
{
|
|
1339
|
+
name: 'nested-page1',
|
|
1340
|
+
label: 'Nested Page 1',
|
|
1341
|
+
component: { name: 'NestedPage1Component' },
|
|
1342
|
+
},
|
|
1343
|
+
{
|
|
1344
|
+
name: 'nested-page2',
|
|
1345
|
+
label: 'Nested Page 2',
|
|
1346
|
+
component: { name: 'NestedPage2Component' },
|
|
1347
|
+
},
|
|
1348
|
+
],
|
|
1349
|
+
},
|
|
1350
|
+
},
|
|
1351
|
+
{
|
|
1352
|
+
name: 'page2',
|
|
1353
|
+
label: 'Page 2',
|
|
1354
|
+
component: { name: 'Page2Component' },
|
|
1355
|
+
},
|
|
1356
|
+
],
|
|
1357
|
+
},
|
|
1358
|
+
},
|
|
1359
|
+
];
|
|
1360
|
+
|
|
1361
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
1362
|
+
|
|
1363
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
1364
|
+
|
|
1365
|
+
// Verify basicType calls include hierarchical paths with :: separators
|
|
1366
|
+
// First call should be for root group items
|
|
1367
|
+
const rootGroupCall = basicTypeCalls.find((call) => call[1] === 'nestedproduct-root-group');
|
|
1368
|
+
|
|
1369
|
+
expect(rootGroupCall).toBeDefined();
|
|
1370
|
+
expect(rootGroupCall[0]).toContain('nestedproduct-root-group-page1');
|
|
1371
|
+
expect(rootGroupCall[0]).toContain('nestedproduct-root-group-page2');
|
|
1372
|
+
expect(rootGroupCall[0]).toContain('nestedproduct-root-group-nested-group');
|
|
1373
|
+
expect(rootGroupCall[0]).toContain('nestedproduct-root-group'); // Root group itself
|
|
1374
|
+
|
|
1375
|
+
// Second call should be for nested group with hierarchical path
|
|
1376
|
+
const nestedGroupCall = basicTypeCalls.find((call) => call[1] === 'nestedproduct-root-group::nestedproduct-root-group-nested-group');
|
|
1377
|
+
|
|
1378
|
+
expect(nestedGroupCall).toBeDefined();
|
|
1379
|
+
expect(nestedGroupCall[0]).toContain('nestedproduct-root-group-nested-group-nested-page1');
|
|
1380
|
+
expect(nestedGroupCall[0]).toContain('nestedproduct-root-group-nested-group-nested-page2');
|
|
1381
|
+
});
|
|
1382
|
+
|
|
1383
|
+
it('should handle 3-level nested groups with correct hierarchical paths', () => {
|
|
1384
|
+
const mockPlugin = createMockPlugin();
|
|
1385
|
+
const mockStore = createMockStore();
|
|
1386
|
+
const basicTypeCalls: any[] = [];
|
|
1387
|
+
|
|
1388
|
+
const mockDSL = {
|
|
1389
|
+
product: jest.fn(),
|
|
1390
|
+
basicType: jest.fn((...args) => basicTypeCalls.push(args)),
|
|
1391
|
+
labelGroup: jest.fn(),
|
|
1392
|
+
setGroupDefaultType: jest.fn(),
|
|
1393
|
+
weightGroup: jest.fn(),
|
|
1394
|
+
virtualType: jest.fn(),
|
|
1395
|
+
configureType: jest.fn(),
|
|
1396
|
+
weightType: jest.fn(),
|
|
1397
|
+
};
|
|
1398
|
+
|
|
1399
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
1400
|
+
|
|
1401
|
+
const productMetadata: ProductMetadata = {
|
|
1402
|
+
name: 'deep-nested',
|
|
1403
|
+
label: 'Deep Nested',
|
|
1404
|
+
};
|
|
1405
|
+
const config: ProductChildGroup[] = [
|
|
1406
|
+
{
|
|
1407
|
+
name: 'level1',
|
|
1408
|
+
label: 'Level 1',
|
|
1409
|
+
sideMenu: {
|
|
1410
|
+
children: [
|
|
1411
|
+
{
|
|
1412
|
+
name: 'level2',
|
|
1413
|
+
label: 'Level 2',
|
|
1414
|
+
sideMenu: {
|
|
1415
|
+
children: [
|
|
1416
|
+
{
|
|
1417
|
+
name: 'level3',
|
|
1418
|
+
label: 'Level 3',
|
|
1419
|
+
sideMenu: {
|
|
1420
|
+
children: [
|
|
1421
|
+
{
|
|
1422
|
+
name: 'deep-page',
|
|
1423
|
+
label: 'Deep Page',
|
|
1424
|
+
component: { name: 'DeepPageComponent' },
|
|
1425
|
+
},
|
|
1426
|
+
],
|
|
1427
|
+
},
|
|
1428
|
+
},
|
|
1429
|
+
],
|
|
1430
|
+
},
|
|
1431
|
+
},
|
|
1432
|
+
],
|
|
1433
|
+
},
|
|
1434
|
+
},
|
|
1435
|
+
];
|
|
1436
|
+
|
|
1437
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
1438
|
+
|
|
1439
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
1440
|
+
|
|
1441
|
+
// Verify level 1 (root)
|
|
1442
|
+
const level1Call = basicTypeCalls.find((call) => call[1] === 'deepnested-level1');
|
|
1443
|
+
|
|
1444
|
+
expect(level1Call).toBeDefined();
|
|
1445
|
+
expect(level1Call[0]).toContain('deepnested-level1-level2');
|
|
1446
|
+
|
|
1447
|
+
// Verify level 2 (nested in level1)
|
|
1448
|
+
const level2Call = basicTypeCalls.find((call) => call[1] === 'deepnested-level1::deepnested-level1-level2');
|
|
1449
|
+
|
|
1450
|
+
expect(level2Call).toBeDefined();
|
|
1451
|
+
expect(level2Call[0]).toContain('deepnested-level1-level2-level3');
|
|
1452
|
+
|
|
1453
|
+
// Verify level 3 (nested in level2)
|
|
1454
|
+
const level3Call = basicTypeCalls.find((call) => call[1] === 'deepnested-level1::deepnested-level1-level2::deepnested-level1-level2-level3');
|
|
1455
|
+
|
|
1456
|
+
expect(level3Call).toBeDefined();
|
|
1457
|
+
expect(level3Call[0]).toContain('deepnested-level1-level2-level3-deep-page');
|
|
1458
|
+
});
|
|
1459
|
+
|
|
1460
|
+
it('should handle mixed nested groups and pages in standard product extension', () => {
|
|
1461
|
+
const mockPlugin = createMockPlugin();
|
|
1462
|
+
const mockStore = createMockStore();
|
|
1463
|
+
const basicTypeCalls: any[] = [];
|
|
1464
|
+
|
|
1465
|
+
const mockDSL = {
|
|
1466
|
+
product: jest.fn(),
|
|
1467
|
+
basicType: jest.fn((...args) => basicTypeCalls.push(args)),
|
|
1468
|
+
labelGroup: jest.fn(),
|
|
1469
|
+
setGroupDefaultType: jest.fn(),
|
|
1470
|
+
weightGroup: jest.fn(),
|
|
1471
|
+
virtualType: jest.fn(),
|
|
1472
|
+
configureType: jest.fn(),
|
|
1473
|
+
weightType: jest.fn(),
|
|
1474
|
+
};
|
|
1475
|
+
|
|
1476
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
1477
|
+
|
|
1478
|
+
const validStandardProduct = StandardProductNames.EXPLORER;
|
|
1479
|
+
const config: (ProductChildGroup | ProductChildPage)[] = [
|
|
1480
|
+
{
|
|
1481
|
+
name: 'top-page',
|
|
1482
|
+
label: 'Top Page',
|
|
1483
|
+
component: { name: 'TopPageComponent' },
|
|
1484
|
+
},
|
|
1485
|
+
{
|
|
1486
|
+
name: 'parent-group',
|
|
1487
|
+
label: 'Parent Group',
|
|
1488
|
+
sideMenu: {
|
|
1489
|
+
weight: 90,
|
|
1490
|
+
children: [
|
|
1491
|
+
{
|
|
1492
|
+
name: 'sibling-page',
|
|
1493
|
+
label: 'Sibling Page',
|
|
1494
|
+
component: { name: 'SiblingPageComponent' },
|
|
1495
|
+
},
|
|
1496
|
+
{
|
|
1497
|
+
name: 'child-group',
|
|
1498
|
+
label: 'Child Group',
|
|
1499
|
+
sideMenu: {
|
|
1500
|
+
weight: 80,
|
|
1501
|
+
children: [
|
|
1502
|
+
{
|
|
1503
|
+
name: 'nested-page',
|
|
1504
|
+
label: 'Nested Page',
|
|
1505
|
+
component: { name: 'NestedPageComponent' },
|
|
1506
|
+
},
|
|
1507
|
+
],
|
|
1508
|
+
},
|
|
1509
|
+
},
|
|
1510
|
+
],
|
|
1511
|
+
},
|
|
1512
|
+
},
|
|
1513
|
+
];
|
|
1514
|
+
|
|
1515
|
+
const pluginProduct = new PluginProduct(mockPlugin, validStandardProduct, config);
|
|
1516
|
+
|
|
1517
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
1518
|
+
|
|
1519
|
+
// Verify parent group has both pages and nested groups
|
|
1520
|
+
const parentGroupCall = basicTypeCalls.find((call) => call[1] === 'explorer-parent-group');
|
|
1521
|
+
|
|
1522
|
+
expect(parentGroupCall).toBeDefined();
|
|
1523
|
+
expect(parentGroupCall[0]).toContain('explorer-parent-group-sibling-page');
|
|
1524
|
+
expect(parentGroupCall[0]).toContain('explorer-parent-group-child-group');
|
|
1525
|
+
|
|
1526
|
+
// Verify child group uses hierarchical path
|
|
1527
|
+
const childGroupCall = basicTypeCalls.find((call) => call[1] === 'explorer-parent-group::explorer-parent-group-child-group');
|
|
1528
|
+
|
|
1529
|
+
expect(childGroupCall).toBeDefined();
|
|
1530
|
+
expect(childGroupCall[0]).toContain('explorer-parent-group-child-group-nested-page');
|
|
1531
|
+
});
|
|
1532
|
+
|
|
1533
|
+
it('should only add root-level groups to their own basicType list', () => {
|
|
1534
|
+
const mockPlugin = createMockPlugin();
|
|
1535
|
+
const mockStore = createMockStore();
|
|
1536
|
+
const basicTypeCalls: any[] = [];
|
|
1537
|
+
|
|
1538
|
+
const mockDSL = {
|
|
1539
|
+
product: jest.fn(),
|
|
1540
|
+
basicType: jest.fn((...args) => basicTypeCalls.push(args)),
|
|
1541
|
+
labelGroup: jest.fn(),
|
|
1542
|
+
setGroupDefaultType: jest.fn(),
|
|
1543
|
+
weightGroup: jest.fn(),
|
|
1544
|
+
virtualType: jest.fn(),
|
|
1545
|
+
configureType: jest.fn(),
|
|
1546
|
+
weightType: jest.fn(),
|
|
1547
|
+
};
|
|
1548
|
+
|
|
1549
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
1550
|
+
|
|
1551
|
+
const productMetadata: ProductMetadata = {
|
|
1552
|
+
name: 'test-self-ref',
|
|
1553
|
+
label: 'Test Self Reference',
|
|
1554
|
+
};
|
|
1555
|
+
const config: ProductChildGroup[] = [
|
|
1556
|
+
{
|
|
1557
|
+
name: 'root',
|
|
1558
|
+
label: 'Root',
|
|
1559
|
+
sideMenu: {
|
|
1560
|
+
children: [
|
|
1561
|
+
{
|
|
1562
|
+
name: 'nested',
|
|
1563
|
+
label: 'Nested',
|
|
1564
|
+
sideMenu: {
|
|
1565
|
+
children: [
|
|
1566
|
+
{
|
|
1567
|
+
name: 'page',
|
|
1568
|
+
label: 'Page',
|
|
1569
|
+
component: { name: 'PageComponent' },
|
|
1570
|
+
},
|
|
1571
|
+
],
|
|
1572
|
+
},
|
|
1573
|
+
},
|
|
1574
|
+
],
|
|
1575
|
+
},
|
|
1576
|
+
},
|
|
1577
|
+
];
|
|
1578
|
+
|
|
1579
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
1580
|
+
|
|
1581
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
1582
|
+
|
|
1583
|
+
// Root group should include itself in its basicType call
|
|
1584
|
+
const rootCall = basicTypeCalls.find((call) => call[1] === 'testselfref-root');
|
|
1585
|
+
|
|
1586
|
+
expect(rootCall).toBeDefined();
|
|
1587
|
+
expect(rootCall[0]).toContain('testselfref-root'); // Self-reference
|
|
1588
|
+
|
|
1589
|
+
// Nested group should NOT include itself (would create wrong hierarchy)
|
|
1590
|
+
const nestedCall = basicTypeCalls.find((call) => call[1] === 'testselfref-root::testselfref-root-nested');
|
|
1591
|
+
|
|
1592
|
+
expect(nestedCall).toBeDefined();
|
|
1593
|
+
expect(nestedCall[0]).not.toContain('testselfref-root-nested'); // No self-reference for nested
|
|
1594
|
+
expect(nestedCall[0]).toContain('testselfref-root-nested-page'); // Contains its child page
|
|
1595
|
+
});
|
|
1596
|
+
|
|
1597
|
+
it('should handle multiple nested groups at the same level', () => {
|
|
1598
|
+
const mockPlugin = createMockPlugin();
|
|
1599
|
+
const mockStore = createMockStore();
|
|
1600
|
+
const basicTypeCalls: any[] = [];
|
|
1601
|
+
|
|
1602
|
+
const mockDSL = {
|
|
1603
|
+
product: jest.fn(),
|
|
1604
|
+
basicType: jest.fn((...args) => basicTypeCalls.push(args)),
|
|
1605
|
+
labelGroup: jest.fn(),
|
|
1606
|
+
setGroupDefaultType: jest.fn(),
|
|
1607
|
+
weightGroup: jest.fn(),
|
|
1608
|
+
virtualType: jest.fn(),
|
|
1609
|
+
configureType: jest.fn(),
|
|
1610
|
+
weightType: jest.fn(),
|
|
1611
|
+
};
|
|
1612
|
+
|
|
1613
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
1614
|
+
|
|
1615
|
+
const productMetadata: ProductMetadata = {
|
|
1616
|
+
name: 'multi-nested',
|
|
1617
|
+
label: 'Multi Nested',
|
|
1618
|
+
};
|
|
1619
|
+
const config: ProductChildGroup[] = [
|
|
1620
|
+
{
|
|
1621
|
+
name: 'parent',
|
|
1622
|
+
label: 'Parent',
|
|
1623
|
+
sideMenu: {
|
|
1624
|
+
children: [
|
|
1625
|
+
{
|
|
1626
|
+
name: 'child1',
|
|
1627
|
+
label: 'Child 1',
|
|
1628
|
+
sideMenu: {
|
|
1629
|
+
children: [
|
|
1630
|
+
{
|
|
1631
|
+
name: 'page1',
|
|
1632
|
+
label: 'Page 1',
|
|
1633
|
+
component: { name: 'Page1Component' },
|
|
1634
|
+
},
|
|
1635
|
+
],
|
|
1636
|
+
},
|
|
1637
|
+
},
|
|
1638
|
+
{
|
|
1639
|
+
name: 'child2',
|
|
1640
|
+
label: 'Child 2',
|
|
1641
|
+
sideMenu: {
|
|
1642
|
+
children: [
|
|
1643
|
+
{
|
|
1644
|
+
name: 'page2',
|
|
1645
|
+
label: 'Page 2',
|
|
1646
|
+
component: { name: 'Page2Component' },
|
|
1647
|
+
},
|
|
1648
|
+
],
|
|
1649
|
+
},
|
|
1650
|
+
},
|
|
1651
|
+
],
|
|
1652
|
+
},
|
|
1653
|
+
},
|
|
1654
|
+
];
|
|
1655
|
+
|
|
1656
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
1657
|
+
|
|
1658
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
1659
|
+
|
|
1660
|
+
// Verify both child groups have correct hierarchical paths
|
|
1661
|
+
const child1Call = basicTypeCalls.find((call) => call[1] === 'multinested-parent::multinested-parent-child1');
|
|
1662
|
+
|
|
1663
|
+
expect(child1Call).toBeDefined();
|
|
1664
|
+
expect(child1Call[0]).toContain('multinested-parent-child1-page1');
|
|
1665
|
+
|
|
1666
|
+
const child2Call = basicTypeCalls.find((call) => call[1] === 'multinested-parent::multinested-parent-child2');
|
|
1667
|
+
|
|
1668
|
+
expect(child2Call).toBeDefined();
|
|
1669
|
+
expect(child2Call[0]).toContain('multinested-parent-child2-page2');
|
|
1670
|
+
|
|
1671
|
+
// Verify parent includes both child groups
|
|
1672
|
+
const parentCall = basicTypeCalls.find((call) => call[1] === 'multinested-parent');
|
|
1673
|
+
|
|
1674
|
+
expect(parentCall).toBeDefined();
|
|
1675
|
+
expect(parentCall[0]).toContain('multinested-parent-child1');
|
|
1676
|
+
expect(parentCall[0]).toContain('multinested-parent-child2');
|
|
1677
|
+
});
|
|
1678
|
+
});
|
|
1679
|
+
|
|
1680
|
+
describe('group default type behavior with components', () => {
|
|
1681
|
+
it('should set correct default types for mixed groups (with and without components)', () => {
|
|
1682
|
+
const mockPlugin = createMockPlugin();
|
|
1683
|
+
const mockStore = createMockStore();
|
|
1684
|
+
const setGroupDefaultTypeCalls: any[] = [];
|
|
1685
|
+
|
|
1686
|
+
const mockDSL = {
|
|
1687
|
+
product: jest.fn(),
|
|
1688
|
+
basicType: jest.fn(),
|
|
1689
|
+
labelGroup: jest.fn(),
|
|
1690
|
+
setGroupDefaultType: jest.fn((...args) => setGroupDefaultTypeCalls.push(args)),
|
|
1691
|
+
weightGroup: jest.fn(),
|
|
1692
|
+
virtualType: jest.fn(),
|
|
1693
|
+
configureType: jest.fn(),
|
|
1694
|
+
weightType: jest.fn(),
|
|
1695
|
+
};
|
|
1696
|
+
|
|
1697
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
1698
|
+
|
|
1699
|
+
const productMetadata: ProductMetadata = {
|
|
1700
|
+
name: 'mixed-groups',
|
|
1701
|
+
label: 'Mixed Groups',
|
|
1702
|
+
};
|
|
1703
|
+
const config: ProductChildGroup[] = [
|
|
1704
|
+
{
|
|
1705
|
+
name: 'group-with-page',
|
|
1706
|
+
label: 'Group With Page',
|
|
1707
|
+
component: { name: 'GroupPageComponent' },
|
|
1708
|
+
sideMenu: {
|
|
1709
|
+
children: [
|
|
1710
|
+
{
|
|
1711
|
+
name: 'child1',
|
|
1712
|
+
label: 'Child 1',
|
|
1713
|
+
component: { name: 'Child1Component' },
|
|
1714
|
+
},
|
|
1715
|
+
],
|
|
1716
|
+
},
|
|
1717
|
+
},
|
|
1718
|
+
{
|
|
1719
|
+
name: 'group-without-page',
|
|
1720
|
+
label: 'Group Without Page',
|
|
1721
|
+
sideMenu: {
|
|
1722
|
+
children: [
|
|
1723
|
+
{
|
|
1724
|
+
name: 'child2',
|
|
1725
|
+
label: 'Child 2',
|
|
1726
|
+
component: { name: 'Child2Component' },
|
|
1727
|
+
},
|
|
1728
|
+
],
|
|
1729
|
+
},
|
|
1730
|
+
},
|
|
1731
|
+
];
|
|
1732
|
+
|
|
1733
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
1734
|
+
|
|
1735
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
1736
|
+
|
|
1737
|
+
// Group WITH component should have defaultType pointing to itself
|
|
1738
|
+
const groupWithPageCall = setGroupDefaultTypeCalls.find((call) => call[0] === 'mixedgroups-group-with-page');
|
|
1739
|
+
|
|
1740
|
+
expect(groupWithPageCall).toBeDefined();
|
|
1741
|
+
expect(groupWithPageCall[0]).toBe('mixedgroups-group-with-page');
|
|
1742
|
+
expect(groupWithPageCall[1]).toBe('mixedgroups-group-with-page'); // Points to itself
|
|
1743
|
+
|
|
1744
|
+
// Group WITHOUT component should have defaultType pointing to first child
|
|
1745
|
+
const groupWithoutPageCall = setGroupDefaultTypeCalls.find((call) => call[0] === 'mixedgroups-group-without-page');
|
|
1746
|
+
|
|
1747
|
+
expect(groupWithoutPageCall).toBeDefined();
|
|
1748
|
+
expect(groupWithoutPageCall[0]).toBe('mixedgroups-group-without-page');
|
|
1749
|
+
expect(groupWithoutPageCall[1]).toBe('mixedgroups-group-without-page-child2'); // Points to first child
|
|
1750
|
+
});
|
|
1751
|
+
|
|
1752
|
+
it('should handle nested groups where both parent and child have components', () => {
|
|
1753
|
+
const mockPlugin = createMockPlugin();
|
|
1754
|
+
const mockStore = createMockStore();
|
|
1755
|
+
const setGroupDefaultTypeCalls: any[] = [];
|
|
1756
|
+
|
|
1757
|
+
const mockDSL = {
|
|
1758
|
+
product: jest.fn(),
|
|
1759
|
+
basicType: jest.fn(),
|
|
1760
|
+
labelGroup: jest.fn(),
|
|
1761
|
+
setGroupDefaultType: jest.fn((...args) => setGroupDefaultTypeCalls.push(args)),
|
|
1762
|
+
weightGroup: jest.fn(),
|
|
1763
|
+
virtualType: jest.fn(),
|
|
1764
|
+
configureType: jest.fn(),
|
|
1765
|
+
weightType: jest.fn(),
|
|
1766
|
+
};
|
|
1767
|
+
|
|
1768
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
1769
|
+
|
|
1770
|
+
const productMetadata: ProductMetadata = {
|
|
1771
|
+
name: 'nested-with-pages',
|
|
1772
|
+
label: 'Nested With Pages',
|
|
1773
|
+
};
|
|
1774
|
+
const config: ProductChildGroup[] = [
|
|
1775
|
+
{
|
|
1776
|
+
name: 'parent',
|
|
1777
|
+
label: 'Parent',
|
|
1778
|
+
component: { name: 'ParentComponent' },
|
|
1779
|
+
sideMenu: {
|
|
1780
|
+
children: [
|
|
1781
|
+
{
|
|
1782
|
+
name: 'child-group',
|
|
1783
|
+
label: 'Child Group',
|
|
1784
|
+
component: { name: 'ChildGroupComponent' },
|
|
1785
|
+
sideMenu: {
|
|
1786
|
+
children: [
|
|
1787
|
+
{
|
|
1788
|
+
name: 'grandchild',
|
|
1789
|
+
label: 'Grandchild',
|
|
1790
|
+
component: { name: 'GrandchildComponent' },
|
|
1791
|
+
},
|
|
1792
|
+
],
|
|
1793
|
+
},
|
|
1794
|
+
},
|
|
1795
|
+
],
|
|
1796
|
+
},
|
|
1797
|
+
},
|
|
1798
|
+
];
|
|
1799
|
+
|
|
1800
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
1801
|
+
|
|
1802
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
1803
|
+
|
|
1804
|
+
// Parent group with component should point to itself
|
|
1805
|
+
const parentCall = setGroupDefaultTypeCalls.find((call) => call[0] === 'nestedwithpages-parent');
|
|
1806
|
+
|
|
1807
|
+
expect(parentCall).toBeDefined();
|
|
1808
|
+
expect(parentCall[1]).toBe('nestedwithpages-parent');
|
|
1809
|
+
|
|
1810
|
+
// Nested child group with component should also point to itself
|
|
1811
|
+
const childCall = setGroupDefaultTypeCalls.find((call) => call[0] === 'nestedwithpages-parent-child-group');
|
|
1812
|
+
|
|
1813
|
+
expect(childCall).toBeDefined();
|
|
1814
|
+
expect(childCall[1]).toBe('nestedwithpages-parent-child-group');
|
|
1815
|
+
});
|
|
1816
|
+
|
|
1817
|
+
it('should reproduce user bug: group with component and children routes to group page not first child', () => {
|
|
1818
|
+
const mockPlugin = createMockPlugin();
|
|
1819
|
+
const mockStore = createMockStore();
|
|
1820
|
+
const setGroupDefaultTypeCalls: any[] = [];
|
|
1821
|
+
const virtualTypeCalls: any[] = [];
|
|
1822
|
+
|
|
1823
|
+
const mockDSL = {
|
|
1824
|
+
product: jest.fn(),
|
|
1825
|
+
basicType: jest.fn(),
|
|
1826
|
+
labelGroup: jest.fn(),
|
|
1827
|
+
setGroupDefaultType: jest.fn((...args) => setGroupDefaultTypeCalls.push(args)),
|
|
1828
|
+
weightGroup: jest.fn(),
|
|
1829
|
+
virtualType: jest.fn((...args) => virtualTypeCalls.push(args)),
|
|
1830
|
+
configureType: jest.fn(),
|
|
1831
|
+
weightType: jest.fn(),
|
|
1832
|
+
};
|
|
1833
|
+
|
|
1834
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
1835
|
+
|
|
1836
|
+
// Exact scenario from user's bug report
|
|
1837
|
+
const productMetadata: ProductMetadata = {
|
|
1838
|
+
name: 'group-with-page',
|
|
1839
|
+
label: 'Group With Page',
|
|
1840
|
+
};
|
|
1841
|
+
const config: (ProductChildGroup | ProductChildPage)[] = [
|
|
1842
|
+
{
|
|
1843
|
+
name: 'general1',
|
|
1844
|
+
label: 'General Settings1',
|
|
1845
|
+
component: { name: 'GeneralComponent' },
|
|
1846
|
+
},
|
|
1847
|
+
{
|
|
1848
|
+
name: 'settings',
|
|
1849
|
+
label: 'Settings',
|
|
1850
|
+
component: { name: 'SettingsOverviewComponent' },
|
|
1851
|
+
sideMenu: {
|
|
1852
|
+
children: [
|
|
1853
|
+
{
|
|
1854
|
+
name: 'general',
|
|
1855
|
+
label: 'General Settings',
|
|
1856
|
+
component: { name: 'GeneralComponent' },
|
|
1857
|
+
},
|
|
1858
|
+
{
|
|
1859
|
+
name: 'advanced',
|
|
1860
|
+
label: 'Advanced Settings',
|
|
1861
|
+
component: { name: 'AdvancedComponent' },
|
|
1862
|
+
},
|
|
1863
|
+
],
|
|
1864
|
+
},
|
|
1865
|
+
},
|
|
1866
|
+
{
|
|
1867
|
+
name: 'general2',
|
|
1868
|
+
label: 'General Settings2',
|
|
1869
|
+
component: { name: 'GeneralComponent' },
|
|
1870
|
+
},
|
|
1871
|
+
];
|
|
1872
|
+
|
|
1873
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
1874
|
+
|
|
1875
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
1876
|
+
|
|
1877
|
+
// Verify the settings group (which has component + children) has defaultType pointing to itself
|
|
1878
|
+
const settingsCall = setGroupDefaultTypeCalls.find((call) => call[0] === 'groupwithpage-settings');
|
|
1879
|
+
|
|
1880
|
+
expect(settingsCall).toBeDefined();
|
|
1881
|
+
expect(settingsCall[0]).toBe('groupwithpage-settings');
|
|
1882
|
+
expect(settingsCall[1]).toBe('groupwithpage-settings'); // Should point to itself, NOT 'groupwithpage-settings-general'
|
|
1883
|
+
|
|
1884
|
+
// Verify the settings virtualType was created with exact + overview flags
|
|
1885
|
+
const settingsVirtualType = virtualTypeCalls.find((call) => call[0].name === 'groupwithpage-settings');
|
|
1886
|
+
|
|
1887
|
+
expect(settingsVirtualType).toBeDefined();
|
|
1888
|
+
expect(settingsVirtualType[0].exact).toBe(true);
|
|
1889
|
+
expect(settingsVirtualType[0].overview).toBe(true);
|
|
1890
|
+
});
|
|
1891
|
+
|
|
1892
|
+
it('should handle empty children array with component (group page, no children)', () => {
|
|
1893
|
+
const mockPlugin = createMockPlugin();
|
|
1894
|
+
const mockStore = createMockStore();
|
|
1895
|
+
const setGroupDefaultTypeCalls: any[] = [];
|
|
1896
|
+
|
|
1897
|
+
const mockDSL = {
|
|
1898
|
+
product: jest.fn(),
|
|
1899
|
+
basicType: jest.fn(),
|
|
1900
|
+
labelGroup: jest.fn(),
|
|
1901
|
+
setGroupDefaultType: jest.fn((...args) => setGroupDefaultTypeCalls.push(args)),
|
|
1902
|
+
weightGroup: jest.fn(),
|
|
1903
|
+
virtualType: jest.fn(),
|
|
1904
|
+
configureType: jest.fn(),
|
|
1905
|
+
weightType: jest.fn(),
|
|
1906
|
+
};
|
|
1907
|
+
|
|
1908
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
1909
|
+
|
|
1910
|
+
const productMetadata: ProductMetadata = {
|
|
1911
|
+
name: 'empty-children',
|
|
1912
|
+
label: 'Empty Children',
|
|
1913
|
+
};
|
|
1914
|
+
const config: ProductChildGroup[] = [
|
|
1915
|
+
{
|
|
1916
|
+
name: 'standalone-group',
|
|
1917
|
+
label: 'Standalone Group',
|
|
1918
|
+
component: { name: 'StandaloneComponent' },
|
|
1919
|
+
sideMenu: { children: [] },
|
|
1920
|
+
},
|
|
1921
|
+
];
|
|
1922
|
+
|
|
1923
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
1924
|
+
|
|
1925
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
1926
|
+
|
|
1927
|
+
// Group with component but empty children should still point to itself
|
|
1928
|
+
const groupCall = setGroupDefaultTypeCalls.find((call) => call[0] === 'emptychildren-standalone-group');
|
|
1929
|
+
|
|
1930
|
+
expect(groupCall).toBeDefined();
|
|
1931
|
+
expect(groupCall[1]).toBe('emptychildren-standalone-group');
|
|
1932
|
+
});
|
|
1933
|
+
});
|
|
1934
|
+
});
|
|
1935
|
+
});
|