writing-wake-lock.spec.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. import { afterEach, describe, expect, it, vi } from "vitest"
  2. import {
  3. resetWritingWakeLockForTests,
  4. withWritingWakeLock,
  5. type WritingWakeLockBindings,
  6. } from "./writing-wake-lock"
  7. function bindings(invoke: WritingWakeLockBindings["invoke"], tauri = true) {
  8. return {
  9. isTauri: () => tauri,
  10. invoke,
  11. warn: vi.fn(),
  12. } satisfies WritingWakeLockBindings
  13. }
  14. describe("withWritingWakeLock", () => {
  15. afterEach(() => {
  16. resetWritingWakeLockForTests()
  17. })
  18. it("acquires before the operation and releases after it completes", async () => {
  19. const events: string[] = []
  20. const invoke = vi.fn(async <T>(command: string) => {
  21. events.push(command)
  22. return (command === "acquire_writing_wake_lock" ? "token-1" : undefined) as T
  23. })
  24. const result = await withWritingWakeLock(true, async () => {
  25. events.push("operation")
  26. return "正文"
  27. }, bindings(invoke))
  28. expect(result).toBe("正文")
  29. expect(events).toEqual([
  30. "acquire_writing_wake_lock",
  31. "operation",
  32. "release_writing_wake_lock",
  33. ])
  34. expect(invoke).toHaveBeenLastCalledWith("release_writing_wake_lock", { token: "token-1" })
  35. })
  36. it("releases after an aborted or failed operation and preserves the original error", async () => {
  37. const abortError = new DOMException("cancelled", "AbortError")
  38. const invoke = vi.fn(async <T>(command: string) => {
  39. if (command === "release_writing_wake_lock") throw new Error("release failed")
  40. return "token-abort" as T
  41. })
  42. const testBindings = bindings(invoke)
  43. await expect(withWritingWakeLock(true, async () => {
  44. throw abortError
  45. }, testBindings)).rejects.toBe(abortError)
  46. expect(invoke).toHaveBeenLastCalledWith("release_writing_wake_lock", { token: "token-abort" })
  47. expect(testBindings.warn).toHaveBeenCalledTimes(1)
  48. })
  49. it("continues generation when acquisition fails", async () => {
  50. const invoke = vi.fn(async <T>() => {
  51. throw new Error("unsupported")
  52. })
  53. const testBindings = bindings(invoke)
  54. await expect(withWritingWakeLock(true, async () => "正文", testBindings)).resolves.toBe("正文")
  55. expect(invoke).toHaveBeenCalledTimes(1)
  56. expect(testBindings.warn).toHaveBeenCalledTimes(1)
  57. })
  58. it("does not let a release failure mask the operation result", async () => {
  59. const invoke = vi.fn(async <T>(command: string) => {
  60. if (command === "release_writing_wake_lock") throw new Error("release failed")
  61. return "token-release" as T
  62. })
  63. const testBindings = bindings(invoke)
  64. await expect(withWritingWakeLock(true, async () => "正文", testBindings)).resolves.toBe("正文")
  65. expect(testBindings.warn).toHaveBeenCalledTimes(1)
  66. })
  67. it("is a no-op outside Tauri or when disabled", async () => {
  68. const invoke = vi.fn()
  69. await expect(withWritingWakeLock(true, async () => "browser", bindings(invoke, false))).resolves.toBe("browser")
  70. await expect(withWritingWakeLock(false, async () => "disabled", bindings(invoke))).resolves.toBe("disabled")
  71. expect(invoke).not.toHaveBeenCalled()
  72. })
  73. it("nests holds so only the first acquire and last release talk to Tauri", async () => {
  74. const invoke = vi.fn(async <T>(command: string) => {
  75. return (command === "acquire_writing_wake_lock" ? "shared-token" : undefined) as T
  76. })
  77. const testBindings = bindings(invoke)
  78. await withWritingWakeLock(true, async () => {
  79. await withWritingWakeLock(true, async () => {
  80. expect(invoke).toHaveBeenCalledTimes(1)
  81. expect(invoke).toHaveBeenCalledWith("acquire_writing_wake_lock")
  82. }, testBindings)
  83. expect(invoke).toHaveBeenCalledTimes(1)
  84. }, testBindings)
  85. expect(invoke).toHaveBeenCalledTimes(2)
  86. expect(invoke).toHaveBeenLastCalledWith("release_writing_wake_lock", { token: "shared-token" })
  87. })
  88. it("serializes concurrent acquires so only one IPC token is created", async () => {
  89. let releaseFirst!: () => void
  90. const firstAcquire = new Promise<void>((resolve) => {
  91. releaseFirst = resolve
  92. })
  93. let acquireCalls = 0
  94. const invoke = vi.fn(async <T>(command: string) => {
  95. if (command === "acquire_writing_wake_lock") {
  96. acquireCalls += 1
  97. if (acquireCalls === 1) await firstAcquire
  98. return "concurrent-token" as T
  99. }
  100. return undefined as T
  101. })
  102. const testBindings = bindings(invoke)
  103. const first = withWritingWakeLock(true, async () => "a", testBindings)
  104. const second = withWritingWakeLock(true, async () => "b", testBindings)
  105. await Promise.resolve()
  106. expect(invoke).toHaveBeenCalledTimes(1)
  107. releaseFirst()
  108. await expect(Promise.all([first, second])).resolves.toEqual(["a", "b"])
  109. expect(invoke.mock.calls.filter(([command]) => command === "acquire_writing_wake_lock")).toHaveLength(1)
  110. expect(invoke.mock.calls.filter(([command]) => command === "release_writing_wake_lock")).toHaveLength(1)
  111. })
  112. })