@rancher/shell 3.0.10 → 3.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/translations/en-us.yaml +7 -5
- package/chart/__tests__/rancher-backup-index.test.ts +248 -0
- package/chart/rancher-backup/index.vue +41 -2
- package/components/BrandImage.vue +6 -5
- package/components/ConsumptionGauge.vue +12 -4
- package/components/DynamicContent/DynamicContentIcon.vue +3 -2
- package/components/ExplorerProjectsNamespaces.vue +1 -4
- package/components/LazyImage.vue +2 -1
- package/components/Resource/Detail/Card/Scaler.vue +4 -4
- package/components/Tabbed/index.vue +6 -0
- package/components/__tests__/ConsumptionGauge.test.ts +31 -0
- package/components/form/ProjectMemberEditor.vue +0 -10
- package/components/nav/TopLevelMenu.helper.ts +7 -79
- package/components/nav/__tests__/TopLevelMenu.helper.test.ts +2 -53
- package/config/private-label.js +2 -1
- package/config/product/apps.js +1 -0
- package/core/__tests__/extension-manager-impl.test.js +187 -2
- package/core/extension-manager-impl.js +4 -2
- package/core/plugin-helpers.ts +31 -0
- package/detail/__tests__/node.test.ts +83 -0
- package/detail/management.cattle.io.oidcclient.vue +2 -1
- package/detail/node.vue +1 -0
- package/edit/catalog.cattle.io.clusterrepo.vue +17 -3
- package/edit/cloudcredential.vue +2 -1
- package/edit/monitoring.coreos.com.alertmanagerconfig/receiverConfig.vue +11 -6
- package/edit/provisioning.cattle.io.cluster/index.vue +5 -4
- package/edit/provisioning.cattle.io.cluster/shared.ts +4 -2
- package/edit/secret/generic.vue +1 -0
- package/edit/secret/index.vue +2 -1
- package/edit/service.vue +2 -14
- package/list/management.cattle.io.feature.vue +7 -1
- package/list/provisioning.cattle.io.cluster.vue +0 -49
- package/mixins/brand.js +2 -1
- package/models/catalog.cattle.io.clusterrepo.js +9 -0
- package/models/cluster.x-k8s.io.machinedeployment.js +8 -3
- package/models/management.cattle.io.authconfig.js +2 -1
- package/models/management.cattle.io.cluster.js +4 -3
- package/models/monitoring.coreos.com.receiver.js +11 -6
- package/models/provisioning.cattle.io.cluster.js +2 -2
- package/package.json +5 -5
- package/pages/c/_cluster/apps/charts/index.vue +3 -8
- package/pages/c/_cluster/apps/charts/install.vue +8 -9
- package/pages/c/_cluster/istio/index.vue +4 -2
- package/pages/c/_cluster/longhorn/index.vue +2 -1
- package/pages/c/_cluster/monitoring/index.vue +2 -2
- package/pages/c/_cluster/neuvector/index.vue +2 -1
- package/pages/c/_cluster/settings/performance.vue +0 -5
- package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +2 -1
- package/pages/c/_cluster/uiplugins/index.vue +2 -1
- package/plugins/steve/steve-pagination-utils.ts +1 -2
- package/plugins/steve/subscribe.js +29 -4
- package/rancher-components/RcButton/RcButton.vue +3 -3
- package/rancher-components/RcButtonSplit/RcButtonSplit.test.ts +253 -0
- package/rancher-components/RcButtonSplit/RcButtonSplit.vue +158 -0
- package/rancher-components/RcButtonSplit/index.ts +1 -0
- package/scripts/test-plugins-build.sh +4 -4
- package/types/shell/index.d.ts +1 -0
- package/utils/__tests__/require-asset.test.ts +98 -0
- package/utils/async.ts +1 -5
- package/utils/brand.ts +3 -1
- package/utils/favicon.js +4 -3
- package/utils/require-asset.ts +95 -0
- package/vue.config.js +4 -3
- package/components/HarvesterServiceAddOnConfig.vue +0 -207
|
@@ -379,7 +379,7 @@ export default {
|
|
|
379
379
|
/* Look for annotation to say this app is a legacy migrated app (we look in either place for now) */
|
|
380
380
|
this.migratedApp = (this.existing?.spec?.chart?.metadata?.annotations?.[CATALOG_ANNOTATIONS.MIGRATED] === 'true');
|
|
381
381
|
|
|
382
|
-
if (this.repo
|
|
382
|
+
if (this.repo?.isSuseAppCollection) {
|
|
383
383
|
let defaultSelectedSecret = await this.$store.getters['cluster/byId'](SECRET, `cattle-system/${ this.repo.spec.clientSecret.name }`);
|
|
384
384
|
|
|
385
385
|
if (!defaultSelectedSecret) {
|
|
@@ -823,7 +823,7 @@ export default {
|
|
|
823
823
|
}
|
|
824
824
|
}
|
|
825
825
|
|
|
826
|
-
if (this.repo
|
|
826
|
+
if (this.repo?.isSuseAppCollection) {
|
|
827
827
|
await this.initializeDataForNamespaceChanges();
|
|
828
828
|
}
|
|
829
829
|
},
|
|
@@ -938,7 +938,7 @@ export default {
|
|
|
938
938
|
|
|
939
939
|
async initializeDataForNamespaceChanges() {
|
|
940
940
|
// Skip the flow if the data still not fetched, it will trigger after fetching manually
|
|
941
|
-
if (
|
|
941
|
+
if (this.appCoDataFetched) {
|
|
942
942
|
try {
|
|
943
943
|
this.defaultImagePullSecret = await this.$store.dispatch('cluster/find', { type: SECRET, id: `${ this.targetNamespace }/${ this.repo.spec.clientSecret.name }-image-pull-secret` });
|
|
944
944
|
} catch (e) {
|
|
@@ -1028,7 +1028,7 @@ export default {
|
|
|
1028
1028
|
},
|
|
1029
1029
|
|
|
1030
1030
|
async setImagePullSecretData() {
|
|
1031
|
-
if (this.selectedSecret && this.repo
|
|
1031
|
+
if (this.selectedSecret && this.repo?.isSuseAppCollection && this.dontUseDefaultOption !== null) {
|
|
1032
1032
|
if (!this.dontUseDefaultOption && this.defaultImagePullSecret) {
|
|
1033
1033
|
// If the default option is used and the default secret already exists, use it
|
|
1034
1034
|
this.selectedImagePullSecret = this.defaultImagePullSecret.name;
|
|
@@ -1159,7 +1159,7 @@ export default {
|
|
|
1159
1159
|
|
|
1160
1160
|
// Create namespace if it doesn't exist (before hooks run)
|
|
1161
1161
|
// And only if it is SUSE APP Collection, overall should just do the same flow
|
|
1162
|
-
if (!isUpgrade && this.isNamespaceNew && this.repo
|
|
1162
|
+
if (!isUpgrade && this.isNamespaceNew && this.repo?.isSuseAppCollection) {
|
|
1163
1163
|
await this.createNamespaceIfNeeded();
|
|
1164
1164
|
}
|
|
1165
1165
|
|
|
@@ -1355,7 +1355,7 @@ export default {
|
|
|
1355
1355
|
...migratedAnnotations,
|
|
1356
1356
|
[CATALOG_ANNOTATIONS.SOURCE_REPO_TYPE]: this.chart.repoType,
|
|
1357
1357
|
[CATALOG_ANNOTATIONS.SOURCE_REPO_NAME]: this.chart.repoName,
|
|
1358
|
-
...(this.repo
|
|
1358
|
+
...(this.repo?.isSuseAppCollection ? { [CATALOG_ANNOTATIONS.SUSE_APP_COLLECTION]: 'true' } : {}),
|
|
1359
1359
|
},
|
|
1360
1360
|
values,
|
|
1361
1361
|
};
|
|
@@ -1497,7 +1497,7 @@ export default {
|
|
|
1497
1497
|
},
|
|
1498
1498
|
|
|
1499
1499
|
async createImagePullSecret() {
|
|
1500
|
-
if (!this.repo
|
|
1500
|
+
if (!this.repo?.isSuseAppCollection) {
|
|
1501
1501
|
return;
|
|
1502
1502
|
}
|
|
1503
1503
|
|
|
@@ -1655,7 +1655,6 @@ export default {
|
|
|
1655
1655
|
</div>
|
|
1656
1656
|
</div>
|
|
1657
1657
|
<NameNsDescription
|
|
1658
|
-
v-if="showNameEditor"
|
|
1659
1658
|
v-model:value="value"
|
|
1660
1659
|
:description-hidden="true"
|
|
1661
1660
|
:mode="mode"
|
|
@@ -1686,7 +1685,7 @@ export default {
|
|
|
1686
1685
|
</template>
|
|
1687
1686
|
</NameNsDescription>
|
|
1688
1687
|
<div
|
|
1689
|
-
v-if="repo
|
|
1688
|
+
v-if="repo?.isSuseAppCollection"
|
|
1690
1689
|
class="mb-20"
|
|
1691
1690
|
>
|
|
1692
1691
|
<Banner
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import { mapGetters } from 'vuex';
|
|
3
3
|
import { SERVICE } from '@shell/config/types';
|
|
4
4
|
import Loading from '@shell/components/Loading';
|
|
5
|
+
import kialiSvg from '~shell/assets/images/vendor/kiali.svg';
|
|
6
|
+
import jaegerSvg from '~shell/assets/images/vendor/jaeger.svg';
|
|
5
7
|
export default {
|
|
6
8
|
components: { Loading },
|
|
7
9
|
|
|
@@ -23,7 +25,7 @@ export default {
|
|
|
23
25
|
|
|
24
26
|
kialiLogo() {
|
|
25
27
|
// @TODO move to theme css
|
|
26
|
-
return
|
|
28
|
+
return kialiSvg;
|
|
27
29
|
},
|
|
28
30
|
|
|
29
31
|
kialiUrl() {
|
|
@@ -31,7 +33,7 @@ export default {
|
|
|
31
33
|
},
|
|
32
34
|
|
|
33
35
|
jaegerLogo() {
|
|
34
|
-
return
|
|
36
|
+
return jaegerSvg;
|
|
35
37
|
},
|
|
36
38
|
|
|
37
39
|
jaegerUrl() {
|
|
@@ -3,6 +3,7 @@ import { mapGetters } from 'vuex';
|
|
|
3
3
|
import { SERVICE } from '@shell/config/types';
|
|
4
4
|
import IconMessage from '@shell/components/IconMessage';
|
|
5
5
|
import LazyImage from '@shell/components/LazyImage';
|
|
6
|
+
import longhornSvg from '~shell/assets/images/vendor/longhorn.svg';
|
|
6
7
|
import Loading from '@shell/components/Loading';
|
|
7
8
|
|
|
8
9
|
export default {
|
|
@@ -24,7 +25,7 @@ export default {
|
|
|
24
25
|
|
|
25
26
|
data() {
|
|
26
27
|
return {
|
|
27
|
-
longhornImgSrc:
|
|
28
|
+
longhornImgSrc: longhornSvg,
|
|
28
29
|
uiServices: null
|
|
29
30
|
};
|
|
30
31
|
},
|
|
@@ -9,6 +9,8 @@ import LazyImage from '@shell/components/LazyImage';
|
|
|
9
9
|
import SimpleBox from '@shell/components/SimpleBox';
|
|
10
10
|
import { canViewAlertManagerLink, canViewGrafanaLink, canViewPrometheusLink } from '@shell/utils/monitoring';
|
|
11
11
|
import Loading from '@shell/components/Loading';
|
|
12
|
+
import grafanaSrc from '~shell/assets/images/vendor/grafana.svg';
|
|
13
|
+
import prometheusSrc from '~shell/assets/images/vendor/prometheus.svg';
|
|
12
14
|
|
|
13
15
|
export default {
|
|
14
16
|
components: {
|
|
@@ -23,8 +25,6 @@ export default {
|
|
|
23
25
|
},
|
|
24
26
|
|
|
25
27
|
data() {
|
|
26
|
-
const grafanaSrc = require('~shell/assets/images/vendor/grafana.svg');
|
|
27
|
-
const prometheusSrc = require('~shell/assets/images/vendor/prometheus.svg');
|
|
28
28
|
const currentCluster = this.$store.getters['currentCluster'];
|
|
29
29
|
|
|
30
30
|
return {
|
|
@@ -4,6 +4,7 @@ import { NEU_VECTOR_NAMESPACE } from '@shell/config/product/neuvector';
|
|
|
4
4
|
|
|
5
5
|
import LazyImage from '@shell/components/LazyImage';
|
|
6
6
|
import Loading from '@shell/components/Loading';
|
|
7
|
+
import neuvectorSvg from '~shell/assets/images/vendor/neuvector.svg';
|
|
7
8
|
|
|
8
9
|
export default {
|
|
9
10
|
components: { LazyImage, Loading },
|
|
@@ -11,7 +12,7 @@ export default {
|
|
|
11
12
|
data() {
|
|
12
13
|
return {
|
|
13
14
|
externalLinks: [],
|
|
14
|
-
neuvectorImgSrc:
|
|
15
|
+
neuvectorImgSrc: neuvectorSvg,
|
|
15
16
|
};
|
|
16
17
|
},
|
|
17
18
|
|
|
@@ -237,11 +237,6 @@ export default {
|
|
|
237
237
|
{{ t('performance.serverPagination.label') }}
|
|
238
238
|
</h2>
|
|
239
239
|
<p>{{ t('performance.serverPagination.description') }}</p>
|
|
240
|
-
<Banner
|
|
241
|
-
color="warning"
|
|
242
|
-
>
|
|
243
|
-
<div v-clean-html="t(`performance.serverPagination.featureFlag`, { ffUrl }, true)" />
|
|
244
|
-
</Banner>
|
|
245
240
|
<Collapse
|
|
246
241
|
:title="t('performance.serverPagination.applicable')"
|
|
247
242
|
:open="steveCacheEnabled && ssPApplicableTypesOpen"
|
|
@@ -3,6 +3,7 @@ import { mapGetters } from 'vuex';
|
|
|
3
3
|
import ChartReadme from '@shell/components/ChartReadme';
|
|
4
4
|
import LazyImage from '@shell/components/LazyImage';
|
|
5
5
|
import { MANAGEMENT } from '@shell/config/types';
|
|
6
|
+
import genericPluginSvg from '~shell/assets/images/generic-plugin.svg';
|
|
6
7
|
import { SETTING } from '@shell/config/settings';
|
|
7
8
|
import { useWatcherBasedSetupFocusTrapWithDestroyIncluded } from '@shell/composables/focusTrap';
|
|
8
9
|
import { getPluginChartVersionLabel, getPluginChartVersion } from '@shell/utils/uiplugins';
|
|
@@ -36,7 +37,7 @@ export default {
|
|
|
36
37
|
infoVersion: undefined,
|
|
37
38
|
versionInfo: undefined,
|
|
38
39
|
versionError: undefined,
|
|
39
|
-
defaultIcon:
|
|
40
|
+
defaultIcon: genericPluginSvg,
|
|
40
41
|
headerBannerSize: 0,
|
|
41
42
|
isActive: false
|
|
42
43
|
};
|
|
@@ -3,6 +3,7 @@ import { mapGetters } from 'vuex';
|
|
|
3
3
|
import day from 'dayjs';
|
|
4
4
|
import { mapPref, PLUGIN_DEVELOPER } from '@shell/store/prefs';
|
|
5
5
|
import { sortBy } from '@shell/utils/sort';
|
|
6
|
+
import genericPluginSvg from '~shell/assets/images/generic-plugin.svg';
|
|
6
7
|
import { allHash } from '@shell/utils/promise';
|
|
7
8
|
import { CATALOG, UI_PLUGIN, MANAGEMENT, ZERO_TIME } from '@shell/config/types';
|
|
8
9
|
import { SETTING } from '@shell/config/settings';
|
|
@@ -73,7 +74,7 @@ export default {
|
|
|
73
74
|
menuTargetEvent: null,
|
|
74
75
|
menuOpen: false,
|
|
75
76
|
hasFeatureFlag: true,
|
|
76
|
-
defaultIcon:
|
|
77
|
+
defaultIcon: genericPluginSvg,
|
|
77
78
|
reloadRequired: false,
|
|
78
79
|
rancherVersion: null
|
|
79
80
|
};
|
|
@@ -766,8 +766,7 @@ export const PAGINATION_SETTINGS_STORE_DEFAULTS: PaginationSettingsStores = {
|
|
|
766
766
|
{ resource: CAPI.RANCHER_CLUSTER, context: ['side-bar'] },
|
|
767
767
|
{ resource: MANAGEMENT.CLUSTER, context: ['side-bar'] },
|
|
768
768
|
{ resource: CATALOG.APP, context: ['branding'] },
|
|
769
|
-
SECRET
|
|
770
|
-
CAPI.MACHINE_SET
|
|
769
|
+
SECRET
|
|
771
770
|
],
|
|
772
771
|
generic: false,
|
|
773
772
|
}
|
|
@@ -131,11 +131,17 @@ const isWaitingForDestroy = (storeName, store) => {
|
|
|
131
131
|
};
|
|
132
132
|
|
|
133
133
|
const waitForSettingsSchema = (storeName, store) => {
|
|
134
|
-
return waitFor(
|
|
134
|
+
return waitFor(
|
|
135
|
+
() => isWaitingForDestroy(storeName, store) || !!store.getters['management/byId'](SCHEMA, MANAGEMENT.SETTING),
|
|
136
|
+
'management settings schema to be available'
|
|
137
|
+
);
|
|
135
138
|
};
|
|
136
139
|
|
|
137
140
|
const waitForSettings = (storeName, store) => {
|
|
138
|
-
return waitFor(
|
|
141
|
+
return waitFor(
|
|
142
|
+
() => isWaitingForDestroy(storeName, store) || !!store.getters['management/byId'](MANAGEMENT.SETTING, SETTING.UI_PERFORMANCE),
|
|
143
|
+
'UI performance settings to be available'
|
|
144
|
+
);
|
|
139
145
|
};
|
|
140
146
|
|
|
141
147
|
const isAdvancedWorker = (ctx) => {
|
|
@@ -195,8 +201,20 @@ export async function createWorker(store, ctx) {
|
|
|
195
201
|
};
|
|
196
202
|
}
|
|
197
203
|
|
|
198
|
-
|
|
199
|
-
|
|
204
|
+
try {
|
|
205
|
+
await waitForSettingsSchema(storeName, store);
|
|
206
|
+
await waitForSettings(storeName, store);
|
|
207
|
+
} catch (e) {
|
|
208
|
+
// Clean up the mock worker and abort so callers are not permanently blocked.
|
|
209
|
+
if (store.$workers[storeName]?.destroy) {
|
|
210
|
+
store.$workers[storeName].destroy();
|
|
211
|
+
} else {
|
|
212
|
+
delete store.$workers[storeName];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
200
218
|
if (store.$workers[storeName].waitingForDestroy()) {
|
|
201
219
|
store.$workers[storeName].destroy();
|
|
202
220
|
|
|
@@ -382,6 +400,13 @@ const sharedActions = {
|
|
|
382
400
|
if (!this.$workers[getters.storeName]) {
|
|
383
401
|
await createWorker(this, ctx);
|
|
384
402
|
}
|
|
403
|
+
|
|
404
|
+
// createWorker cleans up and returns early when schema/settings are unavailable.
|
|
405
|
+
// Guard against calling postMessage on a non-existent worker.
|
|
406
|
+
if (!this.$workers[getters.storeName]) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
385
410
|
const options = { parseJSON: false };
|
|
386
411
|
const csrf = rootGetters['cookies/get']({ key: CSRF, options });
|
|
387
412
|
|
|
@@ -253,7 +253,7 @@ button {
|
|
|
253
253
|
&.btn-small {
|
|
254
254
|
//:not(.btn-sm) is being used to make the style more specific to override global styles. We may want to get rid of those styles at some point.
|
|
255
255
|
&, &:not(.btn-sm) {
|
|
256
|
-
line-height:
|
|
256
|
+
line-height: 140%;
|
|
257
257
|
font-size: 12px;
|
|
258
258
|
min-height: 24px;
|
|
259
259
|
|
|
@@ -265,7 +265,7 @@ button {
|
|
|
265
265
|
&.btn-medium {
|
|
266
266
|
//:not(.btn-sm) is being used to make the style more specific to override global styles. We may want to get rid of those styles at some point.
|
|
267
267
|
&, &:not(.btn-sm) {
|
|
268
|
-
line-height:
|
|
268
|
+
line-height: 140%;
|
|
269
269
|
font-size: 14px;
|
|
270
270
|
min-height: 32px;
|
|
271
271
|
|
|
@@ -277,7 +277,7 @@ button {
|
|
|
277
277
|
&.btn-large {
|
|
278
278
|
// This is the default size brought by the global button styling
|
|
279
279
|
&, &:not(.btn-sm) {
|
|
280
|
-
line-height:
|
|
280
|
+
line-height: 140%;
|
|
281
281
|
font-size: 16px;
|
|
282
282
|
min-height: 40px;
|
|
283
283
|
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils';
|
|
2
|
+
import { defineComponent } from 'vue';
|
|
3
|
+
import RcButtonSplit from './RcButtonSplit.vue';
|
|
4
|
+
import { ButtonVariant, ButtonSize } from '@components/RcButton/types';
|
|
5
|
+
|
|
6
|
+
// v-dropdown is provided by floating-vue and must be mocked in unit tests.
|
|
7
|
+
// The default slot is the trigger anchor; the popper slot is the dropdown content.
|
|
8
|
+
const vDropdownMock = defineComponent({
|
|
9
|
+
template: `
|
|
10
|
+
<div>
|
|
11
|
+
<slot />
|
|
12
|
+
<slot name="popper" />
|
|
13
|
+
</div>
|
|
14
|
+
`,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const globalConfig = { global: { components: { 'v-dropdown': vDropdownMock } } };
|
|
18
|
+
|
|
19
|
+
// RcButton and RcDropdownTrigger both use single-root attribute inheritance, so
|
|
20
|
+
// the class added to the component falls through to the rendered <button> element.
|
|
21
|
+
// Selectors like '.rc-button-split-action' refer directly to the <button> element.
|
|
22
|
+
|
|
23
|
+
describe('rcButtonSplit.vue', () => {
|
|
24
|
+
it('renders the main action button', () => {
|
|
25
|
+
const wrapper = mount(RcButtonSplit, globalConfig);
|
|
26
|
+
|
|
27
|
+
expect(wrapper.find('.rc-button-split-action').exists()).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('renders the dropdown trigger', () => {
|
|
31
|
+
const wrapper = mount(RcButtonSplit, globalConfig);
|
|
32
|
+
|
|
33
|
+
expect(wrapper.find('.rc-button-split-trigger').exists()).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('emits click when the main action button is clicked', async() => {
|
|
37
|
+
const wrapper = mount(RcButtonSplit, globalConfig);
|
|
38
|
+
|
|
39
|
+
await wrapper.find('.rc-button-split-action').trigger('click');
|
|
40
|
+
|
|
41
|
+
expect(wrapper.emitted('click')).toHaveLength(1);
|
|
42
|
+
expect(wrapper.emitted('click')![0][0]).toBeInstanceOf(MouseEvent);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('does not emit click when the dropdown trigger is clicked', async() => {
|
|
46
|
+
const wrapper = mount(RcButtonSplit, globalConfig);
|
|
47
|
+
|
|
48
|
+
await wrapper.find('.rc-button-split-trigger').trigger('click');
|
|
49
|
+
|
|
50
|
+
expect(wrapper.emitted('click')).toBeUndefined();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('variant prop', () => {
|
|
54
|
+
it.each([
|
|
55
|
+
['primary', 'variant-primary'],
|
|
56
|
+
['secondary', 'variant-secondary'],
|
|
57
|
+
['tertiary', 'variant-tertiary'],
|
|
58
|
+
] as [ButtonVariant, string][])('applies %s variant class to the action button', (variant, className) => {
|
|
59
|
+
const wrapper = mount(RcButtonSplit, { ...globalConfig, props: { variant } });
|
|
60
|
+
|
|
61
|
+
expect(wrapper.find('.rc-button-split-action').classes()).toContain(className);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it.each([
|
|
65
|
+
['primary', 'variant-primary'],
|
|
66
|
+
['secondary', 'variant-secondary'],
|
|
67
|
+
['tertiary', 'variant-tertiary'],
|
|
68
|
+
] as [ButtonVariant, string][])('applies %s variant class to the dropdown trigger button', (variant, className) => {
|
|
69
|
+
const wrapper = mount(RcButtonSplit, { ...globalConfig, props: { variant } });
|
|
70
|
+
|
|
71
|
+
expect(wrapper.find('.rc-button-split-trigger').classes()).toContain(className);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('size prop', () => {
|
|
76
|
+
it.each([
|
|
77
|
+
['small', 'btn-small'],
|
|
78
|
+
['medium', 'btn-medium'],
|
|
79
|
+
['large', 'btn-large'],
|
|
80
|
+
] as [ButtonSize, string][])('applies %s size class to the action button', (size, className) => {
|
|
81
|
+
const wrapper = mount(RcButtonSplit, { ...globalConfig, props: { size } });
|
|
82
|
+
|
|
83
|
+
expect(wrapper.find('.rc-button-split-action').classes()).toContain(className);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it.each([
|
|
87
|
+
['small', 'btn-small'],
|
|
88
|
+
['medium', 'btn-medium'],
|
|
89
|
+
['large', 'btn-large'],
|
|
90
|
+
] as [ButtonSize, string][])('applies %s size class to the dropdown trigger button', (size, className) => {
|
|
91
|
+
const wrapper = mount(RcButtonSplit, { ...globalConfig, props: { size } });
|
|
92
|
+
|
|
93
|
+
expect(wrapper.find('.rc-button-split-trigger').classes()).toContain(className);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('slots', () => {
|
|
98
|
+
it('renders default slot content in the main button', () => {
|
|
99
|
+
const wrapper = mount(RcButtonSplit, {
|
|
100
|
+
...globalConfig,
|
|
101
|
+
slots: { default: 'Save' },
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(wrapper.find('.rc-button-split-action').text()).toContain('Save');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('renders before slot content in the main button', () => {
|
|
108
|
+
const wrapper = mount(RcButtonSplit, {
|
|
109
|
+
...globalConfig,
|
|
110
|
+
slots: {
|
|
111
|
+
default: 'Save',
|
|
112
|
+
before: '<span class="before-content">Before</span>',
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
expect(wrapper.find('.rc-button-split-action .before-content').exists()).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('renders after slot content in the main button', () => {
|
|
120
|
+
const wrapper = mount(RcButtonSplit, {
|
|
121
|
+
...globalConfig,
|
|
122
|
+
slots: {
|
|
123
|
+
default: 'Save',
|
|
124
|
+
after: '<span class="after-content">After</span>',
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
expect(wrapper.find('.rc-button-split-action .after-content').exists()).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('renders dropdownCollection slot content', () => {
|
|
132
|
+
const wrapper = mount(RcButtonSplit, {
|
|
133
|
+
...globalConfig,
|
|
134
|
+
slots: { dropdownCollection: '<div class="dropdown-item-test">Item 1</div>' },
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
expect(wrapper.find('.dropdown-item-test').exists()).toBe(true);
|
|
138
|
+
expect(wrapper.find('.dropdown-item-test').text()).toStrictEqual('Item 1');
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('dropdown trigger has aria-haspopup="menu" attribute', () => {
|
|
143
|
+
const wrapper = mount(RcButtonSplit, globalConfig);
|
|
144
|
+
|
|
145
|
+
expect(wrapper.find('.rc-button-split-trigger').attributes('aria-haspopup')).toBe('menu');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('emits update:open when dropdown open state changes', async() => {
|
|
149
|
+
const wrapper = mount(RcButtonSplit, globalConfig);
|
|
150
|
+
|
|
151
|
+
await wrapper.findComponent({ name: 'RcDropdown' }).vm.$emit('update:open', true);
|
|
152
|
+
|
|
153
|
+
expect(wrapper.emitted('update:open')).toHaveLength(1);
|
|
154
|
+
expect(wrapper.emitted('update:open')![0]).toStrictEqual([true]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('applies default variant "primary" when no variant is provided', () => {
|
|
158
|
+
const wrapper = mount(RcButtonSplit, globalConfig);
|
|
159
|
+
|
|
160
|
+
expect(wrapper.find('.rc-button-split-action').classes()).toContain('variant-primary');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('applies default size "medium" when no size is provided', () => {
|
|
164
|
+
const wrapper = mount(RcButtonSplit, globalConfig);
|
|
165
|
+
|
|
166
|
+
expect(wrapper.find('.rc-button-split-action').classes()).toContain('btn-medium');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('aria label props', () => {
|
|
170
|
+
it('ariaLabel is applied to the action button', () => {
|
|
171
|
+
const wrapper = mount(RcButtonSplit, { ...globalConfig, props: { ariaLabel: 'Save document' } });
|
|
172
|
+
|
|
173
|
+
expect(wrapper.find('.rc-button-split-action').attributes('aria-label')).toBe('Save document');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('ariaLabel is absent from the trigger button', () => {
|
|
177
|
+
const wrapper = mount(RcButtonSplit, { ...globalConfig, props: { ariaLabel: 'Save document' } });
|
|
178
|
+
|
|
179
|
+
expect(wrapper.find('.rc-button-split-trigger').attributes('aria-label')).toBeUndefined();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('ariaLabelTrigger is applied to the trigger button', () => {
|
|
183
|
+
const wrapper = mount(RcButtonSplit, { ...globalConfig, props: { ariaLabelTrigger: 'More save options' } });
|
|
184
|
+
|
|
185
|
+
expect(wrapper.find('.rc-button-split-trigger').attributes('aria-label')).toBe('More save options');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('ariaLabelTrigger is absent from the action button', () => {
|
|
189
|
+
const wrapper = mount(RcButtonSplit, { ...globalConfig, props: { ariaLabelTrigger: 'More save options' } });
|
|
190
|
+
|
|
191
|
+
expect(wrapper.find('.rc-button-split-action').attributes('aria-label')).toBeUndefined();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('ariaLabelDropdown is forwarded to RcDropdown', () => {
|
|
195
|
+
const wrapper = mount(RcButtonSplit, { ...globalConfig, props: { ariaLabelDropdown: 'Save actions' } });
|
|
196
|
+
|
|
197
|
+
expect(wrapper.findComponent({ name: 'RcDropdown' }).props('ariaLabel')).toBe('Save actions');
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('items prop', () => {
|
|
202
|
+
const items = [
|
|
203
|
+
{ id: 'draft', label: 'Save as Draft' },
|
|
204
|
+
{ id: 'template', label: 'Save as Template' },
|
|
205
|
+
{ id: 'discard', label: 'Discard Changes' },
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
it('renders an RcDropdownItem for each entry in items', () => {
|
|
209
|
+
const wrapper = mount(RcButtonSplit, { ...globalConfig, props: { items } });
|
|
210
|
+
|
|
211
|
+
const dropdownItems = wrapper.findAllComponents({ name: 'RcDropdownItem' });
|
|
212
|
+
|
|
213
|
+
expect(dropdownItems).toHaveLength(3);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('renders each item\'s label text', () => {
|
|
217
|
+
const wrapper = mount(RcButtonSplit, { ...globalConfig, props: { items } });
|
|
218
|
+
|
|
219
|
+
const dropdownItems = wrapper.findAllComponents({ name: 'RcDropdownItem' });
|
|
220
|
+
|
|
221
|
+
expect(dropdownItems[0].text()).toContain('Save as Draft');
|
|
222
|
+
expect(dropdownItems[1].text()).toContain('Save as Template');
|
|
223
|
+
expect(dropdownItems[2].text()).toContain('Discard Changes');
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('emits select with the item id when a prop item is clicked', async() => {
|
|
227
|
+
const wrapper = mount(RcButtonSplit, { ...globalConfig, props: { items } });
|
|
228
|
+
|
|
229
|
+
await wrapper.findAllComponents({ name: 'RcDropdownItem' })[0].trigger('click');
|
|
230
|
+
|
|
231
|
+
expect(wrapper.emitted('select')).toHaveLength(1);
|
|
232
|
+
expect(wrapper.emitted('select')![0]).toStrictEqual(['draft']);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('does not emit select when no items prop is provided', async() => {
|
|
236
|
+
const wrapper = mount(RcButtonSplit, globalConfig);
|
|
237
|
+
|
|
238
|
+
expect(wrapper.emitted('select')).toBeUndefined();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('renders both prop items and dropdownCollection slot content when both are supplied', () => {
|
|
242
|
+
const wrapper = mount(RcButtonSplit, {
|
|
243
|
+
...globalConfig,
|
|
244
|
+
props: { items: [{ id: 'draft', label: 'Save as Draft' }] },
|
|
245
|
+
slots: { dropdownCollection: '<div class="slot-item">Slot Item</div>' },
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
expect(wrapper.findAllComponents({ name: 'RcDropdownItem' })).toHaveLength(1);
|
|
249
|
+
expect(wrapper.find('.slot-item').exists()).toBe(true);
|
|
250
|
+
expect(wrapper.find('.slot-item').text()).toStrictEqual('Slot Item');
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
});
|