Working selenium on local

This commit is contained in:
myrmidex 2025-09-27 00:23:19 +02:00
parent 8c68cdfe9f
commit f1b3c1c8ac
19 changed files with 1479 additions and 0 deletions

View file

@ -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

View file

@ -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
}
})

2
tests/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/node_modules
/package-lock.json

153
tests/README.md Normal file
View file

@ -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`

View file

@ -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
};
}
});

View file

@ -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
};
}
});

View file

@ -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;

225
tests/e2e/auth.test.js Normal file
View file

@ -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');
});
});
});

34
tests/e2e/debug.test.js Normal file
View file

@ -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
});

View file

@ -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);
});

View file

@ -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);
});

View file

@ -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);
});

58
tests/fixtures/users.json vendored Normal file
View file

@ -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"
}
}
}

6
tests/jest-local.json Normal file
View file

@ -0,0 +1,6 @@
{
"testEnvironment": "node",
"testMatch": ["**/e2e/**/*.test.js"],
"setupFilesAfterEnv": ["<rootDir>/config/jest.setup.local.js"],
"testTimeout": 30000
}

38
tests/package.json Normal file
View file

@ -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": ["<rootDir>/config/jest.setup.js"],
"testTimeout": 30000
},
"jest-local": {
"testEnvironment": "node",
"testMatch": ["**/e2e/**/*.test.js"],
"setupFilesAfterEnv": ["<rootDir>/config/jest.setup.local.js"],
"testTimeout": 30000
}
}

68
tests/pages/BasePage.js Normal file
View file

@ -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;

View file

@ -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;

140
tests/pages/LoginPage.js Normal file
View file

@ -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;

View file

@ -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;