diff --git a/.github/workflows/build-nextjs.yml b/.github/workflows/build-nextjs.yml index 107d8280a..b7ad176aa 100644 --- a/.github/workflows/build-nextjs.yml +++ b/.github/workflows/build-nextjs.yml @@ -26,7 +26,7 @@ jobs: ${{ runner.os }}-build-yarn- - name: Install dependencies - run: yarn + run: yarn install --immutable - name: Build frontend run: | diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 48bc32e5b..751b8daf4 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/checkout@v3 - name: Install dependencies - run: yarn + run: yarn install --immutable - name: Lint frontend run: yarn lint diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 7ebf3f398..6978a0fde 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -67,7 +67,11 @@ jobs: - name: Install Frontend Dependencies working-directory: ./frontend - run: yarn + run: yarn install --immutable + + - name: Install e2e Dependencies + working-directory: ./frontend/e2e + run: yarn install --immutable - name: Setup env working-directory: ./frontend @@ -92,8 +96,8 @@ jobs: run: yarn start &> frontend.log & - name: Install Playwright Browsers - working-directory: ./frontend - run: npx playwright install --with-deps + working-directory: ./frontend/e2e + run: yarn run playwright:install - name: Wait on frontend uses: iFaxity/wait-on-action@v1 @@ -103,13 +107,20 @@ jobs: - name: Run Frontend Tests working-directory: ./frontend - run: yarn playwright test e2e/local + run: yarn run test:e2e - uses: actions/upload-artifact@v3 if: always() with: name: playwright-report - path: ./frontend/playwright-report/ + path: ./frontend/e2e/test-results/ + retention-days: 14 + + - uses: actions/upload-artifact@v3 + if: always() + with: + name: playwright-report + path: ./frontend/e2e/e2e-reports/ retention-days: 14 - uses: actions/upload-artifact@v3 diff --git a/.gitignore b/.gitignore index 411174023..1e32b1ab9 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,13 @@ lerna-debug.log* coverage .nyc_output +# E2E +test-results/ +playwright-report/ +playwright/.cache/ +e2e-reports/ +e2e/.yarn/* + # IDEs and editors .idea .project @@ -51,9 +58,6 @@ build/ bld/ [Bb]in/ [Oo]bj/ -/test-results/ -/playwright-report/ -/playwright/.cache/ # https://yarnpkg.com/getting-started/qa/#which-files-should-be-gitignored .pnp.* diff --git a/e2e/AuthPage.ts b/e2e/AuthPage.ts deleted file mode 100644 index 5bbee26a3..000000000 --- a/e2e/AuthPage.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Page } from '@playwright/test' - -const credentials = { - username: process.env.USERNAME ?? 'admin@podkrepi.bg', - password: process.env.PASSWORD ?? '$ecurePa33', -} - -export class AuthPage { - page: Page - constructor(page: Page) { - this.page = page - } - - async _submitLoginForm() { - await this.page.click('input[type="email"]') - await this.page.fill('input[type="email"]', credentials.username) - await this.page.click('input[type="password"]') - await this.page.fill('input[type="password"]', credentials.password) - await this.page.click('text="Вход"') - await this.page.waitForNavigation({ waitUntil: 'networkidle' }) - } - - async login() { - await Promise.all([ - this.page.goto('http://localhost:3040/login'), - this.page.waitForNavigation(), - ]) - - await this._submitLoginForm() - } -} diff --git a/e2e/README.md b/e2e/README.md index 444b53d23..37cf6d47a 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -45,14 +45,8 @@ Options: yarn test:e2e ``` -### Run only local test suites +### Run test suites in headed mode with enabled debug ```shell -yarn test:e2e local -``` - -### Run local test suites in headed mode with enabled debug - -```shell -yarn test:e2e local --headed --debug -x -g support +yarn test:e2e --headed --debug -x -g support ``` diff --git a/e2e/data/enums/donation-regions.enum.ts b/e2e/data/enums/donation-regions.enum.ts new file mode 100644 index 000000000..327512e3e --- /dev/null +++ b/e2e/data/enums/donation-regions.enum.ts @@ -0,0 +1,5 @@ +export enum DonationRegions { + EUROPE = 'EU', + GREAT_BRITAIN = 'UK', + OTHER = 'Other', +} diff --git a/e2e/data/enums/languages.enum.ts b/e2e/data/enums/languages.enum.ts new file mode 100644 index 000000000..b249974ff --- /dev/null +++ b/e2e/data/enums/languages.enum.ts @@ -0,0 +1,6 @@ +// This enum should be used as a parameter for methods in E2E tests + +export enum LanguagesEnum { + BG = 'BG', + EN = 'EN', +} diff --git a/e2e/data/localization.ts b/e2e/data/localization.ts new file mode 100644 index 000000000..6397cbabd --- /dev/null +++ b/e2e/data/localization.ts @@ -0,0 +1,37 @@ +import bgLocalizationCommonJson from '../../public/locales/bg/common.json' +import enLocalizationCommonJson from '../../public/locales/en/common.json' + +import bgLocalizationIndexJson from '../../public/locales/bg/index.json' +import enLocalizationIndexJson from '../../public/locales/en/index.json' + +import bgLocalizationSupportJson from '../../public/locales/bg/support.json' +import enLocalizationSupportJson from '../../public/locales/en/support.json' + +import bgLocalizationValidationJson from '../../public/locales/bg/validation.json' +import enLocalizationValidationJson from '../../public/locales/en/validation.json' + +import bgLocalizationCampaignsJson from '../../public/locales/bg/campaigns.json' +import enLocalizationCampaignsJson from '../../public/locales/en/campaigns.json' + +import bgLocalizationOneTimeDonationJson from '../../public/locales/bg/one-time-donation.json' +import enLocalizationOneTimeDonationJson from '../../public/locales/en/one-time-donation.json' + +// All these constants are used in the E2E test pages to manipulate web elements in a respective language +// Common localization terms +export const bgLocalizationCommon = bgLocalizationCommonJson +export const enLocalizationCommon = enLocalizationCommonJson +// Home +export const bgLocalizationIndex = bgLocalizationIndexJson +export const enLocalizationIndex = enLocalizationIndexJson +// Support page +export const bgLocalizationSupport = bgLocalizationSupportJson +export const enLocalizationSupport = enLocalizationSupportJson +// Campaigns page +export const bgLocalizationCampaigns = bgLocalizationCampaignsJson +export const enLocalizationCampaigns = enLocalizationCampaignsJson +// Donations +export const bgLocalizationOneTimeDonation = bgLocalizationOneTimeDonationJson +export const enLocalizationOneTimeDonation = enLocalizationOneTimeDonationJson +// Validations +export const bgLocalizationValidation = bgLocalizationValidationJson +export const enLocalizationValidation = enLocalizationValidationJson diff --git a/e2e/data/support-page-tests.data.ts b/e2e/data/support-page-tests.data.ts new file mode 100644 index 000000000..d93cddc8e --- /dev/null +++ b/e2e/data/support-page-tests.data.ts @@ -0,0 +1,15 @@ +export const supportPageVolutneerTestData = { + firstName: 'Test valid first name', + lastName: 'Test valid last name', + email: 'e2e_test_mail@test.bg', + phone: '+359888000000', + comment: 'E2E Test comment', +} + +export const anonDonationTestData = { + cardNumber: '4242 4242 4242 4242', + cardExpDate: '04 / 42', + cardCvc: '424', + billingName: 'E2E Test Anonymous Donation', + country: 'BG', +} diff --git a/e2e/local/campaigns.spec.ts b/e2e/local/campaigns.spec.ts deleted file mode 100644 index 203298aa4..000000000 --- a/e2e/local/campaigns.spec.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { test, expect } from '@playwright/test' - -test.beforeEach(async ({ page }) => { - await page.goto('/') - await page.locator('button[data-testid="jumbotron-donate-button"]').click() - await page.waitForURL('/campaigns') -}) - -test.describe('campaigns page', () => { - test('test rendering and defaults', async ({ page }) => { - expect(page.locator('text=Кампании')).toBeDefined() - expect(page.locator('text=Подкрепете кауза днес!')).toBeDefined() - }) - test('click donate button of active campaign', async ({ page }) => { - await page.locator('button:has-text("Подкрепете сега"):not([disabled])').first().click() - await page.waitForURL((url) => url.pathname.includes('/campaigns/donation')) - expect(page.locator('text=Как желаете да дарите?')).toBeDefined() - expect(page.locator('text=Карта')).toBeDefined() - expect(page.locator('text=5 лв.')).toBeDefined() - }) - test('successful campaign has a disabled donate button', async ({ page }) => { - expect(page.locator('button:has-text("Подкрепете сега"):is([disabled])')).toBeDefined() - }) - test('click show more button leads to campaign details', async ({ page }) => { - await page.locator('text="Вижте повече"').first().click() - await page.waitForURL((url) => url.pathname.includes('/campaigns/')) - expect(page.locator('text=Сподели')).toBeDefined() - }) -}) diff --git a/e2e/local/donation.spec.ts b/e2e/local/donation.spec.ts deleted file mode 100644 index d565121fa..000000000 --- a/e2e/local/donation.spec.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { test, expect } from '@playwright/test' -import { expectCopied } from '../helpers' - -test.beforeEach(async ({ page }) => { - await page.goto('http://localhost:3040/', { waitUntil: 'networkidle' }) - await page.locator('button:not([disabled]):has-text("Подкрепете сега")').first().click() - await page.waitForURL((url) => url.pathname.includes('/campaigns/donation')) -}) - -test.describe('donation page init', () => { - test('test rendering and defaults', async ({ page }) => { - await expect( - page.locator('label', { has: page.locator('text=Карта') }).locator('input[type="radio"]'), - ).toBeChecked() - await expect( - page - .locator('label', { has: page.locator('text=Банков превод') }) - .locator('input[type="radio"]'), - ).not.toBeChecked() - }) -}) - -test.describe('anonymous user card donation flow', () => { - test('choosing a predefined value and donating', async ({ page }) => { - // Choose a predefined value from the radio buttons - await page.locator('input[value="card"]').check() - await page.locator('input[value="500"]').check() - - // Click checkbox to cover the tax by stripe - await page.locator('input[name="cardIncludeFees"]').check() - await page.locator('button:has-text("Напред")').click() - - await page.locator('text=Дарете анонимно').click() - await page.locator('button:has-text("Напред")').click() - - await page.fill('textarea', 'е2е_tester') - await page.locator('button:has-text("Премини към плащане")').click() - - await expect(page.locator('text=BGN 5.00')).toBeDefined() - await page.locator('input[name="email"]').fill('anon_e2e_tester@podkrepi.bg') - await page.locator('input[name="cardNumber"]').fill('4242424242424242') - await page.locator('input[name="cardExpiry"]').fill('04 / 24') - await page.locator('input[name="cardCvc"]').fill('123') - await page.locator('input[name="billingName"]').fill('John Doe') - await page.locator('select[name="billingCountry"]').selectOption('BG') - - await page.locator('button[data-testid="hosted-payment-submit-button"]').click() - - await page.waitForURL((url) => url.pathname.includes('/campaigns/donation')) - - expect(page.locator('text=Благодарим за доверието и подкрепата!')).toBeDefined() - }) - - test('choosing a custom value and donating', async ({ page }) => { - // Choose a predefined value from the radio buttons - await page.locator('input[value="card"]').check() - await page.locator('input[value="other"]').check() - // Need to take the first here because MUICollapse animations creates a copy - await page.locator('input[name="otherAmount"]').first().type('6') - - // Click checkbox to cover the tax by stripe - await page.locator('input[name="cardIncludeFees"]').check() - await page.locator('button:has-text("Напред")').click() - - await page.locator('text=Дарете анонимно').click() - await page.locator('button:has-text("Напред")').click() - - await page.fill('textarea', 'е2е_tester') - await page.locator('button:has-text("Премини към плащане")').click() - - await expect(page.locator('text=BGN 6.58')).toBeDefined() - await page.locator('input[name="email"]').fill('anon_e2e_tester@podkrepi.bg') - await page.locator('input[name="cardNumber"]').fill('4242424242424242') - await page.locator('input[name="cardExpiry"]').fill('0424') - await page.locator('input[name="cardCvc"]').fill('123') - await page.locator('input[name="billingName"]').fill('John Doe') - await page.locator('select[name="billingCountry"]').selectOption('BG') - - await page.locator('button[data-testid="hosted-payment-submit-button"]').click() - - await page.waitForURL((url) => url.pathname.includes('/campaigns/donation')) - expect(page.url().search('success=true')).not.toBe(-1) - - expect(page.locator('text=Благодарим за доверието и подкрепата!')).toBeDefined() - }) -}) - -test.describe('user bank transfer donation flow', () => { - test('check copied values', async ({ page }) => { - // Choose a predefined value from the radio buttons - await page.locator('input[value="bank"]').check() - expect(await page.locator('text="Копирай"').count()).toBe(4) - - if (page.context().browser()?.browserType().name() === 'chromium') { - await page.context().grantPermissions(['clipboard-read', 'clipboard-write']) - await page.locator('text="Копирай"').nth(0).click() - await expectCopied(page, 'Сдружение Подкрепи БГ') - await page.locator('text="Копирай"').nth(1).click() - await expectCopied(page, 'Уникредит Булбанк') - await page.locator('text="Копирай"').nth(2).click() - await expectCopied(page, 'BG66UNCR70001524349032') - await page.locator('text="Копирай"').nth(3).click() - const reference = await page.locator('p[data-testid="payment-reference-field"]').innerText() - await expectCopied(page, reference) - } - }) -}) diff --git a/e2e/local/homepage.spec.ts b/e2e/local/homepage.spec.ts deleted file mode 100644 index 6f028853c..000000000 --- a/e2e/local/homepage.spec.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { test, expect } from '@playwright/test' - -test.beforeEach(async ({ page }) => { - await page.goto('http://localhost:3040/') -}) - -test('test homepage', async ({ page }) => { - await page.waitForURL('http://localhost:3040/') - // Click text=Спешни кампании - await expect(page.locator('text=Спешни кампании')).toBeVisible() - - // Click text=Как работи Подкрепи.бг? - await expect(page.locator('text=Как работи Подкрепи.бг?')).toBeVisible() - - // Click text=Кой стои зад Подкрепи.бг? - await expect(page.locator('text=Кой стои зад Подкрепи.бг?')).toBeVisible() - - // Click text=Присъединете се към Подкрепи.бг - await expect(page.locator('text=Присъединете се към Подкрепи.бг')).toBeVisible() - - // // Click h2:has-text("Често задавани въпроси") - // await expect(page.locator('h2', { hasText: 'Често задавани въпроси' })).toBeVisible() - - // Click text=Какво е Подкрепи.бг? - await page.locator('text=Какво е Подкрепи.бг?').click() - - // Click text=Ние сме общност от доброволци, обединени от идеята да създаваме устойчиви решения за развитието на дарителството в България. - await page - .locator( - 'text=Ние сме общност от доброволци, обединени от идеята да създаваме устойчиви решения за развитието на дарителството в България. ', - ) - .waitFor({ state: 'visible' }) - - // Click text=Какво е „безкомпромисна прозрачност”? - await page.locator('text=Какво е „безкомпромисна прозрачност”?').click() - await page - .locator('text=Нашето разбиране за „безкомпромисна прозрачност” е:') - .waitFor({ state: 'visible' }) - - // Click text=Какви са технологичните ви предимства? - await page.locator('text=Какви са технологичните ви предимства?').click() - - // Click text=Използваме модерни решения и технологии за подсигуряване на платформата – React, Next.js като frontend, PostgreSQL като база данни, а цялостната инфраструктура се управлява на принципа на Infrastructure-as-Codе. - await page - .locator( - 'text=Използваме модерни решения и технологии за подсигуряване на платформата – React, Next.js като frontend, PostgreSQL като база данни, а цялостната инфраструктура се управлява на принципа на Infrastructure-as-Codе.', - ) - .waitFor({ state: 'visible' }) - // Click text=Какво представляват „устойчивите решения”? - await page.locator('text=Какво представляват „устойчивите решения”?').click() - - // Click text=Една африканска поговорка гласи „Ако искаш да стигнеш бързо, тръгни сам, ако искаш да стигнеш далеч, вървете заедно”. - page - .locator( - 'text=Една африканска поговорка гласи „Ако искаш да стигнеш бързо, тръгни сам, ако искаш да стигнеш далеч, вървете заедно”.', - ) - .waitFor({ state: 'visible' }) - - // Click text=Какво представляват „устойчивите решения”? - await page.locator('text=Как се финансира Подкрепи.бг?').click() - - // Click text=Подкрепи.бг НЕ удържа комисиони или такси за дейността си от събраните средства за кампаниите. - await page - .locator( - 'text=Подкрепи.бг НЕ удържа комисиони или такси за дейността си от събраните средства за кампаниите.', - ) - .waitFor({ state: 'visible' }), - // Click text=Вижте всички >> nth=1 - await page.locator('button[data-testid="faq-see-more-button"]').click() - await page.waitForURL('http://localhost:3040/faq') - await expect(page).toHaveURL('http://localhost:3040/faq') -}) diff --git a/e2e/local/support.spec.ts b/e2e/local/support.spec.ts deleted file mode 100644 index bc796e86b..000000000 --- a/e2e/local/support.spec.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { expect, test } from '@playwright/test' - -test.beforeEach(async ({ page }) => { - await page.goto('http://localhost:3040/') -}) - -test('test support page', async ({ page }) => { - // Click text=Станете доброволец >> nth=0 - await page.locator('text=Станете доброволец').first().click() - - await page.waitForURL('http://localhost:3040/support') - // Click h1:has-text("Станете доброволец") - await expect(page.locator('h1:has-text("Станете доброволец")')).toBeVisible() - - // Click text=Как искате да ни подкрепите? - await expect(page.locator('text=Как искате да ни подкрепите?')).toBeVisible() - - // Click text=Включете се в организацията като: - await expect(page.locator('text=Включете се в организацията като:')).toBeVisible() - - // Check input[name="roles\.benefactor"] - await page.locator('input[name="roles\\.benefactor"]').check() - - // Check input[name="roles\.associationMember"] - await page.locator('input[name="roles\\.associationMember"]').check() - - // Click text=Напред - await page.locator('text=Напред').click() - - // Click text=В каква роля искате да ни подкрепите? - await expect(page.locator('text=В каква роля искате да ни подкрепите?')).toBeVisible() - - // Click text=Назад - await page.locator('text=Назад').click() - - // Click text=Как искате да ни подкрепите? - await expect(page.locator('text=Как искате да ни подкрепите?')).toBeVisible() - - // Click text=Напред - await page.locator('text=Напред').click() - - // Check input[name="benefactor\.campaignBenefactor"] - await page.locator('input[name="benefactor\\.campaignBenefactor"]').check() - - // Click text=Дарител в бъдещи кампании - await expect(page.locator('text=Дарител в бъдещи кампании')).toBeVisible() - - // Click text=Моля, изберете си роля - // await page.locator('text=Моля, изберете си роля').click() - - // Click text=Дарител в бъдещи кампании - // await page.locator('text=Дарител в бъдещи кампании').click() - - // Click text=Напред - await page.locator('text=Напред').click() - - // Click text=Изпратете - // Formik seems to have problems validating in webkit and chromium on the first click for some reason - await page.locator('text=Изпратете').click() - await page.locator('text=Изпратете').click() - - expect(await page.locator('text=Задължително поле').count()).toBe(4) - - // Click text=Моля, приемете oбщите условия - await expect(page.locator('text=Моля, приемете oбщите условия')).toBeVisible() - - // Click text=Моля, приемете политиката за защита на личните данни - await expect( - page.locator('text=Моля, приемете политиката за защита на личните данни'), - ).toBeVisible() - - // Click input[name="person\.firstName"] - await page.locator('input[name="person\\.firstName"]').click() - - // Fill input[name="person\.firstName"] - await page.locator('input[name="person\\.firstName"]').fill('test') - - // Click input[name="person\.lastName"] - await page.locator('input[name="person\\.lastName"]').click() - - // Fill input[name="person\.lastName"] - await page.locator('input[name="person\\.lastName"]').fill('test') - - // Click input[name="person\.email"] - await page.locator('input[name="person\\.email"]').click() - - // Fill input[name="person\.email"] - await page.locator('input[name="person\\.email"]').fill('test@test.com') - - // Click input[name="person\.phone"] - await page.locator('input[name="person\\.phone"]').click() - - // Fill input[name="person\.phone"] - await page.locator('input[name="person\\.phone"]').fill('0987654321') - - // Click textarea[name="person\.comment"] - await page.locator('textarea[name="person\\.comment"]').click() - - // Fill textarea[name="person\.comment"] - await page.locator('textarea[name="person\\.comment"]').fill('test test test') - - // Check input[name="person\.terms"] - await page.locator('input[name="person\\.terms"]').check() - - // Check input[name="person\.gdpr"] - await page.locator('input[name="person\\.gdpr"]').check() - - // Check input[name="person\.newsletter"] - await page.locator('input[name="person\\.newsletter"]').check() - - // Click text=Изпратете - await page.locator('text=Изпратете').click() - - // Click text=Благодарим Ви, че ни подкрепихте! - await expect(page.locator('text=Благодарим Ви, че ни подкрепихте!')).toBeVisible() - - // Click text=Очаквайте представител на Подкрепи.бг да се свърже с Вас на посочения имейл адре - await page - .locator( - 'text=Очаквайте представител на Подкрепи.бг да се свърже с Вас на посочения имейл адрес', - ) - .click() -}) diff --git a/e2e/package.json b/e2e/package.json new file mode 100644 index 000000000..18d7e77cc --- /dev/null +++ b/e2e/package.json @@ -0,0 +1,20 @@ +{ + "name": "e2e", + "version": "1.0.0", + "description": "- ", + "main": "index.js", + "scripts": { + "test:e2e": "playwright test", + "playwright:install": "playwright install" + }, + "directories": { + "test": "tests" + }, + "dependencies": { + "@playwright/test": "1.30.0", + "playwright": "1.30.0" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/e2e/pages/web-pages/admin/AuthPage.ts b/e2e/pages/web-pages/admin/AuthPage.ts new file mode 100644 index 000000000..de6dcbdcf --- /dev/null +++ b/e2e/pages/web-pages/admin/AuthPage.ts @@ -0,0 +1,34 @@ +export {} +// import { Page } from '@playwright/test'; + +// const credentials = { +// username: process.env.USERNAME ?? 'admin@podkrepi.bg', +// password: process.env.PASSWORD ?? '$ecurePa33', +// } + +// // TODO: This should be refactored and used after discussions with the devs +// // Also move the credentials out of the repo +// export class AuthPage { +// page: Page +// constructor(page: Page) { +// this.page = page +// } + +// async _submitLoginForm() { +// await this.page.click('input[type="email"]') +// await this.page.fill('input[type="email"]', credentials.username) +// await this.page.click('input[type="password"]') +// await this.page.fill('input[type="password"]', credentials.password) +// await this.page.click('text="Вход"') +// await this.page.waitForNavigation({ waitUntil: 'networkidle' }) +// } + +// async login() { +// await Promise.all([ +// this.page.goto('http://localhost:3040/login'), +// this.page.waitForNavigation(), +// ]) + +// await this._submitLoginForm() +// } +// } diff --git a/e2e/pages/web-pages/base.page.ts b/e2e/pages/web-pages/base.page.ts new file mode 100644 index 000000000..74737fa15 --- /dev/null +++ b/e2e/pages/web-pages/base.page.ts @@ -0,0 +1,480 @@ +import { expect, Locator, Page } from '@playwright/test' +import { ClickOptions, LocatorOptions } from '../../utils/types' + +// Here we define all base methods, which are inherited into the other pages +export class BasePage { + protected page: Page + + constructor(page: Page) { + this.page = page + } + + private readonly checkboxLabelSelector = 'label.MuiFormControlLabel-root' + private readonly checkboxLabelXpath = "//label[contains(@class, 'MuiFormControlLabel-root')]" + private readonly activeHorizontalStepSelector = '.MuiStepLabel-labelContainer span.Mui-active' + + /** + * Wait for element by PW Locator to be attached to the DOM tree + * @param {Locator} elementLocator + * @param {number} timeoutParam - the default is 10sec + */ + async waitForElementToBePresentedByLocator( + elementLocator: Locator, + timeoutParam = 10000, + ): Promise { + await elementLocator.last().waitFor({ state: 'attached', timeout: timeoutParam }) + } + + /** + * Wait for element by PW Locator to be visible on the DOM tree + * @param {Locator} elementLocator + * @param {number} timeoutParam - the default value is 10sec + */ + async waitForElementToBeReadyByLocator( + elementLocator: Locator, + timeoutParam = 10000, + ): Promise { + await elementLocator.first().waitFor({ state: 'visible', timeout: timeoutParam }) + } + + /** + * Wait for element by CSS Selector to be attached to the DOM tree + * @param {string} elementSelector + * @param {LocatorOptions} options + * @param {number} timeoutParam - the default is 10sec + */ + async waitForElementToBePresentedBySelector( + elementSelector: string, + options?: LocatorOptions, + timeoutParam = 10000, + ): Promise { + let elementLocator: Locator + if (options) { + elementLocator = this.page.locator(elementSelector, options) + } else { + elementLocator = this.page.locator(elementSelector) + } + await elementLocator.first().waitFor({ state: 'attached', timeout: timeoutParam }) + } + + /** + * Wait for element by CSS Selector to be visible on the DOM tree + * @param {string} elementSelector + * @param {LocatorOptions} options + * @param {number} timeoutParam + */ + async waitForElementToBeReadyBySelector( + elementSelector: string, + options?: LocatorOptions, + timeoutParam = 10000, + ): Promise { + if (options) { + await this.page + .locator(elementSelector, options) + .first() + .waitFor({ state: 'visible', timeout: timeoutParam }) + } else { + await this.page + .locator(elementSelector) + .first() + .waitFor({ state: 'visible', timeout: timeoutParam }) + } + } + + /** + * Click web element by CSS Selector + * @param {string} elementSelector + * @param {LocatorOptions} elementOptions + * @param {ClickOptions} clickOptions + */ + async clickElement( + elementSelector: string, + elementOptions?: LocatorOptions, + clickOptions?: ClickOptions, + ): Promise { + if (elementOptions) { + const firstElement = this.page.locator(elementSelector, elementOptions).first() + const lastElement = this.page.locator(elementSelector, elementOptions).last() + await this.waitForElementToBePresentedByLocator(lastElement) + await firstElement.click(clickOptions) + } else { + const lastElement = this.page.locator(elementSelector, elementOptions).last() + await this.waitForElementToBePresentedByLocator(lastElement) + await this.page.click(elementSelector, clickOptions) + } + } + + /** + * Click web element by Locator + * @param {Locator} elementLocator + */ + async clickElementByLocator(elementLocator: Locator): Promise { + const firstElement = elementLocator.first() + await this.waitForElementToBePresentedByLocator(firstElement) + await firstElement.click() + } + + /** + * Is element visible on the page by CSS Selector with timeout + * @param {string} elementSelector + * @param {any} options + * @param {number} timeoutParam - the default value is 10sec + */ + async isElementVisibleBySelectorWithTimeout( + elementSelector: string, + options: LocatorOptions | null = null, + timeoutParam = 10000, + ): Promise { + let elementLocator: Locator + if (options) { + elementLocator = this.page.locator(elementSelector, options).first() + } else { + elementLocator = this.page.locator(elementSelector).first() + } + try { + await elementLocator.first().waitFor({ state: 'visible', timeout: timeoutParam }) + } catch (e) { + return false + } + return true + } + + /** + * Is element visible on the page by Locator with timeout + * @param {Locator} elementLocator + * @param {number} timeoutParam - the default value is 10sec + */ + async isElementVisibleByLocatorWithTimeout( + elementLocator: Locator, + timeoutParam = 10000, + ): Promise { + try { + await elementLocator.waitFor({ state: 'visible', timeout: timeoutParam }) + } catch (e) { + return false + } + return true + } + + /** + * Get text of an element by CSS Selector + * @param {string} elementSelector + * @param {any} options + */ + async getTextOfElementBySelector( + elementSelector: string, + options?: LocatorOptions, + ): Promise { + let elementLocator: Locator = this.page.locator(elementSelector) + if (options) { + elementLocator = this.page.locator(elementSelector, options) + } + await this.page.waitForTimeout(500) + await this.waitForElementToBePresentedByLocator(elementLocator) + return elementLocator.innerText() + } + + /** + * Get text of an element by Locator + * @param {Locator} elementLocator + */ + async getTextOfElementByLocator(elementLocator: Locator): Promise { + await this.page.waitForTimeout(500) + await this.waitForElementToBePresentedByLocator(elementLocator) + return elementLocator.innerText() + } + + /** + * Scroll to element center by CSS Selector + * @param {string} elementSelector + * @param {any} options + */ + async scrollToElementCenterBySelector( + elementSelector: string, + options?: LocatorOptions, + ): Promise { + let element: Locator = this.page.locator(elementSelector) + if (options) { + element = this.page.locator(elementSelector, options) + } + await this.waitForElementToBePresentedByLocator(element.first()) + await element.first().scrollIntoViewIfNeeded() + } + + /** + * Scroll to element center by Locator + * @param {Locator} elementLocator + */ + async scrollToElementCenterByLocator(elementLocator: Locator): Promise { + await this.waitForElementToBePresentedByLocator(elementLocator.first()) + await elementLocator.first().scrollIntoViewIfNeeded() + } + + /** + * Navigate to URL and wait for load state + * @param {string} url + */ + async navigateToUrl(url: string): Promise { + await this.page.goto(url) + await this.page.waitForLoadState() + } + + /** + * Get specific attribute from an element by CSS Selector + * @param {string} elementSelector + * @param {string} attributeName + * @param {any} options + */ + async getAttributeBySelector( + elementSelector: string, + attributeName: string, + options?: LocatorOptions, + ): Promise { + if (options) { + const webElement: Locator = this.page.locator(elementSelector, options) + await webElement.last().waitFor({ state: 'attached' }) + return webElement.getAttribute(attributeName) + } else { + return this.page.getAttribute(elementSelector, attributeName) + } + } + + /** + * Get specific attribute from an element by Locator + * @param {Locator} elementLocator + * @param {string} attributeName + * @param {number} timeoutParam + */ + async getAttributeByLocator( + elementLocator: Locator, + attributeName: string, + timeoutParam: number, + ): Promise { + await this.waitForElementToBePresentedByLocator(elementLocator) + return elementLocator.getAttribute(attributeName, { timeout: timeoutParam }) + } + + /** + * Clear the input field of an element by CSS Selector + * @param {string} elementSelector + */ + async clearInputFieldBySelector(elementSelector: string): Promise { + await this.page.fill(elementSelector, '') + } + + /** + * Clear the input field of an element by Locator + * @param {Locator} elementLocator + */ + async clearInputFieldByLocator(elementLocator: Locator): Promise { + await elementLocator.fill('') + } + + /** + * Set a text value into an input field by CSS Selector + * @param {string} elementSelector + * @param {string | number} inputValueToFill + * @param {boolean} clearBeforeInput - the default value is 'true' + * @param {boolean} slowTyping - the default value is 'false' + * @param {boolean} pressEnterKey - the default value is 'false' + */ + async setInputFieldBySelector( + elementSelector: string, + inputValueToFill: string | number, + clearBeforeInput = true, + slowTyping = false, + pressEnterKey = false, + ): Promise { + await this.waitForElementToBePresentedBySelector(elementSelector) + if (clearBeforeInput) { + await this.clearInputFieldBySelector(elementSelector) + } + if (slowTyping) { + await this.page.type(elementSelector, inputValueToFill.toString().trim(), { delay: 100 }) + } else { + await this.page.type(elementSelector, inputValueToFill.toString().trim()) + } + if (pressEnterKey) { + await this.page.press(elementSelector, 'Enter') + } + } + + /** + * Set a text value into an input field by Locator + * TODO - the two methods for setting input fields need to be tested, it is possible no need updates + * @param {Locator} elementSelector + * @param {string | number} inputValueToFill + * @param {boolean} clearBeforeInput - the default value is 'true' + * @param {boolean} pressEnterKey - the default value is 'false' + */ + async setInputFieldByLocator( + elementLocator: Locator, + inputValueToFill: string | number, + clearBeforeInput = true, + pressEnterKey = false, + ): Promise { + await this.waitForElementToBePresentedByLocator(elementLocator) + if (clearBeforeInput) { + await this.clearInputFieldByLocator(elementLocator) + } + await elementLocator.type(inputValueToFill.toLocaleString().trim()) + if (pressEnterKey) { + await elementLocator.press('Enter') + } + } + + /** + * Select checkbox by its label text + * @param {string} elementSelector + * @param {Array} labelText + */ + async selectCheckboxSelectorByLabelText( + elementSelector: string, + labelText: Array, + ): Promise { + await this.waitForElementToBePresentedBySelector(elementSelector) + for (const item of labelText) { + const checkboxElementLocator = this.page + .locator(elementSelector, { hasText: item }) + .locator("input[type='checkbox']") + if (await this.isElementVisibleByLocatorWithTimeout(checkboxElementLocator)) { + await checkboxElementLocator.check() + } else { + throw new Error('The checkbox element is not found!') + } + } + } + + /** + * Select radio button by its label text + * @param {string} elementSelector + * @param {Array} labelText + */ + async selectRadioButtonSelectorByLabelText( + elementSelector: string, + labelText: Array, + ): Promise { + await this.waitForElementToBePresentedBySelector(elementSelector) + for (const item of labelText) { + // Here we use Xpath because of the special symbol " " in the English version + const radioCheckboxElementLocator = this.page.locator( + "(//input[@type='radio']/ancestor::label/p[contains(text(),'" + item + "')])[1]", + ) + if (await this.isElementVisibleByLocatorWithTimeout(radioCheckboxElementLocator)) { + await radioCheckboxElementLocator.click() + } else { + throw new Error('The checkbox element is not found!') + } + } + } + + /** + * Select radio button by its label text - pass Array + * @param {Array} labelTextArray + */ + async selectRadioButtonByLabelText(labelTextArray: Array): Promise { + await this.selectRadioButtonSelectorByLabelText(this.checkboxLabelXpath, labelTextArray) + } + + /** + * Deselect checkbox by its label text + * @param {string} elementSelector + * @param {Array} labelText + */ + async deselectCheckboxSelectorByLabelText( + elementSelector: string, + labelTextArray: Array, + ): Promise { + await this.waitForElementToBePresentedBySelector(elementSelector) + for (const item of labelTextArray) { + const checkboxElementLocator = this.page + .locator(elementSelector, { hasText: item }) + .locator("input[type='checkbox']") + await checkboxElementLocator.uncheck() + } + } + + /** + * Is checkbox selected by its label text + * @param {string} elementSelector + * @param {Array} labelText + */ + async isCheckboxSelectorCheckedByLabelText( + elementSelector: string, + labelText: string, + ): Promise { + await this.waitForElementToBePresentedBySelector(elementSelector) + return this.page.locator(elementSelector, { hasText: labelText }).isChecked() + } + + /** + * Is checkbox selected by its label text - pass one string + * @param {string} labelText + */ + async isCheckboxCheckedByLabelText(labelText: string): Promise { + await this.scrollToElementCenterBySelector(this.checkboxLabelSelector, { hasText: labelText }) + return this.isCheckboxSelectorCheckedByLabelText(this.checkboxLabelSelector, labelText) + } + + /** + * Select checkbox by its label text - pass Array + * @param {Array} labelTextArray + */ + async selectCheckboxByLabelText(labelTextArray: Array): Promise { + await this.selectCheckboxSelectorByLabelText(this.checkboxLabelSelector, labelTextArray) + } + + /** + * Deselect checkbox by its label text - pass Array + * @param {Array} labelTextArray + */ + async deselectCheckboxByLabelText(labelTextArray: Array): Promise { + await this.deselectCheckboxSelectorByLabelText(this.checkboxLabelSelector, labelTextArray) + } + + /** + * Check partial or complete page URL by string + * @param {string} urlRegExpAsString + * @param {number} timeoutParam - the default value is 10sec + */ + // TODO Add poll + async checkPageUrlByRegExp(urlRegExpAsString: string, timeoutParam = 10000): Promise { + await this.page.waitForTimeout(1000) + await expect(this.page, 'The URL is not correct!').toHaveURL(new RegExp(urlRegExpAsString), { + timeout: timeoutParam, + }) + } + + /** + * Get count of elements by selector + * @param {string} elementSelector + */ + async getCountOfElementsBySelector(elementSelector: string): Promise { + await this.waitForElementToBePresentedBySelector(elementSelector) + return this.page.locator(elementSelector).count() + } + + /** + * Is Step active by text into Join Us horizontal stepper + * TODO refactor for two languages + * @param {string} roleText + */ + async isStepActiveByLabelText(roleText: string): Promise { + return this.isElementVisibleBySelectorWithTimeout(this.activeHorizontalStepSelector, { + hasText: roleText, + }) + } + + /** + * Select dropdown option value by CSS Selector + * @param {string} dropdownSelector + * @param {string | string[]} dropdownOptionStringOrArray + */ + async selectDropdownOptionValue( + dropdownSelector: string, + dropdownOptionStringOrArray: string | string[], + ): Promise { + await this.waitForElementToBeReadyBySelector(dropdownSelector, undefined, 2000) + await this.page.selectOption(dropdownSelector, dropdownOptionStringOrArray) + } +} diff --git a/e2e/pages/web-pages/campaigns/campaigns.page.ts b/e2e/pages/web-pages/campaigns/campaigns.page.ts new file mode 100644 index 000000000..a960fc15b --- /dev/null +++ b/e2e/pages/web-pages/campaigns/campaigns.page.ts @@ -0,0 +1,114 @@ +import { Page, expect } from '@playwright/test' +import { LanguagesEnum } from '../../../data/enums/languages.enum' +import { bgLocalizationCampaigns, enLocalizationCampaigns } from '../../../data/localization' +import { SLUG_REGEX } from '../../../utils/helpers' +import { HomePage } from '../home.page' + +export class CampaignsPage extends HomePage { + constructor(page: Page) { + super(page) + } + + private readonly donationGrid = '.InlineDonation-inlineDonationWrapper' + private readonly donationSupportButton = this.donationGrid + ' a button' + private readonly filterButtonsCommonSelector = 'ul button.CampaignFilter-filterButtons' + // private readonly campaignContainerItem = ".MuiGrid-container .MuiGrid-item"; + private readonly cardActions = '.MuiCardActions-root' + private readonly cardActionButtons = this.cardActions + ' button' + // Main headings + private readonly bgMainCampaignsHeading = bgLocalizationCampaigns.campaigns + private readonly enMainCampaignsHeading = enLocalizationCampaigns.campaigns + private readonly bgSupportCauseTodayHeading = bgLocalizationCampaigns.cta['support-cause-today'] + private readonly enSupportCauseTodayHeading = enLocalizationCampaigns.cta['support-cause-today'] + private readonly bgSupportNowActionButtonText = bgLocalizationCampaigns.cta['support-now'] + private readonly enSupportNowActionButtonText = enLocalizationCampaigns.cta['support-now'] + + /** + * Ovverride the method from the BasePage and add the specific selector for the Campaigns page as default + */ + async checkPageUrlByRegExp(urlRegExpAsString?: string, timeoutParam = 10000): Promise { + await this.page.waitForTimeout(1000) + await expect(this.page, 'The URL is not correct!').toHaveURL( + new RegExp(urlRegExpAsString || `^(.*?)/campaigns/${SLUG_REGEX}`), + { + timeout: timeoutParam, + }, + ) + } + + /** + * Click donation Support button into the donation grid container + */ + async clickDonationSupportButton(): Promise { + await this.clickElement(this.donationSupportButton) + } + + /** + * Get filter buttons count on the Campaigns page + */ + async getFilterButtonsCount(): Promise { + await this.waitForElementToBeReadyBySelector(this.filterButtonsCommonSelector) + return this.getCountOfElementsBySelector(this.filterButtonsCommonSelector) + } + + /** + * Check if the main "Campaigns" page H1 heading is visible on the Campaigns page + * @param {LanguagesEnum} language - the default value is BG + */ + async isCampaignsHeadingVisible(language: LanguagesEnum = LanguagesEnum.BG): Promise { + return this.isH1HeadingVisible( + language, + this.bgMainCampaignsHeading, + this.enMainCampaignsHeading, + ) + } + + /** + * Check if "Support cause today" page H6 heading is visible on the Campaigns page + * @param {LanguagesEnum} language - the default value is BG + */ + async isSupportCauseTodayHeadingVisible( + language: LanguagesEnum = LanguagesEnum.BG, + ): Promise { + return this.isH6HeadingVisible( + language, + this.bgSupportCauseTodayHeading, + this.enSupportCauseTodayHeading, + ) + } + + /** + * Click card action button by its H5 heading + * @param {string} heading + * @param {string} action + */ + async clickCampaignCardByIndex(index: number): Promise { + const cardActionButtonElement = this.page.locator(`[data-testid="campaign-card-${index}"]`) + + await this.clickElementByLocator(cardActionButtonElement) + } + + /** + * Click card action button by its H5 heading + * @param {string} heading + * @param {string} action + */ + async clickCampaignCardButtonByIndex( + index: number, + language: LanguagesEnum = LanguagesEnum.BG, + ): Promise { + let supportButtonText = '' + if (language === LanguagesEnum.BG) { + supportButtonText = this.bgSupportNowActionButtonText + } else if (language === LanguagesEnum.EN) { + supportButtonText = this.enSupportNowActionButtonText + } else { + throw new Error('Invalid language!') + } + const cardActionButtonElement = this.page + .locator(`[data-testid="campaign-card-${index}"]`) + .locator('../../..') + .locator(this.cardActionButtons, { hasText: supportButtonText }) + await this.clickElementByLocator(cardActionButtonElement) + } +} diff --git a/e2e/pages/web-pages/campaigns/donation.page.ts b/e2e/pages/web-pages/campaigns/donation.page.ts new file mode 100644 index 000000000..33261ab03 --- /dev/null +++ b/e2e/pages/web-pages/campaigns/donation.page.ts @@ -0,0 +1,245 @@ +import { Page, expect } from '@playwright/test' +import { DonationRegions } from '../../../data/enums/donation-regions.enum' +import { LanguagesEnum } from '../../../data/enums/languages.enum' +import { + bgLocalizationOneTimeDonation, + enLocalizationOneTimeDonation, +} from '../../../data/localization' +import { SLUG_REGEX } from '../../../utils/helpers' +import { CampaignsPage } from './campaigns.page' + +export class DonationPage extends CampaignsPage { + constructor(page: Page) { + super(page) + } + + // -> Select amount section <- + private readonly otherAmountInputField = ".MuiCollapse-entered input[name='otherAmount']" + private readonly allAmountsSelector = '.MuiBox-root strong' + private readonly regionsDropdownRootElement = '.MuiInputBase-root .MuiSelect-select' + private readonly regionsMenuList = '#menu-cardRegion ul.MuiMenu-list li' + private readonly gridRootSelector = '.MuiGrid-root' + private readonly forwardGridButton = this.gridRootSelector + ' button.MuiButton-contained' + // Section labels + private readonly bgSelectAmountSectionText = bgLocalizationOneTimeDonation['step-labels'].amount + private readonly enSelectAmountSectionText = enLocalizationOneTimeDonation['step-labels'].amount + // TODO Add these three IDs into the component (if possible) and update the test methods + private readonly donationAmount = this.allAmountsSelector + ' #donationAmount' + private readonly feeAmount = this.allAmountsSelector + ' #feeAmount' + private readonly totalChargedAmount = this.allAmountsSelector + ' #totalChargedAmount' + // Grid navigation buttons localization + private readonly bgForwardButtonText = bgLocalizationOneTimeDonation.btns.next + private readonly enForwardButtonText = enLocalizationOneTimeDonation.btns.next + + // -> Personal profile section <- + private readonly buttonsContainer = '.MuiTabs-flexContainer button' + private readonly bgDonateAnonymouslyText = + bgLocalizationOneTimeDonation['second-step']['donate-anonymously'] + private readonly enDonateAnonymouslyText = + enLocalizationOneTimeDonation['second-step']['donate-anonymously'] + private readonly inputRootSelector = '.MuiInputBase-root' + private readonly donateAnonymouslyEmailField = + this.inputRootSelector + " input[name='personsEmail']" + // Section labels + private readonly bgPersonalProfileSectionText = + bgLocalizationOneTimeDonation['step-labels']['personal-profile'] + private readonly enPersonalProfileSectionText = + enLocalizationOneTimeDonation['step-labels']['personal-profile'] + + // -> Send a wish section <- + // Section labels + private readonly sendAWishField = this.inputRootSelector + " textarea[name='message']" + private readonly bgSendAWishSectionText = bgLocalizationOneTimeDonation['step-labels'].wish + private readonly enSendAWishSectionText = enLocalizationOneTimeDonation['step-labels'].wish + + // -> Payment <- + // Section labels + private readonly bgPaymentSectionText = bgLocalizationOneTimeDonation['step-labels'].payment + private readonly enPaymentSectionText = enLocalizationOneTimeDonation['step-labels'].payment + private readonly bgFinishButtonText = bgLocalizationOneTimeDonation.btns.end + private readonly enFinishButtonText = enLocalizationOneTimeDonation.btns.end + private readonly bgSuccessfulDonationTitle = bgLocalizationOneTimeDonation.success.title + private readonly enSuccessfulDonationTitle = enLocalizationOneTimeDonation.success.title + + async checkPageUrlByRegExp(urlRegExpAsString?: string, timeoutParam = 10000): Promise { + await this.page.waitForTimeout(1000) + await expect(this.page, 'The URL is not correct!').toHaveURL( + new RegExp(urlRegExpAsString || `^(.*?)/campaigns/donation/${SLUG_REGEX}`), + { + timeout: timeoutParam, + }, + ) + } + + /** + * Is "Select amount" step active + */ + async isSelectAmountStepActive(language: LanguagesEnum = LanguagesEnum.BG): Promise { + if (language === LanguagesEnum.BG) { + return this.isStepActiveByLabelText(this.bgSelectAmountSectionText) + } else if (language === LanguagesEnum.EN) { + return this.isStepActiveByLabelText(this.enSelectAmountSectionText) + } else { + throw new Error('Language not found!') + } + } + + /** + * Is "Personal profile" step active + */ + async isPersonalProfileStepActive(language: LanguagesEnum = LanguagesEnum.BG): Promise { + if (language === LanguagesEnum.BG) { + return this.isStepActiveByLabelText(this.bgPersonalProfileSectionText) + } else if (language === LanguagesEnum.EN) { + return this.isStepActiveByLabelText(this.enPersonalProfileSectionText) + } else { + throw new Error('Language not found!') + } + } + + /** + * Is "Send a wish" step active + */ + async isSendAWishStepActive(language: LanguagesEnum = LanguagesEnum.BG): Promise { + if (language === LanguagesEnum.BG) { + return this.isStepActiveByLabelText(this.bgSendAWishSectionText) + } else if (language === LanguagesEnum.EN) { + return this.isStepActiveByLabelText(this.enSendAWishSectionText) + } else { + throw new Error('Language not found!') + } + } + + /** + * Is "Payment" step active + */ + async isPaymentStepActive(language: LanguagesEnum = LanguagesEnum.BG): Promise { + if (language === LanguagesEnum.BG) { + return this.isStepActiveByLabelText(this.bgPaymentSectionText) + } else if (language === LanguagesEnum.EN) { + return this.isStepActiveByLabelText(this.enPaymentSectionText) + } else { + throw new Error('Language not found!') + } + } + + /** + * Fill in the desired amount of money for donation into the Other Amount input field + * @param {string} amountMoney + */ + async fillOtherAmountInputField(amountMoney: string): Promise { + await this.waitForElementToBePresentedBySelector(this.otherAmountInputField) + await this.setInputFieldBySelector(this.otherAmountInputField, amountMoney) + } + + /** + * Set donation region from the dropdown menu + * @param {string} desiredRegion + */ + async setDonationRegionFromTheDropdown(desiredRegion: DonationRegions): Promise { + await this.clickElement(this.regionsDropdownRootElement) + await this.clickElement(this.regionsMenuList + `[data-value=${desiredRegion}]`) + } + + /** + * Get Total charged amounts as text + */ + async getTotalChargedAmountsAsText(): Promise { + const donationAmount = this.page.locator(this.allAmountsSelector).nth(0) + return this.getTextOfElementByLocator(donationAmount) + // TODO Uncomment when the IDs are added + // return this.getTextOfElementBySelector(this.totalChargedAmount); + } + + /** + * Get Fee amounts as text + */ + async getFeeAmountsAsText(): Promise { + const donationAmount = this.page.locator(this.allAmountsSelector).nth(1) + return this.getTextOfElementByLocator(donationAmount) + // TODO Uncomment when the IDs are added + // return this.getTextOfElementBySelector(this.feeAmount); + } + + /** + * Get Donation amounts as text + */ + async getDonationAmountsAsText(): Promise { + const donationAmount = this.page.locator(this.allAmountsSelector).nth(2) + return this.getTextOfElementByLocator(donationAmount) + // TODO Uncomment when the IDs are added + // return this.getTextOfElementBySelector(this.donationAmount); + } + + /** + * Click Forward/Next button into the donation grid + * @param {LanguagesEnum} language + */ + async clickForwardButton(language: LanguagesEnum = LanguagesEnum.BG): Promise { + if (language === LanguagesEnum.BG) { + await this.clickElement(this.forwardGridButton, { hasText: this.bgForwardButtonText }) + } else if (language === LanguagesEnum.EN) { + await this.clickElement(this.forwardGridButton, { hasText: this.enForwardButtonText }) + } else { + throw new Error('Language not found!') + } + } + + /** + * Click Finish/Go to payment button into the donation grid + * @param {LanguagesEnum} language + */ + async clickFinishButton(language: LanguagesEnum = LanguagesEnum.BG): Promise { + if (language === LanguagesEnum.BG) { + await this.clickElement(this.forwardGridButton, { hasText: this.bgFinishButtonText }) + } else if (language === LanguagesEnum.EN) { + await this.clickElement(this.forwardGridButton, { hasText: this.enFinishButtonText }) + } else { + throw new Error('Language not found!') + } + } + + /** + * Click Donate Anonymously button into the donation grid (Personal Profile step) + * @param {LanguagesEnum} language + */ + async clickDonateAnonymouslyButton(language: LanguagesEnum = LanguagesEnum.BG): Promise { + if (language === LanguagesEnum.BG) { + await this.clickElement(this.buttonsContainer, { hasText: this.bgDonateAnonymouslyText }) + } else if (language === LanguagesEnum.EN) { + await this.clickElement(this.buttonsContainer, { hasText: this.enDonateAnonymouslyText }) + } else { + throw new Error('Language not found!') + } + } + + /** + * Fill Donate anonymously E-mail input field + * @param {string} emailText + */ + async fillDonateAnonymouslyEmailField(emailText: string): Promise { + await this.setInputFieldBySelector(this.donateAnonymouslyEmailField, emailText) + } + + /** + * Fill Send a wish input field + * @param {string} wishText + */ + async fillSendAWishField(wishText: string): Promise { + await this.setInputFieldBySelector(this.sendAWishField, wishText) + } + + /** + * Is "We thank you for your help and trust!" title visible + * @param {LanguagesEnum} language - the default value is BG + */ + async isSuccessfulDonationTitleVisible( + language: LanguagesEnum = LanguagesEnum.BG, + ): Promise { + return this.isH4HeadingVisible( + language, + this.bgSuccessfulDonationTitle, + this.enSuccessfulDonationTitle, + ) + } +} diff --git a/e2e/pages/web-pages/external/stripe-checkout.page.ts b/e2e/pages/web-pages/external/stripe-checkout.page.ts new file mode 100644 index 000000000..8505acbea --- /dev/null +++ b/e2e/pages/web-pages/external/stripe-checkout.page.ts @@ -0,0 +1,54 @@ +import { Page } from '@playwright/test' +import { BasePage } from '../base.page' + +export class StripeCheckoutPage extends BasePage { + constructor(page: Page) { + super(page) + } + + private readonly productSummaryTotalAmount = '#ProductSummary-totalAmount span' + private readonly checkoutPaymentForm = '.CheckoutPaymentForm' + private readonly emailReadonlyInputField = this.checkoutPaymentForm + ' .ReadOnlyFormField-title' + private readonly cardNumberFieldSet = this.checkoutPaymentForm + ' #cardNumber-fieldset' + private readonly cardNumberInputField = this.cardNumberFieldSet + ' #cardNumber' + private readonly cardExpDateInputField = this.cardNumberFieldSet + ' #cardExpiry' + private readonly cardCvcInputField = this.cardNumberFieldSet + ' #cardCvc' + private readonly billingNameInputField = this.checkoutPaymentForm + ' #billingName' + private readonly billingCountryDropdown = this.checkoutPaymentForm + ' #billingCountry' + private readonly submitPayButton = this.checkoutPaymentForm + ' button.SubmitButton' + + /** + * Get the total amount of the donation as text + */ + async getTotalAmountText(): Promise { + return this.getTextOfElementBySelector(this.productSummaryTotalAmount) + } + + /** + * Get readonly e-mail text + */ + async getReadonlyEmailText(): Promise { + return this.getTextOfElementBySelector(this.emailReadonlyInputField) + } + + /** + * Click Pay/Submit button + * @param {LanguagesEnum} language + */ + async clickPayButton(): Promise { + await this.clickElement(this.submitPayButton) + } + + /** + * Fill payment form all input fields + * @param {Array} paymentData + */ + async fillPaymentForm(paymentData: Array): Promise { + await this.setInputFieldBySelector(this.cardNumberInputField, paymentData[0], true, true) + await this.setInputFieldBySelector(this.cardExpDateInputField, paymentData[1], true, true) + await this.setInputFieldBySelector(this.cardCvcInputField, paymentData[2], true, true) + await this.setInputFieldBySelector(this.billingNameInputField, paymentData[3], true, true) + await this.selectDropdownOptionValue(this.billingCountryDropdown, paymentData[4]) + await this.clickPayButton() + } +} diff --git a/e2e/pages/web-pages/header.page.ts b/e2e/pages/web-pages/header.page.ts new file mode 100644 index 000000000..29e68ae8a --- /dev/null +++ b/e2e/pages/web-pages/header.page.ts @@ -0,0 +1,136 @@ +import { Page } from '@playwright/test' +import { BasePage } from '../web-pages/base.page' +import { bgLocalizationCommon, enLocalizationCommon } from '../../data/localization' +import { LanguagesEnum } from '../../data/enums/languages.enum' + +export class HeaderPage extends BasePage { + constructor(page: Page) { + super(page) + } + + private readonly muiToolbarRootSelector = '.MuiToolbar-root' + private readonly headerLogo = this.muiToolbarRootSelector + ' .PodkrepiLogo-letters' + private readonly toolbarCommonButtonsSelector = this.muiToolbarRootSelector + ' button' + private readonly toolbarAnchorButtonSelector = this.muiToolbarRootSelector + ' a .MuiButton-root' + private readonly headerSubmenuLinks = '.MuiMenu-list a span' + + // Values from the localization json file + private readonly bgDonateNavLink = bgLocalizationCommon.nav.donate + private readonly enDonateNavLink = enLocalizationCommon.nav.donate + private readonly bgCampaignsNavLink = bgLocalizationCommon.nav.campaigns.index + private readonly enCampaignsNavLink = enLocalizationCommon.nav.campaigns.index + private readonly bgAboutUsNavLink = bgLocalizationCommon.nav.about['about-us'] + private readonly enAboutUsNavLink = enLocalizationCommon.nav.about['about-us'] + // Header submenu options + private readonly bgAllCampaignsNavLink = bgLocalizationCommon.nav.campaigns['all-campaigns'] + private readonly enAllCampaignsNavLink = enLocalizationCommon.nav.campaigns['all-campaigns'] + // Koi sme nie / Who are we + private readonly bgWhoAreWeNavLink = bgLocalizationCommon.nav.about['who-are-we'] + private readonly enWhoAreWeNavLink = enLocalizationCommon.nav.about['who-are-we'] + // Stanete dobrovolec / Join us + private readonly bgJoinUsNavLink = bgLocalizationCommon.nav.about['support-us'] + private readonly enJoinUsNavLink = enLocalizationCommon.nav.about['support-us'] + + /** + * Click on the header icon Podkrepi.bg + */ + async clickHeaderIcon(): Promise { + await this.clickElement(this.headerLogo) + } + + /** + * Click on the main header navigation link by text + * @param {string} navTextBg + * @param {string} navTextEn + * @param {LanguagesEnum} language + */ + async clickHeaderNavLink( + navTextBg: string, + navTextEn: string, + language: LanguagesEnum, + ): Promise { + await this.waitForElementToBePresentedBySelector(this.toolbarCommonButtonsSelector) + if (language === LanguagesEnum.BG) { + await this.clickElement(this.toolbarCommonButtonsSelector, { hasText: navTextBg }) + } else if (language === LanguagesEnum.EN) { + await this.clickElement(this.toolbarCommonButtonsSelector, { hasText: navTextEn }) + } else { + throw new Error("Invalid language selection. Please, check 'languages.enum.ts'.") + } + } + + /** + * Click on the header submenu navigation link by text + * @param {LanguagesEnum} language + * @param {string} navTextBg + * @param {string} navTextEn + */ + async clickHeaderSubmenuNavLink( + language: LanguagesEnum, + navTextBg: string, + navTextEn: string, + ): Promise { + if (language === LanguagesEnum.BG) { + await this.waitForElementToBeReadyBySelector(this.headerSubmenuLinks, { hasText: navTextBg }) + await this.clickElement(this.headerSubmenuLinks, { hasText: navTextBg }) + } else if (language === LanguagesEnum.EN) { + await this.waitForElementToBeReadyBySelector(this.headerSubmenuLinks, { hasText: navTextEn }) + await this.clickElement(this.headerSubmenuLinks, { hasText: navTextEn }) + } else { + throw new Error("Invalid language selection. Please, check 'languages.enum.ts'.") + } + } + + /** + * Click on the header navigation button 'Donate' + * @param {LanguagesEnum} language - the default is BG + */ + async clickDonateHeaderNavButton(language: LanguagesEnum = LanguagesEnum.BG): Promise { + await this.waitForElementToBePresentedBySelector(this.toolbarAnchorButtonSelector) + if (language === LanguagesEnum.BG) { + await this.clickElement(this.toolbarAnchorButtonSelector, { hasText: this.bgDonateNavLink }) + } else if (language === LanguagesEnum.EN) { + await this.clickElement(this.toolbarAnchorButtonSelector, { hasText: this.enDonateNavLink }) + } else { + throw new Error("Invalid language selection. Please, check 'languages.enum.ts'.") + } + } + + /** + * Click on the header navigation button 'Campaigns' + * @param {LanguagesEnum} language - the default is BG + */ + async clickCampaignsHeaderNavButton(language: LanguagesEnum = LanguagesEnum.BG): Promise { + await this.clickHeaderNavLink(this.bgCampaignsNavLink, this.enCampaignsNavLink, language) + await this.clickHeaderSubmenuNavLink( + language, + this.bgAllCampaignsNavLink, + this.enAllCampaignsNavLink, + ) + } + + /** + * Click on the header navigation button 'About Us' + * @param {LanguagesEnum} language - the default is BG + */ + async clickAboutUsHeaderNavButton(language: LanguagesEnum = LanguagesEnum.BG): Promise { + await this.clickHeaderNavLink(this.bgAboutUsNavLink, this.enAboutUsNavLink, language) + } + + /** + * Click on the header navigation button 'Join Us' + * @param {LanguagesEnum} language - the default is BG + */ + async clickJoinUsHeaderNavButton(language: LanguagesEnum = LanguagesEnum.BG): Promise { + await this.clickAboutUsHeaderNavButton(language) + await this.clickHeaderSubmenuNavLink(language, this.bgJoinUsNavLink, this.enJoinUsNavLink) + } + + /** + * Click on the language header button to change the page language + * @param {LanguagesEnum} languageParam + */ + async changeanguageHeaderButtonToBe(languageParam: LanguagesEnum): Promise { + await this.clickHeaderNavLink(LanguagesEnum.BG, LanguagesEnum.EN, languageParam) + } +} diff --git a/e2e/pages/web-pages/home.page.ts b/e2e/pages/web-pages/home.page.ts new file mode 100644 index 000000000..1eaa68ffa --- /dev/null +++ b/e2e/pages/web-pages/home.page.ts @@ -0,0 +1,294 @@ +import { expect, Page } from '@playwright/test' +import { LanguagesEnum } from '../../data/enums/languages.enum' +import { + bgLocalizationCommon, + bgLocalizationIndex, + enLocalizationCommon, + enLocalizationIndex, +} from '../../data/localization' +import { BasePage } from '../web-pages/base.page' + +export class HomePage extends BasePage { + constructor(page: Page) { + super(page) + } + + private readonly containerRootElement = '.MuiContainer-root' + private readonly h1HeadingsSelector = '.MuiTypography-h1' + private readonly h4HeadingsSelector = '.MuiTypography-h4' + protected readonly h5HeadingsSelector = '.MuiTypography-h5' + private readonly h6HeadingsSelector = '.MuiTypography-h6' + private readonly h6FaqListHeadingItems = '.MuiListItemText-root h6' + private readonly h6FaqListAnswerItems = '.MuiCollapse-entered h6.MuiTypography-root' + + // Pair values from the localization json file + // How does Podkrepi work + private readonly bgHowDoesPodkrepiWork = bgLocalizationIndex['how-we-work'].heading + private readonly enHowDoesPodkrepiWork = enLocalizationIndex['how-we-work'].heading + // Who is behind Podkrepi.bg navigation + private readonly bgTeamSection = bgLocalizationIndex['team-section'].heading + private readonly enTeamSection = enLocalizationIndex['team-section'].heading + // Join Podkrepi.bg navigation + private readonly bgJoinPodkrepiSection = bgLocalizationIndex['join-podkrepi-bg-section'].heading + private readonly enJoinPodkrepiSection = enLocalizationIndex['join-podkrepi-bg-section'].heading + // FAQ navigation + private readonly bgFaqSection = bgLocalizationCommon.nav.campaigns.faq + private readonly enFaqSection = enLocalizationCommon.nav.campaigns.faq + + // FAQ list questions (currently exists only in BG) + // TODO Reuse the values from COMMON_QUESTIONS - src\components\faq\contents\common.tsx + private readonly h6BgWhatIsPodkrepiText = 'Какво е Подкрепи.бг?' + // private readonly h6BgWhatIsTransparecyText = "Какво е „безкомпромисна прозрачност”?"; + // private readonly h6BgWhatAreOurAdvantagesText = "Какви са технологичните ви предимства?"; + // private readonly h6BgWhatAreSustainableSolutionsText = "Какво представляват „устойчивите решения”?"; + // private readonly h6BgHowWeAreFundedText = "Как се финансира Подкрепи.бг?"; + + // FAQ list answers (currently exists only in BG) + private readonly h6BgWhatIsPodkrepiAnswer = + 'Ние сме общност от доброволци, обединени от идеята да създаваме устойчиви решения за развитието на дарителството в България.' + + /** + * Navigate to the Dev test environment homepage + * NOTE: We could use this method for direct tests against the Dev environment + */ + async navigateToEnvHomepage(): Promise { + //Navigating to the homeapage based on the baseUrl from the config file + await this.navigateToUrl('/') + } + + /** + * Check if Heading is visible by CSS Selector and text with timeout + * @param {string} elementSelector + * @param {LanguagesEnum} language + * @param {string} headingBg + * @param {string | null} headingEn + */ + async isHeadingVisibleBySelector( + elementSelector: string, + language: LanguagesEnum, + headingBg: string, + headingEn: string | null, + ): Promise { + await this.waitForElementToBePresentedByLocator(this.page.locator(elementSelector).first()) + if (language === LanguagesEnum.BG) { + return this.isElementVisibleBySelectorWithTimeout(elementSelector, { hasText: headingBg }) + } else if (language === LanguagesEnum.EN) { + return this.isElementVisibleBySelectorWithTimeout(elementSelector, { + hasText: headingEn || undefined, + }) + } else { + throw new Error('Language not found!') + } + } + + /** + * Check if H1 heading is visible by text with timeout + * @param {LanguagesEnum} language + * @param {string} headingBg + * @param {string | null} headingEn + */ + async isH1HeadingVisible( + language: LanguagesEnum, + headingBg: string, + headingEn: string | null, + ): Promise { + return this.isHeadingVisibleBySelector(this.h1HeadingsSelector, language, headingBg, headingEn) + } + + /** + * Check if H4 heading is visible by text with timeout + * @param {LanguagesEnum} language + * @param {string} headingBg + * @param {string | null} headingEn + */ + async isH4HeadingVisible( + language: LanguagesEnum, + headingBg: string, + headingEn: string | null, + ): Promise { + return this.isHeadingVisibleBySelector(this.h4HeadingsSelector, language, headingBg, headingEn) + } + + /** + * Check if H5 heading is visible by text with timeout + * @param {LanguagesEnum} language + * @param {string} headingBg + * @param {string | null} headingEn + */ + async isH5HeadingVisible( + language: LanguagesEnum, + headingBg: string, + headingEn: string | null, + ): Promise { + return this.isHeadingVisibleBySelector(this.h5HeadingsSelector, language, headingBg, headingEn) + } + + /** + * Check if H6 heading is visible by text with timeout + * @param {LanguagesEnum} language + * @param {string} headingBg + * @param {string | null} headingEn + */ + async isH6HeadingVisible( + language: LanguagesEnum, + headingBg: string, + headingEn: string | null, + ): Promise { + return this.isHeadingVisibleBySelector(this.h6HeadingsSelector, language, headingBg, headingEn) + } + + /** + * Check if H6 homepage FAQ heading is visible with timeout + * @param {LanguagesEnum} language + * @param {string} headingBg + * @param {string} headingEn - currently English version is not implemented + */ + async isHomeH6FaqQuestionVisible( + language: LanguagesEnum, + headingBg: string, + headingEn?: string, + ): Promise { + await this.waitForElementToBePresentedByLocator( + this.page.locator(this.containerRootElement).first(), + ) + if (language === LanguagesEnum.BG) { + const h6ListHeadingItemBg = this.page.locator(this.h6FaqListHeadingItems, { + hasText: headingBg, + }) + await this.scrollToElementCenterByLocator(h6ListHeadingItemBg) + return this.isElementVisibleByLocatorWithTimeout(h6ListHeadingItemBg) + } else if (language === LanguagesEnum.EN) { + const h6ListHeadingItemEn = this.page.locator(this.h6FaqListHeadingItems, { + hasText: headingEn, + }) + await this.scrollToElementCenterByLocator(h6ListHeadingItemEn) + return this.isElementVisibleByLocatorWithTimeout(h6ListHeadingItemEn) + } else { + throw new Error('Language not found!') + } + } + + /** + * Get text of a H6 homepage FAQ answer + * @param {LanguagesEnum} language + */ + async getTextOfHomeH6FaqAnswer(language: LanguagesEnum): Promise { + if (language === LanguagesEnum.BG) { + return await this.getTextOfElementBySelector(this.h6FaqListAnswerItems) + } else if (language === LanguagesEnum.EN) { + return await this.getTextOfElementBySelector(this.h6FaqListAnswerItems) + } else { + throw new Error('Language not found!') + } + } + + /** + * Click H5 heading by text + * @param {LanguagesEnum} language + * @param {string} headingBg + * @param {string | null} headingEn + */ + async clickH5HeadingByText( + language: LanguagesEnum = LanguagesEnum.BG, + headingBg: string, + headingEn: string | null, + ): Promise { + if (await this.isH5HeadingVisible(language, headingBg, headingEn)) { + if (language === LanguagesEnum.BG) { + await this.scrollToElementCenterBySelector(this.h5HeadingsSelector, { hasText: headingBg }) + await this.clickElement(this.h5HeadingsSelector, { hasText: headingBg }) + } else if (language === LanguagesEnum.EN) { + await this.scrollToElementCenterBySelector(this.h5HeadingsSelector, { + hasText: headingEn || undefined, + }) + await this.clickElement(this.h5HeadingsSelector, { hasText: headingEn || undefined }) + } else { + throw new Error('Language not found!') + } + } else { + throw new Error('H5 header is not visible.') + } + } + + /** + * Click H6 homepage FAQ heading from the list by text + * @param {LanguagesEnum} language + * @param {string} headingBg + * @param {string} headingEn + */ + async clickHomeH6FaqHeadingByText( + language: LanguagesEnum, + headingBg: string, + headingEn?: string, + ): Promise { + if (await this.isHomeH6FaqQuestionVisible(language, headingBg, headingEn)) { + if (language === LanguagesEnum.BG) { + await this.scrollToElementCenterBySelector(this.h6FaqListHeadingItems, { + hasText: headingBg, + }) + await this.clickElement(this.h6FaqListHeadingItems, { hasText: headingBg }) + } else if (language === LanguagesEnum.EN) { + await this.scrollToElementCenterBySelector(this.h6FaqListHeadingItems, { + hasText: headingEn, + }) + await this.clickElement(this.h6FaqListHeadingItems, { hasText: headingEn }) + } else { + throw new Error('Language not found!') + } + } else { + throw new Error('FAQ from the list not found.') + } + } + + /** + * Check if "How we work" heading is visible with timeout + * @param {LanguagesEnum} language - the default value is BG + */ + async isHowWeWorkHeadingVisible(language: LanguagesEnum = LanguagesEnum.BG): Promise { + return this.isH4HeadingVisible(language, this.bgHowDoesPodkrepiWork, this.enHowDoesPodkrepiWork) + } + + /** + * Check if "Who is behind Podkrepi" heading is visible with timeout + * @param {LanguagesEnum} language - the default value is BG + */ + async isTeamSectionHeadingVisible(language: LanguagesEnum = LanguagesEnum.BG): Promise { + return this.isH4HeadingVisible(language, this.bgTeamSection, this.enTeamSection) + } + + /** + * Check if "Join Podkrepi" heading is visible with timeout + * @param {LanguagesEnum} language - the default value is BG + */ + async isJoinPodkrepiSectionHeadingVisible( + language: LanguagesEnum = LanguagesEnum.BG, + ): Promise { + return this.isH4HeadingVisible(language, this.bgJoinPodkrepiSection, this.enJoinPodkrepiSection) + } + + /** + * Check if "FAQ" heading is visible with timeout + * @param {LanguagesEnum} language - the default value is BG + */ + async isFaqSectionHeadingVisible(language: LanguagesEnum = LanguagesEnum.BG): Promise { + return this.isH4HeadingVisible(language, this.bgFaqSection, this.enFaqSection) + } + + /** + * Click "What is Podkrepi" H6 FAQ from the list + * @param {LanguagesEnum} language - the default value is BG + */ + async clickWhatIsPodkrepiFaqListQuestion( + language: LanguagesEnum = LanguagesEnum.BG, + ): Promise { + await this.clickHomeH6FaqHeadingByText(language, this.h6BgWhatIsPodkrepiText) + } + + /** + * Check if "What is Podkrepi" FAQ list item is visible with timeout + */ + async isPodkrepiFaqListAnswerVisible(language: LanguagesEnum = LanguagesEnum.BG): Promise { + const faqAnswerText = await this.getTextOfHomeH6FaqAnswer(language) + expect(faqAnswerText).toContain(this.h6BgWhatIsPodkrepiAnswer) + } +} diff --git a/e2e/pages/web-pages/support.page.ts b/e2e/pages/web-pages/support.page.ts new file mode 100644 index 000000000..8cf850c99 --- /dev/null +++ b/e2e/pages/web-pages/support.page.ts @@ -0,0 +1,192 @@ +import { Page } from '@playwright/test' +import { LanguagesEnum } from '../../data/enums/languages.enum' +import { + bgLocalizationSupport, + bgLocalizationValidation, + enLocalizationSupport, +} from '../../data/localization' +import { HomePage } from './home.page' + +export class SupportPage extends HomePage { + constructor(page: Page) { + super(page) + } + + private readonly submitNavButton = '.MuiGrid-root button[type=submit]' + private readonly backNavButton = '.MuiGrid-root button[type=button]' + private readonly partnerOtherRoleInputField = "input[name='partner.otherText']" + private readonly companyOtherRoleInputField = "input[name='company.otherText']" + private readonly contactFormFirstNameInputField = "input[name='person.firstName']" + private readonly contactFormLastNameInputField = "input[name='person.lastName']" + private readonly contactFormEmailInputField = "input[name='person.email']" + private readonly contactFormPhoneInputField = "input[name='person.phone']" + private readonly contactFormCommentInputField = "textarea[name='person.comment']" + private readonly agreeWithTerms = bgLocalizationValidation['agree-with'] + private readonly understandTerms = bgLocalizationValidation['informed-agree-with'] + private readonly bgThankYouForSupportH4 = bgLocalizationSupport.steps['thank-you'].content + private readonly enThankYouForSupportH4 = enLocalizationSupport.steps['thank-you'].content + private readonly bgSendSubmitButton = bgLocalizationSupport.cta.submit + private readonly bgSubmitButton = bgLocalizationSupport.cta.next + private readonly enSubmitButton = enLocalizationSupport.cta.submit + private readonly bgBackButton = bgLocalizationSupport.cta.back + private readonly enBackButton = enLocalizationSupport.cta.back + // Step Additional Questions + private readonly bgAdditionalQuestionsStepText = + bgLocalizationSupport.steps['addition-questions'].title + private readonly enAdditionalQuestionsStepText = + enLocalizationSupport.steps['addition-questions'].title + // Step Contact Data + private readonly bgContactDataStepText = bgLocalizationSupport.steps.info.title + private readonly enContactDataStepText = enLocalizationSupport.steps.info.title + // Step Participation + private readonly bgParticipationStepText = bgLocalizationSupport.steps['thank-you'].title + private readonly enParticipationStepText = enLocalizationSupport.steps['thank-you'].title + + /** + * Click on the Submit/Napred button + * @param {LanguagesEnum} language + */ + async clickSubmitButton(language: LanguagesEnum = LanguagesEnum.BG): Promise { + await this.waitForElementToBePresentedBySelector(this.submitNavButton) + if (language === LanguagesEnum.BG) { + await this.clickElement(this.submitNavButton, { hasText: this.bgSubmitButton }) + } else if (language === LanguagesEnum.EN) { + await this.clickElement(this.submitNavButton, { hasText: this.enSubmitButton }) + } else { + throw new Error("Invalid language selection. Please, check 'languages.enum.ts'.") + } + } + + /** + * Click on the Submit/Izpratete button + * @param {LanguagesEnum} language + */ + async clickSendSubmitButton(language: LanguagesEnum = LanguagesEnum.BG): Promise { + await this.waitForElementToBePresentedBySelector(this.submitNavButton) + if (language === LanguagesEnum.BG) { + await this.clickElement(this.submitNavButton, { hasText: this.bgSendSubmitButton }) + } else if (language === LanguagesEnum.EN) { + await this.clickElement(this.submitNavButton, { hasText: this.enSubmitButton }) + } else { + throw new Error("Invalid language selection. Please, check 'languages.enum.ts'.") + } + } + + /** + * Click on the Back nav button + * @param {LanguagesEnum} language + */ + async clickBackButton(language: LanguagesEnum = LanguagesEnum.BG): Promise { + await this.waitForElementToBePresentedBySelector(this.backNavButton) + if (language === LanguagesEnum.BG) { + await this.scrollToElementCenterBySelector(this.backNavButton, { hasText: this.bgBackButton }) + await this.clickElement(this.backNavButton, { hasText: this.bgBackButton }) + } else if (language === LanguagesEnum.EN) { + await this.scrollToElementCenterBySelector(this.backNavButton, { hasText: this.enBackButton }) + await this.clickElement(this.backNavButton, { hasText: this.enBackButton }) + } else { + throw new Error("Invalid language selection. Please, check 'languages.enum.ts'.") + } + } + + /** + * Set other partner role through the input field + * @param {string} otherPartnerRoleText + */ + async setOtherPartnerRoleInputField(otherPartnerRoleText: string): Promise { + await this.setInputFieldBySelector(this.partnerOtherRoleInputField, otherPartnerRoleText) + } + + /** + * Get other partner role input field text + * @param {string} otherPartnerRoleText + */ + async getOtherPartnerRoleInputField(): Promise { + return this.getTextOfElementBySelector(this.partnerOtherRoleInputField) + } + + /** + * Set other company through the input field + * @param {string} otherCompanyText + */ + async setOtherCompanyInputField(otherCompanyText: string): Promise { + await this.setInputFieldBySelector(this.companyOtherRoleInputField, otherCompanyText) + } + + /** + * Get other company input field text + * @param {string} otherPartnerRoleText + */ + async getOtherCompanyInputField(): Promise { + return this.getTextOfElementBySelector(this.companyOtherRoleInputField) + } + + /** + * Fill contact form all input fields + * @param {Array} volunteerData + */ + async fillContactForm(volunteerData: Array): Promise { + await this.setInputFieldBySelector(this.contactFormFirstNameInputField, volunteerData[0]) + await this.setInputFieldBySelector(this.contactFormLastNameInputField, volunteerData[1]) + await this.setInputFieldBySelector(this.contactFormEmailInputField, volunteerData[2]) + await this.setInputFieldBySelector(this.contactFormPhoneInputField, volunteerData[3]) + await this.setInputFieldBySelector(this.contactFormCommentInputField, volunteerData[4]) + await this.selectCheckboxByLabelText([this.agreeWithTerms, this.understandTerms]) + await this.clickSendSubmitButton() + } + + /** + * Is Thank You for your support H4 heading visible + * @param {string} language + */ + async isThankYouSupportH4HeadingVisible( + language: LanguagesEnum = LanguagesEnum.BG, + ): Promise { + return this.isH4HeadingVisible( + language, + this.bgThankYouForSupportH4, + this.enThankYouForSupportH4, + ) + } + + /** + * Is "Additional Questions" step active + */ + async isAdditionalQuestionsStepActive( + language: LanguagesEnum = LanguagesEnum.BG, + ): Promise { + if (language === LanguagesEnum.BG) { + return this.isStepActiveByLabelText(this.bgAdditionalQuestionsStepText) + } else if (language === LanguagesEnum.EN) { + return this.isStepActiveByLabelText(this.enAdditionalQuestionsStepText) + } else { + throw new Error('Language not found!') + } + } + + /** + * Is "Contact Data" step active + */ + async isContactDataStepActive(language: LanguagesEnum = LanguagesEnum.BG): Promise { + if (language === LanguagesEnum.BG) { + return this.isStepActiveByLabelText(this.bgContactDataStepText) + } else if (language === LanguagesEnum.EN) { + return this.isStepActiveByLabelText(this.enContactDataStepText) + } else { + throw new Error('Language not found!') + } + } + + /** + * Is "Participation" step active + */ + async isParticipationStepActive(language: LanguagesEnum = LanguagesEnum.BG): Promise { + if (language === LanguagesEnum.BG) { + return this.isStepActiveByLabelText(this.bgParticipationStepText) + } else if (language === LanguagesEnum.EN) { + return this.isStepActiveByLabelText(this.enParticipationStepText) + } else { + throw new Error('Language not found!') + } + } +} diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts new file mode 100644 index 000000000..eecc2447e --- /dev/null +++ b/e2e/playwright.config.ts @@ -0,0 +1,109 @@ +import type { PlaywrightTestConfig } from '@playwright/test' +import path from 'path' + +const e2eReportsFolder = path.resolve(__dirname, 'e2e-reports') + +/** + * See https://playwright.dev/docs/test-configuration + */ + +const config: PlaywrightTestConfig = { + name: 'Podkrepi.bg E2E tests', + testDir: './tests', + /* Maximum time one test can run for. */ + timeout: 60 * 1000, + expect: { + timeout: 10 * 1000, + }, + /* 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. */ + // TODO update here + workers: process.env.CI ? 1 : undefined, + // outputDir: path.resolve(e2eReportsFolder, 'output-tests'), + reporter: [ + ['html', { outputFolder: path.resolve(e2eReportsFolder, 'html-report'), open: 'never' }], + ], + use: { + browserName: 'chromium', + headless: true, + screenshot: { + mode: 'only-on-failure', + fullPage: true, + }, + video: { + mode: 'off', + size: { + width: 1500, + height: 900, + }, + }, + trace: 'retain-on-failure', + launchOptions: { + args: ['--use-fake-ui-for-media-stream', '--use-fake-device-for-media-stream'], + downloadsPath: path.resolve(e2eReportsFolder, 'downloads'), + }, + baseURL: process.env.STAGING ? 'https://dev.podkrepi.bg' : 'http://localhost:3040', + viewport: { width: 1280, height: 720 }, + ignoreHTTPSErrors: true, + }, + // TODO Update here later + + /* 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: { + // channel: 'msedge', + // }, + // }, + // { + // name: 'Google Chrome', + // use: { + // channel: 'chrome', + // }, + // }, + // ] +} + +export default config diff --git a/e2e/staging/anon-donation-custom.spec.ts b/e2e/staging/anon-donation-custom.spec.ts deleted file mode 100644 index ad03f3751..000000000 --- a/e2e/staging/anon-donation-custom.spec.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { test, expect } from '@playwright/test' - -test('test anonymous donation on staging - custom amount', async ({ page }) => { - // Go to https://dev.podkrepi.bg/ - await page.goto('https://dev.podkrepi.bg/') - - // Click text=Училище за деца с нарушено зрение гр. Варна - стая за ерготерапия - await page - .locator('text=Училище за деца с нарушено зрение гр. Варна - стая за ерготерапия') - .click() - await expect(page).toHaveURL( - 'https://dev.podkrepi.bg/campaigns/uchilishe-za-deca-s-narusheno-zrenie-gr-varna-staya-za-ergoterapiya', - ) - - // Click button:has-text("Подкрепи") - await page.locator('button:has-text("Подкрепи")').click() - - await expect(page).toHaveURL( - 'https://dev.podkrepi.bg/campaigns/donation/uchilishe-za-deca-s-narusheno-zrenie-gr-varna-staya-za-ergoterapiya', - ) - - // Click label:has-text("Друга сума") - await page.locator('label:has-text("Друга сума")').click() - - // Click input[name="otherAmount"] - await page.locator('input[name="otherAmount"]').click() - - // Fill input[name="otherAmount"] - await page.locator('input[name="otherAmount"]').fill('7.50') - - // Check input[name="cardIncludeFees"] - await page.locator('input[name="cardIncludeFees"]').check() - - // Click text=8,11 лв. - await page.locator('text=8,11 лв.').click() - - // Click text=0,61 лв. - await page.locator('text=0,61 лв.').click() - - // Click text=7,50 лв. - await page.locator('text=7,50 лв.').click() - - // Click text=Напред - await page.locator('text=Напред').click() - - // Click text=Дарете анонимно - await page.locator('text=Дарете анонимно').click() - - // Click input[name="personsEmail"] - await page.locator('input[name="personsEmail"]').click() - - // Fill input[name="personsEmail"] - await page.locator('input[name="personsEmail"]').fill('test@example.com') - - // Click text=Напред - await page.locator('text=Напред').click() - - // Click textarea[name="message"] - await page.locator('textarea[name="message"]').click() - - // Fill textarea[name="message"] - await page.locator('textarea[name="message"]').fill('e2e tester 2') - - // Click text=Премини към плащане - await page.locator('text=Премини към плащане').click() - - await page.waitForURL((url) => - url.toString().startsWith('https://checkout.stripe.com/pay/cs_test_'), - ) - - await expect(page.url()).toContain('https://checkout.stripe.com/pay/cs_test_') - - // Click [placeholder="\31 234 1234 1234 1234"] - await page.locator('[placeholder="\\31 234 1234 1234 1234"]').click() - - // Fill [placeholder="\31 234 1234 1234 1234"] - await page.locator('[placeholder="\\31 234 1234 1234 1234"]').fill('4242 4242 4242 4242') - - // Click [placeholder="MM \/ YY"] - await page.locator('[placeholder="MM \\/ YY"]').click() - - // Fill [placeholder="MM \/ YY"] - await page.locator('[placeholder="MM \\/ YY"]').fill('04 / 242') - - // Click [placeholder="CVC"] - await page.locator('[placeholder="CVC"]').click() - - // Fill [placeholder="CVC"] - await page.locator('[placeholder="CVC"]').fill('424') - - // Click input[name="billingName"] - await page.locator('input[name="billingName"]').click() - - // Fill input[name="billingName"] - await page.locator('input[name="billingName"]').fill('tester') - - // Click [data-testid="hosted-payment-submit-button"] - await page.locator('[data-testid="hosted-payment-submit-button"]').click() - - await page.waitForURL( - 'https://dev.podkrepi.bg/campaigns/donation/uchilishe-za-deca-s-narusheno-zrenie-gr-varna-staya-za-ergoterapiya?success=true', - ) - - // Click text=Благодарим за доверието и подкрепата! - await page.locator('text=Благодарим за доверието и подкрепата!').click() -}) diff --git a/e2e/staging/anon-donation-fixed.spec.ts b/e2e/staging/anon-donation-fixed.spec.ts deleted file mode 100644 index 422a92a9e..000000000 --- a/e2e/staging/anon-donation-fixed.spec.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { test, expect } from '@playwright/test' - -test('test anonymous donation on staging - fixed amount', async ({ page }) => { - // Go to https://dev.podkrepi.bg/ - await page.goto('https://dev.podkrepi.bg/') - - // Click text=Дарете сега >> nth=0 - await page.locator('text=Дарете сега').first().click() - await expect(page).toHaveURL( - 'https://dev.podkrepi.bg/campaigns/donation/krizisen-centur-za-postradali-ot-nasilie-shans-za-nov-zhivot', - ) - - // Click label:has-text("10 лв.") - await page.locator('label:has-text("10 лв.")').click() - - // Click text=Искам да покрия таксите за плащане с карта издадена в: - await page.locator('text=Искам да покрия таксата за карта издадена в:').click() - - // Click text=10,65 лв. - await page.locator('text=10,65 лв.').click() - - // Click text=0,65 лв. >> nth=1 - await page.locator('text=0,65 лв.').nth(1).click() - - // Click text=10,00 лв. - await page.locator('text=10,00 лв.').click() - - // Click text=Напред - await page.locator('text=Напред').click() - - // Click text=Дарете анонимно - await page.locator('text=Дарете анонимно').click() - - // Click input[name="personsEmail"] - await page.locator('input[name="personsEmail"]').click() - - // Fill input[name="personsEmail"] - await page.locator('input[name="personsEmail"]').fill('test@example.com') - - // Click text=Напред - await page.locator('text=Напред').click() - - // Click textarea[name="message"] - await page.locator('textarea[name="message"]').click() - - // Fill textarea[name="message"] - await page.locator('textarea[name="message"]').fill('e2e test') - - // Click text=Премини към плащане - await page.locator('text=Премини към плащане').click() - - await page.waitForURL((url) => - url.toString().startsWith('https://checkout.stripe.com/pay/cs_test_'), - ) - - await expect(page.url()).toContain('https://checkout.stripe.com/pay/cs_test_') - - // Click [placeholder="\31 234 1234 1234 1234"] - await page.locator('[placeholder="\\31 234 1234 1234 1234"]').click() - - // Fill [placeholder="\31 234 1234 1234 1234"] - await page.locator('[placeholder="\\31 234 1234 1234 1234"]').fill('4242 4242 4242 4242') - - // Click [placeholder="MM \/ YY"] - await page.locator('[placeholder="MM \\/ YY"]').click() - - // Fill [placeholder="MM \/ YY"] - await page.locator('[placeholder="MM \\/ YY"]').fill('04 / 242') - - // Click [placeholder="CVC"] - await page.locator('[placeholder="CVC"]').click() - - // Fill [placeholder="CVC"] - await page.locator('[placeholder="CVC"]').fill('4242') - - // Click [data-testid="hosted-payment-submit-button"] - await page.locator('[data-testid="hosted-payment-submit-button"]').click() - - // Fill input[name="billingName"] - await page.locator('input[name="billingName"]').fill('e2e tester') - - // Click [data-testid="hosted-payment-submit-button"] - await page.locator('[data-testid="hosted-payment-submit-button"]').click() - - // Go to https://dev.podkrepi.bg/campaigns/donation/krizisen-centur-za-postradali-ot-nasilie-shans-za-nov-zhivot?success=true - await page.goto( - 'https://dev.podkrepi.bg/campaigns/donation/krizisen-centur-za-postradali-ot-nasilie-shans-za-nov-zhivot?success=true', - ) - - // Click text=Благодарим за доверието и подкрепата! - await expect(page.locator('text=Благодарим за доверието и подкрепата!')).toBeDefined() -}) diff --git a/e2e/staging/homepage.spec.ts b/e2e/staging/homepage.spec.ts deleted file mode 100644 index 43182d05d..000000000 --- a/e2e/staging/homepage.spec.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { test, expect } from '@playwright/test' - -test.beforeEach(async ({ page }) => { - await page.goto('https://dev.podkrepi.bg/') -}) - -test('test homepage on staging', async ({ page }) => { - // Go to http://dev.podkrepi.bg/ - - // Click text=Спешни кампании - await page.locator('text=Спешни кампании').click() - - // Click text=Как работи Подкрепи.бг? - await page.locator('text=Как работи Подкрепи.бг?').click() - - // Click text=Кой стои зад Подкрепи.бг? - await page.locator('text=Кой стои зад Подкрепи.бг?').click() - - // Click text=Присъединете се към Подкрепи.бг - await page.locator('text=Присъединете се към Подкрепи.бг').click() - - // Click h2:has-text("Често задавани въпроси") - await page.locator('h2:has-text("Често задавани въпроси")').click() - - // Click text=Какво е Подкрепи.бг? - await page.locator('text=Какво е Подкрепи.бг?').click() - - // Click text=Ние сме общност от доброволци, обединени от идеята да създаваме устойчиви решения за развитието на дарителството в България. - await page - .locator( - 'text=Ние сме общност от доброволци, обединени от идеята да създаваме устойчиви решения за развитието на дарителството в България. ', - ) - .click() - // Click text=Какво е „безкомпромисна прозрачност”? - await page.locator('text=Какво е „безкомпромисна прозрачност”?').click() - - // Click text=Нашето разбиране за „безкомпромисна прозрачност” е: - await page.locator('text=Нашето разбиране за „безкомпромисна прозрачност” е:').click() - - // Click text=Какви са технологичните ви предимства? - await page.locator('text=Какви са технологичните ви предимства?').click() - - // Click text=Използваме модерни решения и технологии за подсигуряване на платформата – React, Next.js като frontend, PostgreSQL като база данни, а цялостната инфраструктура се управлява на принципа на Infrastructure-as-Codе. - await page - .locator( - 'text=Използваме модерни решения и технологии за подсигуряване на платформата – React, Next.js като frontend, PostgreSQL като база данни, а цялостната инфраструктура се управлява на принципа на Infrastructure-as-Codе.', - ) - .click() - - // Click text=Какво представляват „устойчивите решения”? - await page.locator('text=Какво представляват „устойчивите решения”?').click() - - // Click text=Една африканска поговорка гласи „Ако искаш да стигнеш бързо, тръгни сам, ако искаш да стигнеш далеч, вървете заедно”. - await page - .locator( - 'text=Една африканска поговорка гласи „Ако искаш да стигнеш бързо, тръгни сам, ако искаш да стигнеш далеч, вървете заедно”.', - ) - .click() - - // Click text=Какво представляват „устойчивите решения”? - await page.locator('text=Как се финансира Подкрепи.бг?').click() - - // Click text=Подкрепи.бг НЕ удържа комисиони или такси за дейността си от събраните средства за кампаниите. - await page - .locator( - 'text=Подкрепи.бг НЕ удържа комисиони или такси за дейността си от събраните средства за кампаниите.', - ) - .click() - - // Click text=Вижте всички >> nth=1 - await page.locator('text=Вижте всички').nth(1).click() - await expect(page).toHaveURL('https://dev.podkrepi.bg/faq') -}) diff --git a/e2e/tests/regression/donation-flow/anon-donation-custom.spec.ts b/e2e/tests/regression/donation-flow/anon-donation-custom.spec.ts new file mode 100644 index 000000000..851a2c5f7 --- /dev/null +++ b/e2e/tests/regression/donation-flow/anon-donation-custom.spec.ts @@ -0,0 +1,134 @@ +import { test, expect, Page } from '@playwright/test' +import { HeaderPage } from '../../../pages/web-pages/header.page' +import { HomePage } from '../../../pages/web-pages/home.page' +import { CampaignsPage } from '../../../pages/web-pages/campaigns/campaigns.page' +import { bgLocalizationOneTimeDonation } from '../../../data/localization' +import { DonationPage } from '../../../pages/web-pages/campaigns/donation.page' +import { DonationRegions } from '../../../data/enums/donation-regions.enum' +import { StripeCheckoutPage } from '../../../pages/web-pages/external/stripe-checkout.page' +import { anonDonationTestData } from '../../../data/support-page-tests.data' + +// This spec contains E2E tests related to anonymous donation flow - custom amount +// The tests are dependent, the whole describe should be runned +test.describe.serial( + 'Anonymous contributor is able to donate custom amount - BG language version', + async () => { + let page: Page + let homepage: HomePage + let headerPage: HeaderPage + let campaignsPage: CampaignsPage + let donationPage: DonationPage + let stripeCheckoutPage: StripeCheckoutPage + const testEmail = 'E2E_Test_Anon_Donation@e2etest.com' + // Localization texts + const otherAmountText = bgLocalizationOneTimeDonation['first-step'].other + const bgCardIncludeFeesText = bgLocalizationOneTimeDonation['third-step']['card-include-fees'] + + test.beforeAll(async ({ browser }) => { + page = await browser.newPage() + homepage = new HomePage(page) + headerPage = new HeaderPage(page) + campaignsPage = new CampaignsPage(page) + donationPage = new DonationPage(page) + stripeCheckoutPage = new StripeCheckoutPage(page) + // For local executions use method navigateToLocalhostHomepage(); + // await homepage.navigateToLocalhostHomepage(); + await homepage.navigateToEnvHomepage() + }) + + test.afterAll(async () => { + await page.close() + }) + + test('Particular campaign can be opened through the Campaign page', async () => { + await headerPage.clickDonateHeaderNavButton() + await campaignsPage.clickCampaignCardByIndex(0) + // We move from the common Campaigns page to the particular campain page + // check if the url is changed only based on the url pattern http://localhost:3040/campaigns/{slug-based-regexp} + expect( + await campaignsPage.checkPageUrlByRegExp(), + 'The url is not changed after clicking on the campaign card.', + ) + }) + + test('The total charge, fee tax and donation amount are visible on the Campaign page', async () => { + await campaignsPage.clickDonationSupportButton() + await donationPage.checkPageUrlByRegExp() + expect + .soft(await donationPage.isSelectAmountStepActive(), 'Select Amount step is not active.') + .toBeTruthy() + await donationPage.selectRadioButtonByLabelText([otherAmountText]) + await donationPage.fillOtherAmountInputField('7.50') + await donationPage.setDonationRegionFromTheDropdown(DonationRegions.EUROPE) + await donationPage.selectCheckboxByLabelText([bgCardIncludeFeesText]) + // Expected pattern: + // За вашия превод от {totalChargedAmountText} лв., таксата на Stripe ще е {feeAmountText} лв., а кампанията ще получи {donationAmountText} лв. + const totalChargedAmountText = await donationPage.getTotalChargedAmountsAsText() + const feeAmountText = await donationPage.getFeeAmountsAsText() + const donationAmountText = await donationPage.getDonationAmountsAsText() + expect.soft(totalChargedAmountText).toEqual('8,10 лв.') + expect.soft(feeAmountText).toEqual('0,60 лв.') + expect(donationAmountText).toEqual('7,50 лв.') + }) + + test('The total charge, fee tax and donation amount are recalculated correctly when the donation amount is changed', async () => { + await donationPage.fillOtherAmountInputField('12.90') + // Expected pattern: + // За вашия превод от {totalChargedAmountText} лв., таксата на Stripe ще е {feeAmountText} лв., а кампанията ще получи {donationAmountText} лв. + const totalChargedAmountText = await donationPage.getTotalChargedAmountsAsText() + const feeAmountText = await donationPage.getFeeAmountsAsText() + const donationAmountText = await donationPage.getDonationAmountsAsText() + expect.soft(totalChargedAmountText).toEqual('13,56 лв.') + expect.soft(feeAmountText).toEqual('0,66 лв.') + expect(donationAmountText).toEqual('12,90 лв.') + }) + + test('The user is able to fill in e-mail for anonymous donation', async () => { + await donationPage.clickForwardButton() + expect + .soft( + await donationPage.isPersonalProfileStepActive(), + 'Personal Profile step is not active.', + ) + .toBeTruthy() + await donationPage.clickDonateAnonymouslyButton() + await donationPage.fillDonateAnonymouslyEmailField(testEmail) + await donationPage.clickForwardButton() + expect( + await donationPage.isSendAWishStepActive(), + 'Send a wish step is not active.', + ).toBeTruthy() + }) + + test('After sending a wish, the user is redirected to Stripe', async () => { + await donationPage.fillSendAWishField('E2E test - anonymous donation.') + await donationPage.clickFinishButton() + const stripeTotalAmount = await stripeCheckoutPage.getTotalAmountText() + const actualStripeEmail = await stripeCheckoutPage.getReadonlyEmailText() + expect + .soft(stripeTotalAmount, 'The Stripe total donation amount is not correct.') + .toContain('13.56') + expect(actualStripeEmail, 'The user e-mail is not sent correctly to Stripe.').toEqual( + testEmail, + ) + }) + + test('The user is able to pay via Stripe', async () => { + await stripeCheckoutPage.fillPaymentForm([ + anonDonationTestData.cardNumber, + anonDonationTestData.cardExpDate, + anonDonationTestData.cardCvc, + anonDonationTestData.billingName, + anonDonationTestData.country, + ]) + + expect + .soft( + await donationPage.isSuccessfulDonationTitleVisible(), + "'We thank you for your help and trust!' title is not visible.", + ) + .toBeTruthy() + expect(await donationPage.isPaymentStepActive(), 'Payment step is not active.').toBeTruthy() + }) + }, +) diff --git a/e2e/tests/regression/donation-flow/anon-donation-fixed.spec.ts b/e2e/tests/regression/donation-flow/anon-donation-fixed.spec.ts new file mode 100644 index 000000000..c6527c858 --- /dev/null +++ b/e2e/tests/regression/donation-flow/anon-donation-fixed.spec.ts @@ -0,0 +1,142 @@ +import { test, expect, Page } from '@playwright/test' +import { HeaderPage } from '../../../pages/web-pages/header.page' +import { HomePage } from '../../../pages/web-pages/home.page' +import { CampaignsPage } from '../../../pages/web-pages/campaigns/campaigns.page' +import { enLocalizationOneTimeDonation } from '../../../data/localization' +import { DonationPage } from '../../../pages/web-pages/campaigns/donation.page' +import { DonationRegions } from '../../../data/enums/donation-regions.enum' +import { StripeCheckoutPage } from '../../../pages/web-pages/external/stripe-checkout.page' +import { anonDonationTestData } from '../../../data/support-page-tests.data' +import { LanguagesEnum } from '../../../data/enums/languages.enum' + +// This spec contains E2E tests related to anonymous donation flow - fixed amount +// The tests are dependent, the whole describe should be runned +test.describe.serial( + 'Anonymous contributor is able to donate fixed amount - EN language version', + async () => { + let page: Page + let homepage: HomePage + let headerPage: HeaderPage + let campaignsPage: CampaignsPage + let donationPage: DonationPage + let stripeCheckoutPage: StripeCheckoutPage + const testEmail = 'E2E_Test_Anon_Donation@e2etest.com' + // Localization texts + const enCardIncludeFeesText = enLocalizationOneTimeDonation['third-step']['card-include-fees'] + + test.beforeAll(async ({ browser, baseURL }) => { + page = await browser.newPage() + homepage = new HomePage(page) + headerPage = new HeaderPage(page) + campaignsPage = new CampaignsPage(page) + donationPage = new DonationPage(page) + stripeCheckoutPage = new StripeCheckoutPage(page) + // For local executions use method navigateToLocalhostHomepage(); + // await homepage.navigateToLocalhostHomepage(); + await homepage.navigateToEnvHomepage() + await headerPage.changeanguageHeaderButtonToBe(LanguagesEnum.EN) + }) + + test.afterAll(async () => { + await page.close() + }) + + test('Particular campaign can be opened through the Campaign page', async () => { + await headerPage.clickDonateHeaderNavButton() + await campaignsPage.clickCampaignCardByIndex(0) + // We move from the common Campaigns page to the particular campain page + // check if the url is changed only based on the url pattern http://localhost:3040/campaigns/{slug-based-regexp} + // expect to not break + + expect( + await campaignsPage.checkPageUrlByRegExp(), + 'The url is not changed after clicking on the campaign card.', + ) + }) + + test('The total charge, fee tax and donation amount are visible on the Campaign page', async () => { + await campaignsPage.clickDonationSupportButton() + await donationPage.checkPageUrlByRegExp() + expect + .soft( + await donationPage.isSelectAmountStepActive(LanguagesEnum.EN), + 'Select Amount step is not active.', + ) + .toBeTruthy() + await donationPage.selectRadioButtonByLabelText(['10']) + await donationPage.setDonationRegionFromTheDropdown(DonationRegions.EUROPE) + await donationPage.selectCheckboxByLabelText([enCardIncludeFeesText]) + // Expected pattern: + // For your donation of {donationAmountText}, the fee from Stripe will be {feeAmountText}, and the total charged amount will be {totalChargedAmountText}. + const donationAmountText = await donationPage.getTotalChargedAmountsAsText() + const feeAmountText = await donationPage.getFeeAmountsAsText() + const totalChargedAmountText = await donationPage.getDonationAmountsAsText() + expect.soft(donationAmountText).toMatch('10.00') + expect.soft(feeAmountText).toMatch('0.63') + expect(totalChargedAmountText).toMatch('10.63') + }) + + test('The total charge, fee tax and donation amount are recalculated correctly when the donation amount is changed', async () => { + await donationPage.selectRadioButtonByLabelText(['20']) + // Expected pattern: + // For your donation of {donationAmountText}, the fee from Stripe will be {feeAmountText}, and the total charged amount will be {totalChargedAmountText}. + const donationAmountText = await donationPage.getTotalChargedAmountsAsText() + const feeAmountText = await donationPage.getFeeAmountsAsText() + const totalChargedAmountText = await donationPage.getDonationAmountsAsText() + expect.soft(donationAmountText).toMatch('20.00') + expect.soft(feeAmountText).toMatch('0.75') + expect(totalChargedAmountText).toMatch('20.75') + }) + + test('The user is able to fill in e-mail for anonymous donation', async () => { + await donationPage.clickForwardButton(LanguagesEnum.EN) + expect + .soft( + await donationPage.isPersonalProfileStepActive(LanguagesEnum.EN), + 'Personal Profile step is not active.', + ) + .toBeTruthy() + await donationPage.clickDonateAnonymouslyButton(LanguagesEnum.EN) + await donationPage.fillDonateAnonymouslyEmailField(testEmail) + await donationPage.clickForwardButton(LanguagesEnum.EN) + expect( + await donationPage.isSendAWishStepActive(LanguagesEnum.EN), + 'Send a wish step is not active.', + ).toBeTruthy() + }) + + test('After sending a wish, the user is redirected to Stripe', async () => { + await donationPage.fillSendAWishField('E2E test - anonymous donation.') + await donationPage.clickFinishButton(LanguagesEnum.EN) + const stripeTotalAmount = await stripeCheckoutPage.getTotalAmountText() + const actualStripeEmail = await stripeCheckoutPage.getReadonlyEmailText() + expect + .soft(stripeTotalAmount, 'The Stripe total donation amount is not correct.') + .toContain('20.75') + expect(actualStripeEmail, 'The user e-mail is not sent correctly to Stripe.').toEqual( + testEmail, + ) + }) + + test('The user is able to pay via Stripe', async () => { + await stripeCheckoutPage.fillPaymentForm([ + anonDonationTestData.cardNumber, + anonDonationTestData.cardExpDate, + anonDonationTestData.cardCvc, + anonDonationTestData.billingName, + anonDonationTestData.country, + ]) + // Now we're redirected to the Donation page + expect + .soft( + await donationPage.isSuccessfulDonationTitleVisible(LanguagesEnum.EN), + "'We thank you for your help and trust!' title is not visible.", + ) + .toBeTruthy() + expect( + await donationPage.isPaymentStepActive(LanguagesEnum.EN), + 'Payment step is not active.', + ).toBeTruthy() + }) + }, +) diff --git a/e2e/tests/regression/support-as-member.spec.ts b/e2e/tests/regression/support-as-member.spec.ts new file mode 100644 index 000000000..0ff721645 --- /dev/null +++ b/e2e/tests/regression/support-as-member.spec.ts @@ -0,0 +1,145 @@ +import { expect, Page, test } from '@playwright/test' +import { bgLocalizationSupport } from '../../data/localization' +import { supportPageVolutneerTestData } from '../../data/support-page-tests.data' +import { HeaderPage } from '../../pages/web-pages/header.page' +import { HomePage } from '../../pages/web-pages/home.page' +import { SupportPage } from '../../pages/web-pages/support.page' + +// This spec contains E2E tests related tothe Support page +// The tests are dependent, the whole describe should be runned +test.describe.serial('Support page - Join us as member - BG language version', async () => { + let page: Page + let homepage: HomePage + let headerPage: HeaderPage + let supportPage: SupportPage + // rolesTestArray: 'Дарител', 'Доброволец', 'Член на сдружението', 'Партньор', 'Компания' + const rolesTestArray = [ + bgLocalizationSupport.steps.role.fields.benefactor.title, + bgLocalizationSupport.steps.role.fields.volunteer.title, + bgLocalizationSupport.steps.role.fields.associationMember.title, + bgLocalizationSupport.steps.role.fields.partner.title, + bgLocalizationSupport.steps.role.fields.company.title, + ] + const memberText = bgLocalizationSupport.steps['addition-questions'].associationMember.member + const campaignBenefactorText = + bgLocalizationSupport.steps['addition-questions'].benefactor.campaignBenefactor + const npoPartnerText = bgLocalizationSupport.steps['addition-questions'].partner.npo + const companySponsorText = bgLocalizationSupport.steps['addition-questions'].company.sponsor + const projectManagerText = + bgLocalizationSupport.steps['addition-questions'].volunteer.projectManager + const backEndText = bgLocalizationSupport.steps['addition-questions'].volunteer.backend + const frontendText = bgLocalizationSupport.steps['addition-questions'].volunteer.frontend + + test.beforeAll(async ({ browser }) => { + page = await browser.newPage() + homepage = new HomePage(page) + headerPage = new HeaderPage(page) + supportPage = new SupportPage(page) + + await homepage.navigateToEnvHomepage() + await headerPage.clickJoinUsHeaderNavButton() + }) + + test.afterAll(async () => { + await page.close() + }) + + test('"Role" step works correctly', async () => { + await supportPage.selectCheckboxByLabelText(rolesTestArray) + await supportPage.clickSubmitButton() + expect + .soft( + await supportPage.isAdditionalQuestionsStepActive(), + 'Additional Questions step is not active.', + ) + .toBeTruthy() + expect( + await supportPage.isCheckboxCheckedByLabelText(memberText), + "Expected checkbox 'Member' is not checked.", + ).toBeTruthy() + }) + + test('"Additional questions" step works correctly', async () => { + await supportPage.selectCheckboxByLabelText([ + campaignBenefactorText, + npoPartnerText, + companySponsorText, + projectManagerText, + backEndText, + frontendText, + ]) + await supportPage.clickSubmitButton() + expect + .soft( + await supportPage.isAdditionalQuestionsStepActive(), + 'Additional Questions step is active, but should not be.', + ) + .toBeFalsy() + await supportPage.clickBackButton() + expect + .soft( + await supportPage.isCheckboxCheckedByLabelText(campaignBenefactorText), + `${campaignBenefactorText} is not checked`, + ) + .toBeTruthy() + expect + .soft( + await supportPage.isCheckboxCheckedByLabelText(npoPartnerText), + `${npoPartnerText} is not checked`, + ) + .toBeTruthy() + expect + .soft( + await supportPage.isCheckboxCheckedByLabelText(companySponsorText), + `${companySponsorText} is not checked`, + ) + .toBeTruthy() + expect + .soft( + await supportPage.isCheckboxCheckedByLabelText(projectManagerText), + `${projectManagerText} is not checked`, + ) + .toBeTruthy() + expect + .soft( + await supportPage.isCheckboxCheckedByLabelText(backEndText), + `${backEndText} is not checked`, + ) + .toBeTruthy() + expect( + await supportPage.isCheckboxCheckedByLabelText(frontendText), + `${frontendText} is not checked`, + ).toBeTruthy() + }) + + test('"Keep in touch" step works correctly', async () => { + await supportPage.clickSubmitButton() + await supportPage.fillContactForm([ + supportPageVolutneerTestData.firstName, + supportPageVolutneerTestData.lastName, + supportPageVolutneerTestData.email, + supportPageVolutneerTestData.phone, + supportPageVolutneerTestData.comment, + ]) + expect( + await supportPage.isAdditionalQuestionsStepActive(), + 'Additional Questions step is active, but should not be.', + ).toBeFalsy() + }) + + test('"Thank you" step works correctly', async () => { + expect + .soft( + await supportPage.isContactDataStepActive(), + 'Contact Data step is active, but should not be.', + ) + .toBeFalsy() + expect + .soft(await supportPage.isParticipationStepActive(), 'Participation step is Not active.') + .toBeTruthy() + expect( + await supportPage.isThankYouSupportH4HeadingVisible(), + 'Thank you greeting is not visible.', + ).toBeTruthy() + }) +}) diff --git a/e2e/tests/smoke/smoke-campaigns.spec.ts b/e2e/tests/smoke/smoke-campaigns.spec.ts new file mode 100644 index 000000000..402ae6f35 --- /dev/null +++ b/e2e/tests/smoke/smoke-campaigns.spec.ts @@ -0,0 +1,59 @@ +import { test, expect, Page } from '@playwright/test' + +import { CampaignsPage } from '../../pages/web-pages/campaigns/campaigns.page' +import { DonationPage } from '../../pages/web-pages/campaigns/donation.page' +import { HeaderPage } from '../../pages/web-pages/header.page' +import { HomePage } from '../../pages/web-pages/home.page' + +// This spec contains smoke E2E tests for the Campaigns page +// The tests are dependent, the whole describe should be runned +test.describe('Campaigns page smoke tests - BG language version', async () => { + let page: Page + let homepage: HomePage + let headerPage: HeaderPage + let campaignsPage: CampaignsPage + let donationPage: DonationPage + + test.beforeAll(async ({ browser }) => { + page = await browser.newPage() + homepage = new HomePage(page) + headerPage = new HeaderPage(page) + campaignsPage = new CampaignsPage(page) + donationPage = new DonationPage(page) + + await homepage.navigateToEnvHomepage() + await headerPage.clickCampaignsHeaderNavButton() + }) + + test.afterAll(async () => { + await page.close() + }) + + test('The main page headers are visible', async () => { + expect + .soft( + await campaignsPage.isCampaignsHeadingVisible(), + 'The main Campaigns heading is not visible.', + ) + .toBeTruthy() + expect( + await campaignsPage.isSupportCauseTodayHeadingVisible(), + 'The Support Cause Today heading is not visible.', + ).toBeTruthy() + }) + + test('Filter buttons count is correct', async () => { + expect( + await campaignsPage.getFilterButtonsCount(), + 'Filter buttons count is not correct!', + ).toEqual(12) + }) + + test('Support Now action button navigates to the Donation page for particular campaign', async () => { + await campaignsPage.clickCampaignCardButtonByIndex(0) + await donationPage.checkPageUrlByRegExp() + expect + .soft(await donationPage.isSelectAmountStepActive(), 'Select Amount step is not active.') + .toBeTruthy() + }) +}) diff --git a/e2e/tests/smoke/smoke-homepage.spec.ts b/e2e/tests/smoke/smoke-homepage.spec.ts new file mode 100644 index 000000000..ccd1b8f61 --- /dev/null +++ b/e2e/tests/smoke/smoke-homepage.spec.ts @@ -0,0 +1,57 @@ +import { expect, Page, test } from '@playwright/test' +import { HomePage } from '../../pages/web-pages/home.page' + +// This spec contains smoke E2E tests for the Home page +test.describe('Homepage smoke tests - BG language version', async () => { + let page: Page + let homepage: HomePage + + test.beforeAll(async ({ browser }) => { + page = await browser.newPage() + homepage = new HomePage(page) + }) + + test.beforeEach(async () => { + await homepage.navigateToEnvHomepage() + }) + + test.afterAll(async () => { + await page.close() + }) + + test('Check if "How we work" heading heading is visible', async () => { + expect( + await homepage.isHowWeWorkHeadingVisible(), + '"How we work" heading is not visible.', + ).toBeTruthy() + }) + + test('Check if "Who is behind Podkrepi" heading is visible', async () => { + expect( + await homepage.isTeamSectionHeadingVisible(), + '"Who is behind Podkrepi" heading is not visible.', + ).toBeTruthy() + }) + + test('Check if "Join Podkrepi" heading is visible', async () => { + expect( + await homepage.isJoinPodkrepiSectionHeadingVisible(), + '"Join Podkrepi" heading is not visible.', + ).toBeTruthy() + }) + + test('Check if "FAQ" heading is visible', async () => { + expect( + await homepage.isFaqSectionHeadingVisible(), + '"FAQ" heading is not visible.', + ).toBeTruthy() + }) + + test('Check if "What is Podkrepi" FAQ list item is visible', async () => { + await homepage.clickWhatIsPodkrepiFaqListQuestion() + expect( + await homepage.isPodkrepiFaqListAnswerVisible(), + '"What is Podkrepi" FAQ list item is not visible.', + ) + }) +}) diff --git a/e2e/helpers.ts b/e2e/utils/helpers.ts similarity index 70% rename from e2e/helpers.ts rename to e2e/utils/helpers.ts index 6f0faba77..285e098a0 100644 --- a/e2e/helpers.ts +++ b/e2e/utils/helpers.ts @@ -1,5 +1,6 @@ import { expect, Page } from '@playwright/test' +// TODO: Refactor this page. It is not needed in general, because there are easier ways to check this. /** * @param page The page to get the clipboard text from. * @param textToCheck The text to check for in the clipboard. @@ -20,3 +21,10 @@ export const expectCopied = async (page: Page, textToCheck: string) => { await newPage.evaluate(() => document.querySelector('#clipboard-tester-div')?.textContent), ).toBe(textToCheck) } + +/** + * @description + * - (?:-[a-z0-9]+)* matches the characters - and a-z0-9 between one and unlimited times, as many times as possible, giving back as needed (greedy) and does not remember the match + * - $ asserts position at the end of the string + */ +export const SLUG_REGEX = `[a-z0-9]+(?:-[a-z0-9]+)*$` diff --git a/e2e/utils/types.ts b/e2e/utils/types.ts new file mode 100644 index 000000000..982155463 --- /dev/null +++ b/e2e/utils/types.ts @@ -0,0 +1,71 @@ +import { Locator } from '@playwright/test' + +export type LocatorOptions = Partial<{ + has?: Locator | undefined + hasText?: string | RegExp | undefined +}> + +export type ClickOptions = Partial<{ + /** + * Defaults to `left`. + */ + button?: 'left' | 'right' | 'middle' + + /** + * defaults to 1. See [UIEvent.detail]. + */ + clickCount?: number + + /** + * Time to wait between `mousedown` and `mouseup` in milliseconds. Defaults to 0. + */ + delay?: number + + /** + * Whether to bypass the [actionability](https://playwright.dev/docs/actionability) checks. Defaults to `false`. + */ + force?: boolean + + /** + * Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores + * current modifiers back. If not specified, currently pressed modifiers are used. + */ + modifiers?: Array<'Alt' | 'Control' | 'Meta' | 'Shift'> + + /** + * Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You + * can opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as + * navigating to inaccessible pages. Defaults to `false`. + */ + noWaitAfter?: boolean + + /** + * A point to use relative to the top-left corner of element padding box. If not specified, uses some visible point of + * the element. + */ + position?: { + x: number + + y: number + } + + /** + * When true, the call requires selector to resolve to a single element. If given selector resolves to more than one + * element, the call throws an exception. + */ + strict?: boolean + + /** + * Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed + * by using the + * [browserContext.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-timeout) + * or [page.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-timeout) methods. + */ + timeout?: number + + /** + * When set, this method only performs the [actionability](https://playwright.dev/docs/actionability) checks and skips the action. Defaults + * to `false`. Useful to wait until the element is ready for the action without performing it. + */ + trial?: boolean +}> diff --git a/e2e/yarn.lock b/e2e/yarn.lock new file mode 100644 index 000000000..699a36804 --- /dev/null +++ b/e2e/yarn.lock @@ -0,0 +1,54 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 6 + cacheKey: 8 + +"@playwright/test@npm:1.30.0": + version: 1.30.0 + resolution: "@playwright/test@npm:1.30.0" + dependencies: + "@types/node": "*" + playwright-core: 1.30.0 + bin: + playwright: cli.js + checksum: 777432ac9cf3d0341fcd8dd265cb4c0775619d0ef48252b32a7c4d632d8038449756ec34bec873828cadbc08ba634e81176cb193304d34e699472771b7fb4d1e + languageName: node + linkType: hard + +"@types/node@npm:*": + version: 18.11.17 + resolution: "@types/node@npm:18.11.17" + checksum: 1933afd068d5c75c068c6c4df6d10edb3b0b2bb6503d544e2f0496ac007c90596e6a5e284a8ef032451bc16f871b7e46719d7d2bea60e9b25d13a77d52161cac + languageName: node + linkType: hard + +"e2e@workspace:.": + version: 0.0.0-use.local + resolution: "e2e@workspace:." + dependencies: + "@playwright/test": 1.30.0 + playwright: 1.30.0 + languageName: unknown + linkType: soft + +"playwright-core@npm:1.30.0": + version: 1.30.0 + resolution: "playwright-core@npm:1.30.0" + bin: + playwright: cli.js + checksum: 4c5693f27245a1168f94708ecd8e1eb0d200de435b25cc07cfa25b97a094633818954dc00baf24e0ff551825f672050b83d1309362c1f97213fe8ebd2a147ed9 + languageName: node + linkType: hard + +"playwright@npm:1.30.0": + version: 1.30.0 + resolution: "playwright@npm:1.30.0" + dependencies: + playwright-core: 1.30.0 + bin: + playwright: cli.js + checksum: 1987446ed07e25c0c6dedce8314209b49536eb4c7fa82e57e7fea9bd8128bacd08e49f9e89af30a647839bf2603b8c8321f50e23e334a11c1c29eedb838a81a3 + languageName: node + linkType: hard diff --git a/next.config.js b/next.config.js index f5701d182..5e633e383 100644 --- a/next.config.js +++ b/next.config.js @@ -15,6 +15,9 @@ const moduleExports = { sassOptions: { includePaths: [path.join(__dirname, 'src/styles')], }, + typescript: { + tsconfigPath: 'tsconfig.build.json', + }, swcMinify: true, env: { APP_ENV: process.env.APP_ENV, @@ -60,12 +63,21 @@ const moduleExports = { ] }, modularizeImports: { + lodash: { + transform: 'lodash/{{member}}', + }, '@mui/material': { transform: '@mui/material/{{member}}', }, '@mui/icons-material': { transform: '@mui/icons-material/{{member}}', }, + '@mui/core/': { + transform: '@mui/core/{{member}}', + }, + '@mui/lab/': { + transform: '@mui/lab/{{member}}', + }, }, } diff --git a/package.json b/package.json index 7027ac72d..83df00074 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "build": "yarn && next build", "start": "next start -p 3040", "test": "jest --env=jsdom", - "test:e2e": "playwright test", + "test:e2e": "cd ./e2e && yarn run test:e2e", "test:debug": "node --inspect-brk ./node_modules/jest/bin/jest.js --runInBand", "test:watch": "jest --env=jsdom --watch --silent=false", "lint": "eslint . --fix --ext=ts,tsx", @@ -48,6 +48,7 @@ "axios-hooks": "2.7.0", "date-fns": "2.24.0", "formik": "2.2.9", + "formik-persist-values": "^1.4.1", "i18next": "^21.6.16", "jwt-decode": "^3.1.2", "lodash": "^4.17.21", @@ -72,7 +73,6 @@ }, "devDependencies": { "@next/bundle-analyzer": "^12.1.0", - "@playwright/test": "^1.24.2", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@types/cookie": "^0.4.1", @@ -84,6 +84,7 @@ "@types/react-gtm-module": "2.0.0", "@types/react-slick": "^0.23.10", "@types/tryghost__content-api": "^1.3.11", + "@types/uuid": "^9.0.0", "@types/yup": "0.29.11", "@typescript-eslint/eslint-plugin": "4.26.0", "@typescript-eslint/parser": "4.26.0", diff --git a/playwright.config.ts b/playwright.config.ts deleted file mode 100644 index a45a0649c..000000000 --- a/playwright.config.ts +++ /dev/null @@ -1,110 +0,0 @@ -import type { PlaywrightTestConfig } from '@playwright/test' -import { devices } from '@playwright/test' - -/** - * Read environment variables from file. - * https://github.com/motdotla/dotenv - */ -// require('dotenv').config(); - -/** - * See https://playwright.dev/docs/test-configuration. - */ -const config: PlaywrightTestConfig = { - testDir: './e2e', - /* Maximum time one test can run for. */ - timeout: 30 * 1000, - expect: { - /** - * Maximum time expect() should wait for the condition to be met. - * For example in `await expect(locator).toHaveText();` - */ - timeout: 5000, - }, - /* 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: { - /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ - actionTimeout: 0, - /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: process.env.BASE_URL || 'http://localhost:3040', - - /* 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'], - contextOptions: { - permissions: ['clipboard-read'], - }, - }, - }, - - { - 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: { - // channel: 'msedge', - // }, - // }, - // { - // name: 'Google Chrome', - // use: { - // channel: 'chrome', - // }, - // }, - ], - - /* Folder for test artifacts such as screenshots, videos, traces, etc. */ - // outputDir: 'test-results/', - - /* Run your local dev server before starting the tests */ - // webServer: { - // command: 'npm run start', - // port: 3000, - // }, -} - -export default config diff --git a/public/locales/bg/campaigns.json b/public/locales/bg/campaigns.json index b7a0bf2fa..0cf58eaef 100644 --- a/public/locales/bg/campaigns.json +++ b/public/locales/bg/campaigns.json @@ -20,6 +20,8 @@ "targetAmount": "Целева сума", "donationsAmount": "Събрани средства", "blockedAmount": "Блокирани средства", + "withdrawnAmount": "Преведени средства", + "startDate": "Стартова дата", "endDate": "Крайна дата", "createDate": "Съдадена на", diff --git a/public/locales/bg/common.json b/public/locales/bg/common.json index 6fd3a9a57..a87b5f010 100644 --- a/public/locales/bg/common.json +++ b/public/locales/bg/common.json @@ -28,7 +28,7 @@ "open-source": "Отворен Код", "open-data": "Отворени Данни" }, - "donatе": "Дарете", + "donate": "Дарете", "donation-menu": "Кампании", "blog": "Блог", "support_us_button": "Подкрепете ни", diff --git a/public/locales/bg/donations.json b/public/locales/bg/donations.json index e11cb913f..828ce6f0c 100644 --- a/public/locales/bg/donations.json +++ b/public/locales/bg/donations.json @@ -22,8 +22,8 @@ "deleteAllContent": "Това действие ще изтрие избраните елементи завинаги!", "actions": "Действия", "createdAt": "Направено на", - "bankTransactionsFileId": "Номер на файла с банкови транзакции", - "addFiles": "Добави файлове", + "bankTransactionsFileId": "Име на файла с банкови транзакции", + "addFiles": "Избери файл", "noOptions": "Няма резултати", "alerts": { "selectRow": "Моля изберете ред", diff --git a/public/locales/bg/withdrawals.json b/public/locales/bg/withdrawals.json index 9d73d2cc8..22b60ae11 100644 --- a/public/locales/bg/withdrawals.json +++ b/public/locales/bg/withdrawals.json @@ -1,6 +1,6 @@ { "amount-unavailable": "Недостатъчна наличност в трезора!", - "documentId": "ID на документа", + "documentId": "Номер на документа", "amount-available": "Налична сума", "amount-input": "Въведете сума", "form-heading": "Създай нов превод", @@ -30,7 +30,16 @@ "edit": "Преводът беше редактиран успешно!", "delete": "Преводът беше преместен в кошчето!", "deleteAll": "Преводите бяха преместени в кошчето!", - "error": "Възникна грешка! Моля опитайте отново по-късно." + "error": "Възникна грешка! Моля опитайте отново по-късно.", + "no-edit": "Превода е приключен успешно и не може да се променя." + }, + "statuses": { + "initial": "нов", + "invalid": "невалиден", + "incomplete": "непълен", + "declined": "отказан", + "cancelled": "оттеглен", + "succeeded": "успешен" }, "cta": { "add": "Добави", diff --git a/public/locales/en/campaigns.json b/public/locales/en/campaigns.json index 811d88ceb..509badfd2 100644 --- a/public/locales/en/campaigns.json +++ b/public/locales/en/campaigns.json @@ -20,6 +20,7 @@ "targetAmount": "Target amount", "donationsAmount": "Donations amount", "blockedAmount": "Blocked amount", + "withdrawnAmount": "Withdrawn amount", "startDate": "Start date", "endDate": "End date", "createDate": "Created at", diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 3c525acfa..9a58c5380 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -28,7 +28,7 @@ "open-source": "Open Source", "open-data": "Open Data" }, - "donatе": "Donate", + "donate": "Donate", "donation-menu": "Campaigns", "blog": "Blog", "support_us_button": "Support us", diff --git a/public/locales/en/donations.json b/public/locales/en/donations.json index aa04f672a..20f4a9d7a 100644 --- a/public/locales/en/donations.json +++ b/public/locales/en/donations.json @@ -20,8 +20,8 @@ "deleteTitle": "Are you sure?", "deleteContent": "This action will delete this item permanently!", "deleteAllContent": "This action will delete selected items permanently!", - "bankTransactionsFileId": "Number of bank transactions file", - "addFiles": "Add files", + "bankTransactionsFileId": "Name of bank transactions file", + "addFiles": "Select file", "actions": "Actions", "createdAt": "Created at", "noOptions": "No results", diff --git a/public/locales/en/withdrawals.json b/public/locales/en/withdrawals.json index 2a59d288f..39955e1ae 100644 --- a/public/locales/en/withdrawals.json +++ b/public/locales/en/withdrawals.json @@ -25,7 +25,16 @@ "edit": "Withdrawal has been edited successfully!", "delete": "Withdrawal has been deleted successfully!", "deleteAll": "Withdrawals have been deleted successfully!", - "error": "An error has occured! Please try again later." + "error": "An error has occured! Please try again later.", + "no-edit": "This withdrawal is completed successfully and cannot be changed." + }, + "statuses": { + "initial": "initial", + "invalid": "invalid", + "incomplete": "incomplete", + "declined": "declined", + "cancelled": "cancelled", + "succeeded": "succeeded" }, "cta": { "add": "Add", diff --git a/src/common/hooks/campaigns.ts b/src/common/hooks/campaigns.ts index 9535a89fa..f855e438f 100644 --- a/src/common/hooks/campaigns.ts +++ b/src/common/hooks/campaigns.ts @@ -14,16 +14,37 @@ import { DonationStatus } from 'gql/donations.enums' import { apiClient } from 'service/apiClient' // NOTE: shuffling the campaigns so that each gets its fair chance to be on top row -const shuffleQueryFn: QueryFunction = async ({ queryKey }) => { +export const campaignsOrderQueryFunction: QueryFunction = async ({ + queryKey, +}) => { const response = await apiClient.get(queryKey.join('/')) - return shuffle(response.data) + // Put the campaigns in 2 arrays, one for active and one for inactive + const activeCampaigns: CampaignResponse[] = [] + const inactiveCampaigns: CampaignResponse[] = [] + response.data.forEach((campaign: CampaignResponse) => { + if (campaign.state === 'active') { + activeCampaigns.push(campaign) + } else { + inactiveCampaigns.push(campaign) + } + }) + // Shuffle the active campaigns + const shuffledActiveCampaigns = shuffle(activeCampaigns) + // Shuffle the inactive campaigns + const shuffledInactiveCampaigns = shuffle(inactiveCampaigns) + // Concatenate the two arrays + return shuffledActiveCampaigns.concat(shuffledInactiveCampaigns) } export function useCampaignList() { - return useQuery([endpoints.campaign.listCampaigns.url], shuffleQueryFn, { - // Add 15 minutes of cache time - staleTime: 1000 * 60 * 15, - }) + return useQuery( + [endpoints.campaign.listCampaigns.url], + campaignsOrderQueryFunction, + { + // Add 15 minutes of cache time + staleTime: 1000 * 60 * 15, + }, + ) } export function useCampaignAdminList() { diff --git a/src/components/campaigns/CampaignCard.tsx b/src/components/campaigns/CampaignCard.tsx index ef07cb4da..fc50d6e95 100644 --- a/src/components/campaigns/CampaignCard.tsx +++ b/src/components/campaigns/CampaignCard.tsx @@ -101,9 +101,9 @@ const StyledCard = styled(Card)(({ theme }) => ({ }, })) -type Props = { campaign: CampaignResponse } +type Props = { campaign: CampaignResponse; index: number } -export default function CampaignCard({ campaign }: Props) { +export default function CampaignCard({ campaign, index }: Props) { const { t } = useTranslation() const { @@ -123,7 +123,10 @@ export default function CampaignCard({ campaign }: Props) { return ( - +
diff --git a/src/components/campaigns/CampaignFilter.tsx b/src/components/campaigns/CampaignFilter.tsx index 632abb21e..62bef7ed5 100644 --- a/src/components/campaigns/CampaignFilter.tsx +++ b/src/components/campaigns/CampaignFilter.tsx @@ -76,7 +76,6 @@ export default function CampaignFilter() { const { mobile } = useMobile() const { data: campaigns, isLoading } = useCampaignList() const [selectedCategory, setSelectedCategory] = useState('ALL') - // TODO: add filters&sorting of campaigns so people can select based on personal preferences const campaignToShow = useMemo(() => { const filteredCampaigns = diff --git a/src/components/campaigns/CampaignsList.tsx b/src/components/campaigns/CampaignsList.tsx index bdfb8a7c2..a594222fa 100644 --- a/src/components/campaigns/CampaignsList.tsx +++ b/src/components/campaigns/CampaignsList.tsx @@ -34,22 +34,26 @@ export default function CampaignsList({ campaignToShow }: Props) { }, [campaignToShow, all]) return ( - - - {/*
{JSON.stringify(data, null, 2)}
*/} - {campaigns?.map((campaign, index, array) => ( - - ({ - textAlign: 'center', - [theme.breakpoints.down('lg')]: { textAlign: cardAlignment(index, array) }, - [theme.breakpoints.down('md')]: { textAlign: 'center' }, - })}> - - - - ))} -
+ ({ + width: `calc(100% + ${theme.spacing(1.5)})`, + marginLeft: `-${theme.spacing(2.75)}`, + })}> + {campaigns?.map((campaign, index, array) => ( + + ({ + textAlign: 'center', + [theme.breakpoints.down('lg')]: { textAlign: cardAlignment(index, array) }, + [theme.breakpoints.down('md')]: { textAlign: 'center' }, + })}> + + + + ))} {campaignToShow && campaignToShow?.length > numberOfMinimalShownCampaigns && (