trip-planner/tests/specs/integration/timeline-scheduling.test.js

572 lines
24 KiB
JavaScript
Raw 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('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);
});
});
});