chore: удаление устаревших тестовых и временных файлов
- Удалены старые тестовые файлы (test_*.html, test_*.py) - Удалены временные конфиги и скрипты (.env.test, test_reply.sh) - Удалены устаревшие документы (CELERY_NEXT_STEPS.md, FIX_INTEGRATION.md) - Очищена папка localcerts_backup - Удалены выводы pytest (pytest_output.txt) - Удалены конфигурационные остатки (.gitignore.ldap, corp-ca.pem)
This commit is contained in:
parent
fcf4bff3a9
commit
8aa6d6ec87
@ -1,5 +0,0 @@
|
||||
# Добавляем в .gitignore данные LDAP (чтобы не коммитить)
|
||||
|
||||
# LDAP test data
|
||||
backend/ldap_test_data/
|
||||
backend/ldap_test_config/
|
||||
@ -1,201 +0,0 @@
|
||||
# Celery Integration - Следующие шаги
|
||||
|
||||
## ✅ Что уже сделано
|
||||
|
||||
1. ✅ Добавлены зависимости в `requirements.txt`:
|
||||
- `celery==5.5.3`
|
||||
- `redis==5.2.1`
|
||||
- `django-celery-beat==2.7.0`
|
||||
- `django-celery-results==2.5.1`
|
||||
- `flower==2.0.1`
|
||||
|
||||
2. ✅ Создан файл конфигурации `eusrr_backend/celery.py`
|
||||
- Настроена автоматическая регистрация задач
|
||||
- Добавлен debug task для тестирования
|
||||
|
||||
3. ✅ Обновлен `eusrr_backend/__init__.py`
|
||||
- Автоматический импорт celery_app при запуске Django
|
||||
|
||||
4. ✅ Добавлены настройки Celery в `settings.py`:
|
||||
- `CELERY_BROKER_URL` - Redis как брокер сообщений
|
||||
- `CELERY_RESULT_BACKEND` - Redis для хранения результатов
|
||||
- Настройка роутинга задач по очередям
|
||||
- Таймауты и лимиты для workers
|
||||
|
||||
5. ✅ Добавлены приложения в `INSTALLED_APPS`:
|
||||
- `django_celery_beat` - для периодических задач
|
||||
- `django_celery_results` - для хранения результатов
|
||||
|
||||
6. ✅ Созданы Celery задачи в `notifications/tasks.py`:
|
||||
- `send_notification_task` - отправка одного уведомления
|
||||
- `send_bulk_notifications_task` - массовая отправка
|
||||
- `cleanup_old_notifications_task` - очистка старых уведомлений
|
||||
- `send_digest_email_task` - email-дайджесты
|
||||
|
||||
## 🔄 Следующие шаги (выполнить вручную)
|
||||
|
||||
### Шаг 1: Установка зависимостей
|
||||
|
||||
Запустите задачу в VS Code:
|
||||
- **Pip: install requirements**
|
||||
|
||||
Или вручную:
|
||||
```bash
|
||||
cd backend
|
||||
..\.venv\Scripts\pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### Шаг 2: Установка и запуск Redis
|
||||
|
||||
#### Вариант 1: Windows (рекомендуется Memurai)
|
||||
1. Скачайте Memurai (Redis для Windows): https://www.memurai.com/
|
||||
2. Установите и запустите как службу Windows
|
||||
3. Redis будет доступен на `localhost:6379`
|
||||
|
||||
#### Вариант 2: Docker
|
||||
```bash
|
||||
docker run -d -p 6379:6379 --name redis redis:7-alpine
|
||||
```
|
||||
|
||||
#### Проверка Redis:
|
||||
```bash
|
||||
redis-cli ping
|
||||
# Должно вернуть: PONG
|
||||
```
|
||||
|
||||
### Шаг 3: Применение миграций
|
||||
|
||||
Запустите:
|
||||
```bash
|
||||
cd backend
|
||||
migrate_celery.bat
|
||||
```
|
||||
|
||||
Или вручную:
|
||||
```bash
|
||||
..\.venv\Scripts\python manage.py migrate django_celery_beat
|
||||
..\.venv\Scripts\python manage.py migrate django_celery_results
|
||||
```
|
||||
|
||||
### Шаг 4: Тестирование Celery Worker
|
||||
|
||||
В отдельном терминале запустите worker:
|
||||
```bash
|
||||
cd backend
|
||||
..\.venv\Scripts\celery -A eusrr_backend worker -l info --pool=solo
|
||||
```
|
||||
|
||||
**Примечание**: На Windows используйте `--pool=solo` или `--pool=gevent`
|
||||
|
||||
### Шаг 5: Тестирование отправки задачи
|
||||
|
||||
В Django shell:
|
||||
```python
|
||||
..\.venv\Scripts\python manage.py shell
|
||||
|
||||
from celery import current_app
|
||||
current_app.send_task('eusrr_backend.debug_task')
|
||||
```
|
||||
|
||||
Вы должны увидеть выполнение задачи в логах worker.
|
||||
|
||||
### Шаг 6: Интеграция с NotificationService
|
||||
|
||||
Обновите `notifications/services.py`:
|
||||
```python
|
||||
from notifications.tasks import send_notification_task, send_bulk_notifications_task
|
||||
|
||||
class NotificationService:
|
||||
@staticmethod
|
||||
def create_notification(...):
|
||||
# Вместо синхронного создания
|
||||
send_notification_task.delay(
|
||||
notification_type=notification_type,
|
||||
user_id=user.id,
|
||||
title=title,
|
||||
message=message,
|
||||
link=link,
|
||||
sender_id=sender.id if sender else None,
|
||||
metadata=metadata,
|
||||
)
|
||||
```
|
||||
|
||||
### Шаг 7: Запуск Flower (мониторинг)
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
..\.venv\Scripts\celery -A eusrr_backend flower
|
||||
```
|
||||
|
||||
Откройте http://localhost:5555 для просмотра статистики задач.
|
||||
|
||||
### Шаг 8: Настройка периодических задач
|
||||
|
||||
В Django Admin:
|
||||
1. Перейдите в "Periodic Tasks"
|
||||
2. Добавьте задачу для `notifications.cleanup_old_notifications`
|
||||
3. Настройте интервал (например, раз в день)
|
||||
|
||||
Или программно в `eusrr_backend/celery.py`:
|
||||
```python
|
||||
from celery.schedules import crontab
|
||||
|
||||
app.conf.beat_schedule = {
|
||||
'cleanup-old-notifications': {
|
||||
'task': 'notifications.cleanup_old_notifications',
|
||||
'schedule': crontab(hour=3, minute=0), # Каждый день в 3:00
|
||||
'args': (90,) # Удалять уведомления старше 90 дней
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Запустите beat scheduler:
|
||||
```bash
|
||||
..\.venv\Scripts\celery -A eusrr_backend beat -l info
|
||||
```
|
||||
|
||||
## 📊 Ожидаемый результат
|
||||
|
||||
**До внедрения Celery:**
|
||||
- Отправка сообщения в группу с 8 участниками: ~500ms
|
||||
- Блокировка API response до отправки всех уведомлений
|
||||
|
||||
**После внедрения Celery:**
|
||||
- Отправка сообщения: <50ms (только создание задачи)
|
||||
- Уведомления обрабатываются асинхронно
|
||||
- API мгновенно отвечает пользователю
|
||||
|
||||
## 🔍 Отладка
|
||||
|
||||
### Проверка статуса Celery
|
||||
```bash
|
||||
..\.venv\Scripts\celery -A eusrr_backend inspect active
|
||||
..\.venv\Scripts\celery -A eusrr_backend inspect stats
|
||||
```
|
||||
|
||||
### Проверка задач в очереди
|
||||
```bash
|
||||
..\.venv\Scripts\celery -A eusrr_backend inspect reserved
|
||||
```
|
||||
|
||||
### Логи
|
||||
- Worker логи: консоль где запущен worker
|
||||
- Flower: http://localhost:5555
|
||||
- Django логи: стандартные логи приложения
|
||||
|
||||
## 📚 Документация
|
||||
|
||||
- Подробная документация: `docs/guides/CELERY_SETUP.md`
|
||||
- План внедрения (5 дней): `docs/in-progress/CELERY_INTEGRATION_PLAN.md`
|
||||
- Celery docs: https://docs.celeryq.dev/
|
||||
|
||||
## ⚠️ Production Deployment
|
||||
|
||||
Для production:
|
||||
1. Используйте Redis Cluster или Redis Sentinel
|
||||
2. Запускайте несколько workers с supervisord/systemd
|
||||
3. Настройте мониторинг (Flower + Prometheus)
|
||||
4. Добавьте retry policies и dead letter queue
|
||||
5. Настройте автоматический перезапуск workers
|
||||
|
||||
См. `docs/in-progress/CELERY_INTEGRATION_PLAN.md` для деталей.
|
||||
@ -1,207 +0,0 @@
|
||||
# 🔍 Команды для отладки календаря через консоль браузера
|
||||
|
||||
Откройте консоль браузера (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;
|
||||
})();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Выполните команды и покажите результаты! 🔍
|
||||
@ -1,60 +0,0 @@
|
||||
# 🔧 Исправление проблемы инициализации 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()
|
||||
```
|
||||
@ -1,267 +0,0 @@
|
||||
============================= test session starts =============================
|
||||
platform win32 -- Python 3.12.10, pytest-8.4.1, pluggy-1.6.0 -- C:\Users\nadeini\Dev\EUSRR\.venv\Scripts\python.exe
|
||||
cachedir: .pytest_cache
|
||||
django: version: 5.2.4, settings: eusrr_backend.settings (from ini)
|
||||
rootdir: C:\Users\nadeini\Dev\EUSRR\backend
|
||||
configfile: pytest.ini
|
||||
plugins: anyio-4.12.0, django-4.11.1
|
||||
collecting ... collected 19 items
|
||||
|
||||
tests/api/auth/test_auth_and_registration.py::test_register_success_sends_email_and_user_inactive FAILED [ 5%]
|
||||
tests/api/auth/test_auth_and_registration.py::test_register_missing_required_field[first_name] PASSED [ 10%]
|
||||
tests/api/auth/test_auth_and_registration.py::test_register_missing_required_field[last_name] PASSED [ 15%]
|
||||
tests/api/auth/test_auth_and_registration.py::test_register_missing_required_field[phone_number] PASSED [ 21%]
|
||||
tests/api/auth/test_auth_and_registration.py::test_register_missing_required_field[email] PASSED [ 26%]
|
||||
tests/api/auth/test_auth_and_registration.py::test_register_missing_required_field[password] PASSED [ 31%]
|
||||
tests/api/auth/test_auth_and_registration.py::test_register_missing_required_field[birth_date] PASSED [ 36%]
|
||||
tests/api/auth/test_auth_and_registration.py::test_register_requires_at_least_one_contact PASSED [ 42%]
|
||||
tests/api/auth/test_auth_and_registration.py::test_register_birth_date_must_be_valid_date PASSED [ 47%]
|
||||
tests/api/auth/test_auth_and_registration.py::test_register_duplicate_email_phone FAILED [ 52%]
|
||||
tests/api/auth/test_auth_and_registration.py::test_register_accepts_optional_fields FAILED [ 57%]
|
||||
tests/api/auth/test_auth_and_registration.py::test_verify_email_success_activates_user FAILED [ 63%]
|
||||
tests/api/auth/test_auth_and_registration.py::test_verify_email_wrong_code FAILED [ 68%]
|
||||
tests/api/auth/test_auth_and_registration.py::test_resend_email_sends_new_code_and_old_becomes_invalid FAILED [ 73%]
|
||||
tests/api/auth/test_auth_and_registration.py::test_verify_expired_more_than_5_minutes_deletes_account FAILED [ 78%]
|
||||
tests/api/auth/test_auth_and_registration.py::test_login_by_email_denied_before_verify FAILED [ 84%]
|
||||
tests/api/auth/test_auth_and_registration.py::test_login_by_email_allowed_after_verify FAILED [ 89%]
|
||||
tests/api/auth/test_auth_and_registration.py::test_login_by_phone_allowed_after_verify FAILED [ 94%]
|
||||
tests/api/auth/test_auth_and_registration.py::test_login_wrong_password FAILED [100%]
|
||||
|
||||
================================== FAILURES ===================================
|
||||
_____________ test_register_success_sends_email_and_user_inactive _____________
|
||||
|
||||
api = <rest_framework.test.APIClient object at 0x0000013601BABB30>
|
||||
|
||||
def test_register_success_sends_email_and_user_inactive(api):
|
||||
resp = register(api)
|
||||
> assert resp.status_code == status.HTTP_201_CREATED
|
||||
E assert 400 == 201
|
||||
E + where 400 = <Response status_code=400, "application/json">.status_code
|
||||
E + and 201 = status.HTTP_201_CREATED
|
||||
|
||||
tests\api\auth\test_auth_and_registration.py:102: AssertionError
|
||||
---------------------------- Captured stderr setup ----------------------------
|
||||
Creating test database for alias 'default'...
|
||||
---------------------------- Captured stderr call -----------------------------
|
||||
[WARNING] 2026-01-05 07:59:47,115 django.request:253 Bad Request: /api/v1/auth/register/
|
||||
------------------------------ Captured log call ------------------------------
|
||||
WARNING api.v1.employees.views:views.py:368 [REGISTER] Received data: {'first_name': 'Èâàí', 'last_name': 'Èâàíîâ', 'phone_number': '+79990000001', 'email': 'ivan@example.com', 'password': 'Str0ngPass!', 'birth_date': '1990-01-01', 'telegram': '@ivan'}
|
||||
WARNING api.v1.employees.views:views.py:369 [REGISTER] Content-Type: application/json
|
||||
WARNING api.v1.employees.views:views.py:387 [REGISTER] Validation errors: {'avatar': [ErrorDetail(string='Íè îäíîãî ôàéëà íå áûëî îòïðàâëåíî.', code='required')], 'gender': [ErrorDetail(string='Ïîëå "Ïîë" îáÿçàòåëüíî äëÿ çàïîëíåíèÿ.', code='required')]}
|
||||
_____________________ test_register_duplicate_email_phone _____________________
|
||||
|
||||
api = <rest_framework.test.APIClient object at 0x0000013601C54B30>
|
||||
|
||||
def test_register_duplicate_email_phone(api):
|
||||
# 1) Ðåãèñòðèðóåì ïåðâîãî ïîëüçîâàòåëÿ (ïîêà íå ïîäòâåðæä¸í)
|
||||
payload = register_payload()
|
||||
> assert register(api, **payload).status_code == status.HTTP_201_CREATED
|
||||
E assert 400 == 201
|
||||
E + where 400 = <Response status_code=400, "application/json">.status_code
|
||||
E + where <Response status_code=400, "application/json"> = register(<rest_framework.test.APIClient object at 0x0000013601C54B30>, **{'birth_date': '1990-01-01', 'email': 'ivan@example.com', 'first_name': 'Èâàí', 'last_name': 'Èâàíîâ', ...})
|
||||
E + and 201 = status.HTTP_201_CREATED
|
||||
|
||||
tests\api\auth\test_auth_and_registration.py:149: AssertionError
|
||||
---------------------------- Captured stderr call -----------------------------
|
||||
[WARNING] 2026-01-05 07:59:47,550 django.request:253 Bad Request: /api/v1/auth/register/
|
||||
------------------------------ Captured log call ------------------------------
|
||||
WARNING api.v1.employees.views:views.py:368 [REGISTER] Received data: {'first_name': 'Èâàí', 'last_name': 'Èâàíîâ', 'phone_number': '+79990000001', 'email': 'ivan@example.com', 'password': 'Str0ngPass!', 'birth_date': '1990-01-01', 'telegram': '@ivan'}
|
||||
WARNING api.v1.employees.views:views.py:369 [REGISTER] Content-Type: application/json
|
||||
WARNING api.v1.employees.views:views.py:387 [REGISTER] Validation errors: {'avatar': [ErrorDetail(string='Íè îäíîãî ôàéëà íå áûëî îòïðàâëåíî.', code='required')], 'gender': [ErrorDetail(string='Ïîëå "Ïîë" îáÿçàòåëüíî äëÿ çàïîëíåíèÿ.', code='required')]}
|
||||
____________________ test_register_accepts_optional_fields ____________________
|
||||
|
||||
api = <rest_framework.test.APIClient object at 0x0000013601C56780>
|
||||
|
||||
def test_register_accepts_optional_fields(api):
|
||||
resp = register(
|
||||
api,
|
||||
patronymic="Ïåòðîâè÷",
|
||||
gender=1,
|
||||
whatsapp="+79995550011", # ìîæíî óêàçàòü äðóãîé êîíòàêò
|
||||
telegram="", # òåëåãðàì ìîæíî íå óêàçûâàòü, ò.ê. whatsapp åñòü
|
||||
)
|
||||
> assert resp.status_code == status.HTTP_201_CREATED
|
||||
E assert 400 == 201
|
||||
E + where 400 = <Response status_code=400, "application/json">.status_code
|
||||
E + and 201 = status.HTTP_201_CREATED
|
||||
|
||||
tests\api\auth\test_auth_and_registration.py:183: AssertionError
|
||||
---------------------------- Captured stderr call -----------------------------
|
||||
[WARNING] 2026-01-05 07:59:47,563 django.request:253 Bad Request: /api/v1/auth/register/
|
||||
------------------------------ Captured log call ------------------------------
|
||||
WARNING api.v1.employees.views:views.py:368 [REGISTER] Received data: {'first_name': 'Èâàí', 'last_name': 'Èâàíîâ', 'phone_number': '+79990000001', 'email': 'ivan@example.com', 'password': 'Str0ngPass!', 'birth_date': '1990-01-01', 'telegram': '', 'patronymic': 'Ïåòðîâè÷', 'gender': 1, 'whatsapp': '+79995550011'}
|
||||
WARNING api.v1.employees.views:views.py:369 [REGISTER] Content-Type: application/json
|
||||
WARNING api.v1.employees.views:views.py:387 [REGISTER] Validation errors: {'avatar': [ErrorDetail(string='Íè îäíîãî ôàéëà íå áûëî îòïðàâëåíî.', code='required')]}
|
||||
__________________ test_verify_email_success_activates_user ___________________
|
||||
|
||||
api = <rest_framework.test.APIClient object at 0x0000013601C54530>
|
||||
|
||||
def test_verify_email_success_activates_user(api):
|
||||
> assert register(api).status_code == status.HTTP_201_CREATED
|
||||
E assert 400 == 201
|
||||
E + where 400 = <Response status_code=400, "application/json">.status_code
|
||||
E + where <Response status_code=400, "application/json"> = register(<rest_framework.test.APIClient object at 0x0000013601C54530>)
|
||||
E + and 201 = status.HTTP_201_CREATED
|
||||
|
||||
tests\api\auth\test_auth_and_registration.py:190: AssertionError
|
||||
---------------------------- Captured stderr call -----------------------------
|
||||
[WARNING] 2026-01-05 07:59:47,574 django.request:253 Bad Request: /api/v1/auth/register/
|
||||
------------------------------ Captured log call ------------------------------
|
||||
WARNING api.v1.employees.views:views.py:368 [REGISTER] Received data: {'first_name': 'Èâàí', 'last_name': 'Èâàíîâ', 'phone_number': '+79990000001', 'email': 'ivan@example.com', 'password': 'Str0ngPass!', 'birth_date': '1990-01-01', 'telegram': '@ivan'}
|
||||
WARNING api.v1.employees.views:views.py:369 [REGISTER] Content-Type: application/json
|
||||
WARNING api.v1.employees.views:views.py:387 [REGISTER] Validation errors: {'avatar': [ErrorDetail(string='Íè îäíîãî ôàéëà íå áûëî îòïðàâëåíî.', code='required')], 'gender': [ErrorDetail(string='Ïîëå "Ïîë" îáÿçàòåëüíî äëÿ çàïîëíåíèÿ.', code='required')]}
|
||||
________________________ test_verify_email_wrong_code _________________________
|
||||
|
||||
api = <rest_framework.test.APIClient object at 0x0000013601DB01A0>
|
||||
|
||||
def test_verify_email_wrong_code(api):
|
||||
> assert register(api).status_code == status.HTTP_201_CREATED
|
||||
E assert 400 == 201
|
||||
E + where 400 = <Response status_code=400, "application/json">.status_code
|
||||
E + where <Response status_code=400, "application/json"> = register(<rest_framework.test.APIClient object at 0x0000013601DB01A0>)
|
||||
E + and 201 = status.HTTP_201_CREATED
|
||||
|
||||
tests\api\auth\test_auth_and_registration.py:202: AssertionError
|
||||
---------------------------- Captured stderr call -----------------------------
|
||||
[WARNING] 2026-01-05 07:59:47,586 django.request:253 Bad Request: /api/v1/auth/register/
|
||||
------------------------------ Captured log call ------------------------------
|
||||
WARNING api.v1.employees.views:views.py:368 [REGISTER] Received data: {'first_name': 'Èâàí', 'last_name': 'Èâàíîâ', 'phone_number': '+79990000001', 'email': 'ivan@example.com', 'password': 'Str0ngPass!', 'birth_date': '1990-01-01', 'telegram': '@ivan'}
|
||||
WARNING api.v1.employees.views:views.py:369 [REGISTER] Content-Type: application/json
|
||||
WARNING api.v1.employees.views:views.py:387 [REGISTER] Validation errors: {'avatar': [ErrorDetail(string='Íè îäíîãî ôàéëà íå áûëî îòïðàâëåíî.', code='required')], 'gender': [ErrorDetail(string='Ïîëå "Ïîë" îáÿçàòåëüíî äëÿ çàïîëíåíèÿ.', code='required')]}
|
||||
__________ test_resend_email_sends_new_code_and_old_becomes_invalid ___________
|
||||
|
||||
api = <rest_framework.test.APIClient object at 0x0000013601CCEA50>
|
||||
|
||||
def test_resend_email_sends_new_code_and_old_becomes_invalid(api):
|
||||
> assert register(api).status_code == status.HTTP_201_CREATED
|
||||
E assert 400 == 201
|
||||
E + where 400 = <Response status_code=400, "application/json">.status_code
|
||||
E + where <Response status_code=400, "application/json"> = register(<rest_framework.test.APIClient object at 0x0000013601CCEA50>)
|
||||
E + and 201 = status.HTTP_201_CREATED
|
||||
|
||||
tests\api\auth\test_auth_and_registration.py:212: AssertionError
|
||||
---------------------------- Captured stderr call -----------------------------
|
||||
[WARNING] 2026-01-05 07:59:47,598 django.request:253 Bad Request: /api/v1/auth/register/
|
||||
------------------------------ Captured log call ------------------------------
|
||||
WARNING api.v1.employees.views:views.py:368 [REGISTER] Received data: {'first_name': 'Èâàí', 'last_name': 'Èâàíîâ', 'phone_number': '+79990000001', 'email': 'ivan@example.com', 'password': 'Str0ngPass!', 'birth_date': '1990-01-01', 'telegram': '@ivan'}
|
||||
WARNING api.v1.employees.views:views.py:369 [REGISTER] Content-Type: application/json
|
||||
WARNING api.v1.employees.views:views.py:387 [REGISTER] Validation errors: {'avatar': [ErrorDetail(string='Íè îäíîãî ôàéëà íå áûëî îòïðàâëåíî.', code='required')], 'gender': [ErrorDetail(string='Ïîëå "Ïîë" îáÿçàòåëüíî äëÿ çàïîëíåíèÿ.', code='required')]}
|
||||
___________ test_verify_expired_more_than_5_minutes_deletes_account ___________
|
||||
|
||||
api = <rest_framework.test.APIClient object at 0x0000013601CCC290>
|
||||
settings = <pytest_django.fixtures.SettingsWrapper object at 0x0000013601CCF2F0>
|
||||
|
||||
def test_verify_expired_more_than_5_minutes_deletes_account(api, settings):
|
||||
# Ðåãèñòðèðóåì
|
||||
> assert register(api).status_code == status.HTTP_201_CREATED
|
||||
E assert 400 == 201
|
||||
E + where 400 = <Response status_code=400, "application/json">.status_code
|
||||
E + where <Response status_code=400, "application/json"> = register(<rest_framework.test.APIClient object at 0x0000013601CCC290>)
|
||||
E + and 201 = status.HTTP_201_CREATED
|
||||
|
||||
tests\api\auth\test_auth_and_registration.py:239: AssertionError
|
||||
---------------------------- Captured stderr call -----------------------------
|
||||
[WARNING] 2026-01-05 07:59:47,610 django.request:253 Bad Request: /api/v1/auth/register/
|
||||
------------------------------ Captured log call ------------------------------
|
||||
WARNING api.v1.employees.views:views.py:368 [REGISTER] Received data: {'first_name': 'Èâàí', 'last_name': 'Èâàíîâ', 'phone_number': '+79990000001', 'email': 'ivan@example.com', 'password': 'Str0ngPass!', 'birth_date': '1990-01-01', 'telegram': '@ivan'}
|
||||
WARNING api.v1.employees.views:views.py:369 [REGISTER] Content-Type: application/json
|
||||
WARNING api.v1.employees.views:views.py:387 [REGISTER] Validation errors: {'avatar': [ErrorDetail(string='Íè îäíîãî ôàéëà íå áûëî îòïðàâëåíî.', code='required')], 'gender': [ErrorDetail(string='Ïîëå "Ïîë" îáÿçàòåëüíî äëÿ çàïîëíåíèÿ.', code='required')]}
|
||||
__________________ test_login_by_email_denied_before_verify ___________________
|
||||
|
||||
api = <rest_framework.test.APIClient object at 0x00000136007A18E0>
|
||||
|
||||
def test_login_by_email_denied_before_verify(api):
|
||||
> assert register(api).status_code == status.HTTP_201_CREATED
|
||||
E assert 400 == 201
|
||||
E + where 400 = <Response status_code=400, "application/json">.status_code
|
||||
E + where <Response status_code=400, "application/json"> = register(<rest_framework.test.APIClient object at 0x00000136007A18E0>)
|
||||
E + and 201 = status.HTTP_201_CREATED
|
||||
|
||||
tests\api\auth\test_auth_and_registration.py:258: AssertionError
|
||||
---------------------------- Captured stderr call -----------------------------
|
||||
[WARNING] 2026-01-05 07:59:47,622 django.request:253 Bad Request: /api/v1/auth/register/
|
||||
------------------------------ Captured log call ------------------------------
|
||||
WARNING api.v1.employees.views:views.py:368 [REGISTER] Received data: {'first_name': 'Èâàí', 'last_name': 'Èâàíîâ', 'phone_number': '+79990000001', 'email': 'ivan@example.com', 'password': 'Str0ngPass!', 'birth_date': '1990-01-01', 'telegram': '@ivan'}
|
||||
WARNING api.v1.employees.views:views.py:369 [REGISTER] Content-Type: application/json
|
||||
WARNING api.v1.employees.views:views.py:387 [REGISTER] Validation errors: {'avatar': [ErrorDetail(string='Íè îäíîãî ôàéëà íå áûëî îòïðàâëåíî.', code='required')], 'gender': [ErrorDetail(string='Ïîëå "Ïîë" îáÿçàòåëüíî äëÿ çàïîëíåíèÿ.', code='required')]}
|
||||
__________________ test_login_by_email_allowed_after_verify ___________________
|
||||
|
||||
api = <rest_framework.test.APIClient object at 0x00000136007A25D0>
|
||||
|
||||
def test_login_by_email_allowed_after_verify(api):
|
||||
> assert register(api).status_code == status.HTTP_201_CREATED
|
||||
E assert 400 == 201
|
||||
E + where 400 = <Response status_code=400, "application/json">.status_code
|
||||
E + where <Response status_code=400, "application/json"> = register(<rest_framework.test.APIClient object at 0x00000136007A25D0>)
|
||||
E + and 201 = status.HTTP_201_CREATED
|
||||
|
||||
tests\api\auth\test_auth_and_registration.py:265: AssertionError
|
||||
---------------------------- Captured stderr call -----------------------------
|
||||
[WARNING] 2026-01-05 07:59:47,634 django.request:253 Bad Request: /api/v1/auth/register/
|
||||
------------------------------ Captured log call ------------------------------
|
||||
WARNING api.v1.employees.views:views.py:368 [REGISTER] Received data: {'first_name': 'Èâàí', 'last_name': 'Èâàíîâ', 'phone_number': '+79990000001', 'email': 'ivan@example.com', 'password': 'Str0ngPass!', 'birth_date': '1990-01-01', 'telegram': '@ivan'}
|
||||
WARNING api.v1.employees.views:views.py:369 [REGISTER] Content-Type: application/json
|
||||
WARNING api.v1.employees.views:views.py:387 [REGISTER] Validation errors: {'avatar': [ErrorDetail(string='Íè îäíîãî ôàéëà íå áûëî îòïðàâëåíî.', code='required')], 'gender': [ErrorDetail(string='Ïîëå "Ïîë" îáÿçàòåëüíî äëÿ çàïîëíåíèÿ.', code='required')]}
|
||||
__________________ test_login_by_phone_allowed_after_verify ___________________
|
||||
|
||||
api = <rest_framework.test.APIClient object at 0x0000013601B55490>
|
||||
|
||||
def test_login_by_phone_allowed_after_verify(api):
|
||||
# Ðåãèñòðèðóåì ñ òåëåôîíîì è ïîäòâåðæäàåì
|
||||
payload = register_payload()
|
||||
> assert register(api, **payload).status_code == status.HTTP_201_CREATED
|
||||
E assert 400 == 201
|
||||
E + where 400 = <Response status_code=400, "application/json">.status_code
|
||||
E + where <Response status_code=400, "application/json"> = register(<rest_framework.test.APIClient object at 0x0000013601B55490>, **{'birth_date': '1990-01-01', 'email': 'ivan@example.com', 'first_name': 'Èâàí', 'last_name': 'Èâàíîâ', ...})
|
||||
E + and 201 = status.HTTP_201_CREATED
|
||||
|
||||
tests\api\auth\test_auth_and_registration.py:278: AssertionError
|
||||
---------------------------- Captured stderr call -----------------------------
|
||||
[WARNING] 2026-01-05 07:59:47,647 django.request:253 Bad Request: /api/v1/auth/register/
|
||||
------------------------------ Captured log call ------------------------------
|
||||
WARNING api.v1.employees.views:views.py:368 [REGISTER] Received data: {'first_name': 'Èâàí', 'last_name': 'Èâàíîâ', 'phone_number': '+79990000001', 'email': 'ivan@example.com', 'password': 'Str0ngPass!', 'birth_date': '1990-01-01', 'telegram': '@ivan'}
|
||||
WARNING api.v1.employees.views:views.py:369 [REGISTER] Content-Type: application/json
|
||||
WARNING api.v1.employees.views:views.py:387 [REGISTER] Validation errors: {'avatar': [ErrorDetail(string='Íè îäíîãî ôàéëà íå áûëî îòïðàâëåíî.', code='required')], 'gender': [ErrorDetail(string='Ïîëå "Ïîë" îáÿçàòåëüíî äëÿ çàïîëíåíèÿ.', code='required')]}
|
||||
__________________________ test_login_wrong_password __________________________
|
||||
|
||||
api = <rest_framework.test.APIClient object at 0x0000013601B56E40>
|
||||
|
||||
def test_login_wrong_password(api):
|
||||
> assert register(api).status_code == status.HTTP_201_CREATED
|
||||
E assert 400 == 201
|
||||
E + where 400 = <Response status_code=400, "application/json">.status_code
|
||||
E + where <Response status_code=400, "application/json"> = register(<rest_framework.test.APIClient object at 0x0000013601B56E40>)
|
||||
E + and 201 = status.HTTP_201_CREATED
|
||||
|
||||
tests\api\auth\test_auth_and_registration.py:290: AssertionError
|
||||
---------------------------- Captured stderr call -----------------------------
|
||||
[WARNING] 2026-01-05 07:59:47,659 django.request:253 Bad Request: /api/v1/auth/register/
|
||||
------------------------------ Captured log call ------------------------------
|
||||
WARNING api.v1.employees.views:views.py:368 [REGISTER] Received data: {'first_name': 'Èâàí', 'last_name': 'Èâàíîâ', 'phone_number': '+79990000001', 'email': 'ivan@example.com', 'password': 'Str0ngPass!', 'birth_date': '1990-01-01', 'telegram': '@ivan'}
|
||||
WARNING api.v1.employees.views:views.py:369 [REGISTER] Content-Type: application/json
|
||||
WARNING api.v1.employees.views:views.py:387 [REGISTER] Validation errors: {'avatar': [ErrorDetail(string='Íè îäíîãî ôàéëà íå áûëî îòïðàâëåíî.', code='required')], 'gender': [ErrorDetail(string='Ïîëå "Ïîë" îáÿçàòåëüíî äëÿ çàïîëíåíèÿ.', code='required')]}
|
||||
-------------------------- Captured stderr teardown ---------------------------
|
||||
Destroying test database for alias 'default'...
|
||||
============================== warnings summary ===============================
|
||||
tests/api/auth/test_auth_and_registration.py::test_register_success_sends_email_and_user_inactive
|
||||
C:\Users\nadeini\Dev\EUSRR\.venv\Lib\site-packages\pyasn1\codec\ber\encoder.py:952: DeprecationWarning: tagMap is deprecated. Please use TAG_MAP instead.
|
||||
warnings.warn(f"{attr} is deprecated. Please use {newAttr} instead.", DeprecationWarning)
|
||||
|
||||
tests/api/auth/test_auth_and_registration.py::test_register_success_sends_email_and_user_inactive
|
||||
C:\Users\nadeini\Dev\EUSRR\.venv\Lib\site-packages\pyasn1\codec\ber\encoder.py:952: DeprecationWarning: typeMap is deprecated. Please use TYPE_MAP instead.
|
||||
warnings.warn(f"{attr} is deprecated. Please use {newAttr} instead.", DeprecationWarning)
|
||||
|
||||
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||
=========================== short test summary info ===========================
|
||||
FAILED tests/api/auth/test_auth_and_registration.py::test_register_success_sends_email_and_user_inactive
|
||||
FAILED tests/api/auth/test_auth_and_registration.py::test_register_duplicate_email_phone
|
||||
FAILED tests/api/auth/test_auth_and_registration.py::test_register_accepts_optional_fields
|
||||
FAILED tests/api/auth/test_auth_and_registration.py::test_verify_email_success_activates_user
|
||||
FAILED tests/api/auth/test_auth_and_registration.py::test_verify_email_wrong_code
|
||||
FAILED tests/api/auth/test_auth_and_registration.py::test_resend_email_sends_new_code_and_old_becomes_invalid
|
||||
FAILED tests/api/auth/test_auth_and_registration.py::test_verify_expired_more_than_5_minutes_deletes_account
|
||||
FAILED tests/api/auth/test_auth_and_registration.py::test_login_by_email_denied_before_verify
|
||||
FAILED tests/api/auth/test_auth_and_registration.py::test_login_by_email_allowed_after_verify
|
||||
FAILED tests/api/auth/test_auth_and_registration.py::test_login_by_phone_allowed_after_verify
|
||||
FAILED tests/api/auth/test_auth_and_registration.py::test_login_wrong_password
|
||||
================== 11 failed, 8 passed, 2 warnings in 7.69s ===================
|
||||
@ -1,57 +0,0 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# OpenLDAP сервер для тестирования
|
||||
test-ldap:
|
||||
image: osixia/openldap:1.5.0
|
||||
container_name: eusrr-test-ldap
|
||||
hostname: ldap.test.local
|
||||
environment:
|
||||
# Базовая конфигурация
|
||||
LDAP_ORGANISATION: "EUSRR Test"
|
||||
LDAP_DOMAIN: "test.local"
|
||||
LDAP_BASE_DN: "dc=test,dc=local"
|
||||
LDAP_ADMIN_PASSWORD: "test_change-me-redacted-secret"
|
||||
LDAP_CONFIG_PASSWORD: "test_config_password"
|
||||
LDAP_READONLY_USER: "false"
|
||||
|
||||
# Настройки для совместимости с вашим кодом
|
||||
LDAP_RFC2307BIS_SCHEMA: "true"
|
||||
LDAP_BACKEND: "mdb"
|
||||
LDAP_TLS: "false"
|
||||
LDAP_TLS_VERIFY_CLIENT: "never"
|
||||
|
||||
# Логирование для отладки
|
||||
LDAP_LOG_LEVEL: "256"
|
||||
ports:
|
||||
- "10389:389" # Используем порт 10389 чтобы не конфликтовать с prod
|
||||
- "10636:636" # SSL порт (если понадобится)
|
||||
volumes:
|
||||
- ./ldap_test_data:/var/lib/ldap
|
||||
- ./ldap_test_config:/etc/ldap/slapd.d
|
||||
- ./tests/ldap_fixtures:/container/service/slapd/assets/config/bootstrap/ldif/custom
|
||||
networks:
|
||||
- test-network
|
||||
command: --copy-service --loglevel debug
|
||||
|
||||
# phpLDAPadmin - веб-интерфейс для управления (опционально)
|
||||
ldap-admin:
|
||||
image: osixia/phpldapadmin:0.9.0
|
||||
container_name: eusrr-ldap-admin
|
||||
environment:
|
||||
PHPLDAPADMIN_LDAP_HOSTS: "test-ldap"
|
||||
PHPLDAPADMIN_HTTPS: "false"
|
||||
ports:
|
||||
- "8090:80"
|
||||
depends_on:
|
||||
- test-ldap
|
||||
networks:
|
||||
- test-network
|
||||
|
||||
networks:
|
||||
test-network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
ldap_test_data:
|
||||
ldap_test_config:
|
||||
File diff suppressed because it is too large
Load Diff
@ -1 +0,0 @@
|
||||
bash: .venv/Scripts/python: No such file or directory
|
||||
@ -1,758 +0,0 @@
|
||||
<!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>
|
||||
@ -1,720 +0,0 @@
|
||||
<!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>
|
||||
67
backend/test_logging.py
Normal file
67
backend/test_logging.py
Normal file
@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python
|
||||
"""Тест модернизированной системы логирования."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Добавляем backend в путь
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eusrr_backend.settings')
|
||||
|
||||
import django
|
||||
django.setup()
|
||||
|
||||
import logging
|
||||
|
||||
# Тестируем разные логгеры
|
||||
print("=" * 70)
|
||||
print("ТЕСТ МОДЕРНИЗИРОВАННОЙ СИСТЕМЫ ЛОГИРОВАНИЯ")
|
||||
print("=" * 70)
|
||||
|
||||
# 1. Логгер employees
|
||||
print("\n1. Тест логгера 'employees':")
|
||||
employees_logger = logging.getLogger('employees')
|
||||
employees_logger.debug('Тестовое DEBUG сообщение')
|
||||
employees_logger.info('Тестовое INFO сообщение')
|
||||
employees_logger.warning('Тестовое WARNING сообщение')
|
||||
employees_logger.error('Тестовое ERROR сообщение')
|
||||
|
||||
# 2. Логгер security
|
||||
print("\n2. Тест логгера 'employees.ldap' (security):")
|
||||
ldap_logger = logging.getLogger('employees.ldap')
|
||||
ldap_logger.info('LDAP аутентификация успешна', extra={'username': 'test_user'})
|
||||
ldap_logger.warning('LDAP подключение медленное', extra={'latency': '1000ms'})
|
||||
|
||||
# 3. Логгер с исключением
|
||||
print("\n3. Тест логирования исключений:")
|
||||
try:
|
||||
result = 10 / 0
|
||||
except ZeroDivisionError:
|
||||
employees_logger.exception('Тестовое исключение для демонстрации трассировки')
|
||||
|
||||
# 4. Django логгер
|
||||
print("\n4. Тест Django логгера:")
|
||||
django_logger = logging.getLogger('django')
|
||||
django_logger.info('Django логгер работает')
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print("✓ Все логгеры протестированы!")
|
||||
print("=" * 70)
|
||||
|
||||
# Проверяем созданные файлы
|
||||
logs_dir = os.path.join(os.path.dirname(__file__), 'logs')
|
||||
if os.path.exists(logs_dir):
|
||||
log_files = [f for f in os.listdir(logs_dir) if f.endswith('.log')]
|
||||
print(f"\n📁 Найдено файлов логов: {len(log_files)}")
|
||||
for log_file in sorted(log_files):
|
||||
file_path = os.path.join(logs_dir, log_file)
|
||||
file_size = os.path.getsize(file_path)
|
||||
print(f" - {log_file:20s} ({file_size:,} bytes)")
|
||||
else:
|
||||
print("\n⚠ Директория logs/ не найдена")
|
||||
|
||||
print("\n💡 Проверьте содержимое файлов:")
|
||||
print(" tail -f backend/logs/all.log")
|
||||
print(" tail -f backend/logs/error.log")
|
||||
print(" tail -f backend/logs/security.log")
|
||||
@ -1,61 +0,0 @@
|
||||
@echo off
|
||||
REM Скрипт для проверки функции ответа на сообщение (Windows)
|
||||
REM Запускает сервер и открывает тестовую страницу
|
||||
|
||||
echo.
|
||||
echo 🧪 Проверка функции ответа на сообщение
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
REM Проверка виртуального окружения
|
||||
if not exist ".venv" (
|
||||
echo ❌ Виртуальное окружение не найдено
|
||||
echo Создайте его командой: python -m venv .venv
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo ✅ Виртуальное окружение найдено
|
||||
echo.
|
||||
|
||||
REM Проверка Python в виртуальном окружении
|
||||
if exist ".venv\Scripts\python.exe" (
|
||||
.venv\Scripts\python.exe --version
|
||||
echo ✅ Python найден в виртуальном окружении
|
||||
) else (
|
||||
echo ❌ Python не найден в .venv\Scripts\
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo 📊 Что будет проверено:
|
||||
echo 1. Контекстное меню -^> Ответить
|
||||
echo 2. ChatFormManager (режим reply)
|
||||
echo 3. Превью ответа в сообщениях
|
||||
echo 4. Загрузка реакций (дефолтные)
|
||||
echo.
|
||||
|
||||
echo 🚀 Запуск Django сервера...
|
||||
echo.
|
||||
|
||||
REM Запуск сервера
|
||||
start "Django Server" .venv\Scripts\python.exe manage.py runserver 9000
|
||||
|
||||
echo ⏳ Ожидание запуска сервера (5 сек)...
|
||||
timeout /t 5 /nobreak > nul
|
||||
|
||||
echo.
|
||||
echo ✅ Сервер запущен!
|
||||
echo.
|
||||
echo 📝 Откройте в браузере:
|
||||
echo http://localhost:9000/test_reply_functionality.html
|
||||
echo.
|
||||
echo 🌐 Или нажмите Enter чтобы открыть автоматически...
|
||||
pause
|
||||
|
||||
REM Открыть браузер
|
||||
start http://localhost:9000/test_reply_functionality.html
|
||||
|
||||
echo.
|
||||
echo Закройте окно "Django Server" чтобы остановить сервер
|
||||
echo.
|
||||
pause
|
||||
@ -1,60 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Скрипт для проверки функции ответа на сообщение
|
||||
# Запускает сервер и открывает тестовую страницу
|
||||
|
||||
echo "🧪 Проверка функции ответа на сообщение"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
# Проверка виртуального окружения
|
||||
if [ ! -d ".venv" ]; then
|
||||
echo "❌ Виртуальное окружение не найдено"
|
||||
echo "Создайте его командой: python -m venv .venv"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Виртуальное окружение найдено"
|
||||
|
||||
# Активация виртуального окружения (Windows)
|
||||
if [ -f ".venv/Scripts/activate" ]; then
|
||||
source .venv/Scripts/activate
|
||||
echo "✅ Виртуальное окружение активировано"
|
||||
else
|
||||
echo "⚠️ Не удалось активировать виртуальное окружение"
|
||||
fi
|
||||
|
||||
# Проверка Python
|
||||
python_version=$(python --version 2>&1)
|
||||
echo "Python: $python_version"
|
||||
|
||||
echo ""
|
||||
echo "📊 Что будет проверено:"
|
||||
echo " 1. Контекстное меню -> Ответить"
|
||||
echo " 2. ChatFormManager (режим reply)"
|
||||
echo " 3. Превью ответа в сообщениях"
|
||||
echo " 4. Загрузка реакций (дефолтные)"
|
||||
echo ""
|
||||
|
||||
# Запуск сервера
|
||||
echo "🚀 Запуск Django сервера..."
|
||||
echo ""
|
||||
python manage.py runserver 9000 &
|
||||
SERVER_PID=$!
|
||||
|
||||
echo "Server PID: $SERVER_PID"
|
||||
echo ""
|
||||
echo "⏳ Ожидание запуска сервера (5 сек)..."
|
||||
sleep 5
|
||||
|
||||
echo ""
|
||||
echo "✅ Сервер запущен!"
|
||||
echo ""
|
||||
echo "📝 Откройте в браузере:"
|
||||
echo " http://localhost:9000/test_reply_functionality.html"
|
||||
echo ""
|
||||
echo "Нажмите Ctrl+C для остановки сервера"
|
||||
echo ""
|
||||
|
||||
# Ожидание завершения
|
||||
wait $SERVER_PID
|
||||
@ -1,344 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Тест функции ответа на сообщение</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
|
||||
<style>
|
||||
body {
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.test-section {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
.message-preview {
|
||||
background: #e3f2fd;
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.bubble {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
padding: 12px 16px;
|
||||
max-width: 400px;
|
||||
}
|
||||
.reply-indicator {
|
||||
background: #fff3cd;
|
||||
border-left: 4px solid #ffc107;
|
||||
padding: 12px;
|
||||
margin: 10px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.log-output {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.log-entry {
|
||||
margin: 4px 0;
|
||||
padding: 4px;
|
||||
}
|
||||
.log-entry.info { color: #0d6efd; }
|
||||
.log-entry.warn { color: #ffc107; }
|
||||
.log-entry.error { color: #dc3545; }
|
||||
.log-entry.success { color: #198754; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1 class="mb-4">🧪 Тест функции ответа на сообщение</h1>
|
||||
|
||||
<!-- Статус загрузки реакций -->
|
||||
<div class="test-section">
|
||||
<h3>📊 Статус загрузки реакций</h3>
|
||||
<div id="reactionsStatus" class="alert alert-info">
|
||||
Проверка...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Тест 1: Контекстное меню -->
|
||||
<div class="test-section">
|
||||
<h3>1️⃣ Тест контекстного меню</h3>
|
||||
<p>Симуляция вызова функции ответа из контекстного меню</p>
|
||||
|
||||
<div class="message-preview" data-message-id="123">
|
||||
<div class="small text-secondary mb-2">
|
||||
Иван Петров · 10:30
|
||||
</div>
|
||||
<div class="bubble">
|
||||
Привет! Как дела с проектом?
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="testReplyBtn" class="btn btn-primary">
|
||||
<i class="bi bi-reply-fill"></i> Ответить (через контекстное меню)
|
||||
</button>
|
||||
|
||||
<div id="testReplyResult" class="mt-3"></div>
|
||||
</div>
|
||||
|
||||
<!-- Тест 2: ChatFormManager -->
|
||||
<div class="test-section">
|
||||
<h3>2️⃣ Тест ChatFormManager</h3>
|
||||
<p>Проверка состояния формы при ответе</p>
|
||||
|
||||
<form id="chatForm" action="/upload/" method="POST">
|
||||
<input type="hidden" name="chat_id" value="1">
|
||||
|
||||
<div class="mb-3">
|
||||
<textarea id="id_content" class="form-control" rows="3" placeholder="Напишите сообщение..."></textarea>
|
||||
</div>
|
||||
|
||||
<div id="replyIndicatorContainer"></div>
|
||||
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="bi bi-send-fill"></i> Отправить
|
||||
</button>
|
||||
<button type="button" id="cancelBtn" class="btn btn-secondary">
|
||||
Отмена
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div id="formState" class="mt-3 p-3 bg-light border rounded">
|
||||
<strong>Состояние формы:</strong>
|
||||
<pre id="formStateJson">{ "mode": "send" }</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Тест 3: Превью ответа в сообщении -->
|
||||
<div class="test-section">
|
||||
<h3>3️⃣ Тест превью ответа в сообщении</h3>
|
||||
<p>Проверка рендеринга превью при ответе на сообщение</p>
|
||||
|
||||
<div class="message-preview">
|
||||
<div class="message-reply-preview border-start border-3 border-primary ps-2 mb-2 ms-2 rounded"
|
||||
data-reply-to-id="100"
|
||||
style="background-color: rgba(13, 110, 253, 0.05); padding: 6px 8px; cursor: pointer;">
|
||||
<div class="small fw-semibold text-primary">
|
||||
<i class="bi bi-reply-fill me-1"></i>
|
||||
Мария Иванова
|
||||
</div>
|
||||
<div class="small text-muted text-truncate" style="max-width: 400px;">
|
||||
Давайте встретимся завтра
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bubble">
|
||||
Отлично, в какое время удобно?
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="testPreviewClick" class="btn btn-primary">
|
||||
<i class="bi bi-cursor-fill"></i> Клик по превью (переход к сообщению)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Логи -->
|
||||
<div class="test-section">
|
||||
<h3>📝 Журнал событий</h3>
|
||||
<button id="clearLogs" class="btn btn-sm btn-outline-secondary mb-2">Очистить</button>
|
||||
<div id="logOutput" class="log-output"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модули -->
|
||||
<script type="module">
|
||||
import { initChatFormManager } from './static/js/components/chatFormManager.js';
|
||||
import MessageContextMenu from './static/js/components/messageContextMenu.js';
|
||||
import reactionsConfig from './static/js/config/reactionsConfig.js';
|
||||
|
||||
// Логирование
|
||||
const logOutput = document.getElementById('logOutput');
|
||||
|
||||
function addLog(message, type = 'info') {
|
||||
const entry = document.createElement('div');
|
||||
entry.className = `log-entry ${type}`;
|
||||
entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
|
||||
logOutput.appendChild(entry);
|
||||
logOutput.scrollTop = logOutput.scrollHeight;
|
||||
}
|
||||
|
||||
document.getElementById('clearLogs').addEventListener('click', () => {
|
||||
logOutput.innerHTML = '';
|
||||
addLog('Логи очищены', 'info');
|
||||
});
|
||||
|
||||
// Проверка загрузки реакций
|
||||
async function checkReactions() {
|
||||
addLog('Проверка загрузки реакций...', 'info');
|
||||
|
||||
try {
|
||||
await reactionsConfig.load();
|
||||
const emojis = reactionsConfig.getEmojis();
|
||||
|
||||
const statusEl = document.getElementById('reactionsStatus');
|
||||
|
||||
if (emojis && emojis.length > 0) {
|
||||
statusEl.className = 'alert alert-success';
|
||||
statusEl.innerHTML = `
|
||||
<h5>✅ Реакции загружены успешно</h5>
|
||||
<p>Всего доступно: <strong>${emojis.length}</strong> реакций</p>
|
||||
<p>Эмодзи: ${emojis.join(' ')}</p>
|
||||
`;
|
||||
addLog(`✓ Загружено ${emojis.length} реакций: ${emojis.join(' ')}`, 'success');
|
||||
} else {
|
||||
statusEl.className = 'alert alert-warning';
|
||||
statusEl.innerHTML = `
|
||||
<h5>⚠️ Используются дефолтные реакции</h5>
|
||||
<p>API эндпоинт не доступен, используются реакции по умолчанию</p>
|
||||
`;
|
||||
addLog('⚠ Используются дефолтные реакции (API недоступен)', 'warn');
|
||||
}
|
||||
} catch (error) {
|
||||
const statusEl = document.getElementById('reactionsStatus');
|
||||
statusEl.className = 'alert alert-danger';
|
||||
statusEl.innerHTML = `
|
||||
<h5>❌ Ошибка загрузки реакций</h5>
|
||||
<p>${error.message}</p>
|
||||
`;
|
||||
addLog(`✗ Ошибка: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Инициализация ChatFormManager
|
||||
addLog('Инициализация ChatFormManager...', 'info');
|
||||
const chatFormManager = initChatFormManager({
|
||||
formId: 'chatForm',
|
||||
textareaId: 'id_content',
|
||||
chatId: 1,
|
||||
uploadUrl: '/api/v1/communications/messages/upload/',
|
||||
editUrlTemplate: '/api/v1/communications/messages/{id}/'
|
||||
});
|
||||
|
||||
if (chatFormManager) {
|
||||
window.chatFormManager = chatFormManager;
|
||||
addLog('✓ ChatFormManager инициализирован', 'success');
|
||||
} else {
|
||||
addLog('✗ Ошибка инициализации ChatFormManager', 'error');
|
||||
}
|
||||
|
||||
// Обновление отображения состояния формы
|
||||
function updateFormState() {
|
||||
if (!chatFormManager) return;
|
||||
|
||||
const state = chatFormManager.getFormData();
|
||||
document.getElementById('formStateJson').textContent = JSON.stringify(state, null, 2);
|
||||
}
|
||||
|
||||
// Тест 1: Ответ через контекстное меню
|
||||
document.getElementById('testReplyBtn').addEventListener('click', () => {
|
||||
addLog('Тест: Вызов handleReply из контекстного меню', 'info');
|
||||
|
||||
const messageEl = document.querySelector('[data-message-id="123"]');
|
||||
|
||||
if (!chatFormManager) {
|
||||
addLog('✗ ChatFormManager не доступен', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Симуляция логики MessageContextMenu.handleReply
|
||||
const bubble = messageEl.querySelector('.bubble');
|
||||
const messageText = bubble.textContent.trim();
|
||||
const authorName = 'Иван Петров';
|
||||
const messageId = messageEl.dataset.messageId;
|
||||
|
||||
addLog(`handleReply вызван с: ID=${messageId}, Автор="${authorName}"`, 'info');
|
||||
|
||||
chatFormManager.setModeToReply(messageId, authorName, messageText);
|
||||
addLog('✓ Режим ответа активирован', 'success');
|
||||
|
||||
updateFormState();
|
||||
|
||||
// Показываем результат
|
||||
const resultDiv = document.getElementById('testReplyResult');
|
||||
resultDiv.innerHTML = `
|
||||
<div class="alert alert-success">
|
||||
<h5>✅ Тест пройден!</h5>
|
||||
<ul>
|
||||
<li>Режим формы: <strong>reply</strong></li>
|
||||
<li>ID сообщения: <strong>${messageId}</strong></li>
|
||||
<li>Placeholder обновлен: "${document.getElementById('id_content').placeholder}"</li>
|
||||
<li>Hidden input reply_to добавлен: ${document.querySelector('input[name="reply_to"]') ? 'Да' : 'Нет'}</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
} catch (error) {
|
||||
addLog(`✗ Ошибка в handleReply: ${error.message}`, 'error');
|
||||
|
||||
const resultDiv = document.getElementById('testReplyResult');
|
||||
resultDiv.innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
<h5>❌ Тест провален</h5>
|
||||
<p>${error.message}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
// Отмена
|
||||
document.getElementById('cancelBtn').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
addLog('Отмена действия (возврат в режим send)', 'info');
|
||||
|
||||
if (chatFormManager) {
|
||||
chatFormManager.cancel();
|
||||
addLog('✓ Режим send восстановлен', 'success');
|
||||
updateFormState();
|
||||
}
|
||||
});
|
||||
|
||||
// Тест 3: Клик по превью
|
||||
document.getElementById('testPreviewClick').addEventListener('click', () => {
|
||||
const preview = document.querySelector('.message-reply-preview');
|
||||
const replyToId = preview.dataset.replyToId;
|
||||
|
||||
addLog(`Клик по превью ответа, ID оригинального сообщения: ${replyToId}`, 'info');
|
||||
addLog('В реальной ситуации произойдет переход к сообщению #' + replyToId, 'info');
|
||||
|
||||
// В реальности здесь будет вызов messageLoader.loadAroundMessage()
|
||||
alert(`Переход к сообщению #${replyToId}\n\nВ реальной реализации вызывается:\nmessageLoader.loadAroundMessage(${replyToId})`);
|
||||
});
|
||||
|
||||
// Мониторинг изменений в форме
|
||||
document.getElementById('chatForm').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
addLog('=== ОТПРАВКА ФОРМЫ ===', 'info');
|
||||
const formData = new FormData(e.target);
|
||||
|
||||
for (let [key, value] of formData.entries()) {
|
||||
addLog(` ${key}: ${value}`, 'info');
|
||||
}
|
||||
|
||||
const state = chatFormManager.getFormData();
|
||||
addLog(`Режим: ${state.mode}`, 'info');
|
||||
if (state.replyToMessageId) {
|
||||
addLog(`Reply To: ${state.replyToMessageId}`, 'success');
|
||||
}
|
||||
|
||||
addLog('=== КОНЕЦ ===', 'info');
|
||||
});
|
||||
|
||||
// Запуск проверки реакций
|
||||
checkReactions();
|
||||
addLog('Тестовая страница загружена', 'success');
|
||||
updateFormState();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,313 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Test ChatControllerV2 - Smart Autoscroll</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#chat-container {
|
||||
border: 1px solid #ccc;
|
||||
height: 400px;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: 8px 12px;
|
||||
margin: 5px 0;
|
||||
border-radius: 8px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.message.own {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
margin-left: auto;
|
||||
max-width: 70%;
|
||||
}
|
||||
|
||||
.message.other {
|
||||
background: #e9ecef;
|
||||
max-width: 70%;
|
||||
}
|
||||
|
||||
.new-messages-indicator {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.new-messages-indicator.show {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.new-messages-indicator .badge {
|
||||
background: rgba(255,255,255,0.3);
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.test-results {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.test-pass {
|
||||
color: green;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.test-fail {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.log {
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
background: #222;
|
||||
color: #0f0;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>ChatControllerV2 - Smart Autoscroll Test</h1>
|
||||
|
||||
<div class="controls">
|
||||
<button onclick="testAddOwnMessage()">Добавить своё сообщение</button>
|
||||
<button onclick="testAddOtherMessage()">Добавить чужое сообщение</button>
|
||||
<button onclick="testScrollUp()">Прокрутить вверх</button>
|
||||
<button onclick="testScrollDown()">Прокрутить вниз</button>
|
||||
<button onclick="runAllTests()">Запустить все тесты</button>
|
||||
</div>
|
||||
|
||||
<div id="chat-container"></div>
|
||||
|
||||
<div class="test-results">
|
||||
<h3>Результаты тестов:</h3>
|
||||
<div id="test-output">Нажмите "Запустить все тесты"</div>
|
||||
</div>
|
||||
|
||||
<div class="log" id="log"></div>
|
||||
|
||||
<script type="module">
|
||||
import { ChatControllerV2 } from './static/js/controllers/chatControllerV2.js';
|
||||
import { MessageStoreV2 } from './static/js/stores/messageStoreV2.js';
|
||||
|
||||
// Логирование
|
||||
const log = (msg) => {
|
||||
const logEl = document.getElementById('log');
|
||||
logEl.textContent += `[${new Date().toLocaleTimeString()}] ${msg}\n`;
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
console.log(msg);
|
||||
};
|
||||
|
||||
// Инициализация
|
||||
let controller;
|
||||
let messageId = 1;
|
||||
const currentUserId = 1;
|
||||
const otherUserId = 2;
|
||||
|
||||
async function init() {
|
||||
const scrollEl = document.getElementById('chat-container');
|
||||
|
||||
controller = new ChatControllerV2({
|
||||
chatId: 1,
|
||||
scrollElement: scrollEl,
|
||||
containerId: 'chat-container',
|
||||
currentUserId: currentUserId
|
||||
});
|
||||
|
||||
// Добавляем несколько начальных сообщений
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const msg = {
|
||||
id: messageId++,
|
||||
chat_id: 1,
|
||||
author_id: i % 2 === 0 ? currentUserId : otherUserId,
|
||||
content: `Тестовое сообщение ${i + 1}`,
|
||||
created_at: new Date().toISOString(),
|
||||
author_name: i % 2 === 0 ? 'Я' : 'Собеседник'
|
||||
};
|
||||
|
||||
controller.store.addMessage(msg);
|
||||
}
|
||||
|
||||
await controller.init();
|
||||
log('✅ ChatControllerV2 инициализирован');
|
||||
}
|
||||
|
||||
// Тестовые функции
|
||||
window.testAddOwnMessage = async function() {
|
||||
const msg = {
|
||||
id: messageId++,
|
||||
chat_id: 1,
|
||||
author_id: currentUserId,
|
||||
content: `Моё сообщение ${messageId}`,
|
||||
created_at: new Date().toISOString(),
|
||||
author_name: 'Я'
|
||||
};
|
||||
|
||||
controller.store.addMessage(msg);
|
||||
log(`📤 Добавлено своё сообщение #${msg.id}`);
|
||||
};
|
||||
|
||||
window.testAddOtherMessage = async function() {
|
||||
const msg = {
|
||||
id: messageId++,
|
||||
chat_id: 1,
|
||||
author_id: otherUserId,
|
||||
content: `Чужое сообщение ${messageId}`,
|
||||
created_at: new Date().toISOString(),
|
||||
author_name: 'Собеседник'
|
||||
};
|
||||
|
||||
controller.store.addMessage(msg);
|
||||
log(`📥 Добавлено чужое сообщение #${msg.id}`);
|
||||
};
|
||||
|
||||
window.testScrollUp = function() {
|
||||
const scrollEl = document.getElementById('chat-container');
|
||||
scrollEl.scrollTop = 0;
|
||||
log('⬆️ Прокручено вверх');
|
||||
};
|
||||
|
||||
window.testScrollDown = function() {
|
||||
const scrollEl = document.getElementById('chat-container');
|
||||
scrollEl.scrollTop = scrollEl.scrollHeight;
|
||||
log('⬇️ Прокручено вниз');
|
||||
};
|
||||
|
||||
// Автотесты
|
||||
window.runAllTests = async function() {
|
||||
const output = document.getElementById('test-output');
|
||||
output.innerHTML = '<p>Запуск тестов...</p>';
|
||||
|
||||
const results = [];
|
||||
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
||||
|
||||
// Тест 1: Своё сообщение - автоскролл вниз
|
||||
log('🧪 Тест 1: Своё сообщение → автоскролл');
|
||||
testScrollDown();
|
||||
await sleep(200);
|
||||
const oldScroll = controller.scrollElement.scrollTop;
|
||||
testAddOwnMessage();
|
||||
await sleep(500);
|
||||
const newScroll = controller.scrollElement.scrollTop;
|
||||
const test1 = newScroll > oldScroll;
|
||||
results.push({ name: 'Своё сообщение автоскроллит', pass: test1 });
|
||||
log(test1 ? '✅ PASS' : '❌ FAIL');
|
||||
|
||||
// Тест 2: Чужое сообщение внизу - автоскролл
|
||||
log('🧪 Тест 2: Чужое сообщение внизу → автоскролл');
|
||||
testScrollDown();
|
||||
await sleep(200);
|
||||
const oldScroll2 = controller.scrollElement.scrollTop;
|
||||
testAddOtherMessage();
|
||||
await sleep(500);
|
||||
const newScroll2 = controller.scrollElement.scrollTop;
|
||||
const test2 = newScroll2 > oldScroll2;
|
||||
results.push({ name: 'Чужое сообщение внизу автоскроллит', pass: test2 });
|
||||
log(test2 ? '✅ PASS' : '❌ FAIL');
|
||||
|
||||
// Тест 3: Чужое сообщение вверху - индикатор
|
||||
log('🧪 Тест 3: Чужое сообщение вверху → индикатор');
|
||||
testScrollUp();
|
||||
await sleep(200);
|
||||
testAddOtherMessage();
|
||||
await sleep(500);
|
||||
const indicator = document.getElementById('new-messages-btn');
|
||||
const test3 = indicator && indicator.style.display !== 'none';
|
||||
results.push({ name: 'Индикатор показывается', pass: test3 });
|
||||
log(test3 ? '✅ PASS' : '❌ FAIL');
|
||||
|
||||
// Тест 4: Счётчик увеличивается
|
||||
log('🧪 Тест 4: Счётчик увеличивается');
|
||||
const countBefore = controller._newMessagesCount;
|
||||
testAddOtherMessage();
|
||||
await sleep(500);
|
||||
const countAfter = controller._newMessagesCount;
|
||||
const test4 = countAfter > countBefore;
|
||||
results.push({ name: 'Счётчик увеличивается', pass: test4 });
|
||||
log(test4 ? '✅ PASS' : '❌ FAIL');
|
||||
|
||||
// Тест 5: Клик по индикатору → скролл вниз + скрытие
|
||||
log('🧪 Тест 5: Клик по индикатору → скролл + скрытие');
|
||||
if (indicator) {
|
||||
indicator.click();
|
||||
await sleep(500);
|
||||
const isHidden = indicator.style.display === 'none';
|
||||
const isBottom = controller.scrollManager.isNearBottom();
|
||||
const test5 = isHidden && isBottom;
|
||||
results.push({ name: 'Клик работает корректно', pass: test5 });
|
||||
log(test5 ? '✅ PASS' : '❌ FAIL');
|
||||
}
|
||||
|
||||
// Вывод результатов
|
||||
const passed = results.filter(r => r.pass).length;
|
||||
const total = results.length;
|
||||
const passRate = ((passed / total) * 100).toFixed(1);
|
||||
|
||||
let html = `<h4>Результаты: ${passed}/${total} (${passRate}%)</h4><ul>`;
|
||||
results.forEach(r => {
|
||||
html += `<li class="${r.pass ? 'test-pass' : 'test-fail'}">${r.pass ? '✅' : '❌'} ${r.name}</li>`;
|
||||
});
|
||||
html += '</ul>';
|
||||
|
||||
output.innerHTML = html;
|
||||
log(`\n📊 Итого: ${passed}/${total} тестов пройдено (${passRate}%)`);
|
||||
};
|
||||
|
||||
// Автозапуск
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
x
Reference in New Issue
Block a user