javascript

exercises

exercises.js
/**
 * 20.2 Integration & E2E Testing - Exercises
 *
 * Practice integration testing patterns
 */

// ============================================
// EXERCISE 1: Test Order Processing System
// ============================================

/**
 * Complete the integration test for this order processing system.
 * Test the complete flow: validate → reserve stock → process payment → create order
 */

class OrderProcessor {
  constructor(inventory, paymentGateway, orderRepository) {
    this.inventory = inventory;
    this.paymentGateway = paymentGateway;
    this.orderRepo = orderRepository;
  }

  async processOrder(orderData) {
    // Step 1: Validate order
    if (!orderData.items || orderData.items.length === 0) {
      throw new Error('Order must have items');
    }

    // Step 2: Check and reserve inventory
    for (const item of orderData.items) {
      const available = await this.inventory.checkStock(item.productId);
      if (available < item.quantity) {
        throw new Error(`Insufficient stock for product ${item.productId}`);
      }
    }

    // Reserve all items
    for (const item of orderData.items) {
      await this.inventory.reserve(item.productId, item.quantity);
    }

    // Step 3: Process payment
    const total = orderData.items.reduce(
      (sum, item) => sum + item.price * item.quantity,
      0
    );

    try {
      await this.paymentGateway.charge(orderData.paymentMethod, total);
    } catch (e) {
      // Rollback inventory reservation
      for (const item of orderData.items) {
        await this.inventory.release(item.productId, item.quantity);
      }
      throw new Error(`Payment failed: ${e.message}`);
    }

    // Step 4: Create order
    const order = await this.orderRepo.create({
      userId: orderData.userId,
      items: orderData.items,
      total,
      status: 'confirmed',
      createdAt: new Date(),
    });

    // Step 5: Commit inventory changes
    for (const item of orderData.items) {
      await this.inventory.commit(item.productId, item.quantity);
    }

    return order;
  }
}

// Create test doubles and write integration tests
async function testOrderProcessor() {
  // Your implementation here
}

