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

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 (258) hide show
  1. package/assets/styles/global/_layout.scss +4 -0
  2. package/assets/translations/en-us.yaml +144 -41
  3. package/assets/translations/zh-hans.yaml +1 -7
  4. package/chart/monitoring/ClusterSelector.vue +0 -21
  5. package/chart/monitoring/prometheus/index.vue +6 -3
  6. package/components/CruResource.vue +161 -14
  7. package/components/ExplorerMembers.vue +8 -4
  8. package/components/ExplorerProjectsNamespaces.vue +10 -6
  9. package/components/GrowlManager.vue +4 -0
  10. package/components/MgmtNodeList.vue +184 -0
  11. package/components/Resource/Detail/Card/StateCard/__tests__/composables.test.ts +90 -1
  12. package/components/Resource/Detail/Card/StateCard/composables.ts +57 -87
  13. package/components/Resource/Detail/Card/StatusCard/__tests__/StatusCard.test.ts +61 -0
  14. package/components/Resource/Detail/Card/StatusCard/index.vue +61 -15
  15. package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +2 -0
  16. package/components/Resource/Detail/Metadata/KeyValue.vue +5 -2
  17. package/components/Resource/Detail/Metadata/KeyValueRow.vue +2 -6
  18. package/components/ResourceDetail/index.vue +1 -1
  19. package/components/ResourceList/Masthead.vue +7 -1
  20. package/components/ResourceList/index.vue +82 -1
  21. package/components/RichTranslation.vue +5 -2
  22. package/components/Setting.vue +1 -0
  23. package/components/SubtleLink.vue +31 -6
  24. package/components/Tabbed/Tab.vue +29 -3
  25. package/components/Tabbed/index.vue +25 -3
  26. package/components/TableOfContents/TableOfContents.vue +109 -0
  27. package/components/TableOfContents/composables.ts +258 -0
  28. package/components/Window/ContainerShell.vue +21 -11
  29. package/components/Window/__tests__/ContainerShell.test.ts +107 -37
  30. package/components/Wizard.vue +9 -4
  31. package/components/fleet/AppCoChartGrid.vue +401 -0
  32. package/components/fleet/AppCoEmptyState.vue +127 -0
  33. package/components/fleet/AppCoPageHeader.vue +119 -0
  34. package/components/fleet/AppCoVersionSelect.vue +70 -0
  35. package/components/fleet/FleetClusterTargets/ClusterSelectionFields.vue +217 -0
  36. package/components/fleet/FleetClusterTargets/TargetsList.vue +123 -35
  37. package/components/fleet/FleetClusterTargets/index.vue +189 -146
  38. package/components/fleet/FleetIntro.vue +7 -3
  39. package/components/fleet/FleetNoWorkspaces.vue +7 -3
  40. package/components/fleet/FleetSecretSelector.vue +5 -3
  41. package/components/fleet/FleetValuesFrom.vue +8 -2
  42. package/components/fleet/GitRepoTargetTab.vue +0 -2
  43. package/components/fleet/HelmOpAdvancedTab.vue +19 -53
  44. package/components/fleet/HelmOpAppCoConfigTab.vue +593 -0
  45. package/components/fleet/HelmOpAppCoResourcesSection.vue +162 -0
  46. package/components/fleet/HelmOpResourcesSection.vue +82 -0
  47. package/components/fleet/HelmOpTargetOptionsSection.vue +89 -0
  48. package/components/fleet/HelmOpTargetTab.vue +64 -60
  49. package/components/fleet/HelmOpValuesTab.vue +129 -105
  50. package/components/fleet/__tests__/AppCoEmptyState.test.ts +71 -0
  51. package/components/fleet/__tests__/AppCoVersionSelect.test.ts +36 -0
  52. package/components/fleet/__tests__/ClusterSelectionFields.test.ts +62 -0
  53. package/components/fleet/__tests__/FleetClusterTargets.test.ts +253 -0
  54. package/components/fleet/__tests__/FleetSecretSelector.test.ts +16 -0
  55. package/components/fleet/__tests__/FleetValuesFrom.test.ts +44 -0
  56. package/components/fleet/__tests__/HelmOpAppCoConfigTab.test.ts +59 -0
  57. package/components/fleet/__tests__/HelmOpAppCoResourcesSection.test.ts +62 -0
  58. package/components/fleet/__tests__/HelmOpResourcesSection.test.ts +43 -0
  59. package/components/fleet/__tests__/HelmOpTargetOptionsSection.test.ts +34 -0
  60. package/components/fleet/__tests__/HelmOpValuesTab.test.ts +39 -0
  61. package/components/fleet/__tests__/__snapshots__/AppCoEmptyState.test.ts.snap +97 -0
  62. package/components/fleet/__tests__/__snapshots__/AppCoVersionSelect.test.ts.snap +30 -0
  63. package/components/fleet/__tests__/__snapshots__/ClusterSelectionFields.test.ts.snap +209 -0
  64. package/components/fleet/__tests__/__snapshots__/HelmOpTargetOptionsSection.test.ts.snap +140 -0
  65. package/components/fleet/dashboard/Empty.vue +8 -4
  66. package/components/fleet/dashboard/ResourceCard.vue +28 -0
  67. package/components/fleet/dashboard/ResourceDetails.vue +28 -0
  68. package/components/fleet/dashboard/__tests__/ResourceCard.test.ts +87 -0
  69. package/components/form/ArrayList.vue +61 -4
  70. package/components/form/KeyValue.vue +23 -2
  71. package/components/form/LabeledSelect.vue +39 -1
  72. package/components/form/Labels.vue +22 -3
  73. package/components/form/NameNsDescription.vue +13 -5
  74. package/components/form/ResourceTabs/index.vue +1 -0
  75. package/components/form/__tests__/NameNsDescription.test.ts +75 -0
  76. package/components/formatter/InternalExternalIP.vue +10 -4
  77. package/components/formatter/ServiceTargets.vue +26 -7
  78. package/components/formatter/__tests__/InternalExternalIP.test.ts +132 -0
  79. package/components/formatter/__tests__/ServiceTargets.test.ts +412 -0
  80. package/components/nav/Header.vue +4 -0
  81. package/components/nav/TopLevelMenu.vue +7 -2
  82. package/components/nav/__tests__/Header.test.ts +15 -0
  83. package/components/nav/__tests__/TopLevelMenu.test.ts +120 -2
  84. package/components/templates/default.vue +9 -4
  85. package/components/templates/home.vue +9 -4
  86. package/components/templates/plain.vue +9 -4
  87. package/composables/useHelmOpResources.test.ts +56 -0
  88. package/composables/useHelmOpResources.ts +32 -0
  89. package/composables/useStateColor.test.ts +325 -0
  90. package/composables/useStateColor.ts +128 -0
  91. package/config/home-links.js +1 -1
  92. package/config/labels-annotations.js +1 -0
  93. package/config/product/explorer.js +17 -4
  94. package/config/product/manager.js +2 -0
  95. package/config/router/index.js +16 -0
  96. package/config/router/navigation-guards/__tests__/authentication.test.ts +130 -0
  97. package/config/router/navigation-guards/authentication.js +10 -4
  98. package/config/router/routes.js +20 -6
  99. package/config/settings.ts +0 -2
  100. package/config/table-headers.js +3 -4
  101. package/config/types.js +9 -0
  102. package/core/plugin-products-base.ts +3 -3
  103. package/core/plugin-types.ts +83 -30
  104. package/core/plugin.ts +3 -0
  105. package/core/types-provisioning.ts +34 -1
  106. package/core/types.ts +15 -2
  107. package/detail/__tests__/provisioning.cattle.io.cluster.test.ts +114 -0
  108. package/detail/__tests__/workload.test.ts +3 -152
  109. package/detail/catalog.cattle.io.clusterrepo.vue +1 -1
  110. package/detail/provisioning.cattle.io.cluster.vue +30 -4
  111. package/detail/workload/index.vue +12 -55
  112. package/edit/__tests__/catalog.cattle.io.clusterrepo.test.ts +248 -0
  113. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +105 -0
  114. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/General.test.ts.snap +6 -0
  115. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/index.test.ts.snap +1 -0
  116. package/edit/auth/__tests__/azuread.test.ts +34 -9
  117. package/edit/auth/__tests__/github.test.ts +234 -0
  118. package/edit/auth/__tests__/oidc.test.ts +26 -6
  119. package/edit/auth/__tests__/saml.test.ts +196 -0
  120. package/edit/auth/azuread.vue +128 -95
  121. package/edit/auth/github.vue +72 -13
  122. package/edit/auth/ldap/__tests__/index.test.ts +206 -0
  123. package/edit/auth/ldap/config.vue +8 -0
  124. package/edit/auth/ldap/index.vue +75 -1
  125. package/edit/auth/oidc.vue +119 -73
  126. package/edit/auth/saml.vue +76 -12
  127. package/edit/catalog.cattle.io.clusterrepo.vue +140 -32
  128. package/edit/fleet.cattle.io.helmop.vue +491 -136
  129. package/edit/management.cattle.io.user.vue +5 -2
  130. package/edit/provisioning.cattle.io.cluster/rke2.vue +84 -10
  131. package/edit/provisioning.cattle.io.cluster/tabs/MachinePool.vue +11 -0
  132. package/list/group.principal.vue +5 -4
  133. package/list/harvesterhci.io.management.cluster.vue +8 -9
  134. package/list/management.cattle.io.user.vue +12 -9
  135. package/list/provisioning.cattle.io.cluster.vue +16 -10
  136. package/mixins/__tests__/auth-config.test.ts +90 -0
  137. package/mixins/__tests__/chart.test.ts +94 -0
  138. package/mixins/__tests__/resource-fetch-api-pagination.test.ts +48 -0
  139. package/mixins/auth-config.js +7 -0
  140. package/mixins/chart.js +11 -2
  141. package/mixins/child-hook.js +12 -6
  142. package/mixins/create-edit-view/impl.js +5 -3
  143. package/mixins/resource-fetch-api-pagination.js +21 -1
  144. package/models/__tests__/catalog.cattle.io.clusterrepo.test.ts +57 -0
  145. package/models/__tests__/compliance.cattle.io.clusterscan.test.ts +144 -0
  146. package/models/__tests__/fleet-application.test.ts +175 -0
  147. package/models/__tests__/fleet.cattle.io.bundle.test.ts +169 -0
  148. package/models/__tests__/fleet.cattle.io.helmop.test.ts +84 -0
  149. package/models/__tests__/management.cattle.io.node.ts +22 -0
  150. package/models/__tests__/namespace.test.ts +36 -0
  151. package/models/__tests__/provisioning.cattle.io.cluster.test.ts +49 -0
  152. package/models/__tests__/workload.test.ts +401 -26
  153. package/models/catalog.cattle.io.clusterrepo.js +28 -4
  154. package/models/compliance.cattle.io.clusterscan.js +39 -4
  155. package/models/fleet-application.js +4 -0
  156. package/models/fleet.cattle.io.helmop.js +20 -1
  157. package/models/management.cattle.io.cluster.js +18 -2
  158. package/models/management.cattle.io.node.js +44 -3
  159. package/models/namespace.js +1 -1
  160. package/models/pod.js +33 -1
  161. package/models/provisioning.cattle.io.cluster.js +5 -5
  162. package/models/workload.js +108 -13
  163. package/models/workload.service.js +5 -0
  164. package/package.json +14 -13
  165. package/pages/about.vue +5 -6
  166. package/pages/auth/login.vue +0 -35
  167. package/pages/auth/setup.vue +11 -0
  168. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +2 -2
  169. package/pages/c/_cluster/apps/charts/AppChartCardSubHeader.vue +10 -1
  170. package/pages/c/_cluster/apps/charts/__tests__/index.test.ts +93 -0
  171. package/pages/c/_cluster/apps/charts/chart.vue +2 -1
  172. package/pages/c/_cluster/apps/charts/index.vue +48 -10
  173. package/pages/c/_cluster/apps/charts/install.vue +122 -116
  174. package/pages/c/_cluster/auth/roles/index.vue +5 -4
  175. package/pages/c/_cluster/explorer/workload-dashboard/ByNamespaceSection.vue +31 -0
  176. package/pages/c/_cluster/explorer/workload-dashboard/ByStateSection.vue +138 -0
  177. package/pages/c/_cluster/explorer/workload-dashboard/ByTypeSection.vue +30 -0
  178. package/pages/c/_cluster/explorer/workload-dashboard/WorkloadCard.vue +155 -0
  179. package/pages/c/_cluster/explorer/workload-dashboard/WorkloadNamespaceCard.vue +142 -0
  180. package/pages/c/_cluster/explorer/workload-dashboard/WorkloadTypeCard.vue +159 -0
  181. package/pages/c/_cluster/explorer/workload-dashboard/__tests__/composable.test.ts +561 -0
  182. package/pages/c/_cluster/explorer/workload-dashboard/composable.ts +440 -0
  183. package/pages/c/_cluster/explorer/workload-dashboard/index.vue +187 -0
  184. package/pages/c/_cluster/explorer/workload-dashboard/types.ts +80 -0
  185. package/pages/c/_cluster/fleet/application/create.vue +187 -136
  186. package/pages/c/_cluster/fleet/application/index.vue +5 -3
  187. package/pages/c/_cluster/fleet/application/suse-app-collection/ChartDetailBody.vue +338 -0
  188. package/pages/c/_cluster/fleet/application/suse-app-collection/ChartDetailHeader.vue +121 -0
  189. package/pages/c/_cluster/fleet/application/suse-app-collection/chart.vue +369 -0
  190. package/pages/c/_cluster/fleet/application/suse-app-collection/charts.vue +248 -0
  191. package/pages/c/_cluster/fleet/application/suse-app-collection/credentials.vue +310 -0
  192. package/pages/c/_cluster/fleet/index.vue +2 -2
  193. package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +96 -0
  194. package/pages/c/_cluster/uiplugins/index.vue +15 -0
  195. package/pages/fail-whale.vue +16 -11
  196. package/pages/home.vue +16 -46
  197. package/plugins/clean-html.d.ts +9 -0
  198. package/plugins/dashboard-store/__tests__/resource-class.test.ts +93 -0
  199. package/plugins/dashboard-store/resource-class.js +62 -7
  200. package/plugins/steve/__tests__/actions.test.ts +212 -0
  201. package/plugins/steve/actions.js +96 -0
  202. package/plugins/steve/steve-pagination-utils.ts +1 -1
  203. package/rancher-components/Accordion/Accordion.vue +53 -9
  204. package/rancher-components/Form/Checkbox/Checkbox.vue +14 -0
  205. package/rancher-components/Form/Radio/RadioButton.vue +17 -1
  206. package/rancher-components/Form/Radio/RadioGroup.vue +10 -0
  207. package/rancher-components/Pill/RcTag/RcTag.vue +3 -2
  208. package/rancher-components/RcButton/RcButton.test.ts +103 -0
  209. package/rancher-components/RcButton/RcButton.vue +94 -15
  210. package/rancher-components/RcButton/types.ts +3 -0
  211. package/rancher-components/RcItemCard/RcItemCard.test.ts +18 -0
  212. package/rancher-components/RcItemCard/RcItemCard.vue +2 -2
  213. package/rancher-components/RcSection/RcSection.vue +28 -3
  214. package/scripts/extension/helm/package/Dockerfile +1 -1
  215. package/scripts/test-plugins-build.sh +2 -1
  216. package/store/__tests__/notifications.test.ts +434 -0
  217. package/store/catalog.js +57 -0
  218. package/store/plugins.js +7 -4
  219. package/types/components/buttonGroup.ts +5 -0
  220. package/types/shell/index.d.ts +104 -70
  221. package/utils/__tests__/auth.test.ts +273 -0
  222. package/utils/__tests__/computed.test.ts +193 -0
  223. package/utils/__tests__/cspAdaptor.test.ts +163 -0
  224. package/utils/__tests__/dom.test.ts +81 -0
  225. package/utils/__tests__/duration.test.ts +37 -1
  226. package/utils/__tests__/dynamic-importer.test.ts +102 -0
  227. package/utils/__tests__/fleet-appco.test.ts +312 -0
  228. package/utils/__tests__/monitoring.test.ts +130 -0
  229. package/utils/__tests__/object.test.ts +22 -0
  230. package/utils/__tests__/platform.test.ts +91 -0
  231. package/utils/__tests__/position.test.ts +237 -0
  232. package/utils/__tests__/provider.test.ts +51 -1
  233. package/utils/__tests__/queue.test.ts +232 -0
  234. package/utils/__tests__/release-notes.test.ts +221 -0
  235. package/utils/__tests__/router.test.js +254 -1
  236. package/utils/__tests__/select.test.ts +208 -0
  237. package/utils/__tests__/time.test.ts +265 -1
  238. package/utils/__tests__/title.test.ts +47 -0
  239. package/utils/__tests__/width.test.ts +53 -0
  240. package/utils/__tests__/window.test.ts +158 -0
  241. package/utils/__tests__/xccdf.test.ts +126 -6
  242. package/utils/crypto/__tests__/browserHashUtils.test.ts +98 -0
  243. package/utils/crypto/__tests__/index.test.ts +144 -0
  244. package/utils/duration.ts +104 -0
  245. package/utils/dynamic-content/__tests__/notification-handler.test.ts +196 -0
  246. package/utils/dynamic-content/info.ts +2 -1
  247. package/utils/error.js +13 -0
  248. package/utils/fleet-appco.ts +323 -0
  249. package/utils/object.js +22 -2
  250. package/utils/provider.ts +12 -0
  251. package/utils/validators/__tests__/container-images.test.ts +104 -0
  252. package/utils/validators/__tests__/flow-output.test.ts +91 -0
  253. package/utils/validators/__tests__/logging-outputs.test.ts +58 -0
  254. package/utils/validators/__tests__/monitoring-route.test.ts +119 -0
  255. package/utils/xccdf.ts +39 -42
  256. package/vue.config.js +1 -1
  257. package/pages/support/index.vue +0 -264
  258. package/utils/duration.js +0 -43
