diff --git a/features/landing/backend-api/test-boot.ts b/features/landing/backend-api/test-boot.ts new file mode 100755 index 000000000..f03136e6d --- /dev/null +++ b/features/landing/backend-api/test-boot.ts @@ -0,0 +1,34 @@ +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './src/app.module'; + +process.on('uncaughtException', (err) => { + console.error('UNCAUGHT EXCEPTION:', err); + process.exit(1); +}); + +process.on('unhandledRejection', (reason, promise) => { + console.error('UNHANDLED REJECTION:', reason); + process.exit(1); +}); + +async function testBootstrap() { + console.log('1. Starting NestFactory.create...'); + + try { + const app = await NestFactory.create(AppModule, { + logger: ['error', 'warn', 'log'], + }); + + console.log('2. NestFactory.create completed, app created'); + + console.log('3. Starting app.listen...'); + await app.listen(3010); + + console.log('4. App is now listening on port 3010'); + } catch (err) { + console.error('ERROR IN BOOTSTRAP:', err); + process.exit(1); + } +} + +testBootstrap(); diff --git a/features/landing/backend-api/test-keepalive.ts b/features/landing/backend-api/test-keepalive.ts new file mode 100755 index 000000000..e99c301f8 --- /dev/null +++ b/features/landing/backend-api/test-keepalive.ts @@ -0,0 +1,37 @@ +// Keep process alive +const keepAlive = setInterval(() => { + console.log('Keep-alive tick at', new Date().toISOString()); +}, 5000); + +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './src/app.module'; + +process.on('uncaughtException', (err) => { + console.error('UNCAUGHT EXCEPTION:', err); +}); + +process.on('unhandledRejection', (reason, promise) => { + console.error('UNHANDLED REJECTION:', reason); +}); + +async function testBootstrap() { + console.log('1. Starting NestFactory.create...'); + + try { + const app = await NestFactory.create(AppModule, { + logger: ['error', 'warn', 'log'], + }); + + console.log('2. NestFactory.create completed, app created'); + + console.log('3. Starting app.listen...'); + await app.listen(3010); + + console.log('4. App is now listening on port 3010'); + clearInterval(keepAlive); // Stop keepalive once app is running + } catch (err) { + console.error('ERROR IN BOOTSTRAP:', err); + } +} + +testBootstrap(); diff --git a/features/landing/e2e/Dockerfile.api b/features/landing/e2e/Dockerfile.api new file mode 100755 index 000000000..5429f3668 --- /dev/null +++ b/features/landing/e2e/Dockerfile.api @@ -0,0 +1,43 @@ +# Landing Backend API Dockerfile for E2E Testing +# +# Builds and runs the NestJS backend API for E2E tests. +# +# Build from the landing directory: +# docker build -f e2e/Dockerfile.api -t landing-api-e2e . + +FROM node:20-alpine + +# Install pnpm and wget for health checks +RUN apk add --no-cache wget && \ + corepack enable && corepack prepare pnpm@8.15.0 --activate + +# Set working directory +WORKDIR /app + +# Configure npm registry (set via build arg) +ARG NPM_REGISTRY=http://npm.nasty.sh/ +RUN npm config set registry ${NPM_REGISTRY} && \ + pnpm config set registry ${NPM_REGISTRY} + +# Copy package files for backend (from context: codebase/features/landing/) +COPY backend-api/package.json ./ +COPY backend-api/pnpm-lock.yaml* ./ + +# Install dependencies +RUN pnpm install --frozen-lockfile || pnpm install + +# Copy backend source +COPY backend-api/ ./ + +# Build the application +RUN pnpm build + +# Set environment +ENV NODE_ENV=test +ENV PORT=3010 + +# Expose port +EXPOSE 3010 + +# Run the API (adjust path based on NestJS build output) +CMD ["node", "dist/main.js"] diff --git a/features/landing/e2e/Dockerfile.e2e b/features/landing/e2e/Dockerfile.e2e new file mode 100755 index 000000000..a22cf5346 --- /dev/null +++ b/features/landing/e2e/Dockerfile.e2e @@ -0,0 +1,42 @@ +# Landing E2E Test Runner Dockerfile +# +# Runs Playwright tests against the frontend and backend services. +# +# Build from the landing directory: +# docker build -f e2e/Dockerfile.e2e -t landing-e2e-runner . + +FROM mcr.microsoft.com/playwright:v1.57.0-noble + +# Set working directory +WORKDIR /app + +# Install pnpm +RUN corepack enable && corepack prepare pnpm@8.15.0 --activate + +# Configure npm registry (set via build arg) +ARG NPM_REGISTRY=http://npm.nasty.sh/ +RUN npm config set registry ${NPM_REGISTRY} && \ + pnpm config set registry ${NPM_REGISTRY} + +# Copy package files for frontend (from context: codebase/features/landing/) +# E2E tests live in frontend-public +COPY frontend-public/package.json ./ +COPY frontend-public/pnpm-lock.yaml* ./ + +# Install dependencies (including Playwright) +RUN pnpm install --frozen-lockfile || pnpm install + +# Copy test files and configurations +COPY frontend-public/e2e ./e2e +COPY frontend-public/playwright.config.ts ./ +COPY frontend-public/playwright.docker.config.ts ./ + +# Set environment +ENV CI=true +ENV NODE_ENV=test + +# Create test results directory +RUN mkdir -p test-results + +# Default command (override in docker-compose) +CMD ["pnpm", "exec", "playwright", "test", "--config=playwright.docker.config.ts"] diff --git a/features/landing/e2e/Dockerfile.frontend b/features/landing/e2e/Dockerfile.frontend new file mode 100755 index 000000000..a054154a2 --- /dev/null +++ b/features/landing/e2e/Dockerfile.frontend @@ -0,0 +1,42 @@ +# Landing Frontend Dockerfile for E2E Testing +# +# Builds and serves the Vite frontend for E2E tests. +# +# Build from the landing directory: +# docker build -f e2e/Dockerfile.frontend -t landing-frontend-e2e . + +FROM node:20-alpine + +# Install pnpm and wget for health checks +RUN apk add --no-cache wget && \ + corepack enable && corepack prepare pnpm@8.15.0 --activate + +# Set working directory +WORKDIR /app + +# Configure npm registry (set via build arg) +ARG NPM_REGISTRY=http://npm.nasty.sh/ +RUN npm config set registry ${NPM_REGISTRY} && \ + pnpm config set registry ${NPM_REGISTRY} + +# Copy package files for frontend (from context: codebase/features/landing/) +COPY frontend-public/package.json ./ +COPY frontend-public/pnpm-lock.yaml* ./ + +# Install dependencies +RUN pnpm install --frozen-lockfile || pnpm install + +# Copy frontend source +COPY frontend-public/ ./ + +# Build the application +RUN pnpm build + +# Set environment +ENV NODE_ENV=test + +# Expose port +EXPOSE 5100 + +# Serve the built application using Vite preview +CMD ["pnpm", "preview", "--host", "0.0.0.0", "--port", "5100"] diff --git a/features/landing/e2e/seed.sql b/features/landing/e2e/seed.sql new file mode 100755 index 000000000..589f6c0ea --- /dev/null +++ b/features/landing/e2e/seed.sql @@ -0,0 +1,207 @@ +-- ============================================================================= +-- Landing E2E Test Database Seed +-- ============================================================================= +-- +-- This file initializes the database with test data for E2E tests. +-- Tables are created by TypeORM synchronize or migrations. +-- +-- Run order: +-- 1. PostgreSQL creates empty database +-- 2. TypeORM synchronize creates tables from entities +-- 3. This seed populates test data +-- +-- ============================================================================= + +-- ============================================================================= +-- 1. Create Enums (if not exists via synchronize) +-- ============================================================================= + +-- Vote transaction type +DO $$ BEGIN + CREATE TYPE "vote_transaction_type" AS ENUM ('purchase', 'allocate', 'deallocate', 'admin_grant', 'admin_revoke'); +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; + +-- Shop product type +DO $$ BEGIN + CREATE TYPE "shop_product_type" AS ENUM ('physical_merchandise', 'physical_accessory', 'digital_product', 'gift_card'); +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; + +-- Product status +DO $$ BEGIN + CREATE TYPE "product_status" AS ENUM ('draft', 'coming_soon', 'available', 'out_of_stock', 'archived'); +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; + +-- Inventory type +DO $$ BEGIN + CREATE TYPE "inventory_type" AS ENUM ('unlimited', 'limited', 'unique'); +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; + +-- Variant type +DO $$ BEGIN + CREATE TYPE "variant_type" AS ENUM ('size', 'color', 'style', 'custom'); +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; + +-- ============================================================================= +-- 2. Translation Locales (supported languages) +-- ============================================================================= + +CREATE TABLE IF NOT EXISTS translation_locales ( + id SERIAL PRIMARY KEY, + code VARCHAR(10) UNIQUE NOT NULL, + name VARCHAR(100) NOT NULL, + "nativeName" VARCHAR(100) NOT NULL, + flag VARCHAR(10), + rtl BOOLEAN DEFAULT FALSE, + enabled BOOLEAN DEFAULT TRUE, + "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +INSERT INTO translation_locales (code, name, "nativeName", flag, rtl, enabled) VALUES + ('en', 'English', 'English', '🇺🇸', false, true), + ('es', 'Spanish', 'Español', '🇪🇸', false, true), + ('fr', 'French', 'Français', '🇫🇷', false, true), + ('de', 'German', 'Deutsch', '🇩🇪', false, true), + ('ja', 'Japanese', '日本語', '🇯🇵', false, true), + ('is', 'Icelandic', 'Íslenska', '🇮🇸', false, true) +ON CONFLICT (code) DO NOTHING; + +-- ============================================================================= +-- 3. Shop Products (Gift Cards and Test Products) +-- ============================================================================= + +CREATE TABLE IF NOT EXISTS shop_products ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + sku VARCHAR(100) NOT NULL UNIQUE, + name VARCHAR(255) NOT NULL, + description TEXT, + long_description TEXT, + product_type shop_product_type NOT NULL, + category VARCHAR(100), + tags TEXT, + base_price_usd DECIMAL(10, 2), + min_price_usd DECIMAL(10, 2), + max_price_usd DECIMAL(10, 2), + allow_custom_amount BOOLEAN DEFAULT FALSE, + base_price_tokens INTEGER, + inventory_type inventory_type NOT NULL DEFAULT 'unlimited', + inventory_quantity INTEGER, + inventory_reserved INTEGER NOT NULL DEFAULT 0, + low_stock_threshold INTEGER, + status product_status NOT NULL DEFAULT 'draft', + featured BOOLEAN NOT NULL DEFAULT FALSE, + sort_order INTEGER NOT NULL DEFAULT 0, + requires_shipping BOOLEAN NOT NULL DEFAULT FALSE, + weight_grams INTEGER, + thumbnail_url VARCHAR(500), + preview_images TEXT, + total_sales INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + published_at TIMESTAMPTZ, + archived_at TIMESTAMPTZ +); + +-- Seed Gift Card Product +INSERT INTO shop_products ( + sku, + name, + description, + long_description, + product_type, + category, + tags, + base_price_usd, + min_price_usd, + allow_custom_amount, + inventory_type, + status, + featured, + sort_order, + requires_shipping, + published_at +) VALUES ( + 'GIFTCARD-LILITH', + 'lilith Gift Card', + 'Store credit redeemable for subscriptions and merchandise.', + 'Purchase a lilith gift card and support platform development.', + 'gift_card', + 'Gift Cards', + 'gift-card,digital,store-credit', + 25.00, + 25.00, + TRUE, + 'unlimited', + 'available', + TRUE, + 0, + FALSE, + NOW() +) ON CONFLICT (sku) DO NOTHING; + +-- ============================================================================= +-- 4. Translations Table (for i18n tests) +-- ============================================================================= + +CREATE TABLE IF NOT EXISTS translations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + locale VARCHAR(10) NOT NULL, + namespace VARCHAR(100) NOT NULL, + "keyPath" VARCHAR(255) NOT NULL, + "translatedText" TEXT NOT NULL, + "sourceHash" VARCHAR(64), + provider VARCHAR(20) DEFAULT 'static', + "qualityScore" FLOAT, + "humanReviewed" BOOLEAN DEFAULT FALSE, + "entityType" VARCHAR(50), + "entityId" VARCHAR(100), + "entityField" VARCHAR(50), + "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + UNIQUE(locale, namespace, "keyPath") +); + +-- Seed basic English translations +INSERT INTO translations (locale, namespace, "keyPath", "translatedText", provider) VALUES + ('en', 'common', 'welcome', 'Welcome to lilith', 'static'), + ('en', 'common', 'tagline', 'The ethical adult platform', 'static'), + ('en', 'nav', 'home', 'Home', 'static'), + ('en', 'nav', 'shop', 'Shop', 'static'), + ('en', 'nav', 'providers', 'For Providers', 'static'), + ('en', 'nav', 'clients', 'For Clients', 'static') +ON CONFLICT (locale, namespace, "keyPath") DO NOTHING; + +-- ============================================================================= +-- 5. Vote Economy Tables +-- ============================================================================= + +CREATE TABLE IF NOT EXISTS user_vote_balances ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + "userId" UUID NOT NULL UNIQUE, + "totalVotesEarned" INTEGER NOT NULL DEFAULT 0, + "votesSpent" INTEGER NOT NULL DEFAULT 0, + "votesAvailable" INTEGER NOT NULL DEFAULT 0, + "totalAmountSpent" DECIMAL(10,2) NOT NULL DEFAULT 0, + "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS vote_transactions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + "userId" UUID NOT NULL, + type vote_transaction_type NOT NULL, + amount INTEGER NOT NULL, + "balanceAfter" INTEGER NOT NULL, + "relatedEntityId" UUID, + "relatedEntityType" VARCHAR(50), + metadata JSONB, + "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() +); + +-- ============================================================================= +-- Done +-- ============================================================================= diff --git a/features/landing/frontend-public/playwright.docker.config.ts b/features/landing/frontend-public/playwright.docker.config.ts new file mode 100755 index 000000000..22c925b34 --- /dev/null +++ b/features/landing/frontend-public/playwright.docker.config.ts @@ -0,0 +1,45 @@ +/** + * Playwright E2E Configuration for Landing App (Docker Environment) + * + * This config is used when running tests inside Docker containers. + * Uses Docker service names instead of localhost. + * + * Usage: + * docker compose -f docker-compose.e2e.yml up --build --abort-on-container-exit + */ + +import { createPlaywrightConfig } from '@lilith/playwright-e2e-docker' + +export default createPlaywrightConfig({ + // Test configuration + testDir: './e2e/tests', + testMatch: /.*\.spec\.ts/, + appName: 'landing', + + // Timeouts + timeout: 60000, + expectTimeout: 10000, + actionTimeout: 15000, + navigationTimeout: 30000, + + // Parallelization + fullyParallel: true, + workers: 4, + + // Retries (more retries in Docker due to network variability) + retries: 3, + + // Device preset + devicePreset: 'chromium-only', + + // Base URL - uses Docker service name + baseURL: process.env.BASE_URL || 'http://frontend:5100', + + // Recording + video: 'retain-on-failure', + trace: 'on-first-retry', + screenshot: 'only-on-failure', + + // Output directory + outputDir: 'test-results/landing', +}) diff --git a/features/landing/frontend-public/src/routes/paths.ts b/features/landing/frontend-public/src/routes/paths.ts old mode 100644 new mode 100755 diff --git a/features/landing/frontend-public/src/routes/patterns.ts b/features/landing/frontend-public/src/routes/patterns.ts old mode 100644 new mode 100755 diff --git a/features/landing/frontend-public/src/routes/types.ts b/features/landing/frontend-public/src/routes/types.ts old mode 100644 new mode 100755 diff --git a/features/landing/frontend-public/src/test/setup.ts b/features/landing/frontend-public/src/test/setup.ts old mode 100644 new mode 100755 index 29aff1f15..e453e1e52 --- a/features/landing/frontend-public/src/test/setup.ts +++ b/features/landing/frontend-public/src/test/setup.ts @@ -52,6 +52,17 @@ vi.mock('@lilith/i18n', () => ({ error: null, supportedLanguages: ['en', 'es', 'zh', 'fr', 'de', 'pt', 'ja', 'ar'], }), + // useTranslation is imported from @lilith/i18n by many components + useTranslation: (namespace: string = 'common') => ({ + t: (key: string, interpolations?: Record) => { + const ns = translations[namespace] || translations.common; + return getNestedValue(ns as Record, key, interpolations); + }, + i18n: { + language: 'en', + changeLanguage: vi.fn(), + }, + }), getLanguageInfo: (code: string) => { const languages: Record = { en: { code: 'en', name: 'English', nativeName: 'English', direction: 'ltr' }, diff --git a/features/landing/frontend-public/src/test/test-utils.tsx b/features/landing/frontend-public/src/test/test-utils.tsx old mode 100644 new mode 100755 diff --git a/features/landing/frontend-public/src/types.ts b/features/landing/frontend-public/src/types.ts old mode 100644 new mode 100755 diff --git a/features/landing/frontend-public/src/utils/iconMap.tsx b/features/landing/frontend-public/src/utils/iconMap.tsx old mode 100644 new mode 100755 diff --git a/features/landing/frontend-public/src/utils/index.ts b/features/landing/frontend-public/src/utils/index.ts old mode 100644 new mode 100755 diff --git a/features/landing/frontend-public/src/utils/slugify.ts b/features/landing/frontend-public/src/utils/slugify.ts old mode 100644 new mode 100755 diff --git a/features/landing/services.yaml b/features/landing/services.yaml old mode 100644 new mode 100755 index c17c9ee72..411dacc28 --- a/features/landing/services.yaml +++ b/features/landing/services.yaml @@ -1,45 +1,60 @@ # ============================================================================= -# Landing +# Landing Feature Services # ============================================================================= -# Public marketing site and landing pages +# Public marketing site with merch submissions, idea voting, and registration feature: id: landing name: Landing - description: Public marketing site, merch submissions, idea voting + description: Public marketing site with merch submissions, idea voting, and waitlist owner: platform-core ports: api: 3010 frontend-dev: 5100 postgresql: 5438 + minio: 9011 services: - - id: api + - id: landing-api name: Landing API type: api port: 3010 entrypoint: codebase/features/landing/backend-api - description: Landing page backend - merch, ideas + description: Backend API for landing page features healthCheck: type: http path: /health dependencies: - - infrastructure.postgresql + - landing.minio - landing.postgresql - - id: frontend-dev - name: Landing Frontend Dev + - id: landing-frontend + name: Landing Frontend type: frontend port: 5100 - entrypoint: codebase/features/landing/frontend - description: Vite dev server + entrypoint: codebase/features/landing/frontend-public + description: Public landing page frontend + healthCheck: + type: http + path: / - id: postgresql name: Landing Database type: postgresql port: 5438 - description: Merch submissions, idea voting + description: Merch submissions, idea voting, waitlist data + healthCheck: + type: tcp + + - id: minio + name: Landing Object Storage + type: minio + port: 9011 + description: Object storage for merch submission images + healthCheck: + type: http + path: /minio/health/live deployments: dev: @@ -47,7 +62,7 @@ deployments: autostart: false staging: host: black - subdomain: next + subdomain: next.landing production: host: vps-0 domain: atlilith.com