// META: global=window,worker,shadowrealm // META: script=../resources/test-utils.js // META: script=../resources/rs-utils.js 'use strict'; promise_test(t => { const randomSource = new RandomPushSource(); let cancellationFinished = false; const rs = new ReadableStream({ start(c) { randomSource.ondata = c.enqueue.bind(c); randomSource.onend = c.close.bind(c); randomSource.onerror = c.error.bind(c); }, pull() { randomSource.readStart(); }, cancel() { randomSource.readStop(); return new Promise(resolve => { t.step_timeout(() => { cancellationFinished = true; resolve(); }, 1); }); } }); const reader = rs.getReader(); // We call delay multiple times to avoid cancelling too early for the // source to enqueue at least one chunk. const cancel = delay(5).then(() => delay(5)).then(() => delay(5)).then(() => { const cancelPromise = reader.cancel(); assert_false(cancellationFinished, 'cancellation in source should happen later'); return cancelPromise; }); return readableStreamToArray(rs, reader).then(chunks => { assert_greater_than(chunks.length, 0, 'at least one chunk should be read'); for (let i = 0; i < chunks.length; i++) { assert_equals(chunks[i].length, 128, 'chunk ' + i + ' should have 128 bytes'); } return cancel; }).then(() => { assert_true(cancellationFinished, 'it returns a promise that is fulfilled when the cancellation finishes'); }); }, 'ReadableStream cancellation: integration test on an infinite stream derived from a random push source'); test(() => { let recordedReason; const rs = new ReadableStream({ cancel(reason) { recordedReason = reason; } }); const passedReason = new Error('Sorry, it just wasn\'t meant to be.'); rs.cancel(passedReason); assert_equals(recordedReason, passedReason, 'the error passed to the underlying source\'s cancel method should equal the one passed to the stream\'s cancel'); }, 'ReadableStream cancellation: cancel(reason) should pass through the given reason to the underlying source'); promise_test(() => { const rs = new ReadableStream({ start(c) { c.enqueue('a'); c.close(); }, cancel() { assert_unreached('underlying source cancel() should not have been called'); } }); const reader = rs.getReader(); return rs.cancel().then(() => { assert_unreached('cancel() should be rejected'); }, e => { assert_equals(e.name, 'TypeError', 'cancel() should be rejected with a TypeError'); }).then(() => { return reader.read(); }).then(result => { assert_object_equals(result, { value: 'a', done: false }, 'read() should still work after the attempted cancel'); return reader.closed; }); }, 'ReadableStream cancellation: cancel() on a locked stream should fail and not call the underlying source cancel'); promise_test(() => { let cancelReceived = false; const cancelReason = new Error('I am tired of this stream, I prefer to cancel it'); const rs = new ReadableStream({ cancel(reason) { cancelReceived = true; assert_equals(reason, cancelReason, 'cancellation reason given to the underlying source should be equal to the one passed'); } }); return rs.cancel(cancelReason).then(() => { assert_true(cancelReceived); }); }, 'ReadableStream cancellation: should fulfill promise when cancel callback went fine'); promise_test(() => { const rs = new ReadableStream({ cancel() { return 'Hello'; } }); return rs.cancel().then(v => { assert_equals(v, undefined, 'cancel() return value should be fulfilled with undefined'); }); }, 'ReadableStream cancellation: returning a value from the underlying source\'s cancel should not affect the fulfillment value of the promise returned by the stream\'s cancel'); promise_test(() => { const thrownError = new Error('test'); let cancelCalled = false; const rs = new ReadableStream({ cancel() { cancelCalled = true; throw thrownError; } }); return rs.cancel('test').then(() => { assert_unreached('cancel should reject'); }, e => { assert_true(cancelCalled); assert_equals(e, thrownError); }); }, 'ReadableStream cancellation: should reject promise when cancel callback raises an exception'); promise_test(() => { const cancelReason = new Error('test'); const rs = new ReadableStream({ cancel(error) { assert_equals(error, cancelReason); return delay(1); } }); return rs.cancel(cancelReason); }, 'ReadableStream cancellation: if the underlying source\'s cancel method returns a promise, the promise returned by the stream\'s cancel should fulfill when that one does (1)'); promise_test(t => { let resolveSourceCancelPromise; let sourceCancelPromiseHasFulfilled = false; const rs = new ReadableStream({ cancel() { const sourceCancelPromise = new Promise(resolve => resolveSourceCancelPromise = resolve); sourceCancelPromise.then(() => { sourceCancelPromiseHasFulfilled = true; }); return sourceCancelPromise; } }); t.step_timeout(() => resolveSourceCancelPromise('Hello'), 1); return rs.cancel().then(value => { assert_true(sourceCancelPromiseHasFulfilled, 'cancel() return value should be fulfilled only after the promise returned by the underlying source\'s cancel'); assert_equals(value, undefined, 'cancel() return value should be fulfilled with undefined'); }); }, 'ReadableStream cancellation: if the underlying source\'s cancel method returns a promise, the promise returned by the stream\'s cancel should fulfill when that one does (2)'); promise_test(t => { let rejectSourceCancelPromise; let sourceCancelPromiseHasRejected = false; const rs = new ReadableStream({ cancel() { const sourceCancelPromise = new Promise((resolve, reject) => rejectSourceCancelPromise = reject); sourceCancelPromise.catch(() => { sourceCancelPromiseHasRejected = true; }); return sourceCancelPromise; } }); const errorInCancel = new Error('Sorry, it just wasn\'t meant to be.'); t.step_timeout(() => rejectSourceCancelPromise(errorInCancel), 1); return rs.cancel().then(() => { assert_unreached('cancel() return value should be rejected'); }, r => { assert_true(sourceCancelPromiseHasRejected, 'cancel() return value should be rejected only after the promise returned by the underlying source\'s cancel'); assert_equals(r, errorInCancel, 'cancel() return value should be rejected with the underlying source\'s rejection reason'); }); }, 'ReadableStream cancellation: if the underlying source\'s cancel method returns a promise, the promise returned by the stream\'s cancel should reject when that one does'); promise_test(() => { const rs = new ReadableStream({ start() { return new Promise(() => {}); }, pull() { assert_unreached('pull should not have been called'); } }); return Promise.all([rs.cancel(), rs.getReader().closed]); }, 'ReadableStream cancellation: cancelling before start finishes should prevent pull() from being called');