@rancher/shell 3.0.12-rc.3 → 3.0.12-rc.5

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 (315) hide show
  1. package/assets/styles/global/_button.scss +1 -1
  2. package/assets/styles/global/_layout.scss +4 -0
  3. package/assets/translations/en-us.yaml +183 -51
  4. package/assets/translations/zh-hans.yaml +1 -7
  5. package/chart/monitoring/ClusterSelector.vue +0 -21
  6. package/chart/monitoring/prometheus/index.vue +6 -3
  7. package/components/ActionDropdownShell.vue +5 -3
  8. package/components/ButtonGroup.vue +26 -1
  9. package/components/CruResource.vue +212 -16
  10. package/components/ExplorerMembers.vue +8 -4
  11. package/components/ExplorerProjectsNamespaces.vue +10 -6
  12. package/components/GrowlManager.vue +4 -0
  13. package/components/MgmtNodeList.vue +184 -0
  14. package/components/PromptRestore.vue +93 -32
  15. package/components/Questions/index.vue +1 -0
  16. package/components/Resource/Detail/Card/StateCard/__tests__/composables.test.ts +90 -1
  17. package/components/Resource/Detail/Card/StateCard/composables.ts +57 -87
  18. package/components/Resource/Detail/Card/StatusCard/__tests__/StatusCard.test.ts +61 -0
  19. package/components/Resource/Detail/Card/StatusCard/index.vue +61 -15
  20. package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +2 -0
  21. package/components/Resource/Detail/Metadata/KeyValue.vue +5 -2
  22. package/components/Resource/Detail/Metadata/KeyValueRow.vue +2 -6
  23. package/components/ResourceDetail/index.vue +1 -1
  24. package/components/ResourceList/Masthead.vue +7 -1
  25. package/components/ResourceList/index.vue +82 -1
  26. package/components/ResourceTable.vue +1 -0
  27. package/components/RichTranslation.vue +5 -2
  28. package/components/Setting.vue +1 -0
  29. package/components/SortableTable/index.vue +4 -3
  30. package/components/SubtleLink.vue +31 -6
  31. package/components/Tabbed/Tab.vue +29 -3
  32. package/components/Tabbed/index.vue +25 -3
  33. package/components/TableOfContents/TableOfContents.vue +109 -0
  34. package/components/TableOfContents/composables.ts +258 -0
  35. package/components/Window/ContainerShell.vue +21 -11
  36. package/components/Window/__tests__/ContainerShell.test.ts +107 -37
  37. package/components/Wizard.vue +23 -5
  38. package/components/__tests__/ButtonGroup.test.ts +56 -0
  39. package/components/__tests__/PromptRestore.test.ts +169 -19
  40. package/components/fleet/AppCoChartGrid.vue +401 -0
  41. package/components/fleet/AppCoEmptyState.vue +127 -0
  42. package/components/fleet/AppCoPageHeader.vue +119 -0
  43. package/components/fleet/AppCoVersionSelect.vue +70 -0
  44. package/components/fleet/FleetClusterTargets/ClusterSelectionFields.vue +217 -0
  45. package/components/fleet/FleetClusterTargets/TargetsList.vue +123 -35
  46. package/components/fleet/FleetClusterTargets/index.vue +189 -146
  47. package/components/fleet/FleetIntro.vue +7 -3
  48. package/components/fleet/FleetNoWorkspaces.vue +7 -3
  49. package/components/fleet/FleetSecretSelector.vue +5 -3
  50. package/components/fleet/FleetValuesFrom.vue +8 -2
  51. package/components/fleet/GitRepoAdvancedTab.vue +1 -0
  52. package/components/fleet/GitRepoMetadataTab.vue +5 -0
  53. package/components/fleet/GitRepoTargetTab.vue +0 -2
  54. package/components/fleet/HelmOpAdvancedTab.vue +19 -53
  55. package/components/fleet/HelmOpAppCoConfigTab.vue +597 -0
  56. package/components/fleet/HelmOpAppCoResourcesSection.vue +162 -0
  57. package/components/fleet/HelmOpMetadataTab.vue +5 -0
  58. package/components/fleet/HelmOpResourcesSection.vue +82 -0
  59. package/components/fleet/HelmOpTargetOptionsSection.vue +89 -0
  60. package/components/fleet/HelmOpTargetTab.vue +64 -60
  61. package/components/fleet/HelmOpValuesTab.vue +129 -105
  62. package/components/fleet/__tests__/AppCoEmptyState.test.ts +71 -0
  63. package/components/fleet/__tests__/AppCoVersionSelect.test.ts +36 -0
  64. package/components/fleet/__tests__/ClusterSelectionFields.test.ts +62 -0
  65. package/components/fleet/__tests__/FleetClusterTargets.test.ts +253 -0
  66. package/components/fleet/__tests__/FleetSecretSelector.test.ts +16 -0
  67. package/components/fleet/__tests__/FleetValuesFrom.test.ts +44 -0
  68. package/components/fleet/__tests__/HelmOpAppCoConfigTab.test.ts +59 -0
  69. package/components/fleet/__tests__/HelmOpAppCoResourcesSection.test.ts +62 -0
  70. package/components/fleet/__tests__/HelmOpResourcesSection.test.ts +43 -0
  71. package/components/fleet/__tests__/HelmOpTargetOptionsSection.test.ts +34 -0
  72. package/components/fleet/__tests__/HelmOpValuesTab.test.ts +39 -0
  73. package/components/fleet/__tests__/__snapshots__/AppCoEmptyState.test.ts.snap +97 -0
  74. package/components/fleet/__tests__/__snapshots__/AppCoVersionSelect.test.ts.snap +30 -0
  75. package/components/fleet/__tests__/__snapshots__/ClusterSelectionFields.test.ts.snap +209 -0
  76. package/components/fleet/__tests__/__snapshots__/HelmOpTargetOptionsSection.test.ts.snap +140 -0
  77. package/components/fleet/dashboard/Empty.vue +8 -4
  78. package/components/fleet/dashboard/ResourceCard.vue +28 -0
  79. package/components/fleet/dashboard/ResourceDetails.vue +28 -0
  80. package/components/fleet/dashboard/__tests__/ResourceCard.test.ts +87 -0
  81. package/components/form/ArrayList.vue +61 -4
  82. package/components/form/FileSelector.vue +39 -1
  83. package/components/form/KeyValue.vue +23 -2
  84. package/components/form/LabeledSelect.vue +39 -1
  85. package/components/form/Labels.vue +22 -3
  86. package/components/form/NameNsDescription.vue +13 -5
  87. package/components/form/PrivateRegistry.constants.ts +7 -0
  88. package/components/form/PrivateRegistry.vue +253 -18
  89. package/components/form/ResourceTabs/index.vue +1 -0
  90. package/components/form/SelectOrCreateAuthSecret.vue +140 -17
  91. package/components/form/__tests__/FileSelector.test.ts +23 -0
  92. package/components/form/__tests__/NameNsDescription.test.ts +75 -0
  93. package/components/form/__tests__/PrivateRegistry.test.ts +463 -73
  94. package/components/form/__tests__/SelectOrCreateAuthSecret.test.ts +122 -0
  95. package/components/formatter/EtcdSnapshotName.vue +73 -0
  96. package/components/formatter/InternalExternalIP.vue +10 -4
  97. package/components/formatter/ServiceTargets.vue +26 -7
  98. package/components/formatter/__tests__/InternalExternalIP.test.ts +132 -0
  99. package/components/formatter/__tests__/ServiceTargets.test.ts +412 -0
  100. package/components/nav/Header.vue +12 -1
  101. package/components/nav/TopLevelMenu.vue +7 -2
  102. package/components/nav/__tests__/Header.test.ts +15 -0
  103. package/components/nav/__tests__/TopLevelMenu.test.ts +120 -2
  104. package/components/templates/default.vue +16 -4
  105. package/components/templates/home.vue +9 -4
  106. package/components/templates/plain.vue +9 -4
  107. package/composables/useHelmOpResources.test.ts +56 -0
  108. package/composables/useHelmOpResources.ts +32 -0
  109. package/composables/useStateColor.test.ts +325 -0
  110. package/composables/useStateColor.ts +128 -0
  111. package/config/features.js +1 -0
  112. package/config/home-links.js +1 -1
  113. package/config/labels-annotations.js +3 -0
  114. package/config/product/explorer.js +17 -4
  115. package/config/product/manager.js +8 -0
  116. package/config/router/index.js +16 -0
  117. package/config/router/navigation-guards/__tests__/authentication.test.ts +130 -0
  118. package/config/router/navigation-guards/authentication.js +10 -4
  119. package/config/router/routes.js +20 -6
  120. package/config/secret.ts +10 -0
  121. package/config/settings.ts +6 -4
  122. package/config/table-headers.js +3 -4
  123. package/config/types.js +16 -0
  124. package/core/plugin-products-base.ts +3 -3
  125. package/core/plugin-types.ts +83 -30
  126. package/core/plugin.ts +3 -0
  127. package/core/types-provisioning.ts +34 -1
  128. package/core/types.ts +15 -2
  129. package/detail/__tests__/provisioning.cattle.io.cluster.test.ts +114 -0
  130. package/detail/__tests__/workload.test.ts +3 -152
  131. package/detail/catalog.cattle.io.clusterrepo.vue +1 -1
  132. package/detail/provisioning.cattle.io.cluster.vue +109 -7
  133. package/detail/workload/index.vue +12 -55
  134. package/dialog/RotateEncryptionKeyDialog.vue +33 -9
  135. package/dialog/__tests__/RotateEncryptionKeyDialog.test.ts +78 -0
  136. package/edit/__tests__/catalog.cattle.io.clusterrepo.test.ts +248 -0
  137. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +92 -0
  138. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +206 -0
  139. package/edit/__tests__/management.cattle.io.setting.test.ts +2 -1
  140. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/General.test.ts.snap +6 -0
  141. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/index.test.ts.snap +1 -0
  142. package/edit/auth/__tests__/azuread.test.ts +34 -9
  143. package/edit/auth/__tests__/github.test.ts +234 -0
  144. package/edit/auth/__tests__/oidc.test.ts +26 -6
  145. package/edit/auth/__tests__/saml.test.ts +196 -0
  146. package/edit/auth/azuread.vue +128 -95
  147. package/edit/auth/github.vue +72 -13
  148. package/edit/auth/ldap/__tests__/index.test.ts +206 -0
  149. package/edit/auth/ldap/config.vue +8 -0
  150. package/edit/auth/ldap/index.vue +75 -1
  151. package/edit/auth/oidc.vue +119 -73
  152. package/edit/auth/saml.vue +76 -12
  153. package/edit/catalog.cattle.io.clusterrepo.vue +140 -32
  154. package/edit/compliance.cattle.io.clusterscanprofile.vue +39 -41
  155. package/edit/fleet.cattle.io.gitrepo.vue +70 -16
  156. package/edit/fleet.cattle.io.helmop.vue +542 -141
  157. package/edit/helm.cattle.io.projecthelmchart.vue +1 -0
  158. package/edit/{management.cattle.io.setting.vue → management.cattle.io.setting/index.vue} +32 -9
  159. package/edit/management.cattle.io.setting/system-default-registry-pull-secrets.vue +81 -0
  160. package/edit/management.cattle.io.user.vue +5 -2
  161. package/edit/provisioning.cattle.io.cluster/SelectCredential.vue +3 -12
  162. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +18 -0
  163. package/edit/provisioning.cattle.io.cluster/rke2.vue +89 -11
  164. package/edit/provisioning.cattle.io.cluster/tabs/MachinePool.vue +11 -0
  165. package/edit/provisioning.cattle.io.cluster/tabs/etcd/index.vue +0 -1
  166. package/edit/provisioning.cattle.io.cluster/tabs/registries/index.vue +14 -55
  167. package/list/group.principal.vue +5 -4
  168. package/list/harvesterhci.io.management.cluster.vue +8 -9
  169. package/list/management.cattle.io.user.vue +12 -9
  170. package/list/provisioning.cattle.io.cluster.vue +16 -10
  171. package/mixins/__tests__/auth-config.test.ts +90 -0
  172. package/mixins/__tests__/chart.test.ts +94 -0
  173. package/mixins/__tests__/resource-fetch-api-pagination.test.ts +48 -0
  174. package/mixins/auth-config.js +7 -0
  175. package/mixins/chart.js +11 -2
  176. package/mixins/child-hook.js +12 -6
  177. package/mixins/create-edit-view/impl.js +5 -3
  178. package/mixins/resource-fetch-api-pagination.js +21 -1
  179. package/models/__tests__/catalog.cattle.io.clusterrepo.test.ts +57 -0
  180. package/models/__tests__/compliance.cattle.io.clusterscan.test.ts +144 -0
  181. package/models/__tests__/fleet-application.test.ts +175 -0
  182. package/models/__tests__/fleet.cattle.io.bundle.test.ts +169 -0
  183. package/models/__tests__/fleet.cattle.io.helmop.test.ts +84 -0
  184. package/models/__tests__/management.cattle.io.node.ts +22 -0
  185. package/models/__tests__/namespace.test.ts +36 -0
  186. package/models/__tests__/provisioning.cattle.io.cluster.test.ts +205 -0
  187. package/models/__tests__/secret.test.ts +68 -1
  188. package/models/__tests__/workload.test.ts +401 -26
  189. package/models/catalog.cattle.io.clusterrepo.js +28 -4
  190. package/models/compliance.cattle.io.clusterscan.js +39 -4
  191. package/models/fleet-application.js +4 -0
  192. package/models/fleet.cattle.io.helmop.js +20 -1
  193. package/models/management.cattle.io.cluster.js +39 -5
  194. package/models/management.cattle.io.node.js +44 -3
  195. package/models/namespace.js +1 -1
  196. package/models/pod.js +46 -3
  197. package/models/provisioning.cattle.io.cluster.js +64 -14
  198. package/models/rke.cattle.io.etcdsnapshot.js +17 -9
  199. package/models/secret.js +19 -0
  200. package/models/workload.js +120 -20
  201. package/models/workload.service.js +5 -0
  202. package/package.json +14 -13
  203. package/pages/about.vue +5 -6
  204. package/pages/auth/login.vue +0 -35
  205. package/pages/auth/setup.vue +11 -0
  206. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +2 -2
  207. package/pages/c/_cluster/apps/charts/AppChartCardSubHeader.vue +10 -1
  208. package/pages/c/_cluster/apps/charts/__tests__/index.test.ts +93 -0
  209. package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +485 -107
  210. package/pages/c/_cluster/apps/charts/chart.vue +2 -1
  211. package/pages/c/_cluster/apps/charts/index.vue +48 -10
  212. package/pages/c/_cluster/apps/charts/install.vue +236 -144
  213. package/pages/c/_cluster/auth/roles/index.vue +5 -4
  214. package/pages/c/_cluster/explorer/workload-dashboard/ByNamespaceSection.vue +31 -0
  215. package/pages/c/_cluster/explorer/workload-dashboard/ByStateSection.vue +138 -0
  216. package/pages/c/_cluster/explorer/workload-dashboard/ByTypeSection.vue +30 -0
  217. package/pages/c/_cluster/explorer/workload-dashboard/WorkloadCard.vue +155 -0
  218. package/pages/c/_cluster/explorer/workload-dashboard/WorkloadNamespaceCard.vue +142 -0
  219. package/pages/c/_cluster/explorer/workload-dashboard/WorkloadTypeCard.vue +159 -0
  220. package/pages/c/_cluster/explorer/workload-dashboard/__tests__/composable.test.ts +561 -0
  221. package/pages/c/_cluster/explorer/workload-dashboard/composable.ts +440 -0
  222. package/pages/c/_cluster/explorer/workload-dashboard/index.vue +187 -0
  223. package/pages/c/_cluster/explorer/workload-dashboard/types.ts +80 -0
  224. package/pages/c/_cluster/fleet/application/create.vue +187 -136
  225. package/pages/c/_cluster/fleet/application/index.vue +5 -3
  226. package/pages/c/_cluster/fleet/application/suse-app-collection/ChartDetailBody.vue +338 -0
  227. package/pages/c/_cluster/fleet/application/suse-app-collection/ChartDetailHeader.vue +121 -0
  228. package/pages/c/_cluster/fleet/application/suse-app-collection/chart.vue +369 -0
  229. package/pages/c/_cluster/fleet/application/suse-app-collection/charts.vue +248 -0
  230. package/pages/c/_cluster/fleet/application/suse-app-collection/credentials.vue +310 -0
  231. package/pages/c/_cluster/fleet/index.vue +2 -2
  232. package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +96 -0
  233. package/pages/c/_cluster/uiplugins/index.vue +15 -0
  234. package/pages/fail-whale.vue +16 -11
  235. package/pages/home.vue +16 -46
  236. package/pkg/require-asset.lib.js +25 -0
  237. package/pkg/vue.config.js +7 -0
  238. package/plugins/clean-html.d.ts +9 -0
  239. package/plugins/dashboard-store/__tests__/resource-class.test.ts +177 -0
  240. package/plugins/dashboard-store/getters.js +0 -1
  241. package/plugins/dashboard-store/resource-class.js +114 -19
  242. package/plugins/steve/__tests__/actions.test.ts +212 -0
  243. package/plugins/steve/actions.js +96 -0
  244. package/plugins/steve/steve-pagination-utils.ts +1 -1
  245. package/rancher-components/Accordion/Accordion.vue +53 -9
  246. package/rancher-components/Form/Checkbox/Checkbox.vue +14 -0
  247. package/rancher-components/Form/Radio/RadioButton.vue +17 -1
  248. package/rancher-components/Form/Radio/RadioGroup.vue +10 -0
  249. package/rancher-components/Form/TextArea/TextAreaAutoGrow.vue +30 -0
  250. package/rancher-components/Form/TextArea/__tests__/TextAreaAutoGrow.test.ts +95 -0
  251. package/rancher-components/Pill/RcTag/RcTag.vue +3 -2
  252. package/rancher-components/RcButton/RcButton.test.ts +103 -0
  253. package/rancher-components/RcButton/RcButton.vue +94 -15
  254. package/rancher-components/RcButton/index.ts +1 -1
  255. package/rancher-components/RcButton/types.ts +3 -0
  256. package/rancher-components/RcDropdown/RcDropdownTrigger.vue +6 -1
  257. package/rancher-components/RcItemCard/RcItemCard.test.ts +18 -0
  258. package/rancher-components/RcItemCard/RcItemCard.vue +2 -2
  259. package/rancher-components/RcSection/RcSection.vue +28 -3
  260. package/scripts/extension/helm/package/Dockerfile +1 -1
  261. package/scripts/test-plugins-build.sh +2 -1
  262. package/store/__tests__/features.test.ts +131 -0
  263. package/store/__tests__/growl.test.ts +374 -0
  264. package/store/__tests__/modal.test.ts +131 -0
  265. package/store/__tests__/notifications.test.ts +434 -0
  266. package/store/__tests__/slideInPanel.test.ts +88 -0
  267. package/store/__tests__/type-map.utils.test.ts +433 -0
  268. package/store/catalog.js +57 -0
  269. package/store/features.js +4 -0
  270. package/store/plugins.js +7 -4
  271. package/types/components/buttonGroup.ts +5 -0
  272. package/types/shell/index.d.ts +166 -70
  273. package/utils/__tests__/auth.test.ts +273 -0
  274. package/utils/__tests__/computed.test.ts +193 -0
  275. package/utils/__tests__/cspAdaptor.test.ts +163 -0
  276. package/utils/__tests__/dom.test.ts +81 -0
  277. package/utils/__tests__/duration.test.ts +37 -1
  278. package/utils/__tests__/dynamic-importer.test.ts +102 -0
  279. package/utils/__tests__/fleet-appco.test.ts +312 -0
  280. package/utils/__tests__/monitoring.test.ts +130 -0
  281. package/utils/__tests__/object.test.ts +22 -0
  282. package/utils/__tests__/operation-cr.test.ts +34 -0
  283. package/utils/__tests__/platform.test.ts +91 -0
  284. package/utils/__tests__/position.test.ts +237 -0
  285. package/utils/__tests__/provider.test.ts +51 -1
  286. package/utils/__tests__/queue.test.ts +232 -0
  287. package/utils/__tests__/release-notes.test.ts +221 -0
  288. package/utils/__tests__/router.test.js +254 -1
  289. package/utils/__tests__/select.test.ts +208 -0
  290. package/utils/__tests__/time.test.ts +265 -1
  291. package/utils/__tests__/title.test.ts +47 -0
  292. package/utils/__tests__/width.test.ts +53 -0
  293. package/utils/__tests__/window.test.ts +158 -0
  294. package/utils/__tests__/xccdf.test.ts +126 -6
  295. package/utils/crypto/__tests__/browserHashUtils.test.ts +98 -0
  296. package/utils/crypto/__tests__/index.test.ts +144 -0
  297. package/utils/duration.ts +104 -0
  298. package/utils/dynamic-content/__tests__/notification-handler.test.ts +196 -0
  299. package/utils/dynamic-content/info.ts +2 -1
  300. package/utils/error.js +13 -0
  301. package/utils/fleet-appco.ts +323 -0
  302. package/utils/object.js +22 -2
  303. package/utils/operation-cr.js +19 -0
  304. package/utils/provider.ts +12 -0
  305. package/utils/require-asset.ts +7 -0
  306. package/utils/validators/__tests__/container-images.test.ts +104 -0
  307. package/utils/validators/__tests__/flow-output.test.ts +91 -0
  308. package/utils/validators/__tests__/logging-outputs.test.ts +58 -0
  309. package/utils/validators/__tests__/monitoring-route.test.ts +119 -0
  310. package/utils/validators/__tests__/private-registry.test.ts +27 -15
  311. package/utils/validators/private-registry.ts +15 -4
  312. package/utils/xccdf.ts +39 -42
  313. package/vue.config.js +1 -1
  314. package/pages/support/index.vue +0 -264
  315. package/utils/duration.js +0 -43
