app-shell.spec.tsx 2.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061
  1. // @vitest-environment jsdom
  2. /**
  3. * App-shell assembly plugin on the real machinery: bare Context + production
  4. * SlotsService + the test-runtime session/workspace doubles. Deliberately NOT
  5. * mounted through SlotTestRuntime — its create() installs the capturing
  6. * renderer and install() is boot-once; app-shell IS the production installer,
  7. * so this bench hands it the uninstalled service exactly as boot does.
  8. */
  9. import { afterEach, describe, expect, it, vi } from 'vitest'
  10. import { act, cleanup, render } from '@testing-library/react'
  11. import { Context } from 'cordis'
  12. import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client'
  13. import { TestSessions, TestWorkspaces } from '@deepseek-ai/dsh-client-test-runtime'
  14. import type { Stabilizer } from '@deepseek-ai/dsh-client-test-runtime'
  15. import * as AppShell from '@deepseek-ai/dsh-client-web/src/app-shell.ts'
  16. afterEach(cleanup)
  17. const stabilize: Stabilizer = async (fn) => { await act(async () => { await fn() }) }
  18. async function bench() {
  19. const ctx = new Context()
  20. await ctx.plugin(SlotsService).await()
  21. const slots = ctx.get('slots') as SlotsService
  22. ctx.provide('sessions', new TestSessions(stabilize, ctx))
  23. ctx.provide('workspaces', new TestWorkspaces(stabilize))
  24. ctx.provide('layout', { openDetails: vi.fn(), closeDetails: vi.fn() })
  25. const fiber = ctx.plugin({ inject: [...AppShell.inject], apply: AppShell.apply })
  26. await fiber.await()
  27. return { ctx, slots, fiber }
  28. }
  29. describe('app-shell assembly plugin', () => {
  30. it('installs the renderer and provides the assembled appShell face', async () => {
  31. const { ctx, slots } = await bench()
  32. slots.register({ name: 'root' }, () => <div data-testid="root-probe" />)
  33. const shell = ctx.get('appShell')
  34. expect(shell).toBeDefined()
  35. const view = render(<>{shell!.renderApp()}</>)
  36. expect(view.getByTestId('root-probe')).toBeTruthy()
  37. })
  38. it('assembles once: repeated renderApp calls reuse the built closure', async () => {
  39. const { ctx, slots } = await bench()
  40. slots.register({ name: 'root' }, () => <div data-testid="root-probe" />)
  41. const shell = ctx.get('appShell')!
  42. const first = render(<>{shell.renderApp()}</>)
  43. expect(first.getByTestId('root-probe')).toBeTruthy()
  44. first.unmount()
  45. // Second call rides the cached closure (renderApp ??=) and still renders.
  46. const second = render(<>{shell.renderApp()}</>)
  47. expect(second.getByTestId('root-probe')).toBeTruthy()
  48. })
  49. it('fiber dispose retracts the service and uninstalls the renderer', async () => {
  50. const { ctx, slots, fiber } = await bench()
  51. await stabilize(() => fiber.dispose())
  52. expect(ctx.get('appShell')).toBeUndefined()
  53. expect(() => slots.renderSlot('root', {})).toThrow('not installed')
  54. })
  55. })