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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/assets/brand/suse/metadata.json +2 -1
  2. package/assets/translations/en-us.yaml +105 -5
  3. package/components/ActionMenuShell.vue +1 -1
  4. package/components/Inactivity.vue +2 -2
  5. package/components/Resource/Detail/Card/ExtrasCard.vue +49 -15
  6. package/components/Resource/Detail/Card/__tests__/ExtrasCard.test.ts +111 -0
  7. package/components/Resource/Detail/Masthead/__tests__/index.test.ts +0 -17
  8. package/components/Resource/Detail/Masthead/index.vue +11 -4
  9. package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +3 -1
  10. package/components/Resource/Detail/Metadata/index.vue +1 -1
  11. package/components/Resource/Detail/ResourceRow.vue +1 -1
  12. package/components/ResourceDetail/Masthead/latest.vue +12 -2
  13. package/components/ResourceList/index.vue +9 -0
  14. package/components/ResourceTable.vue +38 -4
  15. package/components/Tabbed/Tab.vue +4 -0
  16. package/components/Tabbed/index.vue +4 -1
  17. package/components/__tests__/ProjectRow.test.ts +60 -0
  18. package/components/form/ChangePassword.vue +41 -35
  19. package/components/form/ResourceQuota/Project.vue +42 -1
  20. package/components/form/ResourceQuota/ProjectRow.vue +71 -4
  21. package/components/form/ResourceQuota/__tests__/Project.test.ts +63 -0
  22. package/components/form/SelectOrCreateAuthSecret.vue +6 -1
  23. package/components/form/__tests__/SelectOrCreateAuthSecret.test.ts +35 -0
  24. package/components/formatter/KubeconfigClusters.vue +74 -0
  25. package/components/formatter/MachineSummaryGraph.vue +10 -2
  26. package/components/formatter/__tests__/KubeconfigClusters.test.ts +125 -0
  27. package/components/nav/TopLevelMenu.helper.ts +50 -2
  28. package/components/nav/TopLevelMenu.vue +14 -0
  29. package/components/nav/Type.vue +5 -0
  30. package/components/nav/__tests__/TopLevelMenu.test.ts +3 -3
  31. package/components/nav/__tests__/Type.test.ts +6 -4
  32. package/config/product/explorer.js +4 -3
  33. package/config/product/manager.js +47 -3
  34. package/config/router/navigation-guards/authentication.js +8 -9
  35. package/config/router/routes.js +4 -1
  36. package/config/types.js +10 -2
  37. package/detail/auditlog.cattle.io.auditpolicy.vue +19 -0
  38. package/detail/management.cattle.io.user.vue +1 -2
  39. package/detail/node.vue +0 -1
  40. package/detail/provisioning.cattle.io.cluster.vue +2 -1
  41. package/dialog/ChangePasswordDialog.vue +8 -0
  42. package/dialog/GenericPrompt.vue +20 -3
  43. package/dialog/ScaleMachineDownDialog.vue +65 -15
  44. package/dialog/SearchDialog.vue +10 -2
  45. package/dialog/__tests__/ScaleMachineDownDialog.test.ts +184 -0
  46. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +89 -0
  47. package/edit/__tests__/management.cattle.io.project.test.js +56 -1
  48. package/edit/auditlog.cattle.io.auditpolicy/AdditionalRedactions.vue +114 -0
  49. package/edit/auditlog.cattle.io.auditpolicy/Filters.vue +119 -0
  50. package/edit/auditlog.cattle.io.auditpolicy/General.vue +180 -0
  51. package/edit/auditlog.cattle.io.auditpolicy/__tests__/AdditionalRedactions.test.ts +327 -0
  52. package/edit/auditlog.cattle.io.auditpolicy/__tests__/Filters.test.ts +449 -0
  53. package/edit/auditlog.cattle.io.auditpolicy/__tests__/General.test.ts +472 -0
  54. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/AdditionalRedactions.test.ts.snap +27 -0
  55. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/Filters.test.ts.snap +39 -0
  56. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/General.test.ts.snap +174 -0
  57. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/index.test.ts.snap +29 -0
  58. package/edit/auditlog.cattle.io.auditpolicy/__tests__/index.test.ts +215 -0
  59. package/edit/auditlog.cattle.io.auditpolicy/index.vue +104 -0
  60. package/edit/auditlog.cattle.io.auditpolicy/types.ts +28 -0
  61. package/edit/fleet.cattle.io.gitrepo.vue +16 -1
  62. package/edit/management.cattle.io.project.vue +8 -2
  63. package/edit/management.cattle.io.user.vue +29 -34
  64. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +178 -0
  65. package/edit/provisioning.cattle.io.cluster/rke2.vue +22 -2
  66. package/edit/provisioning.cattle.io.cluster/shared.ts +4 -0
  67. package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +1 -0
  68. package/edit/provisioning.cattle.io.cluster/tabs/etcd/S3Config.vue +57 -2
  69. package/edit/provisioning.cattle.io.cluster/tabs/etcd/__tests__/S3Config.test.ts +109 -0
  70. package/edit/provisioning.cattle.io.cluster/tabs/etcd/index.vue +1 -0
  71. package/list/auditlog.cattle.io.auditpolicy.vue +63 -0
  72. package/list/ext.cattle.io.kubeconfig.vue +118 -0
  73. package/list/group.principal.vue +11 -15
  74. package/list/management.cattle.io.user.vue +11 -21
  75. package/machine-config/azure.vue +14 -0
  76. package/mixins/__tests__/chart.test.ts +147 -0
  77. package/mixins/browser-tab-visibility.js +5 -4
  78. package/mixins/chart.js +10 -8
  79. package/mixins/fetch.client.js +6 -0
  80. package/models/__tests__/auditlog.cattle.io.auditpolicy.test.ts +117 -0
  81. package/models/__tests__/ext.cattle.io.kubeconfig.test.ts +364 -0
  82. package/models/__tests__/secret.test.ts +55 -0
  83. package/models/__tests__/workload.test.ts +49 -6
  84. package/models/auditlog.cattle.io.auditpolicy.js +46 -0
  85. package/models/cluster.x-k8s.io.machine.js +1 -1
  86. package/models/cluster.x-k8s.io.machinedeployment.js +5 -5
  87. package/models/event.js +5 -0
  88. package/models/ext.cattle.io.groupmembershiprefreshrequest.js +15 -0
  89. package/models/ext.cattle.io.kubeconfig.ts +97 -0
  90. package/models/ext.cattle.io.passwordchangerequest.js +15 -0
  91. package/models/ext.cattle.io.selfuser.js +15 -0
  92. package/models/fleet-application.js +17 -7
  93. package/models/management.cattle.io.user.js +28 -31
  94. package/models/schema.js +18 -0
  95. package/models/secret.js +28 -25
  96. package/models/steve-schema.ts +39 -2
  97. package/models/workload.js +3 -2
  98. package/package.json +2 -2
  99. package/pages/about.vue +3 -2
  100. package/pages/account/index.vue +23 -16
  101. package/pages/auth/login.vue +15 -8
  102. package/pages/auth/setup.vue +52 -15
  103. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +38 -14
  104. package/pages/c/_cluster/apps/charts/index.vue +1 -0
  105. package/pages/home.vue +9 -3
  106. package/plugins/dashboard-store/__tests__/resource-class.test.ts +1 -3
  107. package/plugins/dashboard-store/actions.js +7 -0
  108. package/plugins/dashboard-store/getters.js +23 -1
  109. package/plugins/dashboard-store/index.js +3 -2
  110. package/plugins/dashboard-store/mutations.js +4 -0
  111. package/plugins/dashboard-store/resource-class.js +12 -5
  112. package/plugins/steve/__tests__/steve-class.test.ts +167 -0
  113. package/plugins/steve/schema.d.ts +5 -0
  114. package/plugins/steve/steve-class.js +19 -0
  115. package/plugins/steve/steve-pagination-utils.ts +2 -1
  116. package/rancher-components/RcItemCard/RcItemCard.test.ts +4 -2
  117. package/rancher-components/RcItemCard/RcItemCard.vue +27 -10
  118. package/store/auth.js +57 -19
  119. package/store/notifications.ts +1 -1
  120. package/store/type-map.js +12 -1
  121. package/types/shell/index.d.ts +24 -15
  122. package/types/store/dashboard-store.types.ts +7 -0
  123. package/utils/__tests__/chart.test.ts +96 -0
  124. package/utils/__tests__/version.test.ts +1 -19
  125. package/utils/chart.js +64 -0
  126. package/utils/pagination-wrapper.ts +11 -3
  127. package/utils/version.js +5 -17
  128. package/vue.config.js +26 -13
