@rancher/shell 0.3.21 → 0.3.23

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 (60) hide show
  1. package/assets/translations/en-us.yaml +4 -0
  2. package/assets/translations/zh-hans.yaml +8 -1
  3. package/babel.config.js +3 -0
  4. package/cloud-credential/__tests__/azure.test.ts +53 -0
  5. package/cloud-credential/azure.vue +6 -0
  6. package/components/GrowlManager.vue +33 -30
  7. package/components/SortableTable/paging.js +10 -0
  8. package/components/form/GitPicker.vue +16 -0
  9. package/components/form/ResourceQuota/ProjectRow.vue +38 -15
  10. package/components/form/SelectOrCreateAuthSecret.vue +9 -3
  11. package/components/formatter/ClusterProvider.vue +9 -3
  12. package/components/formatter/__tests__/ClusterProvider.test.ts +5 -1
  13. package/components/nav/Header.vue +1 -0
  14. package/config/settings.ts +59 -2
  15. package/config/types.js +2 -0
  16. package/creators/pkg/files/.github/workflows/build-extension-catalog.yml +28 -0
  17. package/creators/pkg/files/.github/workflows/build-extension-charts.yml +26 -0
  18. package/creators/pkg/init +63 -4
  19. package/detail/provisioning.cattle.io.cluster.vue +4 -2
  20. package/edit/fleet.cattle.io.gitrepo.vue +1 -0
  21. package/edit/provisioning.cattle.io.cluster/rke2.vue +4 -4
  22. package/edit/resources.cattle.io.backup.vue +3 -1
  23. package/edit/resources.cattle.io.restore.vue +3 -1
  24. package/mixins/__tests__/chart.test.ts +40 -0
  25. package/mixins/chart.js +5 -0
  26. package/models/catalog.cattle.io.clusterrepo.js +6 -2
  27. package/models/fleet.cattle.io.cluster.js +10 -2
  28. package/package.json +1 -1
  29. package/pages/c/_cluster/gatekeeper/index.vue +10 -1
  30. package/plugins/steve/__tests__/header-warnings.spec.ts +238 -0
  31. package/plugins/steve/actions.js +4 -23
  32. package/plugins/steve/header-warnings.ts +91 -0
  33. package/promptRemove/management.cattle.io.project.vue +9 -6
  34. package/rancher-components/BadgeState/BadgeState.vue +1 -5
  35. package/rancher-components/Banner/Banner.test.ts +1 -51
  36. package/rancher-components/Banner/Banner.vue +53 -134
  37. package/rancher-components/Card/Card.vue +7 -24
  38. package/rancher-components/Form/Checkbox/Checkbox.test.ts +29 -20
  39. package/rancher-components/Form/Checkbox/Checkbox.vue +20 -45
  40. package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +8 -2
  41. package/rancher-components/Form/LabeledInput/LabeledInput.vue +10 -22
  42. package/rancher-components/Form/Radio/RadioButton.vue +13 -30
  43. package/rancher-components/Form/Radio/RadioGroup.vue +7 -26
  44. package/rancher-components/Form/TextArea/TextAreaAutoGrow.vue +6 -7
  45. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.test.ts +38 -25
  46. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.vue +11 -23
  47. package/rancher-components/LabeledTooltip/LabeledTooltip.vue +5 -19
  48. package/rancher-components/StringList/StringList.test.ts +49 -453
  49. package/rancher-components/StringList/StringList.vue +58 -92
  50. package/scripts/extension/parse-tag-name +30 -0
  51. package/types/shell/index.d.ts +16 -9
  52. package/utils/__tests__/formatter.test.ts +77 -0
  53. package/utils/formatter.js +11 -0
  54. package/utils/settings.ts +2 -17
  55. package/vue-config-helper.js +135 -0
  56. package/vue.config.js +29 -141
  57. package/creators/pkg/files/.github/workflows/build-container.yml +0 -64
  58. package/creators/pkg/files/.github/workflows/build-extension.yml +0 -110
  59. package/rancher-components/Card/Card.test.ts +0 -37
  60. package/rancher-components/Form/Radio/RadioButton.test.ts +0 -31
