packages feed

webdriver-precore-0.2.0.0: src/WebDriverPreCore/HTTP/Capabilities.hs

{-# OPTIONS_HADDOCK hide #-}

module WebDriverPreCore.HTTP.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 (asum)
import Data.Aeson
  ( FromJSON (parseJSON),
    Key,
    KeyValue ((.=)),
    Object,
    ToJSON (toJSON),
    Value,
    object,
    withObject,
    withText,
    (.:),
    (.:?),
  )
import Data.Aeson.Key (fromText)
import Data.Aeson.Types
  ( Pair,
    Parser,
    Value (..),
    parseField,
    parseFieldMaybe,
  )
import Data.Map.Strict (Map)
import Data.Maybe (catMaybes)
import Data.Text (Text)
import Data.Vector (fromList)
import GHC.Generics (Generic)
import AesonUtils (opt, parseOpt, toJSONOmitNothing, parseJSONOmitNothing)

{- references:
- https://https://www.w3.org/TR/2025/WD-webdriver2-20251028/#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-20251028/#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-20251028/#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-20251028/#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-20251028/#capabilities)
minCapabilities :: BrowserName -> Capabilities
minCapabilities browserName =
  MkCapabilities
    { browserName = Just browserName,
      browserVersion = Nothing,
      platformName = Nothing,
      acceptInsecureCerts = Nothing,
      pageLoadStrategy = Nothing,
      proxy = Nothing,
      setWindowRect = Nothing,
      timeouts = Nothing,
      strictFileInteractability = Nothing,
      unhandledPromptBehavior = Nothing,
      webSocketUrl = 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-20251028/#capabilities)
data UnhandledPromptBehavior
  = Dismiss
  | Accept
  | DismissAndNotify
  | AcceptAndNotify
  | Ignore
  deriving (Show, Generic, Enum, Bounded, Eq)

-- | [spec](https://www.w3.org/TR/2025/WD-webdriver2-20251028/#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-20251028/#capabilities)
data BrowserName
  = Chrome
  | Firefox
  | Safari
  | Edge
  | InternetExplorer
  deriving (Show, Generic, Enum, Bounded, Eq)

-- | [spec](https://www.w3.org/TR/2025/WD-webdriver2-20251028/#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-20251028/#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,
    setWindowRect :: Maybe Bool,
    timeouts :: Maybe Timeouts,
    strictFileInteractability :: Maybe Bool,
    unhandledPromptBehavior :: Maybe UnhandledPromptBehavior,
    webSocketUrl :: Maybe Bool, 
    vendorSpecific :: Maybe VendorSpecific
  }
  deriving (Show, Generic, Eq)

instance ToJSON Capabilities where
  toJSON :: Capabilities -> Value
  toJSON
    MkCapabilities
      { browserName,
        browserVersion,
        platformName,
        acceptInsecureCerts,
        pageLoadStrategy,
        setWindowRect,
        proxy,
        timeouts,
        strictFileInteractability,
        unhandledPromptBehavior,
        webSocketUrl,
        vendorSpecific
      } =
      object $
        [ "browserName" .= browserName
        ]
          <> catMaybes
            [ opt "browserVersion" browserVersion,
              opt "platformName" platformName,
              opt "acceptInsecureCerts" acceptInsecureCerts,
              opt "pageLoadStrategy" pageLoadStrategy,
              opt "setWindowRect" setWindowRect,
              opt "proxy" proxy,
              opt "timeouts" timeouts,
              opt "strictFileInteractability" strictFileInteractability,
              opt "unhandledPromptBehavior" unhandledPromptBehavior,
              opt "webSocketUrl" webSocketUrl
            ]
          <> vendorSpecificToJSON vendorSpecific

instance FromJSON Capabilities where
  parseJSON :: Value -> Parser Capabilities
  parseJSON = withObject "Capabilities" $ \v ->
    do
      browserName <- v .: "browserName"
      browserVersion <- v .:? "browserVersion"
      platformName <- v .:? "platformName"
      acceptInsecureCerts <- v .:? "acceptInsecureCerts"
      pageLoadStrategy <- v .:? "pageLoadStrategy"
      proxy <- v `parseOpt` "proxy"
      setWindowRect <- v .:? "setWindowRect"
      timeouts <- v .:? "timeouts"
      strictFileInteractability <- v .:? "strictFileInteractability"
      unhandledPromptBehavior <- v .:? "unhandledPromptBehavior"
      webSocketUrl <- v .:? "webSocketUrl"
      vendorSpecific <- parseVendorSpecific v
      pure MkCapabilities {..}

-- | [spec](https://www.w3.org/TR/2025/WD-webdriver2-20251028/#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-20251028/#proxy)
data Proxy
  = Direct
  | Manual
      { 
        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 "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
            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-20251028/#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 :: PerfLoggingPrefs -> Value
  toJSON = toJSONOmitNothing

instance FromJSON PerfLoggingPrefs where
  parseJSON :: Value -> Parser PerfLoggingPrefs
  parseJSON = parseJSONOmitNothing

data MobileEmulation = MkMobileEmulation
  { deviceName :: Maybe Text,
    deviceMetrics :: Maybe DeviceMetrics,
    userAgent :: Maybe Text
  }
  deriving (Show, Eq, Generic)

instance FromJSON MobileEmulation where
  parseJSON :: Value -> Parser MobileEmulation
  parseJSON = parseJSONOmitNothing

instance ToJSON MobileEmulation where
  toJSON :: MobileEmulation -> Value
  toJSON = toJSONOmitNothing

-- | 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 :: LogSettings -> Value
  toJSON = toJSONOmitNothing

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 :: DeviceMetrics -> Value
  toJSON = toJSONOmitNothing

-- | 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-20251028/#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
      ]