apply.client.spec.ts 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  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 kind 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, onTestFinished, 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 { TEXTPREVIEW_ID, TEXTPREVIEW_KIND } from '../src/client/definition.ts'
  14. import { apply, inject } from '../src/client/index.ts'
  15. import { apply as hostApply } from '../src/index.ts'
  16. import { TextPreview } from '../src/client/TextPreview.tsx'
  17. import { TextTitle } from '../src/client/TextTitle.tsx'
  18. import { TextBody } from '../src/client/text/TextBody.tsx'
  19. import { PLAIN_BODY_ID } from '../src/client/text/index.ts'
  20. import { MarkdownBody } from '../src/client/markdown/MarkdownBody.tsx'
  21. import { MARKDOWN_BODY_ID } from '../src/client/markdown/index.ts'
  22. import { HtmlBody } from '../src/client/html/HtmlBody.tsx'
  23. import { HTML_BODY_ID } from '../src/client/html/index.ts'
  24. import { ImageBody } from '../src/client/image/ImageBody.tsx'
  25. import { IMAGE_BODY_ID } from '../src/client/image/index.ts'
  26. import { PdfBody } from '../src/client/pdf/PdfBody.tsx'
  27. import { PDF_BODY_ID } from '../src/client/pdf/index.ts'
  28. import { CodeBody } from '../src/client/code/CodeBody.tsx'
  29. import { en, zh } from '../src/client/locales.ts'
  30. import type { textFace } from '../src/client/face.ts'
  31. import type { TextStore } from '../src/client/store.ts'
  32. import { FILE, SESSION, TAB_ID, page } from './fixtures.client.ts'
  33. interface Recorded {
  34. name: string
  35. key: string
  36. locale?: string
  37. store?: unknown
  38. inject?: unknown
  39. component: unknown
  40. }
  41. async function boot() {
  42. const ctx = new Context()
  43. const tabs = new SidebarRightTabRegistry(ctx)
  44. const registered: Recorded[] = []
  45. const slots = {
  46. inject: vi.fn((_name: string, register: () => () => void) => register()),
  47. register: vi.fn((options: Omit<Recorded, 'component'>, component: unknown) => {
  48. const entry: Recorded = { ...options, component }
  49. registered.push(entry)
  50. return () => { registered.splice(registered.indexOf(entry), 1) }
  51. }),
  52. }
  53. const dictionaries = new Map<string, unknown>()
  54. const locale = {
  55. bind: () => (key: string) => key,
  56. register: vi.fn((ns: string, dicts: unknown) => {
  57. dictionaries.set(ns, dicts)
  58. return () => { dictionaries.delete(ns) }
  59. }),
  60. }
  61. const workspaceFiles = {
  62. read: vi.fn().mockResolvedValue(page(1, ['first'], true)),
  63. readAll: vi.fn().mockResolvedValue({
  64. ok: true, value: { absolutePath: '/workspace/notes.md', version: 'v1', offset: 0, data: 'AAH/', eof: true },
  65. }),
  66. }
  67. ctx.provide('sidebarRightTabs', tabs as never)
  68. ctx.provide('slots', slots as never)
  69. ctx.provide('locale', locale as never)
  70. ctx.provide('remote', { workspaceFiles } as never)
  71. ctx.provide('remote.workspaceFiles', workspaceFiles as never)
  72. const fiber = ctx.plugin({ inject: [...inject], apply })
  73. onTestFinished(async () => { await fiber.dispose() })
  74. await fiber.await()
  75. return { tabs, registered, dictionaries, fiber, workspaceFiles }
  76. }
  77. describe('ui-sidebar-documentpreview apply', () => {
  78. it('keeps the host Loader entry inert', () => {
  79. expect(hostApply).not.toThrow()
  80. })
  81. it('registers the type, its dictionaries, and the body and title seats under the type\'s id, the body with a store and a face', async () => {
  82. const { tabs, registered, dictionaries } = await boot()
  83. expect(tabs.get(TEXTPREVIEW_KIND)?.priority).toBe('fallback')
  84. expect(tabs.get(TEXTPREVIEW_KIND)?.id).toBe(TEXTPREVIEW_ID)
  85. expect(dictionaries.get('sidebarDocumentPreview')).toEqual({ zh, en })
  86. // The seat key is the implementation's id, not the kind: an extension may
  87. // take the kind over, and the seat must still find this body.
  88. expect(registered.map(entry => [entry.name, entry.key, entry.locale, entry.component])).toEqual([
  89. ['sidebar.right.pane.tab', TEXTPREVIEW_ID, 'sidebarDocumentPreview', TextPreview],
  90. ['sidebar.right.pane.tab.title', TEXTPREVIEW_ID, undefined, TextTitle],
  91. ['sidebar.right.tab.document', PLAIN_BODY_ID, undefined, TextBody],
  92. ['sidebar.right.tab.document', MARKDOWN_BODY_ID, 'documentMarkdown', MarkdownBody],
  93. ['sidebar.right.tab.document', HTML_BODY_ID, 'documentHtml', HtmlBody],
  94. ['sidebar.right.tab.document', IMAGE_BODY_ID, 'sidebarImage', ImageBody],
  95. ['sidebar.right.tab.document', PDF_BODY_ID, 'sidebarPdf', PdfBody],
  96. ['sidebar.right.tab.document', '@deepseek-ai/dsh-client-ui-sidebar-documentpreview/code', 'sidebarCodePreview', CodeBody],
  97. ])
  98. expect(registered[0]?.store).toBeDefined()
  99. expect(typeof registered[0]?.inject).toBe('function')
  100. })
  101. it('takes every registration back when the plugin is disposed', async () => {
  102. const { tabs, registered, dictionaries, fiber } = await boot()
  103. await fiber.dispose()
  104. expect(tabs.get(TEXTPREVIEW_KIND)).toBeUndefined()
  105. expect(registered).toEqual([])
  106. expect(dictionaries.size).toBe(0)
  107. })
  108. it('injects ordinary Remote reads without requiring a Resource service', async () => {
  109. const { registered, workspaceFiles } = await boot()
  110. const registration = registered.find(entry => entry.component === TextPreview)
  111. if (registration === undefined) throw new Error('missing preview registration')
  112. const instance = (registration.store as TextStore).create()
  113. const face = (registration.inject as ReturnType<typeof textFace>)(SESSION, instance.actions)
  114. const controller = new AbortController()
  115. onTestFinished(() => { controller.abort() })
  116. face.loadPage(TAB_ID, FILE, 1, controller.signal, 'v1')
  117. await workspaceFiles.read.mock.results[0]?.value
  118. expect(workspaceFiles.read).toHaveBeenCalledExactlyOnceWith(FILE.sessionId, FILE.path, { offset: 1 }, controller.signal)
  119. expect(instance.getSnapshot().byTab[TAB_ID]?.pages[1]?.text).toBe('first')
  120. face.loadAll(TAB_ID, FILE, controller.signal, 'v1')
  121. await workspaceFiles.readAll.mock.results[0]?.value
  122. expect(workspaceFiles.readAll).toHaveBeenCalledExactlyOnceWith(FILE.sessionId, FILE.path, controller.signal)
  123. expect(instance.getSnapshot().byTab[TAB_ID]?.complete?.data).toEqual(new Uint8Array([0, 1, 255]))
  124. })
  125. })