sim: merge PR #17192

This commit is contained in:
Brennan Benson
2026-08-30 15:53:52 -07:00
3 changed files with 65 additions and 0 deletions
@@ -92,6 +92,20 @@ describe('native preload destructive app actions', () => {
})
}
// Why: the lazy-chunk recovery reload joins this member through `?.()`, so leaving
// it off the bridge reads as "checkpoint fine" and reloads over unsaved buffers.
it('exposes the durable checkpoint join the recovery reload refuses on', async () => {
const api = await loadApi()
invoke.mockImplementation(async (channel: string) =>
channel === 'app:await-before-unload-checkpoint' ? { ok: false } : undefined
)
expect(typeof api.app.awaitBeforeUnloadCheckpoint).toBe('function')
await expect(api.app.awaitBeforeUnloadCheckpoint()).rejects.toThrow(
'Failed to persist renderer state before unload.'
)
})
it('preserves both macOS keyboard preload adapters', async () => {
const api = await loadApi()
invoke.mockResolvedValue(undefined)
+4
View File
@@ -594,6 +594,7 @@ const api = {
throw new Error('Failed to stage renderer state before unload.')
}
},
awaitBeforeUnloadCheckpoint,
awaitFirstWindowStartupServices: (): Promise<void> =>
ipcRenderer.invoke('app:awaitFirstWindowStartupServices'),
prepareTerminalStartupRestoration: (): Promise<void> =>
@@ -5328,6 +5329,9 @@ const api = {
}
}
/** The bridge object the renderer receives; checked against PreloadApi in preload-api-implementation-completeness.ts. */
export type PreloadApiImplementation = typeof api
// Expose Electron APIs via contextBridge when context-isolated, otherwise attach to the DOM global.
if (process.contextIsolated) {
try {
@@ -0,0 +1,47 @@
import type { PreloadApi } from './api-types'
import type { PreloadApiImplementation } from './index'
/**
* Compile-time proof that the preload bridge implements every member the
* renderer is allowed to call.
*
* Why this is not redundant with the annotations in index.ts: `api` is handed to
* `contextBridge.exposeInMainWorld`, which takes `any`, and the renderer types
* every call as `PreloadApi` (api-types.ts). Nothing compared the two, so a
* member declared in `PreloadApi` and never implemented reached the renderer as
* `undefined` — and callers guard with `?.()`, which turns the absence into a
* confident default rather than a failure.
*
* Names only, deliberately: `ipcRenderer.invoke` returns `Promise<unknown>`, so
* full assignability reports pre-existing return-type widenings that the call
* sites already cast. Presence is the half that fails silently.
*/
type AssertNoMissingMembers<Missing extends never> = Missing
type UnimplementedGroups = Exclude<keyof PreloadApi, keyof PreloadApiImplementation>
type ObjectMembers<T> = T extends object
? T extends (...args: never[]) => unknown
? never
: T
: never
/** Recurses through nested API records while treating callable leaves as terminals. */
type UnimplementedMembers<Expected, Actual, Prefix extends string = ''> =
| `${Prefix}${Extract<Exclude<keyof Expected, keyof Actual>, string>}`
| {
[Key in keyof Expected & keyof Actual]: ObjectMembers<Expected[Key]> extends never
? never
: ObjectMembers<Actual[Key]> extends never
? `${Prefix}${Extract<Key, string>}`
: UnimplementedMembers<
ObjectMembers<Expected[Key]>,
ObjectMembers<Actual[Key]>,
`${Prefix}${Extract<Key, string>}.`
>
}[keyof Expected & keyof Actual]
export type PreloadApiGroupsAreImplemented = AssertNoMissingMembers<UnimplementedGroups>
export type PreloadApiMembersAreImplemented = AssertNoMissingMembers<
UnimplementedMembers<PreloadApi, PreloadApiImplementation>
>