fix(calendar): improve integration initialization and fallback to legacy mode

- Fixed calendar_scripts.html to wait for DOM ready before initialization
- Added retry logic if widget not found on first attempt
- Changed integration fallback: show ALL events when no calendars configured
- Changed behavior: if no calendars selected, show all events (legacy mode)
- This ensures backward compatibility with existing events

User feedback: Integration wasn't initializing on page load, button '+' didn't work.
Solution: Auto-init with retry + always show events (no empty state).
This commit is contained in:
admin i 2026-02-11 11:26:25 +03:00
parent 6a99acf7e4
commit f8a8b3b2f2
6 changed files with 1780 additions and 15 deletions

View File

@ -0,0 +1,207 @@
# 🔍 Команды для отладки календаря через консоль браузера
Откройте консоль браузера (F12 → Console) и выполните эти команды:
## 1️⃣ Проверить наличие интеграции
```javascript
// Должно вернуть объект, а не undefined
window.calendarIntegration
```
**Ожидаемый результат:**
```javascript
{
fetchEventsForVisibleCalendars: ƒ,
getVisibleCalendarIds: ƒ,
getCalendars: ƒ,
setVisibleCalendars: ƒ,
refresh: ƒ,
instances: {manager: {...}, modal: {...}, widget: {...}}
}
```
**Если undefined** → интеграция не инициализировалась!
---
## 2️⃣ Проверить виджет календаря
```javascript
window.calendarWidget
```
**Должен вернуть объект FullCalendar**
---
## 3️⃣ Проверить список календарей
```javascript
// Если интеграция работает:
window.calendarIntegration.getCalendars()
// Должно вернуть массив календарей:
// [{id: 1, title: "Personal", color: "#0d6efd", ...}, ...]
```
---
## 4️⃣ Проверить видимые календари
```javascript
window.calendarIntegration.getVisibleCalendarIds()
// Должно вернуть массив ID: [1, 2, 3]
```
---
## 5️⃣ Вручную загрузить события
```javascript
// Попробовать загрузить события для видимых календарей
const start = new Date('2026-02-01');
const end = new Date('2026-02-28');
window.calendarIntegration.fetchEventsForVisibleCalendars(start, end)
.then(events => {
console.log('✅ Events loaded:', events.length);
console.table(events);
})
.catch(err => {
console.error('❌ Error:', err);
});
```
---
## 6️⃣ Проверить, загружен ли скрипт интеграции
```javascript
// Проверить, есть ли в DOM скрипт
document.querySelector('script[src*="calendarWidgetIntegration"]')
// Должен вернуть <script> элемент, если файл подключен
```
---
## 7️⃣ Вручную инициализировать интеграцию
Если интеграция не загрузилась автоматически, попробуйте вручную:
```javascript
// 1. Импортировать модуль (если ES6 modules работают)
import('/static/js/components/calendarWidgetIntegration.js')
.then(module => {
const { integrateCalendarManager } = module;
const integration = integrateCalendarManager(window.calendarWidget);
window.calendarIntegration = integration;
console.log('✅ Integration manually initialized');
})
.catch(err => console.error('❌ Import failed:', err));
```
---
## 8️⃣ Перезагрузить события календаря
```javascript
// После того как интеграция инициализирована
if (window.calendarWidget && window.calendarWidget.refetchEvents) {
window.calendarWidget.refetchEvents();
console.log('🔄 Events refetched');
}
```
---
## 9️⃣ Проверить контейнер списка календарей
```javascript
// Проверить, существует ли контейнер
const container = document.getElementById('calendarListContainer');
console.log('Container exists:', !!container);
console.log('Container HTML:', container?.innerHTML.substring(0, 200));
```
---
## 🔟 Проверить модальное окно
```javascript
// Проверить, есть ли модальное окно
const modal = document.getElementById('calendarManageModal');
console.log('Modal exists:', !!modal);
// Попробовать открыть модальное окно
if (window.calendarIntegration?.instances?.modal) {
window.calendarIntegration.instances.modal.openForCreate();
}
```
---
## 🐛 Диагностика проблемы
Выполните все команды по порядку и скопируйте результаты. Это поможет понять, на каком этапе происходит сбой:
```javascript
console.log('=== Calendar Integration Debug ===');
console.log('1. calendarIntegration:', typeof window.calendarIntegration);
console.log('2. calendarWidget:', typeof window.calendarWidget);
console.log('3. Container:', !!document.getElementById('calendarListContainer'));
console.log('4. Modal:', !!document.getElementById('calendarManageModal'));
console.log('5. Create button:', !!document.querySelector('[data-action="create-calendar"]'));
console.log('=================================');
```
---
## 🔧 Если интеграция undefined
Проверьте, загружается ли файл `calendar_scripts.html`:
```javascript
// Найти все script теги с calendar
Array.from(document.querySelectorAll('script[type="module"]'))
.filter(s => s.textContent.includes('calendar'))
.forEach(s => console.log('Found:', s.textContent.substring(0, 100)));
```
---
## 📊 Полный отчёт о состоянии
Скопируйте и выполните весь блок:
```javascript
(function debugCalendar() {
const report = {
integration: typeof window.calendarIntegration,
widget: typeof window.calendarWidget,
container: !!document.getElementById('calendarListContainer'),
modal: !!document.getElementById('calendarManageModal'),
createButton: !!document.querySelector('[data-action="create-calendar"]'),
scripts: Array.from(document.querySelectorAll('script[type="module"]')).length,
errors: []
};
if (window.calendarIntegration) {
try {
report.calendars = window.calendarIntegration.getCalendars().length;
report.visibleIds = window.calendarIntegration.getVisibleCalendarIds();
} catch (e) {
report.errors.push('Error accessing integration: ' + e.message);
}
}
console.table(report);
return report;
})();
```
---
Выполните команды и покажите результаты! 🔍

View File

