text: // META: global=worker,jsshell
  // META: script=../resources/rs-utils.js
  // META: script=../resources/test-utils.js
  // META: script=../resources/recording-streams.js
  'use strict';
  
  test(() => {
    assert_equals(ReadableStream.prototype[Symbol.asyncIterator], ReadableStream.prototype.getIterator);
  }, '@@asyncIterator() method is === to getIterator() method');
  
  test(() => {
    const s = new ReadableStream();
    const it = s.getIterator();
    const proto = Object.getPrototypeOf(it);
  
    const AsyncIteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf(async function* () {}).prototype);
    assert_equals(Object.getPrototypeOf(proto), AsyncIteratorPrototype, 'prototype should extend AsyncIteratorPrototype');
  
    const methods = ['next', 'return'].sort();
    assert_array_equals(Object.getOwnPropertyNames(proto).sort(), methods, 'should have all the correct methods');
  
    for (const m of methods) {
      const propDesc = Object.getOwnPropertyDescriptor(proto, m);
      assert_false(propDesc.enumerable, 'method should be non-enumerable');
      assert_true(propDesc.configurable, 'method should be configurable');
      assert_true(propDesc.writable, 'method should be writable');
      assert_equals(typeof it[m], 'function', 'method should be a function');
      assert_equals(it[m].name, m, 'method should have the correct name');
    }
  
    assert_equals(it.next.length, 0, 'next should have no parameters');
    assert_equals(it.return.length, 1, 'return should have 1 parameter');
    assert_equals(typeof it.throw, 'undefined', 'throw should not exist');
  }, 'Async iterator instances should have the correct list of properties');
  
  promise_test(async () => {
    const s = new ReadableStream({
      start(c) {
        c.enqueue(1);
        c.enqueue(2);
        c.enqueue(3);
        c.close();
      },
    });
  
    const chunks = [];
    for await (const chunk of s) {
      chunks.push(chunk);
    }
    assert_array_equals(chunks, [1, 2, 3]);
  }, 'Async-iterating a push source');
  
  promise_test(async () => {
    let i = 1;
    const s = new ReadableStream({
      pull(c) {
        c.enqueue(i);
        if (i >= 3) {
          c.close();
        }
        i += 1;
      },
    });
  
    const chunks = [];
    for await (const chunk of s) {
      chunks.push(chunk);
    }
    assert_array_equals(chunks, [1, 2, 3]);
  }, 'Async-iterating a pull source');
  
  promise_test(async () => {
    let i = 1;
    const s = recordingReadableStream({
      pull(c) {
        c.enqueue(i);
        if (i >= 3) {
          c.close();
        }
        i += 1;
      },
    }, new CountQueuingStrategy({ highWaterMark: 0 }));
  
    const it = s.getIterator();
    assert_array_equals(s.events, []);
  
    const read1 = await it.next();
    assert_equals(read1.done, false);
    assert_equals(read1.value, 1);
    assert_array_equals(s.events, ['pull']);
  
    const read2 = await it.next();
    assert_equals(read2.done, false);
    assert_equals(read2.value, 2);
    assert_array_equals(s.events, ['pull', 'pull']);
  
    const read3 = await it.next();
    assert_equals(read3.done, false);
    assert_equals(read3.value, 3);
    assert_array_equals(s.events, ['pull', 'pull', 'pull']);
  
    const read4 = await it.next();
    assert_equals(read4.done, true);
    assert_equals(read4.value, undefined);
    assert_array_equals(s.events, ['pull', 'pull', 'pull']);
  }, 'Async-iterating a pull source manually');
  
  promise_test(async () => {
    const s = new ReadableStream({
      start(c) {
        c.error('e');
      },
    });
  
    try {
      for await (const chunk of s) {}
      assert_unreached();
    } catch (e) {
      assert_equals(e, 'e');
    }
  }, 'Async-iterating an errored stream throws');
  
  promise_test(async () => {
    const s = new ReadableStream({
      start(c) {
        c.close();
      }
    });
  
    for await (const chunk of s) {
      assert_unreached();
    }
  }, 'Async-iterating a closed stream never executes the loop body, but works fine');
  
  promise_test(async () => {
    const s = new ReadableStream();
  
    const loop = async () => {
      for await (const chunk of s) {
        assert_unreached();
      }
      assert_unreached();
    };
  
    await Promise.race([
      loop(),
      flushAsyncEvents()
    ]);
  }, 'Async-iterating an empty but not closed/errored stream never executes the loop body and stalls the async function');
  
  promise_test(async () => {
    const s = new ReadableStream({
      start(c) {
        c.enqueue(1);
        c.enqueue(2);
        c.enqueue(3);
        c.close();
      },
    });
  
    const reader = s.getReader();
    const readResult = await reader.read();
    assert_equals(readResult.done, false);
    assert_equals(readResult.value, 1);
    reader.releaseLock();
  
    const chunks = [];
    for await (const chunk of s) {
      chunks.push(chunk);
    }
    assert_array_equals(chunks, [2, 3]);
  }, 'Async-iterating a partially consumed stream');
  
  for (const type of ['throw', 'break', 'return']) {
    for (const preventCancel of [false, true]) {
      promise_test(async () => {
        const s = recordingReadableStream({
          start(c) {
            c.enqueue(0);
          }
        });
  
        // use a separate function for the loop body so return does not stop the test
        const loop = async () => {
          for await (const c of s.getIterator({ preventCancel })) {
            if (type === 'throw') {
              throw new Error();
            } else if (type === 'break') {
              break;
            } else if (type === 'return') {
              return;
            }
          }
        };
  
        try {
          await loop();
        } catch (e) {}
  
        if (preventCancel) {
          assert_array_equals(s.events, ['pull'], `cancel() should not be called`);
        } else {
          assert_array_equals(s.events, ['pull', 'cancel', undefined], `cancel() should be called`);
        }
      }, `Cancellation behavior when ${type}ing inside loop body; preventCancel = ${preventCancel}`);
    }
  }
  
  for (const preventCancel of [false, true]) {
    promise_test(async () => {
      const s = recordingReadableStream({
        start(c) {
          c.enqueue(0);
        }
      });
  
      const it = s.getIterator({ preventCancel });
      await it.return();
  
      if (preventCancel) {
        assert_array_equals(s.events, [], `cancel() should not be called`);
      } else {
        assert_array_equals(s.events, ['cancel', undefined], `cancel() should be called`);
      }
    }, `Cancellation behavior when manually calling return(); preventCancel = ${preventCancel}`);
  }
  
  promise_test(async t => {
    const s = new ReadableStream();
    const it = s[Symbol.asyncIterator]();
    await it.return();
    return promise_rejects_js(t, TypeError, it.return(), 'return should reject');
  }, 'Calling return() twice rejects');
  
  promise_test(async () => {
    const s = new ReadableStream({
      start(c) {
        c.enqueue(0);
        c.close();
      },
    });
    const it = s[Symbol.asyncIterator]();
    const next = await it.next();
    assert_equals(Object.getPrototypeOf(next), Object.prototype);
    assert_array_equals(Object.getOwnPropertyNames(next).sort(), ['done', 'value']);
  }, 'next()\'s fulfillment value has the right shape');
  
  promise_test(async t => {
    const s = recordingReadableStream();
    const it = s[Symbol.asyncIterator]();
    it.next();
  
    await promise_rejects_js(t, TypeError, it.return(), 'return() should reject');
    assert_array_equals(s.events, ['pull']);
  }, 'calling return() while there are pending reads rejects');
  
  test(() => {
    const s = new ReadableStream({
      start(c) {
        c.enqueue(0);
        c.close();
      },
    });
    const it = s.getIterator();
    assert_throws_js(TypeError, () => s.getIterator(), 'getIterator() should throw');
  }, 'getIterator() throws if there\'s already a lock');
  
  promise_test(async () => {
    const s = new ReadableStream({
      start(c) {
        c.enqueue(1);
        c.enqueue(2);
        c.enqueue(3);
        c.close();
      },
    });
  
    const chunks = [];
    for await (const chunk of s) {
      chunks.push(chunk);
    }
    assert_array_equals(chunks, [1, 2, 3]);
  
    const reader = s.getReader();
    await reader.closed;
  }, 'Acquiring a reader after exhaustively async-iterating a stream');
  
  promise_test(async () => {
    const s = new ReadableStream({
      start(c) {
        c.enqueue(1);
        c.enqueue(2);
        c.enqueue(3);
        c.close();
      },
    });
  
    // read the first two chunks, then cancel
    const chunks = [];
    for await (const chunk of s) {
      chunks.push(chunk);
      if (chunk >= 2) {
        break;
      }
    }
    assert_array_equals(chunks, [1, 2]);
  
    const reader = s.getReader();
    await reader.closed;
  }, 'Acquiring a reader after partially async-iterating a stream');
  
  promise_test(async () => {
    const s = new ReadableStream({
      start(c) {
        c.enqueue(1);
        c.enqueue(2);
        c.enqueue(3);
        c.close();
      },
    });
  
    // read the first two chunks, then release lock
    const chunks = [];
    for await (const chunk of s.getIterator({preventCancel: true})) {
      chunks.push(chunk);
      if (chunk >= 2) {
        break;
      }
    }
    assert_array_equals(chunks, [1, 2]);
  
    const reader = s.getReader();
    const readResult = await reader.read();
    assert_equals(readResult.done, false, 'should not be closed yet');
    assert_equals(readResult.value, 3, 'should read remaining chunk');
    await reader.closed;
  }, 'Acquiring a reader and reading the remaining chunks after partially async-iterating a stream with preventCancel = true');
  
  promise_test(async t => {
    const rs = new ReadableStream();
    const it = rs.getIterator();
    await it.return();
    return promise_rejects_js(t, TypeError, it.next(), 'next() should reject');
  }, 'calling next() after return() should reject');
  
  for (const preventCancel of [false, true]) {
    test(() => {
      const rs = new ReadableStream();
      rs.getIterator({ preventCancel }).return();
      // The test passes if this line doesn't throw.
      rs.getReader();
    }, `return() should unlock the stream synchronously when preventCancel = ${preventCancel}`);
  }
  