@rancher/shell 3.0.5-rc.7 → 3.0.5-rc.9
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/brand/classic/metadata.json +3 -0
- package/assets/styles/app.scss +1 -0
- package/assets/styles/base/_color.scss +19 -0
- package/assets/styles/base/_helpers.scss +10 -0
- package/assets/styles/base/_variables.scss +1 -1
- package/assets/styles/fonts/_icons.scss +1 -32
- package/assets/styles/global/_layout.scss +1 -1
- package/assets/styles/global/_tooltip.scss +7 -4
- package/assets/styles/themes/_dark.scss +272 -259
- package/assets/styles/themes/_light.scss +551 -516
- package/assets/styles/themes/_modern.scss +936 -0
- package/assets/translations/en-us.yaml +219 -38
- package/assets/translations/zh-hans.yaml +0 -1
- package/chart/__tests__/S3.test.ts +2 -1
- package/chart/monitoring/grafana/index.vue +8 -2
- package/cloud-credential/generic.vue +18 -10
- package/cloud-credential/harvester.vue +1 -9
- package/components/ActionMenuShell.vue +3 -1
- package/components/AdvancedSection.vue +8 -0
- package/components/ChartReadme.vue +17 -7
- package/components/Cron/CronExpressionEditor.vue +299 -0
- package/components/Cron/CronExpressionEditorModal.vue +247 -0
- package/components/Cron/CronTooltip.vue +87 -0
- package/components/Cron/types.ts +13 -0
- package/components/Drawer/ResourceDetailDrawer/__tests__/composables.test.ts +1 -26
- package/components/Drawer/ResourceDetailDrawer/composables.ts +0 -23
- package/components/Drawer/ResourceDetailDrawer/index.vue +17 -4
- package/components/ForceDirectedTreeChart/composable.ts +11 -0
- package/components/InstallHelmCharts.vue +656 -0
- package/components/LazyImage.vue +60 -4
- package/components/LocaleSelector.vue +7 -2
- package/components/Markdown.vue +4 -0
- package/components/PromptModal.vue +1 -1
- package/components/Resource/Detail/Card/__tests__/StateCard.test.ts +1 -0
- package/components/Resource/Detail/CopyToClipboard.vue +78 -0
- package/components/Resource/Detail/FetchLoader/__tests__/composables.test.ts +69 -0
- package/components/Resource/Detail/FetchLoader/composables.ts +27 -0
- package/components/Resource/Detail/Masthead/composable.ts +16 -0
- package/components/Resource/Detail/Masthead/index.vue +37 -0
- package/components/Resource/Detail/Metadata/Annotations/__tests__/index.test.ts +1 -1
- package/components/Resource/Detail/Metadata/Annotations/index.vue +1 -1
- package/components/Resource/Detail/Metadata/IdentifyingInformation/__tests__/identifying-fields.test.ts +13 -61
- package/components/Resource/Detail/Metadata/IdentifyingInformation/__tests__/index.test.ts +33 -6
- package/components/Resource/Detail/Metadata/IdentifyingInformation/identifying-fields.ts +29 -43
- package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +25 -5
- package/components/Resource/Detail/Metadata/KeyValue.vue +12 -23
- package/components/Resource/Detail/Metadata/KeyValueRow.vue +144 -0
- package/components/Resource/Detail/Metadata/Labels/__tests__/index.test.ts +1 -0
- package/components/Resource/Detail/Metadata/Labels/index.vue +1 -0
- package/components/Resource/Detail/Metadata/__tests__/KeyValue.test.ts +30 -32
- package/components/Resource/Detail/Metadata/__tests__/KeyValueRow.test.ts +108 -0
- package/components/Resource/Detail/Metadata/__tests__/composables.test.ts +10 -20
- package/components/Resource/Detail/Metadata/__tests__/index.test.ts +12 -5
- package/components/Resource/Detail/Metadata/composables.ts +9 -10
- package/components/Resource/Detail/Metadata/index.vue +18 -2
- package/components/Resource/Detail/Page.vue +35 -21
- package/components/Resource/Detail/Preview/Content.vue +63 -0
- package/components/Resource/Detail/Preview/Preview.vue +128 -0
- package/components/Resource/Detail/Preview/__tests__/Content.spec.ts +71 -0
- package/components/Resource/Detail/Preview/__tests__/Preview.spec.ts +121 -0
- package/components/Resource/Detail/ResourcePopover/ResourcePopoverCard.vue +141 -0
- package/components/Resource/Detail/ResourcePopover/__tests__/ResourcePopoverCard.test.ts +136 -0
- package/components/Resource/Detail/ResourcePopover/__tests__/index.test.ts +245 -0
- package/components/Resource/Detail/ResourcePopover/index.vue +226 -0
- package/components/Resource/Detail/SpacedRow.vue +1 -0
- package/components/Resource/Detail/TitleBar/__tests__/composables.test.ts +8 -14
- package/components/Resource/Detail/TitleBar/__tests__/index.test.ts +1 -1
- package/components/Resource/Detail/TitleBar/composables.ts +3 -6
- package/components/Resource/Detail/TitleBar/index.vue +11 -29
- package/components/Resource/Detail/ViewOptions/composable.ts +9 -0
- package/components/Resource/Detail/ViewOptions/index.vue +41 -0
- package/components/Resource/Detail/__tests__/CopyToClipboard.spec.ts +82 -0
- package/components/ResourceDetail/Masthead/legacy.vue +0 -19
- package/components/ResourceDetail/index.vue +544 -74
- package/components/ResourceTable.vue +24 -0
- package/components/SlideInPanelManager.vue +10 -3
- package/components/SortableTable/index.vue +11 -5
- package/components/SortableTable/paging.js +3 -0
- package/components/Tabbed/Tab.vue +43 -1
- package/components/Tabbed/index.vue +32 -4
- package/components/__tests__/Cron/CronExpressionEditor.test.ts +151 -0
- package/components/__tests__/Cron/CronExpressionEditorModal.test.ts +81 -0
- package/components/__tests__/LazyImage.spec.ts +121 -0
- package/components/auth/login/saml.vue +86 -0
- package/components/fleet/FleetStatus.vue +4 -0
- package/components/form/ClusterAppearance.vue +5 -0
- package/components/form/LabeledSelect.vue +8 -8
- package/components/form/Members/ClusterPermissionsEditor.vue +1 -1
- package/components/form/ProjectMemberEditor.vue +1 -1
- package/components/form/ResourceLabeledSelect.vue +19 -6
- package/components/form/ResourceTabs/composable.ts +54 -0
- package/components/form/ResourceTabs/index.vue +30 -7
- package/components/form/SecretSelector.vue +9 -0
- package/components/form/Select.vue +13 -10
- package/components/form/__tests__/LabeledSelect.test.ts +133 -0
- package/components/form/__tests__/Select.test.ts +134 -0
- package/components/form/labeled-select-utils/labeled-select-pagination.ts +3 -38
- package/components/formatter/FleetApplicationSource.vue +25 -17
- package/components/nav/Favorite.vue +4 -0
- package/components/nav/NotificationCenter/Notification.vue +1 -27
- package/components/nav/WindowManager/index.vue +3 -3
- package/composables/useExtensionManager.ts +17 -0
- package/config/home-links.js +12 -0
- package/config/labels-annotations.js +1 -3
- package/config/page-actions.js +0 -1
- package/config/product/explorer.js +3 -1
- package/config/product/fleet.js +2 -7
- package/config/product/manager.js +0 -5
- package/config/query-params.js +1 -0
- package/config/router/navigation-guards/clusters.js +2 -1
- package/config/router/navigation-guards/products.js +1 -1
- package/core/extension-manager-impl.js +518 -0
- package/core/plugins.js +35 -468
- package/core/types.ts +8 -2
- package/detail/__tests__/autoscaling.horizontalpodautoscaler.test.ts +1 -0
- package/detail/__tests__/provisioning.cattle.io.cluster.test.ts +11 -0
- package/detail/__tests__/workload.test.ts +164 -0
- package/detail/catalog.cattle.io.app.vue +7 -4
- package/detail/configmap.vue +33 -75
- package/detail/fleet.cattle.io.bundle.vue +1 -5
- package/detail/fleet.cattle.io.cluster.vue +3 -2
- package/detail/fleet.cattle.io.gitrepo.vue +76 -49
- package/detail/fleet.cattle.io.helmop.vue +78 -49
- package/detail/projectsecret.vue +11 -0
- package/detail/provisioning.cattle.io.cluster.vue +350 -324
- package/detail/secret.vue +49 -308
- package/detail/workload/index.vue +38 -21
- package/dialog/AddonConfigConfirmationDialog.vue +1 -1
- package/dialog/GenericPrompt.vue +1 -1
- package/dialog/ImportDialog.vue +9 -2
- package/dialog/InstallExtensionDialog.vue +26 -15
- package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +2 -1
- package/edit/__tests__/fleet.cattle.io.helmop.test.ts +224 -0
- package/edit/__tests__/resources.cattle.io.restore.test.ts +106 -0
- package/edit/cloudcredential.vue +31 -17
- package/edit/constraints.gatekeeper.sh.constraint/index.vue +10 -2
- package/edit/fleet.cattle.io.cluster.vue +19 -0
- package/edit/fleet.cattle.io.gitrepo.vue +28 -22
- package/edit/fleet.cattle.io.helmop.vue +78 -56
- package/edit/logging.banzaicloud.io.output/index.vue +1 -1
- package/edit/logging.banzaicloud.io.output/providers/awsElasticsearch.vue +5 -6
- package/edit/monitoring.coreos.com.alertmanagerconfig/index.vue +12 -11
- package/edit/monitoring.coreos.com.alertmanagerconfig/receiverConfig.vue +11 -1
- package/edit/networking.k8s.io.ingress/Certificate.vue +9 -11
- package/edit/networking.k8s.io.ingress/DefaultBackend.vue +8 -3
- package/edit/networking.k8s.io.ingress/Rule.vue +2 -5
- package/edit/networking.k8s.io.ingress/RulePath.vue +17 -11
- package/edit/networking.k8s.io.networkpolicy/PolicyRuleTarget.vue +11 -10
- package/edit/networking.k8s.io.networkpolicy/PolicyRules.vue +1 -3
- package/edit/networking.k8s.io.networkpolicy/index.vue +17 -17
- package/edit/provisioning.cattle.io.cluster/index.vue +14 -19
- package/edit/provisioning.cattle.io.cluster/rke2.vue +31 -15
- package/edit/provisioning.cattle.io.cluster/tabs/AgentConfiguration.vue +9 -7
- package/edit/provisioning.cattle.io.cluster/tabs/DirectoryConfig.vue +10 -12
- package/edit/provisioning.cattle.io.cluster/tabs/MachinePool.vue +39 -38
- package/edit/provisioning.cattle.io.cluster/tabs/etcd/S3Config.vue +41 -19
- package/edit/provisioning.cattle.io.cluster/tabs/etcd/index.vue +16 -3
- package/edit/provisioning.cattle.io.cluster/tabs/registries/RegistryConfigs.vue +30 -31
- package/edit/provisioning.cattle.io.cluster/tabs/registries/RegistryMirrors.vue +9 -10
- package/edit/provisioning.cattle.io.cluster/tabs/registries/index.vue +1 -3
- package/edit/provisioning.cattle.io.cluster/tabs/upgrade/DrainOptions.vue +16 -9
- package/edit/resources.cattle.io.restore.vue +5 -8
- package/edit/workload/index.vue +5 -14
- package/list/__tests__/workload.test.ts +1 -0
- package/list/provisioning.cattle.io.cluster.vue +1 -69
- package/list/workload.vue +8 -1
- package/machine-config/__tests__/vmwarevsphere.test.ts +5 -7
- package/machine-config/components/GCEImage.vue +6 -5
- package/machine-config/google.vue +20 -7
- package/machine-config/vmwarevsphere.vue +7 -17
- package/mixins/__tests__/chart.test.ts +139 -1
- package/mixins/chart.js +58 -20
- package/mixins/resource-fetch-api-pagination.js +3 -4
- package/models/__tests__/chart.test.ts +111 -80
- package/models/__tests__/fleet.cattle.io.helmop.test.ts +224 -0
- package/models/__tests__/namespace.test.ts +69 -0
- package/models/__tests__/node.test.ts +7 -63
- package/models/apps.statefulset.js +8 -10
- package/models/catalog.cattle.io.app.js +1 -1
- package/models/catalog.cattle.io.operation.js +1 -1
- package/models/chart.js +41 -21
- package/models/cloudcredential.js +2 -163
- package/models/cluster/node.js +7 -7
- package/models/cluster.x-k8s.io.machine.js +3 -3
- package/models/compliance.cattle.io.clusterscan.js +2 -2
- package/models/configmap.js +4 -0
- package/models/constraints.gatekeeper.sh.constraint.js +1 -1
- package/models/fleet-application.js +16 -63
- package/models/fleet.cattle.io.bundle.js +1 -38
- package/models/fleet.cattle.io.gitrepo.js +19 -1
- package/models/fleet.cattle.io.helmop.js +30 -22
- package/models/management.cattle.io.project.js +12 -0
- package/models/management.cattle.io.setting.js +4 -0
- package/models/namespace.js +30 -0
- package/models/persistentvolumeclaim.js +1 -1
- package/models/pod.js +2 -2
- package/models/provisioning.cattle.io.cluster.js +16 -40
- package/models/rke.cattle.io.etcdsnapshot.js +1 -1
- package/models/secret.js +4 -0
- package/models/storage.k8s.io.storageclass.js +2 -2
- package/models/workload.js +6 -3
- package/package.json +19 -18
- package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +26 -10
- package/pages/c/_cluster/apps/charts/AppChartCardSubHeader.vue +4 -1
- package/pages/c/_cluster/apps/charts/__tests__/AppChartCardFooter.spec.js +41 -0
- package/pages/c/_cluster/apps/charts/chart.vue +440 -183
- package/pages/c/_cluster/apps/charts/index.vue +1 -0
- package/pages/c/_cluster/apps/charts/install.vue +7 -6
- package/pages/c/_cluster/explorer/projectsecret.vue +3 -13
- package/pages/c/_cluster/explorer/tools/__tests__/index.test.ts +102 -12
- package/pages/c/_cluster/explorer/tools/index.vue +145 -254
- package/pages/c/_cluster/fleet/__tests__/index.test.ts +608 -314
- package/pages/c/_cluster/fleet/index.vue +103 -44
- package/pages/c/_cluster/manager/cloudCredential/index.vue +20 -60
- package/pages/c/_cluster/manager/drivers/kontainerDriver/index.vue +12 -2
- package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +11 -4
- package/pages/c/_cluster/uiplugins/__tests__/index.spec.ts +318 -0
- package/pages/c/_cluster/uiplugins/index.vue +256 -387
- package/pages/home.vue +1 -9
- package/plugins/dashboard-store/actions.js +42 -22
- package/plugins/dashboard-store/resource-class.js +80 -0
- package/plugins/steve/__tests__/getters.test.ts +1 -1
- package/plugins/steve/__tests__/subscribe.spec.ts +259 -1
- package/plugins/steve/getters.js +8 -2
- package/plugins/steve/resourceWatcher.js +10 -3
- package/plugins/steve/subscribe.js +192 -19
- package/plugins/steve/worker/web-worker.advanced.js +2 -0
- package/public/index.html +2 -1
- package/rancher-components/Card/Card.vue +1 -19
- package/rancher-components/Form/Checkbox/Checkbox.vue +1 -1
- package/rancher-components/Form/Radio/RadioButton.vue +1 -1
- package/rancher-components/Form/Radio/RadioGroup.vue +1 -1
- package/rancher-components/LabeledTooltip/LabeledTooltip.vue +1 -11
- package/rancher-components/Pill/RcCounterBadge/RcCounterBadge.test.ts +53 -0
- package/rancher-components/Pill/RcCounterBadge/RcCounterBadge.vue +65 -0
- package/rancher-components/Pill/RcCounterBadge/index.ts +1 -0
- package/rancher-components/Pill/RcCounterBadge/types.ts +7 -0
- package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.test.ts +15 -0
- package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.vue +65 -0
- package/rancher-components/Pill/RcStatusBadge/index.ts +2 -0
- package/rancher-components/Pill/RcStatusBadge/types.ts +5 -0
- package/rancher-components/Pill/RcStatusIndicator/RcStatusIndicator.test.ts +33 -0
- package/rancher-components/Pill/RcStatusIndicator/RcStatusIndicator.vue +75 -0
- package/rancher-components/Pill/RcStatusIndicator/index.ts +2 -0
- package/rancher-components/Pill/RcStatusIndicator/types.ts +7 -0
- package/rancher-components/Pill/RcTag/RcTag.test.ts +64 -0
- package/rancher-components/Pill/RcTag/RcTag.vue +94 -0
- package/rancher-components/Pill/RcTag/index.ts +1 -0
- package/rancher-components/Pill/RcTag/types.ts +9 -0
- package/rancher-components/Pill/types.ts +3 -0
- package/rancher-components/RcButton/RcButton.vue +1 -1
- package/rancher-components/RcDropdown/RcDropdown.test.ts +98 -0
- package/rancher-components/RcDropdown/RcDropdown.vue +5 -0
- package/rancher-components/RcDropdown/RcDropdownItem.vue +7 -1
- package/rancher-components/RcDropdown/RcDropdownItemCheckbox.vue +2 -1
- package/rancher-components/RcDropdown/RcDropdownItemSelect.vue +2 -1
- package/rancher-components/RcDropdown/useDropdownContext.ts +21 -0
- package/rancher-components/RcDropdown/useDropdownItem.ts +30 -1
- package/rancher-components/RcItemCard/RcItemCard.test.ts +20 -0
- package/rancher-components/RcItemCard/RcItemCard.vue +41 -6
- package/rancher-components/RcItemCard/RcItemCardAction.vue +12 -0
- package/store/__tests__/catalog.test.ts +156 -1
- package/store/aws.js +19 -8
- package/store/catalog.js +10 -5
- package/store/type-map.js +3 -15
- package/types/extension-manager.ts +26 -0
- package/types/resources/settings.d.ts +1 -1
- package/types/shell/index.d.ts +149 -44
- package/types/uiplugins.ts +73 -0
- package/utils/__tests__/back-off.test.ts +354 -0
- package/utils/__tests__/kontainer.test.ts +19 -0
- package/utils/__tests__/product.test.ts +129 -0
- package/utils/__tests__/resource.test.ts +87 -0
- package/utils/__tests__/uiplugins.test.ts +84 -0
- package/utils/alertmanagerconfig.js +2 -2
- package/utils/auth.js +3 -76
- package/utils/back-off.ts +176 -0
- package/utils/dynamic-importer.js +8 -0
- package/utils/kontainer.ts +3 -5
- package/utils/product.ts +39 -0
- package/utils/resource.ts +35 -0
- package/utils/select.js +0 -24
- package/utils/style.ts +3 -0
- package/utils/uiplugins.ts +29 -2
- package/utils/validators/__tests__/setting.test.js +92 -0
- package/utils/validators/formRules/__tests__/index.test.ts +91 -7
- package/utils/validators/formRules/index.ts +84 -8
- package/utils/validators/setting.js +17 -0
- package/vue.config.js +1 -1
- package/cloud-credential/__tests__/harvester.test.ts +0 -18
- package/components/Resource/Detail/Metadata/Rectangle.vue +0 -34
- package/components/Resource/Detail/Metadata/__tests__/Rectangle.test.ts +0 -24
- package/components/ResourceDetail/Masthead/__tests__/legacy.test.ts +0 -65
- package/components/ResourceDetail/__tests__/index.test.ts +0 -135
- package/components/ResourceDetail/legacy.vue +0 -562
- package/components/formatter/CloudCredExpired.vue +0 -69
- package/pages/explorer/resource/detail/configmap.vue +0 -42
- package/pages/explorer/resource/detail/projectsecret.vue +0 -9
- package/pages/explorer/resource/detail/secret.vue +0 -63
- package/utils/aws.js +0 -0
- /package/components/{ForceDirectedTreeChart.vue → ForceDirectedTreeChart/index.vue} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
-
import { computed, onBeforeUnmount, watch } from 'vue';
|
|
2
|
+
import { computed, onBeforeUnmount, watch, useTemplateRef } from 'vue';
|
|
3
3
|
import { useStore } from 'vuex';
|
|
4
4
|
import {
|
|
5
5
|
DEFAULT_FOCUS_TRAP_OPTS,
|
|
@@ -10,6 +10,9 @@ import { useRouter } from 'vue-router';
|
|
|
10
10
|
|
|
11
11
|
const HEADER_HEIGHT = 55;
|
|
12
12
|
|
|
13
|
+
const slideInPanelManager = useTemplateRef('SlideInPanelManager');
|
|
14
|
+
const slideInPanelManagerClose = useTemplateRef('SlideInPanelManagerClose');
|
|
15
|
+
|
|
13
16
|
const store = useStore();
|
|
14
17
|
const isOpen = computed(() => store.getters['slideInPanel/isOpen']);
|
|
15
18
|
const isClosing = computed(() => store.getters['slideInPanel/isClosing']);
|
|
@@ -72,7 +75,9 @@ watch(
|
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
return returnFocusSelector || '.dashboard-root';
|
|
75
|
-
}
|
|
78
|
+
},
|
|
79
|
+
// putting the initial focus on the first element that is not conditionally displayed
|
|
80
|
+
initialFocus: slideInPanelManagerClose.value
|
|
76
81
|
};
|
|
77
82
|
|
|
78
83
|
useWatcherBasedSetupFocusTrapWithDestroyIncluded(
|
|
@@ -83,7 +88,7 @@ watch(
|
|
|
83
88
|
|
|
84
89
|
return isOpen?.value && !isClosing?.value;
|
|
85
90
|
},
|
|
86
|
-
|
|
91
|
+
slideInPanelManager.value as HTMLElement,
|
|
87
92
|
opts,
|
|
88
93
|
false
|
|
89
94
|
);
|
|
@@ -128,6 +133,7 @@ function closePanel() {
|
|
|
128
133
|
<Teleport to="#slides">
|
|
129
134
|
<div
|
|
130
135
|
id="slide-in-panel-manager"
|
|
136
|
+
ref="SlideInPanelManager"
|
|
131
137
|
@keydown.escape="closePanel"
|
|
132
138
|
>
|
|
133
139
|
<div
|
|
@@ -155,6 +161,7 @@ function closePanel() {
|
|
|
155
161
|
{{ panelTitle }}
|
|
156
162
|
</div>
|
|
157
163
|
<i
|
|
164
|
+
ref="SlideInPanelManagerClose"
|
|
158
165
|
class="icon icon-close"
|
|
159
166
|
data-testid="slide-in-close"
|
|
160
167
|
:tabindex="isOpen ? 0 : -1"
|
|
@@ -3,7 +3,7 @@ import { mapGetters, useStore } from 'vuex';
|
|
|
3
3
|
import { defineAsyncComponent, ref, onMounted, onBeforeUnmount } from 'vue';
|
|
4
4
|
import day from 'dayjs';
|
|
5
5
|
import isEmpty from 'lodash/isEmpty';
|
|
6
|
-
import { dasherize, ucFirst } from '@shell/utils/string';
|
|
6
|
+
import { dasherize, ucFirst, randomStr } from '@shell/utils/string';
|
|
7
7
|
import { get, clone } from '@shell/utils/object';
|
|
8
8
|
import { removeObject } from '@shell/utils/array';
|
|
9
9
|
import { Checkbox } from '@components/Form/Checkbox';
|
|
@@ -26,6 +26,7 @@ import ButtonMultiAction from '@shell/components/ButtonMultiAction.vue';
|
|
|
26
26
|
import ActionMenu from '@shell/components/ActionMenuShell.vue';
|
|
27
27
|
import { useRuntimeFlag } from '@shell/composables/useRuntimeFlag';
|
|
28
28
|
import ActionDropdownShell from '@shell/components/ActionDropdownShell.vue';
|
|
29
|
+
import { useTabCountUpdater } from '@shell/components/form/ResourceTabs/composable';
|
|
29
30
|
|
|
30
31
|
// Uncomment for table performance debugging
|
|
31
32
|
// import tableDebug from './debug';
|
|
@@ -51,7 +52,7 @@ export default {
|
|
|
51
52
|
'group-value-change',
|
|
52
53
|
'selection',
|
|
53
54
|
'rowClick',
|
|
54
|
-
'enter'
|
|
55
|
+
'enter'
|
|
55
56
|
],
|
|
56
57
|
|
|
57
58
|
components: {
|
|
@@ -432,6 +433,7 @@ export default {
|
|
|
432
433
|
$main?.addEventListener('scroll', this._onScroll);
|
|
433
434
|
|
|
434
435
|
this.debouncedPaginationChanged();
|
|
436
|
+
this.updateTabCount(this.totalRows);
|
|
435
437
|
},
|
|
436
438
|
|
|
437
439
|
beforeUnmount() {
|
|
@@ -445,6 +447,7 @@ export default {
|
|
|
445
447
|
const $main = document.querySelector('main');
|
|
446
448
|
|
|
447
449
|
$main?.removeEventListener('scroll', this._onScroll);
|
|
450
|
+
this.clearTabCount();
|
|
448
451
|
},
|
|
449
452
|
|
|
450
453
|
watch: {
|
|
@@ -559,10 +562,13 @@ export default {
|
|
|
559
562
|
|
|
560
563
|
const store = useStore();
|
|
561
564
|
const { featureDropdownMenu } = useRuntimeFlag(store);
|
|
565
|
+
const { updateTabCount, clearTabCount } = useTabCountUpdater();
|
|
562
566
|
|
|
563
567
|
return {
|
|
564
568
|
table,
|
|
565
569
|
featureDropdownMenu,
|
|
570
|
+
updateTabCount,
|
|
571
|
+
clearTabCount
|
|
566
572
|
};
|
|
567
573
|
},
|
|
568
574
|
|
|
@@ -735,7 +741,7 @@ export default {
|
|
|
735
741
|
grp.rows.forEach((row) => {
|
|
736
742
|
const rowData = {
|
|
737
743
|
row,
|
|
738
|
-
key: this.get(row, this.keyField),
|
|
744
|
+
key: this.get(row, this.keyField) ?? randomStr(),
|
|
739
745
|
showSubRow: this.showSubRow(row, this.keyField),
|
|
740
746
|
canRunBulkActionOfInterest: this.canRunBulkActionOfInterest(row),
|
|
741
747
|
columns: []
|
|
@@ -1038,7 +1044,7 @@ export default {
|
|
|
1038
1044
|
handleActionButtonClick(i, event) {
|
|
1039
1045
|
// Each row in the table gets its own ref with
|
|
1040
1046
|
// a number based on its index. If you are using
|
|
1041
|
-
// an ActionMenu that
|
|
1047
|
+
// an ActionMenu that doesn't have a dependency on Vuex,
|
|
1042
1048
|
// these refs are useful because you can reuse the
|
|
1043
1049
|
// same ActionMenu component on a page with many different
|
|
1044
1050
|
// target elements in a list,
|
|
@@ -1398,7 +1404,7 @@ export default {
|
|
|
1398
1404
|
</slot>
|
|
1399
1405
|
<template
|
|
1400
1406
|
v-for="(row, i) in groupedRows.rows"
|
|
1401
|
-
:key="
|
|
1407
|
+
:key="row.key"
|
|
1402
1408
|
>
|
|
1403
1409
|
<slot
|
|
1404
1410
|
name="main-row"
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script>
|
|
2
|
+
import { useTabCountWatcher } from '@shell/components/form/ResourceTabs/composable';
|
|
3
|
+
|
|
2
4
|
export default {
|
|
3
5
|
inject: ['addTab', 'removeTab', 'sideTabs'],
|
|
4
6
|
|
|
@@ -43,6 +45,20 @@ export default {
|
|
|
43
45
|
required: false,
|
|
44
46
|
type: Number
|
|
45
47
|
},
|
|
48
|
+
/**
|
|
49
|
+
* False to hide the count from being displayed in a tab.
|
|
50
|
+
* Number override/display the number as the count on the tab.
|
|
51
|
+
*/
|
|
52
|
+
count: {
|
|
53
|
+
default: undefined,
|
|
54
|
+
type: [Number, Boolean]
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
setup(props) {
|
|
59
|
+
const { count, isCountVisible } = useTabCountWatcher();
|
|
60
|
+
|
|
61
|
+
return { inferredCount: count, isInferredCountVisible: isCountVisible };
|
|
46
62
|
},
|
|
47
63
|
|
|
48
64
|
data() {
|
|
@@ -50,7 +66,7 @@ export default {
|
|
|
50
66
|
},
|
|
51
67
|
|
|
52
68
|
computed: {
|
|
53
|
-
|
|
69
|
+
baseLabelDisplay() {
|
|
54
70
|
if ( this.labelKey ) {
|
|
55
71
|
return this.$store.getters['i18n/t'](this.labelKey);
|
|
56
72
|
}
|
|
@@ -62,12 +78,38 @@ export default {
|
|
|
62
78
|
return this.name;
|
|
63
79
|
},
|
|
64
80
|
|
|
81
|
+
labelDisplay() {
|
|
82
|
+
const baseLabel = this.baseLabelDisplay;
|
|
83
|
+
|
|
84
|
+
if ( this.displayCount === false ) {
|
|
85
|
+
return baseLabel;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return `${ baseLabel } (${ this.displayCount })`;
|
|
89
|
+
},
|
|
90
|
+
|
|
65
91
|
shouldShowHeader() {
|
|
66
92
|
if ( this.showHeader !== null ) {
|
|
67
93
|
return this.showHeader;
|
|
68
94
|
}
|
|
69
95
|
|
|
70
96
|
return this.sideTabs || false;
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
displayCount() {
|
|
100
|
+
if (this.count === false) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (typeof this.count === 'number') {
|
|
105
|
+
return this.count;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (this.isInferredCountVisible) {
|
|
109
|
+
return this.inferredCount;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return false;
|
|
71
113
|
}
|
|
72
114
|
},
|
|
73
115
|
|
|
@@ -71,6 +71,14 @@ export default {
|
|
|
71
71
|
showExtensionTabs: {
|
|
72
72
|
type: Boolean,
|
|
73
73
|
default: true,
|
|
74
|
+
},
|
|
75
|
+
/**
|
|
76
|
+
* Inherited global identifier prefix for tests
|
|
77
|
+
* Define a term based on the parent component to avoid conflicts on multiple components
|
|
78
|
+
*/
|
|
79
|
+
componentTestid: {
|
|
80
|
+
type: String,
|
|
81
|
+
default: 'tabbed'
|
|
74
82
|
}
|
|
75
83
|
},
|
|
76
84
|
|
|
@@ -253,8 +261,12 @@ export default {
|
|
|
253
261
|
|
|
254
262
|
<template>
|
|
255
263
|
<div
|
|
256
|
-
|
|
257
|
-
|
|
264
|
+
class="tabbed-container"
|
|
265
|
+
:class="{
|
|
266
|
+
'side-tabs': !!sideTabs,
|
|
267
|
+
'tabs-only': tabsOnly
|
|
268
|
+
}"
|
|
269
|
+
:data-testid="componentTestid"
|
|
258
270
|
>
|
|
259
271
|
<ul
|
|
260
272
|
v-if="!hideTabs"
|
|
@@ -262,7 +274,7 @@ export default {
|
|
|
262
274
|
role="tablist"
|
|
263
275
|
class="tabs"
|
|
264
276
|
:class="{'clearfix':!sideTabs, 'vertical': sideTabs, 'horizontal': !sideTabs}"
|
|
265
|
-
data-testid="
|
|
277
|
+
:data-testid="`${componentTestid}-block`"
|
|
266
278
|
tabindex="0"
|
|
267
279
|
@keydown.right.prevent="selectNext(1)"
|
|
268
280
|
@keydown.left.prevent="selectNext(-1)"
|
|
@@ -287,7 +299,9 @@ export default {
|
|
|
287
299
|
@click.prevent="select(tab.name, $event)"
|
|
288
300
|
@keyup.enter.space="select(tab.name, $event)"
|
|
289
301
|
>
|
|
290
|
-
<span>
|
|
302
|
+
<span>
|
|
303
|
+
{{ tab.labelDisplay }}
|
|
304
|
+
</span>
|
|
291
305
|
<span
|
|
292
306
|
v-if="tab.badge"
|
|
293
307
|
class="tab-badge"
|
|
@@ -369,6 +383,10 @@ export default {
|
|
|
369
383
|
</template>
|
|
370
384
|
|
|
371
385
|
<style lang="scss" scoped>
|
|
386
|
+
.tabbed-container {
|
|
387
|
+
min-width: fit-content;
|
|
388
|
+
}
|
|
389
|
+
|
|
372
390
|
.tabs {
|
|
373
391
|
list-style-type: none;
|
|
374
392
|
margin: 0;
|
|
@@ -541,6 +559,7 @@ export default {
|
|
|
541
559
|
list-style: none;
|
|
542
560
|
padding: 0;
|
|
543
561
|
margin-top: auto;
|
|
562
|
+
z-index: z-index('default');
|
|
544
563
|
|
|
545
564
|
li {
|
|
546
565
|
display: flex;
|
|
@@ -550,16 +569,25 @@ export default {
|
|
|
550
569
|
flex: 1 1;
|
|
551
570
|
display: flex;
|
|
552
571
|
justify-content: center;
|
|
572
|
+
|
|
573
|
+
&:focus-visible {
|
|
574
|
+
@include focus-outline;
|
|
575
|
+
}
|
|
553
576
|
}
|
|
554
577
|
|
|
555
578
|
button:first-of-type {
|
|
556
579
|
border-top: solid 1px var(--border);
|
|
557
580
|
border-right: solid 1px var(--border);
|
|
581
|
+
border-top-left-radius: 0;
|
|
558
582
|
border-top-right-radius: 0;
|
|
583
|
+
border-bottom-right-radius: 0;
|
|
559
584
|
}
|
|
560
585
|
button:last-of-type {
|
|
561
586
|
border-top: solid 1px var(--border);
|
|
587
|
+
border-top-right-radius: 0;
|
|
562
588
|
border-top-left-radius: 0;
|
|
589
|
+
border-bottom-left-radius: 0;
|
|
590
|
+
border-bottom-right-radius: 0;
|
|
563
591
|
}
|
|
564
592
|
}
|
|
565
593
|
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/* eslint-disable jest/no-hooks */
|
|
2
|
+
import { mount, VueWrapper } from '@vue/test-utils';
|
|
3
|
+
import { nextTick } from 'vue';
|
|
4
|
+
import { createStore } from 'vuex';
|
|
5
|
+
import CronExpressionEditor from '@shell/components/Cron/CronExpressionEditor.vue';
|
|
6
|
+
import type { CronField } from '@shell/components/Cron/types';
|
|
7
|
+
|
|
8
|
+
const translations: Record<string, string> = {
|
|
9
|
+
'component.cron.expressionEditor.label.minute': 'Minute',
|
|
10
|
+
'component.cron.expressionEditor.label.hour': 'Hour',
|
|
11
|
+
'component.cron.expressionEditor.label.dayOfMonth': 'Day of Month',
|
|
12
|
+
'component.cron.expressionEditor.label.month': 'Month',
|
|
13
|
+
'component.cron.expressionEditor.label.dayOfWeek': 'Day of Week',
|
|
14
|
+
'component.cron.expressionEditor.invalidValue': 'Invalid value',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const store = createStore({});
|
|
18
|
+
|
|
19
|
+
interface CronExpressionEditorVm extends InstanceType<typeof CronExpressionEditor> {
|
|
20
|
+
cronValues: Record<CronField, string>;
|
|
21
|
+
handleInput: (field: CronField, value: string) => void;
|
|
22
|
+
isValid: boolean;
|
|
23
|
+
readableCron: string;
|
|
24
|
+
errors: Record<CronField, boolean>;
|
|
25
|
+
focusedField: Record<CronField, boolean>;
|
|
26
|
+
tooltipRefs: Record<CronField, unknown>;
|
|
27
|
+
popperInstances: Record<CronField, unknown>;
|
|
28
|
+
handleFocus: (field: CronField) => void;
|
|
29
|
+
handleBlur: (field: CronField) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('cronExpressionEditor', () => {
|
|
33
|
+
let wrapper: VueWrapper<CronExpressionEditorVm>;
|
|
34
|
+
|
|
35
|
+
const factory = (props: Partial<CronExpressionEditorVm> = {}) => mount(CronExpressionEditor, {
|
|
36
|
+
global: {
|
|
37
|
+
plugins: [store],
|
|
38
|
+
stubs: {
|
|
39
|
+
CronTooltip: true,
|
|
40
|
+
LabeledInput: {
|
|
41
|
+
name: 'LabeledInput',
|
|
42
|
+
props: ['label', 'tooltip', 'type', 'value'],
|
|
43
|
+
template: `
|
|
44
|
+
<div>
|
|
45
|
+
<label>{{ label }}</label>
|
|
46
|
+
<input ref="value" :value="value" />
|
|
47
|
+
</div>
|
|
48
|
+
`
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
mocks: { t: (key: string) => translations[key] || key },
|
|
52
|
+
},
|
|
53
|
+
props: { cronExpression: '0 0 * * *', ...props },
|
|
54
|
+
}) as VueWrapper<CronExpressionEditorVm>;
|
|
55
|
+
|
|
56
|
+
afterEach(() => wrapper?.unmount());
|
|
57
|
+
|
|
58
|
+
const getEmitted = (event: string) => wrapper.emitted(event) as unknown[][] || [];
|
|
59
|
+
|
|
60
|
+
it('renders 5 input fields with correct labels', () => {
|
|
61
|
+
wrapper = factory();
|
|
62
|
+
const labels = wrapper.findAll('label').map((l) => l.text());
|
|
63
|
+
|
|
64
|
+
expect(labels).toStrictEqual(['Minute', 'Hour', 'Day of Month', 'Month', 'Day of Week']);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('initializes cron values and emits initial events', () => {
|
|
68
|
+
wrapper = factory();
|
|
69
|
+
const vm = wrapper.vm;
|
|
70
|
+
|
|
71
|
+
expect(vm.cronValues).toStrictEqual({
|
|
72
|
+
minute: '0', hour: '0', dayOfMonth: '*', month: '*', dayOfWeek: '*'
|
|
73
|
+
});
|
|
74
|
+
expect(vm.isValid).toBe(true);
|
|
75
|
+
expect(vm.readableCron).toContain('12:00');
|
|
76
|
+
|
|
77
|
+
expect(getEmitted('update:cronExpression')[0][0]).toBe('0 0 * * *');
|
|
78
|
+
expect(typeof getEmitted('update:readableCron')[0][0]).toBe('string');
|
|
79
|
+
expect(getEmitted('update:isValid')[0][0]).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('emits correct events when cron value changes', async() => {
|
|
83
|
+
wrapper = factory();
|
|
84
|
+
const vm = wrapper.vm;
|
|
85
|
+
|
|
86
|
+
vm.handleInput('minute', '5');
|
|
87
|
+
await nextTick();
|
|
88
|
+
|
|
89
|
+
expect(getEmitted('update:cronExpression')[1][0]).toBe('5 0 * * *');
|
|
90
|
+
expect(getEmitted('update:readableCron')[1][0]).toBe(vm.readableCron);
|
|
91
|
+
expect(getEmitted('update:isValid')[1][0]).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('validates individual fields correctly', () => {
|
|
95
|
+
wrapper = factory();
|
|
96
|
+
const vm = wrapper.vm;
|
|
97
|
+
|
|
98
|
+
vm.handleInput('minute', '0');
|
|
99
|
+
expect(vm.errors.minute).toBe(false);
|
|
100
|
+
|
|
101
|
+
vm.handleInput('minute', '61');
|
|
102
|
+
expect(vm.errors.minute).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('handles invalid cron expressions gracefully', async() => {
|
|
106
|
+
wrapper = factory({ cronExpression: '61 * * * *' });
|
|
107
|
+
const vm = wrapper.vm;
|
|
108
|
+
|
|
109
|
+
await nextTick();
|
|
110
|
+
expect(vm.isValid).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('updates readableCron correctly for valid and invalid inputs', async() => {
|
|
114
|
+
wrapper = factory({ cronExpression: '0 12 * * *' });
|
|
115
|
+
const vm = wrapper.vm;
|
|
116
|
+
|
|
117
|
+
await nextTick();
|
|
118
|
+
expect(vm.readableCron).toContain('12:00');
|
|
119
|
+
|
|
120
|
+
vm.handleInput('hour', '25');
|
|
121
|
+
await nextTick();
|
|
122
|
+
expect(vm.isValid).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('shows tooltip and manages popper on focus/blur', async() => {
|
|
126
|
+
wrapper = factory();
|
|
127
|
+
const vm = wrapper.vm;
|
|
128
|
+
|
|
129
|
+
await vm.handleFocus('minute');
|
|
130
|
+
expect(vm.focusedField.minute).toBe(true);
|
|
131
|
+
expect(vm.tooltipRefs.minute).not.toBeNull();
|
|
132
|
+
expect(vm.popperInstances.minute).not.toBeNull();
|
|
133
|
+
|
|
134
|
+
vm.handleBlur('minute');
|
|
135
|
+
await nextTick();
|
|
136
|
+
expect(vm.focusedField.minute).toBe(false);
|
|
137
|
+
expect(vm.popperInstances.minute).toBeNull();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('displays error tooltip for invalid input', async() => {
|
|
141
|
+
wrapper = factory();
|
|
142
|
+
const vm = wrapper.vm;
|
|
143
|
+
|
|
144
|
+
vm.handleInput('minute', '61');
|
|
145
|
+
await nextTick();
|
|
146
|
+
|
|
147
|
+
const inputWrapper = wrapper.findComponent({ name: 'LabeledInput' });
|
|
148
|
+
|
|
149
|
+
expect(inputWrapper.props('tooltip')).toBe('Invalid value');
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/* eslint-disable jest/no-hooks */
|
|
2
|
+
import { mount, VueWrapper } from '@vue/test-utils';
|
|
3
|
+
import { createStore } from 'vuex';
|
|
4
|
+
import CronExpressionEditorModal from '@shell/components/Cron/CronExpressionEditorModal.vue';
|
|
5
|
+
|
|
6
|
+
interface CronExpressionEditorModalVm extends InstanceType<typeof CronExpressionEditorModal> {
|
|
7
|
+
localCron: string;
|
|
8
|
+
confirmCron?: () => void;
|
|
9
|
+
closeModal?: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const store = createStore({});
|
|
13
|
+
|
|
14
|
+
describe('cronExpressionEditorModal', () => {
|
|
15
|
+
let modalsDiv: HTMLElement;
|
|
16
|
+
let wrapper: VueWrapper<CronExpressionEditorModalVm>;
|
|
17
|
+
|
|
18
|
+
const factory = (props: Partial<CronExpressionEditorModalVm> = {}) => mount(CronExpressionEditorModal, {
|
|
19
|
+
global: {
|
|
20
|
+
plugins: [store],
|
|
21
|
+
stubs: {
|
|
22
|
+
AppModal: true,
|
|
23
|
+
CronExpressionEditor: true,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
props: {
|
|
27
|
+
cronExpression: '0 0 * * *',
|
|
28
|
+
show: true,
|
|
29
|
+
...props,
|
|
30
|
+
},
|
|
31
|
+
}) as VueWrapper<CronExpressionEditorModalVm>;
|
|
32
|
+
|
|
33
|
+
const getEmitted = (event: string) => wrapper.emitted(event) as unknown[][] || [];
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
modalsDiv = document.createElement('div');
|
|
37
|
+
modalsDiv.id = 'modals';
|
|
38
|
+
document.body.appendChild(modalsDiv);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
wrapper?.unmount();
|
|
43
|
+
modalsDiv.remove();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('renders modal with correct initial props', () => {
|
|
47
|
+
wrapper = factory();
|
|
48
|
+
expect(wrapper.props('cronExpression')).toBe('0 0 * * *');
|
|
49
|
+
expect(wrapper.props('show')).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('updates localCron when cronExpression prop changes', async() => {
|
|
53
|
+
wrapper = factory();
|
|
54
|
+
await wrapper.setProps({ cronExpression: '*/5 * * * *' });
|
|
55
|
+
expect(wrapper.vm.localCron).toBe('*/5 * * * *');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('emits update:cronExpression and update:show on confirm', async() => {
|
|
59
|
+
wrapper = factory();
|
|
60
|
+
await wrapper.vm.confirmCron?.();
|
|
61
|
+
|
|
62
|
+
const cronEmits = getEmitted('update:cronExpression');
|
|
63
|
+
const showEmits = getEmitted('update:show');
|
|
64
|
+
|
|
65
|
+
expect(cronEmits).toHaveLength(1);
|
|
66
|
+
expect(cronEmits[0][0]).toBe('0 0 * * *');
|
|
67
|
+
|
|
68
|
+
expect(showEmits).toHaveLength(1);
|
|
69
|
+
expect(showEmits[0][0]).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('emits update:show on cancel', async() => {
|
|
73
|
+
wrapper = factory();
|
|
74
|
+
await wrapper.vm.closeModal?.();
|
|
75
|
+
|
|
76
|
+
const showEmits = getEmitted('update:show');
|
|
77
|
+
|
|
78
|
+
expect(showEmits).toHaveLength(1);
|
|
79
|
+
expect(showEmits[0][0]).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils';
|
|
2
|
+
import LazyImage from '@shell/components/LazyImage.vue';
|
|
3
|
+
|
|
4
|
+
describe('component: LazyImage.vue', () => {
|
|
5
|
+
const initialSrc = 'initial.jpg';
|
|
6
|
+
const src = 'test.jpg';
|
|
7
|
+
const errorSrc = 'error.jpg';
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
// Clear all mocks before each test to ensure test isolation
|
|
11
|
+
jest.clearAllMocks();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('renders the initial source image', () => {
|
|
15
|
+
const wrapper = mount(LazyImage, {
|
|
16
|
+
propsData: {
|
|
17
|
+
initialSrc,
|
|
18
|
+
src,
|
|
19
|
+
errorSrc
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const img = wrapper.find('img');
|
|
24
|
+
|
|
25
|
+
expect(img.attributes('src')).toBe(initialSrc);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('does not load the main src image if not in viewport', async() => {
|
|
29
|
+
const wrapper = mount(LazyImage, {
|
|
30
|
+
propsData: {
|
|
31
|
+
initialSrc,
|
|
32
|
+
src,
|
|
33
|
+
errorSrc
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const callback = window.IntersectionObserver.mock.calls[0][0];
|
|
38
|
+
|
|
39
|
+
// eslint-disable-next-line node/no-callback-literal
|
|
40
|
+
callback([{ isIntersecting: false }]);
|
|
41
|
+
await wrapper.vm.$nextTick();
|
|
42
|
+
|
|
43
|
+
const img = wrapper.find('img');
|
|
44
|
+
|
|
45
|
+
expect(img.attributes('src')).toBe(initialSrc);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('loads the main src image when it enters the viewport', async() => {
|
|
49
|
+
const wrapper = mount(LazyImage, {
|
|
50
|
+
propsData: {
|
|
51
|
+
initialSrc,
|
|
52
|
+
src,
|
|
53
|
+
errorSrc
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Manually trigger the intersection observer
|
|
58
|
+
const callback = window.IntersectionObserver.mock.calls[0][0];
|
|
59
|
+
|
|
60
|
+
// eslint-disable-next-line node/no-callback-literal
|
|
61
|
+
callback([{ isIntersecting: true }]);
|
|
62
|
+
|
|
63
|
+
await wrapper.vm.$nextTick();
|
|
64
|
+
|
|
65
|
+
const img = wrapper.find('img');
|
|
66
|
+
|
|
67
|
+
expect(img.attributes('src')).toBe(src);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('loads the error image if the main src image fails to load', async() => {
|
|
71
|
+
const wrapper = mount(LazyImage, {
|
|
72
|
+
propsData: {
|
|
73
|
+
initialSrc,
|
|
74
|
+
src,
|
|
75
|
+
errorSrc
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Manually trigger the intersection observer
|
|
80
|
+
const callback = window.IntersectionObserver.mock.calls[0][0];
|
|
81
|
+
|
|
82
|
+
// eslint-disable-next-line node/no-callback-literal
|
|
83
|
+
callback([{ isIntersecting: true }]);
|
|
84
|
+
|
|
85
|
+
await wrapper.vm.$nextTick();
|
|
86
|
+
|
|
87
|
+
const img = wrapper.find('img');
|
|
88
|
+
|
|
89
|
+
img.trigger('error');
|
|
90
|
+
|
|
91
|
+
await wrapper.vm.$nextTick();
|
|
92
|
+
|
|
93
|
+
expect(img.attributes('src')).toBe(errorSrc);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('loads a new src image if the src prop changes', async() => {
|
|
97
|
+
const wrapper = mount(LazyImage, {
|
|
98
|
+
propsData: {
|
|
99
|
+
initialSrc,
|
|
100
|
+
src,
|
|
101
|
+
errorSrc
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Manually trigger the intersection observer
|
|
106
|
+
const callback = window.IntersectionObserver.mock.calls[0][0];
|
|
107
|
+
|
|
108
|
+
// eslint-disable-next-line node/no-callback-literal
|
|
109
|
+
callback([{ isIntersecting: true }]);
|
|
110
|
+
|
|
111
|
+
await wrapper.vm.$nextTick();
|
|
112
|
+
|
|
113
|
+
const newSrc = 'new.jpg';
|
|
114
|
+
|
|
115
|
+
await wrapper.setProps({ src: newSrc });
|
|
116
|
+
|
|
117
|
+
const img = wrapper.find('img');
|
|
118
|
+
|
|
119
|
+
expect(img.attributes('src')).toBe(newSrc);
|
|
120
|
+
});
|
|
121
|
+
});
|