Compare commits

...

69 Commits

Author SHA1 Message Date
Jakub P.
bab87e9e67 chore: add missing dependency for knex method in e2e-tests knexfile 2024-12-12 20:23:16 +01:00
Jakub P.
7d793ce2a2 test: fix application connections check assertions 2024-12-12 20:07:35 +01:00
Jakub P.
f83deac469 test: improve tests execution time 2024-12-06 09:17:21 +01:00
Ömer Faruk Aydın
b8da721e39 Merge pull request #2216 from automatisch/aut-1350-registerUser
test(user): write tests for registerUser
2024-11-25 13:01:55 +03:00
Ali BARIN
db8b98ca16 test(user): write tests for registerUser 2024-11-25 12:56:17 +03:00
Ömer Faruk Aydın
01b8c600fe Merge pull request #2229 from automatisch/playwright-main
chore: Do not run e2e tests in pull requests
2024-11-25 12:54:46 +03:00
Ömer Faruk Aydın
69bd5549a2 Merge pull request #2215 from automatisch/aut-1350-createAdmin
test(user): write test for createAdmin
2024-11-25 12:53:05 +03:00
Faruk AYDIN
bc631e3931 chore: Do not run e2e tests in pull requests 2024-11-25 12:50:59 +03:00
Ali BARIN
8ca4bc5a33 test(user): write test for createAdmin 2024-11-25 12:43:45 +03:00
Ömer Faruk Aydın
58a569afb0 Merge pull request #2214 from automatisch/aut-1350-getInvoices
test(user): write tests for getInvoices
2024-11-25 12:41:10 +03:00
Ali BARIN
db718d6fc3 test(user): write tests for getInvoices 2024-11-25 12:32:07 +03:00
Ömer Faruk Aydın
ca9cb8b07b Merge pull request #2213 from automatisch/aut-1350-getPlanAndUsage
test(user): write tests for getPlanAndUsage
2024-11-25 12:27:55 +03:00
Ali BARIN
ef14586412 test(user): write tests for getPlanAndUsage 2024-11-25 12:19:40 +03:00
Ömer Faruk Aydın
15f1fca6fe Merge pull request #2205 from automatisch/aut-1350-withinLimits
test(user): write tests for withinLimits
2024-11-22 15:40:21 +03:00
Ali BARIN
a570b8eb7a test(user): write tests for withinLimits 2024-11-22 15:31:19 +03:00
Ömer Faruk Aydın
02e2735b7a Merge pull request #2204 from automatisch/aut-1350-hasActiveSubscription
test(user): write tests for hasActiveSubscription
2024-11-22 15:29:10 +03:00
Ali BARIN
54fa347142 test(user): write tests for hasActiveSubscription 2024-11-22 15:21:10 +03:00
Ömer Faruk Aydın
0c752beace Merge pull request #2203 from automatisch/aut-1350-inTrial
test(user): write tests for inTrial
2024-11-22 15:19:00 +03:00
Ali BARIN
c14f808d29 test(user): write tests for inTrial 2024-11-22 15:10:13 +03:00
Ali BARIN
ad71173671 Merge pull request #2223 from automatisch/AUT-1365
feat: introduce inline error messages for ForgotPassword and ResetPasswordForm
2024-11-22 11:36:56 +01:00
Ömer Faruk Aydın
204325ef44 Merge pull request #2200 from automatisch/aut-1350-isAllowedToRunFlows
test(user): write tests for isAllowedToRunFlows
2024-11-22 10:32:03 +03:00
Ali BARIN
7ce6117659 test(user): write tests for isAllowedToRunFlows 2024-11-22 10:02:05 +03:00
Ömer Faruk Aydın
823a2c8b73 Merge pull request #2199 from automatisch/aut-1350-startTrialPeriod
test(user): write test for startTrialPeriod
2024-11-21 17:52:42 +03:00
Ali BARIN
741866e742 test(user): use luxon DateTime with zone over Date 2024-11-21 17:42:42 +03:00
Ali BARIN
41622678b0 test(user): write test for startTrialPeriod 2024-11-21 17:42:40 +03:00
Ömer Faruk Aydın
449b953401 Merge pull request #2198 from automatisch/aut-1350-generateHash
test(user): write tests for generateHash
2024-11-21 17:41:06 +03:00
kasia.oczkowska
551548400f refactor: use non-asyc version of mutate 2024-11-21 14:34:11 +00:00
Ali BARIN
6345ce5195 test(user): write tests for generateHash 2024-11-21 17:29:13 +03:00
Ömer Faruk Aydın
95651f6163 Merge pull request #2197 from automatisch/aut-1350-isInvitationTokenValid
test(user): write tests for isInvitationTokenValid
2024-11-21 17:23:10 +03:00
Ali BARIN
b02c1545b7 test(user): use luxon DateTime with zone over Date 2024-11-21 17:15:00 +03:00
Ali BARIN
2deaab9b24 test(user): write tests for isInvitationTokenValid 2024-11-21 17:14:56 +03:00
Ömer Faruk Aydın
f0d4853533 Merge pull request #2188 from automatisch/aut-1350-sendInvitationEmail
test(user): write test for sendInvitationEmail
2024-11-21 16:46:35 +03:00
Ali BARIN
af81ae812f Merge pull request #2222 from automatisch/fix-devcontainer
fix(devcontainer): remove yarn workspaces
2024-11-21 14:44:40 +01:00
Ali BARIN
bae76064e5 test(user): use luxon DateTime with zone over Date 2024-11-21 13:33:30 +00:00
Ali BARIN
07d9198cc8 test(user): write test for sendInvitationEmail 2024-11-21 13:31:41 +00:00
Ömer Faruk Aydın
a2e07ea2f7 Merge pull request #2187 from automatisch/aut-1350-isResetPasswordTokenValid
test(user): write test cases for isResetPasswordTokenValid
2024-11-21 16:29:50 +03:00
Ali BARIN
864c762fe2 test(user): use luxon DateTime with zone over Date 2024-11-21 12:49:33 +00:00
Ali BARIN
167bb4e8a0 test(user): write test cases for isResetPasswordTokenValid 2024-11-21 12:34:08 +00:00
Ömer Faruk Aydın
4cf64ede74 Merge pull request #2185 from automatisch/aut-1350-sendResetPasswordEmail
test(user): write test for sendResetPasswordEmail
2024-11-21 14:57:42 +03:00
Ali BARIN
bb309fea6f test(user): write test for sendResetPasswordEmail 2024-11-21 14:47:18 +03:00
kasia.oczkowska
90a7b4c1c0 feat: introduce inline error messages for ForgotPassword and ResetPasswordForm 2024-11-21 11:43:00 +00:00
Ömer Faruk Aydın
1133362028 Merge pull request #2184 from automatisch/aut-1350-softRemove
test(user): write test for softRemove
2024-11-21 14:34:53 +03:00
Ali BARIN
eb9226bd4a test(user): write test for softRemove 2024-11-21 14:23:29 +03:00
Ömer Faruk Aydın
a9abdcc37e Merge pull request #2212 from automatisch/remove-lerna
Remove lerna and yarn workspaces
2024-11-21 13:51:57 +03:00
Ali BARIN
6ace93bdbf fix(devcontainer): remove yarn workspaces 2024-11-21 10:17:43 +00:00
Faruk AYDIN
b89197939a docs: Adjust development setup page for new package setup 2024-11-21 12:40:31 +03:00
Faruk AYDIN
da788106af feat: Add separate gitignore file for docs package 2024-11-21 12:40:25 +03:00
Faruk AYDIN
49e92e6f1d fix: Adjust build web CI job to use cache dependency path 2024-11-19 20:50:15 +03:00
Faruk AYDIN
a6c3276104 chore: Remove root package.json file 2024-11-19 20:46:13 +03:00
Faruk AYDIN
6388bfc714 docs: Adjust repository structure page to reflect individual packages 2024-11-19 20:37:08 +03:00
Faruk AYDIN
bebc3b181d chore: Remove lerna debug file from .gitignore 2024-11-19 20:36:48 +03:00
Faruk AYDIN
5a6d561c1a refactor: Adjust dockerfile to work without lerna 2024-11-19 18:31:39 +03:00
Faruk AYDIN
5ba575fdfd fix: Add knex individually to e2e-tests package 2024-11-19 16:04:21 +03:00
Faruk AYDIN
dcf8bbd804 chore: Add axios package to e2e-tests package 2024-11-19 15:56:10 +03:00
Faruk AYDIN
ff93ffd0b1 fix: Adjust e2e-tests CI job working directories 2024-11-19 15:49:25 +03:00
Faruk AYDIN
395c09df92 fix: Use named ClickAwayListener import statement for web 2024-11-19 15:41:59 +03:00
Faruk AYDIN
4c903cd08b fix: Adjust backend start server CI job working directory 2024-11-19 15:36:01 +03:00
Faruk AYDIN
64cb98717c fix: Adjust backend start worker CI job working directory 2024-11-19 15:35:21 +03:00
Faruk AYDIN
b0e4ce54fb fix: Add fakerjs individually to backend package 2024-11-19 15:29:36 +03:00
Faruk AYDIN
d67a37002f fix: Use working directory for backend tests CI job 2024-11-19 15:24:40 +03:00
Faruk AYDIN
965ff8bc3f fix: Add axios individually to web package 2024-11-19 15:20:35 +03:00
Faruk AYDIN
400a495ad2 chore: Remove cache dependency path for build web CI job 2024-11-19 15:14:06 +03:00
Faruk AYDIN
09d0822a8d fix: Adjust working directory for build-web CI job 2024-11-19 15:09:46 +03:00
Faruk AYDIN
7016c20ccc fix: Change working directory for linter CI job 2024-11-19 15:03:54 +03:00
Faruk AYDIN
df54895805 fix: Linter command to work without workspaces 2024-11-19 15:00:23 +03:00
Faruk AYDIN
62d5e6fe51 chore: Remove root package dev dependencies 2024-11-19 14:57:39 +03:00
Faruk AYDIN
4615a0b7ea chore: Remove workspaces from root package.json file 2024-11-19 14:51:53 +03:00
Faruk AYDIN
280d603b14 chore: Remove lerna.json file 2024-11-18 20:21:20 +03:00
Faruk AYDIN
36271f0749 refactor: Explicitly state different packages for lerna 2024-11-18 20:16:20 +03:00
45 changed files with 19512 additions and 17341 deletions

View File

@@ -5,8 +5,11 @@ BACKEND_PORT=3000
WEB_PORT=3001 WEB_PORT=3001
echo "Configuring backend environment variables..." echo "Configuring backend environment variables..."
cd packages/backend cd packages/backend
rm -rf .env rm -rf .env
echo " echo "
PORT=$BACKEND_PORT PORT=$BACKEND_PORT
WEB_APP_URL=http://localhost:$WEB_PORT WEB_APP_URL=http://localhost:$WEB_PORT
@@ -21,22 +24,34 @@ WEBHOOK_SECRET_KEY=sample_webhook_secret_key
APP_SECRET_KEY=sample_app_secret_key APP_SECRET_KEY=sample_app_secret_key
REDIS_HOST=redis REDIS_HOST=redis
SERVE_WEB_APP_SEPARATELY=true" >> .env SERVE_WEB_APP_SEPARATELY=true" >> .env
echo "Installing backend dependencies..."
yarn
cd $CURRENT_DIR cd $CURRENT_DIR
echo "Configuring web environment variables..." echo "Configuring web environment variables..."
cd packages/web cd packages/web
rm -rf .env rm -rf .env
echo " echo "
PORT=$WEB_PORT PORT=$WEB_PORT
REACT_APP_BACKEND_URL=http://localhost:$BACKEND_PORT REACT_APP_BACKEND_URL=http://localhost:$BACKEND_PORT
" >> .env " >> .env
cd $CURRENT_DIR
echo "Installing and linking dependencies..." echo "Installing web dependencies..."
yarn yarn
cd $CURRENT_DIR
echo "Migrating database..." echo "Migrating database..."
cd packages/backend cd packages/backend
yarn db:migrate yarn db:migrate
yarn db:seed:user yarn db:seed:user

View File

@@ -41,8 +41,11 @@ jobs:
with: with:
node-version: 18 node-version: 18
- name: Install dependencies - name: Install dependencies
run: cd packages/backend && yarn run: yarn
working-directory: packages/backend
- name: Copy .env-example.test file to .env.test - name: Copy .env-example.test file to .env.test
run: cd packages/backend && cp .env-example.test .env.test run: cp .env-example.test .env.test
working-directory: packages/backend
- name: Run tests - name: Run tests
run: cd packages/backend && yarn test run: yarn test
working-directory: packages/backend

View File

@@ -18,11 +18,13 @@ jobs:
with: with:
node-version: '18' node-version: '18'
cache: 'yarn' cache: 'yarn'
cache-dependency-path: yarn.lock cache-dependency-path: packages/backend/yarn.lock
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner." - run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner." - run: echo "🖥️ The workflow is now ready to test your code on the runner."
- run: yarn --frozen-lockfile - run: yarn --frozen-lockfile
- run: cd packages/backend && yarn lint working-directory: packages/backend
- run: yarn lint
working-directory: packages/backend
- run: echo "🍏 This job's status is ${{ job.status }}." - run: echo "🍏 This job's status is ${{ job.status }}."
start-backend-server: start-backend-server:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -35,11 +37,13 @@ jobs:
with: with:
node-version: '18' node-version: '18'
cache: 'yarn' cache: 'yarn'
cache-dependency-path: yarn.lock cache-dependency-path: packages/backend/yarn.lock
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner." - run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner." - run: echo "🖥️ The workflow is now ready to test your code on the runner."
- run: yarn --frozen-lockfile - run: yarn --frozen-lockfile
- run: cd packages/backend && yarn start working-directory: packages/backend
- run: yarn start
working-directory: packages/backend
env: env:
ENCRYPTION_KEY: sample_encryption_key ENCRYPTION_KEY: sample_encryption_key
WEBHOOK_SECRET_KEY: sample_webhook_secret_key WEBHOOK_SECRET_KEY: sample_webhook_secret_key
@@ -55,11 +59,13 @@ jobs:
with: with:
node-version: '18' node-version: '18'
cache: 'yarn' cache: 'yarn'
cache-dependency-path: yarn.lock cache-dependency-path: packages/backend/yarn.lock
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner." - run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner." - run: echo "🖥️ The workflow is now ready to test your code on the runner."
- run: yarn --frozen-lockfile - run: yarn --frozen-lockfile
- run: cd packages/backend && yarn start:worker working-directory: packages/backend
- run: yarn start:worker
working-directory: packages/backend
env: env:
ENCRYPTION_KEY: sample_encryption_key ENCRYPTION_KEY: sample_encryption_key
WEBHOOK_SECRET_KEY: sample_webhook_secret_key WEBHOOK_SECRET_KEY: sample_webhook_secret_key
@@ -75,11 +81,13 @@ jobs:
with: with:
node-version: '18' node-version: '18'
cache: 'yarn' cache: 'yarn'
cache-dependency-path: yarn.lock cache-dependency-path: packages/web/yarn.lock
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner." - run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner." - run: echo "🖥️ The workflow is now ready to test your code on the runner."
- run: yarn --frozen-lockfile - run: yarn --frozen-lockfile
- run: cd packages/web && yarn build working-directory: packages/web
- run: yarn build
working-directory: packages/web
env: env:
CI: false CI: false
- run: echo "🍏 This job's status is ${{ job.status }}." - run: echo "🍏 This job's status is ${{ job.status }}."

