use super::{Console, ConsoleState, formatter};
use crate::test::{TestAction, run_test_actions, run_test_actions_with};
use crate::{Logger, NullLogger};
use boa_engine::{Context, JsError, JsResult, JsValue, js_string, property::Attribute};
use boa_gc::{Gc, GcRefCell};
use indoc::indoc;
#[test]
fn formatter_no_args_is_empty_string() {
run_test_actions([TestAction::inspect_context(|ctx| {
assert_eq!(formatter(&[], ctx).unwrap(), "");
})]);
}
#[test]
fn formatter_empty_format_string_is_empty_string() {
run_test_actions([TestAction::inspect_context(|ctx| {
assert_eq!(formatter(&[JsValue::new(js_string!())], ctx).unwrap(), "");
})]);
}
#[test]
fn formatter_format_without_args_renders_verbatim() {
run_test_actions([TestAction::inspect_context(|ctx| {
assert_eq!(
formatter(&[JsValue::new(js_string!("%d %s %% %f"))], ctx).unwrap(),
"%d %s %% %f"
);
})]);
}
#[test]
fn formatter_empty_format_string_concatenates_rest_of_args() {
run_test_actions([TestAction::inspect_context(|ctx| {
assert_eq!(
formatter(
&[
JsValue::new(js_string!("")),
JsValue::new(js_string!("to powinno zostać")),
JsValue::new(js_string!("połączone")),
],
ctx
)
.unwrap(),
" to powinno zostać połączone"
);
})]);
}
#[test]
fn formatter_utf_8_checks() {
run_test_actions([TestAction::inspect_context(|ctx| {
assert_eq!(
formatter(
&[
JsValue::new(js_string!("Są takie chwile %dą %są tu%sów %привет%ź")),
JsValue::new(123),
JsValue::new(1.23),
JsValue::new(js_string!("ł")),
],
ctx
)
.unwrap(),
"Są takie chwile 123ą 1.23ą tułów %привет%ź"
);
})]);
}
#[test]
fn formatter_trailing_format_leader_renders() {
run_test_actions([TestAction::inspect_context(|ctx| {
assert_eq!(
formatter(
&[
JsValue::new(js_string!("%%%%%")),
JsValue::new(js_string!("|"))
],
ctx
)
.unwrap(),
"%%% |"
);
})]);
}
#[test]
#[allow(clippy::approx_constant)]
fn formatter_float_format_works() {
run_test_actions([TestAction::inspect_context(|ctx| {
assert_eq!(
formatter(&[JsValue::new(js_string!("%f")), JsValue::new(3.1415)], ctx).unwrap(),
"3.141500"
);
})]);
}
#[test]
fn console_log_cyclic() {
let mut context = Context::default();
let console = Console::init_with_logger(NullLogger, &mut context);
context
.register_global_property(Console::NAME, console, Attribute::all())
.unwrap();
run_test_actions_with(
[TestAction::run(indoc! {r#"
let a = [1];
a[1] = a;
console.log(a);
"#})],
&mut context,
);
}
#[derive(Clone, Debug, Default, boa_engine::Trace, boa_engine::Finalize)]
pub(crate) struct RecordingLogger {
pub log: Gc<GcRefCell<String>>,
}
impl Logger for RecordingLogger {
fn log(&self, msg: String, state: &ConsoleState, _: &mut Context) -> JsResult<()> {
use std::fmt::Write;
let indent = state.indent();
writeln!(self.log.borrow_mut(), "{msg:>indent$}").map_err(JsError::from_rust)
}
fn info(&self, msg: String, state: &ConsoleState, context: &mut Context) -> JsResult<()> {
self.log(msg, state, context)
}
fn warn(&self, msg: String, state: &ConsoleState, context: &mut Context) -> JsResult<()> {
self.log(msg, state, context)
}
fn error(&self, msg: String, state: &ConsoleState, context: &mut Context) -> JsResult<()> {
self.log(msg, state, context)
}
}
const TEST_HARNESS: &str = r#"
function assert_true(condition, message) {
if (!condition) {
throw new Error(`Assertion failed: ${message}`);
}
}
function assert_own_property(obj, prop) {
assert_true(
Object.prototype.hasOwnProperty.call(obj, prop),
`Expected ${prop.toString()} to be an own property`,
);
}
function assert_equals(actual, expected, message) {
assert_true(
actual === expected,
`${message} (actual: ${actual.toString()}, expected: ${expected.toString()})`,
);
}
function assert_throws_js(error, func) {
try {
func();
} catch (e) {
if (e instanceof error) {
return;
}
throw new Error(`Expected ${error.name} to be thrown, but got ${e.name}`);
}
throw new Error(`Expected ${error.name} to be thrown, but no exception was thrown`);
}
// To keep the tests as close to the WPT tests as possible, we define `self` to
// be `globalThis`.
const self = globalThis;
"#;
#[test]
fn wpt_log_symbol_any() {
let mut context = Context::default();
let logger = RecordingLogger::default();
Console::register_with_logger(logger.clone(), &mut context).unwrap();
run_test_actions_with(
[
TestAction::run(TEST_HARNESS),
TestAction::run(indoc! {r#"
console.log(Symbol());
console.log(Symbol("abc"));
console.log(Symbol.for("def"));
console.log(Symbol.isConcatSpreadable);
"#}),
],
&mut context,
);
let logs = logger.log.borrow().clone();
assert_eq!(
logs,
indoc! { r#"
Symbol()
Symbol(abc)
Symbol(def)
Symbol(Symbol.isConcatSpreadable)
"# }
);
}
#[test]
fn wpt_console_is_a_namespace() {
let mut context = Context::default();
let logger = RecordingLogger::default();
Console::register_with_logger(logger.clone(), &mut context).unwrap();
run_test_actions_with(
[
TestAction::run(TEST_HARNESS),
TestAction::run(indoc! {r#"
assert_true(globalThis.hasOwnProperty("console"));
"#}),
TestAction::run(indoc! {r#"
const propDesc = Object.getOwnPropertyDescriptor(self, "console");
assert_equals(propDesc.writable, true, "must be writable");
assert_equals(propDesc.enumerable, false, "must not be enumerable");
assert_equals(propDesc.configurable, true, "must be configurable");
assert_equals(propDesc.value, console, "must have the right value");
"#}),
TestAction::run(indoc! {r#"
const prototype1 = Object.getPrototypeOf(console);
const prototype2 = Object.getPrototypeOf(prototype1);
assert_equals(Object.getOwnPropertyNames(prototype1).length, 0, "The [[Prototype]] must have no properties");
assert_equals(prototype2, Object.prototype, "The [[Prototype]]'s [[Prototype]] must be %ObjectPrototype%");
"#}),
],
&mut context,
);
}
#[test]
fn wpt_console_label_conversion() {
let mut context = Context::default();
let logger = RecordingLogger::default();
Console::register_with_logger(logger.clone(), &mut context).unwrap();
run_test_actions_with(
[
TestAction::run(TEST_HARNESS),
TestAction::run(indoc! {r#"
const methods = ['count', 'countReset', 'time', 'timeLog', 'timeEnd'];
"#}),
TestAction::run(indoc! {r#"
for (const method of methods) {
let labelToStringCalled = false;
console[method]({
toString() {
labelToStringCalled = true;
}
});
assert_true(labelToStringCalled, `${method}() must call toString() on label when label is an object`);
}
"#}),
TestAction::run(indoc! {r#"
for (const method of methods) {
assert_throws_js(Error, () => {
console[method]({
toString() {
throw new Error('conversion error');
}
});
});
}
"#}),
],
&mut context,
);
}
#[test]
fn console_namespace_object_class_string() {
let mut context = Context::default();
let logger = RecordingLogger::default();
Console::register_with_logger(logger.clone(), &mut context).unwrap();
run_test_actions_with(
[
TestAction::run(TEST_HARNESS),
TestAction::run(indoc! {r#"
assert_own_property(console, Symbol.toStringTag);
const propDesc = Object.getOwnPropertyDescriptor(console, Symbol.toStringTag);
assert_equals(propDesc.value, "console", "value");
assert_equals(propDesc.writable, false, "writable");
assert_equals(propDesc.enumerable, false, "enumerable");
assert_equals(propDesc.configurable, true, "configurable");
"#}),
TestAction::run(indoc! {r#"
assert_equals(console.toString(), "[object console]");
assert_equals(Object.prototype.toString.call(console), "[object console]");
"#}),
TestAction::run(indoc! {r#"
assert_own_property(console, Symbol.toStringTag, "Precondition: @@toStringTag on the namespace object");
// t.add_cleanup(() => {
// Object.defineProperty(console, Symbol.toStringTag, { value: "console" });
// });
Object.defineProperty(console, Symbol.toStringTag, { value: "Test" });
assert_equals(console.toString(), "[object Test]");
assert_equals(Object.prototype.toString.call(console), "[object Test]");
"#}),
TestAction::run(indoc! {r#"
assert_own_property(console, Symbol.toStringTag, "Precondition: @@toStringTag on the namespace object");
// t.add_cleanup(() => {
// Object.defineProperty(console, Symbol.toStringTag, { value: "console" });
// });
assert_true(delete console[Symbol.toStringTag]);
assert_equals(console.toString(), "[object Object]");
assert_equals(Object.prototype.toString.call(console), "[object Object]");
"#}),
],
&mut context,
);
}
#[test]
fn trace_with_stack_trace() {
let mut context = Context::default();
let logger = RecordingLogger::default();
Console::register_with_logger(logger.clone(), &mut context).unwrap();
run_test_actions_with(
[
TestAction::run(TEST_HARNESS),
TestAction::run(indoc! {r#"
console.trace("one");
a();
function a() {
b();
}
function b() {
console.trace("two");
}
"#}),
],
&mut context,
);
let logs = logger.log.borrow().clone();
assert_eq!(
logs,
indoc! { r#"
one
<main>
two
b
a
<main>
"# }
);
}