Skip to content

Commit

Permalink
test(Playwright): add more coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
Betree committed Jan 4, 2024
1 parent a27b4f8 commit 10abc54
Show file tree
Hide file tree
Showing 9 changed files with 219 additions and 39 deletions.
1 change: 1 addition & 0 deletions components/contribution-flow/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -887,6 +887,7 @@ class ContributionFlow extends React.Component {
const currency = tier?.amount.currency || collective.currency;
const currentStepName = this.getCurrentStepName();

console.log({ currentStepName });

Check failure on line 890 in components/contribution-flow/index.js

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement
if (currentStepName === STEPS.SUCCESS) {
return <ContributionFlowSuccess collective={collective} />;
}
Expand Down
33 changes: 1 addition & 32 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ export default defineConfig({
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
baseURL: 'http://localhost:3000',

/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
Expand All @@ -36,36 +35,6 @@ export default defineConfig({
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},

// {
// name: 'firefox',
// use: { ...devices['Desktop Firefox'] },
// },
//
// {
// name: 'webkit',
// use: { ...devices['Desktop Safari'] },
// },

/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },

/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],

/* Run your local dev server before starting the tests */
Expand Down
7 changes: 6 additions & 1 deletion scripts/run_e2e_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,12 @@ wait_for_service PDF 127.0.0.1 3002
echo ""
if [ "$USE_PLAYWRIGHT" = "true" ]; then
echo "> Running playwright tests"
npx playwright test test/playwright/*.spec.ts
pwd
ls
ls test
ls test/playwright
ls test/playwright/integration
npx playwright test test/playwright/integration/*.spec.ts
else
echo "> Running cypress tests"
npm run cypress:run -- ${CYPRESS_RECORD} --env OC_ENV=$OC_ENV --spec "test/cypress/integration/${CYPRESS_TEST_FILES}"
Expand Down
70 changes: 70 additions & 0 deletions test/playwright/commands/authentication.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Readable } from 'stream';

import fetch from 'node-fetch';
import { Page } from 'playwright/test';

import { loggedInUserQuery } from '../../../lib/graphql/v1/queries';

import { randomEmail } from '../../cypress/support/faker';

const baseURL = 'http://localhost:3000'; // TODO: Get this to config

const graphqlQueryV1 = (body: any, token: string) => {
return fetch(`${baseURL}/api/graphql/v1`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: !token ? undefined : `Bearer ${token}`,
},
body: Readable.from([JSON.stringify(body)]),
});
};

const signinRequest = (user, redirect: string, sendLink: boolean = false) => {
return fetch(`${baseURL}/api/users/signin`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: Readable.from([JSON.stringify({ user, redirect, createProfile: true, sendLink })]),
});
};

const getLoggedInUserFromToken = async (token: string): Promise<any> => {
const response = await graphqlQueryV1(
{ operationName: 'LoggedInUser', query: loggedInUserQuery.loc.source.body, variables: {} },
token,
);
const result = await response.json();
return result.data.LoggedInUser;
};

function getTokenFromRedirectUrl(url) {
const regex = /\/signin\/([^?]+)/;
return regex.exec(url)[1];
}

/**
* Signup with the given params and redirect to the provided URL
*/
export const signup = async (
page: Page,
{
user,
redirect = '/',
}: {
user?: { email?: string; name?: string };
redirect?: string;
} = {},
) => {
const email = user?.email || randomEmail();
const relativeRedirect = redirect.startsWith(baseURL) ? redirect.replace(baseURL, '') : redirect;
const response = await signinRequest({ ...user, email }, relativeRedirect);
const result = await response.json();
const signInRedirectUrl = result.redirect;
const token = getTokenFromRedirectUrl(signInRedirectUrl);
await page.goto(signInRedirectUrl); // TODO: Rather than redirecting, we could exchange the token directly
return getLoggedInUserFromToken(token);
};
20 changes: 20 additions & 0 deletions test/playwright/commands/contribution-flow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Page } from 'playwright/test';

export const checkStepsProgress = async (
page: Page,
{
enabled = [],
disabled = [],
}: {
enabled?: string | string[];
disabled?: string | string[];
},
) => {
const isEnabled = step => page.waitForSelector(`[data-cy="progress-step-${step}"][data-disabled=false]`);
const isDisabled = step => page.waitForSelector(`[data-cy="progress-step-${step}"][data-disabled=true]`);

await Promise.all([
...(Array.isArray(enabled) ? enabled.map(isEnabled) : [isEnabled(enabled)]),
...(Array.isArray(disabled) ? disabled.map(isDisabled) : [isDisabled(disabled)]),
]);
};
20 changes: 20 additions & 0 deletions test/playwright/commands/mock-now.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Page } from 'playwright/test';

export const mockNow = async (page: Page, now: number) => {
await page.addInitScript(`{
// Extend Date constructor to default to now
Date = class extends Date {
constructor(...args) {
if (args.length === 0) {
super(${now});
} else {
super(...args);
}
}
}
// Override Date.now() to start from now
const __DateNowOffset = ${now} - Date.now();
const __DateNow = Date.now;
Date.now = () => __DateNow() + __DateNowOffset;
}`);
};
21 changes: 21 additions & 0 deletions test/playwright/commands/stripe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Page } from 'playwright/test';

import { CreditCards } from '../../stripe-helpers';

type FillStripeInputOptions = {
card?: {
creditCardNumber?: string;
expirationDate?: string;
cvcCode?: string;
postalCode?: string;
};
};

