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

japgolly.scalajs.react.test.ReactTestUtils.scala Maven / Gradle / Ivy

There is a newer version: 3.0.0-beta8
Show newest version
package japgolly.scalajs.react.test

import japgolly.scalajs.react._
import japgolly.scalajs.react.facade.{React => RawReact, ReactDOM => RawReactDOM}
import japgolly.scalajs.react.hooks.Hooks
import japgolly.scalajs.react.internal.CoreGeneral._
import japgolly.scalajs.react.test.ReactTestUtilsConfig.aroundReact
import japgolly.scalajs.react.util.DefaultEffects.{Async => DA, Sync => DS}
import japgolly.scalajs.react.util.Effect._
import japgolly.scalajs.react.util.JsUtil
import org.scalajs.dom
import org.scalajs.dom.html.Element
import org.scalajs.dom.{console, document}
import scala.concurrent.{ExecutionContext, Future}
import scala.scalajs.js
import scala.scalajs.js.|

@deprecated("Migrate to React 18 and ReactTestUtils2. This will be replaced by ReactTestUtils2 in scalajs-react 3.0.0.", "2.2.0 / React 18")
object ReactTestUtils extends ReactTestUtils {
  @inline def raw = japgolly.scalajs.react.test.facade.ReactTestUtils

  @deprecated("Use .withNewDocumentElementAsync", "2.0.0")
  def withNewDocumentElementAsyncCallback[F[_], A](use: Element => F[A])(implicit F: Async[F]): F[A] =
    withNewDocumentElementAsync(use)

  @deprecated("Use .withRenderedIntoDocumentAsync", "2.0.0")
  def withRenderedIntoDocumentAsyncCallback[M](u: Unmounted[M]): WithRenderedDslF[DA, M, Element] =
    withRenderedIntoDocumentAsync(u)

  @deprecated("Use .withNewBodyElementAsync", "2.0.0")
  def withNewBodyElementAsyncCallback[F[_], A](use: Element => F[A])(implicit F: Async[F]): F[A] =
    withNewBodyElementAsync(use)

  @deprecated("Use .withRenderedIntoBodyAsync", "2.0.0")
  def withRenderedIntoBodyAsyncCallback[M](u: Unmounted[M]): WithRenderedDslF[DA, M, Element] =
    withRenderedIntoBodyAsync(u)

  @deprecated("Use .withRenderedAsync", "2.0.0")
  def withRenderedAsyncCallback[M](u: Unmounted[M], intoBody: Boolean): WithRenderedDslF[DA, M, Element] =
    withRenderedAsync(u, intoBody)

  trait WithRenderedDsl[M, R] {
    def apply     [A](f: (M, R) => A): A
    def apply     [A](f: M      => A): A = apply((m, _) => f(m))
    def withParent[A](f: R      => A): A = apply((_, r) => f(r))
  }

  trait WithRenderedDslF[F[_], M, R] {
    def apply     [A](f: (M, R) => F[A]): F[A]
    def apply     [A](f: M      => F[A]): F[A] = apply((m, _) => f(m))
    def withParent[A](f: R      => F[A]): F[A] = apply((_, r) => f(r))
  }

