From 02b6bde705b02dc14c8f10d06af0e9fa7111206a Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Wed, 28 Oct 2026 11:29:23 -0700 Subject: [PATCH] Retire extension notification records on native activation (#35139) Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> --- .../extensions/api/notifications.test.ts | 33 +++++++++++++++++++ .../browser/extensions/api/notifications.ts | 2 ++ 2 files changed, 35 insertions(+) diff --git a/src/main/browser/extensions/api/notifications.test.ts b/src/main/browser/extensions/api/notifications.test.ts index b099b9db9ca..d7d2902a5c6 100644 --- a/src/main/browser/extensions/api/notifications.test.ts +++ b/src/main/browser/extensions/api/notifications.test.ts @@ -37,6 +37,7 @@ const { registerNotificationsNamespace, resolveNotificationIconPath } = await import('./notifications') const { getBrowserExtensionNamespace } = await import('../browser-extension-namespace-registry') const { emitBrowserExtensionEvent } = await import('../browser-extension-api-host') +const { noteBrowserExtensionUserGesture } = await import('./permissions') registerNotificationsNamespace() const extension = { id: 'a'.repeat(32), path: join('/ext', 'root') } @@ -82,6 +83,38 @@ function invoke(caller: BrowserExtensionCaller, method: string, args: unknown[] const options = { type: 'basic', title: 'Notification', message: 'Retire with its extension' } describe('notification extension ownership', () => { + it('retires an activated notification while preserving click delivery and its successor', () => { + const { caller } = notificationCaller('clicked') + const sibling = { ...caller, extension: { ...caller.extension, id: 'b'.repeat(32) } } + invoke(caller, 'create', ['clicked', options]) + invoke(sibling, 'create', ['sibling', options]) + const clicked = mocks.notifications[0] + + clicked.emit('click') + + expect(emitBrowserExtensionEvent).toHaveBeenCalledExactlyOnceWith( + { session: caller.session, extensionId: caller.extension.id }, + 'notifications', + 'onClicked', + ['clicked'] + ) + expect(noteBrowserExtensionUserGesture).toHaveBeenCalledExactlyOnceWith( + caller.session, + caller.extension.id + ) + expect(invoke(caller, 'getAll')).toEqual({}) + expect(invoke(caller, 'clear', ['clicked'])).toBe(false) + expect(invoke(sibling, 'getAll')).toEqual({ sibling: true }) + expect(clicked.close).not.toHaveBeenCalled() + expect(clicked.eventNames()).toEqual([]) + + invoke(caller, 'create', ['clicked', options]) + clicked.emit('click') + clicked.emit('close', {}) + expect(invoke(caller, 'getAll')).toEqual({ clicked: true }) + expect(emitBrowserExtensionEvent).toHaveBeenCalledTimes(1) + }) + it.each(['create', 'update'])( 'forgets a failed native %s without reporting a dismissal', (method) => { diff --git a/src/main/browser/extensions/api/notifications.ts b/src/main/browser/extensions/api/notifications.ts index 000f1a10a99..e2105014589 100644 --- a/src/main/browser/extensions/api/notifications.ts +++ b/src/main/browser/extensions/api/notifications.ts @@ -129,6 +129,8 @@ function show(caller: BrowserExtensionCaller, id: string, entry: Shown): void { : {}) }) notification.on('click', () => { + shownFor(caller).delete(id) + detach(entry, false) noteBrowserExtensionUserGesture(caller.session, caller.extension.id) emit(caller, 'onClicked', [id]) })