1use rquickjs::function::{Async, Func, Opt, Rest};
10use rquickjs::{Ctx, Function, Object, Promise, Result, Value};
11
12use super::deep_equal::{deep_equal, loose_equal, strict_equal, Mode};
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(
109 &ctx,
110 actual,
111 expected,
112 message,
113 passed,
114 "==",
115 "Expected values to be loosely equal:",
116 )
117}
118
119fn not_equal<'js>(ctx: Ctx<'js>, actual: Value<'js>, expected: Value<'js>, message: Opt<Value<'js>>) -> Result<()> {
120 let passed = !loose_equal(&actual, &expected);
121 compare(
122 &ctx,
123 actual,
124 expected,
125 message,
126 passed,
127 "!=",
128 "Expected values not to be loosely equal:",
129 )
130}
131
132fn strict_eq<'js>(ctx: Ctx<'js>, actual: Value<'js>, expected: Value<'js>, message: Opt<Value<'js>>) -> Result<()> {
133 let passed = strict_equal(&actual, &expected);
134 compare(
135 &ctx,
136 actual,
137 expected,
138 message,
139 passed,
140 "strictEqual",
141 "Expected values to be strictly equal:",
142 )
143}
144
145fn not_strict_eq<'js>(ctx: Ctx<'js>, actual: Value<'js>, expected: Value<'js>, message: Opt<Value<'js>>) -> Result<()> {
146 let passed = !strict_equal(&actual, &expected);
147 compare(
148 &ctx,
149 actual,
150 expected,
151 message,
152 passed,
153 "notStrictEqual",
154 "Expected values not to be strictly equal:",
155 )
156}
157
158fn deep_eq<'js>(ctx: Ctx<'js>, actual: Value<'js>, expected: Value<'js>, message: Opt<Value<'js>>) -> Result<()> {
159 let passed = deep_equal(&actual, &expected, Mode::Loose)?;
160 compare(
161 &ctx,
162 actual,
163 expected,
164 message,
165 passed,
166 "deepEqual",
167 "Expected values to be loosely deep-equal:",
168 )
169}
170
171fn not_deep_eq<'js>(ctx: Ctx<'js>, actual: Value<'js>, expected: Value<'js>, message: Opt<Value<'js>>) -> Result<()> {
172 let passed = !deep_equal(&actual, &expected, Mode::Loose)?;
173 compare(
174 &ctx,
175 actual,
176 expected,
177 message,
178 passed,
179 "notDeepEqual",
180 "Expected values not to be loosely deep-equal:",
181 )
182}
183
184fn deep_strict_eq<'js>(
185 ctx: Ctx<'js>,
186 actual: Value<'js>,
187 expected: Value<'js>,
188 message: Opt<Value<'js>>,
189) -> Result<()> {
190 let passed = deep_equal(&actual, &expected, Mode::Strict)?;
191 compare(
192 &ctx,
193 actual,
194 expected,
195 message,
196 passed,
197 "deepStrictEqual",
198 "Expected values to be strictly deep-equal:",
199 )
200}
201
202fn not_deep_strict_eq<'js>(
203 ctx: Ctx<'js>,
204 actual: Value<'js>,
205 expected: Value<'js>,
206 message: Opt<Value<'js>>,
207) -> Result<()> {
208 let passed = !deep_equal(&actual, &expected, Mode::Strict)?;
209 compare(
210 &ctx,
211 actual,
212 expected,
213 message,
214 passed,
215 "notDeepStrictEqual",
216 "Expected values not to be strictly deep-equal:",
217 )
218}
219
220fn regexp_test<'js>(regexp: &Object<'js>, subject: &Value<'js>) -> Result<bool> {
221 let test: Function<'js> = regexp.get("test")?;
222 test.call((rquickjs::function::This(regexp.clone()), subject.clone()))
223}
224
225fn matches<'js>(ctx: Ctx<'js>, subject: Value<'js>, regexp: Value<'js>, message: Opt<Value<'js>>) -> Result<()> {
226 let Some(re) = regexp.as_object() else {
227 return Err(rquickjs::Exception::throw_type(
228 &ctx,
229 "The \"regexp\" argument must be an instance of RegExp",
230 ));
231 };
232 let passed = regexp_test(re, &subject)?;
233 compare(
234 &ctx,
235 subject,
236 regexp.clone(),
237 message,
238 passed,
239 "match",
240 "The input did not match the regular expression:",
241 )
242}
243
244fn does_not_match<'js>(ctx: Ctx<'js>, subject: Value<'js>, regexp: Value<'js>, message: Opt<Value<'js>>) -> Result<()> {
245 let Some(re) = regexp.as_object() else {
246 return Err(rquickjs::Exception::throw_type(
247 &ctx,
248 "The \"regexp\" argument must be an instance of RegExp",
249 ));
250 };
251 let passed = !regexp_test(re, &subject)?;
252 compare(
253 &ctx,
254 subject,
255 regexp.clone(),
256 message,
257 passed,
258 "doesNotMatch",
259 "The input was expected to not match the regular expression:",
260 )
261}
262
263fn thrown_matches<'js>(ctx: &Ctx<'js>, error: &Value<'js>, expected: &Value<'js>) -> Result<bool> {
267 let Some(expected_obj) = expected.as_object() else {
268 return Ok(true);
269 };
270
271 if expected.is_function() {
272 let matches: Function<'js> = ctx.eval(
273 "(error, expected) => {
274 if (expected.prototype !== undefined && error instanceof expected) return true;
275 if (Object.prototype.isPrototypeOf.call(Error, expected)) return false;
276 return expected.call({}, error) === true;
277 }",
278 )?;
279 return matches.call((error.clone(), expected.clone()));
280 }
281
282 if regexp_source_present(expected_obj)? {
283 let message: Value<'js> = error
284 .as_object()
285 .map_or_else(|| Ok(error.clone()), |o| o.get::<_, Value<'js>>("message"))?;
286 return regexp_test(expected_obj, &message);
287 }
288
289 let Some(error_obj) = error.as_object() else {
291 return Ok(false);
292 };
293 for key in expected_obj.keys::<String>() {
294 let key = key?;
295 let want: Value<'js> = expected_obj.get(key.as_str())?;
296 let got: Value<'js> = error_obj.get(key.as_str())?;
297 if !deep_equal(&got, &want, Mode::Strict)? {
298 return Ok(false);
299 }
300 }
301 Ok(true)
302}
303
304fn regexp_source_present(object: &Object<'_>) -> Result<bool> {
305 Ok(object.get::<_, Value<'_>>("source").is_ok_and(|v| v.is_string()) && object.get::<_, Value<'_>>("test").is_ok())
306}
307
308fn throws<'js>(ctx: Ctx<'js>, body: Function<'js>, rest: Rest<Value<'js>>) -> Result<()> {
309 let (expected, message) = split_expectation(&rest.0);
310 match body.call::<_, Value<'js>>(()) {
311 Err(_) => {
312 let caught = ctx.catch();
313 if let Some(expected) = expected {
314 if !thrown_matches(&ctx, &caught, &expected)? {
315 return Err(fail_with(
316 &ctx,
317 message,
318 format!("The error did not match the expectation: {}", render(&caught)),
319 caught,
320 expected,
321 "throws",
322 ));
323 }
324 }
325 Ok(())
326 }
327 Ok(_) => Err(fail_with(
328 &ctx,
329 message,
330 "Missing expected exception.".to_string(),
331 Value::new_undefined(ctx.clone()),
332 expected.unwrap_or_else(|| Value::new_undefined(ctx.clone())),
333 "throws",
334 )),
335 }
336}
337
338fn does_not_throw<'js>(ctx: Ctx<'js>, body: Function<'js>, rest: Rest<Value<'js>>) -> Result<()> {
339 let (_, message) = split_expectation(&rest.0);
340 match body.call::<_, Value<'js>>(()) {
341 Ok(_) => Ok(()),
342 Err(_) => {
343 let caught = ctx.catch();
344 Err(fail_with(
345 &ctx,
346 message,
347 format!("Got unwanted exception: {}", render(&caught)),
348 caught,
349 Value::new_undefined(ctx.clone()),
350 "doesNotThrow",
351 ))
352 }
353 }
354}
355
356fn split_expectation<'js>(rest: &[Value<'js>]) -> (Option<Value<'js>>, Opt<Value<'js>>) {
359 match rest {
360 [] => (None, Opt(None)),
361 [only] if only.is_string() => (None, Opt(Some(only.clone()))),
362 [only] => (Some(only.clone()), Opt(None)),
363 [first, second, ..] => (Some(first.clone()), Opt(Some(second.clone()))),
364 }
365}
366
367async fn rejects<'js>(ctx: Ctx<'js>, subject: Value<'js>, rest: Rest<Value<'js>>) -> Result<()> {
368 let (expected, message) = split_expectation(&rest.0);
369 match await_subject(&ctx, subject).await {
370 Err(_) => {
371 let caught = ctx.catch();
372 if let Some(expected) = expected {
373 if !thrown_matches(&ctx, &caught, &expected)? {
374 return Err(fail_with(
375 &ctx,
376 message,
377 format!("The rejection did not match the expectation: {}", render(&caught)),
378 caught,
379 expected,
380 "rejects",
381 ));
382 }
383 }
384 Ok(())
385 }
386 Ok(()) => Err(fail_with(
387 &ctx,
388 message,
389 "Missing expected rejection.".to_string(),
390 Value::new_undefined(ctx.clone()),
391 expected.unwrap_or_else(|| Value::new_undefined(ctx.clone())),
392 "rejects",
393 )),
394 }
395}
396
397async fn does_not_reject<'js>(ctx: Ctx<'js>, subject: Value<'js>, rest: Rest<Value<'js>>) -> Result<()> {
398 let (_, message) = split_expectation(&rest.0);
399 match await_subject(&ctx, subject).await {
400 Ok(()) => Ok(()),
401 Err(_) => {
402 let caught = ctx.catch();
403 Err(fail_with(
404 &ctx,
405 message,
406 format!("Got unwanted rejection: {}", render(&caught)),
407 caught,
408 Value::new_undefined(ctx.clone()),
409 "doesNotReject",
410 ))
411 }
412 }
413}
414
415async fn await_subject<'js>(ctx: &Ctx<'js>, subject: Value<'js>) -> Result<()> {
417 let promise: Value<'js> = if let Some(f) = subject.as_function() {
418 f.call(())?
419 } else {
420 subject
421 };
422 match promise.into_promise() {
423 Some(p) => p.into_future::<Value<'js>>().await.map(|_| ()),
424 None => Err(rquickjs::Exception::throw_type(
425 ctx,
426 "The \"promiseFn\" argument must be a function or a Promise",
427 )),
428 }
429}
430
431fn fail<'js>(ctx: Ctx<'js>, message: Opt<Value<'js>>) -> Result<()> {
432 Err(fail_with(
433 &ctx,
434 message,
435 "Failed".to_string(),
436 Value::new_undefined(ctx.clone()),
437 Value::new_undefined(ctx.clone()),
438 "fail",
439 ))
440}
441
442fn if_error<'js>(ctx: Ctx<'js>, value: Value<'js>) -> Result<()> {
443 if value.is_null() || value.is_undefined() {
444 return Ok(());
445 }
446 let rendered = render(&value);
447 Err(fail_with(
448 &ctx,
449 Opt(None),
450 format!("ifError got unwanted exception: {rendered}"),
451 value,
452 Value::new_null(ctx.clone()),
453 "ifError",
454 ))
455}
456
457pub const ASSERT_MEMBERS: &[&str] = &[
459 "deepEqual",
460 "deepStrictEqual",
461 "doesNotMatch",
462 "doesNotReject",
463 "doesNotThrow",
464 "equal",
465 "fail",
466 "ifError",
467 "match",
468 "notDeepEqual",
469 "notDeepStrictEqual",
470 "notEqual",
471 "notStrictEqual",
472 "ok",
473 "rejects",
474 "strict",
475 "strictEqual",
476 "throws",
477];
478
479fn install_members<'js>(target: &Object<'js>, strict_mode: bool) -> Result<()> {
480 target.set("ok", Func::from(ok))?;
481 target.set("fail", Func::from(fail))?;
482 target.set("ifError", Func::from(if_error))?;
483 target.set("match", Func::from(matches))?;
484 target.set("doesNotMatch", Func::from(does_not_match))?;
485 target.set("throws", Func::from(throws))?;
486 target.set("doesNotThrow", Func::from(does_not_throw))?;
487 target.set("rejects", Func::from(Async(rejects)))?;
488 target.set("doesNotReject", Func::from(Async(does_not_reject)))?;
489 target.set("strictEqual", Func::from(strict_eq))?;
490 target.set("notStrictEqual", Func::from(not_strict_eq))?;
491 target.set("deepStrictEqual", Func::from(deep_strict_eq))?;
492 target.set("notDeepStrictEqual", Func::from(not_deep_strict_eq))?;
493
494 if strict_mode {
497 target.set("equal", Func::from(strict_eq))?;
498 target.set("notEqual", Func::from(not_strict_eq))?;
499 target.set("deepEqual", Func::from(deep_strict_eq))?;
500 target.set("notDeepEqual", Func::from(not_deep_strict_eq))?;
501 } else {
502 target.set("equal", Func::from(equal))?;
503 target.set("notEqual", Func::from(not_equal))?;
504 target.set("deepEqual", Func::from(deep_eq))?;
505 target.set("notDeepEqual", Func::from(not_deep_eq))?;
506 }
507 Ok(())
508}
509
510pub fn assert_object<'js>(ctx: &Ctx<'js>, strict_mode: bool) -> Result<Object<'js>> {
517 let callable = Function::new(ctx.clone(), ok)?.with_name("assert")?;
518 let object = callable
519 .as_object()
520 .cloned()
521 .ok_or_else(|| rquickjs::Error::new_loading("assert"))?;
522 install_members(&object, strict_mode)?;
523
524 if strict_mode {
525 object.set("strict", object.clone())?;
526 } else {
527 let strict = Function::new(ctx.clone(), ok)?.with_name("assert")?;
528 let strict_object = strict
529 .as_object()
530 .cloned()
531 .ok_or_else(|| rquickjs::Error::new_loading("assert"))?;
532 install_members(&strict_object, true)?;
533 strict_object.set("strict", strict_object.clone())?;
534 object.set("strict", strict_object)?;
535 }
536 Ok(object)
537}
538
539type _AsyncMarker<'js> = Promise<'js>;