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;
|
||||
}
|
||||
Reference in New Issue
Block a user