Skip to content
Open
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
2 changes: 2 additions & 0 deletions packages/functional-tests/pages/payments/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
import { Page } from '@playwright/test';
import { BaseTarget } from '../../lib/targets/base';
import { CheckoutPage } from './checkout';
import { UpgradePage } from './upgrade';

export function create(page: Page, target: BaseTarget) {
return {
checkout: new CheckoutPage(page, target),
upgrade: new UpgradePage(page, target),
};
}
114 changes: 114 additions & 0 deletions packages/functional-tests/pages/payments/upgrade.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/* 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 UpgradePage {
constructor(
public page: Page,
readonly target: BaseTarget
) {}

// Start page — upgrade section

get upgradeSection() {
return this.page.getByTestId('subscription-upgrade');
}

get proratedAmount() {
return this.page.getByTestId('amount-due');
}

get acknowledgmentText() {
return this.page.getByTestId('sub-update-acknowledgment');
}

// Consent and submit (shared with checkout via CheckoutForm)

get consentCheckbox() {
return this.page.locator(
'input[type="checkbox"][name="confirm"][aria-required]'
);
}

get confirmUpgradeButton() {
return this.page.getByRole('button', { name: /Subscribe Now/i });
}

// Success page

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

get invoiceNumber() {
return this.page.getByText(/Invoice #/);
}

get successActionButton() {
return this.page
.locator('section[aria-labelledby="subscription-confirmation-heading"]')
.locator('a[role="button"]');
}

// Error page (checkout error page handles eligibility errors)

get eligibilityError() {
return this.page.locator('#page-information-heading');
}

get errorButton() {
return this.page.getByRole('link', {
name: /Contact Support|Manage my subscription|Try again/i,
});
}

// Actions

/**
* Check the consent checkbox for upgrade. Waits for the checkbox to
* become interactive before clicking.
*/
async checkConsent() {
await expect(this.consentCheckbox).toBeVisible({ timeout: 10_000 });
await expect(this.consentCheckbox).not.toHaveAttribute(
'aria-disabled',
'true',
{ timeout: 10_000 }
);
await this.consentCheckbox.click();
await expect(this.consentCheckbox).toBeChecked();
}

/**
* Click the Subscribe Now button to confirm upgrade. Waits for it to
* be enabled (aria-disabled="false") before clicking.
*/
async confirmUpgrade() {
await expect(this.confirmUpgradeButton).toHaveAttribute(
'aria-disabled',
'false',
{ timeout: 30_000 }
);
await this.confirmUpgradeButton.click();
}

/**
* Wait for the upgrade success page heading to become visible.
*/
async waitForSuccess(timeout = 60_000) {
await expect(this.page).toHaveURL(/success/, { timeout });
await expect(this.successHeading).toBeVisible({ timeout: 30_000 });
}

