diff --git a/tests/README.md b/tests/README.md
new file mode 100644
index 00000000..6846bb11
--- /dev/null
+++ b/tests/README.md
@@ -0,0 +1,43 @@
+# KasmVNC Client Tests
+
+The page `/tests/vnc_playback.html` can be used to playback KasmVNC session recordings. The playbacks can be ran in realtime or as fast as possible for performance testing.
+
+## Creating new recordings
+
+In order to create a new recording, you will need to disable KasmVNC's built-in web server, enable the legacy VNC TCP port, and disable authentication.
+
+```bash
+sudo apt-get install websockify
+vncserver -noWebsocket -disableBasicAuth
+websockify --web /usr/share/kasmvnc/www --record=/home/ubuntu/record.bin 8444 localhost:5901
+```
+
+Websockify automatically adds a number to the end of the filename, so the above example might be record.bin.8. After you are finished recording, Ctrl+C the running websockify process and mv the file to the noVNC www directory.
+
+```bash
+sudo mkdir /usr/share/kasmvnc/www/recordings
+mv /home/ubuntu/record.bin.8 /usr/share/kasmvnc/www/recordings
+```
+
+## Playing Back Recordings
+
+Place recordings on the KasmVNC server in the /usr/share/kasmvnc/www/recordings directory, you may need to create this directory. Then navigate to https://server-ip:8444/tests/vnc_playback.html?data=record.bin.8 where record.bin.8 is the name of the playback file you placed in the recordings directory.
+
+## Kasm Provided Recordings
+
+The following recordings are used by Kasm Technologies to provide repeatable performance statisitics using different rendering settings.
+
+| Name | Description | URL|
+|------|-------|----|
+| newyork.1 | Default 'Static' preset mode. | https://kasm-static-content.s3.amazonaws.com/kasmvnc/playbacktests/newyork.1 |
+| losangeles.1 | Default static preset mode with webp disabled | https://kasm-static-content.s3.amazonaws.com/kasmvnc/playbacktests/losangeles.1 |
+
+
+## Historical Statistics
+
+This table keeps track of performance of pre-defined recordings, defined in the previous section, on static hardware that can be replicated over time to track performance improvements.
+
+| File | Hardware | OS | Browser | Webpacked | Result Avg |
+|------|---------|----|---------|-------|---------|
+| newyork.1 | Macbook M1 Pro, 32GB RAM | macOS 12.2 | Chrome 106 | False | 2446ms |
+| losangeles.1 | Macbook M1 Pro, 32GB RAM | macOS 12.2 | Chrome 106 | False | 2272ms |
\ No newline at end of file
diff --git a/tests/playback.js b/tests/playback.js
index 962307c0..e1e326e5 100644
--- a/tests/playback.js
+++ b/tests/playback.js
@@ -42,6 +42,24 @@ if (window.setImmediate === undefined) {
});
}
+class FakeWebSocket {
+ constructor() {
+ this.binaryType = "arraybuffer";
+ this.protocol = "";
+ this.readyState = "open";
+
+ this.onerror = () => {};
+ this.onmessage = () => {};
+ this.onopen = () => {};
+ }
+
+ send() {
+ }
+
+ close() {
+ }
+}
+
export default class RecordingPlayer {
constructor(frames, disconnected) {
this._frames = frames;
@@ -63,13 +81,13 @@ export default class RecordingPlayer {
run(realtime, trafficManagement) {
// initialize a new RFB
- this._rfb = new RFB(document.getElementById('VNC_screen'), 'wss://test');
+ this._ws = new FakeWebSocket();
+ this._rfb = new RFB(document.getElementById('VNC_screen'), document.getElementById('noVNC_keyboardinput'), this._ws);
this._rfb.viewOnly = true;
this._rfb.addEventListener("disconnect",
this._handleDisconnect.bind(this));
this._rfb.addEventListener("credentialsrequired",
this._handleCredentials.bind(this));
- this._enablePlaybackMode();
// reset the frame index and timer
this._frameIndex = 0;
@@ -79,19 +97,7 @@ export default class RecordingPlayer {
this._trafficManagement = (trafficManagement === undefined) ? !realtime : trafficManagement;
this._running = true;
- }
-
- // _enablePlaybackMode mocks out things not required for running playback
- _enablePlaybackMode() {
- const self = this;
- this._rfb._sock.send = () => {};
- this._rfb._sock.close = () => {};
- this._rfb._sock.flush = () => {};
- this._rfb._sock.open = function () {
- this.init();
- this._eventHandlers.open();
- self._queueNextPacket();
- };
+ this._queueNextPacket();
}
_queueNextPacket() {
@@ -136,7 +142,7 @@ export default class RecordingPlayer {
const frame = this._frames[this._frameIndex];
- this._rfb._sock._recvMessage({'data': frame.data});
+ this._ws.onmessage({'data': frame.data});
this._frameIndex++;
this._queueNextPacket();
@@ -153,7 +159,7 @@ export default class RecordingPlayer {
this._rfb._display.flush();
} else {
this._running = false;
- this._rfb._sock._eventHandlers.close({code: 1000, reason: ""});
+ this._ws.onclose({code: 1000, reason: ""});
delete this._rfb;
this.onfinish((new Date()).getTime() - this._startTime);
}
diff --git a/tests/test.inflator.js b/tests/test.inflator.js
new file mode 100644
index 00000000..533bcd86
--- /dev/null
+++ b/tests/test.inflator.js
@@ -0,0 +1,113 @@
+/* eslint-disable no-console */
+const expect = chai.expect;
+
+import { deflateInit, deflate, Z_FULL_FLUSH } from "../vendor/pako/lib/zlib/deflate.js";
+import ZStream from "../vendor/pako/lib/zlib/zstream.js";
+import Inflator from "../core/inflator.js";
+
+function _deflator(data) {
+ let strm = new ZStream();
+
+ deflateInit(strm, 5);
+
+ /* eslint-disable camelcase */
+ strm.input = data;
+ strm.avail_in = strm.input.length;
+ strm.next_in = 0;
+ /* eslint-enable camelcase */
+
+ let chunks = [];
+ let totalLen = 0;
+ while (strm.avail_in > 0) {
+ /* eslint-disable camelcase */
+ strm.output = new Uint8Array(1024 * 10 * 10);
+ strm.avail_out = strm.output.length;
+ strm.next_out = 0;
+ /* eslint-enable camelcase */
+
+ let ret = deflate(strm, Z_FULL_FLUSH);
+
+ // Check that return code is not an error
+ expect(ret).to.be.greaterThan(-1);
+
+ let chunk = new Uint8Array(strm.output.buffer, 0, strm.next_out);
+ totalLen += chunk.length;
+ chunks.push(chunk);
+ }
+
+ // Combine chunks into a single data
+
+ let outData = new Uint8Array(totalLen);
+ let offset = 0;
+
+ for (let i = 0; i < chunks.length; i++) {
+ outData.set(chunks[i], offset);
+ offset += chunks[i].length;
+ }
+
+ return outData;
+}
+
+describe('Inflate data', function () {
+
+ it('should be able to inflate messages', function () {
+ let inflator = new Inflator();
+
+ let text = "123asdf";
+ let preText = new Uint8Array(text.length);
+ for (let i = 0; i < preText.length; i++) {
+ preText[i] = text.charCodeAt(i);
+ }
+
+ let compText = _deflator(preText);
+
+ inflator.setInput(compText);
+ let inflatedText = inflator.inflate(preText.length);
+
+ expect(inflatedText).to.array.equal(preText);
+
+ });
+
+ it('should be able to inflate large messages', function () {
+ let inflator = new Inflator();
+
+ /* Generate a big string with random characters. Used because
+ repetition of letters might be deflated more effectively than
+ random ones. */
+ let text = "";
+ let characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+ for (let i = 0; i < 300000; i++) {
+ text += characters.charAt(Math.floor(Math.random() * characters.length));
+ }
+
+ let preText = new Uint8Array(text.length);
+ for (let i = 0; i < preText.length; i++) {
+ preText[i] = text.charCodeAt(i);
+ }
+
+ let compText = _deflator(preText);
+
+ //Check that the compressed size is expected size
+ expect(compText.length).to.be.greaterThan((1024 * 10 * 10) * 2);
+
+ inflator.setInput(compText);
+ let inflatedText = inflator.inflate(preText.length);
+
+ expect(inflatedText).to.array.equal(preText);
+ });
+
+ it('should throw an error on insufficient data', function () {
+ let inflator = new Inflator();
+
+ let text = "123asdf";
+ let preText = new Uint8Array(text.length);
+ for (let i = 0; i < preText.length; i++) {
+ preText[i] = text.charCodeAt(i);
+ }
+
+ let compText = _deflator(preText);
+
+ inflator.setInput(compText);
+ expect(() => inflator.inflate(preText.length * 2)).to.throw();
+ });
+});
diff --git a/tests/test.jpeg.js b/tests/test.jpeg.js
new file mode 100644
index 00000000..6834f03d
--- /dev/null
+++ b/tests/test.jpeg.js
@@ -0,0 +1,288 @@
+const expect = chai.expect;
+
+import Websock from '../core/websock.js';
+import Display from '../core/display.js';
+
+import JPEGDecoder from '../core/decoders/jpeg.js';
+
+import FakeWebSocket from './fake.websocket.js';
+
+function testDecodeRect(decoder, x, y, width, height, data, display, depth) {
+ let sock;
+
+ sock = new Websock;
+ sock.open("ws://example.com");
+
+ sock.on('message', () => {
+ decoder.decodeRect(x, y, width, height, sock, display, depth);
+ });
+
+ // Empty messages are filtered at multiple layers, so we need to
+ // do a direct call
+ if (data.length === 0) {
+ decoder.decodeRect(x, y, width, height, sock, display, depth);
+ } else {
+ sock._websocket._receiveData(new Uint8Array(data));
+ }
+
+ display.flip();
+}
+
+describe('JPEG Decoder', function () {
+ let decoder;
+ let display;
+
+ before(FakeWebSocket.replace);
+ after(FakeWebSocket.restore);
+
+ beforeEach(function () {
+ decoder = new JPEGDecoder();
+ display = new Display(document.createElement('canvas'));
+ display.resize(4, 4);
+ });
+
+ it('should handle JPEG rects', function (done) {
+ let data = [
+ // JPEG data
+ 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46,
+ 0x49, 0x46, 0x00, 0x01, 0x01, 0x01, 0x01, 0x2c,
+ 0x01, 0x2c, 0x00, 0x42, 0xff, 0xdb, 0x00, 0x43,
+ 0x00, 0x03, 0x02, 0x02, 0x03, 0x02, 0x02, 0x03,
+ 0x03, 0x03, 0x03, 0x04, 0x03, 0x03, 0x04, 0x05,
+ 0x08, 0x05, 0x05, 0x04, 0x04, 0x05, 0x0a, 0x07,
+ 0x07, 0x06, 0x08, 0x0c, 0x0a, 0x0c, 0x0c, 0x0b,
+ 0x0a, 0x0b, 0x0b, 0x0d, 0x0e, 0x12, 0x10, 0x0d,
+ 0x0e, 0x11, 0x0e, 0x0b, 0x0b, 0x10, 0x16, 0x10,
+ 0x11, 0x13, 0x14, 0x15, 0x15, 0x15, 0x0c, 0x0f,
+ 0x17, 0x18, 0x16, 0x14, 0x18, 0x12, 0x14, 0x15,
+ 0x14, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x03, 0x04,
+ 0x04, 0x05, 0x04, 0x05, 0x09, 0x05, 0x05, 0x09,
+ 0x14, 0x0d, 0x0b, 0x0d, 0x14, 0x14, 0x14, 0x14,
+ 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,
+ 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,
+ 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,
+ 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,
+ 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,
+ 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0xff, 0xc0,
+ 0x00, 0x11, 0x08, 0x00, 0x04, 0x00, 0x04, 0x03,
+ 0x01, 0x11, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11,
+ 0x01, 0xff, 0xc4, 0x00, 0x1f, 0x00, 0x00, 0x01,
+ 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+ 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09,
+ 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00,
+ 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05,
+ 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7d, 0x01,
+ 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21,
+ 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22,
+ 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08, 0x23,
+ 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24,
+ 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17,
+ 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28, 0x29,
+ 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a,
+ 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a,
+ 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a,
+ 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a,
+ 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a,
+ 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a,
+ 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99,
+ 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8,
+ 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7,
+ 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6,
+ 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5,
+ 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2, 0xe3,
+ 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1,
+ 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9,
+ 0xfa, 0xff, 0xc4, 0x00, 0x1f, 0x01, 0x00, 0x03,
+ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+ 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+ 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09,
+ 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x11, 0x00,
+ 0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07,
+ 0x05, 0x04, 0x04, 0x00, 0x01, 0x02, 0x77, 0x00,
+ 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31,
+ 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13,
+ 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xa1,
+ 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15,
+ 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1,
+ 0x25, 0xf1, 0x17, 0x18, 0x19, 0x1a, 0x26, 0x27,
+ 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39,
+ 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49,
+ 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59,
+ 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,
+ 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79,
+ 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88,
+ 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97,
+ 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6,
+ 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5,
+ 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4,
+ 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3,
+ 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe2,
+ 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea,
+ 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9,
+ 0xfa, 0xff, 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00,
+ 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0xf9,
+ 0xf7, 0xfb, 0x67, 0x56, 0xff, 0x00, 0x9f, 0xf8,
+ 0x3f, 0xf0, 0x51, 0xa7, 0xff, 0x00, 0xf2, 0x3d,
+ 0x7e, 0x6f, 0xfd, 0xab, 0x94, 0x7f, 0xd0, 0x9a,
+ 0x8f, 0xfe, 0x0d, 0xc7, 0x7f, 0xf3, 0x61, 0xfd,
+ 0xa7, 0xff, 0x00, 0x10, 0x77, 0x0d, 0xff, 0x00,
+ 0x43, 0xec, 0xcf, 0xff, 0x00, 0x0b, 0xab, 0x1f,
+ 0xff, 0xd9,
+ ];
+
+ testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);
+
+ let targetData = new Uint8Array([
+ 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255,
+ 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
+ 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
+ 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255
+ ]);
+
+ // Browsers have rounding errors, so we need an approximate
+ // comparing function
+ function almost(a, b) {
+ let diff = Math.abs(a - b);
+ return diff < 5;
+ }
+
+ display.onflush = () => {
+ expect(display).to.have.displayed(targetData, almost);
+ done();
+ };
+ display.flush();
+ });
+
+ it('should handle JPEG rects without Huffman and quantification tables', function (done) {
+ let data1 = [
+ // JPEG data
+ 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46,
+ 0x49, 0x46, 0x00, 0x01, 0x01, 0x01, 0x01, 0x2c,
+ 0x01, 0x2c, 0x00, 0x42, 0xff, 0xdb, 0x00, 0x43,
+ 0x00, 0x03, 0x02, 0x02, 0x03, 0x02, 0x02, 0x03,
+ 0x03, 0x03, 0x03, 0x04, 0x03, 0x03, 0x04, 0x05,
+ 0x08, 0x05, 0x05, 0x04, 0x04, 0x05, 0x0a, 0x07,
+ 0x07, 0x06, 0x08, 0x0c, 0x0a, 0x0c, 0x0c, 0x0b,
+ 0x0a, 0x0b, 0x0b, 0x0d, 0x0e, 0x12, 0x10, 0x0d,
+ 0x0e, 0x11, 0x0e, 0x0b, 0x0b, 0x10, 0x16, 0x10,
+ 0x11, 0x13, 0x14, 0x15, 0x15, 0x15, 0x0c, 0x0f,
+ 0x17, 0x18, 0x16, 0x14, 0x18, 0x12, 0x14, 0x15,
+ 0x14, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x03, 0x04,
+ 0x04, 0x05, 0x04, 0x05, 0x09, 0x05, 0x05, 0x09,
+ 0x14, 0x0d, 0x0b, 0x0d, 0x14, 0x14, 0x14, 0x14,
+ 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,
+ 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,
+ 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,
+ 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,
+ 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,
+ 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0xff, 0xc0,
+ 0x00, 0x11, 0x08, 0x00, 0x04, 0x00, 0x04, 0x03,
+ 0x01, 0x11, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11,
+ 0x01, 0xff, 0xc4, 0x00, 0x1f, 0x00, 0x00, 0x01,
+ 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+ 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09,
+ 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00,
+ 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05,
+ 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7d, 0x01,
+ 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21,
+ 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22,
+ 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08, 0x23,
+ 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24,
+ 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17,
+ 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28, 0x29,
+ 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a,
+ 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a,
+ 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a,
+ 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a,
+ 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a,
+ 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a,
+ 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99,
+ 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8,
+ 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7,
+ 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6,
+ 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5,
+ 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2, 0xe3,
+ 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1,
+ 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9,
+ 0xfa, 0xff, 0xc4, 0x00, 0x1f, 0x01, 0x00, 0x03,
+ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+ 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+ 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09,
+ 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x11, 0x00,
+ 0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07,
+ 0x05, 0x04, 0x04, 0x00, 0x01, 0x02, 0x77, 0x00,
+ 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31,
+ 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13,
+ 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xa1,
+ 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15,
+ 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1,
+ 0x25, 0xf1, 0x17, 0x18, 0x19, 0x1a, 0x26, 0x27,
+ 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39,
+ 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49,
+ 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59,
+ 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,
+ 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79,
+ 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88,
+ 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97,
+ 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6,
+ 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5,
+ 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4,
+ 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3,
+ 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe2,
+ 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea,
+ 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9,
+ 0xfa, 0xff, 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00,
+ 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0xf9,
+ 0xf7, 0xfb, 0x67, 0x56, 0xff, 0x00, 0x9f, 0xf8,
+ 0x3f, 0xf0, 0x51, 0xa7, 0xff, 0x00, 0xf2, 0x3d,
+ 0x7e, 0x6f, 0xfd, 0xab, 0x94, 0x7f, 0xd0, 0x9a,
+ 0x8f, 0xfe, 0x0d, 0xc7, 0x7f, 0xf3, 0x61, 0xfd,
+ 0xa7, 0xff, 0x00, 0x10, 0x77, 0x0d, 0xff, 0x00,
+ 0x43, 0xec, 0xcf, 0xff, 0x00, 0x0b, 0xab, 0x1f,
+ 0xff, 0xd9,
+ ];
+
+ testDecodeRect(decoder, 0, 0, 4, 4, data1, display, 24);
+
+ let data2 = [
+ // JPEG data
+ 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46,
+ 0x49, 0x46, 0x00, 0x01, 0x01, 0x01, 0x01, 0x2c,
+ 0x01, 0x2c, 0x00, 0x73, 0xff, 0xc0, 0x00, 0x11,
+ 0x08, 0x00, 0x04, 0x00, 0x04, 0x03, 0x01, 0x11,
+ 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff,
+ 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11,
+ 0x03, 0x11, 0x00, 0x3f, 0x00, 0xf9, 0xf7, 0xfb,
+ 0x67, 0x56, 0xff, 0x00, 0x9f, 0xf8, 0x3f, 0xf0,
+ 0x51, 0xa7, 0xff, 0x00, 0xf2, 0x3d, 0x7e, 0x6f,
+ 0xfd, 0xab, 0x94, 0x7f, 0xd0, 0x9a, 0x8f, 0xfe,
+ 0x0d, 0xc7, 0x7f, 0xf3, 0x61, 0xfd, 0xa7, 0xff,
+ 0x00, 0x10, 0x77, 0x0d, 0xff, 0x00, 0x43, 0xec,
+ 0xcf, 0xff, 0x00, 0x0b, 0xab, 0x1f, 0xff, 0xd9,
+ ];
+
+ testDecodeRect(decoder, 0, 0, 4, 4, data2, display, 24);
+
+ let targetData = new Uint8Array([
+ 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255,
+ 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
+ 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
+ 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255
+ ]);
+
+ // Browsers have rounding errors, so we need an approximate
+ // comparing function
+ function almost(a, b) {
+ let diff = Math.abs(a - b);
+ return diff < 5;
+ }
+
+ display.onflush = () => {
+ expect(display).to.have.displayed(targetData, almost);
+ done();
+ };
+ display.flush();
+ });
+});
diff --git a/tests/test.ra2.js b/tests/test.ra2.js
new file mode 100644
index 00000000..cc505b1f
--- /dev/null
+++ b/tests/test.ra2.js
@@ -0,0 +1,357 @@
+const expect = chai.expect;
+
+import RFB from '../core/rfb.js';
+
+import FakeWebSocket from './fake.websocket.js';
+
+function fakeGetRandomValues(arr) {
+ if (arr.length === 16) {
+ arr.set(new Uint8Array([
+ 0x1c, 0x08, 0xfe, 0x21, 0x78, 0xef, 0x4e, 0xf9,
+ 0x3f, 0x05, 0xec, 0xea, 0xd4, 0x6b, 0xa5, 0xd5,
+ ]));
+ } else {
+ arr.set(new Uint8Array([
+ 0xee, 0xe2, 0xf1, 0x5a, 0x3c, 0xa7, 0xbe, 0x95,
+ 0x6f, 0x2a, 0x75, 0xfd, 0x62, 0x01, 0xcb, 0xbf,
+ 0x43, 0x74, 0xca, 0x47, 0x4d, 0xfb, 0x0f, 0xcf,
+ 0x3a, 0x6d, 0x55, 0x6b, 0x59, 0x3a, 0xf6, 0x87,
+ 0xcb, 0x03, 0xb7, 0x28, 0x35, 0x7b, 0x15, 0x8e,
+ 0xb6, 0xc8, 0x8f, 0x2d, 0x5e, 0x7b, 0x1c, 0x9a,
+ 0x32, 0x55, 0xe7, 0x64, 0x36, 0x25, 0x7b, 0xa3,
+ 0xe9, 0x4f, 0x6f, 0x97, 0xdc, 0xa4, 0xd4, 0x62,
+ 0x6d, 0x7f, 0xab, 0x02, 0x6b, 0x13, 0x56, 0x69,
+ 0xfb, 0xd0, 0xd4, 0x13, 0x76, 0xcd, 0x0d, 0xd0,
+ 0x1f, 0xd1, 0x0c, 0x63, 0x3a, 0x34, 0x20, 0x6c,
+ 0xbb, 0x60, 0x45, 0x82, 0x23, 0xfd, 0x7c, 0x77,
+ 0x6d, 0xcc, 0x5e, 0xaa, 0xc3, 0x0c, 0x43, 0xb7,
+ 0x8d, 0xc0, 0x27, 0x6e, 0xeb, 0x1d, 0x6c, 0x5f,
+ 0xd8, 0x1c, 0x3c, 0x1c, 0x60, 0x2e, 0x82, 0x15,
+ 0xfd, 0x2e, 0x5f, 0x3a, 0x15, 0x53, 0x14, 0x70,
+ 0x4f, 0xe1, 0x65, 0x68, 0x35, 0x6d, 0xc7, 0x64,
+ 0xdb, 0xdd, 0x09, 0x31, 0x4f, 0x7b, 0x6d, 0x6c,
+ 0x77, 0x59, 0x5e, 0x1e, 0xfa, 0x4b, 0x06, 0x14,
+ 0xbe, 0xdc, 0x9c, 0x3d, 0x7b, 0xed, 0xf3, 0x2b,
+ 0x19, 0x26, 0x11, 0x8e, 0x3f, 0xab, 0x73, 0x9a,
+ 0x0a, 0x3a, 0xaa, 0x85, 0x06, 0xd5, 0xca, 0x3f,
+ 0xc3, 0xe2, 0x33, 0x7f, 0x97, 0x74, 0x98, 0x8f,
+ 0x2f, 0xa5, 0xfc, 0x7e, 0xb1, 0x77, 0x71, 0x58,
+ 0xf0, 0xbc, 0x04, 0x59, 0xbb, 0xb4, 0xc6, 0xcc,
+ 0x0f, 0x06, 0xcd, 0xa2, 0xd5, 0x01, 0x2f, 0xb2,
+ 0x22, 0x0b, 0xfc, 0x1e, 0x59, 0x9f, 0xd3, 0x4f,
+ 0x30, 0x95, 0xc6, 0x80, 0x0f, 0x69, 0xf3, 0x4a,
+ 0xd4, 0x36, 0xb6, 0x5a, 0x0b, 0x16, 0x0d, 0x81,
+ 0x31, 0xb0, 0x69, 0xd4, 0x4e,
+ ]));
+ }
+}
+
+async function fakeGeneratekey() {
+ let key = JSON.parse('{"alg":"RSA-OAEP-256","d":"B7QR2yI8sXjo8vQhJpX9odqqR\
+6wIuPrTM1B1JJEKVeSrr7OYcc1FRJ52Vap9LIAU-ezigs9QDvWMxknB8motLnG69Wck37nt9_z4s8l\
+FQp0nROA-oaR92HW34KNL1b2fEVWGI0N86h730MvTJC5O2cmKeMezIG-oNqbbfFyP8AW-WLdDlgZm1\
+1-FjzhbVpb0Bc7nRSgBPSV-EY6Sl-LuglxDx4LaTdQW7QE_WXoRUt-GYGfTseuFQQK5WeoyX3yBtQy\
+dpauW6rrgyWdtP4hDFIoZsX6w1i-UMWMMwlIB5FdnUSi26igVGADGpV_vGMP36bv-EHp0bY-Qp0gpI\
+fLfgQ","dp":"Z1v5UceFfV2bhmbG19eGYb30jFxqoRBq36PKNY7IunMs1keYy0FpLbyGhtgMZ1Ymm\
+c8wEzGYsCPEP-ykcun_rlyu7YxmcnyC9YQqTqLyqvO-7rUqDvk9TMfdqWFP6heADRhKZmEbmcau6_m\
+2MwwK9kOkMKWvpqp8_TpJMnAH7zE","dq":"OBacRE15aY3NtCR4cvP5os3sT70JbDdDLHT3IHZM6r\
+E35CYNpLDia2chm_wnMcYvKFW9zC2ajRZ15i9c_VXQzS7ZlTaQYBFyMt7kVhxMEMFsPv1crD6t3uEI\
+j0LNuNYyy0jkon_LPZKQFK654CiL-L2YaNXOH4HbHP02dWeVQIE","e":"AQAB","ext":true,"ke\
+y_ops":["decrypt"],"kty":"RSA","n":"m1c92ZFk9ZI6l_O4YFiNxbv0Ng94SB3yThy1P_mcqr\
+GDQkRiGVdcTxAk38T9PgLztmspF-6U5TAHO-gSmmW88AC9m6f1Mspps6r7zl-M_OG-TwvGzf3BDz8z\
+Eg1FPbZV7whO1M4TCAZ0PqwG7qCc6nK1WiAhaKrSpzuPdL1igfNBsX7qu5wgw4ZTTGSLbVC_LfULQ5\
+FADgFTRXUSaxm1F8C_Lwy6a2e4nTcXilmtN2IHUjHegzm-Tq2HizmR3ARdWJpESYIW5-AXoiqj29tD\
+rqCmu2WPkB2psVp83IzZfaQNQzjNfvA8GpimkcDCkP5VMRrtKCcG4ZAFnO-A3NBX_Q","p":"2Q_lN\
+L7vCOBzAppYzCZo3WSh0hX-MOZyPUznks5U2TjmfdNZoL6_FJRiGyyLvwSiZFdEAAvpAyESFfFigng\
+AqMLSf448nPg15VUGj533CotsEM0WpoEr1JCgqdUbgDAfJQIBcwOmegBqd7lWm7uzEnRCvouB70ybk\
+JfpdprhkVE","q":"tzTt-F3g2u_3Ctj26Ho9iN_wC_W0lXGzslLt5nLmss8JqdLoDDrijjU-gjeRh\
+7lgiuHdUc3dorfFKbaMNOjoW3QKqt9oZ1JM0HKeRw0X2PnWW_0WK6DK5ASWDTXbMq2sUZqJvYEyL74\
+H2Zrt0RPAux7XQLEVgND6ROdXnMJ70O0","qi":"qfl4cXQkz4BNqa2De0-PfdU-8d1w3onnaGqx1D\
+s2fHzD_SJ4cNghn2TksoT9Qo64b3pUjH9igi2pyEjomk6D12N6FG0e10u7vFKv3W5YqUOgTpYdbcWH\
+dZ2qZWJU0XQZIrF8jLGTOO4GYP6_9sJ5R7Wk_0MdqQy8qvixWD4zLcY"}');
+ key = await window.crypto.subtle.importKey("jwk", key, {
+ name: "RSA-OAEP",
+ hash: {name: "SHA-256"}
+ }, true, ["decrypt"]);
+ return {privateKey: key};
+}
+
+const receiveData = new Uint8Array([
+ // server public key
+ 0x00, 0x00, 0x08, 0x00, 0xac, 0x1a, 0xbc, 0x42,
+ 0x8a, 0x2a, 0x69, 0x65, 0x54, 0xf8, 0x9a, 0xe6,
+ 0x43, 0xaa, 0xf7, 0x27, 0xf6, 0x2a, 0xf8, 0x8f,
+ 0x36, 0xd4, 0xae, 0x54, 0x0f, 0x16, 0x28, 0x08,
+ 0xc2, 0x5b, 0xca, 0x23, 0xdc, 0x27, 0x88, 0x1a,
+ 0x12, 0x82, 0xa8, 0x54, 0xea, 0x00, 0x99, 0x8d,
+ 0x02, 0x1d, 0x77, 0x4a, 0xeb, 0xd0, 0x93, 0x40,
+ 0x79, 0x86, 0xcb, 0x37, 0xd4, 0xb2, 0xc7, 0xcd,
+ 0x93, 0xe1, 0x00, 0x4d, 0x86, 0xff, 0x97, 0x33,
+ 0x0c, 0xad, 0x51, 0x47, 0x45, 0x85, 0x56, 0x07,
+ 0x65, 0x21, 0x7c, 0x57, 0x6d, 0x68, 0x7d, 0xd7,
+ 0x00, 0x43, 0x0c, 0x9d, 0x3b, 0xa1, 0x5a, 0x11,
+ 0xed, 0x51, 0x77, 0xf9, 0xd1, 0x5b, 0x33, 0xd7,
+ 0x1a, 0xeb, 0x65, 0x57, 0xc0, 0x01, 0x51, 0xff,
+ 0x9b, 0x82, 0xb3, 0xeb, 0x82, 0xc2, 0x1f, 0xca,
+ 0x47, 0xc0, 0x6a, 0x09, 0xe0, 0xf7, 0xda, 0x39,
+ 0x85, 0x12, 0xe7, 0x45, 0x8d, 0xb4, 0x1a, 0xda,
+ 0xcb, 0x86, 0x58, 0x52, 0x37, 0x66, 0x9d, 0x8a,
+ 0xce, 0xf2, 0x18, 0x78, 0x7d, 0x7f, 0xf0, 0x07,
+ 0x94, 0x8e, 0x6b, 0x17, 0xd9, 0x00, 0x2a, 0x3a,
+ 0xb9, 0xd4, 0x77, 0xde, 0x70, 0x85, 0xc4, 0x3a,
+ 0x62, 0x10, 0x02, 0xee, 0xba, 0xd8, 0xc0, 0x62,
+ 0xd0, 0x8e, 0xc1, 0x98, 0x19, 0x8e, 0x39, 0x0f,
+ 0x3e, 0x1d, 0x61, 0xb1, 0x93, 0x13, 0x59, 0x39,
+ 0xcb, 0x96, 0xf2, 0x17, 0xc9, 0xe1, 0x41, 0xd3,
+ 0x20, 0xdd, 0x62, 0x5e, 0x7d, 0x53, 0xd6, 0xb7,
+ 0x1d, 0xfe, 0x02, 0x18, 0x1f, 0xe0, 0xef, 0x3d,
+ 0x94, 0xe3, 0x0a, 0x9c, 0x59, 0x54, 0xd8, 0x98,
+ 0x16, 0x9c, 0x31, 0xda, 0x41, 0x0f, 0x2e, 0x71,
+ 0x68, 0xe0, 0xa2, 0x62, 0x3e, 0xe5, 0x25, 0x31,
+ 0xcf, 0xfc, 0x67, 0x63, 0xc3, 0xb0, 0xda, 0x3f,
+ 0x7b, 0x59, 0xbe, 0x7e, 0x9e, 0xa8, 0xd0, 0x01,
+ 0x4f, 0x43, 0x7f, 0x8d, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x01, 0x00, 0x01,
+ // server random
+ 0x01, 0x00, 0x5b, 0x58, 0x2a, 0x96, 0x2d, 0xbb,
+ 0x88, 0xec, 0xc3, 0x54, 0x00, 0xf3, 0xbb, 0xbe,
+ 0x17, 0xa3, 0x84, 0xd3, 0xef, 0xd8, 0x4a, 0x31,
+ 0x09, 0x20, 0xdd, 0xbc, 0x16, 0x9d, 0xc9, 0x5b,
+ 0x99, 0x62, 0x86, 0xfe, 0x0b, 0x28, 0x4b, 0xfe,
+ 0x5b, 0x56, 0x2d, 0xcb, 0x6e, 0x6f, 0xec, 0xf0,
+ 0x53, 0x0c, 0x33, 0x84, 0x93, 0xc9, 0xbf, 0x79,
+ 0xde, 0xb3, 0xb9, 0x29, 0x60, 0x78, 0xde, 0xe6,
+ 0x1d, 0xa7, 0x89, 0x48, 0x3f, 0xd1, 0x58, 0x66,
+ 0x27, 0x9c, 0xd4, 0x6e, 0x72, 0x9c, 0x6e, 0x4a,
+ 0xc0, 0x69, 0x79, 0x6f, 0x79, 0x0f, 0x13, 0xc4,
+ 0x20, 0xcf, 0xa6, 0xbb, 0xce, 0x18, 0x6d, 0xd5,
+ 0x9e, 0xd9, 0x67, 0xbe, 0x61, 0x43, 0x67, 0x11,
+ 0x76, 0x2f, 0xfd, 0x78, 0x75, 0x2b, 0x89, 0x35,
+ 0xdd, 0x0f, 0x13, 0x7f, 0xee, 0x78, 0xad, 0x32,
+ 0x56, 0x21, 0x81, 0x08, 0x1f, 0xcf, 0x4c, 0x29,
+ 0xa3, 0xeb, 0x89, 0x2d, 0xbe, 0xba, 0x8d, 0xe4,
+ 0x69, 0x28, 0xba, 0x53, 0x82, 0xce, 0x5c, 0xf6,
+ 0x5e, 0x5e, 0xa5, 0xb3, 0x88, 0xd8, 0x3d, 0xab,
+ 0xf4, 0x24, 0x9e, 0x3f, 0x04, 0xaf, 0xdc, 0x48,
+ 0x90, 0x53, 0x37, 0xe6, 0x82, 0x1d, 0xe0, 0x15,
+ 0x91, 0xa1, 0xc6, 0xa9, 0x54, 0xe5, 0x2a, 0xb5,
+ 0x64, 0x2d, 0x93, 0xc0, 0xc0, 0xe1, 0x0f, 0x6a,
+ 0x4b, 0xdb, 0x77, 0xf8, 0x4a, 0x0f, 0x83, 0x36,
+ 0xdd, 0x5e, 0x1e, 0xdd, 0x39, 0x65, 0xa2, 0x11,
+ 0xc2, 0xcf, 0x56, 0x1e, 0xa1, 0x29, 0xae, 0x11,
+ 0x9f, 0x3a, 0x82, 0xc7, 0xbd, 0x89, 0x6e, 0x59,
+ 0xb8, 0x59, 0x17, 0xcb, 0x65, 0xa0, 0x4b, 0x4d,
+ 0xbe, 0x33, 0x32, 0x85, 0x9c, 0xca, 0x5e, 0x95,
+ 0xc2, 0x5a, 0xd0, 0xc9, 0x8b, 0xf1, 0xf5, 0x14,
+ 0xcf, 0x76, 0x80, 0xc2, 0x24, 0x0a, 0x39, 0x7e,
+ 0x60, 0x64, 0xce, 0xd9, 0xb8, 0xad, 0x24, 0xa8,
+ 0xdf, 0xcb,
+ // server hash
+ 0x00, 0x14, 0x39, 0x30, 0x66, 0xb5, 0x66, 0x8a,
+ 0xcd, 0xb9, 0xda, 0xe0, 0xde, 0xcb, 0xf6, 0x47,
+ 0x5f, 0x54, 0x66, 0xe0, 0xbc, 0x49, 0x37, 0x01,
+ 0xf2, 0x9e, 0xef, 0xcc, 0xcd, 0x4d, 0x6c, 0x0e,
+ 0xc6, 0xab, 0x28, 0xd4, 0x7b, 0x13,
+ // subtype
+ 0x00, 0x01, 0x30, 0x2a, 0xc3, 0x0b, 0xc2, 0x1c,
+ 0xeb, 0x02, 0x44, 0x92, 0x5d, 0xfd, 0xf9, 0xa7,
+ 0x94, 0xd0, 0x19,
+]);
+
+const sendData = new Uint8Array([
+ // client public key
+ 0x00, 0x00, 0x08, 0x00, 0x9b, 0x57, 0x3d, 0xd9,
+ 0x91, 0x64, 0xf5, 0x92, 0x3a, 0x97, 0xf3, 0xb8,
+ 0x60, 0x58, 0x8d, 0xc5, 0xbb, 0xf4, 0x36, 0x0f,
+ 0x78, 0x48, 0x1d, 0xf2, 0x4e, 0x1c, 0xb5, 0x3f,
+ 0xf9, 0x9c, 0xaa, 0xb1, 0x83, 0x42, 0x44, 0x62,
+ 0x19, 0x57, 0x5c, 0x4f, 0x10, 0x24, 0xdf, 0xc4,
+ 0xfd, 0x3e, 0x02, 0xf3, 0xb6, 0x6b, 0x29, 0x17,
+ 0xee, 0x94, 0xe5, 0x30, 0x07, 0x3b, 0xe8, 0x12,
+ 0x9a, 0x65, 0xbc, 0xf0, 0x00, 0xbd, 0x9b, 0xa7,
+ 0xf5, 0x32, 0xca, 0x69, 0xb3, 0xaa, 0xfb, 0xce,
+ 0x5f, 0x8c, 0xfc, 0xe1, 0xbe, 0x4f, 0x0b, 0xc6,
+ 0xcd, 0xfd, 0xc1, 0x0f, 0x3f, 0x33, 0x12, 0x0d,
+ 0x45, 0x3d, 0xb6, 0x55, 0xef, 0x08, 0x4e, 0xd4,
+ 0xce, 0x13, 0x08, 0x06, 0x74, 0x3e, 0xac, 0x06,
+ 0xee, 0xa0, 0x9c, 0xea, 0x72, 0xb5, 0x5a, 0x20,
+ 0x21, 0x68, 0xaa, 0xd2, 0xa7, 0x3b, 0x8f, 0x74,
+ 0xbd, 0x62, 0x81, 0xf3, 0x41, 0xb1, 0x7e, 0xea,
+ 0xbb, 0x9c, 0x20, 0xc3, 0x86, 0x53, 0x4c, 0x64,
+ 0x8b, 0x6d, 0x50, 0xbf, 0x2d, 0xf5, 0x0b, 0x43,
+ 0x91, 0x40, 0x0e, 0x01, 0x53, 0x45, 0x75, 0x12,
+ 0x6b, 0x19, 0xb5, 0x17, 0xc0, 0xbf, 0x2f, 0x0c,
+ 0xba, 0x6b, 0x67, 0xb8, 0x9d, 0x37, 0x17, 0x8a,
+ 0x59, 0xad, 0x37, 0x62, 0x07, 0x52, 0x31, 0xde,
+ 0x83, 0x39, 0xbe, 0x4e, 0xad, 0x87, 0x8b, 0x39,
+ 0x91, 0xdc, 0x04, 0x5d, 0x58, 0x9a, 0x44, 0x49,
+ 0x82, 0x16, 0xe7, 0xe0, 0x17, 0xa2, 0x2a, 0xa3,
+ 0xdb, 0xdb, 0x43, 0xae, 0xa0, 0xa6, 0xbb, 0x65,
+ 0x8f, 0x90, 0x1d, 0xa9, 0xb1, 0x5a, 0x7c, 0xdc,
+ 0x8c, 0xd9, 0x7d, 0xa4, 0x0d, 0x43, 0x38, 0xcd,
+ 0x7e, 0xf0, 0x3c, 0x1a, 0x98, 0xa6, 0x91, 0xc0,
+ 0xc2, 0x90, 0xfe, 0x55, 0x31, 0x1a, 0xed, 0x28,
+ 0x27, 0x06, 0xe1, 0x90, 0x05, 0x9c, 0xef, 0x80,
+ 0xdc, 0xd0, 0x57, 0xfd, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x01, 0x00, 0x01,
+ // client random
+ 0x01, 0x00, 0x84, 0x7f, 0x26, 0x54, 0x74, 0xf6,
+ 0x47, 0xaf, 0x33, 0x64, 0x0d, 0xa6, 0xe5, 0x30,
+ 0xba, 0xe6, 0xe4, 0x8e, 0x50, 0x40, 0x71, 0x1c,
+ 0x0e, 0x06, 0x63, 0xf5, 0x07, 0x2a, 0x26, 0x68,
+ 0xd6, 0xcf, 0xa6, 0x80, 0x84, 0x5e, 0x64, 0xd4,
+ 0x5e, 0x62, 0x31, 0xfe, 0x44, 0x51, 0x0b, 0x7c,
+ 0x4d, 0x55, 0xc5, 0x4a, 0x7e, 0x0d, 0x4d, 0x9b,
+ 0x84, 0xb4, 0x32, 0x2b, 0x4d, 0x8a, 0x34, 0x8d,
+ 0xc8, 0xcf, 0x19, 0x3b, 0x64, 0x82, 0x27, 0x9e,
+ 0xa7, 0x70, 0x2a, 0xc1, 0xb8, 0xf3, 0x6a, 0x3a,
+ 0xf2, 0x75, 0x6e, 0x1d, 0xeb, 0xb6, 0x70, 0x7a,
+ 0x15, 0x18, 0x38, 0x00, 0xb4, 0x4f, 0x55, 0xb5,
+ 0xd8, 0x03, 0x4e, 0xb8, 0x53, 0xff, 0x80, 0x62,
+ 0xf1, 0x9d, 0x27, 0xe8, 0x2a, 0x3d, 0x98, 0x19,
+ 0x32, 0x09, 0x7e, 0x9a, 0xb0, 0xc7, 0x46, 0x23,
+ 0x10, 0x85, 0x35, 0x00, 0x96, 0xce, 0xb3, 0x2c,
+ 0x84, 0x8d, 0xf4, 0x9e, 0xa8, 0x42, 0x67, 0xed,
+ 0x09, 0xa6, 0x09, 0x97, 0xb3, 0x64, 0x26, 0xfb,
+ 0x71, 0x11, 0x9b, 0x3f, 0xbb, 0x57, 0xb8, 0x5b,
+ 0x2e, 0xc5, 0x2d, 0x8c, 0x5c, 0xf7, 0xef, 0x27,
+ 0x25, 0x88, 0x42, 0x45, 0x43, 0xa4, 0xe7, 0xde,
+ 0xea, 0xf9, 0x15, 0x7b, 0x5d, 0x66, 0x24, 0xce,
+ 0xf7, 0xc8, 0x2f, 0xc5, 0xc0, 0x3d, 0xcd, 0xf2,
+ 0x62, 0xfc, 0x1a, 0x5e, 0xec, 0xff, 0xf1, 0x1b,
+ 0xc8, 0xdb, 0xc1, 0x0f, 0x54, 0x66, 0x9e, 0xfd,
+ 0x99, 0x9b, 0x23, 0x70, 0x62, 0x37, 0x80, 0xad,
+ 0x91, 0x6b, 0x84, 0x85, 0x6a, 0x4c, 0x80, 0x9e,
+ 0x60, 0x8a, 0x93, 0xa3, 0xc8, 0x8e, 0xc4, 0x4b,
+ 0x4d, 0xb4, 0x8e, 0x3e, 0xaf, 0xce, 0xcd, 0x83,
+ 0xe5, 0x21, 0x90, 0x95, 0x20, 0x3c, 0x82, 0xb4,
+ 0x7c, 0xab, 0x63, 0x9c, 0xae, 0xc3, 0xc9, 0x71,
+ 0x1a, 0xec, 0x34, 0x18, 0x47, 0xec, 0x5c, 0x4d,
+ 0xed, 0x84,
+ // client hash
+ 0x00, 0x14, 0x9c, 0x91, 0x9e, 0x76, 0xcf, 0x1e,
+ 0x66, 0x87, 0x5e, 0x29, 0xf1, 0x13, 0x80, 0xea,
+ 0x7d, 0xec, 0xae, 0xf9, 0x60, 0x01, 0xd3, 0x6f,
+ 0xb7, 0x9e, 0xb2, 0xcd, 0x2d, 0xc8, 0xf8, 0x84,
+ 0xb2, 0x9f, 0xc3, 0x7e, 0xb4, 0xbe,
+ // credentials
+ 0x00, 0x08, 0x9d, 0xc8, 0x3a, 0xb8, 0x80, 0x4f,
+ 0xe3, 0x52, 0xdb, 0x62, 0x9e, 0x97, 0x64, 0x82,
+ 0xa8, 0xa1, 0x6b, 0x7e, 0x4d, 0x68, 0x8c, 0x29,
+ 0x91, 0x38,
+]);
+
+describe('RA2 handshake', function () {
+ let sock;
+ let rfb;
+ let sentData;
+
+ before(() => {
+ FakeWebSocket.replace();
+ sinon.stub(window.crypto, "getRandomValues").callsFake(fakeGetRandomValues);
+ sinon.stub(window.crypto.subtle, "generateKey").callsFake(fakeGeneratekey);
+ });
+ after(() => {
+ FakeWebSocket.restore();
+ window.crypto.getRandomValues.restore();
+ window.crypto.subtle.generateKey.restore();
+ });
+
+ it('should fire the serververification event', function (done) {
+ sentData = new Uint8Array();
+ rfb = new RFB(document.createElement('div'), "ws://example.com");
+ sock = rfb._sock;
+ sock.send = (data) => {
+ let res = new Uint8Array(sentData.length + data.length);
+ res.set(sentData);
+ res.set(data, sentData.length);
+ sentData = res;
+ };
+ rfb._rfbInitState = "Security";
+ rfb._rfbVersion = 3.8;
+ sock._websocket._receiveData(new Uint8Array([1, 6]));
+ rfb.addEventListener("serververification", (e) => {
+ expect(e.detail.publickey).to.eql(receiveData.slice(0, 516));
+ done();
+ });
+ sock._websocket._receiveData(receiveData);
+ });
+
+ it('should handle approveServer and fire the credentialsrequired event', function (done) {
+ rfb.addEventListener("credentialsrequired", (e) => {
+ expect(e.detail.types).to.eql(["password"]);
+ done();
+ });
+ rfb.approveServer();
+ });
+
+ it('should match sendData after sending credentials', function (done) {
+ rfb.addEventListener("securityresult", (event) => {
+ expect(sentData.slice(1)).to.eql(sendData);
+ done();
+ });
+ rfb.sendCredentials({ "password": "123456" });
+ });
+});
diff --git a/tests/test.rfb.js b/tests/test.rfb.js
index 672d8c98..75d1e118 100644
--- a/tests/test.rfb.js
+++ b/tests/test.rfb.js
@@ -71,6 +71,21 @@ function deflateWithSize(data) {
describe('Remote Frame Buffer Protocol Client', function () {
let clock;
let raf;
+ let fakeResizeObserver = null;
+ const realObserver = window.ResizeObserver;
+
+ // Since we are using fake timers we don't actually want
+ // to wait for the browser to observe the size change,
+ // that's why we use a fake ResizeObserver
+ class FakeResizeObserver {
+ constructor(handler) {
+ this.fire = handler;
+ fakeResizeObserver = this;
+ }
+ disconnect() {}
+ observe(target, options) {}
+ unobserve(target) {}
+ }
before(FakeWebSocket.replace);
after(FakeWebSocket.restore);
@@ -80,6 +95,9 @@ describe('Remote Frame Buffer Protocol Client', function () {
// sinon doesn't support this yet
raf = window.requestAnimationFrame;
window.requestAnimationFrame = setTimeout;
+ // We must do this in a 'before' since it needs to be set before
+ // the RFB constructor, which runs in beforeEach further down
+ window.ResizeObserver = FakeResizeObserver;
// Use a single set of buffers instead of reallocating to
// speed up tests
const sock = new Websock();
@@ -100,6 +118,7 @@ describe('Remote Frame Buffer Protocol Client', function () {
delete Websock.prototype.toString;
this.clock.restore();
window.requestAnimationFrame = raf;
+ window.ResizeObserver = realObserver;
});
let container;
@@ -140,38 +159,112 @@ describe('Remote Frame Buffer Protocol Client', function () {
}
describe('Connecting/Disconnecting', function () {
- describe('#RFB', function () {
- it('should set the current state to "connecting"', function () {
- const client = new RFB(document.createElement('div'), 'wss://host:8675');
- client._rfbConnectionState = '';
- this.clock.tick();
- expect(client._rfbConnectionState).to.equal('connecting');
+ describe('#RFB (constructor)', function () {
+ let open, attach;
+ beforeEach(function () {
+ open = sinon.spy(Websock.prototype, 'open');
+ attach = sinon.spy(Websock.prototype, 'attach');
+ });
+ afterEach(function () {
+ open.restore();
+ attach.restore();
});
it('should actually connect to the websocket', function () {
- const client = new RFB(document.createElement('div'), 'ws://HOST:8675/PATH');
- sinon.spy(client._sock, 'open');
- this.clock.tick();
- expect(client._sock.open).to.have.been.calledOnce;
- expect(client._sock.open).to.have.been.calledWith('ws://HOST:8675/PATH');
+ new RFB(document.createElement('div'), 'ws://HOST:8675/PATH');
+ expect(open).to.have.been.calledOnceWithExactly('ws://HOST:8675/PATH', []);
+ });
+
+ it('should pass on connection problems', function () {
+ open.restore();
+ open = sinon.stub(Websock.prototype, 'open');
+ open.throws(new Error('Failure'));
+ expect(() => new RFB(document.createElement('div'), 'ws://HOST:8675/PATH')).to.throw('Failure');
+ });
+
+ it('should handle WebSocket/RTCDataChannel objects', function () {
+ let sock = new FakeWebSocket('ws://HOST:8675/PATH', []);
+ new RFB(document.createElement('div'), sock);
+ expect(open).to.not.have.been.called;
+ expect(attach).to.have.been.calledOnceWithExactly(sock);
+ });
+
+ it('should handle already open WebSocket/RTCDataChannel objects', function () {
+ let sock = new FakeWebSocket('ws://HOST:8675/PATH', []);
+ sock._open();
+ const client = new RFB(document.createElement('div'), sock);
+ let callback = sinon.spy();
+ client.addEventListener('disconnect', callback);
+ expect(open).to.not.have.been.called;
+ expect(attach).to.have.been.calledOnceWithExactly(sock);
+ // Check if it is ready for some data
+ sock._receiveData(new Uint8Array(['R', 'F', 'B', '0', '0', '3', '0', '0', '8']));
+ expect(callback).to.not.have.been.called;
+ });
+
+ it('should refuse closed WebSocket/RTCDataChannel objects', function () {
+ let sock = new FakeWebSocket('ws://HOST:8675/PATH', []);
+ sock.readyState = WebSocket.CLOSED;
+ expect(() => new RFB(document.createElement('div'), sock)).to.throw();
+ });
+
+ it('should pass on attach problems', function () {
+ attach.restore();
+ attach = sinon.stub(Websock.prototype, 'attach');
+ attach.throws(new Error('Failure'));
+ let sock = new FakeWebSocket('ws://HOST:8675/PATH', []);
+ expect(() => new RFB(document.createElement('div'), sock)).to.throw('Failure');
});
});
describe('#disconnect', function () {
let client;
+ let close;
+
beforeEach(function () {
client = makeRFB();
+ close = sinon.stub(Websock.prototype, "close");
+ });
+ afterEach(function () {
+ close.restore();
});
- it('should go to state "disconnecting" before "disconnected"', function () {
- sinon.spy(client, '_updateConnectionState');
+ it('should start closing WebSocket', function () {
+ let callback = sinon.spy();
+ client.addEventListener('disconnect', callback);
client.disconnect();
- expect(client._updateConnectionState).to.have.been.calledTwice;
- expect(client._updateConnectionState.getCall(0).args[0])
- .to.equal('disconnecting');
- expect(client._updateConnectionState.getCall(1).args[0])
- .to.equal('disconnected');
- expect(client._rfbConnectionState).to.equal('disconnected');
+ expect(close).to.have.been.calledOnceWithExactly();
+ expect(callback).to.not.have.been.called;
+ });
+
+ it('should send disconnect event', function () {
+ let callback = sinon.spy();
+ client.addEventListener('disconnect', callback);
+ client.disconnect();
+ close.thisValues[0]._eventHandlers.close(new CloseEvent("close", { 'code': 1000, 'reason': "", 'wasClean': true }));
+ expect(callback).to.have.been.calledOnce;
+ expect(callback.args[0][0].detail.clean).to.be.true;
+ });
+
+ it('should force disconnect if disconnecting takes too long', function () {
+ let callback = sinon.spy();
+ client.addEventListener('disconnect', callback);
+ client.disconnect();
+ this.clock.tick(3 * 1000);
+ expect(callback).to.have.been.calledOnce;
+ expect(callback.args[0][0].detail.clean).to.be.true;
+ });
+
+ it('should not fail if disconnect completes before timeout', function () {
+ let callback = sinon.spy();
+ client.addEventListener('disconnect', callback);
+ client.disconnect();
+ client._updateConnectionState('disconnecting');
+ this.clock.tick(3 * 1000 / 2);
+ close.thisValues[0]._eventHandlers.close(new CloseEvent("close", { 'code': 1000, 'reason': "", 'wasClean': true }));
+ this.clock.tick(3 * 1000 / 2 + 1);
+ expect(callback).to.have.been.calledOnce;
+ expect(callback.args[0][0].detail.clean).to.be.true;
});
it('should unregister error event handler', function () {
@@ -302,6 +395,13 @@ describe('Remote Frame Buffer Protocol Client', function () {
client.focus();
expect(client._canvas.focus).to.have.been.calledOnce;
});
+
+ it('should include focus options', function () {
+ client._canvas.focus = sinon.spy();
+ client.focus({ foobar: 12, gazonk: true });
+ expect(client._canvas.focus).to.have.been.calledOnce;
+ expect(client._canvas.focus).to.have.been.calledWith({ foobar: 12, gazonk: true});
+ });
});
describe('#blur', function () {
@@ -396,6 +496,7 @@ describe('Remote Frame Buffer Protocol Client', function () {
describe('Clipping', function () {
let client;
+
beforeEach(function () {
client = makeRFB();
container.style.width = '70px';
@@ -421,9 +522,8 @@ describe('Remote Frame Buffer Protocol Client', function () {
container.style.width = '40px';
container.style.height = '50px';
- const event = new UIEvent('resize');
- window.dispatchEvent(event);
- clock.tick();
+ fakeResizeObserver.fire();
+ clock.tick(1000);
expect(client._display.viewportChangeSize).to.have.been.calledOnce;
expect(client._display.viewportChangeSize).to.have.been.calledWith(40, 50);
@@ -440,6 +540,10 @@ describe('Remote Frame Buffer Protocol Client', function () {
sinon.spy(client._display, "viewportChangeSize");
client._sock._websocket._receiveData(new Uint8Array(incoming));
+ // The resize will cause scrollbars on the container, this causes a
+ // resize observation in the browsers
+ fakeResizeObserver.fire();
+ clock.tick(1000);
// FIXME: Display implicitly calls viewportChangeSize() when
// resizing the framebuffer, hence calledTwice.
@@ -453,9 +557,8 @@ describe('Remote Frame Buffer Protocol Client', function () {
container.style.width = '40px';
container.style.height = '50px';
- const event = new UIEvent('resize');
- window.dispatchEvent(event);
- clock.tick();
+ fakeResizeObserver.fire();
+ clock.tick(1000);
expect(client._display.viewportChangeSize).to.not.have.been.called;
});
@@ -466,13 +569,38 @@ describe('Remote Frame Buffer Protocol Client', function () {
container.style.width = '40px';
container.style.height = '50px';
- const event = new UIEvent('resize');
- window.dispatchEvent(event);
- clock.tick();
+ fakeResizeObserver.fire();
+ clock.tick(1000);
expect(client._display.viewportChangeSize).to.not.have.been.called;
});
+ describe('Clipping and remote resize', function () {
+ beforeEach(function () {
+ // Given a remote (100, 100) larger than the container (70x80),
+ client._resize(100, 100);
+ client._supportsSetDesktopSize = true;
+ client.resizeSession = true;
+ sinon.spy(RFB.messages, "setDesktopSize");
+ });
+ afterEach(function () {
+ RFB.messages.setDesktopSize.restore();
+ });
+ it('should not change remote size when changing clipping', function () {
+ // When changing clipping the scrollbars of the container
+ // will appear and disappear and thus trigger resize observations
+ client.clipViewport = false;
+ fakeResizeObserver.fire();
+ clock.tick(1000);
+ client.clipViewport = true;
+ fakeResizeObserver.fire();
+ clock.tick(1000);
+
+ // Then no resize requests should be sent
+ expect(RFB.messages.setDesktopSize).to.not.have.been.called;
+ });
+ });
+
describe('Dragging', function () {
beforeEach(function () {
client.dragViewport = true;
@@ -618,9 +746,8 @@ describe('Remote Frame Buffer Protocol Client', function () {
container.style.width = '40px';
container.style.height = '50px';
- const event = new UIEvent('resize');
- window.dispatchEvent(event);
- clock.tick();
+ fakeResizeObserver.fire();
+ clock.tick(1000);
expect(client._display.autoscale).to.have.been.calledOnce;
expect(client._display.autoscale).to.have.been.calledWith(40, 50);
@@ -637,6 +764,10 @@ describe('Remote Frame Buffer Protocol Client', function () {
sinon.spy(client._display, "autoscale");
client._sock._websocket._receiveData(new Uint8Array(incoming));
+ // The resize will cause scrollbars on the container, this causes a
+ // resize observation in the browsers
+ fakeResizeObserver.fire();
+ clock.tick(1000);
expect(client._display.autoscale).to.have.been.calledOnce;
expect(client._display.autoscale).to.have.been.calledWith(70, 80);
@@ -649,9 +780,8 @@ describe('Remote Frame Buffer Protocol Client', function () {
container.style.width = '40px';
container.style.height = '50px';
- const event = new UIEvent('resize');
- window.dispatchEvent(event);
- clock.tick();
+ fakeResizeObserver.fire();
+ clock.tick(1000);
expect(client._display.autoscale).to.not.have.been.called;
});
@@ -681,20 +811,39 @@ describe('Remote Frame Buffer Protocol Client', function () {
it('should request a resize when initially connecting', function () {
// Simple ExtendedDesktopSize FBU message
- const incoming = [ 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
- 0x00, 0x04, 0x00, 0x04, 0xff, 0xff, 0xfe, 0xcc,
- 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x04,
- 0x00, 0x00, 0x00, 0x00 ];
+ const incoming = [ 0x00, // msg-type=FBU
+ 0x00, // padding
+ 0x00, 0x01, // number of rects = 1
+ 0x00, 0x00, // reason = server initialized
+ 0x00, 0x00, // status = no error
+ 0x00, 0x04, // new width = 4
+ 0x00, 0x04, // new height = 4
+ 0xff, 0xff,
+ 0xfe, 0xcc, // enc = (-308) ExtendedDesktopSize
+ 0x01, // number of screens = 1
+ 0x00, 0x00,
+ 0x00, // padding
+ 0x00, 0x00,
+ 0x00, 0x00, // screen id = 0
+ 0x00, 0x00, // screen x = 0
+ 0x00, 0x00, // screen y = 0
+ 0x00, 0x04, // screen width = 4
+ 0x00, 0x04, // screen height = 4
+ 0x00, 0x00,
+ 0x00, 0x00]; // screen flags
+
+ // This property is indirectly used as a marker for the first update
+ client._supportsSetDesktopSize = false;
// First message should trigger a resize
- client._supportsSetDesktopSize = false;
-
client._sock._websocket._receiveData(new Uint8Array(incoming));
+ // It should match the current size of the container,
+ // not the reported size from the server
expect(RFB.messages.setDesktopSize).to.have.been.calledOnce;
- expect(RFB.messages.setDesktopSize).to.have.been.calledWith(sinon.match.object, 70, 80, 0, 0);
+ expect(RFB.messages.setDesktopSize).to.have.been.calledWith(
+ sinon.match.object, 70, 80, 0, 0);
RFB.messages.setDesktopSize.resetHistory();
@@ -708,27 +857,53 @@ describe('Remote Frame Buffer Protocol Client', function () {
it('should request a resize when the container resizes', function () {
container.style.width = '40px';
container.style.height = '50px';
- const event = new UIEvent('resize');
- window.dispatchEvent(event);
+ fakeResizeObserver.fire();
clock.tick(1000);
expect(RFB.messages.setDesktopSize).to.have.been.calledOnce;
expect(RFB.messages.setDesktopSize).to.have.been.calledWith(sinon.match.object, 40, 50, 0, 0);
});
+ it('should not request the same size twice', function () {
+ container.style.width = '40px';
+ container.style.height = '50px';
+ fakeResizeObserver.fire();
+ clock.tick(1000);
+
+ expect(RFB.messages.setDesktopSize).to.have.been.calledOnce;
+ expect(RFB.messages.setDesktopSize).to.have.been.calledWith(
+ sinon.match.object, 40, 50, 0, 0);
+
+ // Server responds with the requested size 40x50
+ const incoming = [ 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00,
+ 0x00, 0x28, 0x00, 0x32, 0xff, 0xff, 0xfe, 0xcc,
+ 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x00, 0x32,
+ 0x00, 0x00, 0x00, 0x00];
+
+ client._sock._websocket._receiveData(new Uint8Array(incoming));
+ clock.tick(1000);
+
+ RFB.messages.setDesktopSize.resetHistory();
+
+ // size is still 40x50
+ fakeResizeObserver.fire();
+ clock.tick(1000);
+
+ expect(RFB.messages.setDesktopSize).to.not.have.been.called;
+ });
+
it('should not resize until the container size is stable', function () {
container.style.width = '20px';
container.style.height = '30px';
- const event1 = new UIEvent('resize');
- window.dispatchEvent(event1);
+ fakeResizeObserver.fire();
clock.tick(400);
expect(RFB.messages.setDesktopSize).to.not.have.been.called;
container.style.width = '40px';
container.style.height = '50px';
- const event2 = new UIEvent('resize');
- window.dispatchEvent(event2);
+ fakeResizeObserver.fire();
clock.tick(400);
expect(RFB.messages.setDesktopSize).to.not.have.been.called;
@@ -744,8 +919,7 @@ describe('Remote Frame Buffer Protocol Client', function () {
container.style.width = '40px';
container.style.height = '50px';
- const event = new UIEvent('resize');
- window.dispatchEvent(event);
+ fakeResizeObserver.fire();
clock.tick(1000);
expect(RFB.messages.setDesktopSize).to.not.have.been.called;
@@ -756,8 +930,7 @@ describe('Remote Frame Buffer Protocol Client', function () {
container.style.width = '40px';
container.style.height = '50px';
- const event = new UIEvent('resize');
- window.dispatchEvent(event);
+ fakeResizeObserver.fire();
clock.tick(1000);
expect(RFB.messages.setDesktopSize).to.not.have.been.called;
@@ -768,84 +941,44 @@ describe('Remote Frame Buffer Protocol Client', function () {
container.style.width = '40px';
container.style.height = '50px';
- const event = new UIEvent('resize');
- window.dispatchEvent(event);
+ fakeResizeObserver.fire();
clock.tick(1000);
expect(RFB.messages.setDesktopSize).to.not.have.been.called;
});
it('should not try to override a server resize', function () {
- // Simple ExtendedDesktopSize FBU message
+ // Simple ExtendedDesktopSize FBU message, new size: 100x100
const incoming = [ 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
- 0x00, 0x04, 0x00, 0x04, 0xff, 0xff, 0xfe, 0xcc,
+ 0x00, 0x64, 0x00, 0x64, 0xff, 0xff, 0xfe, 0xcc,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x04,
0x00, 0x00, 0x00, 0x00 ];
+ // Note that this will cause the browser to display scrollbars
+ // since the framebuffer is 100x100 and the container is 70x80.
+ // The usable space (clientWidth/clientHeight) will be even smaller
+ // due to the scrollbars taking up space.
client._sock._websocket._receiveData(new Uint8Array(incoming));
+ // The scrollbars cause the ResizeObserver to fire
+ fakeResizeObserver.fire();
+ clock.tick(1000);
expect(RFB.messages.setDesktopSize).to.not.have.been.called;
+
+ // An actual size change must not be ignored afterwards
+ container.style.width = '120px';
+ container.style.height = '130px';
+ fakeResizeObserver.fire();
+ clock.tick(1000);
+
+ expect(RFB.messages.setDesktopSize).to.have.been.calledOnce;
+ expect(RFB.messages.setDesktopSize.firstCall.args[1]).to.equal(120);
+ expect(RFB.messages.setDesktopSize.firstCall.args[2]).to.equal(130);
});
});
describe('Misc Internals', function () {
- describe('#_updateConnectionState', function () {
- let client;
- beforeEach(function () {
- client = makeRFB();
- });
-
- it('should clear the disconnect timer if the state is not "disconnecting"', function () {
- const spy = sinon.spy();
- client._disconnTimer = setTimeout(spy, 50);
- client._rfbConnectionState = 'connecting';
- client._updateConnectionState('connected');
- this.clock.tick(51);
- expect(spy).to.not.have.been.called;
- expect(client._disconnTimer).to.be.null;
- });
-
- it('should set the rfbConnectionState', function () {
- client._rfbConnectionState = 'connecting';
- client._updateConnectionState('connected');
- expect(client._rfbConnectionState).to.equal('connected');
- });
-
- it('should not change the state when we are disconnected', function () {
- client.disconnect();
- expect(client._rfbConnectionState).to.equal('disconnected');
- client._updateConnectionState('connecting');
- expect(client._rfbConnectionState).to.not.equal('connecting');
- });
-
- it('should ignore state changes to the same state', function () {
- const connectSpy = sinon.spy();
- client.addEventListener("connect", connectSpy);
-
- expect(client._rfbConnectionState).to.equal('connected');
- client._updateConnectionState('connected');
- expect(connectSpy).to.not.have.been.called;
-
- client.disconnect();
-
- const disconnectSpy = sinon.spy();
- client.addEventListener("disconnect", disconnectSpy);
-
- expect(client._rfbConnectionState).to.equal('disconnected');
- client._updateConnectionState('disconnected');
- expect(disconnectSpy).to.not.have.been.called;
- });
-
- it('should ignore illegal state changes', function () {
- const spy = sinon.spy();
- client.addEventListener("disconnect", spy);
- client._updateConnectionState('disconnected');
- expect(client._rfbConnectionState).to.not.equal('disconnected');
- expect(spy).to.not.have.been.called;
- });
- });
-
describe('#_fail', function () {
let client;
beforeEach(function () {
@@ -886,106 +1019,6 @@ describe('Remote Frame Buffer Protocol Client', function () {
});
});
- describe('Connection States', function () {
- describe('connecting', function () {
- it('should open the websocket connection', function () {
- const client = new RFB(document.createElement('div'),
- 'ws://HOST:8675/PATH');
- sinon.spy(client._sock, 'open');
- this.clock.tick();
- expect(client._sock.open).to.have.been.calledOnce;
- });
- });
-
- describe('connected', function () {
- let client;
- beforeEach(function () {
- client = makeRFB();
- });
-
- it('should result in a connect event if state becomes connected', function () {
- const spy = sinon.spy();
- client.addEventListener("connect", spy);
- client._rfbConnectionState = 'connecting';
- client._updateConnectionState('connected');
- expect(spy).to.have.been.calledOnce;
- });
-
- it('should not result in a connect event if the state is not "connected"', function () {
- const spy = sinon.spy();
- client.addEventListener("connect", spy);
- client._sock._websocket.open = () => {}; // explicitly don't call onopen
- client._updateConnectionState('connecting');
- expect(spy).to.not.have.been.called;
- });
- });
-
- describe('disconnecting', function () {
- let client;
- beforeEach(function () {
- client = makeRFB();
- });
-
- it('should force disconnect if we do not call Websock.onclose within the disconnection timeout', function () {
- sinon.spy(client, '_updateConnectionState');
- client._sock._websocket.close = () => {}; // explicitly don't call onclose
- client._updateConnectionState('disconnecting');
- this.clock.tick(3 * 1000);
- expect(client._updateConnectionState).to.have.been.calledTwice;
- expect(client._rfbDisconnectReason).to.not.equal("");
- expect(client._rfbConnectionState).to.equal("disconnected");
- });
-
- it('should not fail if Websock.onclose gets called within the disconnection timeout', function () {
- client._updateConnectionState('disconnecting');
- this.clock.tick(3 * 1000 / 2);
- client._sock._websocket.close();
- this.clock.tick(3 * 1000 / 2 + 1);
- expect(client._rfbConnectionState).to.equal('disconnected');
- });
-
- it('should close the WebSocket connection', function () {
- sinon.spy(client._sock, 'close');
- client._updateConnectionState('disconnecting');
- expect(client._sock.close).to.have.been.calledOnce;
- });
-
- it('should not result in a disconnect event', function () {
- const spy = sinon.spy();
- client.addEventListener("disconnect", spy);
- client._sock._websocket.close = () => {}; // explicitly don't call onclose
- client._updateConnectionState('disconnecting');
- expect(spy).to.not.have.been.called;
- });
- });
-
- describe('disconnected', function () {
- let client;
- beforeEach(function () {
- client = new RFB(document.createElement('div'), 'ws://HOST:8675/PATH');
- });
-
- it('should result in a disconnect event if state becomes "disconnected"', function () {
- const spy = sinon.spy();
- client.addEventListener("disconnect", spy);
- client._rfbConnectionState = 'disconnecting';
- client._updateConnectionState('disconnected');
- expect(spy).to.have.been.calledOnce;
- expect(spy.args[0][0].detail.clean).to.be.true;
- });
-
- it('should result in a disconnect event without msg when no reason given', function () {
- const spy = sinon.spy();
- client.addEventListener("disconnect", spy);
- client._rfbConnectionState = 'disconnecting';
- client._rfbDisconnectReason = "";
- client._updateConnectionState('disconnected');
- expect(spy).to.have.been.calledOnce;
- expect(spy.args[0].length).to.equal(1);
- });
- });
- });
-
describe('Protocol Initialization States', function () {
let client;
beforeEach(function () {
@@ -993,17 +1026,21 @@ describe('Remote Frame Buffer Protocol Client', function () {
client._rfbConnectionState = 'connecting';
});
- describe('ProtocolVersion', function () {
- function sendVer(ver, client) {
- const arr = new Uint8Array(12);
- for (let i = 0; i < ver.length; i++) {
- arr[i+4] = ver.charCodeAt(i);
- }
- arr[0] = 'R'; arr[1] = 'F'; arr[2] = 'B'; arr[3] = ' ';
- arr[11] = '\n';
- client._sock._websocket._receiveData(arr);
+ function sendVer(ver, client) {
+ const arr = new Uint8Array(12);
+ for (let i = 0; i < ver.length; i++) {
+ arr[i+4] = ver.charCodeAt(i);
}
+ arr[0] = 'R'; arr[1] = 'F'; arr[2] = 'B'; arr[3] = ' ';
+ arr[11] = '\n';
+ client._sock._websocket._receiveData(arr);
+ }
+ function sendSecurity(type, cl) {
+ cl._sock._websocket._receiveData(new Uint8Array([1, type]));
+ }
+
+ describe('ProtocolVersion', function () {
describe('version parsing', function () {
it('should interpret version 003.003 as version 3.3', function () {
sendVer('003.003', client);
@@ -1015,9 +1052,9 @@ describe('Remote Frame Buffer Protocol Client', function () {
expect(client._rfbVersion).to.equal(3.3);
});
- it('should interpret version 003.889 as version 3.3', function () {
+ it('should interpret version 003.889 as version 3.8', function () {
sendVer('003.889', client);
- expect(client._rfbVersion).to.equal(3.3);
+ expect(client._rfbVersion).to.equal(3.8);
});
it('should interpret version 003.007 as version 3.7', function () {
@@ -1094,44 +1131,24 @@ describe('Remote Frame Buffer Protocol Client', function () {
describe('Security', function () {
beforeEach(function () {
- client._rfbInitState = 'Security';
+ sendVer('003.008\n', client);
+ client._sock._websocket._getSentData();
});
- it('should simply receive the auth scheme when for versions < 3.7', function () {
- client._rfbVersion = 3.6;
- const authSchemeRaw = [1, 2, 3, 4];
- const authScheme = (authSchemeRaw[0] << 24) + (authSchemeRaw[1] << 16) +
- (authSchemeRaw[2] << 8) + authSchemeRaw[3];
- client._sock._websocket._receiveData(new Uint8Array(authSchemeRaw));
- expect(client._rfbAuthScheme).to.equal(authScheme);
- });
-
- it('should prefer no authentication is possible', function () {
- client._rfbVersion = 3.7;
- const authSchemes = [2, 1, 3];
+ it('should respect server preference order', function () {
+ const authSchemes = [ 6, 79, 30, 188, 16, 6, 1 ];
client._sock._websocket._receiveData(new Uint8Array(authSchemes));
- expect(client._rfbAuthScheme).to.equal(1);
- expect(client._sock).to.have.sent(new Uint8Array([1, 1]));
+ expect(client._sock).to.have.sent(new Uint8Array([30]));
});
- it('should choose for the most prefered scheme possible for versions >= 3.7', function () {
- client._rfbVersion = 3.7;
- const authSchemes = [2, 22, 16];
- client._sock._websocket._receiveData(new Uint8Array(authSchemes));
- expect(client._rfbAuthScheme).to.equal(22);
- expect(client._sock).to.have.sent(new Uint8Array([22]));
- });
-
- it('should fail if there are no supported schemes for versions >= 3.7', function () {
+ it('should fail if there are no supported schemes', function () {
sinon.spy(client, "_fail");
- client._rfbVersion = 3.7;
const authSchemes = [1, 32];
client._sock._websocket._receiveData(new Uint8Array(authSchemes));
expect(client._fail).to.have.been.calledOnce;
});
- it('should fail with the appropriate message if no types are sent for versions >= 3.7', function () {
- client._rfbVersion = 3.7;
+ it('should fail with the appropriate message if no types are sent', function () {
const failureData = [0, 0, 0, 0, 6, 119, 104, 111, 111, 112, 115];
sinon.spy(client, '_fail');
client._sock._websocket._receiveData(new Uint8Array(failureData));
@@ -1142,7 +1159,6 @@ describe('Remote Frame Buffer Protocol Client', function () {
});
it('should transition to the Authentication state and continue on successful negotiation', function () {
- client._rfbVersion = 3.7;
const authSchemes = [1, 1];
client._negotiateAuthentication = sinon.spy();
client._sock._websocket._receiveData(new Uint8Array(authSchemes));
@@ -1151,17 +1167,8 @@ describe('Remote Frame Buffer Protocol Client', function () {
});
});
- describe('Authentication', function () {
- beforeEach(function () {
- client._rfbInitState = 'Security';
- });
-
- function sendSecurity(type, cl) {
- cl._sock._websocket._receiveData(new Uint8Array([1, type]));
- }
-
+ describe('Legacy Authentication', function () {
it('should fail on auth scheme 0 (pre 3.7) with the given message', function () {
- client._rfbVersion = 3.6;
const errMsg = "Whoopsies";
const data = [0, 0, 0, 0];
const errLen = errMsg.length;
@@ -1170,37 +1177,42 @@ describe('Remote Frame Buffer Protocol Client', function () {
data.push(errMsg.charCodeAt(i));
}
+ sendVer('003.006\n', client);
+ client._sock._websocket._getSentData();
+
sinon.spy(client, '_fail');
client._sock._websocket._receiveData(new Uint8Array(data));
expect(client._fail).to.have.been.calledWith(
'Security negotiation failed on authentication scheme (reason: Whoopsies)');
});
- it('should transition straight to SecurityResult on "no auth" (1) for versions >= 3.8', function () {
- client._rfbVersion = 3.8;
+ it('should transition straight to ServerInitialisation on "no auth" for versions < 3.7', function () {
+ sendVer('003.006\n', client);
+ client._sock._websocket._getSentData();
+
+ client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 1]));
+ expect(client._rfbInitState).to.equal('ServerInitialisation');
+ });
+ });
+
+ describe('Authentication', function () {
+ beforeEach(function () {
+ sendVer('003.008\n', client);
+ client._sock._websocket._getSentData();
+ });
+
+ it('should transition straight to SecurityResult on "no auth" (1)', function () {
sendSecurity(1, client);
expect(client._rfbInitState).to.equal('SecurityResult');
});
- it('should transition straight to ServerInitialisation on "no auth" for versions < 3.8', function () {
- client._rfbVersion = 3.7;
- sendSecurity(1, client);
- expect(client._rfbInitState).to.equal('ServerInitialisation');
- });
-
it('should fail on an unknown auth scheme', function () {
sinon.spy(client, "_fail");
- client._rfbVersion = 3.8;
sendSecurity(57, client);
expect(client._fail).to.have.been.calledOnce;
});
describe('VNC Authentication (type 2) Handler', function () {
- beforeEach(function () {
- client._rfbInitState = 'Security';
- client._rfbVersion = 3.8;
- });
-
it('should fire the credentialsrequired event if missing a password', function () {
const spy = sinon.spy();
client.addEventListener("credentialsrequired", spy);
@@ -1240,12 +1252,74 @@ describe('Remote Frame Buffer Protocol Client', function () {
});
});
- describe('XVP Authentication (type 22) Handler', function () {
- beforeEach(function () {
- client._rfbInitState = 'Security';
- client._rfbVersion = 3.8;
+ describe('ARD Authentication (type 30) Handler', function () {
+ it('should fire the credentialsrequired event if all credentials are missing', function () {
+ const spy = sinon.spy();
+ client.addEventListener("credentialsrequired", spy);
+ client._rfbCredentials = {};
+ sendSecurity(30, client);
+
+ expect(client._rfbCredentials).to.be.empty;
+ expect(spy).to.have.been.calledOnce;
+ expect(spy.args[0][0].detail.types).to.have.members(["username", "password"]);
});
+ it('should fire the credentialsrequired event if some credentials are missing', function () {
+ const spy = sinon.spy();
+ client.addEventListener("credentialsrequired", spy);
+ client._rfbCredentials = { password: 'password'};
+ sendSecurity(30, client);
+
+ expect(spy).to.have.been.calledOnce;
+ expect(spy.args[0][0].detail.types).to.have.members(["username", "password"]);
+ });
+
+ it('should return properly encrypted credentials and public key', async function () {
+ client._rfbCredentials = { username: 'user',
+ password: 'password' };
+ sendSecurity(30, client);
+
+ expect(client._sock).to.have.sent([30]);
+
+ function byteArray(length) {
+ return Array.from(new Uint8Array(length).keys());
+ }
+
+ let generator = [127, 255];
+ let prime = byteArray(128);
+ let serverPrivateKey = byteArray(128);
+ let serverPublicKey = client._modPow(generator, serverPrivateKey, prime);
+
+ let clientPrivateKey = byteArray(128);
+ let clientPublicKey = client._modPow(generator, clientPrivateKey, prime);
+
+ let padding = Array.from(byteArray(64), byte => String.fromCharCode(65+byte%26)).join('');
+
+ await client._negotiateARDAuthAsync(generator, 128, prime, serverPublicKey, clientPrivateKey, padding);
+
+ client._negotiateARDAuth();
+
+ expect(client._rfbInitState).to.equal('SecurityResult');
+
+ let expectEncrypted = new Uint8Array([
+ 232, 234, 159, 162, 170, 180, 138, 104, 164, 49, 53, 96, 20, 36, 21, 15,
+ 217, 219, 107, 173, 196, 60, 96, 142, 215, 71, 13, 185, 185, 47, 5, 175,
+ 151, 30, 194, 55, 173, 214, 141, 161, 36, 138, 146, 3, 178, 89, 43, 248,
+ 131, 134, 205, 174, 9, 150, 171, 74, 222, 201, 20, 2, 30, 168, 162, 123,
+ 46, 86, 81, 221, 44, 211, 180, 247, 221, 61, 95, 155, 157, 241, 76, 76,
+ 49, 217, 234, 75, 147, 237, 199, 159, 93, 140, 191, 174, 52, 90, 133, 58,
+ 243, 81, 112, 182, 64, 62, 149, 7, 151, 28, 36, 161, 247, 247, 36, 96,
+ 230, 95, 58, 207, 46, 183, 100, 139, 143, 155, 224, 43, 219, 3, 71, 139]);
+
+ let output = new Uint8Array(256);
+ output.set(expectEncrypted, 0);
+ output.set(clientPublicKey, 128);
+
+ expect(client._sock).to.have.sent(output);
+ });
+ });
+
+ describe('XVP Authentication (type 22) Handler', function () {
it('should fall through to standard VNC authentication upon completion', function () {
client._rfbCredentials = { username: 'user',
target: 'target',
@@ -1294,8 +1368,6 @@ describe('Remote Frame Buffer Protocol Client', function () {
describe('TightVNC Authentication (type 16) Handler', function () {
beforeEach(function () {
- client._rfbInitState = 'Security';
- client._rfbVersion = 3.8;
sendSecurity(16, client);
client._sock._websocket._getSentData(); // skip the security reply
});
@@ -1381,8 +1453,6 @@ describe('Remote Frame Buffer Protocol Client', function () {
describe('VeNCrypt Authentication (type 19) Handler', function () {
beforeEach(function () {
- client._rfbInitState = 'Security';
- client._rfbVersion = 3.8;
sendSecurity(19, client);
expect(client._sock).to.have.sent(new Uint8Array([19]));
});
@@ -1393,18 +1463,70 @@ describe('Remote Frame Buffer Protocol Client', function () {
expect(client._fail).to.have.been.calledOnce;
});
- it('should fail if the Plain authentication is not present', function () {
+ it('should fail if there are no supported subtypes', function () {
// VeNCrypt version
client._sock._websocket._receiveData(new Uint8Array([0, 2]));
expect(client._sock).to.have.sent(new Uint8Array([0, 2]));
// Server ACK.
client._sock._websocket._receiveData(new Uint8Array([0]));
- // Subtype list, only list subtype 1.
+ // Subtype list
sinon.spy(client, "_fail");
- client._sock._websocket._receiveData(new Uint8Array([1, 0, 0, 0, 1]));
+ client._sock._websocket._receiveData(new Uint8Array([2, 0, 0, 0, 9, 0, 0, 1, 4]));
expect(client._fail).to.have.been.calledOnce;
});
+ it('should support standard types', function () {
+ // VeNCrypt version
+ client._sock._websocket._receiveData(new Uint8Array([0, 2]));
+ expect(client._sock).to.have.sent(new Uint8Array([0, 2]));
+ // Server ACK.
+ client._sock._websocket._receiveData(new Uint8Array([0]));
+ // Subtype list
+ client._sock._websocket._receiveData(new Uint8Array([2, 0, 0, 0, 2, 0, 0, 1, 4]));
+
+ let expectedResponse = [];
+ push32(expectedResponse, 2); // Chosen subtype.
+
+ expect(client._sock).to.have.sent(new Uint8Array(expectedResponse));
+ });
+
+ it('should respect server preference order', function () {
+ // VeNCrypt version
+ client._sock._websocket._receiveData(new Uint8Array([0, 2]));
+ expect(client._sock).to.have.sent(new Uint8Array([0, 2]));
+ // Server ACK.
+ client._sock._websocket._receiveData(new Uint8Array([0]));
+ // Subtype list
+ let subtypes = [ 6 ];
+ push32(subtypes, 79);
+ push32(subtypes, 30);
+ push32(subtypes, 188);
+ push32(subtypes, 256);
+ push32(subtypes, 6);
+ push32(subtypes, 1);
+ client._sock._websocket._receiveData(new Uint8Array(subtypes));
+
+ let expectedResponse = [];
+ push32(expectedResponse, 30); // Chosen subtype.
+
+ expect(client._sock).to.have.sent(new Uint8Array(expectedResponse));
+ });
+
+ it('should ignore redundant VeNCrypt subtype', function () {
+ // VeNCrypt version
+ client._sock._websocket._receiveData(new Uint8Array([0, 2]));
+ expect(client._sock).to.have.sent(new Uint8Array([0, 2]));
+ // Server ACK.
+ client._sock._websocket._receiveData(new Uint8Array([0]));
+ // Subtype list
+ client._sock._websocket._receiveData(new Uint8Array([2, 0, 0, 0, 19, 0, 0, 0, 2]));
+
+ let expectedResponse = [];
+ push32(expectedResponse, 2); // Chosen subtype.
+
+ expect(client._sock).to.have.sent(new Uint8Array(expectedResponse));
+ });
+
it('should support Plain authentication', function () {
client._rfbCredentials = { username: 'username', password: 'password' };
// VeNCrypt version
@@ -1476,9 +1598,30 @@ describe('Remote Frame Buffer Protocol Client', function () {
});
});
+ describe('Legacy SecurityResult', function () {
+ beforeEach(function () {
+ sendVer('003.007\n', client);
+ client._sock._websocket._getSentData();
+ sendSecurity(1, client);
+ client._sock._websocket._getSentData();
+ });
+
+ it('should not include reason in securityfailure event', function () {
+ const spy = sinon.spy();
+ client.addEventListener("securityfailure", spy);
+ client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 2]));
+ expect(spy).to.have.been.calledOnce;
+ expect(spy.args[0][0].detail.status).to.equal(2);
+ expect('reason' in spy.args[0][0].detail).to.be.false;
+ });
+ });
+
describe('SecurityResult', function () {
beforeEach(function () {
- client._rfbInitState = 'SecurityResult';
+ sendVer('003.008\n', client);
+ client._sock._websocket._getSentData();
+ sendSecurity(1, client);
+ client._sock._websocket._getSentData();
});
it('should fall through to ServerInitialisation on a response code of 0', function () {
@@ -1486,60 +1629,26 @@ describe('Remote Frame Buffer Protocol Client', function () {
expect(client._rfbInitState).to.equal('ServerInitialisation');
});
- it('should fail on an error code of 1 with the given message for versions >= 3.8', function () {
- client._rfbVersion = 3.8;
- sinon.spy(client, '_fail');
- const failureData = [0, 0, 0, 1, 0, 0, 0, 6, 119, 104, 111, 111, 112, 115];
- client._sock._websocket._receiveData(new Uint8Array(failureData));
- expect(client._fail).to.have.been.calledWith(
- 'Security negotiation failed on security result (reason: whoops)');
- });
-
- it('should fail on an error code of 1 with a standard message for version < 3.8', function () {
- sinon.spy(client, '_fail');
- client._rfbVersion = 3.7;
- client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 1]));
- expect(client._fail).to.have.been.calledWith(
- 'Security handshake failed');
- });
-
- it('should result in securityfailure event when receiving a non zero status', function () {
- const spy = sinon.spy();
- client.addEventListener("securityfailure", spy);
- client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 2]));
- expect(spy).to.have.been.calledOnce;
- expect(spy.args[0][0].detail.status).to.equal(2);
- });
-
it('should include reason when provided in securityfailure event', function () {
- client._rfbVersion = 3.8;
const spy = sinon.spy();
client.addEventListener("securityfailure", spy);
const failureData = [0, 0, 0, 1, 0, 0, 0, 12, 115, 117, 99, 104,
32, 102, 97, 105, 108, 117, 114, 101];
client._sock._websocket._receiveData(new Uint8Array(failureData));
+ expect(spy).to.have.been.calledOnce;
expect(spy.args[0][0].detail.status).to.equal(1);
expect(spy.args[0][0].detail.reason).to.equal('such failure');
});
it('should not include reason when length is zero in securityfailure event', function () {
- client._rfbVersion = 3.9;
const spy = sinon.spy();
client.addEventListener("securityfailure", spy);
const failureData = [0, 0, 0, 1, 0, 0, 0, 0];
client._sock._websocket._receiveData(new Uint8Array(failureData));
+ expect(spy).to.have.been.calledOnce;
expect(spy.args[0][0].detail.status).to.equal(1);
expect('reason' in spy.args[0][0].detail).to.be.false;
});
-
- it('should not include reason in securityfailure event for version < 3.8', function () {
- client._rfbVersion = 3.6;
- const spy = sinon.spy();
- client.addEventListener("securityfailure", spy);
- client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 2]));
- expect(spy.args[0][0].detail.status).to.equal(2);
- expect('reason' in spy.args[0][0].detail).to.be.false;
- });
});
describe('ClientInitialisation', function () {
@@ -1686,6 +1795,10 @@ describe('Remote Frame Buffer Protocol Client', function () {
expect(RFB.messages.pixelFormat).to.have.been.calledBefore(RFB.messages.clientEncodings);
expect(RFB.messages.clientEncodings).to.have.been.calledOnce;
expect(RFB.messages.clientEncodings.getCall(0).args[1]).to.include(encodings.encodingTight);
+ RFB.messages.clientEncodings.getCall(0).args[1].forEach((enc) => {
+ expect(enc).to.be.a('number');
+ expect(Number.isInteger(enc)).to.be.true;
+ });
expect(RFB.messages.clientEncodings).to.have.been.calledBefore(RFB.messages.fbUpdateRequest);
expect(RFB.messages.fbUpdateRequest).to.have.been.calledOnce;
expect(RFB.messages.fbUpdateRequest).to.have.been.calledWith(client._sock, false, 0, 0, 27, 32);
@@ -1706,9 +1819,11 @@ describe('Remote Frame Buffer Protocol Client', function () {
});
});
- it('should transition to the "connected" state', function () {
+ it('should send the "connect" event', function () {
+ let spy = sinon.spy();
+ client.addEventListener('connect', spy);
sendServerInit({}, client);
- expect(client._rfbConnectionState).to.equal('connected');
+ expect(spy).to.have.been.calledOnce;
});
});
});
@@ -2610,27 +2725,6 @@ describe('Remote Frame Buffer Protocol Client', function () {
client._canvas.dispatchEvent(ev);
}
- function supportsSendMouseMovementEvent() {
- // Some browsers (like Safari) support the movementX /
- // movementY properties of MouseEvent, but do not allow creation
- // of non-trusted events with those properties.
- let ev;
-
- ev = new MouseEvent('mousemove',
- { 'movementX': 100,
- 'movementY': 100 });
- return ev.movementX === 100 && ev.movementY === 100;
- }
-
- function sendMouseMovementEvent(dx, dy) {
- let ev;
-
- ev = new MouseEvent('mousemove',
- { 'movementX': dx,
- 'movementY': dy });
- client._canvas.dispatchEvent(ev);
- }
-
function sendMouseButtonEvent(x, y, down, button) {
let pos = elementToClient(x, y);
let ev;
@@ -2744,62 +2838,6 @@ describe('Remote Frame Buffer Protocol Client', function () {
50, 70, 0x0);
});
- it('should ignore remote cursor position updates', function () {
- if (!supportsSendMouseMovementEvent()) {
- this.skip();
- return;
- }
- // Simple VMware Cursor Position FBU message with pointer coordinates
- // (50, 50).
- const incoming = [ 0x00, 0x00, 0x00, 0x01, 0x00, 0x32, 0x00, 0x32,
- 0x00, 0x00, 0x00, 0x00, 0x57, 0x4d, 0x56, 0x66 ];
- client._resize(100, 100);
-
- const cursorSpy = sinon.spy(client, '_handleVMwareCursorPosition');
- client._sock._websocket._receiveData(new Uint8Array(incoming));
- expect(cursorSpy).to.have.been.calledOnceWith();
- cursorSpy.restore();
-
- expect(client._mousePos).to.deep.equal({ });
- sendMouseMoveEvent(10, 10);
- clock.tick(100);
- expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
- 10, 10, 0x0);
- });
-
- it('should handle remote mouse position updates in pointer lock mode', function () {
- if (!supportsSendMouseMovementEvent()) {
- this.skip();
- return;
- }
- // Simple VMware Cursor Position FBU message with pointer coordinates
- // (50, 50).
- const incoming = [ 0x00, 0x00, 0x00, 0x01, 0x00, 0x32, 0x00, 0x32,
- 0x00, 0x00, 0x00, 0x00, 0x57, 0x4d, 0x56, 0x66 ];
- client._resize(100, 100);
-
- const spy = sinon.spy();
- client.addEventListener("inputlock", spy);
- let stub = sinon.stub(document, 'pointerLockElement');
- stub.get(function () { return client._canvas; });
- client._handlePointerLockChange();
- stub.restore();
- client._sock._websocket._receiveData(new Uint8Array([0x02, 0x02]));
- expect(spy).to.have.been.calledOnce;
- expect(spy.args[0][0].detail.pointer).to.be.true;
-
- const cursorSpy = sinon.spy(client, '_handleVMwareCursorPosition');
- client._sock._websocket._receiveData(new Uint8Array(incoming));
- expect(cursorSpy).to.have.been.calledOnceWith();
- cursorSpy.restore();
-
- expect(client._mousePos).to.deep.equal({ x: 50, y: 50 });
- sendMouseMovementEvent(10, 10);
- clock.tick(100);
- expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
- 60, 60, 0x0);
- });
-
describe('Event Aggregation', function () {
it('should send a single pointer event on mouse movement', function () {
sendMouseMoveEvent(50, 70);
diff --git a/tests/test.websock.js b/tests/test.websock.js
index 6f35ec35..857fdca8 100644
--- a/tests/test.websock.js
+++ b/tests/test.websock.js
@@ -270,6 +270,14 @@ describe('Websock', function () {
// it('should initialize the event handlers')?
});
+ describe('attaching', function () {
+ it('should attach to an existing websocket', function () {
+ let ws = new FakeWebSocket('ws://localhost:8675');
+ sock.attach(ws);
+ expect(WebSocket).to.not.have.been.called;
+ });
+ });
+
describe('closing', function () {
beforeEach(function () {
sock.open('ws://localhost');
@@ -342,6 +350,93 @@ describe('Websock', function () {
});
});
+ describe('ready state', function () {
+ it('should be "unused" after construction', function () {
+ let sock = new Websock();
+ expect(sock.readyState).to.equal('unused');
+ });
+
+ it('should be "connecting" if WebSocket is connecting', function () {
+ let sock = new Websock();
+ let ws = new FakeWebSocket();
+ ws.readyState = WebSocket.CONNECTING;
+ sock.attach(ws);
+ expect(sock.readyState).to.equal('connecting');
+ });
+
+ it('should be "open" if WebSocket is open', function () {
+ let sock = new Websock();
+ let ws = new FakeWebSocket();
+ ws.readyState = WebSocket.OPEN;
+ sock.attach(ws);
+ expect(sock.readyState).to.equal('open');
+ });
+
+ it('should be "closing" if WebSocket is closing', function () {
+ let sock = new Websock();
+ let ws = new FakeWebSocket();
+ ws.readyState = WebSocket.CLOSING;
+ sock.attach(ws);
+ expect(sock.readyState).to.equal('closing');
+ });
+
+ it('should be "closed" if WebSocket is closed', function () {
+ let sock = new Websock();
+ let ws = new FakeWebSocket();
+ ws.readyState = WebSocket.CLOSED;
+ sock.attach(ws);
+ expect(sock.readyState).to.equal('closed');
+ });
+
+ it('should be "unknown" if WebSocket state is unknown', function () {
+ let sock = new Websock();
+ let ws = new FakeWebSocket();
+ ws.readyState = 666;
+ sock.attach(ws);
+ expect(sock.readyState).to.equal('unknown');
+ });
+
+ it('should be "connecting" if RTCDataChannel is connecting', function () {
+ let sock = new Websock();
+ let ws = new FakeWebSocket();
+ ws.readyState = 'connecting';
+ sock.attach(ws);
+ expect(sock.readyState).to.equal('connecting');
+ });
+
+ it('should be "open" if RTCDataChannel is open', function () {
+ let sock = new Websock();
+ let ws = new FakeWebSocket();
+ ws.readyState = 'open';
+ sock.attach(ws);
+ expect(sock.readyState).to.equal('open');
+ });
+
+ it('should be "closing" if RTCDataChannel is closing', function () {
+ let sock = new Websock();
+ let ws = new FakeWebSocket();
+ ws.readyState = 'closing';
+ sock.attach(ws);
+ expect(sock.readyState).to.equal('closing');
+ });
+
+ it('should be "closed" if RTCDataChannel is closed', function () {
+ let sock = new Websock();
+ let ws = new FakeWebSocket();
+ ws.readyState = 'closed';
+ sock.attach(ws);
+ expect(sock.readyState).to.equal('closed');
+ });
+
+ it('should be "unknown" if RTCDataChannel state is unknown', function () {
+ let sock = new Websock();
+ let ws = new FakeWebSocket();
+ ws.readyState = 'foobar';
+ sock.attach(ws);
+ expect(sock.readyState).to.equal('unknown');
+ });
+ });
+
after(function () {
// eslint-disable-next-line no-global-assign
WebSocket = oldWS;
diff --git a/tests/test.webutil.js b/tests/test.webutil.js
index 82a9cc6d..6681b3c7 100644
--- a/tests/test.webutil.js
+++ b/tests/test.webutil.js
@@ -7,6 +7,48 @@ import * as WebUtil from '../app/webutil.js';
describe('WebUtil', function () {
"use strict";
+ describe('config variables', function () {
+ it('should parse query string variables', function () {
+ // history.pushState() will not cause the browser to attempt loading
+ // the URL, this is exactly what we want here for the tests.
+ history.pushState({}, '', "test?myvar=myval");
+ expect(WebUtil.getConfigVar("myvar")).to.be.equal("myval");
+ });
+ it('should return default value when no query match', function () {
+ history.pushState({}, '', "test?myvar=myval");
+ expect(WebUtil.getConfigVar("other", "def")).to.be.equal("def");
+ });
+ it('should handle no query match and no default value', function () {
+ history.pushState({}, '', "test?myvar=myval");
+ expect(WebUtil.getConfigVar("other")).to.be.equal(null);
+ });
+ it('should parse fragment variables', function () {
+ history.pushState({}, '', "test#myvar=myval");
+ expect(WebUtil.getConfigVar("myvar")).to.be.equal("myval");
+ });
+ it('should return default value when no fragment match', function () {
+ history.pushState({}, '', "test#myvar=myval");
+ expect(WebUtil.getConfigVar("other", "def")).to.be.equal("def");
+ });
+ it('should handle no fragment match and no default value', function () {
+ history.pushState({}, '', "test#myvar=myval");
+ expect(WebUtil.getConfigVar("other")).to.be.equal(null);
+ });
+ it('should handle both query and fragment', function () {
+ history.pushState({}, '', "test?myquery=1#myhash=2");
+ expect(WebUtil.getConfigVar("myquery")).to.be.equal("1");
+ expect(WebUtil.getConfigVar("myhash")).to.be.equal("2");
+ });
+ it('should prioritize fragment if both provide same var', function () {
+ history.pushState({}, '', "test?myvar=1#myvar=2");
+ expect(WebUtil.getConfigVar("myvar")).to.be.equal("2");
+ });
+ });
+
+ describe('cookies', function () {
+ // TODO
+ });
+
describe('settings', function () {
describe('localStorage', function () {
diff --git a/tests/test.zrle.js b/tests/test.zrle.js
new file mode 100644
index 00000000..e09d208d
--- /dev/null
+++ b/tests/test.zrle.js
@@ -0,0 +1,124 @@
+const expect = chai.expect;
+
+import Websock from '../core/websock.js';
+import Display from '../core/display.js';
+
+import ZRLEDecoder from '../core/decoders/zrle.js';
+
+import FakeWebSocket from './fake.websocket.js';
+
+function testDecodeRect(decoder, x, y, width, height, data, display, depth) {
+ let sock;
+
+ sock = new Websock;
+ sock.open("ws://example.com");
+
+ sock.on('message', () => {
+ decoder.decodeRect(x, y, width, height, sock, display, depth);
+ });
+
+ // Empty messages are filtered at multiple layers, so we need to
+ // do a direct call
+ if (data.length === 0) {
+ decoder.decodeRect(x, y, width, height, sock, display, depth);
+ } else {
+ sock._websocket._receiveData(new Uint8Array(data));
+ }
+
+ display.flip();
+}
+
+describe('ZRLE Decoder', function () {
+ let decoder;
+ let display;
+
+ before(FakeWebSocket.replace);
+ after(FakeWebSocket.restore);
+
+ beforeEach(function () {
+ decoder = new ZRLEDecoder();
+ display = new Display(document.createElement('canvas'));
+ display.resize(4, 4);
+ });
+
+ it('should handle the Raw subencoding', function () {
+ testDecodeRect(decoder, 0, 0, 4, 4,
+ [0x00, 0x00, 0x00, 0x0e, 0x78, 0x5e, 0x62, 0x60, 0x60, 0xf8, 0x4f, 0x12, 0x02, 0x00, 0x00, 0x00, 0xff, 0xff],
+ display, 24);
+
+ let targetData = new Uint8Array([
+ 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff,
+ 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff,
+ 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff,
+ 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff
+ ]);
+
+ expect(display).to.have.displayed(targetData);
+ });
+
+ it('should handle the Solid subencoding', function () {
+ testDecodeRect(decoder, 0, 0, 4, 4,
+ [0x00, 0x00, 0x00, 0x0c, 0x78, 0x5e, 0x62, 0x64, 0x60, 0xf8, 0x0f, 0x00, 0x00, 0x00, 0xff, 0xff],
+ display, 24);
+
+ let targetData = new Uint8Array([
+ 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff,
+ 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff,
+ 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff,
+ 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff
+ ]);
+
+ expect(display).to.have.displayed(targetData);
+ });
+
+
+ it('should handle the Palette Tile subencoding', function () {
+ testDecodeRect(decoder, 0, 0, 4, 4,
+ [0x00, 0x00, 0x00, 0x12, 0x78, 0x5E, 0x62, 0x62, 0x60, 248, 0xff, 0x9F, 0x01, 0x08, 0x3E, 0x7C, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff],
+ display, 24);
+
+ let targetData = new Uint8Array([
+ 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff,
+ 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff,
+ 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff,
+ 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff
+ ]);
+
+ expect(display).to.have.displayed(targetData);
+ });
+
+ it('should handle the RLE Tile subencoding', function () {
+ testDecodeRect(decoder, 0, 0, 4, 4,
+ [0x00, 0x00, 0x00, 0x0d, 0x78, 0x5e, 0x6a, 0x60, 0x60, 0xf8, 0x2f, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff],
+ display, 24);
+
+ let targetData = new Uint8Array([
+ 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff,
+ 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff,
+ 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff,
+ 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff
+ ]);
+
+ expect(display).to.have.displayed(targetData);
+ });
+
+ it('should handle the RLE Palette Tile subencoding', function () {
+ testDecodeRect(decoder, 0, 0, 4, 4,
+ [0x00, 0x00, 0x00, 0x11, 0x78, 0x5e, 0x6a, 0x62, 0x60, 0xf8, 0xff, 0x9f, 0x81, 0xa1, 0x81, 0x1f, 0x00, 0x00, 0x00, 0xff, 0xff],
+ display, 24);
+
+ let targetData = new Uint8Array([
+ 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff,
+ 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff,
+ 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff,
+ 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff
+ ]);
+
+ expect(display).to.have.displayed(targetData);
+ });
+
+ it('should fail on an invalid subencoding', function () {
+ let data = [0x00, 0x00, 0x00, 0x0c, 0x78, 0x5e, 0x6a, 0x64, 0x60, 0xf8, 0x0f, 0x00, 0x00, 0x00, 0xff, 0xff];
+ expect(() => testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24)).to.throw();
+ });
+});
diff --git a/tests/vnc_playback.html b/tests/vnc_playback.html
index ffa69906..148e292d 100644
--- a/tests/vnc_playback.html
+++ b/tests/vnc_playback.html
@@ -19,6 +19,16 @@
+
+