Scala.js version of the sbt testing interface that provides a standardized API for test frameworks to integrate with SBT and run tests in a Scala.js (JavaScript) environment
—
Event system for reporting test results, progress, and metadata with support for different test selection patterns and structured error reporting.
Core interface for test events fired during test execution.
/**
* An event fired by the test framework during a run.
* Contains complete information about test execution results.
*/
trait Event {
/**
* The fully qualified name of a class that can rerun the suite or test.
* @return class name for rerunning this test/suite
*/
def fullyQualifiedName(): String
/**
* The fingerprint of the test class whose fully qualified name is returned.
* If fingerprint.isModule indicates module, fullyQualifiedName excludes trailing $.
* @return fingerprint used to identify the test class
*/
def fingerprint(): Fingerprint
/**
* Additional information identifying the suite or test about which an event was fired.
* @return selector providing specific test/suite identification
*/
def selector(): Selector
/**
* Indicates the test result: success, failure, error, skipped, ignored, canceled, pending.
* @return status representing the test outcome
*/
def status(): Status
/**
* An OptionalThrowable associated with this Event.
* Contains exception information for failures and errors.
* @return optional throwable with error details
*/
def throwable(): OptionalThrowable
/**
* Execution time in milliseconds, or -1 if no duration available.
* @return duration in milliseconds or -1
*/
def duration(): Long
}Usage Examples:
// Create success event for test method
class MyEvent(
className: String,
fp: Fingerprint,
sel: Selector,
stat: Status,
opt: OptionalThrowable,
dur: Long
) extends Event {
def fullyQualifiedName() = className
def fingerprint() = fp
def selector() = sel
def status() = stat
def throwable() = opt
def duration() = dur
}
// Create events for different outcomes
val successEvent = new MyEvent(
className = "com.example.MathTest",
fp = testFingerprint,
sel = new TestSelector("shouldAddNumbers"),
stat = Status.Success,
opt = new OptionalThrowable(), // empty
dur = 150L
)
val failureEvent = new MyEvent(
className = "com.example.MathTest",
fp = testFingerprint,
sel = new TestSelector("shouldDivideByZero"),
stat = Status.Failure,
opt = new OptionalThrowable(new AssertionError("Division by zero should throw")),
dur = 75L
)
val suiteEvent = new MyEvent(
className = "com.example.MathTest",
fp = testFingerprint,
sel = new SuiteSelector(),
stat = Status.Success,
opt = new OptionalThrowable(),
dur = 1200L
)Interface for handling events fired by test frameworks during execution.
/**
* Interface implemented by clients that handle events fired by test frameworks.
* An event handler is passed to test framework via Task.execute method.
*/
trait EventHandler {
/**
* Handle an event fired by the test framework.
* @param event the event to handle
*/
def handle(event: Event): Unit
}Usage Examples:
// Simple logging event handler
class LoggingEventHandler(logger: Logger) extends EventHandler {
def handle(event: Event): Unit = {
val testName = event.selector() match {
case suite: SuiteSelector => s"Suite: ${event.fullyQualifiedName()}"
case test: TestSelector => s"Test: ${event.fullyQualifiedName()}.${test.testName()}"
case nested: NestedTestSelector =>
s"Nested: ${event.fullyQualifiedName()}.${nested.suiteId()}.${nested.testName()}"
case _ => s"Unknown: ${event.fullyQualifiedName()}"
}
event.status() match {
case Status.Success =>
logger.info(s"✓ $testName (${event.duration()}ms)")
case Status.Failure =>
logger.error(s"✗ $testName (${event.duration()}ms)")
if (event.throwable().isDefined()) {
logger.trace(event.throwable().get())
}
case Status.Error =>
logger.error(s"⚠ $testName - ERROR (${event.duration()}ms)")
if (event.throwable().isDefined()) {
logger.trace(event.throwable().get())
}
case Status.Skipped =>
logger.warn(s"⚬ $testName - SKIPPED")
case Status.Ignored =>
logger.warn(s"⚬ $testName - IGNORED")
case Status.Canceled =>
logger.warn(s"⚬ $testName - CANCELED")
case Status.Pending =>
logger.warn(s"⚬ $testName - PENDING")
}
}
}
// Aggregating event handler
class TestResultCollector extends EventHandler {
private val results = mutable.Map[String, TestResult]()
def handle(event: Event): Unit = {
val key = s"${event.fullyQualifiedName()}.${selectorKey(event.selector())}"
results(key) = TestResult(
className = event.fullyQualifiedName(),
selector = event.selector(),
status = event.status(),
duration = event.duration(),
throwable = if (event.throwable().isDefined()) Some(event.throwable().get()) else None
)
}
def getResults(): Map[String, TestResult] = results.toMap
def getSummary(): TestSummary = {
val allResults = results.values.toSeq
TestSummary(
total = allResults.size,
passed = allResults.count(_.status == Status.Success),
failed = allResults.count(_.status == Status.Failure),
errors = allResults.count(_.status == Status.Error),
skipped = allResults.count(r =>
r.status == Status.Skipped || r.status == Status.Ignored || r.status == Status.Canceled),
pending = allResults.count(_.status == Status.Pending),
duration = allResults.map(_.duration).sum
)
}
}Represents the outcome of test execution with comprehensive status types.
/**
* Represents the status of running a test.
* Test frameworks can decide which statuses to use and their meanings.
*/
class Status private (name: String, ordinal: Int) extends Enum[Status](name, ordinal)
object Status {
/** Indicates a test succeeded. */
final val Success: Status
/** Indicates an "error" occurred during test execution. */
final val Error: Status
/** Indicates a "failure" occurred during test execution. */
final val Failure: Status
/** Indicates a test was skipped for any reason. */
final val Skipped: Status
/** Indicates a test was ignored (temporarily disabled with intention to fix). */
final val Ignored: Status
/**
* Indicates a test was canceled (unable to complete due to unmet precondition,
* such as database being offline).
*/
final val Canceled: Status
/**
* Indicates a test was declared as pending (with test code and/or
* production code as yet unimplemented).
*/
final val Pending: Status
/** Returns array of all status values. */
def values(): Array[Status]
/**
* Returns status by name.
* @param name status name
* @return Status instance
* @throws IllegalArgumentException if name not found
*/
def valueOf(name: String): Status
}Usage Examples:
// Check status types
def handleTestResult(event: Event): Unit = {
event.status() match {
case Status.Success =>
println("Test passed!")
case Status.Failure =>
println(s"Test failed: ${event.throwable().get().getMessage}")
case Status.Error =>
println(s"Test error: ${event.throwable().get().getMessage}")
case Status.Skipped | Status.Ignored | Status.Canceled =>
println("Test was not executed")
case Status.Pending =>
println("Test is pending implementation")
}
}
// Create events with different statuses
val statuses = Status.values()
println(s"Available statuses: ${statuses.map(_.name).mkString(", ")}")
// Status by name lookup
val failureStatus = Status.valueOf("Failure")
assert(failureStatus == Status.Failure)Container for optional exception information associated with test events.
/**
* An optional Throwable container.
* Used to carry exception information with test events.
*/
final class OptionalThrowable(exception: Throwable) extends Serializable {
/** Creates empty OptionalThrowable with no exception. */
def this()
/**
* Indicates whether this OptionalThrowable contains a Throwable.
* @return true if contains a Throwable
*/
def isDefined(): Boolean
/**
* Indicates whether this OptionalThrowable is empty.
* @return true if contains no Throwable
*/
def isEmpty(): Boolean
/**
* Returns the contained Throwable if defined.
* @return contained Throwable
* @throws IllegalStateException if not defined
*/
def get(): Throwable
}Usage Examples:
// Create OptionalThrowable with exception
val withException = new OptionalThrowable(new RuntimeException("Test failed"))
if (withException.isDefined()) {
println(s"Exception: ${withException.get().getMessage}")
}
// Create empty OptionalThrowable
val empty = new OptionalThrowable()
assert(empty.isEmpty())
// Safe exception handling
def safeGetException(opt: OptionalThrowable): Option[Throwable] = {
if (opt.isDefined()) Some(opt.get()) else None
}
// Create events with optional exceptions
val successEvent = createEvent(Status.Success, new OptionalThrowable())
val failureEvent = createEvent(
Status.Failure,
new OptionalThrowable(new AssertionError("Expected 5 but was 3"))
)Tasks should create and fire events for all test outcomes:
class MyTask(taskDef: TaskDef) extends Task {
def execute(eventHandler: EventHandler, loggers: Array[Logger]): Array[Task] = {
val startTime = System.currentTimeMillis()
try {
// Execute test logic
val result = runTest(taskDef.fullyQualifiedName(), taskDef.selectors())
val duration = System.currentTimeMillis() - startTime
result match {
case TestSuccess =>
val event = new MyEvent(
taskDef.fullyQualifiedName(),
taskDef.fingerprint(),
new SuiteSelector(),
Status.Success,
new OptionalThrowable(),
duration
)
eventHandler.handle(event)
case TestFailure(assertion) =>
val event = new MyEvent(
taskDef.fullyQualifiedName(),
taskDef.fingerprint(),
new SuiteSelector(),
Status.Failure,
new OptionalThrowable(assertion),
duration
)
eventHandler.handle(event)
}
Array.empty // No nested tasks
} catch {
case t: Throwable =>
val duration = System.currentTimeMillis() - startTime
val errorEvent = new MyEvent(
taskDef.fullyQualifiedName(),
taskDef.fingerprint(),
new SuiteSelector(),
Status.Error,
new OptionalThrowable(t),
duration
)
eventHandler.handle(errorEvent)
Array.empty
}
}
}Different selector types should fire appropriate events:
def fireEventsForSelectors(
selectors: Array[Selector],
className: String,
fingerprint: Fingerprint,
eventHandler: EventHandler
): Unit = {
selectors.foreach {
case _: SuiteSelector =>
// Fire suite-level event
val event = createSuiteEvent(className, fingerprint, Status.Success)
eventHandler.handle(event)
case testSel: TestSelector =>
// Fire individual test event
val event = createTestEvent(className, fingerprint, testSel, Status.Success)
eventHandler.handle(event)
case nestedSel: NestedTestSelector =>
// Fire nested test event
val event = createNestedTestEvent(className, fingerprint, nestedSel, Status.Success)
eventHandler.handle(event)
case wildcardSel: TestWildcardSelector =>
// Fire events for matching tests
val matchingTests = findTestsMatching(wildcardSel.testWildcard())
matchingTests.foreach { testName =>
val testSelector = new TestSelector(testName)
val event = createTestEvent(className, fingerprint, testSelector, Status.Success)
eventHandler.handle(event)
}
}
}Events should include detailed exception information for failures and errors:
def createFailureEvent(
className: String,
fingerprint: Fingerprint,
selector: Selector,
exception: Throwable,
duration: Long
): Event = {
val status = exception match {
case _: AssertionError => Status.Failure
case _: Exception => Status.Error
case _ => Status.Error
}
new MyEvent(
className,
fingerprint,
selector,
status,
new OptionalThrowable(exception),
duration
)
}Event handlers should handle processing errors gracefully:
class RobustEventHandler extends EventHandler {
def handle(event: Event): Unit = {
try {
processEvent(event)
} catch {
case t: Throwable =>
// Log error but don't propagate to avoid breaking test execution
System.err.println(s"Event handler error: ${t.getMessage}")
t.printStackTrace()
}
}
private def processEvent(event: Event): Unit = {
// Event processing logic that might throw
}
}Install with Tessl CLI
npx tessl i tessl/maven-org-scala-js--scalajs-test-interface