diff --git a/.github/workflows/run-ci-cd.yaml b/.github/workflows/run-ci-cd.yaml index fc41cf5b36..757398d59b 100644 --- a/.github/workflows/run-ci-cd.yaml +++ b/.github/workflows/run-ci-cd.yaml @@ -327,7 +327,7 @@ jobs: run: | timeout 5m bash -c ' until wget --spider http://localhost:9000/a; do - echo "Waiting for backend..." + echo "Waiting for the backend..." sleep 5 done ' @@ -339,12 +339,53 @@ jobs: run: | pg_restore -h localhost -U nest_user_e2e -d nest_db_e2e < backend/data/nest.dump - - name: Build frontend end-to-end testing image + - name: Build frontend image + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 + with: + build-args: | + ENV_FILE=.env.e2e.example + NEXT_PUBLIC_E2E_BACKEND_BASE_URL=http://localhost:9000 + cache-from: | + type=gha + type=registry,ref=owasp/nest:frontend-e2e-cache + cache-to: | + type=gha,compression=zstd + context: frontend + file: docker/frontend/Dockerfile + load: true + platforms: linux/amd64 + tags: owasp/nest:frontend-e2e-latest + + - name: Run frontend in the background + run: | + docker run \ + --env-file frontend/.env.e2e.example \ + --name e2e-nest-frontend \ + --network host \ + -d \ + -e NEXT_SERVER_CSRF_URL=http://localhost:9000/csrf/ \ + -e NEXT_SERVER_GRAPHQL_URL=http://localhost:9000/graphql/ \ + -p 3000:3000 \ + owasp/nest:frontend-e2e-latest + + - name: Wait for frontend to be ready + run: | + timeout 5m bash -c ' + until wget --spider http://localhost:3000/; do + echo "Waiting for the frontend..." + sleep 5 + done + ' + echo "Frontend is up!" + + - name: Build e2e testing image uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 with: cache-from: | type=gha - type=registry,ref=owasp/nest:test-frontend-e2e-cache + type=registry,ref=owasp/nest:frontend-e2e-tests-cache + cache-to: | + type=gha,compression=zstd context: frontend file: docker/frontend/Dockerfile.e2e.test load: true @@ -353,8 +394,11 @@ jobs: - name: Run frontend end-to-end tests run: | - docker run --env-file frontend/.env.e2e.example owasp/nest:test-frontend-e2e-latest pnpm run test:e2e - timeout-minutes: 15 + docker run \ + --network host \ + -e FRONTEND_URL=http://localhost:3000 \ + owasp/nest:test-frontend-e2e-latest pnpm run test:e2e + timeout-minutes: 20 run-frontend-a11y-tests: name: Run frontend accessibility tests diff --git a/.github/workflows/setup-backend-environment/action.yaml b/.github/workflows/setup-backend-environment/action.yaml index e48ae6bc0e..8a7a22ed8f 100644 --- a/.github/workflows/setup-backend-environment/action.yaml +++ b/.github/workflows/setup-backend-environment/action.yaml @@ -31,7 +31,7 @@ runs: shell: bash - name: Build backend image - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 with: cache-from: | type=gha diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5c7eef2ca5..8835779ca3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -400,6 +400,12 @@ This command automatically: - Executes the e2e tests - Cleans up containers when done +To run e2e tests without initializing the database, use the following command: + +```bash +make test-frontend-e2e-no-init +``` + For debugging, you can run the e2e backend separately: ```bash @@ -412,6 +418,20 @@ Then load data manually in another terminal: make load-data-e2e ``` +For debugging the frontend e2e tests UI mode, run: + +```bash +make test-frontend-e2e-ui +``` + +To run the frontend e2e tests UI mode without the database initialized, use the following command: + +```bash +make test-frontend-e2e-ui-no-db-init +``` + +You can access the UI at [http://localhost:3800](http://localhost:3800). + ### Running Fuzz Tests Run the fuzz tests with the following command: diff --git a/.dockerignore b/backend/.dockerignore similarity index 58% rename from .dockerignore rename to backend/.dockerignore index 2fd9b6cce0..dbe9ede275 100644 --- a/.dockerignore +++ b/backend/.dockerignore @@ -2,14 +2,9 @@ .bash_history .cache .dockerignore -.env -.env.* -.envs .git .gitignore .local -.next -.pnpm-store .pytest_cache .ruff_cache .venv @@ -17,13 +12,8 @@ *.log *.pem **/__pycache__ -backend/data -backend/staticfiles +staticfiles build coverage dist -docker -Dockerfile.* -node_modules -npm-debug.log venv diff --git a/cspell/custom-dict.txt b/cspell/custom-dict.txt index 51c4bee007..223d64c5cb 100644 --- a/cspell/custom-dict.txt +++ b/cspell/custom-dict.txt @@ -1,5 +1,6 @@ ARUN AVD +Abhay Agentic Agsoc Aichi @@ -7,6 +8,8 @@ Aissue Atqc Aupdated BOTTOMPADDING +Balbin +Balbin's Birda CCSP CISSP @@ -57,6 +60,7 @@ Sivagangai Sonarqube Stegen T04T40NHX +Tarandach Truncator Turbopack Twitterbot @@ -113,6 +117,7 @@ geocoders geoloc geopy gha +glueup graphiql graphqler gunicorn diff --git a/docker-compose/e2e/compose.yaml b/docker-compose/e2e/compose.yaml index f8edf5842a..d2f833686e 100644 --- a/docker-compose/e2e/compose.yaml +++ b/docker-compose/e2e/compose.yaml @@ -89,21 +89,49 @@ services: volumes: - e2e-cache-data:/data - e2e-tests: - container_name: e2e-nest-tests + frontend: build: context: ../../frontend - dockerfile: ../docker/frontend/Dockerfile.e2e.test - command: > - sh -c ' - pnpm run test:e2e - ' + dockerfile: ../docker/frontend/Dockerfile + args: + ENV_FILE: .env.e2e.example + NEXT_PUBLIC_E2E_BACKEND_BASE_URL: http://backend:9000 + NEXT_SERVER_CSRF_URL: http://backend:9000/csrf/ + NEXT_SERVER_GRAPHQL_URL: http://backend:9000/graphql/ + container_name: e2e-nest-frontend depends_on: backend: condition: service_healthy env_file: ../../frontend/.env.e2e.example + healthcheck: + interval: 5s + retries: 10 + start_period: 20s + test: > + sh -c ' + wget --spider http://0.0.0.0:3000/ + ' + timeout: 5s networks: - e2e-nest-network + ports: + - 3500:3000 + + e2e-tests: + build: + context: ../../frontend + dockerfile: ../docker/frontend/Dockerfile.e2e.test + container_name: e2e-nest-tests + command: ${E2E_TEST_COMMAND:-sh -c "pnpm run test:e2e"} + depends_on: + frontend: + condition: service_healthy + environment: + - FRONTEND_URL=http://frontend:3000 + networks: + - e2e-nest-network + ports: + - 3800:3800 networks: e2e-nest-network: diff --git a/docker/frontend/Dockerfile b/docker/frontend/Dockerfile index 3f5aaabf0a..f991fe10cd 100644 --- a/docker/frontend/Dockerfile +++ b/docker/frontend/Dockerfile @@ -11,7 +11,11 @@ ENV APK_CACHE_DIR="/app/.cache/apk" \ NPM_CONFIG_CACHE="/app/.npm" \ PNPM_HOME="/pnpm" -ENV PATH="$PNPM_HOME:$PATH" +ARG ENV_FILE=.env +ARG NEXT_PUBLIC_E2E_BACKEND_BASE_URL + +ENV PATH="$PNPM_HOME:$PATH" \ + NEXT_PUBLIC_E2E_BACKEND_BASE_URL=${NEXT_PUBLIC_E2E_BACKEND_BASE_URL} RUN mkdir -p ${APK_CACHE_DIR} && \ ln -fns ${APK_CACHE_DIR} ${APK_SYMLINK_DIR} @@ -27,7 +31,8 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ corepack install && corepack enable pnpm && \ pnpm install --frozen-lockfile --ignore-scripts -COPY --chmod=444 .env .pnpmrc next.config.ts postcss.config.js tailwind.config.mjs tsconfig.json ./ +COPY --chmod=444 .pnpmrc next.config.ts postcss.config.js tailwind.config.mjs tsconfig.json ./ +COPY --chmod=444 ${ENV_FILE} ./.env COPY --chmod=555 public public COPY --chmod=555 src src diff --git a/docker/frontend/Dockerfile.e2e.test b/docker/frontend/Dockerfile.e2e.test index c155221a01..0f9fa93803 100644 --- a/docker/frontend/Dockerfile.e2e.test +++ b/docker/frontend/Dockerfile.e2e.test @@ -11,13 +11,14 @@ ENV NPM_CONFIG_RETRY=5 \ WORKDIR /app -COPY --chmod=444 package.json pnpm-lock.yaml ./ +COPY --chmod=444 __tests__/e2e/package.json __tests__/e2e/pnpm-lock.yaml ./ RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ corepack install && corepack enable pnpm && \ pnpm install --frozen-lockfile --ignore-scripts +COPY .pnpmrc next.config.ts postcss.config.js playwright.config.ts tailwind.config.mjs tsconfig.json ./ +COPY src/utils src/utils COPY __tests__/e2e __tests__/e2e COPY __tests__/mockData __tests__/mockData -COPY .pnpmrc next.config.ts postcss.config.js playwright.config.ts tailwind.config.mjs tsconfig.json ./ -COPY public public -COPY src src + +EXPOSE 3800 diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000000..2b88337c70 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,17 @@ +.ash_history +.bash_history +.cache +.dockerignore +.git +.gitignore +.local +.next +.pnpm-store +.vscode +*.log +*.pem +**/node_modules +build +dist +node_modules +npm-debug.log diff --git a/frontend/.env.e2e.example b/frontend/.env.e2e.example index aec00ea3a7..63b347c91e 100644 --- a/frontend/.env.e2e.example +++ b/frontend/.env.e2e.example @@ -1,16 +1,16 @@ -NEXTAUTH_SECRET= +NEXTAUTH_SECRET=your-nextauth-secret NEXTAUTH_URL=http://localhost:3000/ -NEXT_PUBLIC_API_URL=http://localhost:9000/ -NEXT_PUBLIC_CSRF_URL=http://localhost:9000/csrf/ -NEXT_PUBLIC_ENVIRONMENT=local -NEXT_PUBLIC_GRAPHQL_URL=http://localhost:9000/graphql/ +NEXT_PUBLIC_API_URL=/ +NEXT_PUBLIC_CSRF_URL=/csrf/ +NEXT_PUBLIC_ENVIRONMENT=production +NEXT_PUBLIC_GRAPHQL_URL=/graphql/ NEXT_PUBLIC_GTM_ID= NEXT_PUBLIC_IDX_URL=http://localhost:9000/idx/ NEXT_PUBLIC_IS_PROJECT_HEALTH_ENABLED=true NEXT_PUBLIC_RELEASE_VERSION= NEXT_PUBLIC_SENTRY_DSN= -NEXT_SERVER_CSRF_URL=http://localhost:9000/csrf/ +NEXT_SERVER_CSRF_URL=http://backend:9000/csrf/ NEXT_SERVER_DISABLE_SSR=false NEXT_SERVER_GITHUB_CLIENT_ID=your-github-client-id NEXT_SERVER_GITHUB_CLIENT_SECRET=your-github-client-secret -NEXT_SERVER_GRAPHQL_URL=http://localhost:9000/graphql/ +NEXT_SERVER_GRAPHQL_URL=http://backend:9000/graphql/ diff --git a/frontend/Makefile b/frontend/Makefile index b0a1ed99d6..c2bb0fa794 100644 --- a/frontend/Makefile +++ b/frontend/Makefile @@ -44,6 +44,16 @@ format-frontend-code: graphql-codegen: @cd frontend && pnpm run graphql-codegen +init-e2e-db: + @docker container rm -f e2e-nest-db >/dev/null 2>&1 || true + @docker volume rm -f nest-e2e_e2e-db-data >/dev/null 2>&1 || true + @DOCKER_BUILDKIT=1 docker compose \ + --project-name nest-e2e \ + -f docker-compose/e2e/compose.yaml up \ + --abort-on-container-exit backend cache db data-loader \ + --build \ + --remove-orphans + lighthouse-ci: @cd frontend && pnpm run lighthouse-ci @@ -114,14 +124,26 @@ test-frontend-a11y: -t nest-test-frontend-a11y @docker run --env-file frontend/.env.example --rm nest-test-frontend-a11y pnpm run test:a11y +test-frontend-e2e-no-db-init: + @DOCKER_BUILDKIT=1 docker compose \ + --project-name nest-e2e \ + -f docker-compose/e2e/compose.yaml up \ + --abort-on-container-exit backend cache db frontend e2e-tests \ + --build \ + --remove-orphans + test-frontend-e2e: ## Run frontend e2e tests -test-frontend-e2e: - @docker container rm -f e2e-nest-db >/dev/null 2>&1 || true - @docker volume rm -f nest-e2e_e2e-db-data >/dev/null 2>&1 || true - @DOCKER_BUILDKIT=1 \ - docker compose --project-name nest-e2e -f docker-compose/e2e/compose.yaml up --build --remove-orphans --abort-on-container-exit db cache backend data-loader - @DOCKER_BUILDKIT=1 NEXT_PUBLIC_ENVIRONMENT=local \ - docker compose --project-name nest-e2e -f docker-compose/e2e/compose.yaml up --build --remove-orphans --abort-on-container-exit db cache backend e2e-tests +test-frontend-e2e: init-e2e-db test-frontend-e2e-no-db-init + +test-frontend-e2e-ui-no-db-init: + @DOCKER_BUILDKIT=1 E2E_TEST_COMMAND="pnpm run test:e2e:ui" docker compose \ + --project-name nest-e2e \ + -f docker-compose/e2e/compose.yaml up \ + --abort-on-container-exit backend cache db frontend e2e-tests \ + --build \ + --remove-orphans + +test-frontend-e2e-ui: init-e2e-db test-frontend-e2e-ui-no-db-init test-frontend-unit: @DOCKER_BUILDKIT=1 NEXT_PUBLIC_ENVIRONMENT=local docker build \ diff --git a/frontend/__tests__/e2e/components/Footer.spec.ts b/frontend/__tests__/e2e/components/Footer.spec.ts index 373c6fc032..37543693d5 100644 --- a/frontend/__tests__/e2e/components/Footer.spec.ts +++ b/frontend/__tests__/e2e/components/Footer.spec.ts @@ -1,4 +1,3 @@ -import { mockHomeData } from '@e2e/data/mockHomeData' import { test, expect, devices } from '@playwright/test' // Desktop tests @@ -9,20 +8,6 @@ test.describe('Footer - Desktop (Chrome)', () => { }) test.beforeEach(async ({ page }) => { - await page.route('**/graphql/', async (route) => { - await route.fulfill({ - status: 200, - json: mockHomeData, - }) - }) - await page.context().addCookies([ - { - name: 'csrftoken', - value: 'abc123', - domain: 'localhost', - path: '/', - }, - ]) await page.goto('/') }) test('should have buttons', async ({ page }) => { @@ -45,20 +30,6 @@ test.use({ test.describe('Footer - Mobile (iPhone 13)', () => { test.beforeEach(async ({ page }) => { - await page.route('**/graphql/', async (route) => { - await route.fulfill({ - status: 200, - json: mockHomeData, - }) - }) - await page.context().addCookies([ - { - name: 'csrftoken', - value: 'abc123', - domain: 'localhost', - path: '/', - }, - ]) await page.goto('/') }) test('should have buttons', async ({ page }) => { diff --git a/frontend/__tests__/e2e/components/Header.spec.ts b/frontend/__tests__/e2e/components/Header.spec.ts index 6481ac2e51..0b4492910e 100644 --- a/frontend/__tests__/e2e/components/Header.spec.ts +++ b/frontend/__tests__/e2e/components/Header.spec.ts @@ -1,4 +1,3 @@ -import { mockHomeData } from '@e2e/data/mockHomeData' import { test, expect, devices } from '@playwright/test' // Desktop tests @@ -9,20 +8,6 @@ test.describe('Header - Desktop (Chrome)', () => { }) test.beforeEach(async ({ page }) => { - await page.route('**/graphql/', async (route) => { - await route.fulfill({ - status: 200, - json: mockHomeData, - }) - }) - await page.context().addCookies([ - { - name: 'csrftoken', - value: 'abc123', - domain: 'localhost', - path: '/', - }, - ]) await page.goto('/') }) @@ -74,20 +59,6 @@ test.use({ test.describe('Header - Mobile (iPhone 13)', () => { test.beforeEach(async ({ page }) => { - await page.route('**/graphql/', async (route) => { - await route.fulfill({ - status: 200, - json: mockHomeData, - }) - }) - await page.context().addCookies([ - { - name: 'csrftoken', - value: 'abc123', - domain: 'localhost', - path: '/', - }, - ]) await page.goto('/') }) diff --git a/frontend/__tests__/e2e/data/mockHomeData.ts b/frontend/__tests__/e2e/data/mockHomeData.ts deleted file mode 100644 index 5138caf6a9..0000000000 --- a/frontend/__tests__/e2e/data/mockHomeData.ts +++ /dev/null @@ -1,250 +0,0 @@ -export const mockHomeData = { - data: { - recentProjects: [ - { - createdAt: '2025-03-05T13:49:06.000Z', - key: 'project_1', - leaders: ['Leader 1', 'Leader 2'], - name: 'Project 1', - openIssuesCount: 0, - repositoriesCount: 1, - type: 'documentation', - }, - { - createdAt: '2025-03-05T13:45:28.000Z', - key: 'eks-goat', - leaders: ['Leader 3', 'Leader 4'], - name: 'OWASP EKS Goat', - openIssuesCount: 0, - repositoriesCount: 1, - type: 'code', - }, - { - createdAt: '2025-03-03T16:33:05.000Z', - key: 'dojo-shield', - leaders: ['Leader 5', 'Leader 6'], - name: 'OWASP Dojo Shield', - openIssuesCount: 1, - repositoriesCount: 1, - type: 'documentation', - }, - ], - recentPosts: [ - { - authorName: 'Author 1', - authorImageUrl: 'https://owasp.org/assets/images/people/shruti_kulkarni.jpeg', - publishedAt: '2025-03-06T07:00:00.000Z', - title: 'Post 1', - url: 'https://owasp.org/blog/2025/03/06/owasp-education-and-training-committee-update.html', - }, - { - authorName: 'Author 2', - authorImageUrl: 'https://owasp.org/assets/images/people/leader_springett.png', - publishedAt: '2025-02-24T07:00:00.000Z', - title: 'Post 2', - url: 'https://owasp.org/blog/2025/02/24/advisory-on-implementation-of-software-bill-of-materials-for-vulnerability-management.html', - }, - { - authorName: 'Author 3', - authorImageUrl: 'https://owasp.org/assets/images/people/leader_bjoern.jpg', - publishedAt: '2025-01-29T06:00:00.000Z', - title: 'Author 3', - url: 'https://owasp.org/blog/2025/01/29/juice-shop-leadership.html', - }, - ], - recentChapters: [ - { - createdAt: '2025-03-18T01:03:09.000Z', - key: 'chapter_1', - leaders: ['Leader 1', 'Leader 3'], - name: 'Chapter 1', - suggestedLocation: 'Pune, Maharashtra, India', - }, - { - createdAt: '2025-03-13T01:01:01.000Z', - key: 'chapter_2', - leaders: ['Leader 1', 'Leader 2'], - name: 'Chapter 2', - suggestedLocation: 'Location 2', - }, - { - createdAt: '2025-02-25T02:04:57.000Z', - key: 'chapter_3', - leaders: ['Leader 1', 'Leader 2'], - name: 'Chapter 3', - suggestedLocation: 'Location 3', - }, - ], - topContributors: [ - { - name: 'Contributor 1', - login: 'contributor_1', - avatarUrl: 'https://avatars.githubusercontent.com/u/3531020?v=4', - }, - { - name: 'Contributor 2', - login: 'contributor_2', - avatarUrl: 'https://avatars.githubusercontent.com/u/862914?v=4', - }, - { - name: 'Contributor 3', - login: 'contributor_3', - avatarUrl: 'https://avatars.githubusercontent.com/u/103866?v=4', - }, - ], - recentIssues: [ - { - createdAt: '2025-03-20T14:03:10.000Z', - title: 'Issue 1', - repositoryName: 'Dependency-Track', - url: 'https://github.com/DependencyTrack/hyades/issues/1724', - author: { - avatarUrl: 'https://avatars.githubusercontent.com/u/5693141?v=4', - login: 'user1', - name: 'Niklas', - }, - }, - { - createdAt: '2025-03-20T10:30:33.000Z', - title: 'Issue 2', - repositoryName: 'BLT', - url: 'https://github.com/OWASP-BLT/BLT/issues/4013', - author: { - avatarUrl: 'https://avatars.githubusercontent.com/u/24620264?v=4', - login: 'bug-reporter-bot', - name: '', - }, - }, - { - createdAt: '2025-03-20T09:36:39.000Z', - title: 'Troubleshooting the OWASP Dependency-Check Scanner', - repositoryName: 'devguard', - url: 'https://github.com/l3montree-dev/devguard/issues/336', - author: { - avatarUrl: 'https://avatars.githubusercontent.com/u/38261809?v=4', - login: 'user2', - name: 'Tim Bastin', - }, - }, - ], - recentReleases: [ - { - author: { - avatarUrl: 'https://avatars.githubusercontent.com/in/15368?v=4', - login: 'github-actions[bot]', - name: '', - }, - name: 'Release 1', - publishedAt: '2025-03-19T13:51:35.000Z', - repositoryName: 'repo-1', - tagName: 'v2.1.0', - url: 'https://github.com/CycloneDX/cyclonedx-node-npm/releases/tag/v2.1.0', - }, - { - author: { - avatarUrl: 'https://avatars.githubusercontent.com/u/2201626?v=4', - login: 'arkid15r', - name: 'Arkadii Yakovets', - }, - name: 'Release 2', - publishedAt: '2025-03-20T01:41:36.000Z', - repositoryName: 'Nest', - tagName: '0.1.25', - url: 'https://github.com/OWASP/Nest/releases/tag/0.1.25', - }, - { - author: { - avatarUrl: 'https://avatars.githubusercontent.com/u/13718901?v=4', - login: 'J12934', - name: 'author 3', - }, - name: 'v4.14.0', - publishedAt: '2025-03-18T12:24:40.000Z', - repositoryName: 'secureCodeBox', - tagName: 'v4.14.0', - url: 'https://github.com/secureCodeBox/secureCodeBox/releases/tag/v4.14.0', - }, - ], - recentMilestones: [ - { - author: { - avatarUrl: 'https://avatars.githubusercontent.com/u/33333?v=4', - login: 'milestone-author1', - name: 'Milestone Author 1', - }, - title: 'v2.0.0 Release', - openIssuesCount: 5, - closedIssuesCount: 15, - repositoryName: 'Home Repo One', - organizationName: 'OWASP', - createdAt: '2025-03-01T10:00:00.000Z', - url: 'https://github.com/OWASP/repo-one/milestone/1', - }, - ], - sponsors: [ - { - imageUrl: - 'https://raw.githubusercontent.com/OWASP/owasp.github.io/main/assets/images/corp-member-logo/InfoSecMap-logo-small.png', - name: 'InfoSecMap', - sponsorType: 'DIAMOND', - url: 'https://infosecmap.com', - }, - { - imageUrl: - 'https://raw.githubusercontent.com/OWASP/owasp.github.io/main/assets/images/corp-member-logo/Zimperiumlogo_300x90px.jpeg', - name: 'Sponsor 2', - sponsorType: 'GOLD', - url: 'https://www.zimperium.com', - }, - { - imageUrl: - 'https://raw.githubusercontent.com/OWASP/owasp.github.io/main/assets/images/corp-member-logo/Salt_Security.png', - name: 'Salt Security', - sponsorType: 'SILVER', - url: 'https://salt.security', - }, - ], - statsOverview: { - activeChaptersStats: 280, - activeProjectsStats: 250, - contributorsStats: 11400, - countriesStats: 90, - slackWorkspaceStats: 35800, - }, - upcomingEvents: [ - { - category: 'APPSEC_DAYS', - endDate: '2025-04-06T00:00:00.000Z', - key: 'event_1', - name: 'Event 1', - startDate: '2025-04-05T00:00:00.000Z', - summary: - 'The OWASP Boston Application Security Conference 2025 will take place on April 5, 2025. This event is organized by the Boston chapter of OWASP and features presenters and workshops. The focus will be on the latest trends in application security. It is an opportunity for attendees to learn and engage with experts in the field.', - suggestedLocation: 'Boston, USA', - url: 'https://basconf.org', - }, - { - category: 'PARTNER', - endDate: '2025-05-23T00:00:00.000Z', - key: 'event_2', - name: 'Event 2', - startDate: '2025-05-19T00:00:00.000Z', - summary: - 'The github 2025 event will take place in Oslo from May 19 to May 23, 2025. Attendees can find discounted tickets by using the provided link. This event will focus on various topics related to technology and development. It promises to be an informative experience for those interested in these fields.', - suggestedLocation: 'Oslo, Norway', - url: 'https://event.checkin.no/82562/ndc-oslo-2025?action=campaignInvite&campaignInviteHash=ZGF3bi5haXRrZW5Ab3dhc3AuY29t&campaignInviteId=4927', - }, - { - category: 'GLOBAL', - endDate: '2025-05-30T00:00:00.000Z', - key: 'event_3', - name: 'Event 3', - startDate: '2025-05-26T00:00:00.000Z', - summary: - 'The OWASP Global AppSec EU 2025 event will take place from May 26 to May 30, 2025. The location for this event has not been specified in the information provided. This event focuses on application security and brings together professionals from around the world. Participants will have the opportunity to learn and share knowledge about improving security in software development.', - suggestedLocation: 'Barcelona, Spain', - url: 'https://owasp.glueup.com/event/owasp-global-appsec-eu-2025-123983', - }, - ], - }, -} diff --git a/frontend/__tests__/e2e/package.json b/frontend/__tests__/e2e/package.json new file mode 100644 index 0000000000..9020936ded --- /dev/null +++ b/frontend/__tests__/e2e/package.json @@ -0,0 +1,23 @@ +{ + "name": "e2e", + "private": true, + "scripts": { + "test:e2e": "npx playwright test", + "test:e2e:ui": "npx playwright test --ui-host=0.0.0.0 --ui-port=3800" + }, + "dependencies": { + "@playwright/test": "^1.58.1", + "millify": "^6.1.0", + "dayjs": "^1.11.19" + }, + "engines": { + "node": "^24.0.0" + }, + "packageManager": "pnpm@10.32.1", + "pnpm": { + "overrides": { + "glob": "^11.1.0", + "test-exclude": "^7.0.1" + } + } +} diff --git a/frontend/__tests__/e2e/pages/About.spec.ts b/frontend/__tests__/e2e/pages/About.spec.ts index 7f7dbe6c7a..e99df1aa0f 100644 --- a/frontend/__tests__/e2e/pages/About.spec.ts +++ b/frontend/__tests__/e2e/pages/About.spec.ts @@ -1,46 +1,9 @@ import { expectBreadCrumbsToBeVisible } from '@e2e/helpers/expects' -import { mockAboutData } from '@mockData/mockAboutData' import { test, expect } from '@playwright/test' test.describe('About Page', () => { test.beforeEach(async ({ page }) => { - await page.route('**/graphql/', async (route) => { - const request = route.request() - const postData = request.postDataJSON() - - if (postData.operationName === 'GetAboutPageData') { - const leaders = ['arkid15r', 'kasya', 'mamicidal'] - const leaderData = leaders.reduce( - (acc, leader, index) => ({ - ...acc, - [`leader${index + 1}`]: mockAboutData.users[leader], - }), - {} - ) - return route.fulfill({ - status: 200, - json: { - data: { - project: mockAboutData.project, - topContributors: mockAboutData.topContributors, - ...leaderData, - }, - }, - }) - } - return route.continue() - }) - - await page.context().addCookies([ - { - name: 'csrftoken', - value: 'abc123', - domain: 'localhost', - path: '/', - }, - ]) - - await page.goto('/about') + await page.goto('/about', { timeout: 25000 }) }) test('renders main sections correctly', async ({ page }) => { @@ -52,8 +15,9 @@ test.describe('About Page', () => { }) test('displays contributor information when data is loaded', async ({ page }) => { - await expect(page.getByText('Contributor 1', { exact: true })).toBeVisible() - await expect(page.getByText('Contributor 2', { exact: true })).toBeVisible() + await expect(page.getByText('Ahmed Gouda', { exact: true })).toBeVisible() + await expect(page.getByText('Abhay Mishra', { exact: true })).toBeVisible() + await expect(page.getByText('Raj gupta', { exact: true })).toBeVisible() }) test('displays leaders data when data is loaded', async ({ page }) => { @@ -81,7 +45,12 @@ test.describe('About Page', () => { test('loads roadmap items correctly', async ({ page }) => { await expect(page.getByRole('heading', { name: 'Roadmap' })).toBeVisible() - await expect(page.getByText('Contribution Hub title')).toBeVisible() + await expect(page.getByText('Enhance OWASP Nest Availability and Performance')).toBeVisible() + await expect( + page.getByText( + 'This milestone focuses on modernizing OWASP Nest to improve reliability, scalability, and availability. It includes migrating the Django + Ninja backend to AWS Lambda using Zappa, with static/media served via S3 and long-running tasks adapted for serverless execution. In parallel, a PoC for Infrastructure as Code will provision and manage key components - Lambda, ECS/Fargate, S3, RDS, Redis, and an EC2 instance for nightly syncs - using Terraform or an alternative tool.' + ) + ).toBeVisible() const roadmapSection = page .locator('div') @@ -90,23 +59,14 @@ test.describe('About Page', () => { .last() await roadmapSection.getByRole('button', { name: 'Show more' }).click() - - for (const milestone of mockAboutData.project.recentMilestones) { - await expect(page.getByText(milestone.title, { exact: true })).toBeVisible() - await expect(page.getByText(milestone.body)).toBeVisible() - } - }) - - test('displays project statistics with correct values', async ({ page }) => { - await expect(page.getByText('1.2K+Contributors')).toBeVisible() - await expect(page.getByText('40+Open Issues')).toBeVisible() - await expect(page.getByText('60+Forks')).toBeVisible() - await expect(page.getByText('890+Stars')).toBeVisible() + await expect(page.getByText('OWASP Community Snapshots', { exact: true })).toBeVisible() }) - test('opens user profile in new window when leader button is clicked', async ({ page }) => { - await page.getByRole('button', { name: 'View Profile' }).first().click() - await expect(page).toHaveURL('/members/arkid15r') + test('displays project statistics', async ({ page }) => { + await expect(page.getByText('Contributors').last()).toBeVisible() + await expect(page.getByText('Open Issues').last()).toBeVisible() + await expect(page.getByText('Forks').last()).toBeVisible() + await expect(page.getByText('Stars').last()).toBeVisible() }) test('breadcrumb renders correct segments on /about', async ({ page }) => { @@ -133,4 +93,9 @@ test.describe('About Page', () => { await expect(page.getByText('Project Inception')).toBeVisible() }) + + test('opens user profile in new window when leader button is clicked', async ({ page }) => { + await page.getByRole('button', { name: 'View Profile' }).first().click() + await expect(page).toHaveURL('/members/arkid15r') + }) }) diff --git a/frontend/__tests__/e2e/pages/CalendarButton.spec.ts b/frontend/__tests__/e2e/pages/CalendarButton.spec.ts index c4fd2a875b..ee3eb39e50 100644 --- a/frontend/__tests__/e2e/pages/CalendarButton.spec.ts +++ b/frontend/__tests__/e2e/pages/CalendarButton.spec.ts @@ -1,48 +1,36 @@ import fs from 'node:fs' -import { mockHomeData } from '@e2e/data/mockHomeData' import { test, expect } from '@playwright/test' import slugify from 'utils/slugify' test.describe('Calendar Export Functionality', () => { test.beforeEach(async ({ page }) => { - await page.route('**/graphql/', async (route) => { - await route.fulfill({ - status: 200, - json: mockHomeData, - }) - }) - await page.context().addCookies([ - { - name: 'csrftoken', - value: 'abc123', - domain: 'localhost', - path: '/', - }, - ]) - await page.goto('/') + await page.goto('/', { timeout: 25000 }) }) test('should download a valid ICS file when clicked', async ({ page }) => { - const calendarButton = page.getByRole('button', { name: 'Add Event 1 to Calendar' }) - await expect(calendarButton).toBeVisible() - + const firstEventButton = page.getByRole('button', { name: /Add .* to Calendar/i }).first() + await expect(firstEventButton).toBeVisible() + const buttonText = + (await firstEventButton.getAttribute('aria-label')) || (await firstEventButton.innerText()) + const eventName = buttonText + .replace(/^Add\s+/i, '') + .replace(/\s+to Calendar$/i, '') // NOSONAR + .trim() const downloadPromise = page.waitForEvent('download') - - await calendarButton.click() - + await firstEventButton.click() const download = await downloadPromise - - expect(download.suggestedFilename()).toBe(`${slugify('Event 1')}.ics`) - + const expectedFilename = `${slugify(eventName)}.ics` + expect(download.suggestedFilename()).toBe(expectedFilename) const path = await download.path() expect(path, 'Expected Playwright to provide a download path').toBeTruthy() - const content = fs.readFileSync(path, 'utf-8') - - expect(content).toContain('BEGIN:VCALENDAR') - expect(content).toContain('VERSION:2.0') - expect(content).toContain('BEGIN:VEVENT') - expect(content).toContain('SUMMARY:') - expect(content).toContain('END:VCALENDAR') + if (path) { + const content = fs.readFileSync(path, 'utf-8') + expect(content).toContain('BEGIN:VCALENDAR') + expect(content).toContain('VERSION:2.0') + expect(content).toContain('BEGIN:VEVENT') + expect(content).toContain(`SUMMARY:${eventName}`) + expect(content).toContain('END:VCALENDAR') + } }) }) diff --git a/frontend/__tests__/e2e/pages/ChapterDetails.spec.ts b/frontend/__tests__/e2e/pages/ChapterDetails.spec.ts index 0811c41278..3b14824d2d 100644 --- a/frontend/__tests__/e2e/pages/ChapterDetails.spec.ts +++ b/frontend/__tests__/e2e/pages/ChapterDetails.spec.ts @@ -1,34 +1,23 @@ -import { mockChapterDetailsData } from '@mockData/mockChapterDetailsData' import { test, expect } from '@playwright/test' test.describe('Chapter Details Page', () => { test.beforeEach(async ({ page }) => { - await page.route('**/graphql/', async (route) => { - await route.fulfill({ - status: 200, - json: { data: mockChapterDetailsData }, - }) - }) - await page.context().addCookies([ - { - name: 'csrftoken', - value: 'abc123', - domain: 'localhost', - path: '/', - }, - ]) - await page.goto('/chapters/test-chapter') + await page.goto('/chapters/rosario', { timeout: 25000 }) }) test('should have a heading and summary', async ({ page }) => { - await expect(page.getByRole('heading', { name: 'OWASP Test Chapter' })).toBeVisible() - await expect(page.getByText('This is a test chapter summary.')).toBeVisible() + await expect(page.getByRole('heading', { name: 'OWASP Rosario', exact: true })).toBeVisible() + await expect( + page.getByText(/The OWASP Rosario chapter is located in Argentina, South America/i) + ).toBeVisible() }) test('should have chapter details block', async ({ page }) => { - await expect(page.getByText('Location: Test City, Test')).toBeVisible() - await expect(page.getByText('Region: Test Region')).toBeVisible() - await expect(page.getByRole('link', { name: 'https://owasp.org/test-chapter' })).toBeVisible() + await expect(page.getByText('Location: Rosario, Santa Fe, Argentina')).toBeVisible() + await expect(page.getByText('Region: South America')).toBeVisible() + await expect( + page.getByRole('link', { name: 'https://owasp.org/www-chapter-rosario' }) + ).toBeVisible() }) test('should have map with geolocation', async ({ page }) => { @@ -43,33 +32,20 @@ test.describe('Chapter Details Page', () => { const marker = page.locator('.leaflet-marker-icon').first() await marker.click() - const popupButton = page.getByRole('button', { name: 'OWASP Test Chapter' }) + // The popup typically matches the chapter name + const popupButton = page.getByRole('button', { name: 'OWASP Rosario' }) await expect(popupButton).toBeVisible() }) test('should have top contributors', async ({ page }) => { await expect(page.getByRole('heading', { name: 'Top Contributors' })).toBeVisible() await expect( - page.getByRole('img', { name: "Contributor 1's avatar", exact: true }) + page.getByRole('img', { name: "Tomas Illuminati Balbin's avatar", exact: true }) ).toBeVisible() - await expect(page.getByText('Contributor 1', { exact: true })).toBeVisible() - await expect( - page.getByRole('img', { name: "Contributor 2's avatar", exact: true }) - ).toBeVisible() - await expect(page.getByText('Contributor 2', { exact: true })).toBeVisible() - }) - - test('toggle top contributors', async ({ page }) => { - await expect(page.getByRole('button', { name: 'Show more' })).toBeVisible() - await page.getByRole('button', { name: 'Show more' }).click() - await expect(page.getByRole('button', { name: 'Show less' })).toBeVisible() - await page.getByRole('button', { name: 'Show less' }).click() - await expect(page.getByRole('button', { name: 'Show more' })).toBeVisible() + await expect(page.getByText('Tomas Illuminati Balbin', { exact: true })).toBeVisible() }) - test('should have leaders block', async ({ page }) => { - await expect(page.getByRole('heading', { name: 'Leaders' })).toBeVisible() - await expect(page.getByText('Bob')).toBeVisible() - await expect(page.getByText('Chapter Leader')).toBeVisible() + test('should have buttons for sponsoring', async ({ page }) => { + await expect(page.getByRole('link', { name: 'Sponsor OWASP Rosario' })).toBeVisible() }) }) diff --git a/frontend/__tests__/e2e/pages/Chapters.spec.ts b/frontend/__tests__/e2e/pages/Chapters.spec.ts index 5ffda82696..d4a7e40f86 100644 --- a/frontend/__tests__/e2e/pages/Chapters.spec.ts +++ b/frontend/__tests__/e2e/pages/Chapters.spec.ts @@ -13,15 +13,7 @@ test.describe('Chapters Page', () => { }), }) }) - await page.context().addCookies([ - { - name: 'csrftoken', - value: 'abc123', - domain: 'localhost', - path: '/', - }, - ]) - await page.goto('/chapters') + await page.goto('/chapters', { timeout: 25000 }) }) test('renders chapter data correctly', async ({ page }) => { @@ -43,14 +35,14 @@ test.describe('Chapters Page', () => { }) test('handles page change correctly', async ({ page }) => { - const nextPageButton = await page.getByRole('button', { name: '2' }) - await nextPageButton.waitFor({ state: 'visible' }) // Ensure button is visible + const nextPageButton = page.getByRole('button', { name: '2' }) + await nextPageButton.waitFor({ state: 'visible' }) await nextPageButton.click() await expect(page).toHaveURL(/page=2/) }) test('opens window on View Details button click', async ({ page }) => { - const contributeButton = await page.getByRole('button', { name: 'View Details' }) + const contributeButton = page.getByRole('button', { name: 'View Details' }) await contributeButton.waitFor({ state: 'visible' }) await contributeButton.click() await expect(page).toHaveURL('chapters/chapter_1') diff --git a/frontend/__tests__/e2e/pages/CommitteeDetails.spec.ts b/frontend/__tests__/e2e/pages/CommitteeDetails.spec.ts index a60cead2b9..2d115cb4d3 100644 --- a/frontend/__tests__/e2e/pages/CommitteeDetails.spec.ts +++ b/frontend/__tests__/e2e/pages/CommitteeDetails.spec.ts @@ -1,42 +1,37 @@ -import { mockCommitteeDetailsData } from '@mockData/mockCommitteeDetailsData' import { test, expect } from '@playwright/test' test.describe('Committee Details Page', () => { test.beforeEach(async ({ page }) => { - await page.route('**/graphql/', async (route) => { - await route.fulfill({ - status: 200, - json: { data: mockCommitteeDetailsData }, - }) - }) - await page.context().addCookies([ - { - name: 'csrftoken', - value: 'abc123', - domain: 'localhost', - path: '/', - }, - ]) - await page.goto('/committees/test-committee') + await page.goto('/committees/events', { timeout: 25000 }) }) test('should have a heading and summary', async ({ page }) => { - await expect(page.getByRole('heading', { name: 'Test Committee' })).toBeVisible() - await expect(page.getByText('This is a test committee')).toBeVisible() + await expect( + page.getByRole('heading', { name: 'OWASP Events Committee', exact: true }) + ).toBeVisible() + await expect( + page.getByText( + 'The OWASP Events Committee aims to support and enhance global, regional, and local events' + ) + ).toBeVisible() }) test('should have committee details block', async ({ page }) => { await expect(page.getByRole('heading', { name: 'Committee Details' })).toBeVisible() - await expect(page.getByText('Last Updated: Dec 13, 2024')).toBeVisible() - await expect(page.getByText('Leaders: Leader 1, Leader 2')).toBeVisible() - await expect(page.getByRole('link', { name: 'https://owasp.org/test-committee' })).toBeVisible() + const leadersText = page.getByText(/Leaders:.*Izar Tarandach, Allison Shubert, Maria Mora/i) + await expect(leadersText).toBeVisible() + await expect( + page.getByRole('link', { name: 'https://owasp.org/www-committee-events' }) + ).toBeVisible() }) test('should have top contributors', async ({ page }) => { await expect(page.getByRole('heading', { name: 'Top Contributors' })).toBeVisible() - await expect(page.getByRole('img', { name: 'Contributor 1' })).toBeVisible() - await expect(page.getByText('Contributor 1')).toBeVisible() - await expect(page.getByRole('img', { name: 'Contributor 2' })).toBeVisible() - await expect(page.getByText('Contributor 2')).toBeVisible() + await expect(page.getByRole('img', { name: "Josh Grossman's avatar" })).toBeVisible() + await expect(page.getByText('Josh Grossman')).toBeVisible() + await expect(page.getByRole('img', { name: "Harold Blankenship's avatar" })).toBeVisible() + await expect(page.getByText('Harold Blankenship', { exact: true }).first()).toBeVisible() + await expect(page.getByRole('img', { name: "Andrew van der Stock's avatar" })).toBeVisible() + await expect(page.getByText('Andrew van der Stock')).toBeVisible() }) }) diff --git a/frontend/__tests__/e2e/pages/Committees.spec.ts b/frontend/__tests__/e2e/pages/Committees.spec.ts index f96de9fa4c..fb0ac12631 100644 --- a/frontend/__tests__/e2e/pages/Committees.spec.ts +++ b/frontend/__tests__/e2e/pages/Committees.spec.ts @@ -13,15 +13,7 @@ test.describe('Committees Page', () => { }), }) }) - await page.context().addCookies([ - { - name: 'csrftoken', - value: 'abc123', - domain: 'localhost', - path: '/', - }, - ]) - await page.goto('/committees') + await page.goto('/committees', { timeout: 25000 }) }) test('renders committee data correctly', async ({ page }) => { @@ -43,7 +35,7 @@ test.describe('Committees Page', () => { }) test('handles page change correctly', async ({ page }) => { - const nextPageButton = await page.getByRole('button', { name: '2' }) + const nextPageButton = page.getByRole('button', { name: '2' }) await nextPageButton.waitFor({ state: 'visible' }) await nextPageButton.click() await expect(page).toHaveURL(/page=2/) diff --git a/frontend/__tests__/e2e/pages/Contribute.spec.ts b/frontend/__tests__/e2e/pages/Contribute.spec.ts index 75e23ddf04..f9e3a5b7ed 100644 --- a/frontend/__tests__/e2e/pages/Contribute.spec.ts +++ b/frontend/__tests__/e2e/pages/Contribute.spec.ts @@ -13,15 +13,7 @@ test.describe('Contribute Page', () => { }), }) }) - await page.context().addCookies([ - { - name: 'csrftoken', - value: 'abc123', - domain: 'localhost', - path: '/', - }, - ]) - await page.goto('/contribute') + await page.goto('/contribute', { timeout: 25000 }) }) test('renders issue data correctly', async ({ page }) => { @@ -64,14 +56,13 @@ test.describe('Contribute Page', () => { }) test('closes dialog on close button click', async ({ page }) => { - const contributeButton = await page.getByRole('button', { name: 'Read More' }) - await expect(contributeButton).toBeVisible() + const contributeButton = await page.getByRole('button', { name: 'Read More' }).first() await contributeButton.click() const closeButton = await page.getByRole('button', { name: 'close-modal' }) await expect(closeButton).toBeVisible() await closeButton.click() - await expect(contributeButton).toBeVisible() }) + test('breadcrumb renders correct segments on /contribute', async ({ page }) => { await expectBreadCrumbsToBeVisible(page, ['Home', 'Contribute']) }) diff --git a/frontend/__tests__/e2e/pages/Home.spec.ts b/frontend/__tests__/e2e/pages/Home.spec.ts index fbde9be99a..e7afe65cab 100644 --- a/frontend/__tests__/e2e/pages/Home.spec.ts +++ b/frontend/__tests__/e2e/pages/Home.spec.ts @@ -1,27 +1,12 @@ -import { mockHomeData } from '@e2e/data/mockHomeData' import { test, expect } from '@playwright/test' test.describe('Home Page', () => { test.beforeEach(async ({ page }) => { - await page.route('**/graphql/', async (route) => { - await route.fulfill({ - status: 200, - json: mockHomeData, - }) - }) - await page.context().addCookies([ - { - name: 'csrftoken', - value: 'abc123', - domain: 'localhost', - path: '/', - }, - ]) - await page.goto('/') + await page.goto('/', { timeout: 25000 }) }) - test('should have a heading and searchBar', async ({ page }) => { - await expect(page.getByRole('heading', { name: 'Welcome to OWASP Nest' })).toBeVisible() + test('should have a heading', async ({ page }) => { + await expect(page.getByRole('heading', { name: 'OWASP Nest', exact: true })).toBeVisible() await expect( page.getByText('Your gateway to OWASP. Discover, engage, and help shape the future!') ).toBeVisible() @@ -33,108 +18,62 @@ test.describe('Home Page', () => { test('should have new chapters', async ({ page }) => { await expect(page.getByRole('heading', { name: 'New Chapters' })).toBeVisible() - await expect(page.getByRole('link', { name: 'chapter 1' })).toBeVisible() - await expect(page.getByText('Leader 1').first()).toBeVisible() - await expect(page.getByText('Mar 18, 2025').first()).toBeVisible() - await page.getByRole('link', { name: 'Chapter 1' }).click() - await expect(page).toHaveURL('chapters/chapter_1') + const firstChapter = page + .locator('div') + .filter({ hasText: 'New Chapters' }) + .locator('a') + .first() + await expect(firstChapter).toBeVisible() }) test('should have new projects', async ({ page }) => { await expect(page.getByRole('heading', { name: 'New Projects' })).toBeVisible() - await expect(page.getByRole('link', { name: 'Project 1', exact: true })).toBeVisible() - await expect(page.getByText('Leader 1,').first()).toBeVisible() - await expect(page.getByText('Mar 5, 2025').first()).toBeVisible() - await page.getByRole('link', { name: 'Project 1' }).click() - await expect(page).toHaveURL('projects/project_1') - }) - - test('should have posts', async ({ page }) => { - await expect(page.getByRole('heading', { name: 'News & Opinions' })).toBeVisible({ - timeout: 10000, - }) - await expect(page.getByRole('link', { name: 'Post 1', exact: true })).toBeVisible() - await expect(page.getByText('Author 1')).toBeVisible() - await expect(page.getByText('Mar 6, 2025').first()).toBeVisible() - await page.getByRole('link', { name: 'Post 1' }).click() + const firstProject = page + .locator('div') + .filter({ hasText: 'New Projects' }) + .locator('a') + .first() + await expect(firstProject).toBeVisible() }) test('should have top contributors', async ({ page }) => { await expect(page.getByRole('heading', { name: 'Top Contributors' })).toBeVisible() - await expect( - page.getByRole('img', { name: "Contributor 1's avatar", exact: true }) - ).toBeVisible() - await expect(page.getByText('Contributor 1', { exact: true })).toBeVisible() - }) - - test('should have recent issues', async ({ page }) => { - await expect(page.getByRole('heading', { name: 'Recent Issues' })).toBeVisible() - await expect(page.getByRole('heading', { name: 'Issue 1' })).toBeVisible() - await expect(page.getByText('Mar 20, 2025').first()).toBeVisible() - await expect(page.getByText('Dependency-Track')).toBeVisible() + const contributor = page.locator('img[alt*="avatar"]').first() + await expect(contributor).toBeVisible() }) - test('should have recent Releases', async ({ page }) => { - await expect(page.getByRole('heading', { name: 'Recent Releases' })).toBeVisible() - await expect(page.getByRole('heading', { name: 'Release 1' })).toBeVisible() - await expect(page.getByText('Mar 19, 2025')).toBeVisible() - await expect(page.getByText('repo-1')).toBeVisible() - }) - - test('should have recent milestones', async ({ page }) => { - await expect(page.getByRole('heading', { name: 'Recent Milestones' })).toBeVisible() - await expect(page.getByRole('heading', { name: 'v2.0.0 Release' })).toBeVisible() - await expect(page.getByText('Mar 1, 2025')).toBeVisible() - await expect(page.getByText('Home Repo One')).toBeVisible() + test('should have recent activity sections', async ({ page }) => { + // These sections often load dynamically; grouping to ensure data exists (using data-anchor-title attribute for AnchorTitle components) + await expect( + page.locator('[data-anchor-title="true"]', { hasText: 'Recent Issues' }) + ).toBeVisible() + await expect( + page.locator('[data-anchor-title="true"]', { hasText: 'Recent Releases' }) + ).toBeVisible() + await expect( + page.locator('[data-anchor-title="true"]', { hasText: 'Recent Milestones' }) + ).toBeVisible() }) test('should be able to join OWASP', async ({ page }) => { await expect(page.getByRole('heading', { name: 'Ready to Make a Difference?' })).toBeVisible() - await expect(page.getByText('Join OWASP and be part of the')).toBeVisible() - await expect(page.getByRole('link', { name: 'Join OWASP', exact: true })).toBeVisible() - const page1Promise = page.waitForEvent('popup') - await page.getByRole('link', { name: 'Join OWASP', exact: true }).click() - const page1 = await page1Promise - expect(page1.url()).toBe('https://owasp.glueup.com/organization/6727/memberships/') - }) - - test('should have upcoming events', async ({ page }) => { - await expect(page.getByRole('heading', { name: 'Upcoming Events' })).toBeVisible({ - timeout: 10000, - }) - await expect(page.getByRole('button', { name: 'Event 1', exact: true })).toBeVisible() - await expect(page.getByText('Apr 5 — 6, 2025')).toBeVisible() - await page.getByRole('button', { name: 'Event 1', exact: true }).click() + const joinLink = page.getByRole('link', { name: 'Join OWASP', exact: true }) + await expect(joinLink).toBeVisible() }) test('should have stats', async ({ page }) => { - const headers = [ - 'Active Projects', - 'Local Chapters', - 'Contributors', - 'Countries', - 'Slack Community', - ] - for (const header of headers) { - await expect(page.getByText(header, { exact: true })).toBeVisible() + const statLabels = ['Active Projects', 'Local Chapters', 'Contributors', 'Countries'] + for (const label of statLabels) { + await expect(page.getByText(label, { exact: true })).toBeVisible() } }) - test('Bluesky icon should be present and link correctly', async ({ page }) => { - const blueskyIcon = page.locator('footer a[aria-label="OWASP Nest Bluesky"]') - await expect(blueskyIcon).toBeVisible() - await expect(blueskyIcon).toHaveAttribute('target', '_blank') - }) - - test('GitHub icon should be present and link correctly', async ({ page }) => { - const githubIcon = page.locator('footer a[aria-label="OWASP Nest GitHub"]') - await expect(githubIcon).toBeVisible() - await expect(githubIcon).toHaveAttribute('target', '_blank') - }) - - test('Slack icon should be present and link correctly', async ({ page }) => { - const slackIcon = page.locator('footer a[aria-label="OWASP Nest Slack"]') - await expect(slackIcon).toBeVisible() - await expect(slackIcon).toHaveAttribute('target', '_blank') + test('footer social icons should be present', async ({ page }) => { + const platforms = ['Bluesky', 'GitHub', 'Slack'] + for (const platform of platforms) { + const icon = page.locator(`footer a[aria-label*="${platform}"]`) + await expect(icon).toBeVisible() + await expect(icon).toHaveAttribute('target', '_blank') + } }) }) diff --git a/frontend/__tests__/e2e/pages/Login.spec.ts b/frontend/__tests__/e2e/pages/Login.spec.ts index 2e5b1f3744..a3533ccce6 100644 --- a/frontend/__tests__/e2e/pages/Login.spec.ts +++ b/frontend/__tests__/e2e/pages/Login.spec.ts @@ -1,27 +1,6 @@ -import { mockHomeData } from '@e2e/data/mockHomeData' import { test, expect } from '@playwright/test' test.describe('Login Page', () => { - test.beforeEach(async ({ page }) => { - await page.route('**/graphql/', async (route) => { - await route.fulfill({ - status: 200, - body: JSON.stringify(mockHomeData), - }) - }) - await page.context().addCookies([ - { - name: 'csrftoken', - value: 'abc123', - domain: 'localhost', - path: '/', - httpOnly: false, - secure: false, - sameSite: 'Lax', - }, - ]) - }) - test('shows message authentication is enabled', async ({ page }) => { await page.goto('/auth/login') diff --git a/frontend/__tests__/e2e/pages/OrganizationDetails.spec.ts b/frontend/__tests__/e2e/pages/OrganizationDetails.spec.ts index 604f9213e4..b8dad8255d 100644 --- a/frontend/__tests__/e2e/pages/OrganizationDetails.spec.ts +++ b/frontend/__tests__/e2e/pages/OrganizationDetails.spec.ts @@ -1,73 +1,85 @@ -import { mockOrganizationDetailsData } from '@mockData/mockOrganizationData' import { test, expect } from '@playwright/test' test.describe('Organization Details Page', () => { test.beforeEach(async ({ page }) => { - await page.route('**/graphql/', async (route) => { - await route.fulfill({ - status: 200, - json: { data: mockOrganizationDetailsData }, - }) - }) - await page.context().addCookies([ - { - name: 'csrftoken', - value: 'abc123', - domain: 'localhost', - path: '/', - }, - ]) - await page.goto('/organizations/test-org') + await page.goto('/organizations/OWASP', { timeout: 25000 }) }) - test('should have a heading and summary', async ({ page }) => { - await expect(page.getByRole('heading', { name: 'Test Organization' })).toBeVisible() + test('should have a heading', async ({ page }) => { + await expect(page.getByRole('heading', { name: 'OWASP', exact: true })).toBeVisible() }) test('should display organization details', async ({ page }) => { - await expect(page.getByText('@test-org')).toBeVisible() - await expect(page.getByText('San Francisco, CA')).toBeVisible() - await expect(page.getByText('1000')).toBeVisible() + await expect(page.getByText('@OWASP')).toBeVisible() + await expect(page.getByText('United States of America')).toBeVisible() }) test('should display organization statistics', async ({ page }) => { - await expect(page.getByText('5K Stars')).toBeVisible() - await expect(page.getByText('1.2K Forks')).toBeVisible() - await expect(page.getByText('150 Contributors')).toBeVisible() - await expect(page.getByText('300 Issues')).toBeVisible() - await expect(page.getByText('25 Repositories')).toBeVisible() + const stats = ['Stars', 'Forks', 'Contributors', 'Issues', 'Repositories'] + for (const stat of stats) { + await expect(page.getByText(stat).first()).toBeVisible() + // Verify that there is a number associated with the stat (e.g., "15.4K Stars") + const text = String.raw`\d.*${stat}` + await expect( + page + .locator('div') + .filter({ hasText: new RegExp(text) }) + .first() + ).toBeVisible() + } }) test('should display recent issues section', async ({ page }) => { - await expect(page.getByRole('heading', { name: 'Recent Issues' })).toBeVisible() - await expect(page.getByText('Test Issue 1')).toBeVisible() - await expect(page.getByText('Test Issue 2')).toBeVisible() + await expect( + page.locator('[data-anchor-title="true"]', { hasText: 'Recent Issues' }) + ).toBeVisible() + const firstIssue = page.locator('div').filter({ hasText: 'Recent Issues' }).locator('a').first() + await expect(firstIssue).toBeVisible() }) test('should have organization recent milestones', async ({ page }) => { - await expect(page.getByRole('heading', { name: 'Recent Milestones' })).toBeVisible({ + await expect( + page.locator('[data-anchor-title="true"]', { hasText: 'Recent Milestones' }) + ).toBeVisible({ timeout: 10000, }) - await expect(page.getByRole('heading', { name: 'v2.0.0 Release' })).toBeVisible() - await expect(page.getByText('Mar 1, 2025')).toBeVisible() - await expect(page.getByText('Project Repo 1')).toBeVisible() + const milestone = page + .locator('div') + .filter({ hasText: 'Recent Milestones' }) + .locator('h3') + .first() + await expect(milestone).toBeVisible() }) test('should display recent releases section', async ({ page }) => { - await expect(page.getByRole('heading', { name: 'Recent Releases' })).toBeVisible() - await expect(page.getByRole('link', { name: 'Release v2.0.0' }).first()).toBeVisible() + await expect( + page.locator('[data-anchor-title="true"]', { hasText: 'Recent Releases' }) + ).toBeVisible() + const releaseLink = page + .locator('div') + .filter({ hasText: 'Recent Releases' }) + .locator('a') + .first() + await expect(releaseLink).toBeVisible() }) test('should display top contributors section', async ({ page }) => { - await expect(page.getByRole('heading', { name: 'Top Contributors' })).toBeVisible() - await expect(page.getByText('User One')).toBeVisible() - await expect(page.getByText('User Two')).toBeVisible() - await expect(page.getByText('User Three')).toBeVisible() + await expect( + page.locator('[data-anchor-title="true"]', { hasText: 'Top Contributors' }) + ).toBeVisible() + const contributorAvatar = page.locator('img[alt*="avatar"]').first() + await expect(contributorAvatar).toBeVisible() }) test('should display recent pull requests section', async ({ page }) => { - await expect(page.getByRole('heading', { name: 'Recent Pull Requests' })).toBeVisible() - await expect(page.getByText('Test Pull Request 1')).toBeVisible() - await expect(page.getByText('Test Pull Request 2')).toBeVisible() + await expect( + page.locator('[data-anchor-title="true"]', { hasText: 'Recent Pull Requests' }) + ).toBeVisible() + const firstPR = page + .locator('div') + .filter({ hasText: 'Recent Pull Requests' }) + .locator('a') + .first() + await expect(firstPR).toBeVisible() }) }) diff --git a/frontend/__tests__/e2e/pages/Organizations.spec.ts b/frontend/__tests__/e2e/pages/Organizations.spec.ts index 5708c34ad4..5ef469da74 100644 --- a/frontend/__tests__/e2e/pages/Organizations.spec.ts +++ b/frontend/__tests__/e2e/pages/Organizations.spec.ts @@ -13,15 +13,7 @@ test.describe('Organization Page', () => { }), }) }) - await page.context().addCookies([ - { - name: 'csrftoken', - value: 'abc123', - domain: 'localhost', - path: '/', - }, - ]) - await page.goto('/organizations') + await page.goto('/organizations', { timeout: 25000 }) }) test('renders organization data correctly', async ({ page }) => { @@ -32,11 +24,6 @@ test.describe('Organization Page', () => { await expect(viewDetailsButtons).toHaveCount(2) }) - test('navigation to organization details works', async ({ page }) => { - await page.getByRole('button', { name: 'View Profile' }).first().click() - expect(await page.url()).toContain('organizations') - }) - test('displays followers and repositories counts correctly', async ({ page }) => { await expect(page.getByText('1k')).toBeVisible() await expect(page.getByText('1.5k')).toBeVisible() @@ -45,4 +32,9 @@ test.describe('Organization Page', () => { test('breadcrumb renders correct segments on /organizations', async ({ page }) => { await expectBreadCrumbsToBeVisible(page, ['Home', 'Organizations']) }) + + test('navigation to organization details works', async ({ page }) => { + await page.getByRole('button', { name: 'View Profile' }).first().click() + await expect(page).toHaveURL(/\/organizations\/[^/]+/) + }) }) diff --git a/frontend/__tests__/e2e/pages/ProjectDetails.spec.ts b/frontend/__tests__/e2e/pages/ProjectDetails.spec.ts index d6d9b5baa2..2de16fa665 100644 --- a/frontend/__tests__/e2e/pages/ProjectDetails.spec.ts +++ b/frontend/__tests__/e2e/pages/ProjectDetails.spec.ts @@ -1,125 +1,90 @@ -import { mockProjectDetailsData } from '@mockData/mockProjectDetailsData' import { test, expect } from '@playwright/test' test.describe('Project Details Page', () => { test.beforeEach(async ({ page }) => { - await page.route('**/graphql/', async (route) => { - await route.fulfill({ - status: 200, - json: { - data: mockProjectDetailsData, - }, - }) - }) - await page.context().addCookies([ - { - name: 'csrftoken', - value: 'abc123', - domain: 'localhost', - path: '/', - }, - ]) - await page.goto('/projects/test-project', { timeout: 60000 }) + await page.goto('/projects/nest', { timeout: 25000 }) }) - test('should have a heading and summary', async ({ page }) => { - await expect(page.getByRole('heading', { name: 'Test Project' })).toBeVisible() + test('should have a heading', async ({ page }) => { + await expect(page.getByRole('heading', { name: 'OWASP Nest', exact: true })).toBeVisible() await expect( - page.getByText('An example project showcasing GraphQL and Django integration.') + page.getByText( + 'OWASP Nest is a code project aimed at improving how OWASP manages its collection of projects. It will create a more organized system with better navigation and user experience while keeping the current risk profile intact. The project plans to use the Github API to keep track of project updates and automate certain tasks. Project leaders will be able to add important information using standardized files. The goal is to have a minimum viable product ready by September 2024, with hopes of completing it by the end of the year.' + ) ).toBeVisible() }) test('should have project details block', async ({ page }) => { await expect(page.getByRole('heading', { name: 'Project Details' })).toBeVisible() - await expect(page.getByText('Last Updated: Feb 7, 2025')).toBeVisible() await expect(page.getByText('Level: Lab')).toBeVisible() - await expect(page.getByText('Leaders: alice, bob')).toBeVisible() - await expect(page.getByText('URL: https://github.com/')).toBeVisible() + await expect(page.getByText('Type: Code')).toBeVisible() + await expect( + page.getByText('Leaders: Arkadii Yakovets, Kate Golovanova, Starr Brown') + ).toBeVisible() + await expect(page.getByText('URL: https://owasp.org/www-project-nest')).toBeVisible() }) - test('should have project statics block', async ({ page }) => { - await expect(page.getByText('2.2K Stars')).toBeVisible() - await expect(page.getByText('10 Forks')).toBeVisible() - await expect(page.getByText('1.2K Contributors')).toBeVisible() - await expect(page.getByText('10 Issues')).toBeVisible() - await expect(page.getByText('3 Repositories')).toBeVisible() + test('should have project statistics block', async ({ page }) => { + await expect(page.getByText('Stars').first()).toBeVisible() + await expect(page.getByText('Forks').first()).toBeVisible() + await expect(page.getByText('Contributors').first()).toBeVisible() + await expect(page.getByText('Issues').first()).toBeVisible() + await expect(page.getByText('Repositories').first()).toBeVisible() }) test('should have project topics', async ({ page }) => { await expect(page.getByRole('heading', { name: 'Topics' })).toBeVisible() await expect(page.getByText('graphql', { exact: true })).toBeVisible() await expect(page.getByText('django', { exact: true })).toBeVisible() - await expect(page.getByText('backend')).toBeVisible() + await expect(page.getByText('python', { exact: true })).toBeVisible() + await expect(page.getByText('nextjs', { exact: true })).toBeVisible() }) test('should have project languages', async ({ page }) => { await expect(page.getByRole('heading', { name: 'Languages' })).toBeVisible() await expect(page.getByText('Python', { exact: true })).toBeVisible() - await expect(page.getByText('GraphQL', { exact: true })).toBeVisible() - await expect(page.getByText('JavaScript', { exact: true })).toBeVisible() + await expect(page.getByText('TypeScript', { exact: true })).toBeVisible() }) test('should have top contributors', async ({ page }) => { await expect(page.getByRole('heading', { name: 'Top Contributors' })).toBeVisible() - await expect( - page.getByRole('img', { name: "Contributor 1's avatar", exact: true }) - ).toBeVisible() - await expect(page.getByText('Contributor 1', { exact: true })).toBeVisible() - await expect( - page.getByRole('img', { name: "Contributor 2's avatar", exact: true }) - ).toBeVisible() - await expect(page.getByText('Contributor 2', { exact: true })).toBeVisible() + await expect(page.getByText('Arkadii Yakovets', { exact: true }).first()).toBeVisible() + await expect(page.getByText('Kate Golovanova', { exact: true }).first()).toBeVisible() + await expect(page.getByText('Ahmed Gouda', { exact: true })).toBeVisible() }) test('toggle top contributors', async ({ page }) => { - await expect(page.getByRole('button', { name: 'Show more' })).toBeVisible() - await page.getByRole('button', { name: 'Show more' }).click() + await expect(page.getByRole('button', { name: 'Show more' }).last()).toBeVisible() + await page.getByRole('button', { name: 'Show more' }).last().click() await expect(page.getByRole('button', { name: 'Show less' })).toBeVisible() await page.getByRole('button', { name: 'Show less' }).click() - await expect(page.getByRole('button', { name: 'Show more' })).toBeVisible() + await expect(page.getByRole('button', { name: 'Show more' }).last()).toBeVisible() }) test('should have project recent issues', async ({ page }) => { - await expect(page.getByRole('heading', { name: 'Recent Issues' })).toBeVisible() - await expect(page.getByRole('heading', { name: 'Fix authentication bug' })).toBeVisible() - await expect(page.getByText('Feb 5, 2025')).toBeVisible() - await expect(page.getByText('test-repo')).toBeVisible() + await expect( + page.locator('[data-anchor-title="true"]', { hasText: 'Recent Issues' }) + ).toBeVisible() + await expect(page.getByText('Nest').first()).toBeVisible() }) test('should have project recent releases', async ({ page }) => { - await expect(page.getByRole('heading', { name: 'Recent Releases' })).toBeVisible() - await expect(page.getByRole('heading', { name: 'V1.2.0' })).toBeVisible() - await expect(page.getByText('Jan 20, 2025')).toBeVisible() + await expect( + page.locator('[data-anchor-title="true"]', { hasText: 'Recent Releases' }) + ).toBeVisible() }) test('should have project recent milestones', async ({ page }) => { - await expect(page.getByRole('heading', { name: 'Recent Milestones' })).toBeVisible() - await expect(page.getByRole('heading', { name: 'v2.0.0 Release' })).toBeVisible() - await expect(page.getByText('Mar 1, 2025')).toBeVisible() - await expect(page.getByText('Project Repo 1')).toBeVisible() + await expect( + page.locator('[data-anchor-title="true"]', { hasText: 'Recent Milestones' }) + ).toBeVisible() }) test('should display recent pull requests section', async ({ page }) => { - await expect(page.getByRole('heading', { name: 'Recent Pull Requests' })).toBeVisible() - await expect(page.getByText('Test Pull Request 1')).toBeVisible() - await expect(page.getByText('Test Pull Request 2')).toBeVisible() - }) - - test('should have project repositories', async ({ page }) => { - await expect(page.getByRole('heading', { name: 'Repositories' })).toBeVisible() - await expect(page.getByText('Repo One')).toBeVisible() - await expect(page.getByText('Stars95')).toBeVisible() - await expect(page.getByText('Forks12')).toBeVisible() - await expect(page.getByText('Contributors40')).toBeVisible() - await expect(page.getByText('Issues6')).toBeVisible() - await expect(page.getByText('Repo Two')).toBeVisible() - await expect(page.getByText('Stars60')).toBeVisible() - await expect(page.getByText('Forks8')).toBeVisible() - await expect(page.getByText('Contributors30')).toBeVisible() - await expect(page.getByText('Issues3', { exact: true })).toBeVisible() - - await page.getByText('Repo One').click() - await expect(page).toHaveURL('organizations/OWASP/repositories/repo-1') + await expect( + page.locator('[data-anchor-title="true"]', { hasText: 'Recent Pull Requests' }) + ).toBeVisible() + await expect(page.getByText('Nest').first()).toBeVisible() }) test('should display health metrics section', async ({ page }) => { @@ -130,10 +95,10 @@ test.describe('Project Details Page', () => { await expect(page.getByText('Days Since Last Commit and Release')).toBeVisible() }) - test('should have leaders block', async ({ page }) => { - await expect(page.getByRole('heading', { name: 'Leaders' })).toBeVisible() - const userCard = page.getByRole('button', { name: /Alice/ }) - await expect(userCard.locator('h3')).toHaveText('Alice') - await expect(userCard.getByText('Project Leader')).toBeVisible() + test('should have project repositories', async ({ page }) => { + await expect(page.getByRole('heading', { name: 'Repositories' })).toBeVisible() + await expect(page.getByText('www-project-nest', { exact: true })).toBeVisible() + await page.getByText('www-project-nest', { exact: true }).click() + await expect(page).toHaveURL('organizations/OWASP/repositories/www-project-nest') }) }) diff --git a/frontend/__tests__/e2e/pages/ProjectHealthDashboardMetricsDetails.spec.ts b/frontend/__tests__/e2e/pages/ProjectHealthDashboardMetricsDetails.spec.ts index f246c835d4..14cf68bcbc 100644 --- a/frontend/__tests__/e2e/pages/ProjectHealthDashboardMetricsDetails.spec.ts +++ b/frontend/__tests__/e2e/pages/ProjectHealthDashboardMetricsDetails.spec.ts @@ -9,9 +9,10 @@ test.describe('Project Health Metrics Details Page', () => { await expect(page.getByText('404')).toBeVisible() await expect(page.getByText("Sorry, the page you're looking for doesn't exist.")).toBeVisible() }) + test('renders project health metrics details', async ({ page }) => { await mockDashboardCookies(page, mockProjectsDashboardMetricsDetailsData, true) - await page.goto('/projects/dashboard/metrics/test-project') + await page.goto('/projects/dashboard/metrics/nest') const metricsLatest = mockProjectsDashboardMetricsDetailsData.project.healthMetricsLatest const headers = [ 'Days Metrics', diff --git a/frontend/__tests__/e2e/pages/Projects.spec.ts b/frontend/__tests__/e2e/pages/Projects.spec.ts index 12ba933835..5bf074b3d7 100644 --- a/frontend/__tests__/e2e/pages/Projects.spec.ts +++ b/frontend/__tests__/e2e/pages/Projects.spec.ts @@ -13,14 +13,6 @@ test.describe('Projects Page', () => { }), }) }) - await page.context().addCookies([ - { - name: 'csrftoken', - value: 'abc123', - domain: 'localhost', - path: '/', - }, - ]) await page.goto('/projects') }) diff --git a/frontend/__tests__/e2e/pages/ProjectsHealthDashboardMetrics.spec.ts b/frontend/__tests__/e2e/pages/ProjectsHealthDashboardMetrics.spec.ts index 7003db0639..0a978211fe 100644 --- a/frontend/__tests__/e2e/pages/ProjectsHealthDashboardMetrics.spec.ts +++ b/frontend/__tests__/e2e/pages/ProjectsHealthDashboardMetrics.spec.ts @@ -1,6 +1,7 @@ import { mockDashboardCookies } from '@e2e/helpers/mockDashboardCookies' import { mockHealthMetricsData } from '@mockData/mockProjectsHealthMetricsData' import { test, expect } from '@playwright/test' + test.describe('Projects Health Dashboard Metrics', () => { test('renders 404 when user is not OWASP staff', async ({ page }) => { await mockDashboardCookies(page, mockHealthMetricsData, false) diff --git a/frontend/__tests__/e2e/pages/RepositoryDetails.spec.ts b/frontend/__tests__/e2e/pages/RepositoryDetails.spec.ts index 5ef12097af..c4decea65a 100644 --- a/frontend/__tests__/e2e/pages/RepositoryDetails.spec.ts +++ b/frontend/__tests__/e2e/pages/RepositoryDetails.spec.ts @@ -1,101 +1,77 @@ -import { mockRepositoryData } from '@mockData/mockRepositoryData' import { test, expect } from '@playwright/test' test.describe('Repository Details Page', () => { test.beforeEach(async ({ page }) => { - await page.route('**/graphql/', async (route) => { - await route.fulfill({ - status: 200, - json: { data: mockRepositoryData }, - }) - }) - await page.context().addCookies([ - { - name: 'csrftoken', - value: 'abc123', - domain: 'localhost', - path: '/', - }, - ]) - await page.goto('organizations/OWASP/repositories/test-repository') + await page.goto('/organizations/OWASP/repositories/Nest') }) test('should have a heading and summary', async ({ page }) => { - await expect(page.getByRole('heading', { name: 'Test Repo' })).toBeVisible() - await expect(page.getByText('A sample test repository')).toBeVisible() + await expect(page.getByRole('heading', { name: 'Nest', exact: true })).toBeVisible() + await expect(page.getByText(/Your gateway to OWASP/i)).toBeVisible() }) test('should have repository details block', async ({ page }) => { await expect(page.getByRole('heading', { name: 'Repository Details' })).toBeVisible() - await expect(page.getByText('Last Updated: Jan 1, 2024')).toBeVisible() await expect(page.getByText('License: MIT')).toBeVisible() - await expect(page.getByText('Size: 1200 KB')).toBeVisible() - await expect(page.getByRole('link', { name: 'https://github.com/test-repo' })).toBeVisible() + await expect(page.getByRole('link', { name: 'https://github.com/OWASP/Nest' })).toBeVisible() + await expect(page.getByText(/Last Updated:/i)).toBeVisible() }) - test('should have statics block', async ({ page }) => { - await expect(page.getByText('50K Stars')).toBeVisible() - await expect(page.getByText('3K Forks')).toBeVisible() - await expect(page.getByText('5 Contributors')).toBeVisible() - await expect(page.getByText('2 Issues')).toBeVisible() - await expect(page.getByText('10 Commits')).toBeVisible() + test('should have statistics block', async ({ page }) => { + const stats = ['Stars', 'Forks', 'Contributors', 'Issues', 'Commits'] + for (const stat of stats) { + // Look for the stat label and ensure a number is nearby + const text = String.raw`\d.*${stat}` + await expect( + page + .locator('div') + .filter({ hasText: new RegExp(text) }) + .first() + ).toBeVisible() + } }) test('should have topics', async ({ page }) => { await expect(page.getByRole('heading', { name: 'Topics' })).toBeVisible() - await expect(page.getByText('JavaScript', { exact: true })).toBeVisible() - await expect(page.getByText('TypeScript', { exact: true })).toBeVisible() + const topics = ['python', 'nextjs', 'typescript'] + for (const topic of topics) { + await expect(page.getByText(topic).first()).toBeVisible() + } }) test('should have languages', async ({ page }) => { await expect(page.getByRole('heading', { name: 'Languages' })).toBeVisible() - await expect(page.getByText('web', { exact: true })).toBeVisible() - await expect(page.getByText('security', { exact: true })).toBeVisible() + await expect(page.getByText('TypeScript', { exact: true }).first()).toBeVisible() + await expect(page.getByText('Python', { exact: true }).first()).toBeVisible() }) test('should have top contributors', async ({ page }) => { await expect(page.getByRole('heading', { name: 'Top Contributors' })).toBeVisible() + // Check for presence of contributor avatars/names + const contributor = page.locator('img[alt*="avatar"]').first() + await expect(contributor).toBeVisible() + }) + + test('should have recent activity sections', async ({ page }) => { + // Grouping activity checks as they are dynamic lists (using data-anchor-title attribute for AnchorTitle components) + await expect( + page.locator('[data-anchor-title="true"]', { hasText: 'Recent Issues' }) + ).toBeVisible() await expect( - page.getByRole('img', { name: "Contributor 1's avatar", exact: true }) + page.locator('[data-anchor-title="true"]', { hasText: 'Recent Releases' }) ).toBeVisible() - await expect(page.getByText('Contributor 1', { exact: true })).toBeVisible() await expect( - page.getByRole('img', { name: "Contributor 2's avatar", exact: true }) + page.locator('[data-anchor-title="true"]', { hasText: 'Recent Pull Requests' }) + ).toBeVisible() + await expect( + page.locator('[data-anchor-title="true"]', { hasText: 'Recent Milestones' }) ).toBeVisible() - await expect(page.getByText('Contributor 2', { exact: true })).toBeVisible() }) - test('toggle top contributors', async ({ page }) => { - await expect(page.getByRole('button', { name: 'Show more' })).toBeVisible() - await page.getByRole('button', { name: 'Show more' }).click() - await expect(page.getByRole('button', { name: 'Show less' })).toBeVisible() - await page.getByRole('button', { name: 'Show less' }).click() - await expect(page.getByRole('button', { name: 'Show more' })).toBeVisible() - }) - - test('should have recent issues', async ({ page }) => { - await expect(page.getByRole('heading', { name: 'Recent Issues' })).toBeVisible() - await expect(page.getByRole('heading', { name: 'Bug fix required' })).toBeVisible() - await expect(page.getByText('Jan 2, 2024')).toBeVisible() - await expect(page.getByText('test-repo-2')).toBeVisible() - }) - - test('should have recent releases', async ({ page }) => { - await expect(page.getByRole('heading', { name: 'Recent Releases' })).toBeVisible() - await expect(page.getByRole('heading', { name: 'v1.0.0' })).toBeVisible() - await expect(page.getByText('Jan 1, 2024', { exact: true })).toBeVisible() - }) - - test('should have recent milestones', async ({ page }) => { - await expect(page.getByRole('heading', { name: 'Recent Milestones' })).toBeVisible() - await expect(page.getByRole('heading', { name: 'v2.0.0 Release' })).toBeVisible() - await expect(page.getByText('Mar 1, 2025')).toBeVisible() - await expect(page.getByText('Repo One')).toBeVisible() - }) - - test('should display recent pull requests section', async ({ page }) => { - await expect(page.getByRole('heading', { name: 'Recent Pull Requests' })).toBeVisible() - await expect(page.getByText('Test Pull Request 1')).toBeVisible() - await expect(page.getByText('Test Pull Request 2')).toBeVisible() + await expect(page.getByRole('button', { name: 'Show more' }).last()).toBeVisible() + await page.getByRole('button', { name: 'Show more' }).last().click() + await expect(page.getByRole('button', { name: 'Show less' }).last()).toBeVisible() + await page.getByRole('button', { name: 'Show less' }).last().click() + await expect(page.getByRole('button', { name: 'Show more' }).last()).toBeVisible() }) }) diff --git a/frontend/__tests__/e2e/pages/UserDetails.spec.ts b/frontend/__tests__/e2e/pages/UserDetails.spec.ts index e82d073479..3012ed54cd 100644 --- a/frontend/__tests__/e2e/pages/UserDetails.spec.ts +++ b/frontend/__tests__/e2e/pages/UserDetails.spec.ts @@ -1,67 +1,48 @@ -import { mockUserDetailsData } from '@mockData/mockUserDetails' import { test, expect } from '@playwright/test' test.describe('User Details Page', () => { test.beforeEach(async ({ page }) => { - await page.route('**/graphql/', async (route) => { - await route.fulfill({ - status: 200, - json: { data: mockUserDetailsData }, - }) - }) - await page.context().addCookies([ - { - name: 'csrftoken', - value: 'abc123', - domain: 'localhost', - path: '/', - }, - ]) - await page.goto('members/test-user') + await page.goto('/members/arkid15r', { timeout: 25000 }) }) + test('should have a heading and summary', async ({ page }) => { - await expect(page.getByRole('heading', { name: 'Test User' })).toBeVisible() - await expect(page.getByText('Test @User')).toBeVisible() + await expect(page.getByRole('heading', { name: 'Arkadii Yakovets', exact: true })).toBeVisible() + await expect(page.getByText('@arkid15r')).toBeVisible() }) test('should have user details block', async ({ page }) => { await expect(page.getByRole('heading', { name: 'User Details' })).toBeVisible() - await expect(page.getByText('Location: Test Location')).toBeVisible() - await expect(page.getByText('Email: testuser@example.com')).toBeVisible() - await expect(page.getByText('Company: Test Company')).toBeVisible() + await expect(page.getByText(/Location:/i)).toBeVisible() + await expect(page.getByText(/Email:/i)).toBeVisible() }) test('should have user stats block', async ({ page }) => { - await expect(page.getByText('10 Followers')).toBeVisible() - await expect(page.getByText('5 Following')).toBeVisible() - await expect(page.getByText('3 Repositories')).toBeVisible() - }) - - test('should have user issues', async ({ page }) => { - await expect(page.getByRole('heading', { name: 'Issues' })).toBeVisible() - await expect(page.getByText('Test Issue')).toBeVisible() - await expect(page.getByText('test-repo-1')).toBeVisible() - }) - - test('should have user releases', async ({ page }) => { - await expect(page.getByRole('heading', { name: 'Releases' })).toBeVisible() - await expect(page.getByText('v1.0.0')).toBeVisible() - }) - - test('should have user recent milestones', async ({ page }) => { - await expect(page.getByRole('heading', { name: 'Recent Milestones' })).toBeVisible() - await expect(page.getByRole('heading', { name: 'v2.0.0 Release' })).toBeVisible() - await expect(page.getByText('Mar 1, 2025')).toBeVisible() - await expect(page.getByText('Project Repo 1')).toBeVisible() - }) - - test('should have user pull requests', async ({ page }) => { - await expect(page.getByRole('heading', { name: 'Pull Requests' })).toBeVisible() - await expect(page.getByText('Test Pull Request')).toBeVisible() - }) - - test('should have top repositories', async ({ page }) => { - await expect(page.getByRole('heading', { name: 'Repositories' })).toBeVisible() - await expect(page.getByText('test-repo-2')).toBeVisible() + const stats = ['Followers', 'Following', 'Repositories'] + for (const stat of stats) { + const text = String.raw`\d.*${stat}` + await expect( + page + .locator('div') + .filter({ hasText: new RegExp(text) }) + .first() + ).toBeVisible() + } + }) + + test('should have user activity sections', async ({ page }) => { + await expect( + page.locator('[data-anchor-title="true"]', { hasText: 'Recent Issues' }) + ).toBeVisible() + await expect( + page.locator('[data-anchor-title="true"]', { hasText: 'Recent Pull Requests' }) + ).toBeVisible() + await expect( + page.locator('[data-anchor-title="true"]', { hasText: 'Repositories' }) + ).toBeVisible() + await expect( + page.locator('[data-anchor-title="true"]', { hasText: 'Recent Milestones' }) + ).toBeVisible() + const firstRepo = page.locator('div').filter({ hasText: 'Repositories' }).locator('a').first() + await expect(firstRepo).toBeVisible() }) }) diff --git a/frontend/__tests__/e2e/pages/Users.spec.ts b/frontend/__tests__/e2e/pages/Users.spec.ts index ec058f46c5..067231bca8 100644 --- a/frontend/__tests__/e2e/pages/Users.spec.ts +++ b/frontend/__tests__/e2e/pages/Users.spec.ts @@ -13,14 +13,6 @@ test.describe('Users Page', () => { }), }) }) - await page.context().addCookies([ - { - name: 'csrftoken', - value: 'abc123', - domain: 'localhost', - path: '/', - }, - ]) await page.goto('/members') }) @@ -60,6 +52,7 @@ test.describe('Users Page', () => { await expect(page.getByText('1k')).toBeVisible() await expect(page.getByText('2k')).toBeVisible() }) + test('breadcrumb renders correct segments on /members', async ({ page }) => { await expectBreadCrumbsToBeVisible(page, ['Home', 'Members']) }) diff --git a/frontend/__tests__/e2e/pnpm-lock.yaml b/frontend/__tests__/e2e/pnpm-lock.yaml new file mode 100644 index 0000000000..22bea0f497 --- /dev/null +++ b/frontend/__tests__/e2e/pnpm-lock.yaml @@ -0,0 +1,195 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + glob: ^11.1.0 + test-exclude: ^7.0.1 + +importers: + + .: + dependencies: + '@playwright/test': + specifier: ^1.58.1 + version: 1.58.1 + dayjs: + specifier: ^1.11.19 + version: 1.11.19 + millify: + specifier: ^6.1.0 + version: 6.1.0 + +packages: + + '@playwright/test@1.58.1': + resolution: {integrity: sha512-6LdVIUERWxQMmUSSQi0I53GgCBYgM2RpGngCPY7hSeju+VrKjq3lvs7HpJoPbDiY5QM5EYRtRX5fvrinnMAz3w==} + engines: {node: '>=18'} + hasBin: true + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + dayjs@1.11.19: + resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + millify@6.1.0: + resolution: {integrity: sha512-H/E3J6t+DQs/F2YgfDhxUVZz/dF8JXPPKTLHL/yHCcLZLtCXJDUaqvhJXQwqOVBvbyNn4T0WjLpIHd7PAw7fBA==} + hasBin: true + + playwright-core@1.58.1: + resolution: {integrity: sha512-bcWzOaTxcW+VOOGBCQgnaKToLJ65d6AqfLVKEWvexyS3AS6rbXl+xdpYRMGSRBClPvyj44njOWoxjNdL/H9UNg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.58.1: + resolution: {integrity: sha512-+2uTZHxSCcxjvGc5C891LrS1/NlxglGxzrC4seZiVjcYVQfUa87wBL6rTDqzGjuoWNjnBzRqKmF6zRYGMvQUaQ==} + engines: {node: '>=18'} + hasBin: true + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + +snapshots: + + '@playwright/test@1.58.1': + dependencies: + playwright: 1.58.1 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + dayjs@1.11.19: {} + + emoji-regex@8.0.0: {} + + escalade@3.2.0: {} + + fsevents@2.3.2: + optional: true + + get-caller-file@2.0.5: {} + + is-fullwidth-code-point@3.0.0: {} + + millify@6.1.0: + dependencies: + yargs: 17.7.2 + + playwright-core@1.58.1: {} + + playwright@1.58.1: + dependencies: + playwright-core: 1.58.1 + optionalDependencies: + fsevents: 2.3.2 + + require-directory@2.1.1: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + y18n@5.0.8: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 diff --git a/frontend/next.config.ts b/frontend/next.config.ts index 38b8cfffb1..9cf3da04d9 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -54,6 +54,18 @@ const nextConfig: NextConfig = { productionBrowserSourceMaps: true, serverExternalPackages: ['import-in-the-middle', 'require-in-the-middle'], transpilePackages: ['@react-leaflet/core', 'leaflet', 'react-leaflet', 'react-leaflet-cluster'], + rewrites: process.env.NEXT_PUBLIC_E2E_BACKEND_BASE_URL + ? async () => [ + { + source: '/csrf', + destination: `${process.env.NEXT_PUBLIC_E2E_BACKEND_BASE_URL}/csrf/`, + }, + { + source: '/graphql', + destination: `${process.env.NEXT_PUBLIC_E2E_BACKEND_BASE_URL}/graphql/`, + }, + ] + : undefined, ...(isLocal && !forceStandalone ? {} : { output: 'standalone' }), } diff --git a/frontend/package.json b/frontend/package.json index b88497f33a..50510b0102 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,7 +14,6 @@ "lint:check": "eslint . --config eslint.config.mjs --max-warnings=0", "start": "next start", "test:a11y": "NODE_OPTIONS='--experimental-vm-modules --no-warnings=DEP0040' jest __tests__/a11y/ --coverage=false", - "test:e2e": "npx playwright test", "test:unit": "tsc --noEmit && NODE_OPTIONS='--experimental-vm-modules --no-warnings=DEP0040' jest" }, "dependencies": { diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index c7989afbf3..57c1d4d560 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -2,6 +2,9 @@ import os from 'node:os' import { defineConfig, devices } from '@playwright/test' export default defineConfig({ + expect: { + timeout: 30000, + }, fullyParallel: true, projects: [ { @@ -15,9 +18,6 @@ export default defineConfig({ use: { ...devices['iPhone 13'], }, - expect: { - timeout: 10000, // Mobile Safari needs more time to render - }, }, ], reporter: [['list', { printSteps: true }]], @@ -25,14 +25,9 @@ export default defineConfig({ testDir: './__tests__/e2e', timeout: 120_000, use: { - baseURL: 'http://localhost:3000', + baseURL: process.env.FRONTEND_URL || 'http://localhost:3000', headless: true, trace: 'off', }, - webServer: { - command: 'pnpm run build && NEXT_SERVER_DISABLE_SSR=true pnpm run start', - timeout: 120_000, - url: 'http://localhost:3000', - }, workers: os.cpus().length, })