From 3098b9f4fcedb1e92839285594134a37eb9c08f4 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Tue, 2 Apr 2024 12:13:06 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80(app-impress)=20create=20the=20e2e?= =?UTF-8?q?=20app=20impress?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Create the e2e app impress, it will be used to test the app impress. --- src/frontend/apps/e2e/.eslintrc.js | 9 + src/frontend/apps/e2e/.gitignore | 5 + .../apps/e2e/__tests__/app-desk/404.spec.ts | 29 +++ .../apps/e2e/__tests__/app-desk/common.ts | 89 +++++++ .../e2e/__tests__/app-desk/header.spec.ts | 43 ++++ .../e2e/__tests__/app-desk/language.spec.ts | 29 +++ .../__tests__/app-desk/member-create.spec.ts | 218 ++++++++++++++++++ .../__tests__/app-desk/member-delete.spec.ts | 170 ++++++++++++++ .../__tests__/app-desk/member-grid.spec.ts | 65 ++++++ .../apps/e2e/__tests__/app-desk/menu.spec.ts | 81 +++++++ .../apps/e2e/__tests__/app-desk/team.spec.ts | 62 +++++ .../__tests__/app-desk/teams-create.spec.ts | 127 ++++++++++ .../__tests__/app-desk/teams-delete.spec.ts | 78 +++++++ .../__tests__/app-desk/teams-panel.spec.ts | 97 ++++++++ src/frontend/apps/e2e/__tests__/helpers.ts | 21 ++ src/frontend/apps/e2e/package.json | 17 ++ src/frontend/apps/e2e/playwright.config.ts | 54 +++++ src/frontend/apps/e2e/tsconfig.json | 19 ++ 18 files changed, 1213 insertions(+) create mode 100644 src/frontend/apps/e2e/.eslintrc.js create mode 100644 src/frontend/apps/e2e/.gitignore create mode 100644 src/frontend/apps/e2e/__tests__/app-desk/404.spec.ts create mode 100644 src/frontend/apps/e2e/__tests__/app-desk/common.ts create mode 100644 src/frontend/apps/e2e/__tests__/app-desk/header.spec.ts create mode 100644 src/frontend/apps/e2e/__tests__/app-desk/language.spec.ts create mode 100644 src/frontend/apps/e2e/__tests__/app-desk/member-create.spec.ts create mode 100644 src/frontend/apps/e2e/__tests__/app-desk/member-delete.spec.ts create mode 100644 src/frontend/apps/e2e/__tests__/app-desk/member-grid.spec.ts create mode 100644 src/frontend/apps/e2e/__tests__/app-desk/menu.spec.ts create mode 100644 src/frontend/apps/e2e/__tests__/app-desk/team.spec.ts create mode 100644 src/frontend/apps/e2e/__tests__/app-desk/teams-create.spec.ts create mode 100644 src/frontend/apps/e2e/__tests__/app-desk/teams-delete.spec.ts create mode 100644 src/frontend/apps/e2e/__tests__/app-desk/teams-panel.spec.ts create mode 100644 src/frontend/apps/e2e/__tests__/helpers.ts create mode 100644 src/frontend/apps/e2e/package.json create mode 100644 src/frontend/apps/e2e/playwright.config.ts create mode 100644 src/frontend/apps/e2e/tsconfig.json diff --git a/src/frontend/apps/e2e/.eslintrc.js b/src/frontend/apps/e2e/.eslintrc.js new file mode 100644 index 00000000..57ee5e7a --- /dev/null +++ b/src/frontend/apps/e2e/.eslintrc.js @@ -0,0 +1,9 @@ +module.exports = { + root: true, + extends: ["impress/playwright"], + parserOptions: { + tsconfigRootDir: __dirname, + project: ["./tsconfig.json"], + }, + ignorePatterns: ["node_modules"], +}; diff --git a/src/frontend/apps/e2e/.gitignore b/src/frontend/apps/e2e/.gitignore new file mode 100644 index 00000000..848074ac --- /dev/null +++ b/src/frontend/apps/e2e/.gitignore @@ -0,0 +1,5 @@ +# e2e +test-results/ +report/ +blob-report/ +playwright/.cache/ diff --git a/src/frontend/apps/e2e/__tests__/app-desk/404.spec.ts b/src/frontend/apps/e2e/__tests__/app-desk/404.spec.ts new file mode 100644 index 00000000..4e4fa70d --- /dev/null +++ b/src/frontend/apps/e2e/__tests__/app-desk/404.spec.ts @@ -0,0 +1,29 @@ +import { expect, test } from '@playwright/test'; + +import { keyCloakSignIn } from './common'; + +test.beforeEach(async ({ page, browserName }) => { + await page.goto('/'); + await keyCloakSignIn(page, browserName); + await page.goto('unknown-page404'); +}); + +test.describe('404', () => { + test('Checks all the elements are visible', async ({ page }) => { + await expect(page.getByLabel('Image 404')).toBeVisible(); + await expect(page.getByText('Ouch')).toBeVisible(); + await expect( + page.getByText( + 'It seems that the page you are looking for does not exist or cannot be displayed correctly.', + ), + ).toBeVisible(); + await expect(page.getByText('Back to home page')).toBeVisible(); + }); + + test('checks go back to home page redirects to home page', async ({ + page, + }) => { + await page.getByText('Back to home page').click(); + await expect(page).toHaveURL('/'); + }); +}); diff --git a/src/frontend/apps/e2e/__tests__/app-desk/common.ts b/src/frontend/apps/e2e/__tests__/app-desk/common.ts new file mode 100644 index 00000000..ae77bcbc --- /dev/null +++ b/src/frontend/apps/e2e/__tests__/app-desk/common.ts @@ -0,0 +1,89 @@ +import { Page, expect } from '@playwright/test'; + +export const keyCloakSignIn = async (page: Page, browserName: string) => { + const title = await page.locator('h1').first().textContent({ + timeout: 5000, + }); + + if (title?.includes('Sign in to your account')) { + await page + .getByRole('textbox', { name: 'username' }) + .fill(`user-e2e-${browserName}`); + + await page + .getByRole('textbox', { name: 'password' }) + .fill(`password-e2e-${browserName}`); + + await page.click('input[type="submit"]', { force: true }); + } +}; + +export const randomName = (name: string, browserName: string, length: number) => + Array.from({ length }, (_el, index) => { + return `${browserName}-${Math.floor(Math.random() * 10000)}-${index}-${name}`; + }); + +export const createTeam = async ( + page: Page, + teamName: string, + browserName: string, + length: number, +) => { + const panel = page.getByLabel('Teams panel').first(); + const buttonCreate = page.getByRole('button', { name: 'Create the team' }); + + const randomTeams = randomName(teamName, browserName, length); + + for (let i = 0; i < randomTeams.length; i++) { + await panel.getByRole('button', { name: 'Add a team' }).click(); + await page.getByText('Team name').fill(randomTeams[i]); + await expect(buttonCreate).toBeEnabled(); + await buttonCreate.click(); + await expect( + panel.locator('li').nth(0).getByText(randomTeams[i]), + ).toBeVisible(); + } + + return randomTeams; +}; + +export const addNewMember = async ( + page: Page, + index: number, + role: 'Admin' | 'Owner' | 'Member', + fillText: string = 'test', +) => { + const responsePromiseSearchUser = page.waitForResponse( + (response) => + response.url().includes(`/users/?q=${fillText}`) && + response.status() === 200, + ); + await page.getByLabel('Add members to the team').click(); + const inputSearch = page.getByLabel(/Find a member to add to the team/); + + // Select a new user + await inputSearch.fill(fillText); + + // Intercept response + const responseSearchUser = await responsePromiseSearchUser; + const users = (await responseSearchUser.json()).results as { + name: string; + }[]; + + // Choose user + await page.getByRole('option', { name: users[index].name }).click(); + + // Choose a role + await page.getByRole('radio', { name: role }).click(); + + await page.getByRole('button', { name: 'Validate' }).click(); + + const table = page.getByLabel('List members card').getByRole('table'); + + await expect(table.getByText(users[index].name)).toBeVisible(); + await expect( + page.getByText(`Member ${users[index].name} added to the team`), + ).toBeVisible(); + + return users[index].name; +}; diff --git a/src/frontend/apps/e2e/__tests__/app-desk/header.spec.ts b/src/frontend/apps/e2e/__tests__/app-desk/header.spec.ts new file mode 100644 index 00000000..7651dd53 --- /dev/null +++ b/src/frontend/apps/e2e/__tests__/app-desk/header.spec.ts @@ -0,0 +1,43 @@ +import { expect, test } from '@playwright/test'; + +import { keyCloakSignIn } from './common'; + +test.beforeEach(async ({ page, browserName }) => { + await page.goto('/'); + await keyCloakSignIn(page, browserName); +}); + +test.describe('Header', () => { + test('checks all the elements are visible', async ({ page }) => { + const header = page.locator('header').first(); + + await expect(header.getByAltText('Marianne Logo')).toBeVisible(); + + await expect( + header.getByAltText('Freedom Equality Fraternity Logo'), + ).toBeVisible(); + + await expect(header.getByAltText('Impress Logo')).toBeVisible(); + await expect(header.locator('h2').getByText('Impress')).toHaveCSS( + 'color', + 'rgb(0, 0, 145)', + ); + await expect(header.locator('h2').getByText('Impress')).toHaveCSS( + 'font-family', + /Marianne/i, + ); + + await expect( + header.getByText('Les applications de La Suite numérique'), + ).toBeVisible(); + + await expect(header.getByAltText('Language Icon')).toBeVisible(); + + await expect(header.getByText('John Doe')).toBeVisible(); + await expect( + header.getByRole('img', { + name: 'profile picture', + }), + ).toBeVisible(); + }); +}); diff --git a/src/frontend/apps/e2e/__tests__/app-desk/language.spec.ts b/src/frontend/apps/e2e/__tests__/app-desk/language.spec.ts new file mode 100644 index 00000000..e9cb8efc --- /dev/null +++ b/src/frontend/apps/e2e/__tests__/app-desk/language.spec.ts @@ -0,0 +1,29 @@ +import { expect, test } from '@playwright/test'; + +import { keyCloakSignIn } from './common'; + +test.beforeEach(async ({ page, browserName }) => { + await page.goto('/'); + await keyCloakSignIn(page, browserName); +}); + +test.describe('Language', () => { + test('checks the language picker', async ({ page }) => { + await expect( + page.getByRole('button', { + name: 'Create a new team', + }), + ).toBeVisible(); + + const header = page.locator('header').first(); + await header.getByRole('combobox').getByText('EN').click(); + await header.getByRole('option', { name: 'FR' }).click(); + await expect(header.getByRole('combobox').getByText('FR')).toBeVisible(); + + await expect( + page.getByRole('button', { + name: 'Créer un nouveau groupe', + }), + ).toBeVisible(); + }); +}); diff --git a/src/frontend/apps/e2e/__tests__/app-desk/member-create.spec.ts b/src/frontend/apps/e2e/__tests__/app-desk/member-create.spec.ts new file mode 100644 index 00000000..f6ed17eb --- /dev/null +++ b/src/frontend/apps/e2e/__tests__/app-desk/member-create.spec.ts @@ -0,0 +1,218 @@ +import { expect, test } from '@playwright/test'; + +import { createTeam, keyCloakSignIn, randomName } from './common'; + +test.beforeEach(async ({ page, browserName }) => { + await page.goto('/'); + await keyCloakSignIn(page, browserName); +}); + +test.describe('Members Create', () => { + test('it opens the modals to add a member to the team', async ({ + page, + browserName, + }) => { + await createTeam(page, 'member-open-modal', browserName, 1); + + await page.getByLabel('Add members to the team').click(); + + await expect(page.getByText('Add a member')).toBeVisible(); + await expect( + page.getByLabel(/Find a member to add to the team/), + ).toBeVisible(); + + await expect(page.getByRole('button', { name: 'Validate' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Cancel' })).toBeVisible(); + }); + + test('it selects 2 users and 1 invitation', async ({ page, browserName }) => { + const responsePromise = page.waitForResponse( + (response) => + response.url().includes('/users/?q=test') && response.status() === 200, + ); + await createTeam(page, 'member-modal-search-user', browserName, 1); + + await page.getByLabel('Add members to the team').click(); + + await expect(page.getByRole('radio', { name: 'Owner' })).toBeHidden(); + + const inputSearch = page.getByLabel(/Find a member to add to the team/); + + // Select user 1 + await inputSearch.fill('test'); + + const response = await responsePromise; + const users = (await response.json()).results as { + name: string; + }[]; + + await page.getByRole('option', { name: users[0].name }).click(); + + // Select user 2 + await inputSearch.fill('test1'); + await page.getByRole('option', { name: users[1].name }).click(); + + // Select email + const email = randomName('test@test.fr', browserName, 1)[0]; + await inputSearch.fill(email); + await page.getByRole('option', { name: email }).click(); + + // Check user 1 tag + await expect( + page.getByText(`${users[0].name}`, { exact: true }), + ).toBeVisible(); + await expect(page.getByLabel(`Remove ${users[0].name}`)).toBeVisible(); + + // Check user 2 tag + await expect( + page.getByText(`${users[1].name}`, { exact: true }), + ).toBeVisible(); + await expect(page.getByLabel(`Remove ${users[1].name}`)).toBeVisible(); + + // Check invitation tag + await expect(page.getByText(email, { exact: true })).toBeVisible(); + await expect(page.getByLabel(`Remove ${email}`)).toBeVisible(); + + // Check roles are displayed + await expect(page.getByText(/Choose a role/)).toBeVisible(); + await expect(page.getByRole('radio', { name: 'Member' })).toBeChecked(); + await expect(page.getByRole('radio', { name: 'Owner' })).toBeVisible(); + await expect(page.getByRole('radio', { name: 'Admin' })).toBeVisible(); + }); + + test('it sends a new invitation and adds a new member', async ({ + page, + browserName, + }) => { + const responsePromiseSearchUser = page.waitForResponse( + (response) => + response.url().includes('/users/?q=test') && response.status() === 200, + ); + + await createTeam(page, 'member-invitation', browserName, 1); + + await page.getByLabel('Add members to the team').click(); + + // Select a new email + const inputSearch = page.getByLabel(/Find a member to add to the team/); + + const email = randomName('test@test.fr', browserName, 1)[0]; + await inputSearch.fill(email); + await page.getByRole('option', { name: email }).click(); + + // Select a new user + await inputSearch.fill('test'); + const responseSearchUser = await responsePromiseSearchUser; + const users = (await responseSearchUser.json()).results as { + name: string; + }[]; + await page.getByRole('option', { name: users[0].name }).click(); + + // Choose a role + await page.getByRole('radio', { name: 'Admin' }).click(); + + const responsePromiseCreateInvitation = page.waitForResponse( + (response) => + response.url().includes('/invitations/') && response.status() === 201, + ); + const responsePromiseAddMember = page.waitForResponse( + (response) => + response.url().includes('/accesses/') && response.status() === 201, + ); + + await page.getByRole('button', { name: 'Validate' }).click(); + + // Check invitation sent + await expect(page.getByText(`Invitation sent to ${email}`)).toBeVisible(); + const responseCreateInvitation = await responsePromiseCreateInvitation; + expect(responseCreateInvitation.ok()).toBeTruthy(); + + // Check member added + await expect( + page.getByText(`Member ${users[0].name} added to the team`), + ).toBeVisible(); + const responseAddMember = await responsePromiseAddMember; + expect(responseAddMember.ok()).toBeTruthy(); + + const table = page.getByLabel('List members card').getByRole('table'); + await expect(table.getByText(users[0].name)).toBeVisible(); + await expect(table.getByText('Admin')).toBeVisible(); + }); + + test('it try to add twice the same user', async ({ page, browserName }) => { + const responsePromiseSearchUser = page.waitForResponse( + (response) => + response.url().includes('/users/?q=test') && response.status() === 200, + ); + + await createTeam(page, 'member-twice', browserName, 1); + + await page.getByLabel('Add members to the team').click(); + + const inputSearch = page.getByLabel(/Find a member to add to the team/); + await inputSearch.fill('test'); + const responseSearchUser = await responsePromiseSearchUser; + const users = (await responseSearchUser.json()).results as { + name: string; + }[]; + await page.getByRole('option', { name: users[0].name }).click(); + + // Choose a role + await page.getByRole('radio', { name: 'Owner' }).click(); + + const responsePromiseAddMember = page.waitForResponse( + (response) => + response.url().includes('/accesses/') && response.status() === 201, + ); + + await page.getByRole('button', { name: 'Validate' }).click(); + + await expect( + page.getByText(`Member ${users[0].name} added to the team`), + ).toBeVisible(); + const responseAddMember = await responsePromiseAddMember; + expect(responseAddMember.ok()).toBeTruthy(); + + await page.getByLabel('Add members to the team').click(); + + await inputSearch.fill('test'); + await expect( + page.getByRole('option', { name: users[0].name }), + ).toBeHidden(); + }); + + test('it try to add twice the same invitation', async ({ + page, + browserName, + }) => { + await createTeam(page, 'invitation-twice', browserName, 1); + + await page.getByLabel('Add members to the team').click(); + + const inputSearch = page.getByLabel(/Find a member to add to the team/); + + const email = randomName('test@test.fr', browserName, 1)[0]; + await inputSearch.fill(email); + await page.getByRole('option', { name: email }).click(); + + // Choose a role + await page.getByRole('radio', { name: 'Owner' }).click(); + + const responsePromiseCreateInvitation = page.waitForResponse( + (response) => + response.url().includes('/invitations/') && response.status() === 201, + ); + + await page.getByRole('button', { name: 'Validate' }).click(); + + // Check invitation sent + await expect(page.getByText(`Invitation sent to ${email}`)).toBeVisible(); + const responseCreateInvitation = await responsePromiseCreateInvitation; + expect(responseCreateInvitation.ok()).toBeTruthy(); + + await page.getByLabel('Add members to the team').click(); + + await inputSearch.fill(email); + await expect(page.getByRole('option', { name: email })).toBeHidden(); + }); +}); diff --git a/src/frontend/apps/e2e/__tests__/app-desk/member-delete.spec.ts b/src/frontend/apps/e2e/__tests__/app-desk/member-delete.spec.ts new file mode 100644 index 00000000..e43d3be4 --- /dev/null +++ b/src/frontend/apps/e2e/__tests__/app-desk/member-delete.spec.ts @@ -0,0 +1,170 @@ +import { expect, test } from '@playwright/test'; + +import { addNewMember, createTeam, keyCloakSignIn } from './common'; + +test.beforeEach(async ({ page, browserName }) => { + await page.goto('/'); + await keyCloakSignIn(page, browserName); +}); + +test.describe('Members Delete', () => { + test('it cannot delete himself when it is the last owner', async ({ + page, + browserName, + }) => { + await createTeam(page, 'member-delete-1', browserName, 1); + + const table = page.getByLabel('List members card').getByRole('table'); + + const cells = table.getByRole('row').nth(1).getByRole('cell'); + await expect(cells.nth(1)).toHaveText( + new RegExp(`E2E ${browserName}`, 'i'), + ); + await cells.nth(4).getByLabel('Member options').click(); + await page.getByLabel('Open the modal to delete this member').click(); + + await expect( + page.getByText( + 'You are the last owner, you cannot be removed from your team.', + ), + ).toBeVisible(); + await expect(page.getByRole('button', { name: 'Validate' })).toBeDisabled(); + }); + + test('it deletes himself when it is not the last owner', async ({ + page, + browserName, + }) => { + await createTeam(page, 'member-delete-2', browserName, 1); + + await addNewMember(page, 0, 'Owner'); + + const table = page.getByLabel('List members card').getByRole('table'); + + // find row where regexp match the name + const cells = table + .getByRole('row') + .filter({ hasText: new RegExp(`E2E ${browserName}`, 'i') }) + .getByRole('cell'); + await cells.nth(4).getByLabel('Member options').click(); + await page.getByLabel('Open the modal to delete this member').click(); + + await page.getByRole('button', { name: 'Validate' }).click(); + await expect( + page.getByText(`The member has been removed from the team`), + ).toBeVisible(); + await expect( + page.getByRole('button', { name: `Create a new team` }), + ).toBeVisible(); + }); + + test('it cannot delete owner member', async ({ page, browserName }) => { + await createTeam(page, 'member-delete-3', browserName, 1); + + const username = await addNewMember(page, 0, 'Owner'); + + const table = page.getByLabel('List members card').getByRole('table'); + + // find row where regexp match the name + const cells = table + .getByRole('row') + .filter({ hasText: username }) + .getByRole('cell'); + await cells.nth(4).getByLabel('Member options').click(); + await page.getByLabel('Open the modal to delete this member').click(); + + await expect( + page.getByText(`You cannot remove other owner.`), + ).toBeVisible(); + await expect(page.getByRole('button', { name: 'Validate' })).toBeDisabled(); + }); + + test('it deletes admin member', async ({ page, browserName }) => { + await createTeam(page, 'member-delete-4', browserName, 1); + + const username = await addNewMember(page, 0, 'Admin'); + + const table = page.getByLabel('List members card').getByRole('table'); + + // find row where regexp match the name + const cells = table + .getByRole('row') + .filter({ hasText: username }) + .getByRole('cell'); + await cells.nth(4).getByLabel('Member options').click(); + await page.getByLabel('Open the modal to delete this member').click(); + + await page.getByRole('button', { name: 'Validate' }).click(); + await expect( + page.getByText(`The member has been removed from the team`), + ).toBeVisible(); + await expect(table.getByText(username)).toBeHidden(); + }); + + test('it cannot delete owner member when admin', async ({ + page, + browserName, + }) => { + await createTeam(page, 'member-delete-5', browserName, 1); + + const username = await addNewMember(page, 0, 'Owner'); + + const table = page.getByLabel('List members card').getByRole('table'); + + // find row where regexp match the name + const myCells = table + .getByRole('row') + .filter({ hasText: new RegExp(`E2E ${browserName}`, 'i') }) + .getByRole('cell'); + await myCells.nth(4).getByLabel('Member options').click(); + + // Change role to Admin + await page.getByText('Update the role').click(); + const radioGroup = page.getByLabel('Radio buttons to update the roles'); + await radioGroup.getByRole('radio', { name: 'Admin' }).click(); + await page.getByRole('button', { name: 'Validate' }).click(); + + const cells = table + .getByRole('row') + .filter({ hasText: username }) + .getByRole('cell'); + await expect(cells.getByLabel('Member options')).toBeHidden(); + }); + + test('it deletes admin member when admin', async ({ page, browserName }) => { + await createTeam(page, 'member-delete-6', browserName, 1); + + // To not be the only owner + await addNewMember(page, 0, 'Owner'); + + const username = await addNewMember(page, 1, 'Admin', 'something'); + + const table = page.getByLabel('List members card').getByRole('table'); + + // find row where regexp match the name + const myCells = table + .getByRole('row') + .filter({ hasText: new RegExp(`E2E ${browserName}`, 'i') }) + .getByRole('cell'); + await myCells.nth(4).getByLabel('Member options').click(); + + // Change role to Admin + await page.getByText('Update the role').click(); + const radioGroup = page.getByLabel('Radio buttons to update the roles'); + await radioGroup.getByRole('radio', { name: 'Admin' }).click(); + await page.getByRole('button', { name: 'Validate' }).click(); + + const cells = table + .getByRole('row') + .filter({ hasText: username }) + .getByRole('cell'); + await cells.nth(4).getByLabel('Member options').click(); + await page.getByLabel('Open the modal to delete this member').click(); + + await page.getByRole('button', { name: 'Validate' }).click(); + await expect( + page.getByText(`The member has been removed from the team`), + ).toBeVisible(); + await expect(table.getByText(username)).toBeHidden(); + }); +}); diff --git a/src/frontend/apps/e2e/__tests__/app-desk/member-grid.spec.ts b/src/frontend/apps/e2e/__tests__/app-desk/member-grid.spec.ts new file mode 100644 index 00000000..d09e640f --- /dev/null +++ b/src/frontend/apps/e2e/__tests__/app-desk/member-grid.spec.ts @@ -0,0 +1,65 @@ +import { expect, test } from '@playwright/test'; + +import { createTeam, keyCloakSignIn } from './common'; + +test.beforeEach(async ({ page, browserName }) => { + await page.goto('/'); + await keyCloakSignIn(page, browserName); +}); + +test.describe('Member Grid', () => { + test('checks the owner member is displayed correctly', async ({ + page, + browserName, + }) => { + await createTeam(page, 'team-owner', browserName, 1); + + const table = page.getByLabel('List members card').getByRole('table'); + + const thead = table.locator('thead'); + await expect(thead.getByText(/Names/i)).toBeVisible(); + await expect(thead.getByText(/Emails/i)).toBeVisible(); + await expect(thead.getByText(/Roles/i)).toBeVisible(); + + const cells = table.getByRole('row').nth(1).getByRole('cell'); + await expect(cells.nth(0).getByLabel('Member icon')).toBeVisible(); + await expect(cells.nth(1)).toHaveText( + new RegExp(`E2E ${browserName}`, 'i'), + ); + await expect(cells.nth(2)).toHaveText(`user@${browserName}.e2e`); + await expect(cells.nth(3)).toHaveText(/Owner/i); + }); + + test('try to update the owner role but cannot because it is the last owner', async ({ + page, + browserName, + }) => { + await createTeam(page, 'team-owner-role', browserName, 1); + + const table = page.getByLabel('List members card').getByRole('table'); + + const cells = table.getByRole('row').nth(1).getByRole('cell'); + await expect(cells.nth(1)).toHaveText( + new RegExp(`E2E ${browserName}`, 'i'), + ); + await cells.nth(4).getByLabel('Member options').click(); + await page.getByText('Update the role').click(); + + await expect( + page.getByText('You are the last owner, you cannot change your role.'), + ).toBeVisible(); + + const radioGroup = page.getByLabel('Radio buttons to update the roles'); + + const radios = await radioGroup.getByRole('radio').all(); + for (const radio of radios) { + await expect(radio).toBeDisabled(); + } + + await expect( + page.getByRole('button', { + name: 'Validate', + }), + ).toBeDisabled(); + }); +}); diff --git a/src/frontend/apps/e2e/__tests__/app-desk/menu.spec.ts b/src/frontend/apps/e2e/__tests__/app-desk/menu.spec.ts new file mode 100644 index 00000000..d03892af --- /dev/null +++ b/src/frontend/apps/e2e/__tests__/app-desk/menu.spec.ts @@ -0,0 +1,81 @@ +import { expect, test } from '@playwright/test'; + +import { keyCloakSignIn } from './common'; + +test.beforeEach(async ({ page, browserName }) => { + await page.goto('/'); + await keyCloakSignIn(page, browserName); +}); + +test.describe('Menu', () => { + const menuItems = [ + { name: 'Search', isDefault: true }, + { name: 'Favorite', isDefault: false }, + { name: 'Recent', isDefault: false }, + { name: 'Contacts', isDefault: false }, + { name: 'Groups', isDefault: false }, + ]; + for (const { name, isDefault } of menuItems) { + test(`checks that ${name} menu item is displaying correctly`, async ({ + page, + }) => { + const menu = page.locator('menu').first(); + + const buttonMenu = menu.getByLabel(`${name} button`); + await expect(buttonMenu).toBeVisible(); + await buttonMenu.hover(); + await expect(menu.getByLabel('tooltip')).toHaveText(name); + + // Checks the tooltip is with inactive color + await expect(menu.getByLabel('tooltip')).toHaveCSS( + 'background-color', + isDefault ? 'rgb(255, 255, 255)' : 'rgb(22, 22, 22)', + ); + + await buttonMenu.click(); + + // Checks the tooltip has active color + await buttonMenu.hover(); + await expect(menu.getByLabel('tooltip')).toHaveCSS( + 'background-color', + 'rgb(255, 255, 255)', + ); + }); + + test(`checks that ${name} menu item is routing correctly`, async ({ + page, + }) => { + await expect( + page.getByRole('button', { + name: 'Create a new team', + }), + ).toBeVisible(); + + const menu = page.locator('menu').first(); + + const buttonMenu = menu.getByLabel(`${name} button`); + await buttonMenu.click(); + + /* eslint-disable playwright/no-conditional-expect */ + /* eslint-disable playwright/no-conditional-in-test */ + if (isDefault) { + await expect( + page.getByRole('button', { + name: 'Create a new team', + }), + ).toBeVisible(); + } else { + await expect( + page.getByRole('button', { + name: 'Create a new team', + }), + ).toBeHidden(); + + const reg = new RegExp(name.toLowerCase()); + await expect(page).toHaveURL(reg); + } + /* eslint-enable playwright/no-conditional-expect */ + /* eslint-enable playwright/no-conditional-in-test */ + }); + } +}); diff --git a/src/frontend/apps/e2e/__tests__/app-desk/team.spec.ts b/src/frontend/apps/e2e/__tests__/app-desk/team.spec.ts new file mode 100644 index 00000000..60e43712 --- /dev/null +++ b/src/frontend/apps/e2e/__tests__/app-desk/team.spec.ts @@ -0,0 +1,62 @@ +import { expect, test } from '@playwright/test'; + +import { createTeam, keyCloakSignIn, randomName } from './common'; + +test.beforeEach(async ({ page, browserName }) => { + await page.goto('/'); + await keyCloakSignIn(page, browserName); +}); + +test.describe('Team', () => { + test('checks all the top box elements are visible', async ({ + page, + browserName, + }) => { + const teamName = ( + await createTeam(page, 'team-top-box', browserName, 1) + ).shift(); + + await expect(page.getByLabel('icon group')).toBeVisible(); + await expect( + page.getByRole('heading', { + name: `Members of “${teamName}“`, + level: 3, + }), + ).toBeVisible(); + await expect( + page.getByText(`Add people to the “${teamName}“ group.`), + ).toBeVisible(); + + await expect(page.getByText(`1 member`)).toBeVisible(); + + const today = new Date(Date.now()); + const todayFormated = today.toLocaleDateString('en', { + month: '2-digit', + day: '2-digit', + year: 'numeric', + }); + await expect(page.getByText(`Created at ${todayFormated}`)).toBeVisible(); + await expect( + page.getByText(`Last update at ${todayFormated}`), + ).toBeVisible(); + }); + + test('it updates the team name', async ({ page, browserName }) => { + await createTeam(page, 'team-update-name', browserName, 1); + + await page.getByLabel(`Open the team options`).click(); + await page.getByRole('button', { name: `Update the team` }).click(); + + const teamName = randomName('new-team-update-name', browserName, 1)[0]; + await page.getByText('New name...', { exact: true }).fill(teamName); + + await page + .getByRole('button', { name: 'Validate the modification' }) + .click(); + + await expect(page.getByText('The team has been updated.')).toBeVisible(); + await expect( + page.getByText(`Add people to the “${teamName}“ group.`), + ).toBeVisible(); + }); +}); diff --git a/src/frontend/apps/e2e/__tests__/app-desk/teams-create.spec.ts b/src/frontend/apps/e2e/__tests__/app-desk/teams-create.spec.ts new file mode 100644 index 00000000..7365984d --- /dev/null +++ b/src/frontend/apps/e2e/__tests__/app-desk/teams-create.spec.ts @@ -0,0 +1,127 @@ +import { expect, test } from '@playwright/test'; + +import { keyCloakSignIn } from './common'; + +test.beforeEach(async ({ page, browserName }) => { + await page.goto('/'); + await keyCloakSignIn(page, browserName); +}); + +test.describe('Teams Create', () => { + test('checks all the create team elements are visible', async ({ page }) => { + const buttonCreateHomepage = page.getByRole('button', { + name: 'Create a new team', + }); + await buttonCreateHomepage.click(); + await expect(buttonCreateHomepage).toBeHidden(); + + const card = page.getByLabel('Create new team card').first(); + + await expect(card.getByLabel('Team name')).toBeVisible(); + + await expect(card.getByLabel('icon group')).toBeVisible(); + + await expect( + card.getByRole('heading', { + name: 'Name the team', + level: 3, + }), + ).toBeVisible(); + + await expect( + card.getByRole('button', { + name: 'Create the team', + }), + ).toBeVisible(); + + await expect( + card.getByRole('button', { + name: 'Cancel', + }), + ).toBeVisible(); + }); + + test('checks the cancel button interaction', async ({ page }) => { + const buttonCreateHomepage = page.getByRole('button', { + name: 'Create a new team', + }); + await buttonCreateHomepage.click(); + await expect(buttonCreateHomepage).toBeHidden(); + + const card = page.getByLabel('Create new team card').first(); + + await card + .getByRole('button', { + name: 'Cancel', + }) + .click(); + + await expect(buttonCreateHomepage).toBeVisible(); + }); + + test('checks the routing on new team created', async ({ + page, + browserName, + }) => { + const panel = page.getByLabel('Teams panel').first(); + + await panel.getByRole('button', { name: 'Add a team' }).click(); + + const teamName = `My routing team ${browserName}-${Math.floor(Math.random() * 1000)}`; + await page.getByText('Team name').fill(teamName); + await page.getByRole('button', { name: 'Create the team' }).click(); + + const elTeam = page.getByText(`Members of “${teamName}“`); + await expect(elTeam).toBeVisible(); + + await panel.getByRole('button', { name: 'Add a team' }).click(); + await expect(elTeam).toBeHidden(); + + await panel.locator('li').getByText(teamName).click(); + await expect(elTeam).toBeVisible(); + }); + + test('checks alias teams url with homepage', async ({ page }) => { + await expect(page).toHaveURL('/'); + + const buttonCreateHomepage = page.getByRole('button', { + name: 'Create a new team', + }); + + await expect(buttonCreateHomepage).toBeVisible(); + + await page.goto('/teams'); + await expect(buttonCreateHomepage).toBeVisible(); + await expect(page).toHaveURL(/\/teams$/); + }); + + test('checks error when duplicate team', async ({ page, browserName }) => { + const panel = page.getByLabel('Teams panel').first(); + + await panel.getByRole('button', { name: 'Add a team' }).click(); + + const teamName = `My duplicate team ${browserName}-${Math.floor(Math.random() * 1000)}`; + await page.getByText('Team name').fill(teamName); + await page.getByRole('button', { name: 'Create the team' }).click(); + + await panel.getByRole('button', { name: 'Add a team' }).click(); + + await page.getByText('Team name').fill(teamName); + await page.getByRole('button', { name: 'Create the team' }).click(); + + await expect( + page.getByText('Team with this Slug already exists.'), + ).toBeVisible(); + }); + + test('checks 404 on teams/[id] page', async ({ page }) => { + await page.goto('/teams/some-unknown-team'); + await expect( + page.getByText( + 'It seems that the page you are looking for does not exist or cannot be displayed correctly.', + ), + ).toBeVisible({ + timeout: 15000, + }); + }); +}); diff --git a/src/frontend/apps/e2e/__tests__/app-desk/teams-delete.spec.ts b/src/frontend/apps/e2e/__tests__/app-desk/teams-delete.spec.ts new file mode 100644 index 00000000..159a7ef5 --- /dev/null +++ b/src/frontend/apps/e2e/__tests__/app-desk/teams-delete.spec.ts @@ -0,0 +1,78 @@ +import { expect, test } from '@playwright/test'; + +import { addNewMember, createTeam, keyCloakSignIn } from './common'; + +test.beforeEach(async ({ page, browserName }) => { + await page.goto('/'); + await keyCloakSignIn(page, browserName); +}); + +test.describe('Teams Delete', () => { + test('it deletes the team when we are owner', async ({ + page, + browserName, + }) => { + await createTeam(page, 'team-update-name-1', browserName, 1); + + await page.getByLabel(`Open the team options`).click(); + await page.getByRole('button', { name: `Delete the team` }).click(); + await page.getByRole('button', { name: `Confirm deletion` }).click(); + await expect(page.getByText(`The team has been removed.`)).toBeVisible(); + await expect( + page.getByRole('button', { name: `Create a new team` }), + ).toBeVisible(); + }); + + test('it cannot delete the team when we are admin', async ({ + page, + browserName, + }) => { + await createTeam(page, 'team-update-name-2', browserName, 1); + + await addNewMember(page, 0, 'Owner'); + + // Change role to Admin + const table = page.getByLabel('List members card').getByRole('table'); + const myCells = table + .getByRole('row') + .filter({ hasText: new RegExp(`E2E ${browserName}`, 'i') }) + .getByRole('cell'); + await myCells.nth(4).getByLabel('Member options').click(); + + await page.getByText('Update the role').click(); + const radioGroup = page.getByLabel('Radio buttons to update the roles'); + await radioGroup.getByRole('radio', { name: 'Admin' }).click(); + await page.getByRole('button', { name: 'Validate' }).click(); + + // Delete the team button should be hidden + await page.getByLabel(`Open the team options`).click(); + await expect( + page.getByRole('button', { name: `Delete the team` }), + ).toBeHidden(); + }); + + test('it cannot delete the team when we are member', async ({ + page, + browserName, + }) => { + await createTeam(page, 'team-update-name-3', browserName, 1); + + await addNewMember(page, 0, 'Owner'); + + // Change role to Admin + const table = page.getByLabel('List members card').getByRole('table'); + const myCells = table + .getByRole('row') + .filter({ hasText: new RegExp(`E2E ${browserName}`, 'i') }) + .getByRole('cell'); + await myCells.nth(4).getByLabel('Member options').click(); + + await page.getByText('Update the role').click(); + const radioGroup = page.getByLabel('Radio buttons to update the roles'); + await radioGroup.getByRole('radio', { name: 'Member' }).click(); + await page.getByRole('button', { name: 'Validate' }).click(); + + // Option button should be hidden + await expect(page.getByLabel(`Open the team options`)).toBeHidden(); + }); +}); diff --git a/src/frontend/apps/e2e/__tests__/app-desk/teams-panel.spec.ts b/src/frontend/apps/e2e/__tests__/app-desk/teams-panel.spec.ts new file mode 100644 index 00000000..6440fd37 --- /dev/null +++ b/src/frontend/apps/e2e/__tests__/app-desk/teams-panel.spec.ts @@ -0,0 +1,97 @@ +import { expect, test } from '@playwright/test'; + +import { waitForElementCount } from '../helpers'; + +import { createTeam, keyCloakSignIn } from './common'; + +test.beforeEach(async ({ page, browserName }) => { + await page.goto('/'); + await keyCloakSignIn(page, browserName); +}); + +test.describe('Teams Panel', () => { + test('checks all the elements are visible', async ({ page }) => { + const panel = page.getByLabel('Teams panel').first(); + + await expect(panel.getByText('Recents')).toBeVisible(); + + await expect( + panel.getByRole('button', { + name: 'Sort the teams', + }), + ).toBeVisible(); + + await expect( + panel.getByRole('button', { + name: 'Add a team', + }), + ).toBeVisible(); + }); + + test('checks the sort button', async ({ page }) => { + const responsePromiseSortDesc = page.waitForResponse( + (response) => + response.url().includes('/teams/?page=1&ordering=-created_at') && + response.status() === 200, + ); + + const responsePromiseSortAsc = page.waitForResponse( + (response) => + response.url().includes('/teams/?page=1&ordering=created_at') && + response.status() === 200, + ); + + const panel = page.getByLabel('Teams panel').first(); + + await panel + .getByRole('button', { + name: 'Sort the teams by creation date ascendent', + }) + .click(); + + const responseSortAsc = await responsePromiseSortAsc; + expect(responseSortAsc.ok()).toBeTruthy(); + + await panel + .getByRole('button', { + name: 'Sort the teams by creation date descendent', + }) + .click(); + + const responseSortDesc = await responsePromiseSortDesc; + expect(responseSortDesc.ok()).toBeTruthy(); + }); + + test('checks the infinite scroll', async ({ page, browserName }) => { + test.setTimeout(90000); + const panel = page.getByLabel('Teams panel').first(); + + const randomTeams = await createTeam( + page, + 'team-infinite', + browserName, + 40, + ); + + await expect(panel.locator('li')).toHaveCount(20); + await panel.getByText(randomTeams[24]).click(); + + await waitForElementCount(panel.locator('li'), 21, 10000); + expect(await panel.locator('li').count()).toBeGreaterThan(20); + }); + + test('checks the hover and selected state', async ({ page, browserName }) => { + const panel = page.getByLabel('Teams panel').first(); + await createTeam(page, 'team-hover', browserName, 2); + + const selectedTeam = panel.locator('li').nth(0); + await expect(selectedTeam).toHaveCSS( + 'background-color', + 'rgb(202, 202, 251)', + ); + + const hoverTeam = panel.locator('li').nth(1); + await hoverTeam.hover(); + await expect(hoverTeam).toHaveCSS('background-color', 'rgb(227, 227, 253)'); + }); +}); diff --git a/src/frontend/apps/e2e/__tests__/helpers.ts b/src/frontend/apps/e2e/__tests__/helpers.ts new file mode 100644 index 00000000..678b555a --- /dev/null +++ b/src/frontend/apps/e2e/__tests__/helpers.ts @@ -0,0 +1,21 @@ +import { Locator } from '@playwright/test'; + +export async function waitForElementCount( + locator: Locator, + count: number, + timeout: number, +) { + let elapsedTime = 0; + const interval = 200; // Check every 200 ms + while (elapsedTime < timeout) { + const currentCount = await locator.count(); + if (currentCount >= count) { + return true; + } + await locator.page().waitForTimeout(interval); // Wait for the interval before checking again + elapsedTime += interval; + } + throw new Error( + `Timeout after ${timeout}ms waiting for element count to be at least ${count}`, + ); +} diff --git a/src/frontend/apps/e2e/package.json b/src/frontend/apps/e2e/package.json new file mode 100644 index 00000000..b36b599d --- /dev/null +++ b/src/frontend/apps/e2e/package.json @@ -0,0 +1,17 @@ +{ + "name": "app-e2e", + "version": "0.1.0", + "private": true, + "scripts": { + "lint": "eslint . --ext .ts", + "install": "playwright install --with-deps", + "test": "playwright test", + "test:ui": "yarn test --ui" + }, + "devDependencies": { + "@playwright/test": "1.42.1", + "@types/node": "*", + "eslint-config-impress": "*", + "typescript": "*" + } +} diff --git a/src/frontend/apps/e2e/playwright.config.ts b/src/frontend/apps/e2e/playwright.config.ts new file mode 100644 index 00000000..482c3c4f --- /dev/null +++ b/src/frontend/apps/e2e/playwright.config.ts @@ -0,0 +1,54 @@ +import { defineConfig, devices } from '@playwright/test'; + +const PORT = process.env.PORT || 3000; + +const baseURL = `http://localhost:${PORT}`; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + // Timeout per test + timeout: 30 * 1000, + testDir: './__tests__', + outputDir: './test-results', + + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 3 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: [['html', { outputFolder: './report' }]], + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + baseURL: baseURL, + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + webServer: { + command: `cd ../.. && yarn app:${ + process.env.CI ? 'start -p ' : 'dev --port ' + } ${PORT}`, + url: baseURL, + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI, + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'], locale: 'en-US' }, + }, + { + name: 'webkit', + use: { ...devices['Desktop Safari'], locale: 'en-US' }, + }, + ], +}); diff --git a/src/frontend/apps/e2e/tsconfig.json b/src/frontend/apps/e2e/tsconfig.json new file mode 100644 index 00000000..19e42d22 --- /dev/null +++ b/src/frontend/apps/e2e/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + }, + "include": ["**/*.ts"], + "exclude": ["node_modules"] +}