mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-08-31 01:14:08 +08:00
test(e2e): make donation payment widgets run on every PR (#69477)
This commit is contained in:
@@ -180,7 +180,7 @@ jobs:
|
||||
run: pnpm run seed:certified-user
|
||||
|
||||
- name: Run playwright tests
|
||||
run: pnpm run playwright:run --project=${{ matrix.browsers }} --grep-invert 'third-party-donation.spec.ts'
|
||||
run: pnpm run playwright:run --project=${{ matrix.browsers }}
|
||||
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
name: CI - E2E - 3rd party donation tests
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- 'prod-**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.event.workflow_run.head_branch || github.ref }}
|
||||
cancel-in-progress: ${{ !contains(github.ref, 'main') && !contains(github.ref, 'prod-') }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-client:
|
||||
name: Build Client
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [24]
|
||||
steps:
|
||||
- name: Checkout Source Files
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Checkout client-config
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
repository: freeCodeCamp/client-config
|
||||
path: client-config
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Set freeCodeCamp Environment Variables
|
||||
run: |
|
||||
sed '/STRIPE/d; /PAYPAL/d; /PATREON/d;' sample.env > .env
|
||||
echo 'STRIPE_PUBLIC_KEY=${{ secrets.STRIPE_PUBLIC_KEY }}' >> .env
|
||||
echo 'PAYPAL_CLIENT_ID=${{ secrets.PAYPAL_CLIENT_ID }}' >> .env
|
||||
echo 'PATREON_CLIENT_ID=${{ secrets.PATREON_CLIENT_ID }}' >> .env
|
||||
|
||||
- name: Install and Build
|
||||
run: |
|
||||
pnpm install
|
||||
pnpm run build
|
||||
|
||||
- name: Move serve.json to Public Folder
|
||||
run: cp client-config/serve.json client/public/serve.json
|
||||
|
||||
- name: Tar Files
|
||||
run: tar -cf client-artifact.tar client/public
|
||||
|
||||
- name: Upload Client Artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: client-artifact
|
||||
path: client-artifact.tar
|
||||
|
||||
build-api:
|
||||
name: Build API (Container)
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout Source Files
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Create Image
|
||||
run: |
|
||||
docker build \
|
||||
-t fcc-api \
|
||||
-f docker/api/Dockerfile .
|
||||
|
||||
- name: Save Image
|
||||
run: docker save fcc-api > api-artifact.tar
|
||||
|
||||
- name: Upload API Artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: api-artifact
|
||||
path: api-artifact.tar
|
||||
|
||||
playwright-run-api:
|
||||
name: Run Playwright 3rd Party Donation Tests
|
||||
runs-on: ubuntu-24.04
|
||||
needs: [build-client, build-api]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
browsers: [chromium]
|
||||
node-version: [24]
|
||||
steps:
|
||||
- name: Set Action Environment Variables
|
||||
run: |
|
||||
echo "GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout Source Files
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
|
||||
- name: Unpack Client Artifact
|
||||
run: |
|
||||
tar -xf client-artifact/client-artifact.tar
|
||||
rm client-artifact/client-artifact.tar
|
||||
|
||||
- name: Load API Image
|
||||
run: |
|
||||
docker load < api-artifact/api-artifact.tar
|
||||
rm api-artifact/api-artifact.tar
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Install Dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Set freeCodeCamp Environment Variables (needed by api)
|
||||
run: |
|
||||
sed '/STRIPE/d; /PAYPAL/d; /PATREON/d;' sample.env > .env
|
||||
echo 'STRIPE_PUBLIC_KEY=${{ secrets.STRIPE_PUBLIC_KEY }}' >> .env
|
||||
echo 'PAYPAL_CLIENT_ID=${{ secrets.PAYPAL_CLIENT_ID }}' >> .env
|
||||
echo 'PATREON_CLIENT_ID=${{ secrets.PATREON_CLIENT_ID }}' >> .env
|
||||
|
||||
- name: Install playwright dependencies
|
||||
run: npx playwright install --with-deps
|
||||
|
||||
- name: Install
|
||||
run: pnpm install
|
||||
|
||||
- name: Start apps
|
||||
run: |
|
||||
docker compose -f docker/docker-compose.yml -f docker/docker-compose.e2e.yml up -d
|
||||
pnpm run serve:client-ci &
|
||||
sleep 10
|
||||
|
||||
- name: Seed Database with Certified User
|
||||
run: pnpm run seed:certified-user
|
||||
|
||||
- name: Run playwright tests
|
||||
run: pnpm run playwright:run third-party-donation.spec.ts --project=${{ matrix.browsers }}
|
||||
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: playwright-report-${{ matrix.browsers }}
|
||||
path: e2e/playwright/reporter
|
||||
retention-days: 7
|
||||
@@ -1,48 +0,0 @@
|
||||
{
|
||||
"id": "pm_1PRWUAFI1KEgscdTFyjUCbcd",
|
||||
"object": "payment_method",
|
||||
"allow_redisplay": "unspecified",
|
||||
"billing_details": {
|
||||
"address": {
|
||||
"city": null,
|
||||
"country": null,
|
||||
"line1": null,
|
||||
"line2": null,
|
||||
"postal_code": null,
|
||||
"state": null
|
||||
},
|
||||
"email": null,
|
||||
"name": null,
|
||||
"phone": null
|
||||
},
|
||||
"card": {
|
||||
"brand": "visa",
|
||||
"checks": {
|
||||
"address_line1_check": null,
|
||||
"address_postal_code_check": null,
|
||||
"cvc_check": null
|
||||
},
|
||||
"country": "US",
|
||||
"display_brand": "visa",
|
||||
"exp_month": 10,
|
||||
"exp_year": 2025,
|
||||
"funding": "credit",
|
||||
"generated_from": null,
|
||||
"last4": "4242",
|
||||
"networks": {
|
||||
"available": [
|
||||
"visa"
|
||||
],
|
||||
"preferred": null
|
||||
},
|
||||
"three_d_secure_usage": {
|
||||
"supported": true
|
||||
},
|
||||
"wallet": null
|
||||
},
|
||||
"created": 1718357514,
|
||||
"customer": null,
|
||||
"livemode": false,
|
||||
"radar_options": {},
|
||||
"type": "card"
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { test, expect, type Page } from '@playwright/test';
|
||||
|
||||
import translations from '../client/i18n/locales/english/translations.json';
|
||||
|
||||
// Unlike PayPal and Stripe, Patreon isn't a third-party SDK integration - it's
|
||||
// a plain link built from static config, so it can't crash the page the way
|
||||
// a broken third-party script can. Still worth a basic presence/href check so
|
||||
// a regression in the URL-building logic itself doesn't go unnoticed.
|
||||
async function expectPatreonButtonToBeVisible(page: Page) {
|
||||
await page
|
||||
.getByRole('button', { name: translations.buttons.donate, exact: true })
|
||||
.click();
|
||||
|
||||
const patreonButton = page.locator('.patreon-button');
|
||||
await expect(patreonButton).toBeVisible();
|
||||
await expect(patreonButton).toHaveAttribute(
|
||||
'href',
|
||||
/^https:\/\/www\.patreon\.com\/oauth2\/become-patron\?/
|
||||
);
|
||||
}
|
||||
|
||||
test.describe('Patreon donation button', () => {
|
||||
test.describe('Authenticated User', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/donate');
|
||||
});
|
||||
|
||||
test('renders the Patreon button', async ({ page }) => {
|
||||
await expectPatreonButtonToBeVisible(page);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Unauthenticated User', () => {
|
||||
test.use({ storageState: { cookies: [], origins: [] } });
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/donate');
|
||||
});
|
||||
|
||||
test('renders the Patreon button', async ({ page }) => {
|
||||
await expectPatreonButtonToBeVisible(page);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,135 @@
|
||||
import { test, expect, type Page } from '@playwright/test';
|
||||
|
||||
import translations from '../client/i18n/locales/english/translations.json';
|
||||
|
||||
async function clickDonate(page: Page) {
|
||||
await page
|
||||
.getByRole('button', { name: translations.buttons.donate, exact: true })
|
||||
.click();
|
||||
}
|
||||
|
||||
async function expectRealPaypalButtonToBeVisible(page: Page) {
|
||||
await clickDonate(page);
|
||||
|
||||
// The widget can also render a "Debit or Credit Card" funding option
|
||||
// alongside the PayPal one, so the PayPal link must be targeted by name.
|
||||
const paypalButtonIframe = page.frameLocator('.component-frame');
|
||||
const paypalButton = paypalButtonIframe.getByRole('link', {
|
||||
name: 'PayPal'
|
||||
});
|
||||
await expect(paypalButton).toBeVisible();
|
||||
await expect(paypalButton).toHaveAttribute('aria-label', 'PayPal');
|
||||
}
|
||||
|
||||
// PayPal's SDK is consumed two different ways across the codebase's history:
|
||||
// the legacy \`Buttons.driver('react', { React, ReactDOM })\` API (which
|
||||
// returns a React component, and is what production currently uses), and
|
||||
// the plain \`Buttons({...}).render(container)\` API (imperative, no React
|
||||
// involved). This fake supports both call shapes so the test keeps working
|
||||
// across that implementation detail.
|
||||
const FAKE_APPROVE_DATA = `{ orderID: 'FAKE_ORDER_ID', subscriptionID: 'FAKE_SUBSCRIPTION_ID' }`;
|
||||
const FAKE_APPROVE_ACTIONS = `{ order: { capture: function () { return Promise.resolve({}); } } }`;
|
||||
const FAKE_PAYPAL_SDK = `
|
||||
function FakeButtons(options) {
|
||||
return {
|
||||
render: function (container) {
|
||||
var btn = document.createElement('button');
|
||||
btn.textContent = 'Fake PayPal Approve';
|
||||
// This project configures a custom testIdAttribute in
|
||||
// playwright.config.ts, so getByTestId() looks for this attribute,
|
||||
// not the Playwright default "data-testid".
|
||||
btn.setAttribute('data-playwright-test-label', 'fake-paypal-approve');
|
||||
btn.onclick = function () {
|
||||
options.onApprove(${FAKE_APPROVE_DATA}, ${FAKE_APPROVE_ACTIONS});
|
||||
};
|
||||
container.appendChild(btn);
|
||||
},
|
||||
close: function () { return Promise.resolve(); }
|
||||
};
|
||||
}
|
||||
FakeButtons.driver = function (reactString, deps) {
|
||||
return function FakeDriverButton(props) {
|
||||
return deps.React.createElement('button', {
|
||||
'data-playwright-test-label': 'fake-paypal-approve',
|
||||
onClick: function () {
|
||||
props.onApprove(${FAKE_APPROVE_DATA}, ${FAKE_APPROVE_ACTIONS});
|
||||
}
|
||||
}, 'Fake PayPal Approve');
|
||||
};
|
||||
};
|
||||
window.paypal = {
|
||||
Buttons: FakeButtons
|
||||
};
|
||||
`;
|
||||
|
||||
async function mockPaypalSdk(page: Page) {
|
||||
await page.route('https://www.paypal.com/sdk/js**', route =>
|
||||
route.fulfill({
|
||||
contentType: 'application/javascript',
|
||||
body: FAKE_PAYPAL_SDK
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async function expectDonationToComplete(page: Page) {
|
||||
await clickDonate(page);
|
||||
await page.getByTestId('fake-paypal-approve').click();
|
||||
|
||||
await expect(page.getByText(translations.donate['thank-you'])).toBeVisible();
|
||||
await expect(page.getByText(translations.donate['free-tech'])).toBeVisible();
|
||||
}
|
||||
|
||||
test.describe('PayPal donation button', () => {
|
||||
test.describe('Authenticated User', () => {
|
||||
test('renders the real PayPal button', async ({ page }) => {
|
||||
await page.goto('/donate');
|
||||
await expectRealPaypalButtonToBeVisible(page);
|
||||
});
|
||||
|
||||
test('completes the donation flow once PayPal approves the payment', async ({
|
||||
page
|
||||
}) => {
|
||||
// Register the mock before navigating, so PayPal's real script is
|
||||
// never requested at all.
|
||||
let addDonationCalled = false;
|
||||
await page.route('**/donate/add-donation', route => {
|
||||
addDonationCalled = true;
|
||||
return route.fulfill({ json: {} });
|
||||
});
|
||||
await mockPaypalSdk(page);
|
||||
|
||||
await page.goto('/donate');
|
||||
await expectDonationToComplete(page);
|
||||
|
||||
expect(addDonationCalled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Unauthenticated User', () => {
|
||||
test.use({ storageState: { cookies: [], origins: [] } });
|
||||
|
||||
test('renders the real PayPal button', async ({ page }) => {
|
||||
await page.goto('/donate');
|
||||
await expectRealPaypalButtonToBeVisible(page);
|
||||
});
|
||||
|
||||
test('completes the donation flow once PayPal approves the payment', async ({
|
||||
page
|
||||
}) => {
|
||||
// Unauthenticated donors are never recorded via /donate/add-donation
|
||||
// (see donation-saga.js), so completion here should not call our API
|
||||
// at all - it should go straight to the success screen.
|
||||
let addDonationCalled = false;
|
||||
await page.route('**/donate/add-donation', route => {
|
||||
addDonationCalled = true;
|
||||
return route.fulfill({ json: {} });
|
||||
});
|
||||
await mockPaypalSdk(page);
|
||||
|
||||
await page.goto('/donate');
|
||||
await expectDonationToComplete(page);
|
||||
|
||||
expect(addDonationCalled).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,173 @@
|
||||
import { test, expect, type Page } from '@playwright/test';
|
||||
|
||||
import translations from '../client/i18n/locales/english/translations.json';
|
||||
import { alertToBeVisible } from './utils/alerts';
|
||||
|
||||
async function clickDonate(page: Page) {
|
||||
await page
|
||||
.getByRole('button', { name: translations.buttons.donate, exact: true })
|
||||
.click();
|
||||
}
|
||||
|
||||
async function fillTestCard(page: Page, cardNumber: string) {
|
||||
const cardNumberIframe = page
|
||||
.locator('iframe[src*="elements-inner-card"]')
|
||||
.nth(0)
|
||||
.contentFrame();
|
||||
const cardExpiryIframe = page
|
||||
.locator('iframe[src*="elements-inner-card"]')
|
||||
.nth(1)
|
||||
.contentFrame();
|
||||
|
||||
await cardNumberIframe
|
||||
.locator('input[data-elements-stable-field-name="cardNumber"]')
|
||||
.fill(cardNumber);
|
||||
// Per Stripe's testing guide, any future expiry works with test cards.
|
||||
// Tokenization only validates the card is well-formed; declines and other
|
||||
// failure scenarios only surface once a charge is actually attempted, so
|
||||
// the expiry itself is never the point being tested here - it just needs
|
||||
// to stay in the future indefinitely.
|
||||
await cardExpiryIframe
|
||||
.locator('input[data-elements-stable-field-name="cardExpiry"]')
|
||||
.fill('1239');
|
||||
}
|
||||
|
||||
async function expectWalletsToMountWithoutError(page: Page) {
|
||||
const pageErrors: string[] = [];
|
||||
page.on('pageerror', err => pageErrors.push(err.message));
|
||||
|
||||
await clickDonate(page);
|
||||
await expect(page.locator('.wallets-form')).toBeAttached();
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
expect(pageErrors).toEqual([]);
|
||||
}
|
||||
|
||||
test.describe('Stripe donation form', () => {
|
||||
test.describe('Authenticated User', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/donate');
|
||||
});
|
||||
|
||||
test('renders the real Stripe card element', async ({ page }) => {
|
||||
await clickDonate(page);
|
||||
|
||||
await expect(
|
||||
page.getByText(translations.donate['or-card'])
|
||||
).toBeVisible();
|
||||
|
||||
const cardNumberIframe = page
|
||||
.locator('iframe[src*="elements-inner-card"]')
|
||||
.nth(0)
|
||||
.contentFrame();
|
||||
await expect(
|
||||
cardNumberIframe.locator(
|
||||
'input[data-elements-stable-field-name="cardNumber"]'
|
||||
)
|
||||
).toBeVisible();
|
||||
|
||||
const cardExpiryIframe = page
|
||||
.locator('iframe[src*="elements-inner-card"]')
|
||||
.nth(1)
|
||||
.contentFrame();
|
||||
await expect(
|
||||
cardExpiryIframe.locator(
|
||||
'input[data-elements-stable-field-name="cardExpiry"]'
|
||||
)
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('mounts the wallets (Apple/Google Pay) integration without erroring', async ({
|
||||
page
|
||||
}) => {
|
||||
await expectWalletsToMountWithoutError(page);
|
||||
});
|
||||
|
||||
test('completes a donation with a real (test-mode) card', async ({
|
||||
page
|
||||
}) => {
|
||||
await clickDonate(page);
|
||||
|
||||
// 4242424242424242 is Stripe's well-known always-succeeds test card
|
||||
// number. We let this hit Stripe's real test-mode API for
|
||||
// tokenization (per Stripe's testing guide, this is what test mode is
|
||||
// for) rather than mocking it - only our own backend, which we don't
|
||||
// have test credentials for here, is mocked.
|
||||
await fillTestCard(page, '4242424242424242');
|
||||
|
||||
await page.route(
|
||||
new URL(
|
||||
'donate/charge-stripe-card',
|
||||
process.env.API_LOCATION
|
||||
).toString(),
|
||||
route => route.fulfill({ json: { isDonating: true } })
|
||||
);
|
||||
|
||||
await clickDonate(page);
|
||||
|
||||
await expect(page.getByRole('alert')).toBeVisible();
|
||||
await alertToBeVisible(page, translations.donate['free-tech']);
|
||||
await alertToBeVisible(page, translations.donate['visit-supporters']);
|
||||
});
|
||||
|
||||
test('shows an error when the charge is declined', async ({ page }) => {
|
||||
await clickDonate(page);
|
||||
|
||||
// Per Stripe's testing guide, tokenization (createPaymentMethod)
|
||||
// succeeds for any well-formed test card - card-specific decline
|
||||
// behavior (e.g. 4000000000000002, a generic decline) only surfaces
|
||||
// when a charge is actually attempted, which happens server-side in
|
||||
// our own backend. So the decline itself has to be simulated via our
|
||||
// backend's response, not by choosing a particular "decline" test
|
||||
// card number here.
|
||||
await fillTestCard(page, '4242424242424242');
|
||||
|
||||
await page.route(
|
||||
new URL(
|
||||
'donate/charge-stripe-card',
|
||||
process.env.API_LOCATION
|
||||
).toString(),
|
||||
route =>
|
||||
route.fulfill({
|
||||
json: { error: { type: 'GenericDecline' } }
|
||||
})
|
||||
);
|
||||
|
||||
await clickDonate(page);
|
||||
|
||||
await expect(page.getByRole('alert')).toBeVisible();
|
||||
await alertToBeVisible(page, translations.donate.error);
|
||||
await alertToBeVisible(page, translations.donate['error-2']);
|
||||
await expect(
|
||||
page.getByRole('button', { name: translations.buttons['try-again'] })
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Unauthenticated User', () => {
|
||||
test.use({ storageState: { cookies: [], origins: [] } });
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/donate');
|
||||
});
|
||||
|
||||
test('does not render the Stripe card element (PayPal handles card payment instead)', async ({
|
||||
page
|
||||
}) => {
|
||||
await clickDonate(page);
|
||||
|
||||
await expect(
|
||||
page.getByText(translations.donate['or-card'])
|
||||
).not.toBeVisible();
|
||||
await expect(
|
||||
page.locator('iframe[src*="elements-inner-card"]')
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('mounts the wallets (Apple/Google Pay) integration without erroring', async ({
|
||||
page
|
||||
}) => {
|
||||
await expectWalletsToMountWithoutError(page);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,72 +0,0 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
import translations from '../client/i18n/locales/english/translations.json';
|
||||
import stripeJson from './fixtures/donation/stripe.json';
|
||||
import { alertToBeVisible } from './utils/alerts';
|
||||
|
||||
test.describe('third-party donation tests', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/donate');
|
||||
});
|
||||
|
||||
test('All elements are present in the widget', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Donate', exact: true }).click();
|
||||
|
||||
// Paypal button should be present
|
||||
|
||||
const paypalButtonIframe = page.frameLocator('.component-frame');
|
||||
const paypalButton = paypalButtonIframe.getByRole('link');
|
||||
await expect(paypalButton).toBeVisible();
|
||||
await expect(paypalButton).toHaveAttribute('aria-label', 'PayPal');
|
||||
|
||||
// Patreon button should be present
|
||||
|
||||
const patreonButton = page.locator('.patreon-button');
|
||||
await expect(patreonButton).toBeVisible();
|
||||
|
||||
// "Or dontate with card" button should be present
|
||||
|
||||
await expect(page.getByText('Or donate with card')).toBeVisible();
|
||||
});
|
||||
|
||||
test('It is possible to donate with a card', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Donate', exact: true }).click();
|
||||
|
||||
const cardNumberIframe = page
|
||||
.frameLocator('iframe[src*="elements-inner-card"]')
|
||||
.nth(0);
|
||||
|
||||
const cardExpiryIframe = page
|
||||
.frameLocator('iframe[src*="elements-inner-card"]')
|
||||
.nth(1);
|
||||
|
||||
await cardNumberIframe
|
||||
.locator('input[data-elements-stable-field-name="cardNumber"]')
|
||||
.fill('4242424242424242');
|
||||
|
||||
await cardExpiryIframe
|
||||
.locator('input[data-elements-stable-field-name="cardExpiry"]')
|
||||
.fill('1025');
|
||||
|
||||
await page.getByRole('button', { name: 'Donate', exact: true }).click();
|
||||
|
||||
await page.route(
|
||||
'https://api.stripe.com/v1/payment_methods',
|
||||
async route => {
|
||||
await route.fulfill({ json: stripeJson });
|
||||
}
|
||||
);
|
||||
|
||||
await page.route(
|
||||
new URL('donate/charge-stripe-card', process.env.API_LOCATION).toString(),
|
||||
async route => {
|
||||
await route.fulfill({ json: { isDonating: true } });
|
||||
}
|
||||
);
|
||||
|
||||
await expect(page.getByRole('alert')).toBeVisible();
|
||||
|
||||
await alertToBeVisible(page, translations.donate['free-tech']);
|
||||
await alertToBeVisible(page, translations.donate['visit-supporters']);
|
||||
});
|
||||
});
|
||||
+3
-3
@@ -26,17 +26,17 @@ ALGOLIA_APP_ID=app_id_from_algolia_dashboard
|
||||
ALGOLIA_API_KEY=api_key_from_algolia_dashboard
|
||||
|
||||
# Stripe
|
||||
STRIPE_PUBLIC_KEY=pk_from_stripe_dashboard
|
||||
STRIPE_PUBLIC_KEY=pk_test_TYooMQauvdEDq54NiTphI7jx
|
||||
STRIPE_SECRET_KEY=sk_from_stripe_dashboard
|
||||
|
||||
# Third-party App API
|
||||
TPA_API_BEARER_TOKEN=tpa_api_bearer_token_from_dashboard
|
||||
|
||||
# PayPal
|
||||
PAYPAL_CLIENT_ID=id_from_paypal_dashboard
|
||||
PAYPAL_CLIENT_ID=sb
|
||||
|
||||
# Patreon
|
||||
PATREON_CLIENT_ID=id_from_patreon_dashboard
|
||||
PATREON_CLIENT_ID=local-dev-patreon-client-id
|
||||
|
||||
# Analytics
|
||||
GROWTHBOOK_URI=api_URI_from_Growthbook_dashboard
|
||||
|
||||
Reference in New Issue
Block a user