feat: complete chapter 6 - testing

main
Louis Pearson 2024-01-31 02:15:36 -07:00
parent b6cb2a2750
commit 8d6cc1db71
4 changed files with 220 additions and 45 deletions

1
PhotoGroove/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
elm-stuff

View File

@ -23,7 +23,9 @@
}
},
"test-dependencies": {
"direct": {},
"direct": {
"elm-explorations/test": "2.2.0"
},
"indirect": {}
}
}

View File

@ -1,22 +1,36 @@
port module PhotoGroove exposing (main)
port module PhotoGroove exposing
( Model
, Msg(..)
, Photo
, Status(..)
, initialModel
, main
, photoDecoder
, photoFromUrl
, update
, urlPrefix
, view
)
import Array exposing (Array)
import Browser
import Html exposing (..)
import Html.Attributes as Attr exposing (class, classList, id, name, src, title, type_)
import Html.Events exposing (onClick, on)
import Html.Events exposing (on, onClick)
import Http
import Json.Decode exposing (Decoder, at, int, list, string, succeed)
import Json.Decode.Pipeline exposing (optional, required)
import Json.Encode
import Random
urlPrefix : String
urlPrefix =
"https://elm-in-action.com/"
type Msg
= ClickedPhoto String
= ClickedPhoto String
| SetSize ThumbnailSize
| ClickedSurpriseMe
| GotRandomPhoto Photo
@ -26,6 +40,7 @@ type Msg
| SlidRipple Int
| SlidNoise Int
view : Model -> Html Msg
view model =
div [ class "content" ] <|
@ -39,6 +54,7 @@ view model =
Errored errorMessage ->
[ text ("Error: " ++ errorMessage) ]
viewFilter : (Int -> Msg) -> String -> Int -> Html Msg
viewFilter toMsg name magnitude =
div [ class "filter-slider" ]
@ -52,25 +68,27 @@ viewFilter toMsg name magnitude =
, label [] [ text (String.fromInt magnitude) ]
]
viewLoaded : List Photo -> String -> Model -> List (Html Msg)
viewLoaded photos selectedUrl model =
[ h1 [] [ text "Photo Groove" ]
, button
[ onClick ClickedSurpriseMe ]
[ text "Surprise Me!" ]
, div [ class "activity" ] [ text model.activity ]
, div [ class "filters" ]
[ viewFilter SlidHue "Hue" model.hue
, viewFilter SlidRipple "Ripple" model.ripple
, viewFilter SlidNoise "Noise" model.noise
]
, h3 [] [ text "Thumbnail Size:" ]
, div [ id "choose-size" ]
(List.map viewSizeChooser [ Small, Medium, Large ])
, div [ id "thumbnails", class (sizeToString model.chosenSize) ]
(List.map (viewThumbnail selectedUrl) photos)
, canvas [ id "main-canvas", class "large" ] []
[ h1 [] [ text "Photo Groove" ]
, button
[ onClick ClickedSurpriseMe ]
[ text "Surprise Me!" ]
, div [ class "activity" ] [ text model.activity ]
, div [ class "filters" ]
[ viewFilter SlidHue "Hue" model.hue
, viewFilter SlidRipple "Ripple" model.ripple
, viewFilter SlidNoise "Noise" model.noise
]
, h3 [] [ text "Thumbnail Size:" ]
, div [ id "choose-size" ]
(List.map viewSizeChooser [ Small, Medium, Large ])
, div [ id "thumbnails", class (sizeToString model.chosenSize) ]
(List.map (viewThumbnail selectedUrl) photos)
, canvas [ id "main-canvas", class "large" ] []
]
viewThumbnail : String -> Photo -> Html Msg
viewThumbnail selectedUrl thumb =
@ -78,10 +96,11 @@ viewThumbnail selectedUrl thumb =
[ src (urlPrefix ++ thumb.url)
, title (thumb.title ++ " [" ++ String.fromInt thumb.size ++ " KB]")
, classList [ ( "selected", selectedUrl == thumb.url ) ]
, onClick (ClickedPhoto thumb.url)
, onClick (ClickedPhoto thumb.url)
]
[]
viewSizeChooser : ThumbnailSize -> Html Msg
viewSizeChooser size =
label []
@ -89,35 +108,45 @@ viewSizeChooser size =
, text (sizeToString size)
]
sizeToString : ThumbnailSize -> String
sizeToString size =
case size of
Small ->
"small"
Medium ->
"medium"
Large ->
"large"
type ThumbnailSize
= Small
| Medium
| Large
port setFilters : FilterOptions -> Cmd msg
port activityChanges : (String -> msg) -> Sub msg
type alias FilterOptions =
{ url : String
, filters : List { name : String, amount : Float }
}
type alias Photo =
{ url : String
, size : Int
, title : String
}
photoDecoder : Decoder Photo
photoDecoder =
succeed Photo
@ -125,11 +154,13 @@ photoDecoder =
|> required "size" int
|> optional "title" string "(untitled)"
type Status
= Loading
| Loaded (List Photo) String
| Errored String
type alias Model =
{ status : Status
, activity : String
@ -139,15 +170,16 @@ type alias Model =
, noise : Int
}
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
GotActivity activity ->
( { model | activity = activity }, Cmd.none)
( { model | activity = activity }, Cmd.none )
GotPhotos (Ok photos) ->
case photos of
first ::rest ->
first :: rest ->
applyFilters
{ model
| status =
@ -160,10 +192,10 @@ update msg model =
}
[] ->
( { model | status = Errored "0 photos found"}, Cmd.none )
( { model | status = Errored "0 photos found" }, Cmd.none )
GotPhotos (Err _) ->
( model, Cmd.none )
( model, Cmd.none )
GotRandomPhoto photo ->
applyFilters { model | status = selectUrl photo.url model.status }
@ -200,15 +232,16 @@ update msg model =
SlidNoise noise ->
applyFilters { model | noise = noise }
applyFilters : Model -> ( Model, Cmd msg )
applyFilters model =
case model.status of
Loaded photos selectedUrl ->
let
filters =
[ { name = "Hue", amount = toFloat model.hue / 11}
, { name = "Ripple", amount = toFloat model.ripple / 11}
, { name = "Noise", amount = toFloat model.noise / 11}
[ { name = "Hue", amount = toFloat model.hue / 11 }
, { name = "Ripple", amount = toFloat model.ripple / 11 }
, { name = "Noise", amount = toFloat model.noise / 11 }
]
url =
@ -222,16 +255,20 @@ applyFilters model =
Errored errorMessage ->
( model, Cmd.none )
selectUrl : String -> Status -> Status
selectUrl url status =
case status of
Loaded photos _ ->
Loaded photos url
Loading ->
status
Errored errorMessage ->
status
initialModel : Model
initialModel =
{ status = Loading
@ -242,6 +279,7 @@ initialModel =
, noise = 5
}
initialCmd : Cmd Msg
initialCmd =
Http.get
@ -249,6 +287,7 @@ initialCmd =
, expect = Http.expectJson GotPhotos (list photoDecoder)
}
main : Program Float Model Msg
main =
Browser.element
@ -258,6 +297,7 @@ main =
, subscriptions = subscriptions
}
init : Float -> ( Model, Cmd Msg )
init flags =
let
@ -266,16 +306,24 @@ init flags =
in
( { initialModel | activity = activity }, initialCmd )
subscriptions : Model -> Sub Msg
subscriptions model =
activityChanges GotActivity
rangeSlider : List (Attribute msg) -> List (Html msg) -> Html msg
rangeSlider attributes children =
node "range-slider" attributes children
onSlide : (Int -> msg) -> Attribute msg
onSlide toMsg =
at [ "detail", "userSlidTo" ] int
|> Json.Decode.map toMsg
|> on "slide"
photoFromUrl : String -> Photo
photoFromUrl url =
{ url = url, size = 0, title = "" }

