Skip to content

Commit 17072d8

Browse files
committed
First set of E2E tests on iOS
1 parent 14b27ac commit 17072d8

15 files changed

+252
-161
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 Chrome' --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: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,16 @@ 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,
1211
} from '../const/constants';
12+
import test from 'node:test';
1313

1414

1515
export class SettingsPage {
1616
readonly page: Page;
17+
readonly testPlatform: string;
1718
readonly accountSettingsBtn: Locator;
1819
readonly connectedAppsBtn: Locator;
1920
readonly preferencesBtn: 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 });
@@ -92,6 +94,16 @@ export class SettingsPage {
9294
this.addCaldavCloseModalBtn = this.page.getByRole('img', { name: 'Close' });
9395
this.addGoogleBtn = this.page.getByRole('button', { name: 'Add Google Calendar' });
9496
this.defaultCalendarConnectedCbox = this.page.locator('div').filter({ hasText: /^Default*/ }).getByTestId('checkbox-input');
97+
this.googleSignInHdr = this.page.getByText('Sign in with Google');
98+
}
99+
100+
/**
101+
* Scroll the given element into view. The reason why we do this here is because playright doesn't yet supported this on ios.
102+
*/
103+
async scrollIntoView(targetElement: Locator, timeout: number = 10000) {
104+
if (!this.testPlatform.includes('ios')) {
105+
await targetElement.scrollIntoViewIfNeeded({ timeout: timeout });
106+
}
95107
}
96108

97109
/**
@@ -100,7 +112,7 @@ export class SettingsPage {
100112
async gotoAccountSettings() {
101113
await this.page.goto(APPT_SETTINGS_PAGE);
102114
await this.page.waitForTimeout(TIMEOUT_5_SECONDS);
103-
await this.accountSettingsBtn.scrollIntoViewIfNeeded();
115+
await this.scrollIntoView(this.accountSettingsBtn);
104116
await this.accountSettingsBtn.click();
105117
await this.page.waitForTimeout(TIMEOUT_1_SECOND);
106118
}
@@ -111,7 +123,7 @@ export class SettingsPage {
111123
async gotoPreferencesSettings() {
112124
await this.page.goto(APPT_SETTINGS_PAGE);
113125
await this.page.waitForTimeout(TIMEOUT_5_SECONDS);
114-
await this.preferencesBtn.scrollIntoViewIfNeeded({ timeout: TIMEOUT_30_SECONDS });
126+
await this.scrollIntoView(this.preferencesBtn, TIMEOUT_30_SECONDS);
115127
await this.preferencesBtn.click();
116128
await this.page.waitForTimeout(TIMEOUT_1_SECOND);
117129
}
@@ -122,7 +134,7 @@ export class SettingsPage {
122134
async gotoConnectedAppSettings() {
123135
await this.page.goto(APPT_SETTINGS_PAGE);
124136
await this.page.waitForTimeout(TIMEOUT_5_SECONDS);
125-
await this.connectedAppsBtn.scrollIntoViewIfNeeded();
137+
await this.scrollIntoView(this.connectedAppsBtn);
126138
await this.connectedAppsBtn.click();
127139
await this.page.waitForTimeout(TIMEOUT_1_SECOND);
128140
}
@@ -132,31 +144,32 @@ export class SettingsPage {
132144
*/
133145
async changeDefaultTimezoneSetting(timezone: string) {
134146
await this.defaultTimeZoneSelect.waitFor( { timeout: TIMEOUT_30_SECONDS });
135-
await this.defaultTimeZoneSelect.scrollIntoViewIfNeeded();
147+
await this.scrollIntoView(this.defaultTimeZoneSelect);
136148
await this.page.waitForTimeout(TIMEOUT_1_SECOND);
137149
await this.defaultTimeZoneSelect.selectOption(timezone, { timeout: TIMEOUT_30_SECONDS });
138150
await this.page.waitForTimeout(TIMEOUT_1_SECOND);
139-
await this.saveBtnEN.scrollIntoViewIfNeeded();
151+
await this.scrollIntoView(this.saveBtnEN);
140152
await this.saveBtnEN.click();
141153
await this.page.waitForTimeout(TIMEOUT_1_SECOND);
142154
await expect(this.savedSuccessfullyTextEN).toBeVisible();
155+
await this.page.waitForTimeout(TIMEOUT_2_SECONDS);
143156
}
144157

