apply.spec.ts 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. /** Ownerless-copy registrations: the four seats, the dictionaries, locale refresh, 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-settings-general/client'
  7. import type { GeneralSectionInjected } from '@deepseek-ai/dsh-client-ui-settings-general/client'
  8. import { CloseLabel, HeaderContent, TriggerContent } from '../src/client/chrome.tsx'
  9. import { GeneralSection } from '../src/client/GeneralSection.tsx'
  10. /** The four seats this plugin fills (slot name → expected component). */
  11. const SEATS = [
  12. ['settings.trigger', TriggerContent],
  13. ['settings.header', HeaderContent],
  14. ['settings.close', CloseLabel],
  15. ['settings.section', GeneralSection],
  16. ] as const
  17. async function bench() {
  18. const ctx = new Context()
  19. await ctx.plugin(SlotsService).await()
  20. const locale = new LocaleService(ctx)
  21. ctx.provide('locale', locale)
  22. return { ctx, slots: ctx.get('slots') as SlotsService, locale }
  23. }
  24. /** Declare the shell's four child slots the way ui-settings' entry does. */
  25. function declare(slots: SlotsService): () => void {
  26. return slots.register(
  27. {
  28. name: 'root',
  29. children: {
  30. 'settings.trigger': { kind: 'single', scope: 'root' },
  31. 'settings.header': { kind: 'single', scope: 'root' },
  32. 'settings.close': { kind: 'single', scope: 'root' },
  33. 'settings.section': { kind: 'list', scope: 'root' },
  34. },
  35. } as never,
  36. () => null,
  37. )
  38. }
  39. function generalEntry(slots: SlotsService) {
  40. return slots.entries('settings.section').find(e => e.component === GeneralSection)
  41. }
  42. describe('ui-settings-general apply', () => {
  43. it('declares the services it uses', () => {
  44. expect(inject).toEqual(['slots', 'locale'])
  45. })
  46. it('fills all four seats for declarations before or after apply', async () => {
  47. const before = await bench()
  48. declare(before.slots)
  49. await before.ctx.plugin({ inject: [...inject], apply }).await()
  50. for (const [name, component] of SEATS) {
  51. expect(before.slots.entries(name)[0]!.component).toBe(component)
  52. }
  53. const entry = generalEntry(before.slots)!
  54. expect(entry.options).toEqual({ id: 'general', order: 0, label: '通用设置' })
  55. expect(before.slots.spec('settings.general.item')).toEqual({ kind: 'list', scope: 'root' })
  56. const injected = (entry.inject as unknown as () => GeneralSectionInjected)()
  57. expect(injected.t('permission.title')).toBe('权限')
  58. // The chrome seats share one inject face: the settings-ns translate.
  59. const chrome = (before.slots.entries('settings.trigger')[0]!.inject as unknown as () => GeneralSectionInjected)()
  60. expect(chrome.t('trigger')).toBe('设置')
  61. const after = await bench()
  62. await after.ctx.plugin({ inject: [...inject], apply }).await()
  63. for (const [name] of SEATS) expect(after.slots.entries(name)).toHaveLength(0)
  64. declare(after.slots)
  65. await Promise.resolve()
  66. for (const [name, component] of SEATS) {
  67. expect(after.slots.entries(name)[0]!.component).toBe(component)
  68. // The self-inflicted ledger notifications hit the duplicate guard.
  69. expect(after.slots.entries(name)).toHaveLength(1)
  70. }
  71. })
  72. it('registers the zh/en settings dictionaries and frees the seats on teardown', async () => {
  73. const b = await bench()
  74. declare(b.slots)
  75. const fiber = b.ctx.plugin({ inject: [...inject], apply })
  76. await fiber.await()
  77. expect(b.locale.bind('settings')('title')).toBe('设置')
  78. b.locale.setLocale('en')
  79. expect(b.locale.bind('settings')('close')).toBe('Close')
  80. b.locale.setLocale('zh')
  81. await fiber.dispose()
  82. // The (ns, locale) seats are free again — the dictionary disposers ran.
  83. expect(() => b.locale.register('settings', 'zh', {})).not.toThrow()
  84. expect(() => b.locale.register('settings', 'en', {})).not.toThrow()
  85. })
  86. it('refreshes all four seats on locale change with fresh General label text', async () => {
  87. const b = await bench()
  88. declare(b.slots)
  89. await b.ctx.plugin({ inject: [...inject], apply }).await()
  90. const zhVersions = SEATS.map(([name]) => b.slots.getVersion(name))
  91. b.locale.setLocale('en')
  92. // Every seat re-registered (version moved) and the label re-resolved.
  93. SEATS.forEach(([name], i) => {
  94. expect(b.slots.getVersion(name)).toBeGreaterThan(zhVersions[i]!)
  95. expect(b.slots.entries(name)).toHaveLength(1)
  96. })
  97. expect(generalEntry(b.slots)!.options.label).toBe('General')
  98. b.locale.setLocale('zh')
  99. expect(generalEntry(b.slots)!.options.label).toBe('通用设置')
  100. })
  101. it('locale change while the slots are undeclared stays a no-op', async () => {
  102. const b = await bench()
  103. await b.ctx.plugin({ inject: [...inject], apply }).await()
  104. b.locale.setLocale('en')
  105. for (const [name] of SEATS) expect(b.slots.entries(name)).toHaveLength(0)
  106. b.locale.setLocale('zh')
  107. })
  108. it('re-registers after an HMR collapse of the declaring chain (stale disposers must not block)', async () => {
  109. const b = await bench()
  110. const redeclare = declare(b.slots)
  111. await b.ctx.plugin({ inject: [...inject], apply }).await()
  112. // Declarer unload: the cascade removes every seat entry and the item
  113. // declaration while our local disposers go stale.
  114. redeclare()
  115. for (const [name] of SEATS) expect(b.slots.entries(name)).toHaveLength(0)
  116. expect(b.slots.spec('settings.general.item')).toBeUndefined()
  117. declare(b.slots)
  118. await Promise.resolve()
  119. for (const [name, component] of SEATS) {
  120. expect(b.slots.entries(name)[0]!.component).toBe(component)
  121. }
  122. expect(b.slots.spec('settings.general.item')).toEqual({ kind: 'list', scope: 'root' })
  123. // The recovered registrations still ride the locale path.
  124. b.locale.setLocale('en')
  125. expect(generalEntry(b.slots)!.options.label).toBe('General')
  126. b.locale.setLocale('zh')
  127. })
  128. it('removes every seat and the item declaration on teardown', async () => {
  129. const b = await bench()
  130. declare(b.slots)
  131. const fiber = b.ctx.plugin({ inject: [...inject], apply })
  132. await fiber.await()
  133. expect(b.slots.spec('settings.general.item')).toBeDefined()
  134. await fiber.dispose()
  135. for (const [name] of SEATS) expect(b.slots.entries(name)).toHaveLength(0)
  136. expect(b.slots.spec('settings.general.item')).toBeUndefined()
  137. })
  138. })