/*
// SOLUTION:
async function testOrderProcessor() {
    console.log('=== Order Processing Integration Tests ===\n');
    
    // Create test doubles
    function createInventoryMock(stockLevels) {
        const reservations = new Map();
        
        return {
            stockLevels: new Map(Object.entries(stockLevels)),
            operations: [],
            
            async checkStock(productId) {
                this.operations.push({ op: 'checkStock', productId });
                return this.stockLevels.get(productId) || 0;
            },
            
            async reserve(productId, quantity) {
                this.operations.push({ op: 'reserve', productId, quantity });
                const current = reservations.get(productId) || 0;
                reservations.set(productId, current + quantity);
            },
            
            async release(productId, quantity) {
                this.operations.push({ op: 'release', productId, quantity });
                const current = reservations.get(productId) || 0;
                reservations.set(productId, current - quantity);
            },
            
            async commit(productId, quantity) {
                this.operations.push({ op: 'commit', productId, quantity });
                const stock = this.stockLevels.get(productId) || 0;
                this.stockLevels.set(productId, stock - quantity);
                reservations.delete(productId);
            },
            
            getReservations() {
                return reservations;
            }
        };
    }
    
    function createPaymentMock(shouldFail = false) {
        return {
            charges: [],
            
            async charge(method, amount) {
                this.charges.push({ method, amount, timestamp: Date.now() });
                if (shouldFail) {
                    throw new Error('Payment declined');
                }
                return { success: true, transactionId: 'txn_123' };
            }
        };
    }
    
    function createOrderRepoMock() {
        const orders = [];
        let nextId = 1;
        
        return {
            orders,
            
            async create(orderData) {
                const order = { id: nextId++, ...orderData };
                orders.push(order);
                return order;
            },
            
            async findById(id) {
                return orders.find(o => o.id === id) || null;
            }
        };
    }
    
    // Test 1: Successful order processing
    console.log('Test 1: Successful order processing');
    
    const inventory1 = createInventoryMock({ '1': 10, '2': 5 });
    const payment1 = createPaymentMock(false);
    const orderRepo1 = createOrderRepoMock();
    const processor1 = new OrderProcessor(inventory1, payment1, orderRepo1);
    
    const order1 = await processor1.processOrder({
        userId: 1,
        items: [
            { productId: '1', quantity: 2, price: 10 },
            { productId: '2', quantity: 1, price: 25 }
        ],
        paymentMethod: { type: 'card', last4: '1234' }
    });
    
    console.assert(order1.id === 1, 'Should create order with ID');
    console.assert(order1.status === 'confirmed', 'Should be confirmed');
    console.assert(order1.total === 45, 'Should calculate total');
    console.log('  ✓ Order created successfully');
    
    // Verify inventory operations
    console.assert(
        inventory1.operations.filter(o => o.op === 'checkStock').length === 2,
        'Should check stock for all items'
    );
    console.assert(
        inventory1.operations.filter(o => o.op === 'commit').length === 2,
        'Should commit inventory'
    );
    console.log('  ✓ Inventory operations correct');
    
    // Verify stock updated
    console.assert(inventory1.stockLevels.get('1') === 8, 'Stock should be reduced');
    console.assert(inventory1.stockLevels.get('2') === 4, 'Stock should be reduced');
    console.log('  ✓ Stock levels updated');
    
    // Verify payment
    console.assert(payment1.charges.length === 1, 'Should charge once');
    console.assert(payment1.charges[0].amount === 45, 'Should charge correct amount');
    console.log('  ✓ Payment processed');
    
    // Test 2: Insufficient stock
    console.log('\nTest 2: Insufficient stock');
    
    const inventory2 = createInventoryMock({ '1': 2 });
    const payment2 = createPaymentMock(false);
    const orderRepo2 = createOrderRepoMock();
    const processor2 = new OrderProcessor(inventory2, payment2, orderRepo2);
    
    try {
        await processor2.processOrder({
            userId: 1,
            items: [{ productId: '1', quantity: 5, price: 10 }],
            paymentMethod: { type: 'card' }
        });
        console.log('  ✗ Should have thrown error');
    } catch (e) {
        console.assert(
            e.message.includes('Insufficient stock'),
            'Should indicate stock issue'
        );
        console.log('  ✓ Correctly rejected for insufficient stock');
    }
    
    // Verify no payment attempted
    console.assert(payment2.charges.length === 0, 'Should not charge');
    console.log('  ✓ No payment attempted');
    
    // Test 3: Payment failure with rollback
    console.log('\nTest 3: Payment failure with rollback');
    
    const inventory3 = createInventoryMock({ '1': 10 });
    const payment3 = createPaymentMock(true); // Will fail
    const orderRepo3 = createOrderRepoMock();
    const processor3 = new OrderProcessor(inventory3, payment3, orderRepo3);
    
    try {
        await processor3.processOrder({
            userId: 1,
            items: [{ productId: '1', quantity: 3, price: 10 }],
            paymentMethod: { type: 'card' }
        });
        console.log('  ✗ Should have thrown error');
    } catch (e) {
        console.assert(
            e.message.includes('Payment failed'),
            'Should indicate payment failure'
        );
        console.log('  ✓ Payment failure handled');
    }
    
    // Verify inventory was rolled back
    const releaseOps = inventory3.operations.filter(o => o.op === 'release');
    console.assert(releaseOps.length === 1, 'Should release reservation');
    console.log('  ✓ Inventory reservation released');
    
    // Verify no order created
    console.assert(orderRepo3.orders.length === 0, 'Should not create order');
    console.log('  ✓ No order created');
    
    // Verify stock unchanged
    console.assert(inventory3.stockLevels.get('1') === 10, 'Stock unchanged');
    console.log('  ✓ Stock levels unchanged');
    
    // Test 4: Empty order
    console.log('\nTest 4: Empty order validation');
    
    const inventory4 = createInventoryMock({});
    const payment4 = createPaymentMock(false);
    const orderRepo4 = createOrderRepoMock();
    const processor4 = new OrderProcessor(inventory4, payment4, orderRepo4);
    
    try {
        await processor4.processOrder({ userId: 1, items: [] });
        console.log('  ✗ Should have thrown error');
    } catch (e) {
        console.assert(
            e.message === 'Order must have items',
            'Should validate items'
        );
        console.log('  ✓ Empty order rejected');
    }
    
    console.log('\n=== Order Processing Tests Complete ===\n');
}
*/

