Files
calendar/frontend/e2e/helpers/calendar-helpers.ts
Connor Johnstone 7d00a2dadb
Some checks failed
Integration Tests / e2e-tests (push) Failing after 4s
Integration Tests / unit-tests (push) Failing after 1m1s
Implement comprehensive frontend integration testing with Playwright
- Add Playwright E2E testing framework with cross-browser support (Chrome, Firefox)
- Create authentication helpers for CalDAV server integration
- Implement calendar interaction helpers with event creation, drag-and-drop, and view switching
- Add comprehensive drag-and-drop test suite with event cleanup
- Configure CI/CD integration with Gitea Actions for headless testing
- Support both local development and CI environments with proper dependency management
- Include video recording, screenshots, and HTML reporting for test debugging
- Handle Firefox-specific timing and interaction challenges with force clicks and timeouts

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-08 11:54:40 -04:00

278 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<Locator> {
// 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();
}
}