Skip to content

Commit

Permalink
Forward dev;
Browse files Browse the repository at this point in the history
  • Loading branch information
anarqz committed Aug 15, 2020
1 parent 5d85188 commit 10668f1
Show file tree
Hide file tree
Showing 10 changed files with 1,819 additions and 61 deletions.
58 changes: 58 additions & 0 deletions components/Button/Button.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {
getByLabelText,
getByText,
getByTestId,
queryByTestId,
// Tip: all queries are also exposed on an object
// called "queries" which you could import here as well
waitFor,
} from '@testing-library/dom';
// adds special assertions like toHaveTextContent
import '@testing-library/jest-dom/extend-expect';

function getExampleDOM() {
// This is just a raw example of setting up some DOM
// that we can interact with. Swap this with your UI
// framework of choice 😉
const div = document.createElement('div');
div.innerHTML = `
<label for="username">Username</label>
<input id="username" />
<button>Print Username</button>
`;
const button = div.querySelector('button');
const input = div.querySelector('input');
button.addEventListener('click', () => {
// let's pretend this is making a server request, so it's async
// (you'd want to mock this imaginary request in your unit tests)...
setTimeout(() => {
const printedUsernameContainer = document.createElement('div');
printedUsernameContainer.innerHTML = `
<div data-testid="printed-username">${input.value}</div>
`;
div.appendChild(printedUsernameContainer);
}, Math.floor(Math.random() * 200));
});
return div;
}

test('examples of some things', async () => {
const famousWomanInHistory = 'Ada Lovelace';
const container = getExampleDOM();

// Get form elements by their label text.
// An error will be thrown if one cannot be found (accessibility FTW!)
const input = getByLabelText(container, 'Username');
input.value = famousWomanInHistory;

// Get elements by their text, just like a real user does.
getByText(container, 'Print Username').click();

await waitFor(() => expect(queryByTestId(container, 'printed-username')).toBeTruthy());

// getByTestId and queryByTestId are an escape hatch to get elements
// by a test id (could also attempt to get this element by its text)
expect(getByTestId(container, 'printed-username')).toHaveTextContent(famousWomanInHistory);
// jest snapshots work great with regular DOM nodes!
expect(container).toMatchSnapshot();
});
10 changes: 10 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
"build": "next build",
"start": "node index.js",
"prod": "NODE_ENV=production node index.js",
"test": "concurrently \"jest\"",
"test:ci": "CI=true concurrently \"jest\"",
"test:watch": "concurrently \"jest\"",
"type-check": "tsc",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --quiet",
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
Expand All @@ -14,6 +17,9 @@
"storybook": "start-storybook -p 6006 -s ./public",
"build-storybook": "build-storybook"
},
"jest": {
"verbose": true
},
"husky": {
"hooks": {
"pre-push": "yarn lint",
Expand All @@ -36,6 +42,7 @@
"express": "^4.17.1",
"immutable": "4.0.0-rc.12",
"isomorphic-unfetch": "3.0.0",
"jest": "^26.4.0",
"moment-timezone": "0.5.31",
"next": "9.4.4",
"next-i18next": "^4.4.2",
Expand All @@ -54,6 +61,8 @@
"@storybook/addon-links": "^6.0.5",
"@storybook/preset-typescript": "^3.0.0",
"@storybook/react": "^6.0.5",
"@testing-library/dom": "^7.22.2",
"@testing-library/react": "^10.4.8",
"@types/classnames": "^2.2.10",
"@types/node": "14.0.13",
"@types/react": "16.9.46",
Expand All @@ -63,6 +72,7 @@
"babel-loader": "^8.1.0",
"babel-preset-react-app": "^9.1.2",
"classnames": "^2.2.6",
"concurrently": "^5.3.0",
"eslint": "7.6.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-import": "^2.22.0",
Expand Down
25 changes: 25 additions & 0 deletions utils/hooks/useCombinedRefs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { useRef, useEffect, Ref } from 'react';

const useCombinedRefs = <T>(...refs: (Ref<T> | null)[]) => {
const targetRef = useRef<T>(null);

useEffect(() => {
refs.forEach(ref => {
if (!ref) return;

if (typeof ref === 'function') {
// @ts-ignore
ref(targetRef.current);
} else {
// @ts-ignore
// eslint-disable-next-line no-param-reassign
ref.current = targetRef.current;
}
});
}, [refs]);

return targetRef;
};

export default useCombinedRefs;
21 changes: 21 additions & 0 deletions utils/hooks/useDebounce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useEffect, useState } from 'react';

/**
* Debounce the change of given value
*
* @param value value changes will only be returned after timeout
* @param timeout timeout in ms passed to setTimeout()
*/
const useDebounce = <T>(value: T, timeout: number) => {
const [state, setState] = useState(value);

useEffect(() => {
const handler = setTimeout(() => setState(value), timeout);

return () => clearTimeout(handler);
}, [value]);

return state;
};

export default useDebounce;
30 changes: 30 additions & 0 deletions utils/hooks/useFocus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useState, useEffect, RefObject } from 'react';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const useFocus = <T extends Element | any>(ref: RefObject<T>) => {
const [value, setValue] = useState(false);

const handleFocus = () => setValue(true);
const handleBlur = () => setValue(false);

useEffect(
// eslint-disable-next-line consistent-return
() => {
const node = ref.current;
if (node) {
node.addEventListener('focus', handleFocus);
node.addEventListener('blur', handleBlur);

return () => {
node.removeEventListener('focus', handleFocus);
node.removeEventListener('blur', handleBlur);
};
}
},
[ref],
);

return value;
};

