connected: false,
desktopName: "",
- resizeTimeout: null,
statusTimeout: null,
hideKeyboardTimeout: null,
idleControlbarTimeout: null,
UI.initFullscreen();
// Setup event handlers
- UI.addResizeHandlers();
UI.addControlbarHandlers();
UI.addTouchSpecificHandlers();
UI.addExtraKeysHandlers();
UI.openControlbar();
- UI.updateViewClip();
-
UI.updateVisualState('init');
document.documentElement.classList.remove("noVNC_loading");
* 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);
UI.disableSetting('port');
UI.disableSetting('path');
UI.disableSetting('repeaterID');
- UI.updateViewClip();
UI.setMouseButton(1);
// Hide the controlbar after 2 seconds
}
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 } });
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() {
connectFinished: function (e) {
UI.connected = true;
UI.inhibit_reconnect = false;
- UI.doneInitialResize = false;
let msg;
if (UI.getSetting('encrypt')) {
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) {
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';
},
/* ------^-------
* 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;
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;
}
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();
},
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;
}
if (isTouchDevice) {
viewDragButton.classList.remove("noVNC_hidden");
- if (clipping) {
+ if (UI.rfb.clipViewport) {
viewDragButton.disabled = false;
} else {
viewDragButton.disabled = true;
} else {
viewDragButton.disabled = false;
- if (clipping) {
+ if (UI.rfb.clipViewport) {
viewDragButton.classList.remove("noVNC_hidden");
} else {
viewDragButton.classList.add("noVNC_hidden");
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
this._fb_name = "";
- this._capabilities = { power: false, resize: false };
+ this._capabilities = { power: false };
this._supportsFence = false;
// Timers
this._disconnTimer = null; // disconnection timer
+ this._resizeTimeout = null; // resize rate limiting
// Decoder states and stats
this._encHandlers = {};
// 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);
// 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;
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);
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 =====
}
},
- 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);
+ focus: function () {
+ this._canvas.focus();
},
- viewportChangeSize: function(width, height) {
- if (this._rfb_connection_state !== 'connected') { return; }
- this._display.viewportChangeSize(width, height);
+ blur: function () {
+ this._canvas.blur();
},
- // 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);
+ clipboardPasteFrom: function (text) {
+ if (this._rfb_connection_state !== 'connected' || this._viewOnly) { return; }
+ RFB.messages.clientCutText(this._sock, text);
},
-
// ===== PRIVATE METHODS =====
_connect: function () {
}
}
+ // 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");
},
});
},
- _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) {
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;
},
/*
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();
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();
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);
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();
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 () {
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';
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;
});
});
+ 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 () {}};
});
});
- 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;
});
});
+ 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;
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();
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 () {
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 () {
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);
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) {
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
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 () {
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 () {
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 () {
expect(client._fb_height).to.equal(4);
expect(client._display.resize).to.not.have.been.called;
-
- expect(resizeSpy).to.not.have.been.called;
});
});
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 () {
// 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');