'use strict'; if (self.importScripts) { self.importScripts('/resources/testharness.js'); self.importScripts('../resources/test-utils.js'); self.importScripts('../resources/recording-streams.js'); } const error1 = new Error('error1'); error1.name = 'error1'; const error2 = new Error('error2'); error2.name = 'error2'; promise_test(t => { const ws = new WritableStream({ write: t.unreached_func('write() should not be called') }); const writer = ws.getWriter(); const writePromise = writer.write('a'); const readyPromise = writer.ready; writer.abort(error1); assert_equals(writer.ready, readyPromise, 'the ready promise property should not change'); return Promise.all([ promise_rejects(t, new TypeError(), readyPromise, 'the ready promise should reject with a TypeError'), promise_rejects(t, new TypeError(), writePromise, 'the write() promise should reject with a TypeError') ]); }, 'Aborting a WritableStream before it starts should cause the writer\'s unsettled ready promise to reject'); promise_test(t => { const ws = new WritableStream(); const writer = ws.getWriter(); writer.write('a'); const readyPromise = writer.ready; return readyPromise.then(() => { writer.abort(error1); assert_not_equals(writer.ready, readyPromise, 'the ready promise property should change'); return promise_rejects(t, new TypeError(), writer.ready, 'the ready promise should reject with a TypeError'); }); }, 'Aborting a WritableStream should cause the writer\'s fulfilled ready promise to reset to a rejected one'); promise_test(t => { const ws = new WritableStream(); const writer = ws.getWriter(); writer.releaseLock(); return promise_rejects(t, new TypeError(), writer.abort(), 'abort() should reject with a TypeError'); }, 'abort() on a released writer rejects'); promise_test(t => { const ws = recordingWritableStream(); return delay(0) .then(() => { const writer = ws.getWriter(); const abortPromise = writer.abort(); return Promise.all([ promise_rejects(t, new TypeError(), writer.write(1), 'write(1) must reject with a TypeError'), promise_rejects(t, new TypeError(), writer.write(2), 'write(2) must reject with a TypeError'), abortPromise ]); }) .then(() => { assert_array_equals(ws.events, ['abort', undefined]); }); }, 'Aborting a WritableStream immediately prevents future writes'); promise_test(t => { const ws = recordingWritableStream(); const results = []; return delay(0) .then(() => { const writer = ws.getWriter(); results.push( writer.write(1), promise_rejects(t, new TypeError(), writer.write(2), 'write(2) must reject with a TypeError'), promise_rejects(t, new TypeError(), writer.write(3), 'write(3) must reject with a TypeError') ); const abortPromise = writer.abort(); results.push( promise_rejects(t, new TypeError(), writer.write(4), 'write(4) must reject with a TypeError'), promise_rejects(t, new TypeError(), writer.write(5), 'write(5) must reject with a TypeError') ); return abortPromise; }).then(() => { assert_array_equals(ws.events, ['write', 1, 'abort', undefined]); return Promise.all(results); }); }, 'Aborting a WritableStream prevents further writes after any that are in progress'); promise_test(() => { const ws = new WritableStream({ abort() { return 'Hello'; } }); const writer = ws.getWriter(); return writer.abort('a').then(value => { assert_equals(value, undefined, 'fulfillment value must be undefined'); }); }, 'Fulfillment value of ws.abort() call must be undefined even if the underlying sink returns a non-undefined value'); promise_test(t => { const ws = new WritableStream({ abort() { throw error1; } }); const writer = ws.getWriter(); return promise_rejects(t, error1, writer.abort(undefined), 'rejection reason of abortPromise must be the error thrown by abort'); }, 'WritableStream if sink\'s abort throws, the promise returned by writer.abort() rejects'); promise_test(t => { const ws = new WritableStream({ abort() { throw error1; } }); return promise_rejects(t, error1, ws.abort(undefined), 'rejection reason of abortPromise must be the error thrown by abort'); }, 'WritableStream if sink\'s abort throws, the promise returned by ws.abort() rejects'); promise_test(t => { let resolveWritePromise; const ws = new WritableStream({ write() { return new Promise(resolve => { resolveWritePromise = resolve; }); }, abort() { throw error1; } }); const writer = ws.getWriter(); writer.write().catch(() => {}); return flushAsyncEvents().then(() => { const abortPromise = writer.abort(undefined); resolveWritePromise(); return promise_rejects(t, error1, abortPromise, 'rejection reason of abortPromise must be the error thrown by abort'); }); }, 'WritableStream if sink\'s abort throws, for an abort performed during a write, the promise returned by ' + 'ws.abort() rejects'); promise_test(() => { const ws = recordingWritableStream(); const writer = ws.getWriter(); return writer.abort(error1).then(() => { assert_array_equals(ws.events, ['abort', error1]); }); }, 'Aborting a WritableStream passes through the given reason'); promise_test(t => { const ws = new WritableStream(); const writer = ws.getWriter(); const abortPromise = writer.abort(error1); const events = []; writer.ready.catch(() => { events.push('ready'); }); writer.closed.catch(() => { events.push('closed'); }); return Promise.all([ abortPromise, promise_rejects(t, new TypeError(), writer.write(), 'writing should reject with a TypeError'), promise_rejects(t, new TypeError(), writer.close(), 'closing should reject with a TypeError'), promise_rejects(t, new TypeError(), writer.abort(), 'aborting should reject with a TypeError'), promise_rejects(t, new TypeError(), writer.ready, 'ready should reject with a TypeError'), promise_rejects(t, new TypeError(), writer.closed, 'closed should reject with a TypeError') ]).then(() => { assert_array_equals(['ready', 'closed'], events, 'ready should reject before closed'); }); }, 'Aborting a WritableStream puts it in an errored state, with a TypeError as the stored error'); promise_test(t => { const ws = new WritableStream(); const writer = ws.getWriter(); const writePromise = promise_rejects(t, new TypeError(), writer.write('a'), 'writing should reject with a TypeError'); writer.abort(error1); return writePromise; }, 'Aborting a WritableStream causes any outstanding write() promises to be rejected with a TypeError'); promise_test(t => { const ws = recordingWritableStream(); const writer = ws.getWriter(); const closePromise = writer.close(); const abortPromise = writer.abort(error1); return Promise.all([ promise_rejects(t, new TypeError(), writer.closed, 'closed should reject with a TypeError'), promise_rejects(t, new TypeError(), closePromise, 'close() should reject with a TypeError'), abortPromise ]).then(() => { assert_array_equals(ws.events, ['abort', error1]); }); }, 'Closing but then immediately aborting a WritableStream causes the stream to error'); promise_test(() => { let resolveClose; const ws = new WritableStream({ close() { return new Promise(resolve => { resolveClose = resolve; }); } }); const writer = ws.getWriter(); const closePromise = writer.close(); return delay(0).then(() => { const abortPromise = writer.abort(error1); resolveClose(); return Promise.all([ writer.closed, abortPromise, closePromise ]); }); }, 'Closing a WritableStream and aborting it while it closes causes the stream to ignore the abort attempt'); promise_test(() => { const ws = new WritableStream(); const writer = ws.getWriter(); writer.close(); return delay(0).then(() => writer.abort()); }, 'Aborting a WritableStream after it is closed is a no-op'); promise_test(t => { // Testing that per https://github.com/whatwg/streams/issues/620#issuecomment-263483953 the fallback to close was // removed. // Cannot use recordingWritableStream since it always has an abort let closeCalled = false; const ws = new WritableStream({ close() { closeCalled = true; } }); const writer = ws.getWriter(); writer.abort(); return promise_rejects(t, new TypeError(), writer.closed, 'closed should reject with a TypeError').then(() => { assert_false(closeCalled, 'close must not have been called'); }); }, 'WritableStream should NOT call underlying sink\'s close if no abort is supplied (historical)'); promise_test(() => { let thenCalled = false; const ws = new WritableStream({ abort() { return { then(onFulfilled) { thenCalled = true; onFulfilled(); } }; } }); const writer = ws.getWriter(); return writer.abort().then(() => assert_true(thenCalled, 'then() should be called')); }, 'returning a thenable from abort() should work'); promise_test(t => { const ws = new WritableStream({ write() { return flushAsyncEvents(); } }); const writer = ws.getWriter(); return writer.ready.then(() => { const writePromise = writer.write('a'); writer.abort(error1); let closedRejected = false; return Promise.all([ writePromise.then(() => assert_false(closedRejected, '.closed should not resolve before write()')), promise_rejects(t, new TypeError(), writer.closed, '.closed should reject').then(() => { closedRejected = true; }) ]); }); }, '.closed should not resolve before fulfilled write()'); promise_test(t => { const ws = new WritableStream({ write() { return Promise.reject(error1); } }); const writer = ws.getWriter(); return writer.ready.then(() => { const writePromise = writer.write('a'); const abortPromise = writer.abort(error2); let closedRejected = false; return Promise.all([ promise_rejects(t, error1, writePromise, 'write() should reject') .then(() => assert_false(closedRejected, '.closed should not resolve before write()')), promise_rejects(t, new TypeError(), writer.closed, '.closed should reject') .then(() => { closedRejected = true; }), abortPromise ]); }); }, '.closed should not resolve before rejected write(); write() error should not overwrite abort() error'); promise_test(t => { const ws = new WritableStream({ write() { return flushAsyncEvents(); } }, new CountQueuingStrategy(4)); const writer = ws.getWriter(); return writer.ready.then(() => { const settlementOrder = []; return Promise.all([ writer.write('1').then(() => settlementOrder.push(1)), promise_rejects(t, new TypeError(), writer.write('2'), 'first queued write should be rejected') .then(() => settlementOrder.push(2)), promise_rejects(t, new TypeError(), writer.write('3'), 'second queued write should be rejected') .then(() => settlementOrder.push(3)), writer.abort(error1) ]).then(() => assert_array_equals([1, 2, 3], settlementOrder, 'writes should be satisfied in order')); }); }, 'writes should be satisfied in order when aborting'); promise_test(t => { const ws = new WritableStream({ write() { return Promise.reject(error1); } }, new CountQueuingStrategy(4)); const writer = ws.getWriter(); return writer.ready.then(() => { const settlementOrder = []; return Promise.all([ promise_rejects(t, error1, writer.write('1'), 'in-flight write should be rejected') .then(() => settlementOrder.push(1)), promise_rejects(t, new TypeError(), writer.write('2'), 'first queued write should be rejected') .then(() => settlementOrder.push(2)), promise_rejects(t, new TypeError(), writer.write('3'), 'second queued write should be rejected') .then(() => settlementOrder.push(3)), writer.abort(error2) ]).then(() => assert_array_equals([1, 2, 3], settlementOrder, 'writes should be satisfied in order')); }); }, 'writes should be satisfied in order after rejected write when aborting'); promise_test(t => { const ws = new WritableStream({ write() { return Promise.reject(error1); } }); const writer = ws.getWriter(); return writer.ready.then(() => { return Promise.all([ promise_rejects(t, error1, writer.write('a'), 'writer.write() should reject with error from underlying write()'), promise_rejects(t, new TypeError(), writer.close(), 'writer.close() should reject with error from underlying write()'), writer.abort() ]); }); }, 'close() should reject with TypeError when abort() is first error'); promise_test(() => { let resolveWrite; const ws = recordingWritableStream({ write() { return new Promise(resolve => { resolveWrite = resolve; }); } }); const writer = ws.getWriter(); return writer.ready.then(() => { writer.write('a'); const abortPromise = writer.abort('b'); return flushAsyncEvents().then(() => { assert_array_equals(ws.events, ['write', 'a'], 'abort should not be called while write is in-flight'); resolveWrite(); return abortPromise.then(() => { assert_array_equals(ws.events, ['write', 'a', 'abort', 'b'], 'abort should be called after the write finishes'); }); }); }); }, 'underlying abort() should not be called until underlying write() completes'); promise_test(() => { let resolveClose; const ws = recordingWritableStream({ close() { return new Promise(resolve => { resolveClose = resolve; }); } }); const writer = ws.getWriter(); return writer.ready.then(() => { writer.close(); const abortPromise = writer.abort(); return flushAsyncEvents().then(() => { assert_array_equals(ws.events, ['close'], 'abort should not be called while close is in-flight'); resolveClose(); return abortPromise.then(() => { assert_array_equals(ws.events, ['close'], 'abort should not be called'); }); }); }); }, 'underlying abort() should not be called if underlying close() has started'); promise_test(t => { let rejectClose; let abortCalled = false; const ws = new WritableStream({ close() { return new Promise((resolve, reject) => { rejectClose = reject; }); }, abort() { abortCalled = true; } }); const writer = ws.getWriter(); return writer.ready.then(() => { const closePromise = writer.close(); const abortPromise = writer.abort(); return flushAsyncEvents().then(() => { assert_false(abortCalled, 'underlying abort should not be called while close is in-flight'); rejectClose(error1); return promise_rejects(t, error1, abortPromise, 'abort should reject with the same reason').then(() => { return promise_rejects(t, error1, closePromise, 'close should reject with the same reason'); }).then(() => { assert_false(abortCalled, 'underlying abort should not be called after close completes'); }); }); }); }, 'if underlying close() has started and then rejects, the abort() and close() promises should reject with the ' + 'underlying close rejection reason'); promise_test(t => { let resolveWrite; const ws = recordingWritableStream({ write() { return new Promise(resolve => { resolveWrite = resolve; }); } }); const writer = ws.getWriter(); return writer.ready.then(() => { writer.write('a'); const closePromise = writer.close(); const abortPromise = writer.abort('b'); return flushAsyncEvents().then(() => { assert_array_equals(ws.events, ['write', 'a'], 'abort should not be called while write is in-flight'); resolveWrite(); return abortPromise.then(() => { assert_array_equals(ws.events, ['write', 'a', 'abort', 'b'], 'abort should be called after write completes'); return promise_rejects(t, new TypeError(), closePromise, 'promise returned by close() should be rejected'); }); }); }); }, 'an abort() that happens during a write() should trigger the underlying abort() even with a close() queued'); promise_test(t => { const ws = new WritableStream({ write() { return new Promise(() => {}); } }); const writer = ws.getWriter(); return writer.ready.then(() => { writer.write('a'); writer.abort(); writer.releaseLock(); const writer2 = ws.getWriter(); return promise_rejects(t, new TypeError(), writer2.ready, 'ready of the second writer should be rejected with a TypeError'); }); }, 'if a writer is created for a stream with a pending abort, its ready should be rejected with a TypeError'); promise_test(() => { const ws = new WritableStream(); const writer = ws.getWriter(); return writer.ready.then(() => { const closePromise = writer.close(); const abortPromise = writer.abort(); const events = []; return Promise.all([ closePromise.then(() => { events.push('close'); }), abortPromise.then(() => { events.push('abort'); }) ]).then(() => { assert_array_equals(events, ['close', 'abort']); }); }); }, 'writer close() promise should resolve before abort() promise'); promise_test(t => { const ws = new WritableStream({ write(chunk, controller) { controller.error(error1); return new Promise(() => {}); } }); const writer = ws.getWriter(); return writer.ready.then(() => { writer.write('a'); return promise_rejects(t, error1, writer.ready, 'writer.ready should reject'); }); }, 'writer.ready should reject on controller error without waiting for underlying write'); promise_test(t => { let rejectWrite; const ws = new WritableStream({ write() { return new Promise((resolve, reject) => { rejectWrite = reject; }); } }); let writePromise; let abortPromise; const events = []; const writer = ws.getWriter(); writer.closed.catch(() => { events.push('closed'); }); // Wait for ws to start return flushAsyncEvents().then(() => { writePromise = writer.write('a'); writePromise.catch(() => { events.push('writePromise'); }); abortPromise = writer.abort(error1); abortPromise.then(() => { events.push('abortPromise'); }); const writePromise2 = writer.write('a'); return Promise.all([ promise_rejects(t, new TypeError(), writePromise2, 'writePromise2 must reject with an error indicating abort'), promise_rejects(t, new TypeError(), writer.ready, 'writer.ready must reject with an error indicating abort'), flushAsyncEvents() ]); }).then(() => { assert_array_equals(events, [], 'writePromise, abortPromise and writer.closed must not be rejected yet'); rejectWrite(error2); return Promise.all([ promise_rejects(t, error2, writePromise, 'writePromise must reject with the error returned from the sink\'s write method'), abortPromise, promise_rejects(t, new TypeError(), writer.closed, 'writer.closed must reject with an error indicating abort'), flushAsyncEvents() ]); }).then(() => { assert_array_equals(events, ['writePromise', 'abortPromise', 'closed'], 'writePromise, abortPromise and writer.closed must settle'); const writePromise3 = writer.write('a'); return Promise.all([ promise_rejects(t, new TypeError(), writePromise3, 'writePromise3 must reject with an error indicating abort'), promise_rejects(t, new TypeError(), writer.ready, 'writer.ready must be still rejected with the error indicating abort') ]); }).then(() => { writer.releaseLock(); return Promise.all([ promise_rejects(t, new TypeError(), writer.ready, 'writer.ready must be rejected with an error indicating release'), promise_rejects(t, new TypeError(), writer.closed, 'writer.closed must be rejected with an error indicating release') ]); }); }, 'writer.abort() while there is an in-flight write, and then finish the write with rejection'); promise_test(t => { let resolveWrite; let controller; const ws = new WritableStream({ write(chunk, c) { controller = c; return new Promise(resolve => { resolveWrite = resolve; }); } }); let writePromise; let abortPromise; const events = []; const writer = ws.getWriter(); writer.closed.catch(() => { events.push('closed'); }); // Wait for ws to start return flushAsyncEvents().then(() => { writePromise = writer.write('a'); writePromise.then(() => { events.push('writePromise'); }); abortPromise = writer.abort(error1); abortPromise.then(() => { events.push('abortPromise'); }); const writePromise2 = writer.write('a'); return Promise.all([ promise_rejects(t, new TypeError(), writePromise2, 'writePromise2 must reject with an error indicating abort'), promise_rejects(t, new TypeError(), writer.ready, 'writer.ready must reject with an error indicating abort'), flushAsyncEvents() ]); }).then(() => { assert_array_equals(events, [], 'writePromise, abortPromise and writer.closed must not be fulfilled/rejected yet'); // This error is too late to change anything. abort() has already changed the stream state to 'erroring'. controller.error(error2); const writePromise3 = writer.write('a'); return Promise.all([ promise_rejects(t, new TypeError(), writePromise3, 'writePromise3 must reject with an error indicating abort'), promise_rejects(t, new TypeError(), writer.ready, 'writer.ready must be still rejected with the error indicating abort'), flushAsyncEvents() ]); }).then(() => { assert_array_equals( events, [], 'writePromise, abortPromise and writer.closed must not be fulfilled/rejected yet even after ' + 'controller.error() call'); resolveWrite(); return Promise.all([ writePromise, abortPromise, promise_rejects(t, new TypeError(), writer.closed, 'writer.closed must reject with an error indicating abort'), flushAsyncEvents() ]); }).then(() => { assert_array_equals(events, ['writePromise', 'abortPromise', 'closed'], 'writePromise, abortPromise and writer.closed must settle'); const writePromise4 = writer.write('a'); return Promise.all([ writePromise, promise_rejects(t, new TypeError(), writePromise4, 'writePromise4 must reject with an error indicating abort'), promise_rejects(t, new TypeError(), writer.ready, 'writer.ready must be still rejected with the error indicating abort') ]); }).then(() => { writer.releaseLock(); return Promise.all([ promise_rejects(t, new TypeError(), writer.ready, 'writer.ready must be rejected with an error indicating release'), promise_rejects(t, new TypeError(), writer.closed, 'writer.closed must be rejected with an error indicating release') ]); }); }, 'writer.abort(), controller.error() while there is an in-flight write, and then finish the write'); promise_test(t => { let resolveClose; let controller; const ws = new WritableStream({ start(c) { controller = c; }, close() { return new Promise(resolve => { resolveClose = resolve; }); } }); let closePromise; let abortPromise; const events = []; const writer = ws.getWriter(); writer.closed.then(() => { events.push('closed'); }); // Wait for ws to start return flushAsyncEvents().then(() => { closePromise = writer.close(); closePromise.then(() => { events.push('closePromise'); }); abortPromise = writer.abort(error1); abortPromise.then(() => { events.push('abortPromise'); }); return Promise.all([ promise_rejects(t, new TypeError(), writer.close(), 'writer.close() must reject with an error indicating already closing'), promise_rejects(t, new TypeError(), writer.ready, 'writer.ready must reject with an error indicating abort'), flushAsyncEvents() ]); }).then(() => { assert_array_equals(events, [], 'closePromise, abortPromise and writer.closed must not be fulfilled/rejected yet'); controller.error(error2); return Promise.all([ promise_rejects(t, new TypeError(), writer.close(), 'writer.close() must reject with an error indicating already closing'), promise_rejects(t, new TypeError(), writer.ready, 'writer.ready must be still rejected with the error indicating abort'), flushAsyncEvents() ]); }).then(() => { assert_array_equals( events, [], 'closePromise, abortPromise and writer.closed must not be fulfilled/rejected yet even after ' + 'controller.error() call'); resolveClose(); return Promise.all([ closePromise, abortPromise, writer.closed, flushAsyncEvents() ]); }).then(() => { assert_array_equals(events, ['closePromise', 'abortPromise', 'closed'], 'closedPromise, abortPromise and writer.closed must fulfill'); return Promise.all([ promise_rejects(t, new TypeError(), writer.close(), 'writer.close() must reject with an error indicating already closing'), promise_rejects(t, new TypeError(), writer.ready, 'writer.ready must be still rejected with the error indicating abort') ]); }).then(() => { writer.releaseLock(); return Promise.all([ promise_rejects(t, new TypeError(), writer.close(), 'writer.close() must reject with an error indicating release'), promise_rejects(t, new TypeError(), writer.ready, 'writer.ready must be rejected with an error indicating release'), promise_rejects(t, new TypeError(), writer.closed, 'writer.closed must be rejected with an error indicating release') ]); }); }, 'writer.abort(), controller.error() while there is an in-flight close, and then finish the close'); promise_test(t => { let resolveWrite; let controller; const ws = recordingWritableStream({ write(chunk, c) { controller = c; return new Promise(resolve => { resolveWrite = resolve; }); } }); let writePromise; let abortPromise; const events = []; const writer = ws.getWriter(); writer.closed.catch(() => { events.push('closed'); }); // Wait for ws to start return flushAsyncEvents().then(() => { writePromise = writer.write('a'); writePromise.then(() => { events.push('writePromise'); }); controller.error(error2); const writePromise2 = writer.write('a'); return Promise.all([ promise_rejects(t, error2, writePromise2, 'writePromise2 must reject with the error passed to the controller\'s error method'), promise_rejects(t, error2, writer.ready, 'writer.ready must reject with the error passed to the controller\'s error method'), flushAsyncEvents() ]); }).then(() => { assert_array_equals(events, [], 'writePromise and writer.closed must not be fulfilled/rejected yet'); abortPromise = writer.abort(error1); abortPromise.catch(() => { events.push('abortPromise'); }); const writePromise3 = writer.write('a'); return Promise.all([ promise_rejects(t, error2, writePromise3, 'writePromise3 must reject with the error passed to the controller\'s error method'), flushAsyncEvents() ]); }).then(() => { assert_array_equals( events, [], 'writePromise and writer.closed must not be fulfilled/rejected yet even after writer.abort()'); resolveWrite(); return Promise.all([ promise_rejects(t, error2, abortPromise, 'abort() must reject with the error passed to the controller\'s error method'), promise_rejects(t, error2, writer.closed, 'writer.closed must reject with the error passed to the controller\'s error method'), flushAsyncEvents() ]); }).then(() => { assert_array_equals(events, ['writePromise', 'abortPromise', 'closed'], 'writePromise, abortPromise and writer.closed must fulfill/reject'); assert_array_equals(ws.events, ['write', 'a'], 'sink abort() should not be called'); const writePromise4 = writer.write('a'); return Promise.all([ writePromise, promise_rejects(t, error2, writePromise4, 'writePromise4 must reject with the error passed to the controller\'s error method'), promise_rejects(t, error2, writer.ready, 'writer.ready must be still rejected with the error passed to the controller\'s error method') ]); }).then(() => { writer.releaseLock(); return Promise.all([ promise_rejects(t, new TypeError(), writer.ready, 'writer.ready must be rejected with an error indicating release'), promise_rejects(t, new TypeError(), writer.closed, 'writer.closed must be rejected with an error indicating release') ]); }); }, 'controller.error(), writer.abort() while there is an in-flight write, and then finish the write'); promise_test(t => { let resolveClose; let controller; const ws = new WritableStream({ start(c) { controller = c; }, close() { return new Promise(resolve => { resolveClose = resolve; }); } }); let closePromise; let abortPromise; const events = []; const writer = ws.getWriter(); writer.closed.then(() => { events.push('closed'); }); // Wait for ws to start return flushAsyncEvents().then(() => { closePromise = writer.close(); closePromise.then(() => { events.push('closePromise'); }); controller.error(error2); return flushAsyncEvents(); }).then(() => { assert_array_equals(events, [], 'closePromise must not be fulfilled/rejected yet'); abortPromise = writer.abort(error1); abortPromise.then(() => { events.push('abortPromise'); }); return Promise.all([ promise_rejects(t, error2, writer.ready, 'writer.ready must reject with the error passed to the controller\'s error method'), flushAsyncEvents() ]); }).then(() => { assert_array_equals( events, [], 'closePromise and writer.closed must not be fulfilled/rejected yet even after writer.abort()'); resolveClose(); return Promise.all([ closePromise, promise_rejects(t, error2, writer.ready, 'writer.ready must be still rejected with the error passed to the controller\'s error method'), writer.closed, flushAsyncEvents() ]); }).then(() => { assert_array_equals(events, ['closePromise', 'abortPromise', 'closed'], 'abortPromise, closePromise and writer.closed must fulfill/reject'); }).then(() => { writer.releaseLock(); return Promise.all([ promise_rejects(t, new TypeError(), writer.ready, 'writer.ready must be rejected with an error indicating release'), promise_rejects(t, new TypeError(), writer.closed, 'writer.closed must be rejected with an error indicating release') ]); }); }, 'controller.error(), writer.abort() while there is an in-flight close, and then finish the close'); promise_test(t => { let resolveWrite; const ws = new WritableStream({ write() { return new Promise(resolve => { resolveWrite = resolve; }); } }); const writer = ws.getWriter(); return writer.ready.then(() => { const writePromise = writer.write('a'); const closed = writer.closed; const abortPromise = writer.abort(); writer.releaseLock(); resolveWrite(); return Promise.all([ writePromise, abortPromise, promise_rejects(t, new TypeError(), closed, 'closed should reject')]); }); }, 'releaseLock() while aborting should reject the original closed promise'); // TODO(ricea): Consider removing this test if it is no longer useful. promise_test(t => { let resolveWrite; let resolveAbort; let resolveAbortStarted; const abortStarted = new Promise(resolve => { resolveAbortStarted = resolve; }); const ws = new WritableStream({ write() { return new Promise(resolve => { resolveWrite = resolve; }); }, abort() { resolveAbortStarted(); return new Promise(resolve => { resolveAbort = resolve; }); } }); const writer = ws.getWriter(); return writer.ready.then(() => { const writePromise = writer.write('a'); const closed = writer.closed; const abortPromise = writer.abort(); resolveWrite(); return abortStarted.then(() => { writer.releaseLock(); assert_equals(writer.closed, closed, 'closed promise should not have changed'); resolveAbort(); return Promise.all([ writePromise, abortPromise, promise_rejects(t, new TypeError(), closed, 'closed should reject')]); }); }); }, 'releaseLock() during delayed async abort() should reject the writer.closed promise'); promise_test(() => { let resolveStart; const ws = recordingWritableStream({ start() { return new Promise(resolve => { resolveStart = resolve; }); } }); const abortPromise = ws.abort('done'); return flushAsyncEvents().then(() => { assert_array_equals(ws.events, [], 'abort() should not be called during start()'); resolveStart(); return abortPromise.then(() => { assert_array_equals(ws.events, ['abort', 'done'], 'abort() should be called after start() is done'); }); }); }, 'sink abort() should not be called until sink start() is done'); promise_test(() => { let resolveStart; let controller; const ws = recordingWritableStream({ start(c) { controller = c; return new Promise(resolve => { resolveStart = resolve; }); } }); const abortPromise = ws.abort('done'); controller.error(error1); resolveStart(); return abortPromise.then(() => assert_array_equals(ws.events, ['abort', 'done'], 'abort() should still be called if start() errors the controller')); }, 'if start attempts to error the controller after abort() has been called, then it should lose'); promise_test(() => { const ws = recordingWritableStream({ start() { return Promise.reject(error1); } }); return ws.abort('done').then(() => assert_array_equals(ws.events, ['abort', 'done'], 'abort() should still be called if start() rejects')); }, 'stream abort() promise should still resolve if sink start() rejects'); promise_test(t => { const ws = new WritableStream(); const writer = ws.getWriter(); const writerReady1 = writer.ready; writer.abort('a'); const writerReady2 = writer.ready; assert_not_equals(writerReady1, writerReady2, 'abort() should replace the ready promise with a rejected one'); return Promise.all([writerReady1, promise_rejects(t, new TypeError(), writerReady2, 'writerReady2 should reject')]); }, 'writer abort() during sink start() should replace the writer.ready promise synchronously'); promise_test(t => { const events = []; const ws = recordingWritableStream(); const writer = ws.getWriter(); const writePromise1 = writer.write(1); const abortPromise = writer.abort('a'); const writePromise2 = writer.write(2); const closePromise = writer.close(); writePromise1.catch(() => events.push('write1')); abortPromise.then(() => events.push('abort')); writePromise2.catch(() => events.push('write2')); closePromise.catch(() => events.push('close')); return Promise.all([ promise_rejects(t, new TypeError(), writePromise1, 'first write() should reject'), abortPromise, promise_rejects(t, new TypeError(), writePromise2, 'second write() should reject'), promise_rejects(t, new TypeError(), closePromise, 'close() should reject') ]) .then(() => { assert_array_equals(events, ['write2', 'write1', 'abort', 'close'], 'promises should resolve in the standard order'); assert_array_equals(ws.events, ['abort', 'a'], 'underlying sink write() should not be called'); }); }, 'promises returned from other writer methods should be rejected when writer abort() happens during sink start()'); promise_test(t => { let writeReject; let controller; const ws = new WritableStream({ write(chunk, c) { controller = c; return new Promise((resolve, reject) => { writeReject = reject; }); } }); const writer = ws.getWriter(); return writer.ready.then(() => { const writePromise = writer.write('a'); const abortPromise = writer.abort(); controller.error(error1); writeReject(error2); return Promise.all([ promise_rejects(t, error2, writePromise, 'write() should reject with error2'), abortPromise ]); }); }, 'abort() should succeed despite rejection from write'); promise_test(t => { let closeReject; let controller; const ws = new WritableStream({ start(c) { controller = c; }, close() { return new Promise((resolve, reject) => { closeReject = reject; }); } }); const writer = ws.getWriter(); return writer.ready.then(() => { const closePromise = writer.close(); const abortPromise = writer.abort(); controller.error(error1); closeReject(error2); return Promise.all([ promise_rejects(t, error2, closePromise, 'close() should reject with error2'), promise_rejects(t, error2, abortPromise, 'abort() should reject with error2') ]); }); }, 'abort() should be rejected with the rejection returned from close()'); promise_test(t => { let rejectWrite; const ws = recordingWritableStream({ write() { return new Promise((resolve, reject) => { rejectWrite = reject; }); } }); const writer = ws.getWriter(); return writer.ready.then(() => { const writePromise = writer.write('1'); const abortPromise = writer.abort(error2); rejectWrite(error1); return Promise.all([ promise_rejects(t, error1, writePromise, 'write should reject'), abortPromise, promise_rejects(t, new TypeError(), writer.closed, 'closed should reject with TypeError') ]); }).then(() => { assert_array_equals(ws.events, ['write', '1', 'abort', error2], 'abort sink method should be called'); }); }, 'a rejecting sink.write() should not prevent sink.abort() from being called'); promise_test(() => { const ws = recordingWritableStream({ start() { return Promise.reject(error1); } }); return ws.abort(error2) .then(() => { assert_array_equals(ws.events, ['abort', error2]); }); }, 'when start errors after stream abort(), underlying sink abort() should be called anyway'); promise_test(t => { const ws = new WritableStream(); const abortPromise1 = ws.abort(); const abortPromise2 = ws.abort(); return Promise.all([ abortPromise1, promise_rejects(t, new TypeError(), abortPromise2, 'second abort() should reject') ]); }, 'when calling abort() twice on the same stream, the second call should reject'); promise_test(t => { let controller; let resolveWrite; const ws = recordingWritableStream({ start(c) { controller = c; }, write() { return new Promise(resolve => { resolveWrite = resolve; }); } }); const writer = ws.getWriter(); return writer.ready.then(() => { const writePromise = writer.write('chunk'); controller.error(error1); const abortPromise = writer.abort(error2); resolveWrite(); return Promise.all([ writePromise, promise_rejects(t, error1, abortPromise, 'abort() should reject') ]).then(() => { assert_array_equals(ws.events, ['write', 'chunk'], 'sink abort() should not be called'); }); }); }, 'sink abort() should not be called if stream was erroring due to controller.error() before abort() was called'); promise_test(t => { let resolveWrite; let size = 1; const ws = recordingWritableStream({ write() { return new Promise(resolve => { resolveWrite = resolve; }); } }, { size() { return size; }, highWaterMark: 1 }); const writer = ws.getWriter(); return writer.ready.then(() => { const writePromise1 = writer.write('chunk1'); size = NaN; const writePromise2 = writer.write('chunk2'); const abortPromise = writer.abort(error2); resolveWrite(); return Promise.all([ writePromise1, promise_rejects(t, new RangeError(), writePromise2, 'second write() should reject'), promise_rejects(t, new RangeError(), abortPromise, 'abort() should reject') ]).then(() => { assert_array_equals(ws.events, ['write', 'chunk1'], 'sink abort() should not be called'); }); }); }, 'sink abort() should not be called if stream was erroring due to bad strategy before abort() was called'); done();