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.
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -0,0 +1,89 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user