@rancher/shell 3.0.8 → 3.0.9-rc.2

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 (192) hide show
  1. package/apis/intf/modal.ts +38 -0
  2. package/apis/intf/slide-in.ts +3 -1
  3. package/apis/shell/__tests__/slide-in.test.ts +36 -0
  4. package/apis/shell/slide-in.ts +5 -1
  5. package/assets/styles/base/_color.scss +1 -0
  6. package/assets/styles/base/_typography.scss +14 -5
  7. package/assets/styles/themes/_light.scss +1 -1
  8. package/assets/styles/themes/_modern.scss +1 -1
  9. package/assets/translations/en-us.yaml +94 -33
  10. package/assets/translations/zh-hans.yaml +0 -2
  11. package/components/ActionMenuShell.vue +4 -4
  12. package/components/CodeMirror.vue +4 -3
  13. package/components/DetailText.vue +54 -7
  14. package/components/Drawer/Chrome.vue +11 -4
  15. package/components/Drawer/DrawerCard.vue +19 -0
  16. package/components/Drawer/ResourceDetailDrawer/ConfigTab.vue +3 -11
  17. package/components/Drawer/ResourceDetailDrawer/__tests__/ConfigTab.test.ts +2 -2
  18. package/components/Drawer/ResourceDetailDrawer/index.vue +3 -20
  19. package/components/Drawer/types.ts +1 -0
  20. package/components/DynamicContent/DynamicContentCloseButton.vue +2 -2
  21. package/components/LocaleSelector.vue +1 -1
  22. package/components/Markdown.vue +1 -1
  23. package/components/PopoverCard.vue +3 -3
  24. package/components/Resource/Detail/Card/ExtrasCard.vue +39 -0
  25. package/components/Resource/Detail/Card/StateCard/__tests__/composables.test.ts +142 -0
  26. package/components/Resource/Detail/Card/StateCard/composables.ts +41 -11
  27. package/components/Resource/Detail/Card/StateCard/index.vue +3 -9
  28. package/components/Resource/Detail/Card/StateCard/types.ts +6 -0
  29. package/components/Resource/Detail/Card/{PodsCard → StatusCard}/index.vue +11 -10
  30. package/components/Resource/Detail/Card/__tests__/PodsCard.test.ts +24 -25
  31. package/components/Resource/Detail/Cards.vue +27 -0
  32. package/components/Resource/Detail/Masthead/__tests__/index.test.ts +70 -0
  33. package/components/Resource/Detail/Masthead/index.vue +5 -0
  34. package/components/Resource/Detail/Metadata/KeyValueRow.vue +4 -2
  35. package/components/Resource/Detail/ResourcePopover/ResourcePopoverCard.vue +2 -2
  36. package/components/Resource/Detail/ResourceRow.types.ts +14 -0
  37. package/components/Resource/Detail/ResourceRow.vue +23 -35
  38. package/components/Resource/Detail/StatusRow.vue +5 -2
  39. package/components/Resource/Detail/TitleBar/__tests__/composables.test.ts +38 -7
  40. package/components/Resource/Detail/TitleBar/__tests__/index.test.ts +106 -2
  41. package/components/Resource/Detail/TitleBar/composables.ts +2 -1
  42. package/components/Resource/Detail/TitleBar/index.vue +41 -6
  43. package/components/ResourceDetail/Masthead/__tests__/index.test.ts +49 -1
  44. package/components/ResourceDetail/Masthead/__tests__/latest.test.ts +85 -0
  45. package/components/ResourceDetail/Masthead/index.vue +1 -0
  46. package/components/ResourceDetail/Masthead/latest.vue +8 -1
  47. package/components/ResourceDetail/Masthead/legacy.vue +1 -1
  48. package/components/Setting.vue +1 -1
  49. package/components/SortableTable/index.vue +25 -0
  50. package/components/SortableTable/selection.js +25 -12
  51. package/components/SortableTable/sorting.js +1 -1
  52. package/components/Tabbed/Tab.vue +1 -0
  53. package/components/Tabbed/index.vue +29 -6
  54. package/components/Window/ContainerShell.vue +10 -13
  55. package/components/fleet/FleetClusterTargets/TargetsList.vue +47 -29
  56. package/components/fleet/FleetClusterTargets/index.vue +82 -29
  57. package/components/fleet/FleetClusters.vue +26 -12
  58. package/components/fleet/FleetGitRepoPaths.vue +2 -2
  59. package/components/fleet/FleetResources.vue +14 -0
  60. package/components/fleet/FleetValuesFrom.vue +2 -2
  61. package/components/fleet/__tests__/FleetClusterTargets.test.ts +531 -0
  62. package/components/fleet/__tests__/FleetClusters.test.ts +576 -0
  63. package/components/fleet/dashboard/ResourceDetails.vue +96 -123
  64. package/components/form/Conditions.vue +1 -15
  65. package/components/form/HookOption.vue +5 -0
  66. package/components/form/LabeledSelect.vue +1 -1
  67. package/components/form/LifecycleHooks.vue +2 -6
  68. package/components/form/ResourceLabeledSelect.vue +12 -1
  69. package/components/form/SeccompProfile.vue +113 -0
  70. package/components/form/Security.vue +244 -133
  71. package/components/form/__tests__/LabeledSelect.test.ts +1 -1
  72. package/components/form/__tests__/SeccompProfile.test.js +124 -0
  73. package/components/form/__tests__/Security.test.ts +125 -37
  74. package/components/formatter/Autoscaler.vue +2 -2
  75. package/components/formatter/FleetSummaryGraph.vue +4 -1
  76. package/components/nav/Group.vue +5 -0
  77. package/components/nav/Header.vue +3 -3
  78. package/components/nav/HeaderPageActionMenu.vue +1 -1
  79. package/components/nav/NamespaceFilter.vue +6 -6
  80. package/components/nav/NotificationCenter/index.vue +1 -1
  81. package/components/nav/TopLevelMenu.helper.ts +41 -16
  82. package/components/nav/TopLevelMenu.vue +45 -25
  83. package/components/nav/WorkspaceSwitcher.vue +1 -1
  84. package/components/nav/__tests__/TopLevelMenu.helper.test.ts +277 -0
  85. package/components/nav/__tests__/TopLevelMenu.test.ts +160 -4
  86. package/components/templates/default.vue +0 -3
  87. package/components/templates/home.vue +0 -3
  88. package/components/templates/plain.vue +0 -3
  89. package/composables/useClickOutside.ts +1 -1
  90. package/config/product/explorer.js +1 -2
  91. package/config/types.js +41 -8
  92. package/detail/__tests__/workload.test.ts +8 -16
  93. package/detail/catalog.cattle.io.app.vue +6 -0
  94. package/detail/fleet.cattle.io.cluster.vue +6 -0
  95. package/detail/workload/index.vue +7 -109
  96. package/edit/__tests__/projectsecret.test.ts +42 -0
  97. package/edit/auth/__tests__/oidc.test.ts +50 -0
  98. package/edit/auth/oidc.vue +68 -44
  99. package/edit/autoscaling.horizontalpodautoscaler/index.vue +140 -59
  100. package/edit/autoscaling.horizontalpodautoscaler/metrics-row.vue +41 -5
  101. package/edit/projectsecret.vue +29 -0
  102. package/edit/provisioning.cattle.io.cluster/__tests__/Basics.test.ts +89 -200
  103. package/edit/provisioning.cattle.io.cluster/__tests__/Networking.test.ts +58 -17
  104. package/edit/provisioning.cattle.io.cluster/rke2.vue +11 -0
  105. package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +3 -63
  106. package/edit/provisioning.cattle.io.cluster/tabs/networking/index.vue +82 -14
  107. package/edit/workload/__tests__/index.test.ts +122 -85
  108. package/edit/workload/index.vue +48 -29
  109. package/edit/workload/mixins/workload.js +85 -32
  110. package/list/catalog.cattle.io.clusterrepo.vue +1 -1
  111. package/list/projectsecret.vue +2 -2
  112. package/machine-config/__tests__/vmwarevsphere.test.ts +64 -0
  113. package/machine-config/amazonec2.vue +2 -2
  114. package/machine-config/vmwarevsphere.vue +58 -4
  115. package/mixins/__tests__/brand.spec.ts +18 -13
  116. package/mixins/__tests__/chart.test.ts +63 -0
  117. package/mixins/chart.js +56 -51
  118. package/models/__tests__/catalog.cattle.io.app.test.ts +33 -0
  119. package/models/__tests__/workload.test.ts +333 -0
  120. package/models/catalog.cattle.io.app.js +8 -0
  121. package/models/pod.js +14 -0
  122. package/models/secret.js +1 -1
  123. package/models/workload.js +93 -27
  124. package/package.json +4 -4
  125. package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +91 -0
  126. package/pages/c/_cluster/apps/charts/install.vue +4 -4
  127. package/pages/c/_cluster/explorer/EventsTable.vue +2 -2
  128. package/pages/c/_cluster/fleet/index.vue +18 -12
  129. package/pages/c/_cluster/manager/hostedprovider/index.vue +1 -19
  130. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +1 -1
  131. package/pages/c/_cluster/uiplugins/index.vue +1 -1
  132. package/plugins/dashboard-store/__tests__/resource-class.test.ts +234 -0
  133. package/plugins/dashboard-store/actions.js +9 -8
  134. package/plugins/dashboard-store/resource-class.js +97 -1
  135. package/plugins/steve/__tests__/revision.test.ts +84 -0
  136. package/plugins/steve/__tests__/steve-pagination-utils.test.ts +30 -0
  137. package/plugins/steve/__tests__/subscribe.spec.ts +134 -0
  138. package/plugins/steve/mutations.js +9 -0
  139. package/plugins/steve/revision.ts +26 -0
  140. package/plugins/steve/steve-pagination-utils.ts +6 -5
  141. package/plugins/steve/subscribe.js +211 -51
  142. package/plugins/subscribe-events.ts +2 -2
  143. package/rancher-components/Form/Checkbox/Checkbox.vue +13 -0
  144. package/rancher-components/LabeledTooltip/LabeledTooltip.vue +1 -1
  145. package/rancher-components/Pill/RcCounterBadge/RcCounterBadge.vue +1 -1
  146. package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.vue +3 -1
  147. package/rancher-components/Pill/RcStatusIndicator/RcStatusIndicator.vue +3 -1
  148. package/rancher-components/Pill/RcTag/RcTag.vue +1 -1
  149. package/rancher-components/Pill/index.ts +4 -0
  150. package/rancher-components/RcButton/RcButton.test.ts +53 -9
  151. package/rancher-components/RcButton/RcButton.vue +217 -25
  152. package/rancher-components/RcButton/types.ts +27 -1
  153. package/rancher-components/RcDropdown/RcDropdownMenu.vue +4 -4
  154. package/rancher-components/RcDropdown/types.ts +3 -3
  155. package/rancher-components/RcIcon/RcIcon.test.ts +42 -0
  156. package/rancher-components/RcIcon/RcIcon.vue +9 -6
  157. package/rancher-components/RcIcon/types.ts +13 -9
  158. package/rancher-components/utils/status.test.ts +10 -15
  159. package/rancher-components/utils/status.ts +5 -6
  160. package/store/aws.js +18 -12
  161. package/store/index.js +4 -8
  162. package/store/type-map.utils.ts +1 -1
  163. package/types/kube/kube-api.ts +29 -3
  164. package/types/rancher/steve.api.ts +40 -0
  165. package/types/shell/index.d.ts +99 -0
  166. package/types/store/dashboard-store.types.ts +29 -7
  167. package/types/store/pagination.types.ts +1 -0
  168. package/types/store/subscribe-events.types.ts +1 -0
  169. package/utils/__tests__/azure.test.ts +56 -0
  170. package/utils/__tests__/back-off.test.ts +364 -245
  171. package/utils/__tests__/error.test.ts +44 -0
  172. package/utils/__tests__/fleet.test.ts +8 -1
  173. package/utils/__tests__/pagination-wrapper.test.ts +167 -0
  174. package/utils/__tests__/version.test.ts +55 -1
  175. package/utils/azure.js +12 -0
  176. package/utils/back-off.ts +302 -69
  177. package/utils/cspAdaptor.ts +32 -14
  178. package/utils/dynamic-content/__tests__/index.test.ts +1 -1
  179. package/utils/dynamic-content/__tests__/new-release.test.ts +48 -7
  180. package/utils/dynamic-content/__tests__/support-notice.test.ts +1 -4
  181. package/utils/dynamic-content/index.ts +1 -6
  182. package/utils/dynamic-content/new-release.ts +5 -3
  183. package/utils/dynamic-content/types.d.ts +0 -1
  184. package/utils/error.js +9 -0
  185. package/utils/fleet.ts +2 -2
  186. package/utils/inactivity.ts +2 -3
  187. package/utils/pagination-wrapper.ts +101 -17
  188. package/utils/validators/formRules/index.ts +3 -0
  189. package/utils/version.js +38 -0
  190. package/components/auth/AzureWarning.vue +0 -77
  191. /package/components/Resource/Detail/{Card/PodsCard/Bubble.vue → Bubble.vue} +0 -0
  192. /package/components/Resource/Detail/Card/{PodsCard → StatusCard}/composable.ts +0 -0
