fix(picker): raise the worker-thread dialog to the foreground on showing
The koffi redesign moved the dialog from a spawned child process (which inherits a foreground-activation right from the spawning app) onto a worker thread of the same process, so Windows shows the dialog without activating it — it opens behind the app with a taskbar flash. The app has no native HWND to hand the seam, so raise from the driver: on the 'showing' notice (the worker posts it right before Show, before the dialog window exists), attach this thread's input queue to the dialog thread's, call SetForegroundWindow on its top-level window, and detach — retried on the close cadence until the window appears, stopped on settle/abort/success, never blocking the pick. Injectable seam mirrors closeThreadWindows; driver tests pin the raise and its retry; the in-process note records the mechanism (both languages, pairing re-recorded).
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
/**
|
||||
* Driver tests: the worker message protocol mapped onto the promise, the
|
||||
* WM_CLOSE abort service (including the show-race retry and the terminate
|
||||
* last resort) against fakes, plus the real spawn plumbing — POSIX hosts
|
||||
* prove the default path rejects cleanly (koffi cannot load ole32 there),
|
||||
* and win32 hosts briefly open and auto-abort a real dialog.
|
||||
* foreground raise after the `showing` notice (retried until the dialog
|
||||
* window exists), the WM_CLOSE abort service (including the show-race
|
||||
* retry and the terminate last resort) against fakes, plus the real spawn
|
||||
* plumbing — POSIX hosts prove the default path rejects cleanly (koffi
|
||||
* cannot load ole32 there), and win32 hosts briefly open and auto-abort a
|
||||
* real dialog.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'node:events'
|
||||
@@ -22,15 +24,24 @@ interface Harness {
|
||||
worker: FakeWorker
|
||||
internals: Win32DialogInternals
|
||||
close: ReturnType<typeof vi.fn>
|
||||
raise: ReturnType<typeof vi.fn>
|
||||
}
|
||||
|
||||
function harness(overrides: Partial<Win32DialogInternals> = {}): Harness {
|
||||
const worker = new FakeWorker()
|
||||
const close = vi.fn(async () => undefined)
|
||||
const raise = vi.fn(async () => true)
|
||||
return {
|
||||
worker,
|
||||
close,
|
||||
internals: { spawnWorker: () => worker, closeThreadWindows: close, closeRetryMs: 1, ...overrides },
|
||||
raise,
|
||||
internals: {
|
||||
spawnWorker: () => worker,
|
||||
closeThreadWindows: close,
|
||||
raiseDialogWindow: raise,
|
||||
closeRetryMs: 1,
|
||||
...overrides,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +62,35 @@ describe('pickWin32Directory', () => {
|
||||
await expect(cancelled).resolves.toBeNull()
|
||||
})
|
||||
|
||||
it('raises the dialog window to the foreground after the showing notice', async () => {
|
||||
const { worker, internals, raise } = harness()
|
||||
const picked = pickWin32Directory(live(), internals)
|
||||
worker.post({ kind: 'showing', threadId: 7 })
|
||||
worker.post({ kind: 'done', path: 'C:\\raised' })
|
||||
await expect(picked).resolves.toBe('C:\\raised')
|
||||
expect(raise).toHaveBeenCalledWith(7)
|
||||
})
|
||||
|
||||
it('retries the raise until the dialog window exists, then stops', async () => {
|
||||
const { worker, internals, raise } = harness()
|
||||
// The window is created inside `Show`, after the `showing` notice, so
|
||||
// the first attempts find nothing; once a window is reported, the raise
|
||||
// must stop retrying.
|
||||
raise.mockResolvedValueOnce(false).mockResolvedValueOnce(false).mockResolvedValue(true)
|
||||
const picked = pickWin32Directory(live(), internals)
|
||||
worker.post({ kind: 'showing', threadId: 12 })
|
||||
await vi.waitFor(() => {
|
||||
expect(raise.mock.calls.length).toBeGreaterThanOrEqual(2)
|
||||
})
|
||||
const callsAfterRaised = await new Promise<number>((resolve) => {
|
||||
setTimeout(() =>{ resolve(raise.mock.calls.length); }, 20)
|
||||
})
|
||||
await new Promise(resolve => setTimeout(resolve, 20))
|
||||
expect(raise.mock.calls.length).toBe(callsAfterRaised)
|
||||
worker.post({ kind: 'done', path: 'C:\\raised' })
|
||||
await expect(picked).resolves.toBe('C:\\raised')
|
||||
})
|
||||
|
||||
it('rejects on a reported dialog failure, a worker crash, and a silent exit', async () => {
|
||||
const reported = harness()
|
||||
const failing = pickWin32Directory(live(), reported.internals)
|
||||
@@ -103,7 +143,7 @@ describe('pickWin32Directory', () => {
|
||||
|
||||
it('starts the close service on the showing notice when the abort came first', async () => {
|
||||
const closeFailures = vi.fn(async () => { throw new Error('window not there yet') })
|
||||
const { worker, internals } = harness({ closeThreadWindows: closeFailures })
|
||||
const { worker, internals, raise } = harness({ closeThreadWindows: closeFailures })
|
||||
const controller = new AbortController()
|
||||
// Attached before the race for the same unhandled-rejection reason above.
|
||||
const picked = expect(pickWin32Directory(controller.signal, internals)).rejects.toThrow('native directory picker aborted')
|
||||
@@ -113,6 +153,7 @@ describe('pickWin32Directory', () => {
|
||||
await vi.waitFor(() => {
|
||||
expect(closeFailures.mock.calls.length).toBeGreaterThan(1)
|
||||
})
|
||||
expect(raise).not.toHaveBeenCalled()
|
||||
worker.post({ kind: 'done', path: null })
|
||||
await picked
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user