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:
1 parent
4148de5d66
commit
e72f479639
32 files changed
+2332
-74
No files matched your search
@@ -59,4 +59,4 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
|
||||
</body>
|
||||
</html>
|
||||
<script>
|
||||
window.playwrightReportBase64 = "data:application/zip;base64,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 truncated
|
||||
window.playwrightReportBase64 = "data:application/zip;base64,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";</script>
|
||||
@@ -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