Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
9b94a3e
Added Database checks to the onboarding flow
O-Bots Feb 22, 2026
d0f828f
Added compatibility page setup
O-Bots Feb 25, 2026
862418e
Finished up the onboarding flow suite
O-Bots Mar 4, 2026
02a5fc1
.
O-Bots Mar 4, 2026
9630b20
Fix: Merge conflict
O-Bots Mar 11, 2026
b3a2eb0
.
O-Bots Mar 13, 2026
5af84b6
Fix: Added fix for None discriptive error issue #36
O-Bots Mar 15, 2026
43f6f9d
Linting and Prettier
O-Bots Mar 15, 2026
b0d5f63
Minor cleaning
MartinBraquet Mar 15, 2026
2a4433f
Organizing helper func
O-Bots Mar 16, 2026
2d34bb8
Added Google account to the Onboarding flow
O-Bots Mar 17, 2026
0d9cd88
.
O-Bots Mar 17, 2026
4ef1538
Added account cleanup for google accounts
O-Bots Mar 18, 2026
6595eed
Started work on Sign-in tests
O-Bots Mar 25, 2026
cf951b5
Linting and Prettier
O-Bots Mar 25, 2026
c092182
Added checks to the deleteUser func to check if the accout exists
O-Bots Mar 26, 2026
e925bf8
Linting and Prettier
O-Bots Mar 26, 2026
c130e88
Added POM's for social and organisation page
O-Bots Mar 31, 2026
e350587
Formatting update, fixed homePage locator for signin
O-Bots Apr 2, 2026
13b4c90
.
O-Bots Apr 2, 2026
1330220
.
O-Bots Apr 2, 2026
f6f9253
.
O-Bots Apr 2, 2026
2c48812
Merge branch 'main' into main
MartinBraquet Apr 2, 2026
a5087d8
Merge branch 'main' into main
MartinBraquet Apr 2, 2026
1bd9f0c
Merge branch 'main' into main
MartinBraquet Apr 2, 2026
5776360
Coderabbitai fix's
O-Bots Apr 2, 2026
a3aeb0f
Fix
MartinBraquet Apr 3, 2026
6fd0749
Improve test utilities and stabilize onboarding flow tests
MartinBraquet Apr 3, 2026
892305c
Merge branch 'main' into main
MartinBraquet Apr 3, 2026
497f6cd
Changes requested
O-Bots Apr 3, 2026
b3458b9
Seperated deletion tests from onboarding
O-Bots Apr 3, 2026
fbabb15
Update `.coderabbit.yaml` with improved internationalization guidance…
MartinBraquet Apr 4, 2026
cac3cf6
Clean up `.vscode/settings.json` and add it to `.gitignore`
MartinBraquet Apr 4, 2026
0bf19ee
Add Playwright E2E test guidelines to `.coderabbit.yaml`
MartinBraquet Apr 4, 2026
f39511c
Standardize and improve formatting in `TESTING.md` for better readabi…
MartinBraquet Apr 4, 2026
6770969
Refactor onboarding flow tests and related utilities; improve formatt…
MartinBraquet Apr 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ reviews:
- Flag any hardcoded strings; they should be in the constants file.
- Check for edge cases like null values or empty arrays.
- Suggest performance optimizations where appropriate.

Mobile best practices:
- Proper use of hooks (useRouter, useFonts, useAssets)
- Accessibility: touch targets min 44x44, screen reader support
Expand All @@ -99,4 +99,22 @@ reviews:
- Validate deep linking configurations

Internationalization:
- User-visible strings should be externalized to resource files (useT())
- User-visible strings should be externalized to JSON resource files in common/messages via
```
const t = useT()
const message = t('key', 'english string')
```