@ -0,0 +1,60 @@
# 🔧 Исправление проблемы инициализации calendar integration
## Проблема
`window.calendarIntegration` = undefined, но скрипт подключён.
## Быстрое решение - выполните в консоли:
```javascript
// Проверка наличия скрипта
const scripts = Array.from(document.querySelectorAll('script[type="module"]'));
console.log('Module scripts found:', scripts.length);
scripts.forEach((s, i) => {
if (s.textContent.includes('calendar')) {
console.log(`Script ${i}:`, s.textContent.substring(0, 200));
}
});
```
## Если скрипт найден, но не работает - вручную загрузите:
```javascript
(async function manualInit() {
try {
console.log('🔄 Manually loading calendar integration...');
// Импорт модуля
const module = await import('/static/js/components/calendarWidgetIntegration.js');
console.log('✅ Module loaded:', module);
// Инициализация
const { integrateCalendarManager } = module;
const integration = integrateCalendarManager(window.calendarWidget);
if (integration) {
window.calendarIntegration = integration;
console.log('✅ Integration initialized successfully!');
console.log('Calendars:', integration.getCalendars().length);
console.log('Visible:', integration.getVisibleCalendarIds());
// Перезагрузить события
if (window.calendarWidget.refetchEvents) {
window.calendarWidget.refetchEvents();
console.log('🔄 Events refetched with new integration');
}
}
} catch (error) {
console.error('❌ Manual init failed:', error);
}
})();
```
## После выполнения проверьте:
```javascript
// Должно вернуть объект, а не undefined
window.calendarIntegration
// Должно показать логи с новой системой
window.calendarWidget.refetchEvents()
```

View File

@ -41,19 +41,22 @@ export function integrateCalendarManager(calendarWidgetInstance, options = {}) {
range: `${startStr} - ${endStr}`
});
// Если нет календарей в новой системе, используем legacy режим
// Если нет календарей в новой системе, используем legacy режим (показываем ВСЕ события)
if (calendars.length === 0) {
console.log('[CalendarIntegration] Using legacy mode (no calendars configured)');
console.log('[CalendarIntegration] No calendars configured, using legacy mode (showing all events)');
return await getCalendarEvents({
start: startStr,
end: endStr
});
}
// Если ни один календарь не выбран, возвращаем пустой массив
// Если ни один календарь не выбран, ТОЖЕ показываем все события (legacy режим)
if (visibleCalendarIds.length === 0) {
console.log('[CalendarIntegration] No calendars selected, returning empty array');
return [];
console.log('[CalendarIntegration] No calendars selected, falling back to legacy mode');
return await getCalendarEvents({
start: startStr,
end: endStr
});
}
// Загружаем события для каждого видимого календаря

View File

@ -5,18 +5,35 @@
import { initCalendarWidget } from '{% static "js/components/calendarWidget.js" %}';
import { integrateCalendarManager } from '{% static "js/components/calendarWidgetIntegration.js" %}';
// Инициализация базового виджета календаря
const widget = initCalendarWidget();
if (widget) {
window.calendarWidget = widget;
// Функция инициализации интеграции
function initializeIntegration() {
// Проверяем, есть ли уже виджет
const widget = window.calendarWidget || initCalendarWidget();
// Интеграция с менеджером календарей
const integration = integrateCalendarManager(widget);
if (integration) {
window.calendarIntegration = integration;
console.log('[Calendar] Integration initialized with multiple calendars support');
if (widget) {
window.calendarWidget = widget;
// Интеграция с менеджером календарей
const integration = integrateCalendarManager(widget);
if (integration) {
window.calendarIntegration = integration;
console.log('[Calendar] Integration initialized with multiple calendars support');
} else {
console.warn('[Calendar] Integration failed to initialize');
}
} else {
console.warn('[Calendar] Widget not found, retrying in 500ms...');
setTimeout(initializeIntegration, 500);
}
}
// Инициализация после загрузки DOM
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeIntegration);
} else {
// DOM уже загружен
initializeIntegration();
}
</script>

View File

