diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml
index 9a220daf9..7a8e7ceab 100644
--- a/.github/workflows/deploy-production.yml
+++ b/.github/workflows/deploy-production.yml
@@ -242,6 +242,8 @@ jobs:
env:
APPT_PROD_LOGIN_EMAIL: ${{ secrets.E2E_APPT_PROD_LOGIN_EMAIL }}
APPT_PROD_LOGIN_PWORD: ${{ secrets.E2E_APPT_PROD_LOGIN_PASSWORD }}
+ APPT_PROD_DISPLAY_NAME: ${{ secrets.E2E_APPT_PROD_DISPLAY_NAME }}
+ APPT_PROD_MY_SHARE_LINK: ${{ secrets.E2E_APPT_PROD_MY_SHARE_LINK }}
steps:
- uses: actions/checkout@v4
diff --git a/.github/workflows/nightly-tests.yml b/.github/workflows/nightly-tests.yml
index eb7f02a9d..45bd865e7 100644
--- a/.github/workflows/nightly-tests.yml
+++ b/.github/workflows/nightly-tests.yml
@@ -22,6 +22,8 @@ jobs:
env:
APPT_PROD_LOGIN_EMAIL: ${{ secrets.E2E_APPT_PROD_LOGIN_EMAIL }}
APPT_PROD_LOGIN_PWORD: ${{ secrets.E2E_APPT_PROD_LOGIN_PASSWORD }}
+ APPT_PROD_DISPLAY_NAME: ${{ secrets.E2E_APPT_PROD_DISPLAY_NAME }}
+ APPT_PROD_MY_SHARE_LINK: ${{ secrets.E2E_APPT_PROD_MY_SHARE_LINK }}
steps:
- uses: actions/checkout@v4
diff --git a/test/e2e/.env.example b/test/e2e/.env.example
index 5d35ca009..8e009731b 100644
--- a/test/e2e/.env.example
+++ b/test/e2e/.env.example
@@ -2,7 +2,15 @@
# URLs
APPT_PROD_URL=https://appointment.day/
+APPT_PROD_SHORT_SHARE_LINK_PREFIX=https://apmt.day/
+APPT_PROD_LONG_SHARE_LINK_PREFIX=https://appointment.day/user/
# Production sign-in (FxA) credentials
APPT_PROD_LOGIN_EMAIL=
APPT_PROD_LOGIN_PWORD=
+
+# Appointment user display name (settings => account => display name) for above user
+APPT_PROD_DISPLAY_NAME=
+
+# Production booking share link for the existing user above (settings => account => my link)
+APPT_PROD_MY_SHARE_LINK=
diff --git a/test/e2e/README.md b/test/e2e/README.md
index b15d09ef8..c682bdc76 100644
--- a/test/e2e/README.md
+++ b/test/e2e/README.md
@@ -23,17 +23,26 @@ npx playwright install
## Running Locally
-The E2E tests require credentials for an existing Appointment (FxA) account and reads these from your local env vars. First copy over the provided `.example.env` to a local `.env`:
+The E2E tests require credentials for an existing Appointment (FxA) account and reads these from your local env vars.
+The tests also require the existing Appointment account user's display name and share link.
+
+The display name is found in Appointment => Settings => Account => Display name.
+
+The share link is found in Appointment => Settings => Account => My Link.
+
+First copy over the provided `.example.env` to a local `.env`:
```bash
cd test/e2e
cp .env.example .env
```
-Then edit your local `.env` file and provide the credentials for your Appointment test account:
+Then edit your local `.env` file and provide the following values:
```dotenv
APPT_PROD_LOGIN_EMAIL=
APPT_PROD_LOGIN_PWORD=
+APPT_PROD_ACCT_DISPLAY_NAME=
+APPT_PROD_MY_SHARE_LINK=
```
To run the production sanity test headless (still in `test/e2e`):
@@ -60,11 +69,13 @@ You can run the E2E tests from your local machine but against browsers provided
For security reasons when running the tests on BrowserStack I recommend that you use a dedicated test Appointment FxA account / credentials (NOT your own personal Appointment (FxA) credentials).
-Once you have credentials for an existing Appointemnt test account, edit your local `.env` file and add the credentials:
+Once you have credentials for an existing Appointemnt test account, edit your local `.env` file and add these details:
```dotenv
APPT_PROD_LOGIN_EMAIL=
APPT_PROD_LOGIN_PWORD=
+APPT_PROD_ACCT_DISPLAY_NAME=
+APPT_PROD_MY_SHARE_LINK=
```
Also in order to run on BrowserStack you need to provide your BrowserStack credentials. Sign into your BrowserStack account and navigate to your `User Profile` and find your auth username and access key. In your local terminal export the following env vars to set the BrowserStack credentials that the tests will use:
diff --git a/test/e2e/browserstack.yml b/test/e2e/browserstack.yml
index a9e3199de..c37dba25e 100644
--- a/test/e2e/browserstack.yml
+++ b/test/e2e/browserstack.yml
@@ -29,10 +29,14 @@ platforms:
osVersion: Sequoia
browserName: playwright-firefox
browserVersion: latest
+ playwrightConfigOptions:
+ name: Firefox-OSX
- os: OS X
osVersion: Sequoia
browserName: playwright-chromium
browserVersion: latest
+ playwrightConfigOptions:
+ name: Chromium-OSX
# =======================
# Parallels per Platform
diff --git a/test/e2e/const/constants.ts b/test/e2e/const/constants.ts
index 9f6e1a688..fdc22debe 100644
--- a/test/e2e/const/constants.ts
+++ b/test/e2e/const/constants.ts
@@ -1,10 +1,16 @@
// appointment urls
export const APPT_PROD_URL = process.env.APPT_PROD_URL;
+export const APPT_PROD_MY_SHARE_LINK = process.env.APPT_PROD_MY_SHARE_LINK;
+export const APPT_PROD_SHORT_SHARE_LINK_PREFIX=process.env.APPT_PROD_SHORT_SHARE_LINK_PREFIX;
+export const APPT_PROD_LONG_SHARE_LINK_PREFIX=process.env.APPT_PROD_LONG_SHARE_LINK_PREFIX;
// page titles
export const APPT_PAGE_TITLE = 'Thunderbird Appointment';
export const FXA_PAGE_TITLE = 'Mozilla accounts';
-// production sign-in credentials
+// production sign-in credentials and corresponding account display name
export const PROD_LOGIN_EMAIL = process.env.APPT_PROD_LOGIN_EMAIL;
export const PROD_LOGIN_PWORD = process.env.APPT_PROD_LOGIN_PWORD;
+
+// appointment user display name (settings => account) for above user
+export const PROD_DISPLAY_NAME = process.env.APPT_PROD_DISPLAY_NAME;
diff --git a/test/e2e/pages/booking-page.ts b/test/e2e/pages/booking-page.ts
new file mode 100644
index 000000000..7c142ebb6
--- /dev/null
+++ b/test/e2e/pages/booking-page.ts
@@ -0,0 +1,24 @@
+import { type Page, type Locator } from '@playwright/test';
+
+export class BookingPage {
+ readonly page: Page;
+ readonly titleText: Locator;
+ readonly invitingText: Locator;
+ readonly confirmButton: Locator;
+ readonly bookingCalendar: Locator;
+ readonly calendarHeader: Locator;
+
+ constructor(page: Page) {
+ this.page = page;
+ this.titleText = page.getByTestId('booking-view-title-text');
+ this.invitingText = page.getByTestId('booking-view-inviting-you-text');
+ this.bookingCalendar = page.getByTestId('booking-view-calendar-div');
+ this.confirmButton = page.getByTestId('booking-view-confirm-selection-button');
+ this.calendarHeader = page.locator('.calendar-header__period-name');
+ }
+
+ async gotoBookingPage(bookingPageURL: string) {
+ await this.page.goto(bookingPageURL);
+ await this.page.waitForLoadState('domcontentloaded');
+ }
+}
diff --git a/test/e2e/pages/dashboard-page.ts b/test/e2e/pages/dashboard-page.ts
index 71f9ffaca..9e80e2d82 100644
--- a/test/e2e/pages/dashboard-page.ts
+++ b/test/e2e/pages/dashboard-page.ts
@@ -1,5 +1,4 @@
-import { expect, type Page, type Locator } from '@playwright/test';
-import exp from 'constants';
+import { type Page, type Locator } from '@playwright/test';
export class DashboardPage {
readonly page: Page;
diff --git a/test/e2e/tests/book-appointment.spec.ts b/test/e2e/tests/book-appointment.spec.ts
new file mode 100644
index 000000000..f36fbf2a1
--- /dev/null
+++ b/test/e2e/tests/book-appointment.spec.ts
@@ -0,0 +1,48 @@
+import { test, expect } from '@playwright/test';
+import { BookingPage } from '../pages/booking-page';
+import { PROD_DISPLAY_NAME, APPT_PROD_MY_SHARE_LINK } from '../const/constants';
+import { APPT_PROD_SHORT_SHARE_LINK_PREFIX, APPT_PROD_LONG_SHARE_LINK_PREFIX } from '../const/constants';
+
+let bookingPage: BookingPage;
+
+// verify booking page loaded successfully
+const verifyBookingPageLoaded = async () => {
+ await expect(bookingPage.titleText).toBeVisible({ timeout: 30_000 });
+ await expect(bookingPage.titleText).toContainText(String(PROD_DISPLAY_NAME));
+ await expect(bookingPage.invitingText).toBeVisible();
+ await expect(bookingPage.invitingText).toContainText(String(PROD_DISPLAY_NAME));
+ await expect(bookingPage.bookingCalendar).toBeVisible();
+ // calendar header should contain current MMM YYYY
+ let today = new Date();
+ let curMonth = today.toLocaleString('default', { month: 'short' });
+ var curYear = String(today.getFullYear());
+ await expect(bookingPage.calendarHeader).toHaveText(`${curMonth} ${curYear}`);
+ // confirm button is disabled by default until a slot is selected
+ await expect(bookingPage.confirmButton).toBeDisabled();
+}
+
+test.beforeEach(async ({ page }) => {
+ bookingPage = new BookingPage(page);
+});
+
+// verify we are able to book an appointment using existing user's share link
+test.describe('book an appointment', {
+ tag: '@prod-sanity'
+}, () => {
+ test('able to access booking page via short link', async ({ page }) => {
+ await bookingPage.gotoBookingPage(String(APPT_PROD_MY_SHARE_LINK));
+ await verifyBookingPageLoaded();
+ });
+
+ test('able to access booking page via long link', async ({ page }) => {
+ // the share link is short by default; build the corresponding long link first
+ let prodShareLinkUser: string = String(APPT_PROD_MY_SHARE_LINK).split(String(APPT_PROD_SHORT_SHARE_LINK_PREFIX))[1];
+ let prodShareLinkLong: string = `${APPT_PROD_LONG_SHARE_LINK_PREFIX}${prodShareLinkUser}`;
+ await bookingPage.gotoBookingPage(prodShareLinkLong);
+ await verifyBookingPageLoaded();
+ });
+
+ test('able to request a booking', async ({ page }) => {
+ await bookingPage.gotoBookingPage(String(APPT_PROD_MY_SHARE_LINK));
+ });
+});
diff --git a/test/e2e/tests/sign-in.spec.ts b/test/e2e/tests/sign-in.spec.ts
index 7526417f0..39516c67f 100644
--- a/test/e2e/tests/sign-in.spec.ts
+++ b/test/e2e/tests/sign-in.spec.ts
@@ -1,19 +1,19 @@
import { test, expect } from '@playwright/test';
import { SplashscreenPage } from '../pages/splashscreen-page';
import { FxAPage } from '../pages/fxa-page';
-import { FXA_PAGE_TITLE, APPT_PAGE_TITLE } from '../const/constants';
+import { APPT_PAGE_TITLE } from '../const/constants';
import { DashboardPage } from '../pages/dashboard-page';
-let splashscreen: SplashscreenPage;
-let fxa_sign_in: FxAPage;
-let dashboard_page: DashboardPage;
+let splashscreenPage: SplashscreenPage;
+let signInPage: FxAPage;
+let dashboardPage: DashboardPage;
test.beforeEach(async ({ page }) => {
// navigate to the main appointment page (splashscreen)
- splashscreen = new SplashscreenPage(page);
- fxa_sign_in = new FxAPage(page);
- dashboard_page = new DashboardPage(page);
- await splashscreen.gotoProd();
+ splashscreenPage = new SplashscreenPage(page);
+ signInPage = new FxAPage(page);
+ dashboardPage = new DashboardPage(page);
+ await splashscreenPage.gotoProd();
});
// verify we are able to sign-in
@@ -21,19 +21,19 @@ test.describe('sign-in', {
tag: '@prod-sanity'
}, () => {
test('able to sign-in', async ({ page }) => {
- await splashscreen.getToFxA();
+ await splashscreenPage.getToFxA();
- await expect(fxa_sign_in.signInHeaderText).toBeVisible({ timeout: 30_000 }); // generous time for fxa to appear
- await expect(fxa_sign_in.userAvatar).toBeVisible({ timeout: 30_000});
- await expect(fxa_sign_in.signInButton).toBeVisible();
+ await expect(signInPage.signInHeaderText).toBeVisible({ timeout: 30_000 }); // generous time for fxa to appear
+ await expect(signInPage.userAvatar).toBeVisible({ timeout: 30_000});
+ await expect(signInPage.signInButton).toBeVisible();
- await fxa_sign_in.signIn();
+ await signInPage.signIn();
await page.waitForLoadState('domcontentloaded');
await expect(page).toHaveTitle(APPT_PAGE_TITLE, { timeout: 30_000 }); // give generous time for fxa sign-in
- await expect(dashboard_page.userMenuAvatar).toBeVisible({ timeout: 30_000 });
- await expect(dashboard_page.navBarDashboardBtn).toBeVisible({ timeout: 30_000 });
- await expect(dashboard_page.shareMyLink).toBeVisible({ timeout: 30_000 });
+ await expect(dashboardPage.userMenuAvatar).toBeVisible({ timeout: 30_000 });
+ await expect(dashboardPage.navBarDashboardBtn).toBeVisible({ timeout: 30_000 });
+ await expect(dashboardPage.shareMyLink).toBeVisible({ timeout: 30_000 });
});
});
diff --git a/test/e2e/tests/splashscreen.spec.ts b/test/e2e/tests/splashscreen.spec.ts
index 010e2fd4b..0c05b9c7d 100644
--- a/test/e2e/tests/splashscreen.spec.ts
+++ b/test/e2e/tests/splashscreen.spec.ts
@@ -2,12 +2,12 @@ import { test, expect } from '@playwright/test';
import { SplashscreenPage } from '../pages/splashscreen-page';
import { APPT_PAGE_TITLE } from '../const/constants';
-let splashscreen: SplashscreenPage;
+let splashscreenPage: SplashscreenPage;
test.beforeEach(async ({ page }) => {
// navigate to the main appointment page (splashscreen)
- splashscreen = new SplashscreenPage(page);
- await splashscreen.gotoProd();
+ splashscreenPage = new SplashscreenPage(page);
+ await splashscreenPage.gotoProd();
});
// verify main appointment splash screen appears correctly
@@ -16,7 +16,7 @@ test.describe('splash screen', {
}, () => {
test('appears correctly', async ({ page }) => {
await expect(page).toHaveTitle(APPT_PAGE_TITLE);
- await expect(splashscreen.loginBtn).toBeVisible();
- await expect(splashscreen.signUpBetaBtn).toBeVisible();
+ await expect(splashscreenPage.loginBtn).toBeVisible();
+ await expect(splashscreenPage.signUpBetaBtn).toBeVisible();
});
});