diff --git a/.github/workflows/cd-android-live-update.yml b/.github/workflows/cd-android-live-update.yml index 9d4c2a64..2114750e 100644 --- a/.github/workflows/cd-android-live-update.yml +++ b/.github/workflows/cd-android-live-update.yml @@ -1,7 +1,7 @@ name: CD Android Live Update on: push: - branches: [ main, master ] + branches: [main, master] paths: - 'android/capawesome.json' - '.github/workflows/cd-android-live-update.yml' @@ -73,4 +73,4 @@ jobs: CAPAWESOME_TOKEN: ${{ secrets.CAPAWESOME_TOKEN }} commitRef: ${{ github.head_ref || github.ref_name }} commitSha: ${{ github.sha }} - run: yarn android-live-update \ No newline at end of file + run: yarn android-live-update diff --git a/.github/workflows/cd-android.yml b/.github/workflows/cd-android.yml index 64d8250c..dcac1e90 100644 --- a/.github/workflows/cd-android.yml +++ b/.github/workflows/cd-android.yml @@ -94,4 +94,4 @@ jobs: serviceAccountJsonPlainText: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }} packageName: com.compassconnections.app releaseFiles: android/app/build/outputs/bundle/release/app-release.aab - track: internal \ No newline at end of file + track: internal diff --git a/.github/workflows/cd-api.yml b/.github/workflows/cd-api.yml index e5436a92..f1655e91 100644 --- a/.github/workflows/cd-api.yml +++ b/.github/workflows/cd-api.yml @@ -1,7 +1,7 @@ name: API Release on: push: - branches: [ main, master ] + branches: [main, master] paths: - 'backend/api/package.json' - '.github/workflows/cd-api.yml' @@ -92,4 +92,4 @@ jobs: - name: Run deploy script run: | chmod +x backend/api/deploy-api.sh - backend/api/deploy-api.sh \ No newline at end of file + backend/api/deploy-api.sh diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index fd0b8a0a..24c1ba7f 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -4,7 +4,7 @@ name: GitHub Release on: push: - branches: [ main, master ] + branches: [main, master] paths: - 'package.json' - '.github/workflows/cd.yml' diff --git a/.github/workflows/ci-e2e.yml b/.github/workflows/ci-e2e.yml index 56d215b5..22474414 100644 --- a/.github/workflows/ci-e2e.yml +++ b/.github/workflows/ci-e2e.yml @@ -16,6 +16,31 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Cache Firebase emulators + uses: actions/cache@v4 + with: + path: ~/.cache/firebase/emulators + key: firebase-emulators-${{ hashFiles('firebase.json') }} + restore-keys: firebase-emulators- + + # - name: Cache Docker layers + # uses: ScribeMD/docker-cache@0.5.0 + # with: + # key: docker-${{ runner.os }}-${{ hashFiles('supabase/config.toml') }} + + - name: Cache node_modules + id: cache-node-modules + uses: actions/cache@v4 + with: + path: node_modules + key: node-modules-${{ hashFiles('yarn.lock') }} + + - name: Cache Playwright browsers + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ hashFiles('package.json') }} + - name: Setup Node.js uses: actions/setup-node@v4 with: @@ -28,22 +53,40 @@ jobs: distribution: 'temurin' java-version: '21' # Required for firebase-tools@15+ - - name: Setup Supabase CLI - uses: supabase/setup-cli@v1 - with: - version: latest - - name: Install dependencies run: yarn install --frozen-lockfile + env: + HUSKY: 0 - name: Install Playwright browsers run: npx playwright install chromium --with-deps - - name: Start Supabase # optional step, as if supabase is stopped, it would be started in test:e2e, but done here to avoid cluttering the logs of the E2E tests - run: | - echo "::group::Supabase start (docker pull logs)" - npx supabase start - echo "::endgroup::" + # Docker load from cache is actually slower than pulling the images every time with supabase start + # - name: Cache Supabase Docker images + # uses: actions/cache@v4 + # with: + # path: /tmp/supabase-docker + # key: supabase-docker-${{ hashFiles('supabase/config.toml') }} + # + # - name: Load cached Docker images + # run: | + # if [ -d /tmp/supabase-docker ]; then + # for tar in /tmp/supabase-docker/*.tar; do + # [ -f "$tar" ] && docker load -i "$tar" + # done + # fi + + - name: Start Supabase + run: ./scripts/supabase_start.sh + + # - name: Save Supabase Docker images + # run: | + # mkdir -p /tmp/supabase-docker + # docker images --format '{{.Repository}}:{{.Tag}}' \ + # | grep -E 'supabase|postgrest|gotrue|realtime|storage' \ + # | grep -v '' \ + # | xargs -I{} sh -c \ + # 'docker save {} -o /tmp/supabase-docker/$(echo {} | tr "/:" "--").tar' - name: Run E2E tests env: @@ -58,7 +101,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: playwright-report - path: playwright-report/ + path: tests/reports/playwright-report/ retention-days: 7 - name: Upload test results @@ -68,3 +111,4 @@ jobs: name: test-results path: test-results/ retention-days: 7 + if-no-files-found: ignore diff --git a/backend/shared/src/firebase-utils.ts b/backend/shared/src/firebase-utils.ts index 5b65f546..ceb0a4d4 100644 --- a/backend/shared/src/firebase-utils.ts +++ b/backend/shared/src/firebase-utils.ts @@ -21,7 +21,8 @@ export const getServiceAccountCredentials = () => { try { return JSON.parse(readFileSync(keyPath, {encoding: 'utf8'})) } catch (e) { - throw new Error(`Failed to load service account key from ${keyPath}: ${e}`) + if (!process.env.NEXT_PUBLIC_ISOLATED_ENV && !process.env.FIREBASE_AUTH_EMULATOR_HOST) + throw new Error(`Failed to load service account key from ${keyPath}: ${e}`) } } diff --git a/backend/shared/src/supabase/init.ts b/backend/shared/src/supabase/init.ts index 774204f6..0d27cdff 100644 --- a/backend/shared/src/supabase/init.ts +++ b/backend/shared/src/supabase/init.ts @@ -60,6 +60,14 @@ const newClient = ( console.log('Creating Supabase direct client') } + if (!databaseUrl && process.env.NEXT_PUBLIC_ISOLATED_ENV) { + throw new Error( + `You are running isolated tests (NEXT_PUBLIC_ISOLATED_ENV=true), so you do not want to call the remote supabase. ${{ + databaseUrl, + }}`, + ) + } + const config: any = databaseUrl ? { // Use connection string for local/dev Postgres diff --git a/common/src/choices.ts b/common/src/choices.ts index e1c7c3e5..8507f180 100644 --- a/common/src/choices.ts +++ b/common/src/choices.ts @@ -247,3 +247,15 @@ export const INVERTED_RACE_CHOICES = invert(RACE_CHOICES) export const INVERTED_MBTI_CHOICES = invert(MBTI_CHOICES) export const INVERTED_GENDERS = invert(GENDERS) export const INVERTED_LAST_ONLINE_CHOICES = invert(LAST_ONLINE_CHOICES) + +//Exported types for test files to use when referencing the keys of the choices objects +export type ConnectionTypeKey = keyof typeof RELATIONSHIP_CHOICES +export type RelationshipStatusKey = keyof typeof RELATIONSHIP_STATUS_CHOICES +export type RelationshipStyleKey = keyof typeof ROMANTIC_CHOICES +export type PoliticalBeliefsKey = keyof typeof POLITICAL_CHOICES +export type DietKey = keyof typeof DIET_CHOICES +export type EducationKey = keyof typeof EDUCATION_CHOICES +export type ReligionKey = keyof typeof RELIGION_CHOICES +export type LanguageKey = keyof typeof LANGUAGE_CHOICES +export type EthnicityKey = keyof typeof RACE_CHOICES +export type PersonalityKey = keyof typeof MBTI_CHOICES diff --git a/common/src/supabase/utils.ts b/common/src/supabase/utils.ts index e0463451..d6080565 100644 --- a/common/src/supabase/utils.ts +++ b/common/src/supabase/utils.ts @@ -29,6 +29,7 @@ export function createClient( key: string, opts?: SupabaseClientOptionsGeneric<'public'>, ) { + if (key === 'null') throw new Error(`Invalid supabase anon key: ${key}`) // Allow passing a full Supabase URL directly (e.g., http://localhost:54321) const url = /:\/\//.test(instanceIdOrUrl) ? instanceIdOrUrl diff --git a/package.json b/package.json index a6aa48a4..6c863ea7 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "test:e2e:ui": "./scripts/e2e.sh --ui", "test:e2e:debug": "./scripts/e2e.sh --debug", "test:e2e:services": "./scripts/e2e_services.sh", - "test:db:reset": "npx supabase start && ./scripts/combine-migrations.sh && npx supabase db reset && yarn test:db:seed", + "test:db:reset": "./scripts/test_db_reset.sh", "test:db:reset-postgres": "docker compose -f scripts/docker-compose.test.yml down -v && docker compose -f scripts/docker-compose.test.yml up -d", "test:db:migrate": "./scripts/test_db_migration.sh", "test:db:seed": "./scripts/seed.sh", diff --git a/playwright.config.ts b/playwright.config.ts index d0b296c4..cbdc6cf2 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,15 +1,47 @@ import {defineConfig, devices} from '@playwright/test' +import {execSync} from 'child_process' +import {config} from 'dotenv' + +// Load static env vars from .env.test +config({path: '.env.test', quiet: true}) + +// Dynamically pull Supabase vars +function getSupabaseEnv() { + try { + const raw = execSync('npx supabase status --output json 2>/dev/null', {encoding: 'utf-8'}) + const status = JSON.parse(raw) + if (!status.API_URL) throw new Error('No supabase API URL') + if (!status.ANON_KEY) throw new Error('No supabase ANON_KEY') + if (!status.DB_URL) throw new Error('No supabase DB_URL') + return { + NEXT_PUBLIC_SUPABASE_URL: status.API_URL, + NEXT_PUBLIC_SUPABASE_ANON_KEY: status.ANON_KEY, + DATABASE_URL: status.DB_URL, + } + } catch (e) { + console.error(e) + throw new Error('Failed to get Supabase status. Is it running?') + } +} + +const supabaseEnv = getSupabaseEnv() + +// Inject into process.env so Playwright and your app code can read them +Object.assign(process.env, supabaseEnv) export default defineConfig({ testDir: './tests/e2e', fullyParallel: true, forbidOnly: !!process.env.CI, - retries: process.env.CI ? 2 : 0, + // No retries by default (even in CI) because it slows the CI pipeline and masks real bugs + // 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, workers: process.env.CI ? 1 : undefined, reporter: [['html', {outputFolder: `tests/reports/playwright-report`, open: 'on-failure'}]], use: { baseURL: 'http://localhost:3000', trace: 'on-first-retry', + // headless: false, }, projects: [ { @@ -27,7 +59,7 @@ export default defineConfig({ // use: { ...devices['Desktop Safari'] }, // }, ], - timeout: 60000, + timeout: 120000, expect: { timeout: 120000, }, diff --git a/scripts/combine-migrations.sh b/scripts/combine-migrations.sh index fc018196..47eadc9a 100755 --- a/scripts/combine-migrations.sh +++ b/scripts/combine-migrations.sh @@ -72,7 +72,7 @@ while IFS= read -r file; do cat "$file" } > "$OUTPUT" - echo "✅ $file -> $OUTPUT" +# echo "✅ $file -> $OUTPUT" COUNTER=$((COUNTER + 100)) done <<< "$FILES" diff --git a/scripts/e2e-dev.sh b/scripts/e2e-dev.sh index f4a12279..b1c3ed52 100755 --- a/scripts/e2e-dev.sh +++ b/scripts/e2e-dev.sh @@ -4,6 +4,8 @@ set -euo pipefail cd "$(dirname "$0")"/.. +export NEXT_PUBLIC_ISOLATED_ENV=true + # Colors GREEN='\033[0;32m' RED='\033[0;31m' @@ -11,22 +13,10 @@ NC='\033[0m' print_status() { echo -e "${GREEN}[E2E-DEV]${NC} $1"; } print_error() { echo -e "${RED}[ERROR]${NC} $1"; } -get_supabase_status() { - SUPABASE_STATUS=$(supabase status --output json 2>/dev/null) -} - -get_supabase_status - # Check services are running (fail fast with helpful message) check_services() { local missing=0 - if [ -z "$(echo "$SUPABASE_STATUS" | jq -r '.API_URL')" ]; then - print_error "Supabase is not running. Starting..." - yarn test:db:reset - get_supabase_status - fi - if ! curl -s http://127.0.0.1:9099 > /dev/null 2>&1; then print_error "Firebase emulator is not running. Run: yarn emulate" missing=1 @@ -54,11 +44,6 @@ print_status "Checking services..." check_services print_status "All services running ✅" -export $(cat .env.test | grep -v '^#' | xargs) -export NEXT_PUBLIC_SUPABASE_URL=$(echo "$SUPABASE_STATUS" | jq -r '.API_URL') -export NEXT_PUBLIC_SUPABASE_ANON_KEY=$(echo "$SUPABASE_STATUS" | jq -r '.ANON_KEY') -export DATABASE_URL=$(echo "$SUPABASE_STATUS" | jq -r '.DB_URL') - # Run tests - pass all args through to playwright # Examples: # yarn test:e2e:dev → all e2e tests diff --git a/scripts/e2e.sh b/scripts/e2e.sh index eaf07b1e..27353178 100755 --- a/scripts/e2e.sh +++ b/scripts/e2e.sh @@ -5,6 +5,9 @@ set -euo pipefail # Change to project root cd "$(dirname "$0")"/.. +export NEXT_PUBLIC_ISOLATED_ENV=true +export CI=true + # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' @@ -60,18 +63,7 @@ supabase stop --no-backup 2>/dev/null || true sleep 2 # Give ports time to free up # Build backend (required?) -./scripts/build_api.sh - -# Get connection details -export NEXT_PUBLIC_SUPABASE_URL=$(supabase status --output json | jq -r '.API_URL') -export NEXT_PUBLIC_SUPABASE_ANON_KEY=$(supabase status --output json | jq -r '.ANON_KEY') -export DATABASE_URL=$(supabase status --output json | jq -r '.DB_URL') - -echo $NEXT_PUBLIC_SUPABASE_URL -echo $NEXT_PUBLIC_SUPABASE_ANON_KEY -echo $DATABASE_URL - -print_status "Supabase started at: $DATABASE_URL" +#./scripts/build_api.sh # Install Playwright browsers print_status "Installing Playwright browsers..." @@ -90,6 +82,26 @@ npx wait-on \ # Start Supabase (includes Postgres, Auth, Storage, etc.) and Apply migrations and seed (needs firebase emulator running) yarn test:db:reset +# Get connection details +STATUS_JSON=$(supabase status --output json) +export NEXT_PUBLIC_SUPABASE_URL=$(echo "$STATUS_JSON" | jq -r '.API_URL') +export NEXT_PUBLIC_SUPABASE_ANON_KEY=$(echo "$STATUS_JSON" | jq -r '.ANON_KEY') +export DATABASE_URL=$(echo "$STATUS_JSON" | jq -r '.DB_URL') + +print_status "Supabase started at: $DATABASE_URL" + +echo "Supabase env vars:" +echo $NEXT_PUBLIC_SUPABASE_URL +echo $NEXT_PUBLIC_SUPABASE_ANON_KEY +echo $DATABASE_URL + +for var in NEXT_PUBLIC_SUPABASE_URL NEXT_PUBLIC_SUPABASE_ANON_KEY DATABASE_URL; do + if [ -z "${!var}" ] || [ "${!var}" = "null" ]; then + echo "Error: $var is not set or null" >&2 + exit 1 + fi +done + # Start backend API print_status "Starting backend API..." yarn --cwd=backend/api dev & PIDS+=($!) diff --git a/scripts/e2e_services.sh b/scripts/e2e_services.sh index c9bd280e..0e0bcd19 100755 --- a/scripts/e2e_services.sh +++ b/scripts/e2e_services.sh @@ -8,6 +8,8 @@ set -euo pipefail # Change to project root cd "$(dirname "$0")"/.. +export NEXT_PUBLIC_ISOLATED_ENV=true + export $(cat .env.test | grep -v '^#' | xargs) # Ensure Supabase local stack is running; if not, reset/start it @@ -24,6 +26,13 @@ export NEXT_PUBLIC_SUPABASE_URL=$(echo "$STATUS_JSON" | jq -r '.API_URL') export NEXT_PUBLIC_SUPABASE_ANON_KEY=$(echo "$STATUS_JSON" | jq -r '.ANON_KEY') export DATABASE_URL=$(echo "$STATUS_JSON" | jq -r '.DB_URL') +for var in NEXT_PUBLIC_SUPABASE_URL NEXT_PUBLIC_SUPABASE_ANON_KEY DATABASE_URL; do + if [ -z "${!var}" ] || [ "${!var}" = "null" ]; then + echo "Error: $var is not set or null" >&2 + exit 1 + fi +done + # Build backend (required?) ./scripts/build_api.sh diff --git a/scripts/seed.sh b/scripts/seed.sh index 22f94b95..4a975020 100755 --- a/scripts/seed.sh +++ b/scripts/seed.sh @@ -8,12 +8,10 @@ cd "$(dirname "$0")"/.. export $(cat .env.test | grep -v '^#' | xargs) # Get connection details -export NEXT_PUBLIC_SUPABASE_URL=$(supabase status --output json | jq -r '.API_URL') -export NEXT_PUBLIC_SUPABASE_ANON_KEY=$(supabase status --output json | jq -r '.ANON_KEY') -export DATABASE_URL=$(supabase status --output json | jq -r '.DB_URL') - -# Build backend (required?) -#./scripts/build_api.sh +STATUS_JSON=$(supabase status --output json) +export NEXT_PUBLIC_SUPABASE_URL=$(echo "$STATUS_JSON" | jq -r '.API_URL') +export NEXT_PUBLIC_SUPABASE_ANON_KEY=$(echo "$STATUS_JSON" | jq -r '.ANON_KEY') +export DATABASE_URL=$(echo "$STATUS_JSON" | jq -r '.DB_URL') cd tests/e2e/utils diff --git a/scripts/supabase_start.sh b/scripts/supabase_start.sh new file mode 100755 index 00000000..0aaf5552 --- /dev/null +++ b/scripts/supabase_start.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +set -euo pipefail + +cd "$(dirname "$0")/.." + +if [ "${CI:-false}" = "true" ]; then + npx supabase start --exclude studio +else + npx supabase start +fi diff --git a/scripts/test_db_reset.sh b/scripts/test_db_reset.sh new file mode 100755 index 00000000..b7d2d691 --- /dev/null +++ b/scripts/test_db_reset.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +set -euo pipefail + +cd "$(dirname "$0")/.." + +./scripts/supabase_start.sh + +./scripts/combine-migrations.sh + +npx supabase db reset + +yarn test:db:seed diff --git a/supabase/config.toml b/supabase/config.toml index 5863984b..768ff1ab 100644 --- a/supabase/config.toml +++ b/supabase/config.toml @@ -58,7 +58,7 @@ enabled = true sql_paths = ["./seed.sql"] [realtime] -enabled = true +enabled = false # Bind realtime via either IPv4 or IPv6. (default: IPv4) # ip_version = "IPv6" # The maximum length in bytes of HTTP request headers. (default: 4096) @@ -76,7 +76,7 @@ openai_api_key = "env(OPENAI_API_KEY)" # Email testing server. Emails sent with the local dev setup are not actually sent - rather, they # are monitored, and you can view the emails that would have been sent from the web interface. [inbucket] -enabled = true +enabled = false # Port to use for the email testing server web interface. port = 54324 # Uncomment to expose additional ports for testing user applications that send emails. @@ -86,7 +86,7 @@ port = 54324 # sender_name = "Admin" [storage] -enabled = true +enabled = false # The maximum file size allowed (e.g. "5MB", "500KB"). file_size_limit = "50MiB" @@ -256,7 +256,7 @@ enabled = false # user_pool_region = "us-east-1" [edge_runtime] -enabled = true +enabled = false # Configure one of the supported request policies: `oneshot`, `per_worker`. # Use `oneshot` for hot reload, or `per_worker` for load testing. policy = "oneshot" @@ -276,7 +276,7 @@ inspector_port = 8083 # static_files = [ "./functions/MY_FUNCTION_NAME/*.html" ] [analytics] -enabled = true +enabled = false port = 54327 # Configure one of the supported backends: `postgres`, `bigquery`. backend = "postgres" diff --git a/tests/e2e/utils/databaseUtils.ts b/tests/e2e/utils/databaseUtils.ts new file mode 100644 index 00000000..8370d0fd --- /dev/null +++ b/tests/e2e/utils/databaseUtils.ts @@ -0,0 +1,47 @@ +import {createSupabaseDirectClient} from 'shared/supabase/init' + +export async function deleteFromDb(user_id: string) { + const db = createSupabaseDirectClient() + const deleteEntryById = `DELETE FROM users WHERE id = $1 RETURNING *` + const result = await db.query(deleteEntryById, [user_id]) + + if (!result.length) { + throw new Error(`No user found with id: ${user_id}`) + } + + console.log('Deleted data: ', { + id: result[0].id, + name: result[0].name, + username: result[0].username, + }) +} + +export async function userInformationFromDb(account: any) { + const db = createSupabaseDirectClient() + const queryUserById = ` + SELECT p.* + FROM users AS p + WHERE username = $1 + ` + const userResults = await db.query(queryUserById, [account.username]) + + if (userResults.length === 0) { + throw new Error(`No user found with username: ${account.username}`) + } + + const queryProfileById = ` + SELECT p.* + FROM profiles AS p + WHERE user_id = $1 + ` + const profileResults = await db.query(queryProfileById, [userResults[0].id]) + + if (profileResults.length === 0) { + throw new Error(`No profile found for user: ${userResults[0].id}`) + } + + return { + user: userResults[0], + profile: profileResults[0], + } +} diff --git a/tests/e2e/utils/firebaseUtils.ts b/tests/e2e/utils/firebaseUtils.ts new file mode 100644 index 00000000..91e303e7 --- /dev/null +++ b/tests/e2e/utils/firebaseUtils.ts @@ -0,0 +1,33 @@ +import axios from 'axios' + +import {config} from '../web/SPEC_CONFIG' + +export async function login(email: string, password: string) { + const login = await axios.post( + `${config.FIREBASE_URL.BASE}${config.FIREBASE_URL.SIGN_IN_PASSWORD}`, + { + email, + password, + returnSecureToken: true, + }, + ) + return login +} + +export async function signUp(email: string, password: string) { + // const base = 'http://localhost:9099/identitytoolkit.googleapis.com/v1'; + + await axios.post(`${config.FIREBASE_URL.BASE}${config.FIREBASE_URL.SIGNUP}`, { + email, + password, + returnSecureToken: true, + }) + + console.log('Auth created for', email) +} + +export async function deleteAccount(login: any) { + await axios.post(`${config.FIREBASE_URL.BASE}${config.FIREBASE_URL.DELETE}`, { + idToken: login.data.idToken, + }) +} diff --git a/tests/e2e/utils/seed-test-data.ts b/tests/e2e/utils/seed-test-data.ts index bb19ac26..82b5c283 100644 --- a/tests/e2e/utils/seed-test-data.ts +++ b/tests/e2e/utils/seed-test-data.ts @@ -27,8 +27,8 @@ async function createAuth(email: string, password: string) { ) { return } - if (err.code === 'ECONNREFUSED') - throw Error('Firebase emulator not running. Start it with:\n yarn test:e2e:services\n') + if (err.code === 'ECONNREFUSED') return + // throw Error('Firebase emulator not running. Start it with:\n yarn test:e2e:services\n') console.log(err) throw err } @@ -36,17 +36,35 @@ async function createAuth(email: string, password: string) { async function seedCompatibilityPrompts(pg: any, userId: string | null = null) { // Need some prompts to prevent the onboarding from stopping once it reaches them (just after profile creation) - const question = 'What is your favorite color?' - const multiple_choice_options = {Blue: 0, Green: 1, Red: 2} - const {data, error} = await tryCatch( - insert(pg, 'compatibility_prompts', { - creator_id: userId, - question, - answer_type: 'compatibility_multiple_choice', - multiple_choice_options, - }), - ) - console.log('Compatibility prompts created', {data, error}) + const compatibilityPrompts = [ + { + question: 'What is your favorite color?', + options: {Blue: 0, Green: 1, Red: 2}, + }, + { + question: 'What is your favorite animal?', + options: {Dog: 0, Cat: 1, Bird: 2}, + }, + { + question: 'What is your preferred time of day?', + options: {Morning: 0, Afternoon: 1, Night: 2}, + }, + { + question: 'What type of movies do you enjoy most?', + options: {Action: 0, Comedy: 1, Drama: 2}, + }, + ] + for (let i = 0; i < compatibilityPrompts.length; i++) { + const {data, error} = await tryCatch( + insert(pg, 'compatibility_prompts', { + creator_id: userId, + question: compatibilityPrompts[i].question, + answer_type: 'compatibility_multiple_choice', + multiple_choice_options: compatibilityPrompts[i].options, + }), + ) + console.log('Compatibility prompts created', {data, error}) + } } async function seedNotifications() { @@ -76,7 +94,7 @@ type ProfileType = 'basic' | 'medium' | 'full' } userInfo.user_id = await createAuth(userInfo.email, userInfo.password) if (userInfo.user_id) { - console.log('User created in Supabase:', userInfo) + console.log('User created in Supabase:', userInfo.email) await seedDatabase(pg, userInfo, profileType) } } diff --git a/tests/e2e/utils/setup-auth.ts b/tests/e2e/utils/setup-auth.ts deleted file mode 100644 index 6c8ea3d0..00000000 --- a/tests/e2e/utils/setup-auth.ts +++ /dev/null @@ -1,16 +0,0 @@ -import axios from 'axios' - -import {config} from '../web/SPEC_CONFIG' - -async function createAuth() { - // const base = 'http://localhost:9099/identitytoolkit.googleapis.com/v1'; - - await axios.post(`${config.FIREBASE_URL.BASE}${config.FIREBASE_URL.SIGNUP}`, { - email: config.USERS.DEV_1.EMAIL, - password: config.USERS.DEV_1.PASSWORD, - returnSecureToken: true, - }) - - console.log('Auth created', config.USERS.DEV_1.EMAIL) -} -createAuth() diff --git a/tests/e2e/web/fixtures/base.ts b/tests/e2e/web/fixtures/base.ts index 6c8a43e6..277f5887 100644 --- a/tests/e2e/web/fixtures/base.ts +++ b/tests/e2e/web/fixtures/base.ts @@ -1,6 +1,7 @@ import {test as base} from '@playwright/test' import {AuthPage} from '../pages/AuthPage' +import {ComatibilityPage} from '../pages/compatibilityPage' import {HomePage} from '../pages/homePage' import {OnboardingPage} from '../pages/onboardingPage' import {ProfilePage} from '../pages/profilePage' @@ -14,6 +15,7 @@ export const test = base.extend<{ signUpPage: SignUpPage profilePage: ProfilePage authPage: AuthPage + compatabilityPage: ComatibilityPage cleanUpUsers: void testAccount: OnboardingUser fakerAccount: OnboardingUser @@ -50,12 +52,10 @@ export const test = base.extend<{ const profilePage = new ProfilePage(page) await use(profilePage) }, - // cleanUpUsers: [ - // async ({}, use) => { - // await use() - // }, - // {auto: true}, - // ], + compatabilityPage: async ({page}, use) => { + const compatibilityPage = new ComatibilityPage(page) + await use(compatibilityPage) + }, }) export {expect} from '@playwright/test' diff --git a/tests/e2e/web/fixtures/signInFixture.ts b/tests/e2e/web/fixtures/signInFixture.ts index 9826b507..7ea6b6db 100644 --- a/tests/e2e/web/fixtures/signInFixture.ts +++ b/tests/e2e/web/fixtures/signInFixture.ts @@ -1,5 +1,6 @@ import {expect, Page, test as base} from '@playwright/test' +import {signUp} from '../../utils/firebaseUtils' import {AuthPage} from '../pages/AuthPage' import {config} from '../SPEC_CONFIG' @@ -9,9 +10,18 @@ export const test = base.extend<{ authenticatedPage: async ({page}, use) => { const authPage = new AuthPage(page) + const email = config.USERS.DEV_1.EMAIL + const password = config.USERS.DEV_1.PASSWORD + + try { + await signUp(email, password) + } catch (_e) { + console.log('User already exists for signinFixture', email) + } + await page.goto('/signin') - await authPage.fillEmailField(config.USERS.DEV_1.EMAIL) - await authPage.fillPasswordField(config.USERS.DEV_1.PASSWORD) + await authPage.fillEmailField(email) + await authPage.fillPasswordField(password) await authPage.clickSignInWithEmailButton() await page.waitForURL(/^(?!.*signin).*$/) diff --git a/tests/e2e/web/pages/compatibilityPage.ts b/tests/e2e/web/pages/compatibilityPage.ts new file mode 100644 index 00000000..94b76c08 --- /dev/null +++ b/tests/e2e/web/pages/compatibilityPage.ts @@ -0,0 +1,28 @@ +import {expect, Locator, Page} from '@playwright/test' + +export class ComatibilityPage { + private readonly answeredQuestionsTab: Locator + private readonly remaningQuestionsTab: Locator + private readonly skippedQuestionsTab: Locator + + constructor(public readonly page: Page) { + this.answeredQuestionsTab = page.getByText('Answered') + this.remaningQuestionsTab = page.getByText('To Answer') + this.skippedQuestionsTab = page.getByText('Skipped') + } + + async clickAnsweredQuestionsTab() { + await expect(this.answeredQuestionsTab).toBeVisible() + await this.answeredQuestionsTab.click() + } + + async clickRemainingQuestionsTab() { + await expect(this.remaningQuestionsTab).toBeVisible() + await this.remaningQuestionsTab.click() + } + + async clickSkippedQuestionsTab() { + await expect(this.skippedQuestionsTab).toBeVisible() + await this.skippedQuestionsTab.click() + } +} diff --git a/tests/e2e/web/pages/profilePage.ts b/tests/e2e/web/pages/profilePage.ts index 775214c6..8ff573fb 100644 --- a/tests/e2e/web/pages/profilePage.ts +++ b/tests/e2e/web/pages/profilePage.ts @@ -1,4 +1,5 @@ import {expect, Locator, Page} from '@playwright/test' + import {Socials} from '../utils/accountInformation' type ProfileDropdownOptions = 'Public' | 'Private' | 'Disable' @@ -41,6 +42,8 @@ export class ProfilePage { private readonly bioOptionsDropdown: Locator private readonly editBioDropdownOptions: Locator private readonly deleteBioDropdownOptions: Locator + private readonly answerCoreQuestionsButton: Locator + private readonly viewQuestionListButton: Locator constructor(public readonly page: Page) { this.startAnsweringButton = page.getByRole('button', {}) @@ -86,6 +89,8 @@ export class ProfilePage { this.bioOptionsDropdown = page.getByTestId('profile-bio-options') this.editBioDropdownOptions = page.getByText('Edit', {exact: true}) this.deleteBioDropdownOptions = page.getByText('Delete', {exact: true}) + this.answerCoreQuestionsButton = page.getByRole('button', {name: 'Answer Core Questions'}) + this.viewQuestionListButton = page.getByRole('link', {name: 'View List of Questions'}) } async clickCloseButton() { @@ -113,6 +118,16 @@ export class ProfilePage { await this.editProfileButton.click() } + async clickAnswerQuestionsButton() { + await expect(this.answerCoreQuestionsButton).toBeVisible() + await this.answerCoreQuestionsButton.click() + } + + async clickViewQuestionListButton() { + await expect(this.viewQuestionListButton).toBeVisible() + await this.viewQuestionListButton.click() + } + async selectOptionFromProfileDropdown(option: ProfileDropdownOptions) { await expect(this.profileOptionsDropdown).toBeVisible() await this.profileOptionsDropdown.click() @@ -185,11 +200,12 @@ export class ProfilePage { await expect(textContent?.toLowerCase()).toContain(numberOfKids.toLowerCase()) } - async verifyWantChildrenExpectation(expectation: string | undefined) { + async verifyWantChildrenExpectation(expectation: [string, number] | undefined) { if (!expectation) return + const [label, value] = expectation await expect(this.wantsKidsAboutSection).toBeVisible() const textContent = await this.wantsKidsAboutSection.textContent() - await expect(textContent?.toLowerCase()).toContain(expectation.toLowerCase()) + await expect(textContent?.toLowerCase()).toContain(label.toLowerCase()) } async verifyInterests(interest: string[] | undefined) { @@ -210,6 +226,14 @@ export class ProfilePage { } } + async verifyWorkArea(workArea: string[] | undefined) { + if (!workArea || workArea.length === 0) return + await expect(this.workAreaAboutSection).toBeVisible() + for (let i = 0; i < workArea.length; i++) { + await expect(this.workAreaAboutSection).toContainText(workArea[i], {ignoreCase: true}) + } + } + async verifyEducationLevelAndUniversity(educationLevel?: string, university?: string) { await expect(this.educationAboutSection).toBeVisible() const textContent = await this.educationAboutSection.textContent() @@ -294,4 +318,10 @@ export class ProfilePage { await expect(textContent?.toLowerCase()).toContain(socialMedia[i].urlOrUsername.toLowerCase()) } } + + async verifyBio(bio: string | undefined) { + if (!bio) return + await expect(this.bioSection).toBeVisible() + await expect(this.bioSection).toContainText(bio) + } } diff --git a/tests/e2e/web/pages/signUpPage.ts b/tests/e2e/web/pages/signUpPage.ts index 35184402..cb3a9d8f 100644 --- a/tests/e2e/web/pages/signUpPage.ts +++ b/tests/e2e/web/pages/signUpPage.ts @@ -1,35 +1,25 @@ import {expect, Locator, Page} from '@playwright/test' import { - RELATIONSHIP_CHOICES, - RELATIONSHIP_STATUS_CHOICES, - ROMANTIC_CHOICES, - POLITICAL_CHOICES, - DIET_CHOICES, - EDUCATION_CHOICES, - RELIGION_CHOICES, - LANGUAGE_CHOICES, - RACE_CHOICES, - MBTI_CHOICES, + ConnectionTypeKey, + DietKey, + EducationKey, + EthnicityKey, + LanguageKey, + PersonalityKey, + PoliticalBeliefsKey, + RelationshipStatusKey, + RelationshipStyleKey, + ReligionKey, } from 'common/choices' export type Gender = 'Woman' | 'Man' | 'Other' export type InterestedIn = 'Women' | 'Men' | 'Other' -export type ConnectionType = keyof typeof RELATIONSHIP_CHOICES -export type RelationshipStatus = keyof typeof RELATIONSHIP_STATUS_CHOICES -export type RelationshipStyle = keyof typeof ROMANTIC_CHOICES export type ChildrenExpectation = - | 'Strongly against' - | 'Against' - | 'Neutral' - | 'For' - | 'Strongly for' -export type PoliticalBeliefs = keyof typeof POLITICAL_CHOICES -export type Diet = keyof typeof DIET_CHOICES -export type Education = keyof typeof EDUCATION_CHOICES -export type Religion = keyof typeof RELIGION_CHOICES -export type Language = keyof typeof LANGUAGE_CHOICES -export type Ethnicity = keyof typeof RACE_CHOICES -export type Personality = keyof typeof MBTI_CHOICES + | ['Strongly against', 0] + | ['Against', 1] + | ['Neutral', 2] + | ['For', 3] + | ['Strongly for', 4] export type Interests = 'Chess' | 'Games' | 'Joy' | 'Livres' export type Causes = 'Animal Rights' | 'Feminism' export type Platforms = @@ -110,7 +100,7 @@ export class SignUpPage { this.displayNameField = page.getByPlaceholder('Display name') this.usernameField = page.getByPlaceholder('Username') this.nextButton = page.getByRole('button', {name: 'Next', exact: true}) - this.bioField = page.getByRole('paragraph').filter({hasText: /^$/}) + this.bioField = page.locator('.tiptap') this.locationField = page.getByPlaceholder('Search city...') this.ageField = page.getByPlaceholder('Age', {exact: true}) this.feetHeightField = page.getByTestId('height-feet') @@ -224,7 +214,7 @@ export class SignUpPage { await this.centimetersHeightField.fill(centimeters) } - async fillEthnicity(ethnicity: Ethnicity | undefined) { + async fillEthnicity(ethnicity: EthnicityKey | undefined) { if (ethnicity === 'Other') { await expect( this.page @@ -276,19 +266,19 @@ export class SignUpPage { } } - async setConnectionType(type: ConnectionType | undefined) { + async setConnectionType(type: ConnectionTypeKey | undefined) { await expect(this.page.getByLabel(`${type}`, {exact: true})).toBeVisible() await this.page.getByLabel(`${type}`, {exact: true}).click() await expect(this.page.getByLabel(`${type}`, {exact: true})).toBeChecked() } - async setRelationshipStatus(status: RelationshipStatus | undefined) { + async setRelationshipStatus(status: RelationshipStatusKey | undefined) { await expect(this.page.getByLabel(`${status}`, {exact: true})).toBeVisible() await this.page.getByLabel(`${status}`, {exact: true}).click() await expect(this.page.getByLabel(`${status}`, {exact: true})).toBeChecked() } - async setRelationshipStyle(style: RelationshipStyle | undefined) { + async setRelationshipStyle(style: RelationshipStyleKey | undefined) { await expect(this.page.getByLabel(`${style}`, {exact: true})).toBeVisible() await this.page.getByLabel(`${style}`, {exact: true}).click() await expect(this.page.getByLabel(`${style}`, {exact: true})).toBeChecked() @@ -301,19 +291,21 @@ export class SignUpPage { } async setWantChildrenExpectation(expectation: ChildrenExpectation | undefined) { - if (expectation === 'Strongly against') { + if (!expectation) return + const [label, value] = expectation + if (label === 'Strongly against') { await expect(this.stronglyDisagreeOnWantingKids).toBeVisible() await this.stronglyDisagreeOnWantingKids.click() await expect(this.stronglyDisagreeOnWantingKids).toBeChecked() - } else if (expectation === 'Against') { + } else if (label === 'Against') { await expect(this.disagreeOnWantingKids).toBeVisible() await this.disagreeOnWantingKids.click() await expect(this.disagreeOnWantingKids).toBeChecked() - } else if (expectation === 'Neutral') { + } else if (label === 'Neutral') { await expect(this.neutralOnWantingKids).toBeVisible() await this.neutralOnWantingKids.click() await expect(this.neutralOnWantingKids).toBeChecked() - } else if (expectation === 'For') { + } else if (label === 'For') { await expect(this.agreeOnWantingKids).toBeVisible() await this.agreeOnWantingKids.click() await expect(this.agreeOnWantingKids).toBeChecked() @@ -366,7 +358,7 @@ export class SignUpPage { } } - async setHighestEducationLevel(education: Education | undefined) { + async setHighestEducationLevel(education: EducationKey | undefined) { await expect(this.page.getByText(`${education}`, {exact: true})).toBeVisible() await this.page.getByText(`${education}`, {exact: true}).click() await expect(this.page.getByText(`${education}`, {exact: true})).toBeChecked() @@ -400,11 +392,13 @@ export class SignUpPage { if (isExisting) { await expect(this.page.getByLabel(`${workArea[i]}`, {exact: true})).toBeVisible() await this.page.getByLabel(`${workArea[i]}`, {exact: true}).click() + await this.page.waitForTimeout(500) } else { await expect(this.addWorkAreaField).toBeVisible() await expect(this.addWorkAreaButton).toBeVisible() await this.addWorkAreaField.fill(workArea[i]) await this.addWorkAreaButton.click() + await this.page.waitForTimeout(500) } await expect(this.page.getByLabel(`${workArea[i]}`, {exact: true})).toBeVisible() await expect(this.page.getByLabel(`${workArea[i]}`, {exact: true})).toBeChecked() @@ -412,7 +406,7 @@ export class SignUpPage { } async setPoliticalBeliefs( - politicalBeliefs?: PoliticalBeliefs | undefined, + politicalBeliefs?: PoliticalBeliefsKey | undefined, details?: string | undefined, ) { if (politicalBeliefs) { @@ -427,7 +421,10 @@ export class SignUpPage { } } - async setReligiousBeliefs(religiousBeliefs?: Religion | undefined, details?: string | undefined) { + async setReligiousBeliefs( + religiousBeliefs?: ReligionKey | undefined, + details?: string | undefined, + ) { if (religiousBeliefs && religiousBeliefs === 'Other') { await expect( this.page @@ -458,7 +455,7 @@ export class SignUpPage { } } - async setPersonalityType(personalityType: Personality | undefined) { + async setPersonalityType(personalityType: PersonalityKey | undefined) { await expect(this.page.getByText(`${personalityType}`, {exact: true})).toBeVisible() await this.page.getByText(`${personalityType}`, {exact: true}).click() await expect(this.page.getByText(`${personalityType}`, {exact: true})).toBeChecked() @@ -619,7 +616,7 @@ export class SignUpPage { } } - async setDietType(dietType: Diet | undefined) { + async setDietType(dietType: DietKey | undefined) { if (dietType === 'Other') { await expect(this.page.locator('label').filter({hasText: 'Other'}).nth(4)).toBeVisible() await this.page.locator('label').filter({hasText: 'Other'}).nth(4).click() @@ -649,8 +646,9 @@ export class SignUpPage { await this.alcoholConsumedPerMonthField.fill(amount) } - async setLanguages(language: Language[] | undefined) { + async setLanguages(language: LanguageKey[] | undefined) { if (!language || language.length === 0) return + await this.page.getByRole('checkbox', {name: `English`}).click() for (let i = 0; i < language.length; i++) { await expect(this.page.getByRole('checkbox', {name: `${language[i]}`})).toBeVisible() await this.page.getByRole('checkbox', {name: `${language[i]}`}).click() diff --git a/tests/e2e/web/specs/onboardingFlow.spec.ts b/tests/e2e/web/specs/onboardingFlow.spec.ts index 9fa1eac2..600d0bd7 100644 --- a/tests/e2e/web/specs/onboardingFlow.spec.ts +++ b/tests/e2e/web/specs/onboardingFlow.spec.ts @@ -1,4 +1,5 @@ -import {test} from '../fixtures/base' +import {userInformationFromDb} from '../../utils/databaseUtils' +import {expect, test} from '../fixtures/base' test.describe('when given valid input', () => { test('should successfully complete the onboarding flow', async ({ @@ -9,6 +10,9 @@ test.describe('when given valid input', () => { profilePage, testAccount, }) => { + console.log( + `Starting "should successfully complete the onboarding flow" with ${testAccount.username}`, + ) await homePage.gotToHomePage() await homePage.clickSignUpButton() await authPage.fillEmailField(testAccount.email) @@ -18,10 +22,8 @@ test.describe('when given valid input', () => { await onboardingPage.clickContinueButton() //Second continue await onboardingPage.clickGetStartedButton() await signUpPage.fillDisplayName(testAccount.display_name) - // TODO: fix? - await signUpPage.fillUsername(testAccount.username + Date.now().toString()) + await signUpPage.fillUsername(testAccount.username) await signUpPage.clickNextButton() - await signUpPage.fillBio(testAccount.bio) await signUpPage.chooseGender(testAccount.gender) await signUpPage.fillAge(testAccount.age) await signUpPage.fillHeight({ @@ -45,7 +47,7 @@ test.describe('when given valid input', () => { await signUpPage.fillUniversity(testAccount.university) await signUpPage.fillJobTitle(testAccount.job_title) await signUpPage.fillCompany(testAccount.company) - await signUpPage.setWorkArea(testAccount.work_area) //Is not displayed correctly + await signUpPage.setWorkArea(testAccount.work_area) await signUpPage.setPoliticalBeliefs( testAccount.beliefs?.political?.belief, testAccount.beliefs?.political?.details, @@ -61,10 +63,12 @@ test.describe('when given valid input', () => { await signUpPage.fillAlcoholPerMonth(testAccount.alcohol_consumed_per_month) await signUpPage.setLanguages(testAccount.languages) await signUpPage.addSocialMediaPlatform(testAccount.social_media) + await signUpPage.fillBio(testAccount.bio) await signUpPage.clickNextButton() await profilePage.clickCloseButton() await onboardingPage.clickRefineProfileButton() + //Verify information is correct await profilePage.verifyDisplayNameAndAge(testAccount.display_name, testAccount.age) await profilePage.verifyGenderLocationHeight( testAccount.gender, @@ -91,6 +95,7 @@ test.describe('when given valid input', () => { testAccount.university, ) await profilePage.verifyJobInformation(testAccount.job_title, testAccount.company) + await profilePage.verifyWorkArea(testAccount.work_area) await profilePage.verifyPoliticalBeliefs( testAccount.beliefs?.political?.belief, testAccount.beliefs?.political?.details, @@ -106,6 +111,56 @@ test.describe('when given valid input', () => { await profilePage.verifyDrinksPerMonth(testAccount.alcohol_consumed_per_month) await profilePage.verifyLanguages(testAccount.languages) await profilePage.verifySocialMedia(testAccount.social_media) + await profilePage.verifyBio(testAccount.bio) + + //Verify Database Information + const dbInfo = await userInformationFromDb(testAccount) + // console.log(dbInfo.profile.bio_text); + + await expect(dbInfo.user.name).toBe(testAccount.display_name) + await expect(dbInfo.user.username).toBe(testAccount.username) + await expect(dbInfo.profile.bio_text).toBe(testAccount.bio) + await expect(dbInfo.profile.gender).toEqual(`female`) + await expect(String(dbInfo.profile.age)).toEqual(testAccount.age) + await expect(dbInfo.profile.height_in_inches).toEqual(Number(testAccount.height?.feet) * 12) + await expect(dbInfo.profile.ethnicity).toContain('south_asian') + await expect(dbInfo.profile.pref_gender).toContain('male') + await expect(String(dbInfo.profile.pref_age_min)).toContain(testAccount.Interested_in_ages?.min) + await expect(String(dbInfo.profile.pref_age_max)).toContain(testAccount.Interested_in_ages?.max) + await expect(dbInfo.profile.pref_relation_styles).toContain( + `${testAccount.connection_type}`.toLowerCase(), + ) + await expect(dbInfo.profile.relationship_status).toContain(`open`) + await expect(dbInfo.profile.pref_romantic_styles).toContain(`open`) + await expect(dbInfo.profile.has_kids).toEqual(Number(testAccount.number_of_kids)) + await expect(dbInfo.profile.wants_kids_strength).toEqual(testAccount.children_expectation?.[1]) + await expect(dbInfo.profile.education_level).toContain( + `${testAccount.education_level}`.toLowerCase(), + ) + await expect(dbInfo.profile.university).toContain(testAccount.university) + await expect(dbInfo.profile.occupation_title).toContain(testAccount.job_title) + await expect(dbInfo.profile.company).toContain(testAccount.company) + await expect(dbInfo.profile.political_beliefs).toContain('green') + await expect(dbInfo.profile.political_details).toContain( + testAccount.beliefs?.political?.details, + ) + await expect(dbInfo.profile.religion).toContain('shinto') + await expect(dbInfo.profile.religious_beliefs).toContain( + testAccount.beliefs?.religious?.details, + ) + await expect(dbInfo.profile.mbti).toContain(`${testAccount.personality_type}`.toLowerCase()) + await expect(dbInfo.profile.big5_openness).toEqual( + testAccount.big_five_personality_traits?.openness, + ) + await expect(dbInfo.profile.diet).toContain(`${testAccount.diet}`.toLowerCase()) + await expect(dbInfo.profile.is_smoker).toEqual(testAccount.is_smoker) + await expect(dbInfo.profile.languages).toHaveLength(2) + await expect(dbInfo.profile.languages).toEqual( + expect.arrayContaining(testAccount.languages?.map((l) => l.toLowerCase()) ?? []), + ) + await expect(String(dbInfo.profile.drinks_per_month)).toEqual( + testAccount.alcohol_consumed_per_month, + ) }) test('should successfully skip the onboarding flow', async ({ @@ -116,6 +171,9 @@ test.describe('when given valid input', () => { profilePage, fakerAccount, }) => { + console.log( + `Starting "should successfully skip the onboarding flow" with ${fakerAccount.username}`, + ) await homePage.gotToHomePage() await homePage.clickSignUpButton() await authPage.fillEmailField(fakerAccount.email) @@ -126,9 +184,76 @@ test.describe('when given valid input', () => { await signUpPage.fillUsername(fakerAccount.username) await signUpPage.clickNextButton() await signUpPage.clickNextButton() //Skip optional information - // TODO: fix below - // await profilePage.clickCloseButton(); - // await onboardingPage.clickRefineProfileButton(); + await profilePage.clickCloseButton() + await onboardingPage.clickRefineProfileButton() + + const dbInfo = await userInformationFromDb(fakerAccount) + + await expect(dbInfo.user.name).toContain(fakerAccount.display_name) + await expect(dbInfo.user.username).toContain(fakerAccount.username) + }) + + 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 its an option', async ({ + onboardingPage, + signUpPage, + profilePage, + fakerAccount, + }) => { + console.log(`Starting "the first time its an option" with ${fakerAccount.username}`) + await onboardingPage.clickContinueButton() + await onboardingPage.clickBackButton() + await onboardingPage.clickContinueButton() + await onboardingPage.clickContinueButton() + await onboardingPage.clickGetStartedButton() + await signUpPage.fillDisplayName(fakerAccount.display_name) + await signUpPage.fillUsername(fakerAccount.username) + await signUpPage.clickNextButton() + await signUpPage.clickNextButton() //Skip bio + await signUpPage.clickNextButton() //Skip optional information + await profilePage.clickCloseButton() + await onboardingPage.clickRefineProfileButton() + + const dbInfo = await userInformationFromDb(fakerAccount) + + await expect(dbInfo.user.name).toContain(fakerAccount.display_name) + await expect(dbInfo.user.username).toContain(fakerAccount.username) + }) + + test('the second time its an option', async ({ + onboardingPage, + signUpPage, + profilePage, + fakerAccount, + }) => { + console.log(`Starting "the second time its an option" with ${fakerAccount.username}`) + await onboardingPage.clickContinueButton() + await onboardingPage.clickContinueButton() + await onboardingPage.clickBackButton() + await onboardingPage.clickContinueButton() + await onboardingPage.clickGetStartedButton() + await signUpPage.fillDisplayName(fakerAccount.display_name) + await signUpPage.fillUsername(fakerAccount.username) + await signUpPage.clickNextButton() + await signUpPage.clickNextButton() //Skip bio + await signUpPage.clickNextButton() //Skip optional information + await profilePage.clickCloseButton() + await onboardingPage.clickRefineProfileButton() + + const dbInfo = await userInformationFromDb(fakerAccount) + + await expect(dbInfo.user.name).toContain(fakerAccount.display_name) + await expect(dbInfo.user.username).toContain(fakerAccount.username) + }) }) }) diff --git a/tests/e2e/web/utils/accountInformation.ts b/tests/e2e/web/utils/accountInformation.ts index d84767af..06b7a859 100644 --- a/tests/e2e/web/utils/accountInformation.ts +++ b/tests/e2e/web/utils/accountInformation.ts @@ -1,22 +1,24 @@ import {faker} from '@faker-js/faker' +import { + ConnectionTypeKey, + DietKey, + EducationKey, + EthnicityKey, + LanguageKey, + PersonalityKey, + PoliticalBeliefsKey, + RelationshipStatusKey, + RelationshipStyleKey, + ReligionKey, +} from 'common/choices' import { Causes, ChildrenExpectation, - ConnectionType, - Diet, - Education, - Ethnicity, Gender, InterestedIn, Interests, - Language, - Personality, Platforms, - PoliticalBeliefs, - RelationshipStatus, - RelationshipStyle, - Religion, } from '../pages/signUpPage' export type OnboardingUser = { @@ -28,28 +30,28 @@ export type OnboardingUser = { gender?: Gender age?: string height?: Height - ethnicity_origin?: Ethnicity + ethnicity_origin?: EthnicityKey interested_in?: InterestedIn Interested_in_ages?: InterestedInAges - connection_type?: ConnectionType - relationship_status?: RelationshipStatus - relationship_style?: RelationshipStyle + connection_type?: ConnectionTypeKey + relationship_status?: RelationshipStatusKey + relationship_style?: RelationshipStyleKey number_of_kids?: string children_expectation?: ChildrenExpectation interests?: (Interests | string)[] causes?: (Causes | string)[] - education_level?: Education + education_level?: EducationKey university?: string job_title?: string company?: string work_area?: string[] beliefs?: BeliefDetails - personality_type?: Personality + personality_type?: PersonalityKey big_five_personality_traits?: FiveBigPersonalityTraits - diet?: Diet + diet?: DietKey is_smoker?: boolean alcohol_consumed_per_month?: string - languages?: Language[] + languages?: LanguageKey[] social_media?: Socials[] } @@ -66,11 +68,11 @@ type InterestedInAges = { type BeliefDetails = { political?: { - belief?: PoliticalBeliefs + belief?: PoliticalBeliefsKey details?: string } religious?: { - belief?: Religion + belief?: ReligionKey details?: string } } @@ -95,68 +97,74 @@ type OnboardingConfig = { export const onboarding: OnboardingConfig = { // Use a function so email is unique per test call - faker_account: () => ({ - email: `faker+${crypto.randomUUID()}@test.com`, - password: faker.internet.password(), - display_name: faker.internet.displayName(), - username: `user_${crypto.randomUUID().slice(0, 8)}`, - }), + faker_account: () => { + const id = crypto.randomUUID().slice(0, 6) + return { + email: `faker+${id}@test.com`, + password: faker.internet.password(), + display_name: faker.internet.displayName(), + username: `user_${id}`, + } + }, - account_one: () => ({ - // Use a non-real TLD like @test.compass to make it obvious these are test accounts and prevent accidental emails - email: `onboarding+${crypto.randomUUID()}@test.compass`, - password: 'CompassTest', - display_name: 'Compass Onboarding', - username: `TheGreatOnboarding_${crypto.randomUUID().slice(0, 8)}`, - bio: 'Born beneath twin moons, this wanderer maps forgotten roads, trades riddles for shelter, and keeps stories in glass bottles. Drawn to ancient libraries and glowing forests, they seek lost spells, quiet taverns, and adventures that rewrite fate. Their compass points to wonder. Ever onward. Always. Go', - gender: 'Woman', - age: '25', - height: { - feet: '6', - inches: '0', - centimeters: '182.88', - }, - ethnicity_origin: 'South/Southeast Asian', - interested_in: 'Men', - Interested_in_ages: { - min: '20', - max: '30', - }, - connection_type: 'Relationship', - relationship_status: 'In open relationship', - relationship_style: 'Open Relationship', - number_of_kids: '2', - children_expectation: 'Neutral', - interests: ['Chess', 'Eating'], - causes: ['Animal Rights', 'Free Spotify'], - education_level: 'Bachelors', - university: 'Open-Source University', - job_title: 'Unemployed', - company: 'Home', - work_area: ['Living Room', 'University'], - beliefs: { - political: { - belief: 'Green / Eco-Socialist', - details: 'This will be details', + account_one: () => { + const id = crypto.randomUUID().slice(0, 6) + return { + // Use a non-real TLD like @test.compass to make it obvious these are test accounts and prevent accidental emails + email: `onboarding+${id}@test.compass`, + password: 'CompassTest', + display_name: 'Compass Onboarding', + username: `TheGreatOnboarding_${id}`, // username max length is 25 (see /create-user) + bio: 'Born beneath twin moons, this wanderer maps forgotten roads, trades riddles for shelter, and keeps stories in glass bottles. Drawn to ancient libraries and glowing forests, they seek lost spells, quiet taverns, and adventures that rewrite fate. Their compass points to wonder. Ever onward. Always. Go', + gender: 'Woman', + age: '25', + height: { + feet: '6', + inches: '0', + centimeters: '182.88', }, - religious: { - belief: 'Shinto', - details: 'This will be details', + ethnicity_origin: 'South/Southeast Asian', + interested_in: 'Men', + Interested_in_ages: { + min: '20', + max: '30', }, - }, - personality_type: 'ENFJ', - big_five_personality_traits: { - openness: 43, - }, - diet: 'Omnivore', - is_smoker: false, - alcohol_consumed_per_month: '4', - languages: ['Akan', 'Cebuano'], - social_media: [ - { - platform: 'Bluesky', - urlOrUsername: 'TheGreatConnection', + connection_type: 'Relationship', + relationship_status: 'In open relationship', + relationship_style: 'Open Relationship', + number_of_kids: '2', + children_expectation: ['Neutral', 2], + interests: ['Chess', 'Eating'], + causes: ['Animal Rights', 'Free Spotify'], + education_level: 'Bachelors', + university: 'Open-Source University', + job_title: 'Unemployed', + company: 'Home', + work_area: ['Engineering', 'Academia'], + beliefs: { + political: { + belief: 'Green / Eco-Socialist', + details: 'This will be details', + }, + religious: { + belief: 'Shinto', + details: 'This will be details', + }, }, - ], - }), + personality_type: 'ENFJ', + big_five_personality_traits: { + openness: 43, + }, + diet: 'Omnivore', + is_smoker: false, + alcohol_consumed_per_month: '4', + languages: ['Akan', 'Cebuano'], + social_media: [ + { + platform: 'Bluesky', + urlOrUsername: 'TheGreatConnection', + }, + ], + } + }, } diff --git a/tests/e2e/web/utils/deleteUser.ts b/tests/e2e/web/utils/deleteUser.ts index 516956eb..3446f531 100644 --- a/tests/e2e/web/utils/deleteUser.ts +++ b/tests/e2e/web/utils/deleteUser.ts @@ -1,24 +1,11 @@ -import axios from 'axios' -import {createSupabaseDirectClient} from 'shared/supabase/init' - -import {config} from '../SPEC_CONFIG' +import {deleteFromDb} from '../../utils/databaseUtils' +import {deleteAccount, login} from '../../utils/firebaseUtils' export async function deleteUser(email: string, password: string) { try { - const login = await axios.post( - `${config.FIREBASE_URL.BASE}${config.FIREBASE_URL.SIGN_IN_PASSWORD}`, - { - email, - password, - returnSecureToken: true, - }, - ) - - await deleteFromDb(login.data.localId) - - await axios.post(`${config.FIREBASE_URL.BASE}${config.FIREBASE_URL.DELETE}`, { - idToken: login.data.idToken, - }) + const loginInfo = await login(email, password) + await deleteFromDb(loginInfo.data.localId) + await deleteAccount(loginInfo) } catch (err: any) { // Skip deletion if user doesn't exist or other auth errors occur if ( @@ -31,19 +18,3 @@ export async function deleteUser(email: string, password: string) { console.log(err) } } - -async function deleteFromDb(user_id: string) { - const db = createSupabaseDirectClient() - try { - const deleteEntryById = `DELETE FROM users WHERE id = $1 RETURNING *` - const result = await db.query(deleteEntryById, [user_id]) - console.log('Deleted data: ', { - id: result[0].id, - name: result[0].name, - username: result[0].username, - }) - } catch (error) { - console.error('Failed to delete user data, all changes rolled back: ', error) - throw error - } -} diff --git a/web/components/auth-context.tsx b/web/components/auth-context.tsx index f9c9598b..ecef285e 100644 --- a/web/components/auth-context.tsx +++ b/web/components/auth-context.tsx @@ -10,7 +10,6 @@ import {useStateCheckEquality} from 'web/hooks/use-state-check-equality' import {useWebsocketPrivateUser, useWebsocketUser} from 'web/hooks/use-user' import {api} from 'web/lib/api' import {auth} from 'web/lib/firebase/users' -import {useLocale} from 'web/lib/locale' import {getLocale} from 'web/lib/locale-cookie' import {identifyUser, setUserProperty} from 'web/lib/service/analytics' import {getPrivateUserSafe, getUserSafe} from 'web/lib/supabase/users' @@ -118,8 +117,6 @@ export function AuthProvider(props: {children: ReactNode; serverUser?: AuthUser} ) const [authLoaded, setAuthLoaded] = useState(false) const firebaseUser = useAndSetupFirebaseUser() - const {locale} = useLocale() - console.log('AuthProvider hook locale', locale) const authUser = !user ? user diff --git a/web/components/bio/profile-bio-block.tsx b/web/components/bio/profile-bio-block.tsx index a965223b..e5bafe3e 100644 --- a/web/components/bio/profile-bio-block.tsx +++ b/web/components/bio/profile-bio-block.tsx @@ -30,9 +30,9 @@ export function BioBlock(props: { !edit && 'px-3 py-2', )} > - + {!edit && profile.bio && ( - + )} diff --git a/web/components/required-profile-form.tsx b/web/components/required-profile-form.tsx index 9a29f2a1..090a72c4 100644 --- a/web/components/required-profile-form.tsx +++ b/web/components/required-profile-form.tsx @@ -92,7 +92,7 @@ export const RequiredProfileUserForm = (props: {
{t( 'profile.required.username_locked_warning', - 'You cannot change your username after creating a profile, but you can update you name later in your profile settings.', + 'You cannot change your username after creating a profile, but you can update your name later in your profile settings.', )}
)} diff --git a/web/lib/supabase/db.ts b/web/lib/supabase/db.ts index 357855dc..7d9736e9 100644 --- a/web/lib/supabase/db.ts +++ b/web/lib/supabase/db.ts @@ -9,12 +9,20 @@ export function initSupabaseClient() { const anonKeyOverride = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || process.env.SUPABASE_ANON_KEY if (urlOverride && anonKeyOverride) { - console.log('Initializing Supabase client (env URL override)') + console.log('Initializing Supabase client (env URL override)', {urlOverride, anonKeyOverride}) return createClient(urlOverride, anonKeyOverride) + } else if (process.env.NEXT_PUBLIC_ISOLATED_ENV) { + throw new Error( + `You are running isolated tests (NEXT_PUBLIC_ISOLATED_ENV=true), so you do not want to call the remote supabase. ${{ + urlOverride, + anonKeyOverride, + }}`, + ) } + if (urlOverride || anonKeyOverride) { console.warn( - 'Supabase env override is partially set. Both URL and ANON_KEY are required. Falling back to ENV_CONFIG.' + 'Supabase env override is partially set. Both URL and ANON_KEY are required. Falling back to ENV_CONFIG.', ) } diff --git a/web/pages/[username]/index.tsx b/web/pages/[username]/index.tsx index f7023783..42e330b2 100644 --- a/web/pages/[username]/index.tsx +++ b/web/pages/[username]/index.tsx @@ -168,12 +168,19 @@ export default function UserPage(props: UserPageProps) { const [fetchedProps, setFetchedProps] = useState(props) const [loading, setLoading] = useState(nativeMobile) - console.log('UserPage state:', { - username, - fetchedProps, - loading, - nativeMobile, - }) + console.log( + 'UserPage state:', + JSON.stringify( + { + username, + fetchedProps, + loading, + nativeMobile, + }, + null, + 2, + ), + ) useEffect(() => { if (nativeMobile) { diff --git a/web/pages/_app.tsx b/web/pages/_app.tsx index 77d077e3..7ee606ef 100644 --- a/web/pages/_app.tsx +++ b/web/pages/_app.tsx @@ -94,7 +94,7 @@ function MyApp(props: AppProps) { const router = useRouter() const [locale, setLocaleState] = useState(getLocale()) - console.log('_app locale', locale) + // console.log('_app locale', locale) const setLocale = (newLocale: string) => { console.log('setLocale', newLocale) document.cookie = `lang=${newLocale}; path=/; max-age=31536000`