Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b48b2592d5 |
@@ -5,11 +5,8 @@ BACKEND_PORT=3000
|
||||
WEB_PORT=3001
|
||||
|
||||
echo "Configuring backend environment variables..."
|
||||
|
||||
cd packages/backend
|
||||
|
||||
rm -rf .env
|
||||
|
||||
echo "
|
||||
PORT=$BACKEND_PORT
|
||||
WEB_APP_URL=http://localhost:$WEB_PORT
|
||||
@@ -24,35 +21,24 @@ WEBHOOK_SECRET_KEY=sample_webhook_secret_key
|
||||
APP_SECRET_KEY=sample_app_secret_key
|
||||
REDIS_HOST=redis
|
||||
SERVE_WEB_APP_SEPARATELY=true" >> .env
|
||||
|
||||
echo "Installing backend dependencies..."
|
||||
|
||||
yarn
|
||||
|
||||
cd $CURRENT_DIR
|
||||
|
||||
echo "Configuring web environment variables..."
|
||||
|
||||
cd packages/web
|
||||
|
||||
rm -rf .env
|
||||
|
||||
echo "
|
||||
PORT=$WEB_PORT
|
||||
REACT_APP_BACKEND_URL=http://localhost:$BACKEND_PORT
|
||||
" >> .env
|
||||
|
||||
echo "Installing web dependencies..."
|
||||
|
||||
yarn
|
||||
|
||||
cd $CURRENT_DIR
|
||||
|
||||
echo "Installing and linking dependencies..."
|
||||
yarn
|
||||
yarn lerna bootstrap
|
||||
|
||||
echo "Migrating database..."
|
||||
|
||||
cd packages/backend
|
||||
|
||||
yarn db:migrate
|
||||
yarn db:seed:user
|
||||
|
||||
echo "Done!"
|
||||
echo "Done!"
|
9
.github/workflows/backend.yml
vendored
9
.github/workflows/backend.yml
vendored
@@ -41,11 +41,8 @@ jobs:
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
working-directory: packages/backend
|
||||
run: cd packages/backend && yarn
|
||||
- name: Copy .env-example.test file to .env.test
|
||||
run: cp .env-example.test .env.test
|
||||
working-directory: packages/backend
|
||||
run: cd packages/backend && cp .env-example.test .env.test
|
||||
- name: Run tests
|
||||
run: yarn test
|
||||
working-directory: packages/backend
|
||||
run: cd packages/backend && yarn test
|
||||
|
30
.github/workflows/ci.yml
vendored
30
.github/workflows/ci.yml
vendored
@@ -18,13 +18,11 @@ jobs:
|
||||
with:
|
||||
node-version: '18'
|
||||
cache: 'yarn'
|
||||
cache-dependency-path: packages/backend/yarn.lock
|
||||
cache-dependency-path: yarn.lock
|
||||
- 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: yarn --frozen-lockfile
|
||||
working-directory: packages/backend
|
||||
- run: yarn lint
|
||||
working-directory: packages/backend
|
||||
- run: cd packages/backend && yarn lint
|
||||
- run: echo "🍏 This job's status is ${{ job.status }}."
|
||||
start-backend-server:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -37,13 +35,11 @@ jobs:
|
||||
with:
|
||||
node-version: '18'
|
||||
cache: 'yarn'
|
||||
cache-dependency-path: packages/backend/yarn.lock
|
||||
cache-dependency-path: yarn.lock
|
||||
- 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: yarn --frozen-lockfile
|
||||
working-directory: packages/backend
|
||||
- run: yarn start
|
||||
working-directory: packages/backend
|
||||
- run: yarn --frozen-lockfile && yarn lerna bootstrap
|
||||
- run: cd packages/backend && yarn start
|
||||
env:
|
||||
ENCRYPTION_KEY: sample_encryption_key
|
||||
WEBHOOK_SECRET_KEY: sample_webhook_secret_key
|
||||
@@ -59,13 +55,11 @@ jobs:
|
||||
with:
|
||||
node-version: '18'
|
||||
cache: 'yarn'
|
||||
cache-dependency-path: packages/backend/yarn.lock
|
||||
cache-dependency-path: yarn.lock
|
||||
- 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: yarn --frozen-lockfile
|
||||
working-directory: packages/backend
|
||||
- run: yarn start:worker
|
||||
working-directory: packages/backend
|
||||
- run: yarn --frozen-lockfile && yarn lerna bootstrap
|
||||
- run: cd packages/backend && yarn start:worker
|
||||
env:
|
||||
ENCRYPTION_KEY: sample_encryption_key
|
||||
WEBHOOK_SECRET_KEY: sample_webhook_secret_key
|
||||
@@ -81,13 +75,11 @@ jobs:
|
||||
with:
|
||||
node-version: '18'
|
||||
cache: 'yarn'
|
||||
cache-dependency-path: packages/web/yarn.lock
|
||||
cache-dependency-path: yarn.lock
|
||||
- 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: yarn --frozen-lockfile
|
||||
working-directory: packages/web
|
||||
- run: yarn build
|
||||
working-directory: packages/web
|
||||
- run: yarn --frozen-lockfile && yarn lerna bootstrap
|
||||
- run: cd packages/web && yarn build
|
||||
env:
|
||||
CI: false
|
||||
- run: echo "🍏 This job's status is ${{ job.status }}."
|
||||
|
36
.github/workflows/playwright.yml
vendored
36
.github/workflows/playwright.yml
vendored
@@ -55,44 +55,19 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
cache: '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 }}
|
||||
node-version: 18
|
||||
- name: Install dependencies
|
||||
run: yarn && yarn lerna bootstrap
|
||||
- name: Install Playwright Browsers
|
||||
run: yarn playwright install --with-deps
|
||||
working-directory: ./packages/e2e-tests
|
||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
- name: Build Automatisch web
|
||||
working-directory: ./packages/web
|
||||
run: yarn build
|
||||
env:
|
||||
# Keep this until we clean up warnings in build processes
|
||||
CI: false
|
||||
working-directory: ./packages/web
|
||||
- name: Migrate database
|
||||
working-directory: ./packages/backend
|
||||
run: yarn db:migrate
|
||||
@@ -132,7 +107,6 @@ jobs:
|
||||
env:
|
||||
LOGIN_EMAIL: user@automatisch.io
|
||||
LOGIN_PASSWORD: sample
|
||||
BACKEND_APP_URL: http://localhost:3000
|
||||
BASE_URL: http://localhost:3000
|
||||
GITHUB_CLIENT_ID: 1c0417daf898adfbd99a
|
||||
GITHUB_CLIENT_SECRET: 3328fa814dd582ccd03dbe785cfd683fb8da92b3
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,6 +4,7 @@ logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
|
@@ -11,12 +11,10 @@ WORKDIR /automatisch
|
||||
# copy the app, note .dockerignore
|
||||
COPY . /automatisch
|
||||
|
||||
RUN cd packages/web && yarn
|
||||
RUN yarn
|
||||
|
||||
RUN cd packages/web && yarn build
|
||||
|
||||
RUN cd packages/backend && yarn --production
|
||||
|
||||
RUN \
|
||||
rm -rf /usr/local/share/.cache/ && \
|
||||
apk del build-dependencies
|
||||
|
13
lerna.json
Normal file
13
lerna.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"packages": [
|
||||
"packages/*"
|
||||
],
|
||||
"version": "0.10.0",
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"command": {
|
||||
"add": {
|
||||
"exact": true
|
||||
}
|
||||
}
|
||||
}
|
32
package.json
Normal file
32
package.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"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": "^4.0.0",
|
||||
"prettier": "^2.5.1"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
@@ -23,7 +23,6 @@
|
||||
"dependencies": {
|
||||
"@bull-board/express": "^3.10.1",
|
||||
"@casl/ability": "^6.5.0",
|
||||
"@faker-js/faker": "^9.2.0",
|
||||
"@node-saml/passport-saml": "^4.0.4",
|
||||
"@rudderstack/rudder-sdk-node": "^1.1.2",
|
||||
"@sentry/node": "^7.42.0",
|
||||
@@ -37,9 +36,6 @@
|
||||
"crypto-js": "^4.1.1",
|
||||
"debug": "~2.6.9",
|
||||
"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-async-errors": "^3.1.1",
|
||||
"express-basic-auth": "^1.2.1",
|
||||
@@ -65,7 +61,6 @@
|
||||
"pg": "^8.7.1",
|
||||
"php-serialize": "^4.0.2",
|
||||
"pluralize": "^8.0.0",
|
||||
"prettier": "^2.5.1",
|
||||
"raw-body": "^2.5.2",
|
||||
"showdown": "^2.1.0",
|
||||
"uuid": "^9.0.1",
|
||||
|
@@ -8,7 +8,7 @@ export default {
|
||||
key: 'instanceUrl',
|
||||
label: 'WordPress instance URL',
|
||||
type: 'string',
|
||||
required: true,
|
||||
required: false,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
|
@@ -52,7 +52,7 @@ const appConfig = {
|
||||
isDev: appEnv === 'development',
|
||||
isTest: appEnv === 'test',
|
||||
isProd: appEnv === 'production',
|
||||
version: '0.14.0',
|
||||
version: '0.13.1',
|
||||
postgresDatabase: process.env.POSTGRES_DATABASE || 'automatisch_development',
|
||||
postgresSchema: process.env.POSTGRES_SCHEMA || 'public',
|
||||
postgresPort: parseInt(process.env.POSTGRES_PORT || '5432'),
|
||||
|
@@ -10,7 +10,7 @@ describe('GET /api/v1/automatisch/version', () => {
|
||||
|
||||
const expectedPayload = {
|
||||
data: {
|
||||
version: '0.14.0',
|
||||
version: '0.13.1',
|
||||
},
|
||||
meta: {
|
||||
count: 1,
|
||||
|
@@ -1,9 +1,8 @@
|
||||
import { vi, describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import SamlAuthProvider from '../models/saml-auth-provider.ee';
|
||||
import SamlAuthProvidersRoleMapping from '../models/saml-auth-providers-role-mapping.ee';
|
||||
import Identity from './identity.ee';
|
||||
import Base from './base';
|
||||
import appConfig from '../config/app';
|
||||
|
||||
describe('SamlAuthProvider model', () => {
|
||||
it('tableName should return correct name', () => {
|
||||
@@ -46,39 +45,4 @@ describe('SamlAuthProvider model', () => {
|
||||
|
||||
expect(virtualAttributes).toStrictEqual(expectedAttributes);
|
||||
});
|
||||
|
||||
it('loginUrl should return the URL of login', () => {
|
||||
const samlAuthProvider = new SamlAuthProvider();
|
||||
samlAuthProvider.issuer = 'sample-issuer';
|
||||
|
||||
vi.spyOn(appConfig, 'baseUrl', 'get').mockReturnValue(
|
||||
'https://automatisch.io'
|
||||
);
|
||||
|
||||
expect(samlAuthProvider.loginUrl).toStrictEqual(
|
||||
'https://automatisch.io/login/saml/sample-issuer'
|
||||
);
|
||||
});
|
||||
|
||||
it('loginCallbackUrl should return the URL of login callback', () => {
|
||||
const samlAuthProvider = new SamlAuthProvider();
|
||||
samlAuthProvider.issuer = 'sample-issuer';
|
||||
|
||||
vi.spyOn(appConfig, 'baseUrl', 'get').mockReturnValue(
|
||||
'https://automatisch.io'
|
||||
);
|
||||
|
||||
expect(samlAuthProvider.loginCallBackUrl).toStrictEqual(
|
||||
'https://automatisch.io/login/saml/sample-issuer/callback'
|
||||
);
|
||||
});
|
||||
|
||||
it('remoteLogoutUrl should return the URL from entrypoint', () => {
|
||||
const samlAuthProvider = new SamlAuthProvider();
|
||||
samlAuthProvider.entryPoint = 'https://example.com/saml/logout';
|
||||
|
||||
expect(samlAuthProvider.remoteLogoutUrl).toStrictEqual(
|
||||
'https://example.com/saml/logout'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@@ -407,7 +407,7 @@ class User extends Base {
|
||||
}
|
||||
}
|
||||
|
||||
startTrialPeriod() {
|
||||
async startTrialPeriod() {
|
||||
this.trialExpiryDate = DateTime.now().plus({ days: 30 }).toISODate();
|
||||
}
|
||||
|
||||
@@ -590,7 +590,7 @@ class User extends Base {
|
||||
await this.generateHash();
|
||||
|
||||
if (appConfig.isCloud) {
|
||||
this.startTrialPeriod();
|
||||
await this.startTrialPeriod();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,9 +1,7 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { DateTime, Duration } from 'luxon';
|
||||
import appConfig from '../config/app.js';
|
||||
import Base from './base.js';
|
||||
import AccessToken from './access-token.js';
|
||||
import Config from './config.js';
|
||||
import Connection from './connection.js';
|
||||
import Execution from './execution.js';
|
||||
import Flow from './flow.js';
|
||||
@@ -14,12 +12,6 @@ import Step from './step.js';
|
||||
import Subscription from './subscription.ee.js';
|
||||
import UsageData from './usage-data.ee.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 { createConnection } from '../../test/factories/connection.js';
|
||||
import { createRole } from '../../test/factories/role.js';
|
||||
@@ -27,9 +19,6 @@ import { createPermission } from '../../test/factories/permission.js';
|
||||
import { createFlow } from '../../test/factories/flow.js';
|
||||
import { createStep } from '../../test/factories/step.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', () => {
|
||||
it('tableName should return correct name', () => {
|
||||
@@ -591,597 +580,4 @@ describe('User model', () => {
|
||||
expect(refetchedUser.invitationTokenSentAt).toBe(null);
|
||||
expect(refetchedUser.status).toBe('active');
|
||||
});
|
||||
|
||||
describe('updatePassword', () => {
|
||||
it('should update password when the given current password matches with the user password', async () => {
|
||||
const user = await createUser({ password: 'sample-password' });
|
||||
|
||||
const updatedUser = await user.updatePassword({
|
||||
currentPassword: 'sample-password',
|
||||
password: 'new-password',
|
||||
});
|
||||
|
||||
expect(await updatedUser.login('new-password')).toBe(true);
|
||||
});
|
||||
|
||||
it('should throw validation error when the given current password does not match with the user password', async () => {
|
||||
const user = await createUser({ password: 'sample-password' });
|
||||
|
||||
await expect(
|
||||
user.updatePassword({
|
||||
currentPassword: 'wrong-password',
|
||||
password: 'new-password',
|
||||
})
|
||||
).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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
File diff suppressed because it is too large
Load Diff
1
packages/docs/.gitignore
vendored
1
packages/docs/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
pages/.vitepress/cache
|
@@ -4,7 +4,6 @@
|
||||
"license": "See LICENSE file",
|
||||
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vitepress dev pages --port 3002",
|
||||
"build": "vitepress build pages",
|
||||
|
@@ -6,19 +6,11 @@ Clone main branch of Automatisch.
|
||||
git clone git@github.com:automatisch/automatisch.git
|
||||
```
|
||||
|
||||
Then, install the dependencies for both backend and web packages separately.
|
||||
Then, install the dependencies.
|
||||
|
||||
```bash
|
||||
cd automatisch
|
||||
|
||||
# Install backend dependencies
|
||||
cd packages/backend
|
||||
yarn install
|
||||
|
||||
# Install web dependencies
|
||||
cd packages/web
|
||||
yarn install
|
||||
|
||||
```
|
||||
|
||||
## Backend
|
||||
@@ -61,14 +53,12 @@ yarn db:seed:user
|
||||
Start the main backend server.
|
||||
|
||||
```bash
|
||||
cd packages/backend
|
||||
yarn dev
|
||||
```
|
||||
|
||||
Start the worker server in another terminal tab.
|
||||
|
||||
```bash
|
||||
cd packages/backend
|
||||
yarn worker
|
||||
```
|
||||
|
||||
@@ -94,7 +84,6 @@ It will automatically open [http://localhost:3001](http://localhost:3001) in you
|
||||
|
||||
```bash
|
||||
cd packages/docs
|
||||
yarn install
|
||||
yarn dev
|
||||
```
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# Repository Structure
|
||||
|
||||
We manage a monorepo structure with the following packages:
|
||||
We use `lerna` with `yarn workspaces` to manage the mono repository. We have the following packages:
|
||||
|
||||
```
|
||||
.
|
||||
@@ -15,5 +15,3 @@ We manage a monorepo structure with the following packages:
|
||||
- `docs` - The docs package contains the documentation website.
|
||||
- `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.
|
||||
|
||||
Each package is independently managed, and has its own package.json file to manage dependencies. This allows for better isolation and flexibility.
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -2,5 +2,4 @@ POSTGRES_DB=automatisch
|
||||
POSTGRES_USER=automatisch_user
|
||||
POSTGRES_PASSWORD=automatisch_password
|
||||
POSTGRES_PORT=5432
|
||||
POSTGRES_HOST=localhost
|
||||
BACKEND_APP_URL=http://localhost:3000
|
||||
POSTGRES_HOST=localhost
|
@@ -20,54 +20,44 @@ export class AdminApplicationSettingsPage extends AuthenticatedPage {
|
||||
}
|
||||
|
||||
async allowCustomConnections() {
|
||||
await expect(this.allowCustomConnectionsSwitch).not.toBeChecked();
|
||||
await expect(this.disableConnectionsSwitch).not.toBeChecked();
|
||||
await this.allowCustomConnectionsSwitch.check();
|
||||
await expect(this.allowCustomConnectionsSwitch).toBeChecked();
|
||||
await this.saveButton.click();
|
||||
}
|
||||
|
||||
async allowSharedConnections() {
|
||||
await expect(this.allowSharedConnectionsSwitch).not.toBeChecked();
|
||||
await expect(this.disableConnectionsSwitch).not.toBeChecked();
|
||||
await this.allowSharedConnectionsSwitch.check();
|
||||
await expect(this.allowSharedConnectionsSwitch).toBeChecked();
|
||||
await this.saveButton.click();
|
||||
}
|
||||
|
||||
async disallowConnections() {
|
||||
await expect(this.disableConnectionsSwitch).not.toBeChecked();
|
||||
await this.disableConnectionsSwitch.check();
|
||||
await expect(this.disableConnectionsSwitch).toBeChecked();
|
||||
await this.saveButton.click();
|
||||
}
|
||||
|
||||
async disallowCustomConnections() {
|
||||
await expect(this.allowCustomConnectionsSwitch).toBeChecked();
|
||||
await expect(this.disableConnectionsSwitch).toBeChecked();
|
||||
await this.allowCustomConnectionsSwitch.uncheck();
|
||||
await expect(this.allowCustomConnectionsSwitch).not.toBeChecked();
|
||||
await this.saveButton.click();
|
||||
}
|
||||
|
||||
async disallowSharedConnections() {
|
||||
await expect(this.allowSharedConnectionsSwitch).toBeChecked();
|
||||
await expect(this.disableConnectionsSwitch).toBeChecked();
|
||||
await this.allowSharedConnectionsSwitch.uncheck();
|
||||
await expect(this.allowSharedConnectionsSwitch).not.toBeChecked();
|
||||
await this.saveButton.click();
|
||||
}
|
||||
|
||||
async allowConnections() {
|
||||
await expect(this.disableConnectionsSwitch).toBeChecked();
|
||||
await this.disableConnectionsSwitch.uncheck();
|
||||
await expect(this.disableConnectionsSwitch).not.toBeChecked();
|
||||
await this.saveButton.click();
|
||||
}
|
||||
|
||||
async expectSuccessSnackbarToBeVisible() {
|
||||
const snackbars = await this.successSnackbar.all();
|
||||
for (const snackbar of snackbars) {
|
||||
await expect(await snackbar.getAttribute('data-snackbar-variant')).toBe(
|
||||
'success'
|
||||
);
|
||||
// await snackbar.click();
|
||||
}
|
||||
await expect(this.successSnackbar).toHaveCount(1);
|
||||
await this.successSnackbar.click();
|
||||
await expect(this.successSnackbar).toHaveCount(0);
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,3 @@
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
const { AuthenticatedPage } = require('../authenticated-page');
|
||||
const { RoleConditionsModal } = require('./role-conditions-modal');
|
||||
|
||||
@@ -18,7 +16,6 @@ export class AdminCreateRolePage extends AuthenticatedPage {
|
||||
this.executionRow = page.getByTestId('Execution-permission-row');
|
||||
this.flowRow = page.getByTestId('Flow-permission-row');
|
||||
this.pageTitle = page.getByTestId('create-role-title');
|
||||
this.permissionsCatalog = page.getByTestId('permissions-catalog');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,8 +104,4 @@ export class AdminCreateRolePage extends AuthenticatedPage {
|
||||
throw new Error(`${subject} does not have action ${action}`);
|
||||
}
|
||||
}
|
||||
|
||||
async waitForPermissionsCatalogToVisible() {
|
||||
await expect(this.permissionsCatalog).toBeVisible();
|
||||
}
|
||||
}
|
||||
|
@@ -14,12 +14,8 @@ export class AdminCreateUserPage extends AuthenticatedPage {
|
||||
this.roleInput = page.getByTestId('role.id-autocomplete');
|
||||
this.createButton = page.getByTestId('create-button');
|
||||
this.pageTitle = page.getByTestId('create-user-title');
|
||||
this.invitationEmailInfoAlert = page.getByTestId(
|
||||
'invitation-email-info-alert'
|
||||
);
|
||||
this.acceptInvitationLink = page
|
||||
.getByTestId('invitation-email-info-alert')
|
||||
.getByRole('link');
|
||||
this.invitationEmailInfoAlert = page.getByTestId('invitation-email-info-alert');
|
||||
this.acceptInvitationLink = page.getByTestId('invitation-email-info-alert').getByRole('link');
|
||||
}
|
||||
|
||||
seed(seed) {
|
||||
|
@@ -95,6 +95,7 @@ export class AdminUsersPage extends AuthenticatedPage {
|
||||
});
|
||||
}
|
||||
const rowLocator = await this.getUserRowByEmail(email);
|
||||
console.log('rowLocator.count', email, await rowLocator.count());
|
||||
if ((await rowLocator.count()) === 1) {
|
||||
return rowLocator;
|
||||
}
|
||||
|
@@ -51,20 +51,10 @@ 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
|
||||
*/
|
||||
async closeAllSnackbars() {
|
||||
async closeSnackbar() {
|
||||
const snackbars = await this.snackbar.all();
|
||||
for (const snackbar of snackbars) {
|
||||
await snackbar.click();
|
||||
|
@@ -1,16 +0,0 @@
|
||||
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();
|
||||
};
|
@@ -1,69 +0,0 @@
|
||||
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);
|
||||
};
|
@@ -1,24 +0,0 @@
|
||||
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);
|
||||
};
|
@@ -1,5 +1,3 @@
|
||||
import { knexSnakeCaseMappers } from 'objection';
|
||||
|
||||
const fileExtension = 'js';
|
||||
|
||||
const knexConfig = {
|
||||
@@ -9,7 +7,7 @@ const knexConfig = {
|
||||
user: process.env.POSTGRES_USERNAME,
|
||||
port: process.env.POSTGRES_PORT,
|
||||
password: process.env.POSTGRES_PASSWORD,
|
||||
database: process.env.POSTGRES_DATABASE,
|
||||
database: process.env.POSTGRES_DATABASE
|
||||
},
|
||||
searchPath: ['public'],
|
||||
pool: { min: 0, max: 20 },
|
||||
|
@@ -26,16 +26,13 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^8.2.0",
|
||||
"@playwright/test": "1.49.0",
|
||||
"objection": "^3.1.5"
|
||||
"@playwright/test": "^1.45.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.6.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint": "^8.13.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"knex": "^2.4.0",
|
||||
"luxon": "^3.4.4",
|
||||
"micro": "^10.0.1",
|
||||
"pg": "^8.12.0",
|
||||
|
@@ -15,9 +15,9 @@ module.exports = defineConfig({
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 1 : 0,
|
||||
retries: 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: undefined,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Timeout threshold for each test */
|
||||
timeout: 30000,
|
||||
/* 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',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
trace: 'retain-on-failure',
|
||||
testIdAttribute: 'data-test',
|
||||
viewport: { width: 1280, height: 720 },
|
||||
},
|
||||
|
@@ -5,18 +5,16 @@ test.describe('Admin Applications', () => {
|
||||
test.beforeAll(async () => {
|
||||
const deleteAppAuthClients = {
|
||||
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 = {
|
||||
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 {
|
||||
const deleteAppAuthClientsResult = await pgPool.query(
|
||||
deleteAppAuthClients
|
||||
);
|
||||
const deleteAppAuthClientsResult = await pgPool.query(deleteAppAuthClients);
|
||||
expect(deleteAppAuthClientsResult.command).toBe('DELETE');
|
||||
const deleteAppConfigsResult = await pgPool.query(deleteAppConfigs);
|
||||
expect(deleteAppConfigsResult.command).toBe('DELETE');
|
||||
@@ -30,11 +28,10 @@ test.describe('Admin Applications', () => {
|
||||
await adminApplicationsPage.navigateTo();
|
||||
});
|
||||
|
||||
// TODO skip until https://github.com/automatisch/automatisch/pull/2244
|
||||
test.skip('Admin should be able to toggle Application settings', async ({
|
||||
test('Admin should be able to toggle Application settings', async ({
|
||||
adminApplicationsPage,
|
||||
adminApplicationSettingsPage,
|
||||
page,
|
||||
page
|
||||
}) => {
|
||||
await adminApplicationsPage.openApplication('Carbone');
|
||||
await expect(page.url()).toContain('/admin-settings/apps/carbone/settings');
|
||||
@@ -60,7 +57,7 @@ test.describe('Admin Applications', () => {
|
||||
adminApplicationsPage,
|
||||
adminApplicationSettingsPage,
|
||||
flowEditorPage,
|
||||
page,
|
||||
page
|
||||
}) => {
|
||||
await adminApplicationsPage.openApplication('Spotify');
|
||||
await expect(page.url()).toContain('/admin-settings/apps/spotify/settings');
|
||||
@@ -78,15 +75,11 @@ test.describe('Admin Applications', () => {
|
||||
const triggerStep = flowEditorPage.flowStep.last();
|
||||
await triggerStep.click();
|
||||
|
||||
await flowEditorPage.chooseAppAndEvent('Spotify', 'Create Playlist');
|
||||
await flowEditorPage.chooseAppAndEvent("Spotify", "Create Playlist");
|
||||
await flowEditorPage.connectionAutocomplete.click();
|
||||
|
||||
const newConnectionOption = page
|
||||
.getByRole('option')
|
||||
.filter({ hasText: 'Add new connection' });
|
||||
const newSharedConnectionOption = page
|
||||
.getByRole('option')
|
||||
.filter({ hasText: 'Add new shared connection' });
|
||||
const newConnectionOption = page.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).toHaveCount(1);
|
||||
@@ -98,7 +91,7 @@ test.describe('Admin Applications', () => {
|
||||
adminApplicationSettingsPage,
|
||||
adminApplicationAuthClientsPage,
|
||||
flowEditorPage,
|
||||
page,
|
||||
page
|
||||
}) => {
|
||||
await adminApplicationsPage.openApplication('Reddit');
|
||||
await expect(page.url()).toContain('/admin-settings/apps/reddit/settings');
|
||||
@@ -108,21 +101,13 @@ test.describe('Admin Applications', () => {
|
||||
|
||||
await adminApplicationAuthClientsPage.openAuthClientsTab();
|
||||
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.locator('[name="name"]'))
|
||||
.fill('redditAuthClient');
|
||||
await authClientForm
|
||||
.locator(page.locator('[name="clientId"]'))
|
||||
.fill('redditClientId');
|
||||
await authClientForm
|
||||
.locator(page.locator('[name="clientSecret"]'))
|
||||
.fill('redditClientSecret');
|
||||
await authClientForm.locator(page.locator('[name="name"]')).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.authClientShouldBeVisible(
|
||||
'redditAuthClient'
|
||||
);
|
||||
await adminApplicationAuthClientsPage.authClientShouldBeVisible('redditAuthClient');
|
||||
|
||||
await page.goto('/');
|
||||
await page.getByTestId('create-flow-button').click();
|
||||
@@ -134,15 +119,11 @@ test.describe('Admin Applications', () => {
|
||||
const triggerStep = flowEditorPage.flowStep.last();
|
||||
await triggerStep.click();
|
||||
|
||||
await flowEditorPage.chooseAppAndEvent('Reddit', 'Create link post');
|
||||
await flowEditorPage.chooseAppAndEvent("Reddit", "Create link post");
|
||||
await flowEditorPage.connectionAutocomplete.click();
|
||||
|
||||
const newConnectionOption = page
|
||||
.getByRole('option')
|
||||
.filter({ hasText: 'Add new connection' });
|
||||
const newSharedConnectionOption = page
|
||||
.getByRole('option')
|
||||
.filter({ hasText: 'Add new shared connection' });
|
||||
const newConnectionOption = page.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(newSharedConnectionOption).toBeEnabled();
|
||||
@@ -153,7 +134,7 @@ test.describe('Admin Applications', () => {
|
||||
adminApplicationsPage,
|
||||
adminApplicationSettingsPage,
|
||||
flowEditorPage,
|
||||
page,
|
||||
page
|
||||
}) => {
|
||||
await adminApplicationsPage.openApplication('DeepL');
|
||||
await expect(page.url()).toContain('/admin-settings/apps/deepl/settings');
|
||||
@@ -171,18 +152,12 @@ test.describe('Admin Applications', () => {
|
||||
const triggerStep = flowEditorPage.flowStep.last();
|
||||
await triggerStep.click();
|
||||
|
||||
await flowEditorPage.chooseAppAndEvent('DeepL', 'Translate text');
|
||||
await flowEditorPage.chooseAppAndEvent("DeepL", "Translate text");
|
||||
await flowEditorPage.connectionAutocomplete.click();
|
||||
|
||||
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' });
|
||||
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(noConnectionsOption).toHaveCount(1);
|
||||
await expect(newConnectionOption).toHaveCount(0);
|
||||
@@ -193,11 +168,11 @@ test.describe('Admin Applications', () => {
|
||||
adminApplicationsPage,
|
||||
adminApplicationSettingsPage,
|
||||
flowEditorPage,
|
||||
page,
|
||||
page
|
||||
}) => {
|
||||
const queryUser = {
|
||||
text: 'SELECT * FROM users WHERE email = $1',
|
||||
values: [process.env.LOGIN_EMAIL],
|
||||
values: [process.env.LOGIN_EMAIL]
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -208,16 +183,14 @@ test.describe('Admin Applications', () => {
|
||||
text: 'INSERT INTO connections (key, data, user_id, verified, draft) VALUES ($1, $2, $3, $4, $5)',
|
||||
values: [
|
||||
'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,
|
||||
'true',
|
||||
'false',
|
||||
'false'
|
||||
],
|
||||
};
|
||||
|
||||
const createMailchimpConnectionResult = await pgPool.query(
|
||||
createMailchimpConnection
|
||||
);
|
||||
const createMailchimpConnectionResult = await pgPool.query(createMailchimpConnection);
|
||||
expect(createMailchimpConnectionResult.rowCount).toBe(1);
|
||||
expect(createMailchimpConnectionResult.command).toBe('INSERT');
|
||||
} catch (err) {
|
||||
@@ -226,9 +199,7 @@ test.describe('Admin Applications', () => {
|
||||
}
|
||||
|
||||
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.expectSuccessSnackbarToBeVisible();
|
||||
@@ -243,22 +214,14 @@ test.describe('Admin Applications', () => {
|
||||
const triggerStep = flowEditorPage.flowStep.last();
|
||||
await triggerStep.click();
|
||||
|
||||
await flowEditorPage.chooseAppAndEvent('Mailchimp', 'Create campaign');
|
||||
await flowEditorPage.chooseAppAndEvent("Mailchimp", "Create campaign");
|
||||
await flowEditorPage.connectionAutocomplete.click();
|
||||
await expect(page.getByRole('option').first()).toHaveText('Unnamed');
|
||||
|
||||
const existingConnection = page
|
||||
.getByRole('option')
|
||||
.filter({ hasText: 'Unnamed' });
|
||||
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' });
|
||||
const existingConnection = page.getByRole('option').filter({ hasText: 'Unnamed' });
|
||||
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(noConnectionsOption).toHaveCount(0);
|
||||
|
@@ -22,18 +22,22 @@ test.describe('Role management page', () => {
|
||||
await adminRolesPage.navigateTo();
|
||||
await adminRolesPage.createRoleButton.click();
|
||||
await adminCreateRolePage.isMounted();
|
||||
await adminCreateRolePage.waitForPermissionsCatalogToVisible();
|
||||
await adminCreateRolePage.nameInput.fill('Create Edit Test');
|
||||
await adminCreateRolePage.descriptionInput.fill('Test description');
|
||||
await adminCreateRolePage.createButton.click();
|
||||
await adminCreateRolePage.snackbar.waitFor({
|
||||
state: 'attached',
|
||||
});
|
||||
const snackbar = await adminCreateRolePage.getSnackbarData(
|
||||
'snackbar-create-role-success'
|
||||
);
|
||||
await expect(snackbar.variant).toBe('success');
|
||||
await adminCreateRolePage.closeSnackbar();
|
||||
});
|
||||
|
||||
let roleRow =
|
||||
await test.step('Make sure role data is correct', async () => {
|
||||
let roleRow = await test.step(
|
||||
'Make sure role data is correct',
|
||||
async () => {
|
||||
const roleRow = await adminRolesPage.getRoleRowByName(
|
||||
'Create Edit Test'
|
||||
);
|
||||
@@ -44,7 +48,8 @@ test.describe('Role management page', () => {
|
||||
await expect(roleData.canEdit).toBe(true);
|
||||
await expect(roleData.canDelete).toBe(true);
|
||||
return roleRow;
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
await test.step('Edit the role', async () => {
|
||||
await adminRolesPage.clickEditRole(roleRow);
|
||||
@@ -52,14 +57,19 @@ test.describe('Role management page', () => {
|
||||
await adminEditRolePage.nameInput.fill('Create Update Test');
|
||||
await adminEditRolePage.descriptionInput.fill('Update test description');
|
||||
await adminEditRolePage.updateButton.click();
|
||||
await adminEditRolePage.snackbar.waitFor({
|
||||
state: 'attached',
|
||||
});
|
||||
const snackbar = await adminEditRolePage.getSnackbarData(
|
||||
'snackbar-edit-role-success'
|
||||
);
|
||||
await expect(snackbar.variant).toBe('success');
|
||||
await adminEditRolePage.closeSnackbar();
|
||||
});
|
||||
|
||||
roleRow =
|
||||
await test.step('Make sure changes reflected on roles page', async () => {
|
||||
roleRow = await test.step(
|
||||
'Make sure changes reflected on roles page',
|
||||
async () => {
|
||||
await adminRolesPage.isMounted();
|
||||
const roleRow = await adminRolesPage.getRoleRowByName(
|
||||
'Create Update Test'
|
||||
@@ -71,7 +81,8 @@ test.describe('Role management page', () => {
|
||||
await expect(roleData.canEdit).toBe(true);
|
||||
await expect(roleData.canDelete).toBe(true);
|
||||
return roleRow;
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
await test.step('Delete the role', async () => {
|
||||
await adminRolesPage.clickDeleteRole(roleRow);
|
||||
@@ -80,10 +91,14 @@ test.describe('Role management page', () => {
|
||||
state: 'attached',
|
||||
});
|
||||
await deleteModal.deleteButton.click();
|
||||
await adminRolesPage.snackbar.waitFor({
|
||||
state: 'attached',
|
||||
});
|
||||
const snackbar = await adminRolesPage.getSnackbarData(
|
||||
'snackbar-delete-role-success'
|
||||
);
|
||||
await expect(snackbar.variant).toBe('success');
|
||||
await adminRolesPage.closeSnackbar();
|
||||
await deleteModal.modal.waitFor({
|
||||
state: 'detached',
|
||||
});
|
||||
@@ -158,45 +173,60 @@ test.describe('Role management page', () => {
|
||||
await test.step('Create a new role', async () => {
|
||||
await adminRolesPage.createRoleButton.click();
|
||||
await adminCreateRolePage.isMounted();
|
||||
await adminCreateRolePage.waitForPermissionsCatalogToVisible();
|
||||
await adminCreateRolePage.nameInput.fill('Delete Role');
|
||||
await adminCreateRolePage.createButton.click();
|
||||
await adminCreateRolePage.snackbar.waitFor({
|
||||
state: 'attached',
|
||||
});
|
||||
const snackbar = await adminCreateRolePage.getSnackbarData(
|
||||
'snackbar-create-role-success'
|
||||
);
|
||||
await expect(snackbar.variant).toBe('success');
|
||||
await adminCreateRolePage.closeSnackbar();
|
||||
});
|
||||
await test.step('Create a new user with the "Delete Role" role', async () => {
|
||||
await adminUsersPage.navigateTo();
|
||||
await adminUsersPage.createUserButton.click();
|
||||
await adminCreateUserPage.fullNameInput.fill('User Role Test');
|
||||
await adminCreateUserPage.emailInput.fill(
|
||||
'user-role-test@automatisch.io'
|
||||
);
|
||||
await adminCreateUserPage.roleInput.click();
|
||||
await adminCreateUserPage.page
|
||||
.getByRole('option', { name: 'Delete Role', exact: true })
|
||||
.click();
|
||||
await adminCreateUserPage.createButton.click();
|
||||
await adminCreateUserPage.invitationEmailInfoAlert.waitFor({
|
||||
state: 'attached',
|
||||
});
|
||||
const snackbar = await adminUsersPage.getSnackbarData(
|
||||
'snackbar-create-user-success'
|
||||
);
|
||||
await expect(snackbar.variant).toBe('success');
|
||||
});
|
||||
await test.step('Try to delete "Delete Role" role when new user has it', async () => {
|
||||
await adminRolesPage.navigateTo();
|
||||
const row = await adminRolesPage.getRoleRowByName('Delete Role');
|
||||
const modal = await adminRolesPage.clickDeleteRole(row);
|
||||
await modal.deleteButton.click();
|
||||
const snackbar = await adminRolesPage.getSnackbarData(
|
||||
'snackbar-delete-role-error'
|
||||
);
|
||||
await expect(snackbar.variant).toBe('error');
|
||||
await modal.close();
|
||||
});
|
||||
await test.step(
|
||||
'Create a new user with the "Delete Role" role',
|
||||
async () => {
|
||||
await adminUsersPage.navigateTo();
|
||||
await adminUsersPage.createUserButton.click();
|
||||
await adminCreateUserPage.fullNameInput.fill('User Role Test');
|
||||
await adminCreateUserPage.emailInput.fill(
|
||||
'user-role-test@automatisch.io'
|
||||
);
|
||||
await adminCreateUserPage.roleInput.click();
|
||||
await adminCreateUserPage.page
|
||||
.getByRole('option', { name: 'Delete Role', exact: true })
|
||||
.click();
|
||||
await adminCreateUserPage.createButton.click();
|
||||
await adminCreateUserPage.snackbar.waitFor({
|
||||
state: 'attached',
|
||||
});
|
||||
await adminCreateUserPage.invitationEmailInfoAlert.waitFor({
|
||||
state: 'attached',
|
||||
});
|
||||
const snackbar = await adminUsersPage.getSnackbarData(
|
||||
'snackbar-create-user-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 adminRolesPage.navigateTo();
|
||||
const row = await adminRolesPage.getRoleRowByName('Delete Role');
|
||||
const modal = await adminRolesPage.clickDeleteRole(row);
|
||||
await modal.deleteButton.click();
|
||||
await adminRolesPage.snackbar.waitFor({
|
||||
state: 'attached',
|
||||
});
|
||||
const snackbar = await adminRolesPage.getSnackbarData('snackbar-delete-role-error');
|
||||
await expect(snackbar.variant).toBe('error');
|
||||
await adminRolesPage.closeSnackbar();
|
||||
await modal.close();
|
||||
}
|
||||
);
|
||||
await test.step('Change the role the user has', async () => {
|
||||
await adminUsersPage.navigateTo();
|
||||
await adminUsersPage.usersLoader.waitFor({
|
||||
@@ -211,10 +241,14 @@ test.describe('Role management page', () => {
|
||||
.getByRole('option', { name: 'Admin' })
|
||||
.click();
|
||||
await adminEditUserPage.updateButton.click();
|
||||
await adminEditUserPage.snackbar.waitFor({
|
||||
state: 'attached',
|
||||
});
|
||||
const snackbar = await adminEditUserPage.getSnackbarData(
|
||||
'snackbar-edit-user-success'
|
||||
);
|
||||
await expect(snackbar.variant).toBe('success');
|
||||
await adminEditUserPage.closeSnackbar();
|
||||
});
|
||||
await test.step('Delete the original role', async () => {
|
||||
await adminRolesPage.navigateTo();
|
||||
@@ -222,10 +256,14 @@ test.describe('Role management page', () => {
|
||||
const modal = await adminRolesPage.clickDeleteRole(row);
|
||||
await expect(modal.modal).toBeVisible();
|
||||
await modal.deleteButton.click();
|
||||
await adminRolesPage.snackbar.waitFor({
|
||||
state: 'attached',
|
||||
});
|
||||
const snackbar = await adminRolesPage.getSnackbarData(
|
||||
'snackbar-delete-role-success'
|
||||
);
|
||||
await expect(snackbar.variant).toBe('success');
|
||||
await adminRolesPage.closeSnackbar();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -239,13 +277,16 @@ test.describe('Role management page', () => {
|
||||
await test.step('Create a new role', async () => {
|
||||
await adminRolesPage.createRoleButton.click();
|
||||
await adminCreateRolePage.isMounted();
|
||||
await adminCreateRolePage.waitForPermissionsCatalogToVisible();
|
||||
await adminCreateRolePage.nameInput.fill('Cannot Delete Role');
|
||||
await adminCreateRolePage.createButton.click();
|
||||
await adminCreateRolePage.snackbar.waitFor({
|
||||
state: 'attached',
|
||||
});
|
||||
const snackbar = await adminCreateRolePage.getSnackbarData(
|
||||
'snackbar-create-role-success'
|
||||
);
|
||||
await expect(snackbar.variant).toBe('success');
|
||||
await adminCreateRolePage.closeSnackbar();
|
||||
});
|
||||
await test.step('Create a new user with this role', async () => {
|
||||
await adminUsersPage.navigateTo();
|
||||
@@ -260,6 +301,9 @@ test.describe('Role management page', () => {
|
||||
.getByRole('option', { name: 'Cannot Delete Role' })
|
||||
.click();
|
||||
await adminCreateUserPage.createButton.click();
|
||||
await adminCreateUserPage.snackbar.waitFor({
|
||||
state: 'attached',
|
||||
});
|
||||
await adminCreateUserPage.invitationEmailInfoAlert.waitFor({
|
||||
state: 'attached',
|
||||
});
|
||||
@@ -267,34 +311,40 @@ test.describe('Role management page', () => {
|
||||
'snackbar-create-user-success'
|
||||
);
|
||||
await expect(snackbar.variant).toBe('success');
|
||||
await adminCreateUserPage.closeSnackbar();
|
||||
});
|
||||
await test.step('Delete this user', async () => {
|
||||
await adminUsersPage.navigateTo();
|
||||
const row = await adminUsersPage.findUserPageWithEmail(
|
||||
'user-delete-role-test@automatisch.io'
|
||||
);
|
||||
// await test.waitForTimeout(10000);
|
||||
const modal = await adminUsersPage.clickDeleteUser(row);
|
||||
await modal.deleteButton.click();
|
||||
await adminUsersPage.snackbar.waitFor({
|
||||
state: 'attached',
|
||||
});
|
||||
const snackbar = await adminUsersPage.getSnackbarData(
|
||||
'snackbar-delete-user-success'
|
||||
);
|
||||
await expect(snackbar.variant).toBe('success');
|
||||
await adminUsersPage.closeSnackbar();
|
||||
});
|
||||
await test.step('Try deleting this role', async () => {
|
||||
await adminRolesPage.navigateTo();
|
||||
const row = await adminRolesPage.getRoleRowByName('Cannot Delete Role');
|
||||
const modal = await adminRolesPage.clickDeleteRole(row);
|
||||
await modal.deleteButton.click();
|
||||
const snackbar = await adminRolesPage.getSnackbarData(
|
||||
'snackbar-delete-role-error'
|
||||
);
|
||||
await expect(snackbar.variant).toBe('error');
|
||||
await adminRolesPage.snackbar.waitFor({
|
||||
state: 'attached',
|
||||
});
|
||||
/*
|
||||
* TODO: await snackbar - make assertions based on product
|
||||
* decisions
|
||||
const snackbar = await adminRolesPage.getSnackbarData();
|
||||
await expect(snackbar.variant).toBe('...');
|
||||
*/
|
||||
await adminRolesPage.closeSnackbar();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -312,13 +362,16 @@ test('Accessibility of role management page', async ({
|
||||
await adminRolesPage.navigateTo();
|
||||
await adminRolesPage.createRoleButton.click();
|
||||
await adminCreateRolePage.isMounted();
|
||||
await adminCreateRolePage.waitForPermissionsCatalogToVisible();
|
||||
await adminCreateRolePage.nameInput.fill('Basic Test');
|
||||
await adminCreateRolePage.createButton.click();
|
||||
await adminCreateRolePage.snackbar.waitFor({
|
||||
state: 'attached',
|
||||
});
|
||||
const snackbar = await adminCreateRolePage.getSnackbarData(
|
||||
'snackbar-create-role-success'
|
||||
);
|
||||
await expect(snackbar.variant).toBe('success');
|
||||
await adminCreateRolePage.closeSnackbar();
|
||||
});
|
||||
|
||||
await test.step('Create a new user with the basic role', async () => {
|
||||
@@ -332,6 +385,9 @@ test('Accessibility of role management page', async ({
|
||||
.getByRole('option', { name: 'Basic Test' })
|
||||
.click();
|
||||
await adminCreateUserPage.createButton.click();
|
||||
await adminCreateUserPage.snackbar.waitFor({
|
||||
state: 'attached',
|
||||
});
|
||||
await adminCreateUserPage.invitationEmailInfoAlert.waitFor({
|
||||
state: 'attached',
|
||||
});
|
||||
@@ -339,46 +395,56 @@ test('Accessibility of role management page', async ({
|
||||
'snackbar-create-user-success'
|
||||
);
|
||||
await expect(snackbar.variant).toBe('success');
|
||||
await adminCreateUserPage.closeSnackbar();
|
||||
});
|
||||
|
||||
await test.step('Logout and login to the basic role user', async () => {
|
||||
const acceptInvitationLink = await adminCreateUserPage.acceptInvitationLink;
|
||||
console.log(acceptInvitationLink);
|
||||
const acceptInvitationUrl = await acceptInvitationLink.textContent();
|
||||
console.log(acceptInvitationUrl);
|
||||
const acceptInvitatonToken = acceptInvitationUrl.split('?token=')[1];
|
||||
|
||||
await page.getByTestId('profile-menu-button').click();
|
||||
await page.getByTestId('logout-item').click();
|
||||
|
||||
const acceptInvitationPage = new AcceptInvitation(page);
|
||||
|
||||
await acceptInvitationPage.open(acceptInvitatonToken);
|
||||
|
||||
await acceptInvitationPage.acceptInvitation('sample');
|
||||
|
||||
const loginPage = new LoginPage(page);
|
||||
|
||||
// await loginPage.isMounted();
|
||||
await loginPage.login('basic-role-test@automatisch.io', 'sample');
|
||||
await expect(loginPage.loginButton).not.toBeVisible();
|
||||
await expect(page).toHaveURL('/flows');
|
||||
});
|
||||
|
||||
await test.step('Navigate to the admin settings page and make sure it is blank', async () => {
|
||||
const pageUrl = new URL(page.url());
|
||||
const url = `${pageUrl.origin}/admin-settings/users`;
|
||||
await page.goto(url);
|
||||
await page.waitForTimeout(750);
|
||||
const isUnmounted = await page.evaluate(() => {
|
||||
// eslint-disable-next-line no-undef
|
||||
const root = document.querySelector('#root');
|
||||
await test.step(
|
||||
'Navigate to the admin settings page and make sure it is blank',
|
||||
async () => {
|
||||
const pageUrl = new URL(page.url());
|
||||
const url = `${pageUrl.origin}/admin-settings/users`;
|
||||
await page.goto(url);
|
||||
await page.waitForTimeout(750);
|
||||
const isUnmounted = await page.evaluate(() => {
|
||||
// eslint-disable-next-line no-undef
|
||||
const root = document.querySelector('#root');
|
||||
|
||||
if (root) {
|
||||
// We have react query devtools only in dev env.
|
||||
// In production, there is nothing in root.
|
||||
// That's why `<= 1`.
|
||||
return root.children.length <= 1;
|
||||
}
|
||||
if (root) {
|
||||
// We have react query devtools only in dev env.
|
||||
// In production, there is nothing in root.
|
||||
// That's why `<= 1`.
|
||||
return root.children.length <= 1;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
await expect(isUnmounted).toBe(true);
|
||||
});
|
||||
return false;
|
||||
});
|
||||
await expect(isUnmounted).toBe(true);
|
||||
}
|
||||
);
|
||||
|
||||
await test.step('Log back into the admin account', async () => {
|
||||
await page.goto('/');
|
||||
@@ -399,10 +465,10 @@ test('Accessibility of role management page', async ({
|
||||
await adminEditUserPage.roleInput.click();
|
||||
await adminEditUserPage.page.getByRole('option', { name: 'Admin' }).click();
|
||||
await adminEditUserPage.updateButton.click();
|
||||
const snackbar = await adminEditUserPage.getSnackbarData(
|
||||
'snackbar-edit-user-success'
|
||||
);
|
||||
await expect(snackbar.variant).toBe('success');
|
||||
await adminEditUserPage.snackbar.waitFor({
|
||||
state: 'attached',
|
||||
});
|
||||
await adminEditUserPage.closeSnackbar();
|
||||
});
|
||||
|
||||
await test.step('Delete the role', async () => {
|
||||
@@ -414,10 +480,14 @@ test('Accessibility of role management page', async ({
|
||||
state: 'attached',
|
||||
});
|
||||
await deleteModal.deleteButton.click();
|
||||
await adminRolesPage.snackbar.waitFor({
|
||||
state: 'attached',
|
||||
});
|
||||
const snackbar = await adminRolesPage.getSnackbarData(
|
||||
'snackbar-delete-role-success'
|
||||
);
|
||||
await expect(snackbar.variant).toBe('success');
|
||||
await adminRolesPage.closeSnackbar();
|
||||
await deleteModal.modal.waitFor({
|
||||
state: 'detached',
|
||||
});
|
||||
|
@@ -5,235 +5,281 @@ const { test, expect } = require('../../fixtures/index');
|
||||
* otherwise tests will fail since users are only *soft*-deleted
|
||||
*/
|
||||
test.describe('User management page', () => {
|
||||
|
||||
test.beforeEach(async ({ adminUsersPage }) => {
|
||||
await adminUsersPage.navigateTo();
|
||||
await adminUsersPage.closeAllSnackbars();
|
||||
await adminUsersPage.closeSnackbar();
|
||||
});
|
||||
|
||||
test('User creation and deletion process', async ({
|
||||
adminCreateUserPage,
|
||||
adminEditUserPage,
|
||||
adminUsersPage,
|
||||
}) => {
|
||||
adminCreateUserPage.seed(9000);
|
||||
const user = adminCreateUserPage.generateUser();
|
||||
await adminUsersPage.usersLoader.waitFor({
|
||||
state: 'detached' /* Note: state: 'visible' introduces flakiness
|
||||
test(
|
||||
'User creation and deletion process',
|
||||
async ({ adminCreateUserPage, adminEditUserPage, adminUsersPage }) => {
|
||||
adminCreateUserPage.seed(9000);
|
||||
const user = adminCreateUserPage.generateUser();
|
||||
await adminUsersPage.usersLoader.waitFor({
|
||||
state: 'detached' /* Note: state: 'visible' introduces flakiness
|
||||
because visibility: hidden is used as part of the state transition in
|
||||
notistack, see
|
||||
https://github.com/iamhosseindhv/notistack/blob/122f47057eb7ce5a1abfe923316cf8475303e99a/src/transitions/Collapse/Collapse.tsx#L110
|
||||
*/,
|
||||
});
|
||||
await test.step('Create a user', async () => {
|
||||
await adminUsersPage.createUserButton.click();
|
||||
await adminCreateUserPage.fullNameInput.fill(user.fullName);
|
||||
await adminCreateUserPage.emailInput.fill(user.email);
|
||||
await adminCreateUserPage.roleInput.click();
|
||||
await adminCreateUserPage.page
|
||||
.getByRole('option', { name: 'Admin' })
|
||||
.click();
|
||||
await adminCreateUserPage.createButton.click();
|
||||
await adminCreateUserPage.invitationEmailInfoAlert.waitFor({
|
||||
state: 'attached',
|
||||
*/
|
||||
});
|
||||
await test.step(
|
||||
'Create a user',
|
||||
async () => {
|
||||
await adminUsersPage.createUserButton.click();
|
||||
await adminCreateUserPage.fullNameInput.fill(user.fullName);
|
||||
await adminCreateUserPage.emailInput.fill(user.email);
|
||||
await adminCreateUserPage.roleInput.click();
|
||||
await adminCreateUserPage.page.getByRole(
|
||||
'option', { name: 'Admin' }
|
||||
).click();
|
||||
await adminCreateUserPage.createButton.click();
|
||||
await adminCreateUserPage.invitationEmailInfoAlert.waitFor({
|
||||
state: 'attached'
|
||||
});
|
||||
|
||||
const snackbar = await adminUsersPage.getSnackbarData(
|
||||
'snackbar-create-user-success'
|
||||
const snackbar = await adminUsersPage.getSnackbarData(
|
||||
'snackbar-create-user-success'
|
||||
);
|
||||
await expect(snackbar.variant).toBe('success');
|
||||
await adminUsersPage.navigateTo();
|
||||
await adminUsersPage.closeSnackbar();
|
||||
}
|
||||
);
|
||||
await expect(snackbar.variant).toBe('success');
|
||||
await adminUsersPage.navigateTo();
|
||||
});
|
||||
await test.step('Check the user exists with the expected properties', async () => {
|
||||
await adminUsersPage.findUserPageWithEmail(user.email);
|
||||
const userRow = await adminUsersPage.getUserRowByEmail(user.email);
|
||||
const data = await adminUsersPage.getRowData(userRow);
|
||||
await expect(data.email).toBe(user.email);
|
||||
await expect(data.fullName).toBe(user.fullName);
|
||||
await expect(data.role).toBe('Admin');
|
||||
});
|
||||
await test.step('Edit user info and make sure the edit works correctly', async () => {
|
||||
await adminUsersPage.findUserPageWithEmail(user.email);
|
||||
|
||||
let userRow = await adminUsersPage.getUserRowByEmail(user.email);
|
||||
await adminUsersPage.clickEditUser(userRow);
|
||||
await adminEditUserPage.waitForLoad(user.fullName);
|
||||
const newUserInfo = adminEditUserPage.generateUser();
|
||||
await adminEditUserPage.fullNameInput.fill(newUserInfo.fullName);
|
||||
await adminEditUserPage.updateButton.click();
|
||||
|
||||
const snackbar = await adminUsersPage.getSnackbarData(
|
||||
'snackbar-edit-user-success'
|
||||
await test.step(
|
||||
'Check the user exists with the expected properties',
|
||||
async () => {
|
||||
await adminUsersPage.findUserPageWithEmail(user.email);
|
||||
const userRow = await adminUsersPage.getUserRowByEmail(user.email);
|
||||
const data = await adminUsersPage.getRowData(userRow);
|
||||
await expect(data.email).toBe(user.email);
|
||||
await expect(data.fullName).toBe(user.fullName);
|
||||
await expect(data.role).toBe('Admin');
|
||||
}
|
||||
);
|
||||
await expect(snackbar.variant).toBe('success');
|
||||
await test.step(
|
||||
'Edit user info and make sure the edit works correctly',
|
||||
async () => {
|
||||
await adminUsersPage.findUserPageWithEmail(user.email);
|
||||
|
||||
await adminUsersPage.findUserPageWithEmail(user.email);
|
||||
userRow = await adminUsersPage.getUserRowByEmail(user.email);
|
||||
const rowData = await adminUsersPage.getRowData(userRow);
|
||||
await expect(rowData.fullName).toBe(newUserInfo.fullName);
|
||||
});
|
||||
await test.step('Delete user and check the page confirms this deletion', async () => {
|
||||
await adminUsersPage.findUserPageWithEmail(user.email);
|
||||
const userRow = await adminUsersPage.getUserRowByEmail(user.email);
|
||||
await adminUsersPage.clickDeleteUser(userRow);
|
||||
const modal = adminUsersPage.deleteUserModal;
|
||||
await modal.deleteButton.click();
|
||||
let userRow = await adminUsersPage.getUserRowByEmail(user.email);
|
||||
await adminUsersPage.clickEditUser(userRow);
|
||||
await adminEditUserPage.waitForLoad(user.fullName);
|
||||
const newUserInfo = adminEditUserPage.generateUser();
|
||||
await adminEditUserPage.fullNameInput.fill(newUserInfo.fullName);
|
||||
await adminEditUserPage.updateButton.click();
|
||||
|
||||
const snackbar = await adminUsersPage.getSnackbarData(
|
||||
'snackbar-delete-user-success'
|
||||
const snackbar = await adminUsersPage.getSnackbarData(
|
||||
'snackbar-edit-user-success'
|
||||
);
|
||||
await expect(snackbar.variant).toBe('success');
|
||||
await adminUsersPage.closeSnackbar();
|
||||
|
||||
await adminUsersPage.findUserPageWithEmail(user.email);
|
||||
userRow = await adminUsersPage.getUserRowByEmail(user.email);
|
||||
const rowData = await adminUsersPage.getRowData(userRow);
|
||||
await expect(rowData.fullName).toBe(newUserInfo.fullName);
|
||||
}
|
||||
);
|
||||
await expect(snackbar.variant).toBe('success');
|
||||
await expect(userRow).not.toBeVisible(false);
|
||||
});
|
||||
});
|
||||
await test.step(
|
||||
'Delete user and check the page confirms this deletion',
|
||||
async () => {
|
||||
await adminUsersPage.findUserPageWithEmail(user.email);
|
||||
const userRow = await adminUsersPage.getUserRowByEmail(user.email);
|
||||
await adminUsersPage.clickDeleteUser(userRow);
|
||||
const modal = adminUsersPage.deleteUserModal;
|
||||
await modal.deleteButton.click();
|
||||
|
||||
test('Creating a user which has been deleted', async ({
|
||||
adminCreateUserPage,
|
||||
adminUsersPage,
|
||||
}) => {
|
||||
adminCreateUserPage.seed(9100);
|
||||
const testUser = adminCreateUserPage.generateUser();
|
||||
|
||||
await test.step('Create the test user', async () => {
|
||||
await adminUsersPage.navigateTo();
|
||||
await adminUsersPage.createUserButton.click();
|
||||
await adminCreateUserPage.fullNameInput.fill(testUser.fullName);
|
||||
await adminCreateUserPage.emailInput.fill(testUser.email);
|
||||
await adminCreateUserPage.roleInput.click();
|
||||
await adminCreateUserPage.page
|
||||
.getByRole('option', { name: 'Admin' })
|
||||
.click();
|
||||
await adminCreateUserPage.createButton.click();
|
||||
const snackbar = await adminUsersPage.getSnackbarData(
|
||||
'snackbar-create-user-success'
|
||||
const snackbar = await adminUsersPage.getSnackbarData(
|
||||
'snackbar-delete-user-success'
|
||||
);
|
||||
await expect(snackbar.variant).toBe('success');
|
||||
await adminUsersPage.closeSnackbar();
|
||||
await expect(userRow).not.toBeVisible(false);
|
||||
}
|
||||
);
|
||||
await expect(snackbar.variant).toBe('success');
|
||||
});
|
||||
|
||||
await test.step('Delete the created user', async () => {
|
||||
await adminUsersPage.navigateTo();
|
||||
await adminUsersPage.findUserPageWithEmail(testUser.email);
|
||||
const userRow = await adminUsersPage.getUserRowByEmail(testUser.email);
|
||||
await adminUsersPage.clickDeleteUser(userRow);
|
||||
const modal = adminUsersPage.deleteUserModal;
|
||||
await modal.deleteButton.click();
|
||||
const snackbar = await adminUsersPage.getSnackbarData(
|
||||
'snackbar-delete-user-success'
|
||||
test(
|
||||
'Creating a user which has been deleted',
|
||||
async ({ adminCreateUserPage, adminUsersPage }) => {
|
||||
adminCreateUserPage.seed(9100);
|
||||
const testUser = adminCreateUserPage.generateUser();
|
||||
|
||||
await test.step(
|
||||
'Create the test user',
|
||||
async () => {
|
||||
await adminUsersPage.navigateTo();
|
||||
await adminUsersPage.createUserButton.click();
|
||||
await adminCreateUserPage.fullNameInput.fill(testUser.fullName);
|
||||
await adminCreateUserPage.emailInput.fill(testUser.email);
|
||||
await adminCreateUserPage.roleInput.click();
|
||||
await adminCreateUserPage.page.getByRole(
|
||||
'option', { name: 'Admin' }
|
||||
).click();
|
||||
await adminCreateUserPage.createButton.click();
|
||||
const snackbar = await adminUsersPage.getSnackbarData(
|
||||
'snackbar-create-user-success'
|
||||
);
|
||||
await expect(snackbar.variant).toBe('success');
|
||||
await adminUsersPage.closeSnackbar();
|
||||
}
|
||||
);
|
||||
await expect(snackbar).not.toBeNull();
|
||||
await expect(snackbar.variant).toBe('success');
|
||||
await expect(userRow).not.toBeVisible(false);
|
||||
});
|
||||
|
||||
await test.step('Create the user again', async () => {
|
||||
await adminUsersPage.createUserButton.click();
|
||||
await adminCreateUserPage.fullNameInput.fill(testUser.fullName);
|
||||
await adminCreateUserPage.emailInput.fill(testUser.email);
|
||||
await adminCreateUserPage.roleInput.click();
|
||||
await adminCreateUserPage.page
|
||||
.getByRole('option', { name: 'Admin' })
|
||||
.click();
|
||||
await adminCreateUserPage.createButton.click();
|
||||
const snackbar = await adminUsersPage.getSnackbarData('snackbar-error');
|
||||
await expect(snackbar.variant).toBe('error');
|
||||
});
|
||||
});
|
||||
|
||||
test('Creating a user which already exists', async ({
|
||||
adminCreateUserPage,
|
||||
adminUsersPage,
|
||||
page,
|
||||
}) => {
|
||||
adminCreateUserPage.seed(9200);
|
||||
const testUser = adminCreateUserPage.generateUser();
|
||||
|
||||
await test.step('Create the test user', async () => {
|
||||
await adminUsersPage.createUserButton.click();
|
||||
await adminCreateUserPage.fullNameInput.fill(testUser.fullName);
|
||||
await adminCreateUserPage.emailInput.fill(testUser.email);
|
||||
await adminCreateUserPage.roleInput.click();
|
||||
await adminCreateUserPage.page
|
||||
.getByRole('option', { name: 'Admin' })
|
||||
.click();
|
||||
await adminCreateUserPage.createButton.click();
|
||||
const snackbar = await adminUsersPage.getSnackbarData(
|
||||
'snackbar-create-user-success'
|
||||
await test.step(
|
||||
'Delete the created user',
|
||||
async () => {
|
||||
await adminUsersPage.navigateTo();
|
||||
await adminUsersPage.findUserPageWithEmail(testUser.email);
|
||||
const userRow = await adminUsersPage.getUserRowByEmail(testUser.email);
|
||||
await adminUsersPage.clickDeleteUser(userRow);
|
||||
const modal = adminUsersPage.deleteUserModal;
|
||||
await modal.deleteButton.click();
|
||||
const snackbar = await adminUsersPage.getSnackbarData(
|
||||
'snackbar-delete-user-success'
|
||||
);
|
||||
await expect(snackbar).not.toBeNull();
|
||||
await expect(snackbar.variant).toBe('success');
|
||||
await adminUsersPage.closeSnackbar();
|
||||
await expect(userRow).not.toBeVisible(false);
|
||||
}
|
||||
);
|
||||
await expect(snackbar.variant).toBe('success');
|
||||
});
|
||||
|
||||
await test.step('Create the user again', async () => {
|
||||
await adminUsersPage.navigateTo();
|
||||
await adminUsersPage.createUserButton.click();
|
||||
await adminCreateUserPage.fullNameInput.fill(testUser.fullName);
|
||||
await adminCreateUserPage.emailInput.fill(testUser.email);
|
||||
const createUserPageUrl = page.url();
|
||||
await adminCreateUserPage.roleInput.click();
|
||||
await adminCreateUserPage.page
|
||||
.getByRole('option', { name: 'Admin' })
|
||||
.click();
|
||||
await adminCreateUserPage.createButton.click();
|
||||
|
||||
await expect(page.url()).toBe(createUserPageUrl);
|
||||
const snackbar = await adminUsersPage.getSnackbarData('snackbar-error');
|
||||
await expect(snackbar.variant).toBe('error');
|
||||
});
|
||||
});
|
||||
|
||||
test('Editing a user to have the same email as another user should not be allowed', async ({
|
||||
adminCreateUserPage,
|
||||
adminEditUserPage,
|
||||
adminUsersPage,
|
||||
page,
|
||||
}) => {
|
||||
adminCreateUserPage.seed(9300);
|
||||
const user1 = adminCreateUserPage.generateUser();
|
||||
const user2 = adminCreateUserPage.generateUser();
|
||||
await test.step('Create the first user', async () => {
|
||||
await adminUsersPage.navigateTo();
|
||||
await adminUsersPage.createUserButton.click();
|
||||
await adminCreateUserPage.fullNameInput.fill(user1.fullName);
|
||||
await adminCreateUserPage.emailInput.fill(user1.email);
|
||||
await adminCreateUserPage.roleInput.click();
|
||||
await adminCreateUserPage.page
|
||||
.getByRole('option', { name: 'Admin' })
|
||||
.click();
|
||||
await adminCreateUserPage.createButton.click();
|
||||
const snackbar = await adminUsersPage.getSnackbarData(
|
||||
'snackbar-create-user-success'
|
||||
await test.step(
|
||||
'Create the user again',
|
||||
async () => {
|
||||
await adminUsersPage.createUserButton.click();
|
||||
await adminCreateUserPage.fullNameInput.fill(testUser.fullName);
|
||||
await adminCreateUserPage.emailInput.fill(testUser.email);
|
||||
await adminCreateUserPage.roleInput.click();
|
||||
await adminCreateUserPage.page.getByRole(
|
||||
'option', { name: 'Admin' }
|
||||
).click();
|
||||
await adminCreateUserPage.createButton.click();
|
||||
const snackbar = await adminUsersPage.getSnackbarData('snackbar-error');
|
||||
await expect(snackbar.variant).toBe('error');
|
||||
await adminUsersPage.closeSnackbar();
|
||||
}
|
||||
);
|
||||
await expect(snackbar.variant).toBe('success');
|
||||
await adminUsersPage.closeAllSnackbars();
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
await test.step('Create the second user', async () => {
|
||||
await adminUsersPage.navigateTo();
|
||||
await adminUsersPage.createUserButton.click();
|
||||
await adminCreateUserPage.fullNameInput.fill(user2.fullName);
|
||||
await adminCreateUserPage.emailInput.fill(user2.email);
|
||||
await adminCreateUserPage.roleInput.click();
|
||||
await adminCreateUserPage.page
|
||||
.getByRole('option', { name: 'Admin' })
|
||||
.click();
|
||||
await adminCreateUserPage.createButton.click();
|
||||
const snackbar = await adminUsersPage.getSnackbarData(
|
||||
'snackbar-create-user-success'
|
||||
test(
|
||||
'Creating a user which already exists',
|
||||
async ({ adminCreateUserPage, adminUsersPage, page }) => {
|
||||
adminCreateUserPage.seed(9200);
|
||||
const testUser = adminCreateUserPage.generateUser();
|
||||
|
||||
await test.step(
|
||||
'Create the test user',
|
||||
async () => {
|
||||
await adminUsersPage.createUserButton.click();
|
||||
await adminCreateUserPage.fullNameInput.fill(testUser.fullName);
|
||||
await adminCreateUserPage.emailInput.fill(testUser.email);
|
||||
await adminCreateUserPage.roleInput.click();
|
||||
await adminCreateUserPage.page.getByRole(
|
||||
'option', { name: 'Admin' }
|
||||
).click();
|
||||
await adminCreateUserPage.createButton.click();
|
||||
const snackbar = await adminUsersPage.getSnackbarData(
|
||||
'snackbar-create-user-success'
|
||||
);
|
||||
await expect(snackbar.variant).toBe('success');
|
||||
await adminUsersPage.closeSnackbar();
|
||||
}
|
||||
);
|
||||
await expect(snackbar.variant).toBe('success');
|
||||
});
|
||||
|
||||
await test.step('Try editing the second user to have the email of the first user', async () => {
|
||||
await adminUsersPage.navigateTo();
|
||||
await adminUsersPage.findUserPageWithEmail(user2.email);
|
||||
let userRow = await adminUsersPage.getUserRowByEmail(user2.email);
|
||||
await adminUsersPage.clickEditUser(userRow);
|
||||
await adminEditUserPage.waitForLoad(user2.fullName);
|
||||
await adminEditUserPage.emailInput.fill(user1.email);
|
||||
const editPageUrl = page.url();
|
||||
await adminEditUserPage.updateButton.click();
|
||||
await test.step(
|
||||
'Create the user again',
|
||||
async () => {
|
||||
await adminUsersPage.navigateTo();
|
||||
await adminUsersPage.createUserButton.click();
|
||||
await adminCreateUserPage.fullNameInput.fill(testUser.fullName);
|
||||
await adminCreateUserPage.emailInput.fill(testUser.email);
|
||||
const createUserPageUrl = page.url();
|
||||
await adminCreateUserPage.roleInput.click();
|
||||
await adminCreateUserPage.page.getByRole(
|
||||
'option', { name: 'Admin' }
|
||||
).click();
|
||||
await adminCreateUserPage.createButton.click();
|
||||
|
||||
const snackbar = await adminUsersPage.getSnackbarData('snackbar-error');
|
||||
await expect(snackbar.variant).toBe('error');
|
||||
await expect(page.url()).toBe(editPageUrl);
|
||||
});
|
||||
});
|
||||
await expect(page.url()).toBe(createUserPageUrl);
|
||||
const snackbar = await adminUsersPage.getSnackbarData('snackbar-error');
|
||||
await expect(snackbar.variant).toBe('error');
|
||||
await adminUsersPage.closeSnackbar();
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
test(
|
||||
'Editing a user to have the same email as another user should not be allowed',
|
||||
async ({
|
||||
adminCreateUserPage, adminEditUserPage, adminUsersPage, page
|
||||
}) => {
|
||||
adminCreateUserPage.seed(9300);
|
||||
const user1 = adminCreateUserPage.generateUser();
|
||||
const user2 = adminCreateUserPage.generateUser();
|
||||
await test.step(
|
||||
'Create the first user',
|
||||
async () => {
|
||||
await adminUsersPage.navigateTo();
|
||||
await adminUsersPage.createUserButton.click();
|
||||
await adminCreateUserPage.fullNameInput.fill(user1.fullName);
|
||||
await adminCreateUserPage.emailInput.fill(user1.email);
|
||||
await adminCreateUserPage.roleInput.click();
|
||||
await adminCreateUserPage.page.getByRole(
|
||||
'option', { name: 'Admin' }
|
||||
).click();
|
||||
await adminCreateUserPage.createButton.click();
|
||||
const snackbar = await adminUsersPage.getSnackbarData(
|
||||
'snackbar-create-user-success'
|
||||
);
|
||||
await expect(snackbar.variant).toBe('success');
|
||||
await adminUsersPage.closeSnackbar();
|
||||
}
|
||||
);
|
||||
|
||||
await test.step(
|
||||
'Create the second user',
|
||||
async () => {
|
||||
await adminUsersPage.navigateTo();
|
||||
await adminUsersPage.createUserButton.click();
|
||||
await adminCreateUserPage.fullNameInput.fill(user2.fullName);
|
||||
await adminCreateUserPage.emailInput.fill(user2.email);
|
||||
await adminCreateUserPage.roleInput.click();
|
||||
await adminCreateUserPage.page.getByRole(
|
||||
'option', { name: 'Admin' }
|
||||
).click();
|
||||
await adminCreateUserPage.createButton.click();
|
||||
const snackbar = await adminUsersPage.getSnackbarData(
|
||||
'snackbar-create-user-success'
|
||||
);
|
||||
await expect(snackbar.variant).toBe('success');
|
||||
await adminUsersPage.closeSnackbar();
|
||||
}
|
||||
);
|
||||
|
||||
await test.step(
|
||||
'Try editing the second user to have the email of the first user',
|
||||
async () => {
|
||||
await adminUsersPage.navigateTo();
|
||||
await adminUsersPage.findUserPageWithEmail(user2.email);
|
||||
let userRow = await adminUsersPage.getUserRowByEmail(user2.email);
|
||||
await adminUsersPage.clickEditUser(userRow);
|
||||
await adminEditUserPage.waitForLoad(user2.fullName);
|
||||
await adminEditUserPage.emailInput.fill(user1.email);
|
||||
const editPageUrl = page.url();
|
||||
await adminEditUserPage.updateButton.click();
|
||||
|
||||
const snackbar = await adminUsersPage.getSnackbarData(
|
||||
'snackbar-error'
|
||||
);
|
||||
await expect(snackbar.variant).toBe('error');
|
||||
await adminUsersPage.closeSnackbar();
|
||||
await expect(page.url()).toBe(editPageUrl);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
@@ -1,55 +1,24 @@
|
||||
const { request } = require('@playwright/test');
|
||||
const { test, expect } = require('../../fixtures/index');
|
||||
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');
|
||||
const {AddMattermostConnectionModal} = require('../../fixtures/apps/mattermost/add-mattermost-connection-modal');
|
||||
|
||||
test.describe('Pop-up message on connections', () => {
|
||||
test.beforeEach(async ({ flowEditorPage, page }) => {
|
||||
const apiRequest = await request.newContext();
|
||||
const tokenJsonResponse = await getToken(apiRequest);
|
||||
const token = tokenJsonResponse.data.token;
|
||||
await page.getByTestId('create-flow-button').click();
|
||||
await page.waitForURL(
|
||||
/\/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}/
|
||||
);
|
||||
await expect(page.getByTestId('flow-step')).toHaveCount(2);
|
||||
|
||||
let flow = await createFlow(apiRequest, token);
|
||||
const flowId = flow.data.id;
|
||||
await updateFlowName(apiRequest, token, flowId);
|
||||
flow = await getFlow(apiRequest, token, flowId);
|
||||
const flowSteps = flow.data.steps;
|
||||
await flowEditorPage.flowName.click();
|
||||
await flowEditorPage.flowNameInput.fill('PopupFlow');
|
||||
await flowEditorPage.createWebhookTrigger(true);
|
||||
|
||||
const triggerStepId = flowSteps.find((step) => step.type === 'trigger').id;
|
||||
const actionStepId = flowSteps.find((step) => step.type === 'action').id;
|
||||
|
||||
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.chooseAppAndEvent('Mattermost', 'Send a message to channel');
|
||||
await expect(flowEditorPage.continueButton).toHaveCount(1);
|
||||
await expect(flowEditorPage.continueButton).not.toBeEnabled();
|
||||
|
||||
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 ({
|
||||
page,
|
||||
@@ -59,7 +28,7 @@ test.describe('Pop-up message on connections', () => {
|
||||
// Inject script to override window.open
|
||||
await page.evaluate(() => {
|
||||
// eslint-disable-next-line no-undef
|
||||
window.open = function () {
|
||||
window.open = function() {
|
||||
console.log('Popup blocked!');
|
||||
return null;
|
||||
};
|
||||
@@ -68,10 +37,8 @@ test.describe('Pop-up message on connections', () => {
|
||||
await addMattermostConnectionModal.fillConnectionForm();
|
||||
await addMattermostConnectionModal.submitConnectionForm();
|
||||
|
||||
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")).toHaveCount(1);
|
||||
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 ({
|
||||
@@ -84,15 +51,13 @@ test.describe('Pop-up message on connections', () => {
|
||||
await addMattermostConnectionModal.submitConnectionForm();
|
||||
|
||||
const popup = await popupPromise;
|
||||
await expect(popup.url()).toContain('mattermost');
|
||||
await expect(page.getByTestId('add-connection-error')).toHaveCount(0);
|
||||
await expect(popup.url()).toContain("mattermost");
|
||||
await expect(page.getByTestId("add-connection-error")).toHaveCount(0);
|
||||
|
||||
await test.step('Should show error on failed credentials verification', async () => {
|
||||
await popup.close();
|
||||
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")).toHaveCount(1);
|
||||
await expect(page.getByTestId("add-connection-error")).toHaveText('Error occured while verifying credentials!');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,38 +1,57 @@
|
||||
const { request } = require('@playwright/test');
|
||||
const { publicTest, expect } = require('../../fixtures/index');
|
||||
const { AdminUsersPage } = require('../../fixtures/admin/users-page');
|
||||
const { MyProfilePage } = require('../../fixtures/my-profile-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', () => {
|
||||
let testUser;
|
||||
|
||||
publicTest.beforeEach(
|
||||
async ({ adminCreateUserPage, loginPage, page }) => {
|
||||
let addUserResponse;
|
||||
const apiRequest = await request.newContext();
|
||||
async ({ acceptInvitationPage, adminCreateUserPage, loginPage, page }) => {
|
||||
let acceptInvitationLink;
|
||||
|
||||
adminCreateUserPage.seed(
|
||||
Math.ceil(Math.random() * Number.MAX_SAFE_INTEGER)
|
||||
);
|
||||
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 () => {
|
||||
const tokenJsonResponse = await getToken(apiRequest);
|
||||
addUserResponse = await addUser(
|
||||
apiRequest,
|
||||
tokenJsonResponse.data.token,
|
||||
{
|
||||
fullName: testUser.fullName,
|
||||
email: testUser.email,
|
||||
}
|
||||
await adminUsersPage.navigateTo();
|
||||
await adminUsersPage.createUserButton.click();
|
||||
await adminCreateUserPage.fullNameInput.fill(testUser.fullName);
|
||||
await adminCreateUserPage.emailInput.fill(testUser.email);
|
||||
await adminCreateUserPage.roleInput.click();
|
||||
await adminCreateUserPage.page
|
||||
.getByRole('option', { name: 'Admin' })
|
||||
.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 () => {
|
||||
let acceptToken = addUserResponse.data.acceptInvitationUrl.split('=')[1];
|
||||
await acceptInvitation(apiRequest, {token:acceptToken, password:LoginPage.defaultPassword});
|
||||
await page.goto(acceptInvitationLink);
|
||||
await acceptInvitationPage.acceptInvitation(LoginPage.defaultPassword);
|
||||
});
|
||||
|
||||
await publicTest.step('login as new Admin', async () => {
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,6 @@
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
"@testing-library/user-event": "^12.1.10",
|
||||
"axios": "^1.6.0",
|
||||
"clipboard-copy": "^4.0.1",
|
||||
"compare-versions": "^4.1.3",
|
||||
"lodash": "^4.17.21",
|
||||
|
@@ -112,7 +112,7 @@ export default function ResetPasswordForm() {
|
||||
<Alert
|
||||
data-test="accept-invitation-form-error"
|
||||
severity="error"
|
||||
sx={{ mt: 1 }}
|
||||
sx={{ mt: 1, fontWeight: 500 }}
|
||||
>
|
||||
{formatMessage('acceptInvitationForm.invalidToken')}
|
||||
</Alert>
|
||||
|
@@ -126,7 +126,7 @@ function AddAppConnection(props) {
|
||||
</DialogTitle>
|
||||
|
||||
{authDocUrl && (
|
||||
<Alert severity="info">
|
||||
<Alert severity="info" sx={{ fontWeight: 300 }}>
|
||||
{formatMessage('addAppConnection.callToDocs', {
|
||||
appName: name,
|
||||
docsLink: generateExternalLink(authDocUrl),
|
||||
@@ -138,7 +138,7 @@ function AddAppConnection(props) {
|
||||
<Alert
|
||||
data-test="add-connection-error"
|
||||
severity="error"
|
||||
sx={{ mt: 1, wordBreak: 'break-all' }}
|
||||
sx={{ mt: 1, fontWeight: 500, wordBreak: 'break-all' }}
|
||||
>
|
||||
{!errorDetails && errorMessage}
|
||||
{errorDetails && (
|
||||
|
@@ -32,7 +32,10 @@ function AdminApplicationAuthClientDialog(props) {
|
||||
<Dialog open={true} onClose={onClose}>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mt: 1, wordBreak: 'break-all' }}>
|
||||
<Alert
|
||||
severity="error"
|
||||
sx={{ mt: 1, fontWeight: 500, wordBreak: 'break-all' }}
|
||||
>
|
||||
{error.message}
|
||||
</Alert>
|
||||
)}
|
||||
|
@@ -6,7 +6,7 @@ import FormHelperText from '@mui/material/FormHelperText';
|
||||
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
import { ActionButtonsWrapper } from './style';
|
||||
import { ClickAwayListener } from '@mui/base/ClickAwayListener';
|
||||
import ClickAwayListener from '@mui/base/ClickAwayListener';
|
||||
import InputLabel from '@mui/material/InputLabel';
|
||||
import { createEditor } from 'slate';
|
||||
import { Editable, ReactEditor } from 'slate-react';
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import LoadingButton from '@mui/lab/LoadingButton';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
|
||||
import useForgotPassword from 'hooks/useForgotPassword';
|
||||
import Form from 'components/Form';
|
||||
@@ -12,17 +12,25 @@ import useFormatMessage from 'hooks/useFormatMessage';
|
||||
export default function ForgotPasswordForm() {
|
||||
const formatMessage = useFormatMessage();
|
||||
const {
|
||||
mutate: forgotPassword,
|
||||
mutateAsync: forgotPassword,
|
||||
isPending: loading,
|
||||
isSuccess,
|
||||
isError,
|
||||
error,
|
||||
} = useForgotPassword();
|
||||
|
||||
const handleSubmit = ({ email }) => {
|
||||
forgotPassword({
|
||||
email,
|
||||
});
|
||||
const handleSubmit = async (values) => {
|
||||
const { email } = values;
|
||||
try {
|
||||
await forgotPassword({
|
||||
email,
|
||||
});
|
||||
} catch (error) {
|
||||
enqueueSnackbar(
|
||||
error?.message || formatMessage('forgotPasswordForm.error'),
|
||||
{
|
||||
variant: 'error',
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -49,16 +57,6 @@ export default function ForgotPasswordForm() {
|
||||
margin="dense"
|
||||
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
|
||||
type="submit"
|
||||
variant="contained"
|
||||
@@ -70,6 +68,14 @@ export default function ForgotPasswordForm() {
|
||||
>
|
||||
{formatMessage('forgotPasswordForm.submit')}
|
||||
</LoadingButton>
|
||||
{isSuccess && (
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{ color: (theme) => theme.palette.success.main }}
|
||||
>
|
||||
{formatMessage('forgotPasswordForm.instructionsSent')}
|
||||
</Typography>
|
||||
)}
|
||||
</Form>
|
||||
</Paper>
|
||||
);
|
||||
|
@@ -188,7 +188,7 @@ function InstallationForm() {
|
||||
)}
|
||||
/>
|
||||
{install.isSuccess && (
|
||||
<Alert data-test="success-alert" severity="success" sx={{ mt: 3 }}>
|
||||
<Alert data-test="success-alert" severity="success" sx={{ mt: 3, fontWeight: 500 }}>
|
||||
{formatMessage('installationForm.success', {
|
||||
link: (str) => (
|
||||
<Link
|
||||
|
@@ -2,7 +2,6 @@ import * as React from 'react';
|
||||
import { useNavigate, Link as RouterLink } from 'react-router-dom';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import Link from '@mui/material/Link';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import LoadingButton from '@mui/lab/LoadingButton';
|
||||
import useAuthentication from 'hooks/useAuthentication';
|
||||
@@ -12,18 +11,16 @@ import Form from 'components/Form';
|
||||
import TextField from 'components/TextField';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import useCreateAccessToken from 'hooks/useCreateAccessToken';
|
||||
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
||||
|
||||
function LoginForm() {
|
||||
const isCloud = useCloud();
|
||||
const navigate = useNavigate();
|
||||
const formatMessage = useFormatMessage();
|
||||
const enqueueSnackbar = useEnqueueSnackbar();
|
||||
const authentication = useAuthentication();
|
||||
const {
|
||||
mutateAsync: createAccessToken,
|
||||
isPending: loading,
|
||||
error,
|
||||
isError,
|
||||
} = useCreateAccessToken();
|
||||
const { mutateAsync: createAccessToken, isPending: loading } =
|
||||
useCreateAccessToken();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (authentication.isAuthenticated) {
|
||||
@@ -40,19 +37,23 @@ function LoginForm() {
|
||||
});
|
||||
const { token } = data;
|
||||
authentication.updateToken(token);
|
||||
} catch {}
|
||||
};
|
||||
} catch (error) {
|
||||
const errors = error?.response?.data?.errors
|
||||
? Object.values(error.response.data.errors)
|
||||
: [];
|
||||
|
||||
const renderError = () => {
|
||||
const errors = error?.response?.data?.errors?.general || [
|
||||
error?.message || formatMessage('loginForm.error'),
|
||||
];
|
||||
|
||||
return errors.map((error) => (
|
||||
<Alert severity="error" sx={{ mt: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
));
|
||||
if (errors.length) {
|
||||
for (const [error] of errors) {
|
||||
enqueueSnackbar(error, {
|
||||
variant: 'error',
|
||||
});
|
||||
}
|
||||
} else {
|
||||
enqueueSnackbar(error?.message || formatMessage('loginForm.error'), {
|
||||
variant: 'error',
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -105,8 +106,6 @@ function LoginForm() {
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{isError && renderError()}
|
||||
|
||||
<LoadingButton
|
||||
type="submit"
|
||||
variant="contained"
|
||||
|
@@ -30,7 +30,7 @@ const PermissionCatalogField = ({
|
||||
if (isPermissionCatalogLoading) return <PermissionCatalogFieldLoader />;
|
||||
|
||||
return (
|
||||
<TableContainer data-test="permissions-catalog" component={Paper}>
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { ClickAwayListener } from '@mui/base/ClickAwayListener';
|
||||
import ClickAwayListener from '@mui/base/ClickAwayListener';
|
||||
import FormHelperText from '@mui/material/FormHelperText';
|
||||
import InputLabel from '@mui/material/InputLabel';
|
||||
import * as React from 'react';
|
||||
|
@@ -2,7 +2,6 @@ import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import LoadingButton from '@mui/lab/LoadingButton';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
||||
import * as React from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
@@ -31,8 +30,6 @@ export default function ResetPasswordForm() {
|
||||
mutateAsync: resetPassword,
|
||||
isPending,
|
||||
isSuccess,
|
||||
error,
|
||||
isError,
|
||||
} = useResetPassword();
|
||||
const token = searchParams.get('token');
|
||||
|
||||
@@ -50,23 +47,14 @@ export default function ResetPasswordForm() {
|
||||
},
|
||||
});
|
||||
navigate(URLS.LOGIN);
|
||||
} catch {}
|
||||
};
|
||||
|
||||
const renderError = () => {
|
||||
if (!isError) {
|
||||
return null;
|
||||
} catch (error) {
|
||||
enqueueSnackbar(
|
||||
error?.message || formatMessage('resetPasswordForm.error'),
|
||||
{
|
||||
variant: 'error',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
@@ -108,6 +96,7 @@ export default function ResetPasswordForm() {
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label={formatMessage(
|
||||
'resetPasswordForm.confirmPasswordFieldLabel',
|
||||
@@ -128,7 +117,7 @@ export default function ResetPasswordForm() {
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
{renderError()}
|
||||
|
||||
<LoadingButton
|
||||
type="submit"
|
||||
variant="contained"
|
||||
|
@@ -7,7 +7,7 @@ import FormControl from '@mui/material/FormControl';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
|
||||
export default function SearchInput({ onChange, defaultValue = '' }) {
|
||||
export default function SearchInput({ onChange }) {
|
||||
const formatMessage = useFormatMessage();
|
||||
return (
|
||||
<FormControl variant="outlined" fullWidth>
|
||||
@@ -16,7 +16,6 @@ export default function SearchInput({ onChange, defaultValue = '' }) {
|
||||
</InputLabel>
|
||||
|
||||
<OutlinedInput
|
||||
defaultValue={defaultValue}
|
||||
id="search-input"
|
||||
type="text"
|
||||
size="medium"
|
||||
@@ -35,5 +34,4 @@ export default function SearchInput({ onChange, defaultValue = '' }) {
|
||||
|
||||
SearchInput.propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
defaultValue: PropTypes.string,
|
||||
};
|
||||
|
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
||||
import Button from '@mui/material/Button';
|
||||
import ButtonGroup from '@mui/material/ButtonGroup';
|
||||
import { ClickAwayListener } from '@mui/base/ClickAwayListener';
|
||||
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
||||
import Grow from '@mui/material/Grow';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import MenuList from '@mui/material/MenuList';
|
||||
|
@@ -84,7 +84,10 @@ function TestSubstep(props) {
|
||||
}}
|
||||
>
|
||||
{hasError && (
|
||||
<Alert severity="error" sx={{ mb: 2, width: '100%' }}>
|
||||
<Alert
|
||||
severity="error"
|
||||
sx={{ mb: 2, fontWeight: 500, width: '100%' }}
|
||||
>
|
||||
<pre style={{ margin: 0, whiteSpace: 'pre-wrap' }}>
|
||||
{JSON.stringify(errorDetails, null, 2)}
|
||||
</pre>
|
||||
@@ -101,11 +104,13 @@ function TestSubstep(props) {
|
||||
severity="warning"
|
||||
sx={{ mb: 1, width: '100%' }}
|
||||
>
|
||||
<AlertTitle>
|
||||
<AlertTitle sx={{ fontWeight: 700 }}>
|
||||
{formatMessage('flowEditor.noTestDataTitle')}
|
||||
</AlertTitle>
|
||||
|
||||
<Box>{formatMessage('flowEditor.noTestDataMessage')}</Box>
|
||||
<Box sx={{ fontWeight: 400 }}>
|
||||
{formatMessage('flowEditor.noTestDataMessage')}
|
||||
</Box>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
|
@@ -124,6 +124,7 @@ export default function CreateUser() {
|
||||
<Alert
|
||||
severity="info"
|
||||
color="primary"
|
||||
sx={{ fontWeight: '500' }}
|
||||
data-test="invitation-email-info-alert"
|
||||
>
|
||||
{formatMessage('createUser.invitationEmailInfo', {
|
||||
|
@@ -42,9 +42,13 @@ export default function Execution() {
|
||||
<Grid container item sx={{ mt: 2, mb: [2, 5] }} rowGap={3}>
|
||||
{!isExecutionStepsLoading && !data?.pages?.[0].data.length && (
|
||||
<Alert severity="warning" sx={{ flex: 1 }}>
|
||||
<AlertTitle>{formatMessage('execution.noDataTitle')}</AlertTitle>
|
||||
<AlertTitle sx={{ fontWeight: 700 }}>
|
||||
{formatMessage('execution.noDataTitle')}
|
||||
</AlertTitle>
|
||||
|
||||
<Box>{formatMessage('execution.noDataMessage')}</Box>
|
||||
<Box sx={{ fontWeight: 400 }}>
|
||||
{formatMessage('execution.noDataMessage')}
|
||||
</Box>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
import debounce from 'lodash/debounce';
|
||||
import Box from '@mui/material/Box';
|
||||
import Grid from '@mui/material/Grid';
|
||||
@@ -23,18 +23,13 @@ import useLazyFlows from 'hooks/useLazyFlows';
|
||||
|
||||
export default function Flows() {
|
||||
const formatMessage = useFormatMessage();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const page = parseInt(searchParams.get('page') || '', 10) || 1;
|
||||
const flowName = searchParams.get('flowName') || '';
|
||||
const [isLoading, setIsLoading] = React.useState(true);
|
||||
const [flowName, setFlowName] = React.useState('');
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const currentUserAbility = useCurrentUserAbility();
|
||||
|
||||
const {
|
||||
data,
|
||||
mutate: fetchFlows,
|
||||
isSuccess,
|
||||
} = useLazyFlows(
|
||||
const { data, mutate: fetchFlows } = useLazyFlows(
|
||||
{ flowName, page },
|
||||
{
|
||||
onSettled: () => {
|
||||
@@ -43,36 +38,6 @@ export default function Flows() {
|
||||
},
|
||||
);
|
||||
|
||||
const flows = data?.data || [];
|
||||
const pageInfo = data?.meta;
|
||||
const hasFlows = flows?.length;
|
||||
const navigateToLastPage = isSuccess && !hasFlows && page > 1;
|
||||
|
||||
const onSearchChange = React.useCallback((event) => {
|
||||
setSearchParams({ flowName: event.target.value });
|
||||
}, []);
|
||||
|
||||
const getPathWithSearchParams = (page, flowName) => {
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (page > 1) {
|
||||
searchParams.set('page', page);
|
||||
}
|
||||
if (flowName) {
|
||||
searchParams.set('flowName', flowName);
|
||||
}
|
||||
|
||||
return { search: searchParams.toString() };
|
||||
};
|
||||
|
||||
const onDuplicateFlow = () => {
|
||||
if (pageInfo?.currentPage > 1) {
|
||||
navigate(getPathWithSearchParams(1, flowName));
|
||||
} else {
|
||||
fetchFlows();
|
||||
}
|
||||
};
|
||||
|
||||
const fetchData = React.useMemo(
|
||||
() => debounce(fetchFlows, 300),
|
||||
[fetchFlows],
|
||||
@@ -89,14 +54,21 @@ export default function Flows() {
|
||||
}, [fetchData, flowName, page]);
|
||||
|
||||
React.useEffect(
|
||||
function redirectToLastPage() {
|
||||
if (navigateToLastPage) {
|
||||
navigate(getPathWithSearchParams(pageInfo.totalPages, flowName));
|
||||
}
|
||||
function resetPageOnSearch() {
|
||||
// reset search params which only consists of `page`
|
||||
setSearchParams({});
|
||||
},
|
||||
[navigateToLastPage],
|
||||
[flowName],
|
||||
);
|
||||
|
||||
const flows = data?.data || [];
|
||||
const pageInfo = data?.meta;
|
||||
const hasFlows = flows?.length;
|
||||
|
||||
const onSearchChange = React.useCallback((event) => {
|
||||
setFlowName(event.target.value);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box sx={{ py: 3 }}>
|
||||
<Container>
|
||||
@@ -106,7 +78,7 @@ export default function Flows() {
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm="auto" order={{ xs: 2, sm: 1 }}>
|
||||
<SearchInput onChange={onSearchChange} defaultValue={flowName} />
|
||||
<SearchInput onChange={onSearchChange} />
|
||||
</Grid>
|
||||
|
||||
<Grid
|
||||
@@ -139,7 +111,7 @@ export default function Flows() {
|
||||
</Grid>
|
||||
|
||||
<Divider sx={{ mt: [2, 0], mb: 2 }} />
|
||||
{(isLoading || navigateToLastPage) && (
|
||||
{isLoading && (
|
||||
<CircularProgress sx={{ display: 'block', margin: '20px auto' }} />
|
||||
)}
|
||||
{!isLoading &&
|
||||
@@ -147,11 +119,11 @@ export default function Flows() {
|
||||
<FlowRow
|
||||
key={flow.id}
|
||||
flow={flow}
|
||||
onDuplicateFlow={onDuplicateFlow}
|
||||
onDuplicateFlow={fetchFlows}
|
||||
onDeleteFlow={fetchFlows}
|
||||
/>
|
||||
))}
|
||||
{!isLoading && !navigateToLastPage && !hasFlows && (
|
||||
{!isLoading && !hasFlows && (
|
||||
<NoResultFound
|
||||
text={formatMessage('flows.noFlows')}
|
||||
{...(currentUserAbility.can('create', 'Flow') && {
|
||||
@@ -159,23 +131,23 @@ export default function Flows() {
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{!isLoading &&
|
||||
!navigateToLastPage &&
|
||||
pageInfo &&
|
||||
pageInfo.totalPages > 1 && (
|
||||
<Pagination
|
||||
sx={{ display: 'flex', justifyContent: 'center', mt: 3 }}
|
||||
page={pageInfo?.currentPage}
|
||||
count={pageInfo?.totalPages}
|
||||
renderItem={(item) => (
|
||||
<PaginationItem
|
||||
component={Link}
|
||||
to={getPathWithSearchParams(item.page, flowName)}
|
||||
{...item}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{!isLoading && pageInfo && pageInfo.totalPages > 1 && (
|
||||
<Pagination
|
||||
sx={{ display: 'flex', justifyContent: 'center', mt: 3 }}
|
||||
page={pageInfo?.currentPage}
|
||||
count={pageInfo?.totalPages}
|
||||
onChange={(event, page) =>
|
||||
setSearchParams({ page: page.toString() })
|
||||
}
|
||||
renderItem={(item) => (
|
||||
<PaginationItem
|
||||
component={Link}
|
||||
to={`${item.page === 1 ? '' : `?page=${item.page}`}`}
|
||||
{...item}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
|
@@ -266,8 +266,8 @@ function ProfileSettings() {
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} justifyContent="flex-end" sx={{ pt: 5 }}>
|
||||
<Alert variant="outlined" severity="error">
|
||||
<AlertTitle>
|
||||
<Alert variant="outlined" severity="error" sx={{ fontWeight: 500 }}>
|
||||
<AlertTitle sx={{ fontWeight: 700 }}>
|
||||
{formatMessage('profileSettings.deleteMyAccount')}
|
||||
</AlertTitle>
|
||||
|
||||
|
@@ -278,20 +278,6 @@ export const defaultTheme = createTheme({
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiAlert: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) => ({
|
||||
fontWeight: theme.typography.fontWeightRegular,
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiAlertTitle: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) => ({
|
||||
fontWeight: theme.typography.fontWeightBold,
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
export const mationTheme = createTheme(
|
||||
|
11069
packages/web/yarn.lock
11069
packages/web/yarn.lock
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user