github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ui/src/redux/uiData.spec.ts (about) 1 // Copyright 2018 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 _ from "lodash"; 13 import { Action } from "redux"; 14 import * as protobuf from "protobufjs/minimal"; 15 import fetchMock from "src/util/fetch-mock"; 16 17 import * as protos from "src/js/protos"; 18 import * as api from "src/util/api"; 19 import * as uidata from "./uiData"; 20 21 describe("UIData reducer", function() { 22 describe("actions", function() { 23 it("setUIDataKey() creates the correct action type.", function() { 24 assert.equal(uidata.setUIDataKey("string", null).type, uidata.SET); 25 }); 26 27 it("beginSaveUIData() creates the correct action type.", function() { 28 assert.equal(uidata.beginSaveUIData([]).type, uidata.SAVE); 29 }); 30 31 it("saveErrorUIData() creates the correct action type.", function() { 32 assert.equal(uidata.saveErrorUIData(null, null).type, uidata.SAVE_ERROR); 33 }); 34 35 it("beginLoadUIData() creates the correct action type.", function() { 36 assert.equal(uidata.beginLoadUIData([]).type, uidata.LOAD); 37 }); 38 39 it("loadErrorUIData() creates the correct action type.", function() { 40 assert.equal(uidata.loadErrorUIData(null, null).type, uidata.LOAD_ERROR); 41 }); 42 }); 43 44 describe("helper functions", function () { 45 let state: any; 46 47 beforeEach(function () { 48 state = { uiData: uidata.uiDataReducer(undefined, { type: "unknown" }) }; 49 }); 50 51 const dispatch = (action: Action) => { 52 state = { uiData: uidata.uiDataReducer(state.uiData, action) }; 53 }; 54 55 it("isValid", function () { 56 const key1 = "key1"; 57 const key2 = "key2"; 58 59 assert.isFalse(uidata.isValid(state, key1)); 60 assert.isFalse(uidata.isValid(state, key2)); 61 62 dispatch(uidata.setUIDataKey(key1, null)); 63 64 assert(uidata.isValid(state, key1)); 65 assert.isFalse(uidata.isValid(state, key2)); 66 67 dispatch(uidata.setUIDataKey(key2, null)); 68 69 assert(uidata.isValid(state, key1)); 70 assert(uidata.isValid(state, key2)); 71 }); 72 73 it("getData", function() { 74 const key1 = "key1"; 75 const key2 = "key2"; 76 const value1 = "value1"; 77 const value2 = "value2"; 78 79 assert.isUndefined(uidata.getData(state, key1)); 80 assert.isUndefined(uidata.getData(state, key2)); 81 82 dispatch(uidata.setUIDataKey(key1, value1)); 83 84 assert.equal(uidata.getData(state, key1), value1); 85 assert.isUndefined(uidata.getData(state, key2)); 86 87 dispatch(uidata.setUIDataKey(key2, value2)); 88 89 assert.equal(uidata.getData(state, key1), value1); 90 assert.equal(uidata.getData(state, key2), value2); 91 }); 92 93 it("isSaving and isInFlight", function () { 94 const key1 = "key1"; 95 const key2 = "key2"; 96 const saving = (k: string) => uidata.isSaving(state, k); 97 const inFlight = (k: string) => uidata.isInFlight(state, k); 98 99 assert.isFalse(saving(key1)); 100 assert.isFalse(inFlight(key1)); 101 assert.isFalse(saving(key2)); 102 assert.isFalse(inFlight(key2)); 103 104 dispatch(uidata.beginSaveUIData([key1])); 105 106 assert(saving(key1)); 107 assert(inFlight(key1)); 108 assert.isFalse(saving(key2)); 109 assert.isFalse(inFlight(key2)); 110 111 dispatch(uidata.beginSaveUIData([key1, key2])); 112 113 assert(saving(key1)); 114 assert(inFlight(key1)); 115 assert(saving(key2)); 116 assert(inFlight(key2)); 117 118 dispatch(uidata.beginLoadUIData([key1, key2])); 119 120 assert.isFalse(saving(key1)); 121 assert(inFlight(key1)); 122 assert.isFalse(saving(key2)); 123 assert(inFlight(key2)); 124 125 dispatch(uidata.setUIDataKey(key2, null)); 126 127 assert.isFalse(saving(key1)); 128 assert(inFlight(key1)); 129 assert.isFalse(saving(key2)); 130 assert.isFalse(inFlight(key2)); 131 }); 132 133 it("getSaveError and getLoadError", function () { 134 const key1 = "key1"; 135 const key2 = "key2"; 136 const saveError = (k: string) => uidata.getSaveError(state, k); 137 const loadError = (k: string) => uidata.getLoadError(state, k); 138 139 assert.isNull(saveError(key1)); 140 assert.isNull(saveError(key2)); 141 assert.isNull(loadError(key1)); 142 assert.isNull(loadError(key2)); 143 144 let e = new Error(); 145 dispatch(uidata.saveErrorUIData(key1, e)); 146 147 assert.equal(saveError(key1), e); 148 assert.isNull(saveError(key2)); 149 assert.isNull(loadError(key1)); 150 assert.isNull(loadError(key2)); 151 152 e = new Error(); 153 dispatch(uidata.loadErrorUIData(key1, e)); 154 155 assert.isNull(saveError(key1)); 156 assert.isNull(saveError(key2)); 157 assert.equal(loadError(key1), e); 158 assert.isNull(loadError(key2)); 159 160 dispatch(uidata.beginLoadUIData([key1])); 161 162 assert.isNull(saveError(key1)); 163 assert.isNull(saveError(key2)); 164 assert.equal(loadError(key1), e); 165 assert.isNull(loadError(key2)); 166 167 let e2 = new Error(); 168 dispatch(uidata.saveErrorUIData(key2, e2)); 169 170 assert.isNull(saveError(key1)); 171 assert.equal(saveError(key2), e2); 172 assert.equal(loadError(key1), e); 173 assert.isNull(loadError(key2)); 174 175 e2 = new Error(); 176 dispatch(uidata.loadErrorUIData(key2, e2)); 177 178 assert.isNull(saveError(key1)); 179 assert.isNull(saveError(key2)); 180 assert.equal(loadError(key1), e); 181 assert.equal(loadError(key2), e2); 182 183 dispatch(uidata.beginLoadUIData([key2])); 184 185 assert.isNull(saveError(key1)); 186 assert.isNull(saveError(key2)); 187 assert.equal(loadError(key1), e); 188 assert.equal(loadError(key2), e2); 189 190 dispatch(uidata.setUIDataKey(key1, null)); 191 192 assert.isNull(saveError(key1)); 193 assert.isNull(saveError(key2)); 194 assert.isNull(loadError(key1)); 195 assert.equal(loadError(key2), e2); 196 197 dispatch(uidata.setUIDataKey(key2, null)); 198 199 assert.isNull(saveError(key1)); 200 assert.isNull(saveError(key2)); 201 assert.isNull(loadError(key1)); 202 assert.isNull(loadError(key2)); 203 }); 204 }); 205 206 describe("reducer", function() { 207 let state: uidata.UIDataState; 208 209 beforeEach(function () { 210 state = uidata.uiDataReducer(undefined, { type: "unknown" }); 211 }); 212 213 const dispatch = (action: Action) => { 214 state = uidata.uiDataReducer(state, action); 215 }; 216 217 it("should have the correct default value.", function() { 218 const expected = {}; 219 assert.deepEqual(state, expected); 220 }); 221 222 it("should correctly dispatch setUIDataKey.", function() { 223 const objKey = "obj"; 224 const boolKey = "bool"; 225 const numKey = "num"; 226 const obj = { value: 1 }; 227 const bool = true; 228 const num = 240; 229 230 assert.isUndefined(state[objKey]); 231 assert.isUndefined(state[boolKey]); 232 assert.isUndefined(state[numKey]); 233 234 // Validate setting a variety of object types. 235 dispatch(uidata.setUIDataKey(objKey, obj)); 236 dispatch(uidata.setUIDataKey(boolKey, bool)); 237 dispatch(uidata.setUIDataKey(numKey, num)); 238 239 assert.lengthOf(_.keys(state), 3); 240 assert.equal(state[objKey].data, obj); 241 assert.equal(state[objKey].status, uidata.UIDataStatus.VALID); 242 assert.equal(state[boolKey].data, bool); 243 assert.equal(state[boolKey].status, uidata.UIDataStatus.VALID); 244 assert.equal(state[numKey].data, num); 245 assert.equal(state[numKey].status, uidata.UIDataStatus.VALID); 246 247 // validate overwrite. 248 const obj2 = { value: 2 }; 249 dispatch(uidata.setUIDataKey(objKey, obj2)); 250 assert.lengthOf(_.keys(state), 3); 251 assert.equal(state[objKey].data, obj2); 252 assert.equal(state[objKey].status, uidata.UIDataStatus.VALID); 253 }); 254 255 it("should correctly dispatch loadErrorUIData.", function () { 256 const key1 = "key1"; 257 const err = new Error("an error."); 258 assert.isUndefined(state[key1]); 259 dispatch(uidata.loadErrorUIData(key1, err)); 260 assert.equal(state[key1].status, uidata.UIDataStatus.LOAD_ERROR); 261 assert.equal(state[key1].error, err); 262 263 dispatch(uidata.setUIDataKey(key1, 4)); 264 assert.equal(state[key1].status, uidata.UIDataStatus.VALID); 265 assert.isNull(state[key1].error); 266 }); 267 268 it("should correctly dispatch saveErrorUIData.", function () { 269 const key1 = "key1"; 270 const err = new Error("an error."); 271 assert.isUndefined(state[key1]); 272 dispatch(uidata.saveErrorUIData(key1, err)); 273 assert.equal(state[key1].status, uidata.UIDataStatus.SAVE_ERROR); 274 assert.equal(state[key1].error, err); 275 276 dispatch(uidata.setUIDataKey(key1, 4)); 277 assert.equal(state[key1].status, uidata.UIDataStatus.VALID); 278 assert.isNull(state[key1].error); 279 }); 280 281 it("should correctly dispatch beginSaveUIData", function () { 282 const key1 = "key1"; 283 const key2 = "key2"; 284 const keys = [key1, key2]; 285 dispatch(uidata.beginSaveUIData(keys)); 286 assert.lengthOf(_.keys(state), 2); 287 assert.equal(state[key1].status, uidata.UIDataStatus.SAVING); 288 assert.equal(state[key2].status, uidata.UIDataStatus.SAVING); 289 dispatch(uidata.setUIDataKey(key1, "value1")); 290 dispatch(uidata.setUIDataKey(key2, "value2")); 291 assert.equal(state[key1].status, uidata.UIDataStatus.VALID); 292 assert.equal(state[key2].status, uidata.UIDataStatus.VALID); 293 }); 294 295 it("should correctly dispatch beginLoadUIData", function () { 296 const key1 = "key1"; 297 const key2 = "key2"; 298 const keys = [key1, key2]; 299 dispatch(uidata.beginLoadUIData(keys)); 300 assert.lengthOf(_.keys(state), 2); 301 assert.equal(state[key1].status, uidata.UIDataStatus.LOADING); 302 assert.equal(state[key2].status, uidata.UIDataStatus.LOADING); 303 dispatch(uidata.setUIDataKey(key1, "value1")); 304 dispatch(uidata.setUIDataKey(key2, "value2")); 305 assert.equal(state[key1].status, uidata.UIDataStatus.VALID); 306 assert.equal(state[key2].status, uidata.UIDataStatus.VALID); 307 }); 308 }); 309 310 describe("asynchronous actions", function() { 311 let state: uidata.UIDataState; 312 313 const dispatch = (action: Action) => { 314 state = uidata.uiDataReducer(state, action); 315 }; 316 317 const uiKey1 = "a_key"; 318 const uiObj1 = { 319 setting1: "value", 320 setting2: true, 321 }; 322 323 const uiKey2 = "another_key"; 324 const uiObj2 = 1234; 325 326 const saveUIData = function(...values: uidata.KeyValue[]): Promise<void> { 327 return uidata.saveUIData.apply(this, values)(dispatch, () => { return { uiData: state }; }); 328 }; 329 330 const loadUIData = function(...keys: string[]): Promise<void> { 331 return uidata.loadUIData.apply(this, keys)(dispatch, () => { return { uiData: state }; }); 332 }; 333 334 beforeEach(function () { 335 state = uidata.uiDataReducer(undefined, { type: "unknown" }); 336 }); 337 338 afterEach(fetchMock.restore); 339 340 it("correctly saves UIData", function() { 341 fetchMock.mock({ 342 matcher: `${api.API_PREFIX}/uidata`, 343 method: "POST", 344 response: (_url: string, requestObj: RequestInit) => { 345 assert.equal(state[uiKey1].status, uidata.UIDataStatus.SAVING); 346 assert.equal(state[uiKey2].status, uidata.UIDataStatus.SAVING); 347 348 const kvs = protos.cockroach.server.serverpb.SetUIDataRequest.decode(new Uint8Array(requestObj.body as ArrayBuffer)).key_values; 349 350 assert.lengthOf(_.keys(kvs), 2); 351 352 const deserialize = function(buffer: Uint8Array): Object { 353 return JSON.parse(protobuf.util.utf8.read(buffer, 0, buffer.byteLength)); 354 }; 355 356 assert.deepEqual(deserialize(kvs[uiKey1]), uiObj1); 357 assert.deepEqual(deserialize(kvs[uiKey2]), uiObj2); 358 359 const encodedResponse = protos.cockroach.server.serverpb.SetUIDataResponse.encode({}).finish(); 360 return { 361 body: api.toArrayBuffer(encodedResponse), 362 }; 363 }, 364 }); 365 366 const p = saveUIData( 367 {key: uiKey1, value: uiObj1}, 368 {key: uiKey2, value: uiObj2}, 369 ); 370 371 // Second save should be ignored. 372 const p2 = saveUIData( 373 {key: uiKey1, value: uiObj1}, 374 {key: uiKey2, value: uiObj2}, 375 ); 376 377 return Promise.all([p, p2]).then(() => { 378 assert.lengthOf(fetchMock.calls(`${api.API_PREFIX}/uidata`), 1); 379 assert.lengthOf(_.keys(state), 2); 380 assert.equal(state[uiKey1].data, uiObj1); 381 assert.equal(state[uiKey2].data, uiObj2); 382 assert.isNull(state[uiKey1].error); 383 assert.isNull(state[uiKey2].error); 384 assert.equal(state[uiKey1].status, uidata.UIDataStatus.VALID); 385 assert.equal(state[uiKey2].status, uidata.UIDataStatus.VALID); 386 }); 387 }); 388 389 it("correctly reacts to error during save", function (done) { 390 this.timeout(2000); 391 fetchMock.mock({ 392 matcher: `${api.API_PREFIX}/uidata`, 393 method: "POST", 394 response: () => { 395 return { throws: new Error(), status: 500}; 396 }, 397 }); 398 399 const p = saveUIData( 400 {key: uiKey1, value: uiObj1}, 401 {key: uiKey2, value: uiObj2}, 402 ); 403 404 p.then(() => { 405 assert.lengthOf(fetchMock.calls(`${api.API_PREFIX}/uidata`), 1); 406 assert.lengthOf(_.keys(state), 2); 407 assert.equal(state[uiKey1].status, uidata.UIDataStatus.SAVING); 408 assert.equal(state[uiKey2].status, uidata.UIDataStatus.SAVING); 409 assert.isUndefined(state[uiKey1].data); 410 assert.isUndefined(state[uiKey2].data); 411 assert.notProperty(state[uiKey1], "data"); 412 assert.notProperty(state[uiKey2], "data"); 413 assert.isUndefined(state[uiKey1].error); 414 assert.isUndefined(state[uiKey2].error); 415 setTimeout( 416 () => { 417 assert.equal(state[uiKey1].status, uidata.UIDataStatus.SAVE_ERROR); 418 assert.equal(state[uiKey2].status, uidata.UIDataStatus.SAVE_ERROR); 419 assert.instanceOf(state[uiKey1].error, Error); 420 assert.instanceOf(state[uiKey2].error, Error); 421 done(); 422 }, 423 1000, 424 ); 425 }); 426 }); 427 428 it("correctly loads UIData", function() { 429 const expectedURL = `${api.API_PREFIX}/uidata?keys=${uiKey1}&keys=${uiKey2}`; 430 431 fetchMock.mock({ 432 matcher: expectedURL, 433 method: "GET", 434 response: () => { 435 // FetchMock URL must match the above string exactly, requesting both 436 // keys. 437 assert.equal(state[uiKey1].status, uidata.UIDataStatus.LOADING); 438 assert.equal(state[uiKey2].status, uidata.UIDataStatus.LOADING); 439 440 const response: protos.cockroach.server.serverpb.IGetUIDataResponse = { 441 key_values: {}, 442 }; 443 const setValue = function(key: string, obj: Object) { 444 const stringifiedValue = JSON.stringify(obj); 445 const buffer = new Uint8Array(protobuf.util.utf8.length(stringifiedValue)); 446 protobuf.util.utf8.write(stringifiedValue, buffer, 0); 447 response.key_values[key] = { value: buffer }; 448 }; 449 setValue(uiKey1, uiObj1); 450 setValue(uiKey2, uiObj2); 451 452 const encodedResponse = protos.cockroach.server.serverpb.GetUIDataResponse.encode(response).finish(); 453 return { 454 body: api.toArrayBuffer(encodedResponse), 455 }; 456 }, 457 }); 458 459 const p = loadUIData(uiKey1, uiKey2); 460 const p2 = loadUIData(uiKey1, uiKey2); // Second load should be ignored. 461 462 return Promise.all([p, p2]).then(() => { 463 assert.lengthOf(fetchMock.calls(expectedURL), 1); 464 assert.lengthOf(_.keys(state), 2); 465 assert.deepEqual(state[uiKey1].data, uiObj1); 466 assert.deepEqual(state[uiKey2].data, uiObj2); 467 assert.isNull(state[uiKey1].error); 468 assert.isNull(state[uiKey2].error); 469 assert.equal(state[uiKey1].status, uidata.UIDataStatus.VALID); 470 assert.equal(state[uiKey2].status, uidata.UIDataStatus.VALID); 471 }); 472 }); 473 474 it("correctly reacts to error during load", function (done) { 475 this.timeout(2000); 476 477 const uidataPrefixMatcher = `begin:${api.API_PREFIX}/uidata`; 478 479 fetchMock.mock({ 480 matcher: uidataPrefixMatcher, 481 response: () => { 482 return { throws: new Error() }; 483 }, 484 }); 485 486 const p = loadUIData(uiKey1, uiKey2); 487 488 p.then(() => { 489 assert.lengthOf(fetchMock.calls(uidataPrefixMatcher), 1); 490 assert.lengthOf(_.keys(state), 2); 491 assert.equal(state[uiKey1].status, uidata.UIDataStatus.LOADING); 492 assert.equal(state[uiKey2].status, uidata.UIDataStatus.LOADING); 493 assert.isUndefined(state[uiKey1].data); 494 assert.isUndefined(state[uiKey2].data); 495 assert.notProperty(state[uiKey1], "data"); 496 assert.notProperty(state[uiKey2], "data"); 497 assert.isUndefined(state[uiKey1].error); 498 assert.isUndefined(state[uiKey2].error); 499 setTimeout( 500 () => { 501 assert.equal(state[uiKey1].status, uidata.UIDataStatus.LOAD_ERROR); 502 assert.equal(state[uiKey2].status, uidata.UIDataStatus.LOAD_ERROR); 503 assert.instanceOf(state[uiKey1].error, Error); 504 assert.instanceOf(state[uiKey2].error, Error); 505 done(); 506 }, 507 1000, 508 ); 509 }); 510 }); 511 512 it("handles missing keys", function () { 513 const missingKey = "missingKey"; 514 515 const expectedURL = `${api.API_PREFIX}/uidata?keys=${missingKey}`; 516 517 fetchMock.mock({ 518 matcher: expectedURL, 519 response: () => { 520 assert.equal(state[missingKey].status, uidata.UIDataStatus.LOADING); 521 522 const encodedResponse = protos.cockroach.server.serverpb.GetUIDataResponse.encode({}).finish(); 523 return { 524 body: api.toArrayBuffer(encodedResponse), 525 }; 526 }, 527 }); 528 529 const p = loadUIData(missingKey); 530 531 return p.then(() => { 532 assert.lengthOf(fetchMock.calls(expectedURL), 1); 533 assert.lengthOf(_.keys(state), 1); 534 assert.equal(state[missingKey].data, undefined); 535 assert.property(state[missingKey], "data"); 536 assert.equal(state[missingKey].status, uidata.UIDataStatus.VALID); 537 assert.isNull(state[missingKey].error); 538 }); 539 }); 540 }); 541 });