apply.spec.ts 4.0 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. /** Models section registration: declaration-aware deferral, locale re-registration, and HMR recovery. */
  2. import { Context } from 'cordis'
  3. import { describe, expect, it } from 'vitest'
  4. import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client'
  5. import { LocaleService } from '@deepseek-ai/dsh-client-locale/client'
  6. import { apply, inject } from '@deepseek-ai/dsh-client-ui-models/client'
  7. import { ModelsSection } from '../src/client/ModelsSection.tsx'
  8. async function bench() {
  9. const ctx = new Context()
  10. await ctx.plugin(SlotsService).await()
  11. const locale = new LocaleService(ctx)
  12. ctx.provide('locale', locale)
  13. return { ctx, slots: ctx.get('slots') as SlotsService, locale }
  14. }
  15. function declare(slots: SlotsService): () => void {
  16. return slots.register(
  17. { name: 'root', children: { 'settings.section': { kind: 'list', scope: 'root' } } } as never,
  18. () => null,
  19. )
  20. }
  21. describe('ui-models apply', () => {
  22. it('declares the services it uses', () => {
  23. expect(inject).toEqual(['slots', 'locale'])
  24. })
  25. it('registers the models nav entry for declarations before or after apply', async () => {
  26. const before = await bench()
  27. declare(before.slots)
  28. await before.ctx.plugin({ inject: [...inject], apply }).await()
  29. const entry = before.slots.entries('settings.section')[0]!
  30. expect(entry.component).toBe(ModelsSection)
  31. expect(entry.options).toEqual({ id: 'models', order: 10, label: '模型' })
  32. const after = await bench()
  33. await after.ctx.plugin({ inject: [...inject], apply }).await()
  34. expect(after.slots.entries('settings.section')).toHaveLength(0)
  35. declare(after.slots)
  36. await Promise.resolve()
  37. expect(after.slots.entries('settings.section')[0]!.component).toBe(ModelsSection)
  38. // The self-inflicted ledger notifications hit the duplicate guard.
  39. expect(after.slots.entries('settings.section')).toHaveLength(1)
  40. })
  41. it('re-registers with fresh label text on locale change', async () => {
  42. const b = await bench()
  43. declare(b.slots)
  44. await b.ctx.plugin({ inject: [...inject], apply }).await()
  45. b.locale.setLocale('en')
  46. expect(b.slots.entries('settings.section')[0]!.options.label).toBe('Models')
  47. b.locale.setLocale('zh')
  48. expect(b.slots.entries('settings.section')[0]!.options.label).toBe('模型')
  49. })
  50. it('locale change while the slot is undeclared stays a no-op', async () => {
  51. const b = await bench()
  52. await b.ctx.plugin({ inject: [...inject], apply }).await()
  53. b.locale.setLocale('en')
  54. expect(b.slots.entries('settings.section')).toHaveLength(0)
  55. b.locale.setLocale('zh')
  56. })
  57. it('re-registers after an HMR collapse re-declares the slot (stale disposer must not block)', async () => {
  58. const b = await bench()
  59. const redeclare = declare(b.slots)
  60. await b.ctx.plugin({ inject: [...inject], apply }).await()
  61. expect(b.slots.entries('settings.section')).toHaveLength(1)
  62. // Declarer unload: the cascade removes our entry while our local
  63. // disposer variable goes stale.
  64. redeclare()
  65. expect(b.slots.entries('settings.section')).toHaveLength(0)
  66. declare(b.slots)
  67. await Promise.resolve()
  68. expect(b.slots.entries('settings.section')[0]!.component).toBe(ModelsSection)
  69. // The locale path also recovers through the same ledger re-check.
  70. b.locale.setLocale('en')
  71. expect(b.slots.entries('settings.section')[0]!.options.label).toBe('Models')
  72. b.locale.setLocale('zh')
  73. })
  74. it('registers the zh/en nav dictionaries and disposes everything with the fiber', async () => {
  75. const b = await bench()
  76. declare(b.slots)
  77. const fiber = b.ctx.plugin({ inject: [...inject], apply })
  78. await fiber.await()
  79. expect(b.locale.bind('settings.models')('nav')).toBe('模型')
  80. await fiber.dispose()
  81. expect(b.slots.entries('settings.section')).toHaveLength(0)
  82. // The (ns, locale) seats are free again — the dictionary disposers ran.
  83. expect(() => b.locale.register('settings.models', 'zh', {})).not.toThrow()
  84. expect(() => b.locale.register('settings.models', 'en', {})).not.toThrow()
  85. })
  86. })