From d490cc93f9a4356062c3e5b0cdfd99737ac9f3ea Mon Sep 17 00:00:00 2001 From: Declan Carroll Date: Mon, 15 Dec 2025 12:31:34 +0000 Subject: [PATCH] fix: Improve webhook trigger node tests (#23187) --- ...test-webhook-registrations.service.test.ts | 8 -- .../test-webhook-registrations.service.ts | 4 - packages/cli/src/webhooks/test-webhooks.ts | 5 +- .../playwright/helpers/NodeParameterHelper.ts | 10 ++- .../playwright/pages/NodeDetailsViewPage.ts | 6 ++ .../testing/playwright/services/api-helper.ts | 3 + .../playwright/services/webhook-api-helper.ts | 44 ++++++++++ .../services/workflow-api-helper.ts | 54 ++++-------- .../tests/e2e/api/webhook-external.spec.ts | 5 +- .../tests/e2e/api/webhook-isolation.spec.ts | 2 +- .../tests/e2e/{api => nodes}/webhook.spec.ts | 85 +++++++++---------- .../editor/execution/execution.spec.ts | 3 +- .../workflows/editor/execution/logs.spec.ts | 10 +-- .../e2e/workflows/editor/ndv/ndv-core.spec.ts | 14 --- .../editor/subworkflows/wait.spec.ts | 16 ++-- 15 files changed, 134 insertions(+), 135 deletions(-) create mode 100644 packages/testing/playwright/services/webhook-api-helper.ts rename packages/testing/playwright/tests/e2e/{api => nodes}/webhook.spec.ts (76%) diff --git a/packages/cli/src/webhooks/__tests__/test-webhook-registrations.service.test.ts b/packages/cli/src/webhooks/__tests__/test-webhook-registrations.service.test.ts index 75352e11fa3..586258605ce 100644 --- a/packages/cli/src/webhooks/__tests__/test-webhook-registrations.service.test.ts +++ b/packages/cli/src/webhooks/__tests__/test-webhook-registrations.service.test.ts @@ -107,14 +107,6 @@ describe('TestWebhookRegistrationsService', () => { }); }); - describe('deregisterAll()', () => { - test('should deregister all test webhook registrations', async () => { - await registrations.deregisterAll(); - - expect(cacheService.delete).toHaveBeenCalledWith(cacheKey); - }); - }); - describe('toKey()', () => { test('should convert a test webhook registration to a key', () => { const result = registrations.toKey(registration.webhook); diff --git a/packages/cli/src/webhooks/test-webhook-registrations.service.ts b/packages/cli/src/webhooks/test-webhook-registrations.service.ts index 13815f07dd2..4684e30978c 100644 --- a/packages/cli/src/webhooks/test-webhook-registrations.service.ts +++ b/packages/cli/src/webhooks/test-webhook-registrations.service.ts @@ -113,10 +113,6 @@ export class TestWebhookRegistrationsService { return val; } - async deregisterAll() { - await this.cacheService.delete(this.cacheKey); - } - toKey(webhook: Pick) { const { webhookId, httpMethod, path: webhookPath } = webhook; diff --git a/packages/cli/src/webhooks/test-webhooks.ts b/packages/cli/src/webhooks/test-webhooks.ts index d58808dfe1d..a8da8618bed 100644 --- a/packages/cli/src/webhooks/test-webhooks.ts +++ b/packages/cli/src/webhooks/test-webhooks.ts @@ -500,9 +500,10 @@ export class TestWebhooks implements IWebhookManager { if (staticData) workflow.staticData = staticData; await this.webhookService.deleteWebhook(workflow, webhook, 'internal', 'update'); - } - await this.registrations.deregisterAll(); + // Deregister only this webhook, not all webhooks from other running workflows + await this.registrations.deregister(webhook); + } } /** diff --git a/packages/testing/playwright/helpers/NodeParameterHelper.ts b/packages/testing/playwright/helpers/NodeParameterHelper.ts index 46f662ac3c3..db74efc30ad 100644 --- a/packages/testing/playwright/helpers/NodeParameterHelper.ts +++ b/packages/testing/playwright/helpers/NodeParameterHelper.ts @@ -58,6 +58,7 @@ export class NodeParameterHelper { break; case 'text': await this.ndv.setParameterInput(parameterName, value); + await this.ndv.waitForDebounce(); break; case 'switch': await this.ndv.setParameterSwitch(parameterName, value === 'true'); @@ -80,10 +81,11 @@ export class NodeParameterHelper { await this.setParameter('responseMode', config.responseMode, 'dropdown'); } - /** - * Simplified HTTP Request node parameter configuration - * @param config - Configuration object with parameter values - */ + async getWebhookPath(): Promise { + const input = this.ndv.getParameterInputField('path'); + return await input.inputValue(); + } + async httpRequest(config: { method?: string; url?: string; diff --git a/packages/testing/playwright/pages/NodeDetailsViewPage.ts b/packages/testing/playwright/pages/NodeDetailsViewPage.ts index 04edaffa1f7..843c137ad03 100644 --- a/packages/testing/playwright/pages/NodeDetailsViewPage.ts +++ b/packages/testing/playwright/pages/NodeDetailsViewPage.ts @@ -626,6 +626,12 @@ export class NodeDetailsViewPage extends BasePage { await input.fill(value); } + /** Waits for parameter input debounce (100ms) to flush. */ + async waitForDebounce(): Promise { + // eslint-disable-next-line playwright/no-wait-for-timeout + await this.page.waitForTimeout(150); + } + async clickGetBackToCanvas(): Promise { await this.clickBackToCanvasButton(); } diff --git a/packages/testing/playwright/services/api-helper.ts b/packages/testing/playwright/services/api-helper.ts index acfc574da57..f40b4d10c6d 100644 --- a/packages/testing/playwright/services/api-helper.ts +++ b/packages/testing/playwright/services/api-helper.ts @@ -15,6 +15,7 @@ import { RoleApiHelper } from './role-api-helper'; import { TagApiHelper } from './tag-api-helper'; import { UserApiHelper } from './user-api-helper'; import { VariablesApiHelper } from './variables-api-helper'; +import { WebhookApiHelper } from './webhook-api-helper'; import { WorkflowApiHelper } from './workflow-api-helper'; export interface LoginResponseData { @@ -39,6 +40,7 @@ const DB_TAGS = { export class ApiHelpers { request: APIRequestContext; workflows: WorkflowApiHelper; + webhooks: WebhookApiHelper; projects: ProjectApiHelper; credentials: CredentialApiHelper; variables: VariablesApiHelper; @@ -49,6 +51,7 @@ export class ApiHelpers { constructor(requestContext: APIRequestContext) { this.request = requestContext; this.workflows = new WorkflowApiHelper(this); + this.webhooks = new WebhookApiHelper(this); this.projects = new ProjectApiHelper(this); this.credentials = new CredentialApiHelper(this); this.variables = new VariablesApiHelper(this); diff --git a/packages/testing/playwright/services/webhook-api-helper.ts b/packages/testing/playwright/services/webhook-api-helper.ts new file mode 100644 index 00000000000..a7634e320a2 --- /dev/null +++ b/packages/testing/playwright/services/webhook-api-helper.ts @@ -0,0 +1,44 @@ +import type { APIResponse } from '@playwright/test'; + +import type { ApiHelpers } from './api-helper'; + +type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD'; + +interface TriggerOptions { + method?: HttpMethod; + headers?: Record; + data?: unknown; + /** Max retries for 404 (webhook not yet registered). Default: 3 */ + maxNotFoundRetries?: number; + /** Delay between 404 retries in ms. Default: 250 */ + notFoundRetryDelayMs?: number; +} + +/** Triggers webhooks with retry for 404s (async registration) and connection errors. */ +export class WebhookApiHelper { + constructor(private readonly api: ApiHelpers) {} + + async trigger(path: string, options?: TriggerOptions): Promise { + const maxNotFoundRetries = options?.maxNotFoundRetries ?? 3; + const notFoundRetryDelayMs = options?.notFoundRetryDelayMs ?? 250; + + let lastResponse: APIResponse | undefined; + + for (let attempt = 0; attempt <= maxNotFoundRetries; attempt++) { + lastResponse = await this.api.request.fetch(path, { + method: options?.method ?? 'GET', + headers: options?.headers, + data: options?.data, + maxRetries: 3, // Playwright retry for connection errors + }); + + if (lastResponse.status() !== 404 || attempt === maxNotFoundRetries) { + return lastResponse; + } + + await new Promise((resolve) => setTimeout(resolve, notFoundRetryDelayMs)); + } + + return lastResponse!; + } +} diff --git a/packages/testing/playwright/services/workflow-api-helper.ts b/packages/testing/playwright/services/workflow-api-helper.ts index 83d5ece6a5a..eb10ef850cf 100644 --- a/packages/testing/playwright/services/workflow-api-helper.ts +++ b/packages/testing/playwright/services/workflow-api-helper.ts @@ -19,6 +19,7 @@ type WorkflowImportResult = { createdWorkflow: IWorkflowBase; webhookPath?: string; webhookId?: string; + webhookMethod?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD'; }; export class WorkflowApiHelper { @@ -35,14 +36,7 @@ export class WorkflowApiHelper { return result.data ?? result; } - /** - * Creates a workflow in a project with optional folder placement (Uses Internal API not public API) - * @param project - Required project ID where the workflow will be created - * @param options - Optional configuration for workflow creation - * @param options.folder - Optional folder ID to place the workflow in - * @param options.name - Optional workflow name. If not provided, generates a unique name using nanoid - * @returns Object containing the name, ID, and versionId of the created workflow - */ + /** Creates a workflow in a project with optional folder placement. */ async createInProject( project: string, options?: { @@ -96,10 +90,7 @@ export class WorkflowApiHelper { } } - /** - * Make workflow unique by updating name, IDs, and webhook paths if present. - * This ensures no conflicts when importing workflows for testing. - */ + /** Makes workflow unique by updating name, IDs, and webhook paths. */ private makeWorkflowUnique( workflow: Partial, options?: { webhookPrefix?: string; idLength?: number }, @@ -121,6 +112,7 @@ export class WorkflowApiHelper { // Check if workflow has webhook nodes and process them let webhookId: string | undefined; let webhookPath: string | undefined; + let webhookMethod: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | undefined; if (workflow.nodes) { for (const node of workflow.nodes) { @@ -129,25 +121,24 @@ export class WorkflowApiHelper { webhookPath = `${webhookPrefix}-${webhookId}`; node.webhookId = webhookId; node.parameters.path = webhookPath; + // Extract HTTP method from webhook node, default to GET + webhookMethod = (node.parameters.httpMethod as typeof webhookMethod) ?? 'GET'; } } } - return { webhookId, webhookPath, workflow }; + return { webhookId, webhookPath, webhookMethod, workflow }; } - /** - * Create a workflow from an in-memory definition, making it unique for testing. - * Returns detailed information about what was created. - */ + /** Creates a workflow from definition, making it unique for testing. */ async createWorkflowFromDefinition( workflow: Partial, options?: { webhookPrefix?: string; idLength?: number; makeUnique?: boolean }, ): Promise { const { makeUnique = true, ...rest } = options ?? {}; - const { webhookPath, webhookId } = makeUnique + const { webhookPath, webhookId, webhookMethod } = makeUnique ? this.makeWorkflowUnique(workflow, rest) - : { webhookPath: undefined, webhookId: undefined }; + : { webhookPath: undefined, webhookId: undefined, webhookMethod: undefined }; const createdWorkflow = await this.createWorkflow(workflow); const workflowId: string = String(createdWorkflow.id); @@ -156,14 +147,11 @@ export class WorkflowApiHelper { createdWorkflow, webhookPath, webhookId, + webhookMethod, }; } - /** - * Import a workflow from file and make it unique for testing. - * The workflow will be created with its original active state from the JSON file. - * Returns detailed information about what was imported, including webhook info if present. - */ + /** Imports a workflow from file, making it unique for testing. */ async importWorkflowFromFile( fileName: string, options?: { webhookPrefix?: string; idLength?: number; makeUnique?: boolean }, @@ -179,7 +167,6 @@ export class WorkflowApiHelper { workflowDefinition: Partial, options?: { webhookPrefix?: string; idLength?: number; makeUnique?: boolean }, ): Promise { - // Store original active state const result = await this.createWorkflowFromDefinition(workflowDefinition, options); if (workflowDefinition.active) { @@ -188,7 +175,6 @@ export class WorkflowApiHelper { return result; } - // TODO: workflowId is being ignored async getExecutions(workflowId?: string, limit = 20): Promise { const params = new URLSearchParams(); if (workflowId) params.set('workflowId', workflowId); @@ -229,7 +215,9 @@ export class WorkflowApiHelper { if (executions.length > initialCount) { for (const execution of executions.slice(0, executions.length - initialCount)) { - if (execution.status === 'success' || execution.status === 'error') { + const isCompleted = execution.status === 'success' || execution.status === 'error'; + const isCorrectWorkflow = execution.workflowId === workflowId; + if (isCompleted && isCorrectWorkflow) { return execution; } } @@ -237,7 +225,8 @@ export class WorkflowApiHelper { for (const execution of executions) { const isCompleted = execution.status === 'success' || execution.status === 'error'; - if (isCompleted && execution.mode === 'webhook') { + const isCorrectWorkflow = execution.workflowId === workflowId; + if (isCompleted && isCorrectWorkflow && execution.mode === 'webhook') { const executionTime = new Date( execution.startedAt ?? execution.createdAt ?? Date.now(), ).getTime(); @@ -253,14 +242,7 @@ export class WorkflowApiHelper { throw new TestError(`Execution did not complete within ${timeoutMs}ms`); } - /** - * Wait for a workflow execution to reach a specific status - * @param workflowId - The workflow ID to check - * @param expectedStatus - The expected status (e.g., 'waiting', 'success', 'error') - * @param timeoutMs - Maximum time to wait in milliseconds - * @returns The execution once it reaches the expected status - * @throws TestError if execution doesn't reach the expected status within timeout - */ + /** Waits for a workflow execution to reach a specific status. */ async waitForWorkflowStatus( workflowId: string, expectedStatus: string, diff --git a/packages/testing/playwright/tests/e2e/api/webhook-external.spec.ts b/packages/testing/playwright/tests/e2e/api/webhook-external.spec.ts index bdc714fac69..6d7873a288d 100644 --- a/packages/testing/playwright/tests/e2e/api/webhook-external.spec.ts +++ b/packages/testing/playwright/tests/e2e/api/webhook-external.spec.ts @@ -10,7 +10,8 @@ test.describe('External Webhook Triggering', () => { const testPayload = { message: 'Hello from Playwright test' }; - const webhookResponse = await api.request.post(`/webhook/${webhookPath}`, { + const webhookResponse = await api.webhooks.trigger(`/webhook/${webhookPath}`, { + method: 'POST', data: testPayload, }); @@ -28,7 +29,7 @@ test.describe('External Webhook Triggering', () => { 'webhook-misconfiguration-test.json', ); - const webhookResponse = await api.request.get(`/webhook/${webhookPath}`); + const webhookResponse = await api.webhooks.trigger(`/webhook/${webhookPath}`); expect(webhookResponse.ok()).toBe(false); expect(await webhookResponse.text()).toContain('Unused Respond to Webhook node'); diff --git a/packages/testing/playwright/tests/e2e/api/webhook-isolation.spec.ts b/packages/testing/playwright/tests/e2e/api/webhook-isolation.spec.ts index ce2e0f37ba3..0a653340903 100644 --- a/packages/testing/playwright/tests/e2e/api/webhook-isolation.spec.ts +++ b/packages/testing/playwright/tests/e2e/api/webhook-isolation.spec.ts @@ -31,7 +31,7 @@ test.describe('Webhook Origin Isolation', () => { test(`Webhook responses should include the correct response headers for ${webhookPath}`, async ({ api, }) => { - const webhookResponse = await api.request.get(`/webhook/${webhookPath}`); + const webhookResponse = await api.webhooks.trigger(`/webhook/${webhookPath}`); expect(webhookResponse.ok()).toBe(true); const headers = webhookResponse.headers(); diff --git a/packages/testing/playwright/tests/e2e/api/webhook.spec.ts b/packages/testing/playwright/tests/e2e/nodes/webhook.spec.ts similarity index 76% rename from packages/testing/playwright/tests/e2e/api/webhook.spec.ts rename to packages/testing/playwright/tests/e2e/nodes/webhook.spec.ts index 27637132dee..d467c85aa35 100644 --- a/packages/testing/playwright/tests/e2e/api/webhook.spec.ts +++ b/packages/testing/playwright/tests/e2e/nodes/webhook.spec.ts @@ -7,46 +7,47 @@ import { EditFieldsNode } from '../../../pages/nodes/EditFieldsNode'; const cowBase64 = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k='; -// eslint-disable-next-line n8n-local-rules/no-skipped-tests -- Flaky in multi-main mode due to webhook registration timing issues -test.describe.skip('Webhook Trigger node', () => { - test.describe.configure({ mode: 'serial' }); +test.describe('Webhook Trigger node', () => { test.beforeEach(async ({ n8n }) => { await n8n.start.fromBlankCanvas(); }); - const HTTP_METHODS = ['GET', 'POST', 'DELETE', 'HEAD', 'PATCH', 'PUT']; - for (const httpMethod of HTTP_METHODS) { - test(`should listen for a ${httpMethod} request`, async ({ n8n }) => { - const webhookPath = nanoid(); - await n8n.canvas.addNode('Webhook'); - await n8n.ndv.setupHelper.webhook({ httpMethod, path: webhookPath }); + test('should listen for all HTTP methods (GET, POST, DELETE, HEAD, PATCH, PUT)', async ({ + n8n, + }) => { + await n8n.canvas.addNode('Webhook'); + const webhookPath = await n8n.ndv.setupHelper.getWebhookPath(); + + const methods = ['GET', 'POST', 'DELETE', 'HEAD', 'PATCH', 'PUT'] as const; + for (const method of methods) { + await n8n.ndv.setupHelper.webhook({ httpMethod: method }); await n8n.ndv.execute(); await expect(n8n.ndv.getWebhookTestEvent()).toBeVisible(); - const response = await n8n.api.request.fetch(`/webhook-test/${webhookPath}`, { - method: httpMethod, - }); - expect(response.ok()).toBe(true); - }); - } + + const response = await n8n.api.webhooks.trigger(`/webhook-test/${webhookPath}`, { method }); + expect(response.ok(), `${method} request should succeed`).toBe(true); + + // Wait for output to appear (confirms execution completed) + await expect(n8n.ndv.outputPanel.getDataContainer()).toBeVisible(); + } + }); test('should listen for a GET request and respond with Respond to Webhook node', async ({ n8n, }) => { - const webhookPath = nanoid(); - await n8n.canvas.addNode('Webhook'); await n8n.ndv.setupHelper.webhook({ httpMethod: 'GET', - path: webhookPath, responseMode: "Using 'Respond to Webhook' Node", }); + const webhookPath = await n8n.ndv.setupHelper.getWebhookPath(); await n8n.ndv.close(); await addEditFieldsNode(n8n); await n8n.canvas.addNode('Respond to Webhook', { closeNDV: true }); await n8n.canvas.clickExecuteWorkflowButton(); await expect(n8n.canvas.waitingForTriggerEvent()).toBeVisible(); - const response = await n8n.api.request.get(`/webhook-test/${webhookPath}`); + const response = await n8n.api.webhooks.trigger(`/webhook-test/${webhookPath}`); expect(response.ok()).toBe(true); const responseData = await response.json(); @@ -56,28 +57,27 @@ test.describe.skip('Webhook Trigger node', () => { test('should listen for a GET request and respond with custom status code 201', async ({ n8n, }) => { - const webhookPath = nanoid(); - await n8n.canvas.addNode('Webhook'); - await n8n.ndv.setupHelper.webhook({ httpMethod: 'GET', path: webhookPath }); + await n8n.ndv.setupHelper.webhook({ httpMethod: 'GET' }); + const webhookPath = await n8n.ndv.setupHelper.getWebhookPath(); await n8n.ndv.setOptionalParameter('Response Code', 'responseCode', '201'); + // Extra wait to ensure parameter is persisted before webhook registration + await n8n.ndv.waitForDebounce(); await n8n.ndv.execute(); await expect(n8n.ndv.getWebhookTestEvent()).toBeVisible(); - const response = await n8n.api.request.get(`/webhook-test/${webhookPath}`); + const response = await n8n.api.webhooks.trigger(`/webhook-test/${webhookPath}`); expect(response.status()).toBe(201); }); test('should listen for a GET request and respond with last node', async ({ n8n }) => { - const webhookPath = nanoid(); - await n8n.canvas.addNode('Webhook'); await n8n.ndv.setupHelper.webhook({ httpMethod: 'GET', - path: webhookPath, responseMode: 'When Last Node Finishes', }); + const webhookPath = await n8n.ndv.setupHelper.getWebhookPath(); await n8n.ndv.close(); await addEditFieldsNode(n8n); @@ -86,7 +86,7 @@ test.describe.skip('Webhook Trigger node', () => { await expect(n8n.canvas.waitingForTriggerEvent()).toBeVisible(); - const response = await n8n.api.request.get(`/webhook-test/${webhookPath}`); + const response = await n8n.api.webhooks.trigger(`/webhook-test/${webhookPath}`); expect(response.ok()).toBe(true); const responseData = await response.json(); @@ -96,14 +96,12 @@ test.describe.skip('Webhook Trigger node', () => { test('should listen for a GET request and respond with last node binary data', async ({ n8n, }) => { - const webhookPath = nanoid(); - await n8n.canvas.addNode('Webhook'); await n8n.ndv.setupHelper.webhook({ httpMethod: 'GET', - path: webhookPath, responseMode: 'When Last Node Finishes', }); + const webhookPath = await n8n.ndv.setupHelper.getWebhookPath(); await n8n.ndv.selectOptionInParameterDropdown('responseData', 'First Entry Binary'); await n8n.ndv.close(); @@ -120,7 +118,7 @@ test.describe.skip('Webhook Trigger node', () => { await expect(n8n.canvas.waitingForTriggerEvent()).toBeVisible(); - const response = await n8n.api.request.get(`/webhook-test/${webhookPath}`); + const response = await n8n.api.webhooks.trigger(`/webhook-test/${webhookPath}`); expect(response.ok()).toBe(true); const responseData = await response.json(); @@ -128,19 +126,17 @@ test.describe.skip('Webhook Trigger node', () => { }); test('should listen for a GET request and respond with an empty body', async ({ n8n }) => { - const webhookPath = nanoid(); - await n8n.canvas.addNode('Webhook'); await n8n.ndv.setupHelper.webhook({ httpMethod: 'GET', - path: webhookPath, responseMode: 'When Last Node Finishes', }); + const webhookPath = await n8n.ndv.setupHelper.getWebhookPath(); await n8n.ndv.selectOptionInParameterDropdown('responseData', 'No Response Body'); await n8n.ndv.execute(); await expect(n8n.ndv.getWebhookTestEvent()).toBeVisible(); - const response = await n8n.api.request.get(`/webhook-test/${webhookPath}`); + const response = await n8n.api.webhooks.trigger(`/webhook-test/${webhookPath}`); expect(response.ok()).toBe(true); const responseData = await response.text(); @@ -148,7 +144,6 @@ test.describe.skip('Webhook Trigger node', () => { }); test('should listen for a GET request with Basic Authentication', async ({ n8n }) => { - const webhookPath = nanoid(); const credentialName = `test-${nanoid()}`; const user = `test-${nanoid()}`; const password = `test-${nanoid()}`; @@ -164,21 +159,21 @@ test.describe.skip('Webhook Trigger node', () => { await n8n.canvas.addNode('Webhook'); await n8n.ndv.setupHelper.webhook({ httpMethod: 'GET', - path: webhookPath, authentication: 'Basic Auth', }); + const webhookPath = await n8n.ndv.setupHelper.getWebhookPath(); await n8n.ndv.execute(); await expect(n8n.ndv.getWebhookTestEvent()).toBeVisible(); - const failResponse = await n8n.api.request.get(`/webhook-test/${webhookPath}`, { + const failResponse = await n8n.api.webhooks.trigger(`/webhook-test/${webhookPath}`, { headers: { Authorization: 'Basic ' + Buffer.from('wrong:wrong').toString('base64'), }, }); expect(failResponse.status()).toBe(403); - const successResponse = await n8n.api.request.get(`/webhook-test/${webhookPath}`, { + const successResponse = await n8n.api.webhooks.trigger(`/webhook-test/${webhookPath}`, { headers: { Authorization: 'Basic ' + Buffer.from(`${user}:${password}`).toString('base64'), }, @@ -186,8 +181,7 @@ test.describe.skip('Webhook Trigger node', () => { expect(successResponse.ok()).toBe(true); }); - test('should listen for a GET request with Header Authentication', async ({ n8n, api }) => { - const webhookPath = nanoid(); + test('should listen for a GET request with Header Authentication', async ({ n8n }) => { const credentialName = `test-${nanoid()}`; const name = `test-${nanoid()}`; const value = `test-${nanoid()}`; @@ -203,14 +197,14 @@ test.describe.skip('Webhook Trigger node', () => { await n8n.canvas.addNode('Webhook'); await n8n.ndv.setupHelper.webhook({ httpMethod: 'GET', - path: webhookPath, authentication: 'Header Auth', }); + const webhookPath = await n8n.ndv.setupHelper.getWebhookPath(); await n8n.ndv.execute(); await expect(n8n.ndv.getWebhookTestEvent()).toBeVisible(); - const failResponse = await api.request.get(`/webhook-test/${webhookPath}`, { + const failResponse = await n8n.api.webhooks.trigger(`/webhook-test/${webhookPath}`, { headers: { test: 'wrong', }, @@ -218,7 +212,7 @@ test.describe.skip('Webhook Trigger node', () => { expect(failResponse.status()).toBe(403); - const successResponse = await api.request.get(`/webhook-test/${webhookPath}`, { + const successResponse = await n8n.api.webhooks.trigger(`/webhook-test/${webhookPath}`, { headers: { [name]: value, }, @@ -227,13 +221,10 @@ test.describe.skip('Webhook Trigger node', () => { }); test('CAT-1253-bug-cant-run-workflow-when-unconnected-nodes-have-errors', async ({ n8n }) => { - const webhookPath = nanoid(); - // Add Webhook node await n8n.canvas.addNode('Webhook'); await n8n.ndv.setupHelper.webhook({ httpMethod: 'GET', - path: webhookPath, }); await n8n.ndv.close(); diff --git a/packages/testing/playwright/tests/e2e/workflows/editor/execution/execution.spec.ts b/packages/testing/playwright/tests/e2e/workflows/editor/execution/execution.spec.ts index ec94936909e..69b7acd2687 100644 --- a/packages/testing/playwright/tests/e2e/workflows/editor/execution/execution.spec.ts +++ b/packages/testing/playwright/tests/e2e/workflows/editor/execution/execution.spec.ts @@ -123,8 +123,7 @@ test.describe('Execution', () => { await expect(n8n.canvas.clearExecutionDataButton()).toBeHidden(); }); - // eslint-disable-next-line n8n-local-rules/no-skipped-tests -- Flaky in multi-main mode due to webhook registration timing issues - test.skip('should test webhook workflow', async ({ n8n }) => { + test('should test webhook workflow', async ({ n8n }) => { await n8n.start.fromImportedWorkflow('Webhook_wait_set.json'); await expect(n8n.canvas.getExecuteWorkflowButton()).toBeVisible(); diff --git a/packages/testing/playwright/tests/e2e/workflows/editor/execution/logs.spec.ts b/packages/testing/playwright/tests/e2e/workflows/editor/execution/logs.spec.ts index 9a0ff4eff0a..887b5e4f7f2 100644 --- a/packages/testing/playwright/tests/e2e/workflows/editor/execution/logs.spec.ts +++ b/packages/testing/playwright/tests/e2e/workflows/editor/execution/logs.spec.ts @@ -231,10 +231,7 @@ test.describe('Logs', () => { await expect(n8n.executions.logsPanel.getLogEntries().nth(2)).toContainText('E2E Chat Model'); }); - // eslint-disable-next-line n8n-local-rules/no-skipped-tests -- Flaky in multi-main mode due to webhook registration timing issues - test.skip('should show logs for a workflow with a node that waits for webhook', async ({ - n8n, - }) => { + test('should show logs for a workflow with a node that waits for webhook', async ({ n8n }) => { await n8n.start.fromImportedWorkflow('Workflow_wait_for_webhook.json'); await n8n.canvas.deselectAll(); await n8n.canvas.logsPanel.open(); @@ -270,10 +267,7 @@ test.describe('Logs', () => { await expect(n8n.canvas.logsPanel.getLogEntries().nth(1)).toContainText('Success'); }); - // eslint-disable-next-line n8n-local-rules/no-skipped-tests -- Flaky in multi-main mode due to webhook registration timing issues - test.skip('should allow to cancel a workflow with a node that waits for webhook', async ({ - n8n, - }) => { + test('should allow to cancel a workflow with a node that waits for webhook', async ({ n8n }) => { await n8n.start.fromImportedWorkflow('Workflow_wait_for_webhook.json'); await n8n.canvas.deselectAll(); await n8n.canvas.logsPanel.open(); diff --git a/packages/testing/playwright/tests/e2e/workflows/editor/ndv/ndv-core.spec.ts b/packages/testing/playwright/tests/e2e/workflows/editor/ndv/ndv-core.spec.ts index 2c0e3a8f6b6..dad9c42cbc6 100644 --- a/packages/testing/playwright/tests/e2e/workflows/editor/ndv/ndv-core.spec.ts +++ b/packages/testing/playwright/tests/e2e/workflows/editor/ndv/ndv-core.spec.ts @@ -31,20 +31,6 @@ test.describe('NDV', () => { await expect(n8n.ndv.inputPanel.get()).toContainText('No input connected'); }); - test('should test webhook node', async ({ n8n }) => { - await n8n.canvas.addNode('Webhook', { closeNDV: false }); - - await n8n.ndv.execute(); - - const webhookUrl = await n8n.ndv.getWebhookUrl(); - await expect(n8n.ndv.getWebhookTriggerListening()).toBeVisible(); - const response = await n8n.ndv.makeWebhookRequest(webhookUrl as string); - expect(response.status()).toBe(200); - - await expect(n8n.ndv.outputPanel.get()).toBeVisible(); - await expect(n8n.ndv.outputPanel.getDataContainer()).toBeVisible(); - }); - test('should change input and go back to canvas', async ({ n8n }) => { await n8n.start.fromImportedWorkflow('NDV-test-select-input.json'); await n8n.canvas.clickZoomToFitButton(); diff --git a/packages/testing/playwright/tests/e2e/workflows/editor/subworkflows/wait.spec.ts b/packages/testing/playwright/tests/e2e/workflows/editor/subworkflows/wait.spec.ts index 37961abf67b..65293e15496 100644 --- a/packages/testing/playwright/tests/e2e/workflows/editor/subworkflows/wait.spec.ts +++ b/packages/testing/playwright/tests/e2e/workflows/editor/subworkflows/wait.spec.ts @@ -24,7 +24,7 @@ test.describe('Parent that does not wait for sub-workflow', () => { const { webhookPath, workflowId } = await api.workflows.importWorkflowFromDefinition(workflowDefinition); - const response = await api.request.get(`/webhook/${webhookPath}`); + const response = await api.webhooks.trigger(`/webhook/${webhookPath}`); expect(response.ok()).toBe(true); const execution = await api.workflows.waitForExecution(workflowId, 5000); expect(execution.status).toBe('success'); @@ -57,7 +57,7 @@ test.describe('Parent that does not wait for sub-workflow', () => { const { webhookPath, workflowId } = await api.workflows.importWorkflowFromDefinition(workflowDefinition); - const response = await api.request.get(`/webhook/${webhookPath}`); + const response = await api.webhooks.trigger(`/webhook/${webhookPath}`); expect(response.ok()).toBe(true); // First, wait for the child to finish. @@ -105,7 +105,7 @@ test.describe('CAT-1801: Parent receives correct data from child with wait node' await api.workflows.activate(parentWorkflowId, versionId!); // Trigger parent workflow via webhook - const webhookResponse = await api.request.get(`/webhook/${webhookPath}`); + const webhookResponse = await api.webhooks.trigger(`/webhook/${webhookPath}`); expect(webhookResponse.ok()).toBe(true); // Wait for child execution to appear and enter waiting state @@ -120,7 +120,9 @@ test.describe('CAT-1801: Parent receives correct data from child with wait node' ); // Trigger the wait webhook to resume child using child execution ID - const waitWebhookResponse = await api.request.get(`/webhook-waiting/${childExecution!.id}`); + const waitWebhookResponse = await api.webhooks.trigger( + `/webhook-waiting/${childExecution!.id}`, + ); expect(waitWebhookResponse.ok()).toBe(true); // Wait for parent to complete @@ -169,7 +171,7 @@ test.describe('CAT-1929: Parent should not resume until child with multiple wait await api.workflows.activate(parentWorkflowId, versionId!); // Trigger parent workflow via webhook - const webhookResponse = await api.request.get(`/webhook/${webhookPath}`); + const webhookResponse = await api.webhooks.trigger(`/webhook/${webhookPath}`); expect(webhookResponse.ok()).toBe(true); // Wait for child execution to appear and enter waiting state (first wait node) @@ -179,7 +181,7 @@ test.describe('CAT-1929: Parent should not resume until child with multiple wait await api.workflows.waitForWorkflowStatus(parentWorkflowId, 'waiting'); // Resume first wait node - const firstWaitResponse = await api.request.get(`/webhook-waiting/${childExecution.id}`); + const firstWaitResponse = await api.webhooks.trigger(`/webhook-waiting/${childExecution.id}`); expect(firstWaitResponse.ok()).toBe(true); // Wait for child to reach the second wait node @@ -193,7 +195,7 @@ test.describe('CAT-1929: Parent should not resume until child with multiple wait expect(parentExecAfterFirstWait.status).toBe('waiting'); // Resume second wait node - const secondWaitResponse = await api.request.get(`/webhook-waiting/${childExecution.id}`); + const secondWaitResponse = await api.webhooks.trigger(`/webhook-waiting/${childExecution.id}`); expect(secondWaitResponse.ok()).toBe(true); // Now parent should complete