github.com/freiheit-com/kuberpult@v1.24.2-0.20240328135542-315d5630abe6/services/frontend-service/src/ui/utils/store.test.tsx (about) 1 /*This file is part of kuberpult. 2 3 Kuberpult is free software: you can redistribute it and/or modify 4 it under the terms of the Expat(MIT) License as published by 5 the Free Software Foundation. 6 7 Kuberpult is distributed in the hope that it will be useful, 8 but WITHOUT ANY WARRANTY; without even the implied warranty of 9 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 MIT License for more details. 11 12 You should have received a copy of the MIT License 13 along with kuberpult. If not, see <https://directory.fsf.org/wiki/License:Expat>. 14 15 Copyright 2023 freiheit.com*/ 16 import { act, renderHook } from '@testing-library/react'; 17 import { 18 addAction, 19 AllLocks, 20 appendAction, 21 DisplayLock, 22 FlushRolloutStatus, 23 UpdateAction, 24 updateActions, 25 UpdateOverview, 26 UpdateRolloutStatus, 27 UpdateSnackbar, 28 useLocksConflictingWithActions, 29 useLocksSimilarTo, 30 useNavigateWithSearchParams, 31 useRolloutStatus, 32 } from './store'; 33 import { 34 BatchAction, 35 Environment, 36 EnvironmentGroup, 37 LockBehavior, 38 Priority, 39 RolloutStatus, 40 StreamStatusResponse, 41 } from '../../api/api'; 42 import { makeDisplayLock, makeLock } from '../../setupTests'; 43 import { BrowserRouter } from 'react-router-dom'; 44 45 describe('Test useLocksSimilarTo', () => { 46 type TestDataStore = { 47 name: string; 48 inputEnvGroups: EnvironmentGroup[]; // this just defines what locks generally exist 49 inputAction: BatchAction; // the action we are rendering currently in the sidebar 50 expectedLocks: AllLocks; 51 }; 52 53 const testdata: TestDataStore[] = [ 54 { 55 name: 'empty data', 56 inputAction: { 57 action: { 58 $case: 'deleteEnvironmentLock', 59 deleteEnvironmentLock: { 60 environment: 'dev', 61 lockId: 'l1', 62 }, 63 }, 64 }, 65 inputEnvGroups: [], 66 expectedLocks: { 67 appLocks: [], 68 environmentLocks: [], 69 }, 70 }, 71 { 72 name: 'one env lock: should not find another lock', 73 inputAction: { 74 action: { 75 $case: 'deleteEnvironmentLock', 76 deleteEnvironmentLock: { 77 environment: 'dev', 78 lockId: 'l1', 79 }, 80 }, 81 }, 82 inputEnvGroups: [ 83 { 84 environments: [ 85 { 86 name: 'dev', 87 distanceToUpstream: 0, 88 priority: Priority.UPSTREAM, 89 locks: { 90 l1: makeLock({ lockId: 'l1' }), 91 }, 92 applications: {}, 93 }, 94 ], 95 environmentGroupName: 'group1', 96 distanceToUpstream: 0, 97 priority: Priority.UNRECOGNIZED, 98 }, 99 ], 100 expectedLocks: { 101 appLocks: [], 102 environmentLocks: [], 103 }, 104 }, 105 { 106 name: 'two env locks with same ID on different envs: should find the other lock', 107 inputAction: { 108 action: { 109 $case: 'deleteEnvironmentLock', 110 deleteEnvironmentLock: { 111 environment: 'dev', 112 lockId: 'l1', 113 }, 114 }, 115 }, 116 inputEnvGroups: [ 117 { 118 environments: [ 119 { 120 name: 'dev', 121 distanceToUpstream: 0, 122 priority: Priority.UPSTREAM, 123 locks: { 124 l1: makeLock({ lockId: 'l1' }), 125 }, 126 applications: {}, 127 }, 128 { 129 name: 'staging', 130 distanceToUpstream: 0, 131 priority: Priority.UPSTREAM, 132 locks: { 133 l1: makeLock({ lockId: 'l1' }), 134 }, 135 applications: {}, 136 }, 137 ], 138 environmentGroupName: 'group1', 139 distanceToUpstream: 0, 140 priority: Priority.UNRECOGNIZED, 141 }, 142 ], 143 expectedLocks: { 144 appLocks: [], 145 environmentLocks: [ 146 makeDisplayLock({ 147 lockId: 'l1', 148 environment: 'staging', 149 }), 150 ], 151 }, 152 }, 153 { 154 name: 'env lock and app lock with same ID: deleting the env lock should find the other lock', 155 inputAction: { 156 action: { 157 $case: 'deleteEnvironmentLock', 158 deleteEnvironmentLock: { 159 environment: 'dev', 160 lockId: 'l1', 161 }, 162 }, 163 }, 164 inputEnvGroups: [ 165 { 166 environments: [ 167 { 168 name: 'dev', 169 distanceToUpstream: 0, 170 priority: Priority.UPSTREAM, 171 locks: { 172 l1: makeLock({ lockId: 'l1' }), 173 }, 174 applications: { 175 app1: { 176 name: 'betty', 177 locks: { 178 l1: makeLock({ lockId: 'l1' }), 179 }, 180 version: 666, 181 undeployVersion: false, 182 queuedVersion: 0, 183 argoCd: undefined, 184 }, 185 }, 186 }, 187 ], 188 environmentGroupName: 'group1', 189 distanceToUpstream: 0, 190 priority: Priority.UNRECOGNIZED, 191 }, 192 ], 193 expectedLocks: { 194 appLocks: [ 195 makeDisplayLock({ 196 environment: 'dev', 197 lockId: 'l1', 198 application: 'betty', 199 message: 'lock msg 1', 200 }), 201 ], 202 environmentLocks: [], 203 }, 204 }, 205 { 206 name: 'bug: delete-all button must appear for each entry. 2 Env Locks, 1 App lock exist. 1 Env lock, 1 app lock in cart', 207 inputAction: { 208 action: { 209 $case: 'deleteEnvironmentApplicationLock', 210 deleteEnvironmentApplicationLock: { 211 environment: 'dev', 212 lockId: 'l1', 213 application: 'app1', 214 }, 215 }, 216 }, 217 inputEnvGroups: [ 218 { 219 environments: [ 220 { 221 name: 'dev', 222 distanceToUpstream: 0, 223 priority: Priority.UPSTREAM, 224 locks: { 225 l1: makeLock({ lockId: 'l1' }), 226 }, 227 applications: { 228 app1: { 229 name: 'betty', 230 locks: { 231 l1: makeLock({ lockId: 'l1' }), 232 }, 233 version: 666, 234 undeployVersion: false, 235 queuedVersion: 0, 236 argoCd: undefined, 237 }, 238 }, 239 }, 240 { 241 name: 'dev2', 242 distanceToUpstream: 0, 243 priority: Priority.UPSTREAM, 244 locks: { 245 l1: makeLock({ lockId: 'l1' }), 246 }, 247 applications: {}, 248 }, 249 ], 250 environmentGroupName: 'group1', 251 distanceToUpstream: 0, 252 priority: Priority.UNRECOGNIZED, 253 }, 254 ], 255 expectedLocks: { 256 appLocks: [ 257 makeDisplayLock({ 258 environment: 'dev', 259 lockId: 'l1', 260 application: 'betty', 261 message: 'lock msg 1', 262 }), 263 ], 264 environmentLocks: [ 265 makeDisplayLock({ 266 environment: 'dev', 267 lockId: 'l1', 268 message: 'lock msg 1', 269 }), 270 makeDisplayLock({ 271 environment: 'dev2', 272 lockId: 'l1', 273 message: 'lock msg 1', 274 }), 275 ], 276 }, 277 }, 278 ]; 279 280 describe.each(testdata)('with', (testcase) => { 281 it(testcase.name, () => { 282 // given 283 updateActions([]); 284 UpdateOverview.set({ 285 applications: {}, 286 environmentGroups: testcase.inputEnvGroups, 287 }); 288 // when 289 const actions = renderHook(() => useLocksSimilarTo(testcase.inputAction)).result.current; 290 // then 291 expect(actions.appLocks).toStrictEqual(testcase.expectedLocks.appLocks); 292 expect(actions.environmentLocks).toStrictEqual(testcase.expectedLocks.environmentLocks); 293 }); 294 }); 295 }); 296 297 describe('Test useNavigateWithSearchParams', () => { 298 type TestDataStore = { 299 name: string; 300 currentURL: string; 301 navigationTo: string; 302 expectedURL: string; 303 }; 304 305 const testdata: TestDataStore[] = [ 306 { 307 name: 'url with no search parameters', 308 currentURL: '', 309 navigationTo: 'test-random-page', 310 expectedURL: 'test-random-page', 311 }, 312 { 313 name: 'url with some search parameters', 314 currentURL: '/ui/home/test/whaat?query1=boo&query2=bar', 315 navigationTo: 'test-random-page', 316 expectedURL: 'test-random-page?query1=boo&query2=bar', 317 }, 318 ]; 319 320 describe.each(testdata)('with', (testcase) => { 321 it(testcase.name, () => { 322 // given 323 const wrapper = ({ children }: { children: JSX.Element }) => <BrowserRouter>{children}</BrowserRouter>; 324 window.history.pushState({}, 'Test page', testcase.currentURL); 325 // when 326 const result = renderHook(() => useNavigateWithSearchParams(testcase.navigationTo), { wrapper }).result 327 .current; 328 // then 329 expect(result.navURL).toBe(testcase.expectedURL); 330 // when 331 act(() => { 332 result.navCallback(); 333 }); 334 // then 335 expect(window.location.href).toContain(testcase.expectedURL); 336 }); 337 }); 338 }); 339 340 describe('Rollout Status', () => { 341 type Testcase = { 342 name: string; 343 events: Array<StreamStatusResponse | { error: true }>; 344 expectedApps: Array<{ 345 application: string; 346 environment: string; 347 version: number; 348 rolloutStatus: RolloutStatus | undefined; 349 }>; 350 }; 351 352 const testdata: Array<Testcase> = [ 353 { 354 name: 'not enabled if empty', 355 events: [], 356 357 expectedApps: [ 358 { 359 application: 'app1', 360 environment: 'env1', 361 version: 0, 362 rolloutStatus: undefined, 363 }, 364 ], 365 }, 366 { 367 name: 'updates one app', 368 events: [ 369 { 370 environment: 'env1', 371 application: 'app1', 372 version: 1, 373 rolloutStatus: RolloutStatus.ROLLOUT_STATUS_SUCCESFUL, 374 }, 375 ], 376 377 expectedApps: [ 378 { 379 application: 'app1', 380 environment: 'env1', 381 version: 1, 382 rolloutStatus: RolloutStatus.ROLLOUT_STATUS_SUCCESFUL, 383 }, 384 ], 385 }, 386 { 387 name: 'keep the latest entry per app and environment', 388 events: [ 389 { 390 environment: 'env1', 391 application: 'app1', 392 version: 1, 393 rolloutStatus: RolloutStatus.ROLLOUT_STATUS_SUCCESFUL, 394 }, 395 { 396 environment: 'env1', 397 application: 'app1', 398 version: 2, 399 rolloutStatus: RolloutStatus.ROLLOUT_STATUS_SUCCESFUL, 400 }, 401 ], 402 403 expectedApps: [ 404 { 405 application: 'app1', 406 environment: 'env1', 407 version: 2, 408 rolloutStatus: RolloutStatus.ROLLOUT_STATUS_SUCCESFUL, 409 }, 410 ], 411 }, 412 { 413 name: 'flushes the state', 414 events: [ 415 { 416 environment: 'env1', 417 application: 'app1', 418 version: 1, 419 rolloutStatus: RolloutStatus.ROLLOUT_STATUS_SUCCESFUL, 420 }, 421 { error: true }, 422 ], 423 424 expectedApps: [ 425 { 426 application: 'app1', 427 environment: 'env1', 428 version: 0, 429 rolloutStatus: undefined, 430 }, 431 ], 432 }, 433 ]; 434 435 describe.each(testdata)('with', (testcase) => { 436 it(testcase.name, () => { 437 FlushRolloutStatus(); 438 testcase.events.forEach((ev) => { 439 if ('error' in ev) { 440 FlushRolloutStatus(); 441 } else { 442 UpdateRolloutStatus(ev); 443 } 444 }); 445 testcase.expectedApps.forEach((app) => { 446 const rollout = renderHook(() => 447 useRolloutStatus((getter) => getter.getAppStatus(app.application, app.version, app.environment)) 448 ); 449 expect(rollout.result.current).toEqual(app.rolloutStatus); 450 }); 451 }); 452 }); 453 }); 454 455 describe('Test addAction duplicate detection', () => { 456 type TestDataStore = { 457 name: string; 458 firstAction: BatchAction; 459 differentAction: BatchAction; 460 }; 461 462 const testdata: TestDataStore[] = [ 463 { 464 name: 'create environment lock', 465 firstAction: { 466 action: { 467 $case: 'createEnvironmentLock', 468 createEnvironmentLock: { 469 environment: 'dev', 470 lockId: 'foo', 471 message: 'do it', 472 }, 473 }, 474 }, 475 differentAction: { 476 action: { 477 $case: 'createEnvironmentLock', 478 createEnvironmentLock: { 479 environment: 'staging', 480 lockId: 'foo', 481 message: 'do it', 482 }, 483 }, 484 }, 485 }, 486 { 487 name: 'delete environment lock', 488 firstAction: { 489 action: { 490 $case: 'deleteEnvironmentLock', 491 deleteEnvironmentLock: { 492 environment: 'dev', 493 lockId: 'foo', 494 }, 495 }, 496 }, 497 differentAction: { 498 action: { 499 $case: 'deleteEnvironmentLock', 500 deleteEnvironmentLock: { 501 environment: 'staging', 502 lockId: 'foo', 503 }, 504 }, 505 }, 506 }, 507 { 508 name: 'create app lock', 509 firstAction: { 510 action: { 511 $case: 'createEnvironmentApplicationLock', 512 createEnvironmentApplicationLock: { 513 environment: 'dev', 514 application: 'app1', 515 lockId: 'foo', 516 message: 'do it', 517 }, 518 }, 519 }, 520 differentAction: { 521 action: { 522 $case: 'createEnvironmentApplicationLock', 523 createEnvironmentApplicationLock: { 524 environment: 'dev', 525 application: 'app2', 526 lockId: 'foo', 527 message: 'do it', 528 }, 529 }, 530 }, 531 }, 532 { 533 name: 'delete app lock', 534 firstAction: { 535 action: { 536 $case: 'deleteEnvironmentApplicationLock', 537 deleteEnvironmentApplicationLock: { 538 environment: 'dev', 539 application: 'app1', 540 lockId: 'foo', 541 }, 542 }, 543 }, 544 differentAction: { 545 action: { 546 $case: 'deleteEnvironmentApplicationLock', 547 deleteEnvironmentApplicationLock: { 548 environment: 'dev', 549 application: 'app2', 550 lockId: 'foo', 551 }, 552 }, 553 }, 554 }, 555 { 556 name: 'deploy', 557 firstAction: { 558 action: { 559 $case: 'deploy', 560 deploy: { 561 environment: 'dev', 562 application: 'app1', 563 version: 1, 564 ignoreAllLocks: false, 565 lockBehavior: LockBehavior.IGNORE, 566 }, 567 }, 568 }, 569 differentAction: { 570 action: { 571 $case: 'deploy', 572 deploy: { 573 environment: 'dev', 574 application: 'app2', 575 version: 1, 576 ignoreAllLocks: false, 577 lockBehavior: LockBehavior.IGNORE, 578 }, 579 }, 580 }, 581 }, 582 { 583 name: 'undeploy', 584 firstAction: { 585 action: { 586 $case: 'undeploy', 587 undeploy: { 588 application: 'app1', 589 }, 590 }, 591 }, 592 differentAction: { 593 action: { 594 $case: 'undeploy', 595 undeploy: { 596 application: 'app2', 597 }, 598 }, 599 }, 600 }, 601 { 602 name: 'prepare undeploy', 603 firstAction: { 604 action: { 605 $case: 'prepareUndeploy', 606 prepareUndeploy: { 607 application: 'app1', 608 }, 609 }, 610 }, 611 differentAction: { 612 action: { 613 $case: 'prepareUndeploy', 614 prepareUndeploy: { 615 application: 'app2', 616 }, 617 }, 618 }, 619 }, 620 ]; 621 622 describe.each(testdata)('with', (testcase) => { 623 it(testcase.name, () => { 624 // given 625 updateActions([]); 626 627 // when 628 addAction(testcase.firstAction); 629 // then 630 expect(UpdateAction.get().actions.length).toStrictEqual(1); 631 632 // when 633 addAction(testcase.firstAction); 634 // then 635 expect(UpdateAction.get().actions.length).toStrictEqual(1); 636 637 // when 638 addAction(testcase.differentAction); 639 // then 640 expect(UpdateAction.get().actions.length).toStrictEqual(2); 641 642 // when 643 addAction(testcase.differentAction); 644 // then 645 expect(UpdateAction.get().actions.length).toStrictEqual(2); 646 }); 647 }); 648 }); 649 650 describe('Test maxActions', () => { 651 type TestDataStore = { 652 name: string; 653 inputActionsLen: number; 654 expectedLen: number; 655 expectedShowError: boolean; 656 }; 657 658 const testdata: TestDataStore[] = [ 659 { 660 name: 'below limit', 661 inputActionsLen: 99, 662 expectedLen: 99, 663 expectedShowError: false, 664 }, 665 { 666 name: 'at limit', 667 inputActionsLen: 100, 668 expectedLen: 100, 669 expectedShowError: false, 670 }, 671 { 672 name: 'over limit', 673 inputActionsLen: 101, 674 expectedLen: 100, 675 expectedShowError: true, 676 }, 677 ]; 678 679 describe.each(testdata)('with', (testcase) => { 680 it(testcase.name, () => { 681 // given 682 updateActions([]); 683 684 // when 685 for (let i = 0; i < testcase.inputActionsLen; i++) { 686 appendAction([ 687 { 688 action: { 689 $case: 'deploy', 690 deploy: { 691 environment: 'foo', 692 application: 'bread' + i, 693 version: i, 694 ignoreAllLocks: false, 695 lockBehavior: LockBehavior.IGNORE, 696 }, 697 }, 698 }, 699 ]); 700 } 701 // then 702 expect(UpdateSnackbar.get().show).toStrictEqual(testcase.expectedShowError); 703 expect(UpdateAction.get().actions.length).toStrictEqual(testcase.expectedLen); 704 }); 705 }); 706 }); 707 708 describe('Test useLocksConflictingWithActions', () => { 709 type TestDataStore = { 710 name: string; 711 actions: BatchAction[]; 712 expectedAppLocks: DisplayLock[]; 713 expectedEnvLocks: DisplayLock[]; 714 environments: Environment[]; 715 }; 716 717 const testdata: TestDataStore[] = [ 718 { 719 name: 'empty actions empty locks', 720 actions: [], 721 expectedAppLocks: [], 722 expectedEnvLocks: [], 723 environments: [], 724 }, 725 { 726 name: 'deploy action and related app lock and env lock', 727 actions: [ 728 { 729 action: { 730 $case: 'deploy', 731 deploy: { 732 environment: 'dev', 733 application: 'app1', 734 version: 1, 735 ignoreAllLocks: false, 736 lockBehavior: LockBehavior.IGNORE, 737 }, 738 }, 739 }, 740 ], 741 environments: [ 742 { 743 name: 'dev', 744 locks: { 745 'lock-env-dev': makeLock({ 746 message: 'locked because christmas', 747 lockId: 'my-env-lock1', 748 }), 749 }, 750 applications: { 751 echo: { 752 name: 'app1', 753 version: 0, 754 locks: { 755 applock: makeLock({ 756 lockId: 'app-lock-id', 757 message: 'i do not like this app', 758 }), 759 }, 760 queuedVersion: 0, 761 undeployVersion: false, 762 }, 763 }, 764 distanceToUpstream: 0, 765 priority: 0, 766 }, 767 ], 768 expectedAppLocks: [ 769 makeDisplayLock({ 770 lockId: 'app-lock-id', 771 application: 'app1', 772 message: 'i do not like this app', 773 environment: 'dev', 774 }), 775 ], 776 expectedEnvLocks: [ 777 makeDisplayLock({ 778 lockId: 'my-env-lock1', 779 environment: 'dev', 780 message: 'locked because christmas', 781 }), 782 ], 783 }, 784 { 785 name: 'deploy action and unrelated locks', 786 actions: [ 787 { 788 action: { 789 $case: 'deploy', 790 deploy: { 791 environment: 'dev', 792 application: 'app1', 793 version: 1, 794 ignoreAllLocks: false, 795 lockBehavior: LockBehavior.IGNORE, 796 }, 797 }, 798 }, 799 ], 800 environments: [ 801 { 802 name: 'staging', // this lock differs by stage 803 locks: { 804 'lock-env-dev': makeLock({ 805 message: 'locked because christmas', 806 lockId: 'my-env-lock1', 807 }), 808 }, 809 applications: { 810 echo: { 811 name: 'anotherapp', // this lock differs by app 812 version: 0, 813 locks: { 814 applock: makeLock({ 815 lockId: 'app-lock-id', 816 message: 'i do not like this app', 817 }), 818 }, 819 queuedVersion: 0, 820 undeployVersion: false, 821 }, 822 }, 823 distanceToUpstream: 0, 824 priority: 0, 825 }, 826 ], 827 expectedAppLocks: [], 828 expectedEnvLocks: [], 829 }, 830 ]; 831 832 describe.each(testdata)('with', (testcase) => { 833 it(testcase.name, () => { 834 // given 835 updateActions(testcase.actions); 836 UpdateOverview.set({ 837 applications: {}, 838 environmentGroups: [ 839 { 840 environmentGroupName: 'g1', 841 environments: testcase.environments, 842 distanceToUpstream: 0, 843 priority: Priority.UNRECOGNIZED, 844 }, 845 ], 846 }); 847 848 // when 849 const actualLocks = renderHook(() => useLocksConflictingWithActions()).result.current; 850 // then 851 expect(actualLocks.environmentLocks).toStrictEqual(testcase.expectedEnvLocks); 852 expect(actualLocks.appLocks).toStrictEqual(testcase.expectedAppLocks); 853 }); 854 }); 855 }); 856 857 describe('Test addAction blocking release train additions', () => { 858 type TestDataStore = { 859 name: string; 860 firstAction: BatchAction; 861 differentAction: BatchAction; 862 expectedActions: number; 863 }; 864 865 const testdata: TestDataStore[] = [ 866 { 867 name: 'deploy 2 in a row', 868 expectedActions: 2, 869 firstAction: { 870 action: { 871 $case: 'deploy', 872 deploy: { 873 environment: 'dev', 874 application: 'app1', 875 version: 1, 876 ignoreAllLocks: false, 877 lockBehavior: LockBehavior.IGNORE, 878 }, 879 }, 880 }, 881 differentAction: { 882 action: { 883 $case: 'deploy', 884 deploy: { 885 environment: 'dev', 886 application: 'app2', 887 version: 1, 888 ignoreAllLocks: false, 889 lockBehavior: LockBehavior.IGNORE, 890 }, 891 }, 892 }, 893 }, 894 { 895 name: 'can not add release train after deploy action', 896 expectedActions: 1, 897 firstAction: { 898 action: { 899 $case: 'deploy', 900 deploy: { 901 environment: 'dev', 902 application: 'app1', 903 version: 1, 904 ignoreAllLocks: false, 905 lockBehavior: LockBehavior.IGNORE, 906 }, 907 }, 908 }, 909 differentAction: { 910 action: { 911 $case: 'releaseTrain', 912 releaseTrain: { 913 target: 'dev', 914 team: '', 915 commitHash: '', 916 }, 917 }, 918 }, 919 }, 920 { 921 name: 'can not add release train after release train', 922 expectedActions: 1, 923 firstAction: { 924 action: { 925 $case: 'releaseTrain', 926 releaseTrain: { 927 target: 'dev', 928 team: '', 929 commitHash: '', 930 }, 931 }, 932 }, 933 differentAction: { 934 action: { 935 $case: 'releaseTrain', 936 releaseTrain: { 937 target: 'stagin', 938 team: '', 939 commitHash: '', 940 }, 941 }, 942 }, 943 }, 944 { 945 name: 'can not add deploy action after release train', 946 expectedActions: 1, 947 firstAction: { 948 action: { 949 $case: 'releaseTrain', 950 releaseTrain: { 951 target: 'dev', 952 team: '', 953 commitHash: '', 954 }, 955 }, 956 }, 957 differentAction: { 958 action: { 959 $case: 'deploy', 960 deploy: { 961 environment: 'dev', 962 application: 'app1', 963 version: 1, 964 ignoreAllLocks: false, 965 lockBehavior: LockBehavior.IGNORE, 966 }, 967 }, 968 }, 969 }, 970 ]; 971 972 describe.each(testdata)('with', (testcase) => { 973 it(testcase.name, () => { 974 // given 975 updateActions([]); 976 977 // when 978 addAction(testcase.firstAction); 979 // then 980 expect(UpdateAction.get().actions.length).toStrictEqual(1); 981 982 // when 983 addAction(testcase.differentAction); 984 // then 985 expect(UpdateAction.get().actions.length).toStrictEqual(testcase.expectedActions); 986 }); 987 }); 988 });