@@ -0,0 +1,234 @@
1
+ import { nextTick } from 'vue';
2
+ import { mount, type VueWrapper, flushPromises } from '@vue/test-utils';
3
+ import { _EDIT } from '@shell/config/query-params';
4
+
5
+ import GitHub from '@shell/edit/auth/github.vue';
6
+
7
+ jest.mock('@shell/utils/clipboard', () => {
8
+ return { copyTextToClipboard: jest.fn(() => Promise.resolve({})) };
9
+ });
10
+
11
+ const validClientId = 'github-client-id';
12
+ const validClientSecret = 'github-client-secret';
13
+ const validAppId = '12345';
14
+ const validPrivateKey = '-----BEGIN RSA PRIVATE KEY-----\ntest\n-----END RSA PRIVATE KEY-----';
15
+ const validTargetUrl = 'https://github.mycompany.com';
16
+
17
+ const mockGitHubModel = {
18
+ enabled: false,
19
+ id: 'github',
20
+ clientId: validClientId,
21
+ clientSecret: validClientSecret,
22
+ hostname: 'github.com',
23
+ tls: true,
24
+ };
25
+
26
+ const mockGitHubAppModel = {
27
+ ...mockGitHubModel,
28
+ id: 'githubapp',
29
+ appId: validAppId,
30
+ privateKey: validPrivateKey,
31
+ };
32
+
33
+ const requiredSetup = (modelOverrides = {}, dataOverrides = {}) => ({
34
+ data() {
35
+ return {
36
+ isEnabling: false,
37
+ editConfig: false,
38
+ model: { ...mockGitHubModel, ...modelOverrides },
39
+ serverSetting: null,
40
+ errors: [],
41
+ originalModel: null,
42
+ principals: [],
43
+ authConfigName: 'github',
44
+ targetType: 'public',
45
+ targetUrl: '',
46
+ ...dataOverrides,
47
+ } as any;
48
+ },
49
+ global: {
50
+ mocks: {
51
+ $fetchState: { pending: false },
52
+ $store: {
53
+ getters: {
54
+ currentStore: () => 'current_store',
55
+ 'current_store/schemaFor': jest.fn(),
56
+ 'current_store/all': jest.fn(),
57
+ 'i18n/t': (val: string) => val,
58
+ 'i18n/exists': jest.fn(),
59
+ },
60
+ dispatch: jest.fn(),
61
+ },
62
+ $route: { query: { AS: '' }, params: { id: 'github' } },
63
+ $router: { applyQuery: jest.fn() },
64
+ },
65
+ },
66
+ props: {
67
+ value: {},
68
+ mode: _EDIT,
69
+ },
70
+ });
71
+
72
+ describe('github.vue', () => {
73
+ describe('GitHub provider', () => {
74
+ let wrapper: VueWrapper<any, any>;
75
+
76
+ beforeEach(() => {
77
+ wrapper = mount(GitHub, { ...requiredSetup() });
78
+ });
79
+
80
+ afterEach(() => {
81
+ wrapper.unmount();
82
+ });
83
+
84
+ describe('save button disabled', () => {
85
+ it('when clientId is empty', async() => {
86
+ wrapper.setData({ model: { clientId: '' } });
87
+ await wrapper.vm.validateAllFields();
88
+ await flushPromises();
89
+
90
+ const saveButton = wrapper.find('[data-testid="form-save"]').element as HTMLInputElement;
91
+
92
+ expect(saveButton.disabled).toBe(true);
93
+ });
94
+
95
+ it('when clientSecret is empty', async() => {
96
+ wrapper.setData({ model: { clientSecret: '' } });
97
+ await wrapper.vm.validateAllFields();
98
+ await flushPromises();
99
+
100
+ const saveButton = wrapper.find('[data-testid="form-save"]').element as HTMLInputElement;
101
+
102
+ expect(saveButton.disabled).toBe(true);
103
+ });
104
+
105
+ it('when both clientId and clientSecret are empty', async() => {
106
+ wrapper.setData({ model: { clientId: '' } });
107
+ wrapper.setData({ model: { clientSecret: '' } });
108
+ await wrapper.vm.validateAllFields();
109
+ await flushPromises();
110
+
111
+ const saveButton = wrapper.find('[data-testid="form-save"]').element as HTMLInputElement;
112
+
113
+ expect(saveButton.disabled).toBe(true);
114
+ });
115
+ });
116
+
117
+ describe('save button enabled', () => {
118
+ it('when clientId and clientSecret are filled', async() => {
119
+ await nextTick();
120
+
121
+ const saveButton = wrapper.find('[data-testid="form-save"]').element as HTMLInputElement;
122
+
123
+ expect(saveButton.disabled).toBe(false);
124
+ });
125
+
126
+ it('when provider is already enabled and not editing config', async() => {
127
+ wrapper.setData({ model: { ...mockGitHubModel, enabled: true }, editConfig: false });
128
+ await nextTick();
129
+
130
+ const saveButton = wrapper.find('[data-testid="form-save"]').element as HTMLInputElement;
131
+
132
+ expect(saveButton.disabled).toBe(false);
133
+ });
134
+ });
135
+ });
136
+
137
+ describe('GitHub App provider', () => {
138
+ let wrapper: VueWrapper<any, any>;
139
+
140
+ beforeEach(() => {
141
+ wrapper = mount(GitHub, { ...requiredSetup(mockGitHubAppModel) });
142
+ });
143
+
144
+ afterEach(() => {
145
+ wrapper.unmount();
146
+ });
147
+
148
+ describe('save button disabled', () => {
149
+ it('when appId is empty', async() => {
150
+ wrapper.setData({ model: { appId: '' } });
151
+ await wrapper.vm.validateAllFields();
152
+ await flushPromises();
153
+
154
+ const saveButton = wrapper.find('[data-testid="form-save"]').element as HTMLInputElement;
155
+
156
+ expect(saveButton.disabled).toBe(true);
157
+ });
158
+
159
+ it('when privateKey is empty', async() => {
160
+ wrapper.vm.model.privateKey = '';
161
+ wrapper.setData({ model: { privateKey: '' } });
162
+ await wrapper.vm.validateAllFields();
163
+ await flushPromises();
164
+
165
+ const saveButton = wrapper.find('[data-testid="form-save"]').element as HTMLInputElement;
166
+
167
+ expect(saveButton.disabled).toBe(true);
168
+ });
169
+ });
170
+
171
+ describe('save button enabled', () => {
172
+ it('when all required fields are filled', async() => {
173
+ await nextTick();
174
+
175
+ const saveButton = wrapper.find('[data-testid="form-save"]').element as HTMLInputElement;
176
+
177
+ expect(saveButton.disabled).toBe(false);
178
+ });
179
+ });
180
+ });
181
+
182
+ describe('GitHub App fields not required for GitHub provider', () => {
183
+ let wrapper: VueWrapper<any, any>;
184
+
185
+ beforeEach(() => {
186
+ wrapper = mount(GitHub, { ...requiredSetup() });
187
+ });
188
+
189
+ afterEach(() => {
190
+ wrapper.unmount();
191
+ });
192
+
193
+ it('save button is enabled even when appId and privateKey are absent', async() => {
194
+ await nextTick();
195
+
196
+ const saveButton = wrapper.find('[data-testid="form-save"]').element as HTMLInputElement;
197
+
198
+ expect(saveButton.disabled).toBe(false);
199
+ });
200
+ });
201
+
202
+ describe('private GitHub Enterprise target', () => {
203
+ let wrapper: VueWrapper<any, any>;
204
+
205
+ beforeEach(() => {
206
+ wrapper = mount(
207
+ GitHub,
208
+ { ...requiredSetup({}, { targetType: 'private', targetUrl: validTargetUrl }) }
209
+ );
210
+ });
211
+
212
+ afterEach(() => {
213
+ wrapper.unmount();
214
+ });
215
+
216
+ it('save button is disabled when targetUrl is empty', async() => {
217
+ wrapper.setData({ targetType: 'private', targetUrl: '' });
218
+ await wrapper.vm.validateAllFields();
219
+ await flushPromises();
220
+
221
+ const saveButton = wrapper.find('[data-testid="form-save"]').element as HTMLInputElement;
222
+
223
+ expect(saveButton.disabled).toBe(true);
224
+ });
225
+
226
+ it('save button is enabled when targetUrl is filled', async() => {
227
+ await nextTick();
228
+
229
+ const saveButton = wrapper.find('[data-testid="form-save"]').element as HTMLInputElement;
230
+
231
+ expect(saveButton.disabled).toBe(false);
232
+ });
233
+ });
234
+ });
@@ -1,6 +1,6 @@
1
1
  import { nextTick } from 'vue';
