@rancher/shell 3.0.7 → 3.0.8-rc.2

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 (123) hide show
  1. package/assets/images/vendor/githubapp.svg +13 -0
  2. package/assets/styles/base/_typography.scss +1 -1
  3. package/assets/styles/global/_layout.scss +21 -35
  4. package/assets/styles/themes/_modern.scss +5 -5
  5. package/assets/translations/en-us.yaml +102 -17
  6. package/assets/translations/zh-hans.yaml +0 -4
  7. package/components/EmberPage.vue +1 -1
  8. package/components/Inactivity.vue +222 -106
  9. package/components/InstallHelmCharts.vue +2 -2
  10. package/components/Resource/Detail/CopyToClipboard.vue +1 -1
  11. package/components/Resource/Detail/TitleBar/__tests__/index.test.ts +0 -2
  12. package/components/Resource/Detail/TitleBar/index.vue +10 -6
  13. package/components/ResourceDetail/index.vue +4 -1
  14. package/components/SortableTable/index.vue +18 -2
  15. package/components/{nav/WindowManager → Window}/ContainerLogs.vue +1 -1
  16. package/components/{nav/WindowManager → Window}/ContainerLogsActions.vue +1 -0
  17. package/components/{nav/WindowManager → Window}/__tests__/ContainerLogs.test.ts +1 -1
  18. package/components/{nav/WindowManager → Window}/__tests__/ContainerShell.test.ts +2 -2
  19. package/components/fleet/FleetConfigMapSelector.vue +117 -0
  20. package/components/fleet/FleetSecretSelector.vue +127 -0
  21. package/components/fleet/__tests__/FleetConfigMapSelector.test.ts +125 -0
  22. package/components/fleet/__tests__/FleetSecretSelector.test.ts +82 -0
  23. package/components/form/FileImageSelector.vue +13 -4
  24. package/components/form/FileSelector.vue +11 -2
  25. package/components/form/ResourceLabeledSelect.vue +1 -0
  26. package/components/form/__tests__/ResourceLabeledSelect.test.ts +90 -0
  27. package/components/nav/Header.vue +34 -13
  28. package/components/{DraggableZone.vue → nav/WindowManager/PinArea.vue} +47 -80
  29. package/components/nav/WindowManager/composables/useComponentsMount.ts +70 -0
  30. package/components/nav/WindowManager/composables/useDimensionsHandler.ts +105 -0
  31. package/components/nav/WindowManager/composables/useDragHandler.ts +99 -0
  32. package/components/nav/WindowManager/composables/usePanelHandler.ts +72 -0
  33. package/components/nav/WindowManager/composables/usePanelsHandler.ts +14 -0
  34. package/components/nav/WindowManager/composables/useResizeHandler.ts +167 -0
  35. package/components/nav/WindowManager/composables/useTabsHandler.ts +51 -0
  36. package/components/nav/WindowManager/constants.ts +23 -0
  37. package/components/nav/WindowManager/index.vue +61 -575
  38. package/components/nav/WindowManager/panels/HorizontalPanel.vue +265 -0
  39. package/components/nav/WindowManager/panels/TabBodyContainer.vue +39 -0
  40. package/components/nav/WindowManager/panels/VerticalPanel.vue +308 -0
  41. package/components/templates/default.vue +4 -40
  42. package/components/templates/home.vue +31 -5
  43. package/config/product/auth.js +1 -0
  44. package/config/query-params.js +1 -0
  45. package/config/settings.ts +8 -1
  46. package/config/store.js +4 -2
  47. package/config/types.js +2 -0
  48. package/detail/pod.vue +1 -0
  49. package/dialog/AddonConfigConfirmationDialog.vue +45 -1
  50. package/directives/ui-context.ts +97 -0
  51. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +52 -11
  52. package/edit/auth/AuthProviderWarningBanners.vue +14 -1
  53. package/edit/auth/github-app-steps.vue +97 -0
  54. package/edit/auth/github-steps.vue +75 -0
  55. package/edit/auth/github.vue +94 -65
  56. package/edit/fleet.cattle.io.helmop.vue +51 -2
  57. package/edit/networking.k8s.io.networkpolicy/PolicyRuleTarget.vue +15 -5
  58. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +11 -9
  59. package/edit/provisioning.cattle.io.cluster/rke2.vue +56 -9
  60. package/edit/provisioning.cattle.io.cluster/tabs/AddOnConfig.vue +28 -2
  61. package/initialize/install-directives.js +2 -0
  62. package/list/projectsecret.vue +1 -1
  63. package/machine-config/azure.vue +1 -1
  64. package/mixins/chart.js +1 -1
  65. package/models/__tests__/chart.test.ts +17 -9
  66. package/models/__tests__/compliance.cattle.io.clusterscanprofile.spec.js +30 -0
  67. package/models/catalog.cattle.io.app.js +1 -1
  68. package/models/chart.js +3 -1
  69. package/models/compliance.cattle.io.clusterscanprofile.js +1 -1
  70. package/models/management.cattle.io.authconfig.js +1 -0
  71. package/package.json +2 -2
  72. package/pages/auth/login.vue +5 -2
  73. package/pages/auth/verify.vue +1 -1
  74. package/pages/c/_cluster/apps/charts/AppChartCardSubHeader.vue +3 -2
  75. package/pages/c/_cluster/apps/charts/chart.vue +2 -2
  76. package/pages/c/_cluster/explorer/EventsTable.vue +89 -3
  77. package/pages/c/_cluster/explorer/tools/index.vue +3 -3
  78. package/pages/c/_cluster/settings/performance.vue +12 -25
  79. package/pages/home.vue +313 -12
  80. package/plugins/axios.js +2 -1
  81. package/plugins/dashboard-store/actions.js +1 -1
  82. package/plugins/dashboard-store/resource-class.js +17 -2
  83. package/plugins/steve/steve-pagination-utils.ts +2 -2
  84. package/rancher-components/RcDropdown/RcDropdownItemSelect.vue +5 -1
  85. package/scripts/extension/publish +1 -1
  86. package/store/auth.js +8 -3
  87. package/store/aws.js +8 -6
  88. package/store/features.js +1 -0
  89. package/store/index.js +9 -3
  90. package/store/prefs.js +6 -0
  91. package/store/ui-context.ts +86 -0
  92. package/store/wm.ts +244 -0
  93. package/types/kube/kube-api.ts +2 -1
  94. package/types/rancher/index.d.ts +1 -0
  95. package/types/resources/settings.d.ts +29 -7
  96. package/types/shell/index.d.ts +59 -0
  97. package/types/window-manager.ts +22 -0
  98. package/utils/__tests__/cluster.test.ts +379 -1
  99. package/utils/cluster.js +157 -3
  100. package/utils/dynamic-content/__tests__/config.test.ts +187 -0
  101. package/utils/dynamic-content/__tests__/index.test.ts +390 -0
  102. package/utils/dynamic-content/__tests__/info.test.ts +263 -0
  103. package/utils/dynamic-content/__tests__/new-release.test.ts +216 -0
  104. package/utils/dynamic-content/__tests__/support-notice.test.ts +262 -0
  105. package/utils/dynamic-content/__tests__/util.test.ts +235 -0
  106. package/utils/dynamic-content/config.ts +55 -0
  107. package/utils/dynamic-content/index.ts +273 -0
  108. package/utils/dynamic-content/info.ts +219 -0
  109. package/utils/dynamic-content/new-release.ts +126 -0
  110. package/utils/dynamic-content/support-notice.ts +169 -0
  111. package/utils/dynamic-content/types.d.ts +101 -0
  112. package/utils/dynamic-content/util.ts +122 -0
  113. package/utils/dynamic-importer.js +2 -2
  114. package/utils/inactivity.ts +104 -0
  115. package/utils/pagination-utils.ts +19 -4
  116. package/utils/release-notes.ts +1 -1
  117. package/assets/images/icons/document.svg +0 -3
  118. package/store/wm.js +0 -95
  119. /package/components/{nav/WindowManager → Window}/ChartReadme.vue +0 -0
  120. /package/components/{nav/WindowManager → Window}/ContainerShell.vue +0 -0
  121. /package/components/{nav/WindowManager → Window}/KubectlShell.vue +0 -0
  122. /package/components/{nav/WindowManager → Window}/MachineSsh.vue +0 -0
  123. /package/components/{nav/WindowManager → Window}/Window.vue +0 -0
