use-event.test.tsx 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. /** @jsxImportSource @opentui/solid */
  2. import { describe, expect, test } from "bun:test"
  3. import { testRender } from "@opentui/solid"
  4. import type { Event, GlobalEvent } from "@kilocode/sdk/v2"
  5. import { onMount } from "solid-js"
  6. import { ProjectProvider, useProject } from "../../../src/cli/cmd/tui/context/project"
  7. import { SDKProvider } from "../../../src/cli/cmd/tui/context/sdk"
  8. import { useEvent } from "../../../src/cli/cmd/tui/context/event"
  9. async function wait(fn: () => boolean, timeout = 2000) {
  10. const start = Date.now()
  11. while (!fn()) {
  12. if (Date.now() - start > timeout) throw new Error("timed out waiting for condition")
  13. await Bun.sleep(10)
  14. }
  15. }
  16. function event(payload: Event, input: { directory: string; workspace?: string }): GlobalEvent {
  17. return {
  18. directory: input.directory,
  19. workspace: input.workspace,
  20. payload,
  21. }
  22. }
  23. function vcs(branch: string): Event {
  24. return {
  25. type: "vcs.branch.updated",
  26. properties: {
  27. branch,
  28. },
  29. }
  30. }
  31. function update(version: string): Event {
  32. return {
  33. type: "installation.update-available",
  34. properties: {
  35. version,
  36. },
  37. }
  38. }
  39. function createSource() {
  40. let fn: ((event: GlobalEvent) => void) | undefined
  41. return {
  42. source: {
  43. subscribe: async (handler: (event: GlobalEvent) => void) => {
  44. fn = handler
  45. return () => {
  46. if (fn === handler) fn = undefined
  47. }
  48. },
  49. },
  50. emit(evt: GlobalEvent) {
  51. if (!fn) throw new Error("event source not ready")
  52. fn(evt)
  53. },
  54. }
  55. }
  56. async function mount() {
  57. const source = createSource()
  58. const seen: Event[] = []
  59. let project!: ReturnType<typeof useProject>
  60. let done!: () => void
  61. const ready = new Promise<void>((resolve) => {
  62. done = resolve
  63. })
  64. const app = await testRender(() => (
  65. <SDKProvider url="http://test" directory="/tmp/root" events={source.source}>
  66. <ProjectProvider>
  67. <Probe
  68. onReady={(ctx) => {
  69. project = ctx.project
  70. done()
  71. }}
  72. seen={seen}
  73. />
  74. </ProjectProvider>
  75. </SDKProvider>
  76. ))
  77. await ready
  78. return { app, emit: source.emit, project, seen }
  79. }
  80. function Probe(props: { seen: Event[]; onReady: (ctx: { project: ReturnType<typeof useProject> }) => void }) {
  81. const project = useProject()
  82. const event = useEvent()
  83. onMount(() => {
  84. event.subscribe((evt) => {
  85. props.seen.push(evt)
  86. })
  87. props.onReady({ project })
  88. })
  89. return <box />
  90. }
  91. describe("useEvent", () => {
  92. test("delivers matching directory events without an active workspace", async () => {
  93. const { app, emit, seen } = await mount()
  94. try {
  95. emit(event(vcs("main"), { directory: "/tmp/root" }))
  96. await wait(() => seen.length === 1)
  97. expect(seen).toEqual([vcs("main")])
  98. } finally {
  99. app.renderer.destroy()
  100. }
  101. })
  102. test("ignores non-matching directory events without an active workspace", async () => {
  103. const { app, emit, seen } = await mount()
  104. try {
  105. emit(event(vcs("other"), { directory: "/tmp/other" }))
  106. await Bun.sleep(30)
  107. expect(seen).toHaveLength(0)
  108. } finally {
  109. app.renderer.destroy()
  110. }
  111. })
  112. test("delivers matching workspace events when a workspace is active", async () => {
  113. const { app, emit, project, seen } = await mount()
  114. try {
  115. project.workspace.set("ws_a")
  116. emit(event(vcs("ws"), { directory: "/tmp/other", workspace: "ws_a" }))
  117. await wait(() => seen.length === 1)
  118. expect(seen).toEqual([vcs("ws")])
  119. } finally {
  120. app.renderer.destroy()
  121. }
  122. })
  123. test("ignores non-matching workspace events when a workspace is active", async () => {
  124. const { app, emit, project, seen } = await mount()
  125. try {
  126. project.workspace.set("ws_a")
  127. emit(event(vcs("ws"), { directory: "/tmp/root", workspace: "ws_b" }))
  128. await Bun.sleep(30)
  129. expect(seen).toHaveLength(0)
  130. } finally {
  131. app.renderer.destroy()
  132. }
  133. })
  134. test("delivers truly global events even when a workspace is active", async () => {
  135. const { app, emit, project, seen } = await mount()
  136. try {
  137. project.workspace.set("ws_a")
  138. emit(event(update("1.2.3"), { directory: "global" }))
  139. await wait(() => seen.length === 1)
  140. expect(seen).toEqual([update("1.2.3")])
  141. } finally {
  142. app.renderer.destroy()
  143. }
  144. })
  145. })