diff --git a/e2e/faircalendar.spec.js b/e2e/faircalendar.spec.js new file mode 100644 index 00000000..a02302a4 --- /dev/null +++ b/e2e/faircalendar.spec.js @@ -0,0 +1,59 @@ +// @ts-check +import { test, expect } from '@playwright/test'; + +test('auth required', async ({ page }) => { + await page.goto('/app/faircalendar'); + await page.waitForURL(url => url.pathname.startsWith('/login')); +}); + +test.describe('authenticated', () => { + test.use({ storageState: 'playwright/.auth/johnDoe.json' }); + + test('basic content', async ({ page }) => { + await page.goto('/app/faircalendar?year=2023&month=10'); // month=0..11 + await expect(page).toHaveTitle('FairCalendar novembre 2023 - Permacoop'); + + const calendar = page.getByTestId('pc-event-calendar'); + await expect(calendar).toBeVisible(); + + const toussaint = calendar.getByText('7h - Jour férié'); + await expect(toussaint).toBeVisible(); + }); + + test('go to add event', async ({ page }) => { + await page.goto('/app/faircalendar?year=2023&month=10'); + + const calendar = page.getByTestId('pc-event-calendar'); + + await calendar + .locator('.ec-body .ec-day') + .nth(8) + .click(); + + await page.waitForURL( + '/app/faircalendar/events/add/2023-11-09--2023-11-09' + ); + }); + + test('back button behavior', async ({ page }) => { + await page.goto('/app/faircalendar?year=2023&month=10'); + + const calendar = page.getByTestId('pc-event-calendar'); + + // Ensure rendered calendar is present + expect(await calendar.locator('.ec').count()).toBe(1); + + await page.goto('/app'); + await page.goBack(); + + // Regression test: ensure calendar is rendered without duplication duplicated, and is interactive + expect(await calendar.locator('.ec').count()).toBe(1); + await calendar + .locator('.ec-body .ec-day') + .nth(8) + .click(); + await page.waitForURL( + '/app/faircalendar/events/add/2023-11-09--2023-11-09' + ); + }); +}); diff --git a/src/assets/customElements/eventCalendar.js b/src/assets/customElements/eventCalendar.js index 77cad3b1..5fc1684b 100644 --- a/src/assets/customElements/eventCalendar.js +++ b/src/assets/customElements/eventCalendar.js @@ -1,24 +1,53 @@ +// @ts-check +import * as Turbo from '@hotwired/turbo'; import Calendar from '@event-calendar/core'; import DayGrid from '@event-calendar/day-grid'; import Interaction from '@event-calendar/interaction'; import { format, subDays } from 'date-fns'; export default class extends HTMLElement { + /** @type {Calendar} */ + #ec; + + /** @type{string} */ + #addUrlTemplate; + connectedCallback() { - this.classList.add('pc-eventcalendar'); + this.dataset.testid = 'pc-event-calendar'; - const events = JSON.parse(this.dataset.eventsJson); + const events = JSON.parse(this.dataset.eventsJson || '{}'); const addUrlTemplate = this.dataset.addUrlTemplate; + const date = this.dataset.date || new Date('now').toISOString(); + + if (!addUrlTemplate) { + throw new Error('data-add-url-template is missing'); + } + + this.#addUrlTemplate = addUrlTemplate; + this.#ec = this.#createCalendar(date, events); + + document.addEventListener('turbo:before-cache', this.#onTurboBeforeCache); + } - const goToEventCreate = (startDate, endDate) => { - const url = addUrlTemplate - .replace(':startDate', format(startDate, 'yyyy-MM-dd')) - .replace(':endDate', format(endDate, 'yyyy-MM-dd')); + /** + * @param {Date} startDate + * @param {Date} endDate + */ + #goToEventCreate = (startDate, endDate) => { + const url = this.#addUrlTemplate + .replace(':startDate', format(startDate, 'yyyy-MM-dd')) + .replace(':endDate', format(endDate, 'yyyy-MM-dd')); - window.location = url; - }; + Turbo.visit(url); + }; - const ec = new Calendar({ + /** + * @param {string} date + * @param {any[]} events + * @returns {Calendar} + */ + #createCalendar = (date, events) => { + return new Calendar({ target: this, props: { plugins: [DayGrid, Interaction], @@ -30,30 +59,42 @@ export default class extends HTMLElement { 6, // Saturday 0 // Sunday ], - date: this.dataset.date, + date, headerToolbar: { start: '', center: '', end: '' }, dayMaxEvents: true, eventStartEditable: false, eventDurationEditable: false, + // TODO: add testid on days eventContent: ({ event }) => { const url = event.extendedProps.url; if (url) { - return { html: `${event.title}` }; + return { + html: `${event.title}` + }; } return event.title; }, - dateClick: info => goToEventCreate(info.date, info.date), + dateClick: ({ event, date }) => { + this.#goToEventCreate(date, date); + }, selectable: true, selectBackgroundColor: 'var(--background-action-violet)', - select: info => { - // By default, range will stay selected if navigating using the - // back button. - ec.unselect(); - - goToEventCreate(info.start, subDays(info.end, 1)); + select: ({ start, end }) => { + this.#goToEventCreate(start, subDays(end, 1)); } } } }); + }; + + #onTurboBeforeCache = () => { + this.#ec.destroy(); + }; + + disconnectedCallback() { + document.removeEventListener( + 'turbo:before-cache', + this.#onTurboBeforeCache + ); } } diff --git a/src/assets/styles/faircalendar.css b/src/assets/styles/faircalendar.css index 3354cf3b..b0b2d77a 100644 --- a/src/assets/styles/faircalendar.css +++ b/src/assets/styles/faircalendar.css @@ -2,7 +2,7 @@ /* EventCalendar customizations */ -.pc-eventcalendar { +pc-eventcalendar { display: block; } @@ -14,7 +14,7 @@ background-color: var(--event-today); } -.pc-eventcalendar .ec-day-grid .ec-uniform .ec-day { +pc-eventcalendar .ec-day-grid .ec-uniform .ec-day { /* Fix: by default EventCalendar sets the min-height of day boxes to 0 when dayMaxEvents is true. */ min-height: 5rem; } diff --git a/src/templates/pages/faircalendar/index.njk b/src/templates/pages/faircalendar/index.njk index 16bbbac2..0c46cb7a 100644 --- a/src/templates/pages/faircalendar/index.njk +++ b/src/templates/pages/faircalendar/index.njk @@ -32,7 +32,8 @@ data-events-json="{{ fullCalendarEvents|dump }}" data-add-url-template="{{ path('faircalendar_events_add', { startDate: ':startDate', endDate: ':endDate' }) }}" data-date="{{ date|htmlDate }}" - > + > +