@@ -0,0 +1,221 @@
1
+ import { NotificationLevel } from '@shell/types/notifications';
2
+
3
+ const MOCK_VERSION = '2.9.0';
4
+ const MOCK_VERSION_WITH_SUFFIX = `${ MOCK_VERSION }-rc1`;
5
+ const READ_WHATS_NEW_KEY = 'read-whatsnew';
6
+ const PREFIX = 'release-notes-';
7
+ const CURRENT_ID = `${ PREFIX }${ MOCK_VERSION }`;
8
+ const RELEASE_NOTES_URL = 'https://example.com/release-notes';
9
+
10
+ function makeGetters(overrides: {
11
+ all?: any[];
12
+ lastReadVersion?: string;
13
+ t?: (key: string, args?: any) => string;
14
+ releaseNotesUrl?: string;
15
+ } = {}) {
16
+ const {
17
+ all = [],
18
+ lastReadVersion = '',
19
+ t = (key: string) => key,
20
+ releaseNotesUrl = RELEASE_NOTES_URL,
21
+ } = overrides;
22
+
23
+ return {
24
+ 'notifications/all': all,
25
+ 'prefs/get': (key: string) => (key === READ_WHATS_NEW_KEY ? lastReadVersion : ''),
26
+ 'i18n/t': t,
27
+ releaseNotesUrl,
28
+ };
29
+ }
30
+
31
+ describe('addReleaseNotesNotification', () => {
32
+ let addReleaseNotesNotification: any;
33
+ let mockGetVersionData: jest.Mock;
34
+
35
+ beforeEach(() => {
36
+ jest.resetModules();
37
+ mockGetVersionData = jest.fn(() => ({ Version: MOCK_VERSION_WITH_SUFFIX }));
38
+ jest.mock('@shell/config/version', () => ({ getVersionData: mockGetVersionData }));
39
+ addReleaseNotesNotification = require('@shell/utils/release-notes').addReleaseNotesNotification;
40
+ });
41
+
42
+ describe('version parsing', () => {
43
+ it('strips the pre-release suffix from the version', async() => {
44
+ const dispatch = jest.fn(() => Promise.resolve());
45
+ const getters = makeGetters();
46
+
47
+ await addReleaseNotesNotification(dispatch, getters);
48
+
49
+ expect(dispatch).toHaveBeenCalledWith('notifications/add', expect.objectContaining({ id: CURRENT_ID }));
50
+ });
51
+
52
+ it('uses the version as-is when there is no suffix', async() => {
53
+ mockGetVersionData.mockReturnValue({ Version: MOCK_VERSION });
54
+ const dispatch = jest.fn(() => Promise.resolve());
55
+ const getters = makeGetters();
56
+
57
+ await addReleaseNotesNotification(dispatch, getters);
58
+
59
+ expect(dispatch).toHaveBeenCalledWith('notifications/add', expect.objectContaining({ id: CURRENT_ID }));
60
+ });
61
+ });
62
+
63
+ describe('existing notification for the current version', () => {
64
+ it('does not add a notification when the current version notification already exists', async() => {
65
+ const dispatch = jest.fn(() => Promise.resolve());
66
+ const getters = makeGetters({ all: [{ id: CURRENT_ID }] });
67
+
68
+ await addReleaseNotesNotification(dispatch, getters);
69
+
70
+ expect(dispatch).not.toHaveBeenCalledWith('notifications/add', expect.anything());
71
+ });
72
+
73
+ it('does not remove the current version notification when it already exists', async() => {
74
+ const dispatch = jest.fn(() => Promise.resolve());
75
+ const getters = makeGetters({ all: [{ id: CURRENT_ID }] });
76
+
77
+ await addReleaseNotesNotification(dispatch, getters);
78
+
79
+ expect(dispatch).not.toHaveBeenCalledWith('notifications/remove', CURRENT_ID);
80
+ });
81
+ });
82
+
83
+ describe('old release-notes notifications', () => {
84
+ it('removes a notification for an older version', async() => {
85
+ const oldId = `${ PREFIX }2.8.0`;
86
+ const dispatch = jest.fn(() => Promise.resolve());
87
+ const getters = makeGetters({ all: [{ id: oldId }] });
88
+
89
+ await addReleaseNotesNotification(dispatch, getters);
90
+
91
+ expect(dispatch).toHaveBeenCalledWith('notifications/remove', oldId);
92
+ });
93
+
94
+ it('removes multiple notifications for older versions', async() => {
95
+ const oldId1 = `${ PREFIX }2.7.0`;
96
+ const oldId2 = `${ PREFIX }2.8.0`;
97
+ const dispatch = jest.fn(() => Promise.resolve());
98
+ const getters = makeGetters({ all: [{ id: oldId1 }, { id: oldId2 }] });
99
+
100
+ await addReleaseNotesNotification(dispatch, getters);
101
+
102
+ expect(dispatch).toHaveBeenCalledWith('notifications/remove', oldId1);
103
+ expect(dispatch).toHaveBeenCalledWith('notifications/remove', oldId2);
104
+ });
105
+
106
+ it('still adds a new notification after removing old ones', async() => {
107
+ const dispatch = jest.fn(() => Promise.resolve());
108
+ const getters = makeGetters({ all: [{ id: `${ PREFIX }2.8.0` }] });
109
+
110
+ await addReleaseNotesNotification(dispatch, getters);
111
+
112
+ expect(dispatch).toHaveBeenCalledWith('notifications/add', expect.objectContaining({ id: CURRENT_ID }));
113
+ });
114
+ });
115
+
116
+ describe('non-release-notes notifications', () => {
117
+ it('ignores notifications with a different id prefix', async() => {
118
+ const dispatch = jest.fn(() => Promise.resolve());
119
+ const getters = makeGetters({ all: [{ id: 'cluster-upgrade-1.0.0' }, { id: 'some-other-notification' }] });
120
+
121
+ await addReleaseNotesNotification(dispatch, getters);
122
+
123
+ expect(dispatch).not.toHaveBeenCalledWith('notifications/remove', expect.anything());
124
+ });
125
+ });
126
+
127
+ describe('version preference guard', () => {
128
+ it('does not add notification when the version has already been read', async() => {
129
+ const dispatch = jest.fn(() => Promise.resolve());
130
+ const getters = makeGetters({ lastReadVersion: MOCK_VERSION });
131
+
132
+ await addReleaseNotesNotification(dispatch, getters);
133
+
134
+ expect(dispatch).not.toHaveBeenCalledWith('notifications/add', expect.anything());
135
+ });
136
+
137
+ it('adds notification when the read version pref is for an older version', async() => {
138
+ const dispatch = jest.fn(() => Promise.resolve());
139
+ const getters = makeGetters({ lastReadVersion: '2.8.0' });
140
+
141
+ await addReleaseNotesNotification(dispatch, getters);
142
+
143
+ expect(dispatch).toHaveBeenCalledWith('notifications/add', expect.objectContaining({ id: CURRENT_ID }));
144
+ });
145
+
146
+ it('adds notification when the read version pref is empty', async() => {
147
+ const dispatch = jest.fn(() => Promise.resolve());
148
+ const getters = makeGetters({ lastReadVersion: '' });
149
+
150
+ await addReleaseNotesNotification(dispatch, getters);
151
+
152
+ expect(dispatch).toHaveBeenCalledWith('notifications/add', expect.objectContaining({ id: CURRENT_ID }));
153
+ });
154
+ });
155
+
156
+ describe('notification shape', () => {
157
+ it('adds a notification with the correct id, level and preference', async() => {
158
+ const dispatch = jest.fn(() => Promise.resolve());
159
+ const t = jest.fn((key: string) => key);
160
+ const getters = makeGetters({ t });
161
+
162
+ await addReleaseNotesNotification(dispatch, getters);
163
+
164
+ expect(dispatch).toHaveBeenCalledWith('notifications/add', {
165
+ id: CURRENT_ID,
166
+ level: NotificationLevel.Info,
167
+ title: 'landing.whatsNew.title',
168
+ message: 'landing.whatsNew.message',
169
+ preference: {
170
+ key: READ_WHATS_NEW_KEY,
171
+ value: MOCK_VERSION,
172
+ },
173
+ primaryAction: {
174
+ label: 'landing.whatsNew.link',
175
+ target: RELEASE_NOTES_URL,
176
+ },
177
+ });
178
+ });
179
+
180
+ it('passes version to the title translation function', async() => {
181
+ const dispatch = jest.fn(() => Promise.resolve());
182
+ const t = jest.fn((key: string) => key);
183
+ const getters = makeGetters({ t });
184
+
185
+ await addReleaseNotesNotification(dispatch, getters);
186
+
187
+ expect(t).toHaveBeenCalledWith('landing.whatsNew.title', { version: MOCK_VERSION });
188
+ });
189
+
190
+ it('uses the releaseNotesUrl getter for the primaryAction target', async() => {
191
+ const customUrl = 'https://custom.example.com/notes';
192
+ const dispatch = jest.fn(() => Promise.resolve());
193
+ const getters = makeGetters({ releaseNotesUrl: customUrl });
194
+
195
+ await addReleaseNotesNotification(dispatch, getters);
196
+
197
+ expect(dispatch).toHaveBeenCalledWith('notifications/add', expect.objectContaining({ primaryAction: expect.objectContaining({ target: customUrl }) }));
198
+ });
199
+ });
200
+
201
+ describe('empty state', () => {
202
+ it('adds a notification when there are no existing notifications', async() => {
203
+ const dispatch = jest.fn(() => Promise.resolve());
204
+ const getters = makeGetters({ all: [] });
205
+
206
+ await addReleaseNotesNotification(dispatch, getters);
207
+
208
+ expect(dispatch).toHaveBeenCalledWith('notifications/add', expect.objectContaining({ id: CURRENT_ID }));
209
+ });
210
+
211
+ it('dispatches nothing beyond add when there are no existing notifications', async() => {
212
+ const dispatch = jest.fn(() => Promise.resolve());
213
+ const getters = makeGetters({ all: [] });
214
+
215
+ await addReleaseNotesNotification(dispatch, getters);
216
+
217
+ expect(dispatch).toHaveBeenCalledTimes(1);
218
+ expect(dispatch).toHaveBeenCalledWith('notifications/add', expect.anything());
219
+ });
220
+ });
221
+ });
@@ -1,4 +1,15 @@
1
- import { findRouteDefinitionByName, filterLocationValidParams } from '@shell/utils/router';
1
+ import {
2
+ findRouteDefinitionByName,
3
+ filterLocationValidParams,
4
+ queryParamsFor,
5
+ findMeta,
6
+ getClusterFromRoute,
7
+ getProductFromRoute,
8
+ getPackageFromRoute,
9
+ routeMatched,
10
+ routeRequiresAuthentication,
11
+ routeRequiresInstallRedirect,
12
+ } from '@shell/utils/router';
2
13
 
