From f172633715adc9721e552f58ea2e117ac1ade3ea Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 13 Oct 2022 10:08:10 +0200 Subject: Sort API alphabetically So it is easier to find things as the API grows. --- docs/API.md | 398 ++++++++++++++++++++++++++++++------------------------------ 1 file changed, 199 insertions(+), 199 deletions(-) diff --git a/docs/API.md b/docs/API.md index 77e9056..34a77f9 100644 --- a/docs/API.md +++ b/docs/API.md @@ -16,135 +16,128 @@ protocol stream. ### Properties -`viewOnly` - - Is a `boolean` indicating if any events (e.g. key presses or mouse - movement) should be prevented from being sent to the server. - Disabled by default. +`background` + - Is a valid CSS [background](https://developer.mozilla.org/en-US/docs/Web/CSS/background) + style value indicating which background style should be applied + to the element containing the remote session screen. The default value is `rgb(40, 40, 40)` + (solid gray color). -`focusOnClick` - - Is a `boolean` indicating if keyboard focus should automatically be - moved to the remote session when a `mousedown` or `touchstart` - event is received. Enabled by default. +`capabilities` *Read only* + - Is an `Object` indicating which optional extensions are available + on the server. Some methods may only be called if the corresponding + capability is set. The following capabilities are defined: + + | name | type | description + | -------- | --------- | ----------- + | `power` | `boolean` | Machine power control is available `clipViewport` - 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. +`compressionLevel` + - Is an `int` in range `[0-9]` controlling the desired compression + level. Value `0` means no compression. Level 1 uses a minimum of CPU + resources and achieves weak compression ratios, while level 9 offers + best compression but is slow in terms of CPU consumption on the server + side. Use high levels with very slow network connections. + Default value is `2`. + `dragViewport` - Is a `boolean` indicating if mouse events should control the relative position of a clipped remote session. Only relevant if `clipViewport` is enabled. Disabled by default. -`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. +`focusOnClick` + - Is a `boolean` indicating if keyboard focus should automatically be + moved to the remote session when a `mousedown` or `touchstart` + event is received. Enabled by default. + +`qualityLevel` + - Is an `int` in range `[0-9]` controlling the desired JPEG quality. + Value `0` implies low quality and `9` implies high quality. + Default value is `6`. `resizeSession` - Is a `boolean` indicating if a request to resize the remote session should be sent whenever the container changes dimensions. Disabled by default. +`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. + `showDotCursor` - Is a `boolean` indicating whether a dot cursor should be shown instead of a zero-sized or fully-transparent cursor if the server sets such invisible cursor. Disabled by default. -`background` - - Is a valid CSS [background](https://developer.mozilla.org/en-US/docs/Web/CSS/background) - style value indicating which background style should be applied - to the element containing the remote session screen. The default value is `rgb(40, 40, 40)` - (solid gray color). - -`qualityLevel` - - Is an `int` in range `[0-9]` controlling the desired JPEG quality. - Value `0` implies low quality and `9` implies high quality. - Default value is `6`. +`viewOnly` + - Is a `boolean` indicating if any events (e.g. key presses or mouse + movement) should be prevented from being sent to the server. + Disabled by default. -`compressionLevel` - - Is an `int` in range `[0-9]` controlling the desired compression - level. Value `0` means no compression. Level 1 uses a minimum of CPU - resources and achieves weak compression ratios, while level 9 offers - best compression but is slow in terms of CPU consumption on the server - side. Use high levels with very slow network connections. - Default value is `2`. +### Events -`capabilities` *Read only* - - Is an `Object` indicating which optional extensions are available - on the server. Some methods may only be called if the corresponding - capability is set. The following capabilities are defined: +[`bell`](#bell) + - The `bell` event is fired when a audible bell request is received + from the server. - | name | type | description - | -------- | --------- | ----------- - | `power` | `boolean` | Machine power control is available +[`capabilities`](#capabilities) + - The `capabilities` event is fired when `RFB.capabilities` is + updated. -### Events +[`clipboard`](#clipboard) + - The `clipboard` event is fired when clipboard data is received from + the server. [`connect`](#connect) - The `connect` event is fired when the `RFB` object has completed the connection and handshaking with the server. -[`disconnect`](#disconnect) - - The `disconnect` event is fired when the `RFB` object disconnects. - -[`serververification`](#serververification) - - The `serververification` event is fired when the server identity - must be confirmed by the user. - [`credentialsrequired`](#credentialsrequired) - The `credentialsrequired` event is fired when more credentials must be given to continue. -[`securityfailure`](#securityfailure) - - The `securityfailure` event is fired when the security negotiation - with the server fails. - -[`clipboard`](#clipboard) - - The `clipboard` event is fired when clipboard data is received from - the server. - -[`bell`](#bell) - - The `bell` event is fired when a audible bell request is received - from the server. - [`desktopname`](#desktopname) - The `desktopname` event is fired when the remote desktop name changes. -[`capabilities`](#capabilities) - - The `capabilities` event is fired when `RFB.capabilities` is - updated. +[`disconnect`](#disconnect) + - The `disconnect` event is fired when the `RFB` object disconnects. -### Methods +[`securityfailure`](#securityfailure) + - The `securityfailure` event is fired when the security negotiation + with the server fails. -[`RFB.disconnect()`](#rfbdisconnect) - - Disconnect from the server. +[`serververification`](#serververification) + - The `serververification` event is fired when the server identity + must be confirmed by the user. + +### Methods [`RFB.approveServer()`](#rfbapproveserver) - Proceed connecting to the server. Should be called after the [`serververification`](#serververification) event has fired and the user has verified the identity of the server. -[`RFB.sendCredentials()`](#rfbsendcredentials) - - Send credentials to server. Should be called after the - [`credentialsrequired`](#credentialsrequired) event has fired. +[`RFB.blur()`](#rfbblur) + - Remove keyboard focus from the remote session. -[`RFB.sendKey()`](#rfbsendkey) - - Send a key event. +[`RFB.clipboardPasteFrom()`](#rfbclipboardpastefrom) + - Send clipboard contents to server. -[`RFB.sendCtrlAltDel()`](#rfbsendctrlaltdel) - - Send Ctrl-Alt-Del key sequence. +[`RFB.disconnect()`](#rfbdisconnect) + - Disconnect from the server. [`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. +[`RFB.getImageData()`](#rfbgetimagedata) + - Return the current content of the screen as an ImageData array. [`RFB.machineReboot()`](#rfbmachinereboot) - Request a reboot of the remote machine. @@ -152,18 +145,25 @@ protocol stream. [`RFB.machineReset()`](#rfbmachinereset) - Request a reset of the remote machine. -[`RFB.clipboardPasteFrom()`](#rfbclipboardpastefrom) - - Send clipboard contents to server. +[`RFB.machineShutdown()`](#rfbmachineshutdown) + - Request a shutdown of the remote machine. -[`RFB.getImageData()`](#rfbgetimagedata) - - Return the current content of the screen as an ImageData array. +[`RFB.sendCredentials()`](#rfbsendcredentials) + - Send credentials to server. Should be called after the + [`credentialsrequired`](#credentialsrequired) event has fired. -[`RFB.toDataURL()`](#rfbtodataurl) - - Return the current content of the screen as data-url encoded image file. +[`RFB.sendCtrlAltDel()`](#rfbsendctrlaltdel) + - Send Ctrl-Alt-Del key sequence. + +[`RFB.sendKey()`](#rfbsendkey) + - Send a key event. [`RFB.toBlob()`](#rfbtoblob) - Return the current content of the screen as Blob encoded image file. +[`RFB.toDataURL()`](#rfbtodataurl) + - Return the current content of the screen as data-url encoded image file. + ### Details #### RFB() @@ -216,12 +216,42 @@ connection to a specified VNC server. - An `Array` of `DOMString`s specifying the sub-protocols to use in the WebSocket connection. Empty by default. +#### bell + +The `bell` event is fired when the server has requested an audible +bell. + +#### capabilities + +The `capabilities` event is fired whenever an entry is added or removed +from `RFB.capabilities`. The `detail` property is an `Object` with the +property `capabilities` containing the new value of `RFB.capabilities`. + +#### clipboard + +The `clipboard` event is fired when the server has sent clipboard data. +The `detail` property is an `Object` containing the property `text` +which is a `DOMString` with the clipboard data. + +#### credentialsrequired + +The `credentialsrequired` event is fired when the server requests more +credentials than were specified to [`RFB()`](#rfb-1). The `detail` +property is an `Object` containing the property `types` which is an +`Array` of `DOMString` listing the credentials that are required. + #### connect The `connect` event is fired after all the handshaking with the server is completed and the connection is fully established. After this event the `RFB` object is ready to recieve graphics updates and to send input. +#### desktopname + +The `desktopname` event is fired when the name of the remote desktop +changes. The `detail` property is an `Object` with the property `name` +which is a `DOMString` specifying the new name. + #### disconnect The `disconnect` event is fired when the connection has been @@ -230,27 +260,6 @@ property `clean`. `clean` is a `boolean` indicating if the termination was clean or not. In the event of an unexpected termination or an error `clean` will be set to false. -#### serververification - -The `serververification` event is fired when the server provides -information that allows the user to verify that it is the correct server -and protect against a man-in-the-middle attack. The `detail` property is -an `Object` containing the property `type` which is a `DOMString` -specifying which type of information the server has provided. Other -properties are also available, depending on the value of `type`: - -`"RSA"` - - The server identity is verified using just a RSA key. The property - `publickey` is a `Uint8Array` containing the public key in a unsigned - big endian representation. - -#### credentialsrequired - -The `credentialsrequired` event is fired when the server requests more -credentials than were specified to [`RFB()`](#rfb-1). The `detail` -property is an `Object` containing the property `types` which is an -`Array` of `DOMString` listing the credentials that are required. - #### securityfailure The `securityfailure` event is fired when the handshaking process with @@ -271,37 +280,19 @@ thus the language of the string is not known. However most servers will probably send English strings. The server can choose to not send a reason and in these cases the `reason` property will be omitted. -#### clipboard - -The `clipboard` event is fired when the server has sent clipboard data. -The `detail` property is an `Object` containing the property `text` -which is a `DOMString` with the clipboard data. - -#### bell - -The `bell` event is fired when the server has requested an audible -bell. - -#### desktopname - -The `desktopname` event is fired when the name of the remote desktop -changes. The `detail` property is an `Object` with the property `name` -which is a `DOMString` specifying the new name. - -#### capabilities - -The `capabilities` event is fired whenever an entry is added or removed -from `RFB.capabilities`. The `detail` property is an `Object` with the -property `capabilities` containing the new value of `RFB.capabilities`. - -#### RFB.disconnect() - -The `RFB.disconnect()` method is used to disconnect from the currently -connected server. +#### serververification -##### Syntax +The `serververification` event is fired when the server provides +information that allows the user to verify that it is the correct server +and protect against a man-in-the-middle attack. The `detail` property is +an `Object` containing the property `type` which is a `DOMString` +specifying which type of information the server has provided. Other +properties are also available, depending on the value of `type`: - RFB.disconnect( ); +`"RSA"` + - The server identity is verified using just a RSA key. The property + `publickey` is a `Uint8Array` containing the public key in a unsigned + big endian representation. #### RFB.approveServer() @@ -313,55 +304,38 @@ and that the connection can continue. RFB.approveServer( ); -#### RFB.sendCredentials() +#### RFB.blur() -The `RFB.sendCredentials()` method is used to provide the missing -credentials after a `credentialsrequired` event has been fired. +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.sendCredentials( credentials ); - -###### Parameters - -**`credentials`** - - An `Object` specifying the credentials to provide to the server - when authenticating. See [`RFB()`](#rfb-1) for details. + RFB.blur( ); -#### RFB.sendKey() +#### RFB.clipboardPasteFrom() -The `RFB.sendKey()` method is used to send a key event to the server. +The `RFB.clipboardPasteFrom()` method is used to send clipboard data +to the remote server. ##### Syntax - RFB.sendKey( keysym, code [, down] ); + RFB.clipboardPasteFrom( text ); ###### Parameters -**`keysym`** - - A `long` specifying the RFB keysym to send. Can be `0` if a valid - **`code`** is specified. - -**`code`** - - A `DOMString` specifying the physical key to send. Valid values are - those that can be specified to - [`KeyboardEvent.code`](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code). - If the physical key cannot be determined then `null` shall be - specified. - -**`down`** *Optional* - - A `boolean` specifying if a press or a release event should be - sent. If omitted then both a press and release event are sent. +**`text`** + - A `DOMString` specifying the clipboard data to send. -#### RFB.sendCtrlAltDel() +#### RFB.disconnect() -The `RFB.sendCtrlAltDel()` method is used to send the key sequence -*left Control*, *left Alt*, *Delete*. This is a convenience wrapper -around [`RFB.sendKey()`](#rfbsendkey). +The `RFB.disconnect()` method is used to disconnect from the currently +connected server. ##### Syntax - RFB.sendCtrlAltDel( ); + RFB.disconnect( ); #### RFB.focus() @@ -379,25 +353,14 @@ Keyboard events will be sent to the remote server after this point. performed. Please see [`HTMLElement.focus()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus) for available options. -#### 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() +#### RFB.getImageData() -The `RFB.machineShutdown()` method is used to request to shut down the -remote machine. The capability `power` must be set for this method to -have any effect. +The `RFB.getImageData()` method is used to return the current content of the +screen encoded as [`ImageData`](https://developer.mozilla.org/en-US/docs/Web/API/ImageData). ##### Syntax - RFB.machineShutdown( ); + RFB.getImageData(); #### RFB.machineReboot() @@ -419,48 +382,65 @@ to have any effect. RFB.machineReset( ); -#### RFB.clipboardPasteFrom() +#### RFB.machineShutdown() -The `RFB.clipboardPasteFrom()` method is used to send clipboard data -to the remote server. +The `RFB.machineShutdown()` method is used to request to shut down the +remote machine. The capability `power` must be set for this method to +have any effect. ##### Syntax - RFB.clipboardPasteFrom( text ); + RFB.machineShutdown( ); + +#### RFB.sendCredentials() + +The `RFB.sendCredentials()` method is used to provide the missing +credentials after a `credentialsrequired` event has been fired. + +##### Syntax + + RFB.sendCredentials( credentials ); ###### Parameters -**`text`** - - A `DOMString` specifying the clipboard data to send. +**`credentials`** + - An `Object` specifying the credentials to provide to the server + when authenticating. See [`RFB()`](#rfb-1) for details. -#### RFB.getImageData() +#### RFB.sendCtrlAltDel() -The `RFB.getImageData()` method is used to return the current content of the -screen encoded as [`ImageData`](https://developer.mozilla.org/en-US/docs/Web/API/ImageData). +The `RFB.sendCtrlAltDel()` method is used to send the key sequence +*left Control*, *left Alt*, *Delete*. This is a convenience wrapper +around [`RFB.sendKey()`](#rfbsendkey). ##### Syntax - RFB.getImageData(); + RFB.sendCtrlAltDel( ); -#### RFB.toDataURL() +#### RFB.sendKey() -The `RFB.toDataURL()` method is used to return the current content of the -screen encoded as a data URL that could for example be put in the `src` attribute -of an `img` tag. +The `RFB.sendKey()` method is used to send a key event to the server. ##### Syntax - RFB.toDataURL(); - RFB.toDataURL(type); - RFB.toDataURL(type, encoderOptions); + RFB.sendKey( keysym, code [, down] ); ###### Parameters -**`type`** *Optional* - - A string indicating the requested MIME type of the image +**`keysym`** + - A `long` specifying the RFB keysym to send. Can be `0` if a valid + **`code`** is specified. -**`encoderOptions`** *Optional* - - A number between 0 and 1 indicating the image quality. +**`code`** + - A `DOMString` specifying the physical key to send. Valid values are + those that can be specified to + [`KeyboardEvent.code`](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code). + If the physical key cannot be determined then `null` shall be + specified. + +**`down`** *Optional* + - A `boolean` specifying if a press or a release event should be + sent. If omitted then both a press and release event are sent. #### RFB.toBlob() @@ -484,3 +464,23 @@ screen encoded as [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob **`encoderOptions`** *Optional* - A number between 0 and 1 indicating the image quality. + +#### RFB.toDataURL() + +The `RFB.toDataURL()` method is used to return the current content of the +screen encoded as a data URL that could for example be put in the `src` attribute +of an `img` tag. + +##### Syntax + + RFB.toDataURL(); + RFB.toDataURL(type); + RFB.toDataURL(type, encoderOptions); + +###### Parameters + +**`type`** *Optional* + - A string indicating the requested MIME type of the image + +**`encoderOptions`** *Optional* + - A number between 0 and 1 indicating the image quality. -- cgit v1.2.1 From 7f4a9eebc865a627407e69f22a5de18454cde6df Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 13 Oct 2022 10:34:19 +0200 Subject: Export clipping state externally So that UI can reflect if it is currently possible to drag the viewport or not. --- app/ui.js | 6 +++++- core/rfb.js | 15 +++++++++++++++ docs/API.md | 15 +++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/app/ui.js b/app/ui.js index ff0f176..c277eea 100644 --- a/app/ui.js +++ b/app/ui.js @@ -1049,6 +1049,7 @@ const UI = { UI.rfb.addEventListener("serververification", UI.serverVerify); UI.rfb.addEventListener("credentialsrequired", UI.credentials); UI.rfb.addEventListener("securityfailure", UI.securityFailed); + UI.rfb.addEventListener("clippingviewport", UI.updateViewDrag); UI.rfb.addEventListener("capabilities", UI.updatePowerButton); UI.rfb.addEventListener("clipboard", UI.clipboardReceive); UI.rfb.addEventListener("bell", UI.bell); @@ -1362,7 +1363,8 @@ const UI = { const viewDragButton = document.getElementById('noVNC_view_drag_button'); - if (!UI.rfb.clipViewport && UI.rfb.dragViewport) { + if ((!UI.rfb.clipViewport || !UI.rfb.clippingViewport) && + 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; @@ -1379,6 +1381,8 @@ const UI = { } else { viewDragButton.classList.add("noVNC_hidden"); } + + viewDragButton.disabled = !UI.rfb.clippingViewport; }, /* ------^------- diff --git a/core/rfb.js b/core/rfb.js index 2ccd61a..6afd7c6 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -287,6 +287,7 @@ export default class RFB extends EventTargetMixin { this._viewOnly = false; this._clipViewport = false; + this._clippingViewport = false; this._scaleViewport = false; this._resizeSession = false; @@ -318,6 +319,16 @@ export default class RFB extends EventTargetMixin { get capabilities() { return this._capabilities; } + get clippingViewport() { return this._clippingViewport; } + _setClippingViewport(on) { + if (on === this._clippingViewport) { + return; + } + this._clippingViewport = on; + this.dispatchEvent(new CustomEvent("clippingviewport", + { detail: this._clippingViewport })); + } + get touchButton() { return 0; } set touchButton(button) { Log.Warn("Using old API!"); } @@ -749,6 +760,10 @@ export default class RFB extends EventTargetMixin { const size = this._screenSize(); this._display.viewportChangeSize(size.w, size.h); this._fixScrollbars(); + this._setClippingViewport(size.w < this._display.width || + size.h < this._display.height); + } else { + this._setClippingViewport(false); } // When changing clipping we might show or hide scrollbars. diff --git a/docs/API.md b/docs/API.md index 34a77f9..a16799b 100644 --- a/docs/API.md +++ b/docs/API.md @@ -31,6 +31,11 @@ protocol stream. | -------- | --------- | ----------- | `power` | `boolean` | Machine power control is available +`clippingViewport` *Read only* + - Is a `boolean` indicating if the remote session is currently being + clipped to its container. Only relevant if `clipViewport` is + enabled. + `clipViewport` - Is a `boolean` indicating if the remote session should be clipped to its container. When disabled scrollbars will be shown to handle @@ -94,6 +99,10 @@ protocol stream. - The `clipboard` event is fired when clipboard data is received from the server. +[`clippingviewport`](#clippingviewport) + - The `clippingviewport` event is fired when `RFB.clippingViewport` is + updated. + [`connect`](#connect) - The `connect` event is fired when the `RFB` object has completed the connection and handshaking with the server. @@ -227,6 +236,12 @@ The `capabilities` event is fired whenever an entry is added or removed from `RFB.capabilities`. The `detail` property is an `Object` with the property `capabilities` containing the new value of `RFB.capabilities`. +#### clippingviewport + +The `clippingviewport` event is fired whenever `RFB.clippingViewport` +changes between `true` and `false`. The `detail` property is a `boolean` +with the new value of `RFB.clippingViewport`. + #### clipboard The `clipboard` event is fired when the server has sent clipboard data. -- cgit v1.2.1 From 262a90b0e03da1ddf0b6ac5acd55a3167c4b558d Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Tue, 27 Dec 2022 12:40:49 +0100 Subject: Consistently use "first" indentation We already enforced this for most things, so let's fix up the last few variants as well. --- .eslintrc | 3 +++ app/webutil.js | 4 ++-- core/des.js | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.eslintrc b/.eslintrc index 7797028..10e15ce 100644 --- a/.eslintrc +++ b/.eslintrc @@ -26,10 +26,13 @@ "brace-style": ["error", "1tbs", { "allowSingleLine": true }], "indent": ["error", 4, { "SwitchCase": 1, + "VariableDeclarator": "first", "FunctionDeclaration": { "parameters": "first" }, + "FunctionExpression": { "parameters": "first" }, "CallExpression": { "arguments": "first" }, "ArrayExpression": "first", "ObjectExpression": "first", + "ImportDeclaration": "first", "ignoreComments": true }], "comma-spacing": ["error"], "comma-style": ["error"], diff --git a/app/webutil.js b/app/webutil.js index d42b7f2..084c69f 100644 --- a/app/webutil.js +++ b/app/webutil.js @@ -32,7 +32,7 @@ export function initLogging(level) { export function getQueryVar(name, defVal) { "use strict"; const re = new RegExp('.*[?&]' + name + '=([^&#]*)'), - match = ''.concat(document.location.href, window.location.hash).match(re); + match = ''.concat(document.location.href, window.location.hash).match(re); if (typeof defVal === 'undefined') { defVal = null; } if (match) { @@ -46,7 +46,7 @@ export function getQueryVar(name, defVal) { export function getHashVar(name, defVal) { "use strict"; const re = new RegExp('.*[&#]' + name + '=([^&]*)'), - match = document.location.hash.match(re); + match = document.location.hash.match(re); if (typeof defVal === 'undefined') { defVal = null; } if (match) { diff --git a/core/des.js b/core/des.js index d2f807b..ba1ebde 100644 --- a/core/des.js +++ b/core/des.js @@ -81,7 +81,7 @@ const PC2 = [13,16,10,23, 0, 4, 2,27,14, 5,20, 9,22,18,11, 3, 25, 7,15, 6,26,19,12, 1,40,51,30,36,46,54,29,39, 50,44,32,47,43,48,38,55,33,52,45,41,49,35,28,31 ], - totrot = [ 1, 2, 4, 6, 8,10,12,14,15,17,19,21,23,25,27,28]; + totrot = [ 1, 2, 4, 6, 8,10,12,14,15,17,19,21,23,25,27,28]; const z = 0x0; let a,b,c,d,e,f; -- cgit v1.2.1 From 28c9670427ee987e14975fe75159e4bc5d7ee869 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 13 Oct 2022 08:35:30 +0200 Subject: Remove test code for old Chrome We don't care about ancient versions of Chrome anyway, so let's keep things simple. --- tests/test.helper.js | 20 ++----------------- tests/test.keyboard.js | 49 +++++----------------------------------------- tests/test.localization.js | 10 +--------- tests/test.webutil.js | 9 +-------- 4 files changed, 9 insertions(+), 79 deletions(-) diff --git a/tests/test.helper.js b/tests/test.helper.js index ed65770..ff83c53 100644 --- a/tests/test.helper.js +++ b/tests/test.helper.js @@ -71,18 +71,10 @@ describe('Helpers', function () { origNavigator = Object.getOwnPropertyDescriptor(window, "navigator"); Object.defineProperty(window, "navigator", {value: {}}); - if (window.navigator.platform !== undefined) { - // Object.defineProperty() doesn't work properly in old - // versions of Chrome - this.skip(); - } - window.navigator.platform = "Mac x86_64"; }); afterEach(function () { - if (origNavigator !== undefined) { - Object.defineProperty(window, "navigator", origNavigator); - } + Object.defineProperty(window, "navigator", origNavigator); }); it('should respect ContextMenu on modern browser', function () { @@ -196,19 +188,11 @@ describe('Helpers', function () { origNavigator = Object.getOwnPropertyDescriptor(window, "navigator"); Object.defineProperty(window, "navigator", {value: {}}); - if (window.navigator.platform !== undefined) { - // Object.defineProperty() doesn't work properly in old - // versions of Chrome - this.skip(); - } - window.navigator.platform = "Windows"; }); afterEach(function () { - if (origNavigator !== undefined) { - Object.defineProperty(window, "navigator", origNavigator); - } + Object.defineProperty(window, "navigator", origNavigator); }); const keys = { 'Zenkaku': 0xff2a, 'Hankaku': 0xff2a, diff --git a/tests/test.keyboard.js b/tests/test.keyboard.js index 6b59cde..0d8cac6 100644 --- a/tests/test.keyboard.js +++ b/tests/test.keyboard.js @@ -144,18 +144,10 @@ describe('Key Event Handling', function () { origNavigator = Object.getOwnPropertyDescriptor(window, "navigator"); Object.defineProperty(window, "navigator", {value: {}}); - if (window.navigator.platform !== undefined) { - // Object.defineProperty() doesn't work properly in old - // versions of Chrome - this.skip(); - } - window.navigator.platform = "Mac x86_64"; }); afterEach(function () { - if (origNavigator !== undefined) { - Object.defineProperty(window, "navigator", origNavigator); - } + Object.defineProperty(window, "navigator", origNavigator); }); it('should change Alt to AltGraph', function () { @@ -267,17 +259,10 @@ describe('Key Event Handling', function () { origNavigator = Object.getOwnPropertyDescriptor(window, "navigator"); Object.defineProperty(window, "navigator", {value: {}}); - if (window.navigator.platform !== undefined) { - // Object.defineProperty() doesn't work properly in old - // versions of Chrome - this.skip(); - } }); afterEach(function () { - if (origNavigator !== undefined) { - Object.defineProperty(window, "navigator", origNavigator); - } + Object.defineProperty(window, "navigator", origNavigator); }); it('should toggle caps lock on key press on iOS', function () { @@ -334,19 +319,11 @@ describe('Key Event Handling', function () { origNavigator = Object.getOwnPropertyDescriptor(window, "navigator"); Object.defineProperty(window, "navigator", {value: {}}); - if (window.navigator.platform !== undefined) { - // Object.defineProperty() doesn't work properly in old - // versions of Chrome - this.skip(); - } - window.navigator.platform = "Windows"; }); afterEach(function () { - if (origNavigator !== undefined) { - Object.defineProperty(window, "navigator", origNavigator); - } + Object.defineProperty(window, "navigator", origNavigator); }); const keys = { 'Zenkaku': 0xff2a, 'Hankaku': 0xff2a, @@ -375,20 +352,12 @@ describe('Key Event Handling', function () { origNavigator = Object.getOwnPropertyDescriptor(window, "navigator"); Object.defineProperty(window, "navigator", {value: {}}); - if (window.navigator.platform !== undefined) { - // Object.defineProperty() doesn't work properly in old - // versions of Chrome - this.skip(); - } - window.navigator.platform = "Windows x86_64"; this.clock = sinon.useFakeTimers(); }); afterEach(function () { - if (origNavigator !== undefined) { - Object.defineProperty(window, "navigator", origNavigator); - } + Object.defineProperty(window, "navigator", origNavigator); if (this.clock !== undefined) { this.clock.restore(); } @@ -520,20 +489,12 @@ describe('Key Event Handling', function () { origNavigator = Object.getOwnPropertyDescriptor(window, "navigator"); Object.defineProperty(window, "navigator", {value: {}}); - if (window.navigator.platform !== undefined) { - // Object.defineProperty() doesn't work properly in old - // versions of Chrome - this.skip(); - } - window.navigator.platform = "Windows x86_64"; this.clock = sinon.useFakeTimers(); }); afterEach(function () { - if (origNavigator !== undefined) { - Object.defineProperty(window, "navigator", origNavigator); - } + Object.defineProperty(window, "navigator", origNavigator); if (this.clock !== undefined) { this.clock.restore(); } diff --git a/tests/test.localization.js b/tests/test.localization.js index 311353a..7e8e6c1 100644 --- a/tests/test.localization.js +++ b/tests/test.localization.js @@ -13,18 +13,10 @@ describe('Localization', function () { origNavigator = Object.getOwnPropertyDescriptor(window, "navigator"); Object.defineProperty(window, "navigator", {value: {}}); - if (window.navigator.languages !== undefined) { - // Object.defineProperty() doesn't work properly in old - // versions of Chrome - this.skip(); - } - window.navigator.languages = []; }); afterEach(function () { - if (origNavigator !== undefined) { - Object.defineProperty(window, "navigator", origNavigator); - } + Object.defineProperty(window, "navigator", origNavigator); }); it('should use English by default', function () { diff --git a/tests/test.webutil.js b/tests/test.webutil.js index 6681b3c..76aa763 100644 --- a/tests/test.webutil.js +++ b/tests/test.webutil.js @@ -66,11 +66,6 @@ describe('WebUtil', function () { origLocalStorage = Object.getOwnPropertyDescriptor(window, "localStorage"); Object.defineProperty(window, "localStorage", {value: {}}); - if (window.localStorage.setItem !== undefined) { - // Object.defineProperty() doesn't work properly in old - // versions of Chrome - this.skip(); - } window.localStorage.setItem = sinon.stub(); window.localStorage.getItem = sinon.stub(); @@ -79,9 +74,7 @@ describe('WebUtil', function () { return WebUtil.initSettings(); }); afterEach(function () { - if (origLocalStorage !== undefined) { - Object.defineProperty(window, "localStorage", origLocalStorage); - } + Object.defineProperty(window, "localStorage", origLocalStorage); }); describe('writeSetting', function () { -- cgit v1.2.1 From 88a36370a983c86026fdd0a8ac47ce820034c6a0 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 13 Oct 2022 08:56:47 +0200 Subject: Add unit tests for browser detection --- tests/test.browser.js | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 tests/test.browser.js diff --git a/tests/test.browser.js b/tests/test.browser.js new file mode 100644 index 0000000..ae446cc --- /dev/null +++ b/tests/test.browser.js @@ -0,0 +1,55 @@ +/* eslint-disable no-console */ +const expect = chai.expect; + +import { isSafari, isFirefox } from '../core/util/browser.js'; + +describe('Browser detection', function () { + let origNavigator; + beforeEach(function () { + // window.navigator is a protected read-only property in many + // environments, so we need to redefine it whilst running these + // tests. + origNavigator = Object.getOwnPropertyDescriptor(window, "navigator"); + + Object.defineProperty(window, "navigator", {value: {}}); + }); + + afterEach(function () { + Object.defineProperty(window, "navigator", origNavigator); + }); + + it('should handle Chrome', function () { + navigator.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36"; + + expect(isSafari()).to.be.false; + expect(isFirefox()).to.be.false; + }); + + it('should handle Chromium', function () { + navigator.userAgent = "Mozilla/5.0 (X11; Linux armv7l) AppleWebKit/537.36 (KHTML, like Gecko) Raspbian Chromium/74.0.3729.157 Chrome/74.0.3729.157 Safari/537.36"; + + expect(isSafari()).to.be.false; + expect(isFirefox()).to.be.false; + }); + + it('should handle Firefox', function () { + navigator.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0"; + + expect(isSafari()).to.be.false; + expect(isFirefox()).to.be.true; + }); + + it('should handle Edge', function () { + navigator.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36 Edg/106.0.1370.34"; + + expect(isSafari()).to.be.false; + expect(isFirefox()).to.be.false; + }); + + it('should handle Opera', function () { + navigator.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36 OPR/91.0.4516.20"; + + expect(isSafari()).to.be.false; + expect(isFirefox()).to.be.false; + }); +}); -- cgit v1.2.1 From 4a34ee4b1e1da67111d50eb2cd059e38aae4349a Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 13 Oct 2022 08:59:57 +0200 Subject: Remove navigator check from browser tests This is a fundamental object that should always be present. --- core/util/browser.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/core/util/browser.js b/core/util/browser.js index 24b5e96..fb0d344 100644 --- a/core/util/browser.js +++ b/core/util/browser.js @@ -78,26 +78,25 @@ export const hasScrollbarGutter = _hasScrollbarGutter; */ export function isMac() { - return navigator && !!(/mac/i).exec(navigator.platform); + return !!(/mac/i).exec(navigator.platform); } export function isWindows() { - return navigator && !!(/win/i).exec(navigator.platform); + return !!(/win/i).exec(navigator.platform); } export function isIOS() { - return navigator && - (!!(/ipad/i).exec(navigator.platform) || + return (!!(/ipad/i).exec(navigator.platform) || !!(/iphone/i).exec(navigator.platform) || !!(/ipod/i).exec(navigator.platform)); } export function isSafari() { - return navigator && (navigator.userAgent.indexOf('Safari') !== -1 && - navigator.userAgent.indexOf('Chrome') === -1); + return (navigator.userAgent.indexOf('Safari') !== -1 && + navigator.userAgent.indexOf('Chrome') === -1); } export function isFirefox() { - return navigator && !!(/firefox/i).exec(navigator.userAgent); + return !!(/firefox/i).exec(navigator.userAgent); } -- cgit v1.2.1 From ee5e3c5fa3f247d032d48554cb41a63315522870 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 13 Oct 2022 09:14:49 +0200 Subject: Refine browser detection Try to follow the principle outlined by Mozilla when detecting browsers and engines. --- core/util/browser.js | 46 +++++++++++++++++++++++++-- tests/test.browser.js | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 130 insertions(+), 4 deletions(-) diff --git a/core/util/browser.js b/core/util/browser.js index fb0d344..7d2bfd8 100644 --- a/core/util/browser.js +++ b/core/util/browser.js @@ -77,6 +77,8 @@ export const hasScrollbarGutter = _hasScrollbarGutter; * It's better to use feature detection than platform detection. */ +/* OS */ + export function isMac() { return !!(/mac/i).exec(navigator.platform); } @@ -91,12 +93,50 @@ export function isIOS() { !!(/ipod/i).exec(navigator.platform)); } +/* Browser */ + export function isSafari() { - return (navigator.userAgent.indexOf('Safari') !== -1 && - navigator.userAgent.indexOf('Chrome') === -1); + return !!navigator.userAgent.match('Safari/...') && + !navigator.userAgent.match('Chrome/...') && + !navigator.userAgent.match('Chromium/...') && + !navigator.userAgent.match('Epiphany/...'); } export function isFirefox() { - return !!(/firefox/i).exec(navigator.userAgent); + return !!navigator.userAgent.match('Firefox/...') && + !navigator.userAgent.match('Seamonkey/...'); +} + +export function isChrome() { + return !!navigator.userAgent.match('Chrome/...') && + !navigator.userAgent.match('Chromium/...') && + !navigator.userAgent.match('Edg/...') && + !navigator.userAgent.match('OPR/...'); +} + +export function isChromium() { + return !!navigator.userAgent.match('Chromium/...'); +} + +export function isOpera() { + return !!navigator.userAgent.match('OPR/...'); } +export function isEdge() { + return !!navigator.userAgent.match('Edg/...'); +} + +/* Engine */ + +export function isGecko() { + return !!navigator.userAgent.match('Gecko/...'); +} + +export function isWebKit() { + return !!navigator.userAgent.match('AppleWebKit/...') && + !navigator.userAgent.match('Chrome/...'); +} + +export function isBlink() { + return !!navigator.userAgent.match('Chrome/...'); +} diff --git a/tests/test.browser.js b/tests/test.browser.js index ae446cc..f80b12e 100644 --- a/tests/test.browser.js +++ b/tests/test.browser.js @@ -1,7 +1,8 @@ /* eslint-disable no-console */ const expect = chai.expect; -import { isSafari, isFirefox } from '../core/util/browser.js'; +import { isSafari, isFirefox, isChrome, isChromium, isOpera, isEdge, + isGecko, isWebKit, isBlink } from '../core/util/browser.js'; describe('Browser detection', function () { let origNavigator; @@ -23,6 +24,14 @@ describe('Browser detection', function () { expect(isSafari()).to.be.false; expect(isFirefox()).to.be.false; + expect(isChrome()).to.be.true; + expect(isChromium()).to.be.false; + expect(isOpera()).to.be.false; + expect(isEdge()).to.be.false; + + expect(isGecko()).to.be.false; + expect(isWebKit()).to.be.false; + expect(isBlink()).to.be.true; }); it('should handle Chromium', function () { @@ -30,6 +39,14 @@ describe('Browser detection', function () { expect(isSafari()).to.be.false; expect(isFirefox()).to.be.false; + expect(isChrome()).to.be.false; + expect(isChromium()).to.be.true; + expect(isOpera()).to.be.false; + expect(isEdge()).to.be.false; + + expect(isGecko()).to.be.false; + expect(isWebKit()).to.be.false; + expect(isBlink()).to.be.true; }); it('should handle Firefox', function () { @@ -37,6 +54,44 @@ describe('Browser detection', function () { expect(isSafari()).to.be.false; expect(isFirefox()).to.be.true; + expect(isChrome()).to.be.false; + expect(isChromium()).to.be.false; + expect(isOpera()).to.be.false; + expect(isEdge()).to.be.false; + + expect(isGecko()).to.be.true; + expect(isWebKit()).to.be.false; + expect(isBlink()).to.be.false; + }); + + it('should handle Seamonkey', function () { + navigator.userAgent = "Mozilla/5.0 (Windows NT 6.1; rv:36.0) Gecko/20100101 Firefox/36.0 Seamonkey/2.33.1"; + + expect(isSafari()).to.be.false; + expect(isFirefox()).to.be.false; + expect(isChrome()).to.be.false; + expect(isChromium()).to.be.false; + expect(isOpera()).to.be.false; + expect(isEdge()).to.be.false; + + expect(isGecko()).to.be.true; + expect(isWebKit()).to.be.false; + expect(isBlink()).to.be.false; + }); + + it('should handle Safari', function () { + navigator.userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 12_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6 Safari/605.1.15"; + + expect(isSafari()).to.be.true; + expect(isFirefox()).to.be.false; + expect(isChrome()).to.be.false; + expect(isChromium()).to.be.false; + expect(isOpera()).to.be.false; + expect(isEdge()).to.be.false; + + expect(isGecko()).to.be.false; + expect(isWebKit()).to.be.true; + expect(isBlink()).to.be.false; }); it('should handle Edge', function () { @@ -44,6 +99,14 @@ describe('Browser detection', function () { expect(isSafari()).to.be.false; expect(isFirefox()).to.be.false; + expect(isChrome()).to.be.false; + expect(isChromium()).to.be.false; + expect(isOpera()).to.be.false; + expect(isEdge()).to.be.true; + + expect(isGecko()).to.be.false; + expect(isWebKit()).to.be.false; + expect(isBlink()).to.be.true; }); it('should handle Opera', function () { @@ -51,5 +114,28 @@ describe('Browser detection', function () { expect(isSafari()).to.be.false; expect(isFirefox()).to.be.false; + expect(isChrome()).to.be.false; + expect(isChromium()).to.be.false; + expect(isOpera()).to.be.true; + expect(isEdge()).to.be.false; + + expect(isGecko()).to.be.false; + expect(isWebKit()).to.be.false; + expect(isBlink()).to.be.true; + }); + + it('should handle Epiphany', function () { + navigator.userAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.0 Safari/605.1.15 Epiphany/605.1.15"; + + expect(isSafari()).to.be.false; + expect(isFirefox()).to.be.false; + expect(isChrome()).to.be.false; + expect(isChromium()).to.be.false; + expect(isOpera()).to.be.false; + expect(isEdge()).to.be.false; + + expect(isGecko()).to.be.false; + expect(isWebKit()).to.be.true; + expect(isBlink()).to.be.false; }); }); -- cgit v1.2.1 From 8fb30fb9dc6771ca0e0c2ca8a37d13ee37a503da Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Fri, 23 Dec 2022 16:26:00 +0100 Subject: Add unit tests for OS detection --- tests/test.browser.js | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/tests/test.browser.js b/tests/test.browser.js index f80b12e..4fdc084 100644 --- a/tests/test.browser.js +++ b/tests/test.browser.js @@ -1,9 +1,69 @@ /* eslint-disable no-console */ const expect = chai.expect; -import { isSafari, isFirefox, isChrome, isChromium, isOpera, isEdge, +import { isMac, isWindows, isIOS, + isSafari, isFirefox, isChrome, isChromium, isOpera, isEdge, isGecko, isWebKit, isBlink } from '../core/util/browser.js'; +describe('OS detection', function () { + let origNavigator; + beforeEach(function () { + // window.navigator is a protected read-only property in many + // environments, so we need to redefine it whilst running these + // tests. + origNavigator = Object.getOwnPropertyDescriptor(window, "navigator"); + + Object.defineProperty(window, "navigator", {value: {}}); + }); + + afterEach(function () { + Object.defineProperty(window, "navigator", origNavigator); + }); + + it('should handle macOS', function () { + const platforms = [ + "MacIntel", + "MacPPC", + ]; + + platforms.forEach((platform) => { + navigator.platform = platform; + expect(isMac()).to.be.true; + expect(isWindows()).to.be.false; + expect(isIOS()).to.be.false; + }); + }); + + it('should handle Windows', function () { + const platforms = [ + "Win32", + "Win64", + ]; + + platforms.forEach((platform) => { + navigator.platform = platform; + expect(isMac()).to.be.false; + expect(isWindows()).to.be.true; + expect(isIOS()).to.be.false; + }); + }); + + it('should handle iOS', function () { + const platforms = [ + "iPhone", + "iPod", + "iPad", + ]; + + platforms.forEach((platform) => { + navigator.platform = platform; + expect(isMac()).to.be.false; + expect(isWindows()).to.be.false; + expect(isIOS()).to.be.true; + }); + }); +}); + describe('Browser detection', function () { let origNavigator; beforeEach(function () { -- cgit v1.2.1 From a187821e4f576c0569e7ca9a71ebb525734dda00 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Fri, 23 Dec 2022 16:36:49 +0100 Subject: Add OS checks for Android and ChromeOS --- core/util/browser.js | 9 +++++++++ tests/test.browser.js | 44 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/core/util/browser.js b/core/util/browser.js index 7d2bfd8..8195438 100644 --- a/core/util/browser.js +++ b/core/util/browser.js @@ -93,6 +93,15 @@ export function isIOS() { !!(/ipod/i).exec(navigator.platform)); } +export function isAndroid() { + return !!(/android/i).exec(navigator.platform); +} + +export function isChromeOS() { + /* ChromeOS sets navigator.platform to Linux :/ */ + return !!navigator.userAgent.match(' CrOS '); +} + /* Browser */ export function isSafari() { diff --git a/tests/test.browser.js b/tests/test.browser.js index 4fdc084..4b6c127 100644 --- a/tests/test.browser.js +++ b/tests/test.browser.js @@ -1,7 +1,7 @@ /* eslint-disable no-console */ const expect = chai.expect; -import { isMac, isWindows, isIOS, +import { isMac, isWindows, isIOS, isAndroid, isChromeOS, isSafari, isFirefox, isChrome, isChromium, isOpera, isEdge, isGecko, isWebKit, isBlink } from '../core/util/browser.js'; @@ -26,11 +26,14 @@ describe('OS detection', function () { "MacPPC", ]; + navigator.userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 12_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6 Safari/605.1.15"; platforms.forEach((platform) => { navigator.platform = platform; expect(isMac()).to.be.true; expect(isWindows()).to.be.false; expect(isIOS()).to.be.false; + expect(isAndroid()).to.be.false; + expect(isChromeOS()).to.be.false; }); }); @@ -40,11 +43,14 @@ describe('OS detection', function () { "Win64", ]; + navigator.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36"; platforms.forEach((platform) => { navigator.platform = platform; expect(isMac()).to.be.false; expect(isWindows()).to.be.true; expect(isIOS()).to.be.false; + expect(isAndroid()).to.be.false; + expect(isChromeOS()).to.be.false; }); }); @@ -55,11 +61,47 @@ describe('OS detection', function () { "iPad", ]; + navigator.userAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 16_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Mobile/15E148 Safari/604.1"; platforms.forEach((platform) => { navigator.platform = platform; expect(isMac()).to.be.false; expect(isWindows()).to.be.false; expect(isIOS()).to.be.true; + expect(isAndroid()).to.be.false; + expect(isChromeOS()).to.be.false; + }); + }); + + it('should handle Android', function () { + const platforms = [ + "Android", + ]; + + navigator.userAgent = "Mozilla/5.0 (Android; Mobile; rv:13.0) Gecko/13.0 Firefox/13.0"; + platforms.forEach((platform) => { + navigator.platform = platform; + expect(isMac()).to.be.false; + expect(isWindows()).to.be.false; + expect(isIOS()).to.be.false; + expect(isAndroid()).to.be.true; + expect(isChromeOS()).to.be.false; + }); + }); + + it('should handle ChromeOS', function () { + let userAgents = [ + "Mozilla/5.0 (X11; CrOS x86_64 15183.59.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5359.75 Safari/537.36", + "Mozilla/5.0 (X11; CrOS aarch64 15183.59.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5359.75 Safari/537.36", + ]; + + navigator.platform = "Linux x86_64"; + userAgents.forEach((ua) => { + navigator.userAgent = ua; + expect(isMac()).to.be.false; + expect(isWindows()).to.be.false; + expect(isIOS()).to.be.false; + expect(isAndroid()).to.be.false; + expect(isChromeOS()).to.be.true; }); }); }); -- cgit v1.2.1 From 12a7c6f0de0ca51126c9a7292669c03f483049e0 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Fri, 23 Dec 2022 16:58:45 +0100 Subject: Check for Android using userAgent Modern Android systems seem to report "Linux" for navigator.platform, so we can no longer rely on that. --- core/util/browser.js | 3 ++- tests/test.browser.js | 11 ++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/core/util/browser.js b/core/util/browser.js index 8195438..bbc9f5c 100644 --- a/core/util/browser.js +++ b/core/util/browser.js @@ -94,7 +94,8 @@ export function isIOS() { } export function isAndroid() { - return !!(/android/i).exec(navigator.platform); + /* Android sets navigator.platform to Linux :/ */ + return !!navigator.userAgent.match('Android '); } export function isChromeOS() { diff --git a/tests/test.browser.js b/tests/test.browser.js index 4b6c127..3b2299f 100644 --- a/tests/test.browser.js +++ b/tests/test.browser.js @@ -73,13 +73,14 @@ describe('OS detection', function () { }); it('should handle Android', function () { - const platforms = [ - "Android", + let userAgents = [ + "Mozilla/5.0 (Linux; Android 13; SM-G960U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5359.128 Mobile Safari/537.36", + "Mozilla/5.0 (Android 13; Mobile; LG-M255; rv:108.0) Gecko/108.0 Firefox/108.0", ]; - navigator.userAgent = "Mozilla/5.0 (Android; Mobile; rv:13.0) Gecko/13.0 Firefox/13.0"; - platforms.forEach((platform) => { - navigator.platform = platform; + navigator.platform = "Linux x86_64"; + userAgents.forEach((ua) => { + navigator.userAgent = ua; expect(isMac()).to.be.false; expect(isWindows()).to.be.false; expect(isIOS()).to.be.false; -- cgit v1.2.1 From 5de478d6e7b70c080a4a64ec6deb590c773ca416 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Fri, 23 Dec 2022 16:59:32 +0100 Subject: Restrict forced panning to known bad platforms Let's not punish systems that implement overlay scrollbars in a functional way. The only current example is Firefox on Windows 11 and on Linux. --- app/ui.js | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/app/ui.js b/app/ui.js index c277eea..c1f6776 100644 --- a/app/ui.js +++ b/app/ui.js @@ -8,7 +8,8 @@ import * as Log from '../core/util/logging.js'; import _, { l10n } from './localization.js'; -import { isTouchDevice, isSafari, hasScrollbarGutter, dragThreshold } +import { isTouchDevice, isMac, isIOS, isAndroid, isChromeOS, isSafari, + hasScrollbarGutter, dragThreshold } from '../core/util/browser.js'; import { setCapture, getPointerEvent } from '../core/util/events.js'; import KeyTable from "../core/input/keysym.js"; @@ -1326,13 +1327,25 @@ const UI = { const scaling = UI.getSetting('resize') === 'scale'; + // Some platforms have overlay scrollbars that are difficult + // to use in our case, which means we have to force panning + // FIXME: Working scrollbars can still be annoying to use with + // touch, so we should ideally be able to have both + // panning and scrollbars at the same time + + let brokenScrollbars = false; + + if (!hasScrollbarGutter) { + if (isIOS() || isAndroid() || isMac() || isChromeOS()) { + brokenScrollbars = true; + } + } + if (scaling) { // Can't be clipping if viewport is scaled to fit UI.forceSetting('view_clip', false); UI.rfb.clipViewport = false; - } else if (!hasScrollbarGutter) { - // Some platforms have scrollbars that are difficult - // to use in our case, so we always use our own panning + } else if (brokenScrollbars) { UI.forceSetting('view_clip', true); UI.rfb.clipViewport = true; } else { -- cgit v1.2.1