T015
This commit is contained in:
242
apps/web/tests/e2e/smoke.offline.spec.ts
Normal file
242
apps/web/tests/e2e/smoke.offline.spec.ts
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test.describe('Offline PWA Functionality', () => {
|
||||||
|
test('installs SW, works offline, writes mood/entries, persists on reconnect', async ({ page, context }) => {
|
||||||
|
// Step 1: Navigate to app and ensure it loads
|
||||||
|
await page.goto('/');
|
||||||
|
await expect(page.getByRole('heading', { level: 1 })).toHaveText(/GlowTrack/i);
|
||||||
|
|
||||||
|
// Step 2: Wait for service worker to install and become ready
|
||||||
|
// Check that service worker is registered
|
||||||
|
const swRegistration = await page.evaluate(async () => {
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
// Wait for existing registration or new registration
|
||||||
|
let registration = await navigator.serviceWorker.getRegistration();
|
||||||
|
if (!registration) {
|
||||||
|
// Wait a bit for auto-registration to happen
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
registration = await navigator.serviceWorker.getRegistration();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (registration) {
|
||||||
|
// Wait for service worker to be ready
|
||||||
|
await navigator.serviceWorker.ready;
|
||||||
|
return {
|
||||||
|
scope: registration.scope,
|
||||||
|
active: !!registration.active,
|
||||||
|
installing: !!registration.installing,
|
||||||
|
waiting: !!registration.waiting
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify service worker is registered and active
|
||||||
|
expect(swRegistration).toBeTruthy();
|
||||||
|
expect(swRegistration?.active).toBe(true);
|
||||||
|
|
||||||
|
// Step 3: Add some initial data while online
|
||||||
|
const firstTile = page.locator('[data-testid="day-tile"]').first();
|
||||||
|
await expect(firstTile).toBeVisible();
|
||||||
|
|
||||||
|
// Click on tile to open day editor
|
||||||
|
await firstTile.click();
|
||||||
|
|
||||||
|
// Set mood if controls are available
|
||||||
|
const hueInput = page.locator('[data-testid="mood-hue-slider"]');
|
||||||
|
if (await hueInput.isVisible()) {
|
||||||
|
await hueInput.fill('240'); // Blue mood for initial state
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a positive habit
|
||||||
|
const addPositive = page.locator('[data-testid="add-positive-habit"]');
|
||||||
|
if (await addPositive.isVisible()) {
|
||||||
|
await addPositive.click();
|
||||||
|
const habitInput = page.locator('[data-testid="habit-input"]');
|
||||||
|
if (await habitInput.isVisible()) {
|
||||||
|
await habitInput.fill('Online Exercise');
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close day editor
|
||||||
|
const closeButton = page.locator('[data-testid="close-day-editor"]');
|
||||||
|
if (await closeButton.isVisible()) {
|
||||||
|
await closeButton.click();
|
||||||
|
} else {
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for any saves to complete
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Step 4: Capture initial state for comparison
|
||||||
|
const initialState = await captureGridState(page);
|
||||||
|
|
||||||
|
// Step 5: Go offline
|
||||||
|
await context.setOffline(true);
|
||||||
|
|
||||||
|
// Step 6: Verify app still loads when offline
|
||||||
|
await page.reload();
|
||||||
|
await expect(page.getByRole('heading', { level: 1 })).toHaveText(/GlowTrack/i);
|
||||||
|
|
||||||
|
// Verify the grid is still visible and contains our data
|
||||||
|
await expect(firstTile).toBeVisible();
|
||||||
|
|
||||||
|
// Step 7: Modify data while offline
|
||||||
|
await firstTile.click();
|
||||||
|
|
||||||
|
// Change mood while offline
|
||||||
|
const hueInputOffline = page.locator('[data-testid="mood-hue-slider"]');
|
||||||
|
if (await hueInputOffline.isVisible()) {
|
||||||
|
await hueInputOffline.fill('60'); // Yellow mood for offline state
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add another habit while offline
|
||||||
|
const addPositiveOffline = page.locator('[data-testid="add-positive-habit"]');
|
||||||
|
if (await addPositiveOffline.isVisible()) {
|
||||||
|
await addPositiveOffline.click();
|
||||||
|
const habitInputOffline = page.locator('[data-testid="habit-input"]');
|
||||||
|
if (await habitInputOffline.isVisible()) {
|
||||||
|
await habitInputOffline.fill('Offline Reading');
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close day editor
|
||||||
|
const closeButtonOffline = page.locator('[data-testid="close-day-editor"]');
|
||||||
|
if (await closeButtonOffline.isVisible()) {
|
||||||
|
await closeButtonOffline.click();
|
||||||
|
} else {
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for offline saves to complete
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Step 8: Capture offline state
|
||||||
|
const offlineState = await captureGridState(page);
|
||||||
|
|
||||||
|
// Verify that offline changes were applied (state should be different)
|
||||||
|
expect(offlineState).not.toEqual(initialState);
|
||||||
|
|
||||||
|
// Step 9: Go back online
|
||||||
|
await context.setOffline(false);
|
||||||
|
|
||||||
|
// Step 10: Reload and verify data persistence
|
||||||
|
await page.reload();
|
||||||
|
await expect(page.getByRole('heading', { level: 1 })).toHaveText(/GlowTrack/i);
|
||||||
|
|
||||||
|
// Wait for any sync operations to complete
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Step 11: Verify all changes persisted after reconnection
|
||||||
|
const reconnectedState = await captureGridState(page);
|
||||||
|
|
||||||
|
// The reconnected state should match the offline state (data persisted)
|
||||||
|
expect(reconnectedState).toEqual(offlineState);
|
||||||
|
|
||||||
|
// Step 12: Verify we can still make changes after reconnection
|
||||||
|
await firstTile.click();
|
||||||
|
|
||||||
|
// Add one more habit to verify full functionality is restored
|
||||||
|
const addPositiveOnline = page.locator('[data-testid="add-positive-habit"]');
|
||||||
|
if (await addPositiveOnline.isVisible()) {
|
||||||
|
await addPositiveOnline.click();
|
||||||
|
const habitInputOnline = page.locator('[data-testid="habit-input"]');
|
||||||
|
if (await habitInputOnline.isVisible()) {
|
||||||
|
await habitInputOnline.fill('Back Online Meditation');
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close day editor
|
||||||
|
const closeButtonFinal = page.locator('[data-testid="close-day-editor"]');
|
||||||
|
if (await closeButtonFinal.isVisible()) {
|
||||||
|
await closeButtonFinal.click();
|
||||||
|
} else {
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final verification that changes are still being saved
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
const finalState = await captureGridState(page);
|
||||||
|
expect(finalState).not.toEqual(reconnectedState);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('service worker caches essential resources for offline use', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
await expect(page.getByRole('heading', { level: 1 })).toHaveText(/GlowTrack/i);
|
||||||
|
|
||||||
|
// Wait for service worker to be ready
|
||||||
|
await page.evaluate(async () => {
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
await navigator.serviceWorker.ready;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check that essential resources are cached
|
||||||
|
const cacheStatus = await page.evaluate(async () => {
|
||||||
|
if ('caches' in window) {
|
||||||
|
const cacheNames = await caches.keys();
|
||||||
|
let cachedAssets: string[] = [];
|
||||||
|
|
||||||
|
for (const cacheName of cacheNames) {
|
||||||
|
if (cacheName.includes('gt-cache')) {
|
||||||
|
const cache = await caches.open(cacheName);
|
||||||
|
const requests = await cache.keys();
|
||||||
|
cachedAssets = requests.map(req => req.url);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
cacheNames,
|
||||||
|
cachedAssets: cachedAssets.map(url => new URL(url).pathname),
|
||||||
|
hasCaches: cachedAssets.length > 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { cacheNames: [], cachedAssets: [], hasCaches: false };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify that service worker has cached some resources
|
||||||
|
expect(cacheStatus.hasCaches).toBe(true);
|
||||||
|
expect(cacheStatus.cacheNames.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Check for essential assets that should be cached
|
||||||
|
const essentialAssets = cacheStatus.cachedAssets;
|
||||||
|
expect(essentialAssets.some(asset => asset.includes('app') || asset === '/')).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper function to capture grid state for comparison
|
||||||
|
async function captureGridState(page: import('@playwright/test').Page) {
|
||||||
|
const tiles = page.locator('[data-testid="day-tile"]');
|
||||||
|
const count = await tiles.count();
|
||||||
|
const data: Array<Record<string, string | number | boolean | null>> = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < Math.min(count, 10); i++) { // Limit to first 10 tiles for performance
|
||||||
|
const tile = tiles.nth(i);
|
||||||
|
const handle = await tile.elementHandle();
|
||||||
|
if (!handle) continue;
|
||||||
|
|
||||||
|
const tileData = await page.evaluate((el) => {
|
||||||
|
const attr = (name: string) => el.getAttribute(name);
|
||||||
|
const selCount = (sel: string) => el.querySelectorAll(sel).length;
|
||||||
|
return {
|
||||||
|
index: i,
|
||||||
|
date: attr('data-date'),
|
||||||
|
netScore: attr('data-net-score'),
|
||||||
|
moodHue: attr('data-mood-hue'),
|
||||||
|
positiveCount: selCount('[data-testid="positive-glyphs"] [data-testid="tick"]'),
|
||||||
|
negativeCount: selCount('[data-testid="negative-glyphs"] [data-testid="dot"]'),
|
||||||
|
hasContent: !!attr('data-has-content')
|
||||||
|
};
|
||||||
|
}, handle);
|
||||||
|
|
||||||
|
data.push(tileData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
@@ -103,7 +103,7 @@ Integration scenarios from quickstart.md → e2e smoke tests [P]
|
|||||||
- Steps: render month → export PNG (toBlob) → file within size/time budget
|
- Steps: render month → export PNG (toBlob) → file within size/time budget
|
||||||
- Dependencies: T007, T005
|
- Dependencies: T007, T005
|
||||||
|
|
||||||
- [ ] T015 [P] E2E: offline PWA works
|
- [X] T015 [P] E2E: offline PWA works
|
||||||
- Create /home/jawz/Development/Projects/GlowTrack/apps/web/tests/e2e/smoke.offline.spec.ts
|
- Create /home/jawz/Development/Projects/GlowTrack/apps/web/tests/e2e/smoke.offline.spec.ts
|
||||||
- Steps: install SW → go offline → app loads and writes mood/entries; on reconnect, state persists
|
- Steps: install SW → go offline → app loads and writes mood/entries; on reconnect, state persists
|
||||||
- Dependencies: T007, T008, T005
|
- Dependencies: T007, T008, T005
|
||||||
|
|||||||
Reference in New Issue
Block a user