mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 09:36:44 +00:00
test: Migrate 3 specs from Cypress - Playwright (#19269)
This commit is contained in:
@@ -1,216 +0,0 @@
|
||||
import type { ICredentialType } from 'n8n-workflow';
|
||||
|
||||
import CustomCredential from '../fixtures/Custom_credential.json';
|
||||
import CustomNodeFixture from '../fixtures/Custom_node.json';
|
||||
import CustomNodeWithCustomCredentialFixture from '../fixtures/Custom_node_custom_credential.json';
|
||||
import CustomNodeWithN8nCredentialFixture from '../fixtures/Custom_node_n8n_credential.json';
|
||||
import { CredentialsModal, WorkflowPage } from '../pages';
|
||||
import { NodeCreator } from '../pages/features/node-creator';
|
||||
import {
|
||||
confirmCommunityNodeUninstall,
|
||||
confirmCommunityNodeUpdate,
|
||||
getCommunityCards,
|
||||
installFirstCommunityNode,
|
||||
visitCommunityNodesSettings,
|
||||
} from '../pages/settings-community-nodes';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
|
||||
const credentialsModal = new CredentialsModal();
|
||||
const nodeCreatorFeature = new NodeCreator();
|
||||
const workflowPage = new WorkflowPage();
|
||||
const ADD_TO_WORKFLOW_BUTTON = 'Add to workflow';
|
||||
|
||||
const addCommunityNodeToCanvas = (name: string) => {
|
||||
nodeCreatorFeature.actions.openNodeCreator();
|
||||
nodeCreatorFeature.getters.searchBar().find('input').clear().type(name);
|
||||
|
||||
nodeCreatorFeature.getters.getCreatorItem(name).find('.el-tooltip__trigger').should('exist');
|
||||
nodeCreatorFeature.actions.selectNode(name);
|
||||
|
||||
cy.contains('span', name).should('be.visible');
|
||||
cy.contains(ADD_TO_WORKFLOW_BUTTON).should('be.visible').click();
|
||||
};
|
||||
|
||||
// We separate-out the custom nodes because they require injecting nodes and credentials
|
||||
// so the /nodes and /credentials endpoints are intercepted and non-cached.
|
||||
// We want to keep the other tests as fast as possible so we don't want to break the cache in those.
|
||||
describe('Community and custom nodes in canvas', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept('/types/nodes.json', { middleware: true }, (req) => {
|
||||
req.headers['cache-control'] = 'no-cache, no-store';
|
||||
|
||||
req.on('response', (res) => {
|
||||
const nodes = res.body || [];
|
||||
|
||||
nodes.push(
|
||||
CustomNodeFixture,
|
||||
CustomNodeWithN8nCredentialFixture,
|
||||
CustomNodeWithCustomCredentialFixture,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
cy.intercept('/types/credentials.json', { middleware: true }, (req) => {
|
||||
req.headers['cache-control'] = 'no-cache, no-store';
|
||||
|
||||
req.on('response', (res) => {
|
||||
const credentials: ICredentialType[] = res.body || [];
|
||||
|
||||
credentials.push(CustomCredential as ICredentialType);
|
||||
});
|
||||
});
|
||||
|
||||
// next intercepts are not strictly needed, but they make the tests faster
|
||||
// - intercept request to vetted community types, returning empty list
|
||||
// - intercept request to vetted community type details, return null
|
||||
// - intercept request npm registry, return 404
|
||||
// --------------------------------------------------------------------------
|
||||
cy.intercept('/community-node-types', (req) => {
|
||||
req.reply({
|
||||
statusCode: 200,
|
||||
body: {
|
||||
data: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
cy.intercept('GET', '/community-node-types/*', {
|
||||
statusCode: 200,
|
||||
body: null,
|
||||
});
|
||||
|
||||
cy.intercept('GET', 'https://registry.npmjs.org/*', {
|
||||
statusCode: 404,
|
||||
body: {},
|
||||
});
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
workflowPage.actions.visit();
|
||||
});
|
||||
|
||||
it('should render and select community node', () => {
|
||||
addCommunityNodeToCanvas('E2E Node');
|
||||
|
||||
const nodeParameters = () => cy.getByTestId('node-parameters');
|
||||
const firstParameter = () => nodeParameters().find('.parameter-item').eq(0);
|
||||
const secondParameter = () => nodeParameters().find('.parameter-item').eq(1);
|
||||
|
||||
// Check correct fields are rendered
|
||||
nodeParameters().should('exist');
|
||||
// Test property text input
|
||||
firstParameter().contains('Test property').should('exist');
|
||||
firstParameter().find('input.el-input__inner').should('have.value', 'Some default');
|
||||
// Resource select input
|
||||
secondParameter().find('label').contains('Resource').should('exist');
|
||||
secondParameter().find('input.el-input__inner').should('have.value', 'option2');
|
||||
secondParameter().find('.el-select').click();
|
||||
// Check if all options are rendered and select the fourth one
|
||||
getVisibleSelect().find('li').should('have.length', 4);
|
||||
getVisibleSelect().find('li').eq(3).contains('option4').should('exist').click();
|
||||
secondParameter().find('input.el-input__inner').should('have.value', 'option4');
|
||||
});
|
||||
|
||||
it('should render custom node with n8n credential', () => {
|
||||
workflowPage.actions.addNodeToCanvas('Manual');
|
||||
addCommunityNodeToCanvas('E2E Node with native n8n credential');
|
||||
workflowPage.getters.nodeCredentialsLabel().click();
|
||||
workflowPage.getters.nodeCredentialsCreateOption().click();
|
||||
credentialsModal.getters.editCredentialModal().should('be.visible');
|
||||
credentialsModal.getters.editCredentialModal().should('contain.text', 'Notion API');
|
||||
});
|
||||
|
||||
it('should render custom node with custom credential', () => {
|
||||
workflowPage.actions.addNodeToCanvas('Manual');
|
||||
addCommunityNodeToCanvas('E2E Node with custom credential');
|
||||
workflowPage.getters.nodeCredentialsLabel().click();
|
||||
workflowPage.getters.nodeCredentialsCreateOption().click();
|
||||
credentialsModal.getters.editCredentialModal().should('be.visible');
|
||||
credentialsModal.getters.editCredentialModal().should('contain.text', 'Custom E2E Credential');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Community nodes', () => {
|
||||
const mockPackage = {
|
||||
createdAt: '2024-07-22T19:08:06.505Z',
|
||||
updatedAt: '2024-07-22T19:08:06.505Z',
|
||||
packageName: 'n8n-nodes-chatwork',
|
||||
installedVersion: '1.0.0',
|
||||
authorName: null,
|
||||
authorEmail: null,
|
||||
installedNodes: [
|
||||
{
|
||||
name: 'Chatwork',
|
||||
type: 'n8n-nodes-chatwork.chatwork',
|
||||
latestVersion: 1,
|
||||
},
|
||||
],
|
||||
updateAvailable: '1.1.2',
|
||||
};
|
||||
|
||||
it('can install, update and uninstall community nodes', () => {
|
||||
cy.intercept(
|
||||
{
|
||||
hostname: 'api.npms.io',
|
||||
pathname: '/v2/search',
|
||||
query: { q: 'keywords:n8n-community-node-package' },
|
||||
},
|
||||
{ body: {} },
|
||||
);
|
||||
cy.intercept(
|
||||
{ method: 'GET', pathname: '/rest/community-packages', times: 1 },
|
||||
{
|
||||
body: { data: [] },
|
||||
},
|
||||
).as('getEmptyPackages');
|
||||
visitCommunityNodesSettings();
|
||||
cy.wait('@getEmptyPackages');
|
||||
|
||||
// install a package
|
||||
cy.intercept(
|
||||
{ method: 'POST', pathname: '/rest/community-packages', times: 1 },
|
||||
{
|
||||
body: { data: mockPackage },
|
||||
},
|
||||
).as('installPackage');
|
||||
cy.intercept(
|
||||
{ method: 'GET', pathname: '/rest/community-packages', times: 1 },
|
||||
{
|
||||
body: { data: [mockPackage] },
|
||||
},
|
||||
).as('getPackages');
|
||||
installFirstCommunityNode('n8n-nodes-chatwork@1.0.0');
|
||||
cy.wait('@installPackage');
|
||||
cy.wait('@getPackages');
|
||||
getCommunityCards().should('have.length', 1);
|
||||
getCommunityCards().eq(0).should('include.text', 'v1.0.0');
|
||||
|
||||
// update the package
|
||||
cy.intercept(
|
||||
{ method: 'PATCH', pathname: '/rest/community-packages' },
|
||||
{
|
||||
body: { data: { ...mockPackage, installedVersion: '1.2.0', updateAvailable: undefined } },
|
||||
},
|
||||
).as('updatePackage');
|
||||
getCommunityCards().eq(0).find('button').click();
|
||||
confirmCommunityNodeUpdate();
|
||||
cy.wait('@updatePackage');
|
||||
getCommunityCards().should('have.length', 1);
|
||||
getCommunityCards().eq(0).should('not.include.text', 'v1.0.0');
|
||||
|
||||
// uninstall the package
|
||||
cy.intercept(
|
||||
{
|
||||
method: 'DELETE',
|
||||
pathname: '/rest/community-packages',
|
||||
query: { name: 'n8n-nodes-chatwork' },
|
||||
},
|
||||
{ statusCode: 204 },
|
||||
).as('uninstallPackage');
|
||||
getCommunityCards().getByTestId('action-toggle').click();
|
||||
cy.getByTestId('action-uninstall').click();
|
||||
confirmCommunityNodeUninstall();
|
||||
cy.wait('@uninstallPackage');
|
||||
|
||||
cy.getByTestId('action-box').should('exist');
|
||||
});
|
||||
});
|
||||
@@ -1,124 +0,0 @@
|
||||
import { VariablesPage } from '../pages/variables';
|
||||
|
||||
const variablesPage = new VariablesPage();
|
||||
|
||||
describe('Variables', () => {
|
||||
it('should show the unlicensed action box when the feature is disabled', () => {
|
||||
cy.disableFeature('variables');
|
||||
cy.visit(variablesPage.url);
|
||||
|
||||
variablesPage.getters.unavailableResourcesList().should('be.visible');
|
||||
variablesPage.getters.resourcesList().should('not.exist');
|
||||
});
|
||||
|
||||
describe('licensed', () => {
|
||||
before(() => {
|
||||
cy.enableFeature('variables');
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.intercept('GET', '/rest/variables').as('loadVariables');
|
||||
cy.intercept('GET', '/rest/login').as('login');
|
||||
|
||||
cy.visit(variablesPage.url);
|
||||
cy.wait(['@loadVariables', '@loadSettings', '@login']);
|
||||
});
|
||||
|
||||
it('should show the licensed action box when the feature is enabled', () => {
|
||||
variablesPage.getters.emptyResourcesList().should('be.visible');
|
||||
variablesPage.getters.emptyResourcesListNewVariableButton().should('be.visible');
|
||||
});
|
||||
|
||||
it('should create a new variable using empty state row', () => {
|
||||
const key = 'ENV_VAR';
|
||||
const value = 'value';
|
||||
|
||||
variablesPage.actions.createVariableFromEmptyState(key, value);
|
||||
variablesPage.getters.variableRow(key).should('contain', value).should('be.visible');
|
||||
variablesPage.getters.variablesRows().should('have.length', 1);
|
||||
});
|
||||
|
||||
it('should create a new variable using pre-existing state', () => {
|
||||
const key = 'ENV_VAR_NEW';
|
||||
const value = 'value2';
|
||||
|
||||
variablesPage.actions.createVariable(key, value);
|
||||
variablesPage.getters.variableRow(key).should('contain', value).should('be.visible');
|
||||
variablesPage.getters.variablesRows().should('have.length', 2);
|
||||
|
||||
const otherKey = 'ENV_EXAMPLE';
|
||||
const otherValue = 'value3';
|
||||
|
||||
variablesPage.actions.createVariable(otherKey, otherValue);
|
||||
variablesPage.getters
|
||||
.variableRow(otherKey)
|
||||
.should('contain', otherValue)
|
||||
.should('be.visible');
|
||||
variablesPage.getters.variablesRows().should('have.length', 3);
|
||||
});
|
||||
|
||||
it('should get validation errors and cancel variable creation', () => {
|
||||
const key = 'ENV_VAR_NEW$';
|
||||
const value = 'value3';
|
||||
|
||||
variablesPage.getters.createVariableButton().click();
|
||||
const editingRow = variablesPage.getters.variablesEditableRows().eq(0);
|
||||
variablesPage.actions.setRowValue(editingRow, 'key', key);
|
||||
variablesPage.actions.setRowValue(editingRow, 'value', value);
|
||||
variablesPage.actions.saveRowEditing(editingRow);
|
||||
variablesPage.getters
|
||||
.variablesEditableRows()
|
||||
.eq(0)
|
||||
.should('contain', 'This field may contain only letters');
|
||||
variablesPage.actions.cancelRowEditing(editingRow);
|
||||
|
||||
variablesPage.getters.variablesRows().should('have.length', 3);
|
||||
});
|
||||
|
||||
it('should edit a variable', () => {
|
||||
const key = 'ENV_VAR_NEW';
|
||||
const newValue = 'value4';
|
||||
|
||||
variablesPage.actions.editRow(key);
|
||||
const editingRow = variablesPage.getters.variablesEditableRows().eq(0);
|
||||
variablesPage.actions.setRowValue(editingRow, 'value', newValue);
|
||||
variablesPage.actions.saveRowEditing(editingRow);
|
||||
|
||||
variablesPage.getters.variableRow(key).should('contain', newValue).should('be.visible');
|
||||
variablesPage.getters.variablesRows().should('have.length', 3);
|
||||
});
|
||||
|
||||
it('should delete a variable', () => {
|
||||
const key = 'TO_DELETE';
|
||||
const value = 'xxx';
|
||||
|
||||
variablesPage.actions.createVariable(key, value);
|
||||
variablesPage.actions.deleteVariable(key);
|
||||
});
|
||||
|
||||
it('should search for a variable', () => {
|
||||
// One Result
|
||||
variablesPage.getters.searchBar().type('NEW');
|
||||
variablesPage.getters.variablesRows().should('have.length', 1);
|
||||
variablesPage.getters.variableRow('NEW').should('contain.text', 'ENV_VAR_NEW');
|
||||
cy.url().should('include', 'search=NEW');
|
||||
|
||||
// Multiple Results
|
||||
variablesPage.getters.searchBar().clear().type('ENV_VAR');
|
||||
variablesPage.getters.variablesRows().should('have.length', 2);
|
||||
cy.url().should('include', 'search=ENV_VAR');
|
||||
|
||||
// All Results
|
||||
variablesPage.getters.searchBar().clear().type('ENV');
|
||||
variablesPage.getters.variablesRows().should('have.length', 3);
|
||||
cy.url().should('include', 'search=ENV');
|
||||
|
||||
// No Results
|
||||
variablesPage.getters.searchBar().clear().type('Some non-existent variable');
|
||||
variablesPage.getters.variablesRows().should('not.exist');
|
||||
cy.url().should('include', 'search=Some+non-existent+variable');
|
||||
|
||||
cy.contains('No variables found').should('be.visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,64 +0,0 @@
|
||||
import { META_KEY } from '../constants';
|
||||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||
|
||||
const workflowPage = new WorkflowPageClass();
|
||||
|
||||
describe('Canvas Actions', () => {
|
||||
beforeEach(() => {
|
||||
workflowPage.actions.visit();
|
||||
cy.get('#collapse-change-button').should('be.visible').click();
|
||||
cy.get('#side-menu[class*=collapsed i]').should('be.visible');
|
||||
workflowPage.actions.zoomToFit();
|
||||
});
|
||||
|
||||
it('adds sticky to canvas with default text and position', () => {
|
||||
workflowPage.getters.addStickyButton().should('be.visible');
|
||||
|
||||
addDefaultSticky();
|
||||
workflowPage.actions.deselectAll();
|
||||
workflowPage.actions.addStickyFromContextMenu();
|
||||
workflowPage.actions.hitAddSticky();
|
||||
|
||||
workflowPage.getters.stickies().should('have.length', 3);
|
||||
|
||||
// Should not add a sticky for ctrl+shift+s
|
||||
cy.get('body').type(`{${META_KEY}+shift+s}`);
|
||||
|
||||
workflowPage.getters.stickies().should('have.length', 3);
|
||||
workflowPage.getters
|
||||
.stickies()
|
||||
.eq(0)
|
||||
.should('have.text', 'I’m a note\nDouble click to edit me. Guide\n')
|
||||
.find('a')
|
||||
.contains('Guide')
|
||||
.should('have.attr', 'href');
|
||||
});
|
||||
});
|
||||
|
||||
function shouldHaveOneSticky() {
|
||||
workflowPage.getters.stickies().should('have.length', 1);
|
||||
}
|
||||
|
||||
function shouldBeInDefaultLocation() {
|
||||
workflowPage.getters
|
||||
.stickies()
|
||||
.eq(0)
|
||||
.should(($el) => {
|
||||
expect($el).to.have.css('height', '160px');
|
||||
expect($el).to.have.css('width', '240px');
|
||||
});
|
||||
}
|
||||
|
||||
function shouldHaveDefaultSize() {
|
||||
workflowPage.getters.stickies().should(($el) => {
|
||||
expect($el).to.have.css('height', '160px');
|
||||
expect($el).to.have.css('width', '240px');
|
||||
});
|
||||
}
|
||||
|
||||
function addDefaultSticky() {
|
||||
workflowPage.actions.addSticky();
|
||||
shouldHaveOneSticky();
|
||||
shouldHaveDefaultSize();
|
||||
shouldBeInDefaultLocation();
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import { WorkflowPage, NDV } from '../pages';
|
||||
import { NodeCreator } from '../pages/features/node-creator';
|
||||
|
||||
const workflowPage = new WorkflowPage();
|
||||
const nodeCreatorFeature = new NodeCreator();
|
||||
const ndv = new NDV();
|
||||
|
||||
describe('HTTP Request node', () => {
|
||||
beforeEach(() => {
|
||||
workflowPage.actions.visit();
|
||||
});
|
||||
|
||||
it('should make a request with a URL and receive a response', () => {
|
||||
workflowPage.actions.addInitialNodeToCanvas('Manual');
|
||||
workflowPage.actions.addNodeToCanvas('HTTP Request');
|
||||
workflowPage.actions.openNode('HTTP Request');
|
||||
ndv.actions.typeIntoParameterInput('url', 'https://catfact.ninja/fact');
|
||||
|
||||
ndv.actions.execute();
|
||||
|
||||
ndv.getters.outputPanel().contains('fact');
|
||||
});
|
||||
|
||||
describe('Credential-only HTTP Request Node variants', () => {
|
||||
it('should render a modified HTTP Request Node', () => {
|
||||
workflowPage.actions.addInitialNodeToCanvas('Manual');
|
||||
|
||||
workflowPage.getters.nodeCreatorPlusButton().click();
|
||||
workflowPage.getters.nodeCreatorSearchBar().type('VirusTotal');
|
||||
|
||||
expect(nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'VirusTotal'));
|
||||
expect(
|
||||
nodeCreatorFeature.getters
|
||||
.nodeItemDescription()
|
||||
.first()
|
||||
.should('have.text', 'HTTP request'),
|
||||
);
|
||||
|
||||
nodeCreatorFeature.actions.selectNode('VirusTotal');
|
||||
expect(ndv.getters.nodeNameContainer().should('contain.text', 'VirusTotal HTTP Request'));
|
||||
expect(
|
||||
ndv.getters
|
||||
.parameterInput('url')
|
||||
.find('input')
|
||||
.should('contain.value', 'https://www.virustotal.com/api/v3/'),
|
||||
);
|
||||
|
||||
// These parameters exist for normal HTTP Request Node, but are hidden for credential-only variants
|
||||
expect(ndv.getters.parameterInput('authentication').should('not.exist'));
|
||||
expect(ndv.getters.parameterInput('nodeCredentialType').should('not.exist'));
|
||||
|
||||
expect(
|
||||
workflowPage.getters
|
||||
.nodeCredentialsLabel()
|
||||
.should('contain.text', 'Credential for VirusTotal'),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -26,9 +26,9 @@ export class TestEntryComposer {
|
||||
}
|
||||
|
||||
/**
|
||||
* Start UI test from a workflow in a new project
|
||||
* Start UI test from a workflow in a new project on a new canvas
|
||||
*/
|
||||
async fromNewProject() {
|
||||
async fromNewProjectBlankCanvas() {
|
||||
// Enable features to allow us to create a new project
|
||||
await this.n8n.api.enableFeature('projectRole:admin');
|
||||
await this.n8n.api.enableFeature('projectRole:editor');
|
||||
@@ -43,6 +43,20 @@ export class TestEntryComposer {
|
||||
return projectId;
|
||||
}
|
||||
|
||||
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.navigate.toProject(projectId);
|
||||
return projectId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start UI test from the canvas of an imported workflow
|
||||
* Returns the workflow import result for use in the test
|
||||
|
||||
216
packages/testing/playwright/helpers/NavigationHelper.ts
Normal file
216
packages/testing/playwright/helpers/NavigationHelper.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* NavigationHelper provides centralized navigation methods for all n8n routes.
|
||||
* Handles both project-specific and global routes with proper URL construction.
|
||||
*
|
||||
* URLs are documented to help users understand where they're navigating:
|
||||
* - Home workflows: /home/workflows
|
||||
* - Project workflows: /projects/{projectId}/workflows
|
||||
* - Variables: /variables (global only, no project scope)
|
||||
* - Settings: /settings (global only)
|
||||
* - Credentials: /home/credentials or /projects/{projectId}/credentials
|
||||
* - Executions: /home/executions or /projects/{projectId}/executions
|
||||
*/
|
||||
export class NavigationHelper {
|
||||
constructor(private page: Page) {}
|
||||
|
||||
/**
|
||||
* Navigate to the home dashboard
|
||||
* URL: /home
|
||||
*/
|
||||
async toHome(): Promise<void> {
|
||||
await this.page.goto('/home');
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to workflows page
|
||||
* URLs:
|
||||
* - Home workflows: /home/workflows
|
||||
* - Project workflows: /projects/{projectId}/workflows
|
||||
*/
|
||||
async toWorkflows(projectId?: string): Promise<void> {
|
||||
const url = projectId ? `/projects/${projectId}/workflows` : '/home/workflows';
|
||||
await this.page.goto(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to credentials page
|
||||
* URLs:
|
||||
* - Home credentials: /home/credentials
|
||||
* - Project credentials: /projects/{projectId}/credentials
|
||||
*/
|
||||
async toCredentials(projectId?: string): Promise<void> {
|
||||
const url = projectId ? `/projects/${projectId}/credentials` : '/home/credentials';
|
||||
await this.page.goto(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to executions page
|
||||
* URLs:
|
||||
* - Home executions: /home/executions
|
||||
* - Project executions: /projects/{projectId}/executions
|
||||
*/
|
||||
async toExecutions(projectId?: string): Promise<void> {
|
||||
const url = projectId ? `/projects/${projectId}/executions` : '/home/executions';
|
||||
await this.page.goto(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to variables page (global only)
|
||||
* URL: /variables
|
||||
* Note: Variables are global and don't have project-specific scoping
|
||||
*/
|
||||
async toVariables(): Promise<void> {
|
||||
await this.page.goto('/variables');
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to settings page (global only)
|
||||
* URL: /settings
|
||||
*/
|
||||
async toSettings(): Promise<void> {
|
||||
await this.page.goto('/settings');
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to personal settings
|
||||
* URL: /settings/personal
|
||||
*/
|
||||
async toPersonalSettings(): Promise<void> {
|
||||
await this.page.goto('/settings/personal');
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to projects page
|
||||
* URL: /projects
|
||||
*/
|
||||
async toProjects(): Promise<void> {
|
||||
await this.page.goto('/projects');
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to a specific project's dashboard
|
||||
* URL: /projects/{projectId}
|
||||
*/
|
||||
async toProject(projectId: string): Promise<void> {
|
||||
await this.page.goto(`/projects/${projectId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to project settings
|
||||
* URL: /projects/{projectId}/settings
|
||||
*/
|
||||
async toProjectSettings(projectId: string): Promise<void> {
|
||||
await this.page.goto(`/projects/${projectId}/settings`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to a specific workflow
|
||||
* URLs:
|
||||
* - New workflow: /workflow/new
|
||||
* - Existing workflow: /workflow/{workflowId}
|
||||
* - Project workflow: /projects/{projectId}/workflow/{workflowId}
|
||||
*/
|
||||
async toWorkflow(workflowId: string = 'new', projectId?: string): Promise<void> {
|
||||
const url = projectId
|
||||
? `/projects/${projectId}/workflow/${workflowId}`
|
||||
: `/workflow/${workflowId}`;
|
||||
await this.page.goto(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to workflow canvas (alias for toWorkflow)
|
||||
*/
|
||||
async toCanvas(workflowId: string = 'new', projectId?: string): Promise<void> {
|
||||
await this.toWorkflow(workflowId, projectId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to templates page
|
||||
* URL: /templates
|
||||
*/
|
||||
async toTemplates(): Promise<void> {
|
||||
await this.page.goto('/templates');
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to a specific template
|
||||
* URL: /templates/{templateId}
|
||||
*/
|
||||
async toTemplate(templateId: string): Promise<void> {
|
||||
await this.page.goto(`/templates/${templateId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to community nodes
|
||||
* URL: /settings/community-nodes
|
||||
*/
|
||||
async toCommunityNodes(): Promise<void> {
|
||||
await this.page.goto('/settings/community-nodes');
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to log streaming settings
|
||||
* URL: /settings/log-streaming
|
||||
*/
|
||||
async toLogStreaming(): Promise<void> {
|
||||
await this.page.goto('/settings/log-streaming');
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to worker view
|
||||
* URL: /settings/workers
|
||||
*/
|
||||
async toWorkerView(): Promise<void> {
|
||||
await this.page.goto('/settings/workers');
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to users management
|
||||
* URL: /settings/users
|
||||
*/
|
||||
async toUsers(): Promise<void> {
|
||||
await this.page.goto('/settings/users');
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to API settings
|
||||
* URL: /settings/api
|
||||
*/
|
||||
async toApiSettings(): Promise<void> {
|
||||
await this.page.goto('/settings/api');
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to LDAP settings
|
||||
* URL: /settings/ldap
|
||||
*/
|
||||
async toLdapSettings(): Promise<void> {
|
||||
await this.page.goto('/settings/ldap');
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to SSO settings
|
||||
* URL: /settings/sso
|
||||
*/
|
||||
async toSsoSettings(): Promise<void> {
|
||||
await this.page.goto('/settings/sso');
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to source control settings
|
||||
* URL: /settings/source-control
|
||||
*/
|
||||
async toSourceControl(): Promise<void> {
|
||||
await this.page.goto('/settings/source-control');
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to external secrets settings
|
||||
* URL: /settings/external-secrets
|
||||
*/
|
||||
async toExternalSecrets(): Promise<void> {
|
||||
await this.page.goto('/settings/external-secrets');
|
||||
}
|
||||
}
|
||||
@@ -76,6 +76,10 @@ export class CanvasPage extends BasePage {
|
||||
await this.nodeCreatorItemByName(text).click();
|
||||
}
|
||||
|
||||
async clickAddToWorkflowButton(): Promise<void> {
|
||||
await this.page.getByText('Add to workflow').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a node to the canvas with flexible options
|
||||
* @param nodeName - The name of the node to search for and add
|
||||
|
||||
121
packages/testing/playwright/pages/CommunityNodesPage.ts
Normal file
121
packages/testing/playwright/pages/CommunityNodesPage.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
|
||||
import { BasePage } from './BasePage';
|
||||
|
||||
export class CommunityNodesPage extends BasePage {
|
||||
// Element getters
|
||||
getCommunityCards(): Locator {
|
||||
return this.page.getByTestId('community-package-card');
|
||||
}
|
||||
|
||||
getActionBox(): Locator {
|
||||
return this.page.getByTestId('action-box');
|
||||
}
|
||||
|
||||
getInstallButton(): Locator {
|
||||
// Try action box first (empty state), fallback to header install button
|
||||
const actionBoxButton = this.getActionBox().locator('button');
|
||||
const headerInstallButton = this.page.getByRole('button', { name: 'Install' });
|
||||
|
||||
return actionBoxButton.or(headerInstallButton);
|
||||
}
|
||||
|
||||
getInstallModal(): Locator {
|
||||
return this.page.getByTestId('communityPackageInstall-modal');
|
||||
}
|
||||
|
||||
getConfirmModal(): Locator {
|
||||
return this.page.getByTestId('communityPackageManageConfirm-modal');
|
||||
}
|
||||
|
||||
getPackageNameInput(): Locator {
|
||||
return this.getInstallModal().locator('input').first();
|
||||
}
|
||||
|
||||
getUserAgreementCheckbox(): Locator {
|
||||
return this.page.getByTestId('user-agreement-checkbox');
|
||||
}
|
||||
|
||||
getInstallPackageButton(): Locator {
|
||||
return this.page.getByTestId('install-community-package-button');
|
||||
}
|
||||
|
||||
getActionToggle(): Locator {
|
||||
return this.page.getByTestId('action-toggle');
|
||||
}
|
||||
|
||||
getUninstallAction(): Locator {
|
||||
return this.page.getByTestId('action-uninstall');
|
||||
}
|
||||
|
||||
getUpdateButton(): Locator {
|
||||
return this.getCommunityCards().first().locator('button');
|
||||
}
|
||||
|
||||
getConfirmUpdateButton(): Locator {
|
||||
return this.getConfirmModal().getByRole('button', { name: 'Confirm update' });
|
||||
}
|
||||
|
||||
getConfirmUninstallButton(): Locator {
|
||||
return this.getConfirmModal().getByRole('button', { name: 'Confirm uninstall' });
|
||||
}
|
||||
|
||||
// Simple actions
|
||||
async clickInstallButton(): Promise<void> {
|
||||
await this.getInstallButton().click();
|
||||
}
|
||||
|
||||
async fillPackageName(packageName: string): Promise<void> {
|
||||
await this.getPackageNameInput().fill(packageName);
|
||||
}
|
||||
|
||||
async clickUserAgreementCheckbox(): Promise<void> {
|
||||
await this.getUserAgreementCheckbox().click();
|
||||
}
|
||||
|
||||
async clickInstallPackageButton(): Promise<void> {
|
||||
await this.getInstallPackageButton().click();
|
||||
}
|
||||
|
||||
async clickActionToggle(): Promise<void> {
|
||||
await this.getActionToggle().click();
|
||||
}
|
||||
|
||||
async clickUninstallAction(): Promise<void> {
|
||||
await this.getUninstallAction().click();
|
||||
}
|
||||
|
||||
async clickUpdateButton(): Promise<void> {
|
||||
await this.getUpdateButton().click();
|
||||
}
|
||||
|
||||
async clickConfirmUpdate(): Promise<void> {
|
||||
await this.getConfirmUpdateButton().click();
|
||||
}
|
||||
|
||||
async clickConfirmUninstall(): Promise<void> {
|
||||
await this.getConfirmUninstallButton().click();
|
||||
}
|
||||
|
||||
// Helper methods for common workflows
|
||||
async installPackage(packageName: string): Promise<void> {
|
||||
await this.clickInstallButton();
|
||||
await this.fillPackageName(packageName);
|
||||
await this.clickUserAgreementCheckbox();
|
||||
await this.clickInstallPackageButton();
|
||||
|
||||
// Wait for install modal to close
|
||||
await this.getInstallModal().waitFor({ state: 'hidden' });
|
||||
}
|
||||
|
||||
async updatePackage(): Promise<void> {
|
||||
await this.clickUpdateButton();
|
||||
await this.clickConfirmUpdate();
|
||||
}
|
||||
|
||||
async uninstallPackage(): Promise<void> {
|
||||
await this.clickActionToggle();
|
||||
await this.clickUninstallAction();
|
||||
await this.clickConfirmUninstall();
|
||||
}
|
||||
}
|
||||
@@ -725,4 +725,7 @@ export class NodeDetailsViewPage extends BasePage {
|
||||
getInputSelect() {
|
||||
return this.page.getByTestId('ndv-input-select').locator('input');
|
||||
}
|
||||
getCredentialLabel(credentialType: string) {
|
||||
return this.page.getByText(credentialType);
|
||||
}
|
||||
}
|
||||
|
||||
96
packages/testing/playwright/pages/VariablesPage.ts
Normal file
96
packages/testing/playwright/pages/VariablesPage.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { expect, type Locator } from '@playwright/test';
|
||||
|
||||
import { BasePage } from './BasePage';
|
||||
|
||||
export class VariablesPage extends BasePage {
|
||||
getUnavailableResourcesList() {
|
||||
return this.page.getByTestId('unavailable-resources-list');
|
||||
}
|
||||
|
||||
getResourcesList() {
|
||||
return this.page.getByTestId('resources-list');
|
||||
}
|
||||
|
||||
getEmptyResourcesList() {
|
||||
return this.page.getByTestId('empty-resources-list');
|
||||
}
|
||||
|
||||
getEmptyResourcesListNewVariableButton() {
|
||||
return this.getEmptyResourcesList().locator('button');
|
||||
}
|
||||
|
||||
getSearchBar() {
|
||||
return this.page.getByTestId('resources-list-search');
|
||||
}
|
||||
|
||||
getCreateVariableButton() {
|
||||
return this.page.getByTestId('resources-list-add');
|
||||
}
|
||||
|
||||
getVariablesRows() {
|
||||
return this.page.getByTestId('variables-row');
|
||||
}
|
||||
|
||||
getVariablesEditableRows() {
|
||||
return this.page.getByTestId('variables-row').filter({ has: this.page.locator('input') });
|
||||
}
|
||||
|
||||
getVariableRow(key: string) {
|
||||
return this.getVariablesRows().filter({ hasText: key });
|
||||
}
|
||||
|
||||
getEditableRowCancelButton(row: Locator) {
|
||||
return row.getByTestId('variable-row-cancel-button');
|
||||
}
|
||||
|
||||
getEditableRowSaveButton(row: Locator) {
|
||||
return row.getByTestId('variable-row-save-button');
|
||||
}
|
||||
|
||||
async createVariable(key: string, value: string) {
|
||||
await this.getCreateVariableButton().click();
|
||||
|
||||
const editingRow = this.getVariablesEditableRows().first();
|
||||
await this.setRowValue(editingRow, 'key', key);
|
||||
await this.setRowValue(editingRow, 'value', value);
|
||||
await this.saveRowEditing(editingRow);
|
||||
}
|
||||
|
||||
async createVariableFromEmptyState(key: string, value: string) {
|
||||
await this.getEmptyResourcesListNewVariableButton().click();
|
||||
|
||||
const editingRow = this.getVariablesEditableRows().first();
|
||||
await this.setRowValue(editingRow, 'key', key);
|
||||
await this.setRowValue(editingRow, 'value', value);
|
||||
await this.saveRowEditing(editingRow);
|
||||
}
|
||||
|
||||
async deleteVariable(key: string) {
|
||||
const row = this.getVariableRow(key);
|
||||
await row.getByTestId('variable-row-delete-button').click();
|
||||
|
||||
// Use a more specific selector to avoid strict mode violation with other dialogs
|
||||
const modal = this.page.getByRole('dialog').filter({ hasText: 'Delete variable' });
|
||||
await expect(modal).toBeVisible();
|
||||
await modal.locator('.btn--confirm').click();
|
||||
}
|
||||
|
||||
async editRow(key: string) {
|
||||
const row = this.getVariableRow(key);
|
||||
await row.getByTestId('variable-row-edit-button').click();
|
||||
}
|
||||
|
||||
async setRowValue(row: Locator, field: 'key' | 'value', value: string) {
|
||||
const input = row.getByTestId(`variable-row-${field}-input`).locator('input, textarea');
|
||||
await input.selectText();
|
||||
await input.fill(value);
|
||||
}
|
||||
|
||||
async saveRowEditing(row: Locator) {
|
||||
await this.getEditableRowSaveButton(row).click();
|
||||
}
|
||||
|
||||
async cancelRowEditing(row: Locator) {
|
||||
await this.getEditableRowCancelButton(row).click();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import type { Page } from '@playwright/test';
|
||||
import { AIAssistantPage } from './AIAssistantPage';
|
||||
import { BecomeCreatorCTAPage } from './BecomeCreatorCTAPage';
|
||||
import { CanvasPage } from './CanvasPage';
|
||||
import { CommunityNodesPage } from './CommunityNodesPage';
|
||||
import { CredentialsPage } from './CredentialsPage';
|
||||
import { DemoPage } from './DemoPage';
|
||||
import { ExecutionsPage } from './ExecutionsPage';
|
||||
@@ -14,6 +15,7 @@ import { NpsSurveyPage } from './NpsSurveyPage';
|
||||
import { ProjectSettingsPage } from './ProjectSettingsPage';
|
||||
import { SettingsPage } from './SettingsPage';
|
||||
import { SidebarPage } from './SidebarPage';
|
||||
import { VariablesPage } from './VariablesPage';
|
||||
import { VersionsPage } from './VersionsPage';
|
||||
import { WorkerViewPage } from './WorkerViewPage';
|
||||
import { WorkflowActivationModal } from './WorkflowActivationModal';
|
||||
@@ -24,6 +26,7 @@ import { CanvasComposer } from '../composables/CanvasComposer';
|
||||
import { ProjectComposer } from '../composables/ProjectComposer';
|
||||
import { TestEntryComposer } from '../composables/TestEntryComposer';
|
||||
import { WorkflowComposer } from '../composables/WorkflowComposer';
|
||||
import { NavigationHelper } from '../helpers/NavigationHelper';
|
||||
import type { ApiHelpers } from '../services/api-helper';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
@@ -35,6 +38,7 @@ export class n8nPage {
|
||||
readonly aiAssistant: AIAssistantPage;
|
||||
readonly becomeCreatorCTA: BecomeCreatorCTAPage;
|
||||
readonly canvas: CanvasPage;
|
||||
readonly communityNodes: CommunityNodesPage;
|
||||
readonly demo: DemoPage;
|
||||
readonly iframe: IframePage;
|
||||
readonly interactions: InteractionsPage;
|
||||
@@ -42,6 +46,7 @@ export class n8nPage {
|
||||
readonly npsSurvey: NpsSurveyPage;
|
||||
readonly projectSettings: ProjectSettingsPage;
|
||||
readonly settings: SettingsPage;
|
||||
readonly variables: VariablesPage;
|
||||
readonly versions: VersionsPage;
|
||||
readonly workerView: WorkerViewPage;
|
||||
readonly workflows: WorkflowsPage;
|
||||
@@ -61,6 +66,9 @@ export class n8nPage {
|
||||
readonly canvasComposer: CanvasComposer;
|
||||
readonly start: TestEntryComposer;
|
||||
|
||||
// Helpers
|
||||
readonly navigate: NavigationHelper;
|
||||
|
||||
constructor(page: Page, api: ApiHelpers) {
|
||||
this.page = page;
|
||||
this.api = api;
|
||||
@@ -69,6 +77,7 @@ export class n8nPage {
|
||||
this.aiAssistant = new AIAssistantPage(page);
|
||||
this.becomeCreatorCTA = new BecomeCreatorCTAPage(page);
|
||||
this.canvas = new CanvasPage(page);
|
||||
this.communityNodes = new CommunityNodesPage(page);
|
||||
this.demo = new DemoPage(page);
|
||||
this.iframe = new IframePage(page);
|
||||
this.interactions = new InteractionsPage(page);
|
||||
@@ -76,6 +85,7 @@ export class n8nPage {
|
||||
this.npsSurvey = new NpsSurveyPage(page);
|
||||
this.projectSettings = new ProjectSettingsPage(page);
|
||||
this.settings = new SettingsPage(page);
|
||||
this.variables = new VariablesPage(page);
|
||||
this.versions = new VersionsPage(page);
|
||||
this.workerView = new WorkerViewPage(page);
|
||||
this.workflows = new WorkflowsPage(page);
|
||||
@@ -94,6 +104,9 @@ export class n8nPage {
|
||||
this.projectComposer = new ProjectComposer(this);
|
||||
this.canvasComposer = new CanvasComposer(this);
|
||||
this.start = new TestEntryComposer(this);
|
||||
|
||||
// Helpers
|
||||
this.navigate = new NavigationHelper(page);
|
||||
}
|
||||
|
||||
async goHome() {
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { TestError } from '../Types';
|
||||
import { CredentialApiHelper } from './credential-api-helper';
|
||||
import { ProjectApiHelper } from './project-api-helper';
|
||||
import { VariablesApiHelper } from './variables-api-helper';
|
||||
import { WorkflowApiHelper } from './workflow-api-helper';
|
||||
|
||||
export interface LoginResponseData {
|
||||
@@ -37,12 +38,14 @@ export class ApiHelpers {
|
||||
workflowApi: WorkflowApiHelper;
|
||||
projectApi: ProjectApiHelper;
|
||||
credentialApi: CredentialApiHelper;
|
||||
variablesApi: VariablesApiHelper;
|
||||
|
||||
constructor(requestContext: APIRequestContext) {
|
||||
this.request = requestContext;
|
||||
this.workflowApi = new WorkflowApiHelper(this);
|
||||
this.projectApi = new ProjectApiHelper(this);
|
||||
this.credentialApi = new CredentialApiHelper(this);
|
||||
this.variablesApi = new VariablesApiHelper(this);
|
||||
}
|
||||
|
||||
// ===== MAIN SETUP METHODS =====
|
||||
|
||||
123
packages/testing/playwright/services/variables-api-helper.ts
Normal file
123
packages/testing/playwright/services/variables-api-helper.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import type { ApiHelpers } from './api-helper';
|
||||
import { TestError } from '../Types';
|
||||
|
||||
interface VariableResponse {
|
||||
id: string;
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface CreateVariableDto {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface UpdateVariableDto {
|
||||
key?: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export class VariablesApiHelper {
|
||||
constructor(private api: ApiHelpers) {}
|
||||
|
||||
/**
|
||||
* Create a new variable
|
||||
*/
|
||||
async createVariable(variable: CreateVariableDto): Promise<VariableResponse> {
|
||||
const response = await this.api.request.post('/rest/variables', { data: variable });
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new TestError(`Failed to create variable: ${await response.text()}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return result.data ?? result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all variables
|
||||
*/
|
||||
async getAllVariables(): Promise<VariableResponse[]> {
|
||||
const response = await this.api.request.get('/rest/variables');
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new TestError(`Failed to get variables: ${await response.text()}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return result.data ?? result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a variable by ID
|
||||
*/
|
||||
async getVariable(id: string): Promise<VariableResponse> {
|
||||
const response = await this.api.request.get(`/rest/variables/${id}`);
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new TestError(`Failed to get variable: ${await response.text()}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return result.data ?? result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a variable by ID
|
||||
*/
|
||||
async updateVariable(id: string, updates: UpdateVariableDto): Promise<VariableResponse> {
|
||||
const response = await this.api.request.patch(`/rest/variables/${id}`, { data: updates });
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new TestError(`Failed to update variable: ${await response.text()}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return result.data ?? result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a variable by ID
|
||||
*/
|
||||
async deleteVariable(id: string): Promise<void> {
|
||||
const response = await this.api.request.delete(`/rest/variables/${id}`);
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new TestError(`Failed to delete variable: ${await response.text()}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all variables (useful for test cleanup)
|
||||
*/
|
||||
async deleteAllVariables(): Promise<void> {
|
||||
const variables = await this.getAllVariables();
|
||||
|
||||
// Delete variables in parallel for better performance
|
||||
await Promise.all(variables.map((variable) => this.deleteVariable(variable.id)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a test variable with a unique key
|
||||
*/
|
||||
async createTestVariable(
|
||||
keyPrefix: string = 'TEST_VAR',
|
||||
value: string = 'test_value',
|
||||
): Promise<VariableResponse> {
|
||||
const key = `${keyPrefix}_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
|
||||
return await this.createVariable({ key, value });
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up variables by key pattern (useful for test cleanup)
|
||||
*/
|
||||
async cleanupTestVariables(keyPattern?: string): Promise<void> {
|
||||
const variables = await this.getAllVariables();
|
||||
|
||||
const variablesToDelete = keyPattern
|
||||
? variables.filter((variable) => variable.key.includes(keyPattern))
|
||||
: variables.filter((variable) => variable.key.startsWith('TEST_'));
|
||||
|
||||
await Promise.all(variablesToDelete.map((variable) => this.deleteVariable(variable.id)));
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from '../../utils/performance-helper';
|
||||
|
||||
async function setupPerformanceTest(n8n: n8nPage, size: number) {
|
||||
await n8n.start.fromNewProject();
|
||||
await n8n.start.fromNewProjectBlankCanvas();
|
||||
await n8n.canvas.importWorkflow('large.json', 'Large Workflow');
|
||||
await n8n.notifications.closeNotificationByText('Successful');
|
||||
|
||||
|
||||
169
packages/testing/playwright/tests/ui/21-community-nodes.spec.ts
Normal file
169
packages/testing/playwright/tests/ui/21-community-nodes.spec.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import { MANUAL_TRIGGER_NODE_NAME } from '../../config/constants';
|
||||
import { test, expect } from '../../fixtures/base';
|
||||
import customCredential from '../../workflows/Custom_credential.json';
|
||||
import customNodeFixture from '../../workflows/Custom_node.json';
|
||||
import customNodeWithCustomCredentialFixture from '../../workflows/Custom_node_custom_credential.json';
|
||||
import customNodeWithN8nCredentialFixture from '../../workflows/Custom_node_n8n_credential.json';
|
||||
|
||||
const CUSTOM_NODE_NAME = 'E2E Node';
|
||||
const CUSTOM_NODE_WITH_N8N_CREDENTIAL = 'E2E Node with native n8n credential';
|
||||
const CUSTOM_NODE_WITH_CUSTOM_CREDENTIAL = 'E2E Node with custom credential';
|
||||
const MOCK_PACKAGE = {
|
||||
createdAt: '2024-07-22T19:08:06.505Z',
|
||||
updatedAt: '2024-07-22T19:08:06.505Z',
|
||||
packageName: 'n8n-nodes-chatwork',
|
||||
installedVersion: '1.0.0',
|
||||
authorName: null,
|
||||
authorEmail: null,
|
||||
installedNodes: [
|
||||
{
|
||||
name: 'Chatwork',
|
||||
type: 'n8n-nodes-chatwork.chatwork',
|
||||
latestVersion: 1,
|
||||
},
|
||||
],
|
||||
updateAvailable: '1.1.2',
|
||||
};
|
||||
|
||||
test.describe('Community and custom nodes in canvas', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.route('/types/nodes.json', async (route) => {
|
||||
const response = await route.fetch();
|
||||
const nodes = await response.json();
|
||||
nodes.push(
|
||||
customNodeFixture,
|
||||
customNodeWithN8nCredentialFixture,
|
||||
customNodeWithCustomCredentialFixture,
|
||||
);
|
||||
await route.fulfill({
|
||||
response,
|
||||
json: nodes,
|
||||
headers: { 'cache-control': 'no-cache, no-store' },
|
||||
});
|
||||
});
|
||||
|
||||
await page.route('/types/credentials.json', async (route) => {
|
||||
const response = await route.fetch();
|
||||
const credentials = await response.json();
|
||||
credentials.push(customCredential);
|
||||
await route.fulfill({
|
||||
response,
|
||||
json: credentials,
|
||||
headers: { 'cache-control': 'no-cache, no-store' },
|
||||
});
|
||||
});
|
||||
|
||||
await page.route('/community-node-types', async (route) => {
|
||||
await route.fulfill({ status: 200, json: { data: [] } });
|
||||
});
|
||||
|
||||
await page.route('**/community-node-types/*', async (route) => {
|
||||
await route.fulfill({ status: 200, json: null });
|
||||
});
|
||||
|
||||
await page.route('https://registry.npmjs.org/*', async (route) => {
|
||||
await route.fulfill({ status: 404, json: {} });
|
||||
});
|
||||
});
|
||||
|
||||
test('should render and select community node', async ({ n8n }) => {
|
||||
await n8n.start.fromBlankCanvas();
|
||||
|
||||
await n8n.canvas.clickCanvasPlusButton();
|
||||
await n8n.canvas.fillNodeCreatorSearchBar(CUSTOM_NODE_NAME);
|
||||
await n8n.canvas.clickNodeCreatorItemName(CUSTOM_NODE_NAME);
|
||||
await n8n.canvas.clickAddToWorkflowButton();
|
||||
|
||||
await expect(n8n.ndv.getNodeParameters()).toBeVisible();
|
||||
|
||||
await expect(n8n.ndv.getParameterInputField('testProp')).toHaveValue('Some default');
|
||||
await expect(n8n.ndv.getParameterInputField('resource')).toHaveValue('option2');
|
||||
|
||||
await n8n.ndv.selectOptionInParameterDropdown('resource', 'option4');
|
||||
await expect(n8n.ndv.getParameterInputField('resource')).toHaveValue('option4');
|
||||
});
|
||||
|
||||
test('should render custom node with n8n credential', async ({ n8n }) => {
|
||||
await n8n.start.fromBlankCanvas();
|
||||
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
|
||||
|
||||
await n8n.canvas.clickNodeCreatorPlusButton();
|
||||
await n8n.canvas.fillNodeCreatorSearchBar(CUSTOM_NODE_WITH_N8N_CREDENTIAL);
|
||||
await n8n.canvas.clickNodeCreatorItemName(CUSTOM_NODE_WITH_N8N_CREDENTIAL);
|
||||
await n8n.canvas.clickAddToWorkflowButton();
|
||||
|
||||
await n8n.page.getByTestId('credentials-label').click();
|
||||
await n8n.page.getByTestId('node-credentials-select-item-new').click();
|
||||
|
||||
await expect(n8n.page.getByTestId('editCredential-modal')).toContainText('Notion API');
|
||||
});
|
||||
|
||||
test('should render custom node with custom credential', async ({ n8n }) => {
|
||||
await n8n.start.fromBlankCanvas();
|
||||
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
|
||||
|
||||
await n8n.canvas.clickNodeCreatorPlusButton();
|
||||
await n8n.canvas.fillNodeCreatorSearchBar(CUSTOM_NODE_WITH_CUSTOM_CREDENTIAL);
|
||||
await n8n.canvas.clickNodeCreatorItemName(CUSTOM_NODE_WITH_CUSTOM_CREDENTIAL);
|
||||
await n8n.canvas.clickAddToWorkflowButton();
|
||||
|
||||
await n8n.page.getByTestId('credentials-label').click();
|
||||
await n8n.page.getByTestId('node-credentials-select-item-new').click();
|
||||
|
||||
await expect(n8n.page.getByTestId('editCredential-modal')).toContainText(
|
||||
'Custom E2E Credential',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Community nodes management', () => {
|
||||
test('can install, update and uninstall community nodes', async ({ n8n, page }) => {
|
||||
await page.route('**/api.npms.io/v2/search*', async (route) => {
|
||||
await route.fulfill({ status: 200, json: {} });
|
||||
});
|
||||
|
||||
await page.route('/rest/community-packages', async (route) => {
|
||||
if (route.request().method() === 'GET') {
|
||||
await route.fulfill({ status: 200, json: { data: [] } });
|
||||
}
|
||||
});
|
||||
|
||||
await n8n.navigate.toCommunityNodes();
|
||||
|
||||
await page.route('/rest/community-packages', async (route) => {
|
||||
if (route.request().method() === 'POST') {
|
||||
await route.fulfill({ status: 200, json: { data: MOCK_PACKAGE } });
|
||||
} else if (route.request().method() === 'GET') {
|
||||
await route.fulfill({ status: 200, json: { data: [MOCK_PACKAGE] } });
|
||||
}
|
||||
});
|
||||
|
||||
await n8n.communityNodes.installPackage('n8n-nodes-chatwork@1.0.0');
|
||||
await expect(n8n.communityNodes.getCommunityCards()).toHaveCount(1);
|
||||
await expect(n8n.communityNodes.getCommunityCards().first()).toContainText('v1.0.0');
|
||||
|
||||
const updatedPackage = {
|
||||
...MOCK_PACKAGE,
|
||||
installedVersion: '1.2.0',
|
||||
updateAvailable: undefined,
|
||||
};
|
||||
await page.route('/rest/community-packages', async (route) => {
|
||||
if (route.request().method() === 'PATCH') {
|
||||
await route.fulfill({ status: 200, json: { data: updatedPackage } });
|
||||
}
|
||||
});
|
||||
|
||||
await n8n.communityNodes.updatePackage();
|
||||
await expect(n8n.communityNodes.getCommunityCards()).toHaveCount(1);
|
||||
await expect(n8n.communityNodes.getCommunityCards().first()).not.toContainText('v1.0.0');
|
||||
|
||||
await page.route('/rest/community-packages*', async (route) => {
|
||||
if (route.request().method() === 'DELETE') {
|
||||
await route.fulfill({ status: 204 });
|
||||
}
|
||||
});
|
||||
|
||||
await n8n.communityNodes.uninstallPackage();
|
||||
await expect(n8n.communityNodes.getActionBox()).toBeVisible();
|
||||
});
|
||||
});
|
||||
163
packages/testing/playwright/tests/ui/23-variables.spec.ts
Normal file
163
packages/testing/playwright/tests/ui/23-variables.spec.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { customAlphabet } from 'nanoid';
|
||||
|
||||
import { test, expect } from '../../fixtures/base';
|
||||
|
||||
const generateValidId = customAlphabet(
|
||||
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_',
|
||||
8,
|
||||
);
|
||||
|
||||
test.describe('Variables', () => {
|
||||
// These tests are serial since it's at an instance level and they interact with the same variables
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
test.describe('unlicensed', () => {
|
||||
test('should show the unlicensed action box when the feature is disabled', async ({
|
||||
n8n,
|
||||
api,
|
||||
}) => {
|
||||
await api.disableFeature('variables');
|
||||
await n8n.navigate.toVariables();
|
||||
await expect(n8n.variables.getUnavailableResourcesList()).toBeVisible();
|
||||
await expect(n8n.variables.getResourcesList()).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('licensed', () => {
|
||||
test.beforeEach(async ({ n8n, api }) => {
|
||||
await api.enableFeature('variables');
|
||||
await api.variablesApi.deleteAllVariables();
|
||||
await n8n.navigate.toVariables();
|
||||
});
|
||||
|
||||
test('should create a new variable using empty state', async ({ n8n }) => {
|
||||
const key = `ENV_VAR_${generateValidId()}`;
|
||||
const value = 'test_value';
|
||||
|
||||
await n8n.variables.createVariableFromEmptyState(key, value);
|
||||
|
||||
const variableRow = n8n.variables.getVariableRow(key);
|
||||
await expect(variableRow).toContainText(value);
|
||||
await expect(variableRow).toBeVisible();
|
||||
await expect(n8n.variables.getVariablesRows()).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('should create multiple variables', async ({ n8n }) => {
|
||||
const key1 = `ENV_VAR_NEW_${generateValidId()}`;
|
||||
const value1 = 'test_value_1';
|
||||
await n8n.variables.createVariableFromEmptyState(key1, value1);
|
||||
await expect(n8n.variables.getVariablesRows()).toHaveCount(1);
|
||||
|
||||
const key2 = `ENV_EXAMPLE_${generateValidId()}`;
|
||||
const value2 = 'test_value_2';
|
||||
await n8n.variables.createVariable(key2, value2);
|
||||
|
||||
await expect(n8n.variables.getVariablesRows()).toHaveCount(2);
|
||||
|
||||
const variableRow1 = n8n.variables.getVariableRow(key1);
|
||||
await expect(variableRow1).toContainText(value1);
|
||||
await expect(variableRow1).toBeVisible();
|
||||
|
||||
const variableRow2 = n8n.variables.getVariableRow(key2);
|
||||
await expect(variableRow2).toContainText(value2);
|
||||
await expect(variableRow2).toBeVisible();
|
||||
});
|
||||
|
||||
test('should get validation errors and cancel variable creation', async ({ n8n }) => {
|
||||
await n8n.variables.createVariableFromEmptyState(
|
||||
`ENV_BASE_${generateValidId()}`,
|
||||
'base_value',
|
||||
);
|
||||
const initialCount = await n8n.variables.getVariablesRows().count();
|
||||
|
||||
const key = `ENV_VAR_INVALID_${generateValidId()}$`; // Invalid key with special character
|
||||
const value = 'test_value';
|
||||
|
||||
await n8n.variables.getCreateVariableButton().click();
|
||||
const editingRow = n8n.variables.getVariablesEditableRows().first();
|
||||
await n8n.variables.setRowValue(editingRow, 'key', key);
|
||||
await n8n.variables.setRowValue(editingRow, 'value', value);
|
||||
await n8n.variables.saveRowEditing(editingRow);
|
||||
|
||||
await expect(editingRow).toContainText(
|
||||
'This field may contain only letters, numbers, and underscores',
|
||||
);
|
||||
|
||||
await n8n.variables.cancelRowEditing(editingRow);
|
||||
await expect(n8n.variables.getVariablesRows()).toHaveCount(initialCount);
|
||||
});
|
||||
|
||||
test('should edit a variable', async ({ n8n }) => {
|
||||
const key = `ENV_VAR_EDIT_${generateValidId()}`;
|
||||
const initialValue = 'initial_value';
|
||||
await n8n.variables.createVariableFromEmptyState(key, initialValue);
|
||||
|
||||
const newValue = 'updated_value';
|
||||
|
||||
await n8n.variables.editRow(key);
|
||||
const editingRow = n8n.variables.getVariablesEditableRows().first();
|
||||
await n8n.variables.setRowValue(editingRow, 'value', newValue);
|
||||
await n8n.variables.saveRowEditing(editingRow);
|
||||
|
||||
const variableRow = n8n.variables.getVariableRow(key);
|
||||
await expect(variableRow).toContainText(newValue);
|
||||
await expect(variableRow).toBeVisible();
|
||||
});
|
||||
|
||||
test('should delete a variable', async ({ n8n }) => {
|
||||
const key = `TO_DELETE_${generateValidId()}`;
|
||||
const value = 'delete_test_value';
|
||||
|
||||
await n8n.variables.createVariableFromEmptyState(key, value);
|
||||
const initialCount = await n8n.variables.getVariablesRows().count();
|
||||
|
||||
await n8n.variables.deleteVariable(key);
|
||||
|
||||
await expect(n8n.variables.getVariablesRows()).toHaveCount(initialCount - 1);
|
||||
|
||||
await expect(n8n.variables.getVariableRow(key)).toBeHidden();
|
||||
});
|
||||
|
||||
test('should search for a variable', async ({ n8n, page }) => {
|
||||
const uniqueId = generateValidId();
|
||||
|
||||
const key1 = `SEARCH_VAR_${uniqueId}`;
|
||||
const key2 = `SEARCH_VAR_NEW_${uniqueId}`;
|
||||
const key3 = `SEARCH_EXAMPLE_${uniqueId}`;
|
||||
|
||||
await n8n.variables.createVariableFromEmptyState(key1, 'search_value_1');
|
||||
await n8n.variables.createVariable(key2, 'search_value_2');
|
||||
await n8n.variables.createVariable(key3, 'search_value_3');
|
||||
|
||||
await n8n.variables.getSearchBar().fill('NEW_');
|
||||
await n8n.variables.getSearchBar().press('Enter');
|
||||
await expect(n8n.variables.getVariablesRows()).toHaveCount(1);
|
||||
await expect(n8n.variables.getVariableRow(key2)).toBeVisible();
|
||||
await expect(page).toHaveURL(new RegExp('search=NEW_'));
|
||||
|
||||
await n8n.variables.getSearchBar().clear();
|
||||
await n8n.variables.getSearchBar().fill('SEARCH_VAR_');
|
||||
await n8n.variables.getSearchBar().press('Enter');
|
||||
await expect(n8n.variables.getVariablesRows()).toHaveCount(2);
|
||||
await expect(n8n.variables.getVariableRow(key1)).toBeVisible();
|
||||
await expect(n8n.variables.getVariableRow(key2)).toBeVisible();
|
||||
await expect(page).toHaveURL(new RegExp('search=SEARCH_VAR_'));
|
||||
|
||||
await n8n.variables.getSearchBar().clear();
|
||||
await n8n.variables.getSearchBar().fill('SEARCH_');
|
||||
await n8n.variables.getSearchBar().press('Enter');
|
||||
await expect(n8n.variables.getVariablesRows()).toHaveCount(3);
|
||||
await expect(n8n.variables.getVariableRow(key1)).toBeVisible();
|
||||
await expect(n8n.variables.getVariableRow(key2)).toBeVisible();
|
||||
await expect(n8n.variables.getVariableRow(key3)).toBeVisible();
|
||||
await expect(page).toHaveURL(new RegExp('search=SEARCH_'));
|
||||
|
||||
await n8n.variables.getSearchBar().clear();
|
||||
await n8n.variables.getSearchBar().fill(`NonExistent_${generateValidId()}`);
|
||||
await n8n.variables.getSearchBar().press('Enter');
|
||||
await expect(n8n.variables.getVariablesRows()).toBeHidden();
|
||||
await expect(page).toHaveURL(/search=NonExistent_/);
|
||||
|
||||
await expect(page.getByText('No variables found')).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,7 @@ import { test, expect } from '../../fixtures/base';
|
||||
|
||||
test.describe('OAuth Credentials', () => {
|
||||
test('should create and connect with Google OAuth2', async ({ n8n, page }) => {
|
||||
const projectId = await n8n.start.fromNewProject();
|
||||
const projectId = await n8n.start.fromNewProjectBlankCanvas();
|
||||
await page.goto(`projects/${projectId}/credentials`);
|
||||
await n8n.credentials.emptyListCreateCredentialButton.click();
|
||||
await n8n.credentials.openNewCredentialDialogFromCredentialList('Google OAuth2 API');
|
||||
|
||||
@@ -259,8 +259,8 @@ test.describe('Logs', () => {
|
||||
const response = await n8n.page.request.get(webhookUrl!);
|
||||
expect(response.status()).toBe(200);
|
||||
|
||||
await expect(n8n.canvas.getNodesWithSpinner()).not.toBeVisible();
|
||||
await expect(n8n.canvas.getWaitingNodes()).not.toBeVisible();
|
||||
await expect(n8n.canvas.getNodesWithSpinner()).toBeHidden();
|
||||
await expect(n8n.canvas.getWaitingNodes()).toBeHidden();
|
||||
await expect(
|
||||
n8n.canvas.logsPanel.getOverviewStatus().filter({ hasText: /Success in [\d.]+m?s/ }),
|
||||
).toBeVisible();
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { test, expect } from '../../fixtures/base';
|
||||
|
||||
test.describe('HTTP Request node', () => {
|
||||
test.beforeEach(async ({ n8n }) => {
|
||||
await n8n.start.fromBlankCanvas();
|
||||
});
|
||||
|
||||
test('should make a request with a URL and receive a response', async ({ n8n }) => {
|
||||
await n8n.canvas.addNode('Manual Trigger');
|
||||
await n8n.canvas.addNode('HTTP Request', { closeNDV: false });
|
||||
|
||||
await n8n.ndv.setupHelper.httpRequest({
|
||||
url: 'https://catfact.ninja/fact',
|
||||
});
|
||||
await n8n.ndv.execute();
|
||||
|
||||
await expect(n8n.ndv.outputPanel.get()).toContainText('fact');
|
||||
});
|
||||
|
||||
test.describe('Credential-only HTTP Request Node variants', () => {
|
||||
test('should render a modified HTTP Request Node', async ({ n8n }) => {
|
||||
await n8n.canvas.addNode('Manual Trigger');
|
||||
await n8n.canvas.addNode('VirusTotal');
|
||||
|
||||
await expect(n8n.ndv.getNodeNameContainer()).toContainText('VirusTotal HTTP Request');
|
||||
await expect(n8n.ndv.getParameterInputField('url')).toHaveValue(
|
||||
'https://www.virustotal.com/api/v3/',
|
||||
);
|
||||
|
||||
await expect(n8n.ndv.getParameterInput('authentication')).toBeHidden();
|
||||
await expect(n8n.ndv.getParameterInput('nodeCredentialType')).toBeHidden();
|
||||
|
||||
await expect(n8n.ndv.getCredentialLabel('Credential for VirusTotal')).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -17,7 +17,7 @@ test.describe('01 - UI Test Entry Points', () => {
|
||||
|
||||
test.describe('Entry Point: Basic Workflow Creation', () => {
|
||||
test('should create a new project and workflow', async ({ n8n }) => {
|
||||
await n8n.start.fromNewProject();
|
||||
await n8n.start.fromNewProjectBlankCanvas();
|
||||
await expect(n8n.canvas.canvasPane()).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
21
packages/testing/playwright/workflows/Custom_credential.json
Normal file
21
packages/testing/playwright/workflows/Custom_credential.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "customE2eCredential",
|
||||
"displayName": "Custom E2E Credential",
|
||||
"properties": [
|
||||
{
|
||||
"displayName": "API Key",
|
||||
"name": "apiKey",
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"required": false
|
||||
}
|
||||
],
|
||||
"authenticate": {
|
||||
"type": "generic",
|
||||
"properties": {
|
||||
"qs": {
|
||||
"auth": "={{$credentials.apiKey}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
51
packages/testing/playwright/workflows/Custom_node.json
Normal file
51
packages/testing/playwright/workflows/Custom_node.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"properties": [
|
||||
{
|
||||
"displayName": "Test property",
|
||||
"name": "testProp",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"noDataExpression": false,
|
||||
"default": "Some default"
|
||||
},
|
||||
{
|
||||
"displayName": "Resource",
|
||||
"name": "resource",
|
||||
"type": "options",
|
||||
"noDataExpression": true,
|
||||
"options": [
|
||||
{
|
||||
"name": "option1",
|
||||
"value": "option1"
|
||||
},
|
||||
{
|
||||
"name": "option2",
|
||||
"value": "option2"
|
||||
},
|
||||
{
|
||||
"name": "option3",
|
||||
"value": "option3"
|
||||
},
|
||||
{
|
||||
"name": "option4",
|
||||
"value": "option4"
|
||||
}
|
||||
],
|
||||
"default": "option2"
|
||||
}
|
||||
],
|
||||
"displayName": "E2E Node",
|
||||
"name": "@e2e/n8n-nodes-e2e",
|
||||
"group": ["transform"],
|
||||
"codex": {
|
||||
"categories": ["Custom Category"]
|
||||
},
|
||||
"version": 1,
|
||||
"description": "Demonstrate rendering of node",
|
||||
"defaults": {
|
||||
"name": "E2E Node "
|
||||
},
|
||||
"inputs": ["main"],
|
||||
"outputs": ["main"],
|
||||
"icon": "fa:network-wired"
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"properties": [
|
||||
{
|
||||
"displayName": "Test property",
|
||||
"name": "testProp",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"noDataExpression": false,
|
||||
"default": "Some default"
|
||||
},
|
||||
{
|
||||
"displayName": "Resource",
|
||||
"name": "resource",
|
||||
"type": "options",
|
||||
"noDataExpression": true,
|
||||
"options": [
|
||||
{
|
||||
"name": "option1",
|
||||
"value": "option1"
|
||||
},
|
||||
{
|
||||
"name": "option2",
|
||||
"value": "option2"
|
||||
},
|
||||
{
|
||||
"name": "option3",
|
||||
"value": "option3"
|
||||
},
|
||||
{
|
||||
"name": "option4",
|
||||
"value": "option4"
|
||||
}
|
||||
],
|
||||
"default": "option2"
|
||||
}
|
||||
],
|
||||
"displayName": "E2E Node with custom credential",
|
||||
"name": "@e2e/n8n-nodes-e2e-custom-credential",
|
||||
"group": ["transform"],
|
||||
"codex": {
|
||||
"categories": ["Custom Category"]
|
||||
},
|
||||
"version": 1,
|
||||
"description": "Demonstrate rendering of node with custom credential",
|
||||
"defaults": {
|
||||
"name": "E2E Node with custom credential"
|
||||
},
|
||||
"inputs": ["main"],
|
||||
"outputs": ["main"],
|
||||
"icon": "fa:network-wired",
|
||||
"credentials": [
|
||||
{
|
||||
"name": "customE2eCredential",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"properties": [
|
||||
{
|
||||
"displayName": "Test property",
|
||||
"name": "testProp",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"noDataExpression": false,
|
||||
"default": "Some default"
|
||||
},
|
||||
{
|
||||
"displayName": "Resource",
|
||||
"name": "resource",
|
||||
"type": "options",
|
||||
"noDataExpression": true,
|
||||
"options": [
|
||||
{
|
||||
"name": "option1",
|
||||
"value": "option1"
|
||||
},
|
||||
{
|
||||
"name": "option2",
|
||||
"value": "option2"
|
||||
},
|
||||
{
|
||||
"name": "option3",
|
||||
"value": "option3"
|
||||
},
|
||||
{
|
||||
"name": "option4",
|
||||
"value": "option4"
|
||||
}
|
||||
],
|
||||
"default": "option2"
|
||||
}
|
||||
],
|
||||
"displayName": "E2E Node with native n8n credential",
|
||||
"name": "@e2e/n8n-nodes-e2e-credential",
|
||||
"group": ["transform"],
|
||||
"codex": {
|
||||
"categories": ["Custom Category"]
|
||||
},
|
||||
"version": 1,
|
||||
"description": "Demonstrate rendering of node with native credential",
|
||||
"defaults": {
|
||||
"name": "E2E Node with native n8n credential"
|
||||
},
|
||||
"inputs": ["main"],
|
||||
"outputs": ["main"],
|
||||
"icon": "fa:network-wired",
|
||||
"credentials": [
|
||||
{
|
||||
"name": "notionApi",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user