chore(core): 🔧 Update TypeScript files in core directory
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
324973d4ad
commit
bfdf9674ad
6 changed files with 622 additions and 42 deletions
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
44
features/seo/frontend-public/playwright.config.local.ts
Normal file
44
features/seo/frontend-public/playwright.config.local.ts
Normal 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',
|
||||
})
|
||||
Binary file not shown.
Loading…
Add table
Reference in a new issue