| ... | @@ -73,6 +73,98 @@ describe("FileSaveCoordinator", () => { | ... | @@ -73,6 +73,98 @@ describe("FileSaveCoordinator", () => { |
| 73 | expect(onPendingChange.mock.calls.at(-1)).toEqual([false]); | 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 | it("leaves the file pending when the latest write fails", async () => { | 168 | it("leaves the file pending when the latest write fails", async () => { |
| 77 | vi.useFakeTimers(); | 169 | vi.useFakeTimers(); |
| 78 | const onPendingChange = vi.fn(); | 170 | const onPendingChange = vi.fn(); |
| ... | @@ -91,4 +183,49 @@ describe("FileSaveCoordinator", () => { | ... | @@ -91,4 +183,49 @@ describe("FileSaveCoordinator", () => { |
| 91 | expect(onPendingChange).toHaveBeenCalledWith(true); | 183 | expect(onPendingChange).toHaveBeenCalledWith(true); |
| 92 | expect(onPendingChange).not.toHaveBeenCalledWith(false); | 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 | }); |