From 8a3e76d89c753eab00bc1329c2c7ec65f2e0f97d Mon Sep 17 00:00:00 2001 From: Marco Perone Date: Wed, 30 Mar 2022 16:55:29 +0200 Subject: [PATCH 01/19] add some ids to identify elements --- elm/src/Anonymous.elm | 4 ++-- elm/src/Component.elm | 8 ++++++-- elm/src/Credentials.elm | 5 +++-- elm/src/Logged.elm | 2 ++ elm/src/Main.elm | 4 ++++ 5 files changed, 17 insertions(+), 6 deletions(-) diff --git a/elm/src/Anonymous.elm b/elm/src/Anonymous.elm index 5e4ff23..4e5ead2 100644 --- a/elm/src/Anonymous.elm +++ b/elm/src/Anonymous.elm @@ -64,8 +64,8 @@ update msg model = view : Model -> Element Msg view model = Component.mainRow - [ Credentials.view "Register User" RegisterData Register model.register - , Credentials.view "Login" LoginData Login model.login + [ Credentials.view "register" "Register User" RegisterData Register model.register + , Credentials.view "login" "Login" LoginData Login model.login ] -- HTTP diff --git a/elm/src/Component.elm b/elm/src/Component.elm index 879a6c1..975e9cf 100644 --- a/elm/src/Component.elm +++ b/elm/src/Component.elm @@ -2,6 +2,9 @@ module Component exposing (..) import Style exposing (..) +-- elm/html +import Html.Attributes exposing (id) + -- mdgriffith/elm-ui import Element exposing (..) import Element.Background exposing (..) @@ -12,12 +15,13 @@ import Element.Font mainRow : List ( Element msg ) -> Element msg mainRow elements = row [ Element.width fill ] elements -mainColumn : List ( Element msg ) -> Element msg -mainColumn elements = column +mainColumn : String -> List ( Element msg ) -> Element msg +mainColumn identifier elements = column [ normalPadding , bigSpacing , Element.width fill , alignTop + , htmlAttribute ( id identifier ) ] elements diff --git a/elm/src/Credentials.elm b/elm/src/Credentials.elm index f61c1a1..25362f1 100644 --- a/elm/src/Credentials.elm +++ b/elm/src/Credentials.elm @@ -60,8 +60,9 @@ updateSubmit decoder url credentials submitMessage model = -- VIEW -view : String -> (CredentialsMessage -> msg) -> (SubmitMessage a -> msg) -> Model -> Element msg -view title liftModel liftMessage credentials = Component.mainColumn +view : String -> String -> (CredentialsMessage -> msg) -> (SubmitMessage a -> msg) -> Model -> Element msg +view identifier title liftModel liftMessage credentials = Component.mainColumn + identifier [ Component.columnTitle title , column [ normalSpacing diff --git a/elm/src/Logged.elm b/elm/src/Logged.elm index 60ba5fc..374bd0b 100644 --- a/elm/src/Logged.elm +++ b/elm/src/Logged.elm @@ -73,6 +73,7 @@ viewTag tag = Element.el view : Model -> Element Msg view model = Component.mainRow [ Component.mainColumn + "contents" [ Component.columnTitle "Contents" , Element.map NewFilter ( Tags.view viewTag "Filter by tag" "Add filter" model.filters ) , Element.table @@ -95,6 +96,7 @@ view model = Component.mainRow } ] , Component.mainColumn + "add-content" [ Component.columnTitle "Add content" , Element.Input.text [] { onChange = NewContent diff --git a/elm/src/Main.elm b/elm/src/Main.elm index 9cab958..5c7bdad 100644 --- a/elm/src/Main.elm +++ b/elm/src/Main.elm @@ -13,6 +13,9 @@ import Browser exposing (..) import Set exposing (..) import Tuple exposing (mapBoth) +-- elm/html +import Html.Attributes exposing (id) + -- mdgriffith/elm-ui import Element exposing (..) @@ -68,6 +71,7 @@ view model = Element.column [ titleFont , bigPadding , centerX + , htmlAttribute ( id "title" ) ] ( Element.text "Tagger" ) , case model of From ab4a5bb66a42b59054f13cbca378972fd4ade517 Mon Sep 17 00:00:00 2001 From: Marco Perone Date: Wed, 30 Mar 2022 16:55:47 +0200 Subject: [PATCH 02/19] first sketch of specification --- elm/spec/Tagger.spec.purs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 elm/spec/Tagger.spec.purs diff --git a/elm/spec/Tagger.spec.purs b/elm/spec/Tagger.spec.purs new file mode 100644 index 0000000..c831b0d --- /dev/null +++ b/elm/spec/Tagger.spec.purs @@ -0,0 +1,34 @@ +module Tagger where + +import Quickstrom + +readyWhen :: Selector +readyWhen = "#title" + +register :: String -> String -> ProbabilisticAction +register username password = + focus "#register input[autocomplete=\"username\"]" + `followedBy` enterText username + `followedBy` focus "#register input[autocomplete=\"new-password\"]" + `followedBy` enterText password + `followedBy` click "#register div[role=\"button\"]" + +login :: String -> String -> ProbabilisticAction +login username password = + focus "#login input[autocomplete=\"username\"]" + `followedBy` enterText username + `followedBy` focus "#login input[autocomplete=\"new-password\"]" + `followedBy` enterText password + `followedBy` click "#login div[role=\"button\"]" + +actions :: Actions +actions = + [ register "username" "password" + , register "otheruser" "otherpassword" + , login "username" "password" + , login "username" "wrongpassword" + , login "nonexistinguser" "password" + ] + +proposition :: Boolean +proposition = true From 214490c181267da9086bb8bc8f0093c6aefd5dce Mon Sep 17 00:00:00 2001 From: Marco Perone Date: Wed, 30 Mar 2022 17:12:21 +0200 Subject: [PATCH 03/19] submit should clean form data --- elm/src/Anonymous.elm | 8 ++++---- elm/src/Credentials.elm | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/elm/src/Anonymous.elm b/elm/src/Anonymous.elm index 4e5ead2..f2da021 100644 --- a/elm/src/Anonymous.elm +++ b/elm/src/Anonymous.elm @@ -40,11 +40,11 @@ type Msg | Register (SubmitMessage UserId) | Login (SubmitMessage Token) -updateModelWithRegisterSubmit : Model -> Submit UserId -> Model -updateModelWithRegisterSubmit model registerSubmit = { model | registerSubmit = registerSubmit } +updateModelWithRegisterSubmit : Model -> { model : Credentials.Model, submitState : Submit UserId } -> Model +updateModelWithRegisterSubmit model data = { model | registerSubmit = data.submitState, register = data.model } -updateModelWithLoginSubmit : Model -> Submit Token -> Model -updateModelWithLoginSubmit model loginSubmit = { model | loginSubmit = loginSubmit} +updateModelWithLoginSubmit : Model -> { model : Credentials.Model, submitState : Submit Token } -> Model +updateModelWithLoginSubmit model data = { model | loginSubmit = data.submitState, login = data.model } update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = diff --git a/elm/src/Credentials.elm b/elm/src/Credentials.elm index 25362f1..754c018 100644 --- a/elm/src/Credentials.elm +++ b/elm/src/Credentials.elm @@ -51,12 +51,12 @@ type SubmitMessage a | Failed Http.Error | Succeeded a -updateSubmit : Decoder a -> String -> Model -> SubmitMessage a -> Submit a -> ( Submit a, Cmd (SubmitMessage a) ) -updateSubmit decoder url credentials submitMessage model = +updateSubmit : Decoder a -> String -> Model -> SubmitMessage a -> Submit a -> ( { model : Model, submitState : Submit a }, Cmd (SubmitMessage a) ) +updateSubmit decoder url credentials submitMessage submitState = case submitMessage of - Submit -> ( model, submit decoder url credentials ) - Failed error -> ( Failure error, Cmd.none ) - Succeeded value -> ( Successful value, Cmd.none ) + Submit -> ( { model = emptyCredentials, submitState = submitState }, submit decoder url credentials ) + Failed error -> ( { model = credentials , submitState = Failure error }, Cmd.none ) + Succeeded value -> ( { model = credentials , submitState = Successful value }, Cmd.none ) -- VIEW From 85fd45c5725263e9c95e118adac32bd75e0b6436 Mon Sep 17 00:00:00 2001 From: Marco Perone Date: Wed, 6 Apr 2022 15:17:44 +0200 Subject: [PATCH 04/19] provide instructions on how to run Quickstrom --- elm/README.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/elm/README.md b/elm/README.md index 620afcd..ca5e785 100644 --- a/elm/README.md +++ b/elm/README.md @@ -20,3 +20,34 @@ In the private area, you'll see the contents for the logged in user and you can - add new contents with their tags; - filter the shown contents by tag. + +## Specification + +The `spec` folder contains some end-to-end acceptance tests written using [Quickstrom](https://quickstrom.io/). + +To run them, you could execute the following commands inside the `spec` folder, given your application is exposed on `localhost:8000`: + +``` +docker run --rm -d \ + --name webdriver \ + --network=host \ + -v /dev/shm:/dev/shm \ + -v $PWD:/spec \ + selenium/standalone-chrome:3.141.59-20200826 + +docker run --rm \ + --network=host \ + -v $PWD:/spec \ + quickstrom/quickstrom \ + quickstrom check \ + --webdriver-host=webdriver \ + --webdriver-path=/wd/hub \ + --browser=chrome \ + --reporter=html \ + --html-report-directory=/spec/report \ + --tests=10 \ + /spec/Tagger.spec.purs \ + http://localhost:8000 +``` + +Then in the `spec/report` folder you'll find an `index.html` file containing a report of each test which was executed. From d045325ddf24f54eff5a72a95c87b29705003af9 Mon Sep 17 00:00:00 2001 From: Marco Perone Date: Wed, 6 Apr 2022 15:18:26 +0200 Subject: [PATCH 05/19] ignore Quickstrom report --- elm/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/elm/.gitignore b/elm/.gitignore index e453cba..1dde36a 100644 --- a/elm/.gitignore +++ b/elm/.gitignore @@ -1,2 +1,3 @@ elm-stuff +spec/report index.html From af07461aa8008201ef6611460fa60555defa94d4 Mon Sep 17 00:00:00 2001 From: Marco Perone Date: Wed, 6 Apr 2022 16:49:00 +0200 Subject: [PATCH 06/19] add more ids and classes to html --- elm/src/Anonymous.elm | 1 + elm/src/Component.elm | 11 ++++++++--- elm/src/Credentials.elm | 9 +++++++-- elm/src/Logged.elm | 12 +++++++++--- elm/src/Tags.elm | 22 +++++++++++++++------- 5 files changed, 40 insertions(+), 15 deletions(-) diff --git a/elm/src/Anonymous.elm b/elm/src/Anonymous.elm index f2da021..2f75940 100644 --- a/elm/src/Anonymous.elm +++ b/elm/src/Anonymous.elm @@ -64,6 +64,7 @@ update msg model = view : Model -> Element Msg view model = Component.mainRow + "anonymous" [ Credentials.view "register" "Register User" RegisterData Register model.register , Credentials.view "login" "Login" LoginData Login model.login ] diff --git a/elm/src/Component.elm b/elm/src/Component.elm index 975e9cf..e3a0187 100644 --- a/elm/src/Component.elm +++ b/elm/src/Component.elm @@ -3,7 +3,7 @@ module Component exposing (..) import Style exposing (..) -- elm/html -import Html.Attributes exposing (id) +import Html.Attributes exposing (class, id) -- mdgriffith/elm-ui import Element exposing (..) @@ -12,8 +12,12 @@ import Element.Border exposing (..) import Element.Input exposing (..) import Element.Font -mainRow : List ( Element msg ) -> Element msg -mainRow elements = row [ Element.width fill ] elements +mainRow : String -> List ( Element msg ) -> Element msg +mainRow identifier elements = row + [ Element.width fill + , htmlAttribute ( id identifier ) + ] + elements mainColumn : String -> List ( Element msg ) -> Element msg mainColumn identifier elements = column @@ -32,6 +36,7 @@ button : msg -> String -> Element msg button message label = Element.Input.button ( [ Element.padding 5 , Element.focused [ Element.Background.color purple ] + , htmlAttribute ( class "button" ) ] ++ buttonStyle ) { onPress = Just message , label = Element.text label diff --git a/elm/src/Credentials.elm b/elm/src/Credentials.elm index 754c018..3c66be4 100644 --- a/elm/src/Credentials.elm +++ b/elm/src/Credentials.elm @@ -3,6 +3,9 @@ module Credentials exposing (..) import Component exposing (..) import Style exposing (..) +-- elm/html +import Html.Attributes exposing (class) + -- elm/http import Http exposing (..) @@ -71,13 +74,15 @@ view identifier title liftModel liftMessage credentials = Component.mainColumn [ Element.map liftModel ( column [ normalSpacing ] - [ Element.Input.username [] + [ Element.Input.username + [ htmlAttribute ( class "username" ) ] { onChange = Username , text = credentials.username , placeholder = Just ( Element.Input.placeholder [] ( Element.text "Username" ) ) , label = labelAbove [] ( Element.text "Username" ) } - , Element.Input.newPassword [] + , Element.Input.newPassword + [ htmlAttribute ( class "password" ) ] { onChange = Password , text = credentials.password , placeholder = Just ( Element.Input.placeholder [] ( Element.text "Password" ) ) diff --git a/elm/src/Logged.elm b/elm/src/Logged.elm index 374bd0b..aab2eb7 100644 --- a/elm/src/Logged.elm +++ b/elm/src/Logged.elm @@ -9,6 +9,9 @@ import Tags exposing (..) -- elm/core import Set exposing (..) +-- elm/html +import Html.Attributes exposing (id) + -- elm/http import Http exposing (..) @@ -72,12 +75,14 @@ viewTag tag = Element.el view : Model -> Element Msg view model = Component.mainRow + "logged" [ Component.mainColumn "contents" [ Component.columnTitle "Contents" - , Element.map NewFilter ( Tags.view viewTag "Filter by tag" "Add filter" model.filters ) + , Element.map NewFilter ( Tags.view viewTag "Filter by tag" "Add filter" "filter-by-tag" model.filters ) , Element.table [ normalPadding + , htmlAttribute (id "contents-table") ] { data = model.contents , columns = @@ -98,13 +103,14 @@ view model = Component.mainRow , Component.mainColumn "add-content" [ Component.columnTitle "Add content" - , Element.Input.text [] + , Element.Input.text + [ htmlAttribute (id "new-content") ] { onChange = NewContent , text = model.newContent , placeholder = Just ( Element.Input.placeholder [] ( Element.text "New content" ) ) , label = labelAbove [] ( Element.text "New content" ) } - , Element.map NewTag ( Tags.view viewTag "New tag" "Add tag" model.newTags ) + , Element.map NewTag ( Tags.view viewTag "New tag" "Add tag" "new-tag" model.newTags ) , Component.button SubmitContent "Add content" ] ] diff --git a/elm/src/Tags.elm b/elm/src/Tags.elm index ec0d028..eee0327 100644 --- a/elm/src/Tags.elm +++ b/elm/src/Tags.elm @@ -7,6 +7,9 @@ import Style exposing (..) -- elm/core import Set exposing (..) +-- elm/html +import Html.Attributes exposing (class, id) + -- mdgriffith/elm-ui import Element exposing (..) import Element.Background exposing (..) @@ -48,22 +51,27 @@ update onSubmit msg model = case msg of -- VIEW -removable : String -> Element Msg -> Element Msg -removable id element = row - [ normalSpacing ] +removable : String -> String -> Element Msg -> Element Msg +removable identifier id element = row + [ normalSpacing + , htmlAttribute ( class identifier ) ] [ element , Element.el - ( ( onClick ( Remove id ) ) :: buttonStyle ) + ( [ onClick ( Remove id ) + , htmlAttribute ( class "remove" ) + ] + ++ buttonStyle ) ( Element.text "x" ) ] viewRemovableTag : ( Tag -> Element Msg ) -> Tag -> Element Msg -viewRemovableTag viewTag tag = removable tag ( viewTag tag ) +viewRemovableTag viewTag tag = removable "tag" tag ( viewTag tag ) -view : ( Tag -> Element Msg ) -> String -> String -> Model -> Element Msg -view viewTag label submitText model = column +view : ( Tag -> Element Msg ) -> String -> String -> String -> Model -> Element Msg +view viewTag label submitText identifier model = column [ normalSpacing , Element.centerX + , htmlAttribute (id identifier) ] [ Element.el [] ( Element.Input.text [] { onChange = NewTag From 70ec8ba4df052d60edbee46db3a5b57f413688e9 Mon Sep 17 00:00:00 2001 From: Marco Perone Date: Wed, 6 Apr 2022 16:49:24 +0200 Subject: [PATCH 07/19] improve selectors and add actions for specification --- elm/spec/Tagger.spec.purs | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/elm/spec/Tagger.spec.purs b/elm/spec/Tagger.spec.purs index c831b0d..3a88866 100644 --- a/elm/spec/Tagger.spec.purs +++ b/elm/spec/Tagger.spec.purs @@ -7,19 +7,28 @@ readyWhen = "#title" register :: String -> String -> ProbabilisticAction register username password = - focus "#register input[autocomplete=\"username\"]" + focus "#anonymous #register input.username" `followedBy` enterText username - `followedBy` focus "#register input[autocomplete=\"new-password\"]" + `followedBy` focus "#anonymous #register input.password" `followedBy` enterText password - `followedBy` click "#register div[role=\"button\"]" + `followedBy` click "#anonymous #register div.button" login :: String -> String -> ProbabilisticAction login username password = - focus "#login input[autocomplete=\"username\"]" + focus "#anonymous #login input.username" `followedBy` enterText username - `followedBy` focus "#login input[autocomplete=\"new-password\"]" + `followedBy` focus "#anonymous #login input.password" `followedBy` enterText password - `followedBy` click "#login div[role=\"button\"]" + `followedBy` click "#anonymous #login div.button" + +filterByTag :: String -> ProbabilisticAction +filterByTag tag = + focus "#logged #filter-by-tag input" + `followedBy` enterText tag + `followedBy` click "#logged #filter-by-tag .button" + +removeTag :: ProbabilisticAction +removeTag = click "#logged .tag .remove" actions :: Actions actions = @@ -28,6 +37,10 @@ actions = , login "username" "password" , login "username" "wrongpassword" , login "nonexistinguser" "password" + , filterByTag "tag1" + , filterByTag "tag2" + , filterByTag "tag3" + , removeTag ] proposition :: Boolean From 3bd560a0a68f5e3b161e05f28bc0dac07efe18d4 Mon Sep 17 00:00:00 2001 From: Marco Perone Date: Fri, 8 Apr 2022 10:56:17 +0200 Subject: [PATCH 08/19] resent content after addition --- elm/src/Logged.elm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/elm/src/Logged.elm b/elm/src/Logged.elm index aab2eb7..c92df56 100644 --- a/elm/src/Logged.elm +++ b/elm/src/Logged.elm @@ -60,7 +60,7 @@ update msg model = case msg of NewContent newContent -> ( { model | newContent = newContent }, Cmd.none ) NewFilter filterMsg -> Tuple.mapFirst ( \filters -> { model | filters = filters } ) ( Tags.update ( retrieveContents model.token ) filterMsg model.filters ) NewTag tagMsg -> Tuple.mapFirst ( \newTags -> { model | newTags = newTags } ) ( Tags.update ( always ( Cmd.none ) ) tagMsg model.newTags ) - SubmitContent -> ( model, addContent model.token ( Content model.newContent model.newTags.tags ) ) + SubmitContent -> ( { model | newContent = "" } , addContent model.token ( Content model.newContent model.newTags.tags ) ) SubmitSuccessful content -> ( { model | contents = content :: model.contents }, Cmd.none ) SubmitFailed -> ( model, Cmd.none ) From 4082b380179158a8a4ecfd9095f5a28222bf3fac Mon Sep 17 00:00:00 2001 From: Marco Perone Date: Fri, 8 Apr 2022 10:56:32 +0200 Subject: [PATCH 09/19] add actions to add tags and contents --- elm/spec/Tagger.spec.purs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/elm/spec/Tagger.spec.purs b/elm/spec/Tagger.spec.purs index 3a88866..fc54efb 100644 --- a/elm/spec/Tagger.spec.purs +++ b/elm/spec/Tagger.spec.purs @@ -30,6 +30,18 @@ filterByTag tag = removeTag :: ProbabilisticAction removeTag = click "#logged .tag .remove" +addNewTag :: String -> ProbabilisticAction +addNewTag tag = + focus "#logged #new-tag input" + `followedBy` enterText tag + `followedBy` click "#logged #new-tag .button" + +addNewContent :: String -> ProbabilisticAction +addNewContent content = + focus "#logged input#new-content" + `followedBy` enterText content + `followedBy` click "#logged #add-content > .button" + actions :: Actions actions = [ register "username" "password" @@ -41,6 +53,12 @@ actions = , filterByTag "tag2" , filterByTag "tag3" , removeTag + , addNewTag "tag1" + , addNewTag "tag2" + , addNewTag "tag3" + , addNewContent "content1" + , addNewContent "content2" + , addNewContent "content3" ] proposition :: Boolean From b91f3604e1fbbdb3be18314d9848452517989d80 Mon Sep 17 00:00:00 2001 From: Marco Perone Date: Wed, 13 Apr 2022 17:14:31 +0200 Subject: [PATCH 10/19] add row numbers for contents table --- elm/src/Logged.elm | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/elm/src/Logged.elm b/elm/src/Logged.elm index c92df56..d7060dc 100644 --- a/elm/src/Logged.elm +++ b/elm/src/Logged.elm @@ -8,9 +8,10 @@ import Tags exposing (..) -- elm/core import Set exposing (..) +import String exposing (fromInt) -- elm/html -import Html.Attributes exposing (id) +import Html.Attributes exposing (attribute, class, id) -- elm/http import Http exposing (..) @@ -70,6 +71,7 @@ viewTag : Tag -> Element msg viewTag tag = Element.el [ normalPadding , normalSpacing + , htmlAttribute ( class "tag" ) ] ( Element.text tag ) @@ -80,7 +82,7 @@ view model = Component.mainRow "contents" [ Component.columnTitle "Contents" , Element.map NewFilter ( Tags.view viewTag "Filter by tag" "Add filter" "filter-by-tag" model.filters ) - , Element.table + , Element.indexedTable [ normalPadding , htmlAttribute (id "contents-table") ] @@ -88,14 +90,14 @@ view model = Component.mainRow , columns = [ { header = tableHeader "Content" , width = fill - , view = \content -> Element.el - ( normalPadding :: tableRowStyle ) + , view = \i content -> Element.el + ( normalPadding :: htmlAttribute ( attribute "content-row" ( fromInt i ) ) :: tableRowStyle ) ( Element.text content.message ) } , { header = tableHeader "Tags" , width = fill - , view = \content -> Element.el - tableRowStyle + , view = \i content -> Element.el + ( htmlAttribute ( attribute "tag-row" ( fromInt i ) ) :: tableRowStyle ) ( row [] ( List.map viewTag ( toList content.tags ) ) ) } ] } From e2201dbfcc8683026a7af1c8ecfab26fc22d57c4 Mon Sep 17 00:00:00 2001 From: Marco Perone Date: Wed, 13 Apr 2022 17:41:36 +0200 Subject: [PATCH 11/19] start testing with temporal logic --- elm/spec/Tagger.spec.purs | 53 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/elm/spec/Tagger.spec.purs b/elm/spec/Tagger.spec.purs index fc54efb..09bcde7 100644 --- a/elm/spec/Tagger.spec.purs +++ b/elm/spec/Tagger.spec.purs @@ -1,10 +1,17 @@ module Tagger where +import Data.Maybe +import Data.Symbol + import Quickstrom +-- STARTING POINT + readyWhen :: Selector readyWhen = "#title" +-- ACTIONS + register :: String -> String -> ProbabilisticAction register username password = focus "#anonymous #register input.username" @@ -40,7 +47,9 @@ addNewContent :: String -> ProbabilisticAction addNewContent content = focus "#logged input#new-content" `followedBy` enterText content - `followedBy` click "#logged #add-content > .button" + +submitContent :: ProbabilisticAction +submitContent = click "#logged #add-content > .button" actions :: Actions actions = @@ -59,7 +68,47 @@ actions = , addNewContent "content1" , addNewContent "content2" , addNewContent "content3" + , submitContent ] +-- MODEL + +type Tag = String + +type Content = {content :: String, tags :: Array Tag} + +contentRow :: Attribute "content-row" +contentRow = attribute (SProxy :: SProxy "content-row") + +extractTags :: String -> Array Tag +extractTags i = map _.textContent (queryAll ("#logged #contents-table [tag-row=\" <> i <> \"]") {textContent}) + +extractContents :: Array Content +extractContents = map + (\r -> {content : r.textContent, tags : extractTags (fromMaybe "" r.contentRow)}) + (queryAll "#logged #contents-table [content-row]" {textContent, contentRow}) + +-- INVARIANTS + proposition :: Boolean -proposition = true +proposition = titleIsTagger && isAnonymous && always (remainAmonymous || logIn || remainLogged) + where + remainAmonymous :: Boolean + remainAmonymous = isAnonymous && next isAnonymous + + logIn :: Boolean + logIn = isAnonymous && next isLogged + + remainLogged :: Boolean + remainLogged = isLogged && next isLogged + +titleIsTagger :: Boolean +titleIsTagger = always (title == Just "Tagger") + where + title = map _.textContent (queryOne "#title" {textContent}) + +isAnonymous :: Boolean +isAnonymous = isJust (queryOne "#anonymous" {}) + +isLogged :: Boolean +isLogged = isJust (queryOne "#logged" {}) From 2f3547ba367ecf2e429ab92d46cf31f85a67127f Mon Sep 17 00:00:00 2001 From: Marco Perone Date: Thu, 14 Apr 2022 11:28:29 +0200 Subject: [PATCH 12/19] simplify identification of removable tag --- elm/src/Tags.elm | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/elm/src/Tags.elm b/elm/src/Tags.elm index eee0327..572693f 100644 --- a/elm/src/Tags.elm +++ b/elm/src/Tags.elm @@ -51,10 +51,10 @@ update onSubmit msg model = case msg of -- VIEW -removable : String -> String -> Element Msg -> Element Msg -removable identifier id element = row +removable : String -> Element Msg -> Element Msg +removable id element = row [ normalSpacing - , htmlAttribute ( class identifier ) ] + , htmlAttribute ( class "removable" ) ] [ element , Element.el ( [ onClick ( Remove id ) @@ -65,7 +65,7 @@ removable identifier id element = row ] viewRemovableTag : ( Tag -> Element Msg ) -> Tag -> Element Msg -viewRemovableTag viewTag tag = removable "tag" tag ( viewTag tag ) +viewRemovableTag viewTag tag = removable tag ( viewTag tag ) view : ( Tag -> Element Msg ) -> String -> String -> String -> Model -> Element Msg view viewTag label submitText identifier model = column From 9f25ecf38451c8ed3b627aa2df0f928af6a9ed5b Mon Sep 17 00:00:00 2001 From: Marco Perone Date: Thu, 14 Apr 2022 11:28:53 +0200 Subject: [PATCH 13/19] first complete version of acceptance test specification --- elm/spec/Tagger.spec.purs | 91 +++++++++++++++++++++++++++++++++------ 1 file changed, 78 insertions(+), 13 deletions(-) diff --git a/elm/spec/Tagger.spec.purs b/elm/spec/Tagger.spec.purs index 09bcde7..43f5e1b 100644 --- a/elm/spec/Tagger.spec.purs +++ b/elm/spec/Tagger.spec.purs @@ -1,6 +1,8 @@ module Tagger where +import Data.Array as Array import Data.Maybe +import Data.String.CodeUnits as String import Data.Symbol import Quickstrom @@ -35,7 +37,7 @@ filterByTag tag = `followedBy` click "#logged #filter-by-tag .button" removeTag :: ProbabilisticAction -removeTag = click "#logged .tag .remove" +removeTag = click "#logged .removable .tag .remove" addNewTag :: String -> ProbabilisticAction addNewTag tag = @@ -77,6 +79,8 @@ type Tag = String type Content = {content :: String, tags :: Array Tag} +-- QUERIES + contentRow :: Attribute "content-row" contentRow = attribute (SProxy :: SProxy "content-row") @@ -88,19 +92,29 @@ extractContents = map (\r -> {content : r.textContent, tags : extractTags (fromMaybe "" r.contentRow)}) (queryAll "#logged #contents-table [content-row]" {textContent, contentRow}) --- INVARIANTS +extractFilters :: Array Tag +extractFilters = map _.textContent (queryAll "#logged #contents #filter-by-tag .tag" {textContent}) -proposition :: Boolean -proposition = titleIsTagger && isAnonymous && always (remainAmonymous || logIn || remainLogged) - where - remainAmonymous :: Boolean - remainAmonymous = isAnonymous && next isAnonymous +extractNewContent :: Maybe String +extractNewContent = map _.value (queryOne "#logged #add-content #new-content" {value}) + +extractNewTags :: Array Tag +extractNewTags = map _.textContent (queryAll "#logged #add-content #new-tag .tag" {textContent}) + +-- STATES - logIn :: Boolean - logIn = isAnonymous && next isLogged +anonymous :: Maybe Unit +anonymous = map (const unit) (queryOne "#anonymous" {}) - remainLogged :: Boolean - remainLogged = isLogged && next isLogged +logged :: Maybe {filters :: Array Tag, contents :: Array Content, newContent :: String, newTags :: Array Tag} +logged = queryOne "#logged" {} *> ( + (\filters contents newContent newTags -> {filters : filters, contents : contents, newContent : newContent, newTags : newTags}) + <$> Just extractFilters + <*> Just extractContents + <*> extractNewContent + <*> Just extractNewTags) + +-- ASSERTIONS titleIsTagger :: Boolean titleIsTagger = always (title == Just "Tagger") @@ -108,7 +122,58 @@ titleIsTagger = always (title == Just "Tagger") title = map _.textContent (queryOne "#title" {textContent}) isAnonymous :: Boolean -isAnonymous = isJust (queryOne "#anonymous" {}) +isAnonymous = isJust anonymous isLogged :: Boolean -isLogged = isJust (queryOne "#logged" {}) +isLogged = isJust logged + +-- TRANSITIONS + +remainAmonymous :: Boolean +remainAmonymous = isAnonymous && next isAnonymous + +logIn :: Boolean +logIn = isAnonymous && next isLogged + +addFilter :: Boolean +addFilter + = Array.length extractFilters < next (Array.length extractFilters) + && Array.length extractContents >= next (Array.length extractContents) + && unchanged extractNewContent + && unchanged extractNewTags + +fillNewContent :: Boolean +fillNewContent + = map String.length extractNewContent < next (map String.length extractNewContent) + && unchanged extractFilters + && unchanged extractContents + && unchanged extractNewTags + +addNewContentTag :: Boolean +addNewContentTag + = Array.length extractNewTags < next (Array.length extractNewTags) + && unchanged extractFilters + && unchanged extractContents + && unchanged extractNewContent + +submitNewContent :: Boolean +submitNewContent + = next extractNewContent == Just "" + && next extractNewTags == [] + && next (Array.length extractContents) == Array.length extractContents + 1 + && unchanged extractFilters + +-- INVARIANTS + +proposition :: Boolean +proposition + = titleIsTagger + && isAnonymous + && always + ( remainAmonymous + || logIn + || addFilter + || fillNewContent + || addNewContentTag + || submitNewContent + ) From c9620f1ad7bdcdc2a17afc51ed4f65ab738199b1 Mon Sep 17 00:00:00 2001 From: Marco Perone Date: Thu, 14 Apr 2022 16:20:27 +0200 Subject: [PATCH 14/19] filters and newTags could not increase due to duplicates --- elm/spec/Tagger.spec.purs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/elm/spec/Tagger.spec.purs b/elm/spec/Tagger.spec.purs index 43f5e1b..acbb3d4 100644 --- a/elm/spec/Tagger.spec.purs +++ b/elm/spec/Tagger.spec.purs @@ -137,7 +137,7 @@ logIn = isAnonymous && next isLogged addFilter :: Boolean addFilter - = Array.length extractFilters < next (Array.length extractFilters) + = Array.length extractFilters <= next (Array.length extractFilters) && Array.length extractContents >= next (Array.length extractContents) && unchanged extractNewContent && unchanged extractNewTags @@ -151,7 +151,7 @@ fillNewContent addNewContentTag :: Boolean addNewContentTag - = Array.length extractNewTags < next (Array.length extractNewTags) + = Array.length extractNewTags <= next (Array.length extractNewTags) && unchanged extractFilters && unchanged extractContents && unchanged extractNewContent From 4afd667dbd81532719975f10824fc58a2aeffd76 Mon Sep 17 00:00:00 2001 From: Marco Perone Date: Thu, 14 Apr 2022 16:23:03 +0200 Subject: [PATCH 15/19] avoid showing a new content if it doesn't satisy the filters --- elm/elm.json | 3 ++- elm/spec/Tagger.spec.purs | 2 +- elm/src/Logged.elm | 14 ++++++++++++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/elm/elm.json b/elm/elm.json index 5b051c4..f7388e5 100644 --- a/elm/elm.json +++ b/elm/elm.json @@ -12,7 +12,8 @@ "elm/http": "2.0.0", "elm/json": "1.1.3", "elm/url": "1.0.0", - "mdgriffith/elm-ui": "1.1.8" + "mdgriffith/elm-ui": "1.1.8", + "stoeffel/set-extra": "1.2.3" }, "indirect": { "elm/bytes": "1.0.8", diff --git a/elm/spec/Tagger.spec.purs b/elm/spec/Tagger.spec.purs index acbb3d4..4c83f74 100644 --- a/elm/spec/Tagger.spec.purs +++ b/elm/spec/Tagger.spec.purs @@ -160,7 +160,7 @@ submitNewContent :: Boolean submitNewContent = next extractNewContent == Just "" && next extractNewTags == [] - && next (Array.length extractContents) == Array.length extractContents + 1 + && ((next (Array.length extractContents) == Array.length extractContents + 1) || unchanged (Array.length extractContents)) && unchanged extractFilters -- INVARIANTS diff --git a/elm/src/Logged.elm b/elm/src/Logged.elm index d7060dc..1d91c7e 100644 --- a/elm/src/Logged.elm +++ b/elm/src/Logged.elm @@ -29,6 +29,9 @@ import Element exposing (..) import Element.Border exposing (..) import Element.Input exposing (..) +-- stoeffel/set-extra +import Set.Extra exposing (subset) + -- MODEL type alias Model = @@ -54,6 +57,13 @@ type Msg | SubmitSuccessful Content | SubmitFailed +-- add a content only if has the tags used as filters +addFilteredContent : Content -> Model -> Model +addFilteredContent content model = + if subset model.filters.tags content.tags + then { model | contents = content :: model.contents } + else model + update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of FetchSuccessful contents -> ( { model | contents = contents }, Cmd.none ) @@ -61,8 +71,8 @@ update msg model = case msg of NewContent newContent -> ( { model | newContent = newContent }, Cmd.none ) NewFilter filterMsg -> Tuple.mapFirst ( \filters -> { model | filters = filters } ) ( Tags.update ( retrieveContents model.token ) filterMsg model.filters ) NewTag tagMsg -> Tuple.mapFirst ( \newTags -> { model | newTags = newTags } ) ( Tags.update ( always ( Cmd.none ) ) tagMsg model.newTags ) - SubmitContent -> ( { model | newContent = "" } , addContent model.token ( Content model.newContent model.newTags.tags ) ) - SubmitSuccessful content -> ( { model | contents = content :: model.contents }, Cmd.none ) + SubmitContent -> ( { model | newContent = "", newTags = Tags.init } , addContent model.token ( Content model.newContent model.newTags.tags ) ) + SubmitSuccessful content -> ( addFilteredContent content model, Cmd.none ) SubmitFailed -> ( model, Cmd.none ) -- VIEW From aa5798e461b32f45c378fffb718dca0483d81e16 Mon Sep 17 00:00:00 2001 From: Marco Perone Date: Thu, 14 Apr 2022 18:05:54 +0200 Subject: [PATCH 16/19] improve parameters for running quickstrom --- elm/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/elm/README.md b/elm/README.md index ca5e785..25e838a 100644 --- a/elm/README.md +++ b/elm/README.md @@ -46,6 +46,9 @@ docker run --rm \ --reporter=html \ --html-report-directory=/spec/report \ --tests=10 \ + --max-actions=50 \ + --max-trailing-state-changes=1 \ + --trailing-state-change-timeout=500 \ /spec/Tagger.spec.purs \ http://localhost:8000 ``` From b81b32121ece39f2166df15b453a42ed26d082a4 Mon Sep 17 00:00:00 2001 From: Marco Perone Date: Tue, 14 Jun 2022 10:45:25 +0200 Subject: [PATCH 17/19] provide quickstrom script --- bin/test/quickstrom | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100755 bin/test/quickstrom diff --git a/bin/test/quickstrom b/bin/test/quickstrom new file mode 100755 index 0000000..730bf83 --- /dev/null +++ b/bin/test/quickstrom @@ -0,0 +1,33 @@ +#!/bin/sh + +cd elm/spec + +sudo rm -rf report + +docker stop webdriver + +docker run --rm -d \ + --name webdriver \ + --network=host \ + -v /dev/shm:/dev/shm \ + -v $PWD:/spec \ + selenium/standalone-chrome:3.141.59-20200826 + +sleep 1 + +docker run --rm \ + --network=host \ + -v $PWD:/spec \ + quickstrom/quickstrom \ + quickstrom check \ + --webdriver-host=webdriver \ + --webdriver-path=/wd/hub \ + --browser=chrome \ + --reporter=html \ + --html-report-directory=/spec/report \ + --tests=10 \ + --max-actions=50 \ + --max-trailing-state-changes=1 \ + --trailing-state-change-timeout=500 \ + /spec/Tagger.spec.purs \ + http://localhost:8000 From d710ffb606b66ca5bc0fd440729a589048d78537 Mon Sep 17 00:00:00 2001 From: Marco Perone Date: Tue, 14 Jun 2022 10:46:26 +0200 Subject: [PATCH 18/19] improve `elm/README.md` --- elm/README.md | 36 +++++------------------------------- 1 file changed, 5 insertions(+), 31 deletions(-) diff --git a/elm/README.md b/elm/README.md index 25e838a..ac8ad95 100644 --- a/elm/README.md +++ b/elm/README.md @@ -1,6 +1,6 @@ # Tagger Elm client -This folder contains a client application built with [Elm](https://elm-lang.org/), which allows to interact in a human-friendly way with the Tagger api. +This folder contains a client application built with [Elm](https://elm-lang.org/), which allows interacting in a human-friendly way with the Tagger API. ## Build @@ -14,9 +14,9 @@ Then, you can directly open `index.html` to interact with the application. ## Workflow -The application requires you to first register a new user. Once this is done, you can login with the same credentials and access the private area. +The application requires you to first register a new user. Once this is done, you can log in with the same credentials and access the private area. -In the private area, you'll see the contents for the logged in user and you can also: +In the private area, you'll see the contents for the logged-in user, and you can also: - add new contents with their tags; - filter the shown contents by tag. @@ -25,32 +25,6 @@ In the private area, you'll see the contents for the logged in user and you can The `spec` folder contains some end-to-end acceptance tests written using [Quickstrom](https://quickstrom.io/). -To run them, you could execute the following commands inside the `spec` folder, given your application is exposed on `localhost:8000`: +To run them, just execute `bin/test/quickstrom` from the root of the project, given your application is exposed on `localhost:8000`. -``` -docker run --rm -d \ - --name webdriver \ - --network=host \ - -v /dev/shm:/dev/shm \ - -v $PWD:/spec \ - selenium/standalone-chrome:3.141.59-20200826 - -docker run --rm \ - --network=host \ - -v $PWD:/spec \ - quickstrom/quickstrom \ - quickstrom check \ - --webdriver-host=webdriver \ - --webdriver-path=/wd/hub \ - --browser=chrome \ - --reporter=html \ - --html-report-directory=/spec/report \ - --tests=10 \ - --max-actions=50 \ - --max-trailing-state-changes=1 \ - --trailing-state-change-timeout=500 \ - /spec/Tagger.spec.purs \ - http://localhost:8000 -``` - -Then in the `spec/report` folder you'll find an `index.html` file containing a report of each test which was executed. +Then in the `elm/spec/report` folder you'll find an `index.html` file containing a report of each test which was executed. From 7c6ea1b071dc4fcc8963fb7999754b39f68c2296 Mon Sep 17 00:00:00 2001 From: Marco Perone Date: Thu, 16 Jun 2022 15:45:55 +0200 Subject: [PATCH 19/19] use docker-compose to run quickstrom --- bin/test/quickstrom | 33 --------------------------------- elm/README.md | 2 +- elm/spec/docker-compose.yml | 27 +++++++++++++++++++++++++++ 3 files changed, 28 insertions(+), 34 deletions(-) delete mode 100755 bin/test/quickstrom create mode 100644 elm/spec/docker-compose.yml diff --git a/bin/test/quickstrom b/bin/test/quickstrom deleted file mode 100755 index 730bf83..0000000 --- a/bin/test/quickstrom +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/sh - -cd elm/spec - -sudo rm -rf report - -docker stop webdriver - -docker run --rm -d \ - --name webdriver \ - --network=host \ - -v /dev/shm:/dev/shm \ - -v $PWD:/spec \ - selenium/standalone-chrome:3.141.59-20200826 - -sleep 1 - -docker run --rm \ - --network=host \ - -v $PWD:/spec \ - quickstrom/quickstrom \ - quickstrom check \ - --webdriver-host=webdriver \ - --webdriver-path=/wd/hub \ - --browser=chrome \ - --reporter=html \ - --html-report-directory=/spec/report \ - --tests=10 \ - --max-actions=50 \ - --max-trailing-state-changes=1 \ - --trailing-state-change-timeout=500 \ - /spec/Tagger.spec.purs \ - http://localhost:8000 diff --git a/elm/README.md b/elm/README.md index ac8ad95..0e36848 100644 --- a/elm/README.md +++ b/elm/README.md @@ -25,6 +25,6 @@ In the private area, you'll see the contents for the logged-in user, and you can The `spec` folder contains some end-to-end acceptance tests written using [Quickstrom](https://quickstrom.io/). -To run them, just execute `bin/test/quickstrom` from the root of the project, given your application is exposed on `localhost:8000`. +To run them, just execute `docker-compose up` from the `elm/spec` folder, given your application is exposed on `localhost:8000`. Then in the `elm/spec/report` folder you'll find an `index.html` file containing a report of each test which was executed. diff --git a/elm/spec/docker-compose.yml b/elm/spec/docker-compose.yml new file mode 100644 index 0000000..8de7261 --- /dev/null +++ b/elm/spec/docker-compose.yml @@ -0,0 +1,27 @@ +version: '3' + +services: + webdriver: + image: selenium/standalone-chrome:3.141.59-20200826 + container_name: webdriver + volumes: + - /dev/shm:/dev/shm + - .:/spec + healthcheck: + test: curl -f http://localhost:4444 || exit 1 + interval: 1s + timeout: 1s + retries: 5 + start_period: 10s + network_mode: "host" + + quickstrom: + image: quickstrom/quickstrom + container_name: quickstrom + volumes: + - .:/spec + command: quickstrom check --webdriver-host=webdriver --webdriver-path=/wd/hub --browser=chrome --reporter=html --html-report-directory=/spec/report --tests=10 --max-actions=50 --max-trailing-state-changes=1 --trailing-state-change-timeout=500 /spec/Tagger.spec.purs http://localhost:8000 + depends_on: + webdriver: + condition: service_healthy + network_mode: "host"