import { Page, expect, Locator, TestInfo } from '@playwright/test'; export class CalendarHelpers { constructor(private page: Page) {} async waitForCalendarLoad() { // Wait for calendar to be visible (either month view grid or week view container) await expect( this.page.locator('.calendar-grid').or(this.page.locator('.week-view-container')) ).toBeVisible({ timeout: 5000 }); // Wait for any loading spinners to disappear (optional, may not exist) try { await this.page.waitForSelector('.calendar-loading', { state: 'hidden', timeout: 2000 }); } catch { // Loading spinner might not exist, that's ok } } async switchToWeekView() { await this.page.selectOption('.view-selector-dropdown', 'week'); await this.waitForCalendarLoad(); } async switchToMonthView() { await this.page.selectOption('.view-selector-dropdown', 'month'); await this.waitForCalendarLoad(); } async getCurrentView(): Promise<'week' | 'month'> { const selectedValue = await this.page.locator('.view-selector-dropdown').inputValue(); return selectedValue as 'week' | 'month'; } async navigateToNextPeriod() { await this.page.getByRole('button', { name: '›' }).click(); await this.waitForCalendarLoad(); } async navigateToPreviousPeriod() { await this.page.getByRole('button', { name: '‹' }).click(); await this.waitForCalendarLoad(); } async navigateToToday() { await this.page.getByRole('button', { name: 'Today' }).click(); await this.waitForCalendarLoad(); } async openCreateEventModal(timeSlot?: string) { // Check if we're in week view or month view to determine creation method const isWeekView = await this.page.locator('.week-view-container').count() > 0; if (isWeekView) { // In week view, try element-based drag first (simpler and more reliable) const timeSlots = this.page.locator('.time-slot'); if (await timeSlots.count() > 1) { const startSlot = timeSlots.first(); const endSlot = timeSlots.nth(2); // Skip one to create a longer event // Force the drag await startSlot.dragTo(endSlot, { force: true }); // Wait for the create event modal to appear await expect(this.page.locator('.create-event-modal, .modal')).toBeVisible({ timeout: 5000 }); } else { throw new Error('Not enough time slots available for drag-to-create event'); } } else { // In month view, try right-clicking on calendar day const calendarDay = this.page.locator('.calendar-day').first(); if (await calendarDay.count() > 0) { await calendarDay.click({ button: 'right' }); // Wait for context menu to appear await expect(this.page.locator('.context-menu, .calendar-context-menu')).toBeVisible({ timeout: 3000 }); // Click "Create Event" in the context menu const createEventOption = this.page.getByText(/create event/i); await expect(createEventOption).toBeVisible(); await createEventOption.click(); // Wait for the create event modal to appear await expect(this.page.locator('.create-event-modal, .modal')).toBeVisible({ timeout: 5000 }); } else { throw new Error('Could not find calendar day for event creation in month view'); } } } async createEvent(eventData: { title: string; startTime?: string; endTime?: string; description?: string; calendar?: string; }, testInfo?: TestInfo) { // Open modal by clicking on calendar (using default 14:00 time slot if available) await this.openCreateEventModal(eventData.startTime || '14:00'); // Fill in event details - use exact label match const titleInput = this.page.getByLabel('Event Title *').or(this.page.locator('#event-title')); await titleInput.fill(eventData.title); const titleScreenshot = await this.page.screenshot({ fullPage: true }); if (testInfo) await testInfo.attach('debug-after-title-fill.png', { body: titleScreenshot, contentType: 'image/png' }); if (eventData.startTime) { const startTimeInput = this.page.locator('#start-time'); await startTimeInput.fill(eventData.startTime); const startTimeScreenshot = await this.page.screenshot({ fullPage: true }); if (testInfo) await testInfo.attach('debug-after-start-time-fill.png', { body: startTimeScreenshot, contentType: 'image/png' }); } if (eventData.endTime) { const endTimeInput = this.page.locator('#end-time'); await endTimeInput.fill(eventData.endTime); const endTimeScreenshot = await this.page.screenshot({ fullPage: true }); if (testInfo) await testInfo.attach('debug-after-end-time-fill.png', { body: endTimeScreenshot, contentType: 'image/png' }); } if (eventData.description) { const descInput = this.page.getByLabel('Description').or(this.page.getByLabel('Notes')); await descInput.fill(eventData.description); } if (eventData.calendar) { await this.page.selectOption('[name="calendar"], .calendar-select', eventData.calendar); } // Submit the form const submitButton = this.page.getByRole('button', { name: 'Create Event' }) .or(this.page.getByRole('button', { name: 'Save' })) .or(this.page.getByRole('button', { name: 'Create' })); // Take screenshot before clicking submit button const beforeSubmitScreenshot = await this.page.screenshot({ fullPage: true }); if (testInfo) await testInfo.attach('debug-before-submit-click.png', { body: beforeSubmitScreenshot, contentType: 'image/png' }); // Wait for button to be visible and click with force (since normal click times out) await expect(submitButton).toBeVisible({ timeout: 5000 }); await this.page.waitForTimeout(500); // Give form time to stabilize await submitButton.click({ force: true }); // Wait for modal to close await expect(this.page.locator('.create-event-modal, .modal')).toBeHidden({ timeout: 10000 }); // Take screenshot after modal closes const afterCloseScreenshot = await this.page.screenshot({ fullPage: true }); if (testInfo) await testInfo.attach('debug-after-modal-close.png', { body: afterCloseScreenshot, contentType: 'image/png' }); // Wait for calendar to update await this.page.waitForTimeout(2000); await this.waitForCalendarLoad(); // Refresh the calendar data if needed try { const refreshButton = this.page.locator('button').filter({ hasText: /refresh|reload/i }); if (await refreshButton.count() > 0) { await refreshButton.click(); await this.waitForCalendarLoad(); } } catch { // Refresh button doesn't exist, that's ok } } async getEventByTitle(title: string): Promise { // Check current view to use appropriate selector const isWeekView = await this.page.locator('.week-view-container').count() > 0; if (isWeekView) { // In week view, events use .week-event class, and text is in .event-title return this.page.locator('.week-event').filter({ hasText: title }); } else { // In month view, check for different possible selectors return this.page.locator('.month-event, .calendar-event').filter({ hasText: title }); } } async dragEventToNewTime(eventTitle: string, targetTimeSlot: string) { const event = await this.getEventByTitle(eventTitle); const target = this.page.locator(`[data-time="${targetTimeSlot}"]`); await event.dragTo(target); await this.waitForCalendarLoad(); } async openCalendarManagementModal() { await this.page.getByRole('button', { name: '+ Add Calendar' }).click(); await expect(this.page.locator('.calendar-management-modal')).toBeVisible(); } async createCalendar(calendarData: { name: string; color?: string; type: 'caldav' | 'external'; url?: string; }) { await this.openCalendarManagementModal(); // Switch to appropriate tab if (calendarData.type === 'caldav') { await this.page.locator('.tab-button', { hasText: 'Create Calendar' }).click(); } else { await this.page.locator('.tab-button', { hasText: 'Add External' }).click(); } // Fill in calendar details await this.page.getByLabel('Calendar Name').fill(calendarData.name); if (calendarData.url) { await this.page.getByLabel('Calendar URL').fill(calendarData.url); } if (calendarData.color) { // Click color picker and select color await this.page.locator('.color-picker').getByTestId(calendarData.color).click(); } // Submit the form const buttonText = calendarData.type === 'caldav' ? 'Create Calendar' : 'Add External Calendar'; await this.page.getByRole('button', { name: buttonText }).click(); // Wait for modal to close await expect(this.page.locator('.calendar-management-modal')).toBeHidden(); } async toggleCalendarVisibility(calendarName: string) { // Look for the calendar in regular calendar list first, then external const regularCalendarItem = this.page.locator('.calendar-list li').filter({ hasText: calendarName }); const externalCalendarItem = this.page.locator('.external-calendar-item').filter({ hasText: calendarName }); if (await regularCalendarItem.count() > 0) { await regularCalendarItem.locator('input[type="checkbox"]').click(); } else if (await externalCalendarItem.count() > 0) { await externalCalendarItem.locator('input[type="checkbox"]').click(); } await this.waitForCalendarLoad(); } async changeTheme(theme: string) { await this.page.selectOption('.theme-selector-dropdown', theme); // Wait for theme to apply await this.page.waitForTimeout(500); } async changeStyle(style: string) { await this.page.selectOption('.style-selector-dropdown', style); // Wait for style to apply await this.page.waitForTimeout(500); } async deleteEvent(eventTitle: string) { const event = await this.getEventByTitle(eventTitle); await expect(event).toBeVisible({ timeout: 5000 }); // Right-click to open context menu await event.click({ button: 'right' }); // Wait for context menu and click delete const deleteOption = this.page.getByText(/delete|remove/i); await expect(deleteOption).toBeVisible({ timeout: 3000 }); await deleteOption.click(); // Confirm deletion if there's a confirmation dialog const confirmButton = this.page.getByRole('button', { name: /delete|confirm|yes/i }); if (await confirmButton.count() > 0) { await confirmButton.click(); } // Wait for the event to be removed await expect(event).toBeHidden({ timeout: 5000 }); await this.waitForCalendarLoad(); } }