Files
alighasami 3d5eaf9445
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
first commit
2026-03-17 16:22:57 +03:30

309 lines
10 KiB
TypeScript

import { test, expect } from '../../../../fixtures/base';
import type { n8nPage } from '../../../../pages/n8nPage';
import type { TestRequirements } from '../../../../Types';
import allTemplatesSearchResponse from '../../../../workflows/all_templates_search_response.json';
import onboardingWorkflow from '../../../../workflows/Onboarding_workflow.json';
import salesTemplatesSearchResponse from '../../../../workflows/sales_templates_search_response.json';
import workflowTemplate from '../../../../workflows/Workflow_template_write_http_query.json';
const TEMPLATE_HOST = {
N8N_API: 'https://api.n8n.io/api/',
CUSTOM: 'random.domain',
} as const;
const URLS = {
N8N_WORKFLOWS: 'https://n8n.io/workflows',
} as const;
const TEMPLATE_ID = '1';
const TEST_CATEGORY = 'sales';
const SALES_CATEGORY_ID = 3;
const CATEGORIES = [
{ id: 1, name: 'Engineering' },
{ id: 2, name: 'Finance' },
{ id: 3, name: 'Sales' },
];
const COLLECTIONS = [
{
id: 1,
name: 'Test Collection',
workflows: [{ id: 1 }],
nodes: [],
},
];
// Helper functions
/**
* Prevents the browser's "before unload" confirmation dialog
* Used when navigating away from workflows with unsaved changes in tests
*/
function preventNavigation(n8n: n8nPage) {
return n8n.page.evaluate(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).preventNodeViewBeforeUnload = true;
});
}
/**
* Extracts the numeric count from UI text (e.g., "5 templates" → 5)
* @param text - Text containing a number (e.g., "5 templates", "3 collections")
* @returns The extracted number, or 0 if no number found
*/
function parseCount(text: string | null): number {
const digits = text?.replace(/\D/g, '') ?? '';
return parseInt(digits || '0', 10);
}
function createTemplateHostRequirements(): TestRequirements {
return {
config: {
settings: {
templates: {
enabled: true,
host: TEMPLATE_HOST.N8N_API,
},
},
},
};
}
function createCustomTemplateHostRequirements(hostname: string): TestRequirements {
return {
config: {
settings: {
templates: {
enabled: true,
host: `https://${hostname}/api`,
},
},
},
intercepts: {
health: {
url: `https://${hostname}/api/health`,
response: { status: 'OK' },
},
categories: {
url: `https://${hostname}/api/templates/categories`,
response: { categories: CATEGORIES },
},
getTemplate: {
url: `https://${hostname}/api/workflows/templates/${TEMPLATE_ID}`,
response: {
id: 1,
name: onboardingWorkflow.name,
workflow: onboardingWorkflow,
},
},
getTemplatePreview: {
url: `https://${hostname}/api/templates/workflows/${TEMPLATE_ID}`,
response: workflowTemplate,
},
},
};
}
/**
* Setup dynamic template routes for collections and search
* This handles query parameter-based routing that cannot be configured statically
* - Collections API filters by category
* - Search API returns different results based on category
*/
async function setupDynamicTemplateRoutes(n8n: n8nPage, hostname: string) {
await n8n.page.route(`https://${hostname}/api/templates/collections*`, (route) => {
const url = new URL(route.request().url());
const categoryParam = url.searchParams.get('category[]');
const response = categoryParam === String(SALES_CATEGORY_ID) ? [] : COLLECTIONS;
void route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ collections: response }),
});
});
await n8n.page.route(`https://${hostname}/api/templates/search*`, (route) => {
const url = new URL(route.request().url());
const category = url.searchParams.get('category');
const response =
category === 'Sales' ? salesTemplatesSearchResponse : allTemplatesSearchResponse;
void route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(response),
});
});
}
test.describe('Workflow templates', {
annotation: [
{ type: 'owner', description: 'Adore' },
],
}, () => {
test.describe('For api.n8n.io', () => {
test('Opens website when clicking templates sidebar link', async ({
n8n,
setupRequirements,
}) => {
await setupRequirements(createTemplateHostRequirements());
await n8n.navigate.toWorkflows();
const templatesLink = n8n.sideBar.getTemplatesLink();
await expect(templatesLink).toBeVisible();
const href = await templatesLink.getAttribute('href');
expect(href).toContain(URLS.N8N_WORKFLOWS);
const url = new URL(href!);
const origin = await n8n.page.evaluate(() => window.location.origin);
const utmInstance = url.searchParams.get('utm_instance');
expect(utmInstance).toBeTruthy();
expect(decodeURIComponent(utmInstance!)).toContain(origin);
const utmVersion = url.searchParams.get('utm_n8n_version');
expect(utmVersion).toBeTruthy();
expect(utmVersion).toMatch(/[0-9]+\.[0-9]+\.[0-9]+/);
const utmAwc = url.searchParams.get('utm_awc');
expect(utmAwc).toBeTruthy();
expect(utmAwc).toMatch(/[0-9]+/);
await expect(templatesLink).toHaveAttribute('target', '_blank');
});
test('Redirects to website when visiting templates page directly', async ({
n8n,
setupRequirements,
}) => {
await setupRequirements(createTemplateHostRequirements());
await n8n.navigate.toTemplates();
await expect(n8n.templates.getPageHeading()).toBeVisible({ timeout: 10000 });
});
});
test.describe('For a custom template host', () => {
const hostname = TEMPLATE_HOST.CUSTOM;
test.beforeEach(async ({ n8n, setupRequirements }) => {
await setupRequirements(createCustomTemplateHostRequirements(hostname));
await setupDynamicTemplateRoutes(n8n, hostname);
});
test('can open onboarding flow', async ({ n8n }) => {
await preventNavigation(n8n);
await Promise.all([
n8n.page.waitForResponse(`https://${hostname}/api/workflows/templates/${TEMPLATE_ID}`),
n8n.page.waitForResponse('**/rest/workflows'),
n8n.navigate.toOnboardingTemplate(TEMPLATE_ID),
]);
await expect(n8n.page).toHaveURL(/.*\/workflow\/.*onboardingId=1$/);
const workflowNameOnboarding = await n8n.canvas.getWorkflowName().getAttribute('title');
expect(workflowNameOnboarding).toContain(`Demo: ${onboardingWorkflow.name}`);
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(4);
await expect(n8n.canvas.sticky.getStickies()).toHaveCount(1);
});
test('can import template', async ({ n8n }) => {
await preventNavigation(n8n);
await Promise.all([
n8n.page.waitForResponse(`https://${hostname}/api/workflows/templates/${TEMPLATE_ID}`),
n8n.page.waitForResponse('**/rest/workflows/**'),
n8n.navigate.toTemplateImport(TEMPLATE_ID),
]);
// New workflows redirect to /workflow/<id>?new=true&templateId=1
await expect(n8n.page).toHaveURL(/\/workflow\/[a-zA-Z0-9_-]+\?.*templateId=1/);
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(4);
await expect(n8n.canvas.sticky.getStickies()).toHaveCount(1);
const workflowName = await n8n.canvas.getWorkflowName().getAttribute('title');
expect(workflowName).toContain(onboardingWorkflow.name);
});
test('should save template id with the workflow', async ({ n8n }) => {
await n8n.templatesComposer.importFirstTemplate();
// Execute workflow to trigger autosave (imported templates don't auto-save immediately)
await n8n.canvas.hitExecuteWorkflow();
const saveResponsePromise = n8n.canvas.waitForSaveWorkflowCompleted();
const saveResponse = await saveResponsePromise;
const requestBody = saveResponse.request().postDataJSON();
expect(requestBody.meta.templateId).toBe(TEMPLATE_ID);
});
test('can open template with images and hides workflow screenshots', async ({ n8n }) => {
await n8n.navigate.toTemplate(TEMPLATE_ID);
await expect(n8n.templates.getDescription()).toBeVisible();
await expect(n8n.templates.getDescription().locator('img')).toHaveCount(1);
});
test('renders search elements correctly', async ({ n8n }) => {
await n8n.navigate.toTemplates();
await expect(n8n.templates.getSearchInput()).toBeVisible();
await expect(n8n.templates.getAllCategoriesFilter()).toBeVisible();
const categoryFilterCount = await n8n.templates.getCategoryFilters().count();
expect(categoryFilterCount).toBeGreaterThan(1);
const templateCardCount = await n8n.templates.getTemplateCards().count();
expect(templateCardCount).toBeGreaterThan(0);
});
test('can filter templates by category', async ({ n8n }) => {
await n8n.navigate.toTemplates();
await expect(n8n.templates.getTemplatesLoadingContainer()).toBeHidden();
await expect(n8n.templates.getCategoryFilter(TEST_CATEGORY)).toBeVisible();
const initialTemplateText = await n8n.templates.getTemplateCountLabel().textContent();
const initialTemplateCount = parseCount(initialTemplateText);
const initialCollectionText = await n8n.templates.getCollectionCountLabel().textContent();
const initialCollectionCount = parseCount(initialCollectionText);
await n8n.templates.clickCategoryFilter(TEST_CATEGORY);
await expect(n8n.templates.getTemplatesLoadingContainer()).toBeHidden();
const finalTemplateText = await n8n.templates.getTemplateCountLabel().textContent();
const finalTemplateCount = parseCount(finalTemplateText);
expect(finalTemplateCount).toBeLessThan(initialTemplateCount);
const finalCollectionText = await n8n.templates.getCollectionCountLabel().textContent();
const finalCollectionCount = parseCount(finalCollectionText);
expect(finalCollectionCount).toBeLessThan(initialCollectionCount);
});
test('should preserve search query in URL', async ({ n8n }) => {
await n8n.navigate.toTemplates();
await expect(n8n.templates.getTemplatesLoadingContainer()).toBeHidden();
await expect(n8n.templates.getCategoryFilter(TEST_CATEGORY)).toBeVisible();
await n8n.templates.clickCategoryFilter(TEST_CATEGORY);
await n8n.templates.getSearchInput().fill('auto');
await expect(n8n.page).toHaveURL(/\?categories=/);
await expect(n8n.page).toHaveURL(/&search=/);
await n8n.page.reload();
await expect(n8n.page).toHaveURL(/\?categories=/);
await expect(n8n.page).toHaveURL(/&search=/);
const salesFilterLabel = n8n.templates.getCategoryFilter(TEST_CATEGORY);
await expect(salesFilterLabel).toBeChecked();
await expect(n8n.templates.getSearchInput()).toHaveValue('auto');
await expect(n8n.templates.getCategoryFilters().nth(1)).toHaveText('Sales');
});
});
});