356 lines
No EOL
15 KiB
JavaScript
356 lines
No EOL
15 KiB
JavaScript
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',
|
|
startDate: '2025-02-01',
|
|
endDate: '2025-02-05'
|
|
};
|
|
});
|
|
|
|
afterAll(async () => {
|
|
global.plannableTestsInitialized = false;
|
|
await global.quitDriver(driver);
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
// 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);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// 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);
|
|
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);
|
|
|
|
// 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 () => {
|
|
// 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")]'));
|
|
// 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);
|
|
|
|
// 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 () => {
|
|
// Navigate to trip detail page
|
|
await navigateToTripDetail();
|
|
|
|
// 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")]'));
|
|
await driver.executeScript("arguments[0].scrollIntoView(true);", updateButton);
|
|
await driver.sleep(300);
|
|
await driver.executeScript("arguments[0].click();", updateButton);
|
|
|
|
// Wait for modal to close
|
|
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 () => {
|
|
// Navigate to trip detail page
|
|
await navigateToTripDetail();
|
|
|
|
// 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();
|
|
|
|
// 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();
|
|
|
|
// 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 () => {
|
|
// 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);
|
|
|
|
// Submit the form - use JavaScript click to avoid interception
|
|
const submitButton = await driver.findElement(By.xpath('//button[contains(text(), "Add Item")]'));
|
|
await driver.executeScript("arguments[0].scrollIntoView(true);", submitButton);
|
|
await driver.sleep(500);
|
|
await driver.executeScript("arguments[0].click();", submitButton);
|
|
|
|
// Wait for modal to close
|
|
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('🎯');
|
|
});
|
|
});
|
|
}); |