Skip to content

Commit 31e05aa

Browse files
committed
Merge remote-tracking branch 'origin/main'
2 parents cea45eb + 549eba0 commit 31e05aa

26 files changed

Lines changed: 707 additions & 66 deletions

common/src/choices.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ export const MBTI_CHOICES = {
213213
ESTP: 'estp',
214214
ESFJ: 'esfj',
215215
ESFP: 'esfp',
216-
}
216+
} as const
217217

218218
// MBTI type name mapping
219219
export const MBTI_TYPE_NAMES: Record<string, string> = {

tests/e2e/utils/contextManager.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import {Browser} from '@playwright/test'
2+
3+
import {App} from '../web/pages/app'
4+
5+
export class ContextManager {
6+
private contexts: Map<string, App> = new Map()
7+
8+
constructor(private browser: Browser) {}
9+
10+
async createContext(customName?: string): Promise<App> {
11+
const name = customName ?? crypto.randomUUID().slice(0, 6)
12+
const existing = this.contexts.get(name)
13+
// Return the existing one instead of closing it?
14+
if (existing) await existing.page.context().close()
15+
16+
const context = await this.browser.newContext()
17+
const page = await context.newPage()
18+
const app = new App(page)
19+
this.contexts.set(name, app)
20+
return app
21+
}
22+
23+
getContext(name: string): App | undefined {
24+
return this.contexts.get(name)
25+
}
26+
27+
async closeAll(): Promise<void> {
28+
for (const app of this.contexts.values()) {
29+
await app.page.context().close()
30+
}
31+
this.contexts.clear()
32+
}
33+
}

tests/e2e/utils/firebaseUtils.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,36 @@ export async function findUser(idToken: string) {
3737
}
3838
}
3939

