Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Frontend Tests, hooks and utils #437

Merged
merged 67 commits into from
Nov 5, 2023
Merged
Show file tree
Hide file tree
Changes from 61 commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
77602a6
install vitest
austin-bagwell Aug 9, 2023
979283b
install jsdom
austin-bagwell Aug 9, 2023
57f531c
add test config object
austin-bagwell Aug 9, 2023
351e773
add tests folder
austin-bagwell Aug 9, 2023
f448bf2
errors - updated to use boilerplate vitest code
austin-bagwell Aug 9, 2023
45ba199
wrote test to test vitest functionality. is this recursive testing?
austin-bagwell Aug 10, 2023
e15f364
add testTimeout to test config
austin-bagwell Aug 10, 2023
7d86bd3
rename tests folder to __tests__
austin-bagwell Aug 10, 2023
f4234bb
remove extra 'tests' folder
austin-bagwell Aug 11, 2023
509be7f
basic tests for utils/errors.js
austin-bagwell Aug 12, 2023
c266b7a
create 'util' folder
austin-bagwell Aug 12, 2023
cdc36f8
add hasValidationsErrors test
austin-bagwell Aug 12, 2023
b5352b9
use vitest.spy for getErrorList - still fails test
austin-bagwell Aug 12, 2023
ec19ac0
attempt to render App inside a div container
austin-bagwell Aug 12, 2023
beaf354
add testing-library/react-hooks
austin-bagwell Aug 12, 2023
082b71b
add tests for useKeyPress
austin-bagwell Aug 12, 2023
a9f66cb
added testing-library/react-hooks
austin-bagwell Aug 12, 2023
63ff2ee
fixed getErrorList 'no error passed' test
austin-bagwell Aug 13, 2023
eb0ed8f
use vi.fn() to mock functions, passing all current tests
austin-bagwell Aug 13, 2023
b78e6fb
add basic vitest skeleton, not functional
austin-bagwell Aug 19, 2023
6417aac
hook sets state with initial window size
austin-bagwell Aug 23, 2023
75d023a
reword assertion
austin-bagwell Aug 23, 2023
7fcde98
render hook as result object - not functional yet
austin-bagwell Aug 24, 2023
935392e
put render into act() for app.test - not functional
austin-bagwell Aug 24, 2023
b99bd8f
Fix App test
robert-w-gries Sep 10, 2023
8fcc5f1
simulate clicks inside & outside element
austin-bagwell Sep 10, 2023
7026a0b
remove comments
austin-bagwell Sep 10, 2023
30ab241
Merge branch 'frontend_tests' into frontend_tests
austin-bagwell Sep 10, 2023
9bd8fe6
Merge pull request #3 from robert-w-gries/frontend_tests
austin-bagwell Sep 10, 2023
9249f44
merge rob's changes into frontend_tests
austin-bagwell Sep 10, 2023
e3a30d9
import render, waitFor from react testing lib
austin-bagwell Sep 10, 2023
c65c591
test mock userAgents
austin-bagwell Sep 16, 2023
cd8852e
add test that more closely mocks implementation
austin-bagwell Sep 16, 2023
82a04d5
reword assertion
austin-bagwell Sep 16, 2023
9394816
extract isChrome to function, test Mozilla userAgent, vendor
austin-bagwell Sep 16, 2023
36286d1
add chrome param to setWindowProps
austin-bagwell Sep 16, 2023
f88a929
add iPhone safari test, rm userAgent only tests
austin-bagwell Sep 16, 2023
a3ac796
update userAgent strings with MDN examples
austin-bagwell Sep 16, 2023
fd07355
update Chrome userAgent
austin-bagwell Sep 16, 2023
0d8cf4a
basic test outline - wip
austin-bagwell Sep 16, 2023
863515e
move setWindowProps into inner scope
austin-bagwell Sep 16, 2023
754c8cf
attempt to set multiple window properties
austin-bagwell Sep 16, 2023
293d4be
WIP - initial take on rendering hook
austin-bagwell Sep 20, 2023
109568f
check portal.id
austin-bagwell Sep 20, 2023
9020f70
add first successful debounce test
austin-bagwell Sep 24, 2023
da39c4f
add test where callback not called
austin-bagwell Sep 24, 2023
838f695
reword assertion
austin-bagwell Sep 24, 2023
6adb6d2
Merge branch 'master' into frontend_tests
austin-bagwell Sep 24, 2023
2e94714
remove test for useBrowserWarning
austin-bagwell Sep 26, 2023
aae1007
wip - assertions for window properties only
austin-bagwell Oct 1, 2023
c0fbd05
mock initial window size with vi.stubGlobal
austin-bagwell Oct 3, 2023
07769d8
add resize event test
austin-bagwell Oct 3, 2023
e64d303
action not triggered after hook unmounted
austin-bagwell Oct 5, 2023
40e3ac4
action not triggered after hook unmounted
austin-bagwell Oct 5, 2023
b0671a9
remove redundant assertion
austin-bagwell Oct 5, 2023
df06504
advance timer, add assertion that checks if callack is called
austin-bagwell Oct 5, 2023
1fca00d
reword test description
austin-bagwell Oct 5, 2023
9d9d5bb
add _arg to vi.fn()
austin-bagwell Oct 5, 2023
898b933
check that callback is called with specific arg
austin-bagwell Oct 5, 2023
65874d3
wip - using stubGlobal
austin-bagwell Oct 6, 2023
5e10d51
WIP - clean comments, test only Chrome, Mozilla
austin-bagwell Oct 8, 2023
9621ff3
Fix isChrome test
robert-w-gries Oct 10, 2023
194ab0b
npm run format
robert-w-gries Oct 11, 2023
43aec74
move initial window sizing to beforeEach
austin-bagwell Oct 10, 2023
4f2fba8
use outsideElement on hook unmount test
austin-bagwell Oct 14, 2023
9a63a2b
remove act(), make final assertions more comprehensive
austin-bagwell Oct 30, 2023
5f77ff9
remove redundant assertion
austin-bagwell Nov 5, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
austin-bagwell marked this conversation as resolved.
Show resolved Hide resolved
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();
});
});
66 changes: 66 additions & 0 deletions src/__tests__/hooks/useOnClickOutside.test.js
austin-bagwell marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { vi } from 'vitest';
import { act, 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));

