diff --git a/package.json b/package.json index c216eab0d..821987820 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "lint:ts": "turbo run lint:ts --color --no-daemon", "playwright:test": "pnpm --dir playwright run playwright:test", "playwright:test:ui": "pnpm --dir playwright run playwright:test:ui", + "playwright:test:integration": "pnpm --dir playwright run playwright:test:integration", "prepare": "husky", "test": "turbo run test --concurrency 1 --color --no-daemon", "test:cov": "turbo run test:cov --color --no-daemon", diff --git a/playwright/config/console.ts b/playwright/config/console.ts index 78d13d3f0..1e7ffe8f7 100644 --- a/playwright/config/console.ts +++ b/playwright/config/console.ts @@ -17,19 +17,19 @@ export interface Credentials { // Users referenced in Keycloak dev realm (../keycloak/realms/realm-dev.json) export const adminUser: Credentials = { id: '387216f1-3b87-4211-9cac-4371125e1175', - username: 'admin', - password: 'admin', + username: process.env.CONSOLE_ADMIN_USERNAME?.trim() || 'admin', + password: process.env.CONSOLE_ADMIN_PASSWORD?.trim() || 'admin', firstName: 'Admin', lastName: 'ADMIN', - email: 'admin@test.com', + email: process.env.CONSOLE_ADMIN_EMAIL?.trim() || 'admin@test.com', } export const testUser: Credentials = { id: 'cb8e5b4b-7b7b-40f5-935f-594f48ae6565', - username: 'test', - password: 'test', + username: process.env.CONSOLE_TEST_USERNAME?.trim() || 'test', + password: process.env.CONSOLE_TEST_PASSWORD?.trim() || 'test', firstName: 'Jean', lastName: 'DUPOND', - email: 'test@test.com', + email: process.env.CONSOLE_TEST_EMAIL?.trim() || 'test@test.com', } export const cnolletUser: Credentials = { id: 'cb8e5b4b-7b7b-40f5-935f-594f48ae6567', @@ -57,8 +57,8 @@ export async function signInCloudPiNative({ }) { const { username, password } = credentials await page.getByRole('link', { name: 'Se connecter' }).click() - await page.locator('#username').fill(username) - await page.locator('#password').fill(password) - await page.locator('#kc-login').click() + await page.getByRole('textbox', { name: 'Username or email' }).fill(username) + await page.getByRole('textbox', { name: 'Password' }).fill(password) + await page.getByRole('button', { name: 'Sign In' }).click() await expect(page.locator('#top')).toContainText('Cloud π Native') } diff --git a/playwright/e2e-tests/clusters.spec.ts b/playwright/e2e-tests/clusters.spec.ts index c8ca23778..c9f030da6 100644 --- a/playwright/e2e-tests/clusters.spec.ts +++ b/playwright/e2e-tests/clusters.spec.ts @@ -171,10 +171,7 @@ test.describe('Clusters page', () => { confidentiality: 'dedicated', }) // Delete - await deleteCluster({ - page, - clusterName, - }) + await deleteCluster(page, clusterName) // Validate await page.getByTestId('projectsSearchInput').fill(clusterName) await expect(page.getByTestId('noClusterMsg')).toBeVisible() diff --git a/playwright/e2e-tests/projects.spec.ts b/playwright/e2e-tests/projects.spec.ts index f48abc836..5c3b4da1a 100644 --- a/playwright/e2e-tests/projects.spec.ts +++ b/playwright/e2e-tests/projects.spec.ts @@ -1,6 +1,4 @@ -import type { Page } from '@playwright/test' import { expect, test } from '@playwright/test' -import { faker } from '@faker-js/faker' import { adminUser, clientURL, @@ -8,53 +6,12 @@ import { signInCloudPiNative, testUser, } from '../config/console' -import { addProject, deleteValidationInput } from './utils' - -// Assuming we are on a given Project page, add a random repository with given name, or a generated one -async function addRandomRepositoryToProject({ - page, - repositoryName, -}: { - page: Page - repositoryName?: string -}) { - repositoryName = repositoryName ?? faker.string.alpha(10).toLowerCase() - await page.getByTestId('addRepoLink').click() - await page.getByTestId('internalRepoNameInput').fill(repositoryName) - await page - .getByTestId('externalRepoUrlInput') - .fill(`${faker.internet.url({ appendSlash: true })}myrepository.git`) - await page.getByTestId('addRepoBtn').click() - await expect(page.getByTestId(`repoTr-${repositoryName}`)).toContainText( - repositoryName, - ) - return repositoryName -} - -// Assuming we are on a given Project page, and we have a Repository and a Branch name, -// start branch synchronisation with this branch -async function synchronizeBranchOnRepository({ - page, - repositoryName, - branchName, -}: { - page: Page - repositoryName: string - branchName?: string -}) { - branchName = branchName ?? faker.string.alpha(10).toLowerCase() - await page.getByRole('cell', { name: repositoryName }).click() - await page.getByTestId('branchNameInput').fill(branchName) - await page.getByTestId('syncRepoBtn').click() - await page - .getByTestId('resource-modal') - .getByRole('button', { name: 'Fermer' }) - .click() - await expect( - page.getByText('Travail de synchronisation lancé'), - ).toBeVisible() - return branchName -} +import { + addProject, + addRandomRepositoryToProject, + deleteValidationInput, + synchronizeBranchOnRepository, +} from './utils' test.describe('Projects page', () => { test( diff --git a/playwright/e2e-tests/utils.ts b/playwright/e2e-tests/utils.ts index c42ddb727..6a07046bf 100644 --- a/playwright/e2e-tests/utils.ts +++ b/playwright/e2e-tests/utils.ts @@ -81,6 +81,60 @@ export async function deleteProject(page: Page, projectName: string) { ).not.toBeVisible() } +// Assuming we are on a given Project page, add a random repository with given name, or a generated one +export async function addRandomRepositoryToProject({ + page, + repositoryName, + externalRepoUrlInput, +}: { + page: Page + repositoryName?: string + externalRepoUrlInput?: string +}) { + repositoryName = repositoryName ?? faker.string.alpha(10).toLowerCase() + await page.getByTestId('addRepoLink').click() + await page.getByTestId('internalRepoNameInput').fill(repositoryName) + if (externalRepoUrlInput) { + await page + .getByTestId('externalRepoUrlInput') + .fill(externalRepoUrlInput) + } else { + await page + .getByTestId('externalRepoUrlInput') + .fill(`${faker.internet.url({ appendSlash: true })}myrepository.git`) + } + await page.getByTestId('addRepoBtn').click() + await expect(page.getByTestId(`repoTr-${repositoryName}`)).toContainText( + repositoryName, + ) + return repositoryName +} + +// Assuming we are on a given Project page, and we have a Repository and a Branch name, +// start branch synchronisation with this branch +export async function synchronizeBranchOnRepository({ + page, + repositoryName, + branchName, +}: { + page: Page + repositoryName: string + branchName?: string +}) { + branchName = branchName ?? faker.string.alpha(10).toLowerCase() + await page.getByRole('cell', { name: repositoryName }).click() + await page.getByTestId('branchNameInput').fill(branchName) + await page.getByTestId('syncRepoBtn').click() + await page + .getByTestId('resource-modal') + .getByRole('button', { name: 'Fermer' }) + .click() + await expect( + page.getByText('Travail de synchronisation lancé'), + ).toBeVisible() + return branchName +} + // functions use in admin-stages.spec.ts export async function createStage({ page, @@ -172,7 +226,7 @@ export async function createCluster({ await expect(page.locator('#projects-select')).toBeVisible() } if (associateStageNames) { - for (const customStageName in associateStageNames) { + for (const customStageName of associateStageNames) { await page .getByTestId('choice-selector-search-stages-select') .fill(customStageName) @@ -197,13 +251,10 @@ export async function createCluster({ return clusterName } -export async function deleteCluster({ - page, - clusterName, -}: { - page: Page - clusterName: string -}) { +export async function deleteCluster( + page: Page, + clusterName: string, +) { await page.getByTestId('menuAdministrationClusters').click() await expect(page.getByTestId('cpin-loader')).toHaveCount(0) await page.getByTestId('projectsSearchInput').fill(clusterName) @@ -233,6 +284,17 @@ export async function createZone({ page }: { page: Page }): Promise { return zoneName } +export async function deleteZone( + page: Page, + zoneName: string, +) { + await page.getByTestId('menuAdministrationZones').click() + await page.getByRole('link', { name: zoneName }).click() + await page.getByTestId('showDeleteZoneBtn').click() + await page.getByTestId('deleteZoneInput').fill('DELETE') + await page.getByTestId('deleteZoneBtn').click() +} + // functions use in environment.spec.ts export async function addEnvToProject({ page, diff --git a/playwright/integration-tests/integration.spec.ts b/playwright/integration-tests/integration.spec.ts new file mode 100644 index 000000000..75c8d3c29 --- /dev/null +++ b/playwright/integration-tests/integration.spec.ts @@ -0,0 +1,124 @@ +import { expect, test } from '@playwright/test' +import { adminUser, clientURL, signInCloudPiNative } from '../config/console' + +import { + addProject, + addRandomRepositoryToProject, + createCluster, + createStage, + createZone, + deleteCluster, + deleteProject, + deleteStage, + deleteZone, +} from '../e2e-tests/utils' + +const zonesToDelete: string[] = [] +const projectsToDelete: string[] = [] +const stagesToDelete: string[] = [] +const clustersToDelete: string[] = [] + +test.describe('Integration tests', { tag: '@integ' }, () => { + test.describe.configure({ mode: 'serial' }) + + test('Admin setup', async ({ page }) => { + await page.goto(clientURL) + await signInCloudPiNative({ page, credentials: adminUser }) + await page.getByTestId('menuAdministrationBtn').click() + const zoneName = await createZone({ page }) + zonesToDelete.push(zoneName) + // we need to attains 7 stages to be able to use associateStageNames argument in createCluster + await page.getByRole('link', { name: 'Console Cloud π Native' }).click() + const customStageName1 = await createStage({ page, check: true, stagesToDelete }) + await page.getByRole('link', { name: 'Console Cloud π Native' }).click() + const customStageName2 = await createStage({ page, check: true, stagesToDelete }) + await page.getByRole('link', { name: 'Console Cloud π Native' }).click() + const customStageName3 = await createStage({ page, check: true, stagesToDelete }) + stagesToDelete.push(customStageName1) + stagesToDelete.push(customStageName2) + stagesToDelete.push(customStageName3) + const clusterName = await createCluster({ + page, + zone: zoneName, + confidentiality: 'public', + associateStageNames: [customStageName1, customStageName2, customStageName3], + }) + clustersToDelete.push(clusterName) + }) + + test('Cleanup admin test data', async ({ page }) => { + await page.goto(clientURL) + await signInCloudPiNative({ page, credentials: adminUser }) + await page.getByTestId('menuAdministrationBtn').click() + for (const stageName of stagesToDelete) { + await deleteStage(page, stageName) + } + console.log('Stages:', stagesToDelete) + for (const clusterName of clustersToDelete) { + await deleteCluster(page, clusterName) + } + console.log('Clusters:', clustersToDelete) + for (const zoneName of zonesToDelete) { + await deleteZone(page, zoneName) + } + console.log('Zones:', zonesToDelete) + }) + + test('User flow', async ({ page }) => { + await page.goto(clientURL) + await signInCloudPiNative({ page, credentials: adminUser }) + const project = await addProject({ page }) + const projectName = project.name + projectsToDelete.push(projectName) + await addRandomRepositoryToProject({ + page, + repositoryName: 'tutojava', + externalRepoUrlInput: 'https://github.com/cloud-pi-native/tuto-java.git', + }) + // Check if mirror pipeline is successful + await page.getByTestId('test-tab-services').click() + const page1Promise = page.waitForEvent('popup') + await page.getByRole('link', { name: 'Gitlab' }).click() + const page1 = await page1Promise + await page1.getByTestId('group-name').filter({ hasText: 'mirror' }).click() + await expect(page1.getByTestId('status_success_borderless-icon')).toBeVisible() + // Run build pipeline and check if it is successful + await page1.getByRole('link', { name: projectName }).click() + await page1.getByTestId('group-name').filter({ hasText: 'tutojava' }).click() + await page1.getByRole('button', { name: 'Build' }).hover() + await page1.getByRole('link', { name: 'Pipelines' }).click() + await page1.getByTestId('run-pipeline-button').click() + await page1.getByTestId('run-pipeline-button').click() + await expect( + page1.getByRole('link', { name: 'Status: Passed read_secret' }), + ).toBeVisible() + await expect( + page1.getByRole('link', { name: 'Status: Passed test-app' }), + ).toBeVisible() + await expect( + page1.getByRole('link', { name: 'Status: Passed docker-build', exact: true }), + ).toBeVisible() + await expect( + page1.getByRole('link', { name: 'Status: Passed docker-build-2' }), + ).toBeVisible() + // Check if sonar scan is available + const page2Promise = page.waitForEvent('popup') + await page.getByRole('link', { name: 'SonarQube' }).click() + const page2 = await page2Promise + await page2.getByRole('button', { name: 'OpenID Connect Log in with' }).click() + await page2.getByPlaceholder('Search for projects...').fill(projectName) + await page2.getByRole('link', { name: `${projectName}-tutojava` }).click() + await expect( + page2.getByTestId('overview__quality-gate-panel').getByText('Passed', { exact: true }), + ).toBeVisible() + }) + + test('Cleanup user test data', async ({ page }) => { + await page.goto(clientURL) + await signInCloudPiNative({ page, credentials: adminUser }) + for (const projectName of projectsToDelete) { + await deleteProject(page, projectName) + } + console.log('Projects:', projectsToDelete) + }) +}) diff --git a/playwright/package.json b/playwright/package.json index 88dc7fcfd..6588614cc 100644 --- a/playwright/package.json +++ b/playwright/package.json @@ -13,6 +13,7 @@ "scripts": { "playwright:test": "pnpm exec playwright test", "playwright:test:ui": "pnpm exec playwright test --ui", + "playwright:test:integration": "pnpm exec playwright test -c playwright.config.integration.ts", "format": "eslint ./ --fix" }, "devDependencies": { diff --git a/playwright/playwright.config.integration.ts b/playwright/playwright.config.integration.ts new file mode 100644 index 000000000..0a788d8a1 --- /dev/null +++ b/playwright/playwright.config.integration.ts @@ -0,0 +1,51 @@ +import path from 'node:path' + +import dotenv from 'dotenv' + +import { defineConfig, devices } from '@playwright/test' + +dotenv.config({ + path: path.resolve(__dirname, '..', 'apps/client', '.env.docker'), +}) + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './integration-tests', + /* Run tests in files in parallel */ + fullyParallel: false, + /* 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 ? 3 : 1, + workers: 1, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* All timeouts are in milliseconds */ + // Timeout for each and every `test` block + timeout: Number(process.env.CONSOLE_GLOBAL_TIMEOUT) || 30_000, + // Timeout for each and every `expect` command + expect: { + timeout: Number(process.env.CONSOLE_EXPECT_TIMEOUT) || 10_000, + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + ], +}) diff --git a/playwright/playwright.config.ts b/playwright/playwright.config.ts index 549b7842b..72af27be6 100644 --- a/playwright/playwright.config.ts +++ b/playwright/playwright.config.ts @@ -30,10 +30,10 @@ export default defineConfig({ /* All timeouts are in milliseconds */ // Timeout for each and every `test` block - timeout: 30_000, + timeout: Number(process.env.CONSOLE_GLOBAL_TIMEOUT) || 30_000, // Timeout for each and every `expect` command expect: { - timeout: 10_000, + timeout: Number(process.env.CONSOLE_EXPECT_TIMEOUT) || 10_000, }, /* Configure projects for major browsers */