3
14
  describe('findRouteDefinitionByName', () => {
4
15
  const createMockRouter = (routes) => ({ getRoutes: () => routes });
@@ -236,3 +247,245 @@ describe('filterLocationValidParams', () => {
236
247
  expect(result.params.extra).toBeUndefined();
237
248
  });
238
249
  });
250
+
251
+ describe('queryParamsFor', () => {
252
+ it.each([
253
+ {
254
+ desc: 'includes key when no default is defined',
255
+ current: {},
256
+ qp: { page: 2 },
257
+ defaults: {},
258
+ expected: { page: 2 },
259
+ },
260
+ {
261
+ desc: 'sets key to null when default is false and val is truthy',
262
+ current: {},
263
+ qp: { verbose: true },
264
+ defaults: { verbose: false },
265
+ expected: { verbose: null },
266
+ },
267
+ {
268
+ desc: 'deletes key when default is false and val is falsy',
269
+ current: { verbose: null },
270
+ qp: { verbose: false },
271
+ defaults: { verbose: false },
272
+ expected: {},
273
+ },
274
+ {
275
+ desc: 'deletes key when value matches non-false default',
276
+ current: {},
277
+ qp: { sort: 'name' },
278
+ defaults: { sort: 'name' },
279
+ expected: {},
280
+ },
281
+ {
282
+ desc: 'sets key when value differs from non-false default',
283
+ current: {},
284
+ qp: { sort: 'age' },
285
+ defaults: { sort: 'name' },
286
+ expected: { sort: 'age' },
287
+ },
288
+ {
289
+ desc: 'preserves unrelated keys from current',
290
+ current: { filter: 'active' },
291
+ qp: { page: 2 },
292
+ defaults: {},
293
+ expected: { filter: 'active', page: 2 },
294
+ },
295
+ {
296
+ desc: 'treats null current as empty object',
297
+ current: null,
298
+ qp: { page: 1 },
299
+ defaults: {},
300
+ expected: { page: 1 },
301
+ },
302
+ ])('$desc', ({
303
+ current, qp, defaults, expected
304
+ }) => {
305
+ expect(queryParamsFor(current, qp, defaults)).toStrictEqual(expected);
306
+ });
307
+ });
308
+
309
+ describe('findMeta', () => {
310
+ it.each([
311
+ {
312
+ desc: 'returns undefined when route has no meta',
313
+ route: { params: {} },
314
+ key: 'product',
315
+ expected: undefined,
316
+ },
317
+ {
318
+ desc: 'returns undefined when route is null',
319
+ route: null,
320
+ key: 'product',
321
+ expected: undefined,
322
+ },
323
+ {
324
+ desc: 'returns value from a plain meta object',
325
+ route: { meta: { product: 'explorer' } },
326
+ key: 'product',
327
+ expected: 'explorer',
328
+ },
329
+ {
330
+ desc: 'returns value from a matching item in array meta',
331
+ route: { meta: [{ cluster: 'local' }, { product: 'explorer' }] },
332
+ key: 'product',
333
+ expected: 'explorer',
334
+ },
335
+ {
336
+ desc: 'returns undefined when key is absent from all meta items',
337
+ route: { meta: [{ cluster: 'local' }] },
338
+ key: 'product',
339
+ expected: undefined,
340
+ },
341
+ ])('$desc', ({ route, key, expected }) => {
342
+ expect(findMeta(route, key)).toStrictEqual(expected);
343
+ });
344
+ });
345
+
346
+ describe('getClusterFromRoute', () => {
347
+ it.each([
348
+ {
349
+ desc: 'returns cluster from route params',
350
+ to: { params: { cluster: 'local' } },
351
+ expected: 'local',
352
+ },
353
+ {
354
+ desc: 'returns cluster from route meta when not in params',
355
+ to: { params: {}, meta: { cluster: 'remote' } },
356
+ expected: 'remote',
357
+ },
358
+ {
359
+ desc: 'returns undefined when cluster is not in params or meta',
360
+ to: { params: {} },
361
+ expected: undefined,
362
+ },
363
+ ])('$desc', ({ to, expected }) => {
364
+ expect(getClusterFromRoute(to)).toStrictEqual(expected);
365
+ });
366
+ });
367
+
368
+ describe('getProductFromRoute', () => {
369
+ it.each([
370
+ {
371
+ desc: 'returns product from route params',
372
+ to: { params: { product: 'explorer' } },
373
+ expected: 'explorer',
374
+ },
375
+ {
376
+ desc: 'infers product segment from c-cluster-<product>-* route name',
377
+ to: { name: 'c-cluster-explorer-workloads', params: {} },
378
+ expected: 'explorer',
379
+ },
380
+ {
381
+ desc: 'returns product from route meta when params and name do not have it',
382
+ to: {
383
+ name: 'some-route', params: {}, meta: { product: 'manager' }
384
+ },
385
+ expected: 'manager',
386
+ },
387
+ {
388
+ desc: 'returns undefined when product is not found anywhere',
389
+ to: { params: {} },
390
+ expected: undefined,
391
+ },
392
+ ])('$desc', ({ to, expected }) => {
393
+ expect(getProductFromRoute(to)).toStrictEqual(expected);
394
+ });
395
+ });
396
+
397
+ describe('getPackageFromRoute', () => {
398
+ it.each([
399
+ {
400
+ desc: 'returns undefined when route has no meta',
401
+ route: {},
402
+ expected: undefined,
403
+ },
404
+ {
405
+ desc: 'returns pkg from array meta',
406
+ route: { meta: [{ pkg: 'my-package' }] },
407
+ expected: 'my-package',
408
+ },
409
+ {
410
+ desc: 'returns pkg from plain object meta',
411
+ route: { meta: { pkg: 'my-package' } },
412
+ expected: 'my-package',
413
+ },
414
+ {
415
+ desc: 'returns undefined when no meta item defines pkg',
416
+ route: { meta: [{ product: 'explorer' }] },
417
+ expected: undefined,
418
+ },
419
+ ])('$desc', ({ route, expected }) => {
420
+ expect(getPackageFromRoute(route)).toStrictEqual(expected);
421
+ });
422
+ });
423
+
424
+ describe('routeMatched', () => {
425
+ it.each([
426
+ {
427
+ desc: 'returns false when route has no matched array',
428
+ to: {},
429
+ fn: (m) => !!m.meta?.test,
430
+ expected: false,
431
+ },
432
+ {
433
+ desc: 'returns true when predicate matches a route entry',
434
+ to: { matched: [{ meta: { test: true } }] },
435
+ fn: (m) => !!m.meta?.test,
436
+ expected: true,
437
+ },
438
+ {
439
+ desc: 'returns false when predicate matches no route entry',
440
+ to: { matched: [{ meta: { other: true } }] },
441
+ fn: (m) => !!m.meta?.test,
442
+ expected: false,
443
+ },
444
+ ])('$desc', ({ to, fn, expected }) => {
445
+ expect(routeMatched(to, fn)).toStrictEqual(expected);
446
+ });
447
+ });
448
+
449
+ describe('routeRequiresAuthentication', () => {
450
+ it.each([
451
+ {
452
+ desc: 'returns true when a matched route requires authentication',
453
+ to: { matched: [{ meta: { requiresAuthentication: true } }] },
454
+ expected: true,
455
+ },
456
+ {
457
+ desc: 'returns false when no matched route requires authentication',
458
+ to: { matched: [{ meta: {} }] },
459
+ expected: false,
460
+ },
461
+ {
462
+ desc: 'returns false when route has no matched array',
463
+ to: {},
464
+ expected: false,
465
+ },
466
+ ])('$desc', ({ to, expected }) => {
467
+ expect(routeRequiresAuthentication(to)).toStrictEqual(expected);
468
+ });
469
+ });
470
+
471
+ describe('routeRequiresInstallRedirect', () => {
472
+ it.each([
473
+ {
474
+ desc: 'returns true when a matched route meta contains installRedirect',
475
+ to: { matched: [{ meta: { installRedirect: 'some-product' } }] },
476
+ expected: true,
477
+ },
478
+ {
479
+ desc: 'returns false when no matched route has installRedirect',
480
+ to: { matched: [{ meta: {} }] },
481
+ expected: false,
482
+ },
483
+ {
484
+ desc: 'returns false when route has no matched array',
485
+ to: {},
486
+ expected: false,
487
+ },
488
+ ])('$desc', ({ to, expected }) => {
489
+ expect(routeRequiresInstallRedirect(to)).toStrictEqual(expected);
490
+ });
491
+ });
@@ -0,0 +1,208 @@
1
+ import { calculatePosition } from '@shell/utils/select';
2
+
3
+ function makeSelectEl(rect: { x: number; y: number; width: number; height: number }): HTMLElement {
4
+ const el = document.createElement('div');
5
+
6
+ el.getBoundingClientRect = jest.fn().mockReturnValue({
7
+ x: rect.x,
8
+ y: rect.y,
9
+ width: rect.width,
10
+ height: rect.height,
11
+ top: rect.y,
12
+ bottom: rect.y + rect.height,
13
+ left: rect.x,
14
+ right: rect.x + rect.width,
15
+ });
16
+
17
+ return el;
18
+ }
19
+
20
+ function makeDropdownList(offsetHeight: number): HTMLElement {
21
+ const el = document.createElement('div');
22
+
23
+ Object.defineProperty(el, 'offsetHeight', {
24
+ configurable: true,
25
+ value: offsetHeight,
26
+ });
27
+
28
+ return el;
29
+ }
30
+
31
+ function makeComponent(selectEl: HTMLElement): { $parent: { $el: HTMLElement } } {
32
+ return { $parent: { $el: selectEl } };
33
+ }
34
+
35
+ function setWindowEnv(opts: { scrollY?: number; innerHeight?: number }): void {
36
+ if (opts.scrollY !== undefined) {
37
+ Object.defineProperty(window, 'scrollY', { configurable: true, value: opts.scrollY });
38
+ }
39
+ if (opts.innerHeight !== undefined) {
40
+ Object.defineProperty(window, 'innerHeight', { configurable: true, value: opts.innerHeight });
41
+ }
42
+ }
43
+
44
+ function setDocHeight(height: number): void {
45
+ Object.defineProperty(document.body, 'offsetHeight', { configurable: true, value: height });
46
+ }
47
+
48
+ describe('select.js', () => {
49
+ describe('calculatePosition', () => {
50
+ const rect = {
51
+ x: 50,
52
+ y: 200,
53
+ width: 300,
54
+ height: 40,
55
+ };
56
+
57
+ beforeEach(() => {
58
+ setWindowEnv({ scrollY: 0, innerHeight: 800 });
59
+ setDocHeight(1000);
60
+ });
61
+
62
+ describe('placement direction', () => {
63
+ it('uses bottom positioning when placement is "top"', () => {
64
+ const selectEl = makeSelectEl(rect);
65
+ const dropdownList = makeDropdownList(100);
66
+ const component = makeComponent(selectEl);
67
+
68
+ calculatePosition(dropdownList, component, undefined, 'top');
69
+
70
+ // bottom = docHeight - scrollY - r.y - 1 = 1000 - 0 - 200 - 1 = 799
71
+ expect(dropdownList.style.bottom).toStrictEqual('799px');
72
+ expect(dropdownList.style.top).toStrictEqual('');
73
+ expect(dropdownList.classList.contains('vs__dropdown-up')).toStrictEqual(true);
74
+ expect(selectEl.classList.contains('vs__dropdown-up')).toStrictEqual(true);
75
+ });
76
+
77
+ it('uses bottom positioning when placement is "top-start"', () => {
78
+ const selectEl = makeSelectEl(rect);
79
+ const dropdownList = makeDropdownList(100);
80
+ const component = makeComponent(selectEl);
81
+
82
+ calculatePosition(dropdownList, component, undefined, 'top-start');
83
+
84
+ expect(dropdownList.style.bottom).toStrictEqual('799px');
85
+ expect(dropdownList.style.top).toStrictEqual('');
86
+ expect(dropdownList.classList.contains('vs__dropdown-up')).toStrictEqual(true);
87
+ expect(selectEl.classList.contains('vs__dropdown-up')).toStrictEqual(true);
88
+ });
89
+
90
+ it('uses top positioning when placement is "bottom-start" and dropdown fits below', () => {
91
+ const selectEl = makeSelectEl(rect);
92
+ const dropdownList = makeDropdownList(100);
93
+ const component = makeComponent(selectEl);
94
+
95
+ calculatePosition(dropdownList, component, undefined, 'bottom-start');
96
+
97
+ // top = r.y + r.height - 1 + scrollY = 200 + 40 - 1 + 0 = 239
98
+ expect(dropdownList.style.top).toStrictEqual('239px');
99
+ expect(dropdownList.style.bottom).toStrictEqual('');
100
+ expect(dropdownList.classList.contains('vs__dropdown-up')).toStrictEqual(false);
101
+ expect(selectEl.classList.contains('vs__dropdown-up')).toStrictEqual(false);
102
+ });
103
+
104
+ it('falls back to bottom positioning when dropdown overflows the viewport', () => {
105
+ const selectEl = makeSelectEl(rect);
106
+ // offsetHeight=600: end = 239 + 600 = 839 > 800 (innerHeight) → overflow
107
+ const dropdownList = makeDropdownList(600);
108
+ const component = makeComponent(selectEl);
109
+
110
+ calculatePosition(dropdownList, component, undefined, 'bottom-start');
111
+
112
+ // bottom = 1000 - 0 - 200 - 1 = 799
113
+ expect(dropdownList.style.bottom).toStrictEqual('799px');
114
+ expect(dropdownList.style.top).toStrictEqual('');
115
+ expect(dropdownList.classList.contains('vs__dropdown-up')).toStrictEqual(true);
116
+ expect(selectEl.classList.contains('vs__dropdown-up')).toStrictEqual(true);
117
+ });
118
+
119
+ it('defaults to bottom-start behaviour when no placement is provided', () => {
120
+ const selectEl = makeSelectEl(rect);
121
+ const dropdownList = makeDropdownList(100);
122
+ const component = makeComponent(selectEl);
123
+
124
+ // no placement argument → defaults to 'bottom-start'
125
+ calculatePosition(dropdownList, component, undefined, undefined);
126
+
127
+ expect(dropdownList.style.top).toStrictEqual('239px');
128
+ expect(dropdownList.classList.contains('vs__dropdown-up')).toStrictEqual(false);
129
+ });
130
+ });
131
+
132
+ describe('scroll offset', () => {
133
+ it('adds window.scrollY to the top position when the dropdown fits', () => {
134
+ setWindowEnv({ scrollY: 100, innerHeight: 800 });
135
+ const selectEl = makeSelectEl(rect);
136
+ const dropdownList = makeDropdownList(100);
137
+ const component = makeComponent(selectEl);
138
+
139
+ calculatePosition(dropdownList, component, undefined, 'bottom-start');
140
+
141
+ // top = 200 + 40 - 1 + 100 = 339
142
+ expect(dropdownList.style.top).toStrictEqual('339px');
143
+ });
144
+
145
+ it('subtracts window.scrollY from the bottom position', () => {
146
+ setWindowEnv({ scrollY: 50, innerHeight: 800 });
147
+ const selectEl = makeSelectEl(rect);
148
+ const dropdownList = makeDropdownList(100);
149
+ const component = makeComponent(selectEl);
150
+
151
+ calculatePosition(dropdownList, component, undefined, 'top');
152
+
153
+ // bottom = 1000 - 50 - 200 - 1 = 749
154
+ expect(dropdownList.style.bottom).toStrictEqual('749px');
155
+ });
156
+ });
157
+
158
+ describe('always-set style properties', () => {
159
+ // Note: dropdownList.style.width = 'min-content' is always set by calculatePosition
160
+ // but jsdom does not support the min-content keyword so that property is not asserted here.
161
+ it.each([
162
+ {
163
+ desc: 'top-positioned dropdown',
164
+ placement: 'bottom-start',
165
+ },
166
+ {
167
+ desc: 'bottom-positioned dropdown',
168
+ placement: 'top',
169
+ },
170
+ ])('sets left and minWidth for $desc', ({ placement }) => {
171
+ const selectEl = makeSelectEl(rect);
172
+ const dropdownList = makeDropdownList(100);
173
+ const component = makeComponent(selectEl);
174
+
175
+ calculatePosition(dropdownList, component, undefined, placement);
176
+
177
+ expect(dropdownList.style.left).toStrictEqual('50px');
178
+ expect(dropdownList.style.minWidth).toStrictEqual('300px');
179
+ });
180
+ });
181
+
182
+ describe('vs__dropdown-up class management', () => {
183
+ it('removes vs__dropdown-up from both elements when switching to top positioning', () => {
184
+ const selectEl = makeSelectEl(rect);
185
+ const dropdownList = makeDropdownList(100);
186
+
187
+ // pre-add the class to simulate a previous bottom-positioned state
188
+ dropdownList.classList.add('vs__dropdown-up');
189
+ selectEl.classList.add('vs__dropdown-up');
190
+
191
+ calculatePosition(dropdownList, makeComponent(selectEl), undefined, 'bottom-start');
192
+
193
+ expect(dropdownList.classList.contains('vs__dropdown-up')).toStrictEqual(false);
194
+ expect(selectEl.classList.contains('vs__dropdown-up')).toStrictEqual(false);
195
+ });
196
+
197
+ it('adds vs__dropdown-up to both elements when switching to bottom positioning', () => {
198
+ const selectEl = makeSelectEl(rect);
199
+ const dropdownList = makeDropdownList(600);
200
+
201
+ calculatePosition(dropdownList, makeComponent(selectEl), undefined, 'bottom-start');
202
+
203
+ expect(dropdownList.classList.contains('vs__dropdown-up')).toStrictEqual(true);
204
+ expect(selectEl.classList.contains('vs__dropdown-up')).toStrictEqual(true);
205
+ });
206
+ });
207
+ });
208
+ });