client-apply.spec.ts 2.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
  1. /**
  2. * Connection plugin browser-half apply: ctx.connection handle mounting, mode
  3. * selection off the page URL, and the single-consumer stream-loop ownership.
  4. */
  5. import { Context } from 'cordis'
  6. import { afterEach, describe, expect, it } from 'vitest'
  7. import { apply, type ConnectionHandle } from '../src/client/index.ts'
  8. import { FixtureApiClient } from '../src/client/fixture.ts'
  9. import { WebApiClient } from '../src/client/web-api-client.ts'
  10. type Win = { location?: { search: string } }
  11. afterEach(() => {
  12. delete (globalThis as Win).location
  13. })
  14. async function mount(): Promise<ConnectionHandle> {
  15. const ctx = new Context()
  16. await ctx.plugin({ apply, inject: [] })
  17. const handle = ctx.get('connection') as ConnectionHandle | undefined
  18. if (handle === undefined) throw new Error('ctx.connection not provided')
  19. return handle
  20. }
  21. describe('connection client apply', () => {
  22. it('mounts ctx.connection with the real client when no ?fixture switch is present', async () => {
  23. ;(globalThis as Win).location = { search: '' }
  24. const handle = await mount()
  25. expect(handle.api).toBeInstanceOf(WebApiClient)
  26. })
  27. it('selects the fixture client under ?fixture (and with no location at all stays real)', async () => {
  28. ;(globalThis as Win).location = { search: '?fixture' }
  29. expect((await mount()).api).toBeInstanceOf(FixtureApiClient)
  30. delete (globalThis as Win).location
  31. expect((await mount()).api).toBeInstanceOf(WebApiClient)
  32. })
  33. it('start() hands out one loop, rejects a second consumer, and stop() aborts the streams', async () => {
  34. ;(globalThis as Win).location = { search: '?fixture' }
  35. const handle = await mount()
  36. // config omitted: the `config ?? {}` default arm is part of the surface.
  37. const loop = handle.start({})
  38. expect(() => handle.start({})).toThrow(/already owned by another consumer/)
  39. loop.stop() // teardown must not throw; the fixture streams abort quietly
  40. })
  41. it('WebApiClient carries requests over globalThis.fetch', async () => {
  42. ;(globalThis as Win).location = { search: '' }
  43. const handle = await mount()
  44. const original = globalThis.fetch
  45. const seen: string[] = []
  46. globalThis.fetch = (input: URL | RequestInfo) => {
  47. seen.push(typeof input === 'string' ? input : input instanceof URL ? input.href : input.url)
  48. return Promise.resolve(new Response('{}', { status: 200 }))
  49. }
  50. try {
  51. // Schema rejection is fine — the transport hop is the assertion.
  52. await (handle.api as WebApiClient).host.describe({}).catch(() => undefined)
  53. } finally {
  54. globalThis.fetch = original
  55. }
  56. expect(seen.some(u => u.includes('/api/'))).toBe(true)
  57. })
  58. })