e72f479639
- 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.
90 lines
3.4 KiB
TypeScript
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();
|
|
});
|
|
});
|