From c3d8046f1eafd44991d98e8758ea866668581b36 Mon Sep 17 00:00:00 2001 From: BOHEUS Date: Mon, 12 Aug 2024 18:39:48 +0200 Subject: [PATCH 01/17] Fix for #6597 Created by field can't have null value --- .../typeorm-seeds/workspace/opportunities.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/twenty-server/src/database/typeorm-seeds/workspace/opportunities.ts b/packages/twenty-server/src/database/typeorm-seeds/workspace/opportunities.ts index e8c9c8b3b41e..3c380dd8f5c9 100644 --- a/packages/twenty-server/src/database/typeorm-seeds/workspace/opportunities.ts +++ b/packages/twenty-server/src/database/typeorm-seeds/workspace/opportunities.ts @@ -47,8 +47,8 @@ export const seedOpportunity = async ( pointOfContactId: DEV_SEED_PERSON_IDS.CHRISTOPH, companyId: DEV_SEED_COMPANY_IDS.LINKEDIN, createdBySource: 'MANUAL', - createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.JONY, - createdByName: 'Jony Ive', + createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, + createdByName: 'Tim Cook', }, { id: DEV_SEED_OPPORTUNITY_IDS.OPPORTUNITY_2, @@ -61,8 +61,8 @@ export const seedOpportunity = async ( pointOfContactId: DEV_SEED_PERSON_IDS.CHRISTOPHER_G, companyId: DEV_SEED_COMPANY_IDS.FACEBOOK, createdBySource: 'MANUAL', - createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.JONY, - createdByName: 'Jony Ive', + createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, + createdByName: 'Tim Cook', }, { id: DEV_SEED_OPPORTUNITY_IDS.OPPORTUNITY_3, @@ -75,8 +75,8 @@ export const seedOpportunity = async ( pointOfContactId: DEV_SEED_PERSON_IDS.NICHOLAS, companyId: DEV_SEED_COMPANY_IDS.MICROSOFT, createdBySource: 'MANUAL', - createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.JONY, - createdByName: 'Jony Ive', + createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM, + createdByName: 'Tim Cook', }, { id: DEV_SEED_OPPORTUNITY_IDS.OPPORTUNITY_4, From d3827aa5ca2bb9da4feed436af5e4e9cc079c193 Mon Sep 17 00:00:00 2001 From: BOHEUS Date: Tue, 13 Aug 2024 11:52:42 +0200 Subject: [PATCH 02/17] Playwright initial setup + example tests (WIP) --- .github/workflows/playwright.yml | 27 ++ .gitignore | 5 + package.json | 1 + packages/twenty-e2e-testing/companies.spec.ts | 25 + packages/twenty-e2e-testing/src/config/.env | 4 + .../src/config/customreporter.ts | 33 ++ .../src/drivers/shell_driver.ts | 13 + .../twenty-e2e-testing/workspaces.spec.ts | 71 +++ playwright.config.ts | 78 ++++ tests-examples/demo-todo-app.spec.ts | 437 ++++++++++++++++++ yarn.lock | 14 +- 11 files changed, 707 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/playwright.yml create mode 100644 packages/twenty-e2e-testing/companies.spec.ts create mode 100644 packages/twenty-e2e-testing/src/config/.env create mode 100644 packages/twenty-e2e-testing/src/config/customreporter.ts create mode 100644 packages/twenty-e2e-testing/src/drivers/shell_driver.ts create mode 100644 packages/twenty-e2e-testing/workspaces.spec.ts create mode 100644 playwright.config.ts create mode 100644 tests-examples/demo-todo-app.spec.ts diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 000000000000..f7f7c1c41274 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,27 @@ +name: Playwright Tests +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Install dependencies + run: npm install -g yarn && yarn + - name: Install Playwright Browsers + run: yarn playwright install --with-deps + - name: Run Playwright tests + run: yarn playwright test + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index 376216c452c4..6df61582f43e 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,8 @@ storybook-static .eslintcache .cache .nyc_output +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/packages/twenty-e2e-testing/run_results/ \ No newline at end of file diff --git a/package.json b/package.json index aa05f761c0f0..3112a5e9bb8b 100644 --- a/package.json +++ b/package.json @@ -223,6 +223,7 @@ "@nx/storybook": "18.3.3", "@nx/vite": "18.3.3", "@nx/web": "18.3.3", + "@playwright/test": "^1.46.0", "@sentry/types": "^7.109.0", "@storybook/addon-actions": "^7.6.3", "@storybook/addon-coverage": "^1.0.0", diff --git a/packages/twenty-e2e-testing/companies.spec.ts b/packages/twenty-e2e-testing/companies.spec.ts new file mode 100644 index 000000000000..67a8b34f38cf --- /dev/null +++ b/packages/twenty-e2e-testing/companies.spec.ts @@ -0,0 +1,25 @@ +import { test, expect } from '@playwright/test'; + +const date = new Date(); + +test.beforeEach(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' }).click(); + await page.getByPlaceholder('Password').fill('Applecar2025'); + await page.getByRole('button', { name: 'Sign in' }).click(); +}); + +test.afterEach(async ({ page, browserName }) => { + await page.screenshot({ + path: `./packages/twenty-e2e-testing/__screenshots__/${browserName}/${date.toISOString()}.png`, + }); +}); + +test('Checking if table in Companies is visible', async ({ page }) => { + await page.getByRole('link', { name: 'Companies' }).click(); + expect(page.url()).toContain('/companies'); + await expect(page.locator('table')).toBeVisible(); + await expect(page.locator('tr')).toHaveCount(14); +}); \ No newline at end of file diff --git a/packages/twenty-e2e-testing/src/config/.env b/packages/twenty-e2e-testing/src/config/.env new file mode 100644 index 000000000000..0393367fd349 --- /dev/null +++ b/packages/twenty-e2e-testing/src/config/.env @@ -0,0 +1,4 @@ +URL=http://localhost:3001 +DEFAULT_LOGIN=tim@apple.dev +PASSWORD=Applecar2025 +NEW_WORKSPACE_LOGIN=test@apple.dev \ No newline at end of file diff --git a/packages/twenty-e2e-testing/src/config/customreporter.ts b/packages/twenty-e2e-testing/src/config/customreporter.ts new file mode 100644 index 000000000000..62a602ef8e7b --- /dev/null +++ b/packages/twenty-e2e-testing/src/config/customreporter.ts @@ -0,0 +1,33 @@ +import { + Reporter, + FullConfig, + Suite, + TestCase, + TestResult, + FullResult, +} from '@playwright/test/reporter'; + +class CustomReporter implements Reporter { + constructor(options: { customOption?: string } = {}) { + console.log( + `my-awesome-reporter setup with customOption set to ${options.customOption}`, + ); + } + + onBegin(config: FullConfig, suite: Suite) { + console.log(`Starting the run with ${suite.allTests().length} tests`); + } + + onTestBegin(test: TestCase) { + console.log(`Starting test ${test.title}`); + } + + onTestEnd(test: TestCase, result: TestResult) { + console.log(`Finished test ${test.title}: ${result.status}`); + } + + onEnd(result: FullResult) { + console.log(`Finished the run: ${result.status}`); + } +} +export default CustomReporter; diff --git a/packages/twenty-e2e-testing/src/drivers/shell_driver.ts b/packages/twenty-e2e-testing/src/drivers/shell_driver.ts new file mode 100644 index 000000000000..cf293c032bf1 --- /dev/null +++ b/packages/twenty-e2e-testing/src/drivers/shell_driver.ts @@ -0,0 +1,13 @@ +import { exec } from 'child_process'; + +export async function sh(cmd) { + return new Promise((resolve, reject) => { + exec(cmd, (err, stdout, stderr) => { + if (err) { + reject(err); + } else { + resolve({ stdout, stderr }); + } + }); + }); +} diff --git a/packages/twenty-e2e-testing/workspaces.spec.ts b/packages/twenty-e2e-testing/workspaces.spec.ts new file mode 100644 index 000000000000..5d3f2bfe8d6d --- /dev/null +++ b/packages/twenty-e2e-testing/workspaces.spec.ts @@ -0,0 +1,71 @@ +import { test, expect } from '@playwright/test'; +import { sh } from './src/drivers/shell_driver'; + +const date = new Date(); + +test.beforeEach(async ({ page }) => { + await page.goto('/'); +}); + +test.afterEach(async ({ page, browserName }) => { + await page.screenshot({ + path: `./packages/twenty-e2e-testing/__screenshots__/${browserName}/${date.toISOString()}.png`, + }); +}); + +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' }).click(); + await page.getByPlaceholder('Password').fill('Applecar2025'); + await page.getByRole('button', { name: 'Sign in' }).click(); +}); + +test('Creating new workspace', async ({ page, browserName }) => { + if (browserName == 'firefox') { + 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('yarn command:prod workspace:sync-metadata -f'); + await sh('yarn command:prod workspace:sync-metadata -f'); +}); + +test('Resetting database', async ({ page, browserName }) => { + if (browserName === 'firefox') { + 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 === 'firefox') { + await sh('npx nx workspace:seed:demo'); + await page.goto('/'); + } +}); \ No newline at end of file diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 000000000000..88060ffd7984 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,78 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './packages/twenty-e2e-testing', + snapshotDir: '{testDir}/__screenshots__/', + snapshotPathTemplate: '{testDir}/__screenshots__/{testFilePath}/{arg}{ext}', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + outputDir: '{testDir}/run_results/', + timeout: 90000, + use: { + baseURL: 'http://localhost:3001', + trace: 'on-first-retry', + screenshot: 'only-on-failure', + headless: true, + viewport: { width: 1920, height: 1080 }, // most laptops use this resolution + launchOptions: { + slowMo: 50, + }, + }, + expect: { + timeout: 5000, + }, + reporter: [['html', { open: 'never' }]], + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + /*{ + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + },*/ + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + /* Run your local dev server before starting the tests */ + //webServer: { + // command: 'npx nx start && make postgres-on-docker', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + //}, +}); diff --git a/tests-examples/demo-todo-app.spec.ts b/tests-examples/demo-todo-app.spec.ts new file mode 100644 index 000000000000..8641cb5f5dc7 --- /dev/null +++ b/tests-examples/demo-todo-app.spec.ts @@ -0,0 +1,437 @@ +import { test, expect, type Page } from '@playwright/test'; + +test.beforeEach(async ({ page }) => { + await page.goto('https://demo.playwright.dev/todomvc'); +}); + +const TODO_ITEMS = [ + 'buy some cheese', + 'feed the cat', + 'book a doctors appointment' +] as const; + +test.describe('New Todo', () => { + test('should allow me to add todo items', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create 1st todo. + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press('Enter'); + + // Make sure the list only has one todo item. + await expect(page.getByTestId('todo-title')).toHaveText([ + TODO_ITEMS[0] + ]); + + // Create 2nd todo. + await newTodo.fill(TODO_ITEMS[1]); + await newTodo.press('Enter'); + + // Make sure the list now has two todo items. + await expect(page.getByTestId('todo-title')).toHaveText([ + TODO_ITEMS[0], + TODO_ITEMS[1] + ]); + + await checkNumberOfTodosInLocalStorage(page, 2); + }); + + test('should clear text input field when an item is added', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create one todo item. + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press('Enter'); + + // Check that input is empty. + await expect(newTodo).toBeEmpty(); + await checkNumberOfTodosInLocalStorage(page, 1); + }); + + test('should append new items to the bottom of the list', async ({ page }) => { + // Create 3 items. + await createDefaultTodos(page); + + // create a todo count locator + const todoCount = page.getByTestId('todo-count') + + // Check test using different methods. + await expect(page.getByText('3 items left')).toBeVisible(); + await expect(todoCount).toHaveText('3 items left'); + await expect(todoCount).toContainText('3'); + await expect(todoCount).toHaveText(/3/); + + // Check all items in one call. + await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS); + await checkNumberOfTodosInLocalStorage(page, 3); + }); +}); + +test.describe('Mark all as completed', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test.afterEach(async ({ page }) => { + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test('should allow me to mark all items as completed', async ({ page }) => { + // Complete all todos. + await page.getByLabel('Mark all as complete').check(); + + // Ensure all todos have 'completed' class. + await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + }); + + test('should allow me to clear the complete state of all items', async ({ page }) => { + const toggleAll = page.getByLabel('Mark all as complete'); + // Check and then immediately uncheck. + await toggleAll.check(); + await toggleAll.uncheck(); + + // Should be no completed classes. + await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']); + }); + + test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => { + const toggleAll = page.getByLabel('Mark all as complete'); + await toggleAll.check(); + await expect(toggleAll).toBeChecked(); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + + // Uncheck first todo. + const firstTodo = page.getByTestId('todo-item').nth(0); + await firstTodo.getByRole('checkbox').uncheck(); + + // Reuse toggleAll locator and make sure its not checked. + await expect(toggleAll).not.toBeChecked(); + + await firstTodo.getByRole('checkbox').check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + + // Assert the toggle all is checked again. + await expect(toggleAll).toBeChecked(); + }); +}); + +test.describe('Item', () => { + + test('should allow me to mark items as complete', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create two items. + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } + + // Check first item. + const firstTodo = page.getByTestId('todo-item').nth(0); + await firstTodo.getByRole('checkbox').check(); + await expect(firstTodo).toHaveClass('completed'); + + // Check second item. + const secondTodo = page.getByTestId('todo-item').nth(1); + await expect(secondTodo).not.toHaveClass('completed'); + await secondTodo.getByRole('checkbox').check(); + + // Assert completed class. + await expect(firstTodo).toHaveClass('completed'); + await expect(secondTodo).toHaveClass('completed'); + }); + + test('should allow me to un-mark items as complete', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create two items. + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } + + const firstTodo = page.getByTestId('todo-item').nth(0); + const secondTodo = page.getByTestId('todo-item').nth(1); + const firstTodoCheckbox = firstTodo.getByRole('checkbox'); + + await firstTodoCheckbox.check(); + await expect(firstTodo).toHaveClass('completed'); + await expect(secondTodo).not.toHaveClass('completed'); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + await firstTodoCheckbox.uncheck(); + await expect(firstTodo).not.toHaveClass('completed'); + await expect(secondTodo).not.toHaveClass('completed'); + await checkNumberOfCompletedTodosInLocalStorage(page, 0); + }); + + test('should allow me to edit an item', async ({ page }) => { + await createDefaultTodos(page); + + const todoItems = page.getByTestId('todo-item'); + const secondTodo = todoItems.nth(1); + await secondTodo.dblclick(); + await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]); + await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); + await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter'); + + // Explicitly assert the new text value. + await expect(todoItems).toHaveText([ + TODO_ITEMS[0], + 'buy some sausages', + TODO_ITEMS[2] + ]); + await checkTodosInLocalStorage(page, 'buy some sausages'); + }); +}); + +test.describe('Editing', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test('should hide other controls when editing', async ({ page }) => { + const todoItem = page.getByTestId('todo-item').nth(1); + await todoItem.dblclick(); + await expect(todoItem.getByRole('checkbox')).not.toBeVisible(); + await expect(todoItem.locator('label', { + hasText: TODO_ITEMS[1], + })).not.toBeVisible(); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test('should save edits on blur', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur'); + + await expect(todoItems).toHaveText([ + TODO_ITEMS[0], + 'buy some sausages', + TODO_ITEMS[2], + ]); + await checkTodosInLocalStorage(page, 'buy some sausages'); + }); + + test('should trim entered text', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages '); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); + + await expect(todoItems).toHaveText([ + TODO_ITEMS[0], + 'buy some sausages', + TODO_ITEMS[2], + ]); + await checkTodosInLocalStorage(page, 'buy some sausages'); + }); + + test('should remove the item if an empty text string was entered', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(''); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); + + await expect(todoItems).toHaveText([ + TODO_ITEMS[0], + TODO_ITEMS[2], + ]); + }); + + test('should cancel edits on escape', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape'); + await expect(todoItems).toHaveText(TODO_ITEMS); + }); +}); + +test.describe('Counter', () => { + test('should display the current number of todo items', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // create a todo count locator + const todoCount = page.getByTestId('todo-count') + + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press('Enter'); + + await expect(todoCount).toContainText('1'); + + await newTodo.fill(TODO_ITEMS[1]); + await newTodo.press('Enter'); + await expect(todoCount).toContainText('2'); + + await checkNumberOfTodosInLocalStorage(page, 2); + }); +}); + +test.describe('Clear completed button', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + }); + + test('should display the correct text', async ({ page }) => { + await page.locator('.todo-list li .toggle').first().check(); + await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible(); + }); + + test('should remove completed items when clicked', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).getByRole('checkbox').check(); + await page.getByRole('button', { name: 'Clear completed' }).click(); + await expect(todoItems).toHaveCount(2); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); + }); + + test('should be hidden when there are no items that are completed', async ({ page }) => { + await page.locator('.todo-list li .toggle').first().check(); + await page.getByRole('button', { name: 'Clear completed' }).click(); + await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden(); + }); +}); + +test.describe('Persistence', () => { + test('should persist its data', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } + + const todoItems = page.getByTestId('todo-item'); + const firstTodoCheck = todoItems.nth(0).getByRole('checkbox'); + await firstTodoCheck.check(); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); + await expect(firstTodoCheck).toBeChecked(); + await expect(todoItems).toHaveClass(['completed', '']); + + // Ensure there is 1 completed item. + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + // Now reload. + await page.reload(); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); + await expect(firstTodoCheck).toBeChecked(); + await expect(todoItems).toHaveClass(['completed', '']); + }); +}); + +test.describe('Routing', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + // make sure the app had a chance to save updated todos in storage + // before navigating to a new view, otherwise the items can get lost :( + // in some frameworks like Durandal + await checkTodosInLocalStorage(page, TODO_ITEMS[0]); + }); + + test('should allow me to display active items', async ({ page }) => { + const todoItem = page.getByTestId('todo-item'); + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole('link', { name: 'Active' }).click(); + await expect(todoItem).toHaveCount(2); + await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); + }); + + test('should respect the back button', async ({ page }) => { + const todoItem = page.getByTestId('todo-item'); + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + await test.step('Showing all items', async () => { + await page.getByRole('link', { name: 'All' }).click(); + await expect(todoItem).toHaveCount(3); + }); + + await test.step('Showing active items', async () => { + await page.getByRole('link', { name: 'Active' }).click(); + }); + + await test.step('Showing completed items', async () => { + await page.getByRole('link', { name: 'Completed' }).click(); + }); + + await expect(todoItem).toHaveCount(1); + await page.goBack(); + await expect(todoItem).toHaveCount(2); + await page.goBack(); + await expect(todoItem).toHaveCount(3); + }); + + test('should allow me to display completed items', async ({ page }) => { + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole('link', { name: 'Completed' }).click(); + await expect(page.getByTestId('todo-item')).toHaveCount(1); + }); + + test('should allow me to display all items', async ({ page }) => { + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole('link', { name: 'Active' }).click(); + await page.getByRole('link', { name: 'Completed' }).click(); + await page.getByRole('link', { name: 'All' }).click(); + await expect(page.getByTestId('todo-item')).toHaveCount(3); + }); + + test('should highlight the currently applied filter', async ({ page }) => { + await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected'); + + //create locators for active and completed links + const activeLink = page.getByRole('link', { name: 'Active' }); + const completedLink = page.getByRole('link', { name: 'Completed' }); + await activeLink.click(); + + // Page change - active items. + await expect(activeLink).toHaveClass('selected'); + await completedLink.click(); + + // Page change - completed items. + await expect(completedLink).toHaveClass('selected'); + }); +}); + +async function createDefaultTodos(page: Page) { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + for (const item of TODO_ITEMS) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } +} + +async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) { + return await page.waitForFunction(e => { + return JSON.parse(localStorage['react-todos']).length === e; + }, expected); +} + +async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) { + return await page.waitForFunction(e => { + return JSON.parse(localStorage['react-todos']).filter((todo: any) => todo.completed).length === e; + }, expected); +} + +async function checkTodosInLocalStorage(page: Page, title: string) { + return await page.waitForFunction(t => { + return JSON.parse(localStorage['react-todos']).map((todo: any) => todo.title).includes(t); + }, title); +} diff --git a/yarn.lock b/yarn.lock index 179058fe4d29..2a95531a4802 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9761,6 +9761,17 @@ __metadata: languageName: node linkType: hard +"@playwright/test@npm:^1.46.0": + version: 1.46.0 + resolution: "@playwright/test@npm:1.46.0" + dependencies: + playwright: "npm:1.46.0" + bin: + playwright: cli.js + checksum: 10c0/dced91081efc8b063267626059b37923f44f98132edba906f30281277d919de27c6acf6de082d390ad263112a983f856e7f4bf37722a33cc42f62c5be308bf87 + languageName: node + linkType: hard + "@pnpm/config.env-replace@npm:^1.1.0": version: 1.1.0 resolution: "@pnpm/config.env-replace@npm:1.1.0" @@ -40191,7 +40202,7 @@ __metadata: languageName: node linkType: hard -"playwright@npm:^1.14.0, playwright@npm:^1.40.1": +"playwright@npm:1.46.0, playwright@npm:^1.14.0, playwright@npm:^1.40.1": version: 1.46.0 resolution: "playwright@npm:1.46.0" dependencies: @@ -47236,6 +47247,7 @@ __metadata: "@nx/vite": "npm:18.3.3" "@nx/web": "npm:18.3.3" "@octokit/graphql": "npm:^7.0.2" + "@playwright/test": "npm:^1.46.0" "@ptc-org/nestjs-query-core": "npm:^4.2.0" "@ptc-org/nestjs-query-typeorm": "npm:4.2.1-alpha.2" "@react-email/components": "npm:0.0.12" From bbe00989aec520308272f00057727af8db608098 Mon Sep 17 00:00:00 2001 From: BOHEUS Date: Thu, 15 Aug 2024 21:58:05 +0200 Subject: [PATCH 03/17] Fixed structure of twenty-e2e-testing package, updated configuration of tests --- .gitignore | 7 +- packages/twenty-e2e-testing/.env.example | 12 + packages/twenty-e2e-testing/.gitignore | 11 +- packages/twenty-e2e-testing/companies.spec.ts | 25 - .../{src => }/config/customreporter.ts | 0 .../{src => }/drivers/shell_driver.ts | 0 .../twenty-e2e-testing/e2e/companies.spec.ts | 14 - .../twenty-e2e-testing/playwright.config.ts | 64 ++- packages/twenty-e2e-testing/src/config/.env | 4 - .../tests/companies.spec.ts | 30 ++ .../{ => tests}/workspaces.spec.ts | 8 +- playwright.config.ts | 78 ---- tests-examples/demo-todo-app.spec.ts | 437 ------------------ yarn.lock | 12 +- 14 files changed, 110 insertions(+), 592 deletions(-) delete mode 100644 packages/twenty-e2e-testing/companies.spec.ts rename packages/twenty-e2e-testing/{src => }/config/customreporter.ts (100%) rename packages/twenty-e2e-testing/{src => }/drivers/shell_driver.ts (100%) delete mode 100644 packages/twenty-e2e-testing/e2e/companies.spec.ts delete mode 100644 packages/twenty-e2e-testing/src/config/.env create mode 100644 packages/twenty-e2e-testing/tests/companies.spec.ts rename packages/twenty-e2e-testing/{ => tests}/workspaces.spec.ts (90%) delete mode 100644 playwright.config.ts delete mode 100644 tests-examples/demo-todo-app.spec.ts diff --git a/.gitignore b/.gitignore index 6df61582f43e..55c82d9f6939 100644 --- a/.gitignore +++ b/.gitignore @@ -27,9 +27,4 @@ storybook-static *.tsbuildinfo .eslintcache .cache -.nyc_output -/test-results/ -/playwright-report/ -/blob-report/ -/playwright/.cache/ -/packages/twenty-e2e-testing/run_results/ \ No newline at end of file +.nyc_output \ No newline at end of file diff --git a/packages/twenty-e2e-testing/.env.example b/packages/twenty-e2e-testing/.env.example index 9ff92d0193a7..bbb94df92d9d 100644 --- a/packages/twenty-e2e-testing/.env.example +++ b/packages/twenty-e2e-testing/.env.example @@ -1,2 +1,14 @@ # Note that provide always without trailing forward slash to have expected behaviour FRONTEND_BASE_URL="http://localhost:3001" +DEFAULT_LOGIN=tim@apple.dev +DEFAULT_PASSWORD=Applecar2025 +NEW_WORKSPACE_LOGIN=test@apple.dev + +# === DO NOT USE, WORK IN PROGRESS === +# This URL must have trailing forward slash as all REST API endpoints have object after it +# Documentation for REST API: https://twenty.com/developers/rest-api/core#/ +# REST_API_BASE_URL=http://localhost:3000/rest/ + +# Without this key, all API tests will fail, to generate this key +# Log in to Twenty workspace, go to Settings > Developers, generate new key and paste it here +# REST_API_DEV_KEY=fill_with_proper_key \ No newline at end of file diff --git a/packages/twenty-e2e-testing/.gitignore b/packages/twenty-e2e-testing/.gitignore index 68c5d18f00dc..f1c17ec3cb92 100644 --- a/packages/twenty-e2e-testing/.gitignore +++ b/packages/twenty-e2e-testing/.gitignore @@ -1,5 +1,6 @@ -node_modules/ -/test-results/ -/playwright-report/ -/blob-report/ -/playwright/.cache/ +test-results/ +playwright-report/ +blob-report/ +playwright/.cache/ +run_results/ +results/ \ No newline at end of file diff --git a/packages/twenty-e2e-testing/companies.spec.ts b/packages/twenty-e2e-testing/companies.spec.ts deleted file mode 100644 index 67a8b34f38cf..000000000000 --- a/packages/twenty-e2e-testing/companies.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { test, expect } from '@playwright/test'; - -const date = new Date(); - -test.beforeEach(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' }).click(); - await page.getByPlaceholder('Password').fill('Applecar2025'); - await page.getByRole('button', { name: 'Sign in' }).click(); -}); - -test.afterEach(async ({ page, browserName }) => { - await page.screenshot({ - path: `./packages/twenty-e2e-testing/__screenshots__/${browserName}/${date.toISOString()}.png`, - }); -}); - -test('Checking if table in Companies is visible', async ({ page }) => { - await page.getByRole('link', { name: 'Companies' }).click(); - expect(page.url()).toContain('/companies'); - await expect(page.locator('table')).toBeVisible(); - await expect(page.locator('tr')).toHaveCount(14); -}); \ No newline at end of file diff --git a/packages/twenty-e2e-testing/src/config/customreporter.ts b/packages/twenty-e2e-testing/config/customreporter.ts similarity index 100% rename from packages/twenty-e2e-testing/src/config/customreporter.ts rename to packages/twenty-e2e-testing/config/customreporter.ts diff --git a/packages/twenty-e2e-testing/src/drivers/shell_driver.ts b/packages/twenty-e2e-testing/drivers/shell_driver.ts similarity index 100% rename from packages/twenty-e2e-testing/src/drivers/shell_driver.ts rename to packages/twenty-e2e-testing/drivers/shell_driver.ts diff --git a/packages/twenty-e2e-testing/e2e/companies.spec.ts b/packages/twenty-e2e-testing/e2e/companies.spec.ts deleted file mode 100644 index 48485da04df6..000000000000 --- a/packages/twenty-e2e-testing/e2e/companies.spec.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { expect, test } from '@playwright/test'; - -test.describe('visible table', () => { - test('table should be visible on navigation to /objects/companies', async ({ - page, - }) => { - // Navigate to the page - await page.goto('/objects/companies'); - - // Check if the table is visible - const table = page.locator('table'); - await expect(table).toBeVisible(); - }); -}); diff --git a/packages/twenty-e2e-testing/playwright.config.ts b/packages/twenty-e2e-testing/playwright.config.ts index 4b4f081de794..f5fdae99276a 100644 --- a/packages/twenty-e2e-testing/playwright.config.ts +++ b/packages/twenty-e2e-testing/playwright.config.ts @@ -1,43 +1,73 @@ import { defineConfig, devices } from '@playwright/test'; - import { config } from 'dotenv'; + config(); /** * See https://playwright.dev/docs/test-configuration. - * See https://playwright.dev/docs/trace-viewer to Collect trace when retrying the failed test */ export default defineConfig({ - testDir: 'e2e', - /* Run tests in files in parallel */ - fullyParallel: true, - reporter: 'html', - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + testDir: '.', + outputDir: 'run_results/', // + snapshotPathTemplate: '{testDir}/__screenshots__/{testFilePath}/{arg}{ext}', + fullyParallel: true, // + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + timeout: 5 * 60 * 1000, use: { - /* Base URL to use in actions like `await page.goto('/')`. */ baseURL: process.env.FRONTEND_BASE_URL ?? 'http://localhost:3001', + trace: 'on-first-retry', + screenshot: 'only-on-failure', + headless: true, + viewport: { width: 1920, height: 1080 }, // most laptops use this resolution + launchOptions: { + slowMo: 50, + }, }, - - /* Configure projects for major browsers */ + expect: { + timeout: 5000, + }, + reporter: [['html', { open: 'never' }]], projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, - { name: 'firefox', use: { ...devices['Desktop Firefox'] }, }, - { + /*{ name: 'webkit', use: { ...devices['Desktop Safari'] }, - }, + },*/ - { - name: 'Google Chrome', - use: { ...devices['Desktop Chrome'], channel: 'chrome' }, - }, + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, ], + /* Run your local dev server before starting the tests */ + //webServer: { + // command: 'npx nx start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + //}, }); diff --git a/packages/twenty-e2e-testing/src/config/.env b/packages/twenty-e2e-testing/src/config/.env deleted file mode 100644 index 0393367fd349..000000000000 --- a/packages/twenty-e2e-testing/src/config/.env +++ /dev/null @@ -1,4 +0,0 @@ -URL=http://localhost:3001 -DEFAULT_LOGIN=tim@apple.dev -PASSWORD=Applecar2025 -NEW_WORKSPACE_LOGIN=test@apple.dev \ No newline at end of file diff --git a/packages/twenty-e2e-testing/tests/companies.spec.ts b/packages/twenty-e2e-testing/tests/companies.spec.ts new file mode 100644 index 000000000000..a1e91ff3c457 --- /dev/null +++ b/packages/twenty-e2e-testing/tests/companies.spec.ts @@ -0,0 +1,30 @@ +import { test, expect } from '@playwright/test'; +import { config } from 'dotenv'; +import path = require('path'); +config({ path: path.resolve(__dirname, '..', '.env') }); + +const date = new Date(); + +test.beforeEach(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' }).click(); + await page.getByPlaceholder('Password').fill(process.env.DEFAULT_PASSWORD); + await page.getByRole('button', { name: 'Sign in' }).click(); +}); + +test.afterEach(async ({ page, browserName }, workerInfo) => { + await page.screenshot({ + path: `./packages/twenty-e2e-testing/results/screenshots/${browserName}/`+workerInfo.project.name+`${date.toISOString()}.png`, + }); +}); + +test.describe('Basic check', () => { + test('Checking if table in Companies is visible', async ({ page }) => { + await page.getByRole('link', { name: 'Companies' }).click(); + expect(page.url()).toContain('/companies'); + await expect(page.locator('table')).toBeVisible(); + await expect(page.locator('tbody > tr')).toHaveCount(13); + }) +}); \ No newline at end of file diff --git a/packages/twenty-e2e-testing/workspaces.spec.ts b/packages/twenty-e2e-testing/tests/workspaces.spec.ts similarity index 90% rename from packages/twenty-e2e-testing/workspaces.spec.ts rename to packages/twenty-e2e-testing/tests/workspaces.spec.ts index 5d3f2bfe8d6d..90b5cf6c0bc9 100644 --- a/packages/twenty-e2e-testing/workspaces.spec.ts +++ b/packages/twenty-e2e-testing/tests/workspaces.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from '@playwright/test'; -import { sh } from './src/drivers/shell_driver'; +import { sh } from '../drivers/shell_driver'; const date = new Date(); @@ -9,7 +9,7 @@ test.beforeEach(async ({ page }) => { test.afterEach(async ({ page, browserName }) => { await page.screenshot({ - path: `./packages/twenty-e2e-testing/__screenshots__/${browserName}/${date.toISOString()}.png`, + path: `./packages/twenty-e2e-testing/results/screenshots/${browserName}/${date.toISOString()}.png`, }); }); @@ -44,8 +44,8 @@ test('Creating new workspace', async ({ page, browserName }) => { }); test('Syncing all workspaces', async () => { - await sh('yarn command:prod workspace:sync-metadata -f'); - await sh('yarn command:prod workspace:sync-metadata -f'); + 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 }) => { diff --git a/playwright.config.ts b/playwright.config.ts deleted file mode 100644 index 88060ffd7984..000000000000 --- a/playwright.config.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { defineConfig, devices } from '@playwright/test'; - -/** - * Read environment variables from file. - * https://github.com/motdotla/dotenv - */ -// import dotenv from 'dotenv'; -// dotenv.config({ path: path.resolve(__dirname, '.env') }); - -/** - * See https://playwright.dev/docs/test-configuration. - */ -export default defineConfig({ - testDir: './packages/twenty-e2e-testing', - snapshotDir: '{testDir}/__screenshots__/', - snapshotPathTemplate: '{testDir}/__screenshots__/{testFilePath}/{arg}{ext}', - fullyParallel: true, - forbidOnly: !!process.env.CI, - retries: process.env.CI ? 2 : 0, - workers: process.env.CI ? 1 : undefined, - outputDir: '{testDir}/run_results/', - timeout: 90000, - use: { - baseURL: 'http://localhost:3001', - trace: 'on-first-retry', - screenshot: 'only-on-failure', - headless: true, - viewport: { width: 1920, height: 1080 }, // most laptops use this resolution - launchOptions: { - slowMo: 50, - }, - }, - expect: { - timeout: 5000, - }, - reporter: [['html', { open: 'never' }]], - projects: [ - { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, - }, - { - name: 'firefox', - use: { ...devices['Desktop Firefox'] }, - }, - - /*{ - name: 'webkit', - use: { ...devices['Desktop Safari'] }, - },*/ - - /* Test against mobile viewports. */ - // { - // name: 'Mobile Chrome', - // use: { ...devices['Pixel 5'] }, - // }, - // { - // name: 'Mobile Safari', - // use: { ...devices['iPhone 12'] }, - // }, - - /* Test against branded browsers. */ - // { - // name: 'Microsoft Edge', - // use: { ...devices['Desktop Edge'], channel: 'msedge' }, - // }, - // { - // name: 'Google Chrome', - // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, - // }, - ], - /* Run your local dev server before starting the tests */ - //webServer: { - // command: 'npx nx start && make postgres-on-docker', - // url: 'http://127.0.0.1:3000', - // reuseExistingServer: !process.env.CI, - //}, -}); diff --git a/tests-examples/demo-todo-app.spec.ts b/tests-examples/demo-todo-app.spec.ts deleted file mode 100644 index 8641cb5f5dc7..000000000000 --- a/tests-examples/demo-todo-app.spec.ts +++ /dev/null @@ -1,437 +0,0 @@ -import { test, expect, type Page } from '@playwright/test'; - -test.beforeEach(async ({ page }) => { - await page.goto('https://demo.playwright.dev/todomvc'); -}); - -const TODO_ITEMS = [ - 'buy some cheese', - 'feed the cat', - 'book a doctors appointment' -] as const; - -test.describe('New Todo', () => { - test('should allow me to add todo items', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create 1st todo. - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - - // Make sure the list only has one todo item. - await expect(page.getByTestId('todo-title')).toHaveText([ - TODO_ITEMS[0] - ]); - - // Create 2nd todo. - await newTodo.fill(TODO_ITEMS[1]); - await newTodo.press('Enter'); - - // Make sure the list now has two todo items. - await expect(page.getByTestId('todo-title')).toHaveText([ - TODO_ITEMS[0], - TODO_ITEMS[1] - ]); - - await checkNumberOfTodosInLocalStorage(page, 2); - }); - - test('should clear text input field when an item is added', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create one todo item. - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - - // Check that input is empty. - await expect(newTodo).toBeEmpty(); - await checkNumberOfTodosInLocalStorage(page, 1); - }); - - test('should append new items to the bottom of the list', async ({ page }) => { - // Create 3 items. - await createDefaultTodos(page); - - // create a todo count locator - const todoCount = page.getByTestId('todo-count') - - // Check test using different methods. - await expect(page.getByText('3 items left')).toBeVisible(); - await expect(todoCount).toHaveText('3 items left'); - await expect(todoCount).toContainText('3'); - await expect(todoCount).toHaveText(/3/); - - // Check all items in one call. - await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS); - await checkNumberOfTodosInLocalStorage(page, 3); - }); -}); - -test.describe('Mark all as completed', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test.afterEach(async ({ page }) => { - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should allow me to mark all items as completed', async ({ page }) => { - // Complete all todos. - await page.getByLabel('Mark all as complete').check(); - - // Ensure all todos have 'completed' class. - await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - }); - - test('should allow me to clear the complete state of all items', async ({ page }) => { - const toggleAll = page.getByLabel('Mark all as complete'); - // Check and then immediately uncheck. - await toggleAll.check(); - await toggleAll.uncheck(); - - // Should be no completed classes. - await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']); - }); - - test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => { - const toggleAll = page.getByLabel('Mark all as complete'); - await toggleAll.check(); - await expect(toggleAll).toBeChecked(); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - - // Uncheck first todo. - const firstTodo = page.getByTestId('todo-item').nth(0); - await firstTodo.getByRole('checkbox').uncheck(); - - // Reuse toggleAll locator and make sure its not checked. - await expect(toggleAll).not.toBeChecked(); - - await firstTodo.getByRole('checkbox').check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - - // Assert the toggle all is checked again. - await expect(toggleAll).toBeChecked(); - }); -}); - -test.describe('Item', () => { - - test('should allow me to mark items as complete', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create two items. - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - // Check first item. - const firstTodo = page.getByTestId('todo-item').nth(0); - await firstTodo.getByRole('checkbox').check(); - await expect(firstTodo).toHaveClass('completed'); - - // Check second item. - const secondTodo = page.getByTestId('todo-item').nth(1); - await expect(secondTodo).not.toHaveClass('completed'); - await secondTodo.getByRole('checkbox').check(); - - // Assert completed class. - await expect(firstTodo).toHaveClass('completed'); - await expect(secondTodo).toHaveClass('completed'); - }); - - test('should allow me to un-mark items as complete', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create two items. - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - const firstTodo = page.getByTestId('todo-item').nth(0); - const secondTodo = page.getByTestId('todo-item').nth(1); - const firstTodoCheckbox = firstTodo.getByRole('checkbox'); - - await firstTodoCheckbox.check(); - await expect(firstTodo).toHaveClass('completed'); - await expect(secondTodo).not.toHaveClass('completed'); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - await firstTodoCheckbox.uncheck(); - await expect(firstTodo).not.toHaveClass('completed'); - await expect(secondTodo).not.toHaveClass('completed'); - await checkNumberOfCompletedTodosInLocalStorage(page, 0); - }); - - test('should allow me to edit an item', async ({ page }) => { - await createDefaultTodos(page); - - const todoItems = page.getByTestId('todo-item'); - const secondTodo = todoItems.nth(1); - await secondTodo.dblclick(); - await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]); - await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter'); - - // Explicitly assert the new text value. - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - 'buy some sausages', - TODO_ITEMS[2] - ]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); -}); - -test.describe('Editing', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should hide other controls when editing', async ({ page }) => { - const todoItem = page.getByTestId('todo-item').nth(1); - await todoItem.dblclick(); - await expect(todoItem.getByRole('checkbox')).not.toBeVisible(); - await expect(todoItem.locator('label', { - hasText: TODO_ITEMS[1], - })).not.toBeVisible(); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should save edits on blur', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur'); - - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - 'buy some sausages', - TODO_ITEMS[2], - ]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); - - test('should trim entered text', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages '); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); - - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - 'buy some sausages', - TODO_ITEMS[2], - ]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); - - test('should remove the item if an empty text string was entered', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(''); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); - - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - TODO_ITEMS[2], - ]); - }); - - test('should cancel edits on escape', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape'); - await expect(todoItems).toHaveText(TODO_ITEMS); - }); -}); - -test.describe('Counter', () => { - test('should display the current number of todo items', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // create a todo count locator - const todoCount = page.getByTestId('todo-count') - - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - - await expect(todoCount).toContainText('1'); - - await newTodo.fill(TODO_ITEMS[1]); - await newTodo.press('Enter'); - await expect(todoCount).toContainText('2'); - - await checkNumberOfTodosInLocalStorage(page, 2); - }); -}); - -test.describe('Clear completed button', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - }); - - test('should display the correct text', async ({ page }) => { - await page.locator('.todo-list li .toggle').first().check(); - await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible(); - }); - - test('should remove completed items when clicked', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).getByRole('checkbox').check(); - await page.getByRole('button', { name: 'Clear completed' }).click(); - await expect(todoItems).toHaveCount(2); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); - }); - - test('should be hidden when there are no items that are completed', async ({ page }) => { - await page.locator('.todo-list li .toggle').first().check(); - await page.getByRole('button', { name: 'Clear completed' }).click(); - await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden(); - }); -}); - -test.describe('Persistence', () => { - test('should persist its data', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - const todoItems = page.getByTestId('todo-item'); - const firstTodoCheck = todoItems.nth(0).getByRole('checkbox'); - await firstTodoCheck.check(); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); - await expect(firstTodoCheck).toBeChecked(); - await expect(todoItems).toHaveClass(['completed', '']); - - // Ensure there is 1 completed item. - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - // Now reload. - await page.reload(); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); - await expect(firstTodoCheck).toBeChecked(); - await expect(todoItems).toHaveClass(['completed', '']); - }); -}); - -test.describe('Routing', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - // make sure the app had a chance to save updated todos in storage - // before navigating to a new view, otherwise the items can get lost :( - // in some frameworks like Durandal - await checkTodosInLocalStorage(page, TODO_ITEMS[0]); - }); - - test('should allow me to display active items', async ({ page }) => { - const todoItem = page.getByTestId('todo-item'); - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Active' }).click(); - await expect(todoItem).toHaveCount(2); - await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); - }); - - test('should respect the back button', async ({ page }) => { - const todoItem = page.getByTestId('todo-item'); - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - await test.step('Showing all items', async () => { - await page.getByRole('link', { name: 'All' }).click(); - await expect(todoItem).toHaveCount(3); - }); - - await test.step('Showing active items', async () => { - await page.getByRole('link', { name: 'Active' }).click(); - }); - - await test.step('Showing completed items', async () => { - await page.getByRole('link', { name: 'Completed' }).click(); - }); - - await expect(todoItem).toHaveCount(1); - await page.goBack(); - await expect(todoItem).toHaveCount(2); - await page.goBack(); - await expect(todoItem).toHaveCount(3); - }); - - test('should allow me to display completed items', async ({ page }) => { - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Completed' }).click(); - await expect(page.getByTestId('todo-item')).toHaveCount(1); - }); - - test('should allow me to display all items', async ({ page }) => { - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Active' }).click(); - await page.getByRole('link', { name: 'Completed' }).click(); - await page.getByRole('link', { name: 'All' }).click(); - await expect(page.getByTestId('todo-item')).toHaveCount(3); - }); - - test('should highlight the currently applied filter', async ({ page }) => { - await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected'); - - //create locators for active and completed links - const activeLink = page.getByRole('link', { name: 'Active' }); - const completedLink = page.getByRole('link', { name: 'Completed' }); - await activeLink.click(); - - // Page change - active items. - await expect(activeLink).toHaveClass('selected'); - await completedLink.click(); - - // Page change - completed items. - await expect(completedLink).toHaveClass('selected'); - }); -}); - -async function createDefaultTodos(page: Page) { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - for (const item of TODO_ITEMS) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } -} - -async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) { - return await page.waitForFunction(e => { - return JSON.parse(localStorage['react-todos']).length === e; - }, expected); -} - -async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) { - return await page.waitForFunction(e => { - return JSON.parse(localStorage['react-todos']).filter((todo: any) => todo.completed).length === e; - }, expected); -} - -async function checkTodosInLocalStorage(page: Page, title: string) { - return await page.waitForFunction(t => { - return JSON.parse(localStorage['react-todos']).map((todo: any) => todo.title).includes(t); - }, title); -} diff --git a/yarn.lock b/yarn.lock index 2a95531a4802..50fd70ba5230 100644 --- a/yarn.lock +++ b/yarn.lock @@ -40202,7 +40202,7 @@ __metadata: languageName: node linkType: hard -"playwright@npm:1.46.0, playwright@npm:^1.14.0, playwright@npm:^1.40.1": +"playwright@npm:1.46.0, playwright@npm:^1.14.0, playwright@npm:^1.46.0": version: 1.46.0 resolution: "playwright@npm:1.46.0" dependencies: @@ -47082,6 +47082,14 @@ __metadata: languageName: unknown linkType: soft +"twenty-e2e-testing@workspace:packages/twenty-e2e-testing": + version: 0.0.0-use.local + resolution: "twenty-e2e-testing@workspace:packages/twenty-e2e-testing" + dependencies: + "@playwright/test": "npm:^1.46.0" + languageName: unknown + linkType: soft + "twenty-emails@workspace:packages/twenty-emails": version: 0.0.0-use.local resolution: "twenty-emails@workspace:packages/twenty-emails" @@ -47452,7 +47460,7 @@ __metadata: pg: "npm:^8.11.3" pg-boss: "npm:^9.0.3" planer: "npm:^1.2.0" - playwright: "npm:^1.40.1" + playwright: "npm:^1.46.0" pluralize: "npm:^8.0.0" prettier: "npm:^3.1.1" prism-react-renderer: "npm:^2.1.0" From 1611dc33a01258ec409e3f761abdc9c311c7cf3f Mon Sep 17 00:00:00 2001 From: BOHEUS Date: Fri, 16 Aug 2024 17:32:48 +0200 Subject: [PATCH 04/17] Fixes in Github workflow CI --- .github/workflows/playwright.yml | 2 +- packages/twenty-e2e-testing/.gitignore | 1 + .../twenty-e2e-testing/playwright.config.ts | 32 +++++++++---------- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index f7f7c1c41274..8dbceca4b26e 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -18,7 +18,7 @@ jobs: - name: Install Playwright Browsers run: yarn playwright install --with-deps - name: Run Playwright tests - run: yarn playwright test + run: yarn run test:e2e companies - uses: actions/upload-artifact@v4 if: always() with: diff --git a/packages/twenty-e2e-testing/.gitignore b/packages/twenty-e2e-testing/.gitignore index f1c17ec3cb92..1cf2d1c268ba 100644 --- a/packages/twenty-e2e-testing/.gitignore +++ b/packages/twenty-e2e-testing/.gitignore @@ -3,4 +3,5 @@ playwright-report/ blob-report/ playwright/.cache/ run_results/ +playwright-report/.last-run.json results/ \ No newline at end of file diff --git a/packages/twenty-e2e-testing/playwright.config.ts b/packages/twenty-e2e-testing/playwright.config.ts index f5fdae99276a..22e1fddb4958 100644 --- a/packages/twenty-e2e-testing/playwright.config.ts +++ b/packages/twenty-e2e-testing/playwright.config.ts @@ -8,9 +8,9 @@ config(); */ export default defineConfig({ testDir: '.', - outputDir: 'run_results/', // - snapshotPathTemplate: '{testDir}/__screenshots__/{testFilePath}/{arg}{ext}', - fullyParallel: true, // + 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 forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 1 : undefined, @@ -39,10 +39,10 @@ export default defineConfig({ use: { ...devices['Desktop Firefox'] }, }, - /*{ - name: 'webkit', - use: { ...devices['Desktop Safari'] }, - },*/ + //{ + // name: 'webkit', + // use: { ...devices['Desktop Safari'] }, + //}, /* Test against mobile viewports. */ // { @@ -55,19 +55,19 @@ export default defineConfig({ // }, /* Test against branded browsers. */ - // { - // name: 'Microsoft Edge', - // use: { ...devices['Desktop Edge'], channel: 'msedge' }, - // }, - // { - // name: 'Google Chrome', - // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, - // }, + //{ + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + //}, + //{ + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + //}, ], /* Run your local dev server before starting the tests */ //webServer: { // command: 'npx nx start', - // url: 'http://127.0.0.1:3000', + // url: 'http://localhost:3000', // somehow `localhost` is not mapped to 127.0.0.1 // reuseExistingServer: !process.env.CI, //}, }); From 9662f03b6cd6905f9a3485dbd6d192338351a278 Mon Sep 17 00:00:00 2001 From: BOHEUS Date: Fri, 16 Aug 2024 19:52:45 +0200 Subject: [PATCH 05/17] Too big timeout (5m > 30sec) --- packages/twenty-e2e-testing/.gitignore | 4 +++- packages/twenty-e2e-testing/playwright.config.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/twenty-e2e-testing/.gitignore b/packages/twenty-e2e-testing/.gitignore index 1cf2d1c268ba..a3c4dbdc0a16 100644 --- a/packages/twenty-e2e-testing/.gitignore +++ b/packages/twenty-e2e-testing/.gitignore @@ -4,4 +4,6 @@ blob-report/ playwright/.cache/ run_results/ playwright-report/.last-run.json -results/ \ No newline at end of file +results/ +run_results/.playwright-artifacts-0/ +run_results/.playwright-artifacts-1/ \ No newline at end of file diff --git a/packages/twenty-e2e-testing/playwright.config.ts b/packages/twenty-e2e-testing/playwright.config.ts index 22e1fddb4958..97a4d6c1082a 100644 --- a/packages/twenty-e2e-testing/playwright.config.ts +++ b/packages/twenty-e2e-testing/playwright.config.ts @@ -14,7 +14,7 @@ export default defineConfig({ forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 1 : undefined, - timeout: 5 * 60 * 1000, + timeout: 30 * 1000, use: { baseURL: process.env.FRONTEND_BASE_URL ?? 'http://localhost:3001', trace: 'on-first-retry', From 9c318b1eedc7dbdd806d77b93a88fc1143c96472 Mon Sep 17 00:00:00 2001 From: BOHEUS Date: Fri, 16 Aug 2024 20:34:25 +0200 Subject: [PATCH 06/17] Reverting changes introduced in #6621 --- packages/twenty-front/vite.config.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/twenty-front/vite.config.ts b/packages/twenty-front/vite.config.ts index a711d05916f1..924f9c9d7ee0 100644 --- a/packages/twenty-front/vite.config.ts +++ b/packages/twenty-front/vite.config.ts @@ -104,8 +104,5 @@ export default defineConfig(({ command, mode }) => { localsConvention: 'camelCaseOnly', }, }, - optimizeDeps: { - exclude: ['@tabler/icons-react'], - }, }; }); From 012c94ae9175a7b0c1bc96fad5d45034068d4383 Mon Sep 17 00:00:00 2001 From: BOHEUS Date: Fri, 16 Aug 2024 21:02:55 +0200 Subject: [PATCH 07/17] Revert changes introduced in #6621 --- packages/twenty-front/vite.config.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/twenty-front/vite.config.ts b/packages/twenty-front/vite.config.ts index a711d05916f1..924f9c9d7ee0 100644 --- a/packages/twenty-front/vite.config.ts +++ b/packages/twenty-front/vite.config.ts @@ -104,8 +104,5 @@ export default defineConfig(({ command, mode }) => { localsConvention: 'camelCaseOnly', }, }, - optimizeDeps: { - exclude: ['@tabler/icons-react'], - }, }; }); From 9ac78ae2d21050acd2d65b381cbd05f191e12847 Mon Sep 17 00:00:00 2001 From: BOHEUS Date: Fri, 16 Aug 2024 23:17:08 +0200 Subject: [PATCH 08/17] Changed config and tests --- packages/twenty-e2e-testing/.env.example | 7 +- packages/twenty-e2e-testing/README.md | 4 +- .../twenty-e2e-testing/playwright.config.ts | 1 + .../tests/companies.spec.ts | 9 +- .../tests/workspaces.spec.ts | 108 +++++++++--------- 5 files changed, 70 insertions(+), 59 deletions(-) diff --git a/packages/twenty-e2e-testing/.env.example b/packages/twenty-e2e-testing/.env.example index bbb94df92d9d..126768866eac 100644 --- a/packages/twenty-e2e-testing/.env.example +++ b/packages/twenty-e2e-testing/.env.example @@ -9,6 +9,11 @@ NEW_WORKSPACE_LOGIN=test@apple.dev # Documentation for REST API: https://twenty.com/developers/rest-api/core#/ # REST_API_BASE_URL=http://localhost:3000/rest/ +# Documentation for GraphQL API: https://twenty.com/developers/graphql/core +# GRAPHQL_BASE_URL=http://localhost:3000/graphql + # Without this key, all API tests will fail, to generate this key +# In order to use it, header Authorization: Bearer token must be used # Log in to Twenty workspace, go to Settings > Developers, generate new key and paste it here -# REST_API_DEV_KEY=fill_with_proper_key \ No newline at end of file +# This key works for REST and GraphQL API +# API_DEV_KEY=fill_with_proper_key \ No newline at end of file diff --git a/packages/twenty-e2e-testing/README.md b/packages/twenty-e2e-testing/README.md index 222f1d8070db..dc113a09c5a8 100644 --- a/packages/twenty-e2e-testing/README.md +++ b/packages/twenty-e2e-testing/README.md @@ -1,8 +1,8 @@ # Twenty e2e Testing -## Install +## Prerequisition -Don't forget to install the browsers before launching the tests : +Installing the browsers: ``` yarn playwright install diff --git a/packages/twenty-e2e-testing/playwright.config.ts b/packages/twenty-e2e-testing/playwright.config.ts index 97a4d6c1082a..d53bf904f9e5 100644 --- a/packages/twenty-e2e-testing/playwright.config.ts +++ b/packages/twenty-e2e-testing/playwright.config.ts @@ -20,6 +20,7 @@ export default defineConfig({ trace: 'on-first-retry', screenshot: 'only-on-failure', headless: true, + testIdAttribute: 'data-testid', viewport: { width: 1920, height: 1080 }, // most laptops use this resolution launchOptions: { slowMo: 50, diff --git a/packages/twenty-e2e-testing/tests/companies.spec.ts b/packages/twenty-e2e-testing/tests/companies.spec.ts index a1e91ff3c457..60fcc4c74da8 100644 --- a/packages/twenty-e2e-testing/tests/companies.spec.ts +++ b/packages/twenty-e2e-testing/tests/companies.spec.ts @@ -16,7 +16,10 @@ test.beforeEach(async ({ page }) => { test.afterEach(async ({ page, browserName }, workerInfo) => { await page.screenshot({ - path: `./packages/twenty-e2e-testing/results/screenshots/${browserName}/`+workerInfo.project.name+`${date.toISOString()}.png`, + path: + `./packages/twenty-e2e-testing/results/screenshots/${browserName}/` + + workerInfo.project.name + + `${date.toISOString()}.png`, }); }); @@ -26,5 +29,5 @@ test.describe('Basic check', () => { expect(page.url()).toContain('/companies'); await expect(page.locator('table')).toBeVisible(); await expect(page.locator('tbody > tr')).toHaveCount(13); - }) -}); \ No newline at end of file + }); +}); diff --git a/packages/twenty-e2e-testing/tests/workspaces.spec.ts b/packages/twenty-e2e-testing/tests/workspaces.spec.ts index 90b5cf6c0bc9..ca2eefd4eefa 100644 --- a/packages/twenty-e2e-testing/tests/workspaces.spec.ts +++ b/packages/twenty-e2e-testing/tests/workspaces.spec.ts @@ -3,69 +3,71 @@ import { sh } from '../drivers/shell_driver'; const date = new Date(); -test.beforeEach(async ({ page }) => { - await page.goto('/'); -}); - test.afterEach(async ({ page, browserName }) => { await page.screenshot({ path: `./packages/twenty-e2e-testing/results/screenshots/${browserName}/${date.toISOString()}.png`, }); }); -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' }).click(); - await page.getByPlaceholder('Password').fill('Applecar2025'); - await page.getByRole('button', { name: 'Sign in' }).click(); -}); - -test('Creating new workspace', async ({ page, browserName }) => { - if (browserName == 'firefox') { - 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 === 'firefox') { - 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 +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' }).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(); - } -}); + await page.getByRole('link', { name: 'Opportunities' }).click(); + await expect(page.locator('tbody > tr')).toHaveCount(4); + expect(page.url()).not.toContain('/welcome'); + }); -test('Seeding database', async ({ page, browserName }) => { - if (browserName === 'firefox') { - await sh('npx nx workspace:seed:demo'); - await page.goto('/'); - } -}); \ No newline at end of file + 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 == 'firefox') { + 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 === 'firefox') { + 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 === 'firefox') { + await sh('npx nx workspace:seed:demo'); + await page.goto('/'); + } + }); +}); From 0970c0fca0d3732eb0a986d91f45f2312693355d Mon Sep 17 00:00:00 2001 From: BOHEUS Date: Fri, 16 Aug 2024 23:43:22 +0200 Subject: [PATCH 09/17] Probably working Github CI --- .github/workflows/playwright.yml | 2 +- .gitignore | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 8dbceca4b26e..cffb50287629 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -18,7 +18,7 @@ jobs: - name: Install Playwright Browsers run: yarn playwright install --with-deps - name: Run Playwright tests - run: yarn run test:e2e companies + run: yarn test:e2e companies - uses: actions/upload-artifact@v4 if: always() with: diff --git a/.gitignore b/.gitignore index 55c82d9f6939..65a5a2e7472f 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,5 @@ storybook-static *.tsbuildinfo .eslintcache .cache -.nyc_output \ No newline at end of file +.nyc_output +test-results/ \ No newline at end of file From 2ede4039783ef0be8c13a977f27430b5c959eb7e Mon Sep 17 00:00:00 2001 From: BOHEUS Date: Sat, 17 Aug 2024 09:33:31 +0200 Subject: [PATCH 10/17] Test workflow CI --- packages/twenty-e2e-testing/playwright.config.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/twenty-e2e-testing/playwright.config.ts b/packages/twenty-e2e-testing/playwright.config.ts index d53bf904f9e5..d3d33ea57c75 100644 --- a/packages/twenty-e2e-testing/playwright.config.ts +++ b/packages/twenty-e2e-testing/playwright.config.ts @@ -66,9 +66,9 @@ export default defineConfig({ //}, ], /* Run your local dev server before starting the tests */ - //webServer: { - // command: 'npx nx start', - // url: 'http://localhost:3000', // somehow `localhost` is not mapped to 127.0.0.1 - // reuseExistingServer: !process.env.CI, - //}, + webServer: { + command: 'npx nx start', + url: 'http://localhost:3000', // somehow `localhost` is not mapped to 127.0.0.1 + reuseExistingServer: !process.env.CI, + }, }); From 1ec1a682fa27abdb9e8902171da6888411672dec Mon Sep 17 00:00:00 2001 From: BOHEUS Date: Sat, 17 Aug 2024 14:47:52 +0200 Subject: [PATCH 11/17] Changed config, fixed test --- packages/twenty-e2e-testing/.env.example | 3 ++- .../twenty-e2e-testing/playwright.config.ts | 27 +++++++++---------- .../tests/companies.spec.ts | 5 ++-- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/packages/twenty-e2e-testing/.env.example b/packages/twenty-e2e-testing/.env.example index 126768866eac..b25d1ad1715d 100644 --- a/packages/twenty-e2e-testing/.env.example +++ b/packages/twenty-e2e-testing/.env.example @@ -1,8 +1,9 @@ # Note that provide always without trailing forward slash to have expected behaviour FRONTEND_BASE_URL="http://localhost:3001" DEFAULT_LOGIN=tim@apple.dev -DEFAULT_PASSWORD=Applecar2025 NEW_WORKSPACE_LOGIN=test@apple.dev +DEMO_DEFAULT_LOGIN=noah@demo.dev +DEFAULT_PASSWORD=Applecar2025 # === DO NOT USE, WORK IN PROGRESS === # This URL must have trailing forward slash as all REST API endpoints have object after it diff --git a/packages/twenty-e2e-testing/playwright.config.ts b/packages/twenty-e2e-testing/playwright.config.ts index d3d33ea57c75..9ef1b109afb7 100644 --- a/packages/twenty-e2e-testing/playwright.config.ts +++ b/packages/twenty-e2e-testing/playwright.config.ts @@ -3,6 +3,8 @@ import { config } from 'dotenv'; config(); +/* === Run your local dev server before starting the tests === */ + /** * See https://playwright.dev/docs/test-configuration. */ @@ -13,17 +15,20 @@ export default defineConfig({ fullyParallel: true, // false only for specific tests, overwritten in specific projects or global setups of projects forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, - workers: process.env.CI ? 1 : undefined, - timeout: 30 * 1000, + workers: process.env.CI ? 1 : undefined, // undefined = amount of projects + timeout: 30 * 1000, // timeout can be changed use: { - baseURL: process.env.FRONTEND_BASE_URL ?? 'http://localhost:3001', - trace: 'on-first-retry', - screenshot: 'only-on-failure', - headless: true, - testIdAttribute: 'data-testid', + baseURL: + process.env.CI ?? + 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' + testIdAttribute: 'data-testid', // taken from Twenty source viewport: { width: 1920, height: 1080 }, // most laptops use this resolution launchOptions: { - slowMo: 50, + slowMo: 500, // time in milliseconds between each step, better to use it than explicitly define timeout in tests }, }, expect: { @@ -65,10 +70,4 @@ export default defineConfig({ // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, //}, ], - /* Run your local dev server before starting the tests */ - webServer: { - command: 'npx nx start', - url: 'http://localhost:3000', // somehow `localhost` is not mapped to 127.0.0.1 - reuseExistingServer: !process.env.CI, - }, }); diff --git a/packages/twenty-e2e-testing/tests/companies.spec.ts b/packages/twenty-e2e-testing/tests/companies.spec.ts index 60fcc4c74da8..b56be39a2349 100644 --- a/packages/twenty-e2e-testing/tests/companies.spec.ts +++ b/packages/twenty-e2e-testing/tests/companies.spec.ts @@ -25,9 +25,10 @@ test.afterEach(async ({ page, browserName }, workerInfo) => { test.describe('Basic check', () => { test('Checking if table in Companies is visible', async ({ page }) => { - await page.getByRole('link', { name: 'Companies' }).click(); + 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); + await expect(page.locator('tbody > tr')).toHaveCount(13); // shouldn't be hardcoded in case of tests on demo }); }); From db20a35408f4abe526d429e5f007215b2f6a626a Mon Sep 17 00:00:00 2001 From: BOHEUS Date: Sat, 17 Aug 2024 17:00:20 +0200 Subject: [PATCH 12/17] Typo in readme --- packages/twenty-e2e-testing/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/twenty-e2e-testing/README.md b/packages/twenty-e2e-testing/README.md index dc113a09c5a8..67a527d015c4 100644 --- a/packages/twenty-e2e-testing/README.md +++ b/packages/twenty-e2e-testing/README.md @@ -1,6 +1,6 @@ # Twenty e2e Testing -## Prerequisition +## Prerequisite Installing the browsers: From c8273f372bc5bd0539da2cd298320e7a89b6d03a Mon Sep 17 00:00:00 2001 From: BOHEUS Date: Sat, 17 Aug 2024 17:04:32 +0200 Subject: [PATCH 13/17] Fixed baseURL in config --- packages/twenty-e2e-testing/.env.example | 3 ++- packages/twenty-e2e-testing/playwright.config.ts | 7 +++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/twenty-e2e-testing/.env.example b/packages/twenty-e2e-testing/.env.example index b25d1ad1715d..51aabb693dca 100644 --- a/packages/twenty-e2e-testing/.env.example +++ b/packages/twenty-e2e-testing/.env.example @@ -1,5 +1,6 @@ # Note that provide always without trailing forward slash to have expected behaviour -FRONTEND_BASE_URL="http://localhost:3001" +FRONTEND_BASE_URL=http://localhost:3001 +CI_DEFAULT_BASE_URL=https://demo.twenty.com DEFAULT_LOGIN=tim@apple.dev NEW_WORKSPACE_LOGIN=test@apple.dev DEMO_DEFAULT_LOGIN=noah@demo.dev diff --git a/packages/twenty-e2e-testing/playwright.config.ts b/packages/twenty-e2e-testing/playwright.config.ts index 9ef1b109afb7..a91c798791cd 100644 --- a/packages/twenty-e2e-testing/playwright.config.ts +++ b/packages/twenty-e2e-testing/playwright.config.ts @@ -18,10 +18,9 @@ export default defineConfig({ workers: process.env.CI ? 1 : undefined, // undefined = amount of projects timeout: 30 * 1000, // timeout can be changed use: { - baseURL: - process.env.CI ?? - process.env.FRONTEND_BASE_URL ?? - 'http://localhost:3001', + baseURL: process.env.CI + ? process.env.CI_DEFAULT_BASE_URL + : (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' From 52a3c3d400390c2d97af32dd55bf36f22bb20f78 Mon Sep 17 00:00:00 2001 From: BOHEUS Date: Wed, 21 Aug 2024 19:34:06 +0200 Subject: [PATCH 14/17] Deleted duplicate in .gitignore --- .gitignore | 1 - packages/twenty-e2e-testing/.env.example | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 65a5a2e7472f..febe678d4530 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,5 @@ dist storybook-static *.tsbuildinfo .eslintcache -.cache .nyc_output test-results/ \ No newline at end of file diff --git a/packages/twenty-e2e-testing/.env.example b/packages/twenty-e2e-testing/.env.example index 51aabb693dca..29209f8c8d4b 100644 --- a/packages/twenty-e2e-testing/.env.example +++ b/packages/twenty-e2e-testing/.env.example @@ -5,6 +5,7 @@ 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 # === DO NOT USE, WORK IN PROGRESS === # This URL must have trailing forward slash as all REST API endpoints have object after it @@ -15,7 +16,7 @@ DEFAULT_PASSWORD=Applecar2025 # GRAPHQL_BASE_URL=http://localhost:3000/graphql # Without this key, all API tests will fail, to generate this key -# In order to use it, header Authorization: Bearer token must be used # Log in to Twenty workspace, go to Settings > Developers, generate new key and paste it here -# This key works for REST and GraphQL API +# In order to use it, header Authorization: Bearer token must be used +# This key works for both REST and GraphQL API # API_DEV_KEY=fill_with_proper_key \ No newline at end of file From da4bd73881a607d71a31763dc97a631b1fb1107e Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Wed, 21 Aug 2024 20:12:14 +0200 Subject: [PATCH 15/17] Fix logging error in webhook system --- .../workspace-query-runner/jobs/call-webhook-jobs.job.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/jobs/call-webhook-jobs.job.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/jobs/call-webhook-jobs.job.ts index f4f6f9dca930..74e955093e6d 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/jobs/call-webhook-jobs.job.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/jobs/call-webhook-jobs.job.ts @@ -81,7 +81,7 @@ export class CallWebhookJobsJob { if (webhooks.length) { this.logger.log( - `CallWebhookJobsJob on eventName '${event}' called on webhooks ids [\n"${webhooks + `CallWebhookJobsJob on eventName '${eventName}' called on webhooks ids [\n"${webhooks .map((webhook) => webhook.id) .join('",\n"')}"\n]`, ); From 2d3fd5e622bdf8cf34d4acf7af5f4acf5bed535a Mon Sep 17 00:00:00 2001 From: BOHEUS Date: Wed, 21 Aug 2024 20:18:59 +0200 Subject: [PATCH 16/17] Github CI workflow saved to .yml.bak --- .github/workflows/{playwright.yml => playwright.yml.bak} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{playwright.yml => playwright.yml.bak} (100%) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml.bak similarity index 100% rename from .github/workflows/playwright.yml rename to .github/workflows/playwright.yml.bak From 99b562230fa216e1536a3de97c64a07ebebcaf1e Mon Sep 17 00:00:00 2001 From: BOHEUS Date: Wed, 21 Aug 2024 20:26:39 +0200 Subject: [PATCH 17/17] Bring back reverted changes from #6621 --- packages/twenty-front/vite.config.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/twenty-front/vite.config.ts b/packages/twenty-front/vite.config.ts index 682b6466ab40..f6cbcb45051d 100644 --- a/packages/twenty-front/vite.config.ts +++ b/packages/twenty-front/vite.config.ts @@ -108,5 +108,8 @@ export default defineConfig(({ command, mode }) => { localsConvention: 'camelCaseOnly', }, }, + optimizeDeps: { + exclude: ['@tabler/icons-react'], + }, }; });