@@ -0,0 +1,263 @@
1
+ import { SystemInfoProvider } from '../info';
2
+ import { MANAGEMENT, COUNT } from '@shell/config/types';
3
+ import { SETTING } from '@shell/config/settings';
4
+ import * as version from '@shell/config/version';
5
+ import { sha256 } from '@shell/utils/crypto';
6
+
7
+ // Mock dependencies from @shell
8
+ jest.mock('@shell/config/version', () => ({ getVersionData: jest.fn(), isRancherPrime: jest.fn() }));
9
+
10
+ jest.mock('@shell/utils/crypto', () => ({ sha256: jest.fn((val: string) => `hashed_${ val }`) }));
11
+
12
+ describe('systemInfoProvider', () => {
13
+ let mockGetters: any;
14
+ let mockSettings: any[];
15
+ let mockClusters: any[];
16
+ let mockCounts: any;
17
+ let mockPlugins: any[];
18
+ let originalWindowLocation: Location;
19
+
20
+ beforeAll(() => {
21
+ originalWindowLocation = window.location;
22
+ // Mock window.location
23
+ delete (window as any).location;
24
+ (window as any).location = { host: 'fallback.host' };
25
+ });
26
+
27
+ afterAll(() => {
28
+ (window as any).location = originalWindowLocation;
29
+ });
30
+
31
+ beforeEach(() => {
32
+ // Reset mocks
33
+ (version.getVersionData as jest.Mock).mockClear();
34
+ (version.isRancherPrime as jest.Mock).mockClear();
35
+ (sha256 as jest.Mock).mockClear();
36
+
37
+ // Mock window properties
38
+ Object.defineProperty(window, 'screen', {
39
+ value: { width: 1920, height: 1080 },
40
+ writable: true
41
+ });
42
+ Object.defineProperty(window, 'innerWidth', { value: 1024, writable: true });
43
+ Object.defineProperty(window, 'innerHeight', { value: 768, writable: true });
44
+ Object.defineProperty(window.navigator, 'language', { value: 'en-US', writable: true });
45
+
46
+ mockSettings = [
47
+ { id: SETTING.SERVER_URL, value: 'https://rancher.test' },
48
+ { id: 'install-uuid', value: 'test-uuid' },
49
+ ];
50
+
51
+ mockClusters = [
52
+ {
53
+ id: 'local',
54
+ isLocal: true,
55
+ kubernetesVersionBase: '1.25',
56
+ provisioner: 'k3s',
57
+ status: { nodeCount: 3 },
58
+ },
59
+ {
60
+ id: 'c-12345',
61
+ isLocal: false,
62
+ }
63
+ ];
64
+
65
+ mockCounts = [{ counts: { [MANAGEMENT.CLUSTER]: { summary: { count: 2 } } } }];
66
+
67
+ mockPlugins = [
68
+ { name: 'harvester', builtin: false },
69
+ { name: 'some-custom-ext', builtin: false },
70
+ { name: 'core', builtin: true },
71
+ ];
72
+
73
+ mockGetters = {
74
+ 'management/typeRegistered': jest.fn().mockReturnValue(true),
75
+ 'management/all': jest.fn((type: string) => {
76
+ if (type === MANAGEMENT.SETTING) {
77
+ return mockSettings;
78
+ }
79
+ if (type === MANAGEMENT.CLUSTER) {
80
+ return mockClusters;
81
+ }
82
+ if (type === COUNT) {
83
+ return mockCounts;
84
+ }
85
+
86
+ return [];
87
+ }),
88
+ 'auth/principalId': 'user-123',
89
+ 'uiplugins/plugins': mockPlugins,
90
+ isSingleProduct: false,
91
+ 'management/byId': jest.fn((type: string, id: string) => {
92
+ if (type === MANAGEMENT.SETTING) {
93
+ return mockSettings.find((s) => s.id === id) || null;
94
+ }
95
+
96
+ return undefined;
97
+ }),
98
+ 'management/schemaFor': jest.fn(),
99
+ };
100
+
101
+ (version.getVersionData as jest.Mock).mockReturnValue({
102
+ Version: '2.8.0-rc1',
103
+ RancherPrime: 'false',
104
+ });
105
+
106
+ (version.isRancherPrime as jest.Mock).mockReturnValue(false);
107
+ });
108
+
109
+ it('should build a complete query string with all available data', () => {
110
+ const infoProvider = new SystemInfoProvider(mockGetters, {});
111
+ const qs = infoProvider.buildQueryString();
112
+
113
+ expect(qs).toContain('dcv=v1');
114
+ expect(qs).toContain('s=hashed_https://rancher.test');
115
+ expect(qs).toContain('u=hashed_user-123');
116
+ expect(qs).toContain('uuid=test-uuid');
117
+ expect(qs).toContain('v=2.8.0');
118
+ expect(qs).toContain('dev=true');
119
+ expect(qs).toContain('p=false');
120
+ // Add back when LTS is added
121
+ // expect(qs).toContain('lts=false');
122
+ expect(qs).toContain('cc=2');
123
+ expect(qs).toContain('lkv=1.25');
124
+ expect(qs).toContain('lcp=k3s');
125
+ expect(qs).toContain('lnc=3');
126
+ expect(qs).toContain('xkn=harvester');
127
+ expect(qs).toContain('xcc=1');
128
+ expect(qs).toContain('bl=en-US');
129
+ expect(qs).toContain('bs=1024x768');
130
+ expect(qs).toContain('ss=1920x1080');
131
+ });
132
+
133
+ it('should handle missing or partial data gracefully', () => {
134
+ // Override mocks for this test
135
+ (version.getVersionData as jest.Mock).mockReturnValue({
136
+ Version: '2.9.0', // Not a dev version
137
+ RancherPrime: 'true',
138
+ });
139
+ (version.isRancherPrime as jest.Mock).mockReturnValue(true);
140
+
141
+ mockGetters['management/all'].mockImplementation((type: string) => {
142
+ if (type === MANAGEMENT.SETTING) {
143
+ return [{ id: 'install-uuid', value: 'only-uuid' }]; // No server-url
144
+ }
145
+ if (type === COUNT) {
146
+ return [{ counts: { [MANAGEMENT.CLUSTER]: { summary: { count: 27 } } } }];
147
+ }
148
+ if (type === MANAGEMENT.CLUSTER) {
149
+ return []; // No clusters
150
+ }
151
+
152
+ return [];
153
+ });
154
+ mockGetters['management/byId'].mockImplementation((type: string, id: string) => {
155
+ if (type === MANAGEMENT.SETTING && id === 'install-uuid') {
156
+ return { id: 'install-uuid', value: 'only-uuid' }; // No server-url
157
+ }
158
+ });
159
+
160
+ mockGetters['uiplugins/plugins'] = null; // No plugins
161
+ mockGetters['auth/principalId'] = null; // No user
162
+
163
+ const infoProvider = new SystemInfoProvider(mockGetters, {});
164
+ const qs = infoProvider.buildQueryString();
165
+
166
+ expect(qs).toContain('s=hashed_fallback.host');
167
+ expect(qs).toContain('u=hashed_unknown');
168
+ expect(qs).toContain('uuid=only-uuid');
169
+ expect(qs).toContain('v=2.9.0');
170
+ expect(qs).toContain('dev=false');
171
+ expect(qs).toContain('p=true');
172
+ expect(qs).toContain('cc=27');
173
+ expect(qs).not.toContain('lkv=');
174
+ expect(qs).not.toContain('lcp=');
175
+ expect(qs).not.toContain('lnc=');
176
+ expect(qs).not.toContain('xkn=');
177
+ expect(qs).not.toContain('xcc=');
178
+ });
179
+
180
+ it('should handle getAll returning undefined when types are not registered', () => {
181
+ // Override mocks for this test
182
+ (version.getVersionData as jest.Mock).mockReturnValue({
183
+ Version: '2.9.1',
184
+ RancherPrime: 'false',
185
+ });
186
+
187
+ // Simulate that the management types are not registered in the store
188
+ mockGetters['management/typeRegistered'].mockReturnValue(false);
189
+ mockGetters['management/all'].mockImplementation();
190
+ mockGetters['management/byId'].mockImplementation();
191
+
192
+ mockGetters['auth/principalId'] = 'user-456';
193
+ mockGetters['uiplugins/plugins'] = []; // No plugins
194
+
195
+ const infoProvider = new SystemInfoProvider(mockGetters, {});
196
+ const qs = infoProvider.buildQueryString();
197
+
198
+ expect(mockGetters['management/byId']).toHaveBeenCalledWith(MANAGEMENT.SETTING, 'server-url');
199
+ expect(mockGetters['management/byId']).toHaveBeenCalledWith(MANAGEMENT.SETTING, 'install-uuid');
200
+ expect(mockGetters['management/byId']).toHaveBeenCalledWith(MANAGEMENT.SETTING, 'server-version-type');
201
+ expect(mockGetters['management/typeRegistered']).toHaveBeenCalledWith(COUNT);
202
+ expect(mockGetters['management/typeRegistered']).toHaveBeenCalledWith(MANAGEMENT.CLUSTER);
203
+ expect(mockGetters['management/all']).not.toHaveBeenCalled();
204
+
205
+ // Verify the query string is built with fallback or empty values
206
+ expect(qs).toContain('s=hashed_fallback.host');
207
+ expect(qs).toContain('u=hashed_user-456');
208
+ expect(qs).toContain('v=2.9.1');
209
+ expect(qs).toContain('p=false');
210
+ expect(qs).toContain('xcc=0');
211
+ expect(qs).not.toContain('uuid=');
212
+ expect(qs).not.toContain('lkv=');
213
+ });
214
+
215
+ it('should use UNKNOWN for missing system properties', () => {
216
+ // Override mocks for this test
217
+ (version.getVersionData as jest.Mock).mockReturnValue({
218
+ Version: '2.9.0',
219
+ RancherPrime: 'false',
220
+ });
221
+ (version.isRancherPrime as jest.Mock).mockReturnValue(false);
222
+
223
+ mockGetters['management/byId'].mockImplementation((type: string, id: string) => {
224
+ if (type === MANAGEMENT.SETTING) {
225
+ return { id, value: '' }; // Empty values for all settings
226
+ }
227
+ });
228
+ mockGetters['management/all'].mockImplementation((type: string) => {
229
+ if (type === MANAGEMENT.SETTING) {
230
+ // Return settings, but with empty values
231
+ return [
232
+ { id: SETTING.SERVER_URL, value: '' },
233
+ { id: 'install-uuid', value: '' },
234
+ ];
235
+ }
236
+ if (type === COUNT) {
237
+ return [{ counts: { [MANAGEMENT.CLUSTER]: { summary: { count: 1 } } } }];
238
+ }
239
+ if (type === MANAGEMENT.CLUSTER) {
240
+ // local cluster with missing properties
241
+ return [{
242
+ id: 'local',
243
+ isLocal: true,
244
+ status: { nodeCount: 1 },
245
+ // kubernetesVersionBase is missing
246
+ // provisioner is missing
247
+ }];
248
+ }
249
+
250
+ return [];
251
+ });
252
+
253
+ mockGetters['auth/principalId'] = null; // No user
254
+
255
+ const infoProvider = new SystemInfoProvider(mockGetters, {});
256
+ const qs = infoProvider.buildQueryString();
257
+
258
+ expect(qs).toContain('u=hashed_unknown');
259
+ expect(qs).toContain('lkv=unknown');
260
+ expect(qs).toContain('lcp=unknown');
261
+ expect(qs).not.toContain('uuid='); // systemUUID is UNKNOWN, so it's skipped
262
+ });
263
+ });
@@ -0,0 +1,216 @@
1
+ import { processReleaseVersion } from '../new-release';
2
+ import { NotificationLevel } from '@shell/types/notifications';
3
+ import { READ_NEW_RELEASE } from '@shell/store/prefs';
4
+ import { Context, VersionInfo } from '../types';
5
+ import * as util from '../util'; // Import util to mock removeMatchingNotifications
6
+ import semver from 'semver';
7
+
8
+ describe('processReleaseVersion', () => {
9
+ let mockContext: Context;
10
+ let mockDispatch: jest.Mock;
11
+ let mockGetters: any;
12
+ let mockLogger: any;
13
+ let mockRemoveMatchingNotifications: jest.SpyInstance;
14
+
15
+ beforeEach(() => {
16
+ mockDispatch = jest.fn();
17
+ mockGetters = {
18
+ 'prefs/get': jest.fn(),
19
+ 'i18n/t': jest.fn((key: string, params?: any) => {
20
+ // Simple mock for i18n/t to return predictable strings
21
+ const { version, version1, version2 } = params || {};
22
+
23
+ if (key === 'dynamicContent.newRelease.title') return `A new Rancher release is available`;
24
+ if (key === 'dynamicContent.newRelease.message') return `Rancher ${ version } has been released`;
25
+ if (key === 'dynamicContent.newRelease.moreInfo') return `More Info`;
26
+ if (key === 'dynamicContent.multipleNewReleases.title') return 'New Rancher releases are available';
27
+ if (key === 'dynamicContent.multipleNewReleases.message') return `Message for ${ version1 } and ${ version2 }`;
28
+ if (key === 'dynamicContent.multipleNewReleases.moreInfo') return `More Info for ${ version }`;
29
+
30
+ return key;
31
+ }),
32
+ };
33
+ mockLogger = {
34
+ info: jest.fn(),
35
+ debug: jest.fn(),
36
+ error: jest.fn(),
37
+ };
38
+
39
+ mockContext = {
40
+ dispatch: mockDispatch,
41
+ getters: mockGetters,
42
+ axios: {},
43
+ logger: mockLogger,
44
+ isAdmin: true,
45
+ config: {
46
+ enabled: true,
47
+ debug: false,
48
+ log: false,
49
+ endpoint: '',
50
+ prime: false,
51
+ distribution: 'community',
52
+ },
53
+ settings: {
54
+ releaseNotesUrl: 'https://example.com/releases/v$version',
55
+ suseExtensions: [],
56
+ },
57
+ };
58
+
59
+ // Mock the utility function. Default: notification does not exist, so add it.
60
+ mockRemoveMatchingNotifications = jest.spyOn(util, 'removeMatchingNotifications')
61
+ .mockResolvedValue(false);
62
+ });
63
+
64
+ afterEach(() => {
65
+ jest.restoreAllMocks();
66
+ });
67
+
68
+ it('should return early if releaseInfo is null/undefined or empty', async() => {
69
+ const versionInfo: VersionInfo = { version: semver.coerce('2.12.0')!, isPrime: false };
70
+
71
+ await processReleaseVersion(mockContext, null as any, versionInfo);
72
+ expect(mockDispatch).not.toHaveBeenCalled();
73
+ expect(mockLogger.info).not.toHaveBeenCalled();
74
+
75
+ await processReleaseVersion(mockContext, [], versionInfo);
76
+ expect(mockDispatch).not.toHaveBeenCalled();
77
+ expect(mockLogger.info).not.toHaveBeenCalled();
78
+ });
79
+
80
+ it('should return early if versionInfo is null/undefined or version is missing', async() => {
81
+ const releaseInfo = [{ name: '2.12.1' }];
82
+
83
+ await processReleaseVersion(mockContext, releaseInfo, null as any);
84
+ expect(mockDispatch).not.toHaveBeenCalled();
85
+ expect(mockLogger.info).not.toHaveBeenCalled();
86
+
87
+ await processReleaseVersion(mockContext, releaseInfo, { version: null as any, isPrime: false });
88
+ expect(mockDispatch).not.toHaveBeenCalled();
89
+ expect(mockLogger.info).not.toHaveBeenCalled();
90
+ });
91
+
92
+ it('should not add notification if no newer release exists', async() => {
93
+ const releaseInfo = [{ name: '2.12.0' }, { name: '2.11.0' }];
94
+ const versionInfo: VersionInfo = { version: semver.coerce('2.12.0')!, isPrime: false };
95
+
96
+ await processReleaseVersion(mockContext, releaseInfo, versionInfo);
97
+
98
+ expect(mockLogger.info).not.toHaveBeenCalledWith(expect.stringContaining('Found a newer release'));
99
+ expect(mockDispatch).not.toHaveBeenCalled();
100
+ });
101
+
102
+ it('should add a single new release notification for a newer patch version', async() => {
103
+ const releaseInfo = [{ name: '2.12.1' }, { name: '2.12.0' }, { name: '2.11.0' }];
104
+ const versionInfo: VersionInfo = { version: semver.coerce('2.12.0')!, isPrime: false };
105
+
106
+ await processReleaseVersion(mockContext, releaseInfo, versionInfo);
107
+
108
+ expect(mockLogger.info).toHaveBeenCalledWith('Found a newer release: 2.12.1');
109
+ expect(mockLogger.info).not.toHaveBeenCalledWith(expect.stringContaining('Also found a newer patch release')); // Because newer and newerPatch are the same
110
+ expect(mockDispatch).toHaveBeenCalledTimes(1);
111
+ expect(mockDispatch).toHaveBeenCalledWith('notifications/add', {
112
+ id: 'new-release-2.12.1',
113
+ level: NotificationLevel.Announcement,
114
+ title: 'A new Rancher release is available',
115
+ message: 'Rancher 2.12.1 has been released',
116
+ preference: {
117
+ key: READ_NEW_RELEASE,
118
+ value: '2.12.1',
119
+ },
120
+ primaryAction: {
121
+ label: 'More Info',
122
+ target: 'https://example.com/releases/v2.12.1',
123
+ },
124
+ });
125
+ expect(mockRemoveMatchingNotifications).toHaveBeenCalledWith(mockContext, 'new-release-', '2.12.1');
126
+ });
127
+
128
+ it('should add a single new release notification for a newer major/minor version (no patch)', async() => {
129
+ const releaseInfo = [{ name: '2.13.0' }, { name: '2.12.0' }];
130
+ const versionInfo: VersionInfo = { version: semver.coerce('2.12.0')!, isPrime: false };
131
+
132
+ await processReleaseVersion(mockContext, releaseInfo, versionInfo);
133
+
134
+ expect(mockLogger.info).toHaveBeenCalledWith('Found a newer release: 2.13.0');
135
+ expect(mockLogger.info).not.toHaveBeenCalledWith(expect.stringContaining('Also found a newer patch release'));
136
+ expect(mockDispatch).toHaveBeenCalledTimes(1);
137
+ expect(mockDispatch).toHaveBeenCalledWith('notifications/add', {
138
+ id: 'new-release-2.13.0',
139
+ level: NotificationLevel.Announcement,
140
+ title: 'A new Rancher release is available',
141
+ message: 'Rancher 2.13.0 has been released',
142
+ preference: {
143
+ key: READ_NEW_RELEASE,
144
+ value: '2.13.0',
145
+ },
146
+ primaryAction: {
147
+ label: 'More Info',
148
+ target: 'https://example.com/releases/v2.13.0',
149
+ },
150
+ });
151
+ expect(mockRemoveMatchingNotifications).toHaveBeenCalledWith(mockContext, 'new-release-', '2.13.0');
152
+ });
153
+
154
+ it('should add a multiple new releases notification when both newer patch and newer major/minor exist', async() => {
155
+ const releaseInfo = [{ name: '2.13.0' }, { name: '2.12.1' }, { name: '2.12.0' }];
156
+ const versionInfo: VersionInfo = { version: semver.coerce('2.12.0')!, isPrime: false };
157
+
158
+ await processReleaseVersion(mockContext, releaseInfo, versionInfo);
159
+
160
+ expect(mockLogger.info).toHaveBeenCalledWith('Found a newer release: 2.13.0');
161
+ expect(mockLogger.info).toHaveBeenCalledWith('Also found a newer patch release: 2.12.1');
162
+ expect(mockDispatch).toHaveBeenCalledTimes(1);
163
+ expect(mockDispatch).toHaveBeenCalledWith('notifications/add', {
164
+ id: 'new-release-2.12.1-2.13.0',
165
+ level: NotificationLevel.Announcement,
166
+ title: 'New Rancher releases are available',
167
+ message: 'Message for 2.12.1 and 2.13.0',
168
+ preference: {
169
+ key: READ_NEW_RELEASE,
170
+ value: '2.12.1-2.13.0',
171
+ },
172
+ primaryAction: {
173
+ label: 'More Info for 2.12.1',
174
+ target: 'https://example.com/releases/v2.12.1',
175
+ },
176
+ secondaryAction: {
177
+ label: 'More Info for 2.13.0',
178
+ target: 'https://example.com/releases/v2.13.0',
179
+ },
180
+ });
181
+ expect(mockRemoveMatchingNotifications).toHaveBeenCalledWith(mockContext, 'new-release-', '2.12.1-2.13.0');
182
+ });
183
+
184
+ it('should not add notification if it was already read (single release)', async() => {
185
+ mockGetters['prefs/get'].mockReturnValue('2.12.1'); // Simulate already read
186
+ const releaseInfo = [{ name: '2.12.1' }];
187
+ const versionInfo: VersionInfo = { version: semver.coerce('2.12.0')!, isPrime: false };
188
+
189
+ await processReleaseVersion(mockContext, releaseInfo, versionInfo);
190
+
191
+ expect(mockDispatch).not.toHaveBeenCalled();
192
+ expect(mockLogger.debug).not.toHaveBeenCalledWith(expect.stringContaining('Adding new release notification'));
193
+ });
194
+
195
+ it('should not add notification if it was already read (multiple releases)', async() => {
196
+ mockGetters['prefs/get'].mockReturnValue('2.12.1-2.13.0'); // Simulate already read
197
+ const releaseInfo = [{ name: '2.13.0' }, { name: '2.12.1' }];
198
+ const versionInfo: VersionInfo = { version: semver.coerce('2.12.0')!, isPrime: false };
199
+
200
+ await processReleaseVersion(mockContext, releaseInfo, versionInfo);
201
+
202
+ expect(mockDispatch).not.toHaveBeenCalled();
203
+ expect(mockLogger.info).not.toHaveBeenCalledWith(expect.stringContaining('Adding new multiple release notification'));
204
+ });
205
+
206
+ it('should not add notification if removeMatchingNotifications indicates it exists', async() => {
207
+ mockRemoveMatchingNotifications.mockResolvedValue(true); // Simulate notification already exists
208
+ const releaseInfo = [{ name: '2.12.1' }];
209
+ const versionInfo: VersionInfo = { version: semver.coerce('2.12.0')!, isPrime: false };
210
+
211
+ await processReleaseVersion(mockContext, releaseInfo, versionInfo);
212
+
213
+ expect(mockDispatch).not.toHaveBeenCalled();
214
+ expect(mockLogger.debug).not.toHaveBeenCalledWith(expect.stringContaining('Adding new release notification'));
215
+ });
216
+ });