// ============================================
// EXERCISE 2: Create an API Test Suite
// ============================================

/**
 * Create integration tests for this REST API
 * Test CRUD operations and error handling
 */

class TaskAPI {
  constructor(taskService) {
    this.taskService = taskService;
  }

  async handleRequest(method, path, body) {
    try {
      // GET /tasks
      if (method === 'GET' && path === '/tasks') {
        const tasks = await this.taskService.list();
        return { status: 200, body: tasks };
      }

      // GET /tasks/:id
      if (method === 'GET' && path.match(/^\/tasks\/\d+$/)) {
        const id = parseInt(path.split('/')[2]);
        const task = await this.taskService.get(id);
        if (!task) {
          return { status: 404, body: { error: 'Task not found' } };
        }
        return { status: 200, body: task };
      }

      // POST /tasks
      if (method === 'POST' && path === '/tasks') {
        if (!body.title) {
          return { status: 400, body: { error: 'Title required' } };
        }
        const task = await this.taskService.create(body);
        return { status: 201, body: task };
      }

      // PUT /tasks/:id
      if (method === 'PUT' && path.match(/^\/tasks\/\d+$/)) {
        const id = parseInt(path.split('/')[2]);
        const task = await this.taskService.update(id, body);
        if (!task) {
          return { status: 404, body: { error: 'Task not found' } };
        }
        return { status: 200, body: task };
      }

      // DELETE /tasks/:id
      if (method === 'DELETE' && path.match(/^\/tasks\/\d+$/)) {
        const id = parseInt(path.split('/')[2]);
        const deleted = await this.taskService.delete(id);
        if (!deleted) {
          return { status: 404, body: { error: 'Task not found' } };
        }
        return { status: 204, body: null };
      }

      return { status: 404, body: { error: 'Not found' } };
    } catch (e) {
      return { status: 500, body: { error: e.message } };
    }
  }
}

// Implement tests for the TaskAPI
async function testTaskAPI() {
  // Your implementation here
}

