mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-09 22:24:05 -08:00
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:
parent
5d73b6e48a
commit
77644860c0
7
.github/workflows/ci-master.yml
vendored
7
.github/workflows/ci-master.yml
vendored
|
@ -31,7 +31,12 @@ jobs:
|
|||
run: npm run build --if-present
|
||||
|
||||
- name: Test
|
||||
run: npm run test
|
||||
run:
|
||||
npm run test
|
||||
|
||||
- name: Test E2E
|
||||
run:
|
||||
npm run test:e2e:ci:smoke
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
|
|
4
.github/workflows/ci-pull-requests.yml
vendored
4
.github/workflows/ci-pull-requests.yml
vendored
|
@ -30,6 +30,10 @@ jobs:
|
|||
- name: Test
|
||||
run: npm run test
|
||||
|
||||
- name: Test E2E
|
||||
run:
|
||||
npm run test:e2e:ci:smoke
|
||||
|
||||
- name: Fetch base branch for `git diff`
|
||||
run: git fetch origin ${{ github.event.pull_request.base.ref }}:${{ github.event.pull_request.base.ref }}
|
||||
|
||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -15,4 +15,6 @@ _START_PACKAGE
|
|||
nodelinter.config.json
|
||||
packages/*/package-lock.json
|
||||
packages/*/.turbo
|
||||
cypress/videos/*
|
||||
cypress/screenshots/*
|
||||
*.swp
|
||||
|
|
12
cypress.config.js
Normal file
12
cypress.config.js
Normal 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
4
cypress/constants.ts
Normal 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
23
cypress/e2e/0-smoke.cy.ts
Normal 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
15
cypress/pages/base.ts
Normal 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
4
cypress/pages/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export * from './base';
|
||||
export * from './signin';
|
||||
export * from './signup';
|
||||
export * from './workflows';
|
11
cypress/pages/signin.ts
Normal file
11
cypress/pages/signin.ts
Normal 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
13
cypress/pages/signup.ts
Normal 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'),
|
||||
}
|
||||
}
|
6
cypress/pages/workflows.ts
Normal file
6
cypress/pages/workflows.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { BasePage } from "./base";
|
||||
|
||||
export class WorkflowsPage extends BasePage {
|
||||
url = '/workflows';
|
||||
elements = {}
|
||||
}
|
78
cypress/support/commands.ts
Normal file
78
cypress/support/commands.ts
Normal 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
17
cypress/support/e2e.ts
Normal 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
14
cypress/support/index.ts
Normal 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
9
cypress/types.ts
Normal 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
1765
package-lock.json
generated
File diff suppressed because it is too large
Load diff
16
package.json
16
package.json
|
@ -19,18 +19,30 @@
|
|||
"test": "turbo run test",
|
||||
"watch": "turbo run watch",
|
||||
"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": {
|
||||
"@ngneat/falso": "^6.1.0",
|
||||
"@types/jest": "^28.1.8",
|
||||
"cross-env": "^7.0.3",
|
||||
"cypress": "^10.0.3",
|
||||
"jest": "^28.1.3",
|
||||
"jest-environment-jsdom": "^28.1.3",
|
||||
"jest-mock": "^28.1.3",
|
||||
"patch-package": "^6.4.7",
|
||||
"rimraf": "^3.0.2",
|
||||
"run-script-os": "^1.0.7",
|
||||
"start-server-and-test": "^1.14.0",
|
||||
"ts-jest": "^28.0.8",
|
||||
"turbo": "1.2.15"
|
||||
"turbo": "1.2.15",
|
||||
"typescript": "^4.8.4"
|
||||
},
|
||||
"postcss": {},
|
||||
"workspaces": [
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
v-bind="input.properties"
|
||||
:name="input.name"
|
||||
:value="values[input.name]"
|
||||
:data-test-id="input.name"
|
||||
:showValidationWarnings="showValidationWarnings"
|
||||
@input="(value) => onInput(input.name, value)"
|
||||
@validate="(value) => onValidate(input.name, value)"
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
<div slot="content" :class="$style.triggerWarning">
|
||||
{{ $locale.baseText('ndv.backToCanvas.waitingForTriggerWarning') }}
|
||||
</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-text color="text-xlight" size="medium" :bold="true">
|
||||
{{ $locale.baseText('ndv.backToCanvas') }}
|
||||
|
|
|
@ -66,7 +66,11 @@
|
|||
</template>
|
||||
</i18n>
|
||||
</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'">
|
||||
<node-webhooks :node="node" :nodeType="nodeType" />
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
:closeOnClickModal="false"
|
||||
:closeOnPressEscape="false"
|
||||
width="460px"
|
||||
data-test-id="personalization-form"
|
||||
@enter="onSave"
|
||||
>
|
||||
<template v-slot:content>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<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>
|
||||
<n8n-button
|
||||
v-else
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
<div :class="$style.formContainer">
|
||||
<n8n-form-box
|
||||
v-bind="form"
|
||||
data-test-id="auth-form"
|
||||
:buttonLoading="formLoading"
|
||||
@secondaryClick="onSecondaryClick"
|
||||
@submit="onSubmit"
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
<AuthView
|
||||
:form="FORM_CONFIG"
|
||||
:formLoading="loading"
|
||||
data-test-id="setup-form"
|
||||
@submit="onSubmit"
|
||||
@secondaryClick="showSkipConfirmation"
|
||||
/>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
<AuthView
|
||||
:form="FORM_CONFIG"
|
||||
:formLoading="loading"
|
||||
data-test-id="signin-form"
|
||||
@submit="onSubmit"
|
||||
/>
|
||||
</template>
|
||||
|
|
Loading…
Reference in a new issue