browser_tester 1.5.0

Deterministic lightweight browser runtime for Rust tests
Documentation
use super::*;

#[test]
fn async_generator_instances_support_protocol_tags_and_control_methods() -> Result<()> {
    let html = r#"
        <button id='run'>run</button>
        <p id='out'></p>
        <script>
          document.getElementById('run').addEventListener('click', () => {
            const AsyncGeneratorFunction = (async function* () {}).constructor;
            const AsyncGeneratorPrototype = AsyncGeneratorFunction.prototype.prototype;

            const iter = (async function* () {
              yield Promise.resolve(1);
              yield 2;
            })();
            const selfFactory = iter[Symbol.asyncIterator];
            const self = selfFactory();
            const constructorTag = iter.constructor[Symbol.toStringTag];
            const generatorTag = iter[Symbol.toStringTag];
            const prototypeConstructorMatches =
              AsyncGeneratorPrototype.constructor === AsyncGeneratorFunction.prototype;

            const firstPromise = iter.next();
            const returnPromise = iter.return(Promise.resolve(99));
            const afterReturnPromise = iter.next();

            const thrower = (async function* () {
              yield 'x';
              yield 'y';
            })();
            const thrownFirstPromise = thrower.next();
            const thrownPromise = thrower.throw('boom');
            const thrownReasonPromise = thrownPromise.then(
              () => 'noerr',
              (reason) => reason
            );
            const afterThrowPromise = thrower.next();

            Promise.all([
              firstPromise,
              returnPromise,
              afterReturnPromise,
              thrownFirstPromise,
              thrownReasonPromise,
              afterThrowPromise,
            ]).then((results) => {
              const first = results[0];
              const returned = results[1];
              const afterReturn = results[2];
              const thrownFirst = results[3];
              const thrownReason = results[4];
              const afterThrow = results[5];

              document.getElementById('out').textContent =
                first.value + ':' + first.done + '|' +
                returned.value + ':' + returned.done + ':' + afterReturn.done + '|' +
                thrownFirst.value + ':' + thrownReason + ':' + afterThrow.done + '|' +
                constructorTag + ':' + generatorTag + '|' +
                prototypeConstructorMatches + ':' + (self === iter) + '|' +
                (typeof firstPromise) + ':' +
                (typeof returnPromise) + ':' +
                (typeof thrownPromise);
            });
          });
        </script>
        "#;

    let mut h = Harness::from_html(html)?;
    h.click("#run")?;
    h.assert_text(
        "#out",
        "1:false|99:true:true|x:boom:true|AsyncGeneratorFunction:AsyncGenerator|true:true|object:object:object",
    )?;
    Ok(())
}

#[test]
fn async_generator_iteration_resolves_delayed_promises() -> Result<()> {
    let html = r#"
        <button id='run'>run</button>
        <p id='out'></p>
        <script>
          const delayedValue = function(time, value) {
            return new Promise((resolve) => {
              setTimeout(() => resolve(value), time);
            });
          };

          const generate = async function* () {
            yield delayedValue(20, 1);
            yield delayedValue(10, 2);
            yield delayedValue(5, 3);
            yield delayedValue(3, 4);
            yield delayedValue(2, 5);
            yield delayedValue(1, 6);
          };

          document.getElementById('run').addEventListener('click', () => {
            const generator = generate();
            Promise.all([
              generator.next(),
              generator.next(),
              generator.next(),
              generator.next(),
              generator.next(),
              generator.next(),
              generator.next(),
            ]).then((results) => {
              const values = results
                .slice(0, 6)
                .map((result) => result.value)
                .join(',');
              const done = results[6].done;
              document.getElementById('out').textContent = values + '|' + done;
            });
          });
        </script>
        "#;

    let mut h = Harness::from_html(html)?;
    h.click("#run")?;
    h.assert_text("#out", "")?;
    h.flush()?;
    h.assert_text("#out", "1,2,3,4,5,6|true")?;
    Ok(())
}

