test(e2e): add auth, settings, social and organization page coverage

* Added Database checks to the onboarding flow

* Added compatibility page setup
Added more compatibility questions

* Finished up the onboarding flow suite
Added compatibility question tests and verifications
Updated tests to cover Keywords and Headline changes recently made
Updated tests to cover all of the big5 personality traits

* .

* Fix: Merge conflict

* .

* Fix: Added fix for None discriptive error issue #36
Updated signUp.spec.ts to use new fixture
Updated Account information variable names
Deleted "deleteUserFixture.ts" as it was incorporated into the "base.ts" file

* Linting and Prettier

* Minor cleaning

* Organizing helper func

* Added Google account to the Onboarding flow

* .

* Added account cleanup for google accounts

* Started work on Sign-in tests
Updated seedDatabase.ts to throw an error if the user already exists, to also add display names and usernames so they seedUser func acts like a normal basic user
Some organising of the google auth code

* Linting and Prettier

* Added checks to the deleteUser func to check if the accout exists
Added account deletion checks

* Linting and Prettier

* Added POM's for social and organisation page
Updated settings POM

* Formatting update, fixed homePage locator for signin

* .

* .

* .

* Coderabbitai fix's

* Fix

* Improve test utilities and stabilize onboarding flow tests

* Changes requested

* Seperated deletion tests from onboarding

* Update `.coderabbit.yaml` with improved internationalization guidance and formatting adjustments

* Clean up `.vscode/settings.json` and add it to `.gitignore`

* Add Playwright E2E test guidelines to `.coderabbit.yaml`

* Standardize and improve formatting in `TESTING.md` for better readability and consistency.

* Refactor onboarding flow tests and related utilities; improve formatting and remove redundant tests.

---------

Co-authored-by: MartinBraquet <martin.braquet@gmail.com>
This commit is contained in:
Okechi Jones-Williams
2026-04-04 13:21:40 +01:00
committed by GitHub
parent 09736cd49b
commit d2c9d12b39
34 changed files with 1137 additions and 170 deletions

View File

@@ -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
@@ -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
.gitignore vendored
View File

@@ -102,3 +102,5 @@ test-results
**/coverage
*my-release-key.keystore
.vscode/settings.json

View File