  private[ReactTestUtils] object Internals {

    val reactDataAttrRegex = """\s+data-react\S*?\s*?=\s*?".*?"""".r
    val reactTextCommentRegex = """""".r

    type RawM = japgolly.scalajs.react.facade.React.ComponentUntyped

    def wrapMO(r: RawM | Null): MountedOutput =
      if (r == null)
        null
      else {
        val r2 = JsUtil.notNull[RawM](r) // TODO: https://github.com/lampepfl/dotty/issues/12739
        val x = JsComponent.mounted(r2)
        x.asInstanceOf[MountedOutput]
      }

    def mountedElement(c: RawReact.ComponentUntyped | Null) =
      if (c == null) null else ReactDOM.findDOMNode(c).get.asElement()

    def parentNode(c: RawReact.ComponentUntyped | Null) = {
      val m = mountedElement(c)
      if (m == null) null else m.parentNode
    }

    def parentElement(c: RawReact.ComponentUntyped | Null) = {
      val p = parentNode(c)
      if (p == null) null else p.domCast[Element]
    }

    def attemptFuture[A](f: => Future[A]): Future[A] =
      try f catch { case err: Exception => Future.failed(err) }

    def warnOnError(prefix: String)(a: => Any): Unit =
      try {
        a
        ()
      } catch {
        case t: Throwable =>
          console.warn(s"$prefix: $t")
      }

    def unmount(container: dom.Node): Unit =
      warnOnError("Failed to unmount component") {
        ReactDOM.unmountComponentAtNode(container)
      }

    def _withNewElementAsync[F[_], A](create: => Element,
                                      use   : Element => F[A],
                                      remove: Element => Unit,
                                    )(implicit F: Async[F]): F[A] =
      F.flatMap(F.delay(create))(e =>
        F.finallyRun(use(e), F.delay(act(remove(e)))))

    def _withRenderedAsync[F[_], M, A](u: Unmounted[M], parent: Element, f: (M, Element) => F[A])
                                      (implicit F: Async[F]): F[A] =
      aroundReactAsync {
        F.flatMap(F.delay(act(RawReactDOM.render(u.raw, parent)))) { c =>
          val m = u.mountRawOrNull(c)
          F.finallyRun(f(m, parent), F.delay(act(unmountRawComponent(c))))
        }
      }

    def aroundReactAsync[F[_], A](body: F[A])(implicit F: Async[F]): F[A] = {
      val start = F.delay {
        val stop = aroundReact.start()
        F.delay(stop())
      }
      F.flatMap(start) { stop =>
        F.finallyRun(body, stop)
      }
    }

    def aroundReactFuture[A](body: => Future[A])(implicit ec: ExecutionContext): Future[A] = {
      val stop = aroundReact.start()
      val f    = body
      f.onComplete { _ => stop() }
      f
    }

  } // Internals
}

@deprecated("Migrate to React 18 and ReactTestUtils2. This will be replaced by ReactTestUtils2 in scalajs-react 3.0.0.", "2.2.0 / React 18")
trait ReactTestUtils extends japgolly.scalajs.react.test.internal.ReactTestUtilExtensions {
  import ReactTestUtils._
  import ReactTestUtils.Internals._

  type Unmounted[M] = GenericComponent.Unmounted[_, M]
  type Mounted      = GenericComponent.MountedRaw

  type MountedOutput = JsComponent.Mounted[js.Object, js.Object]

  type CompType = GenericComponent.ComponentRaw {type Raw <: japgolly.scalajs.react.facade.React.ComponentClassUntyped }

  /** When writing UI tests, tasks like rendering, user events, or data fetching can be considered as "units" of
    * interaction with a user interface. React provides a helper called act() that makes sure all updates related to
    * these "units" have been processed and applied to the DOM before you make any assertions:
    *
    * {{{
    *   act {
    *     // render components
    *   }
    *   // make assertions
    * }}}
    *
    * This helps make your tests run closer to what real users would experience when using your application.
    */
  def act[A](body: => A): A = {
    var a = Option.empty[A]
    raw.act(() => { a = Some(body) })
    a.getOrElse(throw new RuntimeException("React's TestUtils.act didn't seem to complete."))
  }

  /** When writing UI tests, tasks like rendering, user events, or data fetching can be considered as "units" of
    * interaction with a user interface. React provides a helper called act() that makes sure all updates related to
    * these "units" have been processed and applied to the DOM before you make any assertions:
    *
    * {{{
    *   await act(async () => {
    *     // render components
    *   });
    *   // make assertions
    * }}}
    *
    * This helps make your tests run closer to what real users would experience when using your application.
    */
  def actAsync[F[_], A](body: F[A])(implicit F: Async[F]): F[A] = {
    F.flatMap(F.delay(new Hooks.Var(Option.empty[A]))) { ref =>
      def setAsync(a: A): F[Unit] = F.delay(DS.runSync(ref.set(Some(a))))
      val body2 = F.flatMap(body)(setAsync)
      val body3 = F.fromJsPromise(raw.actAsync(F.toJsPromise(body2)))
      F.map(body3)(_ => ref.value.getOrElse(throw new RuntimeException("React's TestUtils.act didn't seem to complete.")))
    }
  }

  /** Render a component into a detached DOM node in the document. This function requires a DOM. */
  def renderIntoDocument[M](unmounted: Unmounted[M]): M = {
    val c = act(raw.renderIntoDocument(unmounted.raw))
    unmounted.mountRawOrNull(c)
  }

  def renderIntoDocument(e: vdom.VdomElement): MountedOutput =
    wrapMO(act(raw.renderIntoDocument(e.rawElement)))

  /**
   * Traverse all components in tree and accumulate all components where test(component) is true.
   * This is not that useful on its own, but it's used as a primitive for other test utils.
   */
  def findAllInRenderedTree(tree: Mounted, test: MountedOutput => Boolean): Vector[MountedOutput] =
    raw.findAllInRenderedTree(tree.raw, (m: RawM) => test(wrapMO(m))).iterator.map(wrapMO(_)).toVector

  /**
   * Finds all instance of components in the rendered tree that are DOM components with the class name
   * matching className.
   */
  def scryRenderedDOMComponentsWithClass(tree: Mounted, className: String): Vector[MountedOutput] =
    raw.scryRenderedDOMComponentsWithClass(tree.raw, className).iterator.map(wrapMO(_)).toVector

  /**
   * Like [[scryRenderedDOMComponentsWithClass()]] but expects there to be one result, and returns that one result, or
   * throws exception if there is any other number of matches besides one.
   */
  def findRenderedDOMComponentWithClass(tree: Mounted, className: String): MountedOutput =
    wrapMO(raw.findRenderedDOMComponentWithClass(tree.raw, className))

  /**
   * Finds all instance of components in the rendered tree that are DOM components with the tag name
   * matching tagName.
   */
  def scryRenderedDOMComponentsWithTag(tree: Mounted, tagName: String): Vector[MountedOutput] =
    raw.scryRenderedDOMComponentsWithTag(tree.raw, tagName).iterator.map(wrapMO(_)).toVector

  /**
   * Like [[scryRenderedDOMComponentsWithTag()]] but expects there to be one result, and returns that one result, or
   * throws exception if there is any other number of matches besides one.
   */
  def findRenderedDOMComponentWithTag(tree: Mounted, tagName: String): MountedOutput =
    wrapMO(raw.findRenderedDOMComponentWithTag(tree.raw, tagName))

  /** Finds all instances of components with type equal to componentClass. */
  def scryRenderedComponentsWithType(tree: Mounted, c: CompType): Vector[MountedOutput] =
    raw.scryRenderedComponentsWithType(tree.raw, c.raw).iterator.map(wrapMO(_)).toVector

  /**
   * Same as [[scryRenderedComponentsWithType()]] but expects there to be one result and returns that one result, or throws
   * exception if there is any other number of matches besides one.
   */
  def findRenderedComponentWithType(tree: Mounted, c: CompType): MountedOutput =
    wrapMO(raw.findRenderedComponentWithType(tree.raw, c.raw))

  def unmountRawComponent(c: RawReact.ComponentUntyped | Null): Unit = {
    val p = parentNode(c)
    if (p != null)
      unmount(p)
  }

  // ===================================================================================================================
  // Render into body

  def newBodyElement(): Element = {
    val cont = document.createElement("div").domAsHtml
    document.body.appendChild(cont)
    cont
  }

  def removeNewBodyElement(e: Element): Unit =
    warnOnError("Failed to unmount newBodyElement") {
      ReactDOM unmountComponentAtNode e // Doesn't matter if no component mounted here
      document.body.removeChild(e)
    }

