From 113d87fe0406baebf1c7ad1bcfdb3a0902bb51a7 Mon Sep 17 00:00:00 2001 From: Devin Binnie <52460000+devinbinnie@users.noreply.github.com> Date: Wed, 10 Nov 2021 09:51:56 -0500 Subject: [PATCH] [MM-39888][MM-39960][MM-39961] Added tests for common/tabs, common/config, added coverage tool (#1857) * [MM-39888] Unit tests for common/config * [MM-39960][MM-39961] Added tests for common/tabs, added coverage tool Co-authored-by: Mattermod --- .gitignore | 1 + package.json | 3 +- src/common/config/RegistryConfig.test.js | 72 ++++ src/common/config/defaultPreferences.ts | 1 + src/common/config/index.test.js | 386 +++++++++++++++++++ src/common/config/upgradePreferences.test.js | 141 +++++++ src/common/config/upgradePreferences.ts | 6 +- src/common/tabs/TabView.test.js | 35 ++ 8 files changed, 640 insertions(+), 5 deletions(-) create mode 100644 src/common/config/RegistryConfig.test.js create mode 100644 src/common/config/index.test.js create mode 100644 src/common/config/upgradePreferences.test.js create mode 100644 src/common/tabs/TabView.test.js diff --git a/.gitignore b/.gitignore index bef7390f..f751ab34 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ release/ npm-debug.log* build/ +coverage/ dist/ test-results.xml diff --git a/package.json b/package.json index 2c277cbd..a3d482cd 100644 --- a/package.json +++ b/package.json @@ -44,8 +44,7 @@ "test:e2e:build": "webpack-cli --bail --config webpack.config.test.js", "test:e2e:run": "electron-mocha -r @babel/register --reporter mocha-circleci-reporter dist/tests/e2e_bundle.js", "test:unit": "jest", - "test:unit:build": "cross-env NODE_ENV=test webpack-cli --bail --config webpack.config.test.js", - "test:unit:run": "cross-env NODE_ENV=test mocha --reporter mocha-circleci-reporter dist/tests/test_bundle.js", + "test:coverage": "jest --coverage", "package:all": "cross-env NODE_ENV=production npm-run-all check-build-config package:windows package:mac package:mac-universal package:linux", "package:windows": "cross-env NODE_ENV=production npm-run-all check-build-config build-prod && electron-builder --win --x64 --ia32 --publish=never", "package:mac": "cross-env NODE_ENV=production npm-run-all check-build-config build-prod && electron-builder --mac --x64 --arm64 --publish=never", diff --git a/src/common/config/RegistryConfig.test.js b/src/common/config/RegistryConfig.test.js new file mode 100644 index 00000000..61e74698 --- /dev/null +++ b/src/common/config/RegistryConfig.test.js @@ -0,0 +1,72 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import RegistryConfig from 'common/config/RegistryConfig'; + +jest.mock('winreg-utf8', () => { + return jest.fn().mockImplementation(({hive, key}) => { + return { + values: (fn) => { + if (hive === 'correct-hive') { + fn(null, [ + { + name: `${key}-name-1`, + value: `${key}-value-1`, + + }, + { + name: `${key}-name-2`, + value: `${key}-value-2`, + }, + ]); + } else if (hive === 'really-bad-hive') { + throw new Error('This is an error'); + } else { + fn('Error', []); + } + }, + }; + }); +}); + +jest.mock('electron-log', () => ({ + error: jest.fn(), +})); + +describe('common/config/RegistryConfig', () => { + describe('getRegistryEntryValues', () => { + it('should return correct values', () => { + const registryConfig = new RegistryConfig(); + expect(registryConfig.getRegistryEntryValues('correct-hive', 'correct-key')).resolves.toStrictEqual([ + { + name: 'correct-key-name-1', + value: 'correct-key-value-1', + }, + { + name: 'correct-key-name-2', + value: 'correct-key-value-2', + }, + ]); + }); + + it('should return correct value by name', () => { + const registryConfig = new RegistryConfig(); + expect(registryConfig.getRegistryEntryValues('correct-hive', 'correct-key', 'correct-key-name-1')).resolves.toBe('correct-key-value-1'); + }); + + it('should return undefined with wrong name', () => { + const registryConfig = new RegistryConfig(); + expect(registryConfig.getRegistryEntryValues('correct-hive', 'correct-key', 'wrong-key-name-1')).resolves.toBe(undefined); + }); + + it('should return undefined with bad hive', () => { + const registryConfig = new RegistryConfig(); + expect(registryConfig.getRegistryEntryValues('bad-hive', 'correct-key')).resolves.toBe(undefined); + }); + + it('should call reject when an error occurs', () => { + const registryConfig = new RegistryConfig(); + expect(registryConfig.getRegistryEntryValues('really-bad-hive', 'correct-key')).rejects.toThrow(new Error('This is an error')); + }); + }); +}); diff --git a/src/common/config/defaultPreferences.ts b/src/common/config/defaultPreferences.ts index c6bb8533..9e6adcf9 100644 --- a/src/common/config/defaultPreferences.ts +++ b/src/common/config/defaultPreferences.ts @@ -33,6 +33,7 @@ const defaultPreferences: ConfigV3 = { autostart: true, spellCheckerLocales: [], darkMode: false, + lastActiveTeam: 0, downloadLocation: getDefaultDownloadLocation(), }; diff --git a/src/common/config/index.test.js b/src/common/config/index.test.js new file mode 100644 index 00000000..043f28f7 --- /dev/null +++ b/src/common/config/index.test.js @@ -0,0 +1,386 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import Config from 'common/config'; + +const configPath = '/fake/config/path'; + +jest.mock('electron', () => ({ + app: { + name: 'Mattermost', + }, + nativeTheme: { + shouldUseDarkColors: false, + }, +})); + +jest.mock('main/Validator', () => ({ + validateV0ConfigData: (configData) => (configData.version === 0 ? configData : null), + validateV1ConfigData: (configData) => (configData.version === 1 ? configData : null), + validateV2ConfigData: (configData) => (configData.version === 2 ? configData : null), + validateV3ConfigData: (configData) => (configData.version === 3 ? configData : null), +})); + +jest.mock('electron-log', () => ({ + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), +})); + +jest.mock('common/tabs/TabView', () => ({ + getDefaultTeamWithTabsFromTeam: (value) => ({ + ...value, + tabs: [ + { + name: 'tab1', + }, + { + name: 'tab2', + }, + ], + }), +})); + +const buildTeam = { + name: 'build-team-1', + order: 0, + url: 'http://build-team-1.com', +}; + +const buildTeamWithTabs = { + ...buildTeam, + tabs: [ + { + name: 'tab1', + }, + { + name: 'tab2', + }, + ], +}; + +const registryTeam = { + name: 'registry-team-1', + order: 0, + url: 'http://registry-team-1.com', +}; + +const team = { + name: 'team-1', + order: 0, + url: 'http://team-1.com', + tabs: [ + { + name: 'tab1', + }, + { + name: 'tab2', + }, + ], +}; + +jest.mock('common/config/upgradePreferences', () => { + return jest.fn().mockImplementation((configData) => { + return {...configData, version: 10}; + }); +}); + +jest.mock('common/config/buildConfig', () => { + return { + defaultTeams: [buildTeam], + }; +}); + +jest.mock('common/config/RegistryConfig', () => { + return jest.fn(); +}); + +describe('common/config', () => { + it('should load buildConfig', () => { + const config = new Config(configPath); + expect(config.predefinedTeams).toContainEqual(buildTeamWithTabs); + }); + + describe('loadRegistry', () => { + it('should load the registry items and reload the config', () => { + const config = new Config(configPath); + config.reload = jest.fn(); + config.loadRegistry({teams: [registryTeam]}); + expect(config.reload).toHaveBeenCalled(); + expect(config.predefinedTeams).toContainEqual({ + ...registryTeam, + tabs: [ + { + name: 'tab1', + }, + { + name: 'tab2', + }, + ], + }); + }); + }); + + describe('reload', () => { + it('should emit update and synchronize events', () => { + const config = new Config(configPath); + config.loadDefaultConfigData = jest.fn(); + config.loadBuildConfigData = jest.fn(); + config.loadLocalConfigFile = jest.fn(); + config.checkForConfigUpdates = jest.fn(); + config.regenerateCombinedConfigData = jest.fn().mockImplementation(() => { + config.combinedData = {test: 'test'}; + }); + config.emit = jest.fn(); + + config.reload(); + expect(config.emit).toHaveBeenNthCalledWith(1, 'update', {test: 'test'}); + expect(config.emit).toHaveBeenNthCalledWith(2, 'synchronize'); + }); + }); + + describe('set', () => { + it('should set an arbitrary value and save to local config data', () => { + const config = new Config(configPath); + config.localConfigData = {}; + config.regenerateCombinedConfigData = jest.fn().mockImplementation(() => { + config.combinedData = {...config.localConfigData}; + }); + config.saveLocalConfigData = jest.fn(); + + config.set('setting', 'test_value_1'); + expect(config.combinedData.setting).toBe('test_value_1'); + expect(config.regenerateCombinedConfigData).toHaveBeenCalled(); + expect(config.saveLocalConfigData).toHaveBeenCalled(); + }); + + it('should set teams without including predefined', () => { + const config = new Config(configPath); + config.localConfigData = {}; + config.regenerateCombinedConfigData = jest.fn().mockImplementation(() => { + config.combinedData = {...config.localConfigData}; + }); + config.saveLocalConfigData = jest.fn(); + + config.set('teams', [{...buildTeamWithTabs, name: 'build-team-2'}, team]); + expect(config.localConfigData.teams).not.toContainEqual({...buildTeamWithTabs, name: 'build-team-2'}); + expect(config.localConfigData.teams).toContainEqual(team); + expect(config.predefinedTeams).toContainEqual({...buildTeamWithTabs, name: 'build-team-2'}); + }); + }); + + describe('saveLocalConfigData', () => { + it('should emit update and synchronize events on save', () => { + const config = new Config(configPath); + config.localConfigData = {test: 'test'}; + config.combinedData = {...config.localConfigData}; + config.writeFile = jest.fn().mockImplementation((configFilePath, data, callback) => { + callback(); + }); + config.emit = jest.fn(); + + config.saveLocalConfigData(); + expect(config.emit).toHaveBeenNthCalledWith(1, 'update', {test: 'test'}); + expect(config.emit).toHaveBeenNthCalledWith(2, 'synchronize'); + }); + + it('should emit error when fs.writeSync throws an error', () => { + const config = new Config(configPath); + config.localConfigData = {test: 'test'}; + config.combinedData = {...config.localConfigData}; + config.writeFile = jest.fn().mockImplementation((configFilePath, data, callback) => { + callback({message: 'Error message'}); + }); + config.emit = jest.fn(); + + config.saveLocalConfigData(); + expect(config.emit).toHaveBeenNthCalledWith(1, 'error', {message: 'Error message'}); + }); + + it('should emit error when writeFile throws an error', () => { + const config = new Config(configPath); + config.localConfigData = {test: 'test'}; + config.combinedData = {...config.localConfigData}; + config.writeFile = jest.fn().mockImplementation(() => { + throw new Error('Error message'); + }); + config.emit = jest.fn(); + + config.saveLocalConfigData(); + expect(config.emit).toHaveBeenNthCalledWith(1, 'error', new Error('Error message')); + }); + + it('should retry when file is locked', () => { + const testFunc = jest.fn(); + const config = new Config(configPath); + config.localConfigData = {test: 'test'}; + config.combinedData = {...config.localConfigData}; + config.writeFile = jest.fn().mockImplementation((configFilePath, data, callback) => { + config.saveLocalConfigData = testFunc; + callback({code: 'EBUSY'}); + }); + config.emit = jest.fn(); + + config.saveLocalConfigData(); + expect(testFunc).toHaveBeenCalled(); + }); + }); + + describe('loadLocalConfigFile', () => { + it('should use defaults if readFileSync fails', () => { + const config = new Config(configPath); + config.defaultConfigData = {test: 'test'}; + config.combinedData = {...config.localConfigData}; + config.readFileSync = jest.fn().mockImplementation(() => { + throw new Error('Error message'); + }); + config.writeFileSync = jest.fn(); + + const configData = config.loadLocalConfigFile(); + expect(configData).toStrictEqual({test: 'test'}); + }); + + it('should use defaults if validation fails', () => { + const config = new Config(configPath); + config.defaultConfigData = {test: 'test'}; + config.combinedData = {...config.localConfigData}; + config.readFileSync = jest.fn().mockImplementation(() => { + return {version: -1}; + }); + config.writeFileSync = jest.fn(); + + const configData = config.loadLocalConfigFile(); + expect(configData).toStrictEqual({test: 'test'}); + }); + + it('should return config data if valid', () => { + const config = new Config(configPath); + config.readFileSync = jest.fn().mockImplementation(() => { + return {version: 3}; + }); + config.writeFileSync = jest.fn(); + + const configData = config.loadLocalConfigFile(); + expect(configData).toStrictEqual({version: 3}); + }); + }); + + describe('checkForConfigUpdates', () => { + it('should upgrade to latest version', () => { + const config = new Config(configPath); + config.defaultConfigData = {version: 10}; + config.writeFileSync = jest.fn(); + + const configData = config.checkForConfigUpdates({version: 5, setting: 'true'}); + expect(configData).toStrictEqual({version: 10, setting: 'true'}); + }); + }); + + describe('regenerateCombinedConfigData', () => { + it('should combine config from all sources', () => { + const config = new Config(configPath); + config.predefinedTeams = []; + config.useNativeWindow = false; + config.defaultConfigData = {defaultSetting: 'default', otherDefaultSetting: 'default'}; + config.localConfigData = {otherDefaultSetting: 'local', localSetting: 'local', otherLocalSetting: 'local'}; + config.buildConfigData = {otherLocalSetting: 'build', buildSetting: 'build', otherBuildSetting: 'build'}; + config.registryConfigData = {otherBuildSetting: 'registry', registrySetting: 'registry'}; + + config.regenerateCombinedConfigData(); + config.combinedData.darkMode = false; + expect(config.combinedData).toStrictEqual({ + teams: [], + registryTeams: [], + appName: 'Mattermost', + useNativeWindow: false, + darkMode: false, + otherBuildSetting: 'registry', + registrySetting: 'registry', + otherLocalSetting: 'build', + buildSetting: 'build', + otherDefaultSetting: 'local', + localSetting: 'local', + defaultSetting: 'default', + }); + }); + + it('should combine teams from all sources and filter duplicates', () => { + const config = new Config(configPath); + config.defaultConfigData = {}; + config.localConfigData = {}; + config.buildConfigData = {enableServerManagement: true}; + config.registryConfigData = {}; + config.predefinedTeams = [team, team]; + config.useNativeWindow = false; + config.localConfigData = {teams: [ + team, + { + ...team, + name: 'local-team-2', + url: 'http://local-team-2.com', + }, + { + ...team, + name: 'local-team-1', + order: 1, + url: 'http://local-team-1.com', + }, + ]}; + + config.regenerateCombinedConfigData(); + config.combinedData.darkMode = false; + expect(config.combinedData).toStrictEqual({ + teams: [ + team, + { + ...team, + name: 'local-team-2', + order: 1, + url: 'http://local-team-2.com', + }, + { + ...team, + name: 'local-team-1', + order: 2, + url: 'http://local-team-1.com', + }, + ], + registryTeams: [], + appName: 'Mattermost', + useNativeWindow: false, + darkMode: false, + enableServerManagement: true, + }); + }); + + it('should not include local teams if enableServerManagement is false', () => { + const config = new Config(configPath); + config.defaultConfigData = {}; + config.localConfigData = {}; + config.buildConfigData = {enableServerManagement: false}; + config.registryConfigData = {}; + config.predefinedTeams = [team, team]; + config.useNativeWindow = false; + config.localConfigData = {teams: [ + team, + { + ...team, + name: 'local-team-1', + order: 1, + url: 'http://local-team-1.com', + }, + ]}; + + config.regenerateCombinedConfigData(); + config.combinedData.darkMode = false; + expect(config.combinedData).toStrictEqual({ + teams: [team], + registryTeams: [], + appName: 'Mattermost', + useNativeWindow: false, + darkMode: false, + enableServerManagement: false, + }); + }); + }); +}); diff --git a/src/common/config/upgradePreferences.test.js b/src/common/config/upgradePreferences.test.js new file mode 100644 index 00000000..a6025598 --- /dev/null +++ b/src/common/config/upgradePreferences.test.js @@ -0,0 +1,141 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {upgradeV0toV1, upgradeV1toV2, upgradeV2toV3} from 'common/config/upgradePreferences'; +import pastDefaultPreferences from 'common/config/pastDefaultPreferences'; + +jest.mock('common/tabs/TabView', () => ({ + getDefaultTeamWithTabsFromTeam: (value) => ({ + ...value, + tabs: [ + { + name: 'tab1', + }, + { + name: 'tab2', + }, + ], + }), +})); + +describe('common/config/upgradePreferences', () => { + describe('upgradeV0toV1', () => { + it('should upgrade from v0', () => { + const config = {url: 'http://server-1.com'}; + expect(upgradeV0toV1(config)).toStrictEqual({ + ...pastDefaultPreferences[1], + version: 1, + teams: [ + { + name: 'Primary team', + url: config.url, + }, + ], + }); + }); + }); + describe('upgradeV1toV2', () => { + it('should upgrade from v1', () => { + const config = { + version: 1, + teams: [{ + name: 'Primary team', + url: 'http://server-1.com', + }, { + name: 'Secondary team', + url: 'http://server-2.com', + }], + showTrayIcon: true, + trayIconTheme: 'dark', + minimizeToTray: true, + notifications: { + flashWindow: 2, + bounceIcon: true, + bounceIconType: 'informational', + }, + showUnreadBadge: false, + useSpellChecker: false, + enableHardwareAcceleration: false, + autostart: false, + spellCheckerLocale: 'en-CA', + }; + expect(upgradeV1toV2(config)).toStrictEqual({ + ...pastDefaultPreferences[2], + ...config, + version: 2, + teams: [{ + name: 'Primary team', + url: 'http://server-1.com', + order: 0, + }, { + name: 'Secondary team', + url: 'http://server-2.com', + order: 1, + }], + }); + }); + }); + describe('upgradeV2toV3', () => { + it('should upgrade from v2', () => { + const config = { + version: 2, + teams: [{ + name: 'Primary team', + url: 'http://server-1.com', + order: 0, + }, { + name: 'Secondary team', + url: 'http://server-2.com', + order: 1, + }], + showTrayIcon: true, + trayIconTheme: 'dark', + minimizeToTray: true, + notifications: { + flashWindow: 2, + bounceIcon: true, + bounceIconType: 'informational', + }, + showUnreadBadge: false, + useSpellChecker: false, + enableHardwareAcceleration: false, + autostart: false, + spellCheckerLocale: 'en-CA', + darkMode: true, + downloadLocation: '/some/folder/name', + }; + expect(upgradeV2toV3(config)).toStrictEqual({ + ...pastDefaultPreferences[3], + ...config, + version: 3, + teams: [{ + name: 'Primary team', + url: 'http://server-1.com', + order: 0, + tabs: [ + { + name: 'tab1', + }, + { + name: 'tab2', + }, + ], + lastActiveTab: 0, + }, { + name: 'Secondary team', + url: 'http://server-2.com', + order: 1, + tabs: [ + { + name: 'tab1', + }, + { + name: 'tab2', + }, + ], + lastActiveTab: 0, + }], + }); + }); + }); +}); diff --git a/src/common/config/upgradePreferences.ts b/src/common/config/upgradePreferences.ts index b94737e4..6ad1cd00 100644 --- a/src/common/config/upgradePreferences.ts +++ b/src/common/config/upgradePreferences.ts @@ -12,7 +12,7 @@ function deepCopy(object: T): T { return JSON.parse(JSON.stringify(object)); } -function upgradeV0toV1(configV0: ConfigV0) { +export function upgradeV0toV1(configV0: ConfigV0) { const config = deepCopy(pastDefaultPreferences[1]); config.teams.push({ name: 'Primary team', @@ -21,7 +21,7 @@ function upgradeV0toV1(configV0: ConfigV0) { return config; } -function upgradeV1toV2(configV1: ConfigV1) { +export function upgradeV1toV2(configV1: ConfigV1) { const config: ConfigV2 = Object.assign({}, deepCopy(pastDefaultPreferences[2]), configV1); config.version = 2; config.teams = configV1.teams.map((value, index) => { @@ -33,7 +33,7 @@ function upgradeV1toV2(configV1: ConfigV1) { return config; } -function upgradeV2toV3(configV2: ConfigV2) { +export function upgradeV2toV3(configV2: ConfigV2) { const config: ConfigV3 = Object.assign({}, deepCopy(pastDefaultPreferences[3]), configV2); config.version = 3; config.teams = configV2.teams.map((value) => { diff --git a/src/common/tabs/TabView.test.js b/src/common/tabs/TabView.test.js new file mode 100644 index 00000000..da2d9341 --- /dev/null +++ b/src/common/tabs/TabView.test.js @@ -0,0 +1,35 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {MattermostServer} from 'common/servers/MattermostServer'; +import * as TabView from 'common/tabs/TabView'; + +describe('common/tabs/TabView', () => { + describe('getServerView', () => { + it('should return correct URL on messaging tab', () => { + const server = new MattermostServer('server-1', 'http://server-1.com'); + const tab = {name: TabView.TAB_MESSAGING}; + expect(TabView.getServerView(server, tab).url).toBe(server.url); + }); + + it('should return correct URL on playbooks tab', () => { + const server = new MattermostServer('server-1', 'http://server-1.com'); + const tab = {name: TabView.TAB_PLAYBOOKS}; + expect(TabView.getServerView(server, tab).url.toString()).toBe(`${server.url}playbooks`); + }); + + it('should return correct URL on boards tab', () => { + const server = new MattermostServer('server-1', 'http://server-1.com'); + const tab = {name: TabView.TAB_FOCALBOARD}; + expect(TabView.getServerView(server, tab).url.toString()).toBe(`${server.url}boards`); + }); + + it('should throw error on bad tab name', () => { + const server = new MattermostServer('server-1', 'http://server-1.com'); + const tab = {name: 'not a real tab name'}; + expect(() => { + TabView.getServerView(server, tab); + }).toThrow(Error); + }); + }); +});