'use strict'; if (self.importScripts) { self.importScripts('/resources/testharness.js'); self.importScripts('../resources/test-utils.js'); self.importScripts('../resources/rs-utils.js'); } 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 proto = Object.getPrototypeOf(ts); const writableStream = Object.getOwnPropertyDescriptor(proto, 'writable'); assert_true(writableStream !== undefined, 'it has a writable property'); assert_false(writableStream.enumerable, 'writable should be non-enumerable'); assert_equals(typeof writableStream.get, 'function', 'writable should have a getter'); assert_equals(writableStream.set, undefined, 'writable should not have a setter'); assert_true(writableStream.configurable, 'writable should be configurable'); assert_true(ts.writable instanceof WritableStream, 'writable is an instance of WritableStream'); assert_not_equals(WritableStream.prototype.getWriter.call(ts.writable), undefined, 'writable should pass WritableStream brand check'); const readableStream = Object.getOwnPropertyDescriptor(proto, 'readable'); assert_true(readableStream !== undefined, 'it has a readable property'); assert_false(readableStream.enumerable, 'readable should be non-enumerable'); assert_equals(typeof readableStream.get, 'function', 'readable should have a getter'); assert_equals(readableStream.set, undefined, 'readable should not have a setter'); assert_true(readableStream.configurable, 'readable should be configurable'); assert_true(ts.readable instanceof ReadableStream, 'readable is an instance of ReadableStream'); assert_not_equals(ReadableStream.prototype.getReader.call(ts.readable), undefined, 'readable should pass ReadableStream brand check'); }, 'TransformStream instances must have writable and readable properties of the correct types'); 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(new 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(new 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(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(new RangeError(), () => new TransformStream({ readableType: 'bytes' }), 'constructor should throw'); }, 'specifying a defined readableType should throw'); test(() => { assert_throws(new RangeError(), () => new TransformStream({ writableType: 'bytes' }), 'constructor should throw'); }, 'specifying a defined writableType should throw'); done();