View File

@ -0,0 +1,124 @@
module PhotoGrooveTests exposing (..)
import Expect exposing (Expectation)
import Fuzz exposing (Fuzzer, int, list, string)
import Html.Attributes as Attr exposing (src)
import Json.Decode as Decode exposing (decodeValue)
import Json.Encode as Encode
import PhotoGroove
exposing
( Model
, Msg(..)
, Photo
, Status(..)
, initialModel
, photoFromUrl
, update
, urlPrefix
, view
)
import Test exposing (..)
import Test.Html.Event as Event
import Test.Html.Query as Query
import Test.Html.Selector exposing (attribute, tag, text)
decoderTest : Test
decoderTest =
fuzz2 string int "title defaults to (untitled)" <|
\url size ->
[ ( "url", Encode.string url )
, ( "size", Encode.int size )
]
|> Encode.object
|> decodeValue PhotoGroove.photoDecoder
|> Result.map .title
|> Expect.equal (Ok "(untitled)")
sliders : Test
sliders =
describe "Slider sets the desired field in the model"
[ testSlider "SlidHue" SlidHue .hue
, testSlider "SlidRipple" SlidRipple .ripple
, testSlider "SlidNoise" SlidNoise .noise
]
testSlider : String -> (Int -> Msg) -> (Model -> Int) -> Test
testSlider description toMsg amountFromModel =
fuzz int description <|
\amount ->
initialModel
|> update (toMsg amount)
|> Tuple.first
|> amountFromModel
|> Expect.equal amount
noPhotosNoThumbnails : Test
noPhotosNoThumbnails =
test "No thumbnails render when there are no photos to render." <|
\_ ->
initialModel
|> PhotoGroove.view
|> Query.fromHtml
|> Query.findAll [ tag "img" ]
|> Query.count (Expect.equal 0)
thumbnailRendered : String -> Query.Single msg -> Expectation
thumbnailRendered url query =
query
|> Query.findAll [ tag "img", attribute (Attr.src (urlPrefix ++ url)) ]
|> Query.count (Expect.atLeast 1)
thumbnailsWork : Test
thumbnailsWork =
fuzz urlFuzzer "URLs render as thumbnail" <|
\urls ->
let
thumbnailChecks : List (Query.Single msg -> Expectation)
thumbnailChecks =
List.map thumbnailRendered urls
in
{ initialModel | status = Loaded (List.map photoFromUrl urls) "" }
|> view
|> Query.fromHtml
|> Expect.all thumbnailChecks
urlFuzzer : Fuzzer (List String)
urlFuzzer =
Fuzz.intRange 1 5
|> Fuzz.map urlsFromCount
urlsFromCount : Int -> List String
urlsFromCount urlCount =
List.range 1 urlCount
|> List.map (\num -> String.fromInt num ++ ".png")
clickThumbnail : Test
clickThumbnail =
fuzz3 urlFuzzer string urlFuzzer "clicking a thumbnail selects it" <|
\urlsBefore urlToSelect urlsAfter ->
let
url =
urlToSelect ++ ".jpeg"
photos =
(urlsBefore ++ url :: urlsAfter)
|> List.map photoFromUrl
srcToClick =
urlPrefix ++ url
in
{ initialModel | status = Loaded photos "" }
|> view
|> Query.fromHtml
|> Query.find [ tag "img", attribute (Attr.src srcToClick) ]
|> Event.simulate Event.click
|> Event.expect (ClickedPhoto url)