Skip to content

Commit 9bce44e

Browse files
committed
test(desktop): diagnose native Folder Access popup readiness
1 parent 1897cbb commit 9bce44e

1 file changed

Lines changed: 195 additions & 2 deletions

File tree

‎apps/desktop/e2e/smoke.spec.ts‎

Lines changed: 195 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { mkdtempSync } from 'node:fs'
1+
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'
22
import type { Server } from 'node:http'
33
import { createServer } from 'node:http'
44
import { tmpdir } from 'node:os'
5-
import { join } from 'node:path'
5+
import { dirname, join } from 'node:path'
66
import { fileURLToPath } from 'node:url'
77
import type { ElectronApplication } from '@playwright/test'
88
import { _electron as electron, expect, test } from '@playwright/test'
9+
import { getErrorMessage } from '@sim/utils/errors'
910

1011
const DESKTOP_DIR = fileURLToPath(new URL('..', import.meta.url))
1112

@@ -66,6 +67,7 @@ async function launchApp(origin: string): Promise<ElectronApplication> {
6667
}
6768

6869
test.describe('desktop shell smoke', () => {
70+
test.describe.configure({ retries: 0 })
6971
let server: Server
7072
let origin: string
7173
let app: ElectronApplication
@@ -89,6 +91,197 @@ test.describe('desktop shell smoke', () => {
8991
expect(window.url()).toBe(`${origin}/home`)
9092
})
9193

94+
for (let iteration = 1; iteration <= 10; iteration++) {
95+
test(`Folder Access opens from the focused main window and server picker (${iteration})`, async () => {
96+
const checks: {
97+
name: string
98+
status: 'passed' | 'failed'
99+
durationMs: number
100+
shown?: boolean
101+
closed?: boolean
102+
callbackFinished?: boolean
103+
windowBefore?: unknown
104+
windowAfter?: unknown
105+
events?: unknown
106+
parentUrl?: string | null
107+
error?: string
108+
}[] = []
109+
const reportPath =
110+
process.env.DESKTOP_FOLDER_ACCESS_MENU_REPORT_PATH ??
111+
test.info().outputPath('folder-access-menu.json')
112+
for (const surface of ['main window', 'server picker']) {
113+
const started = Date.now()
114+
const check: (typeof checks)[number] = {
115+
name: `Folder Access opens in the ${surface}`,
116+
status: 'failed',
117+
durationMs: 0,
118+
}
119+
checks.push(check)
120+
try {
121+
if (surface === 'main window') {
122+
app = await launchApp(origin)
123+
const window = await app.firstWindow()
124+
await expect(window.locator('#app')).toHaveText('fixture-app')
125+
}
126+
const expectedUrl =
127+
surface === 'main window' ? `${origin}/home` : 'sim-shell://pages/server.html'
128+
if (surface === 'server picker') {
129+
const opened = app.waitForEvent('window')
130+
await app.evaluate(({ Menu }) => {
131+
const item = Menu.getApplicationMenu()
132+
?.items.flatMap((entry) => entry.submenu?.items ?? [])
133+
.find((entry) => entry.label === 'Server…')
134+
if (!item) throw new Error('Server menu item missing')
135+
item.click()
136+
})
137+
const picker = await opened
138+
await expect(picker).toHaveURL(expectedUrl)
139+
await expect(picker.getByLabel('Server URL')).toBeFocused()
140+
}
141+
await expect
142+
.poll(
143+
() =>
144+
app.evaluate(
145+
({ BrowserWindow }, url) =>
146+
BrowserWindow.getAllWindows()
147+
.find((candidate) => candidate.webContents.getURL() === url)
148+
?.isVisible(),
149+
expectedUrl
150+
),
151+
{ message: `The ${surface} must be natively visible before menu interaction` }
152+
)
153+
.toBe(true)
154+
await app.evaluate(({ app, BrowserWindow }, url) => {
155+
const focused = BrowserWindow.getAllWindows().find(
156+
(candidate) => candidate.webContents.getURL() === url
157+
)
158+
if (!focused) throw new Error('Expected menu owner is missing')
159+
app.focus({ steal: true })
160+
focused.focus()
161+
}, expectedUrl)
162+
await expect
163+
.poll(() =>
164+
app.evaluate(({ BrowserWindow }) =>
165+
BrowserWindow.getFocusedWindow()?.webContents.getURL()
166+
)
167+
)
168+
.toBe(expectedUrl)
169+
const observation = await app.evaluateHandle(({ BrowserWindow, Menu }) => {
170+
const focused = BrowserWindow.getFocusedWindow()
171+
const item = Menu.getApplicationMenu()
172+
?.items.flatMap((entry) => entry.submenu?.items ?? [])
173+
.find((entry) => entry.label === 'Folder Access…')
174+
if (!focused || !item) throw new Error('Folder Access menu or focused window missing')
175+
const state: {
176+
shown: boolean
177+
parentUrl: string | null
178+
closed: boolean
179+
menu: Electron.Menu | null
180+
owner: Electron.BrowserWindow
181+
callbackFinished: boolean
182+
events: { name: string; at: number }[]
183+
windowBefore: unknown
184+
} = {
185+
shown: false,
186+
parentUrl: null,
187+
closed: false,
188+
menu: null,
189+
owner: focused,
190+
callbackFinished: false,
191+
events: [{ name: 'menu-invoked', at: Date.now() }],
192+
windowBefore: {
193+
visible: focused.isVisible(),
194+
focused: focused.isFocused(),
195+
modal: focused.isModal(),
196+
bounds: focused.getBounds(),
197+
parentId: focused.getParentWindow()?.id ?? null,
198+
url: focused.webContents.getURL(),
199+
},
200+
}
201+
const originalPopup = Menu.prototype.popup
202+
Menu.prototype.popup = function (options) {
203+
state.menu = this
204+
const parent = options?.window
205+
if (parent instanceof BrowserWindow) state.parentUrl = parent.webContents.getURL()
206+
this.once('menu-will-show', () => {
207+
state.shown = true
208+
state.events.push({ name: 'menu-will-show', at: Date.now() })
209+
})
210+
this.once('menu-will-close', () => {
211+
state.closed = true
212+
state.events.push({ name: 'menu-will-close', at: Date.now() })
213+
})
214+
originalPopup.call(this, {
215+
...options,
216+
callback: () => {
217+
state.callbackFinished = true
218+
state.events.push({ name: 'popup-callback', at: Date.now() })
219+
options?.callback?.()
220+
},
221+
})
222+
}
223+
try {
224+
item.click(undefined, focused, focused.webContents)
225+
} finally {
226+
Menu.prototype.popup = originalPopup
227+
}
228+
return state
229+
})
230+
try {
231+
await expect
232+
.poll(() => observation.evaluate(({ shown, parentUrl }) => ({ shown, parentUrl })), {
233+
message: `Folder Access should open in the ${surface}`,
234+
})
235+
.toEqual({ shown: true, parentUrl: expectedUrl })
236+
await observation.evaluate(({ menu }) => menu?.closePopup())
237+
await expect
238+
.poll(() => observation.evaluate(({ closed }) => closed), {
239+
message: `Folder Access should close in the ${surface}`,
240+
})
241+
.toBe(true)
242+
} finally {
243+
Object.assign(
244+
check,
245+
await observation.evaluate(
246+
({ shown, parentUrl, closed, callbackFinished, owner, windowBefore, events }) => ({
247+
shown,
248+
parentUrl,
249+
closed,
250+
callbackFinished,
251+
windowBefore,
252+
events,
253+
windowAfter: owner.isDestroyed()
254+
? { destroyed: true }
255+
: {
256+
visible: owner.isVisible(),
257+
focused: owner.isFocused(),
258+
modal: owner.isModal(),
259+
bounds: owner.getBounds(),
260+
parentId: owner.getParentWindow()?.id ?? null,
261+
url: owner.webContents.getURL(),
262+
},
263+
})
264+
)
265+
)
266+
await observation.evaluate(({ menu }) => menu?.closePopup())
267+
await observation.dispose()
268+
}
269+
check.status = 'passed'
270+
} catch (error) {
271+
check.error = getErrorMessage(error)
272+
throw error
273+
} finally {
274+
check.durationMs = Date.now() - started
275+
mkdirSync(dirname(reportPath), { recursive: true })
276+
writeFileSync(reportPath, JSON.stringify({ iteration, checks }, null, 2))
277+
process.stdout.write(
278+
`FOLDER_ACCESS_MENU_REPORT ${JSON.stringify({ iteration, checks })}\n`
279+
)
280+
}
281+
}
282+
})
283+
}
284+
92285
test('presents one stock Chrome user agent on every request from the first load', async () => {
93286
requestUserAgents.length = 0
94287
app = await launchApp(origin)

0 commit comments

Comments
 (0)