diff --git a/cypress/composables/modals/workflow-credential-setup-modal.ts b/cypress/composables/modals/workflow-credential-setup-modal.ts new file mode 100644 index 0000000000..6e903569d0 --- /dev/null +++ b/cypress/composables/modals/workflow-credential-setup-modal.ts @@ -0,0 +1,12 @@ +/** + * Getters + */ + +export const getWorkflowCredentialsModal = () => cy.getByTestId('setup-workflow-credentials-modal'); + +/** + * Actions + */ + +export const closeModal = () => + getWorkflowCredentialsModal().find("button[aria-label='Close this dialog']").click(); diff --git a/cypress/composables/setup-template-form-step.ts b/cypress/composables/setup-template-form-step.ts new file mode 100644 index 0000000000..6f01662783 --- /dev/null +++ b/cypress/composables/setup-template-form-step.ts @@ -0,0 +1,14 @@ +/** + * Getters + */ + +export const getFormStep = () => cy.getByTestId('setup-credentials-form-step'); + +export const getStepHeading = ($el: JQuery) => + cy.wrap($el).findChildByTestId('credential-step-heading'); + +export const getStepDescription = ($el: JQuery) => + cy.wrap($el).findChildByTestId('credential-step-description'); + +export const getCreateAppCredentialsButton = (appName: string) => + cy.get(`button:contains("Create new ${appName} credential")`); diff --git a/cypress/composables/setup-workflow-credentials-button.ts b/cypress/composables/setup-workflow-credentials-button.ts new file mode 100644 index 0000000000..0e19fb23e2 --- /dev/null +++ b/cypress/composables/setup-workflow-credentials-button.ts @@ -0,0 +1,5 @@ +/** + * Getters + */ + +export const getSetupWorkflowCredentialsButton = () => cy.get(`button:contains("Set up Template")`); diff --git a/cypress/e2e/34-template-credentials-setup.cy.ts b/cypress/e2e/34-template-credentials-setup.cy.ts index a55f45d24e..e8fadc1413 100644 --- a/cypress/e2e/34-template-credentials-setup.cy.ts +++ b/cypress/e2e/34-template-credentials-setup.cy.ts @@ -6,6 +6,9 @@ import { import * as templateCredentialsSetupPage from '../pages/template-credential-setup'; import { TemplateWorkflowPage } from '../pages/template-workflow'; import { WorkflowPage } from '../pages/workflow'; +import * as formStep from '../composables/setup-template-form-step'; +import { getSetupWorkflowCredentialsButton } from '../composables/setup-workflow-credentials-button'; +import * as setupCredsModal from '../composables/modals/workflow-credential-setup-modal'; const templateWorkflowPage = new TemplateWorkflowPage(); const workflowPage = new WorkflowPage(); @@ -69,13 +72,9 @@ describe('Template credentials setup', () => { 'The credential you select will be used in the Telegram node of the workflow template.', ]; - templateCredentialsSetupPage.getters.appCredentialSteps().each(($el, index) => { - templateCredentialsSetupPage.getters - .stepHeading($el) - .should('have.text', expectedAppNames[index]); - templateCredentialsSetupPage.getters - .stepDescription($el) - .should('have.text', expectedAppDescriptions[index]); + formStep.getFormStep().each(($el, index) => { + formStep.getStepHeading($el).should('have.text', expectedAppNames[index]); + formStep.getStepDescription($el).should('have.text', expectedAppDescriptions[index]); }); }); @@ -100,10 +99,7 @@ describe('Template credentials setup', () => { templateCredentialsSetupPage.fillInDummyCredentialsForAppWithConfirm('X (Formerly Twitter)'); templateCredentialsSetupPage.fillInDummyCredentialsForApp('Telegram'); - cy.intercept('POST', '/rest/workflows').as('createWorkflow'); - templateCredentialsSetupPage.getters.continueButton().should('be.enabled'); - templateCredentialsSetupPage.getters.continueButton().click(); - cy.wait('@createWorkflow'); + templateCredentialsSetupPage.finishCredentialSetup(); workflowPage.getters.canvasNodes().should('have.length', 3); @@ -137,13 +133,9 @@ describe('Template credentials setup', () => { 'The credential you select will be used in the Nextcloud node of the workflow template.', ]; - templateCredentialsSetupPage.getters.appCredentialSteps().each(($el, index) => { - templateCredentialsSetupPage.getters - .stepHeading($el) - .should('have.text', expectedAppNames[index]); - templateCredentialsSetupPage.getters - .stepDescription($el) - .should('have.text', expectedAppDescriptions[index]); + formStep.getFormStep().each(($el, index) => { + formStep.getStepHeading($el).should('have.text', expectedAppNames[index]); + formStep.getStepDescription($el).should('have.text', expectedAppDescriptions[index]); }); templateCredentialsSetupPage.getters.continueButton().should('be.disabled'); @@ -151,11 +143,68 @@ describe('Template credentials setup', () => { templateCredentialsSetupPage.fillInDummyCredentialsForApp('Email (IMAP)'); templateCredentialsSetupPage.fillInDummyCredentialsForApp('Nextcloud'); - cy.intercept('POST', '/rest/workflows').as('createWorkflow'); - templateCredentialsSetupPage.getters.continueButton().should('be.enabled'); - templateCredentialsSetupPage.getters.continueButton().click(); - cy.wait('@createWorkflow'); + templateCredentialsSetupPage.finishCredentialSetup(); workflowPage.getters.canvasNodes().should('have.length', 3); }); + + describe('Credential setup from workflow editor', () => { + beforeEach(() => { + cy.resetDatabase(); + cy.signinAsOwner(); + }); + + it('should allow credential setup from workflow editor if user skips it during template setup', () => { + templateCredentialsSetupPage.visitTemplateCredentialSetupPage(testTemplate.id); + templateCredentialsSetupPage.getters.skipLink().click(); + + getSetupWorkflowCredentialsButton().should('be.visible'); + + // We need to save the workflow or otherwise a browser native popup + // will block cypress from continuing + workflowPage.actions.saveWorkflowOnButtonClick(); + }); + + it('should allow credential setup from workflow editor if user fills in credentials partially during template setup', () => { + templateCredentialsSetupPage.visitTemplateCredentialSetupPage(testTemplate.id); + templateCredentialsSetupPage.fillInDummyCredentialsForApp('Shopify'); + + templateCredentialsSetupPage.finishCredentialSetup(); + + getSetupWorkflowCredentialsButton().should('be.visible'); + }); + + it('should fill credentials from workflow editor', () => { + templateCredentialsSetupPage.visitTemplateCredentialSetupPage(testTemplate.id); + templateCredentialsSetupPage.getters.skipLink().click(); + + getSetupWorkflowCredentialsButton().click(); + setupCredsModal.getWorkflowCredentialsModal().should('be.visible'); + + templateCredentialsSetupPage.fillInDummyCredentialsForApp('Shopify'); + templateCredentialsSetupPage.fillInDummyCredentialsForAppWithConfirm('X (Formerly Twitter)'); + templateCredentialsSetupPage.fillInDummyCredentialsForApp('Telegram'); + + setupCredsModal.closeModal(); + + // Focus the canvas so the copy to clipboard works + workflowPage.getters.canvasNodes().eq(0).realClick(); + workflowPage.actions.selectAll(); + workflowPage.actions.hitCopy(); + + cy.grantBrowserPermissions('clipboardReadWrite', 'clipboardSanitizedWrite'); + // Check workflow JSON by copying it to clipboard + cy.readClipboard().then((workflowJSON) => { + const workflow = JSON.parse(workflowJSON); + + workflow.nodes.forEach((node: any) => { + expect(Object.keys(node.credentials ?? {})).to.have.lengthOf(1); + }); + }); + + // We need to save the workflow or otherwise a browser native popup + // will block cypress from continuing + workflowPage.actions.saveWorkflowOnButtonClick(); + }); + }); }); diff --git a/cypress/pages/template-credential-setup.ts b/cypress/pages/template-credential-setup.ts index 41fdcfc295..75f75f6ffc 100644 --- a/cypress/pages/template-credential-setup.ts +++ b/cypress/pages/template-credential-setup.ts @@ -1,4 +1,5 @@ import { CredentialsModal, MessageBox } from './modals'; +import * as formStep from '../composables/setup-template-form-step'; export type TemplateTestData = { id: number; @@ -24,17 +25,6 @@ export const getters = { skipLink: () => cy.get('a:contains("Skip")'), title: (title: string) => cy.get(`h1:contains(${title})`), infoCallout: () => cy.getByTestId('info-callout'), - createAppCredentialsButton: (appName: string) => - cy.get(`button:contains("Create new ${appName} credential")`), - appCredentialSteps: () => cy.getByTestId('setup-credentials-form-step'), - stepHeading: ($el: JQuery) => - cy.wrap($el).findChildByTestId('credential-step-heading'), - stepDescription: ($el: JQuery) => - cy.wrap($el).findChildByTestId('credential-step-description'), -}; - -export const visitTemplateCredentialSetupPage = (templateId: number) => { - cy.visit(`/templates/${templateId}/setup`); }; export const enableTemplateCredentialSetupFeatureFlag = () => { @@ -43,11 +33,17 @@ export const enableTemplateCredentialSetupFeatureFlag = () => { }); }; +export const visitTemplateCredentialSetupPage = (templateId: number) => { + cy.visit(`/templates/${templateId}/setup`); + formStep.getFormStep().eq(0).should('be.visible'); + enableTemplateCredentialSetupFeatureFlag(); +}; + /** * Fills in dummy credentials for the given app name. */ export const fillInDummyCredentialsForApp = (appName: string) => { - getters.createAppCredentialsButton(appName).click(); + formStep.getCreateAppCredentialsButton(appName).click(); credentialsModal.getters.editCredentialModal().find('input:first()').type('test'); credentialsModal.actions.save(false); credentialsModal.actions.close(); @@ -62,3 +58,13 @@ export const fillInDummyCredentialsForAppWithConfirm = (appName: string) => { fillInDummyCredentialsForApp(appName); messageBox.actions.cancel(); }; + +/** + * Finishes the credential setup by clicking the continue button. + */ +export const finishCredentialSetup = () => { + cy.intercept('POST', '/rest/workflows').as('createWorkflow'); + getters.continueButton().should('be.enabled'); + getters.continueButton().click(); + cy.wait('@createWorkflow'); +}; diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 23460c365f..238ea0d2a2 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -1,6 +1,12 @@ import 'cypress-real-events'; import { WorkflowPage } from '../pages'; -import { BACKEND_BASE_URL, INSTANCE_MEMBERS, INSTANCE_OWNER, N8N_AUTH_COOKIE } from '../constants'; +import { + BACKEND_BASE_URL, + INSTANCE_ADMIN, + INSTANCE_MEMBERS, + INSTANCE_OWNER, + N8N_AUTH_COOKIE, +} from '../constants'; Cypress.Commands.add('getByTestId', (selector, ...args) => { return cy.get(`[data-test-id="${selector}"]`, ...args); @@ -51,6 +57,10 @@ Cypress.Commands.add('signin', ({ email, password }) => { ); }); +Cypress.Commands.add('signinAsOwner', () => { + cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password }); +}); + Cypress.Commands.add('signout', () => { cy.request('POST', `${BACKEND_BASE_URL}/rest/logout`); cy.getCookie(N8N_AUTH_COOKIE).should('not.exist'); @@ -183,3 +193,11 @@ Cypress.Commands.add('shouldNotHaveConsoleErrors', () => { cy.wrap(spy).should('not.have.been.called'); }); }); + +Cypress.Commands.add('resetDatabase', () => { + cy.request('POST', `${BACKEND_BASE_URL}/rest/e2e/reset`, { + owner: INSTANCE_OWNER, + members: INSTANCE_MEMBERS, + admin: INSTANCE_ADMIN, + }); +}); diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index 74a92a111d..1a209d66b9 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -1,12 +1,8 @@ -import { BACKEND_BASE_URL, INSTANCE_ADMIN, INSTANCE_MEMBERS, INSTANCE_OWNER } from '../constants'; +import { INSTANCE_OWNER } from '../constants'; import './commands'; before(() => { - cy.request('POST', `${BACKEND_BASE_URL}/rest/e2e/reset`, { - owner: INSTANCE_OWNER, - members: INSTANCE_MEMBERS, - admin: INSTANCE_ADMIN, - }); + cy.resetDatabase(); Cypress.on('uncaught:exception', (err) => { return !err.message.includes('ResizeObserver'); diff --git a/cypress/support/index.ts b/cypress/support/index.ts index 09246697ce..f31e50c578 100644 --- a/cypress/support/index.ts +++ b/cypress/support/index.ts @@ -23,6 +23,7 @@ declare global { findChildByTestId(childTestId: string): Chainable>; createFixtureWorkflow(fixtureKey: string, workflowName: string): void; signin(payload: SigninPayload): void; + signinAsOwner(): void; signout(): void; interceptREST(method: string, url: string): Chainable; enableFeature(feature: string): void; @@ -48,6 +49,7 @@ declare global { }; } >; + resetDatabase(): void; } } } diff --git a/packages/editor-ui/src/components/Modals.vue b/packages/editor-ui/src/components/Modals.vue index e1454215b2..894a343977 100644 --- a/packages/editor-ui/src/components/Modals.vue +++ b/packages/editor-ui/src/components/Modals.vue @@ -166,7 +166,7 @@