Files
openccb/e2e/tests/student-offline-sync.spec.ts
Nurfog e72f479639 feat: add PWA support with service worker, offline sync, and connectivity banners
- Added PWA icons for 192x192 and 512x512 resolutions.
- Implemented service worker (sw.js) for caching static assets and handling fetch requests.
- Created ConnectivityBanner component to notify users of online/offline status.
- Developed OfflineSyncPanel component to manage and display offline sync status.
- Introduced PwaInstallPrompt component to prompt users for PWA installation.
- Added PwaRegistration component to handle service worker registration and online event handling.
- Created AdminAiAuditPage for AI audit logs with filtering and review functionality.
- Developed AdminDataEthicsPage to display AI data ethics summary and recent events.
2026-04-24 09:59:57 -04:00

90 lines
3.4 KiB
TypeScript

import { test, expect } from '@playwright/test';
test.describe('Student Offline Sync Flow', () => {
test('should keep pending mutations offline and sync them when back online', async ({ page, context }) => {
const offlineQueue = [
{
id: 'q-grade-1',
dedupeKey: 'POST:/grades:{"user_id":"u1","course_id":"c1","lesson_id":"l1","score":0.9,"metadata":{}}',
kind: 'grade',
url: '/grades',
method: 'POST',
isCMS: false,
body: JSON.stringify({ user_id: 'u1', course_id: 'c1', lesson_id: 'l1', score: 0.9, metadata: {} }),
createdAt: new Date().toISOString(),
},
{
id: 'q-interaction-1',
dedupeKey: 'POST:/lessons/l1/interactions:{"event_type":"heartbeat","video_timestamp":21}',
kind: 'interaction',
url: '/lessons/l1/interactions',
method: 'POST',
isCMS: false,
body: JSON.stringify({ event_type: 'heartbeat', video_timestamp: 21 }),
createdAt: new Date().toISOString(),
},
];
await page.addInitScript((queue) => {
localStorage.setItem('experience_offline_mutation_queue_v1', JSON.stringify(queue));
localStorage.setItem('experience_offline_sync_meta_v1', JSON.stringify({
lastSyncAt: null,
lastFlushedCount: 0,
lastError: null,
}));
}, offlineQueue);
await page.goto('/auth/login');
// Open sync panel details.
await page.getByRole('button', { name: /sync offline/i }).click();
// Simulate offline mode; pending should remain after manual sync attempt.
await context.setOffline(true);
await page.getByRole('button', { name: /sincronizar ahora/i }).click();
await expect(page.getByText(/2 pendiente/i)).toBeVisible();
let gradesHits = 0;
let interactionsHits = 0;
await context.setOffline(false);
await page.route('**/lms-api/grades', async (route) => {
gradesHits += 1;
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
id: 'grade-server-1',
user_id: 'u1',
course_id: 'c1',
lesson_id: 'l1',
score: 0.9,
attempts_count: 1,
metadata: {},
created_at: new Date().toISOString(),
}),
});
});
await page.route('**/lms-api/lessons/l1/interactions', async (route) => {
interactionsHits += 1;
await route.fulfill({ status: 204, body: '' });
});
await page.getByRole('button', { name: /sincronizar ahora/i }).click();
await expect.poll(async () => {
return await page.evaluate(() => {
const raw = localStorage.getItem('experience_offline_mutation_queue_v1');
const queue = raw ? JSON.parse(raw) : [];
return queue.length;
});
}).toBe(0);
expect(gradesHits).toBe(1);
expect(interactionsHits).toBe(1);
await expect(page.getByText(/0 pendientes|0 pendiente/i)).toBeVisible();
});
});