mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 09:36:44 +00:00
test: Add core entry points to allow easier test setup (#18597)
This commit is contained in:
54
packages/testing/playwright/composables/TestEntryComposer.ts
Normal file
54
packages/testing/playwright/composables/TestEntryComposer.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { n8nPage } from '../pages/n8nPage';
|
||||
|
||||
/**
|
||||
* Composer for UI test entry points. All methods in this class navigate to or verify UI state.
|
||||
* For API-only testing, use the standalone `api` fixture directly instead.
|
||||
*/
|
||||
export class TestEntryComposer {
|
||||
constructor(private readonly n8n: n8nPage) {}
|
||||
|
||||
/**
|
||||
* Start UI test from the home page and navigate to canvas
|
||||
*/
|
||||
async fromHome() {
|
||||
await this.n8n.goHome();
|
||||
await this.n8n.page.waitForURL('/home/workflows');
|
||||
}
|
||||
|
||||
/**
|
||||
* Start UI test from a blank canvas (assumes already on canvas)
|
||||
*/
|
||||
async fromBlankCanvas() {
|
||||
await this.n8n.goHome();
|
||||
await this.n8n.workflows.clickAddWorkflowButton();
|
||||
// Verify we're on canvas
|
||||
await this.n8n.canvas.canvasPane().isVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start UI test from a workflow in a new project
|
||||
*/
|
||||
async fromNewProject() {
|
||||
// Enable features to allow us to create a new project
|
||||
await this.n8n.api.enableFeature('projectRole:admin');
|
||||
await this.n8n.api.enableFeature('projectRole:editor');
|
||||
await this.n8n.api.setMaxTeamProjectsQuota(-1);
|
||||
|
||||
// Create a project using the API
|
||||
const response = await this.n8n.api.projectApi.createProject();
|
||||
|
||||
const projectId = response.id;
|
||||
await this.n8n.page.goto(`workflow/new?projectId=${projectId}`);
|
||||
await this.n8n.canvas.canvasPane().isVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start UI test from the canvas of an imported workflow
|
||||
* Returns the workflow import result for use in the test
|
||||
*/
|
||||
async fromImportedWorkflow(workflowFile: string) {
|
||||
const workflowImportResult = await this.n8n.api.workflowApi.importWorkflow(workflowFile);
|
||||
await this.n8n.page.goto(`workflow/${workflowImportResult.workflowId}`);
|
||||
return workflowImportResult;
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,6 @@ export const test = base.extend<TestFixtures, WorkerFixtures>({
|
||||
const envBaseURL = process.env.N8N_BASE_URL;
|
||||
|
||||
if (envBaseURL) {
|
||||
console.log(`Using external N8N_BASE_URL: ${envBaseURL}`);
|
||||
await use(null as unknown as N8NStack);
|
||||
return;
|
||||
}
|
||||
@@ -141,8 +140,8 @@ export const test = base.extend<TestFixtures, WorkerFixtures>({
|
||||
await page.close();
|
||||
},
|
||||
|
||||
n8n: async ({ page }, use) => {
|
||||
const n8nInstance = new n8nPage(page);
|
||||
n8n: async ({ page, api }, use) => {
|
||||
const n8nInstance = new n8nPage(page, api);
|
||||
await use(n8nInstance);
|
||||
},
|
||||
|
||||
|
||||
@@ -24,13 +24,16 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@currents/playwright": "^1.15.3",
|
||||
"@n8n/api-types": "workspace:^",
|
||||
"@playwright/test": "1.54.2",
|
||||
"@types/lodash": "catalog:",
|
||||
"eslint-plugin-playwright": "2.2.2",
|
||||
"generate-schema": "2.6.0",
|
||||
"n8n": "workspace:*",
|
||||
"n8n-containers": "workspace:*",
|
||||
"n8n-core": "workspace:*",
|
||||
"n8n-workflow": "workspace:*",
|
||||
"nanoid": "catalog:",
|
||||
"tsx": "catalog:",
|
||||
"@n8n/api-types": "workspace:^"
|
||||
"tsx": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,6 @@ import type { Locator } from '@playwright/test';
|
||||
import { BasePage } from './BasePage';
|
||||
|
||||
export class WorkflowsPage extends BasePage {
|
||||
async clickNewWorkflowCard() {
|
||||
await this.clickByTestId('new-workflow-card');
|
||||
}
|
||||
|
||||
async clickAddFirstProjectButton() {
|
||||
await this.clickByTestId('add-first-project-button');
|
||||
}
|
||||
@@ -15,10 +11,20 @@ export class WorkflowsPage extends BasePage {
|
||||
await this.clickByTestId('project-plus-button');
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the add workflow button on the workflows page, visible when there are already workflows.
|
||||
*/
|
||||
async clickAddWorkflowButton() {
|
||||
await this.clickByTestId('add-resource-workflow');
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the new workflow button on the workflows page, visible when there are no workflows.
|
||||
*/
|
||||
async clickNewWorkflowCard() {
|
||||
await this.clickByTestId('new-workflow-card');
|
||||
}
|
||||
|
||||
getNewWorkflowCard() {
|
||||
return this.page.getByTestId('new-workflow-card');
|
||||
}
|
||||
|
||||
@@ -19,11 +19,14 @@ import { WorkflowSharingModal } from './WorkflowSharingModal';
|
||||
import { WorkflowsPage } from './WorkflowsPage';
|
||||
import { CanvasComposer } from '../composables/CanvasComposer';
|
||||
import { ProjectComposer } from '../composables/ProjectComposer';
|
||||
import { TestEntryComposer } from '../composables/TestEntryComposer';
|
||||
import { WorkflowComposer } from '../composables/WorkflowComposer';
|
||||
import type { ApiHelpers } from '../services/api-helper';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export class n8nPage {
|
||||
readonly page: Page;
|
||||
readonly api: ApiHelpers;
|
||||
|
||||
// Pages
|
||||
readonly aiAssistant: AIAssistantPage;
|
||||
@@ -51,9 +54,11 @@ export class n8nPage {
|
||||
readonly workflowComposer: WorkflowComposer;
|
||||
readonly projectComposer: ProjectComposer;
|
||||
readonly canvasComposer: CanvasComposer;
|
||||
readonly start: TestEntryComposer;
|
||||
|
||||
constructor(page: Page) {
|
||||
constructor(page: Page, api: ApiHelpers) {
|
||||
this.page = page;
|
||||
this.api = api;
|
||||
|
||||
// Pages
|
||||
this.aiAssistant = new AIAssistantPage(page);
|
||||
@@ -81,6 +86,7 @@ export class n8nPage {
|
||||
this.workflowComposer = new WorkflowComposer(this);
|
||||
this.projectComposer = new ProjectComposer(this);
|
||||
this.canvasComposer = new CanvasComposer(this);
|
||||
this.start = new TestEntryComposer(this);
|
||||
}
|
||||
|
||||
async goHome() {
|
||||
|
||||
@@ -27,7 +27,9 @@ export default defineConfig({
|
||||
retries: IS_CI ? 2 : 0,
|
||||
workers: WORKERS,
|
||||
timeout: 60000,
|
||||
|
||||
expect: {
|
||||
timeout: 10000,
|
||||
},
|
||||
projects: getProjects(),
|
||||
|
||||
// We use this if an n8n url is passed in. If the server is already running, we reuse it.
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
INSTANCE_ADMIN_CREDENTIALS,
|
||||
} from '../config/test-users';
|
||||
import { TestError } from '../Types';
|
||||
import { ProjectApiHelper } from './project-api-helper';
|
||||
import { WorkflowApiHelper } from './workflow-api-helper';
|
||||
|
||||
export interface LoginResponseData {
|
||||
@@ -33,10 +34,12 @@ const DB_TAGS = {
|
||||
export class ApiHelpers {
|
||||
request: APIRequestContext;
|
||||
workflowApi: WorkflowApiHelper;
|
||||
projectApi: ProjectApiHelper;
|
||||
|
||||
constructor(requestContext: APIRequestContext) {
|
||||
this.request = requestContext;
|
||||
this.workflowApi = new WorkflowApiHelper(this);
|
||||
this.projectApi = new ProjectApiHelper(this);
|
||||
}
|
||||
|
||||
// ===== MAIN SETUP METHODS =====
|
||||
|
||||
30
packages/testing/playwright/services/project-api-helper.ts
Normal file
30
packages/testing/playwright/services/project-api-helper.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import type { ApiHelpers } from './api-helper';
|
||||
import { TestError } from '../Types';
|
||||
|
||||
export class ProjectApiHelper {
|
||||
constructor(private api: ApiHelpers) {}
|
||||
|
||||
/**
|
||||
* Create a new project with a unique name
|
||||
* @param projectName Optional base name for the project. If not provided, generates a default name.
|
||||
* @returns The created project data
|
||||
*/
|
||||
async createProject(projectName?: string) {
|
||||
const uniqueName = projectName ? `${projectName} (${nanoid(8)})` : `Test Project ${nanoid(8)}`;
|
||||
|
||||
const response = await this.api.request.post('/rest/projects', {
|
||||
data: {
|
||||
name: uniqueName,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new TestError(`Failed to create project: ${await response.text()}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return result.data ?? result;
|
||||
}
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { setTimeout } from 'timers/promises';
|
||||
|
||||
import type { ApiHelpers } from './api-helper';
|
||||
import { TestError } from '../Types';
|
||||
import { resolveFromRoot } from '../utils/path-helper';
|
||||
|
||||
type WorkflowDefinition = {
|
||||
name?: string;
|
||||
active?: boolean;
|
||||
nodes: Array<{
|
||||
id?: string;
|
||||
name?: string;
|
||||
type: string;
|
||||
typeVersion?: number;
|
||||
position?: [number, number];
|
||||
webhookId?: string;
|
||||
parameters: { [key: string]: unknown } & { path?: string };
|
||||
}>;
|
||||
connections?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate and assign a unique webhook id and path to the first Webhook node in a workflow.
|
||||
*
|
||||
* - Uniqueness: Uses nanoid to ensure both the internal `webhookId` and external `parameters.path`
|
||||
* are unique per call, avoiding collisions across parallel tests and instances.
|
||||
* - Path format: `${prefix}-${nanoid}` (default prefix: `test-webhook`).
|
||||
* - Mutation: Updates the passed-in `workflow` object in-place.
|
||||
*/
|
||||
export function applyUniqueWebhookIds(
|
||||
workflow: WorkflowDefinition,
|
||||
options?: { prefix?: string; idLength?: number },
|
||||
) {
|
||||
const idLength = options?.idLength ?? 12;
|
||||
const prefix = options?.prefix ?? 'test-webhook';
|
||||
|
||||
const generatedId = nanoid(idLength);
|
||||
const generatedPath = `${prefix}-${generatedId}`;
|
||||
|
||||
for (const node of workflow.nodes) {
|
||||
if (node.type === 'n8n-nodes-base.webhook') {
|
||||
node.webhookId = generatedId;
|
||||
node.parameters.path = generatedPath;
|
||||
}
|
||||
}
|
||||
|
||||
return { webhookId: generatedId, webhookPath: generatedPath, workflow };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a webhook workflow from an in-memory definition after assigning unique webhook id/path.
|
||||
*
|
||||
* Returns the externally callable `webhookPath` (what to pass to triggerWebhook)
|
||||
* and the `workflowId` created by the API.
|
||||
*/
|
||||
export async function createWebhookWorkflow(
|
||||
api: ApiHelpers,
|
||||
workflow: WorkflowDefinition,
|
||||
options?: { prefix?: string; idLength?: number },
|
||||
) {
|
||||
const { webhookPath } = applyUniqueWebhookIds(workflow, options);
|
||||
const createdWorkflow = await api.workflowApi.createWorkflow(workflow as object);
|
||||
const workflowId = createdWorkflow.id as string;
|
||||
return { webhookPath, workflowId, createdWorkflow };
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a webhook workflow from `packages/testing/playwright/workflows/{fileName}` and create it
|
||||
* with a unique webhook id/path.
|
||||
*/
|
||||
export async function importWebhookWorkflow(
|
||||
api: ApiHelpers,
|
||||
fileName: string,
|
||||
options?: { prefix?: string; idLength?: number },
|
||||
) {
|
||||
const workflowDefinition = JSON.parse(
|
||||
readFileSync(resolveFromRoot('workflows', fileName), 'utf8'),
|
||||
) as WorkflowDefinition;
|
||||
|
||||
return await createWebhookWorkflow(api, workflowDefinition, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience: import a webhook workflow from file, ensure unique webhook id/path, and activate it.
|
||||
*
|
||||
* Returns the `webhookPath` to call and the `workflowId` for follow-up assertions.
|
||||
*/
|
||||
export async function importAndActivateWebhookWorkflow(
|
||||
api: ApiHelpers,
|
||||
fileName: string,
|
||||
options?: { prefix?: string; idLength?: number },
|
||||
) {
|
||||
const { webhookPath, workflowId, createdWorkflow } = await importWebhookWorkflow(
|
||||
api,
|
||||
fileName,
|
||||
options,
|
||||
);
|
||||
await setTimeout(500);
|
||||
await api.workflowApi.setActive(workflowId, true);
|
||||
return { webhookPath, workflowId, createdWorkflow };
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience: create a webhook workflow from an in-memory definition and activate it.
|
||||
*/
|
||||
export async function createAndActivateWebhookWorkflow(
|
||||
api: ApiHelpers,
|
||||
workflow: WorkflowDefinition,
|
||||
options?: { prefix?: string; idLength?: number },
|
||||
) {
|
||||
const { webhookPath, workflowId, createdWorkflow } = await createWebhookWorkflow(
|
||||
api,
|
||||
workflow,
|
||||
options,
|
||||
);
|
||||
// Timing issue between workflow creation and activation
|
||||
await setTimeout(500);
|
||||
await api.workflowApi.setActive(workflowId, true);
|
||||
return { webhookPath, workflowId, createdWorkflow };
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a webhook endpoint with optional data and parameters.
|
||||
*
|
||||
* @param api - The API helpers instance
|
||||
* @param path - The webhook path (without /webhook/ prefix)
|
||||
* @param options - Configuration for the webhook request
|
||||
*/
|
||||
export async function triggerWebhook(
|
||||
api: ApiHelpers,
|
||||
path: string,
|
||||
options: { method?: 'GET' | 'POST'; data?: object; params?: Record<string, string> } = {},
|
||||
) {
|
||||
const { method = 'POST', data, params } = options;
|
||||
|
||||
let url = `/webhook/${path}`;
|
||||
if (params && Object.keys(params).length > 0) {
|
||||
const searchParams = new URLSearchParams(params);
|
||||
url += `?${searchParams.toString()}`;
|
||||
}
|
||||
|
||||
const requestOptions: Record<string, unknown> = {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
};
|
||||
|
||||
if (data && method === 'POST') {
|
||||
requestOptions.data = data;
|
||||
}
|
||||
|
||||
const response =
|
||||
method === 'GET' ? await api.request.get(url) : await api.request.post(url, requestOptions);
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new TestError(`Webhook trigger failed: ${await response.text()}`);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
@@ -1,10 +1,30 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import type { IWorkflowBase, ExecutionSummary } from 'n8n-workflow';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
// Type for execution responses from the n8n API
|
||||
// Couldn't find the exact type so I put these ones together
|
||||
|
||||
interface ExecutionListResponse extends ExecutionSummary {
|
||||
data: string;
|
||||
workflowData: IWorkflowBase;
|
||||
}
|
||||
|
||||
import type { ApiHelpers } from './api-helper';
|
||||
import { TestError } from '../Types';
|
||||
import { resolveFromRoot } from '../utils/path-helper';
|
||||
|
||||
type WorkflowImportResult = {
|
||||
workflowId: string;
|
||||
createdWorkflow: IWorkflowBase;
|
||||
webhookPath?: string;
|
||||
webhookId?: string;
|
||||
};
|
||||
|
||||
export class WorkflowApiHelper {
|
||||
constructor(private api: ApiHelpers) {}
|
||||
|
||||
async createWorkflow(workflow: object) {
|
||||
async createWorkflow(workflow: IWorkflowBase) {
|
||||
const response = await this.api.request.post('/rest/workflows', { data: workflow });
|
||||
|
||||
if (!response.ok()) {
|
||||
@@ -27,7 +47,83 @@ export class WorkflowApiHelper {
|
||||
}
|
||||
}
|
||||
|
||||
async getExecutions(workflowId?: string, limit = 20) {
|
||||
/**
|
||||
* Make workflow unique by updating name, IDs, and webhook paths if present.
|
||||
* This ensures no conflicts when importing workflows for testing.
|
||||
*/
|
||||
private makeWorkflowUnique(
|
||||
workflow: IWorkflowBase,
|
||||
options?: { webhookPrefix?: string; idLength?: number },
|
||||
) {
|
||||
const idLength = options?.idLength ?? 12;
|
||||
const webhookPrefix = options?.webhookPrefix ?? 'test-webhook';
|
||||
const uniqueSuffix = nanoid(idLength);
|
||||
|
||||
// Make workflow name unique
|
||||
if (workflow.name) {
|
||||
workflow.name = `${workflow.name} (Test ${uniqueSuffix})`;
|
||||
}
|
||||
|
||||
// Check if workflow has webhook nodes and process them
|
||||
let webhookId: string | undefined;
|
||||
let webhookPath: string | undefined;
|
||||
|
||||
for (const node of workflow.nodes) {
|
||||
if (node.type === 'n8n-nodes-base.webhook') {
|
||||
webhookId = nanoid(idLength);
|
||||
webhookPath = `${webhookPrefix}-${webhookId}`;
|
||||
node.webhookId = webhookId;
|
||||
node.parameters.path = webhookPath;
|
||||
}
|
||||
}
|
||||
|
||||
return { webhookId, webhookPath, workflow };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a workflow from an in-memory definition, making it unique for testing.
|
||||
* Returns detailed information about what was created.
|
||||
*/
|
||||
async createWorkflowFromDefinition(
|
||||
workflow: IWorkflowBase,
|
||||
options?: { webhookPrefix?: string; idLength?: number },
|
||||
): Promise<WorkflowImportResult> {
|
||||
const { webhookPath, webhookId } = this.makeWorkflowUnique(workflow, options);
|
||||
const createdWorkflow = await this.createWorkflow(workflow);
|
||||
const workflowId: string = String(createdWorkflow.id);
|
||||
|
||||
return {
|
||||
workflowId,
|
||||
createdWorkflow,
|
||||
webhookPath,
|
||||
webhookId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
async importWorkflow(
|
||||
fileName: string,
|
||||
options?: { webhookPrefix?: string; idLength?: number },
|
||||
): Promise<WorkflowImportResult> {
|
||||
const workflowDefinition: IWorkflowBase = JSON.parse(
|
||||
readFileSync(resolveFromRoot('workflows', fileName), 'utf8'),
|
||||
);
|
||||
|
||||
const result = await this.createWorkflowFromDefinition(workflowDefinition, options);
|
||||
|
||||
// Ensure the workflow is in the correct active state as specified in the JSON
|
||||
if (workflowDefinition.active) {
|
||||
await this.setActive(result.workflowId, workflowDefinition.active);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async getExecutions(workflowId?: string, limit = 20): Promise<ExecutionListResponse[]> {
|
||||
const params = new URLSearchParams();
|
||||
if (workflowId) params.set('workflowId', workflowId);
|
||||
params.set('limit', limit.toString());
|
||||
@@ -47,7 +143,7 @@ export class WorkflowApiHelper {
|
||||
return [];
|
||||
}
|
||||
|
||||
async getExecution(executionId: string) {
|
||||
async getExecution(executionId: string): Promise<ExecutionListResponse> {
|
||||
const response = await this.api.request.get(`/rest/executions/${executionId}`);
|
||||
|
||||
if (!response.ok()) {
|
||||
@@ -58,7 +154,7 @@ export class WorkflowApiHelper {
|
||||
return result.data ?? result;
|
||||
}
|
||||
|
||||
async waitForExecution(workflowId: string, timeoutMs = 10000) {
|
||||
async waitForExecution(workflowId: string, timeoutMs = 10000): Promise<ExecutionListResponse> {
|
||||
const initialExecutions = await this.getExecutions(workflowId, 50);
|
||||
const initialCount = initialExecutions.length;
|
||||
const startTime = Date.now();
|
||||
@@ -77,7 +173,9 @@ export class WorkflowApiHelper {
|
||||
for (const execution of executions) {
|
||||
const isCompleted = execution.status === 'success' || execution.status === 'error';
|
||||
if (isCompleted && execution.mode === 'webhook') {
|
||||
const executionTime = new Date(execution.startedAt ?? execution.createdAt).getTime();
|
||||
const executionTime = new Date(
|
||||
execution.startedAt ?? execution.createdAt ?? Date.now(),
|
||||
).getTime();
|
||||
if (executionTime >= startTime - 5000) {
|
||||
return execution;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
|
||||
test.describe('Core UI Patterns - Building Blocks', () => {
|
||||
test.describe('Entry Point: Home Page', () => {
|
||||
test('should navigate from home', async ({ n8n }) => {
|
||||
await n8n.start.fromHome();
|
||||
expect(n8n.page.url()).toContain('/home/workflows');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Entry Point: Blank Canvas', () => {
|
||||
test('should navigate from blank canvas', async ({ n8n }) => {
|
||||
await n8n.start.fromBlankCanvas();
|
||||
await expect(n8n.canvas.canvasPane()).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Entry Point: Basic Workflow Creation', () => {
|
||||
test('should create a new project and workflow', async ({ n8n }) => {
|
||||
await n8n.start.fromNewProject();
|
||||
await expect(n8n.canvas.canvasPane()).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Entry Point: Imported Workflow', () => {
|
||||
test('should import a webhook workflow', async ({ n8n }) => {
|
||||
const workflowImportResult = await n8n.start.fromImportedWorkflow('simple-webhook-test.json');
|
||||
const { webhookPath } = workflowImportResult;
|
||||
|
||||
const testPayload = { message: 'Hello from Playwright test' };
|
||||
|
||||
await n8n.canvas.clickExecuteWorkflowButton();
|
||||
await expect(n8n.canvas.getExecuteWorkflowButton()).toHaveText('Waiting for trigger event');
|
||||
|
||||
const webhookResponse = await n8n.page.request.post(`/webhook-test/${webhookPath}`, {
|
||||
data: testPayload,
|
||||
});
|
||||
|
||||
expect(webhookResponse.ok()).toBe(true);
|
||||
});
|
||||
|
||||
test('should import a workflow', async ({ n8n }) => {
|
||||
await n8n.start.fromImportedWorkflow('manual.json');
|
||||
await n8n.workflowComposer.executeWorkflowAndWaitForNotification('Success');
|
||||
await expect(n8n.canvas.canvasPane()).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,23 +1,22 @@
|
||||
import { test, expect } from '../../fixtures/base';
|
||||
import { importAndActivateWebhookWorkflow, triggerWebhook } from '../../services/webhook-helper';
|
||||
|
||||
test.describe('External Webhook Triggering @auth:owner', () => {
|
||||
test.describe('External Webhook Triggering', () => {
|
||||
test('should create workflow via API, activate it, trigger webhook externally, and verify execution', async ({
|
||||
api,
|
||||
}) => {
|
||||
const { webhookPath, workflowId } = await importAndActivateWebhookWorkflow(
|
||||
api,
|
||||
const { webhookPath, workflowId } = await api.workflowApi.importWorkflow(
|
||||
'simple-webhook-test.json',
|
||||
);
|
||||
|
||||
const testPayload = { message: 'Hello from Playwright test' };
|
||||
|
||||
const webhookResponse = await triggerWebhook(api, webhookPath, {
|
||||
const webhookResponse = await api.request.post(`/webhook/${webhookPath}`, {
|
||||
data: testPayload,
|
||||
});
|
||||
|
||||
expect(webhookResponse.ok()).toBe(true);
|
||||
|
||||
const execution = await api.workflowApi.waitForExecution(workflowId, 10000);
|
||||
const execution = await api.workflowApi.waitForExecution(workflowId, 5000);
|
||||
expect(execution.status).toBe('success');
|
||||
|
||||
const executionDetails = await api.workflowApi.getExecution(execution.id);
|
||||
|
||||
22
packages/testing/playwright/workflows/manual.json
Normal file
22
packages/testing/playwright/workflows/manual.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "Manual Trigger Workflow",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "0567e1a2-e1cb-480f-9a17-8f37b88bbe7f",
|
||||
"name": "When clicking ‘Execute workflow’"
|
||||
}
|
||||
],
|
||||
"pinData": {},
|
||||
"connections": {},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"versionId": "2071c8bd-6e92-4eb6-81cd-3d693f683955",
|
||||
"id": "YaCQ6nNLk4xJabWT",
|
||||
"tags": []
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Simple Webhook Test",
|
||||
"active": false,
|
||||
"active": true,
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
|
||||
32
pnpm-lock.yaml
generated
32
pnpm-lock.yaml
generated
@@ -3129,9 +3129,18 @@ importers:
|
||||
generate-schema:
|
||||
specifier: 2.6.0
|
||||
version: 2.6.0
|
||||
n8n:
|
||||
specifier: workspace:*
|
||||
version: link:../../cli
|
||||
n8n-containers:
|
||||
specifier: workspace:*
|
||||
version: link:../containers
|
||||
n8n-core:
|
||||
specifier: workspace:*
|
||||
version: link:../../core
|
||||
n8n-workflow:
|
||||
specifier: workspace:*
|
||||
version: link:../../workflow
|
||||
nanoid:
|
||||
specifier: 'catalog:'
|
||||
version: 3.3.8
|
||||
@@ -18795,7 +18804,7 @@ snapshots:
|
||||
'@currents/commit-info': 1.0.1-beta.0
|
||||
async-retry: 1.3.3
|
||||
axios: 1.11.0(debug@4.4.1)
|
||||
axios-retry: 4.5.0(axios@1.11.0(debug@4.4.1))
|
||||
axios-retry: 4.5.0(axios@1.11.0)
|
||||
c12: 1.11.2(magicast@0.3.5)
|
||||
chalk: 4.1.2
|
||||
commander: 12.1.0
|
||||
@@ -23479,14 +23488,9 @@ snapshots:
|
||||
|
||||
axe-core@4.7.2: {}
|
||||
|
||||
axios-retry@4.5.0(axios@1.11.0(debug@4.4.1)):
|
||||
dependencies:
|
||||
axios: 1.11.0(debug@4.4.1)
|
||||
is-retry-allowed: 2.2.0
|
||||
|
||||
axios-retry@4.5.0(axios@1.11.0):
|
||||
dependencies:
|
||||
axios: 1.11.0(debug@4.3.6)
|
||||
axios: 1.11.0(debug@4.4.1)
|
||||
is-retry-allowed: 2.2.0
|
||||
|
||||
axios-retry@4.5.0(axios@1.8.3):
|
||||
@@ -23851,7 +23855,7 @@ snapshots:
|
||||
|
||||
bundlemon@3.1.0(typescript@5.9.2):
|
||||
dependencies:
|
||||
axios: 1.11.0(debug@4.3.6)
|
||||
axios: 1.11.0(debug@4.4.1)
|
||||
axios-retry: 4.5.0(axios@1.11.0)
|
||||
brotli-size: 4.0.0
|
||||
bundlemon-utils: 2.0.1
|
||||
@@ -27000,7 +27004,7 @@ snapshots:
|
||||
|
||||
infisical-node@1.3.0:
|
||||
dependencies:
|
||||
axios: 1.11.0(debug@4.3.6)
|
||||
axios: 1.11.0(debug@4.4.1)
|
||||
dotenv: 16.3.1
|
||||
tweetnacl: 1.0.3
|
||||
tweetnacl-util: 0.15.1
|
||||
@@ -28207,7 +28211,7 @@ snapshots:
|
||||
'@langchain/groq': 0.2.3(@langchain/core@0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.67)))(encoding@0.1.13)
|
||||
'@langchain/mistralai': 0.2.1(@langchain/core@0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.67)))(zod@3.25.67)
|
||||
'@langchain/ollama': 0.2.3(@langchain/core@0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.67)))
|
||||
axios: 1.11.0(debug@4.3.6)
|
||||
axios: 1.11.0(debug@4.4.1)
|
||||
cheerio: 1.0.0
|
||||
handlebars: 4.7.8
|
||||
transitivePeerDependencies:
|
||||
@@ -30259,7 +30263,7 @@ snapshots:
|
||||
|
||||
posthog-node@3.2.1:
|
||||
dependencies:
|
||||
axios: 1.11.0(debug@4.3.6)
|
||||
axios: 1.11.0(debug@4.4.1)
|
||||
rusha: 0.8.14
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
@@ -30938,7 +30942,7 @@ snapshots:
|
||||
|
||||
retry-axios@2.6.0(axios@1.11.0):
|
||||
dependencies:
|
||||
axios: 1.11.0(debug@4.3.6)
|
||||
axios: 1.11.0(debug@4.4.1)
|
||||
|
||||
retry-request@7.0.2(encoding@0.1.13):
|
||||
dependencies:
|
||||
@@ -31443,7 +31447,7 @@ snapshots:
|
||||
asn1.js: 5.4.1
|
||||
asn1.js-rfc2560: 5.0.1(asn1.js@5.4.1)
|
||||
asn1.js-rfc5280: 3.0.0
|
||||
axios: 1.11.0(debug@4.3.6)
|
||||
axios: 1.11.0(debug@4.4.1)
|
||||
big-integer: 1.6.52
|
||||
bignumber.js: 9.1.2
|
||||
binascii: 0.0.2
|
||||
@@ -33587,4 +33591,4 @@ snapshots:
|
||||
zx@8.1.4:
|
||||
optionalDependencies:
|
||||
'@types/fs-extra': 11.0.4
|
||||
'@types/node': 20.17.57
|
||||
'@types/node': 20.17.57
|
||||
|
||||
Reference in New Issue
Block a user