From c3dd6400d9be8be97ee491d8b8c3de36b6c0cb15 Mon Sep 17 00:00:00 2001 From: Atharv Chandratre Date: Mon, 9 Oct 2023 09:00:11 -0500 Subject: [PATCH 1/2] Adding a playwright testing for 16-tiers-page.test.js --- .gitignore | 3 + package-lock.json | 89 ++++++++++++++++++++++++++ package.json | 4 ++ playwright-tests/16-tiers-page.spec.ts | 15 +++++ playwright-tests/playwright-script.py | 27 ++++++++ playwright.config.ts | 77 ++++++++++++++++++++++ 6 files changed, 215 insertions(+) create mode 100644 playwright-tests/16-tiers-page.spec.ts create mode 100644 playwright-tests/playwright-script.py create mode 100644 playwright.config.ts diff --git a/.gitignore b/.gitignore index 37c6caebe65..8db8ff6651b 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,6 @@ public/static/cmaps lib/graphql/types/v2/index.ts lib/graphql/types/v2/gql.ts +/test-results/ +/playwright-report/ +/playwright/.cache/ diff --git a/package-lock.json b/package-lock.json index 4b09d328e30..9212b97bea4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -148,6 +148,8 @@ "@graphql-codegen/client-preset": "^4.2.6", "@graphql-eslint/eslint-plugin": "^3.20.1", "@next/bundle-analyzer": "^14.2.3", + "@playwright/test": "^1.43.1", + "@rollup/plugin-babel": "^6.0.4", "@rollup/plugin-commonjs": "^26.0.0", "@rollup/plugin-image": "^3.0.3", "@rollup/plugin-json": "^6.1.0", @@ -209,6 +211,7 @@ "npm-run-all2": "^6.2.0", "nyc": "^17.0.0", "pdf-parse": "^1.1.1", + "playwright": "^1.43.1", "postcss": "8.4.41", "prettier": "3.3.3", "prettier-plugin-tailwindcss": "0.6.8", @@ -7173,6 +7176,21 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.43.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.43.1.tgz", + "integrity": "sha512-HgtQzFgNEEo4TE22K/X7sYTYNqEMMTZmFS8kTq6m8hXj+m1D8TgwgIbumHddJa9h4yl4GkKb8/bgAl2+g7eDgA==", + "dev": true, + "dependencies": { + "playwright": "1.43.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@pmmmwh/react-refresh-webpack-plugin": { "version": "0.5.11", "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.11.tgz", @@ -9242,6 +9260,33 @@ "integrity": "sha512-l3YHBLAol6d/IKnB9LhpD0cEZWAoe3eFKUyTYWmFmCO2Q/WOckxLQAUyMZWwZV2M/m3+4vgRoaolFqaII82/TA==", "dev": true }, + "node_modules/@rollup/plugin-babel": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-6.0.4.tgz", + "integrity": "sha512-YF7Y52kFdFT/xVSuVdjkV5ZdX/3YtmX0QulG+x0taQOtJdHYzVU61aSSkAgVJ7NOv6qPkIYiJSgSWWN/DM5sGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.18.6", + "@rollup/pluginutils": "^5.0.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "@types/babel__core": "^7.1.9", + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "@types/babel__core": { + "optional": true + }, + "rollup": { + "optional": true + } + } + }, "node_modules/@rollup/plugin-commonjs": { "version": "26.0.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-26.0.1.tgz", @@ -32492,6 +32537,24 @@ "node": ">=10" } }, + "node_modules/playwright": { + "version": "1.43.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.43.1.tgz", + "integrity": "sha512-V7SoH0ai2kNt1Md9E3Gwas5B9m8KR2GVvwZnAI6Pg0m3sh7UvgiYhRrhsziCmqMJNouPckiOhk8T+9bSAK0VIA==", + "dev": true, + "dependencies": { + "playwright-core": "1.43.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, "node_modules/playwright-core": { "version": "1.40.1", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.40.1.tgz", @@ -32505,6 +32568,32 @@ "node": ">=16" } }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright/node_modules/playwright-core": { + "version": "1.43.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.43.1.tgz", + "integrity": "sha512-EI36Mto2Vrx6VF7rm708qSnesVQKbxEWvPrfA1IPY6HgczBplDx7ENtx+K2n4kJ41sLLkuGfmb0ZLSSXlDhqPg==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/please-upgrade-node": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", diff --git a/package.json b/package.json index 3efa0fa31dc..4e67d4e3a80 100644 --- a/package.json +++ b/package.json @@ -161,6 +161,7 @@ "commit": "git-cz", "cypress:open": "cross-env TZ=UTC cypress open --e2e", "cypress:run": "cross-env TZ=UTC cypress run", + "playwright:open": "cross-env TZ=UTC playwright open --e2e", "depcheck": "npx @opencollective/depcheck", "deploy:production": "cross-env ./scripts/pre-deploy.sh production && git push production main", "deploy:staging": "cross-env ./scripts/pre-deploy.sh staging && git push -f staging main", @@ -215,6 +216,8 @@ "@graphql-eslint/eslint-plugin": "^3.20.1", "@next/bundle-analyzer": "^14.2.3", "@rollup/plugin-commonjs": "^26.0.0", + "@playwright/test": "^1.43.1", + "@rollup/plugin-babel": "^6.0.4", "@rollup/plugin-image": "^3.0.3", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^15.2.3", @@ -278,6 +281,7 @@ "postcss": "8.4.41", "prettier": "3.3.3", "prettier-plugin-tailwindcss": "0.6.8", + "playwright": "^1.43.1", "raf": "^3.4.1", "raw-loader": "^4.0.2", "react-test-renderer": "^18.3.1", diff --git a/playwright-tests/16-tiers-page.spec.ts b/playwright-tests/16-tiers-page.spec.ts new file mode 100644 index 00000000000..71cc8085a1c --- /dev/null +++ b/playwright-tests/16-tiers-page.spec.ts @@ -0,0 +1,15 @@ +import { test, expect } from '@playwright/test' + +const baseUrl:String = 'http://localhost:3000' + +test('Can be accessed from "/collective/contribute" (default)', async ({ page }) => { + await page.goto(baseUrl+'/apex/contribute') + await expect(page).toHaveTitle('Contribute to APEX - Open Collective') + await expect(page.locator('link[rel="canonical"]')).toHaveAttribute('href',baseUrl+'/apex/contribute') +}) + +test('Can be accessed from "/collective/tiers"', async ({ page }) => { + await page.goto(baseUrl+'/apex/tiers') + await expect(page).toHaveTitle('Contribute to APEX - Open Collective') + await expect(page.locator('link[rel="canonical"]')).toHaveAttribute('href',baseUrl+'/apex/contribute') +}) diff --git a/playwright-tests/playwright-script.py b/playwright-tests/playwright-script.py new file mode 100644 index 00000000000..f8c6ce06af2 --- /dev/null +++ b/playwright-tests/playwright-script.py @@ -0,0 +1,27 @@ +import subprocess + +t1p = [] +t2p = [] +currentWorkingDirectory = "/Users/atharvchandratre/IdeaProjects/opencollective-frontend/" +for i in range(10): + commandstr = "npx playwright test playwright-tests/16-tiers-page.spec.ts --reporter=json --workers=1 | grep duration" + result = subprocess.run(commandstr.split(" "), capture_output=True, text=True, cwd=currentWorkingDirectory).stdout + splitresult = result.split("\"duration\": ") + t1p.append(int(splitresult[1].split(",")[0])) + t2p.append(int(splitresult[2].split(",")[0])) + +# t1 -> Test 1 of the spec file +# t2 -> Test 2 of the spec file +# c -> Cypress +# p -> Playwright + +# t1c = [2486, 2116, 2156, 2017, 2018, 2106, 2060, 1899, 2108, 2283] +# t2c = [2018, 2120, 2037, 2031, 2135, 2074, 2292, 2225, 2141, 2031] +# +# t1p = [1483, 1402, 1395, 1426, 1570, 2019, 1873, 1443, 1516, 1438] +# t2p = [1356, 1416, 1344, 1356, 1662, 1852, 1434, 1388, 1485, 1460] +# +# print(sum(t1c)/10) = 2124.9 +# print(sum(t2c)/10) = 2110.4 +# print(sum(t1p)/10) = 1556.5 +# print(sum(t2p)/10) = 1475.3 diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000000..bc154a17fd7 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,77 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './playwright-tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* 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: { + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://127.0.0.1:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + // { + // name: 'firefox', + // use: { ...devices['Desktop Firefox'] }, + // }, + // + // { + // name: 'webkit', + // use: { ...devices['Desktop Safari'] }, + // }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); From df79cb33930851d5f82d56eafb2a33bf08600471 Mon Sep 17 00:00:00 2001 From: Benjamin Piouffle Date: Thu, 4 Jan 2024 10:33:56 +0100 Subject: [PATCH 2/2] test(Playwright): fix configuration and add tests --- .github/workflows/ci.yml | 6 + .github/workflows/playwright.yml | 232 ++++++++++++++++++ package.json | 4 +- playwright-tests/playwright-script.py | 27 -- playwright.config.ts | 39 +-- scripts/run_e2e_tests.sh | 16 +- test/playwright/commands/authentication.ts | 70 ++++++ test/playwright/commands/contribution-flow.ts | 20 ++ test/playwright/commands/mock-now.ts | 29 +++ test/playwright/commands/stripe.ts | 21 ++ .../12-contributionFlow.donate.spec.ts | 69 ++++++ .../integration}/16-tiers-page.spec.ts | 20 +- 12 files changed, 473 insertions(+), 80 deletions(-) create mode 100644 .github/workflows/playwright.yml delete mode 100644 playwright-tests/playwright-script.py create mode 100644 test/playwright/commands/authentication.ts create mode 100644 test/playwright/commands/contribution-flow.ts create mode 100644 test/playwright/commands/mock-now.ts create mode 100644 test/playwright/commands/stripe.ts create mode 100644 test/playwright/integration/12-contributionFlow.donate.spec.ts rename {playwright-tests => test/playwright/integration}/16-tiers-page.spec.ts (58%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a70dbb39ce6..326f5687026 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -181,3 +181,9 @@ jobs: needs: build uses: ./.github/workflows/e2e.yml secrets: inherit + + playwright: + if: github.ref_name != 'i18n/crowdin' + needs: build + uses: ./.github/workflows/playwright.yml + secrets: inherit diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 00000000000..af2176ad2b2 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,232 @@ +name: Playwright E2E + +on: + workflow_call: + +env: + TZ: UTC + OC_ENV: ci + NODE_ENV: test + WEBSITE_URL: http://localhost:3000 + IMAGES_URL: http://localhost:3001 + PDF_SERVICE_URL: http://localhost:3002 + API_URL: http://localhost:3060 + API_KEY: dvl-1510egmf4a23d80342403fb599qd + CI: true + + E2E_TEST: 1 + PGHOST: localhost + PGUSER: postgres + GITHUB_CLIENT_ID: ${{ secrets.GH_CLIENT_ID }} + GITHUB_CLIENT_SECRET: ${{ secrets.GH_CLIENT_SECRET }} + FRONTEND_FOLDER: /home/runner/work/opencollective-frontend/opencollective-frontend + API_FOLDER: /home/runner/work/opencollective-frontend/opencollective-frontend/opencollective-api + IMAGES_FOLDER: /home/runner/work/opencollective-frontend/opencollective-frontend/opencollective-images + PDF_FOLDER: /home/runner/work/opencollective-frontend/opencollective-frontend/opencollective-pdf + TERM: xterm + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + STRIPE_WEBHOOK_KEY: ${{ secrets.STRIPE_WEBHOOK_KEY }} + STRIPE_WEBHOOK_SIGNING_SECRET: ${{ secrets.STRIPE_WEBHOOK_SIGNING_SECRET }} + AWS_KEY: ${{ secrets.AWS_KEY }} + AWS_SECRET: ${{ secrets.AWS_SECRET }} + +jobs: + e2e-playwright: + if: github.ref_name != 'i18n/crowdin' + + runs-on: ubuntu-latest + + timeout-minutes: 30 + + services: + redis: + image: redis + ports: + - 6379:6379 + options: --entrypoint redis-server + postgres: + image: postgres:13.13 + env: + POSTGRES_USER: postgres + POSTGRES_DB: postgres + POSTGRES_HOST_AUTH_METHOD: trust + ports: + - 5432:5432 + # needed because the postgres container does not provide a healthcheck + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + + steps: + - name: Update apt + run: sudo apt-get update || exit 0 + + - name: Install postgresql-client + run: sudo apt-get install -y postgresql-client + + - name: Install graphicsmagick + run: sudo apt-get install -y graphicsmagick + + - name: Install stripe-cli + run: | + sudo apt-get install -y wget + wget https://github.com/stripe/stripe-cli/releases/download/v1.13.9/stripe_1.13.9_linux_x86_64.tar.gz -O /tmp/stripe_1.13.9_linux_x86_64.tar.gz + sudo tar xzvf /tmp/stripe_1.13.9_linux_x86_64.tar.gz -C /bin/ + + - name: Checkout (frontend) + uses: actions/checkout@v4 + + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version-file: 'package.json' + cache: 'npm' + + # Checkouts + + - name: Set REF in env, removing the `refs/` part + run: | + echo "MATCHING_BRANCH_REF=${GITHUB_HEAD_REF-${GITHUB_REF##*/}}" >> $GITHUB_ENV + + - name: Check matching branch (api) + id: check-matching-branch + uses: octokit/request-action@v2.x + with: + route: GET /repos/{owner}/{repo}/git/ref/{ref} + owner: opencollective + repo: opencollective-api + ref: heads/${{ env.MATCHING_BRANCH_REF }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + continue-on-error: true + + - name: Checkout (api - matching branch) + if: steps.check-matching-branch.outputs.status == 200 + uses: actions/checkout@v4 + with: + repository: opencollective/opencollective-api + path: opencollective-api + ref: ${{ env.MATCHING_BRANCH_REF }} + + - name: Checkout (api - main) + if: steps.check-matching-branch.outputs.status != 200 + uses: actions/checkout@v4 + with: + repository: opencollective/opencollective-api + path: opencollective-api + + - name: Checkout (images) + uses: actions/checkout@v4 + with: + repository: opencollective/opencollective-images + path: opencollective-images + + - name: Checkout (PDF) + uses: actions/checkout@v4 + with: + repository: opencollective/opencollective-pdf + path: opencollective-pdf + + # Prepare API + + - name: Restore node_modules (api) + uses: actions/cache@v3 + id: api-node-modules + with: + path: opencollective-api/node_modules + key: ${{ runner.os }}-api-node-modules-${{ hashFiles('opencollective-api/package-lock.json') }} + + - name: Install dependencies (api) + working-directory: opencollective-api + if: steps.api-node-modules.outputs.cache-hit != 'true' + run: npm ci --prefer-offline --no-audit + + - name: Build (api) + working-directory: opencollective-api + run: npm run build + + # Prepare Images + + - name: Restore node_modules (images) + uses: actions/cache@v3 + id: images-node-modules + with: + path: opencollective-images/node_modules + key: ${{ runner.os }}-images-node-modules-${{ hashFiles('opencollective-images/package-lock.json') }} + + - name: Install dependencies (images) + working-directory: opencollective-images + if: steps.images-node-modules.outputs.cache-hit != 'true' + run: npm ci --prefer-offline --no-audit + + - name: Build (images) + working-directory: opencollective-images + run: npm run build + + # Prepare PDF + + - name: Restore node_modules (pdf) + uses: actions/cache@v3 + id: pdf-node-modules + with: + path: opencollective-pdf/node_modules + key: ${{ runner.os }}-pdf-node-modules-${{ hashFiles('opencollective-pdf/package-lock.json') }} + + - name: Install dependencies (pdf) + working-directory: opencollective-pdf + if: steps.pdf-node-modules.outputs.cache-hit != 'true' + run: npm ci --prefer-offline --no-audit + + - name: Build (pdf) + working-directory: opencollective-pdf + run: npm run build + + # Prepare Frontend + + - name: Restore node_modules (frontend) + uses: actions/cache@v3 + id: node-modules + with: + path: node_modules + key: ${{ runner.os }}-node-modules-${{ hashFiles('package-lock.json') }}-${{ secrets.CACHE_VERSION }} + + - name: Install dependencies (frontend) + if: steps.node-modules.outputs.cache-hit != 'true' + run: CYPRESS_INSTALL_BINARY=0 npm ci --prefer-offline --no-audit + + - name: Install Playwright + run: npx playwright install --with-deps + + - name: Restore .next build (frontend) + uses: actions/cache@v3 + id: next-build + with: + path: .next + key: ${{ runner.os }}-next-build-${{ github.sha }} + + - name: Restore .next cache (frontend) + if: steps.next-build.outputs.cache-hit != 'true' + uses: actions/cache@v3 + with: + path: .next/cache + key: ${{ runner.os }}-next-cache-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-next-cache-${{ github.sha }} + ${{ runner.os }}-next-cache- + + - name: Build (frontend) + if: steps.next-build.outputs.cache-hit != 'true' + run: npm run build + + - name: Setup DB + run: ./scripts/setup_db.sh + + - name: Run E2E with Playwright + run: ./scripts/run_e2e_tests.sh + env: + USE_PLAYWRIGHT: true + + - uses: actions/upload-artifact@v3 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/package.json b/package.json index 4e67d4e3a80..9980fc6974a 100644 --- a/package.json +++ b/package.json @@ -161,7 +161,7 @@ "commit": "git-cz", "cypress:open": "cross-env TZ=UTC cypress open --e2e", "cypress:run": "cross-env TZ=UTC cypress run", - "playwright:open": "cross-env TZ=UTC playwright open --e2e", + "playwright:open": "cross-env TZ=UTC playwright open", "depcheck": "npx @opencollective/depcheck", "deploy:production": "cross-env ./scripts/pre-deploy.sh production && git push production main", "deploy:staging": "cross-env ./scripts/pre-deploy.sh staging && git push -f staging main", @@ -188,7 +188,7 @@ "publish-components": "npm run script scripts/publish-components/index.ts $1 -- $@", "script": "tsx $@", "start": "node dist/server", - "start:ci": "nyc --silent --exclude \".next/**\" node server", + "start:ci": "nyc --silent --exclude \".next/**\" node dist/server", "start:e2e": "cross-env TZ=UTC NODE_ENV=production OC_ENV=e2e node server", "styled-components-migration-progress": "tsx scripts/styled-components-migration-progress.ts", "styleguide:build": "storybook build", diff --git a/playwright-tests/playwright-script.py b/playwright-tests/playwright-script.py deleted file mode 100644 index f8c6ce06af2..00000000000 --- a/playwright-tests/playwright-script.py +++ /dev/null @@ -1,27 +0,0 @@ -import subprocess - -t1p = [] -t2p = [] -currentWorkingDirectory = "/Users/atharvchandratre/IdeaProjects/opencollective-frontend/" -for i in range(10): - commandstr = "npx playwright test playwright-tests/16-tiers-page.spec.ts --reporter=json --workers=1 | grep duration" - result = subprocess.run(commandstr.split(" "), capture_output=True, text=True, cwd=currentWorkingDirectory).stdout - splitresult = result.split("\"duration\": ") - t1p.append(int(splitresult[1].split(",")[0])) - t2p.append(int(splitresult[2].split(",")[0])) - -# t1 -> Test 1 of the spec file -# t2 -> Test 2 of the spec file -# c -> Cypress -# p -> Playwright - -# t1c = [2486, 2116, 2156, 2017, 2018, 2106, 2060, 1899, 2108, 2283] -# t2c = [2018, 2120, 2037, 2031, 2135, 2074, 2292, 2225, 2141, 2031] -# -# t1p = [1483, 1402, 1395, 1426, 1570, 2019, 1873, 1443, 1516, 1438] -# t2p = [1356, 1416, 1344, 1356, 1662, 1852, 1434, 1388, 1485, 1460] -# -# print(sum(t1c)/10) = 2124.9 -# print(sum(t2c)/10) = 2110.4 -# print(sum(t1p)/10) = 1556.5 -# print(sum(t2p)/10) = 1475.3 diff --git a/playwright.config.ts b/playwright.config.ts index bc154a17fd7..36c9f0ae1e5 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -10,21 +10,16 @@ import { defineConfig, devices } from '@playwright/test'; * See https://playwright.dev/docs/test-configuration. */ export default defineConfig({ - testDir: './playwright-tests', + testDir: './test/playwright/integration', /* Run tests in files in parallel */ fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, - /* Retry on CI only */ - retries: process.env.CI ? 2 : 0, - /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, /* 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: { - /* Base URL to use in actions like `await page.goto('/')`. */ - // baseURL: 'http://127.0.0.1:3000', + baseURL: 'http://localhost:3000', /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry', @@ -36,36 +31,6 @@ export default defineConfig({ name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, - - // { - // name: 'firefox', - // use: { ...devices['Desktop Firefox'] }, - // }, - // - // { - // name: 'webkit', - // use: { ...devices['Desktop Safari'] }, - // }, - - /* Test against mobile viewports. */ - // { - // name: 'Mobile Chrome', - // use: { ...devices['Pixel 5'] }, - // }, - // { - // name: 'Mobile Safari', - // use: { ...devices['iPhone 12'] }, - // }, - - /* Test against branded browsers. */ - // { - // name: 'Microsoft Edge', - // use: { ...devices['Desktop Edge'], channel: 'msedge' }, - // }, - // { - // name: 'Google Chrome', - // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, - // }, ], /* Run your local dev server before starting the tests */ diff --git a/scripts/run_e2e_tests.sh b/scripts/run_e2e_tests.sh index 4647d065a75..833d528b495 100755 --- a/scripts/run_e2e_tests.sh +++ b/scripts/run_e2e_tests.sh @@ -84,9 +84,18 @@ echo "" wait_for_service PDF 127.0.0.1 3002 echo "" -echo "> Running cypress tests" - -npm run cypress:run -- ${CYPRESS_RECORD} --env OC_ENV=$OC_ENV --spec "test/cypress/integration/${CYPRESS_TEST_FILES}" +if [ "$USE_PLAYWRIGHT" = "true" ]; then + echo "> Running playwright tests" + pwd + ls + ls test + ls test/playwright + ls test/playwright/integration + npx playwright test test/playwright/integration/*.spec.ts +else + echo "> Running cypress tests" + npm run cypress:run -- ${CYPRESS_RECORD} --env OC_ENV=$OC_ENV --spec "test/cypress/integration/${CYPRESS_TEST_FILES}" +fi RETURN_CODE=$? if [ $RETURN_CODE -ne 0 ]; then @@ -102,5 +111,6 @@ kill $API_PID kill $FRONTEND_PID kill $IMAGES_PID kill $PDF_PID + echo "Exiting with code $RETURN_CODE" exit $RETURN_CODE diff --git a/test/playwright/commands/authentication.ts b/test/playwright/commands/authentication.ts new file mode 100644 index 00000000000..f237f8b3d54 --- /dev/null +++ b/test/playwright/commands/authentication.ts @@ -0,0 +1,70 @@ +import { Readable } from 'stream'; + +import fetch from 'node-fetch'; +import { Page } from 'playwright/test'; + +import { loggedInUserQuery } from '../../../lib/graphql/v1/queries'; + +import { randomEmail } from '../../cypress/support/faker'; + +const baseURL = 'http://localhost:3000'; // TODO: Get this to config + +const graphqlQueryV1 = (body: any, token: string) => { + return fetch(`${baseURL}/api/graphql/v1`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: !token ? undefined : `Bearer ${token}`, + }, + body: Readable.from([JSON.stringify(body)]), + }); +}; + +const signinRequest = (user, redirect: string, sendLink: boolean = false) => { + return fetch(`${baseURL}/api/users/signin`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: Readable.from([JSON.stringify({ user, redirect, createProfile: true, sendLink })]), + }); +}; + +const getLoggedInUserFromToken = async (token: string): Promise => { + const response = await graphqlQueryV1( + { operationName: 'LoggedInUser', query: loggedInUserQuery.loc.source.body, variables: {} }, + token, + ); + const result = await response.json(); + return result.data.LoggedInUser; +}; + +function getTokenFromRedirectUrl(url) { + const regex = /\/signin\/([^?]+)/; + return regex.exec(url)[1]; +} + +/** + * Signup with the given params and redirect to the provided URL + */ +export const signup = async ( + page: Page, + { + user, + redirect = '/', + }: { + user?: { email?: string; name?: string }; + redirect?: string; + } = {}, +) => { + const email = user?.email || randomEmail(); + const relativeRedirect = redirect.startsWith(baseURL) ? redirect.replace(baseURL, '') : redirect; + const response = await signinRequest({ ...user, email }, relativeRedirect); + const result = await response.json(); + const signInRedirectUrl = result.redirect; + const token = getTokenFromRedirectUrl(signInRedirectUrl); + await page.goto(signInRedirectUrl); // TODO: Rather than redirecting, we could exchange the token directly + return getLoggedInUserFromToken(token); +}; diff --git a/test/playwright/commands/contribution-flow.ts b/test/playwright/commands/contribution-flow.ts new file mode 100644 index 00000000000..dd01567073a --- /dev/null +++ b/test/playwright/commands/contribution-flow.ts @@ -0,0 +1,20 @@ +import { Page } from 'playwright/test'; + +export const checkStepsProgress = async ( + page: Page, + { + enabled = [], + disabled = [], + }: { + enabled?: string | string[]; + disabled?: string | string[]; + }, +) => { + const isEnabled = step => page.waitForSelector(`[data-cy="progress-step-${step}"][data-disabled=false]`); + const isDisabled = step => page.waitForSelector(`[data-cy="progress-step-${step}"][data-disabled=true]`); + + await Promise.all([ + ...(Array.isArray(enabled) ? enabled.map(isEnabled) : [isEnabled(enabled)]), + ...(Array.isArray(disabled) ? disabled.map(isDisabled) : [isDisabled(disabled)]), + ]); +}; diff --git a/test/playwright/commands/mock-now.ts b/test/playwright/commands/mock-now.ts new file mode 100644 index 00000000000..ce07dcf4f7f --- /dev/null +++ b/test/playwright/commands/mock-now.ts @@ -0,0 +1,29 @@ +import { Page } from 'playwright/test'; + +export const mockNow = async (page: Page, now: number) => { + await page.addInitScript(`{ + // Extend Date constructor to default to now + Date = class extends Date { + constructor(...args) { + if (args.length === 0) { + super(${now}); + } else { + super(...args); + } + } + } + + // Override Date.now() to start from now + Date.__DateNowOffset = ${now} - Date.now(); + Date.__originalDateNow = Date.now; + Date.now = () => Date.__originalDateNow() + Date.__DateNowOffset; + }`); +}; + +export const restoreNow = async (page: Page) => { + await page.evaluate(` + Date.now = Date.__originalDateNow; + delete Date.__originalDateNow; + delete Date.__DateNowOffset; + `); +}; diff --git a/test/playwright/commands/stripe.ts b/test/playwright/commands/stripe.ts new file mode 100644 index 00000000000..5b90f577f16 --- /dev/null +++ b/test/playwright/commands/stripe.ts @@ -0,0 +1,21 @@ +import { Page } from 'playwright/test'; + +import { CreditCards } from '../../stripe-helpers'; + +type FillStripeInputOptions = { + card?: { + creditCardNumber?: string; + expirationDate?: string; + cvcCode?: string; + postalCode?: string; + }; +}; + +export const fillStripeInput = async (page: Page, { card = CreditCards.CARD_DEFAULT }: FillStripeInputOptions = {}) => { + const stripeIframeSelector = '.__PrivateStripeElement iframe'; + const stripeFrame = page.frameLocator(stripeIframeSelector).first(); + card.creditCardNumber && (await stripeFrame.locator('[placeholder="Card number"]').fill(card.creditCardNumber)); + card.expirationDate && (await stripeFrame.locator('[placeholder="MM / YY"]').fill(card.expirationDate)); + card.cvcCode && (await stripeFrame.locator('[placeholder="CVC"]').fill(card.cvcCode)); + card.postalCode && (await stripeFrame.locator('[placeholder="ZIP"]').fill(card.postalCode)); +}; diff --git a/test/playwright/integration/12-contributionFlow.donate.spec.ts b/test/playwright/integration/12-contributionFlow.donate.spec.ts new file mode 100644 index 00000000000..3bf9fc6a894 --- /dev/null +++ b/test/playwright/integration/12-contributionFlow.donate.spec.ts @@ -0,0 +1,69 @@ +import { expect, test } from '@playwright/test'; + +import { signup } from '../commands/authentication'; +import { checkStepsProgress } from '../commands/contribution-flow'; +import { fillStripeInput } from '../commands/stripe'; + +test('Can donate as new user', async ({ page }) => { + const userParams = { name: 'Donate Tester' }; + const user = await signup(page, { redirect: `/apex/donate`, user: userParams }); + // General checks + await expect(page).toHaveTitle('Contribute to APEX - Open Collective'); + await expect(page.locator('link[rel="canonical"]')).toHaveAttribute('href', /\/apex\/donate$/); + + // ---- Step Details ---- + // Has default amount selected + await page.waitForSelector('#amount button.selected'); + + // Change amount + await page.click('[data-cy="amount-picker-btn-other"]'); + await page.fill('input[type=number][name=custom-amount]', '1337'); + await page.waitForSelector('[data-cy="progress-step-details"] :text("1,337.00")'); + + // Change frequency - monthly + await page.click('text="Monthly"'); + await page.waitForSelector('[data-cy="progress-step-details"] :text("1,337.00 USD / mo.")'); + // await page.waitForSelector(':has-text("the next charge will be on July 1, 2042")'); + + // Change frequency - yearly + await page.click('text="Yearly"'); + await page.waitForSelector('[data-cy="progress-step-details"] :text("1,337.00 USD / yr.")'); + await page.waitForSelector('text="Today\'s charge"'); + // await page.waitForSelector(':has-text("the next charge will be on May 1, 2043")'); + + // Click the button + await page.click('button[data-cy="cf-next-step"]'); + + // ---- Step profile ---- + await checkStepsProgress(page, { enabled: ['profile', 'details'], disabled: 'payment' }); + + // Personal account must be the first entry, and it must be checked + await page.waitForSelector(`[data-cy="contribute-profile-picker"] :text("${user.collective.name}")`); + await page.waitForSelector('[data-cy="contribute-profile-picker"] :text("Personal")'); + await page.click('[data-cy="contribute-profile-picker"]'); + await page.waitForSelector(`[data-cy="select-option"]:first-of-type :text("${user.collective.name}")`); + await page.waitForSelector('[data-cy="select-option"]:first-of-type :text("Personal")'); + await page.keyboard.press('Escape'); + + // User profile is shown on step, all other steps must be disabled + await page.waitForSelector(`[data-cy="progress-step-profile"] :text("${user.collective.name}")`); + await page.click('button[data-cy="cf-next-step"]'); + + // ---- Step Payment ---- + await checkStepsProgress(page, { enabled: ['profile', 'details', 'payment'] }); + + // As this is a new account, no payment method is configured yet, so + // we should have the credit card form selected by default. + await page.waitForSelector('input[type=checkbox][name=save]:checked', { state: 'hidden' }); // Our checkbox has custom styles and the input is hidden + + // Ensure we display errors + await fillStripeInput(page, { card: { creditCardNumber: '123' } }); + await page.click('button:has-text("Contribute $1,337")'); + await page.waitForSelector('text="Credit card ZIP code and CVC are required"'); + + // Submit with valid credit card + await fillStripeInput(page); + await page.click('button:has-text("Contribute $1,337")'); + await page.waitForURL(`**/apex/donate/success**`, { timeout: 10000 }); + await page.waitForSelector('text="You are now supporting APEX."'); +}); diff --git a/playwright-tests/16-tiers-page.spec.ts b/test/playwright/integration/16-tiers-page.spec.ts similarity index 58% rename from playwright-tests/16-tiers-page.spec.ts rename to test/playwright/integration/16-tiers-page.spec.ts index 71cc8085a1c..f2c0bf1a9b8 100644 --- a/playwright-tests/16-tiers-page.spec.ts +++ b/test/playwright/integration/16-tiers-page.spec.ts @@ -1,15 +1,13 @@ -import { test, expect } from '@playwright/test' - -const baseUrl:String = 'http://localhost:3000' +import { expect, test } from '@playwright/test'; test('Can be accessed from "/collective/contribute" (default)', async ({ page }) => { - await page.goto(baseUrl+'/apex/contribute') - await expect(page).toHaveTitle('Contribute to APEX - Open Collective') - await expect(page.locator('link[rel="canonical"]')).toHaveAttribute('href',baseUrl+'/apex/contribute') -}) + await page.goto(`/apex/contribute`); + await expect(page).toHaveTitle('Contribute to APEX - Open Collective'); + await expect(page.locator('link[rel="canonical"]')).toHaveAttribute('href', /\/apex\/contribute$/); +}); test('Can be accessed from "/collective/tiers"', async ({ page }) => { - await page.goto(baseUrl+'/apex/tiers') - await expect(page).toHaveTitle('Contribute to APEX - Open Collective') - await expect(page.locator('link[rel="canonical"]')).toHaveAttribute('href',baseUrl+'/apex/contribute') -}) + await page.goto(`/apex/tiers`); + await expect(page).toHaveTitle('Contribute to APEX - Open Collective'); + await expect(page.locator('link[rel="canonical"]')).toHaveAttribute('href', /\/apex\/contribute$/); +});