// Navigation
//
// Navigation methods return an expectation on a part of the value under
// test. The label becomes a path to that part, so a failure shows where the
// value came from: `user.address.city: expect(received).to_equal(expected)`.
//
// Navigation changes the value under test, so it cannot be used after
// `not()`.
///|
/// An expectation on `value`, a part of the value under test. `segment`
/// extends the label: `name` gives `label.name`, `[0]` gives `label[0]`.
fn[T, U] Expectation::navigate(
self : Expectation[T],
value : U,
segment : String,
) -> Expectation[U] {
let label = if self.label.is_empty() {
segment
} else if segment.has_prefix("[") {
self.label + segment
} else {
"\{self.label}.\{segment}"
}
{
actual: value,
negated: false,
label,
reason: self.reason,
scope: self.scope,
source: None,
}
}
///|
/// Apply `f` to the value under test and return an expectation on the
/// result. `name` extends the label, so failures show the path to the value.
/// Cannot be used after `not()`.
#callsite(autofill(loc))
pub fn[T, U] Expectation::get(
self : Expectation[T],
name : String,
f : (T) -> U,
loc~ : SourceLoc,
) -> Expectation[U] raise Error {
self.check_not_negated("get", loc)
self.navigate(f(self.actual), name)
}
///|
/// Assert an Array has an element at `index`, and return an expectation on
/// it. Cannot be used after `not()`.
#callsite(autofill(loc))
pub fn[T : @debug.Debug] Expectation::element(
self : Expectation[Array[T]],
index : Int,
loc~ : SourceLoc,
) -> Expectation[T] raise Error {
self.check_not_negated("element", loc)
if index < 0 || index >= self.actual.length() {
self.report(
"element",
"index",
[
("Expected", "an element at index \{index}"),
("Received", show(self.actual)),
("Length", self.actual.length().to_string()),
],
loc,
)
}
self.navigate(self.actual[index], "[\{index}]")
}
///|
/// Assert an Array is not empty, and return an expectation on its first
/// element. Cannot be used after `not()`.
#callsite(autofill(loc))
pub fn[T : @debug.Debug] Expectation::first(
self : Expectation[Array[T]],
loc~ : SourceLoc,
) -> Expectation[T] raise Error {
self.check_not_negated("first", loc)
match self.actual {
[first, ..] => self.navigate(first, "[0]")
[] =>
self.report(
"first",
"",
[("Expected", "a first element"), ("Received", show(self.actual))],
loc,
)
}
}
///|
/// Assert an Array is not empty, and return an expectation on its last
/// element. Cannot be used after `not()`.
#callsite(autofill(loc))
pub fn[T : @debug.Debug] Expectation::last(
self : Expectation[Array[T]],
loc~ : SourceLoc,
) -> Expectation[T] raise Error {
self.check_not_negated("last", loc)
match self.actual {
[.., last] => self.navigate(last, "[\{self.actual.length() - 1}]")
[] =>
self.report(
"last",
"",
[("Expected", "a last element"), ("Received", show(self.actual))],
loc,
)
}
}
///|
/// Assert an Array has exactly one element, and return an expectation on it.
/// Cannot be used after `not()`.
#callsite(autofill(loc))
pub fn[T : @debug.Debug] Expectation::single(
self : Expectation[Array[T]],
loc~ : SourceLoc,
) -> Expectation[T] raise Error {
self.check_not_negated("single", loc)
match self.actual {
[only] => self.navigate(only, "[0]")
_ =>
self.report(
"single",
"",
[
("Expected", "exactly one element"),
("Received", show(self.actual)),
("Length", self.actual.length().to_string()),
],
loc,
)
}
}
///|
/// Assert a Map contains `key`, and return an expectation on its value.
/// Cannot be used after `not()`.
#callsite(autofill(loc))
pub fn[K : Hash + Eq + @debug.Debug, V : @debug.Debug] Expectation::value_at(
self : Expectation[Map[K, V]],
key : K,
loc~ : SourceLoc,
) -> Expectation[V] raise Error {
self.check_not_negated("value_at", loc)
match self.actual.get(key) {
Some(value) => self.navigate(value, "[\{show(key)}]")
None =>
self.report(
"value_at",
"key",
[
("Expected", "containing key \{show(key)}"),
("Received", show(self.actual)),
],
loc,
)
}
}
///|
/// Return an expectation on the first part of a pair. Cannot be used after
/// `not()`.
#callsite(autofill(loc))
pub fn[A, B] Expectation::fst(
self : Expectation[(A, B)],
loc~ : SourceLoc,
) -> Expectation[A] raise Error {
self.check_not_negated("fst", loc)
self.navigate(self.actual.0, "0")
}
///|
/// Return an expectation on the second part of a pair. Cannot be used after
/// `not()`.
#callsite(autofill(loc))
pub fn[A, B] Expectation::snd(
self : Expectation[(A, B)],
loc~ : SourceLoc,
) -> Expectation[B] raise Error {
self.check_not_negated("snd", loc)
self.navigate(self.actual.1, "1")
}
///|
/// Run several matchers on the same value:
/// `expect(age).all(it => { it.to_be_greater_than(0); it.to_be_less_than(150) })`.
///
/// Matchers return `Unit` and do not chain, because MoonBit does not let a
/// statement ignore a returned value. Use `all` instead. Cannot be used after
/// `not()`.
#callsite(autofill(loc))
pub fn[T] Expectation::all(
self : Expectation[T],
block : (Expectation[T]) -> Unit raise Error,
loc~ : SourceLoc,
) -> Unit raise Error {
self.check_not_negated("all", loc)
block(self)
}