View File

@@ -55,19 +55,44 @@ jobs:
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v3 - uses: actions/setup-node@v4
with: with:
node-version: 18 node-version: '18'
- name: Install dependencies cache: 'yarn'
run: yarn cache-dependency-path: |
packages/backend/yarn.lock
packages/web/yarn.lock
packages/e2e-tests/yarn.lock
- name: Install backend dependencies
run: yarn --frozen-lockfile
working-directory: ./packages/backend
- name: Install web dependencies
run: yarn --frozen-lockfile
working-directory: ./packages/web
- name: Install e2e-tests dependencies
run: yarn --frozen-lockfile
working-directory: ./packages/e2e-tests
- name: Get installed Playwright version
id: playwright-version
run: echo "PLAYWRIGHT_VERSION=$(node -e "console.log(require('./package.json').devDependencies['@playwright/test'])")" >> $GITHUB_ENV
working-directory: ./packages/e2e-tests
- name: Cache playwright binaries
uses: actions/cache@v3
id: playwright-cache
with:
path: |
~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }}
- name: Install Playwright Browsers - name: Install Playwright Browsers
run: yarn playwright install --with-deps run: yarn playwright install --with-deps
working-directory: ./packages/e2e-tests
if: steps.playwright-cache.outputs.cache-hit != 'true'
- name: Build Automatisch web - name: Build Automatisch web
working-directory: ./packages/web
run: yarn build run: yarn build
env: env:
# Keep this until we clean up warnings in build processes # Keep this until we clean up warnings in build processes
CI: false CI: false
working-directory: ./packages/web
- name: Migrate database - name: Migrate database
working-directory: ./packages/backend working-directory: ./packages/backend
run: yarn db:migrate run: yarn db:migrate
@@ -107,6 +132,7 @@ jobs:
env: env:
LOGIN_EMAIL: user@automatisch.io LOGIN_EMAIL: user@automatisch.io
LOGIN_PASSWORD: sample LOGIN_PASSWORD: sample
BACKEND_APP_URL: http://localhost:3000
BASE_URL: http://localhost:3000 BASE_URL: http://localhost:3000
GITHUB_CLIENT_ID: 1c0417daf898adfbd99a GITHUB_CLIENT_ID: 1c0417daf898adfbd99a
GITHUB_CLIENT_SECRET: 3328fa814dd582ccd03dbe785cfd683fb8da92b3 GITHUB_CLIENT_SECRET: 3328fa814dd582ccd03dbe785cfd683fb8da92b3

1
.gitignore vendored
View File

@@ -4,7 +4,6 @@ logs
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
lerna-debug.log*
.pnpm-debug.log* .pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html) # Diagnostic reports (https://nodejs.org/api/report.html)

View File

@@ -11,10 +11,12 @@ WORKDIR /automatisch
# copy the app, note .dockerignore # copy the app, note .dockerignore
COPY . /automatisch COPY . /automatisch
RUN yarn RUN cd packages/web && yarn
RUN cd packages/web && yarn build RUN cd packages/web && yarn build
RUN cd packages/backend && yarn --production
RUN \ RUN \
rm -rf /usr/local/share/.cache/ && \ rm -rf /usr/local/share/.cache/ && \
apk del build-dependencies apk del build-dependencies

View File

@@ -1,11 +0,0 @@
{
"packages": ["packages/*"],
"version": "0.14.0",
"npmClient": "yarn",
"command": {
"add": {
"exact": true
}
},
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
}

View File

@@ -1,32 +0,0 @@
{
"name": "@automatisch/root",
"license": "See LICENSE file",
"private": true,
"scripts": {
"start": "lerna run --stream --parallel --scope=@*/{web,backend} dev",
"start:web": "lerna run --stream --scope=@*/web dev",
"start:backend": "lerna run --stream --scope=@*/backend dev",
"build:docs": "cd ./packages/docs && yarn install && yarn build"
},
"workspaces": {
"packages": [
"packages/*"
],
"nohoist": [
"**/babel-loader",
"**/webpack",
"**/@automatisch/web",
"**/ajv"
]
},
"devDependencies": {
"eslint": "^8.13.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"lerna": "^8.0.0",
"prettier": "^2.5.1"
},
"publishConfig": {
"access": "public"
}
}

View File

@@ -23,6 +23,7 @@
"dependencies": { "dependencies": {
"@bull-board/express": "^3.10.1", "@bull-board/express": "^3.10.1",
"@casl/ability": "^6.5.0", "@casl/ability": "^6.5.0",
"@faker-js/faker": "^9.2.0",
"@node-saml/passport-saml": "^4.0.4", "@node-saml/passport-saml": "^4.0.4",
"@rudderstack/rudder-sdk-node": "^1.1.2", "@rudderstack/rudder-sdk-node": "^1.1.2",
"@sentry/node": "^7.42.0", "@sentry/node": "^7.42.0",
@@ -36,6 +37,9 @@
"crypto-js": "^4.1.1", "crypto-js": "^4.1.1",
"debug": "~2.6.9", "debug": "~2.6.9",
"dotenv": "^10.0.0", "dotenv": "^10.0.0",
"eslint": "^8.13.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"express": "~4.18.2", "express": "~4.18.2",
"express-async-errors": "^3.1.1", "express-async-errors": "^3.1.1",
"express-basic-auth": "^1.2.1", "express-basic-auth": "^1.2.1",
@@ -61,6 +65,7 @@
"pg": "^8.7.1", "pg": "^8.7.1",
"php-serialize": "^4.0.2", "php-serialize": "^4.0.2",
"pluralize": "^8.0.0", "pluralize": "^8.0.0",
"prettier": "^2.5.1",
"raw-body": "^2.5.2", "raw-body": "^2.5.2",
"showdown": "^2.1.0", "showdown": "^2.1.0",
"uuid": "^9.0.1", "uuid": "^9.0.1",

View File

@@ -407,7 +407,7 @@ class User extends Base {
} }
} }
async startTrialPeriod() { startTrialPeriod() {
this.trialExpiryDate = DateTime.now().plus({ days: 30 }).toISODate(); this.trialExpiryDate = DateTime.now().plus({ days: 30 }).toISODate();
} }
@@ -590,7 +590,7 @@ class User extends Base {
await this.generateHash(); await this.generateHash();
if (appConfig.isCloud) { if (appConfig.isCloud) {
await this.startTrialPeriod(); this.startTrialPeriod();
} }
} }

View File

