Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions workout-tracker/e2e/intersperse-accessories.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,27 @@ test.describe('Intersperse Accessories', () => {
// Wait for the setting to be saved to IndexedDB by confirming checkbox state
await expect(page.locator('[data-testid="intersperse-checkbox"]')).toBeChecked();

// Wait for the async IndexedDB write triggered by the change event to complete
await page.waitForFunction(async () => {
const { openDB } = await import('/src/db/database.ts' as any).catch(() => ({ openDB: null }));
// Fallback: read directly from IndexedDB
return new Promise<boolean>((resolve) => {
const req = indexedDB.open('workout-tracker');
req.onsuccess = () => {
const db = req.result;
const tx = db.transaction('state', 'readonly');
const store = tx.objectStore('state');
const getReq = store.get('settings');
getReq.onsuccess = () => {
const settings = getReq.result;
resolve(settings?.intersperseAccessories === true);
};
getReq.onerror = () => resolve(false);
};
req.onerror = () => resolve(false);
});
});

// Reload page
await page.reload();
await page.waitForSelector('#app');
Expand Down
134 changes: 134 additions & 0 deletions workout-tracker/e2e/wakelock.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { test, expect } from '@playwright/test';

test.describe('Wake Lock', () => {
test('requests wake lock on workout screen and re-acquires after visibility change', async ({ page }) => {
// Mock the Wake Lock API before navigating
await page.addInitScript(() => {
const calls: string[] = [];
(window as any).__wakeLockCalls = calls;

function createSentinel() {
const sentinel = {
released: false,
type: 'screen' as const,
_listeners: [] as Array<() => void>,
addEventListener(_event: string, cb: () => void) {
sentinel._listeners.push(cb);
},
removeEventListener() {},
release() {
sentinel.released = true;
for (const cb of sentinel._listeners) cb();
return Promise.resolve();
},
onrelease: null,
dispatchEvent: () => true,
};
return sentinel;
}

Object.defineProperty(navigator, 'wakeLock', {
value: {
request: (_type: string) => {
calls.push('request');
const sentinel = createSentinel();
(window as any).__currentWakeLockSentinel = sentinel;
return Promise.resolve(sentinel);
},
},
configurable: true,
});
});

await page.goto('/');
await page.waitForSelector('#app');
await page.click('#start-workout-btn');
await page.waitForSelector('.workout-screen');

// Verify initial wake lock was requested
const callsAfterInit = await page.evaluate(() => (window as any).__wakeLockCalls.length);
expect(callsAfterInit).toBeGreaterThanOrEqual(1);

// Simulate page becoming hidden then visible (as happens when switching apps on iOS).
// When the page goes hidden, the browser releases the wake lock sentinel automatically.
await page.evaluate(() => {
// Release the current sentinel (simulates browser behavior on hide)
const sentinel = (window as any).__currentWakeLockSentinel;
if (sentinel) sentinel.release();

Object.defineProperty(document, 'visibilityState', { value: 'hidden', configurable: true });
document.dispatchEvent(new Event('visibilitychange'));
});

// Simulate becoming visible again — should re-acquire
await page.evaluate(() => {
Object.defineProperty(document, 'visibilityState', { value: 'visible', configurable: true });
document.dispatchEvent(new Event('visibilitychange'));
});

// Wait briefly for the async re-request
await page.waitForTimeout(100);

const callsAfterReacquire = await page.evaluate(() => (window as any).__wakeLockCalls.length);
expect(callsAfterReacquire).toBeGreaterThanOrEqual(2);
});

test('does not re-acquire wake lock after it has been explicitly released', async ({ page }) => {
await page.addInitScript(() => {
const calls: string[] = [];
(window as any).__wakeLockCalls = calls;

Object.defineProperty(navigator, 'wakeLock', {
value: {
request: (_type: string) => {
calls.push('request');
const sentinel = {
released: false,
type: 'screen' as const,
_listeners: [] as Array<() => void>,
addEventListener(_event: string, cb: () => void) {
this._listeners.push(cb);
},
removeEventListener() {},
release() {
this.released = true;
for (const cb of this._listeners) cb();
return Promise.resolve();
},
onrelease: null,
dispatchEvent: () => true,
};
return Promise.resolve(sentinel);
},
},
configurable: true,
});
});

await page.goto('/');
await page.waitForSelector('#app');
await page.click('#start-workout-btn');
await page.waitForSelector('.workout-screen');

const callsBeforeBack = await page.evaluate(() => (window as any).__wakeLockCalls.length);

// Navigate away (releases wake lock explicitly)
await page.click('#back-btn');
await page.waitForSelector('h1');

// Simulate visibility change — should NOT re-acquire
await page.evaluate(() => {
Object.defineProperty(document, 'visibilityState', { value: 'hidden', configurable: true });
document.dispatchEvent(new Event('visibilitychange'));
});
await page.evaluate(() => {
Object.defineProperty(document, 'visibilityState', { value: 'visible', configurable: true });
document.dispatchEvent(new Event('visibilitychange'));
});

await page.waitForTimeout(100);

const callsAfterBack = await page.evaluate(() => (window as any).__wakeLockCalls.length);
expect(callsAfterBack).toBe(callsBeforeBack);
});
});
8 changes: 5 additions & 3 deletions workout-tracker/src/ui/wakelock.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
let wakeLock: WakeLockSentinel | null = null;
let wakeLockActive = false;

export async function requestWakeLock(): Promise<void> {
wakeLockActive = true;
try {
if ('wakeLock' in navigator) {
wakeLock = await navigator.wakeLock.request('screen');
Expand All @@ -14,14 +16,14 @@ export async function requestWakeLock(): Promise<void> {
}

export function releaseWakeLock(): void {
wakeLockActive = false;
wakeLock?.release();
wakeLock = null;
}

// Re-acquire wake lock when page becomes visible again
document.addEventListener('visibilitychange', async () => {
if (document.visibilityState === 'visible' && wakeLock === null) {
// Only re-request if we had one before (i.e., during active workout)
// The workout screen will call requestWakeLock() itself
if (document.visibilityState === 'visible' && wakeLock === null && wakeLockActive) {
await requestWakeLock();
}
});