|
module WebCheck.PureScript.TodoMVC where |
|
|
|
import WebCheck.DSL |
|
|
|
import Data.Array (filter, foldMap, head, last, zip) |
|
import Data.Foldable (length) |
|
import Data.Int as Int |
|
import Data.Maybe (Maybe(..), fromMaybe) |
|
import Data.String (Pattern(..), split, trim) |
|
import Data.Tuple (Tuple(..)) |
|
|
|
-- Start running actions once this selector matches an element. |
|
readyWhen :: Selector |
|
readyWhen = queries.newTodo |
|
|
|
-- Actions to draw from, with assigned probabilities. |
|
actions :: Actions |
|
actions = appFoci <> appClicks <> appKeyPresses |
|
where |
|
appClicks = |
|
[ Tuple 5 (Click queries.filters.notSelected) |
|
, Tuple 1 (Click queries.filters.selected) |
|
, Tuple 1 (Click queries.toggleAll) |
|
, Tuple 1 (Click queries.destroy) |
|
] |
|
appFoci = [ Tuple 5 (Focus queries.newTodo) ] |
|
appKeyPresses = |
|
[ Tuple 5 (keyPress 'a') |
|
, Tuple 5 (specialKeyPress KeyReturn) |
|
] |
|
|
|
-- The safety property of the specification. |
|
proposition :: Boolean |
|
proposition = |
|
initial |
|
&& always (enterText |
|
|| addNew |
|
|| changeFilter |
|
|| checkOne |
|
|| uncheckOne |
|
|| delete |
|
|| toggleAll |
|
) |
|
&& always hasFilters |
|
where |
|
|
|
initial :: Boolean |
|
initial = |
|
(selectedFilter == Nothing || selectedFilter == Just All) |
|
&& (numItems == 0) |
|
&& (pendingText == "") |
|
|
|
enterText :: Boolean |
|
enterText = |
|
pendingText /= next pendingText |
|
&& itemTexts == next itemTexts |
|
&& selectedFilter == next selectedFilter |
|
|
|
changeFilter :: Boolean |
|
changeFilter = |
|
case selectedFilter, next selectedFilter of |
|
Nothing, Just All -> true -- adding the first todo item switches to the All filter |
|
Nothing, _ -> false -- any other transition from an empty todo list is incorrect |
|
_, Nothing -> false -- removing the last todo item is not handled by this action |
|
Just All, _ -> numItems >= next numItems |
|
_, Just Active -> next (numItemsLeft == Just numUnchecked && numItems == numUnchecked) |
|
Just f1, Just f2 |
|
| f1 == f2 -> false |
|
| otherwise -> pendingText == next pendingText |
|
|
|
addNew = |
|
next (pendingText == "") |
|
&& case next selectedFilter of |
|
Just All -> Just pendingText == next lastItemText |
|
Just Active -> Just pendingText == next lastItemText |
|
Just Completed -> itemTexts == next itemTexts |
|
Nothing -> false |
|
|
|
checkOne = |
|
pendingText == next pendingText |
|
&& selectedFilter == next selectedFilter |
|
&& (selectedFilter /= Just Completed) |
|
&& ( (selectedFilter == Just All) |
|
`implies` (numItems == next numItems && numChecked < next numChecked) |
|
) |
|
&& ( (selectedFilter == Just Active) |
|
`implies` (numItems > next numItems && numItemsLeft > next numItemsLeft) |
|
) |
|
|
|
uncheckOne = |
|
pendingText == next pendingText |
|
&& selectedFilter == next selectedFilter |
|
&& (selectedFilter /= Just Active) |
|
&& ( (selectedFilter == Just All) |
|
`implies` (numItems == next numItems && numChecked > next numChecked) |
|
) |
|
&& ( (selectedFilter == Just Completed) |
|
`implies` (numItems > next numItems && numItemsLeft < next numItemsLeft) |
|
) |
|
|
|
delete = |
|
pendingText == next pendingText |
|
&& case selectedFilter, numItems of |
|
_, 1 -> next (numItems == 0) |
|
Just filter, n -> |
|
selectedFilter == next selectedFilter |
|
&& next numItems == n - 1 |
|
&& case filter of |
|
All -> true |
|
Active -> numItemsLeft == next ((_ - 1) <$> numItemsLeft) |
|
Completed -> numItemsLeft == next numItemsLeft |
|
Nothing, _ -> false |
|
|
|
toggleAll = |
|
pendingText == next pendingText |
|
&& selectedFilter == next selectedFilter |
|
&& case selectedFilter of |
|
Just All -> numItems == next numItems && next (numItems == numChecked) |
|
Just Active -> (numItems > 0) `implies` (next numItems == 0) |
|
|| (numItems == 0) `implies` (next numItems > 0) |
|
Just Completed -> numItems + fromMaybe 0 numItemsLeft == (next numItems) |
|
Nothing -> false |
|
|
|
|
|
selectedFilter = do |
|
f <- queryOne queries.filters.selected { text: textContent } |
|
parse f.text |
|
where |
|
parse = case _ of |
|
"All" -> pure All |
|
"Active" -> pure Active |
|
"Completed" -> pure Completed |
|
_ -> Nothing |
|
|
|
hasFilters = |
|
case length items, availableFilters of |
|
0, _ -> true |
|
_, ["All", "Active", "Completed"] -> true |
|
_, _ -> false |
|
where |
|
availableFilters = map _.textContent (queryAll queries.filters.all { textContent }) |
|
|
|
items :: Array { text :: String } |
|
items = |
|
foldMap (\(Tuple li label) -> if li.display /= "none" then pure label else mempty) |
|
(zip |
|
(queryAll queries.items.listItems { display: cssValue "display" }) |
|
(queryAll queries.items.labels { text: textContent })) |
|
|
|
itemTexts = map _.text items |
|
|
|
lastItemText = last itemTexts |
|
|
|
numItems :: Int |
|
numItems = length items |
|
|
|
checkboxes = queryAll queries.items.checkboxes { checked: checked } |
|
|
|
numUnchecked :: Int |
|
numUnchecked = length (filter (not <<< _.checked) checkboxes) |
|
|
|
numChecked :: Int |
|
numChecked = length (filter _.checked checkboxes) |
|
|
|
pendingText :: String |
|
pendingText = case queryOne queries.newTodo { value: value } of |
|
Just el -> el.value |
|
Nothing -> "" |
|
|
|
numItemsLeft :: Maybe Int |
|
numItemsLeft = do |
|
strong <- queryOne queries.todoCount { text: textContent } |
|
first <- head (split (Pattern " ") (trim strong.text)) |
|
Int.fromString first |
|
|
|
data Filter = All | Active | Completed |
|
|
|
derive instance eqFilter :: Eq Filter |
|
|
|
-- All queries used in this specification. |
|
queries = { |
|
top: ".todoapp", |
|
filters: { |
|
all: ".todoapp .filters a", |
|
selected: ".todoapp .filters a.selected", |
|
notSelected: ".todoapp .filters a:not(.selected)" |
|
}, |
|
destroy: ".todoapp .destroy", |
|
toggleAll: ".todoapp label[for=toggle-all]", |
|
newTodo: ".todoapp .new-todo", |
|
todoCount: ".todoapp .todo-count strong", |
|
items: { |
|
listItems: ".todo-list li", |
|
labels: ".todo-list li label", |
|
checkboxes: ".todo-list li input[type=checkbox]" |
|
} |
|
} |