export default useFocus;
57 changes: 57 additions & 0 deletions utils/hooks/useHover.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React, { RefObject, FC, useRef } from 'react';
import { fireEvent } from '@testing-library/dom';
import { render, act } from '@testing-library/react';

import useHover from './useHover';

interface RenderHookProps {
callback: (isHovering: boolean, ref: RefObject<HTMLDivElement>) => void;
}

const RenderHook: FC<RenderHookProps> = ({ callback }) => {
const ref = useRef(null);
const isHovering = useHover(ref);

callback(isHovering, ref);

return <div ref={ref}>test</div>;
};

it('should update return value on hover', () => {
jest.useFakeTimers();

let hasHover = false;
let currentReference: RefObject<HTMLDivElement> | null = null;
render(
<RenderHook
callback={(isHovering, ref) => {
hasHover = isHovering;
currentReference = ref;
}}
/>,
);

expect(hasHover).toBe(false);

act(() => {
if (!currentReference || !currentReference.current) {
throw new Error('ref is null');
}
fireEvent.mouseOver(currentReference.current);
});

act(() => {
jest.runOnlyPendingTimers();
});

expect(hasHover).toBe(true);

act(() => {
if (!currentReference || !currentReference.current) {
throw new Error('ref is null');
}
fireEvent.mouseOut(currentReference.current);
});

expect(hasHover).toBe(false);
});
30 changes: 30 additions & 0 deletions utils/hooks/useHover.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useState, useEffect, RefObject } from 'react';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const useHover = <T extends Element | any>(ref: RefObject<T>): boolean => {
const [value, setValue] = useState(false);

const handleMouseOver = () => setValue(true);
const handleMouseOut = () => setValue(false);

useEffect(
// eslint-disable-next-line consistent-return
() => {
const node = ref.current;
if (node) {
node.addEventListener('mouseover', handleMouseOver);
node.addEventListener('mouseout', handleMouseOut);

return () => {
node.removeEventListener('mouseover', handleMouseOver);
node.removeEventListener('mouseout', handleMouseOut);
};
}
},
[ref],
);

return value;
};

export default useHover;
38 changes: 38 additions & 0 deletions utils/hooks/useKeyPress.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { useState, useEffect, useCallback } from 'react';
import { Key } from 'ts-key-enum';

const useKeyPress = (targetKey: Key | string | number): boolean => {
const [keyPressed, setKeyPressed] = useState(false);

const downHandler = useCallback(
({ key }: KeyboardEvent) => {
if (key === targetKey) {
setKeyPressed(true);
}
},
[targetKey],
);

const upHandler = useCallback(
({ key }: KeyboardEvent) => {
if (key === targetKey) {
setKeyPressed(false);
}
},
[targetKey],
);

useEffect(() => {
window.addEventListener('keydown', downHandler);
window.addEventListener('keyup', upHandler);

return () => {
window.removeEventListener('keydown', downHandler);
window.removeEventListener('keyup', upHandler);
};
}, [downHandler, upHandler]);

return keyPressed;
};

export default useKeyPress;
13 changes: 13 additions & 0 deletions utils/hooks/usePrevious.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useRef, useEffect } from 'react';

const usePrevious = <T>(value: T) => {
const ref = useRef<T>();

useEffect(() => {
ref.current = value;
});

return ref.current;
};

export default usePrevious;
Loading

0 comments on commit 10668f1

Please sign in to comment.