@sanity/sdk-react 3.4.0-rc.0 → 3.5.0-next.20260922182803

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/_exports/dashboard.d.ts +151 -45
  2. package/dist/_exports/dashboard.d.ts.map +1 -1
  3. package/dist/_exports/dashboard.js +267 -125
  4. package/dist/_exports/dashboard.js.map +1 -1
  5. package/dist/index.d.ts +21 -141
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +66 -147
  8. package/dist/index.js.map +1 -1
  9. package/dist/{useStudioWorkspacesByProjectIdDataset-B5A5kBH9.js → useStudioWorkspacesByProjectIdDataset-DL6_9jgU.js} +61 -4
  10. package/dist/useStudioWorkspacesByProjectIdDataset-DL6_9jgU.js.map +1 -0
  11. package/package.json +9 -9
  12. package/src/_exports/dashboard.test-d.ts +3 -0
  13. package/src/_exports/dashboard.ts +13 -2
  14. package/src/_exports/sdk-react.ts +0 -3
  15. package/src/components/auth/AuthBoundary.test.tsx +2 -81
  16. package/src/components/auth/AuthBoundary.tsx +3 -17
  17. package/src/components/auth/LoginCallback.test.tsx +7 -46
  18. package/src/components/auth/LoginCallback.tsx +4 -22
  19. package/src/hooks/dashboard/useAgentResourceContext.test.tsx +67 -2
  20. package/src/hooks/dashboard/useAgentResourceContext.ts +35 -6
  21. package/src/hooks/dashboard/useApplicationContext.test.tsx +95 -0
  22. package/src/hooks/dashboard/useApplicationContext.ts +47 -0
  23. package/src/hooks/dashboard/useApplications.test.tsx +165 -7
  24. package/src/hooks/dashboard/useApplications.ts +112 -28
  25. package/src/hooks/dashboard/useCapabilities.test.tsx +84 -0
  26. package/src/hooks/dashboard/useCapabilities.ts +27 -0
  27. package/src/hooks/dashboard/useNavigate.test.ts +392 -5
  28. package/src/hooks/dashboard/useNavigate.ts +169 -26
  29. package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +116 -9
  30. package/src/hooks/dashboard/useNavigateToStudioDocument.ts +110 -39
  31. package/src/hooks/dashboard/useRecordDocumentHistoryEvent.test.ts +99 -1
  32. package/src/hooks/dashboard/useRecordDocumentHistoryEvent.ts +73 -2
  33. package/src/hooks/dashboard/useStudioWorkspacesByProjectIdDataset.test.tsx +23 -1
  34. package/src/hooks/dashboard/useStudioWorkspacesByProjectIdDataset.ts +2 -1
  35. package/dist/useStudioWorkspacesByProjectIdDataset-B5A5kBH9.js.map +0 -1
  36. package/src/hooks/auth/useHandleOAuthCallback.test.tsx +0 -16
  37. package/src/hooks/auth/useHandleOAuthCallback.tsx +0 -49
  38. package/src/hooks/auth/useOAuthAuthorize.test.tsx +0 -16
  39. package/src/hooks/auth/useOAuthAuthorize.tsx +0 -28
  40. package/src/hooks/auth/useOAuthTokens.test.tsx +0 -240
  41. package/src/hooks/auth/useOAuthTokens.tsx +0 -95
@@ -1,7 +1,9 @@
1
+ import {installMessageBus, resetMessageBus} from '@sanity/sdk/_internal'
2
+ import {type MessageBusHost, type PayloadOf} from '@sanity/sdk/dashboard'
1
3
  import {renderHook} from '@testing-library/react'
2
- import {beforeEach, describe, expect, it, vi} from 'vitest'
4
+ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
3
5
 
4
- import {AppProviders} from '../../../test/test-utils'
6
+ import {AppProviders, renderHook as renderHookWithInstance} from '../../../test/test-utils'
5
7
  import {useWindowConnection} from '../comlink/useWindowConnection'
6
8
  import {useAgentResourceContext} from './useAgentResourceContext'
7
9
 
@@ -243,3 +245,66 @@ describe('useAgentResourceContext', () => {
243
245
  })
244
246
  })
245
247
  })
