packages feed

webdriver-precore-0.2.0.0: src/WebDriverPreCore/Error.hs

{-# OPTIONS_HADDOCK hide #-}

module WebDriverPreCore.Error
  ( ErrorType (..),
    WebDriverException (..),
    JSONEncodeException (..),
    errorDescription,
    toErrorType,
    toErrorCode,
    parseWebDriverException,
    parseErrorType,
  )
where

import AesonUtils (jsonToText)
import Control.Exception (Exception (..))
import Data.Aeson (FromJSON (..), Options (..), Value, defaultOptions, genericParseJSON, withObject)
import Data.Aeson.Types (Parser, parseEither, parseMaybe, (.:))
import Data.Bifunctor (first)
import Data.Char (isUpper, toLower)
import Data.Function ((&))
import Data.Text as T (Text, concat, pack, toTitle, unpack, words)
import GHC.Generics (Generic)
import Text.Read (readEither)
import WebDriverPreCore.Internal.HTTPBidiCommon (JSUInt (..))
import Prelude as P hiding (error, words)

{-
Error Code 	HTTP Status 	JSON Error Code 	Description
element click intercepted 	400 	element click intercepted 	The Element Click command could not be completed because the element receiving the events is obscuring the element that was requested clicked.
element not interactable 	400 	element not interactable 	A command could not be completed because the element is not pointer- or keyboard interactable.
insecure certificate 	400 	insecure certificate 	Navigation caused the user agent to hit a certificate warning, which is usually the result of an expired or invalid TLS certificate.
invalid argument 	400 	invalid argument 	The arguments passed to a command are either invalid or malformed.
invalid cookie domain 	400 	invalid cookie domain 	An illegal attempt was made to set a cookie under a different domain than the current page.
invalid element state 	400 	invalid element state 	A command could not be completed because the element is in an invalid state, e.g. attempting to clear an element that isn't both editable and resettable.
invalid selector 	400 	invalid selector 	Argument was an invalid selector.
invalid session id 	404 	invalid session id 	Occurs if the given session id is not in the list of active sessions, meaning the session either does not exist or that it's not active.
javascript error 	500 	javascript error 	An error occurred while executing JavaScript supplied by the user.
move target out of bounds 	500 	move target out of bounds 	The target for mouse interaction is not in the browser's viewport and cannot be brought into that viewport.
no such alert 	404 	no such alert 	An attempt was made to operate on a modal dialog when one was not open.
no such cookie 	404 	no such cookie 	No cookie matching the given path name was found amongst the associated cookies of session's current browsing context's active document.
no such element 	404 	no such element 	An element could not be located on the page using the given search parameters.
no such frame 	404 	no such frame 	A command to switch to a frame could not be satisfied because the frame could not be found.
no such window 	404 	no such window 	A command to switch to a window could not be satisfied because the window could not be found.
no such shadow root 	404 	no such shadow root 	The element does not have a shadow root.
script timeout error 	500 	script timeout 	A script did not complete before its timeout expired.
session not created 	500 	session not created 	A new session could not be created.
stale element reference 	404 	stale element reference 	A command failed because the referenced element is no longer attached to the DOM.
detached shadow root 	404 	detached shadow root 	A command failed because the referenced shadow root is no longer attached to the DOM.
timeout 	500 	timeout 	An operation did not complete before its timeout expired.
unable to set cookie 	500 	unable to set cookie 	A command to set a cookie's value could not be satisfied.
unable to capture screen 	500 	unable to capture screen 	A screen capture was made impossible.
unexpected alert open 	500 	unexpected alert open 	A modal dialog was open, blocking this operation.
unknown command 	404 	unknown command 	A command could not be executed because the remote end is not aware of it.
unknown error 	500 	unknown error 	An unknown error occurred in the remote end while processing the command.
unknown method 	405 	unknown method 	The requested command matched a known URL but did not match any method for that URL.
unsupported operation 	500 	unsupported operation 	Indicates that a command that should have executed properly cannot be supported for some reason.
-}

-- | Known WevDriver Error Types
--
-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20251028/#errors)
data ErrorType
  = ElementClickIntercepted
  | ElementNotInteractable
  | InsecureCertificate
  | InvalidArgument
  | InvalidCookieDomain
  | InvalidElementState
  | InvalidSelector
  | InvalidSessionId
  | JavascriptError
  | MoveTargetOutOfBounds
  | NoSuchAlert
  | NoSuchCookie
  | NoSuchElement
  | NoSuchFrame
  | NoSuchWindow
  | NoSuchShadowRoot
  | ScriptTimeout
  | SessionNotCreated
  | StaleElementReference
  | DetachedShadowRoot
  | Timeout
  | UnableToSetCookie
  | UnableToCaptureScreen
  | UnexpectedAlertOpen
  | UnknownCommand
  | UnknownError
  | UnknownMethod
  | UnsupportedOperation
  | --- Bidi
    InvalidWebExtension
  | NoSuchClientWindow
  | NoSuchHandle
  | NoSuchHistoryEntry
  | NoSuchNetworkCollector
  | NoSuchIntercept
  | NoSuchNetworkData
  | NoSuchNode
  | NoSuchRequest
  | NoSuchScript
  | NoSuchStoragePartition
  | NoSuchUserContext
  | NoSuchWebExtension
  | UnableToCloseBrowser
  | UnableToSetFileInput
  | UnavailableNetworkData
  | UnderspecifiedStoragePartition
  deriving (Eq, Read, Show, Ord, Bounded, Enum)

instance FromJSON ErrorType where
  parseJSON :: Value -> Parser ErrorType
  parseJSON = withObject "ErrorType" $ \o -> do
    errCode <- o .: "error"
    case toErrorType errCode of
      Left e -> fail $ "Unknown ErrorType code: " <> unpack e
      Right et -> pure et

toErrorType :: Text -> Either Text ErrorType
toErrorType =
  first pack . readEither . unpack . T.concat . fmap T.toTitle . T.words

toErrorCode :: ErrorType -> Text
toErrorCode =
  pack . P.unwords . splitCamel . show
  where
    splitCamel :: String -> [String]
    splitCamel = \case
      [] -> []
      (x : xs) ->
        let (w, rest) = break isUpper xs
         in (toLower x : w) : splitCamel rest

errorDescription :: ErrorType -> Text
errorDescription = \case
  ElementClickIntercepted -> "The Element Click command could not be completed because the element receiving the events is obscuring the element that was requested clicked"
  ElementNotInteractable -> "A command could not be completed because the element is not pointer- or keyboard interactable"
  InsecureCertificate -> "Navigation caused the user agent to hit a certificate warning, which is usually the result of an expired or invalid TLS certificate"
  InvalidArgument -> "The arguments passed to a command are either invalid or malformed"
  InvalidCookieDomain -> "An illegal attempt was made to set a cookie under a different domain than the current page"
  InvalidElementState -> "A command could not be completed because the element is in an invalid state, e.g. attempting to clear an element that isn't both editable and resettable"
  InvalidSelector -> "Argument was an invalid selector"
  InvalidSessionId -> "Occurs if the given session id is not in the list of active sessions, meaning the session either does not exist or that it's not active"
  JavascriptError -> "An error occurred while executing JavaScript supplied by the user"
  MoveTargetOutOfBounds -> "The target for mouse interaction is not in the browser's viewport and cannot be brought into that viewport"
  NoSuchAlert -> "An attempt was made to operate on a modal dialog when one was not open"
  NoSuchCookie -> "No cookie matching the given path name was found amongst the associated cookies of session's current browsing context's active document"
  NoSuchElement -> "An element could not be located on the page using the given search parameters"
  NoSuchFrame -> "A command to switch to a frame could not be satisfied because the frame could not be found"
  NoSuchWindow -> "A command to switch to a window could not be satisfied because the window could not be found"
  NoSuchShadowRoot -> "The element does not have a shadow root"
  ScriptTimeout -> "A script did not complete before its timeout expired"
  SessionNotCreated -> "A new session could not be created"
  StaleElementReference -> "A command failed because the referenced element is no longer attached to the DOM"
  DetachedShadowRoot -> "A command failed because the referenced shadow root is no longer attached to the DOM"
  Timeout -> "An operation did not complete before its timeout expired"
  -- Description from the BiDi spec differs from WebDriver spec
  -- UnableToSetCookie -> "A command to set a cookie's value could not be satisfied."
  UnableToSetCookie -> "Tried to create a cookie, but the user agent rejected it"
  UnableToCaptureScreen -> "A screen capture was made impossible"
  UnexpectedAlertOpen -> "A modal dialog was open, blocking this operation"
  UnknownCommand -> "A command could not be executed because the remote end is not aware of it"
  UnknownError -> "An unknown error occurred in the remote end while processing the command"
  UnknownMethod -> "The requested command matched a known URL but did not match any method for that URL"
  UnsupportedOperation -> "Indicates that a command that should have executed properly cannot be supported for some reason"
  -- Bidi
  InvalidWebExtension -> "Tried to install an invalid web extension"
  NoSuchClientWindow -> "Tried to interact with an unknown client window"
  NoSuchNetworkCollector -> "Tried to remove an unknown collector"
  NoSuchHandle -> "Tried to deserialize an unknown RemoteObjectReference"
  NoSuchHistoryEntry -> "Tried to navigate to an unknown session history entry"
  NoSuchIntercept -> "Tried to remove an unknown network intercept"
  NoSuchNetworkData -> "Tried to reference an unknown network data"
  NoSuchNode -> "Tried to deserialize an unknown SharedReference"
  NoSuchRequest -> "Tried to continue an unknown request"
  NoSuchScript -> "Tried to remove an unknown preload script"
  NoSuchStoragePartition -> "Tried to access data in a non-existent storage partition"
  NoSuchUserContext -> "Tried to reference an unknown user context"
  NoSuchWebExtension -> "Tried to reference an unknown web extension"
  UnableToCloseBrowser -> "Tried to close the browser, but failed to do so"
  UnableToSetFileInput -> "Tried to set a file input, but failed to do so"
  UnavailableNetworkData -> "Tried to get network data which was not collected or already evicted"
  UnderspecifiedStoragePartition -> "Tried to interact with data in a storage partition which was not adequately specified"

data WebDriverException
  = ResponseParseException
      { message :: Text,
        response :: Value
      }
  | UnrecognisedErrorTypeException {errorType :: Text, response :: Value}
  | JSONEncodeException JSONEncodeException
  | ProtocolException
      { error :: ErrorType,
        description :: Text,
        message :: Text,
        stacktrace :: Maybe Text,
        errorData :: Maybe Value,
        response :: Value
      }
  deriving (Eq, Show, Ord, Generic)

data JSONEncodeException = MkJSONEncodeException
  { message :: Text,
    responseText :: Text
  }
  deriving (Show, Eq, Ord, Generic)

instance FromJSON JSONEncodeException

instance Exception WebDriverException where
  displayException :: WebDriverException -> String
  displayException =
    unpack . \case
      ResponseParseException {message, response} ->
        "Error parsing WebDriver response: "
          <> message
          <> "\nResponse was:\n"
          <> jsonToText response
      UnrecognisedErrorTypeException {response} ->
        "Unrecognised WebDriver error type in response:\n"
          <> jsonToText response
      JSONEncodeException MkJSONEncodeException {message, responseText} ->
        "Error converting WebDriver response to JSON: "
          <> message
          <> "\nResponse text was:\n"
          <> responseText
      ProtocolException {error, description, message, stacktrace} ->
        "Error executing WebDriver command: "
          <> "\nError Type: "
          <> toErrorCode error
          <> "\nDescription: "
          <> description
          <> "\nMessage: "
          <> message
          <> maybe "" (\st -> "\nStacktrace: \n" <> st) stacktrace


parseWebDriverException :: Text -> Value -> WebDriverException
parseWebDriverException errInfo response =
  parseMaybe getErrorProp response
    & maybe
      (parserErr (errInfo <> "\n" <> "Could not find 'error' property in response\n" <> jsonToText response))
      ( either
          (flip UnrecognisedErrorTypeException response)
          mkWebDriverException
          . toErrorType
      )
  where
    parserErr message = ResponseParseException {message, response}
    mkWebDriverException :: ErrorType -> WebDriverException {-  -}
    mkWebDriverException et =
      parseEither parseJSON response
        & either
          (\msg -> parserErr $ "Error object parsing failed:\n" <> pack msg <> "\n" <> jsonToText response)
          \MkWebDriverExceptionRaw {..} ->
            ProtocolException
              { error = et,
                description = errorDescription et,
                response,
                message,
                stacktrace,
                errorData
              }

getErrorProp :: Value -> Parser Text
getErrorProp =
  withObject "error" (.: "error")

parseErrorType :: Value -> Maybe ErrorType
parseErrorType resp =
  case parseWebDriverException "parse error type result" resp of
    ProtocolException {error} -> Just error
    JSONEncodeException {} -> Nothing
    ResponseParseException {} -> Nothing
    UnrecognisedErrorTypeException {} -> Nothing

data WebDriverExceptionRaw = MkWebDriverExceptionRaw
  { error :: Text,
    message :: Text,
    biDiId :: Maybe JSUInt,
    stacktrace :: Maybe Text,
    errorData :: Maybe Value
  }
  deriving (Show, Eq, Generic)

instance FromJSON WebDriverExceptionRaw where
  parseJSON :: Value -> Parser WebDriverExceptionRaw
  parseJSON =
    genericParseJSON
      defaultOptions
        { omitNothingFields = True,
          fieldLabelModifier = \case
            "data" -> "errorData"
            "id" -> "biDiId"
            other -> other
        }

{- FROM BIDI

- get compiling
   check empty result
- try known missign Bidi demos
- review unit tests
- a couple of integration demos
  - http - element does not exist
  - bidi specific - element does not exist

data DriverError = MkDriverError
  { id :: Maybe JSUInt,
    error :: ErrorCode,
    description :: Text,
    message :: Text,
    stacktrace :: Maybe Text,
    extensions :: EmptyResult
  }
  deriving (Show, Generic, Eq)

instance FromJSON DriverError where
  parseJSON :: Value -> Parser DriverError
  parseJSON = withObject "Driver Error" $ \o -> do
    id' <- o .: "id"
    error' <- o .: "error"
    message <- o .: "message"
    stacktrace <- o .: "stacktrace"
    pure $
      MkDriverError
        { id = id',
          error = error',
          description = errorDescription error',
          message,
          stacktrace,
          extensions =
            MkEmptyResult $
              subtractProps
                [ "id",module ErrorCoverageTest where

                  "type",
                  "error",
                  "description",
                  "message",
                  "stacktrace"
                ]
                o
        }
-}