From f3ecee5da360bdef53c48b2c6c2d9ed9840d6399 Mon Sep 17 00:00:00 2001 From: Karen Rasmussen Date: Tue, 30 Dec 2025 16:21:04 -0300 Subject: [PATCH 1/3] Improve focus order for new annotations --- src/sidebar/services/streamer.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/sidebar/services/streamer.ts b/src/sidebar/services/streamer.ts index cb6b4a918e3..63325da6a64 100644 --- a/src/sidebar/services/streamer.ts +++ b/src/sidebar/services/streamer.ts @@ -1,5 +1,6 @@ import { generateHexString } from '../../shared/random'; import { warnOnce } from '../../shared/warn-once'; +import { tabForAnnotation } from '../helpers/tabs'; import type { SidebarStore } from '../store'; import { watch } from '../util/watch'; import { Socket } from '../websocket'; @@ -95,6 +96,18 @@ export class StreamerService { updates.map(({ id }) => id).filter(Boolean) as string[], ); this._window.setTimeout(() => this._store.highlightAnnotations([]), 5000); + + // Move keyboard focus into the first updated annotation + const sortedUpdates = [...updates] + .filter(annotation => annotation.id) + .sort((a, b) => a.updated.localeCompare(b.updated)); + const first = sortedUpdates[0]; + + if (first?.id) { + const tab = tabForAnnotation(first); + this._store.selectTab(tab); + this._store.setAnnotationFocusRequest(first.id); + } } const deletions = Object.keys(this._store.pendingDeletions()).map(id => ({ From c7107f624cd0d6a44fc2c169c426b0bf416d0d88 Mon Sep 17 00:00:00 2001 From: Santiago Regusci Date: Mon, 5 Jan 2026 09:48:37 -0300 Subject: [PATCH 2/3] update test --- src/sidebar/services/test/streamer-test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/sidebar/services/test/streamer-test.js b/src/sidebar/services/test/streamer-test.js index ea0db360614..08a54c4a0fb 100644 --- a/src/sidebar/services/test/streamer-test.js +++ b/src/sidebar/services/test/streamer-test.js @@ -130,6 +130,8 @@ describe('StreamerService', () => { receiveRealTimeUpdates: sinon.stub(), removeAnnotations: sinon.stub(), highlightAnnotations: sinon.stub(), + selectTab: sinon.stub(), + setAnnotationFocusRequest: sinon.stub(), }, ); From 0b7b8b3e993fc25ab198629b05afec56414b996e Mon Sep 17 00:00:00 2001 From: Santiago Regusci Date: Mon, 5 Jan 2026 09:59:19 -0300 Subject: [PATCH 3/3] update coverage --- src/sidebar/services/test/streamer-test.js | 83 ++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/src/sidebar/services/test/streamer-test.js b/src/sidebar/services/test/streamer-test.js index 08a54c4a0fb..a7a35f82cd4 100644 --- a/src/sidebar/services/test/streamer-test.js +++ b/src/sidebar/services/test/streamer-test.js @@ -432,6 +432,27 @@ describe('StreamerService', () => { await timeoutPromise; assert.calledWith(fakeStore.highlightAnnotations, []); }); + + it('handles "past" notifications', () => { + const pastNotification = { + type: 'annotation-notification', + options: { action: 'past' }, + payload: [{ id: 'past-id' }], + }; + fakeWebSocket.notify(pastNotification); + assert.calledWith(fakeStore.receiveRealTimeUpdates, { + updatedAnnotations: pastNotification.payload, + }); + }); + + it('ignores unknown actions', () => { + fakeWebSocket.notify({ + type: 'annotation-notification', + options: { action: 'unknown' }, + payload: [], + }); + assert.notCalled(fakeStore.receiveRealTimeUpdates); + }); }); context('when the app is the sidebar', () => { @@ -494,6 +515,44 @@ describe('StreamerService', () => { activeStreamer.applyPendingUpdates(); assert.calledWith(fakeStore.clearPendingUpdates); }); + + it('focuses the oldest updated annotation and selects the correct tab', () => { + const updates = { + newest: { + id: 'newest', + updated: '2024-01-02T00:00:00Z', + target: [{ selector: [{ type: 'TextQuoteSelector' }] }], + }, + oldest: { + id: 'oldest', + updated: '2024-01-01T00:00:00Z', + target: [], + }, + }; + fakeStore.pendingUpdates.returns(updates); + + activeStreamer.applyPendingUpdates(); + + assert.calledWith(fakeStore.selectTab, 'note'); + assert.calledWith(fakeStore.setAnnotationFocusRequest, 'oldest'); + }); + + it('does not focus if no updates have IDs', () => { + fakeStore.pendingUpdates.returns({ + 'no-id': { updated: '2024-01-01T00:00:00Z' }, + }); + activeStreamer.applyPendingUpdates(); + assert.notCalled(fakeStore.setAnnotationFocusRequest); + }); + + it('does nothing if there are no pending updates or deletions', () => { + fakeStore.pendingUpdates.returns({}); + fakeStore.pendingDeletions.returns({}); + activeStreamer.applyPendingUpdates(); + assert.notCalled(fakeStore.addAnnotations); + assert.notCalled(fakeStore.removeAnnotations); + assert.called(fakeStore.clearPendingUpdates); + }); }); describe('session change notifications', () => { @@ -577,6 +636,30 @@ describe('StreamerService', () => { }); }); + describe('#setConfig', () => { + it('sends configuration messages immediately if connected', async () => { + createDefaultStreamer(); + await activeStreamer.connect(); + fakeWebSocket.messages = []; + + activeStreamer.setConfig('test', { foo: 'bar' }); + assert.deepEqual(fakeWebSocket.messages, [{ foo: 'bar' }]); + }); + + it('does not send null configuration messages on reconnect', async () => { + createDefaultStreamer(); + activeStreamer.setConfig('test', null); + await activeStreamer.connect(); + + // Reconnect to trigger _sendClientConfig + fakeWebSocket.messages = []; + fakeWebSocket.emit('open'); + + const testMsg = fakeWebSocket.messages.find(msg => msg === null); + assert.isUndefined(testMsg); + }); + }); + describe('reconnections', () => { it('resends configuration messages when a reconnection occurs', () => { createDefaultStreamer();