1
0

html-apply.client.spec.ts 3.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
  1. /** HTML metadata and keyed slot contributions share one identity and unwind with their fiber. */
  2. import { afterEach, describe, expect, it, vi } from 'vitest'
  3. import { Context } from '@deepseek-ai/cordis'
  4. import { sessionFileAddress } from '@deepseek-ai/dsh-util-workspace-path'
  5. import { DocumentPreviewRegistry } from '../src/client/document/registry.ts'
  6. import { apply, HTML_BODY_ID, htmlBodyDefinition } from '../src/client/html/index.ts'
  7. import { HtmlBody } from '../src/client/html/HtmlBody.tsx'
  8. import { en, zh } from '../src/client/html/locales.ts'
  9. import type { HtmlBodyProps } from '../src/client/html/HtmlBody.tsx'
  10. type Registration = {
  11. name: string
  12. key: string
  13. locale: string
  14. inject: () => Pick<HtmlBodyProps, 'readRelated'>
  15. }
  16. let dispose: (() => Promise<void>) | undefined
  17. afterEach(async () => { await dispose?.(); dispose = undefined })
  18. describe('HTML registration', () => {
  19. it('claims HTML suffixes as a builtin complete-byte renderer without wrap', () => {
  20. const title = vi.fn(() => 'localized HTML')
  21. expect(htmlBodyDefinition(title)).toEqual({
  22. id: HTML_BODY_ID, extensions: ['html', 'htm'], priority: 'builtin', title, loading: 'bytes-complete', wrap: false,
  23. })
  24. expect(title).not.toHaveBeenCalled()
  25. expect(htmlBodyDefinition(title).title()).toBe('localized HTML')
  26. })
  27. it('registers its dictionary and matching keyed body, and removes all contributions on disposal', async () => {
  28. const ctx = new Context()
  29. // No Session or Tab services are mounted; the global callback must use its file address.
  30. const registry = new DocumentPreviewRegistry()
  31. const dictionaries = new Map<string, unknown>()
  32. const bodies = new Map<string, unknown>()
  33. const readRelated = vi.fn().mockResolvedValue({ ok: true, value: { data: '' } })
  34. ctx.provide('remote', { workspaceFiles: { readRelated } } as never)
  35. const register = vi.fn((options: Registration, body: unknown) => {
  36. bodies.set(options.key, body)
  37. return () => { bodies.delete(options.key) }
  38. })
  39. ctx.provide('documentPreviews', registry)
  40. ctx.provide('slots', { inject: (_key: string, callback: () => () => void) => callback(), register } as never)
  41. ctx.provide('locale', {
  42. bind: () => (key: keyof typeof en) => en[key],
  43. register: (name: string, value: unknown) => { dictionaries.set(name, value); return () => { dictionaries.delete(name) } },
  44. } as never)
  45. const fiber = ctx.plugin({ apply })
  46. dispose = async () => { await fiber.dispose() }
  47. await fiber.await()
  48. expect(registry.candidates('INDEX.HTM').map(entry => entry.id)).toEqual([HTML_BODY_ID])
  49. expect(registry.getSnapshot()[0]?.title()).toBe(en.title)
  50. expect(dictionaries.get('documentHtml')).toEqual({ zh, en })
  51. expect(register).toHaveBeenCalledOnce()
  52. const registration = register.mock.calls[0]?.[0]
  53. expect(registration).toMatchObject({ name: 'sidebar.right.tab.document', key: HTML_BODY_ID, locale: 'documentHtml' })
  54. expect(register.mock.calls[0]?.[1]).toBe(HtmlBody)
  55. const injected = registration?.inject()
  56. expect(typeof injected?.readRelated).toBe('function')
  57. const signal = new AbortController().signal
  58. await injected?.readRelated('dsh-resource://file/session/explicit-session/sub/index.html', '../app.js', signal)
  59. expect(readRelated).toHaveBeenLastCalledWith('explicit-session', 'sub/index.html', '../app.js', signal)
  60. await injected?.readRelated(sessionFileAddress('absolute-session', '/workspace/index.html'), './app.js', signal)
  61. expect(readRelated).toHaveBeenLastCalledWith('absolute-session', '/workspace/index.html', './app.js', signal)
  62. expect(() => injected?.readRelated('dsh-resource://file/absolute/workspace/index.html', './app.js', signal))
  63. .toThrow('not a session file address')
  64. expect(readRelated).toHaveBeenCalledTimes(2)
  65. await dispose()
  66. expect(registry.getSnapshot()).toEqual([])
  67. expect(bodies.size).toBe(0)
  68. expect(dictionaries.size).toBe(0)
  69. })
  70. })