feat: Implement Playwright testing setup

This commit is contained in:
gpt-engineer-app[bot]
2025-10-30 15:42:28 +00:00
parent 41ae88d1bc
commit 8ac61e01e3
16 changed files with 1700 additions and 0 deletions

123
tests/fixtures/auth.ts vendored Normal file
View File

@@ -0,0 +1,123 @@
/**
* Authentication Fixtures for Playwright Tests
*
* Manages authentication state for different user roles.
* Creates reusable auth states to avoid logging in for every test.
*/
import { chromium, type FullConfig } from '@playwright/test';
import { setupTestUser, supabase } from './database';
import * as fs from 'fs';
import * as path from 'path';
const TEST_USERS = {
user: {
email: process.env.TEST_USER_EMAIL || 'test-user@thrillwiki.test',
password: process.env.TEST_USER_PASSWORD || 'TestUser123!',
role: 'user' as const,
},
moderator: {
email: process.env.TEST_MODERATOR_EMAIL || 'test-moderator@thrillwiki.test',
password: process.env.TEST_MODERATOR_PASSWORD || 'TestModerator123!',
role: 'moderator' as const,
},
admin: {
email: process.env.TEST_ADMIN_EMAIL || 'test-admin@thrillwiki.test',
password: process.env.TEST_ADMIN_PASSWORD || 'TestAdmin123!',
role: 'admin' as const,
},
superuser: {
email: process.env.TEST_SUPERUSER_EMAIL || 'test-superuser@thrillwiki.test',
password: process.env.TEST_SUPERUSER_PASSWORD || 'TestSuperuser123!',
role: 'superuser' as const,
},
};
/**
* Setup authentication states for all test users
*/
export async function setupAuthStates(config: FullConfig): Promise<void> {
const baseURL = config.projects[0].use.baseURL || 'http://localhost:8080';
// Ensure .auth directory exists
const authDir = path.join(process.cwd(), '.auth');
if (!fs.existsSync(authDir)) {
fs.mkdirSync(authDir, { recursive: true });
}
const browser = await chromium.launch();
for (const [roleName, userData] of Object.entries(TEST_USERS)) {
const context = await browser.newContext();
const page = await context.newPage();
try {
// Create test user if doesn't exist
await setupTestUser(userData.email, userData.password, userData.role);
// Navigate to login page
await page.goto(`${baseURL}/auth`);
// Wait for page to load
await page.waitForLoadState('networkidle');
// Fill login form
await page.fill('input[type="email"]', userData.email);
await page.fill('input[type="password"]', userData.password);
// Click login button
await page.click('button[type="submit"]');
// Wait for navigation to complete
await page.waitForURL('**/', { timeout: 10000 });
// Save authenticated state
const authFile = path.join(authDir, `${roleName}.json`);
await context.storageState({ path: authFile });
console.log(`✓ Created auth state for ${roleName}`);
} catch (error) {
console.error(`✗ Failed to create auth state for ${roleName}:`, error);
throw error;
} finally {
await context.close();
}
}
await browser.close();
}
/**
* Get auth credentials for a specific role
*/
export function getTestUserCredentials(role: keyof typeof TEST_USERS) {
return TEST_USERS[role];
}
/**
* Login programmatically (for use within tests)
*/
export async function loginAsUser(
email: string,
password: string
): Promise<{ userId: string; accessToken: string }> {
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) throw error;
if (!data.user || !data.session) throw new Error('Login failed');
return {
userId: data.user.id,
accessToken: data.session.access_token,
};
}
/**
* Logout programmatically
*/
export async function logout(): Promise<void> {
await supabase.auth.signOut();
}

193
tests/fixtures/database.ts vendored Normal file
View File

