-
-
Notifications
You must be signed in to change notification settings - Fork 402
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
228 additions
and
40 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)]), | ||
]); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}`); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
76
test/playwright/integration/12-contributionFlow.donate.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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."'); | ||
}); |
10 changes: 4 additions & 6 deletions
10
test/playwright/16-tiers-page.spec.ts → ...ywright/integration/16-tiers-page.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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`); | ||
}); |