40+
export async function sendVerificationEmail(idToken: string) {
41+
await axios.post(`${config.FIREBASE_URL.BASE}${config.FIREBASE_URL.SEND_EMAIL_VERIFICATION}`, {
42+
requestType: 'VERIFY_EMAIL',
43+
idToken,
44+
})
45+
}
46+
export async function getOobCode(oobCodes: any[], email: string) {
47+
return oobCodes.find((item) => item.email.toLowerCase() === email.toLowerCase())?.oobCode
48+
}
49+
50+
export async function verifyEmail(email: string, password: string) {
51+
try {
52+
const loginInfo = await firebaseLoginEmailPassword(email, password)
53+
await sendVerificationEmail(loginInfo.data.idToken)
54+
const oobResponse = await axios.get(`${config.FIREBASE_URL.FIREBASE_EMULATOR_API}`)
55+
const oobCode = await getOobCode(oobResponse.data.oobCodes, email)
56+
if (!oobCode) throw new Error(`No verification OOB code found for email: ${email}`)
57+
58+
const response = await axios.post(
59+
`${config.FIREBASE_URL.BASE}${config.FIREBASE_URL.CONFIRM_EMAIL_VERIFICATION}`,
60+
{
61+
oobCode,
62+
},
63+
)
64+
} catch (err: any) {
65+
console.log(err)
66+
throw err
67+
}
68+
}
69+
4070
export async function firebaseSignUp(email: string, password: string) {
4171
try {
4272
const response = await axios.post(`${config.FIREBASE_URL.BASE}${config.FIREBASE_URL.SIGNUP}`, {

tests/e2e/utils/seed-test-data.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@ import {createSomeNotifications} from 'shared/create-notification'
33
import {createSupabaseDirectClient} from 'shared/supabase/init'
44
import {insert} from 'shared/supabase/utils'
55

6-
import {seedUser} from './seedDatabase'
6+
import {
7+
seedUser,
8+
TEST_USER_DISPLAY_NAME,
9+
TEST_USER_EMAIL,
10+
TEST_USER_PASSWORD,
11+
TEST_USER_USERNAME,
12+
} from './seedDatabase'
713

814
async function seedCompatibilityPrompts(userId: string | null = null) {
915
// Need some prompts to prevent the onboarding from stopping once it reaches them (just after profile creation)
@@ -59,6 +65,15 @@ type ProfileType = 'basic' | 'medium' | 'full'
5965
}
6066
}
6167

68+
// Used in some tests that require interaction with a permanent user
69+
await seedUser(
70+
TEST_USER_EMAIL,
71+
TEST_USER_PASSWORD,
72+
'full',
73+
TEST_USER_DISPLAY_NAME,
74+
TEST_USER_USERNAME,
75+
)
76+
6277
await seedCompatibilityPrompts()
6378
await seedNotifications()
6479

tests/e2e/utils/seedDatabase.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@ import {insert} from 'shared/supabase/utils'
99
import {getUser} from 'shared/utils'
1010

1111
import UserAccountInformationForSeeding from '../backend/utils/userInformation'
12-
import {firebaseSignUp} from './firebaseUtils'
12+
import {firebaseSignUp, verifyEmail} from './firebaseUtils'
13+
14+
export const TEST_USER_EMAIL = 'user@compass.test'
15+
export const TEST_USER_PASSWORD = 'pass'
16+
export const TEST_USER_DISPLAY_NAME = 'Test User'
17+
export const TEST_USER_USERNAME = 'TestUser'
1318

1419
/**
1520
* Function used to populate the database with profiles.
@@ -151,6 +156,7 @@ export async function seedUser(
151156
profileType?: string | undefined,
152157
displayName?: string | undefined,
153158
userName?: string | undefined,
159+
verifyUserEmail?: boolean,
154160
) {
155161
const userInfo = new UserAccountInformationForSeeding()
156162
if (email) userInfo.email = email
@@ -162,4 +168,5 @@ export async function seedUser(
162168
// Fall back to the pre-generated faker id when Firebase is unreachable
163169
const created = await seedDbUser(userInfo, profileType ?? 'full')
164170
if (created) debug('User created in Supabase:', userInfo.email)
171+
if (verifyUserEmail) await verifyEmail(userInfo.email, userInfo.password)
165172
}

tests/e2e/web/SPEC_CONFIG.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ export const config = {
22
BASE_URL: 'http://localhost:3000',
33
FIREBASE_URL: {
44
BASE: 'http://localhost:9099/identitytoolkit.googleapis.com/v1',
5+
FIREBASE_EMULATOR_API: 'http://localhost:9099/emulator/v1/projects/compass-57c3c/oobCodes',
56
SIGNUP: '/accounts:signUp?key=fake-api-key',
67
SIGN_IN_PASSWORD: '/accounts:signInWithPassword?key=fake-api-key',
78
ACCOUNT_LOOKUP: '/accounts:lookup?key=fake-api-key',
89
DELETE: '/accounts:delete?key=fake-api-key',
10+
SEND_EMAIL_VERIFICATION: '/accounts:sendOobCode?key=fake-api-key',
11+
CONFIRM_EMAIL_VERIFICATION: '/accounts:update?key=fake-api-key',
912
},
1013
USERS: {
1114
DEV_1: {

tests/e2e/web/fixtures/signInFixture.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import {deleteUser} from '../utils/deleteUser'
77

88
export const test = base.extend<{
99
app: App
10-
dev_one_account: UserAccountInformation
10+
devOneAccount: UserAccountInformation
11+
devTwoAccount: UserAccountInformation
12+
specAccount: UserAccountInformation
1113
fakerAccount: UserAccountInformation
1214
googleAccountOne: UserAccountInformation
1315
googleAccountTwo: UserAccountInformation
@@ -17,6 +19,7 @@ export const test = base.extend<{
1719
app: async ({page}, use) => {
1820
const appPage = new App(page)
1921
await use(appPage)
22+
await appPage.contextManager?.closeAll()
2023
},
2124
signedInAccount: async ({app}: {app: App}, use) => {
2225
const account = testAccounts.faker_account()
@@ -26,6 +29,7 @@ export const test = base.extend<{
2629
undefined,
2730
account.display_name,
2831
account.username,
32+
true,
2933
)
3034
await app.signinWithEmail(account)
3135
await use(account)
@@ -39,6 +43,46 @@ export const test = base.extend<{
3943
undefined,
4044
account.display_name,
4145
account.username,
46+
true,
47+
)
48+
await use(account)
49+
await deleteUser('Email/Password', account)
50+
},
51+
devOneAccount: async ({}, use) => {
52+
const account = testAccounts.dev_one_account()
53+
await seedUser(
54+
account.email,
55+
account.password,
56+
undefined,
57+
account.display_name,
58+
account.username,
59+
true,
60+
)
61+
await use(account)
62+
await deleteUser('Email/Password', account)
63+
},
64+
devTwoAccount: async ({}, use) => {
65+
const account = testAccounts.dev_two_account()
66+
await seedUser(
67+
account.email,
68+
account.password,
69+
undefined,
70+
account.display_name,
71+
account.username,
72+
true,
73+
)
74+
await use(account)
75+
await deleteUser('Email/Password', account)
76+
},
77+
specAccount: async ({}, use) => {
78+
const account = testAccounts.spec_account()
79+
await seedUser(
80+
account.email,
81+
account.password,
82+
undefined,
83+
account.display_name,
84+
account.username,
85+
true,
4286
)
4387
await use(account)
4488
await deleteUser('Email/Password', account)

tests/e2e/web/pages/app.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
1-
import {Page} from '@playwright/test'
1+
import {BrowserContext, Page} from '@playwright/test'
22

3+
import {ContextManager} from '../../utils/contextManager'
34
import {UserAccountInformation} from '../utils/accountInformation'
45
import {AuthPage} from './authPage'
56
import {CompatibilityPage} from './compatibilityPage'
67
import {HomePage} from './homePage'
8+
import {MessagesPage} from './messagesPage'
9+
import {NotificationPage} from './notificationsPage'
710
import {OnboardingPage} from './onboardingPage'
811
import {OrganizationPage} from './organizationPage'
12+
import {PeoplePage} from './peoplePage'
913
import {ProfilePage} from './profilePage'
1014
import {SettingsPage} from './settingsPage'
1115
import {SignUpPage} from './signUpPage'
1216
import {SocialPage} from './socialPage'
13-
import {PeoplePage} from './peoplePage'
14-
import {NotificationPage} from './notificationsPage'
1517

1618
export class App {
1719
readonly auth: AuthPage
@@ -25,6 +27,9 @@ export class App {
2527
readonly social: SocialPage
2628
readonly people: PeoplePage
2729
readonly notifs: NotificationPage
30+
readonly messages: MessagesPage
31+
readonly contextManager: ContextManager
32+
readonly context: BrowserContext
2833

2934
constructor(public readonly page: Page) {
3035
this.auth = new AuthPage(page)
@@ -38,6 +43,12 @@ export class App {
3843
this.social = new SocialPage(page)
3944
this.people = new PeoplePage(page)
4045
this.notifs = new NotificationPage(page)
46+
this.messages = new MessagesPage(page)
47+
this.context = page.context()
48+
49+
const browser = page.context().browser()
50+
if (!browser) throw new Error('Could not get Browser from page.context().browser()')
51+
this.contextManager = new ContextManager(browser)
4152
}
4253

4354
async deleteProfileFromSettings() {
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import {expect, Locator, Page} from '@playwright/test'
2+
import {sleep} from 'common/util/time'
3+
4+
export class MessagesPage {
5+
private readonly messagesPageHeader: Locator
6+
private readonly messagesTable: Locator
7+
private readonly messagesRow: Locator
8+
private readonly messagesUsername: Locator
9+
private readonly messagesTimestamp: Locator
10+
private readonly newMessageButton: Locator
11+
private readonly newMessageSearchUsers: Locator
12+
private readonly newMessageSearchResults: Locator
13+
private readonly newMessageSearchCreateButton: Locator
14+
private readonly newMessageStart: Locator
15+
private readonly messageInput: Locator
16+
private readonly messageSubmit: Locator
17+
private readonly conversation: Locator
18+
private readonly conversationMessage: Locator
19+
20+
constructor(public readonly page: Page) {
21+
this.messagesPageHeader = page.getByRole('heading', {name: 'Messages'})
22+
this.messagesTable = page.getByTestId('messages-table')
23+
this.messagesRow = page.getByTestId('messages-row')
24+
this.messagesUsername = page.getByTestId('messages-username')
25+
this.messagesTimestamp = page.getByTestId('messages-timestamp')
26+
this.newMessageButton = page.getByRole('button', {name: 'New Message'})
27+
this.newMessageSearchUsers = page.getByRole('textbox', {name: 'Search users...'})
28+
this.newMessageSearchResults = page.getByTestId('search-results')
29+
this.newMessageSearchCreateButton = page.getByRole('button', {name: 'Create'})
30+
this.newMessageStart = page.getByText('No messages yet.', {exact: true})
31+
this.messageInput = page.locator('.tiptap')
32+
this.messageSubmit = page.getByTestId('conversation-message-submit')
33+
this.conversation = page.getByTestId('conversation')
34+
this.conversationMessage = page.getByTestId('conversation-message')
35+
}
36+
37+
async verifyMessagesPage() {
38+
await expect(this.messagesPageHeader).toBeVisible()
39+
}
40+
41+
async createNewMessage(username: string[]) {
42+
await expect(this.newMessageButton).toBeVisible()
43+
await this.newMessageButton.click()
44+
await expect(this.newMessageSearchUsers).toBeVisible()
45+
for (let i = 0; i < username.length; i++) {
46+
await this.newMessageSearchUsers.fill(username[i])
47+
await sleep(1000)
48+
await expect(this.newMessageSearchResults).toBeVisible()
49+
const results = await this.newMessageSearchResults
50+
.getByTestId('search-results-username')
51+
.all()
52+
const targetUser = username[i].toLowerCase()
53+
for (let j = 0; j < results.length; j++) {
54+
const usernameResults = (await results[j].textContent())?.toLowerCase()
55+
if (usernameResults === targetUser) {
56+
await results[j].click()
57+
break
58+
}
59+
}
60+
}
61+
62+
await expect(this.newMessageSearchCreateButton).toBeVisible()
63+
await this.newMessageSearchCreateButton.click()
64+
}
65+
66+
async sendMessage(message: string) {
67+
await expect(this.messageInput).toBeVisible()
68+
await this.messageInput.fill(message)
69+
await expect(this.messageSubmit).toBeVisible()
70+
await this.messageSubmit.click()
71+
const verified = await this.verifyMessage(message)
72+
if (!verified)
73+
throw new Error(`Message "${message}" was not found in conversation after sending`)
74+
}
75+
76+
async findMessageConversation(displayName: string) {
77+
await expect(this.messagesTable).toBeVisible()
78+
await this.page.waitForTimeout(1000)
79+
const doMessagesExist = (await this.messagesRow.count()) > 0
80+
if (doMessagesExist) {
81+
const messages = await this.messagesRow.getByTestId('messages-username').all()
82+
83+
for (let i = 0; i < messages.length; i++) {
84+
await expect(messages[i]).toBeVisible()
85+
const messageFromUser = await messages[i].textContent()
86+
if (messageFromUser?.toLowerCase() === displayName.toLowerCase()) await messages[i].click()
87+
}
88+
} else {
89+
throw new Error('There are no messages on this account')
90+
}
91+
}
92+
93+
async verifyMessage(messageContent: string) {
94+
await expect(this.conversation).toBeVisible()
95+
await sleep(1000)
96+
const messageCount = (await this.conversationMessage.count()) > 0
97+
if (messageCount) {
98+
const messages = await this.conversationMessage.all()
99+
for (let i = 0; i < messages.length; i++) {
100+
const message = await messages[i].textContent()
101+
if (message?.toLowerCase() === messageContent.toLowerCase()) return true
102+
}
103+
return false
104+
} else {
105+
throw new Error('There are no messages in this conversation')
106+
}
107+
}
108+
}

0 commit comments

Comments
 (0)