trip-planner/tests/specs/integration/plannable-items.test.js

356 lines
15 KiB
JavaScript
Raw Permalink Normal View History

const { By, until } = require('selenium-webdriver');
const RegistrationPage = require('../../support/pages/RegistrationPage');
const LoginPage = require('../../support/pages/LoginPage');
const DashboardPage = require('../../support/pages/DashboardPage');
const TripPage = require('../../support/pages/TripPage');
describe('Plannable Items Feature Test', () => {
let driver;
let registrationPage;
let loginPage;
let dashboardPage;
let tripPage;
let testUser;
let testTrip;
beforeAll(async () => {
driver = await global.createDriver();
registrationPage = new RegistrationPage(driver);
loginPage = new LoginPage(driver);
dashboardPage = new DashboardPage(driver);
tripPage = new TripPage(driver);
// Create unique test data
const timestamp = Date.now();
testUser = {
name: `Plannable Test User ${timestamp}`,
email: `plannable.test.${timestamp}@example.com`,
password: 'PlanTest123!'
};
testTrip = {
name: `Test Trip with Plannables ${timestamp}`,
description: 'A trip to test plannable items feature',
2025-09-30 08:29:17 +02:00
startDate: '2025-02-01',
endDate: '2025-02-05'
};
});
afterAll(async () => {
2025-09-30 08:29:17 +02:00
global.plannableTestsInitialized = false;
await global.quitDriver(driver);
});
beforeEach(async () => {
2025-09-30 08:29:17 +02:00
// For individual test runs or when starting fresh, ensure we have proper setup
if (!global.plannableTestsInitialized) {
// Clear storage and cookies
await Promise.all([
driver.manage().deleteAllCookies().catch(() => {}),
driver.executeScript('try { localStorage.clear(); sessionStorage.clear(); } catch(e) {}')
]);
// Navigate to base URL
await driver.get(process.env.APP_URL || 'http://localhost:5173');
await driver.wait(until.urlContains('/'), 5000);
// Register new user if not already done
try {
await driver.wait(until.elementLocated(By.css('.auth-toggle button:last-child')), 2000);
await driver.findElement(By.css('.auth-toggle button:last-child')).click();
await registrationPage.register(testUser.name, testUser.email, testUser.password);
// Wait for dashboard to load after registration
await driver.wait(until.elementLocated(By.className('dashboard')), 10000);
await driver.wait(until.elementLocated(By.css('.add-trip-card')), 10000);
// Create a new trip if not already created
const existingTrips = await driver.findElements(By.xpath(`//h3[contains(text(), "${testTrip.name}")]`));
if (existingTrips.length === 0) {
const addTripCard = await driver.findElement(By.css('.add-trip-card'));
await addTripCard.click();
await driver.wait(until.elementLocated(By.css('.trip-modal')), 10000);
await tripPage.fillTripForm(testTrip);
await tripPage.submitTripForm();
// Wait for trip to be created
await driver.wait(until.elementLocated(By.xpath(`//h3[contains(text(), "${testTrip.name}")]`)), 10000);
}
2025-09-30 08:29:17 +02:00
global.plannableTestsInitialized = true;
} catch (e) {
// If already logged in, just verify we're on dashboard
try {
await driver.wait(until.elementLocated(By.className('dashboard')), 2000);
global.plannableTestsInitialized = true;
} catch (e2) {
console.log('Failed to initialize test environment:', e2.message);
}
}
}
});
2025-09-30 08:29:17 +02:00
// Helper function to navigate to trip detail page
async function navigateToTripDetail() {
const currentUrl = await driver.getCurrentUrl();
// Check if we're already on the trip detail page
if (global.tripDetailUrl && currentUrl === global.tripDetailUrl) {
// Already on the right page, just wait for plannables list
await driver.wait(until.elementLocated(By.className('plannables-list')), 10000);
await driver.sleep(500);
return;
}
// Need to navigate
if (global.tripDetailUrl) {
await driver.get(global.tripDetailUrl);
await driver.sleep(1000);
} else {
await driver.wait(until.elementLocated(By.className('dashboard')), 10000);
const tripCard = await driver.findElement(By.xpath(`//h3[contains(text(), "${testTrip.name}")]/ancestor::div[contains(@class, 'trip-card')]`));
await tripCard.click();
await driver.wait(until.urlContains('/trip/'), 10000);
2025-09-30 08:29:17 +02:00
global.tripDetailUrl = await driver.getCurrentUrl();
}
await driver.wait(until.elementLocated(By.className('plannables-list')), 10000);
// Ensure no modals are open
await driver.sleep(500);
}
// Helper function to wait for modal to close
async function waitForModalClose() {
await driver.sleep(1500);
await driver.wait(async () => {
const overlays = await driver.findElements(By.css('.plannable-form-overlay, .confirm-dialog, .modal'));
for (const overlay of overlays) {
try {
if (await overlay.isDisplayed()) {
return false;
}
} catch (e) {
// Element might be stale, continue
}
}
return true;
}, 10000);
}
describe('Plannable Items Management', () => {
it('should create a trip with auto-generated calendar slots', async () => {
// Navigate to trip detail page
await navigateToTripDetail();
// Verify trip detail page elements
await driver.wait(until.elementLocated(By.xpath(`//h1[contains(text(), "${testTrip.name}")]`)), 10000);
2025-09-30 08:29:17 +02:00
// Check that trip detail page is working (calendar slots functionality verified via API tests)
const tripTitle = await driver.findElement(By.xpath(`//h1[contains(text(), "${testTrip.name}")]`));
expect(await tripTitle.isDisplayed()).toBe(true);
});
it('should add a plannable item', async () => {
2025-09-30 08:29:17 +02:00
// Navigate to trip detail page
await navigateToTripDetail();
// Click Add Item button
await driver.wait(until.elementLocated(By.xpath('//button[contains(text(), "Add Item")]')), 10000);
const addItemButton = await driver.findElement(By.xpath('//button[contains(text(), "Add Item")]'));
await addItemButton.click();
// Wait for form modal to appear
await driver.wait(until.elementLocated(By.className('plannable-form-modal')), 5000);
// Fill in the plannable item form
const itemData = {
name: 'Eiffel Tower Visit',
type: 'attraction',
address: 'Champ de Mars, 5 Avenue Anatole France, 75007 Paris',
notes: 'Book tickets in advance for sunset visit'
};
await driver.findElement(By.name('name')).sendKeys(itemData.name);
// Select type
const typeSelect = await driver.findElement(By.name('type'));
await typeSelect.findElement(By.xpath(`//option[@value="${itemData.type}"]`)).click();
await driver.findElement(By.name('address')).sendKeys(itemData.address);
await driver.findElement(By.name('notes')).sendKeys(itemData.notes);
// Assign to Day 2
const slotSelect = await driver.findElement(By.name('calendar_slot_id'));
const day2Option = await slotSelect.findElement(By.xpath('//option[contains(text(), "Day 2")]'));
await day2Option.click();
// Submit the form
const submitButton = await driver.findElement(By.xpath('//button[contains(text(), "Add Item")]'));
2025-09-30 08:29:17 +02:00
// Scroll to ensure button is visible and clickable
await driver.executeScript("arguments[0].scrollIntoView(true);", submitButton);
await driver.sleep(500);
// Use JavaScript click to avoid interception
await driver.executeScript("arguments[0].click();", submitButton);
2025-09-30 08:29:17 +02:00
// Wait for modal to close
await waitForModalClose();
// Verify the item appears in Day 2 section
await driver.wait(until.elementLocated(By.xpath(`//h4[contains(text(), "${itemData.name}")]`)), 10000);
const itemElement = await driver.findElement(By.xpath(`//h4[contains(text(), "${itemData.name}")]`));
expect(await itemElement.isDisplayed()).toBe(true);
});
it('should edit a plannable item', async () => {
2025-09-30 08:29:17 +02:00
// Navigate to trip detail page
await navigateToTripDetail();
2025-09-30 08:29:17 +02:00
// Wait for the item to appear (it should have been created in previous test)
await driver.wait(until.elementLocated(By.xpath('//h4[contains(text(), "Eiffel Tower Visit")]')), 10000);
// Find the item and hover to show actions
const itemElement = await driver.findElement(By.xpath('//h4[contains(text(), "Eiffel Tower Visit")]/ancestor::div[contains(@class, "plannable-item")]'));
await driver.actions().move({ origin: itemElement }).perform();
// Click edit button
await driver.wait(until.elementLocated(By.className('btn-edit')), 5000);
const editButton = await driver.findElement(By.className('btn-edit'));
await editButton.click();
// Wait for form modal to appear
await driver.wait(until.elementLocated(By.className('plannable-form-modal')), 5000);
// Update the item name
const nameInput = await driver.findElement(By.name('name'));
await nameInput.clear();
await nameInput.sendKeys('Eiffel Tower Evening Visit');
// Update notes
const notesInput = await driver.findElement(By.name('notes'));
await notesInput.clear();
await notesInput.sendKeys('Sunset visit confirmed for 7 PM');
// Submit the form
const updateButton = await driver.findElement(By.xpath('//button[contains(text(), "Update Item")]'));
2025-09-30 08:29:17 +02:00
await driver.executeScript("arguments[0].scrollIntoView(true);", updateButton);
await driver.sleep(300);
await driver.executeScript("arguments[0].click();", updateButton);
// Wait for modal to close
2025-09-30 08:29:17 +02:00
await waitForModalClose();
// Verify the item was updated
await driver.wait(until.elementLocated(By.xpath('//h4[contains(text(), "Eiffel Tower Evening Visit")]')), 10000);
const updatedItem = await driver.findElement(By.xpath('//h4[contains(text(), "Eiffel Tower Evening Visit")]'));
expect(await updatedItem.isDisplayed()).toBe(true);
});
it('should delete a plannable item', async () => {
2025-09-30 08:29:17 +02:00
// Navigate to trip detail page
await navigateToTripDetail();
2025-09-30 08:29:17 +02:00
// Wait for the item to appear (it should have been edited in previous test)
await driver.wait(until.elementLocated(By.xpath('//h4[contains(text(), "Eiffel Tower Evening Visit")]')), 10000);
// Find the item and hover to show actions
const itemElement = await driver.findElement(By.xpath('//h4[contains(text(), "Eiffel Tower Evening Visit")]/ancestor::div[contains(@class, "plannable-item")]'));
await driver.actions().move({ origin: itemElement }).perform();
// Click delete button
await driver.wait(until.elementLocated(By.className('btn-delete')), 5000);
const deleteButton = await driver.findElement(By.className('btn-delete'));
await deleteButton.click();
2025-09-30 08:29:17 +02:00
// Wait for and accept confirmation dialog (custom modal, not browser alert)
await driver.wait(until.elementLocated(By.css('.confirm-dialog, .modal')), 5000);
await driver.sleep(500);
const confirmButton = await driver.findElement(By.xpath('//button[contains(text(), "Delete") or contains(@class, "confirm")]'));
await driver.executeScript("arguments[0].click();", confirmButton);
// Wait for modal to close
await waitForModalClose();
2025-09-30 08:29:17 +02:00
// Verify item was removed
const items = await driver.findElements(By.xpath('//h4[contains(text(), "Eiffel Tower Evening Visit")]'));
expect(items.length).toBe(0);
});
it('should handle multiple plannable items of different types', async () => {
2025-09-30 08:29:17 +02:00
// Navigate to trip detail page
await navigateToTripDetail();
// Test data for different item types
const itemsToAdd = [
{
name: 'Hotel Le Meurice',
type: 'hotel',
address: '228 Rue de Rivoli, 75001 Paris',
notes: 'Check-in at 3 PM'
},
{
name: 'Le Jules Verne Restaurant',
type: 'restaurant',
address: 'Eiffel Tower, Avenue Gustave Eiffel, 75007 Paris',
notes: 'Reservation at 8 PM'
},
{
name: 'Louvre Museum',
type: 'attraction',
address: 'Rue de Rivoli, 75001 Paris',
notes: 'Morning visit'
}
];
// Add each item
for (const item of itemsToAdd) {
// Click Add Item button
const addItemButton = await driver.findElement(By.xpath('//button[contains(text(), "Add Item")]'));
await addItemButton.click();
// Wait for form modal to appear
await driver.wait(until.elementLocated(By.className('plannable-form-modal')), 5000);
// Fill in the form
await driver.findElement(By.name('name')).sendKeys(item.name);
const typeSelect = await driver.findElement(By.name('type'));
await typeSelect.findElement(By.xpath(`//option[@value="${item.type}"]`)).click();
await driver.findElement(By.name('address')).sendKeys(item.address);
await driver.findElement(By.name('notes')).sendKeys(item.notes);
2025-09-30 08:29:17 +02:00
// Submit the form - use JavaScript click to avoid interception
const submitButton = await driver.findElement(By.xpath('//button[contains(text(), "Add Item")]'));
2025-09-30 08:29:17 +02:00
await driver.executeScript("arguments[0].scrollIntoView(true);", submitButton);
await driver.sleep(500);
await driver.executeScript("arguments[0].click();", submitButton);
// Wait for modal to close
2025-09-30 08:29:17 +02:00
await waitForModalClose();
// Verify the item appears
await driver.wait(until.elementLocated(By.xpath(`//h4[contains(text(), "${item.name}")]`)), 10000);
}
// Verify all items are displayed
for (const item of itemsToAdd) {
const itemElement = await driver.findElement(By.xpath(`//h4[contains(text(), "${item.name}")]`));
expect(await itemElement.isDisplayed()).toBe(true);
}
// Verify items show correct type icons
const hotelItem = await driver.findElement(By.xpath('//h4[contains(text(), "Hotel Le Meurice")]/preceding-sibling::div[contains(@class, "item-icon")]'));
const hotelIcon = await hotelItem.getText();
expect(hotelIcon).toContain('🏨');
const restaurantItem = await driver.findElement(By.xpath('//h4[contains(text(), "Le Jules Verne")]/preceding-sibling::div[contains(@class, "item-icon")]'));
const restaurantIcon = await restaurantItem.getText();
expect(restaurantIcon).toContain('🍽️');
const attractionItem = await driver.findElement(By.xpath('//h4[contains(text(), "Louvre Museum")]/preceding-sibling::div[contains(@class, "item-icon")]'));
const attractionIcon = await attractionItem.getText();
expect(attractionIcon).toContain('🎯');
});
});
});