@@ -0,0 +1,193 @@
/**
* Database Fixtures for Playwright Tests
*
* Provides direct database access for test setup and teardown using service role.
* IMPORTANT: Only use for test data management, never in production code!
*/
import { createClient } from '@supabase/supabase-js';
import type { Database } from '@/integrations/supabase/types';
const supabaseUrl = 'https://ydvtmnrszybqnbcqbdcy.supabase.co';
const supabaseAnonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InlkdnRtbnJzenlicW5iY3FiZGN5Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTgzMjYzNTYsImV4cCI6MjA3MzkwMjM1Nn0.DM3oyapd_omP5ZzIlrT0H9qBsiQBxBRgw2tYuqgXKX4';
// For test setup/teardown only - requires service role key
const supabaseServiceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
// Regular client for authenticated operations
export const supabase = createClient<Database>(supabaseUrl, supabaseAnonKey);
// Service role client for test setup/teardown (bypasses RLS)
export const supabaseAdmin = supabaseServiceRoleKey
? createClient<Database>(supabaseUrl, supabaseServiceRoleKey)
: null;
/**
* Create a test user with specific role
*/
export async function setupTestUser(
email: string,
password: string,
role: 'user' | 'moderator' | 'admin' | 'superuser' = 'user'
): Promise<{ userId: string; email: string }> {
if (!supabaseAdmin) {
throw new Error('Service role key not configured');
}
// Create user in auth
const { data: authData, error: authError } = await supabaseAdmin.auth.admin.createUser({
email,
password,
email_confirm: true,
});
if (authError) throw authError;
if (!authData.user) throw new Error('User creation failed');
const userId = authData.user.id;
// Create profile
const { error: profileError } = await supabaseAdmin
.from('profiles')
.upsert({
id: userId,
username: email.split('@')[0],
email,
role,
is_test_data: true,
});
if (profileError) throw profileError;
return { userId, email };
}
/**
* Clean up all test data
*/
export async function cleanupTestData(): Promise<void> {
if (!supabaseAdmin) {
throw new Error('Service role key not configured');
}
// Delete in dependency order (child tables first)
const tables = [
'ride_photos',
'park_photos',
'submission_items',
'content_submissions',
'ride_versions',
'park_versions',
'company_versions',
'ride_model_versions',
'rides',
'ride_models',
'parks',
'companies',
];
for (const table of tables) {
await supabaseAdmin
.from(table as any)
.delete()
.eq('is_test_data', true);
}
// Delete test profiles
const { data: profiles } = await supabaseAdmin
.from('profiles')
.select('id')
.eq('is_test_data', true);
if (profiles) {
for (const profile of profiles) {
await supabaseAdmin.auth.admin.deleteUser(profile.id);
}
}
}
/**
* Query database directly for assertions
*/
export async function queryDatabase<T = any>(
table: string,
query: (qb: any) => any
): Promise<T[]> {
if (!supabaseAdmin) {
throw new Error('Service role key not configured');
}
const { data, error } = await query(supabaseAdmin.from(table));
if (error) throw error;
return data || [];
}
/**
* Wait for a version to be created
*/
export async function waitForVersion(
entityId: string,
versionNumber: number,
table: string,
maxWaitMs: number = 5000
): Promise<boolean> {
if (!supabaseAdmin) {
throw new Error('Service role key not configured');
}
const startTime = Date.now();
while (Date.now() - startTime < maxWaitMs) {
const { data } = await supabaseAdmin
.from(table as any)
.select('version_number')
.eq('entity_id', entityId)
.eq('version_number', versionNumber)
.single();
if (data) return true;
await new Promise(resolve => setTimeout(resolve, 500));
}
return false;
}
/**
* Approve a submission directly (for test setup)
*/
export async function approveSubmissionDirect(submissionId: string): Promise<void> {
if (!supabaseAdmin) {
throw new Error('Service role key not configured');
}
const { error } = await supabaseAdmin.rpc('approve_submission', {
submission_id: submissionId,
});
if (error) throw error;
}
/**
* Get test data statistics
*/
export async function getTestDataStats(): Promise<Record<string, number>> {
if (!supabaseAdmin) {
throw new Error('Service role key not configured');
}
const tables = ['parks', 'rides', 'companies', 'ride_models', 'content_submissions'];
const stats: Record<string, number> = {};
for (const table of tables) {
const { count } = await supabaseAdmin
.from(table as any)
.select('*', { count: 'exact', head: true })
.eq('is_test_data', true);
stats[table] = count || 0;
}
return stats;
}

