release/v0.1.0 #24
19 changed files with 1479 additions and 0 deletions
|
|
@ -72,6 +72,36 @@ services:
|
||||||
networks:
|
networks:
|
||||||
- trip-planner-network
|
- 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:
|
networks:
|
||||||
trip-planner-network:
|
trip-planner-network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,8 @@ import react from '@vitejs/plugin-react'
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
host: '0.0.0.0', // Allow external connections
|
||||||
|
port: 5173
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
2
tests/.gitignore
vendored
Normal file
2
tests/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
/node_modules
|
||||||
|
/package-lock.json
|
||||||
153
tests/README.md
Normal file
153
tests/README.md
Normal 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`
|
||||||
63
tests/config/jest.setup.js
Normal file
63
tests/config/jest.setup.js
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
63
tests/config/jest.setup.local.js
Normal file
63
tests/config/jest.setup.local.js
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
82
tests/config/test-utils.js
Normal file
82
tests/config/test-utils.js
Normal 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
225
tests/e2e/auth.test.js
Normal 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
34
tests/e2e/debug.test.js
Normal 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
|
||||||
|
});
|
||||||
61
tests/e2e/screenshot.test.js
Normal file
61
tests/e2e/screenshot.test.js
Normal 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);
|
||||||
|
});
|
||||||
77
tests/e2e/simple-auth.test.js
Normal file
77
tests/e2e/simple-auth.test.js
Normal 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);
|
||||||
|
});
|
||||||
136
tests/e2e/visual-auth.test.js
Normal file
136
tests/e2e/visual-auth.test.js
Normal 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
58
tests/fixtures/users.json
vendored
Normal 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
6
tests/jest-local.json
Normal 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
38
tests/package.json
Normal 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
68
tests/pages/BasePage.js
Normal 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;
|
||||||
64
tests/pages/DashboardPage.js
Normal file
64
tests/pages/DashboardPage.js
Normal 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
140
tests/pages/LoginPage.js
Normal 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;
|
||||||
175
tests/pages/RegistrationPage.js
Normal file
175
tests/pages/RegistrationPage.js
Normal 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;
|
||||||
Loading…
Reference in a new issue