261 lines
9.5 KiB
TypeScript
261 lines
9.5 KiB
TypeScript
/**
|
|
* E2E Test - Complete Booking Workflow
|
|
*
|
|
* Tests the complete booking flow from rate search to booking confirmation
|
|
*/
|
|
|
|
import { test, expect } from '@playwright/test';
|
|
|
|
// Test configuration
|
|
const BASE_URL = process.env.BASE_URL || 'http://localhost:3000';
|
|
const API_URL = process.env.API_URL || 'http://localhost:4000/api/v1';
|
|
|
|
// Test user credentials (should be set via environment variables)
|
|
const TEST_USER = {
|
|
email: process.env.TEST_USER_EMAIL || 'test@example.com',
|
|
password: process.env.TEST_USER_PASSWORD || 'TestPassword123!',
|
|
};
|
|
|
|
test.describe('Complete Booking Workflow', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
// Navigate to homepage
|
|
await page.goto(BASE_URL);
|
|
});
|
|
|
|
test('should complete full booking flow', async ({ page }) => {
|
|
// Step 1: Login
|
|
await test.step('User Login', async () => {
|
|
await page.click('text=Login');
|
|
await page.fill('input[name="email"]', TEST_USER.email);
|
|
await page.fill('input[name="password"]', TEST_USER.password);
|
|
await page.click('button[type="submit"]');
|
|
|
|
// Wait for redirect to dashboard
|
|
await page.waitForURL('**/dashboard');
|
|
await expect(page).toHaveURL(/.*dashboard/);
|
|
});
|
|
|
|
// Step 2: Navigate to Rate Search
|
|
await test.step('Navigate to Rate Search', async () => {
|
|
await page.click('text=Search Rates');
|
|
await expect(page).toHaveURL(/.*rates\/search/);
|
|
});
|
|
|
|
// Step 3: Search for Rates
|
|
await test.step('Search for Shipping Rates', async () => {
|
|
// Fill search form
|
|
await page.fill('input[name="origin"]', 'Rotterdam');
|
|
await page.waitForTimeout(500); // Wait for autocomplete
|
|
await page.keyboard.press('ArrowDown');
|
|
await page.keyboard.press('Enter');
|
|
|
|
await page.fill('input[name="destination"]', 'Shanghai');
|
|
await page.waitForTimeout(500);
|
|
await page.keyboard.press('ArrowDown');
|
|
await page.keyboard.press('Enter');
|
|
|
|
// Select departure date (2 weeks from now)
|
|
const departureDate = new Date();
|
|
departureDate.setDate(departureDate.getDate() + 14);
|
|
await page.fill('input[name="departureDate"]', departureDate.toISOString().split('T')[0]);
|
|
|
|
// Select container type
|
|
await page.selectOption('select[name="containerType"]', '40HC');
|
|
await page.fill('input[name="quantity"]', '1');
|
|
|
|
// Submit search
|
|
await page.click('button[type="submit"]');
|
|
|
|
// Wait for results
|
|
await page.waitForSelector('.rate-results', { timeout: 10000 });
|
|
|
|
// Verify results are displayed
|
|
const resultsCount = await page.locator('.rate-card').count();
|
|
expect(resultsCount).toBeGreaterThan(0);
|
|
});
|
|
|
|
// Step 4: Select a Rate and Create Booking
|
|
await test.step('Select Rate and Create Booking', async () => {
|
|
// Select first available rate
|
|
await page.locator('.rate-card').first().click('button:has-text("Book")');
|
|
|
|
// Should navigate to booking form
|
|
await expect(page).toHaveURL(/.*bookings\/create/);
|
|
|
|
// Fill booking details
|
|
await page.fill('input[name="shipperName"]', 'Test Shipper Inc.');
|
|
await page.fill('input[name="shipperAddress"]', '123 Test St');
|
|
await page.fill('input[name="shipperCity"]', 'Rotterdam');
|
|
await page.fill('input[name="shipperCountry"]', 'Netherlands');
|
|
await page.fill('input[name="shipperEmail"]', 'shipper@test.com');
|
|
await page.fill('input[name="shipperPhone"]', '+31612345678');
|
|
|
|
await page.fill('input[name="consigneeName"]', 'Test Consignee Ltd.');
|
|
await page.fill('input[name="consigneeAddress"]', '456 Dest Ave');
|
|
await page.fill('input[name="consigneeCity"]', 'Shanghai');
|
|
await page.fill('input[name="consigneeCountry"]', 'China');
|
|
await page.fill('input[name="consigneeEmail"]', 'consignee@test.com');
|
|
await page.fill('input[name="consigneePhone"]', '+8613812345678');
|
|
|
|
// Container details
|
|
await page.fill('input[name="cargoDescription"]', 'Test Cargo - Electronics');
|
|
await page.fill('input[name="cargoWeight"]', '15000'); // kg
|
|
await page.fill('input[name="cargoValue"]', '50000'); // USD
|
|
|
|
// Submit booking
|
|
await page.click('button:has-text("Create Booking")');
|
|
|
|
// Wait for confirmation
|
|
await page.waitForSelector('.booking-confirmation', { timeout: 10000 });
|
|
});
|
|
|
|
// Step 5: Verify Booking in Dashboard
|
|
await test.step('Verify Booking in Dashboard', async () => {
|
|
// Navigate to dashboard
|
|
await page.click('text=Dashboard');
|
|
await expect(page).toHaveURL(/.*dashboard/);
|
|
|
|
// Verify new booking appears in list
|
|
await page.waitForSelector('.bookings-table');
|
|
|
|
// Check that first row contains the booking
|
|
const firstBooking = page.locator('.booking-row').first();
|
|
await expect(firstBooking).toBeVisible();
|
|
|
|
// Verify booking number format (WCM-YYYY-XXXXXX)
|
|
const bookingNumber = await firstBooking.locator('.booking-number').textContent();
|
|
expect(bookingNumber).toMatch(/WCM-\d{4}-[A-Z0-9]{6}/);
|
|
});
|
|
|
|
// Step 6: View Booking Details
|
|
await test.step('View Booking Details', async () => {
|
|
// Click on booking to view details
|
|
await page.locator('.booking-row').first().click();
|
|
|
|
// Should navigate to booking details page
|
|
await expect(page).toHaveURL(/.*bookings\/[a-f0-9-]+/);
|
|
|
|
// Verify all details are displayed
|
|
await expect(page.locator('text=Test Shipper Inc.')).toBeVisible();
|
|
await expect(page.locator('text=Test Consignee Ltd.')).toBeVisible();
|
|
await expect(page.locator('text=Rotterdam')).toBeVisible();
|
|
await expect(page.locator('text=Shanghai')).toBeVisible();
|
|
});
|
|
});
|
|
|
|
test('should handle rate search errors gracefully', async ({ page }) => {
|
|
await test.step('Login', async () => {
|
|
await page.goto(`${BASE_URL}/login`);
|
|
await page.fill('input[name="email"]', TEST_USER.email);
|
|
await page.fill('input[name="password"]', TEST_USER.password);
|
|
await page.click('button[type="submit"]');
|
|
await page.waitForURL('**/dashboard');
|
|
});
|
|
|
|
await test.step('Test Invalid Search', async () => {
|
|
await page.goto(`${BASE_URL}/rates/search`);
|
|
|
|
// Try to search without filling required fields
|
|
await page.click('button[type="submit"]');
|
|
|
|
// Should show validation errors
|
|
await expect(page.locator('.error-message')).toBeVisible();
|
|
});
|
|
});
|
|
|
|
test('should filter bookings in dashboard', async ({ page }) => {
|
|
await test.step('Login and Navigate to Dashboard', async () => {
|
|
await page.goto(`${BASE_URL}/login`);
|
|
await page.fill('input[name="email"]', TEST_USER.email);
|
|
await page.fill('input[name="password"]', TEST_USER.password);
|
|
await page.click('button[type="submit"]');
|
|
await page.waitForURL('**/dashboard');
|
|
});
|
|
|
|
await test.step('Apply Filters', async () => {
|
|
// Open filter panel
|
|
await page.click('button:has-text("Filters")');
|
|
|
|
// Filter by status
|
|
await page.check('input[value="confirmed"]');
|
|
|
|
// Apply filters
|
|
await page.click('button:has-text("Apply")');
|
|
|
|
// Wait for filtered results
|
|
await page.waitForTimeout(1000);
|
|
|
|
// Verify all visible bookings have confirmed status
|
|
const bookings = page.locator('.booking-row');
|
|
const count = await bookings.count();
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
const status = await bookings.nth(i).locator('.status-badge').textContent();
|
|
expect(status?.toLowerCase()).toContain('confirmed');
|
|
}
|
|
});
|
|
});
|
|
|
|
test('should export bookings', async ({ page }) => {
|
|
await test.step('Login and Navigate to Dashboard', async () => {
|
|
await page.goto(`${BASE_URL}/login`);
|
|
await page.fill('input[name="email"]', TEST_USER.email);
|
|
await page.fill('input[name="password"]', TEST_USER.password);
|
|
await page.click('button[type="submit"]');
|
|
await page.waitForURL('**/dashboard');
|
|
});
|
|
|
|
await test.step('Export Bookings', async () => {
|
|
// Wait for download event
|
|
const downloadPromise = page.waitForEvent('download');
|
|
|
|
// Click export button
|
|
await page.click('button:has-text("Export")');
|
|
await page.click('text=CSV');
|
|
|
|
// Wait for download
|
|
const download = await downloadPromise;
|
|
|
|
// Verify filename
|
|
expect(download.suggestedFilename()).toMatch(/bookings.*\.csv/);
|
|
});
|
|
});
|
|
});
|
|
|
|
test.describe('Authentication', () => {
|
|
test('should prevent access to protected pages', async ({ page }) => {
|
|
// Try to access dashboard without logging in
|
|
await page.goto(`${BASE_URL}/dashboard`);
|
|
|
|
// Should redirect to login
|
|
await expect(page).toHaveURL(/.*login/);
|
|
});
|
|
|
|
test('should show error for invalid credentials', async ({ page }) => {
|
|
await page.goto(`${BASE_URL}/login`);
|
|
|
|
await page.fill('input[name="email"]', 'wrong@example.com');
|
|
await page.fill('input[name="password"]', 'wrongpassword');
|
|
await page.click('button[type="submit"]');
|
|
|
|
// Should show error message
|
|
await expect(page.locator('.error-message')).toBeVisible();
|
|
await expect(page.locator('text=Invalid credentials')).toBeVisible();
|
|
});
|
|
|
|
test('should logout successfully', async ({ page }) => {
|
|
// Login first
|
|
await page.goto(`${BASE_URL}/login`);
|
|
await page.fill('input[name="email"]', TEST_USER.email);
|
|
await page.fill('input[name="password"]', TEST_USER.password);
|
|
await page.click('button[type="submit"]');
|
|
await page.waitForURL('**/dashboard');
|
|
|
|
// Logout
|
|
await page.click('button:has-text("Logout")');
|
|
|
|
// Should redirect to home/login
|
|
await expect(page).toHaveURL(/.*(\/$|\/login)/);
|
|
});
|
|
});
|