/**
* Wait for the eligibility error page to become visible.
* Used for duplicate subscription and downgrade blocked scenarios.
*/
async waitForEligibilityError(timeout = 60_000) {
await expect(this.page).toHaveURL(/error/, { timeout });
await expect(this.eligibilityError).toBeVisible({ timeout: 10_000 });
}
}
175 changes: 175 additions & 0 deletions packages/functional-tests/tests-payments-next/upgrade.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
/* 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 { expect, test } from '../lib/fixtures/payments';
import { StripeTestCards } from '../lib/stripe-test-cards';

// Upgrade tests are resource-intensive (require an existing subscription,
// then trigger the upgrade flow). Run serially to avoid overwhelming
// local services and Stripe rate limits.
test.describe.configure({ mode: 'serial' });

test.describe('severity-1 #smoke', () => {
test.setTimeout(240_000);
test.use({ viewport: { width: 1280, height: 1080 } });

test.beforeEach(async ({}, { project }) => {
test.skip(
project.name.includes('production'),
'Upgrade smoke tests are not run in production'
);
});

test('Upgrade happy path — lower-tier to higher-tier with prorated price', async ({
target,
page,
pages: { relier, signin },
paymentPages: { checkout, upgrade },
testAccountTracker,
}) => {
const credentials = await testAccountTracker.signUp();

// Step 1: Subscribe to the monthly (lower-tier) plan
await relier.goto();
await relier.clickSubscribeMonthly();

await checkout.handleLocationIfNeeded();

await checkout.emailInput.fill(credentials.email);
await checkout.signInContinueButton.click();

await expect(page).toHaveURL(new RegExp(target.contentServerUrl), {
timeout: 30_000,
});
await signin.fillOutPasswordForm(credentials.password);

await expect(checkout.paymentHeading).toBeVisible({ timeout: 30_000 });

await checkout.waitForStripeReady();
await checkout.checkConsent();
await checkout.fillCard(StripeTestCards.SUCCESS);
await checkout.submit();

await checkout.waitForSuccess();
await expect(checkout.successHeading).toContainText('check your email');

// Step 2: Navigate to the 12-month (higher-tier) plan to trigger upgrade
await relier.goto();
await relier.clickSubscribe12Month();

// The system detects the existing subscription and redirects to the
// upgrade flow instead of a new checkout
await expect(upgrade.upgradeSection).toBeVisible({ timeout: 60_000 });
await expect(upgrade.proratedAmount).toBeVisible();
await expect(upgrade.acknowledgmentText).toBeVisible();

// Confirm the upgrade
await upgrade.checkConsent();
await upgrade.confirmUpgrade();

// Verify success
await upgrade.waitForSuccess();
await expect(upgrade.successHeading).toBeVisible();
});
});

test.describe('severity-2', () => {
test.setTimeout(240_000);
test.use({ viewport: { width: 1280, height: 1080 } });

test.beforeEach(async ({}, { project }) => {
test.skip(
project.name.includes('production'),
'Upgrade tests are not run in production'
);
});

test('Duplicate subscription blocked — already subscribed user is rejected', async ({
target,
page,
pages: { relier, signin },
paymentPages: { checkout, upgrade },
testAccountTracker,
}) => {
const credentials = await testAccountTracker.signUp();

// Step 1: Complete initial monthly subscription
await relier.goto();
await relier.clickSubscribeMonthly();

await checkout.handleLocationIfNeeded();

await checkout.emailInput.fill(credentials.email);
await checkout.signInContinueButton.click();

await expect(page).toHaveURL(new RegExp(target.contentServerUrl), {
timeout: 30_000,
});
await signin.fillOutPasswordForm(credentials.password);

await expect(checkout.paymentHeading).toBeVisible({ timeout: 30_000 });

await checkout.waitForStripeReady();
await checkout.checkConsent();
await checkout.fillCard(StripeTestCards.SUCCESS);
await checkout.submit();

await checkout.waitForSuccess();

// Step 2: Attempt to subscribe to the same monthly plan again
await relier.goto();
await relier.clickSubscribeMonthly();

// The system detects the duplicate subscription and shows an error
await upgrade.waitForEligibilityError();
await expect(upgrade.eligibilityError).toContainText(
/already subscribed/i
);
await expect(upgrade.errorButton).toBeVisible();
});

test('Downgrade blocked — higher-tier user cannot subscribe to lower tier', async ({
target,
page,
pages: { relier, signin },
paymentPages: { checkout, upgrade },
testAccountTracker,
}) => {
const credentials = await testAccountTracker.signUp();

// Step 1: Complete initial 12-month (higher-tier) subscription
await relier.goto();
await relier.clickSubscribe12Month();

await checkout.handleLocationIfNeeded();

await checkout.emailInput.fill(credentials.email);
await checkout.signInContinueButton.click();

await expect(page).toHaveURL(new RegExp(target.contentServerUrl), {
timeout: 30_000,
});
await signin.fillOutPasswordForm(credentials.password);

await expect(checkout.paymentHeading).toBeVisible({ timeout: 30_000 });

await checkout.waitForStripeReady();
await checkout.checkConsent();
await checkout.fillCard(StripeTestCards.SUCCESS);
await checkout.submit();

await checkout.waitForSuccess();

// Step 2: Attempt to subscribe to the monthly (lower-tier) plan
await relier.goto();
await relier.clickSubscribeMonthly();

// The system detects the downgrade attempt and shows an error
await upgrade.waitForEligibilityError();
await expect(upgrade.eligibilityError).toContainText(
/contact support/i
);
await expect(upgrade.errorButton).toBeVisible();
});
});