act(() => {
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));

act(() => {
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));

act(() => {
const insideElement = document.createElement('div');
ref.current.appendChild(insideElement);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should use the outsideElement code here instead as the handler would only be called if the click was on an element outside the one hosting the hook.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushed through new changes, but I'm not 100% sure that my test is doing the right thing.

I changed this so that the event is dispatched to outerElement after unmounting the ref element hosting the hook, but for some reason it only works if I don't use act(). Keeping the both unmount() and outsideElement.dispatchEvent(event) inside the act() scope caused the test to fail, removing act results in a failed test with the same assertion.

I read the docs and this related post, but I'm still not 100% sure what is going on.

Am I actually testing the correct behavior this way?

@robert-w-gries

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AB - use RTL debug to check that all DOM elements are present. Can also get more specific with assertions to double check that correct behavior is being tested


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

expect(handler).not.toHaveBeenCalled();
});
});
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`);
});
});
36 changes: 36 additions & 0 deletions src/__tests__/hooks/useWindowSize.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import useWindowSize from '../../hooks/useWindowSize';
import { vi } from 'vitest';
import { renderHook } from '@testing-library/react-hooks';

describe('useWindowSize', () => {
it('sets state to an initial window size', () => {
vi.stubGlobal('innerWidth', 1920);
vi.stubGlobal('innerHeight', 1080);

const { result } = renderHook(() => useWindowSize());

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

vi.unstubAllGlobals();
});
robert-w-gries marked this conversation as resolved.
Show resolved Hide resolved

it('adjusts after window resize event', () => {
vi.stubGlobal('innerWidth', 1920);
vi.stubGlobal('innerHeight', 1080);

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);
});
});
38 changes: 38 additions & 0 deletions src/__tests__/util/isChrome.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { vi } from 'vitest';

describe('isChrome test', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.resetAllMocks();
});

afterEach(() => {
vi.stubGlobal('chrome', null);
vi.stubGlobal('navigator', {
userAgent: '',
vendor: '',
});
});

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.',
});

const result = (await import('../../util/isChrome.js')).default;
expect(result).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: '',
});

const result = (await import('../../util/isChrome.js')).default;
expect(result).toEqual(false);
});
});
Loading
Loading