diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml
index 0b3c37486..042413e6e 100644
--- a/.github/workflows/deploy-production.yml
+++ b/.github/workflows/deploy-production.yml
@@ -244,7 +244,7 @@ jobs:
APPT_LOGIN_PWORD: ${{ secrets.E2E_APPT_PROD_LOGIN_PASSWORD }}
APPT_DISPLAY_NAME: ${{ secrets.E2E_APPT_PROD_DISPLAY_NAME }}
APPT_MY_SHARE_LINK: ${{ secrets.E2E_APPT_PROD_MY_SHARE_LINK }}
- APPT_BOOKING_REQUESTER_EMAIL: ${{ secrets.E2E_APPT_PROD_BOOKING_REQUESTER_EMAIL }}
+ APPT_BOOKEE_EMAIL: ${{ secrets.E2E_APPT_PROD_BOOKEE_EMAIL }}
steps:
- uses: actions/checkout@v4
diff --git a/.github/workflows/nightly-tests.yml b/.github/workflows/nightly-tests.yml
index 5f8d8a527..6390dbd80 100644
--- a/.github/workflows/nightly-tests.yml
+++ b/.github/workflows/nightly-tests.yml
@@ -24,7 +24,7 @@ jobs:
APPT_LOGIN_PWORD: ${{ secrets.E2E_APPT_PROD_LOGIN_PASSWORD }}
APPT_DISPLAY_NAME: ${{ secrets.E2E_APPT_PROD_DISPLAY_NAME }}
APPT_MY_SHARE_LINK: ${{ secrets.E2E_APPT_PROD_MY_SHARE_LINK }}
- APPT_BOOKING_REQUESTER_EMAIL: ${{ secrets.E2E_APPT_PROD_BOOKING_REQUESTER_EMAIL }}
+ APPT_BOOKEE_EMAIL: ${{ secrets.E2E_APPT_PROD_BOOKEE_EMAIL }}
steps:
- uses: actions/checkout@v4
diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
index 0cb04fc24..17d1db47f 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/validate.yml
@@ -192,3 +192,43 @@ jobs:
- name: Test with vitest
run: |
cd ./frontend && npm run test -- --run
+
+ e2e-tests-browserstack:
+ name: e2e-tests-browserstack
+ needs: detect-changes
+ if: needs.detect-changes.outputs.validate-frontend == 'true' || needs.detect-changes.outputs.validate-backend == 'true'
+ runs-on: ubuntu-latest
+ environment: staging
+ env:
+ APPT_LOGIN_EMAIL: ${{ secrets.E2E_APPT_STAGE_LOGIN_EMAIL }}
+ APPT_LOGIN_PWORD: ${{ secrets.E2E_APPT_STAGE_LOGIN_PASSWORD }}
+ APPT_DISPLAY_NAME: ${{ secrets.E2E_APPT_STAGE_DISPLAY_NAME }}
+ APPT_MY_SHARE_LINK: ${{ secrets.E2E_APPT_STAGE_MY_SHARE_LINK }}
+ APPT_BOOKEE_EMAIL: ${{ secrets.E2E_APPT_STAGE_BOOKEE_EMAIL }}
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: 'npm'
+ cache-dependency-path: 'test/e2e/package-lock.json'
+
+ - name: Install dependencies
+ run: |
+ cd ./test/e2e
+ npm install
+
+ - name: BrowserStack Env Setup
+ uses: browserstack/github-actions/setup-env@master
+ with:
+ username: ${{ secrets.BROWSERSTACK_USERNAME }}
+ access-key: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
+ project-name: 'Thunderbird Appointment'
+ build-name: 'E2E Tests: BUILD_INFO'
+
+ - name: Run E2E Tests on Browserstack
+ run: |
+ cd ./test/e2e
+ cp .env.stage.example .env
+ npm run e2e-test-browserstack-gha
diff --git a/frontend/.env.example b/frontend/.env.example
index 402a2bb28..338eebed9 100644
--- a/frontend/.env.example
+++ b/frontend/.env.example
@@ -3,7 +3,7 @@
# -- Frontend --
VITE_BASE_URL=localhost:8080
VITE_SHORT_BASE_URL=http://localhost:8080/user
-# Set true to activate polling for dev server
+# Set true to activate polling for dev server
VITE_SERVER_WATCH_POLLING=
VITE_SERVER_WATCH_INTERVAL=
VITE_SERVER_WATCH_BINARY_INTERVAL=
@@ -34,4 +34,3 @@ VITE_DEFAULT_HOUR_FORMAT=12
# You could use a url or a mailto: email address.
VITE_SUPPORT_URL=
-
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index e1e3e5a69..6e6a18db0 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -33,7 +33,6 @@ const apiUrl = inject(apiUrlKey);
const route = useRoute();
const routeName = typeof route.name === 'string' ? route.name : '';
const router = useRouter();
-const lang = defaultLocale();
const siteNotificationStore = useSiteNotificationStore();
const {
@@ -166,7 +165,7 @@ const onPageLoad = async () => {
resolution: deviceRes,
effective_resolution: effectiveDeviceRes,
user_agent: navigator.userAgent,
- locale: lang,
+ locale: defaultLocale(),
}).json();
const { data } = response;
diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json
index 7bbabe290..768e1e531 100644
--- a/frontend/src/locales/en.json
+++ b/frontend/src/locales/en.json
@@ -450,7 +450,7 @@
"recipientsCanScheduleBetween": "Recipients can schedule a {duration} appointment between {earliest} and {farthest} ahead of time.",
"redirectedNotice": "You are being redirected to {url}.",
"refreshLinkNotice": "This refreshes your link. Your old links will no longer work.",
- "requestInformationSentToOwner": "An information about this booking request has been emailed to the owner.",
+ "requestInformationSentToOwner": "Information about this booking request has been emailed to the owner.",
"scheduleSettings": {
"clickHereToConnect": "Click here to connect a calendar!",
"create": "Select a calendar under Scheduling details and click save to get started!",
diff --git a/frontend/src/views/ProfileView.vue b/frontend/src/views/ProfileView.vue
index 406e9d4c5..f5bbdc07e 100644
--- a/frontend/src/views/ProfileView.vue
+++ b/frontend/src/views/ProfileView.vue
@@ -1,5 +1,5 @@
diff --git a/test/e2e/.env.dev.example b/test/e2e/.env.dev.example
new file mode 100644
index 000000000..be14dd47b
--- /dev/null
+++ b/test/e2e/.env.dev.example
@@ -0,0 +1,20 @@
+# Appointment E2E Test Configuration
+APPT_TARGET_ENV=dev
+
+# URLs
+APPT_URL=http://localhost:8080/
+
+# Local sign-in credentials
+APPT_LOGIN_EMAIL=
+APPT_LOGIN_PWORD=
+
+# Local Appointment user display name (settings => account => display name) for above user
+APPT_DISPLAY_NAME=
+
+# Booking share link for the existing local user above (settings => account => my link)
+APPT_MY_SHARE_LINK=
+
+# Booking requester's name and email address (used when a booking slot is requested via the
+# share link). Important: real appointment booking emails will be sent to the provided email.
+APPT_BOOKEE_NAME='Automated-Test-Bot-Dev'
+APPT_BOOKEE_EMAIL=
diff --git a/test/e2e/.env.prod.example b/test/e2e/.env.prod.example
index 2d7ab6910..23b77d2f8 100644
--- a/test/e2e/.env.prod.example
+++ b/test/e2e/.env.prod.example
@@ -1,4 +1,5 @@
# Appointment E2E Test Configuration
+APPT_TARGET_ENV=prod
# URLs
APPT_URL=https://appointment.day/
@@ -17,5 +18,5 @@ APPT_MY_SHARE_LINK=
# Booking requester's name and email address (used when a booking slot is requested via the
# share link). Important: real appointment booking emails will be sent to the provided email.
-APPT_BOOKING_REQUESTER_NAME='Automated-Test-Bot'
-APPT_BOOKING_REQUESTER_EMAIL=
+APPT_BOOKEE_NAME='Automated-Test-Bot-Prod'
+APPT_BOOKEE_EMAIL=
diff --git a/test/e2e/.env.stage.example b/test/e2e/.env.stage.example
new file mode 100644
index 000000000..472139136
--- /dev/null
+++ b/test/e2e/.env.stage.example
@@ -0,0 +1,22 @@
+# Appointment E2E Test Configuration
+APPT_TARGET_ENV=stage
+
+# URLs
+APPT_URL=https://stage.appointment.day/
+APPT_SHORT_SHARE_LINK_PREFIX=https://stage.apmt.day/
+APPT_LONG_SHARE_LINK_PREFIX=https://stage.appointment.day/user/
+
+# Sign-in (FxA) credentials
+APPT_LOGIN_EMAIL=
+APPT_LOGIN_PWORD=
+
+# Appointment user display name (settings => account => display name) for above user
+APPT_DISPLAY_NAME=
+
+# Booking share link for the existing user above (settings => account => my link)
+APPT_MY_SHARE_LINK=
+
+# Booking requester's name and email address (used when a booking slot is requested via the
+# share link). Important: real appointment booking emails will be sent to the provided email.
+APPT_BOOKEE_NAME='Automated-Test-Bot-Stage'
+APPT_BOOKEE_EMAIL=
diff --git a/test/e2e/README.md b/test/e2e/README.md
index 8a5976053..94c527075 100644
--- a/test/e2e/README.md
+++ b/test/e2e/README.md
@@ -2,12 +2,6 @@
Guide for running the Thunderbird Appointment E2E tests.
-## Prerequisite
-
-You must have a pre-existing Appointment user test account (using FxA credentials) on the platform where you are running the tests. ie. For the production sanity test you must have an Appointment test account on production (using production FxA credentials) already set up.
-
-The tests expect that default Appointment application settings exist for the provided test user; for example the user scheduling availability hasn't been changed from the default settings; and the default calendar view is the current month view. This is important so that the tests can find an available booking slot, etc.
-
## Installation
First install the E2E suite (includes Playwright):
@@ -23,13 +17,95 @@ Next install the Playwright browsers (Playwright uses it's own bundled browers)
npx playwright install
```
-## Running Locally
+## E2E Test Prerequisites
+The E2E tests require an existing Appointment (and corresponding FxA account) and associated data, and reads this from your local .env file. This includes:
+- Credentials for an existing Appointment (FxA) account (email address, password)
+- The 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.
+- The tests also require an email address to be used as the appointment bookee's email address when actually requesting bookings. This is the email address entered on the `Book selection` dialog (after an appointment slot was selected on the booking share link page). Note that real Appointment emails will be sent to this email address.
+
+The tests expect that the default Appointment application settings haven't been changed for the provided test user:
+- The user scheduling availability hasn't been changed from the default settings;
+- In the dashboard the default calendar view is the current month view; this is important so that the tests can find an available booking slot, etc.
+- In `Booking Settings`, the `Booking Confirmation` option is enabled, so that requested appointments generate HOLD appointments that need to be confirmed
+
+## Running the E2E tests against your local dev environment
+
+First ensure that you have a local Appointment account created and you can sign in to Appointment at http://localhost:8080/.
+
+Then copy over the provided `.env.dev.example` to a local `.env`:
+
+```bash
+cd test/e2e
+cp .env.dev.example .env
+```
+
+Then edit your local `.env` file and provide the following values:
+```dotenv
+APPT_LOGIN_EMAIL=
+APPT_LOGIN_PWORD=
+APPT_DISPLAY_NAME=
+APPT_MY_SHARE_LINK=
+APPT_BOOKEE_EMAIL=
+```
+
+To run the E2E tests headless (still in `test/e2e`):
+
+```bash
+npm run e2e-test
+```
+
+To run the E2E tests with a UI so you can watch the tests run (still in `test/e2e`):
+
+```bash
+npm run e2e-test-headed
+```
+
+To run the E2E tests in debug mode (still in `test/e2e`):
+
+```bash
+npm run e2e-test-debug
+```
+
+## Running the E2E tests against the staging environmnent
+
+First copy over the provided `.env.stage.example` to a local `.env`:
+
+```bash
+cd test/e2e
+cp .env.stage.example .env
+```
+
+Then edit your local `.env` file and provide the following values:
+```dotenv
+APPT_LOGIN_EMAIL=
+APPT_LOGIN_PWORD=
+APPT_DISPLAY_NAME=
+APPT_MY_SHARE_LINK=
+APPT_BOOKEE_EMAIL=
+```
+
+To run the E2E tests headless (still in `test/e2e`):
+
+```bash
+npm run e2e-test
+```
+
+To run the E2E tests with a UI so you can watch the tests run (still in `test/e2e`):
+
+```bash
+npm run e2e-test-headed
+```
+
+To run the E2E tests in debug mode (still in `test/e2e`):
+
+```bash
+npm run e2e-test-debug
+```
+
+## Running the production sanity test
-The E2E tests require credentials for an existing Appointment (FxA) account and reads these from your local env vars.
-This includes the existing Appointment account's email address, password, 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.
-The tests also require an email address to be used when actually requesting bookings. This is the email address entered on the `Book selection` dialog (after an appoitment slot was selected on the booking share link page). Note that real Appointment emails will be sent to this email address.
First copy over the provided `.env.prod.example` to a local `.env`:
```bash
@@ -39,11 +115,11 @@ cp .env.prod.example .env
Then edit your local `.env` file and provide the following values:
```dotenv
-APPT_LOGIN_EMAIL=
-APPT_LOGIN_PWORD=
-APPT_DISPLAY_NAME=
-APPT_MY_SHARE_LINK=
-APPT_BOOKING_REQUESTER_EMAIL=
+APPT_LOGIN_EMAIL=
+APPT_LOGIN_PWORD=
+APPT_DISPLAY_NAME=
+APPT_MY_SHARE_LINK=
+APPT_BOOKEE_EMAIL=
```
To run the production sanity test headless (still in `test/e2e`):
@@ -55,7 +131,7 @@ npm run prod-sanity-test
To run the production sanity test with a UI so you can watch the tests run (still in `test/e2e`):
```bash
-npm run prod-sanity-test-ui
+npm run prod-sanity-test-headed
```
To run the production sanity test in debug mode (still in `test/e2e`):
@@ -77,7 +153,7 @@ APPT_LOGIN_EMAIL=
APPT_LOGIN_PWORD=
APPT_DISPLAY_NAME=
APPT_MY_SHARE_LINK=
-APPT_BOOKING_REQUESTER_EMAIL=
+APPT_BOOKEE_EMAIL=
```
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:
@@ -90,6 +166,12 @@ export BROWSERSTACK_USERNAME=
export BROWSERSTACK_ACCESS_KEY=
```
+To run the E2E tests on BrowserStack (still in `test/e2e`):
+
+```bash
+npm run e2e-test-browserstack
+```
+
To run the production sanity test on BrowserStack (still in `test/e2e`):
```bash
diff --git a/test/e2e/const/constants.ts b/test/e2e/const/constants.ts
index 4cdd12c2f..ef6107234 100644
--- a/test/e2e/const/constants.ts
+++ b/test/e2e/const/constants.ts
@@ -1,21 +1,29 @@
+// environment where the tests will run
+export const APPT_TARGET_ENV = String(process.env.APPT_TARGET_ENV);
+
// appointment urls
export const APPT_URL = String(process.env.APPT_URL);
export const APPT_MY_SHARE_LINK = String(process.env.APPT_MY_SHARE_LINK);
export const APPT_SHORT_SHARE_LINK_PREFIX = String(process.env.APPT_SHORT_SHARE_LINK_PREFIX);
export const APPT_LONG_SHARE_LINK_PREFIX = String(process.env.APPT_LONG_SHARE_LINK_PREFIX);
-export const APPT_PROD_PENDING_BOOKINGS_PAGE = `${process.env.APPT_URL}bookings/pending`;
+export const APPT_PENDING_BOOKINGS_PAGE = `${process.env.APPT_URL}bookings/pending`;
+export const APPT_BOOKED_BOOKINGS_PAGE = `${process.env.APPT_URL}bookings/booked`;
// page titles
export const APPT_PAGE_TITLE = 'Thunderbird Appointment';
export const FXA_PAGE_TITLE = 'Mozilla accounts';
-// production sign-in credentials and corresponding account display name
-export const PROD_LOGIN_EMAIL = String(process.env.APPT_LOGIN_EMAIL);
-export const PROD_LOGIN_PWORD = String(process.env.APPT_LOGIN_PWORD);
+// sign-in credentials and corresponding account display name
+export const APPT_LOGIN_EMAIL = String(process.env.APPT_LOGIN_EMAIL);
+export const APPT_LOGIN_PWORD = String(process.env.APPT_LOGIN_PWORD);
// appointment user display name (settings => account) for above user
-export const PROD_DISPLAY_NAME = String(process.env.APPT_DISPLAY_NAME);
+export const APPT_DISPLAY_NAME = String(process.env.APPT_DISPLAY_NAME);
// appointment requester's name and email address
-export const APPT_BOOKING_REQUESTER_NAME = String(process.env.APPT_BOOKING_REQUESTER_NAME);
-export const APPT_BOOKING_REQUESTER_EMAIL = String(process.env.APPT_BOOKING_REQUESTER_EMAIL);
+export const APPT_BOOKEE_NAME = String(process.env.APPT_BOOKEE_NAME);
+export const APPT_BOOKEE_EMAIL = String(process.env.APPT_BOOKEE_EMAIL);
+
+// playwright test tags
+export const PLAYWRIGHT_TAG_PROD_SANITY = '@prod-sanity';
+export const PLAYWRIGHT_TAG_E2E_SUITE = '@e2e-suite';
diff --git a/test/e2e/package.json b/test/e2e/package.json
index 8a84c0421..8d6c7393f 100644
--- a/test/e2e/package.json
+++ b/test/e2e/package.json
@@ -3,8 +3,13 @@
"version": "1.0.0",
"main": "index.js",
"scripts": {
+ "e2e-test": "npx playwright test --grep e2e-suite --project=firefox",
+ "e2e-test-headed": "npx playwright test --grep e2e-suite --project=firefox --headed",
+ "e2e-test-debug": "npx playwright test --grep e2e-suite --project=firefox --headed --ui",
+ "e2e-test-browserstack": "npx browserstack-node-sdk playwright test --grep e2e-suite --browserstack.buildName 'Appointment E2E Tests'",
+ "e2e-test-browserstack-gha": "npx browserstack-node-sdk playwright test --grep e2e-suite",
"prod-sanity-test": "npx playwright test --grep prod-sanity --project=firefox",
- "prod-sanity-test-ui": "npx playwright test --grep prod-sanity --project=firefox --headed",
+ "prod-sanity-test-headed": "npx playwright test --grep prod-sanity --project=firefox --headed",
"prod-sanity-test-debug": "npx playwright test --grep prod-sanity --project=firefox --headed --ui",
"prod-sanity-test-browserstack": "npx browserstack-node-sdk playwright test --grep prod-sanity --browserstack.buildName 'Production Sanity Test'",
"prod-sanity-test-browserstack-gha": "npx browserstack-node-sdk playwright test --grep prod-sanity",
diff --git a/test/e2e/pages/booking-page.ts b/test/e2e/pages/booking-page.ts
index b4d4b52aa..8efd61701 100644
--- a/test/e2e/pages/booking-page.ts
+++ b/test/e2e/pages/booking-page.ts
@@ -17,6 +17,7 @@ export class BookingPage {
readonly requestSentTitleText: Locator;
readonly requestSentAvailabilityText: Locator;
readonly requestSentCloseBtn: Locator;
+ readonly eventBookedTitleText: Locator;
constructor(page: Page) {
this.page = page;
@@ -33,6 +34,7 @@ export class BookingPage {
this.requestSentTitleText = this.page.getByText('Booking request sent');
this.requestSentAvailabilityText = this.page.getByText("'s Availability");
this.requestSentCloseBtn = this.page.getByRole('button', { name: 'Close' });
+ this.eventBookedTitleText = this.page.getByText('Event booked!');
}
/**
diff --git a/test/e2e/pages/dashboard-page.ts b/test/e2e/pages/dashboard-page.ts
index 74859791b..9aac99357 100644
--- a/test/e2e/pages/dashboard-page.ts
+++ b/test/e2e/pages/dashboard-page.ts
@@ -1,5 +1,5 @@
import { expect, type Page, type Locator } from '@playwright/test';
-import { APPT_PROD_PENDING_BOOKINGS_PAGE } from '../const/constants';
+import { APPT_TARGET_ENV, APPT_PENDING_BOOKINGS_PAGE, APPT_BOOKED_BOOKINGS_PAGE } from '../const/constants';
export class DashboardPage {
@@ -23,16 +23,26 @@ export class DashboardPage {
this.nextMonthArrow = this.page.locator('[data-icon="chevron-right"]');
this.pendingBookingsPageHeader = this.page.getByText('Bookings');
this.pendingBookingsFilterSelect = this.page.getByTestId('bookings-filter-select');
- this. apptsFilterInput = this.page.getByPlaceholder('Search bookings');
+ this.apptsFilterInput = this.page.getByPlaceholder('Search bookings');
}
/**
* Navigate to the pending bookings page and display all future pending bookings
*/
async gotoPendingBookings() {
- await this.page.goto(APPT_PROD_PENDING_BOOKINGS_PAGE);
+ await this.page.goto(APPT_PENDING_BOOKINGS_PAGE);
await this.page.waitForLoadState('domcontentloaded');
- // ensure all future bookings are displayed
+ // ensure all future pending bookings are displayed
+ await this.pendingBookingsFilterSelect.selectOption(this.allFutureBookingsOptionText, { timeout: 60_000 });
+ }
+
+ /**
+ * Navigate to the booked bookings page and display all confirmed/booked bookings
+ */
+ async gotoBookedBookings() {
+ await this.page.goto(APPT_BOOKED_BOOKINGS_PAGE);
+ await this.page.waitForLoadState('domcontentloaded');
+ // ensure all future booked bookings are displayed
await this.pendingBookingsFilterSelect.selectOption(this.allFutureBookingsOptionText, { timeout: 60_000 });
}
@@ -44,19 +54,26 @@ export class DashboardPage {
}
/**
- * Given a requested booking's time slot reference, verify that a corresponding hold event
- * exists in the host account's list of future pending bookings
+ * Given a requested booking's time slot reference, verify that a corresponding event exists
+ * in the host account's list of bookings. If the host user's `Booking Confirmation` setting
+ * is enabled (which is the default), then the created appointment will be pending (HOLD) as
+ * it needs to be confirmed; but if the `Booking Confirmation` option is turned off then the
+ * created appt is automaticaly confirmed and booked, so the event text won't contain 'HOLD'.
* @param hostUserDisplayName String containing the host account's user display name
* @param requsterName String containing the name of the requester (provided at booking request)
* @param slotDate String containing date of the requested slot (format e.g: 'February 7, 2025')
* @param slotTime String containg the time of the requested slot (format e.g: '03:30 PM')
+ * @param confirmBooking Boolean whether the requested appt booking requires confirmation
*/
- async verifyHoldEventCreated(hostUserDisplayName: string, requsterName: string, slotDate: string, slotTime: string) {
- // switch to the 'bookings' tab and display all future pending bookings; use the URL instead of UI
- await this.gotoPendingBookings();
-
- // with all future pending bookings now displayed, filter by appointments for our host user and requester
- const eventFilter = `HOLD: Appointment - ${hostUserDisplayName} and ${requsterName}`;
+ async verifyEventCreated(hostUserDisplayName: string, requsterName: string, slotDate: string, slotTime: string, confirmBooking: boolean = true) {
+ // depending on environment switch to the bookings page and verify the appt was created
+ if (confirmBooking) {
+ await this.gotoPendingBookings();
+ var eventFilter = `HOLD: Appointment - ${hostUserDisplayName} and ${requsterName}`;
+ } else {
+ await this.gotoBookedBookings();
+ var eventFilter = `Appointment - ${hostUserDisplayName} and ${requsterName}`;
+ }
await this.filterPendingBookings(eventFilter);
// now we have a list of future pending appointments for our host and requster; now ensure one
diff --git a/test/e2e/pages/fxa-page.ts b/test/e2e/pages/fxa-page.ts
index b5334c79c..b7d11b882 100644
--- a/test/e2e/pages/fxa-page.ts
+++ b/test/e2e/pages/fxa-page.ts
@@ -1,5 +1,5 @@
import { expect, type Page, type Locator } from '@playwright/test';
-import { PROD_LOGIN_PWORD } from '../const/constants';
+import { APPT_LOGIN_PWORD } from '../const/constants';
export class FxAPage {
readonly page: Page;
@@ -17,8 +17,8 @@ export class FxAPage {
}
async signIn() {
- expect(PROD_LOGIN_PWORD, 'getting APPT_LOGIN_PWORD env var').toBeTruthy();
- await this.passwordInput.fill(String(PROD_LOGIN_PWORD));
+ expect(APPT_LOGIN_PWORD, 'getting APPT_LOGIN_PWORD env var').toBeTruthy();
+ await this.passwordInput.fill(String(APPT_LOGIN_PWORD));
await this.signInButton.click();
}
}
diff --git a/test/e2e/pages/splashscreen-page.ts b/test/e2e/pages/splashscreen-page.ts
index d40ddd3e0..793019105 100644
--- a/test/e2e/pages/splashscreen-page.ts
+++ b/test/e2e/pages/splashscreen-page.ts
@@ -1,11 +1,12 @@
import { expect, type Page, type Locator } from '@playwright/test';
-import { APPT_URL, PROD_LOGIN_EMAIL, FXA_PAGE_TITLE } from '../const/constants';
+import { APPT_URL, APPT_LOGIN_EMAIL, FXA_PAGE_TITLE, APPT_LOGIN_PWORD } from '../const/constants';
export class SplashscreenPage {
readonly page: Page;
readonly loginBtn: Locator;
readonly signUpBetaBtn: Locator;
readonly loginEmailInput: Locator;
+ readonly passwordInput: Locator;
readonly loginContinueBtn: Locator;
constructor(page: Page) {
@@ -13,10 +14,11 @@ export class SplashscreenPage {
this.loginBtn = this.page.getByTestId('home-login-btn');
this.signUpBetaBtn = this.page.getByTestId('home-sign-up-beta-btn');
this.loginEmailInput = this.page.getByLabel('Email address');
+ this.passwordInput = this.page.getByLabel('Password');
this.loginContinueBtn = this.page.getByTitle('Continue');
}
- async gotoProd() {
+ async gotoDashboard() {
await this.page.goto(APPT_URL);
await this.page.waitForLoadState('domcontentloaded');
}
@@ -29,6 +31,10 @@ export class SplashscreenPage {
await this.loginEmailInput.fill(emailAddress);
}
+ async enterPassword(password: string) {
+ await this.passwordInput.fill(password);
+ }
+
async clickLoginContinueBtn() {
await this.loginContinueBtn.click();
}
@@ -38,9 +44,20 @@ export class SplashscreenPage {
await this.clickLoginBtn();
await expect(this.loginEmailInput).toBeVisible();
await expect(this.loginContinueBtn).toBeVisible();
- expect(PROD_LOGIN_EMAIL, 'getting APPT_LOGIN_EMAIL env var').toBeTruthy();
- await this.enterLoginEmail(String(PROD_LOGIN_EMAIL))
+ expect(APPT_LOGIN_EMAIL, 'getting APPT_LOGIN_EMAIL env var').toBeTruthy();
+ await this.enterLoginEmail(APPT_LOGIN_EMAIL);
await this.clickLoginContinueBtn();
await expect(this.page).toHaveTitle(FXA_PAGE_TITLE, { timeout: 30_000 }); // be generous in case FxA is slow to load
}
+
+ async localApptSignIn() {
+ await expect(this.loginBtn).toBeVisible();
+ await this.clickLoginBtn();
+ await expect(this.loginEmailInput).toBeVisible();
+ await expect(this.loginContinueBtn).toBeVisible();
+ expect(APPT_LOGIN_EMAIL, 'getting APPT_LOGIN_EMAIL env var').toBeTruthy();
+ await this.enterLoginEmail(APPT_LOGIN_EMAIL);
+ await this.enterPassword(APPT_LOGIN_PWORD);
+ await this.clickLoginContinueBtn();
+ }
}
diff --git a/test/e2e/tests/book-appointment.spec.ts b/test/e2e/tests/book-appointment.spec.ts
index f6bab4437..2057eedbd 100644
--- a/test/e2e/tests/book-appointment.spec.ts
+++ b/test/e2e/tests/book-appointment.spec.ts
@@ -1,8 +1,9 @@
import { test, expect } from '@playwright/test';
import { BookingPage } from '../pages/booking-page';
import { DashboardPage } from '../pages/dashboard-page';
-import { navigateToAppointmentProdAndSignIn } from '../utils/utils';
-import { PROD_DISPLAY_NAME, APPT_BOOKING_REQUESTER_NAME, APPT_BOOKING_REQUESTER_EMAIL } from '../const/constants';
+import { navigateToAppointmentAndSignIn } from '../utils/utils';
+import { APPT_DISPLAY_NAME, APPT_BOOKEE_NAME, APPT_BOOKEE_EMAIL,
+ PLAYWRIGHT_TAG_PROD_SANITY, PLAYWRIGHT_TAG_E2E_SUITE } from '../const/constants';
var bookingPage: BookingPage;
var dashboardPage: DashboardPage;
@@ -10,9 +11,9 @@ var dashboardPage: DashboardPage;
// verify booking page loaded successfully
const verifyBookingPageLoaded = async () => {
await expect(bookingPage.titleText).toBeVisible({ timeout: 60_000 });
- await expect(bookingPage.titleText).toContainText(PROD_DISPLAY_NAME);
+ await expect(bookingPage.titleText).toContainText(APPT_DISPLAY_NAME);
await expect(bookingPage.invitingText).toBeVisible();
- await expect(bookingPage.invitingText).toContainText(PROD_DISPLAY_NAME);
+ await expect(bookingPage.invitingText).toContainText(APPT_DISPLAY_NAME);
await expect(bookingPage.bookingCalendar).toBeVisible();
// verify calendar header
@@ -52,34 +53,39 @@ test.beforeEach(async ({ 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 }) => {
+test.describe('book an appointment', () => {
+ test('able to access booking page via short link', {
+ tag: PLAYWRIGHT_TAG_PROD_SANITY,
+ }, async ({ page }) => {
await bookingPage.gotoBookingPageShortUrl();
await verifyBookingPageLoaded();
});
- test('able to access booking page via long link', async ({ page }) => {
+ test('able to access booking page via long link', {
+ tag: PLAYWRIGHT_TAG_PROD_SANITY,
+ }, async ({ page }) => {
await bookingPage.gotoBookingPageLongUrl();
await verifyBookingPageLoaded();
});
- test('able to request a booking', async ({ page }) => {
+ test('able to request a booking', {
+ tag: [PLAYWRIGHT_TAG_PROD_SANITY, PLAYWRIGHT_TAG_E2E_SUITE],
+ }, async ({ page }) => {
// in order to ensure we find an available slot we can click on, first switch to week view URL
await bookingPage.gotoBookingPageWeekView();
await expect(bookingPage.titleText).toBeVisible({ timeout: 30_000 });
// now select an available booking time slot
- const selectedSlot: string|null = await bookingPage.selectAvailableBookingSlot(PROD_DISPLAY_NAME);
+ const selectedSlot: string|null = await bookingPage.selectAvailableBookingSlot(APPT_DISPLAY_NAME);
console.log(`selected appointment time slot: ${selectedSlot}`);
// now we have an availble booking time slot selected, click confirm button
await bookingPage.confirmBtn.click();
// now fill out the book selection dialog with booking requester's info and book it
- await bookingPage.finishBooking(APPT_BOOKING_REQUESTER_NAME, APPT_BOOKING_REQUESTER_EMAIL);
+ await bookingPage.finishBooking(APPT_BOOKEE_NAME, APPT_BOOKEE_EMAIL);
+ // by default after a slot is booked it requires confirmation from the host user first
// 'boooking request sent' text appears twice, once in the pop-up and once in underlying page
await expect(bookingPage.requestSentTitleText.first()).toBeVisible({ timeout: 60_000 });
await expect(bookingPage.requestSentTitleText.nth(1)).toBeVisible();
@@ -88,7 +94,7 @@ test.describe('book an appointment', {
// this text also appears twice, once in the pop-up and once in underlying page
await expect(bookingPage.requestSentAvailabilityText.first()).toBeVisible();
await expect(bookingPage.requestSentAvailabilityText.nth(1)).toBeVisible();
- const expectedText: string = `${PROD_DISPLAY_NAME}'s Availability`;
+ const expectedText: string = `${APPT_DISPLAY_NAME}'s Availability`;
expect(bookingPage.requestSentAvailabilityText.first()).toContainText(expectedText);
expect(bookingPage.requestSentAvailabilityText.nth(1)).toContainText(expectedText);
@@ -106,11 +112,11 @@ test.describe('book an appointment', {
await bookingPage.requestSentCloseBtn.click();
// navigate to and sign into appointment (host account whom we requested a booking with/owns the share link)
- await navigateToAppointmentProdAndSignIn(page);
+ await navigateToAppointmentAndSignIn(page);
// now verify a corresponding pending booking was created on the host account's list of pending bookings
// (drop the day of the week from our time slot string as this function just needs the month, day, and year)
const expMonthDayYear = expDateStr.substring(expDateStr.indexOf(',') + 2);
- await dashboardPage.verifyHoldEventCreated(PROD_DISPLAY_NAME, APPT_BOOKING_REQUESTER_NAME, expMonthDayYear, expTimeStr);
+ await dashboardPage.verifyEventCreated(APPT_DISPLAY_NAME, APPT_BOOKEE_NAME, expMonthDayYear, expTimeStr);
});
});
diff --git a/test/e2e/tests/sign-in.spec.ts b/test/e2e/tests/sign-in.spec.ts
index 39516c67f..18938876f 100644
--- a/test/e2e/tests/sign-in.spec.ts
+++ b/test/e2e/tests/sign-in.spec.ts
@@ -1,7 +1,7 @@
import { test, expect } from '@playwright/test';
import { SplashscreenPage } from '../pages/splashscreen-page';
import { FxAPage } from '../pages/fxa-page';
-import { APPT_PAGE_TITLE } from '../const/constants';
+import { APPT_TARGET_ENV, APPT_PAGE_TITLE, PLAYWRIGHT_TAG_PROD_SANITY, PLAYWRIGHT_TAG_E2E_SUITE } from '../const/constants';
import { DashboardPage } from '../pages/dashboard-page';
let splashscreenPage: SplashscreenPage;
@@ -13,25 +13,28 @@ test.beforeEach(async ({ page }) => {
splashscreenPage = new SplashscreenPage(page);
signInPage = new FxAPage(page);
dashboardPage = new DashboardPage(page);
- await splashscreenPage.gotoProd();
+ await splashscreenPage.gotoDashboard();
});
// verify we are able to sign-in
test.describe('sign-in', {
- tag: '@prod-sanity'
+ tag: [PLAYWRIGHT_TAG_PROD_SANITY, PLAYWRIGHT_TAG_E2E_SUITE],
}, () => {
test('able to sign-in', async ({ page }) => {
- await splashscreenPage.getToFxA();
-
- 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 signInPage.signIn();
+ // prod and stage use fxa to sign in; when running on local dev env we sign in to appt directly
+ if (APPT_TARGET_ENV == 'prod' || APPT_TARGET_ENV == 'stage') {
+ await splashscreenPage.getToFxA();
+ 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 signInPage.signIn();
+ } else {
+ await splashscreenPage.localApptSignIn();
+ }
await page.waitForLoadState('domcontentloaded');
- await expect(page).toHaveTitle(APPT_PAGE_TITLE, { timeout: 30_000 }); // give generous time for fxa sign-in
+ await expect(page).toHaveTitle(APPT_PAGE_TITLE, { timeout: 30_000 }); // give generous time for sign-in
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 0c05b9c7d..abc3e6175 100644
--- a/test/e2e/tests/splashscreen.spec.ts
+++ b/test/e2e/tests/splashscreen.spec.ts
@@ -1,18 +1,18 @@
import { test, expect } from '@playwright/test';
import { SplashscreenPage } from '../pages/splashscreen-page';
-import { APPT_PAGE_TITLE } from '../const/constants';
+import { APPT_PAGE_TITLE, PLAYWRIGHT_TAG_PROD_SANITY, PLAYWRIGHT_TAG_E2E_SUITE } from '../const/constants';
let splashscreenPage: SplashscreenPage;
test.beforeEach(async ({ page }) => {
// navigate to the main appointment page (splashscreen)
splashscreenPage = new SplashscreenPage(page);
- await splashscreenPage.gotoProd();
+ await splashscreenPage.gotoDashboard();
});
// verify main appointment splash screen appears correctly
test.describe('splash screen', {
- tag: '@prod-sanity'
+ tag: [PLAYWRIGHT_TAG_PROD_SANITY, PLAYWRIGHT_TAG_E2E_SUITE],
}, () => {
test('appears correctly', async ({ page }) => {
await expect(page).toHaveTitle(APPT_PAGE_TITLE);
diff --git a/test/e2e/utils/utils.ts b/test/e2e/utils/utils.ts
index 2209c5ba4..4b17cfef2 100644
--- a/test/e2e/utils/utils.ts
+++ b/test/e2e/utils/utils.ts
@@ -3,20 +3,31 @@ import { SplashscreenPage } from "../pages/splashscreen-page";
import { FxAPage } from "../pages/fxa-page";
import { DashboardPage } from "../pages/dashboard-page";
import { expect, type Page } from '@playwright/test';
-import { APPT_URL, APPT_PAGE_TITLE } from "../const/constants";
+import { APPT_TARGET_ENV, APPT_URL, APPT_PAGE_TITLE } from "../const/constants";
/**
- * Navigate to and sign into the Appointment application using the production URL and
- * production credentials provided in the .env file.
+ * Navigate to and sign into the Appointment application target environment, using the URL and
+ * credentials provided in the .env file. When signing into Appointment on production or stage
+ * you provide the username (email) and then are redirected to the FxA sign in page. When signing
+ * in on the local dev environment you provide a username (email) and password directly and are
+ * not redirected to sign in to FxA.
*/
-export const navigateToAppointmentProdAndSignIn = async (page: Page) => {
- console.log(`navigating to appointment production (${APPT_URL}) and signing in`);
+export const navigateToAppointmentAndSignIn = async (page: Page) => {
+ console.log(`navigating to appointment ${APPT_TARGET_ENV} (${APPT_URL}) and signing in`);
const homePage = new SplashscreenPage(page);
- const signInPage = new FxAPage(page);
+ const fxaSignInPage = new FxAPage(page);
const dashboardPage = new DashboardPage(page);
- await homePage.gotoProd();
- await homePage.getToFxA();
- await signInPage.signIn();
+
+ await homePage.gotoDashboard();
+
+ if (APPT_TARGET_ENV == 'prod' || APPT_TARGET_ENV == 'stage') {
+ await homePage.getToFxA();
+ await fxaSignInPage.signIn();
+ } else {
+ // local dev env doesn't use fxa; just signs into appt using username and pword
+ await homePage.localApptSignIn();
+ }
+
// now that we're signed into the appointment dashboard give it time to load
await page.waitForLoadState('domcontentloaded');
await expect(page).toHaveTitle(APPT_PAGE_TITLE, { timeout: 30_000 }); // give generous time