pipa-js 0.1.1

A fast, minimal ES2023 JavaScript runtime built in Rust.
Documentation
use pipa::{JSRuntime, eval};

fn assert_js_ok(ctx: &mut pipa::JSContext, code: &str, msg: &str) {
    let r = eval(ctx, code);
    assert!(r.is_ok(), "{}: {:?}", msg, r);
}

#[test]
fn test_string_length() {
    let mut runtime = JSRuntime::new();
    let mut ctx = runtime.new_context();

    assert!(eval(&mut ctx, "'hello'.length").is_ok());
    assert_js_ok(
        &mut ctx,
        "if ('hello'.length !== 5) throw new Error('length hello mismatch'); if (''.length !== 0) throw new Error('length empty mismatch');",
        "string length failed",
    );
}

#[test]
fn test_string_char_at() {
    let mut runtime = JSRuntime::new();
    let mut ctx = runtime.new_context();

    assert!(eval(&mut ctx, "'hello'.charAt(1)").is_ok());
}

#[test]
fn test_string_concat() {
    let mut runtime = JSRuntime::new();
    let mut ctx = runtime.new_context();

    assert!(eval(&mut ctx, "'hello'.concat(' ', 'world')").is_ok());
}

#[test]
fn test_string_index_of() {
    let mut runtime = JSRuntime::new();
    let mut ctx = runtime.new_context();

    assert!(eval(&mut ctx, "'hello'.indexOf('l')").is_ok());
}

#[test]
fn test_string_to_uppercase() {
    let mut runtime = JSRuntime::new();
    let mut ctx = runtime.new_context();

    assert!(eval(&mut ctx, "'hello'.toUpperCase()").is_ok());
}

#[test]
fn test_string_to_lowercase() {
    let mut runtime = JSRuntime::new();
    let mut ctx = runtime.new_context();

    assert!(eval(&mut ctx, "'HELLO'.toLowerCase()").is_ok());
}

#[test]
fn test_string_substring() {
    let mut runtime = JSRuntime::new();
    let mut ctx = runtime.new_context();

    assert!(eval(&mut ctx, "'hello'.substring(1, 4)").is_ok());
}

#[test]
fn test_string_methods() {
    let mut runtime = JSRuntime::new();
    let mut ctx = runtime.new_context();

    assert!(eval(&mut ctx, "'hello'.charAt(0)").is_ok());
    assert!(eval(&mut ctx, "'hello'.indexOf('l')").is_ok());
    assert!(eval(&mut ctx, "'HELLO'.toLowerCase()").is_ok());
    assert!(eval(&mut ctx, "'hello'.lastIndexOf('l')").is_ok());
    assert!(eval(&mut ctx, "'hello'.split('')").is_ok());
}

#[test]
fn test_string_trim() {
    let mut runtime = JSRuntime::new();
    let mut ctx = runtime.new_context();

    assert!(eval(&mut ctx, "'  hello  '.trim()").is_ok());
}

#[test]
fn test_string_starts_with() {
    let mut runtime = JSRuntime::new();
    let mut ctx = runtime.new_context();

    assert_js_ok(
        &mut ctx,
        "if ('hello'.startsWith('hel') !== true) throw new Error('startsWith true mismatch'); if ('hello'.startsWith('world') !== false) throw new Error('startsWith false mismatch');",
        "string startsWith failed",
    );
}

#[test]
fn test_string_ends_with() {
    let mut runtime = JSRuntime::new();
    let mut ctx = runtime.new_context();

    assert_js_ok(
        &mut ctx,
        "if ('hello'.endsWith('llo') !== true) throw new Error('endsWith true mismatch'); if ('hello'.endsWith('world') !== false) throw new Error('endsWith false mismatch');",
        "string endsWith failed",
    );
}

#[test]
fn test_string_repeat() {
    let mut runtime = JSRuntime::new();
    let mut ctx = runtime.new_context();

    let result = eval(&mut ctx, "'abc'.repeat(3)");
    assert!(result.is_ok());
}

#[test]
fn test_string_includes() {
    let mut runtime = JSRuntime::new();
    let mut ctx = runtime.new_context();

    assert_js_ok(
        &mut ctx,
        "if ('hello'.includes('ell') !== true) throw new Error('includes true mismatch'); if ('hello'.includes('world') !== false) throw new Error('includes false mismatch');",
        "string includes failed",
    );
}