/*
// SOLUTION:
async function testTaskAPI() {
    console.log('=== Task API Integration Tests ===\n');
    
    // Create in-memory task service
    function createTaskService() {
        const tasks = new Map();
        let nextId = 1;
        
        return {
            async list() {
                return Array.from(tasks.values());
            },
            
            async get(id) {
                return tasks.get(id) || null;
            },
            
            async create(data) {
                const task = {
                    id: nextId++,
                    title: data.title,
                    description: data.description || '',
                    completed: false,
                    createdAt: new Date().toISOString()
                };
                tasks.set(task.id, task);
                return task;
            },
            
            async update(id, data) {
                const task = tasks.get(id);
                if (!task) return null;
                
                const updated = { ...task, ...data, id };
                tasks.set(id, updated);
                return updated;
            },
            
            async delete(id) {
                if (!tasks.has(id)) return false;
                tasks.delete(id);
                return true;
            },
            
            clear() {
                tasks.clear();
                nextId = 1;
            }
        };
    }
    
    const taskService = createTaskService();
    const api = new TaskAPI(taskService);
    
    // Test helper
    async function request(method, path, body = null) {
        return api.handleRequest(method, path, body);
    }
    
    // Test 1: Create task
    console.log('Test 1: POST /tasks - Create task');
    let response = await request('POST', '/tasks', { 
        title: 'Write tests', 
        description: 'Write comprehensive tests' 
    });
    console.assert(response.status === 201, 'Should return 201');
    console.assert(response.body.id === 1, 'Should have ID');
    console.assert(response.body.title === 'Write tests', 'Should have title');
    console.assert(response.body.completed === false, 'Should be incomplete');
    console.log('  ✓ Task created');
    
    // Test 2: Create without title
    console.log('\nTest 2: POST /tasks - Missing title');
    response = await request('POST', '/tasks', { description: 'No title' });
    console.assert(response.status === 400, 'Should return 400');
    console.assert(response.body.error === 'Title required', 'Should have error');
    console.log('  ✓ Validation error returned');
    
    // Test 3: List tasks
    console.log('\nTest 3: GET /tasks - List tasks');
    response = await request('GET', '/tasks');
    console.assert(response.status === 200, 'Should return 200');
    console.assert(response.body.length === 1, 'Should have one task');
    console.log('  ✓ Tasks listed');
    
    // Test 4: Get single task
    console.log('\nTest 4: GET /tasks/1 - Get task');
    response = await request('GET', '/tasks/1');
    console.assert(response.status === 200, 'Should return 200');
    console.assert(response.body.title === 'Write tests', 'Should return task');
    console.log('  ✓ Task retrieved');
    
    // Test 5: Get non-existent task
    console.log('\nTest 5: GET /tasks/999 - Not found');
    response = await request('GET', '/tasks/999');
    console.assert(response.status === 404, 'Should return 404');
    console.log('  ✓ 404 returned for missing task');
    
    // Test 6: Update task
    console.log('\nTest 6: PUT /tasks/1 - Update task');
    response = await request('PUT', '/tasks/1', { 
        title: 'Write more tests',
        completed: true 
    });
    console.assert(response.status === 200, 'Should return 200');
    console.assert(response.body.title === 'Write more tests', 'Title updated');
    console.assert(response.body.completed === true, 'Completed updated');
    console.log('  ✓ Task updated');
    
    // Test 7: Update non-existent task
    console.log('\nTest 7: PUT /tasks/999 - Update not found');
    response = await request('PUT', '/tasks/999', { title: 'New' });
    console.assert(response.status === 404, 'Should return 404');
    console.log('  ✓ 404 returned for missing task');
    
    // Test 8: Delete task
    console.log('\nTest 8: DELETE /tasks/1 - Delete task');
    response = await request('DELETE', '/tasks/1');
    console.assert(response.status === 204, 'Should return 204');
    console.log('  ✓ Task deleted');
    
    // Verify deletion
    response = await request('GET', '/tasks/1');
    console.assert(response.status === 404, 'Should not find deleted task');
    console.log('  ✓ Task no longer exists');
    
    // Test 9: Delete non-existent task
    console.log('\nTest 9: DELETE /tasks/999 - Delete not found');
    response = await request('DELETE', '/tasks/999');
    console.assert(response.status === 404, 'Should return 404');
    console.log('  ✓ 404 returned for missing task');
    
    // Test 10: Invalid route
    console.log('\nTest 10: GET /invalid - Invalid route');
    response = await request('GET', '/invalid');
    console.assert(response.status === 404, 'Should return 404');
    console.log('  ✓ 404 returned for invalid route');
    
    console.log('\n=== Task API Tests Complete ===\n');
}
*/

// ============================================
// EXERCISE 3: Create Page Objects
// ============================================

/**
 * Implement Page Object classes for testing a shopping site
 * Follow the Page Object pattern
 */

// Simulated browser (from examples)
class Browser {
  constructor() {
    this.currentURL = '';
    this.elements = new Map();
  }

  async goto(url) {
    this.currentURL = url;
  }

  setElements(elements) {
    this.elements = new Map(Object.entries(elements));
  }

  async $(selector) {
    return this.elements.get(selector) || null;
  }

  async click(selector) {
    const el = this.elements.get(selector);
    if (el && el.onClick) await el.onClick();
  }

  async type(selector, text) {
    const el = this.elements.get(selector);
    if (el) el.value = text;
  }

