Add E2E tests (#9309)

Try adding E2E tests when labelling the PR or merging on main branch
This commit is contained in:
Félix Malfait
2025-01-02 13:28:02 +01:00
committed by GitHub
parent 5da744ebc5
commit afae244057
15 changed files with 247 additions and 168 deletions

141
.github/workflows/ci-e2e.yml vendored Normal file
View File

@ -0,0 +1,141 @@
name: CI E2E Tests
on:
push:
branches:
- main
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
if: github.event_name == 'push' || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'run-e2e'))
timeout-minutes: 30
env:
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
# https://github.com/actions/runner-images/issues/70#issuecomment-589562148
NODE_OPTIONS: "--max-old-space-size=10240"
services:
postgres:
image: twentycrm/twenty-postgres-spilo
env:
PGUSER_SUPERUSER: postgres
PGPASSWORD_SUPERUSER: postgres
ALLOW_NOSSL: "true"
SPILO_PROVIDER: "local"
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Check system resources
run: |
echo "Available memory:"
free -h
echo "Available disk space:"
df -h
echo "CPU info:"
lscpu
- name: Check for changed files
id: changed-files
uses: tj-actions/changed-files@v11
with:
files: |
packages/**
playwright.config.ts
.github/workflows/ci-e2e.yml
- name: Skip if no relevant changes
if: steps.changed-files.outputs.any_changed == 'false'
run: echo "No relevant changes detected. Marking as valid."
- name: Install dependencies
if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/workflows/actions/yarn-install
- name: Build twenty-shared
if: steps.changed-files.outputs.any_changed == 'true'
run: npx nx build twenty-shared
- name: Setup environment files
if: steps.changed-files.outputs.any_changed == 'true'
run: |
cp packages/twenty-e2e-testing/.env.example packages/twenty-e2e-testing/.env
cp packages/twenty-front/.env.example packages/twenty-front/.env
cp packages/twenty-e2e-testing/.env.example packages/twenty-e2e-testing/.env
npx nx reset:env twenty-server
- name: Build frontend
if: steps.changed-files.outputs.any_changed == 'true'
run: NODE_ENV=production NODE_OPTIONS="--max-old-space-size=10240" npx nx build twenty-front
- name: Build server
if: steps.changed-files.outputs.any_changed == 'true'
run: NODE_ENV=production npx nx build twenty-server
- name: Create and setup database
if: steps.changed-files.outputs.any_changed == 'true'
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
npx nx run twenty-server:database:reset
- name: Start server
if: steps.changed-files.outputs.any_changed == 'true'
run: |
npx nx start twenty-server &
echo "Waiting for server to be ready..."
timeout 60 bash -c 'until curl -s http://localhost:3000/health; do sleep 2; done'
- name: Start frontend
if: steps.changed-files.outputs.any_changed == 'true'
run: |
npm_config_yes=true npx serve -s packages/twenty-front/build -l 3001 &
echo "Waiting for frontend to be ready..."
timeout 60 bash -c 'until curl -s http://localhost:3001; do sleep 2; done'
- name: Start worker
if: steps.changed-files.outputs.any_changed == 'true'
run: |
npx nx run twenty-server:worker:ci &
echo "Worker started"
- name: Install Playwright Browsers
if: steps.changed-files.outputs.any_changed == 'true'
run: npx nx setup twenty-e2e-testing
- name: Run Playwright tests
if: steps.changed-files.outputs.any_changed == 'true'
run: npx nx test twenty-e2e-testing
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: packages/twenty-e2e-testing/run_results/
retention-days: 30
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: packages/twenty-e2e-testing/playwright-report/
retention-days: 30

View File

@ -1,52 +0,0 @@
name: CI E2E Tests
on:
push:
branches:
- main
pull_request:
branches:
- '**'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Check for changed files
id: changed-files
uses: tj-actions/changed-files@v11
with:
files: |
packages/**
playwright.config.ts
- name: Skip if no relevant changes
if: steps.changed-files.outputs.any_changed == 'false'
run: echo "No relevant changes detected. Marking as valid."
- name: Install dependencies
if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/workflows/actions/yarn-install
- name: Install Playwright Browsers
if: steps.changed-files.outputs.any_changed == 'true'
run: yarn playwright install --with-deps
- name: Run Playwright tests
if: steps.changed-files.outputs.any_changed == 'true'
run: yarn test:e2e companies
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30

View File

@ -14,6 +14,7 @@
"styled-components.vscode-styled-components",
"unifiedjs.vscode-mdx",
"xyc.vscode-mdx-preview",
"yoavbls.pretty-ts-errors"
"yoavbls.pretty-ts-errors",
"ms-playwright.playwright"
]
}

23
.vscode/launch.json vendored
View File

@ -6,12 +6,11 @@
"name": "twenty-server - start debug",
"type": "node",
"request": "launch",
"runtimeExecutable": "yarn",
"runtimeVersion": "18",
"runtimeExecutable": "npx",
"runtimeArgs": [
"nx",
"run",
"twenty-server:start",
"twenty-server:start"
],
"outputCapture": "std",
"internalConsoleOptions": "openOnSessionStart",
@ -22,12 +21,11 @@
"name": "twenty-server - worker debug",
"type": "node",
"request": "launch",
"runtimeExecutable": "yarn",
"runtimeVersion": "18",
"runtimeExecutable": "npx",
"runtimeArgs": [
"nx",
"run",
"twenty-server:worker",
"twenty-server:worker"
],
"outputCapture": "std",
"internalConsoleOptions": "openOnSessionStart",
@ -45,12 +43,23 @@
"run",
"twenty-server:command",
"my-command",
"--my-parameter value",
"--my-parameter value"
],
"outputCapture": "std",
"internalConsoleOptions": "openOnSessionStart",
"console": "internalConsole",
"cwd": "${workspaceFolder}/packages/twenty-server/"
},
{
"name": "Playwright Test current file",
"type": "node",
"request": "launch",
"runtimeExecutable": "npx",
"runtimeArgs": ["nx", "test", "twenty-e2e-testing", "${file}"],
"console": "integratedTerminal",
"cwd": "${workspaceFolder}",
"internalConsoleOptions": "neverOpen",
"envFile": "${workspaceFolder}/packages/twenty-e2e-testing/.env"
}
]
}

View File

@ -48,5 +48,5 @@
"eslint.debug": true,
"files.associations": {
".cursorrules": "markdown"
}
},
}

View File

@ -44,6 +44,10 @@
"name": "tools/eslint-rules",
"path": "../tools/eslint-rules"
},
{
"name": "packages/twenty-e2e-testing",
"path": "../packages/twenty-e2e-testing"
}
],
"settings": {
"editor.formatOnSave": false,

View File

@ -1,6 +1,5 @@
# Note that provide always without trailing forward slash to have expected behaviour
FRONTEND_BASE_URL=http://app.localhost:3001
CI_DEFAULT_BASE_URL=https://demo.twenty.com
FRONTEND_BASE_URL=http://localhost:3001
DEFAULT_LOGIN=tim@apple.dev
NEW_WORKSPACE_LOGIN=test@apple.dev
DEMO_DEFAULT_LOGIN=noah@demo.dev

View File

@ -2,7 +2,13 @@ import { defineConfig, devices } from '@playwright/test';
import { config } from 'dotenv';
import path from 'path';
config();
const envResult = config({
path: path.resolve(__dirname, '.env'),
});
if (envResult.error) {
throw new Error('Failed to load .env file');
}
/* === Run your local dev server before starting the tests === */
@ -19,9 +25,7 @@ export default defineConfig({
workers: 1, // 1 worker = 1 test at the time, tests can't be parallelized
timeout: 30 * 1000, // timeout can be changed
use: {
baseURL: process.env.CI
? process.env.CI_DEFAULT_BASE_URL
: (process.env.FRONTEND_BASE_URL ?? 'http://app.localhost:3001'),
baseURL: process.env.FRONTEND_BASE_URL || 'http://localhost:3001',
trace: 'retain-on-failure', // trace takes EVERYTHING from page source, records every single step, should be used only when normal debugging won't work
screenshot: 'on', // either 'on' here or in different method in modules, if 'on' all screenshots are overwritten each time the test is run
headless: true, // instead of changing it to false, run 'yarn test:e2e:debug' or 'yarn test:e2e:ui'
@ -54,7 +58,7 @@ export default defineConfig({
storageState: path.resolve(__dirname, '.auth', 'user.json'), // takes saved cookies from directory
},
dependencies: ['Login setup'], // forces to run login setup before running tests from this project - CASE SENSITIVE
testMatch: /all\/.+\.spec\.ts/,
testMatch: /all\/.+\.e2e-spec\.ts/,
},
{
name: 'firefox',
@ -63,11 +67,7 @@ export default defineConfig({
storageState: path.resolve(__dirname, '.auth', 'user.json'),
},
dependencies: ['Login setup'],
testMatch: /all\/.+\.spec\.ts/,
},
{
name: 'Authentication',
testMatch: /authentication\/.*\.spec\.ts/,
testMatch: /all\/.+\.e2e-spec\.ts/,
},
//{

View File

@ -1,4 +1,4 @@
import { test, expect } from '../../lib/fixtures/screenshot';
import { expect, test } from '../../lib/fixtures/screenshot';
test.describe('Basic check', () => {
test('Checking if table in Companies is visible', async ({ page }) => {

View File

@ -0,0 +1,40 @@
import { test } from '@playwright/test';
import { sh } from '../../drivers/shell_driver';
test.describe('', () => {
test.use({ storageState: { cookies: [], origins: [] } });
/*
test('Creating new workspace', async ({ page, browserName }) => {
// this test must use only 1 browser, otherwise it will lead to success and fail (1 workspace is created instead of x workspaces)
if (browserName == 'chromium') {
await sh(
'npx nx run twenty-server:database:reset --configuration=no-seed',
);
await page.goto('/');
await page.getByRole('button', { name: 'Continue With Email' }).click();
await page.getByPlaceholder('Email').fill('test@apple.dev'); // email must be changed each time test is run
await page.getByPlaceholder('Email').press('Enter'); // otherwise if tests fails after this step, new workspace is created
await page.getByPlaceholder('Password').fill('Applecar2025');
await page.getByPlaceholder('Password').press('Enter');
await page.getByPlaceholder('Apple').fill('Test');
await page.getByRole('button', { name: 'Continue' }).click();
await page.getByPlaceholder('Tim').click();
await page.getByPlaceholder('Tim').fill('Test2');
await page.getByPlaceholder('Cook').click();
await page.getByPlaceholder('Cook').fill('Test2');
await page.getByRole('button', { name: 'Continue' }).click();
await page.getByText('Continue without sync').click();
await page.getByRole('button', { name: 'Finish' }).click();
await expect(page.locator('table')).toBeVisible({ timeout: 1000 });
await sh('npx nx run twenty-server:database:reset');
}
});
*/
test('Syncing all workspaces', async () => {
await sh('npx nx run twenty-server:command workspace:sync-metadata -f');
});
});

View File

@ -1,66 +0,0 @@
import { test, expect } from '@playwright/test';
import { sh } from '../../drivers/shell_driver';
test.describe('', () => {
test('Testing logging', async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: 'Continue With Email' }).click();
await page.getByPlaceholder('Email').fill('tim@apple.dev');
await page.getByRole('button', { name: 'Continue', exact: true }).click();
await page.getByPlaceholder('Password').fill('Applecar2025');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByText('Welcome to Twenty')).not.toBeVisible();
expect(page.url()).not.toContain('/welcome');
await page.getByRole('link', { name: 'Opportunities' }).click();
await expect(page.locator('tbody > tr')).toHaveCount(4);
});
test('Creating new workspace', async ({ page, browserName }) => {
// this test must use only 1 browser, otherwise it will lead to success and fail (1 workspace is created instead of x workspaces)
if (browserName == 'chromium') {
await page.goto('/');
await page.getByRole('button', { name: 'Continue With Email' }).click();
await page.getByPlaceholder('Email').fill('test@apple.dev'); // email must be changed each time test is run
await page.getByPlaceholder('Email').press('Enter'); // otherwise if tests fails after this step, new workspace is created
await page.getByPlaceholder('Password').fill('Applecar2025');
await page.getByPlaceholder('Password').press('Enter');
await page.getByPlaceholder('Apple').fill('Test');
await page.getByRole('button', { name: 'Continue' }).click();
await page.getByPlaceholder('Tim').click();
await page.getByPlaceholder('Tim').fill('Test2');
await page.getByPlaceholder('Cook').click();
await page.getByPlaceholder('Cook').fill('Test2');
await page.getByRole('button', { name: 'Continue' }).click();
await page.getByText('Continue without sync').click();
await page.getByRole('button', { name: 'Finish' }).click();
await expect(page.locator('table')).toBeVisible({ timeout: 1000 });
}
});
test('Syncing all workspaces', async () => {
await sh('npx nx run twenty-server:command workspace:sync-metadata -f');
await sh('npx nx run twenty-server:command workspace:sync-metadata -f');
});
test('Resetting database', async ({ page, browserName }) => {
if (browserName === 'chromium') {
await sh('yarn nx database:reset twenty-server'); // if this command fails for any reason, database must be restarted manually using the same command because database is in unstable state
await page.goto('/');
await page.getByRole('button', { name: 'Continue With Email' }).click();
await page.getByPlaceholder('Email').fill('tim@apple.dev');
await page.getByRole('button', { name: 'Continue' }).click();
await page.getByPlaceholder('Password').fill('Applecar2025');
await page.getByRole('button', { name: 'Sign in' }).click();
await page.getByRole('link', { name: 'Companies' }).click();
expect(page.url()).toContain('/companies');
await expect(page.locator('table')).toBeVisible();
}
});
test('Seeding database', async ({ page, browserName }) => {
if (browserName === 'chromium') {
await sh('npx nx workspace:seed:demo');
await page.goto('/');
}
});
});

View File

@ -1,17 +0,0 @@
import { test as base, expect } from '../../lib/fixtures/screenshot';
import { LoginPage } from '../../lib/pom/loginPage';
// fixture
const test = base.extend<{ loginPage: LoginPage }>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
});
test('Check login with email', async ({ loginPage }) => {
await loginPage.typeEmail(process.env.DEFAULT_LOGIN);
await loginPage.clickContinueButton();
await loginPage.typePassword(process.env.DEFAULT_PASSWORD);
await loginPage.clickSignInButton();
await expect(loginPage.signInButton).not.toBeVisible();
});

View File

@ -1,4 +1,4 @@
import { test, expect } from '@playwright/test';
import { expect, test } from '@playwright/test';
test('Check if demo account is working properly @demo-only', async ({
page,

View File

@ -1,18 +1,38 @@
import { test as setup, expect } from '@playwright/test';
import { expect, test as setup } from '@playwright/test';
import path from 'path';
setup('Login test', async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: 'Continue With Email' }).click();
await page.getByPlaceholder('Email').fill(process.env.DEFAULT_LOGIN);
await page.getByRole('button', { name: 'Continue', exact: true }).click();
await page.getByPlaceholder('Password').fill(process.env.DEFAULT_PASSWORD);
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByText('Welcome to Twenty')).not.toBeVisible();
console.log('Starting login test');
// End of authentication steps.
await page.goto('/');
console.log('Navigated to homepage');
await page.getByRole('button', { name: 'Continue With Email' }).click();
console.log('Clicked email login button');
console.log('Default login', process.env.DEFAULT_LOGIN);
await page.getByPlaceholder('Email').fill(process.env.DEFAULT_LOGIN ?? '');
console.log('Filled email field');
await page.getByRole('button', { name: 'Continue', exact: true }).click();
console.log('Clicked continue button');
await page
.getByPlaceholder('Password')
.fill(process.env.DEFAULT_PASSWORD ?? '');
console.log('Filled password field');
await page.getByRole('button', { name: 'Sign in' }).click();
console.log('Clicked sign in button');
await page.waitForLoadState('networkidle');
console.log('Waited for network to be idle');
await expect(page.getByText('Welcome to Twenty')).not.toBeVisible();
console.log('Verified welcome message not visible');
await page.context().storageState({
path: path.resolve(__dirname, '..', '.auth', 'user.json'),
});
console.log('Saved auth state');
});

View File

@ -4,7 +4,7 @@
"private": true,
"type": "module",
"scripts": {
"build": "VITE_DISABLE_TYPESCRIPT_CHECKER=true VITE_DISABLE_ESLINT_CHECKER=true NODE_OPTIONS=--max-old-space-size=3000 npx vite build && sh ./scripts/inject-runtime-env.sh",
"build": "VITE_DISABLE_TYPESCRIPT_CHECKER=true VITE_DISABLE_ESLINT_CHECKER=true NODE_OPTIONS=--max-old-space-size=4000 npx vite build && sh ./scripts/inject-runtime-env.sh",
"build:sourcemaps": "VITE_BUILD_SOURCEMAP=true VITE_DISABLE_TYPESCRIPT_CHECKER=true VITE_DISABLE_ESLINT_CHECKER=true NODE_OPTIONS=--max-old-space-size=6000 npx vite build && sh ./scripts/inject-runtime-env.sh",
"start:prod": "NODE_ENV=production npx vite --host",
"tsup": "npx tsup"