packages feed

webdriver-precore-0.2.0.0: test/Test.hs

{-# LANGUAGE CPP #-}

module Main where

import ApiCoverageTest qualified as API
import BiDi.DemoUtils (BiDiDemo (..), expectError, runDemo', FailTest (..), toText)
import BiDi.Demos.BrowserDemos qualified as Browser
import BiDi.Demos.BrowsingContextDemos qualified as BrowsingContext
import BiDi.Demos.BrowsingContextEventDemos qualified as BrowsingContextEvent
import BiDi.Demos.EmulationDemos qualified as Emulation
import BiDi.Demos.FallbackDemos qualified as Fallback
import BiDi.Demos.InputDemos qualified as Input
import BiDi.Demos.InputEventDemos qualified as InputEvent
import BiDi.Demos.LogEventDemos qualified as LogEvent
import BiDi.Demos.NetworkDemos qualified as Network
import BiDi.Demos.NetworkEventDemos qualified as NetworkEvent
import BiDi.Demos.OtherDemos qualified as Other
import BiDi.Demos.ScriptDemos qualified as Script
import BiDi.Demos.ScriptEventDemos qualified as ScriptEvent
import BiDi.Demos.SessionDemos qualified as Session
import BiDi.Demos.StorageDemos qualified as Storage
import BiDi.Demos.WebExtensionDemos qualified as WebExtension
import BiDi.ErrorDemo qualified as BiDiError
import Config (Config (..), DemoBrowser (..))
import ConfigLoader (loadConfig)
import Control.Exception (SomeException, catch)
import Data.Text (Text, unpack)
import ErrorCoverageTest qualified as Error
import HTTP.DemoUtils (HttpDemo (..), runDemoWithConfig)
import HTTP.ErrorDemo qualified as HttpError
import HTTP.HttpDemo qualified as Http
#ifndef LEGACY_TEST
import HTTP.FallbackDemo qualified as HttpFallback
#endif
import JSONParsingTest qualified as JSON
import Test.Tasty (TestTree, defaultMain, testGroup)
import Test.Tasty.HUnit (testCase)
import qualified Data.Text as T

main :: IO ()
main = do
  testCfg <- loadConfig
  -- defaultMain $ httpDemoSingleIsolated testCfg 
  -- defaultMain $ bidiSingleForDebug testCfg 
  defaultMain $ tests testCfg

tests :: Config -> TestTree
tests cfg =
  testGroup
    "Tests"
#ifdef LEGACY_TEST
    [ 
      httpDemos cfg
    ]
#else
    [ unitTests,
      httpDemos cfg,
      propertyTests,
      bidiDemos cfg
    ]
#endif

unitTests :: TestTree
unitTests =
  testGroup
    "Unit Tests"
    [ testGroup
        "API Coverage"
        [ testCase "All endpoints covered" API.unit_test_all_endpoints_covered
        ],
      testGroup
        "Error Coverage"
        [ testCase "All errors covered" Error.unit_test_all_errors_covered,
          testCase "Round trip error codes" Error.unit_round_trip_error_codes,
          testCase "All BiDi errors covered" Error.unit_test_all_errors_covered,
          testCase "Round trip BiDi error codes" Error.unit_round_trip_error_codes
        ],
      testGroup
        "JSON Parsing"
        [ testCase "WebSocket URL from JSON" JSON.unit_websocketUrlFromJSon
        ]
    ]

propertyTests :: TestTree
propertyTests =
  testGroup
    "Property Tests"
    [ testGroup
        "JSON Parsing"
        [ JSON.test_round_trip
        ]
    ]


-- Single isolated HTTP demo for CI debugging 
httpDemoSingleIsolated :: Config -> TestTree
httpDemoSingleIsolated cfg =
  testGroup
    "HTTP Demos"
    $ fromHttpDemo cfg
      <$> [ 
            Http.demoForwardBackRefresh
          ]

-- Single isolated Bidi demo for CI debugging 
bidiSingleForDebug :: Config -> TestTree
bidiSingleForDebug cfg =
  let 
    run = bidiTest cfg
    thisBrowser = cfg.browser
    expectFail bts txt = biDiError thisBrowser bts (Fragment txt)
   in testGroup
            "BiDi Single Demo"
            [ 
              run
                "Emulation"
                [ 
                  expectFail [Firefox']
                    "Expected \\\\\\\"coordinates\\\\\\\" to be an object"
                    Emulation.emulationSetGeolocationOverridePositionErrorDemo
                ]
        ]

httpDemos :: Config -> TestTree
httpDemos cfg =
  let thisBrowser = cfg.browser
      expectHttpFail = httpError thisBrowser
   in testGroup
        "HTTP Demos"
        $ fromHttpDemo cfg
          <$> [ Http.newSessionDemo,
                -- W3C spec requires status.ready=false when sessions exist. Chrome diverges from spec.
                expectHttpFail [Chrome'] "status.ready expected to be False"
                  Http.driverStatusDemo,
                Http.demoSendKeysClear,
            Http.demoForwardBackRefresh,
            -- this test is redundant but used in docs so run anyway
            Http.documentationDemo,
            Http.demoWindowHandles,
            Http.demoWindowSizes,
            Http.demoElementPageProps,
            Http.demoTimeouts,
            Http.demoWindowRecs,
            Http.demoWindowFindElement,
            Http.demoFrames,
            Http.demoShadowDom,
            Http.demoIsElementSelected,
            Http.demoGetPageSourceScreenShot,
            Http.demoPrintPage,
            Http.demoExecuteScript,
            Http.demoCookies,
            Http.demoCookiesWithDomain,
            Http.demoAlerts,
            Http.demoPointerNoneActions,
            Http.demoKeyAndReleaseActions,
            Http.demoWheelActions,
            Http.demoError,
            HttpError.errorDemo
-- fallback commands not implemented for legacy
#ifndef LEGACY_TEST
            , HttpFallback.demoFallbackActions
            , HttpFallback.demoFallbackCoercions
            , HttpFallback.demoExtendPost
#endif
          ]

httpTest :: Config -> Text -> [HttpDemo] -> TestTree
httpTest cfg title = testGroup (unpack title) . fmap (fromHttpDemo cfg)

fromHttpDemo :: Config -> HttpDemo -> TestTree
fromHttpDemo cfg demo = testCase (unpack demo.name) $ runDemoWithConfig cfg demo

-- testCase (unpack demo.name) $ runDemo' logNothingLogger MkTimeout {microseconds = 0} demo

bidiTest :: Config -> Text -> [BiDiDemo] -> TestTree
bidiTest cfg title =
  testGroup (unpack title) . fmap fromBidiDemo
  where
    fromBidiDemo demo = testCase (unpack demo.name) $ runDemo' cfg demo



bidiDemos :: Config -> TestTree
bidiDemos cfg =
  let run = bidiTest cfg
      thisBrowser = cfg.browser
      browserType = fromBrowser thisBrowser
      unknownCommand = unknownCommandError thisBrowser
      expectFail bts txt = biDiError thisBrowser bts (Fragment txt)
   in testGroup
        "BiDi Demos"
        [ testGroup
            "BiDi Commands"
            [ testGroup
                "BiDi Exception tests - threads rigged to explode"
                [ testCase "send exception" $ Other.sendFailDemo cfg,
                  testCase "get exception" $ Other.getFailDemo cfg,
                  testCase "event fail exception" $ Other.eventFailDemo cfg
                ],
              run
                "Browser"
                [ Browser.browserGetClientWindowsDemo,
                  Browser.browserCreateUserContextDemo,
                  Browser.browserGetUserContextsDemo,
                  unknownCommand [Firefox', Chrome']
                    Browser.browserSetClientWindowStateDemo,
                  Browser.browserRemoveUserContextDemo,
                  Browser.browserCompleteWorkflowDemo,
                  expectFail [Firefox']
                    "Closing the browser in a session started with WebDriver classic is not supported"
                    Browser.browserCloseDemo,
                  unknownCommand [Firefox']
                    -- since https://www.w3.org/TR/2025/WD-webdriver-bidi-20250918/#command-browser-seProtocolExceptiontDownloadBehavior
                    Browser.browserSetDownloadBehaviorDemo
                ],
              run
                "Browsing Context"
                [ BrowsingContext.browsingContextCreateActivateCloseDemo,
                  BrowsingContext.browsingContextCaptureScreenshotCloseDemo,
                  BrowsingContext.browsingContextClosePromptUnloadDemo,
                  BrowsingContext.browsingContextGetTreeDemo,
                  BrowsingContext.browsingContextHandleUserPromptDemo,
                  BrowsingContext.browsingNavigateReloadTraverseHistoryDemo,
                  BrowsingContext.browsingContextLocateNodesDemo,
                  BrowsingContext.browsingContextContextLocatorDemo,
                  BrowsingContext.browsingContextPrintDemo,
                  BrowsingContext.browsingContextSetViewportDemo,
                  BiDiError.errorDemo
                  -- TODO: WHEN NEW DRIVERS ADDED make conditional - hangs in firefox
                  -- , BrowsingContext.browsingContextSetViewportResetDemo
                ],
              run
                "Emulation"
                [ unknownCommand [Firefox', Chrome']
                    -- since https:\/\/www.w3.org\/TR\/2025\/WD-webdriver-bidi-20250729
                    Emulation.emulationSetForcedColorsModeThemeOverrideDemo,
                  Emulation.emulationSetGeolocationOverrideDemo,
                  -- Geckodriver bug: incorrectly requires 'coordinates' when 'error' is provided
                  -- Spec section 7.4.2.2 states that 'error' and 'coordinates' are mutually exclusive
                  expectFail [Firefox']
                    "Expected \\\\\\\"coordinates\\\\\\\" to be an object"
                    Emulation.emulationSetGeolocationOverridePositionErrorDemo,
                  Emulation.emulationSetLocaleOverrideDemo,
                  unknownCommand [Firefox']
                    -- since https://www.w3.org/TR/2025/WD-webdriver-bidi-20251007
                    Emulation.emulationSetNetworkConditionsDemo,
                  Emulation.emulationSetScreenOrientationOverrideDemo,
                  unknownCommand [Chrome']
                    -- since https://www.w3.org/TR/2025/WD-webdriver-bidi-20251120
                    Emulation.emulationSetScreenSettingsOverrideDemo,
                  unknownCommand [Firefox']
                    -- since https://www.w3.org/TR/2025/WD-webdriver-bidi-20250811
                    Emulation.emulationSetScriptingEnabledDemo,
                  Emulation.emulationSetTimezoneOverrideDemo,
                  unknownCommand [Firefox', Chrome']
                    -- since https://www.w3.org/TR/2026/WD-webdriver-bidi-20260109
                    Emulation.emulationSetTouchOverrideDemo,
                  Emulation.emulationSetUserAgentOverrideDemo,
                  Emulation.emulationCompleteWorkflowDemo
                ],
              run
                "Fallback"
                [ Fallback.fallbackExtendCommandDemo,
                  Fallback.fallbackOffSpecCommandDemo,
                  Fallback.fallbackCommandCoercionsDemo,
                  Fallback.fallbackSubscribeUnknownEventDemo,
                  Fallback.fallbackSubscribeUnknownEventFilteredDemo
                ],
              run
                "Input"
                [ Input.inputKeyboardDemo,
                  Input.inputPointerDemo,
                  Input.inputWheelDemo,
                  Input.inputCombinedActionsDemo,
                  Input.inputReleaseActionsDemo,
                  Input.inputSetFilesDemo
                ],
              run
                "Network"
                [ 
                  Network.networkDataCollectorDemo,
                  Network.networkInterceptDemo,
                  Network.networkRequestModificationDemo,
                  Network.networkResponseModificationDemo,
                  Network.networkAuthCancelDemo,
                  Network.networkAuthWithCredentialsDemo,
                  Network.networkFailRequestDemo,
                  Network.networkProvideResponseJSONDemo,
                  Network.networkProvideResponseHTMLDemo,
                  Network.networkProvideResponseWithCookiesDemo,
                  Network.networkProvideResponseBase64Demo,
                  Network.networkProvideResponseErrorDemo,
                  Network.networkDataRetrievalDemo,
                  Network.networkDisownDataDemo,
                  Network.networkCacheBehaviorDemo,
                  -- since https://www.w3.org/TR/2025/WD-webdriver-bidi-20251106
                  -- Chromedriver does not support setting non-string header values
                  expectFail [Chrome']
                    "Only string headers values are supported"
                    Network.networkSetExtraHeadersDemo
                ],
              run
                "Script"
                [ Script.scriptEvaluateAllPrimitiveTypesDemo,
                  Script.scriptEvaluateAdvancedDemo,
                  Script.serializationOptionsDemo,
                  Script.scriptPreloadScriptDemo,
                  Script.scriptPreloadScriptMultiContextDemo,
                  Script.scriptChannelArgumentDemo,
                  Script.scriptUserContextsDemo,
                  Script.scriptCallFunctionDemo,
                  Script.scriptGetRealmsAndDisownDemo
                ],
              run
                "Session"
                [ Session.sessionStatusDemo,
                  expectFail [Firefox', Chrome']
                    (case thisBrowser of 
                      Firefox{} -> "Maximum number of active sessions"
                      Chrome{} -> "session already exists"
                      )
                    Session.sessionNewDemo,

                  Session.sessionSubscribeDemo,
                  Session.sessionUnsubscribeDemo,
                  expectFail [Firefox', Chrome']
                    (case thisBrowser of 
                      Firefox{} -> "Maximum number of active sessions"
                      Chrome{} -> "session already exists"
                      )
                    Session.sessionCapabilityNegotiationDemo,
                  Session.sessionCompleteLifecycleDemo
                ],
                run
                "Session - firefox only" (
                  if browserType == Firefox' then 
                  [
                  -- todo: - calling `session.end` on the test BiDi runner throws `ConnectionClosed` when 
                  -- the server closes the WebSocket after session termination - needs orchestration 
                  -- fix in bidi runner when sesssion is closed
                  expectFail [Firefox']
                    "Ending a session started with WebDriver classic is not supported"
                    Session.sessionEndDemo
                ] else []),
              run
                "Storage"
                [ Storage.storageGetCookiesDemo,
                  -- ChromeDriver does not support storageKey partition type in storage.setCookie
                  expectFail [Chrome']
                    "unable to set cookie"
                  Storage.storageSetCookieDemo,
                  Storage.storageDeleteCookiesDemo,
                  Storage.storagePartitionKeyDemo,
                  Storage.storageCompleteWorkflowDemo
                ],
              run
                "WebExtension"
                [ -- ChromeDriver doesn't support BiDi WebExtension methods
                  expectFail [Chrome']
                    "Method not available"
                    WebExtension.webExtensionInstallPathDemo,
                  expectFail [Chrome']
                    "Archived and Base64 extensions are not supported"
                    WebExtension.webExtensionInstallArchiveDemo,
                  expectFail [Chrome']
                    "Archived and Base64 extensions are not supported"
                    WebExtension.webExtensionInstallBase64Demo,
                  WebExtension.webExtensionValidationDemo
                ]
            ],
          testGroup
            "BiDi Events"
            [ run
                "Browsing Context Events"
                [ BrowsingContextEvent.browsingContextEventDemo,
                  BrowsingContextEvent.browsingContextEventDemoMulti,
                  BrowsingContextEvent.browsingContextEventDemoFilteredSubscriptions,
                  BrowsingContextEvent.browsingContextEventDemoUserContextFiltered,
                  BrowsingContextEvent.browsingContextEventCreateDestroy,
                  BrowsingContextEvent.browsingContextEventNavigationLifecycle,
                  BrowsingContextEvent.browsingContextEventFragmentNavigation,
                  BrowsingContextEvent.browsingContextEventUserPrompts,
                  BrowsingContextEvent.browsingContextEventUserPromptsVariants,
                  expectFail [Firefox', Chrome']
                    (case thisBrowser of 
                      Firefox{} -> "Expected event did not fire: BrowsingContextHistoryUpdated"
                      Chrome{} -> "Timeout"
                      )
                    BrowsingContextEvent.browsingContextEventHistoryUpdated,
                  -- not supporrted in geckodriver yet
                  expectFail [Firefox', Chrome']
                    (case thisBrowser of 
                      Firefox{} -> "browsingContext.navigationAborted is not a valid event name"
                      Chrome{} -> "Expected event did not fire: BrowsingContextNavigationAborted"
                      )
                      BrowsingContextEvent.browsingContextEventNavigationAborted,
                  expectFail [Firefox', Chrome']
                    (case thisBrowser of 
                      Firefox{} -> "NS_ERROR_UNKNOWN_HOST"
                      Chrome{} -> "ERR_NAME_NOT_RESOLVED"
                      )
                    BrowsingContextEvent.browsingContextEventNavigationFailed,
                  BrowsingContextEvent.browsingContextEventDownloadWillBegin,
                  BrowsingContextEvent.browsingContextEventDownloadEnd
                ],
              run
                "Input Events"
                [],
              run
                "Input Events - File Dialog Opened" (
                   case thisBrowser of 
                     Chrome {} -> [InputEvent.inputEventFileDialogOpened]
                     Firefox {headless = False} -> [InputEvent.inputEventFileDialogOpened]
                     -- firfox throws error on file dialog open in headless mode
                     -- ConnectionClosed is not coming from main thread so not being caught TODO: reinstate this when runner fixed
                     Firefox {headless = True} ->  [] -- [expectFail [Firefox'] "ConnectionClosed" InputEvent.inputEventFileDialogOpened] 
                ),
              run
                "Log Events"
                [ LogEvent.logEventConsoleEntries,
                  LogEvent.logEventConsoleLevelDebug,
                  LogEvent.logEventConsoleLevelInfo,
                  LogEvent.logEventConsoleLevelWarn,
                  LogEvent.logEventConsoleLevelError,
                  LogEvent.logEventJavascriptErrorFromButton
                ],
              run
                "Network Events"
                [ expectFail [Chrome']
                    "Timeout - Expected event did not fire: NetworkResponseStarted"
                    NetworkEvent.networkEventRequestResponseLifecycle,
                  NetworkEvent.networkEventFetchError,
                  expectFail [Chrome']
                    "Timeout - Expected event did not fire: NetworkAuthRequired"
                    NetworkEvent.networkEventAuthRequired
                ],
              run
                "Script Events"
                [ ScriptEvent.scriptEventRealmLifecycle,
                  ScriptEvent.scriptEventMessage,
                  ScriptEvent.scriptEventMessageRuntime
                ]
            ]
        ]

expectFailure :: DemoBrowser -> [BrowserType] -> Bool
expectFailure actualBrowser failBrowsers = 
  fromBrowser actualBrowser `elem` failBrowsers

biDiError :: DemoBrowser -> [BrowserType] -> FailTest -> BiDiDemo -> BiDiDemo
biDiError actualBrowser failBrowsers failTest demo@MkBiDiDemo {name, action} =
  if expectFailure actualBrowser failBrowsers then
  MkBiDiDemo
    { name = name <> " - EXPECTED ERROR: " <> toText failTest,
      action = \utils bidi -> expectError name failTest (action utils bidi)
    }
  else demo

data BrowserType = Firefox' | Chrome' deriving (Eq, Show)

fromBrowser :: DemoBrowser -> BrowserType
fromBrowser = \case 
  Firefox {} -> Firefox'
  Chrome {} -> Chrome'

unknownCommandError :: DemoBrowser -> [BrowserType] -> BiDiDemo -> BiDiDemo
unknownCommandError actualBrowser failBrowsers  demo = 
  biDiError actualBrowser failBrowsers failTest demo
  where
    failTest = Predicate \txt -> "not implemented" `T.isInfixOf` txt || "unknown command" `T.isInfixOf` txt
    
httpError :: DemoBrowser -> [BrowserType] -> Text -> HttpDemo -> HttpDemo
httpError actualBrowser failBrowsers errorFragment demo =
  if expectFailure actualBrowser failBrowsers then
    case demo of
      Demo {name, action} ->
        Demo
          { name = name <> " - EXPECTED ERROR: " <> errorFragment,
            action = \demoActions httpActions ->
              catch
                ( do
                    action demoActions httpActions
                    error $ "Expected test to fail with error containing: " <> unpack errorFragment
                )
                (\(_ :: SomeException) -> pure ())
          }
      SessionDemo {name, sessionAction} ->
        SessionDemo
          { name = name <> " - EXPECTED ERROR: " <> errorFragment,
            sessionAction = \session demoActions httpActions ->
              catch
                ( do
                    sessionAction session demoActions httpActions
                    error $ "Expected test to fail with error containing: " <> unpack errorFragment
                )
                (\(_ :: SomeException) -> pure ())
          }
  else demo