248
+
249
+ describe('useAgentResourceContext (message bus)', () => {
250
+ const MESSAGE_BUS_KEY = Symbol.for('sanity.os.bus')
251
+ let host: MessageBusHost
252
+
253
+ const collect = (): PayloadOf<'applications.context.update'>[] => {
254
+ const payloads: PayloadOf<'applications.context.update'>[] = []
255
+ host.subscribe('applications.context.update', (message) => payloads.push(message.payload))
256
+ return payloads
257
+ }
258
+
259
+ beforeEach(() => {
260
+ vi.stubGlobal('__SANITY_APP_ID__', 'app')
261
+ host = installMessageBus({appId: 'dashboard'})
262
+ })
263
+
264
+ afterEach(() => {
265
+ resetMessageBus()
266
+ delete (globalThis as {[MESSAGE_BUS_KEY]?: unknown})[MESSAGE_BUS_KEY]
267
+ vi.unstubAllGlobals()
268
+ })
269
+
270
+ it('maps the options to an application context payload', () => {
271
+ const payloads = collect()
272
+
273
+ renderHookWithInstance(() =>
274
+ useAgentResourceContext({projectId: 'proj', dataset: 'ds', documentId: 'doc'}),
275
+ )
276
+
277
+ expect(payloads).toEqual([{resource: {id: 'proj.ds', type: 'dataset'}, document: {id: 'doc'}}])
278
+ expect(useWindowConnection).not.toHaveBeenCalled()
279
+ })
280
+
281
+ it('does not re-emit when re-rendered with equal options', () => {
282
+ const payloads = collect()
283
+
284
+ const {rerender} = renderHookWithInstance((options) => useAgentResourceContext(options), {
285
+ initialProps: {projectId: 'proj', dataset: 'ds', documentId: 'doc'},
286
+ })
287
+ rerender({projectId: 'proj', dataset: 'ds', documentId: 'doc'})
288
+
289
+ expect(payloads).toHaveLength(1)
290
+ })
291
+
292
+ it('sets document to null when no documentId is given', () => {
293
+ const payloads = collect()
294
+
295
+ renderHookWithInstance(() => useAgentResourceContext({projectId: 'proj', dataset: 'ds'}))
296
+
297
+ expect(payloads).toEqual([{resource: {id: 'proj.ds', type: 'dataset'}, document: null}])
298
+ })
299
+
300
+ it.each([
301
+ {projectId: '', dataset: 'ds'},
302
+ {projectId: 'proj', dataset: ''},
303
+ ])('publishes null when a required field is missing (%o)', ({projectId, dataset}) => {
304
+ const payloads = collect()
305
+
306
+ renderHookWithInstance(() => useAgentResourceContext({projectId, dataset, documentId: 'doc'}))
307
+
308
+ expect(payloads).toEqual([null])
309
+ })
310
+ })
@@ -1,8 +1,12 @@
1
+ /* eslint-disable react-compiler/react-compiler -- the transport branch in `useAgentResourceContext` is a deliberate rules-of-hooks exception; the compiler refuses files that disable it */
1
2
  import {type Events, SDK_CHANNEL_NAME, SDK_NODE_NAME} from '@sanity/message-protocol'
3
+ import {isDashboardEnvironment} from '@sanity/sdk/_internal'
2
4
  import {type FrameMessage} from '@sanity/sdk/comlink'
3
- import {useCallback, useEffect, useRef} from 'react'
5
+ import {type ApplicationContext} from '@sanity/sdk/dashboard'
6
+ import {useCallback, useEffect, useMemo, useRef} from 'react'
4
7
 
5
8
  import {useWindowConnection} from '../comlink/useWindowConnection'
9
+ import {useApplicationContext} from './useApplicationContext'
6
10
 
