From bb25d3d6c5ebd3226490cc9269b136f178d94308 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Wed, 29 Nov 2017 16:37:35 +0100 Subject: [PATCH 1/4] Forced cleanup of RFB objects in tests We need to make sure RFB objects are properly disposed or they might have event listeners and other stuff hanging around that can influence subsequent tests. --- tests/test.rfb.js | 85 +++++++++++++++++++++++++++++------------------ 1 file changed, 52 insertions(+), 33 deletions(-) diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 81ee1dd6..542ce514 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -55,12 +55,30 @@ describe('Remote Frame Buffer Protocol Client', function() { this.clock.restore(); }); + var rfbs; + + beforeEach(function () { + // Track all created RFB objects + rfbs = []; + }); + afterEach(function () { + // Make sure every created RFB object is properly cleaned up + // or they might affect subsequent tests + rfbs.forEach(function (rfb) { + rfb.disconnect(); + expect(rfb._disconnect).to.have.been.called; + }); + rfbs = []; + }); + function make_rfb (url, options) { url = url || 'wss://host:8675'; var rfb = new RFB(document.createElement('canvas'), url, options); clock.tick(); rfb._sock._websocket._open(); rfb._rfb_connection_state = 'connected'; + sinon.spy(rfb, "_disconnect"); + rfbs.push(rfb); return rfb; } @@ -161,7 +179,7 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should not send the keys if we are not in a normal state', function () { sinon.spy(client._sock, 'flush'); - client._rfb_connection_state = "broken"; + client._rfb_connection_state = "connecting"; client.sendCtrlAltDel(); expect(client._sock.flush).to.not.have.been.called; }); @@ -192,7 +210,7 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should not send the key if we are not in a normal state', function () { sinon.spy(client._sock, 'flush'); - client._rfb_connection_state = "broken"; + client._rfb_connection_state = "connecting"; client.sendKey(123, 'Key123'); expect(client._sock.flush).to.not.have.been.called; }); @@ -231,7 +249,7 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should not send the text if we are not in a normal state', function () { sinon.spy(client._sock, 'flush'); - client._rfb_connection_state = "broken"; + client._rfb_connection_state = "connecting"; client.clipboardPasteFrom('abc'); expect(client._sock.flush).to.not.have.been.called; }); @@ -269,7 +287,7 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should not send the request if we are not in a normal state', function () { sinon.spy(client._sock, 'flush'); - client._rfb_connection_state = "broken"; + client._rfb_connection_state = "connecting"; client.requestDesktopSize(1,2); expect(client._sock.flush).to.not.have.been.called; }); @@ -321,28 +339,32 @@ describe('Remote Frame Buffer Protocol Client', function() { }); it('should set the rfb_connection_state', function () { - client._rfb_connection_state = 'disconnecting'; - client._updateConnectionState('disconnected'); - expect(client._rfb_connection_state).to.equal('disconnected'); + client._rfb_connection_state = 'connecting'; + client._updateConnectionState('connected'); + expect(client._rfb_connection_state).to.equal('connected'); }); it('should not change the state when we are disconnected', function () { - client._rfb_connection_state = 'disconnected'; + client.disconnect(); + expect(client._rfb_connection_state).to.equal('disconnected'); client._updateConnectionState('connecting'); expect(client._rfb_connection_state).to.not.equal('connecting'); }); it('should ignore state changes to the same state', function () { var connectSpy = sinon.spy(); - var disconnectSpy = sinon.spy(); client.addEventListener("connect", connectSpy); - client.addEventListener("disconnect", disconnectSpy); - client._rfb_connection_state = 'connected'; + expect(client._rfb_connection_state).to.equal('connected'); client._updateConnectionState('connected'); expect(connectSpy).to.not.have.been.called; - client._rfb_connection_state = 'disconnected'; + client.disconnect(); + + var disconnectSpy = sinon.spy(); + client.addEventListener("disconnect", disconnectSpy); + + expect(client._rfb_connection_state).to.equal('disconnected'); client._updateConnectionState('disconnected'); expect(disconnectSpy).to.not.have.been.called; }); @@ -350,7 +372,6 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should ignore illegal state changes', function () { var spy = sinon.spy(); client.addEventListener("disconnect", spy); - client._rfb_connection_state = 'connected'; client._updateConnectionState('disconnected'); expect(client._rfb_connection_state).to.not.equal('disconnected'); expect(spy).to.not.have.been.called; @@ -460,11 +481,21 @@ describe('Remote Frame Buffer Protocol Client', function() { client._updateConnectionState('disconnecting'); expect(client._sock.close).to.have.been.calledOnce; }); + + it('should not result in a disconnect event', function () { + var spy = sinon.spy(); + client.addEventListener("disconnect", spy); + client._sock._websocket.close = function () {}; // explicitly don't call onclose + client._updateConnectionState('disconnecting'); + expect(spy).to.not.have.been.called; + }); }); describe('disconnected', function () { var client; - beforeEach(function () { client = make_rfb(); }); + beforeEach(function () { + client = new RFB(document.createElement('canvas'), 'ws://HOST:8675/PATH'); + }); it('should result in a disconnect event if state becomes "disconnected"', function () { var spy = sinon.spy(); @@ -475,14 +506,6 @@ describe('Remote Frame Buffer Protocol Client', function() { expect(spy.args[0][0].detail.clean).to.be.true; }); - it('should not result in a disconnect event if the state is not "disconnected"', function () { - var spy = sinon.spy(); - client.addEventListener("disconnect", spy); - client._sock._websocket.close = function () {}; // explicitly don't call onclose - client._updateConnectionState('disconnecting'); - expect(spy).to.not.have.been.called; - }); - it('should result in a disconnect event without msg when no reason given', function () { var spy = sinon.spy(); client.addEventListener("disconnect", spy); @@ -892,7 +915,6 @@ describe('Remote Frame Buffer Protocol Client', function() { }); it('should fall through to ServerInitialisation on a response code of 0', function () { - client._updateConnectionState = sinon.spy(); client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0])); expect(client._rfb_init_state).to.equal('ServerInitialisation'); }); @@ -1898,14 +1920,18 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should fail if we are not currently ready to connect and we get an "open" event', function () { sinon.spy(client, "_fail"); - client._rfb_connection_state = 'some_other_state'; + client._rfb_connection_state = 'connected'; client._sock._websocket._open(); expect(client._fail).to.have.been.calledOnce; }); // close events it('should transition to "disconnected" from "disconnecting" on a close event', function () { - client._rfb_connection_state = 'disconnecting'; + var real = client._sock._websocket.close; + client._sock._websocket.close = function () {}; + client.disconnect(); + expect(client._rfb_connection_state).to.equal('disconnecting'); + client._sock._websocket.close = real; client._sock._websocket.close(); expect(client._rfb_connection_state).to.equal('disconnected'); }); @@ -1917,16 +1943,9 @@ describe('Remote Frame Buffer Protocol Client', function() { expect(client._fail).to.have.been.calledOnce; }); - it('should fail if we get a close event while disconnected', function () { - sinon.spy(client, "_fail"); - client._rfb_connection_state = 'disconnected'; - client._sock._websocket.close(); - expect(client._fail).to.have.been.calledOnce; - }); - it('should unregister close event handler', function () { sinon.spy(client._sock, 'off'); - client._rfb_connection_state = 'disconnecting'; + client.disconnect(); client._sock._websocket.close(); expect(client._sock.off).to.have.been.calledWith('close'); }); From 898cd32c0717d491e116c3cebfac6e656f703d41 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 30 Nov 2017 16:11:23 +0100 Subject: [PATCH 2/4] Don't send pointer event on end of drag We should only send an event to the server if we didn't actually end up dragging the viewport. --- core/rfb.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/core/rfb.js b/core/rfb.js index 41157452..a5bd843e 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -634,18 +634,26 @@ RFB.prototype = { if (down && !this._viewportDragging) { this._viewportDragging = true; this._viewportDragPos = {'x': x, 'y': y}; + this._viewportHasMoved = false; // Skip sending mouse events return; } else { this._viewportDragging = false; - // If the viewport didn't actually move, then treat as a mouse click event - // Send the button down event here, as the button up event is sent at the end of this function - if (!this._viewportHasMoved && !this._viewOnly) { - RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), bmask); + // If we actually performed a drag then we are done + // here and should not send any mouse events + if (this._viewportHasMoved) { + return; } - this._viewportHasMoved = false; + + // Otherwise we treat this as a mouse click event. + // Send the button down event here, as the button up + // event is sent at the end of this function. + RFB.messages.pointerEvent(this._sock, + this._display.absX(x), + this._display.absY(y), + bmask); } } From 9b84f51685243309afb9ca4cefe1de697594705b Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Fri, 24 Nov 2017 15:25:23 +0100 Subject: [PATCH 3/4] Move resize handling in to RFB object Makes the API simpler and makes it easier for other frontends to get this functionality. --- app/styles/base.css | 24 -- app/styles/lite.css | 7 - app/ui.js | 150 +---------- core/display.js | 5 - core/rfb.js | 240 ++++++++++++----- docs/API-internal.md | 1 - docs/API.md | 140 ++++------ tests/test.display.js | 9 - tests/test.rfb.js | 589 +++++++++++++++++++++++++++++++----------- vnc.html | 23 +- vnc_lite.html | 36 +-- 11 files changed, 691 insertions(+), 533 deletions(-) diff --git a/app/styles/base.css b/app/styles/base.css index b8ce81bd..344db9b2 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -854,30 +854,6 @@ select:active { ime-mode: disabled; } -/* HTML5 Canvas */ -#noVNC_screen { - display: flex; - width: 100%; - height: 100%; - overflow: auto; - background-color: rgb(40, 40, 40); -} -:root:not(.noVNC_connected) #noVNC_screen { - display: none; -} - -/* Do not set width/height for VNC_canvas or incorrect - * scaling will occur. Canvas size depends on remote VNC - * settings and noVNC settings. */ -#noVNC_canvas { - margin: auto; - /* IE miscalculates width without this :( */ - flex-shrink: 0; -} -#noVNC_canvas:focus { - outline: none; -} - /*Default noVNC logo.*/ /* From: http://fonts.googleapis.com/css?family=Orbitron:700 */ @font-face { diff --git a/app/styles/lite.css b/app/styles/lite.css index b7df1e39..13e11c7e 100644 --- a/app/styles/lite.css +++ b/app/styles/lite.css @@ -61,10 +61,3 @@ html { display: flex; justify-content: flex-end; } - -/* Do not set width/height for VNC_canvas or incorrect - * scaling will occur. Canvas size depends on remote VNC - * settings and noVNC settings. */ -#noVNC_canvas { - margin: auto; -} diff --git a/app/ui.js b/app/ui.js index d9da9a88..3c909cd2 100644 --- a/app/ui.js +++ b/app/ui.js @@ -27,7 +27,6 @@ var UI = { connected: false, desktopName: "", - resizeTimeout: null, statusTimeout: null, hideKeyboardTimeout: null, idleControlbarTimeout: null, @@ -87,7 +86,6 @@ var UI = { UI.initFullscreen(); // Setup event handlers - UI.addResizeHandlers(); UI.addControlbarHandlers(); UI.addTouchSpecificHandlers(); UI.addExtraKeysHandlers(); @@ -103,8 +101,6 @@ var UI = { UI.openControlbar(); - UI.updateViewClip(); - UI.updateVisualState('init'); document.documentElement.classList.remove("noVNC_loading"); @@ -205,11 +201,6 @@ var UI = { * EVENT HANDLERS * ------v------*/ - addResizeHandlers: function() { - window.addEventListener('resize', UI.applyResizeMode); - window.addEventListener('resize', UI.updateViewClip); - }, - addControlbarHandlers: function() { document.getElementById("noVNC_control_bar") .addEventListener('mousemove', UI.activateControlbar); @@ -432,7 +423,6 @@ var UI = { UI.disableSetting('port'); UI.disableSetting('path'); UI.disableSetting('repeaterID'); - UI.updateViewClip(); UI.setMouseButton(1); // Hide the controlbar after 2 seconds @@ -1037,7 +1027,7 @@ var UI = { } url += '/' + path; - UI.rfb = new RFB(document.getElementById('noVNC_canvas'), url, + UI.rfb = new RFB(document.getElementById('noVNC_container'), url, { shared: UI.getSetting('shared'), repeaterID: UI.getSetting('repeaterID'), credentials: { password: password } }); @@ -1045,11 +1035,13 @@ var UI = { UI.rfb.addEventListener("disconnect", UI.disconnectFinished); UI.rfb.addEventListener("credentialsrequired", UI.credentials); UI.rfb.addEventListener("securityfailure", UI.securityFailed); - UI.rfb.addEventListener("capabilities", function () { UI.updatePowerButton(); UI.initialResize(); }); + UI.rfb.addEventListener("capabilities", function () { UI.updatePowerButton(); }); UI.rfb.addEventListener("clipboard", UI.clipboardReceive); UI.rfb.addEventListener("bell", UI.bell); - UI.rfb.addEventListener("fbresize", UI.updateSessionSize); UI.rfb.addEventListener("desktopname", UI.updateDesktopName); + UI.rfb.clipViewport = UI.getSetting('view_clip'); + UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale'; + UI.rfb.resizeSession = UI.getSetting('resize') === 'remote'; }, disconnect: function() { @@ -1092,7 +1084,6 @@ var UI = { connectFinished: function (e) { UI.connected = true; UI.inhibit_reconnect = false; - UI.doneInitialResize = false; let msg; if (UI.getSetting('encrypt')) { @@ -1104,7 +1095,7 @@ var UI = { UI.updateVisualState('connected'); // Do this last because it can only be used on rendered elements - document.getElementById('noVNC_canvas').focus(); + UI.rfb.focus(); }, disconnectFinished: function (e) { @@ -1238,74 +1229,8 @@ var UI = { applyResizeMode: function() { if (!UI.rfb) return; - var screen = UI.screenSize(); - - if (screen && UI.connected) { - - var resizeMode = UI.getSetting('resize'); - UI.rfb.viewportScale = 1.0; - - // Make sure the viewport is adjusted first - UI.updateViewClip(); - - if (resizeMode === 'remote') { - - // Request changing the resolution of the remote display to - // the size of the local browser viewport. - - // In order to not send multiple requests before the browser-resize - // is finished we wait 0.5 seconds before sending the request. - clearTimeout(UI.resizeTimeout); - UI.resizeTimeout = setTimeout(function(){ - // Request a remote size covering the viewport - if (UI.rfb.requestDesktopSize(screen.w, screen.h)) { - Log.Debug('Requested new desktop size: ' + - screen.w + 'x' + screen.h); - } - }, 500); - - } else { - UI.updateScaling(); - } - } - }, - - // Re-calculate local scaling - updateScaling: function() { - if (!UI.rfb) return; - - var resizeMode = UI.getSetting('resize'); - if (resizeMode !== 'scale') { - return; - } - - var screen = UI.screenSize(); - - if (!screen || !UI.connected) { - return; - } - - UI.rfb.autoscale(screen.w, screen.h); - UI.fixScrollbars(); - }, - - // Gets the the size of the available viewport in the browser window - screenSize: function() { - var screen = document.getElementById('noVNC_screen'); - return {w: screen.offsetWidth, h: screen.offsetHeight}; - }, - - // Normally we only apply the current resize mode after a window resize - // event. This means that when a new connection is opened, there is no - // resize mode active. - // We have to wait until we know the capabilities of the server as - // some calls later in the chain is dependant on knowing the - // server-capabilities. - initialResize: function() { - if (UI.doneInitialResize) return; - - UI.applyResizeMode(); - UI.doneInitialResize = true; + UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale'; + UI.rfb.resizeSession = UI.getSetting('resize') === 'remote'; }, /* ------^------- @@ -1314,12 +1239,6 @@ var UI = { * VIEW CLIPPING * ------v------*/ - // Set and configure viewport clipping - setViewClip: function(clip) { - UI.updateSetting('view_clip', clip); - UI.updateViewClip(); - }, - // Update parameters that depend on the viewport clip setting updateViewClip: function() { if (!UI.rfb) return; @@ -1327,11 +1246,7 @@ var UI = { var cur_clip = UI.rfb.clipViewport; var new_clip = UI.getSetting('view_clip'); - var resizeSetting = UI.getSetting('resize'); - if (resizeSetting === 'scale') { - // Disable viewport clipping if we are scaling - new_clip = false; - } else if (isTouchDevice) { + if (isTouchDevice) { // Touch devices usually have shit scrollbars new_clip = true; } @@ -1340,15 +1255,6 @@ var UI = { UI.rfb.clipViewport = new_clip; } - var size = UI.screenSize(); - - if (new_clip && size) { - // When clipping is enabled, the screen is limited to - // the size of the browser window. - UI.rfb.viewportChangeSize(size.w, size.h); - UI.fixScrollbars(); - } - // Changing the viewport may change the state of // the dragging button UI.updateViewDrag(); @@ -1389,23 +1295,13 @@ var UI = { }, updateViewDrag: function() { - var clipping = false; - if (!UI.connected) return; - // Check if viewport drag is possible. It is only possible - // if the remote display is clipping the client display. - if (UI.rfb.clipViewport && UI.rfb.isClipped) { - clipping = true; - } - var viewDragButton = document.getElementById('noVNC_view_drag_button'); - if (!clipping && - UI.rfb.dragViewport) { - // The size of the remote display is the same or smaller - // than the client display. Make sure viewport drag isn't - // active when it can't be used. + if (!UI.rfb.clipViewport && UI.rfb.dragViewport) { + // We are no longer clipping the viewport. Make sure + // viewport drag isn't active when it can't be used. UI.rfb.dragViewport = false; } @@ -1420,7 +1316,7 @@ var UI = { if (isTouchDevice) { viewDragButton.classList.remove("noVNC_hidden"); - if (clipping) { + if (UI.rfb.clipViewport) { viewDragButton.disabled = false; } else { viewDragButton.disabled = true; @@ -1428,7 +1324,7 @@ var UI = { } else { viewDragButton.disabled = false; - if (clipping) { + if (UI.rfb.clipViewport) { viewDragButton.classList.remove("noVNC_hidden"); } else { viewDragButton.classList.add("noVNC_hidden"); @@ -1703,24 +1599,6 @@ var UI = { WebUtil.init_logging(UI.getSetting('logging')); }, - updateSessionSize: function(e) { - UI.updateViewClip(); - UI.updateScaling(); - UI.fixScrollbars(); - }, - - fixScrollbars: function() { - // This is a hack because Chrome screws up the calculation - // for when scrollbars are needed. So to fix it we temporarily - // toggle them off and on. - var screen = document.getElementById('noVNC_screen'); - screen.style.overflow = 'hidden'; - // Force Chrome to recalculate the layout by asking for - // an element's dimensions - screen.getBoundingClientRect(); - screen.style.overflow = ""; - }, - updateDesktopName: function(e) { UI.desktopName = e.detail.name; // Display the desktop name in the document title diff --git a/core/display.js b/core/display.js index e61802a6..b252f99e 100644 --- a/core/display.js +++ b/core/display.js @@ -106,11 +106,6 @@ Display.prototype = { return this._fb_height; }, - get isClipped() { - var vp = this._viewportLoc; - return this._fb_width > vp.w || this._fb_height > vp.h; - }, - logo: null, // ===== EVENT HANDLERS ===== diff --git a/core/rfb.js b/core/rfb.js index a5bd843e..63c6c6ba 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -66,7 +66,7 @@ export default function RFB(target, url, options) { this._fb_name = ""; - this._capabilities = { power: false, resize: false }; + this._capabilities = { power: false }; this._supportsFence = false; @@ -88,6 +88,7 @@ export default function RFB(target, url, options) { // Timers this._disconnTimer = null; // disconnection timer + this._resizeTimeout = null; // resize rate limiting // Decoder states and stats this._encHandlers = {}; @@ -140,15 +141,29 @@ export default function RFB(target, url, options) { // Bound event handlers this._eventHandlers = { focusCanvas: this._focusCanvas.bind(this), + windowResize: this._windowResize.bind(this), }; // main setup Log.Debug(">> RFB.constructor"); - // Target canvas must be able to have focus - if (!this._target.hasAttribute('tabindex')) { - this._target.tabIndex = -1; - } + // Create DOM elements + this._screen = document.createElement('div'); + this._screen.style.display = 'flex'; + this._screen.style.width = '100%'; + this._screen.style.height = '100%'; + this._screen.style.overflow = 'auto'; + this._screen.style.backgroundColor = 'rgb(40, 40, 40)'; + this._canvas = document.createElement('canvas'); + this._canvas.style.margin = 'auto'; + // Some browsers add an outline on focus + this._canvas.style.outline = 'none'; + // IE miscalculates width without this :( + this._canvas.style.flexShrink = '0'; + this._canvas.width = 0; + this._canvas.height = 0; + this._canvas.tabIndex = -1; + this._screen.appendChild(this._canvas); // populate encHandlers with bound versions this._encHandlers[encodings.encodingRaw] = RFB.encodingHandlers.RAW.bind(this); @@ -166,7 +181,7 @@ export default function RFB(target, url, options) { // NB: nothing that needs explicit teardown should be done // before this point, since this can throw an exception try { - this._display = new Display(this._target); + this._display = new Display(this._canvas); } catch (exc) { Log.Error("Display exception: " + exc); throw exc; @@ -174,10 +189,10 @@ export default function RFB(target, url, options) { this._display.onflush = this._onFlush.bind(this); this._display.clear(); - this._keyboard = new Keyboard(this._target); + this._keyboard = new Keyboard(this._canvas); this._keyboard.onkeyevent = this._handleKeyEvent.bind(this); - this._mouse = new Mouse(this._target); + this._mouse = new Mouse(this._canvas); this._mouse.onmousebutton = this._handleMouseButton.bind(this); this._mouse.onmousemove = this._handleMouseMove.bind(this); @@ -266,13 +281,36 @@ RFB.prototype = { get touchButton() { return this._mouse.touchButton; }, set touchButton(button) { this._mouse.touchButton = button; }, - get viewportScale() { return this._display.scale; }, - set viewportScale(scale) { this._display.scale = scale; }, + _clipViewport: false, + get clipViewport() { return this._clipViewport; }, + set clipViewport(viewport) { + this._clipViewport = viewport; + this._updateClip(); + }, - get clipViewport() { return this._display.clipViewport; }, - set clipViewport(viewport) { this._display.clipViewport = viewport; }, + _scaleViewport: false, + get scaleViewport() { return this._scaleViewport; }, + set scaleViewport(scale) { + this._scaleViewport = scale; + // Scaling trumps clipping, so we may need to adjust + // clipping when enabling or disabling scaling + if (scale && this._clipViewport) { + this._updateClip(); + } + this._updateScale(); + if (!scale && this._clipViewport) { + this._updateClip(); + } + }, - get isClipped() { return this._display.isClipped; }, + _resizeSession: false, + get resizeSession() { return this._resizeSession; }, + set resizeSession(resize) { + this._resizeSession = resize; + if (resize) { + this._requestRemoteResize(); + } + }, // ===== PUBLIC METHODS ===== @@ -341,38 +379,19 @@ RFB.prototype = { } }, + focus: function () { + this._canvas.focus(); + }, + + blur: function () { + this._canvas.blur(); + }, + clipboardPasteFrom: function (text) { if (this._rfb_connection_state !== 'connected' || this._viewOnly) { return; } RFB.messages.clientCutText(this._sock, text); }, - autoscale: function (width, height) { - if (this._rfb_connection_state !== 'connected') { return; } - this._display.autoscale(width, height); - }, - - viewportChangeSize: function(width, height) { - if (this._rfb_connection_state !== 'connected') { return; } - this._display.viewportChangeSize(width, height); - }, - - // Requests a change of remote desktop size. This message is an extension - // and may only be sent if we have received an ExtendedDesktopSize message - requestDesktopSize: function (width, height) { - if (this._rfb_connection_state !== 'connected' || - this._viewOnly) { - return; - } - - if (!this._supportsSetDesktopSize) { - return; - } - - RFB.messages.setDesktopSize(this._sock, width, height, - this._screen_id, this._screen_flags); - }, - - // ===== PRIVATE METHODS ===== _connect: function () { @@ -391,20 +410,31 @@ RFB.prototype = { } } + // Make our elements part of the page + this._target.appendChild(this._screen); + + // Monitor size changes of the screen + // FIXME: Use ResizeObserver, or hidden overflow + window.addEventListener('resize', this._eventHandlers.windowResize); + // Always grab focus on some kind of click event - this._target.addEventListener("mousedown", this._eventHandlers.focusCanvas); - this._target.addEventListener("touchstart", this._eventHandlers.focusCanvas); + this._canvas.addEventListener("mousedown", this._eventHandlers.focusCanvas); + this._canvas.addEventListener("touchstart", this._eventHandlers.focusCanvas); Log.Debug("<< RFB.connect"); }, _disconnect: function () { Log.Debug(">> RFB.disconnect"); - this._target.removeEventListener("mousedown", this._eventHandlers.focusCanvas); - this._target.removeEventListener("touchstart", this._eventHandlers.focusCanvas); - this._cleanup(); + this._canvas.removeEventListener("mousedown", this._eventHandlers.focusCanvas); + this._canvas.removeEventListener("touchstart", this._eventHandlers.focusCanvas); + window.removeEventListener('resize', this._eventHandlers.windowResize); + this._keyboard.ungrab(); + this._mouse.ungrab(); this._sock.close(); this._print_stats(); + this._target.removeChild(this._screen); + clearTimeout(this._resizeTimeout); Log.Debug("<< RFB.disconnect"); }, @@ -426,17 +456,6 @@ RFB.prototype = { }); }, - _cleanup: function () { - if (!this._viewOnly) { this._keyboard.ungrab(); } - if (!this._viewOnly) { this._mouse.ungrab(); } - this._display.defaultCursor(); - if (Log.get_logging() !== 'debug') { - // Show noVNC logo when disconnected, unless in - // debug mode - this._display.clear(); - } - }, - _focusCanvas: function(event) { // Respect earlier handlers' request to not do side-effects if (event.defaultPrevented) { @@ -447,7 +466,97 @@ RFB.prototype = { return; } - this._target.focus(); + this.focus(); + }, + + _windowResize: function (event) { + // If the window resized then our screen element might have + // as well. Update the viewport dimensions. + window.requestAnimationFrame(function () { + this._updateClip(); + this._updateScale(); + }.bind(this)); + + if (this._resizeSession) { + // Request changing the resolution of the remote display to + // the size of the local browser viewport. + + // In order to not send multiple requests before the browser-resize + // is finished we wait 0.5 seconds before sending the request. + clearTimeout(this._resizeTimeout); + this._resizeTimeout = setTimeout(this._requestRemoteResize.bind(this), 500); + } + }, + + // Update state of clipping in Display object, and make sure the + // configured viewport matches the current screen size + _updateClip: function () { + var cur_clip = this._display.clipViewport; + var new_clip = this._clipViewport; + + if (this._scaleViewport) { + // Disable viewport clipping if we are scaling + new_clip = false; + } + + if (cur_clip !== new_clip) { + this._display.clipViewport = new_clip; + } + + if (new_clip) { + // When clipping is enabled, the screen is limited to + // the size of the container. + let size = this._screenSize(); + this._display.viewportChangeSize(size.w, size.h); + this._fixScrollbars(); + } + }, + + _updateScale: function () { + if (!this._scaleViewport) { + this._display.scale = 1.0; + } else { + let size = this._screenSize(); + this._display.autoscale(size.w, size.h); + } + this._fixScrollbars(); + }, + + // Requests a change of remote desktop size. This message is an extension + // and may only be sent if we have received an ExtendedDesktopSize message + _requestRemoteResize: function () { + clearTimeout(this._resizeTimeout); + this._resizeTimeout = null; + + if (!this._resizeSession || this._viewOnly || + !this._supportsSetDesktopSize) { + return; + } + + let size = this._screenSize(); + RFB.messages.setDesktopSize(this._sock, size.w, size.h, + this._screen_id, this._screen_flags); + + Log.Debug('Requested new desktop size: ' + + size.w + 'x' + size.h); + }, + + // Gets the the size of the available screen + _screenSize: function () { + return { w: this._screen.offsetWidth, + h: this._screen.offsetHeight }; + }, + + _fixScrollbars: function () { + // This is a hack because Chrome screws up the calculation + // for when scrollbars are needed. So to fix it we temporarily + // toggle them off and on. + var orig = this._screen.style.overflow; + this._screen.style.overflow = 'hidden'; + // Force Chrome to recalculate the layout by asking for + // an element's dimensions + this._screen.getBoundingClientRect(); + this._screen.style.overflow = orig; }, /* @@ -1467,10 +1576,9 @@ RFB.prototype = { this._display.resize(this._fb_width, this._fb_height); - var event = new CustomEvent("fbresize", - { detail: { width: this._fb_width, - height: this._fb_height } }); - this.dispatchEvent(event); + // Adjust the visible viewport based on the new dimensions + this._updateClip(); + this._updateScale(); this._timing.fbu_rt_start = (new Date()).getTime(); this._updateContinuousUpdates(); @@ -2308,8 +2416,16 @@ RFB.encodingHandlers = { this._FBU.bytes = 1; if (this._sock.rQwait("ExtendedDesktopSize", this._FBU.bytes)) { return false; } + var firstUpdate = !this._supportsSetDesktopSize; this._supportsSetDesktopSize = true; - this._setCapability("resize", true); + + // Normally we only apply the current resize mode after a + // window resize event. However there is no such trigger on the + // initial connect. And we don't know if the server supports + // resizing until we've gotten here. + if (firstUpdate) { + this._requestRemoteResize(); + } var number_of_screens = this._sock.rQpeek8(); diff --git a/docs/API-internal.md b/docs/API-internal.md index f030dc38..4943c1a1 100644 --- a/docs/API-internal.md +++ b/docs/API-internal.md @@ -89,7 +89,6 @@ None | clipViewport | bool | RW | false | Use viewport clipping | width | int | RO | | Display area width | height | int | RO | | Display area height -| isClipped | bool | RO | | Is the remote display is larger than the client display ### 2.3.2 Methods diff --git a/docs/API.md b/docs/API.md index 4d4f4d51..c5923e3f 100644 --- a/docs/API.md +++ b/docs/API.md @@ -23,8 +23,8 @@ protocol stream. `focusOnClick` - Is a `boolean` indicating if keyboard focus should automatically be - moved to the canvas when a `mousedown` or `touchstart` event is - received. + moved to the remote session when a `mousedown` or `touchstart` + event is received. `touchButton` - Is a `long` controlling the button mask that should be simulated @@ -32,24 +32,26 @@ protocol stream. [`MouseEvent.button`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button). Is set to `1` by default. -`viewportScale` - - Is a `double` indicating how the framebuffer contents should be - scaled before being rendered on to the canvas. See also - [`RFB.autoscale()`](#rfbautoscale). Is set to `1.0` by default. - `clipViewport` - - Is a `boolean` indicating if the canvas should be clipped to its - container. When disabled the container must be able to handle the - resulting overflow. Disabled by default. + - Is a `boolean` indicating if the remote session should be clipped + to its container. When disabled scrollbars will be shown to handle + the resulting overflow. Disabled by default. `dragViewport` - Is a `boolean` indicating if mouse events should control the - relative position of a clipped canvas. Only relevant if + relative position of a clipped remote session. Only relevant if `clipViewport` is enabled. Disabled by default. -`isClipped` *Read only* - - Is a `boolean` indicating if the framebuffer is larger than the - current canvas, i.e. it is being clipped. +`scaleViewport` + - Is a `boolean` indicating if the remote session should be scaled + locally so it fits its container. When disabled it will be centered + if the remote session is smaller than its container, or handled + according to `clipViewport` if it is larger. Disabled by default. + +`resizeSession` + - Is a `boolean` indicating if a request to resize the remote session + should be sent whenever the container changes dimensions. Disabled + by default. `capabilities` *Read only* - Is an `Object` indicating which optional extensions are available @@ -59,7 +61,6 @@ protocol stream. | name | type | description | -------- | --------- | ----------- | `power` | `boolean` | Machine power control is available - | `resize` | `boolean` | The framebuffer can be resized ### Events @@ -86,9 +87,6 @@ protocol stream. - The `bell` event is fired when a audible bell request is received from the server. -[`fbresize`](#fbresize) - - The `fbresize` event is fired when the framebuffer size is changed. - [`desktopname`](#desktopname) - The `desktopname` event is fired when the remote desktop name changes. @@ -112,6 +110,12 @@ protocol stream. [`RFB.sendCtrlAltDel()`](#rfbsendctrlaltdel) - Send Ctrl-Alt-Del key sequence. +[`RFB.focus()`](#rfbfocus) + - Move keyboard focus to the remote session. + +[`RFB.blur()`](#rfbblur) + - Remove keyboard focus from the remote session. + [`RFB.machineShutdown()`](#rfbmachineshutdown) - Request a shutdown of the remote machine. @@ -124,16 +128,6 @@ protocol stream. [`RFB.clipboardPasteFrom()`](#rfbclipboardPasteFrom) - Send clipboard contents to server. -[`RFB.autoscale()`](#rfbautoscale) - - Set `RFB.viewportScale` so that the framebuffer fits a specified - container. - -[`RFB.requestDesktopSize()`](#rfbrequestDesktopSize) - - Send a request to change the remote desktop size. - -[`RFB.viewportChangeSize()`](#rfbviewportChangeSize) - - Change size of the viewport. - ### Details #### RFB() @@ -148,9 +142,10 @@ connection to a specified VNC server. ###### Parameters **`target`** - - A [`HTMLCanvasElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement) - that specifies where graphics should be rendered and input events - should be monitored. + - A block [`HTMLElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement) + that specifies where the `RFB` object should attach itself. The + existing contents of the `HTMLElement` will be untouched, but new + elements will be added during the lifetime of the `RFB` object. **`url`** - A `DOMString` specifying the VNC server to connect to. This must be @@ -233,12 +228,6 @@ which is a `DOMString` with the clipboard data. The `bell` event is fired when the server has requested an audible bell. -#### fbresize - -The `fbresize` event is fired when the framebuffer has changed -dimensions. The `detail` property is an `Object` with the properties -`width` and `height` specifying the new dimensions. - #### desktopname The `desktopname` event is fired when the name of the remote desktop @@ -310,6 +299,25 @@ around [`RFB.sendKey()`](#rfbsendkey). RFB.sendCtrlAltDel( ); +#### RFB.focus() + +The `RFB.focus()` method sets the keyboard focus on the remote session. +Keyboard events will be sent to the remote server after this point. + +##### Syntax + + RFB.focus( ); + +#### RFB.blur() + +The `RFB.blur()` method remove keyboard focus on the remote session. +Keyboard events will no longer be sent to the remote server after this +point. + +##### Syntax + + RFB.blur( ); + #### RFB.machineShutdown() The `RFB.machineShutdown()` method is used to request to shut down the @@ -354,61 +362,3 @@ to the remote server. **`text`** - A `DOMString` specifying the clipboard data to send. Currently only characters from ISO 8859-1 are supported. - -#### RFB.autoscale() - -The `RFB.autoscale()` method is used to automatically adjust -`RFB.viewportScale` to fit given dimensions. - -##### Syntax - - RFB.autoscale( width, height ); - -###### Parameters - -**`width`** - - A `long` specifying the maximum width of the canvas in CSS pixels. - -**`height`** - - A `long` specifying the maximum height of the canvas in CSS pixels. - -#### RFB.requestDesktopSize() - -The `RFB.requestDesktopSize()` method is used to request a change of -the framebuffer. The capability `resize` must be set for this method to -have any effect. - -Note that this is merely a request and the server may deny it. -The [`fbresize`](#fbresize) event will be fired when the framebuffer -actually changes dimensions. - -##### Syntax - - RFB.requestDesktopSize( width, height ); - -###### Parameters - -**`width`** - - A `long` specifying the new requested width in CSS pixels. - -**`height`** - - A `long` specifying the new requested height in CSS pixels. - -#### RFB.viewportChangeSize() - -The `RFB.viewportChangeSize()` method is used to change the size of the -canvas rather than the underlying framebuffer. - -This method has no effect if `RFB.clipViewport` is set to `false`. - -##### Syntax - - RFB.viewportChangeSize( width, height ); - -###### Parameters - -**`width`** - - A `long` specifying the new width in CSS pixels. - -**`height`** - - A `long` specifying the new height in CSS pixels. diff --git a/tests/test.display.js b/tests/test.display.js index b8e9b51f..9e6f0491 100644 --- a/tests/test.display.js +++ b/tests/test.display.js @@ -91,15 +91,6 @@ describe('Display/Canvas Helper', function () { expect(display.flip).to.have.been.calledOnce; }); - it('should report clipping when framebuffer > viewport', function () { - expect(display.isClipped).to.be.true; - }); - - it('should report not clipping when framebuffer = viewport', function () { - display.viewportChangeSize(5, 5); - expect(display.isClipped).to.be.false; - }); - it('should show the entire framebuffer when disabling the viewport', function() { display.clipViewport = false; expect(display.absX(0)).to.equal(0); diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 542ce514..31a7f2d5 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -9,6 +9,22 @@ import { encodings } from '../core/encodings.js'; import FakeWebSocket from './fake.websocket.js'; import sinon from '../vendor/sinon.js'; +/* UIEvent constructor polyfill for IE */ +(function () { + if (typeof window.UIEvent === "function") return; + + function UIEvent ( event, params ) { + params = params || { bubbles: false, cancelable: false, view: window, detail: undefined }; + var evt = document.createEvent( 'UIEvent' ); + evt.initUIEvent( event, params.bubbles, params.cancelable, params.view, params.detail ); + return evt; + } + + UIEvent.prototype = window.UIEvent.prototype; + + window.UIEvent = UIEvent; +})(); + var push8 = function (arr, num) { "use strict"; arr.push(num & 0xFF); @@ -30,12 +46,16 @@ var push32 = function (arr, num) { describe('Remote Frame Buffer Protocol Client', function() { var clock; + var raf; before(FakeWebSocket.replace); after(FakeWebSocket.restore); before(function () { this.clock = clock = sinon.useFakeTimers(); + // sinon doesn't support this yet + raf = window.requestAnimationFrame; + window.requestAnimationFrame = setTimeout; // Use a single set of buffers instead of reallocating to // speed up tests var sock = new Websock(); @@ -53,12 +73,20 @@ describe('Remote Frame Buffer Protocol Client', function() { after(function () { Websock.prototype._allocate_buffers = Websock.prototype._old_allocate_buffers; this.clock.restore(); + window.requestAnimationFrame = raf; }); + var container; var rfbs; beforeEach(function () { - // Track all created RFB objects + // Create a container element for all RFB objects to attach to + container = document.createElement('div'); + container.style.width = "100%"; + container.style.height = "100%"; + document.body.appendChild(container); + + // And track all created RFB objects rfbs = []; }); afterEach(function () { @@ -69,11 +97,14 @@ describe('Remote Frame Buffer Protocol Client', function() { expect(rfb._disconnect).to.have.been.called; }); rfbs = []; + + document.body.removeChild(container); + container = null; }); function make_rfb (url, options) { url = url || 'wss://host:8675'; - var rfb = new RFB(document.createElement('canvas'), url, options); + var rfb = new RFB(container, url, options); clock.tick(); rfb._sock._websocket._open(); rfb._rfb_connection_state = 'connected'; @@ -85,14 +116,14 @@ describe('Remote Frame Buffer Protocol Client', function() { describe('Connecting/Disconnecting', function () { describe('#RFB', function () { it('should set the current state to "connecting"', function () { - var client = new RFB(document.createElement('canvas'), 'wss://host:8675'); + var client = new RFB(document.createElement('div'), 'wss://host:8675'); client._rfb_connection_state = ''; this.clock.tick(); expect(client._rfb_connection_state).to.equal('connecting'); }); it('should actually connect to the websocket', function () { - var client = new RFB(document.createElement('canvas'), 'ws://HOST:8675/PATH'); + var 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; @@ -239,6 +270,22 @@ describe('Remote Frame Buffer Protocol Client', function() { }); }); + describe('#focus', function () { + it('should move focus to canvas object', function () { + client._canvas.focus = sinon.spy(); + client.focus(); + expect(client._canvas.focus).to.have.been.called.once; + }); + }); + + describe('#blur', function () { + it('should remove focus from canvas object', function () { + client._canvas.blur = sinon.spy(); + client.blur(); + expect(client._canvas.blur).to.have.been.called.once; + }); + }); + describe('#clipboardPasteFrom', function () { it('should send the given text in a paste event', function () { var expected = {_sQ: new Uint8Array(11), _sQlen: 0, flush: function () {}}; @@ -255,44 +302,6 @@ describe('Remote Frame Buffer Protocol Client', function() { }); }); - describe("#requestDesktopSize", function () { - beforeEach(function() { - client._supportsSetDesktopSize = true; - }); - - it('should send the request with the given width and height', function () { - var expected = [251]; - push8(expected, 0); // padding - push16(expected, 1); // width - push16(expected, 2); // height - push8(expected, 1); // number-of-screens - push8(expected, 0); // padding before screen array - push32(expected, 0); // id - push16(expected, 0); // x-position - push16(expected, 0); // y-position - push16(expected, 1); // width - push16(expected, 2); // height - push32(expected, 0); // flags - - client.requestDesktopSize(1, 2); - expect(client._sock).to.have.sent(new Uint8Array(expected)); - }); - - it('should not send the request if the client has not recieved a ExtendedDesktopSize rectangle', function () { - sinon.spy(client._sock, 'flush'); - client._supportsSetDesktopSize = false; - client.requestDesktopSize(1,2); - expect(client._sock.flush).to.not.have.been.called; - }); - - it('should not send the request if we are not in a normal state', function () { - sinon.spy(client._sock, 'flush'); - client._rfb_connection_state = "connecting"; - client.requestDesktopSize(1,2); - expect(client._sock.flush).to.not.have.been.called; - }); - }); - describe("XVP operations", function () { beforeEach(function () { client._rfb_xvp_ver = 1; @@ -321,6 +330,394 @@ describe('Remote Frame Buffer Protocol Client', function() { }); }); + describe('Clipping', function () { + var client; + beforeEach(function () { + client = make_rfb(); + container.style.width = '70px'; + container.style.height = '80px'; + client.clipViewport = true; + }); + + it('should update display clip state when changing the property', function () { + var spy = sinon.spy(client._display, "clipViewport", ["set"]); + + client.clipViewport = false; + expect(spy.set).to.have.been.calledOnce; + expect(spy.set).to.have.been.calledWith(false); + spy.set.reset(); + + client.clipViewport = true; + expect(spy.set).to.have.been.calledOnce; + expect(spy.set).to.have.been.calledWith(true); + }); + + it('should update the viewport when the container size changes', function () { + sinon.spy(client._display, "viewportChangeSize"); + + container.style.width = '40px'; + container.style.height = '50px'; + var event = new UIEvent('resize'); + window.dispatchEvent(event); + clock.tick(); + + expect(client._display.viewportChangeSize).to.have.been.calledOnce; + expect(client._display.viewportChangeSize).to.have.been.calledWith(40, 50); + }); + + it('should update the viewport when the remote session resizes', function () { + // Simple ExtendedDesktopSize FBU message + var incoming = [ 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xff, 0x00, 0xff, 0xff, 0xff, 0xfe, 0xcc, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0xff, + 0x00, 0x00, 0x00, 0x00 ]; + + sinon.spy(client._display, "viewportChangeSize"); + + client._sock._websocket._receive_data(new Uint8Array(incoming)); + + // FIXME: Display implicitly calls viewportChangeSize() when + // resizing the framebuffer, hence calledTwice. + expect(client._display.viewportChangeSize).to.have.been.calledTwice; + expect(client._display.viewportChangeSize).to.have.been.calledWith(70, 80); + }); + + it('should not update the viewport if not clipping', function () { + client.clipViewport = false; + sinon.spy(client._display, "viewportChangeSize"); + + container.style.width = '40px'; + container.style.height = '50px'; + var event = new UIEvent('resize'); + window.dispatchEvent(event); + clock.tick(); + + expect(client._display.viewportChangeSize).to.not.have.been.called; + }); + + it('should not update the viewport if scaling', function () { + client.scaleViewport = true; + sinon.spy(client._display, "viewportChangeSize"); + + container.style.width = '40px'; + container.style.height = '50px'; + var event = new UIEvent('resize'); + window.dispatchEvent(event); + clock.tick(); + + expect(client._display.viewportChangeSize).to.not.have.been.called; + }); + + describe('Dragging', function () { + beforeEach(function () { + client.dragViewport = true; + sinon.spy(RFB.messages, "pointerEvent"); + }); + + afterEach(function () { + RFB.messages.pointerEvent.restore(); + }); + + it('should not send button messages when initiating viewport dragging', function () { + client._handleMouseButton(13, 9, 0x001); + expect(RFB.messages.pointerEvent).to.not.have.been.called; + }); + + it('should send button messages when release without movement', function () { + // Just up and down + client._handleMouseButton(13, 9, 0x001); + client._handleMouseButton(13, 9, 0x000); + expect(RFB.messages.pointerEvent).to.have.been.calledTwice; + + RFB.messages.pointerEvent.reset(); + + // Small movement + client._handleMouseButton(13, 9, 0x001); + client._handleMouseMove(15, 14); + client._handleMouseButton(15, 14, 0x000); + expect(RFB.messages.pointerEvent).to.have.been.calledTwice; + }); + + it('should send button message directly when drag is disabled', function () { + client.dragViewport = false; + client._handleMouseButton(13, 9, 0x001); + expect(RFB.messages.pointerEvent).to.have.been.calledOnce; + }); + + it('should be initiate viewport dragging on sufficient movement', function () { + sinon.spy(client._display, "viewportChangePos"); + + // Too small movement + + client._handleMouseButton(13, 9, 0x001); + client._handleMouseMove(18, 9); + + expect(RFB.messages.pointerEvent).to.not.have.been.called; + expect(client._display.viewportChangePos).to.not.have.been.called; + + // Sufficient movement + + client._handleMouseMove(43, 9); + + expect(RFB.messages.pointerEvent).to.not.have.been.called; + expect(client._display.viewportChangePos).to.have.been.calledOnce; + expect(client._display.viewportChangePos).to.have.been.calledWith(-30, 0); + + client._display.viewportChangePos.reset(); + + // Now a small movement should move right away + + client._handleMouseMove(43, 14); + + expect(RFB.messages.pointerEvent).to.not.have.been.called; + expect(client._display.viewportChangePos).to.have.been.calledOnce; + expect(client._display.viewportChangePos).to.have.been.calledWith(0, -5); + }); + + it('should not send button messages when dragging ends', function () { + // First the movement + + client._handleMouseButton(13, 9, 0x001); + client._handleMouseMove(43, 9); + client._handleMouseButton(43, 9, 0x000); + + expect(RFB.messages.pointerEvent).to.not.have.been.called; + }); + + it('should terminate viewport dragging on a button up event', function () { + // First the dragging movement + + client._handleMouseButton(13, 9, 0x001); + client._handleMouseMove(43, 9); + client._handleMouseButton(43, 9, 0x000); + + // Another movement now should not move the viewport + + sinon.spy(client._display, "viewportChangePos"); + + client._handleMouseMove(43, 59); + + expect(client._display.viewportChangePos).to.not.have.been.called; + }); + }); + }); + + describe('Scaling', function () { + var client; + beforeEach(function () { + client = make_rfb(); + container.style.width = '70px'; + container.style.height = '80px'; + client.scaleViewport = true; + }); + + it('should update display scale factor when changing the property', function () { + var spy = sinon.spy(client._display, "scale", ["set"]); + sinon.spy(client._display, "autoscale"); + + client.scaleViewport = false; + expect(spy.set).to.have.been.calledOnce; + expect(spy.set).to.have.been.calledWith(1.0); + expect(client._display.autoscale).to.not.have.been.called; + + client.scaleViewport = true; + expect(client._display.autoscale).to.have.been.calledOnce; + expect(client._display.autoscale).to.have.been.calledWith(70, 80); + }); + + it('should update the clipping setting when changing the property', function () { + client.clipViewport = true; + + var spy = sinon.spy(client._display, "clipViewport", ["set"]); + + client.scaleViewport = false; + expect(spy.set).to.have.been.calledOnce; + expect(spy.set).to.have.been.calledWith(true); + + spy.set.reset(); + + client.scaleViewport = true; + expect(spy.set).to.have.been.calledOnce; + expect(spy.set).to.have.been.calledWith(false); + }); + + it('should update the scaling when the container size changes', function () { + sinon.spy(client._display, "autoscale"); + + container.style.width = '40px'; + container.style.height = '50px'; + var event = new UIEvent('resize'); + window.dispatchEvent(event); + clock.tick(); + + expect(client._display.autoscale).to.have.been.calledOnce; + expect(client._display.autoscale).to.have.been.calledWith(40, 50); + }); + + it('should update the scaling when the remote session resizes', function () { + // Simple ExtendedDesktopSize FBU message + var incoming = [ 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xff, 0x00, 0xff, 0xff, 0xff, 0xfe, 0xcc, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0xff, + 0x00, 0x00, 0x00, 0x00 ]; + + sinon.spy(client._display, "autoscale"); + + client._sock._websocket._receive_data(new Uint8Array(incoming)); + + expect(client._display.autoscale).to.have.been.calledOnce; + expect(client._display.autoscale).to.have.been.calledWith(70, 80); + }); + + it('should not update the display scale factor if not scaling', function () { + client.scaleViewport = false; + + sinon.spy(client._display, "autoscale"); + + container.style.width = '40px'; + container.style.height = '50px'; + var event = new UIEvent('resize'); + window.dispatchEvent(event); + clock.tick(); + + expect(client._display.autoscale).to.not.have.been.called; + }); + }); + + describe('Remote resize', function () { + var client; + beforeEach(function () { + client = make_rfb(); + client._supportsSetDesktopSize = true; + client.resizeSession = true; + container.style.width = '70px'; + container.style.height = '80px'; + sinon.spy(RFB.messages, "setDesktopSize"); + }); + + afterEach(function () { + RFB.messages.setDesktopSize.restore(); + }); + + it('should only request a resize when turned on', function () { + client.resizeSession = false; + expect(RFB.messages.setDesktopSize).to.not.have.been.called; + client.resizeSession = true; + expect(RFB.messages.setDesktopSize).to.have.been.calledOnce; + }); + + it('should request a resize when initially connecting', function () { + // Simple ExtendedDesktopSize FBU message + var 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 ]; + + // First message should trigger a resize + + client._supportsSetDesktopSize = false; + + client._sock._websocket._receive_data(new Uint8Array(incoming)); + + expect(RFB.messages.setDesktopSize).to.have.been.calledOnce; + expect(RFB.messages.setDesktopSize).to.have.been.calledWith(sinon.match.object, 70, 80, 0, 0); + + RFB.messages.setDesktopSize.reset(); + + // Second message should not trigger a resize + + client._sock._websocket._receive_data(new Uint8Array(incoming)); + + expect(RFB.messages.setDesktopSize).to.not.have.been.called; + }); + + it('should request a resize when the container resizes', function () { + container.style.width = '40px'; + container.style.height = '50px'; + var event = new UIEvent('resize'); + window.dispatchEvent(event); + 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 resize until the container size is stable', function () { + container.style.width = '20px'; + container.style.height = '30px'; + var event = new UIEvent('resize'); + window.dispatchEvent(event); + clock.tick(400); + + expect(RFB.messages.setDesktopSize).to.not.have.been.called; + + container.style.width = '40px'; + container.style.height = '50px'; + var event = new UIEvent('resize'); + window.dispatchEvent(event); + clock.tick(400); + + expect(RFB.messages.setDesktopSize).to.not.have.been.called; + + clock.tick(200); + + 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 resize when resize is disabled', function () { + client._resizeSession = false; + + container.style.width = '40px'; + container.style.height = '50px'; + var event = new UIEvent('resize'); + window.dispatchEvent(event); + clock.tick(1000); + + expect(RFB.messages.setDesktopSize).to.not.have.been.called; + }); + + it('should not resize when resize is not supported', function () { + client._supportsSetDesktopSize = false; + + container.style.width = '40px'; + container.style.height = '50px'; + var event = new UIEvent('resize'); + window.dispatchEvent(event); + clock.tick(1000); + + expect(RFB.messages.setDesktopSize).to.not.have.been.called; + }); + + it('should not resize when in view only mode', function () { + client._viewOnly = true; + + container.style.width = '40px'; + container.style.height = '50px'; + var event = new UIEvent('resize'); + window.dispatchEvent(event); + 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 + var 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 ]; + + client._sock._websocket._receive_data(new Uint8Array(incoming)); + + expect(RFB.messages.setDesktopSize).to.not.have.been.called; + }); + }); + describe('Misc Internals', function () { describe('#_updateConnectionState', function () { var client; @@ -421,7 +818,7 @@ describe('Remote Frame Buffer Protocol Client', function() { describe('Connection States', function () { describe('connecting', function () { it('should open the websocket connection', function () { - var client = new RFB(document.createElement('canvas'), + var client = new RFB(document.createElement('div'), 'ws://HOST:8675/PATH'); sinon.spy(client._sock, 'open'); this.clock.tick(); @@ -494,7 +891,7 @@ describe('Remote Frame Buffer Protocol Client', function() { describe('disconnected', function () { var client; beforeEach(function () { - client = new RFB(document.createElement('canvas'), 'ws://HOST:8675/PATH'); + client = new RFB(document.createElement('div'), 'ws://HOST:8675/PATH'); }); it('should result in a disconnect event if state becomes "disconnected"', function () { @@ -1082,17 +1479,12 @@ describe('Remote Frame Buffer Protocol Client', function() { expect(client._rfb_connection_state).to.equal('connected'); }); - it('should call the resize callback and resize the display', function () { - var spy = sinon.spy(); - client.addEventListener("fbresize", spy); + it('should resize the display', function () { sinon.spy(client._display, 'resize'); send_server_init({ width: 27, height: 32 }, client); expect(client._display.resize).to.have.been.calledOnce; expect(client._display.resize).to.have.been.calledWith(27, 32); - expect(spy).to.have.been.calledOnce; - expect(spy.args[0][0].detail.width).to.equal(27); - expect(spy.args[0][0].detail.height).to.equal(32); }); it('should grab the mouse and keyboard', function () { @@ -1493,14 +1885,9 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should handle the DesktopSize pseduo-encoding', function () { var spy = sinon.spy(); - client.addEventListener("fbresize", spy); sinon.spy(client._display, 'resize'); send_fbu_msg([{ x: 0, y: 0, width: 20, height: 50, encoding: -223 }], [[]], client); - expect(spy).to.have.been.calledOnce; - expect(spy.args[0][0].detail.width).to.equal(20); - expect(spy.args[0][0].detail.height).to.equal(50); - expect(client._fb_width).to.equal(20); expect(client._fb_height).to.equal(50); @@ -1512,14 +1899,12 @@ describe('Remote Frame Buffer Protocol Client', function() { var resizeSpy; beforeEach(function () { - client._supportsSetDesktopSize = false; // a really small frame client._fb_width = 4; client._fb_height = 4; client._display.resize(4, 4); sinon.spy(client._display, 'resize'); resizeSpy = sinon.spy(); - client.addEventListener("fbresize", resizeSpy); }); function make_screen_data (nr_of_screens) { @@ -1538,26 +1923,6 @@ describe('Remote Frame Buffer Protocol Client', function() { return data; } - it('should call callback when resize is supported', function () { - var spy = sinon.spy(); - client.addEventListener("capabilities", spy); - - expect(client._supportsSetDesktopSize).to.be.false; - expect(client.capabilities.resize).to.be.false; - - var reason_for_change = 0; // server initiated - var status_code = 0; // No error - - send_fbu_msg([{ x: reason_for_change, y: status_code, - width: 4, height: 4, encoding: -308 }], - make_screen_data(1), client); - - expect(client._supportsSetDesktopSize).to.be.true; - expect(spy).to.have.been.calledOnce; - expect(spy.args[0][0].detail.capabilities.resize).to.be.true; - expect(client.capabilities.resize).to.be.true; - }), - it('should handle a resize requested by this client', function () { var reason_for_change = 1; // requested by this client var status_code = 0; // No error @@ -1571,10 +1936,6 @@ describe('Remote Frame Buffer Protocol Client', function() { expect(client._display.resize).to.have.been.calledOnce; expect(client._display.resize).to.have.been.calledWith(20, 50); - - expect(resizeSpy).to.have.been.calledOnce; - expect(resizeSpy.args[0][0].detail.width).to.equal(20); - expect(resizeSpy.args[0][0].detail.height).to.equal(50); }); it('should handle a resize requested by another client', function () { @@ -1590,10 +1951,6 @@ describe('Remote Frame Buffer Protocol Client', function() { expect(client._display.resize).to.have.been.calledOnce; expect(client._display.resize).to.have.been.calledWith(20, 50); - - expect(resizeSpy).to.have.been.calledOnce; - expect(resizeSpy.args[0][0].detail.width).to.equal(20); - expect(resizeSpy.args[0][0].detail.height).to.equal(50); }); it('should be able to recieve requests which contain data for multiple screens', function () { @@ -1609,10 +1966,6 @@ describe('Remote Frame Buffer Protocol Client', function() { expect(client._display.resize).to.have.been.calledOnce; expect(client._display.resize).to.have.been.calledWith(60, 50); - - expect(resizeSpy).to.have.been.calledOnce; - expect(resizeSpy.args[0][0].detail.width).to.equal(60); - expect(resizeSpy.args[0][0].detail.height).to.equal(50); }); it('should not handle a failed request', function () { @@ -1627,8 +1980,6 @@ describe('Remote Frame Buffer Protocol Client', function() { expect(client._fb_height).to.equal(4); expect(client._display.resize).to.not.have.been.called; - - expect(resizeSpy).to.not.have.been.called; }); }); @@ -1808,60 +2159,6 @@ describe('Remote Frame Buffer Protocol Client', function() { RFB.messages.pointerEvent(pointer_msg, 13, 9, 0x010); expect(client._sock).to.have.sent(pointer_msg._sQ); }); - - // NB(directxman12): we don't need to test not sending messages in - // non-normal modes, since we haven't grabbed input - // yet (grabbing input should be checked in the lifecycle tests). - - it('should not send movement messages when viewport dragging', function () { - client._viewportDragging = true; - client._display.viewportChangePos = sinon.spy(); - sinon.spy(client._sock, 'flush'); - client._handleMouseMove(13, 9); - expect(client._sock.flush).to.not.have.been.called; - }); - - it('should not send button messages when initiating viewport dragging', function () { - client.dragViewport = true; - sinon.spy(client._sock, 'flush'); - client._handleMouseButton(13, 9, 0x001); - expect(client._sock.flush).to.not.have.been.called; - }); - - it('should be initiate viewport dragging on a button down event, if enabled', function () { - client.dragViewport = true; - client._handleMouseButton(13, 9, 0x001); - expect(client._viewportDragging).to.be.true; - expect(client._viewportDragPos).to.deep.equal({ x: 13, y: 9 }); - }); - - it('should terminate viewport dragging on a button up event, if enabled', function () { - client.dragViewport = true; - client._viewportDragging = true; - client._handleMouseButton(13, 9, 0x000); - expect(client._viewportDragging).to.be.false; - }); - - it('if enabled, viewportDragging should occur on mouse movement while a button is down', function () { - var oldX = 123; - var oldY = 109; - var newX = 123 + 11 * window.devicePixelRatio; - var newY = 109 + 4 * window.devicePixelRatio; - - client.dragViewport = true; - client._viewportDragging = true; - client._viewportHasMoved = false; - client._viewportDragPos = { x: oldX, y: oldY }; - client._display.viewportChangePos = sinon.spy(); - - client._handleMouseMove(newX, newY); - - expect(client._viewportDragging).to.be.true; - expect(client._viewportHasMoved).to.be.true; - expect(client._viewportDragPos).to.deep.equal({ x: newX, y: newY }); - expect(client._display.viewportChangePos).to.have.been.calledOnce; - expect(client._display.viewportChangePos).to.have.been.calledWith(oldX - newX, oldY - newY); - }); }); describe('Keyboard Event Handlers', function () { @@ -1912,7 +2209,7 @@ describe('Remote Frame Buffer Protocol Client', function() { // open events it('should update the state to ProtocolVersion on open (if the state is "connecting")', function () { - client = new RFB(document.createElement('canvas'), 'wss://host:8675'); + client = new RFB(document.createElement('div'), 'wss://host:8675'); this.clock.tick(); client._sock._websocket._open(); expect(client._rfb_init_state).to.equal('ProtocolVersion'); diff --git a/vnc.html b/vnc.html index fba59b1b..9c945efb 100644 --- a/vnc.html +++ b/vnc.html @@ -315,22 +315,15 @@
+
- -
- - - - - Canvas not supported. - -
- + +