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          ]