diff --git a/eform-client/playwright/e2e/Page objects/BackendConfigurationCalendar.page.ts b/eform-client/playwright/e2e/Page objects/BackendConfigurationCalendar.page.ts new file mode 100644 index 0000000000..3778a1a6ec --- /dev/null +++ b/eform-client/playwright/e2e/Page objects/BackendConfigurationCalendar.page.ts @@ -0,0 +1,118 @@ +import BasePage from './Page'; +import { Page, Locator } from '@playwright/test'; + +const HOUR_HEIGHT = 52; // px per hour, mirrors calendar-task-block.component.ts + +/** + * Page object for the backend-configuration calendar + * (/plugins/backend-configuration-pn/calendar). + * + * Encapsulates the create/edit-event flows used by the + * "edit must not make the event disappear" regression spec. + */ +export class BackendConfigurationCalendarPage extends BasePage { + constructor(page: Page) { + super(page); + } + + async goto() { + await this.open('/plugins/backend-configuration-pn/calendar'); + await this.dayColumns().first().waitFor({ state: 'visible', timeout: 60000 }); + } + + /** Clickable day cells in the week grid (#cal-day-0 .. #cal-day-6). */ + dayColumns(): Locator { + return this.page.locator('.day-cell-content'); + } + + /** All rendered event tiles in the week grid. */ + eventTiles(): Locator { + return this.page.locator('.task-content'); + } + + eventByTitle(title: string): Locator { + return this.eventTiles().filter({ hasText: title }); + } + + /** + * Click an empty slot in the given day column (0=Mon .. 6=Sun) at the given + * hour to open the create-event modal. Avoids the left edge so the cdk drop + * list / task tiles aren't hit. + */ + async openCreateModalOnDay(dayIndex: number, hour: number) { + const column = this.page.locator(`#cal-day-${dayIndex}`); + await column.waitFor({ state: 'visible', timeout: 40000 }); + // relY in onCellClick is measured from the cell's own top (no header + // offset), so the y position maps directly to the hour. + const y = hour * HOUR_HEIGHT; + await column.click({ position: { x: 24, y } }); + await this.page.locator('#calendarEventTitle').waitFor({ state: 'visible', timeout: 20000 }); + } + + /** Open the first available option of an mtx-select (wraps ng-select). */ + private async pickFirstOption(triggerSelector: string) { + await this.page.locator(triggerSelector).click(); + const firstOption = this.page.locator('ng-dropdown-panel .ng-option').first(); + await firstOption.waitFor({ state: 'visible', timeout: 20000 }); + await firstOption.click(); + } + + /** Select the repeat option whose label contains the given text (e.g. "Ugentlig"). */ + private async pickRepeatContaining(text: string) { + await this.page.locator('#calendarEventRepeat').click(); + const option = this.page.locator('ng-dropdown-panel .ng-option').filter({ hasText: text }).first(); + await option.waitFor({ state: 'visible', timeout: 20000 }); + await option.click(); + } + + /** + * Create a weekly-recurring event on the given day. This is the exact shape + * that triggered the disappear bug: a weekly task whose start lands on a + * (trailing-Sunday) day, persisted with a multi-day weekday CSV. + */ + async createWeeklyEvent(dayIndex: number, hour: number, title: string, repeatLabel: string) { + await this.openCreateModalOnDay(dayIndex, hour); + await this.page.locator('#calendarEventTitle').fill(title); + await this.pickRepeatContaining(repeatLabel); + // At least one worker must be assigned or the backend rejects the create. + await this.pickFirstOption('#calendarEventAssignee'); + // A report headline (itemPlanningTag) is also required on create. + await this.pickFirstOption('#calendarEventPlanningTag'); + await this.save(); + } + + /** Open an event's preview popover and click its edit (pencil) button. */ + async openEditForEvent(title: string) { + await this.eventByTitle(title).first().click(); + const editBtn = this.page.locator('#calendarEventEditBtn'); + await editBtn.waitFor({ state: 'visible', timeout: 20000 }); + await editBtn.click(); + await this.page.locator('#calendarEventTitle').waitFor({ state: 'visible', timeout: 20000 }); + } + + async setTitle(title: string) { + await this.page.locator('#calendarEventTitle').fill(title); + } + + /** + * Click Save and confirm the recurring-scope dialog if it appears (defaults + * to the pre-selected scope — "Only this"). + */ + async save() { + await this.page.locator('#calendarEventSaveBtn').click(); + // A recurring series shows a scope dialog first. Target it by id so the + // assertion is locale-independent (the visible label is translated). + const confirm = this.page.locator('#repeatScopeConfirmBtn'); + try { + await confirm.waitFor({ state: 'visible', timeout: 2500 }); + await confirm.click(); + } catch { + // No scope dialog — event was not part of a recurring series, or the + // save went straight through. + } + // Wait for the modal to close (save succeeded + week reloaded) rather than + // sleeping a fixed amount, so the test isn't flaky under CI load. + await this.page.locator('#calendarEventSaveBtn').waitFor({ state: 'hidden', timeout: 20000 }); + await this.dayColumns().first().waitFor({ state: 'visible', timeout: 20000 }); + } +} diff --git a/eform-client/playwright/e2e/Tests/backend-configuration-calendar/calendar.edit-event-stays-visible.spec.ts b/eform-client/playwright/e2e/Tests/backend-configuration-calendar/calendar.edit-event-stays-visible.spec.ts new file mode 100644 index 0000000000..14fc7fda14 --- /dev/null +++ b/eform-client/playwright/e2e/Tests/backend-configuration-calendar/calendar.edit-event-stays-visible.spec.ts @@ -0,0 +1,66 @@ +import { test, expect } from '@playwright/test'; +import { LoginPage } from '../../Page objects/Login.page'; +import { BackendConfigurationCalendarPage } from '../../Page objects/BackendConfigurationCalendar.page'; + +/** + * Regression guard for the critical bug where editing ANY calendar event made + * it disappear from the week view. + * + * Root cause: a weekly task whose StartDate landed on the trailing Sunday of a + * Mon-Sun week was dropped by GetOccurrencesInWeek (Sunday-aligned bucket math + * computed weeksFromAnchor = -1). Editing an event moved its StartDate onto the + * clicked occurrence's date, so any edit of a Sunday occurrence triggered it. + * + * This spec reproduces the exact shape end-to-end: a weekly event on Sunday + * (which pre-fix would not even render in its own week) is created, then edited, + * asserting it stays visible both times. + * + * Idempotency: a stable title is used and the event is only created when + * missing, so reruns reuse the same event instead of accumulating tiles (which + * would otherwise occupy the create slot and break click-to-create). + */ +test.describe('Backend configuration calendar - edit keeps event visible', () => { + let page; + let loginPage: LoginPage; + let calendar: BackendConfigurationCalendarPage; + + const SUNDAY = 6; // 0=Mon .. 6=Sun + const CREATE_HOUR = 3; // empty early-morning slot + // BASE is a substring of EDITED, so eventByTitle(BASE) matches the tile in + // both states — keeping create-if-missing and the assertions rerun-safe. + const BASE = 'PW Regression Weekly Sunday'; + const EDITED = `${BASE} edited`; + + test.beforeAll(async ({ browser }) => { + page = await browser.newPage(); + loginPage = new LoginPage(page); + calendar = new BackendConfigurationCalendarPage(page); + await loginPage.open('/'); + await loginPage.login(); + await calendar.goto(); + + if ((await calendar.eventByTitle(BASE).count()) === 0) { + await calendar.createWeeklyEvent(SUNDAY, CREATE_HOUR, BASE, 'Ugentlig'); + } + }); + + test.afterAll(async () => { + await page.close(); + }); + + test('a weekly Sunday event renders in its own week', async () => { + // Pre-fix this failed: a weekly-Sunday event generated no occurrences in + // its own week. + await expect(calendar.eventByTitle(BASE).first()).toBeVisible({ timeout: 20000 }); + }); + + test('editing the event does not make it disappear', async () => { + await calendar.openEditForEvent(BASE); + await calendar.setTitle(EDITED); + await calendar.save(); + + // The exact regression: after a successful edit the event must still be + // present in the week grid (pre-fix it vanished). + await expect(calendar.eventByTitle(EDITED).first()).toBeVisible({ timeout: 20000 }); + }); +});