diff --git a/packages/functional-tests/pages/payments/index.ts b/packages/functional-tests/pages/payments/index.ts index 012f6a98318..a7e754ab74f 100644 --- a/packages/functional-tests/pages/payments/index.ts +++ b/packages/functional-tests/pages/payments/index.ts @@ -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), }; } diff --git a/packages/functional-tests/pages/payments/upgrade.ts b/packages/functional-tests/pages/payments/upgrade.ts new file mode 100644 index 00000000000..bfa1561303d --- /dev/null +++ b/packages/functional-tests/pages/payments/upgrade.ts @@ -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 }); + } +} diff --git a/packages/functional-tests/tests-payments-next/upgrade.spec.ts b/packages/functional-tests/tests-payments-next/upgrade.spec.ts new file mode 100644 index 00000000000..e352ec5da1c --- /dev/null +++ b/packages/functional-tests/tests-payments-next/upgrade.spec.ts @@ -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(); + }); +});