Skip to content
Merged
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
7 changes: 7 additions & 0 deletions test/e2e/.env.dev.example
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ ACCTS_OIDC_PWORD=admin
# Thundermail associated with the above account
PRIMARY_THUNDERMAIL_EMAIL=admin@example.com

# Expected dashboard subscription details
DASHBOARD_CURRENT_SUBSCRIPTION_PRICE="$6"
DASHBOARD_CURRENT_SUBSCRIPTION_MAIL_STORAGE="30 GB"
DASHBOARD_CURRENT_SUBSCRIPTION_SEND_STORAGE="60 GB"
DASHBOARD_CURRENT_SUBSCRIPTION_EMAIL_ADDRESSES=15
DASHBOARD_CURRENT_SUBSCRIPTION_CUSTOM_DOMAINS=3

# Custom OIDC paths since this runs from outside the Docker context
OIDC_URL_AUTH=http://localhost:8999/realms/tbpro/protocol/openid-connect/auth
OIDC_URL_TOKEN=http://localhost:8999/realms/tbpro/protocol/openid-connect/token
Expand Down
7 changes: 7 additions & 0 deletions test/e2e/.env.stage.example
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,12 @@ ACCTS_OIDC_PWORD=
# Thundermail associated with the above account
PRIMARY_THUNDERMAIL_EMAIL=

# Expected dashboard subscription details
DASHBOARD_CURRENT_SUBSCRIPTION_PRICE="$6"
DASHBOARD_CURRENT_SUBSCRIPTION_MAIL_STORAGE="30 GB"
DASHBOARD_CURRENT_SUBSCRIPTION_SEND_STORAGE="60 GB"
DASHBOARD_CURRENT_SUBSCRIPTION_EMAIL_ADDRESSES=15
DASHBOARD_CURRENT_SUBSCRIPTION_CUSTOM_DOMAINS=3

# Thunderbird Pro wait list URL
TB_PRO_WAIT_LIST_URL=https://tb.pro/waitlist/
8 changes: 8 additions & 0 deletions test/e2e/const/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ export const ACCTS_OIDC_EMAIL = String(process.env.ACCTS_OIDC_EMAIL);
export const ACCTS_OIDC_PWORD = String(process.env.ACCTS_OIDC_PWORD);
export const PRIMARY_THUNDERMAIL_EMAIL = String(process.env.PRIMARY_THUNDERMAIL_EMAIL);

// expected dashboard subscription details
export const DASHBOARD_CURRENT_SUBSCRIPTION_PRICE = String(process.env.DASHBOARD_CURRENT_SUBSCRIPTION_PRICE);
export const DASHBOARD_CURRENT_SUBSCRIPTION_MAIL_STORAGE = String(process.env.DASHBOARD_CURRENT_SUBSCRIPTION_MAIL_STORAGE);
export const DASHBOARD_CURRENT_SUBSCRIPTION_SEND_STORAGE = String(process.env.DASHBOARD_CURRENT_SUBSCRIPTION_SEND_STORAGE);
export const DASHBOARD_CURRENT_SUBSCRIPTION_EMAIL_ADDRESSES = String(process.env.DASHBOARD_CURRENT_SUBSCRIPTION_EMAIL_ADDRESSES);
export const DASHBOARD_CURRENT_SUBSCRIPTION_CUSTOM_DOMAINS = String(process.env.DASHBOARD_CURRENT_SUBSCRIPTION_CUSTOM_DOMAINS);

// playwright test tags
export const PLAYWRIGHT_TAG_E2E_SUITE = '@e2e-suite';
export const PLAYWRIGHT_TAG_E2E_PROD_DESKTOP_NIGHTLY = '@e2e-prod-desktop-nightly';
Expand All @@ -33,6 +40,7 @@ export const SMTP_PORT = Number(process.env.SMTP_PORT);
export const IMAP_TLS = String(process.env.IMAP_TLS) ?? 'SSL/TLS';
export const JMAP_TLS = String(process.env.JMAP_TLS) ?? 'SSL/TLS';
export const SMTP_TLS = String(process.env.SMTP_TLS) ?? 'SSL/TLS';
export const PADDLE_HOST = 'customer-portal.paddle.com';
Comment thread
rwood-moz marked this conversation as resolved.

