// META: global=window,worker // META: script=../resources/test-utils.js // META: script=../resources/rs-utils.js 'use strict'; test(() => { new TransformStream({ transform() { } }); }, 'TransformStream can be constructed with a transform function'); test(() => { new TransformStream(); new TransformStream({}); }, 'TransformStream can be constructed with no transform function'); test(() => { const ts = new TransformStream({ transform() { } }); const writer = ts.writable.getWriter(); assert_equals(writer.desiredSize, 1, 'writer.desiredSize should be 1'); }, 'TransformStream writable starts in the writable state'); promise_test(() => { const ts = new TransformStream(); const writer = ts.writable.getWriter(); writer.write('a'); assert_equals(writer.desiredSize, 0, 'writer.desiredSize should be 0 after write()'); return ts.readable.getReader().read().then(result => { assert_equals(result.value, 'a', 'result from reading the readable is the same as was written to writable'); assert_false(result.done, 'stream should not be done'); return delay(0).then(() => assert_equals(writer.desiredSize, 1, 'desiredSize should be 1 again')); }); }, 'Identity TransformStream: can read from readable what is put into writable'); promise_test(() => { let c; const ts = new TransformStream({ start(controller) { c = controller; }, transform(chunk) { c.enqueue(chunk.toUpperCase()); } }); const writer = ts.writable.getWriter(); writer.write('a'); return ts.readable.getReader().read().then(result => { assert_equals(result.value, 'A', 'result from reading the readable is the transformation of what was written to writable'); assert_false(result.done, 'stream should not be done'); }); }, 'Uppercaser sync TransformStream: can read from readable transformed version of what is put into writable'); promise_test(() => { let c; const ts = new TransformStream({ start(controller) { c = controller; }, transform(chunk) { c.enqueue(chunk.toUpperCase()); c.enqueue(chunk.toUpperCase()); } }); const writer = ts.writable.getWriter(); writer.write('a'); const reader = ts.readable.getReader(); return reader.read().then(result1 => { assert_equals(result1.value, 'A', 'the first chunk read is the transformation of the single chunk written'); assert_false(result1.done, 'stream should not be done'); return reader.read().then(result2 => { assert_equals(result2.value, 'A', 'the second chunk read is also the transformation of the single chunk written'); assert_false(result2.done, 'stream should not be done'); }); }); }, 'Uppercaser-doubler sync TransformStream: can read both chunks put into the readable'); promise_test(() => { let c; const ts = new TransformStream({ start(controller) { c = controller; }, transform(chunk) { return delay(0).then(() => c.enqueue(chunk.toUpperCase())); } }); const writer = ts.writable.getWriter(); writer.write('a'); return ts.readable.getReader().read().then(result => { assert_equals(result.value, 'A', 'result from reading the readable is the transformation of what was written to writable'); assert_false(result.done, 'stream should not be done'); }); }, 'Uppercaser async TransformStream: can read from readable transformed version of what is put into writable'); promise_test(() => { let doSecondEnqueue; let returnFromTransform; const ts = new TransformStream({ transform(chunk, controller) { delay(0).then(() => controller.enqueue(chunk.toUpperCase())); doSecondEnqueue = () => controller.enqueue(chunk.toUpperCase()); return new Promise(resolve => { returnFromTransform = resolve; }); } }); const reader = ts.readable.getReader(); const writer = ts.writable.getWriter(); writer.write('a'); return reader.read().then(result1 => { assert_equals(result1.value, 'A', 'the first chunk read is the transformation of the single chunk written'); assert_false(result1.done, 'stream should not be done'); doSecondEnqueue(); return reader.read().then(result2 => { assert_equals(result2.value, 'A', 'the second chunk read is also the transformation of the single chunk written'); assert_false(result2.done, 'stream should not be done'); returnFromTransform(); }); }); }, 'Uppercaser-doubler async TransformStream: can read both chunks put into the readable'); promise_test(() => { const ts = new TransformStream({ transform() { } }); const writer = ts.writable.getWriter(); writer.close(); return Promise.all([writer.closed, ts.readable.getReader().closed]); }, 'TransformStream: by default, closing the writable closes the readable (when there are no queued writes)'); promise_test(() => { let transformResolve; const transformPromise = new Promise(resolve => { transformResolve = resolve; }); const ts = new TransformStream({ transform() { return transformPromise; } }, undefined, { highWaterMark: 1 }); const writer = ts.writable.getWriter(); writer.write('a'); writer.close(); let rsClosed = false; ts.readable.getReader().closed.then(() => { rsClosed = true; }); return delay(0).then(() => { assert_equals(rsClosed, false, 'readable is not closed after a tick'); transformResolve(); return writer.closed.then(() => { // TODO: Is this expectation correct? assert_equals(rsClosed, true, 'readable is closed at that point'); }); }); }, 'TransformStream: by default, closing the writable waits for transforms to finish before closing both'); promise_test(() => { let c; const ts = new TransformStream({ start(controller) { c = controller; }, transform() { c.enqueue('x'); c.enqueue('y'); return delay(0); } }); const writer = ts.writable.getWriter(); writer.write('a'); writer.close(); const readableChunks = readableStreamToArray(ts.readable); return writer.closed.then(() => { return readableChunks.then(chunks => { assert_array_equals(chunks, ['x', 'y'], 'both enqueued chunks can be read from the readable'); }); }); }, 'TransformStream: by default, closing the writable closes the readable after sync enqueues and async done'); promise_test(() => { let c; const ts = new TransformStream({ start(controller) { c = controller; }, transform() { return delay(0) .then(() => c.enqueue('x')) .then(() => c.enqueue('y')) .then(() => delay(0)); } }); const writer = ts.writable.getWriter(); writer.write('a'); writer.close(); const readableChunks = readableStreamToArray(ts.readable); return writer.closed.then(() => { return readableChunks.then(chunks => { assert_array_equals(chunks, ['x', 'y'], 'both enqueued chunks can be read from the readable'); }); }); }, 'TransformStream: by default, closing the writable closes the readable after async enqueues and async done'); promise_test(() => { let c; const ts = new TransformStream({ suffix: '-suffix', start(controller) { c = controller; c.enqueue('start' + this.suffix); }, transform(chunk) { c.enqueue(chunk + this.suffix); }, flush() { c.enqueue('flushed' + this.suffix); } }); const writer = ts.writable.getWriter(); writer.write('a'); writer.close(); const readableChunks = readableStreamToArray(ts.readable); return writer.closed.then(() => { return readableChunks.then(chunks => { assert_array_equals(chunks, ['start-suffix', 'a-suffix', 'flushed-suffix'], 'all enqueued chunks have suffixes'); }); }); }, 'Transform stream should call transformer methods as methods'); promise_test(() => { function functionWithOverloads() {} functionWithOverloads.apply = () => assert_unreached('apply() should not be called'); functionWithOverloads.call = () => assert_unreached('call() should not be called'); const ts = new TransformStream({ start: functionWithOverloads, transform: functionWithOverloads, flush: functionWithOverloads }); const writer = ts.writable.getWriter(); writer.write('a'); writer.close(); return readableStreamToArray(ts.readable); }, 'methods should not not have .apply() or .call() called'); promise_test(t => { let startCalled = false; let startDone = false; let transformDone = false; let flushDone = false; const ts = new TransformStream({ start() { startCalled = true; return flushAsyncEvents().then(() => { startDone = true; }); }, transform() { return t.step(() => { assert_true(startDone, 'transform() should not be called until the promise returned from start() has resolved'); return flushAsyncEvents().then(() => { transformDone = true; }); }); }, flush() { return t.step(() => { assert_true(transformDone, 'flush() should not be called until the promise returned from transform() has resolved'); return flushAsyncEvents().then(() => { flushDone = true; }); }); } }, undefined, { highWaterMark: 1 }); assert_true(startCalled, 'start() should be called synchronously'); const writer = ts.writable.getWriter(); const writePromise = writer.write('a'); return writer.close().then(() => { assert_true(flushDone, 'promise returned from flush() should have resolved'); return writePromise; }); }, 'TransformStream start, transform, and flush should be strictly ordered'); promise_test(() => { let transformCalled = false; const ts = new TransformStream({ transform() { transformCalled = true; } }, undefined, { highWaterMark: Infinity }); // transform() is only called synchronously when there is no backpressure and all microtasks have run. return delay(0).then(() => { const writePromise = ts.writable.getWriter().write(); assert_true(transformCalled, 'transform() should have been called'); return writePromise; }); }, 'it should be possible to call transform() synchronously'); promise_test(() => { const ts = new TransformStream({}, undefined, { highWaterMark: 0 }); const writer = ts.writable.getWriter(); writer.close(); return Promise.all([writer.closed, ts.readable.getReader().closed]); }, 'closing the writable should close the readable when there are no queued chunks, even with backpressure'); test(() => { new TransformStream({ start(controller) { controller.terminate(); assert_throws_js(TypeError, () => controller.enqueue(), 'enqueue should throw'); } }); }, 'enqueue() should throw after controller.terminate()'); promise_test(() => { let controller; const ts = new TransformStream({ start(c) { controller = c; } }); const cancelPromise = ts.readable.cancel(); assert_throws_js(TypeError, () => controller.enqueue(), 'enqueue should throw'); return cancelPromise; }, 'enqueue() should throw after readable.cancel()'); test(() => { new TransformStream({ start(controller) { controller.terminate(); controller.terminate(); } }); }, 'controller.terminate() should do nothing the second time it is called'); promise_test(t => { let controller; const ts = new TransformStream({ start(c) { controller = c; } }); const cancelReason = { name: 'cancelReason' }; const cancelPromise = ts.readable.cancel(cancelReason); controller.terminate(); return Promise.all([ cancelPromise, promise_rejects_exactly(t, cancelReason, ts.writable.getWriter().closed, 'closed should reject with cancelReason') ]); }, 'terminate() should do nothing after readable.cancel()'); promise_test(() => { let calls = 0; new TransformStream({ start() { ++calls; } }); return flushAsyncEvents().then(() => { assert_equals(calls, 1, 'start() should have been called exactly once'); }); }, 'start() should not be called twice'); test(() => { assert_throws_js(RangeError, () => new TransformStream({ readableType: 'bytes' }), 'constructor should throw'); }, 'specifying a defined readableType should throw'); test(() => { assert_throws_js(RangeError, () => new TransformStream({ writableType: 'bytes' }), 'constructor should throw'); }, 'specifying a defined writableType should throw'); test(() => { class Subclass extends TransformStream { extraFunction() { return true; } } assert_equals( Object.getPrototypeOf(Subclass.prototype), TransformStream.prototype, 'Subclass.prototype\'s prototype should be TransformStream.prototype'); assert_equals(Object.getPrototypeOf(Subclass), TransformStream, 'Subclass\'s prototype should be TransformStream'); const sub = new Subclass(); assert_true(sub instanceof TransformStream, 'Subclass object should be an instance of TransformStream'); assert_true(sub instanceof Subclass, 'Subclass object should be an instance of Subclass'); const readableGetter = Object.getOwnPropertyDescriptor( TransformStream.prototype, 'readable').get; assert_equals(readableGetter.call(sub), sub.readable, 'Subclass object should pass brand check'); assert_true(sub.extraFunction(), 'extraFunction() should be present on Subclass object'); }, 'Subclassing TransformStream should work');