Skip to content

Commit

Permalink
Integrate unit testing framework with sbt test discovery.
Browse files Browse the repository at this point in the history
- Tests can now be run using the test facilities built in sbt. To use it
  you need to add a dependency on a Scala.js test framework (for example
  Scala.js jasmine test framework) and add a marker interface
  (`scala.scalajs.test.Test`) to your test classes (or extend
  JasmineTest when using the Scala.js jasmine test framework).
- The default Scala.js test framework and Scala.js test bridge class can
  be changed using sbt settings.
- A reference implementation of the test framework using jasmine is
  provided.
- Added experimental support for `env.js` in the run and test
  environment. Just add `env.js` webjar as a dependency to your project.
- Run command now searches for objects that have a `main` method (normal
  sbt behavior).
  • Loading branch information
EECOLOR authored and sjrd committed Jan 4, 2014
1 parent da6d1d3 commit 82c0ed1
Show file tree
Hide file tree
Showing 58 changed files with 1,560 additions and 559 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
language: scala
script:
- sbt package scalajs-test/test
- sbt package testing/test scalajs-test/test
after_success:
- if [[ "${PUBLISH_ENABLED}" == "true" && "${TRAVIS_PULL_REQUEST}" == "false" && "${TRAVIS_BRANCH}" == "master" && "${PUBLISH_USER}" != "" && "${PUBLISH_PASS}" != "" ]]; then sbt publish; fi
scala:
Expand Down
Empty file added examples/testing/build.sbt
Empty file.
7 changes: 7 additions & 0 deletions examples/testing/src/main/scala/ElementCreator.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import scala.scalajs.js.Dynamic.global

object ElementCreator {
val jQ = global.jQuery

def create() = jQ("body").append(jQ("<h1>Test</h1>"))
}
26 changes: 26 additions & 0 deletions examples/testing/src/test/scala/ElementCreatorTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import scala.scalajs.js
import scala.scalajs.js.Dynamic.global
import scala.scalajs.test.JasmineTest