- path: "tests/e2e/**/*.ts"
instructions: |
Playwright E2E test guidelines for this repo:
- Page objects live in `tests/e2e/web/pages/`. Each class wraps one page/route, holds only `private readonly` Locators, and exposes action methods.
- All tests must use the `app` fixture (type `App`) from `tests/e2e/web/fixtures/base.ts`. Never instantiate page objects directly in a test.
- Cross-page flows (actions spanning multiple pages) belong as methods on the `App` class, not as standalone helper functions.
- Action methods in page objects must assert `expect(locator).toBeVisible()` before interacting.
- Never use `page.waitForTimeout()`. Use Playwright's built-in auto-waiting or `waitForURL` / `waitForSelector`.
- No hardcoded credentials in spec files; use `SPEC_CONFIG.ts` or account fixtures.
- Test account cleanup must be done in fixture teardown (after `await use(...)`), not in `afterEach` hooks.
- File and class names must use PascalCase (e.g., `CompatibilityPage.ts` / `class CompatibilityPage`).
- No DB or Firebase calls inside page object classes; those belong in `tests/e2e/utils/`.
- Flag any new page object not yet registered in `App`.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,5 @@ test-results
**/coverage

*my-release-key.keystore

.vscode/settings.json
68 changes: 66 additions & 2 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ jest.mock('path/to/module')
* This creates an object containing all named exports from ./path/to/module
*/
import * as mockModule from 'path/to/module'

;(mockModule.module as jest.Mock).mockResolvedValue(mockReturnValue)
```

Expand Down Expand Up @@ -705,6 +706,66 @@ Use this priority order for selecting elements in Playwright tests:

This hierarchy mirrors how users actually interact with your application, making tests more reliable and meaningful.

### Page Object Model (POM)

Tests often receive multiple page objects as fixtures (e.g. `homePage`, `authPage`, `profilePage`). This is the **Page
Object Model** pattern — a way to organize selectors and actions by the area of the app they belong to.

**Page objects are not separate browser tabs.** They are all wrappers around the same underlying `page` instance. Each
class simply encapsulates the selectors and actions relevant to one part of the UI:

```typescript
class ProfilePage {
constructor(private page: Page) {}

async verifyDisplayName(name: string) {
await expect(this.page.getByTestId('display-name')).toHaveText(name)
}
}

class SettingsPage {
constructor(private page: Page) {} // same page instance

async deleteAccount() {
await this.page.getByRole('button', {name: 'Delete account'}).click()
}
}
```

**Why use POM instead of raw `page`?**

Without it, tests are full of inline selectors that are brittle and hard to read. POM moves implementation details into
dedicated classes so that the test itself reads like a plain-English description of user behavior. When a selector
changes, you fix it in one place.

```typescript
// ❌ Without POM — noisy and brittle
await page.locator('#email-input').fill(account.email)
await page.locator('#submit-btn').click()
await page.locator('[data-testid="skip-onboarding"]').click()
// ...50 more lines of noise

// ✅ With POM — readable and maintainable
await registerWithEmail(homePage, authPage, fakerAccount)
await skipOnboardingHeadToProfile(onboardingPage, signUpPage, profilePage, fakerAccount)
await profilePage.verifyDisplayName(fakerAccount.display_name)
```

**What happens if you call a method on the "wrong" page object?**

Nothing special — it still runs. Since all page objects share the same `page`, the method simply acts on whatever is
currently rendered in the browser. Page objects do not track which screen you're on; that's your responsibility as the
test author. If you call `profilePage.verifyDisplayName()` while the browser is showing the settings screen, the locator
won't find its element and the test will **time out**.

```typescript
// ⚠️ This fails at runtime if navigation hasn't happened yet
await settingsPage.deleteAccount() // navigates away from profile
await profilePage.verifyDisplayName(name) // locator not found → timeout
```

Always ensure navigation has completed before calling methods that depend on a specific screen being visible.

### Setting up test data

Since the tests run in parallel (i.e., at the same time) and share the same database and Firebase emulator, it can
Expand Down Expand Up @@ -802,7 +863,9 @@ These are seeded automatically by `yarn test:db:seed`:

## Troubleshooting

For comprehensive troubleshooting guidance beyond testing-specific issues, see the [Troubleshooting Guide](TROUBLESHOOTING.md) which covers development environment setup, database and emulator issues, API problems, and more.
For comprehensive troubleshooting guidance beyond testing-specific issues, see
the [Troubleshooting Guide](TROUBLESHOOTING.md) which covers development environment setup, database and emulator
issues, API problems, and more.

### Port already in use

Expand Down Expand Up @@ -885,4 +948,5 @@ To download the Playwright report from a failed CI run:
3. Download `playwright-report`
4. Open `index.html` in your browser

For performance testing guidance and benchmarking strategies, see the [Performance Optimization Guide](PERFORMANCE_OPTIMIZATION.md).
For performance testing guidance and benchmarking strategies, see
the [Performance Optimization Guide](PERFORMANCE_OPTIMIZATION.md).
5 changes: 3 additions & 2 deletions tests/e2e/backend/utils/userInformation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ import {
SUBSTANCE_PREFERENCE_CHOICES,
} from 'common/choices'

class UserAccountInformation {
class UserAccountInformationForSeeding {
name = faker.person.fullName()
userName = faker.internet.displayName()
email = faker.internet.email()
user_id = faker.string.alpha(28)
password = faker.internet.password()
Expand Down Expand Up @@ -54,4 +55,4 @@ class UserAccountInformation {
}
}

export default UserAccountInformation
export default UserAccountInformationForSeeding
7 changes: 4 additions & 3 deletions tests/e2e/utils/databaseUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ export async function deleteFromDb(user_id: string) {
const result = await db.query(deleteEntryById, [user_id])

if (!result.length) {
throw new Error(`No user found with id: ${user_id}`)
console.debug(`No user found with id: ${user_id}`)
return
}

console.log('Deleted data: ', {
Expand All @@ -19,8 +20,8 @@ export async function deleteFromDb(user_id: string) {
export async function userInformationFromDb(account: any) {
const db = createSupabaseDirectClient()
const queryUserById = `
SELECT p.*
FROM users AS p
SELECT *
FROM users
WHERE username = $1
`
const userResults = await db.query(queryUserById, [account.username])
Expand Down
35 changes: 29 additions & 6 deletions tests/e2e/utils/firebaseUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import axios from 'axios'

import {config} from '../web/SPEC_CONFIG'

export async function firebaseLogin(email: string, password: string) {
export async function firebaseLoginEmailPassword(
email: string | undefined,
password: string | undefined,
) {
const login = await axios.post(
`${config.FIREBASE_URL.BASE}${config.FIREBASE_URL.SIGN_IN_PASSWORD}`,
{
Expand All @@ -13,15 +16,28 @@ export async function firebaseLogin(email: string, password: string) {
)
return login
}

export async function getUserId(email: string, password: string) {
try {
const loginInfo = await firebaseLogin(email, password)
const loginInfo = await firebaseLoginEmailPassword(email, password)
return loginInfo.data.localId
} catch {
return
}
}

export async function findUser(idToken: string) {
const response = await axios.post(
`${config.FIREBASE_URL.BASE}${config.FIREBASE_URL.ACCOUNT_LOOKUP}`,
{
idToken,
},
)
if (response?.data?.users?.length > 0) {
return response.data.users[0]
}
}

export async function firebaseSignUp(email: string, password: string) {
try {
const response = await axios.post(`${config.FIREBASE_URL.BASE}${config.FIREBASE_URL.SIGNUP}`, {
Expand All @@ -46,8 +62,15 @@ export async function firebaseSignUp(email: string, password: string) {
}
}

export async function deleteAccount(login: any) {
await axios.post(`${config.FIREBASE_URL.BASE}${config.FIREBASE_URL.DELETE}`, {
idToken: login.data.idToken,
})
export async function deleteAccount(idToken: any) {
try {
await axios.post(`${config.FIREBASE_URL.BASE}${config.FIREBASE_URL.DELETE}`, {
idToken: idToken,
})
} catch (err: any) {
if (err.response?.data?.error?.message?.includes('USER_NOT_FOUND')) {
return
}
throw err
}
}
24 changes: 16 additions & 8 deletions tests/e2e/utils/seedDatabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {createSupabaseDirectClient} from 'shared/supabase/init'
import {insert} from 'shared/supabase/utils'
import {getUser} from 'shared/utils'

import UserAccountInformation from '../backend/utils/userInformation'
import UserAccountInformationForSeeding from '../backend/utils/userInformation'
import {firebaseSignUp} from './firebaseUtils'

/**
Expand All @@ -16,7 +16,10 @@ import {firebaseSignUp} from './firebaseUtils'
* @param userInfo - Class object containing information to create a user account generated by `fakerjs`.
* @param profileType - Optional param used to signify how much information is used in the account generation.
*/
export async function seedDbUser(userInfo: UserAccountInformation, profileType?: string) {
export async function seedDbUser(
userInfo: UserAccountInformationForSeeding,
profileType?: string,
): Promise<Boolean> {
const pg = createSupabaseDirectClient()
const userId = userInfo.user_id
const deviceToken = randomString()
Expand Down Expand Up @@ -83,14 +86,14 @@ export async function seedDbUser(userInfo: UserAccountInformation, profileType?:
blockedByUserIds: [],
}

await pg.tx(async (tx: any) => {
return pg.tx(async (tx: any) => {
const preexistingUser = await getUser(userId, tx)
if (preexistingUser) return
if (preexistingUser) return false

await insert(tx, 'users', {
id: userId,
name: userInfo.name,
username: cleanUsername(userInfo.name),
username: cleanUsername(userInfo.userName),
data: {},
Comment on lines +96 to 97

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard against empty usernames after normalization.

Line 96 can normalize symbol/whitespace-heavy input to an empty username, then insert invalid data.

🛡️ Proposed fix
-    await insert(tx, 'users', {
+    const normalizedUsername = cleanUsername(userInfo.userName).trim()
+    if (!normalizedUsername) throw new Error('seedDbUser: username is empty after normalization')
+
+    await insert(tx, 'users', {
       id: userId,
       name: userInfo.name,
-      username: cleanUsername(userInfo.userName),
+      username: normalizedUsername,
       data: {},
     })
As per coding guidelines, "Check for edge cases like null values or empty arrays".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/utils/seedDatabase.ts` around lines 96 - 97, The username field is
set to cleanUsername(userInfo.userName) which can return an empty string for
symbol/whitespace-heavy input; before inserting the user (in
tests/e2e/utils/seedDatabase.ts where the user object is built with username:
cleanUsername(userInfo.userName), data: {}), validate the normalized username
and guard against empty values—either skip creating/inserting that user or throw
a clear error; add a conditional that checks the result of
cleanUsername(userInfo.userName) and handles the empty-case appropriately (e.g.,
log/throw or generate a fallback) so no invalid empty username is inserted.

})

