boot.client.spec.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. // @vitest-environment jsdom
  2. import type { Context } from '@deepseek-ai/cordis'
  3. import * as modulesClient from '@deepseek-ai/dsh-client-modules/client'
  4. import type {
  5. ClientBundleRegistration, ClientModuleCreateOptions, ClientModuleLoaderTarget, DshWindow,
  6. WebBootEntry,
  7. } from '@deepseek-ai/dsh-client-modules/client'
  8. import { afterEach, describe, expect, it, vi } from 'vitest'
  9. import { AppWebEntry } from '../src/boot.ts'
  10. const MODULES_ID = '@deepseek-ai/dsh-client-modules'
  11. const win = globalThis as DshWindow
  12. const moduleFace = modulesClient as unknown as Record<string, unknown>
  13. afterEach(() => {
  14. vi.restoreAllMocks()
  15. delete win.__DSH_BOOT__
  16. delete win.__ModuleLoader__
  17. document.body.innerHTML = ''
  18. })
  19. /** Install the stable facade shape that the Host injects before AppWebEntry runs. */
  20. function installFacade(
  21. create?: (options: ClientModuleCreateOptions) => modulesClient.ClientModuleSystem,
  22. ): ClientModuleLoaderTarget {
  23. const pendingQueue: ClientBundleRegistration[] = []
  24. const target: ClientModuleLoaderTarget = {
  25. mode: 'queue',
  26. pendingQueue,
  27. load: (registration) => { pendingQueue.push(registration) },
  28. create: create ?? (options => modulesClient.createClientModuleSystem(target, {
  29. id: MODULES_ID,
  30. exports: moduleFace,
  31. }, options)),
  32. }
  33. win.__ModuleLoader__ = target
  34. return target
  35. }
  36. async function expectBootFailure(setup: () => void, message: string): Promise<void> {
  37. const error = vi.spyOn(console, 'error').mockImplementation(() => {})
  38. const container = document.createElement('div')
  39. document.body.append(container)
  40. setup()
  41. const entry = new AppWebEntry(container)
  42. await entry.run()
  43. expect(container.textContent).toContain(message)
  44. expect(error).toHaveBeenCalledOnce()
  45. await entry.dispose()
  46. }
  47. describe('bootstrap failure rendering', () => {
  48. it('renders a missing bootstrap facade', async () => {
  49. await expectBootFailure(
  50. () => { delete win.__ModuleLoader__ },
  51. 'window.__ModuleLoader__ bootstrap facade is missing',
  52. )
  53. })
  54. it('renders a create failure owned by the facade', async () => {
  55. await expectBootFailure(() => {
  56. installFacade(() => { throw new Error('facade create failed') })
  57. }, 'facade create failed')
  58. })
  59. it('renders a malformed boot manifest', async () => {
  60. await expectBootFailure(() => {
  61. installFacade()
  62. delete win.__DSH_BOOT__
  63. }, 'window.__DSH_BOOT__ is missing or not an object')
  64. })
  65. it('renders a module-system construction failure', async () => {
  66. await expectBootFailure(() => {
  67. installFacade()
  68. const duplicate = { id: 'duplicate', url: '/duplicate/client.js', rev: '1' }
  69. win.__DSH_BOOT__ = { rev: 'graph', entries: [duplicate, duplicate] }
  70. }, 'duplicate graph entry "duplicate"')
  71. })
  72. })
  73. describe('plugin activation', () => {
  74. it('allows a modules-dependent row to be created before the modules row', async () => {
  75. const events: string[] = []
  76. const container = document.createElement('div')
  77. document.body.append(container)
  78. const target = installFacade()
  79. const entries: WebBootEntry[] = [
  80. { id: 'consumer', url: '/consumer.js', rev: '1' },
  81. { id: MODULES_ID, url: '/modules.js', rev: '1' },
  82. { id: 'renderer', url: '/renderer.js', rev: '1' },
  83. ]
  84. win.__DSH_BOOT__ = { rev: 'graph', entries }
  85. const registrations = new Map<string, ClientBundleRegistration>([
  86. ['/consumer.js', {
  87. id: 'consumer',
  88. factory: () => ({
  89. inject: ['modules'],
  90. apply: (ctx: Context) => {
  91. expect(ctx.modules).toBeDefined()
  92. events.push('consumer')
  93. },
  94. }),
  95. }],
  96. ['/renderer.js', {
  97. id: 'renderer',
  98. factory: () => ({
  99. apply: (ctx: Context) => {
  100. ctx.reflect.provide('uiRenderer', {
  101. mount: (element: HTMLElement) => {
  102. events.push('mount')
  103. element.textContent = 'mounted'
  104. return () => {}
  105. },
  106. })
  107. },
  108. }),
  109. }],
  110. ])
  111. const entry = new AppWebEntry(container, {
  112. loadBundle: async (url) => {
  113. const registration = registrations.get(url)
  114. if (registration === undefined) throw new Error(`missing fixture registration ${url}`)
  115. target.load(registration)
  116. },
  117. })
  118. await entry.run()
  119. expect(target.mode).toBe('live')
  120. expect(events).toEqual(['consumer', 'mount'])
  121. expect(container.textContent).toBe('mounted')
  122. await entry.dispose()
  123. })
  124. })