Skip to content

Commit

Permalink
Frontend Tests, hooks and utils (#437)
Browse files Browse the repository at this point in the history
* install vitest

* install jsdom

* add test config object

* add tests folder

* errors - updated to use boilerplate vitest code

* wrote test to test vitest functionality. is this recursive testing?

* add testTimeout to test config

* rename tests folder to __tests__

* remove extra 'tests' folder

* basic tests for utils/errors.js

* create 'util' folder

* add hasValidationsErrors test

* use vitest.spy for getErrorList - still fails test

* attempt to render App inside a div container

* add testing-library/react-hooks

* add tests for useKeyPress

* added testing-library/react-hooks

* fixed getErrorList 'no error passed' test

* use vi.fn() to mock functions, passing all current tests

* add basic vitest skeleton, not functional

* hook sets state with initial window size

* reword assertion

* render hook as result object - not functional yet

* put render into act() for app.test - not functional

* Fix App test

* simulate clicks inside & outside element

* remove comments

* import render, waitFor from react testing lib

* test mock userAgents

* add test that more closely mocks implementation

* reword assertion

* extract isChrome to function, test Mozilla userAgent, vendor

* add chrome param to setWindowProps

* add iPhone safari test, rm userAgent only tests

* update userAgent strings with MDN examples

* update Chrome userAgent

* basic test outline - wip

* move setWindowProps into inner scope

* attempt to set multiple window properties

* WIP - initial take on rendering hook

* check portal.id

* add first successful debounce test

* add test where callback not called

* reword assertion

* remove test for useBrowserWarning

* wip - assertions for window properties only

* mock initial window size with vi.stubGlobal

* add resize event test

* action not triggered after hook unmounted

* action not triggered after hook unmounted

* remove redundant assertion

* advance timer, add assertion that checks if callack is called

* reword test description

* add _arg to vi.fn()

* check that callback is called with specific arg

* wip - using stubGlobal

* WIP - clean comments, test only Chrome, Mozilla

* Fix isChrome test

* npm run format

* move initial window sizing to beforeEach

* use outsideElement on hook unmount test

* remove act(), make final assertions more comprehensive

* remove redundant assertion

---------

Co-authored-by: Rob Gries <[email protected]>
  • Loading branch information
austin-bagwell and robert-w-gries authored Nov 5, 2023
1 parent 7097056 commit dae0d3c
Show file tree
Hide file tree
Showing 15 changed files with 2,540 additions and 394 deletions.
2,539 changes: 2,170 additions & 369 deletions package-lock.json

Large diffs are not rendered by default.

12 changes: 8 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,29 +29,33 @@
},
"devDependencies": {
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.0.0",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "^8.0.1",
"@types/file-saver": "^2.0.5",
"@vitejs/plugin-react": "^4.0.0",
"@vitejs/plugin-react": "^4.0.4",
"autoprefixer": "^10.4.2",
"eslint": "^8.16.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-jsx-a11y": "^6.6.1",
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.3.0",
"jsdom": "^22.1.0",
"postcss": "^8.4.6",
"postcss-import": "^15.1.0",
"prettier": "^2.7.0",
"tailwindcss": "^3.0.23",
"vite": "^4.3.5",
"vite-plugin-eslint": "^1.8.1"
"vite-plugin-eslint": "^1.8.1",
"vitest": "^0.34.4"
},
"scripts": {
"start": "vite",
"build": "vite build",
"serve": "vite preview",
"format": "prettier --write .",
"lint": "eslint . --ext .js,.jsx"
"lint": "eslint . --ext .js,.jsx",
"test": "vitest"
},
"browserslist": {
"production": [
Expand Down
7 changes: 0 additions & 7 deletions src/App.test.jsx

This file was deleted.

19 changes: 19 additions & 0 deletions src/__tests__/App.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import App from '../components/App';
import { render, waitFor } from '@testing-library/react';
import { test } from 'vitest';
import store from '../store';
import { Provider } from 'react-redux';

test('When the app starts it renders a log in button', async () => {
const container = document.createElement('div');
container.setAttribute('id', 'test-root');
document.body.appendChild(container);

const { getByText } = render(
<Provider store={store}>
<App />
</Provider>,
container,
);
await waitFor(() => expect(getByText('Log In')).toBeInTheDocument());
});
42 changes: 42 additions & 0 deletions src/__tests__/hooks/useDebounce.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { vi } from 'vitest';
import { renderHook } from '@testing-library/react-hooks';
import useDebounce from '../../hooks/useDebounce';

describe('useDebounce', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.restoreAllMocks();
});

it('should debounce the callback function', async () => {
const callback = vi.fn((_arg) => console.log());
const { result } = renderHook(() => useDebounce(callback, { timeout: 500 }));

// Call the debounced function multiple times within a short period
result.current('call 1');
result.current('call 2');
result.current('call 3');

vi.runAllTimers();

// Ensure that the callback has been called only once with the last argument
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith('call 3');
});

it('should not execute the function until timer exceeds timeout', () => {
const callback = vi.fn((_arg) => console.log());
const { result } = renderHook(() => useDebounce(callback, { timeout: 500 }));

result.current('call 1');

// advancing by 2ms won't trigger callback
vi.advanceTimersByTime(2);
expect(callback).not.toHaveBeenCalled();
// advancing by 500ms total will trigger callback
vi.advanceTimersByTime(498);
expect(callback).toHaveBeenCalledWith('call 1');
});
});
39 changes: 39 additions & 0 deletions src/__tests__/hooks/useKeyPress.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { vi } from 'vitest';
import { renderHook } from '@testing-library/react-hooks';
import useKeyPress from '../../hooks/useKeyPress';