7
11
  /**
8
12
  * @public
@@ -24,12 +28,14 @@ export interface AgentResourceContextOptions {
24
28
 
25
29
  /**
26
30
  * @public
27
- * Hook for emitting agent resource context updates to the Dashboard.
28
- * This allows the Agent to understand what resource the user is currently
29
- * interacting with (e.g., which document they're editing).
31
+ * Hook for reporting the resource the user is currently interacting with (for example, which
32
+ * document they're editing) so the Agent can understand their context. The hook reports on mount
33
+ * and again whenever the context changes.
30
34
  *
31
- * The hook will automatically emit the context when it changes, and also
32
- * emit the initial context when the hook is first mounted.
35
+ * The two Dashboard runtimes differ in transport, but the signature is the same:
36
+ * - In an iframe (Comlink), it emits the `dashboard/v1/events/agent/resource/update` event.
37
+ * - In a federated app (message bus), it publishes {@link useApplicationContext}'s
38
+ * `applications.context.update` topic. Reach for `useApplicationContext` directly in new code.
33
39
  *
34
40
  * @category Agent
35
41
  * @param options - The resource context options containing projectId, dataset, and optional documentId
@@ -53,6 +59,29 @@ export interface AgentResourceContextOptions {
53
59
  * ```
54
60
  */
