From 0f5c816caab61a12e7debd8bca3422fe68157bd0 Mon Sep 17 00:00:00 2001 From: Daniel Vega Date: Wed, 6 Aug 2025 09:16:01 +0200 Subject: [PATCH 1/6] Creating a seeding script with minimal data for e2e testing --- E2E_SEEDING.md | 100 ++++++++++++ apps/api/package.json | 1 + apps/api/src/seed-e2e.ts | 21 +++ apps/gauzy-e2e/E2E_CREDENTIALS.md | 75 +++++++++ .../Base/pagedata/EditProfilePageData.ts | 6 +- .../support/Base/pagedata/LoginPageData.ts | 4 +- docker-compose.infra.yml | 13 +- package.json | 1 + packages/core/src/lib/app/app.service.ts | 8 +- .../src/lib/core/seeds/seed-data.service.ts | 146 +++++++++++++++++- packages/core/src/lib/core/seeds/seed.ts | 33 +++- packages/core/src/lib/user/e2e-user.seed.ts | 140 +++++++++++++++++ packages/core/src/lib/user/e2e-users.ts | 39 +++++ packages/core/src/lib/user/user.entity.ts | 4 +- 14 files changed, 575 insertions(+), 16 deletions(-) create mode 100644 E2E_SEEDING.md create mode 100644 apps/api/src/seed-e2e.ts create mode 100644 apps/gauzy-e2e/E2E_CREDENTIALS.md create mode 100644 packages/core/src/lib/user/e2e-user.seed.ts create mode 100644 packages/core/src/lib/user/e2e-users.ts diff --git a/E2E_SEEDING.md b/E2E_SEEDING.md new file mode 100644 index 00000000000..4e954f249c6 --- /dev/null +++ b/E2E_SEEDING.md @@ -0,0 +1,100 @@ +# E2E Organization Seeding + +## Overview + +The E2E seeding approach has been modified to seed only organization-specific data within an existing database, rather than creating a separate database. This provides better isolation and efficiency for e2e testing. + +## What Changed + +### Before +- Created a separate database file (`gauzy-e2e.sqlite3`) +- Reset entire database and seeded all data +- Required separate database setup + +### After +- Uses existing database +- Seeds only organization-specific data +- Creates E2E Testing Tenant and Organization +- Adds essential tags and employee levels for testing + +## How It Works + +### 1. Database Connection +- Connects to existing database (no reset) +- Uses same database as development/production + +### 2. Tenant Creation +- Creates "E2E Testing Tenant" if it doesn't exist +- Reuses existing tenant if found + +### 3. Organization Creation +- Creates "E2E Testing Organization" if it doesn't exist +- Reuses existing organization if found +- Sets as default organization for the tenant + +### 4. Essential Data Seeding +- Creates tag types and tags for testing +- Creates employee levels for testing +- Minimal data for fast test execution + +## Usage + +### For CI/CD +```bash +# Seed organization data for e2e testing +yarn seed:e2e + +# Start API (will skip auto-seeding due to E2E_TESTING=true) +E2E_TESTING=true yarn start:api + +# Run e2e tests +yarn start:e2e +``` + +### For Development +```bash +# Seed organization data +yarn seed:e2e + +# Start API with e2e testing +E2E_TESTING=true yarn start:api +``` + +## Benefits + +1. **No Database Conflicts**: Uses existing database, no separate files +2. **Faster Setup**: No need to reset entire database +3. **Better Isolation**: Organization-level isolation +4. **Reusable**: Can run multiple times safely +5. **CI/CD Friendly**: Simple one-command seeding + +## Data Created + +- ✅ E2E Testing Tenant +- ✅ E2E Testing Organization (default) +- ✅ E2E-specific users (different emails to avoid conflicts) +- ✅ Tag Types and Tags +- ✅ Employee Levels +- ❌ No demo or random data + +## E2E User Credentials + +### **E2E Super Admin** +- Email: `e2e.admin@dspot.com.pl` +- Password: `admin` + +### **E2E Local Admin** +- Email: `e2e.local.admin@dspot.com.pl` +- Password: `admin` + +### **E2E Employee** +- Email: `e2e.employee@dspot.com.pl` +- Password: `123456` + +## Environment Variables + +The seeding respects these environment variables: +- `E2E_TESTING=true` - Prevents automatic API seeding +- `DB_TYPE` - Database type (better-sqlite3, postgres, mysql) +- `DB_PATH` - Database path (for SQLite) +- `DB_HOST`, `DB_PORT`, etc. - Database connection settings diff --git a/apps/api/package.json b/apps/api/package.json index 1f09d0da8f3..4fcb2c8281c 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -42,6 +42,7 @@ "seed:build": "yarn ng run api:seed", "seed:all": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=14000 yarn ts-node -r tsconfig-paths/register --project apps/api/tsconfig.app.json src/seed-all.ts", "seed:module": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=14000 yarn ts-node -r tsconfig-paths/register --project apps/api/tsconfig.app.json src/seed-module.ts --name", + "seed:e2e": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=14000 yarn ts-node -r tsconfig-paths/register --project apps/api/tsconfig.app.json src/seed-e2e.ts", "seed:all:build": "yarn ng run api:seed-all", "seed:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=14000 yarn ts-node -r tsconfig-paths/register --project apps/api/tsconfig.app.json src/seed.ts", "seed:prod:build": "yarn ng run api:seed -c=production" diff --git a/apps/api/src/seed-e2e.ts b/apps/api/src/seed-e2e.ts new file mode 100644 index 00000000000..f06903b8b51 --- /dev/null +++ b/apps/api/src/seed-e2e.ts @@ -0,0 +1,21 @@ +import { Logger } from '@nestjs/common'; +import { seedE2E } from '@gauzy/core'; +import { pluginConfig } from './plugin-config'; + +const logger = new Logger('GZY - SeedE2E'); + +/** + * E2E Organization Seeding Script + * + * This script seeds only organization-specific data within an existing database + * for e2e testing purposes. It creates: + * - E2E Testing Tenant (if not exists) + * - E2E Testing Organization (if not exists) + * - Essential tags and employee levels for testing + * + * Usage: yarn seed:e2e + */ +seedE2E(pluginConfig).catch((error) => { + logger.error(`Error seeding e2e organization data: ${error}`); + process.exit(1); +}); diff --git a/apps/gauzy-e2e/E2E_CREDENTIALS.md b/apps/gauzy-e2e/E2E_CREDENTIALS.md new file mode 100644 index 00000000000..ebb50de7e29 --- /dev/null +++ b/apps/gauzy-e2e/E2E_CREDENTIALS.md @@ -0,0 +1,75 @@ +# E2E Testing Credentials + +This document outlines the credentials used for E2E testing in the Cypress project. + +## Updated Credentials + +The E2E testing credentials have been updated to avoid conflicts with the default development users. + +### **E2E Super Admin** +- **Email**: `e2e.admin@dspot.com.pl` +- **Password**: `admin` +- **Role**: Super Admin +- **Used in**: Main login tests, admin functionality tests + +### **E2E Local Admin** +- **Email**: `e2e.local.admin@dspot.com.pl` +- **Password**: `admin` +- **Role**: Local Admin +- **Used in**: Profile editing tests, admin management tests + +### **E2E Employee** +- **Email**: `e2e.employee@dspot.com.pl` +- **Password**: `123456` +- **Role**: Employee +- **Used in**: Employee functionality tests, time tracking tests + +## Files Updated + +### `LoginPageData.ts` +```typescript +export const LoginPageData = { + TitleText: 'DSpot ERP', + email: 'e2e.admin@dspot.com.pl', // Updated + password: 'admin', + empEmail: 'e2e.employee@dspot.com.pl', // Updated + empPassword: '123456' +}; +``` + +### `EditProfilePageData.ts` +```typescript +export const EditProfilePageData = { + preferredLanguage: 'English', + email: 'e2e.local.admin@dspot.com.pl', // Updated + password: 'admin', + firstName: 'E2E', // Updated + lastName: 'Local Admin' // Updated +}; +``` + +## Benefits + +1. **No Conflicts**: E2E users won't interfere with development users +2. **Isolated Testing**: Each test environment has its own users +3. **Predictable**: Same credentials every time tests run +4. **Multiple Roles**: Different user types for different test scenarios + +## Usage in Tests + +```typescript +// Login as E2E Super Admin +CustomCommands.login(loginPage, LoginPageData, dashboardPage); + +// Login as E2E Employee +CustomCommands.loginAsEmployee(loginPage, dashboardPage, LoginPageData.empEmail, LoginPageData.empPassword); +``` + +## Database Seeding + +These credentials are created by the E2E seeding process: +```bash +yarn seed:e2e +``` + +The seeding creates users with these exact email addresses in the "E2E Testing Organization". diff --git a/apps/gauzy-e2e/src/support/Base/pagedata/EditProfilePageData.ts b/apps/gauzy-e2e/src/support/Base/pagedata/EditProfilePageData.ts index ad9341b7f24..4b18261bcb4 100644 --- a/apps/gauzy-e2e/src/support/Base/pagedata/EditProfilePageData.ts +++ b/apps/gauzy-e2e/src/support/Base/pagedata/EditProfilePageData.ts @@ -1,7 +1,7 @@ export const EditProfilePageData = { preferredLanguage: 'English', - email: 'local.admin@dspot.com.pl', + email: 'e2e.local.admin@dspot.com.pl', password: 'admin', - firstName: 'Admin', - lastName: 'Local' + firstName: 'E2E', + lastName: 'Local Admin' }; diff --git a/apps/gauzy-e2e/src/support/Base/pagedata/LoginPageData.ts b/apps/gauzy-e2e/src/support/Base/pagedata/LoginPageData.ts index 0f43f012bbf..821f79a8f39 100644 --- a/apps/gauzy-e2e/src/support/Base/pagedata/LoginPageData.ts +++ b/apps/gauzy-e2e/src/support/Base/pagedata/LoginPageData.ts @@ -1,7 +1,7 @@ export const LoginPageData = { TitleText: 'DSpot ERP', - email: 'admin@dspot.com.pl', + email: 'e2e.admin@dspot.com.pl', password: 'admin', - empEmail: 'employee@dspot.com.pl', + empEmail: 'e2e.employee@dspot.com.pl', empPassword: '123456' }; diff --git a/docker-compose.infra.yml b/docker-compose.infra.yml index 4a6ed9082b3..0e6e9d40c97 100644 --- a/docker-compose.infra.yml +++ b/docker-compose.infra.yml @@ -222,18 +222,19 @@ services: - overlay pgweb: - image: sosedoff/pgweb + image: sosedoff/pgweb:latest container_name: pgweb - restart: always + restart: unless-stopped depends_on: - - db - links: - - db:${DB_HOST:-db} + db: + condition: service_healthy environment: + DATABASE_URL: postgres://${DB_USER:-postgres}:${DB_PASS:-gauzy_password}@db:5432/${DB_NAME:-gauzy}?sslmode=disable + # Alternative environment variables (pgweb supports both) POSTGRES_DB: ${DB_NAME:-gauzy} POSTGRES_USER: ${DB_USER:-postgres} POSTGRES_PASSWORD: ${DB_PASS:-gauzy_password} - PGWEB_DATABASE_URL: postgres://${DB_USER:-postgres}:${DB_PASS:-gauzy_password}@${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-gauzy}?sslmode=disable + PGWEB_DATABASE_URL: postgres://${DB_USER:-postgres}:${DB_PASS:-gauzy_password}@db:5432/${DB_NAME:-gauzy}?sslmode=disable ports: - '8081:8081' networks: diff --git a/package.json b/package.json index 6cd8f225794..9a6ab68630e 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "seed:ever": "yarn seed:base ./apps/api/src/seed-ever.ts", "seed:all": "yarn seed:base ./apps/api/src/seed-all.ts", "seed:jobs": "yarn seed:base ./apps/api/src/seed-jobs.ts", + "seed:e2e": "yarn seed:base ./apps/api/src/seed-e2e.ts", "seed:module": "yarn seed:base ./apps/api/src/seed-module.ts --name", "seed:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn run build:package:api:prod && yarn run config:prod && yarn ts-node -r tsconfig-paths/register --project apps/api/tsconfig.app.json ./apps/api/src/seed.ts", "prebuild": "rimraf dist coverage", diff --git a/packages/core/src/lib/app/app.service.ts b/packages/core/src/lib/app/app.service.ts index 30ebe91f210..8312c30db3c 100644 --- a/packages/core/src/lib/app/app.service.ts +++ b/packages/core/src/lib/app/app.service.ts @@ -6,7 +6,7 @@ import { UserService } from '../user/user.service'; @Injectable() export class AppService { - public count: number = 0; + public count = 0; constructor( @Inject(forwardRef(() => SeedDataService)) @@ -24,6 +24,12 @@ export class AppService { * TODO: this should actually include more checks, e.g. if schema migrated and many other things */ async seedDBIfEmpty() { + // Skip automatic seeding if E2E_TESTING environment variable is set + if (process.env.E2E_TESTING === 'true') { + console.log(chalk.yellow('Skipping automatic seeding for E2E testing')); + return; + } + this.count = await this.userService.countFast(); console.log(chalk.magenta(`Found ${this.count} users in DB`)); if (this.count === 0) { diff --git a/packages/core/src/lib/core/seeds/seed-data.service.ts b/packages/core/src/lib/core/seeds/seed-data.service.ts index 1771ead3d31..0518c6497cf 100644 --- a/packages/core/src/lib/core/seeds/seed-data.service.ts +++ b/packages/core/src/lib/core/seeds/seed-data.service.ts @@ -22,6 +22,8 @@ import { createRandomSuperAdminUsers, createRandomUsers } from '../../user/user.seed'; +import { createE2EAdminUsers, createE2EEmployeesUsers } from '../../user/e2e-user.seed'; +import { E2E_EMPLOYEES } from '../../user/e2e-users'; import { createDefaultEmployees, createRandomEmployees } from '../../employee/employee.seed'; import { createDefaultOrganizations, @@ -224,7 +226,8 @@ import { createRandomOrganizationTagTypes, createTagTypes } from '../../tag-type export enum SeederTypeEnum { ALL = 'all', EVER = 'ever', - DEFAULT = 'default' + DEFAULT = 'default', + E2E = 'e2e' } @Injectable() @@ -360,6 +363,29 @@ export class SeedDataService { } } + /** + * Seed E2E Testing Data + * Seeds only an organization within an existing database for e2e testing + */ + public async runE2ESeed(fromAPI: boolean) { + try { + this.seedType = SeederTypeEnum.E2E; + + // Connect to database (don't reset, use existing) + await this.createConnection(); + + // Seed minimal organization data for e2e testing + await this.seedE2EOrganizationData(); + + // Disconnect to database + await this.closeConnection(); + + console.log('E2E Organization Seed Completed'); + } catch (error) { + this.handleError(error); + } + } + /** * Seed Default Report Data */ @@ -571,6 +597,124 @@ export class SeedDataService { this.log(chalk.magenta(`✅ SEEDED BASIC ${env.production ? 'PRODUCTION' : ''} DATABASE`)); } + /** + * Populate Database with E2E Testing Organization Data + * Seeds only organization-specific data within an existing database + */ + private async seedE2EOrganizationData() { + this.log(chalk.magenta(`🌱 SEEDING E2E TESTING ORGANIZATION...`)); + + // Get or create E2E tenant + this.tenant = await this.getOrCreateE2ETenant(); + + // Get or create E2E organization + this.defaultOrganization = await this.getOrCreateE2EOrganization(); + + // Create E2E-specific users with different emails to avoid conflicts + await this.createE2EUsers(); + + // Create essential tags for e2e testing + const defaultTagTypes = await this.tryExecute( + 'E2E Tag Types', + createTagTypes(this.dataSource, this.tenant, [this.defaultOrganization]) + ); + + await this.tryExecute( + 'E2E Tags', + createDefaultTags(this.dataSource, this.tenant, [this.defaultOrganization], defaultTagTypes || []) + ); + + // Create employee levels for e2e testing + await this.tryExecute( + 'E2E Employee Levels', + createEmployeeLevels(this.dataSource, this.tenant, [this.defaultOrganization]) + ); + + this.log(chalk.magenta(`✅ SEEDED E2E TESTING ORGANIZATION`)); + } + + /** + * Create E2E-specific users with different emails to avoid conflicts + */ + private async createE2EUsers() { + // Create E2E admin users with different emails + const { e2eSuperAdminUsers, e2eAdminUsers } = await createE2EAdminUsers(this.dataSource, this.tenant); + + // Create E2E employee users + const { e2eEmployeeUsers } = await createE2EEmployeesUsers(this.dataSource, this.tenant); + + // Create user-organization relationships + const e2eUsers = [...e2eSuperAdminUsers, ...e2eAdminUsers, ...e2eEmployeeUsers]; + await this.tryExecute( + 'E2E Users', + createDefaultUsersOrganizations(this.dataSource, this.tenant, [this.defaultOrganization], e2eUsers) + ); + + // Create employees + this.defaultEmployees = await createDefaultEmployees( + this.dataSource, + this.tenant, + this.defaultOrganization, + e2eEmployeeUsers, + E2E_EMPLOYEES + ); + } + + /** + * Get or create E2E tenant + */ + private async getOrCreateE2ETenant(): Promise { + // Try to find existing E2E tenant + const existingTenant = await this.dataSource.getRepository('Tenant').findOne({ + where: { name: 'E2E Testing Tenant' } + }); + + if (existingTenant) { + this.log(chalk.blue(`Found existing E2E tenant: ${existingTenant.name}`)); + return existingTenant; + } + + // Create new E2E tenant + this.log(chalk.blue(`Creating new E2E tenant...`)); + return (await this.tryExecute( + 'E2E Tenant', + createDefaultTenant(this.dataSource, 'E2E Testing Tenant') + )) as ITenant; + } + + /** + * Get or create E2E organization + */ + private async getOrCreateE2EOrganization(): Promise { + // Try to find existing E2E organization + const existingOrg = (await this.dataSource.getRepository('Organization').findOne({ + where: { name: 'E2E Testing Organization' } + })) as IOrganization; + + if (existingOrg) { + this.log(chalk.blue(`Found existing E2E organization: ${existingOrg.name}`)); + return existingOrg; + } + + // Create new E2E organization + this.log(chalk.blue(`Creating new E2E organization...`)); + const organizations = (await this.tryExecute( + 'E2E Organization', + createDefaultOrganizations(this.dataSource, this.tenant, [ + { + name: 'E2E Testing Organization', + currency: 'USD', + defaultValueDateType: 'TODAY', + imageUrl: 'assets/images/logos/dspot_erp_light.svg', + isDefault: true, + totalEmployees: 1 + } + ]) + )) as IOrganization[]; + + return organizations.find((org) => org.name === 'E2E Testing Organization'); + } + /** * Populate default data for default tenant */ diff --git a/packages/core/src/lib/core/seeds/seed.ts b/packages/core/src/lib/core/seeds/seed.ts index d53ead4fef1..ee5dbde208a 100644 --- a/packages/core/src/lib/core/seeds/seed.ts +++ b/packages/core/src/lib/core/seeds/seed.ts @@ -27,7 +27,38 @@ export async function seedDefault(devConfig: Partial) { const seeder = app.get(SeedDataService); seeder .runDefaultSeed(false) - .then(() => {}) + .then(() => { + // Seed completed successfully + }) + .catch((error) => { + throw error; + }) + .finally(() => app.close()); + }) + .catch((error) => { + throw error; + }); +} + +/** + * Seeds organization-specific data for e2e testing within an existing database. + * Creates E2E Testing Tenant and Organization with essential data for testing. + * Does not reset the database, only adds organization-specific data. + * + */ +export async function seedE2E(devConfig: Partial) { + await registerPluginConfig(devConfig); + + NestFactory.createApplicationContext(SeederModule.forPlugins(), { + logger: ['log', 'error', 'warn', 'debug', 'verbose'] + }) + .then((app) => { + const seeder = app.get(SeedDataService); + seeder + .runE2ESeed(false) + .then(() => { + // E2E seed completed successfully + }) .catch((error) => { throw error; }) diff --git a/packages/core/src/lib/user/e2e-user.seed.ts b/packages/core/src/lib/user/e2e-user.seed.ts new file mode 100644 index 00000000000..9fb800e9724 --- /dev/null +++ b/packages/core/src/lib/user/e2e-user.seed.ts @@ -0,0 +1,140 @@ +import { DataSource } from 'typeorm'; +import * as bcrypt from 'bcrypt'; +import { faker } from '@faker-js/faker'; +import * as moment from 'moment'; +import { + IDefaultUser, + RolesEnum, + ISeedUsers, + LanguagesEnum, + IRole, + ITenant, + IUser, + ComponentLayoutStyleEnum +} from '@gauzy/contracts'; +import { getEmailWithPostfix } from '../core/seeds/utils'; +import { User } from './user.entity'; +import { getUserDummyImage, Role } from '../core'; +import { E2E_SUPER_ADMINS, E2E_ADMINS, E2E_EMPLOYEES } from './e2e-users'; + +export const createE2EAdminUsers = async ( + dataSource: DataSource, + tenant: ITenant +): Promise<{ + e2eSuperAdminUsers: IUser[]; + e2eAdminUsers: IUser[]; +}> => { + // E2E Super Admin Users + const _e2eSuperAdminUsers: Promise = seedE2ESuperAdminUsers(dataSource, tenant); + // E2E Admin Users + const _e2eAdminUsers: Promise = seedE2EAdminUsers(dataSource, tenant); + + const [e2eSuperAdminUsers, e2eAdminUsers] = await Promise.all([_e2eSuperAdminUsers, _e2eAdminUsers]); + + await insertUsers(dataSource, [...e2eSuperAdminUsers, ...e2eAdminUsers]); + + return { + e2eSuperAdminUsers, + e2eAdminUsers + }; +}; + +export const createE2EEmployeesUsers = async ( + dataSource: DataSource, + tenant: ITenant +): Promise<{ e2eEmployeeUsers: IUser[] }> => { + // E2E Employee Users + const _e2eEmployeeUsers: Promise = seedE2EEmployeeUsers(dataSource, tenant, E2E_EMPLOYEES); + const [e2eEmployeeUsers] = await Promise.all([_e2eEmployeeUsers]); + + await insertUsers(dataSource, [...e2eEmployeeUsers]); + + return { + e2eEmployeeUsers + }; +}; + +const seedE2ESuperAdminUsers = async (dataSource: DataSource, tenant: ITenant): Promise => { + const superAdmins: Promise[] = []; + + const { id: tenantId } = tenant; + const superAdminRole = await dataSource.manager.findOneBy(Role, { + tenantId, + name: RolesEnum.SUPER_ADMIN + }); + + // Generate E2E super admins + for (const superAdmin of E2E_SUPER_ADMINS) { + const superAdminUser: Promise = generateE2EUser(superAdmin, superAdminRole, tenant); + superAdmins.push(superAdminUser); + } + return Promise.all(superAdmins); +}; + +const seedE2EAdminUsers = async (dataSource: DataSource, tenant: ITenant): Promise => { + const admins: Promise[] = []; + + const { id: tenantId } = tenant; + const adminRole = await dataSource.manager.findOneBy(Role, { + tenantId, + name: RolesEnum.ADMIN + }); + + // Generate E2E admins + for (const admin of E2E_ADMINS) { + const adminUser: Promise = generateE2EUser(admin, adminRole, tenant); + admins.push(adminUser); + } + return Promise.all(admins); +}; + +const seedE2EEmployeeUsers = async (dataSource: DataSource, tenant: ITenant, employees: any[]): Promise => { + const employeeUsers: Promise[] = []; + + const { id: tenantId } = tenant; + const employeeRole = await dataSource.manager.findOneBy(Role, { + tenantId, + name: RolesEnum.EMPLOYEE + }); + + // Generate E2E employee users + for (const employee of employees) { + const employeeUser: Promise = generateE2EUser(employee, employeeRole, tenant); + employeeUsers.push(employeeUser); + } + return Promise.all(employeeUsers); +}; + +const generateE2EUser = async (defaultUser: IDefaultUser, role: IRole, tenant: ITenant): Promise => { + const user = new User(); + const { firstName, lastName, email, password, imageUrl, preferredLanguage, preferredComponentLayout } = defaultUser; + + user.email = email; + user.firstName = firstName; + user.lastName = lastName; + user.username = email; + user.role = role; + user.tenant = tenant; + user.hash = await bcrypt.hash(password, 12); + user.imageUrl = imageUrl || getUserDummyImage(user); + user.preferredLanguage = preferredLanguage || LanguagesEnum.ENGLISH; + user.preferredComponentLayout = preferredComponentLayout || ComponentLayoutStyleEnum.TABLE; + user.emailVerifiedAt = moment().toDate(); + user.isActive = true; + + return user; +}; + +const insertUsers = async (dataSource: DataSource, users: IUser[]): Promise => { + for (const user of users) { + // Check if user already exists by email + const existingUser = await dataSource.manager.findOneBy(User, { + email: user.email + }); + + if (!existingUser) { + // Only insert if user doesn't exist + await dataSource.manager.save(user); + } + } +}; diff --git a/packages/core/src/lib/user/e2e-users.ts b/packages/core/src/lib/user/e2e-users.ts new file mode 100644 index 00000000000..6ed96b9f993 --- /dev/null +++ b/packages/core/src/lib/user/e2e-users.ts @@ -0,0 +1,39 @@ +import { ComponentLayoutStyleEnum, LanguagesEnum } from '@gauzy/contracts'; + +export const E2E_SUPER_ADMINS = [ + { + email: 'e2e.admin@dspot.com.pl', + password: 'admin', + firstName: 'E2E', + lastName: 'Admin', + imageUrl: 'assets/images/avatars/avatar-default.svg', + preferredLanguage: LanguagesEnum.ENGLISH, + preferredComponentLayout: ComponentLayoutStyleEnum.TABLE + } +]; + +export const E2E_ADMINS = [ + { + email: 'e2e.local.admin@dspot.com.pl', + password: 'admin', + firstName: 'E2E', + lastName: 'Local Admin', + imageUrl: 'assets/images/avatars/avatar-default.svg', + preferredLanguage: LanguagesEnum.ENGLISH, + preferredComponentLayout: ComponentLayoutStyleEnum.TABLE + } +]; + +export const E2E_EMPLOYEES = [ + { + email: 'e2e.employee@dspot.com.pl', + password: '123456', + firstName: 'E2E', + lastName: 'Employee', + imageUrl: 'assets/images/avatars/avatar-default.svg', + startedWorkOn: '2018-03-20', + employeeLevel: 'A', + preferredLanguage: LanguagesEnum.ENGLISH, + preferredComponentLayout: ComponentLayoutStyleEnum.TABLE + } +]; diff --git a/packages/core/src/lib/user/user.entity.ts b/packages/core/src/lib/user/user.entity.ts index b4a92fbf17e..c6d9521c0e3 100644 --- a/packages/core/src/lib/user/user.entity.ts +++ b/packages/core/src/lib/user/user.entity.ts @@ -4,7 +4,7 @@ import { ApiPropertyOptional } from '@nestjs/swagger'; import { JoinColumn, RelationId, JoinTable } from 'typeorm'; -// eslint-disable-next-line @typescript-eslint/no-unused-vars + import { EntityRepositoryType } from '@mikro-orm/core'; import { Exclude } from 'class-transformer'; import { IsEmail, IsEnum, IsOptional, IsString, IsUUID } from 'class-validator'; @@ -74,7 +74,7 @@ export class User extends TenantBaseEntity implements IUser { @IsOptional() @IsEmail() @ColumnIndex() - @MultiORMColumn({ nullable: true }) + @MultiORMColumn({ nullable: true, unique: true }) email?: string; @ApiPropertyOptional({ type: () => String, minLength: 4, maxLength: 12 }) From 64ae2d080c120ab429b5bf566658a9058a2aa371 Mon Sep 17 00:00:00 2001 From: Daniel Vega Date: Thu, 7 Aug 2025 17:42:53 +0200 Subject: [PATCH 2/6] Improving e2e seeding for it to work similar to regular seed, but for e2e organization --- .../Base/pages/OrganizationProjects.po.ts | 7 +- apps/gauzy-e2e/src/support/commands.ts | 5 + .../src/lib/core/seeds/seed-data.service.ts | 149 ++++++++++++++---- .../e2e-organization-teams.ts | 29 ++++ .../organization-team.seed.ts | 21 ++- .../lib/organization/default-organizations.ts | 11 ++ .../core/src/lib/tenant/default-tenants.ts | 1 + 7 files changed, 181 insertions(+), 42 deletions(-) create mode 100644 packages/core/src/lib/organization-team/e2e-organization-teams.ts diff --git a/apps/gauzy-e2e/src/support/Base/pages/OrganizationProjects.po.ts b/apps/gauzy-e2e/src/support/Base/pages/OrganizationProjects.po.ts index 1bd0ae4d665..0a49eaa38bb 100644 --- a/apps/gauzy-e2e/src/support/Base/pages/OrganizationProjects.po.ts +++ b/apps/gauzy-e2e/src/support/Base/pages/OrganizationProjects.po.ts @@ -9,7 +9,8 @@ import { verifyTextNotExisting, waitElementToHide, clickElementByText, - clickLastButton + clickLastButton, + waitElementToShowAndHide } from '../utils/util'; import { OrganizationProjectsPage } from '../pageobjects/OrganizationProjectsPageObject'; @@ -217,6 +218,10 @@ export const waitMessageToHide = () => { waitElementToHide(OrganizationProjectsPage.toastrMessageCss); }; +export const waitMessageToShowAndHide = () => { + waitElementToShowAndHide(OrganizationProjectsPage.toastrMessageCss); +}; + export const clickSaveProjectButtonWithIndex = (index: number) => { clickButtonByIndex(OrganizationProjectsPage.saveProjectButtonCss, index); }; diff --git a/apps/gauzy-e2e/src/support/commands.ts b/apps/gauzy-e2e/src/support/commands.ts index 074a6c87657..1cfbe26a724 100644 --- a/apps/gauzy-e2e/src/support/commands.ts +++ b/apps/gauzy-e2e/src/support/commands.ts @@ -153,6 +153,10 @@ export const CustomCommands = { organizationProjectsPage.clickRequestProjectButton(); organizationProjectsPage.nameInputVisible(); organizationProjectsPage.enterNameInputData(OrganizationProjectsPageData.name); + organizationProjectsPage.clickOwnerDropdown(); + organizationProjectsPage.selectOwnerDropdownOption(0); + organizationProjectsPage.clickClientsDropdown(); + organizationProjectsPage.selectClientsDropdownOption(0); organizationProjectsPage.selectEmployeeDropdownVisible(); organizationProjectsPage.clickSelectEmployeeDropdown(); if (!employeeFullName) { @@ -169,6 +173,7 @@ export const CustomCommands = { organizationProjectsPage.enterColorInputData(OrganizationProjectsPageData.color); organizationProjectsPage.saveProjectButtonVisible(); organizationProjectsPage.clickSaveProjectButton(); + organizationProjectsPage.waitMessageToShowAndHide(); }, addTask: (addTaskPage: any, AddTasksPageData: any, employeeFullName?: string) => { cy.visit('/#/pages/tasks/dashboard', { timeout: pageLoadTimeout }); diff --git a/packages/core/src/lib/core/seeds/seed-data.service.ts b/packages/core/src/lib/core/seeds/seed-data.service.ts index 0518c6497cf..26eb73d285f 100644 --- a/packages/core/src/lib/core/seeds/seed-data.service.ts +++ b/packages/core/src/lib/core/seeds/seed-data.service.ts @@ -28,8 +28,9 @@ import { createDefaultEmployees, createRandomEmployees } from '../../employee/em import { createDefaultOrganizations, createRandomOrganizations, + DEFAULT_ORGANIZATIONS, DEFAULT_EVER_ORGANIZATIONS, - DEFAULT_ORGANIZATIONS + DEFAULT_E2E_ORGANIZATIONS } from '../../organization'; import { createDefaultEmployeeDashboards, createRandomEmployeeDashboards } from '../../dashboard/dashboard.seed'; import { createDefaultIncomes, createRandomIncomes } from '../../income/income.seed'; @@ -40,8 +41,15 @@ import { } from '../../user-organization/user-organization.seed'; import { createCountries } from '../../country/country.seed'; import { createDefaultTeams, createRandomTeam } from '../../organization-team/organization-team.seed'; +import { E2E_ORGANIZATION_TEAMS } from '../../organization-team/e2e-organization-teams'; import { createRolePermissions } from '../../role-permission/role-permission.seed'; -import { createDefaultTenant, createRandomTenants, DEFAULT_EVER_TENANT, DEFAULT_TENANT } from '../../tenant'; +import { + createDefaultTenant, + createRandomTenants, + DEFAULT_EVER_TENANT, + DEFAULT_TENANT, + DEFAULT_E2E_TENANT +} from '../../tenant'; import { createDefaultTenantSetting } from './../../tenant/tenant-setting/tenant-setting.seed'; import { createDefaultEmailTemplates } from '../../email-template/email-template.seed'; import { @@ -321,6 +329,9 @@ export class SeedDataService { // Seed basic default data for default tenant await this.seedBasicDefaultData(); + // Seed default data including contacts, projects, teams, etc. + await this.seedDefaultData(); + // Seed reports related data await this.seedReportsData(); @@ -351,6 +362,9 @@ export class SeedDataService { // Seed basic default data for default tenant await this.seedBasicDefaultData(); + // Seed default data including contacts, projects, teams, etc. + await this.seedDefaultData(); + // Seed reports related data await this.seedReportsData(); @@ -371,11 +385,22 @@ export class SeedDataService { try { this.seedType = SeederTypeEnum.E2E; - // Connect to database (don't reset, use existing) + await this.cleanUpPreviousRuns(); + + // Connect to database await this.createConnection(); - // Seed minimal organization data for e2e testing - await this.seedE2EOrganizationData(); + // Reset database to start with new, fresh data + await this.resetDatabase(); + + // Seed basic default data for e2e tenant + await this.seedBasicDefaultData(); + + // Seed default data including contacts, projects, teams, etc. + await this.seedDefaultData(); + + // Seed reports related data + await this.seedReportsData(); // Disconnect to database await this.closeConnection(); @@ -486,7 +511,12 @@ export class SeedDataService { await this.tryExecute('Issue Types', createDefaultIssueTypes(this.dataSource)); // default and internal tenant - const tenantName = this.seedType !== SeederTypeEnum.DEFAULT ? DEFAULT_EVER_TENANT : DEFAULT_TENANT; + const tenantName = + this.seedType === SeederTypeEnum.E2E + ? DEFAULT_E2E_TENANT + : this.seedType !== SeederTypeEnum.DEFAULT + ? DEFAULT_EVER_TENANT + : DEFAULT_TENANT; this.tenant = (await this.tryExecute('Tenant', createDefaultTenant(this.dataSource, tenantName))) as ITenant; this.roles = await createRoles(this.dataSource, [this.tenant]); @@ -497,7 +527,11 @@ export class SeedDataService { // Tenant level inserts which only need connection, tenant, roles const organizations = - this.seedType !== SeederTypeEnum.DEFAULT ? DEFAULT_EVER_ORGANIZATIONS : DEFAULT_ORGANIZATIONS; + this.seedType === SeederTypeEnum.E2E + ? DEFAULT_E2E_ORGANIZATIONS + : this.seedType !== SeederTypeEnum.DEFAULT + ? DEFAULT_EVER_ORGANIZATIONS + : DEFAULT_ORGANIZATIONS; this.organizations = (await this.tryExecute( 'Organizations', createDefaultOrganizations(this.dataSource, this.tenant, organizations) @@ -517,15 +551,35 @@ export class SeedDataService { await this.tryExecute('Skills', createDefaultSkills(this.dataSource, this.tenant, this.defaultOrganization)); - const { defaultSuperAdminUsers, defaultAdminUsers } = await createDefaultAdminUsers( - this.dataSource, - this.tenant - ); - this.superAdminUsers.push(...(defaultSuperAdminUsers as IUser[])); + // Create users based on seed type + let defaultSuperAdminUsers: IUser[] = []; + let defaultAdminUsers: IUser[] = []; + let defaultEmployeeUsers: IUser[] = []; + + if (this.seedType === SeederTypeEnum.E2E) { + // Use E2E-specific user creation + const { e2eSuperAdminUsers, e2eAdminUsers } = await createE2EAdminUsers(this.dataSource, this.tenant); + const { e2eEmployeeUsers } = await createE2EEmployeesUsers(this.dataSource, this.tenant); + + defaultSuperAdminUsers = e2eSuperAdminUsers; + defaultAdminUsers = e2eAdminUsers; + defaultEmployeeUsers = e2eEmployeeUsers; + } else { + // Use default user creation + const { defaultSuperAdminUsers: superAdmins, defaultAdminUsers: admins } = await createDefaultAdminUsers( + this.dataSource, + this.tenant + ); + const { defaultEmployeeUsers: employees } = await createDefaultEmployeesUsers(this.dataSource, this.tenant); - const { defaultEmployeeUsers } = await createDefaultEmployeesUsers(this.dataSource, this.tenant); + defaultSuperAdminUsers = superAdmins as IUser[]; + defaultAdminUsers = admins; + defaultEmployeeUsers = employees; + } + + this.superAdminUsers.push(...defaultSuperAdminUsers); - if (this.seedType !== SeederTypeEnum.DEFAULT) { + if (this.seedType !== SeederTypeEnum.DEFAULT && this.seedType !== SeederTypeEnum.E2E) { const { defaultEverEmployeeUsers, defaultCandidateUsers } = await createDefaultUsers( this.dataSource, this.tenant @@ -540,7 +594,8 @@ export class SeedDataService { createDefaultUsersOrganizations(this.dataSource, this.tenant, this.organizations, defaultUsers) ); - const allDefaultEmployees = DEFAULT_EMPLOYEES.concat(DEFAULT_EVER_EMPLOYEES); + const allDefaultEmployees = + this.seedType === SeederTypeEnum.E2E ? E2E_EMPLOYEES : DEFAULT_EMPLOYEES.concat(DEFAULT_EVER_EMPLOYEES); //User level data that needs dataSource, tenant, organization, role, users this.defaultEmployees = await createDefaultEmployees( this.dataSource, @@ -613,22 +668,50 @@ export class SeedDataService { // Create E2E-specific users with different emails to avoid conflicts await this.createE2EUsers(); - // Create essential tags for e2e testing - const defaultTagTypes = await this.tryExecute( - 'E2E Tag Types', - createTagTypes(this.dataSource, this.tenant, [this.defaultOrganization]) - ); + // Check if tag types already exist before creating them + const existingTagTypes = await this.dataSource.getRepository('TagType').find({ + where: { tenantId: this.tenant.id } + }); - await this.tryExecute( - 'E2E Tags', - createDefaultTags(this.dataSource, this.tenant, [this.defaultOrganization], defaultTagTypes || []) - ); + if (existingTagTypes.length === 0) { + await this.tryExecute( + 'E2E Tag Types', + createTagTypes(this.dataSource, this.tenant, [this.defaultOrganization]) + ); + } else { + this.log(chalk.blue(`Tag types already exist for E2E tenant, skipping...`)); + } - // Create employee levels for e2e testing - await this.tryExecute( - 'E2E Employee Levels', - createEmployeeLevels(this.dataSource, this.tenant, [this.defaultOrganization]) - ); + // Check if tags already exist before creating them + const existingTags = await this.dataSource.getRepository('Tag').find({ + where: { tenantId: this.tenant.id } + }); + + if (existingTags.length === 0) { + const defaultTagTypes = (await this.dataSource.getRepository('TagType').find({ + where: { tenantId: this.tenant.id } + })) as any[]; + await this.tryExecute( + 'E2E Tags', + createDefaultTags(this.dataSource, this.tenant, [this.defaultOrganization], defaultTagTypes) + ); + } else { + this.log(chalk.blue(`Tags already exist for E2E tenant, skipping...`)); + } + + // Check if employee levels already exist before creating them + const existingEmployeeLevels = await this.dataSource.getRepository('EmployeeLevel').find({ + where: { tenantId: this.tenant.id } + }); + + if (existingEmployeeLevels.length === 0) { + await this.tryExecute( + 'E2E Employee Levels', + createEmployeeLevels(this.dataSource, this.tenant, [this.defaultOrganization]) + ); + } else { + this.log(chalk.blue(`Employee levels already exist for E2E tenant, skipping...`)); + } this.log(chalk.magenta(`✅ SEEDED E2E TESTING ORGANIZATION`)); } @@ -776,7 +859,13 @@ export class SeedDataService { // Employee level data that need connection, tenant, organization, role, users, employee await this.tryExecute( 'Default Teams', - createDefaultTeams(this.dataSource, this.defaultOrganization, this.defaultEmployees, this.roles) + createDefaultTeams( + this.dataSource, + this.defaultOrganization, + this.defaultEmployees, + this.roles, + this.seedType === SeederTypeEnum.E2E ? E2E_ORGANIZATION_TEAMS : undefined + ) ); this.defaultProjects = await this.tryExecute( diff --git a/packages/core/src/lib/organization-team/e2e-organization-teams.ts b/packages/core/src/lib/organization-team/e2e-organization-teams.ts new file mode 100644 index 00000000000..eb127ea9113 --- /dev/null +++ b/packages/core/src/lib/organization-team/e2e-organization-teams.ts @@ -0,0 +1,29 @@ +import { environment } from '@gauzy/config'; + +export const E2E_ORGANIZATION_TEAMS = [ + { + name: 'Employees', + defaultMembers: ['e2e.admin@dspot.com.pl', 'e2e.local.admin@dspot.com.pl', 'e2e.employee@dspot.com.pl'], + manager: ['e2e.admin@dspot.com.pl'] + }, + { + name: 'Contractors', + defaultMembers: ['e2e.employee@dspot.com.pl'], + manager: ['e2e.admin@dspot.com.pl'] + }, + { + name: 'Designers', + defaultMembers: ['e2e.employee@dspot.com.pl'], + manager: ['e2e.admin@dspot.com.pl'] + }, + { + name: 'QA', + defaultMembers: ['e2e.employee@dspot.com.pl'], + manager: ['e2e.admin@dspot.com.pl'] + }, + { + name: 'Default Team', + defaultMembers: ['e2e.employee@dspot.com.pl'], + manager: ['e2e.admin@dspot.com.pl'] + } +]; diff --git a/packages/core/src/lib/organization-team/organization-team.seed.ts b/packages/core/src/lib/organization-team/organization-team.seed.ts index e00fc5f0282..a01a46789b5 100644 --- a/packages/core/src/lib/organization-team/organization-team.seed.ts +++ b/packages/core/src/lib/organization-team/organization-team.seed.ts @@ -5,14 +5,19 @@ import { IEmployee, IOrganization, IRole, ITenant, RolesEnum } from '@gauzy/cont import * as _ from 'underscore'; import { faker } from '@faker-js/faker'; import { DEFAULT_ORGANIZATION_TEAMS } from './default-organization-teams'; +import { E2E_ORGANIZATION_TEAMS } from './e2e-organization-teams'; export const createDefaultTeams = async ( dataSource: DataSource, organization: IOrganization, employees: IEmployee[], - roles: IRole[] + roles: IRole[], + teamsConfig?: any ): Promise => { - const teams = DEFAULT_ORGANIZATION_TEAMS; + // Use provided teams config or default to DEFAULT_ORGANIZATION_TEAMS + // Note: DEFAULT_ORGANIZATION_TEAMS contains hardcoded emails like 'ruslan@example-dspot.com.pl' + // For E2E, we use E2E_ORGANIZATION_TEAMS with E2E-specific emails + const teams = teamsConfig || DEFAULT_ORGANIZATION_TEAMS; const organizationTeams: OrganizationTeam[] = []; for (let i = 0; i < teams.length; i++) { @@ -21,9 +26,7 @@ export const createDefaultTeams = async ( team.organizationId = organization.id; team.tenant = organization.tenant; - const filteredEmployees = employees.filter( - (e) => (teams[i].defaultMembers || []).indexOf(e.user.email) > -1 - ); + const filteredEmployees = employees.filter((e) => (teams[i].defaultMembers || []).indexOf(e.user.email) > -1); const teamEmployees: OrganizationTeamEmployee[] = []; @@ -33,16 +36,12 @@ export const createDefaultTeams = async ( teamEmployees.push(teamEmployee); }); - const managers = employees.filter( - (e) => (teams[i].manager || []).indexOf(e.user.email) > -1 - ); + const managers = employees.filter((e) => (teams[i].manager || []).indexOf(e.user.email) > -1); managers.forEach((emp) => { const teamEmployee = new OrganizationTeamEmployee(); teamEmployee.employeeId = emp.id; - teamEmployee.role = roles.filter( - (x) => x.name === RolesEnum.MANAGER - )[0]; + teamEmployee.role = roles.filter((x) => x.name === RolesEnum.MANAGER)[0]; teamEmployees.push(teamEmployee); }); diff --git a/packages/core/src/lib/organization/default-organizations.ts b/packages/core/src/lib/organization/default-organizations.ts index 1f5bdaf9d9c..9cae92fa7f7 100644 --- a/packages/core/src/lib/organization/default-organizations.ts +++ b/packages/core/src/lib/organization/default-organizations.ts @@ -29,3 +29,14 @@ export const DEFAULT_ORGANIZATIONS = [ totalEmployees: 1 } ]; + +export const DEFAULT_E2E_ORGANIZATIONS = [ + { + name: 'E2E Testing Organization', + currency: CurrenciesEnum.USD, + defaultValueDateType: DefaultValueDateTypeEnum.TODAY, + imageUrl: 'assets/images/logos/dspot_erp_light.svg', + isDefault: true, + totalEmployees: 1 + } +]; diff --git a/packages/core/src/lib/tenant/default-tenants.ts b/packages/core/src/lib/tenant/default-tenants.ts index 24ab53aeac5..52d46835a5f 100644 --- a/packages/core/src/lib/tenant/default-tenants.ts +++ b/packages/core/src/lib/tenant/default-tenants.ts @@ -1,2 +1,3 @@ export const DEFAULT_EVER_TENANT = 'DSpot'; export const DEFAULT_TENANT = 'Default Tenant'; +export const DEFAULT_E2E_TENANT = 'E2E Testing Tenant'; From 2e90350218c7c22ddf9431450448fc587f75073a Mon Sep 17 00:00:00 2001 From: Daniel Vega Date: Fri, 8 Aug 2025 09:29:37 +0200 Subject: [PATCH 3/6] Removing known excessive data from e2e seeding --- E2E_SEEDING.md | 20 +++--- apps/gauzy-e2e/src/e2e/ManageEmployeesTest.ts | 23 +++--- .../support/Base/pages/ManageEmployees.po.ts | 11 ++- apps/gauzy-e2e/src/support/Base/utils/util.ts | 2 +- apps/gauzy-e2e/src/support/commands.ts | 2 +- .../src/lib/core/seeds/seed-data.service.ts | 71 ++++++++++++------- packages/core/src/lib/goal/goal.seed.ts | 16 ++--- packages/core/src/lib/tasks/task.seed.ts | 17 +++-- 8 files changed, 99 insertions(+), 63 deletions(-) diff --git a/E2E_SEEDING.md b/E2E_SEEDING.md index 4e954f249c6..28bed3a6c4b 100644 --- a/E2E_SEEDING.md +++ b/E2E_SEEDING.md @@ -2,20 +2,22 @@ ## Overview -The E2E seeding approach has been modified to seed only organization-specific data within an existing database, rather than creating a separate database. This provides better isolation and efficiency for e2e testing. +This is the first implementation of dedicated E2E testing seeding. Previously, E2E tests were running against the default organization, which caused conflicts with manual testing and introduced noise depending on the environment. ## What Changed -### Before -- Created a separate database file (`gauzy-e2e.sqlite3`) -- Reset entire database and seeded all data -- Required separate database setup +### Before (No Dedicated E2E Setup) +- E2E tests ran against the default organization +- Constant conflicts with manual testing setup +- Manually induced noise depending on the environment +- No isolation between E2E tests and development data -### After -- Uses existing database -- Seeds only organization-specific data -- Creates E2E Testing Tenant and Organization +### After (First E2E Implementation) +- Dedicated E2E Testing Tenant and Organization +- Seeds only organization-specific data within existing database +- Creates E2E-specific users with different emails to avoid conflicts - Adds essential tags and employee levels for testing +- Complete isolation from development data ## How It Works diff --git a/apps/gauzy-e2e/src/e2e/ManageEmployeesTest.ts b/apps/gauzy-e2e/src/e2e/ManageEmployeesTest.ts index c4aa966794a..8246b1278b9 100644 --- a/apps/gauzy-e2e/src/e2e/ManageEmployeesTest.ts +++ b/apps/gauzy-e2e/src/e2e/ManageEmployeesTest.ts @@ -92,7 +92,7 @@ describe('Manage employees test', { testIsolation: false }, () => { it('Should be able to edit employee', () => { manageEmployeesPage.filterByTag(tagName); manageEmployeesPage.tableRowVisible(); - manageEmployeesPage.selectTableRow(0); + manageEmployeesPage.selectLastTableRow(); manageEmployeesPage.editButtonVisible(); manageEmployeesPage.clickEditButton(); manageEmployeesPage.usernameEditInputVisible(); @@ -114,7 +114,7 @@ describe('Manage employees test', { testIsolation: false }, () => { it('Should be able to end work', () => { manageEmployeesPage.waitMessageToHide(); manageEmployeesPage.filterByTag(tagName); - manageEmployeesPage.selectTableRow(0); + manageEmployeesPage.selectLastTableRow(); manageEmployeesPage.endWorkButtonVisible(); manageEmployeesPage.clickEndWorkButton(); manageEmployeesPage.confirmEndWorkButtonVisible(); @@ -123,7 +123,7 @@ describe('Manage employees test', { testIsolation: false }, () => { }); it('Should be able to delete employee', () => { manageEmployeesPage.filterByTag(tagName); - manageEmployeesPage.selectTableRow(0); + manageEmployeesPage.selectLastTableRow(); manageEmployeesPage.deleteButtonVisible(); manageEmployeesPage.clickDeleteButton(); manageEmployeesPage.confirmDeleteButtonVisible(); @@ -134,15 +134,15 @@ describe('Manage employees test', { testIsolation: false }, () => { }); it('Should be able to copy invite link', () => { manageEmployeesPage.clickManageInviteButton(); - manageEmployeesPage.selectTableRow(0); + manageEmployeesPage.selectTableRowsWithProject(ManageEmployeesPageData.defaultProject); manageEmployeesPage.copyLinkButtonVisible(); manageEmployeesPage.clickCopyLinkButton(); manageEmployeesPage.waitMessageToHide(); - manageEmployeesPage.selectTableRow(0); //unselect + manageEmployeesPage.selectLastTableRow(); //unselect }); it('Should be able to resend invite', () => { manageEmployeesPage.clickManageInviteButton(); - manageEmployeesPage.selectTableRow(0); + manageEmployeesPage.selectTableRowsWithProject(ManageEmployeesPageData.defaultProject); manageEmployeesPage.resendInviteButtonVisible(); manageEmployeesPage.clickResendInviteButton(); manageEmployeesPage.confirmResendInviteButtonVisible(); @@ -151,10 +151,11 @@ describe('Manage employees test', { testIsolation: false }, () => { }); it('Should be able to delete invite', () => { manageEmployeesPage.clickManageInviteButton(); - manageEmployeesPage.selectTableRow(0); - manageEmployeesPage.deleteInviteButtonVisible(); - manageEmployeesPage.clickDeleteInviteButton(); - manageEmployeesPage.confirmDeleteInviteButtonVisible(); - manageEmployeesPage.clickConfirmDeleteInviteButton(); + manageEmployeesPage.selectTableRowsWithProject(ManageEmployeesPageData.defaultProject); + manageEmployeesPage.deleteInviteButtonVisible(); + manageEmployeesPage.clickDeleteInviteButton(); + manageEmployeesPage.confirmDeleteInviteButtonVisible(); + manageEmployeesPage.clickConfirmDeleteInviteButton(); + manageEmployeesPage.waitMessageToHide(); }); }); diff --git a/apps/gauzy-e2e/src/support/Base/pages/ManageEmployees.po.ts b/apps/gauzy-e2e/src/support/Base/pages/ManageEmployees.po.ts index 962a4acc248..f347b8ca954 100644 --- a/apps/gauzy-e2e/src/support/Base/pages/ManageEmployees.po.ts +++ b/apps/gauzy-e2e/src/support/Base/pages/ManageEmployees.po.ts @@ -11,7 +11,8 @@ import { waitElementToHide, verifyText, verifyTextNotExisting, - clickButtonWithForce + clickButtonWithForce, + clickLastButton } from '../utils/util'; import { ManageEmployeesPage } from '../pageobjects/ManageEmployeesPageObject'; @@ -191,6 +192,14 @@ export const selectTableRow = (index) => { clickButtonByIndex(ManageEmployeesPage.selectTableRowCss, index); }; +export const selectLastTableRow = () => { + clickLastButton(ManageEmployeesPage.selectTableRowCss); +}; + +export const selectTableRowsWithProject = (projectName, index = 0) => { + cy.get(ManageEmployeesPage.selectTableRowCss).contains(projectName).eq(index).click(); +}; + export const editButtonVisible = () => { cy.get(ManageEmployeesPage.actionsBarCss).findByRole('button', { name: ManageEmployeesPage.editButtonName }); }; diff --git a/apps/gauzy-e2e/src/support/Base/utils/util.ts b/apps/gauzy-e2e/src/support/Base/utils/util.ts index 0c697d699be..dd719754e47 100644 --- a/apps/gauzy-e2e/src/support/Base/utils/util.ts +++ b/apps/gauzy-e2e/src/support/Base/utils/util.ts @@ -144,7 +144,7 @@ export const getNotEqualElement = (loc, text) => { ); }; -export const waitElementToHide = (loc, waitBefore = 10000) => { +export const waitElementToHide = (loc, waitBefore = 3000) => { cy.wait(waitBefore); cy.get(loc, { timeout: defaultCommandTimeout }).should('not.exist'); }; diff --git a/apps/gauzy-e2e/src/support/commands.ts b/apps/gauzy-e2e/src/support/commands.ts index 1cfbe26a724..1ebd2b2f26f 100644 --- a/apps/gauzy-e2e/src/support/commands.ts +++ b/apps/gauzy-e2e/src/support/commands.ts @@ -46,7 +46,7 @@ export const CustomCommands = { dashboardPage.verifyCreateButton(); }, addTag: (organizationTagsUserPage: any, OrganizationTagsPageData: any) => { - cy.visit('/#/pages/organization/tags', { timeout: pageLoadTimeout }); + cy.visitAndWait('/#/pages/organization/tags', { timeout: pageLoadTimeout }); organizationTagsUserPage.gridButtonVisible(); organizationTagsUserPage.clickGridButton(1); organizationTagsUserPage.addTagButtonVisible(); diff --git a/packages/core/src/lib/core/seeds/seed-data.service.ts b/packages/core/src/lib/core/seeds/seed-data.service.ts index 26eb73d285f..bd0db51a877 100644 --- a/packages/core/src/lib/core/seeds/seed-data.service.ts +++ b/packages/core/src/lib/core/seeds/seed-data.service.ts @@ -238,6 +238,12 @@ export enum SeederTypeEnum { E2E = 'e2e' } +export interface SeedConfig { + tasks?: number; + goals?: number; + [key: string]: any; +} + @Injectable() export class SeedDataService { dataSource: DataSource; @@ -397,7 +403,7 @@ export class SeedDataService { await this.seedBasicDefaultData(); // Seed default data including contacts, projects, teams, etc. - await this.seedDefaultData(); + await this.seedDefaultData({ tasks: 0, goals: 0 }); // Seed reports related data await this.seedReportsData(); @@ -801,7 +807,7 @@ export class SeedDataService { /** * Populate default data for default tenant */ - private async seedDefaultData() { + private async seedDefaultData(config?: SeedConfig) { this.log(chalk.magenta(`🌱 SEEDING DEFAULT ${env.production ? 'PRODUCTION' : ''} DATABASE...`)); await this.tryExecute( @@ -873,10 +879,14 @@ export class SeedDataService { createDefaultOrganizationProjects(this.dataSource, this.tenant, this.defaultOrganization) ); - await this.tryExecute( - 'Default Projects Task', - createDefaultTask(this.dataSource, this.tenant, this.defaultOrganization) - ); + // Create tasks based on config + const taskCount = config?.tasks; + if (taskCount !== 0) { + await this.tryExecute( + 'Default Projects Task', + createDefaultTask(this.dataSource, this.tenant, this.defaultOrganization, taskCount) + ); + } await this.tryExecute( 'Default Organization Departments', @@ -1006,29 +1016,33 @@ export class SeedDataService { ) ); - await this.tryExecute( - 'Default Goal KPIs', - createDefaultGoalKpi(this.dataSource, this.tenant, this.organizations, this.defaultEmployees) - ); + // Create goals based on config + const goalCount = config?.goals; + if (goalCount !== 0) { + await this.tryExecute( + 'Default Goal KPIs', + createDefaultGoalKpi(this.dataSource, this.tenant, this.organizations, this.defaultEmployees) + ); - const goals = await this.tryExecute( - 'Default Goals', - createDefaultGoals(this.dataSource, this.tenant, this.organizations, this.defaultEmployees) - ); + const goals = await this.tryExecute( + 'Default Goals', + createDefaultGoals(this.dataSource, this.tenant, this.organizations, this.defaultEmployees, goalCount) + ); - const keyResults = await this.tryExecute( - 'Default Key Results', - createDefaultKeyResults(this.dataSource, this.tenant, this.defaultEmployees, goals) - ); + const keyResults = await this.tryExecute( + 'Default Key Results', + createDefaultKeyResults(this.dataSource, this.tenant, this.defaultEmployees, goals) + ); - await this.tryExecute( - 'Default Key Result Updates', - createDefaultKeyResultUpdates(this.dataSource, this.tenant, this.defaultOrganization, keyResults) - ); + await this.tryExecute( + 'Default Key Result Updates', + createDefaultKeyResultUpdates(this.dataSource, this.tenant, this.defaultOrganization, keyResults) + ); - await this.tryExecute('Default Key Result Progress', updateDefaultKeyResultProgress(this.dataSource)); + await this.tryExecute('Default Key Result Progress', updateDefaultKeyResultProgress(this.dataSource)); - await this.tryExecute('Default Goal Progress', updateDefaultGoalProgress(this.dataSource)); + await this.tryExecute('Default Goal Progress', updateDefaultGoalProgress(this.dataSource)); + } const approvalPolicies = await this.tryExecute( 'Default Approval Policies', @@ -1945,12 +1959,13 @@ export class SeedDataService { const databaseType = getDBType(this.configService.dbConnectionOptions); switch (databaseType) { - case DatabaseTypeEnum.postgres: + case DatabaseTypeEnum.postgres: { const tables = entities.map((entity) => '"' + entity.tableName + '"'); const truncateSql = `TRUNCATE TABLE ${tables.join(',')} RESTART IDENTITY CASCADE;`; await manager.query(truncateSql); break; - case DatabaseTypeEnum.mysql: + } + case DatabaseTypeEnum.mysql: { // -- disable foreign_key_checks to avoid query failing when there is a foreign key in the table await manager.query(`SET foreign_key_checks = 0;`); for (const entity of entities) { @@ -1958,13 +1973,15 @@ export class SeedDataService { } await manager.query(`SET foreign_key_checks = 1;`); break; + } case DatabaseTypeEnum.sqlite: - case DatabaseTypeEnum.betterSqlite3: + case DatabaseTypeEnum.betterSqlite3: { await manager.query(`PRAGMA foreign_keys = OFF;`); for (const entity of entities) { await manager.query(`DELETE FROM "${entity.tableName}";`); } break; + } default: throw Error(`Unsupported database type: ${databaseType}`); } diff --git a/packages/core/src/lib/goal/goal.seed.ts b/packages/core/src/lib/goal/goal.seed.ts index 53c6eff3cf1..cd825a5c521 100644 --- a/packages/core/src/lib/goal/goal.seed.ts +++ b/packages/core/src/lib/goal/goal.seed.ts @@ -10,18 +10,18 @@ export const createDefaultGoals = async ( dataSource: DataSource, tenant: ITenant, organizations: IOrganization[], - employees: IEmployee[] + employees: IEmployee[], + count?: number ): Promise => { const defaultGoals = []; - const goalTimeFrames: GoalTimeFrame[] = await dataSource.manager.find( - GoalTimeFrame - ); - const orgTeams: OrganizationTeam[] = await dataSource.manager.find( - OrganizationTeam - ); + const goalTimeFrames: GoalTimeFrame[] = await dataSource.manager.find(GoalTimeFrame); + const orgTeams: OrganizationTeam[] = await dataSource.manager.find(OrganizationTeam); for await (const organization of organizations) { - DEFAULT_GOALS.forEach((goalData) => { + // Use count if provided, otherwise use all DEFAULT_GOALS + const goalsToCreate = count ? DEFAULT_GOALS.slice(0, count) : DEFAULT_GOALS; + + goalsToCreate.forEach((goalData) => { const goal = new Goal(); goal.name = goalData.name; goal.progress = 0; diff --git a/packages/core/src/lib/tasks/task.seed.ts b/packages/core/src/lib/tasks/task.seed.ts index 1e79159745b..e94f869913a 100644 --- a/packages/core/src/lib/tasks/task.seed.ts +++ b/packages/core/src/lib/tasks/task.seed.ts @@ -19,7 +19,12 @@ import { // GITHUB API URL export const GITHUB_API_URL = 'https://api.github.com'; -export const createDefaultTask = async (dataSource: DataSource, tenant: ITenant, organization: IOrganization) => { +export const createDefaultTask = async ( + dataSource: DataSource, + tenant: ITenant, + organization: IOrganization, + count?: number +) => { const httpService = new HttpService(); console.log(`${GITHUB_API_URL}/repos/ever-co/ever-gauzy/issues`); @@ -46,8 +51,10 @@ export const createDefaultTask = async (dataSource: DataSource, tenant: ITenant, const users = await dataSource.manager.find(User); const employees = await dataSource.manager.find(Employee); - let count = 0; - for (const issue of issues) { + let taskCount = 0; + // Limit the number of issues to process based on count parameter + const issuesToProcess = count ? issues.slice(0, count) : issues; + for (const issue of issuesToProcess) { const project = faker.helpers.arrayElement(defaultProjects); const taskNumber = await getTaskNumber(dataSource, project.id); @@ -66,13 +73,13 @@ export const createDefaultTask = async (dataSource: DataSource, tenant: ITenant, task.number = taskNumber; task.createdByUser = faker.helpers.arrayElement(users); - if (count % 2 === 0) { + if (taskCount % 2 === 0) { task.members = faker.helpers.arrayElements(employees, 5); } else { task.teams = [faker.helpers.arrayElement(teams)]; } await dataSource.manager.save(task); - count++; + taskCount++; } }; From e80bc46962ad4b9b78d8518f3ceb0453e9cba9aa Mon Sep 17 00:00:00 2001 From: Daniel Vega Date: Fri, 8 Aug 2025 10:24:42 +0200 Subject: [PATCH 4/6] Fixing TimeTracking test --- apps/gauzy-e2e/src/e2e/TimeTrackingTest.ts | 8 +++----- .../support/Base/pageobjects/TimeTrackingPageObject.ts | 9 +++++---- apps/gauzy-e2e/src/support/Base/pages/TimeTracking.po.ts | 4 ++++ 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/apps/gauzy-e2e/src/e2e/TimeTrackingTest.ts b/apps/gauzy-e2e/src/e2e/TimeTrackingTest.ts index 8e1beedf9ec..d13ea0081dd 100644 --- a/apps/gauzy-e2e/src/e2e/TimeTrackingTest.ts +++ b/apps/gauzy-e2e/src/e2e/TimeTrackingTest.ts @@ -6,13 +6,13 @@ import * as dashboardPage from '../support/Base/pages/Dashboard.po'; import { CustomCommands } from '../support/commands'; //! Expected to find element: nb-card-header > h4, but never found it. -describe.skip('Time tracking page test', () => { +describe('Time tracking page test', () => { before(() => { CustomCommands.login(loginPage, LoginPageData, dashboardPage); + timeTrackingPage.visit(); }); it('Should be able to verify time tracking page', () => { - cy.visit('/#/pages/dashboard/time-tracking'); timeTrackingPage.headerTextExist(TimeTrackingPageData.header); timeTrackingPage.topCardTextExist(TimeTrackingPageData.membersWorked); timeTrackingPage.topCardTextExist(TimeTrackingPageData.projectsWorked); @@ -20,9 +20,7 @@ describe.skip('Time tracking page test', () => { timeTrackingPage.topCardTextExist(TimeTrackingPageData.workedThisWeek); timeTrackingPage.topCardTextExist(TimeTrackingPageData.todayActivity); timeTrackingPage.topCardTextExist(TimeTrackingPageData.workedToday); - timeTrackingPage.bottomCardTextExist( - TimeTrackingPageData.recentActivities - ); + timeTrackingPage.bottomCardTextExist(TimeTrackingPageData.recentActivities); timeTrackingPage.bottomCardTextExist(TimeTrackingPageData.projects); timeTrackingPage.bottomCardTextExist(TimeTrackingPageData.tasks); timeTrackingPage.bottomCardTextExist(TimeTrackingPageData.appsUrls); diff --git a/apps/gauzy-e2e/src/support/Base/pageobjects/TimeTrackingPageObject.ts b/apps/gauzy-e2e/src/support/Base/pageobjects/TimeTrackingPageObject.ts index bf517cb63fb..7ea4d57b9bb 100644 --- a/apps/gauzy-e2e/src/support/Base/pageobjects/TimeTrackingPageObject.ts +++ b/apps/gauzy-e2e/src/support/Base/pageobjects/TimeTrackingPageObject.ts @@ -1,7 +1,7 @@ export const TimeTrackingPage = { - headerTextCss: 'nb-card-header > h4', - topCardHeaderTextCss: 'div.col-sm-4 > nb-card > nb-card-body > p', - bottomCardHeaderTextCss: 'div.col-sm-6 > nb-card > nb-card-header', + headerTextCss: 'nb-card-header h4', + topCardHeaderTextCss: 'nb-card-body .title', + bottomCardHeaderTextCss: 'nb-card nb-card-header', recentActivitiesEmployeeCss: 'div.hour-lable > ngx-avatar > div.row > div.col > div.ng-star-inserted', projectsCss: 'nb-list > nb-lit-item > div.w-100 > div.row > div[class="col text-left"]', membersInfoCss: 'div.col > ngx-avatar > div[class="row align-items-center"] > div.col > div.ng-star-inserted', @@ -24,7 +24,8 @@ export const TimeTrackingPage = { addTimeBtnCss: 'div.time-manual > button[class="btn btn-success"]', todaySessionCss: 'div.time-count > span[class="today-time mt-2"]', tabBtnCss: 'li.route-tab > a.tab-link', - verifyWorkCss: 'nb-list[role="list"] > nb-list-item[role="listitem"] > div.w-100 > div[class="row align-items-center"] > div[class="col text-left"]', + verifyWorkCss: + 'nb-list[role="list"] > nb-list-item[role="listitem"] > div.w-100 > div[class="row align-items-center"] > div[class="col text-left"]', verifyTimeCss: 'div[class="col"] > nb-card > nb-card-body > div.h1', toastrMessageCss: 'nb-toast.ng-trigger', verifyManualTimeCss: 'div[class="row border-bottom py-3 align-items-center ng-star-inserted"] > div.col', diff --git a/apps/gauzy-e2e/src/support/Base/pages/TimeTracking.po.ts b/apps/gauzy-e2e/src/support/Base/pages/TimeTracking.po.ts index 8167ad1a3d2..06f9fcde1e4 100644 --- a/apps/gauzy-e2e/src/support/Base/pages/TimeTracking.po.ts +++ b/apps/gauzy-e2e/src/support/Base/pages/TimeTracking.po.ts @@ -12,6 +12,10 @@ import { } from '../utils/util'; import { TimeTrackingPage } from '../pageobjects/TimeTrackingPageObject'; +export const visit = (options = {}) => { + cy.visit('/pages/dashboard/time-tracking', options); +}; + export const headerTextExist = (text) => { verifyText(TimeTrackingPage.headerTextCss, text); }; From 12eaacee8d2e45c0989ac8d828b9bb3a47863cff Mon Sep 17 00:00:00 2001 From: Daniel Vega Date: Fri, 8 Aug 2025 13:40:12 +0200 Subject: [PATCH 5/6] Fixing TimeOffTest test --- apps/gauzy-e2e/src/e2e/TimeOffTest.ts | 59 ++++------ .../support/Base/pagedata/TimeOffPageData.ts | 2 +- .../Base/pageobjects/TimeOffPageObject.ts | 20 +++- .../src/support/Base/pages/TimeOff.po.ts | 105 ++++++++++++------ .../TimeOffTest/TimeOffTest.ts | 2 +- 5 files changed, 111 insertions(+), 77 deletions(-) diff --git a/apps/gauzy-e2e/src/e2e/TimeOffTest.ts b/apps/gauzy-e2e/src/e2e/TimeOffTest.ts index 091027007ac..e1430cec07e 100644 --- a/apps/gauzy-e2e/src/e2e/TimeOffTest.ts +++ b/apps/gauzy-e2e/src/e2e/TimeOffTest.ts @@ -15,7 +15,7 @@ let employeeEmail = ' '; let imgUrl = ' '; //! Expected to find element: div.col-4 > button[status="primary"], but never found it. -describe.skip('Time Off test', () => { +describe('Time Off test', { testIsolation: false }, () => { before(() => { firstName = faker.person.firstName(); lastName = faker.person.lastName(); @@ -25,18 +25,10 @@ describe.skip('Time Off test', () => { imgUrl = faker.image.avatar(); CustomCommands.login(loginPage, LoginPageData, dashboardPage); + CustomCommands.addEmployee(manageEmployeesPage, firstName, lastName, username, employeeEmail, password, imgUrl); + timeOffPage.visit(); }); it('Should be able to create new time off request', () => { - CustomCommands.addEmployee( - manageEmployeesPage, - firstName, - lastName, - username, - employeeEmail, - password, - imgUrl - ); - cy.visit('/#/pages/employees/time-off'); timeOffPage.requestButtonVisible(); timeOffPage.clickRequestButton(); timeOffPage.employeeSelectorVisible(); @@ -52,9 +44,7 @@ describe.skip('Time Off test', () => { timeOffPage.endDateInputVisible(); timeOffPage.enterEndDateData(); timeOffPage.descriptionInputVisible(); - timeOffPage.enterDescriptionInputData( - TimeOffPageData.defaultDescription - ); + timeOffPage.enterDescriptionInputData(TimeOffPageData.defaultDescription); timeOffPage.saveRequestButtonVisible(); timeOffPage.clickSaveRequestButton(); timeOffPage.waitMessageToHide(); @@ -63,47 +53,35 @@ describe.skip('Time Off test', () => { it('Should be able to DENY time off request', () => { timeOffPage.timeOffTableRowVisible(); timeOffPage.selectTimeOffTableRow(0); + timeOffPage.clickSeeMoreButtonIfVisible(); timeOffPage.denyTimeOffButtonVisible(); timeOffPage.clickDenyTimeOffButton(); timeOffPage.clickDenyTimeOffButton(); + timeOffPage.waitMessageToHide(); + timeOffPage.verifyRowIsDenied(0); }); it('Should be able to APPROVE time off request', () => { - timeOffPage.waitMessageToHide(); + timeOffPage.timeOffTableRowVisible(); + timeOffPage.selectTimeOffTableRow(0); + timeOffPage.clickSeeMoreButtonIfVisible(); timeOffPage.approveTimeOffButtonVisible(); timeOffPage.clickApproveTimeOffButton(); - timeOffPage.clickApproveTimeOffButton(); - timeOffPage.requestButtonVisible(); - timeOffPage.clickRequestButton(); - timeOffPage.employeeSelectorVisible(); - timeOffPage.clickEmployeeSelector(); - timeOffPage.employeeDropdownVisible(); - timeOffPage.selectEmployeeFromDropdown(1); - timeOffPage.selectTimeOffPolicyVisible(); - timeOffPage.clickTimeOffPolicyDropdown(); - timeOffPage.timeOffPolicyDropdownOptionVisible(); - timeOffPage.selectTimeOffPolicy(TimeOffPageData.defaultPolicy); - timeOffPage.startDateInputVisible(); - timeOffPage.enterStartDateData(); - timeOffPage.endDateInputVisible(); - timeOffPage.enterEndDateData(); - timeOffPage.descriptionInputVisible(); - timeOffPage.enterDescriptionInputData( - TimeOffPageData.defaultDescription - ); - timeOffPage.saveRequestButtonVisible(); - timeOffPage.clickSaveRequestButton(); + timeOffPage.waitMessageToHide(); + timeOffPage.verifyRowIsApproved(0); }); it('Should be able to delete time off request', () => { - timeOffPage.waitMessageToHide(); + timeOffPage.countRows(); timeOffPage.selectTimeOffTableRow(0); timeOffPage.deleteTimeOffBtnVisible(); timeOffPage.clickDeleteTimeOffButton(); timeOffPage.confirmDeleteTimeOffBtnVisible(); timeOffPage.clickConfirmDeleteTimeOffButton(); + timeOffPage.waitMessageToHide(); + timeOffPage.verifyARowWasDeleted(); }); it('Should be able to add holiday', () => { - timeOffPage.addHolidayButtonVisible(); - timeOffPage.clickAddHolidayButton(); + timeOffPage.addHolidaysButtonVisible(); + timeOffPage.clickAddHolidaysButton(); timeOffPage.selectHolidayNameVisible(); timeOffPage.clickSelectHolidayName(); timeOffPage.selectHolidayOption(TimeOffPageData.defaultHoliday); @@ -122,10 +100,11 @@ describe.skip('Time Off test', () => { timeOffPage.clickKeyboardButtonByKeyCode(9); timeOffPage.saveButtonVisible(); timeOffPage.clickSaveButton(); + timeOffPage.waitMessageToHide(); }); it('Should be able to add new policy', () => { timeOffPage.timeOffSettingsButtonVisible(); - timeOffPage.clickTimeOffSettingsButton(1); + timeOffPage.clickTimeOffSettingsButton(); timeOffPage.addNewPolicyButtonVisible(); timeOffPage.clickAddNewPolicyButton(); timeOffPage.policyInputFieldVisible(); diff --git a/apps/gauzy-e2e/src/support/Base/pagedata/TimeOffPageData.ts b/apps/gauzy-e2e/src/support/Base/pagedata/TimeOffPageData.ts index 4b0c0e79e41..6b73360b9a7 100644 --- a/apps/gauzy-e2e/src/support/Base/pagedata/TimeOffPageData.ts +++ b/apps/gauzy-e2e/src/support/Base/pagedata/TimeOffPageData.ts @@ -1,6 +1,6 @@ export const TimeOffPageData = { defaultPolicy: 'Default Policy', defaultDescription: 'Going to the sea', - defaultHoliday: 'Нова Година', + defaultHoliday: "New Year's Day", addNewPolicyData: 'TEST' }; diff --git a/apps/gauzy-e2e/src/support/Base/pageobjects/TimeOffPageObject.ts b/apps/gauzy-e2e/src/support/Base/pageobjects/TimeOffPageObject.ts index c3659e3265a..6e449531355 100644 --- a/apps/gauzy-e2e/src/support/Base/pageobjects/TimeOffPageObject.ts +++ b/apps/gauzy-e2e/src/support/Base/pageobjects/TimeOffPageObject.ts @@ -1,3 +1,5 @@ +const ACTIONS_BAR_CSS = '.actions-container'; + export const TimeOffPage = { requestButtonCss: 'div.col-4 > button[status="primary"]', employeeDropdownCss: 'ngx-time-off-request-mutation > nb-card.main ga-employee-selector.employees', @@ -10,18 +12,19 @@ export const TimeOffPage = { saveRequestButtonCss: 'nb-card-footer.text-right > button[status="success"]', addHolidayButtonCss: 'div.col-4 > button[status="info"]', selectHolidayDropdownOptionCss: '.option-list nb-option', - selectEmployeeCss: 'button[class="select-button placeholder"]', + selectEmployeeCss: '[ng-reflect-placeholder="Add or Remove Employees"] button', selectEmployeeDropdownOptionCss: '.option-list nb-option', startHolidayDateCss: '[formControlName="start"]', endHolidayDateCss: '[formControlName="end"]', saveButtonCss: 'nb-card-footer > button[status="success"]', selectTableRowCss: 'table > tbody > tr.angular2-smart-row', - deleteTimeOfRequestButtonCss: 'div.actions-container > button[status="danger"]', + deleteTimeOfRequestButtonCss: `${ACTIONS_BAR_CSS} button:has([icon*="trash"])`, confirmDeleteTimeOfButtonCss: 'nb-card-footer > button[status="danger"]', + seeMoreButtonCss: `${ACTIONS_BAR_CSS} button:has([icon*="more-horizontal"])`, editTimeOfRequestButtonCss: 'div.actions-container > button[status="primary"]', denyTimeOffRequestButtonCss: 'div.actions-container > button[status="warning"]', approveTimeOffRequestButtonCss: 'div.actions-container > button[status="success"]', - timeOffSettingsButtonCss: 'div.mb-3 > div > button[status="primary"]', + timeOffSettingsButtonCss: '.card-header-title button[routerLink*="settings"]', addNewPolicyButtonCss: 'div.mb-3 > button[status="success"]', editPolicyButtonCss: 'div.mb-3 > button[status="info"]', deletePolicyButtonCss: 'div.mb-3 > button[status="danger"]', @@ -31,5 +34,14 @@ export const TimeOffPage = { verifyPolicyCss: 'div.ng-star-inserted', holidayNameSelectCss: 'nb-select[ng-reflect-placeholder="Select Holiday name"]', employeeSelectorCss: 'nb-select[ng-reflect-placeholder="Add or Remove Employees"]', - timeOffPolicySelectorCss: 'nb-select[ng-reflect-placeholder="Select Time-off Policy"]' + timeOffPolicySelectorCss: 'nb-select[ng-reflect-placeholder="Select Time-off Policy"]', + // testing library + actionsBarCss: ACTIONS_BAR_CSS, + dialogCardActionsCss: 'nb-dialog-container nb-card-footer', + requestTimeOffButtonName: 'Add', + addNewPolicyButtonName: 'Add', + saveButtonName: 'Save', + denyButtonName: 'Deny', + approveButtonName: 'Approve', + addHolidaysButtonName: 'Add Holidays' }; diff --git a/apps/gauzy-e2e/src/support/Base/pages/TimeOff.po.ts b/apps/gauzy-e2e/src/support/Base/pages/TimeOff.po.ts index 1cab353d2c3..1ab56175f2a 100644 --- a/apps/gauzy-e2e/src/support/Base/pages/TimeOff.po.ts +++ b/apps/gauzy-e2e/src/support/Base/pages/TimeOff.po.ts @@ -10,23 +10,29 @@ import { waitElementToHide, verifyText, verifyTextNotExisting, - clickButtonDouble + clickButtonDouble, + waitElementToShowAndHide } from '../utils/util'; import { TimeOffPage } from '../pageobjects/TimeOffPageObject'; +export const visit = (options = {}) => { + cy.intercept('GET', '/api/time-off-request/*').as('getTimeOffRequest'); + cy.visit('/#/pages/employees/time-off', options); + cy.wait('@getTimeOffRequest'); +}; + export const requestButtonVisible = () => { - verifyElementIsVisible(TimeOffPage.requestButtonCss); + cy.get(TimeOffPage.actionsBarCss).contains(TimeOffPage.requestTimeOffButtonName).should('be.visible'); }; export const clickRequestButton = () => { - clickButton(TimeOffPage.requestButtonCss); + cy.get(TimeOffPage.actionsBarCss).contains(TimeOffPage.requestTimeOffButtonName).click(); }; export const employeeSelectorVisible = () => { cy.intercept('GET', '/api/employee/working*').as('getUsersXhr'); verifyElementIsVisible(TimeOffPage.employeeDropdownCss); cy.wait('@getUsersXhr'); - }; export const clickEmployeeSelector = () => { @@ -75,7 +81,8 @@ export const endDateInputVisible = () => { export const enterEndDateData = () => { clearField(TimeOffPage.endDateInputCss); const date = dayjs().add(5, 'days').format('MMM D, YYYY'); - enterInput(TimeOffPage.endDateInputCss, date); + enterInput(TimeOffPage.endDateInputCss, `${date}`); + cy.press(Cypress.Keyboard.Keys.TAB); }; export const descriptionInputVisible = () => { @@ -88,20 +95,20 @@ export const enterDescriptionInputData = (data) => { }; export const saveRequestButtonVisible = () => { - verifyElementIsVisible(TimeOffPage.saveRequestButtonCss); + cy.get(TimeOffPage.dialogCardActionsCss).contains(TimeOffPage.saveButtonName).should('be.visible'); }; export const clickSaveRequestButton = () => { - clickButton(TimeOffPage.saveRequestButtonCss); + cy.get(TimeOffPage.dialogCardActionsCss).contains(TimeOffPage.saveButtonName).click(); }; -export const addHolidayButtonVisible = () => { - verifyElementIsVisible(TimeOffPage.addHolidayButtonCss); +export const addHolidaysButtonVisible = () => { + cy.get(TimeOffPage.actionsBarCss).contains(TimeOffPage.addHolidaysButtonName).should('be.visible'); }; -export const clickAddHolidayButton = () => { +export const clickAddHolidaysButton = () => { cy.intercept('GET', '/api/employee*').as('waitForm'); - clickButton(TimeOffPage.addHolidayButtonCss); + cy.get(TimeOffPage.actionsBarCss).contains(TimeOffPage.addHolidaysButtonName).click(); cy.wait('@waitForm'); }; @@ -113,8 +120,8 @@ export const clickSelectHolidayName = () => { clickButton(TimeOffPage.holidayNameSelectCss); }; -export const selectHolidayOption = (index) => { - clickButtonByIndex(TimeOffPage.selectHolidayDropdownOptionCss, index); +export const selectHolidayOption = (text) => { + clickElementByText(TimeOffPage.selectHolidayDropdownOptionCss, text); }; export const selectEmployeeDropdownVisible = () => { @@ -135,10 +142,7 @@ export const startHolidayDateInputVisible = () => { export const enterStartHolidayDate = () => { clearField(TimeOffPage.startHolidayDateCss); - const date = dayjs() - .add(1, 'years') - .startOf('year') - .format('MMM D, YYYY'); + const date = dayjs().add(1, 'years').startOf('year').format('MMM D, YYYY'); enterInput(TimeOffPage.startHolidayDateCss, date); }; @@ -148,11 +152,7 @@ export const endHolidayDateInputVisible = () => { export const enterEndHolidayDate = () => { clearField(TimeOffPage.endHolidayDateCss); - const date = dayjs() - .add(1, 'years') - .startOf('year') - .add(1, 'days') - .format('MMM D, YYYY'); + const date = dayjs().add(1, 'years').startOf('year').add(1, 'days').format('MMM D, YYYY'); enterInput(TimeOffPage.endHolidayDateCss, date); }; @@ -192,20 +192,36 @@ export const clickDeleteTimeOffButton = () => { clickButton(TimeOffPage.deleteTimeOfRequestButtonCss); }; +export const seeMoreButtonVisible = () => { + verifyElementIsVisible(TimeOffPage.seeMoreButtonCss); +}; + +export const clickSeeMoreButton = () => { + clickButton(TimeOffPage.seeMoreButtonCss); +}; +export const clickSeeMoreButtonIfVisible = () => { + cy.document().then((doc) => { + const seeMoreButton = doc.querySelector(TimeOffPage.seeMoreButtonCss); + if (seeMoreButton) { + clickButton(seeMoreButton); + } + }); +}; + export const denyTimeOffButtonVisible = () => { - verifyElementIsVisible(TimeOffPage.denyTimeOffRequestButtonCss); + cy.get(TimeOffPage.actionsBarCss).contains(TimeOffPage.denyButtonName).should('be.visible'); }; export const clickDenyTimeOffButton = () => { - clickButton(TimeOffPage.denyTimeOffRequestButtonCss); + cy.get(TimeOffPage.actionsBarCss).contains(TimeOffPage.denyButtonName).click(); }; export const approveTimeOffButtonVisible = () => { - verifyElementIsVisible(TimeOffPage.approveTimeOffRequestButtonCss); + cy.get(TimeOffPage.actionsBarCss).contains(TimeOffPage.approveButtonName).should('be.visible'); }; export const clickApproveTimeOffButton = () => { - clickButton(TimeOffPage.approveTimeOffRequestButtonCss); + cy.get(TimeOffPage.actionsBarCss).contains(TimeOffPage.approveButtonName).click(); }; export const confirmDeleteTimeOffBtnVisible = () => { @@ -220,16 +236,16 @@ export const timeOffSettingsButtonVisible = () => { verifyElementIsVisible(TimeOffPage.timeOffSettingsButtonCss); }; -export const clickTimeOffSettingsButton = (index) => { - clickButtonByIndex(TimeOffPage.timeOffSettingsButtonCss, index); +export const clickTimeOffSettingsButton = () => { + clickButton(TimeOffPage.timeOffSettingsButtonCss); }; export const addNewPolicyButtonVisible = () => { - verifyElementIsVisible(TimeOffPage.addNewPolicyButtonCss); + cy.get(TimeOffPage.actionsBarCss).contains(TimeOffPage.addNewPolicyButtonName).should('be.visible'); }; export const clickAddNewPolicyButton = () => { - clickButton(TimeOffPage.addNewPolicyButtonCss); + cy.get(TimeOffPage.actionsBarCss).contains(TimeOffPage.addNewPolicyButtonName).click(); }; export const policyInputFieldVisible = () => { @@ -242,7 +258,7 @@ export const enterNewPolicyName = (data) => { }; export const waitMessageToHide = () => { - waitElementToHide(TimeOffPage.toastrMessageCss); + waitElementToShowAndHide(TimeOffPage.toastrMessageCss); }; export const verifyPolicyExists = (text) => { @@ -273,6 +289,20 @@ export const verifyTimeOffPolicyVisible = () => { verifyElementIsVisible(TimeOffPage.timeOffPolicySelectorCss); }; +export const verifyRowIsDenied = (index) => { + cy.get(TimeOffPage.selectTableRowCss) + .eq(index) + .contains(/denied/i) + .should('be.visible'); +}; + +export const verifyRowIsApproved = (index) => { + cy.get(TimeOffPage.selectTableRowCss) + .eq(index) + .contains(/approved/i) + .should('be.visible'); +}; + export const clickTimeOffPolicySelector = () => { clickButton(TimeOffPage.timeOffPolicySelectorCss); }; @@ -280,3 +310,16 @@ export const clickTimeOffPolicySelector = () => { export const employeeSelectorVisibleAgain = () => { verifyElementIsVisible(TimeOffPage.employeeDropdownCss); }; + +export const countRows = () => { + cy.document().then((doc) => { + const rows = doc.querySelectorAll(TimeOffPage.selectTableRowCss); + cy.wrap(rows.length).as('lastRowsCount'); + }); +}; + +export const verifyARowWasDeleted = () => { + cy.get('@lastRowsCount').then((lastRowsCount: any) => { + cy.get(TimeOffPage.selectTableRowCss).should('have.length', lastRowsCount - 1); + }); +}; diff --git a/apps/gauzy-e2e/src/support/step_definitions/TimeOffTest/TimeOffTest.ts b/apps/gauzy-e2e/src/support/step_definitions/TimeOffTest/TimeOffTest.ts index 3dc52c48a44..00080043300 100644 --- a/apps/gauzy-e2e/src/support/step_definitions/TimeOffTest/TimeOffTest.ts +++ b/apps/gauzy-e2e/src/support/step_definitions/TimeOffTest/TimeOffTest.ts @@ -323,7 +323,7 @@ And('User can see add holiday button', () => { }); When('User click on add holiday button', () => { - timeOffPage.clickAddHolidayButton(); + timeOffPage.clickAddHolidaysButton(); }); Then('User can see holiday name select', () => { From d419b0991c68ccd732db994ca1eeffec5d6e6802 Mon Sep 17 00:00:00 2001 From: Daniel Vega Date: Fri, 8 Aug 2025 16:43:01 +0200 Subject: [PATCH 6/6] Adding e2e restart script (cleanup + seed) --- E2E_SEEDING.md | 75 ++++++++ apps/api/src/cleanup-e2e.ts | 21 ++ apps/api/src/reset-e2e.ts | 22 +++ package.json | 2 + .../src/lib/core/seeds/seed-data.service.ts | 182 ++++++++++++++++++ packages/core/src/lib/core/seeds/seed.ts | 102 ++++++---- 6 files changed, 365 insertions(+), 39 deletions(-) create mode 100644 apps/api/src/cleanup-e2e.ts create mode 100644 apps/api/src/reset-e2e.ts diff --git a/E2E_SEEDING.md b/E2E_SEEDING.md index 28bed3a6c4b..c697ec07536 100644 --- a/E2E_SEEDING.md +++ b/E2E_SEEDING.md @@ -62,6 +62,79 @@ yarn seed:e2e E2E_TESTING=true yarn start:api ``` +## Reset Functionality + +### Why Reset is Needed +Sometimes you need to completely reset the E2E testing environment to ensure a clean state. This is especially useful when: +- Tests have modified data that affects other tests +- You want to ensure a fresh start for testing +- Debugging test issues that might be caused by accumulated data + +### Reset Command +The most efficient way to reset E2E data is using the combined reset command: + +```bash +# Reset E2E data (cleanup + seed in one operation) +yarn reset:e2e +``` + +### How Reset Works +The reset command: +1. **Cleans up existing E2E data** (organization, tenant, users, all related entities) +2. **Seeds fresh E2E data** (new organization, users, essential data) +3. **Runs internally** without rebuilding dependencies multiple times + +### Individual Commands +You can also run cleanup and seed separately if needed: + +```bash +# Clean up existing E2E data only +yarn cleanup:e2e + +# Seed fresh E2E data only +yarn seed:e2e +``` + +### Performance Benefits +- **Reset**: Single dependency build, single database connection +- **Separate commands**: Multiple dependency builds, multiple database connections + +## Cleanup + +### Why Cleanup is Complex +Removing an organization and all its related data is complex due to the many database relationships. The E2E organization has relationships with: +- 60+ organization-scoped entities (extending `TenantOrganizationBaseEntity`) +- User relationships (default organization, last organization) +- Many-to-many relationships with tags, skills, etc. +- Cascade relationships that need to be handled properly + +### Cleanup Script +A comprehensive cleanup script is available to safely remove all E2E testing data: + +```bash +# Remove all E2E testing data +yarn cleanup:e2e +``` + +The cleanup script will: +1. **Delete organization-scoped entities**: Removes all data from 60+ entities that belong to the E2E organization +2. **Delete E2E-specific users**: Removes the three E2E users with their unique emails +3. **Delete the E2E organization**: Removes the organization itself +4. **Delete the E2E tenant**: Only if no other organizations exist for that tenant + +### What Gets Cleaned Up +- ✅ E2E Testing Organization +- ✅ E2E Testing Tenant (if no other organizations) +- ✅ E2E-specific users (`e2e.admin@dspot.com.pl`, `e2e.local.admin@dspot.com.pl`, `e2e.employee@dspot.com.pl`) +- ✅ All organization-scoped data (tasks, timesheets, activities, etc.) +- ✅ All related entities (tags, skills, projects, teams, etc.) + +### Safety Features +- **Idempotent**: Can be run multiple times safely +- **Selective**: Only removes E2E-specific data +- **Logging**: Provides detailed logs of what was deleted +- **Error handling**: Continues even if some entities fail to delete + ## Benefits 1. **No Database Conflicts**: Uses existing database, no separate files @@ -69,6 +142,8 @@ E2E_TESTING=true yarn start:api 3. **Better Isolation**: Organization-level isolation 4. **Reusable**: Can run multiple times safely 5. **CI/CD Friendly**: Simple one-command seeding +6. **Complete Cleanup**: Comprehensive cleanup script available +7. **Efficient Reset**: Combined cleanup and seed in one operation ## Data Created diff --git a/apps/api/src/cleanup-e2e.ts b/apps/api/src/cleanup-e2e.ts new file mode 100644 index 00000000000..cdac4d8d4ae --- /dev/null +++ b/apps/api/src/cleanup-e2e.ts @@ -0,0 +1,21 @@ +import { Logger } from '@nestjs/common'; +import { cleanupE2E } from '@gauzy/core'; +import { pluginConfig } from './plugin-config'; + +const logger = new Logger('GZY - CleanupE2E'); + +/** + * E2E Cleanup Script + * + * This script removes all E2E testing data from the database: + * - E2E Testing Organization and all its related data + * - E2E Testing Tenant (if no other organizations exist) + * - E2E-specific users + * - All organization-scoped data for E2E organization + * + * Usage: yarn cleanup:e2e + */ +cleanupE2E(pluginConfig).catch((error) => { + logger.error(`Error cleaning up e2e data: ${error}`); + process.exit(1); +}); diff --git a/apps/api/src/reset-e2e.ts b/apps/api/src/reset-e2e.ts new file mode 100644 index 00000000000..fc87bb59ae4 --- /dev/null +++ b/apps/api/src/reset-e2e.ts @@ -0,0 +1,22 @@ +import { Logger } from '@nestjs/common'; +import { resetE2E } from '@gauzy/core'; +import { pluginConfig } from './plugin-config'; + +const logger = new Logger('GZY - ResetE2E'); + +/** + * E2E Reset Script + * + * This script resets all E2E testing data by: + * 1. Cleaning up existing E2E data (organization, tenant, users, etc.) + * 2. Seeding fresh E2E data + * + * This is more efficient than running cleanup and seed separately + * as it doesn't require rebuilding dependencies multiple times. + * + * Usage: yarn reset:e2e + */ +resetE2E(pluginConfig).catch((error) => { + logger.error(`Error resetting e2e data: ${error}`); + process.exit(1); +}); diff --git a/package.json b/package.json index 9a6ab68630e..4746899cd95 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,8 @@ "seed:all": "yarn seed:base ./apps/api/src/seed-all.ts", "seed:jobs": "yarn seed:base ./apps/api/src/seed-jobs.ts", "seed:e2e": "yarn seed:base ./apps/api/src/seed-e2e.ts", + "cleanup:e2e": "yarn seed:base ./apps/api/src/cleanup-e2e.ts", + "reset:e2e": "yarn seed:base ./apps/api/src/reset-e2e.ts", "seed:module": "yarn seed:base ./apps/api/src/seed-module.ts --name", "seed:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn run build:package:api:prod && yarn run config:prod && yarn ts-node -r tsconfig-paths/register --project apps/api/tsconfig.app.json ./apps/api/src/seed.ts", "prebuild": "rimraf dist coverage", diff --git a/packages/core/src/lib/core/seeds/seed-data.service.ts b/packages/core/src/lib/core/seeds/seed-data.service.ts index bd0db51a877..5cc067c5bd3 100644 --- a/packages/core/src/lib/core/seeds/seed-data.service.ts +++ b/packages/core/src/lib/core/seeds/seed-data.service.ts @@ -417,6 +417,188 @@ export class SeedDataService { } } + /** + * Cleanup E2E Testing Data + * Removes all E2E testing data from the database + */ + public async runE2ECleanup() { + try { + this.log(chalk.yellow('🧹 STARTING E2E CLEANUP...')); + + // Connect to database + await this.createConnection(); + + // Find E2E organization + const e2eOrganization = await this.dataSource.getRepository('Organization').findOne({ + where: { name: 'E2E Testing Organization' } + }); + + if (!e2eOrganization) { + this.log(chalk.blue('No E2E Testing Organization found. Nothing to cleanup.')); + return; + } + + this.log(chalk.magenta(`Found E2E Testing Organization: ${e2eOrganization.id}`)); + + // Find E2E tenant + const e2eTenant = await this.dataSource.getRepository('Tenant').findOne({ + where: { name: 'E2E Testing Tenant' } + }); + + if (!e2eTenant) { + this.log(chalk.blue('No E2E Testing Tenant found.')); + return; + } + + this.log(chalk.magenta(`Found E2E Testing Tenant: ${e2eTenant.id}`)); + + // List of entities that extend TenantOrganizationBaseEntity (organization-scoped) + const organizationScopedEntities = [ + 'Activity', + 'ApprovalPolicy', + 'Changelog', + 'DailyPlan', + 'EmployeeNotificationSetting', + 'EquipmentSharingPolicy', + 'HelpCenter', + 'HelpCenterArticle', + 'HelpCenterAuthor', + 'IntegrationTenant', + 'Invite', + 'InvoiceItem', + 'IssueType', + 'JobPreset', + 'JobSearchCategory', + 'JobSearchOccupation', + 'Merchant', + 'OrganizationAward', + 'OrganizationEmploymentType', + 'OrganizationLanguage', + 'OrganizationPosition', + 'OrganizationProject', + 'OrganizationProjectEmployee', + 'OrganizationProjectModule', + 'OrganizationProjectModuleEmployee', + 'OrganizationRecurringExpense', + 'OrganizationSprint', + 'OrganizationTaskSetting', + 'OrganizationTeam', + 'OrganizationTeamEmployee', + 'OrganizationTeamJoinRequest', + 'OrganizationVendor', + 'Payment', + 'Pipeline', + 'PipelineStage', + 'Plugin', + 'PluginInstallation', + 'PluginSource', + 'PluginVersion', + 'ProductOption', + 'ProductOptionGroup', + 'ProductOptionGroupTranslation', + 'ProductOptionTranslation', + 'ProductReview', + 'ProductVariant', + 'ProductVariantPrice', + 'ProductVariantSetting', + 'Proposal', + 'Reaction', + 'ReportOrganization', + 'RequestApproval', + 'RequestApprovalEmployee', + 'RequestApprovalTeam', + 'Screenshot', + 'ScreeningTask', + 'Skill', + 'Task', + 'TaskEstimation', + 'TaskLinkedIssue', + 'TaskPriority', + 'TaskRelatedIssueType', + 'TaskSize', + 'TaskStatus', + 'TaskVersion', + 'TaskView', + 'TimeLog', + 'TimeOffPolicy', + 'TimeOffRequest', + 'TimeSlot', + 'TimeSlotMinute', + 'Timesheet', + 'UserOrganization', + 'Video', + 'Warehouse', + 'WarehouseProduct', + 'WarehouseProductVariant', + 'ZapierWebhookSubscription' + ]; + + // Delete organization-scoped entities + this.log(chalk.yellow('Deleting organization-scoped entities...')); + for (const entityName of organizationScopedEntities) { + try { + const count = await this.dataSource.getRepository(entityName).delete({ + organizationId: e2eOrganization.id + }); + if (count.affected > 0) { + this.log(chalk.green(`Deleted ${count.affected} records from ${entityName}`)); + } + } catch (error) { + this.log(chalk.yellow(`Could not delete from ${entityName}: ${error.message}`)); + } + } + + // Delete E2E-specific users + this.log(chalk.yellow('Deleting E2E-specific users...')); + const e2eUserEmails = [ + 'e2e.admin@dspot.com.pl', + 'e2e.local.admin@dspot.com.pl', + 'e2e.employee@dspot.com.pl' + ]; + + for (const email of e2eUserEmails) { + try { + const user = await this.dataSource.getRepository('User').findOne({ + where: { email } + }); + if (user) { + await this.dataSource.getRepository('User').delete(user.id); + this.log(chalk.green(`Deleted user: ${email}`)); + } + } catch (error) { + this.log(chalk.yellow(`Could not delete user ${email}: ${error.message}`)); + } + } + + // Delete the E2E organization + this.log(chalk.yellow('Deleting E2E Testing Organization...')); + await this.dataSource.getRepository('Organization').delete(e2eOrganization.id); + this.log(chalk.green('E2E Testing Organization deleted')); + + // Check if E2E tenant has any other organizations + const remainingOrganizations = await this.dataSource.getRepository('Organization').count({ + where: { tenantId: e2eTenant.id } + }); + + if (remainingOrganizations === 0) { + this.log(chalk.yellow('No other organizations found for E2E tenant. Deleting E2E Testing Tenant...')); + await this.dataSource.getRepository('Tenant').delete(e2eTenant.id); + this.log(chalk.green('E2E Testing Tenant deleted')); + } else { + this.log( + chalk.blue(`E2E Testing Tenant has ${remainingOrganizations} other organizations. Keeping tenant.`) + ); + } + + // Disconnect to database + await this.closeConnection(); + + this.log(chalk.green('✅ E2E CLEANUP COMPLETED SUCCESSFULLY')); + } catch (error) { + this.handleError(error); + } + } + /** * Seed Default Report Data */ diff --git a/packages/core/src/lib/core/seeds/seed.ts b/packages/core/src/lib/core/seeds/seed.ts index ee5dbde208a..5603f0dca43 100644 --- a/packages/core/src/lib/core/seeds/seed.ts +++ b/packages/core/src/lib/core/seeds/seed.ts @@ -9,37 +9,47 @@ import { SeedDataService } from './seed-data.service'; import { SeederModule } from './seeder.module'; /** - * WARNING: Running this file will DELETE all data in your database - * and generate and insert new, system default minimal data into your database. - * - * BE CAREFUL running this file in production env. It's possible to delete all production data. - * SeedData checks if environment is in production or not by checking src/environments/environment.ts file configs. - * If environment.production config is set to true, then the seeding process will only generate default roles and 2 default users. - * + * Common utility to run SeedDataService operations */ -export async function seedDefault(devConfig: Partial) { +async function runSeedOperation( + devConfig: Partial, + operation: (seeder: SeedDataService) => Promise +) { await registerPluginConfig(devConfig); - NestFactory.createApplicationContext(SeederModule.forPlugins(), { + return NestFactory.createApplicationContext(SeederModule.forPlugins(), { logger: ['log', 'error', 'warn', 'debug', 'verbose'] }) - .then((app) => { - const seeder = app.get(SeedDataService); - seeder - .runDefaultSeed(false) - .then(() => { - // Seed completed successfully - }) - .catch((error) => { - throw error; - }) - .finally(() => app.close()); + .then(async (app) => { + try { + const seeder = app.get(SeedDataService); + await operation(seeder); + } catch (error) { + throw error; + } finally { + await app.close(); + } }) .catch((error) => { throw error; }); } +/** + * WARNING: Running this file will DELETE all data in your database + * and generate and insert new, system default minimal data into your database. + * + * BE CAREFUL running this file in production env. It's possible to delete all production data. + * SeedData checks if environment is in production or not by checking src/environments/environment.ts file configs. + * If environment.production config is set to true, then the seeding process will only generate default roles and 2 default users. + * + */ +export async function seedDefault(devConfig: Partial) { + await runSeedOperation(devConfig, async (seeder) => { + await seeder.runDefaultSeed(false); + }); +} + /** * Seeds organization-specific data for e2e testing within an existing database. * Creates E2E Testing Tenant and Organization with essential data for testing. @@ -47,24 +57,38 @@ export async function seedDefault(devConfig: Partial) { * */ export async function seedE2E(devConfig: Partial) { - await registerPluginConfig(devConfig); + await runSeedOperation(devConfig, async (seeder) => { + await seeder.runE2ESeed(false); + }); +} - NestFactory.createApplicationContext(SeederModule.forPlugins(), { - logger: ['log', 'error', 'warn', 'debug', 'verbose'] - }) - .then((app) => { - const seeder = app.get(SeedDataService); - seeder - .runE2ESeed(false) - .then(() => { - // E2E seed completed successfully - }) - .catch((error) => { - throw error; - }) - .finally(() => app.close()); - }) - .catch((error) => { - throw error; - }); +/** + * Cleanup E2E testing data from the database. + * Removes E2E Testing Organization, Tenant, and all related data. + * + */ +export async function cleanupE2E(devConfig: Partial) { + await runSeedOperation(devConfig, async (seeder) => { + await seeder.runE2ECleanup(); + }); +} + +/** + * Reset E2E testing data - cleanup and seed in one operation. + * This function runs cleanup and seed internally without rebuilding dependencies. + * More efficient than running cleanup and seed separately via yarn commands. + * + */ +export async function resetE2E(devConfig: Partial) { + await runSeedOperation(devConfig, async (seeder) => { + // First cleanup any existing E2E data + console.log('🧹 Cleaning up existing E2E data...'); + await seeder.runE2ECleanup(); + + // Then seed fresh E2E data + console.log('🌱 Seeding fresh E2E data...'); + await seeder.runE2ESeed(false); + + console.log('✅ E2E reset completed successfully'); + }); }