@@ -1,7 +1,9 @@
import { describe, it, expect, vi } from 'vitest'; import { describe, it, expect, vi } from 'vitest';
import { DateTime, Duration } from 'luxon';
import appConfig from '../config/app.js'; import appConfig from '../config/app.js';
import Base from './base.js'; import Base from './base.js';
import AccessToken from './access-token.js'; import AccessToken from './access-token.js';
import Config from './config.js';
import Connection from './connection.js'; import Connection from './connection.js';
import Execution from './execution.js'; import Execution from './execution.js';
import Flow from './flow.js'; import Flow from './flow.js';
@@ -12,6 +14,12 @@ import Step from './step.js';
import Subscription from './subscription.ee.js'; import Subscription from './subscription.ee.js';
import UsageData from './usage-data.ee.js'; import UsageData from './usage-data.ee.js';
import User from './user.js'; import User from './user.js';
import deleteUserQueue from '../queues/delete-user.ee.js';
import emailQueue from '../queues/email.js';
import {
REMOVE_AFTER_30_DAYS_OR_150_JOBS,
REMOVE_AFTER_7_DAYS_OR_50_JOBS,
} from '../helpers/remove-job-configuration.js';
import { createUser } from '../../test/factories/user.js'; import { createUser } from '../../test/factories/user.js';
import { createConnection } from '../../test/factories/connection.js'; import { createConnection } from '../../test/factories/connection.js';
import { createRole } from '../../test/factories/role.js'; import { createRole } from '../../test/factories/role.js';
@@ -19,6 +27,9 @@ import { createPermission } from '../../test/factories/permission.js';
import { createFlow } from '../../test/factories/flow.js'; import { createFlow } from '../../test/factories/flow.js';
import { createStep } from '../../test/factories/step.js'; import { createStep } from '../../test/factories/step.js';
import { createExecution } from '../../test/factories/execution.js'; import { createExecution } from '../../test/factories/execution.js';
import { createSubscription } from '../../test/factories/subscription.js';
import { createUsageData } from '../../test/factories/usage-data.js';
import Billing from '../helpers/billing/index.ee.js';
describe('User model', () => { describe('User model', () => {
it('tableName should return correct name', () => { it('tableName should return correct name', () => {
@@ -604,4 +615,573 @@ describe('User model', () => {
).rejects.toThrowError('currentPassword: is incorrect.'); ).rejects.toThrowError('currentPassword: is incorrect.');
}); });
}); });
it('softRemove should soft remove the user, its associations and queue it for hard deletion in 30 days', async () => {
vi.useFakeTimers();
const date = new Date(2024, 10, 12, 12, 50, 0, 0);
vi.setSystemTime(date);
const user = await createUser();
const softRemoveAssociationsSpy = vi
.spyOn(user, 'softRemoveAssociations')
.mockReturnValue();
const deleteUserQueueAddSpy = vi
.spyOn(deleteUserQueue, 'add')
.mockResolvedValue();
await user.softRemove();
const refetchedSoftDeletedUser = await user.$query().withSoftDeleted();
const millisecondsFor30Days = Duration.fromObject({ days: 30 }).toMillis();
const jobName = `Delete user - ${user.id}`;
const jobPayload = { id: user.id };
const jobOptions = {
delay: millisecondsFor30Days,
};
expect(softRemoveAssociationsSpy).toHaveBeenCalledOnce();
expect(refetchedSoftDeletedUser.deletedAt).toStrictEqual(date);
expect(deleteUserQueueAddSpy).toHaveBeenCalledWith(
jobName,
jobPayload,
jobOptions
);
vi.useRealTimers();
});
it.todo('softRemoveAssociations');
it('sendResetPasswordEmail should generate reset password token and queue to send reset password email', async () => {
vi.useFakeTimers();
const date = new Date(2024, 10, 12, 14, 33, 0, 0);
vi.setSystemTime(date);
const user = await createUser();
const generateResetPasswordTokenSpy = vi
.spyOn(user, 'generateResetPasswordToken')
.mockReturnValue();
const emailQueueAddSpy = vi.spyOn(emailQueue, 'add').mockResolvedValue();
await user.sendResetPasswordEmail();
const refetchedUser = await user.$query();
const jobName = `Reset Password Email - ${user.id}`;
const jobPayload = {
email: refetchedUser.email,
subject: 'Reset Password',
template: 'reset-password-instructions.ee',
params: {
token: refetchedUser.resetPasswordToken,
webAppUrl: appConfig.webAppUrl,
fullName: refetchedUser.fullName,
},
};
const jobOptions = {
removeOnComplete: REMOVE_AFTER_7_DAYS_OR_50_JOBS,
removeOnFail: REMOVE_AFTER_30_DAYS_OR_150_JOBS,
};
expect(generateResetPasswordTokenSpy).toHaveBeenCalledOnce();
expect(emailQueueAddSpy).toHaveBeenCalledWith(
jobName,
jobPayload,
jobOptions
);
vi.useRealTimers();
});
describe('isResetPasswordTokenValid', () => {
it('should return true when resetPasswordTokenSentAt is within the next four hours', async () => {
vi.useFakeTimers();
const date = DateTime.fromObject(
{ year: 2024, month: 11, day: 12, hour: 16, minute: 30 },
{ zone: 'UTC+0' }
);
vi.setSystemTime(date);
const user = new User();
user.resetPasswordTokenSentAt = '2024-11-12T13:31:00.000Z';
expect(user.isResetPasswordTokenValid()).toBe(true);
vi.useRealTimers();
});
it('should return false when there is no resetPasswordTokenSentAt', async () => {
const user = new User();
expect(user.isResetPasswordTokenValid()).toBe(false);
});
it('should return false when resetPasswordTokenSentAt is older than four hours', async () => {
vi.useFakeTimers();
const date = DateTime.fromObject(
{ year: 2024, month: 11, day: 12, hour: 16, minute: 30 },
{ zone: 'UTC+0' }
);
vi.setSystemTime(date);
const user = new User();
user.resetPasswordTokenSentAt = '2024-11-12T12:29:00.000Z';
expect(user.isResetPasswordTokenValid()).toBe(false);
vi.useRealTimers();
});
});
it('sendInvitationEmail should generate invitation token and queue to send invitation email', async () => {
vi.useFakeTimers();
const date = DateTime.fromObject(
{ year: 2024, month: 11, day: 12, hour: 17, minute: 10 },
{ zone: 'UTC+0' }
);
vi.setSystemTime(date);
const user = await createUser();
const generateInvitationTokenSpy = vi
.spyOn(user, 'generateInvitationToken')
.mockReturnValue();
const emailQueueAddSpy = vi.spyOn(emailQueue, 'add').mockResolvedValue();
await user.sendInvitationEmail();
const refetchedUser = await user.$query();
const jobName = `Invitation Email - ${refetchedUser.id}`;
const jobPayload = {
email: refetchedUser.email,
subject: 'You are invited!',
template: 'invitation-instructions',
params: {
fullName: refetchedUser.fullName,
acceptInvitationUrl: refetchedUser.acceptInvitationUrl,
},
};
const jobOptions = {
removeOnComplete: REMOVE_AFTER_7_DAYS_OR_50_JOBS,
removeOnFail: REMOVE_AFTER_30_DAYS_OR_150_JOBS,
};
expect(generateInvitationTokenSpy).toHaveBeenCalledOnce();
expect(emailQueueAddSpy).toHaveBeenCalledWith(
jobName,
jobPayload,
jobOptions
);
vi.useRealTimers();
});
describe('isInvitationTokenValid', () => {
it('should return truen when invitationTokenSentAt is within the next four hours', async () => {
vi.useFakeTimers();
const date = DateTime.fromObject(
{ year: 2024, month: 11, day: 14, hour: 14, minute: 30 },
{ zone: 'UTC+0' }
);
vi.setSystemTime(date);
const user = new User();
user.invitationTokenSentAt = '2024-11-14T13:31:00.000Z';
expect(user.isInvitationTokenValid()).toBe(true);
vi.useRealTimers();
});
it('should return false when there is no invitationTokenSentAt', async () => {
const user = new User();
expect(user.isInvitationTokenValid()).toBe(false);
});
it('should return false when invitationTokenSentAt is older than seventy two hours', async () => {
vi.useFakeTimers();
const date = DateTime.fromObject(
{ year: 2024, month: 11, day: 14, hour: 14, minute: 30 },
{ zone: 'UTC+0' }
);
vi.setSystemTime(date);
const user = new User();
user.invitationTokenSentAt = '2024-11-11T14:20:00.000Z';
expect(user.isInvitationTokenValid()).toBe(false);
vi.useRealTimers();
});
});
describe('generateHash', () => {
it('should hash password and re-assign it', async () => {
const user = new User();
user.password = 'sample-password';
await user.generateHash();
expect(user.password).not.toBe('sample-password');
expect(await user.login('sample-password')).toBe(true);
});
it('should do nothing when password does not exist', async () => {
const user = new User();
await user.generateHash();
expect(user.password).toBe(undefined);
});
});
it('startTrialPeriod should assign trialExpiryDate 30 days from now', () => {
vi.useFakeTimers();
const date = DateTime.fromObject(
{ year: 2024, month: 11, day: 14, hour: 16 },
{ zone: 'UTC+0' }
);
vi.setSystemTime(date);
const user = new User();
user.startTrialPeriod();
expect(user.trialExpiryDate).toBe('2024-12-14');
vi.useRealTimers();
});
describe('isAllowedToRunFlows', () => {
it('should return true when Automatisch is self hosted', async () => {
const user = new User();
vi.spyOn(appConfig, 'isSelfHosted', 'get').mockReturnValue(true);
expect(await user.isAllowedToRunFlows()).toBe(true);
});
it('should return true when the user is in trial', async () => {
const user = new User();
vi.spyOn(user, 'inTrial').mockResolvedValue(true);
expect(await user.isAllowedToRunFlows()).toBe(true);
});
it('should return true when the user has active subscription and within quota limits', async () => {
const user = new User();
vi.spyOn(user, 'hasActiveSubscription').mockResolvedValue(true);
vi.spyOn(user, 'withinLimits').mockResolvedValue(true);
expect(await user.isAllowedToRunFlows()).toBe(true);
});
it('should return false when the user has active subscription over quota limits', async () => {
const user = new User();
vi.spyOn(user, 'hasActiveSubscription').mockResolvedValue(true);
vi.spyOn(user, 'withinLimits').mockResolvedValue(false);
expect(await user.isAllowedToRunFlows()).toBe(false);
});
it('should return false otherwise', async () => {
const user = new User();
expect(await user.isAllowedToRunFlows()).toBe(false);
});
});
describe('inTrial', () => {
it('should return false when Automatisch is self hosted', async () => {
const user = new User();
vi.spyOn(appConfig, 'isSelfHosted', 'get').mockReturnValue(true);
expect(await user.inTrial()).toBe(false);
});
it('should return false when the user does not have trial expiry date', async () => {
const user = new User();
vi.spyOn(appConfig, 'isSelfHosted', 'get').mockReturnValue(false);
expect(await user.inTrial()).toBe(false);
});
it('should return false when the user has an active subscription', async () => {
const user = new User();
user.trialExpiryDate = '2024-12-14';
vi.spyOn(appConfig, 'isSelfHosted', 'get').mockReturnValue(false);
const hasActiveSubscriptionSpy = vi
.spyOn(user, 'hasActiveSubscription')
.mockResolvedValue(true);
expect(await user.inTrial()).toBe(false);
expect(hasActiveSubscriptionSpy).toHaveBeenCalledOnce();
});
it('should return true when trial expiry date is in future', async () => {
vi.useFakeTimers();
const date = DateTime.fromObject(
{ year: 2024, month: 11, day: 12, hour: 17, minute: 30 },
{ zone: 'UTC+0' }
);
vi.setSystemTime(date);
const user = await createUser();
await user.startTrialPeriod();
const refetchedUser = await user.$query();
vi.spyOn(appConfig, 'isSelfHosted', 'get').mockReturnValue(false);
vi.spyOn(refetchedUser, 'hasActiveSubscription').mockResolvedValue(false);
expect(await refetchedUser.inTrial()).toBe(true);
vi.useRealTimers();
});
it('should return false when trial expiry date is in past', async () => {
vi.useFakeTimers();
const user = await createUser();
const presentDate = DateTime.fromObject(
{ year: 2024, month: 11, day: 17, hour: 11, minute: 30 },
{ zone: 'UTC+0' }
);
vi.setSystemTime(presentDate);
await user.startTrialPeriod();
const futureDate = DateTime.fromObject(
{ year: 2025, month: 1, day: 1 },
{ zone: 'UTC+0' }
);
vi.setSystemTime(futureDate);
const refetchedUser = await user.$query();
vi.spyOn(appConfig, 'isSelfHosted', 'get').mockReturnValue(false);
vi.spyOn(refetchedUser, 'hasActiveSubscription').mockResolvedValue(false);
expect(await refetchedUser.inTrial()).toBe(false);
vi.useRealTimers();
});
});
describe('hasActiveSubscription', () => {
it('should return true if current subscription is valid', async () => {
const user = await createUser();
await createSubscription({ userId: user.id, status: 'active' });
expect(await user.hasActiveSubscription()).toBe(true);
});
it('should return false if current subscription is not valid', async () => {
const user = await createUser();
await createSubscription({
userId: user.id,
status: 'deleted',
cancellationEffectiveDate: DateTime.now().minus({ day: 1 }).toString(),
});
expect(await user.hasActiveSubscription()).toBe(false);
});
it('should return false if Automatisch is not a cloud installation', async () => {
const user = new User();
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(false);
expect(await user.hasActiveSubscription()).toBe(false);
});
});
describe('withinLimits', () => {
it('should return true when the consumed task count is less than the quota', async () => {
const user = await createUser();
const subscription = await createSubscription({ userId: user.id });
await createUsageData({
subscriptionId: subscription.id,
userId: user.id,
consumedTaskCount: 100,
});
expect(await user.withinLimits()).toBe(true);
});
it('should return true when the consumed task count is less than the quota', async () => {
const user = await createUser();
const subscription = await createSubscription({ userId: user.id });
await createUsageData({
subscriptionId: subscription.id,
userId: user.id,
consumedTaskCount: 10000,
});
expect(await user.withinLimits()).toBe(false);
});
});
describe('getPlanAndUsage', () => {
it('should return plan and usage', async () => {
const user = await createUser();
const subscription = await createSubscription({ userId: user.id });
expect(await user.getPlanAndUsage()).toStrictEqual({
usage: {
task: 0,
},
plan: {
id: subscription.paddlePlanId,
name: '10k - monthly',
limit: '10,000',
},
});
});
it('should return trial plan and usage if no subscription exists', async () => {
const user = await createUser();
expect(await user.getPlanAndUsage()).toStrictEqual({
usage: {
task: 0,
},
plan: {
id: null,
name: 'Free Trial',
limit: null,
},
});
});
it('should throw not found when the current usage data does not exist', async () => {
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(false);
const user = await createUser();
expect(() => user.getPlanAndUsage()).rejects.toThrow('NotFoundError');
});
});
describe('getInvoices', () => {
it('should return invoices for the current subscription', async () => {
const user = await createUser();
const subscription = await createSubscription({ userId: user.id });
const getInvoicesSpy = vi
.spyOn(Billing.paddleClient, 'getInvoices')
.mockResolvedValue('dummy-invoices');
expect(await user.getInvoices()).toBe('dummy-invoices');
expect(getInvoicesSpy).toHaveBeenCalledWith(
Number(subscription.paddleSubscriptionId)
);
});
it('should return empty array without any subscriptions', async () => {
const user = await createUser();
expect(await user.getInvoices()).toStrictEqual([]);
});
});
it.todo('getApps');
it('createAdmin should create admin with given data and mark the installation completed', async () => {
const adminRole = await createRole({ name: 'Admin' });
const markInstallationCompletedSpy = vi
.spyOn(Config, 'markInstallationCompleted')
.mockResolvedValue();
const adminUser = await User.createAdmin({
fullName: 'Sample admin',
email: 'admin@automatisch.io',
password: 'sample',
});
expect(adminUser).toMatchObject({
fullName: 'Sample admin',
email: 'admin@automatisch.io',
roleId: adminRole.id,
});
expect(markInstallationCompletedSpy).toHaveBeenCalledOnce();
expect(await adminUser.login('sample')).toBe(true);
});
describe('registerUser', () => {
it('should register user with user role and given data', async () => {
const userRole = await createRole({ name: 'User' });
const user = await User.registerUser({
fullName: 'Sample user',
email: 'user@automatisch.io',
password: 'sample-password',
});
expect(user).toMatchObject({
fullName: 'Sample user',
email: 'user@automatisch.io',
roleId: userRole.id,
});
expect(await user.login('sample-password')).toBe(true);
});
it('should throw not found error when user role does not exist', async () => {
expect(() =>
User.registerUser({
fullName: 'Sample user',
email: 'user@automatisch.io',
password: 'sample-password',
})
).rejects.toThrowError('NotFoundError');
});
});
}); });

4807
packages/backend/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

1
packages/docs/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
pages/.vitepress/cache

View File

@@ -4,6 +4,7 @@
"license": "See LICENSE file", "license": "See LICENSE file",
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.", "description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
"private": true, "private": true,
"type": "module",
"scripts": { "scripts": {
"dev": "vitepress dev pages --port 3002", "dev": "vitepress dev pages --port 3002",
"build": "vitepress build pages", "build": "vitepress build pages",

View File

@@ -6,11 +6,19 @@ Clone main branch of Automatisch.
git clone git@github.com:automatisch/automatisch.git git clone git@github.com:automatisch/automatisch.git
``` ```
Then, install the dependencies. Then, install the dependencies for both backend and web packages separately.
```bash ```bash
cd automatisch cd automatisch
# Install backend dependencies
cd packages/backend
yarn install yarn install
# Install web dependencies
cd packages/web
yarn install
``` ```
## Backend ## Backend
@@ -53,12 +61,14 @@ yarn db:seed:user
Start the main backend server. Start the main backend server.
```bash ```bash
cd packages/backend
yarn dev yarn dev
``` ```
Start the worker server in another terminal tab. Start the worker server in another terminal tab.
```bash ```bash
cd packages/backend
yarn worker yarn worker
``` ```
@@ -84,6 +94,7 @@ It will automatically open [http://localhost:3001](http://localhost:3001) in you
```bash ```bash
cd packages/docs cd packages/docs
yarn install
yarn dev yarn dev
``` ```

View File

@@ -1,6 +1,6 @@
# Repository Structure # Repository Structure
We use `lerna` with `yarn workspaces` to manage the mono repository. We have the following packages: We manage a monorepo structure with the following packages:
``` ```
. .
@@ -15,3 +15,5 @@ We use `lerna` with `yarn workspaces` to manage the mono repository. We have the
- `docs` - The docs package contains the documentation website. - `docs` - The docs package contains the documentation website.
- `e2e-tests` - The e2e-tests package contains the end-to-end tests for the internal usage. - `e2e-tests` - The e2e-tests package contains the end-to-end tests for the internal usage.
- `web` - The web package contains the frontend application of Automatisch. - `web` - The web package contains the frontend application of Automatisch.
Each package is independently managed, and has its own package.json file to manage dependencies. This allows for better isolation and flexibility.

1192
packages/docs/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -3,3 +3,4 @@ POSTGRES_USER=automatisch_user
POSTGRES_PASSWORD=automatisch_password POSTGRES_PASSWORD=automatisch_password
POSTGRES_PORT=5432 POSTGRES_PORT=5432
POSTGRES_HOST=localhost POSTGRES_HOST=localhost
BACKEND_APP_URL=http://localhost:3000

View File

@@ -20,44 +20,54 @@ export class AdminApplicationSettingsPage extends AuthenticatedPage {
} }
async allowCustomConnections() { async allowCustomConnections() {
await expect(this.disableConnectionsSwitch).not.toBeChecked(); await expect(this.allowCustomConnectionsSwitch).not.toBeChecked();
await this.allowCustomConnectionsSwitch.check(); await this.allowCustomConnectionsSwitch.check();
await expect(this.allowCustomConnectionsSwitch).toBeChecked();
await this.saveButton.click(); await this.saveButton.click();
} }
async allowSharedConnections() { async allowSharedConnections() {
await expect(this.disableConnectionsSwitch).not.toBeChecked(); await expect(this.allowSharedConnectionsSwitch).not.toBeChecked();
await this.allowSharedConnectionsSwitch.check(); await this.allowSharedConnectionsSwitch.check();
await expect(this.allowSharedConnectionsSwitch).toBeChecked();
await this.saveButton.click(); await this.saveButton.click();
} }
async disallowConnections() { async disallowConnections() {
await expect(this.disableConnectionsSwitch).not.toBeChecked(); await expect(this.disableConnectionsSwitch).not.toBeChecked();
await this.disableConnectionsSwitch.check(); await this.disableConnectionsSwitch.check();
await expect(this.disableConnectionsSwitch).toBeChecked();
await this.saveButton.click(); await this.saveButton.click();
} }
async disallowCustomConnections() { async disallowCustomConnections() {
await expect(this.disableConnectionsSwitch).toBeChecked(); await expect(this.allowCustomConnectionsSwitch).toBeChecked();
await this.allowCustomConnectionsSwitch.uncheck(); await this.allowCustomConnectionsSwitch.uncheck();
await expect(this.allowCustomConnectionsSwitch).not.toBeChecked();
await this.saveButton.click(); await this.saveButton.click();
} }
async disallowSharedConnections() { async disallowSharedConnections() {
await expect(this.disableConnectionsSwitch).toBeChecked(); await expect(this.allowSharedConnectionsSwitch).toBeChecked();
await this.allowSharedConnectionsSwitch.uncheck(); await this.allowSharedConnectionsSwitch.uncheck();
await expect(this.allowSharedConnectionsSwitch).not.toBeChecked();
await this.saveButton.click(); await this.saveButton.click();
} }
async allowConnections() { async allowConnections() {
await expect(this.disableConnectionsSwitch).toBeChecked(); await expect(this.disableConnectionsSwitch).toBeChecked();
await this.disableConnectionsSwitch.uncheck(); await this.disableConnectionsSwitch.uncheck();
await expect(this.disableConnectionsSwitch).not.toBeChecked();
await this.saveButton.click(); await this.saveButton.click();
} }
async expectSuccessSnackbarToBeVisible() { async expectSuccessSnackbarToBeVisible() {
await expect(this.successSnackbar).toHaveCount(1); const snackbars = await this.successSnackbar.all();
await this.successSnackbar.click(); for (const snackbar of snackbars) {
await expect(this.successSnackbar).toHaveCount(0); await expect(await snackbar.getAttribute('data-snackbar-variant')).toBe(
'success'
);
// await snackbar.click();
}
} }
} }

View File

