| ... | ... | @@ -73,6 +73,98 @@ describe("FileSaveCoordinator", () => { |
| 73 | 73 | expect(onPendingChange.mock.calls.at(-1)).toEqual([false]); |
| 74 | 74 | }); |
| 75 | 75 | |
| 76 | it("saves an edit made inside the debounce window when the editor closes", async () => { |
| 77 | vi.useFakeTimers(); |
| 78 | const persist = vi |
| 79 | .fn<(contents: string) => Promise<AtomCommandResult<void, never>>>() |
| 80 | .mockResolvedValue(AsyncResult.success(undefined)); |
| 81 | const coordinator = new FileSaveCoordinator({ |
| 82 | debounceMs: 500, |
| 83 | persist, |
| 84 | onPendingChange: vi.fn(), |
| 85 | onConfirmed: vi.fn(), |
| 86 | }); |
| 87 | |
| 88 | coordinator.change("unsaved"); |
| 89 | coordinator.dispose(); |
| 90 | await vi.runAllTimersAsync(); |
| 91 | |
| 92 | expect(persist).toHaveBeenCalledOnce(); |
| 93 | expect(persist).toHaveBeenCalledWith("unsaved"); |
| 94 | }); |
| 95 | |
| 96 | it("flushes an edit made while a write was in flight when the editor closes", async () => { |
| 97 | vi.useFakeTimers(); |
| 98 | const inFlight = deferred(); |
| 99 | const persist = vi |
| 100 | .fn<(contents: string) => Promise<AtomCommandResult<void, never>>>() |
| 101 | .mockReturnValueOnce(inFlight.promise) |
| 102 | .mockResolvedValue(AsyncResult.success(undefined)); |
| 103 | const coordinator = new FileSaveCoordinator({ |
| 104 | debounceMs: 500, |
| 105 | persist, |
| 106 | onPendingChange: vi.fn(), |
| 107 | onConfirmed: vi.fn(), |
| 108 | }); |
| 109 | |
| 110 | coordinator.change("first"); |
| 111 | await vi.advanceTimersByTimeAsync(500); |
| 112 | coordinator.change("latest"); |
| 113 | coordinator.dispose(); |
| 114 | inFlight.resolve(AsyncResult.success(undefined)); |
| 115 | await vi.runAllTimersAsync(); |
| 116 | |
| 117 | expect(persist).toHaveBeenCalledTimes(2); |
| 118 | expect(persist).toHaveBeenLastCalledWith("latest"); |
| 119 | }); |
| 120 | |
| 121 | it("does not rewrite a write that lands while the editor closes", async () => { |
| 122 | vi.useFakeTimers(); |
| 123 | const inFlight = deferred(); |
| 124 | const persist = vi |
| 125 | .fn<(contents: string) => Promise<AtomCommandResult<void, never>>>() |
| 126 | .mockReturnValueOnce(inFlight.promise) |
| 127 | .mockResolvedValue(AsyncResult.success(undefined)); |
| 128 | const coordinator = new FileSaveCoordinator({ |
| 129 | debounceMs: 500, |
| 130 | persist, |
| 131 | onPendingChange: vi.fn(), |
| 132 | onConfirmed: vi.fn(), |
| 133 | }); |
| 134 | |
| 135 | coordinator.change("only"); |
| 136 | await vi.advanceTimersByTimeAsync(500); |
| 137 | coordinator.dispose(); |
| 138 | inFlight.resolve(AsyncResult.success(undefined)); |
| 139 | await vi.runAllTimersAsync(); |
| 140 | |
| 141 | expect(persist).toHaveBeenCalledOnce(); |
| 142 | }); |
| 143 | |
| 144 | it("retries a failed write when the editor closes", async () => { |
| 145 | vi.useFakeTimers(); |
| 146 | const persist = vi |
| 147 | .fn() |
| 148 | .mockResolvedValueOnce(AsyncResult.failure(Cause.fail(new Error("write failed")))) |
| 149 | .mockResolvedValue(AsyncResult.success(undefined)); |
| 150 | const coordinator = new FileSaveCoordinator({ |
| 151 | debounceMs: 500, |
| 152 | persist, |
| 153 | onPendingChange: vi.fn(), |
| 154 | onConfirmed: vi.fn(), |
| 155 | }); |
| 156 | |
| 157 | coordinator.change("latest"); |
| 158 | await vi.advanceTimersByTimeAsync(500); |
| 159 | expect(persist).toHaveBeenCalledOnce(); |
| 160 | |
| 161 | coordinator.dispose(); |
| 162 | await vi.runAllTimersAsync(); |
| 163 | |
| 164 | expect(persist).toHaveBeenCalledTimes(2); |
| 165 | expect(persist).toHaveBeenLastCalledWith("latest"); |
| 166 | }); |
| 167 | |
| 76 | 168 | it("leaves the file pending when the latest write fails", async () => { |
| 77 | 169 | vi.useFakeTimers(); |
| 78 | 170 | const onPendingChange = vi.fn(); |
| ... | ... | @@ -91,4 +183,49 @@ describe("FileSaveCoordinator", () => { |
| 91 | 183 | expect(onPendingChange).toHaveBeenCalledWith(true); |
| 92 | 184 | expect(onPendingChange).not.toHaveBeenCalledWith(false); |
| 93 | 185 | }); |
| 186 | |
| 187 | it("ignores editor changes emitted after disposal", async () => { |
| 188 | vi.useFakeTimers(); |
| 189 | const persist = vi |
| 190 | .fn<(contents: string) => Promise<AtomCommandResult<void, never>>>() |
| 191 | .mockResolvedValue(AsyncResult.success(undefined)); |
| 192 | const onPendingChange = vi.fn(); |
| 193 | const coordinator = new FileSaveCoordinator({ |
| 194 | debounceMs: 500, |
| 195 | persist, |
| 196 | onPendingChange, |
| 197 | onConfirmed: vi.fn(), |
| 198 | }); |
| 199 | |
| 200 | coordinator.dispose(); |
| 201 | coordinator.change("stale contents"); |
| 202 | await vi.runAllTimersAsync(); |
| 203 | |
| 204 | expect(persist).not.toHaveBeenCalled(); |
| 205 | expect(onPendingChange).not.toHaveBeenCalled(); |
| 206 | }); |
| 207 | |
| 208 | it("does not persist confirmed contents again on disposal", async () => { |
| 209 | vi.useFakeTimers(); |
| 210 | const persist = vi |
| 211 | .fn<(contents: string) => Promise<AtomCommandResult<void, never>>>() |
| 212 | .mockResolvedValue(AsyncResult.success(undefined)); |
| 213 | const coordinator = new FileSaveCoordinator({ |
| 214 | debounceMs: 500, |
| 215 | persist, |
| 216 | onPendingChange: vi.fn(), |
| 217 | onConfirmed: vi.fn(), |
| 218 | }); |
| 219 | |
| 220 | coordinator.change("temporary edit"); |
| 221 | await vi.advanceTimersByTimeAsync(500); |
| 222 | coordinator.change("original contents"); |
| 223 | await vi.advanceTimersByTimeAsync(500); |
| 224 | expect(persist).toHaveBeenCalledTimes(2); |
| 225 | |
| 226 | coordinator.dispose(); |
| 227 | await vi.runAllTimersAsync(); |
| 228 | |
| 229 | expect(persist).toHaveBeenCalledTimes(2); |
| 230 | }); |
| 94 | 231 | }); |