From 3ca66be38082e7a3866d53d07328be58e913067f Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Tue, 4 Jul 2023 09:42:58 +0200 Subject: [PATCH] fix(editor): Show retry information in execution list only when it exists (#6587) * fix(editor): Show retry information in execution list only when it exists * build: Fix checking for test files --- .github/scripts/check-tests.mjs | 20 ++- .../src/components/ExecutionsList.vue | 4 +- .../__tests__/ExecutionsList.test.ts | 149 ++++++++++-------- packages/workflow/src/Interfaces.ts | 4 +- 4 files changed, 105 insertions(+), 72 deletions(-) diff --git a/.github/scripts/check-tests.mjs b/.github/scripts/check-tests.mjs index b1b859c869..1694fad35d 100644 --- a/.github/scripts/check-tests.mjs +++ b/.github/scripts/check-tests.mjs @@ -17,6 +17,18 @@ const filterAsync = async (asyncPredicate, arr) => { return filterResults.filter(({shouldKeep}) => shouldKeep).map(({item}) => item); } +const isAbstractClass = (node) => { + if (ts.isClassDeclaration(node)) { + return node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AbstractKeyword) || false; + } + return false; +} + +const isAbstractMethod = (node) => { + return ts.isMethodDeclaration(node) && Boolean(node.modifiers?.find((modifier) => modifier.kind === ts.SyntaxKind.AbstractKeyword)); +} + + // Function to check if a file has a function declaration, function expression, object method or class const hasFunctionOrClass = async filePath => { const fileContent = await readFileAsync(filePath, 'utf-8'); @@ -24,7 +36,13 @@ const hasFunctionOrClass = async filePath => { let hasFunctionOrClass = false; const visit = node => { - if (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isMethodDeclaration(node) || ts.isClassDeclaration(node)) { + if ( + ts.isFunctionDeclaration(node) + || ts.isFunctionExpression(node) + || ts.isArrowFunction(node) + || (ts.isMethodDeclaration(node) && !isAbstractMethod(node)) + || (ts.isClassDeclaration(node) && !isAbstractClass(node)) + ) { hasFunctionOrClass = true; } node.forEachChild(visit); diff --git a/packages/editor-ui/src/components/ExecutionsList.vue b/packages/editor-ui/src/components/ExecutionsList.vue index e9c8e99292..3045185a4c 100644 --- a/packages/editor-ui/src/components/ExecutionsList.vue +++ b/packages/editor-ui/src/components/ExecutionsList.vue @@ -126,13 +126,13 @@ #{{ execution.id }} - +
({{ $locale.baseText('executionsList.retryOf') }} #{{ execution.retryOf }})
- +
({{ $locale.baseText('executionsList.successRetry') }} #{{ diff --git a/packages/editor-ui/src/components/__tests__/ExecutionsList.test.ts b/packages/editor-ui/src/components/__tests__/ExecutionsList.test.ts index dd238c617f..acafa33217 100644 --- a/packages/editor-ui/src/components/__tests__/ExecutionsList.test.ts +++ b/packages/editor-ui/src/components/__tests__/ExecutionsList.test.ts @@ -1,5 +1,5 @@ import { vi, describe, it, expect } from 'vitest'; -import Vue from 'vue'; +import { merge } from 'lodash-es'; import { PiniaVuePlugin } from 'pinia'; import { createTestingPinia } from '@pinia/testing'; import { render } from '@testing-library/vue'; @@ -7,15 +7,29 @@ import userEvent from '@testing-library/user-event'; import { faker } from '@faker-js/faker'; import { STORES } from '@/constants'; import ExecutionsList from '@/components/ExecutionsList.vue'; -import { externalHooks } from '@/mixins/externalHooks'; -import { genericHelpers } from '@/mixins/genericHelpers'; -import { executionHelpers } from '@/mixins/executionsHelpers'; import { i18nInstance } from '@/plugins/i18n'; import type { IWorkflowDb } from '@/Interface'; import type { IExecutionsSummary } from 'n8n-workflow'; -import { retry, waitAllPromises } from '@/__tests__/utils'; +import { retry, SETTINGS_STORE_DEFAULT_STATE, waitAllPromises } from '@/__tests__/utils'; import { useWorkflowsStore } from '@/stores'; +let pinia: ReturnType; + +const generateUndefinedNullOrString = () => { + switch (Math.floor(Math.random() * 4)) { + case 0: + return undefined; + case 1: + return null; + case 2: + return faker.datatype.uuid(); + case 3: + return ''; + default: + return undefined; + } +}; + const workflowDataFactory = (): IWorkflowDb => ({ createdAt: faker.date.past().toDateString(), updatedAt: faker.date.past().toDateString(), @@ -38,83 +52,71 @@ const executionDataFactory = (): IExecutionsSummary => ({ workflowName: faker.datatype.string(), status: faker.helpers.arrayElement(['failed', 'success']), nodeExecutionStatus: {}, + retryOf: generateUndefinedNullOrString(), + retrySuccessId: generateUndefinedNullOrString(), }); -const workflowsData = Array.from({ length: 10 }, workflowDataFactory); +const generateWorkflowsData = () => Array.from({ length: 10 }, workflowDataFactory); -const executionsData = Array.from({ length: 2 }, () => ({ - count: 20, - results: Array.from({ length: 10 }, executionDataFactory), - estimated: false, -})); - -const renderOptions = { - pinia: createTestingPinia({ - initialState: { - [STORES.SETTINGS]: { - settings: { - templates: { - enabled: true, - host: 'https://api.n8n.io/api/', - }, - license: { - environment: 'development', - }, - deployment: { - type: 'default', - }, - enterprise: { - advancedExecutionFilters: true, - }, - }, - }, - }, - }), - propsData: { - autoRefreshEnabled: false, - }, - i18n: i18nInstance, - stubs: ['font-awesome-icon'], - mixins: [externalHooks, genericHelpers, executionHelpers], -}; - -function TelemetryPlugin(vue: typeof Vue): void { - Object.defineProperty(vue, '$telemetry', { - get() { - return { - track: () => {}, - }; - }, - }); - Object.defineProperty(vue.prototype, '$telemetry', { - get() { - return { - track: () => {}, - }; - }, - }); -} +const generateExecutionsData = () => + Array.from({ length: 2 }, () => ({ + count: 20, + results: Array.from({ length: 10 }, executionDataFactory), + estimated: false, + })); const renderComponent = async () => { - const renderResult = render(ExecutionsList, renderOptions); + const renderResult = render( + ExecutionsList, + { + pinia, + propsData: { + autoRefreshEnabled: false, + }, + i18n: i18nInstance, + stubs: ['font-awesome-icon'], + }, + (vue) => { + vue.use(PiniaVuePlugin); + vue.prototype.$telemetry = { + track: () => {}, + }; + }, + ); await waitAllPromises(); return renderResult; }; -Vue.use(TelemetryPlugin); -Vue.use(PiniaVuePlugin); - describe('ExecutionsList.vue', () => { - const workflowsStore: ReturnType = useWorkflowsStore(); + let workflowsStore: ReturnType; + let workflowsData: IWorkflowDb[]; + let executionsData: Array<{ + count: number; + results: IExecutionsSummary[]; + estimated: boolean; + }>; + beforeEach(() => { + workflowsData = generateWorkflowsData(); + executionsData = generateExecutionsData(); + + pinia = createTestingPinia({ + initialState: { + [STORES.SETTINGS]: { + settings: merge(SETTINGS_STORE_DEFAULT_STATE.settings, { + enterprise: { + advancedExecutionFilters: true, + }, + }), + }, + }, + }); + workflowsStore = useWorkflowsStore(); + vi.spyOn(workflowsStore, 'fetchAllWorkflows').mockResolvedValue(workflowsData); vi.spyOn(workflowsStore, 'getCurrentExecutions').mockResolvedValue([]); }); - afterEach(() => { - vi.clearAllMocks(); - }); - it('should render empty list', async () => { vi.spyOn(workflowsStore, 'getPastExecutions').mockResolvedValueOnce({ count: 0, @@ -182,4 +184,17 @@ describe('ExecutionsList.vue', () => { expect(getByTestId('select-visible-executions-checkbox')).toBeInTheDocument(); expect(queryByTestId('select-all-executions-checkbox')).not.toBeInTheDocument(); }); + + it('should show "retry" data when appropriate', async () => { + vi.spyOn(workflowsStore, 'getPastExecutions').mockResolvedValue(executionsData[0]); + const retryOf = executionsData[0].results.filter((execution) => execution.retryOf); + const retrySuccessId = executionsData[0].results.filter( + (execution) => !execution.retryOf && execution.retrySuccessId, + ); + + const { queryAllByText } = await renderComponent(); + + expect(queryAllByText(/Retry of/).length).toBe(retryOf.length); + expect(queryAllByText(/Success retry/).length).toBe(retrySuccessId.length); + }); }); diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index df3216de2e..e9d4ed1f61 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -1958,8 +1958,8 @@ export interface IExecutionsSummary { id: string; finished?: boolean; mode: WorkflowExecuteMode; - retryOf?: string; - retrySuccessId?: string; + retryOf?: string | null; + retrySuccessId?: string | null; waitTill?: Date; startedAt: Date; stoppedAt?: Date;