Calendar/test/integrationtesting/test-init.js
Janus C. H. Knudsen e7011526e3 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
2025-11-05 00:37:57 +01:00

452 lines
13 KiB
JavaScript

/**
* 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);
});
}