definition module iTasks.Testing.Selenium.Interface

import StdGeneric

from Testing.TestEvents import :: EndEventType

import ABC.Interpreter.JavaScript.Monad

from iTasks.Testing.Selenium import :: ClientTestOptions, :: Browser

:: ElementLocator
	= ByClassName !String    //* HTML class
	| ByCSS !String          //* CSS selector
	| ByXPath !String        //* XPath
	| ById !String           //* HTML id attribute

	| ByTestName !String     //* Test name, set with `TestProperty`
	| ByLabel !String        //* The editor corresponding to an explicit label
	| ByDefaultLabel !String //* Like `ByLabel`, but with a default label name (e.g. based on a record field name)

	| ButtonByLabel !String  //* Find a button by its label

:: MaybeElement
	= NoElement
	| Element !JSVal

:: Keys
	= TextKeys !String

:: Key
	= ArrowDownKey
	| ArrowLeftKey
	| ArrowRightKey
	| ArrowUpKey
	| TabKey

:: Button
	= LeftButton
	| MiddleButton
	| RightButton

:: TestState =
	{ testNamePrefix :: !String
	, clientOptions  :: !ClientTestOptions
	}

newTestState :: !ClientTestOptions -> TestState

/**
 * Run a Selenium test.
 * @param The name of the test.
 * @param The promise monad to run. This monad is executed in WebAssembly. If
 *   {{`fail`}} is called within, the test fails with the given fail reason. If
 *   any promise is rejected during execution of the monad, the test fails as
 *   well. If the monad is successfully executed the test passes.
 */
seleniumTest :: !String !(JS TestState JSPromise) -> JS TestState JSPromise

//* Prefix the names of all tests started with `startTest` with some string.
prefixTest :: !String !(JS TestState a) -> JS TestState a

/**
 * Finish the currently running test with a custom fail reason.
 *
 * This function must be used within the second argument of {{`seleniumTest`}}.
 */
fail :: !String -> JS TestState JSPromise

/**
 * Check for a condition. If the condition is false, the currently running test
 * is finished with the given reason. Otherwise the result is a resolving
 * promise.
 *
 * This function must be used within the second argument of {{`seleniumTest`}}.
 *
 * @param A description of the stage of the test (i.e., what exactly is being
 *   checked).
 * @param The condition.
 */
expect :: !String !Bool -> JS TestState JSPromise

/**
 * Wrap a test specification with functionality to start and stop the web
 * driver.
 * @param The browser to test with.
 * @param The test options.
 * @param The name of the program under test.
 * @param The URL being tested.
 * @param The tests.
 * @result A promise resolving when tests have finished.
 */
withWebDriver :: !Browser !ClientTestOptions !String !String !(JS TestState JSVal) -> JS TestState JSVal

//* Retrieve a URL using the currently active webdriver.
getURL :: !String -> JS TestState JSVal

/**
 * Wait for a task to become idle.
 * @param Any (child) element of the task.
 * @param A continuation.
 */
waitForIdle :: !JSVal !(JS TestState JSVal) -> JS TestState JSVal

/**
 * Do something and wait for (1) a UI event to be sent to the server and (2)
 * all websocket messages to be responded to.
 * @param The thing to do (may return a promise).
 * @result A promise resolving when the condition is satisfied.
 */
doAndWaitForUIEvent :: !(JS TestState JSVal) -> JS TestState JSVal

/**
 * Wait for an element to be enabled before continuing. If the element did not
 * get enabled within the timeout specified by the `ClientTestOptions`, the
 * currently running test fails with the given reason.
 *
 * This function must be used within the second argument of {{`seleniumTest`}}.
 *
 * @param A description of the stage of the test (i.e., what exactly is being checked).
 * @param The element.
 */
expectElementToEnable :: !String !JSVal -> JS TestState JSVal

/**
 * Wait for an element to be disabled before continuing. If the element did not
 * get disabled within the timeout specified by the `ClientTestOptions`, the
 * currently running test fails with the given reason.
 *
 * This function must be used within the second argument of {{`seleniumTest`}}.
 *
 * @param A description of the stage of the test (i.e., what exactly is being checked).
 * @param The element.
 */
expectElementToDisable :: !String !JSVal -> JS TestState JSVal

