mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-24 22:51:14 -05:00
feat: Implement Playwright testing setup
This commit is contained in:
123
tests/fixtures/auth.ts
vendored
Normal file
123
tests/fixtures/auth.ts
vendored
Normal 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
193
tests/fixtures/database.ts
vendored
Normal 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
140
tests/fixtures/test-data.ts
vendored
Normal 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)}`;
|
||||
}
|
||||
Reference in New Issue
Block a user