This commit is contained in:
2025-09-18 11:53:53 -06:00
parent d3d24223e8
commit a3d0f8c4c1
2 changed files with 243 additions and 1 deletions

View 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;
}