mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
test: Add credential helper and ndv helper (#18636)
This commit is contained in:
106
packages/testing/playwright/helpers/NodeParameterHelper.ts
Normal file
106
packages/testing/playwright/helpers/NodeParameterHelper.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import type { NodeDetailsViewPage } from '../pages/NodeDetailsViewPage';
|
||||
|
||||
/**
|
||||
* Helper class for setting node parameters in the NDV
|
||||
*/
|
||||
export class NodeParameterHelper {
|
||||
constructor(private ndv: NodeDetailsViewPage) {}
|
||||
|
||||
/**
|
||||
* Detects parameter type by checking DOM structure
|
||||
* Supports dropdown, text, and switch parameters
|
||||
* @param parameterName - The parameter name to check
|
||||
* @returns The detected parameter type
|
||||
*/
|
||||
async detectParameterType(parameterName: string): Promise<'dropdown' | 'text' | 'switch'> {
|
||||
const parameterContainer = this.ndv.getParameterInput(parameterName);
|
||||
const [hasSwitch, hasSelect, hasSelectCaret] = await Promise.all([
|
||||
parameterContainer
|
||||
.locator('.el-switch')
|
||||
.count()
|
||||
.then((count) => count > 0),
|
||||
parameterContainer
|
||||
.locator('.el-select')
|
||||
.count()
|
||||
.then((count) => count > 0),
|
||||
parameterContainer
|
||||
.locator('.el-select__caret')
|
||||
.count()
|
||||
.then((count) => count > 0),
|
||||
]);
|
||||
|
||||
if (hasSwitch) return 'switch';
|
||||
if (hasSelect && hasSelectCaret) return 'dropdown';
|
||||
return 'text';
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a parameter value with automatic type detection or explicit type
|
||||
* Supports dropdown, text, and switch parameters
|
||||
* @param parameterName - Name of the parameter to set
|
||||
* @param value - Value to set (string or boolean)
|
||||
* @param type - Optional explicit type to skip detection for better performance
|
||||
*/
|
||||
async setParameter(
|
||||
parameterName: string,
|
||||
value: string | boolean,
|
||||
type?: 'dropdown' | 'text' | 'switch',
|
||||
): Promise<void> {
|
||||
if (typeof value === 'boolean') {
|
||||
await this.ndv.setParameterSwitch(parameterName, value);
|
||||
return;
|
||||
}
|
||||
|
||||
const parameterType = type ?? (await this.detectParameterType(parameterName));
|
||||
switch (parameterType) {
|
||||
case 'dropdown':
|
||||
await this.ndv.setParameterDropdown(parameterName, value);
|
||||
break;
|
||||
case 'text':
|
||||
await this.ndv.setParameterInput(parameterName, value);
|
||||
break;
|
||||
case 'switch':
|
||||
await this.ndv.setParameterSwitch(parameterName, value === 'true');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async webhook(config: {
|
||||
httpMethod?: string;
|
||||
path?: string;
|
||||
authentication?: string;
|
||||
responseMode?: string;
|
||||
}): Promise<void> {
|
||||
if (config.httpMethod !== undefined)
|
||||
await this.setParameter('httpMethod', config.httpMethod, 'dropdown');
|
||||
if (config.path !== undefined) await this.setParameter('path', config.path, 'text');
|
||||
if (config.authentication !== undefined)
|
||||
await this.setParameter('authentication', config.authentication, 'dropdown');
|
||||
if (config.responseMode !== undefined)
|
||||
await this.setParameter('responseMode', config.responseMode, 'dropdown');
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplified HTTP Request node parameter configuration
|
||||
* @param config - Configuration object with parameter values
|
||||
*/
|
||||
async httpRequest(config: {
|
||||
method?: string;
|
||||
url?: string;
|
||||
authentication?: string;
|
||||
sendQuery?: boolean;
|
||||
sendHeaders?: boolean;
|
||||
sendBody?: boolean;
|
||||
}): Promise<void> {
|
||||
if (config.method !== undefined) await this.setParameter('method', config.method, 'dropdown');
|
||||
if (config.url !== undefined) await this.setParameter('url', config.url, 'text');
|
||||
if (config.authentication !== undefined)
|
||||
await this.setParameter('authentication', config.authentication, 'dropdown');
|
||||
if (config.sendQuery !== undefined)
|
||||
await this.setParameter('sendQuery', config.sendQuery, 'switch');
|
||||
if (config.sendHeaders !== undefined)
|
||||
await this.setParameter('sendHeaders', config.sendHeaders, 'switch');
|
||||
if (config.sendBody !== undefined)
|
||||
await this.setParameter('sendBody', config.sendBody, 'switch');
|
||||
}
|
||||
}
|
||||
@@ -77,6 +77,18 @@ export class CanvasPage extends BasePage {
|
||||
await this.nodeCreatorSubItem(subItemText).click();
|
||||
}
|
||||
|
||||
async addActionNode(searchText: string, subItemText: string): Promise<void> {
|
||||
await this.addNode(searchText);
|
||||
await this.page.getByText('Actions').click();
|
||||
await this.nodeCreatorSubItem(subItemText).click();
|
||||
}
|
||||
|
||||
async addTriggerNode(searchText: string, subItemText: string): Promise<void> {
|
||||
await this.addNode(searchText);
|
||||
await this.page.getByText('Triggers').click();
|
||||
await this.nodeCreatorSubItem(subItemText).click();
|
||||
}
|
||||
|
||||
async deleteNodeByName(nodeName: string): Promise<void> {
|
||||
await this.nodeDeleteButton(nodeName).click();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import { BasePage } from './BasePage';
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { BasePage } from './BasePage';
|
||||
import { NodeParameterHelper } from '../helpers/NodeParameterHelper';
|
||||
|
||||
export class NodeDetailsViewPage extends BasePage {
|
||||
readonly setupHelper: NodeParameterHelper;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
this.setupHelper = new NodeParameterHelper(this);
|
||||
}
|
||||
|
||||
export class NodeDisplayViewPage extends BasePage {
|
||||
async clickBackToCanvasButton() {
|
||||
await this.clickByTestId('back-to-canvas');
|
||||
}
|
||||
@@ -187,16 +198,82 @@ export class NodeDisplayViewPage extends BasePage {
|
||||
}
|
||||
|
||||
/**
|
||||
* Select option in parameter dropdown
|
||||
* Get parameter input field
|
||||
* @param parameterName - The name of the parameter
|
||||
*/
|
||||
getParameterInputField(parameterName: string) {
|
||||
return this.getParameterInput(parameterName).getByTestId('parameter-input-field');
|
||||
}
|
||||
|
||||
/**
|
||||
* Select option in parameter dropdown (improved with Playwright best practices)
|
||||
* @param parameterName - The parameter name
|
||||
* @param optionText - The text of the option to select
|
||||
*/
|
||||
async selectOptionInParameterDropdown(parameterName: string, optionText: string) {
|
||||
const dropdown = this.getParameterInput(parameterName);
|
||||
await dropdown.click();
|
||||
|
||||
// Wait for dropdown to be visible and select option - following Playwright best practices
|
||||
await this.page.getByRole('option', { name: optionText }).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Click parameter dropdown by name (test-id based selector)
|
||||
* @param parameterName - The parameter name e.g 'httpMethod', 'authentication'
|
||||
*/
|
||||
async clickParameterDropdown(parameterName: string): Promise<void> {
|
||||
await this.clickByTestId(`parameter-input-${parameterName}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select option from visible dropdown using Playwright role-based selectors
|
||||
* This follows the pattern used in working n8n tests
|
||||
* @param optionText - The text of the option to select
|
||||
*/
|
||||
async selectFromVisibleDropdown(optionText: string): Promise<void> {
|
||||
// Use Playwright's role-based selector - this is more reliable than CSS selectors
|
||||
await this.page.getByRole('option', { name: optionText }).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill parameter input field by parameter name
|
||||
* @param parameterName - The parameter name e.g 'path', 'url'
|
||||
* @param value - The value to fill
|
||||
*/
|
||||
async fillParameterInputByName(parameterName: string, value: string): Promise<void> {
|
||||
const input = this.getParameterInputField(parameterName);
|
||||
await input.click();
|
||||
await input.fill(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Click parameter options expansion (e.g. for Response Code)
|
||||
*/
|
||||
async clickParameterOptions(): Promise<void> {
|
||||
await this.page.locator('.param-options').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get visible Element UI popper (dropdown/popover)
|
||||
* Ported from Cypress pattern with Playwright selectors
|
||||
*/
|
||||
getVisiblePopper() {
|
||||
return this.page
|
||||
.locator('.el-popper')
|
||||
.filter({ hasNot: this.page.locator('[aria-hidden="true"]') });
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for parameter dropdown to be visible and ready for interaction
|
||||
* @param parameterName - The parameter name
|
||||
*/
|
||||
async waitForParameterDropdown(parameterName: string): Promise<void> {
|
||||
const dropdown = this.getParameterInput(parameterName);
|
||||
await dropdown.waitFor({ state: 'visible' });
|
||||
await expect(dropdown).toBeEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on a floating node in the NDV (for switching between connected nodes)
|
||||
* @param nodeName - The name of the node to click
|
||||
@@ -279,4 +356,91 @@ export class NodeDisplayViewPage extends BasePage {
|
||||
getErrorMessageText(message: string) {
|
||||
return this.page.locator(`text=${message}`);
|
||||
}
|
||||
|
||||
async setParameterDropdown(parameterName: string, optionText: string): Promise<void> {
|
||||
await this.getParameterInput(parameterName).click();
|
||||
await this.page.getByRole('option', { name: optionText }).click();
|
||||
}
|
||||
|
||||
async setParameterInput(parameterName: string, value: string): Promise<void> {
|
||||
await this.fillParameterInputByName(parameterName, value);
|
||||
}
|
||||
|
||||
async setParameterSwitch(parameterName: string, enabled: boolean): Promise<void> {
|
||||
const switchElement = this.getParameterInput(parameterName).locator('.el-switch');
|
||||
const isCurrentlyEnabled = (await switchElement.getAttribute('aria-checked')) === 'true';
|
||||
if (isCurrentlyEnabled !== enabled) {
|
||||
await switchElement.click();
|
||||
}
|
||||
}
|
||||
|
||||
async setMultipleParameters(
|
||||
parameters: Record<string, string | number | boolean>,
|
||||
): Promise<void> {
|
||||
for (const [parameterName, value] of Object.entries(parameters)) {
|
||||
if (typeof value === 'string') {
|
||||
const parameterType = await this.setupHelper.detectParameterType(parameterName);
|
||||
if (parameterType === 'dropdown') {
|
||||
await this.setParameterDropdown(parameterName, value);
|
||||
} else {
|
||||
await this.setParameterInput(parameterName, value);
|
||||
}
|
||||
} else if (typeof value === 'boolean') {
|
||||
await this.setParameterSwitch(parameterName, value);
|
||||
} else if (typeof value === 'number') {
|
||||
await this.setParameterInput(parameterName, value.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getParameterValue(parameterName: string): Promise<string> {
|
||||
const parameterType = await this.setupHelper.detectParameterType(parameterName);
|
||||
|
||||
switch (parameterType) {
|
||||
case 'text':
|
||||
return await this.getTextParameterValue(parameterName);
|
||||
case 'dropdown':
|
||||
return await this.getDropdownParameterValue(parameterName);
|
||||
case 'switch':
|
||||
return await this.getSwitchParameterValue(parameterName);
|
||||
default:
|
||||
// Fallback for unknown types
|
||||
return (await this.getParameterInput(parameterName).textContent()) ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value from a text parameter - simplified approach
|
||||
*/
|
||||
private async getTextParameterValue(parameterName: string): Promise<string> {
|
||||
const parameterContainer = this.getParameterInput(parameterName);
|
||||
const input = parameterContainer.locator('input').first();
|
||||
return await input.inputValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value from a dropdown parameter
|
||||
*/
|
||||
private async getDropdownParameterValue(parameterName: string): Promise<string> {
|
||||
const selectedOption = this.getParameterInput(parameterName).locator('.el-select__tags-text');
|
||||
return (await selectedOption.textContent()) ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value from a switch parameter
|
||||
*/
|
||||
private async getSwitchParameterValue(parameterName: string): Promise<string> {
|
||||
const switchElement = this.getParameterInput(parameterName).locator('.el-switch');
|
||||
const isEnabled = (await switchElement.getAttribute('aria-checked')) === 'true';
|
||||
return isEnabled ? 'true' : 'false';
|
||||
}
|
||||
|
||||
async validateParameter(parameterName: string, expectedValue: string): Promise<void> {
|
||||
const actualValue = await this.getParameterValue(parameterName);
|
||||
if (actualValue !== expectedValue) {
|
||||
throw new Error(
|
||||
`Parameter ${parameterName} has value "${actualValue}", expected "${expectedValue}"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { CanvasPage } from './CanvasPage';
|
||||
import { CredentialsPage } from './CredentialsPage';
|
||||
import { ExecutionsPage } from './ExecutionsPage';
|
||||
import { IframePage } from './IframePage';
|
||||
import { NodeDisplayViewPage } from './NodeDisplayViewPage';
|
||||
import { NodeDetailsViewPage } from './NodeDetailsViewPage';
|
||||
import { NotificationsPage } from './NotificationsPage';
|
||||
import { NpsSurveyPage } from './NpsSurveyPage';
|
||||
import { ProjectSettingsPage } from './ProjectSettingsPage';
|
||||
@@ -34,7 +34,7 @@ export class n8nPage {
|
||||
readonly canvas: CanvasPage;
|
||||
|
||||
readonly iframe: IframePage;
|
||||
readonly ndv: NodeDisplayViewPage;
|
||||
readonly ndv: NodeDetailsViewPage;
|
||||
readonly npsSurvey: NpsSurveyPage;
|
||||
readonly projectSettings: ProjectSettingsPage;
|
||||
readonly settings: SettingsPage;
|
||||
@@ -66,7 +66,7 @@ export class n8nPage {
|
||||
this.canvas = new CanvasPage(page);
|
||||
|
||||
this.iframe = new IframePage(page);
|
||||
this.ndv = new NodeDisplayViewPage(page);
|
||||
this.ndv = new NodeDetailsViewPage(page);
|
||||
this.npsSurvey = new NpsSurveyPage(page);
|
||||
this.projectSettings = new ProjectSettingsPage(page);
|
||||
this.settings = new SettingsPage(page);
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
INSTANCE_ADMIN_CREDENTIALS,
|
||||
} from '../config/test-users';
|
||||
import { TestError } from '../Types';
|
||||
import { CredentialApiHelper } from './credential-api-helper';
|
||||
import { ProjectApiHelper } from './project-api-helper';
|
||||
import { WorkflowApiHelper } from './workflow-api-helper';
|
||||
|
||||
@@ -35,11 +36,13 @@ export class ApiHelpers {
|
||||
request: APIRequestContext;
|
||||
workflowApi: WorkflowApiHelper;
|
||||
projectApi: ProjectApiHelper;
|
||||
credentialApi: CredentialApiHelper;
|
||||
|
||||
constructor(requestContext: APIRequestContext) {
|
||||
this.request = requestContext;
|
||||
this.workflowApi = new WorkflowApiHelper(this);
|
||||
this.projectApi = new ProjectApiHelper(this);
|
||||
this.credentialApi = new CredentialApiHelper(this);
|
||||
}
|
||||
|
||||
// ===== MAIN SETUP METHODS =====
|
||||
|
||||
195
packages/testing/playwright/services/credential-api-helper.ts
Normal file
195
packages/testing/playwright/services/credential-api-helper.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import type {
|
||||
CreateCredentialDto,
|
||||
CredentialsGetManyRequestQuery,
|
||||
CredentialsGetOneRequestQuery,
|
||||
} from '@n8n/api-types';
|
||||
import type { ICredentialDataDecryptedObject } from 'n8n-workflow';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import type { ApiHelpers } from './api-helper';
|
||||
import { TestError } from '../Types';
|
||||
|
||||
interface CredentialResponse {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
data?: ICredentialDataDecryptedObject;
|
||||
scopes?: string[];
|
||||
shared?: Array<{
|
||||
id: string;
|
||||
projectId: string;
|
||||
role: string;
|
||||
}>;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
type CredentialImportResult = {
|
||||
credentialId: string;
|
||||
createdCredential: CredentialResponse;
|
||||
};
|
||||
|
||||
export class CredentialApiHelper {
|
||||
constructor(private api: ApiHelpers) {}
|
||||
|
||||
/**
|
||||
* Create a new credential
|
||||
*/
|
||||
async createCredential(credential: CreateCredentialDto): Promise<CredentialResponse> {
|
||||
const response = await this.api.request.post('/rest/credentials', { data: credential });
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new TestError(`Failed to create credential: ${await response.text()}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return result.data ?? result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all credentials with optional query parameters
|
||||
*/
|
||||
async getCredentials(query?: CredentialsGetManyRequestQuery): Promise<CredentialResponse[]> {
|
||||
const params = new URLSearchParams();
|
||||
if (query?.includeScopes) params.set('includeScopes', String(query.includeScopes));
|
||||
if (query?.includeData) params.set('includeData', String(query.includeData));
|
||||
if (query?.onlySharedWithMe) params.set('onlySharedWithMe', String(query.onlySharedWithMe));
|
||||
|
||||
const response = await this.api.request.get('/rest/credentials', { params });
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new TestError(`Failed to get credentials: ${await response.text()}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return Array.isArray(result) ? result : (result.data ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific credential by ID
|
||||
*/
|
||||
async getCredential(
|
||||
credentialId: string,
|
||||
query?: CredentialsGetOneRequestQuery,
|
||||
): Promise<CredentialResponse> {
|
||||
const params = new URLSearchParams();
|
||||
if (query?.includeData) params.set('includeData', String(query.includeData));
|
||||
|
||||
const response = await this.api.request.get(`/rest/credentials/${credentialId}`, { params });
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new TestError(`Failed to get credential: ${await response.text()}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return result.data ?? result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing credential
|
||||
*/
|
||||
async updateCredential(
|
||||
credentialId: string,
|
||||
updates: Partial<CreateCredentialDto>,
|
||||
): Promise<CredentialResponse> {
|
||||
const existingCredential = await this.getCredential(credentialId);
|
||||
|
||||
const updateData = {
|
||||
name: existingCredential.name,
|
||||
type: existingCredential.type,
|
||||
...updates,
|
||||
};
|
||||
|
||||
const response = await this.api.request.patch(`/rest/credentials/${credentialId}`, {
|
||||
data: updateData,
|
||||
});
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new TestError(`Failed to update credential: ${await response.text()}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return result.data ?? result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a credential
|
||||
*/
|
||||
async deleteCredential(credentialId: string): Promise<boolean> {
|
||||
const response = await this.api.request.delete(`/rest/credentials/${credentialId}`);
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new TestError(`Failed to delete credential: ${await response.text()}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get credentials available for a specific workflow or project
|
||||
*/
|
||||
async getCredentialsForWorkflow(options: {
|
||||
workflowId?: string;
|
||||
projectId?: string;
|
||||
}): Promise<CredentialResponse[]> {
|
||||
const params = new URLSearchParams();
|
||||
if (options.workflowId) params.set('workflowId', options.workflowId);
|
||||
if (options.projectId) params.set('projectId', options.projectId);
|
||||
|
||||
const response = await this.api.request.get('/rest/credentials/for-workflow', { params });
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new TestError(`Failed to get credentials for workflow: ${await response.text()}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return Array.isArray(result) ? result : (result.data ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfer a credential to another project
|
||||
*/
|
||||
async transferCredential(credentialId: string, destinationProjectId: string): Promise<void> {
|
||||
const response = await this.api.request.put(`/rest/credentials/${credentialId}/transfer`, {
|
||||
data: { destinationProjectId },
|
||||
});
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new TestError(`Failed to transfer credential: ${await response.text()}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make credential unique by adding a unique suffix to avoid naming conflicts in tests.
|
||||
*/
|
||||
private makeCredentialUnique(
|
||||
credential: CreateCredentialDto,
|
||||
options?: { idLength?: number },
|
||||
): CreateCredentialDto {
|
||||
const idLength = options?.idLength ?? 8;
|
||||
const uniqueSuffix = nanoid(idLength);
|
||||
|
||||
return {
|
||||
...credential,
|
||||
name: `${credential.name} (Test ${uniqueSuffix})`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a credential from definition with automatic unique naming for testing.
|
||||
* Returns detailed information about what was created.
|
||||
*/
|
||||
async createCredentialFromDefinition(
|
||||
credential: CreateCredentialDto,
|
||||
options?: { idLength?: number },
|
||||
): Promise<CredentialImportResult> {
|
||||
const uniqueCredential = this.makeCredentialUnique(credential, options);
|
||||
const createdCredential = await this.createCredential(uniqueCredential);
|
||||
const credentialId = createdCredential.id;
|
||||
|
||||
return {
|
||||
credentialId,
|
||||
createdCredential,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -27,4 +27,19 @@ export class ProjectApiHelper {
|
||||
const result = await response.json();
|
||||
return result.data ?? result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a project
|
||||
* @param projectId The ID of the project to delete
|
||||
* @returns True if deletion was successful
|
||||
*/
|
||||
async deleteProject(projectId: string): Promise<boolean> {
|
||||
const response = await this.api.request.delete(`/rest/projects/${projectId}`);
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new TestError(`Failed to delete project: ${await response.text()}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
|
||||
test.describe('Core UI Patterns - Building Blocks', () => {
|
||||
test.describe('01 - UI Test Entry Points', () => {
|
||||
test.describe('Entry Point: Home Page', () => {
|
||||
test('should navigate from home', async ({ n8n }) => {
|
||||
await n8n.start.fromHome();
|
||||
@@ -0,0 +1,50 @@
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
|
||||
test.describe('03 - Node Details Configuration', () => {
|
||||
test.beforeEach(async ({ n8n }) => {
|
||||
await n8n.start.fromBlankCanvas();
|
||||
});
|
||||
|
||||
test('should configure webhook node', async ({ n8n }) => {
|
||||
await n8n.canvas.addNode('Webhook');
|
||||
|
||||
await n8n.ndv.setupHelper.webhook({
|
||||
httpMethod: 'POST',
|
||||
path: 'test-webhook',
|
||||
authentication: 'Basic Auth',
|
||||
});
|
||||
|
||||
await expect(n8n.ndv.getParameterInputField('path')).toHaveValue('test-webhook');
|
||||
});
|
||||
|
||||
test('should configure HTTP Request node', async ({ n8n }) => {
|
||||
await n8n.canvas.addNode('HTTP Request');
|
||||
|
||||
await n8n.ndv.setupHelper.httpRequest({
|
||||
method: 'POST',
|
||||
url: 'https://api.example.com/test',
|
||||
sendQuery: true,
|
||||
sendHeaders: false,
|
||||
});
|
||||
|
||||
await expect(n8n.ndv.getParameterInputField('url')).toHaveValue('https://api.example.com/test');
|
||||
});
|
||||
|
||||
test('should auto-detect parameter types', async ({ n8n }) => {
|
||||
await n8n.canvas.addNode('Webhook');
|
||||
|
||||
await n8n.ndv.setupHelper.setParameter('httpMethod', 'PUT');
|
||||
await n8n.ndv.setupHelper.setParameter('path', 'auto-detect-test');
|
||||
|
||||
await expect(n8n.ndv.getParameterInputField('path')).toHaveValue('auto-detect-test');
|
||||
});
|
||||
|
||||
test('should use explicit types for better performance', async ({ n8n }) => {
|
||||
await n8n.canvas.addNode('Webhook');
|
||||
|
||||
await n8n.ndv.setupHelper.setParameter('httpMethod', 'PATCH', 'dropdown');
|
||||
await n8n.ndv.setupHelper.setParameter('path', 'explicit-types', 'text');
|
||||
|
||||
await expect(n8n.ndv.getParameterInputField('path')).toHaveValue('explicit-types');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,195 @@
|
||||
import type { CreateCredentialDto } from '@n8n/api-types';
|
||||
|
||||
import { test, expect } from '../../fixtures/base';
|
||||
|
||||
test.describe('Credential API Operations', () => {
|
||||
test.describe('Basic CRUD Operations', () => {
|
||||
test('should create, retrieve, update, and delete credential', async ({ api }) => {
|
||||
const credentialData: CreateCredentialDto = {
|
||||
name: 'Test HTTP Basic Auth',
|
||||
type: 'httpBasicAuth',
|
||||
data: {
|
||||
user: 'test_user',
|
||||
password: 'test_password',
|
||||
},
|
||||
};
|
||||
|
||||
const { credentialId, createdCredential } =
|
||||
await api.credentialApi.createCredentialFromDefinition(credentialData);
|
||||
|
||||
expect(credentialId).toBeTruthy();
|
||||
expect(createdCredential.type).toBe('httpBasicAuth');
|
||||
expect(createdCredential.name).toContain('Test HTTP Basic Auth (Test');
|
||||
|
||||
const retrievedCredential = await api.credentialApi.getCredential(credentialId);
|
||||
expect(retrievedCredential.id).toBe(credentialId);
|
||||
expect(retrievedCredential.type).toBe('httpBasicAuth');
|
||||
expect(retrievedCredential.name).toBe(createdCredential.name);
|
||||
|
||||
const credentialWithData = await api.credentialApi.getCredential(credentialId, {
|
||||
includeData: true,
|
||||
});
|
||||
expect(credentialWithData.data).toBeDefined();
|
||||
expect(credentialWithData.data?.user).toBe('test_user');
|
||||
|
||||
const updatedName = 'Updated HTTP Basic Auth';
|
||||
const updatedCredential = await api.credentialApi.updateCredential(credentialId, {
|
||||
name: updatedName,
|
||||
data: {
|
||||
user: 'updated_user',
|
||||
password: 'updated_password',
|
||||
},
|
||||
});
|
||||
expect(updatedCredential.name).toBe(updatedName);
|
||||
|
||||
const verifyUpdated = await api.credentialApi.getCredential(credentialId, {
|
||||
includeData: true,
|
||||
});
|
||||
expect(verifyUpdated.name).toBe(updatedName);
|
||||
expect(verifyUpdated.data?.user).toBe('updated_user');
|
||||
|
||||
const deleteResult = await api.credentialApi.deleteCredential(credentialId);
|
||||
expect(deleteResult).toBe(true);
|
||||
|
||||
await expect(api.credentialApi.getCredential(credentialId)).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Credential Listing', () => {
|
||||
test('should list credentials with different query options', async ({ api }) => {
|
||||
const credential1 = await api.credentialApi.createCredentialFromDefinition({
|
||||
name: 'First Test Credential',
|
||||
type: 'httpBasicAuth',
|
||||
data: { user: 'user1', password: 'pass1' },
|
||||
});
|
||||
|
||||
const credential2 = await api.credentialApi.createCredentialFromDefinition({
|
||||
name: 'Second Test Credential',
|
||||
type: 'httpHeaderAuth',
|
||||
data: { name: 'Authorization', value: 'Bearer token' },
|
||||
});
|
||||
|
||||
const allCredentials = await api.credentialApi.getCredentials();
|
||||
expect(allCredentials.length).toBeGreaterThanOrEqual(2);
|
||||
|
||||
const createdIds = [credential1.credentialId, credential2.credentialId];
|
||||
const foundCredentials = allCredentials.filter((c) => createdIds.includes(c.id));
|
||||
expect(foundCredentials).toHaveLength(2);
|
||||
|
||||
const credentialsWithScopes = await api.credentialApi.getCredentials({
|
||||
includeScopes: true,
|
||||
});
|
||||
expect(credentialsWithScopes[0].scopes).toBeDefined();
|
||||
expect(Array.isArray(credentialsWithScopes[0].scopes)).toBe(true);
|
||||
|
||||
const credentialsWithData = await api.credentialApi.getCredentials({
|
||||
includeData: true,
|
||||
});
|
||||
const foundWithData = credentialsWithData.filter((c) => createdIds.includes(c.id));
|
||||
expect(foundWithData.some((c) => c.data)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Project Integration', () => {
|
||||
test('should handle credential-project associations', async ({ api }) => {
|
||||
await api.enableFeature('projectRole:admin');
|
||||
await api.enableFeature('projectRole:editor');
|
||||
await api.setMaxTeamProjectsQuota(-1);
|
||||
|
||||
const project = await api.projectApi.createProject('Test Project for Credentials');
|
||||
|
||||
const credential = await api.credentialApi.createCredentialFromDefinition({
|
||||
name: 'Project Credential',
|
||||
type: 'httpBasicAuth',
|
||||
data: { user: 'user', password: 'pass' },
|
||||
projectId: project.id,
|
||||
});
|
||||
|
||||
const projectCredentials = await api.credentialApi.getCredentialsForWorkflow({
|
||||
projectId: project.id,
|
||||
});
|
||||
|
||||
expect(projectCredentials).toBeDefined();
|
||||
expect(Array.isArray(projectCredentials)).toBe(true);
|
||||
|
||||
const foundCredential = projectCredentials.find((c) => c.id === credential.credentialId);
|
||||
expect(foundCredential).toBeDefined();
|
||||
});
|
||||
|
||||
test('should transfer credential between projects', async ({ api }) => {
|
||||
await api.enableFeature('projectRole:admin');
|
||||
await api.enableFeature('projectRole:editor');
|
||||
await api.setMaxTeamProjectsQuota(-1);
|
||||
|
||||
const sourceProject = await api.projectApi.createProject('Source Project');
|
||||
const destinationProject = await api.projectApi.createProject('Destination Project');
|
||||
|
||||
const credential = await api.credentialApi.createCredentialFromDefinition({
|
||||
name: 'Transfer Test Credential',
|
||||
type: 'httpBasicAuth',
|
||||
data: { user: 'user', password: 'pass' },
|
||||
projectId: sourceProject.id,
|
||||
});
|
||||
|
||||
const sourceCredentials = await api.credentialApi.getCredentialsForWorkflow({
|
||||
projectId: sourceProject.id,
|
||||
});
|
||||
const foundInSource = sourceCredentials.find((c) => c.id === credential.credentialId);
|
||||
expect(foundInSource).toBeDefined();
|
||||
|
||||
await api.credentialApi.transferCredential(credential.credentialId, destinationProject.id);
|
||||
|
||||
const destinationCredentials = await api.credentialApi.getCredentialsForWorkflow({
|
||||
projectId: destinationProject.id,
|
||||
});
|
||||
const foundInDestination = destinationCredentials.find(
|
||||
(c) => c.id === credential.credentialId,
|
||||
);
|
||||
expect(foundInDestination).toBeDefined();
|
||||
|
||||
const sourceCredentialsAfter = await api.credentialApi.getCredentialsForWorkflow({
|
||||
projectId: sourceProject.id,
|
||||
});
|
||||
const stillInSource = sourceCredentialsAfter.find((c) => c.id === credential.credentialId);
|
||||
expect(stillInSource).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Data Persistence', () => {
|
||||
test('should maintain credential data across operations', async ({ api }) => {
|
||||
const originalData: CreateCredentialDto = {
|
||||
name: 'Persistence Test Credential',
|
||||
type: 'httpBasicAuth',
|
||||
data: {
|
||||
user: 'persistent_user',
|
||||
password: 'persistent_password',
|
||||
},
|
||||
};
|
||||
|
||||
const { credentialId } = await api.credentialApi.createCredentialFromDefinition(originalData);
|
||||
|
||||
const afterCreate = await api.credentialApi.getCredential(credentialId, {
|
||||
includeData: true,
|
||||
});
|
||||
expect(afterCreate.data?.user).toBe('persistent_user');
|
||||
|
||||
await api.credentialApi.updateCredential(credentialId, {
|
||||
data: {
|
||||
user: 'updated_persistent_user',
|
||||
password: 'updated_persistent_password',
|
||||
},
|
||||
});
|
||||
|
||||
const afterUpdate = await api.credentialApi.getCredential(credentialId, {
|
||||
includeData: true,
|
||||
});
|
||||
expect(afterUpdate.data?.user).toBe('updated_persistent_user');
|
||||
expect(afterUpdate.data?.password).toBeDefined();
|
||||
|
||||
const allCredentials = await api.credentialApi.getCredentials();
|
||||
const foundCredential = allCredentials.find((c) => c.id === credentialId);
|
||||
expect(foundCredential).toBeDefined();
|
||||
expect(foundCredential!.type).toBe('httpBasicAuth');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user