2
2
  /* eslint-disable jest/no-hooks */
3
- import { mount, type VueWrapper } from '@vue/test-utils';
3
+ import { mount, type VueWrapper, flushPromises } from '@vue/test-utils';
4
4
  import { _EDIT } from '@shell/config/query-params';
5
5
 
6
6
  import oidc from '@shell/edit/auth/oidc.vue';
@@ -9,6 +9,18 @@ jest.mock('@shell/utils/clipboard', () => {
9
9
  return { copyTextToClipboard: jest.fn(() => Promise.resolve({})) };
10
10
  });
11
11
 
12
+ const mockStore = {
13
+ getters: {
14
+ 'i18n/t': (key: string) => key,
15
+ 'i18n/exists': () => false,
16
+ },
17
+ };
18
+
19
+ jest.mock('vuex', () => ({
20
+ ...jest.requireActual('vuex'),
21
+ useStore: () => mockStore,
22
+ }));
23
+
12
24
  const validClientId = 'rancheroidc';
13
25
  const validClientSecret = 'TOkUxg0P67m1UXWNkJLHDPkUZFIKOWSq';
14
26
  const validUrl = 'https://localhost:8080';
@@ -79,20 +91,26 @@ describe('oidc.vue', () => {
79
91
  });
80
92
 
