Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/123done/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@
<a class="btn btn-persona btn-subscribe" data-sp3="sp3-1m-gb">
SP3 - Sub to Pro 1m (EN GB)
</a>
<a class="btn btn-persona btn-subscribe" data-sp3="sp3-plus-1m">
SP3 - Sub to Pro Plus 1m
</a>
</div>
</div>

Expand Down
1 change: 1 addition & 0 deletions packages/123done/static/js/123done.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ $(document).ready(function () {
'sp3-6m': '123donepro/halfyearly/landing',
'sp3-12m': '123donepro/yearly/landing',
'sp3-1m-gb': 'en-GB/123donepro/monthly/landing',
'sp3-plus-1m': '123doneproplus/monthly/landing',
},
};

Expand Down
29 changes: 29 additions & 0 deletions packages/functional-tests/lib/fixtures/standard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export type TestOptions = {
};
export type WorkerOptions = { targetName: TargetName; target: ServerTarget };

const CI_WAF_TOKEN = process.env.CI_WAF_TOKEN;

export const test = base.extend<TestOptions, WorkerOptions>({
targetName: ['local', { scope: 'worker', option: true }],

Expand All @@ -47,6 +49,33 @@ export const test = base.extend<TestOptions, WorkerOptions>({
{ scope: 'worker', auto: true },
],

page: async ({ page, target }, use) => {
// Add WAF bypass header only to FXA domains, not external
// services like Stripe/hCaptcha which reject unknown headers
if (CI_WAF_TOKEN) {
const fxaDomains = [
new URL(target.contentServerUrl).host,
new URL(target.authServerUrl).host,
new URL(target.paymentsNextUrl).host,
new URL(target.relierUrl).host,
];
await page.route('**/*', async (route) => {
const url = new URL(route.request().url());
if (fxaDomains.some((domain) => url.host === domain)) {
await route.continue({
headers: {
...route.request().headers(),
'fxa-ci': CI_WAF_TOKEN,
},
});
} else {
await route.continue();
}
});
}
await use(page);
},

pages: async ({ target, page }, use) => {
const pages = createPages(page, target);
await use(pages);
Expand Down
1 change: 1 addition & 0 deletions packages/functional-tests/lib/stripe-test-cards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

export const StripeTestCards = {
SUCCESS: '4242424242424242',
SUCCESS_MASTERCARD: '5555555555554444',
THREE_DS_AUTHENTICATE: '4000000000003220',
DECLINED: '4000000000000002',
INSUFFICIENT_FUNDS: '4000000000009995',
Expand Down
70 changes: 70 additions & 0 deletions packages/functional-tests/pages/payments/cancel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import { Page, expect } from '@playwright/test';
import { BaseTarget } from '../../lib/targets/base';

export class CancelPage {
constructor(
public page: Page,
readonly target: BaseTarget
) {}

// Cancel confirmation page

get cancelHeading() {
return this.page.locator('#cancel-subscription-heading');
}

get confirmCheckbox() {
return this.page.locator('input#cancelAccess[type="checkbox"]');
}

get submitCancelButton() {
return this.page.getByRole('button', {
name: /Cancel your subscription to/i,
});
}

get keepSubscriptionButton() {
return this.page.getByRole('link', { name: /Keep subscription/i });
}

// Already-cancelled state

get expirationDate() {
return this.page.getByText(/You will still have access .* until/);
}

get productName() {
return this.cancelHeading;
}

// Error state

get errorMessage() {
return this.page.locator('[role="alert"]');
}

// Actions

/**
* Check the acknowledge checkbox and click the cancel button.
*/
async confirmAndCancel() {
await expect(this.confirmCheckbox).toBeVisible({ timeout: 10_000 });
await this.confirmCheckbox.click();
await expect(this.confirmCheckbox).toBeChecked();
await this.submitCancelButton.click();
}

/**
* Wait for the already-cancelled confirmation state.
*/
async waitForCancelConfirmation(timeout = 30_000) {
await expect(this.page.getByText(/sorry to see you go/i)).toBeVisible({
timeout,
});
}
}
6 changes: 6 additions & 0 deletions packages/functional-tests/pages/payments/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@

import { Page } from '@playwright/test';
import { BaseTarget } from '../../lib/targets/base';
import { CancelPage } from './cancel';
import { CheckoutPage } from './checkout';
import { ManagePage } from './manage';
import { StaySubscribedPage } from './stay-subscribed';

export function create(page: Page, target: BaseTarget) {
return {
cancel: new CancelPage(page, target),
checkout: new CheckoutPage(page, target),
manage: new ManagePage(page, target),
staySubscribed: new StaySubscribedPage(page, target),
};
}
106 changes: 106 additions & 0 deletions packages/functional-tests/pages/payments/manage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import { Page, expect } from '@playwright/test';
import { BaseTarget } from '../../lib/targets/base';

export class ManagePage {
constructor(
public page: Page,
readonly target: BaseTarget
) {}

// Page heading

get subscriptionHeading() {
return this.page.locator('#subscription-management');
}

// Active subscriptions section

get activeSubscriptionsList() {
return this.page.locator('ul[aria-label="Your active subscriptions"]');
}

getSubscriptionCard(productName: string) {
return this.page.locator(
`li[aria-labelledby="${productName}-information"]`
);
}

getProductName(productName: string) {
return this.page.locator(`#${productName}-information`);
}

getPlanInterval(productName: string) {
return this.getSubscriptionCard(productName).locator('.text-grey-500');
}

// Payment details section

get paymentDetailsSection() {
return this.page.locator('#payment-details');
}

get paymentMethodLastFour() {
return this.paymentDetailsSection.getByText(/Card ending in \d{4}/);
}

get paymentMethodIcon() {
return this.paymentDetailsSection.locator(
'.flex.items-center.gap-2 img'
);
}

get managePaymentButton() {
return this.page.locator(
'a[aria-label="Manage payment method"]'
);
}

// Empty state

get emptyStateMessage() {
return this.page.getByText('You have no active subscriptions');
}

// Subscription actions

cancelButton() {
return this.page.getByRole('link', {
name: /Cancel your subscription to/i,
});
}

staySubscribedButton() {
return this.page.getByRole('link', {
name: /Stay subscribed to/i,
});
}

// Cancelled status

get cancelledStatus() {
return this.page.locator('.text-yellow-800').getByText(/Expires on/);
}

// Actions

/**
* Navigate to the subscriptions landing page, which triggers
* FXA auth and redirects to the manage page.
*/
async goto(locale = 'en') {
await this.page.goto(
`${this.target.paymentsNextUrl}/${locale}/subscriptions/landing`
);
}

/**
* Wait for the manage page to fully load.
*/
async waitForManagePage(timeout = 60_000) {
await expect(this.subscriptionHeading).toBeVisible({ timeout });
}
}
64 changes: 64 additions & 0 deletions packages/functional-tests/pages/payments/stay-subscribed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import { Page, expect } from '@playwright/test';
import { BaseTarget } from '../../lib/targets/base';

export class StaySubscribedPage {
constructor(
public page: Page,
readonly target: BaseTarget
) {}

// Resubscribe page

get staySubscribedHeading() {
return this.page.locator('#stay-subscribed-heading');
}

get productName() {
return this.staySubscribedHeading;
}

get nextChargeAmount() {
return this.page.getByText(/Your next charge will be/);
}

get resubscribeButton() {
return this.page.getByRole('button', { name: /Resubscribe/i });
}

// Success state

get successHeading() {
return this.page.getByText(/Thanks! You.re all set/i);
}

get backToSubscriptionsLink() {
return this.page.getByRole('link', { name: /Back to subscriptions/i });
}

// Error state

get errorMessage() {
return this.page.locator('[role="alert"]');
}

// Actions

/**
* Click the Resubscribe button and wait for success.
*/
async resubscribe() {
await expect(this.resubscribeButton).toBeVisible({ timeout: 10_000 });
await this.resubscribeButton.click();
}

/**
* Wait for the resubscribe success confirmation.
*/
async waitForSuccess(timeout = 30_000) {
await expect(this.successHeading).toBeVisible({ timeout });
}
}
11 changes: 11 additions & 0 deletions packages/functional-tests/pages/relier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,17 @@ export class RelierPage extends BaseLayout {
);
}

// Uses 123doneproplus offering (no free trial) to test that subscription
// appears under "Active subscriptions" on the manage page
async clickSubscribePlusMonthly() {
await this.page
.getByRole('link', { name: 'SP3 - Sub to Pro Plus 1m', exact: true })
.click();
await this.page.waitForURL(
(url) => !url.href.includes(this.target.relierUrl)
);
}

async clickRequire2FA() {
await this.page.getByText('Sign In (Require 2FA)').click();
return this.page.waitForURL(`${this.target.contentServerUrl}/**`);
Expand Down
11 changes: 0 additions & 11 deletions packages/functional-tests/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,6 @@ import { TargetNames } from './lib/targets';
import { getFirefoxUserPrefs } from './lib/targets/firefoxUserPrefs';

const CI = !!process.env.CI;
const CI_WAF_TOKEN = process.env.CI_WAF_TOKEN;

/**
* Returns a header used for WAF bypass.
*
* Requires `CI_WAF_TOKEN` set in CircleCI, or local environment, and corresponding WAF condition set for target rule
*/
function getCIHeader(): Record<string, string> {
return CI_WAF_TOKEN ? { 'fxa-ci': CI_WAF_TOKEN } : {};
}

// If using the CircleCI parallelism feature, assure that the JUNIT XML report
// has a unique name
Expand Down Expand Up @@ -74,7 +64,6 @@ export default defineConfig<PlaywrightTestConfig<TestOptions, WorkerOptions>>({

use: {
viewport: { width: 1280, height: 720 },
extraHTTPHeaders: getCIHeader(),
},
projects: [
...TargetNames.map(
Expand Down
Loading