140
tests/fixtures/test-data.ts vendored Normal file
View File

@@ -0,0 +1,140 @@
/**
* Test Data Generators
*
* Factory functions for creating realistic test data.
*/
import { faker } from '@faker-js/faker';
export interface ParkTestData {
name: string;
slug: string;
description: string;
park_type: string;
status: string;
location_country: string;
location_city: string;
latitude: number;
longitude: number;
opened_date: string;
is_test_data: boolean;
}
export interface RideTestData {
name: string;
slug: string;
description: string;
category: string;
status: string;
park_id: string;
opened_date: string;
is_test_data: boolean;
}
export interface CompanyTestData {
name: string;
slug: string;
description: string;
company_type: string;
person_type: string;
founded_date: string;
is_test_data: boolean;
}
export interface RideModelTestData {
name: string;
slug: string;
description: string;
category: string;
manufacturer_id: string;
is_test_data: boolean;
}
/**
* Generate random park test data
*/
export function generateParkData(overrides?: Partial<ParkTestData>): ParkTestData {
const name = faker.company.name() + ' Park';
return {
name,
slug: faker.helpers.slugify(name).toLowerCase(),
description: faker.lorem.paragraphs(2),
park_type: faker.helpers.arrayElement(['theme_park', 'amusement_park', 'water_park']),
status: faker.helpers.arrayElement(['operating', 'closed', 'under_construction']),
location_country: faker.location.countryCode(),
location_city: faker.location.city(),
latitude: parseFloat(faker.location.latitude()),
longitude: parseFloat(faker.location.longitude()),
opened_date: faker.date.past({ years: 50 }).toISOString().split('T')[0],
is_test_data: true,
...overrides,
};
}
/**
* Generate random ride test data
*/
export function generateRideData(parkId: string, overrides?: Partial<RideTestData>): RideTestData {
const name = faker.word.adjective() + ' ' + faker.word.noun();
return {
name,
slug: faker.helpers.slugify(name).toLowerCase(),
description: faker.lorem.paragraphs(2),
category: faker.helpers.arrayElement(['roller_coaster', 'flat_ride', 'water_ride', 'dark_ride']),
status: faker.helpers.arrayElement(['operating', 'closed', 'sbno']),
park_id: parkId,
opened_date: faker.date.past({ years: 30 }).toISOString().split('T')[0],
is_test_data: true,
...overrides,
};
}
/**
* Generate random company test data
*/
export function generateCompanyData(
companyType: 'manufacturer' | 'designer' | 'operator' | 'property_owner',
overrides?: Partial<CompanyTestData>
): CompanyTestData {
const name = faker.company.name();
return {
name,
slug: faker.helpers.slugify(name).toLowerCase(),
description: faker.lorem.paragraphs(2),
company_type: companyType,
person_type: faker.helpers.arrayElement(['individual', 'company']),
founded_date: faker.date.past({ years: 100 }).toISOString().split('T')[0],
is_test_data: true,
...overrides,
};
}
/**
* Generate random ride model test data
*/
export function generateRideModelData(
manufacturerId: string,
overrides?: Partial<RideModelTestData>
): RideModelTestData {
const name = faker.word.adjective() + ' Model';
return {
name,
slug: faker.helpers.slugify(name).toLowerCase(),
description: faker.lorem.paragraphs(2),
category: faker.helpers.arrayElement(['roller_coaster', 'flat_ride', 'water_ride']),
manufacturer_id: manufacturerId,
is_test_data: true,
...overrides,
};
}
/**
* Generate unique test identifier
*/
export function generateTestId(): string {
return `test-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}