#[test]
fn test_string_replace() {
    let mut runtime = JSRuntime::new();
    let mut ctx = runtime.new_context();

    let result = eval(&mut ctx, "'hello world'.replace('world', 'rust')");
    assert!(result.is_ok());
}

#[test]
fn test_string_pad_start() {
    let mut runtime = JSRuntime::new();
    let mut ctx = runtime.new_context();

    let result = eval(&mut ctx, "'abc'.padStart(5)");
    assert!(result.is_ok());

    let result = eval(&mut ctx, "'abc'.padStart(5, 'x')");
    assert!(result.is_ok());

    let result = eval(&mut ctx, "'abc'.padStart(10, '123')");
    assert!(result.is_ok());
}

#[test]
fn test_string_pad_end() {
    let mut runtime = JSRuntime::new();
    let mut ctx = runtime.new_context();

    let result = eval(&mut ctx, "'abc'.padEnd(5)");
    assert!(result.is_ok());

    let result = eval(&mut ctx, "'abc'.padEnd(5, 'x')");
    assert!(result.is_ok());

    let result = eval(&mut ctx, "'abc'.padEnd(10, '123')");
    assert!(result.is_ok());
}

#[test]
fn test_template_literal_basic() {
    let mut runtime = JSRuntime::new();
    let mut ctx = runtime.new_context();

    assert_js_ok(
        &mut ctx,
        "var name = 'World'; var s = `Hello ${name}!`; if (s !== 'Hello World!') throw new Error('template basic mismatch');",
        "template literal basic failed",
    );
}

#[test]
fn test_template_literal_expression() {
    let mut runtime = JSRuntime::new();
    let mut ctx = runtime.new_context();

    assert_js_ok(
        &mut ctx,
        "var x = 3; var y = 4; var s = `sum is ${x + y}`; if (s !== 'sum is 7') throw new Error('template expression mismatch');",
        "template literal expression failed",
    );
}

#[test]
fn test_template_literal_no_interpolation() {
    let mut runtime = JSRuntime::new();
    let mut ctx = runtime.new_context();

    assert_js_ok(
        &mut ctx,
        "var s = `hello world`; if (s !== 'hello world') throw new Error('template plain mismatch');",
        "template literal no interpolation failed",
    );
}

#[test]
fn test_template_literal_multiple_interpolations() {
    let mut runtime = JSRuntime::new();
    let mut ctx = runtime.new_context();

    assert_js_ok(
        &mut ctx,
        "var a = 1; var b = 2; var c = 3; var s = `${a} + ${b} = ${c}`; if (s !== '1 + 2 = 3') throw new Error('template multi mismatch');",
        "template literal multiple interpolations failed",
    );
}

#[test]
fn test_template_escape_sequences() {
    let mut runtime = JSRuntime::new();
    let mut ctx = runtime.new_context();

    assert_js_ok(
        &mut ctx,
        r#"var s = `tab:\there`; if (s !== 'tab:\there') throw new Error('template escape tab mismatch: ' + s);"#,
        "template escape tab",
    );
}

#[test]
fn test_template_escape_newline() {
    let mut runtime = JSRuntime::new();
    let mut ctx = runtime.new_context();

    assert_js_ok(
        &mut ctx,
        r#"var s = `line1\nline2`; if (s !== 'line1\nline2') throw new Error('newline mismatch: ' + s);"#,
        "template escape newline",
    );
}

#[test]
fn test_template_escape_backtick() {
    let mut runtime = JSRuntime::new();
    let mut ctx = runtime.new_context();

    assert_js_ok(
        &mut ctx,
        r#"var s = `a\`b`; if (s !== 'a`b') throw new Error('backtick escape mismatch: ' + s);"#,
        "template escape backtick",
    );
}

#[test]
fn test_template_escape_dollar() {
    let mut runtime = JSRuntime::new();
    let mut ctx = runtime.new_context();

    assert_js_ok(
        &mut ctx,
        r#"var s = `price: \${42}`; if (s !== 'price: ${42}') throw new Error('dollar escape mismatch: ' + s);"#,
        "template escape dollar",
    );
}

#[test]
fn test_template_multiline() {
    let mut runtime = JSRuntime::new();
    let mut ctx = runtime.new_context();

    assert_js_ok(
        &mut ctx,
        "var s = `line1\nline2`; if (s.length < 10) throw new Error('multiline length');",
        "template multiline",
    );
}

#[test]
fn test_template_adjacent_interpolations() {
    let mut runtime = JSRuntime::new();
    let mut ctx = runtime.new_context();

    assert_js_ok(
        &mut ctx,
        "var a = 'x'; var b = 'y'; var s = `${a}${b}`; if (s !== 'xy') throw new Error('adjacent interp mismatch: ' + s);",
        "template adjacent interpolations",
    );
}

#[test]
fn test_template_nested() {
    let mut runtime = JSRuntime::new();
    let mut ctx = runtime.new_context();

    assert_js_ok(
        &mut ctx,
        "var x = 1; var s = `outer ${`inner ${x}`} end`; if (s !== 'outer inner 1 end') throw new Error('nested template mismatch: ' + s);",
        "template nested",
    );
}

#[test]
fn test_template_with_object_literal() {
    let mut runtime = JSRuntime::new();
    let mut ctx = runtime.new_context();

    assert_js_ok(
        &mut ctx,
        "var obj = {a: 1}; var s = `val: ${obj.a}`; if (s !== 'val: 1') throw new Error('obj interp mismatch: ' + s);",
        "template with object member",
    );
}

#[test]
fn test_template_with_ternary() {
    let mut runtime = JSRuntime::new();
    let mut ctx = runtime.new_context();

    assert_js_ok(
        &mut ctx,
        "var s = `${true ? 'yes' : 'no'}`; if (s !== 'yes') throw new Error('ternary mismatch: ' + s);",
        "template with ternary",
    );
}

#[test]
fn test_template_empty() {
    let mut runtime = JSRuntime::new();
    let mut ctx = runtime.new_context();

    assert_js_ok(
        &mut ctx,
        "var s = ``; if (s !== '') throw new Error('empty template mismatch');",
        "template empty",
    );
}

#[test]
fn test_string_concatenation() {
    let mut runtime = JSRuntime::new();
    let mut ctx = runtime.new_context();

    assert_js_ok(
        &mut ctx,
        "var a = 'hello' + ' ' + 'world'; if (a !== 'hello world') throw new Error('concat mismatch'); var b = 'num: ' + 42; if (b !== 'num: 42') throw new Error('concat num mismatch');",
        "string concatenation failed",
    );
}

#[test]
fn test_string_from_code_point() {
    let mut runtime = JSRuntime::new();
    let mut ctx = runtime.new_context();

    assert_js_ok(
        &mut ctx,
        "var s = String.fromCodePoint(42); if (s !== '*') throw new Error('fromCodePoint mismatch');",
        "string fromCodePoint failed",
    );
}

#[test]
fn test_string_code_point_at() {
    let mut runtime = JSRuntime::new();
    let mut ctx = runtime.new_context();

    assert_js_ok(
        &mut ctx,
        "if ('hello'.codePointAt(1) !== 101) throw new Error('cp1'); if ('hello'.codePointAt(0) !== 104) throw new Error('cp0'); if ('hello'.codePointAt(4) !== 111) throw new Error('cp4'); if ('😀'.codePointAt(0) !== 128512) throw new Error('cp emoji'); if ('hello'.codePointAt(-1) !== 111) throw new Error('cp neg'); if ('hello'.codePointAt(10) !== undefined) throw new Error('cp oob');",
        "string codePointAt failed",
    );
}

#[test]
fn test_string_match_all() {
    let mut runtime = JSRuntime::new();
    let mut ctx = runtime.new_context();

    assert_js_ok(
        &mut ctx,
        "if ('hello hello'.matchAll('hello').length !== 2) throw new Error('matchAll len'); if ('ab ab'.matchAll('ab')[0][0] !== 'ab') throw new Error('matchAll token'); if ('ab ab'.matchAll('ab')[0].index !== 0) throw new Error('matchAll index'); if ('test'.matchAll('t')[0].input !== 'test') throw new Error('matchAll input'); if ('hello'.matchAll('').length !== 0) throw new Error('matchAll empty');",
        "string matchAll failed",
    );
}