object ElementCreatorTest extends JasmineTest {

// ElementCreator expects jquery to be present
global.importScripts("jquery.js")

describe("ElementCreator") {

it("should be able to create an element in the body") {
// create the element
ElementCreator.create()

// jquery would make this easier, but I wanted to
// only use pure html in the test itself
val body = global.document.getElementsByTagName("body")
.asInstanceOf[js.Array[js.Dynamic]].head

// the Scala.js DOM API would make this easier
expect(body.firstChild.tagName.toString == "H1").toBeTruthy
expect(body.firstChild.innerHTML.toString == "Test").toBeTruthy
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.scalajs.jasmine

import scala.scalajs.js

object Jasmine extends js.GlobalScope {
def describe(name: String, suite: js.Function0[_]): Unit = ???
def it(title: String, test: js.Function0[_]): Unit = ???
def xdescribe(name: String, suite: js.Function0[_]): Unit = ???
def xit(title: String, test: js.Function0[_]): Unit = ???
def beforeEach(block: js.Function0[_]): Unit = ???
def afterEach(block: js.Function0[_]): Unit = ???
def expect(exp: js.Any): JasmineExpectation = ???
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
/* __ *\
** ________ ___ / / ___ __ ____ Scala.js Test Suite **
** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL **
** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ **
** /____/\___/_/ |_/____/_/ | |__/ /____/ **
** |/____/ **
\* */
package scala.scalajs.test
package org.scalajs.jasmine

import scala.scalajs.js
import java.util.regex.Pattern
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.scalajs.jasmine

import scala.scalajs.js

trait Result extends js.Object {
def `type`: js.String = ???
val trace: js.Dynamic = ???
}

trait ExpectationResult extends Result {
def passed(): js.Boolean = ???
val message: js.String = ???
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.scalajs.jasmine

import scala.scalajs.js

trait Spec extends js.Object {
def results(): SpecResults = ???
val description: js.String = ???
val suite: Suite = ???
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.scalajs.jasmine

import scala.scalajs.js

trait SpecResults extends js.Object {
def passed(): js.Boolean = ???
def getItems(): js.Array[Result] = ???
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.scalajs.jasmine

import scala.scalajs.js

trait Suite extends js.Object {
def results(): SuiteResults = ???
val description: js.String = ???
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.scalajs.jasmine

import scala.scalajs.js

trait SuiteResults extends js.Object {
val passedCount: js.Number = ???
val failedCount: js.Number = ???
val totalCount: js.Number = ???
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
/* __ *\
** ________ ___ / / ___ __ ____ Scala.js Test Suite **
** ________ ___ / / ___ __ ____ Scala.js Test Framework **
** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL **
** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ **
** /____/\___/_/ |_/____/_/ | |__/ /____/ **
** |/____/ **
\* */


package scala.scalajs.test

import scala.scalajs.js
import java.util.regex.Pattern
import org.scalajs.jasmine.Jasmine
import org.scalajs.jasmine.JasmineExpectation

class ScalaJSTest {
class JasmineTest extends Test {
def describe(name: String)(suite: => Unit): Unit = Jasmine.describe(name, suite _)
def it(title: String)(test: => Unit): Unit = Jasmine.it(title, test _)
def xdescribe(name: String)(suite: => Unit): Unit = Jasmine.xdescribe(name, suite _)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/* __ *\
** ________ ___ / / ___ __ ____ Scala.js Test Framework **
** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL **
** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ **
** /____/\___/_/ |_/____/_/ | |__/ /____/ **
** |/____/ **
\* */


package scala.scalajs.test

import scala.scalajs.js
import scala.scalajs.js.Dynamic.global
import scala.scalajs.js.JavaScriptException

import org.scalajs.jasmine.ExpectationResult
import org.scalajs.jasmine.Result
import org.scalajs.jasmine.Spec
import org.scalajs.jasmine.Suite

object JasmineTestFramework extends TestFramework {

/* Stub-out timer methods used by Jasmine and not provided by Rhino. */
if (!global.setTimeout) {
global.setTimeout = scalaJSStub("setTimeout")
global.clearTimeout = scalaJSStub("clearTimeout")
global.setInterval = scalaJSStub("setInterval")
global.clearInterval = scalaJSStub("clearInterval")
}

def scalaJSStub(name: String): js.Function = { () =>
global.console.log("Stub for " + name + " called")
}

// make sure jasmine is loaded
global.importScripts("jasmine.js")

def runTests(testOutput: TestOutput)(tests: => Unit): Unit = {
val jasmine = global.jasmine
val reporter = new JasmineTestFramework(testOutput)

try {
tests

val jasmineEnv = jasmine.getEnv()
jasmineEnv.addReporter(reporter.asInstanceOf[js.Any])
jasmineEnv.updateInterval = 0
jasmineEnv.execute()
} catch {
case JavaScriptException(exception) =>
val stack = exception.asInstanceOf[js.Dynamic].stack
testOutput.error("Problem executing code in tests: " + exception,
reporter.getScriptStack(stack))
}
}
}

class JasmineTestFramework(testOutput: TestOutput) {
private var currentSuite: Suite = _

def reportRunnerStarting(): Unit = {
testOutput.log.info("")
}

def reportSpecStarting(spec: Spec): Unit = {
if (currentSuite != spec.suite) {
currentSuite = spec.suite
info(currentSuite.description)
}
}

def reportSpecResults(spec: Spec): Unit = {
val results = spec.results()
val description = spec.description

if (results.passed) {
testOutput.succeeded(s" $success $description")
} else {
error(s" $failure $description")

results.getItems foreach displayResult
}
}

def reportSuiteResults(suite: Suite): Unit = {
var results = suite.results()

info("")
val title = "Total for suite " + suite.description
val message =
s"${results.totalCount} specs, ${results.failedCount} failure"

if (results.passedCount != results.totalCount) {
error(title)
errorWithInfoColor(message)
} else {
info(title)
infoWithInfoColor(message)
}
info("")
}

def reportRunnerResults(): Unit = {
// no need to report
}

private def info(str: String) =
testOutput.log.info(str)

private def infoWithInfoColor(str: String) =
info(withColor(testOutput.infoColor, str))

private def errorWithInfoColor(str: String) =
error(withColor(testOutput.infoColor, str))

private def error(msg: js.Any) =
testOutput.log.error(msg.toString)

private def withColor(color: String, message: String) =
testOutput.color(message, color)

private def sanitizeMessage(message: String) = {
val FilePattern = """^(.+?) [^ ]+\.js \(line \d+\)\.*?$""".r
val EvalPattern = """^(.+?) in eval.+\(eval\).+?\(line \d+\).*?$""".r

message match {
case FilePattern(originalMessage) => originalMessage
case EvalPattern(originalMessage) => originalMessage
case message => message
}
}

private def failure = withColor(testOutput.errorColor, "x")
private def success = withColor(testOutput.successColor, "+")

private def displayResult(result: Result) = {
(result.`type`: String) match {
case "log" =>
info(s" ${result.toString}")
case "expect" =>
val r = result.asInstanceOf[ExpectationResult]
if (!r.passed()) {
val message = sanitizeMessage(r.message)
val stack = getScriptStack(r.trace.stack)

if (stack.isEmpty) testOutput.failure(s" $message")
else testOutput.error(s" $message", stack)
}
}
}

private def getScriptStack(stack: js.Any): Array[ScriptStackElement] = {
if (stack.isInstanceOf[js.String]) {
val StackTracePattern = """^(.+?) ([^ ]+\.js):(\d+).*?$""".r
val stackString: String = stack.asInstanceOf[js.String]

stackString
.split("\n")
.map {
case StackTracePattern(originalMessage, fileName, lineNumber) =>
ScriptStackElement(fileName, "", lineNumber.toInt)
case unknown =>
throw JavaScriptException("Unknown stack element: " + unknown)
}
.takeWhile(e => !(e.fileName contains "jasmine.js"))
} else Array.empty
}
}
25 changes: 25 additions & 0 deletions library/src/main/scala/scala/scalajs/test/EventProxy.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/* __ *\
** ________ ___ / / ___ __ ____ Scala.js API **
** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL **
** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ **
** /____/\___/_/ |_/____/_/ | |__/ /____/ **
** |/____/ **
\* */


package scala.scalajs.test

import scala.scalajs.js

trait EventProxy extends js.Object {
def error(message: js.String, stack: js.Array[js.Dictionary]): Unit = ???
def failure(message: js.String, stack: js.Array[js.Dictionary]): Unit = ???
def succeeded(message: js.String): Unit = ???
def skipped(message: js.String): Unit = ???
def pending(message: js.String): Unit = ???
def ignored(message: js.String): Unit = ???
def canceled(message: js.String): Unit = ???

def error(message: js.String): Unit = ???
def info(message: js.String): Unit = ???
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
/* __ *\
** ________ ___ / / ___ __ ____ Scala.js Test Suite **
** ________ ___ / / ___ __ ____ Scala.js API **
** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL **
** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ **
** /____/\___/_/ |_/____/_/ | |__/ /____/ **
** |/____/ **
\* */


package scala.scalajs.test

import scala.scalajs.js

object Jasmine extends js.GlobalScope {
def describe(name: String, suite: js.Function0[_]): Unit = ???
def it(title: String, test: js.Function0[_]): Unit = ???
def xdescribe(name: String, suite: js.Function0[_]): Unit = ???
def xit(title: String, test: js.Function0[_]): Unit = ???
def beforeEach(block: js.Function0[_]): Unit = ???
def afterEach(block: js.Function0[_]): Unit = ???
def expect(exp: js.Any): JasmineExpectation = ???
class ScriptStackElement(
val fileName: String,
val functionName: String,
val lineNumber: Int)

object ScriptStackElement {
def apply(fileName: String, functionName: String, lineNumber: Int) =
new ScriptStackElement(fileName, functionName, lineNumber)
}
13 changes: 13 additions & 0 deletions library/src/main/scala/scala/scalajs/test/Test.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/* __ *\
** ________ ___ / / ___ __ ____ Scala.js API **
** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL **
** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ **
** /____/\___/_/ |_/____/_/ | |__/ /____/ **
** |/____/ **
\* */


package scala.scalajs.test

/** Marker trait for Scala.js tests */
trait Test
Loading

0 comments on commit 82c0ed1

Please sign in to comment.