Skip to content

Commit bf81622

Browse files
committed
Add E2E tests for main accounts dashboard
1 parent 65e1bbd commit bf81622

6 files changed

Lines changed: 298 additions & 0 deletions

File tree

test/e2e/.env.dev.example

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ ACCTS_OIDC_PWORD=admin
2323
# Thundermail associated with the above account
2424
PRIMARY_THUNDERMAIL_EMAIL=admin@example.com
2525

26+
# Expected dashboard subscription details
27+
DASHBOARD_CURRENT_SUBSCRIPTION_PRICE="$6"
28+
DASHBOARD_CURRENT_SUBSCRIPTION_MAIL_STORAGE="30 GB"
29+
DASHBOARD_CURRENT_SUBSCRIPTION_SEND_STORAGE="60 GB"
30+
DASHBOARD_CURRENT_SUBSCRIPTION_EMAIL_ADDRESSES=15
31+
DASHBOARD_CURRENT_SUBSCRIPTION_CUSTOM_DOMAINS=3
32+
2633
# Custom OIDC paths since this runs from outside the Docker context
2734
OIDC_URL_AUTH=http://localhost:8999/realms/tbpro/protocol/openid-connect/auth
2835
OIDC_URL_TOKEN=http://localhost:8999/realms/tbpro/protocol/openid-connect/token

test/e2e/.env.stage.example

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,12 @@ ACCTS_OIDC_PWORD=
2020
# Thundermail associated with the above account
2121
PRIMARY_THUNDERMAIL_EMAIL=
2222

23+
# Expected dashboard subscription details
24+
DASHBOARD_CURRENT_SUBSCRIPTION_PRICE="$6"
25+
DASHBOARD_CURRENT_SUBSCRIPTION_MAIL_STORAGE="30 GB"
26+
DASHBOARD_CURRENT_SUBSCRIPTION_SEND_STORAGE="60 GB"
27+
DASHBOARD_CURRENT_SUBSCRIPTION_EMAIL_ADDRESSES=15
28+
DASHBOARD_CURRENT_SUBSCRIPTION_CUSTOM_DOMAINS=3
29+
2330
# Thunderbird Pro wait list URL
2431
TB_PRO_WAIT_LIST_URL=https://tb.pro/waitlist/

test/e2e/const/constants.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ export const ACCTS_OIDC_EMAIL = String(process.env.ACCTS_OIDC_EMAIL);
1313
export const ACCTS_OIDC_PWORD = String(process.env.ACCTS_OIDC_PWORD);
1414
export const PRIMARY_THUNDERMAIL_EMAIL = String(process.env.PRIMARY_THUNDERMAIL_EMAIL);
1515

16+
// expected dashboard subscription details
17+
export const DASHBOARD_CURRENT_SUBSCRIPTION_PRICE = String(process.env.DASHBOARD_CURRENT_SUBSCRIPTION_PRICE);
18+
export const DASHBOARD_CURRENT_SUBSCRIPTION_MAIL_STORAGE = String(process.env.DASHBOARD_CURRENT_SUBSCRIPTION_MAIL_STORAGE);
19+
export const DASHBOARD_CURRENT_SUBSCRIPTION_SEND_STORAGE = String(process.env.DASHBOARD_CURRENT_SUBSCRIPTION_SEND_STORAGE);
20+
export const DASHBOARD_CURRENT_SUBSCRIPTION_EMAIL_ADDRESSES = String(process.env.DASHBOARD_CURRENT_SUBSCRIPTION_EMAIL_ADDRESSES);
21+
export const DASHBOARD_CURRENT_SUBSCRIPTION_CUSTOM_DOMAINS = String(process.env.DASHBOARD_CURRENT_SUBSCRIPTION_CUSTOM_DOMAINS);
22+
1623
// playwright test tags
1724
export const PLAYWRIGHT_TAG_E2E_SUITE = '@e2e-suite';
1825
export const PLAYWRIGHT_TAG_E2E_PROD_DESKTOP_NIGHTLY = '@e2e-prod-desktop-nightly';
@@ -33,6 +40,7 @@ export const SMTP_PORT = Number(process.env.SMTP_PORT);
3340
export const IMAP_TLS = String(process.env.IMAP_TLS) ?? 'SSL/TLS';
3441
export const JMAP_TLS = String(process.env.JMAP_TLS) ?? 'SSL/TLS';
3542
export const SMTP_TLS = String(process.env.SMTP_TLS) ?? 'SSL/TLS';
43+
export const PADDLE_HOST = 'customer-portal.paddle.com';
3644