  def withNewBodyElement[A](use: Element => A): A = {
    val e = newBodyElement()
    try
      use(e)
    finally
      act(removeNewBodyElement(e))
  }

  /** Renders a component into the document body via [[ReactDOM.render()]],
    * then unmounts and cleans up after use.
    *
    * Unlike [[ReactTestUtils.renderIntoDocument()]], this allows DOM focus to work.
    */
  def withRenderedIntoBody[M](u: Unmounted[M]): WithRenderedDsl[M, Element] =
    new WithRenderedDsl[M, Element] {
      override def apply[A](f: (M, Element) => A): A =
        withNewBodyElement { parent =>
          aroundReact {
            val c = act(RawReactDOM.render(u.raw, parent))
            try
              f(u.mountRawOrNull(c), parent)
            finally
              unmountRawComponent(c)
          }
        }
    }

  /** Renders a component into the document body via [[ReactDOM.render()]].
    *
    * Unlike [[ReactTestUtils.renderIntoDocument()]], this allows DOM focus to work.
    */
  def renderIntoBody[M](u: Unmounted[M]): M = {
    val c = act(RawReactDOM.render(u.raw, newBodyElement()))
    u.mountRawOrNull(c)
  }

  def withNewBodyElementFuture[A](use: Element => Future[A])(implicit ec: ExecutionContext): Future[A] = {
    val e = newBodyElement()
    attemptFuture(use(e)).andThen { case _ => act(removeNewBodyElement(e)) }
  }

  /** Renders a component into the document body via [[ReactDOM.render()]],
    * and asynchronously waits for the Future to complete before unmounting.
    */
  def withRenderedIntoBodyFuture[M, A](u: Unmounted[M])(f: M => Future[A])(implicit ec: ExecutionContext): Future[A] =
    withNewBodyElementFuture { parent =>
      aroundReactFuture {
        val c = act(RawReactDOM.render(u.raw, parent))
        val m = u.mountRawOrNull(c)
        attemptFuture(f(m)).andThen { case _ => act(unmountRawComponent(c)) }
      }
    }

  def withNewBodyElementAsync[F[_], A](use: Element => F[A])(implicit F: Async[F]): F[A] =
    _withNewElementAsync(newBodyElement(), use, removeNewBodyElement)

  /** Renders a component into the document body via [[ReactDOM.render()]],
    * and asynchronously waits for the Async to complete before unmounting.
    */
  def withRenderedIntoBodyAsync[M](u: Unmounted[M]): WithRenderedDslF[DA, M, Element] =
    new WithRenderedDslF[DA, M, Element] {
      override def apply[A](f: (M, Element) => DA[A]) =
        withNewBodyElementAsync(_withRenderedAsync(u, _, f))
    }

  // ===================================================================================================================
  // Render into document

  def newDocumentElement(): Element =
    document.createElement("div").domAsHtml

  def removeNewDocumentElement(e: Element): Unit =
    warnOnError("Failed to unmount newDocumentElement") {
      // This DOM is detached so the best we can do (for memory) is remove its children
      while (e.hasChildNodes()) {
        val c = e.childNodes(0)
        unmount(c) // Doesn't matter if no component mounted here
        e.removeChild(c)
      }
    }

  def withNewDocumentElement[A](use: Element => A): A = {
    val e = newDocumentElement()
    try
      use(e)
    finally
      act(removeNewDocumentElement(e))
  }

  /** Renders a component into detached DOM via [[ReactTestUtils.renderIntoDocument()]],
    * then unmounts and cleans up after use.
    */
  def withRenderedIntoDocument[M](u: Unmounted[M]): WithRenderedDsl[M, Element] =
    new WithRenderedDsl[M, Element] {
      override def apply[A](f: (M, Element) => A): A =
        aroundReact {
          val c = act(raw.renderIntoDocument(u.raw))
          try {
            val p = parentElement(c)
            val m = u.mountRawOrNull(c)
            f(m, p)
          } finally
            act(unmountRawComponent(c))
        }
  }

