Playwright E2E test - sign up with invite link via email (#9332)

Related to
https://github.com/twentyhq/twenty/issues/8469#issuecomment-2471573054

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
BOHEUS
2025-01-12 20:52:30 +00:00
committed by GitHub
parent ba77091b06
commit c1847054f8
8 changed files with 156 additions and 109 deletions

View File

@ -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

View File

@ -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
},
//{

View File

@ -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();
});
});

View File

@ -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');
});
});

View File

@ -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<Fixtures>({
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';

View File

@ -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');
});
}
},
);
});

View File

@ -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();
});
});