@rancher/shell 3.0.12-rc.4 → 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.
Files changed (159) hide show
  1. package/apis/intf/resources-api/resource-base.ts +42 -2
  2. package/apis/intf/resources-api/resource-instance.ts +101 -0
  3. package/apis/intf/resources-api/resources-api.ts +177 -23
  4. package/apis/intf/resources.ts +8 -6
  5. package/apis/resources/__tests__/resources-api-class.test.ts +309 -28
  6. package/apis/resources/resources-api-class.ts +232 -43
  7. package/assets/images/providers/traefik.png +0 -0
  8. package/assets/styles/global/_button.scss +1 -1
  9. package/assets/translations/en-us.yaml +49 -14
  10. package/chart/__tests__/rancher-monitoring-dashboards-index.test.ts +269 -0
  11. package/chart/rancher-monitoring-dashboards/index.vue +155 -0
  12. package/cloud-credential/__tests__/generic.test.ts +132 -0
  13. package/cloud-credential/generic.vue +33 -3
  14. package/components/ActionDropdownShell.vue +5 -3
  15. package/components/ButtonGroup.vue +26 -1
  16. package/components/CruResource.vue +51 -2
  17. package/components/EtcdInfoBanner.vue +2 -12
  18. package/components/GrafanaDashboard.vue +2 -8
  19. package/components/PromptRestore.vue +93 -32
  20. package/components/Questions/index.vue +1 -0
  21. package/components/Resource/Detail/Masthead/__tests__/index.test.ts +68 -14
  22. package/components/Resource/Detail/Masthead/index.vue +8 -3
  23. package/components/ResourceTable.vue +1 -0
  24. package/components/SortableTable/index.vue +4 -3
  25. package/components/Wizard.vue +14 -1
  26. package/components/__tests__/ButtonGroup.test.ts +56 -0
  27. package/components/__tests__/PromptRestore.test.ts +169 -19
  28. package/components/fleet/GitRepoAdvancedTab.vue +1 -0
  29. package/components/fleet/GitRepoMetadataTab.vue +5 -0
  30. package/components/fleet/HelmOpAppCoConfigTab.vue +4 -0
  31. package/components/fleet/HelmOpMetadataTab.vue +5 -0
  32. package/components/form/FileSelector.vue +39 -1
  33. package/components/form/PrivateRegistry.constants.ts +7 -0
  34. package/components/form/PrivateRegistry.vue +253 -18
  35. package/components/form/Security.vue +1 -1
  36. package/components/form/SelectOrCreateAuthSecret.vue +140 -17
  37. package/components/form/__tests__/FileSelector.test.ts +23 -0
  38. package/components/form/__tests__/PrivateRegistry.test.ts +463 -73
  39. package/components/form/__tests__/SelectOrCreateAuthSecret.test.ts +122 -0
  40. package/components/formatter/EtcdSnapshotName.vue +73 -0
  41. package/components/formatter/Translate.vue +22 -1
  42. package/components/nav/Group.vue +5 -10
  43. package/components/nav/Header.vue +8 -1
  44. package/components/nav/TopLevelMenu.vue +1 -30
  45. package/components/nav/__tests__/Group.test.ts +61 -0
  46. package/components/templates/default.vue +7 -0
  47. package/config/features.js +1 -0
  48. package/config/labels-annotations.js +3 -0
  49. package/config/product/manager.js +6 -0
  50. package/config/secret.ts +10 -0
  51. package/config/settings.ts +6 -2
  52. package/config/table-headers.js +5 -2
  53. package/config/types.js +7 -0
  54. package/core/__tests__/plugin-products-apply.test.ts +787 -0
  55. package/core/__tests__/plugin-products-extend.test.ts +189 -0
  56. package/core/__tests__/plugin-products-helpers.test.ts +81 -78
  57. package/core/__tests__/plugin-products-new.test.ts +389 -0
  58. package/core/__tests__/plugin-products-scenarios.test.ts +980 -0
  59. package/core/__tests__/plugin-products-side-menu.test.ts +1935 -0
  60. package/core/plugin-products-base.ts +201 -93
  61. package/core/plugin-products-extending.ts +1 -1
  62. package/core/plugin-products-external.ts +380 -0
  63. package/core/plugin-products-helpers.ts +28 -24
  64. package/core/plugin-products-internal.ts +207 -0
  65. package/core/plugin-products-top-level.ts +10 -5
  66. package/core/plugin-products-type-guards.ts +17 -6
  67. package/core/plugin-products.ts +5 -7
  68. package/core/plugin-types.ts +1 -389
  69. package/core/plugin.ts +10 -11
  70. package/core/types.ts +6 -202
  71. package/detail/__tests__/provisioning.cattle.io.cluster.test.ts +52 -0
  72. package/detail/pod.vue +1 -1
  73. package/detail/provisioning.cattle.io.cluster.vue +79 -5
  74. package/dialog/RotateEncryptionKeyDialog.vue +33 -9
  75. package/dialog/__tests__/RotateEncryptionKeyDialog.test.ts +78 -0
  76. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +92 -0
  77. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +101 -0
  78. package/edit/__tests__/management.cattle.io.setting.test.ts +2 -1
  79. package/edit/auth/AuthProviderWarningBanners.vue +12 -0
  80. package/edit/auth/__tests__/AuthProviderWarningBanners.test.ts +10 -1
  81. package/edit/auth/__tests__/azuread.test.ts +1 -0
  82. package/edit/auth/__tests__/github.test.ts +1 -0
  83. package/edit/auth/__tests__/oidc.test.ts +1 -0
  84. package/edit/auth/__tests__/saml.test.ts +1 -0
  85. package/edit/auth/ldap/__tests__/index.test.ts +1 -0
  86. package/edit/compliance.cattle.io.clusterscanprofile.vue +39 -41
  87. package/edit/fleet.cattle.io.gitrepo.vue +70 -16
  88. package/edit/fleet.cattle.io.helmop.vue +51 -5
  89. package/edit/helm.cattle.io.projecthelmchart.vue +1 -0
  90. package/edit/{management.cattle.io.setting.vue → management.cattle.io.setting/index.vue} +32 -9
  91. package/edit/management.cattle.io.setting/system-default-registry-pull-secrets.vue +81 -0
  92. package/edit/provisioning.cattle.io.cluster/SelectCredential.vue +3 -12
  93. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +18 -0
  94. package/edit/provisioning.cattle.io.cluster/index.vue +1 -1
  95. package/edit/provisioning.cattle.io.cluster/rke2.vue +6 -2
  96. package/edit/provisioning.cattle.io.cluster/tabs/etcd/index.vue +0 -1
  97. package/edit/provisioning.cattle.io.cluster/tabs/networking/index.vue +2 -2
  98. package/edit/provisioning.cattle.io.cluster/tabs/registries/index.vue +14 -55
  99. package/list/management.cattle.io.user.vue +24 -5
  100. package/list/utils/management.cattle.io.cluster.utils.ts +1 -1
  101. package/machine-config/__tests__/generic.test.ts +163 -0
  102. package/machine-config/components/EC2Networking.vue +31 -7
  103. package/machine-config/components/__tests__/EC2Networking.test.ts +38 -15
  104. package/machine-config/generic.vue +32 -5
  105. package/mixins/resource-fetch.js +1 -1
  106. package/models/__tests__/provisioning.cattle.io.cluster.test.ts +156 -0
  107. package/models/__tests__/rke-machine.cattle.io.vmwarevspheremachinetemplate.test.ts +22 -0
  108. package/models/__tests__/secret.test.ts +68 -1
  109. package/models/management.cattle.io.cluster.js +21 -3
  110. package/models/pod.js +25 -12
  111. package/models/provisioning.cattle.io.cluster.js +59 -9
  112. package/models/rke-machine.cattle.io.vmwarevspheremachinetemplate.js +9 -0
  113. package/models/rke.cattle.io.etcdsnapshot.js +17 -9
  114. package/models/secret.js +19 -0
  115. package/models/workload.js +12 -7
  116. package/package.json +1 -1
  117. package/pages/auth/login.vue +1 -1
  118. package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +569 -81
  119. package/pages/c/_cluster/apps/charts/install.vue +143 -28
  120. package/pages/c/_cluster/auth/config/index.vue +11 -4
  121. package/pages/c/_cluster/monitoring/index.vue +6 -1
  122. package/pkg/require-asset.lib.js +25 -0
  123. package/pkg/vue.config.js +7 -0
  124. package/plugins/dashboard-store/__tests__/resource-class.test.ts +84 -0
  125. package/plugins/dashboard-store/getters.js +0 -1
  126. package/plugins/dashboard-store/resource-class.js +62 -15
  127. package/plugins/steve/__tests__/steve-class-resource-api.test.ts +147 -0
  128. package/plugins/steve/steve-class.js +43 -0
  129. package/plugins/steve/subscribe.js +11 -0
  130. package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +115 -0
  131. package/rancher-components/Form/LabeledInput/LabeledInput.vue +83 -3
  132. package/rancher-components/Form/TextArea/TextAreaAutoGrow.vue +30 -0
  133. package/rancher-components/Form/TextArea/__tests__/TextAreaAutoGrow.test.ts +95 -0
  134. package/rancher-components/RcButton/index.ts +1 -1
  135. package/rancher-components/RcDropdown/RcDropdownTrigger.vue +6 -1
  136. package/store/__tests__/auth.test.ts +18 -0
  137. package/store/__tests__/features.test.ts +131 -0
  138. package/store/__tests__/growl.test.ts +374 -0
  139. package/store/__tests__/modal.test.ts +131 -0
  140. package/store/__tests__/notifications.test.ts +466 -1
  141. package/store/__tests__/slideInPanel.test.ts +88 -0
  142. package/store/__tests__/type-map.utils.test.ts +433 -0
  143. package/store/__tests__/ui-context.test.ts +255 -0
  144. package/store/auth.js +11 -1
  145. package/store/features.js +5 -0
  146. package/store/plugins.js +6 -0
  147. package/types/rancher/steve.api.ts +2 -2
  148. package/types/shell/index.d.ts +120 -0
  149. package/types/store/dashboard-store.types.ts +2 -2
  150. package/types/store/type-map.ts +262 -1
  151. package/utils/__tests__/operation-cr.test.ts +34 -0
  152. package/utils/async.ts +2 -0
  153. package/utils/grafana.js +2 -17
  154. package/utils/monitoring.js +38 -1
  155. package/utils/operation-cr.js +19 -0
  156. package/utils/require-asset.ts +7 -0
  157. package/utils/validators/__tests__/private-registry.test.ts +27 -15
  158. package/utils/validators/private-registry.ts +15 -4
  159. package/core/__tests__/plugin-products.test.ts +0 -4694
