feat: Add cypress e2e tests for signup and signin (#3490)

* feat: Added cypress setup files.

* feat: Added server bootup and initial test run.

* feat: Added e2e tests for signin, signup, and personalization form.

* feat: Added e2e tests for adding a function node.

* feat: Added set node and workflow execution steps.

* feat: Added test id to main sidebar.

* feat: Added test for creating a new workflow.

* feat: Finished test for creating a blank workflow

* chore: Removed screenshots from e2e tests.

* refactor: change e2e tests to per page structure

* feat: add cypress type enchancements

* feat: add typescript for cypress tests

* fix: remove component after merge

* feat: update cypress definitions

* feat: add cypress cleanup task

* refactor: update cypress script names

* ci: add smoke tests to workflow

* chore: remove cypress example files

* feat: update signup flow to be reusable

* fix: fix signup route for cypress page object

* fix: remove cypress reset command

* fix: remove unused imports

* fix: Add unhandled error catcher
This commit is contained in:
Alex Grozav 2022-11-08 14:21:10 +02:00 committed by GitHub
parent 5d73b6e48a
commit 77644860c0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 2008 additions and 7 deletions

View file

@ -31,7 +31,12 @@ jobs:
run: npm run build --if-present run: npm run build --if-present
- name: Test - name: Test
run: npm run test run:
npm run test
- name: Test E2E
run:
npm run test:e2e:ci:smoke
- name: Lint - name: Lint
run: npm run lint run: npm run lint

View file

@ -30,6 +30,10 @@ jobs:
- name: Test - name: Test
run: npm run test run: npm run test
- name: Test E2E
run:
npm run test:e2e:ci:smoke
- name: Fetch base branch for `git diff` - name: Fetch base branch for `git diff`
run: git fetch origin ${{ github.event.pull_request.base.ref }}:${{ github.event.pull_request.base.ref }} run: git fetch origin ${{ github.event.pull_request.base.ref }}:${{ github.event.pull_request.base.ref }}

2
.gitignore vendored
View file

@ -15,4 +15,6 @@ _START_PACKAGE
nodelinter.config.json nodelinter.config.json
packages/*/package-lock.json packages/*/package-lock.json
packages/*/.turbo packages/*/.turbo
cypress/videos/*
cypress/screenshots/*
*.swp *.swp

12
cypress.config.js Normal file
View file

@ -0,0 +1,12 @@
const { defineConfig } = require("cypress");
module.exports = defineConfig({
e2e: {
baseUrl: 'http://localhost:5678',
video: false,
screenshotOnRunFailure: false,
experimentalSessionAndOrigin: true,
experimentalInteractiveRunEvents: true,
}
});

4
cypress/constants.ts Normal file
View file

@ -0,0 +1,4 @@
export const N8N_AUTH_COOKIE = 'n8n-auth';
export const DEFAULT_USER_EMAIL = 'nathan@n8n.io';
export const DEFAULT_USER_PASSWORD = 'CypressTest123';

23
cypress/e2e/0-smoke.cy.ts Normal file
View file

@ -0,0 +1,23 @@
import {DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD} from "../constants";
import {randFirstName, randLastName} from "@ngneat/falso";
const username = DEFAULT_USER_EMAIL;
const password = DEFAULT_USER_PASSWORD;
const firstName = randFirstName();
const lastName = randLastName();
describe('Authentication flow', () => {
it('should sign user up', () => {
cy.signup(username, firstName, lastName, password);
});
it('should sign user in', () => {
cy.on('uncaught:exception', (err, runnable) => {
expect(err.message).to.include('Not logged in');
return false;
})
cy.signin(username, password);
});
});

15
cypress/pages/base.ts Normal file
View file

@ -0,0 +1,15 @@
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);
}
}

4
cypress/pages/index.ts Normal file
View file

@ -0,0 +1,4 @@
export * from './base';
export * from './signin';
export * from './signup';
export * from './workflows';

11
cypress/pages/signin.ts Normal file
View file

@ -0,0 +1,11 @@
import { BasePage } from "./base";
export class SigninPage extends BasePage {
url = '/signin';
elements = {
form: () => cy.getByTestId('auth-form'),
email: () => cy.getByTestId('email'),
password: () => cy.getByTestId('password'),
submit: () => cy.get('button'),
}
}

13
cypress/pages/signup.ts Normal file
View file

@ -0,0 +1,13 @@
import { BasePage } from "./base";
export class SignupPage extends BasePage {
url = '/setup';
elements = {
form: () => cy.getByTestId('auth-form'),
email: () => cy.getByTestId('email'),
firstName: () => cy.getByTestId('firstName'),
lastName: () => cy.getByTestId('lastName'),
password: () => cy.getByTestId('password'),
submit: () => cy.get('button'),
}
}

View file

@ -0,0 +1,6 @@
import { BasePage } from "./base";
export class WorkflowsPage extends BasePage {
url = '/workflows';
elements = {}
}

View file

@ -0,0 +1,78 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
import { WorkflowsPage, SigninPage, SignupPage } from "../pages";
import { N8N_AUTH_COOKIE } from "../constants";
Cypress.Commands.add('getByTestId', (selector, ...args) => {
return cy.get(`[data-test-id="${selector}"]`, ...args)
})
Cypress.Commands.add(
'signin',
(email, password) => {
const signinPage = new SigninPage();
const workflowsPage = new WorkflowsPage();
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();
});
// we should be redirected to /workflows
cy.url().should('include', workflowsPage.url);
},
{
validate() {
cy.getCookie(N8N_AUTH_COOKIE).should('exist');
},
});
});
Cypress.Commands.add('signup', (email, firstName, lastName, password) => {
const signupPage = new SignupPage();
cy.visit(signupPage.url);
signupPage.get('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();
} else {
cy.log('User already signed up');
}
});
});
})

17
cypress/support/e2e.ts Normal file
View file

@ -0,0 +1,17 @@
// ***********************************************************
// This example support/e2e.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
import './commands'

14
cypress/support/index.ts Normal file
View file

@ -0,0 +1,14 @@
// Load type definitions that come with Cypress module
/// <reference types="cypress" />
declare global {
namespace Cypress {
interface Chainable {
getByTestId(selector: string, ...args: (Partial<Loggable & Timeoutable & Withinable & Shadow> | undefined)[]): Chainable<JQuery<HTMLElement>>
signin(email: string, password: string): void;
signup(email: string, firstName: string, lastName: string, password: string): void;
}
}
}
export {};

9
cypress/types.ts Normal file
View file

@ -0,0 +1,9 @@
export type IE2ETestPageElement = (...args: unknown[]) =>
| Cypress.Chainable<JQuery<HTMLElement>>
| Cypress.Chainable<JQuery<HTMLButtonElement>>;
export interface IE2ETestPage {
url?: string;
elements: Record<string, IE2ETestPageElement>;
get(id: string, ...args: unknown[]): ReturnType<IE2ETestPageElement>;
}

1765
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -19,18 +19,30 @@
"test": "turbo run test", "test": "turbo run test",
"watch": "turbo run watch", "watch": "turbo run watch",
"webhook": "./packages/cli/bin/n8n webhook", "webhook": "./packages/cli/bin/n8n webhook",
"worker": "./packages/cli/bin/n8n worker" "worker": "./packages/cli/bin/n8n worker",
"test:e2e:db:clean": "rimraf ~/.n8n/cypress.sqlite ~/.n8n/cypress.sqlite.bak",
"test:e2e:cypress:run": "cypress run",
"test:e2e": "npm run test:e2e:db:clean && cross-env DB_FILE=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": "npm run test:e2e:db:clean && cross-env DB_FILE=cypress.sqlite N8N_DIAGNOSTICS_ENABLED=false start-server-and-test start http://localhost:5678/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": "npm run test:e2e:db:clean && cross-env DB_FILE=cypress.sqlite N8N_DIAGNOSTICS_ENABLED=false start-server-and-test start http://localhost:5678/favicon.ico test:e2e:cypress:ci:smoke"
}, },
"devDependencies": { "devDependencies": {
"@ngneat/falso": "^6.1.0",
"@types/jest": "^28.1.8", "@types/jest": "^28.1.8",
"cross-env": "^7.0.3",
"cypress": "^10.0.3",
"jest": "^28.1.3", "jest": "^28.1.3",
"jest-environment-jsdom": "^28.1.3", "jest-environment-jsdom": "^28.1.3",
"jest-mock": "^28.1.3", "jest-mock": "^28.1.3",
"patch-package": "^6.4.7", "patch-package": "^6.4.7",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"run-script-os": "^1.0.7", "run-script-os": "^1.0.7",
"start-server-and-test": "^1.14.0",
"ts-jest": "^28.0.8", "ts-jest": "^28.0.8",
"turbo": "1.2.15" "turbo": "1.2.15",
"typescript": "^4.8.4"
}, },
"postcss": {}, "postcss": {},
"workspaces": [ "workspaces": [

View file

@ -16,6 +16,7 @@
v-bind="input.properties" v-bind="input.properties"
:name="input.name" :name="input.name"
:value="values[input.name]" :value="values[input.name]"
:data-test-id="input.name"
:showValidationWarnings="showValidationWarnings" :showValidationWarnings="showValidationWarnings"
@input="(value) => onInput(input.name, value)" @input="(value) => onInput(input.name, value)"
@validate="(value) => onValidate(input.name, value)" @validate="(value) => onValidate(input.name, value)"

View file

@ -17,7 +17,7 @@
<div slot="content" :class="$style.triggerWarning"> <div slot="content" :class="$style.triggerWarning">
{{ $locale.baseText('ndv.backToCanvas.waitingForTriggerWarning') }} {{ $locale.baseText('ndv.backToCanvas.waitingForTriggerWarning') }}
</div> </div>
<div :class="$style.backToCanvas" @click="close"> <div :class="$style.backToCanvas" @click="close" data-test-id="back-to-canvas">
<n8n-icon icon="arrow-left" color="text-xlight" size="medium" /> <n8n-icon icon="arrow-left" color="text-xlight" size="medium" />
<n8n-text color="text-xlight" size="medium" :bold="true"> <n8n-text color="text-xlight" size="medium" :bold="true">
{{ $locale.baseText('ndv.backToCanvas') }} {{ $locale.baseText('ndv.backToCanvas') }}

View file

@ -66,7 +66,11 @@
</template> </template>
</i18n> </i18n>
</div> </div>
<div class="node-parameters-wrapper" v-if="node && nodeValid"> <div
class="node-parameters-wrapper"
data-test-id="node-parameters"
v-if="node && nodeValid"
>
<div v-show="openPanel === 'params'"> <div v-show="openPanel === 'params'">
<node-webhooks :node="node" :nodeType="nodeType" /> <node-webhooks :node="node" :nodeType="nodeType" />

View file

@ -13,6 +13,7 @@
:closeOnClickModal="false" :closeOnClickModal="false"
:closeOnPressEscape="false" :closeOnPressEscape="false"
width="460px" width="460px"
data-test-id="personalization-form"
@enter="onSave" @enter="onSave"
> >
<template v-slot:content> <template v-slot:content>

View file

@ -1,5 +1,5 @@
<template> <template>
<span :class="$style.container"> <span :class="$style.container" data-test-id="save-button">
<span :class="$style.saved" v-if="saved">{{ $locale.baseText('saveButton.saved') }}</span> <span :class="$style.saved" v-if="saved">{{ $locale.baseText('saveButton.saved') }}</span>
<n8n-button <n8n-button
v-else v-else

View file

@ -9,6 +9,7 @@
<div :class="$style.formContainer"> <div :class="$style.formContainer">
<n8n-form-box <n8n-form-box
v-bind="form" v-bind="form"
data-test-id="auth-form"
:buttonLoading="formLoading" :buttonLoading="formLoading"
@secondaryClick="onSecondaryClick" @secondaryClick="onSecondaryClick"
@submit="onSubmit" @submit="onSubmit"

View file

@ -2,6 +2,7 @@
<AuthView <AuthView
:form="FORM_CONFIG" :form="FORM_CONFIG"
:formLoading="loading" :formLoading="loading"
data-test-id="setup-form"
@submit="onSubmit" @submit="onSubmit"
@secondaryClick="showSkipConfirmation" @secondaryClick="showSkipConfirmation"
/> />

View file

@ -2,6 +2,7 @@
<AuthView <AuthView
:form="FORM_CONFIG" :form="FORM_CONFIG"
:formLoading="loading" :formLoading="loading"
data-test-id="signin-form"
@submit="onSubmit" @submit="onSubmit"
/> />
</template> </template>