Skip to content

Commit 9954f6c

Browse files
authored
First set of E2E tests on iOS (#1377)
* First set of E2E tests on iOS * Post-review updates * More updates
1 parent 5e5abf8 commit 9954f6c

16 files changed

+274
-196
lines changed

.github/workflows/nightly-tests-mobile.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,18 @@ jobs:
4747
project-name: 'Thunderbird Appointment'
4848
build-name: 'TB Appointment Nightly Tests (Mobile): BUILD_INFO'
4949

50-
- name: Run E2E Tests on Production on Browserstack (Mobile)
50+
- name: Prod E2E Tests on Android Chrome
5151
# don't send GHA failure email if any of the E2E tests fail, can be annoying (I check the jobs each day in BrowserStack)
5252
continue-on-error: true
5353
run: |
5454
cd ./test/e2e
5555
cp .env.prod.example .env
5656
npm run prod-nightly-tests-mobile-browserstack-android-chrome
57+
58+
- name: Prod E2E Tests on iOS Safari
59+
# don't send GHA failure email if any of the E2E tests fail, can be annoying (I check the jobs each day in BrowserStack)
60+
continue-on-error: true
61+
run: |
62+
cd ./test/e2e
63+
cp .env.prod.example .env
64+
npm run prod-nightly-tests-mobile-browserstack-ios-safari

backend/test/unit/test_schedule_availability.py

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55

66
class TestScheduleAvailability:
7-
87
def test_empty_availability_is_valid(self):
98
# Test empty availability is valid
109
schedule = schemas.ScheduleValidationIn(
@@ -17,7 +16,6 @@ def test_empty_availability_is_valid(self):
1716
)
1817
assert all_availability_is_valid(schedule)
1918

20-
2119
def test_all_availability_is_valid(self):
2220
# Test already sorted availabilities
2321
schedule = schemas.ScheduleValidationIn(
@@ -37,7 +35,7 @@ def test_all_availability_is_valid(self):
3735
schemas.AvailabilityValidationIn(
3836
schedule_id=1, day_of_week=2, start_time=time(9, 0), end_time=time(10, 0)
3937
),
40-
]
38+
],
4139
)
4240
assert all_availability_is_valid(schedule)
4341

@@ -49,16 +47,13 @@ def test_all_availability_is_valid(self):
4947
schemas.AvailabilityValidationIn(
5048
schedule_id=1, day_of_week=1, start_time=time(10, 0), end_time=time(11, 0)
5149
),
52-
schemas.AvailabilityValidationIn(
53-
schedule_id=1, day_of_week=1, start_time=time(9, 0), end_time=time(10, 0)
54-
),
50+
schemas.AvailabilityValidationIn(schedule_id=1, day_of_week=1, start_time=time(9, 0), end_time=time(10, 0)),
5551
schemas.AvailabilityValidationIn(
5652
schedule_id=1, day_of_week=1, start_time=time(17, 0), end_time=time(18, 0)
5753
),
5854
]
5955
assert all_availability_is_valid(schedule)
6056

61-
6257
def test_all_availability_is_invalid(self):
6358
# Test overlapping end-start times
6459
schedule = schemas.ScheduleValidationIn(
@@ -75,15 +70,13 @@ def test_all_availability_is_invalid(self):
7570
schemas.AvailabilityValidationIn(
7671
schedule_id=1, day_of_week=1, start_time=time(10, 0), end_time=time(12, 0)
7772
),
78-
]
73+
],
7974
)
8075
assert not all_availability_is_valid(schedule)
8176

