453 lines
13 KiB
JavaScript
453 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);
|
||
|
|
});
|
||
|
|
}
|