55
61
  export function useAgentResourceContext(options: AgentResourceContextOptions): void {
62
+ // The branch is stable: the transport is fixed for the page lifetime, so one set of hooks
63
+ // always runs and the other never does.
64
+ // eslint-disable-next-line react-hooks/rules-of-hooks -- transport is fixed for the page lifetime
65
+ if (isDashboardEnvironment()) return useBusAgentResourceContext(options)
66
+ // eslint-disable-next-line react-hooks/rules-of-hooks -- transport is fixed for the page lifetime
67
+ return useComlinkAgentResourceContext(options)
68
+ }
69
+
70
+ function useBusAgentResourceContext({projectId, dataset, documentId}: AgentResourceContextOptions) {
71
+ const context = useMemo<ApplicationContext | null>(
72
+ () =>
73
+ projectId && dataset
74
+ ? {
75
+ resource: {id: `${projectId}.${dataset}`, type: 'dataset'},
76
+ document: documentId ? {id: documentId} : null,
77
+ }
78
+ : null,
79
+ [projectId, dataset, documentId],
80
+ )
81
+ useApplicationContext(context)
82
+ }
83
+
84
+ function useComlinkAgentResourceContext(options: AgentResourceContextOptions): void {
56
85
  const {projectId, dataset, documentId} = options
57
86
  const {sendMessage} = useWindowConnection<Events.AgentResourceUpdateMessage, FrameMessage>({
58
87
  name: SDK_NODE_NAME,
@@ -0,0 +1,95 @@
1
+ import {installMessageBus, resetMessageBus} from '@sanity/sdk/_internal'
2
+ import {
3
+ type ApplicationContext,
4
+ type MessageBusHost,
5
+ type MessageBusMeta,
6
+ type PayloadOf,
7
+ } from '@sanity/sdk/dashboard'
8
+ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
9
+
10
+ import {renderHook} from '../../../test/test-utils'
11
+ import {useApplicationContext} from './useApplicationContext'
12
+
13
+ type ContextUpdate = {
14
+ payload: PayloadOf<'applications.context.update'>
15
+ meta: MessageBusMeta
16
+ }
17
+
18
+ const MESSAGE_BUS_KEY = Symbol.for('sanity.os.bus')
19
+
20
+ let host: MessageBusHost
21
+
22
+ const context: ApplicationContext = {
23
+ resource: {id: 'proj.ds', type: 'dataset'},
24
+ document: {id: 'doc'},
25
+ }
26
+
27
+ describe('useApplicationContext', () => {
28
+ beforeEach(() => {
29
+ // The SDK resolves its own app ID from the CLI-embedded global.
30
+ vi.stubGlobal('__SANITY_APP_ID__', 'app')
31
+ host = installMessageBus({appId: 'dashboard'})
32
+ })
33
+
34
+ afterEach(() => {
35
+ resetMessageBus()
36
+ delete (globalThis as {[MESSAGE_BUS_KEY]?: unknown})[MESSAGE_BUS_KEY]
37
+ vi.unstubAllGlobals()
38
+ })
39
+
40
+ const collect = (): ContextUpdate[] => {
41
+ const messages: ContextUpdate[] = []
42
+ host.subscribe('applications.context.update', (message) =>
43
+ messages.push({payload: message.payload, meta: message.meta}),
44
+ )
45
+ return messages
46
+ }
47
+
48
+ it('emits the context on mount', () => {
49
+ const messages = collect()
50
+
51
+ renderHook(() => useApplicationContext(context))
52
+
53
+ expect(messages.map((message) => message.payload)).toEqual([context])
54
+ })
55
+
56
+ it('does not re-emit when re-rendered with the same context', () => {
57
+ const messages = collect()
58
+
59
+ const {rerender} = renderHook(({value}) => useApplicationContext(value), {
60
+ initialProps: {value: context},
61
+ })
62
+ rerender({value: context})
63
+
64
+ expect(messages).toHaveLength(1)
65
+ })
66
+
67
+ it('clears the old context before publishing a new one', () => {
68
+ const messages = collect()
69
+
70
+ const {rerender} = renderHook(({value}) => useApplicationContext(value), {
71
+ initialProps: {value: context},
72
+ })
73
+ const next: ApplicationContext = {resource: {id: 'proj.ds', type: 'dataset'}, document: null}
74
+ rerender({value: next})
75
+
76
+ expect(messages.map((message) => message.payload)).toEqual([context, null, next])
77
+ })
78
+
79
+ it('emits null on unmount', () => {
80
+ const messages = collect()
81
+
82
+ const {unmount} = renderHook(() => useApplicationContext(context))
83
+ unmount()
84
+
85
+ expect(messages.map((message) => message.payload)).toEqual([context, null])
86
+ })
87
+
88
+ it('stamps the sending application id on the message', () => {
89
+ const messages = collect()
90
+
91
+ renderHook(() => useApplicationContext(context))
92
+
93
+ expect(messages[0].meta.appId).toBe('app')
94
+ })
95
+ })
@@ -0,0 +1,47 @@
1
+ import {type ApplicationContext} from '@sanity/sdk/dashboard'
2
+ import {useEffect} from 'react'
3
+
4
+ import {useEmit} from './useEmit'
5
+
6
+ /**
7
+ * Publishes what this application is currently showing to the Dashboard message bus, so the host
8
+ * can hold the foreground application's context. Pass `null` when nothing is open.
9
+ *
10
+ * The context is published on mount and whenever it changes, and cleared with `null` on unmount.
11
+ * A change publishes `null` before the new context, as the component has stopped showing the old
12
+ * one. Pass a memoised value: the hook compares by reference, so a new object on every render
13
+ * republishes on every render.
14
+ *
15
+ * The host reads the sending application from `message.meta.appId`, so the payload carries no
16
+ * application ID.
17
+ *
18
+ * @param context - The application context to publish, or `null` to clear it
19
+ *
20
+ * @example
21
+ * ```tsx
22
+ * import {useApplicationContext} from '@sanity/sdk-react/dashboard'
23
+ * import {useMemo} from 'react'
24
+ *
25
+ * function DocumentView({documentId}: {documentId: string}) {
26
+ * const context = useMemo(
27
+ * () => ({resource: {id: 'my-project.production', type: 'dataset' as const}, document: {id: documentId}}),
28
+ * [documentId],
29
+ * )
30
+ * useApplicationContext(context)
31
+ * return <div>Editing {documentId}</div>
32
+ * }
33
+ * ```
34
+ *
35
+ * @public
36
+ * @category Dashboard
37
+ */
38
+ export function useApplicationContext(context: ApplicationContext | null): void {
39
+ const emit = useEmit('applications.context.update')
40
+
41
+ useEffect(() => {
42
+ emit(context)
43
+ return () => {
44
+ emit(null)
45
+ }
46
+ }, [emit, context])
47
+ }
@@ -1,5 +1,16 @@
1
+ import {
2
+ type Application,
3
+ type ApplicationInclude,
4
+ type Installation,
5
+ type InstallationInclude,
6
+ } from '@sanity/sdk'
1
7
  import {installMessageBus, resetMessageBus} from '@sanity/sdk/_internal'
2
- import {type MessageBusHost, TopicError, type ValueOf} from '@sanity/sdk/dashboard'
8
+ import {
9
+ type LocalApplication,
10
+ type MessageBusHost,
11
+ TopicError,
12
+ type ValueOf,
13
+ } from '@sanity/sdk/dashboard'
3
14
  import {Suspense} from 'react'
4
15
  import {ErrorBoundary} from 'react-error-boundary'
5
16
  import {afterEach, beforeEach, describe, expect, expectTypeOf, it, vi} from 'vitest'
@@ -62,7 +73,7 @@ const application = {
62
73
  title: 'Summary',
63
74
  version: '1',
64
75
  moduleId: 'views/summary',
65
- metadata: null,
76
+ metadata: {size: 'small'},
66
77
  },
67
78
  {
68
79
  id: 'asset-source-1',
@@ -84,7 +95,7 @@ const application = {
84
95
  },
85
96
  ],
86
97
  },
87
- }
98
+ } satisfies Application<ApplicationInclude>
88
99
 
