Skip to main content

ferridriver_expect/
value.rs

1//! Synchronous value matchers (Jest-style).
2
3use std::panic::Location;
4
5use regex::Regex;
6use serde_json::Value;
7
8use crate::asymmetric::{deep_equal, float_bit_eq, json_short, match_object};
9use crate::diff::json_diff;
10use crate::{AssertionFailure, CallerLocation};
11
12/// A match that supports both exact string equality and regex.
13///
14/// Note: web-first matchers in `ferridriver-test` use this with
15/// *exact-equality* semantics for the `String` variant (matching
16/// Playwright). The Jest-style [`ExpectValue::to_match`] downgrades the
17/// `String` variant to *substring containment* explicitly.
18#[derive(Debug, Clone)]
19pub enum StringOrRegex {
20  String(String),
21  Regex(Regex),
22}
23
24impl StringOrRegex {
25  pub fn matches(&self, actual: &str) -> bool {
26    match self {
27      Self::String(expected) => actual == expected,
28      Self::Regex(re) => re.is_match(actual),
29    }
30  }
31
32  pub fn description(&self) -> String {
33    match self {
34      Self::String(s) => format!("\"{s}\""),
35      Self::Regex(re) => format!("/{}/", re.as_str()),
36    }
37  }
38}
39
40impl From<&str> for StringOrRegex {
41  fn from(s: &str) -> Self {
42    Self::String(s.to_string())
43  }
44}
45
46impl From<String> for StringOrRegex {
47  fn from(s: String) -> Self {
48    Self::String(s)
49  }
50}
51
52impl From<Regex> for StringOrRegex {
53  fn from(re: Regex) -> Self {
54    Self::Regex(re)
55  }
56}
57
58// ── ExpectValue ──────────────────────────────────────────────────────
59
60pub struct ExpectValue {
61  actual: Value,
62  is_not: bool,
63  is_soft: bool,
64  message: Option<String>,
65}
66
67/// Wrap a value for synchronous assertion.
68#[must_use]
69pub fn expect_value(actual: Value) -> ExpectValue {
70  ExpectValue {
71    actual,
72    is_not: false,
73    is_soft: false,
74    message: None,
75  }
76}
77
78impl ExpectValue {
79  #[must_use]
80  pub fn not(mut self) -> Self {
81    self.is_not = !self.is_not;
82    self
83  }
84
85  #[must_use]
86  pub fn soft(mut self) -> Self {
87    self.is_soft = true;
88    self
89  }
90
91  #[must_use]
92  pub fn with_message(mut self, message: impl Into<String>) -> Self {
93    self.message = Some(message.into());
94    self
95  }
96
97  pub fn is_soft(&self) -> bool {
98    self.is_soft
99  }
100
101  pub fn actual(&self) -> &Value {
102    &self.actual
103  }
104
105  fn fail(
106    &self,
107    method: &str,
108    expected: impl Into<String>,
109    received: impl Into<String>,
110    rich_diff: Option<String>,
111    location: Option<&'static Location<'static>>,
112  ) -> AssertionFailure {
113    let expected = expected.into();
114    let received = received.into();
115    let not = if self.is_not { ".not" } else { "" };
116    let prefix = self.message.as_ref().map(|m| format!("{m}: ")).unwrap_or_default();
117    // Two-field split:
118    //   `message` = a single-line title that a reporter can highlight
119    //               on its own (`expect(value).toEqual() failed`).
120    //   `diff`    = the full body (Expected/Received + optional unified
121    //               diff) — printed below the title.
122    // JS-throw concatenates the two; reporters print them in sequence.
123    let message = format!("{prefix}expect(value){not}.{method}() failed");
124    let summary_diff = format!("Expected: {expected}\nReceived: {received}");
125    let body = match rich_diff {
126      Some(d) => format!("{summary_diff}\n\nDiff:\n{d}"),
127      None => summary_diff,
128    };
129    let mut failure = AssertionFailure::new(message, Some(body));
130    if let Some(loc) = location {
131      failure = failure.with_location(CallerLocation::from_std(loc));
132    }
133    failure
134  }
135
136  #[track_caller]
137  fn check(
138    &self,
139    pass: bool,
140    method: &str,
141    expected: impl Into<String>,
142    received: impl Into<String>,
143  ) -> Result<(), AssertionFailure> {
144    let pass = if self.is_not { !pass } else { pass };
145    if pass {
146      Ok(())
147    } else {
148      Err(self.fail(method, expected, received, None, Some(Location::caller())))
149    }
150  }
151
152  #[track_caller]
153  fn check_with_diff(
154    &self,
155    pass: bool,
156    method: &str,
157    expected: impl Into<String>,
158    received: impl Into<String>,
159    diff: String,
160  ) -> Result<(), AssertionFailure> {
161    let pass = if self.is_not { !pass } else { pass };
162    if pass {
163      Ok(())
164    } else {
165      Err(self.fail(method, expected, received, Some(diff), Some(Location::caller())))
166    }
167  }
168
169  // ── primitive equality ──────────────────────────────────────────
170
171  /// `toBe(expected)` — strict-equality of primitives. Object/array
172  /// values are rejected with guidance to use `toEqual`.
173  #[track_caller]
174  pub fn to_be(&self, expected: &Value) -> Result<(), AssertionFailure> {
175    if self.actual.is_object() || self.actual.is_array() || expected.is_object() || expected.is_array() {
176      return Err(self.fail(
177        "toBe",
178        format!("primitive equal to {}", json_short(expected)),
179        format!("{} (use toEqual for objects/arrays)", json_short(&self.actual)),
180        None,
181        Some(Location::caller()),
182      ));
183    }
184    let pass = deep_equal(&self.actual, expected);
185    self.check(pass, "toBe", json_short(expected), json_short(&self.actual))
186  }
187
188  /// `toEqual(expected)` — recursive equality. Honours asymmetric
189  /// matchers embedded in `expected`.
190  #[track_caller]
191  pub fn to_equal(&self, expected: &Value) -> Result<(), AssertionFailure> {
192    let pass = deep_equal(&self.actual, expected);
193    let diff = if pass {
194      None
195    } else {
196      Some(json_diff(expected, &self.actual))
197    };
198    match diff {
199      Some(d) => self.check_with_diff(pass, "toEqual", json_short(expected), json_short(&self.actual), d),
200      None => self.check(pass, "toEqual", json_short(expected), json_short(&self.actual)),
201    }
202  }
203
204  /// `toStrictEqual(expected)` — alias of [`Self::to_equal`] for
205  /// `serde_json::Value` (no `undefined` to differentiate).
206  #[track_caller]
207  pub fn to_strict_equal(&self, expected: &Value) -> Result<(), AssertionFailure> {
208    let pass = deep_equal(&self.actual, expected);
209    let diff = if pass {
210      None
211    } else {
212      Some(json_diff(expected, &self.actual))
213    };
214    match diff {
215      Some(d) => self.check_with_diff(pass, "toStrictEqual", json_short(expected), json_short(&self.actual), d),
216      None => self.check(pass, "toStrictEqual", json_short(expected), json_short(&self.actual)),
217    }
218  }
219
220  // ── nullishness ──────────────────────────────────────────────────
221
222  #[track_caller]
223  pub fn to_be_null(&self) -> Result<(), AssertionFailure> {
224    self.check(self.actual.is_null(), "toBeNull", "null", json_short(&self.actual))
225  }
226
227  /// `toBeUndefined` is satisfied by `null` because `serde_json` cannot
228  /// distinguish — the QuickJS binding emits both as `Value::Null`.
229  #[track_caller]
230  pub fn to_be_undefined(&self) -> Result<(), AssertionFailure> {
231    self.check(
232      self.actual.is_null(),
233      "toBeUndefined",
234      "undefined",
235      json_short(&self.actual),
236    )
237  }
238
239  #[track_caller]
240  pub fn to_be_defined(&self) -> Result<(), AssertionFailure> {
241    self.check(
242      !self.actual.is_null(),
243      "toBeDefined",
244      "defined value",
245      json_short(&self.actual),
246    )
247  }
248
249  // ── truthiness ───────────────────────────────────────────────────
250
251  #[track_caller]
252  pub fn to_be_truthy(&self) -> Result<(), AssertionFailure> {
253    self.check(
254      is_truthy(&self.actual),
255      "toBeTruthy",
256      "truthy",
257      json_short(&self.actual),
258    )
259  }
260
261  #[track_caller]
262  pub fn to_be_falsy(&self) -> Result<(), AssertionFailure> {
263    self.check(!is_truthy(&self.actual), "toBeFalsy", "falsy", json_short(&self.actual))
264  }
265
266  // ── numeric ──────────────────────────────────────────────────────
267
268  #[track_caller]
269  pub fn to_be_nan(&self) -> Result<(), AssertionFailure> {
270    let pass = self.actual.as_f64().is_some_and(f64::is_nan);
271    self.check(pass, "toBeNaN", "NaN", json_short(&self.actual))
272  }
273
274  #[track_caller]
275  pub fn to_be_close_to(&self, expected: f64, digits: Option<u8>) -> Result<(), AssertionFailure> {
276    let digits = digits.unwrap_or(2);
277    let actual = self.actual.as_f64().ok_or_else(|| {
278      self.fail(
279        "toBeCloseTo",
280        format!("number close to {expected}"),
281        json_short(&self.actual),
282        None,
283        Some(Location::caller()),
284      )
285    })?;
286    let pass = close_enough_within(actual, expected, digits);
287    self.check(
288      pass,
289      "toBeCloseTo",
290      format!("{expected} (±{digits} decimal places)"),
291      format!("{actual}"),
292    )
293  }
294
295  #[track_caller]
296  pub fn to_be_greater_than(&self, expected: f64) -> Result<(), AssertionFailure> {
297    let actual = self.numeric_or_fail("toBeGreaterThan", expected)?;
298    self.check(
299      actual > expected,
300      "toBeGreaterThan",
301      format!("> {expected}"),
302      format!("{actual}"),
303    )
304  }
305
306  #[track_caller]
307  pub fn to_be_greater_than_or_equal(&self, expected: f64) -> Result<(), AssertionFailure> {
308    let actual = self.numeric_or_fail("toBeGreaterThanOrEqual", expected)?;
309    self.check(
310      actual >= expected,
311      "toBeGreaterThanOrEqual",
312      format!(">= {expected}"),
313      format!("{actual}"),
314    )
315  }
316
317  #[track_caller]
318  pub fn to_be_less_than(&self, expected: f64) -> Result<(), AssertionFailure> {
319    let actual = self.numeric_or_fail("toBeLessThan", expected)?;
320    self.check(
321      actual < expected,
322      "toBeLessThan",
323      format!("< {expected}"),
324      format!("{actual}"),
325    )
326  }
327
328  pub fn to_be_less_than_or_equal(&self, expected: f64) -> Result<(), AssertionFailure> {
329    let actual = self.numeric_or_fail("toBeLessThanOrEqual", expected)?;
330    self.check(
331      actual <= expected,
332      "toBeLessThanOrEqual",
333      format!("<= {expected}"),
334      format!("{actual}"),
335    )
336  }
337
338  #[track_caller]
339  fn numeric_or_fail(&self, method: &str, expected: f64) -> Result<f64, AssertionFailure> {
340    let loc = Location::caller();
341    self.actual.as_f64().ok_or_else(|| {
342      self.fail(
343        method,
344        format!("{expected}"),
345        format!("non-numeric {}", json_short(&self.actual)),
346        None,
347        Some(loc),
348      )
349    })
350  }
351
352  // ── containment ──────────────────────────────────────────────────
353
354  /// `toContain` — primitive membership in array, substring in string.
355  #[track_caller]
356  pub fn to_contain(&self, expected: &Value) -> Result<(), AssertionFailure> {
357    let pass = match (&self.actual, expected) {
358      (Value::Array(arr), exp) => arr.iter().any(|v| primitive_strict_equal(v, exp)),
359      (Value::String(s), Value::String(needle)) => s.contains(needle.as_str()),
360      _ => false,
361    };
362    self.check(
363      pass,
364      "toContain",
365      format!("containing {}", json_short(expected)),
366      json_short(&self.actual),
367    )
368  }
369
370  /// Deep `toContain` — every element compared by `deep_equal`.
371  #[track_caller]
372  pub fn to_contain_equal(&self, expected: &Value) -> Result<(), AssertionFailure> {
373    let pass = match &self.actual {
374      Value::Array(arr) => arr.iter().any(|v| deep_equal(v, expected)),
375      _ => false,
376    };
377    if pass {
378      self.check(
379        pass,
380        "toContainEqual",
381        format!("containing equal {}", json_short(expected)),
382        json_short(&self.actual),
383      )
384    } else {
385      let diff = json_diff(expected, &self.actual);
386      self.check_with_diff(
387        pass,
388        "toContainEqual",
389        format!("containing equal {}", json_short(expected)),
390        json_short(&self.actual),
391        diff,
392      )
393    }
394  }
395
396  #[track_caller]
397  pub fn to_have_length(&self, expected: usize) -> Result<(), AssertionFailure> {
398    let actual_len = match &self.actual {
399      Value::Array(a) => Some(a.len()),
400      Value::String(s) => Some(s.chars().count()),
401      _ => None,
402    };
403    match actual_len {
404      Some(len) => self.check(
405        len == expected,
406        "toHaveLength",
407        format!("length {expected}"),
408        format!("length {len}"),
409      ),
410      None => Err(self.fail(
411        "toHaveLength",
412        format!("length {expected}"),
413        format!("value without .length: {}", json_short(&self.actual)),
414        None,
415        Some(Location::caller()),
416      )),
417    }
418  }
419
420  /// `toHaveProperty(path, value?)` — `path` may be `"a.b.c"` or a
421  /// JSON array of keys / indexes.
422  #[track_caller]
423  pub fn to_have_property(&self, path: &Value, expected: Option<&Value>) -> Result<(), AssertionFailure> {
424    let loc = Location::caller();
425    let segments =
426      parse_property_path(path).map_err(|e| self.fail("toHaveProperty", e, json_short(path), None, Some(loc)))?;
427    let descended = descend(&self.actual, &segments);
428    let pass = match (descended, expected) {
429      (Some(_), None) => true,
430      (Some(val), Some(exp)) => deep_equal(val, exp),
431      (None, _) => false,
432    };
433    let desc = format!(
434      "property {} {}",
435      path_describe(&segments),
436      expected.map(|v| format!("= {}", json_short(v))).unwrap_or_default()
437    );
438    let received = match descend(&self.actual, &segments) {
439      Some(v) => format!("= {}", json_short(v)),
440      None => "(missing)".to_string(),
441    };
442    self.check(pass, "toHaveProperty", desc, received)
443  }
444
445  /// Jest's `toMatch(string)` is substring containment; `toMatch(/re/)`
446  /// is regex. Differs from [`StringOrRegex::matches`] (which is
447  /// exact-equality for the string variant).
448  #[track_caller]
449  pub fn to_match(&self, pattern: &StringOrRegex) -> Result<(), AssertionFailure> {
450    let actual = match self.actual.as_str() {
451      Some(s) => s,
452      None => {
453        return Err(self.fail(
454          "toMatch",
455          format!("matching {}", pattern.description()),
456          format!("non-string {}", json_short(&self.actual)),
457          None,
458          Some(Location::caller()),
459        ));
460      },
461    };
462    let pass = match pattern {
463      StringOrRegex::String(needle) => actual.contains(needle.as_str()),
464      StringOrRegex::Regex(re) => re.is_match(actual),
465    };
466    self.check(pass, "toMatch", pattern.description(), format!("{actual:?}"))
467  }
468
469  #[track_caller]
470  pub fn to_match_object(&self, subset: &Value) -> Result<(), AssertionFailure> {
471    let pass = match_object(&self.actual, subset);
472    if pass {
473      self.check(pass, "toMatchObject", json_short(subset), json_short(&self.actual))
474    } else {
475      let diff = json_diff(subset, &self.actual);
476      self.check_with_diff(
477        pass,
478        "toMatchObject",
479        json_short(subset),
480        json_short(&self.actual),
481        diff,
482      )
483    }
484  }
485
486  /// `toBeInstanceOf(ctorName)` — checked against the binding-supplied
487  /// constructor name (`Class.name`). When `actual_ctor_name` is
488  /// `None`, falls back to the inferred built-in (e.g. `Array` for an
489  /// array value).
490  #[track_caller]
491  pub fn to_be_instance_of(&self, ctor_name: &str, actual_ctor_name: Option<&str>) -> Result<(), AssertionFailure> {
492    let actual_name = actual_ctor_name.unwrap_or_else(|| infer_builtin_ctor(&self.actual));
493    let pass = actual_name == ctor_name;
494    self.check(
495      pass,
496      "toBeInstanceOf",
497      format!("instance of {ctor_name}"),
498      format!("instance of {actual_name}"),
499    )
500  }
501}
502
503fn close_enough_within(a: f64, b: f64, digits: u8) -> bool {
504  if !a.is_finite() || !b.is_finite() {
505    return float_bit_eq(a, b);
506  }
507  let tol = 10f64.powi(-i32::from(digits)) / 2.0;
508  (a - b).abs() < tol
509}
510
511fn is_truthy(v: &Value) -> bool {
512  match v {
513    Value::Null => false,
514    Value::Bool(b) => *b,
515    Value::Number(n) => n
516      .as_f64()
517      .is_some_and(|f| !float_bit_eq(f, 0.0) && !float_bit_eq(f, -0.0) && !f.is_nan()),
518    Value::String(s) => !s.is_empty(),
519    Value::Array(_) | Value::Object(_) => true,
520  }
521}
522
523fn primitive_strict_equal(a: &Value, b: &Value) -> bool {
524  match (a, b) {
525    (Value::Null, Value::Null) => true,
526    (Value::Bool(x), Value::Bool(y)) => x == y,
527    (Value::Number(x), Value::Number(y)) => match (x.as_f64(), y.as_f64()) {
528      (Some(xf), Some(yf)) => float_bit_eq(xf, yf),
529      _ => false,
530    },
531    (Value::String(x), Value::String(y)) => x == y,
532    _ => deep_equal(a, b),
533  }
534}
535
536#[derive(Debug, Clone)]
537enum PropSegment {
538  Key(String),
539  Index(usize),
540}
541
542fn parse_property_path(path: &Value) -> Result<Vec<PropSegment>, String> {
543  match path {
544    Value::String(s) => Ok(s.split('.').map(|seg| PropSegment::Key(seg.to_string())).collect()),
545    Value::Array(arr) => arr
546      .iter()
547      .map(|seg| match seg {
548        Value::String(s) => Ok(PropSegment::Key(s.clone())),
549        Value::Number(n) => n
550          .as_u64()
551          .map(|i| PropSegment::Index(i as usize))
552          .ok_or_else(|| "property path index must be a non-negative integer".to_string()),
553        other => Err(format!(
554          "property path segment must be string or integer; got {}",
555          json_short(other)
556        )),
557      })
558      .collect(),
559    _ => Err("property path must be a string or array".into()),
560  }
561}
562
563fn descend<'a>(v: &'a Value, segments: &[PropSegment]) -> Option<&'a Value> {
564  let mut cur = v;
565  for seg in segments {
566    cur = match (cur, seg) {
567      (Value::Object(map), PropSegment::Key(k)) => map.get(k)?,
568      (Value::Array(arr), PropSegment::Index(i)) => arr.get(*i)?,
569      (Value::Array(arr), PropSegment::Key(k)) => arr.get(k.parse::<usize>().ok()?)?,
570      _ => return None,
571    };
572  }
573  Some(cur)
574}
575
576fn path_describe(segments: &[PropSegment]) -> String {
577  let parts: Vec<String> = segments
578    .iter()
579    .map(|s| match s {
580      PropSegment::Key(k) => k.clone(),
581      PropSegment::Index(i) => format!("[{i}]"),
582    })
583    .collect();
584  parts.join(".")
585}
586
587fn infer_builtin_ctor(v: &Value) -> &'static str {
588  match v {
589    Value::Null => "Null",
590    Value::Bool(_) => "Boolean",
591    Value::Number(_) => "Number",
592    Value::String(_) => "String",
593    Value::Array(_) => "Array",
594    Value::Object(_) => "Object",
595  }
596}
597
598#[cfg(test)]
599mod tests {
600  use super::*;
601  use crate::asymmetric::ASYM_TAG_KEY;
602  use serde_json::json;
603
604  fn ok(r: Result<(), AssertionFailure>) {
605    if let Err(e) = r {
606      panic!("expected ok, got: {}", e.message);
607    }
608  }
609  fn err(r: Result<(), AssertionFailure>) {
610    assert!(r.is_err(), "expected err");
611  }
612
613  #[test]
614  fn to_be_primitive_only() {
615    ok(expect_value(json!(1)).to_be(&json!(1)));
616    err(expect_value(json!(1)).to_be(&json!(2)));
617    err(expect_value(json!([1])).to_be(&json!([1])));
618  }
619
620  #[test]
621  fn to_equal_failure_carries_diff_and_location() {
622    let actual = json!({"id": 1, "name": "Alice", "tags": ["admin", "user"]});
623    let expected = json!({"id": 2, "name": "Alice", "tags": ["admin"]});
624    let err = expect_value(actual)
625      .to_equal(&expected)
626      .expect_err("toEqual should fail");
627    // Diff is multi-line + has +/- markers.
628    let diff = err.diff.as_deref().unwrap_or("");
629    assert!(diff.contains('-'), "diff missing '-' line: {diff}");
630    assert!(diff.contains('+'), "diff missing '+' line: {diff}");
631    assert!(diff.contains("\"id\""), "diff lacks pretty-JSON key context: {diff}");
632    // Location captured.
633    let loc = err.location.expect("location captured");
634    assert!(loc.file.contains("value.rs"), "location file: {}", loc.file);
635    assert!(loc.line > 0);
636  }
637
638  #[test]
639  fn to_equal_recurses() {
640    ok(expect_value(json!({"a": [1, 2]})).to_equal(&json!({"a": [1, 2]})));
641    err(expect_value(json!({"a": [1, 2]})).to_equal(&json!({"a": [1, 3]})));
642  }
643
644  #[test]
645  fn to_equal_with_asymmetric() {
646    let exp = json!({"id": {ASYM_TAG_KEY: "any", "name": "Number"}});
647    ok(expect_value(json!({"id": 7})).to_equal(&exp));
648    err(expect_value(json!({"id": "x"})).to_equal(&exp));
649  }
650
651  #[test]
652  fn not_inverts() {
653    ok(expect_value(json!(1)).not().to_be(&json!(2)));
654    err(expect_value(json!(1)).not().to_be(&json!(1)));
655  }
656
657  #[test]
658  fn to_contain_array_and_string() {
659    ok(expect_value(json!([1, 2, 3])).to_contain(&json!(2)));
660    err(expect_value(json!([1, 2, 3])).to_contain(&json!(4)));
661    ok(expect_value(json!("hello world")).to_contain(&json!("world")));
662  }
663
664  #[test]
665  fn to_contain_equal_deep() {
666    ok(expect_value(json!([{"id": 1}, {"id": 2}])).to_contain_equal(&json!({"id": 2})));
667    err(expect_value(json!([{"id": 1}, {"id": 2}])).to_contain_equal(&json!({"id": 3})));
668  }
669
670  #[test]
671  fn to_have_length_works() {
672    ok(expect_value(json!([1, 2, 3])).to_have_length(3));
673    ok(expect_value(json!("abcd")).to_have_length(4));
674    err(expect_value(json!(42)).to_have_length(1));
675  }
676
677  #[test]
678  fn to_have_property_dot_path() {
679    ok(expect_value(json!({"a": {"b": 1}})).to_have_property(&json!("a.b"), None));
680    ok(expect_value(json!({"a": {"b": 1}})).to_have_property(&json!("a.b"), Some(&json!(1))));
681    err(expect_value(json!({"a": {"b": 1}})).to_have_property(&json!("a.c"), None));
682  }
683
684  #[test]
685  fn to_have_property_array_path_with_index() {
686    ok(expect_value(json!({"arr": [10, 20]})).to_have_property(&json!(["arr", 1]), Some(&json!(20))));
687  }
688
689  #[test]
690  fn to_match_string_substring() {
691    ok(expect_value(json!("hello")).to_match(&StringOrRegex::String("ello".into())));
692    ok(expect_value(json!("hello")).to_match(&StringOrRegex::Regex(Regex::new("^h.+o$").unwrap())));
693    err(expect_value(json!("hello")).to_match(&StringOrRegex::String("bye".into())));
694  }
695
696  #[test]
697  fn to_match_object_subset() {
698    ok(expect_value(json!({"a": 1, "b": 2})).to_match_object(&json!({"a": 1})));
699    err(expect_value(json!({"a": 1, "b": 2})).to_match_object(&json!({"a": 2})));
700  }
701
702  #[test]
703  fn close_to_default_two_digits() {
704    ok(expect_value(json!(0.1 + 0.2)).to_be_close_to(0.3, None));
705    err(expect_value(json!(0.1 + 0.2)).to_be_close_to(0.4, None));
706  }
707
708  #[test]
709  fn truthy_and_falsy() {
710    ok(expect_value(json!(1)).to_be_truthy());
711    ok(expect_value(json!("")).to_be_falsy());
712    ok(expect_value(json!(0)).to_be_falsy());
713    ok(expect_value(Value::Null).to_be_falsy());
714  }
715
716  #[test]
717  fn to_be_instance_of_builtins() {
718    ok(expect_value(json!([1])).to_be_instance_of("Array", None));
719    ok(expect_value(json!("x")).to_be_instance_of("String", None));
720    err(expect_value(json!(1)).to_be_instance_of("String", None));
721  }
722
723  #[test]
724  fn greater_less_than() {
725    ok(expect_value(json!(5)).to_be_greater_than(3.0));
726    err(expect_value(json!(3)).to_be_greater_than(3.0));
727    ok(expect_value(json!(3)).to_be_greater_than_or_equal(3.0));
728    ok(expect_value(json!(2)).to_be_less_than(3.0));
729    ok(expect_value(json!(3)).to_be_less_than_or_equal(3.0));
730  }
731}