packages feed

webdriver-precore (empty) → 0.0.0.2

raw patch · 14 files changed

+4161/−0 lines, 14 filesdep +aesondep +aeson-prettydep +base

Dependencies added: aeson, aeson-pretty, base, bytestring, containers, falsify, ghc, pretty-show, raw-strings-qq, tasty, tasty-discover, tasty-hunit, tasty-quickcheck, text, vector, webdriver-precore

Files

+ ChangeLog.md view
@@ -0,0 +1,7 @@+# webdriver-precore-??.??.??.?? (????-??-??) - Unreleased++# webdriver-precore-0.0.0.2 (2025-04-21)++The initial release of this library.++See [README](README.md) for details
+ LICENSE view
@@ -0,0 +1,28 @@+BSD 3-Clause License++Copyright (c) 2025, pyrethrum++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++1. Redistributions of source code must retain the above copyright notice, this+   list of conditions and the following disclaimer.++2. Redistributions in binary form must reproduce the above copyright notice,+   this list of conditions and the following disclaimer in the documentation+   and/or other materials provided with the distribution.++3. Neither the name of the copyright holder nor the names of its+   contributors may be used to endorse or promote products derived from+   this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,318 @@+# webdriver-precore++- [webdriver-precore](#webdriver-precore)+  - [What is This Library?](#what-is-this-library)+  - [Why This Library?](#why-this-library)+    - [Core Principles](#core-principles)+    - [Library Non-Goals](#library-non-goals)+    - [Acknowledgements](#acknowledgements)+- [Minimal Example](#minimal-example)+  - [1. Implementing a runner](#1-implementing-a-runner)+      - [Main Types (Used in the Runner)](#main-types-used-in-the-runner)+        - [W3Spec](#w3spec)+        - [HttpResponse](#httpresponse)+      - [The Runner](#the-runner)+    - [1.1 Convert W3Spec to params for req](#11-convert-w3spec-to-params-for-req)+    - [1.2 Call the WebDriver](#12-call-the-webdriver)+    - [1.3 Parse HttpResponse Using the Parser Provided in the W3Spec](#13-parse-httpresponse-using-the-parser-provided-in-the-w3spec)+    - [2. Applying the Runner to the W3Spec Functions](#2-applying-the-runner-to-the-w3spec-functions)+    - [3. Install a Vendor Provided WebDriver](#3-install-a-vendor-provided-webdriver)+    - [4. Launch WebDriver From the Terminal](#4-launch-webdriver-from-the-terminal)+    - [5. Drive the Browser Via the IO API](#5-drive-the-browser-via-the-io-api)++## What is This Library?++This library provides a minimal abstraction over the [WebDriver W3C Protocol endpoints](https://www.w3.org/TR/webdriver2/) without providing any implementation. It provides a description of the W3C API as a list of functions that return a [W3Spec type](#w3spec). The intention is that other libraries will provide the actual implementation.++You can not use this library directly to drive a browser. If you are looking for a fully featured library to drive a browser, you may be interested in an alternative library such as [haskell-webdriver](https://hackage.haskell.org/package/webdriver), a Selenium 2 client that is actively maintained.++## Why This Library?++Several libraries provide WebDriver bindings for Haskell. However, when development on this library began, the existing options were either unmaintained, dependent on Selenium, or tightly coupled with larger "batteries included" testing frameworks.++We, the authors of this library, are building our own standalone test framework. To support browser based testing within this framework we're first creating a series of independent low-level libraries. This library is the first in that series. Our aim is to make each of our low level libraries broadly useful to others, outside its use within our own framework. ++### Core Principles+- **Direct W3C WebDriver Implementation**  +  - No Selenium dependency  +  - Full control over protocol handling  +  *Note: the [W3C WebDriver standard](https://www.w3.org/TR/webdriver2/) is an initiative driven largely by the core Selenium contributors. It provides a uniform HTTP API to drive browsers, and can be leveraged by any library, including Selenium.*++- **Minimalist Design**  +  - Boring Haskell+  - Few external dependencies  ++- **Enable a Layered Architecture**  +  - Provide an unopinionated WebDriver client for use in higher level libraries++### Library Non-Goals+  +  * Any convenience or utility functions, that do not directly correspond to an endpoint on the WC3 spec. Such functions belong in downstream libraries.+  * Any transformers, effects or similar abstractions. These too belong downstream.++### Acknowledgements++This library would not have been possible without the prior work in: ++**Haskell (particularly)**:+* [haskell-webdriver](https://hackage.haskell.org/package/webdriver)+* [webdriver-w3c](https://hackage.haskell.org/package/webdriver-w3c)++**Selenium and WebDriver Standards**:++The decade+ efforts of the [Selenium](https://www.selenium.dev/) maintainers, both in forging the way with Selenium and their ongoing work in the development of the [W3C standards](https://www.w3.org/TR/webdriver2/) +++# Minimal Example++*TLDR ~ bring your own HTTP client and use it to implement the endpoints as defined in this library.*++Driving a browser using this library requires the following:+1. Implement a `runner` that takes a [W3Spec](#w3spec) and makes HTTP calls an active WebDriver instance+2. Create an IO API by applying the `runner` to each of the endpoint functions in this library+3. Install the desired browser and browser driver+4. Run the driver+5. Drive the browser via the IO API++The full source can be found in the [example project repo](https://github.com/pyrethrum/webdriver/tree/jw-busywork/webdriver-examples/driver-demo-e2e).+++## 1. Implementing a runner++The first step in writing a WebDriver implementation is to choose an HTTP library. In this example, the chosen library is [req](https://hackage.haskell.org/package/req).++Then to implement a run function requires the following:++1. Transform a [W3Spec](#w3spec) to RequestParams compatible with the chosen HTTP library.+2. Make an HTTP call to WebDriver as per the RequestParams and return a simplified [HttpResponse](#httpresponse).+3. Use the parser provided by the [W3Spec](#w3spec) to parse the [HttpResponse](#httpresponse) and handle any errors.++#### Main Types (Used in the Runner)++The two most important types in this library are:++##### W3Spec++The `W3Spec` returned by each of this library's endpoint functions. This type represents a driver endpoint.++```haskell+data W3Spec a+  = Get+      { description :: Text,+        path :: UrlPath,+        parser :: HttpResponse -> Result a+      }+  | Post+      { description :: Text,+        path :: UrlPath,+        body :: Value,+        parser :: HttpResponse -> Result a+      }+  | PostEmpty+      { description :: Text,+        path :: UrlPath,+        parser :: HttpResponse -> Result a+      }+  | Delete+      { description :: Text,+        path :: UrlPath,+        parser :: HttpResponse -> Result a+      }+```++##### HttpResponse++`HttpResponse` is consumed by the `parser` provided by this library and needs to be constructed by the `runner`++```haskell+data HttpResponse = MkHttpResponse+  { -- | HTTP status code.+    statusCode :: Int,+    -- | HTTP status message.+    statusMessage :: Text,+    -- | Response body in JSON format.+    body :: Value+  }+```++#### The Runner++[source](https://github.com/pyrethrum/webdriver/blob/main/webdriver-examples/driver-demo-e2e/IORunner.hs)+++```haskell+run :: W3Spec a -> IO a+run spec = do+  -- 1. Convert W3Spec to params for req+  let request = mkRequest spec+  -- 2. Call WebDriver server (via req) and return a simplified HttpResponse +  response <- callReq request+  -- 3. Apply the W3Spec parser to the HttpResponse get result type and handle errors  +  parseResponse spec response  +```++### 1.1 Convert W3Spec to params for req++*W3Spec -> ReqRequestParams*++```haskell+-- A custom data type specific to req+data ReqRequestParams where+  MkRequestParams ::+    (HttpBodyAllowed (AllowsBody method) (ProvidesBody body), HttpMethod method, HttpBody body) =>+    { url :: Url 'Http,+      method :: method,+      body :: body,+      port :: Int+    } ->+    ReqRequestParams++-- W3Spec -> ReqRequestParams+-- the url and port would not normally be hard coded+mkRequest :: forall a. W3Spec a -> ReqRequestParams+mkRequest spec = case spec of+  Get {} -> MkRequestParams url GET NoReqBody 4444 +  Post {body} -> MkRequestParams url POST (ReqBodyJson body) 4444+  PostEmpty {} -> MkRequestParams url POST (ReqBodyJson $ object []) 4444+  Delete {} -> MkRequestParams url DELETE NoReqBody 4444+  where+    url =  foldl' (/:) (http "127.0.0.1") spec.path.segments+```++### 1.2 Call the WebDriver++*ReqRequestParams -> HttpResponse*++```haskell+callReq :: ReqRequestParams -> IO HttpResponse+callReq MkRequestParams {url, method, body, port = prt} =+  runReq defaultHttpConfig {httpConfigCheckResponse = \_ _ _ -> Nothing} $ do+    r <- req method url body jsonResponse $ port prt+    pure $+      MkHttpResponse+        { statusCode = responseStatusCode r,+          statusMessage = responseStatusText r,+          body = responseBody r :: Value+        }+  where+    responseStatusText = decodeUtf8Lenient . responseStatusMessage+```++### 1.3 Parse HttpResponse Using the Parser Provided in the [W3Spec](#w3spec)++*HttpResponse -> Return Type*++```haskell+-- in this implementation we are just throwing exceptions on failure+parseResponse :: W3Spec a -> HttpResponse -> IO a+parseResponse spec r =+  spec.parser r+    & \case+      Error msg -> fail $ parseWebDriverError r & \case+          e@NotAnError {} -> unpack spec.description <> "\n" <> +                            "Failed to parse response:\n " <> msg <> "\nin response:" <> show e+          e@UnrecognisedError {} -> "UnrecognisedError:\n " <> "\nin response:" <> show e+          e@WebDriverError {} -> "WebDriver error thrown:\n " <> show e+      Success a -> pure a+```++### 2. Applying the Runner to the W3Spec Functions++*Create an IO API by applying run to each endpoint definition exposed by this library*++The full source for can be found in the [example project repo](https://github.com/pyrethrum/webdriver/blob/main/webdriver-examples/driver-demo-e2e/IOAPI.hs).+```haskell+module IOAPI where ++import Data.Aeson (Value)+import Data.Text  as T (Text)+import WebDriverPreCore (DriverStatus, ElementId, Selector, SessionId)+import WebDriverPreCore qualified as W+import Prelude hiding (log)+import IOUtils (sleepMs, encodeFileToBase64)+import IORunner (run)++status :: IO DriverStatus+status = run W.status++newSession :: W.FullCapabilities -> IO SessionId+newSession = run . W.newSession++getTimeouts :: SessionId -> IO W.Timeouts+getTimeouts = run . W.getTimeouts++setTimeouts :: SessionId -> W.Timeouts -> IO ()+setTimeouts s = run . W.setTimeouts s++getCurrentUrl :: SessionId -> IO Text+getCurrentUrl = run . W.getCurrentUrl++getTitle :: SessionId -> IO Text+getTitle = run . W.getTitle++maximizeWindow :: SessionId -> IO W.WindowRect+maximizeWindow = run . W.maximizeWindow++-- ... and 50+ more API functions+```++### 3. Install a Vendor Provided WebDriver++*Once all the required endpoints are implemented you will be able to interact with browsers via WebDriver*++Examples:+  1. [Firefox](https://github.com/mozilla/geckodriver/releases)+  2. [Chrome](https://googlechromelabs.github.io/chrome-for-testing/)+  3. [Edge](https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver?form=MA13LH)+  4. [Opera](https://github.com/operasoftware/operachromiumdriver?tab=readme-ov-file)+  5. [Safari](https://developer.apple.com/documentation/webkit/testing-with-webdriver-in-safari)++*Ensure the corresponding browser is installed on your system*++### 4. Launch WebDriver From the Terminal++e.g. For Firefox and geckodriver on Linux or WSL you could start geckodriver from the terminal as follows: ++*Note: we are setting the port to 4444, which is the hard coded port in our example.*++```bash+> pkill -f geckodriver || true  && geckodriver --port=4444 &+```+or with logging:++```bash+> pkill -f geckodriver || true  && geckodriver --log trace --port=4444 &+```++or similarly for Chrome and chromedriver:++```bash+> pkill -f chromedriver || true && chromedriver --port=4444 &+```++or with logging:++```bash+> pkill -f chromedriver || true && chromedriver --log-level=ALL --port=4444 &+```++### 5. Drive the Browser Via the IO API++*With the driver running you can now run code that interacts with the browser:*++Full source file can be found in the [example project repo](https://github.com/pyrethrum/webdriver/blob/main/webdriver-examples/driver-demo-e2e/WebDriverE2EDemoTest.hs).++```haskell+demoForwardBackRefresh :: IO ()+demoForwardBackRefresh = do+  ses <- newSession $ minFullCapabilities Firefox+  navigateTo ses "https://the-internet.herokuapp.com/"+  link findElement ses $ CSS "#content ul:nth-child(4) > li:nth-child(6) > a:nth-child(1)"+  elementClick ses link+  back ses+  forward ses+  refresh ses+  deleteSession ses+```++*This is a minimal API. There is plenty of scope to build on this to provide more constrained types, user-friendly functions and capabilities such as retries, and session and driver management.*
+ src/WebDriverPreCore.hs view
@@ -0,0 +1,150 @@+module WebDriverPreCore+  ( +    -- ** The W3Spec Type+    module WC3Spec,++    -- ** Root Methods+    module RootMethods,++    -- ** Session Methods+    -- | /See also 'newSession' and 'newSession''/+    module SessionMethods,++    -- ** Window Methods+    module WindowMethods,++    -- ** Frame Methods+    module FrameMethods,++    -- ** Element(s) Methods+    module ElementMethods,++    -- ** Element Instance Methods+    module ElementInstanceMethods,++    -- ** Shadow DOM Methods+    module ShadowDOMMethods,++    -- * HTTP Response+    module WebDriverPreCore.HttpResponse,++    -- * Capabilities+    module CoreCapabilities,+    module WebDriverPreCore.Capabilities,++    -- * Errors+    module WebDriverPreCore.Error,++    -- * Action Types+    module ActionTypes,++    -- * Auxiliary Spec Types+    module AuxTypes,+  )+where++import WebDriverPreCore.Capabilities as CoreCapabilities (FullCapabilities(..), Capabilities(..) )+import WebDriverPreCore.Capabilities+import WebDriverPreCore.Error+import WebDriverPreCore.HttpResponse+import WebDriverPreCore.SpecDefinition as ActionTypes+  ( Action (..),+    Actions (..),+    KeyAction (..),+    Pointer (..),+    PointerAction (..),+    PointerOrigin (..),+    WheelAction (..),+  )+import WebDriverPreCore.SpecDefinition as AuxTypes+  ( Cookie (..),+    DriverStatus (..),+    ElementId (..),+    FrameReference (..),+    HttpResponse (..),+    SameSite (..),+    Selector (..),+    SessionId (..),+    Timeouts (..),+    UrlPath (..),+    WindowHandle (..),+    WindowHandleSpec (..),+    WindowRect (..),+  )+import WebDriverPreCore.SpecDefinition as ElementInstanceMethods+  ( elementClear,+    elementClick,+    elementSendKeys,+    findElementFromElement,+    findElementsFromElement,+    getElementAttribute,+    getElementComputedLabel,+    getElementComputedRole,+    getElementCssValue,+    getElementProperty,+    getElementRect,+    getElementShadowRoot,+    getElementTagName,+    getElementText,+    isElementEnabled,+    isElementSelected,+    takeElementScreenshot,+  )+import WebDriverPreCore.SpecDefinition as ElementMethods+  ( findElement,+    findElements,+    getActiveElement,+  )+import WebDriverPreCore.SpecDefinition as FrameMethods (switchToParentFrame)+import WebDriverPreCore.SpecDefinition as RootMethods (newSession, newSession', status)+import WebDriverPreCore.SpecDefinition as SessionMethods+  ( acceptAlert,+    addCookie,+    back,+    closeWindow,+    deleteAllCookies,+    deleteCookie,+    deleteSession,+    dismissAlert,+    executeScript,+    executeScriptAsync,+    forward,+    fullscreenWindow,+    getAlertText,+    getAllCookies,+    getCurrentUrl,+    getNamedCookie,+    getPageSource,+    getTimeouts,+    getTitle,+    getWindowHandle,+    getWindowHandles,+    getWindowRect,+    maximizeWindow,+    minimizeWindow,+    navigateTo,+    newWindow,+    performActions,+    printPage,+    refresh,+    releaseActions,+    sendAlertText,+    setTimeouts,+    setWindowRect,+    switchToFrame,+    switchToWindow,+    takeScreenshot,+  )+import WebDriverPreCore.SpecDefinition as ShadowDOMMethods (findElementFromShadowRoot, findElementsFromShadowRoot)+import WebDriverPreCore.SpecDefinition as WC3Spec (W3Spec (..))+import WebDriverPreCore.SpecDefinition as WindowMethods+  ( closeWindow,+    fullscreenWindow,+    getWindowHandles,+    getWindowRect,+    maximizeWindow,+    minimizeWindow,+    newWindow,+    setWindowRect,+    switchToWindow,+  )
+ src/WebDriverPreCore/Capabilities.hs view
@@ -0,0 +1,730 @@+{-# OPTIONS_HADDOCK hide #-}++module WebDriverPreCore.Capabilities+  ( FullCapabilities (..),+    Capabilities (..),+    UnhandledPromptBehavior (..),+    PageLoadStrategy (..),+    BrowserName (..),+    PlatformName (..),+    Proxy (..),+    Timeouts (..),+    VendorSpecific (..),+    SocksProxy (..),+    PerfLoggingPrefs (..),+    MobileEmulation (..),+    LogLevel (..),+    LogSettings (..),+    DeviceMetrics (..),+    alwaysMatchCapabilities,+    minCapabilities,+    minFullCapabilities,+    minFirefoxCapabilities,+    minChromeCapabilities,+  )+where++import Control.Applicative (Applicative (..), asum)+import Control.Monad (Monad ((>>=)), MonadFail (..))+import Data.Aeson+  ( FromJSON (parseJSON),+    Key,+    KeyValue ((.=)),+    Object,+    ToJSON (toJSON),+    Value,+    defaultOptions,+    genericParseJSON,+    genericToJSON,+    object,+    withObject,+    withText,+    (.:),+    (.:?),+  )+import Data.Aeson.Key (fromText)+import Data.Aeson.Types+  ( Pair,+    Parser,+    Value (..),+    omitNothingFields,+    parseField,+    parseFieldMaybe,+  )+import Data.Bool (Bool (..))+import Data.Enum (Enum)+import Data.Eq (Eq)+import Data.Function (($), (.), flip)+import Data.Functor ((<$>))+import Data.Int (Int)+import Data.Map.Strict (Map)+import Data.Maybe (Maybe (..), catMaybes, maybe)+import Data.Semigroup (Semigroup (..))+import Data.Text (Text)+import Data.Vector (fromList)+import GHC.Enum (Bounded)+import GHC.Float (Double)+import GHC.Generics (Generic)+import GHC.IO (FilePath)+import GHC.Show (Show (..))+import WebDriverPreCore.Internal.Utils (opt)++{- references:+- https://https://www.w3.org/TR/2025/WD-webdriver2-20250306/#capabilities++ - https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities+ - https://mucsi96.gitbook.io/w3c-webdriver/capabilities++ -}++-- | 'FullCapabilities' is the object that is passed to webdriver to define the properties of the session via the 'Spec.newSession' function.+--   It is a combination of 'alwaysMatch' and 'firstMatch' properties.+--+--   [spec](https://https://www.w3.org/TR/2025/WD-webdriver2-20250306/#capabilities)+--+--   See also: 'Capabilities' and related constructors such as 'minCapabilities', 'minFullCapabilities', 'minFirefoxCapabilities' and 'minChromeCapabilities'+data FullCapabilities = MkFullCapabilities+  { -- | capabilities that are always matched+    alwaysMatch :: Maybe Capabilities,+    -- | a list of capabilities that are matched in order, the first matching capabilities that matches the capabilities of the session is used+    firstMatch :: [Capabilities]+  }+  deriving (Show, Generic)++instance ToJSON FullCapabilities where+  toJSON :: FullCapabilities -> Value+  toJSON MkFullCapabilities {alwaysMatch, firstMatch} =+    object $+      [ "capabilities" .= (object $ catMaybes [opt "alwaysMatch" $ alwaysMatch]),+        "firstMatch" .= firstMatch'+      ]+    where+      firstMatch' :: Value+      firstMatch' = Array . fromList $ toJSON <$> firstMatch++instance FromJSON FullCapabilities where+  parseJSON :: Value -> Parser FullCapabilities+  parseJSON =+    withObject "FullCapabilities" $ \v ->+      do+        capabilities <- v .: "capabilities"+        alwaysMatch <- capabilities .:? "alwaysMatch"+        firstMatch <- capabilities .: "firstMatch"+        pure MkFullCapabilities {..}++-- | Returns the minimal FullCapabilities object where the 'firstMatch' property is empty.+--+-- It is very common for 'alwaysMatch' to be the only field populated and the 'firstMatch' field to be empty. +--+-- [spec](https://https://www.w3.org/TR/2025/WD-webdriver2-20250306/#capabilities)+alwaysMatchCapabilities :: Capabilities -> FullCapabilities+alwaysMatchCapabilities = flip MkFullCapabilities [] . Just++-- | Returns the minimal FullCapabilities object for a given browser+-- The browserName in the 'alwaysMatch' field is the only field populated+-- [spec](https://https://www.w3.org/TR/2025/WD-webdriver2-20250306/#capabilities)+minFullCapabilities :: BrowserName -> FullCapabilities+minFullCapabilities  =  alwaysMatchCapabilities . minCapabilities ++-- | Returns the minimal Capabilities object for a given browser+-- The browserName is the only field populated+-- [spec](https://https://www.w3.org/TR/2025/WD-webdriver2-20250306/#capabilities)+minCapabilities :: BrowserName -> Capabilities+minCapabilities browserName =+  MkCapabilities+    { browserName = Just browserName,+      browserVersion = Nothing,+      platformName = Nothing,+      acceptInsecureCerts = Nothing,+      pageLoadStrategy = Nothing,+      proxy = Nothing,+      timeouts = Nothing,+      strictFileInteractability = Nothing,+      unhandledPromptBehavior = Nothing,+      vendorSpecific = Nothing+    }++-- | Returns the minimal FullCapabilities object for Firefox+minFirefoxCapabilities :: FullCapabilities+minFirefoxCapabilities = minFullCapabilities Firefox++-- | Returns the minimal FullCapabilities object for Chrome+minChromeCapabilities :: FullCapabilities+minChromeCapabilities = minFullCapabilities Chrome++-- Custom Types for Enums++-- | [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#capabilities)+data UnhandledPromptBehavior+  = Dismiss+  | Accept+  | DismissAndNotify+  | AcceptAndNotify+  | Ignore+  deriving (Show, Generic, Enum, Bounded, Eq)++-- | [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#capabilities)+data PageLoadStrategy+  = None'+  | Eager+  | Normal+  deriving (Show, Generic, Enum, Bounded, Eq)++instance ToJSON PageLoadStrategy where+  toJSON :: PageLoadStrategy -> Value+  toJSON = \case+    None' -> "none"+    Eager -> "eager"+    Normal -> "normal"++instance FromJSON PageLoadStrategy where+  parseJSON :: Value -> Parser PageLoadStrategy+  parseJSON = withText "PageLoadStrategy" $ \case+    "none" -> pure None'+    "eager" -> pure Eager+    "normal" -> pure Normal+    _ -> fail "Invalid PageLoadStrategy"++-- | [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#capabilities)+data BrowserName+  = Chrome+  | Firefox+  | Safari+  | Edge+  | InternetExplorer+  deriving (Show, Generic, Enum, Bounded, Eq)++-- | [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#capabilities)+data PlatformName+  = Windows+  | Mac+  | Linux+  | Android+  | IOS+  deriving (Show, Generic, Enum, Bounded, Eq)++-- | 'Capabilities' define the properties of the session and are passed to the webdriver+-- via fields of the 'FullCapabilities' object.+--+-- [spec](https://https://www.w3.org/TR/2025/WD-webdriver2-20250306/#capabilities)+--+-- See also: 'FullCapabilities' and related constructors such as: 'minCapabilities',+--   'minFullCapabilities',  'minFirefoxCapabilities' and 'minChromeCapabilities'+data Capabilities = MkCapabilities+  { browserName :: Maybe BrowserName,+    browserVersion :: Maybe Text,+    platformName :: Maybe PlatformName,+    acceptInsecureCerts :: Maybe Bool,+    pageLoadStrategy :: Maybe PageLoadStrategy,+    proxy :: Maybe Proxy,+    timeouts :: Maybe Timeouts,+    strictFileInteractability :: Maybe Bool,+    unhandledPromptBehavior :: Maybe UnhandledPromptBehavior,+    vendorSpecific :: Maybe VendorSpecific+  }+  deriving (Show, Generic, Eq)++instance ToJSON Capabilities where+  toJSON :: Capabilities -> Value+  toJSON+    MkCapabilities+      { browserName,+        browserVersion,+        platformName,+        acceptInsecureCerts,+        pageLoadStrategy,+        proxy,+        timeouts,+        strictFileInteractability,+        unhandledPromptBehavior,+        vendorSpecific+      } =+      object $+        [ "browserName" .= browserName+        ]+          <> catMaybes+            [ opt "browserVersion" browserVersion,+              opt "platformName" platformName,+              opt "acceptInsecureCerts" acceptInsecureCerts,+              opt "pageLoadStrategy" pageLoadStrategy,+              opt "proxy" proxy,+              opt "timeouts" timeouts,+              opt "strictFileInteractability" strictFileInteractability,+              opt "unhandledPromptBehavior" unhandledPromptBehavior+            ]+          <> vendorSpecificToJSON vendorSpecific++instance FromJSON Capabilities where+  parseJSON :: Value -> Parser Capabilities+  parseJSON = withObject "Capabilities" $ \v ->+    do+      let m :: forall a. (FromJSON a) => Key -> Parser (Maybe a)+          m = parseFieldMaybe v+      browserName <- v .: "browserName"+      browserVersion <- m "browserVersion"+      platformName <- m "platformName"+      acceptInsecureCerts <- m "acceptInsecureCerts"+      pageLoadStrategy <- m "pageLoadStrategy"+      proxy <- m "proxy"+      timeouts <- m "timeouts"+      strictFileInteractability <- m "strictFileInteractability"+      unhandledPromptBehavior <- m "unhandledPromptBehavior"+      vendorSpecific <- parseVendorSpecific v+      pure MkCapabilities {..}++-- | [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#proxy)+data SocksProxy = MkSocksProxy+  { socksProxy :: Text,+    socksVersion :: Int+  }+  deriving (Eq, Show, Generic)++instance ToJSON SocksProxy where+  toJSON :: SocksProxy -> Value+  toJSON MkSocksProxy {..} =+    object+      [ "socksProxy" .= socksProxy,+        "socksVersion" .= socksVersion+      ]++instance FromJSON SocksProxy where+  parseJSON :: Value -> Parser SocksProxy+  parseJSON = withObject "SocksProxy" $ \v ->+    do+      socksProxy <- v .: "socksProxy"+      socksVersion <- v .: "socksVersion"+      pure MkSocksProxy {..}++-- | [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#proxy)+data Proxy+  = Direct+  | Manual+      { ftpProxy :: Maybe Text,+        httpProxy :: Maybe Text,+        sslProxy :: Maybe Text,+        socksProxy :: Maybe SocksProxy,+        noProxy :: Maybe [Text]+      }+  | AutoDetect+  | System+  | Pac+      { proxyAutoconfigUrl :: Text+      }+  deriving (Show, Eq)++instance ToJSON Proxy where+  toJSON :: Proxy -> Value+  toJSON p =+    object $+      [ "proxyType" .= proxyType+      ]+        <> details+    where+      proxyType = case p of+        Direct -> "direct"+        AutoDetect -> "autodetect"+        System -> "system"+        Pac {} -> "pac"+        Manual {} -> "manual"+      details = case p of+        Direct -> []+        AutoDetect -> []+        System -> []+        Pac {..} -> ["proxyAutoconfigUrl" .= proxyAutoconfigUrl]+        Manual {..} ->+          catMaybes+            [ opt "ftpProxy" ftpProxy,+              opt "httpProxy" httpProxy,+              opt "sslProxy" sslProxy,+              opt "socksProxy" socksProxy,+              opt "noProxy" noProxy+            ]++instance FromJSON Proxy where+  parseJSON :: Value -> Parser Proxy+  parseJSON = withObject "Proxy" $+    \v ->+      v .: "proxyType" >>= \case+        String "direct" -> pure Direct+        String "autodetect" -> pure AutoDetect+        String "system" -> pure System+        String "pac" -> Pac <$> v .: "proxyAutoconfigUrl"+        String "manual" ->+          do+            ftpProxy <- v .:? "ftpProxy"+            httpProxy <- v .:? "httpProxy"+            sslProxy <- v .:? "sslProxy"+            socksProxy <- v .:? "socksProxy"+            noProxy <- v .:? "noProxy"+            pure Manual {..}+        _ -> fail "Invalid Proxy"++-- Vendor Capabilities++-- | [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#extensions-0)+data VendorSpecific+  = -- | Chrome capabilities - [spec](https://developer.chrome.com/docs/chromedriver/capabilities)+    --+    -- Use also for Opera - [spec](https://github.com/operasoftware/operachromiumdriver)+    ChromeOptions+      { chromeArgs :: Maybe [Text],+        chromeBinary :: Maybe Text,+        chromeExtensions :: Maybe [Text], -- Base64-encoded+        chromeLocalState :: Maybe (Map Text Value), -- Local state preferences+        chromeMobileEmulation :: Maybe MobileEmulation,+        chromePrefs :: Maybe (Map Text Value), -- User preferences+        chromeDetach :: Maybe Bool, -- Keep browser running after driver exit+        chromeDebuggerAddress :: Maybe Text, -- Remote debugger address+        chromeExcludeSwitches :: Maybe [Text], -- Chrome switches to exclude+        chromeMinidumpPath :: Maybe FilePath, -- Crash dump directory+        chromePerfLoggingPrefs :: Maybe PerfLoggingPrefs,+        chromeWindowTypes :: Maybe [Text] -- Window types to create+      }+  -- | Edge capabilities - [spec](https://learn.microsoft.com/en-us/microsoft-edge/webdriver-chromium/capabilities-edge-options)+  | EdgeOptions+      { edgeArgs :: Maybe [Text],+        edgeBinary :: Maybe Text,+        edgeExtensions :: Maybe [Text], -- Base64-encoded+        edgeLocalState :: Maybe (Map Text Value), -- Local state preferences+        edgeMobileEmulation :: Maybe MobileEmulation,+        edgePrefs :: Maybe (Map Text Value), -- User preferences+        edgeDetach :: Maybe Bool, -- Keep browser running after driver exit+        edgeDebuggerAddress :: Maybe Text, -- Remote debugger address+        edgeExcludeSwitches :: Maybe [Text], -- Chrome switches to exclude+        edgeMinidumpPath :: Maybe FilePath, -- Crash dump directory+        edgePerfLoggingPrefs :: Maybe PerfLoggingPrefs,+        edgeWindowTypes :: Maybe [Text] -- Window types to create+      }+  -- | Firefox capabilities - [spec](https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities)+  | FirefoxOptions+      { firefoxArgs :: Maybe [Text],+        firefoxBinary :: Maybe Text,+        firefoxProfile :: Maybe Text, -- Base64-encoded profile+        firefoxLog :: Maybe LogSettings+      }+  -- | Safari capabilities - [spec](https://developer.apple.com/documentation/webkit/testing_with_webdriver_in_safari)+  | SafariOptions+      { safariAutomaticInspection :: Maybe Bool,+        safariAutomaticProfiling :: Maybe Bool+      }+  deriving (Show, Generic, Eq)++data PerfLoggingPrefs = MkPerfLoggingPrefs+  { enableNetwork :: Maybe Bool,+    enablePage :: Maybe Bool,+    enableTimeline :: Maybe Bool,+    traceCategories :: Maybe Text,+    bufferUsageReportingInterval :: Maybe Int+  }+  deriving (Show, Eq, Generic)++instance ToJSON PerfLoggingPrefs where+  toJSON = genericToJSON defaultOptions {omitNothingFields = True}++instance FromJSON PerfLoggingPrefs where+  parseJSON = genericParseJSON defaultOptions {omitNothingFields = True}++data MobileEmulation = MkMobileEmulation+  { deviceName :: Maybe Text,+    deviceMetrics :: Maybe DeviceMetrics,+    userAgent :: Maybe Text+  }+  deriving (Show, Eq, Generic)++instance FromJSON MobileEmulation where+  parseJSON = genericParseJSON defaultOptions {omitNothingFields = True}++instance ToJSON MobileEmulation where+  toJSON = genericToJSON defaultOptions {omitNothingFields = True}++-- | Browser log levels as defined in vendor specs+data LogLevel+  = -- | Most verbose logging+    Trace+  | -- | Debug-level information+    Debug+  | -- | Configuration details+    Config+  | -- | General operational logs+    Info+  | -- | Potential issues+    Warning+  | -- | Recoverable errors+    Error+  | -- | Critical failures+    Fatal+  | -- | No logging+    Off+  deriving (Show, Eq, Enum, Bounded, Generic)++instance ToJSON LogLevel where+  toJSON :: LogLevel -> Value+  toJSON =+    String . \case+      Trace -> "trace"+      Debug -> "debug"+      Config -> "config"+      Info -> "info"+      Warning -> "warn"+      Error -> "error"+      Fatal -> "fatal"+      Off -> "off"++instance FromJSON LogLevel where+  parseJSON = withText "LogLevel" $ \case+    "trace" -> pure Trace+    "debug" -> pure Debug+    "config" -> pure Config+    "info" -> pure Info+    "warn" -> pure Warning+    "error" -> pure Error+    "fatal" -> pure Fatal+    "off" -> pure Off+    other -> fail $ "Invalid log level: " <> show other++-- | Log settings structure for vendor capabilities+data LogSettings = MkLogSettings+  { level :: LogLevel+  }+  deriving (Show, Eq, Generic)++instance FromJSON LogSettings where+  parseJSON = withObject "LogSettings" $ \v ->+    do+      level <- v .: "level"+      pure MkLogSettings {..}++instance ToJSON LogSettings where+  toJSON = genericToJSON defaultOptions {omitNothingFields = True}++data DeviceMetrics = MkDeviceMetrics+  { width :: Int,+    height :: Int,+    pixelRatio :: Double,+    touch :: Bool+  }+  deriving (Show, Eq, Generic)++instance FromJSON DeviceMetrics where+  parseJSON = withObject "DeviceMetrics" $ \v ->+    do+      let m :: forall a. (FromJSON a) => Key -> Parser a+          m = parseField v+      width <- m "width"+      height <- m "height"+      pixelRatio <- m "pixelRatio"+      touch <- m "touch"+      pure MkDeviceMetrics {..}++instance ToJSON DeviceMetrics where+  toJSON = genericToJSON defaultOptions {omitNothingFields = True}++-- | ToJSON Instance for VendorSpecific++-- ToJSON Instance for VendorSpecific+instance ToJSON VendorSpecific where+  toJSON :: VendorSpecific -> Value+  toJSON vs =+    object $ catMaybes props+    where+      props = case vs of+        ChromeOptions {..} ->+          [ opt "args" chromeArgs,+            opt "binary" chromeBinary,+            opt "extensions" chromeExtensions,+            opt "localState" chromeLocalState,+            opt "mobileEmulation" chromeMobileEmulation,+            opt "prefs" chromePrefs,+            opt "detach" chromeDetach,+            opt "debuggerAddress" chromeDebuggerAddress,+            opt "excludeSwitches" chromeExcludeSwitches,+            opt "minidumpPath" chromeMinidumpPath,+            opt "perfLoggingPrefs" chromePerfLoggingPrefs,+            opt "windowTypes" chromeWindowTypes+          ]+        EdgeOptions {..} ->+          [ opt "args" edgeArgs,+            opt "binary" edgeBinary,+            opt "extensions" edgeExtensions,+            opt "localState" edgeLocalState,+            opt "prefs" edgePrefs,+            opt "detach" edgeDetach,+            opt "debuggerAddress" edgeDebuggerAddress,+            opt "excludeSwitches" edgeExcludeSwitches,+            opt "minidumpPath" edgeMinidumpPath,+            opt "mobileEmulation" edgeMobileEmulation,+            opt "perfLoggingPrefs" edgePerfLoggingPrefs,+            opt "windowTypes" edgeWindowTypes+          ]+        FirefoxOptions {..} ->+          [ opt "args" firefoxArgs,+            opt "binary" firefoxBinary,+            opt "profile" firefoxProfile,+            opt "log" firefoxLog+          ]+        SafariOptions {..} ->+          [ opt "automaticInspection" safariAutomaticInspection,+            opt "automaticProfiling" safariAutomaticProfiling+          ]++vendorSpecificPropName :: VendorSpecific -> Text+vendorSpecificPropName = \case+  ChromeOptions {} -> "goog:chromeOptions"+  EdgeOptions {} -> "ms:edgeOptions"+  FirefoxOptions {} -> "moz:firefoxOptions"+  SafariOptions {} -> "safari:options"++vendorSpecificToJSON :: Maybe VendorSpecific -> [Pair]+vendorSpecificToJSON = maybe [] vendorSpecificToJSON'+  where+    vendorSpecificToJSON' :: VendorSpecific -> [Pair]+    vendorSpecificToJSON' vs = [(fromText (vendorSpecificPropName vs), toJSON vs)]++-- ToJSON Instances for Custom Types+instance ToJSON UnhandledPromptBehavior where+  toJSON :: UnhandledPromptBehavior -> Value+  toJSON = \case+    Dismiss -> "dismiss"+    Accept -> "accept"+    DismissAndNotify -> "dismiss and notify"+    AcceptAndNotify -> "accept and notify"+    Ignore -> "ignore"++instance ToJSON BrowserName where+  toJSON :: BrowserName -> Value+  toJSON = \case+    Chrome -> "chrome"+    Firefox -> "firefox"+    Safari -> "safari"+    Edge -> "edge"+    InternetExplorer -> "internet explorer"++instance ToJSON PlatformName where+  toJSON :: PlatformName -> Value+  toJSON = \case+    Windows -> "windows"+    Mac -> "mac"+    Linux -> "linux"+    Android -> "android"+    IOS -> "ios"++instance FromJSON UnhandledPromptBehavior where+  parseJSON :: Value -> Parser UnhandledPromptBehavior+  parseJSON = withText "UnhandledPromptBehavior" $ \case+    "dismiss" -> pure Dismiss+    "accept" -> pure Accept+    "dismiss and notify" -> pure DismissAndNotify+    "accept and notify" -> pure AcceptAndNotify+    "ignore" -> pure Ignore+    other -> fail $ "UnhandledPromptBehavior: " <> show other++instance FromJSON BrowserName where+  parseJSON :: Value -> Parser BrowserName+  parseJSON = withText "BrowserName" $ \case+    "chrome" -> pure Chrome+    "firefox" -> pure Firefox+    "safari" -> pure Safari+    "edge" -> pure Edge+    "internet explorer" -> pure InternetExplorer+    _ -> fail "Invalid BrowserName"++instance FromJSON PlatformName where+  parseJSON :: Value -> Parser PlatformName+  parseJSON = withText "PlatformName" $ \case+    "windows" -> pure Windows+    "mac" -> pure Mac+    "linux" -> pure Linux+    "android" -> pure Android+    "ios" -> pure IOS+    _ -> fail "Invalid PlatformName"++-- FromJSON Instances for Data Structures+parseVendorSpecific :: Object -> Parser (Maybe VendorSpecific)+parseVendorSpecific v =+  asum+    [ Just <$> (v .: "goog:chromeOptions" >>= parseChromeOptions),+      Just <$> (v .: "ms:edgeOptions" >>= parseEdgeOptions),+      Just <$> (v .: "moz:firefoxOptions" >>= parseFirefoxOptions),+      Just <$> (v .: "safari:options" >>= parseSafariOptions),+      pure Nothing+    ]+  where+    parseChromeOptions o = do+      chromeArgs <- m "args"+      chromeBinary <- m "binary"+      chromeExtensions <- m "extensions"+      chromeLocalState <- m "localState" -- Local state preferences+      chromeMobileEmulation <- m "mobileEmulation"+      chromePrefs <- m "prefs"+      chromeDetach <- m "detach"+      chromeDebuggerAddress <- m "debuggerAddress"+      chromeExcludeSwitches <- m "excludeSwitches"+      chromeMinidumpPath <- m "minidumpPath"+      chromePerfLoggingPrefs <- m "perfLoggingPrefs"+      chromeWindowTypes <- m "windowTypes"+      pure $ ChromeOptions {..}+      where+        m :: forall a. (FromJSON a) => Key -> Parser (Maybe a)+        m = parseFieldMaybe o++    parseEdgeOptions o = do+      edgeArgs <- m "args"+      edgeBinary <- m "binary"+      edgeExtensions <- m "extensions"+      edgeLocalState <- m "localState"+      edgePrefs <- m "prefs"+      edgeDetach <- m "detach"+      edgeDebuggerAddress <- m "debuggerAddress"+      edgeExcludeSwitches <- m "excludeSwitches"+      edgeMinidumpPath <- m "minidumpPath"+      edgeMobileEmulation <- m "mobileEmulation"+      edgePerfLoggingPrefs <- m "perfLoggingPrefs"+      edgeWindowTypes <- m "windowTypes"+      pure EdgeOptions {..}+      where+        m :: forall a. (FromJSON a) => Key -> Parser (Maybe a)+        m = parseFieldMaybe o++    parseFirefoxOptions o = do+      firefoxArgs <- m "args"+      firefoxBinary <- m "binary"+      firefoxProfile <- m "profile"+      firefoxLog <- m "log"+      pure FirefoxOptions {..}+      where+        m :: forall a. (FromJSON a) => Key -> Parser (Maybe a)+        m = parseFieldMaybe o++    parseSafariOptions o = do+      safariAutomaticInspection <- m "automaticInspection"+      safariAutomaticProfiling <- m "automaticProfiling"+      pure SafariOptions {..}+      where+        m = parseFieldMaybe o++-- | Timeouts in milliseconds+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#timeouts)+data Timeouts = MkTimeouts+  { implicit :: Maybe Int, -- field order needs to be the same as FromJSON below+    pageLoad :: Maybe Int,+    script :: Maybe Int+  }+  deriving (Show, Generic, Eq)++instance FromJSON Timeouts where+  parseJSON :: Value -> Parser Timeouts+  parseJSON = withObject "Timeouts" $ \v ->+    do+      implicit <- v .:? "implicit"+      pageLoad <- v .:? "pageLoad"+      script <- v .:? "script"+      pure MkTimeouts {..}++instance ToJSON Timeouts where+  toJSON :: Timeouts -> Value+  toJSON MkTimeouts {..} =+    object+      [ "implicit" .= implicit,+        "pageLoad" .= pageLoad,+        "script" .= script+      ]
+ src/WebDriverPreCore/Error.hs view
@@ -0,0 +1,223 @@+{-# OPTIONS_HADDOCK hide #-}++module WebDriverPreCore.Error (+  WebDriverErrorType(..),+  ErrorClassification(..),+  errorDescription,+  errorCodeToErrorType,+  errorTypeToErrorCode,+  parseWebDriverError,+  parseWebDriverErrorType +) where++import Data.Aeson (Value, withObject)+import Data.Aeson.Types ((.:), parseMaybe)+import Data.Text (Text)+import Data.Eq (Eq)+import GHC.Show (Show)+import Data.Ord (Ord)+import Data.Maybe (Maybe (..))+import WebDriverPreCore.HttpResponse (HttpResponse (..))+import GHC.Enum ( Bounded, Enum )+import Data.Either (Either (..))+import Control.Monad ((>>=))+import Data.Function (($))++{-+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-20250306/#errors)+data WebDriverErrorType =+  ElementClickIntercepted |+  ElementNotInteractable |+  InsecureCertificate |+  InvalidArgument |+  InvalidCookieDomain |+  InvalidElementState |+  InvalidSelector |+  InvalidSessionId |+  JavascriptError |+  MoveTargetOutOfBounds |+  NoSuchAlert |+  NoSuchCookie |+  NoSuchElement |+  NoSuchFrame |+  NoSuchWindow |+  NoSuchShadowRoot |+  ScriptTimeoutError |+  SessionNotCreated |+  StaleElementReference |+  DetachedShadowRoot |+  Timeout |+  UnableToSetCookie |+  UnableToCaptureScreen |+  UnexpectedAlertOpen |+  UnknownCommand |+  UnknownError |+  UnknownMethod |+  UnsupportedOperation+  deriving (Eq, Show, Ord, Bounded, Enum)+++getError :: Value -> Maybe Text+getError =+  parseMaybe $ withObject "root" $ \o ->+                      o .: "value" >>= withObject "inner" (.: "error")+                   +++errorCodeToErrorType :: Text -> Either Text WebDriverErrorType+errorCodeToErrorType errCode = +   case errCode of+      "element click intercepted" -> r ElementClickIntercepted+      "element not interactable" -> r ElementNotInteractable+      "insecure certificate" -> r InsecureCertificate+      "invalid argument" -> r InvalidArgument+      "invalid cookie domain" -> r InvalidCookieDomain+      "invalid element state" -> r InvalidElementState+      "invalid selector" -> r InvalidSelector+      "invalid session id" -> r InvalidSessionId+      "javascript error" -> r JavascriptError+      "move target out of bounds" -> r MoveTargetOutOfBounds+      "no such alert" -> r NoSuchAlert+      "no such cookie" -> r NoSuchCookie+      "no such element" -> r NoSuchElement+      "no such frame" -> r NoSuchFrame+      "no such window" -> r NoSuchWindow+      "no such shadow root" -> r NoSuchShadowRoot+      "script timeout" -> r ScriptTimeoutError+      "session not created" -> r SessionNotCreated+      "stale element reference" -> r StaleElementReference+      "detached shadow root" -> r DetachedShadowRoot+      "timeout" -> r Timeout+      "unable to set cookie" -> r UnableToSetCookie+      "unable to capture screen" -> r UnableToCaptureScreen+      "unexpected alert open" -> r UnexpectedAlertOpen+      "unknown command" -> r UnknownCommand+      "unknown error" -> r UnknownError+      "unknown method" -> r UnknownMethod+      "unsupported operation" -> r UnsupportedOperation+      er -> Left er+    where+      r = Right++errorTypeToErrorCode :: WebDriverErrorType -> Text+errorTypeToErrorCode = \case+  ElementClickIntercepted -> "element click intercepted"+  ElementNotInteractable -> "element not interactable"+  InsecureCertificate -> "insecure certificate"+  InvalidArgument -> "invalid argument"+  InvalidCookieDomain -> "invalid cookie domain"+  InvalidElementState -> "invalid element state"+  InvalidSelector -> "invalid selector"+  InvalidSessionId -> "invalid session id"+  JavascriptError -> "javascript error"+  MoveTargetOutOfBounds -> "move target out of bounds"+  NoSuchAlert -> "no such alert"+  NoSuchCookie -> "no such cookie"+  NoSuchElement -> "no such element"+  NoSuchFrame -> "no such frame"+  NoSuchWindow -> "no such window"+  NoSuchShadowRoot -> "no such shadow root"+  ScriptTimeoutError -> "script timeout"+  SessionNotCreated -> "session not created"+  StaleElementReference -> "stale element reference"+  DetachedShadowRoot -> "detached shadow root"+  Timeout -> "timeout"+  UnableToSetCookie -> "unable to set cookie"+  UnableToCaptureScreen -> "unable to capture screen"+  UnexpectedAlertOpen -> "unexpected alert open"+  UnknownCommand -> "unknown command"+  UnknownError -> "unknown error"+  UnknownMethod -> "unknown method"+  UnsupportedOperation -> "unsupported operation"++errorDescription :: WebDriverErrorType -> 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"+  ScriptTimeoutError -> "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"+  UnableToSetCookie -> "A command to set a cookie's value could not be satisfied"+  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"++data ErrorClassification = +  NotAnError {httpResponse :: HttpResponse} |+  UnrecognisedError {httpResponse :: HttpResponse} |+  WebDriverError {+    error :: WebDriverErrorType,+    description :: Text,+    httpResponse :: HttpResponse+    -- todo find stacktrace+    -- stacktrace :: Maybe Text+  } deriving (Eq, Show, Ord)++parseWebDriverError :: HttpResponse -> ErrorClassification+parseWebDriverError resp = +  case getError resp.body of+    Nothing -> NotAnError resp+    Just err -> +      case errorCodeToErrorType err of+        Right et -> WebDriverError et (errorDescription et) resp+        Left _ -> UnrecognisedError resp++parseWebDriverErrorType :: HttpResponse -> Maybe WebDriverErrorType+parseWebDriverErrorType resp = +  case parseWebDriverError resp of+    WebDriverError {error} -> Just error+    NotAnError {} -> Nothing+    UnrecognisedError {} -> Nothing+  
+ src/WebDriverPreCore/HttpResponse.hs view
@@ -0,0 +1,25 @@+{-# OPTIONS_HADDOCK hide #-}++module WebDriverPreCore.HttpResponse (+  HttpResponse(..)+) where++import Data.Aeson (Value)+import Data.Text (Text)+import Data.Int (Int)+import GHC.Show (Show)+import Data.Eq (Eq)+import Data.Ord (Ord)++-- | 'HttpResponse' represents a WebDriver HTTP response.+--+-- An instance of 'HttpResponse' needs to be constructed in order to run the 'WebDriverPreCore.parser' suppplied in the [W3CSpec](WebDriverPreCore.Get)+data HttpResponse = MkHttpResponse+  { -- | HTTP status code.+    statusCode :: Int,+    -- | HTTP status message.+    statusMessage :: Text,+    -- | Response body in JSON format.+    body :: Value+  }+  deriving (Show, Eq, Ord)
+ src/WebDriverPreCore/Internal/Utils.hs view
@@ -0,0 +1,69 @@+{-# OPTIONS_HADDOCK hide #-}++module WebDriverPreCore.Internal.Utils+  ( opt,+    txt,+    enumerate,+    -- Aeson utils+    jsonToText,+    lsbToText,+    prettyPrintJson,+    parseJson+  )+where++import Data.Aeson+  ( Key,+    KeyValue ((.=)),+    ToJSON (),+    Value (..), eitherDecodeStrict,+  )+import Data.Function ((.), ($))+import Data.Functor (Functor, (<$>))+import Data.String (String)+import Data.Text ( Text, pack)+import Data.Text.Encoding (encodeUtf8, decodeUtf8)+import GHC.Show (Show (..))+import Data.Aeson.Encode.Pretty (encodePretty)+import Data.ByteString.Lazy qualified as LBS+import Data.Text.IO qualified as T+import Control.Exception (try, Exception (displayException), SomeException)+import GHC.Base (IO)+import Data.Either (Either, either)+import System.IO (print)+import GHC.Enum (Enum, Bounded (..))+++{-+  this module is used between the library and testing modules+  it will be removed in a later relaease+-}++txt :: (Show a) => a -> Text+txt = pack . show+++enumerate :: (Enum a, Bounded a) => [a]+enumerate = [minBound ..]++-- Aeson stuff+-- TODO move to separte library++opt :: (Functor f, KeyValue e b, ToJSON a) => Key -> f a -> f b+opt lbl mb = (lbl .=) <$> mb++-- https://blog.ssanj.net/posts/2019-09-24-pretty-printing-json-in-haskell.html+lsbToText :: LBS.ByteString -> Text+lsbToText = decodeUtf8 . LBS.toStrict++jsonToText :: Value -> Text+jsonToText = lsbToText . encodePretty++prettyPrintJson :: Value -> IO ()+prettyPrintJson v = do+  e <- (try @SomeException @_) $ T.putStrLn (jsonToText v)+  either (print . displayException) print e++parseJson :: Text -> Either String Value+parseJson input =+  eitherDecodeStrict (encodeUtf8 input)
+ src/WebDriverPreCore/SpecDefinition.hs view
@@ -0,0 +1,1551 @@+{-# OPTIONS_HADDOCK hide #-}++-- |+-- Description : All Webdriver W3C endpoints+--+--+-- Here is a longer description of this module, containing some+-- commentary with @some markup@.+module WebDriverPreCore.SpecDefinition+  ( -- * The W3Spec Type+    W3Spec (..),++    -- * The API++    -- ** Root Methods+    newSession,+    newSession',+    status,++    -- ** Session Methods+    deleteSession,+    getTimeouts,+    setTimeouts,+    navigateTo,+    getCurrentUrl,+    back,+    forward,+    refresh,+    getTitle,+    getWindowHandle,+    newWindow,+    closeWindow,+    switchToWindow,+    switchToFrame,+    getPageSource,+    executeScript,+    executeScriptAsync,+    addCookie,+    getAllCookies,+    getNamedCookie,+    deleteCookie,+    deleteAllCookies,+    performActions,+    releaseActions,+    dismissAlert,+    acceptAlert,+    getAlertText,+    sendAlertText,+    takeScreenshot,+    printPage,++    -- ** Window Methods+    getWindowHandles,+    getWindowRect,+    setWindowRect,+    maximizeWindow,+    minimizeWindow,+    fullscreenWindow,++    -- ** Frame Methods+    switchToParentFrame,++    -- ** Element(s) Methods+    getActiveElement,+    findElement,+    findElements,+    +    -- ** Element Instance Methods+    findElementFromElement,+    findElementsFromElement,+    isElementSelected,+    getElementAttribute,+    getElementProperty,+    getElementCssValue,+    getElementShadowRoot,+    getElementText,+    getElementTagName,+    getElementRect,+    isElementEnabled,+    getElementComputedRole,+    getElementComputedLabel,+    elementClick,+    elementClear,+    elementSendKeys,+    takeElementScreenshot,++    -- ** Shadow DOM Methods+    findElementFromShadowRoot,+    findElementsFromShadowRoot,++    -- * auxiliary Types+    Cookie (..),+    DriverStatus (..),+    ElementId (..),+    FrameReference (..),+    HandleType (..),+    HttpResponse (..),+    SameSite (..),+    Selector (..),+    SessionId (..),+    Timeouts (..),+    WindowHandle (..),+    WindowHandleSpec (..),+    WindowRect (..),+    UrlPath (..),++    -- ** Action Types+    Action (..),+    Actions (..),+    KeyAction (..),+    Pointer (..),+    PointerAction (..),+    PointerOrigin (..),+    WheelAction (..),+  )+where++import Data.Aeson as A+  ( FromJSON (..),+    Key,+    KeyValue ((.=)),+    Result (..),+    ToJSON (toJSON),+    Value (..),+    fromJSON,+    object,+    withObject,+    withText,+    (.:),+  )+import Data.Aeson.KeyMap qualified as AKM+import Data.Aeson.Types (Parser)+import Data.Foldable (toList)+import Data.Function ((&))+import Data.Maybe (catMaybes)+import Data.Set (Set)+import Data.Text (Text, pack, unpack)+import Data.Text qualified as T+import Data.Word (Word16)+import GHC.Generics (Generic)+import WebDriverPreCore.Internal.Utils (jsonToText, opt, txt)+import WebDriverPreCore.Capabilities as Capabilities+import WebDriverPreCore.HttpResponse (HttpResponse (..))+import Prelude hiding (id, lookup)++-- | Url as returned by 'W3Spec'+-- The 'UrlPath' type is a newtype wrapper around a list of 'Text' segments representing a path.+--+-- e.g. the path: @\/session\/session-no-1-2-3\/window@ would be represented as: @MkUrlPath ["session", "session-no-1-2-3", "window"]@+newtype UrlPath = MkUrlPath {segments :: [Text]}+  deriving newtype (Show, Eq, Ord, Semigroup)++{-|+  The 'W3Spec' type is a specification for a WebDriver command.+  Every endpoint function in this module returns a 'W3Spec' object.+-}+data W3Spec a+  = Get+      { description :: Text,+        path :: UrlPath,+        parser :: HttpResponse -> Result a+      }+  | Post+      { description :: Text,+        path :: UrlPath,+        body :: Value,+        parser :: HttpResponse -> Result a+      }+  | PostEmpty+      { description :: Text,+        path :: UrlPath,+        parser :: HttpResponse -> Result a+      }+  | Delete+      { description :: Text,+        path :: UrlPath,+        parser :: HttpResponse -> Result a+      }++instance (Show a) => Show (W3Spec a) where+  show :: W3Spec a -> String+  show = Prelude.show . mkShowable++data W3SpecShowable = Request+  { description :: Text,+    method :: Text,+    path :: UrlPath,+    body :: Maybe Text+  }+  deriving (Show)++-- | [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#dfn-get-window-handle)+newtype WindowHandle = Handle {handle :: Text}+  deriving (Show, Eq)++-- | [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#new-window)+data WindowHandleSpec = HandleSpec+  { handle :: WindowHandle,+    handletype :: HandleType+  }+  deriving (Show, Eq)++instance ToJSON WindowHandleSpec where+  toJSON :: WindowHandleSpec -> Value+  toJSON HandleSpec {handle, handletype} =+    object+      [ "handle" .= handle.handle,+        "type" .= handletype+      ]++instance FromJSON WindowHandleSpec where+  parseJSON :: Value -> Parser WindowHandleSpec+  parseJSON = withObject "WindowHandleSpec" $ \v -> do+    handle <- Handle <$> v .: "handle"+    handletype <- v .: "type"+    pure $ HandleSpec {..}++data HandleType+  = Window+  | Tab+  deriving (Show, Eq)++instance ToJSON HandleType where+  toJSON :: HandleType -> Value+  toJSON = String . T.toLower . pack . show++instance FromJSON HandleType where+  parseJSON :: Value -> Parser HandleType+  parseJSON = withText "HandleType" $ \case+    "window" -> pure Window+    "tab" -> pure Tab+    v -> fail $ unpack $ "Unknown HandleType " <> v++-- | [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#dfn-find-element)+newtype ElementId = Element {id :: Text}+  deriving (Show, Eq, Generic)++-- | [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#dfn-new-sessions)+newtype SessionId = Session {id :: Text}+  deriving (Show)++-- | [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#dfn-status)+data DriverStatus+  = Ready+  | Running+  | ServiceError {statusCode :: Int, statusMessage :: Text}+  | Unknown {statusCode :: Int, statusMessage :: Text}+  deriving (Show, Eq)++-- | [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#cookies)+data SameSite+  = Lax+  | Strict+  | None+  deriving (Show, Eq, Ord)++instance ToJSON SameSite where+  toJSON :: SameSite -> Value+  toJSON = String . txt++-- | [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#dfn-switch-to-frame)+data FrameReference+  = TopLevelFrame+  | FrameNumber Word16+  | FrameElementId ElementId+  deriving (Show, Eq)++frameJson :: FrameReference -> Value+frameJson fr =+  object+    ["id" .= toJSON (frameVariant fr)]+  where+    frameVariant =+      \case+        TopLevelFrame -> Null+        FrameNumber n -> Number $ fromIntegral n+        FrameElementId elm -> object [elementFieldName .= elm.id]++-- | [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#cookies)+data Cookie = MkCookie+  { name :: Text,+    value :: Text,+    -- optional+    path :: Maybe Text,+    domain :: Maybe Text,+    secure :: Maybe Bool,+    httpOnly :: Maybe Bool,+    sameSite :: Maybe SameSite,+    -- When the cookie expires, specified in seconds since Unix Epoch.+    expiry :: Maybe Int+  }+  deriving (Show, Eq)++instance ToJSON Cookie where+  toJSON :: Cookie -> Value+  toJSON MkCookie {name, value, path, domain, secure, httpOnly, sameSite, expiry} =+    object $+      [ "name" .= name,+        "value" .= value+      ]+        <> catMaybes+          [ opt "path" path,+            opt "domain" domain,+            opt "secure" secure,+            opt "httpOnly" httpOnly,+            opt "sameSite" sameSite,+            opt "expiry" expiry+          ]++cookieJSON :: Cookie -> Value+cookieJSON c = object ["cookie" .= c]++-- | [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#locator-strategies)+data Selector+  = CSS Text+  | XPath Text+  | LinkText Text+  | PartialLinkText Text+  | TagName Text+  deriving (Show, Eq)++-- ######################################################################+-- ########################### WebDriver API ############################+-- ######################################################################++-- https://www.w3.org/TR/2025/WD-webdriver2-20250306/+-- 61 endpoints+-- Method 	URI Template 	Command++-- ** Root Methods++-- |+--  Return a spec to create a new session given 'FullCapabilities' object.+--+-- 'newSession'' can be used if 'FullCapabilities' doesn't meet your requirements.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#new-session)+--+--  @POST 	\/session 	New Session@+newSession :: FullCapabilities -> W3Spec SessionId+newSession = newSession'++-- |+--+--  Return a spec to create a new session given an object of any type that implements `ToJSON`.+--+-- The 'FullCapabilities' type and associated types should work for the vast majority use cases, but if the required capabilities are not covered by the types provided, 'newSession''.+-- can be used with a custom type instead. 'newSession'' works with any type that implements 'ToJSON', (including an Aeson 'Value').+-- +-- Obviously, any type used must produce a JSON object compatible with [capabilities as defined W3C spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#capabilities).+--+--  [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#new-session)+--+--  @POST 	\/session 	New Session@+newSession' :: (ToJSON a) => a -> W3Spec SessionId+newSession' capabilities = Post "New Session" (MkUrlPath [session]) (toJSON capabilities) parseSessionRef++-- |+--+-- Return a spec to get the status of the driver.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#status)+--+-- @GET 	\/status 	Status@+status :: W3Spec DriverStatus+status = Get "Status" (MkUrlPath ["status"]) parseDriverStatus++-- ############################ Session Methods ##########################################++-- |+--+-- Return a spec to delete a session given a 'SessionId'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#delete-session)+--+-- @DELETE 	\/session\/{session id} 	Delete Session@+deleteSession :: SessionId -> W3Spec ()+deleteSession sessionRef = Delete "Delete Session" (sessionUri sessionRef.id) voidParser++-- |+--+-- Return a spec to get the timeouts of a session given a 'SessionId'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#get-timeouts)+--+-- @GET 	\/session\/{session id}\/timeouts 	Get Timeouts@+getTimeouts :: SessionId -> W3Spec Timeouts+getTimeouts sessionRef = Get "Get Timeouts" (sessionUri1 sessionRef "timeouts") parseTimeouts++-- |+--+-- Return a spec to set the timeouts of a session given a 'SessionId' and 'Timeouts'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#set-timeouts)+--+-- @POST 	\/session\/{session id}\/timeouts 	Set Timeouts@+setTimeouts :: SessionId -> Timeouts -> W3Spec ()+setTimeouts sessionRef timeouts =+  Post "Set Timeouts" (sessionUri1 sessionRef "timeouts") (toJSON timeouts) voidParser++-- |+--+-- Return a spec to navigate to a URL given a 'SessionId' and a 'Text' URL.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#navigate-to)+--+-- @POST 	\/session\/{session id}\/url 	Navigate To@+navigateTo :: SessionId -> Text -> W3Spec ()+navigateTo sessionRef url = Post "Navigate To" (sessionUri1 sessionRef "url") (object ["url" .= url]) voidParser++-- |+--+-- Return a spec to get the current URL of a session given a 'SessionId'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#get-current-url)+--+-- @GET 	\/session\/{session id}\/url 	Get Current URL@+getCurrentUrl :: SessionId -> W3Spec Text+getCurrentUrl sessionRef = Get "Get Current URL" (sessionUri1 sessionRef "url") parseBodyTxt++-- |+--+-- Return a spec to navigate back in the browser history given a 'SessionId'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#back)+--+-- @POST 	\/session\/{session id}\/back 	Back@+back :: SessionId -> W3Spec ()+back sessionRef = PostEmpty "Back" (sessionUri1 sessionRef "back") voidParser++-- |+--+-- Return a spec to navigate forward in the browser history given a 'SessionId'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#forward)+--+-- @POST 	\/session\/{session id}\/forward 	Forward@+forward :: SessionId -> W3Spec ()+forward sessionRef = PostEmpty "Forward" (sessionUri1 sessionRef "forward") voidParser++-- |+--+-- Return a spec to refresh the current page given a 'SessionId'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#refresh)+--+-- @POST 	\/session\/{session id}\/refresh 	Refresh@+refresh :: SessionId -> W3Spec ()+refresh sessionRef = PostEmpty "Refresh" (sessionUri1 sessionRef "refresh") voidParser++-- |+--+-- Return a spec to get the title of the current page given a 'SessionId'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#get-title)+--+-- @GET 	\/session\/{session id}\/title 	Get Title@+getTitle :: SessionId -> W3Spec Text+getTitle sessionRef = Get "Get Title" (sessionUri1 sessionRef "title") parseBodyTxt++-- |+--+-- Return a spec to get the current window handle given a 'SessionId'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#get-window-handle)+--+-- @GET 	\/session\/{session id}\/window 	Get Window Handle@+getWindowHandle :: SessionId -> W3Spec WindowHandle+getWindowHandle sessionRef = Get "Get Window Handle" (sessionUri1 sessionRef "window") (fmap Handle . parseBodyTxt)++-- |+--+-- Return a spec to create a new window given a 'SessionId'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#new-window)+--+-- @POST 	\/session\/{session id}\/window\/new 	New Window@+newWindow :: SessionId -> W3Spec WindowHandleSpec+newWindow sessionRef = PostEmpty "New Window" (sessionUri2 sessionRef "window" "new") windowHandleParser++-- |+--+-- Return a spec to close the current window given a 'SessionId'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#close-window)+--+-- @DELETE 	\/session\/{session id}\/window 	Close Window@+closeWindow :: SessionId -> W3Spec ()+closeWindow sessionRef = Delete "Close Window" (sessionUri1 sessionRef "window") voidParser++-- |+--+-- Return a spec to switch to a different window given a 'SessionId' and 'WindowHandle'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#switch-to-window)+--+-- @POST 	\/session\/{session id}\/window 	Switch To Window@+switchToWindow :: SessionId -> WindowHandle -> W3Spec ()+switchToWindow sessionRef Handle {handle} = Post "Switch To Window" (sessionUri1 sessionRef "window") (object ["handle" .= handle]) voidParser++-- |+--+-- Return a spec to switch to a different frame given a 'SessionId' and 'FrameReference'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#switch-to-frame)+--+-- @POST 	\/session\/{session id}\/frame 	Switch To Frame@+switchToFrame :: SessionId -> FrameReference -> W3Spec ()+switchToFrame sessionRef frameRef = Post "Switch To Frame" (sessionUri1 sessionRef "frame") (frameJson frameRef) voidParser++-- |+--+-- Return a spec to get the source of the current page given a 'SessionId'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#get-page-source)+--+-- @GET 	\/session\/{session id}\/source 	Get Page Source@+getPageSource :: SessionId -> W3Spec Text+getPageSource sessionId = Get "Get Page Source" (sessionUri1 sessionId "source") parseBodyTxt++-- |+--+-- Return a spec to execute a script in the context of the current page given a 'SessionId', 'Text' script, and a list of 'Value' arguments.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#execute-script)+--+-- @POST 	\/session\/{session id}\/execute\/sync 	Execute Script@+executeScript :: SessionId -> Text -> [Value] -> W3Spec Value+executeScript sessionId script args = Post "Execute Script" (sessionUri2 sessionId "execute" "sync") (mkScript script args) bodyValue++-- |+--+-- Return a spec to execute an asynchronous script in the context of the current page given a 'SessionId', 'Text' script, and a list of 'Value' arguments.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#execute-async-script)+--+-- @POST 	\/session\/{session id}\/execute\/async 	Execute Async Script@+executeScriptAsync :: SessionId -> Text -> [Value] -> W3Spec Value+executeScriptAsync sessionId script args = Post "Execute Async Script" (sessionUri2 sessionId "execute" "async") (mkScript script args) bodyValue++-- |+--+-- Return a spec to get all cookies of the current page given a 'SessionId'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#get-all-cookies)+--+-- @GET 	\/session\/{session id}\/cookie 	Get All Cookies@+getAllCookies :: SessionId -> W3Spec [Cookie]+getAllCookies sessionId = Get "Get All Cookies" (sessionUri1 sessionId "cookie") parseCookies++-- |+--+-- Return a spec to get a named cookie of the current page given a 'SessionId' and cookie name.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#get-named-cookie)+--+-- @GET 	\/session\/{session id}\/cookie\/{name} 	Get Named Cookie@+getNamedCookie :: SessionId -> Text -> W3Spec Cookie+getNamedCookie sessionId cookieName = Get "Get Named Cookie" (sessionUri2 sessionId "cookie" cookieName) parseCookie++-- |+--+-- Return a spec to add a cookie to the current page given a 'SessionId' and 'Cookie'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#add-cookie)+--+-- @POST 	\/session\/{session id}\/cookie 	Add Cookie@+addCookie :: SessionId -> Cookie -> W3Spec ()+addCookie sessionId cookie = Post "Add Cookie" (sessionUri1 sessionId "cookie") (cookieJSON cookie) voidParser++-- |+--+-- Return a spec to delete a named cookie from the current page given a 'SessionId' and cookie name.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#delete-cookie)+--+-- @DELETE 	\/session\/{session id}\/cookie\/{name} 	Delete Cookie@+deleteCookie :: SessionId -> Text -> W3Spec ()+deleteCookie sessionId cookieName = Delete "Delete Cookie" (sessionUri2 sessionId "cookie" cookieName) voidParser++-- |+--+-- Return a spec to delete all cookies from the current page given a 'SessionId'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#delete-all-cookies)+--+-- @DELETE 	\/session\/{session id}\/cookie 	Delete All Cookies@+deleteAllCookies :: SessionId -> W3Spec ()+deleteAllCookies sessionId = Delete "Delete All Cookies" (sessionUri1 sessionId "cookie") voidParser++-- |+--+-- Return a spec to perform actions on the current page given a 'SessionId' and 'Actions'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#perform-actions)+--+-- @POST 	\/session\/{session id}\/actions 	Perform Actions@+performActions :: SessionId -> Actions -> W3Spec ()+performActions sessionId actions = Post "Perform Actions" (sessionUri1 sessionId "actions") (actionsToJson actions) voidParser++-- |+--+-- Return a spec to release actions on the current page given a 'SessionId'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#release-actions)+--+-- @DELETE 	\/session\/{session id}\/actions 	Release Actions@+releaseActions :: SessionId -> W3Spec ()+releaseActions sessionId = Delete "Release Actions" (sessionUri1 sessionId "actions") voidParser++-- |+--+-- Return a spec to dismiss an alert on the current page given a 'SessionId'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#dismiss-alert)+--+-- @POST 	\/session\/{session id}\/alert\/dismiss 	Dismiss Alert@+dismissAlert :: SessionId -> W3Spec ()+dismissAlert sessionId = PostEmpty "Dismiss Alert" (sessionUri2 sessionId "alert" "dismiss") voidParser++-- |+--+-- Return a spec to accept an alert on the current page given a 'SessionId'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#accept-alert)+--+-- @POST 	\/session\/{session id}\/alert\/accept 	Accept Alert@+acceptAlert :: SessionId -> W3Spec ()+acceptAlert sessionId = PostEmpty "Accept Alert" (sessionUri2 sessionId "alert" "accept") voidParser++-- |+--+-- Return a spec to get the text of an alert on the current page given a 'SessionId'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#get-alert-text)+--+-- @GET 	\/session\/{session id}\/alert\/text 	Get Alert Text@+getAlertText :: SessionId -> W3Spec Text+getAlertText sessionId = Get "Get Alert Text" (sessionUri2 sessionId "alert" "text") parseBodyTxt++-- |+--+-- Return a spec to send text to an alert on the current page given a 'SessionId' and 'Text'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#send-alert-text)+--+-- @POST 	\/session\/{session id}\/alert\/text 	Send Alert Text@+sendAlertText :: SessionId -> Text -> W3Spec ()+sendAlertText sessionId text = Post "Send Alert Text" (sessionUri2 sessionId "alert" "text") (object ["text" .= text]) voidParser++-- |+--+-- Return a spec to take a screenshot of the current page given a 'SessionId'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#take-screenshot)+--+-- @GET 	\/session\/{session id}\/screenshot 	Take Screenshot@+takeScreenshot :: SessionId -> W3Spec Text+takeScreenshot sessionId = Get "Take Screenshot" (sessionUri1 sessionId "screenshot") parseBodyTxt++-- |+--+-- Return a spec to print the current page given a 'SessionId'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#print-page)+--+-- @POST 	\/session\/{session id}\/print 	Print Page@+printPage :: SessionId -> W3Spec Text+printPage sessionId = PostEmpty "Print Page" (sessionUri1 sessionId "print") parseBodyTxt++-- ############################ Window Methods ##########################################++-- |+--+-- Return a spec to get all window handles of the current session given a 'SessionId'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#get-window-handles)+--+-- @GET 	\/session\/{session id}\/window\/handles 	Get Window Handles@+getWindowHandles :: SessionId -> W3Spec [WindowHandle]+getWindowHandles sessionRef = Get "Get Window Handles" (sessionUri2 sessionRef "window" "handles") windowHandlesParser++-- |+--+-- Return a spec to get the window rect of the current window given a 'SessionId'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#get-window-rect)+--+-- @GET 	\/session\/{session id}\/window\/rect 	Get Window Rect@+getWindowRect :: SessionId -> W3Spec WindowRect+getWindowRect sessionRef = Get "Get Window Rect" (sessionUri2 sessionRef "window" "rect") parseWindowRect++-- |+--+-- Return a spec to set the window rect of the current window given a 'SessionId' and 'WindowRect'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#set-window-rect)+--+-- @POST 	\/session\/{session id}\/window\/rect 	Set Window Rect@+setWindowRect :: SessionId -> WindowRect -> W3Spec WindowRect+setWindowRect sessionRef rect = Post "Set Window Rect" (sessionUri2 sessionRef "window" "rect") (toJSON rect) parseWindowRect++-- |+--+-- Return a spec to maximize the current window given a 'SessionId'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#maximize-window)+--+-- @POST 	\/session\/{session id}\/window\/maximize 	Maximize Window@+maximizeWindow :: SessionId -> W3Spec WindowRect+maximizeWindow sessionRef = PostEmpty "Maximize Window" (windowUri1 sessionRef "maximize") parseWindowRect++-- |+--+-- Return a spec to minimize the current window given a 'SessionId'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#minimize-window)+--+-- @POST 	\/session\/{session id}\/window\/minimize 	Minimize Window@+minimizeWindow :: SessionId -> W3Spec WindowRect+minimizeWindow sessionRef = PostEmpty "Minimize Window" (windowUri1 sessionRef "minimize") parseWindowRect++-- |+--+-- Return a spec to fullscreen the current window given a 'SessionId'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#fullscreen-window)+--+-- @POST 	\/session\/{session id}\/window\/fullscreen 	Fullscreen Window@+fullscreenWindow :: SessionId -> W3Spec WindowRect+fullscreenWindow sessionRef = PostEmpty "Fullscreen Window" (windowUri1 sessionRef "fullscreen") parseWindowRect++-- ############################ Frame Methods ##########################################++-- |+--+-- Return a spec to switch to the parent frame given a 'SessionId'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#switch-to-parent-frame)+--+-- @POST 	\/session\/{session id}\/frame\/parent 	Switch To Parent Frame@+switchToParentFrame :: SessionId -> W3Spec ()+switchToParentFrame sessionRef = PostEmpty "Switch To Parent Frame" (sessionUri2 sessionRef "frame" "parent") voidParser++-- ############################ Element(s) Methods ##########################################++-- |+--+-- Return a spec to get the active element of the current page given a 'SessionId'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#get-active-element)+--+-- @GET 	\/session\/{session id}\/element\/active 	Get Active Element@+getActiveElement :: SessionId -> W3Spec ElementId+getActiveElement sessionId = Get "Get Active Element" (sessionUri2 sessionId "element" "active") parseElementRef++-- |+--+-- Return a spec to find an element on the current page given a 'SessionId' and 'Selector'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#find-element)+--+-- @POST 	\/session\/{session id}\/element 	Find Element@+findElement :: SessionId -> Selector -> W3Spec ElementId+findElement sessionRef = findElement' sessionRef . selectorJson++-- |+--+-- Return a spec to find elements on the current page given a 'SessionId' and 'Selector'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#find-elements)+--+-- @POST 	\/session\/{session id}\/elements 	Find Elements@+findElements :: SessionId -> Selector -> W3Spec [ElementId]+findElements sessionRef selector = Post "Find Elements" (sessionUri1 sessionRef "elements") (selectorJson selector) parseElementsRef++-- ############################ Element Instance Methods ##########################################++-- |+--+-- Return a spec to get the shadow root of an element given a 'SessionId' and 'ElementId'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#get-element-shadow-root)+--+-- @GET 	\/session\/{session id}\/element\/{element id}\/shadow 	Get Element Shadow Root@+getElementShadowRoot :: SessionId -> ElementId -> W3Spec ElementId+getElementShadowRoot sessionId elementId = Get "Get Element Shadow Root" (elementUri1 sessionId elementId "shadow") parseShadowElementRef++-- |+--+-- Return a spec to find an element from another element given a 'SessionId', 'ElementId', and 'Selector'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#find-element-from-element)+--+-- @POST 	\/session\/{session id}\/element\/{element id}\/element 	Find Element From Element@+findElementFromElement :: SessionId -> ElementId -> Selector -> W3Spec ElementId+findElementFromElement sessionId elementId selector = Post "Find Element From Element" (elementUri1 sessionId elementId "element") (selectorJson selector) parseElementRef++-- |+--+-- Return a spec to find elements from another element given a 'SessionId', 'ElementId', and 'Selector'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#find-elements-from-element)+--+-- @POST 	\/session\/{session id}\/element\/{element id}\/elements 	Find Elements From Element@+findElementsFromElement :: SessionId -> ElementId -> Selector -> W3Spec [ElementId]+findElementsFromElement sessionId elementId selector = Post "Find Elements From Element" (elementUri1 sessionId elementId "elements") (selectorJson selector) parseElementsRef++-- |+--+-- Return a spec to check if an element is selected given a 'SessionId' and 'ElementId'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#is-element-selected)+--+-- @GET 	\/session\/{session id}\/element\/{element id}\/selected 	Is Element Selected@+isElementSelected :: SessionId -> ElementId -> W3Spec Bool+isElementSelected sessionId elementId = Get "Is Element Selected" (elementUri1 sessionId elementId "selected") parseBodyBool++-- |+--+-- Return a spec to get an attribute of an element given a 'SessionId', 'ElementId', and attribute name.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#get-element-attribute)+--+-- @GET 	\/session\/{session id}\/element\/{element id}\/attribute\/{name} 	Get Element Attribute@+getElementAttribute :: SessionId -> ElementId -> Text -> W3Spec Text+getElementAttribute sessionId elementId attributeName = Get "Get Element Attribute" (elementUri2 sessionId elementId "attribute" attributeName) parseBodyTxt++-- |+--+-- Return a spec to get a property of an element given a 'SessionId', 'ElementId', and property name.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#get-element-property)+--+-- @GET 	\/session\/{session id}\/element\/{element id}\/property\/{name} 	Get Element Property@+getElementProperty :: SessionId -> ElementId -> Text -> W3Spec Value+getElementProperty sessionId elementId propertyName = Get "Get Element Property" (elementUri2 sessionId elementId "property" propertyName) bodyValue++-- |+--+-- Return a spec to get the CSS value of an element given a 'SessionId', 'ElementId', and CSS property name.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#get-element-css-value)+--+-- @GET 	\/session\/{session id}\/element\/{element id}\/css\/{property name} 	Get Element CSS Value@+getElementCssValue :: SessionId -> ElementId -> Text -> W3Spec Text+getElementCssValue sessionId elementId propertyName = Get "Get Element CSS Value" (elementUri2 sessionId elementId "css" propertyName) parseBodyTxt++-- |+--+-- Return a spec to get the text of an element given a 'SessionId' and 'ElementId'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#get-element-text)+--+-- @GET 	\/session\/{session id}\/element\/{element id}\/text 	Get Element Text@+getElementText :: SessionId -> ElementId -> W3Spec Text+getElementText sessionId elementId = Get "Get Element Text" (elementUri1 sessionId elementId "text") parseBodyTxt++-- |+--+-- Return a spec to get the tag name of an element given a 'SessionId' and 'ElementId'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#get-element-tag-name)+--+-- @GET 	\/session\/{session id}\/element\/{element id}\/name 	Get Element Tag Name@+getElementTagName :: SessionId -> ElementId -> W3Spec Text+getElementTagName sessionId elementId = Get "Get Element Tag Name" (elementUri1 sessionId elementId "name") parseBodyTxt++-- |+--+-- Return a spec to get the rect of an element given a 'SessionId' and 'ElementId'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#get-element-rect)+--+-- @GET 	\/session\/{session id}\/element\/{element id}\/rect 	Get Element Rect@+getElementRect :: SessionId -> ElementId -> W3Spec WindowRect+getElementRect sessionId elementId = Get "Get Element Rect" (elementUri1 sessionId elementId "rect") parseWindowRect++-- |+--+-- Return a spec to check if an element is enabled given a 'SessionId' and 'ElementId'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#is-element-enabled)+--+-- @GET 	\/session\/{session id}\/element\/{element id}\/enabled 	Is Element Enabled@+isElementEnabled :: SessionId -> ElementId -> W3Spec Bool+isElementEnabled sessionId elementId = Get "Is Element Enabled" (elementUri1 sessionId elementId "enabled") parseBodyBool++-- |+--+-- Return a spec to get the computed role of an element given a 'SessionId' and 'ElementId'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#get-computed-role)+--+-- @GET 	\/session\/{session id}\/element\/{element id}\/computedrole 	Get Computed Role@+getElementComputedRole :: SessionId -> ElementId -> W3Spec Text+getElementComputedRole sessionId elementId = Get "Get Computed Role" (elementUri1 sessionId elementId "computedrole") parseBodyTxt++-- |+--+-- Return a spec to get the computed label of an element given a 'SessionId' and 'ElementId'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#get-computed-label)+--+-- @GET 	\/session\/{session id}\/element\/{element id}\/computedlabel 	Get Computed Label@+getElementComputedLabel :: SessionId -> ElementId -> W3Spec Text+getElementComputedLabel sessionId elementId = Get "Get Computed Label" (elementUri1 sessionId elementId "computedlabel") parseBodyTxt++-- |+--+-- Return a spec to click an element given a 'SessionId' and 'ElementId'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#element-click)+--+-- @POST 	\/session\/{session id}\/element\/{element id}\/click 	Element Click@+elementClick :: SessionId -> ElementId -> W3Spec ()+elementClick sessionId elementId = PostEmpty "Element Click" (elementUri1 sessionId elementId "click") voidParser++-- |+--+-- Return a spec to clear an element given a 'SessionId' and 'ElementId'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#element-clear)+--+-- @POST 	\/session\/{session id}\/element\/{element id}\/clear 	Element Clear@+elementClear :: SessionId -> ElementId -> W3Spec ()+elementClear sessionId elementId = PostEmpty "Element Clear" (elementUri1 sessionId elementId "clear") voidParser++-- |+--+-- Return a spec to send keys to an element given a 'SessionId', 'ElementId', and keys to send.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#element-send-keys)+--+-- @POST 	\/session\/{session id}\/element\/{element id}\/value 	Element Send Keys@+elementSendKeys :: SessionId -> ElementId -> Text -> W3Spec ()+elementSendKeys sessionId elementId keysToSend = Post "Element Send Keys" (elementUri1 sessionId elementId "value") (keysJson keysToSend) voidParser++-- |+--+-- Return a spec to take a screenshot of an element given a 'SessionId' and 'ElementId'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#take-element-screenshot)+--+-- @GET 	\/session\/{session id}\/element\/{element id}\/screenshot 	Take Element Screenshot@+takeElementScreenshot :: SessionId -> ElementId -> W3Spec Text+takeElementScreenshot sessionId elementId = Get "Take Element Screenshot" (elementUri1 sessionId elementId "screenshot") parseBodyTxt++-- ############################ Shadow DOM Methods ##########################################++-- |+--+-- Return a spec to find an element from the shadow root given a 'SessionId', 'ElementId', and 'Selector'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#find-element-from-shadow-root)+--+-- @POST 	\/session\/{session id}\/shadow\/{shadow id}\/element 	Find Element From Shadow Root@+findElementFromShadowRoot :: SessionId -> ElementId -> Selector -> W3Spec ElementId+findElementFromShadowRoot sessionId shadowId selector = Post "Find Element From Shadow Root" (sessionUri3 sessionId "shadow" shadowId.id "element") (selectorJson selector) parseElementRef++-- |+--+-- Return a spec to find elements from the shadow root given a 'SessionId', 'ElementId', and 'Selector'.+--+-- [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#find-elements-from-shadow-root)+--+-- @POST 	\/session\/{session id}\/shadow\/{shadow id}\/elements 	Find Elements From Shadow Root@+findElementsFromShadowRoot :: SessionId -> ElementId -> Selector -> W3Spec [ElementId]+findElementsFromShadowRoot sessionId shadowId selector = Post "Find Elements From Shadow Root" (sessionUri3 sessionId "shadow" shadowId.id "elements") (selectorJson selector) parseElementsRef++-- ############################ Utils ##########################################++findElement' :: SessionId -> Value -> W3Spec ElementId+findElement' sessionRef selector = Post "Find Element" (sessionUri1 sessionRef "element") selector parseElementRef++-- | [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#dfn-get-element-rect)+data WindowRect = Rect+  { x :: Int,+    y :: Int,+    width :: Int,+    height :: Int+  }+  deriving (Show, Eq)++instance ToJSON WindowRect where+  toJSON :: WindowRect -> Value+  toJSON Rect {x, y, width, height} =+    object+      [ "x" .= x,+        "y" .= y,+        "width" .= width,+        "height" .= height+      ]++parseTimeouts :: HttpResponse -> Result Timeouts+parseTimeouts r = do+  r' <- bodyValue r+  fromJSON r'++parseWindowRect :: HttpResponse -> Result WindowRect+parseWindowRect r =+  do+    x <- bdyInt "x"+    y <- bdyInt "y"+    width <- bdyInt "width"+    height <- bdyInt "height"+    pure $ Rect {..}+  where+    bdyInt = bodyInt r++mkScript :: Text -> [Value] -> Value+mkScript script args = object ["script" .= script, "args" .= args]++windowHandleParser :: HttpResponse -> Result WindowHandleSpec+windowHandleParser r =+  bodyValue r+    >>= fromJSON++windowHandlesParser :: HttpResponse -> Result [WindowHandle]+windowHandlesParser r = do+  bodyValue r+    >>= \case+      Array a -> (Handle <$>) <$> (sequence . toList $ asText <$> a)+      v -> aesonTypeError "Array" v++-- windowHandleFromValue :: Value -> Maybe WindowHandleSpec+-- windowHandleFromValue v =+--   liftA2 HandleSpec (Handle <$> lookupTxt "handle" v) ( <$> lookupTxt "type" v)++parseCookies :: HttpResponse -> Result [Cookie]+parseCookies r =+  bodyValue r+    >>= \case+      Array a -> mapM cookieFromBody (toList a)+      v -> aesonTypeError "Array" v++parseCookie :: HttpResponse -> Result Cookie+parseCookie r =+  bodyValue r+    >>= cookieFromBody++cookieFromBody :: Value -> Result Cookie+cookieFromBody b = case b of+  Object kv -> do+    name <- lookupTxt "name" b+    value <- lookupTxt "value" b+    path <- opt' "path"+    domain <- opt' "domain"+    secure <- optBool "secure"+    httpOnly <- optBool "httpOnly"+    sameSite <- optBase toSameSite "sameSite"+    expiry <- optInt "expiry"+    pure $ MkCookie {..}+    where+      optBase :: (Value -> Result a) -> Key -> Result (Maybe a)+      optBase typeCaster k = AKM.lookup k kv & maybe (Success Nothing) (fmap Just . typeCaster)+      opt' = optBase asText+      optInt = optBase asInt+      optBool = optBase asBool+  v -> aesonTypeError "Object" v++selectorJson :: Selector -> Value+selectorJson = \case+  CSS css -> sJSON "css selector" css+  XPath xpath -> sJSON "xpath" xpath+  LinkText lt -> sJSON "link text" lt+  PartialLinkText plt -> sJSON "partial link text" plt+  TagName tn -> sJSON "tag name" tn+  where+    sJSON using value = object ["using" .= using, "value" .= value]++voidParser :: HttpResponse -> Result ()+voidParser _ = pure ()++bodyText' :: Result Value -> Key -> Result Text+bodyText' v k = v >>= lookupTxt k++bodyText :: HttpResponse -> Key -> Result Text+bodyText r = bodyText' (bodyValue r)++bodyInt' :: Result Value -> Key -> Result Int+bodyInt' v k = v >>= lookupInt k++bodyInt :: HttpResponse -> Key -> Result Int+bodyInt r = bodyInt' (bodyValue r)++parseBodyTxt :: HttpResponse -> Result Text+parseBodyTxt r = bodyValue r >>= asText++parseBodyBool :: HttpResponse -> Result Bool+parseBodyBool r =+  bodyValue r >>= asBool++asBool :: Value -> Result Bool+asBool = \case+  Bool b -> Success b+  v -> aesonTypeError "Bool" v++parseElementsRef :: HttpResponse -> Result [ElementId]+parseElementsRef r =+  bodyValue r+    >>= \case+      Array a -> mapM elemtRefFromBody $ toList a+      v -> aesonTypeError "Array" v++-- TODO Aeson helpers separate module+lookup :: Key -> Value -> Result Value+lookup k v =+  v & \case+    Object o -> AKM.lookup k o & maybe (A.Error ("the key: " <> show k <> "does not exist in the object:\n" <> jsonPrettyString v)) pure+    _ -> aesonTypeError "Object" v++lookupTxt :: Key -> Value -> Result Text+lookupTxt k v = lookup k v >>= asText++toSameSite :: Value -> Result SameSite+toSameSite = \case+  String "Lax" -> Success Lax+  String "Strict" -> Success Strict+  String "None" -> Success None+  v -> aesonTypeError' "SameSite" "Expected one of: Lax, Strict, None" v++lookupInt :: Key -> Value -> Result Int+lookupInt k v = lookup k v >>= asInt++aesonTypeErrorMessage :: Text -> Value -> Text+aesonTypeErrorMessage t v = "Expected Json Value to be of type: " <> t <> "\nbut got:\n" <> jsonToText v++aesonTypeError :: Text -> Value -> Result a+aesonTypeError t v = A.Error . unpack $ aesonTypeErrorMessage t v++aesonTypeError' :: Text -> Text -> Value -> Result a+aesonTypeError' typ info v = A.Error . unpack $ aesonTypeErrorMessage typ v <> "\n" <> info++asText :: Value -> Result Text+asText = \case+  String t -> Success t+  v -> aesonTypeError "Text" v++asInt :: Value -> Result Int+asInt = \case+  Number t -> Success $ floor t+  v -> aesonTypeError "Int" v++parseSessionRef :: HttpResponse -> Result SessionId+parseSessionRef r =+  Session+    <$> bodyText r "sessionId"++bodyValue :: HttpResponse -> Result Value+bodyValue r = lookup "value" r.body++-- https://www.w3.org/TR/webdriver2/#elements+elementFieldName :: Key+elementFieldName = "element-6066-11e4-a52e-4f735466cecf"++-- https://www.w3.org/TR/webdriver2/#shadow-root+shadowRootFieldName :: Key+shadowRootFieldName = "shadow-6066-11e4-a52e-4f735466cecf"++parseElementRef :: HttpResponse -> Result ElementId+parseElementRef r =+  Element <$> bodyText r elementFieldName++parseShadowElementRef :: HttpResponse -> Result ElementId+parseShadowElementRef r =+  Element <$> bodyText r shadowRootFieldName++elemtRefFromBody :: Value -> Result ElementId+elemtRefFromBody b = Element <$> lookupTxt elementFieldName b++session :: Text+session = "session"++sessionUri :: Text -> UrlPath+sessionUri sp = MkUrlPath [session, sp]++sessionUri1 :: SessionId -> Text -> UrlPath+sessionUri1 s sp = MkUrlPath [session, s.id, sp]++sessionUri2 :: SessionId -> Text -> Text -> UrlPath+sessionUri2 s sp sp2 = MkUrlPath [session, s.id, sp, sp2]++sessionUri3 :: SessionId -> Text -> Text -> Text -> UrlPath+sessionUri3 s sp sp2 sp3 = MkUrlPath [session, s.id, sp, sp2, sp3]++sessionUri4 :: SessionId -> Text -> Text -> Text -> Text -> UrlPath+sessionUri4 s sp sp2 sp3 sp4 = MkUrlPath [session, s.id, sp, sp2, sp3, sp4]++window :: Text+window = "window"++windowUri1 :: SessionId -> Text -> UrlPath+windowUri1 sr sp = sessionUri2 sr window sp++elementUri1 :: SessionId -> ElementId -> Text -> UrlPath+elementUri1 s er ep = sessionUri3 s "element" er.id ep++elementUri2 :: SessionId -> ElementId -> Text -> Text -> UrlPath+elementUri2 s er ep ep2 = sessionUri4 s "element" er.id ep ep2++jsonPrettyString :: Value -> String+jsonPrettyString = unpack . jsonToText++mkShowable :: W3Spec a -> W3SpecShowable+mkShowable = \case+  Get d p _ -> Request d "GET" p Nothing+  Post d p b _ -> Request d "POST" p (Just $ jsonToText b)+  PostEmpty d p _ -> Request d "POST" p Nothing+  Delete d p _ -> Request d "DELETE" p Nothing++parseDriverStatus :: HttpResponse -> Result DriverStatus+parseDriverStatus MkHttpResponse {statusCode, statusMessage} =+  Success $+    statusCode & \case+      200 -> Ready+      500 -> ServiceError {statusCode, statusMessage}+      501 -> Running+      _ -> Unknown {statusCode, statusMessage}++keysJson :: Text -> Value+keysJson keysToSend = object ["text" .= keysToSend]++-- actions+-- | [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#actions)+newtype Actions = MkActions {actions :: [Action]}++actionsToJson :: Actions -> Value+actionsToJson MkActions {actions} =+  object+    [ "actions" .= actions+    ]++-- | [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#actions)+data KeyAction+  = PauseKey {duration :: Maybe Int} -- ms+  | KeyDown+      { value :: Text+      }+  | KeyUp+      { value :: Text+      }+  deriving (Show, Eq)++instance ToJSON KeyAction where+  toJSON :: KeyAction -> Value+  toJSON PauseKey {duration} =+    object $+      [ "type" .= ("pause" :: Text)+      ]+        <> catMaybes [opt "duration" duration]+  toJSON KeyDown {value} =+    object+      [ "type" .= ("keyDown" :: Text),+        "value" .= String value+      ]+  toJSON KeyUp {value} =+    object+      [ "type" .= ("keyUp" :: Text),+        "value" .= String value+      ]++-- Pointer subtypes+-- | [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#actions)+data Pointer+  = Mouse+  | Pen+  | Touch+  deriving (Show, Eq)++mkLwrTxt :: (Show a) => a -> Value+mkLwrTxt = String . T.toLower . txt++instance ToJSON Pointer where+  toJSON :: Pointer -> Value+  toJSON = mkLwrTxt++-- | [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#actions)+data PointerOrigin+  = Viewport+  | OriginPointer+  | OriginElement ElementId+  deriving (Show, Eq)++instance ToJSON PointerOrigin where+  toJSON :: PointerOrigin -> Value+  toJSON = \case+    Viewport -> "viewport"+    OriginPointer -> "pointer"+    OriginElement (Element id') -> object ["element" .= id']+++-- | [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#actions)+data Action+  = NoneAction+      { id :: Text,+        -- the numeric id of the pointing device. This is a positive integer, with the values 0 and 1 reserved for mouse-type pointers.+        noneActions :: [Maybe Int] -- delay+      }+  | Key+      { id :: Text,+        keyActions :: [KeyAction]+        -- https://github.com/jlipps/simple-wd-spec?tab=readme-ov-file#perform-actions+        -- keys codepoint https://www.w3.org/TR/webdriver2/#keyboard-actions+      }+  | Pointer+      { id :: Text,+        subType :: Pointer,+        -- the numeric id of the pointing device. This is a positive integer, with the values 0 and 1 reserved for mouse-type pointers.+        pointerId :: Int,+        pressed :: Set Int, -- pressed buttons+        x :: Int, -- start x location in viewport coordinates.+        y :: Int, -- start y location in viewport coordinates+        actions :: [PointerAction]+      }+  | Wheel+      { id :: Text,+        wheelActions :: [WheelAction]+      }+  deriving (Show, Eq)++-- | [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#actions)+data WheelAction+  = PauseWheel {duration :: Maybe Int} -- ms+  | Scroll+      { origin :: PointerOrigin,+        x :: Int,+        y :: Int,+        deltaX :: Int,+        deltaY :: Int,+        duration :: Maybe Int -- ms+      }+  deriving (Show, Eq)++instance ToJSON WheelAction where+  toJSON :: WheelAction -> Value+  toJSON wa =+    object $ base <> catMaybes [opt "duration" wa.duration]+    where+      base = case wa of+        PauseWheel _ -> ["type" .= ("pause" :: Text)]+        Scroll+          { origin,+            x,+            y,+            deltaX,+            deltaY+          } ->+            [ "type" .= ("scroll" :: Text),+              "origin" .= origin,+              "x" .= x,+              "y" .= y,+              "deltaX" .= deltaX,+              "deltaY" .= deltaY+            ]++-- | [spec](https://www.w3.org/TR/2025/WD-webdriver2-20250306/#actions)+data PointerAction+  = PausePointer {duration :: Maybe Int} -- ms+  | Up+      { button :: Int,+        width :: Maybe Int,+        height :: Maybe Int,+        pressure :: Maybe Float, -- 0 -> 1+        tangentialPressure :: Maybe Float, -- -1 -> 1+        tiltX :: Maybe Int, -- -90 -> 90+        tiltY :: Maybe Int, -- -90 -> 90+        twist :: Maybe Int, -- 0 -> 359+        altitudeAngle :: Maybe Double, -- 0 -> pi/2+        azimuthAngle :: Maybe Double -- 0 -> 2pi-- button} -- button+      }+  | Down+      { button :: Int,+        width :: Maybe Int,+        height :: Maybe Int,+        pressure :: Maybe Float, -- 0 -> 1+        tangentialPressure :: Maybe Float, -- -1 -> 1+        tiltX :: Maybe Int, -- -90 -> 90+        tiltY :: Maybe Int, -- -90 -> 90+        twist :: Maybe Int, -- 0 -> 359+        altitudeAngle :: Maybe Double, -- 0 -> pi/2+        azimuthAngle :: Maybe Double -- 0 -> 2pi-- button+      }+  | Move+      { origin :: PointerOrigin,+        duration :: Maybe Int, -- ms+        -- where to move to+        -- though the spec seems to indicate width and height are double+        -- gecko driver was blowing up with anything other than int+        width :: Maybe Int,+        height :: Maybe Int,+        pressure :: Maybe Float, -- 0 -> 1+        tangentialPressure :: Maybe Float, -- -1 -> 1+        tiltX :: Maybe Int, -- -90 -> 90+        tiltY :: Maybe Int, -- -90 -> 90+        twist :: Maybe Int, -- 0 -> 359+        altitudeAngle :: Maybe Double, -- 0 -> pi/2+        azimuthAngle :: Maybe Double, -- 0 -> 2pi+        x :: Int,+        y :: Int+      }+  | -- looks like not supported yet by gecko driver 02-02-2025+    -- https://searchfox.org/mozilla-central/source/remote/shared/webdriver/Actions.sys.mjs#2340+    Cancel+  deriving (Show, Eq)++instance ToJSON PointerAction where+  toJSON :: PointerAction -> Value+  toJSON = \case+    PausePointer d ->+      object $+        ["type" .= ("pause" :: Text)]+          <> catMaybes [opt "duration" d]+    Up+      { -- https://www.w3.org/TR/pointerevents/#dom-pointerevent-pointerid+        button,+        width, -- magnitude on the X axis), in CSS pixels (see [CSS21]) -- default = 1+        height, -- (magnitude on the Y axis), in CSS pixels (see [CSS21]) -- default = 1+        pressure, -- 0 - 1+        tangentialPressure, -- -1 -> 1+        tiltX, -- -90 -> 90+        tiltY, -- -90 -> 90+        twist, -- 0 -> 359+        altitudeAngle, -- 0 -> pi/2+        azimuthAngle -- 0 -> 2pi+      } ->+        object $+          [ "type" .= ("pointerUp" :: Text),+            "button" .= button+          ]+            <> catMaybes+              [ opt "height" height,+                opt "width" width,+                opt "pressure" pressure,+                opt "tangentialPressure" tangentialPressure,+                opt "tiltX" tiltX,+                opt "tiltY" tiltY,+                opt "twist" twist,+                opt "altitudeAngle" altitudeAngle,+                opt "azimuthAngle" azimuthAngle+              ]+    Down+      { button,+        width,+        height,+        pressure,+        tangentialPressure, -- -1 -> 1+        tiltX, -- -90 -> 90+        tiltY, -- -90 -> 90+        twist, -- 0 -> 359+        altitudeAngle, -- 0 -> pi/2+        azimuthAngle -- 0 -> 2pi+      } ->+        object $+          [ "type" .= ("pointerDown" :: Text),+            "button" .= button+          ]+            <> catMaybes+              [ opt "height" height,+                opt "width" width,+                opt "pressure" pressure,+                opt "tangentialPressure" tangentialPressure,+                opt "tiltX" tiltX,+                opt "tiltY" tiltY,+                opt "twist" twist,+                opt "altitudeAngle" altitudeAngle,+                opt "azimuthAngle" azimuthAngle+              ]+    Move+      { origin,+        duration,+        width,+        height,+        pressure,+        tangentialPressure, -- -1 -> 1+        tiltX, -- -90 -> 90+        tiltY, -- -90 -> 90+        twist, -- 0 -> 359+        altitudeAngle, -- 0 -> pi/2+        azimuthAngle, -- 0 -> 2pi+        x,+        y+      } ->+        object $+          [ "type" .= ("pointerMove" :: Text),+            "origin" .= origin,+            "x" .= x,+            "y" .= y+          ]+            <> catMaybes+              [ opt "duration" duration,+                opt "height" height,+                opt "width" width,+                opt "pressure" pressure,+                opt "tangentialPressure" tangentialPressure,+                opt "tiltX" tiltX,+                opt "tiltY" tiltY,+                opt "twist" twist,+                opt "altitudeAngle" altitudeAngle,+                opt "azimuthAngle" azimuthAngle+              ]+    -- looks like Cancel not supported yet by gecko driver 02-02-2025+    -- https://searchfox.org/mozilla-central/source/remote/shared/webdriver/Actions.sys.mjs#2340+    Cancel -> object ["type" .= ("pointerCancel" :: Text)]++mkPause :: Maybe Int -> Value+mkPause d = object $ ["type" .= ("pause" :: Text)] <> catMaybes [opt "duration" d]++instance ToJSON Action where+  toJSON :: Action -> Value+  toJSON = \case+    NoneAction+      { id,+        noneActions+      } ->+        object+          [ "type" .= ("none" :: Text),+            "id" .= id,+            "actions" .= (mkPause <$> noneActions)+          ]+    Key {id, keyActions} ->+      object+        [ "id" .= id,+          "type" .= ("key" :: Text),+          "actions" .= keyActions+        ]+    Pointer+      { subType,+        actions,+        pointerId,+        pressed,+        id,+        x,+        y+      } ->+        object+          [ "id" .= id,+            "type" .= ("pointer" :: Text),+            "subType" .= subType,+            "pointerId" .= pointerId,+            "pressed" .= pressed,+            "x" .= x,+            "y" .= y,+            "actions" .= actions+          ]+    Wheel {id, wheelActions} ->+      object+        [ "id" .= id,+          "type" .= ("wheel" :: Text),+          "actions" .= wheelActions+        ]
+ test/ApiCoverageTest.hs view
@@ -0,0 +1,304 @@+module ApiCoverageTest where
+
+import Data.Set as S (Set, difference, fromList, null)
+import Data.Text as T (Text, lines, null, pack, replace, strip, unwords, words, intercalate)
+import GHC.Utils.Misc (filterOut)
+import Test.Tasty.HUnit as HUnit ( assertBool, Assertion )
+import Text.RawString.QQ (r)
+import WebDriverPreCore
+    ( SessionId(Session),
+      ElementId(Element),
+      Selector(CSS),
+      WindowHandle(Handle),
+      WindowRect(Rect),
+      FrameReference(TopLevelFrame),
+      Cookie(MkCookie),
+      Actions(MkActions),
+      Timeouts(..),
+      W3Spec(description, Get, Post, PostEmpty, Delete, path),
+      minFirefoxCapabilities,
+      acceptAlert,
+      addCookie,
+      back,
+      closeWindow,
+      deleteAllCookies,
+      deleteCookie,
+      deleteSession,
+      dismissAlert,
+      elementClear,
+      elementClick,
+      elementSendKeys,
+      executeScript,
+      executeScriptAsync,
+      findElement,
+      findElementFromElement,
+      findElementFromShadowRoot,
+      findElements,
+      findElementsFromElement,
+      findElementsFromShadowRoot,
+      forward,
+      fullscreenWindow,
+      getActiveElement,
+      getAlertText,
+      getAllCookies,
+      getCurrentUrl,
+      getElementAttribute,
+      getElementComputedLabel,
+      getElementComputedRole,
+      getElementCssValue,
+      getElementProperty,
+      getElementRect,
+      getElementShadowRoot,
+      getElementTagName,
+      getElementText,
+      getNamedCookie,
+      getPageSource,
+      getTimeouts,
+      getTitle,
+      getWindowHandle,
+      getWindowHandles,
+      getWindowRect,
+      isElementEnabled,
+      isElementSelected,
+      maximizeWindow,
+      minimizeWindow,
+      navigateTo,
+      newSession,
+      newWindow,
+      performActions,
+      printPage,
+      refresh,
+      releaseActions,
+      sendAlertText,
+      setTimeouts,
+      setWindowRect,
+      status,
+      switchToFrame,
+      switchToParentFrame,
+      switchToWindow,
+      takeElementScreenshot,
+      takeScreenshot )
+import GHC.Show (Show (..))
+import Data.Eq (Eq)
+import Data.Ord (Ord)
+import Data.Function (($), (.))
+import Data.Semigroup ((<>))
+import Data.List ((!!), drop)
+import Data.Functor ((<$>))
+import Data.Maybe (Maybe(..))
+import WebDriverPreCore (UrlPath(..))
+
+
+{-- TODO use Haddock variable
+ Covers Spec Version https://www.w3.org/TR/2025/WD-webdriver2-20250306 
+ --}
+
+
+{-
+!! Replace this the endepoints from the spec with every release
+https://www.w3.org/TR/2025/WD-webdriver2-20250306 - W3C Editor's Draft 10 February 2025
+61 endpoints
+Method 	URI Template 	Command
+POST 	/session 	New Session
+-}
+endPointsCopiedFromSpc :: Text
+endPointsCopiedFromSpc = pack 
+  [r|POST 	/session 	New Session
+DELETE 	/session/{session id} 	Delete Session
+GET 	/status 	Status
+GET 	/session/{session id}/timeouts 	Get Timeouts
+POST 	/session/{session id}/timeouts 	Set Timeouts
+POST 	/session/{session id}/url 	Navigate To
+GET 	/session/{session id}/url 	Get Current URL
+POST 	/session/{session id}/back 	Back
+POST 	/session/{session id}/forward 	Forward
+POST 	/session/{session id}/refresh 	Refresh
+GET 	/session/{session id}/title 	Get Title
+GET 	/session/{session id}/window 	Get Window Handle
+DELETE 	/session/{session id}/window 	Close Window
+POST 	/session/{session id}/window 	Switch To Window
+GET 	/session/{session id}/window/handles 	Get Window Handles
+POST 	/session/{session id}/window/new 	New Window
+POST 	/session/{session id}/frame 	Switch To Frame
+POST 	/session/{session id}/frame/parent 	Switch To Parent Frame
+GET 	/session/{session id}/window/rect 	Get Window Rect
+POST 	/session/{session id}/window/rect 	Set Window Rect
+POST 	/session/{session id}/window/maximize 	Maximize Window
+POST 	/session/{session id}/window/minimize 	Minimize Window
+POST 	/session/{session id}/window/fullscreen 	Fullscreen Window
+GET 	/session/{session id}/element/active 	Get Active Element
+GET 	/session/{session id}/element/{element id}/shadow 	Get Element Shadow Root
+POST 	/session/{session id}/element 	Find Element
+POST 	/session/{session id}/elements 	Find Elements
+POST 	/session/{session id}/element/{element id}/element 	Find Element From Element
+POST 	/session/{session id}/element/{element id}/elements 	Find Elements From Element
+POST 	/session/{session id}/shadow/{shadow id}/element 	Find Element From Shadow Root
+POST 	/session/{session id}/shadow/{shadow id}/elements 	Find Elements From Shadow Root
+GET 	/session/{session id}/element/{element id}/selected 	Is Element Selected
+GET 	/session/{session id}/element/{element id}/attribute/{name} 	Get Element Attribute
+GET 	/session/{session id}/element/{element id}/property/{name} 	Get Element Property
+GET 	/session/{session id}/element/{element id}/css/{property name} 	Get Element CSS Value
+GET 	/session/{session id}/element/{element id}/text 	Get Element Text
+GET 	/session/{session id}/element/{element id}/name 	Get Element Tag Name
+GET 	/session/{session id}/element/{element id}/rect 	Get Element Rect
+GET 	/session/{session id}/element/{element id}/enabled 	Is Element Enabled
+GET 	/session/{session id}/element/{element id}/computedrole 	Get Computed Role
+GET 	/session/{session id}/element/{element id}/computedlabel 	Get Computed Label
+POST 	/session/{session id}/element/{element id}/click 	Element Click
+POST 	/session/{session id}/element/{element id}/clear 	Element Clear
+POST 	/session/{session id}/element/{element id}/value 	Element Send Keys
+GET 	/session/{session id}/source 	Get Page Source
+POST 	/session/{session id}/execute/sync 	Execute Script
+POST 	/session/{session id}/execute/async 	Execute Async Script
+GET 	/session/{session id}/cookie 	Get All Cookies
+GET 	/session/{session id}/cookie/{name} 	Get Named Cookie
+POST 	/session/{session id}/cookie 	Add Cookie
+DELETE 	/session/{session id}/cookie/{name} 	Delete Cookie
+DELETE 	/session/{session id}/cookie 	Delete All Cookies
+POST 	/session/{session id}/actions 	Perform Actions
+DELETE 	/session/{session id}/actions 	Release Actions
+POST 	/session/{session id}/alert/dismiss 	Dismiss Alert
+POST 	/session/{session id}/alert/accept 	Accept Alert
+GET 	/session/{session id}/alert/text 	Get Alert Text
+POST 	/session/{session id}/alert/text 	Send Alert Text
+GET 	/session/{session id}/screenshot 	Take Screenshot
+GET 	/session/{session id}/element/{element id}/screenshot 	Take Element Screenshot
+POST 	/session/{session id}/print   Print Page
+|]
+
+parseLine :: Text -> SpecLine
+parseLine line = MkSpecLine method uriTemplate command
+  where
+    segments = filterOut T.null $ T.words $ replaceTemplateTxt line
+    method = case segments of
+      [] -> "PARSER ERROR NO METHOD IN LINE" <> line
+      x : _ -> x
+    uriTemplate = segments !! 1
+    command = T.unwords $ drop 2 segments
+
+specLinesFromSpec :: Set SpecLine
+specLinesFromSpec = fromList $ parseLine <$> filterOut T.null (strip <$> T.lines endPointsCopiedFromSpc)
+
+sessionId :: Text
+sessionId = "session_id"
+
+session :: SessionId
+session = Session sessionId
+
+elementId :: Text
+elementId = "element_id"
+
+element :: ElementId
+element = Element elementId
+
+windowHandle :: WindowHandle
+windowHandle = Handle "window-handle"
+
+selector :: Selector
+selector = CSS "Blahh"
+
+name :: Text
+name = "name"
+
+data SpecLine = MkSpecLine
+  { method :: Text,
+    uriTemplate :: Text,
+    command :: Text
+  }
+  deriving (Show, Eq, Ord)
+
+replaceTemplateTxt :: Text -> Text
+replaceTemplateTxt =
+  replace "{property name}" name
+    . replace "{name}" name
+    . replace "{session id}" sessionId
+    . replace "{element id}" elementId
+    . replace "{shadow id}" elementId
+
+toSpecLine :: W3Spec a -> SpecLine
+toSpecLine w3 = case w3 of
+  Get {} -> MkSpecLine "GET" path command
+  Post {} -> MkSpecLine "POST" path command
+  PostEmpty {} -> MkSpecLine "POST" path command
+  Delete {} -> MkSpecLine "DELETE" path command
+  where
+    command = w3.description
+    path = replaceTemplateTxt $ "/" <> intercalate "/" w3.path.segments
+
+allSpecsSample :: Set SpecLine
+allSpecsSample =
+  fromList
+    [ toSpecLine $ newSession minFirefoxCapabilities,
+      toSpecLine status,
+      toSpecLine $ maximizeWindow session,
+      toSpecLine $ minimizeWindow session,
+      toSpecLine $ fullscreenWindow session,
+      toSpecLine $ getTimeouts session,
+      toSpecLine $ setTimeouts session $ MkTimeouts Nothing Nothing Nothing,
+      toSpecLine $ switchToFrame session TopLevelFrame,
+      toSpecLine $ getCurrentUrl session,
+      toSpecLine $ findElementFromElement session element selector,
+      toSpecLine $ findElementsFromElement session element selector,
+      toSpecLine $ findElements session selector,
+      toSpecLine $ getTitle session,
+      toSpecLine $ getWindowHandle session,
+      toSpecLine $ isElementSelected session element,
+      toSpecLine $ closeWindow session,
+      toSpecLine $ back session,
+      toSpecLine $ forward session,
+      toSpecLine $ refresh session,
+      toSpecLine $ newSession minFirefoxCapabilities,
+      toSpecLine $ deleteSession session,
+      toSpecLine $ getActiveElement session,
+      toSpecLine $ getWindowHandles session,
+      toSpecLine $ newWindow session,
+      toSpecLine $ switchToWindow session windowHandle,
+      toSpecLine $ navigateTo session "url",
+      toSpecLine $ findElement session selector,
+      toSpecLine $ getWindowRect session,
+      toSpecLine $ elementClick session element,
+      toSpecLine $ getElementText session element,
+      toSpecLine $ switchToParentFrame session,
+      toSpecLine $ getElementProperty session element name,
+      toSpecLine $ getElementAttribute session element name,
+      toSpecLine $ getElementCssValue session element name,
+      toSpecLine $ setWindowRect session (Rect 0 0 1280 720),
+      toSpecLine $ findElementsFromShadowRoot session element selector,
+      toSpecLine $ getElementShadowRoot session element,
+      toSpecLine $ findElementFromShadowRoot session element selector,
+      toSpecLine $ getElementTagName session element,
+      toSpecLine $ getElementRect session element,
+      toSpecLine $ isElementEnabled session element,
+      toSpecLine $ getElementComputedRole session element,
+      toSpecLine $ getElementComputedLabel session element,
+      toSpecLine $ elementClear session element,
+      toSpecLine $ elementSendKeys session element "Some keys",
+      toSpecLine $ getPageSource session,
+      toSpecLine $ takeScreenshot session,
+      toSpecLine $ takeElementScreenshot session element,
+      toSpecLine $ printPage session,
+      toSpecLine $ executeScript session "console.log('test');" [],
+      toSpecLine $ executeScriptAsync session "console.log('test');" [],
+      toSpecLine $ getAllCookies session,
+      toSpecLine $ getNamedCookie session name,
+      toSpecLine $ addCookie session $ MkCookie "testCookie" "testValue" Nothing Nothing Nothing Nothing Nothing Nothing,
+      toSpecLine $ deleteCookie session name,
+      toSpecLine $ deleteAllCookies session,
+      toSpecLine $ dismissAlert session,
+      toSpecLine $ acceptAlert session,
+      toSpecLine $ getAlertText session,
+      toSpecLine $ sendAlertText session "Test alert",
+      toSpecLine $ performActions session $ MkActions [],
+      toSpecLine $ releaseActions session
+    ]
+
+-- >>> unit_test_all_endpoints_covered
+unit_test_all_endpoints_covered :: Assertion
+unit_test_all_endpoints_covered = do
+  -- print allSpecsSample
+  -- putStrLn ""
+  assertBool ("Missing specs:\n " <> show missing) (S.null missing)
+  assertBool ("Extra specs:\n " <> show extra) (S.null extra)
+  where
+    missing = specLinesFromSpec `difference` allSpecsSample
+    extra = allSpecsSample `difference` specLinesFromSpec
+ test/Driver.hs view
@@ -0,0 +1,3 @@+{-# OPTIONS_GHC -F -pgmF tasty-discover #-}++-- add tasty discover to dev container / image -- cabal install tasty-discover
+ test/ErrorCoverageTest.hs view
@@ -0,0 +1,109 @@+module ErrorCoverageTest where
+
+import Data.Set as S (Set, difference, fromList, null)
+import Data.Text as T (Text, lines, null, pack, strip, split)
+import GHC.Utils.Misc (filterOut)
+import Test.Tasty.HUnit as HUnit ( assertBool, Assertion, (@=?) )
+import Text.RawString.QQ (r)
+import WebDriverPreCore
+    ( errorTypeToErrorCode,
+      errorDescription,
+      errorCodeToErrorType )
+import GHC.Show (Show (..))
+import Data.Eq (Eq ((==)))
+import Data.Ord (Ord)
+import Data.Function (($), (.), (&))
+import Data.Semigroup ((<>))
+import Data.Functor ((<$>))
+import GHC.Base (error)
+import WebDriverPreCore.Internal.Utils (enumerate)
+import Data.Foldable (traverse_)
+import Data.Either (either)
+
+{-
+!! Replace this the endepoints from the spec with every release
+!! remove full stops and replace tabs with " | "
+https://www.w3.org/TR/2025/WD-webdriver2-20250306 - W3C Editor's Draft 10 February 2025
+61 endpoints
+Error Code 	HTTP Status 	JSON Error Code 	Description 
+-}
+errorsFromSpec :: Text
+errorsFromSpec = pack
+  [r|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 
+|]
+
+data ErrorLine = MkErrorLine
+  { 
+    jsonErrorCode :: Text,
+    description :: Text
+  }
+  deriving (Show, Eq, Ord)
+
+-- >>> allErrorsFromSpec
+-- fromList [MkErrorLine {jsonErrorCode = "detached shadow root", description = "A command failed because the referenced shadow root is no longer attached to the DOM"},MkErrorLine {jsonErrorCode = "element click intercepted", description = "The Element Click command could not be completed because the element receiving the events is obscuring the element that was requested clicked"},MkErrorLine {jsonErrorCode = "element not interactable", description = "A command could not be completed because the element is not pointer- or keyboard interactable"},MkErrorLine {jsonErrorCode = "insecure certificate", description = "Navigation caused the user agent to hit a certificate warning, which is usually the result of an expired or invalid TLS certificate"},MkErrorLine {jsonErrorCode = "invalid argument", description = "The arguments passed to a command are either invalid or malformed"},MkErrorLine {jsonErrorCode = "invalid cookie domain", description = "An illegal attempt was made to set a cookie under a different domain than the current page"},MkErrorLine {jsonErrorCode = "invalid element state", description = "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"},MkErrorLine {jsonErrorCode = "invalid selector", description = "Argument was an invalid selector"},MkErrorLine {jsonErrorCode = "invalid session id", description = "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"},MkErrorLine {jsonErrorCode = "javascript error", description = "An error occurred while executing JavaScript supplied by the user"},MkErrorLine {jsonErrorCode = "move target out of bounds", description = "The target for mouse interaction is not in the browser's viewport and cannot be brought into that viewport"},MkErrorLine {jsonErrorCode = "no such alert", description = "An attempt was made to operate on a modal dialog when one was not open"},MkErrorLine {jsonErrorCode = "no such cookie", description = "No cookie matching the given path name was found amongst the associated cookies of session's current browsing context's active document"},MkErrorLine {jsonErrorCode = "no such element", description = "An element could not be located on the page using the given search parameters"},MkErrorLine {jsonErrorCode = "no such frame", description = "A command to switch to a frame could not be satisfied because the frame could not be found"},MkErrorLine {jsonErrorCode = "no such shadow root", description = "The element does not have a shadow root"},MkErrorLine {jsonErrorCode = "no such window", description = "A command to switch to a window could not be satisfied because the window could not be found"},MkErrorLine {jsonErrorCode = "script timeout", description = "A script did not complete before its timeout expired"},MkErrorLine {jsonErrorCode = "session not created", description = "A new session could not be created"},MkErrorLine {jsonErrorCode = "stale element reference", description = "A command failed because the referenced element is no longer attached to the DOM"},MkErrorLine {jsonErrorCode = "timeout", description = "An operation did not complete before its timeout expired"},MkErrorLine {jsonErrorCode = "unable to capture screen", description = "A screen capture was made impossible"},MkErrorLine {jsonErrorCode = "unable to set cookie", description = "A command to set a cookie's value could not be satisfied"},MkErrorLine {jsonErrorCode = "unexpected alert open", description = "A modal dialog was open, blocking this operation"},MkErrorLine {jsonErrorCode = "unknown command", description = "A command could not be executed because the remote end is not aware of it"},MkErrorLine {jsonErrorCode = "unknown error", description = "An unknown error occurred in the remote end while processing the command"},MkErrorLine {jsonErrorCode = "unknown method", description = "The requested command matched a known URL but did not match any method for that URL"},MkErrorLine {jsonErrorCode = "unsupported operation", description = "Indicates that a command that should have executed properly cannot be supported for some reason"}]
+allErrorsFromSpec :: Set ErrorLine
+allErrorsFromSpec = fromList $ parseErrorLine <$> filterOut T.null (T.lines errorsFromSpec)
+ where
+  parseErrorLine :: Text -> ErrorLine
+  parseErrorLine line = 
+    T.split (== '|') line & \case
+      [_errrorCode, _httpStatus , jsonErrorCode, description] -> MkErrorLine (strip jsonErrorCode) $ strip description
+      _ -> error $ "Error parsing line: " <> show line
+
+-- >>> allErrors
+-- fromList [MkErrorLine {jsonErrorCode = "detached shadow root", description = "A command failed because the referenced shadow root is no longer attached to the DOM"},MkErrorLine {jsonErrorCode = "element click intercepted", description = "The Element Click command could not be completed because the element receiving the events is obscuring the element that was requested clicked"},MkErrorLine {jsonErrorCode = "element not interactable", description = "A command could not be completed because the element is not pointer- or keyboard interactable"},MkErrorLine {jsonErrorCode = "insecure certificate", description = "Navigation caused the user agent to hit a certificate warning, which is usually the result of an expired or invalid TLS certificate"},MkErrorLine {jsonErrorCode = "invalid argument", description = "The arguments passed to a command are either invalid or malformed"},MkErrorLine {jsonErrorCode = "invalid cookie domain", description = "An illegal attempt was made to set a cookie under a different domain than the current page"},MkErrorLine {jsonErrorCode = "invalid element state", description = "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"},MkErrorLine {jsonErrorCode = "invalid selector", description = "Argument was an invalid selector"},MkErrorLine {jsonErrorCode = "invalid session id", description = "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"},MkErrorLine {jsonErrorCode = "javascript error", description = "An error occurred while executing JavaScript supplied by the user"},MkErrorLine {jsonErrorCode = "move target out of bounds", description = "The target for mouse interaction is not in the browser's viewport and cannot be brought into that viewport"},MkErrorLine {jsonErrorCode = "no such alert", description = "An attempt was made to operate on a modal dialog when one was not open"},MkErrorLine {jsonErrorCode = "no such cookie", description = "No cookie matching the given path name was found amongst the associated cookies of session's current browsing context's active document"},MkErrorLine {jsonErrorCode = "no such element", description = "An element could not be located on the page using the given search parameters"},MkErrorLine {jsonErrorCode = "no such frame", description = "A command to switch to a frame could not be satisfied because the frame could not be found"},MkErrorLine {jsonErrorCode = "no such shadow root", description = "The element does not have a shadow root"},MkErrorLine {jsonErrorCode = "no such window", description = "A command to switch to a window could not be satisfied because the window could not be found"},MkErrorLine {jsonErrorCode = "script timeout", description = "A script did not complete before its timeout expired"},MkErrorLine {jsonErrorCode = "session not created", description = "A new session could not be created"},MkErrorLine {jsonErrorCode = "stale element reference", description = "A command failed because the referenced element is no longer attached to the DOM"},MkErrorLine {jsonErrorCode = "timeout", description = "An operation did not complete before its timeout expired"},MkErrorLine {jsonErrorCode = "unable to capture screen", description = "A screen capture was made impossible"},MkErrorLine {jsonErrorCode = "unable to set cookie", description = "A command to set a cookie's value could not be satisfied"},MkErrorLine {jsonErrorCode = "unexpected alert open", description = "A modal dialog was open, blocking this operation"},MkErrorLine {jsonErrorCode = "unknown command", description = "A command could not be executed because the remote end is not aware of it"},MkErrorLine {jsonErrorCode = "unknown error", description = "An unknown error occurred in the remote end while processing the command"},MkErrorLine {jsonErrorCode = "unknown method", description = "The requested command matched a known URL but did not match any method for that URL"},MkErrorLine {jsonErrorCode = "unsupported operation", description = "Indicates that a command that should have executed properly cannot be supported for some reason"}]
+allErrors :: Set ErrorLine
+allErrors = fromList $
+ (\et -> MkErrorLine { 
+    jsonErrorCode  = errorTypeToErrorCode et,
+    description = errorDescription et
+  })  <$> enumerate
+
+-- >>> unit_test_all_errors_covered
+unit_test_all_errors_covered :: Assertion
+unit_test_all_errors_covered = do
+  -- print allErrors
+  -- putStrLn ""
+  assertBool ("Missing errors (in spec not captured by WebDriverErrorType):\n " <> show missing) (S.null missing)
+  assertBool ("Extra errors (in WebDriverErrorType but not in spec):\n " <> show extra) (S.null extra)
+  where
+    missing = allErrors `difference` allErrorsFromSpec
+    extra = allErrorsFromSpec `difference` allErrors
+
+
+-- >>> unit_round_trip_error_codes
+unit_round_trip_error_codes :: Assertion
+unit_round_trip_error_codes = do
+  traverse_ checkRoundTripErrorCodes enumerate
+  where
+    checkRoundTripErrorCodes errorType = do
+      let errorCode = errorTypeToErrorCode errorType
+          errorType' = errorCodeToErrorType errorCode
+      errorType' & either (error . show) (errorType @=?)
+ test/JSONParsingTest.hs view
@@ -0,0 +1,488 @@+module JSONParsingTest where
+
+import Data.Aeson (ToJSON (toJSON), Value (..), decode, encode)
+import Data.Aeson.KeyMap qualified as KM
+
+import Data.Bool (Bool, (&&), (||))
+import Data.Enum (Bounded (minBound), Enum, maxBound)
+import Data.Foldable (all, null)
+import Data.Function (($), (.), id)
+import Data.Functor ((<$>))
+import Data.Map.Strict qualified as M
+import Data.Maybe (Maybe (..), isNothing)
+import Data.String (String)
+import Data.Text (Text, unpack)
+import Data.Text qualified as T
+import GHC.Base (Applicative (..), Bool (..), Eq (..), Int, const, Functor (..))
+import GHC.Data.Maybe (maybe)
+import GHC.Float (Double)
+import GHC.IO (FilePath)
+import Test.Falsify.Generator as G
+  ( Gen,
+    bool,
+    frequency,
+    inRange,
+    list,
+  )
+import Test.Falsify.Predicate (dot, expect, fn, (.$))
+import Test.Falsify.Range as R (between, enum)
+import Test.Tasty (TestTree)
+import Test.Tasty.Falsify
+  ( ExpectFailure (DontExpectFailure),
+    Property,
+    TestOptions (..),
+    Verbose (Verbose),
+    assert,
+    gen,
+    info,
+    testPropertyWith,
+  )
+import Text.Show.Pretty (ppShow)
+import WebDriverPreCore
+  ( Capabilities (..),
+    DeviceMetrics (..),
+    LogLevel (..),
+    MobileEmulation (..),
+    PerfLoggingPrefs (..),
+    Proxy (..),
+    SocksProxy (..),
+    Timeouts (..),
+    VendorSpecific (..), LogSettings (MkLogSettings)
+  )
+import WebDriverPreCore.Internal.Utils (jsonToText)
+import GHC.Real (Integral, Fractional (..), fromIntegral)
+import Data.Bits (FiniteBits)
+import GHC.Num (Num(..))
+
+genMaybe :: G.Gen a -> G.Gen (Maybe a)
+genMaybe gen' =
+  G.frequency
+    [ (1, pure Nothing),
+      (3, Just <$> gen')
+    ]
+
+genOneOf :: forall a. (Enum a, Bounded a) => G.Gen a
+genOneOf = G.inRange $ R.enum (minBound @a, maxBound @a)
+
+genMEnum :: forall a. (Enum a, Bounded a) => G.Gen (Maybe a)
+genMEnum = genMaybe $ genOneOf @a
+
+genTextValueMap :: G.Gen (M.Map Text Value)
+genTextValueMap =
+  M.fromList
+    <$> G.list
+      (R.between (0, 4))
+      ((,) <$> genText <*> genValue)
+  where
+    genValue :: G.Gen Value
+    genValue =
+      G.frequency
+        [ (1, pure $ toJSON False),
+          (1, pure $ toJSON ""),
+          (1, pure $ toJSON 0),
+          (1, pure $ toJSON 1),
+          (1, pure $ toJSON 2),
+          (1, pure $ toJSON 3),
+          (1, pure $ toJSON 4),
+          (1, pure $ toJSON 5),
+          (1, pure $ toJSON 6),
+          (1, pure $ toJSON 7),
+          (1, pure $ toJSON 8),
+          (1, pure $ toJSON 9)
+        ]
+
+genDeviseMetrics :: Gen DeviceMetrics
+genDeviseMetrics = do
+  width <- genInt 0 10000
+  height <- genInt 0 10000
+  pixelRatio <- genDouble 0.0 10000.0
+  touch <- genBool
+  pure $ MkDeviceMetrics {..}
+
+genMobileEmulation :: Gen MobileEmulation
+genMobileEmulation = MkMobileEmulation <$> genMaybeText <*> genMaybe genDeviseMetrics <*> genMaybeText
+
+genPerfLoggingPrefs :: G.Gen PerfLoggingPrefs
+genPerfLoggingPrefs = do
+  enableNetwork <- genMaybeBool
+  enablePage <- genMaybeBool
+  enableTimeline <- genMaybeBool
+  traceCategories <- genMaybeText
+  bufferUsageReportingInterval <- genMaybeInt
+  pure $ MkPerfLoggingPrefs {..}
+
+genVendorSpecific :: G.Gen VendorSpecific
+genVendorSpecific =
+  G.frequency
+    [ ( 1,
+        -- edge and chrome options are the same at this stage
+        ChromeOptions
+          <$> genMaybeTextList
+            <*> genMaybeText
+            <*> genMaybeTextList
+            <*> genMaybe genTextValueMap
+            <*> genMaybe genMobileEmulation
+            <*> genMaybe genTextValueMap
+            <*> genMaybeBool
+            <*> genMaybe genText
+            <*> genMaybeTextList
+            <*> genMaybe genFilePath
+            <*> genMaybe genPerfLoggingPrefs
+            <*> genMaybeTextList
+      ),
+      ( 1,
+        EdgeOptions
+          <$> genMaybeTextList
+            <*> genMaybeText
+            <*> genMaybeTextList
+            <*> genMaybe genTextValueMap
+            <*> genMaybe genMobileEmulation
+            <*> genMaybe genTextValueMap
+            <*> genMaybeBool
+            <*> genMaybe genText
+            <*> genMaybeTextList
+            <*> genMaybe genFilePath
+            <*> genMaybe genPerfLoggingPrefs
+            <*> genMaybeTextList
+      ),
+      (1, FirefoxOptions <$> genMaybeTextList <*> genMaybeText <*> genMaybeText <*> genMaybe genLogSettings),
+      (1, SafariOptions <$> genMaybeBool <*> genMaybeBool)
+    ]
+
+genLogLevel :: Gen LogLevel
+genLogLevel =
+  G.frequency
+    [ (1, pure Trace),
+      (1, pure Debug),
+      (1, pure Info),
+      (1, pure Warning),
+      (1, pure Error),
+      (1, pure Fatal),
+      (1, pure Off)
+    ]
+
+genLogSettings :: G.Gen LogSettings
+genLogSettings = do
+  level <- genLogLevel
+  pure $ MkLogSettings level
+
+genBool :: G.Gen Bool
+genBool = bool False
+
+genMaybeBool :: G.Gen (Maybe Bool)
+genMaybeBool = genMaybe genBool
+
+genMaybeTimeout :: G.Gen (Maybe Int)
+genMaybeTimeout = genMaybeInt
+
+genMaybeInt :: G.Gen (Maybe Int)
+genMaybeInt = genMaybe $ genInt 0 10000
+
+genTimeouts :: G.Gen Timeouts
+genTimeouts =
+  MkTimeouts
+    <$> genMaybeTimeout
+      <*> genMaybeTimeout
+      <*> genMaybeTimeout
+
+genText :: G.Gen Text
+genText =
+  G.frequency
+    [ (1, pure ""),
+      (1, pure "Awd34rtf"),
+      (1, pure "f"),
+      (1, pure "sfdsds"),
+      (1, pure "5"),
+      (1, pure "ttttttttttttt"),
+      (1, pure "a"),
+      (1, pure "zzzzz"),
+      (1, pure "rr"),
+      (1, pure "aa"),
+      (1, pure "pppp")
+    ]
+
+genFilePath :: G.Gen FilePath
+genFilePath =
+  G.frequency
+    [ (1, pure "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"),
+      (1, pure "C:\\Program Files\\Mozilla Firefox\\firefox.exe"),
+      (1, pure "C:\\Program Files\\Safari\\Safari.exe"),
+      (1, pure "C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe"),
+      (1, pure "C:\\Program Files (x86)\\Internet Explorer\\iexplore.exe"),
+      (1, pure "C:\\Program Files (x86)\\Opera Software\\Opera Stable\\opera.exe")
+    ]
+
+genTextList :: G.Gen [Text]
+genTextList = list (R.between (0, 4)) genText
+
+genMaybeTextList :: G.Gen (Maybe [Text])
+genMaybeTextList = genMaybe genTextList
+
+genMaybeText :: G.Gen (Maybe Text)
+genMaybeText = genMaybe genText
+
+genManualProxy :: G.Gen Proxy
+genManualProxy =
+  Manual
+    <$> genMaybeText
+      <*> genMaybeText
+      <*> genMaybeText
+      <*> genMaybe genSocksProxy
+      <*> (genMaybe genTextList)
+
+
+genFromRange :: (Integral a,  FiniteBits a) => a -> a -> Gen a
+genFromRange lb ub = G.inRange $ R.between (lb, ub)
+
+genInt :: Int -> Int -> G.Gen Int
+genInt = genFromRange @Int
+
+genDouble :: Double -> Double -> G.Gen Double
+genDouble lb ub = do
+  -- Generate an integer in [0..1000000].
+  bits <- G.inRange (R.between (0, 1000000 :: Int))
+  -- Convert bits to a fraction in [0..1], then interpolate between lb and ub.
+  let fraction = fromIntegral bits / 1000000
+  pure $ lb + fraction * (ub - lb)
+
+genSocksProxy :: G.Gen SocksProxy
+genSocksProxy = MkSocksProxy <$> genText <*> genInt 1 7
+
+genProxy :: G.Gen Proxy
+genProxy =
+  G.frequency
+    [ (1, pure Direct),
+      (1, genManualProxy),
+      (1, pure AutoDetect),
+      (1, pure System),
+      (1, Pac <$> genText)
+    ]
+
+-- Generate random Capabilities
+genCapabilities :: G.Gen Capabilities
+genCapabilities = do
+  browserName <- genMEnum
+  browserVersion <- genMaybe genText
+  platformName <- genMEnum
+  strictFileInteractability <- genMaybe genBool
+  unhandledPromptBehavior <- genMEnum
+  acceptInsecureCerts <- genMaybe genBool
+  pageLoadStrategy <- genMEnum
+  proxy <- genMaybe genProxy
+  timeouts <- genMaybe genTimeouts
+  vendorSpecific <- genMaybe genVendorSpecific
+  pure MkCapabilities {..}
+
+options :: TestOptions
+options =
+  TestOptions
+    { expectFailure = DontExpectFailure,
+      overrideVerbose = if wantLogging then Just Verbose else Nothing,
+      overrideMaxShrinks = Nothing,
+      overrideNumTests = Just 1000,
+      overrideMaxRatio = Nothing
+    }
+
+
+subEmt :: forall a. (a -> Bool) -> Maybe a -> Maybe a
+subEmt f = maybe Nothing (\x -> if f x then Nothing else Just x)
+
+subEmptyTxt :: Maybe Text -> Maybe Text
+subEmptyTxt = subEmt T.null
+
+subEmptyList :: Maybe String -> Maybe String
+subEmptyList = subEmt null 
+
+subEmptyTxtLst :: Maybe [Text] -> Maybe [Text]
+subEmptyTxtLst = subEmt emptyTextList
+
+
+emptyVal :: Value -> Bool
+emptyVal = \case
+  Bool _ -> False
+  String t -> T.null t
+  Number _ -> False
+  Array arr -> null arr
+  Object o -> KM.null o
+  Null -> True
+
+subEmtyValueMap :: Maybe (M.Map Text Value) -> Maybe (M.Map Text Value)
+subEmtyValueMap = fmap M.fromList . subEmt emptyValList . fmap M.toList
+ where
+  emptyValList :: [(Text, Value)] -> Bool
+  emptyValList ml = null ml || all (\(t, v) -> T.null t || emptyVal v) ml
+
+-- | Substitutes empty fields with Nothing
+subEmptyVendor :: Maybe VendorSpecific -> Maybe VendorSpecific
+subEmptyVendor = subEmt allPropsNothing . fmap subEmptFields
+ where 
+  allPropsNothing :: VendorSpecific -> Bool
+  allPropsNothing = \case 
+    ChromeOptions p1 p2 p3 p4 p5 p6 p7 p8 p9 p10 p11 p12 -> allTrue [n p1, n p2, n p3, n p4, n p5, n p6, n p7, n p8, n p9, n p10, n p11, n p12]
+    EdgeOptions p1 p2 p3 p4 p5 p6 p7 p8 p9 p10 p11 p12 -> allTrue [n p1, n p2, n p3, n p4, n p5, n p6, n p7, n p8, n p9, n p10, n p11, n p12]
+    FirefoxOptions p1 p2 p3 p4 -> allTrue [n p1, n p2, n p3, n p4]
+    SafariOptions p1 p2-> allTrue [n p1, n p2]
+    
+    where 
+      n = isNothing
+      allTrue :: [Bool] -> Bool
+      allTrue = all id
+
+  subEmptFields :: VendorSpecific -> VendorSpecific
+  subEmptFields = \case
+    ChromeOptions {..} ->
+      ChromeOptions
+        { chromeArgs = subEmptyTxtLst chromeArgs,
+          chromeBinary = subEmptyTxt chromeBinary,
+          chromeExtensions = subEmptyTxtLst chromeExtensions,
+          chromeLocalState = subEmtyValueMap chromeLocalState,
+          chromePrefs = subEmtyValueMap chromePrefs,
+          chromeDetach,
+          chromeDebuggerAddress = subEmptyTxt chromeDebuggerAddress,
+          chromeExcludeSwitches = subEmptyTxtLst chromeExcludeSwitches,
+          chromeMobileEmulation = subEmptyMobileEmulation chromeMobileEmulation,
+          chromeMinidumpPath = subEmptyList chromeMinidumpPath,
+          chromePerfLoggingPrefs = subEmptyPerfLoggingPrefs chromePerfLoggingPrefs,
+          chromeWindowTypes = subEmptyTxtLst chromeWindowTypes
+        }
+    EdgeOptions {..} ->
+      EdgeOptions
+        { edgeArgs = subEmptyTxtLst edgeArgs,
+          edgeBinary = subEmptyTxt edgeBinary,
+          edgeExtensions = subEmptyTxtLst edgeExtensions,
+          edgeLocalState = subEmtyValueMap edgeLocalState,
+          edgePrefs = subEmtyValueMap edgePrefs,
+          edgeDetach,
+          edgeDebuggerAddress = subEmptyTxt edgeDebuggerAddress,
+          edgeExcludeSwitches = subEmptyTxtLst edgeExcludeSwitches,
+          edgeMobileEmulation = subEmptyMobileEmulation edgeMobileEmulation,
+          edgeMinidumpPath = subEmptyList edgeMinidumpPath,
+          edgePerfLoggingPrefs = subEmptyPerfLoggingPrefs edgePerfLoggingPrefs,
+          edgeWindowTypes = subEmptyTxtLst edgeWindowTypes
+        }
+    FirefoxOptions {..} ->
+      FirefoxOptions
+        { firefoxArgs = subEmptyTxtLst firefoxArgs,
+          firefoxBinary = subEmptyTxt firefoxBinary,
+          firefoxProfile = subEmptyTxt firefoxProfile,
+          firefoxLog
+        }
+    s@SafariOptions {} -> s
+    where
+      subEmptyMobileEmulation :: Maybe MobileEmulation -> Maybe MobileEmulation
+      subEmptyMobileEmulation = subEmt emptyMobileEmulation
+
+      emptyMobileEmulation :: MobileEmulation -> Bool
+      emptyMobileEmulation = \case
+        MkMobileEmulation {deviceName, deviceMetrics, userAgent} ->
+          emptyTxt deviceName && isNothing deviceMetrics && emptyTxt userAgent
+
+      subEmptyPerfLoggingPrefs :: Maybe PerfLoggingPrefs -> Maybe PerfLoggingPrefs
+      subEmptyPerfLoggingPrefs = subEmt emptyPerfLoggingPrefs
+
+      emptyPerfLoggingPrefs :: PerfLoggingPrefs -> Bool
+      emptyPerfLoggingPrefs = \case
+        MkPerfLoggingPrefs {enableNetwork, enablePage, enableTimeline, traceCategories, bufferUsageReportingInterval} ->
+          emptyTxt traceCategories && isNothing bufferUsageReportingInterval && isNothing enableNetwork && isNothing enablePage && isNothing enableTimeline
+
+
+emptyTxt :: Maybe Text -> Bool
+emptyTxt = maybe True T.null
+
+emptyList :: Maybe [a] -> Bool
+emptyList = maybe True null
+
+emptyTextList :: [Text] -> Bool
+emptyTextList ml = null ml || (all T.null) ml
+
+
+isNothingProxy :: Proxy -> Bool
+isNothingProxy = \case
+  Direct -> False
+  Manual ftpProxy httpProxy sslProxy noProxy socksProxy-> 
+    all isNothing [ftpProxy, httpProxy, sslProxy] && isNothing noProxy && isNothing socksProxy
+  AutoDetect -> False
+  Pac url -> T.null url
+  System -> False
+
+subEmptyProxy :: Maybe Proxy -> Maybe Proxy
+subEmptyProxy p = subEmt isNothingProxy $ subEmptProps <$> p
+  where
+    subEmptProps :: Proxy -> Proxy
+    subEmptProps p' = case p' of
+      Manual {..} ->
+        Manual
+          { ftpProxy = subEmptyTxt ftpProxy,
+            httpProxy = subEmptyTxt httpProxy,
+            sslProxy = subEmptyTxt sslProxy,
+            noProxy = subEmptyTxtLst noProxy,
+            socksProxy
+          }
+      Direct -> p'
+      AutoDetect -> p'
+      Pac {} -> p'
+      System -> p'
+
+
+subEmptyTimeouts :: Maybe Timeouts -> Maybe Timeouts
+subEmptyTimeouts = subEmt isNothingTimeouts 
+  where
+    isNothingTimeouts :: Timeouts -> Bool
+    isNothingTimeouts = \case
+      MkTimeouts {..} ->
+        all isNothing [implicit, pageLoad, script]
+
+emptyFieldsToNothing :: Capabilities -> Capabilities
+emptyFieldsToNothing caps@MkCapabilities {..} = caps {
+  browserVersion = subEmptyTxt browserVersion,
+  vendorSpecific = subEmptyVendor vendorSpecific,
+  platformName,
+  acceptInsecureCerts,
+  pageLoadStrategy,
+  proxy = subEmptyProxy proxy,
+  timeouts = subEmptyTimeouts timeouts,
+  strictFileInteractability,
+  unhandledPromptBehavior
+ }
+
+subEmptyCapabilities :: Capabilities -> Capabilities
+subEmptyCapabilities caps@MkCapabilities {..} = caps {
+  browserVersion = subEmptyTxt browserVersion,
+  strictFileInteractability = strictFileInteractability,
+  acceptInsecureCerts = acceptInsecureCerts,
+  proxy = subEmptyProxy proxy,
+  timeouts = subEmptyTimeouts timeouts,
+  vendorSpecific = subEmptyVendor vendorSpecific
+ }
+
+
+jsonEq :: Capabilities -> Maybe Capabilities -> Bool
+jsonEq expected =
+  maybe
+    False
+    (\actual -> subEmptyCapabilities expected == subEmptyCapabilities actual)
+
+wantLogging :: Bool
+wantLogging = False
+
+log :: String -> Property ()
+log = if wantLogging then info else const $ pure ()
+
+test_round_trip :: TestTree
+test_round_trip = testPropertyWith options "Roundtrip Capabilities Parsing" $ do
+  c <- gen genCapabilities
+  let encoded = encode c
+      showEncode = jsonToText <$> decode @Value encoded
+      decoded = decode @Capabilities encoded
+      asExpected = jsonEq c
+
+  log ""
+  log "Initial Capabilities:"
+  log $ ppShow c
+  log ""
+  log "Encoded Capabilities:"
+  log $ maybe "Nothing" unpack showEncode
+  log ""
+  log "Decoded Capabilities:"
+  log $ maybe "Nothing" ppShow decoded
+  assert $ expect True `dot` fn ("matches encoded", asExpected) .$ ("decoded", decoded)
+ webdriver-precore.cabal view
@@ -0,0 +1,156 @@+cabal-version: 3.12++name:           webdriver-precore+version:        0.0.0.2+homepage:       https://github.com/pyrethrum/webdriver-precore#readme+bug-reports:    https://github.com/pyrethrum/webdriver-precore/issues+author:         John Walker, Adrian Glouftsis+maintainer:     theghostjw@gmail.com+category:       Webb, WebDriver, Testing+copyright:      2025 John Walker, Adrian Glouftsis+license:        BSD-3-Clause+license-file:   LICENSE+build-type:     Simple+extra-doc-files: +  README.md+  ChangeLog.md+tested-with: GHC == { 9.8.2, 9.10.1 }+    ++synopsis:       A typed wrapper for W3C WebDriver protocol. A base for other libraries.++description:+  This library provides typed definitions for the endpoints of the @<https://www.w3.org/TR/2025/WD-webdriver2-20250306/ W3C Webdriver>@.++  It is intended to be used as a base for other libraries that provide a WebDriver client implementation and higher level functions. +  +  A WebDriver client implementation can be built by pattern matching on the 'W3Spec' type returned by the functions in this library,+  sending the corresponding HTTP requests to a vendor provided WebDriver, then parsing the response using the parser provided as part +  of the 'W3Spec' type.++  See "WebDriverPreCore" for further details and [the project repo](https://github.com/pyrethrum/webdriver/blob/main/webdriver-examples/driver-demo-e2e/WebDriverE2EDemoTest.hs) for an examples.+  +  If you are looking for a fully implemented web client library, you should check out an alternative such as [haskell-webdriver](https://github.com/haskell-webdriver/haskell-webdriver#readme).+  ++common commonExtensions+  default-extensions:+    AllowAmbiguousTypes+      BangPatterns+      BlockArguments+      ConstrainedClassMethods+      ConstraintKinds+      DisambiguateRecordFields+      DuplicateRecordFields+      DataKinds+      DefaultSignatures+      DeriveFoldable+      DeriveFunctor+      DeriveGeneric+      DeriveLift+      DeriveTraversable+      DerivingStrategies+      EmptyCase+      ExistentialQuantification+      ExtendedDefaultRules+      FlexibleContexts+      FlexibleInstances+      FunctionalDependencies+      GADTs+      GeneralizedNewtypeDeriving+      ImportQualifiedPost+      InstanceSigs+      KindSignatures+      LambdaCase+      LiberalTypeSynonyms+      MultiParamTypeClasses+      MultiWayIf+      NamedFieldPuns+      NoFieldSelectors+      NoImplicitPrelude+      NumericUnderscores+      OverloadedStrings+      OverloadedRecordDot+      PartialTypeSignatures+      PatternSynonyms+      PolyKinds+      QuasiQuotes+      RankNTypes+      RecordWildCards+      ScopedTypeVariables+      StandaloneDeriving+      StrictData+      TemplateHaskell+      TupleSections+      TypeApplications+      TypeFamilies+      TypeFamilyDependencies+      TypeOperators++common commonGhcOptions+  ghc-options:+    -fmax-pmcheck-models=10000000 +    -ferror-spans +    -fprint-potential-instances +    -Wall +    -Wcompat +    -Wincomplete-record-updates +    -Wincomplete-uni-patterns +    -Wredundant-constraints+    -fwarn-tabs +    -fwrite-ide-info +    -fno-warn-type-defaults +    -fno-warn-unused-do-bind +    -Wno-overlapping-patterns +    -fprefer-byte-code +    -fbyte-code-and-object-code+    -haddock+    -- -fdefer-type-errors++library+  import: commonExtensions, commonGhcOptions+  exposed-modules:+      WebDriverPreCore+      WebDriverPreCore.Internal.Utils+  other-modules:+      WebDriverPreCore.SpecDefinition+      WebDriverPreCore.Capabilities+      WebDriverPreCore.Error+      WebDriverPreCore.HttpResponse+  hs-source-dirs: src+  build-depends:+    aeson         >=1.4   && <2.3, +    aeson-pretty  >=0.8   && <0.9,+    base          >=4.16  && <5,+    bytestring    >=0.10  && <0.12.3,+    text          >=2.1   && <2.2,+    containers    >=0.6   && <0.8,+    vector        >=0.12  && <0.14+  default-language: Haskell2010++test-suite test+  import: commonExtensions, commonGhcOptions+  type: exitcode-stdio-1.0+  main-is: Driver.hs+  hs-source-dirs: test+  build-depends:+    , aeson+    , base+    , containers+    , ghc+    , pretty-show+    , text+    , tasty+    , tasty-hunit+    , tasty-quickcheck+    , tasty-discover+    , falsify+    , webdriver-precore+    , raw-strings-qq+  other-modules:+    ApiCoverageTest,+    ErrorCoverageTest,+    JSONParsingTest,+  build-tool-depends:+    tasty-discover:tasty-discover+  default-language: Haskell2010