1use rquickjs::function::{Async, Func, Opt, Rest};
10use rquickjs::{Ctx, Function, Object, Promise, Result, Value};
11
12use super::deep_equal::{Mode, deep_equal, loose_equal, strict_equal};
13use super::inspect::Inspector;
14
15fn render(value: &Value<'_>) -> String {
16 let mut out = String::new();
17 if Inspector::new(false).quoted().value(&mut out, value, 0).is_err() {
18 out.push_str("<unrenderable>");
19 }
20 out
21}
22
23fn fail_with<'js>(
25 ctx: &Ctx<'js>,
26 message: Opt<Value<'js>>,
27 generated: String,
28 actual: Value<'js>,
29 expected: Value<'js>,
30 operator: &str,
31) -> rquickjs::Error {
32 if let Some(value) = message.0.clone() {
34 if value.is_error() {
35 return ctx.throw(value);
36 }
37 }
38 let text = match message.0.as_ref().and_then(rquickjs::Value::as_string) {
39 Some(s) => s.to_string().unwrap_or(generated),
40 None => generated,
41 };
42
43 let build = |ctx: &Ctx<'js>| -> Result<Value<'js>> {
44 let error_ctor: rquickjs::function::Constructor<'js> = ctx.globals().get("Error")?;
45 let error: Object<'js> = error_ctor.construct((text.clone(),))?;
46 error.set("name", "AssertionError")?;
47 error.set("code", "ERR_ASSERTION")?;
48 error.set("actual", actual.clone())?;
49 error.set("expected", expected.clone())?;
50 error.set("operator", operator)?;
51 error.set("generatedMessage", message.0.is_none())?;
52 Ok(error.into_value())
53 };
54 match build(ctx) {
55 Ok(error) => ctx.throw(error),
56 Err(e) => e,
57 }
58}
59
60fn truthy(value: &Value<'_>) -> bool {
61 match value.type_of() {
62 rquickjs::Type::Undefined | rquickjs::Type::Null | rquickjs::Type::Uninitialized => false,
63 rquickjs::Type::Bool => value.as_bool().unwrap_or(false),
64 rquickjs::Type::Int | rquickjs::Type::Float => value.as_number().is_some_and(|n| n != 0.0 && !n.is_nan()),
65 rquickjs::Type::String => value
66 .as_string()
67 .and_then(|s| s.to_string().ok())
68 .is_some_and(|s| !s.is_empty()),
69 _ => true,
70 }
71}
72
73fn ok<'js>(ctx: Ctx<'js>, value: Value<'js>, message: Opt<Value<'js>>) -> Result<()> {
74 if truthy(&value) {
75 return Ok(());
76 }
77 let rendered = render(&value);
78 Err(fail_with(
79 &ctx,
80 message,
81 format!("The expression evaluated to a falsy value: {rendered}"),
82 value,
83 Value::new_bool(ctx.clone(), true),
84 "==",
85 ))
86}
87
88fn compare<'js>(
91 ctx: &Ctx<'js>,
92 actual: Value<'js>,
93 expected: Value<'js>,
94 message: Opt<Value<'js>>,
95 passed: bool,
96 operator: &str,
97 wording: &str,
98) -> Result<()> {
99 if passed {
100 return Ok(());
101 }
102 let generated = format!("{wording}\n\n{} {operator} {}\n", render(&actual), render(&expected));
103 Err(fail_with(ctx, message, generated, actual, expected, operator))
104}
105
106fn equal<'js>(ctx: Ctx<'js>, actual: Value<'js>, expected: Value<'js>, message: Opt<Value<'js>>) -> Result<()> {
107 let passed = loose_equal(&actual, &expected);
108 compare(&ctx, actual, expected, message, passed, "==", "Expected values to be loosely equal:")
109}
110
111fn not_equal<'js>(ctx: Ctx<'js>, actual: Value<'js>, expected: Value<'js>, message: Opt<Value<'js>>) -> Result<()> {
112 let passed = !loose_equal(&actual, &expected);
113 compare(
114 &ctx,
115 actual,
116 expected,
117 message,
118 passed,
119 "!=",
120 "Expected values not to be loosely equal:",
121 )
122}
123
124fn strict_eq<'js>(ctx: Ctx<'js>, actual: Value<'js>, expected: Value<'js>, message: Opt<Value<'js>>) -> Result<()> {
125 let passed = strict_equal(&actual, &expected);
126 compare(
127 &ctx,
128 actual,
129 expected,
130 message,
131 passed,
132 "strictEqual",
133 "Expected values to be strictly equal:",
134 )
135}
136
137fn not_strict_eq<'js>(ctx: Ctx<'js>, actual: Value<'js>, expected: Value<'js>, message: Opt<Value<'js>>) -> Result<()> {
138 let passed = !strict_equal(&actual, &expected);
139 compare(
140 &ctx,
141 actual,
142 expected,
143 message,
144 passed,
145 "notStrictEqual",
146 "Expected values not to be strictly equal:",
147 )
148}
149
150fn deep_eq<'js>(ctx: Ctx<'js>, actual: Value<'js>, expected: Value<'js>, message: Opt<Value<'js>>) -> Result<()> {
151 let passed = deep_equal(&actual, &expected, Mode::Loose)?;
152 compare(
153 &ctx,
154 actual,
155 expected,
156 message,
157 passed,
158 "deepEqual",
159 "Expected values to be loosely deep-equal:",
160 )
161}
162
163fn not_deep_eq<'js>(ctx: Ctx<'js>, actual: Value<'js>, expected: Value<'js>, message: Opt<Value<'js>>) -> Result<()> {
164 let passed = !deep_equal(&actual, &expected, Mode::Loose)?;
165 compare(
166 &ctx,
167 actual,
168 expected,
169 message,
170 passed,
171 "notDeepEqual",
172 "Expected values not to be loosely deep-equal:",
173 )
174}
175
176fn deep_strict_eq<'js>(ctx: Ctx<'js>, actual: Value<'js>, expected: Value<'js>, message: Opt<Value<'js>>) -> Result<()> {
177 let passed = deep_equal(&actual, &expected, Mode::Strict)?;
178 compare(
179 &ctx,
180 actual,
181 expected,
182 message,
183 passed,
184 "deepStrictEqual",
185 "Expected values to be strictly deep-equal:",
186 )
187}
188
189fn not_deep_strict_eq<'js>(
190 ctx: Ctx<'js>,
191 actual: Value<'js>,
192 expected: Value<'js>,
193 message: Opt<Value<'js>>,
194) -> Result<()> {
195 let passed = !deep_equal(&actual, &expected, Mode::Strict)?;
196 compare(
197 &ctx,
198 actual,
199 expected,
200 message,
201 passed,
202 "notDeepStrictEqual",
203 "Expected values not to be strictly deep-equal:",
204 )
205}
206
207fn regexp_test<'js>(regexp: &Object<'js>, subject: &Value<'js>) -> Result<bool> {
208 let test: Function<'js> = regexp.get("test")?;
209 test.call((rquickjs::function::This(regexp.clone()), subject.clone()))
210}
211
212fn matches<'js>(ctx: Ctx<'js>, subject: Value<'js>, regexp: Value<'js>, message: Opt<Value<'js>>) -> Result<()> {
213 let Some(re) = regexp.as_object() else {
214 return Err(rquickjs::Exception::throw_type(
215 &ctx,
216 "The \"regexp\" argument must be an instance of RegExp",
217 ));
218 };
219 let passed = regexp_test(re, &subject)?;
220 compare(
221 &ctx,
222 subject,
223 regexp.clone(),
224 message,
225 passed,
226 "match",
227 "The input did not match the regular expression:",
228 )
229}
230
231fn does_not_match<'js>(ctx: Ctx<'js>, subject: Value<'js>, regexp: Value<'js>, message: Opt<Value<'js>>) -> Result<()> {
232 let Some(re) = regexp.as_object() else {
233 return Err(rquickjs::Exception::throw_type(
234 &ctx,
235 "The \"regexp\" argument must be an instance of RegExp",
236 ));
237 };
238 let passed = !regexp_test(re, &subject)?;
239 compare(
240 &ctx,
241 subject,
242 regexp.clone(),
243 message,
244 passed,
245 "doesNotMatch",
246 "The input was expected to not match the regular expression:",
247 )
248}
249
250fn thrown_matches<'js>(ctx: &Ctx<'js>, error: &Value<'js>, expected: &Value<'js>) -> Result<bool> {
254 let Some(expected_obj) = expected.as_object() else {
255 return Ok(true);
256 };
257
258 if expected.is_function() {
259 let instance_of: Function<'js> = ctx.eval("(e, C) => e instanceof C")?;
260 return instance_of.call((error.clone(), expected.clone()));
261 }
262
263 if regexp_source_present(expected_obj)? {
264 let message: Value<'js> = error
265 .as_object()
266 .map_or_else(|| Ok(error.clone()), |o| o.get::<_, Value<'js>>("message"))?;
267 return regexp_test(expected_obj, &message);
268 }
269
270 let Some(error_obj) = error.as_object() else {
272 return Ok(false);
273 };
274 for key in expected_obj.keys::<String>() {
275 let key = key?;
276 let want: Value<'js> = expected_obj.get(key.as_str())?;
277 let got: Value<'js> = error_obj.get(key.as_str())?;
278 if !deep_equal(&got, &want, Mode::Strict)? {
279 return Ok(false);
280 }
281 }
282 Ok(true)
283}
284
285fn regexp_source_present(object: &Object<'_>) -> Result<bool> {
286 Ok(object.get::<_, Value<'_>>("source").is_ok_and(|v| v.is_string()) && object.get::<_, Value<'_>>("test").is_ok())
287}
288
289fn throws<'js>(ctx: Ctx<'js>, body: Function<'js>, rest: Rest<Value<'js>>) -> Result<()> {
290 let (expected, message) = split_expectation(&rest.0);
291 match body.call::<_, Value<'js>>(()) {
292 Err(_) => {
293 let caught = ctx.catch();
294 if let Some(expected) = expected {
295 if !thrown_matches(&ctx, &caught, &expected)? {
296 return Err(fail_with(
297 &ctx,
298 message,
299 format!("The error did not match the expectation: {}", render(&caught)),
300 caught,
301 expected,
302 "throws",
303 ));
304 }
305 }
306 Ok(())
307 },
308 Ok(_) => Err(fail_with(
309 &ctx,
310 message,
311 "Missing expected exception.".to_string(),
312 Value::new_undefined(ctx.clone()),
313 expected.unwrap_or_else(|| Value::new_undefined(ctx.clone())),
314 "throws",
315 )),
316 }
317}
318
319fn does_not_throw<'js>(ctx: Ctx<'js>, body: Function<'js>, rest: Rest<Value<'js>>) -> Result<()> {
320 let (_, message) = split_expectation(&rest.0);
321 match body.call::<_, Value<'js>>(()) {
322 Ok(_) => Ok(()),
323 Err(_) => {
324 let caught = ctx.catch();
325 Err(fail_with(
326 &ctx,
327 message,
328 format!("Got unwanted exception: {}", render(&caught)),
329 caught,
330 Value::new_undefined(ctx.clone()),
331 "doesNotThrow",
332 ))
333 },
334 }
335}
336
337fn split_expectation<'js>(rest: &[Value<'js>]) -> (Option<Value<'js>>, Opt<Value<'js>>) {
340 match rest {
341 [] => (None, Opt(None)),
342 [only] if only.is_string() => (None, Opt(Some(only.clone()))),
343 [only] => (Some(only.clone()), Opt(None)),
344 [first, second, ..] => (Some(first.clone()), Opt(Some(second.clone()))),
345 }
346}
347
348async fn rejects<'js>(ctx: Ctx<'js>, subject: Value<'js>, rest: Rest<Value<'js>>) -> Result<()> {
349 let (expected, message) = split_expectation(&rest.0);
350 match await_subject(&ctx, subject).await {
351 Err(_) => {
352 let caught = ctx.catch();
353 if let Some(expected) = expected {
354 if !thrown_matches(&ctx, &caught, &expected)? {
355 return Err(fail_with(
356 &ctx,
357 message,
358 format!("The rejection did not match the expectation: {}", render(&caught)),
359 caught,
360 expected,
361 "rejects",
362 ));
363 }
364 }
365 Ok(())
366 },
367 Ok(()) => Err(fail_with(
368 &ctx,
369 message,
370 "Missing expected rejection.".to_string(),
371 Value::new_undefined(ctx.clone()),
372 expected.unwrap_or_else(|| Value::new_undefined(ctx.clone())),
373 "rejects",
374 )),
375 }
376}
377
378async fn does_not_reject<'js>(ctx: Ctx<'js>, subject: Value<'js>, rest: Rest<Value<'js>>) -> Result<()> {
379 let (_, message) = split_expectation(&rest.0);
380 match await_subject(&ctx, subject).await {
381 Ok(()) => Ok(()),
382 Err(_) => {
383 let caught = ctx.catch();
384 Err(fail_with(
385 &ctx,
386 message,
387 format!("Got unwanted rejection: {}", render(&caught)),
388 caught,
389 Value::new_undefined(ctx.clone()),
390 "doesNotReject",
391 ))
392 },
393 }
394}
395
396async fn await_subject<'js>(ctx: &Ctx<'js>, subject: Value<'js>) -> Result<()> {
398 let promise: Value<'js> = if let Some(f) = subject.as_function() {
399 f.call(())?
400 } else {
401 subject
402 };
403 match promise.into_promise() {
404 Some(p) => p.into_future::<Value<'js>>().await.map(|_| ()),
405 None => Err(rquickjs::Exception::throw_type(
406 ctx,
407 "The \"promiseFn\" argument must be a function or a Promise",
408 )),
409 }
410}
411
412fn fail<'js>(ctx: Ctx<'js>, message: Opt<Value<'js>>) -> Result<()> {
413 Err(fail_with(
414 &ctx,
415 message,
416 "Failed".to_string(),
417 Value::new_undefined(ctx.clone()),
418 Value::new_undefined(ctx.clone()),
419 "fail",
420 ))
421}
422
423fn if_error<'js>(ctx: Ctx<'js>, value: Value<'js>) -> Result<()> {
424 if value.is_null() || value.is_undefined() {
425 return Ok(());
426 }
427 let rendered = render(&value);
428 Err(fail_with(
429 &ctx,
430 Opt(None),
431 format!("ifError got unwanted exception: {rendered}"),
432 value,
433 Value::new_null(ctx.clone()),
434 "ifError",
435 ))
436}
437
438pub const ASSERT_MEMBERS: &[&str] = &[
440 "deepEqual",
441 "deepStrictEqual",
442 "doesNotMatch",
443 "doesNotReject",
444 "doesNotThrow",
445 "equal",
446 "fail",
447 "ifError",
448 "match",
449 "notDeepEqual",
450 "notDeepStrictEqual",
451 "notEqual",
452 "notStrictEqual",
453 "ok",
454 "rejects",
455 "strict",
456 "strictEqual",
457 "throws",
458];
459
460fn install_members<'js>(target: &Object<'js>, strict_mode: bool) -> Result<()> {
461 target.set("ok", Func::from(ok))?;
462 target.set("fail", Func::from(fail))?;
463 target.set("ifError", Func::from(if_error))?;
464 target.set("match", Func::from(matches))?;
465 target.set("doesNotMatch", Func::from(does_not_match))?;
466 target.set("throws", Func::from(throws))?;
467 target.set("doesNotThrow", Func::from(does_not_throw))?;
468 target.set("rejects", Func::from(Async(rejects)))?;
469 target.set("doesNotReject", Func::from(Async(does_not_reject)))?;
470 target.set("strictEqual", Func::from(strict_eq))?;
471 target.set("notStrictEqual", Func::from(not_strict_eq))?;
472 target.set("deepStrictEqual", Func::from(deep_strict_eq))?;
473 target.set("notDeepStrictEqual", Func::from(not_deep_strict_eq))?;
474
475 if strict_mode {
478 target.set("equal", Func::from(strict_eq))?;
479 target.set("notEqual", Func::from(not_strict_eq))?;
480 target.set("deepEqual", Func::from(deep_strict_eq))?;
481 target.set("notDeepEqual", Func::from(not_deep_strict_eq))?;
482 } else {
483 target.set("equal", Func::from(equal))?;
484 target.set("notEqual", Func::from(not_equal))?;
485 target.set("deepEqual", Func::from(deep_eq))?;
486 target.set("notDeepEqual", Func::from(not_deep_eq))?;
487 }
488 Ok(())
489}
490
491pub fn assert_object<'js>(ctx: &Ctx<'js>, strict_mode: bool) -> Result<Object<'js>> {
498 let callable = Function::new(ctx.clone(), ok)?.with_name("assert")?;
499 let object = callable
500 .as_object()
501 .cloned()
502 .ok_or_else(|| rquickjs::Error::new_loading("assert"))?;
503 install_members(&object, strict_mode)?;
504
505 if strict_mode {
506 object.set("strict", object.clone())?;
507 } else {
508 let strict = Function::new(ctx.clone(), ok)?.with_name("assert")?;
509 let strict_object = strict
510 .as_object()
511 .cloned()
512 .ok_or_else(|| rquickjs::Error::new_loading("assert"))?;
513 install_members(&strict_object, true)?;
514 strict_object.set("strict", strict_object.clone())?;
515 object.set("strict", strict_object)?;
516 }
517 Ok(object)
518}
519
520type _AsyncMarker<'js> = Promise<'js>;