mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-28 08:46:57 -05:00
feat: Implement Playwright testing setup
This commit is contained in:
134
tests/e2e/auth/login.spec.ts
Normal file
134
tests/e2e/auth/login.spec.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* Login E2E Tests
|
||||
*
|
||||
* Tests authentication flow, session persistence, and error handling.
|
||||
*/
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { LoginPage } from '../../helpers/page-objects/LoginPage';
|
||||
import { getTestUserCredentials, logout } from '../../fixtures/auth';
|
||||
|
||||
// These tests run without pre-authenticated state
|
||||
test.use({ storageState: { cookies: [], origins: [] } });
|
||||
|
||||
test.describe('Login Flow', () => {
|
||||
let loginPage: LoginPage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
loginPage = new LoginPage(page);
|
||||
await loginPage.goto();
|
||||
});
|
||||
|
||||
test('should login successfully with valid credentials', async ({ page }) => {
|
||||
const { email, password } = getTestUserCredentials('user');
|
||||
|
||||
await loginPage.login(email, password);
|
||||
await loginPage.expectLoginSuccess();
|
||||
|
||||
// Verify we're redirected to homepage
|
||||
await expect(page).toHaveURL('/');
|
||||
});
|
||||
|
||||
test('should show error with invalid password', async ({ page }) => {
|
||||
const { email } = getTestUserCredentials('user');
|
||||
|
||||
await loginPage.login(email, 'wrongpassword123');
|
||||
await loginPage.expectLoginError();
|
||||
|
||||
// Verify we're still on auth page
|
||||
await expect(page).toHaveURL(/\/auth/);
|
||||
});
|
||||
|
||||
test('should show error with non-existent email', async ({ page }) => {
|
||||
await loginPage.login('nonexistent@example.com', 'password123');
|
||||
await loginPage.expectLoginError();
|
||||
});
|
||||
|
||||
test('should persist session after page refresh', async ({ page }) => {
|
||||
const { email, password } = getTestUserCredentials('user');
|
||||
|
||||
// Login
|
||||
await loginPage.login(email, password);
|
||||
await loginPage.expectLoginSuccess();
|
||||
|
||||
// Reload page
|
||||
await page.reload();
|
||||
|
||||
// Should still be logged in (not redirected to /auth)
|
||||
await expect(page).not.toHaveURL(/\/auth/);
|
||||
});
|
||||
|
||||
test('should clear session on logout', async ({ page }) => {
|
||||
const { email, password } = getTestUserCredentials('user');
|
||||
|
||||
// Login
|
||||
await loginPage.login(email, password);
|
||||
await loginPage.expectLoginSuccess();
|
||||
|
||||
// Logout
|
||||
await page.click('button:has-text("Logout")').or(page.click('[data-testid="logout"]'));
|
||||
|
||||
// Should be redirected to auth or homepage
|
||||
// And trying to access protected route should redirect to auth
|
||||
await page.goto('/moderation/queue');
|
||||
await expect(page).toHaveURL(/\/auth/);
|
||||
});
|
||||
|
||||
test('should validate email format', async ({ page }) => {
|
||||
await loginPage.login('invalid-email', 'password123');
|
||||
|
||||
// Should show validation error
|
||||
await expect(page.getByText(/invalid.*email/i)).toBeVisible();
|
||||
});
|
||||
|
||||
test('should require password', async ({ page }) => {
|
||||
const { email } = getTestUserCredentials('user');
|
||||
|
||||
await page.fill('input[type="email"]', email);
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
// Should show validation error
|
||||
await expect(page.getByText(/password.*required/i)).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Role-Based Access', () => {
|
||||
test('moderator can access moderation queue', async ({ browser }) => {
|
||||
const context = await browser.newContext({ storageState: '.auth/moderator.json' });
|
||||
const page = await context.newPage();
|
||||
|
||||
await page.goto('/moderation/queue');
|
||||
|
||||
// Should not be redirected
|
||||
await expect(page).toHaveURL(/\/moderation\/queue/);
|
||||
|
||||
// Page should load successfully
|
||||
await expect(page.getByText(/moderation.*queue/i)).toBeVisible();
|
||||
|
||||
await context.close();
|
||||
});
|
||||
|
||||
test('regular user cannot access moderation queue', async ({ browser }) => {
|
||||
const context = await browser.newContext({ storageState: '.auth/user.json' });
|
||||
const page = await context.newPage();
|
||||
|
||||
await page.goto('/moderation/queue');
|
||||
|
||||
// Should be redirected or see access denied
|
||||
await expect(page.getByText(/access denied/i).or(page.getByText(/not authorized/i))).toBeVisible();
|
||||
|
||||
await context.close();
|
||||
});
|
||||
|
||||
test('admin can access admin panel', async ({ browser }) => {
|
||||
const context = await browser.newContext({ storageState: '.auth/admin.json' });
|
||||
const page = await context.newPage();
|
||||
|
||||
await page.goto('/admin/users');
|
||||
|
||||
// Should not be redirected
|
||||
await expect(page).toHaveURL(/\/admin/);
|
||||
|
||||
await context.close();
|
||||
});
|
||||
});
|
||||
139
tests/e2e/moderation/approval-flow.spec.ts
Normal file
139
tests/e2e/moderation/approval-flow.spec.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* Moderation Approval Flow E2E Tests
|
||||
*
|
||||
* Tests the complete submission approval process.
|
||||
*/
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { ModerationQueuePage } from '../../helpers/page-objects/ModerationQueuePage';
|
||||
import { ParkCreationPage } from '../../helpers/page-objects/ParkCreationPage';
|
||||
import { generateParkData, generateTestId } from '../../fixtures/test-data';
|
||||
import { queryDatabase, cleanupTestData, waitForVersion } from '../../fixtures/database';
|
||||
|
||||
test.describe('Submission Approval Flow', () => {
|
||||
test.afterAll(async () => {
|
||||
await cleanupTestData();
|
||||
});
|
||||
|
||||
test('should approve park submission and create entity', async ({ browser }) => {
|
||||
// Step 1: Create submission as regular user
|
||||
const userContext = await browser.newContext({ storageState: '.auth/user.json' });
|
||||
const userPage = await userContext.newPage();
|
||||
|
||||
const parkData = generateParkData({
|
||||
name: `Test Park ${generateTestId()}`,
|
||||
});
|
||||
|
||||
const parkCreationPage = new ParkCreationPage(userPage);
|
||||
await parkCreationPage.goto();
|
||||
await parkCreationPage.fillBasicInfo(parkData.name, parkData.description);
|
||||
await parkCreationPage.submitForm();
|
||||
await parkCreationPage.expectSuccess();
|
||||
|
||||
await userContext.close();
|
||||
|
||||
// Step 2: Approve as moderator
|
||||
const modContext = await browser.newContext({ storageState: '.auth/moderator.json' });
|
||||
const modPage = await modContext.newPage();
|
||||
|
||||
const moderationPage = new ModerationQueuePage(modPage);
|
||||
await moderationPage.goto();
|
||||
|
||||
// Find the submission
|
||||
await moderationPage.expectSubmissionVisible(parkData.name);
|
||||
|
||||
// Claim it
|
||||
await moderationPage.claimSubmission(0);
|
||||
|
||||
// Approve it
|
||||
await moderationPage.approveSubmission('Looks good!');
|
||||
|
||||
// Step 3: Verify entity created in database
|
||||
await modPage.waitForTimeout(2000); // Give DB time to process
|
||||
|
||||
const parks = await queryDatabase('parks', (qb) =>
|
||||
qb.select('*').eq('name', parkData.name)
|
||||
);
|
||||
|
||||
expect(parks).toHaveLength(1);
|
||||
expect(parks[0].is_test_data).toBe(true);
|
||||
|
||||
// Step 4: Verify version created
|
||||
const versions = await queryDatabase('park_versions', (qb) =>
|
||||
qb.select('*').eq('park_id', parks[0].id).eq('version_number', 1)
|
||||
);
|
||||
|
||||
expect(versions).toHaveLength(1);
|
||||
expect(versions[0].change_type).toBe('created');
|
||||
|
||||
// Step 5: Verify submission marked as approved
|
||||
const submissions = await queryDatabase('content_submissions', (qb) =>
|
||||
qb.select('*').eq('entity_type', 'park').contains('submission_data', { name: parkData.name })
|
||||
);
|
||||
|
||||
expect(submissions[0].status).toBe('approved');
|
||||
expect(submissions[0].approved_by).toBeTruthy();
|
||||
expect(submissions[0].approved_at).toBeTruthy();
|
||||
|
||||
// Step 6: Verify lock released
|
||||
expect(submissions[0].assigned_to).toBeNull();
|
||||
expect(submissions[0].locked_until).toBeNull();
|
||||
|
||||
await modContext.close();
|
||||
});
|
||||
|
||||
test('should show change comparison for edits', async ({ browser }) => {
|
||||
// This test would require:
|
||||
// 1. Creating and approving a park
|
||||
// 2. Editing the park
|
||||
// 3. Viewing the edit in moderation queue
|
||||
// 4. Verifying change comparison displays
|
||||
// Left as TODO - requires more complex setup
|
||||
});
|
||||
|
||||
test('should send notification to submitter on approval', async ({ browser }) => {
|
||||
// This test would verify that Novu notification is sent
|
||||
// Left as TODO - requires Novu testing setup
|
||||
});
|
||||
|
||||
test('should prevent approval without lock', async ({ browser }) => {
|
||||
// Create submission as user
|
||||
const userContext = await browser.newContext({ storageState: '.auth/user.json' });
|
||||
const userPage = await userContext.newPage();
|
||||
|
||||
const parkData = generateParkData({
|
||||
name: `Test Park ${generateTestId()}`,
|
||||
});
|
||||
|
||||
const parkCreationPage = new ParkCreationPage(userPage);
|
||||
await parkCreationPage.goto();
|
||||
await parkCreationPage.fillBasicInfo(parkData.name, parkData.description);
|
||||
await parkCreationPage.submitForm();
|
||||
await parkCreationPage.expectSuccess();
|
||||
|
||||
await userContext.close();
|
||||
|
||||
// Try to approve as moderator WITHOUT claiming
|
||||
const modContext = await browser.newContext({ storageState: '.auth/moderator.json' });
|
||||
const modPage = await modContext.newPage();
|
||||
|
||||
const moderationPage = new ModerationQueuePage(modPage);
|
||||
await moderationPage.goto();
|
||||
|
||||
// Approve button should be disabled or not visible
|
||||
const approveButton = modPage.locator('button:has-text("Approve")').first();
|
||||
await expect(approveButton).toBeDisabled();
|
||||
|
||||
await modContext.close();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Bulk Approval', () => {
|
||||
test('should approve all items in submission', async ({ browser }) => {
|
||||
// TODO: Test approving all submission items at once
|
||||
});
|
||||
|
||||
test('should allow selective item approval', async ({ browser }) => {
|
||||
// TODO: Test approving only specific items from a submission
|
||||
});
|
||||
});
|
||||
138
tests/e2e/submission/park-creation.spec.ts
Normal file
138
tests/e2e/submission/park-creation.spec.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* Park Creation E2E Tests
|
||||
*
|
||||
* Tests the complete park submission flow through the UI.
|
||||
*/
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { ParkCreationPage } from '../../helpers/page-objects/ParkCreationPage';
|
||||
import { generateParkData, generateTestId } from '../../fixtures/test-data';
|
||||
import { queryDatabase, cleanupTestData } from '../../fixtures/database';
|
||||
|
||||
test.describe('Park Creation Flow', () => {
|
||||
let parkCreationPage: ParkCreationPage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
parkCreationPage = new ParkCreationPage(page);
|
||||
await parkCreationPage.goto();
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
// Clean up test data
|
||||
await cleanupTestData();
|
||||
});
|
||||
|
||||
test('should create park submission successfully', async ({ page }) => {
|
||||
const parkData = generateParkData({
|
||||
name: `Test Park ${generateTestId()}`,
|
||||
});
|
||||
|
||||
// Fill form
|
||||
await parkCreationPage.fillBasicInfo(parkData.name, parkData.description);
|
||||
await parkCreationPage.selectParkType(parkData.park_type);
|
||||
await parkCreationPage.selectStatus(parkData.status);
|
||||
await parkCreationPage.setOpeningDate(parkData.opened_date);
|
||||
|
||||
// Submit
|
||||
await parkCreationPage.submitForm();
|
||||
await parkCreationPage.expectSuccess();
|
||||
|
||||
// Verify submission created in database
|
||||
const submissions = await queryDatabase('content_submissions', (qb) =>
|
||||
qb.select('*').eq('entity_type', 'park').eq('is_test_data', true).order('created_at', { ascending: false }).limit(1)
|
||||
);
|
||||
|
||||
expect(submissions).toHaveLength(1);
|
||||
expect(submissions[0].status).toBe('pending');
|
||||
|
||||
// Verify NO park created yet (should be in moderation queue)
|
||||
const parks = await queryDatabase('parks', (qb) =>
|
||||
qb.select('*').eq('slug', parkData.slug)
|
||||
);
|
||||
|
||||
expect(parks).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should validate required fields', async ({ page }) => {
|
||||
// Try to submit empty form
|
||||
await parkCreationPage.submitForm();
|
||||
|
||||
// Should show validation errors
|
||||
await parkCreationPage.expectValidationError('Name is required');
|
||||
});
|
||||
|
||||
test('should auto-generate slug from name', async ({ page }) => {
|
||||
const parkName = `Amazing Theme Park ${generateTestId()}`;
|
||||
|
||||
await page.fill('input[name="name"]', parkName);
|
||||
|
||||
// Wait for slug to auto-generate
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const slugValue = await page.inputValue('input[name="slug"]');
|
||||
expect(slugValue).toContain('amazing-theme-park');
|
||||
expect(slugValue).not.toContain(' '); // No spaces
|
||||
});
|
||||
|
||||
test('should support custom date precision', async ({ page }) => {
|
||||
const parkData = generateParkData({
|
||||
name: `Test Park ${generateTestId()}`,
|
||||
});
|
||||
|
||||
await parkCreationPage.fillBasicInfo(parkData.name, parkData.description);
|
||||
await parkCreationPage.setOpeningDate('2020-01-01', 'year');
|
||||
|
||||
await parkCreationPage.submitForm();
|
||||
await parkCreationPage.expectSuccess();
|
||||
|
||||
// Verify date precision in submission
|
||||
const submissions = await queryDatabase('content_submissions', (qb) =>
|
||||
qb.select('submission_data').eq('entity_type', 'park').eq('is_test_data', true).order('created_at', { ascending: false }).limit(1)
|
||||
);
|
||||
|
||||
const submissionData = submissions[0].submission_data;
|
||||
expect(submissionData.opened_date_precision).toBe('year');
|
||||
});
|
||||
|
||||
test('should display submission in user profile', async ({ page }) => {
|
||||
const parkData = generateParkData({
|
||||
name: `Test Park ${generateTestId()}`,
|
||||
});
|
||||
|
||||
// Create submission
|
||||
await parkCreationPage.fillBasicInfo(parkData.name, parkData.description);
|
||||
await parkCreationPage.submitForm();
|
||||
await parkCreationPage.expectSuccess();
|
||||
|
||||
// Navigate to user profile
|
||||
await page.click('[data-testid="user-menu"]').or(page.click('button:has-text("Profile")'));
|
||||
await page.click('text=My Submissions');
|
||||
|
||||
// Verify submission appears
|
||||
await expect(page.getByText(parkData.name)).toBeVisible();
|
||||
await expect(page.getByText(/pending/i)).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Park Form Validation', () => {
|
||||
let parkCreationPage: ParkCreationPage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
parkCreationPage = new ParkCreationPage(page);
|
||||
await parkCreationPage.goto();
|
||||
});
|
||||
|
||||
test('should enforce minimum description length', async ({ page }) => {
|
||||
await page.fill('input[name="name"]', 'Test Park');
|
||||
await page.fill('textarea[name="description"]', 'Too short');
|
||||
|
||||
await parkCreationPage.submitForm();
|
||||
|
||||
await expect(page.getByText(/description.*too short/i)).toBeVisible();
|
||||
});
|
||||
|
||||
test('should prevent duplicate slugs', async ({ page }) => {
|
||||
// This test would require creating a park first, then trying to create another with same slug
|
||||
// Left as TODO - requires more complex setup
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user