#[test]
fn async_generator_declaration_is_hoisted_and_yields_promises() -> Result<()> {
    let html = r#"
        <button id='run'>run</button>
        <p id='out'></p>
        <script>
          document.getElementById('run').addEventListener('click', () => {
            const beforeType = typeof declaredLater;
            document.getElementById('out').textContent = beforeType;
            const iter = declaredLater(4);
            const first = iter.next();
            first.then((result) => {
              document.getElementById('out').textContent =
                document.getElementById('out').textContent +
                ':' + result.value + ':' + result.done;
            });

            async function
            * declaredLater(step = 1) {
              yield await Promise.resolve(step);
            }
          });
        </script>
        "#;

    let mut h = Harness::from_html(html)?;
    h.click("#run")?;
    h.flush()?;
    h.assert_text("#out", "function:4:false")?;
    Ok(())
}

#[test]
fn async_line_break_before_function_star_is_treated_as_asi() -> Result<()> {
    let html = r#"
        <button id='run'>run</button>
        <p id='out'></p>
        <script>
          document.getElementById('run').addEventListener('click', () => {
            const async = 'marker';
            async
            function* splitDeclaration() {
              yield 'ok';
            }

            const iter = splitDeclaration();
            const first = iter.next();
            document.getElementById('out').textContent =
              (typeof iter) + ':' + first.value + ':' + first.done + ':' + async;
          });
        </script>
        "#;

    let mut h = Harness::from_html(html)?;
    h.click("#run")?;
    h.assert_text("#out", "object:ok:false:marker")?;
    Ok(())
}

#[test]
fn async_generator_next_rejects_when_yielded_promise_rejects() -> Result<()> {
    let html = r#"
        <button id='run'>run</button>
        <p id='out'></p>
        <script>
          async function* failingGenerator() {
            yield Promise.reject('boom');
          }

          document.getElementById('run').addEventListener('click', () => {
            const iter = failingGenerator();
            iter.next().then(
              () => {
                document.getElementById('out').textContent = 'fulfilled';
              },
              (reason) => {
                document.getElementById('out').textContent = 'rejected:' + reason;
              }
            );
          });
        </script>
        "#;

    let mut h = Harness::from_html(html)?;
    h.click("#run")?;
    h.assert_text("#out", "rejected:boom")?;
    Ok(())
}

#[test]
fn async_function_star_expression_can_be_used_in_expression_contexts() -> Result<()> {
    let html = r#"
        <button id='run'>run</button>
        <p id='out'></p>
        <script>
          async function joinAll(factory) {
            let text = '';
            for await (const value of factory()) {
              text += value;
            }
            return text;
          }

          document.getElementById('run').addEventListener('click', () => {
            const iife = (async function* (x) {
              yield await Promise.resolve(x);
              yield await Promise.resolve(x + 1);
            })(10);

            Promise.all([
              joinAll(async function* () {
                yield await Promise.resolve('a');
                yield await Promise.resolve('b');
                yield await Promise.resolve('c');
              }),
              iife.next(),
              iife.next(),
              iife.next(),
            ]).then((values) => {
              const joined = values[0];
              const first = values[1];
              const second = values[2];
              const done = values[3];
              document.getElementById('out').textContent =
                joined + '|' +
                first.value + ',' + second.value + ',' + done.done;
            });
          });
        </script>
        "#;

    let mut h = Harness::from_html(html)?;
    h.click("#run")?;
    h.assert_text("#out", "abc|10,11,true")?;
    Ok(())
}

#[test]
fn named_async_function_star_expression_uses_local_name_binding() -> Result<()> {
    let html = r#"
        <button id='run'>run</button>
        <p id='out'></p>
        <script>
          document.getElementById('run').addEventListener('click', () => {
            const self = 'outer';
            const gen = async function* self(value = 6) {
              yield typeof self;
              yield await Promise.resolve(value * value);
            };

            const iter = gen();
            Promise.all([iter.next(), iter.next(), iter.next()]).then(([first, second, done]) => {
              document.getElementById('out').textContent =
                self + '|' +
                first.value + ':' +
                second.value + ':' +
                done.done;
            });
          });
        </script>
        "#;

    let mut h = Harness::from_html(html)?;
    h.click("#run")?;
    h.assert_text("#out", "outer|function:36:true")?;
    Ok(())
}

#[test]
fn async_function_star_expression_statement_without_name_is_rejected() {
    let err = Harness::from_html("<script>async function* () { yield 1; }</script>")
        .expect_err("anonymous async function* at statement start should parse as declaration");
    match err {
        Error::ScriptParse(msg) => {
            assert!(
                msg.contains("function declaration requires a function name")
                    || msg.contains("expected function name")
            )
        }
        other => panic!("unexpected error: {other:?}"),
    }
}