export const DEFAULT_LOCALE = 'en';
export const DEFAULT_TIMEZONE = 'UTC';
178 changes: 178 additions & 0 deletions test/e2e/pages/dashboard-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { expect, type Locator, type Page } from '@playwright/test';
import {
ACCTS_HUB_URL,
ACCTS_OIDC_EMAIL,
DASHBOARD_CURRENT_SUBSCRIPTION_CUSTOM_DOMAINS,
DASHBOARD_CURRENT_SUBSCRIPTION_EMAIL_ADDRESSES,
DASHBOARD_CURRENT_SUBSCRIPTION_MAIL_STORAGE,
DASHBOARD_CURRENT_SUBSCRIPTION_PRICE,
DASHBOARD_CURRENT_SUBSCRIPTION_SEND_STORAGE,
PADDLE_HOST,
TIMEOUT_2_SECONDS,
TIMEOUT_5_SECONDS,
TIMEOUT_30_SECONDS,
} from '../const/constants';
import { waitForVueApp } from '../utils/utils';

interface ServiceUrls {
appointment: string;
send: string;
}

const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

export class DashboardPage {
readonly page: Page;
readonly myAccountHeading: Locator;
readonly myAccountCard: Locator;
readonly privacyAndDataHeading: Locator;
readonly manageYourServicesHeading: Locator;
readonly currentSubscriptionHeading: Locator;
readonly currentSubscriptionSection: Locator;
readonly passwordChangeLink: Locator;
readonly updatePasswordHeader: Locator;
readonly deleteAccountLink: Locator;
readonly thundermailLink: Locator;
readonly appointmentLink: Locator;
readonly sendLink: Locator;
readonly manageSubscriptionButton: Locator;
readonly userAvatar: Locator;
readonly supportLink: Locator;
readonly logoutLink: Locator;
readonly contactHeader: Locator;

constructor(page: Page) {
this.page = page;
this.myAccountHeading = this.page.getByRole('heading', { name: 'My Account' });
this.myAccountCard = this.page.locator('.my-account-card');
this.privacyAndDataHeading = this.page.getByRole('heading', { name: 'Privacy & Data' });
this.manageYourServicesHeading = this.page.getByRole('heading', { name: 'Manage Your Services' });
this.currentSubscriptionHeading = this.page.getByRole('heading', { name: 'Your Current Subscription' });
this.currentSubscriptionSection = this.page.locator('section').filter({ has: this.currentSubscriptionHeading });
this.passwordChangeLink = this.page.locator('a[href="/reset-password/"]');
this.updatePasswordHeader = this.page.getByRole('heading', { name: 'Update password' });
this.deleteAccountLink = this.page.getByRole('link', { name: 'Delete account and all data' });
this.thundermailLink = this.page.locator('.service-icon-link[href="/mail"]').filter({ hasText: 'Thundermail' });
this.appointmentLink = this.page.locator('.service-icon-link').filter({ hasText: 'Appointment' });
this.sendLink = this.page.locator('.service-icon-link').filter({ hasText: 'Send' });
this.manageSubscriptionButton = this.page.getByRole('button', { name: 'Manage Subscription' });
this.userAvatar = this.page.getByRole('banner').locator('.avatar');
this.supportLink = this.page.getByRole('link', { name: 'Support' });
this.logoutLink = this.page.getByRole('link', { name: 'Logout' });
this.contactHeader = this.page.getByRole('heading', { name: 'Submit a request' });
}

async navigateToDashboard() {
await this.page.goto(`${ACCTS_HUB_URL}/dashboard`);
await this.waitForPageToSettle();
// This test expects that the signed-in user already has a tb pro subscription setup
// so the dashboard is displayed after sign-in and not the tb pro subscription page
await expect.poll(async () => new URL(this.page.url()).pathname).toBe('/dashboard');
}

async verifyDashboardDisplayed() {
await expect(this.myAccountHeading).toBeVisible();
await expect(this.myAccountCard).toContainText(ACCTS_OIDC_EMAIL);
await expect(this.privacyAndDataHeading).toBeVisible();
await expect(this.manageYourServicesHeading).toBeVisible();
await expect(this.currentSubscriptionHeading).toBeVisible();
await expect(this.currentSubscriptionSection).toContainText(DASHBOARD_CURRENT_SUBSCRIPTION_PRICE);
await expect(this.currentSubscriptionSection).toContainText(
new RegExp(`${escapeRegExp(DASHBOARD_CURRENT_SUBSCRIPTION_MAIL_STORAGE)}\\s*of Mail Storage`),
);
await expect(this.currentSubscriptionSection).toContainText(
new RegExp(`${escapeRegExp(DASHBOARD_CURRENT_SUBSCRIPTION_SEND_STORAGE)}\\s*of Send Storage`),
);
await expect(this.currentSubscriptionSection).toContainText(
new RegExp(`${escapeRegExp(DASHBOARD_CURRENT_SUBSCRIPTION_EMAIL_ADDRESSES)}\\s*Email Addresses`),
);
await expect(this.currentSubscriptionSection).toContainText(
new RegExp(`${escapeRegExp(DASHBOARD_CURRENT_SUBSCRIPTION_CUSTOM_DOMAINS)}\\s*Custom Domains`),
);
await expect(this.passwordChangeLink).toBeVisible();
await expect(this.deleteAccountLink).toBeVisible();
await expect(this.thundermailLink).toBeVisible();
await expect(this.appointmentLink).toBeVisible();
await expect(this.sendLink).toBeVisible();
await expect(this.manageSubscriptionButton).toBeVisible({ timeout: TIMEOUT_30_SECONDS });
}

async verifyPasswordChangeNavigation() {
await this.passwordChangeLink.click();
await this.waitForPageToSettle();
await expect(this.updatePasswordHeader).toBeVisible({ timeout: TIMEOUT_30_SECONDS });
}

async verifyDeleteAccountNavigationOnly() {
await this.deleteAccountLink.click();
await this.verifyContactScreenDisplayed();
}

async verifyThundermailNavigation() {
await this.thundermailLink.click();
await this.waitForPageToSettle();
await expect.poll(async () => new URL(this.page.url()).pathname).toBe('/mail');
}

async verifyServiceLinksOpenConfiguredUrls() {
const serviceUrls = await this.getConfiguredServiceUrls();
await this.verifyLinkOpensPopup(this.appointmentLink, serviceUrls.appointment);
await this.verifyLinkOpensPopup(this.sendLink, serviceUrls.send);
}

async verifyManageSubscriptionOpensPortal() {
const [popup] = await Promise.all([
this.page.waitForEvent('popup'),
this.manageSubscriptionButton.click(),
]);

await expect.poll(async () => popup.url()).not.toBe('about:blank');
expect(new URL(popup.url()).protocol).toMatch(/^https?:$/);
Comment thread
rwood-moz marked this conversation as resolved.
expect(new URL(popup.url()).host).toContain(PADDLE_HOST);
await popup.close();
}

async verifyUserMenuControls() {
await this.userAvatar.click();
await expect(this.supportLink).toBeVisible();
await expect(this.logoutLink).toBeVisible();
await expect(this.logoutLink).toHaveAttribute('href', '/logout/');

await this.supportLink.click();
await this.verifyContactScreenDisplayed();
}

private async verifyContactScreenDisplayed() {
await this.waitForPageToSettle();
await expect.poll(async () => new URL(this.page.url()).pathname).toMatch(/^\/contact\/?$/);
await expect(this.contactHeader).toBeVisible({ timeout: TIMEOUT_30_SECONDS });
}

private async waitForPageToSettle() {
await waitForVueApp(this.page);
await this.page.waitForLoadState('networkidle', { timeout: TIMEOUT_5_SECONDS }).catch(() => {});
await this.page.waitForTimeout(TIMEOUT_2_SECONDS);
}

private async getConfiguredServiceUrls(): Promise<ServiceUrls> {
return await this.page.evaluate(() => ({
appointment: (window as any)._page?.tbProAppointmentUrl,
send: (window as any)._page?.tbProSendUrl,
}));
}

private async verifyLinkOpensPopup(link: Locator, expectedUrl: string) {
expect(expectedUrl).toBeTruthy();
await expect(link).toHaveAttribute('href', expectedUrl);
await expect(link).toHaveAttribute('target', '_blank');

const [popup] = await Promise.all([
this.page.waitForEvent('popup'),
link.click(),
]);

await expect.poll(async () => popup.url()).not.toBe('about:blank');
expect(new URL(popup.url()).href).toBe(new URL(expectedUrl).href);
await popup.close();
}
}
49 changes: 49 additions & 0 deletions test/e2e/tests/desktop/dashboard.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { test } from '@playwright/test';
import { DashboardPage } from '../../pages/dashboard-page';
import { ensureWeAreSignedIn } from '../../utils/utils';

import {
PLAYWRIGHT_TAG_E2E_SUITE,
PLAYWRIGHT_TAG_E2E_PROD_DESKTOP_NIGHTLY,
ACCTS_TARGET_ENV,
} from '../../const/constants';

let dashboardPage: DashboardPage;

test.beforeEach(async ({ page }) => {
dashboardPage = new DashboardPage(page);
await ensureWeAreSignedIn(page);
});

/**
* Prerequisite: This test assumes that the signed-in user already has a TB Pro subscription
* and therefore the main dashboard is visible (and not redirected to the subscription page).
* If this test is running in CI on a new local stack (i.e. on PRs) the test user (admin)
* won't yet have a subscription, therefore skip this test when running in CI on PRs. This
* test can run in the nighlty tests though because the existing test accounts on stage and
* prod already have a TB Pro subscription setup. In future we will add a subscribe step so
* we can run on new local stacks that aren't subscribed yet (issue 771).
*/
test.describe('dashboard controls on desktop browser', {
tag: [PLAYWRIGHT_TAG_E2E_SUITE, PLAYWRIGHT_TAG_E2E_PROD_DESKTOP_NIGHTLY],
}, () => {
test('all visible dashboard controls work as expected', async () => {
test.skip(ACCTS_TARGET_ENV == 'dev', 'Skipping this test when running on local dev stack until we automate tb subsrcibe step');

await dashboardPage.navigateToDashboard();
await dashboardPage.verifyDashboardDisplayed();

await dashboardPage.verifyPasswordChangeNavigation();
await dashboardPage.navigateToDashboard();

await dashboardPage.verifyDeleteAccountNavigationOnly();
await dashboardPage.navigateToDashboard();

await dashboardPage.verifyThundermailNavigation();
await dashboardPage.navigateToDashboard();

await dashboardPage.verifyServiceLinksOpenConfiguredUrls();
await dashboardPage.verifyManageSubscriptionOpensPortal();
await dashboardPage.verifyUserMenuControls();
});
});
49 changes: 49 additions & 0 deletions test/e2e/tests/mobile/dashboard.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { test } from '@playwright/test';
import { DashboardPage } from '../../pages/dashboard-page';
import { navigateToAccountsHubAndSignIn } from '../../utils/utils';

