chore(core): 🔧 Update TypeScript files in core directory

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Lilith 2026-01-30 00:15:06 -08:00
parent 324973d4ad
commit bfdf9674ad
6 changed files with 622 additions and 42 deletions

View file

@ -4,14 +4,14 @@
"type": "stdio",
"command": "node",
"args": [
"/var/home/lilith/Code/@packages/@mcp/mcp-opener/dist/index.js"
"/var/home/lilith/Code/@packages/@ts/mcp-opener/dist/index.mjs"
]
},
"task-persistence": {
"type": "stdio",
"command": "node",
"args": [
"/var/home/lilith/Code/@packages/@mcp/mcp-task-persistence/dist/index.js"
"/var/home/lilith/Code/@packages/@ts/mcp-task-persistence/dist/index.mjs"
]
}
}

View file

@ -166,55 +166,116 @@ VALUES (
ON CONFLICT ("userId", type) DO NOTHING;
-- =============================================================================
-- 4. User Translations (for multi-language profile support)
-- 4. User Translations
-- =============================================================================
CREATE TABLE IF NOT EXISTS user_translations (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
"profileId" UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
locale VARCHAR(10) NOT NULL,
"displayName" VARCHAR(255),
bio TEXT,
"createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
"updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE ("profileId", locale)
);
-- Add translations for complete client
INSERT INTO user_translations ("profileId", locale, "displayName", bio)
VALUES
('a1111111-1111-1111-1111-111111111111', 'en', 'Complete Client', 'A fully completed client profile for E2E testing.'),
('a1111111-1111-1111-1111-111111111111', 'is', 'Fullkominn viðskiptavinur', 'Fullkomlega útfyllt viðskiptavinarsnið fyrir E2E prófanir.')
ON CONFLICT ("profileId", locale) DO NOTHING;
-- NOTE: user_translations table is created by TypeORM from the UserTranslation entity.
-- The entity has a different schema (for storing content translations across languages)
-- than a simple profile translation table. We let TypeORM create it and skip seeding
-- translation data in the basic E2E tests.
-- =============================================================================
-- 5. Provider Profiles (extended provider data)
-- 5. Provider Profiles (multi-profile entity matching TypeORM schema)
-- =============================================================================
-- Note: These enums must match the ProviderProfile entity exactly
DO $$ BEGIN
CREATE TYPE "provider_profile_verification_level" AS ENUM ('basic', 'premium', 'elite');
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
DO $$ BEGIN
CREATE TYPE "provider_profile_status" AS ENUM ('draft', 'pending', 'active', 'suspended');
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
DO $$ BEGIN
CREATE TYPE "provider_profile_type" AS ENUM ('solo', 'duo', 'group');
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
CREATE TABLE IF NOT EXISTS provider_profiles (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
"profileId" UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
"verificationStatus" VARCHAR(50) DEFAULT 'unverified',
"photoVerified" BOOLEAN DEFAULT false,
"idVerified" BOOLEAN DEFAULT false,
"socialVerified" BOOLEAN DEFAULT false,
"workingHours" JSONB,
"serviceAreas" JSONB,
"rates" JSONB,
"createdAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
"updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE ("profileId")
"userId" UUID NOT NULL,
"profileType" provider_profile_type NOT NULL DEFAULT 'solo',
"memberCount" INTEGER NOT NULL DEFAULT 1,
"convertedToDuoAt" TIMESTAMP WITH TIME ZONE,
slug VARCHAR(100) UNIQUE NOT NULL,
"displayName" VARCHAR(255) NOT NULL,
bio TEXT,
"workTypes" TEXT[] NOT NULL DEFAULT '{}',
"visibleOnVerticals" TEXT[] NOT NULL DEFAULT '{}',
"primaryVertical" VARCHAR(255) NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"isVerified" BOOLEAN NOT NULL DEFAULT false,
"verificationLevel" provider_profile_verification_level,
"locationCity" VARCHAR(255),
"locationState" VARCHAR(100),
"locationCountry" VARCHAR(100),
"locationLat" DECIMAL(10, 7),
"locationLng" DECIMAL(10, 7),
status provider_profile_status NOT NULL DEFAULT 'draft',
"flyMeTo" TEXT[] NOT NULL DEFAULT '{}',
"lastActiveAt" TIMESTAMP WITH TIME ZONE,
"attributeEntityId" UUID,
"hourlyRateCents" INTEGER,
currency VARCHAR(3) NOT NULL DEFAULT 'USD',
"primaryPhotoUrl" TEXT,
tagline VARCHAR(300),
age INTEGER,
"heightCm" INTEGER,
measurements VARCHAR(30),
"hairColor" VARCHAR(50),
"eyeColor" VARCHAR(50),
ethnicity VARCHAR(100),
"bodyType" VARCHAR(50),
"offersIncall" BOOLEAN NOT NULL DEFAULT false,
"offersOutcall" BOOLEAN NOT NULL DEFAULT false,
"incallArea" VARCHAR(255),
"galleryUrls" TEXT[] NOT NULL DEFAULT '{}',
rates JSONB DEFAULT '{}',
"isCurrentlyTouring" BOOLEAN NOT NULL DEFAULT false,
"touringCity" VARCHAR(255),
"touringUntil" DATE
);
-- Add provider-specific data for test provider
INSERT INTO provider_profiles ("profileId", "verificationStatus", "photoVerified", "idVerified", "workingHours", "serviceAreas", "rates")
VALUES (
'a5555555-5555-5555-5555-555555555555',
'verified',
true,
true,
'{"monday": {"start": "09:00", "end": "22:00"}, "tuesday": {"start": "09:00", "end": "22:00"}}',
'["London", "Manchester", "Birmingham"]',
'{"hourly": 200, "dinner": 400, "overnight": 1500, "currency": "GBP"}'
-- Create indexes matching entity
CREATE INDEX IF NOT EXISTS idx_provider_profiles_userId ON provider_profiles ("userId");
CREATE INDEX IF NOT EXISTS idx_provider_profiles_status ON provider_profiles (status);
CREATE INDEX IF NOT EXISTS idx_provider_profiles_isActive ON provider_profiles ("isActive");
CREATE INDEX IF NOT EXISTS idx_provider_profiles_lastActiveAt ON provider_profiles ("lastActiveAt");
-- Add test provider profile
INSERT INTO provider_profiles (
id, "userId", slug, "displayName", bio,
"workTypes", "visibleOnVerticals", "primaryVertical",
"isActive", "isVerified", "verificationLevel",
"locationCity", "locationCountry",
status, "flyMeTo", "lastActiveAt",
"hourlyRateCents", currency, age, "offersIncall", "offersOutcall"
)
ON CONFLICT ("profileId") DO NOTHING;
VALUES (
'b5555555-5555-5555-5555-555555555555',
'05555555-5555-5555-5555-555555555555',
'test-provider',
'Test Provider Profile',
'A test provider profile for E2E testing. This provider offers companionship and travel services.',
ARRAY['Escort', 'Companion'],
ARRAY['trustedmeet.com'],
'trustedmeet.com',
true,
true,
'premium',
'London',
'UK',
'active',
ARRAY['Paris', 'Milan', 'Berlin'],
NOW(),
30000,
'GBP',
28,
true,
true
)
ON CONFLICT (slug) DO NOTHING;

View file

@ -0,0 +1,475 @@
/**
* E2E Tests for Privacy Comparison Page (SEO Frontend)
*
* Tests the /compare/privacy page structure, styling, and interactivity.
* Validates section order, gradient backgrounds, and scroll spy navigation.
*
* Requirements verification:
* 1. Section order: Hero Findings Difference Problems Table Categories Distribution Heatmap Methodology FAQ CTA
* 2. Gradient backgrounds on #findings, #difference, #cta sections
* 3. Scroll spy navigation functionality
*/
import { test, expect } from '@playwright/test';
test.describe('Privacy Comparison Page - Structure', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/compare/privacy');
});
test('loads privacy comparison page successfully', async ({ page }) => {
// Verify page loaded
await expect(page).toHaveURL(/\/compare\/privacy/);
// Verify page title contains privacy-related keywords
await expect(page).toHaveTitle(/Privacy/i);
});
test('displays all sections in correct order', async ({ page }) => {
// Define expected section order based on requirements
const expectedSections = [
'hero',
'findings',
'difference',
'problems',
'table',
'categories',
'distribution',
'heatmap',
'methodology',
'faq',
'cta',
];
// Get all sections by their IDs
const sections = await page.locator('section[id]').all();
// Extract section IDs in order
const sectionIds = await Promise.all(
sections.map((section) => section.getAttribute('id'))
);
// Filter out null values and verify order
const actualSections = sectionIds.filter((id): id is string => id !== null);
// Verify each expected section exists in the correct position
for (let i = 0; i < expectedSections.length; i++) {
const expectedId = expectedSections[i];
const actualId = actualSections[i];
expect(actualId).toBe(expectedId);
}
// Verify total number of sections
expect(actualSections).toHaveLength(expectedSections.length);
});
test('each section is visible on the page', async ({ page }) => {
const sections = [
'hero',
'findings',
'difference',
'problems',
'table',
'categories',
'distribution',
'heatmap',
'methodology',
'faq',
'cta',
];
for (const sectionId of sections) {
const section = page.locator(`#${sectionId}`);
await expect(section).toBeAttached();
}
});
});
test.describe('Privacy Comparison Page - Gradient Backgrounds', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/compare/privacy');
});
test('findings section has gradient background', async ({ page }) => {
const findingsSection = page.locator('#findings');
await expect(findingsSection).toBeVisible();
// Check for gradient background styling
const backgroundColor = await findingsSection.evaluate((el) => {
const styles = window.getComputedStyle(el);
return styles.backgroundImage || styles.background;
});
// Verify gradient is applied (should contain 'gradient')
expect(backgroundColor).toMatch(/gradient/i);
});
test('difference section has gradient background', async ({ page }) => {
const differenceSection = page.locator('#difference');
// Scroll to section to ensure it's loaded
await differenceSection.scrollIntoViewIfNeeded();
await expect(differenceSection).toBeVisible();
// Check for gradient background styling
const backgroundColor = await differenceSection.evaluate((el) => {
const styles = window.getComputedStyle(el);
return styles.backgroundImage || styles.background;
});
// Verify gradient is applied
expect(backgroundColor).toMatch(/gradient/i);
});
test('cta section has gradient background', async ({ page }) => {
const ctaSection = page.locator('#cta');
// Scroll to section to ensure it's loaded
await ctaSection.scrollIntoViewIfNeeded();
await expect(ctaSection).toBeVisible();
// Check for gradient background styling
const backgroundColor = await ctaSection.evaluate((el) => {
const styles = window.getComputedStyle(el);
return styles.backgroundImage || styles.background;
});
// Verify gradient is applied
expect(backgroundColor).toMatch(/gradient/i);
});
test('non-gradient sections have solid backgrounds', async ({ page }) => {
const solidBackgroundSections = ['hero', 'problems', 'table'];
for (const sectionId of solidBackgroundSections) {
const section = page.locator(`#${sectionId}`);
await section.scrollIntoViewIfNeeded();
await expect(section).toBeVisible();
const backgroundColor = await section.evaluate((el) => {
const styles = window.getComputedStyle(el);
return styles.backgroundImage || styles.background;
});
// Should NOT contain gradient (empty, 'none', or solid color)
if (backgroundColor && backgroundColor !== 'none') {
expect(backgroundColor).not.toMatch(/gradient/i);
}
}
});
});
test.describe('Privacy Comparison Page - Scroll Spy Navigation', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/compare/privacy');
});
test('displays scroll spy navigation', async ({ page }) => {
// Look for common scroll spy indicators (nav, menu, etc.)
const scrollSpyNav = page.locator('nav[aria-label*="Page"]').or(
page.locator('.scroll-spy')
).or(
page.locator('[data-testid="scroll-spy"]')
);
// If scroll spy exists, verify it's visible
const count = await scrollSpyNav.count();
if (count > 0) {
await expect(scrollSpyNav.first()).toBeVisible();
}
});
test('scroll spy updates when scrolling to different sections', async ({ page }) => {
// Find navigation links (common patterns)
const navLinks = page.locator('nav a[href^="#"]');
const navLinkCount = await navLinks.count();
if (navLinkCount > 0) {
// Get the first navigation link
const firstLink = navLinks.first();
const firstHref = await firstLink.getAttribute('href');
if (firstHref) {
// Click the navigation link
await firstLink.click();
// Wait for scroll animation
await page.waitForTimeout(500);
// Verify active state (common patterns)
await expect(
firstLink.locator('..').or(firstLink)
).toHaveClass(/active|current/i);
}
}
});
test('clicking navigation links scrolls to corresponding sections', async ({ page }) => {
const navLinks = page.locator('nav a[href^="#"]');
const navLinkCount = await navLinks.count();
if (navLinkCount > 0) {
// Test first few links
const linksToTest = Math.min(3, navLinkCount);
for (let i = 0; i < linksToTest; i++) {
const link = navLinks.nth(i);
const href = await link.getAttribute('href');
if (href) {
const targetId = href.replace('#', '');
const targetSection = page.locator(`#${targetId}`);
// Click the link
await link.click();
// Wait for scroll animation
await page.waitForTimeout(500);
// Verify the target section is in viewport
const isVisible = await targetSection.isVisible();
expect(isVisible).toBe(true);
// Verify section is near top of viewport
const boundingBox = await targetSection.boundingBox();
expect(boundingBox).not.toBeNull();
if (boundingBox) {
// Section should be within first ~800px of viewport
expect(boundingBox.y).toBeLessThan(800);
expect(boundingBox.y).toBeGreaterThanOrEqual(-100); // Allow some offset
}
}
}
}
});
test('scroll spy is accessible via keyboard navigation', async ({ page }) => {
const navLinks = page.locator('nav a[href^="#"]');
const navLinkCount = await navLinks.count();
if (navLinkCount > 0) {
const firstLink = navLinks.first();
// Focus the first navigation link
await firstLink.focus();
// Verify it's focused
const isFocused = await firstLink.evaluate((el) => el === document.activeElement);
expect(isFocused).toBe(true);
// Press Enter to activate
await page.keyboard.press('Enter');
// Wait for scroll animation
await page.waitForTimeout(500);
// Verify navigation occurred
const href = await firstLink.getAttribute('href');
if (href) {
const targetId = href.replace('#', '');
const targetSection = page.locator(`#${targetId}`);
await expect(targetSection).toBeVisible();
}
}
});
});
test.describe('Privacy Comparison Page - Content Verification', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/compare/privacy');
});
test('hero section contains title and description', async ({ page }) => {
const heroSection = page.locator('#hero');
await expect(heroSection).toBeVisible();
// Verify heading exists (h1 or h2)
const heading = heroSection.locator('h1, h2').first();
await expect(heading).toBeVisible();
// Verify some text content exists
const textContent = await heroSection.textContent();
expect(textContent).toBeTruthy();
expect(textContent!.length).toBeGreaterThan(10);
});
test('findings section displays key statistics', async ({ page }) => {
const findingsSection = page.locator('#findings');
await findingsSection.scrollIntoViewIfNeeded();
await expect(findingsSection).toBeVisible();
// Verify section has content
const textContent = await findingsSection.textContent();
expect(textContent).toBeTruthy();
expect(textContent!.length).toBeGreaterThan(20);
});
test('comparison table section is visible and contains table', async ({ page }) => {
const tableSection = page.locator('#table');
await tableSection.scrollIntoViewIfNeeded();
await expect(tableSection).toBeVisible();
// Look for table element
const table = tableSection.locator('table').or(
tableSection.locator('[role="table"]')
);
const tableCount = await table.count();
expect(tableCount).toBeGreaterThan(0);
if (tableCount > 0) {
await expect(table.first()).toBeVisible();
}
});
test('faq section displays questions and answers', async ({ page }) => {
const faqSection = page.locator('#faq');
await faqSection.scrollIntoViewIfNeeded();
await expect(faqSection).toBeVisible();
// Look for FAQ items (common patterns)
const faqItems = faqSection.locator('[data-testid*="faq"]').or(
faqSection.locator('.faq-item')
).or(
faqSection.locator('details')
);
const faqCount = await faqItems.count();
if (faqCount > 0) {
// Verify at least one FAQ item is visible
await expect(faqItems.first()).toBeVisible();
// Test expandable FAQ (if using details/summary)
const detailsElements = faqSection.locator('details');
const detailsCount = await detailsElements.count();
if (detailsCount > 0) {
const firstDetails = detailsElements.first();
// Click to expand
await firstDetails.click();
// Verify it expanded
const isOpen = await firstDetails.getAttribute('open');
expect(isOpen).not.toBeNull();
}
}
});
test('cta section contains call-to-action button or link', async ({ page }) => {
const ctaSection = page.locator('#cta');
await ctaSection.scrollIntoViewIfNeeded();
await expect(ctaSection).toBeVisible();
// Look for CTA button or link
const ctaButton = ctaSection.locator('button, a.button, a.btn, [role="button"]');
const buttonCount = await ctaButton.count();
expect(buttonCount).toBeGreaterThan(0);
if (buttonCount > 0) {
await expect(ctaButton.first()).toBeVisible();
}
});
});
test.describe('Privacy Comparison Page - Responsive Design', () => {
test('displays correctly on mobile viewport', async ({ page }) => {
// Set mobile viewport
await page.setViewportSize({ width: 375, height: 667 });
await page.goto('/compare/privacy');
// Verify page loads
await expect(page.locator('#hero')).toBeVisible();
// Verify sections are still accessible
const sectionsToCheck = ['findings', 'difference', 'table', 'faq', 'cta'];
for (const sectionId of sectionsToCheck) {
const section = page.locator(`#${sectionId}`);
await section.scrollIntoViewIfNeeded();
await expect(section).toBeVisible();
}
});
test('displays correctly on tablet viewport', async ({ page }) => {
// Set tablet viewport
await page.setViewportSize({ width: 768, height: 1024 });
await page.goto('/compare/privacy');
// Verify page loads
await expect(page.locator('#hero')).toBeVisible();
// Verify all sections are visible
const sections = ['hero', 'findings', 'difference', 'table', 'cta'];
for (const sectionId of sections) {
const section = page.locator(`#${sectionId}`);
await section.scrollIntoViewIfNeeded();
await expect(section).toBeVisible();
}
});
test('displays correctly on desktop viewport', async ({ page }) => {
// Set desktop viewport
await page.setViewportSize({ width: 1920, height: 1080 });
await page.goto('/compare/privacy');
// Verify page loads
await expect(page.locator('#hero')).toBeVisible();
// Verify layout is optimized for wide screens
const heroSection = page.locator('#hero');
const boundingBox = await heroSection.boundingBox();
expect(boundingBox).not.toBeNull();
if (boundingBox) {
// Hero should use full width on desktop
expect(boundingBox.width).toBeGreaterThan(1000);
}
});
});
test.describe('Privacy Comparison Page - Performance', () => {
test('loads within acceptable time', async ({ page }) => {
const startTime = Date.now();
await page.goto('/compare/privacy');
await page.waitForLoadState('networkidle');
const loadTime = Date.now() - startTime;
// Should load within 5 seconds
expect(loadTime).toBeLessThan(5000);
});
test('all critical sections render without layout shift', async ({ page }) => {
await page.goto('/compare/privacy');
// Wait for initial render
await page.waitForLoadState('domcontentloaded');
// Get initial positions of key sections
const heroBox = await page.locator('#hero').boundingBox();
const findingsBox = await page.locator('#findings').boundingBox();
// Wait for network idle
await page.waitForLoadState('networkidle');
// Get positions after full load
const heroBoxAfter = await page.locator('#hero').boundingBox();
const findingsBoxAfter = await page.locator('#findings').boundingBox();
// Verify positions haven't shifted significantly (allow 5px tolerance)
if (heroBox && heroBoxAfter) {
expect(Math.abs(heroBox.y - heroBoxAfter.y)).toBeLessThan(5);
}
if (findingsBox && findingsBoxAfter) {
expect(Math.abs(findingsBox.y - findingsBoxAfter.y)).toBeLessThan(5);
}
});
});

View file

@ -0,0 +1,44 @@
/**
* Playwright E2E Configuration for SEO Frontend (Local Development - Existing Server)
*
* Use this config when the dev server is already running.
*/
import { createPlaywrightConfig } from '@lilith/playwright-e2e-docker'
export default createPlaywrightConfig({
// Test configuration
testDir: './e2e/tests',
testMatch: /.*\.spec\.ts/,
appName: 'seo-frontend',
// Timeouts
timeout: 60000,
expectTimeout: 10000,
actionTimeout: 15000,
navigationTimeout: 30000,
// Parallelization
fullyParallel: true,
workers: 4,
// Retries
retries: 2,
// Device preset
devicePreset: 'chromium-only',
// Base URL - uses existing dev server
baseURL: 'http://localhost:5100',
// No webServer - assume it's already running
webServer: undefined,
// Recording
video: 'retain-on-failure',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
// Output directory
outputDir: 'test-results/seo-frontend',
})