11{-# OPTIONS_GHC -fno-warn-name-shadowing #-}
22{-# OPTIONS_GHC -fno-warn-type-defaults #-}
3+ {-# OPTIONS_GHC -fno-warn-unused-do-bind #-}
4+
35{-# LANGUAGE OverloadedStrings #-}
6+
47module TestUserAcceptance where
58
69import qualified Data.Text as T
@@ -17,14 +20,20 @@ import Control.Monad.IO.Class (liftIO)
1720import Control.Monad.Reader (runReaderT , ask , ReaderT )
1821
1922import Control.Lens (view , _3 , _2 )
20- import Data.List (isInfixOf )
23+ import Data.List (isInfixOf , intercalate )
2124import System.Process (callProcess , readProcess )
2225import System.Directory
2326 (getCurrentDirectory , removeDirectoryRecursive , removeFile )
2427import Test.Tasty (TestTree , TestName , testGroup , withResource )
2528import Test.Tasty.HUnit (testCaseSteps , assertBool )
2629import Text.Regex.Posix ((=~) )
2730
31+ -- | A condition to check for in the output of the program
32+ data Condition
33+ = Literal String
34+ | Regex String
35+ deriving (Show )
36+
2837systemTests ::
2938 TestTree
3039systemTests =
@@ -47,43 +56,39 @@ testErrorHandling = withTmuxSession "error handling" $
4756 liftIO $ removeFile (testmdir <> " /new/1502941827.R15455991756849358775.url" )
4857
4958 liftIO $ step " shows error message"
50- out <- sendKeys " Enter" " FileReadError"
51- assertSubstrInOutput " openFile: does not exist" out
59+ sendKeys " Enter" ( Literal " FileReadError" )
60+ >>= assertSubstrInOutput " openFile: does not exist"
5261
5362testSetsMailToRead ::
5463 TestTree
5564testSetsMailToRead = withTmuxSession " user can toggle read tag" $
5665 \ step -> do
5766 startApplication
5867 liftIO $ step " mail is shown as unread (bold)"
59- out <- capture
60- assertRegex " \ESC\\ [1;.*Testmail" out
68+ capture >>= assertRegex (buildAnsiRegex [" 1" ] [" 37" ] [" 43" ] <> " .*Testmail" )
6169
62- liftIO $ step " view mail and purebred sets it to read"
63- _ <- sendKeys " Enter" " This is a test mail"
64- out <- sendKeys " Escape" " is Purebred"
65- assertRegex " \ESC\\ [37.*Testmail" out
70+ liftIO $ step " view mail and purebred sets it to read (unbold) "
71+ sendKeys " Enter" ( Literal " This is a test mail" )
72+ sendKeys " Escape" ( Literal " is Purebred" )
73+ >>= assertRegex " \ESC\\ [37.*Testmail"
6674
67- liftIO $ step " toggle it back to unread"
68- -- wait for the screen turns bold
69- out <- sendKeys " t" " 1;37;43m"
70- assertRegex " \ESC\\ [1;.*Testmail" out
75+ liftIO $ step " toggle it back to unread (bold again)"
76+ sendKeys " t" (Regex (buildAnsiRegex [" 1" ] [" 37" ] [" 43" ] <> " .*Testmail" ))
77+ pure ()
7178
7279testCanToggleHeaders ::
7380 TestTree
7481testCanToggleHeaders = withTmuxSession " user can toggle Headers" $
7582 \ step -> do
7683 startApplication
7784 liftIO $ step " view mail"
78- out <- sendKeys " Enter" " This is a test mail"
79- assertSubstrInOutput " This is a test mail" out
85+ sendKeys " Enter" (Literal " This is a test mail" )
8086
8187 liftIO $ step " toggle to show all headers"
82- out <- sendKeys " h" " return-path"
83- assertSubstrInOutput " return-path" out
88+ sendKeys " h" (Literal " return-path" )
8489
8590 liftIO $ step " toggle filtered headers"
86- out <- sendKeys " h" " This is a test mail"
91+ out <- sendKeys " h" ( Literal " This is a test mail" )
8792 assertRegex " Purebred.*\n .*from" out
8893
8994testUserViewsMailSuccessfully ::
@@ -96,8 +101,8 @@ testUserViewsMailSuccessfully = withTmuxSession "user can view mail" $
96101 assertSubstrInOutput " inbox" out
97102
98103 liftIO $ step " view mail"
99- out <- sendKeys " Enter" " This is a test mail"
100- assertSubstrInOutput " This is a test mail " out
104+ sendKeys " Enter" ( Literal " This is a test mail" )
105+ pure ()
101106
102107testUserCanManipulateNMQuery ::
103108 TestTree
@@ -107,23 +112,20 @@ testUserCanManipulateNMQuery =
107112 \ step -> do
108113 startApplication
109114 liftIO $ step " focus command"
110- out <- sendKeys " :" " 37;40mtag"
111- assertSubstrInOutput " 37;40mtag" out
115+ sendKeys " :" (Regex (buildAnsiRegex [] [" 37" ] [" 40" ] <> " tag" ))
112116
113117 liftIO $ step " delete all input"
114- out <- sendKeys " C-u" " 37;40m"
115- assertSubstrInOutput " 37;40m" out
118+ sendKeys " C-u" (Regex (buildAnsiRegex [] [" 37" ] [" 40" ]))
116119
117120 liftIO $ step " enter new tag"
118121 _ <- sendLiteralKeys " tag:replied"
119122
120123 liftIO $ step " apply"
121- out <- sendKeys " Enter" " Item 0 of 1"
122- assertSubstrInOutput " Item 0 of 1" out
124+ sendKeys " Enter" (Literal " Item 0 of 1" )
123125
124126 liftIO $ step " view currently selected mail"
125- out <- sendKeys " Enter" " HOLY PUREBRED"
126- assertSubstrInOutput " HOLY PUREBRED " out
127+ sendKeys " Enter" ( Literal " HOLY PUREBRED" )
128+ pure ()
127129
128130testUserCanSwitchBackToIndex ::
129131 TestTree
@@ -132,48 +134,40 @@ testUserCanSwitchBackToIndex =
132134 \ step -> do
133135 startApplication
134136 liftIO $ step " start composition"
135- out <- sendKeys " m" " From"
136- assertSubstrInOutput " From" out
137+ sendKeys " m" (Literal " From" )
137138
138139 liftIO $ step " enter from email"
139- out <- sendKeys " testuser@foo.test\r " " To"
140- assertSubstrInOutput " To" out
140+ sendKeys " testuser@foo.test\r " (Literal " To" )
141141
142142 liftIO $ step " enter to: email"
143- out <- sendKeys " user@to.test\r " " Subject"
144- assertSubstrInOutput " Subject" out
143+ sendKeys " user@to.test\r " (Literal " Subject" )
145144
146145 liftIO $ step " enter subject"
147- out <- sendKeys " test subject\r " " ~"
148- assertSubstrInOutput " ~" out
146+ sendKeys " test subject\r " (Literal " ~" )
149147
150148 liftIO $ step " enter mail body"
151- out <- sendKeys " iThis is a test body" " body"
152- assertSubstrInOutput " body" out
149+ sendKeys " iThis is a test body" (Literal " body" )
153150
154151 liftIO $ step " exit insert mode in vim"
155- out <- sendKeys " Escape" " body"
156- assertSubstrInOutput " body" out
152+ sendKeys " Escape" (Literal " body" )
157153
158154 liftIO $ step " exit vim"
159- out <- sendKeys " : x\r " " Attachments"
160- assertSubstrInOutput " Attachments" out
155+ sendKeys " : x\r " (Literal " Attachments" )
161156
162157 liftIO $ step " switch back to index"
163- out <- sendKeys " Tab" " Testmail"
164- assertSubstrInOutput " Testmail" out
158+ sendKeys " Tab" (Literal " Testmail" )
165159
166160 liftIO $ step " switch back to the compose editor"
167- out <- sendKeys " Tab" " test subject"
168- assertSubstrInOutput " test subject " out
161+ sendKeys " Tab" ( Literal " test subject" )
162+ pure ()
169163
170164type Env = (String , String , String )
171165
172166assertSubstrInOutput :: String -> String -> ReaderT Env IO ()
173167assertSubstrInOutput substr out = liftIO $ assertBool (substr <> " not found in\n\n " <> out) $ substr `isInfixOf` out
174168
175169assertRegex :: String -> String -> ReaderT Env IO ()
176- assertRegex regex out = liftIO $ assertBool (regex <> " does not match out\n\n " <> out) $ out =~ (regex :: String )
170+ assertRegex regex out = liftIO $ assertBool (regex <> " does not match out\n\n " <> out <> " \n\n raw: \n\n " <> show out ) $ out =~ (regex :: String )
177171
178172defaultSessionName :: String
179173defaultSessionName = " purebredtest"
@@ -254,10 +248,13 @@ withTmuxSession tcname testfx =
254248 withResource setUp tearDown $
255249 \ env -> testCaseSteps tcname $ \ stepfx -> env >>= runReaderT (testfx stepfx)
256250
257- sendKeys :: String -> String -> ReaderT Env IO (String )
251+ -- | Send keys into the program and wait for the condition to be
252+ -- met, failing the test if the condition is not met after some
253+ -- time.
254+ sendKeys :: String -> Condition -> ReaderT Env IO String
258255sendKeys keys expect = do
259256 liftIO $ callProcess " tmux" $ communicateSessionArgs keys False
260- waitForString expect defaultCountdown
257+ waitForCondition expect defaultCountdown
261258
262259sendLiteralKeys :: String -> ReaderT Env IO (String )
263260sendLiteralKeys keys = do
@@ -285,24 +282,35 @@ debugOutput out = do
285282 when (isJust d) $ hPutStr stderr (" \n\n " <> out)
286283
287284-- | wait for the application to render a new interface which we determine with
288- -- a given substring. If we exceed the number of tries return with the last
289- -- captured output, but indicate an error by setting the baton to 0
290- waitForString :: String -> Int -> ReaderT Env IO (String )
291- waitForString substr n = do
285+ -- a given condition. We check up to @n@ times, waiting a short duration
286+ -- between each check, and failing if the tries exhaust with the condition
287+ -- not met.
288+ waitForCondition :: Condition -> Int -> ReaderT Env IO String
289+ waitForCondition cond n = do
292290 out <- capture >>= checkPane
293- liftIO $ assertBool ( " Wait time exceeded. Expected: ' "
294- <> substr
295- <> " ' last screen shot: \n\n "
296- <> out) (substr `isInfixOf` out)
291+ liftIO $ assertBool
292+ ( " Wait time exceeded. Condition not met: ' " <> show cond
293+ <> " ' last screen shot: \n\n " <> out <> " \n\n " <> " raw: " <> show out )
294+ (checkCondition cond out)
297295 pure out
298296 where
299297 checkPane :: String -> ReaderT Env IO String
300298 checkPane out
301- | substr `isInfixOf` out = pure out
299+ | checkCondition cond out = pure out
302300 | n <= 0 = pure out
303301 | otherwise = do
304302 liftIO $ threadDelay holdOffTime
305- waitForString substr (n - 1 )
303+ waitForCondition cond (n - 1 )
304+
305+ checkCondition :: Condition -> String -> Bool
306+ checkCondition (Literal s) = (s `isInfixOf` )
307+ checkCondition (Regex re) = (=~ re)
308+
309+ -- | Convenience version of 'waitForCondition' that checks for a
310+ -- literal string.
311+ --
312+ waitForString :: String -> Int -> ReaderT Env IO (String )
313+ waitForString = waitForCondition . Literal
306314
307315defaultCountdown :: Int
308316defaultCountdown = 5
@@ -324,3 +332,35 @@ communicateSessionArgs keys asLiteral =
324332 then [" -l" ]
325333 else []
326334 in base <> postfix <> [keys]
335+
336+
337+ type AnsiAttrParam = String
338+ type AnsiFGParam = String
339+ type AnsiBGParam = String
340+
341+ -- | Generate a regex for an escape sequence setting the given
342+ -- foreground and background parameters
343+ --
344+ -- tmux < 03d01eabb5c5227f56b6b44d04964c1328802628 (first released
345+ -- in tmux-2.5) ran attributes, foreground colour and background
346+ -- colour params separated by semicolons (foreground first).
347+ --
348+ -- After that commit, attributes, foreground colours and background
349+ -- colours are written in separate escape sequences. Therefore for
350+ -- compatibility with different versions of tmux there are two
351+ -- patterns to check.
352+ --
353+ buildAnsiRegex :: [AnsiAttrParam ] -> [AnsiFGParam ] -> [AnsiBGParam ] -> String
354+ buildAnsiRegex attrs fgs bgs =
355+ let
356+ withSemis = intercalate " ;"
357+ wrap [] = " "
358+ wrap xs = " \ESC\\ [" <> withSemis xs <> " m"
359+ tmux24 = wrap (attrs <> fgs <> bgs)
360+ tmux25 = wrap attrs <> wrap fgs <> wrap bgs
361+ choice " " " " = " "
362+ choice " " r = r
363+ choice l " " = l
364+ choice l r = " (" <> l <> " |" <> r <> " )"
365+ in
366+ choice tmux24 tmux25
0 commit comments