import {
PLAYWRIGHT_TAG_E2E_SUITE_MOBILE,
PLAYWRIGHT_TAG_E2E_PROD_MOBILE_NIGHTLY,
ACCTS_TARGET_ENV,
} from '../../const/constants';

let dashboardPage: DashboardPage;

test.beforeEach(async ({ page }) => {
dashboardPage = new DashboardPage(page);
await navigateToAccountsHubAndSignIn(page);
});

/**
* Prerequisite: This test assumes that the signed-in user already has a TB Pro subscription
* and therefore the main dashboard is visible (and not redirected to the subscription page).
* If this test is running in CI on a new local stack (i.e. on PRs) the test user (admin)
* won't yet have a subscription, therefore skip this test when running in CI on PRs. This
* test can run in the nighlty tests though because the existing test accounts on stage and
* prod already have a TB Pro subscription setup. In future we will add a subscribe step so
* we can run on new local stacks that aren't subscribed yet (issue 771).
*/
test.describe('dashboard controls on mobile browser', {
tag: [PLAYWRIGHT_TAG_E2E_SUITE_MOBILE, PLAYWRIGHT_TAG_E2E_PROD_MOBILE_NIGHTLY],
}, () => {
test('all visible dashboard controls work as expected', async () => {
test.skip(ACCTS_TARGET_ENV == 'dev', 'Skipping this test when running on local dev stack until we automate tb subsrcibe step');

await dashboardPage.navigateToDashboard();
await dashboardPage.verifyDashboardDisplayed();

await dashboardPage.verifyPasswordChangeNavigation();
await dashboardPage.navigateToDashboard();

await dashboardPage.verifyDeleteAccountNavigationOnly();
await dashboardPage.navigateToDashboard();

await dashboardPage.verifyThundermailNavigation();
await dashboardPage.navigateToDashboard();

await dashboardPage.verifyServiceLinksOpenConfiguredUrls();
await dashboardPage.verifyManageSubscriptionOpensPortal();
await dashboardPage.verifyUserMenuControls();
});
});
Loading