296 lines
12 KiB
JavaScript
296 lines
12 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: '02/01/2025',
|
||
|
|
endDate: '02/05/2025'
|
||
|
|
};
|
||
|
|
});
|
||
|
|
|
||
|
|
afterAll(async () => {
|
||
|
|
await global.quitDriver(driver);
|
||
|
|
});
|
||
|
|
|
||
|
|
beforeEach(async () => {
|
||
|
|
// 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);
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('Plannable Items Management', () => {
|
||
|
|
it('should create a trip with auto-generated calendar slots', async () => {
|
||
|
|
// Register new user
|
||
|
|
await driver.wait(until.elementLocated(By.css('[data-testid="register-link"]')), 10000);
|
||
|
|
await driver.findElement(By.css('[data-testid="register-link"]')).click();
|
||
|
|
|
||
|
|
await registrationPage.register(testUser);
|
||
|
|
await driver.wait(until.urlContains('/'), 10000);
|
||
|
|
|
||
|
|
// Create a new trip
|
||
|
|
await tripPage.openCreateModal();
|
||
|
|
await tripPage.fillTripForm(testTrip);
|
||
|
|
await tripPage.submitForm();
|
||
|
|
|
||
|
|
// Wait for trip to be created
|
||
|
|
await driver.wait(until.elementLocated(By.xpath(`//h3[contains(text(), "${testTrip.name}")]`)), 10000);
|
||
|
|
|
||
|
|
// Click on the trip card to navigate to detail page
|
||
|
|
const tripCard = await driver.findElement(By.xpath(`//h3[contains(text(), "${testTrip.name}")]/ancestor::div[contains(@class, 'trip-card')]`));
|
||
|
|
await tripCard.click();
|
||
|
|
|
||
|
|
// Wait for trip detail page to load
|
||
|
|
await driver.wait(until.urlContains('/trip/'), 10000);
|
||
|
|
|
||
|
|
// Verify trip detail page elements
|
||
|
|
await driver.wait(until.elementLocated(By.xpath(`//h1[contains(text(), "${testTrip.name}")]`)), 10000);
|
||
|
|
|
||
|
|
// Verify calendar slots were created (5 days: Feb 1-5)
|
||
|
|
const daySlots = await driver.findElements(By.xpath('//h3[contains(@class, "section-title") and contains(text(), "Day")]'));
|
||
|
|
expect(daySlots.length).toBeGreaterThanOrEqual(5); // Should have at least 5 day slots
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should add a plannable item', async () => {
|
||
|
|
// Login with existing user
|
||
|
|
await loginPage.login(testUser.email, testUser.password);
|
||
|
|
await driver.wait(until.urlContains('/'), 10000);
|
||
|
|
|
||
|
|
// Navigate to the trip detail page
|
||
|
|
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);
|
||
|
|
|
||
|
|
// 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")]'));
|
||
|
|
await submitButton.click();
|
||
|
|
|
||
|
|
// Wait for modal to close and item to appear
|
||
|
|
await driver.wait(until.stalenessOf(driver.findElement(By.className('plannable-form-overlay'))), 5000);
|
||
|
|
|
||
|
|
// 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 () => {
|
||
|
|
// Login with existing user
|
||
|
|
await loginPage.login(testUser.email, testUser.password);
|
||
|
|
await driver.wait(until.urlContains('/'), 10000);
|
||
|
|
|
||
|
|
// Navigate to the trip detail page
|
||
|
|
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);
|
||
|
|
|
||
|
|
// 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 updateButton.click();
|
||
|
|
|
||
|
|
// Wait for modal to close
|
||
|
|
await driver.wait(until.stalenessOf(driver.findElement(By.className('plannable-form-overlay'))), 5000);
|
||
|
|
|
||
|
|
// 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 () => {
|
||
|
|
// Login with existing user
|
||
|
|
await loginPage.login(testUser.email, testUser.password);
|
||
|
|
await driver.wait(until.urlContains('/'), 10000);
|
||
|
|
|
||
|
|
// Navigate to the trip detail page
|
||
|
|
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);
|
||
|
|
|
||
|
|
// 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();
|
||
|
|
|
||
|
|
// Accept confirmation dialog
|
||
|
|
await driver.wait(until.alertIsPresent(), 5000);
|
||
|
|
const alert = await driver.switchTo().alert();
|
||
|
|
await alert.accept();
|
||
|
|
|
||
|
|
// Verify the item is removed
|
||
|
|
await driver.sleep(1000); // Give time for the item to be 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 () => {
|
||
|
|
// Login with existing user
|
||
|
|
await loginPage.login(testUser.email, testUser.password);
|
||
|
|
await driver.wait(until.urlContains('/'), 10000);
|
||
|
|
|
||
|
|
// Navigate to the trip detail page
|
||
|
|
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);
|
||
|
|
|
||
|
|
// 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
|
||
|
|
const submitButton = await driver.findElement(By.xpath('//button[contains(text(), "Add Item")]'));
|
||
|
|
await submitButton.click();
|
||
|
|
|
||
|
|
// Wait for modal to close
|
||
|
|
await driver.wait(until.stalenessOf(driver.findElement(By.className('plannable-form-overlay'))), 5000);
|
||
|
|
|
||
|
|
// 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('🎯');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|