diff --git a/src/components/fields/Checkbox.tsx b/src/components/fields/Checkbox.tsx index d167e5ee4..4f4bc6583 100644 --- a/src/components/fields/Checkbox.tsx +++ b/src/components/fields/Checkbox.tsx @@ -6,6 +6,7 @@ interface IFieldCheckbox { checked: boolean; onChange: any; placeholder?: string; + disabled?: boolean; } export const FieldCheckbox = (props: IFieldCheckbox) => { @@ -18,6 +19,7 @@ export const FieldCheckbox = (props: IFieldCheckbox) => { className="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded" checked={props.checked} onChange={props.onChange} + disabled={props.disabled} /> @@ -25,6 +27,9 @@ export const FieldCheckbox = (props: IFieldCheckbox) => { diff --git a/src/context/App.test.tsx b/src/context/App.test.tsx index 390fefc98..05e00520c 100644 --- a/src/context/App.test.tsx +++ b/src/context/App.test.tsx @@ -264,7 +264,7 @@ describe('context/App.tsx', () => { participating: true, playSound: true, showNotifications: true, - colors: true, + colors: false, }, ); }); @@ -302,7 +302,7 @@ describe('context/App.tsx', () => { participating: false, playSound: true, showNotifications: true, - colors: true, + colors: false, }, ); }); diff --git a/src/context/App.tsx b/src/context/App.tsx index fb94f545f..34543cf38 100644 --- a/src/context/App.tsx +++ b/src/context/App.tsx @@ -1,11 +1,13 @@ import React, { - useState, createContext, useCallback, useEffect, useMemo, + useState, } from 'react'; +import { useInterval } from '../hooks/useInterval'; +import { useNotifications } from '../hooks/useNotifications'; import { AccountNotifications, Appearance, @@ -15,14 +17,12 @@ import { SettingsState, } from '../types'; import { apiRequestAuth } from '../utils/api-requests'; -import { addAccount, authGitHub, getToken, getUserData } from '../utils/auth'; -import { clearState, loadState, saveState } from '../utils/storage'; import { setAppearance } from '../utils/appearance'; +import { addAccount, authGitHub, getToken, getUserData } from '../utils/auth'; import { setAutoLaunch } from '../utils/comms'; -import { useInterval } from '../hooks/useInterval'; -import { useNotifications } from '../hooks/useNotifications'; import Constants from '../utils/constants'; import { generateGitHubAPIUrl } from '../utils/helpers'; +import { clearState, loadState, saveState } from '../utils/storage'; const defaultAccounts: AuthState = { token: null, @@ -37,7 +37,7 @@ export const defaultSettings: SettingsState = { markOnClick: false, openAtStartup: false, appearance: Appearance.SYSTEM, - colors: true, + colors: false, }; interface AppContextState { diff --git a/src/routes/Settings.test.tsx b/src/routes/Settings.test.tsx index d8565a1fc..daa7d38e6 100644 --- a/src/routes/Settings.test.tsx +++ b/src/routes/Settings.test.tsx @@ -1,26 +1,33 @@ +import { fireEvent, render, screen } from '@testing-library/react'; import React from 'react'; -import TestRenderer, { act } from 'react-test-renderer'; -import { render, fireEvent } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; +import TestRenderer, { act } from 'react-test-renderer'; const { ipcRenderer } = require('electron'); -import { SettingsRoute } from './Settings'; +import { AxiosResponse } from 'axios'; +import { mockAccounts, mockSettings } from '../__mocks__/mock-state'; import { AppContext } from '../context/App'; -import { mockSettings } from '../__mocks__/mock-state'; +import * as apiRequests from '../utils/api-requests'; +import Constants from '../utils/constants'; +import { SettingsRoute } from './Settings'; const mockNavigate = jest.fn(); jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useNavigate: () => mockNavigate, })); +jest.spyOn(apiRequests, 'apiRequestAuth').mockResolvedValue({ + headers: { + 'x-oauth-scopes': Constants.AUTH_SCOPE.join(', '), + }, +} as unknown as AxiosResponse); describe('routes/Settings.tsx', () => { const updateSetting = jest.fn(); beforeEach(() => { - mockNavigate.mockReset(); - updateSetting.mockReset(); + jest.clearAllMocks(); }); it('should render itself & its children', async () => { @@ -28,7 +35,9 @@ describe('routes/Settings.tsx', () => { await act(async () => { tree = TestRenderer.create( - + @@ -45,7 +54,11 @@ describe('routes/Settings.tsx', () => { await act(async () => { const { getByLabelText: getByLabelTextLocal } = render( @@ -70,7 +83,12 @@ describe('routes/Settings.tsx', () => { await act(async () => { const { getByLabelText: getByLabelTextLocal } = render( - + @@ -88,7 +106,13 @@ describe('routes/Settings.tsx', () => { await act(async () => { const { getByLabelText: getByLabelTextLocal } = render( - + @@ -110,7 +134,13 @@ describe('routes/Settings.tsx', () => { await act(async () => { const { getByLabelText: getByLabelTextLocal } = render( - + @@ -132,7 +162,13 @@ describe('routes/Settings.tsx', () => { await act(async () => { const { getByLabelText: getByLabelTextLocal } = render( - + @@ -154,7 +190,13 @@ describe('routes/Settings.tsx', () => { await act(async () => { const { getByLabelText: getByLabelTextLocal } = render( - + @@ -176,7 +218,13 @@ describe('routes/Settings.tsx', () => { await act(async () => { const { getByLabelText: getByLabelTextLocal } = render( - + @@ -198,7 +246,13 @@ describe('routes/Settings.tsx', () => { await act(async () => { const { getByLabelText: getByLabelTextLocal } = render( - + @@ -218,7 +272,12 @@ describe('routes/Settings.tsx', () => { await act(async () => { const { getByLabelText: getByLabelTextLocal } = render( - + @@ -238,7 +297,9 @@ describe('routes/Settings.tsx', () => { await act(async () => { const { getByLabelText: getByLabelTextLocal } = render( - + @@ -250,4 +311,86 @@ describe('routes/Settings.tsx', () => { fireEvent.click(getByLabelText('Quit Gitify')); expect(ipcRenderer.send).toHaveBeenCalledWith('app-quit'); }); + + it('should be able to enable colors', async () => { + jest.spyOn(apiRequests, 'apiRequestAuth').mockResolvedValue({ + headers: { + 'x-oauth-scopes': Constants.AUTH_SCOPE.join(', '), + }, + } as unknown as AxiosResponse); + + await act(async () => { + render( + + + + + , + ); + }); + + await screen.findByLabelText('Use GitHub-like state colors'); + + fireEvent.click(screen.getByLabelText('Use GitHub-like state colors')); + + expect(updateSetting).toHaveBeenCalledTimes(1); + expect(updateSetting).toHaveBeenCalledWith('colors', true); + }); + + it('should not be able to enable colors due to missing scope', async () => { + jest.spyOn(apiRequests, 'apiRequestAuth').mockResolvedValue({ + headers: { + 'x-oauth-scopes': 'read:user, notifications', + }, + } as unknown as AxiosResponse); + + await act(async () => { + render( + + + + + , + ); + }); + + expect( + screen + .getByLabelText('Use GitHub-like state colors (requires repo scope)') + .closest('input'), + ).toHaveProperty('disabled', true); + + // click the checkbox + fireEvent.click( + screen.getByLabelText( + 'Use GitHub-like state colors (requires repo scope)', + ), + ); + + // check if the checkbox is still unchecked + expect(updateSetting).not.toHaveBeenCalled(); + expect( + screen.getByLabelText( + 'Use GitHub-like state colors (requires repo scope)', + ), + ).not.toBe('checked'); + + expect( + screen.getByLabelText( + 'Use GitHub-like state colors (requires repo scope)', + ).parentNode.parentNode, + ).toMatchSnapshot(); + }); }); diff --git a/src/routes/Settings.tsx b/src/routes/Settings.tsx index f04da658d..b43551699 100644 --- a/src/routes/Settings.tsx +++ b/src/routes/Settings.tsx @@ -1,24 +1,34 @@ -import React, { useCallback, useContext, useState, useEffect } from 'react'; +import { ArrowLeftIcon } from '@primer/octicons-react'; import { ipcRenderer } from 'electron'; +import React, { + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from 'react'; import { useNavigate } from 'react-router-dom'; -import { ArrowLeftIcon } from '@primer/octicons-react'; -import { AppContext } from '../context/App'; -import { Appearance } from '../types'; import { FieldCheckbox } from '../components/fields/Checkbox'; import { FieldRadioGroup } from '../components/fields/RadioGroup'; +import { AppContext } from '../context/App'; import { IconAddAccount } from '../icons/AddAccount'; import { IconLogOut } from '../icons/Logout'; import { IconQuit } from '../icons/Quit'; -import { updateTrayIcon } from '../utils/comms'; +import { Appearance } from '../types'; +import { apiRequestAuth } from '../utils/api-requests'; import { setAppearance } from '../utils/appearance'; +import { updateTrayIcon } from '../utils/comms'; +import Constants from '../utils/constants'; +import { generateGitHubAPIUrl } from '../utils/helpers'; export const SettingsRoute: React.FC = () => { - const { settings, updateSetting, logout } = useContext(AppContext); + const { accounts, settings, updateSetting, logout } = useContext(AppContext); const navigate = useNavigate(); const [isLinux, setIsLinux] = useState(false); const [appVersion, setAppVersion] = useState(null); + const [colorScope, setColorScope] = useState(false); useEffect(() => { ipcRenderer.invoke('get-platform').then((result: string) => { @@ -30,6 +40,19 @@ export const SettingsRoute: React.FC = () => { }); }, []); + useMemo(() => { + (async () => { + const response = await apiRequestAuth( + `${generateGitHubAPIUrl(Constants.DEFAULT_AUTH_OPTIONS.hostname)}`, + 'GET', + accounts.token, + ); + + if (response.headers['x-oauth-scopes'].includes('repo')) + setColorScope(true); + })(); + }, [accounts.token]); + ipcRenderer.on('update-native-theme', (_, updatedAppearance: Appearance) => { if (settings.appearance === Appearance.SYSTEM) { setAppearance(updatedAppearance); @@ -84,9 +107,14 @@ export const SettingsRoute: React.FC = () => { updateSetting('colors', evt.target.checked)} + label={`Use GitHub-like state colors${ + !colorScope ? ' (requires repo scope)' : '' + }`} + checked={colorScope && settings.colors} + onChange={(evt) => + colorScope && updateSetting('colors', evt.target.checked) + } + disabled={!colorScope} /> +
+ +
+
+ +
+ +`; + exports[`routes/Settings.tsx should render itself & its children 1`] = `
- Use colors to indicate state + Use GitHub-like state colors