@ -0,0 +1,758 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Calendar Frontend Test</title>
<!-- Bootstrap CSS (offline fallback) -->
<style>
@import url('https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css');
@import url('https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css');
</style>
<!-- Inline styles as fallback -->
<style>
body {
padding: 20px;
background: #f8f9fa;
}
.test-section {
background: white;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.test-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 15px;
border-bottom: 2px solid #e9ecef;
}
.status-badge {
padding: 5px 10px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
}
.status-pending { background: #ffc107; color: #000; }
.status-success { background: #28a745; color: #fff; }
.status-error { background: #dc3545; color: #fff; }
.log-console {
background: #2d2d2d;
color: #f8f8f2;
padding: 15px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 13px;
max-height: 400px;
overflow-y: auto;
margin-top: 15px;
}
.log-entry {
margin: 3px 0;
padding: 2px 0;
}
.log-info { color: #66d9ef; }
.log-success { color: #a6e22e; }
.log-error { color: #f92672; }
.log-warn { color: #e6db74; }
#calendarContainer {
min-height: 600px;
background: white;
padding: 20px;
border-radius: 8px;
}
.calendar-list-container {
max-height: 400px;
overflow-y: auto;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 10px;
background: white;
}
.calendar-list-item {
padding: 10px;
margin-bottom: 8px;
border: 1px solid #e9ecef;
border-radius: 4px;
display: flex;
align-items: center;
gap: 10px;
transition: all 0.2s;
}
.calendar-list-item:hover {
background: #f8f9fa;
border-color: #0d6efd;
}
.calendar-color-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
border: 2px solid rgba(0,0,0,0.1);
flex-shrink: 0;
}
.calendar-list-item-info {
flex: 1;
min-width: 0;
}
.calendar-list-item-title {
font-weight: 600;
font-size: 14px;
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.calendar-list-item-meta {
font-size: 12px;
color: #6c757d;
margin: 2px 0 0 0;
}
.calendar-list-item-actions {
display: flex;
gap: 5px;
flex-shrink: 0;
}
</style>
</head>
<body>
<div class="container-fluid">
<h1 class="mb-4">🧪 Calendar Frontend Test Suite</h1>
<!-- Test Controls -->
<div class="test-section">
<div class="test-header">
<h3>🎮 Test Controls</h3>
<div>
<button id="runAllTests" class="btn btn-primary">
<i class="bi-play-fill"></i> Run All Tests
</button>
<button id="clearLogs" class="btn btn-secondary">
<i class="bi-trash"></i> Clear Logs
</button>
</div>
</div>
<div class="row g-3">
<div class="col-md-4">
<button id="testApiWrapper" class="btn btn-outline-primary w-100">
Test 1: API Wrapper
</button>
</div>
<div class="col-md-4">
<button id="testCalendarManager" class="btn btn-outline-primary w-100">
Test 2: Calendar Manager
</button>
</div>
<div class="col-md-4">
<button id="testModalComponent" class="btn btn-outline-primary w-100">
Test 3: Modal Component
</button>
</div>
<div class="col-md-4">
<button id="testWidgetIntegration" class="btn btn-outline-primary w-100">
Test 4: Widget Integration
</button>
</div>
<div class="col-md-4">
<button id="testEventFiltering" class="btn btn-outline-primary w-100">
Test 5: Event Filtering
</button>
</div>
<div class="col-md-4">
<button id="testUserInteractions" class="btn btn-outline-primary w-100">
Test 6: User Interactions
</button>
</div>
</div>
</div>
<!-- Test Results -->
<div class="test-section">
<div class="test-header">
<h3>📊 Test Results</h3>
<div id="testSummary">
<span class="badge bg-secondary">Ready</span>
</div>
</div>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Test</th>
<th>Status</th>
<th>Duration</th>
<th>Details</th>
</tr>
</thead>
<tbody id="testResultsTable">
<tr>
<td colspan="4" class="text-center text-muted">No tests run yet</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Calendar UI Demo -->
<div class="test-section">
<div class="test-header">
<h3>📅 Calendar UI Demo</h3>
<button id="btnCreateCalendar" class="btn btn-sm btn-outline-primary" data-action="create-calendar">
<i class="bi-plus-lg"></i> Create Calendar
</button>
</div>
<div class="row">
<div class="col-md-3">
<h5>My Calendars</h5>
<div id="calendarListContainer" class="calendar-list-container">
<div class="text-center text-muted py-3">
<div class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mb-0 mt-2">Loading calendars...</p>
</div>
</div>
</div>
<div class="col-md-9">
<div id="calendarContainer"></div>
</div>
</div>
</div>
<!-- Console Log -->
<div class="test-section">
<div class="test-header">
<h3>📝 Console Log</h3>
<button id="toggleConsole" class="btn btn-sm btn-outline-secondary">
<i class="bi-chevron-up"></i> Collapse
</button>
</div>
<div id="logConsole" class="log-console"></div>
</div>
</div>
<!-- Modal Template -->
<div class="modal fade" id="calendarManageModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Manage Calendar</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="calendarForm">
<div class="mb-3">
<label for="calendarTitle" class="form-label">Title</label>
<input type="text" class="form-control" id="calendarTitle" required>
</div>
<div class="mb-3">
<label for="calendarDescription" class="form-label">Description</label>
<textarea class="form-control" id="calendarDescription" rows="3"></textarea>
</div>
<div class="mb-3">
<label for="calendarColor" class="form-label">Color</label>
<input type="color" class="form-control form-control-color" id="calendarColor" value="#0d6efd">
</div>
<div class="mb-3">
<label class="form-label">Type</label>
<div>
<div class="form-check">
<input class="form-check-input" type="radio" name="calendarType" id="typePersonal" value="personal" checked>
<label class="form-check-label" for="typePersonal">Personal</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="calendarType" id="typePublic" value="public">
<label class="form-check-label" for="typePublic">Public</label>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="saveCalendarBtn">Save</button>
</div>
</div>
</div>
</div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- FullCalendar JS -->
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.8/index.global.min.js"></script>
<script>
// =========================
// Mock Data & API
// =========================
let mockCalendars = [
{ id: 1, title: 'Personal', description: 'My personal calendar', color: '#0d6efd', calendar_type: 'personal', user_can_edit: true, user_can_view: true, is_subscribed: true },
{ id: 2, title: 'Work', description: 'Work events', color: '#198754', calendar_type: 'public', user_can_edit: true, user_can_view: true, is_subscribed: true },
{ id: 3, title: 'Team Meetings', description: 'Team sync meetings', color: '#ffc107', calendar_type: 'department', user_can_edit: false, user_can_view: true, is_subscribed: true },
];
let mockEvents = [
{ id: 1, title: 'Morning Standup', start: new Date(2026, 1, 11, 9, 0), end: new Date(2026, 1, 11, 9, 30), color: '#0d6efd', calendar_id: 1 },
{ id: 2, title: 'Client Meeting', start: new Date(2026, 1, 11, 14, 0), end: new Date(2026, 1, 11, 15, 0), color: '#198754', calendar_id: 2 },
{ id: 3, title: 'Sprint Planning', start: new Date(2026, 1, 12, 10, 0), end: new Date(2026, 1, 12, 12, 0), color: '#ffc107', calendar_id: 3 },
{ id: 4, title: 'Code Review', start: new Date(2026, 1, 13, 15, 0), end: new Date(2026, 1, 13, 16, 0), color: '#0d6efd', calendar_id: 1 },
];
// Mock API
const mockAPI = {
getMyCalendars: async () => {
await delay(300);
log('API: getMyCalendars() → ' + mockCalendars.length + ' calendars', 'info');
return { ok: true, data: [...mockCalendars] };
},
createCalendar: async (data) => {
await delay(500);
const newCalendar = {
id: Date.now(),
...data,
user_can_edit: true,
user_can_view: true,
is_subscribed: true
};
mockCalendars.push(newCalendar);
log('API: createCalendar() → Calendar #' + newCalendar.id + ' created', 'success');
return { ok: true, data: newCalendar };
},
updateCalendar: async (id, data) => {
await delay(500);
const index = mockCalendars.findIndex(c => c.id === id);
if (index !== -1) {
mockCalendars[index] = { ...mockCalendars[index], ...data };
log('API: updateCalendar(' + id + ') → Updated', 'success');
return { ok: true, data: mockCalendars[index] };
}
return { ok: false, error: 'Calendar not found' };
},
deleteCalendar: async (id) => {
await delay(500);
mockCalendars = mockCalendars.filter(c => c.id !== id);
log('API: deleteCalendar(' + id + ') → Deleted', 'success');
return { ok: true };
},
getCalendarEvents: async (params) => {
await delay(300);
let events = [...mockEvents];
if (params.calendar_id) {
events = events.filter(e => e.calendar_id === params.calendar_id);
}
log('API: getCalendarEvents(' + JSON.stringify(params) + ') → ' + events.length + ' events', 'info');
return { ok: true, data: events };
}
};
// =========================
// Test Logger
// =========================
const logConsole = document.getElementById('logConsole');
const testResultsTable = document.getElementById('testResultsTable');
let testResults = [];
function log(message, type = 'info') {
const timestamp = new Date().toLocaleTimeString();
const entry = document.createElement('div');
entry.className = 'log-entry log-' + type;
entry.textContent = '[' + timestamp + '] ' + message;
logConsole.appendChild(entry);
logConsole.scrollTop = logConsole.scrollHeight;
// Also log to real console
console.log('[Test]', message);
}
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function updateTestResult(testName, status, duration, details) {
testResults.push({ testName, status, duration, details });
renderTestResults();
}
function renderTestResults() {
if (testResults.length === 0) {
testResultsTable.innerHTML = '<tr><td colspan="4" class="text-center text-muted">No tests run yet</td></tr>';
return;
}
testResultsTable.innerHTML = testResults.map(result => `
<tr>
<td>${result.testName}</td>
<td><span class="badge bg-${result.status === 'passed' ? 'success' : 'danger'}">${result.status}</span></td>
<td>${result.duration}ms</td>
<td class="text-muted small">${result.details}</td>
</tr>
`).join('');
const passed = testResults.filter(r => r.status === 'passed').length;
const failed = testResults.filter(r => r.status === 'failed').length;
document.getElementById('testSummary').innerHTML = `
<span class="badge bg-success">${passed} Passed</span>
<span class="badge bg-danger">${failed} Failed</span>
`;
}
// =========================
// Tests
// =========================
async function testApiWrapper() {
log('=== Test 1: API Wrapper ===', 'info');
const startTime = Date.now();
try {
// Test getMyCalendars
const calendars = await mockAPI.getMyCalendars();
if (!calendars.ok || !Array.isArray(calendars.data)) {
throw new Error('getMyCalendars failed');
}
log('✓ getMyCalendars works', 'success');
// Test createCalendar
const newCal = await mockAPI.createCalendar({
title: 'Test Calendar',
description: 'Test',
color: '#ff0000',
calendar_type: 'personal'
});
if (!newCal.ok || !newCal.data.id) {
throw new Error('createCalendar failed');
}
log('✓ createCalendar works', 'success');
// Test getCalendarEvents
const events = await mockAPI.getCalendarEvents({ calendar_id: 1 });
if (!events.ok || !Array.isArray(events.data)) {
throw new Error('getCalendarEvents failed');
}
log('✓ getCalendarEvents works', 'success');
const duration = Date.now() - startTime;
updateTestResult('API Wrapper', 'passed', duration, 'All API methods working');
log('✅ Test 1: PASSED', 'success');
} catch (error) {
const duration = Date.now() - startTime;
updateTestResult('API Wrapper', 'failed', duration, error.message);
log('❌ Test 1: FAILED - ' + error.message, 'error');
}
}
async function testCalendarManager() {
log('=== Test 2: Calendar Manager ===', 'info');
const startTime = Date.now();
try {
// Render calendar list
const calendars = await mockAPI.getMyCalendars();
const container = document.getElementById('calendarListContainer');
container.innerHTML = calendars.data.map(cal => `
<div class="calendar-list-item" data-calendar-id="${cal.id}">
<input type="checkbox" class="form-check-input" ${cal.is_subscribed ? 'checked' : ''}>
<div class="calendar-color-indicator" style="background-color: ${cal.color}"></div>
<div class="calendar-list-item-info">
<p class="calendar-list-item-title">${cal.title}</p>
<p class="calendar-list-item-meta">${cal.calendar_type}</p>
</div>
<div class="calendar-list-item-actions">
${cal.user_can_edit ? '<button class="btn btn-sm btn-outline-secondary"><i class="bi-pencil"></i></button>' : ''}
</div>
</div>
`).join('');
log('✓ Calendar list rendered', 'success');
// Test checkbox interaction
const firstCheckbox = container.querySelector('input[type="checkbox"]');
if (!firstCheckbox) {
throw new Error('Checkbox not found');
}
firstCheckbox.click();
log('✓ Checkbox interaction works', 'success');
const duration = Date.now() - startTime;
updateTestResult('Calendar Manager', 'passed', duration, 'List rendering and interaction OK');
log('✅ Test 2: PASSED', 'success');
} catch (error) {
const duration = Date.now() - startTime;
updateTestResult('Calendar Manager', 'failed', duration, error.message);
log('❌ Test 2: FAILED - ' + error.message, 'error');
}
}
async function testModalComponent() {
log('=== Test 3: Modal Component ===', 'info');
const startTime = Date.now();
try {
const modal = new bootstrap.Modal(document.getElementById('calendarManageModal'));
// Test modal open
modal.show();
await delay(500);
log('✓ Modal opens', 'success');
// Test form validation
const titleInput = document.getElementById('calendarTitle');
titleInput.value = 'Test Calendar';
if (titleInput.value !== 'Test Calendar') {
throw new Error('Form input failed');
}
log('✓ Form inputs work', 'success');
// Close modal
modal.hide();
await delay(500);
log('✓ Modal closes', 'success');
const duration = Date.now() - startTime;
updateTestResult('Modal Component', 'passed', duration, 'Modal and form working');
log('✅ Test 3: PASSED', 'success');
} catch (error) {
const duration = Date.now() - startTime;
updateTestResult('Modal Component', 'failed', duration, error.message);
log('❌ Test 3: FAILED - ' + error.message, 'error');
}
}
async function testWidgetIntegration() {
log('=== Test 4: Widget Integration ===', 'info');
const startTime = Date.now();
try {
// Initialize FullCalendar
const calendarEl = document.getElementById('calendarContainer');
const calendar = new FullCalendar.Calendar(calendarEl, {
initialView: 'dayGridMonth',
initialDate: '2026-02-11',
events: async (info, success, failure) => {
try {
const events = await mockAPI.getCalendarEvents({});
success(events.data);
} catch (e) {
failure(e);
}
}
});
calendar.render();
await delay(1000);
log('✓ FullCalendar initialized', 'success');
// Check if events loaded
const eventCount = calendar.getEvents().length;
if (eventCount === 0) {
throw new Error('No events loaded');
}
log('✓ Events loaded: ' + eventCount, 'success');
const duration = Date.now() - startTime;
updateTestResult('Widget Integration', 'passed', duration, eventCount + ' events loaded');
log('✅ Test 4: PASSED', 'success');
} catch (error) {
const duration = Date.now() - startTime;
updateTestResult('Widget Integration', 'failed', duration, error.message);
log('❌ Test 4: FAILED - ' + error.message, 'error');
}
}
async function testEventFiltering() {
log('=== Test 5: Event Filtering ===', 'info');
const startTime = Date.now();
try {
// Test filtering by calendar_id
const allEvents = await mockAPI.getCalendarEvents({});
const filteredEvents = await mockAPI.getCalendarEvents({ calendar_id: 1 });
if (filteredEvents.data.length >= allEvents.data.length) {
throw new Error('Filtering not working');
}
log('✓ Calendar filtering works', 'success');
log(' All events: ' + allEvents.data.length, 'info');
log(' Filtered (calendar 1): ' + filteredEvents.data.length, 'info');
const duration = Date.now() - startTime;
updateTestResult('Event Filtering', 'passed', duration, 'Filtering by calendar_id works');
log('✅ Test 5: PASSED', 'success');
} catch (error) {
const duration = Date.now() - startTime;
updateTestResult('Event Filtering', 'failed', duration, error.message);
log('❌ Test 5: FAILED - ' + error.message, 'error');
}
}
async function testUserInteractions() {
log('=== Test 6: User Interactions ===', 'info');
const startTime = Date.now();
try {
// Test create button
const createBtn = document.getElementById('btnCreateCalendar');
if (!createBtn) {
throw new Error('Create button not found');
}
log('✓ Create button exists', 'success');
// Test calendar list items
const calendarItems = document.querySelectorAll('.calendar-list-item');
if (calendarItems.length === 0) {
throw new Error('No calendar items found');
}
log('✓ Calendar items rendered: ' + calendarItems.length, 'success');
// Test checkboxes
const checkboxes = document.querySelectorAll('.calendar-list-item input[type="checkbox"]');
if (checkboxes.length === 0) {
throw new Error('No checkboxes found');
}
log('✓ Checkboxes present: ' + checkboxes.length, 'success');
const duration = Date.now() - startTime;
updateTestResult('User Interactions', 'passed', duration, 'All UI elements interactive');
log('✅ Test 6: PASSED', 'success');
} catch (error) {
const duration = Date.now() - startTime;
updateTestResult('User Interactions', 'failed', duration, error.message);
log('❌ Test 6: FAILED - ' + error.message, 'error');
}
}
// =========================
// Event Listeners
// =========================
document.getElementById('runAllTests').addEventListener('click', async () => {
testResults = [];
log('🚀 Running all tests...', 'info');
await testApiWrapper();
await delay(500);
await testCalendarManager();
await delay(500);
await testModalComponent();
await delay(500);
await testWidgetIntegration();
await delay(500);
await testEventFiltering();
await delay(500);
await testUserInteractions();
log('🏁 All tests completed!', 'success');
});
document.getElementById('clearLogs').addEventListener('click', () => {
logConsole.innerHTML = '';
log('Logs cleared', 'info');
});
document.getElementById('toggleConsole').addEventListener('click', () => {
const console = document.getElementById('logConsole');
const btn = document.getElementById('toggleConsole');
if (console.style.display === 'none') {
console.style.display = 'block';
btn.innerHTML = '<i class="bi-chevron-up"></i> Collapse';
} else {
console.style.display = 'none';
btn.innerHTML = '<i class="bi-chevron-down"></i> Expand';
}
});
document.getElementById('testApiWrapper').addEventListener('click', testApiWrapper);
document.getElementById('testCalendarManager').addEventListener('click', testCalendarManager);
document.getElementById('testModalComponent').addEventListener('click', testModalComponent);
document.getElementById('testWidgetIntegration').addEventListener('click', testWidgetIntegration);
document.getElementById('testEventFiltering').addEventListener('click', testEventFiltering);
document.getElementById('testUserInteractions').addEventListener('click', testUserInteractions);
// Modal save button
document.getElementById('saveCalendarBtn').addEventListener('click', async () => {
const title = document.getElementById('calendarTitle').value;
const description = document.getElementById('calendarDescription').value;
const color = document.getElementById('calendarColor').value;
const type = document.querySelector('input[name="calendarType"]:checked').value;
if (!title) {
alert('Title is required');
return;
}
log('Creating calendar: ' + title, 'info');
const result = await mockAPI.createCalendar({ title, description, color, calendar_type: type });
if (result.ok) {
log('Calendar created successfully!', 'success');
const modal = bootstrap.Modal.getInstance(document.getElementById('calendarManageModal'));
modal.hide();
// Refresh calendar list
await testCalendarManager();
}
});
// Create calendar button
document.getElementById('btnCreateCalendar').addEventListener('click', () => {
log('Opening create calendar modal', 'info');
const modal = new bootstrap.Modal(document.getElementById('calendarManageModal'));
// Reset form
document.getElementById('calendarTitle').value = '';
document.getElementById('calendarDescription').value = '';
document.getElementById('calendarColor').value = '#0d6efd';
document.getElementById('typePersonal').checked = true;
modal.show();
});
// =========================
// Initialize
// =========================
log('✅ Test suite initialized', 'success');
log('Click "Run All Tests" to start', 'info');
// Auto-load calendar list on startup (wrapped in async IIFE)
(async function init() {
try {
await testCalendarManager();
await testWidgetIntegration();
} catch (error) {
log('Initialization error: ' + error.message, 'error');
}
})();
</script>
</body>
</html>

View File

@ -0,0 +1,720 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Calendar Frontend Test - Simple Version</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
padding: 20px;
background: #f5f5f5;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
h1 {
color: #333;
margin-bottom: 20px;
}
.test-section {
background: white;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.test-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 15px;
border-bottom: 2px solid #e9ecef;
}
h2 {
font-size: 20px;
color: #333;
}
button {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.btn-primary {
background: #0d6efd;
color: white;
}
.btn-primary:hover {
background: #0b5ed7;
}
.btn-secondary {
background: #6c757d;
color: white;
margin-left: 10px;
}
.btn-secondary:hover {
background: #5c636a;
}
.btn-outline {
background: white;
border: 1px solid #dee2e6;
color: #333;
padding: 10px;
margin: 5px;
width: calc(33.33% - 10px);
display: inline-block;
}
.btn-outline:hover {
background: #f8f9fa;
border-color: #0d6efd;
}
.test-controls {
display: flex;
flex-wrap: wrap;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 10px;
text-align: left;
border-bottom: 1px solid #dee2e6;
}
th {
background: #f8f9fa;
font-weight: 600;
}
.badge {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
}
.badge-success {
background: #28a745;
color: white;
}
.badge-danger {
background: #dc3545;
color: white;
}
.badge-secondary {
background: #6c757d;
color: white;
}
.log-console {
background: #2d2d2d;
color: #f8f8f2;
padding: 15px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 13px;
max-height: 400px;
overflow-y: auto;
}
.log-entry {
margin: 3px 0;
padding: 2px 0;
}
.log-info { color: #66d9ef; }
.log-success { color: #a6e22e; }
.log-error { color: #f92672; }
.log-warn { color: #e6db74; }
.calendar-list {
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 10px;
max-height: 400px;
overflow-y: auto;
}
.calendar-item {
padding: 10px;
margin-bottom: 8px;
border: 1px solid #e9ecef;
border-radius: 4px;
display: flex;
align-items: center;
gap: 10px;
background: white;
}
.calendar-item:hover {
background: #f8f9fa;
border-color: #0d6efd;
}
.calendar-color {
width: 12px;
height: 12px;
border-radius: 50%;
border: 2px solid rgba(0,0,0,0.1);
}
.calendar-info {
flex: 1;
}
.calendar-title {
font-weight: 600;
font-size: 14px;
}
.calendar-meta {
font-size: 12px;
color: #6c757d;
}
.text-muted {
color: #6c757d;
}
.text-center {
text-align: center;
}
input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
}
</style>
</head>
<body>
<div class="container">
<h1>🧪 Calendar Frontend Test Suite (Offline Version)</h1>
<!-- Test Controls -->
<div class="test-section">
<div class="test-header">
<h2>🎮 Test Controls</h2>
<div>
<button id="runAllTests" class="btn-primary">▶ Run All Tests</button>
<button id="clearLogs" class="btn-secondary">🗑 Clear Logs</button>
</div>
</div>
<div class="test-controls">
<button class="btn-outline" onclick="testApiWrapper()">Test 1: API Wrapper</button>
<button class="btn-outline" onclick="testCalendarManager()">Test 2: Calendar Manager</button>
<button class="btn-outline" onclick="testEventFiltering()">Test 3: Event Filtering</button>
<button class="btn-outline" onclick="testStateManagement()">Test 4: State Management</button>
<button class="btn-outline" onclick="testCallbacks()">Test 5: Callbacks</button>
<button class="btn-outline" onclick="testDataStructures()">Test 6: Data Structures</button>
</div>
</div>
<!-- Test Results -->
<div class="test-section">
<div class="test-header">
<h2>📊 Test Results</h2>
<div id="testSummary">
<span class="badge badge-secondary">Ready</span>
</div>
</div>
<table>
<thead>
<tr>
<th>Test</th>
<th>Status</th>
<th>Duration</th>
<th>Details</th>
</tr>
</thead>
<tbody id="testResultsTable">
<tr>
<td colspan="4" class="text-center text-muted">No tests run yet</td>
</tr>
</tbody>
</table>
</div>
<!-- Calendar UI Demo -->
<div class="test-section">
<div class="test-header">
<h2>📅 Calendar List Demo</h2>
<button class="btn-primary" onclick="createCalendar()">➕ Create Calendar</button>
</div>
<div id="calendarListContainer" class="calendar-list">
<div class="text-center text-muted">Loading calendars...</div>
</div>
</div>
<!-- Console Log -->
<div class="test-section">
<div class="test-header">
<h2>📝 Console Log</h2>
<button class="btn-secondary" onclick="toggleConsole()">▲ Toggle</button>
</div>
<div id="logConsole" class="log-console"></div>
</div>
</div>
<script>
// =========================
// State Management
// =========================
let mockCalendars = [
{ id: 1, title: 'Personal', description: 'My personal calendar', color: '#0d6efd', calendar_type: 'personal', user_can_edit: true, user_can_view: true, is_subscribed: true, visible: true },
{ id: 2, title: 'Work', description: 'Work events', color: '#198754', calendar_type: 'public', user_can_edit: true, user_can_view: true, is_subscribed: true, visible: true },
{ id: 3, title: 'Team Meetings', description: 'Team sync', color: '#ffc107', calendar_type: 'department', user_can_edit: false, user_can_view: true, is_subscribed: true, visible: false },
];
let mockEvents = [
{ id: 1, title: 'Morning Standup', start: '2026-02-11T09:00', end: '2026-02-11T09:30', color: '#0d6efd', calendar_id: 1 },
{ id: 2, title: 'Client Meeting', start: '2026-02-11T14:00', end: '2026-02-11T15:00', color: '#198754', calendar_id: 2 },
{ id: 3, title: 'Sprint Planning', start: '2026-02-12T10:00', end: '2026-02-12T12:00', color: '#ffc107', calendar_id: 3 },
{ id: 4, title: 'Code Review', start: '2026-02-13T15:00', end: '2026-02-13T16:00', color: '#0d6efd', calendar_id: 1 },
];
let testResults = [];
let visibleCalendarIds = [1, 2];
// =========================
// Logger
// =========================
function log(message, type = 'info') {
const timestamp = new Date().toLocaleTimeString();
const logConsole = document.getElementById('logConsole');
const entry = document.createElement('div');
entry.className = 'log-entry log-' + type;
entry.textContent = '[' + timestamp + '] ' + message;
logConsole.appendChild(entry);
logConsole.scrollTop = logConsole.scrollHeight;
console.log('[Test]', message);
}
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function updateTestResult(testName, status, duration, details) {
testResults.push({ testName, status, duration, details });
renderTestResults();
}
function renderTestResults() {
const tbody = document.getElementById('testResultsTable');
if (testResults.length === 0) {
tbody.innerHTML = '<tr><td colspan="4" class="text-center text-muted">No tests run yet</td></tr>';
return;
}
tbody.innerHTML = testResults.map(r => `
<tr>
<td>${r.testName}</td>
<td><span class="badge badge-${r.status === 'passed' ? 'success' : 'danger'}">${r.status.toUpperCase()}</span></td>
<td>${r.duration}ms</td>
<td class="text-muted">${r.details}</td>
</tr>
`).join('');
const passed = testResults.filter(r => r.status === 'passed').length;
const failed = testResults.filter(r => r.status === 'failed').length;
document.getElementById('testSummary').innerHTML = `
<span class="badge badge-success">${passed} Passed</span>
<span class="badge badge-danger">${failed} Failed</span>
`;
}
// =========================
// Mock API
// =========================
async function mockApiGetCalendars() {
await delay(200);
return { ok: true, data: [...mockCalendars] };
}
async function mockApiGetEvents(calendarId = null) {
await delay(150);
let events = [...mockEvents];
if (calendarId) {
events = events.filter(e => e.calendar_id === calendarId);
}
return { ok: true, data: events };
}
async function mockApiCreateCalendar(data) {
await delay(300);
const newCal = {
id: Date.now(),
...data,
user_can_edit: true,
user_can_view: true,
is_subscribed: true,
visible: true
};
mockCalendars.push(newCal);
return { ok: true, data: newCal };
}
// =========================
// Tests
// =========================
async function testApiWrapper() {
log('=== Test 1: API Wrapper ===', 'info');
const startTime = Date.now();
try {
const calendars = await mockApiGetCalendars();
if (!calendars.ok || !Array.isArray(calendars.data)) {
throw new Error('getCalendars failed');
}
log('✓ mockApiGetCalendars() → ' + calendars.data.length + ' calendars', 'success');
const events = await mockApiGetEvents(1);
if (!events.ok || !Array.isArray(events.data)) {
throw new Error('getEvents failed');
}
log('✓ mockApiGetEvents(1) → ' + events.data.length + ' events', 'success');
const allEvents = await mockApiGetEvents();
log('✓ mockApiGetEvents() → ' + allEvents.data.length + ' total events', 'success');
const duration = Date.now() - startTime;
updateTestResult('API Wrapper', 'passed', duration, 'All API methods working');
log('✅ Test 1: PASSED', 'success');
} catch (error) {
const duration = Date.now() - startTime;
updateTestResult('API Wrapper', 'failed', duration, error.message);
log('❌ Test 1: FAILED - ' + error.message, 'error');
}
}
async function testCalendarManager() {
log('=== Test 2: Calendar Manager ===', 'info');
const startTime = Date.now();
try {
const calendars = await mockApiGetCalendars();
renderCalendarList(calendars.data);
const container = document.getElementById('calendarListContainer');
const items = container.querySelectorAll('.calendar-item');
if (items.length !== calendars.data.length) {
throw new Error('Calendar count mismatch: ' + items.length + ' != ' + calendars.data.length);
}
log('✓ Rendered ' + items.length + ' calendar items', 'success');
const checkboxes = container.querySelectorAll('input[type="checkbox"]');
if (checkboxes.length === 0) {
throw new Error('No checkboxes found');
}
log('✓ ' + checkboxes.length + ' checkboxes created', 'success');
const duration = Date.now() - startTime;
updateTestResult('Calendar Manager', 'passed', duration, 'List rendering OK');
log('✅ Test 2: PASSED', 'success');
} catch (error) {
const duration = Date.now() - startTime;
updateTestResult('Calendar Manager', 'failed', duration, error.message);
log('❌ Test 2: FAILED - ' + error.message, 'error');
}
}
async function testEventFiltering() {
log('=== Test 3: Event Filtering ===', 'info');
const startTime = Date.now();
try {
const allEvents = await mockApiGetEvents();
const filteredEvents = await mockApiGetEvents(1);
if (filteredEvents.data.length >= allEvents.data.length) {
throw new Error('Filtering not working correctly');
}
log('✓ All events: ' + allEvents.data.length, 'info');
log('✓ Calendar 1 events: ' + filteredEvents.data.length, 'info');
// Test visible calendar filtering
const visibleEvents = fetchEventsForVisibleCalendars(allEvents.data);
log('✓ Visible calendars: ' + visibleCalendarIds.join(', '), 'info');
log('✓ Visible events: ' + visibleEvents.length, 'info');
const duration = Date.now() - startTime;
updateTestResult('Event Filtering', 'passed', duration, 'Filtering works correctly');
log('✅ Test 3: PASSED', 'success');
} catch (error) {
const duration = Date.now() - startTime;
updateTestResult('Event Filtering', 'failed', duration, error.message);
log('❌ Test 3: FAILED - ' + error.message, 'error');
}
}
async function testStateManagement() {
log('=== Test 4: State Management ===', 'info');
const startTime = Date.now();
try {
const initialVisible = [...visibleCalendarIds];
log('✓ Initial visible: ' + initialVisible.join(', '), 'info');
// Toggle visibility
toggleCalendarVisibility(3, true);
if (!visibleCalendarIds.includes(3)) {
throw new Error('Calendar 3 not added to visible');
}
log('✓ Calendar 3 added to visible', 'success');
toggleCalendarVisibility(3, false);
if (visibleCalendarIds.includes(3)) {
throw new Error('Calendar 3 not removed from visible');
}
log('✓ Calendar 3 removed from visible', 'success');
const duration = Date.now() - startTime;
updateTestResult('State Management', 'passed', duration, 'State updates correctly');
log('✅ Test 4: PASSED', 'success');
} catch (error) {
const duration = Date.now() - startTime;
updateTestResult('State Management', 'failed', duration, error.message);
log('❌ Test 4: FAILED - ' + error.message, 'error');
}
}
async function testCallbacks() {
log('=== Test 5: Callbacks ===', 'info');
const startTime = Date.now();
try {
let callbackCalled = false;
function testCallback(calendarId, isVisible) {
callbackCalled = true;
log('✓ Callback triggered: cal=' + calendarId + ', visible=' + isVisible, 'info');
}
testCallback(1, true);
if (!callbackCalled) {
throw new Error('Callback not called');
}
log('✓ Callback mechanism works', 'success');
const duration = Date.now() - startTime;
updateTestResult('Callbacks', 'passed', duration, 'Callbacks working');
log('✅ Test 5: PASSED', 'success');
} catch (error) {
const duration = Date.now() - startTime;
updateTestResult('Callbacks', 'failed', duration, error.message);
log('❌ Test 5: FAILED - ' + error.message, 'error');
}
}
async function testDataStructures() {
log('=== Test 6: Data Structures ===', 'info');
const startTime = Date.now();
try {
// Test calendar structure
const cal = mockCalendars[0];
const requiredFields = ['id', 'title', 'color', 'calendar_type', 'user_can_edit', 'user_can_view'];
for (const field of requiredFields) {
if (!(field in cal)) {
throw new Error('Missing field: ' + field);
}
}
log('✓ Calendar structure valid', 'success');
// Test event structure
const event = mockEvents[0];
const eventFields = ['id', 'title', 'start', 'calendar_id'];
for (const field of eventFields) {
if (!(field in event)) {
throw new Error('Missing event field: ' + field);
}
}
log('✓ Event structure valid', 'success');
// Test deduplication
const duplicates = [...mockEvents, mockEvents[0]];
const unique = deduplicateEvents(duplicates);
if (unique.length !== mockEvents.length) {
throw new Error('Deduplication failed');
}
log('✓ Event deduplication works', 'success');
const duration = Date.now() - startTime;
updateTestResult('Data Structures', 'passed', duration, 'All structures valid');
log('✅ Test 6: PASSED', 'success');
} catch (error) {
const duration = Date.now() - startTime;
updateTestResult('Data Structures', 'failed', duration, error.message);
log('❌ Test 6: FAILED - ' + error.message, 'error');
}
}
// =========================
// Helper Functions
// =========================
function fetchEventsForVisibleCalendars(allEvents) {
return allEvents.filter(e => visibleCalendarIds.includes(e.calendar_id));
}
function toggleCalendarVisibility(calendarId, isVisible) {
if (isVisible) {
if (!visibleCalendarIds.includes(calendarId)) {
visibleCalendarIds.push(calendarId);
}
} else {
visibleCalendarIds = visibleCalendarIds.filter(id => id !== calendarId);
}
log('Visible calendars: ' + visibleCalendarIds.join(', '), 'info');
}
function deduplicateEvents(events) {
const seen = new Set();
return events.filter(e => {
if (seen.has(e.id)) return false;
seen.add(e.id);
return true;
});
}
function renderCalendarList(calendars) {
const container = document.getElementById('calendarListContainer');
container.innerHTML = calendars.map(cal => `
<div class="calendar-item">
<input
type="checkbox"
${cal.visible ? 'checked' : ''}
onchange="handleCalendarToggle(${cal.id}, this.checked)"
>
<div class="calendar-color" style="background-color: ${cal.color}"></div>
<div class="calendar-info">
<div class="calendar-title">${cal.title}</div>
<div class="calendar-meta">${cal.calendar_type} ${cal.user_can_edit ? '✏️' : '👁️'}</div>
</div>
</div>
`).join('');
}
function handleCalendarToggle(calendarId, isChecked) {
log('Calendar ' + calendarId + ' toggled: ' + isChecked, 'info');
toggleCalendarVisibility(calendarId, isChecked);
// Update calendar state
const calendar = mockCalendars.find(c => c.id === calendarId);
if (calendar) {
calendar.visible = isChecked;
}
}
function createCalendar() {
const title = prompt('Calendar title:');
if (!title) return;
const color = '#' + Math.floor(Math.random()*16777215).toString(16);
mockApiCreateCalendar({
title: title,
description: '',
color: color,
calendar_type: 'personal'
}).then(result => {
log('Calendar created: ' + result.data.title, 'success');
renderCalendarList(mockCalendars);
});
}
function toggleConsole() {
const console = document.getElementById('logConsole');
console.style.display = console.style.display === 'none' ? 'block' : 'none';
}
// =========================
// Event Listeners
// =========================
document.getElementById('runAllTests').addEventListener('click', async () => {
testResults = [];
log('🚀 Running all tests...', 'info');
await testApiWrapper();
await delay(300);
await testCalendarManager();
await delay(300);
await testEventFiltering();
await delay(300);
await testStateManagement();
await delay(300);
await testCallbacks();
await delay(300);
await testDataStructures();
log('🏁 All tests completed!', 'success');
});
document.getElementById('clearLogs').addEventListener('click', () => {
document.getElementById('logConsole').innerHTML = '';
log('Logs cleared', 'info');
});
// =========================
// Initialize
// =========================
(async function init() {
log('✅ Test suite initialized', 'success');
log('Click "Run All Tests" to start', 'info');
log('📡 Offline mode - using mock data', 'warn');
try {
await testCalendarManager();
} catch (error) {
log('Init error: ' + error.message, 'error');
}
})();
</script>
</body>
</html>