mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-04-04 14:53:33 -04:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b6c30b987 | ||
|
|
8da9bd8883 | ||
|
|
bdbce67423 | ||
|
|
e2cdfc01cd | ||
|
|
d2c9d12b39 | ||
|
|
09736cd49b | ||
|
|
f16bef97dc | ||
|
|
92d4222f96 | ||
|
|
008110b015 | ||
|
|
29ace2d2e5 | ||
|
|
07f927d738 | ||
|
|
993117ba72 | ||
|
|
c8801a0235 | ||
|
|
93cd105871 | ||
|
|
4f98d99dd9 | ||
|
|
b46d39d9b7 | ||
|
|
5ea095662b | ||
|
|
2400d50247 | ||
|
|
8ffd69ff15 | ||
|
|
0b721ec7b9 | ||
|
|
2019c835a0 | ||
|
|
ff23a8c1bc | ||
|
|
df775e9aa3 |
120
.coderabbit.yaml
Normal file
120
.coderabbit.yaml
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
# Enables IDE autocompletion for this config file
|
||||||
|
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
|
||||||
|
|
||||||
|
# Language for CodeRabbit's review comments
|
||||||
|
language: en
|
||||||
|
|
||||||
|
# Enable experimental features (currently not using any specific early_access features)
|
||||||
|
early_access: true
|
||||||
|
|
||||||
|
chat:
|
||||||
|
# CodeRabbit will automatically respond to @coderabbitai mentions in PR comments
|
||||||
|
auto_reply: true
|
||||||
|
|
||||||
|
reviews:
|
||||||
|
auto_review:
|
||||||
|
# Automatically trigger reviews when PRs are opened or updated
|
||||||
|
enabled: true
|
||||||
|
# Skip auto-review if PR title contains these keywords
|
||||||
|
ignore_title_keywords:
|
||||||
|
- "WIP"
|
||||||
|
# Don't auto-review draft PRs
|
||||||
|
drafts: false
|
||||||
|
# Only auto-review PRs targeting these branches
|
||||||
|
base_branches:
|
||||||
|
- main
|
||||||
|
- develop
|
||||||
|
|
||||||
|
# Include a high-level summary at the start of each review
|
||||||
|
high_level_summary: true
|
||||||
|
|
||||||
|
# Generate sequence diagrams for complex code flows
|
||||||
|
sequence_diagrams: true
|
||||||
|
|
||||||
|
# Don't include poems in reviews (fun feature, but keeping it professional)
|
||||||
|
poem: false
|
||||||
|
|
||||||
|
# Show review completion status
|
||||||
|
review_status: true
|
||||||
|
|
||||||
|
# Keep the walkthrough section expanded by default
|
||||||
|
collapse_walkthrough: false
|
||||||
|
|
||||||
|
# Include summary of all changed files
|
||||||
|
changed_files_summary: true
|
||||||
|
|
||||||
|
# Don't automatically request changes on the PR (just leave comments)
|
||||||
|
request_changes_workflow: false
|
||||||
|
|
||||||
|
# Pre-merge checks to enforce before merging PRs
|
||||||
|
pre_merge_checks:
|
||||||
|
description:
|
||||||
|
# Validate that PR has a proper description
|
||||||
|
mode: warning # Options: off, warning, error
|
||||||
|
docstrings:
|
||||||
|
# Disable docstring coverage checks (let's assume we don't need them)
|
||||||
|
mode: off
|
||||||
|
|
||||||
|
# Exclude these paths from reviews (build artifacts and dependencies)
|
||||||
|
path_filters:
|
||||||
|
- "!**/node_modules/**" # npm dependencies
|
||||||
|
- "!**/android/**" # Native Android build files
|
||||||
|
- "!**/ios/**" # Native iOS build files
|
||||||
|
- "!**/.expo/**" # Expo build cache
|
||||||
|
- "!**/.expo-shared/**" # Expo shared config
|
||||||
|
- "!**/dist/**" # Build output
|
||||||
|
|
||||||
|
# Custom review instructions for specific file patterns
|
||||||
|
path_instructions:
|
||||||
|
# TypeScript/JavaScript files - main app code
|
||||||
|
- path: "**/*.{ts,tsx,js,jsx}"
|
||||||
|
instructions: |
|
||||||
|
General practices:
|
||||||
|
- Summarize the changes clearly.
|
||||||
|
- Format the summary with bullet points.
|
||||||
|
- Highlight any potential breaking changes for users.
|
||||||
|
- We use early returns to avoid deep nesting.
|
||||||
|
- Ensure all public functions have docstrings.
|
||||||
|
- 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
|
||||||
|
- Safe area handling and platform-specific code (iOS vs Android)
|
||||||
|
- Memory leaks in useEffect and event listeners
|
||||||
|
|
||||||
|
Performance:
|
||||||
|
- Use FlatList/SectionList for lists (never ScrollView with .map)
|
||||||
|
- React.memo, useMemo, useCallback where appropriate
|
||||||
|
|
||||||
|
TypeScript:
|
||||||
|
- Avoid 'any', use explicit types
|
||||||
|
- Prefer 'import type' for type imports
|
||||||
|
|
||||||
|
Security:
|
||||||
|
- No exposed API keys or sensitive data
|
||||||
|
- Use expo-secure-store for sensitive storage
|
||||||
|
- Validate deep linking configurations
|
||||||
|
|
||||||
|
Internationalization:
|
||||||
|
- 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`.
|
||||||
5
.coderabbitignore
Normal file
5
.coderabbitignore
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
**/*.md
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
dist/**
|
||||||
|
test/mocks/**
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -102,3 +102,5 @@ test-results
|
|||||||
**/coverage
|
**/coverage
|
||||||
|
|
||||||
*my-release-key.keystore
|
*my-release-key.keystore
|
||||||
|
|
||||||
|
.vscode/settings.json
|
||||||
@@ -11,8 +11,8 @@ android {
|
|||||||
applicationId "com.compassconnections.app"
|
applicationId "com.compassconnections.app"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 79
|
versionCode 84
|
||||||
versionName "1.16.0"
|
versionName "1.17.0"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
|||||||
@@ -238,7 +238,16 @@ public class MainActivity extends BridgeActivity implements ModifiedMainActivity
|
|||||||
checkForUpdates();
|
checkForUpdates();
|
||||||
|
|
||||||
Uri data = getIntent().getData();
|
Uri data = getIntent().getData();
|
||||||
if (data != null) pendingDeepLink = data.toString();
|
if (data != null) {
|
||||||
|
pendingDeepLink = data.toString();
|
||||||
|
} else {
|
||||||
|
// Check for notification endpoint when app is opened from cold start via notification click
|
||||||
|
String endpoint = getIntent().getStringExtra("endpoint");
|
||||||
|
if (endpoint != null) {
|
||||||
|
Log.i("CompassApp", "onCreate found endpoint from notification: " + endpoint);
|
||||||
|
pendingDeepLink = endpoint;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleDeepLink(String url) {
|
private void handleDeepLink(String url) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@compass/api",
|
"name": "@compass/api",
|
||||||
"version": "1.30.3",
|
"version": "1.31.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Backend API endpoints",
|
"description": "Backend API endpoints",
|
||||||
"main": "src/serve.ts",
|
"main": "src/serve.ts",
|
||||||
|
|||||||
@@ -43,6 +43,144 @@ function getCacheKey(content: string): string {
|
|||||||
return hash.digest('hex')
|
return hash.digest('hex')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function validateProfileFields(
|
||||||
|
llmProfile: Partial<ProfileWithoutUser>,
|
||||||
|
validChoices: Record<string, string[]>,
|
||||||
|
): Promise<Partial<ProfileWithoutUser>> {
|
||||||
|
const result: Partial<Record<keyof ProfileWithoutUser, any>> = {...llmProfile}
|
||||||
|
|
||||||
|
const toArray: (keyof ProfileWithoutUser)[] = [
|
||||||
|
'diet',
|
||||||
|
'ethnicity',
|
||||||
|
'interests',
|
||||||
|
'causes',
|
||||||
|
'work',
|
||||||
|
'languages',
|
||||||
|
'religion',
|
||||||
|
'political_beliefs',
|
||||||
|
'pref_gender',
|
||||||
|
'pref_relation_styles',
|
||||||
|
'pref_romantic_styles',
|
||||||
|
'relationship_status',
|
||||||
|
'keywords',
|
||||||
|
]
|
||||||
|
for (const key of toArray) {
|
||||||
|
if (result[key] !== undefined) {
|
||||||
|
if (!Array.isArray(result[key])) {
|
||||||
|
result[key] = [String(result[key])]
|
||||||
|
} else {
|
||||||
|
result[key] = result[key].map(String)
|
||||||
|
}
|
||||||
|
// Filter out invalid values
|
||||||
|
if (validChoices[key]) {
|
||||||
|
result[key] = result[key].filter((v: string) => validChoices[key].includes(v))
|
||||||
|
if (result[key].length === 0) {
|
||||||
|
result[key] = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toString: (keyof ProfileWithoutUser)[] = [
|
||||||
|
'gender',
|
||||||
|
'education_level',
|
||||||
|
'mbti',
|
||||||
|
'psychedelics',
|
||||||
|
'cannabis',
|
||||||
|
'psychedelics_intention',
|
||||||
|
'cannabis_intention',
|
||||||
|
'psychedelics_pref',
|
||||||
|
'cannabis_pref',
|
||||||
|
'headline',
|
||||||
|
'city',
|
||||||
|
'country',
|
||||||
|
'raised_in_city',
|
||||||
|
'raised_in_country',
|
||||||
|
'university',
|
||||||
|
'company',
|
||||||
|
'occupation_title',
|
||||||
|
'religious_beliefs',
|
||||||
|
'political_details',
|
||||||
|
]
|
||||||
|
for (const key of toString) {
|
||||||
|
if (result[key] !== undefined) {
|
||||||
|
if (Array.isArray(result[key])) {
|
||||||
|
result[key] = result[key][0] ?? ''
|
||||||
|
}
|
||||||
|
result[key] = String(result[key])
|
||||||
|
if (validChoices[key] && !validChoices[key].includes(result[key])) {
|
||||||
|
result[key] = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toNumber: (keyof ProfileWithoutUser)[] = [
|
||||||
|
'age',
|
||||||
|
'height_in_inches',
|
||||||
|
'drinks_per_month',
|
||||||
|
'has_kids',
|
||||||
|
'wants_kids_strength',
|
||||||
|
'big5_openness',
|
||||||
|
'big5_conscientiousness',
|
||||||
|
'big5_extraversion',
|
||||||
|
'big5_agreeableness',
|
||||||
|
'big5_neuroticism',
|
||||||
|
'pref_age_min',
|
||||||
|
'pref_age_max',
|
||||||
|
'city_latitude',
|
||||||
|
'city_longitude',
|
||||||
|
'raised_in_lat',
|
||||||
|
'raised_in_lon',
|
||||||
|
]
|
||||||
|
for (const key of toNumber) {
|
||||||
|
if (result[key] !== undefined) {
|
||||||
|
const num = Number(result[key])
|
||||||
|
result[key] = isNaN(num) ? undefined : num
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toBoolean: (keyof ProfileWithoutUser)[] = ['is_smoker']
|
||||||
|
for (const key of toBoolean) {
|
||||||
|
if (result[key] !== undefined) {
|
||||||
|
result[key] = Boolean(result[key])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.city) {
|
||||||
|
if (!result.city_latitude || !result.city_longitude) {
|
||||||
|
const response = await searchLocation({term: result.city, limit: 1})
|
||||||
|
const locations = response.data?.data
|
||||||
|
result.city_latitude = locations?.[0]?.latitude
|
||||||
|
result.city_longitude = locations?.[0]?.longitude
|
||||||
|
result.country ??= locations?.[0]?.country
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.raised_in_city) {
|
||||||
|
if (!result.raised_in_lat || !result.raised_in_lon) {
|
||||||
|
const response = await searchLocation({term: result.raised_in_city, limit: 1})
|
||||||
|
const locations = response.data?.data
|
||||||
|
result.raised_in_lat = locations?.[0]?.latitude
|
||||||
|
result.raised_in_lon = locations?.[0]?.longitude
|
||||||
|
result.raised_in_country ??= locations?.[0]?.country
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.links) {
|
||||||
|
const sites = Object.keys(result.links).filter((key) => SITE_ORDER.includes(key as any))
|
||||||
|
result.links = sites.reduce(
|
||||||
|
(acc, key) => {
|
||||||
|
const link = (result.links as Record<string, any>)[key]
|
||||||
|
if (link) acc[key] = link
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{} as Record<string, any>,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
async function getCachedResult(cacheKey: string): Promise<Partial<ProfileWithoutUser> | null> {
|
async function getCachedResult(cacheKey: string): Promise<Partial<ProfileWithoutUser> | null> {
|
||||||
if (!USE_CACHE) return null
|
if (!USE_CACHE) return null
|
||||||
try {
|
try {
|
||||||
@@ -180,10 +318,34 @@ async function callLLM(content: string, locale?: string): Promise<Partial<Profil
|
|||||||
getOptions('work', locale),
|
getOptions('work', locale),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const validChoices: Partial<Record<keyof ProfileWithoutUser, string[]>> = {
|
||||||
|
interests: INTERESTS,
|
||||||
|
causes: CAUSE_AREAS,
|
||||||
|
work: WORK_AREAS,
|
||||||
|
diet: Object.values(DIET_CHOICES),
|
||||||
|
ethnicity: Object.values(RACE_CHOICES),
|
||||||
|
languages: Object.values(LANGUAGE_CHOICES),
|
||||||
|
religion: Object.values(RELIGION_CHOICES),
|
||||||
|
political_beliefs: Object.values(POLITICAL_CHOICES),
|
||||||
|
pref_gender: Object.values(GENDERS),
|
||||||
|
pref_relation_styles: Object.values(RELATIONSHIP_CHOICES),
|
||||||
|
pref_romantic_styles: Object.values(ROMANTIC_CHOICES),
|
||||||
|
relationship_status: Object.values(RELATIONSHIP_STATUS_CHOICES),
|
||||||
|
cannabis: Object.values(CANNABIS_CHOICES),
|
||||||
|
education_level: Object.values(EDUCATION_CHOICES),
|
||||||
|
gender: Object.values(GENDERS),
|
||||||
|
mbti: Object.values(MBTI_CHOICES),
|
||||||
|
psychedelics: Object.values(PSYCHEDELICS_CHOICES),
|
||||||
|
psychedelics_intention: Object.values(SUBSTANCE_INTENTION_CHOICES),
|
||||||
|
cannabis_intention: Object.values(SUBSTANCE_INTENTION_CHOICES),
|
||||||
|
psychedelics_pref: Object.values(SUBSTANCE_PREFERENCE_CHOICES),
|
||||||
|
cannabis_pref: Object.values(SUBSTANCE_PREFERENCE_CHOICES),
|
||||||
|
}
|
||||||
|
|
||||||
const PROFILE_FIELDS: Partial<Record<keyof ProfileWithoutUser, any>> = {
|
const PROFILE_FIELDS: Partial<Record<keyof ProfileWithoutUser, any>> = {
|
||||||
// Basic info
|
// Basic info
|
||||||
age: 'Number. Age in years.',
|
age: 'Number. Age in years.',
|
||||||
gender: `One of: ${Object.values(GENDERS).join(', ')}. Infer if you have enough evidence`,
|
gender: `String. One of: ${validChoices.pref_gender?.join(', ')}. If multiple mentioned, use the most likely one. Infer if you have enough evidence`,
|
||||||
height_in_inches: 'Number. Height converted to inches.',
|
height_in_inches: 'Number. Height converted to inches.',
|
||||||
city: 'String. Current city of residence (English spelling).',
|
city: 'String. Current city of residence (English spelling).',
|
||||||
country: 'String. Current country of residence (English spelling).',
|
country: 'String. Current country of residence (English spelling).',
|
||||||
@@ -196,7 +358,7 @@ async function callLLM(content: string, locale?: string): Promise<Partial<Profil
|
|||||||
raised_in_lat: 'Number. Latitude of city where they grew up.',
|
raised_in_lat: 'Number. Latitude of city where they grew up.',
|
||||||
raised_in_lon: 'Number. Longitude of city where they grew up.',
|
raised_in_lon: 'Number. Longitude of city where they grew up.',
|
||||||
university: 'String. University or college attended.',
|
university: 'String. University or college attended.',
|
||||||
education_level: `One of: ${Object.values(EDUCATION_CHOICES).join(', ')}`,
|
education_level: `String. One of: ${validChoices.education_level?.join(', ')}. Highest level completed`,
|
||||||
company: 'String. Current employer or company name.',
|
company: 'String. Current employer or company name.',
|
||||||
occupation_title: 'String. Current job title.',
|
occupation_title: 'String. Current job title.',
|
||||||
|
|
||||||
@@ -206,19 +368,19 @@ async function callLLM(content: string, locale?: string): Promise<Partial<Profil
|
|||||||
has_kids: 'Number. 0 if no kids, otherwise number of kids.',
|
has_kids: 'Number. 0 if no kids, otherwise number of kids.',
|
||||||
wants_kids_strength:
|
wants_kids_strength:
|
||||||
'Number 0–4. How strongly they want kids (0 = definitely not, 4 = definitely yes).',
|
'Number 0–4. How strongly they want kids (0 = definitely not, 4 = definitely yes).',
|
||||||
diet: `Array. Any of: ${Object.values(DIET_CHOICES).join(', ')}`,
|
diet: `Array. Any of: ${validChoices.diet?.join(', ')}`,
|
||||||
ethnicity: `Array. Any of: ${Object.values(RACE_CHOICES).join(', ')}`,
|
ethnicity: `Array. Any of: ${validChoices.ethnicity?.join(', ')}`,
|
||||||
|
|
||||||
// Substances
|
// Substances
|
||||||
psychedelics: `One of: ${Object.values(PSYCHEDELICS_CHOICES).join(', ')}. Usage frequency of psychedelics/plant medicine, only if explicitly stated.`,
|
psychedelics: `String. One of: ${validChoices.psychedelics?.join(', ')}. Usage frequency of psychedelics/plant medicine, only if explicitly stated.`,
|
||||||
cannabis: `One of: ${Object.values(CANNABIS_CHOICES).join(', ')}. Usage frequency of cannabis, only if explicitly stated.`,
|
cannabis: `String. One of: ${validChoices.cannabis?.join(', ')}. Usage frequency of cannabis, only if explicitly stated.`,
|
||||||
psychedelics_intention: `Array. Any of: ${Object.values(SUBSTANCE_INTENTION_CHOICES).join(', ')}. Only if they use psychedelics.`,
|
psychedelics_intention: `String. Array. Any of: ${validChoices.psychedelics_intention?.join(', ')}. Only if they use psychedelics.`,
|
||||||
cannabis_intention: `Array. Any of: ${Object.values(SUBSTANCE_INTENTION_CHOICES).join(', ')}. Only if they use cannabis.`,
|
cannabis_intention: `String. Array. Any of: ${validChoices.cannabis_intention?.join(', ')}. Only if they use cannabis.`,
|
||||||
psychedelics_pref: `Array. Any of: ${Object.values(SUBSTANCE_PREFERENCE_CHOICES).join(', ')}. Partner preference for psychedelics use.`,
|
psychedelics_pref: `String. Array. Any of: ${validChoices.psychedelics_pref?.join(', ')}. Partner preference for psychedelics use.`,
|
||||||
cannabis_pref: `Array. Any of: ${Object.values(SUBSTANCE_PREFERENCE_CHOICES).join(', ')}. Partner preference for cannabis use.`,
|
cannabis_pref: `String. Array. Any of: ${validChoices.cannabis_pref?.join(', ')}. Partner preference for cannabis use.`,
|
||||||
|
|
||||||
// Identity — big5 only if person explicitly states a score, never infer from personality description
|
// Identity — big5 only if person explicitly states a score, never infer from personality description
|
||||||
mbti: `One of: ${Object.values(MBTI_CHOICES).join(', ')}`,
|
mbti: `String. One of: ${validChoices.mbti?.join(', ')}`,
|
||||||
big5_openness: 'Number 0–100. Only if explicitly self-reported, never infer.',
|
big5_openness: 'Number 0–100. Only if explicitly self-reported, never infer.',
|
||||||
big5_conscientiousness: 'Number 0–100. Only if explicitly self-reported, never infer.',
|
big5_conscientiousness: 'Number 0–100. Only if explicitly self-reported, never infer.',
|
||||||
big5_extraversion: 'Number 0–100. Only if explicitly self-reported, never infer.',
|
big5_extraversion: 'Number 0–100. Only if explicitly self-reported, never infer.',
|
||||||
@@ -226,23 +388,23 @@ async function callLLM(content: string, locale?: string): Promise<Partial<Profil
|
|||||||
big5_neuroticism: 'Number 0–100. Only if explicitly self-reported, never infer.',
|
big5_neuroticism: 'Number 0–100. Only if explicitly self-reported, never infer.',
|
||||||
|
|
||||||
// Beliefs
|
// Beliefs
|
||||||
religion: `Array. Any of: ${Object.values(RELIGION_CHOICES).join(', ')}`,
|
religion: `Array. Any of: ${validChoices.religion?.join(', ')}`,
|
||||||
religious_beliefs:
|
religious_beliefs:
|
||||||
'String. Free-form elaboration on religious views, only if explicitly stated.',
|
'String. Free-form elaboration on religious views, only if explicitly stated.',
|
||||||
political_beliefs: `Array. Any of: ${Object.values(POLITICAL_CHOICES).join(', ')}`,
|
political_beliefs: `Array. Any of: ${validChoices.political_beliefs?.join(', ')}`,
|
||||||
political_details:
|
political_details:
|
||||||
'String. Free-form elaboration on political views, only if explicitly stated.',
|
'String. Free-form elaboration on political views, only if explicitly stated.',
|
||||||
|
|
||||||
// Preferences
|
// Preferences
|
||||||
pref_age_min: 'Number. Minimum preferred age of match.',
|
pref_age_min: 'Number. Minimum preferred age of match.',
|
||||||
pref_age_max: 'Number. Maximum preferred age of match.',
|
pref_age_max: 'Number. Maximum preferred age of match.',
|
||||||
pref_gender: `Array. Any of: ${Object.values(GENDERS).join(', ')}`,
|
pref_gender: `Array. Any of: ${validChoices.pref_gender?.join(', ')}`,
|
||||||
pref_relation_styles: `Array. Any of: ${Object.values(RELATIONSHIP_CHOICES).join(', ')}`,
|
pref_relation_styles: `Array. Any of: ${validChoices.pref_relation_styles?.join(', ')}`,
|
||||||
pref_romantic_styles: `Array. Any of: ${Object.values(ROMANTIC_CHOICES).join(', ')}`,
|
pref_romantic_styles: `Array. Any of: ${validChoices.pref_romantic_styles?.join(', ')}`,
|
||||||
relationship_status: `Array. Any of: ${Object.values(RELATIONSHIP_STATUS_CHOICES).join(', ')}`,
|
relationship_status: `Array. Any of: ${validChoices.relationship_status?.join(', ')}`,
|
||||||
|
|
||||||
// Languages
|
// Languages
|
||||||
languages: `Array. Any of: ${Object.values(LANGUAGE_CHOICES).join(', ')}. If none, infer from text.`,
|
languages: `Array. Any of: ${validChoices.languages?.join(', ')}. If none, infer from text.`,
|
||||||
|
|
||||||
// Free-form
|
// Free-form
|
||||||
headline:
|
headline:
|
||||||
@@ -251,9 +413,9 @@ async function callLLM(content: string, locale?: string): Promise<Partial<Profil
|
|||||||
links: `Object. Key is any of: ${SITE_ORDER.join(', ')}.`,
|
links: `Object. Key is any of: ${SITE_ORDER.join(', ')}.`,
|
||||||
|
|
||||||
// Taxonomies — match existing labels first, only add new if truly no close match exists
|
// Taxonomies — match existing labels first, only add new if truly no close match exists
|
||||||
interests: `Array. Prefer existing labels, only add new if no close match. Any of: ${INTERESTS.join(', ')}`,
|
interests: `Array. Prefer existing labels, only add new if no close match. Any of: ${validChoices.interests?.join(', ')}`,
|
||||||
causes: `Array. Prefer existing labels, only add new if no close match. Any of: ${CAUSE_AREAS.join(', ')}`,
|
causes: `Array. Prefer existing labels, only add new if no close match. Any of: ${validChoices.causes?.join(', ')}`,
|
||||||
work: `Array. Use only existing labels, do not add new if no close match. Any of: ${WORK_AREAS.join(', ')}`,
|
work: `Array. Use only existing labels, do not add new if no close match. Any of: ${validChoices.work?.join(', ')}`,
|
||||||
}
|
}
|
||||||
|
|
||||||
const EXTRACTION_PROMPT = `You are a profile information extraction expert analyzing text from a personal webpage, LinkedIn, bio, or similar source.
|
const EXTRACTION_PROMPT = `You are a profile information extraction expert analyzing text from a personal webpage, LinkedIn, bio, or similar source.
|
||||||
@@ -296,42 +458,13 @@ TEXT TO ANALYZE:
|
|||||||
let parsed: Partial<ProfileWithoutUser>
|
let parsed: Partial<ProfileWithoutUser>
|
||||||
try {
|
try {
|
||||||
parsed = typeof outputText === 'string' ? JSON.parse(outputText) : outputText
|
parsed = typeof outputText === 'string' ? JSON.parse(outputText) : outputText
|
||||||
|
parsed = await validateProfileFields(parsed, validChoices)
|
||||||
parsed = removeNullOrUndefinedProps(parsed)
|
parsed = removeNullOrUndefinedProps(parsed)
|
||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
log('Failed to parse LLM response as JSON', {outputText, parseError})
|
log('Failed to parse LLM response as JSON', {outputText, parseError})
|
||||||
throw APIErrors.internalServerError('Failed to parse extracted data')
|
throw APIErrors.internalServerError('Failed to parse extracted data')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsed.city) {
|
|
||||||
if (!parsed.city_latitude || !parsed.city_longitude) {
|
|
||||||
const result = await searchLocation({term: parsed.city, limit: 1})
|
|
||||||
const locations = result.data?.data
|
|
||||||
parsed.city_latitude = locations?.[0]?.latitude
|
|
||||||
parsed.city_longitude = locations?.[0]?.longitude
|
|
||||||
parsed.country ??= locations?.[0]?.country
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (parsed.raised_in_city) {
|
|
||||||
if (!parsed.raised_in_lat || !parsed.raised_in_lon) {
|
|
||||||
const result = await searchLocation({term: parsed.raised_in_city, limit: 1})
|
|
||||||
const locations = result.data?.data
|
|
||||||
parsed.raised_in_lat = locations?.[0]?.latitude
|
|
||||||
parsed.raised_in_lon = locations?.[0]?.longitude
|
|
||||||
parsed.raised_in_country ??= locations?.[0]?.country
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (parsed.links) {
|
|
||||||
const sites = Object.keys(parsed.links).filter((key) => SITE_ORDER.includes(key as any))
|
|
||||||
parsed.links = sites.reduce(
|
|
||||||
(acc, key) => {
|
|
||||||
const link = (parsed.links as Record<string, any>)[key]
|
|
||||||
if (link) acc[key] = link
|
|
||||||
return acc
|
|
||||||
},
|
|
||||||
{} as Record<string, any>,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
await setCachedResult(cacheKey, parsed)
|
await setCachedResult(cacheKey, parsed)
|
||||||
|
|
||||||
return parsed
|
return parsed
|
||||||
|
|||||||
@@ -55,7 +55,11 @@ export const LOCAL_BACKEND_DOMAIN = `${IS_WEBVIEW_DEV_PHONE ? '192.168.1.3' : IS
|
|||||||
|
|
||||||
export const DOMAIN = IS_LOCAL ? LOCAL_WEB_DOMAIN : ENV_CONFIG.domain
|
export const DOMAIN = IS_LOCAL ? LOCAL_WEB_DOMAIN : ENV_CONFIG.domain
|
||||||
export const DEPLOYED_WEB_URL = `https://www.${ENV_CONFIG.domain}`
|
export const DEPLOYED_WEB_URL = `https://www.${ENV_CONFIG.domain}`
|
||||||
export const WEB_URL = IS_LOCAL ? `http://${LOCAL_WEB_DOMAIN}` : `https://www.${DOMAIN}`
|
|
||||||
|
// Careful: buildOgUrl uses WEB_URL and works only on https://www.compassmeet.com (not https://compassmeet.com)
|
||||||
|
// x-vercel-error: INVALID_IMAGE_OPTIMIZE_REQUEST. Could work if needed though with some Vercel tweak
|
||||||
|
export const WEB_URL = IS_LOCAL ? `http://${LOCAL_WEB_DOMAIN}` : `https://${DOMAIN}`
|
||||||
|
|
||||||
export const BACKEND_DOMAIN = IS_LOCAL ? LOCAL_BACKEND_DOMAIN : ENV_CONFIG.backendDomain
|
export const BACKEND_DOMAIN = IS_LOCAL ? LOCAL_BACKEND_DOMAIN : ENV_CONFIG.backendDomain
|
||||||
export const FIREBASE_CONFIG = ENV_CONFIG.firebaseConfig
|
export const FIREBASE_CONFIG = ENV_CONFIG.firebaseConfig
|
||||||
export const PROJECT_ID = ENV_CONFIG.firebaseConfig.projectId
|
export const PROJECT_ID = ENV_CONFIG.firebaseConfig.projectId
|
||||||
|
|||||||
@@ -66,7 +66,9 @@ export async function run<T>(
|
|||||||
export async function run<T>(q: PromiseLike<PostgrestSingleResponse<T> | PostgrestResponse<T>>) {
|
export async function run<T>(q: PromiseLike<PostgrestSingleResponse<T> | PostgrestResponse<T>>) {
|
||||||
const {data, count, error} = await q
|
const {data, count, error} = await q
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
throw error
|
const err = new Error(error.message)
|
||||||
|
Object.assign(err, error) // copies code, details, hint onto the Error
|
||||||
|
throw err
|
||||||
} else {
|
} else {
|
||||||
return {data, count}
|
return {data, count}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -348,6 +348,7 @@ jest.mock('path/to/module')
|
|||||||
* This creates an object containing all named exports from ./path/to/module
|
* This creates an object containing all named exports from ./path/to/module
|
||||||
*/
|
*/
|
||||||
import * as mockModule from 'path/to/module'
|
import * as mockModule from 'path/to/module'
|
||||||
|
|
||||||
;(mockModule.module as jest.Mock).mockResolvedValue(mockReturnValue)
|
;(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.
|
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
|
### 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
|
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
|
## 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
|
### Port already in use
|
||||||
|
|
||||||
@@ -885,4 +948,5 @@ To download the Playwright report from a failed CI run:
|
|||||||
3. Download `playwright-report`
|
3. Download `playwright-report`
|
||||||
4. Open `index.html` in your browser
|
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).
|
||||||
|
|||||||
@@ -38,7 +38,10 @@ export default defineConfig({
|
|||||||
// If there is a known intermittent browser timing issue for some tests, it's fine to give 1 retry to those flaky tests
|
// If there is a known intermittent browser timing issue for some tests, it's fine to give 1 retry to those flaky tests
|
||||||
retries: process.env.CI ? 0 : 0,
|
retries: process.env.CI ? 0 : 0,
|
||||||
workers: process.env.CI ? 1 : undefined,
|
workers: process.env.CI ? 1 : undefined,
|
||||||
reporter: [['html', {outputFolder: `tests/reports/playwright-report`, open: 'on-failure'}]],
|
reporter: [
|
||||||
|
['list'],
|
||||||
|
['html', {outputFolder: `tests/reports/playwright-report`, open: 'on-failure'}],
|
||||||
|
],
|
||||||
use: {
|
use: {
|
||||||
baseURL: 'http://localhost:3000',
|
baseURL: 'http://localhost:3000',
|
||||||
trace: 'on-first-retry',
|
trace: 'on-first-retry',
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ import {
|
|||||||
SUBSTANCE_PREFERENCE_CHOICES,
|
SUBSTANCE_PREFERENCE_CHOICES,
|
||||||
} from 'common/choices'
|
} from 'common/choices'
|
||||||
|
|
||||||
class UserAccountInformation {
|
class UserAccountInformationForSeeding {
|
||||||
name = faker.person.fullName()
|
name = faker.person.fullName()
|
||||||
|
userName = faker.internet.displayName()
|
||||||
email = faker.internet.email()
|
email = faker.internet.email()
|
||||||
user_id = faker.string.alpha(28)
|
user_id = faker.string.alpha(28)
|
||||||
password = faker.internet.password()
|
password = faker.internet.password()
|
||||||
@@ -54,4 +55,4 @@ class UserAccountInformation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default UserAccountInformation
|
export default UserAccountInformationForSeeding
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ export async function deleteFromDb(user_id: string) {
|
|||||||
const result = await db.query(deleteEntryById, [user_id])
|
const result = await db.query(deleteEntryById, [user_id])
|
||||||
|
|
||||||
if (!result.length) {
|
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: ', {
|
console.log('Deleted data: ', {
|
||||||
@@ -19,8 +20,8 @@ export async function deleteFromDb(user_id: string) {
|
|||||||
export async function userInformationFromDb(account: any) {
|
export async function userInformationFromDb(account: any) {
|
||||||
const db = createSupabaseDirectClient()
|
const db = createSupabaseDirectClient()
|
||||||
const queryUserById = `
|
const queryUserById = `
|
||||||
SELECT p.*
|
SELECT *
|
||||||
FROM users AS p
|
FROM users
|
||||||
WHERE username = $1
|
WHERE username = $1
|
||||||
`
|
`
|
||||||
const userResults = await db.query(queryUserById, [account.username])
|
const userResults = await db.query(queryUserById, [account.username])
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import axios from 'axios'
|
|||||||
|
|
||||||
import {config} from '../web/SPEC_CONFIG'
|
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(
|
const login = await axios.post(
|
||||||
`${config.FIREBASE_URL.BASE}${config.FIREBASE_URL.SIGN_IN_PASSWORD}`,
|
`${config.FIREBASE_URL.BASE}${config.FIREBASE_URL.SIGN_IN_PASSWORD}`,
|
||||||
{
|
{
|
||||||
@@ -13,15 +16,28 @@ export async function firebaseLogin(email: string, password: string) {
|
|||||||
)
|
)
|
||||||
return login
|
return login
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getUserId(email: string, password: string) {
|
export async function getUserId(email: string, password: string) {
|
||||||
try {
|
try {
|
||||||
const loginInfo = await firebaseLogin(email, password)
|
const loginInfo = await firebaseLoginEmailPassword(email, password)
|
||||||
return loginInfo.data.localId
|
return loginInfo.data.localId
|
||||||
} catch {
|
} catch {
|
||||||
return
|
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) {
|
export async function firebaseSignUp(email: string, password: string) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post(`${config.FIREBASE_URL.BASE}${config.FIREBASE_URL.SIGNUP}`, {
|
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) {
|
export async function deleteAccount(idToken: any) {
|
||||||
await axios.post(`${config.FIREBASE_URL.BASE}${config.FIREBASE_URL.DELETE}`, {
|
try {
|
||||||
idToken: login.data.idToken,
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {createSupabaseDirectClient} from 'shared/supabase/init'
|
|||||||
import {insert} from 'shared/supabase/utils'
|
import {insert} from 'shared/supabase/utils'
|
||||||
import {getUser} from 'shared/utils'
|
import {getUser} from 'shared/utils'
|
||||||
|
|
||||||
import UserAccountInformation from '../backend/utils/userInformation'
|
import UserAccountInformationForSeeding from '../backend/utils/userInformation'
|
||||||
import {firebaseSignUp} from './firebaseUtils'
|
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 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.
|
* @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 pg = createSupabaseDirectClient()
|
||||||
const userId = userInfo.user_id
|
const userId = userInfo.user_id
|
||||||
const deviceToken = randomString()
|
const deviceToken = randomString()
|
||||||
@@ -83,14 +86,14 @@ export async function seedDbUser(userInfo: UserAccountInformation, profileType?:
|
|||||||
blockedByUserIds: [],
|
blockedByUserIds: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
await pg.tx(async (tx: any) => {
|
return pg.tx(async (tx: any) => {
|
||||||
const preexistingUser = await getUser(userId, tx)
|
const preexistingUser = await getUser(userId, tx)
|
||||||
if (preexistingUser) return
|
if (preexistingUser) return false
|
||||||
|
|
||||||
await insert(tx, 'users', {
|
await insert(tx, 'users', {
|
||||||
id: userId,
|
id: userId,
|
||||||
name: userInfo.name,
|
name: userInfo.name,
|
||||||
username: cleanUsername(userInfo.name),
|
username: cleanUsername(userInfo.userName),
|
||||||
data: {},
|
data: {},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -100,6 +103,7 @@ export async function seedDbUser(userInfo: UserAccountInformation, profileType?:
|
|||||||
})
|
})
|
||||||
|
|
||||||
await insert(tx, 'profiles', profileData)
|
await insert(tx, 'profiles', profileData)
|
||||||
|
return true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,13 +111,17 @@ export async function seedUser(
|
|||||||
email?: string | undefined,
|
email?: string | undefined,
|
||||||
password?: string | undefined,
|
password?: string | undefined,
|
||||||
profileType?: 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 (email) userInfo.email = email
|
||||||
if (password) userInfo.password = password
|
if (password) userInfo.password = password
|
||||||
|
if (displayName) userInfo.name = displayName
|
||||||
|
if (userName) userInfo.userName = userName
|
||||||
userInfo.user_id = await firebaseSignUp(userInfo.email, userInfo.password)
|
userInfo.user_id = await firebaseSignUp(userInfo.email, userInfo.password)
|
||||||
if (userInfo.user_id) {
|
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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export const config = {
|
|||||||
BASE: 'http://localhost:9099/identitytoolkit.googleapis.com/v1',
|
BASE: 'http://localhost:9099/identitytoolkit.googleapis.com/v1',
|
||||||
SIGNUP: '/accounts:signUp?key=fake-api-key',
|
SIGNUP: '/accounts:signUp?key=fake-api-key',
|
||||||
SIGN_IN_PASSWORD: '/accounts:signInWithPassword?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',
|
DELETE: '/accounts:delete?key=fake-api-key',
|
||||||
},
|
},
|
||||||
USERS: {
|
USERS: {
|
||||||
|
|||||||
@@ -4,10 +4,14 @@ import {AuthPage} from '../pages/AuthPage'
|
|||||||
import {ComatibilityPage} from '../pages/compatibilityPage'
|
import {ComatibilityPage} from '../pages/compatibilityPage'
|
||||||
import {HomePage} from '../pages/homePage'
|
import {HomePage} from '../pages/homePage'
|
||||||
import {OnboardingPage} from '../pages/onboardingPage'
|
import {OnboardingPage} from '../pages/onboardingPage'
|
||||||
|
import {OrganizationPage} from '../pages/organizationPage'
|
||||||
import {ProfilePage} from '../pages/profilePage'
|
import {ProfilePage} from '../pages/profilePage'
|
||||||
|
import {SettingsPage} from '../pages/settingsPage'
|
||||||
import {SignUpPage} from '../pages/signUpPage'
|
import {SignUpPage} from '../pages/signUpPage'
|
||||||
|
import {SocialPage} from '../pages/socialPage'
|
||||||
import {testAccounts, UserAccountInformation} from '../utils/accountInformation'
|
import {testAccounts, UserAccountInformation} from '../utils/accountInformation'
|
||||||
import {deleteUser} from '../utils/deleteUser'
|
import {deleteUser} from '../utils/deleteUser'
|
||||||
|
import {getAuthAccountInfo} from '../utils/networkUtils'
|
||||||
|
|
||||||
export const test = base.extend<{
|
export const test = base.extend<{
|
||||||
homePage: HomePage
|
homePage: HomePage
|
||||||
@@ -15,29 +19,48 @@ export const test = base.extend<{
|
|||||||
signUpPage: SignUpPage
|
signUpPage: SignUpPage
|
||||||
profilePage: ProfilePage
|
profilePage: ProfilePage
|
||||||
authPage: AuthPage
|
authPage: AuthPage
|
||||||
|
settingsPage: SettingsPage
|
||||||
|
socialPage: SocialPage
|
||||||
|
organizationPage: OrganizationPage
|
||||||
compatabilityPage: ComatibilityPage
|
compatabilityPage: ComatibilityPage
|
||||||
cleanUpUsers: void
|
cleanUpUsers: void
|
||||||
onboardingAccount: UserAccountInformation
|
onboardingAccount: UserAccountInformation
|
||||||
fakerAccount: UserAccountInformation
|
fakerAccount: UserAccountInformation
|
||||||
specAccount: UserAccountInformation
|
specAccount: UserAccountInformation
|
||||||
|
googleAccountOne: UserAccountInformation
|
||||||
|
googleAccountTwo: UserAccountInformation
|
||||||
}>({
|
}>({
|
||||||
onboardingAccount: async ({}, use) => {
|
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)
|
await use(account)
|
||||||
console.log('Cleaning up onboarding 1 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) => {
|
fakerAccount: async ({}, use) => {
|
||||||
const account = testAccounts.faker_account() // email captured here
|
const account = testAccounts.faker_account()
|
||||||
await use(account)
|
await use(account)
|
||||||
console.log('Cleaning up faker 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) => {
|
specAccount: async ({}, use) => {
|
||||||
const account = testAccounts.spec_account()
|
const account = testAccounts.spec_account()
|
||||||
await use(account)
|
await use(account)
|
||||||
console.log('Cleaning up spec account...')
|
console.log('Cleaning up spec account...')
|
||||||
await deleteUser(account.email, account.password)
|
await deleteUser('Email/Password', account)
|
||||||
},
|
},
|
||||||
onboardingPage: async ({page}, use) => {
|
onboardingPage: async ({page}, use) => {
|
||||||
const onboardingPage = new OnboardingPage(page)
|
const onboardingPage = new OnboardingPage(page)
|
||||||
@@ -63,6 +86,18 @@ export const test = base.extend<{
|
|||||||
const compatibilityPage = new ComatibilityPage(page)
|
const compatibilityPage = new ComatibilityPage(page)
|
||||||
await use(compatibilityPage)
|
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'
|
export {expect} from '@playwright/test'
|
||||||
|
|||||||
@@ -1,33 +1,64 @@
|
|||||||
import {expect, Page, test as base} from '@playwright/test'
|
import {test as base} from '@playwright/test'
|
||||||
|
|
||||||
import {seedUser} from '../../utils/seedDatabase'
|
|
||||||
import {AuthPage} from '../pages/AuthPage'
|
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<{
|
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 authPage = new AuthPage(page)
|
||||||
|
await use(authPage)
|
||||||
const email = config.USERS.DEV_1.EMAIL
|
},
|
||||||
const password = config.USERS.DEV_1.PASSWORD
|
dev_one_account: async ({}, use) => {
|
||||||
|
const account = testAccounts.dev_one_account()
|
||||||
try {
|
await use(account)
|
||||||
await seedUser(email, password)
|
},
|
||||||
} catch (_e) {
|
fakerAccount: async ({}, use) => {
|
||||||
console.log('User already exists for signinFixture', email)
|
const account = testAccounts.faker_account()
|
||||||
}
|
await use(account)
|
||||||
|
},
|
||||||
await page.goto('/signin')
|
googleAccountOne: async ({}, use) => {
|
||||||
await authPage.fillEmailField(email)
|
const account = testAccounts.google_account_one()
|
||||||
await authPage.fillPasswordField(password)
|
await use(account)
|
||||||
await authPage.clickSignInWithEmailButton()
|
},
|
||||||
|
googleAccountTwo: async ({}, use) => {
|
||||||
await page.waitForURL(/^(?!.*signin).*$/)
|
const account = testAccounts.google_account_two()
|
||||||
|
await use(account)
|
||||||
expect(page.url()).not.toContain('/signin')
|
|
||||||
|
|
||||||
await use(page)
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export {expect} from '@playwright/test'
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export class AuthPage {
|
|||||||
private readonly emailField: Locator
|
private readonly emailField: Locator
|
||||||
private readonly passwordField: Locator
|
private readonly passwordField: Locator
|
||||||
private readonly signInWithEmailButton: Locator
|
private readonly signInWithEmailButton: Locator
|
||||||
private readonly signInWithGoogleButton: Locator
|
private readonly googleButton: Locator
|
||||||
private readonly signUpWithEmailButton: Locator
|
private readonly signUpWithEmailButton: Locator
|
||||||
|
|
||||||
constructor(public readonly page: Page) {
|
constructor(public readonly page: Page) {
|
||||||
@@ -16,7 +16,7 @@ export class AuthPage {
|
|||||||
this.emailField = page.getByLabel('Email')
|
this.emailField = page.getByLabel('Email')
|
||||||
this.passwordField = page.getByLabel('Password')
|
this.passwordField = page.getByLabel('Password')
|
||||||
this.signInWithEmailButton = page.getByRole('button', {name: 'Sign in with Email'})
|
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'})
|
this.signUpWithEmailButton = page.getByRole('button', {name: 'Sign up with Email'})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,9 +35,28 @@ export class AuthPage {
|
|||||||
await this.signInWithEmailButton.click()
|
await this.signInWithEmailButton.click()
|
||||||
}
|
}
|
||||||
|
|
||||||
async clickSignInWithGoogleButton() {
|
async clickGoogleButton() {
|
||||||
await expect(this.signInWithGoogleButton).toBeVisible()
|
await expect(this.googleButton).toBeVisible()
|
||||||
await this.signInWithGoogleButton.click()
|
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() {
|
async clickSignUpWithEmailButton() {
|
||||||
|
|||||||
@@ -2,78 +2,144 @@ import {expect, Locator, Page} from '@playwright/test'
|
|||||||
import {LocaleTuple} from 'common/constants'
|
import {LocaleTuple} from 'common/constants'
|
||||||
|
|
||||||
export class HomePage {
|
export class HomePage {
|
||||||
|
private readonly sidebar: Locator
|
||||||
private readonly homePageLink: Locator
|
private readonly homePageLink: Locator
|
||||||
private readonly aboutLink: Locator
|
private readonly profileLink: 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 signUpButton: Locator
|
private readonly signUpButton: Locator
|
||||||
private readonly localePicker: Locator
|
private readonly localePicker: Locator
|
||||||
private readonly signInLink: Locator
|
private readonly signInLink: Locator
|
||||||
|
private readonly signOutLink: Locator
|
||||||
private readonly closeButton: Locator
|
private readonly closeButton: Locator
|
||||||
|
|
||||||
constructor(public readonly page: Page) {
|
constructor(public readonly page: Page) {
|
||||||
this.homePageLink = page.getByText('Compass', {exact: true})
|
this.sidebar = page.getByTestId('sidebar')
|
||||||
this.aboutLink = page.getByTestId('sidebar-about')
|
this.homePageLink = page.locator('a[href="/home"]')
|
||||||
this.faqLink = page.getByTestId('sidebar-faq')
|
this.profileLink = page.getByTestId('sidebar-username')
|
||||||
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.signUpButton = page.locator('button').filter({hasText: 'Sign up'}).first()
|
this.signUpButton = page.locator('button').filter({hasText: 'Sign up'}).first()
|
||||||
this.localePicker = page.getByTestId('sidebar-locale-picker')
|
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'})
|
this.closeButton = page.getByRole('button', {name: 'Close'})
|
||||||
}
|
}
|
||||||
|
|
||||||
async gotToHomePage() {
|
get sidebarAbout() {
|
||||||
await this.page.goto('/')
|
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() {
|
async clickAboutLink() {
|
||||||
await expect(this.aboutLink).toBeVisible()
|
await expect(this.sidebarAbout).toBeVisible()
|
||||||
await this.aboutLink.click()
|
await this.sidebarAbout.click()
|
||||||
}
|
}
|
||||||
|
|
||||||
async clickFaqLink() {
|
async clickFaqLink() {
|
||||||
await expect(this.faqLink).toBeVisible()
|
await expect(this.sidebarFaq).toBeVisible()
|
||||||
await this.faqLink.click()
|
await this.sidebarFaq.click()
|
||||||
}
|
}
|
||||||
|
|
||||||
async clickVoteLink() {
|
async clickVoteLink() {
|
||||||
await expect(this.voteLink).toBeVisible()
|
await expect(this.sidebarVote).toBeVisible()
|
||||||
await this.voteLink.click()
|
await this.sidebarVote.click()
|
||||||
}
|
}
|
||||||
|
|
||||||
async clickEventsLink() {
|
async clickEventsLink() {
|
||||||
await expect(this.eventsLink).toBeVisible()
|
await expect(this.sidebarEvents).toBeVisible()
|
||||||
await this.eventsLink.click()
|
await this.sidebarEvents.click()
|
||||||
}
|
}
|
||||||
|
|
||||||
async clickWhatsNewLink() {
|
async clickWhatsNewLink() {
|
||||||
await expect(this.whatsNewLink).toBeVisible()
|
await expect(this.sidebarWhatsNew).toBeVisible()
|
||||||
await this.whatsNewLink.click()
|
await this.sidebarWhatsNew.click()
|
||||||
}
|
}
|
||||||
|
|
||||||
async clickSocialsLink() {
|
async clickSocialsLink() {
|
||||||
await expect(this.socialsLink).toBeVisible()
|
await expect(this.sidebarSocials).toBeVisible()
|
||||||
await this.socialsLink.click()
|
await this.sidebarSocials.click()
|
||||||
}
|
}
|
||||||
|
|
||||||
async clickOrganizationLink() {
|
async clickOrganizationLink() {
|
||||||
await expect(this.organizationLink).toBeVisible()
|
await expect(this.sidebarOrganization).toBeVisible()
|
||||||
await this.organizationLink.click()
|
await this.sidebarOrganization.click()
|
||||||
}
|
}
|
||||||
|
|
||||||
async clickContactLink() {
|
async clickContactLink() {
|
||||||
await expect(this.contactLink).toBeVisible()
|
await expect(this.sidebarContact).toBeVisible()
|
||||||
await this.contactLink.click()
|
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() {
|
async clickSignUpButton() {
|
||||||
@@ -88,7 +154,44 @@ export class HomePage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async clickSignInLink() {
|
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 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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
134
tests/e2e/web/pages/organizationPage.ts
Normal file
134
tests/e2e/web/pages/organizationPage.ts
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
159
tests/e2e/web/pages/settingsPage.ts
Normal file
159
tests/e2e/web/pages/settingsPage.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
113
tests/e2e/web/pages/socialPage.ts
Normal file
113
tests/e2e/web/pages/socialPage.ts
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import {userInformationFromDb} from '../../utils/databaseUtils'
|
import {userInformationFromDb} from '../../utils/databaseUtils'
|
||||||
import {progressToRequiredForm} from '../utils/testCleanupHelpers'
|
|
||||||
import {expect, test} from '../fixtures/base'
|
import {expect, test} from '../fixtures/base'
|
||||||
|
import {registerWithEmail, skipOnboardingHeadToProfile} from '../utils/testCleanupHelpers'
|
||||||
|
|
||||||
test.describe('when given valid input', () => {
|
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,
|
homePage,
|
||||||
onboardingPage,
|
onboardingPage,
|
||||||
signUpPage,
|
signUpPage,
|
||||||
@@ -11,14 +11,7 @@ test.describe('when given valid input', () => {
|
|||||||
profilePage,
|
profilePage,
|
||||||
onboardingAccount,
|
onboardingAccount,
|
||||||
}) => {
|
}) => {
|
||||||
console.log(
|
await registerWithEmail(homePage, authPage, onboardingAccount)
|
||||||
`Starting "should successfully complete the onboarding flow" with ${onboardingAccount.username}`,
|
|
||||||
)
|
|
||||||
await homePage.gotToHomePage()
|
|
||||||
await homePage.clickSignUpButton()
|
|
||||||
await authPage.fillEmailField(onboardingAccount.email)
|
|
||||||
await authPage.fillPasswordField(onboardingAccount.password)
|
|
||||||
await authPage.clickSignUpWithEmailButton()
|
|
||||||
await onboardingPage.clickContinueButton() //First continue
|
await onboardingPage.clickContinueButton() //First continue
|
||||||
await onboardingPage.clickContinueButton() //Second continue
|
await onboardingPage.clickContinueButton() //Second continue
|
||||||
await onboardingPage.clickGetStartedButton()
|
await onboardingPage.clickGetStartedButton()
|
||||||
@@ -225,6 +218,35 @@ test.describe('when given valid input', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('should successfully complete the onboarding flow with google account', async ({
|
||||||
|
homePage,
|
||||||
|
onboardingPage,
|
||||||
|
signUpPage,
|
||||||
|
authPage,
|
||||||
|
profilePage,
|
||||||
|
googleAccountOne,
|
||||||
|
headless,
|
||||||
|
}) => {
|
||||||
|
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 ({
|
test('should successfully skip the onboarding flow', async ({
|
||||||
homePage,
|
homePage,
|
||||||
onboardingPage,
|
onboardingPage,
|
||||||
@@ -233,16 +255,8 @@ test.describe('when given valid input', () => {
|
|||||||
profilePage,
|
profilePage,
|
||||||
fakerAccount,
|
fakerAccount,
|
||||||
}) => {
|
}) => {
|
||||||
console.log(
|
await registerWithEmail(homePage, authPage, fakerAccount)
|
||||||
`Starting "should successfully skip the onboarding flow" with ${fakerAccount.username}`,
|
await skipOnboardingHeadToProfile(onboardingPage, signUpPage, profilePage, fakerAccount)
|
||||||
)
|
|
||||||
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()
|
|
||||||
|
|
||||||
//Verify displayed information is correct
|
//Verify displayed information is correct
|
||||||
await profilePage.verifyDisplayName(fakerAccount.display_name)
|
await profilePage.verifyDisplayName(fakerAccount.display_name)
|
||||||
@@ -262,16 +276,8 @@ test.describe('when given valid input', () => {
|
|||||||
profilePage,
|
profilePage,
|
||||||
fakerAccount,
|
fakerAccount,
|
||||||
}) => {
|
}) => {
|
||||||
console.log(
|
await registerWithEmail(homePage, authPage, fakerAccount)
|
||||||
`Starting "should successfully enter optional information after completing flow" with ${fakerAccount.username}`,
|
await skipOnboardingHeadToProfile(onboardingPage, signUpPage, profilePage, fakerAccount)
|
||||||
)
|
|
||||||
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 profilePage.clickEditProfileButton()
|
await profilePage.clickEditProfileButton()
|
||||||
await signUpPage.chooseGender(fakerAccount.gender)
|
await signUpPage.chooseGender(fakerAccount.gender)
|
||||||
await signUpPage.fillAge(fakerAccount.age)
|
await signUpPage.fillAge(fakerAccount.age)
|
||||||
@@ -310,10 +316,8 @@ test.describe('when given valid input', () => {
|
|||||||
fakerAccount,
|
fakerAccount,
|
||||||
onboardingAccount,
|
onboardingAccount,
|
||||||
}) => {
|
}) => {
|
||||||
console.log(
|
await registerWithEmail(homePage, authPage, fakerAccount)
|
||||||
`Starting "should successfully use the start answering option" with ${fakerAccount.username}`,
|
await onboardingPage.clickSkipOnboardingButton()
|
||||||
)
|
|
||||||
await progressToRequiredForm(homePage, authPage, fakerAccount, onboardingPage)
|
|
||||||
await signUpPage.fillDisplayName(fakerAccount.display_name)
|
await signUpPage.fillDisplayName(fakerAccount.display_name)
|
||||||
await signUpPage.fillUsername(fakerAccount.username)
|
await signUpPage.fillUsername(fakerAccount.username)
|
||||||
await signUpPage.clickNextButton()
|
await signUpPage.clickNextButton()
|
||||||
@@ -338,22 +342,15 @@ test.describe('when given valid input', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test.describe('should successfully complete the onboarding flow after using the back button', () => {
|
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 ({
|
test("the first time it's an option", async ({
|
||||||
|
homePage,
|
||||||
|
authPage,
|
||||||
onboardingPage,
|
onboardingPage,
|
||||||
signUpPage,
|
signUpPage,
|
||||||
profilePage,
|
profilePage,
|
||||||
fakerAccount,
|
fakerAccount,
|
||||||
}) => {
|
}) => {
|
||||||
console.log(`Starting "the first time its an option" with ${fakerAccount.username}`)
|
await registerWithEmail(homePage, authPage, fakerAccount)
|
||||||
await onboardingPage.clickContinueButton()
|
await onboardingPage.clickContinueButton()
|
||||||
await onboardingPage.clickBackButton()
|
await onboardingPage.clickBackButton()
|
||||||
await onboardingPage.clickContinueButton()
|
await onboardingPage.clickContinueButton()
|
||||||
@@ -377,12 +374,14 @@ test.describe('when given valid input', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("the second time it's an option", async ({
|
test("the second time it's an option", async ({
|
||||||
|
homePage,
|
||||||
|
authPage,
|
||||||
onboardingPage,
|
onboardingPage,
|
||||||
signUpPage,
|
signUpPage,
|
||||||
profilePage,
|
profilePage,
|
||||||
fakerAccount,
|
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.clickContinueButton()
|
await onboardingPage.clickContinueButton()
|
||||||
await onboardingPage.clickBackButton()
|
await onboardingPage.clickBackButton()
|
||||||
@@ -407,6 +406,6 @@ test.describe('when given valid input', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test.describe('when an error occurs', () => {
|
// test.describe('when an error occurs', () => {
|
||||||
test('placeholder', async () => {})
|
// test('placeholder', async ({}) => {})
|
||||||
})
|
// })
|
||||||
|
|||||||
@@ -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()
|
|
||||||
})
|
|
||||||
108
tests/e2e/web/specs/signIn.spec.ts
Normal file
108
tests/e2e/web/specs/signIn.spec.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
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,
|
||||||
|
}) => {
|
||||||
|
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,
|
||||||
|
}) => {
|
||||||
|
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,
|
||||||
|
}) => {
|
||||||
|
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,
|
||||||
|
}) => {
|
||||||
|
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()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import {expect, test} from '../fixtures/base'
|
import {expect, test} from '../fixtures/base'
|
||||||
import {progressToRequiredForm} from '../utils/testCleanupHelpers'
|
import {registerWithEmail} from '../utils/testCleanupHelpers'
|
||||||
|
|
||||||
test.describe('when given valid input', () => {
|
// test.describe('when given valid input', () => {
|
||||||
test('placeholder', async () => {})
|
// test('placeholder', async () => {})
|
||||||
})
|
// })
|
||||||
|
|
||||||
test.describe('when an error occurs', () => {
|
test.describe('when an error occurs', () => {
|
||||||
test('should disable the button "Next" when the display name field is empty', async ({
|
test('should disable the button "Next" when the display name field is empty', async ({
|
||||||
@@ -13,7 +13,8 @@ test.describe('when an error occurs', () => {
|
|||||||
onboardingPage,
|
onboardingPage,
|
||||||
signUpPage,
|
signUpPage,
|
||||||
}) => {
|
}) => {
|
||||||
await progressToRequiredForm(homePage, authPage, specAccount, onboardingPage)
|
await registerWithEmail(homePage, authPage, specAccount)
|
||||||
|
await onboardingPage.clickSkipOnboardingButton()
|
||||||
await signUpPage.fillDisplayName('')
|
await signUpPage.fillDisplayName('')
|
||||||
await signUpPage.fillUsername(specAccount.username)
|
await signUpPage.fillUsername(specAccount.username)
|
||||||
await signUpPage.verifyDisplayNameError()
|
await signUpPage.verifyDisplayNameError()
|
||||||
@@ -27,7 +28,8 @@ test.describe('when an error occurs', () => {
|
|||||||
onboardingPage,
|
onboardingPage,
|
||||||
signUpPage,
|
signUpPage,
|
||||||
}) => {
|
}) => {
|
||||||
await progressToRequiredForm(homePage, authPage, specAccount, onboardingPage)
|
await registerWithEmail(homePage, authPage, specAccount)
|
||||||
|
await onboardingPage.clickSkipOnboardingButton()
|
||||||
await signUpPage.fillDisplayName(specAccount.display_name)
|
await signUpPage.fillDisplayName(specAccount.display_name)
|
||||||
await signUpPage.fillUsername('')
|
await signUpPage.fillUsername('')
|
||||||
await signUpPage.verifyUsernameError()
|
await signUpPage.verifyUsernameError()
|
||||||
|
|||||||
@@ -102,7 +102,9 @@ type AccountConfig = {
|
|||||||
spec_account: () => UserAccountInformation
|
spec_account: () => UserAccountInformation
|
||||||
dev_one_account: () => UserAccountInformation
|
dev_one_account: () => UserAccountInformation
|
||||||
dev_two_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 = {
|
export const testAccounts: AccountConfig = {
|
||||||
@@ -137,10 +139,10 @@ export const testAccounts: AccountConfig = {
|
|||||||
dev_one_account: () => {
|
dev_one_account: () => {
|
||||||
const id = crypto.randomUUID().slice(0, 6)
|
const id = crypto.randomUUID().slice(0, 6)
|
||||||
return {
|
return {
|
||||||
email: `dev_1_${id}@compass.com`,
|
email: `dev_1@compass.com`,
|
||||||
password: 'dev_1Password',
|
password: 'dev_1Password',
|
||||||
display_name: 'Dev1.Compass',
|
display_name: 'Dev1.Compass',
|
||||||
username: `Dev1.Connections_${id}`,
|
username: `Dev1.Connections`,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -150,11 +152,31 @@ export const testAccounts: AccountConfig = {
|
|||||||
email: 'dev_2@compass.com',
|
email: 'dev_2@compass.com',
|
||||||
password: 'dev_2Password',
|
password: 'dev_2Password',
|
||||||
display_name: 'Dev2.Compass',
|
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)
|
const id = crypto.randomUUID().slice(0, 6)
|
||||||
return {
|
return {
|
||||||
// Use a non-real TLD like @test.compass to make it obvious these are test accounts and prevent accidental emails
|
// Use a non-real TLD like @test.compass to make it obvious these are test accounts and prevent accidental emails
|
||||||
|
|||||||
@@ -1,18 +1,31 @@
|
|||||||
import {deleteFromDb} from '../../utils/databaseUtils'
|
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 {
|
try {
|
||||||
const loginInfo = await firebaseLogin(email, password)
|
let loginInfo
|
||||||
await deleteAccount(loginInfo)
|
if (authType === 'Email/Password') {
|
||||||
await deleteFromDb(loginInfo.data.localId)
|
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) {
|
} catch (err: any) {
|
||||||
// Skip deletion if user doesn't exist or other auth errors occur
|
// Skip deletion if user doesn't exist or other auth errors occur
|
||||||
if (
|
if (
|
||||||
err.response?.status === 400 ||
|
err.response?.status === 400 ||
|
||||||
err.response?.data?.error?.message?.includes('EMAIL_NOT_FOUND')
|
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
|
return
|
||||||
}
|
}
|
||||||
console.log(err)
|
console.log(err)
|
||||||
|
|||||||
25
tests/e2e/web/utils/networkUtils.ts
Normal file
25
tests/e2e/web/utils/networkUtils.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,64 @@
|
|||||||
import {AuthPage} from '../pages/AuthPage'
|
import {AuthPage} from '../pages/AuthPage'
|
||||||
import {HomePage} from '../pages/homePage'
|
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'
|
import {UserAccountInformation} from '../utils/accountInformation'
|
||||||
|
|
||||||
export async function progressToRequiredForm(
|
export async function registerWithEmail(
|
||||||
homePage: HomePage,
|
homePage: HomePage,
|
||||||
authPage: AuthPage,
|
authPage: AuthPage,
|
||||||
account: UserAccountInformation,
|
account: UserAccountInformation,
|
||||||
onboardingPage: OnboardingPage,
|
|
||||||
) {
|
) {
|
||||||
await homePage.gotToHomePage()
|
await homePage.goToRegisterPage()
|
||||||
await homePage.clickSignUpButton()
|
|
||||||
await authPage.fillEmailField(account.email)
|
await authPage.fillEmailField(account.email)
|
||||||
await authPage.fillPasswordField(account.password)
|
await authPage.fillPasswordField(account.password)
|
||||||
await authPage.clickSignUpWithEmailButton()
|
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()
|
||||||
}
|
}
|
||||||
@@ -67,6 +67,7 @@ export function ConnectionPreferencesSettings() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<SwitchSetting
|
<SwitchSetting
|
||||||
|
testId="settings-direct-message-toggle"
|
||||||
checked={allowDirectMessaging}
|
checked={allowDirectMessaging}
|
||||||
onChange={handleDirectMessagingChange}
|
onChange={handleDirectMessagingChange}
|
||||||
disabled={isUpdating}
|
disabled={isUpdating}
|
||||||
@@ -87,6 +88,7 @@ export function ConnectionPreferencesSettings() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<SwitchSetting
|
<SwitchSetting
|
||||||
|
testId="settings-private-interest-signal-toggle"
|
||||||
checked={allowInterestIndicating}
|
checked={allowInterestIndicating}
|
||||||
onChange={handleInterestIndicatingChange}
|
onChange={handleInterestIndicatingChange}
|
||||||
disabled={isUpdating}
|
disabled={isUpdating}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {QuestionMarkCircleIcon} from '@heroicons/react/24/outline'
|
|||||||
import {DisplayUser} from 'common/api/user-types'
|
import {DisplayUser} from 'common/api/user-types'
|
||||||
import {FilterFields} from 'common/filters'
|
import {FilterFields} from 'common/filters'
|
||||||
import {Profile} from 'common/profiles/profile'
|
import {Profile} from 'common/profiles/profile'
|
||||||
|
import {debounce as debounceFn} from 'lodash'
|
||||||
import {forwardRef, ReactElement, useEffect, useRef, useState} from 'react'
|
import {forwardRef, ReactElement, useEffect, useRef, useState} from 'react'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import {IoFilterSharp} from 'react-icons/io5'
|
import {IoFilterSharp} from 'react-icons/io5'
|
||||||
@@ -180,6 +181,22 @@ export const Search = forwardRef<
|
|||||||
const isClearedFilters = useIsClearedFilters(filters)
|
const isClearedFilters = useIsClearedFilters(filters)
|
||||||
const choices = useChoicesContext()
|
const choices = useChoicesContext()
|
||||||
|
|
||||||
|
const [keywordInput, setKeywordInput] = useState(filters.name ?? '')
|
||||||
|
|
||||||
|
const debouncedUpdateFilter = useRef(
|
||||||
|
debounceFn((value: string) => {
|
||||||
|
updateFilter({name: value || undefined})
|
||||||
|
}, 500),
|
||||||
|
).current
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
debouncedUpdateFilter(keywordInput)
|
||||||
|
}, [keywordInput, debouncedUpdateFilter])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setKeywordInput(filters.name ?? '')
|
||||||
|
}, [filters.name])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isHolding) return
|
if (isHolding) return
|
||||||
|
|
||||||
@@ -214,11 +231,11 @@ export const Search = forwardRef<
|
|||||||
<Row className={'mb-2 justify-between gap-2'}>
|
<Row className={'mb-2 justify-between gap-2'}>
|
||||||
<Input
|
<Input
|
||||||
ref={ref}
|
ref={ref}
|
||||||
value={filters.name ?? ''}
|
value={keywordInput}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
className={'w-full max-w-xs'}
|
className={'w-full max-w-xs'}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
updateFilter({name: e.target.value || undefined})
|
setKeywordInput(e.target.value)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export function FontPicker(props: {className?: string} = {}) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<select
|
<select
|
||||||
|
data-testid="settings-font-picker"
|
||||||
id="font-picker"
|
id="font-picker"
|
||||||
value={font}
|
value={font}
|
||||||
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => setFont(e.target.value as FontOption)}
|
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => setFont(e.target.value as FontOption)}
|
||||||
@@ -35,3 +36,8 @@ export function FontPicker(props: {className?: string} = {}) {
|
|||||||
</select>
|
</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]
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export default function MeasurementSystemToggle(props: {className?: string}) {
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<Switch
|
<Switch
|
||||||
|
data-testid="measurement-system-toggle"
|
||||||
checked={isEnabled}
|
checked={isEnabled}
|
||||||
onChange={(enabled: boolean) => setMeasurementSystem(enabled ? 'metric' : 'imperial')}
|
onChange={(enabled: boolean) => setMeasurementSystem(enabled ? 'metric' : 'imperial')}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export function ProfileSummary(props: {user: User; className?: string}) {
|
|||||||
<div className="w-2 shrink" />
|
<div className="w-2 shrink" />
|
||||||
<Avatar avatarUrl={profile?.pinned_url ?? ''} username={user.username} noLink />
|
<Avatar avatarUrl={profile?.pinned_url ?? ''} username={user.username} noLink />
|
||||||
<div className="mr-1 w-2 shrink-[2]" />
|
<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 className="group-hover:text-primary-700">{user.name}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-2 shrink" />
|
<div className="w-2 shrink" />
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ export function SidebarItem(props: {item: Item; currentPage?: string}) {
|
|||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={item.href}
|
href={item.href}
|
||||||
data-testid={`sidebar-${item.href?.replace('/', '')}`}
|
|
||||||
aria-current={isCurrentPage ? 'page' : undefined}
|
aria-current={isCurrentPage ? 'page' : undefined}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={sidebarClass}
|
className={sidebarClass}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export default function Sidebar(props: {
|
|||||||
<nav
|
<nav
|
||||||
id="main-navigation"
|
id="main-navigation"
|
||||||
aria-label="Sidebar"
|
aria-label="Sidebar"
|
||||||
|
data-testid="sidebar"
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'flex flex-col h-[calc(100dvh-var(--hloss))] mb-[calc(var(--bnh))] mt-[calc(var(--tnh))]',
|
'flex flex-col h-[calc(100dvh-var(--hloss))] mb-[calc(var(--bnh))] mt-[calc(var(--tnh))]',
|
||||||
className,
|
className,
|
||||||
@@ -119,7 +120,6 @@ export const SignUpButton = (props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
data-testid="side-bar-sign-up-button"
|
|
||||||
color={color ?? 'gradient'}
|
color={color ?? 'gradient'}
|
||||||
size={size ?? 'xl'}
|
size={size ?? 'xl'}
|
||||||
onClick={startSignup}
|
onClick={startSignup}
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export const ViewProfileCardButton = (props: {
|
|||||||
src={src}
|
src={src}
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
alt={t('profile_card.loading', 'Loading your card, it may take a few seconds...')}
|
alt={t('profile_card.loading', `${user.username}'s profile card`)}
|
||||||
className={'rounded-2xl'}
|
className={'rounded-2xl'}
|
||||||
/>
|
/>
|
||||||
<ShareProfileButtons
|
<ShareProfileButtons
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ export function DeleteAccountSurveyModal() {
|
|||||||
onSubmitWithSuccess={handleDeleteAccount}
|
onSubmitWithSuccess={handleDeleteAccount}
|
||||||
disabled={false}
|
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>
|
<Title>{t('delete_survey.title', 'Sorry to see you go')}</Title>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -157,7 +157,7 @@ export function DeleteAccountSurveyModal() {
|
|||||||
{t('delete_survey.reason_label', 'Why are you deleting your account?')}
|
{t('delete_survey.reason_label', 'Why are you deleting your account?')}
|
||||||
</RadioGroup.Label>
|
</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]) => (
|
{Object.entries(reasonsMap).map(([key, value]) => (
|
||||||
<RadioGroup.Option
|
<RadioGroup.Option
|
||||||
key={key}
|
key={key}
|
||||||
|
|||||||
@@ -49,7 +49,10 @@ export function HiddenProfilesModal(props: {open: boolean; setOpen: (open: boole
|
|||||||
{t('settings.hidden_profiles.title', "Profiles you've hidden")}
|
{t('settings.hidden_profiles.title', "Profiles you've hidden")}
|
||||||
</Title>
|
</Title>
|
||||||
{hiddenProfiles && hiddenProfiles.length > 0 && (
|
{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) => (
|
{hiddenProfiles.map((u) => (
|
||||||
<Row key={u.id} className="items-center justify-between py-2 gap-2">
|
<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}>
|
<Link className="w-full rounded-md hover:bg-canvas-100 p-2" href={'/' + u.username}>
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ export function SkipLink() {
|
|||||||
<>
|
<>
|
||||||
<a
|
<a
|
||||||
href="#main-content"
|
href="#main-content"
|
||||||
className="absolute -top-10 left-4 z-50 bg-primary-500 px-4 py-2 text-white transition-all focus:top-4"
|
className="absolute -top-10 left-4 z-50 bg-canvas-100 px-4 py-2 transition-all focus:top-4"
|
||||||
>
|
>
|
||||||
Skip to main content
|
Skip to main content
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="#main-navigation"
|
href="#main-navigation"
|
||||||
className="absolute -top-10 left-4 z-50 bg-primary-500 px-4 py-2 text-white transition-all focus:top-4"
|
className="absolute -top-10 left-4 z-50 bg-canvas-100 px-4 py-2 transition-all focus:top-4"
|
||||||
>
|
>
|
||||||
Skip to navigation
|
Skip to navigation
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -9,10 +9,11 @@ export const SwitchSetting = (props: {
|
|||||||
label?: 'Web' | 'Email' | 'Mobile'
|
label?: 'Web' | 'Email' | 'Mobile'
|
||||||
disabled: boolean
|
disabled: boolean
|
||||||
colorMode?: ToggleColorMode
|
colorMode?: ToggleColorMode
|
||||||
|
testId?: string
|
||||||
}) => {
|
}) => {
|
||||||
const {colorMode, checked, onChange, label, disabled} = props
|
const {colorMode, checked, onChange, label, disabled, testId} = props
|
||||||
return (
|
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} />
|
<ShortToggle colorMode={colorMode} on={checked} setOn={onChange} disabled={disabled} />
|
||||||
<Switch.Label
|
<Switch.Label
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
|||||||
@@ -31,7 +31,12 @@ export default function ThemeIcon(props: {className?: string}) {
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
<button onClick={toggleTheme} className={'w-fit'}>
|
<button
|
||||||
|
type={'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">
|
<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}
|
{icon}
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ export default function ShortToggle(props: {
|
|||||||
className?: string
|
className?: string
|
||||||
colorMode?: ToggleColorMode
|
colorMode?: ToggleColorMode
|
||||||
size?: 'sm'
|
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 =
|
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'
|
'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 (
|
return (
|
||||||
<Switch
|
<Switch
|
||||||
|
data-testid={testId}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
checked={on}
|
checked={on}
|
||||||
onChange={setOn}
|
onChange={setOn}
|
||||||
|
|||||||
@@ -6,13 +6,15 @@ import {
|
|||||||
} from 'common/profiles/profile'
|
} from 'common/profiles/profile'
|
||||||
import {Row} from 'common/supabase/utils'
|
import {Row} from 'common/supabase/utils'
|
||||||
import {User} from 'common/user'
|
import {User} from 'common/user'
|
||||||
import {useEffect} from 'react'
|
import {createContext, ReactNode, useContext, useEffect} from 'react'
|
||||||
import {usePersistentInMemoryState} from 'web/hooks/use-persistent-in-memory-state'
|
import {usePersistentInMemoryState} from 'web/hooks/use-persistent-in-memory-state'
|
||||||
import {usePersistentLocalState} from 'web/hooks/use-persistent-local-state'
|
import {usePersistentLocalState} from 'web/hooks/use-persistent-local-state'
|
||||||
import {useUser} from 'web/hooks/use-user'
|
import {useUser} from 'web/hooks/use-user'
|
||||||
import {db} from 'web/lib/supabase/db'
|
import {db} from 'web/lib/supabase/db'
|
||||||
|
|
||||||
export const useProfile = () => {
|
type OwnProfile = (Row<'profiles'> & {user: User}) | null | undefined
|
||||||
|
|
||||||
|
const useOwnProfile = (): OwnProfile => {
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
const [profile, setProfile] = usePersistentLocalState<Row<'profiles'> | undefined | null>(
|
const [profile, setProfile] = usePersistentLocalState<Row<'profiles'> | undefined | null>(
|
||||||
undefined,
|
undefined,
|
||||||
@@ -20,13 +22,11 @@ export const useProfile = () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const refreshProfile = () => {
|
const refreshProfile = () => {
|
||||||
if (user) {
|
if (!user?.id) return
|
||||||
// logger.debug('Refreshing profile in useProfile for', user?.username, profile);
|
debug('Refreshing own profile for', user.username)
|
||||||
getProfileRowWithFrontendSupabase(user.id, db).then((profile) => {
|
getProfileRowWithFrontendSupabase(user.id, db).then((p) => {
|
||||||
if (!profile) setProfile(null)
|
setProfile(p ?? null)
|
||||||
else setProfile(profile)
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -36,6 +36,21 @@ export const useProfile = () => {
|
|||||||
return user && profile ? {...profile, user} : profile === null ? null : undefined
|
return user && profile ? {...profile, user} : profile === null ? null : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MISSING = Symbol('missing')
|
||||||
|
|
||||||
|
const ProfileContext = createContext<OwnProfile | typeof MISSING>(MISSING)
|
||||||
|
|
||||||
|
export const useProfile = () => {
|
||||||
|
const ctx = useContext(ProfileContext)
|
||||||
|
if (ctx === MISSING) throw new Error('useProfile must be used within a ProfileProvider')
|
||||||
|
return ctx as OwnProfile
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProfileProvider = ({children}: {children: ReactNode}) => {
|
||||||
|
const profile = useOwnProfile()
|
||||||
|
return <ProfileContext.Provider value={profile}>{children}</ProfileContext.Provider>
|
||||||
|
}
|
||||||
|
|
||||||
export const useProfileByUser = (user: User | undefined) => {
|
export const useProfileByUser = (user: User | undefined) => {
|
||||||
const userId = user?.id
|
const userId = user?.id
|
||||||
const [profile, setProfile] = usePersistentInMemoryState<Profile | undefined | null>(
|
const [profile, setProfile] = usePersistentInMemoryState<Profile | undefined | null>(
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import {PushNotifications} from '@capacitor/push-notifications'
|
import {PushNotifications} from '@capacitor/push-notifications'
|
||||||
import {debug} from 'common/logger'
|
import {debug} from 'common/logger'
|
||||||
import {useRouter} from 'next/router'
|
|
||||||
import {useEffect} from 'react'
|
import {useEffect} from 'react'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
import {useUser} from 'web/hooks/use-user'
|
import {useUser} from 'web/hooks/use-user'
|
||||||
import {api} from 'web/lib/api'
|
import {api} from 'web/lib/api'
|
||||||
import {isAndroidApp} from 'web/lib/util/webview'
|
import {isAndroidApp} from 'web/lib/util/webview'
|
||||||
@@ -9,7 +9,6 @@ import {isAndroidApp} from 'web/lib/util/webview'
|
|||||||
export default function AndroidPush() {
|
export default function AndroidPush() {
|
||||||
const user = useUser() // authenticated user
|
const user = useUser() // authenticated user
|
||||||
const isAndroid = isAndroidApp()
|
const isAndroid = isAndroidApp()
|
||||||
const router = useRouter()
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!user?.id || !isAndroid) return
|
if (!user?.id || !isAndroid) return
|
||||||
debug('AndroidPush', user)
|
debug('AndroidPush', user)
|
||||||
@@ -36,12 +35,14 @@ export default function AndroidPush() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
PushNotifications.addListener('pushNotificationReceived', (notif) => {
|
PushNotifications.addListener('pushNotificationReceived', (notif) => {
|
||||||
debug('Push received', notif)
|
console.debug('Push received', notif, window.location.pathname)
|
||||||
const url = notif?.data?.url
|
const endpoint = notif?.data?.endpoint as string
|
||||||
if (url) {
|
if (!endpoint) return
|
||||||
router.push(url)
|
if (!endpoint.startsWith('/messages/')) return
|
||||||
window.location.href = url
|
if (endpoint === window.location.pathname) return
|
||||||
}
|
const author = notif?.title
|
||||||
|
const message = notif?.body
|
||||||
|
toast.success(`${author}: "${message}"`)
|
||||||
})
|
})
|
||||||
}, [user?.id, isAndroid])
|
}, [user?.id, isAndroid])
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ const nextConfig: NextConfig = {
|
|||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
{hostname: 'martinbraquet.com'},
|
{hostname: 'martinbraquet.com'},
|
||||||
{hostname: 'compassmeet.com'},
|
{hostname: 'compassmeet.com'},
|
||||||
|
{hostname: 'www.compassmeet.com'},
|
||||||
{hostname: 'lh3.googleusercontent.com'},
|
{hostname: 'lh3.googleusercontent.com'},
|
||||||
{hostname: 'i.imgur.com'},
|
{hostname: 'i.imgur.com'},
|
||||||
{hostname: 'firebasestorage.googleapis.com'},
|
{hostname: 'firebasestorage.googleapis.com'},
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import {App} from '@capacitor/app'
|
|||||||
import {Capacitor} from '@capacitor/core'
|
import {Capacitor} from '@capacitor/core'
|
||||||
import {Keyboard} from '@capacitor/keyboard'
|
import {Keyboard} from '@capacitor/keyboard'
|
||||||
import {StatusBar} from '@capacitor/status-bar'
|
import {StatusBar} from '@capacitor/status-bar'
|
||||||
|
import * as Sentry from '@sentry/node'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import {DEPLOYED_WEB_URL} from 'common/envs/constants'
|
import {DEPLOYED_WEB_URL} from 'common/envs/constants'
|
||||||
import {IS_VERCEL, PNG_FAVICON} from 'common/hosting/constants'
|
import {IS_VERCEL, PNG_FAVICON} from 'common/hosting/constants'
|
||||||
import {debug} from 'common/logger'
|
import {debug} from 'common/logger'
|
||||||
|
import {isUrl} from 'common/parsing'
|
||||||
import type {AppProps} from 'next/app'
|
import type {AppProps} from 'next/app'
|
||||||
import {Major_Mono_Display} from 'next/font/google'
|
import {Major_Mono_Display} from 'next/font/google'
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
@@ -25,6 +27,7 @@ import {useFontPreferenceManager} from 'web/hooks/use-font-preference'
|
|||||||
import {useHasLoaded} from 'web/hooks/use-has-loaded'
|
import {useHasLoaded} from 'web/hooks/use-has-loaded'
|
||||||
import {HiddenProfilesProvider} from 'web/hooks/use-hidden-profiles'
|
import {HiddenProfilesProvider} from 'web/hooks/use-hidden-profiles'
|
||||||
import {PinnedQuestionIdsProvider} from 'web/hooks/use-pinned-question-ids'
|
import {PinnedQuestionIdsProvider} from 'web/hooks/use-pinned-question-ids'
|
||||||
|
import {ProfileProvider} from 'web/hooks/use-profile'
|
||||||
import {updateStatusBar} from 'web/hooks/use-theme'
|
import {updateStatusBar} from 'web/hooks/use-theme'
|
||||||
import {updateBackendLocale} from 'web/lib/api'
|
import {updateBackendLocale} from 'web/lib/api'
|
||||||
import {DAYJS_LOCALE_IMPORTS, registerDatePickerLocale} from 'web/lib/dayjs'
|
import {DAYJS_LOCALE_IMPORTS, registerDatePickerLocale} from 'web/lib/dayjs'
|
||||||
@@ -162,10 +165,23 @@ function MyApp(props: AppProps<PageProps>) {
|
|||||||
|
|
||||||
const link = window.AndroidBridge?.getPendingDeepLink?.()
|
const link = window.AndroidBridge?.getPendingDeepLink?.()
|
||||||
if (link) {
|
if (link) {
|
||||||
handleAppLink({url: link, endpoint: new URL(link).pathname})
|
handleAppLink({endpoint: isUrl(link) ? new URL(link).pathname : link})
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchAppInfo = async () => {
|
||||||
|
if (!Capacitor.isNativePlatform()) return
|
||||||
|
const appInfo = await App.getInfo().catch((e) => debug('Could not load Android app info:', e))
|
||||||
|
const appVersion = appInfo?.version
|
||||||
|
if (appVersion) {
|
||||||
|
Sentry.setTag('androidApp.version', appVersion)
|
||||||
|
Sentry.setTag('androidApp.buildNumber', appInfo?.build)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchAppInfo()
|
||||||
|
}, [])
|
||||||
|
|
||||||
const title = 'Compass'
|
const title = 'Compass'
|
||||||
const description = 'The platform for intentional connections'
|
const description = 'The platform for intentional connections'
|
||||||
|
|
||||||
@@ -208,15 +224,17 @@ function MyApp(props: AppProps<PageProps>) {
|
|||||||
<I18nContext.Provider value={{locale, setLocale}}>
|
<I18nContext.Provider value={{locale, setLocale}}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<AuthProvider serverUser={pageProps.auth}>
|
<AuthProvider serverUser={pageProps.auth}>
|
||||||
<ChoicesProvider>
|
<ProfileProvider>
|
||||||
<PinnedQuestionIdsProvider>
|
<ChoicesProvider>
|
||||||
<HiddenProfilesProvider>
|
<PinnedQuestionIdsProvider>
|
||||||
<WebPush />
|
<HiddenProfilesProvider>
|
||||||
<AndroidPush />
|
<WebPush />
|
||||||
<Component {...pageProps} />
|
<AndroidPush />
|
||||||
</HiddenProfilesProvider>
|
<Component {...pageProps} />
|
||||||
</PinnedQuestionIdsProvider>
|
</HiddenProfilesProvider>
|
||||||
</ChoicesProvider>
|
</PinnedQuestionIdsProvider>
|
||||||
|
</ChoicesProvider>
|
||||||
|
</ProfileProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</I18nContext.Provider>
|
</I18nContext.Provider>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"package_name": "com.compassconnections.app",
|
"package_name": "com.compassconnections.app",
|
||||||
"sha256_cert_fingerprints": [
|
"sha256_cert_fingerprints": [
|
||||||
"29:61:EB:C6:DE:D4:E3:50:FE:8B:56:D2:B5:F1:E7:1D:8D:6F:96:82:26:05:63:5B:44:BB:56:45:B1:D7:6A:EE",
|
"29:61:EB:C6:DE:D4:E3:50:FE:8B:56:D2:B5:F1:E7:1D:8D:6F:96:82:26:05:63:5B:44:BB:56:45:B1:D7:6A:EE",
|
||||||
|
"4B:80:99:49:D0:30:6F:8F:3C:D0:E7:BB:F6:39:CA:7B:99:A3:17:20:96:F9:4E:1F:13:CC:81:90:54:65:51:3B",
|
||||||
"9D:88:96:66:81:E9:C5:82:5E:96:6C:B7:C4:A0:0C:AA:26:1B:0A:04:F8:7D:9D:F2:4D:C4:3E:02:99:A3:42:DF"
|
"9D:88:96:66:81:E9:C5:82:5E:96:6C:B7:C4:A0:0C:AA:26:1B:0A:04:F8:7D:9D:F2:4D:C4:3E:02:99:A3:42:DF"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user