From 184b2aebab883e133113206c4fca834986c1ec64 Mon Sep 17 00:00:00 2001 From: ethernet Date: Sun, 6 Sep 2026 00:00:56 -0400 Subject: [PATCH] fix(install-e2e): await zoom IPC outside Playwright's predicate Playwright 1.58 accepts a Promise-valued waitForFunction predicate before its false result. Await each zoom read explicitly; cover delayed responses and fresh-install startup. --- apps/desktop/e2e/onboarding-settings.spec.ts | 30 +++++++++++++- apps/desktop/e2e/window-input.unit.test.ts | 43 ++++++++++++++++++++ tests/install/e2e-assets/window-input.cjs | 13 ++++-- 3 files changed, 80 insertions(+), 6 deletions(-) create mode 100644 apps/desktop/e2e/window-input.unit.test.ts diff --git a/apps/desktop/e2e/onboarding-settings.spec.ts b/apps/desktop/e2e/onboarding-settings.spec.ts index 3608f500db..6fe1253cdb 100644 --- a/apps/desktop/e2e/onboarding-settings.spec.ts +++ b/apps/desktop/e2e/onboarding-settings.spec.ts @@ -1,14 +1,39 @@ -import { readFileSync } from 'node:fs' +import { readFileSync, unlinkSync, writeFileSync } from 'node:fs' import { createRequire } from 'node:module' import path from 'node:path' -import { setupNoProvider } from './fixtures' +import { buildAppEnv, createSandbox, launchDesktop, setupNoProvider } from './fixtures' import { type ElectronApplication, expect, type Page, test } from './test' const { prepareWindowForInput } = createRequire(import.meta.url)( '../../../tests/install/e2e-assets/window-input.cjs', ) as { prepareWindowForInput: (app: ElectronApplication, page: Page) => Promise } +test('input setup survives a fresh-install zoom restore before onboarding', async () => { + const sandbox = createSandbox('cold-input') + unlinkSync(path.join(sandbox.userDataDir, 'zoom-state.json')) + writeFileSync(path.join(sandbox.hermesHome, 'config.yaml'), '# no provider\n', 'utf8') + let app: ElectronApplication | undefined + + try { + const launched = await launchDesktop(buildAppEnv(sandbox)) + app = launched.app + const page = launched.page + await page.waitForSelector('button', { state: 'attached' }) + await prepareWindowForInput(app, page) + const later = page.getByRole('button', { name: /choose a provider later/i }) + await expect(later).toBeVisible({ timeout: 60_000 }) + const appWindow = await app.browserWindow(page) + await appWindow.evaluate(win => win.emit('focus')) + await expect.poll(() => appWindow.evaluate(win => win.webContents.getZoomFactor())).toBeCloseTo(1) + await later.click({ timeout: 5_000 }) + await expect(later).toBeHidden() + } finally { + await app?.close().catch(() => undefined) + sandbox.cleanup() + } +}) + // Exercise the install driver's input setup against the real renderer/backend, // with no installer, update, credentials, or live user data. for (const lifecycleEvent of ['focus', 'navigation'] as const) { @@ -17,6 +42,7 @@ for (const lifecycleEvent of ['focus', 'navigation'] as const) { const { app, page, sandbox } = fixture try { + await prepareWindowForInput(app, page) const later = page.getByRole('button', { name: /choose a provider later/i }) await expect(later).toBeVisible({ timeout: 60_000 }) const zoomFile = path.join(sandbox.userDataDir, 'zoom-state.json') diff --git a/apps/desktop/e2e/window-input.unit.test.ts b/apps/desktop/e2e/window-input.unit.test.ts new file mode 100644 index 0000000000..a8870329c2 --- /dev/null +++ b/apps/desktop/e2e/window-input.unit.test.ts @@ -0,0 +1,43 @@ +import { createRequire } from 'node:module' + +import { expect, test } from 'vitest' + +const { prepareWindowForInput } = createRequire(import.meta.url)( + '../../../tests/install/e2e-assets/window-input.cjs', +) + +test('awaits the zoom response instead of accepting a truthy Promise', async () => { + let reads = 0 + let factor = 0.9 + + const zoom = { + setPercent: () => undefined, + get: async () => { + reads++ + + if (reads > 1) {factor = 1} + + return { percent: factor * 100 } + }, + } + + const previous = (globalThis as any).hermesDesktop + + ;(globalThis as any).hermesDesktop = { zoom } + const window = { evaluate: async (fn: any) => fn({ webContents: { getZoomFactor: () => factor } }) } + + const page = { + evaluate: async (fn: any) => fn(), + // Playwright 1.58 accepts the predicate's Promise before it resolves. + waitForFunction: async (fn: any) => { await fn() }, + waitForTimeout: async () => undefined, + } + + try { + await prepareWindowForInput({ browserWindow: async () => window }, page) + expect(reads).toBeGreaterThan(1) + expect(factor).toBe(1) + } finally { + ;(globalThis as any).hermesDesktop = previous + } +}) diff --git a/tests/install/e2e-assets/window-input.cjs b/tests/install/e2e-assets/window-input.cjs index a05846209f..8dccfa0ccd 100644 --- a/tests/install/e2e-assets/window-input.cjs +++ b/tests/install/e2e-assets/window-input.cjs @@ -11,10 +11,15 @@ async function prepareWindowForInput(app, page) { return true }) if (persistent) { - await page.waitForFunction(async () => { - const state = await globalThis.hermesDesktop.zoom.get() - return state.percent === 100 - }, undefined, { timeout: 15_000 }) + // Playwright 1.58 treats an async waitForFunction predicate's Promise as + // truthy even when it resolves false. Await each IPC read on the driver. + const deadline = Date.now() + 15_000 + for (;;) { + const state = await page.evaluate(() => globalThis.hermesDesktop.zoom.get()) + if (state.percent === 100) break + if (Date.now() >= deadline) throw new Error('timed out waiting for 100% app window zoom') + await page.waitForTimeout(100) + } } else { // Older sampled releases have no zoom preference bridge. await window.evaluate(win => win.webContents.setZoomLevel(0))