89
100
  const nonFederatedApplication = {
90
101
  ...application,
@@ -95,7 +106,7 @@ const nonFederatedApplication = {
95
106
  title: 'Legacy',
96
107
  isSingleton: false,
97
108
  config: {},
98
- }
109
+ } satisfies Application<ApplicationInclude>
99
110
 
100
111
  const nonSingletonApplication = {
101
112
  ...application,
@@ -105,7 +116,7 @@ const nonSingletonApplication = {
105
116
  slug: 'canvas',
106
117
  title: 'Canvas',
107
118
  isSingleton: false,
108
- }
119
+ } satisfies Application<ApplicationInclude>
109
120
 
110
121
  const externalApplication = {
111
122
  ...application,
@@ -113,7 +124,57 @@ const externalApplication = {
113
124
  name: 'external',
114
125
  slug: null,
115
126
  externalUrl: 'https://apps.example.com/external/index.html',
116
- }
127
+ } satisfies Application<ApplicationInclude>
128
+
129
+ // The workbench synthesises a dev-server app into the deployed shape and marks it with `local`.
130
+ const localApplication = {
131
+ ...application,
132
+ id: 'application-5',
133
+ name: 'dev',
134
+ slug: null,
135
+ isSingleton: false,
136
+ externalUrl: 'http://localhost:3333',
137
+ organizationId: 'local',
138
+ local: {host: 'localhost', port: 3333},
139
+ } satisfies LocalApplication
140
+
141
+ const installation = {
142
+ id: 'installation-1',
143
+ applicationId: 'app-remote-1',
144
+ organizationId: 'organization-1',
145
+ installedBy: 'user-1',
146
+ createdAt: '2026-01-01T00:00:00.000Z',
147
+ updatedAt: '2026-01-02T00:00:00.000Z',
148
+ application: {
149
+ title: 'Remote App',
150
+ name: 'remote',
151
+ reference: 'sanity/remote',
152
+ slug: 'remote',
153
+ icon: null,
154
+ // Distinct from the installing org: the bundle is hosted under the publisher's org.
155
+ organizationId: 'organization-publisher',
156
+ },
157
+ interfaces: [
158
+ {
159
+ id: 'remote-view-1',
160
+ type: 'app',
161
+ name: 'remote',
162
+ title: 'Remote App',
163
+ version: '1',
164
+ moduleId: 'App',
165
+ metadata: null,
166
+ },
167
+ {
168
+ id: 'remote-panel-1',
169
+ type: 'panel',
170
+ name: 'remote-panel',
171
+ title: 'Remote Panel',
172
+ version: '1',
173
+ moduleId: 'views/panel',
174
+ metadata: null,
175
+ },
176
+ ],
177
+ } satisfies Installation<InstallationInclude>
117
178
 
118
179
  const emitApplications = (value: unknown[]) =>