@@ -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)
```
@@ -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
@@ -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
@@ -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).

View File

@@ -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()
@@ -54,4 +55,4 @@ class UserAccountInformation {
}
}
export default UserAccountInformation
export default UserAccountInformationForSeeding

View File

@@ -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: ', {
@@ -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])

View File

@@ -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}`,
{
@@ -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}`, {
@@ -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
}
}

View File

@@ -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'
/**
@@ -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()
@@ -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: {},
})
@@ -100,6 +103,7 @@ export async function seedDbUser(userInfo: UserAccountInformation, profileType?:
})
await insert(tx, 'profiles', profileData)
return true
})
}
@@ -107,13 +111,17 @@ 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
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)
}

View File

@@ -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: {

View File

@@ -4,10 +4,14 @@ 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
@@ -15,29 +19,48 @@ export const test = base.extend<{
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())
},
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)
@@ -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'

View File

@@ -1,33 +1,64 @@
import {expect, Page, test as base} from '@playwright/test'
import {seedUser} from '../../utils/seedDatabase'
import {test as base} from '@playwright/test'
import {AuthPage} from '../pages/AuthPage'
import {config} from '../SPEC_CONFIG'
import {HomePage} from '../pages/homePage'
import {testAccounts, UserAccountInformation} from '../utils/accountInformation'
import { OnboardingPage } from '../pages/onboardingPage'
import { SignUpPage } from '../pages/signUpPage'
import { ProfilePage } from '../pages/profilePage'
import { SettingsPage } from '../pages/settingsPage'
export const test = base.extend<{
authenticatedPage: Page
homePage: HomePage
onboardingPage: OnboardingPage
signUpPage: SignUpPage
profilePage: ProfilePage
settingsPage: SettingsPage
authPage: AuthPage
dev_one_account: UserAccountInformation
fakerAccount: UserAccountInformation
googleAccountOne: UserAccountInformation
googleAccountTwo: UserAccountInformation
}>({
authenticatedPage: async ({page}, use) => {
homePage: async ({page}, use) => {
const homePage = new HomePage(page)
await use(homePage)
},
onboardingPage: async ({page}, use) => {
const onboardingPage = new OnboardingPage(page)
await use(onboardingPage)
},
signUpPage: async ({page}, use) => {
const signUpPage = new SignUpPage(page)
await use(signUpPage)
},
profilePage: async ({page}, use) => {
const profilePage = new ProfilePage(page)
await use(profilePage)
},
settingsPage: async ({page}, use) => {
const settingsPage = new SettingsPage(page)
await use(settingsPage)
},
authPage: async ({page}, use) => {
const authPage = new AuthPage(page)
const email = config.USERS.DEV_1.EMAIL
const password = config.USERS.DEV_1.PASSWORD
try {
await seedUser(email, password)
} catch (_e) {
console.log('User already exists for signinFixture', email)
}
await page.goto('/signin')
await authPage.fillEmailField(email)
await authPage.fillPasswordField(password)
await authPage.clickSignInWithEmailButton()
await page.waitForURL(/^(?!.*signin).*$/)
expect(page.url()).not.toContain('/signin')
await use(page)
await use(authPage)
},
dev_one_account: async ({}, use) => {
const account = testAccounts.dev_one_account()
await use(account)
},
fakerAccount: async ({}, use) => {
const account = testAccounts.faker_account()
await use(account)
},
googleAccountOne: async ({}, use) => {
const account = testAccounts.google_account_one()
await use(account)
},
googleAccountTwo: async ({}, use) => {
const account = testAccounts.google_account_two()
await use(account)
},
})
export {expect} from '@playwright/test'

View File

@@ -7,7 +7,7 @@ export class AuthPage {
private readonly emailField: Locator
private readonly passwordField: Locator
private readonly signInWithEmailButton: Locator
private readonly signInWithGoogleButton: Locator
private readonly googleButton: Locator
private readonly signUpWithEmailButton: Locator
constructor(public readonly page: Page) {
@@ -16,7 +16,7 @@ export class AuthPage {
this.emailField = page.getByLabel('Email')
this.passwordField = page.getByLabel('Password')
this.signInWithEmailButton = page.getByRole('button', {name: 'Sign in with Email'})
this.signInWithGoogleButton = page.getByRole('button', {name: 'Google'})
this.googleButton = page.getByRole('button', {name: 'Google'})
this.signUpWithEmailButton = page.getByRole('button', {name: 'Sign up with Email'})
}
@@ -35,9 +35,28 @@ export class AuthPage {
await this.signInWithEmailButton.click()
}
async clickSignInWithGoogleButton() {
await expect(this.signInWithGoogleButton).toBeVisible()
await this.signInWithGoogleButton.click()
async clickGoogleButton() {
await expect(this.googleButton).toBeVisible()
await this.googleButton.click()
}
async getGooglePopupPage(): Promise<Page> {
const [popup] = await Promise.all([
this.page.context().waitForEvent('page'),
this.clickGoogleButton(),
])
await popup.waitForLoadState()
return popup
}
async signInToGoogleAccount(email: string, display_name?: string, username?: string) {
const popup = await this.getGooglePopupPage()
await popup.getByText('Add new account', {exact: true}).click()
await popup.getByLabel('Email').fill(email)
if (display_name) await popup.getByLabel('Display name').fill(display_name)
if (username) await popup.getByLabel('Screen name', {exact: true}).fill(username)
await popup.getByText('Sign in with Google.com', {exact: true}).click()
await popup.waitForEvent('close')
}
async clickSignUpWithEmailButton() {

View File

@@ -2,78 +2,144 @@ import {expect, Locator, Page} from '@playwright/test'
import {LocaleTuple} from 'common/constants'
export class HomePage {
private readonly sidebar: Locator
private readonly homePageLink: Locator
private readonly aboutLink: Locator
private readonly faqLink: Locator
private readonly voteLink: Locator
private readonly eventsLink: Locator
private readonly whatsNewLink: Locator
private readonly socialsLink: Locator
private readonly organizationLink: Locator
private readonly contactLink: Locator
private readonly profileLink: Locator
private readonly signUpButton: Locator
private readonly localePicker: Locator
private readonly signInLink: Locator
private readonly signOutLink: Locator
private readonly closeButton: Locator
constructor(public readonly page: Page) {
this.homePageLink = page.getByText('Compass', {exact: true})
this.aboutLink = page.getByTestId('sidebar-about')
this.faqLink = page.getByTestId('sidebar-faq')
this.voteLink = page.getByTestId('sidebar-vote')
this.eventsLink = page.getByTestId('sidebar-events')
this.whatsNewLink = page.getByTestId('sidebar-news')
this.socialsLink = page.getByTestId('sidebar-social')
this.organizationLink = page.getByTestId('sidebar-organization')
this.contactLink = page.getByTestId('sidebar-contact')
this.sidebar = page.getByTestId('sidebar')
this.homePageLink = page.locator('a[href="/home"]')
this.profileLink = page.getByTestId('sidebar-username')
this.signUpButton = page.locator('button').filter({hasText: 'Sign up'}).first()
this.localePicker = page.getByTestId('sidebar-locale-picker')
this.signInLink = page.getByTestId('sidebar-signin')
this.signInLink = page.locator('a[href="/signin"]').first()
this.signOutLink = page.getByText('Sign out', {exact: true})
this.closeButton = page.getByRole('button', {name: 'Close'})
}
async gotToHomePage() {
await this.page.goto('/')
get sidebarAbout() {
return this.sidebar.getByText('About')
}
get sidebarFaq() {
return this.sidebar.getByText('FAQ')
}
get sidebarVote() {
return this.sidebar.getByText('Vote')
}
get sidebarEvents() {
return this.sidebar.getByText('Events')
}
get sidebarWhatsNew() {
return this.sidebar.getByText("What's new")
}
get sidebarSocials() {
return this.sidebar.getByText('Socials')
}
get sidebarOrganization() {
return this.sidebar.getByText('Organization')
}
get sidebarSettings() {
return this.sidebar.getByText('Settings')
}
get sidebarPeople() {
return this.sidebar.getByText('People')
}
get sidebarNotifs() {
return this.sidebar.getByText('Notifs')
}
get sidebarMessages() {
return this.sidebar.getByText('Messages')
}
get sidebarContact() {
return this.sidebar.getByText('Contact')
}
async goToHomePage() {
await this.page.goto('/home')
}
async goToRegisterPage() {
await this.page.goto('/register')
}
async goToSigninPage() {
await this.page.goto('/signin')
}
async clickAboutLink() {
await expect(this.aboutLink).toBeVisible()
await this.aboutLink.click()
await expect(this.sidebarAbout).toBeVisible()
await this.sidebarAbout.click()
}
async clickFaqLink() {
await expect(this.faqLink).toBeVisible()
await this.faqLink.click()
await expect(this.sidebarFaq).toBeVisible()
await this.sidebarFaq.click()
}
async clickVoteLink() {
await expect(this.voteLink).toBeVisible()
await this.voteLink.click()
await expect(this.sidebarVote).toBeVisible()
await this.sidebarVote.click()
}
async clickEventsLink() {
await expect(this.eventsLink).toBeVisible()
await this.eventsLink.click()
await expect(this.sidebarEvents).toBeVisible()
await this.sidebarEvents.click()
}
async clickWhatsNewLink() {
await expect(this.whatsNewLink).toBeVisible()
await this.whatsNewLink.click()
await expect(this.sidebarWhatsNew).toBeVisible()
await this.sidebarWhatsNew.click()
}
async clickSocialsLink() {
await expect(this.socialsLink).toBeVisible()
await this.socialsLink.click()
await expect(this.sidebarSocials).toBeVisible()
await this.sidebarSocials.click()
}
async clickOrganizationLink() {
await expect(this.organizationLink).toBeVisible()
await this.organizationLink.click()
await expect(this.sidebarOrganization).toBeVisible()
await this.sidebarOrganization.click()
}
async clickContactLink() {
await expect(this.contactLink).toBeVisible()
await this.contactLink.click()
await expect(this.sidebarContact).toBeVisible()
await this.sidebarContact.click()
}
async clickSettingsLink() {
await expect(this.sidebarSettings).toBeVisible()
await this.sidebarSettings.click()
}
async clickPeopleLink() {
await expect(this.sidebarPeople).toBeVisible()
await this.sidebarPeople.click()
}
async clickNotifsLink() {
await expect(this.sidebarNotifs).toBeVisible()
await this.sidebarNotifs.click()
}
async clickMessagesLink() {
await expect(this.sidebarMessages).toBeVisible()
await this.sidebarMessages.click()
}
async clickSignUpButton() {
@@ -88,7 +154,44 @@ export class HomePage {
}
async clickSignInLink() {
await expect(this.sidebar).toBeVisible()
await this.sidebar.getByText('Sign in').click()
}
async verifyHomePageLinks() {
await expect(this.homePageLink).toBeVisible()
await expect(this.sidebarAbout).toBeVisible()
await expect(this.sidebarFaq).toBeVisible()
await expect(this.sidebarVote).toBeVisible()
await expect(this.sidebarEvents).toBeVisible()
await expect(this.sidebarWhatsNew).toBeVisible()
await expect(this.sidebarSocials).toBeVisible()
await expect(this.sidebarOrganization).toBeVisible()
await expect(this.sidebarContact).toBeVisible()
await expect(this.signUpButton).toBeVisible()
await expect(this.signInLink).toBeVisible()
await this.signInLink.click()
await expect(this.localePicker).toBeVisible()
}
async verifySignedInHomePage(displayName: string) {
await expect(this.homePageLink).toBeVisible()
await expect(this.profileLink).toBeVisible()
await expect(this.profileLink).toContainText(displayName)
await expect(this.sidebarPeople).toBeVisible()
await expect(this.sidebarNotifs).toBeVisible()
await expect(this.sidebarMessages).toBeVisible()
await expect(this.sidebarSettings).toBeVisible()
await expect(this.sidebarAbout).toBeVisible()
await expect(this.sidebarFaq).toBeVisible()
await expect(this.sidebarVote).toBeVisible()
await expect(this.sidebarEvents).toBeVisible()
await expect(this.sidebarWhatsNew).toBeVisible()
await expect(this.sidebarSocials).toBeVisible()
await expect(this.sidebarOrganization).toBeVisible()
await expect(this.sidebarContact).toBeVisible()
await expect(this.signOutLink).toBeVisible()
await expect(this.signUpButton).not.toBeVisible()
await expect(this.signInLink).not.toBeVisible()
await expect(this.localePicker).not.toBeVisible()
}
}

View File

@@ -0,0 +1,134 @@
import {expect, Locator, Page} from '@playwright/test'
export class OrganizationPage {
private readonly pageTitle: Locator
private readonly aboutUsHeading: Locator
private readonly proofAndTransparencyHeading: Locator
private readonly contactAndSupportHeading: Locator
private readonly trustAndLegalHeading: Locator
private readonly aboutCompassLink: Locator
private readonly constitutionLink: Locator
private readonly keyMetricsLink: Locator
private readonly pressLink: Locator
private readonly financialTransparencyLink: Locator
private readonly contactUsLink: Locator
private readonly helpAndSupportLink: Locator
private readonly securityLink: Locator
private readonly termsAndConditionsLink: Locator
private readonly privacyPolicyLink: Locator
constructor(public readonly page: Page) {
this.pageTitle = page.getByRole('heading', {name: 'Organization'})
this.aboutUsHeading = page.getByRole('heading', {name: 'About us'})
this.proofAndTransparencyHeading = page.getByRole('heading', {name: 'Proof & transparency'})
this.contactAndSupportHeading = page.getByRole('heading', {name: 'Contact & support'})
this.trustAndLegalHeading = page.getByRole('heading', {name: 'Trust & legal'})
this.aboutCompassLink = page.getByRole('link', {name: 'About Compass'})
this.constitutionLink = page.getByRole('link', {name: 'Our constitution'})
this.keyMetricsLink = page.getByRole('link', {name: 'Key metrics & growth'})
this.pressLink = page.getByRole('link', {name: 'Press'})
this.financialTransparencyLink = page.getByRole('link', {name: 'Financial transparency'})
this.contactUsLink = page.getByRole('link', {name: 'Contact us'})
this.helpAndSupportLink = page.getByRole('link', {name: 'Help & support center'})
this.securityLink = page.getByRole('link', {name: 'Security'})
this.termsAndConditionsLink = page.getByRole('link', {name: 'Terms and conditions'})
this.privacyPolicyLink = page.getByRole('link', {name: 'Privacy policy'})
}
async goToOrganizationPage() {
await this.page.goto('/organization')
}
async verifyOrganizationPage() {
await expect(this.page).toHaveURL(/\/organization$/)
await expect(this.pageTitle).toBeVisible()
await expect(this.aboutUsHeading).toBeVisible()
await expect(this.proofAndTransparencyHeading).toBeVisible()
await expect(this.contactAndSupportHeading).toBeVisible()
await expect(this.trustAndLegalHeading).toBeVisible()
await this.verifyOrganizationLinks()
}
async verifyOrganizationLinks() {
await expect(this.aboutCompassLink).toBeVisible()
await expect(this.aboutCompassLink).toHaveAttribute('href', '/about')
await expect(this.constitutionLink).toBeVisible()
await expect(this.constitutionLink).toHaveAttribute('href', '/constitution')
await expect(this.keyMetricsLink).toBeVisible()
await expect(this.keyMetricsLink).toHaveAttribute('href', '/stats')
await expect(this.pressLink).toBeVisible()
await expect(this.pressLink).toHaveAttribute('href', '/press')
await expect(this.financialTransparencyLink).toBeVisible()
await expect(this.financialTransparencyLink).toHaveAttribute('href', '/financials')
await expect(this.contactUsLink).toBeVisible()
await expect(this.contactUsLink).toHaveAttribute('href', '/contact')
await expect(this.helpAndSupportLink).toBeVisible()
await expect(this.helpAndSupportLink).toHaveAttribute('href', '/help')
await expect(this.securityLink).toBeVisible()
await expect(this.securityLink).toHaveAttribute('href', '/security')
await expect(this.termsAndConditionsLink).toBeVisible()
await expect(this.termsAndConditionsLink).toHaveAttribute('href', '/terms')
await expect(this.privacyPolicyLink).toBeVisible()
await expect(this.privacyPolicyLink).toHaveAttribute('href', '/privacy')
}
async clickAboutCompassLink() {
await expect(this.aboutCompassLink).toBeVisible()
await this.aboutCompassLink.click()
}
async clickConstitutionLink() {
await expect(this.constitutionLink).toBeVisible()
await this.constitutionLink.click()
}
async clickKeyMetricsLink() {
await expect(this.keyMetricsLink).toBeVisible()
await this.keyMetricsLink.click()
}
async clickPressLink() {
await expect(this.pressLink).toBeVisible()
await this.pressLink.click()
}
async clickFinancialTransparencyLink() {
await expect(this.financialTransparencyLink).toBeVisible()
await this.financialTransparencyLink.click()
}
async clickContactUsLink() {
await expect(this.contactUsLink).toBeVisible()
await this.contactUsLink.click()
}
async clickHelpAndSupportLink() {
await expect(this.helpAndSupportLink).toBeVisible()
await this.helpAndSupportLink.click()
}
async clickSecurityLink() {
await expect(this.securityLink).toBeVisible()
await this.securityLink.click()
}
async clickTermsAndConditionsLink() {
await expect(this.termsAndConditionsLink).toBeVisible()
await this.termsAndConditionsLink.click()
}
async clickPrivacyPolicyLink() {
await expect(this.privacyPolicyLink).toBeVisible()
await this.privacyPolicyLink.click()
}
}

View File

@@ -0,0 +1,159 @@
import {expect, Locator, Page} from '@playwright/test'
import {LocaleTuple} from 'common/constants'
import {FontsTuple} from 'web/components/font-picker'
export class SettingsPage {
private readonly localePicker: Locator
private readonly measurementSystemToggle: Locator
private readonly themeToggle: Locator
private readonly fontPicker: Locator
private readonly downloadProfileJSONDataButton: Locator
private readonly manageHiddenProfilesButton: Locator
private readonly hiddenProfilesSection: Locator
private readonly directMessagingPreferenceToggle: Locator
private readonly privateInterestSignalsToggle: Locator
private readonly sendVerificationEmailButton: Locator
private readonly verifiedEmailLink: Locator
private readonly changeEmailButton: Locator
private readonly sendPasswordResetButton: Locator
private readonly deleteAccountButton: Locator
private readonly closeButton: Locator
private readonly cancelButton: Locator
private readonly deleteSurveyModal: Locator
private readonly deleteSurveyReasons: Locator
private readonly deleteSurveyDetails: Locator
constructor(public readonly page: Page) {
this.localePicker = page.getByTestId('sidebar-locale-picker')
this.measurementSystemToggle = page.getByTestId('measurement-system-toggle')
this.themeToggle = page.getByTestId('settings-dark-light-toggle')
this.fontPicker = page.getByTestId('settings-font-picker')
this.downloadProfileJSONDataButton = page.getByRole('button', {
name: 'Download all my data (JSON)',
})
this.manageHiddenProfilesButton = page.getByRole('button', {name: 'Manage hidden profiles'})
this.hiddenProfilesSection = page.getByTestId('hidden-profiles')
this.directMessagingPreferenceToggle = page.getByTestId('settings-direct-message-toggle')
this.privateInterestSignalsToggle = page.getByTestId('settings-private-interest-signal-toggle')
this.sendVerificationEmailButton = page.getByRole('button', {name: 'Send verification email'})
this.verifiedEmailLink = page.getByRole('button', {name: 'I verified my email'}) // Need method for this
this.changeEmailButton = page.getByRole('button', {name: 'Change email address'})
this.sendPasswordResetButton = page.getByRole('button', {name: 'Send password reset email'})
this.deleteAccountButton = page.getByRole('button', {name: 'Delete account'})
this.closeButton = page.getByRole('button', {name: 'Close'})
this.cancelButton = page.getByRole('button', {name: 'Cancel'})
this.deleteSurveyModal = page.getByTestId('delete-survey-modal')
this.deleteSurveyReasons = page.getByTestId('delete-account-survey-reasons')
this.deleteSurveyDetails = page.getByRole('textbox')
}
async setLocale(locale: LocaleTuple) {
if (!locale) return
await expect(this.localePicker).toBeVisible()
await this.localePicker.selectOption(locale[0])
}
async toggleMeasurementSystem() {
await expect(this.measurementSystemToggle).toBeVisible()
await this.measurementSystemToggle.click()
}
async toggleDisplayTheme() {
await expect(this.themeToggle).toBeVisible()
await this.themeToggle.click()
}
async setFont(font: FontsTuple) {
if (!font) return
await expect(this.fontPicker).toBeVisible()
await this.fontPicker.selectOption(font[0])
}
async clickdownloadProfileDataButton() {
await expect(this.downloadProfileJSONDataButton).toBeVisible()
await this.downloadProfileJSONDataButton.click()
}
async clickManageHiddenProfilesButton() {
await expect(this.manageHiddenProfilesButton).toBeVisible()
await this.manageHiddenProfilesButton.click()
}
async clickCancelButton() {
await expect(this.cancelButton).toBeVisible()
await this.cancelButton.click()
}
async verifyHiddenProfiles(profiles: string[]) {
await expect(this.hiddenProfilesSection).toBeVisible()
for (let i = 0; i < profiles.length; i++) {
try {
await expect(
this.hiddenProfilesSection.getByRole('link', {name: `${profiles[i]}`}),
).toBeVisible({timeout: 2000})
} catch (error) {
throw new Error(`Profile ${profiles[i]} has not been hidden`)
}
}
}
async unhideProfiles(profile: string) {
await expect(this.hiddenProfilesSection).toBeVisible()
const hiddenProfiles = await this.hiddenProfilesSection.count()
let matchIndex = -1
for (let i = 0; i < hiddenProfiles; i++) {
const target = await this.hiddenProfilesSection.getByRole('link', {name: `${profile}`})
if (target) {
matchIndex = i
}
}
await this.hiddenProfilesSection
.locator('div')
.nth(matchIndex)
.getByRole('button', {name: 'Unhide'})
.click()
}
async toggleDirectMessagingPreferences() {
await expect(this.directMessagingPreferenceToggle).toBeVisible()
await this.directMessagingPreferenceToggle.click()
}
async togglePrivateInterestSignalsPreferences() {
await expect(this.privateInterestSignalsToggle).toBeVisible()
await this.privateInterestSignalsToggle.click()
}
async clickSendVerificationEmailButton() {
await expect(this.sendVerificationEmailButton).toBeVisible()
await this.sendVerificationEmailButton.click()
}
async clickChangeEmailAddressButton() {
await expect(this.changeEmailButton).toBeVisible()
await this.changeEmailButton.click()
}
async clickSendPasswordResetEmailButton() {
await expect(this.sendPasswordResetButton).toBeVisible()
await this.sendPasswordResetButton.click()
}
async clickDeleteAccountButton() {
await expect(this.deleteAccountButton).toBeVisible()
await this.deleteAccountButton.click()
}
async clickCloseButton() {
await expect(this.closeButton).toBeVisible()
await this.closeButton.click()
}
async fillDeleteAccountSurvey(reason: string) {
await expect(this.deleteSurveyModal).toBeVisible()
await expect(this.deleteSurveyReasons).toBeVisible()
await this.deleteSurveyReasons.locator('div').nth(1).click()
await expect(this.deleteSurveyDetails).toBeVisible()
await this.deleteSurveyDetails.fill(reason)
}
}

View File

@@ -0,0 +1,113 @@
import {expect, Locator, Page} from '@playwright/test'
import {
discordLink,
githubRepo,
instagramLink,
redditLink,
stoatLink,
supportEmail,
xLink,
} from 'common/constants'
export class SocialPage {
private readonly pageTitle: Locator
private readonly communityHeading: Locator
private readonly followAndUpdatesHeading: Locator
private readonly developmentHeading: Locator
private readonly contactHeading: Locator
private readonly discordButton: Locator
private readonly redditButton: Locator
private readonly stoatButton: Locator
private readonly xButton: Locator
private readonly instagramButton: Locator
private readonly githubButton: Locator
private readonly emailButton: Locator
constructor(public readonly page: Page) {
this.pageTitle = page.getByRole('heading', {name: 'Socials'})
this.communityHeading = page.getByRole('heading', {name: 'Community'})
this.followAndUpdatesHeading = page.getByRole('heading', {name: 'Follow & Updates'})
this.developmentHeading = page.getByRole('heading', {name: 'Development'})
this.contactHeading = page.getByRole('heading', {name: 'Contact'})
this.discordButton = page.getByRole('link', {name: 'Discord'})
this.redditButton = page.getByRole('link', {name: 'Reddit'})
this.stoatButton = page.getByRole('link', {name: 'Revolt / Stoat'})
this.xButton = page.getByRole('link', {name: 'X'})
this.instagramButton = page.getByRole('link', {name: 'Instagram'})
this.githubButton = page.getByRole('link', {name: 'GitHub'})
this.emailButton = page.getByRole('link', {name: `Email ${supportEmail}`})
}
async goToSocialPage() {
await this.page.goto('/social')
}
async verifySocialPage() {
await expect(this.page).toHaveURL(/\/social$/)
await expect(this.pageTitle).toBeVisible()
await expect(this.communityHeading).toBeVisible()
await expect(this.followAndUpdatesHeading).toBeVisible()
await expect(this.developmentHeading).toBeVisible()
await expect(this.contactHeading).toBeVisible()
await this.verifySocialLinks()
}
async verifySocialLinks() {
await expect(this.discordButton).toBeVisible()
await expect(this.discordButton).toHaveAttribute('href', discordLink)
await expect(this.redditButton).toBeVisible()
await expect(this.redditButton).toHaveAttribute('href', redditLink)
await expect(this.stoatButton).toBeVisible()
await expect(this.stoatButton).toHaveAttribute('href', stoatLink)
await expect(this.xButton).toBeVisible()
await expect(this.xButton).toHaveAttribute('href', xLink)
await expect(this.instagramButton).toBeVisible()
await expect(this.instagramButton).toHaveAttribute('href', instagramLink)
await expect(this.githubButton).toBeVisible()
await expect(this.githubButton).toHaveAttribute('href', githubRepo)
await expect(this.emailButton).toBeVisible()
await expect(this.emailButton).toHaveAttribute('href', `mailto:${supportEmail}`)
}
async clickDiscordButton() {
await expect(this.discordButton).toBeVisible()
await this.discordButton.click()
}
async clickRedditButton() {
await expect(this.redditButton).toBeVisible()
await this.redditButton.click()
}
async clickStoatButton() {
await expect(this.stoatButton).toBeVisible()
await this.stoatButton.click()
}
async clickXButton() {
await expect(this.xButton).toBeVisible()
await this.xButton.click()
}
async clickInstagramButton() {
await expect(this.instagramButton).toBeVisible()
await this.instagramButton.click()
}
async clickGitHubButton() {
await expect(this.githubButton).toBeVisible()
await this.githubButton.click()
}
async clickEmailButton() {
await expect(this.emailButton).toBeVisible()
await this.emailButton.click()
}
}

View File

@@ -1,9 +1,9 @@
import {userInformationFromDb} from '../../utils/databaseUtils'
import {progressToRequiredForm} from '../utils/testCleanupHelpers'
import {expect, test} from '../fixtures/base'
import {registerWithEmail, skipOnboardingHeadToProfile} from '../utils/testCleanupHelpers'
test.describe('when given valid input', () => {
test('should successfully complete the onboarding flow', async ({
test('should successfully complete the onboarding flow with email', async ({
homePage,
onboardingPage,
signUpPage,
@@ -12,13 +12,9 @@ test.describe('when given valid input', () => {
onboardingAccount,
}) => {
console.log(
`Starting "should successfully complete the onboarding flow" with ${onboardingAccount.username}`,
`Starting "should successfully complete the onboarding flow with email" with ${onboardingAccount.username}`,
)
await homePage.gotToHomePage()
await homePage.clickSignUpButton()
await authPage.fillEmailField(onboardingAccount.email)
await authPage.fillPasswordField(onboardingAccount.password)
await authPage.clickSignUpWithEmailButton()
await registerWithEmail(homePage, authPage, onboardingAccount)
await onboardingPage.clickContinueButton() //First continue
await onboardingPage.clickContinueButton() //Second continue
await onboardingPage.clickGetStartedButton()
@@ -225,6 +221,38 @@ test.describe('when given valid input', () => {
)
})
test('should successfully complete the onboarding flow with google account', async ({
homePage,
onboardingPage,
signUpPage,
authPage,
profilePage,
googleAccountOne,
headless,
}) => {
console.log(
`Starting "should successfully complete the onboarding flow with google account" with ${googleAccountOne.username}`,
)
test.skip(headless, 'Google popup auth test requires headed mode')
await homePage.goToRegisterPage()
await authPage.fillPasswordField('') //The test only passes when this is added...something is weird here
await authPage.signInToGoogleAccount(
googleAccountOne.email,
googleAccountOne.display_name,
googleAccountOne.username,
)
await skipOnboardingHeadToProfile(onboardingPage, signUpPage, profilePage, googleAccountOne)
//Verify displayed information is correct
await profilePage.verifyDisplayName(googleAccountOne.display_name)
//Verify database info
const dbInfo = await userInformationFromDb(googleAccountOne)
await expect(dbInfo.user.name).toContain(googleAccountOne.display_name)
await expect(dbInfo.user.username).toContain(googleAccountOne.username)
})
test('should successfully skip the onboarding flow', async ({
homePage,
onboardingPage,
@@ -236,13 +264,8 @@ test.describe('when given valid input', () => {
console.log(
`Starting "should successfully skip the onboarding flow" with ${fakerAccount.username}`,
)
await progressToRequiredForm(homePage, authPage, fakerAccount, onboardingPage)
await signUpPage.fillDisplayName(fakerAccount.display_name)
await signUpPage.fillUsername(fakerAccount.username)
await signUpPage.clickNextButton()
await signUpPage.clickNextButton() //Skip optional information
await profilePage.clickCloseButton()
await onboardingPage.clickRefineProfileButton()
await registerWithEmail(homePage, authPage, fakerAccount)
await skipOnboardingHeadToProfile(onboardingPage, signUpPage, profilePage, fakerAccount)
//Verify displayed information is correct
await profilePage.verifyDisplayName(fakerAccount.display_name)
@@ -265,13 +288,8 @@ test.describe('when given valid input', () => {
console.log(
`Starting "should successfully enter optional information after completing flow" with ${fakerAccount.username}`,
)
await progressToRequiredForm(homePage, authPage, fakerAccount, onboardingPage)
await signUpPage.fillDisplayName(fakerAccount.display_name)
await signUpPage.fillUsername(fakerAccount.username)
await signUpPage.clickNextButton()
await signUpPage.clickNextButton() //Skip optional information
await profilePage.clickCloseButton()
await onboardingPage.clickRefineProfileButton()
await registerWithEmail(homePage, authPage, fakerAccount)
await skipOnboardingHeadToProfile(onboardingPage, signUpPage, profilePage, fakerAccount)
await profilePage.clickEditProfileButton()
await signUpPage.chooseGender(fakerAccount.gender)
await signUpPage.fillAge(fakerAccount.age)
@@ -313,7 +331,8 @@ test.describe('when given valid input', () => {
console.log(
`Starting "should successfully use the start answering option" with ${fakerAccount.username}`,
)
await progressToRequiredForm(homePage, authPage, fakerAccount, onboardingPage)
await registerWithEmail(homePage, authPage, fakerAccount)
await onboardingPage.clickSkipOnboardingButton()
await signUpPage.fillDisplayName(fakerAccount.display_name)
await signUpPage.fillUsername(fakerAccount.username)
await signUpPage.clickNextButton()
@@ -338,22 +357,16 @@ test.describe('when given valid input', () => {
})
test.describe('should successfully complete the onboarding flow after using the back button', () => {
test.beforeEach(async ({homePage, authPage, fakerAccount}) => {
console.log(`Before each with ${fakerAccount.username}`)
await homePage.gotToHomePage()
await homePage.clickSignUpButton()
await authPage.fillEmailField(fakerAccount.email)
await authPage.fillPasswordField(fakerAccount.password)
await authPage.clickSignUpWithEmailButton()
})
test("the first time it's an option", async ({
homePage,
authPage,
onboardingPage,
signUpPage,
profilePage,
fakerAccount,
}) => {
console.log(`Starting "the first time its an option" with ${fakerAccount.username}`)
await registerWithEmail(homePage, authPage, fakerAccount)
await onboardingPage.clickContinueButton()
await onboardingPage.clickBackButton()
await onboardingPage.clickContinueButton()
@@ -377,12 +390,15 @@ test.describe('when given valid input', () => {
})
test("the second time it's an option", async ({
homePage,
authPage,
onboardingPage,
signUpPage,
profilePage,
fakerAccount,
}) => {
console.log(`Starting "the second time its an option" with ${fakerAccount.username}`)
await registerWithEmail(homePage, authPage, fakerAccount)
await onboardingPage.clickContinueButton()
await onboardingPage.clickContinueButton()
await onboardingPage.clickBackButton()
@@ -407,6 +423,6 @@ test.describe('when given valid input', () => {
})
})
test.describe('when an error occurs', () => {
test('placeholder', async () => {})
})
// test.describe('when an error occurs', () => {
// test('placeholder', async ({}) => {})
// })

View File

@@ -1,9 +0,0 @@
import {expect} from '@playwright/test'
import {test} from '../fixtures/signInFixture'
test('should be logged in and see settings page', async ({authenticatedPage}) => {
await authenticatedPage.goto('/settings')
await expect(authenticatedPage.getByRole('heading', {name: 'Theme'})).toBeVisible()
})

View File

@@ -0,0 +1,120 @@
import { userInformationFromDb } from '../../utils/databaseUtils'
import {seedUser} from '../../utils/seedDatabase'
import {expect, test} from '../fixtures/signInFixture'
import {testAccounts} from '../utils/accountInformation'
import {
deleteProfileFromSettings,
registerWithEmail,
signinWithEmail,
skipOnboardingHeadToProfile
} from '../utils/testCleanupHelpers'
//Seed the account
test.beforeAll(async () => {
const dev_1_Account = testAccounts.dev_one_account()
try {
await seedUser(
dev_1_Account.email,
dev_1_Account.password,
undefined,
dev_1_Account.display_name,
dev_1_Account.username,
)
} catch (_e) {
console.log('User already exists for signinFixture', dev_1_Account.email)
}
})
test.describe('when given valid input', () => {
test('should be able to sign in to an available account', async ({
homePage,
authPage,
dev_one_account,
}) => {
console.log(
`Starting "should be able to sign in to an available account" with ${dev_one_account.username}`,
)
await signinWithEmail(homePage, authPage, dev_one_account)
await homePage.goToHomePage()
await homePage.verifySignedInHomePage(dev_one_account.display_name)
})
test('should successfully delete an account created via email and password', async ({
homePage,
onboardingPage,
signUpPage,
authPage,
profilePage,
settingsPage,
fakerAccount,
}) => {
console.log(
`Starting "should successfully delete an account created via email and password" with ${fakerAccount.username}`,
)
await registerWithEmail(homePage, authPage, fakerAccount)
await skipOnboardingHeadToProfile(onboardingPage, signUpPage, profilePage, fakerAccount)
//Verify displayed information is correct
await profilePage.verifyDisplayName(fakerAccount.display_name)
//Verify database info
const dbInfo = await userInformationFromDb(fakerAccount)
await expect(dbInfo.user.name).toContain(fakerAccount.display_name)
await expect(dbInfo.user.username).toContain(fakerAccount.username)
await deleteProfileFromSettings(homePage, settingsPage)
})
test('should successfully delete an account created via google auth', async ({
homePage,
onboardingPage,
signUpPage,
authPage,
profilePage,
settingsPage,
googleAccountTwo,
headless,
}) => {
console.log(
`Starting "should successfully delete an account created via google auth" with ${googleAccountTwo.username}`,
)
test.skip(headless, 'Google popup auth test requires headed mode')
await homePage.goToRegisterPage()
await authPage.fillPasswordField('') //The test only passes when this is added...something is weird here
await authPage.signInToGoogleAccount(
googleAccountTwo.email,
googleAccountTwo.display_name,
googleAccountTwo.username,
)
await skipOnboardingHeadToProfile(onboardingPage, signUpPage, profilePage, googleAccountTwo)
//Verify displayed information is correct
await profilePage.verifyDisplayName(googleAccountTwo.display_name)
//Verify database info
const dbInfo = await userInformationFromDb(googleAccountTwo)
await expect(dbInfo.user.name).toContain(googleAccountTwo.display_name)
await expect(dbInfo.user.username).toContain(googleAccountTwo.username)
await deleteProfileFromSettings(homePage, settingsPage)
})
})
test.describe('when given invalid input', () => {
test('should not be able to sign in to an available account', async ({
homePage,
authPage,
dev_one_account,
page,
}) => {
console.log(
`Starting "should not be able to sign in to an available account" with ${dev_one_account.username}`,
)
await signinWithEmail(homePage, authPage, dev_one_account.email, 'ThisPassword')
await expect(
page.getByText('Failed to sign in with your email and password', {exact: true}),
).toBeVisible()
})
})

View File

@@ -1,5 +1,5 @@
import {expect, test} from '../fixtures/base'
import {progressToRequiredForm} from '../utils/testCleanupHelpers'
import {registerWithEmail} from '../utils/testCleanupHelpers'
test.describe('when given valid input', () => {
test('placeholder', async () => {})
@@ -13,7 +13,11 @@ test.describe('when an error occurs', () => {
onboardingPage,
signUpPage,
}) => {
await progressToRequiredForm(homePage, authPage, specAccount, onboardingPage)
console.log(
`Starting "should disable the button "Next" when the display name field is empty" with ${specAccount.username}`,
)
await registerWithEmail(homePage, authPage, specAccount)
await onboardingPage.clickSkipOnboardingButton()
await signUpPage.fillDisplayName('')
await signUpPage.fillUsername(specAccount.username)
await signUpPage.verifyDisplayNameError()
@@ -27,7 +31,11 @@ test.describe('when an error occurs', () => {
onboardingPage,
signUpPage,
}) => {
await progressToRequiredForm(homePage, authPage, specAccount, onboardingPage)
console.log(
`Starting "should disable the button "Next" when the username field is empty" with ${specAccount.username}`,
)
await registerWithEmail(homePage, authPage, specAccount)
await onboardingPage.clickSkipOnboardingButton()
await signUpPage.fillDisplayName(specAccount.display_name)
await signUpPage.fillUsername('')
await signUpPage.verifyUsernameError()

View File

@@ -102,7 +102,9 @@ type AccountConfig = {
spec_account: () => UserAccountInformation
dev_one_account: () => UserAccountInformation
dev_two_account: () => UserAccountInformation
account_all_info: () => UserAccountInformation
google_account_one: () => UserAccountInformation
google_account_two: () => UserAccountInformation
email_account_all_info: () => UserAccountInformation
}
export const testAccounts: AccountConfig = {
@@ -137,10 +139,10 @@ export const testAccounts: AccountConfig = {
dev_one_account: () => {
const id = crypto.randomUUID().slice(0, 6)
return {
email: `dev_1_${id}@compass.com`,
email: `dev_1@compass.com`,
password: 'dev_1Password',
display_name: 'Dev1.Compass',
username: `Dev1.Connections_${id}`,
username: `Dev1.Connections`,
}
},
@@ -150,11 +152,31 @@ export const testAccounts: AccountConfig = {
email: 'dev_2@compass.com',
password: 'dev_2Password',
display_name: 'Dev2.Compass',
username: `Dev2.Connections_${id}`,
username: `Dev2.Connections_`,
}
},
account_all_info: () => {
google_account_one: () => {
const id = crypto.randomUUID().slice(0, 6)
return {
email: `g1_compass_${id}@gmail.com`,
password: 'G_oneCompassTest',
display_name: 'Google_one_Compass',
username: `G1_Connect_${id}`,
}
},
google_account_two: () => {
const id = crypto.randomUUID().slice(0, 6)
return {
email: `g2_compass_${id}@gmail.com`,
password: 'G_twoCompassTest',
display_name: 'Google_two_Compass',
username: `G2_Connect_${id}`,
}
},
email_account_all_info: () => {
const id = crypto.randomUUID().slice(0, 6)
return {
// Use a non-real TLD like @test.compass to make it obvious these are test accounts and prevent accidental emails

View File

@@ -1,18 +1,31 @@
import {deleteFromDb} from '../../utils/databaseUtils'
import {deleteAccount, firebaseLogin} from '../../utils/firebaseUtils'
import {deleteAccount, firebaseLoginEmailPassword} from '../../utils/firebaseUtils'
import {UserAccountInformation} from './accountInformation'
import {AuthObject} from './networkUtils'
export async function deleteUser(email: string, password: string) {
type AuthType = 'Email/Password' | 'Google'
export async function deleteUser(
authType: AuthType,
account?: UserAccountInformation,
authInfo?: AuthObject,
) {
try {
const loginInfo = await firebaseLogin(email, password)
await deleteAccount(loginInfo)
await deleteFromDb(loginInfo.data.localId)
let loginInfo
if (authType === 'Email/Password') {
loginInfo = await firebaseLoginEmailPassword(account?.email, account?.password)
await deleteAccount(loginInfo?.data.idToken)
await deleteFromDb(loginInfo?.data.localId)
} else if (authType === 'Google' && authInfo) {
await deleteAccount(authInfo.idToken)
await deleteFromDb(authInfo.localId)
}
} catch (err: any) {
// Skip deletion if user doesn't exist or other auth errors occur
if (
err.response?.status === 400 ||
err.response?.data?.error?.message?.includes('EMAIL_NOT_FOUND')
) {
console.log(`Email not found, skipping user deletion for ${email}`)
console.log(`Email not found, skipping user deletion for ${account?.email}`)
return
}
console.log(err)

View File

@@ -0,0 +1,25 @@
import {Page} from '@playwright/test'
export type AuthObject = {
idToken: string
localId: string
}
export async function getAuthAccountInfo(page: Page): Promise<() => AuthObject> {
let accountIdTokenAndLocalId: AuthObject | undefined
await page.route('**/accounts:signInWithIdp**', async (route) => {
const response = await route.fetch()
const body = await response.json()
accountIdTokenAndLocalId = {idToken: body.idToken, localId: body.localId}
await route.fulfill({response})
})
return () => {
if (!accountIdTokenAndLocalId) {
console.log('Sign-in was never intercepted — test may have been skipped')
return undefined as unknown as AuthObject
}
return accountIdTokenAndLocalId
}
}

View File

@@ -1,18 +1,64 @@
import {AuthPage} from '../pages/AuthPage'
import {HomePage} from '../pages/homePage'
import {OnboardingPage} from '../pages/onboardingPage'
import { OnboardingPage } from '../pages/onboardingPage'
import { ProfilePage } from '../pages/profilePage'
import { SettingsPage } from '../pages/settingsPage'
import { SignUpPage } from '../pages/signUpPage'
import {UserAccountInformation} from '../utils/accountInformation'
export async function progressToRequiredForm(
export async function registerWithEmail(
homePage: HomePage,
authPage: AuthPage,
account: UserAccountInformation,
onboardingPage: OnboardingPage,
) {
await homePage.gotToHomePage()
await homePage.clickSignUpButton()
await homePage.goToRegisterPage()
await authPage.fillEmailField(account.email)
await authPage.fillPasswordField(account.password)
await authPage.clickSignUpWithEmailButton()
await onboardingPage.clickSkipOnboardingButton()
}
export async function signinWithEmail(
homePage: HomePage,
authPage: AuthPage,
accountOrEmail: UserAccountInformation | string,
password?: string,
) {
const email = typeof accountOrEmail === 'string' ? accountOrEmail : accountOrEmail.email
const resolvedPassword = typeof accountOrEmail === 'string' ? password : accountOrEmail.password
if (!email || !resolvedPassword) {
throw new Error('Provide either an `account` or `email` and `password`.')
}
await homePage.goToSigninPage()
await authPage.fillEmailField(email)
await authPage.fillPasswordField(resolvedPassword)
await authPage.clickSignInWithEmailButton()
}
export async function skipOnboardingHeadToProfile(
onboardingPage: OnboardingPage,
signUpPage: SignUpPage,
profilePage: ProfilePage,
account: UserAccountInformation,
) {
await onboardingPage.clickSkipOnboardingButton()
await signUpPage.fillDisplayName(account.display_name)
await signUpPage.fillUsername(account.username)
await signUpPage.clickNextButton()
await signUpPage.clickNextButton()
await profilePage.clickCloseButton()
await onboardingPage.clickRefineProfileButton()
}
export async function deleteProfileFromSettings(
homePage: HomePage,
settingsPage: SettingsPage,
) {
await homePage.clickSettingsLink()
await settingsPage.clickDeleteAccountButton()
await settingsPage.fillDeleteAccountSurvey('Delete me')
await settingsPage.clickDeleteAccountButton()
await homePage.verifyHomePageLinks()
}

View File

@@ -67,6 +67,7 @@ export function ConnectionPreferencesSettings() {
</div>
</div>
<SwitchSetting
testId="settings-direct-message-toggle"
checked={allowDirectMessaging}
onChange={handleDirectMessagingChange}
disabled={isUpdating}
@@ -87,6 +88,7 @@ export function ConnectionPreferencesSettings() {
</div>
</div>
<SwitchSetting
testId="settings-private-interest-signal-toggle"
checked={allowInterestIndicating}
onChange={handleInterestIndicatingChange}
disabled={isUpdating}

View File

@@ -19,6 +19,7 @@ export function FontPicker(props: {className?: string} = {}) {
return (
<select
data-testid="settings-font-picker"
id="font-picker"
value={font}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => setFont(e.target.value as FontOption)}
@@ -35,3 +36,8 @@ export function FontPicker(props: {className?: string} = {}) {
</select>
)
}
//Exported types for test files to use when referencing the keys of the choices objects
export type FontsTuple = {
[K in keyof typeof EN_TRANSLATIONS]: [K, (typeof EN_TRANSLATIONS)[K]]
}[keyof typeof EN_TRANSLATIONS]

View File

@@ -18,6 +18,7 @@ export default function MeasurementSystemToggle(props: {className?: string}) {
</span>
<Switch
data-testid="measurement-system-toggle"
checked={isEnabled}
onChange={(enabled: boolean) => setMeasurementSystem(enabled ? 'metric' : 'imperial')}
className={clsx(

View File

@@ -22,7 +22,7 @@ export function ProfileSummary(props: {user: User; className?: string}) {
<div className="w-2 shrink" />
<Avatar avatarUrl={profile?.pinned_url ?? ''} username={user.username} noLink />
<div className="mr-1 w-2 shrink-[2]" />
<div className="shrink-0 grow">
<div className="shrink-0 grow" data-testid="sidebar-username">
<div className="group-hover:text-primary-700">{user.name}</div>
</div>
<div className="w-2 shrink" />

View File

@@ -54,7 +54,6 @@ export function SidebarItem(props: {item: Item; currentPage?: string}) {
return (
<Link
href={item.href}
data-testid={`sidebar-${item.href?.replace('/', '')}`}
aria-current={isCurrentPage ? 'page' : undefined}
onClick={onClick}
className={sidebarClass}

View File

@@ -41,6 +41,7 @@ export default function Sidebar(props: {
<nav
id="main-navigation"
aria-label="Sidebar"
data-testid="sidebar"
className={clsx(
'flex flex-col h-[calc(100dvh-var(--hloss))] mb-[calc(var(--bnh))] mt-[calc(var(--tnh))]',
className,
@@ -119,7 +120,6 @@ export const SignUpButton = (props: {
return (
<Button
data-testid="side-bar-sign-up-button"
color={color ?? 'gradient'}
size={size ?? 'xl'}
onClick={startSignup}

View File

@@ -141,7 +141,7 @@ export function DeleteAccountSurveyModal() {
onSubmitWithSuccess={handleDeleteAccount}
disabled={false}
>
<Col className="gap-4">
<Col className="gap-4" data-testid="delete-survey-modal">
<Title>{t('delete_survey.title', 'Sorry to see you go')}</Title>
<div>
@@ -157,7 +157,7 @@ export function DeleteAccountSurveyModal() {
{t('delete_survey.reason_label', 'Why are you deleting your account?')}
</RadioGroup.Label>
<div className="space-y-2 mt-2">
<div className="space-y-2 mt-2" data-testid="delete-account-survey-reasons">
{Object.entries(reasonsMap).map(([key, value]) => (
<RadioGroup.Option
key={key}

View File

@@ -49,7 +49,10 @@ export function HiddenProfilesModal(props: {open: boolean; setOpen: (open: boole
{t('settings.hidden_profiles.title', "Profiles you've hidden")}
</Title>
{hiddenProfiles && hiddenProfiles.length > 0 && (
<Col className={clsx('divide-y divide-canvas-300 w-full pr-4', SCROLLABLE_MODAL_CLASS)}>
<Col
className={clsx('divide-y divide-canvas-300 w-full pr-4', SCROLLABLE_MODAL_CLASS)}
data-testid="hidden-profiles"
>
{hiddenProfiles.map((u) => (
<Row key={u.id} className="items-center justify-between py-2 gap-2">
<Link className="w-full rounded-md hover:bg-canvas-100 p-2" href={'/' + u.username}>

View File

@@ -9,10 +9,11 @@ export const SwitchSetting = (props: {
label?: 'Web' | 'Email' | 'Mobile'
disabled: boolean
colorMode?: ToggleColorMode
testId?: string
}) => {
const {colorMode, checked, onChange, label, disabled} = props
const {colorMode, checked, onChange, label, disabled, testId} = props
return (
<Switch.Group as="div" className="flex items-center gap-3">
<Switch.Group as="div" className="flex items-center gap-3" data-testid={testId}>
<ShortToggle colorMode={colorMode} on={checked} setOn={onChange} disabled={disabled} />
<Switch.Label
className={clsx(

View File

@@ -31,7 +31,7 @@ export default function ThemeIcon(props: {className?: string}) {
</>
)
return (
<button onClick={toggleTheme} className={'w-fit'}>
<button onClick={toggleTheme} className={'w-fit'} data-testid="settings-dark-light-toggle">
<Row className="items-center gap-1 border-2 border-gray-500 rounded-full p-1 w-fit mx-2 px-3 hover:bg-canvas-100">
{icon}
{children}

View File

@@ -10,8 +10,9 @@ export default function ShortToggle(props: {
className?: string
colorMode?: ToggleColorMode
size?: 'sm'
testId?: string
}) {
const {on, size, setOn, disabled, className, colorMode = 'primary'} = props
const {on, size, setOn, disabled, className, colorMode = 'primary', testId} = props
const toggleBaseClasses =
'group relative inline-flex flex-shrink-0 rounded-full border-2 border-transparent ring-offset-2 transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2'
@@ -34,6 +35,7 @@ export default function ShortToggle(props: {
return (
<Switch
data-testid={testId}
disabled={disabled}
checked={on}
onChange={setOn}