/**
 * Wait for an element to become visible before continuing. If the element did
 * not get enabled within the timeout specified by the `ClientTestOptions`, the
 * currently running test fails with the given reason.
 *
 * This function must be used within the second argument of {{`seleniumTest`}}.
 *
 * @param A description of the stage of the test (i.e., what exactly is being checked).
 * @param The element.
 */
expectElementToBecomeVisible :: !String !JSVal -> JS TestState JSVal

/**
 * Like `doAndWaitForUIEvent`, but also waits for at least one UI change to be
 * sent from the server. This can be used when you are sure that a UI change
 * will occur.
 * @param The thing to do (may return a promise).
 * @result A promise resolving when the condition is satisfied.
 */
doAndWaitForUIChange :: !(JS TestState JSVal) -> JS TestState JSVal

//* Sleep a number of milliseconds.
sleep :: !Int -> JS TestState JSVal

/**
 * Take a screenshot (to be stored by the iTasks server).
 * Be aware that in headless mode, form controls will look different in
 * screenshots due to https://bugzilla.mozilla.org/show_bug.cgi?id=1385711.
 * They are styled with selenium-headless.css to make them somewhat visible.
 *
 * @param The filename.
 * @param Optionally, the element to take a screenshot of.
 * @result A promise, resolving when the screenshot has been taken.
 */
takeScreenshot :: !String !MaybeElement -> JS TestState JSVal

/**
 * @param A locator.
 * @param Optionally, the root element to search from.
 * @result A promise for an element or `null` if it could not be found.
 */
findElement :: !ElementLocator !MaybeElement -> JS TestState JSVal

/**
 * @param A locator.
 * @param Optionally, the root element to search from.
 * @result A promise for an array of elements.
 */
findElements :: !ElementLocator !MaybeElement -> JS TestState JSVal

/**
 * Returns a promise for the focused element. When no element is focused, this
 * is the body (as defined by JavaScript `document.activeElement`).
 */
getFocusedElement :: JS TestState JSVal

//* Checks if two web elements are equal. Returns a promise.
elementsAreEqual :: !JSVal !JSVal -> JS TestState JSVal

/**
 * Clear an element (should be `input` or `textarea`).
 *
 * TODO: Because this unsets the `value` attribute, no `input` event is
 * dispatched (only a `change` event), so the change is not synced with the
 * iTasks server. To sync the change to the iTasks server one could use
 * `sendKeys` to send Ctrl-A Backspace to the element, but this does not work
 * on `select`, checkboxes and radio input fields.
 *
 * @result A promise, resolving when the element has been cleared.
 */
clear :: !JSVal -> JS TestState JSVal

//* Click on the element. Returns a promise.
click :: !JSVal -> JS TestState JSVal

/**
 * Press a mouse button on some element. It can be released with `release`.
 * @result A promise.
 */
press :: !Button !JSVal -> JS TestState JSVal

/**
 * Releases a mouse button after it has been `press`ed.
 * @result A promise.
 */
release :: !Button -> JS TestState JSVal

/**
 * Presses and releases a key. Selenium does not support pressing a key down
 * and releasing it later.
 * @result A promise.
 */
pressKey :: !Key -> JS TestState JSVal

/**
 * @param The attribute.
 * @param The element to get the attribute of.
 * @param The continuation.
 * @result A promise resolving when the continuation has finished.
 */
getAttribute :: !String !JSVal !(String -> JS TestState JSVal) -> JS TestState JSVal

/**
 * @param The element to find the classes of.
 * @param The continuation.
 * @result A promise resolving when the continuation has finished.
 */
getClasses :: !JSVal !([String] -> JS TestState JSVal) -> JS TestState JSVal

/**
 * @param The CSS property name.
 * @param The element to get the CSS property value of.
 * @param The continuation.
 * @result A promise resolving when the continuation has finished.
 */
getCssValue :: !String !JSVal !(String -> JS TestState JSVal) -> JS TestState JSVal

/**
 * @param The element to get the tag name of.
 * @param The continuation.
 * @result A promise resolving when the continuation has finished.
 */
getTagName :: !JSVal !(String -> JS TestState JSVal) -> JS TestState JSVal

