Skip to content

Commit dfd0dc9

Browse files
authored
[Doc update] Udated testing.md (#56)
* Changed fixtures used in accounts that dont need to check sign-in functionality * Updated testing doc: added fixture info and expanded on POM pattern * Update testing.md * Udated testing.md Added Superbase emulator workaround
1 parent 82eab67 commit dfd0dc9

2 files changed

Lines changed: 194 additions & 71 deletions

File tree

docs/testing.md

Lines changed: 180 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ web/
136136

137137
### Jest Unit Testing Guide
138138

139-
This guide provides guidelines and best practices for writing unit tests using Jest in this project. Following these
139+
These are the guidelines and best practices we follow when writing unit tests using Jest in this project. Following these
140140
standards ensures consistency, maintainability, and comprehensive test coverage.
141141

142142
#### Best Practices
@@ -348,7 +348,6 @@ jest.mock('path/to/module')
348348
* This creates an object containing all named exports from ./path/to/module
349349
*/
350350
import * as mockModule from 'path/to/module'
351-
352351
;(mockModule.module as jest.Mock).mockResolvedValue(mockReturnValue)
353352
```
354353

@@ -493,14 +492,28 @@ jest.spyOn(Array.prototype, 'includes').mockImplementation(function (value) {
493492

494493
# Playwright (E2E) Testing Guide
495494

496-
E2E tests use [Playwright](https://playwright.dev/) and run against a fully isolated local stack:
495+
These are the guidelines and best practices we follow when writing E2E tests using [Playwright](https://playwright.dev/) in this project. Following these
496+
standards ensures consistency, maintainability, and comprehensive test coverage.
497+
498+
E2E tests live in `tests/e2e/` and follow the `*.spec.ts` naming convention, when executed they are run against a fully isolated local stack:
497499

498-
- **Supabase** (Postgres) via `npx supabase start`
499-
- **Firebase** (Auth and Storage) via `firebase emulators:start`
500+
- **Supabase** (Postgres)
501+
- **Firebase** (Auth and Storage)
500502
- **Backend API** (`backend/api`)
501503
- **Next.js frontend** (`web`)
502504

503-
Tests live in `tests/e2e/` and follow the `*.e2e.spec.ts` naming convention.
505+
### Best Practices
506+
507+
1. Test one scenario per test - Each test should verify a single behavior.
508+
2. Keep tests independent - Each test should be fully independent.
509+
3. Use locators that reflect how users will interact with the application - Outlined in the [Component Selection Hierarchy](###component-selection-hierarchy) below.
510+
4. Use the Page Object Model (POM) - Keeps the test files lightweight and easily readable at a glance.
511+
5. Store authentication state - Persist login state via `storageState` and reuse it across tests where needed.
512+
6. Use fixtures for setup and teardown - Keeps test files lightweight.
513+
7. Use web-first assertions - Use Playwright's built-in `expect` assertions (e.g., `toBeVisible`, `toHaveText`) which auto-retry until the condition is met.
514+
8. Use environment variables for config - Store credentials, base URLs, and environment-specific settings in env vars rather than hardcoding them in test files.
515+
9. Organise tests with a clear folder structure - Separate test files, page objects, fixtures, and helpers into distinct directories for scalability and maintainability.
516+
10. Integrate with CI/CD - Run Playwright tests in headless mode in your pipeline, alerting you of possible issues early.
504517

505518
---
506519

@@ -603,9 +616,15 @@ This opens a visual browser interface where you can:
603616
- 🔄 Re-run tests without restarting anything
604617
- 🕵️ Time-travel debug through test steps
605618

619+
Alternatively if you only want to open the Playwright UI you can use:
620+
621+
```bash
622+
npx playwright test --ui
623+
```
624+
606625
### 3. Edit tests and re-run
607626

608-
Edit your `*.e2e.spec.ts` file, save, then click **Run** in the Playwright UI.
627+
Edit your `*.spec.ts` file, save, then click **Run** in the Playwright UI.
609628
No restart needed for test file changes.
610629

611630
### 4. Reset data when needed
@@ -672,10 +691,10 @@ tests/
672691
└── e2e/
673692
├── web/
674693
│ └── specs/
675-
│ └── auth.e2e.spec.ts
694+
│ └── auth.spec.ts
676695
└── backend/
677696
└── specs/
678-
└── api.e2e.spec.ts
697+
└── api.spec.ts
679698
```
680699

681700
### Component Selection Hierarchy
@@ -712,23 +731,62 @@ This hierarchy mirrors how users actually interact with your application, making
712731
Tests often receive multiple page objects as fixtures (e.g. `homePage`, `authPage`, `profilePage`). This is the **Page
713732
Object Model** pattern — a way to organize selectors and actions by the area of the app they belong to.
714733

715-
**Page objects are not separate browser tabs.** They are all wrappers around the same underlying `page` instance. Each
716-
class simply encapsulates the selectors and actions relevant to one part of the UI:
734+
**Page objects** are all wrappers around the same underlying `page` instance. Each
735+
class simply encapsulates the selectors and actions relevant to an entire page of the application.
736+
737+
The `app.ts` file improves scalability by acting as a central hub for page objects and shared modules. Instead of importing 40 different pages into a test, modules can be accessed through `app.ts`, making tests cleaner and easier to maintain. It also supports functionality that spans multiple pages.
717738

718739
```typescript
719-
class ProfilePage {
720-
constructor(private page: Page) {}
740+
//profilePage.ts
741+
import {expect, Locator, Page} from '@playwright/test'
742+
743+
export class ProfilePage {
744+
private readonly displayName: Locator
745+
746+
constructor(public readonly page: Page) {
747+
this.displayName = page.getByTestId('display-name')
748+
}
721749

722750
async verifyDisplayName(name: string) {
723-
await expect(this.page.getByTestId('display-name')).toHaveText(name)
751+
await expect(this.displayName).toBeVisible()
752+
await expect(this.displayName).toHaveText(name)
724753
}
725754
}
726755

727-
class SettingsPage {
728-
constructor(private page: Page) {} // same page instance
756+
//settingsPage.ts
757+
import {expect, Locator, Page} from '@playwright/test'
758+
759+
export class SettingsPage {
760+
private readonly deleteAccountButton: Locator
761+
762+
constructor(public readonly page: Page) {
763+
this.deleteAccountButton = page.getByRole('button', {name: 'Delete account'})
764+
}
729765

730766
async deleteAccount() {
731-
await this.page.getByRole('button', {name: 'Delete account'}).click()
767+
await expect(this.deleteAccountButton).toBeVisible()
768+
await this.deleteAccountButton.click()
769+
}
770+
}
771+
772+
//app.ts
773+
import {ProfilePage} from './profilePage'
774+
import {SettingsPage} from './settingsPage'
775+
776+
export class App {
777+
readonly profile: ProfilePage
778+
readonly settings: SettingsPage
779+
780+
constructor(public readonly page: Page) {
781+
this.profile = new ProfilePage(page)
782+
this.settings = new SettingsPage(page)
783+
}
784+
785+
//Methods that span multiple pages can be outlined here
786+
async verifyAccountThenDelete(name: string) {
787+
this.profile.verifyDisplayName(name)
788+
//navigation to the settings page
789+
this.settings.deleteAccount()
732790
}
733791
}
734792
```
@@ -747,9 +805,9 @@ await page.locator('[data-testid="skip-onboarding"]').click()
747805
// ...50 more lines of noise
748806

749807
// ✅ With POM — readable and maintainable
750-
await registerWithEmail(homePage, authPage, fakerAccount)
751-
await skipOnboardingHeadToProfile(onboardingPage, signUpPage, profilePage, fakerAccount)
752-
await profilePage.verifyDisplayName(fakerAccount.display_name)
808+
await app.registerWithEmail(fakerAccount)
809+
await app.skipOnboardingHeadToProfile(fakerAccount)
810+
await app.profile.verifyDisplayName(fakerAccount)
753811
```
754812

755813
**What happens if you call a method on the "wrong" page object?**
@@ -761,20 +819,93 @@ won't find its element and the test will **time out**.
761819

762820
```typescript
763821
// ⚠️ This fails at runtime if navigation hasn't happened yet
764-
await settingsPage.deleteAccount() // navigates away from profile
765-
await profilePage.verifyDisplayName(name) // locator not found → timeout
822+
await app.settings.deleteAccount() // navigates away from profile
823+
await app.profile.verifyDisplayName(name) // locator not found → timeout
766824
```
767825

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

828+
### Fixtures
829+
830+
To further improve readability, and simplify the creation/implimentation of tests, fixtures are used for test case setup and teardown where appropriate.
831+
832+
```typescript
833+
//baseFixture.ts
834+
import {test as base} from '@playwright/test'
835+
import {App} from './app.ts'
836+
import {testAccounts, UserAccountInformation} from './accountInformation'
837+
import {deleteUser} from './deleteUser'
838+
import {seedUser} from './seedDatabase'
839+
840+
export const test = base.extend<{
841+
app: App
842+
signedInAccount: UserAccountInformation
843+
}>({
844+
//This gives access to the entire POM structure for the application
845+
app: async ({page}, use) => {
846+
const appPage = new App(page)
847+
await use(appPage)
848+
},
849+
/**
850+
* This generates a test account
851+
* Seeds the database with the user
852+
* Signs the user into the app
853+
* Executes the test
854+
* Deletes the user after execution is complete
855+
*/
856+
signedInAccount: async ({app}: {app: App}, use) => {
857+
const account = testAccounts.faker_account()
858+
await seedUser(account.email, account.password)
859+
await app.signinWithEmail(account)
860+
await use(account)
861+
await deleteUser(account)
862+
},
863+
})
864+
865+
export {expect} from '@playwright/test'
866+
```
867+
868+
```typescript
869+
//test.spec.ts
870+
//This is an example of how the above fixture would be used in a test
871+
import {expect, test} from './fixtures/baseFixture'
872+
873+
test.describe('when given valid input', () => {
874+
test('should already be signed into the correct account', async ({
875+
signedInAccount, //This is the fixture that contains the account information and is already signed in
876+
app, //This is the fixture that gives access to the entire POM structure
877+
}) => {
878+
await app.home.goToProfilePage()
879+
await app.profile.verifyDisplayName(signedInAccount.display_name)
880+
})
881+
})
882+
883+
//This is how the test would look without the fixture
884+
import {test, expect} from '@playwright/test'
885+
import {App} from './app.ts'
886+
import {testAccounts, UserAccountInformation} from './accountInformation'
887+
import {deleteUser} from './deleteUser'
888+
889+
test.describe('when given valid input', () => {
890+
test('should already be signed into the correct account', async ({page}) => {
891+
const app = new App(page)
892+
const account = testAccounts.faker_account()
893+
894+
await app.auth.signUpWithEmail(account)
895+
await app.home.goToProfilePage()
896+
await app.profile.verifyDisplayName(signedInAccount.display_name)
897+
await deleteUser(account)
898+
})
899+
})
900+
```
901+
770902
### Setting up test data
771903

772904
Since the tests run in parallel (i.e., at the same time) and share the same database and Firebase emulator, it can
773905
create issues where one tests edits or deletes data that another test is using, hence breaking that test.
774906

775907
The standard solution for shared data is **test isolation via unique data per test**. Each test generates its own unique
776-
identifiers so
777-
they never touch each other's data.
908+
identifiers so they never touch each other's data.
778909

779910
**1. Use unique emails/username/IDs per test**
780911

@@ -792,16 +923,8 @@ This way no two tests share the same user, so deletes/reads never conflict.
792923

793924
**2. Cleanup only your own data**
794925

795-
Each test must fully attend to their own (and only their own) garden, by tracking what it created and cleaning up only
796-
that:
797-
798-
```js
799-
afterEach(async () => {
800-
await deleteUser(email, password) // only the one this test created
801-
})
802-
```
803-
804-
Avoid `deleteAllUsers()` or broad wipes in parallel tests — that's what causes race conditions.
926+
Each test must fully attend to their own (and only their own) data, by tracking what it created and cleaning up only
927+
that, correct use of fixtures helps data cleanup.
805928

806929
**3. If you must share fixtures, use read-only shared data**
807930

@@ -825,21 +948,13 @@ This gives each parallel worker its own emulator namespace, so even aggressive c
825948

826949
This is not implemented yet, but it will be very userful as the playwright test suite grows.
827950

828-
**Recommended approach in practice:**
829-
830-
- Unique email/username/ID per test → no sharing, no conflict
831-
- `afterEach` cleans up only own data
832-
- `beforeAll` seeds any read-only shared fixtures once
833-
834-
This eliminates race conditions without needing locks or sequencing.
835-
836951
### Example test
837952

838953
```typescript
839954
import {test, expect} from '@playwright/test'
840955

841-
test.describe('Authentication', () => {
842-
test('should login successfully', async ({page}) => {
956+
test.describe('When given valid input', () => {
957+
test('this should login successfully', async ({page}) => {
843958
await page.goto('/')
844959
await page.getByRole('button', {name: 'Sign In'}).click()
845960
await page.getByLabel('Email').fill('test@example.com')
@@ -868,6 +983,27 @@ For comprehensive troubleshooting guidance beyond testing-specific issues, see
868983
the [Troubleshooting Guide](troubleshooting.md) which covers development environment setup, database and emulator
869984
issues, API problems, and more.
870985

986+
### Supabase emulator not working
987+
988+
There might be compatability issues with the Supabase emulator and your setup this can cause a `Runtime error` on the app pointing to an issue in the `supabase/utils.ts (69: 17)` file, and the Supabase emulator showing a generic `site can't be reached` browser error.
989+
990+
The workaround for this is to use a remote db and the local firebase emulator
991+
992+
Install DBeaver (contact the main maintainer for the postgres db connection info) to view and edit the database
993+
994+
```bash
995+
# Comment out "Object.assign(process.env, supabaseEnv)" in playwright.config.ts
996+
997+
# This launches the Firebase Emulator
998+
yarn emulate
999+
1000+
# This launches the app
1001+
yarn dev
1002+
1003+
# Launch Playwright
1004+
npx playwright test --ui
1005+
```
1006+
8711007
### Port already in use
8721008

8731009
```bash

0 commit comments

Comments
 (0)