diff --git a/packages/frontend/editor-ui/src/components/WorkflowSettings.test.ts b/packages/frontend/editor-ui/src/components/WorkflowSettings.test.ts index ae0e27131f..41a63400b4 100644 --- a/packages/frontend/editor-ui/src/components/WorkflowSettings.test.ts +++ b/packages/frontend/editor-ui/src/components/WorkflowSettings.test.ts @@ -1,55 +1,61 @@ -import { createPinia, setActivePinia } from 'pinia'; -import WorkflowSettingsVue from '@/components/WorkflowSettings.vue'; - -import { setupServer } from '@/__tests__/server'; +import { nextTick, reactive } from 'vue'; +import { createTestingPinia } from '@pinia/testing'; import type { MockInstance } from 'vitest'; -import { afterAll, beforeAll } from 'vitest'; -import { within } from '@testing-library/vue'; +import { within, waitFor } from '@testing-library/vue'; import userEvent from '@testing-library/user-event'; - +import type { FrontendSettings } from '@n8n/api-types'; +import { createComponentRenderer } from '@/__tests__/render'; +import { getDropdownItems, mockedStore, type MockedStore } from '@/__tests__/utils'; +import { EnterpriseEditionFeature } from '@/constants'; +import WorkflowSettingsVue from '@/components/WorkflowSettings.vue'; import { useWorkflowsStore } from '@/stores/workflows.store'; import { useSettingsStore } from '@/stores/settings.store'; -import { useUIStore } from '@/stores/ui.store'; +import { useSourceControlStore } from '@/stores/sourceControl.store'; -import { createComponentRenderer } from '@/__tests__/render'; -import { cleanupAppModals, createAppModals, getDropdownItems } from '@/__tests__/utils'; -import { EnterpriseEditionFeature, WORKFLOW_SETTINGS_MODAL_KEY } from '@/constants'; +vi.mock('vue-router', async () => ({ + useRouter: vi.fn(), + useRoute: () => + reactive({ + params: { + name: '1', + }, + }), + RouterLink: { + template: '', + }, +})); -import { nextTick } from 'vue'; -import type { IWorkflowDb } from '@/Interface'; -import * as permissions from '@/permissions'; -import type { PermissionsRecord } from '@/permissions'; - -let pinia: ReturnType; -let workflowsStore: ReturnType; -let settingsStore: ReturnType; -let uiStore: ReturnType; +let workflowsStore: MockedStore; +let settingsStore: MockedStore; +let sourceControlStore: MockedStore; +let pinia: ReturnType; let fetchAllWorkflowsSpy: MockInstance<(typeof workflowsStore)['fetchAllWorkflows']>; -const createComponent = createComponentRenderer(WorkflowSettingsVue); +const createComponent = createComponentRenderer(WorkflowSettingsVue, { + global: { + stubs: { + Modal: { + template: + '
', + }, + }, + }, +}); describe('WorkflowSettingsVue', () => { - let server: ReturnType; - beforeAll(() => { - server = setupServer(); - }); - beforeEach(async () => { - pinia = createPinia(); - setActivePinia(pinia); + pinia = createTestingPinia(); + workflowsStore = mockedStore(useWorkflowsStore); + settingsStore = mockedStore(useSettingsStore); + sourceControlStore = mockedStore(useSourceControlStore); - createAppModals(); - - workflowsStore = useWorkflowsStore(); - settingsStore = useSettingsStore(); - uiStore = useUIStore(); - - await settingsStore.getSettings(); - - vi.spyOn(workflowsStore, 'workflowName', 'get').mockReturnValue('Test Workflow'); - vi.spyOn(workflowsStore, 'workflowId', 'get').mockReturnValue('1'); - fetchAllWorkflowsSpy = vi.spyOn(workflowsStore, 'fetchAllWorkflows').mockResolvedValue([ + settingsStore.settings = { + enterprise: {}, + } as FrontendSettings; + workflowsStore.workflowName = 'Test Workflow'; + workflowsStore.workflowId = '1'; + fetchAllWorkflowsSpy = workflowsStore.fetchAllWorkflows.mockResolvedValue([ { id: '1', name: 'Test Workflow', @@ -61,7 +67,7 @@ describe('WorkflowSettingsVue', () => { versionId: '123', }, ]); - vi.spyOn(workflowsStore, 'getWorkflowById').mockReturnValue({ + workflowsStore.getWorkflowById.mockImplementation(() => ({ id: '1', name: 'Test Workflow', active: true, @@ -70,24 +76,12 @@ describe('WorkflowSettingsVue', () => { createdAt: 1, updatedAt: 1, versionId: '123', - } as IWorkflowDb); - vi.spyOn(permissions, 'getResourcePermissions').mockReturnValue({ - workflow: { - update: true, - }, - } as PermissionsRecord); - - uiStore.modalsById[WORKFLOW_SETTINGS_MODAL_KEY] = { - open: true, - }; + scopes: ['workflow:update'], + })); }); afterEach(() => { - cleanupAppModals(); - }); - - afterAll(() => { - server.shutdown(); + vi.clearAllMocks(); }); it('should render correctly', async () => { @@ -220,4 +214,79 @@ describe('WorkflowSettingsVue', () => { expect(dropdownItems[0]).toHaveTextContent(optionText); }, ); + + it('should save time saved per execution correctly', async () => { + const { getByTestId, getByRole } = createComponent({ pinia }); + await nextTick(); + + const timeSavedPerExecutionInput = getByTestId('workflow-settings-time-saved-per-execution'); + + expect(timeSavedPerExecutionInput).toBeVisible(); + + await userEvent.type(timeSavedPerExecutionInput as Element, '10'); + expect(timeSavedPerExecutionInput).toHaveValue(10); + + await userEvent.click(getByRole('button', { name: 'Save' })); + expect(workflowsStore.updateWorkflow).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ settings: expect.objectContaining({ timeSavedPerExecution: 10 }) }), + ); + }); + + it('should remove time saved per execution setting', async () => { + workflowsStore.workflowSettings.timeSavedPerExecution = 10; + + const { getByTestId, getByRole } = createComponent({ pinia }); + await nextTick(); + + const timeSavedPerExecutionInput = getByTestId('workflow-settings-time-saved-per-execution'); + + expect(timeSavedPerExecutionInput).toBeVisible(); + await waitFor(() => expect(timeSavedPerExecutionInput).toHaveValue(10)); + + await userEvent.clear(timeSavedPerExecutionInput as Element); + expect(timeSavedPerExecutionInput).not.toHaveValue(); + + await userEvent.click(getByRole('button', { name: 'Save' })); + expect(workflowsStore.updateWorkflow).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + settings: expect.not.objectContaining({ timeSavedPerExecution: 10 }), + }), + ); + }); + + it('should disable save time saved per execution if env is read-only', async () => { + sourceControlStore.preferences.branchReadOnly = true; + + const { getByTestId } = createComponent({ pinia }); + await nextTick(); + + const timeSavedPerExecutionInput = getByTestId('workflow-settings-time-saved-per-execution'); + + expect(timeSavedPerExecutionInput).toBeVisible(); + expect(timeSavedPerExecutionInput).toBeDisabled(); + }); + + it('should disable save time saved per execution if user has no permission to update workflow', async () => { + workflowsStore.getWorkflowById.mockImplementation(() => ({ + id: '1', + name: 'Test Workflow', + active: true, + nodes: [], + connections: {}, + createdAt: 1, + updatedAt: 1, + versionId: '123', + scopes: ['workflow:read'], + })); + + const { getByTestId } = createComponent({ pinia }); + await nextTick(); + + const timeSavedPerExecutionInput = getByTestId('workflow-settings-time-saved-per-execution'); + + expect(timeSavedPerExecutionInput).toBeVisible(); + expect(timeSavedPerExecutionInput).toBeDisabled(); + }); }); diff --git a/packages/frontend/editor-ui/src/components/WorkflowSettings.vue b/packages/frontend/editor-ui/src/components/WorkflowSettings.vue index 447a0a2506..c08b51bf42 100644 --- a/packages/frontend/editor-ui/src/components/WorkflowSettings.vue +++ b/packages/frontend/editor-ui/src/components/WorkflowSettings.vue @@ -378,6 +378,11 @@ const toggleTimeout = () => { workflowSettings.value.executionTimeout = workflowSettings.value.executionTimeout === -1 ? 0 : -1; }; +const updateTimeSavedPerExecution = (value: string) => { + const numValue = parseInt(value, 10); + workflowSettings.value.timeSavedPerExecution = isNaN(numValue) ? undefined : numValue; +}; + onMounted(async () => { executionTimeout.value = rootStore.executionTimeout; maxExecutionTimeout.value = rootStore.maxExecutionTimeout; @@ -484,7 +489,7 @@ onMounted(async () => { {{ i18n.baseText('workflowSettings.executionOrder') + ':' }} - { :limit-popper-width="true" data-test-id="workflow-settings-execution-order" > - - - + + {{ i18n.baseText('workflowSettings.errorWorkflow') + ':' }} - + - + - { :limit-popper-width="true" data-test-id="workflow-settings-error-workflow" > - - - + +
{{ i18n.baseText('workflowSettings.callerPolicy') + ':' }} - + - + - - - - + + {{ i18n.baseText('workflowSettings.callerIds') + ':' }} - + - + - { {{ i18n.baseText('workflowSettings.timezone') + ':' }} - + - + - { :limit-popper-width="true" data-test-id="workflow-settings-timezone" > - - - + + {{ i18n.baseText('workflowSettings.saveDataErrorExecution') + ':' }} - + - + - { :limit-popper-width="true" data-test-id="workflow-settings-save-failed-executions" > - - - + + {{ i18n.baseText('workflowSettings.saveDataSuccessExecution') + ':' }} - + - + - { :limit-popper-width="true" data-test-id="workflow-settings-save-success-executions" > - - - + + {{ i18n.baseText('workflowSettings.saveManualExecutions') + ':' }} - + - + - { :limit-popper-width="true" data-test-id="workflow-settings-save-manual-executions" > - - - + + {{ i18n.baseText('workflowSettings.saveExecutionProgress') + ':' }} - + - + - { :limit-popper-width="true" data-test-id="workflow-settings-save-execution-progress" > - - - + + {{ i18n.baseText('workflowSettings.timeoutWorkflow') + ':' }} - + - +
@@ -760,25 +765,25 @@ onMounted(async () => { {{ i18n.baseText('workflowSettings.timeoutAfter') + ':' }} - + - + - - + - { @update:model-value="(value: string) => setTheTimeout('minutes', value)" > - + - { @update:model-value="(value: string) => setTheTimeout('seconds', value)" > - +
+ + + + + +
+ + {{ i18n.baseText('workflowSettings.timeSavedPerExecution.hint') }} +
+
+