browser-plugin.client.spec.tsx 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  1. // @vitest-environment jsdom
  2. import { Context, Service } from '@deepseek-ai/cordis'
  3. import { afterEach, describe, expect, it, vi } from 'vitest'
  4. import { cleanup } from '@testing-library/react'
  5. import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client'
  6. import { SlotRegistry } from '@deepseek-ai/dsh-client-ui-renderer/client'
  7. import { resolveSlotLabel } from '@deepseek-ai/dsh-client-ui-slots'
  8. import { usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime'
  9. import { apply, inject, NS } from '../src/client/index.ts'
  10. import { PluginInventorySettingsTab } from '../src/client/PluginInventorySettingsTab.tsx'
  11. import type { PluginInventorySettingsTabInjected } from '../src/client/PluginInventorySettingsTab.tsx'
  12. usePinnedBrowserLanguages('zh-CN')
  13. afterEach(cleanup)
  14. const EMPTY = { entries: [] }
  15. type ListResult =
  16. | { readonly ok: true; readonly value: typeof EMPTY }
  17. | { readonly ok: false; readonly error: { readonly code: string; readonly message: string } }
  18. async function bench() {
  19. const ctx = new Context()
  20. await ctx.plugin(SlotRegistry).await()
  21. const locale = new LocaleRuntime(ctx)
  22. ctx.provide('locale', locale)
  23. class RemoteService extends Service {
  24. constructor(serviceCtx: Context) {
  25. super(serviceCtx, 'remote')
  26. }
  27. }
  28. new RemoteService(ctx)
  29. const list = vi.fn<() => Promise<ListResult>>()
  30. .mockResolvedValue({ ok: true, value: EMPTY })
  31. ctx.provide('remote.pluginInventory', { list })
  32. return { ctx, slots: ctx.get('slots') as SlotRegistry, locale, list }
  33. }
  34. function declare(slots: SlotRegistry): () => void {
  35. return slots.register({
  36. name: 'root',
  37. children: { 'settings.plugins.tab': { kind: 'list', scope: 'root' } },
  38. } as never, () => null)
  39. }
  40. describe('ui-settings-plugin-inventory browser plugin', () => {
  41. it('declares only the services used by the Settings Remote contribution', () => {
  42. expect(inject).toEqual(['slots', 'locale', 'remote', 'remote.pluginInventory'])
  43. })
  44. it('registers a localized tab without reading the Remote eagerly', async () => {
  45. const b = await bench()
  46. declare(b.slots)
  47. await b.ctx.plugin({ inject: [...inject], apply }).await()
  48. const entry = b.slots.entries('settings.plugins.tab')[0]!
  49. expect(entry.component).toBe(PluginInventorySettingsTab)
  50. expect(entry.options).toMatchObject({ id: 'all', order: 10 })
  51. expect(entry.locale).toBe(NS)
  52. expect(resolveSlotLabel(entry.options.label)).toBe('插件列表')
  53. expect(b.list).not.toHaveBeenCalled()
  54. const injected = (entry.inject as unknown as () => PluginInventorySettingsTabInjected)()
  55. await expect(injected.list()).resolves.toEqual(EMPTY)
  56. expect(b.list).toHaveBeenCalledOnce()
  57. b.list.mockResolvedValueOnce({ ok: false, error: { code: 'REMOTE_ERROR', message: 'unavailable' } })
  58. await expect(injected.list()).rejects.toThrow('pluginInventory.list failed: REMOTE_ERROR: unavailable')
  59. await b.ctx.fiber.dispose()
  60. })
  61. it('follows locale and recovers across late declaration and declarer reload', async () => {
  62. const b = await bench()
  63. const fiber = b.ctx.plugin({ inject: [...inject], apply })
  64. await fiber.await()
  65. expect(b.slots.entries('settings.plugins.tab')).toHaveLength(0)
  66. const stop = declare(b.slots)
  67. await vi.waitFor(() => { expect(b.slots.entries('settings.plugins.tab')).toHaveLength(1) })
  68. b.locale.setLocale('en')
  69. expect(resolveSlotLabel(b.slots.entries('settings.plugins.tab')[0]!.options.label)).toBe('Plugin list')
  70. stop()
  71. expect(b.slots.entries('settings.plugins.tab')).toHaveLength(0)
  72. declare(b.slots)
  73. await vi.waitFor(() => {
  74. expect(b.slots.entries('settings.plugins.tab')[0]?.component).toBe(PluginInventorySettingsTab)
  75. })
  76. await fiber.dispose()
  77. expect(b.slots.entries('settings.plugins.tab')).toHaveLength(0)
  78. expect(() => b.locale.register(NS, 'zh', {})).not.toThrow()
  79. await b.ctx.fiber.dispose()
  80. })
  81. })