8277
# Test completely overlapping slots
8378
schedule.availabilities = [
84-
schemas.AvailabilityValidationIn(
85-
schedule_id=1, day_of_week=1, start_time=time(9, 0), end_time=time(12, 0)
86-
),
79+
schemas.AvailabilityValidationIn(schedule_id=1, day_of_week=1, start_time=time(9, 0), end_time=time(12, 0)),
8780
schemas.AvailabilityValidationIn(
8881
schedule_id=1, day_of_week=1, start_time=time(10, 0), end_time=time(11, 0)
8982
),
@@ -92,9 +85,7 @@ def test_all_availability_is_invalid(self):
9285

9386
# Test slots with invalid start/end time
9487
schedule.availabilities = [
95-
schemas.AvailabilityValidationIn(
96-
schedule_id=1, day_of_week=1, start_time=time(9, 0), end_time=time(12, 0)
97-
),
88+
schemas.AvailabilityValidationIn(schedule_id=1, day_of_week=1, start_time=time(9, 0), end_time=time(12, 0)),
9889
schemas.AvailabilityValidationIn(
9990
schedule_id=1, day_of_week=1, start_time=time(14, 0), end_time=time(13, 0)
10091
),
@@ -103,8 +94,6 @@ def test_all_availability_is_invalid(self):
10394

10495
# Test slots that are too small for the defined duration
10596
schedule.availabilities = [
106-
schemas.AvailabilityValidationIn(
107-
schedule_id=1, day_of_week=1, start_time=time(9, 0), end_time=time(9, 15)
108-
),
97+
schemas.AvailabilityValidationIn(schedule_id=1, day_of_week=1, start_time=time(9, 0), end_time=time(9, 15)),
10998
]
11099
assert not all_availability_is_valid(schedule)

test/e2e/README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -175,28 +175,28 @@ export BROWSERSTACK_USERNAME=<your-browserstack-user-name>
175175
export BROWSERSTACK_ACCESS_KEY=<your-browserstack-access-key>
176176
```
177177

178-
To run the E2E tests on BrowserStack (still in `test/e2e`):
178+
To run the E2E tests on Firefox Desktop on BrowserStack (still in `test/e2e`):
179179

180180
```bash
181-
npm run e2e-test-browserstack
181+
npm run e2e-test-browserstack-firefox
182182
```
183183

184-
To run the production sanity test on BrowserStack (still in `test/e2e`):
184+
To run the production sanity test on Firefox Desktop on BrowserStack (still in `test/e2e`):
185185

186186
```bash
187-
npm run prod-sanity-test-browserstack
187+
npm run prod-sanity-test-browserstack-firefox
188188
```
189189

190-
To run the E2E tests on mobile devices on BrowserStack (still in `test/e2e`):
190+
To run the E2E tests on Android Chrome on a real Google Pixel device on BrowserStack (still in `test/e2e`):
191191

192192
```bash
193-
npm run e2e-test-mobile-browserstack
193+
npm run e2e-tests-mobile-browserstack-android-chrome
194194
```
195195

196-
To run the nightly test suite on real mobile devices in BrowserStack (still in `test/e2e`):
196+
To run the E2E tests on iOS Safari on a real iPhone device on BrowserStack (still in `test/e2e`):
197197

198198
```bash
199-
npm run prod-nightly-tests-mobile-browserstack-gha
199+
npm run e2e-tests-mobile-browserstack-ios-safari
200200
```
201201

202202
After the tests finish in your local console you'll see a link to the BrowserStack test session; when signed into your BrowserStack account you'll be able to use that link to see the test session results including video playback.

test/e2e/browserstack-mobile-nightly.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,20 @@ platforms:
2929
osVersion: 16.0
3030
deviceName: Google Pixel 10
3131
playwrightConfigOptions:
32-
name: Chrome-GooglePixel10
32+
name: android-chrome
3333
chrome:
3434
chromeOptions:
3535
prefs:
3636
credentials_enable_service: false
3737
profile:
3838
password_manager_enabled: false
3939

40+
- browserName: safari
41+
osVersion: 26
42+
deviceName: iPhone 17
43+
playwrightConfigOptions:
44+
name: ios-safari
45+
4046
# =======================
4147
# Parallels per Platform
4248
# =======================
@@ -68,6 +74,7 @@ debug: false # <boolean> # Set to true if you need screenshots for every seleniu
6874
networkLogs: false # <boolean> Set to true to enable HAR logs capturing; off as may contain sensitive info like login API requests
6975
consoleLogs: info # <string> Remote browser's console debug levels to be printed (`disable`, `errors`, `warnings`, `info`, or `verbose`)
7076
framework: playwright
77+
idleTimeout: 500 # seeing if this helps rid BROWSERSTACK_IDLE_TIMEOUTs on ios
7178
# the e2e tests use the same node.js project so the playwright version dependecy must work on both desktop
7279
# and mobile browsers; and therefore must be the same in all of our browserstack-*.yml config files
7380
# for playwright ver we need one compatible with our desktop and mobile browsers, see:

test/e2e/browserstack-mobile.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,20 @@ platforms:
2929
osVersion: 16.0
3030
deviceName: Google Pixel 10
3131
playwrightConfigOptions:
32-
name: Chrome-GooglePixel10
32+
name: android-chrome
3333
chrome:
3434
chromeOptions:
3535
prefs:
3636
credentials_enable_service: false
3737
profile:
3838
password_manager_enabled: false
3939

40+
- browserName: safari
41+
osVersion: 26
42+
deviceName: iPhone 17
43+
playwrightConfigOptions:
44+
name: ios-safari
45+
4046
# =======================
4147
# Parallels per Platform
4248
# =======================
@@ -68,6 +74,7 @@ debug: false # <boolean> # Set to true if you need screenshots for every seleniu
6874
networkLogs: false # <boolean> Set to true to enable HAR logs capturing; off as may contain sensitive info like login API requests
6975
consoleLogs: info # <string> Remote browser's console debug levels to be printed (`disable`, `errors`, `warnings`, `info`, or `verbose`)
7076
framework: playwright
77+
idleTimeout: 500 # seeing if this helps rid BROWSERSTACK_IDLE_TIMEOUTs on ios
7178
# the e2e tests use the same node.js project so the playwright version dependecy must work on both desktop
7279
# and mobile browsers; and therefore must be the same in all of our browserstack-*.yml config files
7380
# for playwright ver we need one compatible with our desktop and mobile browsers, see:

test/e2e/package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,17 @@
55
"scripts": {
66
"e2e-test": "npx playwright test --grep e2e-suite --project=firefox",
77
"e2e-test-headed": "npx playwright test --grep e2e-suite --project=firefox --headed",
8-
"e2e-test-browserstack-firefox": "npx browserstack-node-sdk playwright test --grep e2e-suite --project=Firefox-OSX --browserstack.buildName 'Appointment E2E Tests Firefox Desktop' --browserstack.config 'browserstack-desktop.yml'",
8+
"e2e-test-browserstack-firefox": "npx browserstack-node-sdk playwright test --grep e2e-suite --project=Firefox-OSX --max-failures=2 --browserstack.buildName 'Appointment E2E Tests Firefox Desktop' --browserstack.config 'browserstack-desktop.yml'",
9+
"e2e-tests-mobile-browserstack-android-chrome": "npx browserstack-node-sdk playwright test --grep e2e-mobile-suite --project=android-chrome --max-failures=2 --browserstack.buildName 'Appointment E2E Tests Android Chrome' --browserstack.config 'browserstack-mobile.yml'",
10+
"e2e-tests-mobile-browserstack-ios-safari": "npx browserstack-node-sdk playwright test --grep e2e-mobile-suite --project=ios-safari --max-failures=2 --browserstack.buildName 'Appointment E2E Tests iOS Safari' --browserstack.config 'browserstack-mobile.yml'",
911
"prod-sanity-test": "npx playwright test --grep prod-sanity --project=firefox",
1012
"prod-sanity-test-headed": "npx playwright test --grep prod-sanity --project=firefox --headed",
11-
"prod-sanity-test-browserstack-firefox": "npx browserstack-node-sdk playwright test --grep prod-sanity --project=Firefox-OSX --browserstack.buildName 'Appointment Production Sanity Test Firefox Desktop' --browserstack.config 'browserstack-desktop.yml'",
13+
"prod-sanity-test-browserstack-firefox": "npx browserstack-node-sdk playwright test --grep prod-sanity --project=Firefox-OSX --max-failures=2 --browserstack.buildName 'Appointment Production Sanity Test Firefox Desktop' --browserstack.config 'browserstack-desktop.yml'",
1214
"prod-nightly-tests-browserstack-firefox": "npx browserstack-node-sdk playwright test --grep prod-nightly --project=Firefox-OSX --browserstack.buildName 'Appointment Nightly Tests Firefox Desktop' --browserstack.config 'browserstack-desktop-nightly.yml'",
1315
"prod-nightly-tests-browserstack-safari": "npx browserstack-node-sdk playwright test --grep prod-nightly --project=Safari-OSX --browserstack.buildName 'Appointment Nightly Tests Safari Desktop' --browserstack.config 'browserstack-desktop-nightly.yml'",
1416
"prod-nightly-tests-browserstack-chromium": "npx browserstack-node-sdk playwright test --grep prod-nightly --project=Chromium-Win11 --browserstack.buildName 'Appointment Nightly Tests Chromium Desktop' --browserstack.config 'browserstack-desktop-nightly.yml'",
15-
"prod-nightly-tests-mobile-browserstack-android-chrome": "npx browserstack-node-sdk playwright test --grep prod-mobile-nightly --project=Chrome-GooglePixel10 --browserstack.buildName 'Appointment Nightly Tests Android Chrome' --browserstack.config 'browserstack-mobile-nightly.yml'",
17+
"prod-nightly-tests-mobile-browserstack-android-chrome": "npx browserstack-node-sdk playwright test --grep prod-mobile-nightly --project=android-chrome --browserstack.buildName 'Appointment Nightly Tests Android Chrome' --browserstack.config 'browserstack-mobile-nightly.yml'",
18+
"prod-nightly-tests-mobile-browserstack-ios-safari": "npx browserstack-node-sdk playwright test --grep prod-mobile-nightly --project=ios-safari --browserstack.buildName 'Appointment Nightly Tests iOS Safari' --browserstack.config 'browserstack-mobile-nightly.yml'",
1619
"postinstall": "npm update browserstack-node-sdk"
1720
},
1821
"keywords": [],

test/e2e/pages/settings-page.ts

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {
55
APPT_HTML_DARK_MODE_CLASS,
66
TIMEOUT_1_SECOND,
77
TIMEOUT_2_SECONDS,
8-
TIMEOUT_3_SECONDS,
98
TIMEOUT_5_SECONDS,
109
TIMEOUT_30_SECONDS,
1110
APPT_LANGUAGE_SETTING_EN,
@@ -14,6 +13,7 @@ import {
1413

1514
export class SettingsPage {
1615
readonly page: Page;
16+
readonly testPlatform: string;
1717
readonly accountSettingsBtn: Locator;
1818
readonly connectedAppsBtn: Locator;
1919
readonly preferencesBtn: Locator;
@@ -30,6 +30,7 @@ export class SettingsPage {
3030
readonly displayNameInput: Locator;
3131
readonly bookingPageURLInput: Locator;
3232
readonly copyLinkBtn: Locator;
33+
readonly copyLinkToolTipText: Locator;
3334
readonly cancelServiceBtn: Locator;
3435
readonly cancelServiceConfirmCancelBtn: Locator;
3536
readonly bookingPageSettingsBtn: Locator;
@@ -47,10 +48,11 @@ export class SettingsPage {
4748
readonly savedSuccessfullyTextDE: Locator;
4849
readonly saveBtnDE: Locator;
4950
readonly revertBtn: Locator;
51+
readonly googleSignInHdr: Locator;
5052

51-
52-
constructor(page: Page) {
53+
constructor(page: Page, testPlatform: string = 'desktop') {
5354
this.page = page;
55+
this.testPlatform = testPlatform;
5456

5557
// main settings view
5658
this.settingsHeaderEN = this.page.getByRole('main').getByText('Settings', { exact: true });
@@ -67,6 +69,7 @@ export class SettingsPage {
6769
this.displayNameInput = this.page.locator('#booking-page-display-name');
6870
this.bookingPageURLInput = this.page.locator('#booking-page-url');
6971
this.copyLinkBtn = this.page.locator('#copy-booking-page-url-button');
72+
this.copyLinkToolTipText = this.page.locator('#tooltip-body');
7073
this.cancelServiceBtn = this.page.getByRole('button', { name: 'Cancel Service' });
7174
this.cancelServiceConfirmCancelBtn = this.page.getByRole('button', { name: 'Cancel', exact: true });
7275
this.bookingPageSettingsBtn = this.page.getByRole('button', { name: 'Booking Page Settings' });
@@ -92,6 +95,16 @@ export class SettingsPage {
9295
this.addCaldavCloseModalBtn = this.page.getByRole('img', { name: 'Close' });
9396
this.addGoogleBtn = this.page.getByRole('button', { name: 'Add Google Calendar' });
9497
this.defaultCalendarConnectedCbox = this.page.locator('div').filter({ hasText: /^Default*/ }).getByTestId('checkbox-input');
98+
this.googleSignInHdr = this.page.getByText('Sign in with Google');
99+
}
100+
101+
/**
102+
* Scroll the given element into view. The reason why we do this here is because playright doesn't yet supported this on ios.
103+
*/
104+
async scrollIntoView(targetElement: Locator, timeout: number = 10000) {
105+
if (!this.testPlatform.includes('ios')) {
106+
await targetElement.scrollIntoViewIfNeeded({ timeout: timeout });
107+
}
95108
}
96109

97110
/**
@@ -100,7 +113,7 @@ export class SettingsPage {
100113
async gotoAccountSettings() {
101114
await this.page.goto(APPT_SETTINGS_PAGE);
102115
await this.page.waitForTimeout(TIMEOUT_5_SECONDS);
103-
await this.accountSettingsBtn.scrollIntoViewIfNeeded();
116+
await this.scrollIntoView(this.accountSettingsBtn);
104117
await this.accountSettingsBtn.click();
105118
await this.page.waitForTimeout(TIMEOUT_1_SECOND);
106119
}
@@ -111,7 +124,7 @@ export class SettingsPage {
111124
async gotoPreferencesSettings() {
112125
await this.page.goto(APPT_SETTINGS_PAGE);
113126
await this.page.waitForTimeout(TIMEOUT_5_SECONDS);
114-
await this.preferencesBtn.scrollIntoViewIfNeeded({ timeout: TIMEOUT_30_SECONDS });
127+
await this.scrollIntoView(this.preferencesBtn, TIMEOUT_30_SECONDS);
115128
await this.preferencesBtn.click();
116129
await this.page.waitForTimeout(TIMEOUT_1_SECOND);
117130
}
@@ -122,7 +135,7 @@ export class SettingsPage {
122135
async gotoConnectedAppSettings() {
123136
await this.page.goto(APPT_SETTINGS_PAGE);
124137
await this.page.waitForTimeout(TIMEOUT_5_SECONDS);
125-
await this.connectedAppsBtn.scrollIntoViewIfNeeded();
138+
await this.scrollIntoView(this.connectedAppsBtn);
126139
await this.connectedAppsBtn.click();
127140
await this.page.waitForTimeout(TIMEOUT_1_SECOND);
128141
}
@@ -132,31 +145,32 @@ export class SettingsPage {
132145
*/
133146
async changeDefaultTimezoneSetting(timezone: string) {
134147
await this.defaultTimeZoneSelect.waitFor( { timeout: TIMEOUT_30_SECONDS });
135-
await this.defaultTimeZoneSelect.scrollIntoViewIfNeeded();
148+
await this.scrollIntoView(this.defaultTimeZoneSelect);
136149
await this.page.waitForTimeout(TIMEOUT_1_SECOND);
137150
await this.defaultTimeZoneSelect.selectOption(timezone, { timeout: TIMEOUT_30_SECONDS });
138151
await this.page.waitForTimeout(TIMEOUT_1_SECOND);
139-
await this.saveBtnEN.scrollIntoViewIfNeeded();
152+
await this.scrollIntoView(this.saveBtnEN);
140153
await this.saveBtnEN.click();
141154
await this.page.waitForTimeout(TIMEOUT_1_SECOND);
142155
await expect(this.savedSuccessfullyTextEN).toBeVisible();
156+
await this.page.waitForTimeout(TIMEOUT_2_SECONDS);
143157
}
144158

145159
/**
146160
* Change the language setting
147161
*/
148162
async changeLanguageSetting(currentLanguage: string, newLanguage: string) {
149-
await this.languageSelect.scrollIntoViewIfNeeded();
163+
await this.scrollIntoView(this.languageSelect);
150164
await this.page.waitForTimeout(TIMEOUT_1_SECOND);
151165
await this.languageSelect.selectOption(newLanguage, { timeout: TIMEOUT_30_SECONDS });
152166
await this.page.waitForTimeout(TIMEOUT_1_SECOND);
153167
if (currentLanguage == APPT_LANGUAGE_SETTING_EN) {
154-
await this.saveBtnEN.scrollIntoViewIfNeeded();
168+
await this.scrollIntoView(this.saveBtnEN);
155169
await this.saveBtnEN.click();
156170
await this.page.waitForTimeout(TIMEOUT_1_SECOND);
157171
await expect(this.savedSuccessfullyTextDE).toBeVisible();
158172
} else {
159-
await this.saveBtnDE.scrollIntoViewIfNeeded();
173+
await this.scrollIntoView(this.saveBtnDE);
160174
await this.saveBtnDE.click();
161175
await this.page.waitForTimeout(TIMEOUT_1_SECOND);
162176
await expect(this.savedSuccessfullyTextEN).toBeVisible();
@@ -169,10 +183,11 @@ export class SettingsPage {
169183
*/
170184
async changeThemeSetting(theme: string) {
171185
await this.themeSelect.waitFor({ timeout: TIMEOUT_30_SECONDS });
172-
await this.themeSelect.scrollIntoViewIfNeeded();
186+
await this.scrollIntoView(this.themeSelect);
173187
await this.themeSelect.selectOption(theme);
174188
await this.page.waitForTimeout(TIMEOUT_2_SECONDS);
175189
await this.saveBtnEN.click();
190+
await this.page.waitForTimeout(TIMEOUT_2_SECONDS);
176191
await expect(this.savedSuccessfullyTextEN).toBeVisible({ timeout: TIMEOUT_30_SECONDS });
177192
// wait for theme to take affect, can take time especially on browserstack
178193
await this.page.waitForTimeout(TIMEOUT_5_SECONDS);
@@ -191,15 +206,15 @@ export class SettingsPage {
191206
* Change the start of week setting
192207
*/
193208
async changeStartOfWeekSetting(startOfWeek: string) {
194-
await this.startOfWeekMondayBtn
209+
await this.scrollIntoView(this.startOfWeekMondayBtn);
195210
await this.page.waitForTimeout(TIMEOUT_1_SECOND);
196211
if (startOfWeek == 'M') {
197212
await this.startOfWeekMondayBtn.click({ timeout: TIMEOUT_30_SECONDS });
198213
} else {
199214
await this.startOfWeekSundayBtn.click({ timeout: TIMEOUT_30_SECONDS });
200215
}
201216
await this.page.waitForTimeout(TIMEOUT_1_SECOND);
202-
await this.saveBtnEN.scrollIntoViewIfNeeded();
217+
await this.scrollIntoView(this.saveBtnEN);
203218
await this.saveBtnEN.click();
204219
await this.page.waitForTimeout(TIMEOUT_1_SECOND);
205220
await expect(this.savedSuccessfullyTextEN).toBeVisible();

0 commit comments

Comments
 (0)