browser-controller.client.spec.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. // @vitest-environment jsdom
  2. import { afterEach, describe, expect, it, vi } from 'vitest'
  3. import type { TabId } from '@deepseek-ai/dsh-client-ui-dockkit'
  4. import { BrowserController, createBrowserControllers } from '../src/client/browser/BrowserController.ts'
  5. import { createBrowserStore } from '../src/client/browser/store.ts'
  6. const TAB = 'tab' as TabId
  7. const APP = 'https://dsh.example'
  8. const lifetimes = new Set<AbortController>()
  9. function lifetime(): AbortController {
  10. const controller = new AbortController()
  11. lifetimes.add(controller)
  12. return controller
  13. }
  14. afterEach(() => {
  15. for (const controller of lifetimes) controller.abort()
  16. lifetimes.clear()
  17. localStorage.clear()
  18. vi.restoreAllMocks()
  19. })
  20. describe('BrowserController', () => {
  21. it('ignores navigation commands after its tab occurrence ends', () => {
  22. const store = createBrowserStore().create('browser-controller-ended-test')
  23. const tabLifetime = lifetime()
  24. const controller = new BrowserController({
  25. tabId: TAB, signal: tabLifetime.signal, applicationOrigin: APP, actions: store.actions,
  26. })
  27. tabLifetime.abort()
  28. controller.loadUrl('https://ignored.example/')
  29. controller.goBack()
  30. controller.goForward()
  31. controller.reload()
  32. controller.frame.reportLoaded(1)
  33. expect(store.getSnapshot().byTab[TAB]).toBeUndefined()
  34. })
  35. it('owns the four navigation commands and frame load state', () => {
  36. const store = createBrowserStore().create('browser-controller-test')
  37. const face = createBrowserControllers(store.actions)
  38. const tabLifetime = lifetime()
  39. face.mount(TAB, tabLifetime.signal, APP)
  40. const frame = face.keyedHooks.browserFrame(TAB)!
  41. face.mount(TAB, tabLifetime.signal, APP)
  42. expect(face.keyedHooks.browserFrame(TAB)).toBe(frame)
  43. face.goBack(TAB)
  44. face.goForward(TAB)
  45. face.reload(TAB)
  46. face.loadUrl(TAB, 'https://example.test/one')
  47. const first = frame.getSnapshot().document!
  48. expect(first.src).toBe('https://example.test/one')
  49. face.reportLoaded(TAB, first.revision - 1)
  50. face.reportLoaded(TAB, first.revision)
  51. expect(store.getSnapshot().byTab[TAB]?.navigation.status).toBe('known')
  52. face.reportLoaded(TAB, first.revision)
  53. expect(store.getSnapshot().byTab[TAB]?.navigation.status).toBe('unknown')
  54. face.goBack(TAB)
  55. face.goForward(TAB)
  56. expect(store.getSnapshot().byTab[TAB]?.navigation.status).toBe('unknown')
  57. face.loadUrl(TAB, 'https://example.test/two')
  58. face.goBack(TAB)
  59. expect(frame.getSnapshot().document?.target.url).toBe('https://example.test/one')
  60. face.goForward(TAB)
  61. expect(frame.getSnapshot().document?.target.url).toBe('https://example.test/two')
  62. const beforeReload = store.getSnapshot().byTab[TAB]!.request!.revision
  63. face.reload(TAB)
  64. expect(store.getSnapshot().byTab[TAB]?.request?.revision).toBe(beforeReload + 1)
  65. face.loadUrl(TAB, 'https://example.test/two')
  66. expect(store.getSnapshot().byTab[TAB]?.request?.revision).toBe(beforeReload + 2)
  67. tabLifetime.abort()
  68. expect(store.getSnapshot().byTab[TAB]).toBeUndefined()
  69. face.reportLoaded(TAB, first.revision)
  70. face.loadUrl(TAB, 'https://ignored.example/')
  71. face.goBack(TAB)
  72. face.goForward(TAB)
  73. face.reload(TAB)
  74. })
  75. it('does not let an old occurrence delete a replacement controller', () => {
  76. const store = createBrowserStore().create('browser-controller-replacement-test')
  77. const face = createBrowserControllers(store.actions)
  78. const first = lifetime()
  79. const second = lifetime()
  80. face.mount(TAB, first.signal, APP)
  81. const original = face.keyedHooks.browserFrame(TAB)
  82. face.mount(TAB, second.signal, APP)
  83. const replacement = face.keyedHooks.browserFrame(TAB)
  84. expect(replacement).not.toBe(original)
  85. first.abort()
  86. expect(store.getSnapshot().byTab[TAB]).toBeUndefined()
  87. face.loadUrl(TAB, 'https://example.test/')
  88. expect(store.getSnapshot().byTab[TAB]).toBeDefined()
  89. face.mount(TAB, second.signal, APP)
  90. expect(face.keyedHooks.browserFrame(TAB)).toBe(replacement)
  91. })
  92. it('loads loopback under sandbox and reloads it across sandbox changes', () => {
  93. const store = createBrowserStore().create('browser-controller-loopback-test')
  94. const face = createBrowserControllers(store.actions)
  95. const tabLifetime = lifetime()
  96. face.mount(TAB, tabLifetime.signal, APP)
  97. const frame = face.keyedHooks.browserFrame(TAB)!
  98. face.loadUrl(TAB, 'http://localhost:5173/app')
  99. expect(frame.getSnapshot()).toMatchObject({
  100. sandboxed: true,
  101. document: { src: 'http://localhost:5173/app' },
  102. })
  103. const beforeToggle = store.getSnapshot().byTab[TAB]!.request!.revision
  104. face.toggleSandbox(TAB)
  105. expect(frame.getSnapshot().sandboxed).toBe(false)
  106. expect(frame.getSnapshot().document?.src).toBe('http://localhost:5173/app')
  107. expect(store.getSnapshot().byTab[TAB]?.request?.revision).toBe(beforeToggle + 1)
  108. face.toggleSandbox(TAB)
  109. expect(frame.getSnapshot()).toMatchObject({
  110. sandboxed: true,
  111. document: { src: 'http://localhost:5173/app' },
  112. })
  113. expect(store.getSnapshot().byTab[TAB]?.request?.revision).toBe(beforeToggle + 2)
  114. })
  115. it('loads public HTTP without changing the sandbox policy', () => {
  116. const store = createBrowserStore().create('browser-controller-http-test')
  117. const face = createBrowserControllers(store.actions)
  118. const tabLifetime = lifetime()
  119. face.mount(TAB, tabLifetime.signal, APP)
  120. face.loadUrl(TAB, 'http://example.test/path')
  121. expect(face.keyedHooks.browserFrame(TAB)?.getSnapshot()).toMatchObject({
  122. sandboxed: true,
  123. document: { src: 'http://example.test/path' },
  124. })
  125. })
  126. })