diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 000000000..f412fe027 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,19 @@ +const config = { + "preset": "ts-jest/presets/js-with-ts", + "setupFiles": [ + "/src/__helpers__/setupEnvVars.js" + ], + "testEnvironment": "jsdom", + "coverageThreshold": { + "global": { + "lines": 90 + } + }, + "moduleNameMapper": { + // Force CommonJS build for http adapter to be available. + // via https://github.com/axios/axios/issues/5101#issuecomment-1276572468 + '^axios$': require.resolve('axios'), + }, +} + +module.exports = config; diff --git a/main.js b/main.js index 9f03f89a1..8de0d1902 100644 --- a/main.js +++ b/main.js @@ -4,6 +4,8 @@ const { autoUpdater } = require('electron-updater'); const { onFirstRunMaybe } = require('./first-run'); const path = require('path'); +require('@electron/remote/main').initialize() + app.setAppUserModelId('com.electron.gitify'); const iconIdle = path.join( @@ -75,6 +77,13 @@ menubarApp.on('ready', () => { } } }); + ipcMain.handle('get-platform', async () => { + return process.platform; + }); + + ipcMain.handle('get-app-version', async () => { + return app.getVersion(); + }); menubarApp.window.webContents.on('devtools-opened', () => { menubarApp.window.setSize(800, 600); diff --git a/package.json b/package.json index 014e9f1a8..a1dce386b 100644 --- a/package.json +++ b/package.json @@ -48,18 +48,6 @@ "url": "https://github.com/manosim/gitify/issues" }, "homepage": "https://www.gitify.io/", - "jest": { - "preset": "ts-jest/presets/js-with-ts", - "setupFiles": [ - "/src/__helpers__/setupEnvVars.js" - ], - "testEnvironment": "jsdom", - "coverageThreshold": { - "global": { - "lines": 90 - } - } - }, "build": { "appId": "com.electron.gitify", "productName": "Gitify", @@ -105,8 +93,9 @@ "afterSign": "scripts/notarize.js" }, "dependencies": { + "@electron/remote": "^2.0.11", "@primer/octicons-react": "19.8.0", - "axios": "0.27.2", + "axios": "1.5.1", "date-fns": "2.30.0", "electron-updater": "6.1.4", "final-form": "4.20.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7e3362eda..3b83d9549 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,12 +5,15 @@ settings: excludeLinksFromLockfile: false dependencies: + '@electron/remote': + specifier: ^2.0.11 + version: 2.0.11(electron@13.1.7) '@primer/octicons-react': specifier: 19.8.0 version: 19.8.0(react@18.2.0) axios: - specifier: 0.27.2 - version: 0.27.2 + specifier: 1.5.1 + version: 1.5.1 date-fns: specifier: 2.30.0 version: 2.30.0 @@ -566,6 +569,14 @@ packages: - supports-color dev: true + /@electron/remote@2.0.11(electron@13.1.7): + resolution: {integrity: sha512-PYEs7W3GrQNuhgiMHjFEvL5MbAL6C7m1AwSAHGqC+xc33IdP7rcGtJSdTP2eg1ssyB3oI00KwTsiSlsQbAoXpA==} + peerDependencies: + electron: '>= 13.0.0' + dependencies: + electron: 13.1.7 + dev: false + /@electron/universal@1.4.1: resolution: {integrity: sha512-lE/U3UNw1YHuowNbTmKNs9UlS3En3cPgwM5MI+agIgr/B1hSze9NdOP0qn7boZaI9Lph8IDv3/24g9IxnJP7aQ==} engines: {node: '>=8.6'} @@ -1541,11 +1552,12 @@ packages: resolution: {integrity: sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==} dev: true - /axios@0.27.2: - resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==} + /axios@1.5.1: + resolution: {integrity: sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==} dependencies: follow-redirects: 1.15.2 form-data: 4.0.0 + proxy-from-env: 1.1.0 transitivePeerDependencies: - debug dev: false @@ -4649,6 +4661,10 @@ packages: requiresBuild: true optional: true + /proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + dev: false + /psl@1.9.0: resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} dev: true diff --git a/src/__mocks__/@electron/remote.js b/src/__mocks__/@electron/remote.js new file mode 100644 index 000000000..5f7c61648 --- /dev/null +++ b/src/__mocks__/@electron/remote.js @@ -0,0 +1,38 @@ +let instance; + +class BrowserWindow { + constructor() { + if (!instance) { + instance = this; + } + return instance; + } + loadURL = jest.fn(); + webContents = { + on: () => {}, + session: { + clearStorageData: jest.fn(), + }, + }; + on() {} + close = jest.fn(); + hide = jest.fn(); + destroy = jest.fn(); +} + +const dialog = { + showErrorBox: jest.fn(), +}; + +module.exports = { + BrowserWindow: BrowserWindow, + dialog: dialog, + process: { + platform: 'darwin', + }, + app: { + getLoginItemSettings: jest.fn(), + setLoginItemSettings: () => {}, + }, + getCurrentWindow: jest.fn(() => instance || new BrowserWindow()), +}; diff --git a/src/__mocks__/electron.js b/src/__mocks__/electron.js index 83fd9ad4c..25a226c17 100644 --- a/src/__mocks__/electron.js +++ b/src/__mocks__/electron.js @@ -27,49 +27,21 @@ window.localStorage = { window.alert = jest.fn(); -let instance; - -class BrowserWindow { - constructor() { - if (!instance) { - instance = this; - } - return instance; - } - loadURL = jest.fn(); - webContents = { - on: () => {}, - session: { - clearStorageData: jest.fn(), - }, - }; - on() {} - close = jest.fn(); - hide = jest.fn(); - destroy = jest.fn(); -} - -const dialog = { - showErrorBox: jest.fn(), -}; - module.exports = { - remote: { - BrowserWindow: BrowserWindow, - dialog: dialog, - process: { - platform: 'darwin', - }, - app: { - getVersion: () => '0.0.1', - getLoginItemSettings: jest.fn(), - setLoginItemSettings: () => {}, - }, - getCurrentWindow: jest.fn(() => instance || new BrowserWindow()), - }, ipcRenderer: { send: jest.fn(), on: jest.fn(), + sendSync: jest.fn(), + invoke: jest.fn((channel, ...args) => { + switch (channel) { + case 'get-platform': + return Promise.resolve('darwin'); + case 'get-app-version': + return Promise.resolve('0.0.1'); + default: + return Promise.reject(new Error(`Unknown channel: ${channel}`)); + } + }), }, shell: { openExternal: jest.fn(), diff --git a/src/hooks/useNotifications.test.ts b/src/hooks/useNotifications.test.ts index b9ab59296..dce5e9e18 100644 --- a/src/hooks/useNotifications.test.ts +++ b/src/hooks/useNotifications.test.ts @@ -9,7 +9,9 @@ import { mockedUser } from '../__mocks__/mockedData'; describe('hooks/useNotifications.ts', () => { beforeEach(() => { - axios.defaults.adapter = require('axios/lib/adapters/http'); + // axios will default to using the XHR adapter which can't be intercepted + // by nock. So, configure axios to use the node adapter. + axios.defaults.adapter = 'http'; }); describe('fetchNotifications', () => { diff --git a/src/routes/Settings.test.tsx b/src/routes/Settings.test.tsx index 185b7ac29..a5aedeac6 100644 --- a/src/routes/Settings.test.tsx +++ b/src/routes/Settings.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import TestRenderer from 'react-test-renderer'; +import TestRenderer, { act } from 'react-test-renderer'; import { render, fireEvent } from '@testing-library/react'; import { Router } from 'react-router'; import { MemoryRouter } from 'react-router-dom'; @@ -26,29 +26,38 @@ describe('routes/Settings.tsx', () => { updateSetting.mockReset(); }); - it('should render itself & its children', () => { - const tree = TestRenderer.create( - - - - - , - ); + it('should render itself & its children', async () => { + let tree: TestRenderer; + + await act(async () => { + tree = TestRenderer.create( + + + + + , + ); + }); expect(tree).toMatchSnapshot(); }); - it('should press the logout', () => { + it('should press the logout', async () => { const logoutMock = jest.fn(); - - const { getByLabelText } = render( - - - - - , - ); + let getByLabelText; + + await act(async () => { + const { getByLabelText: getByLabelTextLocal } = render( + + + + + , + ); + + getByLabelText = getByLabelTextLocal; + }); fireEvent.click(getByLabelText('Logout')); @@ -59,26 +68,37 @@ describe('routes/Settings.tsx', () => { expect(mockNavigate).toHaveBeenNthCalledWith(1, -1); }); - it('should go back by pressing the icon', () => { - const { getByLabelText } = render( - - - - - , - ); + it('should go back by pressing the icon', async () => { + let getByLabelText; + + await act(async () => { + const { getByLabelText: getByLabelTextLocal } = render( + + + + + , + ); + + getByLabelText = getByLabelTextLocal; + }); fireEvent.click(getByLabelText('Go Back')); expect(mockNavigate).toHaveBeenNthCalledWith(1, -1); }); - it('should toggle the showOnlyParticipating checkbox', () => { - const { getByLabelText } = render( - - - - - , - ); + it('should toggle the showOnlyParticipating checkbox', async () => { + let getByLabelText; + + await act(async () => { + const { getByLabelText: getByLabelTextLocal } = render( + + + + + , + ); + getByLabelText = getByLabelTextLocal; + }); fireEvent.click(getByLabelText('Show only participating'), { target: { checked: true }, @@ -88,14 +108,19 @@ describe('routes/Settings.tsx', () => { expect(updateSetting).toHaveBeenCalledWith('participating', false); }); - it('should toggle the playSound checkbox', () => { - const { getByLabelText } = render( - - - - - , - ); + it('should toggle the playSound checkbox', async () => { + let getByLabelText; + + await act(async () => { + const { getByLabelText: getByLabelTextLocal } = render( + + + + + , + ); + getByLabelText = getByLabelTextLocal; + }); fireEvent.click(getByLabelText('Play sound'), { target: { checked: true }, @@ -105,14 +130,19 @@ describe('routes/Settings.tsx', () => { expect(updateSetting).toHaveBeenCalledWith('playSound', false); }); - it('should toggle the showNotifications checkbox', () => { - const { getByLabelText } = render( - - - - - , - ); + it('should toggle the showNotifications checkbox', async () => { + let getByLabelText; + + await act(async () => { + const { getByLabelText: getByLabelTextLocal } = render( + + + + + , + ); + getByLabelText = getByLabelTextLocal; + }); fireEvent.click(getByLabelText('Show notifications'), { target: { checked: true }, @@ -122,14 +152,19 @@ describe('routes/Settings.tsx', () => { expect(updateSetting).toHaveBeenCalledWith('showNotifications', false); }); - it('should toggle the onClickMarkAsRead checkbox', () => { - const { getByLabelText } = render( - - - - - , - ); + it('should toggle the onClickMarkAsRead checkbox', async () => { + let getByLabelText; + + await act(async () => { + const { getByLabelText: getByLabelTextLocal } = render( + + + + + , + ); + getByLabelText = getByLabelTextLocal; + }); fireEvent.click(getByLabelText('Mark as read on click'), { target: { checked: true }, @@ -139,14 +174,19 @@ describe('routes/Settings.tsx', () => { expect(updateSetting).toHaveBeenCalledWith('markOnClick', false); }); - it('should toggle the openAtStartup checkbox', () => { - const { getByLabelText } = render( - - - - - , - ); + it('should toggle the openAtStartup checkbox', async () => { + let getByLabelText; + + await act(async () => { + const { getByLabelText: getByLabelTextLocal } = render( + + + + + , + ); + getByLabelText = getByLabelTextLocal; + }); fireEvent.click(getByLabelText('Open at startup'), { target: { checked: true }, @@ -156,14 +196,19 @@ describe('routes/Settings.tsx', () => { expect(updateSetting).toHaveBeenCalledWith('openAtStartup', false); }); - it('should change the appearance radio group', () => { - const { getByLabelText } = render( - - - - - , - ); + it('should change the appearance radio group', async () => { + let getByLabelText; + + await act(async () => { + const { getByLabelText: getByLabelTextLocal } = render( + + + + + , + ); + getByLabelText = getByLabelTextLocal; + }); fireEvent.click(getByLabelText('Light')); @@ -171,28 +216,40 @@ describe('routes/Settings.tsx', () => { expect(updateSetting).toHaveBeenCalledWith('appearance', 'LIGHT'); }); - it('should go to the enterprise login route', () => { - const { getByLabelText } = render( - - - - - , - ); + it('should go to the enterprise login route', async () => { + let getByLabelText; + + await act(async () => { + const { getByLabelText: getByLabelTextLocal } = render( + + + + + , + ); + getByLabelText = getByLabelTextLocal; + }); + fireEvent.click(getByLabelText('Login with GitHub Enterprise')); expect(mockNavigate).toHaveBeenNthCalledWith(1, '/login-enterprise', { replace: true, }); }); - it('should quit the app', () => { - const { getByLabelText } = render( - - - - - , - ); + it('should quit the app', async () => { + let getByLabelText; + + await act(async () => { + const { getByLabelText: getByLabelTextLocal } = render( + + + + + , + ); + getByLabelText = getByLabelTextLocal; + }); + fireEvent.click(getByLabelText('Quit Gitify')); expect(ipcRenderer.send).toHaveBeenCalledWith('app-quit'); }); diff --git a/src/routes/Settings.tsx b/src/routes/Settings.tsx index a7b929fb6..f04da658d 100644 --- a/src/routes/Settings.tsx +++ b/src/routes/Settings.tsx @@ -1,5 +1,5 @@ -import React, { useCallback, useContext } from 'react'; -import { ipcRenderer, remote } from 'electron'; +import React, { useCallback, useContext, useState, useEffect } from 'react'; +import { ipcRenderer } from 'electron'; import { useNavigate } from 'react-router-dom'; import { ArrowLeftIcon } from '@primer/octicons-react'; @@ -13,12 +13,23 @@ import { IconQuit } from '../icons/Quit'; import { updateTrayIcon } from '../utils/comms'; import { setAppearance } from '../utils/appearance'; -const isLinux = remote.process.platform === 'linux'; - export const SettingsRoute: React.FC = () => { const { settings, updateSetting, logout } = useContext(AppContext); const navigate = useNavigate(); + const [isLinux, setIsLinux] = useState(false); + const [appVersion, setAppVersion] = useState(null); + + useEffect(() => { + ipcRenderer.invoke('get-platform').then((result: string) => { + setIsLinux(result === 'linux'); + }); + + ipcRenderer.invoke('get-app-version').then((result: string) => { + setAppVersion(result); + }); + }, []); + ipcRenderer.on('update-native-theme', (_, updatedAppearance: Appearance) => { if (settings.appearance === Appearance.SYSTEM) { setAppearance(updatedAppearance); @@ -116,9 +127,7 @@ export const SettingsRoute: React.FC = () => {
- - Gitify v{remote.app.getVersion()} - + Gitify v{appVersion}