Implements offline-first calendar sync infrastructure
Adds IndexedDB and operation queue for robust offline synchronization Introduces SyncManager to handle background data synchronization Supports local event operations with automatic remote sync queuing Enhances application reliability and user experience in low/no connectivity scenarios
This commit is contained in:
parent
9c765b35ab
commit
e7011526e3
20 changed files with 3822 additions and 57 deletions
130
test/integrationtesting/README.md
Normal file
130
test/integrationtesting/README.md
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
# Integration Testing
|
||||
|
||||
Denne folder indeholder integration test pages til offline-first calendar funktionalitet.
|
||||
|
||||
## Test Filer
|
||||
|
||||
### Test Pages
|
||||
- **`offline-test.html`** - Interaktiv CRUD testing playground
|
||||
- **`sync-visualization.html`** - Live monitoring af sync queue og IndexedDB
|
||||
|
||||
### Data & Scripts
|
||||
- **`test-events.json`** - 10 test events til seeding af IndexedDB
|
||||
- **`test-init.js`** - Standalone initialisering af IndexedDB, queue, event manager og sync manager
|
||||
|
||||
## Sådan Bruges Test Siderne
|
||||
|
||||
### 1. Start Development Server
|
||||
Test siderne skal køres via en web server (ikke file://) for at kunne loade test-events.json:
|
||||
|
||||
```bash
|
||||
# Fra root af projektet
|
||||
npm run dev
|
||||
# eller
|
||||
npx http-server -p 8080
|
||||
```
|
||||
|
||||
### 2. Åbn Test Siderne
|
||||
Naviger til:
|
||||
- `http://localhost:8080/test/integrationtesting/offline-test.html`
|
||||
- `http://localhost:8080/test/integrationtesting/sync-visualization.html`
|
||||
|
||||
### 3. Test Offline Mode
|
||||
1. Åbn DevTools (F12)
|
||||
2. Gå til Network tab
|
||||
3. Aktiver "Offline" mode
|
||||
4. Test CRUD operationer - de skulle gemmes lokalt i IndexedDB
|
||||
5. Deaktiver "Offline" mode
|
||||
6. Observer sync queue blive processeret
|
||||
|
||||
## Test Pages Detaljer
|
||||
|
||||
### offline-test.html
|
||||
Interaktiv testing af:
|
||||
- ✅ Create timed events
|
||||
- ✅ Create all-day events
|
||||
- ✅ Update event title
|
||||
- ✅ Toggle all-day status
|
||||
- ✅ Delete events
|
||||
- ✅ List all events
|
||||
- ✅ Show operation queue
|
||||
- ✅ Trigger manual sync
|
||||
- ✅ Clear all data
|
||||
|
||||
### sync-visualization.html
|
||||
Live monitoring af:
|
||||
- 📊 IndexedDB events med sync status badges
|
||||
- 📊 Operation queue med retry counts
|
||||
- 📊 Statistics (synced/pending/error counts)
|
||||
- 📊 Real-time sync log
|
||||
- 🔄 Auto-refresh hver 2 sekunder
|
||||
- ⏱️ Last sync timestamp i status bar
|
||||
|
||||
## Teknisk Implementation
|
||||
|
||||
### test-init.js
|
||||
Standalone JavaScript fil der initialiserer:
|
||||
|
||||
```javascript
|
||||
window.calendarDebug = {
|
||||
indexedDB, // TestIndexedDBService instance
|
||||
queue, // TestOperationQueue instance
|
||||
eventManager, // TestEventManager instance
|
||||
syncManager // TestSyncManager instance
|
||||
}
|
||||
```
|
||||
|
||||
**Forskel fra main app:**
|
||||
- Ingen NovaDI dependency injection
|
||||
- Ingen DOM afhængigheder (swp-calendar-container etc.)
|
||||
- Simplified event manager uden event bus
|
||||
- Mock sync manager med simuleret API logic (80% success, 20% failure rate)
|
||||
- Auto-seed fra test-events.json hvis IndexedDB er tom
|
||||
- Pending events fra seed får automatisk queue operations
|
||||
|
||||
**TestSyncManager Behavior:**
|
||||
- ✅ Tjekker `navigator.onLine` før sync (respekterer offline mode)
|
||||
- ✅ Simulerer netværk delay (100-500ms per operation)
|
||||
- ✅ 80% chance for success → fjerner fra queue, markerer som 'synced'
|
||||
- ✅ 20% chance for failure → incrementerer retryCount
|
||||
- ✅ Efter 5 fejl → markerer event som 'error' og fjerner fra queue
|
||||
- ✅ Viser detaljeret logging i console
|
||||
- ✅ Network listeners opdaterer online/offline status automatisk
|
||||
|
||||
### Data Flow
|
||||
```
|
||||
User Action → EventManager
|
||||
→ IndexedDB (saveEvent)
|
||||
→ OperationQueue (enqueue)
|
||||
→ SyncManager (background sync når online)
|
||||
```
|
||||
|
||||
### Database Isolation
|
||||
Test-siderne bruger **`CalendarDB_Test`** som database navn, mens main calendar app bruger **`CalendarDB`**. Dette sikrer at test data IKKE blandes med produktions data. De to systemer er helt isolerede fra hinanden.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Calendar system failed to initialize"
|
||||
- Kontroller at du kører via web server (ikke file://)
|
||||
- Check browser console for fejl
|
||||
- Verificer at test-init.js loades korrekt
|
||||
|
||||
### "Could not load test-events.json"
|
||||
- Normal warning hvis IndexedDB allerede har data
|
||||
- For at reset: Open DevTools → Application → IndexedDB → Delete CalendarDB
|
||||
|
||||
### Events forsvinder efter refresh
|
||||
- Dette skulle IKKE ske - IndexedDB persisterer data
|
||||
- Hvis det sker: Check console for IndexedDB errors
|
||||
- Verificer at browser ikke er i private/incognito mode
|
||||
|
||||
### Test events vises i prod calendar
|
||||
- Test-siderne bruger `CalendarDB_Test` database
|
||||
- Main calendar bruger `CalendarDB` database
|
||||
- Hvis de blandes: Clear begge databases i DevTools → Application → IndexedDB
|
||||
|
||||
## Development Notes
|
||||
|
||||
Test siderne bruger IKKE den compiled calendar.js bundle. De er helt standalone og initialiserer deres egne services direkte. Dette gør dem hurtigere at udvikle på og lettere at debugge.
|
||||
|
||||
Når API backend implementeres skal `TestSyncManager` opdateres til at lave rigtige HTTP calls i stedet for mock sync.
|
||||
974
test/integrationtesting/offline-test.html
Normal file
974
test/integrationtesting/offline-test.html
Normal file
|
|
@ -0,0 +1,974 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="da">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>OFFLINE MODE TESTING | Calendar System</title>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap');
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg-primary: #f8f9fa;
|
||||
--bg-secondary: #ffffff;
|
||||
--bg-tertiary: #f1f3f5;
|
||||
--border-color: #dee2e6;
|
||||
--text-primary: #212529;
|
||||
--text-secondary: #495057;
|
||||
--text-muted: #6c757d;
|
||||
--accent-primary: #0066cc;
|
||||
--accent-secondary: #6610f2;
|
||||
--success: #28a745;
|
||||
--warning: #ffc107;
|
||||
--error: #dc3545;
|
||||
--info: #17a2b8;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
padding: 20px;
|
||||
line-height: 1.6;
|
||||
font-size: 13px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: var(--bg-secondary);
|
||||
padding: 24px;
|
||||
border: 2px solid var(--border-color);
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 6px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1::before {
|
||||
content: '▶ ';
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
margin-bottom: 20px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.network-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 6px 14px;
|
||||
border: 2px solid;
|
||||
font-weight: 600;
|
||||
margin-bottom: 20px;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.network-online {
|
||||
background: #d4edda;
|
||||
color: var(--success);
|
||||
border-color: var(--success);
|
||||
}
|
||||
|
||||
.network-offline {
|
||||
background: #f8d7da;
|
||||
color: var(--error);
|
||||
border-color: var(--error);
|
||||
}
|
||||
|
||||
.test-section {
|
||||
background: var(--bg-secondary);
|
||||
padding: 20px;
|
||||
border: 2px solid var(--border-color);
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.test-section::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, transparent, var(--accent-primary), transparent);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: var(--accent-primary);
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
}
|
||||
|
||||
.test-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.test-card {
|
||||
border: 2px solid var(--border-color);
|
||||
padding: 16px;
|
||||
background: var(--bg-tertiary);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.test-card:hover {
|
||||
border-color: var(--accent-primary);
|
||||
box-shadow: 0 4px 12px rgba(0, 102, 204, 0.15);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 8px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.card-description {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 16px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 6px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
input, select, textarea {
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
border: 2px solid var(--border-color);
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
font-size: 12px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
input:focus, select:focus, textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-primary);
|
||||
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
border: 2px solid var(--border-color);
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
transition: all 0.15s ease;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
button:hover:not(:disabled) {
|
||||
border-color: var(--accent-primary);
|
||||
color: var(--accent-primary);
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
button:active:not(:disabled) {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-create {
|
||||
border-color: var(--success);
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.btn-create:hover:not(:disabled) {
|
||||
background: #d4edda;
|
||||
}
|
||||
|
||||
.btn-update {
|
||||
border-color: var(--info);
|
||||
color: var(--info);
|
||||
}
|
||||
|
||||
.btn-update:hover:not(:disabled) {
|
||||
background: #d1ecf1;
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
border-color: var(--error);
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.btn-delete:hover:not(:disabled) {
|
||||
background: #f8d7da;
|
||||
}
|
||||
|
||||
.btn-utility {
|
||||
border-color: var(--accent-secondary);
|
||||
color: var(--accent-secondary);
|
||||
}
|
||||
|
||||
.btn-utility:hover:not(:disabled) {
|
||||
background: #e7d8ff;
|
||||
}
|
||||
|
||||
.result-box {
|
||||
background: var(--bg-tertiary);
|
||||
border-left: 3px solid var(--accent-primary);
|
||||
padding: 12px;
|
||||
margin-top: 12px;
|
||||
font-size: 11px;
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
line-height: 1.6;
|
||||
border: 2px solid var(--border-color);
|
||||
border-left: 3px solid var(--accent-primary);
|
||||
}
|
||||
|
||||
.result-success {
|
||||
border-left-color: var(--success);
|
||||
background: #d4edda;
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.result-error {
|
||||
border-left-color: var(--error);
|
||||
background: #f8d7da;
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.result-info {
|
||||
border-left-color: var(--info);
|
||||
background: #d1ecf1;
|
||||
color: var(--info);
|
||||
}
|
||||
|
||||
.instructions {
|
||||
background: #fff3cd;
|
||||
border-left: 3px solid var(--warning);
|
||||
padding: 16px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 11px;
|
||||
border: 2px solid #ffeeba;
|
||||
border-left: 3px solid var(--warning);
|
||||
}
|
||||
|
||||
.instructions h3 {
|
||||
color: #856404;
|
||||
margin-bottom: 10px;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.instructions ol {
|
||||
margin-left: 20px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.instructions a {
|
||||
color: var(--accent-primary);
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid transparent;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.instructions a:hover {
|
||||
border-bottom-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.quick-actions button {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
code {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--accent-primary);
|
||||
padding: 2px 6px;
|
||||
border: 1px solid var(--border-color);
|
||||
font-size: 11px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 3px 10px;
|
||||
border: 2px solid;
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background: #d4edda;
|
||||
color: var(--success);
|
||||
border-color: var(--success);
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
border-color: var(--warning);
|
||||
}
|
||||
|
||||
.badge-error {
|
||||
background: #f8d7da;
|
||||
color: var(--error);
|
||||
border-color: var(--error);
|
||||
}
|
||||
|
||||
.event-preview {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.event-preview-item {
|
||||
padding: 12px;
|
||||
border: 2px solid var(--border-color);
|
||||
background: var(--bg-tertiary);
|
||||
font-size: 11px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.event-preview-item:hover {
|
||||
border-color: var(--accent-primary);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0, 102, 204, 0.1);
|
||||
}
|
||||
|
||||
.event-preview-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 6px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.event-preview-id {
|
||||
font-size: 9px;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 6px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.event-preview-status {
|
||||
font-size: 10px;
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>OFFLINE MODE TESTING</h1>
|
||||
<p class="subtitle">// Interactive testing playground for offline-first calendar functionality</p>
|
||||
<div id="initStatus" class="network-status" style="background: var(--warning); color: #000; display: block; margin-bottom: 8px;">
|
||||
[⏳] INITIALIZING CALENDAR SYSTEM...
|
||||
</div>
|
||||
<div id="networkStatus" class="network-status network-online">
|
||||
[●] NETWORK: ONLINE
|
||||
</div>
|
||||
|
||||
<div class="instructions">
|
||||
<h3>TESTING PROTOCOL</h3>
|
||||
<ol>
|
||||
<li>Perform CRUD operations below (create, update, delete events)</li>
|
||||
<li>Open DevTools → Network tab → Check "Offline" to simulate offline mode</li>
|
||||
<li>Continue performing operations → they will be queued</li>
|
||||
<li>Open <a href="/test/integrationtesting/sync-visualization.html" target="_blank">Sync Visualization</a> to monitor the queue</li>
|
||||
<li>Uncheck "Offline" to go back online → operations will sync automatically</li>
|
||||
<li>Press F5 while offline → verify data persists from IndexedDB</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create Event -->
|
||||
<div class="test-section">
|
||||
<div class="section-title">CREATE OPERATIONS</div>
|
||||
<div class="test-grid">
|
||||
<div class="test-card">
|
||||
<div class="card-title">Create Timed Event</div>
|
||||
<div class="card-description">// Creates a new timed event in the calendar</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Title</label>
|
||||
<input type="text" id="createTitle" placeholder="Team Meeting" value="Test Event">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Start Time</label>
|
||||
<input type="datetime-local" id="createStart">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>End Time</label>
|
||||
<input type="datetime-local" id="createEnd">
|
||||
</div>
|
||||
|
||||
<button class="btn-create" onclick="createTimedEvent()">CREATE TIMED EVENT</button>
|
||||
<div id="createResult"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-card">
|
||||
<div class="card-title">Create All-Day Event</div>
|
||||
<div class="card-description">// Creates a new all-day event</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Title</label>
|
||||
<input type="text" id="createAllDayTitle" placeholder="Holiday" value="All-Day Test">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Date</label>
|
||||
<input type="date" id="createAllDayDate">
|
||||
</div>
|
||||
|
||||
<button class="btn-create" onclick="createAllDayEvent()">CREATE ALL-DAY EVENT</button>
|
||||
<div id="createAllDayResult"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Update Event -->
|
||||
<div class="test-section">
|
||||
<div class="section-title">UPDATE OPERATIONS</div>
|
||||
<div class="test-grid">
|
||||
<div class="test-card">
|
||||
<div class="card-title">Update Event Title</div>
|
||||
<div class="card-description">// Update the title of an existing event</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Event ID</label>
|
||||
<input type="text" id="updateEventId" placeholder="event_123456">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>New Title</label>
|
||||
<input type="text" id="updateTitle" placeholder="Updated Meeting">
|
||||
</div>
|
||||
|
||||
<button class="btn-update" onclick="updateEventTitle()">UPDATE TITLE</button>
|
||||
<div id="updateTitleResult"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-card">
|
||||
<div class="card-title">Toggle All-Day Status</div>
|
||||
<div class="card-description">// Convert between timed and all-day event</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Event ID</label>
|
||||
<input type="text" id="toggleEventId" placeholder="event_123456">
|
||||
</div>
|
||||
|
||||
<button class="btn-update" onclick="toggleAllDay()">TOGGLE ALL-DAY</button>
|
||||
<div id="toggleResult"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Event -->
|
||||
<div class="test-section">
|
||||
<div class="section-title">DELETE OPERATIONS</div>
|
||||
<div class="test-grid">
|
||||
<div class="test-card">
|
||||
<div class="card-title">Delete by ID</div>
|
||||
<div class="card-description">// Permanently delete an event</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Event ID</label>
|
||||
<input type="text" id="deleteEventId" placeholder="event_123456">
|
||||
</div>
|
||||
|
||||
<button class="btn-delete" onclick="deleteEvent()">DELETE EVENT</button>
|
||||
<div id="deleteResult"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Utilities -->
|
||||
<div class="test-section">
|
||||
<div class="section-title">UTILITY OPERATIONS</div>
|
||||
|
||||
<div class="quick-actions">
|
||||
<button class="btn-utility" onclick="listAllEvents()">LIST ALL EVENTS</button>
|
||||
<button class="btn-utility" onclick="showQueue()">SHOW QUEUE</button>
|
||||
<button class="btn-utility" onclick="triggerSync()">TRIGGER SYNC</button>
|
||||
<button class="btn-delete" onclick="clearAllData()">CLEAR ALL DATA</button>
|
||||
</div>
|
||||
|
||||
<div id="utilityResult"></div>
|
||||
</div>
|
||||
|
||||
<!-- Event Preview -->
|
||||
<div class="test-section">
|
||||
<div class="section-title">
|
||||
EVENT PREVIEW
|
||||
<button class="btn-utility" onclick="refreshPreview()" style="width: auto; padding: 6px 12px; font-size: 10px; margin-left: auto;">REFRESH</button>
|
||||
</div>
|
||||
<div id="eventPreview" class="event-preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Load Test Initialization Script -->
|
||||
<script src="test-init.js"></script>
|
||||
|
||||
<script>
|
||||
// Wait for calendar to initialize
|
||||
let calendarReady = false;
|
||||
let initCheckInterval;
|
||||
|
||||
function waitForCalendar() {
|
||||
return new Promise((resolve) => {
|
||||
if (window.calendarDebug?.indexedDB) {
|
||||
calendarReady = true;
|
||||
const initStatus = document.getElementById('initStatus');
|
||||
if (initStatus) {
|
||||
initStatus.style.display = 'none';
|
||||
}
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
initCheckInterval = setInterval(() => {
|
||||
if (window.calendarDebug?.indexedDB) {
|
||||
calendarReady = true;
|
||||
clearInterval(initCheckInterval);
|
||||
const initStatus = document.getElementById('initStatus');
|
||||
if (initStatus) {
|
||||
initStatus.style.background = 'var(--success)';
|
||||
initStatus.style.color = '#fff';
|
||||
initStatus.textContent = '[✓] CALENDAR SYSTEM READY';
|
||||
setTimeout(() => {
|
||||
initStatus.style.display = 'none';
|
||||
}, 1000);
|
||||
}
|
||||
resolve();
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// Timeout after 10 seconds
|
||||
setTimeout(() => {
|
||||
if (!calendarReady) {
|
||||
clearInterval(initCheckInterval);
|
||||
console.error('Calendar failed to initialize within 10 seconds');
|
||||
const initStatus = document.getElementById('initStatus');
|
||||
if (initStatus) {
|
||||
initStatus.style.background = 'var(--error)';
|
||||
initStatus.style.color = '#fff';
|
||||
initStatus.textContent = '[✗] CALENDAR SYSTEM FAILED TO INITIALIZE';
|
||||
}
|
||||
document.getElementById('eventPreview').innerHTML = `
|
||||
<div style="color: var(--error); padding: 20px; text-align: center;">
|
||||
[ERROR] Calendar system failed to initialize<br>
|
||||
<small>Check console for details</small>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}, 10000);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize datetime inputs with current time
|
||||
function initDateTimeInputs() {
|
||||
const now = new Date();
|
||||
const start = new Date(now.getTime() + 60 * 60 * 1000); // +1 hour
|
||||
const end = new Date(start.getTime() + 60 * 60 * 1000); // +1 hour from start
|
||||
|
||||
document.getElementById('createStart').value = formatDateTimeLocal(start);
|
||||
document.getElementById('createEnd').value = formatDateTimeLocal(end);
|
||||
document.getElementById('createAllDayDate').value = formatDateLocal(now);
|
||||
}
|
||||
|
||||
function formatDateTimeLocal(date) {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
function formatDateLocal(date) {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
// Network status monitoring
|
||||
function updateNetworkStatus() {
|
||||
const statusDiv = document.getElementById('networkStatus');
|
||||
const isOnline = navigator.onLine;
|
||||
|
||||
statusDiv.textContent = isOnline ? '[●] NETWORK: ONLINE' : '[●] NETWORK: OFFLINE';
|
||||
statusDiv.className = `network-status ${isOnline ? 'network-online' : 'network-offline'}`;
|
||||
}
|
||||
|
||||
window.addEventListener('online', updateNetworkStatus);
|
||||
window.addEventListener('offline', updateNetworkStatus);
|
||||
|
||||
// Get EventManager
|
||||
function getEventManager() {
|
||||
if (!window.calendarDebug?.eventManager) {
|
||||
throw new Error('Calendar not loaded - window.calendarDebug.eventManager not available');
|
||||
}
|
||||
return window.calendarDebug.eventManager;
|
||||
}
|
||||
|
||||
// Create Timed Event
|
||||
async function createTimedEvent() {
|
||||
const result = document.getElementById('createResult');
|
||||
try {
|
||||
const title = document.getElementById('createTitle').value;
|
||||
const start = new Date(document.getElementById('createStart').value);
|
||||
const end = new Date(document.getElementById('createEnd').value);
|
||||
|
||||
const eventManager = getEventManager();
|
||||
const newEvent = await eventManager.addEvent({
|
||||
title,
|
||||
start,
|
||||
end,
|
||||
type: 'meeting',
|
||||
allDay: false,
|
||||
syncStatus: 'pending'
|
||||
});
|
||||
|
||||
showResult(result, 'success', `[OK] Event created<br>ID: ${newEvent.id}<br>Status: ${newEvent.syncStatus}`);
|
||||
await refreshPreview();
|
||||
} catch (error) {
|
||||
showResult(result, 'error', `[ERROR] ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Create All-Day Event
|
||||
async function createAllDayEvent() {
|
||||
const result = document.getElementById('createAllDayResult');
|
||||
try {
|
||||
const title = document.getElementById('createAllDayTitle').value;
|
||||
const dateStr = document.getElementById('createAllDayDate').value;
|
||||
const start = new Date(dateStr + 'T00:00:00');
|
||||
const end = new Date(dateStr + 'T23:59:59');
|
||||
|
||||
const eventManager = getEventManager();
|
||||
const newEvent = await eventManager.addEvent({
|
||||
title,
|
||||
start,
|
||||
end,
|
||||
type: 'holiday',
|
||||
allDay: true,
|
||||
syncStatus: 'pending'
|
||||
});
|
||||
|
||||
showResult(result, 'success', `[OK] All-day event created<br>ID: ${newEvent.id}`);
|
||||
await refreshPreview();
|
||||
} catch (error) {
|
||||
showResult(result, 'error', `[ERROR] ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Update Event Title
|
||||
async function updateEventTitle() {
|
||||
const result = document.getElementById('updateTitleResult');
|
||||
try {
|
||||
const eventId = document.getElementById('updateEventId').value;
|
||||
const newTitle = document.getElementById('updateTitle').value;
|
||||
|
||||
if (!eventId) {
|
||||
showResult(result, 'error', '[ERROR] Please enter an event ID');
|
||||
return;
|
||||
}
|
||||
|
||||
const eventManager = getEventManager();
|
||||
const updated = await eventManager.updateEvent(eventId, { title: newTitle });
|
||||
|
||||
if (updated) {
|
||||
showResult(result, 'success', `[OK] Event updated<br>New title: "${updated.title}"`);
|
||||
await refreshPreview();
|
||||
} else {
|
||||
showResult(result, 'error', '[ERROR] Event not found');
|
||||
}
|
||||
} catch (error) {
|
||||
showResult(result, 'error', `[ERROR] ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle All-Day Status
|
||||
async function toggleAllDay() {
|
||||
const result = document.getElementById('toggleResult');
|
||||
try {
|
||||
const eventId = document.getElementById('toggleEventId').value;
|
||||
|
||||
if (!eventId) {
|
||||
showResult(result, 'error', '[ERROR] Please enter an event ID');
|
||||
return;
|
||||
}
|
||||
|
||||
const eventManager = getEventManager();
|
||||
const event = await eventManager.getEventById(eventId);
|
||||
|
||||
if (!event) {
|
||||
showResult(result, 'error', '[ERROR] Event not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const updated = await eventManager.updateEvent(eventId, {
|
||||
allDay: !event.allDay
|
||||
});
|
||||
|
||||
showResult(result, 'success', `[OK] Event toggled<br>Now: ${updated.allDay ? 'all-day' : 'timed'}`);
|
||||
await refreshPreview();
|
||||
} catch (error) {
|
||||
showResult(result, 'error', `[ERROR] ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete Event
|
||||
async function deleteEvent() {
|
||||
const result = document.getElementById('deleteResult');
|
||||
try {
|
||||
const eventId = document.getElementById('deleteEventId').value;
|
||||
|
||||
if (!eventId) {
|
||||
showResult(result, 'error', '[ERROR] Please enter an event ID');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(`Delete event ${eventId}?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const eventManager = getEventManager();
|
||||
const success = await eventManager.deleteEvent(eventId);
|
||||
|
||||
if (success) {
|
||||
showResult(result, 'success', `[OK] Event deleted<br>ID: ${eventId}`);
|
||||
await refreshPreview();
|
||||
} else {
|
||||
showResult(result, 'error', '[ERROR] Event not found');
|
||||
}
|
||||
} catch (error) {
|
||||
showResult(result, 'error', `[ERROR] ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// List All Events
|
||||
async function listAllEvents() {
|
||||
const result = document.getElementById('utilityResult');
|
||||
try {
|
||||
const db = window.calendarDebug.indexedDB;
|
||||
const events = await db.getAllEvents();
|
||||
|
||||
const html = `
|
||||
<strong>[EVENTS] Total: ${events.length}</strong><br><br>
|
||||
${events.map(e => `
|
||||
<div style="margin-bottom: 10px; padding: 10px; background: var(--bg-secondary); border: 2px solid var(--border-color);">
|
||||
<strong>${e.title}</strong><br>
|
||||
<span style="font-size: 10px; color: var(--text-muted);">ID: ${e.id}</span><br>
|
||||
<span class="badge badge-${e.syncStatus === 'synced' ? 'success' : e.syncStatus === 'pending' ? 'warning' : 'error'}">
|
||||
${e.syncStatus}
|
||||
</span>
|
||||
${e.allDay ? '[ALL-DAY]' : '[TIMED]'}
|
||||
</div>
|
||||
`).join('')}
|
||||
`;
|
||||
|
||||
showResult(result, 'info', html);
|
||||
} catch (error) {
|
||||
showResult(result, 'error', `[ERROR] ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Show Queue
|
||||
async function showQueue() {
|
||||
const result = document.getElementById('utilityResult');
|
||||
try {
|
||||
const queue = window.calendarDebug.queue;
|
||||
const items = await queue.getAll();
|
||||
|
||||
const html = `
|
||||
<strong>[QUEUE] Size: ${items.length}</strong><br><br>
|
||||
${items.length === 0 ? '[INFO] Queue is empty' : items.map(item => `
|
||||
<div style="margin-bottom: 10px; padding: 10px; background: var(--bg-secondary); border: 2px solid var(--border-color); border-left: 3px solid var(--accent-primary);">
|
||||
<strong>${item.type.toUpperCase()}</strong> → Event ${item.eventId}<br>
|
||||
<span style="font-size: 10px; color: var(--text-muted);">Retry: ${item.retryCount}/5</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
`;
|
||||
|
||||
showResult(result, 'info', html);
|
||||
} catch (error) {
|
||||
showResult(result, 'error', `[ERROR] ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger Sync
|
||||
async function triggerSync() {
|
||||
const result = document.getElementById('utilityResult');
|
||||
const timestamp = new Date().toLocaleTimeString('da-DK');
|
||||
try {
|
||||
const syncManager = window.calendarDebug.syncManager;
|
||||
await syncManager.triggerManualSync();
|
||||
showResult(result, 'success', `[OK] Sync triggered at ${timestamp}<br>Check sync-visualization.html for details`);
|
||||
await refreshPreview();
|
||||
} catch (error) {
|
||||
const isOffline = error.message.includes('offline');
|
||||
const icon = isOffline ? '⚠️' : '❌';
|
||||
showResult(result, 'error', `${icon} [ERROR] Sync failed at ${timestamp}<br>${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear All Data
|
||||
async function clearAllData() {
|
||||
if (!confirm('⚠️ WARNING: Delete ALL events and queue? This cannot be undone!')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = document.getElementById('utilityResult');
|
||||
try {
|
||||
const db = window.calendarDebug.indexedDB;
|
||||
const queue = window.calendarDebug.queue;
|
||||
|
||||
await queue.clear();
|
||||
|
||||
const events = await db.getAllEvents();
|
||||
for (const event of events) {
|
||||
await db.deleteEvent(event.id);
|
||||
}
|
||||
|
||||
showResult(result, 'success', '[OK] All data cleared');
|
||||
await refreshPreview();
|
||||
} catch (error) {
|
||||
showResult(result, 'error', `[ERROR] ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh Event Preview
|
||||
async function refreshPreview() {
|
||||
const preview = document.getElementById('eventPreview');
|
||||
try {
|
||||
const db = window.calendarDebug.indexedDB;
|
||||
const events = await db.getAllEvents();
|
||||
|
||||
if (events.length === 0) {
|
||||
preview.innerHTML = '<div style="grid-column: 1/-1; text-align: center; color: var(--text-muted); padding: 40px; font-size: 11px; text-transform: uppercase; letter-spacing: 1px; font-weight: 600;">[EMPTY] No events in IndexedDB</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
preview.innerHTML = events.map(e => `
|
||||
<div class="event-preview-item">
|
||||
<div class="event-preview-title">${e.title}</div>
|
||||
<div class="event-preview-id">${e.id}</div>
|
||||
<div class="event-preview-status">
|
||||
<span class="badge badge-${e.syncStatus === 'synced' ? 'success' : e.syncStatus === 'pending' ? 'warning' : 'error'}">
|
||||
${e.syncStatus}
|
||||
</span>
|
||||
<span>${e.allDay ? '[ALL-DAY]' : '[TIMED]'}</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} catch (error) {
|
||||
preview.innerHTML = `<div style="color: var(--error);">[ERROR] ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Show Result Helper
|
||||
function showResult(element, type, message) {
|
||||
element.innerHTML = `<div class="result-box result-${type}">${message}</div>`;
|
||||
}
|
||||
|
||||
// Initialize
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
initDateTimeInputs();
|
||||
updateNetworkStatus();
|
||||
await waitForCalendar();
|
||||
refreshPreview();
|
||||
});
|
||||
} else {
|
||||
(async () => {
|
||||
initDateTimeInputs();
|
||||
updateNetworkStatus();
|
||||
await waitForCalendar();
|
||||
refreshPreview();
|
||||
})();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
150
test/integrationtesting/stacking-visualization-new.html
Normal file
150
test/integrationtesting/stacking-visualization-new.html
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="da">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Event Stacking Scenarios - Test Suite</title>
|
||||
<link rel="stylesheet" href="scenarios/scenario-styles.css">
|
||||
<style>
|
||||
.scenario-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.scenario-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.scenario-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.scenario-card h3 {
|
||||
margin-top: 0;
|
||||
color: #b53f7a;
|
||||
}
|
||||
|
||||
.scenario-card p {
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.scenario-link {
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
background: #b53f7a;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.scenario-link:hover {
|
||||
background: #8e3260;
|
||||
}
|
||||
|
||||
.intro {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-left: 4px solid #b53f7a;
|
||||
margin-bottom: 30px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.intro h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="scenario-container">
|
||||
<h1>Event Stacking & Grid Layout - Test Scenarios</h1>
|
||||
|
||||
<div class="intro">
|
||||
<h2>About This Test Suite</h2>
|
||||
<p>
|
||||
This test suite validates the event layout algorithm for the Calendar Plantempus application.
|
||||
The algorithm determines how overlapping events should be rendered using two strategies:
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>GRID Layout:</strong> Events that start within a threshold (±30 minutes) are placed in a grid container where they can share columns</li>
|
||||
<li><strong>STACKED Layout:</strong> Events are stacked with horizontal offsets (15px per level)</li>
|
||||
</ul>
|
||||
<p>
|
||||
Each scenario tests a specific edge case or layout pattern. Click on a scenario below to view the visual representation and test results.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="scenario-grid">
|
||||
<div class="scenario-card">
|
||||
<h3>Scenario 1: No Overlap</h3>
|
||||
<p>Three sequential events with no time overlap. All should have stack level 0.</p>
|
||||
<a href="scenarios/scenario-1.html" class="scenario-link">View Test →</a>
|
||||
</div>
|
||||
|
||||
<div class="scenario-card">
|
||||
<h3>Scenario 2: Column Sharing (Grid)</h3>
|
||||
<p>Two events starting at same time (10:00) - should share columns in a grid with 2 columns.</p>
|
||||
<a href="scenarios/scenario-2.html" class="scenario-link">View Test →</a>
|
||||
</div>
|
||||
|
||||
<div class="scenario-card">
|
||||
<h3>Scenario 3: Nested Stacking</h3>
|
||||
<p>Events with progressive nesting: A contains B, B contains C, C and D overlap. Tests stack level calculation.</p>
|
||||
<a href="scenarios/scenario-3.html" class="scenario-link">View Test →</a>
|
||||
</div>
|
||||
|
||||
<div class="scenario-card">
|
||||
<h3>Scenario 4: Complex Stacking</h3>
|
||||
<p>Long event (A) with multiple shorter events (B, C, D) nested inside at different times.</p>
|
||||
<a href="scenarios/scenario-4.html" class="scenario-link">View Test →</a>
|
||||
</div>
|
||||
|
||||
<div class="scenario-card">
|
||||
<h3>Scenario 5: Three Column Share</h3>
|
||||
<p>Three events all starting at 10:00 - should create a 3-column grid layout.</p>
|
||||
<a href="scenarios/scenario-5.html" class="scenario-link">View Test →</a>
|
||||
</div>
|
||||
|
||||
<div class="scenario-card">
|
||||
<h3>Scenario 6: Overlapping Pairs</h3>
|
||||
<p>Two separate pairs of overlapping events. Each pair should be independent.</p>
|
||||
<a href="scenarios/scenario-6.html" class="scenario-link">View Test →</a>
|
||||
</div>
|
||||
|
||||
<div class="scenario-card">
|
||||
<h3>Scenario 7: Long Event Container</h3>
|
||||
<p>One long event (A) containing two shorter events (B, C) that don't overlap each other.</p>
|
||||
<a href="scenarios/scenario-7.html" class="scenario-link">View Test →</a>
|
||||
</div>
|
||||
|
||||
<div class="scenario-card">
|
||||
<h3>Scenario 8: Edge-Adjacent Events</h3>
|
||||
<p>Events that touch but don't overlap (A ends when B starts). Should not stack.</p>
|
||||
<a href="scenarios/scenario-8.html" class="scenario-link">View Test →</a>
|
||||
</div>
|
||||
|
||||
<div class="scenario-card">
|
||||
<h3>Scenario 9: End-to-Start Chain</h3>
|
||||
<p>Events linked by end-to-start conflicts within threshold. Tests conflict chain detection.</p>
|
||||
<a href="scenarios/scenario-9.html" class="scenario-link">View Test →</a>
|
||||
</div>
|
||||
|
||||
<div class="scenario-card">
|
||||
<h3>Scenario 10: Four Column Grid</h3>
|
||||
<p>Four events all starting at same time - maximum column sharing test.</p>
|
||||
<a href="scenarios/scenario-10.html" class="scenario-link">View Test →</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
1836
test/integrationtesting/stacking-visualization.html
Normal file
1836
test/integrationtesting/stacking-visualization.html
Normal file
File diff suppressed because it is too large
Load diff
854
test/integrationtesting/sync-visualization.html
Normal file
854
test/integrationtesting/sync-visualization.html
Normal file
|
|
@ -0,0 +1,854 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="da">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SYNC QUEUE VISUALIZATION | Calendar System</title>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap');
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg-primary: #f8f9fa;
|
||||
--bg-secondary: #ffffff;
|
||||
--bg-tertiary: #f1f3f5;
|
||||
--border-color: #dee2e6;
|
||||
--text-primary: #212529;
|
||||
--text-secondary: #495057;
|
||||
--text-muted: #6c757d;
|
||||
--accent-primary: #0066cc;
|
||||
--accent-secondary: #6610f2;
|
||||
--success: #28a745;
|
||||
--warning: #ffc107;
|
||||
--error: #dc3545;
|
||||
--info: #17a2b8;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
padding: 20px;
|
||||
line-height: 1.6;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: var(--bg-secondary);
|
||||
padding: 24px;
|
||||
border: 2px solid var(--border-color);
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 6px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1::before {
|
||||
content: '▶ ';
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
margin-bottom: 20px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
padding: 12px 0;
|
||||
border-top: 2px solid var(--border-color);
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 4px 12px;
|
||||
border-radius: 2px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.8px;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
.status-online {
|
||||
background: var(--success);
|
||||
color: white;
|
||||
border-color: var(--success);
|
||||
}
|
||||
|
||||
.status-offline {
|
||||
background: var(--error);
|
||||
color: white;
|
||||
border-color: var(--error);
|
||||
}
|
||||
|
||||
.status-syncing {
|
||||
background: var(--warning);
|
||||
color: #212529;
|
||||
border-color: var(--warning);
|
||||
}
|
||||
|
||||
.status-idle {
|
||||
background: var(--text-muted);
|
||||
color: white;
|
||||
border-color: var(--text-muted);
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 8px 14px;
|
||||
border: 2px solid var(--border-color);
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
transition: all 0.15s ease;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
border-color: var(--accent-primary);
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
border-color: var(--info);
|
||||
color: var(--info);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
border-color: var(--success);
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
border-color: var(--error);
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: var(--bg-secondary);
|
||||
border: 2px solid var(--border-color);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.panel::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, transparent, var(--accent-primary), transparent);
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--accent-primary);
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.count-badge {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
padding: 4px 10px;
|
||||
border: 2px solid var(--border-color);
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.event-list {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.event-list::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
.event-list::-webkit-scrollbar-track {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.event-list::-webkit-scrollbar-thumb {
|
||||
background: var(--border-color);
|
||||
border: 2px solid var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.event-list::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--accent-primary);
|
||||
}
|
||||
|
||||
.event-item {
|
||||
padding: 12px;
|
||||
border: 2px solid var(--border-color);
|
||||
margin-bottom: 8px;
|
||||
transition: all 0.15s ease;
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.event-item:hover {
|
||||
border-color: var(--accent-primary);
|
||||
transform: translateX(2px);
|
||||
box-shadow: 2px 0 0 var(--accent-primary);
|
||||
}
|
||||
|
||||
.event-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.event-title {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.sync-status {
|
||||
padding: 3px 10px;
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.8px;
|
||||
border: 2px solid;
|
||||
}
|
||||
|
||||
.sync-synced {
|
||||
background: #d4edda;
|
||||
color: var(--success);
|
||||
border-color: var(--success);
|
||||
}
|
||||
|
||||
.sync-pending {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
border-color: var(--warning);
|
||||
}
|
||||
|
||||
.sync-error {
|
||||
background: #f8d7da;
|
||||
color: var(--error);
|
||||
border-color: var(--error);
|
||||
}
|
||||
|
||||
.event-details {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.queue-item {
|
||||
padding: 12px;
|
||||
border-left: 3px solid var(--accent-primary);
|
||||
background: var(--bg-tertiary);
|
||||
margin-bottom: 8px;
|
||||
border: 2px solid var(--border-color);
|
||||
border-left: 3px solid var(--accent-primary);
|
||||
}
|
||||
|
||||
.queue-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.operation-type {
|
||||
padding: 3px 10px;
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.8px;
|
||||
border: 2px solid;
|
||||
}
|
||||
|
||||
.op-create {
|
||||
background: #d4edda;
|
||||
color: var(--success);
|
||||
border-color: var(--success);
|
||||
}
|
||||
|
||||
.op-update {
|
||||
background: #d1ecf1;
|
||||
color: var(--info);
|
||||
border-color: var(--info);
|
||||
}
|
||||
|
||||
.op-delete {
|
||||
background: #f8d7da;
|
||||
color: var(--error);
|
||||
border-color: var(--error);
|
||||
}
|
||||
|
||||
.retry-count {
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.log-panel {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.log-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
background: var(--bg-tertiary);
|
||||
padding: 16px;
|
||||
font-size: 11px;
|
||||
border: 2px solid var(--border-color);
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
margin-bottom: 4px;
|
||||
padding: 8px;
|
||||
border-left: 3px solid transparent;
|
||||
padding-left: 12px;
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.log-entry:hover {
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.log-timestamp {
|
||||
color: var(--text-muted);
|
||||
margin-right: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.log-info { border-left-color: var(--info); }
|
||||
.log-success { border-left-color: var(--success); }
|
||||
.log-warning { border-left-color: var(--warning); }
|
||||
.log-error { border-left-color: var(--error); }
|
||||
|
||||
.log-info .log-message { color: var(--info); }
|
||||
.log-success .log-message { color: var(--success); }
|
||||
.log-warning .log-message { color: #856404; }
|
||||
.log-error .log-message { color: var(--error); }
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
background: var(--bg-tertiary);
|
||||
border: 2px solid var(--border-color);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.stat-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: var(--accent-primary);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: var(--accent-primary);
|
||||
line-height: 1;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 9px;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.refresh-indicator {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--success);
|
||||
border-radius: 50%;
|
||||
margin-left: 8px;
|
||||
animation: pulse 2s infinite;
|
||||
box-shadow: 0 0 8px var(--success);
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>SYNC QUEUE VISUALIZATION</h1>
|
||||
<p class="subtitle">// Live monitoring of offline-first calendar sync operations</p>
|
||||
|
||||
<div id="initStatus" style="background: var(--warning); color: #000; padding: 12px; margin-bottom: 16px; border: 2px solid var(--border-color); text-align: center; font-weight: 600; letter-spacing: 0.5px;">
|
||||
[⏳] INITIALIZING CALENDAR SYSTEM...
|
||||
</div>
|
||||
|
||||
<div class="status-bar">
|
||||
<div class="status-item">
|
||||
<span class="status-label">NETWORK:</span>
|
||||
<span id="networkStatus" class="status-badge status-online">ONLINE</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-label">SYNC:</span>
|
||||
<span id="syncStatus" class="status-badge status-idle">IDLE</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-label">AUTO-REFRESH:</span>
|
||||
<span class="refresh-indicator"></span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-label">LAST SYNC:</span>
|
||||
<span id="lastSyncTime" class="status-badge status-idle">NEVER</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button class="btn-primary" onclick="manualSync()">TRIGGER SYNC</button>
|
||||
<button class="btn-success" onclick="refreshData()">REFRESH DATA</button>
|
||||
<button onclick="toggleNetworkSimulator()">TOGGLE NETWORK</button>
|
||||
<button class="btn-danger" onclick="clearQueue()">CLEAR QUEUE</button>
|
||||
<button class="btn-danger" onclick="clearDatabase()">CLEAR DATABASE</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<!-- IndexedDB Events -->
|
||||
<div class="panel">
|
||||
<div class="panel-title">
|
||||
<span>INDEXEDDB EVENTS</span>
|
||||
<span id="eventCount" class="count-badge">0</span>
|
||||
</div>
|
||||
<div id="eventList" class="event-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- Operation Queue -->
|
||||
<div class="panel">
|
||||
<div class="panel-title">
|
||||
<span>OPERATION QUEUE</span>
|
||||
<span id="queueCount" class="count-badge">0</span>
|
||||
</div>
|
||||
<div id="queueList" class="event-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- Statistics -->
|
||||
<div class="panel">
|
||||
<div class="panel-title">
|
||||
<span>STATISTICS</span>
|
||||
</div>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<div class="stat-value" id="statSynced">0</div>
|
||||
<div class="stat-label">Synced</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value" id="statPending">0</div>
|
||||
<div class="stat-label">Pending</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value" id="statError">0</div>
|
||||
<div class="stat-label">Errors</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value" id="statQueue">0</div>
|
||||
<div class="stat-label">In Queue</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sync Log -->
|
||||
<div class="panel log-panel">
|
||||
<div class="panel-title">
|
||||
<span>SYNC LOG</span>
|
||||
<button onclick="clearLog()" style="font-size: 10px; padding: 4px 8px;">CLEAR</button>
|
||||
</div>
|
||||
<div id="logList" class="log-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Load Test Initialization Script -->
|
||||
<script src="test-init.js"></script>
|
||||
|
||||
<script>
|
||||
let logEntries = [];
|
||||
const MAX_LOG_ENTRIES = 100;
|
||||
let calendarReady = false;
|
||||
|
||||
// Wait for calendar to initialize
|
||||
function waitForCalendar() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (window.calendarDebug?.indexedDB) {
|
||||
calendarReady = true;
|
||||
const initStatus = document.getElementById('initStatus');
|
||||
if (initStatus) {
|
||||
initStatus.style.display = 'none';
|
||||
}
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const checkInterval = setInterval(() => {
|
||||
if (window.calendarDebug?.indexedDB) {
|
||||
calendarReady = true;
|
||||
clearInterval(checkInterval);
|
||||
const initStatus = document.getElementById('initStatus');
|
||||
if (initStatus) {
|
||||
initStatus.style.background = 'var(--success)';
|
||||
initStatus.style.color = '#fff';
|
||||
initStatus.textContent = '[✓] CALENDAR SYSTEM READY';
|
||||
setTimeout(() => {
|
||||
initStatus.style.display = 'none';
|
||||
}, 1000);
|
||||
}
|
||||
resolve();
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// Timeout after 10 seconds
|
||||
setTimeout(() => {
|
||||
if (!calendarReady) {
|
||||
clearInterval(checkInterval);
|
||||
const initStatus = document.getElementById('initStatus');
|
||||
if (initStatus) {
|
||||
initStatus.style.background = 'var(--error)';
|
||||
initStatus.style.color = '#fff';
|
||||
initStatus.textContent = '[✗] CALENDAR SYSTEM FAILED TO INITIALIZE - Check console for details';
|
||||
}
|
||||
reject(new Error('Calendar failed to initialize within 10 seconds'));
|
||||
}
|
||||
}, 10000);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize
|
||||
async function init() {
|
||||
log('info', 'Waiting for calendar system to initialize...');
|
||||
|
||||
try {
|
||||
await waitForCalendar();
|
||||
log('success', 'Connected to calendar IndexedDB');
|
||||
} catch (error) {
|
||||
log('error', 'Calendar system failed to initialize: ' + error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
// Listen to network events
|
||||
window.addEventListener('online', () => {
|
||||
updateNetworkStatus(true);
|
||||
log('success', 'Network online');
|
||||
});
|
||||
|
||||
window.addEventListener('offline', () => {
|
||||
updateNetworkStatus(false);
|
||||
log('warning', 'Network offline');
|
||||
});
|
||||
|
||||
// Initial load
|
||||
await refreshData();
|
||||
|
||||
// Auto-refresh every 2 seconds
|
||||
setInterval(refreshData, 2000);
|
||||
}
|
||||
|
||||
async function refreshData() {
|
||||
try {
|
||||
const db = window.calendarDebug.indexedDB;
|
||||
const queue = window.calendarDebug.queue;
|
||||
|
||||
if (!db || !queue) {
|
||||
log('error', 'IndexedDB or Queue not available');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get events
|
||||
const events = await db.getAllEvents();
|
||||
renderEvents(events);
|
||||
|
||||
// Get queue
|
||||
const queueItems = await queue.getAll();
|
||||
renderQueue(queueItems);
|
||||
|
||||
// Update statistics
|
||||
updateStatistics(events, queueItems);
|
||||
|
||||
} catch (error) {
|
||||
log('error', `Refresh failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function renderEvents(events) {
|
||||
const container = document.getElementById('eventList');
|
||||
document.getElementById('eventCount').textContent = events.length;
|
||||
|
||||
if (events.length === 0) {
|
||||
container.innerHTML = '<div class="empty-state">No events in IndexedDB</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = events.map(event => `
|
||||
<div class="event-item">
|
||||
<div class="event-header">
|
||||
<span class="event-title">${event.title}</span>
|
||||
<span class="sync-status sync-${event.syncStatus}">${event.syncStatus}</span>
|
||||
</div>
|
||||
<div class="event-details">
|
||||
ID: ${event.id}<br>
|
||||
${event.allDay ? 'ALL-DAY' : formatTime(event.start) + ' - ' + formatTime(event.end)}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function renderQueue(queueItems) {
|
||||
const container = document.getElementById('queueList');
|
||||
document.getElementById('queueCount').textContent = queueItems.length;
|
||||
|
||||
if (queueItems.length === 0) {
|
||||
container.innerHTML = '<div class="empty-state">Queue is empty</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = queueItems.map(item => `
|
||||
<div class="queue-item">
|
||||
<div class="queue-header">
|
||||
<span class="operation-type op-${item.type}">${item.type}</span>
|
||||
<span class="retry-count">RETRY: ${item.retryCount}/5</span>
|
||||
</div>
|
||||
<div class="event-details">
|
||||
EVENT ID: ${item.eventId}<br>
|
||||
TIMESTAMP: ${new Date(item.timestamp).toLocaleTimeString('da-DK')}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function updateStatistics(events, queueItems) {
|
||||
const synced = events.filter(e => e.syncStatus === 'synced').length;
|
||||
const pending = events.filter(e => e.syncStatus === 'pending').length;
|
||||
const error = events.filter(e => e.syncStatus === 'error').length;
|
||||
|
||||
document.getElementById('statSynced').textContent = synced;
|
||||
document.getElementById('statPending').textContent = pending;
|
||||
document.getElementById('statError').textContent = error;
|
||||
document.getElementById('statQueue').textContent = queueItems.length;
|
||||
}
|
||||
|
||||
function updateNetworkStatus(isOnline) {
|
||||
const badge = document.getElementById('networkStatus');
|
||||
badge.textContent = isOnline ? 'ONLINE' : 'OFFLINE';
|
||||
badge.className = `status-badge ${isOnline ? 'status-online' : 'status-offline'}`;
|
||||
}
|
||||
|
||||
function updateSyncStatus(isSyncing) {
|
||||
const badge = document.getElementById('syncStatus');
|
||||
badge.textContent = isSyncing ? 'SYNCING' : 'IDLE';
|
||||
badge.className = `status-badge ${isSyncing ? 'status-syncing' : 'status-idle'}`;
|
||||
}
|
||||
|
||||
async function manualSync() {
|
||||
const timestamp = new Date().toLocaleTimeString('da-DK');
|
||||
log('info', `Manual sync triggered at ${timestamp}`);
|
||||
updateSyncStatus(true);
|
||||
|
||||
try {
|
||||
const syncManager = window.calendarDebug.syncManager;
|
||||
if (syncManager) {
|
||||
await syncManager.triggerManualSync();
|
||||
log('success', `Manual sync completed at ${timestamp}`);
|
||||
updateLastSyncTime(timestamp, 'success');
|
||||
} else {
|
||||
log('error', 'SyncManager not available');
|
||||
updateLastSyncTime(timestamp, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
log('error', `Manual sync failed: ${error.message}`);
|
||||
updateLastSyncTime(timestamp, 'error');
|
||||
} finally {
|
||||
updateSyncStatus(false);
|
||||
await refreshData();
|
||||
}
|
||||
}
|
||||
|
||||
function updateLastSyncTime(timestamp, status = 'success') {
|
||||
const badge = document.getElementById('lastSyncTime');
|
||||
badge.textContent = timestamp;
|
||||
badge.className = `status-badge status-${status}`;
|
||||
}
|
||||
|
||||
async function clearQueue() {
|
||||
if (!confirm('Clear all operations from the queue?')) return;
|
||||
|
||||
log('warning', 'Clearing queue...');
|
||||
try {
|
||||
const queue = window.calendarDebug.queue;
|
||||
await queue.clear();
|
||||
log('success', 'Queue cleared');
|
||||
await refreshData();
|
||||
} catch (error) {
|
||||
log('error', `Failed to clear queue: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function clearDatabase() {
|
||||
if (!confirm('⚠️ WARNING: This will delete ALL events from IndexedDB! Continue?')) return;
|
||||
|
||||
log('warning', 'Clearing database...');
|
||||
try {
|
||||
const db = window.calendarDebug.indexedDB;
|
||||
db.close();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const request = indexedDB.deleteDatabase('CalendarDB');
|
||||
request.onsuccess = resolve;
|
||||
request.onerror = reject;
|
||||
});
|
||||
|
||||
log('success', 'Database cleared - please reload the page');
|
||||
alert('Database cleared! Please reload the page.');
|
||||
} catch (error) {
|
||||
log('error', `Failed to clear database: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleNetworkSimulator() {
|
||||
const isCurrentlyOnline = navigator.onLine;
|
||||
log('info', `Network simulator toggle (currently ${isCurrentlyOnline ? 'online' : 'offline'})`);
|
||||
log('warning', 'Use DevTools > Network > Offline for real offline testing');
|
||||
}
|
||||
|
||||
function log(level, message) {
|
||||
const timestamp = new Date().toLocaleTimeString('da-DK');
|
||||
const entry = { timestamp, level, message };
|
||||
|
||||
logEntries.unshift(entry);
|
||||
if (logEntries.length > MAX_LOG_ENTRIES) {
|
||||
logEntries.pop();
|
||||
}
|
||||
|
||||
renderLog();
|
||||
}
|
||||
|
||||
function renderLog() {
|
||||
const container = document.getElementById('logList');
|
||||
container.innerHTML = logEntries.map(entry => `
|
||||
<div class="log-entry log-${entry.level}">
|
||||
<span class="log-timestamp">[${entry.timestamp}]</span>
|
||||
<span class="log-message">${entry.message}</span>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function clearLog() {
|
||||
logEntries = [];
|
||||
renderLog();
|
||||
log('info', 'Log cleared');
|
||||
}
|
||||
|
||||
function formatTime(date) {
|
||||
return new Date(date).toLocaleTimeString('da-DK', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
// Start on load
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
132
test/integrationtesting/test-events.json
Normal file
132
test/integrationtesting/test-events.json
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
[
|
||||
{
|
||||
"id": "test-1",
|
||||
"title": "Morning Standup",
|
||||
"start": "2025-11-04T08:00:00Z",
|
||||
"end": "2025-11-04T08:30:00Z",
|
||||
"type": "meeting",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 30,
|
||||
"color": "#ff5722"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "test-2",
|
||||
"title": "Development Sprint",
|
||||
"start": "2025-11-04T09:00:00Z",
|
||||
"end": "2025-11-04T12:00:00Z",
|
||||
"type": "work",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 180,
|
||||
"color": "#2196f3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "test-3",
|
||||
"title": "Lunch Break",
|
||||
"start": "2025-11-04T12:00:00Z",
|
||||
"end": "2025-11-04T13:00:00Z",
|
||||
"type": "break",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 60,
|
||||
"color": "#4caf50"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "test-4",
|
||||
"title": "Client Meeting",
|
||||
"start": "2025-11-04T14:00:00Z",
|
||||
"end": "2025-11-04T15:30:00Z",
|
||||
"type": "meeting",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 90,
|
||||
"color": "#673ab7"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "test-5",
|
||||
"title": "Code Review Session",
|
||||
"start": "2025-11-04T16:00:00Z",
|
||||
"end": "2025-11-04T17:00:00Z",
|
||||
"type": "meeting",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 60,
|
||||
"color": "#ff9800"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "test-6",
|
||||
"title": "Public Holiday",
|
||||
"start": "2025-11-05T00:00:00Z",
|
||||
"end": "2025-11-05T23:59:59Z",
|
||||
"type": "holiday",
|
||||
"allDay": true,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 1440,
|
||||
"color": "#f44336"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "test-7",
|
||||
"title": "Team Workshop",
|
||||
"start": "2025-11-06T09:00:00Z",
|
||||
"end": "2025-11-06T11:30:00Z",
|
||||
"type": "meeting",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 150,
|
||||
"color": "#9c27b0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "test-8",
|
||||
"title": "Birthday Celebration",
|
||||
"start": "2025-11-07T00:00:00Z",
|
||||
"end": "2025-11-07T23:59:59Z",
|
||||
"type": "personal",
|
||||
"allDay": true,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 1440,
|
||||
"color": "#e91e63"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "test-9",
|
||||
"title": "Sprint Retrospective",
|
||||
"start": "2025-11-07T13:00:00Z",
|
||||
"end": "2025-11-07T14:30:00Z",
|
||||
"type": "meeting",
|
||||
"allDay": false,
|
||||
"syncStatus": "pending",
|
||||
"metadata": {
|
||||
"duration": 90,
|
||||
"color": "#3f51b5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "test-10",
|
||||
"title": "Documentation Update",
|
||||
"start": "2025-11-08T10:00:00Z",
|
||||
"end": "2025-11-08T12:00:00Z",
|
||||
"type": "work",
|
||||
"allDay": false,
|
||||
"syncStatus": "pending",
|
||||
"metadata": {
|
||||
"duration": 120,
|
||||
"color": "#009688"
|
||||
}
|
||||
}
|
||||
]
|
||||
452
test/integrationtesting/test-init.js
Normal file
452
test/integrationtesting/test-init.js
Normal file
|
|
@ -0,0 +1,452 @@
|
|||
/**
|
||||
* Test Initialization Script
|
||||
* Standalone initialization for test pages without requiring full calendar DOM
|
||||
*/
|
||||
|
||||
// IndexedDB Service (simplified standalone version)
|
||||
class TestIndexedDBService {
|
||||
constructor() {
|
||||
this.DB_NAME = 'CalendarDB_Test'; // Separate test database
|
||||
this.DB_VERSION = 1;
|
||||
this.EVENTS_STORE = 'events';
|
||||
this.QUEUE_STORE = 'operationQueue';
|
||||
this.SYNC_STATE_STORE = 'syncState';
|
||||
this.db = null;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(this.DB_NAME, this.DB_VERSION);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => {
|
||||
this.db = request.result;
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = event.target.result;
|
||||
|
||||
// Create events store
|
||||
if (!db.objectStoreNames.contains(this.EVENTS_STORE)) {
|
||||
const eventStore = db.createObjectStore(this.EVENTS_STORE, { keyPath: 'id' });
|
||||
eventStore.createIndex('start', 'start', { unique: false });
|
||||
eventStore.createIndex('end', 'end', { unique: false });
|
||||
eventStore.createIndex('syncStatus', 'syncStatus', { unique: false });
|
||||
}
|
||||
|
||||
// Create operation queue store
|
||||
if (!db.objectStoreNames.contains(this.QUEUE_STORE)) {
|
||||
const queueStore = db.createObjectStore(this.QUEUE_STORE, { keyPath: 'id', autoIncrement: true });
|
||||
queueStore.createIndex('timestamp', 'timestamp', { unique: false });
|
||||
queueStore.createIndex('eventId', 'eventId', { unique: false });
|
||||
}
|
||||
|
||||
// Create sync state store
|
||||
if (!db.objectStoreNames.contains(this.SYNC_STATE_STORE)) {
|
||||
db.createObjectStore(this.SYNC_STATE_STORE, { keyPath: 'key' });
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async getAllEvents() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([this.EVENTS_STORE], 'readonly');
|
||||
const store = transaction.objectStore(this.EVENTS_STORE);
|
||||
const request = store.getAll();
|
||||
|
||||
request.onsuccess = () => {
|
||||
const events = request.result.map(event => ({
|
||||
...event,
|
||||
start: new Date(event.start),
|
||||
end: new Date(event.end)
|
||||
}));
|
||||
resolve(events);
|
||||
};
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
async getEvent(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([this.EVENTS_STORE], 'readonly');
|
||||
const store = transaction.objectStore(this.EVENTS_STORE);
|
||||
const request = store.get(id);
|
||||
|
||||
request.onsuccess = () => {
|
||||
const event = request.result;
|
||||
if (event) {
|
||||
event.start = new Date(event.start);
|
||||
event.end = new Date(event.end);
|
||||
}
|
||||
resolve(event || null);
|
||||
};
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
async saveEvent(event) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([this.EVENTS_STORE], 'readwrite');
|
||||
const store = transaction.objectStore(this.EVENTS_STORE);
|
||||
const eventToSave = {
|
||||
...event,
|
||||
start: event.start instanceof Date ? event.start.toISOString() : event.start,
|
||||
end: event.end instanceof Date ? event.end.toISOString() : event.end
|
||||
};
|
||||
const request = store.put(eventToSave);
|
||||
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
async deleteEvent(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([this.EVENTS_STORE], 'readwrite');
|
||||
const store = transaction.objectStore(this.EVENTS_STORE);
|
||||
const request = store.delete(id);
|
||||
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
async addToQueue(operation) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([this.QUEUE_STORE], 'readwrite');
|
||||
const store = transaction.objectStore(this.QUEUE_STORE);
|
||||
const request = store.add(operation);
|
||||
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
async getQueue() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([this.QUEUE_STORE], 'readonly');
|
||||
const store = transaction.objectStore(this.QUEUE_STORE);
|
||||
const request = store.getAll();
|
||||
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
async removeFromQueue(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([this.QUEUE_STORE], 'readwrite');
|
||||
const store = transaction.objectStore(this.QUEUE_STORE);
|
||||
const request = store.delete(id);
|
||||
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
async clearQueue() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([this.QUEUE_STORE], 'readwrite');
|
||||
const store = transaction.objectStore(this.QUEUE_STORE);
|
||||
const request = store.clear();
|
||||
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.db) {
|
||||
this.db.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Operation Queue (simplified standalone version)
|
||||
class TestOperationQueue {
|
||||
constructor(indexedDB) {
|
||||
this.indexedDB = indexedDB;
|
||||
}
|
||||
|
||||
async enqueue(operation) {
|
||||
await this.indexedDB.addToQueue(operation);
|
||||
}
|
||||
|
||||
async getAll() {
|
||||
return await this.indexedDB.getQueue();
|
||||
}
|
||||
|
||||
async remove(id) {
|
||||
await this.indexedDB.removeFromQueue(id);
|
||||
}
|
||||
|
||||
async clear() {
|
||||
await this.indexedDB.clearQueue();
|
||||
}
|
||||
|
||||
async incrementRetryCount(operationId) {
|
||||
const queue = await this.getAll();
|
||||
const operation = queue.find(op => op.id === operationId);
|
||||
if (operation) {
|
||||
operation.retryCount = (operation.retryCount || 0) + 1;
|
||||
await this.indexedDB.removeFromQueue(operationId);
|
||||
await this.indexedDB.addToQueue(operation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Simple EventManager for tests
|
||||
class TestEventManager {
|
||||
constructor(indexedDB, queue) {
|
||||
this.indexedDB = indexedDB;
|
||||
this.queue = queue;
|
||||
}
|
||||
|
||||
async getAllEvents() {
|
||||
return await this.indexedDB.getAllEvents();
|
||||
}
|
||||
|
||||
async getEvent(id) {
|
||||
return await this.indexedDB.getEvent(id);
|
||||
}
|
||||
|
||||
async addEvent(eventData) {
|
||||
const id = eventData.id || `event_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
const syncStatus = eventData.syncStatus || 'pending';
|
||||
|
||||
const newEvent = {
|
||||
...eventData,
|
||||
id,
|
||||
syncStatus
|
||||
};
|
||||
|
||||
await this.indexedDB.saveEvent(newEvent);
|
||||
|
||||
if (syncStatus === 'pending') {
|
||||
await this.queue.enqueue({
|
||||
type: 'create',
|
||||
eventId: id,
|
||||
data: newEvent,
|
||||
timestamp: Date.now(),
|
||||
retryCount: 0
|
||||
});
|
||||
}
|
||||
|
||||
return newEvent;
|
||||
}
|
||||
|
||||
async updateEvent(id, updates) {
|
||||
const event = await this.indexedDB.getEvent(id);
|
||||
if (!event) return null;
|
||||
|
||||
const updatedEvent = { ...event, ...updates, syncStatus: 'pending' };
|
||||
await this.indexedDB.saveEvent(updatedEvent);
|
||||
|
||||
await this.queue.enqueue({
|
||||
type: 'update',
|
||||
eventId: id,
|
||||
data: updates,
|
||||
timestamp: Date.now(),
|
||||
retryCount: 0
|
||||
});
|
||||
|
||||
return updatedEvent;
|
||||
}
|
||||
|
||||
async deleteEvent(id) {
|
||||
await this.indexedDB.deleteEvent(id);
|
||||
await this.queue.enqueue({
|
||||
type: 'delete',
|
||||
eventId: id,
|
||||
data: null,
|
||||
timestamp: Date.now(),
|
||||
retryCount: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Minimal SyncManager for tests with mock API simulation
|
||||
class TestSyncManager {
|
||||
constructor(queue, indexedDB) {
|
||||
this.queue = queue;
|
||||
this.indexedDB = indexedDB;
|
||||
this.isOnline = navigator.onLine;
|
||||
this.maxRetries = 5;
|
||||
this.setupNetworkListeners();
|
||||
}
|
||||
|
||||
setupNetworkListeners() {
|
||||
window.addEventListener('online', () => {
|
||||
this.isOnline = true;
|
||||
console.log('[TestSyncManager] Network online');
|
||||
});
|
||||
|
||||
window.addEventListener('offline', () => {
|
||||
this.isOnline = false;
|
||||
console.log('[TestSyncManager] Network offline');
|
||||
});
|
||||
}
|
||||
|
||||
async triggerManualSync() {
|
||||
console.log('[TestSyncManager] Manual sync triggered');
|
||||
|
||||
// Check if online before syncing
|
||||
if (!this.isOnline) {
|
||||
console.warn('[TestSyncManager] ⚠️ Cannot sync - offline mode');
|
||||
throw new Error('Cannot sync while offline');
|
||||
}
|
||||
|
||||
const queueItems = await this.queue.getAll();
|
||||
console.log(`[TestSyncManager] Queue has ${queueItems.length} items`);
|
||||
|
||||
if (queueItems.length === 0) {
|
||||
console.log('[TestSyncManager] Queue is empty - nothing to sync');
|
||||
return [];
|
||||
}
|
||||
|
||||
// Process each operation
|
||||
for (const operation of queueItems) {
|
||||
await this.processOperation(operation);
|
||||
}
|
||||
|
||||
return queueItems;
|
||||
}
|
||||
|
||||
async processOperation(operation) {
|
||||
console.log(`[TestSyncManager] Processing operation ${operation.id} (retry: ${operation.retryCount})`);
|
||||
|
||||
// Check if max retries exceeded
|
||||
if (operation.retryCount >= this.maxRetries) {
|
||||
console.error(`[TestSyncManager] Max retries (${this.maxRetries}) exceeded for operation ${operation.id}`);
|
||||
await this.queue.remove(operation.id);
|
||||
await this.markEventAsError(operation.eventId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Simulate API call with delay
|
||||
await this.simulateApiCall();
|
||||
|
||||
// Simulate success (80%) or failure (20%)
|
||||
const success = Math.random() > 0.2;
|
||||
|
||||
if (success) {
|
||||
console.log(`[TestSyncManager] ✓ Operation ${operation.id} synced successfully`);
|
||||
await this.queue.remove(operation.id);
|
||||
await this.markEventAsSynced(operation.eventId);
|
||||
} else {
|
||||
console.warn(`[TestSyncManager] ✗ Operation ${operation.id} failed - will retry`);
|
||||
await this.queue.incrementRetryCount(operation.id);
|
||||
}
|
||||
}
|
||||
|
||||
async simulateApiCall() {
|
||||
// Simulate network delay (100-500ms)
|
||||
const delay = Math.floor(Math.random() * 400) + 100;
|
||||
return new Promise(resolve => setTimeout(resolve, delay));
|
||||
}
|
||||
|
||||
async markEventAsSynced(eventId) {
|
||||
try {
|
||||
const event = await this.indexedDB.getEvent(eventId);
|
||||
if (event) {
|
||||
event.syncStatus = 'synced';
|
||||
await this.indexedDB.saveEvent(event);
|
||||
console.log(`[TestSyncManager] Event ${eventId} marked as synced`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[TestSyncManager] Failed to mark event ${eventId} as synced:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
async markEventAsError(eventId) {
|
||||
try {
|
||||
const event = await this.indexedDB.getEvent(eventId);
|
||||
if (event) {
|
||||
event.syncStatus = 'error';
|
||||
await this.indexedDB.saveEvent(event);
|
||||
console.log(`[TestSyncManager] Event ${eventId} marked as error`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[TestSyncManager] Failed to mark event ${eventId} as error:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize test environment
|
||||
async function initializeTestEnvironment() {
|
||||
console.log('[Test Init] Initializing test environment...');
|
||||
|
||||
const indexedDB = new TestIndexedDBService();
|
||||
await indexedDB.initialize();
|
||||
console.log('[Test Init] IndexedDB initialized');
|
||||
|
||||
const queue = new TestOperationQueue(indexedDB);
|
||||
console.log('[Test Init] Operation queue created');
|
||||
|
||||
const eventManager = new TestEventManager(indexedDB, queue);
|
||||
console.log('[Test Init] Event manager created');
|
||||
|
||||
const syncManager = new TestSyncManager(queue, indexedDB);
|
||||
console.log('[Test Init] Sync manager created');
|
||||
|
||||
// Seed with test data if empty
|
||||
const existingEvents = await indexedDB.getAllEvents();
|
||||
if (existingEvents.length === 0) {
|
||||
console.log('[Test Init] Seeding with test data...');
|
||||
try {
|
||||
const response = await fetch('test-events.json');
|
||||
const testEvents = await response.json();
|
||||
for (const event of testEvents) {
|
||||
const savedEvent = {
|
||||
...event,
|
||||
start: new Date(event.start),
|
||||
end: new Date(event.end)
|
||||
};
|
||||
await indexedDB.saveEvent(savedEvent);
|
||||
|
||||
// If event is pending, also add to queue
|
||||
if (event.syncStatus === 'pending') {
|
||||
await queue.enqueue({
|
||||
type: 'create',
|
||||
eventId: event.id,
|
||||
data: savedEvent,
|
||||
timestamp: Date.now(),
|
||||
retryCount: 0
|
||||
});
|
||||
console.log(`[Test Init] Added pending event ${event.id} to queue`);
|
||||
}
|
||||
}
|
||||
console.log(`[Test Init] Seeded ${testEvents.length} test events`);
|
||||
} catch (error) {
|
||||
console.warn('[Test Init] Could not load test-events.json:', error);
|
||||
}
|
||||
} else {
|
||||
console.log(`[Test Init] IndexedDB already has ${existingEvents.length} events`);
|
||||
}
|
||||
|
||||
// Expose to window
|
||||
window.calendarDebug = {
|
||||
indexedDB,
|
||||
queue,
|
||||
eventManager,
|
||||
syncManager
|
||||
};
|
||||
|
||||
console.log('[Test Init] Test environment ready');
|
||||
return { indexedDB, queue, eventManager, syncManager };
|
||||
}
|
||||
|
||||
// Auto-initialize if script is loaded
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initializeTestEnvironment().catch(error => {
|
||||
console.error('[Test Init] Failed to initialize:', error);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
initializeTestEnvironment().catch(error => {
|
||||
console.error('[Test Init] Failed to initialize:', error);
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue