obeli-sk-boa-runtime 1.0.0-obeli-sk.7

Example runtime for the Boa JavaScript engine.
Documentation
use crate::test::{TestAction, run_test_actions};

#[test]
fn abort_controller_exists() {
    run_test_actions([
        TestAction::run("let ctrl = new AbortController()"),
        TestAction::run("let sig = ctrl.signal"),
    ]);
}

#[test]
fn signal_not_aborted_initially() {
    run_test_actions([TestAction::run(
        r"
        let ctrl = new AbortController();
        if (ctrl.signal.aborted !== false) {
            throw new Error('signal should not be aborted initially');
        }
        ",
    )]);
}

#[test]
fn abort_sets_aborted() {
    run_test_actions([TestAction::run(
        r"
        let ctrl = new AbortController();
        ctrl.abort();
        if (ctrl.signal.aborted !== true) {
            throw new Error('signal should be aborted after abort()');
        }
        ",
    )]);
}

#[test]
fn abort_with_custom_reason() {
    run_test_actions([TestAction::run(
        r#"
        let ctrl = new AbortController();
        ctrl.abort("custom reason");
        if (ctrl.signal.reason !== "custom reason") {
            throw new Error('reason should be "custom reason", got: ' + ctrl.signal.reason);
        }
        "#,
    )]);
}

#[test]
fn abort_default_reason() {
    run_test_actions([TestAction::run(
        r#"
        let ctrl = new AbortController();
        ctrl.abort();
        if (ctrl.signal.reason.name !== "AbortError") {
            throw new Error('default reason.name should be "AbortError", got: ' + ctrl.signal.reason.name);
        }
        "#,
    )]);
}

#[test]
fn throw_if_aborted_does_nothing_when_not_aborted() {
    run_test_actions([TestAction::run(
        r"
        let ctrl = new AbortController();
        ctrl.signal.throwIfAborted();
        ",
    )]);
}

#[test]
fn throw_if_aborted_throws_when_aborted() {
    run_test_actions([TestAction::run(
        r#"
        let ctrl = new AbortController();
        ctrl.abort("test error");
        let threw = false;
        try {
            ctrl.signal.throwIfAborted();
        } catch (e) {
            threw = true;
            if (e !== "test error") {
                throw new Error('expected "test error", got: ' + e);
            }
        }
        if (!threw) {
            throw new Error('throwIfAborted should have thrown');
        }
        "#,
    )]);
}

#[test]
fn add_event_listener_fires_on_abort() {
    run_test_actions([
        TestAction::run(
            r"
            let ctrl = new AbortController();
            let called = false;
            ctrl.signal.addEventListener('abort', function() {
                called = true;
            });
            ctrl.abort();
            ",
        ),
        TestAction::inspect_context(|ctx| {
            ctx.run_jobs().unwrap();
        }),
        TestAction::run(
            r"
            if (!called) {
                throw new Error('abort listener was not called');
            }
            ",
        ),
    ]);
}

#[test]
fn add_event_listener_ignores_unknown_event_names() {
    run_test_actions([
        TestAction::run(
            r"
            let ctrl = new AbortController();
            let called = false;
            ctrl.signal.addEventListener('nope', function() {
                called = true;
            });
            ctrl.abort();
            ",
        ),
        TestAction::inspect_context(|ctx| {
            ctx.run_jobs().unwrap();
        }),
        TestAction::run(
            r"
            if (called) {
                throw new Error('unknown event listener should not fire');
            }
            ",
        ),
    ]);
}

#[test]
fn add_event_listener_does_not_fire_abort_listener_added_after_abort() {
    run_test_actions([
        TestAction::run(
            r"
            let ctrl = new AbortController();
            ctrl.abort();

            let called = false;
            ctrl.signal.addEventListener('abort', function() {
                called = true;
            });
            ",
        ),
        TestAction::inspect_context(|ctx| {
            ctx.run_jobs().unwrap();
        }),
        TestAction::run(
            r"
            if (called) {
                throw new Error('abort listener should not fire when added after abort');
            }
            ",
        ),
    ]);
}

#[test]
fn add_event_listener_deduplicates_same_type_and_callback() {
    run_test_actions([
        TestAction::run(
            r"
            let ctrl = new AbortController();
            let count = 0;

            function onAbort() {
                count += 1;
            }

            ctrl.signal.addEventListener('abort', onAbort);
            ctrl.signal.addEventListener('abort', onAbort);
            ctrl.abort();
            ",
        ),
        TestAction::inspect_context(|ctx| {
            ctx.run_jobs().unwrap();
        }),
        TestAction::run(
            r"
            if (count !== 1) {
                throw new Error('expected duplicate abort listener to fire once, got: ' + count);
            }
            ",
        ),
    ]);
}

#[test]
fn remove_event_listener_uses_event_type() {
    run_test_actions([
        TestAction::run(
            r"
            let ctrl = new AbortController();
            let count = 0;

            function listener() {
                count += 1;
            }

            ctrl.signal.addEventListener('nope', listener);
            ctrl.signal.addEventListener('abort', listener);
            ctrl.signal.removeEventListener('nope', listener);
            ctrl.abort();
            ",
        ),
        TestAction::inspect_context(|ctx| {
            ctx.run_jobs().unwrap();
        }),
        TestAction::run(
            r"
            if (count !== 1) {
                throw new Error('removing a different event type should not remove the abort listener');
            }
            ",
        ),
    ]);
}

#[test]
fn multiple_listeners() {
    run_test_actions([
        TestAction::run(
            r"
            let ctrl = new AbortController();
            let count = 0;
            ctrl.signal.addEventListener('abort', function() { count += 1; });
            ctrl.signal.addEventListener('abort', function() { count += 1; });
            ctrl.signal.addEventListener('abort', function() { count += 1; });
            ctrl.abort();
            ",
        ),
        TestAction::inspect_context(|ctx| {
            ctx.run_jobs().unwrap();
        }),
        TestAction::run(
            r"
            if (count !== 3) {
                throw new Error('expected 3 listeners called, got: ' + count);
            }
            ",
        ),
    ]);
}

#[test]
fn repeated_abort_is_idempotent() {
    run_test_actions([
        TestAction::run(
            r"
            let ctrl = new AbortController();
            let count = 0;
            ctrl.signal.addEventListener('abort', function() { count += 1; });
            ctrl.abort();
            ctrl.abort();
            ctrl.abort();
            ",
        ),
        TestAction::inspect_context(|ctx| {
            ctx.run_jobs().unwrap();
        }),
        TestAction::run(
            r"
            if (count !== 1) {
                throw new Error('listeners should fire only once, got: ' + count);
            }
            ",
        ),
    ]);
}

#[test]
fn signal_reuse_across_references() {
    run_test_actions([TestAction::run(
        r"
        let ctrl = new AbortController();
        let sig1 = ctrl.signal;
        let sig2 = ctrl.signal;
        ctrl.abort();
        if (sig1.aborted !== true || sig2.aborted !== true) {
            throw new Error('both signal references should be aborted');
        }
        ",
    )]);
}

#[test]
fn reason_undefined_when_not_aborted() {
    run_test_actions([TestAction::run(
        r"
        let ctrl = new AbortController();
        if (ctrl.signal.reason !== undefined) {
            throw new Error('reason should be undefined before abort, got: ' + ctrl.signal.reason);
        }
        ",
    )]);
}

#[test]
fn reason_abort_error_when_aborted_without_reason() {
    run_test_actions([TestAction::run(
        r#"
        let ctrl = new AbortController();
        ctrl.abort();
        if (!(ctrl.signal.reason instanceof Error)) {
            throw new Error('reason should be an instance of Error');
        }
        if (ctrl.signal.reason.name !== "AbortError") {
            throw new Error('reason.name should be "AbortError", got: ' + ctrl.signal.reason.name);
        }
        "#,
    )]);
}