@@ -1,3 +1,5 @@
import { expect } from '@playwright/test';
const { AuthenticatedPage } = require('../authenticated-page'); const { AuthenticatedPage } = require('../authenticated-page');
const { RoleConditionsModal } = require('./role-conditions-modal'); const { RoleConditionsModal } = require('./role-conditions-modal');
@@ -16,6 +18,7 @@ export class AdminCreateRolePage extends AuthenticatedPage {
this.executionRow = page.getByTestId('Execution-permission-row'); this.executionRow = page.getByTestId('Execution-permission-row');
this.flowRow = page.getByTestId('Flow-permission-row'); this.flowRow = page.getByTestId('Flow-permission-row');
this.pageTitle = page.getByTestId('create-role-title'); this.pageTitle = page.getByTestId('create-role-title');
this.permissionsCatalog = page.getByTestId('permissions-catalog');
} }
/** /**
@@ -104,4 +107,8 @@ export class AdminCreateRolePage extends AuthenticatedPage {
throw new Error(`${subject} does not have action ${action}`); throw new Error(`${subject} does not have action ${action}`);
} }
} }
async waitForPermissionsCatalogToVisible() {
await expect(this.permissionsCatalog).toBeVisible();
}
} }

View File

@@ -14,8 +14,12 @@ export class AdminCreateUserPage extends AuthenticatedPage {
this.roleInput = page.getByTestId('role.id-autocomplete'); this.roleInput = page.getByTestId('role.id-autocomplete');
this.createButton = page.getByTestId('create-button'); this.createButton = page.getByTestId('create-button');
this.pageTitle = page.getByTestId('create-user-title'); this.pageTitle = page.getByTestId('create-user-title');
this.invitationEmailInfoAlert = page.getByTestId('invitation-email-info-alert'); this.invitationEmailInfoAlert = page.getByTestId(
this.acceptInvitationLink = page.getByTestId('invitation-email-info-alert').getByRole('link'); 'invitation-email-info-alert'
);
this.acceptInvitationLink = page
.getByTestId('invitation-email-info-alert')
.getByRole('link');
} }
seed(seed) { seed(seed) {

View File

@@ -95,7 +95,6 @@ export class AdminUsersPage extends AuthenticatedPage {
}); });
} }
const rowLocator = await this.getUserRowByEmail(email); const rowLocator = await this.getUserRowByEmail(email);
console.log('rowLocator.count', email, await rowLocator.count());
if ((await rowLocator.count()) === 1) { if ((await rowLocator.count()) === 1) {
return rowLocator; return rowLocator;
} }

View File

@@ -51,10 +51,20 @@ export class BasePage {
}; };
} }
async closeSnackbar() {
await this.snackbar.click();
}
async closeSnackbarAndWaitUntilDetached() {
const snackbar = await this.snackbar;
await snackbar.click();
await snackbar.waitFor({ state: 'detached' });
}
/** /**
* Closes all snackbars, should be replaced later * Closes all snackbars, should be replaced later
*/ */
async closeSnackbar() { async closeAllSnackbars() {
const snackbars = await this.snackbar.all(); const snackbars = await this.snackbar.all();
for (const snackbar of snackbars) { for (const snackbar of snackbars) {
await snackbar.click(); await snackbar.click();

View File

@@ -0,0 +1,16 @@
const { expect } = require('../fixtures/index');
export const getToken = async (apiRequest) => {
const tokenResponse = await apiRequest.post(
`${process.env.BACKEND_APP_URL}/api/v1/access-tokens`,
{
data: {
email: process.env.LOGIN_EMAIL,
password: process.env.LOGIN_PASSWORD,
},
}
);
await expect(tokenResponse.status()).toBe(200);
return await tokenResponse.json();
};

View File

@@ -0,0 +1,69 @@
const { expect } = require('../fixtures/index');
export const createFlow = async (request, token) => {
const response = await request.post(
`${process.env.BACKEND_APP_URL}/api/v1/flows`,
{ headers: { Authorization: token } }
);
await expect(response.status()).toBe(201);
return await response.json();
};
export const getFlow = async (request, token, flowId) => {
const response = await request.get(
`${process.env.BACKEND_APP_URL}/api/v1/flows/${flowId}`,
{ headers: { Authorization: token } }
);
await expect(response.status()).toBe(200);
return await response.json();
};
export const updateFlowName = async (request, token, flowId) => {
const updateFlowNameResponse = await request.patch(
`${process.env.BACKEND_APP_URL}/api/v1/flows/${flowId}`,
{
headers: { Authorization: token },
data: { name: flowId },
}
);
await expect(updateFlowNameResponse.status()).toBe(200);
};
export const updateFlowStep = async (request, token, stepId, requestBody) => {
const updateTriggerStepResponse = await request.patch(
`${process.env.BACKEND_APP_URL}/api/v1/steps/${stepId}`,
{
headers: { Authorization: token },
data: requestBody,
}
);
await expect(updateTriggerStepResponse.status()).toBe(200);
return await updateTriggerStepResponse.json();
};
export const testStep = async (request, token, stepId) => {
const testTriggerStepResponse = await request.post(
`${process.env.BACKEND_APP_URL}/api/v1/steps/${stepId}/test`,
{
headers: { Authorization: token },
}
);
await expect(testTriggerStepResponse.status()).toBe(200);
};
export const publishFlow = async (request, token, flowId) => {
const publishFlowResponse = await request.patch(
`${process.env.BACKEND_APP_URL}/api/v1/flows/${flowId}/status`,
{
headers: { Authorization: token },
data: { active: true },
}
);
await expect(publishFlowResponse.status()).toBe(200);
return publishFlowResponse.json();
};
export const triggerFlow = async (request, url) => {
const triggerFlowResponse = await request.get(url);
await expect(triggerFlowResponse.status()).toBe(204);
};

View File

@@ -0,0 +1,24 @@
const { expect } = require('../fixtures/index');
export const addUser = async (apiRequest, token, request) => {
const addUserResponse = await apiRequest.post(
`${process.env.BACKEND_APP_URL}/api/v1/admin/users`,
{
headers: { Authorization: token },
data: request,
}
);
await expect(addUserResponse.status()).toBe(201);
return await addUserResponse.json();
};
export const acceptInvitation = async (apiRequest, request) => {
const acceptInvitationResponse = await apiRequest.post(
`${process.env.BACKEND_APP_URL}/api/v1/users/invitation`,
{
data: request,
}
);
await expect(acceptInvitationResponse.status()).toBe(204);
};

View File

@@ -1,3 +1,5 @@
import { knexSnakeCaseMappers } from 'objection';
const fileExtension = 'js'; const fileExtension = 'js';
const knexConfig = { const knexConfig = {
@@ -7,7 +9,7 @@ const knexConfig = {
user: process.env.POSTGRES_USERNAME, user: process.env.POSTGRES_USERNAME,
port: process.env.POSTGRES_PORT, port: process.env.POSTGRES_PORT,
password: process.env.POSTGRES_PASSWORD, password: process.env.POSTGRES_PASSWORD,
database: process.env.POSTGRES_DATABASE database: process.env.POSTGRES_DATABASE,
}, },
searchPath: ['public'], searchPath: ['public'],
pool: { min: 0, max: 20 }, pool: { min: 0, max: 20 },

View File

@@ -26,13 +26,16 @@
}, },
"devDependencies": { "devDependencies": {
"@faker-js/faker": "^8.2.0", "@faker-js/faker": "^8.2.0",
"@playwright/test": "^1.45.1" "@playwright/test": "1.49.0",
"objection": "^3.1.5"
}, },
"dependencies": { "dependencies": {
"axios": "^1.6.0",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"eslint": "^8.13.0", "eslint": "^8.13.0",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0", "eslint-plugin-prettier": "^4.0.0",
"knex": "^2.4.0",
"luxon": "^3.4.4", "luxon": "^3.4.4",
"micro": "^10.0.1", "micro": "^10.0.1",
"pg": "^8.12.0", "pg": "^8.12.0",

View File

@@ -15,9 +15,9 @@ module.exports = defineConfig({
fullyParallel: true, fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */ /* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI, forbidOnly: !!process.env.CI,
retries: 0, retries: process.env.CI ? 1 : 0,
/* Opt out of parallel tests on CI. */ /* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined, workers: undefined,
/* Timeout threshold for each test */ /* Timeout threshold for each test */
timeout: 30000, timeout: 30000,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
@@ -30,7 +30,7 @@ module.exports = defineConfig({
baseURL: process.env.BASE_URL || 'http://localhost:3001', baseURL: process.env.BASE_URL || 'http://localhost:3001',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'retain-on-failure', trace: 'on-first-retry',
testIdAttribute: 'data-test', testIdAttribute: 'data-test',
viewport: { width: 1280, height: 720 }, viewport: { width: 1280, height: 720 },
}, },

View File

@@ -5,16 +5,18 @@ test.describe('Admin Applications', () => {
test.beforeAll(async () => { test.beforeAll(async () => {
const deleteAppAuthClients = { const deleteAppAuthClients = {
text: 'DELETE FROM app_auth_clients WHERE app_key in ($1, $2, $3, $4, $5)', text: 'DELETE FROM app_auth_clients WHERE app_key in ($1, $2, $3, $4, $5)',
values: ['carbone', 'spotify', 'deepl', 'mailchimp', 'reddit'] values: ['carbone', 'spotify', 'deepl', 'mailchimp', 'reddit'],
}; };
const deleteAppConfigs = { const deleteAppConfigs = {
text: 'DELETE FROM app_configs WHERE key in ($1, $2, $3, $4, $5)', text: 'DELETE FROM app_configs WHERE key in ($1, $2, $3, $4, $5)',
values: ['carbone', 'spotify', 'deepl', 'mailchimp', 'reddit'] values: ['carbone', 'spotify', 'deepl', 'mailchimp', 'reddit'],
}; };
try { try {
const deleteAppAuthClientsResult = await pgPool.query(deleteAppAuthClients); const deleteAppAuthClientsResult = await pgPool.query(
deleteAppAuthClients
);
expect(deleteAppAuthClientsResult.command).toBe('DELETE'); expect(deleteAppAuthClientsResult.command).toBe('DELETE');
const deleteAppConfigsResult = await pgPool.query(deleteAppConfigs); const deleteAppConfigsResult = await pgPool.query(deleteAppConfigs);
expect(deleteAppConfigsResult.command).toBe('DELETE'); expect(deleteAppConfigsResult.command).toBe('DELETE');
@@ -28,10 +30,11 @@ test.describe('Admin Applications', () => {
await adminApplicationsPage.navigateTo(); await adminApplicationsPage.navigateTo();
}); });
test('Admin should be able to toggle Application settings', async ({ // TODO skip until https://github.com/automatisch/automatisch/pull/2244
test.skip('Admin should be able to toggle Application settings', async ({
adminApplicationsPage, adminApplicationsPage,
adminApplicationSettingsPage, adminApplicationSettingsPage,
page page,
}) => { }) => {
await adminApplicationsPage.openApplication('Carbone'); await adminApplicationsPage.openApplication('Carbone');
await expect(page.url()).toContain('/admin-settings/apps/carbone/settings'); await expect(page.url()).toContain('/admin-settings/apps/carbone/settings');
@@ -57,7 +60,7 @@ test.describe('Admin Applications', () => {
adminApplicationsPage, adminApplicationsPage,
adminApplicationSettingsPage, adminApplicationSettingsPage,
flowEditorPage, flowEditorPage,
page page,
}) => { }) => {
await adminApplicationsPage.openApplication('Spotify'); await adminApplicationsPage.openApplication('Spotify');
await expect(page.url()).toContain('/admin-settings/apps/spotify/settings'); await expect(page.url()).toContain('/admin-settings/apps/spotify/settings');
@@ -75,11 +78,15 @@ test.describe('Admin Applications', () => {
const triggerStep = flowEditorPage.flowStep.last(); const triggerStep = flowEditorPage.flowStep.last();
await triggerStep.click(); await triggerStep.click();
await flowEditorPage.chooseAppAndEvent("Spotify", "Create Playlist"); await flowEditorPage.chooseAppAndEvent('Spotify', 'Create Playlist');
await flowEditorPage.connectionAutocomplete.click(); await flowEditorPage.connectionAutocomplete.click();
const newConnectionOption = page.getByRole('option').filter({ hasText: 'Add new connection' }); const newConnectionOption = page
const newSharedConnectionOption = page.getByRole('option').filter({ hasText: 'Add new shared connection' }); .getByRole('option')
.filter({ hasText: 'Add new connection' });
const newSharedConnectionOption = page
.getByRole('option')
.filter({ hasText: 'Add new shared connection' });
await expect(newConnectionOption).toBeEnabled(); await expect(newConnectionOption).toBeEnabled();
await expect(newConnectionOption).toHaveCount(1); await expect(newConnectionOption).toHaveCount(1);
@@ -91,7 +98,7 @@ test.describe('Admin Applications', () => {
adminApplicationSettingsPage, adminApplicationSettingsPage,
adminApplicationAuthClientsPage, adminApplicationAuthClientsPage,
flowEditorPage, flowEditorPage,
page page,
}) => { }) => {
await adminApplicationsPage.openApplication('Reddit'); await adminApplicationsPage.openApplication('Reddit');
await expect(page.url()).toContain('/admin-settings/apps/reddit/settings'); await expect(page.url()).toContain('/admin-settings/apps/reddit/settings');
@@ -101,13 +108,21 @@ test.describe('Admin Applications', () => {
await adminApplicationAuthClientsPage.openAuthClientsTab(); await adminApplicationAuthClientsPage.openAuthClientsTab();
await adminApplicationAuthClientsPage.openFirstAuthClientCreateForm(); await adminApplicationAuthClientsPage.openFirstAuthClientCreateForm();
const authClientForm = page.getByTestId("auth-client-form"); const authClientForm = page.getByTestId('auth-client-form');
await authClientForm.locator(page.getByTestId('switch')).check(); await authClientForm.locator(page.getByTestId('switch')).check();
await authClientForm.locator(page.locator('[name="name"]')).fill('redditAuthClient'); await authClientForm
await authClientForm.locator(page.locator('[name="clientId"]')).fill('redditClientId'); .locator(page.locator('[name="name"]'))
await authClientForm.locator(page.locator('[name="clientSecret"]')).fill('redditClientSecret'); .fill('redditAuthClient');
await authClientForm
.locator(page.locator('[name="clientId"]'))
.fill('redditClientId');
await authClientForm
.locator(page.locator('[name="clientSecret"]'))
.fill('redditClientSecret');
await adminApplicationAuthClientsPage.submitAuthClientForm(); await adminApplicationAuthClientsPage.submitAuthClientForm();
await adminApplicationAuthClientsPage.authClientShouldBeVisible('redditAuthClient'); await adminApplicationAuthClientsPage.authClientShouldBeVisible(
'redditAuthClient'
);
await page.goto('/'); await page.goto('/');
await page.getByTestId('create-flow-button').click(); await page.getByTestId('create-flow-button').click();
@@ -119,11 +134,15 @@ test.describe('Admin Applications', () => {
const triggerStep = flowEditorPage.flowStep.last(); const triggerStep = flowEditorPage.flowStep.last();
await triggerStep.click(); await triggerStep.click();
await flowEditorPage.chooseAppAndEvent("Reddit", "Create link post"); await flowEditorPage.chooseAppAndEvent('Reddit', 'Create link post');
await flowEditorPage.connectionAutocomplete.click(); await flowEditorPage.connectionAutocomplete.click();
const newConnectionOption = page.getByRole('option').filter({ hasText: 'Add new connection' }); const newConnectionOption = page
const newSharedConnectionOption = page.getByRole('option').filter({ hasText: 'Add new shared connection' }); .getByRole('option')
.filter({ hasText: 'Add new connection' });
const newSharedConnectionOption = page
.getByRole('option')
.filter({ hasText: 'Add new shared connection' });
await expect(newConnectionOption).toHaveCount(0); await expect(newConnectionOption).toHaveCount(0);
await expect(newSharedConnectionOption).toBeEnabled(); await expect(newSharedConnectionOption).toBeEnabled();
@@ -134,7 +153,7 @@ test.describe('Admin Applications', () => {
adminApplicationsPage, adminApplicationsPage,
adminApplicationSettingsPage, adminApplicationSettingsPage,
flowEditorPage, flowEditorPage,
page page,
}) => { }) => {
await adminApplicationsPage.openApplication('DeepL'); await adminApplicationsPage.openApplication('DeepL');
await expect(page.url()).toContain('/admin-settings/apps/deepl/settings'); await expect(page.url()).toContain('/admin-settings/apps/deepl/settings');
@@ -152,12 +171,18 @@ test.describe('Admin Applications', () => {
const triggerStep = flowEditorPage.flowStep.last(); const triggerStep = flowEditorPage.flowStep.last();
await triggerStep.click(); await triggerStep.click();
await flowEditorPage.chooseAppAndEvent("DeepL", "Translate text"); await flowEditorPage.chooseAppAndEvent('DeepL', 'Translate text');
await flowEditorPage.connectionAutocomplete.click(); await flowEditorPage.connectionAutocomplete.click();
const newConnectionOption = page.getByRole('option').filter({ hasText: 'Add new connection' }); const newConnectionOption = page
const newSharedConnectionOption = page.getByRole('option').filter({ hasText: 'Add new shared connection' }); .getByRole('option')
const noConnectionsOption = page.locator('.MuiAutocomplete-noOptions').filter({ hasText: 'No options' }); .filter({ hasText: 'Add new connection' });
const newSharedConnectionOption = page
.getByRole('option')
.filter({ hasText: 'Add new shared connection' });
const noConnectionsOption = page
.locator('.MuiAutocomplete-noOptions')
.filter({ hasText: 'No options' });
await expect(noConnectionsOption).toHaveCount(1); await expect(noConnectionsOption).toHaveCount(1);
await expect(newConnectionOption).toHaveCount(0); await expect(newConnectionOption).toHaveCount(0);
@@ -168,11 +193,11 @@ test.describe('Admin Applications', () => {
adminApplicationsPage, adminApplicationsPage,
adminApplicationSettingsPage, adminApplicationSettingsPage,
flowEditorPage, flowEditorPage,
page page,
}) => { }) => {
const queryUser = { const queryUser = {
text: 'SELECT * FROM users WHERE email = $1', text: 'SELECT * FROM users WHERE email = $1',
values: [process.env.LOGIN_EMAIL] values: [process.env.LOGIN_EMAIL],
}; };
try { try {
@@ -183,14 +208,16 @@ test.describe('Admin Applications', () => {
text: 'INSERT INTO connections (key, data, user_id, verified, draft) VALUES ($1, $2, $3, $4, $5)', text: 'INSERT INTO connections (key, data, user_id, verified, draft) VALUES ($1, $2, $3, $4, $5)',
values: [ values: [
'mailchimp', 'mailchimp',
"U2FsdGVkX1+cAtdHwLiuRL4DaK/T1aljeeKyPMmtWK0AmAIsKhYwQiuyQCYJO3mdZ31z73hqF2Y+yj2Kn2/IIpLRqCxB2sC0rCDCZyolzOZ290YcBXSzYRzRUxhoOcZEtwYDKsy8AHygKK/tkj9uv9k6wOe1LjipNik4VmRhKjEYizzjLrJpbeU1oY+qW0GBpPYomFTeNf+MejSSmsUYyYJ8+E/4GeEfaonvsTSwMT7AId98Lck6Vy4wrfgpm7sZZ8xU15/HqXZNc8UCo2iTdw45xj/Oov9+brX4WUASFPG8aYrK8dl/EdaOvr89P8uIofbSNZ25GjJvVF5ymarrPkTZ7djjJXchzpwBY+7GTJfs3funR/vIk0Hq95jgOFFP1liZyqTXSa49ojG3hzojRQ==", 'U2FsdGVkX1+cAtdHwLiuRL4DaK/T1aljeeKyPMmtWK0AmAIsKhYwQiuyQCYJO3mdZ31z73hqF2Y+yj2Kn2/IIpLRqCxB2sC0rCDCZyolzOZ290YcBXSzYRzRUxhoOcZEtwYDKsy8AHygKK/tkj9uv9k6wOe1LjipNik4VmRhKjEYizzjLrJpbeU1oY+qW0GBpPYomFTeNf+MejSSmsUYyYJ8+E/4GeEfaonvsTSwMT7AId98Lck6Vy4wrfgpm7sZZ8xU15/HqXZNc8UCo2iTdw45xj/Oov9+brX4WUASFPG8aYrK8dl/EdaOvr89P8uIofbSNZ25GjJvVF5ymarrPkTZ7djjJXchzpwBY+7GTJfs3funR/vIk0Hq95jgOFFP1liZyqTXSa49ojG3hzojRQ==',
queryUserResult.rows[0].id, queryUserResult.rows[0].id,
'true', 'true',
'false' 'false',
], ],
}; };
const createMailchimpConnectionResult = await pgPool.query(createMailchimpConnection); const createMailchimpConnectionResult = await pgPool.query(
createMailchimpConnection
);
expect(createMailchimpConnectionResult.rowCount).toBe(1); expect(createMailchimpConnectionResult.rowCount).toBe(1);
expect(createMailchimpConnectionResult.command).toBe('INSERT'); expect(createMailchimpConnectionResult.command).toBe('INSERT');
} catch (err) { } catch (err) {
@@ -199,7 +226,9 @@ test.describe('Admin Applications', () => {
} }
await adminApplicationsPage.openApplication('Mailchimp'); await adminApplicationsPage.openApplication('Mailchimp');
await expect(page.url()).toContain('/admin-settings/apps/mailchimp/settings'); await expect(page.url()).toContain(
'/admin-settings/apps/mailchimp/settings'
);
await adminApplicationSettingsPage.disallowConnections(); await adminApplicationSettingsPage.disallowConnections();
await adminApplicationSettingsPage.expectSuccessSnackbarToBeVisible(); await adminApplicationSettingsPage.expectSuccessSnackbarToBeVisible();
@@ -214,14 +243,22 @@ test.describe('Admin Applications', () => {
const triggerStep = flowEditorPage.flowStep.last(); const triggerStep = flowEditorPage.flowStep.last();
await triggerStep.click(); await triggerStep.click();
await flowEditorPage.chooseAppAndEvent("Mailchimp", "Create campaign"); await flowEditorPage.chooseAppAndEvent('Mailchimp', 'Create campaign');
await flowEditorPage.connectionAutocomplete.click(); await flowEditorPage.connectionAutocomplete.click();
await expect(page.getByRole('option').first()).toHaveText('Unnamed'); await expect(page.getByRole('option').first()).toHaveText('Unnamed');
const existingConnection = page.getByRole('option').filter({ hasText: 'Unnamed' }); const existingConnection = page
const newConnectionOption = page.getByRole('option').filter({ hasText: 'Add new connection' }); .getByRole('option')
const newSharedConnectionOption = page.getByRole('option').filter({ hasText: 'Add new shared connection' }); .filter({ hasText: 'Unnamed' });
const noConnectionsOption = page.locator('.MuiAutocomplete-noOptions').filter({ hasText: 'No options' }); const newConnectionOption = page
.getByRole('option')
.filter({ hasText: 'Add new connection' });
const newSharedConnectionOption = page
.getByRole('option')
.filter({ hasText: 'Add new shared connection' });
const noConnectionsOption = page
.locator('.MuiAutocomplete-noOptions')
.filter({ hasText: 'No options' });
await expect(await existingConnection.count()).toBeGreaterThan(0); await expect(await existingConnection.count()).toBeGreaterThan(0);
await expect(noConnectionsOption).toHaveCount(0); await expect(noConnectionsOption).toHaveCount(0);

View File

@@ -22,22 +22,18 @@ test.describe('Role management page', () => {
await adminRolesPage.navigateTo(); await adminRolesPage.navigateTo();
await adminRolesPage.createRoleButton.click(); await adminRolesPage.createRoleButton.click();
await adminCreateRolePage.isMounted(); await adminCreateRolePage.isMounted();
await adminCreateRolePage.waitForPermissionsCatalogToVisible();
await adminCreateRolePage.nameInput.fill('Create Edit Test'); await adminCreateRolePage.nameInput.fill('Create Edit Test');
await adminCreateRolePage.descriptionInput.fill('Test description'); await adminCreateRolePage.descriptionInput.fill('Test description');
await adminCreateRolePage.createButton.click(); await adminCreateRolePage.createButton.click();
await adminCreateRolePage.snackbar.waitFor({
state: 'attached',
});
const snackbar = await adminCreateRolePage.getSnackbarData( const snackbar = await adminCreateRolePage.getSnackbarData(
'snackbar-create-role-success' 'snackbar-create-role-success'
); );
await expect(snackbar.variant).toBe('success'); await expect(snackbar.variant).toBe('success');
await adminCreateRolePage.closeSnackbar();
}); });
let roleRow = await test.step( let roleRow =
'Make sure role data is correct', await test.step('Make sure role data is correct', async () => {
async () => {
const roleRow = await adminRolesPage.getRoleRowByName( const roleRow = await adminRolesPage.getRoleRowByName(
'Create Edit Test' 'Create Edit Test'
); );
@@ -48,8 +44,7 @@ test.describe('Role management page', () => {
await expect(roleData.canEdit).toBe(true); await expect(roleData.canEdit).toBe(true);
await expect(roleData.canDelete).toBe(true); await expect(roleData.canDelete).toBe(true);
return roleRow; return roleRow;
} });
);
await test.step('Edit the role', async () => { await test.step('Edit the role', async () => {
await adminRolesPage.clickEditRole(roleRow); await adminRolesPage.clickEditRole(roleRow);
@@ -57,19 +52,14 @@ test.describe('Role management page', () => {
await adminEditRolePage.nameInput.fill('Create Update Test'); await adminEditRolePage.nameInput.fill('Create Update Test');
await adminEditRolePage.descriptionInput.fill('Update test description'); await adminEditRolePage.descriptionInput.fill('Update test description');
await adminEditRolePage.updateButton.click(); await adminEditRolePage.updateButton.click();
await adminEditRolePage.snackbar.waitFor({
state: 'attached',
});
const snackbar = await adminEditRolePage.getSnackbarData( const snackbar = await adminEditRolePage.getSnackbarData(
'snackbar-edit-role-success' 'snackbar-edit-role-success'
); );
await expect(snackbar.variant).toBe('success'); await expect(snackbar.variant).toBe('success');
await adminEditRolePage.closeSnackbar();
}); });
roleRow = await test.step( roleRow =
'Make sure changes reflected on roles page', await test.step('Make sure changes reflected on roles page', async () => {
async () => {
await adminRolesPage.isMounted(); await adminRolesPage.isMounted();
const roleRow = await adminRolesPage.getRoleRowByName( const roleRow = await adminRolesPage.getRoleRowByName(
'Create Update Test' 'Create Update Test'
@@ -81,8 +71,7 @@ test.describe('Role management page', () => {
await expect(roleData.canEdit).toBe(true); await expect(roleData.canEdit).toBe(true);
await expect(roleData.canDelete).toBe(true); await expect(roleData.canDelete).toBe(true);
return roleRow; return roleRow;
} });
);
await test.step('Delete the role', async () => { await test.step('Delete the role', async () => {
await adminRolesPage.clickDeleteRole(roleRow); await adminRolesPage.clickDeleteRole(roleRow);
@@ -91,14 +80,10 @@ test.describe('Role management page', () => {
state: 'attached', state: 'attached',
}); });
await deleteModal.deleteButton.click(); await deleteModal.deleteButton.click();
await adminRolesPage.snackbar.waitFor({
state: 'attached',
});
const snackbar = await adminRolesPage.getSnackbarData( const snackbar = await adminRolesPage.getSnackbarData(
'snackbar-delete-role-success' 'snackbar-delete-role-success'
); );
await expect(snackbar.variant).toBe('success'); await expect(snackbar.variant).toBe('success');
await adminRolesPage.closeSnackbar();
await deleteModal.modal.waitFor({ await deleteModal.modal.waitFor({
state: 'detached', state: 'detached',
}); });
@@ -173,20 +158,15 @@ test.describe('Role management page', () => {
await test.step('Create a new role', async () => { await test.step('Create a new role', async () => {
await adminRolesPage.createRoleButton.click(); await adminRolesPage.createRoleButton.click();
await adminCreateRolePage.isMounted(); await adminCreateRolePage.isMounted();
await adminCreateRolePage.waitForPermissionsCatalogToVisible();
await adminCreateRolePage.nameInput.fill('Delete Role'); await adminCreateRolePage.nameInput.fill('Delete Role');
await adminCreateRolePage.createButton.click(); await adminCreateRolePage.createButton.click();
await adminCreateRolePage.snackbar.waitFor({
state: 'attached',
});
const snackbar = await adminCreateRolePage.getSnackbarData( const snackbar = await adminCreateRolePage.getSnackbarData(
'snackbar-create-role-success' 'snackbar-create-role-success'
); );
await expect(snackbar.variant).toBe('success'); await expect(snackbar.variant).toBe('success');
await adminCreateRolePage.closeSnackbar();
}); });
await test.step( await test.step('Create a new user with the "Delete Role" role', async () => {
'Create a new user with the "Delete Role" role',
async () => {
await adminUsersPage.navigateTo(); await adminUsersPage.navigateTo();
await adminUsersPage.createUserButton.click(); await adminUsersPage.createUserButton.click();
await adminCreateUserPage.fullNameInput.fill('User Role Test'); await adminCreateUserPage.fullNameInput.fill('User Role Test');
@@ -198,9 +178,6 @@ test.describe('Role management page', () => {
.getByRole('option', { name: 'Delete Role', exact: true }) .getByRole('option', { name: 'Delete Role', exact: true })
.click(); .click();
await adminCreateUserPage.createButton.click(); await adminCreateUserPage.createButton.click();
await adminCreateUserPage.snackbar.waitFor({
state: 'attached',
});
await adminCreateUserPage.invitationEmailInfoAlert.waitFor({ await adminCreateUserPage.invitationEmailInfoAlert.waitFor({
state: 'attached', state: 'attached',
}); });
@@ -208,25 +185,18 @@ test.describe('Role management page', () => {
'snackbar-create-user-success' 'snackbar-create-user-success'
); );
await expect(snackbar.variant).toBe('success'); await expect(snackbar.variant).toBe('success');
await adminUsersPage.closeSnackbar(); });
} await test.step('Try to delete "Delete Role" role when new user has it', async () => {
);
await test.step(
'Try to delete "Delete Role" role when new user has it',
async () => {
await adminRolesPage.navigateTo(); await adminRolesPage.navigateTo();
const row = await adminRolesPage.getRoleRowByName('Delete Role'); const row = await adminRolesPage.getRoleRowByName('Delete Role');
const modal = await adminRolesPage.clickDeleteRole(row); const modal = await adminRolesPage.clickDeleteRole(row);
await modal.deleteButton.click(); await modal.deleteButton.click();
await adminRolesPage.snackbar.waitFor({ const snackbar = await adminRolesPage.getSnackbarData(
state: 'attached', 'snackbar-delete-role-error'
});
const snackbar = await adminRolesPage.getSnackbarData('snackbar-delete-role-error');
await expect(snackbar.variant).toBe('error');
await adminRolesPage.closeSnackbar();
await modal.close();
}
); );
await expect(snackbar.variant).toBe('error');
await modal.close();
});
await test.step('Change the role the user has', async () => { await test.step('Change the role the user has', async () => {
await adminUsersPage.navigateTo(); await adminUsersPage.navigateTo();
await adminUsersPage.usersLoader.waitFor({ await adminUsersPage.usersLoader.waitFor({
@@ -241,14 +211,10 @@ test.describe('Role management page', () => {
.getByRole('option', { name: 'Admin' }) .getByRole('option', { name: 'Admin' })
.click(); .click();
await adminEditUserPage.updateButton.click(); await adminEditUserPage.updateButton.click();
await adminEditUserPage.snackbar.waitFor({
state: 'attached',
});
const snackbar = await adminEditUserPage.getSnackbarData( const snackbar = await adminEditUserPage.getSnackbarData(
'snackbar-edit-user-success' 'snackbar-edit-user-success'
); );
await expect(snackbar.variant).toBe('success'); await expect(snackbar.variant).toBe('success');
await adminEditUserPage.closeSnackbar();
}); });
await test.step('Delete the original role', async () => { await test.step('Delete the original role', async () => {
await adminRolesPage.navigateTo(); await adminRolesPage.navigateTo();
@@ -256,14 +222,10 @@ test.describe('Role management page', () => {
const modal = await adminRolesPage.clickDeleteRole(row); const modal = await adminRolesPage.clickDeleteRole(row);
await expect(modal.modal).toBeVisible(); await expect(modal.modal).toBeVisible();
await modal.deleteButton.click(); await modal.deleteButton.click();
await adminRolesPage.snackbar.waitFor({
state: 'attached',
});
const snackbar = await adminRolesPage.getSnackbarData( const snackbar = await adminRolesPage.getSnackbarData(
'snackbar-delete-role-success' 'snackbar-delete-role-success'
); );
await expect(snackbar.variant).toBe('success'); await expect(snackbar.variant).toBe('success');
await adminRolesPage.closeSnackbar();
}); });
}); });
@@ -277,16 +239,13 @@ test.describe('Role management page', () => {
await test.step('Create a new role', async () => { await test.step('Create a new role', async () => {
await adminRolesPage.createRoleButton.click(); await adminRolesPage.createRoleButton.click();
await adminCreateRolePage.isMounted(); await adminCreateRolePage.isMounted();
await adminCreateRolePage.waitForPermissionsCatalogToVisible();
await adminCreateRolePage.nameInput.fill('Cannot Delete Role'); await adminCreateRolePage.nameInput.fill('Cannot Delete Role');
await adminCreateRolePage.createButton.click(); await adminCreateRolePage.createButton.click();
await adminCreateRolePage.snackbar.waitFor({
state: 'attached',
});
const snackbar = await adminCreateRolePage.getSnackbarData( const snackbar = await adminCreateRolePage.getSnackbarData(
'snackbar-create-role-success' 'snackbar-create-role-success'
); );
await expect(snackbar.variant).toBe('success'); await expect(snackbar.variant).toBe('success');
await adminCreateRolePage.closeSnackbar();
}); });
await test.step('Create a new user with this role', async () => { await test.step('Create a new user with this role', async () => {
await adminUsersPage.navigateTo(); await adminUsersPage.navigateTo();
@@ -301,9 +260,6 @@ test.describe('Role management page', () => {
.getByRole('option', { name: 'Cannot Delete Role' }) .getByRole('option', { name: 'Cannot Delete Role' })
.click(); .click();
await adminCreateUserPage.createButton.click(); await adminCreateUserPage.createButton.click();
await adminCreateUserPage.snackbar.waitFor({
state: 'attached',
});
await adminCreateUserPage.invitationEmailInfoAlert.waitFor({ await adminCreateUserPage.invitationEmailInfoAlert.waitFor({
state: 'attached', state: 'attached',
}); });
@@ -311,40 +267,34 @@ test.describe('Role management page', () => {
'snackbar-create-user-success' 'snackbar-create-user-success'
); );
await expect(snackbar.variant).toBe('success'); await expect(snackbar.variant).toBe('success');
await adminCreateUserPage.closeSnackbar();
}); });
await test.step('Delete this user', async () => { await test.step('Delete this user', async () => {
await adminUsersPage.navigateTo(); await adminUsersPage.navigateTo();
const row = await adminUsersPage.findUserPageWithEmail( const row = await adminUsersPage.findUserPageWithEmail(
'user-delete-role-test@automatisch.io' 'user-delete-role-test@automatisch.io'
); );
// await test.waitForTimeout(10000);
const modal = await adminUsersPage.clickDeleteUser(row); const modal = await adminUsersPage.clickDeleteUser(row);
await modal.deleteButton.click(); await modal.deleteButton.click();
await adminUsersPage.snackbar.waitFor({
state: 'attached',
});
const snackbar = await adminUsersPage.getSnackbarData( const snackbar = await adminUsersPage.getSnackbarData(
'snackbar-delete-user-success' 'snackbar-delete-user-success'
); );
await expect(snackbar.variant).toBe('success'); await expect(snackbar.variant).toBe('success');
await adminUsersPage.closeSnackbar();
}); });
await test.step('Try deleting this role', async () => { await test.step('Try deleting this role', async () => {
await adminRolesPage.navigateTo(); await adminRolesPage.navigateTo();
const row = await adminRolesPage.getRoleRowByName('Cannot Delete Role'); const row = await adminRolesPage.getRoleRowByName('Cannot Delete Role');
const modal = await adminRolesPage.clickDeleteRole(row); const modal = await adminRolesPage.clickDeleteRole(row);
await modal.deleteButton.click(); await modal.deleteButton.click();
await adminRolesPage.snackbar.waitFor({ const snackbar = await adminRolesPage.getSnackbarData(
state: 'attached', 'snackbar-delete-role-error'
}); );
await expect(snackbar.variant).toBe('error');
/* /*
* TODO: await snackbar - make assertions based on product * TODO: await snackbar - make assertions based on product
* decisions * decisions
const snackbar = await adminRolesPage.getSnackbarData(); const snackbar = await adminRolesPage.getSnackbarData();
await expect(snackbar.variant).toBe('...'); await expect(snackbar.variant).toBe('...');
*/ */
await adminRolesPage.closeSnackbar();
}); });
}); });
}); });
@@ -362,16 +312,13 @@ test('Accessibility of role management page', async ({
await adminRolesPage.navigateTo(); await adminRolesPage.navigateTo();
await adminRolesPage.createRoleButton.click(); await adminRolesPage.createRoleButton.click();
await adminCreateRolePage.isMounted(); await adminCreateRolePage.isMounted();
await adminCreateRolePage.waitForPermissionsCatalogToVisible();
await adminCreateRolePage.nameInput.fill('Basic Test'); await adminCreateRolePage.nameInput.fill('Basic Test');
await adminCreateRolePage.createButton.click(); await adminCreateRolePage.createButton.click();
await adminCreateRolePage.snackbar.waitFor({
state: 'attached',
});
const snackbar = await adminCreateRolePage.getSnackbarData( const snackbar = await adminCreateRolePage.getSnackbarData(
'snackbar-create-role-success' 'snackbar-create-role-success'
); );
await expect(snackbar.variant).toBe('success'); await expect(snackbar.variant).toBe('success');
await adminCreateRolePage.closeSnackbar();
}); });
await test.step('Create a new user with the basic role', async () => { await test.step('Create a new user with the basic role', async () => {
@@ -385,9 +332,6 @@ test('Accessibility of role management page', async ({
.getByRole('option', { name: 'Basic Test' }) .getByRole('option', { name: 'Basic Test' })
.click(); .click();
await adminCreateUserPage.createButton.click(); await adminCreateUserPage.createButton.click();
await adminCreateUserPage.snackbar.waitFor({
state: 'attached',
});
await adminCreateUserPage.invitationEmailInfoAlert.waitFor({ await adminCreateUserPage.invitationEmailInfoAlert.waitFor({
state: 'attached', state: 'attached',
}); });
@@ -395,36 +339,27 @@ test('Accessibility of role management page', async ({
'snackbar-create-user-success' 'snackbar-create-user-success'
); );
await expect(snackbar.variant).toBe('success'); await expect(snackbar.variant).toBe('success');
await adminCreateUserPage.closeSnackbar();
}); });
await test.step('Logout and login to the basic role user', async () => { await test.step('Logout and login to the basic role user', async () => {
const acceptInvitationLink = await adminCreateUserPage.acceptInvitationLink; const acceptInvitationLink = await adminCreateUserPage.acceptInvitationLink;
console.log(acceptInvitationLink);
const acceptInvitationUrl = await acceptInvitationLink.textContent(); const acceptInvitationUrl = await acceptInvitationLink.textContent();
console.log(acceptInvitationUrl);
const acceptInvitatonToken = acceptInvitationUrl.split('?token=')[1]; const acceptInvitatonToken = acceptInvitationUrl.split('?token=')[1];
await page.getByTestId('profile-menu-button').click(); await page.getByTestId('profile-menu-button').click();
await page.getByTestId('logout-item').click(); await page.getByTestId('logout-item').click();
const acceptInvitationPage = new AcceptInvitation(page); const acceptInvitationPage = new AcceptInvitation(page);
await acceptInvitationPage.open(acceptInvitatonToken); await acceptInvitationPage.open(acceptInvitatonToken);
await acceptInvitationPage.acceptInvitation('sample'); await acceptInvitationPage.acceptInvitation('sample');
const loginPage = new LoginPage(page); const loginPage = new LoginPage(page);
// await loginPage.isMounted();
await loginPage.login('basic-role-test@automatisch.io', 'sample'); await loginPage.login('basic-role-test@automatisch.io', 'sample');
await expect(loginPage.loginButton).not.toBeVisible(); await expect(loginPage.loginButton).not.toBeVisible();
await expect(page).toHaveURL('/flows'); await expect(page).toHaveURL('/flows');
}); });
await test.step( await test.step('Navigate to the admin settings page and make sure it is blank', async () => {
'Navigate to the admin settings page and make sure it is blank',
async () => {
const pageUrl = new URL(page.url()); const pageUrl = new URL(page.url());
const url = `${pageUrl.origin}/admin-settings/users`; const url = `${pageUrl.origin}/admin-settings/users`;
await page.goto(url); await page.goto(url);
@@ -443,8 +378,7 @@ test('Accessibility of role management page', async ({
return false; return false;
}); });
await expect(isUnmounted).toBe(true); await expect(isUnmounted).toBe(true);
} });
);
await test.step('Log back into the admin account', async () => { await test.step('Log back into the admin account', async () => {
await page.goto('/'); await page.goto('/');
@@ -465,10 +399,10 @@ test('Accessibility of role management page', async ({
await adminEditUserPage.roleInput.click(); await adminEditUserPage.roleInput.click();
await adminEditUserPage.page.getByRole('option', { name: 'Admin' }).click(); await adminEditUserPage.page.getByRole('option', { name: 'Admin' }).click();
await adminEditUserPage.updateButton.click(); await adminEditUserPage.updateButton.click();
await adminEditUserPage.snackbar.waitFor({ const snackbar = await adminEditUserPage.getSnackbarData(
state: 'attached', 'snackbar-edit-user-success'
}); );
await adminEditUserPage.closeSnackbar(); await expect(snackbar.variant).toBe('success');
}); });
await test.step('Delete the role', async () => { await test.step('Delete the role', async () => {
@@ -480,14 +414,10 @@ test('Accessibility of role management page', async ({
state: 'attached', state: 'attached',
}); });
await deleteModal.deleteButton.click(); await deleteModal.deleteButton.click();
await adminRolesPage.snackbar.waitFor({
state: 'attached',
});
const snackbar = await adminRolesPage.getSnackbarData( const snackbar = await adminRolesPage.getSnackbarData(
'snackbar-delete-role-success' 'snackbar-delete-role-success'
); );
await expect(snackbar.variant).toBe('success'); await expect(snackbar.variant).toBe('success');
await adminRolesPage.closeSnackbar();
await deleteModal.modal.waitFor({ await deleteModal.modal.waitFor({
state: 'detached', state: 'detached',
}); });

View File

@@ -5,15 +5,16 @@ const { test, expect } = require('../../fixtures/index');
* otherwise tests will fail since users are only *soft*-deleted * otherwise tests will fail since users are only *soft*-deleted
*/ */
test.describe('User management page', () => { test.describe('User management page', () => {
test.beforeEach(async ({ adminUsersPage }) => { test.beforeEach(async ({ adminUsersPage }) => {
await adminUsersPage.navigateTo(); await adminUsersPage.navigateTo();
await adminUsersPage.closeSnackbar(); await adminUsersPage.closeAllSnackbars();
}); });
test( test('User creation and deletion process', async ({
'User creation and deletion process', adminCreateUserPage,
async ({ adminCreateUserPage, adminEditUserPage, adminUsersPage }) => { adminEditUserPage,
adminUsersPage,
}) => {
adminCreateUserPage.seed(9000); adminCreateUserPage.seed(9000);
const user = adminCreateUserPage.generateUser(); const user = adminCreateUserPage.generateUser();
await adminUsersPage.usersLoader.waitFor({ await adminUsersPage.usersLoader.waitFor({
@@ -21,21 +22,19 @@ test.describe('User management page', () => {
because visibility: hidden is used as part of the state transition in because visibility: hidden is used as part of the state transition in
notistack, see notistack, see
https://github.com/iamhosseindhv/notistack/blob/122f47057eb7ce5a1abfe923316cf8475303e99a/src/transitions/Collapse/Collapse.tsx#L110 https://github.com/iamhosseindhv/notistack/blob/122f47057eb7ce5a1abfe923316cf8475303e99a/src/transitions/Collapse/Collapse.tsx#L110
*/ */,
}); });
await test.step( await test.step('Create a user', async () => {
'Create a user',
async () => {
await adminUsersPage.createUserButton.click(); await adminUsersPage.createUserButton.click();
await adminCreateUserPage.fullNameInput.fill(user.fullName); await adminCreateUserPage.fullNameInput.fill(user.fullName);
await adminCreateUserPage.emailInput.fill(user.email); await adminCreateUserPage.emailInput.fill(user.email);
await adminCreateUserPage.roleInput.click(); await adminCreateUserPage.roleInput.click();
await adminCreateUserPage.page.getByRole( await adminCreateUserPage.page
'option', { name: 'Admin' } .getByRole('option', { name: 'Admin' })
).click(); .click();
await adminCreateUserPage.createButton.click(); await adminCreateUserPage.createButton.click();
await adminCreateUserPage.invitationEmailInfoAlert.waitFor({ await adminCreateUserPage.invitationEmailInfoAlert.waitFor({
state: 'attached' state: 'attached',
}); });
const snackbar = await adminUsersPage.getSnackbarData( const snackbar = await adminUsersPage.getSnackbarData(
@@ -43,23 +42,16 @@ test.describe('User management page', () => {
); );
await expect(snackbar.variant).toBe('success'); await expect(snackbar.variant).toBe('success');
await adminUsersPage.navigateTo(); await adminUsersPage.navigateTo();
await adminUsersPage.closeSnackbar(); });
} await test.step('Check the user exists with the expected properties', async () => {
);
await test.step(
'Check the user exists with the expected properties',
async () => {
await adminUsersPage.findUserPageWithEmail(user.email); await adminUsersPage.findUserPageWithEmail(user.email);
const userRow = await adminUsersPage.getUserRowByEmail(user.email); const userRow = await adminUsersPage.getUserRowByEmail(user.email);
const data = await adminUsersPage.getRowData(userRow); const data = await adminUsersPage.getRowData(userRow);
await expect(data.email).toBe(user.email); await expect(data.email).toBe(user.email);
await expect(data.fullName).toBe(user.fullName); await expect(data.fullName).toBe(user.fullName);
await expect(data.role).toBe('Admin'); await expect(data.role).toBe('Admin');
} });
); await test.step('Edit user info and make sure the edit works correctly', async () => {
await test.step(
'Edit user info and make sure the edit works correctly',
async () => {
await adminUsersPage.findUserPageWithEmail(user.email); await adminUsersPage.findUserPageWithEmail(user.email);
let userRow = await adminUsersPage.getUserRowByEmail(user.email); let userRow = await adminUsersPage.getUserRowByEmail(user.email);
@@ -73,17 +65,13 @@ test.describe('User management page', () => {
'snackbar-edit-user-success' 'snackbar-edit-user-success'
); );
await expect(snackbar.variant).toBe('success'); await expect(snackbar.variant).toBe('success');
await adminUsersPage.closeSnackbar();
await adminUsersPage.findUserPageWithEmail(user.email); await adminUsersPage.findUserPageWithEmail(user.email);
userRow = await adminUsersPage.getUserRowByEmail(user.email); userRow = await adminUsersPage.getUserRowByEmail(user.email);
const rowData = await adminUsersPage.getRowData(userRow); const rowData = await adminUsersPage.getRowData(userRow);
await expect(rowData.fullName).toBe(newUserInfo.fullName); await expect(rowData.fullName).toBe(newUserInfo.fullName);
} });
); await test.step('Delete user and check the page confirms this deletion', async () => {
await test.step(
'Delete user and check the page confirms this deletion',
async () => {
await adminUsersPage.findUserPageWithEmail(user.email); await adminUsersPage.findUserPageWithEmail(user.email);
const userRow = await adminUsersPage.getUserRowByEmail(user.email); const userRow = await adminUsersPage.getUserRowByEmail(user.email);
await adminUsersPage.clickDeleteUser(userRow); await adminUsersPage.clickDeleteUser(userRow);
@@ -94,41 +82,34 @@ test.describe('User management page', () => {
'snackbar-delete-user-success' 'snackbar-delete-user-success'
); );
await expect(snackbar.variant).toBe('success'); await expect(snackbar.variant).toBe('success');
await adminUsersPage.closeSnackbar();
await expect(userRow).not.toBeVisible(false); await expect(userRow).not.toBeVisible(false);
} });
);
}); });
test( test('Creating a user which has been deleted', async ({
'Creating a user which has been deleted', adminCreateUserPage,
async ({ adminCreateUserPage, adminUsersPage }) => { adminUsersPage,
}) => {
adminCreateUserPage.seed(9100); adminCreateUserPage.seed(9100);
const testUser = adminCreateUserPage.generateUser(); const testUser = adminCreateUserPage.generateUser();
await test.step( await test.step('Create the test user', async () => {
'Create the test user',
async () => {
await adminUsersPage.navigateTo(); await adminUsersPage.navigateTo();
await adminUsersPage.createUserButton.click(); await adminUsersPage.createUserButton.click();
await adminCreateUserPage.fullNameInput.fill(testUser.fullName); await adminCreateUserPage.fullNameInput.fill(testUser.fullName);
await adminCreateUserPage.emailInput.fill(testUser.email); await adminCreateUserPage.emailInput.fill(testUser.email);
await adminCreateUserPage.roleInput.click(); await adminCreateUserPage.roleInput.click();
await adminCreateUserPage.page.getByRole( await adminCreateUserPage.page
'option', { name: 'Admin' } .getByRole('option', { name: 'Admin' })
).click(); .click();
await adminCreateUserPage.createButton.click(); await adminCreateUserPage.createButton.click();
const snackbar = await adminUsersPage.getSnackbarData( const snackbar = await adminUsersPage.getSnackbarData(
'snackbar-create-user-success' 'snackbar-create-user-success'
); );
await expect(snackbar.variant).toBe('success'); await expect(snackbar.variant).toBe('success');
await adminUsersPage.closeSnackbar(); });
}
);
await test.step( await test.step('Delete the created user', async () => {
'Delete the created user',
async () => {
await adminUsersPage.navigateTo(); await adminUsersPage.navigateTo();
await adminUsersPage.findUserPageWithEmail(testUser.email); await adminUsersPage.findUserPageWithEmail(testUser.email);
const userRow = await adminUsersPage.getUserRowByEmail(testUser.email); const userRow = await adminUsersPage.getUserRowByEmail(testUser.email);
@@ -140,129 +121,107 @@ test.describe('User management page', () => {
); );
await expect(snackbar).not.toBeNull(); await expect(snackbar).not.toBeNull();
await expect(snackbar.variant).toBe('success'); await expect(snackbar.variant).toBe('success');
await adminUsersPage.closeSnackbar();
await expect(userRow).not.toBeVisible(false); await expect(userRow).not.toBeVisible(false);
} });
);
await test.step( await test.step('Create the user again', async () => {
'Create the user again',
async () => {
await adminUsersPage.createUserButton.click(); await adminUsersPage.createUserButton.click();
await adminCreateUserPage.fullNameInput.fill(testUser.fullName); await adminCreateUserPage.fullNameInput.fill(testUser.fullName);
await adminCreateUserPage.emailInput.fill(testUser.email); await adminCreateUserPage.emailInput.fill(testUser.email);
await adminCreateUserPage.roleInput.click(); await adminCreateUserPage.roleInput.click();
await adminCreateUserPage.page.getByRole( await adminCreateUserPage.page
'option', { name: 'Admin' } .getByRole('option', { name: 'Admin' })
).click(); .click();
await adminCreateUserPage.createButton.click(); await adminCreateUserPage.createButton.click();
const snackbar = await adminUsersPage.getSnackbarData('snackbar-error'); const snackbar = await adminUsersPage.getSnackbarData('snackbar-error');
await expect(snackbar.variant).toBe('error'); await expect(snackbar.variant).toBe('error');
await adminUsersPage.closeSnackbar(); });
} });
);
}
);
test( test('Creating a user which already exists', async ({
'Creating a user which already exists', adminCreateUserPage,
async ({ adminCreateUserPage, adminUsersPage, page }) => { adminUsersPage,
page,
}) => {
adminCreateUserPage.seed(9200); adminCreateUserPage.seed(9200);
const testUser = adminCreateUserPage.generateUser(); const testUser = adminCreateUserPage.generateUser();
await test.step( await test.step('Create the test user', async () => {
'Create the test user',
async () => {
await adminUsersPage.createUserButton.click(); await adminUsersPage.createUserButton.click();
await adminCreateUserPage.fullNameInput.fill(testUser.fullName); await adminCreateUserPage.fullNameInput.fill(testUser.fullName);
await adminCreateUserPage.emailInput.fill(testUser.email); await adminCreateUserPage.emailInput.fill(testUser.email);
await adminCreateUserPage.roleInput.click(); await adminCreateUserPage.roleInput.click();
await adminCreateUserPage.page.getByRole( await adminCreateUserPage.page
'option', { name: 'Admin' } .getByRole('option', { name: 'Admin' })
).click(); .click();
await adminCreateUserPage.createButton.click(); await adminCreateUserPage.createButton.click();
const snackbar = await adminUsersPage.getSnackbarData( const snackbar = await adminUsersPage.getSnackbarData(
'snackbar-create-user-success' 'snackbar-create-user-success'
); );
await expect(snackbar.variant).toBe('success'); await expect(snackbar.variant).toBe('success');
await adminUsersPage.closeSnackbar(); });
}
);
await test.step( await test.step('Create the user again', async () => {
'Create the user again',
async () => {
await adminUsersPage.navigateTo(); await adminUsersPage.navigateTo();
await adminUsersPage.createUserButton.click(); await adminUsersPage.createUserButton.click();
await adminCreateUserPage.fullNameInput.fill(testUser.fullName); await adminCreateUserPage.fullNameInput.fill(testUser.fullName);
await adminCreateUserPage.emailInput.fill(testUser.email); await adminCreateUserPage.emailInput.fill(testUser.email);
const createUserPageUrl = page.url(); const createUserPageUrl = page.url();
await adminCreateUserPage.roleInput.click(); await adminCreateUserPage.roleInput.click();
await adminCreateUserPage.page.getByRole( await adminCreateUserPage.page
'option', { name: 'Admin' } .getByRole('option', { name: 'Admin' })
).click(); .click();
await adminCreateUserPage.createButton.click(); await adminCreateUserPage.createButton.click();
await expect(page.url()).toBe(createUserPageUrl); await expect(page.url()).toBe(createUserPageUrl);
const snackbar = await adminUsersPage.getSnackbarData('snackbar-error'); const snackbar = await adminUsersPage.getSnackbarData('snackbar-error');
await expect(snackbar.variant).toBe('error'); await expect(snackbar.variant).toBe('error');
await adminUsersPage.closeSnackbar(); });
} });
);
}
);
test( test('Editing a user to have the same email as another user should not be allowed', async ({
'Editing a user to have the same email as another user should not be allowed', adminCreateUserPage,
async ({ adminEditUserPage,
adminCreateUserPage, adminEditUserPage, adminUsersPage, page adminUsersPage,
page,
}) => { }) => {
adminCreateUserPage.seed(9300); adminCreateUserPage.seed(9300);
const user1 = adminCreateUserPage.generateUser(); const user1 = adminCreateUserPage.generateUser();
const user2 = adminCreateUserPage.generateUser(); const user2 = adminCreateUserPage.generateUser();
await test.step( await test.step('Create the first user', async () => {
'Create the first user',
async () => {
await adminUsersPage.navigateTo(); await adminUsersPage.navigateTo();
await adminUsersPage.createUserButton.click(); await adminUsersPage.createUserButton.click();
await adminCreateUserPage.fullNameInput.fill(user1.fullName); await adminCreateUserPage.fullNameInput.fill(user1.fullName);
await adminCreateUserPage.emailInput.fill(user1.email); await adminCreateUserPage.emailInput.fill(user1.email);
await adminCreateUserPage.roleInput.click(); await adminCreateUserPage.roleInput.click();
await adminCreateUserPage.page.getByRole( await adminCreateUserPage.page
'option', { name: 'Admin' } .getByRole('option', { name: 'Admin' })
).click(); .click();
await adminCreateUserPage.createButton.click(); await adminCreateUserPage.createButton.click();
const snackbar = await adminUsersPage.getSnackbarData( const snackbar = await adminUsersPage.getSnackbarData(
'snackbar-create-user-success' 'snackbar-create-user-success'
); );
await expect(snackbar.variant).toBe('success'); await expect(snackbar.variant).toBe('success');
await adminUsersPage.closeSnackbar(); await adminUsersPage.closeAllSnackbars();
} });
);
await test.step( await test.step('Create the second user', async () => {
'Create the second user',
async () => {
await adminUsersPage.navigateTo(); await adminUsersPage.navigateTo();
await adminUsersPage.createUserButton.click(); await adminUsersPage.createUserButton.click();
await adminCreateUserPage.fullNameInput.fill(user2.fullName); await adminCreateUserPage.fullNameInput.fill(user2.fullName);
await adminCreateUserPage.emailInput.fill(user2.email); await adminCreateUserPage.emailInput.fill(user2.email);
await adminCreateUserPage.roleInput.click(); await adminCreateUserPage.roleInput.click();
await adminCreateUserPage.page.getByRole( await adminCreateUserPage.page
'option', { name: 'Admin' } .getByRole('option', { name: 'Admin' })
).click(); .click();
await adminCreateUserPage.createButton.click(); await adminCreateUserPage.createButton.click();
const snackbar = await adminUsersPage.getSnackbarData( const snackbar = await adminUsersPage.getSnackbarData(
'snackbar-create-user-success' 'snackbar-create-user-success'
); );
await expect(snackbar.variant).toBe('success'); await expect(snackbar.variant).toBe('success');
await adminUsersPage.closeSnackbar(); });
}
);
await test.step( await test.step('Try editing the second user to have the email of the first user', async () => {
'Try editing the second user to have the email of the first user',
async () => {
await adminUsersPage.navigateTo(); await adminUsersPage.navigateTo();
await adminUsersPage.findUserPageWithEmail(user2.email); await adminUsersPage.findUserPageWithEmail(user2.email);
let userRow = await adminUsersPage.getUserRowByEmail(user2.email); let userRow = await adminUsersPage.getUserRowByEmail(user2.email);
@@ -272,14 +231,9 @@ test.describe('User management page', () => {
const editPageUrl = page.url(); const editPageUrl = page.url();
await adminEditUserPage.updateButton.click(); await adminEditUserPage.updateButton.click();
const snackbar = await adminUsersPage.getSnackbarData( const snackbar = await adminUsersPage.getSnackbarData('snackbar-error');
'snackbar-error'
);
await expect(snackbar.variant).toBe('error'); await expect(snackbar.variant).toBe('error');
await adminUsersPage.closeSnackbar();
await expect(page.url()).toBe(editPageUrl); await expect(page.url()).toBe(editPageUrl);
} });
); });
}
);
}); });

View File

@@ -1,24 +1,55 @@
const { request } = require('@playwright/test');
const { test, expect } = require('../../fixtures/index'); const { test, expect } = require('../../fixtures/index');
const {AddMattermostConnectionModal} = require('../../fixtures/apps/mattermost/add-mattermost-connection-modal'); const {
AddMattermostConnectionModal,
} = require('../../fixtures/apps/mattermost/add-mattermost-connection-modal');
const { createFlow, updateFlowName, getFlow, updateFlowStep, testStep } = require('../../helpers/flow-api-helper');
const { getToken } = require('../../helpers/auth-api-helper');
test.describe('Pop-up message on connections', () => { test.describe('Pop-up message on connections', () => {
test.beforeEach(async ({ flowEditorPage, page }) => { test.beforeEach(async ({ flowEditorPage, page }) => {
await page.getByTestId('create-flow-button').click(); const apiRequest = await request.newContext();
await page.waitForURL( const tokenJsonResponse = await getToken(apiRequest);
/\/editor\/[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}/ const token = tokenJsonResponse.data.token;
);
await expect(page.getByTestId('flow-step')).toHaveCount(2);
await flowEditorPage.flowName.click(); let flow = await createFlow(apiRequest, token);
await flowEditorPage.flowNameInput.fill('PopupFlow'); const flowId = flow.data.id;
await flowEditorPage.createWebhookTrigger(true); await updateFlowName(apiRequest, token, flowId);
flow = await getFlow(apiRequest, token, flowId);
const flowSteps = flow.data.steps;
await flowEditorPage.chooseAppAndEvent('Mattermost', 'Send a message to channel'); const triggerStepId = flowSteps.find((step) => step.type === 'trigger').id;
await expect(flowEditorPage.continueButton).toHaveCount(1); const actionStepId = flowSteps.find((step) => step.type === 'action').id;
await expect(flowEditorPage.continueButton).not.toBeEnabled();
const triggerStep = await updateFlowStep(apiRequest, token, triggerStepId, {
appKey: 'webhook',
key: 'catchRawWebhook',
parameters: {
workSynchronously: false,
},
});
await apiRequest.get(triggerStep.data.webhookUrl);
await testStep(apiRequest, token, triggerStepId);
await updateFlowStep(apiRequest, token, actionStepId, {
appKey: 'mattermost',
key: 'sendMessageToChannel',
});
await testStep(apiRequest, token, actionStepId);
await page.reload();
const flowRow = await page.getByTestId('flow-row').filter({
hasText: flowId,
});
await flowRow.click();
const flowTriggerStep = await page.getByTestId('flow-step').nth(1);
await flowTriggerStep.click();
await page.getByText('Choose connection').click();
await flowEditorPage.connectionAutocomplete.click(); await flowEditorPage.connectionAutocomplete.click();
await flowEditorPage.addNewConnectionItem.click(); }); await flowEditorPage.addNewConnectionItem.click();
});
test('should show error to remind to enable pop-up on connection create', async ({ test('should show error to remind to enable pop-up on connection create', async ({
page, page,
@@ -37,8 +68,10 @@ test.describe('Pop-up message on connections', () => {
await addMattermostConnectionModal.fillConnectionForm(); await addMattermostConnectionModal.fillConnectionForm();
await addMattermostConnectionModal.submitConnectionForm(); await addMattermostConnectionModal.submitConnectionForm();
await expect(page.getByTestId("add-connection-error")).toHaveCount(1); await expect(page.getByTestId('add-connection-error')).toHaveCount(1);
await expect(page.getByTestId("add-connection-error")).toHaveText('Make sure pop-ups are enabled in your browser.'); await expect(page.getByTestId('add-connection-error')).toHaveText(
'Make sure pop-ups are enabled in your browser.'
);
}); });
test('should not show pop-up error if pop-ups are enabled on connection create', async ({ test('should not show pop-up error if pop-ups are enabled on connection create', async ({
@@ -51,13 +84,15 @@ test.describe('Pop-up message on connections', () => {
await addMattermostConnectionModal.submitConnectionForm(); await addMattermostConnectionModal.submitConnectionForm();
const popup = await popupPromise; const popup = await popupPromise;
await expect(popup.url()).toContain("mattermost"); await expect(popup.url()).toContain('mattermost');
await expect(page.getByTestId("add-connection-error")).toHaveCount(0); await expect(page.getByTestId('add-connection-error')).toHaveCount(0);
await test.step('Should show error on failed credentials verification', async () => { await test.step('Should show error on failed credentials verification', async () => {
await popup.close(); await popup.close();
await expect(page.getByTestId("add-connection-error")).toHaveCount(1); await expect(page.getByTestId('add-connection-error')).toHaveCount(1);
await expect(page.getByTestId("add-connection-error")).toHaveText('Error occured while verifying credentials!'); await expect(page.getByTestId('add-connection-error')).toHaveText(
'Error occured while verifying credentials!'
);
}); });
}); });
}); });

View File

@@ -1,57 +1,38 @@
const { request } = require('@playwright/test');
const { publicTest, expect } = require('../../fixtures/index'); const { publicTest, expect } = require('../../fixtures/index');
const { AdminUsersPage } = require('../../fixtures/admin/users-page');
const { MyProfilePage } = require('../../fixtures/my-profile-page'); const { MyProfilePage } = require('../../fixtures/my-profile-page');
const { LoginPage } = require('../../fixtures/login-page'); const { LoginPage } = require('../../fixtures/login-page');
const { addUser, acceptInvitation } = require('../../helpers/user-api-helper');
const { getToken } = require('../../helpers/auth-api-helper');
publicTest.describe('My Profile', () => { publicTest.describe('My Profile', () => {
let testUser; let testUser;
publicTest.beforeEach( publicTest.beforeEach(
async ({ acceptInvitationPage, adminCreateUserPage, loginPage, page }) => { async ({ adminCreateUserPage, loginPage, page }) => {
let acceptInvitationLink; let addUserResponse;
const apiRequest = await request.newContext();
adminCreateUserPage.seed( adminCreateUserPage.seed(
Math.ceil(Math.random() * Number.MAX_SAFE_INTEGER) Math.ceil(Math.random() * Number.MAX_SAFE_INTEGER)
); );
testUser = adminCreateUserPage.generateUser(); testUser = adminCreateUserPage.generateUser();
const adminUsersPage = new AdminUsersPage(page);
const myProfilePage = new MyProfilePage(page);
await publicTest.step('login as Admin', async () => {
await loginPage.login();
await expect(loginPage.page).toHaveURL('/flows');
});
await publicTest.step('create new user', async () => { await publicTest.step('create new user', async () => {
await adminUsersPage.navigateTo(); const tokenJsonResponse = await getToken(apiRequest);
await adminUsersPage.createUserButton.click(); addUserResponse = await addUser(
await adminCreateUserPage.fullNameInput.fill(testUser.fullName); apiRequest,
await adminCreateUserPage.emailInput.fill(testUser.email); tokenJsonResponse.data.token,
await adminCreateUserPage.roleInput.click(); {
await adminCreateUserPage.page fullName: testUser.fullName,
.getByRole('option', { name: 'Admin' }) email: testUser.email,
.click(); }
await adminCreateUserPage.createButton.click();
const snackbar = await adminUsersPage.getSnackbarData(
'snackbar-create-user-success'
); );
await expect(snackbar.variant).toBe('success');
});
await publicTest.step('copy invitation link', async () => {
const invitationMessage =
await adminCreateUserPage.acceptInvitationLink;
acceptInvitationLink = await invitationMessage.getAttribute('href');
});
await publicTest.step('logout', async () => {
await myProfilePage.logout();
}); });
await publicTest.step('accept invitation', async () => { await publicTest.step('accept invitation', async () => {
await page.goto(acceptInvitationLink); let acceptToken = addUserResponse.data.acceptInvitationUrl.split('=')[1];
await acceptInvitationPage.acceptInvitation(LoginPage.defaultPassword); await acceptInvitation(apiRequest, {token:acceptToken, password:LoginPage.defaultPassword});
}); });
await publicTest.step('login as new Admin', async () => { await publicTest.step('login as new Admin', async () => {

1145
packages/e2e-tests/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -18,6 +18,7 @@
"@testing-library/jest-dom": "^5.11.4", "@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0", "@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10", "@testing-library/user-event": "^12.1.10",
"axios": "^1.6.0",
"clipboard-copy": "^4.0.1", "clipboard-copy": "^4.0.1",
"compare-versions": "^4.1.3", "compare-versions": "^4.1.3",
"lodash": "^4.17.21", "lodash": "^4.17.21",

View File

@@ -6,7 +6,7 @@ import FormHelperText from '@mui/material/FormHelperText';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import ClearIcon from '@mui/icons-material/Clear'; import ClearIcon from '@mui/icons-material/Clear';
import { ActionButtonsWrapper } from './style'; import { ActionButtonsWrapper } from './style';
import ClickAwayListener from '@mui/base/ClickAwayListener'; import { ClickAwayListener } from '@mui/base/ClickAwayListener';
import InputLabel from '@mui/material/InputLabel'; import InputLabel from '@mui/material/InputLabel';
import { createEditor } from 'slate'; import { createEditor } from 'slate';
import { Editable, ReactEditor } from 'slate-react'; import { Editable, ReactEditor } from 'slate-react';

View File

@@ -1,8 +1,8 @@
import * as React from 'react'; import * as React from 'react';
import Paper from '@mui/material/Paper'; import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import Alert from '@mui/material/Alert';
import LoadingButton from '@mui/lab/LoadingButton'; import LoadingButton from '@mui/lab/LoadingButton';
import { enqueueSnackbar } from 'notistack';
import useForgotPassword from 'hooks/useForgotPassword'; import useForgotPassword from 'hooks/useForgotPassword';
import Form from 'components/Form'; import Form from 'components/Form';
@@ -12,25 +12,17 @@ import useFormatMessage from 'hooks/useFormatMessage';
export default function ForgotPasswordForm() { export default function ForgotPasswordForm() {
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const { const {
mutateAsync: forgotPassword, mutate: forgotPassword,
isPending: loading, isPending: loading,
isSuccess, isSuccess,
isError,
error,
} = useForgotPassword(); } = useForgotPassword();
const handleSubmit = async (values) => { const handleSubmit = ({ email }) => {
const { email } = values; forgotPassword({
try {
await forgotPassword({
email, email,
}); });
} catch (error) {
enqueueSnackbar(
error?.message || formatMessage('forgotPasswordForm.error'),
{
variant: 'error',
},
);
}
}; };
return ( return (
@@ -57,6 +49,16 @@ export default function ForgotPasswordForm() {
margin="dense" margin="dense"
autoComplete="username" autoComplete="username"
/> />
{isError && (
<Alert severity="error" sx={{ mt: 2 }}>
{error?.message || formatMessage('forgotPasswordForm.error')}
</Alert>
)}
{isSuccess && (
<Alert severity="success" sx={{ mt: 2 }}>
{formatMessage('forgotPasswordForm.instructionsSent')}
</Alert>
)}
<LoadingButton <LoadingButton
type="submit" type="submit"
variant="contained" variant="contained"
@@ -68,14 +70,6 @@ export default function ForgotPasswordForm() {
> >
{formatMessage('forgotPasswordForm.submit')} {formatMessage('forgotPasswordForm.submit')}
</LoadingButton> </LoadingButton>
{isSuccess && (
<Typography
variant="body1"
sx={{ color: (theme) => theme.palette.success.main }}
>
{formatMessage('forgotPasswordForm.instructionsSent')}
</Typography>
)}
</Form> </Form>
</Paper> </Paper>
); );

View File

@@ -2,6 +2,7 @@ import * as React from 'react';
import { useNavigate, Link as RouterLink } from 'react-router-dom'; import { useNavigate, Link as RouterLink } from 'react-router-dom';
import Paper from '@mui/material/Paper'; import Paper from '@mui/material/Paper';
import Link from '@mui/material/Link'; import Link from '@mui/material/Link';
import Alert from '@mui/material/Alert';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import LoadingButton from '@mui/lab/LoadingButton'; import LoadingButton from '@mui/lab/LoadingButton';
import useAuthentication from 'hooks/useAuthentication'; import useAuthentication from 'hooks/useAuthentication';
@@ -11,7 +12,6 @@ import Form from 'components/Form';
import TextField from 'components/TextField'; import TextField from 'components/TextField';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';
import useCreateAccessToken from 'hooks/useCreateAccessToken'; import useCreateAccessToken from 'hooks/useCreateAccessToken';
import { Alert } from '@mui/material';
function LoginForm() { function LoginForm() {
const isCloud = useCloud(); const isCloud = useCloud();
@@ -45,7 +45,7 @@ function LoginForm() {
const renderError = () => { const renderError = () => {
const errors = error?.response?.data?.errors?.general || [ const errors = error?.response?.data?.errors?.general || [
formatMessage('loginForm.error'), error?.message || formatMessage('loginForm.error'),
]; ];
return errors.map((error) => ( return errors.map((error) => (

View File

@@ -30,7 +30,7 @@ const PermissionCatalogField = ({
if (isPermissionCatalogLoading) return <PermissionCatalogFieldLoader />; if (isPermissionCatalogLoading) return <PermissionCatalogFieldLoader />;
return ( return (
<TableContainer component={Paper}> <TableContainer data-test="permissions-catalog" component={Paper}>
<Table> <Table>
<TableHead> <TableHead>
<TableRow> <TableRow>

View File

@@ -1,5 +1,5 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ClickAwayListener from '@mui/base/ClickAwayListener'; import { ClickAwayListener } from '@mui/base/ClickAwayListener';
import FormHelperText from '@mui/material/FormHelperText'; import FormHelperText from '@mui/material/FormHelperText';
import InputLabel from '@mui/material/InputLabel'; import InputLabel from '@mui/material/InputLabel';
import * as React from 'react'; import * as React from 'react';

View File

@@ -2,6 +2,7 @@ import { yupResolver } from '@hookform/resolvers/yup';
import LoadingButton from '@mui/lab/LoadingButton'; import LoadingButton from '@mui/lab/LoadingButton';
import Paper from '@mui/material/Paper'; import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import Alert from '@mui/material/Alert';
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
import * as React from 'react'; import * as React from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom'; import { useNavigate, useSearchParams } from 'react-router-dom';
@@ -30,6 +31,8 @@ export default function ResetPasswordForm() {
mutateAsync: resetPassword, mutateAsync: resetPassword,
isPending, isPending,
isSuccess, isSuccess,
error,
isError,
} = useResetPassword(); } = useResetPassword();
const token = searchParams.get('token'); const token = searchParams.get('token');
@@ -47,14 +50,23 @@ export default function ResetPasswordForm() {
}, },
}); });
navigate(URLS.LOGIN); navigate(URLS.LOGIN);
} catch (error) { } catch {}
enqueueSnackbar( };
error?.message || formatMessage('resetPasswordForm.error'),
{ const renderError = () => {
variant: 'error', if (!isError) {
}, return null;
);
} }
const errors = error?.response?.data?.errors?.general || [
error?.message || formatMessage('resetPasswordForm.error'),
];
return errors.map((error) => (
<Alert severity="error" sx={{ mt: 2 }}>
{error}
</Alert>
));
}; };
return ( return (
@@ -96,7 +108,6 @@ export default function ResetPasswordForm() {
: '' : ''
} }
/> />
<TextField <TextField
label={formatMessage( label={formatMessage(
'resetPasswordForm.confirmPasswordFieldLabel', 'resetPasswordForm.confirmPasswordFieldLabel',
@@ -117,7 +128,7 @@ export default function ResetPasswordForm() {
: '' : ''
} }
/> />
{renderError()}
<LoadingButton <LoadingButton
type="submit" type="submit"
variant="contained" variant="contained"

View File

@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import ButtonGroup from '@mui/material/ButtonGroup'; import ButtonGroup from '@mui/material/ButtonGroup';
import ClickAwayListener from '@mui/material/ClickAwayListener'; import { ClickAwayListener } from '@mui/base/ClickAwayListener';
import Grow from '@mui/material/Grow'; import Grow from '@mui/material/Grow';
import MenuItem from '@mui/material/MenuItem'; import MenuItem from '@mui/material/MenuItem';
import MenuList from '@mui/material/MenuList'; import MenuList from '@mui/material/MenuList';

11069
packages/web/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

16740
yarn.lock

File diff suppressed because it is too large Load Diff