describe('useKeyPress', () => {
it('should call the action function on Enter key press', () => {
const key = 'Enter';
const action = vi.fn();

renderHook(() => useKeyPress(key, action));
const event = new KeyboardEvent('keyup', { key });
window.dispatchEvent(event);

expect(action).toHaveBeenCalledTimes(1);
});

it('should not call the action function on Escape key press', () => {
const key = 'Enter';
const action = vi.fn();

renderHook(() => useKeyPress(key, action));
const event = new KeyboardEvent('keyup', { key: 'Escape' });
window.dispatchEvent(event);

expect(action).not.toHaveBeenCalled();
});

it('action is not triggered after hook is unmounted', () => {
const key = 'Enter';
const action = vi.fn();

const { unmount } = renderHook(() => useKeyPress(key, action));
unmount();
const event = new KeyboardEvent('keyup', { key });
window.dispatchEvent(event);

expect(action).not.toHaveBeenCalled();
});
});
64 changes: 64 additions & 0 deletions src/__tests__/hooks/useOnClickOutside.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { vi } from 'vitest';
import { renderHook } from '@testing-library/react-hooks';
import useOnClickOutside from '../../hooks/useOnClickOutside';

describe('useOnClickOutside', () => {
it('should call the handler when clicking outside the ref', () => {
const ref = { current: document.createElement('div') };
const handler = vi.fn();

renderHook(() => useOnClickOutside(ref, handler));

const outsideElement = document.createElement('div');
document.body.appendChild(outsideElement);

const event = new MouseEvent('mousedown', {
bubbles: true,
cancelable: true,
});
outsideElement.dispatchEvent(event);

expect(handler).toHaveBeenCalled();
});

it('should not call the handler when clicking inside the ref', () => {
const ref = { current: document.createElement('div') };
const handler = vi.fn();

renderHook(() => useOnClickOutside(ref, handler));

const insideElement = document.createElement('div');
ref.current.appendChild(insideElement);

const event = new MouseEvent('mousedown', {
bubbles: true,
cancelable: true,
});
insideElement.dispatchEvent(event);

expect(handler).not.toHaveBeenCalled();
});

it('should not call the handler after hook is unmounted', () => {
const ref = { current: document.createElement('div') };
const handler = vi.fn();

const { unmount } = renderHook(() => useOnClickOutside(ref, handler));

const outsideElement = document.createElement('div');
document.body.appendChild(outsideElement);

const event = new MouseEvent('mousedown', {
bubbles: true,
cancelable: true,
});

outsideElement.dispatchEvent(event);
expect(handler).toHaveBeenCalledTimes(1);

unmount();

outsideElement.dispatchEvent(event);
expect(handler).toHaveBeenCalledTimes(1);
});
});
17 changes: 17 additions & 0 deletions src/__tests__/hooks/usePortal.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { renderHook } from '@testing-library/react-hooks';
import usePortal from '../../hooks/usePortal';

describe('usePortal', () => {
it('inserts an empty div into DOM with correct ID', () => {
const sibling = document.createElement('section');
document.body.insertAdjacentElement('beforeend', sibling);

const { result } = renderHook(() => usePortal('usePortal-test'));
const portal = result.current;
const portalId = document.querySelector('#usePortal-test').id;

expect(portal).toBeInTheDocument();
expect(portal).toBeEmptyDOMElement();
expect(portalId).toBe(`usePortal-test`);
});
});
35 changes: 35 additions & 0 deletions src/__tests__/hooks/useWindowSize.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import useWindowSize from '../../hooks/useWindowSize';
import { vi } from 'vitest';
import { renderHook } from '@testing-library/react-hooks';

