diff --git a/.eslintignore b/.eslintignore
index d8443a88..11c7540f 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -3,6 +3,7 @@ dist
build
coverage
packages/docs/*
+packages/e2e-tests
.eslintrc.js
husky.config.js
diff --git a/.gitignore b/.gitignore
index 0125458e..ba7f9dc3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -74,6 +74,15 @@ web_modules/
.env.test
.env.production
+# cypress environment variables file
+cypress.env.json
+
+# cypress screenshots
+packages/e2e-tests/cypress/screenshots
+
+# cypress videos
+packages/e2e-tests/cypress/videos/
+
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
diff --git a/packages/e2e-tests/cypress.config.js b/packages/e2e-tests/cypress.config.js
new file mode 100644
index 00000000..2acda4eb
--- /dev/null
+++ b/packages/e2e-tests/cypress.config.js
@@ -0,0 +1,17 @@
+const { defineConfig } = require("cypress");
+
+const TO_BE_PROVIDED = 'HAS_TO_BE_PROVIDED_IN_cypress.env.json';
+
+module.exports = defineConfig({
+ e2e: {
+ baseUrl: 'http://localhost:3001',
+ env: {
+ login_email: "user@automatisch.io",
+ login_password: "sample",
+ slack_user_token: TO_BE_PROVIDED,
+ },
+ specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}',
+ viewportWidth: 1280,
+ viewportHeight: 768
+ },
+});
diff --git a/packages/e2e-tests/cypress/e2e/apps/list-apps.js b/packages/e2e-tests/cypress/e2e/apps/list-apps.js
new file mode 100644
index 00000000..d5dfb7b2
--- /dev/null
+++ b/packages/e2e-tests/cypress/e2e/apps/list-apps.js
@@ -0,0 +1,50 @@
+///
+
+describe('Apps page', () => {
+ before(() => {
+ cy.login();
+
+ cy.og('apps-page-drawer-link').click();
+ });
+
+ after(() => {
+ cy.logout();
+ });
+
+ it('displays applications', () => {
+ cy.og('apps-loader').should('not.exist');
+ cy.og('app-row').should('have.length', 3);
+
+ cy.ss('Applications');
+ });
+
+ context('can add connection', () => {
+ before(() => {
+ cy.og('add-connection-button').click();
+ });
+
+ it('lists applications', () => {
+ cy.og('app-list-item').should('have.length', 3);
+ });
+
+ it('searches an application', () => {
+ cy.og('search-for-app-text-field').type('Slack');
+ cy.og('app-list-item').should('have.length', 1);
+ });
+
+ it('goes to app page to create a connection', () => {
+ cy.og('app-list-item').first().click();
+
+ cy.location('pathname').should('equal', '/app/slack/connections/add');
+
+ cy.og('add-app-connection-dialog').should('be.visible');
+ });
+
+ it('closes the dialog on backdrop click', () => {
+ cy.clickOutside();
+
+ cy.location('pathname').should('equal', '/app/slack/connections');
+ cy.og('add-app-connection-dialog').should('not.exist');
+ });
+ });
+});
\ No newline at end of file
diff --git a/packages/e2e-tests/cypress/e2e/connections/create-connection.js b/packages/e2e-tests/cypress/e2e/connections/create-connection.js
new file mode 100644
index 00000000..570dab31
--- /dev/null
+++ b/packages/e2e-tests/cypress/e2e/connections/create-connection.js
@@ -0,0 +1,52 @@
+///
+
+describe('Connections page', () => {
+ before(() => {
+ cy.login();
+
+ cy.og('apps-page-drawer-link').click();
+ });
+
+ after(() => {
+ cy.logout();
+ });
+
+ it('opens via applications page', () => {
+ cy.og('apps-loader').should('not.exist');
+
+ cy.og('app-row').contains('Slack').click();
+
+ cy.og('app-connection-row').should('be.visible');
+
+ cy.ss('Slack connections before creating a connection');
+ });
+
+ context('can add connection', () => {
+ it('has a button to open add connection dialog', () => {
+ cy
+ .og('add-connection-button')
+ .scrollIntoView()
+ .should('be.visible');
+ });
+
+ it('add connection button takes user to add connection page', () => {
+ cy
+ .og('add-connection-button')
+ .click({ force: true });
+
+ cy.location('pathname').should('equal', '/app/slack/connections/add');
+ });
+
+ it('shows add connection dialog to create a new connection', () => {
+ cy
+ .get('input[name="accessToken"]')
+ .type(Cypress.env('slack_user_token'));
+
+ cy.og('create-connection-button').click();
+
+ cy.og('create-connection-button').should('not.exist');
+
+ cy.ss('Slack connections after creating a connection');
+ });
+ });
+});
diff --git a/packages/e2e-tests/cypress/e2e/executions/display-execution.js b/packages/e2e-tests/cypress/e2e/executions/display-execution.js
new file mode 100644
index 00000000..647f97ca
--- /dev/null
+++ b/packages/e2e-tests/cypress/e2e/executions/display-execution.js
@@ -0,0 +1,34 @@
+///
+
+describe('Execution page', () => {
+ before(() => {
+ cy.login();
+
+ cy.og('executions-page-drawer-link').click();
+ cy.og('execution-row').first().click({ force: true });
+
+ cy.location('pathname').should('match', /^\/executions\//);
+ });
+
+ after(() => {
+ cy.logout();
+ });
+
+ it('displays data in by default', () => {
+ cy.og('execution-step').should('have.length', 2);
+
+ cy.ss('Execution - data in');
+ });
+
+ it('displays data out', () => {
+ cy.og('data-out-tab').click({ multiple: true });
+
+ cy.ss('Execution - data out');
+ });
+
+ it('displays error', () => {
+ cy.og('error-tab').click({ multiple: true, force: true });
+
+ cy.ss('Execution - error');
+ });
+});
\ No newline at end of file
diff --git a/packages/e2e-tests/cypress/e2e/executions/list-executions.js b/packages/e2e-tests/cypress/e2e/executions/list-executions.js
new file mode 100644
index 00000000..66fcb026
--- /dev/null
+++ b/packages/e2e-tests/cypress/e2e/executions/list-executions.js
@@ -0,0 +1,20 @@
+///
+
+describe('Executions page', () => {
+ before(() => {
+ cy.login();
+
+ cy.og('executions-page-drawer-link').click();
+ });
+
+ after(() => {
+ cy.logout();
+ });
+
+ it('displays executions', () => {
+ cy.og('executions-loader').should('not.exist');
+ cy.og('execution-row').should('exist');
+
+ cy.ss('Executions');
+ });
+});
\ No newline at end of file
diff --git a/packages/e2e-tests/cypress/e2e/flow-editor/create-flow.js b/packages/e2e-tests/cypress/e2e/flow-editor/create-flow.js
new file mode 100644
index 00000000..77d08af9
--- /dev/null
+++ b/packages/e2e-tests/cypress/e2e/flow-editor/create-flow.js
@@ -0,0 +1,229 @@
+///
+
+describe('Flow editor page', () => {
+ before(() => {
+ cy.login();
+ });
+
+ after(() => {
+ cy.logout();
+ });
+
+ it('create flow', () => {
+ cy.og('create-flow-button').click({ force: true });
+ });
+
+ it('has two steps by default', () => {
+ cy.og('flow-step').should('have.length', 2);
+ });
+
+ context('edit flow', () => {
+ context('arrange Scheduler trigger', () => {
+ context('choose app and event substep', () => {
+ it('choose application', () => {
+ cy.og('choose-app-autocomplete').click();
+
+ cy.get('li[role="option"]:contains("Scheduler")').click();
+ });
+
+ it('choose an event', () => {
+ cy
+ .og('choose-event-autocomplete')
+ .should('be.visible')
+ .click();
+
+ cy.get('li[role="option"]:contains("Every hour")').click();
+ });
+
+ it('continue to next step', () => {
+ cy.og('flow-substep-continue-button').click();
+ });
+
+ it('collapses the substep', () => {
+ cy.og('choose-app-autocomplete').should('not.be.visible');
+ cy.og('choose-event-autocomplete').should('not.be.visible');
+ })
+ });
+
+ context('set up a trigger', () => {
+ it('choose "yes" in "trigger on weekends?"', () => {
+ cy
+ .og('parameters.triggersOnWeekend-autocomplete')
+ .should('be.visible')
+ .click();
+
+ cy.get('li[role="option"]:contains("Yes")').click();
+ });
+
+ it('continue to next step', () => {
+ cy.og('flow-substep-continue-button').click();
+ });
+
+ it('collapses the substep', () => {
+ cy.og('parameters.triggersOnWeekend-autocomplete').should('not.exist');
+ });
+ });
+
+ context('test trigger', () => {
+ it('show sample output', () => {
+ cy.og('flow-test-substep-output').should('not.exist');
+
+ cy.og('flow-substep-continue-button').click();
+
+ cy.og('flow-test-substep-output').should('be.visible');
+
+ cy.ss('Scheduler trigger test output');
+
+ cy.og('flow-substep-continue-button').click();
+ });
+ });
+ });
+
+ context('arrange Slack action', () => {
+ context('choose app and event substep', () => {
+ it('choose application', () => {
+ cy.og('choose-app-autocomplete').click();
+
+ cy.get('li[role="option"]:contains("Slack")').click();
+ });
+
+ it('choose an event', () => {
+ cy
+ .og('choose-event-autocomplete')
+ .should('be.visible')
+ .click();
+
+ cy.get('li[role="option"]:contains("Send a message to channel")').click();
+ });
+
+ it('continue to next step', () => {
+ cy.og('flow-substep-continue-button').click();
+ });
+
+ it('collapses the substep', () => {
+ cy.og('choose-app-autocomplete').should('not.be.visible');
+ cy.og('choose-event-autocomplete').should('not.be.visible');
+ });
+ });
+
+ context('choose connection', () => {
+ it('choose connection', () => {
+ cy.og('choose-connection-autocomplete').click();
+
+ cy.get('li[role="option"]').first().click();
+ });
+
+ it('continue to next step', () => {
+ cy.og('flow-substep-continue-button').click();
+ });
+
+ it('collapses the substep', () => {
+ cy.og('choose-connection-autocomplete').should('not.be.visible');
+ });
+ });
+
+ context('set up action', () => {
+ it('choose channel', () => {
+ cy.og('parameters.channel-autocomplete').click();
+
+ cy.get('li[role="option"]').last().click();
+ });
+
+ it('arrange message text', () => {
+ cy
+ .og('power-input', ' [contenteditable]')
+ .click()
+ .type(`Hello from e2e tests! Here is the first suggested variable's value; `);
+
+ cy
+ .og('power-input-suggestion-group').first()
+ .og('power-input-suggestion-item').first()
+ .click();
+
+ cy.clickOutside();
+
+ cy.ss('Slack action message text');
+ });
+
+ it('continue to next step', () => {
+ cy.og('flow-substep-continue-button').click();
+ });
+
+ it('collapses the substep', () => {
+ cy
+ .og('power-input', ' [contenteditable]')
+ .should('not.exist');
+ });
+ });
+
+ context('test trigger', () => {
+ it('show sample output', () => {
+ cy.og('flow-test-substep-output').should('not.exist');
+
+ cy.og('flow-substep-continue-button').click();
+
+ cy.og('flow-test-substep-output').should('be.visible');
+
+ cy.ss('Slack action test output');
+
+ cy.og('flow-substep-continue-button').click();
+ });
+ });
+ });
+ });
+
+ context('publish and unpublish', () => {
+ it('publish flow', () => {
+ cy.og('unpublish-flow-button').should('not.exist');
+
+ cy
+ .og('publish-flow-button')
+ .should('be.visible')
+ .click();
+
+ cy.og('publish-flow-button').should('not.exist');
+ });
+
+ it('shows read-only sticky snackbar', () => {
+ cy.og('flow-cannot-edit-info-snackbar').should('be.visible');
+
+ cy.ss('Published flow');
+ });
+
+ it('unpublish from snackbar', () => {
+ cy
+ .og('unpublish-flow-from-snackbar')
+ .click();
+
+ cy.og('flow-cannot-edit-info-snackbar').should('not.exist');
+ })
+
+ it('publish once again', () => {
+ cy
+ .og('publish-flow-button')
+ .should('be.visible')
+ .click();
+
+ cy.og('publish-flow-button').should('not.exist');
+ });
+
+ it('unpublish from layout top bar', () => {
+ cy
+ .og('unpublish-flow-button')
+ .should('be.visible')
+ .click();
+
+ cy.og('unpublish-flow-button').should('not.exist');
+
+ cy.ss('Unpublished flow');
+ });
+ });
+
+ context('in layout', () => {
+ it('can go back to flows page', () => {
+ cy.og('editor-go-back-button').click();
+
+ cy.location('pathname').should('equal', '/flows');
+ });
+ });
+});
diff --git a/packages/e2e-tests/cypress/support/commands.js b/packages/e2e-tests/cypress/support/commands.js
new file mode 100644
index 00000000..7d28515e
--- /dev/null
+++ b/packages/e2e-tests/cypress/support/commands.js
@@ -0,0 +1,45 @@
+Cypress.Commands.add('og', { prevSubject: 'optional' }, (subject, selector, suffix = '') => {
+ if (subject) {
+ return cy.wrap(subject).get(`[data-test="${selector}"]${suffix}`);
+ }
+
+ return cy.get(`[data-test="${selector}"]${suffix}`);
+});
+
+Cypress.Commands.add('login', () => {
+ cy.visit('/login');
+
+ cy.og('email-text-field').type(Cypress.env('login_email'));
+ cy.og('password-text-field').type(Cypress.env('login_password'));
+
+ cy
+ .intercept('/graphql')
+ .as('graphqlCalls');
+ cy
+ .intercept('https://notifications.automatisch.io/notifications.json')
+ .as('notificationsCall');
+ cy.og('login-button').click();
+
+ cy.wait(['@graphqlCalls', '@notificationsCall']);
+});
+
+Cypress.Commands.add('logout', () => {
+ cy.og('profile-menu-button').click();
+
+ cy.og('logout-item').click();
+});
+
+Cypress.Commands.add('ss', (name, opts = {}) => {
+ return cy.screenshot(
+ name,
+ {
+ overwrite: true,
+ capture: 'viewport',
+ ...opts,
+ }
+ );
+});
+
+Cypress.Commands.add('clickOutside', () => {
+ return cy.get('body').click(0, 0);
+});
diff --git a/packages/e2e-tests/cypress/support/e2e.js b/packages/e2e-tests/cypress/support/e2e.js
new file mode 100644
index 00000000..0e7290a1
--- /dev/null
+++ b/packages/e2e-tests/cypress/support/e2e.js
@@ -0,0 +1,20 @@
+// ***********************************************************
+// 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.js using ES2015 syntax:
+import './commands'
+
+// Alternatively you can use CommonJS syntax:
+// require('./commands')
\ No newline at end of file
diff --git a/packages/e2e-tests/package.json b/packages/e2e-tests/package.json
new file mode 100644
index 00000000..d67c8d12
--- /dev/null
+++ b/packages/e2e-tests/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "@automatisch/e2e-tests",
+ "version": "0.1.4",
+ "license": "AGPL-3.0",
+ "private": true,
+ "description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
+ "scripts": {
+ "open": "cypress open"
+ },
+ "dependencies": {},
+ "contributors": [
+ {
+ "name": "automatisch contributors",
+ "url": "https://github.com/automatisch/automatisch/graphs/contributors"
+ }
+ ],
+ "homepage": "https://github.com/automatisch/automatisch#readme",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/automatisch/automatisch.git"
+ },
+ "bugs": {
+ "url": "https://github.com/automatisch/automatisch/issues"
+ },
+ "devDependencies": {
+ "cypress": "^10.9.0"
+ }
+}
\ No newline at end of file
diff --git a/packages/web/src/components/AccountDropdownMenu/index.tsx b/packages/web/src/components/AccountDropdownMenu/index.tsx
index 31ee564e..4f2dbbf7 100644
--- a/packages/web/src/components/AccountDropdownMenu/index.tsx
+++ b/packages/web/src/components/AccountDropdownMenu/index.tsx
@@ -62,6 +62,7 @@ function AccountDropdownMenu(props: AccountDropdownMenuProps): React.ReactElemen
diff --git a/packages/web/src/components/AddAppConnection/index.tsx b/packages/web/src/components/AddAppConnection/index.tsx
index 3759514b..193bcde9 100644
--- a/packages/web/src/components/AddAppConnection/index.tsx
+++ b/packages/web/src/components/AddAppConnection/index.tsx
@@ -84,7 +84,7 @@ export default function AddAppConnection(props: AddAppConnectionProps): React.Re
}, [connectionId, key, steps, onClose]);
return (
-