145158
/**
146159
* Change the language setting
147160
*/
148161
async changeLanguageSetting(currentLanguage: string, newLanguage: string) {
149-
await this.languageSelect.scrollIntoViewIfNeeded();
162+
await this.scrollIntoView(this.languageSelect);
150163
await this.page.waitForTimeout(TIMEOUT_1_SECOND);
151164
await this.languageSelect.selectOption(newLanguage, { timeout: TIMEOUT_30_SECONDS });
152165
await this.page.waitForTimeout(TIMEOUT_1_SECOND);
153166
if (currentLanguage == APPT_LANGUAGE_SETTING_EN) {
154-
await this.saveBtnEN.scrollIntoViewIfNeeded();
167+
await this.scrollIntoView(this.saveBtnEN);
155168
await this.saveBtnEN.click();
156169
await this.page.waitForTimeout(TIMEOUT_1_SECOND);
157170
await expect(this.savedSuccessfullyTextDE).toBeVisible();
158171
} else {
159-
await this.saveBtnDE.scrollIntoViewIfNeeded();
172+
await this.scrollIntoView(this.saveBtnDE);
160173
await this.saveBtnDE.click();
161174
await this.page.waitForTimeout(TIMEOUT_1_SECOND);
162175
await expect(this.savedSuccessfullyTextEN).toBeVisible();
@@ -169,10 +182,11 @@ export class SettingsPage {
169182
*/
170183
async changeThemeSetting(theme: string) {
171184
await this.themeSelect.waitFor({ timeout: TIMEOUT_30_SECONDS });
172-
await this.themeSelect.scrollIntoViewIfNeeded();
185+
await this.scrollIntoView(this.themeSelect);
173186
await this.themeSelect.selectOption(theme);
174187
await this.page.waitForTimeout(TIMEOUT_2_SECONDS);
175188
await this.saveBtnEN.click();
189+
await this.page.waitForTimeout(TIMEOUT_2_SECONDS);
176190
await expect(this.savedSuccessfullyTextEN).toBeVisible({ timeout: TIMEOUT_30_SECONDS });
177191
// wait for theme to take affect, can take time especially on browserstack
178192
await this.page.waitForTimeout(TIMEOUT_5_SECONDS);
@@ -199,7 +213,7 @@ export class SettingsPage {
199213
await this.startOfWeekSundayBtn.click({ timeout: TIMEOUT_30_SECONDS });
200214
}
201215
await this.page.waitForTimeout(TIMEOUT_1_SECOND);
202-
await this.saveBtnEN.scrollIntoViewIfNeeded();
216+
await this.scrollIntoView(this.saveBtnEN);
203217
await this.saveBtnEN.click();
204218
await this.page.waitForTimeout(TIMEOUT_1_SECOND);
205219
await expect(this.savedSuccessfullyTextEN).toBeVisible();

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,17 @@ export class TBAcctsPage {
1818
this.userAvatar = this.page.getByTestId('avatar-default');
1919
this.emailInput = this.page.getByTestId('username-input');
2020
this.passwordInput = this.page.getByTestId('password-input');
21-
this.signInButton = this.page.getByRole('button', { name: 'Sign in' });
21+
this.signInButton = this.page.getByTestId('submit-btn');
2222
this.loginEmailInput = this.page.getByLabel('Email');
2323
this.localDevpasswordInput = this.page.getByLabel('Password');
2424
this.loginDialogContinueBtn = this.page.getByTitle('Continue');
2525
}
2626

2727
/**
28-
* Sign in to TB Accounts using the provided email and password.
28+
* Sign in to TB Accounts using the provided email and password. We provide the playwright
29+
* test project name (i.e. 'android-chrome') as some actions differ on different mobile platforms.
2930
*/
30-
async signIn() {
31+
async signIn(testProjectName: string = 'desktop') {
3132
console.log('signing in to TB Accounts');
3233
expect(TB_ACCTS_EMAIL, 'getting TB_ACCTS_EMAIL env var').toBeTruthy();
3334
expect(TB_ACCTS_PWORD, 'getting TB_ACCTS_PWORD env var').toBeTruthy();
@@ -37,7 +38,12 @@ export class TBAcctsPage {
3738
await this.passwordInput.fill(String(TB_ACCTS_PWORD));
3839
await this.page.waitForTimeout(TIMEOUT_1_SECOND);
3940
await this.page.waitForTimeout(TIMEOUT_1_SECOND);
40-
await this.signInButton.click({ force: true });
41+
// 'force' is needed for android but doesn't work on ios
42+
if (testProjectName.includes('android')) {
43+
await this.signInButton.click({ force: true });
44+
} else {
45+
await this.signInButton.click({ timeout: TIMEOUT_10_SECONDS });
46+
}
4147
await this.page.waitForTimeout(TIMEOUT_10_SECONDS);
4248
}
4349

0 commit comments

Comments
 (0)