Expand All @@ -100,20 +103,25 @@ export async function seedDbUser(userInfo: UserAccountInformation, profileType?:
})

await insert(tx, 'profiles', profileData)
return true
})
}

export async function seedUser(
email?: string | undefined,
password?: string | undefined,
profileType?: string | undefined,
displayName?: string | undefined,
userName?: string | undefined,
) {
const userInfo = new UserAccountInformation()
const userInfo = new UserAccountInformationForSeeding()
if (email) userInfo.email = email
if (password) userInfo.password = password
if (displayName) userInfo.name = displayName
if (userName) userInfo.userName = userName
Comment thread
coderabbitai[bot] marked this conversation as resolved.
userInfo.user_id = await firebaseSignUp(userInfo.email, userInfo.password)
if (userInfo.user_id) {
await seedDbUser(userInfo, profileType ?? 'full')
const created = await seedDbUser(userInfo, profileType ?? 'full')
if (created) debug('User created in Firebase and Supabase:', userInfo.email)
}
debug('User created in Firebase and Supabase:', userInfo.email)
}
1 change: 1 addition & 0 deletions tests/e2e/web/SPEC_CONFIG.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export const config = {
BASE: 'http://localhost:9099/identitytoolkit.googleapis.com/v1',
SIGNUP: '/accounts:signUp?key=fake-api-key',
SIGN_IN_PASSWORD: '/accounts:signInWithPassword?key=fake-api-key',
ACCOUNT_LOOKUP: '/accounts:lookup?key=fake-api-key',
DELETE: '/accounts:delete?key=fake-api-key',
},
USERS: {
Expand Down
45 changes: 40 additions & 5 deletions tests/e2e/web/fixtures/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,63 @@ import {AuthPage} from '../pages/AuthPage'
import {ComatibilityPage} from '../pages/compatibilityPage'
import {HomePage} from '../pages/homePage'
import {OnboardingPage} from '../pages/onboardingPage'
import {OrganizationPage} from '../pages/organizationPage'
import {ProfilePage} from '../pages/profilePage'
import {SettingsPage} from '../pages/settingsPage'
import {SignUpPage} from '../pages/signUpPage'
import {SocialPage} from '../pages/socialPage'
import {testAccounts, UserAccountInformation} from '../utils/accountInformation'
import {deleteUser} from '../utils/deleteUser'
import {getAuthAccountInfo} from '../utils/networkUtils'

export const test = base.extend<{
homePage: HomePage
onboardingPage: OnboardingPage
signUpPage: SignUpPage
profilePage: ProfilePage
authPage: AuthPage
settingsPage: SettingsPage
socialPage: SocialPage
organizationPage: OrganizationPage
compatabilityPage: ComatibilityPage
cleanUpUsers: void
onboardingAccount: UserAccountInformation
fakerAccount: UserAccountInformation
specAccount: UserAccountInformation
googleAccountOne: UserAccountInformation
googleAccountTwo: UserAccountInformation
}>({
onboardingAccount: async ({}, use) => {
const account = testAccounts.account_all_info() // email captured here
const account = testAccounts.email_account_all_info() // email captured here
await use(account)
console.log('Cleaning up onboarding 1 account...')
await deleteUser(account.email, account.password) // same account, guaranteed
await deleteUser('Email/Password', account) // same account, guaranteed
},
fakerAccount: async ({}, use) => {
const account = testAccounts.faker_account() // email captured here
const account = testAccounts.faker_account()
await use(account)
console.log('Cleaning up faker account...')
await deleteUser(account.email, account.password) // same account, guaranteed
await deleteUser('Email/Password', account)
},
googleAccountOne: async ({page}, use) => {
const account = testAccounts.google_account_one()
const getAuthObject = await getAuthAccountInfo(page)
await use(account)
console.log('Cleaning up google account...')
await deleteUser('Google', undefined, getAuthObject())
},
googleAccountTwo: async ({page}, use) => {
const account = testAccounts.google_account_two()
const getAuthObject = await getAuthAccountInfo(page)
await use(account)
console.log('Cleaning up google account...')
await deleteUser('Google', undefined, getAuthObject())
Comment thread
MartinBraquet marked this conversation as resolved.
},
specAccount: async ({}, use) => {
const account = testAccounts.spec_account()
await use(account)
console.log('Cleaning up spec account...')
await deleteUser(account.email, account.password)
await deleteUser('Email/Password', account)
},
onboardingPage: async ({page}, use) => {
const onboardingPage = new OnboardingPage(page)
Expand All @@ -63,6 +86,18 @@ export const test = base.extend<{
const compatibilityPage = new ComatibilityPage(page)
await use(compatibilityPage)
},
settingsPage: async ({page}, use) => {
const settingsPage = new SettingsPage(page)
await use(settingsPage)
},
socialPage: async ({page}, use) => {
const socialPage = new SocialPage(page)
await use(socialPage)
},
organizationPage: async ({page}, use) => {
const organizationPage = new OrganizationPage(page)
await use(organizationPage)
},
})

export {expect} from '@playwright/test'
Loading
Loading