  async waitForSelector(selector) {
    return this.elements.get(selector);
  }
}

// Implement these Page Objects:
class ProductListPage {
  // Your implementation
}

class ProductDetailPage {
  // Your implementation
}

class CartPage {
  // Your implementation
}

class CheckoutPage {
  // Your implementation
}

/*
// SOLUTION:
class ProductListPage {
    constructor(browser) {
        this.browser = browser;
        this.url = 'https://shop.example.com/products';
        this.selectors = {
            productCard: '.product-card',
            productName: '.product-name',
            productPrice: '.product-price',
            addToCart: '.add-to-cart',
            cartCount: '.cart-count',
            searchInput: '#search',
            filterCategory: '#category-filter',
            sortSelect: '#sort-by'
        };
    }
    
    async navigate() {
        await this.browser.goto(this.url);
        return this;
    }
    
    async getProducts() {
        const products = [];
        let i = 0;
        while (true) {
            const card = await this.browser.$(
                `${this.selectors.productCard}[data-index="${i}"]`
            );
            if (!card) break;
            products.push({
                name: card.name,
                price: card.price,
                id: card.id
            });
            i++;
        }
        return products;
    }
    
    async searchProducts(query) {
        await this.browser.type(this.selectors.searchInput, query);
        await this.browser.click('#search-button');
        return this;
    }
    
    async filterByCategory(category) {
        await this.browser.click(this.selectors.filterCategory);
        await this.browser.click(`[data-category="${category}"]`);
        return this;
    }
    
    async sortBy(option) {
        await this.browser.click(this.selectors.sortSelect);
        await this.browser.click(`[data-sort="${option}"]`);
        return this;
    }
    
    async addProductToCart(productId) {
        await this.browser.click(`[data-product-id="${productId}"] .add-to-cart`);
        return this;
    }
    
    async getCartCount() {
        const el = await this.browser.$(this.selectors.cartCount);
        return el ? parseInt(el.textContent) : 0;
    }
    
    async goToProduct(productId) {
        await this.browser.click(`[data-product-id="${productId}"]`);
        return new ProductDetailPage(this.browser);
    }
    
    async goToCart() {
        await this.browser.click('.cart-link');
        return new CartPage(this.browser);
    }
}

class ProductDetailPage {
    constructor(browser) {
        this.browser = browser;
        this.selectors = {
            productName: '.product-title',
            productPrice: '.product-price',
            productDescription: '.product-description',
            quantity: '#quantity',
            addToCart: '#add-to-cart',
            buyNow: '#buy-now',
            reviews: '.review-list',
            rating: '.rating-stars'
        };
    }
    
    async getProductDetails() {
        const name = await this.browser.$(this.selectors.productName);
        const price = await this.browser.$(this.selectors.productPrice);
        const description = await this.browser.$(this.selectors.productDescription);
        
        return {
            name: name?.textContent || '',
            price: parseFloat(price?.textContent?.replace('$', '') || 0),
            description: description?.textContent || ''
        };
    }
    
    async setQuantity(quantity) {
        await this.browser.type(this.selectors.quantity, String(quantity));
        return this;
    }
    
    async addToCart() {
        await this.browser.click(this.selectors.addToCart);
        return this;
    }
    
    async buyNow() {
        await this.browser.click(this.selectors.buyNow);
        return new CheckoutPage(this.browser);
    }
    
    async getRating() {
        const el = await this.browser.$(this.selectors.rating);
        return el ? parseFloat(el.dataset.rating) : 0;
    }
    
    async getReviewCount() {
        const el = await this.browser.$(this.selectors.reviews);
        return el ? el.children.length : 0;
    }
    
    async goBack() {
        await this.browser.click('.back-link');
        return new ProductListPage(this.browser);
    }
}

class CartPage {
    constructor(browser) {
        this.browser = browser;
        this.url = 'https://shop.example.com/cart';
        this.selectors = {
            cartItem: '.cart-item',
            itemName: '.item-name',
            itemPrice: '.item-price',
            itemQuantity: '.item-quantity',
            removeItem: '.remove-item',
            subtotal: '.subtotal',
            tax: '.tax',
            total: '.total',
            checkout: '#checkout-button',
            continueShopping: '.continue-shopping'
        };
    }
    
    async navigate() {
        await this.browser.goto(this.url);
        return this;
    }
    
    async getItems() {
        const items = [];
        let i = 0;
        while (true) {
            const item = await this.browser.$(
                `${this.selectors.cartItem}[data-index="${i}"]`
            );
            if (!item) break;
            items.push({
                name: item.name,
                price: item.price,
                quantity: item.quantity
            });
            i++;
        }
        return items;
    }
    
    async updateQuantity(itemIndex, quantity) {
        await this.browser.type(
            `${this.selectors.cartItem}[data-index="${itemIndex}"] ${this.selectors.itemQuantity}`,
            String(quantity)
        );
        return this;
    }
    
    async removeItem(itemIndex) {
        await this.browser.click(
            `${this.selectors.cartItem}[data-index="${itemIndex}"] ${this.selectors.removeItem}`
        );
        return this;
    }
    
    async getSubtotal() {
        const el = await this.browser.$(this.selectors.subtotal);
        return parseFloat(el?.textContent?.replace('$', '') || 0);
    }
    
    async getTax() {
        const el = await this.browser.$(this.selectors.tax);
        return parseFloat(el?.textContent?.replace('$', '') || 0);
    }
    
    async getTotal() {
        const el = await this.browser.$(this.selectors.total);
        return parseFloat(el?.textContent?.replace('$', '') || 0);
    }
    
    async isEmpty() {
        const items = await this.getItems();
        return items.length === 0;
    }
    
    async proceedToCheckout() {
        await this.browser.click(this.selectors.checkout);
        return new CheckoutPage(this.browser);
    }
    
    async continueShopping() {
        await this.browser.click(this.selectors.continueShopping);
        return new ProductListPage(this.browser);
    }
}

class CheckoutPage {
    constructor(browser) {
        this.browser = browser;
        this.selectors = {
            // Shipping
            firstName: '#first-name',
            lastName: '#last-name',
            address: '#address',
            city: '#city',
            zipCode: '#zip',
            country: '#country',
            
            // Payment
            cardNumber: '#card-number',
            cardExpiry: '#expiry',
            cardCVC: '#cvc',
            
            // Actions
            submitOrder: '#place-order',
            orderConfirmation: '.order-confirmation',
            orderNumber: '.order-number',
            
            // Errors
            errorMessage: '.error-message'
        };
    }
    
    async fillShippingInfo(info) {
        await this.browser.type(this.selectors.firstName, info.firstName);
        await this.browser.type(this.selectors.lastName, info.lastName);
        await this.browser.type(this.selectors.address, info.address);
        await this.browser.type(this.selectors.city, info.city);
        await this.browser.type(this.selectors.zipCode, info.zipCode);
        await this.browser.type(this.selectors.country, info.country);
        return this;
    }
    
    async fillPaymentInfo(payment) {
        await this.browser.type(this.selectors.cardNumber, payment.cardNumber);
        await this.browser.type(this.selectors.cardExpiry, payment.expiry);
        await this.browser.type(this.selectors.cardCVC, payment.cvc);
        return this;
    }
    
    async placeOrder() {
        await this.browser.click(this.selectors.submitOrder);
        return this;
    }
    
    async isOrderConfirmed() {
        const el = await this.browser.$(this.selectors.orderConfirmation);
        return el !== null;
    }
    
    async getOrderNumber() {
        const el = await this.browser.$(this.selectors.orderNumber);
        return el?.textContent || null;
    }
    
    async getErrorMessage() {
        const el = await this.browser.$(this.selectors.errorMessage);
        return el?.textContent || null;
    }
    
    async completeCheckout(shippingInfo, paymentInfo) {
        await this.fillShippingInfo(shippingInfo);
        await this.fillPaymentInfo(paymentInfo);
        await this.placeOrder();
        return this;
    }
}

// Usage test
async function testPageObjects() {
    console.log('=== Page Object Tests ===\n');
    
    const browser = new Browser();
    
    // Simulate product list page
    browser.setElements({
        '.product-card[data-index="0"]': { 
            name: 'Widget', 
            price: 9.99, 
            id: '1' 
        },
        '.cart-count': { textContent: '0' }
    });
    
    const productList = new ProductListPage(browser);
    await productList.navigate();
    console.log('  ✓ Navigated to product list');
    
    const count = await productList.getCartCount();
    console.assert(count === 0, 'Cart should be empty');
    console.log('  ✓ Cart count retrieved');
    
    // Simulate checkout
    const checkout = new CheckoutPage(browser);
    await checkout.fillShippingInfo({
        firstName: 'John',
        lastName: 'Doe',
        address: '123 Main St',
        city: 'New York',
        zipCode: '10001',
        country: 'USA'
    });
    console.log('  ✓ Shipping info filled');
    
    console.log('\n=== Page Object Tests Complete ===\n');
}
*/

