Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions e2e/calendar.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3022,4 +3022,137 @@ test.describe('FreeShard WebCalendar E2E Suite', () => {
}
});
});

test.describe('Issue #163 — errors are announced and a failed delete stays on screen', () => {
// page.route needs the service worker off.
test.use({ serviceWorkers: 'block' });

const failWith = (message, status = 500) => route =>
route.fulfill({ status, contentType: 'application/json', body: JSON.stringify({ error: message }) });

test('a failed event save is announced as an alert in the event dialog', async ({ page }) => {
await page.route('**/api/events', route =>
route.request().method() === 'POST' ? failWith('Speichern auf dem Server fehlgeschlagen')(route) : route.continue());
await page.goto('/?date=2028-12-28');
await expect(page.locator('#app')).not.toHaveClass(/hidden/, { timeout: 10000 });

await page.click('#new-event-btn');
await expect(page.locator('#event-modal-backdrop')).not.toHaveClass(/hidden/);
await page.fill('#ev-title', `E2E 163 ${Date.now()}`);
await page.fill('#ev-start-date', '2028-12-28');
await page.fill('#ev-end-date', '2028-12-28');
await page.check('#ev-allday');
await page.click('#event-save-btn');

const alert = page.locator('#event-modal').getByRole('alert');
await expect(alert).toHaveText('Speichern auf dem Server fehlgeschlagen');
await expect(alert).toHaveAttribute('id', 'event-form-error');
});

test('a failed account add is announced as an alert in the account dialog', async ({ page }) => {
await page.route('**/api/accounts', route =>
route.request().method() === 'POST' ? failWith('Server nicht erreichbar', 502)(route) : route.continue());
await page.goto('/');
await expect(page.locator('#app')).not.toHaveClass(/hidden/, { timeout: 10000 });

await page.click('#sidebar-settings-btn');
await expect(page.locator('#settings-drawer')).not.toHaveClass(/hidden/);
await page.click('#settings-add-account-btn');
await expect(page.locator('#account-modal-backdrop')).not.toHaveClass(/hidden/);
await page.fill('#acc-url', 'https://caldav.example.com/');
await page.fill('#acc-username', 'testuser');
await page.fill('#acc-password', 'not-a-real-password');
await page.click('#account-save-btn');

const alert = page.locator('#account-modal').getByRole('alert');
await expect(alert).toHaveText('Server nicht erreichbar');
await expect(alert).toHaveAttribute('id', 'account-form-error');
});

test('a failed calendar create is announced as an alert in the add-calendar dialog', async ({ page }) => {
await page.route('**/api/calendars', route =>
route.request().method() === 'POST' ? failWith('Kalender konnte nicht angelegt werden')(route) : route.continue());
await page.goto('/');
await expect(page.locator('#app')).not.toHaveClass(/hidden/, { timeout: 10000 });

await page.click('#add-calendar-btn');
await page.fill('#add-cal-name', `E2E 163 ${Date.now()}`);
await page.click('#add-cal-submit');

const alert = page.locator('#add-cal-dialog-backdrop').getByRole('alert');
await expect(alert).toHaveText(/Kalender konnte nicht angelegt werden/);
await expect(alert).toHaveAttribute('id', 'add-cal-error');
});

test('every inline form error box is an alert region', async ({ page }) => {
await page.goto('/');
await expect(page.locator('#app')).not.toHaveClass(/hidden/, { timeout: 10000 });
for (const id of ['onboarding-error', 'event-form-error', 'account-form-error', 'webcal-form-error']) {
await expect(page.locator(`#${id}`), id).toHaveAttribute('role', 'alert');
}
await page.locator('#calendar-list [data-edit-cal]').first().click();
await expect(page.locator('#edit-cal-error')).toHaveAttribute('role', 'alert');
});

test('a failed delete from the popover leaves an assertive error that outlives four seconds and closes from the keyboard', async ({ page }) => {
await page.goto('/?date=2028-12-28');
await expect(page.locator('#app')).not.toHaveClass(/hidden/, { timeout: 10000 });

const title = `E2E 163 Löschfehler ${Date.now()}`;
await page.click('#new-event-btn');
await page.fill('#ev-title', title);
await page.fill('#ev-start-date', '2028-12-28');
await page.fill('#ev-end-date', '2028-12-28');
await page.check('#ev-allday');
await page.click('#event-save-btn');
await expect(page.locator('#event-modal-backdrop')).toHaveClass(/hidden/, { timeout: 10000 });
const chip = page.locator('.event-chip').filter({ hasText: title }).first();
await expect(chip).toBeVisible({ timeout: 15000 });

const failDelete = route =>
route.request().method() === 'DELETE' ? failWith('Termin ist gesperrt')(route) : route.continue();
await page.route('**/api/events/**', failDelete);
try {
await chip.click();
await page.click('#popover-delete-btn');
await page.click('#confirm-dialog-ok');

const toast = page.locator('#toast-container').getByRole('alert').filter({ hasText: 'Termin ist gesperrt' });
await expect(toast).toBeVisible();
await expect(toast).toHaveAttribute('aria-live', 'assertive');
await page.waitForTimeout(4600);
await expect(toast).toBeVisible();
await expect(chip).toBeVisible();

const close = toast.getByRole('button', { name: 'Meldung schließen' });
await close.focus();
await expect(close).toBeFocused();
await page.keyboard.press('Enter');
await expect(toast).toHaveCount(0);
} finally {
await page.unroute('**/api/events/**', failDelete);
await page.keyboard.press('Escape');
await chip.click();
await page.click('#popover-delete-btn');
await page.click('#confirm-dialog-ok');
await expect(chip).not.toBeVisible();
}
});

test('a failed calendar delete in settings names the failure once', async ({ page }) => {
await page.route('**/api/calendars/**', route =>
route.request().method() === 'DELETE' ? failWith('Kalender ist gesperrt')(route) : route.continue());
await page.goto('/');
await expect(page.locator('#app')).not.toHaveClass(/hidden/, { timeout: 10000 });

await page.click('#sidebar-settings-btn');
await expect(page.locator('#settings-drawer')).not.toHaveClass(/hidden/);
await page.locator('[data-delete-cal-settings]').first().click();
await page.click('#confirm-dialog-ok');

const toast = page.locator('#toast-container').getByRole('alert');
await expect(toast).toHaveText(/Kalender ist gesperrt/);
await expect(page.locator('#status-bar')).toHaveText('Fehler: Kalender ist gesperrt');
});
});
});
8 changes: 4 additions & 4 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ <h1 class="onboarding-title">CalDAV-Konto verbinden</h1>
autocomplete="off" />
</div>

