fix: Improve webhook trigger node tests (#23187)

This commit is contained in:
Declan Carroll
2025-12-15 12:31:34 +00:00
committed by GitHub
parent 8722ba38db
commit d490cc93f9
15 changed files with 134 additions and 135 deletions
@@ -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);
@@ -113,10 +113,6 @@ export class TestWebhookRegistrationsService {
return val;
}
async deregisterAll() {
await this.cacheService.delete(this.cacheKey);
}
toKey(webhook: Pick<IWebhookData, 'webhookId' | 'httpMethod' | 'path'>) {
const { webhookId, httpMethod, path: webhookPath } = webhook;
+3 -2
View File
@@ -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);
}
}
/**
@@ -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<string> {
const input = this.ndv.getParameterInputField('path');
return await input.inputValue();
}
async httpRequest(config: {
method?: string;
url?: string;
@@ -626,6 +626,12 @@ export class NodeDetailsViewPage extends BasePage {
await input.fill(value);
}
/** Waits for parameter input debounce (100ms) to flush. */
async waitForDebounce(): Promise<void> {
// eslint-disable-next-line playwright/no-wait-for-timeout
await this.page.waitForTimeout(150);
}
async clickGetBackToCanvas(): Promise<void> {
await this.clickBackToCanvasButton();
}
@@ -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);
@@ -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<string, string>;
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<APIResponse> {
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!;
}
}
@@ -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<IWorkflowBase>,
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<IWorkflowBase>,
options?: { webhookPrefix?: string; idLength?: number; makeUnique?: boolean },
): Promise<WorkflowImportResult> {
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<IWorkflowBase>,
options?: { webhookPrefix?: string; idLength?: number; makeUnique?: boolean },
): Promise<WorkflowImportResult> {
// 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<ExecutionListResponse[]> {
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,
@@ -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');
@@ -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();
@@ -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();
@@ -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();
@@ -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();
@@ -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();
@@ -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