  def withNewDocumentElementFuture[A](use: Element => Future[A])(implicit ec: ExecutionContext): Future[A] = {
    val e = newDocumentElement()
    attemptFuture(use(e)).andThen { case _ => act(removeNewDocumentElement(e)) }
  }

  /** Renders a component into detached DOM via [[ReactTestUtils.renderIntoDocument()]],
    * and asynchronously waits for the Future to complete before unmounting.
    */
  def withRenderedIntoDocumentFuture[M, A](u: Unmounted[M])(f: M => Future[A])(implicit ec: ExecutionContext): Future[A] =
    aroundReactFuture {
      val c = act(raw.renderIntoDocument(u.raw))
      val m = u.mountRawOrNull(c)
      attemptFuture(f(m)).andThen { case _ => act(unmountRawComponent(c)) }
    }

  def withNewDocumentElementAsync[F[_], A](use: Element => F[A])(implicit F: Async[F]): F[A] =
    _withNewElementAsync(newDocumentElement(), use, removeNewDocumentElement)

  /** Renders a component into the document body via [[ReactDOM.render()]],
    * and asynchronously waits for the Async to complete before unmounting.
    */
  def withRenderedIntoDocumentAsync[M](u: Unmounted[M]): WithRenderedDslF[DA, M, Element] =
    new WithRenderedDslF[DA, M, Element] {
      override def apply[A](f: (M, Element) => DA[A]) =
        withNewDocumentElementAsync(_withRenderedAsync(u, _, f))
  }

  // ===================================================================================================================
  // Render into body/document

  /** Renders a component then unmounts and cleans up after use.
    *
    * @param intoBody Whether to use [[renderIntoBody()]] or [[ReactTestUtils.renderIntoDocument()]].
    */
  def withRendered[M](u: Unmounted[M], intoBody: Boolean): WithRenderedDsl[M, Element] =
    if (intoBody)
      withRenderedIntoBody(u)
    else
      withRenderedIntoDocument(u)

  /** Renders a component then unmounts and cleans up after use.
    *
    * @param intoBody Whether to use [[renderIntoBodyFuture()]] or [[renderIntoDocumentFuture()]].
    */
  def withRenderedFuture[M, A](u: Unmounted[M], intoBody: Boolean)(f: M => Future[A])(implicit ec: ExecutionContext): Future[A] =
    if (intoBody)
      withRenderedIntoBodyFuture(u)(f)
    else
      withRenderedIntoDocumentFuture(u)(f)

  /** Renders a component then unmounts and cleans up after use.
    *
    * @param intoBody Whether to use [[renderIntoBodyAsync()]] or [[renderIntoDocumentAsync()]].
    */
  def withRenderedAsync[M](u: Unmounted[M], intoBody: Boolean): WithRenderedDslF[DA, M, Element] =
    if (intoBody)
      withRenderedIntoBodyAsync(u)
    else
      withRenderedIntoDocumentAsync(u)

  // ===================================================================================================================

  def modifyProps[P, U <: GenericComponent.Unmounted[P, M], M <: GenericComponent.MountedImpure[P, _]]
      (c: GenericComponent[P, CtorType.Props, U], m: M)(f: P => P): M = {
    val container = m.getDOMNode.asMounted().node.parentNode
    val p2 = f(m.props)
    act(c(p2).renderIntoDOM(container.domCast[org.scalajs.dom.Element]))
  }

  def replaceProps[P, U <: GenericComponent.Unmounted[P, M], M <: GenericComponent.MountedImpure[P, _]]
      (c: GenericComponent[P, CtorType.Props, U], m: M)(p: P): M =
    modifyProps(c, m)(_ => p)

  /**
    * Turn `<div data-reactroot="">hello</div>`
    * into `<div>hello</div>`
    */
  def removeReactInternals(html: String): String =
    reactDataAttrRegex.replaceAllIn(
      reactTextCommentRegex.replaceAllIn(
        html,
      ""),
    "")
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy