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('Timeline Scheduling 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: `Timeline Test User ${timestamp}`, email: `timeline.test.${timestamp}@example.com`, password: 'TimelineTest123!' }; testTrip = { name: `Timeline Test Trip ${timestamp}`, description: 'Testing timeline scheduling feature', startDate: '2025-03-01', endDate: '2025-03-03' }; }); afterAll(async () => { global.timelineTestsInitialized = false; await global.quitDriver(driver); }); beforeEach(async () => { // For individual test runs or when starting fresh, ensure we have proper setup if (!global.timelineTestsInitialized) { // 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 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 await driver.wait(until.elementLocated(By.className('dashboard')), 10000); await driver.wait(until.elementLocated(By.css('.add-trip-card')), 10000); // Create a trip 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.timelineTestsInitialized = true; } catch (e) { console.log('Failed to initialize timeline test environment:', e.message); } } }); // Helper function to navigate to trip detail page async function navigateToTripDetail() { const currentUrl = await driver.getCurrentUrl(); if (global.timelineTripDetailUrl && currentUrl === global.timelineTripDetailUrl) { await driver.wait(until.elementLocated(By.className('trip-timeline')), 10000); await driver.sleep(500); return; } if (global.timelineTripDetailUrl) { await driver.get(global.timelineTripDetailUrl); 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.timelineTripDetailUrl = await driver.getCurrentUrl(); } await driver.wait(until.elementLocated(By.className('trip-timeline')), 10000); await driver.sleep(500); } // Helper function to create a plannable item async function createPlannableItem(itemData) { // Ensure no modal is already open await driver.sleep(500); // Click Add Item button in sidebar using JavaScript click const addItemButton = await driver.findElement(By.xpath('//button[contains(text(), "Add Item")]')); await driver.executeScript("arguments[0].scrollIntoView(true);", addItemButton); await driver.sleep(300); await driver.executeScript("arguments[0].click();", addItemButton); // Wait for form modal await driver.wait(until.elementLocated(By.className('plannable-form-modal')), 5000); // Fill in the form await driver.findElement(By.name('name')).sendKeys(itemData.name); if (itemData.type) { const typeSelect = await driver.findElement(By.name('type')); await typeSelect.findElement(By.xpath(`//option[@value="${itemData.type}"]`)).click(); } if (itemData.address) { await driver.findElement(By.name('address')).sendKeys(itemData.address); } if (itemData.notes) { await driver.findElement(By.name('notes')).sendKeys(itemData.notes); } // Submit without assigning to a slot (leave in "Unplanned Items") 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 driver.sleep(1500); await driver.wait(async () => { const overlays = await driver.findElements(By.css('.plannable-form-overlay, .plannable-form-modal')); for (const overlay of overlays) { try { if (await overlay.isDisplayed()) { return false; } } catch (e) { // Stale element } } return true; }, 10000); // Additional wait to ensure UI is stable await driver.sleep(500); } // Helper to wait for modal close async function waitForModalClose() { await driver.sleep(1000); await driver.wait(async () => { const modals = await driver.findElements(By.css('.modal, .base-modal')); for (const modal of modals) { try { if (await modal.isDisplayed()) { return false; } } catch (e) { // Stale element } } return true; }, 10000); } describe('Timeline Display', () => { it('should display timeline with correct structure', async () => { await navigateToTripDetail(); // Verify timeline container exists const timeline = await driver.findElement(By.className('trip-timeline')); expect(await timeline.isDisplayed()).toBe(true); // Verify timeline has header const timelineHeader = await driver.findElement(By.xpath('//h3[contains(text(), "Trip Timeline")]')); expect(await timelineHeader.isDisplayed()).toBe(true); }); it('should display all days of the trip', async () => { await navigateToTripDetail(); // Trip is from March 1-3, so we should have 3 day sections const daySections = await driver.findElements(By.className('day-section')); expect(daySections.length).toBe(3); // Verify day headers const day1Header = await driver.findElement(By.xpath('//h4[contains(text(), "Day 1")]')); expect(await day1Header.isDisplayed()).toBe(true); const day2Header = await driver.findElement(By.xpath('//h4[contains(text(), "Day 2")]')); expect(await day2Header.isDisplayed()).toBe(true); const day3Header = await driver.findElement(By.xpath('//h4[contains(text(), "Day 3")]')); expect(await day3Header.isDisplayed()).toBe(true); }); it('should display hour rows for each day', async () => { await navigateToTripDetail(); // Get first day section const firstDaySection = await driver.findElement(By.className('day-section')); // Should have hour rows (06:00 to 23:00 = 18 hours) const hourRows = await firstDaySection.findElements(By.className('hour-row')); expect(hourRows.length).toBeGreaterThanOrEqual(18); // Verify specific hour labels exist const hour6am = await driver.findElement(By.xpath('//div[contains(@class, "hour-label") and contains(text(), "06:00")]')); expect(await hour6am.isDisplayed()).toBe(true); const hour11pm = await driver.findElement(By.xpath('//div[contains(@class, "hour-label") and contains(text(), "23:00")]')); expect(await hour11pm.isDisplayed()).toBe(true); }); }); describe('Scheduling Items via Timeline', () => { it('should show + button on hover over hour row', async () => { await navigateToTripDetail(); // Find an hour row const hourRow = await driver.findElement(By.className('hour-row')); // Hover over it await driver.actions().move({ origin: hourRow }).perform(); await driver.sleep(500); // Should see a + button const addButton = await hourRow.findElement(By.css('button')); expect(await addButton.isDisplayed()).toBe(true); }); it('should open schedule modal when clicking + button', async () => { await navigateToTripDetail(); // First create a plannable item to schedule await createPlannableItem({ name: 'Breakfast at Cafe', type: 'restaurant', notes: 'Morning coffee' }); // Now find the 08:00 hour row const hour8am = await driver.findElement(By.xpath('//div[contains(@class, "hour-label") and contains(text(), "08:00")]/following-sibling::div[contains(@class, "hour-content")]')); // Hover to show + button await driver.actions().move({ origin: hour8am }).perform(); await driver.sleep(500); // Click the + button const addButton = await hour8am.findElement(By.css('button')); await addButton.click(); // Wait for schedule modal to appear await driver.wait(until.elementLocated(By.xpath('//*[contains(text(), "Schedule Item")]')), 5000); // Verify modal elements const itemSelect = await driver.findElement(By.css('select')); expect(await itemSelect.isDisplayed()).toBe(true); // Should have time pickers const timeInputs = await driver.findElements(By.css('select')); expect(timeInputs.length).toBeGreaterThanOrEqual(2); // Start time and end time }); it('should schedule an item at specific time', async () => { await navigateToTripDetail(); // Create plannable item await createPlannableItem({ name: 'Louvre Museum Visit', type: 'attraction', notes: 'Morning visit to see Mona Lisa' }); await driver.sleep(1000); // Find the 10:00 hour row on Day 1 const daySections = await driver.findElements(By.className('day-section')); const day1Section = daySections[0]; const hour10am = await day1Section.findElement(By.xpath('.//div[contains(@class, "hour-label") and contains(text(), "10:00")]/following-sibling::div[contains(@class, "hour-content")]')); // Hover and click + await driver.actions().move({ origin: hour10am }).perform(); await driver.sleep(500); const addButton = await hour10am.findElement(By.css('button')); await addButton.click(); // Wait for modal await driver.wait(until.elementLocated(By.xpath('//*[contains(text(), "Schedule Item")]')), 5000); // Select the item const itemSelect = await driver.findElement(By.xpath('//label[contains(text(), "Select Item")]/following-sibling::select')); await itemSelect.findElement(By.xpath('//option[contains(text(), "Louvre Museum Visit")]')).click(); // Start time should default to 10:00 const startTimeSelect = await driver.findElement(By.xpath('//label[contains(text(), "Start Time")]/following-sibling::select')); const startTimeValue = await startTimeSelect.getAttribute('value'); expect(startTimeValue).toBe('10:00'); // Set end time to 12:00 const endTimeSelect = await driver.findElement(By.xpath('//label[contains(text(), "End Time")]/following-sibling::select')); await endTimeSelect.findElement(By.xpath('//option[@value="12:00"]')).click(); // Submit const scheduleButton = await driver.findElement(By.xpath('//button[contains(text(), "Schedule")]')); await scheduleButton.click(); // Wait for modal to close await waitForModalClose(); // Verify item appears in timeline at 10:00 await driver.sleep(1000); const scheduledItem = await driver.findElement(By.xpath('//div[contains(@class, "scheduled-slot") and contains(., "Louvre Museum Visit")]')); expect(await scheduledItem.isDisplayed()).toBe(true); // Verify time display shows 10:00 - 12:00 const timeDisplay = await scheduledItem.getText(); expect(timeDisplay).toContain('10:00'); expect(timeDisplay).toContain('12:00'); }); it('should schedule multiple items on the same day', async () => { await navigateToTripDetail(); // Create multiple plannable items await createPlannableItem({ name: 'Morning Croissant', type: 'restaurant' }); await driver.sleep(500); await createPlannableItem({ name: 'Afternoon Tea', type: 'restaurant' }); await driver.sleep(1000); // Schedule first item at 08:00 const daySections = await driver.findElements(By.className('day-section')); const day2Section = daySections[1]; // Day 2 const hour8am = await day2Section.findElement(By.xpath('.//div[contains(@class, "hour-label") and contains(text(), "08:00")]/following-sibling::div[contains(@class, "hour-content")]')); await driver.actions().move({ origin: hour8am }).perform(); await driver.sleep(500); await hour8am.findElement(By.css('button')).click(); await driver.wait(until.elementLocated(By.xpath('//*[contains(text(), "Schedule Item")]')), 5000); const itemSelect1 = await driver.findElement(By.xpath('//label[contains(text(), "Select Item")]/following-sibling::select')); await itemSelect1.findElement(By.xpath('//option[contains(text(), "Morning Croissant")]')).click(); const scheduleButton1 = await driver.findElement(By.xpath('//button[contains(text(), "Schedule")]')); await scheduleButton1.click(); await waitForModalClose(); await driver.sleep(1000); // Schedule second item at 15:00 const hour3pm = await day2Section.findElement(By.xpath('.//div[contains(@class, "hour-label") and contains(text(), "15:00")]/following-sibling::div[contains(@class, "hour-content")]')); await driver.actions().move({ origin: hour3pm }).perform(); await driver.sleep(500); await hour3pm.findElement(By.css('button')).click(); await driver.wait(until.elementLocated(By.xpath('//*[contains(text(), "Schedule Item")]')), 5000); const itemSelect2 = await driver.findElement(By.xpath('//label[contains(text(), "Select Item")]/following-sibling::select')); await itemSelect2.findElement(By.xpath('//option[contains(text(), "Afternoon Tea")]')).click(); const endTimeSelect = await driver.findElement(By.xpath('//label[contains(text(), "End Time")]/following-sibling::select')); await endTimeSelect.findElement(By.xpath('//option[@value="16:00"]')).click(); const scheduleButton2 = await driver.findElement(By.xpath('//button[contains(text(), "Schedule")]')); await scheduleButton2.click(); await waitForModalClose(); await driver.sleep(1000); // Verify both items appear in Day 2 section const croissantItem = await day2Section.findElement(By.xpath('.//div[contains(., "Morning Croissant")]')); expect(await croissantItem.isDisplayed()).toBe(true); const teaItem = await day2Section.findElement(By.xpath('.//div[contains(., "Afternoon Tea")]')); expect(await teaItem.isDisplayed()).toBe(true); }); it('should validate end time is after start time', async () => { await navigateToTripDetail(); // Create plannable item await createPlannableItem({ name: 'Invalid Time Test', type: 'other' }); await driver.sleep(1000); // Open schedule modal at 14:00 const daySections = await driver.findElements(By.className('day-section')); const day3Section = daySections[2]; // Day 3 const hour2pm = await day3Section.findElement(By.xpath('.//div[contains(@class, "hour-label") and contains(text(), "14:00")]/following-sibling::div[contains(@class, "hour-content")]')); await driver.actions().move({ origin: hour2pm }).perform(); await driver.sleep(500); await hour2pm.findElement(By.css('button')).click(); await driver.wait(until.elementLocated(By.xpath('//*[contains(text(), "Schedule Item")]')), 5000); // Select item const itemSelect = await driver.findElement(By.xpath('//label[contains(text(), "Select Item")]/following-sibling::select')); await itemSelect.findElement(By.xpath('//option[contains(text(), "Invalid Time Test")]')).click(); // Try to set end time before start time (e.g., start 14:00, end 13:00) const endTimeSelect = await driver.findElement(By.xpath('//label[contains(text(), "End Time")]/following-sibling::select')); await endTimeSelect.findElement(By.xpath('//option[@value="13:00"]')).click(); // Submit const scheduleButton = await driver.findElement(By.xpath('//button[contains(text(), "Schedule")]')); await scheduleButton.click(); await driver.sleep(1000); // Should show error message const errorMessage = await driver.findElement(By.xpath('//*[contains(text(), "End time must be after start time")]')); expect(await errorMessage.isDisplayed()).toBe(true); }); }); describe('Timeline Integration', () => { it('should display item in both sidebar and timeline after scheduling', async () => { await navigateToTripDetail(); // Create plannable item await createPlannableItem({ name: 'Arc de Triomphe', type: 'attraction' }); await driver.sleep(1000); // Verify it appears in sidebar under "Unplanned Items" const sidebarItem = await driver.findElement(By.xpath('//h4[contains(text(), "Arc de Triomphe")]')); expect(await sidebarItem.isDisplayed()).toBe(true); // Schedule it via timeline const daySections = await driver.findElements(By.className('day-section')); const day1Section = daySections[0]; const hour11am = await day1Section.findElement(By.xpath('.//div[contains(@class, "hour-label") and contains(text(), "11:00")]/following-sibling::div[contains(@class, "hour-content")]')); await driver.actions().move({ origin: hour11am }).perform(); await driver.sleep(500); await hour11am.findElement(By.css('button')).click(); await driver.wait(until.elementLocated(By.xpath('//*[contains(text(), "Schedule Item")]')), 5000); const itemSelect = await driver.findElement(By.xpath('//label[contains(text(), "Select Item")]/following-sibling::select')); await itemSelect.findElement(By.xpath('//option[contains(text(), "Arc de Triomphe")]')).click(); const scheduleButton = await driver.findElement(By.xpath('//button[contains(text(), "Schedule")]')); await scheduleButton.click(); await waitForModalClose(); await driver.sleep(1000); // Verify it now appears in timeline const timelineItem = await driver.findElement(By.xpath('//div[contains(@class, "scheduled-slot") and contains(., "Arc de Triomphe")]')); expect(await timelineItem.isDisplayed()).toBe(true); // Note: The sidebar behavior might change - item could stay in unplanned or move to a day section // For now, we just verify it's in the timeline }); it('should maintain chronological order when items are scheduled out of order', async () => { await navigateToTripDetail(); // Create items await createPlannableItem({ name: 'Dinner', type: 'restaurant' }); await driver.sleep(500); await createPlannableItem({ name: 'Breakfast', type: 'restaurant' }); await driver.sleep(500); await createPlannableItem({ name: 'Lunch', type: 'restaurant' }); await driver.sleep(1000); const daySections = await driver.findElements(By.className('day-section')); const day1Section = daySections[0]; // Schedule Dinner first (19:00) const hour7pm = await day1Section.findElement(By.xpath('.//div[contains(@class, "hour-label") and contains(text(), "19:00")]/following-sibling::div[contains(@class, "hour-content")]')); await driver.actions().move({ origin: hour7pm }).perform(); await driver.sleep(500); await hour7pm.findElement(By.css('button')).click(); await driver.wait(until.elementLocated(By.xpath('//*[contains(text(), "Schedule Item")]')), 5000); let itemSelect = await driver.findElement(By.xpath('//label[contains(text(), "Select Item")]/following-sibling::select')); await itemSelect.findElement(By.xpath('//option[contains(text(), "Dinner")]')).click(); let scheduleButton = await driver.findElement(By.xpath('//button[contains(text(), "Schedule")]')); await scheduleButton.click(); await waitForModalClose(); await driver.sleep(1000); // Schedule Breakfast (07:00) const hour7am = await day1Section.findElement(By.xpath('.//div[contains(@class, "hour-label") and contains(text(), "07:00")]/following-sibling::div[contains(@class, "hour-content")]')); await driver.actions().move({ origin: hour7am }).perform(); await driver.sleep(500); await hour7am.findElement(By.css('button')).click(); await driver.wait(until.elementLocated(By.xpath('//*[contains(text(), "Schedule Item")]')), 5000); itemSelect = await driver.findElement(By.xpath('//label[contains(text(), "Select Item")]/following-sibling::select')); await itemSelect.findElement(By.xpath('//option[contains(text(), "Breakfast")]')).click(); scheduleButton = await driver.findElement(By.xpath('//button[contains(text(), "Schedule")]')); await scheduleButton.click(); await waitForModalClose(); await driver.sleep(1000); // Schedule Lunch (12:00) const hour12pm = await day1Section.findElement(By.xpath('.//div[contains(@class, "hour-label") and contains(text(), "12:00")]/following-sibling::div[contains(@class, "hour-content")]')); await driver.actions().move({ origin: hour12pm }).perform(); await driver.sleep(500); await hour12pm.findElement(By.css('button')).click(); await driver.wait(until.elementLocated(By.xpath('//*[contains(text(), "Schedule Item")]')), 5000); itemSelect = await driver.findElement(By.xpath('//label[contains(text(), "Select Item")]/following-sibling::select')); await itemSelect.findElement(By.xpath('//option[contains(text(), "Lunch")]')).click(); scheduleButton = await driver.findElement(By.xpath('//button[contains(text(), "Schedule")]')); await scheduleButton.click(); await waitForModalClose(); await driver.sleep(1000); // Verify items appear in chronological order in Day 1 section const scheduledSlots = await day1Section.findElements(By.className('scheduled-slot')); // Get text of all scheduled items to verify order const itemTexts = []; for (const slot of scheduledSlots) { const text = await slot.getText(); itemTexts.push(text); } // Breakfast should appear before Lunch, and Lunch before Dinner const breakfastIndex = itemTexts.findIndex(text => text.includes('Breakfast')); const lunchIndex = itemTexts.findIndex(text => text.includes('Lunch')); const dinnerIndex = itemTexts.findIndex(text => text.includes('Dinner')); expect(breakfastIndex).toBeLessThan(lunchIndex); expect(lunchIndex).toBeLessThan(dinnerIndex); }); }); });