81
93
  describe('have "Create" button disabled', () => {
82
- it('given missing Auth endpoint URL', () => {
94
+ // validateAllFields() replicates what happens on blur in a real browser: it runs
95
+ // the toTypedSchema validators for every registered field and flushes async errors.
96
+ it('given missing Auth endpoint URL', async() => {
83
97
  wrapper.vm.model.authEndpoint = '';
84
98
  wrapper.vm.model.scopes = 'openid profile email'; // set scope to be sure
85
99
  wrapper.vm.oidcScope = ['openid', 'profile', 'email']; // TODO #13457: this is duplicated due wrong format of scopes
100
+ await wrapper.vm.validateAllFields();
101
+ await flushPromises();
86
102
 
87
103
  const saveButton = wrapper.find('[data-testid="form-save"]').element as HTMLInputElement;
88
104
 
89
105
  expect(saveButton.disabled).toBe(true);
90
106
  });
91
107
 
92
- it('given missing required basic scopes', () => {
93
- wrapper.vm.model.authEndpoint = 'whatever'; // set auth endpoint to be sure
108
+ it('given missing required basic scopes', async() => {
109
+ wrapper.vm.model.authEndpoint = validAuthEndpoint; // set auth endpoint to be sure
94
110
  wrapper.vm.model.scopes = 'something else'; // set wrong scope
95
111
  wrapper.vm.oidcScope = ['something', 'else']; // TODO #13457: this is duplicated due wrong format of scopes
112
+ await wrapper.vm.validateAllFields();
113
+ await flushPromises();
96
114
 
97
115
  const saveButton = wrapper.find('[data-testid="form-save"]').element as HTMLInputElement;
98
116
 
@@ -101,7 +119,8 @@ describe('oidc.vue', () => {
101
119
 
102
120
  it('when provider is disabled and editing config before fields are filled in', async() => {
103
121
  wrapper.setData({ model: {}, editConfig: true });
104
- await nextTick();
122
+ await wrapper.vm.validateAllFields();
123
+ await flushPromises();
105
124
 
106
125
  const saveButton = wrapper.find('[data-testid="form-save"]').element as HTMLInputElement;
107
126
 
@@ -110,7 +129,8 @@ describe('oidc.vue', () => {
110
129
 
111
130
  it('when provider is disabled and editing config after required fields and scope is missing openid', async() => {
112
131
  wrapper.setData({ oidcUrls: { url: validUrl, realm: validRealm } });
113
- await nextTick();
132
+ await wrapper.vm.validateAllFields();
133
+ await flushPromises();
114
134
 
115
135
  const saveButton = wrapper.find('[data-testid="form-save"]').element as HTMLInputElement;
116
136
 
@@ -0,0 +1,196 @@
1
+ import { nextTick } from 'vue';
2
+ import { mount, type VueWrapper, flushPromises } from '@vue/test-utils';
3
+ import { _EDIT } from '@shell/config/query-params';
4
+ import Saml from '@shell/edit/auth/saml.vue';
5
+
6
+ const REQUIRED_FIELDS = [
7
+ 'displayNameField',
8
+ 'userNameField',
9
+ 'uidField',
10
+ 'groupsField',
11
+ 'rancherApiHost',
12
+ 'spKey',
13
+ 'spCert',
14
+ 'idpMetadataContent',
15
+ ] as const;
16
+
17
+ type RequiredField = typeof REQUIRED_FIELDS[number];
18
+
19
+ const validModel = {
20
+ enabled: false,
21
+ id: 'shibboleth',
22
+ displayNameField: 'givenName',
23
+ userNameField: 'uid',
24
+ uidField: 'uid',
25
+ groupsField: 'memberOf',
26
+ rancherApiHost: 'https://rancher.example.com',
27
+ spKey: '-----BEGIN RSA PRIVATE KEY-----\ntest\n-----END RSA PRIVATE KEY-----',
28
+ spCert: '-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----',
29
+ idpMetadataContent: '<EntityDescriptor/>',
30
+ };
31
+
32
+ const emptyRequiredFields: Record<RequiredField, string> = {
33
+ displayNameField: '',
34
+ userNameField: '',
35
+ uidField: '',
36
+ groupsField: '',
37
+ rancherApiHost: '',
38
+ spKey: '',
39
+ spCert: '',
40
+ idpMetadataContent: '',
41
+ };
42
+
43
+ const mountOptions = (model: object) => ({
44
+ data() {
45
+ return {
46
+ isEnabling: false,
47
+ editConfig: false,
48
+ model: { ...model },
49
+ serverSetting: null,
50
+ errors: [],
51
+ originalModel: null,
52
+ principals: [],
53
+ authConfigName: 'shibboleth',
54
+ } as any;
55
+ },
56
+ global: {
57
+ mocks: {
58
+ $fetchState: { pending: false },
59
+ $store: {
60
+ getters: {
61
+ currentStore: () => 'current_store',
62
+ 'current_store/schemaFor': jest.fn(),
63
+ 'current_store/all': jest.fn(),
64
+ 'i18n/t': (val: string) => val,
65
+ 'i18n/exists': jest.fn(),
66
+ },
67
+ dispatch: jest.fn(),
68
+ },
69
+ $route: { query: { AS: '' }, params: { id: 'shibboleth' } },
70
+ $router: { applyQuery: jest.fn() },
71
+ },
72
+ },
73
+ props: {
74
+ value: {},
75
+ mode: _EDIT,
76
+ },
77
+ });
78
+
79
+ describe('saml.vue', () => {
80
+ describe('validationPassed computed', () => {
81
+ let wrapper: VueWrapper<any, any>;
82
+
83
+ afterEach(() => {
84
+ wrapper.unmount();
85
+ });
86
+
87
+ it('returns true when all required fields are filled', async() => {
88
+ wrapper = mount(Saml, mountOptions(validModel));
89
+ await flushPromises();
90
+
91
+ expect(wrapper.vm.validationPassed).toBe(true);
92
+ });
93
+
94
+ it('returns false when all required fields are empty', async() => {
95
+ wrapper = mount(Saml, mountOptions({ ...validModel, ...emptyRequiredFields }));
96
+ await wrapper.vm.validateAllFields();
97
+ await flushPromises();
98
+
99
+ expect(wrapper.vm.validationPassed).toBe(false);
100
+ });
101
+
102
+ it('returns true when provider is enabled and not editing config, regardless of field state', async() => {
103
+ wrapper = mount(Saml, mountOptions({
104
+ ...validModel, ...emptyRequiredFields, enabled: true
105
+ }));
106
+ await wrapper.vm.validateAllFields();
107
+ await flushPromises();
108
+
109
+ expect(wrapper.vm.validationPassed).toBe(true);
110
+ });
111
+
112
+ it('returns false when provider is enabled but editConfig is true and required fields are empty', async() => {
113
+ wrapper = mount(Saml, {
114
+ ...mountOptions({
115
+ ...validModel, ...emptyRequiredFields, enabled: true
116
+ }),
117
+ data() {
118
+ return {
119
+ isEnabling: false,
120
+ editConfig: true,
121
+ model: {
122
+ ...validModel, ...emptyRequiredFields, enabled: true
123
+ },
124
+ serverSetting: null,
125
+ errors: [],
126
+ originalModel: null,
127
+ principals: [],
128
+ authConfigName: 'shibboleth',
129
+ } as any;
130
+ },
131
+ });
132
+ await wrapper.vm.validateAllFields();
133
+ await flushPromises();
134
+
135
+ expect(wrapper.vm.validationPassed).toBe(false);
136
+ });
137
+ });
138
+
139
+ describe('Enable button', () => {
140
+ describe('is disabled', () => {
141
+ let wrapper: VueWrapper<any, any>;
142
+
143
+ afterEach(() => {
144
+ wrapper.unmount();
145
+ });
146
+
147
+ it('when all required fields are empty', async() => {
148
+ wrapper = mount(Saml, mountOptions({ ...validModel, ...emptyRequiredFields }));
149
+ await wrapper.vm.validateAllFields();
150
+ await flushPromises();
151
+
152
+ const saveButton = wrapper.find('[data-testid="form-save"]').element as HTMLInputElement;
153
+
154
+ expect(saveButton.disabled).toBe(true);
155
+ });
156
+
157
+ it.each([...REQUIRED_FIELDS])('when only %s is empty', async(field: RequiredField) => {
158
+ wrapper = mount(Saml, mountOptions({ ...validModel, [field]: '' }));
159
+ await wrapper.vm.validateAllFields();
160
+ await flushPromises();
161
+
162
+ const saveButton = wrapper.find('[data-testid="form-save"]').element as HTMLInputElement;
163
+
164
+ expect(saveButton.disabled).toBe(true);
165
+ });
166
+ });
167
+
168
+ describe('is enabled', () => {
169
+ let wrapper: VueWrapper<any, any>;
170
+
171
+ afterEach(() => {
172
+ wrapper.unmount();
173
+ });
174
+
175
+ it('when all required fields are filled', async() => {
176
+ wrapper = mount(Saml, mountOptions(validModel));
177
+ await flushPromises();
178
+
179
+ const saveButton = wrapper.find('[data-testid="form-save"]').element as HTMLInputElement;
180
+
181
+ expect(saveButton.disabled).toBe(false);
182
+ });
183
+
184
+ it('when provider is already enabled and not editing config', async() => {
185
+ wrapper = mount(Saml, mountOptions({
186
+ ...validModel, ...emptyRequiredFields, enabled: true
187
+ }));
188
+ await nextTick();
189
+
190
+ const saveButton = wrapper.find('[data-testid="form-save"]').element as HTMLInputElement;
191
+
192
+ expect(saveButton.disabled).toBe(false);
193
+ });
194
+ });
195
+ });
196
+ });