import { test, expect } from '@playwright/test'; import { closeOnboardingModal, navigateToDate } from '../helpers/navigation.js'; import { drawSelectionRectangle } from '../helpers/selection.js'; /** * Side Panel (Visits Drawer) Tests * * Tests for the side panel that displays visits when selection tool is used. * The panel can be toggled via the drawer button and shows suggested/confirmed visits * with options to confirm, decline, or merge them. */ test.describe('Side Panel', () => { test.beforeEach(async ({ page }) => { await page.goto('/map'); await closeOnboardingModal(page); // Wait for map to be fully loaded await page.waitForSelector('.leaflet-container', { state: 'visible', timeout: 10000 }); await page.waitForTimeout(2000); // Navigate to October 2024 (has demo data) await navigateToDate(page, '2024-10-01T00:00', '2024-10-31T23:59'); await page.waitForTimeout(2000); }); /** * Helper function to click the drawer button */ async function clickDrawerButton(page) { const drawerButton = page.locator('.drawer-button'); await expect(drawerButton).toBeVisible({ timeout: 5000 }); await drawerButton.click(); await page.waitForTimeout(500); // Wait for drawer animation } /** * Helper function to check if drawer is open */ async function isDrawerOpen(page) { const drawer = page.locator('#visits-drawer'); const exists = await drawer.count() > 0; if (!exists) return false; const hasOpenClass = await drawer.evaluate(el => el.classList.contains('open')); return hasOpenClass; } /** * Helper function to perform selection and wait for visits to load * This is a simplified version that doesn't use the shared helper * because we need custom waiting logic for the drawer */ async function selectAreaWithVisits(page) { // First, enable Suggested Visits layer to ensure visits are loaded const { enableLayer } = await import('../helpers/map.js'); await enableLayer(page, 'Suggested'); await page.waitForTimeout(1000); // Enable selection mode const selectionButton = page.locator('#selection-tool-button'); await selectionButton.click(); await page.waitForTimeout(500); // Get map bounds for drawing selection const map = page.locator('.leaflet-container'); const mapBox = await map.boundingBox(); // Calculate coordinates for drawing a large selection area // Make it much wider to catch visits - use most of the map area const startX = mapBox.x + 100; const startY = mapBox.y + 100; const endX = mapBox.x + mapBox.width - 400; // Leave room for drawer on right const endY = mapBox.y + mapBox.height - 100; // Draw selection rectangle await page.mouse.move(startX, startY); await page.mouse.down(); await page.mouse.move(endX, endY, { steps: 10 }); await page.mouse.up(); // Wait for drawer to be created and opened await page.waitForSelector('#visits-drawer.open', { timeout: 10000 }); await page.waitForTimeout(3000); // Wait longer for visits API response } test('should open and close drawer panel via button click', async ({ page }) => { // Verify drawer is initially closed const initiallyOpen = await isDrawerOpen(page); expect(initiallyOpen).toBe(false); // Click to open await clickDrawerButton(page); // Verify drawer is now open let drawerOpen = await isDrawerOpen(page); expect(drawerOpen).toBe(true); // Verify drawer content is visible const drawerContent = page.locator('#visits-drawer .drawer'); await expect(drawerContent).toBeVisible(); // Click to close await clickDrawerButton(page); // Verify drawer is now closed drawerOpen = await isDrawerOpen(page); expect(drawerOpen).toBe(false); }); test('should show visits in panel after selection', async ({ page }) => { await selectAreaWithVisits(page); // Verify drawer is open const drawerOpen = await isDrawerOpen(page); expect(drawerOpen).toBe(true); // Verify visits list container exists const visitsList = page.locator('#visits-list'); await expect(visitsList).toBeVisible(); // Wait for API response - check if we have visit items or "no visits" message await page.waitForTimeout(2000); // Check what content is actually shown const visitItems = page.locator('.visit-item'); const visitCount = await visitItems.count(); const noVisitsMessage = page.locator('#visits-list p.text-gray-500'); // Either we have visits OR we have a "no visits" message (not "Loading...") if (visitCount > 0) { // We have visits - verify the title shows count const drawerTitle = page.locator('#visits-drawer .drawer h2'); const titleText = await drawerTitle.textContent(); expect(titleText).toMatch(/\d+ visits? found/); } else { // No visits found - verify we show the appropriate message // Should NOT still be showing "Loading visits..." const messageText = await noVisitsMessage.textContent(); expect(messageText).not.toContain('Loading visits'); expect(messageText).toContain('No visits'); } }); test('should display visit details in panel', async ({ page }) => { await selectAreaWithVisits(page); // Open the visits collapsible section const visitsSection = page.locator('#visits-section-collapse'); await expect(visitsSection).toBeVisible(); const visitsSummary = visitsSection.locator('summary'); await visitsSummary.click(); await page.waitForTimeout(500); // Check if we have any visits const visitCount = await page.locator('.visit-item').count(); if (visitCount === 0) { console.log('Test skipped: No visits available in test data'); test.skip(); return; } // Get first visit item const firstVisit = page.locator('.visit-item').first(); await expect(firstVisit).toBeVisible(); // Verify visit has required information const visitName = firstVisit.locator('.font-semibold'); await expect(visitName).toBeVisible(); const nameText = await visitName.textContent(); expect(nameText.length).toBeGreaterThan(0); // Verify time information is present const timeInfo = firstVisit.locator('.text-sm.text-gray-600'); await expect(timeInfo).toBeVisible(); // Check if this is a suggested visit (has confirm/decline buttons) const hasSuggestedButtons = (await firstVisit.locator('.confirm-visit').count()) > 0; if (hasSuggestedButtons) { // For suggested visits, verify action buttons are present const confirmButton = firstVisit.locator('.confirm-visit'); const declineButton = firstVisit.locator('.decline-visit'); await expect(confirmButton).toBeVisible(); await expect(declineButton).toBeVisible(); expect(await confirmButton.textContent()).toBe('Confirm'); expect(await declineButton.textContent()).toBe('Decline'); } }); test('should confirm individual suggested visit from panel', async ({ page }) => { await selectAreaWithVisits(page); // Open the visits collapsible section const visitsSection = page.locator('#visits-section-collapse'); await expect(visitsSection).toBeVisible(); const visitsSummary = visitsSection.locator('summary'); await visitsSummary.click(); await page.waitForTimeout(500); // Find a suggested visit (one with confirm/decline buttons) const suggestedVisit = page.locator('.visit-item').filter({ has: page.locator('.confirm-visit') }).first(); // Check if any suggested visits exist const suggestedCount = await page.locator('.visit-item').filter({ has: page.locator('.confirm-visit') }).count(); if (suggestedCount === 0) { console.log('Test skipped: No suggested visits available'); test.skip(); return; } await expect(suggestedVisit).toBeVisible(); // Verify it has the suggested visit styling (dashed border) const hasDashedBorder = await suggestedVisit.evaluate(el => el.classList.contains('border-dashed') ); expect(hasDashedBorder).toBe(true); // Get initial count of visits const initialVisitCount = await page.locator('.visit-item').count(); // Click confirm button const confirmButton = suggestedVisit.locator('.confirm-visit'); await confirmButton.click(); // Wait for API call and UI update await page.waitForTimeout(2000); // Verify flash message appears const flashMessage = page.locator('.flash-message'); await expect(flashMessage).toBeVisible({ timeout: 5000 }); // The visit should still be in the list but without confirm/decline buttons // Or the count might decrease if it was removed from suggested visits const finalVisitCount = await page.locator('.visit-item').count(); expect(finalVisitCount).toBeLessThanOrEqual(initialVisitCount); }); test('should decline individual suggested visit from panel', async ({ page }) => { await selectAreaWithVisits(page); // Open the visits collapsible section const visitsSection = page.locator('#visits-section-collapse'); await expect(visitsSection).toBeVisible(); const visitsSummary = visitsSection.locator('summary'); await visitsSummary.click(); await page.waitForTimeout(500); // Find a suggested visit const suggestedVisit = page.locator('.visit-item').filter({ has: page.locator('.decline-visit') }).first(); const suggestedCount = await page.locator('.visit-item').filter({ has: page.locator('.decline-visit') }).count(); if (suggestedCount === 0) { console.log('Test skipped: No suggested visits available'); test.skip(); return; } await expect(suggestedVisit).toBeVisible(); // Get initial count const initialVisitCount = await page.locator('.visit-item').count(); // Click decline button const declineButton = suggestedVisit.locator('.decline-visit'); await declineButton.click(); // Wait for API call and UI update await page.waitForTimeout(2000); // Verify flash message const flashMessage = page.locator('.flash-message'); await expect(flashMessage).toBeVisible({ timeout: 5000 }); // Visit should be removed from the list const finalVisitCount = await page.locator('.visit-item').count(); expect(finalVisitCount).toBeLessThan(initialVisitCount); }); test('should show checkboxes on hover for mass selection', async ({ page }) => { await selectAreaWithVisits(page); // Open the visits collapsible section const visitsSection = page.locator('#visits-section-collapse'); await expect(visitsSection).toBeVisible(); const visitsSummary = visitsSection.locator('summary'); await visitsSummary.click(); await page.waitForTimeout(500); // Check if we have any visits const visitCount = await page.locator('.visit-item').count(); if (visitCount === 0) { console.log('Test skipped: No visits available in test data'); test.skip(); return; } const firstVisit = page.locator('.visit-item').first(); await expect(firstVisit).toBeVisible(); // Initially, checkbox should be hidden const checkboxContainer = firstVisit.locator('.visit-checkbox-container'); let opacity = await checkboxContainer.evaluate(el => el.style.opacity); expect(opacity === '0' || opacity === '').toBe(true); // Hover over the visit item await firstVisit.hover(); await page.waitForTimeout(300); // Checkbox should now be visible opacity = await checkboxContainer.evaluate(el => el.style.opacity); expect(opacity).toBe('1'); // Checkbox should be clickable const pointerEvents = await checkboxContainer.evaluate(el => el.style.pointerEvents); expect(pointerEvents).toBe('auto'); }); test('should select multiple visits and show bulk action buttons', async ({ page }) => { await selectAreaWithVisits(page); // Open the visits collapsible section const visitsSection = page.locator('#visits-section-collapse'); await expect(visitsSection).toBeVisible(); const visitsSummary = visitsSection.locator('summary'); await visitsSummary.click(); await page.waitForTimeout(500); // Verify we have at least 2 visits const visitCount = await page.locator('.visit-item').count(); if (visitCount < 2) { console.log('Test skipped: Need at least 2 visits'); test.skip(); return; } // Select first visit by hovering and clicking checkbox const firstVisit = page.locator('.visit-item').first(); await firstVisit.hover(); await page.waitForTimeout(300); const firstCheckbox = firstVisit.locator('.visit-checkbox'); await firstCheckbox.click(); await page.waitForTimeout(500); // Select second visit const secondVisit = page.locator('.visit-item').nth(1); await secondVisit.hover(); await page.waitForTimeout(300); const secondCheckbox = secondVisit.locator('.visit-checkbox'); await secondCheckbox.click(); await page.waitForTimeout(500); // Verify bulk action buttons appear const bulkActionsContainer = page.locator('.visit-bulk-actions'); await expect(bulkActionsContainer).toBeVisible(); // Verify all three action buttons are present const mergeButton = bulkActionsContainer.locator('button').filter({ hasText: 'Merge' }); const confirmButton = bulkActionsContainer.locator('button').filter({ hasText: 'Confirm' }); const declineButton = bulkActionsContainer.locator('button').filter({ hasText: 'Decline' }); await expect(mergeButton).toBeVisible(); await expect(confirmButton).toBeVisible(); await expect(declineButton).toBeVisible(); // Verify selection count text const selectionText = bulkActionsContainer.locator('.text-sm.text-center'); const selectionTextContent = await selectionText.textContent(); expect(selectionTextContent).toContain('2 visits selected'); // Verify cancel button exists const cancelButton = bulkActionsContainer.locator('button').filter({ hasText: 'Cancel Selection' }); await expect(cancelButton).toBeVisible(); }); test('should cancel mass selection', async ({ page }) => { await selectAreaWithVisits(page); // Open the visits collapsible section const visitsSection = page.locator('#visits-section-collapse'); await expect(visitsSection).toBeVisible(); const visitsSummary = visitsSection.locator('summary'); await visitsSummary.click(); await page.waitForTimeout(500); const visitCount = await page.locator('.visit-item').count(); if (visitCount < 2) { console.log('Test skipped: Need at least 2 visits'); test.skip(); return; } // Select two visits const firstVisit = page.locator('.visit-item').first(); await firstVisit.hover(); await page.waitForTimeout(300); await firstVisit.locator('.visit-checkbox').click(); await page.waitForTimeout(500); const secondVisit = page.locator('.visit-item').nth(1); await secondVisit.hover(); await page.waitForTimeout(300); await secondVisit.locator('.visit-checkbox').click(); await page.waitForTimeout(500); // Verify bulk actions are visible const bulkActions = page.locator('.visit-bulk-actions'); await expect(bulkActions).toBeVisible(); // Click cancel button const cancelButton = bulkActions.locator('button').filter({ hasText: 'Cancel Selection' }); await cancelButton.click(); await page.waitForTimeout(500); // Verify bulk actions are removed await expect(bulkActions).not.toBeVisible(); // Verify checkboxes are unchecked const checkedCheckboxes = await page.locator('.visit-checkbox:checked').count(); expect(checkedCheckboxes).toBe(0); }); test('should mass confirm multiple visits', async ({ page }) => { await selectAreaWithVisits(page); // Open the visits collapsible section const visitsSection = page.locator('#visits-section-collapse'); await expect(visitsSection).toBeVisible(); const visitsSummary = visitsSection.locator('summary'); await visitsSummary.click(); await page.waitForTimeout(500); // Find suggested visits (those with confirm buttons) const suggestedVisits = page.locator('.visit-item').filter({ has: page.locator('.confirm-visit') }); const suggestedCount = await suggestedVisits.count(); if (suggestedCount < 2) { console.log('Test skipped: Need at least 2 suggested visits'); test.skip(); return; } // Get initial count const initialVisitCount = await page.locator('.visit-item').count(); // Select first two suggested visits const firstSuggested = suggestedVisits.first(); await firstSuggested.hover(); await page.waitForTimeout(300); await firstSuggested.locator('.visit-checkbox').click(); await page.waitForTimeout(500); const secondSuggested = suggestedVisits.nth(1); await secondSuggested.hover(); await page.waitForTimeout(300); await secondSuggested.locator('.visit-checkbox').click(); await page.waitForTimeout(500); // Click mass confirm button const bulkActions = page.locator('.visit-bulk-actions'); const confirmButton = bulkActions.locator('button').filter({ hasText: 'Confirm' }); await confirmButton.click(); // Wait for API call await page.waitForTimeout(2000); // Verify flash message const flashMessage = page.locator('.flash-message'); await expect(flashMessage).toBeVisible({ timeout: 5000 }); // The visits might be removed or updated in the list // At minimum, bulk actions should be removed const bulkActionsVisible = await bulkActions.isVisible().catch(() => false); expect(bulkActionsVisible).toBe(false); }); test('should mass decline multiple visits', async ({ page }) => { await selectAreaWithVisits(page); // Open the visits collapsible section const visitsSection = page.locator('#visits-section-collapse'); await expect(visitsSection).toBeVisible(); const visitsSummary = visitsSection.locator('summary'); await visitsSummary.click(); await page.waitForTimeout(500); const suggestedVisits = page.locator('.visit-item').filter({ has: page.locator('.decline-visit') }); const suggestedCount = await suggestedVisits.count(); if (suggestedCount < 2) { console.log('Test skipped: Need at least 2 suggested visits'); test.skip(); return; } // Get initial count const initialVisitCount = await page.locator('.visit-item').count(); // Select two visits const firstSuggested = suggestedVisits.first(); await firstSuggested.hover(); await page.waitForTimeout(300); await firstSuggested.locator('.visit-checkbox').click(); await page.waitForTimeout(500); const secondSuggested = suggestedVisits.nth(1); await secondSuggested.hover(); await page.waitForTimeout(300); await secondSuggested.locator('.visit-checkbox').click(); await page.waitForTimeout(500); // Click mass decline button const bulkActions = page.locator('.visit-bulk-actions'); const declineButton = bulkActions.locator('button').filter({ hasText: 'Decline' }); await declineButton.click(); // Wait for API call await page.waitForTimeout(2000); // Verify flash message const flashMessage = page.locator('.flash-message'); await expect(flashMessage).toBeVisible({ timeout: 5000 }); // Visits should be removed from the list const finalVisitCount = await page.locator('.visit-item').count(); expect(finalVisitCount).toBeLessThan(initialVisitCount); }); test('should mass merge multiple visits', async ({ page }) => { await selectAreaWithVisits(page); // Open the visits collapsible section const visitsSection = page.locator('#visits-section-collapse'); // Check if visits section is visible, if not, no visits were found const hasVisitsSection = await visitsSection.isVisible().catch(() => false); if (!hasVisitsSection) { console.log('Test skipped: No visits found in selection area'); test.skip(); return; } await expect(visitsSection).toBeVisible(); const visitsSummary = visitsSection.locator('summary'); await visitsSummary.click(); await page.waitForTimeout(500); const visitCount = await page.locator('.visit-item').count(); if (visitCount < 2) { console.log('Test skipped: Need at least 2 visits'); test.skip(); return; } // Select two visits const firstVisit = page.locator('.visit-item').first(); await firstVisit.hover(); await page.waitForTimeout(300); await firstVisit.locator('.visit-checkbox').click(); await page.waitForTimeout(500); const secondVisit = page.locator('.visit-item').nth(1); await secondVisit.hover(); await page.waitForTimeout(300); await secondVisit.locator('.visit-checkbox').click(); await page.waitForTimeout(500); // Click merge button const bulkActions = page.locator('.visit-bulk-actions'); const mergeButton = bulkActions.locator('button').filter({ hasText: 'Merge' }); await mergeButton.click(); // Wait for API call await page.waitForTimeout(2000); // Verify flash message appears const flashMessage = page.locator('.flash-message'); await expect(flashMessage).toBeVisible({ timeout: 5000 }); // After merge, the visits should be combined into one // So final count should be less than initial const finalVisitCount = await page.locator('.visit-item').count(); expect(finalVisitCount).toBeLessThan(visitCount); }); test('should open and close panel without shifting controls', async ({ page }) => { // Get the layer control element const layerControl = page.locator('.leaflet-control-layers'); await expect(layerControl).toBeVisible(); // Get initial position of the control const initialBox = await layerControl.boundingBox(); // Open the drawer await clickDrawerButton(page); await page.waitForTimeout(500); // Verify drawer is open const drawerOpen = await isDrawerOpen(page); expect(drawerOpen).toBe(true); // Get position after opening - should be the same (no shifting) const afterOpenBox = await layerControl.boundingBox(); expect(afterOpenBox.x).toBe(initialBox.x); expect(afterOpenBox.y).toBe(initialBox.y); // Close the drawer await clickDrawerButton(page); await page.waitForTimeout(500); // Verify drawer is closed const drawerClosed = await isDrawerOpen(page); expect(drawerClosed).toBe(false); // Get final position - should still be the same const afterCloseBox = await layerControl.boundingBox(); expect(afterCloseBox.x).toBe(initialBox.x); expect(afterCloseBox.y).toBe(initialBox.y); }); });