~alcinnz/amphiarao

ref: 097acc6e5d470b3f3651a65475808274932c7e48 amphiarao/src/Webdriver.hs -rw-r--r-- 12.2 KiB
097acc6e — Adrian Cochrane Display CSS styles in web UI. 2 years ago
                                                                                
505d1225 Adrian Cochrane
d4c10f89 Adrian Cochrane
505d1225 Adrian Cochrane
23e97519 Adrian Cochrane
74b98fb8 Adrian Cochrane
505d1225 Adrian Cochrane
d4c10f89 Adrian Cochrane
74b98fb8 Adrian Cochrane
d4c10f89 Adrian Cochrane
505d1225 Adrian Cochrane
38e35ef9 Adrian Cochrane
d4c10f89 Adrian Cochrane
74b98fb8 Adrian Cochrane
505d1225 Adrian Cochrane
d4c10f89 Adrian Cochrane
38e35ef9 Adrian Cochrane
74b98fb8 Adrian Cochrane
1067e7da Adrian Cochrane
505d1225 Adrian Cochrane
d4c10f89 Adrian Cochrane
8e195046 Adrian Cochrane
c4c655a0 Adrian Cochrane
1067e7da Adrian Cochrane
d4c10f89 Adrian Cochrane
8a39949f Adrian Cochrane
7a6334be Adrian Cochrane
afb3d55d Adrian Cochrane
d647a0fa Adrian Cochrane
8e195046 Adrian Cochrane
d647a0fa Adrian Cochrane
9414d297 Adrian Cochrane
86bfb71d Adrian Cochrane
38e35ef9 Adrian Cochrane
23e97519 Adrian Cochrane
28b06502 Adrian Cochrane
58648b62 Adrian Cochrane
e7e16d8e Adrian Cochrane
d4c10f89 Adrian Cochrane
505d1225 Adrian Cochrane
d4c10f89 Adrian Cochrane
505d1225 Adrian Cochrane
d4c10f89 Adrian Cochrane
8a39949f Adrian Cochrane
7a6334be Adrian Cochrane
afb3d55d Adrian Cochrane
7a6334be Adrian Cochrane
d647a0fa Adrian Cochrane
8e195046 Adrian Cochrane
d647a0fa Adrian Cochrane
86bfb71d Adrian Cochrane
9414d297 Adrian Cochrane
8e195046 Adrian Cochrane
38e35ef9 Adrian Cochrane
23e97519 Adrian Cochrane
38e35ef9 Adrian Cochrane
23e97519 Adrian Cochrane
58648b62 Adrian Cochrane
74b98fb8 Adrian Cochrane
1067e7da Adrian Cochrane
74b98fb8 Adrian Cochrane
c4c655a0 Adrian Cochrane
e7e16d8e Adrian Cochrane
58648b62 Adrian Cochrane
28b06502 Adrian Cochrane
74b98fb8 Adrian Cochrane
1067e7da Adrian Cochrane
74b98fb8 Adrian Cochrane
c4c655a0 Adrian Cochrane
e7e16d8e Adrian Cochrane
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
{-# LANGUAGE OverloadedStrings, ScopedTypeVariables, DeriveGeneric #-}
module Webdriver(serveWebdriver) where

import Happstack.Lite
import Control.Concurrent.MVar
import Data.Aeson
import Data.Text (Text, unpack)
import qualified Data.Text as Txt
import GHC.Generics
import Data.ByteString.Lazy (ByteString)

import qualified Data.HashMap.Strict as M
import qualified Data.Map.Strict as M'
import Data.UUID as ID
import Data.UUID.V4

import Control.Monad.IO.Class (liftIO)
import Control.Monad (mapM)
import Data.Maybe (fromMaybe, isJust)
import Data.String as Str

import qualified Network.URI as URI
import qualified Network.URI.Fetch as URI
import qualified Text.XML.Cursor as XC
import qualified Text.XML as X
import qualified Data.CSS.Syntax.Tokens as CSS

import Capabilities (processCaps)
import JSON
import qualified Internal as WD
import qualified Internal.Load as WD
import qualified Internal.Elements as WDE
import qualified Internal.Forms as WDF
import qualified Internal.Style as WDS

serveWebdriver :: WD.Sessions -> ServerPart Response
serveWebdriver sessions = do
  msum [
      dir "status" serveStatus,
      dir "session" $ postSession sessions,
      dir "session" $ path $ serveSession sessions,
      nullDir >> ok (toResponse ("This is a WebDriver endpoint. Please copy the URL to hand to Selenium." :: Text))
    ]
serveSession :: WD.Sessions -> String -> ServerPart Response
serveSession sessions = WD.withSession fail (\uuid session -> msum [
        delSession sessions uuid,
        dir "timeouts" $ getTimeout session,
        dir "timeouts" $ setTimeout session,
        dir "url" $ navigateTo session,
        dir "url" $ getURL session,
        dir "refresh" $ reloadPage session,
        dir "back" $ sessionAction WD.back session,
        dir "forward" $ sessionAction WD.next session,
        dir "title" $ sessionTitle session,
        dir "window" $ msum [ -- Noops
            getWindowHandle uuid,
            delSession sessions uuid, -- Closing the only window closes the session.
            switchWindowHandle uuid,
            dir "handles" $ getWindowHandles uuid,
            dir "rect" $ unsupportedOp,
            dir "maximize" $ unsupportedOp,
            dir "minimize" $ unsupportedOp,
            dir "fullscreen" $ unsupportedOp
        ],
        dir "frame" $ msum [noSuchFrame, dir "parent" $ ok $ toResponse ()], -- Noops
        dir "element" $ findFromRoot session,
        dir "element" $ dir "active" noSuchEl,
        dir "element" $ path $ serveElement session,
        dir "elements" $ findAllFromRoot session,
        dir "source" $ viewSource session,
        dir "execute" $ dir "sync" unsupportedOp,
        dir "execute" $ dir "async" unsupportedOp,
        dir "cookie" $ msum [getCookies, addCookie, deleteCookies],
        dir "cookie" $ path $ \cookie -> msum [getCookie cookie, deleteCookie cookie],
        dir "actions" unsupportedOp, -- I avoid dealing in terms of mouse/keyboard/touchscreen.
        dir "alert" $ msum [
            dir "dismiss" $ handleAlert,
            dir "accept" $ handleAlert,
            dir "text" $ alertText
        ],
        dir "screenshot" $ unsupportedOp -- Will implement for Haphaestus.
    ]) sessions
  where
    fail uuid'| Just _ <- ID.fromString uuid' = errJSON 404 "invalid session ID" $ (
        "Session " ++ uuid' ++ " was not found in active sessions")
        | otherwise = errJSON 404 "invalid session ID" $ ("UUID " ++ uuid' ++ " is not a valid UUID")

data Status = Status {ready :: Bool, message :: Text} deriving Generic
instance ToJSON Status
serveStatus = okJSON $ Status True "Always ready to open new sessions"

data NewSession = NewSession {sessionId :: UUID, capabilities :: Object} deriving Generic
instance ToJSON NewSession
postSession sessions = do
    method POST
    nullDir
    caps' <- getJSON
    case processCaps caps' of
        Just caps -> do
            (uuid, _) <- liftIO $ WD.createSession sessions caps
            okJSON $ NewSession uuid caps
        Nothing -> errJSON 500 "session not created" "Invalid capabilities specified!"

delSession sessions uuid = do
    method DELETE
    nullDir
    liftIO $ WD.delSession uuid sessions
    ok $ toResponse ()

getTimeout session = do
    method GET
    nullDir
    session' <- liftIO $ readMVar session
    okJSON $ WD.timeouts session'

setTimeout session = do
    method POST
    nullDir
    update' <- getJSON
    session' <- liftIO $ readMVar session
    case update' of
        Just (Object update) | Object current <- toJSON $ WD.timeouts session',
                Success new <- fromJSON $ toJSON $ M.union update current -> do
            liftIO $ swapMVar session session' {WD.timeouts = new}
            ok $ toResponse ()
        _ -> errJSON 400 "invalid argument" "Failed to parse JSON input"

data NavigateTo = NavigateTo { url :: String } deriving Generic
instance FromJSON NavigateTo
navigateTo session = do
    method POST
    nullDir
    target' <- getJSON
    case target' of
        Just target | Just url' <- URI.parseAbsoluteURI target -> do
            liftIO $ WD.load session url'
            ok $ toResponse ()
        Just target -> errJSON 400 "invalid argument" (target ++ " is not an absolute URL")
        Nothing -> errJSON 400 "invalid argument" "Failed to parse JSON input"

getURL session = do
    method GET
    nullDir
    session' <- liftIO $ readMVar session
    ok $ toResponse $ show $ WD.currentURL session'

reloadPage session = do
    method POST
    nullDir
    session' <- liftIO $ readMVar session
    liftIO $ WD.load' session $ WD.currentURL session'
    ok $ toResponse ()

sessionAction cb session = do
    method POST
    nullDir
    liftIO $ cb session
    ok $ toResponse ()

sessionTitle session = do
    method GET
    nullDir
    ret <- liftIO $ WDE.getTitle session
    ok $ toResponse ret

---- Windowing noops
getWindowHandle uuid = do
    method GET
    nullDir
    ok $ toResponse ("window-" ++ ID.toString uuid)

data WindowHandle = WindowHandle { handle :: String } deriving Generic
instance FromJSON WindowHandle
switchWindowHandle uuid = do
    method POST
    nullDir
    handle <- getJSON
    case handle of
        Nothing -> errJSON 400 "invalid argument" "Failed to parse JSON input"
        Just (WindowHandle handle') | handle' /= "window-" ++ ID.toString uuid ->
            errJSON 404 "no such window" "Rhapsode isn't a multi-window or multi-tab browser"
        _ -> ok $ toResponse ()

getWindowHandles uuid = do
    method GET
    nullDir
    okJSON ["window-" ++ ID.toString uuid]

noSuchFrame = do
    method POST
    nullDir
    errJSON 404 "no such frame" "Rhapsode doesn't support frames"

unsupportedOp = do
    nullDir
    errJSON 400 "unsupported operation" "Windowsize is meaningless to Rhapsode"

----

findAllFromRoot session = do
    method POST
    nullDir
    req <- getJSON
    session' <- liftIO $ readMVar session
    case req of
        Just req' -> case WDE.find req' $ XC.fromDocument $ WD.document session' of
            Right res -> okJSON =<< mapM (liftIO . WD.serializeEl session) res
            Left (True, msg) -> errJSON 400 "invalid selector" msg
            Left (False, msg) -> errJSON 400 "invalid argument" msg
        Nothing -> errJSON 400 "invalid argument" "Failed to parse JSON"

findFromRoot session = do
    method POST
    nullDir
    req <- getJSON
    session' <- liftIO $ readMVar session
    case req of
        Just req' -> case WDE.find req' $ XC.fromDocument $ WD.document session' of
            Right (res:_) -> okJSON =<< liftIO (WD.serializeEl session res)
            Right [] | WDE.Find using query <- req' -> errJSON 404 "No such element" (
                "No elements match " ++ unpack using ++ " query: " ++ query)
            Left (True, msg) -> errJSON 400 "invalid selector" msg
            Left (False, msg) -> errJSON 400 "invalid argument" msg
        Nothing -> errJSON 400 "invalid argument" "Failed to parse JSON"

serveElement session elUUID = do
    session' <- liftIO $ readMVar session
    case WD.getEl session' =<< ID.fromString elUUID of
        Just el -> msum [
            dir "element" $ findFromEl session el,
            dir "elements" $ findAllFromEl session el,
            dir "attribute" $ path $ getAttribute el,
            dir "property" $ path $ getAttribute el, -- Don't want to implement the DOM abomination!
            dir "css" $ path $ getStyle session' el,
            dir "text" $ getElText el,
            dir "name" $ getElName el,
            dir "rect" $ unsupportedOp, -- Will be meaningful for Haphaestus!
            dir "click" $ actionClickEl session el,
            dir "reset" $ actionResetEl session el,
            dir "value" $ actionTypeEl session el,
            dir "screenshot" unsupportedOp -- Will be meaningful for Haphaestus!
          ]
        Nothing | Nothing <- ID.fromString elUUID ->
            errJSON 404 "no such element" "Invalid UUID"
        Nothing -> errJSON 404 "no such element" "Unknown UUID."

findFromEl session el = do
    method POST
    nullDir
    req <- getJSON
    case req of
        Just req' -> case WDE.find req' el of
            Right (res:_) -> okJSON =<< liftIO (WD.serializeEl session res)
            Right [] | WDE.Find using query <- req' -> errJSON 404 "No such element" (
                "No child elements match " ++ unpack using ++ " query: " ++ query)
            Left (True, msg) -> errJSON 400 "invalid selector" msg
            Left (False, msg) -> errJSON 400 "invalid argument" msg
        Nothing -> errJSON 400 "invalid argument" "Failed to parse JSON"

findAllFromEl session el = do
    method POST
    nullDir
    req <- getJSON
    case req of
        Just req' -> case WDE.find req' el of
            Right res -> okJSON =<< mapM (liftIO . WD.serializeEl session) res
            Left (True, msg) -> errJSON 400 "invalid selector" msg
            Left (False, msg) -> errJSON 400 "invalid argument" msg
        Nothing -> errJSON 400 "invalid argument" "Failed to parse JSON"

noSuchEl = do
    method GET
    nullDir
    errJSON 404 "no such element" "Rhapsode does not have active elements."

getAttribute el name = do
    method GET
    nullDir
    ok $ toResponse $ case XC.node el of
        X.NodeElement (X.Element _ attrs _) ->
            fromMaybe "" $ M'.lookup (Str.fromString name) attrs
        _ -> ""

getStyle session el name = do
    method GET
    nullDir
    let res = M.lookupDefault [] name $ WDS.styleCursor session el
    ok $ toResponse $ CSS.serialize res

getElText el = do -- TODO allow CSS to impact the response.
    method GET
    nullDir
    ok $ toResponse $ Txt.concat $ XC.content el

getElName el = do
    method GET
    nullDir
    ok $ toResponse $ case XC.node el of
        X.NodeElement (X.Element name _ _) -> name2text name
        _ -> ""

name2text (X.Name name _ (Just prefix)) = Txt.concat [prefix, ":", name]
name2text (X.Name name _ Nothing) = name

actionClickEl session el = do
    method POST
    nullDir
    liftIO $ WD.clickEl session el
    ok $ toResponse ()

actionResetEl session el = do
    method POST
    nullDir
    liftIO $ WDF.clearForm session el
    ok $ toResponse ()

actionTypeEl session el = do
    method POST
    nullDir
    req <- getJSON
    case req of
        Just req' -> do
            liftIO $ WDF.sendText req' el session
            ok $ toResponse ()
        Nothing -> errJSON 400 "invalid argument" "Failed to parse JSON"

viewSource session = do
    method GET
    nullDir
    WD.Session { WD.document = doc } <- liftIO $ readMVar session
    ok $ toResponse $ X.renderLBS X.def doc

-- TODO Expose cookies from HURL, until then emulate Rhapsodes existing behaviour of not supporting cookies.
getCookies = do
    method GET
    nullDir
    let res :: [Value] = []
    okJSON res
getCookie :: String -> ServerPart Response
getCookie cookie = do
    method GET
    nullDir
    errJSON 404 "no such cookie" "Cookie access is as yet unsupported"
addCookie = do
    method POST
    nullDir
    ok $ toResponse ()
deleteCookie :: String -> ServerPart Response
deleteCookie cookie = do
    method DELETE
    nullDir
    ok $ toResponse ()
deleteCookies = do
    method DELETE
    nullDir
    ok $ toResponse ()

-- I don't ever open alerts, they're terrible UX!
handleAlert = do
    method POST
    nullDir
    errJSON 404 "no such alert" "Rhapsode doesn't open alerts."
alertText = do
    method [GET, POST]
    nullDir
    errJSON 404 "no such alert" "Rhapsode doesn't open alerts."