diff --git a/cypress/e2e/20-workflow-executions.cy.ts b/cypress/e2e/20-workflow-executions.cy.ts index bdc7c3b711..b44b9337a7 100644 --- a/cypress/e2e/20-workflow-executions.cy.ts +++ b/cypress/e2e/20-workflow-executions.cy.ts @@ -1,18 +1,20 @@ import { WorkflowPage } from '../pages'; import { WorkflowExecutionsTab } from '../pages/workflow-executions-tab'; +import type { RouteHandler } from 'cypress/types/net-stubbing'; const workflowPage = new WorkflowPage(); const executionsTab = new WorkflowExecutionsTab(); +const executionsRefreshInterval = 4000; // Test suite for executions tab describe('Current Workflow Executions', () => { beforeEach(() => { workflowPage.actions.visit(); cy.createFixtureWorkflow('Test_workflow_4_executions_view.json', `My test workflow`); - createMockExecutions(); }); it('should render executions tab correctly', () => { + createMockExecutions(); cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions'); cy.intercept('GET', '/rest/executions-current?filter=*').as('getCurrentExecutions'); @@ -29,6 +31,45 @@ describe('Current Workflow Executions', () => { .invoke('attr', 'class') .should('match', /_active_/); }); + + it('should not redirect back to execution tab when request is not done before leaving the page', () => { + cy.intercept('GET', '/rest/executions?filter=*'); + cy.intercept('GET', '/rest/executions-current?filter=*'); + + executionsTab.actions.switchToExecutionsTab(); + executionsTab.actions.switchToEditorTab(); + cy.wait(executionsRefreshInterval); + cy.url().should('not.include', '/executions'); + executionsTab.actions.switchToExecutionsTab(); + executionsTab.actions.switchToEditorTab(); + executionsTab.actions.switchToExecutionsTab(); + executionsTab.actions.switchToEditorTab(); + executionsTab.actions.switchToExecutionsTab(); + executionsTab.actions.switchToEditorTab(); + cy.wait(executionsRefreshInterval); + cy.url().should('not.include', '/executions'); + executionsTab.actions.switchToExecutionsTab(); + cy.wait(1000); + executionsTab.actions.switchToEditorTab(); + cy.wait(executionsRefreshInterval); + cy.url().should('not.include', '/executions'); + }); + + it('should not redirect back to execution tab when slow request is not done before leaving the page', () => { + const throttleResponse: RouteHandler = (req) => { + return new Promise((resolve) => { + setTimeout(() => resolve(req.continue()), 2000); + }); + }; + + cy.intercept('GET', '/rest/executions?filter=*', throttleResponse); + cy.intercept('GET', '/rest/executions-current?filter=*', throttleResponse); + + executionsTab.actions.switchToExecutionsTab(); + executionsTab.actions.switchToEditorTab(); + cy.wait(executionsRefreshInterval); + cy.url().should('not.include', '/executions'); + }); }); const createMockExecutions = () => { diff --git a/cypress/pages/workflow-executions-tab.ts b/cypress/pages/workflow-executions-tab.ts index 70774fa135..eb855f026f 100644 --- a/cypress/pages/workflow-executions-tab.ts +++ b/cypress/pages/workflow-executions-tab.ts @@ -39,9 +39,11 @@ export class WorkflowExecutionsTab extends BasePage { }, switchToExecutionsTab: () => { this.getters.executionsTabButton().click(); + cy.url().should('include', '/executions'); }, switchToEditorTab: () => { workflowPage.getters.editorTabButton().click(); + cy.url().should('match', /\/workflow\/[^\/]+$/); }, deleteExecutionInPreview: () => { this.getters.executionPreviewDeleteButton().click(); diff --git a/packages/editor-ui/src/components/ExecutionsList.vue b/packages/editor-ui/src/components/ExecutionsList.vue index efe0852110..2357ed0f80 100644 --- a/packages/editor-ui/src/components/ExecutionsList.vue +++ b/packages/editor-ui/src/components/ExecutionsList.vue @@ -310,6 +310,7 @@ import { isEmpty } from '@/utils/typesUtils'; import { setPageTitle } from '@/utils/htmlUtils'; import { executionFilterToQueryFilter } from '@/utils/executionUtils'; import { useExternalHooks } from '@/composables/useExternalHooks'; +import { useRoute } from 'vue-router'; export default defineComponent({ name: 'ExecutionsList', @@ -328,11 +329,13 @@ export default defineComponent({ const i18n = useI18n(); const telemetry = useTelemetry(); const externalHooks = useExternalHooks(); + const route = useRoute(); return { i18n, telemetry, externalHooks, + route, ...useToast(), ...useMessage(), }; @@ -346,7 +349,7 @@ export default defineComponent({ allVisibleSelected: false, allExistingSelected: false, - autoRefresh: this.autoRefreshEnabled, + autoRefresh: false, autoRefreshTimeout: undefined as undefined | NodeJS.Timer, filter: {} as ExecutionFilterType, @@ -361,6 +364,9 @@ export default defineComponent({ workflows: [] as IWorkflowShortResponse[], }; }, + created() { + this.autoRefresh = this.autoRefreshEnabled; + }, mounted() { setPageTitle(`n8n - ${this.pageTitle}`); @@ -376,6 +382,7 @@ export default defineComponent({ }); }, beforeUnmount() { + this.autoRefresh = false; this.stopAutoRefreshInterval(); document.removeEventListener('visibilitychange', this.onDocumentVisibilityChange); }, @@ -938,7 +945,7 @@ export default defineComponent({ } }, async startAutoRefreshInterval() { - if (this.autoRefresh) { + if (this.autoRefresh && this.route.name === VIEWS.WORKFLOW_EXECUTIONS) { await this.loadAutoRefresh(); this.autoRefreshTimeout = setTimeout(() => { void this.startAutoRefreshInterval(); @@ -946,10 +953,8 @@ export default defineComponent({ } }, stopAutoRefreshInterval() { - if (this.autoRefreshTimeout) { - clearTimeout(this.autoRefreshTimeout); - this.autoRefreshTimeout = undefined; - } + clearTimeout(this.autoRefreshTimeout); + this.autoRefreshTimeout = undefined; }, onDocumentVisibilityChange() { if (document.visibilityState === 'hidden') { diff --git a/packages/editor-ui/src/components/ExecutionsView/ExecutionsList.vue b/packages/editor-ui/src/components/ExecutionsView/ExecutionsList.vue index 2b732b11c2..d553e42933 100644 --- a/packages/editor-ui/src/components/ExecutionsView/ExecutionsList.vue +++ b/packages/editor-ui/src/components/ExecutionsView/ExecutionsList.vue @@ -145,7 +145,6 @@ export default defineComponent({ }, }, async beforeRouteLeave(to, from, next) { - this.stopAutoRefreshInterval(); if (getNodeViewTab(to) === MAIN_HEADER_TABS.WORKFLOW) { next(); return; @@ -181,6 +180,9 @@ export default defineComponent({ next(); } }, + created() { + this.autoRefresh = this.uiStore.executionSidebarAutoRefresh; + }, async mounted() { this.loading = true; const workflowUpdated = this.$route.params.name !== this.workflowsStore.workflowId; @@ -198,16 +200,15 @@ export default defineComponent({ await this.setExecutions(); } } - - this.autoRefresh = this.uiStore.executionSidebarAutoRefresh; void this.startAutoRefreshInterval(); document.addEventListener('visibilitychange', this.onDocumentVisibilityChange); this.loading = false; }, beforeUnmount() { - this.stopAutoRefreshInterval(); document.removeEventListener('visibilitychange', this.onDocumentVisibilityChange); + this.autoRefresh = false; + this.stopAutoRefreshInterval(); }, methods: { async initView(loadWorkflow: boolean): Promise { @@ -292,7 +293,7 @@ export default defineComponent({ } if (this.executions.length > 0) { await this.$router - .push({ + .replace({ name: VIEWS.EXECUTION_PREVIEW, params: { name: this.currentWorkflow, executionId: nextExecution.id }, }) @@ -302,7 +303,7 @@ export default defineComponent({ } else { // If there are no executions left, show empty state and clear active execution from the store this.workflowsStore.activeWorkflowExecution = null; - await this.$router.push({ + await this.$router.replace({ name: VIEWS.EXECUTION_HOME, params: { name: this.currentWorkflow }, }); @@ -363,10 +364,8 @@ export default defineComponent({ } }, stopAutoRefreshInterval() { - if (this.autoRefreshTimeout) { - clearTimeout(this.autoRefreshTimeout); - this.autoRefreshTimeout = undefined; - } + clearTimeout(this.autoRefreshTimeout); + this.autoRefreshTimeout = undefined; }, onAutoRefreshToggle(value: boolean): void { this.autoRefresh = value; @@ -448,7 +447,7 @@ export default defineComponent({ params: { name: this.currentWorkflow, executionId: this.executions[0].id }, }) .catch(() => {}); - } else if (this.executions.length === 0) { + } else if (this.executions.length === 0 && this.$route.name === VIEWS.EXECUTION_PREVIEW) { this.$router .push({ name: VIEWS.EXECUTION_HOME, diff --git a/packages/editor-ui/src/components/__tests__/ExecutionsList.test.ts b/packages/editor-ui/src/components/__tests__/ExecutionsList.test.ts index e9e57a2197..e66b6d43b3 100644 --- a/packages/editor-ui/src/components/__tests__/ExecutionsList.test.ts +++ b/packages/editor-ui/src/components/__tests__/ExecutionsList.test.ts @@ -3,7 +3,7 @@ import { merge } from 'lodash-es'; import { createTestingPinia } from '@pinia/testing'; import userEvent from '@testing-library/user-event'; import { faker } from '@faker-js/faker'; -import { STORES } from '@/constants'; +import { STORES, VIEWS } from '@/constants'; import ExecutionsList from '@/components/ExecutionsList.vue'; import type { IWorkflowDb } from '@/Interface'; import type { IExecutionsSummary } from 'n8n-workflow'; @@ -12,6 +12,12 @@ import { useWorkflowsStore } from '@/stores/workflows.store'; import type { RenderOptions } from '@/__tests__/render'; import { createComponentRenderer } from '@/__tests__/render'; +vi.mock('vue-router', () => ({ + useRoute: vi.fn().mockReturnValue({ + name: VIEWS.WORKFLOW_EXECUTIONS, + }), +})); + let pinia: ReturnType; const generateUndefinedNullOrString = () => {