diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 7e4f380..a4f4c06 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -72,6 +72,36 @@ services: networks: - trip-planner-network + selenium-hub: + image: selenium/hub:latest + container_name: trip-planner-selenium-hub + ports: + - "4442:4442" + - "4443:4443" + - "4444:4444" + environment: + - GRID_MAX_SESSION=4 + - GRID_BROWSER_TIMEOUT=300 + - GRID_TIMEOUT=300 + networks: + - trip-planner-network + + selenium-chrome: + image: selenium/node-chrome:latest + container_name: trip-planner-selenium-chrome + environment: + - HUB_HOST=selenium-hub + - HUB_PORT=4444 + - SE_EVENT_BUS_HOST=selenium-hub + - SE_EVENT_BUS_PUBLISH_PORT=4442 + - SE_EVENT_BUS_SUBSCRIBE_PORT=4443 + - NODE_MAX_INSTANCES=4 + - NODE_MAX_SESSION=4 + depends_on: + - selenium-hub + networks: + - trip-planner-network + networks: trip-planner-network: driver: bridge diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 8b0f57b..362566a 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -4,4 +4,8 @@ import react from '@vitejs/plugin-react' // https://vite.dev/config/ export default defineConfig({ plugins: [react()], + server: { + host: '0.0.0.0', // Allow external connections + port: 5173 + } }) diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000..d502512 --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1,2 @@ +/node_modules +/package-lock.json diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..bbaa1d6 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,153 @@ +# E2E Testing Suite for Trip Planner + +This directory contains end-to-end tests using Selenium WebDriver for the Trip Planner application. + +## Setup + +### Prerequisites + +1. **Docker Setup (Recommended)** + - Docker/Podman installed and running + - All services running: `podman-compose -f ../docker-compose.dev.yml up -d` + +2. **Local Setup (For visible browser testing)** + - Chrome or Chromium browser installed locally + - Frontend running on `http://localhost:5173` + - Backend running on `http://localhost:8000` + +### Installation + +```bash +cd tests +npm install +``` + +## Running Tests + +### Using Docker Selenium (Headless - No visible browser) + +```bash +# Run all tests +npm test + +# Run all tests in headless mode (explicitly) +npm run test:headless + +# Run authentication tests only +npm run test:auth + +# Run with verbose output +npm run test:debug +``` + +### Using Local Chrome (Visible browser on your machine) + +```bash +# Run all tests with visible browser +npm run test:local + +# Run debug test with visible browser +npm run test:local:debug + +# Run specific test file locally +HEADLESS=false jest --config jest-local.json e2e/visual-auth.test.js +``` + +### Debugging Tests + +```bash +# Simple debug test +npm run test:debug-simple + +# Visual authentication test (slow, with pauses) +npm run test:local e2e/visual-auth.test.js + +# Screenshot test (saves screenshots to tests/screenshots/) +npm test screenshot.test.js +``` + +## Test Structure + +``` +tests/ +├── config/ +│ ├── jest.setup.js # Docker Selenium configuration +│ ├── jest.setup.local.js # Local Chrome configuration +│ └── test-utils.js # Helper utilities +├── e2e/ +│ ├── auth.test.js # Main authentication test suite +│ ├── debug.test.js # Simple connection test +│ ├── visual-auth.test.js # Slow visual test for debugging +│ └── screenshot.test.js # Takes screenshots for debugging +├── fixtures/ +│ └── users.json # Test user data +├── pages/ +│ ├── BasePage.js # Base page object +│ ├── LoginPage.js # Login page object +│ ├── RegistrationPage.js # Registration page object +│ └── DashboardPage.js # Dashboard page object +└── screenshots/ # Screenshot output directory +``` + +## Environment Variables + +- `HEADLESS=true/false` - Run Chrome in headless mode (Docker only) +- `BASE_URL` - Override default frontend URL (default: http://localhost:5173) +- `SELENIUM_HUB` - Override Selenium hub URL (default: http://localhost:4444) + +## Common Issues + +### Tests hang with no browser window +- You're using Docker Selenium. The browser runs inside a container (invisible) +- Use `npm run test:local` to see the browser on your machine + +### Connection refused errors +- Ensure all Docker services are running: `podman-compose -f ../docker-compose.dev.yml up -d` +- Check frontend is accessible: `curl http://localhost:5173` +- Check backend is accessible: `curl http://localhost:8000` + +### Chrome not found (local testing) +- Install Chrome: https://www.google.com/chrome/ +- Or install Chromium: `sudo dnf install chromium` (Fedora) or `sudo apt install chromium-browser` (Ubuntu) + +### Tests can't find auth forms +- The app uses a single-page auth guard, not separate /login and /register routes +- Forms are toggled on the same page using buttons + +## Writing New Tests + +1. Create test file in `e2e/` directory +2. Use page objects from `pages/` for better maintainability +3. Add test data to `fixtures/` as needed +4. Follow the pattern in existing tests + +Example: +```javascript +const LoginPage = require('../pages/LoginPage'); + +describe('My New Test', () => { + let driver; + let loginPage; + + beforeAll(async () => { + driver = await global.createDriver(); + loginPage = new LoginPage(driver); + }); + + afterAll(async () => { + await global.quitDriver(driver); + }); + + test('should do something', async () => { + await loginPage.navigateToLogin(); + // ... test logic + }); +}); +``` + +## Tips + +- Use `npm run test:local` when debugging to see what's happening +- Use `npm test screenshot.test.js` to capture screenshots when tests fail +- Add `await driver.sleep(2000)` to slow down tests for debugging +- Check Docker logs: `podman logs -f trip-planner-selenium-chrome` \ No newline at end of file diff --git a/tests/config/jest.setup.js b/tests/config/jest.setup.js new file mode 100644 index 0000000..355b633 --- /dev/null +++ b/tests/config/jest.setup.js @@ -0,0 +1,63 @@ +// Jest setup file for Selenium E2E tests +const { Builder } = require('selenium-webdriver'); +const chrome = require('selenium-webdriver/chrome'); + +// Global test configuration +global.testConfig = { + baseUrl: process.env.BASE_URL || 'http://host.docker.internal:5173', + seleniumHub: process.env.SELENIUM_HUB || 'http://localhost:4444', + timeout: 30000, + isHeadless: process.env.HEADLESS === 'true' +}; + +// Global test utilities +global.createDriver = async () => { + const chromeOptions = new chrome.Options(); + + if (global.testConfig.isHeadless) { + chromeOptions.addArguments('--headless'); + } + + chromeOptions.addArguments('--no-sandbox'); + chromeOptions.addArguments('--disable-dev-shm-usage'); + chromeOptions.addArguments('--disable-gpu'); + chromeOptions.addArguments('--window-size=1920,1080'); + + const driver = await new Builder() + .forBrowser('chrome') + .setChromeOptions(chromeOptions) + .usingServer(global.testConfig.seleniumHub) + .build(); + + // Set implicit wait + await driver.manage().setTimeouts({ implicit: 10000 }); + + return driver; +}; + +// Global cleanup function +global.quitDriver = async (driver) => { + if (driver) { + await driver.quit(); + } +}; + +// Extend Jest matchers for better assertions +expect.extend({ + async toBeDisplayed(element) { + const isDisplayed = await element.isDisplayed(); + return { + message: () => `expected element to ${this.isNot ? 'not ' : ''}be displayed`, + pass: isDisplayed + }; + }, + + async toHaveText(element, expectedText) { + const actualText = await element.getText(); + const pass = actualText.includes(expectedText); + return { + message: () => `expected element to contain text "${expectedText}", but got "${actualText}"`, + pass + }; + } +}); \ No newline at end of file diff --git a/tests/config/jest.setup.local.js b/tests/config/jest.setup.local.js new file mode 100644 index 0000000..1c1ef8b --- /dev/null +++ b/tests/config/jest.setup.local.js @@ -0,0 +1,63 @@ +// Jest setup file for LOCAL Selenium tests (browser runs on host) +const { Builder } = require('selenium-webdriver'); +const chrome = require('selenium-webdriver/chrome'); + +// Global test configuration for local testing +global.testConfig = { + baseUrl: process.env.BASE_URL || 'http://localhost:5173', + seleniumHub: null, // Direct connection, no hub + timeout: 30000, + isHeadless: process.env.HEADLESS === 'true' +}; + +// Create driver directly without Selenium Grid +global.createDriver = async () => { + const chromeOptions = new chrome.Options(); + + if (global.testConfig.isHeadless) { + chromeOptions.addArguments('--headless'); + } + + chromeOptions.addArguments('--no-sandbox'); + chromeOptions.addArguments('--disable-dev-shm-usage'); + chromeOptions.addArguments('--disable-gpu'); + chromeOptions.addArguments('--window-size=1920,1080'); + + // Direct connection to Chrome on host + const driver = await new Builder() + .forBrowser('chrome') + .setChromeOptions(chromeOptions) + .build(); + + // Set implicit wait + await driver.manage().setTimeouts({ implicit: 10000 }); + + return driver; +}; + +// Global cleanup function +global.quitDriver = async (driver) => { + if (driver) { + await driver.quit(); + } +}; + +// Extend Jest matchers for better assertions +expect.extend({ + async toBeDisplayed(element) { + const isDisplayed = await element.isDisplayed(); + return { + message: () => `expected element to ${this.isNot ? 'not ' : ''}be displayed`, + pass: isDisplayed + }; + }, + + async toHaveText(element, expectedText) { + const actualText = await element.getText(); + const pass = actualText.includes(expectedText); + return { + message: () => `expected element to contain text "${expectedText}", but got "${actualText}"`, + pass + }; + } +}); \ No newline at end of file diff --git a/tests/config/test-utils.js b/tests/config/test-utils.js new file mode 100644 index 0000000..ef05d5b --- /dev/null +++ b/tests/config/test-utils.js @@ -0,0 +1,82 @@ +const { By, until } = require('selenium-webdriver'); + +class TestUtils { + constructor(driver) { + this.driver = driver; + } + + async waitForElement(selector, timeout = 10000) { + const element = await this.driver.wait( + until.elementLocated(By.css(selector)), + timeout, + `Element with selector '${selector}' not found within ${timeout}ms` + ); + return element; + } + + async waitForElementVisible(selector, timeout = 10000) { + const element = await this.waitForElement(selector, timeout); + await this.driver.wait( + until.elementIsVisible(element), + timeout, + `Element with selector '${selector}' not visible within ${timeout}ms` + ); + return element; + } + + async waitForElementClickable(selector, timeout = 10000) { + const element = await this.waitForElementVisible(selector, timeout); + await this.driver.wait( + until.elementIsEnabled(element), + timeout, + `Element with selector '${selector}' not clickable within ${timeout}ms` + ); + return element; + } + + async waitForText(selector, text, timeout = 10000) { + await this.driver.wait( + until.elementTextContains( + this.driver.findElement(By.css(selector)), + text + ), + timeout, + `Text '${text}' not found in element '${selector}' within ${timeout}ms` + ); + } + + async clearAndType(element, text) { + await element.clear(); + await element.sendKeys(text); + } + + async scrollToElement(element) { + await this.driver.executeScript('arguments[0].scrollIntoView(true);', element); + // Small delay to ensure scroll completes + await this.driver.sleep(500); + } + + async takeScreenshot(filename) { + const screenshot = await this.driver.takeScreenshot(); + require('fs').writeFileSync(`screenshots/${filename}`, screenshot, 'base64'); + } + + async getCurrentUrl() { + return await this.driver.getCurrentUrl(); + } + + async navigateTo(path) { + const url = `${global.testConfig.baseUrl}${path}`; + await this.driver.get(url); + } + + async refreshPage() { + await this.driver.navigate().refresh(); + } + + async goBack() { + await this.driver.navigate().back(); + } +} + +module.exports = TestUtils; \ No newline at end of file diff --git a/tests/e2e/auth.test.js b/tests/e2e/auth.test.js new file mode 100644 index 0000000..0c92155 --- /dev/null +++ b/tests/e2e/auth.test.js @@ -0,0 +1,225 @@ +const RegistrationPage = require('../pages/RegistrationPage'); +const LoginPage = require('../pages/LoginPage'); +const DashboardPage = require('../pages/DashboardPage'); +const users = require('../fixtures/users.json'); + +describe('Authentication E2E Tests', () => { + let driver; + let registrationPage; + let loginPage; + let dashboardPage; + + beforeAll(async () => { + driver = await global.createDriver(); + registrationPage = new RegistrationPage(driver); + loginPage = new LoginPage(driver); + dashboardPage = new DashboardPage(driver); + }); + + afterAll(async () => { + await global.quitDriver(driver); + }); + + beforeEach(async () => { + // Navigate to home page before each test to ensure clean state + await driver.get(global.testConfig.baseUrl); + }); + + describe('User Registration', () => { + test('should successfully register a new user', async () => { + const testUser = users.registrationTestUser; + + await registrationPage.navigateToRegistration(); + + expect(await registrationPage.isRegistrationFormDisplayed()).toBe(true); + expect(await registrationPage.getRegistrationHeading()).toBe('Register'); + + await registrationPage.register( + testUser.name, + testUser.email, + testUser.password + ); + + await registrationPage.waitForSuccessfulRegistration(); + + const successMessage = await registrationPage.getSuccessMessage(); + expect(successMessage).toContain('Registration successful'); + expect(await registrationPage.isFormCleared()).toBe(true); + }); + + test('should show validation errors for invalid registration data', async () => { + const invalidUser = users.invalidUsers.shortPassword; + + await registrationPage.navigateToRegistration(); + await registrationPage.register( + invalidUser.name, + invalidUser.email, + invalidUser.password + ); + + await registrationPage.waitForRegistrationError(); + + // Check that form shows validation errors + const hasErrors = await registrationPage.hasPasswordFieldError() || + await registrationPage.getPasswordErrorMessage() !== null || + await registrationPage.getGeneralErrorMessage() !== null; + + expect(hasErrors).toBe(true); + }); + + test('should show error for password mismatch', async () => { + const mismatchUser = users.invalidUsers.passwordMismatch; + + await registrationPage.navigateToRegistration(); + await registrationPage.register( + mismatchUser.name, + mismatchUser.email, + mismatchUser.password, + mismatchUser.passwordConfirmation + ); + + await registrationPage.waitForRegistrationError(); + + const hasPasswordConfirmationError = await registrationPage.hasPasswordConfirmationFieldError() || + await registrationPage.getPasswordConfirmationErrorMessage() !== null; + + expect(hasPasswordConfirmationError).toBe(true); + }); + + test('should show error for empty required fields', async () => { + await registrationPage.navigateToRegistration(); + await registrationPage.clickSubmit(); + + // The form should prevent submission with empty required fields + // or show validation errors + const submitButtonText = await registrationPage.getSubmitButtonText(); + expect(submitButtonText).toBe('Register'); // Button text shouldn't change to "Registering..." + }); + + test('should show error for invalid email format', async () => { + const invalidEmailUser = users.invalidUsers.invalidEmail; + + await registrationPage.navigateToRegistration(); + await registrationPage.register( + invalidEmailUser.name, + invalidEmailUser.email, + invalidEmailUser.password + ); + + // The HTML5 email validation should prevent form submission + // or backend should return validation error + const currentUrl = await registrationPage.getCurrentUrl(); + expect(currentUrl).toContain('/register'); + }); + }); + + describe('User Login', () => { + test('should successfully login with valid credentials', async () => { + const validCredentials = users.loginTestCases.validCredentials; + + await loginPage.navigateToLogin(); + + expect(await loginPage.isLoginFormDisplayed()).toBe(true); + expect(await loginPage.getLoginHeading()).toBe('Login'); + + await loginPage.login( + validCredentials.email, + validCredentials.password + ); + + await loginPage.waitForSuccessfulLogin(); + + // Should see dashboard after successful login + const dashboardVisible = await dashboardPage.isDashboardDisplayed(); + expect(dashboardVisible).toBe(true); + }); + + test('should show error for invalid credentials', async () => { + const invalidCredentials = users.loginTestCases.invalidCredentials; + + await loginPage.navigateToLogin(); + await loginPage.login( + invalidCredentials.email, + invalidCredentials.password + ); + + await loginPage.waitForLoginError(); + + const generalError = await loginPage.getGeneralErrorMessage(); + expect(generalError).toContain('Invalid email or password'); + }); + + test('should show validation errors for empty fields', async () => { + await loginPage.navigateToLogin(); + await loginPage.clickSubmit(); + + // The form should prevent submission with empty required fields + const submitButtonText = await loginPage.getSubmitButtonText(); + expect(submitButtonText).toBe('Login'); // Button text shouldn't change to "Logging in..." + }); + + test('should show error for invalid email format', async () => { + const invalidEmailCredentials = users.loginTestCases.invalidEmailFormat; + + await loginPage.navigateToLogin(); + await loginPage.login( + invalidEmailCredentials.email, + invalidEmailCredentials.password + ); + + // The HTML5 email validation should prevent form submission + // or backend should return validation error + const currentUrl = await loginPage.getCurrentUrl(); + expect(currentUrl).toContain('/login'); + }); + + test('should disable submit button while login is in progress', async () => { + const validCredentials = users.loginTestCases.validCredentials; + + await loginPage.navigateToLogin(); + await loginPage.enterEmail(validCredentials.email); + await loginPage.enterPassword(validCredentials.password); + + // Click submit and immediately check if button is disabled + await loginPage.clickSubmit(); + + // Check if button text changes to "Logging in..." (indicating loading state) + const submitButtonText = await loginPage.getSubmitButtonText(); + const isButtonDisabled = await loginPage.isSubmitButtonDisabled(); + + expect(submitButtonText === 'Logging in...' || isButtonDisabled).toBe(true); + }); + }); + + describe('Authentication Flow Integration', () => { + test('should allow registration followed by immediate login', async () => { + // Use a unique email for this test to avoid conflicts + const timestamp = Date.now(); + const testUser = { + name: 'Integration Test User', + email: `integration.test.${timestamp}@example.com`, + password: 'password123' + }; + + // Register new user + await registrationPage.navigateToRegistration(); + await registrationPage.register( + testUser.name, + testUser.email, + testUser.password + ); + + await registrationPage.waitForSuccessfulRegistration(); + + // Navigate to login and use the same credentials + await loginPage.navigateToLogin(); + await loginPage.login(testUser.email, testUser.password); + + await loginPage.waitForSuccessfulLogin(); + + // Should be successfully logged in + const currentUrl = await loginPage.getCurrentUrl(); + expect(currentUrl).not.toContain('/login'); + }); + }); +}); \ No newline at end of file diff --git a/tests/e2e/debug.test.js b/tests/e2e/debug.test.js new file mode 100644 index 0000000..9a1ff9d --- /dev/null +++ b/tests/e2e/debug.test.js @@ -0,0 +1,34 @@ +describe('Debug Test', () => { + let driver; + + beforeAll(async () => { + console.log('Creating driver...'); + driver = await global.createDriver(); + console.log('Driver created successfully'); + }); + + afterAll(async () => { + console.log('Quitting driver...'); + await global.quitDriver(driver); + }); + + test('should connect to frontend', async () => { + console.log('Attempting to navigate to:', global.testConfig.baseUrl); + + try { + await driver.get(global.testConfig.baseUrl); + console.log('Navigation successful'); + + const title = await driver.getTitle(); + console.log('Page title:', title); + + const currentUrl = await driver.getCurrentUrl(); + console.log('Current URL:', currentUrl); + + expect(title).toBeDefined(); + } catch (error) { + console.error('Error during navigation:', error.message); + throw error; + } + }, 60000); // 60 second timeout +}); \ No newline at end of file diff --git a/tests/e2e/screenshot.test.js b/tests/e2e/screenshot.test.js new file mode 100644 index 0000000..4c5bc64 --- /dev/null +++ b/tests/e2e/screenshot.test.js @@ -0,0 +1,61 @@ +const fs = require('fs'); +const path = require('path'); + +describe('Screenshot Debug Test', () => { + let driver; + + beforeAll(async () => { + driver = await global.createDriver(); + + // Create screenshots directory if it doesn't exist + const screenshotsDir = path.join(__dirname, '../screenshots'); + if (!fs.existsSync(screenshotsDir)) { + fs.mkdirSync(screenshotsDir); + } + }); + + afterAll(async () => { + await global.quitDriver(driver); + }); + + test('take screenshot of homepage', async () => { + console.log('Navigating to:', global.testConfig.baseUrl); + await driver.get(global.testConfig.baseUrl); + + // Wait for page to load + await driver.sleep(3000); + + // Take screenshot + const screenshot = await driver.takeScreenshot(); + const screenshotPath = path.join(__dirname, '../screenshots/homepage.png'); + fs.writeFileSync(screenshotPath, screenshot, 'base64'); + console.log('Screenshot saved to:', screenshotPath); + + // Get page info + const title = await driver.getTitle(); + const url = await driver.getCurrentUrl(); + console.log('Page title:', title); + console.log('Current URL:', url); + + // Try to find and screenshot the auth container + try { + const authContainer = await driver.findElement({ className: 'auth-container' }); + console.log('Found auth container'); + + // Click register button and take screenshot + const registerButton = await driver.findElement({ css: '.auth-toggle button:last-child' }); + await registerButton.click(); + await driver.sleep(1000); + + const screenshot2 = await driver.takeScreenshot(); + const screenshotPath2 = path.join(__dirname, '../screenshots/register-form.png'); + fs.writeFileSync(screenshotPath2, screenshot2, 'base64'); + console.log('Register form screenshot saved to:', screenshotPath2); + + } catch (e) { + console.log('Auth container not found:', e.message); + } + + expect(true).toBe(true); + }, 60000); +}); \ No newline at end of file diff --git a/tests/e2e/simple-auth.test.js b/tests/e2e/simple-auth.test.js new file mode 100644 index 0000000..8a839f0 --- /dev/null +++ b/tests/e2e/simple-auth.test.js @@ -0,0 +1,77 @@ +const { By, until } = require('selenium-webdriver'); + +describe('Simple Authentication Test', () => { + let driver; + + beforeAll(async () => { + driver = await global.createDriver(); + }); + + afterAll(async () => { + await global.quitDriver(driver); + }); + + test('should display auth forms', async () => { + console.log('Navigating to:', global.testConfig.baseUrl); + await driver.get(global.testConfig.baseUrl); + + // Wait for page to load + await driver.sleep(2000); + + // Log page source to see what's actually there + const pageSource = await driver.getPageSource(); + console.log('Page contains auth-container?', pageSource.includes('auth-container')); + console.log('Page contains login-form?', pageSource.includes('login-form')); + console.log('Page contains registration-form?', pageSource.includes('registration-form')); + + // Try to find auth container + try { + const authContainer = await driver.findElement(By.className('auth-container')); + console.log('Found auth-container'); + + // Check for toggle buttons + const toggleButtons = await driver.findElements(By.css('.auth-toggle button')); + console.log('Found toggle buttons:', toggleButtons.length); + + // Check which form is visible + try { + const loginForm = await driver.findElement(By.className('login-form')); + const isLoginVisible = await loginForm.isDisplayed(); + console.log('Login form visible:', isLoginVisible); + } catch (e) { + console.log('Login form not found'); + } + + try { + const regForm = await driver.findElement(By.className('registration-form')); + const isRegVisible = await regForm.isDisplayed(); + console.log('Registration form visible:', isRegVisible); + } catch (e) { + console.log('Registration form not found'); + } + + } catch (error) { + console.log('Auth container not found'); + + // Check if we're seeing the dashboard instead + try { + const dashboard = await driver.findElement(By.className('dashboard')); + console.log('Dashboard found - user might already be logged in'); + } catch (e) { + console.log('Dashboard not found either'); + } + + // Log the actual page title and URL + const title = await driver.getTitle(); + const url = await driver.getCurrentUrl(); + console.log('Page title:', title); + console.log('Current URL:', url); + + // Get first 500 chars of body text + const bodyText = await driver.findElement(By.tagName('body')).getText(); + console.log('Page text (first 500 chars):', bodyText.substring(0, 500)); + } + + expect(true).toBe(true); // Just to make test pass while debugging + }, 60000); +}); \ No newline at end of file diff --git a/tests/e2e/visual-auth.test.js b/tests/e2e/visual-auth.test.js new file mode 100644 index 0000000..6cf5360 --- /dev/null +++ b/tests/e2e/visual-auth.test.js @@ -0,0 +1,136 @@ +const { By, until } = require('selenium-webdriver'); + +describe('Visual Authentication Test', () => { + let driver; + + beforeAll(async () => { + console.log('Starting Chrome browser...'); + driver = await global.createDriver(); + console.log('Browser started!'); + }); + + afterAll(async () => { + console.log('Test complete - keeping browser open for 5 seconds...'); + await driver.sleep(5000); + await global.quitDriver(driver); + }); + + test('should test registration and login visually', async () => { + console.log('Navigating to app...'); + await driver.get(global.testConfig.baseUrl); + + // Wait for page load + console.log('Waiting for page to load...'); + await driver.sleep(2000); + + // Check if auth container exists + try { + const authContainer = await driver.findElement(By.className('auth-container')); + console.log('✓ Found auth container'); + + // Click Register tab + console.log('Clicking Register tab...'); + const registerButton = await driver.findElement(By.css('.auth-toggle button:last-child')); + await registerButton.click(); + await driver.sleep(1000); + + // Fill registration form + console.log('Filling registration form...'); + const nameInput = await driver.findElement(By.id('name')); + await nameInput.sendKeys('Test User'); + await driver.sleep(500); + + const emailInput = await driver.findElement(By.id('email')); + await emailInput.sendKeys('test' + Date.now() + '@example.com'); + await driver.sleep(500); + + const passwordInput = await driver.findElement(By.id('password')); + await passwordInput.sendKeys('password123'); + await driver.sleep(500); + + const confirmPasswordInput = await driver.findElement(By.id('password_confirmation')); + await confirmPasswordInput.sendKeys('password123'); + await driver.sleep(1000); + + console.log('Submitting registration form...'); + const submitButton = await driver.findElement(By.css('button[type="submit"]')); + await submitButton.click(); + + // Wait to see the result + console.log('Waiting for registration response...'); + await driver.sleep(3000); + + // Check for success message + try { + const successMessage = await driver.findElement(By.className('alert-success')); + const text = await successMessage.getText(); + console.log('✓ Registration successful:', text); + } catch (e) { + console.log('No success message found'); + + // Check for errors + try { + const errorMessage = await driver.findElement(By.className('alert-error')); + const errorText = await errorMessage.getText(); + console.log('✗ Error:', errorText); + } catch (e2) { + console.log('No error message found either'); + } + } + + // Now test login + console.log('\nSwitching to Login form...'); + const loginButton = await driver.findElement(By.css('.auth-toggle button:first-child')); + await loginButton.click(); + await driver.sleep(1000); + + console.log('Filling login form...'); + const loginEmail = await driver.findElement(By.css('.login-form #email')); + await loginEmail.clear(); + await loginEmail.sendKeys('test@example.com'); + await driver.sleep(500); + + const loginPassword = await driver.findElement(By.css('.login-form #password')); + await loginPassword.clear(); + await loginPassword.sendKeys('password123'); + await driver.sleep(1000); + + console.log('Submitting login form...'); + const loginSubmit = await driver.findElement(By.css('.login-form button[type="submit"]')); + await loginSubmit.click(); + + console.log('Waiting for login response...'); + await driver.sleep(3000); + + // Check if we reached dashboard + try { + const dashboard = await driver.findElement(By.className('dashboard')); + console.log('✓ Successfully logged in - Dashboard visible'); + } catch (e) { + console.log('Dashboard not found - checking for login errors...'); + + try { + const errorMessage = await driver.findElement(By.className('alert-error')); + const errorText = await errorMessage.getText(); + console.log('✗ Login error:', errorText); + } catch (e2) { + console.log('Still on login form'); + } + } + + } catch (error) { + console.log('✗ Auth container not found'); + console.log('Error:', error.message); + + // Check if already logged in + try { + const dashboard = await driver.findElement(By.className('dashboard')); + console.log('User appears to be already logged in (dashboard visible)'); + } catch (e) { + console.log('Dashboard not found either - page structure might be different'); + } + } + + expect(true).toBe(true); + }, 120000); +}); \ No newline at end of file diff --git a/tests/fixtures/users.json b/tests/fixtures/users.json new file mode 100644 index 0000000..cbe20d4 --- /dev/null +++ b/tests/fixtures/users.json @@ -0,0 +1,58 @@ +{ + "validUser": { + "name": "Test User", + "email": "test@example.com", + "password": "password123" + }, + "registrationTestUser": { + "name": "Registration Test", + "email": "registration.test@example.com", + "password": "securepass123" + }, + "invalidUsers": { + "invalidEmail": { + "name": "Invalid Email User", + "email": "invalid-email", + "password": "password123" + }, + "shortPassword": { + "name": "Short Password User", + "email": "shortpass@example.com", + "password": "123" + }, + "emptyFields": { + "name": "", + "email": "", + "password": "" + }, + "missingName": { + "name": "", + "email": "noname@example.com", + "password": "password123" + }, + "passwordMismatch": { + "name": "Mismatch User", + "email": "mismatch@example.com", + "password": "password123", + "passwordConfirmation": "differentpass" + } + }, + "loginTestCases": { + "validCredentials": { + "email": "test@example.com", + "password": "password123" + }, + "invalidCredentials": { + "email": "nonexistent@example.com", + "password": "wrongpassword" + }, + "emptyCredentials": { + "email": "", + "password": "" + }, + "invalidEmailFormat": { + "email": "invalid-email-format", + "password": "password123" + } + } +} \ No newline at end of file diff --git a/tests/jest-local.json b/tests/jest-local.json new file mode 100644 index 0000000..829ded7 --- /dev/null +++ b/tests/jest-local.json @@ -0,0 +1,6 @@ +{ + "testEnvironment": "node", + "testMatch": ["**/e2e/**/*.test.js"], + "setupFilesAfterEnv": ["/config/jest.setup.local.js"], + "testTimeout": 30000 +} \ No newline at end of file diff --git a/tests/package.json b/tests/package.json new file mode 100644 index 0000000..e0534ba --- /dev/null +++ b/tests/package.json @@ -0,0 +1,38 @@ +{ + "name": "trip-planner-e2e-tests", + "version": "1.0.0", + "description": "End-to-end tests for Trip Planner application", + "main": "index.js", + "scripts": { + "test": "jest", + "test:headless": "HEADLESS=true jest", + "test:watch": "jest --watch", + "test:debug": "HEADLESS=false jest --detectOpenHandles", + "test:auth": "jest e2e/auth.test.js", + "test:debug-simple": "HEADLESS=false jest e2e/debug.test.js --verbose", + "test:local": "jest --config jest-local.json", + "test:local:debug": "HEADLESS=false jest --config jest-local.json e2e/debug.test.js --verbose" + }, + "keywords": ["selenium", "e2e", "testing"], + "author": "", + "license": "MIT", + "dependencies": { + "selenium-webdriver": "^4.24.0" + }, + "devDependencies": { + "jest": "^29.7.0", + "@types/jest": "^29.5.12" + }, + "jest": { + "testEnvironment": "node", + "testMatch": ["**/e2e/**/*.test.js"], + "setupFilesAfterEnv": ["/config/jest.setup.js"], + "testTimeout": 30000 + }, + "jest-local": { + "testEnvironment": "node", + "testMatch": ["**/e2e/**/*.test.js"], + "setupFilesAfterEnv": ["/config/jest.setup.local.js"], + "testTimeout": 30000 + } +} \ No newline at end of file diff --git a/tests/pages/BasePage.js b/tests/pages/BasePage.js new file mode 100644 index 0000000..f537b02 --- /dev/null +++ b/tests/pages/BasePage.js @@ -0,0 +1,68 @@ +const { By } = require('selenium-webdriver'); +const TestUtils = require('../config/test-utils'); + +class BasePage { + constructor(driver) { + this.driver = driver; + this.utils = new TestUtils(driver); + } + + async navigateTo(path = '/') { + await this.utils.navigateTo(path); + await this.waitForPageLoad(); + } + + async waitForPageLoad() { + // Wait for React to mount + await this.driver.wait( + () => this.driver.executeScript('return document.readyState === "complete"'), + 10000 + ); + + // Additional wait for React components to render + await this.driver.sleep(1000); + } + + async getCurrentUrl() { + return await this.utils.getCurrentUrl(); + } + + async getPageTitle() { + return await this.driver.getTitle(); + } + + async isElementPresent(selector) { + try { + await this.driver.findElement(By.css(selector)); + return true; + } catch (error) { + return false; + } + } + + async isElementVisible(selector) { + try { + const element = await this.driver.findElement(By.css(selector)); + return await element.isDisplayed(); + } catch (error) { + return false; + } + } + + async getElementText(selector) { + const element = await this.utils.waitForElement(selector); + return await element.getText(); + } + + async clickElement(selector) { + const element = await this.utils.waitForElementClickable(selector); + await element.click(); + } + + async typeIntoElement(selector, text) { + const element = await this.utils.waitForElement(selector); + await this.utils.clearAndType(element, text); + } +} + +module.exports = BasePage; \ No newline at end of file diff --git a/tests/pages/DashboardPage.js b/tests/pages/DashboardPage.js new file mode 100644 index 0000000..e0208ba --- /dev/null +++ b/tests/pages/DashboardPage.js @@ -0,0 +1,64 @@ +const BasePage = require('./BasePage'); + +class DashboardPage extends BasePage { + constructor(driver) { + super(driver); + + // Selectors for the Dashboard component + this.selectors = { + dashboard: '.dashboard', + welcomeMessage: '.dashboard h2', + userInfo: '.user-info', + logoutButton: 'button[onclick*="logout"]' + }; + } + + async navigateToDashboard() { + await this.navigateTo('/dashboard'); + await this.waitForDashboard(); + } + + async waitForDashboard() { + await this.utils.waitForElementVisible(this.selectors.dashboard); + } + + async isDashboardDisplayed() { + return await this.isElementVisible(this.selectors.dashboard); + } + + async getWelcomeMessage() { + try { + return await this.getElementText(this.selectors.welcomeMessage); + } catch (error) { + return null; + } + } + + async getUserInfo() { + try { + return await this.getElementText(this.selectors.userInfo); + } catch (error) { + return null; + } + } + + async logout() { + if (await this.isElementVisible(this.selectors.logoutButton)) { + await this.clickElement(this.selectors.logoutButton); + } + } + + async waitForLogout() { + // Wait for redirect away from dashboard + await this.driver.wait( + async () => { + const currentUrl = await this.getCurrentUrl(); + return !currentUrl.includes('/dashboard'); + }, + 10000, + 'Logout did not redirect within expected time' + ); + } +} + +module.exports = DashboardPage; \ No newline at end of file diff --git a/tests/pages/LoginPage.js b/tests/pages/LoginPage.js new file mode 100644 index 0000000..9479f25 --- /dev/null +++ b/tests/pages/LoginPage.js @@ -0,0 +1,140 @@ +const BasePage = require('./BasePage'); + +class LoginPage extends BasePage { + constructor(driver) { + super(driver); + + // Selectors based on the LoginForm component + this.selectors = { + form: '.login-form', + heading: '.login-form h2', + emailInput: '#email', + passwordInput: '#password', + submitButton: 'button[type="submit"]', + errorMessage: '.error-message', + generalError: '.alert-error', + fieldError: '.error-message' + }; + } + + async navigateToLogin() { + await this.navigateTo('/'); + // Click the Login tab if not already active + const loginButton = await this.driver.findElement({ css: '.auth-toggle button:first-child' }); + await loginButton.click(); + await this.waitForLoginForm(); + } + + async waitForLoginForm() { + await this.utils.waitForElementVisible(this.selectors.form); + } + + async isLoginFormDisplayed() { + return await this.isElementVisible(this.selectors.form); + } + + async getLoginHeading() { + return await this.getElementText(this.selectors.heading); + } + + async enterEmail(email) { + await this.typeIntoElement(this.selectors.emailInput, email); + } + + async enterPassword(password) { + await this.typeIntoElement(this.selectors.passwordInput, password); + } + + async clickSubmit() { + await this.clickElement(this.selectors.submitButton); + } + + async getSubmitButtonText() { + return await this.getElementText(this.selectors.submitButton); + } + + async isSubmitButtonDisabled() { + const button = await this.utils.waitForElement(this.selectors.submitButton); + return await button.getAttribute('disabled') !== null; + } + + async login(email, password) { + await this.enterEmail(email); + await this.enterPassword(password); + await this.clickSubmit(); + } + + async getGeneralErrorMessage() { + try { + return await this.getElementText(this.selectors.generalError); + } catch (error) { + return null; + } + } + + async getEmailErrorMessage() { + try { + const emailField = await this.utils.waitForElement(this.selectors.emailInput); + const parent = await emailField.findElement({ xpath: '..' }); + const errorElement = await parent.findElement({ css: this.selectors.fieldError }); + return await errorElement.getText(); + } catch (error) { + return null; + } + } + + async getPasswordErrorMessage() { + try { + const passwordField = await this.utils.waitForElement(this.selectors.passwordInput); + const parent = await passwordField.findElement({ xpath: '..' }); + const errorElement = await parent.findElement({ css: this.selectors.fieldError }); + return await errorElement.getText(); + } catch (error) { + return null; + } + } + + async hasEmailFieldError() { + const emailField = await this.utils.waitForElement(this.selectors.emailInput); + const className = await emailField.getAttribute('class'); + return className.includes('error'); + } + + async hasPasswordFieldError() { + const passwordField = await this.utils.waitForElement(this.selectors.passwordInput); + const className = await passwordField.getAttribute('class'); + return className.includes('error'); + } + + async waitForSuccessfulLogin() { + // Wait for dashboard to appear (auth guard passes) + await this.driver.wait( + async () => { + try { + const dashboard = await this.driver.findElement({ css: '.dashboard' }); + return await dashboard.isDisplayed(); + } catch (error) { + return false; + } + }, + 10000, + 'Login did not show dashboard within expected time' + ); + } + + async waitForLoginError() { + // Wait for either general error or field errors to appear + await this.driver.wait( + async () => { + const hasGeneralError = await this.isElementVisible(this.selectors.generalError); + const hasEmailError = await this.hasEmailFieldError(); + const hasPasswordError = await this.hasPasswordFieldError(); + return hasGeneralError || hasEmailError || hasPasswordError; + }, + 10000, + 'No error message appeared within expected time' + ); + } +} + +module.exports = LoginPage; \ No newline at end of file diff --git a/tests/pages/RegistrationPage.js b/tests/pages/RegistrationPage.js new file mode 100644 index 0000000..da40614 --- /dev/null +++ b/tests/pages/RegistrationPage.js @@ -0,0 +1,175 @@ +const BasePage = require('./BasePage'); + +class RegistrationPage extends BasePage { + constructor(driver) { + super(driver); + + // Selectors based on the RegistrationForm component + this.selectors = { + form: '.registration-form', + heading: '.registration-form h2', + nameInput: '#name', + emailInput: '#email', + passwordInput: '#password', + passwordConfirmationInput: '#password_confirmation', + submitButton: 'button[type="submit"]', + successMessage: '.alert-success', + generalError: '.alert-error', + fieldError: '.error-message' + }; + } + + async navigateToRegistration() { + await this.navigateTo('/'); + // Click the Register tab + const registerButton = await this.driver.findElement({ css: '.auth-toggle button:last-child' }); + await registerButton.click(); + await this.waitForRegistrationForm(); + } + + async waitForRegistrationForm() { + await this.utils.waitForElementVisible(this.selectors.form); + } + + async isRegistrationFormDisplayed() { + return await this.isElementVisible(this.selectors.form); + } + + async getRegistrationHeading() { + return await this.getElementText(this.selectors.heading); + } + + async enterName(name) { + await this.typeIntoElement(this.selectors.nameInput, name); + } + + async enterEmail(email) { + await this.typeIntoElement(this.selectors.emailInput, email); + } + + async enterPassword(password) { + await this.typeIntoElement(this.selectors.passwordInput, password); + } + + async enterPasswordConfirmation(password) { + await this.typeIntoElement(this.selectors.passwordConfirmationInput, password); + } + + async clickSubmit() { + await this.clickElement(this.selectors.submitButton); + } + + async getSubmitButtonText() { + return await this.getElementText(this.selectors.submitButton); + } + + async isSubmitButtonDisabled() { + const button = await this.utils.waitForElement(this.selectors.submitButton); + return await button.getAttribute('disabled') !== null; + } + + async register(name, email, password, passwordConfirmation = null) { + await this.enterName(name); + await this.enterEmail(email); + await this.enterPassword(password); + await this.enterPasswordConfirmation(passwordConfirmation || password); + await this.clickSubmit(); + } + + async getSuccessMessage() { + try { + return await this.getElementText(this.selectors.successMessage); + } catch (error) { + return null; + } + } + + async getGeneralErrorMessage() { + try { + return await this.getElementText(this.selectors.generalError); + } catch (error) { + return null; + } + } + + async getFieldErrorMessage(fieldSelector) { + try { + const field = await this.utils.waitForElement(fieldSelector); + const parent = await field.findElement({ xpath: '..' }); + const errorElement = await parent.findElement({ css: this.selectors.fieldError }); + return await errorElement.getText(); + } catch (error) { + return null; + } + } + + async getNameErrorMessage() { + return await this.getFieldErrorMessage(this.selectors.nameInput); + } + + async getEmailErrorMessage() { + return await this.getFieldErrorMessage(this.selectors.emailInput); + } + + async getPasswordErrorMessage() { + return await this.getFieldErrorMessage(this.selectors.passwordInput); + } + + async getPasswordConfirmationErrorMessage() { + return await this.getFieldErrorMessage(this.selectors.passwordConfirmationInput); + } + + async hasFieldError(fieldSelector) { + const field = await this.utils.waitForElement(fieldSelector); + const className = await field.getAttribute('class'); + return className.includes('error'); + } + + async hasNameFieldError() { + return await this.hasFieldError(this.selectors.nameInput); + } + + async hasEmailFieldError() { + return await this.hasFieldError(this.selectors.emailInput); + } + + async hasPasswordFieldError() { + return await this.hasFieldError(this.selectors.passwordInput); + } + + async hasPasswordConfirmationFieldError() { + return await this.hasFieldError(this.selectors.passwordConfirmationInput); + } + + async waitForSuccessfulRegistration() { + await this.utils.waitForElementVisible(this.selectors.successMessage); + } + + async waitForRegistrationError() { + // Wait for either general error or field errors to appear + await this.driver.wait( + async () => { + const hasGeneralError = await this.isElementVisible(this.selectors.generalError); + const hasNameError = await this.hasNameFieldError(); + const hasEmailError = await this.hasEmailFieldError(); + const hasPasswordError = await this.hasPasswordFieldError(); + const hasPasswordConfirmationError = await this.hasPasswordConfirmationFieldError(); + + return hasGeneralError || hasNameError || hasEmailError || hasPasswordError || hasPasswordConfirmationError; + }, + 10000, + 'No error message appeared within expected time' + ); + } + + async isFormCleared() { + const nameValue = await (await this.utils.waitForElement(this.selectors.nameInput)).getAttribute('value'); + const emailValue = await (await this.utils.waitForElement(this.selectors.emailInput)).getAttribute('value'); + const passwordValue = await (await this.utils.waitForElement(this.selectors.passwordInput)).getAttribute('value'); + const passwordConfirmationValue = await (await this.utils.waitForElement(this.selectors.passwordConfirmationInput)).getAttribute('value'); + + return nameValue === '' && emailValue === '' && passwordValue === '' && passwordConfirmationValue === ''; + } +} + +module.exports = RegistrationPage; \ No newline at end of file