// ============================================
// EXERCISE 4: Test Data Builder Pattern
// ============================================

/**
 * Implement a Test Data Builder for creating test objects
 * with fluent API
 */

class UserBuilder {
  // Your implementation
}

class OrderBuilder {
  // Your implementation
}

/*
// SOLUTION:
class UserBuilder {
    constructor() {
        this.user = {
            id: Math.floor(Math.random() * 10000),
            name: 'Test User',
            email: `test${Date.now()}@example.com`,
            password: 'defaultPassword123',
            role: 'user',
            active: true,
            createdAt: new Date().toISOString(),
            settings: {}
        };
    }
    
    static create() {
        return new UserBuilder();
    }
    
    withId(id) {
        this.user.id = id;
        return this;
    }
    
    withName(name) {
        this.user.name = name;
        return this;
    }
    
    withEmail(email) {
        this.user.email = email;
        return this;
    }
    
    withPassword(password) {
        this.user.password = password;
        return this;
    }
    
    asAdmin() {
        this.user.role = 'admin';
        return this;
    }
    
    asModerator() {
        this.user.role = 'moderator';
        return this;
    }
    
    inactive() {
        this.user.active = false;
        return this;
    }
    
    withSettings(settings) {
        this.user.settings = { ...this.user.settings, ...settings };
        return this;
    }
    
    createdOn(date) {
        this.user.createdAt = date.toISOString();
        return this;
    }
    
    build() {
        return { ...this.user };
    }
    
    // Preset builders for common test cases
    static admin() {
        return UserBuilder.create()
            .withName('Admin User')
            .withEmail('admin@example.com')
            .asAdmin();
    }
    
    static inactiveUser() {
        return UserBuilder.create()
            .withName('Inactive User')
            .inactive();
    }
}

class OrderBuilder {
    constructor() {
        this.order = {
            id: Math.floor(Math.random() * 10000),
            userId: null,
            items: [],
            status: 'pending',
            shippingAddress: null,
            paymentMethod: null,
            subtotal: 0,
            tax: 0,
            shipping: 0,
            total: 0,
            createdAt: new Date().toISOString(),
            updatedAt: new Date().toISOString()
        };
    }
    
    static create() {
        return new OrderBuilder();
    }
    
    withId(id) {
        this.order.id = id;
        return this;
    }
    
    forUser(userId) {
        this.order.userId = userId;
        return this;
    }
    
    addItem(product, quantity = 1) {
        this.order.items.push({
            productId: product.id,
            name: product.name,
            price: product.price,
            quantity
        });
        this.recalculate();
        return this;
    }
    
    withItems(items) {
        this.order.items = items;
        this.recalculate();
        return this;
    }
    
    withStatus(status) {
        this.order.status = status;
        this.order.updatedAt = new Date().toISOString();
        return this;
    }
    
    confirmed() {
        return this.withStatus('confirmed');
    }
    
    shipped() {
        return this.withStatus('shipped');
    }
    
    delivered() {
        return this.withStatus('delivered');
    }
    
    cancelled() {
        return this.withStatus('cancelled');
    }
    
    withShippingAddress(address) {
        this.order.shippingAddress = address;
        return this;
    }
    
    withPaymentMethod(method) {
        this.order.paymentMethod = method;
        return this;
    }
    
    withShippingCost(cost) {
        this.order.shipping = cost;
        this.recalculate();
        return this;
    }
    
    withTaxRate(rate) {
        this.taxRate = rate;
        this.recalculate();
        return this;
    }
    
    recalculate() {
        this.order.subtotal = this.order.items.reduce(
            (sum, item) => sum + (item.price * item.quantity),
            0
        );
        this.order.tax = this.order.subtotal * (this.taxRate || 0.1);
        this.order.total = this.order.subtotal + this.order.tax + this.order.shipping;
        return this;
    }
    
    createdOn(date) {
        this.order.createdAt = date.toISOString();
        this.order.updatedAt = date.toISOString();
        return this;
    }
    
    build() {
        return { ...this.order };
    }
    
    // Preset builders
    static simpleOrder(userId) {
        return OrderBuilder.create()
            .forUser(userId)
            .addItem({ id: 1, name: 'Widget', price: 10 }, 2)
            .withShippingAddress({
                street: '123 Main St',
                city: 'New York',
                zipCode: '10001'
            });
    }
    
    static completedOrder(userId) {
        return OrderBuilder.simpleOrder(userId)
            .delivered()
            .withPaymentMethod({ type: 'card', last4: '4242' });
    }
}

// Test builders
function testBuilders() {
    console.log('=== Test Data Builder Tests ===\n');
    
    // User builder
    const adminUser = UserBuilder.admin().build();
    console.log('Admin User:', adminUser.name, adminUser.role);
    console.assert(adminUser.role === 'admin', 'Should be admin');
    console.log('  ✓ Admin user created');
    
    const customUser = UserBuilder.create()
        .withName('John Doe')
        .withEmail('john@example.com')
        .withSettings({ theme: 'dark', notifications: true })
        .build();
    console.log('Custom User:', customUser.name, customUser.settings);
    console.log('  ✓ Custom user created');
    
    // Order builder
    const simpleOrder = OrderBuilder.simpleOrder(1).build();
    console.log('Simple Order:', simpleOrder.status, 'Total:', simpleOrder.total);
    console.log('  ✓ Simple order created');
    
    const complexOrder = OrderBuilder.create()
        .forUser(1)
        .addItem({ id: 1, name: 'Widget', price: 10 }, 3)
        .addItem({ id: 2, name: 'Gadget', price: 25 }, 1)
        .withShippingCost(5)
        .withTaxRate(0.08)
        .confirmed()
        .build();
    console.log('Complex Order:', complexOrder.status, 'Total:', complexOrder.total.toFixed(2));
    console.log('  ✓ Complex order created');
    
    console.log('\n=== Builder Tests Complete ===\n');
}
*/

// ============================================
// RUN EXERCISES
// ============================================

console.log('=== Integration & E2E Testing Exercises ===');
console.log('');
console.log('Implement the following exercises:');
console.log('1. testOrderProcessor - Integration test for order processing');
console.log('2. testTaskAPI - API integration tests');
console.log('3. Page Objects - ProductListPage, CartPage, CheckoutPage');
console.log('4. Test Data Builders - UserBuilder, OrderBuilder');
console.log('');
console.log('Uncomment solutions to verify your implementation.');

// Export
if (typeof module !== 'undefined' && module.exports) {
  module.exports = {
    OrderProcessor,
    TaskAPI,
    Browser,
    ProductListPage,
    ProductDetailPage,
    CartPage,
    CheckoutPage,
  };
}
Exercises - JavaScript Tutorial | DeepML