<!DOCTYPE html>
<script src='../../resources/testharness.js'></script>
<script src='../../resources/testharnessreport.js'></script>
<script src='resources/streams-utils.js'></script>
<script>
// This is updated till https://github.com/whatwg/streams/commit/4ba861e6f60c248060811830e11271c84b439cc3

test(function() {
    new TransformStream({ transform: function() { } }); // TransformStream constructed with no errors.
}, 'TransformStream can be constructed with a transform function');

test(function() {
    assert_throws_js(TypeError, function() { new TransformStream(); }, 'TransformStream cannot be constructed with no arguments');
    assert_throws_js(TypeError, function() { new TransformStream({}); }, 'TransformStream cannot be constructed with an empty object');
}, 'TransformStream cannot be constructed with no transform function');

test(function() {
    var ts = new TransformStream({ transform: function() { } });

    assert_true(Object.prototype.hasOwnProperty.call(ts, 'writable'), 'it has a writable property');
    assert_true(ts.writable instanceof WritableStream, 'writable is an instance of WritableStream');

    assert_true(Object.prototype.hasOwnProperty.call(ts, 'readable'), 'it has a readable property');
    assert_true(ts.readable instanceof ReadableStream, 'readable is an instance of ReadableStream');
}, 'TransformStream instances must have writable and readable properties of the correct types');

test(function() {
    var ts = new TransformStream({ transform: function() { } });

    assert_equals(ts.writable.state, 'writable', 'writable starts writable');
}, 'TransformStream writable starts in the writable state');

var test1 = async_test('Pass-through sync TransformStream: can read from readable what is put into writable');
test1.step(function()
{
    var ts = new TransformStream({
        transform: function(chunk, enqueue, done) {
            enqueue(chunk);
            done();
        }
    });

    ts.writable.write('a');

    assert_equals(ts.writable.state, 'waiting', 'writable is waiting after one write');
    ts.readable.getReader().read().then(test1.step_func(function(result) {
        assert_object_equals(result, { value: 'a', done: false }, 'result from reading the readable is the same as was written to writable');

        return ts.writable.ready.then(test1.step_func(function() {
            assert_equals(ts.writable.state, 'writable', 'writable becomes writable again');
            test1.done();
        }));
    })).catch(test1.step_func(function(e) { assert_unreached(e); }));
});

var test2 = async_test('Uppercaser sync TransformStream: can read from readable transformed version of what is put into writable');
test2.step(function()
{
    var ts = new TransformStream({
        transform: function(chunk, enqueue, done) {
            enqueue(chunk.toUpperCase());
            done();
        }
    });

    ts.writable.write('a');

    assert_equals(ts.writable.state, 'waiting', 'writable is waiting after one write');

    ts.readable.getReader().read().then(test2.step_func(function(result) {
        assert_object_equals(result, { value: 'A', done: false },
                             'result from reading the readable is the transformation of what was written to writable');

        return ts.writable.ready.then(test2.step_func(function() {
            assert_equals(ts.writable.state, 'writable', 'writable becomes writable again');
            test2.done();
        }));
    })).catch(test2.step_func(function(e) { assert_unreached(e); }));
});

var test3 = async_test('Uppercaser-doubler sync TransformStream: can read both chunks put into the readable');
test3.step(function()
{
    var ts = new TransformStream({
        transform: function(chunk, enqueue, done) {
            enqueue(chunk.toUpperCase());
            enqueue(chunk.toUpperCase());
            done();
        }
    });

    ts.writable.write('a');

    assert_equals(ts.writable.state, 'waiting', 'writable is waiting after one write');

    var reader = ts.readable.getReader();

    reader.read().then(test3.step_func(function(result1) {
        assert_object_equals(result1, { value: 'A', done: false },
                             'the first chunk read is the transformation of the single chunk written');

        return reader.read().then(test3.step_func(function(result2) {
            assert_object_equals(result2, { value: 'A', done: false },
                                 'the second chunk read is also the transformation of the single chunk written');

            return ts.writable.ready.then(test3.step_func(function() {
                assert_equals(ts.writable.state, 'writable', 'writable becomes writable again');
                test3.done();
            }));
        }));
    })).catch(test3.step_func(function(e) { assert_unreached(e); }));
});