3745
export const DEFAULT_LOCALE = 'en';
3846
export const DEFAULT_TIMEZONE = 'UTC';

test/e2e/pages/dashboard-page.ts

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import { expect, type Locator, type Page } from '@playwright/test';
2+
import {
3+
ACCTS_HUB_URL,
4+
ACCTS_OIDC_EMAIL,
5+
DASHBOARD_CURRENT_SUBSCRIPTION_CUSTOM_DOMAINS,
6+
DASHBOARD_CURRENT_SUBSCRIPTION_EMAIL_ADDRESSES,
7+
DASHBOARD_CURRENT_SUBSCRIPTION_MAIL_STORAGE,
8+
DASHBOARD_CURRENT_SUBSCRIPTION_PRICE,
9+
DASHBOARD_CURRENT_SUBSCRIPTION_SEND_STORAGE,
10+
PADDLE_HOST,
11+
TIMEOUT_2_SECONDS,
12+
TIMEOUT_5_SECONDS,
13+
TIMEOUT_30_SECONDS,
14+
} from '../const/constants';
15+
import { waitForVueApp } from '../utils/utils';
16+
17+
interface ServiceUrls {
18+
appointment: string;
19+
send: string;
20+
}
21+
22+
const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
23+
24+
export class DashboardPage {
25+
readonly page: Page;
26+
readonly myAccountHeading: Locator;
27+
readonly myAccountCard: Locator;
28+
readonly privacyAndDataHeading: Locator;
29+
readonly manageYourServicesHeading: Locator;
30+
readonly currentSubscriptionHeading: Locator;
31+
readonly currentSubscriptionSection: Locator;
32+
readonly passwordChangeLink: Locator;
33+
readonly updatePasswordHeader: Locator;
34+
readonly deleteAccountLink: Locator;
35+
readonly thundermailLink: Locator;
36+
readonly appointmentLink: Locator;
37+
readonly sendLink: Locator;
38+
readonly manageSubscriptionButton: Locator;
39+
readonly userAvatar: Locator;
40+
readonly supportLink: Locator;
41+
readonly logoutLink: Locator;
42+
readonly contactHeader: Locator;
43+
44+
constructor(page: Page) {
45+
this.page = page;
46+
this.myAccountHeading = this.page.getByRole('heading', { name: 'My Account' });
47+
this.myAccountCard = this.page.locator('.my-account-card');
48+
this.privacyAndDataHeading = this.page.getByRole('heading', { name: 'Privacy & Data' });
49+
this.manageYourServicesHeading = this.page.getByRole('heading', { name: 'Manage Your Services' });
50+
this.currentSubscriptionHeading = this.page.getByRole('heading', { name: 'Your Current Subscription' });
51+
this.currentSubscriptionSection = this.page.locator('section').filter({ has: this.currentSubscriptionHeading });
52+
this.passwordChangeLink = this.page.locator('a[href="/reset-password/"]');
53+
this.updatePasswordHeader = this.page.getByRole('heading', { name: 'Update password' });
54+
this.deleteAccountLink = this.page.getByRole('link', { name: 'Delete account and all data' });
55+
this.thundermailLink = this.page.locator('.service-icon-link[href="/mail"]').filter({ hasText: 'Thundermail' });
56+
this.appointmentLink = this.page.locator('.service-icon-link').filter({ hasText: 'Appointment' });
57+
this.sendLink = this.page.locator('.service-icon-link').filter({ hasText: 'Send' });
58+
this.manageSubscriptionButton = this.page.getByRole('button', { name: 'Manage Subscription' });
59+
this.userAvatar = this.page.getByRole('banner').locator('.avatar');
60+
this.supportLink = this.page.getByRole('link', { name: 'Support' });
61+
this.logoutLink = this.page.getByRole('link', { name: 'Logout' });
62+
this.contactHeader = this.page.getByRole('heading', { name: 'Submit a request' });
63+
}
64+
65+
async navigateToDashboard() {
66+
await this.page.goto(`${ACCTS_HUB_URL}/dashboard`);
67+
await this.waitForPageToSettle();
68+
// This test expects that the signed-in user already has a tb pro subscription setup
69+
// so the dashboard is displayed after sign-in and not the tb pro subscription page
70+
await expect.poll(async () => new URL(this.page.url()).pathname).toBe('/dashboard');
71+
}
72+
73+
async verifyDashboardDisplayed() {
74+
await expect(this.myAccountHeading).toBeVisible();
75+
await expect(this.myAccountCard).toContainText(ACCTS_OIDC_EMAIL);
76+
await expect(this.privacyAndDataHeading).toBeVisible();
77+
await expect(this.manageYourServicesHeading).toBeVisible();
78+
await expect(this.currentSubscriptionHeading).toBeVisible();
79+
await expect(this.currentSubscriptionSection).toContainText(DASHBOARD_CURRENT_SUBSCRIPTION_PRICE);
80+
await expect(this.currentSubscriptionSection).toContainText(
81+
new RegExp(`${escapeRegExp(DASHBOARD_CURRENT_SUBSCRIPTION_MAIL_STORAGE)}\\s*of Mail Storage`),
82+
);
83+
await expect(this.currentSubscriptionSection).toContainText(
84+
new RegExp(`${escapeRegExp(DASHBOARD_CURRENT_SUBSCRIPTION_SEND_STORAGE)}\\s*of Send Storage`),
85+
);
86+
await expect(this.currentSubscriptionSection).toContainText(
87+
new RegExp(`${escapeRegExp(DASHBOARD_CURRENT_SUBSCRIPTION_EMAIL_ADDRESSES)}\\s*Email Addresses`),
88+
);
89+
await expect(this.currentSubscriptionSection).toContainText(
90+
new RegExp(`${escapeRegExp(DASHBOARD_CURRENT_SUBSCRIPTION_CUSTOM_DOMAINS)}\\s*Custom Domains`),
91+
);
92+
await expect(this.passwordChangeLink).toBeVisible();
93+
await expect(this.deleteAccountLink).toBeVisible();
94+
await expect(this.thundermailLink).toBeVisible();
95+
await expect(this.appointmentLink).toBeVisible();
96+
await expect(this.sendLink).toBeVisible();
97+
await expect(this.manageSubscriptionButton).toBeVisible({ timeout: TIMEOUT_30_SECONDS });
98+
}
99+
100+
async verifyPasswordChangeNavigation() {
101+
await this.passwordChangeLink.click();
102+
await this.waitForPageToSettle();
103+
await expect(this.updatePasswordHeader).toBeVisible({ timeout: TIMEOUT_30_SECONDS });
104+
}
105+
106+
async verifyDeleteAccountNavigationOnly() {
107+
await this.deleteAccountLink.click();
108+
await this.verifyContactScreenDisplayed();
109+
}
110+
111+
async verifyThundermailNavigation() {
112+
await this.thundermailLink.click();
113+
await this.waitForPageToSettle();
114+
await expect.poll(async () => new URL(this.page.url()).pathname).toBe('/mail');
115+
}
116+
117+
async verifyServiceLinksOpenConfiguredUrls() {
118+
const serviceUrls = await this.getConfiguredServiceUrls();
119+
await this.verifyLinkOpensPopup(this.appointmentLink, serviceUrls.appointment);
120+
await this.verifyLinkOpensPopup(this.sendLink, serviceUrls.send);
121+
}
122+
123+
async verifyManageSubscriptionOpensPortal() {
124+
const [popup] = await Promise.all([
125+
this.page.waitForEvent('popup'),
126+
this.manageSubscriptionButton.click(),
127+
]);
128+
129+
await expect.poll(async () => popup.url()).not.toBe('about:blank');
130+
expect(new URL(popup.url()).protocol).toMatch(/^https?:$/);
131+
expect(new URL(popup.url()).host).toBe(PADDLE_HOST);
132+
await popup.close();
133+
}
134+
135+
async verifyUserMenuControls() {
136+
await this.userAvatar.click();
137+
await expect(this.supportLink).toBeVisible();
138+
await expect(this.logoutLink).toBeVisible();
139+
await expect(this.logoutLink).toHaveAttribute('href', '/logout/');
140+
141+
await this.supportLink.click();
142+
await this.verifyContactScreenDisplayed();
143+
}
144+
145+
private async verifyContactScreenDisplayed() {
146+
await this.waitForPageToSettle();
147+
await expect.poll(async () => new URL(this.page.url()).pathname).toMatch(/^\/contact\/?$/);
148+
await expect(this.contactHeader).toBeVisible({ timeout: TIMEOUT_30_SECONDS });
149+
}
150+
151+
private async waitForPageToSettle() {
152+
await waitForVueApp(this.page);
153+
await this.page.waitForLoadState('networkidle', { timeout: TIMEOUT_5_SECONDS }).catch(() => {});
154+
await this.page.waitForTimeout(TIMEOUT_2_SECONDS);
155+
}
156+
157+
private async getConfiguredServiceUrls(): Promise<ServiceUrls> {
158+
return await this.page.evaluate(() => ({
159+
appointment: (window as any)._page?.tbProAppointmentUrl,
160+
send: (window as any)._page?.tbProSendUrl,
161+
}));
162+
}
163+
164+
private async verifyLinkOpensPopup(link: Locator, expectedUrl: string) {
165+
expect(expectedUrl).toBeTruthy();
166+
await expect(link).toHaveAttribute('href', expectedUrl);
167+
await expect(link).toHaveAttribute('target', '_blank');
168+
169+
const [popup] = await Promise.all([
170+
this.page.waitForEvent('popup'),
171+
link.click(),
172+
]);
173+
174+
await expect.poll(async () => popup.url()).not.toBe('about:blank');
175+
expect(new URL(popup.url()).href).toBe(new URL(expectedUrl).href);
176+
await popup.close();
177+
}
178+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { test } from '@playwright/test';
2+
import { DashboardPage } from '../../pages/dashboard-page';
3+
import { ensureWeAreSignedIn } from '../../utils/utils';
4+
5+
import {
6+
PLAYWRIGHT_TAG_E2E_SUITE,
7+
PLAYWRIGHT_TAG_E2E_PROD_DESKTOP_NIGHTLY,
8+
ACCTS_TARGET_ENV,
9+
} from '../../const/constants';
10+
11+
let dashboardPage: DashboardPage;
12+
13+
test.beforeEach(async ({ page }) => {
14+
dashboardPage = new DashboardPage(page);
15+
await ensureWeAreSignedIn(page);
16+
});
17+
18+
/**
19+
* Prerequisite: This test assumes that the signed-in user already has a TB Pro subscription
20+
* and therefore the main dashboard is visible (and not redirected to the subscription page).
21+
* If this test is running in CI on a new local stack (i.e. on PRs) the test user (admin)
22+
* won't yet have a subscription, therefore skip this test when running in CI on PRs. This
23+
* test can run in the nighlty tests though because the existing test accounts on stage and
24+
* prod already have a TB Pro subscription setup. In future we will add a subscribe step so
25+
* we can run on new local stacks that aren't subscribed yet (issue 771).
26+
*/
27+
test.describe('dashboard controls on desktop browser', {
28+
tag: [PLAYWRIGHT_TAG_E2E_SUITE, PLAYWRIGHT_TAG_E2E_PROD_DESKTOP_NIGHTLY],
29+
}, () => {
30+
test('all visible dashboard controls work as expected', async () => {
31+
test.skip(ACCTS_TARGET_ENV == 'dev', 'Skipping this test when running on local dev stack until we automate tb subsrcibe step');
32+
33+
await dashboardPage.navigateToDashboard();
34+
await dashboardPage.verifyDashboardDisplayed();
35+
36+
await dashboardPage.verifyPasswordChangeNavigation();
37+
await dashboardPage.navigateToDashboard();
38+
39+
await dashboardPage.verifyDeleteAccountNavigationOnly();
40+
await dashboardPage.navigateToDashboard();
41+
42+
await dashboardPage.verifyThundermailNavigation();
43+
await dashboardPage.navigateToDashboard();
44+
45+
await dashboardPage.verifyServiceLinksOpenConfiguredUrls();
46+
await dashboardPage.verifyManageSubscriptionOpensPortal();
47+
await dashboardPage.verifyUserMenuControls();
48+
});
49+
});
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { test } from '@playwright/test';
2+
import { DashboardPage } from '../../pages/dashboard-page';
3+
import { navigateToAccountsHubAndSignIn } from '../../utils/utils';
4+
5+
import {
6+
PLAYWRIGHT_TAG_E2E_SUITE_MOBILE,
7+
PLAYWRIGHT_TAG_E2E_PROD_MOBILE_NIGHTLY,
8+
ACCTS_TARGET_ENV,
9+
} from '../../const/constants';
10+
11+
let dashboardPage: DashboardPage;
12+
13+
test.beforeEach(async ({ page }) => {
14+
dashboardPage = new DashboardPage(page);
15+
await navigateToAccountsHubAndSignIn(page);
16+
});
17+
18+
/**
19+
* Prerequisite: This test assumes that the signed-in user already has a TB Pro subscription
20+
* and therefore the main dashboard is visible (and not redirected to the subscription page).
21+
* If this test is running in CI on a new local stack (i.e. on PRs) the test user (admin)
22+
* won't yet have a subscription, therefore skip this test when running in CI on PRs. This
23+
* test can run in the nighlty tests though because the existing test accounts on stage and
24+
* prod already have a TB Pro subscription setup. In future we will add a subscribe step so
25+
* we can run on new local stacks that aren't subscribed yet (issue 771).
26+
*/
27+
test.describe('dashboard controls on mobile browser', {
28+
tag: [PLAYWRIGHT_TAG_E2E_SUITE_MOBILE, PLAYWRIGHT_TAG_E2E_PROD_MOBILE_NIGHTLY],
29+
}, () => {
30+
test('all visible dashboard controls work as expected', async () => {
31+
test.skip(ACCTS_TARGET_ENV == 'dev', 'Skipping this test when running on local dev stack until we automate tb subsrcibe step');
32+
33+
await dashboardPage.navigateToDashboard();
34+
await dashboardPage.verifyDashboardDisplayed();
35+
36+
await dashboardPage.verifyPasswordChangeNavigation();
37+
await dashboardPage.navigateToDashboard();
38+
39+
await dashboardPage.verifyDeleteAccountNavigationOnly();
40+
await dashboardPage.navigateToDashboard();
41+
42+
await dashboardPage.verifyThundermailNavigation();
43+
await dashboardPage.navigateToDashboard();
44+
45+
await dashboardPage.verifyServiceLinksOpenConfiguredUrls();
46+
await dashboardPage.verifyManageSubscriptionOpensPortal();
47+
await dashboardPage.verifyUserMenuControls();
48+
});
49+
});

0 commit comments

Comments
 (0)