@@ -0,0 +1,576 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import FleetClusters from '@shell/components/fleet/FleetClusters.vue';
3
+ import ResourceTable from '@shell/components/ResourceTable.vue';
4
+
5
+ describe('component: FleetClusters', () => {
6
+ const mockStore = {
7
+ getters: {
8
+ 'i18n/t': (key: string) => key,
9
+ 'management/schemaFor': () => ({ id: 'fleet.cattle.io.cluster' }),
10
+ 'type-map/labelFor': (schema: any, count?: number) => count === 99 ? 'Clusters' : 'Cluster',
11
+ 'prefs/get': () => false,
12
+ currentProduct: () => ({ inStore: 'cluster' }),
13
+ 'type-map/optionsFor': () => ({}),
14
+ 'type-map/headersFor': () => [],
15
+ defaultClusterId: () => 'local',
16
+ },
17
+ dispatch: jest.fn(),
18
+ };
19
+
20
+ const mockRow = {
21
+ id: 'test-cluster',
22
+ customLabels: [],
23
+ displayCustomLabels: false,
24
+ stateDescription: 'Active',
25
+ nameDisplay: 'test-cluster',
26
+ reposReady: '1/1',
27
+ bundleInfo: {
28
+ ready: 1,
29
+ total: 1
30
+ },
31
+ helmOpsReady: '0/0',
32
+ lastSeen: new Date().toISOString(),
33
+ stateObj: {
34
+ name: 'active',
35
+ transitioning: false,
36
+ error: false
37
+ }
38
+ };
39
+
40
+ const createWrapper = (props: any = {}, slots = {}) => {
41
+ const defaultRows = props.rows || [mockRow];
42
+
43
+ return mount(FleetClusters, {
44
+ props: {
45
+ rows: defaultRows,
46
+ schema: { id: 'fleet.cattle.io.cluster' },
47
+ ...props,
48
+ },
49
+ slots,
50
+ global: {
51
+ mocks: { $store: mockStore },
52
+ components: { ResourceTable },
53
+ stubs: {
54
+ ResourceTable: {
55
+ template: `
56
+ <div class="resource-table">
57
+ <slot
58
+ name="additional-sub-row"
59
+ :fullColspan="10"
60
+ :row="rows[0]"
61
+ :onRowMouseEnter="() => {}"
62
+ :onRowMouseLeave="() => {}"
63
+ :showSubRow="rows[0].stateDescription"
64
+ />
65
+ </div>
66
+ `,
67
+ props: ['rows', 'schema', 'headers', 'subRows', 'loading', 'useQueryParamsForSimpleFiltering', 'keyField']
68
+ },
69
+ Tag: { template: '<span class="tag"><slot /></span>' }
70
+ }
71
+ }
72
+ });
73
+ };
74
+
75
+ describe('headers configuration', () => {
76
+ it('should include all required column headers', () => {
77
+ const wrapper = createWrapper();
78
+ const headers = wrapper.vm.headers;
79
+
80
+ expect(headers).toHaveLength(8);
81
+ expect(headers.map((h: any) => h.name || h)).toContain('state');
82
+ expect(headers.map((h: any) => h.name || h)).toContain('name');
83
+ expect(headers.some((h: any) => h.name === 'reposReady')).toBe(true);
84
+ expect(headers.some((h: any) => h.name === 'helmOpsReady')).toBe(true);
85
+ expect(headers.some((h: any) => h.name === 'bundlesReady')).toBe(true);
86
+ expect(headers.some((h: any) => h.name === 'lastSeen')).toBe(true);
87
+ });
88
+
89
+ it('should configure reposReady column correctly', () => {
90
+ const wrapper = createWrapper();
91
+ const reposReady = wrapper.vm.headers.find((h: any) => h.name === 'reposReady');
92
+
93
+ expect(reposReady.labelKey).toBe('tableHeaders.reposReady');
94
+ expect(reposReady.value).toBe('status.readyGitRepos');
95
+ expect(reposReady.search).toBe(false);
96
+ });
97
+
98
+ it('should configure helmOpsReady column correctly', () => {
99
+ const wrapper = createWrapper();
100
+ const helmOpsReady = wrapper.vm.headers.find((h: any) => h.name === 'helmOpsReady');
101
+
102
+ expect(helmOpsReady.labelKey).toBe('tableHeaders.helmOpsReady');
103
+ expect(helmOpsReady.value).toBe('status.readyHelmOps');
104
+ expect(helmOpsReady.search).toBe(false);
105
+ });
106
+
107
+ it('should configure bundlesReady column correctly', () => {
108
+ const wrapper = createWrapper();
109
+ const bundlesReady = wrapper.vm.headers.find((h: any) => h.name === 'bundlesReady');
110
+
111
+ expect(bundlesReady.labelKey).toBe('tableHeaders.bundlesReady');
112
+ expect(bundlesReady.value).toBe('status.display.readyBundles');
113
+ expect(bundlesReady.search).toBe(false);
114
+ });
115
+
116
+ it('should configure lastSeen column with LiveDate formatter', () => {
117
+ const wrapper = createWrapper();
118
+ const lastSeen = wrapper.vm.headers.find((h: any) => h.name === 'lastSeen');
119
+
120
+ expect(lastSeen.formatter).toBe('LiveDate');
121
+ expect(lastSeen.formatterOpts).toStrictEqual({ addSuffix: true });
122
+ expect(lastSeen.width).toBe(120);
123
+ });
124
+ });
125
+
126
+ describe('additional-sub-row slot', () => {
127
+ it('should render labels row when customLabels exist', () => {
128
+ const rows = [{
129
+ customLabels: ['label1', 'label2', 'label3'],
130
+ displayCustomLabels: false
131
+ }];
132
+
133
+ const wrapper = createWrapper({ rows });
134
+
135
+ expect(wrapper.find('.labels-row').exists()).toBe(true);
136
+ });
137
+
138
+ it('should display up to 7 labels by default', () => {
139
+ const rows = [{
140
+ customLabels: ['label1', 'label2', 'label3', 'label4', 'label5', 'label6', 'label7', 'label8'],
141
+ displayCustomLabels: false
142
+ }];
143
+
144
+ const wrapper = createWrapper({ rows });
145
+ const tags = wrapper.findAll('.tag');
146
+
147
+ expect(tags).toHaveLength(7);
148
+ });
149
+
150
+ it('should display all labels when displayCustomLabels is true', () => {
151
+ const rows = [{
152
+ customLabels: ['label1', 'label2', 'label3', 'label4', 'label5', 'label6', 'label7', 'label8'],
153
+ displayCustomLabels: true
154
+ }];
155
+
156
+ const wrapper = createWrapper({ rows });
157
+ const tags = wrapper.findAll('.tag');
158
+
159
+ expect(tags).toHaveLength(8);
160
+ });
161
+
162
+ it('should show toggle link when more than 7 labels', () => {
163
+ const rows = [{
164
+ customLabels: ['label1', 'label2', 'label3', 'label4', 'label5', 'label6', 'label7', 'label8'],
165
+ displayCustomLabels: false
166
+ }];
167
+
168
+ const wrapper = createWrapper({ rows });
169
+ const toggleLink = wrapper.find('a[href="#"]');
170
+
171
+ expect(toggleLink.exists()).toBe(true);
172
+ });
173
+
174
+ it('should not show toggle link when 7 or fewer labels', () => {
175
+ const rows = [{
176
+ customLabels: ['label1', 'label2', 'label3'],
177
+ displayCustomLabels: false
178
+ }];
179
+
180
+ const wrapper = createWrapper({ rows });
181
+ const toggleLink = wrapper.find('a[href="#"]');
182
+
183
+ expect(toggleLink.exists()).toBe(false);
184
+ });
185
+
186
+ it('should toggle displayCustomLabels when clicking show/hide link', async() => {
187
+ const rows = [{
188
+ customLabels: ['label1', 'label2', 'label3', 'label4', 'label5', 'label6', 'label7', 'label8'],
189
+ displayCustomLabels: false
190
+ }];
191
+
192
+ const wrapper = createWrapper({ rows });
193
+ const toggleLink = wrapper.find('a[href="#"]');
194
+
195
+ await toggleLink.trigger('click');
196
+
197
+ expect(rows[0].displayCustomLabels).toBe(true);
198
+ });
199
+
200
+ it('should render empty cell when no custom labels', () => {
201
+ const rows = [{
202
+ customLabels: [],
203
+ displayCustomLabels: false
204
+ }];
205
+
206
+ const wrapper = createWrapper({ rows });
207
+ const labelsRow = wrapper.find('.labels-row');
208
+
209
+ expect(labelsRow.exists()).toBe(true);
210
+ expect(labelsRow.findAll('.tag')).toHaveLength(0);
211
+ });
212
+
213
+ it('should apply has-sub-row class when showSubRow is true', () => {
214
+ const rows = [{
215
+ customLabels: ['label1'],
216
+ displayCustomLabels: false,
217
+ stateDescription: 'Active',
218
+ }];
219
+
220
+ const wrapper = createWrapper({ rows });
221
+ const labelsRow = wrapper.find('.labels-row');
222
+
223
+ expect(labelsRow.classes()).toContain('has-sub-row');
224
+ });
225
+ });
226
+
227
+ describe('toggleCustomLabels method', () => {
228
+ it('should toggle displayCustomLabels property', () => {
229
+ const wrapper = createWrapper();
230
+ const row = { displayCustomLabels: false };
231
+
232
+ wrapper.vm.toggleCustomLabels(row);
233
+ expect(row.displayCustomLabels).toBe(true);
234
+
235
+ wrapper.vm.toggleCustomLabels(row);
236
+ expect(row.displayCustomLabels).toBe(false);
237
+ });
238
+ });
239
+
240
+ describe('props validation', () => {
241
+ it('should accept required rows prop', () => {
242
+ const rows = [{ id: '1', name: 'cluster1' }];
243
+ const wrapper = createWrapper({ rows });
244
+
245
+ expect(wrapper.props('rows')).toStrictEqual(rows);
246
+ });
247
+
248
+ it('should accept schema prop', () => {
249
+ const schema = { id: 'fleet.cattle.io.cluster' };
250
+ const wrapper = createWrapper({ schema });
251
+
252
+ expect(wrapper.props('schema')).toStrictEqual(schema);
253
+ });
254
+
255
+ it('should accept loading prop', () => {
256
+ const wrapper = createWrapper({ loading: true });
257
+
258
+ expect(wrapper.props('loading')).toBe(true);
259
+ });
260
+
261
+ it('should accept useQueryParamsForSimpleFiltering prop', () => {
262
+ const wrapper = createWrapper({ useQueryParamsForSimpleFiltering: true });
263
+
264
+ expect(wrapper.props('useQueryParamsForSimpleFiltering')).toBe(true);
265
+ });
266
+ });
267
+
268
+ describe('computed MANAGEMENT_CLUSTER property', () => {
269
+ it('should return MANAGEMENT.CLUSTER', () => {
270
+ const wrapper = createWrapper();
271
+
272
+ expect(wrapper.vm.MANAGEMENT_CLUSTER).toBe('management.cattle.io.cluster');
273
+ });
274
+ });
275
+
276
+ describe('pagingParams computed property', () => {
277
+ it('should return singular and plural labels', () => {
278
+ const wrapper = createWrapper();
279
+ const params = wrapper.vm.pagingParams;
280
+
281
+ expect(params.singularLabel).toBe('Cluster');
282
+ expect(params.pluralLabel).toBe('Clusters');
283
+ });
284
+ });
285
+
286
+ describe('resourceTable integration', () => {
287
+ it('should pass correct props to ResourceTable', () => {
288
+ const rows = [{ id: '1', customLabels: [] }];
289
+ const schema = { id: 'fleet.cattle.io.cluster' };
290
+ const wrapper = createWrapper({
291
+ rows,
292
+ schema,
293
+ loading: true
294
+ });
295
+
296
+ const resourceTable = wrapper.find('.resource-table');
297
+
298
+ expect(resourceTable.exists()).toBe(true);
299
+ expect(wrapper.vm.headers).toBeDefined();
300
+ expect(wrapper.vm.headers).toHaveLength(8);
301
+ expect(wrapper.vm.MANAGEMENT_CLUSTER).toBe('management.cattle.io.cluster');
302
+ });
303
+ });
304
+
305
+ describe('label display behavior', () => {
306
+ it('should show "fleet.cluster.labels" text when labels exist', () => {
307
+ const rows = [{
308
+ customLabels: ['label1'],
309
+ displayCustomLabels: false
310
+ }];
311
+
312
+ const wrapper = createWrapper({ rows });
313
+
314
+ expect(wrapper.text()).toContain('fleet.cluster.labels');
315
+ });
316
+
317
+ it('should render each label in a Tag component', () => {
318
+ const rows = [{
319
+ customLabels: ['env:prod', 'team:backend', 'region:us-west'],
320
+ displayCustomLabels: false
321
+ }];
322
+
323
+ const wrapper = createWrapper({ rows });
324
+ const tags = wrapper.findAll('.tag');
325
+
326
+ expect(tags[0].text()).toBe('env:prod');
327
+ expect(tags[1].text()).toBe('team:backend');
328
+ expect(tags[2].text()).toBe('region:us-west');
329
+ });
330
+
331
+ it('should handle labels with special characters', () => {
332
+ const rows = [{
333
+ customLabels: ['app.kubernetes.io/name=nginx', 'version=1.0.0-beta'],
334
+ displayCustomLabels: false
335
+ }];
336
+
337
+ const wrapper = createWrapper({ rows });
338
+ const tags = wrapper.findAll('.tag');
339
+
340
+ expect(tags[0].text()).toBe('app.kubernetes.io/name=nginx');
341
+ expect(tags[1].text()).toBe('version=1.0.0-beta');
342
+ });
343
+ });
344
+
345
+ describe('mouse events', () => {
346
+ it('should call onRowMouseEnter when mouse enters labels row', async() => {
347
+ const rows = [{
348
+ customLabels: ['label1'],
349
+ displayCustomLabels: false
350
+ }];
351
+
352
+ const wrapper = createWrapper({ rows });
353
+ const labelsRow = wrapper.find('.labels-row');
354
+
355
+ await labelsRow.trigger('mouseenter');
356
+
357
+ // Event binding is tested through the template
358
+ expect(labelsRow.exists()).toBe(true);
359
+ });
360
+
361
+ it('should call onRowMouseLeave when mouse leaves labels row', async() => {
362
+ const rows = [{
363
+ customLabels: ['label1'],
364
+ displayCustomLabels: false
365
+ }];
366
+
367
+ const wrapper = createWrapper({ rows });
368
+ const labelsRow = wrapper.find('.labels-row');
369
+
370
+ await labelsRow.trigger('mouseleave');
371
+
372
+ // Event binding is tested through the template
373
+ expect(labelsRow.exists()).toBe(true);
374
+ });
375
+ });
376
+
377
+ describe('edge cases', () => {
378
+ it('should handle undefined customLabels gracefully', () => {
379
+ const rows = [{
380
+ customLabels: undefined,
381
+ displayCustomLabels: false
382
+ }];
383
+
384
+ // Component should handle this without crashing
385
+ expect(() => createWrapper({ rows })).not.toThrow();
386
+ });
387
+
388
+ it('should handle exactly 7 labels without toggle link', () => {
389
+ const rows = [{
390
+ customLabels: ['l1', 'l2', 'l3', 'l4', 'l5', 'l6', 'l7'],
391
+ displayCustomLabels: false
392
+ }];
393
+
394
+ const wrapper = createWrapper({ rows });
395
+ const tags = wrapper.findAll('.tag');
396
+ const toggleLink = wrapper.find('a[href="#"]');
397
+
398
+ expect(tags).toHaveLength(7);
399
+ expect(toggleLink.exists()).toBe(false);
400
+ });
401
+
402
+ it('should handle exactly 8 labels with toggle link', () => {
403
+ const rows = [{
404
+ customLabels: ['l1', 'l2', 'l3', 'l4', 'l5', 'l6', 'l7', 'l8'],
405
+ displayCustomLabels: false
406
+ }];
407
+
408
+ const wrapper = createWrapper({ rows });
409
+ const tags = wrapper.findAll('.tag');
410
+ const toggleLink = wrapper.find('a[href="#"]');
411
+
412
+ expect(tags).toHaveLength(7); // Only first 7 shown initially
413
+ expect(toggleLink.exists()).toBe(true);
414
+ });
415
+
416
+ it('should handle large number of labels', () => {
417
+ const rows = [{
418
+ customLabels: Array.from({ length: 50 }, (_, i) => `label${ i + 1 }`),
419
+ displayCustomLabels: false
420
+ }];
421
+
422
+ const wrapper = createWrapper({ rows });
423
+ const tags = wrapper.findAll('.tag');
424
+
425
+ expect(tags).toHaveLength(7);
426
+ });
427
+
428
+ it('should show all labels when displayCustomLabels is true for large set', () => {
429
+ const rows = [{
430
+ customLabels: Array.from({ length: 50 }, (_, i) => `label${ i + 1 }`),
431
+ displayCustomLabels: true
432
+ }];
433
+
434
+ const wrapper = createWrapper({ rows });
435
+ const tags = wrapper.findAll('.tag');
436
+
437
+ expect(tags).toHaveLength(50);
438
+ });
439
+ });
440
+
441
+ describe('styling classes', () => {
442
+ it('should apply labels-row class', () => {
443
+ const rows = [{
444
+ customLabels: ['label1'],
445
+ displayCustomLabels: false
446
+ }];
447
+
448
+ const wrapper = createWrapper({ rows });
449
+
450
+ expect(wrapper.find('.labels-row').exists()).toBe(true);
451
+ });
452
+
453
+ it('should apply additional-sub-row class', () => {
454
+ const rows = [{
455
+ customLabels: ['label1'],
456
+ displayCustomLabels: false
457
+ }];
458
+
459
+ const wrapper = createWrapper({ rows });
460
+
461
+ expect(wrapper.find('.additional-sub-row').exists()).toBe(true);
462
+ });
463
+
464
+ it('should apply mt-5 class to labels container', () => {
465
+ const rows = [{
466
+ customLabels: ['label1'],
467
+ displayCustomLabels: false
468
+ }];
469
+
470
+ const wrapper = createWrapper({ rows });
471
+
472
+ expect(wrapper.find('.mt-5').exists()).toBe(true);
473
+ });
474
+
475
+ it('should apply mr-5 class to label tags', () => {
476
+ const rows = [{
477
+ customLabels: ['label1'],
478
+ displayCustomLabels: false
479
+ }];
480
+
481
+ const wrapper = createWrapper({ rows });
482
+
483
+ expect(wrapper.find('.mr-5').exists()).toBe(true);
484
+ });
485
+ });
486
+
487
+ // Tests for gap removal fix - Issue #16502
488
+ describe('additional-sub-row properties passed properly', () => {
489
+ it('should render additional-sub-row when customLabels is empty', () => {
490
+ const rows = [{
491
+ customLabels: [],
492
+ displayCustomLabels: false
493
+ }];
494
+
495
+ const wrapper = createWrapper({ rows });
496
+ const additionalSubRow = wrapper.find('.labels-row.additional-sub-row');
497
+
498
+ // Should exist when no custom labels, it should be there to have the border
499
+ expect(additionalSubRow.exists()).toBe(true);
500
+ });
501
+
502
+ it('should render additional-sub-row when customLabels is undefined', () => {
503
+ const rows = [{
504
+ customLabels: undefined,
505
+ displayCustomLabels: false
506
+ }];
507
+
508
+ const wrapper = createWrapper({ rows });
509
+ const additionalSubRow = wrapper.find('.labels-row.additional-sub-row');
510
+
511
+ // Should exist when no custom labels, it should be there to have the border
512
+ expect(additionalSubRow.exists()).toBe(true);
513
+ });
514
+
515
+ it('should render additional-sub-row when customLabels is null', () => {
516
+ const rows = [{
517
+ customLabels: null,
518
+ displayCustomLabels: false
519
+ }];
520
+
521
+ const wrapper = createWrapper({ rows });
522
+ const additionalSubRow = wrapper.find('.labels-row.additional-sub-row');
523
+
524
+ // Should not exist when customLabels is null
525
+ expect(additionalSubRow.exists()).toBe(true);
526
+ });
527
+
528
+ it('should handle mixed scenarios - some clusters with labels, some without', () => {
529
+ const mixedRows = [
530
+ {
531
+ id: 'cluster-1',
532
+ customLabels: ['env:prod'],
533
+ displayCustomLabels: false
534
+ },
535
+ {
536
+ id: 'cluster-2',
537
+ customLabels: [],
538
+ displayCustomLabels: false
539
+ },
540
+ {
541
+ id: 'cluster-3',
542
+ customLabels: ['team:backend', 'region:us-west'],
543
+ displayCustomLabels: false
544
+ }
545
+ ];
546
+
547
+ // We need to create separate wrappers since our stub only shows one row
548
+ const wrapper1 = createWrapper({ rows: [mixedRows[0]] });
549
+ const wrapper2 = createWrapper({ rows: [mixedRows[1]] });
550
+ const wrapper3 = createWrapper({ rows: [mixedRows[2]] });
551
+
552
+ // Cluster 1: has labels -> should render additional-sub-row
553
+ expect(wrapper1.find('tr.additional-sub-row').exists()).toBe(true);
554
+
555
+ // Cluster 2: no labels -> should render additional-sub-row
556
+ expect(wrapper2.find('tr.additional-sub-row').exists()).toBe(true);
557
+
558
+ // Cluster 3: has labels -> should render additional-sub-row
559
+ expect(wrapper3.find('tr.additional-sub-row').exists()).toBe(true);
560
+ });
561
+
562
+ it('should render additional-sub-row without the has-sub-row when there is no stateDescription', () => {
563
+ const rows = [{
564
+ customLabels: null,
565
+ displayCustomLabels: false,
566
+ stateDescription: null,
567
+ }];
568
+
569
+ const wrapper = createWrapper({ rows });
570
+ const additionalSubRow = wrapper.find('.labels-row.additional-sub-row.has-sub-row');
571
+
572
+ // Should not exist when customLabels is null
573
+ expect(additionalSubRow.exists()).toBe(false);
574
+ });
575
+ });
576
+ });