apply.client.spec.ts 3.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
  1. /**
  2. * The plugin's registrations, and their removal when the plugin goes.
  3. *
  4. * The registry is real, because "registered" means what it says a type is; the
  5. * slot, locale, and Remote faces are recorders, because what matters here is
  6. * what was handed to them — one body seat under the type's id with its store
  7. * and face — and that every registration is gone after dispose, which is what
  8. * makes a reload safe.
  9. */
  10. import { describe, expect, it, vi } from 'vitest'
  11. import { Context } from '@deepseek-ai/cordis'
  12. import { SidebarRightTabRegistry } from '@deepseek-ai/dsh-client-ui-sidebar-right/src/client/tab-registry.ts'
  13. import { FILES_ID, FILES_KIND } from '../src/client/definition.tsx'
  14. import { apply, inject } from '../src/client/index.ts'
  15. import { apply as hostApply } from '../src/index.ts'
  16. import { FilesBody } from '../src/client/FilesBody.tsx'
  17. import { FilesTitle } from '../src/client/FilesTitle.tsx'
  18. import { en, zh } from '../src/client/locales.ts'
  19. interface Recorded {
  20. name: string
  21. key: string
  22. locale: string
  23. store: unknown
  24. inject: unknown
  25. component: unknown
  26. }
  27. async function boot() {
  28. const ctx = new Context()
  29. const tabs = new SidebarRightTabRegistry(ctx)
  30. const registered: Recorded[] = []
  31. const slots = {
  32. inject: vi.fn((_name: string, register: () => () => void) => register()),
  33. register: vi.fn((options: Omit<Recorded, 'component'>, component: unknown) => {
  34. const entry: Recorded = { ...options, component }
  35. registered.push(entry)
  36. return () => { registered.splice(registered.indexOf(entry), 1) }
  37. }),
  38. }
  39. const dictionaries = new Map<string, unknown>()
  40. const locale = {
  41. // Copy is the dictionary's contract; the key stands in for the translation.
  42. bind: vi.fn(() => (key: string) => key),
  43. register: vi.fn((ns: string, dicts: unknown) => {
  44. dictionaries.set(ns, dicts)
  45. return () => { dictionaries.delete(ns) }
  46. }),
  47. }
  48. const workspaceFiles = { list: vi.fn() }
  49. ctx.provide('sidebarRightTabs', tabs as never)
  50. ctx.provide('slots', slots as never)
  51. ctx.provide('locale', locale as never)
  52. ctx.provide('remote', { workspaceFiles } as never)
  53. ctx.provide('remote.workspaceFiles', workspaceFiles as never)
  54. const fiber = ctx.plugin({ inject: [...inject], apply })
  55. await fiber.await()
  56. return { tabs, registered, dictionaries, fiber }
  57. }
  58. describe('ui-sidebar-files apply', () => {
  59. it('keeps the host Loader entry inert', () => {
  60. expect(hostApply).not.toThrow()
  61. })
  62. it('registers the type, its dictionaries, and the body and title seats under the type\'s id', async () => {
  63. const { tabs, registered, dictionaries } = await boot()
  64. const definition = tabs.get(FILES_KIND)
  65. expect(definition?.id).toBe(FILES_ID)
  66. expect(definition?.priority).toBe('builtin')
  67. expect(definition?.title('sidebar://files')).toBe('type.label')
  68. expect(definition?.guide?.map(entry => [entry.order, entry.title(), entry.description?.()]))
  69. .toEqual([[10, 'guide.title', 'guide.description']])
  70. expect(dictionaries.get('sidebarFiles')).toEqual({ zh, en })
  71. // The seat key is the implementation's id, not the kind: an extension may
  72. // take the kind over, and the seat must still find this body.
  73. expect(registered.map(entry => [entry.name, entry.key, entry.locale, entry.component])).toEqual([
  74. ['sidebar.right.pane.tab', FILES_ID, 'sidebarFiles', FilesBody],
  75. ['sidebar.right.pane.tab.title', FILES_ID, undefined, FilesTitle],
  76. ])
  77. expect(registered[0]?.store).toBeDefined()
  78. expect(typeof registered[0]?.inject).toBe('function')
  79. })
  80. it('takes every registration back when the plugin is disposed', async () => {
  81. const { tabs, registered, dictionaries, fiber } = await boot()
  82. await fiber.dispose()
  83. expect(tabs.get(FILES_KIND)).toBeUndefined()
  84. expect(registered).toEqual([])
  85. expect(dictionaries.size).toBe(0)
  86. })
  87. })