@@ -0,0 +1,433 @@
1
+ import {
2
+ conditionalDepaginate,
3
+ configureConditionalDepaginate,
4
+ headerFromSchemaCol,
5
+ rowValueGetter,
6
+ } from '../type-map.utils';
7
+
8
+ const makeGetters = (overrides: any = {}) => ({
9
+ 'i18n/exists': jest.fn(() => false),
10
+ 'i18n/t': jest.fn((key: string) => key),
11
+ currentStore: jest.fn(() => 'cluster'),
12
+ 'cluster/all': jest.fn(() => []),
13
+ ...overrides,
14
+ });
15
+
16
+ const makeCol = (overrides: any = {}) => ({
17
+ description: '',
18
+ field: '$.metadata.name',
19
+ format: '',
20
+ name: 'Name',
21
+ priority: 0,
22
+ type: 'string',
23
+ ...overrides,
24
+ });
25
+
26
+ const makeAgeColumn = () => ({
27
+ name: 'age',
28
+ label: 'Age',
29
+ value: 'metadata.creationTimestamp',
30
+ sort: ['metadata.creationTimestamp'],
31
+ });
32
+
33
+ describe('rowValueGetter', () => {
34
+ describe('fields matching $.metadata.fields[N] pattern', () => {
35
+ it.each([
36
+ {
37
+ desc: 'returns function accessing index 0',
38
+ field: '$.metadata.fields[0]',
39
+ asFn: true as const,
40
+ expectedValue: { metadata: { fields: ['first', 'second'] } },
41
+ expectedIdx: 0,
42
+ },
43
+ {
44
+ desc: 'returns function accessing index 2',
45
+ field: '$.metadata.fields[2]',
46
+ asFn: true as const,
47
+ expectedValue: { metadata: { fields: ['a', 'b', 'c'] } },
48
+ expectedIdx: 2,
49
+ },
50
+ ])('$desc', ({
51
+ field, asFn, expectedValue, expectedIdx,
52
+ }) => {
53
+ const getter = rowValueGetter(makeCol({ field }), asFn) as (row: any) => any;
54
+
55
+ expect(typeof getter).toStrictEqual('function');
56
+ expect(getter(expectedValue)).toStrictEqual(expectedValue.metadata.fields[expectedIdx]);
57
+ });
58
+
59
+ it.each([
60
+ {
61
+ desc: 'returns string path for index 0',
62
+ field: '$.metadata.fields[0]',
63
+ asFn: false as const,
64
+ expected: 'metadata.fields.0',
65
+ },
66
+ {
67
+ desc: 'returns string path for index 3',
68
+ field: '$.metadata.fields[3]',
69
+ asFn: false as const,
70
+ expected: 'metadata.fields.3',
71
+ },
72
+ ])('$desc', ({ field, asFn, expected }) => {
73
+ expect(rowValueGetter(makeCol({ field }), asFn)).toStrictEqual(expected);
74
+ });
75
+
76
+ it('returns undefined when row has no fields array', () => {
77
+ const getter = rowValueGetter(makeCol({ field: '$.metadata.fields[0]' }), true) as (row: any) => any;
78
+
79
+ expect(getter({})).toBeUndefined();
80
+ });
81
+
82
+ it('returns undefined when metadata is missing', () => {
83
+ const getter = rowValueGetter(makeCol({ field: '$.metadata.fields[1]' }), true) as (row: any) => any;
84
+
85
+ expect(getter({ other: 'data' })).toBeUndefined();
86
+ });
87
+ });
88
+
89
+ describe('fields starting with . (dot-prefixed)', () => {
90
+ it('prepends $ to dot-prefixed fields', () => {
91
+ const result = rowValueGetter(makeCol({ field: '.metadata.name' }), false);
92
+
93
+ expect(result).toStrictEqual('$.metadata.name');
94
+ });
95
+
96
+ it('does not modify fields already starting with $', () => {
97
+ const result = rowValueGetter(makeCol({ field: '$.metadata.name' }), false);
98
+
99
+ expect(result).toStrictEqual('$.metadata.name');
100
+ });
101
+ });
102
+
103
+ describe('fields with escaped dots (rewriteJsonPath)', () => {
104
+ it.each([
105
+ {
106
+ desc: 'rewrites single escaped dot in label key',
107
+ field: '$.metadata.labels.topology\\.kubernetes\\.io/zone',
108
+ expected: '$.metadata.labels.["topology.kubernetes.io/zone"]',
109
+ },
110
+ {
111
+ desc: 'rewrites escaped dot in annotation key',
112
+ field: '$.metadata.annotations.cattle\\.io/hash',
113
+ expected: '$.metadata.annotations.["cattle.io/hash"]',
114
+ },
115
+ ])('$desc', ({ field, expected }) => {
116
+ expect(rowValueGetter(makeCol({ field }), false)).toStrictEqual(expected);
117
+ });
118
+
119
+ it('returns path unchanged when no escaped dots present', () => {
120
+ const field = '$.metadata.labels.app';
121
+
122
+ expect(rowValueGetter(makeCol({ field }), false)).toStrictEqual(field);
123
+ });
124
+ });
125
+ });
126
+
127
+ describe('conditionalDepaginate', () => {
128
+ it.each([
129
+ {
130
+ desc: 'returns true when depaginate is boolean true',
131
+ depaginate: true,
132
+ args: undefined,
133
+ expected: true,
134
+ },
135
+ {
136
+ desc: 'returns false when depaginate is boolean false',
137
+ depaginate: false,
138
+ args: undefined,
139
+ expected: false,
140
+ },
141
+ {
142
+ desc: 'returns undefined when depaginate is undefined',
143
+ depaginate: undefined,
144
+ args: undefined,
145
+ expected: undefined,
146
+ },
147
+ ])('$desc', ({ depaginate, args, expected }) => {
148
+ expect(conditionalDepaginate(depaginate as any, args)).toStrictEqual(expected);
149
+ });
150
+
151
+ it('calls function with args and returns its result when args are provided', () => {
152
+ const fn = jest.fn(() => true);
153
+ const args = { ctx: { rootGetters: {} as any }, args: { type: 'pod', opt: {} } };
154
+
155
+ const result = conditionalDepaginate(fn, args);
156
+
157
+ expect(fn).toHaveBeenCalledWith(args);
158
+ expect(result).toStrictEqual(true);
159
+ });
160
+
161
+ it('returns false when depaginate is a function but no args are provided', () => {
162
+ const fn = jest.fn(() => true);
163
+
164
+ expect(conditionalDepaginate(fn, undefined)).toStrictEqual(false);
165
+ expect(fn).not.toHaveBeenCalled();
166
+ });
167
+ });
168
+
169
+ describe('configureConditionalDepaginate', () => {
170
+ const makeRootGetters = (resourceCount: number | undefined, type: string, store = 'cluster') => {
171
+ const counts: any = {};
172
+
173
+ if (resourceCount !== undefined) {
174
+ counts[type] = { summary: { count: resourceCount } };
175
+ }
176
+
177
+ return {
178
+ currentStore: jest.fn(() => store),
179
+ [`${ store }/all`]: jest.fn(() => [{ counts }]),
180
+ };
181
+ };
182
+
183
+ it('returns true when resource count is below maxResourceCount', () => {
184
+ const type = 'pod';
185
+ const rootGetters = makeRootGetters(50, type) as any;
186
+ const fn = configureConditionalDepaginate({ maxResourceCount: 100, isNorman: false });
187
+ const result = fn({
188
+ ctx: { rootGetters },
189
+ args: { type, opt: {} },
190
+ });
191
+
192
+ expect(result).toStrictEqual(true);
193
+ });
194
+
195
+ it('returns false when resource count equals maxResourceCount', () => {
196
+ const type = 'pod';
197
+ const rootGetters = makeRootGetters(100, type) as any;
198
+ const fn = configureConditionalDepaginate({ maxResourceCount: 100, isNorman: false });
199
+ const result = fn({
200
+ ctx: { rootGetters },
201
+ args: { type, opt: {} },
202
+ });
203
+
204
+ expect(result).toStrictEqual(false);
205
+ });
206
+
207
+ it('returns false when resource count exceeds maxResourceCount', () => {
208
+ const type = 'pod';
209
+ const rootGetters = makeRootGetters(200, type) as any;
210
+ const fn = configureConditionalDepaginate({ maxResourceCount: 100, isNorman: false });
211
+ const result = fn({
212
+ ctx: { rootGetters },
213
+ args: { type, opt: {} },
214
+ });
215
+
216
+ expect(result).toStrictEqual(false);
217
+ });
218
+
219
+ it('returns false when resource count is undefined', () => {
220
+ const type = 'pod';
221
+ const rootGetters = makeRootGetters(undefined, type) as any;
222
+ const fn = configureConditionalDepaginate({ maxResourceCount: 100, isNorman: false });
223
+ const result = fn({
224
+ ctx: { rootGetters },
225
+ args: { type, opt: {} },
226
+ });
227
+
228
+ expect(result).toStrictEqual(false);
229
+ });
230
+
231
+ it('uses management.cattle.io. prefix for Norman types', () => {
232
+ const type = 'node';
233
+ const normanType = `management.cattle.io.${ type }`;
234
+ const rootGetters = makeRootGetters(5, normanType) as any;
235
+ const fn = configureConditionalDepaginate({ maxResourceCount: 100, isNorman: true });
236
+ const result = fn({
237
+ ctx: { rootGetters },
238
+ args: { type, opt: {} },
239
+ });
240
+
241
+ expect(result).toStrictEqual(true);
242
+ });
243
+ });
244
+
245
+ describe('headerFromSchemaCol', () => {
246
+ describe('age column shortcut', () => {
247
+ it.each([
248
+ {
249
+ desc: 'returns ageColumn when format is empty and name is age',
250
+ col: makeCol({
251
+ name: 'age',
252
+ format: '',
253
+ }),
254
+ },
255
+ {
256
+ desc: 'returns ageColumn when format is date and name is age',
257
+ col: makeCol({
258
+ name: 'age',
259
+ format: 'date',
260
+ }),
261
+ },
262
+ {
263
+ desc: 'returns ageColumn when type is date and name is age',
264
+ col: makeCol({
265
+ name: 'age',
266
+ type: 'date',
267
+ format: '',
268
+ }),
269
+ },
270
+ {
271
+ desc: 'returns ageColumn when name is Age (case insensitive)',
272
+ col: makeCol({
273
+ name: 'Age',
274
+ format: '',
275
+ }),
276
+ },
277
+ ])('$desc', ({ col }) => {
278
+ const ageColumn = makeAgeColumn() as any;
279
+ const result = headerFromSchemaCol(col as any, makeGetters() as any, false, ageColumn);
280
+
281
+ expect(result).toStrictEqual(ageColumn);
282
+ });
283
+
284
+ it('does not return ageColumn when ageColumn is not provided', () => {
285
+ const col = makeCol({ name: 'age', format: '' });
286
+ const result = headerFromSchemaCol(col as any, makeGetters() as any, false, null as any);
287
+
288
+ expect(result).not.toStrictEqual(null);
289
+ expect(result.name).toStrictEqual('age');
290
+ });
291
+ });
292
+
293
+ describe('formatter assignment', () => {
294
+ it('sets formatter to Date and width 120 for date format', () => {
295
+ const col = makeCol({
296
+ name: 'created',
297
+ format: 'date',
298
+ });
299
+ const result = headerFromSchemaCol(col as any, makeGetters() as any, false, null as any);
300
+
301
+ expect(result.formatter).toStrictEqual('Date');
302
+ expect(result.width).toStrictEqual(120);
303
+ expect(result.formatterOpts).toStrictEqual({ multiline: true });
304
+ });
305
+
306
+ it('sets formatter to Date and width 120 when type is date', () => {
307
+ const col = makeCol({
308
+ name: 'modified',
309
+ format: '',
310
+ type: 'date',
311
+ });
312
+ const result = headerFromSchemaCol(col as any, makeGetters() as any, false, null as any);
313
+
314
+ expect(result.formatter).toStrictEqual('Date');
315
+ expect(result.width).toStrictEqual(120);
316
+ });
317
+
318
+ it('sets formatter to Number when type is number', () => {
319
+ const col = makeCol({
320
+ name: 'count',
321
+ type: 'number',
322
+ });
323
+ const result = headerFromSchemaCol(col as any, makeGetters() as any, false, null as any);
324
+
325
+ expect(result.formatter).toStrictEqual('Number');
326
+ });
327
+
328
+ it('sets formatter to Number when type is int', () => {
329
+ const col = makeCol({
330
+ name: 'replicas',
331
+ type: 'int',
332
+ });
333
+ const result = headerFromSchemaCol(col as any, makeGetters() as any, false, null as any);
334
+
335
+ expect(result.formatter).toStrictEqual('Number');
336
+ });
337
+
338
+ it('does not set formatter for plain text column', () => {
339
+ const col = makeCol({ name: 'status', type: 'string' });
340
+ const result = headerFromSchemaCol(col as any, makeGetters() as any, false, null as any);
341
+
342
+ expect(result.formatter).toBeUndefined();
343
+ });
344
+ });
345
+
346
+ describe('label resolution', () => {
347
+ it('uses i18n translation when key exists', () => {
348
+ const getters = makeGetters({
349
+ 'i18n/exists': jest.fn(() => true),
350
+ 'i18n/t': jest.fn((key: string) => `translated:${ key }`),
351
+ });
352
+ const col = makeCol({ name: 'status' });
353
+ const result = headerFromSchemaCol(col as any, getters as any, false, null as any);
354
+
355
+ expect(result.label).toStrictEqual('translated:tableHeaders.status');
356
+ });
357
+
358
+ it('uses column name when i18n key does not exist', () => {
359
+ const getters = makeGetters({ 'i18n/exists': jest.fn(() => false) });
360
+ const col = makeCol({ name: 'MyColumn' });
361
+ const result = headerFromSchemaCol(col as any, getters as any, false, null as any);
362
+
363
+ expect(result.label).toStrictEqual('MyColumn');
364
+ });
365
+
366
+ it('camelCases the i18n key when col name has spaces', () => {
367
+ const getters = makeGetters({
368
+ 'i18n/exists': jest.fn((key: string) => key === 'tableHeaders.myLabel'),
369
+ 'i18n/t': jest.fn((key: string) => `t:${ key }`),
370
+ });
371
+ const col = makeCol({ name: 'my label' });
372
+ const result = headerFromSchemaCol(col as any, getters as any, false, null as any);
373
+
374
+ expect(result.label).toStrictEqual('t:tableHeaders.myLabel');
375
+ });
376
+ });
377
+
378
+ describe('tooltip from description', () => {
379
+ it('uses description as tooltip when not ending with dot', () => {
380
+ const col = makeCol({ name: 'status', description: 'Current status' });
381
+ const result = headerFromSchemaCol(col as any, makeGetters() as any, false, null as any);
382
+
383
+ expect(result.tooltip).toStrictEqual('Current status');
384
+ });
385
+
386
+ it('strips trailing dot from description for tooltip', () => {
387
+ const col = makeCol({ name: 'status', description: 'Current status.' });
388
+ const result = headerFromSchemaCol(col as any, makeGetters() as any, false, null as any);
389
+
390
+ expect(result.tooltip).toStrictEqual('Current status');
391
+ });
392
+
393
+ it('uses empty string as tooltip when description is empty', () => {
394
+ const col = makeCol({ name: 'status', description: '' });
395
+ const result = headerFromSchemaCol(col as any, makeGetters() as any, false, null as any);
396
+
397
+ expect(result.tooltip).toStrictEqual('');
398
+ });
399
+ });
400
+
401
+ describe('pagination mode', () => {
402
+ it('uses string path as value when pagination is true', () => {
403
+ const col = makeCol({ name: 'status', field: '$.metadata.name' });
404
+ const result = headerFromSchemaCol(col as any, makeGetters() as any, true, null as any);
405
+
406
+ expect(typeof result.value).toStrictEqual('string');
407
+ });
408
+
409
+ it('uses function as value when pagination is false', () => {
410
+ const col = makeCol({ name: 'status', field: '$.metadata.fields[0]' });
411
+ const result = headerFromSchemaCol(col as any, makeGetters() as any, false, null as any);
412
+
413
+ expect(typeof result.value).toStrictEqual('function');
414
+ });
415
+ });
416
+
417
+ describe('output shape', () => {
418
+ it('sets name to lowercase version of col.name', () => {
419
+ const col = makeCol({ name: 'Status' });
420
+ const result = headerFromSchemaCol(col as any, makeGetters() as any, false, null as any);
421
+
422
+ expect(result.name).toStrictEqual('status');
423
+ });
424
+
425
+ it('sets sort and search from string path', () => {
426
+ const col = makeCol({ name: 'name', field: '$.metadata.name' });
427
+ const result = headerFromSchemaCol(col as any, makeGetters() as any, true, null as any);
428
+
429
+ expect(result.sort).toStrictEqual(['$.metadata.name']);
430
+ expect(result.search).toStrictEqual('$.metadata.name');
431
+ });
432
+ });
433
+ });
@@ -0,0 +1,255 @@
1
+ import { state, getters, mutations, actions } from '../ui-context';
2
+
3
+ describe('ui-context store', () => {
4
+ describe('state', () => {
5
+ it('returns initial state with empty elements and zero idCounter', () => {
6
+ expect(state()).toStrictEqual({ idCounter: 0, elements: {} });
7
+ });
8
+
9
+ it('returns a fresh object on each call', () => {
10
+ const s1 = state();
11
+ const s2 = state();
12
+
13
+ expect(s1).not.toBe(s2);
14
+ });
15
+ });
16
+
17
+ describe('getters', () => {
18
+ describe('all', () => {
19
+ it('returns empty array when no elements exist', () => {
20
+ expect(getters.all(state())).toStrictEqual([]);
21
+ });
22
+
23
+ it('returns contexts sorted alphabetically by tag', () => {
24
+ const s = state();
25
+
26
+ s.elements = {
27
+ 1: { id: 1, context: { tag: 'z-tag', value: 'a' } },
28
+ 2: { id: 2, context: { tag: 'a-tag', value: 'b' } },
29
+ 3: { id: 3, context: { tag: 'm-tag', value: 'c' } },
30
+ } as any;
31
+
32
+ const result = getters.all(s);
33
+
34
+ expect(result.map((c: any) => c.tag)).toStrictEqual(['a-tag', 'm-tag', 'z-tag']);
35
+ });
36
+
37
+ it('sorts contexts without tag before tagged contexts', () => {
38
+ const s = state();
39
+
40
+ s.elements = {
41
+ 1: { id: 1, context: { tag: 'b-tag', value: 'a' } },
42
+ 2: { id: 2, context: { value: 'b' } },
43
+ } as any;
44
+
45
+ const result = getters.all(s);
46
+
47
+ expect(result[0]).toStrictEqual({ value: 'b' });
48
+ expect(result[1].tag).toBe('b-tag');
49
+ });
50
+
51
+ it('returns context objects, not full element wrappers', () => {
52
+ const context = { tag: 'my-tag', value: 'test-val' };
53
+ const s = state();
54
+
55
+ s.elements = { 1: { id: 1, context } } as any;
56
+
57
+ expect(getters.all(s)).toStrictEqual([context]);
58
+ });
59
+ });
60
+ });
61
+
62
+ describe('mutations', () => {
63
+ describe('add', () => {
64
+ it('adds element to state', () => {
65
+ const s = state();
66
+ const element = { id: 1, context: { tag: 'test', value: 'val' } };
67
+
68
+ mutations.add(s, element);
69
+
70
+ expect(s.elements[1]).toStrictEqual(element);
71
+ });
72
+
73
+ it('adds multiple elements without overwriting', () => {
74
+ const s = state();
75
+ const e1 = { id: 1, context: { tag: 'a', value: 1 } };
76
+ const e2 = { id: 2, context: { tag: 'b', value: 2 } };
77
+
78
+ mutations.add(s, e1);
79
+ mutations.add(s, e2);
80
+
81
+ expect(Object.keys(s.elements)).toStrictEqual(['1', '2']);
82
+ });
83
+ });
84
+
85
+ describe('update', () => {
86
+ it('updates context of existing element', () => {
87
+ const s = state();
88
+
89
+ s.elements[1] = { id: 1, context: { tag: 'test', value: 'original' } };
90
+ mutations.update(s, { id: 1, context: { tag: 'test', value: 'updated' } });
91
+
92
+ expect(s.elements[1].context).toStrictEqual({ tag: 'test', value: 'updated' });
93
+ });
94
+
95
+ it('does nothing when element id is not found', () => {
96
+ const s = state();
97
+ const element = { id: 1, context: { tag: 'test', value: 'val' } };
98
+
99
+ s.elements[1] = element;
100
+ mutations.update(s, { id: 999, context: { tag: 'other', value: 'new' } });
101
+
102
+ expect(s.elements[1]).toStrictEqual(element);
103
+ });
104
+ });
105
+
106
+ describe('remove', () => {
107
+ it('removes element from state by id', () => {
108
+ const s = state();
109
+ const element = { id: 1, context: { tag: 'test', value: 'val' } };
110
+
111
+ s.elements[1] = element;
112
+ mutations.remove(s, element);
113
+
114
+ expect(s.elements[1]).toBeUndefined();
115
+ });
116
+
117
+ it('leaves other elements intact when removing one', () => {
118
+ const s = state();
119
+ const e1 = { id: 1, context: { tag: 'a', value: 1 } };
120
+ const e2 = { id: 2, context: { tag: 'b', value: 2 } };
121
+
122
+ s.elements[1] = e1;
123
+ s.elements[2] = e2;
124
+ mutations.remove(s, e1);
125
+
126
+ expect(s.elements[2]).toStrictEqual(e2);
127
+ });
128
+ });
129
+ });
130
+
131
+ describe('actions', () => {
132
+ const makeStore = () => {
133
+ const s = state() as any;
134
+ const commit = jest.fn((mutation: string, payload: any) => {
135
+ (mutations as any)[mutation](s, payload);
136
+ });
137
+
138
+ return { s, commit };
139
+ };
140
+
141
+ describe('add', () => {
142
+ it.each([
143
+ {
144
+ desc: 'undefined context',
145
+ ctx: undefined as any,
146
+ },
147
+ {
148
+ desc: 'null context',
149
+ ctx: null as any,
150
+ },
151
+ {
152
+ desc: 'missing tag property',
153
+ ctx: { value: 'something' } as any,
154
+ },
155
+ {
156
+ desc: 'empty string tag',
157
+ ctx: { value: 'something', tag: '' } as any,
158
+ },
159
+ {
160
+ desc: 'missing value property',
161
+ ctx: { tag: 'test' } as any,
162
+ },
163
+ {
164
+ desc: 'explicit undefined value',
165
+ ctx: { tag: 'test', value: undefined } as any,
166
+ },
167
+ ])('throws for invalid context: $desc', ({ ctx }) => {
168
+ const { s, commit } = makeStore();
169
+
170
+ expect(() => actions.add({ commit, state: s }, ctx)).toThrow('[ui-context]');
171
+ });
172
+
173
+ it('commits add and returns generated id for valid context', () => {
174
+ const { s, commit } = makeStore();
175
+ const ctx = { tag: 'my-tag', value: 'my-value' };
176
+
177
+ const id = actions.add({ commit, state: s }, ctx);
178
+
179
+ expect(id).toBe('ctx-0');
180
+ expect(commit).toHaveBeenCalledWith('add', { id: 'ctx-0', context: ctx });
181
+ });
182
+
183
+ it('increments idCounter on successive calls', () => {
184
+ const { s, commit } = makeStore();
185
+ const ctx = { tag: 't', value: 'v' };
186
+
187
+ const id1 = actions.add({ commit, state: s }, ctx);
188
+ const id2 = actions.add({ commit, state: s }, ctx);
189
+
190
+ expect(id1).toBe('ctx-0');
191
+ expect(id2).toBe('ctx-1');
192
+ });
193
+
194
+ it.each([
195
+ {
196
+ desc: 'null value',
197
+ ctx: { tag: 'test', value: null },
198
+ },
199
+ {
200
+ desc: 'false value',
201
+ ctx: { tag: 'test', value: false },
202
+ },
203
+ {
204
+ desc: 'zero value',
205
+ ctx: { tag: 'test', value: 0 },
206
+ },
207
+ {
208
+ desc: 'empty string value',
209
+ ctx: { tag: 'test', value: '' },
210
+ },
211
+ ])('does not throw for valid context with $desc', ({ ctx }) => {
212
+ const { s, commit } = makeStore();
213
+
214
+ expect(() => actions.add({ commit, state: s }, ctx as any)).not.toThrow();
215
+ });
216
+ });
217
+
218
+ describe('update', () => {
219
+ it('throws when element id is not found in state', () => {
220
+ const { s, commit } = makeStore();
221
+
222
+ expect(() => actions.update({ commit, state: s }, { id: 42, context: { tag: 't', value: 'v' } } as any)).toThrow('[ui-context]');
223
+ });
224
+
225
+ it('commits update for existing element', () => {
226
+ const { s, commit } = makeStore();
227
+
228
+ s.elements[1] = { id: 1, context: { tag: 'test', value: 'original' } };
229
+ const updated = { id: 1, context: { tag: 'test', value: 'updated' } };
230
+
231
+ actions.update({ commit, state: s }, updated as any);
232
+
233
+ expect(commit).toHaveBeenCalledWith('update', updated);
234
+ });
235
+ });
236
+
237
+ describe('remove', () => {
238
+ it('throws when element id is not found in state', () => {
239
+ const { s, commit } = makeStore();
240
+
241
+ expect(() => actions.remove({ commit, state: s }, 999 as any)).toThrow('[ui-context]');
242
+ });
243
+
244
+ it('commits remove with the found element', () => {
245
+ const { s, commit } = makeStore();
246
+ const element = { id: 1, context: { tag: 'test', value: 'val' } };
247
+
248
+ s.elements[1] = element;
249
+ actions.remove({ commit, state: s }, 1 as any);
250
+
251
+ expect(commit).toHaveBeenCalledWith('remove', element);
252
+ });
253
+ });
254
+ });
255
+ });