Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add credentials E2E test suite and page object #4596

Merged
merged 2 commits into from
Nov 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cypress.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module.exports = defineConfig({
e2e: {
baseUrl: 'http://localhost:5678',
video: false,
screenshotOnRunFailure: false,
screenshotOnRunFailure: true,
experimentalSessionAndOrigin: true,
experimentalInteractiveRunEvents: true,
}
Expand Down
2 changes: 1 addition & 1 deletion cypress/e2e/0-smoke.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const password = DEFAULT_USER_PASSWORD;
const firstName = randFirstName();
const lastName = randLastName();

describe('Authentication flow', () => {
describe('Authentication', () => {
it('should sign user up', () => {
cy.signup(username, firstName, lastName, password);
});
Expand Down
52 changes: 27 additions & 25 deletions cypress/e2e/1-workflows.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const lastName = randLastName();
const WorkflowsPage = new WorkflowsPageClass();
const WorkflowPage = new WorkflowPageClass();

describe('Workflows flow', () => {
describe('Workflows', () => {
beforeEach(() => {
cy.signup(username, firstName, lastName, password);

Expand All @@ -26,59 +26,61 @@ describe('Workflows flow', () => {
});

it('should create a new workflow using empty state card', () => {
WorkflowsPage.get('newWorkflowButtonCard').should('be.visible');
WorkflowsPage.get('newWorkflowButtonCard').click();
WorkflowsPage.getters.newWorkflowButtonCard().should('be.visible');
WorkflowsPage.getters.newWorkflowButtonCard().click();

cy.createFixtureWorkflow('Test_workflow_1.json', `Empty State Card Workflow ${uuid()}`);

WorkflowPage.get('workflowTags').should('contain.text', 'some-tag-1');
WorkflowPage.get('workflowTags').should('contain.text', 'some-tag-2');
WorkflowPage.getters.workflowTags().should('contain.text', 'some-tag-1');
WorkflowPage.getters.workflowTags().should('contain.text', 'some-tag-2');
})

it('should create a new workflow using add workflow button', () => {
WorkflowsPage.get('newWorkflowButtonCard').should('not.exist');
WorkflowsPage.get('createWorkflowButton').click();
WorkflowsPage.getters.newWorkflowButtonCard().should('not.exist');
WorkflowsPage.getters.createWorkflowButton().click();

cy.createFixtureWorkflow('Test_workflow_2.json', `Add Workflow Button Workflow ${uuid()}`);

WorkflowPage.get('workflowTags').should('contain.text', 'other-tag-1');
WorkflowPage.get('workflowTags').should('contain.text', 'other-tag-2');
WorkflowPage.getters.workflowTags().should('contain.text', 'other-tag-1');
WorkflowPage.getters.workflowTags().should('contain.text', 'other-tag-2');
})

it('should search for a workflow', () => {
WorkflowsPage.get('searchBar').type('Empty State Card Workflow');
WorkflowsPage.getters.searchBar().type('Empty State Card Workflow');

WorkflowsPage.get('workflowCards').should('have.length', 1);
WorkflowsPage.get('workflowCard', 'Empty State Card Workflow').should('contain.text', 'Empty State Card Workflow');
WorkflowsPage.getters.workflowCards().should('have.length', 1);
WorkflowsPage.getters.workflowCard('Empty State Card Workflow').should('contain.text', 'Empty State Card Workflow');

WorkflowsPage.get('searchBar').clear().type('Add Workflow Button Workflow');
WorkflowsPage.getters.searchBar().clear().type('Add Workflow Button Workflow');

WorkflowsPage.get('workflowCards').should('have.length', 1);
WorkflowsPage.get('workflowCard', 'Add Workflow Button Workflow').should('contain.text', 'Add Workflow Button Workflow');
WorkflowsPage.getters.workflowCards().should('have.length', 1);
WorkflowsPage.getters.workflowCard('Add Workflow Button Workflow').should('contain.text', 'Add Workflow Button Workflow');

WorkflowsPage.getters.searchBar().clear().type('Some non-existent workflow');
WorkflowsPage.getters.workflowCards().should('not.exist');

WorkflowsPage.get('searchBar').clear().type('Some non-existent workflow');
WorkflowsPage.get('workflowCards').should('not.exist');
cy.contains('No workflows found').should('be.visible');
})

it('should delete all the workflows', () => {
WorkflowsPage.get('workflowCards').should('have.length', 2);
WorkflowsPage.getters.workflowCards().should('have.length', 2);

WorkflowsPage.get('workflowCards').each(($el) => {
WorkflowsPage.getters.workflowCards().each(($el) => {
const workflowName = $el.find('[data-test-id="workflow-card-name"]').text();

WorkflowsPage.get('workflowCardActions', workflowName).click();
WorkflowsPage.get('workflowDeleteButton').click();
WorkflowsPage.getters.workflowCardActions(workflowName).click();
WorkflowsPage.getters.workflowDeleteButton().click();

cy.get('button').contains('delete').click();
})

WorkflowsPage.get('newWorkflowButtonCard').should('be.visible');
WorkflowsPage.get('newWorkflowTemplateCard').should('be.visible');
WorkflowsPage.getters.newWorkflowButtonCard().should('be.visible');
WorkflowsPage.getters.newWorkflowTemplateCard().should('be.visible');
})

it('should contain empty state cards', () => {
WorkflowsPage.get('newWorkflowButtonCard').should('be.visible');
WorkflowsPage.get('newWorkflowTemplateCard').should('be.visible');
WorkflowsPage.getters.newWorkflowButtonCard().should('be.visible');
WorkflowsPage.getters.newWorkflowTemplateCard().should('be.visible');
});

});
41 changes: 41 additions & 0 deletions cypress/e2e/2-credentials.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from "../constants";
import { randFirstName, randLastName } from "@ngneat/falso";
import { CredentialsPage, CredentialsModal } from '../pages';
// import { v4 as uuid } from 'uuid';

const username = DEFAULT_USER_EMAIL;
const password = DEFAULT_USER_PASSWORD;
const firstName = randFirstName();
const lastName = randLastName();
const credentialsPage = new CredentialsPage();
const credentialsModal = new CredentialsModal();

describe('Credentials', () => {
beforeEach(() => {
cy.signup(username, firstName, lastName, password);

cy.on('uncaught:exception', (err, runnable) => {
expect(err.message).to.include('Not logged in');

return false;
})

cy.signin(username, password);
cy.visit(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('API Key').type('1234567890');

credentialsModal.actions.setName('My awesome Notion account');
credentialsModal.actions.save();
});
});
13 changes: 2 additions & 11 deletions cypress/pages/base.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,6 @@
import { IE2ETestPage, IE2ETestPageElement } from "../types";


export class BasePage implements IE2ETestPage {
elements: Record<string, IE2ETestPageElement> = {};
get(id: keyof BasePage['elements'], ...args: unknown[]): ReturnType<IE2ETestPageElement> {
const getter = this.elements[id];

if (!getter) {
throw new Error(`No element with id "${id}" found. Check your page object definition.`);
}

return getter(...args);
}
getters: Record<string, IE2ETestPageElement> = {};
actions: Record<string, (...args: any[]) => void> = {};
}
17 changes: 17 additions & 0 deletions cypress/pages/credentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { BasePage } from "./base";

export class CredentialsPage extends BasePage {
url = '/credentials';
getters = {
emptyListCreateCredentialButton: () => cy.getByTestId('empty-resources-list').find('button'),
createCredentialButton: () => cy.getByTestId('resources-list-add'),
searchBar: () => cy.getByTestId('resources-list-search'),
credentialCards: () => cy.getByTestId('credential-card'),
credentialCard: (credentialName: string) => cy.getByTestId('credential-card')
.contains(credentialName)
.parents('[data-test-id="credential-card"]'),
credentialCardActions: (credentialName: string) => this.getters.credentialCard(credentialName)
.findChildByTestId('credential-card-actions'),
credentialDeleteButton: () => cy.getByTestId('action-toggle-dropdown').filter(':visible').contains('Delete')
};
}
2 changes: 2 additions & 0 deletions cypress/pages/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export * from './base';
export * from './credentials';
export * from './signin';
export * from './signup';
export * from './workflows';
export * from './modals';
26 changes: 26 additions & 0 deletions cypress/pages/modals/credentials-modal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { BasePage } from "../base";

export class CredentialsModal extends BasePage {
getters = {
newCredentialModal: () => cy.getByTestId('selectCredential-modal', { timeout: 5000 }),
newCredentialTypeSelect: () => cy.getByTestId('new-credential-type-select'),
newCredentialTypeOption: (credentialType: string) => cy.getByTestId('new-credential-type-select-option').contains(credentialType),
newCredentialTypeButton: () => cy.getByTestId('new-credential-type-button'),
connectionParameters: () => cy.getByTestId('credential-connection-parameter'),
connectionParameter: (fieldName: string) => this.getters.connectionParameters().contains(fieldName)
.parents('[data-test-id="credential-connection-parameter"]')
.find('.n8n-input input'),
name: () => cy.getByTestId('credential-name'),
nameInput: () => cy.getByTestId('credential-name').find('input'),
saveButton: () => cy.getByTestId('credential-save-button')
};
actions = {
setName: (name: string) => {
this.getters.name().click();
this.getters.nameInput().clear().type(name);
},
save: () => {
this.getters.saveButton().click();
}
};
}
1 change: 1 addition & 0 deletions cypress/pages/modals/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './credentials-modal';
2 changes: 1 addition & 1 deletion cypress/pages/signin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { BasePage } from "./base";

export class SigninPage extends BasePage {
url = '/signin';
elements = {
getters = {
form: () => cy.getByTestId('auth-form'),
email: () => cy.getByTestId('email'),
password: () => cy.getByTestId('password'),
Expand Down
2 changes: 1 addition & 1 deletion cypress/pages/signup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { BasePage } from "./base";

export class SignupPage extends BasePage {
url = '/setup';
elements = {
getters = {
form: () => cy.getByTestId('auth-form'),
email: () => cy.getByTestId('email'),
firstName: () => cy.getByTestId('firstName'),
Expand Down
2 changes: 1 addition & 1 deletion cypress/pages/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { BasePage } from "./base";

export class WorkflowPage extends BasePage {
url = '/workflow/new';
elements = {
getters = {
workflowNameInput: () => cy.getByTestId('workflow-name-input').then($el => cy.wrap($el.find('input'))),
workflowImportInput: () => cy.getByTestId('workflow-import-input'),
workflowTags: () => cy.getByTestId('workflow-tags'),
Expand Down
10 changes: 5 additions & 5 deletions cypress/pages/workflows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { BasePage } from "./base";

export class WorkflowsPage extends BasePage {
url = '/workflows';
elements = {
getters = {
newWorkflowButtonCard: () => cy.getByTestId('new-workflow-card'),
newWorkflowTemplateCard: () => cy.getByTestId('new-workflow-template-card'),
searchBar: () => cy.getByTestId('resources-list-search'),
Expand All @@ -11,13 +11,13 @@ export class WorkflowsPage extends BasePage {
workflowCard: (workflowName: string) => cy.getByTestId(`workflow-card`)
.contains(workflowName)
.parents('[data-test-id="workflow-card"]'),
workflowTags: (workflowName: string) => this.elements.workflowCard(workflowName)
workflowTags: (workflowName: string) => this.getters.workflowCard(workflowName)
.findChildByTestId('workflow-card-tags'),
workflowActivator: (workflowName: string) => this.elements.workflowCard(workflowName)
workflowActivator: (workflowName: string) => this.getters.workflowCard(workflowName)
.findChildByTestId('workflow-card-activator'),
workflowActivatorStatus: (workflowName: string) => this.elements.workflowActivator(workflowName)
workflowActivatorStatus: (workflowName: string) => this.getters.workflowActivator(workflowName)
.findChildByTestId('workflow-activator-status'),
workflowCardActions: (workflowName: string) => this.elements.workflowCard(workflowName)
workflowCardActions: (workflowName: string) => this.getters.workflowCard(workflowName)
.findChildByTestId('workflow-card-actions'),
workflowDeleteButton: () => cy.getByTestId('action-toggle-dropdown').filter(':visible').contains('Delete')
// Not yet implemented
Expand Down
32 changes: 16 additions & 16 deletions cypress/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,13 @@ Cypress.Commands.add('createFixtureWorkflow', (fixtureKey, workflowName) => {
const WorkflowPage = new WorkflowPageClass()

// We need to force the click because the input is hidden
WorkflowPage.get('workflowImportInput').selectFile(`cypress/fixtures/${fixtureKey}`, { force: true});
WorkflowPage.get('workflowNameInput').should('be.disabled');
WorkflowPage.get('workflowNameInput').parent().click()
WorkflowPage.get('workflowNameInput').should('be.enabled');
WorkflowPage.get('workflowNameInput').clear().type(workflowName).type('{enter}');
WorkflowPage.getters.workflowImportInput().selectFile(`cypress/fixtures/${fixtureKey}`, { force: true});
WorkflowPage.getters.workflowNameInput().should('be.disabled');
WorkflowPage.getters.workflowNameInput().parent().click()
WorkflowPage.getters.workflowNameInput().should('be.enabled');
WorkflowPage.getters.workflowNameInput().clear().type(workflowName).type('{enter}');

WorkflowPage.get('saveButton').should('contain', 'Saved');
WorkflowPage.getters.saveButton().should('contain', 'Saved');
})

Cypress.Commands.add('findChildByTestId', { prevSubject: true }, (subject: Cypress.Chainable<JQuery<HTMLElement>>, childTestId) => {
Expand All @@ -58,10 +58,10 @@ Cypress.Commands.add(
cy.session([email, password], () => {
cy.visit(signinPage.url);

signinPage.get('form').within(() => {
signinPage.get('email').type(email);
signinPage.get('password').type(password);
signinPage.get('submit').click();
signinPage.getters.form().within(() => {
signinPage.getters.email().type(email);
signinPage.getters.password().type(password);
signinPage.getters.submit().click();
});

// we should be redirected to /workflows
Expand All @@ -79,14 +79,14 @@ Cypress.Commands.add('signup', (email, firstName, lastName, password) => {

cy.visit(signupPage.url);

signupPage.get('form').within(() => {
signupPage.getters.form().within(() => {
cy.url().then((url) => {
if (url.endsWith(signupPage.url)) {
signupPage.get('email').type(email);
signupPage.get('firstName').type(firstName);
signupPage.get('lastName').type(lastName);
signupPage.get('password').type(password);
signupPage.get('submit').click();
signupPage.getters.email().type(email);
signupPage.getters.firstName().type(firstName);
signupPage.getters.lastName().type(lastName);
signupPage.getters.password().type(password);
signupPage.getters.submit().click();
} else {
cy.log('User already signed up');
}
Expand Down
5 changes: 3 additions & 2 deletions cypress/types.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
export type IE2ETestPageElement = (...args: any[]) =>
| Cypress.Chainable<JQuery<HTMLElement>>
| Cypress.Chainable<JQuery<HTMLInputElement>>
| Cypress.Chainable<JQuery<HTMLButtonElement>>;

export interface IE2ETestPage {
url?: string;
elements: Record<string, IE2ETestPageElement>;
get(id: string, ...args: unknown[]): ReturnType<IE2ETestPageElement>;
getters: Record<string, IE2ETestPageElement>;
actions: Record<string, (...args: any[]) => void>;
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@
"webhook": "./packages/cli/bin/n8n webhook",
"worker": "./packages/cli/bin/n8n worker",
"cypress:install": "cypress install",
"test:e2e:db:clean": "rimraf ~/.n8n/cypress.sqlite ~/.n8n/cypress.sqlite.bak",
"test:e2e:db:clean": "rimraf ~/.n8n/cypress.sqlite",
"test:e2e:cypress:run": "cypress run",
"test:e2e": "pnpm test:e2e:db:clean && cross-env DB_SQLITE_DATABASE=cypress.sqlite N8N_DIAGNOSTICS_ENABLED=false start-server-and-test start http://localhost:5678/favicon.ico test:e2e:cypress:run",
"test:e2e:cypress:dev": "cypress open",
"test:e2e:dev": "pnpm test:e2e:db:clean && cross-env DB_SQLITE_DATABASE=cypress.sqlite N8N_DIAGNOSTICS_ENABLED=false start-server-and-test start http://localhost:5678/favicon.ico test:e2e:cypress:dev",
"test:e2e:dev": "pnpm test:e2e:db:clean && cross-env DB_SQLITE_DATABASE=cypress.sqlite N8N_DIAGNOSTICS_ENABLED=false CYPRESS_BASE_URL=http://localhost:8080 start-server-and-test dev http://localhost:8080/favicon.ico test:e2e:cypress:dev",
"test:e2e:cypress:ci:smoke": "cypress run --headless --spec \"cypress/e2e/0-smoke.cy.ts\"",
"test:e2e:ci:smoke": "pnpm test:e2e:db:clean && cross-env DB_SQLITE_DATABASE=cypress.sqlite N8N_DIAGNOSTICS_ENABLED=false start-server-and-test start http://localhost:5678/favicon.ico test:e2e:cypress:ci:smoke"
},
Expand Down
1 change: 1 addition & 0 deletions packages/editor-ui/src/components/CredentialCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<n8n-card
:class="$style['card-link']"
@click="onClick"
data-test-id="credential-card"
>
<template #prepend>
<credential-icon :credential-type-name="credentialType ? credentialType.name : ''" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
:readonly="!credentialPermissions.updateName"
type="Credential"
@input="onNameEdit"
data-test-id="credential-name"
/>
</div>
<div :class="$style.credActions">
Expand All @@ -32,6 +33,7 @@
:disabled="isSaving"
:loading="isDeleting"
@click="deleteCredential"
data-test-id="credential-delete-button"
/>
<SaveButton
v-if="(hasUnsavedChanges || credentialId) && credentialPermissions.save"
Expand All @@ -41,6 +43,7 @@
? $locale.baseText('credentialEdit.credentialEdit.testing')
: $locale.baseText('credentialEdit.credentialEdit.saving')"
@click="saveCredential"
data-test-id="credential-save-button"
/>
</div>
</div>
Expand Down
Loading