describe('useWindowSize', () => {
beforeEach(() => {
vi.stubGlobal('innerWidth', 1920);
vi.stubGlobal('innerHeight', 1080);
});

it('sets state to an initial window size', () => {
const { result } = renderHook(() => useWindowSize());

expect(result.current.width).toBe(1920);
expect(result.current.height).toBe(1080);

vi.unstubAllGlobals();
});

it('adjusts after window resize event', () => {
const { result } = renderHook(() => useWindowSize());

expect(result.current.width).toBe(1920);
expect(result.current.height).toBe(1080);

window.innerWidth = 1280;
window.innerHeight = 720;
window.dispatchEvent(new Event('resize'));

expect(result.current.width).toBe(1280);
expect(result.current.height).toBe(720);

vi.unstubAllGlobals();
});
});
39 changes: 39 additions & 0 deletions src/__tests__/util/downloadFile.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { vi } from 'vitest';
// import { screen } from '@testing-library/react';
import { downloadPdf } from '../../util/downloadFile'; // Import your utility functions here

describe('File Download Utility Functions', () => {
it('should download a PDF file', () => {
// Mock Blob and createObjectURL
window.Blob = vi.fn();
window.URL.createObjectURL = vi.fn(() => 'test-object-url');

const pdfData = 'Mock PDF Data';
const pdfFilename = 'sample.pdf';

// Mock the createElement and click methods for the link element
const createElementMock = vi.spyOn(document, 'createElement').mockImplementation(() => {
return {
href: '',
download: '',
click: vi.fn(),
remove: vi.fn(),
};
});

// const mockDownload = vi.fn().mockImplementation(downloadPdf);
// const fakeDl = mockDownload(pdfData, pdfFilename);
// console.log(fakeDl);
downloadPdf(pdfData, pdfFilename);

// Assertions
expect(window.Blob).toHaveBeenCalledWith([pdfData], { type: 'application/pdf' });
expect(window.URL.createObjectURL).toHaveBeenCalledWith(new Blob([pdfData], { type: 'application/pdf' }));

expect(createElementMock).toHaveBeenCalledWith('a');
// expect(createElementMock.download).toBe(pdfFilename);
// expect(createElementMock.href).toBe('test-object-url');

createElementMock.mockRestore();
});
});
46 changes: 46 additions & 0 deletions src/__tests__/util/errors.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { vi } from 'vitest';
import { getErrorList, hasValidationsErrors } from '../../util/errors';

describe('Utils: errors.js', () => {
afterEach(() => {
vi.resetAllMocks();
});

it('hasValidationsErrors returns true if errors exist', () => {
const hasValidationsErrorsSpy = vi.fn(hasValidationsErrors);
const mockErrors = {
errorOne: ['Error 1 message'],
errorTwo: ['Error 2 message'],
};
const hasErrors = hasValidationsErrorsSpy(mockErrors);
expect(hasErrors).toBe(true);
expect(hasValidationsErrorsSpy).toHaveBeenCalledTimes(1);
});

it('hasValidationsErrors returns false if no errors exist', () => {
const hasValidationsErrorsSpy = vi.fn(hasValidationsErrors);
const noErrors = hasValidationsErrorsSpy({});
expect(noErrors).toBe(false);
expect(hasValidationsErrorsSpy).toHaveBeenCalledTimes(1);
});

it('getErrorList returns a list of passed errors', () => {
const getErrorListSpy = vi.fn(getErrorList);
const mockErrors = {
errorOne: ['Error 1 message'],
errorTwo: ['Error 2 message'],
};
const expectedResult = [`ErrorOne: Error 1 message`, `ErrorTwo: Error 2 message`];
const result = getErrorListSpy(mockErrors);

expect(result).toEqual(expectedResult);
expect(getErrorListSpy).toHaveBeenCalledTimes(1);
});

it('getErrorList returns [] if no errors passed', () => {
const getErrorListSpy = vi.fn(getErrorList);
const noErrors = getErrorListSpy({});
expect(noErrors).toStrictEqual([]);
expect(getErrorListSpy).toHaveBeenCalledTimes(1);
});
});
37 changes: 37 additions & 0 deletions src/__tests__/util/isChrome.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { vi } from 'vitest';
import isChrome from '../../util/isChrome.js';

describe('isChrome test', () => {
beforeEach(() => {
vi.stubGlobal('chrome', null);
vi.stubGlobal('navigator', {
userAgent: '',
vendor: '',
});
});

afterEach(() => {
vi.clearAllMocks();
vi.resetAllMocks();
});

it('browser is Chrome, isChrome=true', async () => {
vi.stubGlobal('chrome', true);
vi.stubGlobal('navigator', {
userAgent: `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36`,
vendor: 'Google Inc.',
});

expect(isChrome()).toEqual(true);
});

it('browser is Mozilla, isChrome=false', async () => {
vi.stubGlobal('chrome', undefined);
vi.stubGlobal('navigator', {
userAgent: `Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0`,
vendor: '',
});

expect(isChrome()).toEqual(false);
});
});
Loading

0 comments on commit dae0d3c

Please sign in to comment.