From c2b9198cbd5077c917386e55dd98e498e641c82d Mon Sep 17 00:00:00 2001 From: ulrichschulte Date: Fri, 6 Feb 2026 15:18:16 +0100 Subject: [PATCH 1/4] Fixes #5003: suppress toast on error via option to axios request config (boolean or function) --- spring-boot-admin-server-ui/package-lock.json | 52 ++++++- spring-boot-admin-server-ui/package.json | 1 + .../@stekoe/vue-toast-notificationcenter.js | 15 ++ .../main/frontend/services/instance.spec.ts | 145 ++++++++++++++++++ .../src/main/frontend/services/instance.ts | 20 ++- .../src/main/frontend/tests/setup.ts | 11 ++ .../src/main/frontend/utils/axios.spec.ts | 85 +++++++++- .../src/main/frontend/utils/axios.ts | 35 +++-- .../instances/details/details-cache.spec.ts | 2 +- .../views/instances/details/details-cache.vue | 34 ++-- 10 files changed, 367 insertions(+), 33 deletions(-) create mode 100644 spring-boot-admin-server-ui/src/main/frontend/__mocks__/@stekoe/vue-toast-notificationcenter.js diff --git a/spring-boot-admin-server-ui/package-lock.json b/spring-boot-admin-server-ui/package-lock.json index a983171144c..6e82aa99b4f 100644 --- a/spring-boot-admin-server-ui/package-lock.json +++ b/spring-boot-admin-server-ui/package-lock.json @@ -78,6 +78,7 @@ "@vue/eslint-config-typescript": "^14.0.0", "@vue/test-utils": "2.4.6", "autoprefixer": "10.4.24", + "axios-mock-adapter": "^2.1.0", "babel-loader": "10.0.0", "eslint": "^9.0.0", "eslint-config-prettier": "^10.0.0", @@ -4645,6 +4646,20 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/axios-mock-adapter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-2.1.0.tgz", + "integrity": "sha512-AZUe4OjECGCNNssH8SOdtneiQELsqTsat3SQQCWLPjN436/H+L9AjWfV7bF+Zg/YL9cgbhrz5671hoh+Tbn98w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "is-buffer": "^2.0.5" + }, + "peerDependencies": { + "axios": ">= 0.17.0" + } + }, "node_modules/babel-loader": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-10.0.0.tgz", @@ -5872,9 +5887,9 @@ "license": "Apache-2.0" }, "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -7077,9 +7092,10 @@ } }, "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -7591,6 +7607,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", diff --git a/spring-boot-admin-server-ui/package.json b/spring-boot-admin-server-ui/package.json index f6f1b631664..995b9ac314c 100644 --- a/spring-boot-admin-server-ui/package.json +++ b/spring-boot-admin-server-ui/package.json @@ -89,6 +89,7 @@ "@vue/eslint-config-typescript": "^14.0.0", "@vue/test-utils": "2.4.6", "autoprefixer": "10.4.24", + "axios-mock-adapter": "^2.1.0", "babel-loader": "10.0.0", "eslint": "^9.0.0", "eslint-config-prettier": "^10.0.0", diff --git a/spring-boot-admin-server-ui/src/main/frontend/__mocks__/@stekoe/vue-toast-notificationcenter.js b/spring-boot-admin-server-ui/src/main/frontend/__mocks__/@stekoe/vue-toast-notificationcenter.js new file mode 100644 index 00000000000..6a39169d5dd --- /dev/null +++ b/spring-boot-admin-server-ui/src/main/frontend/__mocks__/@stekoe/vue-toast-notificationcenter.js @@ -0,0 +1,15 @@ +// __mocks__/@stekoe/vue-toast-notificationcenter.js +import { vi } from 'vitest'; + +// Ensure errorSpy is available +if (!globalThis.errorSpy) { + globalThis.errorSpy = vi.fn(); +} + +export const useNotificationCenter = () => ({ error: globalThis.errorSpy }); + +export default { + install(app) { + app.config.globalProperties.$nc = { error: globalThis.errorSpy }; + }, +}; diff --git a/spring-boot-admin-server-ui/src/main/frontend/services/instance.spec.ts b/spring-boot-admin-server-ui/src/main/frontend/services/instance.spec.ts index efdfeb958b7..b3547d52e19 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/services/instance.spec.ts +++ b/spring-boot-admin-server-ui/src/main/frontend/services/instance.spec.ts @@ -1,3 +1,4 @@ +import { AxiosError } from 'axios'; import { describe, expect, test, vi } from 'vitest'; import Instance from '@/services/instance'; @@ -40,4 +41,148 @@ describe('Instance', () => { expect(instance.showUrl()).toEqual(expectUrlToBeShownOnUI); }, ); + + describe('fetchMetric', () => { + test('should pass suppressToast option to axios config', async () => { + const instance = new Instance({ + id: 'test-id', + registration: { name: 'test' }, + }); + + // Spy on axios.get + const axiosGetSpy = vi.spyOn(instance.axios, 'get'); + + // Mock the axios request + axiosGetSpy.mockResolvedValue({ + data: { + measurements: [{ value: 42 }], + }, + }); + + await instance.fetchMetric( + 'test.metric', + { tag: 'value' }, + { + suppressToast: true, + }, + ); + + // Verify suppressToast was passed in config + expect(axiosGetSpy).toHaveBeenCalledWith( + expect.stringContaining('actuator/metrics/test.metric'), + expect.objectContaining({ + suppressToast: true, + }), + ); + }); + + test('should work without options parameter for backward compatibility', async () => { + const instance = new Instance({ + id: 'test-id', + registration: { name: 'test' }, + }); + + const axiosGetSpy = vi.spyOn(instance.axios, 'get'); + + axiosGetSpy.mockResolvedValue({ + data: { + measurements: [{ value: 42 }], + }, + }); + + await instance.fetchMetric('test.metric', { tag: 'value' }); + + // Verify it was called without suppressToast + expect(axiosGetSpy).toHaveBeenCalledWith( + expect.stringContaining('actuator/metrics/test.metric'), + expect.objectContaining({ + suppressToast: undefined, + }), + ); + }); + + test('should pass suppressToast=false when explicitly set to false', async () => { + const instance = new Instance({ + id: 'test-id', + registration: { name: 'test' }, + }); + + const axiosGetSpy = vi.spyOn(instance.axios, 'get'); + + axiosGetSpy.mockResolvedValue({ + data: { + measurements: [{ value: 42 }], + }, + }); + + await instance.fetchMetric( + 'test.metric', + { tag: 'value' }, + { + suppressToast: false, + }, + ); + + expect(axiosGetSpy).toHaveBeenCalledWith( + expect.stringContaining('actuator/metrics/test.metric'), + expect.objectContaining({ + suppressToast: false, + }), + ); + }); + + test('should include tags in request parameters', async () => { + const instance = new Instance({ + id: 'test-id', + registration: { name: 'test' }, + }); + + const axiosGetSpy = vi.spyOn(instance.axios, 'get'); + + axiosGetSpy.mockResolvedValue({ + data: { + measurements: [{ value: 42 }], + }, + }); + + await instance.fetchMetric('cache.gets', { + name: 'my-cache', + result: 'hit', + }); + + const callArgs = axiosGetSpy.mock.calls[0]; + const params = callArgs[1]?.params as URLSearchParams; + + expect(params).toBeInstanceOf(URLSearchParams); + expect(params.getAll('tag')).toContain('name:my-cache'); + expect(params.getAll('tag')).toContain('result:hit'); + }); + + test('should pass suppressToast function to axios config', async () => { + const instance = new Instance({ + id: 'test-id', + registration: { name: 'test' }, + }); + + const axiosGetSpy = vi.spyOn(instance.axios, 'get'); + + axiosGetSpy.mockResolvedValue({ + data: { + measurements: [{ value: 42 }], + }, + }); + + const suppressFn = (err: AxiosError) => err.response?.status === 404; + await instance.fetchMetric( + 'cache.size', + {}, + { suppressToast: suppressFn }, + ); + + expect(axiosGetSpy).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ suppressToast: suppressFn }), + ); + }); + }); }); diff --git a/spring-boot-admin-server-ui/src/main/frontend/services/instance.ts b/spring-boot-admin-server-ui/src/main/frontend/services/instance.ts index c25b1fba306..62f5841979f 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/services/instance.ts +++ b/spring-boot-admin-server-ui/src/main/frontend/services/instance.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { AxiosInstance } from 'axios'; +import { AxiosError, AxiosInstance } from 'axios'; import saveAs from 'file-saver'; import { Observable, concat, from, ignoreElements } from 'rxjs'; @@ -29,6 +29,17 @@ import { useSbaConfig } from '@/sba-config'; import { actuatorMimeTypes } from '@/services/spring-mime-types'; import { transformToJSON } from '@/utils/transformToJSON'; +// Extend AxiosRequestConfig to allow suppressToast +declare module 'axios' { + interface AxiosRequestConfig { + suppressToast?: boolean | ((error: AxiosError) => boolean); + } +} + +export type FetchMetricOptions = { + suppressToast?: boolean | ((error: AxiosError) => boolean); +}; + const isInstanceActuatorRequest = (url: string) => url.match(/^instances[/][^/]+[/]actuator([/].*)?$/); @@ -166,7 +177,11 @@ class Instance { return this.axios.get(uri`actuator/metrics`); } - async fetchMetric(metric, tags) { + async fetchMetric( + metric: string, + tags?: Record, + options?: FetchMetricOptions, + ) { const params = new URLSearchParams(); if (tags) { let firstElementDuplicated = false; @@ -187,6 +202,7 @@ class Instance { } return this.axios.get(uri`actuator/metrics/${metric}`, { params, + suppressToast: options?.suppressToast, }); } diff --git a/spring-boot-admin-server-ui/src/main/frontend/tests/setup.ts b/spring-boot-admin-server-ui/src/main/frontend/tests/setup.ts index 382a2af9a5a..e455d3e9f7e 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/tests/setup.ts +++ b/spring-boot-admin-server-ui/src/main/frontend/tests/setup.ts @@ -31,6 +31,17 @@ global.EventSource = class { global.SBA = sbaConfig; +// Mock localStorage globally for all tests +Object.defineProperty(global, 'localStorage', { + value: { + getItem: vi.fn(), + setItem: vi.fn(), + removeItem: vi.fn(), + clear: vi.fn(), + }, + writable: true, +}); + beforeAll(() => server.listen({ onUnhandledRequest: 'error' })); afterAll(() => server.close()); afterEach(() => server.resetHandlers()); diff --git a/spring-boot-admin-server-ui/src/main/frontend/utils/axios.spec.ts b/spring-boot-admin-server-ui/src/main/frontend/utils/axios.spec.ts index a69386c4de2..3fa6921c2d9 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/utils/axios.spec.ts +++ b/spring-boot-admin-server-ui/src/main/frontend/utils/axios.spec.ts @@ -13,9 +13,30 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import axios, { AxiosError } from 'axios'; +import MockAdapter from 'axios-mock-adapter'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { redirectOn401 } from './axios'; +import { redirectOn401, registerErrorToastInterceptor } from './axios'; + +// Initialize errorSpy BEFORE any mocks or imports +globalThis.errorSpy = vi.fn(); + +// Mock sba-config to enable toasts +vi.mock('../sba-config', () => ({ + default: { + uiSettings: { + enableToasts: true, + }, + csrf: { + parameterName: '_csrf', + headerName: 'X-XSRF-TOKEN', + }, + }, +})); + +// Use manual mock for @stekoe/vue-toast-notificationcenter +vi.mock('@stekoe/vue-toast-notificationcenter'); describe('redirectOn401', () => { beforeEach(() => { @@ -78,3 +99,63 @@ describe('redirectOn401', () => { expect(window.location.assign).not.toBeCalled(); }); }); + +describe('registerErrorToastInterceptor', () => { + let axiosInstance; + let mock; + + beforeEach(() => { + globalThis.errorSpy.mockClear(); + axiosInstance = axios.create(); + // Pass a mock notification center directly + registerErrorToastInterceptor(axiosInstance, { + error: globalThis.errorSpy, + }); + mock = new MockAdapter(axiosInstance); + }); + + afterEach(() => { + if (mock) mock.restore(); + vi.restoreAllMocks(); + }); + + it('shows toast by default', async () => { + mock.onGet('/fail').reply(500); + await expect(axiosInstance.get('/fail')).rejects.toBeDefined(); + expect(globalThis.errorSpy).toHaveBeenCalled(); + }); + + it('suppresses toast if suppressToast is true', async () => { + mock.onGet('/fail').reply(500); + await expect( + axiosInstance.get('/fail', { suppressToast: true }), + ).rejects.toBeDefined(); + expect(globalThis.errorSpy).not.toHaveBeenCalled(); + }); + + it('shows toast if suppressToast is false', async () => { + mock.onGet('/fail').reply(500); + await expect( + axiosInstance.get('/fail', { suppressToast: false }), + ).rejects.toBeDefined(); + expect(globalThis.errorSpy).toHaveBeenCalled(); + }); + + it('suppresses toast if suppressToast function returns true', async () => { + mock.onGet('/fail').reply(404); + const suppressFn = (err: AxiosError) => err.response?.status === 404; + await expect( + axiosInstance.get('/fail', { suppressToast: suppressFn }), + ).rejects.toBeDefined(); + expect(globalThis.errorSpy).not.toHaveBeenCalled(); + }); + + it('shows toast if suppressToast function returns false', async () => { + mock.onGet('/fail').reply(500); + const suppressFn = (err: AxiosError) => err.response?.status === 404; + await expect( + axiosInstance.get('/fail', { suppressToast: suppressFn }), + ).rejects.toBeDefined(); + expect(globalThis.errorSpy).toHaveBeenCalled(); + }); +}); diff --git a/spring-boot-admin-server-ui/src/main/frontend/utils/axios.ts b/spring-boot-admin-server-ui/src/main/frontend/utils/axios.ts index e78809aae09..919bfb92740 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/utils/axios.ts +++ b/spring-boot-admin-server-ui/src/main/frontend/utils/axios.ts @@ -42,21 +42,34 @@ axios.interceptors.response.use((response) => response, redirectOn401()); export default axios; -export const registerErrorToastInterceptor = (axios) => { - if (sbaConfig.uiSettings.enableToasts === true) { +export const registerErrorToastInterceptor = ( + axios, + notificationCenter = nc, +) => { + if (sbaConfig.uiSettings.enableToasts) { axios.interceptors.response.use( (response) => response, (error) => { - const data = error.request; - const message = ` - Request failed: ${data.statusText}
- ${data.responseURL} + const suppress = error.config?.suppressToast; + let shouldSuppress = false; + if (typeof suppress === 'function') { + shouldSuppress = suppress(error); + } else { + shouldSuppress = !!suppress; + } + if (!shouldSuppress) { + const data = error.response; + const message = ` + Request failed: ${data?.statusText}
+ ${data?.config?.url || data?.request?.responseURL || ''} `; - nc.error(message, { - context: data.status ?? 'axios', - title: `Error ${data.status}`, - duration: 10000, - }); + notificationCenter.error(message, { + context: data?.status ?? 'axios', + title: `Error ${data?.status}`, + duration: 10000, + }); + } + return Promise.reject(error); }, ); } diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-cache.spec.ts b/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-cache.spec.ts index 43c64902cc9..90252d3b9f7 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-cache.spec.ts +++ b/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-cache.spec.ts @@ -48,7 +48,7 @@ describe('DetailsCache', () => { const instance = application.instances[0]; return render(DetailsCache, { global: { - stubs, + stubs: { cacheChart: stubChart, ...stubs }, }, props: { instance, diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-cache.vue b/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-cache.vue index c069a3cc143..6bdfa9444de 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-cache.vue +++ b/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-cache.vue @@ -166,10 +166,14 @@ export default { async fetchCacheHits() { if (this.shouldFetchCacheHits) { try { - const response = await this.instance.fetchMetric('cache.gets', { - name: this.cacheName, - result: 'hit', - }); + const response = await this.instance.fetchMetric( + 'cache.gets', + { + name: this.cacheName, + result: 'hit', + }, + { suppressToast: true }, + ); return response.data.measurements[0].value; } catch (error) { this.shouldFetchCacheHits = false; @@ -184,10 +188,14 @@ export default { async fetchCacheMisses() { if (this.shouldFetchCacheMisses) { try { - const response = await this.instance.fetchMetric('cache.gets', { - name: this.cacheName, - result: 'miss', - }); + const response = await this.instance.fetchMetric( + 'cache.gets', + { + name: this.cacheName, + result: 'miss', + }, + { suppressToast: true }, + ); return response.data.measurements[0].value; } catch (error) { this.shouldFetchCacheMisses = false; @@ -202,9 +210,13 @@ export default { async fetchCacheSize() { if (this.shouldFetchCacheSize) { try { - const response = await this.instance.fetchMetric('cache.size', { - name: this.cacheName, - }); + const response = await this.instance.fetchMetric( + 'cache.size', + { + name: this.cacheName, + }, + { suppressToast: true }, + ); return response.data.measurements[0].value; } catch (error) { this.shouldFetchCacheSize = false; From 51ea5a275d8a033d3351a1cd0754243915603675 Mon Sep 17 00:00:00 2001 From: ulrichschulte Date: Wed, 11 Feb 2026 08:52:43 +0100 Subject: [PATCH 2/4] #5003: Suppress toast on cache.size not found --- .../main/frontend/views/instances/details/details-cache.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-cache.vue b/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-cache.vue index 6bdfa9444de..4d23ab7f908 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-cache.vue +++ b/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-cache.vue @@ -210,12 +210,13 @@ export default { async fetchCacheSize() { if (this.shouldFetchCacheSize) { try { + const suppressFn = (err) => err.response?.status === 404; const response = await this.instance.fetchMetric( 'cache.size', { name: this.cacheName, }, - { suppressToast: true }, + { suppressToast: suppressFn }, ); return response.data.measurements[0].value; } catch (error) { From b2efcdb43b4bf0107b37acc9dc706111c83b1eb9 Mon Sep 17 00:00:00 2001 From: ulrichschulte Date: Wed, 11 Feb 2026 15:10:44 +0100 Subject: [PATCH 3/4] #5003: Suppress toast only on cache.size not found --- .../views/instances/details/details-cache.vue | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-cache.vue b/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-cache.vue index 4d23ab7f908..377e21a4a9e 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-cache.vue +++ b/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-cache.vue @@ -166,14 +166,10 @@ export default { async fetchCacheHits() { if (this.shouldFetchCacheHits) { try { - const response = await this.instance.fetchMetric( - 'cache.gets', - { - name: this.cacheName, - result: 'hit', - }, - { suppressToast: true }, - ); + const response = await this.instance.fetchMetric('cache.gets', { + name: this.cacheName, + result: 'hit', + }); return response.data.measurements[0].value; } catch (error) { this.shouldFetchCacheHits = false; @@ -188,14 +184,10 @@ export default { async fetchCacheMisses() { if (this.shouldFetchCacheMisses) { try { - const response = await this.instance.fetchMetric( - 'cache.gets', - { - name: this.cacheName, - result: 'miss', - }, - { suppressToast: true }, - ); + const response = await this.instance.fetchMetric('cache.gets', { + name: this.cacheName, + result: 'miss', + }); return response.data.measurements[0].value; } catch (error) { this.shouldFetchCacheMisses = false; From aca0657fd061fa3f09102e78aead55967c452d55 Mon Sep 17 00:00:00 2001 From: ulrichschulte Date: Sun, 15 Feb 2026 22:25:48 +0100 Subject: [PATCH 4/4] Fixes #5003: address changes from default branch to tests --- .../main/frontend/services/instance.spec.ts | 30 ++++--------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/spring-boot-admin-server-ui/src/main/frontend/services/instance.spec.ts b/spring-boot-admin-server-ui/src/main/frontend/services/instance.spec.ts index b3547d52e19..c11d903b86d 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/services/instance.spec.ts +++ b/spring-boot-admin-server-ui/src/main/frontend/services/instance.spec.ts @@ -43,12 +43,12 @@ describe('Instance', () => { ); describe('fetchMetric', () => { + const instance = new Instance({ + id: 'test-id', + registration: { name: 'test' }, + availableMetrics: ['test.metric', 'cache.size', 'cache.gets'], + }); test('should pass suppressToast option to axios config', async () => { - const instance = new Instance({ - id: 'test-id', - registration: { name: 'test' }, - }); - // Spy on axios.get const axiosGetSpy = vi.spyOn(instance.axios, 'get'); @@ -77,11 +77,6 @@ describe('Instance', () => { }); test('should work without options parameter for backward compatibility', async () => { - const instance = new Instance({ - id: 'test-id', - registration: { name: 'test' }, - }); - const axiosGetSpy = vi.spyOn(instance.axios, 'get'); axiosGetSpy.mockResolvedValue({ @@ -102,11 +97,6 @@ describe('Instance', () => { }); test('should pass suppressToast=false when explicitly set to false', async () => { - const instance = new Instance({ - id: 'test-id', - registration: { name: 'test' }, - }); - const axiosGetSpy = vi.spyOn(instance.axios, 'get'); axiosGetSpy.mockResolvedValue({ @@ -132,11 +122,6 @@ describe('Instance', () => { }); test('should include tags in request parameters', async () => { - const instance = new Instance({ - id: 'test-id', - registration: { name: 'test' }, - }); - const axiosGetSpy = vi.spyOn(instance.axios, 'get'); axiosGetSpy.mockResolvedValue({ @@ -159,11 +144,6 @@ describe('Instance', () => { }); test('should pass suppressToast function to axios config', async () => { - const instance = new Instance({ - id: 'test-id', - registration: { name: 'test' }, - }); - const axiosGetSpy = vi.spyOn(instance.axios, 'get'); axiosGetSpy.mockResolvedValue({