All Downloads are FREE. Search and download functionalities are using the official Maven repository.

gwen.web.WebEngine.scala Maven / Gradle / Ivy

There is a newer version: 4.1.1
Show newest version
/*
 * Copyright 2014-2017 Branko Juric, Brady Wood
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package gwen.web

import scala.concurrent.duration.Duration
import org.openqa.selenium.Keys
import gwen.Predefs.Formatting.DurationFormatter
import gwen.Predefs.Kestrel
import gwen.Predefs.RegexContext
import gwen.{GwenSettings, Settings}
import gwen.dsl._
import gwen.errors.undefinedStepError
import gwen.eval.EvalEngine
import gwen.eval.GwenOptions
import gwen.eval.ScopedDataStack
import gwen.eval.support.DecodingSupport
import gwen.eval.support.DefaultEngineSupport

/**
  * A web engine that uses the Selenium web driver
  * API to automate various web operations.
  * 
  * @author Branko Juric, Brady Wood
  */
trait WebEngine extends EvalEngine[WebEnvContext] 
  with DefaultEngineSupport[WebEnvContext] 
  with DecodingSupport {
  
  /**
    * Initialises and returns a new web environment context.
    * 
    * @param options command line options
    * @param scopes initial data scopes
    */
  override def init(options: GwenOptions, scopes: ScopedDataStack) = new WebEnvContext(options, scopes)
  
  /**
    * Evaluates a given step.  This method matches the incoming step against a 
    * set of supported steps and evaluates only those that are successfully 
    * matched.
    *
    * @param step the step to evaluate
    * @param env the web environment context
    */
  override def evaluate(step: Step, env: WebEnvContext): Unit = {
    
   step.expression match {
    
      case r"""I wait for (.+?)$element text for (.+?)$seconds second(?:s?)""" =>
        val elementBinding = env.getLocatorBinding(element)
        env.execute {
          env.waitUntil(s"Waiting for $element text after $seconds second(s)", seconds.toInt) {
            env.waitForText(elementBinding)
          }
        } getOrElse {
          env.scopes.set(s"$element/text", "text")
        }

      case r"""I wait for (.+?)$element text""" =>
        val elementBinding = env.getLocatorBinding(element)
        env.execute {
          env.waitUntil(s"Waiting for $element text") {
            env.waitForText(elementBinding)
          }
        } getOrElse {
          env.scopes.set(s"$element/text", "text")
        }

      case r"""I wait for (.+?)$element for (.+?)$seconds second(?:s?)""" =>
        val elementBinding = env.getLocatorBinding(element)
        env.execute {
          env.waitUntil(s"Waiting for $element after $seconds second(s)", seconds.toInt) {
            env.withWebElement(elementBinding) { _=> true }
          }
        }

      case r"""I wait for (.+?)$$$element""" =>
        val elementBinding = env.getLocatorBinding(element)
        env.execute {
          env.waitUntil(s"Waiting for $element") {
            env.withWebElement(elementBinding) { _=> true }
          }
        }

      case r"""I wait ([0-9]+?)$duration second(?:s?) when (.+?)$element is (clicked|submitted|checked|ticked|unchecked|unticked|selected|typed|entered|tabbed|cleared)$$$event""" =>
        env.getLocatorBinding(element)
        env.scopes.set(s"$element/${WebEvents.EventToAction(event)}/wait", duration)
        
      case r"""I wait until (.+?)$condition when (.+?)$element is (clicked|submitted|checked|ticked|unchecked|unticked|selected|typed|entered|tabbed|cleared)$$$event""" =>
        env.scopes.get(s"$condition/javascript")
        env.getLocatorBinding(element)
        env.scopes.set(s"$element/${WebEvents.EventToAction(event)}/condition", condition)
        
      case r"""I wait until "(.+?)$javascript"""" => env.execute {
        env.waitUntil(s"Waiting until $javascript") {
          env.executeScriptPredicate(javascript)
        }
      }
        
      case r"""I wait until (.+?)$$$condition""" =>
        val javascript = env.scopes.get(s"$condition/javascript")
        env.execute {
          env.waitUntil(s"Waiting until $condition") {
            env.executeScriptPredicate(javascript)
          }
        }

      case r"""(.+?)$doStep for each (.+?)$element located by (id|name|tag name|css selector|xpath|class name|link text|partial link text|javascript)$locator "(.+?)"$expression in (.+?)$$$container""" =>
        env.getLocatorBinding(container)
        val binding = LocatorBinding(s"${element}/list", locator, expression, Some(container))
        foreach(binding, element, step, doStep, env)

      case r"""(.+?)$doStep for each (.+?)$element located by (id|name|tag name|css selector|xpath|class name|link text|partial link text|javascript)$locator "(.+?)"$$$expression""" =>
        val binding = LocatorBinding(s"${element}/list", locator, expression, None)
        foreach(binding, element, step, doStep, env)

      case r"""(.+?)$doStep for each (.+?)$element in (.+?)$$$iteration""" =>
        val binding = env.getLocatorBinding(iteration)
        foreach(binding, element, step, doStep, env)

      case r"""(.+?)$doStep (until|while)$operation (.+?)$condition using no delay and (.+?)$timeoutPeriod (minute|second|millisecond)$timeoutUnit timeout""" =>
        repeat(operation, step, doStep, condition, Duration.Zero, Duration(timeoutPeriod.toLong, timeoutUnit), env)
        
      case r"""(.+?)$doStep (until|while)$operation (.+?)$condition using no delay""" =>
        repeat(operation, step, doStep, condition, Duration.Zero, defaultRepeatTimeout(DefaultRepeatDelay), env)
        
      case r"""(.+?)$doStep (until|while)$operation (.+?)$condition using (.+?)$delayPeriod (second|millisecond)$delayUnit delay and (.+?)$timeoutPeriod (minute|second|millisecond)$timeoutUnit timeout""" =>
        repeat(operation, step, doStep, condition, Duration(delayPeriod.toLong, delayUnit), Duration(timeoutPeriod.toLong, timeoutUnit), env)
        
      case r"""(.+?)$doStep (until|while)$operation (.+?)$condition using (.+?)$delayPeriod (second|millisecond)$delayUnit delay""" =>
        val delayDuration = Duration(delayPeriod.toLong, delayUnit)
        repeat(operation, step, doStep, condition, delayDuration, defaultRepeatTimeout(delayDuration), env)

      case r"""(.+?)$doStep (until|while)$operation (.+?)$condition using (.+?)$timeoutPeriod (minute|second|millisecond)$timeoutUnit timeout""" =>
        repeat(operation, step, doStep, condition, DefaultRepeatDelay, Duration(timeoutPeriod.toLong, timeoutUnit), env)
        
      case r"""(.+?)$doStep (until|while)$operation (.+?)$$$condition""" =>
        repeat(operation, step, doStep, condition, DefaultRepeatDelay, defaultRepeatTimeout(DefaultRepeatDelay), env)

      case r"""I am on the (.+?)$$$page""" =>
        env.scopes.addScope(page)
        
      case r"""I navigate to the (.+?)$$$page""" => 
        env.scopes.addScope(page)
        val url = env.getAttribute("url")
        env.execute {
          env.withWebDriver(_.get(url))(WebSettings.`gwen.web.capture.screenshots`)
        }
        
      case r"""I navigate to "(.+?)"$$$url""" => 
        env.scopes.addScope(url)
        env.execute {
          env.withWebDriver(_.get(url))(WebSettings.`gwen.web.capture.screenshots`)
        }
        
      case r"""I scroll to the (top|bottom)$position of (.+?)$$$element""" =>
        val elementBinding = env.getLocatorBinding(element)
        env.execute {
          env.scrollIntoView(elementBinding, ScrollTo.withName(position))
        }

      case r"""the url will be defined by (?:property|setting) "(.+?)"$$$name""" => 
        env.scopes.set("url", Settings.get(name))
        
      case r"""the url will be "(.+?)"$$$url""" => 
        env.scopes.set("url", url)   

      case r"""(.+?)$element can be located by (id|name|tag name|css selector|xpath|class name|link text|partial link text|javascript)$locator "(.+?)"$expression in (.+?)$$$container""" =>
        env.getLocatorBinding(container)
        env.scopes.set(s"$element/locator", locator)
        env.scopes.set(s"$element/locator/$locator", expression)
        env.scopes.set(s"$element/locator/$locator/container", container)
        
      case r"""(.+?)$element can be located by (id|name|tag name|css selector|xpath|class name|link text|partial link text|javascript)$locator "(.+?)"$$$expression""" =>
        env.scopes.set(s"$element/locator", locator)
        env.scopes.set(s"$element/locator/$locator", expression)
        env.scopes.getOpt(s"$element/locator/$locator/container") foreach { _ =>
          env.scopes.set(s"$element/locator/$locator/container", null)
        }
        
      case r"""(.+?)$element can be (clicked|submitted|checked|ticked|unchecked|unticked)$event by javascript "(.+?)"$$$expression""" =>
        env.getLocatorBinding(element)
        env.scopes.set(s"$element/action/${WebEvents.EventToAction(event)}/javascript", expression)

      case r"""the page title should( not)?$negation (be|contain|start with|end with|match regex|match xpath|match json path)$operator "(.*?)"$$$expression""" =>  env.execute {
        env.compare("title", expression, () => env.getTitle, operator, Option(negation).isDefined)
      }
        
      case r"""the page title should( not)?$negation (be|contain|start with|end with|match regex|match xpath|match json path)$operator (.+?)$$$attribute""" =>
        val expected = env.getAttribute(attribute)
        env.execute {
          env.compare("title", expected, () => env.getTitle, operator, Option(negation).isDefined)
        }

      case r"""(.+?)$element should( not)?$negation be (displayed|hidden|checked|ticked|unchecked|unticked|enabled|disabled)$$$state""" =>
        val elementBinding = env.getLocatorBinding(element)
        env.execute {
          env.checkElementState(elementBinding, state, Option(negation).nonEmpty)
        }

      case r"""(.+?)$element( text| value)?$selection should( not)?$negation (be|contain|start with|end with|match regex|match xpath|match json path)$operator "(.*?)"$$$expression""" =>
        if (element == "I") undefinedStepError(step)
        val actual = env.boundAttributeOrSelection(element, Option(selection))
        env.execute {
          env.compare(element + Option(selection).getOrElse(""), expression, actual, operator, Option(negation).isDefined)
        }

      case r"""(.+?)$element( value| text)?$selection should( not)?$negation (be|contain|start with|end with|match regex|match xpath|match json path)$operator (.+?)$$$attribute""" =>
        if (element == "I") undefinedStepError(step)
        val expected = env.getAttribute(attribute)
        val actual = env.boundAttributeOrSelection(element, Option(selection))
        env.execute {
          env.compare(element + Option(selection).getOrElse(""), expected, actual, operator, Option(negation).isDefined)
        }

      case r"""I capture the (text|node|nodeset)$targetType in (.+?)$source by xpath "(.+?)"$expression as (.+?)$$$name""" =>
        val src = env.getBoundReferenceValue(source)
        env.featureScope.set(name, env.execute(env.evaluateXPath(expression, src, env.XMLNodeType.withName(targetType)) tap { content =>
          env.addAttachment(name, "txt", content)
        }).getOrElse(s"$$[xpath:$expression]"))

      case r"""I capture the text in (.+?)$source by regex "(.+?)"$expression as (.+?)$$$name""" =>
        val src = env.getBoundReferenceValue(source)
        env.featureScope.set(name, env.execute(env.extractByRegex(expression, src) tap { content =>
          env.addAttachment(name, "txt", content)
        }).getOrElse(s"$$[regex:$expression"))

      case r"""I capture the content in (.+?)$source by json path "(.+?)"$expression as (.+?)$$$name""" =>
        val src = env.getBoundReferenceValue(source)
        env.featureScope.set(name, env.execute(env.evaluateJsonPath(expression, src) tap { content => 
          env.addAttachment(name, "txt", content) 
        }).getOrElse(s"$$[json path:$expression"))
      
      case r"""I capture the current URL""" => 
        env.captureCurrentUrl()
      
      case r"""I capture the current URL as (.+?)$name""" => 
        env.featureScope.set(name, env.execute(env.withWebDriver(_.getCurrentUrl()) tap { content => 
          env.addAttachment(name, "txt", content) 
        }).getOrElse("$[currentUrl]"))
        
      case r"""I capture the current screenshot""" => 
        env.execute {
          env.captureScreenshot(true)
        }
        
      case r"""I capture (.+?)$element( value| text)?$selection as (.+?)$attribute""" =>
        val value = Option(selection) match {
          case None => env.getBoundReferenceValue(element)
          case Some(sel) => env.getElementSelection(element, sel)
        }
        env.featureScope.set(attribute, value tap { content => 
          env.addAttachment(attribute, "txt", content) 
        })
        
      case r"""I capture (.+?)$element( value| text)?$$$selection""" =>
        val value = Option(selection) match {
          case None => env.getBoundReferenceValue(element)
          case Some(sel) => env.getElementSelection(element, sel)
        }
        env.featureScope.set(element, value tap { content => 
          env.addAttachment(element, "txt", content) 
        })
        
      case r"""I base64 decode (.+?)$attribute as (.+?)$$$name""" => 
        val source = env.getBoundReferenceValue(attribute)
        env.featureScope.set(name, env.execute(decodeBase64(source) tap { content => 
          env.addAttachment(name, "txt", content) 
        }).getOrElse(s"$$[base64 decoded $attribute]"))
        
      case r"""I base64 decode (.+?)$attribute""" => 
        val source = env.getBoundReferenceValue(attribute)
        env.featureScope.set(attribute, env.execute(decodeBase64(source) tap { content => 
          env.addAttachment(attribute, "txt", content) 
        }).getOrElse(s"$$[base64 decoded $attribute]"))
        
      case r"""(.+?)$attribute (?:is|will be) defined by (javascript|system process|property|setting|file)$attrType "(.+?)"$$$expression""" =>
        attrType match {
          case "javascript" => env.scopes.set(s"$attribute/javascript", expression)
          case "system process" => env.scopes.set(s"$attribute/sysproc", expression)
          case "file" => env.scopes.set(s"$attribute/file", expression)
          case _ => env.featureScope.set(attribute, Settings.get(expression))
        }

      case r"""(.+?)$attribute (?:is|will be) defined by the (text|node|nodeset)$targetType in (.+?)$source by xpath "(.+?)"$$$expression""" =>
        env.scopes.set(s"$attribute/xpath/source", source)
        env.scopes.set(s"$attribute/xpath/targetType", targetType)
        env.scopes.set(s"$attribute/xpath/expression", expression)
      
      case r"""(.+?)$attribute (?:is|will be) defined in (.+?)$source by regex "(.+?)"$$$expression""" => 
        env.scopes.set(s"$attribute/regex/source", source)
        env.scopes.set(s"$attribute/regex/expression", expression)
        
      case r"""(.+?)$attribute (?:is|will be) defined in (.+?)$source by json path "(.+?)"$$$expression""" => 
        env.scopes.set(s"$attribute/json path/source", source)
        env.scopes.set(s"$attribute/json path/expression", expression)
      
      case r"""(.+?)$attribute (?:is|will be) defined by sql "(.+?)"$selectStmt in the (.+?)$dbName database""" =>
        env.scopes.set(s"$attribute/sql/selectStmt", selectStmt)
        env.scopes.set(s"$attribute/sql/dbName", dbName)

      case r"""I clear (.+?)$$$element""" =>
        val elementBinding = env.getLocatorBinding(element)
        env.execute {
          env.clearText(elementBinding)
        }

      case r"""I press (enter|tab)$key in (.+?)$$$element""" =>
        val elementBinding = env.getLocatorBinding(element)
        env.execute {
          env.withWebElement(elementBinding) { elem =>
            key match {
              case "enter" =>
                elem.sendKeys(Keys.RETURN)
              case _ =>
                elem.sendKeys(Keys.TAB)
            }
            env.bindAndWait(element, key, "true")
          }
        }

      case r"""I (enter|type)$action "(.*?)"$value in (.+?)$$$element""" =>
        val elementBinding = env.getLocatorBinding(element)
        env.execute {
          env.sendKeys(elementBinding, value, clearFirst = true, sendEnterKey = action == "enter")
        }
        
      case r"""I (enter|type)$action (.+?)$attribute in (.+?)$$$element""" =>
        val elementBinding = env.getLocatorBinding(element)
        val value = env.getAttribute(attribute)
        env.execute {
          env.sendKeys(elementBinding, value, clearFirst = true, sendEnterKey = action == "enter")
        }

      case r"""I select the (\d+?)$position(?:st|nd|rd|th) option in (.+?)$$$element""" =>
        val elementBinding = env.getLocatorBinding(element)
        env.execute {
          env.selectByIndex(elementBinding, position.toInt - 1)
        }

      case r"""I select "(.*?)"$value in (.+?)$element by value""" =>
        val elementBinding = env.getLocatorBinding(element)
        env.execute { 
          env.selectByValue(elementBinding, value)
        }
        
      case r"""I select "(.*?)"$value in (.+?)$$$element""" =>
        val elementBinding = env.getLocatorBinding(element)
        env.execute {
          env.selectByVisibleText(elementBinding, value)
        }

      case r"""I select (.+?)$attribute in (.+?)$element by value""" =>
        val value = env.getAttribute(attribute)
        val elementBinding = env.getLocatorBinding(element)
        env.execute {
          env.selectByValue(elementBinding, value)
        }

      case r"""I select (.+?)$attribute in (.+?)$$$element""" =>
        val value = env.getAttribute(attribute)
        val elementBinding = env.getLocatorBinding(element)
        env.execute {
          env.selectByVisibleText(elementBinding, value)
        }

      case r"""I (click|check|tick|uncheck|untick)$action (.+?)$element of (.+?)$$$context""" =>
        try {
          val contextBinding = env.getLocatorBinding(context)
          val elementBinding = env.getLocatorBinding(element)
          env.execute {
            env.performActionIn(action, elementBinding, contextBinding)
          }
        } catch {
          case e1: LocatorBindingException =>
            try {
              val elementBinding = env.getLocatorBinding(s"$element of $context")
              env.execute {
                env.performAction(action, elementBinding)
              }
            } catch {
              case e2: LocatorBindingException =>
                throw new LocatorBindingException(s"'$element', '$context', or '$element of $context'", s"${e1.getMessage}, ${e2.getMessage}")
            }
        }

      case r"""I (click|submit|check|tick|uncheck|untick)$action (.+?)$$$element""" =>
        val elementBinding = env.getLocatorBinding(element)
        env.execute {
          env.performAction(action, elementBinding)
        }

      case r"""I (?:highlight|locate) (.+?)$$$element""" =>
        val elementBinding = env.getLocatorBinding(element)
        env.execute {
          env.withWebElement(elementBinding) { _ => }
        }

      case "I refresh the current page" => env.execute { 
        env.withWebDriver { _.navigate().refresh() }
      }
      
      case r"I start a new browser" => env.execute {
        env.quit("primary")
        env.switchToSession("primary")
      }
      
      case r"""I start a browser for (.+?)$$$session""" => env.execute {
        env.quit(session)
        env.switchToSession(session)
      }
      
      case r"I close the(?: current)? browser" => env.execute {
        env.quit()
      }
      
      case r"""I close the browser for (.+?)$session""" => env.execute {
        env.quit(session)
      }
      
      case r"""I switch to the child window""" => env.execute {
        env.withWebDriver(env.switchToChild)
      }
      
      case r"""I close the child window""" => env.execute {
        env.closeChild()
      }
      
      case r"""I switch to the parent window""" => env.execute {
        env.switchToParent(false)
      }
      
      case r"""I switch to (.+?)$session""" => env.execute {
        env.switchToSession(session)
      }
      
      case r"I (accept|dismiss)$action the (?:alert|confirmation) popup" => env.execute {
        env.withWebDriver { driver =>
          if (action == "accept") {
            driver.switchTo().alert().accept()
          } else {
            driver.switchTo().alert().dismiss()
          }
        }
      }

      case r"""I resize the window to width (\d+?)$width and height (\d+?)$$$height""" => env.execute {
        env.resizeWindow(width.toInt, height.toInt)
      }

      case r"""I maximi(?:z|s)e the window""" => env.execute {
        env.maximizeWindow()
      }
        
      case _ => super.evaluate(step, env)
      
    }
  }
  
  /**
    * Performs a repeat until or while operation 
    */
  private def repeat(operation: String, step: Step, doStep: String, condition: String, delay: Duration, timeout: Duration, env: WebEnvContext) {
    assert(delay.gteq(Duration.Zero), "delay cannot be less than zero")
    assert(timeout.gt(Duration.Zero), "timeout must be greater than zero")
    assert(timeout.gteq(delay), "timeout cannot be less than or equal to delay")
    env.execute {
      var attempt = 0L
      env.waitUntil(s"Repeating $operation $condition", timeout.toSeconds) {
        attempt = attempt + 1
        operation match {
          case "until" =>
            logger.info(s"Repeat-until[$attempt]")
            evaluateStep(Step(step.keyword, doStep), env).evalStatus match {
              case Failed(_, e) => throw e
              case _ =>
                val javascript = env.scopes.get(s"$condition/javascript")
                env.executeScriptPredicate(javascript) tap { result =>
                  if (!result) {
                    logger.info(s"Repeat-until[$attempt] not completed, ..${if (delay.gt(Duration.Zero)) s"will try again in ${DurationFormatter.format(delay)}" else "trying again"}")
                    Thread.sleep(delay.toMillis)
                  } else {
                    logger.info(s"Repeat-until[$attempt] completed")
                  }
                }
            }
          case "while" =>
            val javascript = env.scopes.get(s"$condition/javascript")
            val result = env.executeScriptPredicate(javascript)
            if (result) {
              logger.info(s"Repeat-while[$attempt]")
              evaluateStep(Step(step.keyword, doStep), env).evalStatus match {
                case Failed(_, e) => throw e
                case _ => 
                  logger.info(s"Repeat-while[$attempt] not completed, ..${if (delay.gt(Duration.Zero)) s"will try again in ${DurationFormatter.format(delay)}" else "trying again"}")
                  Thread.sleep(delay.toMillis)
              }
            } else {
              logger.info(s"Repeat-while[$attempt] completed")
            }
            !result
        }
      }
    } getOrElse { 
      env.scopes.get(s"$condition/javascript")
      this.evaluateStep(Step(step.keyword, doStep), env).evalStatus match {
        case Failed(_, e) => throw e
        case _ => step
      }
    }
  }

  /**
    * Repeats a step for each web element in an iteration of elements.
    */
  private def foreach(elementsBinding: LocatorBinding, elementName: String, step: Step, doStep: String, env: WebEnvContext) {
    val steps = env.execute {
      env.locateAll(env, elementsBinding) match {
        case Nil =>
          logger.info(s"For-each[$elementName]: none found")
          Nil
        case webElements =>
          val noOfElements = webElements.size
          logger.info(s"For-each[$elementName]: $noOfElements found")
          webElements.zipWithIndex.foldLeft(List[Step]()) { case (acc, (webElement, index)) =>
            env.featureScope.objects.bind(elementName, webElement)
            (try {
              EvalStatus(acc.map(_.evalStatus)) match {
                case Failed(_, _) if (env.execute(GwenSettings.`gwen.feature.failfast`).getOrElse(false)) =>
                  logger.info(s"Skipping [$elementName] ${index + 1} of $noOfElements")
                  Step(step.pos, if (index == 0) StepKeyword.Given else StepKeyword.And, doStep, Skipped)
                case _ =>
                  logger.info(s"Processing [$elementName] ${index + 1} of $noOfElements")
                  evaluateStep(Step(step.pos, if (index == 0) StepKeyword.Given else StepKeyword.And, doStep), env)
              }
            } finally {
              env.featureScope.objects.clear(elementName)
            }) :: acc
          } reverse
      }
    } getOrElse {
      env.featureScope.objects.bind(elementName, "CurrentElement[DryRun]")
      try {
        List(evaluateStep(Step(step.keyword, doStep), env))
      } finally {
        env.featureScope.objects.clear(elementName)
      }
    }
    val foreachStepDef = new Scenario(List(Tag.StepDefTag, Tag.ForEachTag), elementName, Nil, None, steps, false, Nil, None)
    env.foreachStepDefs += (step.uniqueId -> foreachStepDef)
  }
  
  lazy val DefaultRepeatDelay: Duration = {
    val waitSecs = WebSettings.`gwen.web.wait.seconds` 
    if (waitSecs > 9 && waitSecs % 10 == 0) Duration(waitSecs / 10, "second") else Duration(waitSecs * 100, "millisecond")
  }
  
  private def defaultRepeatTimeout(delay: Duration): Duration = delay * 30
  
}





© 2015 - 2025 Weber Informatics LLC | Privacy Policy