From 5e7f63b5ff76dc52cb98975f15b744b8c24f2193 Mon Sep 17 00:00:00 2001 From: Lisa Chan Date: Tue, 12 May 2026 15:58:30 -0400 Subject: [PATCH 1/3] temp - to drop --- .circleci/config.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 6e5a76cea09..d9f4483c7ff 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1044,6 +1044,11 @@ workflows: workflow: test_pull_request requires: - Build (PR) + - playwright-payments-tests: + name: Payments Functional Tests - Playwright (PR) + workflow: test_pull_request + requires: + - Build (PR) - on-complete: name: Tests Complete (PR) stage: Tests From eb7b21fdb30d8cdd04f4d83cab71115b9636da16 Mon Sep 17 00:00:00 2001 From: Lisa Chan Date: Tue, 19 May 2026 16:46:53 -0400 Subject: [PATCH 2/3] reduce flakiness --- .../pages/payments/checkout.ts | 32 +++++++++++++++++-- .../pages/payments/upgrade.ts | 17 ++++++++-- .../functional-tests/playwright.config.ts | 1 + .../tests-payments-next/upgrade.spec.ts | 7 ++-- 4 files changed, 47 insertions(+), 10 deletions(-) diff --git a/packages/functional-tests/pages/payments/checkout.ts b/packages/functional-tests/pages/payments/checkout.ts index ae50af2b6ff..11c7a9590a9 100644 --- a/packages/functional-tests/pages/payments/checkout.ts +++ b/packages/functional-tests/pages/payments/checkout.ts @@ -149,6 +149,32 @@ export class CheckoutPage extends BasePaymentPage { } } + // FXA auth locators (used when the payments app redirects to FXA sign-in) + + private get passwordFormHeading() { + return this.page.getByRole('heading', { name: /^Enter your password/ }); + } + + private get passwordTextbox() { + return this.page.getByRole('textbox', { name: 'password' }); + } + + private get fxaSignInButton() { + return this.page.getByRole('button', { name: 'Sign in' }); + } + + /** + * If the payments app redirected to FXA for authentication, fill out the + * password form. No-op when the page is not on the content server. + */ + async handleAuthIfNeeded(password: string) { + if (this.page.url().includes(this.target.contentServerUrl)) { + await expect(this.passwordFormHeading).toBeVisible(); + await this.passwordTextbox.fill(password); + await this.fxaSignInButton.click(); + } + } + // Actions /** @@ -201,7 +227,7 @@ export class CheckoutPage extends BasePaymentPage { await this.typeInStripeField(this.postalCodeInput, zip); // Link always auto-checks after card fill — uncheck it - await expect(this.stripeLinkCheckbox).toBeVisible({ timeout: 5_000 }); + await expect(this.stripeLinkCheckbox).toBeVisible({ timeout: 15_000 }); await this.stripeLinkCheckbox.click(); } @@ -258,7 +284,7 @@ export class CheckoutPage extends BasePaymentPage { await expect(this.page).toHaveURL(/success|error|needs_input/, { timeout, }); - await expect(this.page).toHaveURL(/success/); + await expect(this.page).toHaveURL(/success/, { timeout }); await expect(this.successHeading).toBeVisible({ timeout: 30_000 }); } @@ -271,7 +297,7 @@ export class CheckoutPage extends BasePaymentPage { await expect(this.page).toHaveURL(/success|error|needs_input/, { timeout, }); - await expect(this.page).toHaveURL(/error/); + await expect(this.page).toHaveURL(/error/, { timeout }); await expect(this.errorBanner).toBeVisible({ timeout: 10_000 }); } diff --git a/packages/functional-tests/pages/payments/upgrade.ts b/packages/functional-tests/pages/payments/upgrade.ts index 506bd0d9897..26101853655 100644 --- a/packages/functional-tests/pages/payments/upgrade.ts +++ b/packages/functional-tests/pages/payments/upgrade.ts @@ -24,6 +24,19 @@ export class UpgradePage extends BasePaymentPage { return this.page.getByRole('button', { name: /Subscribe Now/i }); } + /** + * Wait for the upgrade page to resolve eligibility and fully render. + * Waits for the URL to leave /start, the upgrade section to appear, + * and all content (prorated amount, acknowledgment) to be visible + * before the caller interacts with the page. + */ + async waitForUpgradePage(timeout = 60_000) { + await expect(this.page).not.toHaveURL(/\/start(\?|$)/, { timeout }); + await expect(this.upgradeSection).toBeVisible({ timeout: 30_000 }); + await expect(this.proratedAmount).toBeVisible({ timeout: 15_000 }); + await expect(this.acknowledgmentText).toBeVisible({ timeout: 15_000 }); + } + // Actions /** @@ -46,7 +59,7 @@ export class UpgradePage extends BasePaymentPage { */ async waitForSuccess(timeout = 60_000) { await expect(this.page).toHaveURL(/success|error/, { timeout }); - await expect(this.page).toHaveURL(/success/, { timeout: 15_000 }); + await expect(this.page).toHaveURL(/success/, { timeout }); await expect(this.successHeading).toBeVisible({ timeout: 30_000 }); } @@ -57,7 +70,7 @@ export class UpgradePage extends BasePaymentPage { */ async waitForEligibilityError(timeout = 60_000) { await expect(this.page).toHaveURL(/success|error/, { timeout }); - await expect(this.page).toHaveURL(/error/, { timeout: 15_000 }); + await expect(this.page).toHaveURL(/error/, { timeout }); await expect(this.errorHeading).toBeVisible({ timeout: 10_000 }); } } diff --git a/packages/functional-tests/playwright.config.ts b/packages/functional-tests/playwright.config.ts index 4a849502458..875d966a0bb 100644 --- a/packages/functional-tests/playwright.config.ts +++ b/packages/functional-tests/playwright.config.ts @@ -100,6 +100,7 @@ export default defineConfig>({ ({ name: `${name}-payments-next`, testDir: 'tests-payments-next', + fullyParallel: false, use: { browserName: 'chromium', targetName: name, diff --git a/packages/functional-tests/tests-payments-next/upgrade.spec.ts b/packages/functional-tests/tests-payments-next/upgrade.spec.ts index 6d7f80982c7..0f4d03e79c2 100644 --- a/packages/functional-tests/tests-payments-next/upgrade.spec.ts +++ b/packages/functional-tests/tests-payments-next/upgrade.spec.ts @@ -56,11 +56,8 @@ test.describe('severity-1 #smoke', () => { 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(); + // Wait for eligibility to resolve and the upgrade page to fully render + await upgrade.waitForUpgradePage(); // Confirm the upgrade await upgrade.checkConsent(); From 76221b74609709a75c6aae4252ab2d2b301ca869 Mon Sep 17 00:00:00 2001 From: Lisa Chan Date: Fri, 15 May 2026 14:59:51 -0400 Subject: [PATCH 3/3] feat(functional-tests): Add payments-next coverage for subscription management --- packages/123done/static/index.html | 3 + packages/123done/static/js/123done.js | 1 + .../functional-tests/lib/stripe-test-cards.ts | 1 + .../functional-tests/pages/payments/cancel.ts | 71 +++++ .../functional-tests/pages/payments/index.ts | 6 + .../functional-tests/pages/payments/manage.ts | 181 ++++++++++++ .../pages/payments/stay-subscribed.ts | 64 +++++ packages/functional-tests/pages/relier.ts | 11 + .../tests-payments-next/subscription.spec.ts | 258 ++++++++++++++++++ 9 files changed, 596 insertions(+) create mode 100644 packages/functional-tests/pages/payments/cancel.ts create mode 100644 packages/functional-tests/pages/payments/manage.ts create mode 100644 packages/functional-tests/pages/payments/stay-subscribed.ts create mode 100644 packages/functional-tests/tests-payments-next/subscription.spec.ts diff --git a/packages/123done/static/index.html b/packages/123done/static/index.html index 2654702d2d5..d936bba1cce 100644 --- a/packages/123done/static/index.html +++ b/packages/123done/static/index.html @@ -86,6 +86,9 @@ SP3 - Sub to Pro 1m (EN GB) + + SP3 - Sub to Pro Plus 1m + diff --git a/packages/123done/static/js/123done.js b/packages/123done/static/js/123done.js index 72015c87992..0c8b44c91fe 100644 --- a/packages/123done/static/js/123done.js +++ b/packages/123done/static/js/123done.js @@ -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', }, }; diff --git a/packages/functional-tests/lib/stripe-test-cards.ts b/packages/functional-tests/lib/stripe-test-cards.ts index 611c52e85b0..2424f31e7c4 100644 --- a/packages/functional-tests/lib/stripe-test-cards.ts +++ b/packages/functional-tests/lib/stripe-test-cards.ts @@ -9,6 +9,7 @@ export const StripeTestCards = { SUCCESS: '4242424242424242', + SUCCESS_MASTERCARD: '5555555555554444', THREE_DS_AUTHENTICATE: '4000000000003220', DECLINED: '4000000000000002', INSUFFICIENT_FUNDS: '4000000000009995', diff --git a/packages/functional-tests/pages/payments/cancel.ts b/packages/functional-tests/pages/payments/cancel.ts new file mode 100644 index 00000000000..254549d9fdb --- /dev/null +++ b/packages/functional-tests/pages/payments/cancel.ts @@ -0,0 +1,71 @@ +/* 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(); + await expect(this.submitCancelButton).toBeDisabled({ timeout: 5_000 }); + } + + /** + * 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, + }); + } +} diff --git a/packages/functional-tests/pages/payments/index.ts b/packages/functional-tests/pages/payments/index.ts index a7e754ab74f..105d6045dee 100644 --- a/packages/functional-tests/pages/payments/index.ts +++ b/packages/functional-tests/pages/payments/index.ts @@ -4,12 +4,18 @@ 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'; import { UpgradePage } from './upgrade'; 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), upgrade: new UpgradePage(page, target), }; } diff --git a/packages/functional-tests/pages/payments/manage.ts b/packages/functional-tests/pages/payments/manage.ts new file mode 100644 index 00000000000..7e484557984 --- /dev/null +++ b/packages/functional-tests/pages/payments/manage.ts @@ -0,0 +1,181 @@ +/* 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'; +import { TestCardDefaults } from '../../lib/stripe-test-cards'; + +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).getByText( + /\/(month|year|week)/i + ); + } + + // 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('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.activeSubscriptionsList.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 }); + // Wait for either the subscription list or the empty-state message to + // render, so callers don't race against async API responses. + await expect( + this.activeSubscriptionsList.or(this.emptyStateMessage) + ).toBeVisible({ timeout: 15_000 }); + } + + /** + * Navigate to the Stripe payment management page, fill a new card, + * and save it. Waits for redirect back to the manage page. + * + * Uses pressSequentially inside the Stripe iframe since Stripe's + * internal event handlers require real keystrokes to register + * field completion. + */ + async updatePaymentMethod( + cardNumber: string, + exp = `${TestCardDefaults.EXP_MONTH}/${String(TestCardDefaults.EXP_YEAR).slice(-2)}`, + cvc = TestCardDefaults.CVC, + zip = '10001' + ) { + // Navigate to the Stripe payment management page + await this.page.goto( + `${this.target.paymentsNextUrl}/en/subscriptions/payments/stripe` + ); + await expect(this.page).toHaveURL(/payments\/stripe/, { timeout: 30_000 }); + + // Wait for the Stripe PaymentElement iframe + const stripeIframe = this.page.locator( + 'iframe[title*="Secure payment input frame"]' + ); + await expect(stripeIframe).toBeVisible({ timeout: 30_000 }); + + const stripeFrame = this.page.frameLocator( + 'iframe[title*="Secure payment input frame"]' + ); + + // Expand the card form in the accordion + await stripeFrame.getByRole('button', { name: 'Card' }).click(); + + // Fill card fields + const fields: Array<{ locator: ReturnType; value: string }> = [ + { locator: stripeFrame.locator('[autocomplete="cc-number"]'), value: cardNumber }, + { locator: stripeFrame.locator('[autocomplete="cc-exp"]'), value: exp }, + { locator: stripeFrame.locator('[autocomplete="cc-csc"]'), value: cvc }, + { + locator: stripeFrame.locator( + '[autocomplete="postal-code"], [name="postalCode"], [name="zip"]' + ), + value: zip, + }, + ]; + + for (const { locator, value } of fields) { + await expect(locator).toBeAttached({ timeout: 10_000 }); + await locator.click(); + await locator.pressSequentially(value, { delay: 50 }); + } + + // Wait for Stripe's onChange to propagate complete:true, then save + const saveButton = this.page.getByRole('button', { + name: /Save payment method/i, + }); + await expect(saveButton).toBeVisible({ timeout: 10_000 }); + await expect(saveButton).not.toHaveAttribute('aria-disabled', 'true', { + timeout: 30_000, + }); + await expect(saveButton).toBeEnabled(); + await saveButton.click(); + + // Wait for save to process and redirect back to manage page + await expect(this.page).toHaveURL(/subscriptions\/manage/, { + timeout: 90_000, + }); + } +} diff --git a/packages/functional-tests/pages/payments/stay-subscribed.ts b/packages/functional-tests/pages/payments/stay-subscribed.ts new file mode 100644 index 00000000000..af26f4a7eb2 --- /dev/null +++ b/packages/functional-tests/pages/payments/stay-subscribed.ts @@ -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 }); + } +} diff --git a/packages/functional-tests/pages/relier.ts b/packages/functional-tests/pages/relier.ts index 5485b260721..85452ea9558 100644 --- a/packages/functional-tests/pages/relier.ts +++ b/packages/functional-tests/pages/relier.ts @@ -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}/**`); diff --git a/packages/functional-tests/tests-payments-next/subscription.spec.ts b/packages/functional-tests/tests-payments-next/subscription.spec.ts new file mode 100644 index 00000000000..a7fb4981da9 --- /dev/null +++ b/packages/functional-tests/tests-payments-next/subscription.spec.ts @@ -0,0 +1,258 @@ +/* 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'; + +// Each test creates its own account and subscription — no shared state. +test.describe.configure({ retries: 2 }); + +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'), + 'Subscription management smoke tests are not run in production' + ); + }); + + test('Manage page loads with active subscription', async ({ + target, + page, + pages: { relier, signin }, + paymentPages: { checkout, manage }, + testAccountTracker, + }) => { + const credentials = await testAccountTracker.signUp(); + + // Create subscription via UI checkout + await relier.goto(); + await relier.clickSubscribePlusMonthly(); + + 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(); + + // Navigate to manage page + await manage.goto(); + await manage.waitForManagePage(); + + // Verify subscription card details + await expect(manage.activeSubscriptionsList).toBeVisible(); + await expect(manage.paymentMethodLastFour).toBeVisible(); + await expect(manage.paymentMethodIcon).toBeVisible(); + }); + + test('Change payment method (Stripe)', async ({ + target, + page, + pages: { relier, signin }, + paymentPages: { checkout, manage }, + testAccountTracker, + }) => { + const credentials = await testAccountTracker.signUp(); + + // Create subscription via UI checkout + await relier.goto(); + await relier.clickSubscribePlusMonthly(); + + 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(); + + // Update payment method via the Stripe management page + await manage.updatePaymentMethod(StripeTestCards.SUCCESS_MASTERCARD); + + await manage.waitForManagePage(); + await expect(manage.paymentMethodLastFour).toContainText('4444'); + }); + + test('Cancel subscription', async ({ + target, + page, + pages: { relier, signin }, + paymentPages: { checkout, manage, cancel }, + testAccountTracker, + }) => { + const credentials = await testAccountTracker.signUp(); + + // Create subscription via UI checkout + await relier.goto(); + await relier.clickSubscribePlusMonthly(); + + 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(); + + // Navigate to manage page and cancel + await manage.goto(); + await manage.waitForManagePage(); + + await manage.cancelButton().click(); + + // Confirm cancellation + await expect(cancel.cancelHeading).toBeVisible({ timeout: 30_000 }); + await cancel.confirmAndCancel(); + await cancel.waitForCancelConfirmation(); + + // Navigate back to manage and verify cancelled status + await manage.goto(); + await manage.waitForManagePage(); + await expect(manage.cancelledStatus).toBeVisible(); + }); + + test('Resubscribe cancelled subscription', async ({ + target, + page, + pages: { relier, signin }, + paymentPages: { checkout, manage, cancel, staySubscribed }, + testAccountTracker, + }) => { + const credentials = await testAccountTracker.signUp(); + + // Create subscription via UI checkout + await relier.goto(); + await relier.clickSubscribePlusMonthly(); + + 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(); + + // Cancel the subscription first + await manage.goto(); + await manage.waitForManagePage(); + + await manage.cancelButton().click(); + + await expect(cancel.cancelHeading).toBeVisible({ timeout: 30_000 }); + await cancel.confirmAndCancel(); + await cancel.waitForCancelConfirmation(); + + // Navigate back to manage and resubscribe + await manage.goto(); + await manage.waitForManagePage(); + + const stayBtn = manage.staySubscribedButton(); + await expect(stayBtn).toBeVisible({ timeout: 10_000 }); + await stayBtn.click(); + + // Confirm resubscribe + await expect(staySubscribed.staySubscribedHeading).toBeVisible({ + timeout: 30_000, + }); + await staySubscribed.resubscribe(); + await staySubscribed.waitForSuccess(); + + // Navigate back to manage and verify active again + await expect(staySubscribed.backToSubscriptionsLink).toBeVisible({ + timeout: 10_000, + }); + await staySubscribed.backToSubscriptionsLink.click(); + await manage.waitForManagePage(); + + // Cancelled status should no longer be visible — subscription is active + await expect(manage.cancelledStatus).toBeHidden(); + await expect(manage.activeSubscriptionsList).toBeVisible(); + }); +}); + +test.describe('severity-2', () => { + test.setTimeout(180_000); + test.use({ viewport: { width: 1280, height: 1080 } }); + + test.beforeEach(async ({}, { project }) => { + test.skip( + project.name.includes('production'), + 'Subscription management tests are not run in production' + ); + }); + + test('Manage page empty state', async ({ + target, + page, + pages: { signin }, + paymentPages: { manage }, + testAccountTracker, + }) => { + const credentials = await testAccountTracker.signUp(); + + // Navigate to manage page (triggers FXA auth) + await manage.goto(); + + // Sign in when redirected to FXA + await expect(page).toHaveURL(new RegExp(target.contentServerUrl), { + timeout: 30_000, + }); + await signin.fillOutEmailFirstForm(credentials.email); + await signin.fillOutPasswordForm(credentials.password); + + // Wait for redirect back to manage page + await manage.waitForManagePage(); + + // Verify empty state + await expect(manage.emptyStateMessage).toBeVisible(); + }); +});