@@ -0,0 +1,91 @@
1
+ import { PerfSettingsWarningHeaders } from '@shell/config/settings';
2
+ import { getPerformanceSetting } from '@shell/utils/settings';
3
+
4
+ interface HttpResponse {
5
+ headers?: { [key: string]: string},
6
+ data?: any,
7
+ config: {
8
+ url: string,
9
+ }
10
+ }
11
+
12
+ /**
13
+ * Cache the kube api warning header settings that will determine if they are growled or not
14
+ */
15
+ let warningHeaderSettings: PerfSettingsWarningHeaders;
16
+
17
+ /**
18
+ * Extract sanitised warnings from the warnings header string
19
+ */
20
+ function kubeApiHeaderWarnings(allWarnings: string): string[] {
21
+ // Find each warning.
22
+ // Each warning is separated by `,`... however... this can appear within the warning itself so can't `split` on it
23
+ // Instead provide a configurable way to split (default 299 - )
24
+ const warnings = allWarnings.split(warningHeaderSettings.separator) || [];
25
+
26
+ // Trim and remove effects of split
27
+ return warnings.reduce((res, warning) => {
28
+ const trimmedWarning = warning.trim();
29
+
30
+ if (!trimmedWarning) {
31
+ return res;
32
+ }
33
+
34
+ const fixedWarning = trimmedWarning.endsWith(',') ? trimmedWarning.slice(0, -1) : trimmedWarning;
35
+
36
+ // Why add the separator again? It's almost certainly `299 - ` which is important info to include
37
+ res.push(warningHeaderSettings.separator + fixedWarning);
38
+
39
+ return res;
40
+ }, [] as string[]);
41
+ }
42
+
43
+ /**
44
+ * Take action given the `warnings` in the response header of a kube api request
45
+ */
46
+ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
47
+ export function handleKubeApiHeaderWarnings(res: HttpResponse, dispatch: any, rootGetters: any, method: string, refreshCache = false): void {
48
+ const safeMethod = method?.toLowerCase(); // Some requests have this as uppercase
49
+
50
+ // Exit early if there's no warnings
51
+ if ((safeMethod !== 'post' && safeMethod !== 'put') || !res.headers?.warning) {
52
+ return;
53
+ }
54
+
55
+ // Grab the required settings
56
+ if (!warningHeaderSettings || refreshCache) {
57
+ const settings = getPerformanceSetting(rootGetters);
58
+
59
+ // Cache this, we don't need to react to changes within the same session
60
+ warningHeaderSettings = settings?.kubeAPI.warningHeader;
61
+ }
62
+
63
+ // Determine each warning
64
+ const sanitisedWarnings = kubeApiHeaderWarnings(res.headers?.warning);
65
+
66
+ if (!sanitisedWarnings.length) {
67
+ return;
68
+ }
69
+
70
+ // Shows warnings as growls
71
+ const growlWarnings = sanitisedWarnings.filter((w) => !warningHeaderSettings.notificationBlockList.find((blocked) => w.startsWith(blocked)));
72
+
73
+ if (growlWarnings.length) {
74
+ const resourceType = res.data?.type || res.data?.kind || rootGetters['i18n/t']('generic.resource', { count: 1 });
75
+
76
+ dispatch('growl/warning', {
77
+ title: method === 'put' ? rootGetters['i18n/t']('growl.kubeApiHeaderWarning.titleUpdate', { resourceType }) : rootGetters['i18n/t']('growl.kubeApiHeaderWarning.titleCreate', { resourceType }),
78
+ message: growlWarnings.join(', '),
79
+ timeout: 0,
80
+ }, { root: true });
81
+ }
82
+
83
+ // Print warnings to console
84
+ const message = `Validation Warnings for ${ res.config.url }\n\n${ sanitisedWarnings.join('\n') }`;
85
+
86
+ if (process.env.dev) {
87
+ console.warn(`${ message }\n\n`, res.data); // eslint-disable-line no-console
88
+ } else {
89
+ console.debug(message); // eslint-disable-line no-console
90
+ }
91
+ }
@@ -71,9 +71,12 @@ export default {
71
71
  names() {
72
72
  return this.filteredNamespaces.map((obj) => obj.nameDisplay).slice(0, 5);
73
73
  },
74
- // Only admins and cluster owners can see namespaces outside of projects
75
- canSeeProjectlessNamespaces() {
76
- return this.currentCluster.canUpdate;
74
+
75
+ canManageNamespaces() {
76
+ // Only admins and cluster owners can see namespaces outside of projects
77
+ // BUT cluster members can also manage projects and namespaces and may want to not delete the namespaces associated with the project
78
+ // as per https://github.com/rancher/dashboard/issues/9517 despite the namespaces cannot be seen afterwards (projectless)
79
+ return this.currentCluster.canUpdate || (this.currentProject.canDelete && this.filteredNamespaces.length && this.filteredNamespaces[0]?.canDelete);
77
80
  }
78
81
  },
79
82
  methods: {
@@ -81,7 +84,7 @@ export default {
81
84
  remove() {
82
85
  // Delete all of thre namespaces and return false - this tells the prompt remove dialog to continue and delete the project
83
86
  // Delete all namespaces if the user wouldn't be able to see them after deleting the project
84
- if (this.deleteProjectNamespaces || !this.canSeeProjectlessNamespaces) {
87
+ if (this.deleteProjectNamespaces || !this.canManageNamespaces) {
85
88
  return Promise.all(this.filteredNamespaces.map((n) => n.remove())).then(() => false);
86
89
  }
87
90
 
@@ -97,7 +100,7 @@ export default {
97
100
  <div>
98
101
  <div class="mb-10">
99
102
  {{ t('promptRemove.attemptingToRemove', { type }) }} <span class="display-name">{{ `${displayName}.` }}</span>
100
- <template v-if="!canSeeProjectlessNamespaces">
103
+ <template v-if="!canManageNamespaces">
101
104
  <span class="delete-warning"> {{ t('promptRemove.willDeleteAssociatedNamespaces') }}</span> <br>
102
105
  <div
103
106
  v-clean-html="resourceNames(names, plusMore, t)"
@@ -106,7 +109,7 @@ export default {
106
109
  </template>
107
110
  </div>
108
111
  <div
109
- v-if="filteredNamespaces.length > 0 && canSeeProjectlessNamespaces"
112
+ v-if="filteredNamespaces.length > 0 && canManageNamespaces"
110
113
  class="mt-20 remove-project-dialog"
111
114
  >
112
115
  <Checkbox
@@ -60,11 +60,7 @@ export default Vue.extend({
60
60
 
61
61
  <template>
62
62
  <span :class="{'badge-state': true, [bg]: true}">
63
- <i
64
- v-if="icon"
65
- class="icon"
66
- :class="{[icon]: true, 'mr-5': !!msg}"
67
- />{{ msg }}
63
+ <i v-if="icon" class="icon" :class="{[icon]: true, 'mr-5': !!msg}" />{{ msg }}
68
64
  </span>
69
65
  </template>
70
66
 
@@ -1,63 +1,13 @@
1
1
  import { mount } from '@vue/test-utils';
2
2
  import { Banner } from './index';
3
- import { cleanHtmlDirective } from '@shell/plugins/clean-html-directive';
4
3
 
5
4
  describe('component: Banner', () => {
6
5
  it('should display text based on label', () => {
7
6
  const label = 'test';
8
- const wrapper = mount(
9
- Banner,
10
- {
11
- directives: { cleanHtmlDirective },
12
- propsData: { label }
13
- });
7
+ const wrapper = mount(Banner, { propsData: { label } });
14
8
 
15
9
  const element = wrapper.find('span').element;
16
10
 
17
11
  expect(element.textContent).toBe(label);
18
12
  });
19
-
20
- it('should display an icon', () => {
21
- const icon = 'my-icon';
22
- const wrapper = mount(Banner, { propsData: { icon } });
23
-
24
- const element = wrapper.find(`.${ icon }`).element;
25
-
26
- expect(element.classList).toContain(icon);
27
- });
28
-
29
- it('should not display an icon', () => {
30
- const wrapper = mount(Banner);
31
-
32
- const element = wrapper.find(`[data-testid="banner-icon"]`).element;
33
-
34
- expect(element).not.toBeDefined();
35
- });
36
-
37
- it('should emit close event', () => {
38
- const wrapper = mount(Banner, { propsData: { closable: true } });
39
- const element = wrapper.find(`[data-testid="banner-close"]`).element;
40
-
41
- element.click();
42
-
43
- expect(wrapper.emitted('close')).toHaveLength(1);
44
- });
45
-
46
- it('should add the right color', () => {
47
- const color = 'red';
48
- const wrapper = mount(Banner, { propsData: { color } });
49
-
50
- const element = wrapper.element;
51
-
52
- expect(element.classList).toContain(color);
53
- });
54
-
55
- it('should stack the banner messages', () => {
56
- const stacked = true;
57
- const wrapper = mount(Banner, { propsData: { stacked } });
58
-
59
- const element = wrapper.find(`[data-testid="banner-content"]`).element;
60
-
61
- expect(element.classList).toContain('stacked');
62
- });
63
13
  });
@@ -27,13 +27,6 @@ export default Vue.extend({
27
27
  type: String,
28
28
  default: null
29
29
  },
30
- /**
31
- * Add icon for the banner
32
- */
33
- icon: {
34
- type: String,
35
- default: null
36
- },
37
30
  /**
38
31
  * Toggles the banner's close button.
39
32
  */
@@ -65,137 +58,31 @@ export default Vue.extend({
65
58
  class="banner"
66
59
  :class="{
67
60
  [color]: true,
61
+ closable,
62
+ stacked
68
63
  }"
69
64
  >
70
- <div
71
- v-if="icon"
72
- class="banner__icon"
73
- data-testid="banner-icon"
74
- >
75
- <i
76
- class="icon icon-2x"
77
- :class="icon"
78
- />
79
- </div>
80
- <div
81
- class="banner__content"
82
- data-testid="banner-content"
83
- :class="{
84
- closable,
85
- stacked,
86
- icon
87
- }"
88
- >
89
- <slot>
90
- <t
91
- v-if="labelKey"
92
- :k="labelKey"
93
- :raw="true"
94
- />
95
- <span v-else-if="messageLabel">{{ messageLabel }}</span>
96
- <span
97
- v-else
98
- v-clean-html="nlToBr(label)"
99
- />
100
- </slot>
101
- <div
102
- v-if="closable"
103
- class="banner__content__closer"
104
- @click="$emit('close')"
105
- >
106
- <i
107
- data-testid="banner-close"
108
- class="icon icon-close closer-icon"
109
- />
110
- </div>
65
+ <slot>
66
+ <t v-if="labelKey" :k="labelKey" :raw="true" />
67
+ <span v-else-if="messageLabel">{{ messageLabel }}</span>
68
+ <span v-else v-html="nlToBr(label)" />
69
+ </slot>
70
+ <div v-if="closable" class="closer" @click="$emit('close')">
71
+ <i class="icon icon-2x icon-close closer-icon" />
111
72
  </div>
112
73
  </div>
113
74
  </template>
114
75
 
115
76
  <style lang="scss" scoped>
116
- $left-border-size: 4px;
117
- $icon-size: 24px;
118
-
119
- .banner {
120
- display: flex;
121
- margin: 15px 0;
122
- position: relative;
123
- width: 100%;
124
- color: var(--body-text);
125
-
126
- &__icon {
127
- width: $icon-size * 2;
128
- flex-grow: 1;
129
- display: flex;
130
- justify-content: center;
131
- align-items: center;
132
- box-sizing: content-box;
133
-
134
- .primary & {
135
- background: var(--primary);
136
- }
137
-
138
- .secondary & {
139
- background: var(--default);
140
- }
141
-
142
- .success & {
143
- background: var(--success);
144
- }
77
+ $left-border-size: 4px;
145
78
 
146
- .info & {
147
- background: var(--info);
148
- }
149
-
150
- .warning & {
151
- background: var(--warning);
152
- }
153
-
154
- .error & {
155
- background: var(--error);
156
- color: var(--primary-text);
157
- }
158
- }
159
-
160
- &__content {
79
+ .banner {
161
80
  padding: 10px;
81
+ margin: 15px 0;
82
+ width: 100%;
162
83
  transition: all 0.2s ease;
84
+ position: relative;
163
85
  line-height: 20px;
164
- width: 100%;
165
- border-left: solid $left-border-size transparent;
166
- display: flex;
167
- gap: 3px;
168
-
169
- .primary & {
170
- background: var(--primary);
171
- border-color: var(--primary);
172
- }
173
-
174
- .secondary & {
175
- background: var(--default-banner-bg);
176
- border-color: var(--default);
177
- }
178
-
179
- .success & {
180
- background: var(--success-banner-bg);
181
- border-color: var(--success);
182
- }
183
-
184
- .info & {
185
- background: var(--info-banner-bg);
186
- border-color: var(--info);
187
- }
188
-
189
- .warning & {
190
- background: var(--warning-banner-bg);
191
- border-color: var(--warning);
192
- }
193
-
194
- .error & {
195
- background: var(--error-banner-bg);
196
- border-color: var(--error);
197
- color: var(--error);
198
- }
199
86
 
200
87
  &.stacked {
201
88
  padding: 0 10px;
@@ -210,10 +97,10 @@ $icon-size: 24px;
210
97
  }
211
98
 
212
99
  &.closable {
213
- padding-right: $icon-size * 2;
100
+ padding-right: 40px;
214
101
  }
215
102
 
216
- &__closer {
103
+ .closer {
217
104
  display: flex;
218
105
  align-items: center;
219
106
 
@@ -222,11 +109,12 @@ $icon-size: 24px;
222
109
  top: 0;
223
110
  right: 0;
224
111
  bottom: 0;
225
- width: $icon-size;
226
- line-height: $icon-size;
112
+ width: 40px;
113
+ line-height: 42px;
227
114
  text-align: center;
228
115
 
229
116
  .closer-icon {
117
+ font-size: 22px;
230
118
  opacity: 0.7;
231
119
 
232
120
  &:hover {
@@ -236,9 +124,40 @@ $icon-size: 24px;
236
124
  }
237
125
  }
238
126
 
239
- &.icon {
240
- border-left: none;
127
+ &.primary {
128
+ background: var(--primary);
129
+ border-left: solid $left-border-size var(--primary);
130
+ color: var(--body-text);
131
+ }
132
+
133
+ &.secondary {
134
+ background: var(--default-banner-bg);
135
+ border-left: solid $left-border-size var(--default);
136
+ color: var(--body-text);
137
+ }
138
+
139
+ &.success {
140
+ background: var(--success-banner-bg);
141
+ border-left: solid $left-border-size var(--success);
142
+ color: var(--body-text);
143
+ }
144
+
145
+ &.info {
146
+ background: var(--info-banner-bg);
147
+ border-left: solid $left-border-size var(--info);
148
+ color: var(--body-text);
149
+ }
150
+
151
+ &.warning {
152
+ background: var(--warning-banner-bg);
153
+ border-left: solid $left-border-size var(--warning);
154
+ color: var(--body-text);
155
+ }
156
+
157
+ &.error {
158
+ background: var(--error-banner-bg);
159
+ border-left: solid $left-border-size var(--error);
160
+ color: var(--error);
241
161
  }
242
162
  }
243
- }
244
163
  </style>
@@ -49,45 +49,28 @@ export default Vue.extend({
49
49
  sticky: {
50
50
  type: Boolean,
51
51
  default: false,
52
- },
52
+ },
53
53
  }
54
54
  });
55
55
  </script>
56
56
 
57
57
  <template>
58
- <div
59
- class="card-container"
60
- :class="{'highlight-border': showHighlightBorder, 'card-sticky': sticky}"
61
- data-testid="card"
62
- >
58
+ <div class="card-container" :class="{'highlight-border': showHighlightBorder, 'card-sticky': sticky}">
63
59
  <div class="card-wrap">
64
- <div
65
- class="card-title"
66
- data-testid="card-title-slot"
67
- >
60
+ <div class="card-title">
68
61
  <slot name="title">
69
62
  {{ title }}
70
63
  </slot>
71
64
  </div>
72
- <hr>
73
- <div
74
- class="card-body"
75
- data-testid="card-body-slot"
76
- >
65
+ <hr />
66
+ <div class="card-body">
77
67
  <slot name="body">
78
68
  {{ content }}
79
69
  </slot>
80
70
  </div>
81
- <div
82
- v-if="showActions"
83
- class="card-actions"
84
- data-testid="card-actions-slot"
85
- >
71
+ <div v-if="showActions" class="card-actions">
86
72
  <slot name="actions">
87
- <button
88
- class="btn role-primary"
89
- @click="buttonAction"
90
- >
73
+ <button class="btn role-primary" @click="buttonAction">
91
74
  {{ buttonText }}
92
75
  </button>
93
76
  </slot>
@@ -1,13 +1,7 @@
1
- import { shallowMount, Wrapper } from '@vue/test-utils';
1
+ import { shallowMount } from '@vue/test-utils';
2
2
  import { Checkbox } from './index';
3
3
 
4
- describe('checkbox.vue', () => {
5
- const event = {
6
- target: { tagName: 'input', href: null },
7
- stopPropagation: () => { },
8
- preventDefault: () => { }
9
- } as unknown as MouseEvent;
10
-
4
+ describe('Checkbox.vue', () => {
11
5
  it('is unchecked by default', () => {
12
6
  const wrapper = shallowMount(Checkbox);
13
7
  const cbInput = wrapper.find('input[type="checkbox"]').element as HTMLInputElement;
@@ -22,7 +16,7 @@ describe('checkbox.vue', () => {
22
16
  expect(cbInput.checked).toBe(true);
23
17
  });
24
18
 
25
- it('updates from false to true when props change', async() => {
19
+ it('updates from false to true when props change', async () => {
26
20
  const wrapper = shallowMount(Checkbox);
27
21
  const cbInput = wrapper.find('input[type="checkbox"]').element as HTMLInputElement;
28
22
 
@@ -33,36 +27,51 @@ describe('checkbox.vue', () => {
33
27
  expect(cbInput.checked).toBe(true);
34
28
  });
35
29
 
36
- it('emits an input event with a true value', async() => {
37
- const wrapper: Wrapper<InstanceType<typeof Checkbox>> = shallowMount(Checkbox);
30
+ it('emits an input event with a true value', async () => {
31
+ const wrapper = shallowMount(Checkbox);
32
+ const event = {
33
+ target: { tagName: 'input', href: null },
34
+ stopPropagation: () => { },
35
+ preventDefault: () => { }
36
+ };
38
37
 
39
- wrapper.vm.clicked(event);
38
+ (wrapper.vm as any).clicked(event);
40
39
  await wrapper.vm.$nextTick();
41
40
 
42
41
  expect(wrapper.emitted().input?.length).toBe(1);
43
42
  expect(wrapper.emitted().input?.[0][0]).toBe(true);
44
43
  });
45
44
 
46
- it('emits an input event with a custom valueWhenTrue', async() => {
45
+ it('emits an input event with a custom valueWhenTrue', async () => {
47
46
  const valueWhenTrue = 'BIG IF TRUE';
47
+ const event = {
48
+ target: { tagName: 'input', href: null },
49
+ stopPropagation: () => { },
50
+ preventDefault: () => { }
51
+ };
48
52
 
49
- const wrapper: Wrapper<InstanceType<typeof Checkbox>> = shallowMount(Checkbox, { propsData: { value: false, valueWhenTrue } });
53
+ const wrapper = shallowMount(Checkbox, { propsData: { value: false, valueWhenTrue } });
50
54
 
51
- wrapper.vm.clicked(event);
55
+ (wrapper.vm as any).clicked(event);
52
56
  await wrapper.vm.$nextTick();
53
57
 
54
58
  expect(wrapper.emitted().input?.length).toBe(1);
55
59
  expect(wrapper.emitted().input?.[0][0]).toBe(valueWhenTrue);
56
60
  });
57
61
 
58
- it('updates from valueWhenTrue to falsy', async() => {
62
+ it('updates from valueWhenTrue to falsy', async () => {
59
63
  const valueWhenTrue = 'REAL HUGE IF FALSE';
64
+ const event = {
65
+ target: { tagName: 'input', href: null },
66
+ stopPropagation: () => { },
67
+ preventDefault: () => { }
68
+ };
60
69
 
61
- const wrapper: Wrapper<InstanceType<typeof Checkbox>> = shallowMount(Checkbox, { propsData: { value: valueWhenTrue, valueWhenTrue } });
70
+ const wrapper = shallowMount(Checkbox, { propsData: { value: valueWhenTrue, valueWhenTrue } });
62
71
 
63
- wrapper.vm.clicked(event);
72
+ (wrapper.vm as any).clicked(event);
64
73
  await wrapper.vm.$nextTick();
65
74
 
66
- expect(wrapper.emitted().input?.[0][0]).toBeNull();
67
- });
75
+ expect(wrapper.emitted().input?.[0][0]).toBe(null);
76
+ })
68
77
  });