/**
 * @param The element to get the visible (not hidden by CSS) `innerText` of.
 * @param The continuation.
 * @result A promise resolving when the continuation has finished.
 */
getText :: !JSVal !(String -> JS TestState JSVal) -> JS TestState JSVal

/**
 * @param The element to get the `value` attribute of.
 * @param A function to transform the JavaScript value to a Clean value; e.g.
 *   `fromJS ""`.
 * @param The continuation.
 * @result A promise resolving when the continuation has finished.
 */
getValue :: !JSVal !(JSVal -> a) !(a -> JS TestState JSVal) -> JS TestState JSVal

/**
 * Assert that the value of a given CSS property of a given element matches the expected value.
 * The test case fails if the values do not match.
 *
 * @param The CSS property to compare.
 * @param The expected value for this CSS property
 * @param The element
 * @result A promise that resolves when the expected value matches the actual value and that fails otherwise.
 */
expectCssValue :: !String !String !JSVal -> JS TestState JSPromise

/**
 * @param A default value.
 * @param The element to check.
 * @param The continuation.
 * @result A promise resolving when the continuation has finished.
 */
isDisplayed :: !Bool !JSVal !(Bool -> JS TestState JSVal) -> JS TestState JSVal

/**
 * @param A default value.
 * @param The element to check.
 * @param The continuation.
 * @result A promise resolving when the continuation has finished.
 */
isEnabled :: !Bool !JSVal !(Bool -> JS TestState JSVal) -> JS TestState JSVal

/**
 * @param A default value.
 * @param The element to check.
 * @param The continuation.
 * @result A promise resolving when the continuation has finished.
 */
isSelected :: !Bool !JSVal !(Bool -> JS TestState JSVal) -> JS TestState JSVal

//* Focus an element. Returns a promise.
focusElement :: !JSVal -> JS TestState JSVal

//* Focus an element and emulate key presses. Returns a promise.
sendKeys :: !Keys !JSVal -> JS TestState JSVal

//* Submit the form containing this element. Returns a promise.
submit :: !JSVal -> JS TestState JSVal

/**
 * Input a value into a generic editor.
 *
 * @param A function to transform field names to label names.
 * @param The value to enter.
 * @param Optionally, a root element.
 * @result A promise to resolve when the value has been entered, at least one
 *   UI event has been sent, and all outgoing websocket messages have been
 *   answered.
 */
input :: !(String -> String) !a !MaybeElement -> JS TestState JSVal | gInput{|*|} a

/**
 * Check that an editor holds a certain value. If this is not the case the
 * currently running test fails with the given reason.
 *
 * This function must be used within the second argument of {{`seleniumTest`}}.
 *
 * @param A function to transform field names to label names.
 * @param The expected value.
 * @param Optionally, a root element.
 * @result A promise. The test given in the first parameter has been resolved,
 *   either with failure and a descriptive message, or with success.
 */
check :: !(String -> String) !a !MaybeElement -> JS TestState JSVal | gInput{|*|} a

:: GInputMode
	= GIM_Enter
	| GIM_Check

//* Internal function for `input` and `check`. Use these functions instead.
generic gInput a :: !(String -> String) !GInputMode !a !MaybeElement !(JS TestState JSVal) ![JSVal] ![String] !([JSVal] -> JS TestState JSVal) -> JS TestState JSVal
derive gInput String, Int, Real, Char, Bool
derive gInput UNIT, EITHER, PAIR, RECORD, FIELD of {gfd_name}, OBJECT, CONS of {gcd_name,gcd_arity,gcd_index}

/**
 * Execute a JavaScript snippet in the context of the currently selected frame.
 * The snippet is executed as the body of an anonymous function. It can use the
 * arguments by making reference to the `arguments` keyword.
 *
 * @param The script.
 * @param The arguments.
 * @result A promise that resolves to the script's return value.
 */
executeScript :: !String !a -> JS TestState JSVal | toJSArgs a

/**
 * Select a concrete tab in a parallel task with tabs.
 *
 * @param The name of the tab to select.
 * @param The element of the parallel task.
 * @result A promise that resolves when the tab has been selected and the
 *   subtask div has become visible.
 */
selectTab :: !String !JSVal -> JS TestState JSVal