<div id="onboarding-error" class="form-error hidden"></div>
<div id="onboarding-error" class="form-error hidden" role="alert"></div>

<button type="submit" class="btn btn-primary btn-full" id="onboarding-submit">
<span class="btn-label">Verbinden</span>
Expand Down Expand Up @@ -720,7 +720,7 @@ <h3 class="modal-title" id="event-modal-title">Neuer Termin</h3>
<ul class="attendee-list" id="ev-attendee-list"></ul>
</div>

<div id="event-form-error" class="form-error hidden"></div>
<div id="event-form-error" class="form-error hidden" role="alert"></div>

<div class="modal-actions">
<button type="button" class="btn btn-danger" id="delete-event-btn" style="display:none">
Expand Down Expand Up @@ -811,7 +811,7 @@ <h3 class="modal-title" id="account-modal-title">Account hinzufügen</h3>
<label class="form-label" for="acc-displayname">Anzeigename <span class="optional">(optional)</span></label>
<input class="form-input" id="acc-displayname" type="text" name="displayName" />
</div>
<div id="account-form-error" class="form-error hidden"></div>
<div id="account-form-error" class="form-error hidden" role="alert"></div>
<div class="modal-actions">
<div class="modal-actions-right">
<button type="button" class="btn btn-ghost" id="account-cancel-btn">Abbrechen</button>
Expand Down Expand Up @@ -862,7 +862,7 @@ <h3 class="modal-title" id="webcal-modal-title">Webcal-Abonnement hinzufügen</h
</select>
</div>
</div>
<div id="webcal-form-error" class="form-error hidden"></div>
<div id="webcal-form-error" class="form-error hidden" role="alert"></div>
<div class="modal-actions" style="margin-top: 16px;">
<div class="modal-actions-right">
<button type="button" class="btn btn-ghost" id="webcal-cancel-btn">Abbrechen</button>
Expand Down
10 changes: 5 additions & 5 deletions public/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
import { store, toggleCalendar, invalidateEventCache, cacheEvents, getCachedEvents, setCalendarSyncCache, getCalendarSyncCache } from './store.js';
import { AccountsAPI, CalendarsAPI, EventsAPI, TasksAPI, WebcalAPI } from './api.js';
import { renderView, getToolbarTitle, navigate, getDateRange, renderMiniCalendar, sanitizeColor } from './calendar.js';
import { initEventModal, openCreateModal, openEditModal, closeModal, chooseRecurrenceScope } from './eventModal.js';

