diff --git a/tests/smoke/focus-no-freeze.spec.ts b/tests/smoke/focus-no-freeze.spec.ts new file mode 100644 index 00000000..53af395e --- /dev/null +++ b/tests/smoke/focus-no-freeze.spec.ts @@ -0,0 +1,50 @@ +import { test, expect } from '@playwright/test' + +test.describe('Focus event does not freeze the UI', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + }) + + test('window focus event completes without blocking UI for >500ms', async ({ page }) => { + // Verify the app loaded + await expect(page.locator('[data-testid="sidebar"]').or(page.locator('nav')).first()).toBeVisible() + + // Measure how long the UI is blocked after a focus event. + // We dispatch focus, then immediately schedule a rAF callback. + // If the main thread is blocked (sync IPC), the rAF callback is delayed. + const blockMs = await page.evaluate(() => { + return new Promise((resolve) => { + const t0 = performance.now() + window.dispatchEvent(new Event('focus')) + requestAnimationFrame(() => { + resolve(performance.now() - t0) + }) + }) + }) + + // The focus handler must not block the main thread for more than 500ms. + // Before the fix, git_pull ran synchronously and blocked for 2-3 seconds. + expect(blockMs).toBeLessThan(500) + }) + + test('rapid focus events (5x) do not accumulate freezes', async ({ page }) => { + await expect(page.locator('[data-testid="sidebar"]').or(page.locator('nav')).first()).toBeVisible() + + const totalMs = await page.evaluate(() => { + return new Promise((resolve) => { + const t0 = performance.now() + for (let i = 0; i < 5; i++) { + window.dispatchEvent(new Event('focus')) + } + requestAnimationFrame(() => { + resolve(performance.now() - t0) + }) + }) + }) + + // 5 rapid focus events should still complete in under 500ms total + // thanks to the cooldown preventing redundant work. + expect(totalMs).toBeLessThan(500) + }) +})