var test4 = async_test('Uppercaser async TransformStream: can read from readable transformed version of what is put into writable');
test4.step(function()
{
    var ts = new TransformStream({
        transform: function(chunk, enqueue, done) {
            setTimeout(test4.step_func(function() { enqueue(chunk.toUpperCase()); }), 100);
            setTimeout(test4.step_func(done), 500);
        }
    });

    ts.writable.write('a');

    assert_equals(ts.writable.state, 'waiting', 'writable is waiting after one write');

    ts.readable.getReader().read().then(test4.step_func(function(result) {
        assert_object_equals(result, { value: 'A', done: false },
                             'result from reading the readable is the transformation of what was written to writable');

        return ts.writable.ready.then(test4.step_func(function() {
            assert_equals(ts.writable.state, 'writable', 'writable becomes writable again');
            test4.done();
        }));
    })).catch(test4.step_func(function(e) { assert_unreached(e); }));
});

var test5 = async_test('Uppercaser-doubler async TransformStream: can read both chunks put into the readable');
test5.step(function()
{
    var ts = new TransformStream({
        transform: function(chunk, enqueue, done) {
            setTimeout(test5.step_func(function() { enqueue(chunk.toUpperCase()); }), 100);
            setTimeout(test5.step_func(function() { enqueue(chunk.toUpperCase()); }), 500);
            setTimeout(test5.step_func(done), 900);
        }
    });

    var reader = ts.readable.getReader();

    ts.writable.write('a');

    assert_equals(ts.writable.state, 'waiting', 'writable is waiting after one write');
    reader.read().then(test5.step_func(function(result1) {
        assert_object_equals(result1, { value: 'A', done: false },
                             'the first chunk read is the transformation of the single chunk written');

        return reader.read().then(test5.step_func(function(result2) {
            assert_object_equals(result2, { value: 'A', done: false },
                                 'the second chunk read is also the transformation of the single chunk written');

            return ts.writable.ready.then(test5.step_func(function() {
                assert_equals(ts.writable.state, 'writable', 'writable becomes writable again');
                test5.done();
            }));
        }));
    })).catch(test5.step_func(function(e) { assert_unreached(e); }));
});

var test6 = async_test('TransformStream: by default, closing the writable closes the readable (when there are no queued writes)');
test6.step(function()
{
    var ts = new TransformStream({ transform: function() { } });

    ts.writable.close();
    assert_equals(ts.writable.state, 'closing', 'writable is closing');

    Promise.all([ts.writable.closed, ts.readable.getReader().closed]).then(test6.step_func(function() {
        assert_equals(ts.writable.state, 'closed', 'writable state becomes closed eventually');
        test6.done('both writable and readable closed promises fulfill');
    })).catch(test6.step_func(function(e) { assert_unreached(e); }));
});

var test7 = async_test('TransformStream: by default, closing the writable waits for transforms to finish before closing both');
test7.step(function()
{
    var ts = new TransformStream({
        transform: function(chunk, enqueue, done) {
            setTimeout(test7.step_func(done), 500);
        }
    });

    ts.writable.write('a');
    ts.writable.close();
    assert_equals(ts.writable.state, 'closing', 'writable is closing');

    var rsClosed = false;
    ts.readable.getReader().closed.then(test7.step_func(function() {
        rsClosed = true;
    }));

    setTimeout(test7.step_func(function() {
        assert_equals(rsClosed, false, 'readable is not closed after a tick');

        ts.writable.closed.then(test7.step_func(function() {
            assert_equals(ts.writable.state, 'closed', 'writable becomes closed eventually');
            assert_true(rsClosed, 'readable is closed at that point');
            test7.done();
        })).catch(test7.step_func(function(e) { assert_unreached(e); }));
    }), 0);
});

var test8 = async_test('TransformStream: by default, closing the writable closes the readable after sync enqueues and async done');
test8.step(function()
{
    var ts = new TransformStream({
        transform: function(chunk, enqueue, done) {
            enqueue('x');
            enqueue('y');
            setTimeout(test8.step_func(done), 500);
        }
    });

    ts.writable.write('a');
    ts.writable.close();
    assert_equals(ts.writable.state, 'closing', 'writable is closing');

    ts.writable.closed.then(test8.step_func(function() {
        assert_equals(ts.writable.state, 'closed', 'writable becomes closed eventually');

        return readableStreamToArray(ts.readable).then(test8.step_func(function(chunks) {
            assert_array_equals(chunks, ['x', 'y'], 'both enqueued chunks can be read from the readable');
            test8.done();
        }));
    })).catch(test8.step_func(function(e) { assert_unreached(e); }));
});

var test9 = async_test('TransformStream: by default, closing the writable closes the readable after async enqueues and async done');
test9.step(function()
{
    var ts = new TransformStream({
        transform: function(chunk, enqueue, done) {
            setTimeout(test9.step_func(function() { enqueue('x'); }), 100);
            setTimeout(test9.step_func(function() { enqueue('y'); }), 500);
            setTimeout(test9.step_func(done), 900);
        }
    });

    ts.writable.write('a');
    ts.writable.close();
    assert_equals(ts.writable.state, 'closing', 'writable is closing');

    ts.writable.closed.then(test9.step_func(function() {
        assert_equals(ts.writable.state, 'closed', 'writable becomes closed eventually');

        return readableStreamToArray(ts.readable).then(test9.step_func(function(chunks) {
            assert_array_equals(chunks, ['x', 'y'], 'both enqueued chunks can be read from the readable');
            test9.done();
        }));
    })).catch(test9.step_func(function(e) { assert_unreached(e); }));
});