export const fillStripeInput = async (page: Page, { card = CreditCards.CARD_DEFAULT }: FillStripeInputOptions = {}) => {
const stripeIframeSelector = '.__PrivateStripeElement iframe';
const stripeFrame = page.frameLocator(stripeIframeSelector).first();
card.creditCardNumber && (await stripeFrame.locator('[placeholder="Card number"]').fill(card.creditCardNumber));
card.expirationDate && (await stripeFrame.locator('[placeholder="MM / YY"]').fill(card.expirationDate));
card.cvcCode && (await stripeFrame.locator('[placeholder="CVC"]').fill(card.cvcCode));
card.postalCode && (await stripeFrame.locator('[placeholder="ZIP"]').fill(card.postalCode));
};
76 changes: 76 additions & 0 deletions test/playwright/integration/12-contributionFlow.donate.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { expect, test } from '@playwright/test';

import { signup } from '../commands/authentication';
import { checkStepsProgress } from '../commands/contribution-flow';
import { mockNow } from '../commands/mock-now';
import { fillStripeInput } from '../commands/stripe';

const donateUrl = `/apex/donate`;

test('Can donate as new user', async ({ page }) => {
// Mock clock so we can check next contribution date in a consistent way
await mockNow(page, Date.parse('2042/05/25').valueOf());

const userParams = { name: 'Donate Tester' };
const user = await signup(page, { redirect: donateUrl, user: userParams });

// General checks
await expect(page).toHaveTitle('Contribute to APEX - Open Collective');
await expect(page.locator('link[rel="canonical"]')).toHaveAttribute('href', donateUrl);

// ---- Step Details ----
// Has default amount selected
await page.waitForSelector('#amount button.selected');

// Change amount
await page.click('[data-cy="amount-picker-btn-other"]');
await page.fill('input[type=number][name=custom-amount]', '1337');
await page.waitForSelector('[data-cy="progress-step-details"] :text("1,337.00")');

// Change frequency - monthly
await page.click('text="Monthly"');
await page.waitForSelector('[data-cy="progress-step-details"] :text("1,337.00 USD / mo.")');
await page.waitForSelector(':has-text("the next charge will be on July 1, 2042")');

// Change frequency - yearly
await page.click('text="Yearly"');
await page.waitForSelector('[data-cy="progress-step-details"] :text("1,337.00 USD / yr.")');
await page.waitForSelector('text="Today\'s charge"');
await page.waitForSelector(':has-text("the next charge will be on May 1, 2043")');

// Click the button
await page.click('button[data-cy="cf-next-step"]');

// ---- Step profile ----
await checkStepsProgress(page, { enabled: ['profile', 'details'], disabled: 'payment' });

// Personal account must be the first entry, and it must be checked
await page.waitForSelector(`[data-cy="contribute-profile-picker"] :text("${user.collective.name}")`);
await page.waitForSelector('[data-cy="contribute-profile-picker"] :text("Personal")');
await page.click('[data-cy="contribute-profile-picker"]');
await page.waitForSelector(`[data-cy="select-option"]:first-of-type :text("${user.collective.name}")`);
await page.waitForSelector('[data-cy="select-option"]:first-of-type :text("Personal")');
await page.keyboard.press('Escape');

// User profile is shown on step, all other steps must be disabled
await page.waitForSelector(`[data-cy="progress-step-profile"] :text("${user.collective.name}")`);
await page.click('button[data-cy="cf-next-step"]');

// ---- Step Payment ----
await checkStepsProgress(page, { enabled: ['profile', 'details', 'payment'] });

// As this is a new account, no payment method is configured yet, so
// we should have the credit card form selected by default.
await page.waitForSelector('input[type=checkbox][name=save]:checked', { state: 'hidden' }); // Our checkbox has custom styles and the input is hidden

// Ensure we display errors
await fillStripeInput(page, { card: { creditCardNumber: '123' } });
await page.click('button:has-text("Contribute $1,337")');
await page.waitForSelector('text="Credit card ZIP code and CVC are required"');

// Submit with valid credit card
await fillStripeInput(page);
await page.click('button:has-text("Contribute $1,337")');
await page.waitForURL(`**/apex/donate/success**`, { timeout: 10000 });
await page.waitForSelector('text="You are now supporting APEX."');
});
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import { expect, test } from '@playwright/test';

const baseUrl: String = 'http://localhost:3000';

test('Can be accessed from "/collective/contribute" (default)', async ({ page }) => {
await page.goto(`${baseUrl}/apex/contribute`);
await page.goto(`/apex/contribute`);
await expect(page).toHaveTitle('Contribute to APEX - Open Collective');
await expect(page.locator('link[rel="canonical"]')).toHaveAttribute('href', `${baseUrl}/apex/contribute`);
await expect(page.locator('link[rel="canonical"]')).toHaveAttribute('href', `/apex/contribute`);
});

test('Can be accessed from "/collective/tiers"', async ({ page }) => {
await page.goto(`${baseUrl}/apex/tiers`);
await page.goto(`/apex/tiers`);
await expect(page).toHaveTitle('Contribute to APEX - Open Collective');
await expect(page.locator('link[rel="canonical"]')).toHaveAttribute('href', `${baseUrl}/apex/contribute`);
await expect(page.locator('link[rel="canonical"]')).toHaveAttribute('href', `/apex/contribute`);
});

0 comments on commit 10abc54

Please sign in to comment.