@@ -0,0 +1,119 @@
1
+ <script setup lang="ts">
2
+ import { ref } from 'vue';
3
+ import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
4
+ import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
5
+ import ArrayList from '@shell/components/form/ArrayList.vue';
6
+ import { AuditPolicy, FilterRule } from '@shell/edit/auditlog.cattle.io.auditpolicy/types';
7
+
8
+ // Component Props & Emits
9
+ const props = defineProps({
10
+ value: {
11
+ type: Object,
12
+ default: () => ({})
13
+ },
14
+ mode: {
15
+ type: String,
16
+ default: 'create'
17
+ },
18
+ });
19
+
20
+ const emit = defineEmits<{
21
+ 'update:value': [value: AuditPolicy];
22
+ }>();
23
+
24
+ // Default Values & Reactive Spec
25
+ const defaults: AuditPolicy = { filters: [] };
26
+ const spec = ref<AuditPolicy>({ ...defaults, ...props.value });
27
+ const defaultAddValue: FilterRule = {
28
+ action: 'allow',
29
+ requestURI: '',
30
+ };
31
+
32
+ // Methods
33
+ function addRow(key: 'action' | 'requestURI', filters: FilterRule[]) {
34
+ const valueToEmit = { ...props.value, ...spec.value };
35
+
36
+ valueToEmit.filters = filters;
37
+
38
+ emit('update:value', valueToEmit);
39
+ }
40
+
41
+ function updateRow(key: 'action' | 'requestURI', index: number, value: string) {
42
+ const valueToEmit = { ...props.value, ...spec.value };
43
+
44
+ if (!valueToEmit.filters) {
45
+ valueToEmit.filters = [];
46
+ }
47
+
48
+ // Ensure the filter exists at the given index
49
+ if (!valueToEmit.filters[index]) {
50
+ valueToEmit.filters[index] = { action: '', requestURI: '' };
51
+ }
52
+
53
+ valueToEmit.filters[index][key] = value;
54
+ emit('update:value', valueToEmit);
55
+ }
56
+ </script>
57
+
58
+ <template>
59
+ <div>
60
+ <div class="row mb-40">
61
+ <div class="col span-12">
62
+ <ArrayList
63
+ key="headers"
64
+ v-model:value="spec.filters"
65
+ :value-placeholder="t('auditPolicy.filters.placeholder')"
66
+ :add-label="t('auditPolicy.filters.add')"
67
+ :mode="mode"
68
+ :protip="false"
69
+ :defaultAddValue="defaultAddValue"
70
+ :show-header="true"
71
+ @update:value="addRow('action', $event)"
72
+ >
73
+ <template v-slot:column-headers>
74
+ <div class="filters-heading mb-10">
75
+ <div
76
+ class="row"
77
+ >
78
+ <div class="col span-6">
79
+ <span class="text-label">{{ t('auditPolicy.filters.action.title') }}</span>
80
+ </div>
81
+ <div class="col span-6 send-to">
82
+ <span class="text-label">{{ t('auditPolicy.filters.requestURI.title') }}</span>
83
+ </div>
84
+ </div>
85
+ </div>
86
+ </template>
87
+ <template v-slot:columns="scope">
88
+ <div class="row">
89
+ <div class="col span-6">
90
+ <LabeledSelect
91
+ v-model:value="scope.row.value.action"
92
+ :options="[{ value: 'allow', label: t('auditPolicy.filters.action.allow')},{ value: 'deny', label: t('auditPolicy.filters.action.deny') }]"
93
+ :mode="mode"
94
+ :placeholder="t('auditPolicy.filters.action.placeholder')"
95
+ @update:value="updateRow('action', scope.i, $event)"
96
+ />
97
+ </div>
98
+ <div class="col span-6">
99
+ <LabeledInput
100
+ v-model:value="scope.row.value.requestURI"
101
+ :mode="mode"
102
+ :placeholder="t('auditPolicy.filters.requestURI.placeholder')"
103
+ @update:value="updateRow('requestURI', scope.i, $event, )"
104
+ />
105
+ </div>
106
+ </div>
107
+ </template>
108
+ </ArrayList>
109
+ </div>
110
+ </div>
111
+ </div>
112
+ </template>
113
+
114
+ <style lang="scss" scoped>
115
+ .filters-heading {
116
+ display: grid;
117
+ grid-template-columns: auto $array-list-remove-margin;
118
+ }
119
+ </style>
@@ -0,0 +1,180 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref, watch } from 'vue';
3
+ import { useStore } from 'vuex';
4
+ import { useI18n } from '@shell/composables/useI18n';
5
+ import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
6
+ import Checkbox from '@components/Form/Checkbox/Checkbox.vue';
7
+ import { AuditPolicy } from '@shell/edit/auditlog.cattle.io.auditpolicy/types';
8
+ import Banner from '@components/Banner/Banner.vue';
9
+
10
+ // Component Props & Emits
11
+ const props = defineProps({
12
+ value: {
13
+ type: Object,
14
+ default: () => ({})
15
+ },
16
+ mode: {
17
+ type: String,
18
+ default: 'create'
19
+ }
20
+ });
21
+
22
+ const emit = defineEmits<{
23
+ 'update:value': [value: AuditPolicy];
24
+ }>();
25
+
26
+ // Options
27
+ const levelOptions = [0, 1, 2, 3];
28
+
29
+ // Store & i18n
30
+ const store = useStore();
31
+ const { t } = useI18n(store);
32
+
33
+ // Default Values & Reactive Spec
34
+ const defaults: AuditPolicy = {
35
+ enabled: false,
36
+ verbosity: {
37
+ level: 0, // The default level is 0, even if you set null, it will save as 0
38
+ request: {
39
+ headers: false,
40
+ body: false,
41
+ },
42
+ response: {
43
+ headers: false,
44
+ body: false,
45
+ }
46
+ }
47
+ };
48
+
49
+ const spec = ref<AuditPolicy>({
50
+ ...defaults,
51
+ ...props.value,
52
+ verbosity: {
53
+ ...defaults.verbosity,
54
+ ...props.value?.verbosity,
55
+ request: {
56
+ ...defaults.verbosity?.request,
57
+ ...props.value?.verbosity?.request,
58
+ },
59
+ response: {
60
+ ...defaults.verbosity?.response,
61
+ ...props.value?.verbosity?.response,
62
+ }
63
+ },
64
+ });
65
+
66
+ // Emit update immediately after initializing spec
67
+ emit('update:value', { ...props.value, ...spec.value });
68
+
69
+ // Watch for changes and emit updates
70
+ watch(spec, (newSpec) => {
71
+ const valueToEmit = { ...props.value, ...newSpec };
72
+
73
+ emit('update:value', valueToEmit);
74
+ }, { deep: true });
75
+
76
+ // Computed Properties
77
+ const levelOptionsMap = computed(() => levelOptions.map((value) => {
78
+ return { value, label: `${ t(`auditPolicy.general.verbosity.level.${ value }`) }` };
79
+ }));
80
+ </script>
81
+
82
+ <template>
83
+ <div>
84
+ <div class="row">
85
+ <div class="col span-6">
86
+ <fieldset>
87
+ <h3>{{ t("auditPolicy.general.enabled.title") }}</h3>
88
+ <Checkbox
89
+ v-model:value="spec.enabled"
90
+ :mode="mode"
91
+ label-key="auditPolicy.general.enabled.checkbox"
92
+ data-testid="auditPolicy-enabled"
93
+ />
94
+ </fieldset>
95
+ </div>
96
+ </div>
97
+ <div class="spacer" />
98
+ <div class="row">
99
+ <div class="col span-6">
100
+ <fieldset>
101
+ <h3>{{ t("auditPolicy.general.verbosity.title") }}</h3>
102
+ <Banner
103
+ class="mt-0"
104
+ color="info"
105
+ label-key="auditPolicy.general.verbosity.banner"
106
+ />
107
+
108
+ <h4>
109
+ {{ t("auditPolicy.general.verbosity.level.title") }}
110
+ <i
111
+ v-clean-tooltip="t('auditPolicy.general.verbosity.level.tooltip')"
112
+ class="icon icon-info"
113
+ />
114
+ </h4>
115
+ <div class="row">
116
+ <div class="col span-12">
117
+ <LabeledSelect
118
+ v-model:value="spec.verbosity!.level"
119
+ :label="t('auditPolicy.general.verbosity.level.label')"
120
+ :options="levelOptionsMap"
121
+ :mode="mode"
122
+ />
123
+ </div>
124
+ </div>
125
+ <div class="spacer-small" />
126
+ <div
127
+ class="row"
128
+ >
129
+ <div class="col span-6">
130
+ <h4>
131
+ {{ t("auditPolicy.general.verbosity.request.title") }}
132
+ <i
133
+ v-clean-tooltip="t('auditPolicy.general.verbosity.requestResponse.tooltip')"
134
+ class="icon icon-info"
135
+ />
136
+ </h4>
137
+ <div class="row">
138
+ <Checkbox
139
+ v-model:value="spec.verbosity!.request!.headers"
140
+ :label="t('auditPolicy.general.verbosity.request.requestHeaders')"
141
+ :mode="mode"
142
+ />
143
+ </div>
144
+ <div class="row">
145
+ <Checkbox
146
+ v-model:value="spec.verbosity!.request!.body"
147
+ :label="t('auditPolicy.general.verbosity.request.requestBody')"
148
+ :mode="mode"
149
+ />
150
+ </div>
151
+ </div>
152
+ <div class="col span-6">
153
+ <h4>
154
+ {{ t("auditPolicy.general.verbosity.response.title") }}
155
+ <i
156
+ v-clean-tooltip="t('auditPolicy.general.verbosity.requestResponse.tooltip')"
157
+ class="icon icon-info"
158
+ />
159
+ </h4>
160
+ <div class="row">
161
+ <Checkbox
162
+ v-model:value="spec.verbosity!.response!.headers"
163
+ :label="t('auditPolicy.general.verbosity.response.responseHeaders')"
164
+ :mode="mode"
165
+ />
166
+ </div>
167
+ <div class="row">
168
+ <Checkbox
169
+ v-model:value="spec.verbosity!.response!.body"
170
+ :label="t('auditPolicy.general.verbosity.response.responseBody')"
171
+ :mode="mode"
172
+ />
173
+ </div>
174
+ </div>
175
+ </div>
176
+ </fieldset>
177
+ </div>
178
+ </div>
179
+ </div>
180
+ </template>
@@ -0,0 +1,327 @@
1
+ import { shallowMount, VueWrapper } from '@vue/test-utils';
2
+ import AdditionalRedactions from '../AdditionalRedactions.vue';
3
+ import { ComponentPublicInstance } from 'vue';
4
+ import { AuditPolicy } from '@shell/edit/auditlog.cattle.io.auditpolicy/types';
5
+
6
+ // Mock the ID generation to have consistent snapshots
7
+ jest.mock('@shell/utils/string', () => ({ generateRandomAlphaString: () => 'test-id-123' }));
8
+
9
+ interface AdditionalRedactionsComponent extends ComponentPublicInstance {
10
+ spec: AuditPolicy;
11
+ addRedaction: () => void;
12
+ removeRedaction: (index: number) => void;
13
+ redactionLabel: (index: number) => string;
14
+ }
15
+
16
+ const defaultProps = {
17
+ value: { additionalRedactions: [] },
18
+ mode: 'create'
19
+ };
20
+
21
+ const globalMocks = {
22
+ global: {
23
+ mocks: {
24
+ $t: (key: string) => key,
25
+ t: (key: string) => key,
26
+ $store: {
27
+ getters: { 'i18n/t': (key: string) => key },
28
+ dispatch: jest.fn()
29
+ },
30
+ $route: {
31
+ params: {},
32
+ query: {}
33
+ },
34
+ $router: {
35
+ push: jest.fn(),
36
+ replace: jest.fn()
37
+ }
38
+ },
39
+ stubs: {
40
+ Tabbed: true,
41
+ Tab: true,
42
+ ArrayList: true
43
+ }
44
+ }
45
+ };
46
+
47
+ function factory(props: Record<string, any> = {}, options: Record<string, any> = {}): VueWrapper<AdditionalRedactionsComponent> {
48
+ return shallowMount(AdditionalRedactions, {
49
+ props: { ...defaultProps, ...props },
50
+ ...globalMocks,
51
+ ...options
52
+ }) as unknown as VueWrapper<AdditionalRedactionsComponent>;
53
+ }
54
+
55
+ describe('component: AdditionalRedactions', () => {
56
+ describe('rendering & initial state', () => {
57
+ it('should render with default props (snapshot)', () => {
58
+ const wrapper = factory();
59
+
60
+ expect(wrapper.element).toMatchSnapshot();
61
+ });
62
+
63
+ it('should render with create mode', () => {
64
+ const wrapper = factory({ mode: 'create' });
65
+
66
+ expect(wrapper.exists()).toBe(true);
67
+ expect(wrapper.find('.row.mb-40').exists()).toBe(true);
68
+ });
69
+
70
+ it('should render with edit mode', () => {
71
+ const wrapper = factory({ mode: 'edit' });
72
+
73
+ expect(wrapper.exists()).toBe(true);
74
+ expect(wrapper.find('.row.mb-40').exists()).toBe(true);
75
+ });
76
+
77
+ it('should render with initial redactions data', () => {
78
+ const value = {
79
+ additionalRedactions: [
80
+ { headers: ['X-Test-Header'], paths: ['/api/test'] },
81
+ { headers: ['Authorization'], paths: ['/secure'] }
82
+ ]
83
+ };
84
+ const wrapper = factory({ value });
85
+
86
+ expect(wrapper.vm.spec.additionalRedactions).toHaveLength(2);
87
+ });
88
+ });
89
+
90
+ describe('props & state changes', () => {
91
+ it('should handle empty value prop gracefully', () => {
92
+ const wrapper = factory({ value: undefined });
93
+
94
+ expect(wrapper.exists()).toBe(true);
95
+ expect(wrapper.vm.spec.additionalRedactions).toStrictEqual([]);
96
+ });
97
+
98
+ it('should handle null value prop gracefully', () => {
99
+ const wrapper = factory({ value: null });
100
+
101
+ expect(wrapper.exists()).toBe(true);
102
+ expect(wrapper.vm.spec.additionalRedactions).toStrictEqual([]);
103
+ });
104
+
105
+ it('should update when mode prop changes', async() => {
106
+ const wrapper = factory({ mode: 'create' });
107
+
108
+ expect((wrapper.props() as any).mode).toBe('create');
109
+ await wrapper.setProps({ mode: 'view' });
110
+ expect((wrapper.props() as any).mode).toBe('view');
111
+ });
112
+
113
+ it('should merge defaults with provided value', () => {
114
+ const value = {
115
+ additionalRedactions: [{ headers: ['Custom'], paths: ['/custom'] }],
116
+ customProp: 'test'
117
+ };
118
+ const wrapper = factory({ value });
119
+
120
+ expect((wrapper.vm.spec.additionalRedactions ?? [])).toHaveLength(1);
121
+ expect((wrapper.vm.spec.additionalRedactions ?? [])[0]).toStrictEqual({ headers: ['Custom'], paths: ['/custom'] });
122
+ expect((wrapper.vm.spec as any).customProp).toBe('test');
123
+ });
124
+ });
125
+
126
+ describe('user interaction', () => {
127
+ it('should emit update:value when addRedaction is called', () => {
128
+ const wrapper = factory();
129
+
130
+ wrapper.vm.addRedaction();
131
+
132
+ expect(wrapper.emitted('update:value')).toBeTruthy();
133
+ const events = wrapper.emitted('update:value');
134
+
135
+ expect(events && events[0]).toBeTruthy();
136
+
137
+ const emitted = events && events[0] && events[0][0] as AuditPolicy;
138
+
139
+ expect(emitted && emitted.additionalRedactions?.length).toBe(1);
140
+ expect(emitted && emitted.additionalRedactions?.[0]).toStrictEqual({
141
+ headers: [],
142
+ paths: []
143
+ });
144
+ });
145
+
146
+ it('should emit update:value when removeRedaction is called', () => {
147
+ const value = {
148
+ additionalRedactions: [
149
+ { headers: ['X-Test'], paths: ['/foo'] },
150
+ { headers: ['Authorization'], paths: ['/bar'] }
151
+ ]
152
+ };
153
+ const wrapper = factory({ value });
154
+
155
+ wrapper.vm.removeRedaction(0);
156
+
157
+ expect(wrapper.emitted('update:value')).toBeTruthy();
158
+ const events = wrapper.emitted('update:value');
159
+
160
+ expect(events && events[0]).toBeTruthy();
161
+
162
+ const emitted = events && events[0] && events[0][0] as AuditPolicy;
163
+
164
+ expect(emitted && emitted.additionalRedactions?.length).toBe(1);
165
+ expect(emitted && emitted.additionalRedactions?.[0]).toStrictEqual({
166
+ headers: ['Authorization'],
167
+ paths: ['/bar']
168
+ });
169
+ });
170
+
171
+ it('should preserve existing prop values when emitting updates', () => {
172
+ const existingValue = { someOtherProp: 'existing' };
173
+ const wrapper = factory({ value: existingValue });
174
+
175
+ wrapper.vm.addRedaction();
176
+
177
+ const events = wrapper.emitted('update:value');
178
+
179
+ expect(events && events[0]).toBeTruthy();
180
+ const emitted = events && events[0] && events[0][0] as AuditPolicy & { someOtherProp: string };
181
+
182
+ expect(emitted && emitted.someOtherProp).toBe('existing');
183
+ expect(emitted && emitted.additionalRedactions).toHaveLength(1);
184
+ });
185
+
186
+ it('should remove correct redaction by index', () => {
187
+ const value = {
188
+ additionalRedactions: [
189
+ { headers: ['First'], paths: ['/first'] },
190
+ { headers: ['Second'], paths: ['/second'] },
191
+ { headers: ['Third'], paths: ['/third'] }
192
+ ]
193
+ };
194
+ const wrapper = factory({ value });
195
+
196
+ wrapper.vm.removeRedaction(1); // Remove middle item
197
+
198
+ const events = wrapper.emitted('update:value');
199
+
200
+ expect(events && events[0]).toBeTruthy();
201
+ const emitted = events && events[0] && events[0][0] as AuditPolicy;
202
+
203
+ expect(emitted && emitted.additionalRedactions).toHaveLength(2);
204
+ expect(emitted && emitted.additionalRedactions?.[0]).toStrictEqual({ headers: ['First'], paths: ['/first'] });
205
+ expect(emitted && emitted.additionalRedactions?.[1]).toStrictEqual({ headers: ['Third'], paths: ['/third'] });
206
+ });
207
+ });
208
+
209
+ describe('computed properties & logic', () => {
210
+ it('should return correct redactionLabel values', () => {
211
+ const wrapper = factory();
212
+
213
+ expect(wrapper.vm.redactionLabel(0)).toBe('Rule 1');
214
+ expect(wrapper.vm.redactionLabel(4)).toBe('Rule 5');
215
+ expect(wrapper.vm.redactionLabel(99)).toBe('Rule 100');
216
+ });
217
+
218
+ it('should have reactive redactionLabel computed property', () => {
219
+ const wrapper = factory();
220
+ const labelFn = wrapper.vm.redactionLabel;
221
+
222
+ // Test that it returns a function that computes labels
223
+ expect(typeof labelFn).toBe('function');
224
+ expect(labelFn(0)).toBe('Rule 1');
225
+ expect(labelFn(1)).toBe('Rule 2');
226
+ });
227
+
228
+ it('should initialize spec reactive ref correctly', () => {
229
+ const wrapper = factory();
230
+
231
+ expect(wrapper.vm.spec).toBeDefined();
232
+ expect(Array.isArray(wrapper.vm.spec.additionalRedactions ?? [])).toBe(true);
233
+ });
234
+
235
+ it('should merge defaults with props correctly in spec', () => {
236
+ const value = { additionalRedactions: [{ headers: ['Test'], paths: ['/test'] }] };
237
+ const wrapper = factory({ value });
238
+
239
+ expect((wrapper.vm.spec.additionalRedactions ?? [])).toHaveLength(1);
240
+ expect((wrapper.vm.spec.additionalRedactions ?? [])[0]).toStrictEqual({
241
+ headers: ['Test'],
242
+ paths: ['/test']
243
+ });
244
+ });
245
+ });
246
+
247
+ describe('component configuration', () => {
248
+ it('should configure Tabbed component with correct props', () => {
249
+ const wrapper = factory();
250
+ const tabbedComponent = wrapper.findComponent({ name: 'Tabbed' });
251
+
252
+ expect(tabbedComponent.exists()).toBe(true);
253
+ expect(tabbedComponent.props('sideTabs')).toBe(true);
254
+ expect(tabbedComponent.props('useHash')).toBe(true);
255
+ });
256
+
257
+ it('should show/hide add/remove tabs based on mode', () => {
258
+ const createWrapper = factory({ mode: 'create' });
259
+ const editWrapper = factory({ mode: 'edit' });
260
+ const viewWrapper = factory({ mode: 'view' });
261
+
262
+ expect(createWrapper.findComponent({ name: 'Tabbed' }).props('showTabsAddRemove')).toBe(true);
263
+ expect(editWrapper.findComponent({ name: 'Tabbed' }).props('showTabsAddRemove')).toBe(true);
264
+ expect(viewWrapper.findComponent({ name: 'Tabbed' }).props('showTabsAddRemove')).toBe(false);
265
+ });
266
+
267
+ it('should bind correct event handlers to Tabbed component', () => {
268
+ const wrapper = factory();
269
+ const tabbedComponent = wrapper.findComponent({ name: 'Tabbed' });
270
+
271
+ // Simulate events from Tabbed component
272
+ tabbedComponent.vm.$emit('addTab');
273
+ expect(wrapper.emitted('update:value')).toBeTruthy();
274
+
275
+ const value = { additionalRedactions: [{ headers: [], paths: [] }] };
276
+ const wrapperWithData = factory({ value });
277
+ const tabbedWithData = wrapperWithData.findComponent({ name: 'Tabbed' });
278
+
279
+ tabbedWithData.vm.$emit('removeTab', 0);
280
+ expect(wrapperWithData.emitted('update:value')).toBeTruthy();
281
+ });
282
+ });
283
+
284
+ describe('edge cases', () => {
285
+ it('should handle empty additionalRedactions array', () => {
286
+ const wrapper = factory({ value: { additionalRedactions: [] } });
287
+
288
+ expect((wrapper.vm.spec.additionalRedactions ?? [])).toStrictEqual([]);
289
+ expect(wrapper.findAllComponents({ name: 'Tab' })).toHaveLength(0);
290
+ });
291
+
292
+ it('should handle missing additionalRedactions property', () => {
293
+ const wrapper = factory({ value: {} });
294
+
295
+ expect((wrapper.vm.spec.additionalRedactions ?? [])).toStrictEqual([]);
296
+ });
297
+
298
+ it('should not crash when removing from empty array', () => {
299
+ const wrapper = factory({ value: { additionalRedactions: [] } });
300
+
301
+ expect(() => {
302
+ wrapper.vm.removeRedaction(0);
303
+ }).not.toThrow();
304
+
305
+ const events = wrapper.emitted('update:value');
306
+
307
+ expect(events && events[0]).toBeTruthy();
308
+ const emitted = events && events[0] && events[0][0] as AuditPolicy;
309
+
310
+ expect(emitted && emitted.additionalRedactions).toStrictEqual([]);
311
+ });
312
+
313
+ it('should handle out of bounds removal index gracefully', () => {
314
+ const value = { additionalRedactions: [{ headers: ['Test'], paths: ['/test'] }] };
315
+ const wrapper = factory({ value });
316
+
317
+ wrapper.vm.removeRedaction(5); // Index out of bounds
318
+
319
+ const events = wrapper.emitted('update:value');
320
+
321
+ expect(events && events[0]).toBeTruthy();
322
+ const emitted = events && events[0] && events[0][0] as AuditPolicy;
323
+
324
+ expect(emitted && emitted.additionalRedactions).toHaveLength(1); // Should remain unchanged
325
+ });
326
+ });
327
+ });