119
180
  host.connections.subscribe((client) =>
@@ -140,8 +201,12 @@ describe('useApplications', () => {
140
201
  const {result} = renderHook(() => useApplications())
141
202
 
142
203
  expectTypeOf(result.current).toEqualTypeOf<DashboardApplication[]>()
204
+ // The application member keeps its raw deployment fields; the union also admits installations.
143
205
  expectTypeOf<
144
- Extract<keyof DashboardApplication, 'activeDeployment' | 'config'>
206
+ Extract<
207
+ keyof Exclude<DashboardApplication, {type: 'installation'}>,
208
+ 'activeDeployment' | 'config'
209
+ >
145
210
  >().toEqualTypeOf<'activeDeployment' | 'config'>()
146
211
  const [federated, nonFederated, nonSingleton] = result.current
147
212
  expect(federated).toMatchObject({
@@ -180,6 +245,99 @@ describe('useApplications', () => {
180
245
  ])
181
246
  expect(nonFederated).toMatchObject({views: [], webWorkers: []})
182
247
  expect(nonSingleton?.views[0]?.module.entry).toBe('https://canvas.sanity.studio')
248
+ expect(result.current.map(({isLocal}) => isLocal)).toEqual([false, false, false])
249
+ })
250
+
251
+ it('marks dev-server applications local and loads their modules from the dev server', () => {
252
+ emitApplications([application, localApplication, installation])
253
+
254
+ const {result} = renderHook(() => useApplications())
255
+
256
+ expect(result.current.map(({id, isLocal}) => [id, isLocal])).toEqual([
257
+ ['application-1', false],
258
+ ['application-5', true],
259
+ ['installation-1', false],
260
+ ])
261
+ const local = result.current[1]
262
+ expect(local).toMatchObject({type: 'coreApp', local: {host: 'localhost', port: 3333}})
263
+ expect(local?.views[0]?.module.entry).toBe('http://localhost:3333')
264
+ expect(local?.views[0]?.application).toMatchObject({id: 'application-5', isLocal: true})
265
+ })
266
+
267
+ it('shapes installations alongside applications in one consistent shape', () => {
268
+ emitApplications([application, installation])
269
+
270
+ const {result} = renderHook(() => useApplications())
271
+
272
+ const shaped = result.current.find(({id}) => id === 'installation-1')
273
+ expect(shaped).toMatchObject({
274
+ id: 'installation-1',
275
+ type: 'installation',
276
+ title: 'Remote App',
277
+ name: 'remote',
278
+ reference: 'sanity/remote',
279
+ slug: 'remote',
280
+ icon: null,
281
+ isSingleton: true,
282
+ visibility: 'default',
283
+ externalUrl: null,
284
+ organizationId: 'organization-publisher',
285
+ createdAt: '2026-01-01T00:00:00.000Z',
286
+ updatedAt: '2026-01-02T00:00:00.000Z',
287
+ installation,
288
+ })
289
+ // Raw record fields must not leak onto the shaped entry, nor the raw record onto its views.
290
+ expect(shaped).not.toHaveProperty('applicationId')
291
+ expect(shaped).not.toHaveProperty('installedBy')
292
+ expect(shaped?.views[0]?.application).not.toHaveProperty('installation')
293
+ expect(shaped?.views).toEqual([
294
+ expect.objectContaining({
295
+ name: 'remote',
296
+ surface: 'window',
297
+ module: {
298
+ entry: 'https://remote-apps-organization-publisher.sanity.run',
299
+ moduleId: 'installation-1/App',
300
+ version: '1',
301
+ },
302
+ }),
303
+ expect.objectContaining({
304
+ name: 'remote-panel',
305
+ surface: 'panel',
306
+ module: expect.objectContaining({moduleId: 'installation-1/views/panel'}),
307
+ }),
308
+ ])
309
+ expect(shaped?.webWorkers).toEqual([])
310
+ })
311
+
312
+ it('exposes no views for an installation without interfaces', () => {
313
+ const {interfaces: _interfaces, ...withoutInterfaces} = installation
314
+ emitApplications([withoutInterfaces])
315
+
316
+ const {result} = renderHook(() => useApplications())
317
+
318
+ expect(result.current[0]).toMatchObject({id: 'installation-1', views: [], webWorkers: []})
319
+ })
320
+
321
+ it('exposes no views for an installation without a slug to derive an origin from', () => {
322
+ emitApplications([{...installation, application: {...installation.application, slug: null}}])
323
+
324
+ const {result} = renderHook(() => useApplications())
325
+
326
+ expect(result.current[0]).toMatchObject({id: 'installation-1', views: [], webWorkers: []})
327
+ })
328
+
329
+ it('loads a dev-server application without a module federation manifest', () => {
330
+ const {config: _config, ...withoutManifest} = localApplication
331
+ emitApplications([withoutManifest])
332
+
333
+ const {result} = renderHook(() => useApplications())
334
+
335
+ expect(result.current[0]?.views.map(({name}) => name)).toEqual([
336
+ 'inbox',
337
+ 'notifications',
338
+ 'summary',
339
+ 'library',
340
+ ])
183
341
  })
184
342
 
185
343
  it('follows topic updates without remapping unchanged lists', () => {