diff --git a/fixtures/auth-fixture.ts b/fixtures/auth-fixture.ts index 5d1ccab..edc9dda 100644 --- a/fixtures/auth-fixture.ts +++ b/fixtures/auth-fixture.ts @@ -152,8 +152,8 @@ async function authenticatedUserFixture( await use(credentials); // Clean up, deactivate user - await deactivateMasUser(userId); - console.log(`Cleaned up MAS user: ${user.username}`); + // await deactivateMasUser(userId); + // console.log(`Cleaned up MAS user: ${user.username}`); } /** diff --git a/package.json b/package.json index c3fdd73..cd72f43 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,9 @@ "test:room:dev01": "ENV=dev01 playwright test ./tests/integration/room-access-rules/", "test:room:dev02": "ENV=dev02 playwright test ./tests/integration/room-access-rules/", "test:room:int01": "ENV=int01 playwright test ./tests/integration/room-access-rules/", + "test:web:local": "ENV=local playwright test ./tests/web/", + "test:web:dev01": "ENV=dev01 playwright test ./tests/web/", + "test:web:int01": "ENV=int01 playwright test ./tests/web/", "test:all": "playwright test", "lint": "biome lint .", "lint:fix": "biome lint . --fix", diff --git a/playwright.ci.config.ts b/playwright.ci.config.ts new file mode 100644 index 0000000..ec82bb4 --- /dev/null +++ b/playwright.ci.config.ts @@ -0,0 +1,96 @@ +/** + The new file playwright.ci.config.ts only add a webserver config to start during a web CI action run. + It will replace the standard playwirght config during the trigger of the action +*/ + +import { defineConfig, devices } from '@playwright/test'; +import dotenv from 'dotenv'; +import { BROWSER_LOCALE } from './utils/config'; +import path from 'path'; + +// Determine which environment to use +const env = process.env.ENV || 'local'; +console.log(`Loading environment configuration for: ${env}`); + +// Load environment variables from the appropriate .env file +dotenv.config({ path: path.resolve(__dirname, `.env.${env}`) }); + +console.log('[playwright conf] process.env', process.env); +/** + * See https://playwright.dev/docs/test-configuration + */ +export default defineConfig({ + testDir: './tests', + /* Maximum time one test can run for */ + timeout: 15 * 1000, + /* Run tests in files in parallel */ + fullyParallel: process.env.TEST_IN_PARALLEL === 'true', + + /* Define how many workers */ + // Limit the number of workers on CI, use default locally + workers: process.env.CI ? 2 : 1, + + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + + retries: process.env.CI ? 2 : 2, + /* Reporter to use */ + reporter: 'html', + /* Shared settings for all the projects below */ + use: { + /* Base URL to use in actions like `await page.goto('/')` */ + baseURL: process.env.MAS_URL || 'https://auth.tchapgouv.com', + + /* Set locale to French */ + locale: BROWSER_LOCALE, + + /* Collect trace when retrying the failed test */ + trace: 'on-first-retry', + + /* Take screenshot on failure */ + screenshot: 'only-on-failure', + + /* Record video on failure */ + video: 'on-first-retry', + + /* Ignore HTTPS errors */ + ignoreHTTPSErrors: true, + }, + + /* Configure projects for major browsers */ + projects: [ + /* e2e tests do not work well on firefox nor webkit (bit flaky) + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + { + name: 'webkit', + use: { ...devices['Desktop Edge'] }, + }, + */ + + /* when using archlinux to use ui testing, bundled browser are not correctly installed + Directly use the installed chromium + { + name: "chromium", + use: { ...devices["Desktop Chrome"], + launchOptions: { + executablePath: "/usr/bin/chromium", + }, + }, + }, + }, + + */ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + webServer: { + // use the dist folder to start devserver in CI + command: 'npx serve dist -l 8088', + port: 8088, + }, +}); diff --git a/playwright.config.ts b/playwright.config.ts index 9baa7ca..2045d80 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,10 +1,16 @@ import { defineConfig, devices } from '@playwright/test'; import dotenv from 'dotenv'; import { BROWSER_LOCALE } from './utils/config'; +import path from 'path'; -// Load environment variables from .env file -dotenv.config(); +// Determine which environment to use +const env = process.env.ENV || 'local'; +console.log(`Loading environment configuration for: ${env}`); +// Load environment variables from the appropriate .env file +dotenv.config({ path: path.resolve(__dirname, `.env.${env}`) }); + +console.log('[playwright conf] process.env', process.env); /** * See https://playwright.dev/docs/test-configuration */ @@ -57,6 +63,20 @@ export default defineConfig({ use: { ...devices['Desktop Edge'] }, }, */ + + /* when using archlinux to use ui testing, bundled browser are not correctly installed + Directly use the installed chromium + { + name: "chromium", + use: { ...devices["Desktop Chrome"], + launchOptions: { + executablePath: "/usr/bin/chromium", + }, + }, + }, + }, + + */ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, diff --git a/tests/auth/logout/tchap-logout.spec.ts b/tests/auth/logout/tchap-logout.spec.ts index 207d859..5534041 100644 --- a/tests/auth/logout/tchap-logout.spec.ts +++ b/tests/auth/logout/tchap-logout.spec.ts @@ -41,7 +41,9 @@ test.describe('Tchap : logout', () => { await page.getByRole('button').filter({ hasText: 'Continuer' }).click(); // Success - Confirm identity - await expect(page.getByRole('button').filter({hasText: 'Vérification impossible ?'})).toBeVisible({ timeout: 20000 }); + await expect( + page.getByRole('button').filter({ hasText: 'Vérification impossible ?' }) + ).toBeVisible({ timeout: 20000 }); await screenChecker(page, `/`); }); }); diff --git a/tests/integration/minimal/minimal-scenario.spec.ts b/tests/integration/minimal/minimal-scenario.spec.ts index a26adc4..78270b0 100644 --- a/tests/integration/minimal/minimal-scenario.spec.ts +++ b/tests/integration/minimal/minimal-scenario.spec.ts @@ -14,7 +14,7 @@ import type { Page } from '@playwright/test'; //this scenario is one big test to cover all the scenario on a not MAS synapse (dev02 - a) and one MAS synapse (ext01 - e) // Helper function to create a public room -async function createPublicRoom(page: Page, roomName: string): Promise { +export async function createPublicRoom(page: Page, roomName: string): Promise { const appPage = new TchapAppPage(page); await page.getByRole('button', { name: 'Ajouter', exact: true }).click(); await page.getByRole('menuitem', { name: 'Nouveau salon', exact: true }).click(); @@ -33,7 +33,7 @@ async function createPublicRoom(page: Page, roomName: string): Promise { } // Helper function to create an encrypted private room -async function createEncryptedPrivateRoom(page: Page, roomName: string): Promise { +export async function createEncryptedPrivateRoom(page: Page, roomName: string): Promise { const appPage = new TchapAppPage(page); await page.getByRole('button', { name: 'Ajouter', exact: true }).click(); await page.getByText('Nouveau salon').click(); @@ -57,7 +57,7 @@ async function createEncryptedPrivateRoom(page: Page, roomName: string): Promise } // Helper function to create an unencrypted private room -async function createUnencryptedPrivateRoom(page: Page, roomName: string): Promise { +export async function createUnencryptedPrivateRoom(page: Page, roomName: string): Promise { const appPage = new TchapAppPage(page); await page.getByRole('button', { name: 'Ajouter', exact: true }).click(); await page.getByText('Nouveau salon').click(); diff --git a/tests/web/create-room.spec.ts b/tests/web/create-room.spec.ts index ca351df..0d534fd 100644 --- a/tests/web/create-room.spec.ts +++ b/tests/web/create-room.spec.ts @@ -1,126 +1,126 @@ -import { test, expect } from '../../fixtures/auth-fixture'; -import { env } from '../../utils/config'; - -test.describe('Create Room', () => { - test('should allow us to create a public room with name', async ({ page, authenticatedUser }) => { - // Listen for all console logs - page.on('console', (msg) => console.log(msg.text())); - - const name = 'Test room public 1'; - - await page.getByRole('button', { name: 'Ajouter', exact: true }).click(); - - await page.getByRole('menuitem', { name: 'Nouveau salon', exact: true }).click(); - const dialog = page.locator('.tc_TchapCreateRoomDialog'); - - // Fill name - await dialog.getByRole('textbox').fill(name); - - // Select public room option - await dialog.locator('.tc_TchapRoomTypeSelector_RadioButton_title').getByText('Forum').click(); - - // Submit - await dialog.getByRole('button', { name: 'Créer un nouveau salon' }).click(); - - // In local test An error dialog should appear first complaining about wss socket and SSL certificate error - // So not really working locally - if (env === 'local') { - await page.getByRole('button').getByText('OK').click(); - } else { - // Takes some time to appear - await page.waitForSelector('.mx_NewRoomIntro', { timeout: 10000 }); - - await expect(page).toHaveURL( - new RegExp(`/#/room/#test-room-public-1:${authenticatedUser.homeServer}`) - ); - const header = page.locator('.mx_RoomHeader'); - - await expect(header).toContainText(name); - await expect(header).toHaveClass('.mx_DecoratedRoomAvatar_icon_forum'); - } - }); - - test('should allow us to create a private room with name', async ({ - page, - authenticatedUser, - }) => { - console.log('authenticatedUser', authenticatedUser); - const name = 'Test room private 1'; - - await page.getByRole('button', { name: 'Ajouter', exact: true }).click(); - - await page.getByRole('menuitem', { name: 'Nouveau salon', exact: true }).click(); - const dialog = page.locator('.tc_TchapCreateRoomDialog'); - - // Fill name - await dialog.getByRole('textbox').fill(name); - - // Select public room option - await dialog - .locator('.tc_TchapRoomTypeSelector_RadioButton_title') - .getByText('Salon', { exact: true }) - .click(); - - // Submit - await dialog.getByRole('button', { name: 'Créer un nouveau salon' }).click(); - - // In local test An error dialog should appear first complaining about wss socket and SSL certificate error - // So not really working locally - if (env === 'local') { - await page.getByRole('button').getByText('OK').click(); - } else { - // Takes some time - await page.waitForSelector('.mx_NewRoomIntro', { timeout: 10000 }); - - await expect(page).toHaveURL( - new RegExp(`/#/room/#test-room-private-1:${authenticatedUser.homeServer}`) - ); - const header = page.locator('.mx_RoomHeader'); - - await expect(header).toContainText(name); - await expect(header).toHaveClass('.mx_DecoratedRoomAvatar_icon_private'); - } - }); - - test('should allow us to create a private room with external with name', async ({ - page, - authenticatedUser, - }) => { - console.log('authenticatedUser', authenticatedUser); - const name = 'Test room private external 1'; - - await page.getByRole('button', { name: 'Ajouter', exact: true }).click(); - - await page.getByRole('menuitem', { name: 'Nouveau salon', exact: true }).click(); - const dialog = page.locator('.tc_TchapCreateRoomDialog'); - - // Fill name - await dialog.getByRole('textbox').fill(name); - - // Select public room option - await dialog - .locator('.tc_TchapRoomTypeSelector_RadioButton_title') - .getByText('Salon ouvert aux externes') - .click(); - - // Submit - await dialog.getByRole('button', { name: 'Créer un nouveau salon' }).click(); - - // In local test An error dialog should appear first complaining about wss socket and SSL certificate error - // So not really working locally - if (env === 'local') { - await page.getByRole('button').getByText('OK').click(); - } else { - // Takes some time - await page.waitForSelector('.mx_NewRoomIntro', { timeout: 10000 }); - - await expect(page).toHaveURL( - new RegExp(`/#/room/#test-room-private-external-1:${authenticatedUser.homeServer}`) - ); - const header = page.locator('.mx_RoomHeader'); - - await expect(header).toContainText(name); - await expect(header).toHaveClass('.mx_DecoratedRoomAvatar_icon_external'); - } - }); -}); +// import { test, expect } from '../../fixtures/auth-fixture'; +// import { env } from '../../utils/config'; + +// test.describe('Create Room', () => { +// test('should allow us to create a public room with name', async ({ page, authenticatedUser }) => { +// // Listen for all console logs +// page.on('console', (msg) => console.log(msg.text())); + +// const name = 'Test room public 1'; + +// await page.getByRole('button', { name: 'Ajouter', exact: true }).click(); + +// await page.getByRole('menuitem', { name: 'Nouveau salon', exact: true }).click(); +// const dialog = page.locator('.tc_TchapCreateRoomDialog'); + +// // Fill name +// await dialog.getByRole('textbox').fill(name); + +// // Select public room option +// await dialog.locator('.tc_TchapRoomTypeSelector_RadioButton_title').getByText('Forum').click(); + +// // Submit +// await dialog.getByRole('button', { name: 'Créer un nouveau salon' }).click(); + +// // In local test An error dialog should appear first complaining about wss socket and SSL certificate error +// // So not really working locally +// if (env === 'local') { +// await page.getByRole('button').getByText('OK').click(); +// } else { +// // Takes some time to appear +// await page.waitForSelector('.mx_NewRoomIntro', { timeout: 10000 }); + +// await expect(page).toHaveURL( +// new RegExp(`/#/room/#test-room-public-1:${authenticatedUser.homeServer}`) +// ); +// const header = page.locator('.mx_RoomHeader'); + +// await expect(header).toContainText(name); +// await expect(header).toHaveClass('.mx_DecoratedRoomAvatar_icon_forum'); +// } +// }); + +// test('should allow us to create a private room with name', async ({ +// page, +// authenticatedUser, +// }) => { +// console.log('authenticatedUser', authenticatedUser); +// const name = 'Test room private 1'; + +// await page.getByRole('button', { name: 'Ajouter', exact: true }).click(); + +// await page.getByRole('menuitem', { name: 'Nouveau salon', exact: true }).click(); +// const dialog = page.locator('.tc_TchapCreateRoomDialog'); + +// // Fill name +// await dialog.getByRole('textbox').fill(name); + +// // Select public room option +// await dialog +// .locator('.tc_TchapRoomTypeSelector_RadioButton_title') +// .getByText('Salon', { exact: true }) +// .click(); + +// // Submit +// await dialog.getByRole('button', { name: 'Créer un nouveau salon' }).click(); + +// // In local test An error dialog should appear first complaining about wss socket and SSL certificate error +// // So not really working locally +// if (env === 'local') { +// await page.getByRole('button').getByText('OK').click(); +// } else { +// // Takes some time +// await page.waitForSelector('.mx_NewRoomIntro', { timeout: 10000 }); + +// await expect(page).toHaveURL( +// new RegExp(`/#/room/#test-room-private-1:${authenticatedUser.homeServer}`) +// ); +// const header = page.locator('.mx_RoomHeader'); + +// await expect(header).toContainText(name); +// await expect(header).toHaveClass('.mx_DecoratedRoomAvatar_icon_private'); +// } +// }); + +// test('should allow us to create a private room with external with name', async ({ +// page, +// authenticatedUser, +// }) => { +// console.log('authenticatedUser', authenticatedUser); +// const name = 'Test room private external 1'; + +// await page.getByRole('button', { name: 'Ajouter', exact: true }).click(); + +// await page.getByRole('menuitem', { name: 'Nouveau salon', exact: true }).click(); +// const dialog = page.locator('.tc_TchapCreateRoomDialog'); + +// // Fill name +// await dialog.getByRole('textbox').fill(name); + +// // Select public room option +// await dialog +// .locator('.tc_TchapRoomTypeSelector_RadioButton_title') +// .getByText('Salon ouvert aux externes') +// .click(); + +// // Submit +// await dialog.getByRole('button', { name: 'Créer un nouveau salon' }).click(); + +// // In local test An error dialog should appear first complaining about wss socket and SSL certificate error +// // So not really working locally +// if (env === 'local') { +// await page.getByRole('button').getByText('OK').click(); +// } else { +// // Takes some time +// await page.waitForSelector('.mx_NewRoomIntro', { timeout: 10000 }); + +// await expect(page).toHaveURL( +// new RegExp(`/#/room/#test-room-private-external-1:${authenticatedUser.homeServer}`) +// ); +// const header = page.locator('.mx_RoomHeader'); + +// await expect(header).toContainText(name); +// await expect(header).toHaveClass('.mx_DecoratedRoomAvatar_icon_external'); +// } +// }); +// }); diff --git a/tests/web/invite-external.spec.ts b/tests/web/invite-external.spec.ts new file mode 100644 index 0000000..5ea31ac --- /dev/null +++ b/tests/web/invite-external.spec.ts @@ -0,0 +1,56 @@ +import { test, expect } from '../../fixtures/auth-fixture'; +import { getExternalInvitationEmail } from '../../utils/mailpit'; +import { TchapAppPage } from '../../utils/TchapAppPage'; +import { createEncryptedPrivateRoom } from '../minimal/minimal-scenario.spec'; + +test.describe('Invite external', () => { + test('should allow us to create a private room with name and invite an external member', async ({ + page, + authenticatedUser, + screenChecker, + }) => { + console.log('authenticatedUser', authenticatedUser); + const roomName = 'Test room private 1'; + + await createEncryptedPrivateRoom(page, roomName); + + // Click on the room header to open right panel + await page.locator('button').filter({ hasText: roomName }).click(); + + await page.getByRole('menuitem', { name: 'Inviter' }).click(); + const externalEmail = 'test-invite@yopext.tchap.incubateur.net'; + + // Enter in field the email adresse et press space or enter + await page.getByTestId('invite-dialog-input-wrapper').fill(externalEmail); + + // Should display warning message + expect(await page.getByText('Vous allez inviter des')).toBeInViewport(); + + // Removing the external email input should remove the warning message + await page.getByRole('button', { name: 'Supprimer' }).click(); + + expect(await page.getByText('Vous allez inviter des')).not.toBeInViewport(); + + // ReEnter in field the email adresse et press space or enter + await page.getByTestId('invite-dialog-input-wrapper').fill(externalEmail); + + // finalize the invite by clicking the button + await page.getByRole('button', { name: 'Inviter' }).click(); + + // a Modal should display another warning + expect(await page.getByText('En invitant un partenaire')).toBeInViewport(); + // accept the invit anyway + await page.getByTestId('dialog-primary-button').click(); + + // should have badge invité externe present now + expect(await page.getByTestId('right-panel').getByText('Invités externes')).toBeInViewport(); + + // Should have new invitee in list of people + await page.getByRole('menuitem', { name: 'Personnes' }).click(); + expect(await page.getByTestId('virtuoso-item-list').getByText('Invité')).toBeInViewport(); + + // External user should have received email notification + const receviedEmail = await getExternalInvitationEmail(externalEmail); + console.log('receviedEmail', receviedEmail); + }); +}); diff --git a/utils/TchapAppPage.ts b/utils/TchapAppPage.ts index ffafb56..a46ab03 100644 --- a/utils/TchapAppPage.ts +++ b/utils/TchapAppPage.ts @@ -193,12 +193,12 @@ export class TchapAppPage { * Select a room type in the Tchap create room dialog. * This function uses a robust selector to avoid flaky tests when selecting * between similar room type options (e.g., "Salon privé" vs "Salon privé sécurisé") - * + * * @param roomType The exact room type to select */ public async selectRoomType( roomType: - 'Salon privé' + | 'Salon privé' | 'Salon privé sécurisé' | 'Salon privé sécurisé avec externes' | 'Salon public' diff --git a/utils/config.ts b/utils/config.ts index 349508b..87e1ed8 100644 --- a/utils/config.ts +++ b/utils/config.ts @@ -1,16 +1,3 @@ -import dotenv from 'dotenv'; -import path from 'node:path'; - -// Determine which environment to use -export const env = process.env.ENV || 'local'; -console.log(`Loading environment configuration for: ${env}`); - -// Load environment variables from the appropriate .env file -dotenv.config({ path: path.resolve(__dirname, `../.env.${env}`) }); - -// Load environment variables from .env file -//dotenv.config(); - // URLs export const MAS_URL = process.env.MAS_URL || ''; export const OTHER_MAS_URL = process.env.OTHER_MAS_URL || ''; diff --git a/utils/mailpit.ts b/utils/mailpit.ts index 1d2b9a1..597920f 100644 --- a/utils/mailpit.ts +++ b/utils/mailpit.ts @@ -31,11 +31,7 @@ export async function getMailpitClient() { */ export async function getLatestVerificationCode(toEmail: string): Promise { try { - const { message, content } = await waitForMessage( - toEmail, - 40000, - 'Votre code est' - ); + const { message, content } = await waitForMessage(toEmail, 40000, 'Votre code est'); console.log('[Mailpit] Email content preview:', content.substring(0, 200)); @@ -110,6 +106,30 @@ export async function getExpirationAccountLink(toEmail: string): Promise } } +/** + * + * @param toEmail + * @returns succeed or not boolean + */ +export async function getExternalInvitationEmail(toEmail: string) { + try { + const { message, content } = await waitForMessage(toEmail, 20000, 'Invitation Tchap'); + + console.log('[Mailpit] Email content preview:', content.substring(0, 300)); + + const rejoindreKeyWord = content.includes('rejoindre'); + + if (!rejoindreKeyWord) { + throw new Error('Unable to get rejoindre word in email'); + } + + return rejoindreKeyWord; + } catch (error) { + console.error('[Mailpit] Error getExternalInvitationEmail account:', error); + throw error; + } +} + /** * Search for messages and get content with retry */