Skip to content

Commit a3c211c

Browse files
committed
Port the book appointment E2E test to mobile
1 parent c1937e3 commit a3c211c

File tree

7 files changed

+139
-34
lines changed

7 files changed

+139
-34
lines changed

test/e2e/pages/availability-page.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export class AvailabilityPage {
2121
readonly timeZoneSelect: Locator;
2222
readonly calendarSelect: Locator;
2323
readonly autoConfirmBookingsCheckBox: Locator;
24+
readonly autoConfirmBookingsCheckBoxContainer: Locator;
2425
readonly customizePerDayCheckBox: Locator;
2526
readonly customizePerDayCheckBoxContainer: Locator;
2627
readonly allStartTimeInput: Locator;
@@ -58,6 +59,8 @@ export class AvailabilityPage {
5859
this.timeZoneSelect = this.page.getByLabel(APPT_TIMEZONE_SETTING_PRIMARY);
5960
this.calendarSelect = this.page.locator('select[name="calendar"]');
6061
this.autoConfirmBookingsCheckBox = this.page.getByTestId('availability-automatically-confirm-checkbox');
62+
this.autoConfirmBookingsCheckBoxContainer = this.page.locator('label').filter({ hasText: 'Automatically confirm bookings' }).locator('span').first();
63+
6164
this.customizePerDayCheckBox = this.page.getByRole('checkbox', { name: 'Set custom times for each day'});
6265
this.customizePerDayCheckBoxContainer = this.page.locator('label').filter({ hasText: 'Set custom times for each day' }).locator('span').first();
6366

test/e2e/pages/booking-page.ts

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { APPT_MY_SHARE_LINK, APPT_SHORT_SHARE_LINK_PREFIX, APPT_LONG_SHARE_LINK_
44

55
export class BookingPage {
66
readonly page: Page;
7+
readonly testPlatform: String;
78
readonly titleText: Locator;
89
readonly bookATimeToMeetText: Locator;
910
readonly selectTimeSlotText: Locator;
@@ -29,8 +30,10 @@ export class BookingPage {
2930
readonly bookApptPage630PMSlot: Locator;
3031
readonly bookApptPage15MinSlot: Locator;
3132

32-
constructor(page: Page) {
33+
constructor(page: Page, testPlatform: string = 'desktop') {
3334
this.page = page;
35+
this.testPlatform = testPlatform;
36+
3437
this.titleText = this.page.getByTestId('booking-view-title-text');
3538
this.bookATimeToMeetText = this.page.getByTestId('booking-view-book-a-time-to-meet-with-text');
3639
this.selectTimeSlotText = this.page.getByText('Select an open time slot from the calendar');
@@ -42,11 +45,11 @@ export class BookingPage {
4245
this.bookingCalendarHdrFri = this.page.getByText('FRI', { exact: true });
4346
this.bookingCalendarHdrSat = this.page.getByText('SAT', { exact: true });
4447
this.bookingWeekPickerBtn = this.page.locator('.week-picker-button');
45-
this.bookApptBtn = this.page.getByTestId('booking-view-confirm-selection-button');
4648
this.nextWeekArrow = this.page.getByRole('button', { name: 'Next week' });
47-
this.availableBookingSlot = this.page.locator('.selectable-slot', { hasNotText: 'Busy'});
48-
this.bookSelectionNameInput = this.page.getByPlaceholder('First and last name');
49-
this.bookSelectionEmailInput = this.page.getByPlaceholder('john.doe@example.com');
49+
this.availableBookingSlot = this.page.locator('.selectable-slot', { hasNotText: 'Busy' });
50+
this.bookSelectionNameInput = this.page.locator('[name="booker-view-user-name"]');
51+
this.bookSelectionEmailInput = this.page.locator('[name="booker-view-user-email"]');
52+
this.bookApptBtn = this.page.getByTestId('booking-view-confirm-selection-button');
5053
this.bookingConfirmedTitleText = this.page.getByText('Booking confirmed');
5154
this.requestSentAvailabilityText = this.page.getByText("'s Availability");
5255
this.requestSentCloseBtn = this.page.getByRole('button', { name: 'Close' });
@@ -86,6 +89,15 @@ export class BookingPage {
8689
await expect(this.selectTimeSlotText).toBeVisible( { timeout: TIMEOUT_30_SECONDS });
8790
}
8891

92+
/**
93+
* Scroll the given element into view. The reason why we do this here is because playright doesn't yet supported this on ios.
94+
*/
95+
async scrollIntoView(targetElement: Locator, timeout: number = 10000) {
96+
if (!this.testPlatform.includes('ios')) {
97+
await targetElement.scrollIntoViewIfNeeded({ timeout: timeout });
98+
}
99+
}
100+
89101
/**
90102
* With the booking page week view already displayed, go forward to the next week.
91103
*/
@@ -105,28 +117,26 @@ export class BookingPage {
105117
*/
106118
async selectAvailableBookingSlot(userDisplayName: string): Promise<string> {
107119
// let's check if a non-busy appointment slot exists in the current week view
108-
const slotCount: number = await this.availableBookingSlot.count();
109-
console.log(`available slot count: ${slotCount}`);
120+
// playwrignt doesn't yet support 'count' or 'all' or 'elementHandles' on ios
121+
var availableSlot: Locator = this.availableBookingSlot.first();
110122

111123
// if no slots are available in current week view then fast forward to next week
112-
if (slotCount === 0) {
124+
if (!availableSlot) {
113125
console.log('no slots available in current week, skipping ahead to the next week');
114126
await this.goForwardOneWeek();
115127
// now check again for available slots; if none then fail out the test (safety catch but shouldn't happen)
116-
const newSlotCount: number = await this.availableBookingSlot.count();
117-
console.log(`available slot count: ${newSlotCount}`);
118-
expect(newSlotCount, `no booking slots available, please check availability settings for ${userDisplayName}`).toBeGreaterThan(0);
128+
availableSlot = this.availableBookingSlot.first();
129+
expect(availableSlot, `no booking slots available, please check availability settings for ${userDisplayName}`).toBeTruthy();
119130
}
120131

121-
// slots are available in current week view so get the first one
122-
const firstSlot: Locator = this.availableBookingSlot.first();
123-
let slotRef = await firstSlot.getAttribute('data-testid'); // ie. 'event-2025-01-08 09:30'
132+
// get our slot info for the available slot that we are going to request
133+
let slotRef = await availableSlot.getAttribute('data-testid'); // ie. 'event-2025-01-08 09:30'
124134
if (!slotRef)
125135
slotRef = 'none';
126136
expect(slotRef).toContain('event-');
127137

128138
// now that we've found an availalbe slot select it and confirm
129-
await firstSlot.click();
139+
await availableSlot.click();
130140
return slotRef;
131141
}
132142

@@ -141,7 +151,13 @@ export class BookingPage {
141151
async finishBooking(bookerName: string, bookerEmail: string) {
142152
await this.bookSelectionNameInput.fill(bookerName);
143153
await this.bookSelectionEmailInput.fill(bookerEmail);
144-
await this.bookApptBtn.click();
154+
// when clicking the book appt button for some reason on android it won't click it unless we force it; but
155+
// force doesn't work on ios
156+
if (this.testPlatform.includes('android')) {
157+
await this.bookApptBtn.click({ force: true });
158+
} else {
159+
await this.bookApptBtn.click();
160+
}
145161
}
146162

147163
/**

test/e2e/pages/tb-accts-page.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ export class TBAcctsPage {
88
readonly emailInput: Locator;
99
readonly passwordInput: Locator;
1010
readonly signInButton: Locator;
11-
readonly loginEmailInput: Locator;
11+
readonly localDevEmailInput: Locator;
1212
readonly localDevpasswordInput: Locator;
13-
readonly loginDialogContinueBtn: Locator;
13+
readonly localDevLoginContinueBtn: Locator;
1414

1515
constructor(page: Page) {
1616
this.page = page;
@@ -19,9 +19,10 @@ export class TBAcctsPage {
1919
this.emailInput = this.page.getByTestId('username-input');
2020
this.passwordInput = this.page.getByTestId('password-input');
2121
this.signInButton = this.page.getByTestId('submit-btn');
22-
this.loginEmailInput = this.page.getByLabel('Email');
23-
this.localDevpasswordInput = this.page.getByLabel('Password');
24-
this.loginDialogContinueBtn = this.page.getByTitle('Continue');
22+
23+
this.localDevEmailInput = this.page.getByTestId('login-email-input');
24+
this.localDevpasswordInput = this.page.getByTestId('login-password-input');
25+
this.localDevLoginContinueBtn = this.page.getByTestId('login-continue-btn');
2526
}
2627

2728
/**
@@ -48,17 +49,17 @@ export class TBAcctsPage {
4849
}
4950

5051
/**
51-
* Sign in when running Appointment on the local dev stack; doesn't redirect to TB Accounts login; just local sign-in
52+
* Sign in when running Appointment on the local dev stack and not using TB Accounts OIDC; just local password
5253
*/
5354
async localApptSignIn() {
54-
await expect(this.loginEmailInput).toBeVisible({ timeout: TIMEOUT_30_SECONDS });
55-
await expect(this.loginDialogContinueBtn).toBeVisible();
55+
await expect(this.localDevEmailInput).toBeVisible({ timeout: TIMEOUT_30_SECONDS });
56+
await expect(this.localDevLoginContinueBtn).toBeVisible();
5657
expect(TB_ACCTS_EMAIL, 'getting TB_ACCTS_EMAIL env var').toBeTruthy();
57-
await this.loginEmailInput.fill(TB_ACCTS_EMAIL);
58+
await this.localDevEmailInput.fill(TB_ACCTS_EMAIL);
5859
await this.page.waitForTimeout(TIMEOUT_1_SECOND);
5960
await this.localDevpasswordInput.fill(TB_ACCTS_PWORD);
6061
await this.page.waitForTimeout(TIMEOUT_1_SECOND);
61-
await this.loginDialogContinueBtn.click();
62+
await this.localDevLoginContinueBtn.click();
6263
await this.page.waitForTimeout(TIMEOUT_10_SECONDS);
6364
}
6465
}

test/e2e/tests/desktop/auth.desktop.setup.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,15 @@ setup('desktop browser authenticate', async ({ page }) => {
6565
console.log('turned booking availibility on');
6666
}
6767

68+
// Ensure 'automatically confirm bookings' is turned on.
69+
// The section containing the auto confirm checkbox prevents playwright from being able to interact with the actual
70+
// checkbox element directly; we can use the checkbox element to check if it is chekced, but in order to turn it on
71+
// we need to then click on the checkbox container instead of the actual checkbox
72+
if (!await availabilityPage.autoConfirmBookingsCheckBox.isChecked()) {
73+
await availabilityPage.autoConfirmBookingsCheckBoxContainer.click();
74+
await expect(availabilityPage.autoConfirmBookingsCheckBox).toBeChecked();
75+
}
76+
6877
// And ensure availability start time is 9am, end time 5pm
6978
// Sometimes it takes a couple of seconds for the start/end time values to appear
7079
await availabilityPage.allStartTimeInput.scrollIntoViewIfNeeded();
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { test, expect } from '@playwright/test';
2+
import { BookingPage } from '../../pages/booking-page';
3+
import { DashboardPage } from '../../pages/dashboard-page';
4+
import { ensureWeAreSignedIn } from '../../utils/utils';
5+
6+
import {
7+
APPT_DISPLAY_NAME,
8+
PLAYWRIGHT_TAG_PROD_MOBILE_NIGHTLY,
9+
PLAYWRIGHT_TAG_E2E_SUITE_MOBILE,
10+
APPT_BOOKEE_NAME,
11+
APPT_BOOKEE_EMAIL,
12+
TIMEOUT_3_SECONDS,
13+
TIMEOUT_10_SECONDS,
14+
TIMEOUT_30_SECONDS,
15+
TIMEOUT_60_SECONDS,
16+
} from '../../const/constants';
17+
18+
var bookingPage: BookingPage;
19+
var dashboardPage: DashboardPage;
20+
21+
test.beforeEach(async ({ page }, testInfo) => {
22+
bookingPage = new BookingPage(page, testInfo.project.name); // i.e. 'ios-safari'
23+
dashboardPage = new DashboardPage(page);
24+
});
25+
26+
27+
test.describe('book an appointment on mobile browser', () => {
28+
29+
test('able to request a booking on mobile browser', {
30+
tag: [PLAYWRIGHT_TAG_E2E_SUITE_MOBILE, PLAYWRIGHT_TAG_PROD_MOBILE_NIGHTLY],
31+
}, async ({ page }) => {
32+
// in order to ensure we find an available slot we can click on, first switch to week view URL
33+
await bookingPage.gotoBookingPageWeekView();
34+
await expect(bookingPage.titleText).toBeVisible({ timeout: TIMEOUT_30_SECONDS });
35+
36+
// now select an available booking time slot
37+
const selectedSlot: string|null = await bookingPage.selectAvailableBookingSlot(APPT_DISPLAY_NAME);
38+
console.log(`selected appointment time slot: ${selectedSlot}`);
39+
40+
// now provide the bookee details for our selected time slot; when signed into Appointment this
41+
// info is provided automatically however for this test we are selecting a time slot without being
42+
// signed in / without being an Appointment user; so we must specify the bookee name and email
43+
// also after entering bookee details, this clicks the 'book appointment' button to request the slot
44+
await bookingPage.finishBooking(APPT_BOOKEE_NAME, APPT_BOOKEE_EMAIL);
45+
46+
// verify booking request sent pop-up
47+
await expect(bookingPage.bookingConfirmedTitleText.first()).toBeVisible({ timeout: TIMEOUT_60_SECONDS });
48+
await bookingPage.scrollIntoView(bookingPage.bookingConfirmedTitleText);
49+
50+
// booking request sent dialog should display the correct time slot that was requested
51+
// our requested time slot is stored in this format, as example: 'event-2025-01-14 14:30'
52+
// the dialog reports the slot in this format, as example: 'January 14, 2025' with start time
53+
// on next line; convert our selected slot value to same format as displayed so we can verify
54+
const expDateStr = await bookingPage.getDateFromSlotString(selectedSlot);
55+
const expTimeStr = await bookingPage.getTimeFromSlotString(selectedSlot);
56+
57+
// now verify the correct date/time is dispalyed on the booking request sent pop-up
58+
await bookingPage.verifyRequestedSlotTextDisplayed(expDateStr, expTimeStr);
59+
60+
// now verify a corresponding pending booking was created on the host account's list of pending bookings
61+
// wait N seconds for the appointment dashboard to update, sometimes the test is so fast when it
62+
// switches back to the dashboard the new pending appointment hasn't been added/displayed yet
63+
await page.waitForTimeout(TIMEOUT_10_SECONDS);
64+
await ensureWeAreSignedIn(page);
65+
await dashboardPage.verifyEventCreated(expDateStr, expTimeStr);
66+
67+
// also go back to main dashboard and check that pending requests link now appears
68+
await dashboardPage.gotoToDashboardMonthView();
69+
await expect(dashboardPage.pendingBookingRequestsLink).toBeVisible();
70+
71+
// click the pending requests link and verify it navigates to the correct URL
72+
await dashboardPage.pendingBookingRequestsLink.click();
73+
await page.waitForTimeout(TIMEOUT_3_SECONDS);
74+
await expect(page).toHaveURL(/.*\/bookings\?unconfirmed=true/);
75+
});
76+
});

test/e2e/tests/mobile/settings-account.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ test.describe('account settings on mobile browser', {
2828
settingsPage = new SettingsPage(page, testInfo.project.name); // i.e. 'ios-safari'
2929
dashboardPage = new DashboardPage(page);
3030
availabilityPage = new AvailabilityPage(page);
31-
bookApptPage = new BookingPage(page);
31+
bookApptPage = new BookingPage(page, testInfo.project.name); // i.e. 'ios-safari'
3232

3333
// mobile browsers don't support saving auth storage state so must sign in before each test
3434
// send in the playright test project name i.e. safari-ios because some mobile platforms differ

test/e2e/utils/utils.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,14 @@ export const navigateToAppointmentAndSignIn = async (page: Page, testProjectName
3636
await page.goto(`${APPT_URL}`);
3737
await page.waitForTimeout(TIMEOUT_5_SECONDS);
3838

39-
// if we are already signed in then we can skip this
39+
// local dev can use 'password' only or oidc; check if using password only
40+
if (APPT_TARGET_ENV == 'dev' && await tbAcctsSignInPage.localDevEmailInput.isVisible()) {
41+
await tbAcctsSignInPage.localApptSignIn();
42+
}
43+
44+
// using oidc/keyloak; if we are already signed in then we can skip this
4045
if (await tbAcctsSignInPage.signInHeaderText.isVisible() && await tbAcctsSignInPage.signInButton.isEnabled()) {
41-
if (APPT_TARGET_ENV == 'prod' || APPT_TARGET_ENV == 'stage') {
42-
await tbAcctsSignInPage.signIn(testProjectName);
43-
} else {
44-
// local dev env doesn't use tb accts; just signs into appt using username and pword
45-
await tbAcctsSignInPage.localApptSignIn();
46-
}
46+
await tbAcctsSignInPage.signIn(testProjectName);
4747
}
4848

4949
// now that we're signed into the appointment dashboard give it time to load

0 commit comments

Comments
 (0)