diff --git a/.github/workflows/ci-e2e.yml b/.github/workflows/ci-e2e.yml index 9b94cb938..371024788 100644 --- a/.github/workflows/ci-e2e.yml +++ b/.github/workflows/ci-e2e.yml @@ -1,9 +1,10 @@ -name: CI E2E Tests +name: CI E2E Playwright Tests on: push: branches: - main pull_request: + types: [opened, synchronize, reopened, labeled] concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/packages/twenty-e2e-testing/.env.example b/packages/twenty-e2e-testing/.env.example index 4cebcc110..c00e8e0d4 100644 --- a/packages/twenty-e2e-testing/.env.example +++ b/packages/twenty-e2e-testing/.env.example @@ -1,8 +1,6 @@ # Note that provide always without trailing forward slash to have expected behaviour FRONTEND_BASE_URL=http://localhost:3001 DEFAULT_LOGIN=tim@apple.dev -NEW_WORKSPACE_LOGIN=test@apple.dev -DEMO_DEFAULT_LOGIN=noah@demo.dev DEFAULT_PASSWORD=Applecar2025 WEBSITE_URL=https://twenty.com diff --git a/packages/twenty-e2e-testing/playwright.config.ts b/packages/twenty-e2e-testing/playwright.config.ts index 6e76d0c5e..448c9f8d8 100644 --- a/packages/twenty-e2e-testing/playwright.config.ts +++ b/packages/twenty-e2e-testing/playwright.config.ts @@ -16,10 +16,10 @@ if (envResult.error) { * See https://playwright.dev/docs/test-configuration. */ export default defineConfig({ - testDir: '.', + testDir: './tests', outputDir: 'run_results/', // directory for screenshots and videos snapshotPathTemplate: '{testDir}/__screenshots__/{testFilePath}/{arg}{ext}', // just in case, do not delete it - fullyParallel: true, // false only for specific tests, overwritten in specific projects or global setups of projects + fullyParallel: false, // parallelization of tests will be done later in the future forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: 1, // 1 worker = 1 test at the time, tests can't be parallelized @@ -49,25 +49,16 @@ export default defineConfig({ use: { ...devices['Desktop Chrome'], }, - testMatch: /demo\/demo_basic\.spec\.ts/, + testMatch: /demo\/demo_basic\.e2e-spec\.ts/, }, { - name: 'chromium', + name: 'Authentication', use: { - ...devices['Desktop Chrome'], + permissions: ['clipboard-read', 'clipboard-write'], 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\/.+\.e2e-spec\.ts/, - }, - { - name: 'firefox', - use: { - ...devices['Desktop Firefox'], - storageState: path.resolve(__dirname, '.auth', 'user.json'), - }, dependencies: ['Login setup'], - testMatch: /all\/.+\.e2e-spec\.ts/, + testMatch: /authentication\/.+\.e2e-spec\.ts/, // forces to run login setup before running tests from this project - CASE SENSITIVE }, //{ diff --git a/packages/twenty-e2e-testing/tests/all/companies.e2e-spec.ts b/packages/twenty-e2e-testing/tests/all/companies.e2e-spec.ts deleted file mode 100644 index a2cdd2ad7..000000000 --- a/packages/twenty-e2e-testing/tests/all/companies.e2e-spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { expect, test } from '../../lib/fixtures/screenshot'; - -test.describe('Basic check', () => { - test('Checking if table in Companies is visible', async ({ page }) => { - await expect(page.getByTestId('tooltip').nth(0)).toHaveText('Companies'); - await expect(page.getByTestId('tooltip').nth(0)).toBeVisible(); - expect(page.url()).toContain('/companies'); - await expect(page.locator('table')).toBeVisible(); - await expect(page.locator('tbody > tr')).toHaveCount(13); // shouldn't be hardcoded in case of tests on demo - }); - - test('', async ({ page }) => { - await page.getByRole('link', { name: 'Opportunities' }).click(); - await expect(page.locator('table')).toBeVisible(); - }); -}); diff --git a/packages/twenty-e2e-testing/tests/all/workspaces.e2e-spec.ts b/packages/twenty-e2e-testing/tests/all/workspaces.e2e-spec.ts deleted file mode 100644 index 24816e13e..000000000 --- a/packages/twenty-e2e-testing/tests/all/workspaces.e2e-spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -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'); - }); -}); diff --git a/packages/twenty-e2e-testing/tests/authentication/fixture.ts b/packages/twenty-e2e-testing/tests/authentication/fixture.ts new file mode 100644 index 000000000..16cf35bec --- /dev/null +++ b/packages/twenty-e2e-testing/tests/authentication/fixture.ts @@ -0,0 +1,45 @@ +import { test as base } from '../../lib/fixtures/screenshot'; +import { LoginPage } from '../../lib/pom/loginPage'; +import { LeftMenu } from '../../lib/pom/leftMenu'; +import { SettingsPage } from '../../lib/pom/settingsPage'; +import { MembersSection } from '../../lib/pom/settings/membersSection'; +import { ProfileSection } from '../../lib/pom/settings/profileSection'; +import { ConfirmationModal } from '../../lib/pom/helper/confirmationModal'; + +type Fixtures = { + confirmationModal: ConfirmationModal; + loginPage: LoginPage; + leftMenu: LeftMenu; + settingsPage: SettingsPage; + membersSection: MembersSection; + profileSection: ProfileSection; +}; + +export const test = base.extend({ + confirmationModal: async ({ page }, use) => { + const confirmationModal = new ConfirmationModal(page); + await use(confirmationModal); + }, + loginPage: async ({ page }, use) => { + const loginPage = new LoginPage(page); + await use(loginPage); + }, + leftMenu: async ({ page }, use) => { + const leftMenu = new LeftMenu(page); + await use(leftMenu); + }, + settingsPage: async ({ page }, use) => { + const settingsPage = new SettingsPage(page); + await use(settingsPage); + }, + membersSection: async ({ page }, use) => { + const membersSection = new MembersSection(page); + await use(membersSection); + }, + profileSection: async ({ page }, use) => { + const profileSection = new ProfileSection(page); + await use(profileSection); + }, +}); + +export { expect } from '@playwright/test'; diff --git a/packages/twenty-e2e-testing/tests/authentication/signup_invite_email.e2e-spec.ts b/packages/twenty-e2e-testing/tests/authentication/signup_invite_email.e2e-spec.ts new file mode 100644 index 000000000..fc7f3537d --- /dev/null +++ b/packages/twenty-e2e-testing/tests/authentication/signup_invite_email.e2e-spec.ts @@ -0,0 +1,64 @@ +import { expect, test } from './fixture'; + +test.describe('Authentication test', () => { + const email = 'test@apple.dev'; + const firstName = 'John'; + const lastName = 'Doe'; + let testCompleted = false; + + test('Sign up with invite link via email', async ({ + page, + loginPage, + leftMenu, + membersSection, + settingsPage, + }) => { + const inviteLink: string = + await test.step('Go to Settings and copy invite link', async () => { + await page.goto(process.env.LINK); // skip login page (and redirect) when running on environments with multi-workspace enabled + await leftMenu.goToSettings(); + await settingsPage.goToMembersSection(); + await membersSection.copyInviteLink(); + return await page.evaluate('navigator.clipboard.readText()'); + }); + await test.step('Go to invite link', async () => { + await settingsPage.logout(); + await page.goto(inviteLink); + }); + await test.step('Create new account', async () => { + await loginPage.clickLoginWithEmail(); + await loginPage.typeEmail(email); + await loginPage.clickContinueButton(); + await loginPage.typePassword(process.env.DEFAULT_PASSWORD); + await loginPage.clickSignUpButton(); + await loginPage.typeFirstName(firstName); + await loginPage.typeLastName(lastName); + await loginPage.clickContinueButton(); + await loginPage.noSyncWithGoogle(); + testCompleted = true; + }); + }); + + test.afterEach( + async ({ + page, + confirmationModal, + leftMenu, + profileSection, + settingsPage, + }) => { + if (testCompleted) { + // security measurement to clean up only after test is completed, + // otherwise default account used for tests may be deleted and resetting database will be necessary + await test.step('Cleanup - deleting account', async () => { + await leftMenu.goToSettings(); + await settingsPage.goToProfileSection(); + await profileSection.deleteAccount(); + await confirmationModal.typePlaceholderToInput(); + await confirmationModal.clickConfirmButton(); + expect(page.url()).toContain('/welcome'); + }); + } + }, + ); +}); diff --git a/packages/twenty-e2e-testing/tests/login.setup.ts b/packages/twenty-e2e-testing/tests/login.setup.ts index 74150ea3f..3d68ccbcb 100644 --- a/packages/twenty-e2e-testing/tests/login.setup.ts +++ b/packages/twenty-e2e-testing/tests/login.setup.ts @@ -1,38 +1,42 @@ -import { expect, test as setup } from '@playwright/test'; +import { expect, test as base } from '@playwright/test'; +import { LoginPage } from '../lib/pom/loginPage'; import path from 'path'; -setup('Login test', async ({ page }) => { - console.log('Starting login test'); - - 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'); +// fixture +const test = base.extend<{ loginPage: LoginPage }>({ + loginPage: async ({ page }, use) => { + const loginPage = new LoginPage(page); + await use(loginPage); + }, +}); + +test('Login test', async ({ loginPage, page }) => { + await test.step('Navigated to login page', async () => { + await page.goto('/'); + }); + await test.step( + 'Logging in '.concat(page.url(), ' as ', process.env.DEFAULT_LOGIN), + async () => { + await page.waitForLoadState('networkidle'); + if ( + page.url().includes('demo.twenty.com') || + !page.url().includes('app.localhost:3001') + ) { + await loginPage.clickLoginWithEmail(); + } + await loginPage.typeEmail(process.env.DEFAULT_LOGIN); + await loginPage.clickContinueButton(); + await loginPage.typePassword(process.env.DEFAULT_PASSWORD); + await page.waitForLoadState('networkidle'); + await loginPage.clickSignInButton(); + await expect(page.getByText('Welcome to Twenty')).not.toBeVisible(); + }, + ); + + await test.step('Saved auth state', async () => { + await page.context().storageState({ + path: path.resolve(__dirname, '..', '.auth', 'user.json'), + }); + process.env.LINK = page.url(); + }); });