|
| 1 | +import { expect } from '@playwright/test'; |
| 2 | +import { type Page, type Locator } from '@playwright/test'; |
| 3 | +import { APPT_PROD_MY_SHARE_LINK, APPT_PROD_SHORT_SHARE_LINK_PREFIX, APPT_PROD_LONG_SHARE_LINK_PREFIX } from '../const/constants'; |
| 4 | + |
| 5 | +export class BookingPage { |
| 6 | + readonly page: Page; |
| 7 | + readonly titleText: Locator; |
| 8 | + readonly invitingText: Locator; |
| 9 | + readonly confirmBtn: Locator; |
| 10 | + readonly bookingCalendar: Locator; |
| 11 | + readonly calendarHeader: Locator; |
| 12 | + readonly nextMonthArrow: Locator; |
| 13 | + readonly availableBookingSlot: Locator; |
| 14 | + readonly bookSelectionNameInput: Locator; |
| 15 | + readonly bookSelectionEmailInput: Locator; |
| 16 | + readonly bookSelectionBookBtn: Locator; |
| 17 | + readonly requestSentTitleText: Locator; |
| 18 | + readonly requestSentAvailabilityText: Locator; |
| 19 | + readonly requestSentCloseBtn: Locator; |
| 20 | + |
| 21 | + constructor(page: Page) { |
| 22 | + this.page = page; |
| 23 | + this.titleText = this.page.getByTestId('booking-view-title-text'); |
| 24 | + this.invitingText = this.page.getByTestId('booking-view-inviting-you-text'); |
| 25 | + this.bookingCalendar = this.page.getByTestId('booking-view-calendar-div'); |
| 26 | + this.confirmBtn = this.page.getByTestId('booking-view-confirm-selection-button'); |
| 27 | + this.calendarHeader = this.page.locator('.calendar-header__period-name'); |
| 28 | + this.nextMonthArrow = this.page.locator('[data-icon="chevron-right"]'); |
| 29 | + this.availableBookingSlot = this.page.locator('[data-test="day-event"]', { hasNotText: 'Busy'}); |
| 30 | + this.bookSelectionNameInput = this.page.getByPlaceholder('First and last name'); |
| 31 | + this.bookSelectionEmailInput = this.page.getByPlaceholder('john.doe@example.com'); |
| 32 | + this.bookSelectionBookBtn = this.page.getByRole('button', { name: 'Book' }); |
| 33 | + this.requestSentTitleText = this.page.getByText('Booking request sent'); |
| 34 | + this.requestSentAvailabilityText = this.page.getByText("'s Availability"); |
| 35 | + this.requestSentCloseBtn = this.page.getByRole('button', { name: 'Close' }); |
| 36 | + } |
| 37 | + |
| 38 | + /** |
| 39 | + * Navigate to the booking page using the share link short URL. |
| 40 | + */ |
| 41 | + async gotoBookingPageShortUrl() { |
| 42 | + // the default share link is a short URL |
| 43 | + await this.page.goto(APPT_PROD_MY_SHARE_LINK); |
| 44 | + await this.page.waitForLoadState('domcontentloaded'); |
| 45 | + } |
| 46 | + |
| 47 | + /** |
| 48 | + * Navigatge to the booking page using the share link long URL. |
| 49 | + */ |
| 50 | + async gotoBookingPageLongUrl() { |
| 51 | + // the share link is short by default; build the corresponding long link first |
| 52 | + const prodShareLinkUser: string = APPT_PROD_MY_SHARE_LINK.split(APPT_PROD_SHORT_SHARE_LINK_PREFIX)[1]; |
| 53 | + const longLink: string = `${APPT_PROD_LONG_SHARE_LINK_PREFIX}${prodShareLinkUser}`; |
| 54 | + await this.page.goto(longLink); |
| 55 | + await this.page.waitForLoadState('domcontentloaded'); |
| 56 | + } |
| 57 | + |
| 58 | + /** |
| 59 | + * Go to the booking page week view (via the booking share link) |
| 60 | + */ |
| 61 | + async gotoBookingPageWeekView() { |
| 62 | + const weekLink: string = `${APPT_PROD_MY_SHARE_LINK}#week`; |
| 63 | + await this.page.goto(weekLink); |
| 64 | + await this.page.waitForLoadState('domcontentloaded'); |
| 65 | + await expect(this.confirmBtn).toBeVisible({ timeout: 30_000 }); |
| 66 | + } |
| 67 | + |
| 68 | + /** |
| 69 | + * With the booking page week view already displayed, go forward to the next week. |
| 70 | + */ |
| 71 | + async goForwardOneWeek() { |
| 72 | + await this.nextMonthArrow.click(); |
| 73 | + await this.page.waitForLoadState('domcontentloaded'); |
| 74 | + await expect(this.confirmBtn).toBeVisible({ timeout: 30_000 }); |
| 75 | + } |
| 76 | + |
| 77 | + /** |
| 78 | + * With the booking page week view already displayed, select the first available booking slot. |
| 79 | + * If there is no slot available on the current week, this methond will skip to the next week |
| 80 | + * and look for slots there. If no slots are avaible on the next week either, then an error |
| 81 | + * will be raised. |
| 82 | + * @param userDisplayName String containing the display name of the Appointment user |
| 83 | + * @returns String containing the reference text for the time slot that was requested |
| 84 | + * as retrieved from the DOM ie. 'event-2025-01-08 09:30'. |
| 85 | + */ |
| 86 | + async selectAvailableBookingSlot(userDisplayName: string): Promise<string> { |
| 87 | + // let's check if a non-busy appointment slot exists in the current week view |
| 88 | + const slotCount: number = await this.availableBookingSlot.count(); |
| 89 | + console.log(`available slot count: ${slotCount}`); |
| 90 | + |
| 91 | + // if no slots are available in current week view then fast forward to next week |
| 92 | + if (slotCount === 0) { |
| 93 | + console.log('no slots available in current week, skipping ahead to the next week'); |
| 94 | + await this.goForwardOneWeek(); |
| 95 | + // now check again for available slots; if none then fail out the test (safety catch but shouldn't happen) |
| 96 | + const newSlotCount: number = await this.availableBookingSlot.count(); |
| 97 | + console.log(`available slot count: ${newSlotCount}`); |
| 98 | + expect(newSlotCount, `no booking slots available, please check availability settings for ${userDisplayName}`).toBeGreaterThan(0); |
| 99 | + } |
| 100 | + |
| 101 | + // slots are available in current week view so get the first one |
| 102 | + const firstSlot: Locator = this.availableBookingSlot.first(); |
| 103 | + let slotRef = await firstSlot.getAttribute('data-ref'); // ie. 'event-2025-01-08 09:30' |
| 104 | + if (!slotRef) |
| 105 | + slotRef = 'none'; |
| 106 | + expect(slotRef).toContain('event-'); |
| 107 | + |
| 108 | + // now that we've found an availalbe slot select it and confirm |
| 109 | + await firstSlot.click(); |
| 110 | + return slotRef; |
| 111 | + } |
| 112 | + |
| 113 | + /** |
| 114 | + * Fill out the 'book selection' dialog with the given values. |
| 115 | + * The 'book selection' dialog appears after an appointment slot has been selected (on the |
| 116 | + * booking page provided by the share link). This method will fill in the booking requester's |
| 117 | + * name and email address and then click the 'book' button to finalize the booking request. |
| 118 | + * @param bookerName String to fill in as the booking requester's name |
| 119 | + * @param bookerEmail String to fill in as the booking requester's email |
| 120 | + */ |
| 121 | + async finishBooking(bookerName: string, bookerEmail: string) { |
| 122 | + await this.bookSelectionNameInput.fill(bookerName); |
| 123 | + await this.bookSelectionEmailInput.fill(bookerEmail); |
| 124 | + await this.bookSelectionBookBtn.click(); |
| 125 | + } |
| 126 | + |
| 127 | + /** |
| 128 | + * Verify the given appointment time slot text is displayed in the current page |
| 129 | + * @param expSlotDateStr Expected slot date string formatted as 'Friday, January 10, 2025' |
| 130 | + * @param expSlotTimeStr Expected time slot time string formatted as '14:30' (24 hr time) |
| 131 | + */ |
| 132 | + async verifyRequestedSlotTextDisplayed(expSlotDateStr: string, expSlotTimeStr: string) { |
| 133 | + // due to the way the element is we must locate by the date text only |
| 134 | + const slotDisplayText: Locator = this.page.getByText(expSlotDateStr); |
| 135 | + await expect(slotDisplayText).toBeVisible(); |
| 136 | + // the slot text has been found so now verify it contains both the given date and time |
| 137 | + await expect(slotDisplayText).toHaveText(`${expSlotDateStr} ${expSlotTimeStr}`); |
| 138 | + } |
| 139 | + |
| 140 | + /** |
| 141 | + * Utility to return a string containing the date abstracted from a given time slot string |
| 142 | + * @param timeSlotString Slot string read from DOM (ie. 'event-2025-01-14 14:30') |
| 143 | + * @returns Formatted date string (ie. 'Tuesday, January 14, 2025') |
| 144 | + */ |
| 145 | + async getDateFromSlotString(timeSlotString: string): Promise<string> { |
| 146 | + const selectedSlotDateTime = new Date(timeSlotString.substring(6)); |
| 147 | + return selectedSlotDateTime.toLocaleDateString('default', { dateStyle: 'full' }); |
| 148 | + } |
| 149 | + |
| 150 | + /** |
| 151 | + * Utility to return a string containg the time abstracted from a given time slot string. |
| 152 | + * The time in the given time slot string is in 24 hour format (i.e. 14:30), but we want |
| 153 | + * it to be like '02:30 PM' |
| 154 | + * @param timeSlotString Slot string read from DOM (ie. 'event-2025-01-14 14:30') |
| 155 | + * @returns Formatted time string (ie. '02:30 PM') |
| 156 | + */ |
| 157 | + async getTimeFromSlotString(timeSlotString: string): Promise<string> { |
| 158 | + const selectedSlotDateTime = new Date(timeSlotString.substring(6)); |
| 159 | + const expTimeStr = selectedSlotDateTime.toLocaleTimeString('default', { hour12: true, hour: '2-digit', minute: '2-digit' }); |
| 160 | + // now expTimeStr looks like this, for example: '04:30 p.m.' but need it to be like '04:30 PM' |
| 161 | + return expTimeStr.toUpperCase().replace('.', '').replace('.', ''); |
| 162 | + } |
| 163 | +} |
0 commit comments