From 21bf1c6ccdb758d41b5b8c787e78af05351bbdb5 Mon Sep 17 00:00:00 2001 From: Eduardo Medeiros Date: Thu, 26 Dec 2024 13:18:47 -0300 Subject: [PATCH 1/8] tests: add services tests --- .../api/resources/__tests__/GPT.spec.js | 105 +++++ .../api/resources/__tests__/dashboard.spec.js | 422 ++++++++++++++++++ .../api/resources/__tests__/projects.spec.js | 94 ++++ .../api/resources/__tests__/widgets.spec.js | 52 +++ 4 files changed, 673 insertions(+) create mode 100644 src/services/api/resources/__tests__/GPT.spec.js create mode 100644 src/services/api/resources/__tests__/dashboard.spec.js create mode 100644 src/services/api/resources/__tests__/projects.spec.js create mode 100644 src/services/api/resources/__tests__/widgets.spec.js diff --git a/src/services/api/resources/__tests__/GPT.spec.js b/src/services/api/resources/__tests__/GPT.spec.js new file mode 100644 index 00000000..5f4685ff --- /dev/null +++ b/src/services/api/resources/__tests__/GPT.spec.js @@ -0,0 +1,105 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { collection, addDoc } from 'firebase/firestore'; + +import AIService from '../GPT'; +import http from '@/services/api/http'; + +vi.mock('@/services/api/http', () => ({ + default: { + post: vi.fn(), + }, +})); + +vi.mock('@/store/modules/config', () => ({ + default: { + state: { + project: { uuid: 'mock-project-uuid' }, + }, + }, +})); + +vi.mock('@/utils/plugins/Firebase.js', () => ({ + db: {}, +})); + +vi.mock('firebase/firestore', () => ({ + collection: vi.fn(() => ({})), + addDoc: vi.fn(), +})); + +describe('GPT Service', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('getInsights', () => { + it('should call http.post with the correct URL and payload', async () => { + const mockResponse = { data: 'mock-data' }; + + http.post.mockResolvedValueOnce(mockResponse); + + const prompt = 'Test prompt'; + const response = await AIService.getInsights(prompt); + + expect(http.post).toHaveBeenCalledWith( + '/projects/mock-project-uuid/sources/chat_completion/search/', + { prompt }, + ); + + expect(response).toEqual(mockResponse); + }); + + it('should propagate errors from the API', async () => { + const mockError = new Error('API Error'); + http.post.mockRejectedValueOnce(mockError); + + await expect(AIService.getInsights('Test prompt')).rejects.toThrow( + 'API Error', + ); + }); + }); + + describe('createReview', () => { + it('should call addDoc with the correct data', async () => { + const mockDocRef = { id: 'mock-doc-id' }; + addDoc.mockResolvedValueOnce(mockDocRef); + + const review = { + user: 'test-user', + helpful: true, + comment: 'This is a test comment', + }; + + const response = await AIService.createReview(review); + + expect(collection).toHaveBeenCalledWith({}, 'AI Reviews'); + + expect(addDoc).toHaveBeenCalledWith(expect.anything(), { + ...review, + timestamp: expect.any(Date), + }); + + expect(response).toEqual(mockDocRef); + }); + + it('should log an error if addDoc fails', async () => { + const mockError = new Error('Firestore Error'); + addDoc.mockRejectedValueOnce(mockError); + + console.error = vi.fn(); + + const review = { + user: 'test-user', + helpful: false, + comment: 'This is a test comment', + }; + + await AIService.createReview(review); + + expect(console.error).toHaveBeenCalledWith( + 'Error writing AI Reviews document: ', + mockError, + ); + }); + }); +}); diff --git a/src/services/api/resources/__tests__/dashboard.spec.js b/src/services/api/resources/__tests__/dashboard.spec.js new file mode 100644 index 00000000..bbeb19de --- /dev/null +++ b/src/services/api/resources/__tests__/dashboard.spec.js @@ -0,0 +1,422 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import http from '@/services/api/http'; +import DashboardService from '../dashboards'; + +vi.mock('@/utils/filter', () => ({ + isFilteringDates: vi.fn(() => false), +})); + +vi.mock('@/services/api/http', () => ({ + default: { + get: vi.fn(), + post: vi.fn(), + patch: vi.fn(), + delete: vi.fn(), + }, +})); + +vi.mock('@/store/modules/config', () => ({ + default: { + state: { + project: { uuid: 'mock-project-uuid' }, + }, + }, +})); + +vi.mock('@/store/modules/dashboards', () => ({ + default: { + state: { + appliedFilters: { status: 'open', priority: 'high' }, + currentDashboardFilters: [{ name: 'status', type: 'select' }], + }, + }, +})); + +describe('DashboardService', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + describe('getAll', () => { + it('should fetch and map dashboards correctly', async () => { + http.get.mockResolvedValueOnce({ + results: [ + { + uuid: 'uuid1', + name: 'Dashboard 1', + grid: [12, 6], + is_default: true, + is_editable: true, + is_deletable: false, + config: {}, + }, + ], + }); + + const dashboards = await DashboardService.getAll(); + + expect(http.get).toHaveBeenCalledWith('/dashboards/', { + params: { project: 'mock-project-uuid' }, + }); + expect(dashboards).toHaveLength(1); + expect(dashboards[0]).toMatchObject({ + uuid: 'uuid1', + name: 'Dashboard 1', + grid: { columns: 12, rows: 6 }, + }); + }); + }); + + describe('getDashboardFilters', () => { + it('should throw an error if no UUID is provided', async () => { + await expect(DashboardService.getDashboardFilters()).rejects.toThrow( + 'Please provide a valid UUID to request dashboard filters.', + ); + }); + + it('should fetch and map filters correctly', async () => { + http.get.mockResolvedValueOnce({ + filter1: { + label: 'Filter 1', + placeholder: 'Placeholder 1', + type: 'type1', + source: 'source1', + depends_on: null, + start_sufix: 'start', + end_sufix: 'end', + field: 'field1', + }, + }); + + const filters = await DashboardService.getDashboardFilters('mock-uuid'); + + expect(http.get).toHaveBeenCalledWith('/dashboards/mock-uuid/filters/', { + params: { project: 'mock-project-uuid' }, + }); + expect(filters).toHaveLength(1); + expect(filters[0]).toMatchObject({ + name: 'filter1', + label: 'Filter 1', + }); + }); + }); + + describe('getDashboardWidgets', () => { + it('should throw an error if no UUID is provided', async () => { + await expect(DashboardService.getDashboardWidgets()).rejects.toThrow( + 'Please provide a valid UUID parameter to request widgets from this dashboard.', + ); + }); + + it('should fetch and map widgets correctly', async () => { + http.get.mockResolvedValueOnce({ + results: [ + { + uuid: 'widget-uuid', + name: 'Widget 1', + type: 'chart', + config: {}, + position: { + columns: [0, 4], + rows: [0, 2], + }, + report: {}, + source: 'source1', + is_configurable: true, + }, + ], + }); + + const widgets = await DashboardService.getDashboardWidgets('mock-uuid'); + + expect(http.get).toHaveBeenCalledWith( + '/dashboards/mock-uuid/list_widgets/', + { + params: { project: 'mock-project-uuid' }, + }, + ); + expect(widgets).toHaveLength(1); + expect(widgets[0]).toMatchObject({ + uuid: 'widget-uuid', + name: 'Widget 1', + grid_position: { + column_start: 0, + column_end: 4, + row_start: 0, + row_end: 2, + }, + }); + }); + }); + + describe('getDashboardWidgetData', () => { + it('should throw an error if no dashboardUuid or widgetUuid is provided', async () => { + await expect( + DashboardService.getDashboardWidgetData({ + dashboardUuid: null, + widgetUuid: null, + }), + ).rejects.toThrow( + 'Please provide valids UUIDs parameters to request data of widget.', + ); + }); + + it('should fetch widget data with proper query parameters', async () => { + // Mock de resposta da API + http.get.mockResolvedValueOnce({ data: 'mock-widget-data' }); + + const response = await DashboardService.getDashboardWidgetData({ + dashboardUuid: 'mock-dashboard-uuid', + widgetUuid: 'mock-widget-uuid', + params: { limit: 10 }, + }); + + expect(http.get).toHaveBeenCalledWith( + '/dashboards/mock-dashboard-uuid/widgets/mock-widget-uuid/data/', + { + params: { + project: 'mock-project-uuid', + is_live: true, // Porque `isFilteringDates` retorna `false`. + status: 'open', + priority: 'high', + limit: 10, + }, + }, + ); + + expect(response).toEqual({ data: 'mock-widget-data' }); + }); + + it('should set is_live to undefined if filtering dates', async () => { + http.get.mockResolvedValueOnce({ data: 'mock-widget-data' }); + + const response = await DashboardService.getDashboardWidgetData({ + dashboardUuid: 'mock-dashboard-uuid', + widgetUuid: 'mock-widget-uuid', + params: { limit: 10 }, + }); + + expect(http.get).toHaveBeenCalledWith( + '/dashboards/mock-dashboard-uuid/widgets/mock-widget-uuid/data/', + { + params: { + project: 'mock-project-uuid', + is_live: true, + status: 'open', + priority: 'high', + limit: 10, + }, + }, + ); + + expect(response).toEqual({ data: 'mock-widget-data' }); + }); + }); + + describe('getDashboardWidgetReport', () => { + it('should throw an error if dashboardUuid or widgetUuid is not provided', async () => { + await expect( + DashboardService.getDashboardWidgetReport({ + dashboardUuid: null, + widgetUuid: null, + }), + ).rejects.toThrow( + 'Please provide valids UUIDs parameters to request report of widget.', + ); + + await expect( + DashboardService.getDashboardWidgetReport({ + dashboardUuid: 'mock-dashboard-uuid', + widgetUuid: null, + }), + ).rejects.toThrow( + 'Please provide valids UUIDs parameters to request report of widget.', + ); + }); + + it('should call http.get with the correct URL and query parameters', async () => { + const mockResponse = { data: 'mock-widget-report-data' }; + http.get.mockResolvedValueOnce(mockResponse); + + const response = await DashboardService.getDashboardWidgetReport({ + dashboardUuid: 'mock-dashboard-uuid', + widgetUuid: 'mock-widget-uuid', + }); + + expect(http.get).toHaveBeenCalledWith( + '/dashboards/mock-dashboard-uuid/widgets/mock-widget-uuid/report/', + { params: { project: 'mock-project-uuid' } }, + ); + + expect(response).toEqual(mockResponse); + }); + + it('should propagate errors from the API', async () => { + const mockError = new Error('API Error'); + http.get.mockRejectedValueOnce(mockError); + + await expect( + DashboardService.getDashboardWidgetReport({ + dashboardUuid: 'mock-dashboard-uuid', + widgetUuid: 'mock-widget-uuid', + }), + ).rejects.toThrow('API Error'); + }); + }); + + describe('setDefaultDashboard', () => { + it('should call http.patch with the correct URL, body, and query parameters', async () => { + const mockResponse = { success: true }; + http.patch.mockResolvedValueOnce(mockResponse); + + const dashboardUuid = 'mock-dashboard-uuid'; + const isDefault = true; + + const response = await DashboardService.setDefaultDashboard({ + dashboardUuid, + isDefault, + }); + + expect(http.patch).toHaveBeenCalledWith( + `/dashboards/${dashboardUuid}/is_default/`, + { is_default: isDefault }, + { + params: { project: 'mock-project-uuid' }, + }, + ); + expect(response).toEqual(mockResponse); + }); + + it('should propagate errors from the API', async () => { + const mockError = new Error('API Error'); + http.patch.mockRejectedValueOnce(mockError); + + const dashboardUuid = 'mock-dashboard-uuid'; + const isDefault = false; + + await expect( + DashboardService.setDefaultDashboard({ dashboardUuid, isDefault }), + ).rejects.toThrow('API Error'); + }); + }); + + describe('createFlowsDashboard', () => { + it('should call http.post with the correct URL, body, and query parameters', async () => { + const mockResponse = { success: true }; + http.post.mockResolvedValueOnce(mockResponse); + + const dashboardName = 'New Dashboard'; + const funnelAmount = 100; + const currencyType = 'USD'; + + const response = await DashboardService.createFlowsDashboard({ + dashboardName, + funnelAmount, + currencyType, + }); + + expect(http.post).toHaveBeenCalledWith( + '/dashboards/create_flows_dashboard/', + { + name: dashboardName, + funnel_amount: funnelAmount, + currency_type: currencyType, + }, + { + params: { project: 'mock-project-uuid' }, + }, + ); + expect(response).toEqual(mockResponse); + }); + + it('should propagate errors from the API', async () => { + const mockError = new Error('API Error'); + http.post.mockRejectedValueOnce(mockError); + + const dashboardName = 'New Dashboard'; + const funnelAmount = 100; + const currencyType = 'USD'; + + await expect( + DashboardService.createFlowsDashboard({ + dashboardName, + funnelAmount, + currencyType, + }), + ).rejects.toThrow('API Error'); + }); + }); + + describe('updateFlowsDashboard', () => { + it('should call http.patch with the correct URL, body, and query parameters', async () => { + const mockResponse = { success: true }; + http.patch.mockResolvedValueOnce(mockResponse); + + const dashboardUuid = 'mock-dashboard-uuid'; + const dashboardName = 'Updated Dashboard'; + const currencyType = 'USD'; + + const response = await DashboardService.updateFlowsDashboard({ + dashboardUuid, + dashboardName, + currencyType, + }); + + expect(http.patch).toHaveBeenCalledWith( + `/dashboards/${dashboardUuid}/`, + { + name: dashboardName, + config: { currency_type: currencyType }, + }, + { + params: { project: 'mock-project-uuid' }, + }, + ); + expect(response).toEqual(mockResponse); + }); + + it('should propagate errors from the API', async () => { + const mockError = new Error('API Error'); + http.patch.mockRejectedValueOnce(mockError); + + const dashboardUuid = 'mock-dashboard-uuid'; + const dashboardName = 'Updated Dashboard'; + const currencyType = 'USD'; + + await expect( + DashboardService.updateFlowsDashboard({ + dashboardUuid, + dashboardName, + currencyType, + }), + ).rejects.toThrow('API Error'); + }); + }); + + describe('deleteDashboard', () => { + it('should call http.delete with the correct URL and query parameters', async () => { + const mockResponse = { success: true }; + http.delete.mockResolvedValueOnce(mockResponse); + + const response = await DashboardService.deleteDashboard( + 'mock-dashboard-uuid', + ); + + expect(http.delete).toHaveBeenCalledWith( + '/dashboards/mock-dashboard-uuid/', + { + params: { project: 'mock-project-uuid' }, + }, + ); + expect(response).toEqual(mockResponse); + }); + + it('should propagate errors from the API', async () => { + const mockError = new Error('API Error'); + http.delete.mockRejectedValueOnce(mockError); + + await expect( + DashboardService.deleteDashboard('mock-dashboard-uuid'), + ).rejects.toThrow('API Error'); + }); + }); +}); diff --git a/src/services/api/resources/__tests__/projects.spec.js b/src/services/api/resources/__tests__/projects.spec.js new file mode 100644 index 00000000..fcd81afd --- /dev/null +++ b/src/services/api/resources/__tests__/projects.spec.js @@ -0,0 +1,94 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +import SourceService from '../projects'; +import http from '@/services/api/http'; + +vi.mock('@/services/api/http', () => ({ + default: { get: vi.fn() }, +})); + +vi.mock('@/store/modules/config', () => ({ + default: { + state: { + project: { uuid: 'mock-project-uuid' }, + }, + }, +})); + +vi.mock('@/utils/request', () => ({ + createRequestQuery: vi.fn((params) => params), +})); + +describe('Projects Service', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('getProjectSource', () => { + it('should throw an error if no slug is provided', async () => { + await expect(SourceService.getProjectSource()).rejects.toThrow( + 'Please provide a valid id to request data of source.', + ); + }); + + it('should call the API with the correct URL and query parameters', async () => { + const mockResponse = { + results: [ + { uuid: '1', name: 'Source 1', extra: 'data' }, + { uuid: '2', name: 'Source 2', extra: 'data' }, + ], + }; + http.get.mockResolvedValueOnce(mockResponse); + + const slug = 'mock-slug'; + const queryParams = { filter: 'test' }; + + const sources = await SourceService.getProjectSource(slug, queryParams); + + expect(http.get).toHaveBeenCalledWith( + '/projects/mock-project-uuid/sources/mock-slug/search/', + { params: queryParams }, + ); + + expect(sources).toEqual([ + { uuid: '1', name: 'Source 1', extra: 'data' }, + { uuid: '2', name: 'Source 2', extra: 'data' }, + ]); + }); + + it('should handle empty results correctly', async () => { + const mockResponse = { results: [] }; + http.get.mockResolvedValueOnce(mockResponse); + + const slug = 'mock-slug'; + + const sources = await SourceService.getProjectSource(slug); + + expect(sources).toEqual([]); + }); + }); + + describe('verifyProjectIndexer', () => { + it('should call the API with the correct URL', async () => { + const mockResponse = { status: 'success' }; + http.get.mockResolvedValueOnce(mockResponse); + + const response = await SourceService.verifyProjectIndexer(); + + expect(http.get).toHaveBeenCalledWith( + '/projects/mock-project-uuid/verify_project_indexer/', + ); + + expect(response).toEqual(mockResponse); + }); + + it('should propagate errors from the API', async () => { + const mockError = new Error('API Error'); + http.get.mockRejectedValueOnce(mockError); + + await expect(SourceService.verifyProjectIndexer()).rejects.toThrow( + 'API Error', + ); + }); + }); +}); diff --git a/src/services/api/resources/__tests__/widgets.spec.js b/src/services/api/resources/__tests__/widgets.spec.js new file mode 100644 index 00000000..29ed48f1 --- /dev/null +++ b/src/services/api/resources/__tests__/widgets.spec.js @@ -0,0 +1,52 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +import WidgetService from '../widgets'; +import http from '@/services/api/http'; +import { WidgetOutgoing } from '@/models'; + +vi.mock('@/services/api/http', () => ({ + default: { patch: vi.fn() }, +})); + +vi.mock('@/models', () => ({ + WidgetOutgoing: vi.fn(), +})); + +describe('updateWidget', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should throw an error if no widget is provided', async () => { + await expect(WidgetService.updateWidget({})).rejects.toThrow( + 'Please provide a valid uuid to request update widget.', + ); + }); + + it('should call the API with the correct URL and data', async () => { + const widget = { uuid: 'mock-uuid', name: 'Widget 1' }; + const mockResponse = { success: true }; + http.patch.mockResolvedValueOnce(mockResponse); + + const expectedWidgetOutgoing = new WidgetOutgoing(widget); + + const response = await WidgetService.updateWidget({ widget }); + + expect(WidgetOutgoing).toHaveBeenCalledWith(widget); + expect(http.patch).toHaveBeenCalledWith( + '/widgets/mock-uuid/', + expectedWidgetOutgoing, + ); + expect(response).toEqual(mockResponse); + }); + + it('should handle API errors correctly', async () => { + const widget = { uuid: 'mock-uuid', name: 'Widget 1' }; + const mockError = new Error('API Error'); + http.patch.mockRejectedValueOnce(mockError); + + await expect(WidgetService.updateWidget({ widget })).rejects.toThrow( + 'API Error', + ); + }); +}); From 101ab44c4b16f465c5862145eab5129eb261f194 Mon Sep 17 00:00:00 2001 From: Eduardo Medeiros Date: Thu, 26 Dec 2024 16:06:43 -0300 Subject: [PATCH 2/8] tests: add HorizontalBarChartLoading tests --- .../SkeletonHorizontalBarChart.unit.spec.js | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/components/insights/charts/loadings/__tests__/SkeletonHorizontalBarChart.unit.spec.js diff --git a/src/components/insights/charts/loadings/__tests__/SkeletonHorizontalBarChart.unit.spec.js b/src/components/insights/charts/loadings/__tests__/SkeletonHorizontalBarChart.unit.spec.js new file mode 100644 index 00000000..10028e82 --- /dev/null +++ b/src/components/insights/charts/loadings/__tests__/SkeletonHorizontalBarChart.unit.spec.js @@ -0,0 +1,49 @@ +import { describe, it, expect } from 'vitest'; + +import { mount, config } from '@vue/test-utils'; + +import { createI18n } from 'vue-i18n'; +import en from '@/locales/en.json'; +import UnnnicSystem from '@/utils/plugins/UnnnicSystem'; + +const i18n = createI18n({ + legacy: false, + locale: 'en', + messages: { en }, + fallbackWarn: false, + missingWarn: false, +}); + +config.global.plugins = [i18n, UnnnicSystem]; + +import SkeletonHorizontalBarChart from '../SkeletonHorizontalBarChart.vue'; + +describe('SkeletonHorizontalBarChart', () => { + const BAR_HEIGHT = 48; + + it('renders correctly when props are valid', () => { + const wrapper = mount(SkeletonHorizontalBarChart, { + props: { width: 300, height: 480 }, + }); + + const totalBars = Math.floor(480 / BAR_HEIGHT); + expect(wrapper.findAll('.skeleton-h-bar-container__bar')).toHaveLength( + totalBars, + ); + }); + + it('applies the correct styles and structure', () => { + const wrapper = mount(SkeletonHorizontalBarChart, { + props: { width: 300, height: 480 }, + }); + + expect(wrapper.classes()).toContain('skeleton-h-bar-container'); + expect( + wrapper.findAll('.skeleton-h-bar-container__bar').length, + ).toBeGreaterThan(0); + + expect( + wrapper.findComponent({ name: 'UnnnicSkeletonLoading' }).exists(), + ).toBe(true); + }); +}); From c1f34b251cfc17fa0a556b4d70370bdccb843864 Mon Sep 17 00:00:00 2001 From: Eduardo Medeiros Date: Thu, 26 Dec 2024 16:07:01 -0300 Subject: [PATCH 3/8] tests: Add ModalDeleteDashboard tests --- .../dashboards/ModalDeleteDashboard.vue | 7 +- .../__tests__/ModalDeleteDashboard.spec.js | 117 ++++++++++++++++++ 2 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 src/components/insights/dashboards/__tests__/ModalDeleteDashboard.spec.js diff --git a/src/components/insights/dashboards/ModalDeleteDashboard.vue b/src/components/insights/dashboards/ModalDeleteDashboard.vue index d2422768..89858590 100644 --- a/src/components/insights/dashboards/ModalDeleteDashboard.vue +++ b/src/components/insights/dashboards/ModalDeleteDashboard.vue @@ -11,16 +11,21 @@ showActionsDivider showCloseIcon size="sm" + data-testid="modal-delete-dashboard" @update:model-value="!$event ? close() : {}" @primary-button-click="deleteDashboard" > -

+

{{ $t('delete_dashboard.notice') }}

diff --git a/src/components/insights/dashboards/__tests__/ModalDeleteDashboard.spec.js b/src/components/insights/dashboards/__tests__/ModalDeleteDashboard.spec.js new file mode 100644 index 00000000..1ae48b82 --- /dev/null +++ b/src/components/insights/dashboards/__tests__/ModalDeleteDashboard.spec.js @@ -0,0 +1,117 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { mount } from '@vue/test-utils'; +import Dashboards from '@/services/api/resources/dashboards'; +import { createStore } from 'vuex'; + +import unnnic from '@weni/unnnic-system'; + +import ModalDeleteDashboard from '../ModalDeleteDashboard.vue'; + +vi.mock('@/services/api/resources/dashboards'); + +describe('ModalDeleteDashboard', () => { + let store; + let wrapper; + const mockDashboard = { + uuid: '123', + name: 'Test Dashboard', + }; + + beforeEach(() => { + store = createStore({ + modules: { + dashboards: { + namespaced: true, + state: { + dashboards: [mockDashboard], + }, + getters: { + dashboardDefault: () => mockDashboard, + }, + mutations: { + SET_DASHBOARDS: vi.fn(), + }, + }, + }, + }); + + Dashboards.deleteDashboard.mockResolvedValue(); + + wrapper = mount(ModalDeleteDashboard, { + props: { modelValue: true, dashboard: mockDashboard }, + global: { plugins: [store] }, + }); + }); + + it('renders correctly with required props', () => { + expect(wrapper.find('[data-testid="delete-notice"]').text()).toContain( + wrapper.vm.$t('delete_dashboard.notice'), + ); + expect( + wrapper.findComponent('[data-testid="modal-delete-dashboard"]').exists(), + ).toBe(true); + }); + + it('enables primary button only if dashboard name matches', async () => { + const input = wrapper.findComponent('[data-testid="input-dashboard-name"]'); + + const deleteButton = wrapper.find('[data-testid="primary-button"]'); + + expect(deleteButton.attributes('disabled')).toBeDefined(); + + await input.setValue('Test Dashboard'); + + expect(deleteButton.attributes('disabled')).toBeUndefined(); + }); + + it('calls deleteDashboard on primary button click', async () => { + const input = wrapper.findComponent('[data-testid="input-dashboard-name"]'); + const deleteButton = wrapper.find('[data-testid="primary-button"]'); + + await input.setValue('Test Dashboard'); + await deleteButton.trigger('click'); + + expect(Dashboards.deleteDashboard).toHaveBeenCalledWith(mockDashboard.uuid); + }); + + it('shows success alert and updates state on successful deletion', async () => { + const setDashboards = vi.spyOn(wrapper.vm, 'setDashboards'); + + const input = wrapper.findComponent('[data-testid="input-dashboard-name"]'); + const deleteButton = wrapper.find('[data-testid="primary-button"]'); + + await input.setValue('Test Dashboard'); + await deleteButton.trigger('click'); + + expect(setDashboards).toHaveBeenCalledWith([]); + }); + + it('shows error alert on failed deletion', async () => { + const callAlertSpy = vi.spyOn(unnnic, 'unnnicCallAlert'); + Dashboards.deleteDashboard.mockRejectedValueOnce(new Error('Failed')); + + const input = wrapper.findComponent('[data-testid="input-dashboard-name"]'); + const deleteButton = wrapper.find('[data-testid="primary-button"]'); + + await input.setValue('Test Dashboard'); + await deleteButton.trigger('click'); + + expect(Dashboards.deleteDashboard).toHaveBeenCalled(); + + expect(callAlertSpy).toHaveBeenCalledWith({ + props: { + text: wrapper.vm.$t('delete_dashboard.alert.error'), + type: 'error', + }, + seconds: 5, + }); + }); + + it('closes modal and emits close event when secondary button is clicked', async () => { + const cancelButton = wrapper.find('[data-testid="secondary-button"]'); + + await cancelButton.trigger('click'); + + expect(wrapper.emitted('close')).toBeTruthy(); + }); +}); From edf75cbeb617f8566fd97d13e9f410640f45ab66 Mon Sep 17 00:00:00 2001 From: Eduardo Medeiros Date: Thu, 26 Dec 2024 17:08:45 -0300 Subject: [PATCH 4/8] wip: remove comments --- src/services/api/resources/__tests__/dashboard.spec.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/services/api/resources/__tests__/dashboard.spec.js b/src/services/api/resources/__tests__/dashboard.spec.js index bbeb19de..32e951ab 100644 --- a/src/services/api/resources/__tests__/dashboard.spec.js +++ b/src/services/api/resources/__tests__/dashboard.spec.js @@ -161,7 +161,6 @@ describe('DashboardService', () => { }); it('should fetch widget data with proper query parameters', async () => { - // Mock de resposta da API http.get.mockResolvedValueOnce({ data: 'mock-widget-data' }); const response = await DashboardService.getDashboardWidgetData({ @@ -175,7 +174,7 @@ describe('DashboardService', () => { { params: { project: 'mock-project-uuid', - is_live: true, // Porque `isFilteringDates` retorna `false`. + is_live: true, status: 'open', priority: 'high', limit: 10, From 737a9800df2e7a0894c4f0b534183783590172ae Mon Sep 17 00:00:00 2001 From: Eduardo Medeiros Date: Thu, 26 Dec 2024 17:22:42 -0300 Subject: [PATCH 5/8] tests: add getDashboardWidgetReportData test --- .../api/resources/__tests__/dashboard.spec.js | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/services/api/resources/__tests__/dashboard.spec.js b/src/services/api/resources/__tests__/dashboard.spec.js index 32e951ab..15bd44a0 100644 --- a/src/services/api/resources/__tests__/dashboard.spec.js +++ b/src/services/api/resources/__tests__/dashboard.spec.js @@ -1,6 +1,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import http from '@/services/api/http'; import DashboardService from '../dashboards'; +import { createRequestQuery } from '@/utils/request'; vi.mock('@/utils/filter', () => ({ isFilteringDates: vi.fn(() => false), @@ -262,6 +263,51 @@ describe('DashboardService', () => { }); }); + describe('getDashboardWidgetReportData', () => { + it('should throw an error if dashboardUuid or widgetUuid is not provided', async () => { + await expect( + DashboardService.getDashboardWidgetReportData({ + dashboardUuid: null, + widgetUuid: null, + }), + ).rejects.toThrow( + 'Please provide valids UUIDs parameters to request report data of widget.', + ); + }); + + it('should call http.get with the correct URL and query parameters', () => { + const mockResponse = { data: 'mock-widget-report-data' }; + http.get.mockResolvedValueOnce(mockResponse); + + DashboardService.getDashboardWidgetReportData({ + dashboardUuid: 'dashboard-uuid', + widgetUuid: 'widget-uuid', + slug: 'slug', + offset: 0, + limit: 5, + next: null, + }); + + const params = createRequestQuery( + { status: 'open', priority: 'high' }, + { + project: 'mock-project-uuid', + is_live: true, + slug: 'slug', + offset: 0, + limit: 5, + next: null, + }, + ); + + expect(http.get).toHaveBeenCalled(); + expect(http.get).toHaveBeenCalledWith( + '/dashboards/dashboard-uuid/widgets/widget-uuid/report/data/', + { params }, + ); + }); + }); + describe('setDefaultDashboard', () => { it('should call http.patch with the correct URL, body, and query parameters', async () => { const mockResponse = { success: true }; From b89b42d3b1de83673ca74da42a6fb7a7e3e84717 Mon Sep 17 00:00:00 2001 From: Eduardo Medeiros Date: Fri, 27 Dec 2024 11:52:14 -0300 Subject: [PATCH 6/8] wip: remove api files from coverage --- vitest.config.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/vitest.config.js b/vitest.config.js index bec363ae..009b4372 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -7,7 +7,13 @@ export default mergeConfig( defineConfig({ test: { environment: 'jsdom', - exclude: [...configDefaults.exclude, 'e2e/*'], + exclude: [ + ...configDefaults.exclude, + 'e2e/*', + 'src/router/**', + 'src/services/api/http.js', + 'src/services/api/customError.js', + ], root: fileURLToPath(new URL('./', import.meta.url)), globals: true, setupFiles: './setupVitest.js', @@ -16,7 +22,13 @@ export default mergeConfig( reporter: ['text', 'json', 'html'], reportsDirectory: './coverage', include: ['src/**/*.{vue,js,ts}'], - exclude: ['src/main.js', '**/__tests__/**'], + exclude: [ + 'src/main.js', + '**/__tests__/**', + 'src/router/**', + 'src/services/api/http.js', + 'src/services/api/customError.js', + ], }, }, }), From 35d06df6d77975f5dabd5100a66c835a162c75b5 Mon Sep 17 00:00:00 2001 From: Eduardo Medeiros Date: Fri, 27 Dec 2024 11:52:29 -0300 Subject: [PATCH 7/8] tests: add more tests --- .../Layout/HeaderDashboardSettings.vue | 7 +- .../__tests__/HeaderDashboardSettings.spec.js | 79 ++++++++++++ .../drawers/DrawerConfigContentCard.vue | 2 + .../__tests__/DrawerConfigContentCard.spec.js | 92 ++++++++++++++ .../__tests__/DashboardOnboarding.spec.js | 119 ++++++++++++++++++ 5 files changed, 298 insertions(+), 1 deletion(-) create mode 100644 src/components/insights/Layout/__tests__/HeaderDashboardSettings.spec.js create mode 100644 src/components/insights/drawers/__tests__/DrawerConfigContentCard.spec.js create mode 100644 src/components/insights/onboardings/__tests__/DashboardOnboarding.spec.js diff --git a/src/components/insights/Layout/HeaderDashboardSettings.vue b/src/components/insights/Layout/HeaderDashboardSettings.vue index ef825aaf..2cc33f7c 100644 --- a/src/components/insights/Layout/HeaderDashboardSettings.vue +++ b/src/components/insights/Layout/HeaderDashboardSettings.vue @@ -5,9 +5,13 @@ type="secondary" size="large" iconCenter="tune" + data-testid="options-dashboard-button" /> - + {{ $t('edit_dashboard.title') }} @@ -15,6 +19,7 @@ v-if="showEditDashboard" v-model="showEditDashboard" :dashboard="currentDashboard" + data-testid="edit-dashboard-drawer" @close="showEditDashboard = false" /> diff --git a/src/components/insights/Layout/__tests__/HeaderDashboardSettings.spec.js b/src/components/insights/Layout/__tests__/HeaderDashboardSettings.spec.js new file mode 100644 index 00000000..fff0ca75 --- /dev/null +++ b/src/components/insights/Layout/__tests__/HeaderDashboardSettings.spec.js @@ -0,0 +1,79 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { mount } from '@vue/test-utils'; +import { createStore } from 'vuex'; +import HeaderDashboardSettings from '../HeaderDashboardSettings.vue'; +import DrawerDashboardConfig from '../../dashboards/DrawerDashboardConfig.vue'; + +describe('HeaderDashboardSettings.vue', () => { + let store; + let wrapper; + + beforeEach(() => { + store = createStore({ + modules: { + dashboards: { + namespaced: true, + state: { + currentDashboard: { + uuid: '123', + name: 'Dashboard 1', + is_editable: true, + }, + }, + }, + }, + }); + wrapper = mount(HeaderDashboardSettings, { + global: { + plugins: [store], + components: { DrawerDashboardConfig }, + }, + }); + }); + + it('renders dropdown trigger when dashboard is editable', () => { + const dropdownTrigger = wrapper.findComponent({ name: 'UnnnicButton' }); + expect(dropdownTrigger.exists()).toBe(true); + }); + + it('shows DrawerDashboardConfig when "showEditDashboard" is true', async () => { + expect( + wrapper.findComponent('[data-testid="edit-dashboard-drawer"]').exists(), + ).toBe(false); + + const optionMenuButton = wrapper.findComponent( + '[data-testid="options-dashboard-button"]', + ); + + await optionMenuButton.trigger('click'); + + const dropdownItem = wrapper.findComponent( + '[data-testid="edit-dashboard-button"]', + ); + + await dropdownItem.trigger('click'); + + expect(wrapper.vm.showEditDashboard).toBe(true); + }); + + it('closes DrawerDashboardConfig when close event is emitted', async () => { + const optionMenuButton = wrapper.findComponent( + '[data-testid="options-dashboard-button"]', + ); + + await optionMenuButton.trigger('click'); + + const dropdownItem = wrapper.findComponent( + '[data-testid="edit-dashboard-button"]', + ); + + await dropdownItem.trigger('click'); + + expect(wrapper.findComponent(DrawerDashboardConfig).exists()).toBe(true); + + const drawerConfig = wrapper.findComponent(DrawerDashboardConfig); + await drawerConfig.vm.$emit('close'); + + expect(wrapper.findComponent(DrawerDashboardConfig).exists()).toBe(false); + }); +}); diff --git a/src/components/insights/drawers/DrawerConfigContentCard.vue b/src/components/insights/drawers/DrawerConfigContentCard.vue index 4170d284..dba7af9b 100644 --- a/src/components/insights/drawers/DrawerConfigContentCard.vue +++ b/src/components/insights/drawers/DrawerConfigContentCard.vue @@ -9,6 +9,7 @@ @@ -21,6 +22,7 @@ :text="$t('drawers.reset_widget')" type="tertiary" :disabled="disableResetWidgetButton" + data-testid="reset-widget-button" @click="$emit('reset-widget')" /> diff --git a/src/components/insights/drawers/__tests__/DrawerConfigContentCard.spec.js b/src/components/insights/drawers/__tests__/DrawerConfigContentCard.spec.js new file mode 100644 index 00000000..91148574 --- /dev/null +++ b/src/components/insights/drawers/__tests__/DrawerConfigContentCard.spec.js @@ -0,0 +1,92 @@ +import { flushPromises, shallowMount } from '@vue/test-utils'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { createStore } from 'vuex'; + +import DrawerConfigContentCard from '../DrawerConfigContentCard.vue'; + +describe('DrawerConfigContentCard', () => { + let wrapper; + let mockStore; + let store; + + beforeEach(() => { + mockStore = { + actions: { 'widgets/updateCurrentWidgetEditingConfig': vi.fn() }, + }; + store = createStore({ + modules: { + widgets: { + namespaced: true, + state: { + currentWidgetEditing: { + config: { + name: 'Test Widget', + friendly_id: 'emoji-id', + }, + }, + }, + }, + }, + actions: mockStore.actions, + }); + + wrapper = shallowMount(DrawerConfigContentCard, { + props: { type: 'executions' }, + global: { + plugins: [store], + }, + }); + }); + + it('renders the correct form component based on type', async () => { + expect( + wrapper.findComponent('[data-testid="form-executions"]').exists(), + ).toBe(true); + + await wrapper.setProps({ type: 'flow_result' }); + expect( + wrapper.findComponent('[data-testid="form-flow_result"]').exists(), + ).toBe(true); + + await wrapper.setProps({ type: 'data_crossing' }); + expect( + wrapper.findComponent('[data-testid="form-data_crossing"]').exists(), + ).toBe(true); + }); + + it('emits the "reset-widget" event on button click', async () => { + const resetButton = wrapper.findComponent( + '[data-testid="reset-widget-button"]', + ); + await resetButton.trigger('click'); + + expect(wrapper.emitted('reset-widget')).toBeTruthy(); + }); + + it('updates Vuex store when config changes', async () => { + wrapper.vm.config.name = 'Updated Widget Name'; + expect( + mockStore.actions['widgets/updateCurrentWidgetEditingConfig'], + ).toHaveBeenCalled(); + }); + + it('disables reset button when widgetConfig is empty', async () => { + wrapper.vm.$store.state.widgets.currentWidgetEditing.config = {}; + + await wrapper.vm.$forceUpdate(); + + const resetButton = wrapper.findComponent( + '[data-testid="reset-widget-button"]', + ); + expect(resetButton.attributes('disabled')).toBe('true'); + }); + + it('emits update-disable-primary-button change values', async () => { + wrapper.vm.$store.state.widgets.currentWidgetEditing.config.name = + 'Initial Name'; + + await flushPromises(); + + expect(wrapper.emitted('update-disable-primary-button')).toBeTruthy(); + }); +}); diff --git a/src/components/insights/onboardings/__tests__/DashboardOnboarding.spec.js b/src/components/insights/onboardings/__tests__/DashboardOnboarding.spec.js new file mode 100644 index 00000000..3f07b3bf --- /dev/null +++ b/src/components/insights/onboardings/__tests__/DashboardOnboarding.spec.js @@ -0,0 +1,119 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { mount } from '@vue/test-utils'; +import { createStore } from 'vuex'; + +import DashboardOnboarding from '../DashboardOnboarding.vue'; + +describe('DashboardOnboarding', () => { + let wrapper; + let actionsMock; + let mutationsMock; + + beforeEach(() => { + actionsMock = { + 'onboarding/beforeOpenDashboardList': vi.fn(), + }; + + mutationsMock = { + 'dashboards/SET_SHOW_DASHBOARD_CONFIG': vi.fn(), + 'onboarding/SET_ONBOARDING_REF': vi.fn(), + 'onboarding/SET_SHOW_CREATE_DASHBOARD_ONBOARDING': vi.fn(), + }; + const store = createStore({ + modules: { + onboarding: { + namespaced: true, + state: { + onboardingRefs: { + 'select-dashboard': 'select-dashboard', + 'create-dashboard-button': null, + 'widget-card-metric': null, + 'widget-gallery': null, + 'drawer-card-metric-config': null, + 'widget-graph-empty': null, + 'drawer-graph-empty': null, + 'dashboard-onboarding-tour': { + name: 'dashboard-onboarding-tour', + start: vi.fn(), + attachedElement: 'dashboard-onboarding-tour', + }, + 'widgets-onboarding-tour': null, + }, + showCreateDashboardOnboarding: false, + showConfigWidgetOnboarding: false, + showCompleteOnboardingModal: false, + }, + }, + dashboards: { + namespaced: true, + state: { dashboards: [], currentDashboard: {} }, + }, + }, + actions: actionsMock, + mutations: mutationsMock, + dispatch: vi.fn(), + commit: vi.fn(), + }); + + wrapper = mount(DashboardOnboarding, { + global: { + plugins: [store], + stubs: { + UnnnicTour: { + template: + '
', + methods: { + start: vi.fn(), + setShowDashboardConfig: + mutationsMock['dashboards/SET_SHOW_DASHBOARD_CONFIG'], + setShowCreateDashboardOnboarding: + mutationsMock[ + 'onboarding/SET_SHOW_CREATE_DASHBOARD_ONBOARDING' + ], + }, + }, + }, + }, + }); + + vi.clearAllMocks(); + }); + + it('renders the component correctly', () => { + expect(wrapper.exists()).toBe(true); + const tour = wrapper.find('[data-testid="tour"]'); + expect(tour.exists()).toBe(true); + }); + + it('computes the dashboardTourSteps correctly', () => { + const steps = wrapper.vm.dashboardTourSteps; + expect(steps).toHaveLength(2); + + expect(steps[0].title).toBe( + wrapper.vm.$t('dashboard_onboarding.step.create_dashboard.title'), + ); + expect(steps[0].attachedElement).toBe('select-dashboard'); + }); + + it('calls setOnboardingRef on mounted', async () => { + await wrapper.vm.$nextTick(); + expect(mutationsMock['onboarding/SET_ONBOARDING_REF']).toHaveBeenCalled(); + }); + + it('calls setShowDashboardConfig when the tour ends', async () => { + const tour = wrapper.findComponent({ ref: 'dashboardOnboardingTour' }); + await tour.vm.$emit('end-tour'); + expect( + mutationsMock['dashboards/SET_SHOW_DASHBOARD_CONFIG'], + ).toHaveBeenCalled(); + }); + + it('calls setShowCreateDashboardOnboarding when the tour is closed', async () => { + const tour = wrapper.findComponent({ ref: 'dashboardOnboardingTour' }); + await tour.vm.$emit('close'); + + expect( + mutationsMock['onboarding/SET_SHOW_CREATE_DASHBOARD_ONBOARDING'], + ).toHaveBeenCalled(); + }); +}); From 0f22b3dd5c2b2a658dbd19f2a6a327bb0a879181 Mon Sep 17 00:00:00 2001 From: Eduardo Medeiros Date: Mon, 30 Dec 2024 10:15:32 -0300 Subject: [PATCH 8/8] fix: remove router files from exclude tests coverage --- vitest.config.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/vitest.config.js b/vitest.config.js index 009b4372..1a275d24 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -10,7 +10,6 @@ export default mergeConfig( exclude: [ ...configDefaults.exclude, 'e2e/*', - 'src/router/**', 'src/services/api/http.js', 'src/services/api/customError.js', ], @@ -25,7 +24,6 @@ export default mergeConfig( exclude: [ 'src/main.js', '**/__tests__/**', - 'src/router/**', 'src/services/api/http.js', 'src/services/api/customError.js', ],