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(); }); });