recording.test.ts 3.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
  1. import { expect, test } from "bun:test"
  2. import { mkdtemp, rm } from "node:fs/promises"
  3. import { tmpdir } from "node:os"
  4. import { join } from "node:path"
  5. import { TextRenderable } from "@opentui/core"
  6. import { createHarness, matches } from "../src/frontend/actions"
  7. import { SimulationRenderer } from "../src/frontend/renderer"
  8. import { Timeline, type Event } from "../src/recording"
  9. test("streams ANSI chunks into a versioned JSONL timeline", async () => {
  10. const directory = await mkdtemp(join(tmpdir(), "simulation-recording-"))
  11. const path = join(directory, "nested", "timeline.jsonl")
  12. try {
  13. const timeline = await Timeline.create(path, 80, 24)
  14. await new Promise<void>((resolve, reject) => {
  15. timeline.write(Buffer.from("\u001b[2Jhello"), (error) => (error ? reject(error) : resolve()))
  16. })
  17. timeline.resize(100, 30)
  18. expect(await timeline.finish()).toBe(path)
  19. await new Promise<void>((resolve) => timeline.write(Buffer.from("ignored"), () => resolve()))
  20. const events = (await Bun.file(path).text())
  21. .trim()
  22. .split("\n")
  23. .map((line) => JSON.parse(line) as Event)
  24. expect(events[0]).toEqual({ type: "header", version: 1, cols: 80, rows: 24, encoding: "base64" })
  25. expect(events[1]?.type).toBe("output")
  26. if (events[1]?.type !== "output") throw new Error("Missing output event")
  27. expect(Buffer.from(events[1].data, "base64").toString()).toBe("\u001b[2Jhello")
  28. expect(events[1].at_ms).toBeGreaterThanOrEqual(0)
  29. expect(events[2]).toMatchObject({ type: "resize", cols: 100, rows: 30 })
  30. expect(events.at(-1)).toMatchObject({ type: "output", data: "" })
  31. } finally {
  32. await rm(directory, { recursive: true, force: true })
  33. }
  34. })
  35. test("captures native renderer output and finishes on destroy", async () => {
  36. const directory = await mkdtemp(join(tmpdir(), "simulation-renderer-recording-"))
  37. const path = join(directory, "timeline.jsonl")
  38. const renderer = await SimulationRenderer.create({}, path)
  39. try {
  40. await SimulationRenderer.setupFor(renderer)?.renderOnce()
  41. renderer.destroy()
  42. expect(await SimulationRenderer.finish(renderer)).toBe(path)
  43. const events = (await Bun.file(path).text())
  44. .trim()
  45. .split("\n")
  46. .map((line) => JSON.parse(line) as Event)
  47. expect(events.some((event) => event.type === "output")).toBe(true)
  48. } finally {
  49. if (!renderer.isDestroyed) renderer.destroy()
  50. await SimulationRenderer.finish(renderer)
  51. await rm(directory, { recursive: true, force: true })
  52. }
  53. })
  54. test("matches live screen text while recording", async () => {
  55. const directory = await mkdtemp(join(tmpdir(), "simulation-recording-matches-"))
  56. const path = join(directory, "timeline.jsonl")
  57. const renderer = await SimulationRenderer.create({}, path)
  58. try {
  59. renderer.root.add(new TextRenderable(renderer, { content: "recorded screen text" }))
  60. await SimulationRenderer.setupFor(renderer)?.renderOnce()
  61. expect(matches(createHarness(renderer), "recorded screen text")).toBe(true)
  62. } finally {
  63. renderer.destroy()
  64. await SimulationRenderer.finish(renderer)
  65. await rm(directory, { recursive: true, force: true })
  66. }
  67. })