1/* 2 * desktop-options.test.ts 3 * 4 * Copyright (C) 2021 by RStudio, PBC 5 * 6 * Unless you have received this program directly from RStudio pursuant 7 * to the terms of a commercial license agreement with RStudio, then 8 * this program is licensed to you under the terms of version 3 of the 9 * GNU Affero General Public License. This program is distributed WITHOUT 10 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT, 11 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the 12 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details. 13 */ 14 15import { BrowserWindow, Rectangle, screen } from 'electron'; 16import { describe } from 'mocha'; 17import { assert } from 'chai'; 18import sinon from 'sinon'; 19import { createSinonStubInstanceForSandbox } from '../unit-utils'; 20 21import { DesktopOptions, DesktopOptionsImpl, kDesktopOptionDefaults, clearOptionsSingleton, firstIsInsideSecond } from '../../../src/main/desktop-options'; 22import { FilePath } from '../../../src/core/file-path'; 23import { Err, isSuccessful } from '../../../src/core/err'; 24import { tempDirectory } from '../unit-utils'; 25import { Display } from 'electron/main'; 26 27const kTestingConfigDirectory = tempDirectory('DesktopOptionsTesting').toString(); 28 29function testingDesktopOptions(): DesktopOptionsImpl { 30 return DesktopOptions(kTestingConfigDirectory); 31} 32 33function deleteTestingDesktopOptions(): Err { 34 clearOptionsSingleton(); 35 const filepath = new FilePath(kTestingConfigDirectory); 36 return filepath.removeIfExistsSync(); 37} 38 39function rec(height = 10, width = 10, x = 0, y = 0): Rectangle { 40 return {height: height, width: width, x: x, y: y}; 41} 42 43describe('DesktopOptions', () => { 44 afterEach(() => { 45 assert(isSuccessful(deleteTestingDesktopOptions())); 46 }); 47 48 it('use default values when no value has been set before', () => { 49 const options = testingDesktopOptions(); 50 51 const nonWindowsRBinDir = ''; 52 const nonWindowsPreferR64 = false; 53 54 assert.equal(options.proportionalFont(), kDesktopOptionDefaults.Font.ProportionalFont); 55 assert.equal(options.fixWidthFont(), kDesktopOptionDefaults.Font.FixWidthFont); 56 assert.equal(options.useFontConfigDb(), kDesktopOptionDefaults.Font.UseFontConfigDb); 57 assert.equal(options.zoomLevel(), kDesktopOptionDefaults.View.ZoomLevel); 58 assert.deepEqual(options.windowBounds(), kDesktopOptionDefaults.View.WindowBounds); 59 assert.equal(options.accessibility(), kDesktopOptionDefaults.View.Accessibility); 60 assert.equal(options.lastRemoteSessionUrl(), kDesktopOptionDefaults.Session.LastRemoteSessionUrl); 61 assert.deepEqual(options.authCookies(), kDesktopOptionDefaults.Session.AuthCookies); 62 assert.deepEqual(options.tempAuthCookies(), kDesktopOptionDefaults.Session.TempAuthCookies); 63 assert.deepEqual(options.ignoredUpdateVersions(), kDesktopOptionDefaults.General.IgnoredUpdateVersions); 64 assert.equal(options.clipboardMonitoring(), kDesktopOptionDefaults.General.ClipboardMonitoring); 65 if (process.platform === 'win32') { 66 assert.equal(options.rBinDir(), kDesktopOptionDefaults.Platform.Windows.RBinDir); 67 assert.equal(options.peferR64(), kDesktopOptionDefaults.Platform.Windows.PreferR64); 68 } else { 69 assert.equal(options.rBinDir(), nonWindowsRBinDir); 70 assert.equal(options.peferR64(), nonWindowsPreferR64); 71 } 72 }); 73 it('set/get functionality returns correct values', () => { 74 const options = testingDesktopOptions(); 75 76 const newProportionalFont = 'testProportionalFont'; 77 const newFixWidthFont = 'testFixWidthFont'; 78 const newUseFontConfigDb = !kDesktopOptionDefaults.Font.UseFontConfigDb; 79 const newZoom = 123; 80 const newWindowBounds = {width: 123, height: 321, x: 0, y: 0}; 81 const newAccessibility = !kDesktopOptionDefaults.View.Accessibility; 82 const newLastRemoteSessionUrl = 'testLastRemoteSessionUrl'; 83 const newAuthCookies = ['test', 'Autht', 'Cookies']; 84 const newTempAuthCookies = ['test', 'Temp', 'Auth', 'Cookies']; 85 const newIgnoredUpdateVersions = ['test', 'Ignored', 'Update', 'Versions']; 86 const newClipboardMonitoring = !kDesktopOptionDefaults.General.ClipboardMonitoring; 87 const newRBinDir = 'testRBinDir'; 88 const newPeferR64 = !kDesktopOptionDefaults.Platform.Windows.PreferR64; 89 90 const nonWindowsRBinDir = ''; 91 const nonWindowsPreferR64 = false; 92 93 options.setProportionalFont(newProportionalFont); 94 options.setFixWidthFont(newFixWidthFont); 95 options.setUseFontConfigDb(newUseFontConfigDb); 96 options.setZoomLevel(newZoom); 97 options.saveWindowBounds(newWindowBounds); 98 options.setAccessibility(newAccessibility); 99 options.setLastRemoteSessionUrl(newLastRemoteSessionUrl); 100 options.setAuthCookies(newAuthCookies); 101 options.setTempAuthCookies(newTempAuthCookies); 102 options.setIgnoredUpdateVersions(newIgnoredUpdateVersions); 103 options.setClipboardMonitoring(newClipboardMonitoring); 104 options.setRBinDir(newRBinDir); 105 options.setPeferR64(newPeferR64); 106 107 assert.equal(options.proportionalFont(), newProportionalFont); 108 assert.equal(options.fixWidthFont(), newFixWidthFont); 109 assert.equal(options.useFontConfigDb(), newUseFontConfigDb); 110 assert.equal(options.zoomLevel(), newZoom); 111 assert.deepEqual(options.windowBounds(), newWindowBounds); 112 assert.equal(options.accessibility(), newAccessibility); 113 assert.equal(options.lastRemoteSessionUrl(), newLastRemoteSessionUrl); 114 assert.deepEqual(options.authCookies(), newAuthCookies); 115 assert.deepEqual(options.tempAuthCookies(), newTempAuthCookies); 116 assert.deepEqual(options.ignoredUpdateVersions(), newIgnoredUpdateVersions); 117 assert.equal(options.clipboardMonitoring(), newClipboardMonitoring); 118 if (process.platform === 'win32') { 119 assert.equal(options.rBinDir(), newRBinDir); 120 assert.equal(options.peferR64(), newPeferR64); 121 } else { 122 assert.equal(options.rBinDir(), nonWindowsRBinDir); 123 assert.equal(options.peferR64(), nonWindowsPreferR64); 124 } 125 }); 126 it('values persist between instances', () => { 127 const options1 = testingDesktopOptions(); 128 const newZoom = 1234; 129 130 assert.equal(options1.zoomLevel(), kDesktopOptionDefaults.View.ZoomLevel); 131 options1.setZoomLevel(newZoom); 132 assert.equal(options1.zoomLevel(), newZoom); 133 134 clearOptionsSingleton(); 135 const options2 = testingDesktopOptions(); 136 assert.equal(options2.zoomLevel(), newZoom); 137 }); 138 it('restores window bounds to correct display', () => { 139 const displays = [{workArea: {width: 2000, height: 2000, x: 0, y: 0}}, {workArea: {width: 2000, height: 2000, x: 2000, y: 0}}]; 140 const savedWinBounds = {width: 500, height: 500, x: 2100, y: 100}; 141 142 // Save bounds onto a secondary display on the right 143 DesktopOptions().saveWindowBounds(savedWinBounds); 144 145 const sandbox = sinon.createSandbox(); 146 sandbox.stub(screen, 'getAllDisplays').returns(displays as Display[]); 147 const testMainWindow = createSinonStubInstanceForSandbox(sandbox, BrowserWindow); 148 testMainWindow.setBounds.withArgs(savedWinBounds); 149 testMainWindow.getSize.returns([savedWinBounds.width, savedWinBounds.height]); 150 151 DesktopOptions().restoreMainWindowBounds(testMainWindow); 152 153 sandbox.assert.calledOnceWithExactly(testMainWindow.setBounds, savedWinBounds); 154 sandbox.assert.calledOnce(testMainWindow.setSize); 155 sandbox.assert.alwaysCalledWith(testMainWindow.setSize, savedWinBounds.width, savedWinBounds.height); 156 sandbox.assert.callCount(testMainWindow.setPosition, 0); 157 sandbox.restore(); 158 }); 159 it('restores window bounds to default when saved display no longer present', () => { 160 const defaultDisplay = {bounds: {width: 2000, height: 2000, x: 0, y: 0}}; 161 const savedWinBounds = {width: 500, height: 500, x: 0, y: 0}; 162 const defaultWinWidth = kDesktopOptionDefaults.View.WindowBounds.width; 163 const defaultWinHeight = kDesktopOptionDefaults.View.WindowBounds.height; 164 165 const sandbox = sinon.createSandbox(); 166 sandbox.stub(screen, 'getAllDisplays').returns([]); 167 sandbox.stub(screen, 'getPrimaryDisplay').returns(defaultDisplay as Display); 168 const testMainWindow = createSinonStubInstanceForSandbox(sandbox, BrowserWindow); 169 testMainWindow.setSize 170 .withArgs(defaultWinWidth, defaultWinHeight); 171 testMainWindow.getSize.returns([defaultWinWidth, defaultWinHeight]); 172 173 // Make sure some bounds are already saved 174 DesktopOptions().saveWindowBounds(savedWinBounds); 175 176 DesktopOptions().restoreMainWindowBounds(testMainWindow); 177 178 sandbox.assert.calledTwice(testMainWindow.setSize); 179 sandbox.assert.alwaysCalledWith(testMainWindow.setSize, defaultWinWidth, defaultWinHeight); 180 sandbox.restore(); 181 }); 182}); 183 184/** 185 * A note on Electron's rectangle/display coordinate system: 186 * (x, y) coord is top left corner of a Rectangle or Display object 187 * (x + width, y + height) is bottom right corner 188 * 189 * x increases to the right, decreases to the left 190 * y increases downwards, decreases upwards 191 * 192 * primary display's (x, y) coord is always (0, 0) 193 * negative values are legal 194 * external display to the right of primary display could be (primary.width, 0) ex. (1920, 0) 195 * external display to the left of primary display could be (-secondary.width, 0) ex. (-1200, 0) 196 */ 197describe('FirstIsInsideSecond', () => { 198 const INNER_WIDTH = 10; 199 const INNER_HEIGHT = 10; 200 const INNER_X = 0; 201 const INNER_Y = 0; 202 203 const OUTER_WIDTH = 20; 204 const OUTER_HEIGHT = 20; 205 const OUTER_X = 0; 206 const OUTER_Y = 0; 207 208 const X_FAR_OUT_WEST = -100; 209 const X_FAR_BACK_EAST = 100; 210 const Y_FAR_UP_NORTH = -100; 211 const Y_FAR_DOWN_SOUTH = 100; 212 213 it('basic case', () => { 214 assert.isTrue(firstIsInsideSecond( 215 rec(INNER_WIDTH, INNER_HEIGHT, INNER_X + 1, INNER_Y + 1), 216 rec(OUTER_WIDTH, OUTER_HEIGHT, OUTER_X, OUTER_Y))); // entirely inside 217 assert.isTrue(firstIsInsideSecond( 218 rec(), 219 rec(OUTER_WIDTH, OUTER_HEIGHT, OUTER_X, OUTER_Y))); // top and left boarders shared 220 assert.isTrue(firstIsInsideSecond( 221 rec(), 222 rec())); // same size rectangles is valid 223 }); 224 it('backwards case', () => { 225 assert.isFalse(firstIsInsideSecond( 226 rec(OUTER_WIDTH, OUTER_HEIGHT, OUTER_X, OUTER_Y), 227 rec())); 228 }); 229 it ('partially outside', () => { 230 assert.isFalse(firstIsInsideSecond( 231 rec(INNER_WIDTH, INNER_HEIGHT, INNER_X + 11, INNER_Y), 232 rec(OUTER_WIDTH, OUTER_HEIGHT, OUTER_X, OUTER_Y))); 233 assert.isFalse(firstIsInsideSecond( 234 rec(INNER_WIDTH, INNER_HEIGHT, INNER_X, INNER_Y + 11), 235 rec(OUTER_WIDTH, OUTER_HEIGHT, OUTER_X, OUTER_Y))); 236 assert.isFalse(firstIsInsideSecond( 237 rec(INNER_WIDTH, INNER_HEIGHT, INNER_X - 1, INNER_Y), 238 rec(OUTER_WIDTH, OUTER_HEIGHT, OUTER_X, OUTER_Y))); 239 assert.isFalse(firstIsInsideSecond( 240 rec(INNER_WIDTH, INNER_HEIGHT, INNER_X, INNER_Y - 1), 241 rec(OUTER_WIDTH, OUTER_HEIGHT, OUTER_X, OUTER_Y))); 242 }); 243 it ('entirely outside', () => { 244 assert.isFalse(firstIsInsideSecond( 245 rec(INNER_WIDTH, INNER_HEIGHT, X_FAR_BACK_EAST, Y_FAR_DOWN_SOUTH), 246 rec(OUTER_WIDTH, OUTER_HEIGHT, OUTER_X, OUTER_Y))); 247 assert.isFalse(firstIsInsideSecond( 248 rec(INNER_WIDTH, INNER_HEIGHT, X_FAR_OUT_WEST, Y_FAR_DOWN_SOUTH), 249 rec(OUTER_WIDTH, OUTER_HEIGHT, OUTER_X, OUTER_Y))); 250 assert.isFalse(firstIsInsideSecond( 251 rec(INNER_WIDTH, INNER_HEIGHT, X_FAR_BACK_EAST, Y_FAR_UP_NORTH), 252 rec(OUTER_WIDTH, OUTER_HEIGHT, OUTER_X, OUTER_Y))); 253 assert.isFalse(firstIsInsideSecond( 254 rec(INNER_WIDTH, INNER_HEIGHT, X_FAR_OUT_WEST, Y_FAR_UP_NORTH), 255 rec(OUTER_WIDTH, OUTER_HEIGHT, OUTER_X, OUTER_Y))); 256 }); 257});