Skip to content

Commit

Permalink
frontend tests for src/components (Heroic-Games-Launcher#396)
Browse files Browse the repository at this point in the history
* Added first test for src/components

* Fixed Header and added NavBar test

- removed args from Header which are served through ContextProvider

* Fix: missing new endline end of file
  • Loading branch information
Nocccer authored May 31, 2021
1 parent 42b4955 commit 61f63e4
Show file tree
Hide file tree
Showing 28 changed files with 2,230 additions and 151 deletions.
22 changes: 22 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Test

on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:

jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout repository.
uses: actions/checkout@v2
- name: Install modules.
run: yarn
- name: Test and collect coverage.
uses: artiomtr/[email protected]
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
test_script: yarn test --coverage
50 changes: 50 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
module.exports = {
// The root of your source code, typically /src
// `<rootDir>` is a token Jest substitutes
roots: ["<rootDir>"],

moduleDirectories: [
"node_modules",
"<rootDir>"
],

// Jest transformations -- this adds support for TypeScript
// using ts-jest
transform: {
"^.+\\.tsx?$": "ts-jest"
},

moduleNameMapper: {
"\\.css$": "<rootDir>/src/test_helpers/mock/css.ts",
"electron": "<rootDir>/src/test_helpers/mock/electron.ts"
},

resetMocks: true,

setupFilesAfterEnv: ["<rootDir>/src/test_helpers/setupTests.ts"],

// Test spec file resolution pattern
// should contain `test` or `spec`.
testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$",

// Module file extensions for importing
moduleFileExtensions: ["ts", "tsx", "js", "jsx"],

coverageDirectory: "<rootDir>/coverage",

collectCoverageFrom: [
"**/*.{js,jsx,ts,tsx}",
"!**/*.config.js"
],

coveragePathIgnorePatterns: [
"<rootDir>/node_modules>",
"<rootDir>/public",
"<rootDir>/electron",
"<rootDir>/dist",
"<rootDir>/build",
"<rootDir>/coverage"
],

coverageReporters: ['text', 'html']
};
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@
"electron": "yarn build-electron && electron .",
"react-start": "BROWSER=none react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"test": "jest",
"eject": "react-scripts eject",
"ci-build": "GH_TOKEN='${{ secrets.WORKFLOW_TOKEN }}' npm run build-electron && npm run build && electron-builder -c.extraMetadata.main=build/main.js --linux deb AppImage rpm pacman",
"dist": "yarn build-electron && yarn build && electron-builder -c.extraMetadata.main=build/main.js --linux",
Expand Down Expand Up @@ -133,6 +133,8 @@
"foreman": "^3.0.1",
"husky": "^4.3.8",
"i18next-parser": "^3.6.0",
"npm": "^7.14.0",
"ts-jest": "^26.5.6",
"typescript": "^4.1.3"
},
"husky": {
Expand Down
13 changes: 7 additions & 6 deletions src/App.test.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import React from 'react';

import {
render,
screen
render
} from '@testing-library/react';

import App from './App';

test('renders learn react link', () => {
render(<App />)
const linkElement = screen.getByText(/learn react/i)
expect(linkElement).toBeInTheDocument()
describe('App', () => {

test('renders', () => {
render(<React.Suspense fallback="App loaded"><App /></React.Suspense>);
})

})
5 changes: 1 addition & 4 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const Login = lazy(() => import('./screens/Login'))
function App() {
const context = useContext(ContextProvider)

const { user, data: library, refresh, handleFilter, handleLayout, handleCategory } = context
const { user, data: library, refresh } = context

if (!user && !library.length) {
return <Login refresh={refresh} />
Expand All @@ -32,10 +32,7 @@ function App() {
<Header
goTo={''}
renderBackButton={false}
handleFilter={handleFilter}
numberOfGames={numberOfGames}
handleLayout={handleLayout}
handleCategory={handleCategory}
/>
<div id="top"></div>
<Library library={library} />
Expand Down
81 changes: 81 additions & 0 deletions src/components/Navbar/components/SearchBar/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React from 'react';

import {
fireEvent,
render
} from '@testing-library/react'

import { ContextType } from 'src/types';
import ContextProvider from 'src/state/ContextProvider';
import SearchBar from './index';

jest.mock('react-i18next', () => ({
// this mock makes sure any components using the translate hook can use it without a warning being shown
useTranslation: () => {
return {
i18n: {
changeLanguage: () => new Promise(() => { return; })
},
t: (str: string) => str
};
}
}));

function renderSearchBar(props: Partial<ContextType> = {}) {
const defaultProps: ContextType = {
category: 'games',
data: [],
error: false,
filter: 'all',
gameUpdates: [],
handleCategory: () => null,
handleFilter: () => null,
handleGameStatus: () => Promise.resolve(),
handleLayout: () => null,
handleSearch: () => null,
layout: 'grid',
libraryStatus: [],
refresh: () => Promise.resolve(),
refreshLibrary: () => Promise.resolve(),
refreshing: false,
user: 'user'
};

return render(
<ContextProvider.Provider value={{ ...defaultProps, ...props }}>
<SearchBar />
</ContextProvider.Provider>);
}

describe('SearchBar', () => {
test('renders', () => {
renderSearchBar();
})

test('set text in input field and calls handle search', () => {
const onHandleSearch = jest.fn();
const { getByTestId } = renderSearchBar({ handleSearch: onHandleSearch});
const searchInput = getByTestId('searchInput');
fireEvent.change(searchInput, {target: { value: 'Test Search'}});
expect(searchInput).toHaveValue('Test Search');
expect(onHandleSearch).toBeCalledWith('Test Search');
})

test('calls handle search by clicking on search', () => {
const onHandleSearch = jest.fn();
const { getByTestId } = renderSearchBar({ handleSearch: onHandleSearch});
const searchButton = getByTestId('searchButton');
fireEvent.click(searchButton);
expect(onHandleSearch).toBeCalledWith('');
})

test('calls handle search with empty string by clicking on cancel', () => {
const onHandleSearch = jest.fn();
const { getByTestId } = renderSearchBar({ handleSearch: onHandleSearch});
const searchInput = getByTestId('searchInput');
fireEvent.change(searchInput, {target: { value: 'Test Search'}});
const closeButton = getByTestId('closeButton');
fireEvent.click(closeButton);
expect(onHandleSearch).toBeCalledWith('');
})
})
5 changes: 4 additions & 1 deletion src/components/Navbar/components/SearchBar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ export default function SearchBar() {
const { t } = useTranslation()

return (
<div className="SearchBar">
<div className="SearchBar" data-testid="searchBar">
<label htmlFor="search">
<Search
onClick={() => handleSearch(textValue)}
className="material-icons"
data-testid="searchButton"
/>
</label>
<input
data-testid="searchInput"
className="searchInput"
value={textValue}
onChange={(event) => {
Expand All @@ -38,6 +40,7 @@ export default function SearchBar() {
handleSearch('')
}}
className="material-icons close"
data-testid="closeButton"
/>
)}
</div>
Expand Down
124 changes: 124 additions & 0 deletions src/components/Navbar/components/UserSelector/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import React from 'react';

import {
fireEvent,
render
} from '@testing-library/react';

import {ContextType} from 'src/types';
import { ipcRenderer } from 'electron';
import ContextProvider from 'src/state/ContextProvider';
import UserSelector from './index';

jest.mock('react-i18next', () => ({
// this mock makes sure any components using the translate hook can use it without a warning being shown
useTranslation: () => {
return {
i18n: {
changeLanguage: () => new Promise(() => { return; })
},
t: (str: string) => str
};
}
}));

function renderUserSelector(props: Partial<ContextType> = {}) {
const defaultProps: ContextType = {
category: 'games',
data: [],
error: false,
filter: 'all',
gameUpdates: [],
handleCategory: () => null,
handleFilter: () => null,
handleGameStatus: () => Promise.resolve(),
handleLayout: () => null,
handleSearch: () => null,
layout: 'grid',
libraryStatus: [],
refresh: () => Promise.resolve(),
refreshLibrary: () => Promise.resolve(),
refreshing: false,
user: 'user'
};

return render(
<ContextProvider.Provider value={{ ...defaultProps, ...props }}>
<UserSelector />
</ContextProvider.Provider>);
}

describe('UserSelector', () => {

test('render', () => {
renderUserSelector();
})

test('shows correct username', () => {
const { getByTestId} = renderUserSelector( {user: 'test-user'});
const userName = getByTestId('userName');
expect(userName).toHaveTextContent('test-user');
})

test('calls refresh library on click', () => {
const onRefreshLibrary = jest.fn();
const { getByTestId } = renderUserSelector({ refreshLibrary: onRefreshLibrary});
const divLibrary = getByTestId('refreshLibrary');
expect(onRefreshLibrary).not.toBeCalled();
fireEvent.click(divLibrary);
expect(onRefreshLibrary).toBeCalledTimes(1);
})

test('calls handle kofi on click', () => {
const { getByTestId } = renderUserSelector();
const divKofi = getByTestId('handleKofi');
expect(ipcRenderer.send).not.toBeCalled();
fireEvent.click(divKofi);
expect(ipcRenderer.send).toBeCalledWith('openSupportPage');
})

test('calls open discord link on click', () => {
const { getByTestId } = renderUserSelector();
const divDiscordLink = getByTestId('openDiscordLink');
expect(ipcRenderer.send).not.toBeCalled();
fireEvent.click(divDiscordLink);
expect(ipcRenderer.send).toBeCalledWith('openDiscordLink');
})

test('calls open about window on click', () => {
const { getByTestId } = renderUserSelector();
const divAboutWindow = getByTestId('openAboutWindow');
expect(ipcRenderer.send).not.toBeCalled();
fireEvent.click(divAboutWindow);
expect(ipcRenderer.send).toBeCalledWith('showAboutWindow');
})

test('calls handle logout on click and invoke ipc renderer if user confirm', () => {
window.confirm = jest.fn().mockImplementation(() => true)

const { getByTestId } = renderUserSelector();
const divLogout = getByTestId('handleLogout');
expect(ipcRenderer.invoke).not.toBeCalled();
fireEvent.click(divLogout);
expect(ipcRenderer.invoke).toBeCalledTimes(1);
expect(ipcRenderer.invoke).toBeCalledWith('logout');
})

test('calls handle logout on click and doesn not invoke ipc renderer if user does not confirm', () => {
window.confirm = jest.fn().mockImplementation(() => false)

const { getByTestId } = renderUserSelector();
const divLogout = getByTestId('handleLogout');
expect(ipcRenderer.invoke).not.toBeCalled();
fireEvent.click(divLogout);
expect(ipcRenderer.invoke).not.toBeCalled();
})

test('calls handle quit on click', () => {
const { getByTestId } = renderUserSelector();
const divQuit = getByTestId('handleQuit');
expect(ipcRenderer.send).not.toBeCalled();
fireEvent.click(divQuit);
expect(ipcRenderer.send).toBeCalledWith('quit');
})
})
Loading

0 comments on commit 61f63e4

Please sign in to comment.