var test10 = async_test('TransformStream flush is called immediately when the writable is closed, if no writes are queued');
test10.step(function()
{
    var flushCalled = false;
    var ts = new TransformStream({
        transform: function() { },
        flush: function(enqueue) {
            flushCalled = true;
        }
    });

    ts.writable.close().then(test10.step_func(function() {
        assert_true(flushCalled, 'closing the writable triggers the transform flush immediately');
        test10.done();
    }));
});

var test11 = async_test('TransformStream flush is called after all queued writes finish, once the writable is closed');
test11.step(function()
{
    var flushCalled = false;
    var ts = new TransformStream({
        transform: function(chunk, enqueue, done) {
            setTimeout(test11.step_func(done), 100);
        },
        flush: function(enqueue) {
            flushCalled = true;
        }
    });

    ts.writable.write('a');
    ts.writable.close();
    assert_false(flushCalled, 'closing the writable does not immediately call flush if writes are not finished');

    var rsClosed = false;
    ts.readable.getReader().closed.then(test11.step_func(function() {
        rsClosed = true;
    }));

    setTimeout(test11.step_func(function() {
        assert_true(flushCalled, 'flush is eventually called');
        assert_false(rsClosed, 'if flush does not call close, the readable does not become closed');
        test11.done();
    }), 500);
});

var test12 = async_test('TransformStream flush gets a chance to enqueue more into the readable');
test12.step(function()
{
    var ts = new TransformStream({
        transform: function(chunk, enqueue, done) {
            done();
        },
        flush: function(enqueue) {
            enqueue('x');
            enqueue('y');
        }
    });

    var reader = ts.readable.getReader();

    ts.writable.write('a');
    ts.writable.close();
    reader.read().then(test12.step_func(function(result1) {
        assert_object_equals(result1, { value: 'x', done: false }, 'the first chunk read is the first one enqueued in flush');

        return reader.read().then(test12.step_func(function(result2) {
            assert_object_equals(result2, { value: 'y', done: false }, 'the second chunk read is the second one enqueued in flush');
            test12.done();
        }));
    })).catch(test12.step_func(function(e) { assert_unreached(e); }));
});

var test13 = async_test('TransformStream flush gets a chance to enqueue more into the readable, and can then async close');
test13.step(function()
{
    var promiseCalls = 0;
    var ts = new TransformStream({
        transform: function(chunk, enqueue, done) {
            done();
        },
        flush: function(enqueue, close) {
            enqueue('x');
            enqueue('y');
            setTimeout(test13.step_func(close), 100);
        }
    });

    var reader = ts.readable.getReader();

    ts.writable.write('a');
    ts.writable.close();
    reader.read().then(test13.step_func(function(result1) {
        assert_equals(++promiseCalls, 1);
        assert_object_equals(result1, { value: 'x', done: false }, 'the first chunk read is the first one enqueued in flush');

        return reader.read().then(test13.step_func(function(result2) {
            assert_equals(++promiseCalls, 2);
            assert_object_equals(result2, { value: 'y', done: false }, 'the second chunk read is the second one enqueued in flush');
        }));
    })).catch(test13.step_func(function(e) { assert_unreached(e); }));

    reader.closed.then(test13.step_func(function() {
        assert_equals(++promiseCalls, 3);
        test13.done('readable reader becomes closed');
    })).catch(test13.step_func(function(e) { assert_unreached(e); }));
});

var test14 = async_test('Transform stream should call transformer methods as methods');
test14.step(function()
{
    var ts = new TransformStream({
        suffix: '-suffix',

        transform: function(chunk, enqueue, done) {
            enqueue(chunk + this.suffix);
            done();
        },

        flush: function(enqueue, close) {
            enqueue('flushed' + this.suffix);
            close();
        }
    });

    ts.writable.write('a');
    ts.writable.close();
    ts.writable.closed.then(
        test14.step_func(function() {
            return readableStreamToArray(ts.readable).then(test14.step_func(function(chunks) {
                assert_array_equals(chunks, ['a-suffix', 'flushed-suffix'], 'both enqueued chunks have suffixes');
                test14.done();
            }));
        }),
        test14.step_func(function(e) { assert_unreached(e); })
    );
});
</script>
