mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 09:36:44 +00:00
test: Credential test migration part 1 (#19420)
This commit is contained in:
@@ -1,351 +0,0 @@
|
||||
import { type ICredentialType } from 'n8n-workflow';
|
||||
|
||||
import * as credentialsComposables from '../composables/credentialsComposables';
|
||||
import { getCredentialSaveButton, saveCredential } from '../composables/modals/credential-modal';
|
||||
import {
|
||||
AGENT_NODE_NAME,
|
||||
AI_TOOL_HTTP_NODE_NAME,
|
||||
GMAIL_NODE_NAME,
|
||||
HTTP_REQUEST_NODE_NAME,
|
||||
NEW_GOOGLE_ACCOUNT_NAME,
|
||||
NEW_NOTION_ACCOUNT_NAME,
|
||||
NEW_QUERY_AUTH_ACCOUNT_NAME,
|
||||
NEW_TRELLO_ACCOUNT_NAME,
|
||||
NOTION_NODE_NAME,
|
||||
PIPEDRIVE_NODE_NAME,
|
||||
SCHEDULE_TRIGGER_NODE_NAME,
|
||||
TRELLO_NODE_NAME,
|
||||
} from '../constants';
|
||||
import { CredentialsModal, CredentialsPage, NDV, WorkflowPage } from '../pages';
|
||||
import { errorToast, successToast } from '../pages/notifications';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
|
||||
const credentialsPage = new CredentialsPage();
|
||||
const credentialsModal = new CredentialsModal();
|
||||
const workflowPage = new WorkflowPage();
|
||||
const nodeDetailsView = new NDV();
|
||||
|
||||
const NEW_CREDENTIAL_NAME = 'Something else';
|
||||
const NEW_CREDENTIAL_NAME2 = 'Something else entirely';
|
||||
|
||||
function createNotionCredential() {
|
||||
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME);
|
||||
workflowPage.actions.openNode(NOTION_NODE_NAME);
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
workflowPage.getters.nodeCredentialsCreateOption().click();
|
||||
credentialsModal.actions.fillCredentialsForm();
|
||||
cy.get('body').type('{esc}');
|
||||
workflowPage.actions.deleteNode(NOTION_NODE_NAME);
|
||||
}
|
||||
|
||||
function deleteSelectedCredential() {
|
||||
workflowPage.getters.nodeCredentialsEditButton().click();
|
||||
credentialsModal.getters.deleteButton().click();
|
||||
cy.get('.el-message-box').find('button').contains('Yes').click();
|
||||
}
|
||||
|
||||
describe('Credentials', () => {
|
||||
beforeEach(() => {
|
||||
credentialsComposables.loadCredentialsPage(credentialsPage.url);
|
||||
});
|
||||
|
||||
it('should create a new credential using empty state', () => {
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
|
||||
credentialsModal.getters.newCredentialModal().should('be.visible');
|
||||
credentialsModal.getters.newCredentialTypeSelect().should('be.visible');
|
||||
credentialsModal.getters.newCredentialTypeOption('Notion API').click();
|
||||
|
||||
credentialsModal.getters.newCredentialTypeButton().click();
|
||||
credentialsModal.getters.connectionParameter('Internal Integration Secret').type('1234567890');
|
||||
|
||||
credentialsModal.actions.setName('My awesome Notion account');
|
||||
credentialsModal.actions.save();
|
||||
credentialsModal.actions.close();
|
||||
|
||||
credentialsPage.getters.credentialCards().should('have.length', 1);
|
||||
});
|
||||
|
||||
it('should sort credentials', () => {
|
||||
credentialsPage.actions.search('');
|
||||
credentialsPage.actions.sortBy('nameDesc');
|
||||
credentialsPage.getters.credentialCards().eq(0).should('contain.text', 'Notion');
|
||||
credentialsPage.actions.sortBy('nameAsc');
|
||||
});
|
||||
|
||||
it('should create credentials from NDV for node with multiple auth options', () => {
|
||||
workflowPage.actions.visit();
|
||||
workflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
workflowPage.actions.addNodeToCanvas(GMAIL_NODE_NAME);
|
||||
workflowPage.getters.canvasNodes().last().click();
|
||||
cy.get('body').type('{enter}');
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
workflowPage.getters.nodeCredentialsCreateOption().click();
|
||||
credentialsModal.getters.credentialsEditModal().should('be.visible');
|
||||
credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2);
|
||||
credentialsModal.getters.credentialAuthTypeRadioButtons().first().click();
|
||||
credentialsModal.actions.fillCredentialsForm();
|
||||
cy.get('.el-message-box').find('button').contains('Close').click();
|
||||
workflowPage.getters
|
||||
.nodeCredentialsSelect()
|
||||
.find('input')
|
||||
.should('have.value', NEW_GOOGLE_ACCOUNT_NAME);
|
||||
});
|
||||
|
||||
it('should show multiple credential types in the same dropdown', () => {
|
||||
workflowPage.actions.visit();
|
||||
workflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
workflowPage.actions.addNodeToCanvas(GMAIL_NODE_NAME);
|
||||
workflowPage.getters.canvasNodes().last().click();
|
||||
cy.get('body').type('{enter}');
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
// Add oAuth credentials
|
||||
workflowPage.getters.nodeCredentialsCreateOption().click();
|
||||
credentialsModal.getters.credentialsEditModal().should('be.visible');
|
||||
credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2);
|
||||
credentialsModal.getters.credentialAuthTypeRadioButtons().first().click();
|
||||
credentialsModal.actions.fillCredentialsForm();
|
||||
cy.get('.el-message-box').find('button').contains('Close').click();
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
// Add Service account credentials
|
||||
workflowPage.getters.nodeCredentialsCreateOption().click();
|
||||
credentialsModal.getters.credentialsEditModal().should('be.visible');
|
||||
credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2);
|
||||
credentialsModal.getters.credentialAuthTypeRadioButtons().last().click();
|
||||
credentialsModal.actions.fillCredentialsForm();
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
getVisibleSelect().find('li').should('have.length', 3);
|
||||
});
|
||||
|
||||
it('should correctly render required and optional credentials', () => {
|
||||
workflowPage.actions.visit();
|
||||
workflowPage.actions.addNodeToCanvas(PIPEDRIVE_NODE_NAME, true, true);
|
||||
cy.get('body').type('{downArrow}');
|
||||
cy.get('body').type('{enter}');
|
||||
// Select incoming authentication
|
||||
nodeDetailsView.getters.parameterInput('incomingAuthentication').should('exist');
|
||||
nodeDetailsView.getters.parameterInput('incomingAuthentication').click();
|
||||
getVisibleSelect().find('li').first().click();
|
||||
// There should be two credential fields
|
||||
workflowPage.getters.nodeCredentialsSelect().should('have.length', 2);
|
||||
|
||||
workflowPage.getters.nodeCredentialsSelect().first().click();
|
||||
workflowPage.getters.nodeCredentialsCreateOption().first().click();
|
||||
// This one should show auth type selector
|
||||
credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2);
|
||||
cy.get('body').type('{esc}');
|
||||
|
||||
workflowPage.getters.nodeCredentialsSelect().last().click();
|
||||
workflowPage.getters.nodeCredentialsCreateOption().last().click();
|
||||
// This one should not show auth type selector
|
||||
credentialsModal.getters.credentialsAuthTypeSelector().should('not.exist');
|
||||
});
|
||||
|
||||
it('should create credentials from NDV for node with no auth options', () => {
|
||||
workflowPage.actions.visit();
|
||||
workflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
workflowPage.actions.addNodeToCanvas(TRELLO_NODE_NAME);
|
||||
workflowPage.getters.canvasNodes().last().click();
|
||||
cy.get('body').type('{enter}');
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
workflowPage.getters.nodeCredentialsCreateOption().click();
|
||||
credentialsModal.getters.credentialsAuthTypeSelector().should('not.exist');
|
||||
credentialsModal.actions.fillCredentialsForm();
|
||||
workflowPage.getters
|
||||
.nodeCredentialsSelect()
|
||||
.find('input')
|
||||
.should('have.value', NEW_TRELLO_ACCOUNT_NAME);
|
||||
});
|
||||
|
||||
it('should delete credentials from NDV', () => {
|
||||
workflowPage.actions.visit();
|
||||
workflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME);
|
||||
workflowPage.getters.canvasNodes().last().click();
|
||||
cy.get('body').type('{enter}');
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
workflowPage.getters.nodeCredentialsCreateOption().click();
|
||||
credentialsModal.actions.fillCredentialsForm();
|
||||
workflowPage.getters
|
||||
.nodeCredentialsSelect()
|
||||
.find('input')
|
||||
.should('have.value', NEW_NOTION_ACCOUNT_NAME);
|
||||
|
||||
workflowPage.getters.nodeCredentialsEditButton().click();
|
||||
credentialsModal.getters.credentialsEditModal().should('be.visible');
|
||||
credentialsModal.getters.deleteButton().click();
|
||||
cy.get('.el-message-box').find('button').contains('Yes').click();
|
||||
successToast().contains('Credential deleted');
|
||||
workflowPage.getters
|
||||
.nodeCredentialsSelect()
|
||||
.find('input')
|
||||
.should('not.have.value', NEW_TRELLO_ACCOUNT_NAME);
|
||||
});
|
||||
|
||||
it('should rename credentials from NDV', () => {
|
||||
workflowPage.actions.visit();
|
||||
workflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
workflowPage.actions.addNodeToCanvas(TRELLO_NODE_NAME);
|
||||
workflowPage.getters.canvasNodes().last().click();
|
||||
cy.get('body').type('{enter}');
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
workflowPage.getters.nodeCredentialsCreateOption().click();
|
||||
credentialsModal.actions.fillCredentialsForm();
|
||||
workflowPage.getters.nodeCredentialsEditButton().click();
|
||||
credentialsModal.getters.credentialsEditModal().should('be.visible');
|
||||
credentialsModal.actions.renameCredential(NEW_CREDENTIAL_NAME);
|
||||
saveCredential();
|
||||
credentialsModal.getters.closeButton().click();
|
||||
workflowPage.getters
|
||||
.nodeCredentialsSelect()
|
||||
.find('input')
|
||||
.should('have.value', NEW_CREDENTIAL_NAME);
|
||||
|
||||
// Reload page to make sure this also works when the credential hasn't been
|
||||
// just created.
|
||||
nodeDetailsView.actions.close();
|
||||
workflowPage.actions.saveWorkflowOnButtonClick();
|
||||
cy.reload();
|
||||
workflowPage.getters.canvasNodes().last().click();
|
||||
cy.get('body').type('{enter}');
|
||||
workflowPage.getters.nodeCredentialsEditButton().click();
|
||||
credentialsModal.getters.credentialsEditModal().should('be.visible');
|
||||
credentialsModal.actions.renameCredential(NEW_CREDENTIAL_NAME2);
|
||||
saveCredential();
|
||||
credentialsModal.getters.closeButton().click();
|
||||
workflowPage.getters
|
||||
.nodeCredentialsSelect()
|
||||
.find('input')
|
||||
.should('have.value', NEW_CREDENTIAL_NAME2);
|
||||
});
|
||||
|
||||
it('should edit credential for non-standard credential type', () => {
|
||||
workflowPage.actions.visit();
|
||||
workflowPage.actions.addNodeToCanvas(AGENT_NODE_NAME);
|
||||
workflowPage.actions.addNodeToCanvas(AI_TOOL_HTTP_NODE_NAME);
|
||||
workflowPage.getters.canvasNodes().last().click();
|
||||
cy.get('body').type('{enter}');
|
||||
cy.getByTestId('parameter-input-authentication').click();
|
||||
cy.contains('Predefined Credential Type').click();
|
||||
cy.getByTestId('credential-select').click();
|
||||
cy.contains('Adalo API').click();
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
workflowPage.getters.nodeCredentialsCreateOption().click();
|
||||
credentialsModal.actions.fillCredentialsForm();
|
||||
workflowPage.getters.nodeCredentialsEditButton().click();
|
||||
credentialsModal.getters.credentialsEditModal().should('be.visible');
|
||||
credentialsModal.actions.renameCredential(NEW_CREDENTIAL_NAME);
|
||||
saveCredential();
|
||||
credentialsModal.getters.closeButton().click();
|
||||
workflowPage.getters
|
||||
.nodeCredentialsSelect()
|
||||
.find('input')
|
||||
.should('have.value', NEW_CREDENTIAL_NAME);
|
||||
});
|
||||
|
||||
it('should set a default credential when adding nodes', () => {
|
||||
workflowPage.actions.visit();
|
||||
|
||||
createNotionCredential();
|
||||
|
||||
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);
|
||||
workflowPage.getters
|
||||
.nodeCredentialsSelect()
|
||||
.find('input')
|
||||
.should('have.value', NEW_NOTION_ACCOUNT_NAME);
|
||||
|
||||
deleteSelectedCredential();
|
||||
});
|
||||
|
||||
it('should set a default credential when editing a node', () => {
|
||||
workflowPage.actions.visit();
|
||||
|
||||
createNotionCredential();
|
||||
|
||||
workflowPage.actions.addNodeToCanvas(HTTP_REQUEST_NODE_NAME, true, true);
|
||||
nodeDetailsView.getters.parameterInput('authentication').click();
|
||||
getVisibleSelect().find('li').contains('Predefined').click();
|
||||
|
||||
nodeDetailsView.getters.parameterInput('nodeCredentialType').click();
|
||||
getVisibleSelect().find('li').contains('Notion API').click();
|
||||
|
||||
workflowPage.getters
|
||||
.nodeCredentialsSelect()
|
||||
.find('input')
|
||||
.should('have.value', NEW_NOTION_ACCOUNT_NAME);
|
||||
|
||||
deleteSelectedCredential();
|
||||
});
|
||||
|
||||
it('should setup generic authentication for HTTP node', () => {
|
||||
workflowPage.actions.visit();
|
||||
workflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
workflowPage.actions.addNodeToCanvas(HTTP_REQUEST_NODE_NAME);
|
||||
workflowPage.getters.canvasNodes().last().click();
|
||||
cy.get('body').type('{enter}');
|
||||
nodeDetailsView.getters.parameterInput('authentication').click();
|
||||
getVisibleSelect().find('li').should('have.length', 3);
|
||||
getVisibleSelect().find('li').last().click();
|
||||
nodeDetailsView.getters.parameterInput('genericAuthType').should('exist');
|
||||
nodeDetailsView.getters.parameterInput('genericAuthType').click();
|
||||
getVisibleSelect().find('li').should('have.length.greaterThan', 0);
|
||||
getVisibleSelect().find('li').last().click();
|
||||
|
||||
workflowPage.getters.nodeCredentialsSelect().should('exist');
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
workflowPage.getters.nodeCredentialsCreateOption().click();
|
||||
credentialsModal.actions.fillCredentialsForm();
|
||||
workflowPage.getters
|
||||
.nodeCredentialsSelect()
|
||||
.find('input')
|
||||
.should('have.value', NEW_QUERY_AUTH_ACCOUNT_NAME);
|
||||
});
|
||||
|
||||
it('should not show OAuth redirect URL section when OAuth2 credentials are overridden', () => {
|
||||
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 || [];
|
||||
|
||||
const index = credentials.findIndex((c) => c.name === 'slackOAuth2Api');
|
||||
|
||||
credentials[index] = {
|
||||
...credentials[index],
|
||||
__overwrittenProperties: ['clientId', 'clientSecret'],
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
workflowPage.actions.visit(true);
|
||||
workflowPage.actions.addNodeToCanvas('Manual');
|
||||
workflowPage.actions.addNodeToCanvas('Slack', true, true, 'Get a channel');
|
||||
workflowPage.getters.nodeCredentialsSelect().should('exist');
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
workflowPage.getters.nodeCredentialsCreateOption().click();
|
||||
credentialsModal.getters.credentialAuthTypeRadioButtons().first().click();
|
||||
nodeDetailsView.getters.copyInput().should('not.exist');
|
||||
});
|
||||
|
||||
it('ADO-2583 should show notifications above credential modal overlay', () => {
|
||||
// check error notifications because they are sticky
|
||||
cy.intercept('POST', '/rest/credentials', { forceNetworkError: true });
|
||||
credentialsPage.getters.createCredentialButton().click();
|
||||
|
||||
credentialsModal.getters.newCredentialModal().should('be.visible');
|
||||
credentialsModal.getters.newCredentialTypeSelect().should('be.visible');
|
||||
credentialsModal.getters.newCredentialTypeOption('Notion API').click();
|
||||
|
||||
credentialsModal.getters.newCredentialTypeButton().click();
|
||||
credentialsModal.getters.connectionParameter('Internal Integration Secret').type('1234567890');
|
||||
|
||||
credentialsModal.actions.setName('My awesome Notion account');
|
||||
getCredentialSaveButton().click();
|
||||
|
||||
errorToast().should('have.length', 1);
|
||||
errorToast().should('be.visible');
|
||||
|
||||
errorToast().should('have.css', 'z-index', '2100');
|
||||
cy.get('.el-overlay').should('have.css', 'z-index', '2001');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { CreateCredentialDto } from '@n8n/api-types';
|
||||
|
||||
import type { n8nPage } from '../pages/n8nPage';
|
||||
|
||||
export class CredentialsComposer {
|
||||
constructor(private readonly n8n: n8nPage) {}
|
||||
|
||||
/**
|
||||
* Create a credential through the Credentials list UI.
|
||||
* Expects the visible label of the credential type (e.g. 'Notion API').
|
||||
*/
|
||||
async createFromList(
|
||||
credentialType: string,
|
||||
fields: Record<string, string>,
|
||||
options?: { name?: string; projectId?: string; closeDialog?: boolean },
|
||||
) {
|
||||
if (options?.projectId) {
|
||||
await this.n8n.navigate.toCredentials(options.projectId);
|
||||
} else {
|
||||
await this.n8n.navigate.toCredentials();
|
||||
}
|
||||
|
||||
// Open the "new credential" chooser: open add resource -> credential
|
||||
await this.n8n.credentials.addResourceButton.click();
|
||||
await this.n8n.credentials.actionCredentialButton.click();
|
||||
await this.n8n.credentials.createCredentialFromCredentialPicker(credentialType, fields, {
|
||||
name: options?.name,
|
||||
closeDialog: options?.closeDialog,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a credential through the NDV flow.
|
||||
* Type is implied by the open node's credential requirement.
|
||||
*/
|
||||
async createFromNdv(
|
||||
fields: Record<string, string>,
|
||||
options?: { name?: string; closeDialog?: boolean },
|
||||
) {
|
||||
await this.n8n.ndv.clickCreateNewCredential();
|
||||
await this.n8n.canvas.credentialModal.addCredential(fields, {
|
||||
name: options?.name,
|
||||
closeDialog: options?.closeDialog,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a credential directly via API. Returns created credential object.
|
||||
*/
|
||||
async createFromApi(payload: CreateCredentialDto & { projectId?: string }) {
|
||||
return await this.n8n.api.credentialApi.createCredential(payload);
|
||||
}
|
||||
}
|
||||
@@ -36,11 +36,9 @@ export class ProjectComposer {
|
||||
credentialValue: string,
|
||||
) {
|
||||
await this.n8n.sideBar.openNewCredentialDialogForProject(projectName);
|
||||
await this.n8n.credentials.openNewCredentialDialogFromCredentialList(credentialType);
|
||||
await this.n8n.credentials.fillCredentialField(credentialFieldName, credentialValue);
|
||||
await this.n8n.credentials.saveCredential();
|
||||
await this.n8n.notifications.waitForNotificationAndClose('Credential successfully created');
|
||||
await this.n8n.credentials.closeCredentialDialog();
|
||||
await this.n8n.credentials.createCredentialFromCredentialPicker(credentialType, {
|
||||
[credentialFieldName]: credentialValue,
|
||||
});
|
||||
}
|
||||
|
||||
extractIdFromUrl(url: string, beforeWord: string, afterWord: string): string {
|
||||
|
||||
@@ -4,12 +4,14 @@ import { nanoid } from 'nanoid';
|
||||
import { BasePage } from './BasePage';
|
||||
import { ROUTES } from '../config/constants';
|
||||
import { resolveFromRoot } from '../utils/path-helper';
|
||||
import { CredentialModal } from './components/CredentialModal';
|
||||
import { LogsPanel } from './components/LogsPanel';
|
||||
import { StickyComponent } from './components/StickyComponent';
|
||||
|
||||
export class CanvasPage extends BasePage {
|
||||
readonly sticky = new StickyComponent(this.page);
|
||||
readonly logsPanel = new LogsPanel(this.page.getByTestId('logs-panel'));
|
||||
readonly credentialModal = new CredentialModal(this.page.getByTestId('editCredential-modal'));
|
||||
|
||||
saveWorkflowButton(): Locator {
|
||||
return this.page.getByRole('button', { name: 'Save' });
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { BasePage } from './BasePage';
|
||||
|
||||
export class CredentialsEditModal extends BasePage {
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
}
|
||||
|
||||
getModal(): Locator {
|
||||
return this.page.getByTestId('editCredential-modal');
|
||||
}
|
||||
|
||||
async waitForModal(): Promise<void> {
|
||||
await this.getModal().waitFor({ state: 'visible' });
|
||||
}
|
||||
|
||||
async fillField(key: string, value: string): Promise<void> {
|
||||
const input = this.page.getByTestId(`parameter-input-${key}`).locator('input, textarea');
|
||||
await input.fill(value);
|
||||
await expect(input).toHaveValue(value);
|
||||
}
|
||||
|
||||
async fillAllFields(values: Record<string, string>): Promise<void> {
|
||||
for (const [key, val] of Object.entries(values)) {
|
||||
await this.fillField(key, val);
|
||||
}
|
||||
}
|
||||
|
||||
getSaveButton(): Locator {
|
||||
return this.page.getByTestId('credential-save-button');
|
||||
}
|
||||
|
||||
async save(): Promise<void> {
|
||||
const saveBtn = this.getSaveButton();
|
||||
await saveBtn.click();
|
||||
await saveBtn.waitFor({ state: 'visible' });
|
||||
|
||||
// Saved state changes the button text to "Saved"
|
||||
// Defensive wait for text when UI updates
|
||||
try {
|
||||
await saveBtn
|
||||
.getByText('Saved', { exact: true })
|
||||
.waitFor({ state: 'visible', timeout: 3000 });
|
||||
} catch {
|
||||
// ignore if text assertion is flaky; modal close below will still ensure flow continues
|
||||
}
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
const closeBtn = this.getModal().locator('.el-dialog__close').first();
|
||||
if (await closeBtn.isVisible()) {
|
||||
await closeBtn.click();
|
||||
}
|
||||
}
|
||||
|
||||
async setValues(values: Record<string, string>, save: boolean = true): Promise<void> {
|
||||
await this.waitForModal();
|
||||
await this.fillAllFields(values);
|
||||
|
||||
if (save) {
|
||||
await this.save();
|
||||
await this.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
import { BasePage } from './BasePage';
|
||||
import { CredentialModal } from './components/CredentialModal';
|
||||
|
||||
export class CredentialsPage extends BasePage {
|
||||
readonly credentialModal = new CredentialModal(this.page.getByTestId('editCredential-modal'));
|
||||
|
||||
get emptyListCreateCredentialButton() {
|
||||
return this.page.getByRole('button', { name: 'Add first credential' });
|
||||
}
|
||||
@@ -10,14 +13,60 @@ export class CredentialsPage extends BasePage {
|
||||
}
|
||||
|
||||
get credentialCards() {
|
||||
return this.page.getByTestId('credential-cards');
|
||||
return this.page.getByTestId('resources-list-item');
|
||||
}
|
||||
|
||||
getCredentialByName(name: string) {
|
||||
return this.credentialCards.filter({ hasText: name }).first();
|
||||
}
|
||||
|
||||
get addResourceButton() {
|
||||
return this.page.getByTestId('add-resource');
|
||||
}
|
||||
get actionCredentialButton() {
|
||||
return this.page.getByTestId('action-credential');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new credential of the specified type
|
||||
* Create a credential from the credentials list, fill fields, save, and close the modal.
|
||||
* @param credentialType - The type of credential to create (e.g. 'Notion API')
|
||||
* @param fields - Key-value pairs for credential fields to fill
|
||||
*/
|
||||
async openNewCredentialDialogFromCredentialList(credentialType: string): Promise<void> {
|
||||
async createCredentialFromCredentialPicker(
|
||||
credentialType: string,
|
||||
fields: Record<string, string>,
|
||||
options?: { closeDialog?: boolean; name?: string },
|
||||
): Promise<void> {
|
||||
await this.page.getByRole('combobox', { name: 'Search for app...' }).fill(credentialType);
|
||||
await this.page
|
||||
.getByTestId('new-credential-type-select-option')
|
||||
.filter({ hasText: credentialType })
|
||||
.click();
|
||||
await this.page.getByTestId('new-credential-type-button').click();
|
||||
await this.credentialModal.addCredential(fields, {
|
||||
name: options?.name,
|
||||
closeDialog: options?.closeDialog,
|
||||
});
|
||||
}
|
||||
|
||||
async clearSearch() {
|
||||
await this.page.getByTestId('resources-list-search').clear();
|
||||
}
|
||||
|
||||
async sortByNameDescending() {
|
||||
await this.page.getByTestId('resources-list-sort').click();
|
||||
await this.page.getByText('Name (Z-A)').click();
|
||||
}
|
||||
|
||||
async sortByNameAscending() {
|
||||
await this.page.getByTestId('resources-list-sort').click();
|
||||
await this.page.getByText('Name (A-Z)').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Select credential type without auto-saving (for tests that need to handle save manually)
|
||||
*/
|
||||
async selectCredentialType(credentialType: string): Promise<void> {
|
||||
await this.page.getByRole('combobox', { name: 'Search for app...' }).fill(credentialType);
|
||||
await this.page
|
||||
.getByTestId('new-credential-type-select-option')
|
||||
@@ -25,56 +74,4 @@ export class CredentialsPage extends BasePage {
|
||||
.click();
|
||||
await this.page.getByTestId('new-credential-type-button').click();
|
||||
}
|
||||
|
||||
async openCredentialSelector() {
|
||||
await this.page.getByRole('combobox', { name: 'Select Credential' }).click();
|
||||
}
|
||||
|
||||
async createNewCredential() {
|
||||
await this.clickByText('Create new credential');
|
||||
}
|
||||
|
||||
async fillCredentialField(fieldName: string, value: string) {
|
||||
const field = this.page
|
||||
.getByTestId(`parameter-input-${fieldName}`)
|
||||
.getByTestId('parameter-input-field');
|
||||
await field.click();
|
||||
await field.fill(value);
|
||||
}
|
||||
get saveCredentialButton() {
|
||||
return this.page.getByRole('button', { name: 'Save' });
|
||||
}
|
||||
|
||||
async saveCredential() {
|
||||
await this.clickButtonByName('Save');
|
||||
}
|
||||
|
||||
async closeCredentialDialog() {
|
||||
await this.clickButtonByName('Close this dialog');
|
||||
}
|
||||
|
||||
async createAndSaveNewCredential(fieldName: string, value: string) {
|
||||
await this.openCredentialSelector();
|
||||
await this.createNewCredential();
|
||||
await this.filLCredentialSaveClose(fieldName, value);
|
||||
}
|
||||
|
||||
async filLCredentialSaveClose(fieldName: string, value: string) {
|
||||
await this.fillCredentialField(fieldName, value);
|
||||
await this.saveCredential();
|
||||
await this.page.getByText('Connection tested successfully').waitFor({ state: 'visible' });
|
||||
await this.closeCredentialDialog();
|
||||
}
|
||||
|
||||
getOauthConnectButton() {
|
||||
return this.page.getByTestId('oauth-connect-button');
|
||||
}
|
||||
|
||||
getOauthConnectSuccessBanner() {
|
||||
return this.page.getByTestId('oauth-connect-success-banner');
|
||||
}
|
||||
|
||||
getSaveButton() {
|
||||
return this.page.getByTestId('credential-save-button');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,26 @@ export class NodeDetailsViewPage extends BasePage {
|
||||
this.editFields = new EditFieldsNode(page);
|
||||
}
|
||||
|
||||
getNodeCredentialsSelect() {
|
||||
return this.page.getByTestId('node-credentials-select');
|
||||
}
|
||||
|
||||
credentialDropdownCreateNewCredential() {
|
||||
return this.page.getByText('Create new credential');
|
||||
}
|
||||
|
||||
getCredentialOptionByText(text: string) {
|
||||
return this.page.getByText(text);
|
||||
}
|
||||
|
||||
getCredentialDropdownOptions() {
|
||||
return this.page.getByRole('option');
|
||||
}
|
||||
|
||||
getCredentialSelect() {
|
||||
return this.page.getByRole('combobox', { name: 'Select Credential' });
|
||||
}
|
||||
|
||||
async clickBackToCanvasButton() {
|
||||
await this.clickByTestId('back-to-canvas');
|
||||
}
|
||||
@@ -554,7 +574,7 @@ export class NodeDetailsViewPage extends BasePage {
|
||||
// Credentials modal helpers
|
||||
async clickCreateNewCredential(eq: number = 0): Promise<void> {
|
||||
await this.page.getByTestId('node-credentials-select').nth(eq).click();
|
||||
await this.page.getByTestId('node-credentials-select-item-new').click();
|
||||
await this.page.getByTestId('node-credentials-select-item-new').nth(eq).click();
|
||||
}
|
||||
|
||||
// Run selector and linking helpers
|
||||
|
||||
120
packages/testing/playwright/pages/components/CredentialModal.ts
Normal file
120
packages/testing/playwright/pages/components/CredentialModal.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Credential modal component for canvas and credentials interactions.
|
||||
* Used within CanvasPage as `n8n.canvas.credentialModal.*`
|
||||
* Used within CredentialsPage as `n8n.credentials.modal.*`
|
||||
*
|
||||
* @example
|
||||
* // Access via canvas page or credentials page
|
||||
* await n8n.canvas.credentialModal.addCredential();
|
||||
* await expect(n8n.canvas.credentialModal.getModal()).toBeVisible();
|
||||
*/
|
||||
export class CredentialModal {
|
||||
constructor(private root: Locator) {}
|
||||
|
||||
getModal(): Locator {
|
||||
return this.root;
|
||||
}
|
||||
|
||||
getCredentialName(): Locator {
|
||||
return this.root.getByTestId('credential-name');
|
||||
}
|
||||
|
||||
getNameInput(): Locator {
|
||||
return this.getCredentialName().getByTestId('inline-edit-input');
|
||||
}
|
||||
|
||||
async waitForModal(): Promise<void> {
|
||||
await this.root.waitFor({ state: 'visible' });
|
||||
}
|
||||
|
||||
async fillField(key: string, value: string): Promise<void> {
|
||||
const input = this.root.getByTestId(`parameter-input-${key}`).locator('input, textarea');
|
||||
await input.fill(value);
|
||||
await expect(input).toHaveValue(value);
|
||||
}
|
||||
|
||||
async fillAllFields(values: Record<string, string>): Promise<void> {
|
||||
for (const [key, val] of Object.entries(values)) {
|
||||
await this.fillField(key, val);
|
||||
}
|
||||
}
|
||||
|
||||
getSaveButton(): Locator {
|
||||
return this.root.getByTestId('credential-save-button');
|
||||
}
|
||||
|
||||
async save(): Promise<void> {
|
||||
const saveBtn = this.getSaveButton();
|
||||
await saveBtn.click();
|
||||
await saveBtn.waitFor({ state: 'visible' });
|
||||
|
||||
await saveBtn.getByText('Saved', { exact: true }).waitFor({ state: 'visible', timeout: 3000 });
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
const closeBtn = this.root.locator('.el-dialog__close').first();
|
||||
if (await closeBtn.isVisible()) {
|
||||
await closeBtn.click();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a credential to the modal
|
||||
* @param fields - The fields to fill in the modal
|
||||
* @param options - The options to pass to the modal
|
||||
* @param options.closeDialog - Whether to close the modal after saving
|
||||
* @param options.name - The name of the credential
|
||||
*/
|
||||
async addCredential(
|
||||
fields: Record<string, string>,
|
||||
options?: { closeDialog?: boolean; name?: string },
|
||||
): Promise<void> {
|
||||
await this.fillAllFields(fields);
|
||||
if (options?.name) {
|
||||
await this.getCredentialName().click();
|
||||
await this.getNameInput().fill(options.name);
|
||||
}
|
||||
await this.save();
|
||||
const shouldClose = options?.closeDialog ?? true;
|
||||
if (shouldClose) {
|
||||
await this.close();
|
||||
}
|
||||
}
|
||||
|
||||
get oauthConnectButton() {
|
||||
return this.root.getByTestId('oauth-connect-button');
|
||||
}
|
||||
|
||||
get oauthConnectSuccessBanner() {
|
||||
return this.root.getByTestId('oauth-connect-success-banner');
|
||||
}
|
||||
|
||||
async editCredential(): Promise<void> {
|
||||
await this.root.page().getByTestId('credential-edit-button').click();
|
||||
}
|
||||
|
||||
async deleteCredential(): Promise<void> {
|
||||
await this.root.page().getByTestId('credential-delete-button').click();
|
||||
}
|
||||
|
||||
async confirmDelete(): Promise<void> {
|
||||
await this.root.page().getByRole('button', { name: 'Yes' }).click();
|
||||
}
|
||||
|
||||
async renameCredential(newName: string): Promise<void> {
|
||||
await this.getCredentialName().click();
|
||||
await this.getNameInput().fill(newName);
|
||||
await this.getNameInput().press('Enter');
|
||||
}
|
||||
|
||||
getAuthMethodSelector() {
|
||||
return this.root.page().getByText('Select Authentication Method');
|
||||
}
|
||||
|
||||
getOAuthRedirectUrl() {
|
||||
return this.root.page().getByTestId('oauth-redirect-url');
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import { AIAssistantPage } from './AIAssistantPage';
|
||||
import { BecomeCreatorCTAPage } from './BecomeCreatorCTAPage';
|
||||
import { CanvasPage } from './CanvasPage';
|
||||
import { CommunityNodesPage } from './CommunityNodesPage';
|
||||
import { CredentialsEditModal } from './CredentialsEditModal';
|
||||
import { CredentialsPage } from './CredentialsPage';
|
||||
import { DemoPage } from './DemoPage';
|
||||
import { ExecutionsPage } from './ExecutionsPage';
|
||||
@@ -24,6 +23,7 @@ import { WorkflowSettingsModal } from './WorkflowSettingsModal';
|
||||
import { WorkflowSharingModal } from './WorkflowSharingModal';
|
||||
import { WorkflowsPage } from './WorkflowsPage';
|
||||
import { CanvasComposer } from '../composables/CanvasComposer';
|
||||
import { CredentialsComposer } from '../composables/CredentialsComposer';
|
||||
import { ProjectComposer } from '../composables/ProjectComposer';
|
||||
import { TestEntryComposer } from '../composables/TestEntryComposer';
|
||||
import { WorkflowComposer } from '../composables/WorkflowComposer';
|
||||
@@ -60,12 +60,12 @@ export class n8nPage {
|
||||
readonly workflowActivationModal: WorkflowActivationModal;
|
||||
readonly workflowSettingsModal: WorkflowSettingsModal;
|
||||
readonly workflowSharingModal: WorkflowSharingModal;
|
||||
readonly credentialsModal: CredentialsEditModal;
|
||||
|
||||
// Composables
|
||||
readonly workflowComposer: WorkflowComposer;
|
||||
readonly projectComposer: ProjectComposer;
|
||||
readonly canvasComposer: CanvasComposer;
|
||||
readonly credentialsComposer: CredentialsComposer;
|
||||
readonly start: TestEntryComposer;
|
||||
|
||||
// Helpers
|
||||
@@ -100,12 +100,12 @@ export class n8nPage {
|
||||
// Modals
|
||||
this.workflowActivationModal = new WorkflowActivationModal(page);
|
||||
this.workflowSettingsModal = new WorkflowSettingsModal(page);
|
||||
this.credentialsModal = new CredentialsEditModal(page);
|
||||
|
||||
// Composables
|
||||
this.workflowComposer = new WorkflowComposer(this);
|
||||
this.projectComposer = new ProjectComposer(this);
|
||||
this.canvasComposer = new CanvasComposer(this);
|
||||
this.credentialsComposer = new CredentialsComposer(this);
|
||||
this.start = new TestEntryComposer(this);
|
||||
|
||||
// Helpers
|
||||
|
||||
@@ -34,6 +34,10 @@ export class CredentialApiHelper {
|
||||
|
||||
/**
|
||||
* Create a new credential
|
||||
*
|
||||
* Notes:
|
||||
* - The `type` field is the credential type ID (e.g., 'notionApi'), which differs from the UI display name (e.g., 'Notion API').
|
||||
* - You can find available credential type IDs in the codebase under `packages/nodes-base/credentials/*.credentials.ts` and by inspecting node credential references (e.g., Notion nodes use `type: 'notionApi'`).
|
||||
*/
|
||||
async createCredential(credential: CreateCredentialDto): Promise<CredentialResponse> {
|
||||
const response = await this.api.request.post('/rest/credentials', { data: credential });
|
||||
|
||||
362
packages/testing/playwright/tests/ui/2-credentials.spec.ts
Normal file
362
packages/testing/playwright/tests/ui/2-credentials.spec.ts
Normal file
@@ -0,0 +1,362 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { test, expect } from '../../fixtures/base';
|
||||
|
||||
test.describe('Credentials', () => {
|
||||
test.beforeEach(async ({ n8n }) => {
|
||||
await n8n.goHome();
|
||||
});
|
||||
|
||||
test('should create a new credential using empty state', async ({ n8n }) => {
|
||||
const projectId = await n8n.start.fromNewProject();
|
||||
const credentialName = `My awesome Notion account ${nanoid()}`;
|
||||
|
||||
await n8n.credentialsComposer.createFromList(
|
||||
'Notion API',
|
||||
{ apiKey: '1234567890' },
|
||||
{ name: credentialName, projectId },
|
||||
);
|
||||
|
||||
await expect(n8n.credentials.credentialCards).toHaveCount(1);
|
||||
await expect(n8n.credentials.getCredentialByName(credentialName)).toBeVisible();
|
||||
});
|
||||
|
||||
test('should sort credentials', async ({ n8n, api }) => {
|
||||
const projectId = await n8n.start.fromNewProject();
|
||||
const credentialA = `A Credential ${nanoid()}`;
|
||||
const credentialZ = `Z Credential ${nanoid()}`;
|
||||
|
||||
await api.credentialApi.createCredential({
|
||||
name: credentialA,
|
||||
type: 'notionApi',
|
||||
data: { apiKey: '1234567890' },
|
||||
projectId,
|
||||
});
|
||||
|
||||
await api.credentialApi.createCredential({
|
||||
name: credentialZ,
|
||||
type: 'trelloApi',
|
||||
data: { apiKey: 'test_api_key', apiToken: 'test_api_token' },
|
||||
projectId,
|
||||
});
|
||||
|
||||
await n8n.navigate.toCredentials(projectId);
|
||||
await n8n.credentials.clearSearch();
|
||||
await n8n.credentials.sortByNameDescending();
|
||||
|
||||
const firstCardDescending = n8n.credentials.credentialCards.first();
|
||||
await expect(firstCardDescending).toContainText(credentialZ);
|
||||
|
||||
await n8n.credentials.sortByNameAscending();
|
||||
|
||||
const firstCardAscending = n8n.credentials.credentialCards.first();
|
||||
await expect(firstCardAscending).toContainText(credentialA);
|
||||
});
|
||||
|
||||
test('should create credentials from NDV for node with multiple auth options', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
await n8n.start.fromNewProjectBlankCanvas();
|
||||
const credentialName = `My Google OAuth2 Account ${nanoid()}`;
|
||||
|
||||
await n8n.canvas.addNode('Manual Trigger');
|
||||
await n8n.canvas.addNode('Gmail', { action: 'Send a message' });
|
||||
|
||||
await n8n.ndv.clickCreateNewCredential();
|
||||
|
||||
await expect(
|
||||
n8n.canvas.credentialModal
|
||||
.getModal()
|
||||
.getByTestId('node-auth-type-selector')
|
||||
.locator('label.el-radio'),
|
||||
).toHaveCount(2);
|
||||
|
||||
await n8n.canvas.credentialModal
|
||||
.getModal()
|
||||
.getByTestId('node-auth-type-selector')
|
||||
.locator('label.el-radio')
|
||||
.first()
|
||||
.click();
|
||||
|
||||
await n8n.canvas.credentialModal.addCredential(
|
||||
{
|
||||
clientId: 'test_client_id',
|
||||
clientSecret: 'test_client_secret',
|
||||
},
|
||||
{ name: credentialName },
|
||||
);
|
||||
|
||||
await expect(n8n.ndv.getCredentialSelect()).toHaveValue(credentialName);
|
||||
});
|
||||
|
||||
test('should show multiple credential types in the same dropdown', async ({ n8n, api }) => {
|
||||
const projectId = await n8n.start.fromNewProjectBlankCanvas();
|
||||
const serviceAccountCredentialName2 = `OAuth2 Credential ${nanoid()}`;
|
||||
const serviceAccountCredentialName = `Service Account Credential ${nanoid()}`;
|
||||
|
||||
await api.credentialApi.createCredential({
|
||||
name: serviceAccountCredentialName2,
|
||||
type: 'googleApi',
|
||||
data: { email: 'test@service.com', privateKey: 'test_key' },
|
||||
projectId,
|
||||
});
|
||||
|
||||
await api.credentialApi.createCredential({
|
||||
name: serviceAccountCredentialName,
|
||||
type: 'googleApi',
|
||||
data: { email: 'test@service.com', privateKey: 'test_key' },
|
||||
projectId,
|
||||
});
|
||||
|
||||
await n8n.canvas.addNode('Manual Trigger');
|
||||
await n8n.canvas.addNode('Gmail', { action: 'Send a message' });
|
||||
|
||||
await n8n.ndv.getCredentialSelect().click();
|
||||
await expect(n8n.ndv.getCredentialOptionByText(serviceAccountCredentialName2)).toBeVisible();
|
||||
await expect(n8n.ndv.getCredentialOptionByText(serviceAccountCredentialName)).toBeVisible();
|
||||
await expect(n8n.ndv.credentialDropdownCreateNewCredential()).toBeVisible();
|
||||
await expect(n8n.ndv.getCredentialDropdownOptions()).toHaveCount(2);
|
||||
});
|
||||
|
||||
test('should correctly render required and optional credentials', async ({ n8n }) => {
|
||||
await n8n.start.fromNewProjectBlankCanvas();
|
||||
|
||||
await n8n.canvas.addNode('Pipedrive', { trigger: 'On new Pipedrive event' });
|
||||
await n8n.ndv.selectOptionInParameterDropdown('incomingAuthentication', 'Basic Auth');
|
||||
await expect(n8n.ndv.getNodeCredentialsSelect()).toHaveCount(2);
|
||||
|
||||
await n8n.ndv.clickCreateNewCredential(0);
|
||||
await expect(
|
||||
n8n.canvas.credentialModal
|
||||
.getModal()
|
||||
.getByTestId('node-auth-type-selector')
|
||||
.locator('label.el-radio'),
|
||||
).toHaveCount(2);
|
||||
await n8n.canvas.credentialModal.close();
|
||||
|
||||
await n8n.ndv.clickCreateNewCredential(1);
|
||||
await expect(n8n.canvas.credentialModal.getModal()).toBeVisible();
|
||||
await expect(n8n.canvas.credentialModal.getAuthMethodSelector()).toBeHidden();
|
||||
await n8n.canvas.credentialModal.close();
|
||||
});
|
||||
|
||||
test('should create credentials from NDV for node with no auth options', async ({ n8n }) => {
|
||||
await n8n.start.fromNewProjectBlankCanvas();
|
||||
const credentialName = `My Trello Account ${nanoid()}`;
|
||||
|
||||
await n8n.canvas.addNode('Manual Trigger');
|
||||
await n8n.canvas.addNode('Trello', { action: 'Create a card' });
|
||||
|
||||
await n8n.credentialsComposer.createFromNdv(
|
||||
{
|
||||
apiKey: 'test_api_key',
|
||||
apiToken: 'test_api_token',
|
||||
},
|
||||
{ name: credentialName },
|
||||
);
|
||||
|
||||
await expect(n8n.ndv.getCredentialSelect()).toHaveValue(credentialName);
|
||||
});
|
||||
|
||||
test('should delete credentials from NDV', async ({ n8n }) => {
|
||||
await n8n.start.fromNewProjectBlankCanvas();
|
||||
const credentialName = `Notion Credential ${nanoid()}`;
|
||||
|
||||
await n8n.canvas.addNode('Manual Trigger');
|
||||
await n8n.canvas.addNode('Notion', { action: 'Append a block' });
|
||||
|
||||
await n8n.credentialsComposer.createFromNdv({ apiKey: '1234567890' }, { name: credentialName });
|
||||
await expect(n8n.ndv.getCredentialSelect()).toHaveValue(credentialName);
|
||||
|
||||
await n8n.canvas.credentialModal.editCredential();
|
||||
await n8n.canvas.credentialModal.deleteCredential();
|
||||
await n8n.canvas.credentialModal.confirmDelete();
|
||||
|
||||
await expect(
|
||||
n8n.notifications.getNotificationByTitleOrContent('Credential deleted'),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(n8n.ndv.getCredentialSelect()).not.toHaveValue(credentialName);
|
||||
});
|
||||
|
||||
test('should rename credentials from NDV', async ({ n8n }) => {
|
||||
await n8n.start.fromNewProjectBlankCanvas();
|
||||
const initialName = `My Trello Account ${nanoid()}`;
|
||||
const renamedName = `Something else ${nanoid()}`;
|
||||
|
||||
await n8n.canvas.addNode('Manual Trigger');
|
||||
await n8n.canvas.addNode('Trello', { action: 'Create a card' });
|
||||
|
||||
await n8n.credentialsComposer.createFromNdv(
|
||||
{
|
||||
apiKey: 'test_api_key',
|
||||
apiToken: 'test_api_token',
|
||||
},
|
||||
{ name: initialName },
|
||||
);
|
||||
|
||||
await n8n.canvas.credentialModal.editCredential();
|
||||
await n8n.canvas.credentialModal.renameCredential(renamedName);
|
||||
await n8n.canvas.credentialModal.save();
|
||||
await n8n.canvas.credentialModal.close();
|
||||
|
||||
await expect(n8n.ndv.getCredentialSelect()).toHaveValue(renamedName);
|
||||
});
|
||||
|
||||
test('should edit credential for non-standard credential type', async ({ n8n }) => {
|
||||
await n8n.start.fromNewProjectBlankCanvas();
|
||||
const initialName = `Adalo Credential ${nanoid()}`;
|
||||
const editedName = `Something else ${nanoid()}`;
|
||||
|
||||
await n8n.canvas.addNode('AI Agent', { closeNDV: true });
|
||||
await n8n.canvas.addNode('HTTP Request Tool');
|
||||
|
||||
await n8n.ndv.selectOptionInParameterDropdown('authentication', 'Predefined Credential Type');
|
||||
await n8n.ndv.selectOptionInParameterDropdown('nodeCredentialType', 'Adalo API');
|
||||
|
||||
await n8n.credentialsComposer.createFromNdv(
|
||||
{
|
||||
apiKey: 'test_adalo_key',
|
||||
appId: 'test_app_id',
|
||||
},
|
||||
{ name: initialName },
|
||||
);
|
||||
|
||||
await n8n.canvas.credentialModal.editCredential();
|
||||
await n8n.canvas.credentialModal.renameCredential(editedName);
|
||||
await n8n.canvas.credentialModal.save();
|
||||
await n8n.canvas.credentialModal.close();
|
||||
|
||||
await expect(n8n.ndv.getCredentialSelect()).toHaveValue(editedName);
|
||||
});
|
||||
|
||||
test('should set a default credential when adding nodes', async ({ n8n, api }) => {
|
||||
const projectId = await n8n.start.fromNewProjectBlankCanvas();
|
||||
const credentialName = `My awesome Notion account ${nanoid()}`;
|
||||
|
||||
await api.credentialApi.createCredential({
|
||||
name: credentialName,
|
||||
type: 'notionApi',
|
||||
data: { apiKey: '1234567890' },
|
||||
projectId,
|
||||
});
|
||||
|
||||
await n8n.canvas.addNode('Manual Trigger');
|
||||
await n8n.canvas.addNode('Notion', { action: 'Append a block' });
|
||||
await expect(n8n.ndv.getCredentialSelect()).toHaveValue(credentialName);
|
||||
|
||||
const credentials = await api.credentialApi.getCredentials();
|
||||
const credential = credentials.find((c) => c.name === credentialName);
|
||||
await api.credentialApi.deleteCredential(credential!.id);
|
||||
});
|
||||
|
||||
test('should set a default credential when editing a node', async ({ n8n, api }) => {
|
||||
const projectId = await n8n.start.fromNewProjectBlankCanvas();
|
||||
const credentialName = `My awesome Notion account ${nanoid()}`;
|
||||
|
||||
await api.credentialApi.createCredential({
|
||||
name: credentialName,
|
||||
type: 'notionApi',
|
||||
data: { apiKey: '1234567890' },
|
||||
projectId,
|
||||
});
|
||||
|
||||
await n8n.canvas.addNode('Manual Trigger');
|
||||
await n8n.canvas.addNode('HTTP Request');
|
||||
|
||||
await n8n.ndv.selectOptionInParameterDropdown('authentication', 'Predefined Credential Type');
|
||||
await n8n.ndv.selectOptionInParameterDropdown('nodeCredentialType', 'Notion API');
|
||||
await expect(n8n.ndv.getCredentialSelect()).toHaveValue(credentialName);
|
||||
|
||||
const credentials = await api.credentialApi.getCredentials();
|
||||
const credential = credentials.find((c) => c.name === credentialName);
|
||||
await api.credentialApi.deleteCredential(credential!.id);
|
||||
});
|
||||
|
||||
test('should setup generic authentication for HTTP node', async ({ n8n }) => {
|
||||
await n8n.start.fromNewProjectBlankCanvas();
|
||||
const credentialName = `Query Auth Credential ${nanoid()}`;
|
||||
|
||||
await n8n.canvas.addNode('Manual Trigger');
|
||||
await n8n.canvas.addNode('HTTP Request');
|
||||
|
||||
await n8n.ndv.selectOptionInParameterDropdown('authentication', 'Generic Credential Type');
|
||||
await n8n.ndv.selectOptionInParameterDropdown('genericAuthType', 'Query Auth');
|
||||
|
||||
await n8n.credentialsComposer.createFromNdv(
|
||||
{
|
||||
name: 'api_key',
|
||||
value: 'test_query_value',
|
||||
},
|
||||
{ name: credentialName },
|
||||
);
|
||||
|
||||
await expect(n8n.ndv.getCredentialSelect()).toHaveValue(credentialName);
|
||||
});
|
||||
|
||||
test('should not show OAuth redirect URL section when OAuth2 credentials are overridden', async ({
|
||||
n8n,
|
||||
page,
|
||||
}) => {
|
||||
// Mock credential types response to simulate admin override
|
||||
await page.route('**/rest/types/credentials.json', async (route) => {
|
||||
const response = await route.fetch();
|
||||
const json = await response.json();
|
||||
|
||||
// Override Slack OAuth2 credential properties
|
||||
if (json.slackOAuth2Api) {
|
||||
json.slackOAuth2Api.__overwrittenProperties = ['clientId', 'clientSecret'];
|
||||
}
|
||||
|
||||
await route.fulfill({ json });
|
||||
});
|
||||
|
||||
await n8n.start.fromNewProjectBlankCanvas();
|
||||
|
||||
await n8n.canvas.addNode('Manual Trigger');
|
||||
await n8n.canvas.addNode('Slack', { action: 'Get a channel' });
|
||||
|
||||
await n8n.ndv.clickCreateNewCredential();
|
||||
|
||||
await n8n.canvas.credentialModal
|
||||
.getModal()
|
||||
.getByTestId('node-auth-type-selector')
|
||||
.locator('label.el-radio')
|
||||
.first()
|
||||
.click();
|
||||
|
||||
await expect(n8n.canvas.credentialModal.getOAuthRedirectUrl()).toBeHidden();
|
||||
await expect(n8n.canvas.credentialModal.getModal()).toBeVisible();
|
||||
});
|
||||
|
||||
test('ADO-2583 should show notifications above credential modal overlay', async ({
|
||||
n8n,
|
||||
page,
|
||||
}) => {
|
||||
await page.route('**/rest/credentials', async (route) => {
|
||||
if (route.request().method() === 'POST') {
|
||||
await route.abort('failed');
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
const projectId = await n8n.start.fromNewProject();
|
||||
await n8n.navigate.toCredentials(projectId);
|
||||
await n8n.credentials.addResourceButton.click();
|
||||
await n8n.credentials.actionCredentialButton.click();
|
||||
await n8n.credentials.selectCredentialType('Notion API');
|
||||
await n8n.canvas.credentialModal.fillField('apiKey', '1234567890');
|
||||
|
||||
const saveBtn = n8n.canvas.credentialModal.getSaveButton();
|
||||
await saveBtn.click();
|
||||
|
||||
const errorNotification = page.locator('.el-notification:has(.el-notification--error)');
|
||||
await expect(errorNotification).toBeVisible();
|
||||
await expect(n8n.canvas.credentialModal.getModal()).toBeVisible();
|
||||
|
||||
const modalOverlay = page.locator('.el-overlay').first();
|
||||
await expect(errorNotification).toHaveCSS('z-index', '2100');
|
||||
await expect(modalOverlay).toHaveCSS('z-index', '2001');
|
||||
});
|
||||
});
|
||||
@@ -29,8 +29,7 @@ async function addOpenAILanguageModelWithCredentials(
|
||||
options,
|
||||
);
|
||||
|
||||
await n8n.ndv.clickCreateNewCredential();
|
||||
await n8n.credentialsModal.setValues({
|
||||
await n8n.credentialsComposer.createFromNdv({
|
||||
apiKey: 'abcd',
|
||||
});
|
||||
await n8n.ndv.clickBackToCanvasButton();
|
||||
@@ -351,8 +350,7 @@ test.describe('Langchain Integration @capability:proxy', () => {
|
||||
{ closeNDV: false },
|
||||
);
|
||||
|
||||
await n8n.ndv.clickCreateNewCredential();
|
||||
await n8n.credentialsModal.setValues({
|
||||
await n8n.credentialsComposer.createFromNdv({
|
||||
password: 'testtesttest',
|
||||
});
|
||||
|
||||
|
||||
@@ -75,8 +75,9 @@ test.describe('Projects', () => {
|
||||
|
||||
await subn8n.canvas.deleteNodeByName('Replace me with your logic');
|
||||
await subn8n.canvas.addNode(NOTION_NODE_NAME, { action: 'Append a block' });
|
||||
|
||||
await subn8n.credentials.createAndSaveNewCredential('apiKey', NOTION_API_KEY);
|
||||
await subn8n.credentialsComposer.createFromNdv({
|
||||
apiKey: NOTION_API_KEY,
|
||||
});
|
||||
|
||||
await subn8n.ndv.clickBackToCanvasButton();
|
||||
await subn8n.canvas.saveWorkflow();
|
||||
|
||||
@@ -5,13 +5,17 @@ test.describe('OAuth Credentials', () => {
|
||||
const projectId = await n8n.start.fromNewProjectBlankCanvas();
|
||||
await page.goto(`projects/${projectId}/credentials`);
|
||||
await n8n.credentials.emptyListCreateCredentialButton.click();
|
||||
await n8n.credentials.openNewCredentialDialogFromCredentialList('Google OAuth2 API');
|
||||
await n8n.credentials.fillCredentialField('clientId', 'test-key');
|
||||
await n8n.credentials.fillCredentialField('clientSecret', 'test-secret');
|
||||
await n8n.credentials.saveCredential();
|
||||
await n8n.credentials.createCredentialFromCredentialPicker(
|
||||
'Google OAuth2 API',
|
||||
{
|
||||
clientId: 'test-key',
|
||||
clientSecret: 'test-secret',
|
||||
},
|
||||
{ closeDialog: false },
|
||||
);
|
||||
|
||||
const popupPromise = page.waitForEvent('popup');
|
||||
await n8n.credentials.getOauthConnectButton().click();
|
||||
await n8n.credentials.credentialModal.oauthConnectButton.click();
|
||||
|
||||
const popup = await popupPromise;
|
||||
const popupUrl = popup.url();
|
||||
@@ -25,7 +29,8 @@ test.describe('OAuth Credentials', () => {
|
||||
channel.postMessage('success');
|
||||
});
|
||||
|
||||
await expect(n8n.credentials.getSaveButton()).toContainText('Saved');
|
||||
await expect(n8n.credentials.getOauthConnectSuccessBanner()).toContainText('Account connected');
|
||||
await expect(n8n.credentials.credentialModal.oauthConnectSuccessBanner).toContainText(
|
||||
'Account connected',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -486,7 +486,9 @@ test.describe('NDV', () => {
|
||||
await n8n.canvas.addNode('Notion', { action: 'Update a database page', closeNDV: false });
|
||||
await expect(n8n.ndv.getContainer()).toBeVisible();
|
||||
|
||||
await n8n.credentials.createAndSaveNewCredential('apiKey', 'sk_test_123');
|
||||
await n8n.credentialsComposer.createFromNdv({
|
||||
apiKey: 'sk_test_123',
|
||||
});
|
||||
await n8n.ndv.addItemToFixedCollection('propertiesUi');
|
||||
await expect(
|
||||
n8n.ndv.getParameterInputWithIssues('propertiesUi.propertyValues[0].key'),
|
||||
@@ -631,8 +633,10 @@ test.describe('NDV', () => {
|
||||
await n8n.canvas.addNode('Manual Trigger');
|
||||
await n8n.canvas.addNode('Discord', { closeNDV: false, action: 'Delete a message' });
|
||||
await expect(n8n.ndv.getContainer()).toBeVisible();
|
||||
await n8n.credentialsComposer.createFromNdv({
|
||||
botToken: 'sk_test_123',
|
||||
});
|
||||
|
||||
await n8n.credentials.createAndSaveNewCredential('botToken', 'sk_test_123');
|
||||
const resourceInput = n8n.ndv.getParameterInputField('resource');
|
||||
const operationInput = n8n.ndv.getParameterInputField('operation');
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ test.describe('Logs', () => {
|
||||
|
||||
await n8n.canvas.logsPanel.getClearExecutionButton().click();
|
||||
await expect(n8n.canvas.logsPanel.getLogEntries()).toHaveCount(0);
|
||||
await expect(n8n.canvas.getNodeIssuesByName(NODES.CODE1)).not.toBeVisible();
|
||||
await expect(n8n.canvas.getNodeIssuesByName(NODES.CODE1)).toBeHidden();
|
||||
});
|
||||
|
||||
test('should allow to trigger partial execution', async ({ n8n, setupRequirements }) => {
|
||||
|
||||
@@ -11,7 +11,9 @@ test.describe('AI-716 Correctly set up agent model shows error', () => {
|
||||
|
||||
await n8n.canvas.addNode('OpenAI Chat Model');
|
||||
|
||||
await n8n.credentials.createAndSaveNewCredential('apiKey', 'sk-123');
|
||||
await n8n.credentialsComposer.createFromNdv({
|
||||
apiKey: 'sk-123',
|
||||
});
|
||||
|
||||
await n8n.page.keyboard.press('Escape');
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
|
||||
test.describe('04 - Credentials', () => {
|
||||
test('composer: createFromList creates credential', async ({ n8n }) => {
|
||||
const projectId = await n8n.start.fromNewProject();
|
||||
const credentialName = `credential-${nanoid()}`;
|
||||
await n8n.navigate.toCredentials(projectId);
|
||||
|
||||
await n8n.credentialsComposer.createFromList(
|
||||
'Notion API',
|
||||
{ apiKey: '1234567890' },
|
||||
{
|
||||
name: credentialName,
|
||||
closeDialog: false,
|
||||
},
|
||||
);
|
||||
await expect(n8n.credentials.getCredentialByName(credentialName)).toBeVisible();
|
||||
});
|
||||
|
||||
test('composer: createFromNdv creates credential for node', async ({ n8n }) => {
|
||||
const name = `credential-${nanoid()}`;
|
||||
await n8n.start.fromNewProjectBlankCanvas();
|
||||
await n8n.canvas.addNode('Manual Trigger');
|
||||
await n8n.canvas.addNode('Notion', { action: 'Append a block' });
|
||||
|
||||
await n8n.credentialsComposer.createFromNdv({ apiKey: '1234567890' }, { name });
|
||||
await expect(n8n.ndv.getCredentialSelect()).toHaveValue(name);
|
||||
});
|
||||
|
||||
test('composer: createFromApi creates credential (then NDV picks it up)', async ({ n8n }) => {
|
||||
const name = `credential-${nanoid()}`;
|
||||
const projectId = await n8n.start.fromNewProjectBlankCanvas();
|
||||
await n8n.credentialsComposer.createFromApi({
|
||||
name,
|
||||
type: 'notionApi',
|
||||
data: { apiKey: '1234567890' },
|
||||
projectId,
|
||||
});
|
||||
|
||||
await n8n.canvas.addNode('Manual Trigger');
|
||||
await n8n.canvas.addNode('Notion', { action: 'Append a block' });
|
||||
await expect(n8n.ndv.getCredentialSelect()).toHaveValue(name);
|
||||
});
|
||||
|
||||
test('create a new credential from empty state using the credential chooser list', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
const projectId = await n8n.start.fromNewProject();
|
||||
await n8n.navigate.toCredentials(projectId);
|
||||
await n8n.credentials.emptyListCreateCredentialButton.click();
|
||||
await n8n.credentials.createCredentialFromCredentialPicker('Notion API', {
|
||||
apiKey: '1234567890',
|
||||
});
|
||||
await expect(n8n.credentials.credentialCards).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('create a new credential from the NDV', async ({ n8n }) => {
|
||||
const uniqueCredentialName = `credential-${nanoid()}`;
|
||||
await n8n.start.fromNewProjectBlankCanvas();
|
||||
await n8n.canvas.addNode('Manual Trigger');
|
||||
await n8n.canvas.addNode('Notion', { action: 'Append a block' });
|
||||
|
||||
await n8n.ndv.getNodeCredentialsSelect().click();
|
||||
await n8n.ndv.credentialDropdownCreateNewCredential().click();
|
||||
await n8n.canvas.credentialModal.addCredential(
|
||||
{
|
||||
apiKey: '1234567890',
|
||||
},
|
||||
{ name: uniqueCredentialName },
|
||||
);
|
||||
await expect(n8n.ndv.getCredentialSelect()).toHaveValue(uniqueCredentialName);
|
||||
});
|
||||
|
||||
test('add an existing credential from the NDV', async ({ n8n, api }) => {
|
||||
const uniqueCredentialName = `credential-${nanoid()}`;
|
||||
const projectId = await n8n.start.fromNewProjectBlankCanvas();
|
||||
|
||||
await api.credentialApi.createCredential({
|
||||
name: uniqueCredentialName,
|
||||
type: 'notionApi',
|
||||
data: {
|
||||
apiKey: '1234567890',
|
||||
},
|
||||
projectId,
|
||||
});
|
||||
|
||||
await n8n.canvas.addNode('Manual Trigger');
|
||||
await n8n.canvas.addNode('Notion', { action: 'Append a block' });
|
||||
await expect(n8n.ndv.getCredentialSelect()).toHaveValue(uniqueCredentialName);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user