/** * Copyright (c) 2019 The xterm.js authors. All rights reserved. * @license MIT */ import { assert } from 'chai'; import { WriteBuffer } from './WriteBuffer'; // eslint-disable-next-line @typescript-eslint/naming-convention declare let Buffer: any; function toBytes(s: string): Uint8Array { return Buffer.from(s); } function fromBytes(bytes: Uint8Array): string { return bytes.toString(); } describe('WriteBuffer', () => { let wb: WriteBuffer; let stack: (string | Uint8Array)[] = []; let cbStack: string[] = []; beforeEach(() => { stack = []; cbStack = []; wb = new WriteBuffer(data => { stack.push(data); }); }); describe('write input', () => { it('string', done => { wb.write('a._'); wb.write('b.x', () => { cbStack.push('b'); }); wb.write('c._'); wb.write('d.x', () => { cbStack.push('d'); }); wb.write('e', () => { assert.deepEqual(stack, ['a._', 'b.x', 'c._', 'd.x', 'e']); assert.deepEqual(cbStack, ['b', 'd']); done(); }); }); it('bytes', done => { wb.write(toBytes('a._')); wb.write(toBytes('b.x'), () => { cbStack.push('b'); }); wb.write(toBytes('c._')); wb.write(toBytes('d.x'), () => { cbStack.push('d'); }); wb.write(toBytes('e'), () => { assert.deepEqual(stack.map(val => typeof val === 'string' ? '' : fromBytes(val)), ['a._', 'b.x', 'c._', 'd.x', 'e']); assert.deepEqual(cbStack, ['b', 'd']); done(); }); }); it('string/bytes mixed', done => { wb.write('a._'); wb.write('b.x', () => { cbStack.push('b'); }); wb.write(toBytes('c._')); wb.write(toBytes('d.x'), () => { cbStack.push('d'); }); wb.write(toBytes('e'), () => { assert.deepEqual(stack.map(val => typeof val === 'string' ? val : fromBytes(val)), ['a._', 'b.x', 'c._', 'd.x', 'e']); assert.deepEqual(cbStack, ['b', 'd']); done(); }); }); it('write callback works for empty chunks', done => { wb.write('a', () => { cbStack.push('a'); }); wb.write('', () => { cbStack.push('b'); }); wb.write(toBytes('c'), () => { cbStack.push('c'); }); wb.write(new Uint8Array(0), () => { cbStack.push('d'); }); wb.write('e', () => { assert.deepEqual(stack.map(val => typeof val === 'string' ? val : fromBytes(val)), ['a', '', 'c', '', 'e']); assert.deepEqual(cbStack, ['a', 'b', 'c', 'd']); done(); }); }); it('writeSync', done => { wb.write('a', () => { cbStack.push('a'); }); wb.write('b', () => { cbStack.push('b'); }); wb.write('c', () => { cbStack.push('c'); }); wb.writeSync('d'); assert.deepEqual(stack, ['a', 'b', 'c', 'd']); assert.deepEqual(cbStack, ['a', 'b', 'c']); wb.write('x', () => { cbStack.push('x'); }); wb.write('', () => { assert.deepEqual(stack, ['a', 'b', 'c', 'd', 'x', '']); assert.deepEqual(cbStack, ['a', 'b', 'c', 'x']); done(); }); }); it('writeSync called from action does not overflow callstack - issue #3265', () => { wb = new WriteBuffer(data => { const num = parseInt(data as string); if (num < 10000) { wb.writeSync('' + (num + 1)); } }); wb.writeSync('1'); }); it('writeSync maxSubsequentCalls argument', () => { let last: string = ''; wb = new WriteBuffer(data => { last = data as string; const num = parseInt(data as string); if (num < 1000000) { wb.writeSync('' + (num + 1), 10); } }); wb.writeSync('1', 10); assert.equal(last, '11'); // 1 + 10 sub calls = 11 }); it('flushSync processes all pending writes', done => { wb.write('a', () => { cbStack.push('a'); }); wb.write('b', () => { cbStack.push('b'); }); wb.write('c', () => { cbStack.push('c'); }); wb.flushSync(); assert.deepEqual(stack, ['a', 'b', 'c']); assert.deepEqual(cbStack, ['a', 'b', 'c']); wb.write('x', () => { cbStack.push('x'); }); wb.write('', () => { assert.deepEqual(stack, ['a', 'b', 'c', 'x', '']); assert.deepEqual(cbStack, ['a', 'b', 'c', 'x']); done(); }); }); it('flushSync with no pending writes is a no-op', () => { wb.flushSync(); assert.deepEqual(stack, []); assert.deepEqual(cbStack, []); }); it('flushSync fires onWriteParsed', () => { let parsed = 0; wb.onWriteParsed(() => parsed++); wb.write('a'); wb.write('b'); assert.equal(parsed, 0); wb.flushSync(); assert.equal(parsed, 1); }); it('flushSync with no pending writes does not fire onWriteParsed', () => { let parsed = 0; wb.onWriteParsed(() => parsed++); wb.flushSync(); assert.equal(parsed, 0); }); it('dispose cancels scheduled innerWrite', done => { wb.write('a'); wb.dispose(); setTimeout(() => { assert.deepEqual(stack, []); done(); }, 20); }); it('dispose does not fire onWriteParsed for pending writes', done => { let parsed = 0; wb.onWriteParsed(() => parsed++); wb.write('a'); wb.dispose(); setTimeout(() => { assert.equal(parsed, 0); done(); }, 20); }); it('write after dispose is a no-op', done => { wb.dispose(); wb.write('a'); setTimeout(() => { assert.deepEqual(stack, []); done(); }, 20); }); it('dispose is idempotent', done => { wb.write('a'); wb.dispose(); wb.dispose(); setTimeout(() => { assert.deepEqual(stack, []); done(); }, 20); }); it('async handler continuation is skipped after dispose', done => { let resolve!: (value: boolean) => void; const pending = new Promise(r => { resolve = r; }); wb = new WriteBuffer(() => pending); wb.write('a'); wb.dispose(); resolve(true); setTimeout(() => { assert.deepEqual(stack, []); done(); }, 20); }); it('handleUserInput still processes first chunk synchronously', () => { wb.handleUserInput(); wb.write('a'); assert.deepEqual(stack, ['a']); }); it('flushSync after dispose is a no-op', done => { wb.write('a'); wb.dispose(); wb.flushSync(); setTimeout(() => { assert.deepEqual(stack, []); done(); }, 20); }); it('writeSync after dispose is a no-op', () => { wb.dispose(); wb.writeSync('a'); assert.deepEqual(stack, []); }); }); });