controller.client.spec.ts 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
  1. /** Controller wire behavior: host-base resolution, availability filtering, and launch errors. */
  2. import { afterEach, describe, expect, it, vi } from 'vitest'
  3. import { OpenInAppController } from '../src/client/controller.ts'
  4. afterEach(() => {
  5. vi.unstubAllGlobals()
  6. })
  7. function jsonResponse(payload: unknown, status = 200): Response {
  8. return new Response(JSON.stringify(payload), { status })
  9. }
  10. describe('OpenInAppController availability', () => {
  11. it('starts without a platform-specific choice', () => {
  12. const controller = new OpenInAppController(async () => jsonResponse({ apps: [] }))
  13. expect(controller.choice.getSnapshot()).toBe('')
  14. })
  15. it('shares one availability read across concurrent loads', async () => {
  16. const fetcher = vi.fn(async () => jsonResponse({ apps: ['finder'] }))
  17. const controller = new OpenInAppController(fetcher)
  18. await Promise.all([controller.load(), controller.load()])
  19. await controller.load()
  20. expect(fetcher).toHaveBeenCalledOnce()
  21. expect(controller.apps.getSnapshot()).toEqual(['finder'])
  22. })
  23. it('publishes an empty list for a non-OK availability answer and for a non-array payload', async () => {
  24. const failing = new OpenInAppController(async () => jsonResponse({}, 500))
  25. await failing.load()
  26. expect(failing.apps.getSnapshot()).toEqual([])
  27. const malformed = new OpenInAppController(async () => jsonResponse({ apps: 'nope' }))
  28. await malformed.load()
  29. expect(malformed.apps.getSnapshot()).toEqual([])
  30. })
  31. it('resolves routes against the page origin when the page has one', async () => {
  32. vi.stubGlobal('location', { origin: 'http://dsh.example:8080' })
  33. const fetcher = vi.fn(async (input: string | URL) => { void input; return jsonResponse({ apps: [] }) })
  34. const controller = new OpenInAppController(fetcher)
  35. await controller.load()
  36. expect(String(fetcher.mock.calls[0]?.[0])).toBe('http://dsh.example:8080/open-in-app/apps')
  37. })
  38. it('falls back to the internal host base under a null origin', async () => {
  39. vi.stubGlobal('location', { origin: 'null' })
  40. const fetcher = vi.fn(async (input: string | URL) => { void input; return jsonResponse({ apps: [] }) })
  41. const controller = new OpenInAppController(fetcher)
  42. await controller.load()
  43. expect(String(fetcher.mock.calls[0]?.[0])).toBe('http://dsh.internal/open-in-app/apps')
  44. })
  45. })
  46. describe('OpenInAppController launching', () => {
  47. it('restores the chosen app from the open-in-app storage key', () => {
  48. const values = new Map<string, string>()
  49. vi.stubGlobal('localStorage', {
  50. getItem: (key: string) => values.get(key) ?? null,
  51. setItem: (key: string, value: string) => { values.set(key, value) },
  52. })
  53. const controller = new OpenInAppController(async () => jsonResponse({ apps: [] }))
  54. controller.choose('cursor')
  55. expect(controller.choice.getSnapshot()).toBe('cursor')
  56. expect(values.get('dsh.open-in-app.choice')).toBe('"cursor"')
  57. const reloaded = new OpenInAppController(async () => jsonResponse({ apps: [] }))
  58. expect(reloaded.choice.getSnapshot()).toBe('cursor')
  59. })
  60. it('posts the launch body and surfaces HTTP failures', async () => {
  61. const fetcher = vi.fn(async (input: string | URL, init?: RequestInit) => { void input; void init; return jsonResponse({ ok: true }) })
  62. const controller = new OpenInAppController(fetcher)
  63. await controller.launch('cursor', '/w/dir')
  64. expect(fetcher.mock.calls[0]?.[1]).toMatchObject({
  65. method: 'POST',
  66. headers: { 'content-type': 'application/json' },
  67. body: JSON.stringify({ app: 'cursor', path: '/w/dir' }),
  68. })
  69. const failing = new OpenInAppController(async () => jsonResponse({}, 404))
  70. await expect(failing.launch('cursor', '/w/dir')).rejects.toThrow('open failed: HTTP 404')
  71. })
  72. })