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});