Check warning on line 9 in public/js/app.js

View workflow job for this annotation

GitHub Actions / lint

'closeModal' is defined but never used
import { initAccountModal, openAccountModal } from './accountModal.js';
import { initNotificationService, checkAndTriggerAlarms, requestNotificationPermission } from './notifications.js';
import { initTaskUI, openTaskModal, renderKanbanBoard, renderSidebarTasks } from './tasks.js';

Check warning on line 12 in public/js/app.js

View workflow job for this annotation

GitHub Actions / lint

'openTaskModal' is defined but never used
import { initInvitations } from './invitations.js';
import { initAvailabilityManager } from './availability.js';
import { initMSGraphModal, openMSGraphModal } from './msgraph.js';

Check warning on line 15 in public/js/app.js

View workflow job for this annotation

GitHub Actions / lint

'openMSGraphModal' is defined but never used
import { initDialogFocusManager, openDialog, closeDialog, confirmDialog, handleEscapeKey } from './dialogFocus.js';

const { initTheme, setTheme, getStoredTheme } = (typeof window !== 'undefined' && window.ThemeManager) ? window.ThemeManager : {};
Expand Down Expand Up @@ -987,7 +987,7 @@
value="${cal.color || '#007080'}"
style="height:40px;padding:4px 8px;cursor:pointer;" />
</div>
<div id="edit-cal-error" class="form-error hidden"></div>
<div id="edit-cal-error" class="form-error hidden" role="alert"></div>
<div class="modal-actions">
<div class="modal-actions-right">
<button type="button" class="btn btn-ghost" id="edit-cal-cancel">Abbrechen</button>
Expand Down Expand Up @@ -1367,7 +1367,7 @@
showToast('Freigabe entfernt.', 'info');
loadShares();
} catch (e) {
showToast('Fehler: ' + e.message, 'error');
showToast(e.message, 'error');
btn.disabled = false;
}
});
Expand Down Expand Up @@ -1795,7 +1795,7 @@
renderSidebar();
renderCalendarView();
} catch (err) {
showToast('Fehler: ' + err.message, 'error');
showToast(err.message, 'error');
}
});
});
Expand Down Expand Up @@ -1847,7 +1847,7 @@
<select class="form-select" id="add-cal-account">${accountOptions}</select>
</div>
</div>
<div id="add-cal-error" class="form-error hidden"></div>
<div id="add-cal-error" class="form-error hidden" role="alert"></div>
<div class="modal-actions">
<div class="modal-actions-right">
<button type="button" class="btn btn-ghost" id="add-cal-cancel">Abbrechen</button>
Expand Down Expand Up @@ -2549,7 +2549,7 @@
toast.innerHTML = `
<svg class="toast-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">${icons[type] || icons.info}</svg>
<span class="toast-msg">${escHtml(message)}</span>
<span class="toast-close">×</span>`;
<button type="button" class="toast-close" aria-label="Meldung schließen">×</button>`;

container.appendChild(toast);
toast.querySelector('.toast-close').addEventListener('click', () => toast.remove());
Expand Down
45 changes: 0 additions & 45 deletions server/test/issue163_error_announcements.test.js

This file was deleted.

Loading