github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ui/src/redux/queryManager/saga.spec.ts (about) 1 // Copyright 2019 The Cockroach Authors. 2 // 3 // Use of this software is governed by the Business Source License 4 // included in the file licenses/BSL.txt. 5 // 6 // As of the Change Date specified in that file, in accordance with 7 // the Business Source License, use of this software will be governed 8 // by the Apache License, Version 2.0, included in the file 9 // licenses/APL.txt. 10 11 import { assert } from "chai"; 12 import moment from "moment"; 13 14 import { channel } from "redux-saga"; 15 import { delay, call } from "redux-saga/effects"; 16 import { expectSaga, testSaga } from "redux-saga-test-plan"; 17 18 import { 19 refresh, 20 autoRefresh, 21 stopAutoRefresh, 22 ManagedQuerySagaState, 23 processQueryManagementAction, 24 queryManagerSaga, 25 timeToNextRefresh, 26 getMoment, 27 DEFAULT_REFRESH_INTERVAL, 28 DEFAULT_RETRY_DELAY, 29 } from "./saga"; 30 31 import { 32 queryManagerReducer, 33 } from "./reducer"; 34 35 describe("Query Management Saga", function() { 36 let queryCounterCalled = 0; 37 const testQueryCounter = { 38 id: "testQueryCounter", 39 refreshInterval: moment.duration(50), 40 retryDelay: moment.duration(500), 41 querySaga: function* () { 42 yield delay(0); 43 yield call(() => queryCounterCalled++); 44 }, 45 }; 46 47 const sentinelError = new Error("error"); 48 let queryErrorCalled = 0; 49 const testQueryError = { 50 id: "testQueryError", 51 refreshInterval: moment.duration(500), 52 retryDelay: moment.duration(50), 53 querySaga: function* (): IterableIterator<void> { 54 queryErrorCalled++; 55 throw sentinelError; 56 }, 57 }; 58 59 beforeEach(function() { 60 queryCounterCalled = 0; 61 queryErrorCalled = 0; 62 }); 63 64 describe("integration tests", function() { 65 describe("REFRESH action", function() { 66 it("immediately runs a saga when refresh is called", function() { 67 return expectSaga(queryManagerSaga) 68 .dispatch(refresh(testQueryCounter)) 69 .silentRun() 70 .then(() => { 71 assert.equal(queryCounterCalled, 1); 72 }); 73 }); 74 it("does not run refresh again if query is currently in progress", function() { 75 return expectSaga(queryManagerSaga) 76 .dispatch(refresh(testQueryCounter)) 77 .dispatch(refresh(testQueryCounter)) 78 .silentRun() 79 .then(() => { 80 assert.equal(queryCounterCalled, 1); 81 }); 82 }); 83 it("does refresh again if query is allowed to finish.", function() { 84 return expectSaga(queryManagerSaga) 85 .dispatch(refresh(testQueryCounter)) 86 .delay(10) 87 .dispatch(refresh(testQueryCounter)) 88 .silentRun() 89 .then(() => { 90 assert.equal(queryCounterCalled, 2); 91 }); 92 }); 93 it("correctly records error (and does not retry).", function() { 94 return expectSaga(queryManagerSaga) 95 .withReducer(queryManagerReducer) 96 .dispatch(refresh(testQueryError)) 97 .silentRun() 98 .then(runResult => { 99 assert.isObject(runResult.storeState[testQueryError.id]); 100 assert.equal(runResult.storeState[testQueryError.id].lastError, sentinelError); 101 assert.isFalse(runResult.storeState[testQueryError.id].isRunning); 102 }); 103 }); 104 it("immediately runs a saga if refresh is called even if AUTO_REFRESH wait is active", function () { 105 return expectSaga(queryManagerSaga) 106 .dispatch(autoRefresh(testQueryCounter)) 107 .delay(10) 108 .dispatch(refresh(testQueryCounter)) 109 .dispatch(stopAutoRefresh(testQueryCounter)) 110 .silentRun() 111 .then(() => { 112 assert.equal(queryCounterCalled, 2); 113 }); 114 }); 115 }); 116 describe("AUTO_REFRESH/STOP_AUTO_REFRESH action", function() { 117 it("immediately runs if query result is out of date", function() { 118 return expectSaga(queryManagerSaga) 119 .dispatch(autoRefresh(testQueryCounter)) 120 .dispatch(stopAutoRefresh(testQueryCounter)) 121 .silentRun() 122 .then(() => { 123 assert.equal(queryCounterCalled, 1); 124 }); 125 }); 126 it("does not run again if query result is considered current.", function() { 127 return expectSaga(queryManagerSaga) 128 .dispatch(refresh(testQueryCounter)) 129 .dispatch(autoRefresh(testQueryCounter)) 130 .dispatch(stopAutoRefresh(testQueryCounter)) 131 .silentRun() 132 .then(() => { 133 assert.equal(queryCounterCalled, 1); 134 }); 135 }); 136 it("runs again after a delay while refresh refcount is positive.", function() { 137 const tester = expectSaga(queryManagerSaga); 138 139 // A query which stops itself by dispatching a stopAutoRefresh 140 // after being called some number of times. 141 let queryCalled = 0; 142 const selfStopQuery = { 143 id: "selfStopQuery", 144 refreshInterval: moment.duration(50), 145 querySaga: function* (): IterableIterator<void> { 146 queryCalled++; 147 if (queryCalled > 3) { 148 tester.dispatch(stopAutoRefresh(selfStopQuery)); 149 } 150 }, 151 }; 152 return tester 153 .dispatch(autoRefresh(selfStopQuery)) 154 .dispatch(autoRefresh(selfStopQuery)) 155 .dispatch(autoRefresh(selfStopQuery)) 156 .silentRun(250) 157 .then(() => { 158 assert.equal(queryCalled, 5); 159 }); 160 }); 161 it("Uses retry delay when errors are encountered", function() { 162 return expectSaga(queryManagerSaga) 163 .dispatch(autoRefresh(testQueryError)) 164 .silentRun(200) 165 .then(() => { 166 // RefreshTimeout is high enough that it would only be 167 // called once. 168 assert.isAtLeast(queryErrorCalled, 3); 169 }); 170 }); 171 it("sets inRunning flag on reducer when query is running.", function() { 172 const neverResolveQuery = { 173 id: "explicitResolveQuery", 174 refreshInterval: moment.duration(0), 175 querySaga: function* (): IterableIterator<Promise<void>> { 176 yield new Promise((_resolve, _reject) => {}); 177 }, 178 }; 179 return expectSaga(queryManagerSaga) 180 .withReducer(queryManagerReducer) 181 .dispatch(refresh(neverResolveQuery)) 182 .dispatch(refresh(testQueryCounter)) 183 .silentRun() 184 .then(runResult => { 185 assert.isTrue(runResult.storeState[neverResolveQuery.id].isRunning); 186 assert.isFalse(runResult.storeState[testQueryCounter.id].isRunning); 187 assert.equal(queryCounterCalled, 1); 188 }); 189 }); 190 it("continues to count AUTO_REFRESH refcounts even while query is running", function() { 191 let queryCalledCount = 0; 192 let resolveQuery: () => void; 193 const explicitResolveQuery = { 194 id: "explicitResolveQuery", 195 refreshInterval: moment.duration(0), 196 querySaga: function* (): IterableIterator<Promise<void>> { 197 queryCalledCount++; 198 yield new Promise((resolve, _reject) => { 199 resolveQuery = resolve; 200 }); 201 }, 202 }; 203 return async function() { 204 const tester = expectSaga(queryManagerSaga) 205 .dispatch(refresh(explicitResolveQuery)); 206 207 const testFinished = tester.silentRun(); 208 await delay(0); 209 210 // Query is now in progress, waiting on explicit resolve to 211 // complete. Dispatch two autoRefresh requests, which should 212 // still be serviced. 213 tester 214 .dispatch(autoRefresh(explicitResolveQuery)) 215 .dispatch(autoRefresh(explicitResolveQuery)); 216 217 // resolve the query, which should result in the query 218 // immediately being called again due to the auto-refresh 219 // count. 220 await delay(0); 221 resolveQuery(); 222 223 // Dispatch stopAutoRefresh and resolve the query. This 224 // should still result in the query being called again, 225 // because autoRefresh has not been fully decremented. 226 tester 227 .dispatch(stopAutoRefresh(explicitResolveQuery)); 228 await delay(0); 229 resolveQuery(); 230 231 // Fully decrement stopAutoRefresh and resolve the query. 232 // Query should not be called again. 233 tester 234 .dispatch(stopAutoRefresh(explicitResolveQuery)); 235 await delay(0); 236 resolveQuery(); 237 await testFinished; 238 239 assert.equal(queryCalledCount, 3); 240 }(); 241 }); 242 }); 243 }); 244 245 describe("component unit tests", function() { 246 describe("processQueryManagementAction", function() { 247 it("initially processes first action", function() { 248 const state = new ManagedQuerySagaState(); 249 state.channel = channel<any>(); 250 testSaga(processQueryManagementAction, state) 251 .next() 252 .take(state.channel); 253 }); 254 it("correctly handles REFRESH action", function() { 255 const state = new ManagedQuerySagaState(); 256 state.channel = channel<any>(); 257 testSaga(processQueryManagementAction, state) 258 .next() 259 .take(state.channel) 260 .next(refresh(testQueryCounter)) 261 .isDone(); 262 const expected = new ManagedQuerySagaState(); 263 expected.channel = state.channel; 264 expected.shouldRefreshQuery = true; 265 assert.deepEqual(state, expected); 266 }); 267 it("correctly handles AUTO_REFRESH action", function() { 268 const state = new ManagedQuerySagaState(); 269 state.channel = channel<any>(); 270 testSaga(processQueryManagementAction, state) 271 .next() 272 .take(state.channel) 273 .next(autoRefresh(testQueryCounter)) 274 .isDone(); 275 const expected = new ManagedQuerySagaState(); 276 expected.channel = state.channel; 277 expected.autoRefreshCount = 1; 278 assert.equal(state.autoRefreshCount, 1); 279 assert.deepEqual(state, expected); 280 }); 281 it("correctly handles STOP_AUTO_REFRESH action", function() { 282 const state = new ManagedQuerySagaState(); 283 state.channel = channel<any>(); 284 testSaga(processQueryManagementAction, state) 285 .next() 286 .take(state.channel) 287 .next(stopAutoRefresh(testQueryCounter)) 288 .isDone(); 289 const expected = new ManagedQuerySagaState(); 290 expected.channel = state.channel; 291 expected.autoRefreshCount = -1; 292 assert.equal(state.autoRefreshCount, -1); 293 assert.deepEqual(state, expected); 294 }); 295 }); 296 297 describe("timeToNextRefresh", function() { 298 it("returns 0 if the query has never run.", function() { 299 const state = new ManagedQuerySagaState(); 300 testSaga(timeToNextRefresh, state) 301 .next() 302 .returns(0); 303 }); 304 it("applies refresh interval if specified.", function() { 305 const state = new ManagedQuerySagaState(); 306 state.query = testQueryCounter; 307 state.queryCompletedAt = moment(5000); 308 testSaga(timeToNextRefresh, state) 309 .next() 310 .call(getMoment) 311 .next(5030) 312 .returns(testQueryCounter.refreshInterval.asMilliseconds() - 30); 313 }); 314 it("applies default refresh interval if none specified.", function() { 315 const state = new ManagedQuerySagaState(); 316 state.query = { 317 id: "defaultQuery", 318 querySaga: function* () { 319 return null; 320 }, 321 }; 322 state.queryCompletedAt = moment(5000); 323 testSaga(timeToNextRefresh, state) 324 .next() 325 .call(getMoment) 326 .next(5030) 327 .returns(DEFAULT_REFRESH_INTERVAL.asMilliseconds() - 30); 328 }); 329 it("applies retry delay in error case if specified.", function() { 330 const state = new ManagedQuerySagaState(); 331 state.query = testQueryCounter; 332 state.queryCompletedAt = moment(5000); 333 state.lastAttemptFailed = true; 334 testSaga(timeToNextRefresh, state) 335 .next() 336 .call(getMoment) 337 .next(5030) 338 .returns(testQueryCounter.retryDelay.asMilliseconds() - 30); 339 }); 340 it("applies default retry delay in error case if none specified.", function() { 341 const state = new ManagedQuerySagaState(); 342 state.query = { 343 id: "defaultQuery", 344 querySaga: function* () { 345 return null; 346 }, 347 }; 348 state.queryCompletedAt = moment(5000); 349 state.lastAttemptFailed = true; 350 testSaga(timeToNextRefresh, state) 351 .next() 352 .call(getMoment) 353 .next(5030) 354 .returns(DEFAULT_RETRY_DELAY.asMilliseconds() - 30); 355 }); 356 }); 357 }); 358 });