@tishlang/tish-lsp 3.6.0 → 3.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +1 -0
- package/bin/tish-lsp +0 -0
- package/crates/tish/Cargo.toml +4 -1
- package/crates/tish/src/cli_help.rs +84 -0
- package/crates/tish/src/main.rs +93 -1
- package/crates/tish/tests/test_runner.rs +673 -0
- package/crates/tish_bytecode/src/compiler.rs +25 -4
- package/crates/tish_compile/src/resolve.rs +45 -12
- package/crates/tish_core/src/lib.rs +103 -11
- package/crates/tish_eval/src/eval.rs +16 -0
- package/crates/tish_eval/src/value_convert.rs +10 -0
- package/crates/tish_lsp/Cargo.toml +1 -1
- package/crates/tish_test/Cargo.toml +47 -0
- package/crates/tish_test/src/assert.rs +435 -0
- package/crates/tish_test/src/config.rs +85 -0
- package/crates/tish_test/src/coverage.rs +237 -0
- package/crates/tish_test/src/deep_equal.rs +261 -0
- package/crates/tish_test/src/discovery.rs +70 -0
- package/crates/tish_test/src/expect.rs +977 -0
- package/crates/tish_test/src/instrument.rs +457 -0
- package/crates/tish_test/src/isolation.rs +14 -0
- package/crates/tish_test/src/lib.rs +22 -0
- package/crates/tish_test/src/load.rs +152 -0
- package/crates/tish_test/src/mocks.rs +270 -0
- package/crates/tish_test/src/module.rs +318 -0
- package/crates/tish_test/src/registry.rs +235 -0
- package/crates/tish_test/src/report.rs +312 -0
- package/crates/tish_test/src/runner.rs +797 -0
- package/crates/tish_test/src/snapshots.rs +197 -0
- package/crates/tish_vm/src/vm.rs +17 -14
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-lsp +0 -0
- package/platform/darwin-x64/tish-lsp +0 -0
- package/platform/linux-arm64/tish-lsp +0 -0
- package/platform/linux-x64/tish-lsp +0 -0
- package/platform/win32-x64/tish-lsp.exe +0 -0
|
@@ -0,0 +1,977 @@
|
|
|
1
|
+
//! Jest/Bun-shaped `expect` matchers.
|
|
2
|
+
|
|
3
|
+
use tishlang_core::{
|
|
4
|
+
has_pending_throw, set_pending_throw, take_pending_throw, value_call, Arc, ObjectMap, Value,
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
use crate::assert::assertion_error;
|
|
8
|
+
use crate::deep_equal::deep_strict_equal;
|
|
9
|
+
|
|
10
|
+
fn fail(msg: String, actual: Value, expected: Option<Value>, op: &str) -> Value {
|
|
11
|
+
set_pending_throw(assertion_error(msg, Some(actual), expected, op, true));
|
|
12
|
+
Value::Null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
fn is_truthy(v: &Value) -> bool {
|
|
16
|
+
match v {
|
|
17
|
+
Value::Null => false,
|
|
18
|
+
Value::Bool(b) => *b,
|
|
19
|
+
Value::Number(n) => *n != 0.0 && !n.is_nan(),
|
|
20
|
+
Value::String(s) => !s.is_empty(),
|
|
21
|
+
_ => true,
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// Can `value_call` actually invoke this? Objects are callable only via a `__call` member.
|
|
26
|
+
pub(crate) fn is_callable(v: &Value) -> bool {
|
|
27
|
+
match v {
|
|
28
|
+
Value::Function(_) => true,
|
|
29
|
+
Value::Object(o) => o.borrow().strings.contains_key("__call"),
|
|
30
|
+
_ => false,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// Human-readable text for a thrown value: `Error`-shaped objects report their `message`.
|
|
35
|
+
fn error_text(err: &Value) -> String {
|
|
36
|
+
if let Value::Object(o) = err {
|
|
37
|
+
if let Some(Value::String(m)) = o.borrow().strings.get("message") {
|
|
38
|
+
return m.to_string();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
err.to_display_string()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// Does a thrown value satisfy `expected` (string substring, regex, `Error`-shaped object,
|
|
45
|
+
/// or a deep-equal value)? Node/Jest semantics — the previous version accepted *any* throw.
|
|
46
|
+
pub(crate) fn error_matches(err: &Value, expected: &Value) -> bool {
|
|
47
|
+
match expected {
|
|
48
|
+
Value::String(pat) => error_text(err).contains(pat.as_str()),
|
|
49
|
+
#[cfg(feature = "regex")]
|
|
50
|
+
Value::RegExp(re) => re.borrow_mut().test(&error_text(err)),
|
|
51
|
+
// `{ message, name, … }` — every listed field must match.
|
|
52
|
+
Value::Object(o) if !o.borrow().strings.contains_key(ASYMMETRIC_KEY) => {
|
|
53
|
+
let want = o.borrow();
|
|
54
|
+
if want.strings.is_empty() {
|
|
55
|
+
return deep_strict_equal(err, expected);
|
|
56
|
+
}
|
|
57
|
+
let Value::Object(got) = err else {
|
|
58
|
+
return false;
|
|
59
|
+
};
|
|
60
|
+
let got = got.borrow();
|
|
61
|
+
want.strings.iter().all(|(k, wv)| match got.strings.get(k) {
|
|
62
|
+
Some(gv) => deep_strict_equal(gv, wv),
|
|
63
|
+
None => false,
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
_ => deep_strict_equal(err, expected),
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fn describe_error_matcher(expected: &Value) -> String {
|
|
71
|
+
match expected {
|
|
72
|
+
Value::String(s) => format!("a message containing {:?}", s.as_str()),
|
|
73
|
+
_ => expected.to_display_string(),
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fn type_of_name(v: &Value) -> &'static str {
|
|
78
|
+
match v {
|
|
79
|
+
Value::Null => "null",
|
|
80
|
+
Value::Bool(_) => "boolean",
|
|
81
|
+
Value::Number(_) => "number",
|
|
82
|
+
Value::String(_) => "string",
|
|
83
|
+
Value::Symbol(_) => "symbol",
|
|
84
|
+
Value::Function(_) => "function",
|
|
85
|
+
_ => "object",
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// Resolve a `toHaveProperty` path (`"a.b.0"` or `["a", "b", 0]`) to its value.
|
|
90
|
+
fn property_at(root: &Value, path: &Value) -> Option<Value> {
|
|
91
|
+
let segments: Vec<String> = match path {
|
|
92
|
+
Value::Array(a) => a.borrow().iter().map(|v| v.to_display_string()).collect(),
|
|
93
|
+
other => other
|
|
94
|
+
.to_display_string()
|
|
95
|
+
.split('.')
|
|
96
|
+
.map(|s| s.to_string())
|
|
97
|
+
.collect(),
|
|
98
|
+
};
|
|
99
|
+
let mut cur = root.clone();
|
|
100
|
+
for seg in segments {
|
|
101
|
+
cur = match &cur {
|
|
102
|
+
Value::Object(o) => o.borrow().strings.get(seg.as_str())?.clone(),
|
|
103
|
+
Value::Array(a) => {
|
|
104
|
+
let idx: usize = seg.parse().ok()?;
|
|
105
|
+
a.borrow().get(idx)?.clone()
|
|
106
|
+
}
|
|
107
|
+
Value::NumberArray(a) => {
|
|
108
|
+
let idx: usize = seg.parse().ok()?;
|
|
109
|
+
a.borrow().to_values().get(idx)?.clone()
|
|
110
|
+
}
|
|
111
|
+
_ => return None,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
Some(cur)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
fn length_of(v: &Value) -> Option<f64> {
|
|
118
|
+
match v {
|
|
119
|
+
Value::Array(a) => Some(a.borrow().len() as f64),
|
|
120
|
+
Value::NumberArray(a) => Some(a.borrow().len() as f64),
|
|
121
|
+
Value::String(s) => Some(s.chars().count() as f64),
|
|
122
|
+
Value::Object(o) => o.borrow().strings.get("length").and_then(|x| x.as_number()),
|
|
123
|
+
_ => None,
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// Resolve Jest/Bun mock metadata from `expect(fn)` / `expect(fn.mock)`.
|
|
128
|
+
fn mock_meta(v: &Value) -> Option<Value> {
|
|
129
|
+
let Value::Object(o) = v else {
|
|
130
|
+
return None;
|
|
131
|
+
};
|
|
132
|
+
let b = o.borrow();
|
|
133
|
+
if b.strings.contains_key("calls") && b.strings.contains_key("callCount") {
|
|
134
|
+
return Some(v.clone());
|
|
135
|
+
}
|
|
136
|
+
b.strings.get("mock").cloned().and_then(|m| {
|
|
137
|
+
if let Value::Object(mo) = &m {
|
|
138
|
+
let mb = mo.borrow();
|
|
139
|
+
if mb.strings.contains_key("calls") {
|
|
140
|
+
return Some(m.clone());
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
None
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
fn mock_call_count(meta: &Value) -> f64 {
|
|
148
|
+
let Value::Object(o) = meta else {
|
|
149
|
+
return 0.0;
|
|
150
|
+
};
|
|
151
|
+
o.borrow()
|
|
152
|
+
.strings
|
|
153
|
+
.get("callCount")
|
|
154
|
+
.and_then(|v| v.as_number())
|
|
155
|
+
.unwrap_or(0.0)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
fn mock_calls(meta: &Value) -> Vec<Value> {
|
|
159
|
+
let Value::Object(o) = meta else {
|
|
160
|
+
return Vec::new();
|
|
161
|
+
};
|
|
162
|
+
match o.borrow().strings.get("calls") {
|
|
163
|
+
Some(Value::Array(a)) => a.borrow().clone(),
|
|
164
|
+
_ => Vec::new(),
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// `!(a > b)` and friends are deliberate: they are TRUE when either side is NaN, which is what
|
|
169
|
+
// these matchers must report. `partial_cmp` would silently pass an incomparable pair.
|
|
170
|
+
#[allow(clippy::neg_cmp_op_on_partial_ord)]
|
|
171
|
+
fn matcher_object(actual: Value) -> Value {
|
|
172
|
+
let actual_be = actual.clone();
|
|
173
|
+
let actual_eq = actual.clone();
|
|
174
|
+
let actual_strict = actual.clone();
|
|
175
|
+
let actual_throw = actual.clone();
|
|
176
|
+
let actual_match = actual.clone();
|
|
177
|
+
let actual_contain = actual.clone();
|
|
178
|
+
let actual_null = actual.clone();
|
|
179
|
+
let actual_truthy = actual.clone();
|
|
180
|
+
let actual_falsy = actual.clone();
|
|
181
|
+
let actual_defined = actual.clone();
|
|
182
|
+
let actual_len = actual.clone();
|
|
183
|
+
let actual_gt = actual.clone();
|
|
184
|
+
let actual_gte = actual.clone();
|
|
185
|
+
let actual_lt = actual.clone();
|
|
186
|
+
let actual_lte = actual.clone();
|
|
187
|
+
let actual_snap = actual.clone();
|
|
188
|
+
let actual_called = actual.clone();
|
|
189
|
+
let actual_called_times = actual.clone();
|
|
190
|
+
let actual_called_with = actual.clone();
|
|
191
|
+
let actual_last_with = actual.clone();
|
|
192
|
+
|
|
193
|
+
let mut m = ObjectMap::default();
|
|
194
|
+
m.insert(
|
|
195
|
+
Arc::from("toBe"),
|
|
196
|
+
Value::native(move |args: &[Value]| {
|
|
197
|
+
let expected = args.first().cloned().unwrap_or(Value::Null);
|
|
198
|
+
if !actual_be.strict_eq(&expected) {
|
|
199
|
+
return fail(
|
|
200
|
+
format!(
|
|
201
|
+
"expect(received).toBe(expected)\n\nExpected: {}\nReceived: {}",
|
|
202
|
+
expected.to_display_string(),
|
|
203
|
+
actual_be.to_display_string()
|
|
204
|
+
),
|
|
205
|
+
actual_be.clone(),
|
|
206
|
+
Some(expected),
|
|
207
|
+
"toBe",
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
Value::Null
|
|
211
|
+
}),
|
|
212
|
+
);
|
|
213
|
+
m.insert(
|
|
214
|
+
Arc::from("toEqual"),
|
|
215
|
+
Value::native(move |args: &[Value]| {
|
|
216
|
+
let expected = args.first().cloned().unwrap_or(Value::Null);
|
|
217
|
+
if !deep_strict_equal(&actual_eq, &expected) {
|
|
218
|
+
return fail(
|
|
219
|
+
format!(
|
|
220
|
+
"expect(received).toEqual(expected)\n\nExpected: {}\nReceived: {}",
|
|
221
|
+
expected.to_display_string(),
|
|
222
|
+
actual_eq.to_display_string()
|
|
223
|
+
),
|
|
224
|
+
actual_eq.clone(),
|
|
225
|
+
Some(expected),
|
|
226
|
+
"toEqual",
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
Value::Null
|
|
230
|
+
}),
|
|
231
|
+
);
|
|
232
|
+
m.insert(
|
|
233
|
+
Arc::from("toStrictEqual"),
|
|
234
|
+
Value::native(move |args: &[Value]| {
|
|
235
|
+
let expected = args.first().cloned().unwrap_or(Value::Null);
|
|
236
|
+
if !deep_strict_equal(&actual_strict, &expected) {
|
|
237
|
+
return fail(
|
|
238
|
+
format!(
|
|
239
|
+
"expect(received).toStrictEqual(expected)\n\nExpected: {}\nReceived: {}",
|
|
240
|
+
expected.to_display_string(),
|
|
241
|
+
actual_strict.to_display_string()
|
|
242
|
+
),
|
|
243
|
+
actual_strict.clone(),
|
|
244
|
+
Some(expected),
|
|
245
|
+
"toStrictEqual",
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
Value::Null
|
|
249
|
+
}),
|
|
250
|
+
);
|
|
251
|
+
m.insert(
|
|
252
|
+
Arc::from("toThrow"),
|
|
253
|
+
Value::native(move |args: &[Value]| {
|
|
254
|
+
// A non-callable would make `value_call` set a pending throw, which used to read
|
|
255
|
+
// as "the code threw" and pass. Reject it up front instead.
|
|
256
|
+
if !is_callable(&actual_throw) {
|
|
257
|
+
return fail(
|
|
258
|
+
format!(
|
|
259
|
+
"expect(received).toThrow()\n\nReceived value is not a function: {}",
|
|
260
|
+
actual_throw.to_display_string()
|
|
261
|
+
),
|
|
262
|
+
actual_throw.clone(),
|
|
263
|
+
None,
|
|
264
|
+
"toThrow",
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
let _ = take_pending_throw();
|
|
268
|
+
let _ = value_call(&actual_throw, &[]);
|
|
269
|
+
if !has_pending_throw() {
|
|
270
|
+
return fail(
|
|
271
|
+
"expect(received).toThrow()\n\nReceived function did not throw".into(),
|
|
272
|
+
actual_throw.clone(),
|
|
273
|
+
None,
|
|
274
|
+
"toThrow",
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
let err = take_pending_throw().unwrap_or(Value::Null);
|
|
278
|
+
if let Some(expected) = args.first() {
|
|
279
|
+
if !error_matches(&err, expected) {
|
|
280
|
+
return fail(
|
|
281
|
+
format!(
|
|
282
|
+
"expect(received).toThrow(expected)\n\nExpected: {}\nReceived: {}",
|
|
283
|
+
describe_error_matcher(expected),
|
|
284
|
+
error_text(&err)
|
|
285
|
+
),
|
|
286
|
+
err,
|
|
287
|
+
Some(expected.clone()),
|
|
288
|
+
"toThrow",
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
Value::Null
|
|
293
|
+
}),
|
|
294
|
+
);
|
|
295
|
+
m.insert(
|
|
296
|
+
Arc::from("toMatch"),
|
|
297
|
+
Value::native(move |args: &[Value]| {
|
|
298
|
+
let s = actual_match.to_display_string();
|
|
299
|
+
let pattern = args.first().cloned().unwrap_or(Value::Null);
|
|
300
|
+
let ok = match &pattern {
|
|
301
|
+
#[cfg(feature = "regex")]
|
|
302
|
+
Value::RegExp(r) => r.borrow_mut().test(&s),
|
|
303
|
+
Value::String(p) => s.contains(p.as_str()),
|
|
304
|
+
other => s.contains(&other.to_display_string()),
|
|
305
|
+
};
|
|
306
|
+
if !ok {
|
|
307
|
+
return fail(
|
|
308
|
+
format!(
|
|
309
|
+
"expect(received).toMatch(expected)\n\nExpected pattern: {}\nReceived: {}",
|
|
310
|
+
pattern.to_display_string(),
|
|
311
|
+
s
|
|
312
|
+
),
|
|
313
|
+
actual_match.clone(),
|
|
314
|
+
Some(pattern),
|
|
315
|
+
"toMatch",
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
Value::Null
|
|
319
|
+
}),
|
|
320
|
+
);
|
|
321
|
+
m.insert(
|
|
322
|
+
Arc::from("toContain"),
|
|
323
|
+
Value::native(move |args: &[Value]| {
|
|
324
|
+
let item = args.first().cloned().unwrap_or(Value::Null);
|
|
325
|
+
let ok = match &actual_contain {
|
|
326
|
+
Value::String(s) => s.as_str().contains(&item.to_display_string()),
|
|
327
|
+
Value::Array(a) => a.borrow().iter().any(|v| v.strict_eq(&item)),
|
|
328
|
+
Value::NumberArray(a) => a
|
|
329
|
+
.borrow()
|
|
330
|
+
.to_values()
|
|
331
|
+
.iter()
|
|
332
|
+
.any(|v| v.strict_eq(&item)),
|
|
333
|
+
_ => false,
|
|
334
|
+
};
|
|
335
|
+
if !ok {
|
|
336
|
+
return fail(
|
|
337
|
+
format!(
|
|
338
|
+
"expect(received).toContain(expected)\n\nExpected to contain: {}\nReceived: {}",
|
|
339
|
+
item.to_display_string(),
|
|
340
|
+
actual_contain.to_display_string()
|
|
341
|
+
),
|
|
342
|
+
actual_contain.clone(),
|
|
343
|
+
Some(item),
|
|
344
|
+
"toContain",
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
Value::Null
|
|
348
|
+
}),
|
|
349
|
+
);
|
|
350
|
+
m.insert(
|
|
351
|
+
Arc::from("toBeNull"),
|
|
352
|
+
Value::native(move |_args: &[Value]| {
|
|
353
|
+
if !matches!(actual_null, Value::Null) {
|
|
354
|
+
return fail(
|
|
355
|
+
format!(
|
|
356
|
+
"expect(received).toBeNull()\n\nReceived: {}",
|
|
357
|
+
actual_null.to_display_string()
|
|
358
|
+
),
|
|
359
|
+
actual_null.clone(),
|
|
360
|
+
Some(Value::Null),
|
|
361
|
+
"toBeNull",
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
Value::Null
|
|
365
|
+
}),
|
|
366
|
+
);
|
|
367
|
+
m.insert(
|
|
368
|
+
Arc::from("toBeTruthy"),
|
|
369
|
+
Value::native(move |_args: &[Value]| {
|
|
370
|
+
if !is_truthy(&actual_truthy) {
|
|
371
|
+
return fail(
|
|
372
|
+
format!(
|
|
373
|
+
"expect(received).toBeTruthy()\n\nReceived: {}",
|
|
374
|
+
actual_truthy.to_display_string()
|
|
375
|
+
),
|
|
376
|
+
actual_truthy.clone(),
|
|
377
|
+
None,
|
|
378
|
+
"toBeTruthy",
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
Value::Null
|
|
382
|
+
}),
|
|
383
|
+
);
|
|
384
|
+
m.insert(
|
|
385
|
+
Arc::from("toBeFalsy"),
|
|
386
|
+
Value::native(move |_args: &[Value]| {
|
|
387
|
+
if is_truthy(&actual_falsy) {
|
|
388
|
+
return fail(
|
|
389
|
+
format!(
|
|
390
|
+
"expect(received).toBeFalsy()\n\nReceived: {}",
|
|
391
|
+
actual_falsy.to_display_string()
|
|
392
|
+
),
|
|
393
|
+
actual_falsy.clone(),
|
|
394
|
+
None,
|
|
395
|
+
"toBeFalsy",
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
Value::Null
|
|
399
|
+
}),
|
|
400
|
+
);
|
|
401
|
+
// Tish has no `undefined`; `null` is the absence value, so these mirror `toBeNull`.
|
|
402
|
+
// Always-passing / always-failing versions were traps: ported Node suites got no signal.
|
|
403
|
+
m.insert(
|
|
404
|
+
Arc::from("toBeDefined"),
|
|
405
|
+
Value::native(move |_args: &[Value]| {
|
|
406
|
+
if matches!(actual_defined, Value::Null) {
|
|
407
|
+
return fail(
|
|
408
|
+
"expect(received).toBeDefined()\n\nReceived: null".into(),
|
|
409
|
+
actual_defined.clone(),
|
|
410
|
+
None,
|
|
411
|
+
"toBeDefined",
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
Value::Null
|
|
415
|
+
}),
|
|
416
|
+
);
|
|
417
|
+
let actual_undefined = actual.clone();
|
|
418
|
+
m.insert(
|
|
419
|
+
Arc::from("toBeUndefined"),
|
|
420
|
+
Value::native(move |_args: &[Value]| {
|
|
421
|
+
if !matches!(actual_undefined, Value::Null) {
|
|
422
|
+
return fail(
|
|
423
|
+
format!(
|
|
424
|
+
"expect(received).toBeUndefined()\n\nTish has no undefined; null is the absence value.\nReceived: {}",
|
|
425
|
+
actual_undefined.to_display_string()
|
|
426
|
+
),
|
|
427
|
+
actual_undefined.clone(),
|
|
428
|
+
None,
|
|
429
|
+
"toBeUndefined",
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
Value::Null
|
|
433
|
+
}),
|
|
434
|
+
);
|
|
435
|
+
let actual_close = actual.clone();
|
|
436
|
+
m.insert(
|
|
437
|
+
Arc::from("toBeCloseTo"),
|
|
438
|
+
Value::native(move |args: &[Value]| {
|
|
439
|
+
let expected = args.first().and_then(|v| v.as_number()).unwrap_or(0.0);
|
|
440
|
+
let digits = args.get(1).and_then(|v| v.as_number()).unwrap_or(2.0);
|
|
441
|
+
let act = actual_close.as_number().unwrap_or(f64::NAN);
|
|
442
|
+
// Jest/Bun: |a - b| < 10^-digits / 2. The un-halved form accepted values Jest rejects.
|
|
443
|
+
let prec = 10f64.powf(-digits.max(0.0)) / 2.0;
|
|
444
|
+
if !((act - expected).abs() < prec) {
|
|
445
|
+
return fail(
|
|
446
|
+
format!(
|
|
447
|
+
"expect(received).toBeCloseTo({expected}, {digits})\n\nReceived: {act}"
|
|
448
|
+
),
|
|
449
|
+
actual_close.clone(),
|
|
450
|
+
Some(Value::Number(expected)),
|
|
451
|
+
"toBeCloseTo",
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
Value::Null
|
|
455
|
+
}),
|
|
456
|
+
);
|
|
457
|
+
let actual_match_obj = actual.clone();
|
|
458
|
+
m.insert(
|
|
459
|
+
Arc::from("toMatchObject"),
|
|
460
|
+
Value::native(move |args: &[Value]| {
|
|
461
|
+
let expected = args.first().cloned().unwrap_or(Value::Null);
|
|
462
|
+
if !crate::deep_equal::partial_deep_strict_equal(&actual_match_obj, &expected) {
|
|
463
|
+
return fail(
|
|
464
|
+
format!(
|
|
465
|
+
"expect(received).toMatchObject(expected)\n\nExpected: {}\nReceived: {}",
|
|
466
|
+
expected.to_display_string(),
|
|
467
|
+
actual_match_obj.to_display_string()
|
|
468
|
+
),
|
|
469
|
+
actual_match_obj.clone(),
|
|
470
|
+
Some(expected),
|
|
471
|
+
"toMatchObject",
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
Value::Null
|
|
475
|
+
}),
|
|
476
|
+
);
|
|
477
|
+
m.insert(
|
|
478
|
+
Arc::from("toHaveLength"),
|
|
479
|
+
Value::native(move |args: &[Value]| {
|
|
480
|
+
let expected = args.first().and_then(|v| v.as_number()).unwrap_or(0.0);
|
|
481
|
+
match length_of(&actual_len) {
|
|
482
|
+
Some(n) if (n - expected).abs() < f64::EPSILON => Value::Null,
|
|
483
|
+
Some(n) => fail(
|
|
484
|
+
format!(
|
|
485
|
+
"expect(received).toHaveLength({})\n\nReceived length: {}",
|
|
486
|
+
expected, n
|
|
487
|
+
),
|
|
488
|
+
actual_len.clone(),
|
|
489
|
+
Some(Value::Number(expected)),
|
|
490
|
+
"toHaveLength",
|
|
491
|
+
),
|
|
492
|
+
None => fail(
|
|
493
|
+
format!(
|
|
494
|
+
"expect(received).toHaveLength()\n\nReceived value has no length: {}",
|
|
495
|
+
actual_len.to_display_string()
|
|
496
|
+
),
|
|
497
|
+
actual_len.clone(),
|
|
498
|
+
Some(Value::Number(expected)),
|
|
499
|
+
"toHaveLength",
|
|
500
|
+
),
|
|
501
|
+
}
|
|
502
|
+
}),
|
|
503
|
+
);
|
|
504
|
+
m.insert(
|
|
505
|
+
Arc::from("toBeGreaterThan"),
|
|
506
|
+
Value::native(move |args: &[Value]| {
|
|
507
|
+
let exp = args.first().and_then(|v| v.as_number()).unwrap_or(0.0);
|
|
508
|
+
let act = actual_gt.as_number().unwrap_or(f64::NAN);
|
|
509
|
+
if !(act > exp) {
|
|
510
|
+
return fail(
|
|
511
|
+
format!("expect({act}).toBeGreaterThan({exp})"),
|
|
512
|
+
actual_gt.clone(),
|
|
513
|
+
Some(Value::Number(exp)),
|
|
514
|
+
"toBeGreaterThan",
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
Value::Null
|
|
518
|
+
}),
|
|
519
|
+
);
|
|
520
|
+
m.insert(
|
|
521
|
+
Arc::from("toBeGreaterThanOrEqual"),
|
|
522
|
+
Value::native(move |args: &[Value]| {
|
|
523
|
+
let exp = args.first().and_then(|v| v.as_number()).unwrap_or(0.0);
|
|
524
|
+
let act = actual_gte.as_number().unwrap_or(f64::NAN);
|
|
525
|
+
if !(act >= exp) {
|
|
526
|
+
return fail(
|
|
527
|
+
format!("expect({act}).toBeGreaterThanOrEqual({exp})"),
|
|
528
|
+
actual_gte.clone(),
|
|
529
|
+
Some(Value::Number(exp)),
|
|
530
|
+
"toBeGreaterThanOrEqual",
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
Value::Null
|
|
534
|
+
}),
|
|
535
|
+
);
|
|
536
|
+
m.insert(
|
|
537
|
+
Arc::from("toBeLessThan"),
|
|
538
|
+
Value::native(move |args: &[Value]| {
|
|
539
|
+
let exp = args.first().and_then(|v| v.as_number()).unwrap_or(0.0);
|
|
540
|
+
let act = actual_lt.as_number().unwrap_or(f64::NAN);
|
|
541
|
+
if !(act < exp) {
|
|
542
|
+
return fail(
|
|
543
|
+
format!("expect({act}).toBeLessThan({exp})"),
|
|
544
|
+
actual_lt.clone(),
|
|
545
|
+
Some(Value::Number(exp)),
|
|
546
|
+
"toBeLessThan",
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
Value::Null
|
|
550
|
+
}),
|
|
551
|
+
);
|
|
552
|
+
m.insert(
|
|
553
|
+
Arc::from("toBeLessThanOrEqual"),
|
|
554
|
+
Value::native(move |args: &[Value]| {
|
|
555
|
+
let exp = args.first().and_then(|v| v.as_number()).unwrap_or(0.0);
|
|
556
|
+
let act = actual_lte.as_number().unwrap_or(f64::NAN);
|
|
557
|
+
if !(act <= exp) {
|
|
558
|
+
return fail(
|
|
559
|
+
format!("expect({act}).toBeLessThanOrEqual({exp})"),
|
|
560
|
+
actual_lte.clone(),
|
|
561
|
+
Some(Value::Number(exp)),
|
|
562
|
+
"toBeLessThanOrEqual",
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
Value::Null
|
|
566
|
+
}),
|
|
567
|
+
);
|
|
568
|
+
m.insert(
|
|
569
|
+
Arc::from("toHaveBeenCalled"),
|
|
570
|
+
Value::native(move |_args: &[Value]| {
|
|
571
|
+
let Some(meta) = mock_meta(&actual_called) else {
|
|
572
|
+
return fail(
|
|
573
|
+
"expect(...).toHaveBeenCalled() requires a mock or spy".into(),
|
|
574
|
+
actual_called.clone(),
|
|
575
|
+
None,
|
|
576
|
+
"toHaveBeenCalled",
|
|
577
|
+
);
|
|
578
|
+
};
|
|
579
|
+
if mock_call_count(&meta) < 1.0 {
|
|
580
|
+
return fail(
|
|
581
|
+
"expect(mock).toHaveBeenCalled()".into(),
|
|
582
|
+
actual_called.clone(),
|
|
583
|
+
None,
|
|
584
|
+
"toHaveBeenCalled",
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
Value::Null
|
|
588
|
+
}),
|
|
589
|
+
);
|
|
590
|
+
m.insert(
|
|
591
|
+
Arc::from("toHaveBeenCalledTimes"),
|
|
592
|
+
Value::native(move |args: &[Value]| {
|
|
593
|
+
let Some(meta) = mock_meta(&actual_called_times) else {
|
|
594
|
+
return fail(
|
|
595
|
+
"expect(...).toHaveBeenCalledTimes() requires a mock or spy".into(),
|
|
596
|
+
actual_called_times.clone(),
|
|
597
|
+
None,
|
|
598
|
+
"toHaveBeenCalledTimes",
|
|
599
|
+
);
|
|
600
|
+
};
|
|
601
|
+
let expected = args.first().and_then(|v| v.as_number()).unwrap_or(0.0);
|
|
602
|
+
let got = mock_call_count(&meta);
|
|
603
|
+
if got != expected {
|
|
604
|
+
return fail(
|
|
605
|
+
format!("expect(mock).toHaveBeenCalledTimes({expected})\n\nReceived: {got}"),
|
|
606
|
+
actual_called_times.clone(),
|
|
607
|
+
Some(Value::Number(expected)),
|
|
608
|
+
"toHaveBeenCalledTimes",
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
Value::Null
|
|
612
|
+
}),
|
|
613
|
+
);
|
|
614
|
+
m.insert(
|
|
615
|
+
Arc::from("toHaveBeenCalledWith"),
|
|
616
|
+
Value::native(move |args: &[Value]| {
|
|
617
|
+
let Some(meta) = mock_meta(&actual_called_with) else {
|
|
618
|
+
return fail(
|
|
619
|
+
"expect(...).toHaveBeenCalledWith() requires a mock or spy".into(),
|
|
620
|
+
actual_called_with.clone(),
|
|
621
|
+
None,
|
|
622
|
+
"toHaveBeenCalledWith",
|
|
623
|
+
);
|
|
624
|
+
};
|
|
625
|
+
let expected = args.to_vec();
|
|
626
|
+
let ok = mock_calls(&meta).iter().any(|call| match call {
|
|
627
|
+
Value::Array(a) => {
|
|
628
|
+
let got = a.borrow();
|
|
629
|
+
got.len() == expected.len()
|
|
630
|
+
&& got
|
|
631
|
+
.iter()
|
|
632
|
+
.zip(expected.iter())
|
|
633
|
+
.all(|(g, e)| deep_strict_equal(g, e))
|
|
634
|
+
}
|
|
635
|
+
_ => false,
|
|
636
|
+
});
|
|
637
|
+
if !ok {
|
|
638
|
+
return fail(
|
|
639
|
+
format!(
|
|
640
|
+
"expect(mock).toHaveBeenCalledWith({})\n\nNumber of calls: {}",
|
|
641
|
+
expected
|
|
642
|
+
.iter()
|
|
643
|
+
.map(|v| v.to_display_string())
|
|
644
|
+
.collect::<Vec<_>>()
|
|
645
|
+
.join(", "),
|
|
646
|
+
mock_call_count(&meta)
|
|
647
|
+
),
|
|
648
|
+
actual_called_with.clone(),
|
|
649
|
+
None,
|
|
650
|
+
"toHaveBeenCalledWith",
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
Value::Null
|
|
654
|
+
}),
|
|
655
|
+
);
|
|
656
|
+
m.insert(
|
|
657
|
+
Arc::from("toHaveBeenLastCalledWith"),
|
|
658
|
+
Value::native(move |args: &[Value]| {
|
|
659
|
+
let Some(meta) = mock_meta(&actual_last_with) else {
|
|
660
|
+
return fail(
|
|
661
|
+
"expect(...).toHaveBeenLastCalledWith() requires a mock or spy".into(),
|
|
662
|
+
actual_last_with.clone(),
|
|
663
|
+
None,
|
|
664
|
+
"toHaveBeenLastCalledWith",
|
|
665
|
+
);
|
|
666
|
+
};
|
|
667
|
+
let expected = args.to_vec();
|
|
668
|
+
let calls = mock_calls(&meta);
|
|
669
|
+
let Some(last) = calls.last() else {
|
|
670
|
+
return fail(
|
|
671
|
+
"expect(mock).toHaveBeenLastCalledWith(...)\n\nNumber of calls: 0".into(),
|
|
672
|
+
actual_last_with.clone(),
|
|
673
|
+
None,
|
|
674
|
+
"toHaveBeenLastCalledWith",
|
|
675
|
+
);
|
|
676
|
+
};
|
|
677
|
+
let ok = match last {
|
|
678
|
+
Value::Array(a) => {
|
|
679
|
+
let got = a.borrow();
|
|
680
|
+
got.len() == expected.len()
|
|
681
|
+
&& got
|
|
682
|
+
.iter()
|
|
683
|
+
.zip(expected.iter())
|
|
684
|
+
.all(|(g, e)| deep_strict_equal(g, e))
|
|
685
|
+
}
|
|
686
|
+
_ => false,
|
|
687
|
+
};
|
|
688
|
+
if !ok {
|
|
689
|
+
return fail(
|
|
690
|
+
"expect(mock).toHaveBeenLastCalledWith(...)".into(),
|
|
691
|
+
actual_last_with.clone(),
|
|
692
|
+
None,
|
|
693
|
+
"toHaveBeenLastCalledWith",
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
Value::Null
|
|
697
|
+
}),
|
|
698
|
+
);
|
|
699
|
+
m.insert(
|
|
700
|
+
Arc::from("toMatchSnapshot"),
|
|
701
|
+
Value::native(move |args: &[Value]| {
|
|
702
|
+
let hint = args.first().map(|v| v.to_display_string());
|
|
703
|
+
crate::snapshots::match_snapshot(&actual_snap, hint.as_deref())
|
|
704
|
+
}),
|
|
705
|
+
);
|
|
706
|
+
let actual_nan = actual.clone();
|
|
707
|
+
m.insert(
|
|
708
|
+
Arc::from("toBeNaN"),
|
|
709
|
+
Value::native(move |_args: &[Value]| {
|
|
710
|
+
if !matches!(actual_nan, Value::Number(n) if n.is_nan()) {
|
|
711
|
+
return fail(
|
|
712
|
+
format!(
|
|
713
|
+
"expect(received).toBeNaN()\n\nReceived: {}",
|
|
714
|
+
actual_nan.to_display_string()
|
|
715
|
+
),
|
|
716
|
+
actual_nan.clone(),
|
|
717
|
+
None,
|
|
718
|
+
"toBeNaN",
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
Value::Null
|
|
722
|
+
}),
|
|
723
|
+
);
|
|
724
|
+
let actual_typeof = actual.clone();
|
|
725
|
+
m.insert(
|
|
726
|
+
Arc::from("toBeTypeOf"),
|
|
727
|
+
Value::native(move |args: &[Value]| {
|
|
728
|
+
let want = args
|
|
729
|
+
.first()
|
|
730
|
+
.map(|v| v.to_display_string())
|
|
731
|
+
.unwrap_or_default();
|
|
732
|
+
let got = type_of_name(&actual_typeof);
|
|
733
|
+
if got != want {
|
|
734
|
+
return fail(
|
|
735
|
+
format!("expect(received).toBeTypeOf({want:?})\n\nReceived type: {got}"),
|
|
736
|
+
actual_typeof.clone(),
|
|
737
|
+
None,
|
|
738
|
+
"toBeTypeOf",
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
Value::Null
|
|
742
|
+
}),
|
|
743
|
+
);
|
|
744
|
+
let actual_prop = actual.clone();
|
|
745
|
+
m.insert(
|
|
746
|
+
Arc::from("toHaveProperty"),
|
|
747
|
+
Value::native(move |args: &[Value]| {
|
|
748
|
+
let path = args.first().cloned().unwrap_or(Value::Null);
|
|
749
|
+
let Some(found) = property_at(&actual_prop, &path) else {
|
|
750
|
+
return fail(
|
|
751
|
+
format!(
|
|
752
|
+
"expect(received).toHaveProperty({})\n\nProperty not found on: {}",
|
|
753
|
+
path.to_display_string(),
|
|
754
|
+
actual_prop.to_display_string()
|
|
755
|
+
),
|
|
756
|
+
actual_prop.clone(),
|
|
757
|
+
None,
|
|
758
|
+
"toHaveProperty",
|
|
759
|
+
);
|
|
760
|
+
};
|
|
761
|
+
if let Some(expected) = args.get(1) {
|
|
762
|
+
if !deep_strict_equal(&found, expected) {
|
|
763
|
+
return fail(
|
|
764
|
+
format!(
|
|
765
|
+
"expect(received).toHaveProperty({}, expected)\n\nExpected: {}\nReceived: {}",
|
|
766
|
+
path.to_display_string(),
|
|
767
|
+
expected.to_display_string(),
|
|
768
|
+
found.to_display_string()
|
|
769
|
+
),
|
|
770
|
+
found,
|
|
771
|
+
Some(expected.clone()),
|
|
772
|
+
"toHaveProperty",
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
Value::Null
|
|
777
|
+
}),
|
|
778
|
+
);
|
|
779
|
+
let actual_contain_eq = actual.clone();
|
|
780
|
+
m.insert(
|
|
781
|
+
Arc::from("toContainEqual"),
|
|
782
|
+
Value::native(move |args: &[Value]| {
|
|
783
|
+
let item = args.first().cloned().unwrap_or(Value::Null);
|
|
784
|
+
let items = match actual_contain_eq.clone().coerce_number_array() {
|
|
785
|
+
Value::Array(a) => a.borrow().clone(),
|
|
786
|
+
_ => Vec::new(),
|
|
787
|
+
};
|
|
788
|
+
if !items.iter().any(|v| deep_strict_equal(v, &item)) {
|
|
789
|
+
return fail(
|
|
790
|
+
format!(
|
|
791
|
+
"expect(received).toContainEqual(expected)\n\nExpected to contain: {}\nReceived: {}",
|
|
792
|
+
item.to_display_string(),
|
|
793
|
+
actual_contain_eq.to_display_string()
|
|
794
|
+
),
|
|
795
|
+
actual_contain_eq.clone(),
|
|
796
|
+
Some(item),
|
|
797
|
+
"toContainEqual",
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
Value::Null
|
|
801
|
+
}),
|
|
802
|
+
);
|
|
803
|
+
|
|
804
|
+
// `toThrowError` is Jest's alias for `toThrow`.
|
|
805
|
+
if let Some(t) = m.get("toThrow").cloned() {
|
|
806
|
+
m.insert(Arc::from("toThrowError"), t);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
attach_not(&mut m, actual);
|
|
810
|
+
Value::object(m)
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/// Build `expect(x).not.*` by inverting each matcher via pending-throw polarity.
|
|
814
|
+
fn attach_not(m: &mut ObjectMap, actual: Value) {
|
|
815
|
+
let mut not_map = ObjectMap::default();
|
|
816
|
+
let keys: Vec<Arc<str>> = m.keys().cloned().collect();
|
|
817
|
+
for key in keys {
|
|
818
|
+
// `.not.toMatchSnapshot()` would write (or diff-invert) a snapshot; Jest has no such
|
|
819
|
+
// matcher either.
|
|
820
|
+
if key.as_ref() == "not" || key.as_ref() == "toMatchSnapshot" {
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
let Some(matcher) = m.get(key.as_ref()).cloned() else {
|
|
824
|
+
continue;
|
|
825
|
+
};
|
|
826
|
+
let act = actual.clone();
|
|
827
|
+
let name = key.to_string();
|
|
828
|
+
not_map.insert(
|
|
829
|
+
Arc::clone(&key),
|
|
830
|
+
Value::native(move |args: &[Value]| {
|
|
831
|
+
let _ = take_pending_throw();
|
|
832
|
+
let _ = value_call(&matcher, args);
|
|
833
|
+
if has_pending_throw() {
|
|
834
|
+
let _ = take_pending_throw();
|
|
835
|
+
return Value::Null;
|
|
836
|
+
}
|
|
837
|
+
fail(
|
|
838
|
+
format!("expect(received).not.{name}() expected matcher to fail"),
|
|
839
|
+
act.clone(),
|
|
840
|
+
None,
|
|
841
|
+
&format!("not.{name}"),
|
|
842
|
+
)
|
|
843
|
+
}),
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
m.insert(Arc::from("not"), Value::object(not_map));
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
thread_local! {
|
|
850
|
+
static ASSERTIONS_MADE: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
|
851
|
+
/// From `expect.assertions(n)`.
|
|
852
|
+
static ASSERTIONS_EXPECTED: std::cell::Cell<Option<usize>> =
|
|
853
|
+
const { std::cell::Cell::new(None) };
|
|
854
|
+
/// From `expect.hasAssertions()` — "at least one".
|
|
855
|
+
static ASSERTIONS_AT_LEAST: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/// Reset the per-test assertion counters. Called by the runner before each test body.
|
|
859
|
+
pub fn begin_test_assertions() {
|
|
860
|
+
ASSERTIONS_MADE.with(|c| c.set(0));
|
|
861
|
+
ASSERTIONS_EXPECTED.with(|c| c.set(None));
|
|
862
|
+
ASSERTIONS_AT_LEAST.with(|c| c.set(false));
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/// Verify an `expect.assertions(n)` / `expect.hasAssertions()` contract for the test that just ran.
|
|
866
|
+
pub fn check_test_assertions() -> Result<(), String> {
|
|
867
|
+
let made = ASSERTIONS_MADE.with(|c| c.get());
|
|
868
|
+
if ASSERTIONS_AT_LEAST.with(|c| c.get()) && made == 0 {
|
|
869
|
+
return Err("expect.hasAssertions(): no assertions were made".to_string());
|
|
870
|
+
}
|
|
871
|
+
if let Some(want) = ASSERTIONS_EXPECTED.with(|c| c.get()) {
|
|
872
|
+
if made != want {
|
|
873
|
+
return Err(format!(
|
|
874
|
+
"expect.assertions({want}): {made} assertion(s) were made"
|
|
875
|
+
));
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
Ok(())
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
fn expect_call(args: &[Value]) -> Value {
|
|
882
|
+
ASSERTIONS_MADE.with(|c| c.set(c.get() + 1));
|
|
883
|
+
let actual = args.first().cloned().unwrap_or(Value::Null);
|
|
884
|
+
matcher_object(actual)
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
fn asymmetric_any(args: &[Value]) -> Value {
|
|
888
|
+
let type_name = args
|
|
889
|
+
.first()
|
|
890
|
+
.map(|v| v.to_display_string())
|
|
891
|
+
.unwrap_or_else(|| "Object".into());
|
|
892
|
+
let mut m = ObjectMap::default();
|
|
893
|
+
m.insert(Arc::from(ASYMMETRIC_KEY), Value::String("any".into()));
|
|
894
|
+
m.insert(Arc::from("typeName"), Value::String(type_name.into()));
|
|
895
|
+
Value::object(m)
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
fn asymmetric_object_containing(args: &[Value]) -> Value {
|
|
899
|
+
let sample = args.first().cloned().unwrap_or(Value::Null);
|
|
900
|
+
let mut m = ObjectMap::default();
|
|
901
|
+
m.insert(
|
|
902
|
+
Arc::from(ASYMMETRIC_KEY),
|
|
903
|
+
Value::String("objectContaining".into()),
|
|
904
|
+
);
|
|
905
|
+
m.insert(Arc::from("sample"), sample);
|
|
906
|
+
Value::object(m)
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
fn asymmetric_array_containing(args: &[Value]) -> Value {
|
|
910
|
+
let sample = args.first().cloned().unwrap_or(Value::Null);
|
|
911
|
+
let mut m = ObjectMap::default();
|
|
912
|
+
m.insert(
|
|
913
|
+
Arc::from(ASYMMETRIC_KEY),
|
|
914
|
+
Value::String("arrayContaining".into()),
|
|
915
|
+
);
|
|
916
|
+
m.insert(Arc::from("sample"), sample);
|
|
917
|
+
Value::object(m)
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
fn asymmetric_string_matching(args: &[Value]) -> Value {
|
|
921
|
+
let sample = args.first().cloned().unwrap_or(Value::Null);
|
|
922
|
+
let mut m = ObjectMap::default();
|
|
923
|
+
m.insert(
|
|
924
|
+
Arc::from(ASYMMETRIC_KEY),
|
|
925
|
+
Value::String("stringMatching".into()),
|
|
926
|
+
);
|
|
927
|
+
m.insert(Arc::from("sample"), sample);
|
|
928
|
+
Value::object(m)
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/// Marker key for `expect.any` / `objectContaining` / … Deliberately *not* React's `$$typeof`,
|
|
932
|
+
/// which a plain user object may legitimately carry — treating those as matchers made them
|
|
933
|
+
/// compare unequal to everything.
|
|
934
|
+
pub(crate) const ASYMMETRIC_KEY: &str = "__tishAsymmetricMatcher";
|
|
935
|
+
|
|
936
|
+
/// Build the callable `expect` object.
|
|
937
|
+
pub fn expect_object() -> Value {
|
|
938
|
+
let mut m = ObjectMap::default();
|
|
939
|
+
m.insert(Arc::from("__call"), Value::native(expect_call));
|
|
940
|
+
m.insert(
|
|
941
|
+
Arc::from("assertions"),
|
|
942
|
+
Value::native(|args: &[Value]| {
|
|
943
|
+
let n = args.first().and_then(|v| v.as_number()).unwrap_or(0.0);
|
|
944
|
+
ASSERTIONS_EXPECTED.with(|c| c.set(Some(n.max(0.0) as usize)));
|
|
945
|
+
Value::Null
|
|
946
|
+
}),
|
|
947
|
+
);
|
|
948
|
+
m.insert(
|
|
949
|
+
Arc::from("hasAssertions"),
|
|
950
|
+
Value::native(|_args: &[Value]| {
|
|
951
|
+
ASSERTIONS_AT_LEAST.with(|c| c.set(true));
|
|
952
|
+
Value::Null
|
|
953
|
+
}),
|
|
954
|
+
);
|
|
955
|
+
m.insert(Arc::from("any"), Value::native(asymmetric_any));
|
|
956
|
+
m.insert(
|
|
957
|
+
Arc::from("anything"),
|
|
958
|
+
Value::native(|_args: &[Value]| {
|
|
959
|
+
let mut o = ObjectMap::default();
|
|
960
|
+
o.insert(Arc::from(ASYMMETRIC_KEY), Value::String("anything".into()));
|
|
961
|
+
Value::object(o)
|
|
962
|
+
}),
|
|
963
|
+
);
|
|
964
|
+
m.insert(
|
|
965
|
+
Arc::from("objectContaining"),
|
|
966
|
+
Value::native(asymmetric_object_containing),
|
|
967
|
+
);
|
|
968
|
+
m.insert(
|
|
969
|
+
Arc::from("arrayContaining"),
|
|
970
|
+
Value::native(asymmetric_array_containing),
|
|
971
|
+
);
|
|
972
|
+
m.insert(
|
|
973
|
+
Arc::from("stringMatching"),
|
|
974
|
+
Value::native(asymmetric_string_matching),
|
|
975
|
+
);
|
|
976
|
+
Value::object(m)
|
|
977
|
+
}
|