github.com/pf-qiu/concourse/v6@v6.7.3-0.20201207032516-1f455d73275f/web/elm/tests/FlySuccessFeature.elm (about) 1 module FlySuccessFeature exposing (all) 2 3 import Application.Application as Application 4 import Assets 5 import Common exposing (defineHoverBehaviour, queryView) 6 import DashboardTests exposing (iconSelector) 7 import Expect exposing (Expectation) 8 import Html.Attributes as Attr 9 import Http 10 import Message.Callback exposing (Callback(..)) 11 import Message.Effects as Effects 12 import Message.Message 13 import Message.Subscription as Subscription 14 import Message.TopLevelMessage as Msgs 15 import Test exposing (..) 16 import Test.Html.Event as Event 17 import Test.Html.Query as Query 18 import Test.Html.Selector 19 exposing 20 ( attribute 21 , containing 22 , id 23 , style 24 , tag 25 , text 26 ) 27 import Url 28 import Views.Styles 29 30 31 32 -- CONSTANTS (might be able to remove this and refer to "configuration"-type 33 -- files like Colors.elm) 34 35 36 almostWhite : String 37 almostWhite = 38 "#e6e7e8" 39 40 41 darkGrey : String 42 darkGrey = 43 "#323030" 44 45 46 darkerGrey : String 47 darkerGrey = 48 "#242424" 49 50 51 blue : String 52 blue = 53 "#196ac8" 54 55 56 authToken : String 57 authToken = 58 "some_auth_token" 59 60 61 flyPort : Int 62 flyPort = 63 1234 64 65 66 flags : Application.Flags 67 flags = 68 { turbulenceImgSrc = "" 69 , notFoundImgSrc = "" 70 , csrfToken = "" 71 , authToken = authToken 72 , pipelineRunningKeyframes = "" 73 } 74 75 76 77 -- SETUPS (i dunno, maybe use fuzzers?) 78 79 80 type alias SetupSteps = 81 () -> ( Application.Model, List Effects.Effect ) 82 83 84 type alias Setup = 85 ( String, SetupSteps ) 86 87 88 setupDesc : Setup -> String 89 setupDesc = 90 Tuple.first 91 92 93 steps : Setup -> SetupSteps 94 steps = 95 Tuple.second 96 97 98 makeSetup : String -> SetupSteps -> Setup 99 makeSetup = 100 \a b -> ( a, b ) 101 102 103 whenOnFlySuccessPage : Setup 104 whenOnFlySuccessPage = 105 makeSetup "when on fly success page" 106 (\_ -> 107 Application.init 108 flags 109 { protocol = Url.Http 110 , host = "" 111 , port_ = Nothing 112 , path = "/fly_success" 113 , query = Just <| "fly_port=" ++ String.fromInt flyPort 114 , fragment = Nothing 115 } 116 ) 117 118 119 whenOnNoopPage : Setup 120 whenOnNoopPage = 121 makeSetup "when on fly success page with noop parameter" 122 (\_ -> 123 Application.init 124 flags 125 { protocol = Url.Http 126 , host = "" 127 , port_ = Nothing 128 , path = "/fly_success" 129 , query = Just <| "noop=true&fly_port=" ++ String.fromInt flyPort 130 , fragment = Nothing 131 } 132 ) 133 134 135 invalidFlyPort : Setup 136 invalidFlyPort = 137 makeSetup "with invalid fly port" 138 (\_ -> 139 Application.init 140 flags 141 { protocol = Url.Http 142 , host = "" 143 , port_ = Nothing 144 , path = "/fly_success" 145 , query = Just "fly_port=banana" 146 , fragment = Nothing 147 } 148 ) 149 150 151 tokenSendSuccess : Setup 152 tokenSendSuccess = 153 makeSetup "when token successfully sent to fly" 154 (steps whenOnFlySuccessPage 155 >> Tuple.first 156 >> Application.handleDelivery 157 (Subscription.TokenSentToFly Subscription.Success) 158 ) 159 160 161 tokenSendFailed : Setup 162 tokenSendFailed = 163 makeSetup "when token failed to send to fly" 164 (steps whenOnFlySuccessPage 165 >> Tuple.first 166 >> Application.handleDelivery 167 (Subscription.TokenSentToFly Subscription.NetworkError) 168 ) 169 170 171 tokenSendBlocked : Setup 172 tokenSendBlocked = 173 makeSetup "when token sending is blocked by the browser" 174 (steps whenOnFlySuccessPage 175 >> Tuple.first 176 >> Application.handleDelivery 177 (Subscription.TokenSentToFly Subscription.BrowserError) 178 ) 179 180 181 tokenCopied : Setup 182 tokenCopied = 183 makeSetup "when token copied to clipboard" 184 (steps tokenSendFailed 185 >> Tuple.first 186 >> Application.update 187 (Msgs.Update <| 188 Message.Message.Click Message.Message.CopyTokenButton 189 ) 190 ) 191 192 193 allCases : List Setup 194 allCases = 195 [ whenOnFlySuccessPage 196 , invalidFlyPort 197 , tokenSendFailed 198 , tokenSendSuccess 199 ] 200 201 202 203 -- QUERIES 204 205 206 type alias Query = 207 Application.Model -> Query.Single Msgs.TopLevelMessage 208 209 210 topBar : Query 211 topBar = 212 queryView >> Query.find [ id "top-bar-app" ] 213 214 215 successCard : Query 216 successCard = 217 queryView >> Query.find [ id "success-card" ] 218 219 220 title : Query 221 title = 222 successCard >> Query.find [ id "success-card-title" ] 223 224 225 body : Query 226 body = 227 successCard >> Query.find [ id "success-card-body" ] 228 229 230 firstParagraph : Query 231 firstParagraph = 232 successCard 233 >> Query.find [ id "success-card-body" ] 234 >> Query.find [ id "first-paragraph" ] 235 236 237 secondParagraph : Query 238 secondParagraph = 239 successCard 240 >> Query.find [ id "success-card-body" ] 241 >> Query.find [ id "second-paragraph" ] 242 243 244 copyTokenButton : Query 245 copyTokenButton = 246 body >> Query.find [ id "copy-token" ] 247 248 249 copyTokenInput : Query 250 copyTokenInput = 251 body >> Query.find [ id "manual-copy-token" ] 252 253 254 sendTokenButton : Query 255 sendTokenButton = 256 body >> Query.find [ id "send-token" ] 257 258 259 copyTokenButtonIcon : Query 260 copyTokenButtonIcon = 261 body 262 >> Query.find [ id "copy-token" ] 263 >> Query.find [ id "copy-icon" ] 264 265 266 267 -- PROPERTIES 268 269 270 type alias Assertion = 271 Query.Single Msgs.TopLevelMessage -> Expectation 272 273 274 type alias Property = 275 Setup -> Test 276 277 278 property : Query -> String -> Assertion -> Property 279 property query description assertion setup = 280 test (setupDesc setup ++ ", " ++ description) <| 281 steps setup 282 >> Tuple.first 283 >> query 284 >> assertion 285 286 287 288 -- token send effect 289 290 291 sendsToken : Property 292 sendsToken setup = 293 test (setupDesc setup ++ ", sends token to fly") <| 294 steps setup 295 >> Tuple.second 296 >> Common.contains (Effects.SendTokenToFly authToken flyPort) 297 298 299 doesNotSendToken : Property 300 doesNotSendToken setup = 301 test (setupDesc setup ++ ", does not send token to fly") <| 302 steps setup 303 >> Tuple.second 304 >> Common.notContains (Effects.SendTokenToFly authToken flyPort) 305 306 307 308 -- subscription 309 310 311 listensForTokenResponse : Property 312 listensForTokenResponse setup = 313 test (setupDesc setup ++ ", listens for token response") <| 314 steps setup 315 >> Tuple.first 316 >> Application.subscriptions 317 >> Common.contains Subscription.OnTokenSentToFly 318 319 320 321 -- card 322 323 324 cardProperties : List Property 325 cardProperties = 326 [ cardBackground 327 , cardSize 328 , cardPosition 329 , cardLayout 330 , cardStyle 331 ] 332 333 334 cardBackground : Property 335 cardBackground = 336 property successCard "card has dark grey background" <| 337 Query.has [ style "background-color" darkGrey ] 338 339 340 cardSize : Property 341 cardSize = 342 property successCard "is 330px wide with 30px padding" <| 343 Query.has [ style "padding" "30px", style "width" "330px" ] 344 345 346 cardPosition : Property 347 cardPosition = 348 property successCard "is centered 50px from the top of the document" <| 349 Query.has [ style "margin" "50px auto" ] 350 351 352 cardLayout : Property 353 cardLayout = 354 property successCard "lays out contents vertically and center aligned" <| 355 Query.has 356 [ style "display" "flex" 357 , style "flex-direction" "column" 358 , style "align-items" "center" 359 , style "text-align" "center" 360 ] 361 362 363 cardStyle : Property 364 cardStyle = 365 property successCard "has light font" <| 366 Query.has [ style "font-weight" Views.Styles.fontWeightLight ] 367 368 369 370 -- title 371 372 373 titleText : Property 374 titleText = 375 property title "has success text" <| 376 Query.has [ text "login successful!" ] 377 378 379 titleStyle : Property 380 titleStyle = 381 property title "has 18px font" <| 382 Query.has 383 [ style "font-size" "18px" ] 384 385 386 titleProperties : List Property 387 titleProperties = 388 [ titleText 389 , titleStyle 390 ] 391 392 393 394 -- body 395 396 397 bodyPendingText : Property 398 bodyPendingText = 399 property body "has pending text" <| 400 Query.has [ text "sending token to fly..." ] 401 402 403 bodyNoButton : Property 404 bodyNoButton = 405 property body "has no 'copy token' button" <| 406 Query.hasNot [ id "copy-token" ] 407 408 409 bodyStyle : Property 410 bodyStyle = 411 property body "has 14px font" <| 412 Query.has [ style "font-size" "14px" ] 413 414 415 bodyPosition : Property 416 bodyPosition = 417 property body "has 10px margin above and below" <| 418 Query.has [ style "margin" "10px 0" ] 419 420 421 bodyLayout : Property 422 bodyLayout = 423 property body "lays out contents vertically, centering horizontally" <| 424 Query.has 425 [ style "display" "flex" 426 , style "flex-direction" "column" 427 , style "align-items" "center" 428 ] 429 430 431 bodyParagraphPositions : Property 432 bodyParagraphPositions = 433 property body "paragraphs have 5px margin above and below" <| 434 Query.findAll [ tag "p" ] 435 >> Query.each (Query.has [ style "margin" "5px 0" ]) 436 437 438 439 -- body on any type of failure 440 441 442 copyLinkInput : Property 443 copyLinkInput = 444 property body "label gives instructions for manual copying" <| 445 Query.children [] 446 >> Expect.all 447 [ Query.index 1 448 >> Query.has 449 [ text "copy token here" ] 450 , Query.index 1 451 >> Query.children [ tag "input" ] 452 >> Query.first 453 >> Query.has 454 [ attribute <| Attr.value authToken 455 , style "white-space" "nowrap" 456 , style "overflow" "hidden" 457 , style "text-overflow" "ellipsis" 458 ] 459 ] 460 461 462 463 -- body on invalid fly port 464 465 466 secondParagraphErrorText : Property 467 secondParagraphErrorText = 468 property secondParagraph "error message describes invalid fly port" <| 469 Query.children [] 470 >> Expect.all 471 [ Query.count (Expect.equal 3) 472 , Query.index 0 473 >> Query.has 474 [ text "could not find a valid fly port to send to." ] 475 , Query.index 2 476 >> Query.has 477 [ text "maybe your URL is broken?" ] 478 ] 479 480 481 482 -- body on browser blocking token from sending 483 484 485 firstParagraphBlockedText : Property 486 firstParagraphBlockedText = 487 property firstParagraph 488 "explains that your browser blocked the token from sending" 489 <| 490 Query.children [] 491 >> Expect.all 492 [ Query.count (Expect.equal 5) 493 , Query.index 0 494 >> Query.has 495 [ text "however, your token could not be sent" ] 496 , Query.index 2 497 >> Query.has 498 [ text "to fly because your browser blocked" ] 499 , Query.index 4 500 >> Query.has 501 [ text "the attempt." ] 502 ] 503 504 505 secondParagraphBlockedText : Property 506 secondParagraphBlockedText = 507 property secondParagraph "describes copy-pasting option" <| 508 Query.children [] 509 >> Expect.all 510 [ Query.count (Expect.equal 7) 511 , Query.index 0 512 >> Query.has 513 [ text "if that fails, you will need to copy" ] 514 , Query.index 2 515 >> Query.has 516 [ text "the token to your clipboard, return" ] 517 , Query.index 4 518 >> Query.has 519 [ text "to fly, and paste your token into" ] 520 , Query.index 6 521 >> Query.has 522 [ text "the prompt." ] 523 ] 524 525 526 527 -- body on successfully sending token 528 529 530 firstParagraphSuccessText : Property 531 firstParagraphSuccessText = 532 property firstParagraph "says 'your token has been transferred to fly'" <| 533 Query.has [ text "your token has been transferred to fly." ] 534 535 536 secondParagraphSuccessText : Property 537 secondParagraphSuccessText = 538 property secondParagraph "says 'you may now close this window'" <| 539 Query.has [ text "you may now close this window." ] 540 541 542 543 -- body on failing to send token 544 545 546 firstParagraphFailureText : Property 547 firstParagraphFailureText = 548 property firstParagraph 549 "says 'however, your token could not be sent to fly.'" 550 <| 551 Query.children [] 552 >> Expect.all 553 [ Query.count (Expect.equal 3) 554 , Query.index 0 555 >> Query.has 556 [ text "however, your token could not be" ] 557 , Query.index 2 >> Query.has [ text "sent to fly." ] 558 ] 559 560 561 pasteInstructions : Query -> Property 562 pasteInstructions query = 563 property query 564 ("says 'after copying, return to fly and paste your token " 565 ++ "into the prompt.'" 566 ) 567 <| 568 Query.children [] 569 >> Expect.all 570 [ Query.count (Expect.equal 3) 571 , Query.index 0 572 >> Query.has 573 [ text "after copying, return to fly and paste" ] 574 , Query.index 2 575 >> Query.has 576 [ text "your token into the prompt." ] 577 ] 578 579 580 secondParagraphFailureText : Property 581 secondParagraphFailureText = 582 property secondParagraph 583 ("says 'after copying, return to fly and paste your token " 584 ++ "into the prompt.'" 585 ) 586 <| 587 Query.children [] 588 >> Expect.all 589 [ Query.count (Expect.equal 3) 590 , Query.index 0 591 >> Query.has 592 [ text "after copying, return to fly and paste" ] 593 , Query.index 2 594 >> Query.has 595 [ text "your token into the prompt." ] 596 ] 597 598 599 600 -- button 601 602 603 copyTokenButtonStyleUnclicked : Property 604 copyTokenButtonStyleUnclicked = 605 property copyTokenButton "display inline and has almost-white border" <| 606 Query.has 607 [ tag "span" 608 , style "border" <| "1px solid " ++ almostWhite 609 ] 610 611 612 sendTokenButtonStyle : Property 613 sendTokenButtonStyle = 614 property sendTokenButton "display inline and has almost-white border" <| 615 Query.has 616 [ tag "a" 617 , style "border" <| "1px solid " ++ almostWhite 618 ] 619 620 621 buttonStyleClicked : Property 622 buttonStyleClicked = 623 property copyTokenButton "has blue border and background" <| 624 Query.has 625 [ style "background-color" blue 626 , style "border" <| "1px solid " ++ blue 627 ] 628 629 630 buttonSize : Property 631 buttonSize = 632 property copyTokenButton "is 212px wide with 10px padding above and below" <| 633 Query.has 634 [ style "width" "212px" 635 , style "padding" "10px 0" 636 ] 637 638 639 buttonPosition : Property 640 buttonPosition = 641 property copyTokenButton "has 15px margin above and below" <| 642 Query.has [ style "margin" "15px 0" ] 643 644 645 buttonLayout : Property 646 buttonLayout = 647 property copyTokenButton "lays out contents horizontally, centering" <| 648 Query.has 649 [ style "display" "flex" 650 , style "justify-content" "center" 651 , style "align-items" "center" 652 ] 653 654 655 sendTokenButtonText : Property 656 sendTokenButtonText = 657 property sendTokenButton "says 'send token to fly directly'" <| 658 Query.has [ text "send token to fly directly" ] 659 660 661 copyTokenButtonTextPrompt : Property 662 copyTokenButtonTextPrompt = 663 property copyTokenButton "says 'copy token to clipboard'" <| 664 Query.has [ text "copy token to clipboard" ] 665 666 667 copyTokenButtonTextClicked : Property 668 copyTokenButtonTextClicked = 669 property copyTokenButton "says 'token copied'" <| 670 Query.has [ text "token copied" ] 671 672 673 buttonCursorUnclicked : Property 674 buttonCursorUnclicked = 675 property copyTokenButton "has pointer cursor" <| 676 Query.has [ style "cursor" "pointer" ] 677 678 679 buttonCursorClicked : Property 680 buttonCursorClicked = 681 property copyTokenButton "has default cursor" <| 682 Query.has [ style "cursor" "default" ] 683 684 685 buttonClipboardAttr : Property 686 buttonClipboardAttr = 687 property copyTokenButton "has attribute that is readable by clipboard.js" <| 688 Query.has 689 [ attribute <| 690 Attr.attribute 691 "data-clipboard-text" 692 authToken 693 ] 694 695 696 copyTokenButtonClickHandler : Property 697 copyTokenButtonClickHandler = 698 property copyTokenButton "sends CopyToken on click" <| 699 Event.simulate Event.click 700 >> Event.expect 701 (Msgs.Update <| 702 Message.Message.Click Message.Message.CopyTokenButton 703 ) 704 705 706 sendTokenButtonClickHandler : Property 707 sendTokenButtonClickHandler = 708 property sendTokenButton "is a link to fly" <| 709 Query.has 710 [ attribute <| 711 Attr.href <| 712 "http://127.0.0.1:1234/?token=" 713 ++ authToken 714 ] 715 716 717 718 -- icon 719 720 721 iconStyle : Property 722 iconStyle = 723 property copyTokenButtonIcon "has clipboard icon" <| 724 Query.has <| 725 iconSelector { size = "20px", image = Assets.ClippyIcon } 726 727 728 iconPosition : Property 729 iconPosition = 730 property copyTokenButtonIcon "has margin on the right" <| 731 Query.has [ style "margin-right" "5px" ] 732 733 734 735 -- TESTS 736 737 738 tests : List Setup -> List Property -> List Test 739 tests setups properties = 740 setups 741 |> List.concatMap 742 (\setup -> List.map ((|>) setup) properties) 743 744 745 cardTests : List Test 746 cardTests = 747 tests allCases cardProperties 748 749 750 titleTests : List Test 751 titleTests = 752 tests allCases titleProperties 753 754 755 all : Test 756 all = 757 describe "Fly login success page" 758 [ describe "page load" 759 [ whenOnFlySuccessPage |> listensForTokenResponse ] 760 , describe "card" cardTests 761 , describe "title" titleTests 762 , describe "token sending" 763 [ whenOnFlySuccessPage |> sendsToken 764 , whenOnNoopPage |> doesNotSendToken 765 ] 766 , describe "body" 767 [ describe "style" <| 768 tests allCases 769 [ bodyStyle 770 , bodyPosition 771 , bodyLayout 772 , bodyParagraphPositions 773 ] 774 , invalidFlyPort |> firstParagraphFailureText 775 , invalidFlyPort |> secondParagraphErrorText 776 , invalidFlyPort |> copyLinkInput 777 , tokenSendBlocked |> firstParagraphBlockedText 778 , tokenSendBlocked |> secondParagraphBlockedText 779 , tokenSendBlocked |> copyLinkInput 780 , tokenSendFailed |> firstParagraphFailureText 781 , tokenSendFailed |> secondParagraphFailureText 782 , tokenSendFailed |> copyLinkInput 783 , tokenCopied |> firstParagraphFailureText 784 , tokenCopied |> secondParagraphFailureText 785 , whenOnFlySuccessPage |> bodyPendingText 786 , whenOnFlySuccessPage |> bodyNoButton 787 , tokenSendSuccess |> firstParagraphSuccessText 788 , tokenSendSuccess |> secondParagraphSuccessText 789 ] 790 , describe "copy token input" 791 [ defineHoverBehaviour 792 { name = "copy token input" 793 , setup = steps tokenSendFailed () |> Tuple.first 794 , query = copyTokenInput 795 , unhoveredSelector = 796 { description = 797 "same background as card" 798 , selector = [ style "background-color" darkGrey ] 799 } 800 , hoverable = 801 Message.Message.CopyTokenInput 802 , hoveredSelector = 803 { description = "darker background" 804 , selector = 805 [ style "background-color" darkerGrey ] 806 } 807 } 808 ] 809 , describe "copy token button" 810 [ describe "always" <| 811 tests [ tokenSendFailed, tokenCopied, tokenSendBlocked ] 812 [ buttonSize 813 , buttonPosition 814 , buttonLayout 815 , buttonClipboardAttr 816 ] 817 , describe "when token sending failed" <| 818 tests [ tokenSendFailed ] 819 [ copyTokenButtonStyleUnclicked 820 , copyTokenButtonClickHandler 821 , copyTokenButtonTextPrompt 822 , iconStyle 823 , iconPosition 824 , buttonCursorUnclicked 825 ] 826 , describe "after copying token" <| 827 tests [ tokenCopied ] 828 [ buttonStyleClicked 829 , copyTokenButtonTextClicked 830 , iconStyle 831 , iconPosition 832 , buttonCursorClicked 833 ] 834 , defineHoverBehaviour 835 { name = "copy token button" 836 , setup = steps tokenSendFailed () |> Tuple.first 837 , query = copyTokenButton 838 , unhoveredSelector = 839 { description = 840 "same background as card" 841 , selector = [ style "background-color" darkGrey ] 842 } 843 , hoverable = 844 Message.Message.CopyTokenButton 845 , hoveredSelector = 846 { description = "darker background" 847 , selector = 848 [ style "background-color" darkerGrey ] 849 } 850 } 851 ] 852 , describe "send token button" 853 [ tokenSendBlocked |> sendTokenButtonStyle 854 , tokenSendBlocked |> sendTokenButtonText 855 , tokenSendBlocked |> sendTokenButtonClickHandler 856 , defineHoverBehaviour 857 { name = "send token button" 858 , setup = steps tokenSendBlocked () |> Tuple.first 859 , query = sendTokenButton 860 , unhoveredSelector = 861 { description = 862 "same background as card" 863 , selector = [ style "background-color" darkGrey ] 864 } 865 , hoverable = 866 Message.Message.SendTokenButton 867 , hoveredSelector = 868 { description = "darker background" 869 , selector = 870 [ style "background-color" darkerGrey ] 871 } 872 } 873 ] 874 ]